一、终止线程模式
问题:终止线程正确方式???
答复:使用java的中断机制,设置中断标志。使用中需要注意的有些api会清除中断标识,需要重新设置。
Two-phase Termination(两阶段终止)模式——优雅的终止线程
终止过程分成两个阶段,
第一阶段主要是线程 T1 向线程 T2发送终止指令,
第二阶段则是线程 T2响应终止指令。
Java 线程进入终止状态的前提是线程处于 RUNNABLE状态,而利用java线程中断机制的interrupt() 方法,可以让线程从休眠状态转换到RUNNABLE 状态。RUNNABLE 状态转换到终止状态。
优雅的方式是让 Java 线程自己执行完 run() 方法,所以一般我们采用的方法是设置一个标志位,然后线程会在合适的时机检查这个标志位,如果发现符合终止条件,则自动退出 run() 方法。
两阶段终止模式是一种应用很广泛的并发设计模式,在 Java 语言中使用两阶段终止模式需要注意两个关键点:
一是仅检查终止标志位是不够的,因为线程的状态可能处于休眠态;
二是仅检查线程的中断状态也是不够的,因为我们依赖的第三方类库很可能没有正确处理中断异常,
例如第三方类库在捕获到 Thread.sleep() 方法抛出的中断异常后,没有重新设置线程的中断状态,那么就会导致线程不能够正常终止。所以我们可以自定义线程的终止标志位用于终止线程。
常用错误方式
方式一:
使用线程对象的 stop() 方法停止线程。
引发问题:stop 方法会真正杀死线程,但是如果当前线程锁住了共享资源,当它被杀死后就再也没有机会释放锁, 其它线程将永远无法获取锁
方式二:
使用 System.exit(int) 方法停止线程
引发问题:这种做法会让整个程序都停止,而不是仅停止一个线程
二、防共享模式
不变性模式
对象一旦被创建之后,状态就不再发生变化。换句话说,就是变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性。
实现方式
将一个类所有的属性都设置成 final 的,并且只允许存在只读方法,则类基本上就具备了不可变性。更严格的做法是这个类本身也是 final 的,也就是不允许继承。
jdk中很多类都具备不可变性,例如经常用到的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。
它们都严格遵守了不可变类的三点要求:类和属性都是 final 的,所有方法均是只读的。
Copy-on-Write模式
解决不可变对象的修改问题时经常用到。所谓 Copy-on-Write,经常被缩写为 COW 或者 CoW,顾名思义就是写时复制。不可变对象的写操作往往都是使用 Copy-on-Write 方法解决的,
例如:Java 里 String 在实现 replace() 方法的时候,并没有更改原字符串里面 value[]数组的内容,而是创建了一个新字符串。它本质上是一种 Copy-on-Write 方法。
Copy-on-Write 才是最简单的并发解决方案, Java 中的基本数据类型 String、Integer、Long 等都是基于 Copy-on-Write 方案实现的。
缺点就是消耗内存,每次修改都需要复制一个新的对象出来,不过随着自动垃圾回收(GC)算法的成熟以及硬件的发展,这种内存消耗已经渐渐可以接受了。适合读多写少的场景。
应用场景
CopyOnWriteArrayList 和 CopyOnWriteArraySet 它们背后的设计思想就是 Copy-on-Write;通过 Copy-on-Write 这两个容器实现的读操作是无锁的,由于无锁,所以将读操作的性能发挥到了极致。
Copy-on-Write 在操作系统领域也有广泛的应用。Linux 中fork() 子进程的时候,并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间;只有在父进程或者子进程需要写入的时候才会复制地址空间,从而使父子进程拥有各自的地址空间。
Copy-on-Write 最大的应用领域还是在函数式编程领域。函数式编程的基础是不可变性(Immutability),所以函数式编程里面所有的修改操作都需要 Copy-on-Write 来解决。
像一些RPC框架还有服务注册中心,也会利用Copy-on-Write设计思想维护服务路由表。路由表是典型的读多写少,而且路由表对数据的一致性要求并不高,一个服务提供方从上线到反馈到客户端的路由表里,即便有 5 秒钟延迟,很多时候也都是能接受的。
Thread-Specific Storage (线程本地存储)模式
没有共享就没有伤害
Thread-Specific Storage(线程本地存储) 模式是在内部为每个线程分配特有的存储空间的模式。在 Java 标准类库中,ThreadLocal 类实现了该模式。
线程本地存储模式本质上是一种避免共享的方案,由于没有共享,所以自然也就没有并发问题。如果你需要在并发场景中使用一个线程不安全的工具类,最简单的方案就是避免共享。避免共享有两种方案,一种方案是将这个工具类作为局部变量使用,另外一种方案就是线程本地存储模式。
局部变量方案的缺点是在高并发场景下会频繁创建对象,而线程本地存储方案,每个线程只需要创建一个工具类的实例,所以不存在频繁创建对象的问题(ThreadLocal需要手动清理。调用remove方法)。
应用场景
SimpleDateFormat 不是线程安全的,那如果需要在并发场景下使用它,有一个办法就是用 ThreadLocal 来解决。
三、多线程条件模式
守护-挂起模式。在多线程开发中,常常为了提高应用程序的并发性,会将一个任务分解为多个子任务交给多个线程并行执行,而多个线程之间相互协作时,仍然会存在一个线程需要等待另外的线程完成后继续下一步操作。
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列
- JDK 中,join 的实现、Future 的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
- 等待唤醒机制的规范实现。此模式依赖于Java线程的阻塞唤醒机制:
- sychronized+wait/notify/notifyAll
- reentrantLock+Condition(await/singal/singalAll)
- cas+park/unpark
四、多线程分工模式
Thread-Per-Message 模式、Worker Thread 模式和生产者-消费者模式属于多线程分工模式。
- Thread-Per-Message 模式需要注意线程的创建,销毁以及是否会导致OOM。
- Worker Thread 模式需要注意死锁问题,提交的任务之间不要有依赖性。
- 生产者 - 消费者模式可以直接使用线程池来实现