多线程和并发编程(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();
    }
}

使用两阶段终止线程可以使用 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());
            }
        }));

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

避免共享

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

不可变模式Immutable

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

多线程和并发编程(6)—并发编程的设计模式_第1张图片

写时复制 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 模式类比的是工厂里车间工人的工作模式。但其实在现实世界,工厂里还有一种流水线的工作模式,类比到编程领域,就是生产者 - 消费者模式。

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

多线程和并发编程(6)—并发编程的设计模式_第2张图片

未来模式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 发布!

你可能感兴趣的:(并发编程)