多线程编程的实战技巧

最近面试某大厂,考了一道题:三个线程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() 的时候,参数顺序一致,都是可以的。

生活小记

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

贴一张实习公司的图吧:

相关推荐
undeflined14 分钟前
vite + vue3 + tailwind 启动之后报错
开发语言·后端·rust
猿来入此小猿39 分钟前
基于SpringBoot在线音乐系统平台功能实现十七
java·spring boot·后端·毕业设计·音乐系统·音乐平台·毕业源码
重整旗鼓~1 小时前
2.flask中使用装饰器统一验证用户登录
后端·python·flask
it噩梦2 小时前
springboot 工程使用proguard混淆
java·spring boot·后端
从种子到参天大树2 小时前
SpringBoot源码阅读系列(二):自动配置原理深度解析
后端
狠难说2 小时前
SpringCloud(八) - 自定义token令牌,鉴权(注解+拦截器),参数解析(注解+解析器)
后端
从种子到参天大树2 小时前
SpringBoot源码阅读系列(一):启动流程概述
后端
m0_748254883 小时前
Spring Boot实现多数据源连接和切换
spring boot·后端·oracle
庄周de蝴蝶3 小时前
一次 MySQL IF 函数的误用导致的生产小事故
后端·mysql
韩数3 小时前
Nping: 支持图表实时展示的多地址并发终端命令行 Ping
后端·rust·github