多线程编程的实战技巧

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

生活小记

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

贴一张实习公司的图吧:

相关推荐
丁卯40420 分钟前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
bing_1584 小时前
简单工厂模式 (Simple Factory Pattern) 在Spring Boot 中的应用
spring boot·后端·简单工厂模式
天上掉下来个程小白4 小时前
案例-14.文件上传-简介
数据库·spring boot·后端·mybatis·状态模式
Asthenia04125 小时前
基于Jackson注解的JSON工具封装与Redis集成实战
后端
编程星空5 小时前
css主题色修改后会多出一个css吗?css怎么定义变量?
开发语言·后端·rust
程序员侠客行6 小时前
Spring事务原理 二
java·后端·spring
dmy6 小时前
docker 快速构建开发环境
后端·docker·容器
sjsjsbbsbsn7 小时前
Spring Boot定时任务原理
java·spring boot·后端
计算机毕设指导67 小时前
基于Springboot学生宿舍水电信息管理系统【附源码】
java·spring boot·后端·mysql·spring·tomcat·maven
计算机-秋大田7 小时前
基于Spring Boot的兴顺物流管理系统设计与实现(LW+源码+讲解)
java·vue.js·spring boot·后端·spring·课程设计