• 多线程和并发编程(6)—并发编程的设计模式


    优雅终止

    如何优雅终止线程?

    中断线程的思路是使用两阶段法:第一阶段发生中断请求,第二阶段根据中断标识结束线程;

    public class Test1 {
        private volatile static boolean interrupted = false;
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!interrupted) {
                        try {
                            Thread.sleep(1000);
                            System.out.println(Thread.currentThread().getName() + " waiting");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            interrupted = true;
                        }
    //                    System.out.println(Thread.currentThread().getName());
                    }
                }
            });
            thread.start();
            Thread.sleep(5000);
            thread.interrupt();
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    使用两阶段终止线程可以使用 thread.interrupt()方法将运行中线程置为中断状态,或者让等待状态的线程抛出InterruptedException异常,再通过判断中断状态来实现线程退出。

    如何优雅终止线程池

    停止线程池的方法有3中,包括使用shutdown()方法、使用shutdownNow()方法和使用JVM停服钩子函数

    1. shutdown()方法会停止线程池接受新的任务,并等待线程池中的所有任务执行完毕,然后关闭线程池。在调用shutdown()方法后,线程池不再接受新的任务,但是会将任务队列中的任务继续执行直到队列为空。如果线程池中的任务正在执行,但是还没有执行完毕,线程池会等待所有任务执行完毕后再关闭线程池。
    2. shutdownNow()方法会停止线程池接受新的任务,并尝试中断正在执行任务的线程,然后关闭线程池。在调用shutdownNow()方法后,线程池不再接受新的任务,同时会中断正在执行任务的线程并返回一个未执行的任务列表。该方法会调用每个任务的interrupt()方法尝试中断任务执行的线程,但是并不能保证线程一定会被中断,因为线程可以选择忽略中断请求。
    3. 使用JVM提供的停服钩子函数来实现优雅停机,当停服时就会执行addShutdownHook()方法中的线程逻辑。
            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        executor.awaitTermination(5,TimeUnit.MINUTES);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());
                }
            }));
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    总的来说,使用shutdown()方法停止线程池会等线程池中已经执行的任务完成后再销毁线程池,shutdownNow()方法会调用运行中线程的interrupt()方法尝试中断任务,但是否能真的停止无法保障,使用JVM停服钩子函数可以使用awaitTermination(5,TimeUnit.MINUTES)方法,等待固定时间让线程执行完,过了时间后再销毁。

    避免共享

    出现线程并发问题的条件是多个线程同时存在读写操作,所以一种思路是避免存在同时写的操作,甚至不能对共享变量进行写操作。

    不可变模式Immutable

    声明一个变量,让其不能进行写的操作,只能进行读的操作。这可以理解为一种设计模式:不可变模式。在Java中的实现可以是声明一个变量为final,这个变量赋值后就不能再被修改。可以使用类似guava中的ImmutableCollection的集合类型来实现。

    image-20230926002124167

    写时复制 CopyOnWrite

    在Java中,CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个 Copy-on-Write 容器,它们背后的设计思想就是 Copy-on-Write;通过 Copy-on-Write 这两个容器实现的读操作是无锁的,由于无锁,所以将读操作的性能发挥到了极致。

    Copy-on-Write模式适合读多写少的场景,他的实现思路是在需要进行写操作时候,会复制一个副本,在副本中进行写的操作,在写完之后再合并到原来的变量中。该种设计模式的问题在于每次都需要复制到新的内存中,所以会比较消耗内存。

    线程本地变量ThreadLocal

    线程本地存储模式用于解决多线程环境下的数据共享和数据隔离问题。该模式的基本思想是为每个线程创建独立的存储空间,用于存储线程私有的数据。通过这种方式,可以保证线程之间的数据隔离和互不干扰。在 Java 标准类库中,ThreadLocal 类实现了该模式。

    线程本地存储模式本质上是一种避免共享的方案,由于没有共享,所以自然也就没有并发问题。如果你需要在并发场景中使用一个线程不安全的工具类,最简单的方案就是避免共享。避免共享有两种方案,一种方案是将这个工具类作为局部变量使用,另外一种方案就是线程本地存储模式。这两种方案,局部变量方案的缺点是在高并发场景下会频繁创建对象,而线程本地存储方案,每个线程只需要创建一个工具类的实例,所以不存在频繁创建对象的问题。

    多线程版本的if模式

    守护阻塞模式Guarded Suspension

    Guarded Suspension 模式是通过让线程等待来保护实例的安全性,即守护-挂起模式。在多线程开发中,常常为了提高应用程序的并发性,会将一个任务分解为多个子任务交给多个线程并行执行,而多个线程之间相互协作时,仍然会存在一个线程需要等待另外的线程完成后继续下一步操作。而Guarded Suspension模式可以帮助我们解决上述的等待问题。

    在Java中的实现包括:

    1. sychronized+wait/notify/notifyAll
    2. reentrantLock+Condition(await/singal/singalAll)
    3. cas+park/unpark

    守护中断模式Balking

    Balking是“退缩不前”的意思。如果现在不适合执行这个操作,或者没必要执行这个操作,就停止处理,直接返回。当流程的执行顺序依赖于某个共享变量的场景,可以归纳为多线程if模式。Balking 模式常用于一个线程发现另一个线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回。

    Balking模式和Guarded Suspension模式一样,存在守护条件,如果守护条件不满足,则中断处理;这与Guarded Suspension模式不同,Guarded Suspension模式在守护条件不满足的时候会一直等待至可以运行。

    常见的使用场景有:

    1. 单例的DCL模式;
    2. 组件服务的初始化操作;

    多线程分工模式

    *消息单线程模式Thread-Per-Message *

    为每一个处理任务分配一个线程。Thread-Per-Message 模式作为一种最简单的分工方案,Java 中使用会存在性能缺陷。在 Java 中的线程是一个重量级的对象,创建成本很高,一方面创建线程比较耗时,另一方面线程占用的内存也比较大。所以为每个请求创建一个新的线程并不适合高并发场景。为了解决这个缺点,Java 并发包里提供了线程池等工具类。

    工作线程模式Worker Thread

    要想有效避免线程的频繁创建、销毁以及 OOM 问题,就不得不提 Java 领域使用最多的 Worker Thread 模式。Worker Thread 模式可以类比现实世界里车间的工作模式:车间里的工人,有活儿了,大家一起干,没活儿了就聊聊天等着。Worker Thread 模式中 Worker Thread 对应到现实世界里,其实指的就是车间里的工人

    Worker Thread 模式能避免线程频繁创建、销毁的问题,而且能够限制线程的最大数量。Java 语言里可以直接使用线程池来实现 Worker Thread 模式,线程池是一个非常基础和优秀的工具类,甚至有些大厂的编码规范都不允许用 new Thread() 来创建线程,必须使用线程池。

    异步模式

    对于共享资源的操作分为生产和消费两端,可以采用生产者-消费者模式,实现两种操作的解耦。

    生产者-消费者模式Producer-Consumer

    Worker Thread 模式类比的是工厂里车间工人的工作模式。但其实在现实世界,工厂里还有一种流水线的工作模式,类比到编程领域,就是生产者 - 消费者模式。

    生产者 - 消费者模式的核心是一个任务队列,生产者线程生产任务,并将任务添加到任务队列中,而消费者线程从任务队列中获取任务并执行。

    image

    未来模式Future

    Future未来模式是一种异步处理的模式,就是在针对处理事件比较长的任务时,创建一个异步线程来处理任务,返回一个Future的引用,等到后面必须要要结果的时候,可以通过Future的引用来获取异步线程处理的结果。这样可以提高程序的吞吐量,减少用户等待时间。

    参考资料

    JAVA并发编程知识总结(全是干货超详细):https://zhuanlan.zhihu.com/p/362843892

    Java 多线程模式 —— Guarded Suspension 模式:https://cloud.tencent.com/developer/article/2003740

    15-并发设计模式:https://www.cnblogs.com/lusaisai/p/15983313.html (主要参考)

    本文由博客一文多发平台 OpenWrite 发布!

  • 相关阅读:
    Ubuntu 发布 qt 程序(c++)
    4_1 数据库系统
    吐血整理的大数据学习资源大全
    pycharm爬虫模块(scrapy)基础使用
    网络七层结构(讲人话)
    M2DGR 多源多场景 地面机器人SLAM数据集
    【花雕动手做】有趣好玩的音乐可视化系列小项目(20)--首饰盒镜子灯
    2023版 STM32实战1 LED灯驱动(电路与代码都讲解)
    3.4 设置环境变量MAKEFILES
    Webrtc支持FFMPEG硬解码之Intel(一)
  • 原文地址:https://blog.csdn.net/ynkimage/article/details/133327465