多线程编程的实战技巧

很多同学都会在简历上写自己熟悉并发编程,那么就会衍生一道很经典的题:三个线程 A,B,C,按序来执行 N 次并且打印输出 A B C......;很明显,这就是一个多线程的编程题,挺有意思,借此,我想来写一篇文章来记录一下这个题的写法,更多的在于使用上而不是原理。

前言

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

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

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

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

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

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

实践

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

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

线程操纵资源类:

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

 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 亦先如此写着;后面的业务的主要逻辑后面再去实现。

 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

 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 为例子:

 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 也是这样的一个方法,你自己去试试?哈哈。至此上面的业务逻辑已经结束,我们再来一个测试用例验证一下:

 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);然后我们生产者和消费者就是线程来的;当然,这里我觉得不应该像上面一样,将生产者和消费者作为一个对象线程(这词也许不恰当,但就是不想作为一个类)来定义,因为生产者和消费者个数可以很多,但是队列的个数只有一个,且不论是生产者还是消费者,其实核心都是围绕着队列进行操作的,所以我们应该在队列中进行定义生产和消费的逻辑。

 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();
     }
 }

测试类:

 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();
     }
     }
 }

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

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

资源类:

 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;
     }
 }

线程类:

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());
            }
        }
    }
}

测试类:

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() 的时候,参数顺序一致,都是可以的。

#最后再改一次简历##简历被挂麻了,求建议##牛客在线求职答疑中心##我的实习求职记录##实习,投递多份简历没人回复怎么办#
Java 实用知识 文章被收录于专栏

该栏目主要是分享一些实用的编码技巧、编程知识以及个人认为有大用的八股知识。

全部评论
以后打算开个专栏(免费),专门收录一些有意思的八股,我会给他起个名叫“大白话八股”,主要内容是想写一些比较有用的八股,尽量做到一篇文章从三个角度出发:“应用”--- “原理” --- “拓展”,欢迎点赞关注送花花给我呀~ 尽量做到周更三两篇,还是想保证好的文章质量。
1 回复 分享
发布于 07-03 16:03 广东
多线程编程的实战技巧主要包括线程操纵资源类、避免虚假唤醒、使用锁和条件变量等工具。以下是一个具体的例子: 1. 定义一个资源类Source,包含一个锁lock、三个条件变量conditionA、conditionB、conditionC和一个状态变量state。 ```java class Source { Lock lock = new ReentrantLock(); Condition conditionA = lock.newCondition(); Condition conditionB = lock.newCondition(); Condition conditionC = lock.newCondition(); int state = 1; void printA() { System.out.println("A"); } void printB() { System.out.println("B"); } void printC() { System.out.println("C"); } } ``` 2. 定义三个线程类A、B、C,每个线程都包含一个Source对象,并实现run方法。在run方法中,线程首先获取锁,然后判断状态是否等于自己的序号,如果不是,则等待。如果是,则执行打印操作,并将状态设置为下一个线程的序号,最后通知下一个线程。 ```java class A implements Runnable { private Source source; public A(Source source) { this.source = source; } @Override public void run() { source.lock.lock(); try { while (source.state != 1) { source.conditionA.await(); } source.printA(); source.state = 2; source.conditionB.signal(); } catch (Exception e) { e.printStackTrace(); } finally { source.lock.unlock(); } } } ``` 3. 在主函数中,创建Source对象和三个线程,并启动线程。 ```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(
点赞 回复 分享
发布于 07-03 16:09 AI生成

相关推荐

8 37 评论
分享
牛客网
牛客企业服务