多线程编程的实战技巧

最近面试某大厂,考了一道题:三个线程A,B,C,按序来执行 100 次并且打印输出A B C......;很明显,这就是一个多线程的编程题,挺有意思,借此,我想来写一篇文章来记录一下我对多线程编程的学习以及总结,更多的在于使用上而不是原理。

前言

在进行多线程编程之前:我们需要有一些多线程编程的共识,对于这类控制多线程执行 的题目,我们需要有一个方法论

  • 线程操纵资源类
  • 在写业务逻辑的时候遵循这么一个流程:判断 -- 执行 -- 通知
  • 避免虚假唤醒,用 while 而不用 if 来进行判断。

上面说的三个点是基本的框架逻辑,任何控制多线程执行的题目都是遵循这个框架的,然后,对于多线程编程来说,有哪些工具或者类可以使用呢?最熟悉莫过于 synchronized 以及 lock;那好,这里又有一个方法论

  • synchronized -- wait() -- notify()
  • lock -- await() -- signal()

一般来说,我更喜欢用 lock ,因为这是一个 API 层面的工具,能提供更多更便利的方法,况且,lock 的那个组合可以实现精准通知!当然你还可以利用其他的工具类来实现,那也是可以的。

至此,控制类多线程编程的基本知识就此结束。

实践

就拿上面的题目来说,我们在思考这些题目如何解答的时候,应该将相关逻辑梳理出来:三个线程执行任务并且要按序执行;

基本逻辑就是如此,所以我们可以开干,将该题目套到上面说的方法论上。

线程操纵资源类:

首先我们需要定义一个资源类来给线程进行操作的呀,且资源类中可以定义对应的方法,让线程直接进行调用就好了(当然,线程自己也是可以在各自的线程里面输出对应的字符)。

java 复制代码
 class Source {
     void printA() {
         System.out.println("A");
     }
      void printB() {
         System.out.println("B");
     }
     void printC() {
         System.out.println("C");
     }
 }

三个线程 A B C:其中线程的逻辑就是进行操纵资源类,所以所需要将 Source 注入进去;然后 B 和 C 亦先如此写着;后面的业务的主要逻辑后面再去实现。

java 复制代码
 class A implements Runnable {
     private Source source;
 ​
     public A(Source source) {
         this.source = source;
     }
 ​
     @Override
     public void run() {
     }
 }
 ​
 class B implements Runnable {
     private Source source;
 ​
     public A(Source source) {
         this.source = source;
     }
 ​
     @Override
     public void run() {
     }
 }
 ​
 class C implements Runnable {
     private Source source;
 ​
     public A(Source source) {
         this.source = source;
     }
 ​
     @Override
     public void run() {
     }
 }

好了,至此整个题目的大致框架都几乎完成,那么我们就需要思考,按序执行和打印输出该如何实现呢?

所以现在的难题变成:如何实现控制线程的执行顺序?

上面我们说过,要使用 lock 那个组合,但是问题又来了:各个线程都是独立的呀,我如何将他们进行联合控制呢?都是独立的那就很不好进行控制了么。所以我们可以看看他们是否有一个共同之处:对了!就是都有一个共同的变量 Source 资源类!这是一个很好的突破口。所以,我们改进我们的资源类 Source

java 复制代码
 class Source {
     Lock lock = new ReentrantLock();
     // 每一个 condition 都是用来控制自己线程的唤醒和阻塞行为的
     Condition conditionA = lock.newCondition();
     Condition conditionB = lock.newCondition();
     Condition conditionC = lock.newCondition();
     // 1代表线程A执行;2代表线程B执行;3代表线程C执行
     int state = 1;
     
     void printA() {
         System.out.println("A");
     }
      void printB() {
         System.out.println("B");
     }
     void printC() {
         System.out.println("C");
     }
 }

那好,真正执行控制线程输出以及打印的逻辑来了!!!用线程A 为例子:

java 复制代码
 class A implements Runnable {
     private Source source;
 ​
     public A(Source source) {
         this.source = source;
     }
 ​
     @Override
     public void run() {
         source.lock.lock();
         try {
             // 这里就是【判断】逻辑,必须要用 while,避免虚假唤醒
             while (source.state != 1) {
                 source.conditionA.await();
             }   
             // 【执行】
             // 当然这里可以不使用资源类的,可以自己在这里输出打印逻辑
             source.printA();
             // 【通知】
             source.state = 2;
             source.conditionB.signal();
         } catch (Excepition e) {
             e.printStackTrace();
         } finally {
             source.lock.unlock();
         }
     }
 }

线程B和C 也是这样的一个方法,你自己去试试?哈哈。至此上面的业务逻辑已经结束,我们再来一个测试用例验证一下:

Java 复制代码
 class Test {
     public static void main(String[] args) throws InterruptedException {
         Source source = new Source();
         for (int i = 0; i < 100; i++) {
             new Thread(new A(source)).start();
             new Thread(new B(source)).start();
             new Thread(new C(source)).start();
         }
         Thread.sleep(10000);
         System.out.println("执行完毕~");
     } 
 }

对于一些常考的多线程编程的思考

我相信,基于上面的一道题目的讲解,你应该有所了解控制多线程类的题目应该怎么做了吧,意犹未尽?那行吧,你来尝试一下这些题目:

  • 三个线程按序打印ABC
  • 两个线程,一个打印奇数,一个打印偶数
  • 三个线程ABC,线程A 输出 "A" 5次,线程B 输出 "B" 10次,线程C 输出 "C" 15次
  • 生产者消费者模型

对于生产者消费者模型,我想写写 demo 以及我的思考:

首先,我们要抽象出最简单的模型出来,一个队列 Queue(当然可以自己去实现,也可以使用 JavaAPI);然后我们生产者和消费者就是线程来的;当然,这里我觉得不应该像上面一样,将生产者和消费者作为一个对象线程(这词也许不恰当,但就是不想作为一个类)来定义,因为生产者和消费者个数可以很多,但是队列的个数只有一个,且不论是生产者还是消费者,其实核心都是围绕着队列进行操作的,所以我们应该在队列中进行定义生产和消费的逻辑。

java 复制代码
 public class MyQueue {
     private final Queue<Integer> queue = new LinkedBlockingQueue<>(10);
     Lock lock = new ReentrantLock();
     /**
      * 从消费队列的入口和出口进行限制,这思路才是正确的
      */
     Condition producer = lock.newCondition();
     Condition consumer = lock.newCondition();
 ​
     void setElement(Integer element) {
         lock.lock();
         try {
             // 阻塞队列的元素个数大于10,那么生产者就不要生产了
             while (size > 10) {
                 producer.await();
             }
             queue.add(element);
             size++;
             consumer.signal();
         } catch (Exception e) {
             System.out.println(e.getMessage());
         } finally {
             lock.unlock();
         }
     }
 ​
    
     Integer getElement() {
         lock.lock();
         try {
             // 队列为空,消费者就别消费了
             while (queue.isEmpty()) {
                 consumer.await();
             }
             producer.signal();
         } catch (Exception e) {
             System.out.println(e.getMessage());
         } finally {
             lock.unlock();
         }
         return queue.poll();
     }
 }

测试类:

java 复制代码
 class Test {
     public static void main(args[] String) {
          MyQueue myQueue = new MyQueue();
         Random random = new Random();
         // 生产者
         new Thread(() -> {
             while (true) {
                 try {
                     Source source = new Source(random.nextInt(100) + 1);
                     System.out.println("往队列中添加元素++++++++++" + source);
                     myQueue.setElement(source.getNumber());
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     throw new RuntimeException(e);
                 }
             }
         }).start();
 ​
         // 消费者
         new Thread(() -> {
             while (true) {
                 try {
                     System.out.println("从队列中获取元素:" + myQueue.getElement());
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     throw new RuntimeException(e);
                 }
             }
         }).start();
     }
     }
 }

至此,控制线程类型的题目结束;下面再插播一个多线程编程的案例:死锁

死锁的定义(个人理解):多个线程都持有自己独立且不共享的锁(变量),然后相互依赖对方的锁,并且彼此等待对方释放自己的锁从而循环等待,最终形成死锁。

资源类:

java 复制代码
 class LockDemo {
     private String  name;
 ​
     public LockDemo(String name) {
         this.name = name;
     }
 ​
     public String getName() {
         return name;
     }
 ​
     public void setName(String name) {
         this.name = name;
     }
 }

线程类:

java 复制代码
public class ThreadDemo implements Runnable {
    /**
     * 死锁的构建重大原因:资源独享非共享;多线程抢夺;循环等待;线程A已经拥有了一个线程B所需要的资源,线程B拥有了线程A所需要的资源
     */
    private final LockDemo lock1;
    private final LockDemo lock2;

    public ThreadDemo(LockDemo lock1, LockDemo lock2) {
        this.lock1 = lock1;
        this.lock2 = lock2;
    }

    @Override
    public void run() {
        synchronized (lock1) {
            System.out.println(Thread.currentThread().getName() + "获取到" + lock1.getName() + ",正准备获取" + lock2.getName());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + "获取到" + lock2.getName());
            }
        }
    }
}

测试类:

java 复制代码
public class DeadLockTest {
    public static void main(String[] args) {
        LockDemo lock1 = new LockDemo("lock1");
        LockDemo lock2 = new LockDemo("lock2");

        new Thread(new ThreadDemo(lock1, lock2)).start();
        new Thread(new ThreadDemo(lock2, lock1)).start();
    }
}

一运行,死锁就形成了,那么如何解决?打破其中一个规则都是可以的,譬如:将 ThreadDemo 中的资源 LockDemo 对象用 static 修饰;或者说,你线程 new ThreadDemo() 的时候,参数顺序一致,都是可以的。

生活小记

今天周六,想写点东西了。在北京实习快两个月了,从一开始的不习惯到现在的被迫习惯。当初来北京这边的实习,也当作是一场体验,目前来说,还算是有收获,不论是在工作上,个人学习上,以及见识上;回想着当初自己在秋招的时候困境,现在想想还是很值得回忆的,毕竟结果不算很差;当然我还需要继续努力,我觉得我在自己的发展上,还可以更好充分利用自己的价值嘛,所以我是打算春招的。接下来的日子,我大概率会更多的输出类似的文章,因为八股文说实话,写的意义不是很大了哈哈,毕竟各大论坛这么多的八股,怎么也轮不到我来凑字数了哈哈,所以我打算写一些我在学习过程中,更多偏向于如何使用的场景,就是将一个知识点能变成一个实战的场景,那样,即使你在面试的时候吹,也有的吹嘛,结合场景业务来吹的八股,才是真的神!

贴一张实习公司的图吧:

相关推荐
摇滚侠2 小时前
Spring Boot 3零基础教程,IOC容器中组件的注册,笔记08
spring boot·笔记·后端
程序员小凯5 小时前
Spring Boot测试框架详解
java·spring boot·后端
你的人类朋友5 小时前
什么是断言?
前端·后端·安全
程序员小凯6 小时前
Spring Boot缓存机制详解
spring boot·后端·缓存
i学长的猫7 小时前
Ruby on Rails 从0 开始入门到进阶到高级 - 10分钟速通版
后端·ruby on rails·ruby
用户21411832636027 小时前
别再为 Claude 付费!Codex + 免费模型 + cc-switch,多场景 AI 编程全搞定
后端
茯苓gao7 小时前
Django网站开发记录(一)配置Mniconda,Python虚拟环境,配置Django
后端·python·django
Cherry Zack7 小时前
Django视图进阶:快捷函数、装饰器与请求响应
后端·python·django
爱读源码的大都督8 小时前
为什么有了HTTP,还需要gPRC?
java·后端·架构
码事漫谈8 小时前
致软件新手的第一个项目指南:阶段、文档与破局之道
后端