目录
两阶段终止
说明
Two Phase Termination 在线程 t1 中优雅地终止线程 t2
错误方法:
① 使用线程对象的 stop 方法,会真正杀死线程,但是线程持有锁的话,它就无法释放,其他线程也就无法获得锁
② System.exit(int) 会将整个进程杀死
利用共享标记打断
@Slf4j(topic = "c.TwoPhaseTermination") public class TwoPhaseTermination { private Thread thread; private volatile boolean interrupted = false; public final void start(){ thread = new Thread(() -> { log.debug("正常启动......"); while (true){ if(interrupted){ log.debug("结束运行......"); break; } try { log.debug("正常运行......"); Thread.sleep(1000); } catch (InterruptedException e) { } } }, "执行线程"); thread.start(); } public final void stop(){ interrupted = true; thread.interrupt(); } public static void main(String[] args) throws InterruptedException { TwoPhaseTermination tpt = new TwoPhaseTermination(); tpt.start(); log.debug("4s 后,关闭"); Thread.sleep(4000); tpt.stop(); } }
此处 interrupted 标记,会被“执行线程”反复读取,需要保证它的可见性
Balking 模式
犹豫模式;用于一个线程想做某件事,另一个线程已经做了;那么该线程就不再重复,直接结束返回
static void balking(){ for(int i = 0; i < 10; i++){ new Thread(() -> { start(); }, "t" + i).start(); } } static void start(){ log.debug("尝试启动..."); synchronized (VolatileTest.class){ if(starting){ log.debug("已经启动...不可重复启动"); return; } } // 只有一个线程,会执行到这里 log.debug("启动成功!"); starting = true; }
① 其实这里不需要用到 volatile,主要保证的是一个可见性
② 在这里,starting 是会被所有线程读到的;所以这里必须保证它的可见性
③ starting 在 synchronized 内部被读取,保证了可见,从主存读取
④ 线程 t8 首先执行了 start,启动了;后面的线程发现已经启动,则直接结束返回
⑤ 对 starting 的修改操作,放在锁外面;因为只有线程 t8 能到这一步,后面线程过不了 if
DCL 优化
线程不安全 —— 懒汉式单例
@Slf4j(topic = "c.Singleton") public final class Singleton { private static Singleton INSTANCE = null; private Singleton(){log.debug("创建......");} // 创建一个对象,就打印一次 public static Singleton instance(){ if(INSTANCE == null){ INSTANCE = new Singleton(); } return INSTANCE; } } @Slf4j(topic = "c.SingletonTest") class SingletonTest{ public static void main(String[] args) { for(int i = 0; i < 5; i++){ new Thread(() -> { Singleton.instance(); }, "t" + i).start(); } } }
① 并发情况下,多个线程同时判断 INSTANCE 是否为 null
② 都发现不为 null,于是都进入 if;创建出一个新对象
线程安全 —— 懒汉式单例
public static synchronized Singleton instance() //--------------------------------------------- synchronized(Singleton.class){ if(INSTANCE == null){ INSTANCE = new Singleton(); } }
![]()
优点:实现简单
缺点:所有线程都需要获取锁,执行同步代码块,即使对象已经创建;降低并发度
DCL —— 懒汉式单例
DCL 简介
Double - Checked Locking 双重检查锁:保证不出现,所有线程都来获取锁;假如对象已经创建,则不再获取锁;提高了并发度
DCL 实现单例(不安全)
@Slf4j(topic = "c.Singleton") public final class Singleton { private static Singleton INSTANCE = null; private static int count = 0; private Singleton(){} public static Singleton instance(){ if(INSTANCE == null){ // 第一次检查,最多前面的线程会获取锁 synchronized(Singleton.class){ log.debug("进入...... {}", ++count); // 进入了同步块,就记一次数 if(INSTANCE == null){ // 第二次检查 INSTANCE = new Singleton(); } } } return INSTANCE; } } @Slf4j(topic = "c.SingletonTest") class SingletonTest{ public static void main(String[] args) { for(int i = 0; i < 200; i++){ new Thread(() -> { Singleton.instance(); }, "t" + i).start(); } } }
① 200 个线程,并没有都进入同步代码块;提高并发度
② 但是这种写法,线程不安全
DCL 实现单例(安全)
如上产生线程不安全原因如下:
① 对象创建有四步(并且我们都知道,INSTANCE 只是一个引用地址,找到对象)
1. new 2. dup 3. invokespecial 4. putstatic
1. 产生对象引用地址,此时对象还没真正创建
2. 复制一份引用地址
3. 通过复制的引用地址调用构造方法,此时才创建对象
4. 将引用地址符值给 INSTANCE
② 此时可能发生指令重排,先执行 4,后执行 3
③ 线程 t1 中,先将引用地址符值给了 INSTANCE,然后再调用构造方法;正好线程 t2 进入外层 if(没有在锁内部,不受到保护),此时 INSTANCE 已经不为空了,于是直接 return;对象还没有创建完,就已经被用了,肯定会出现问题
解决:使用 volatile 保证有序性;引用地址符值给 INSTANCE 属于写操作,则 4 后面会加入写屏障,保证前面的指令不会重排到后面
private static volatile Singleton INSTANCE = null;
总结
① 上面发生的指令重排情况,在单线程下,是没有影响的
② ⭐ synchronized 不能阻止指令重排发生; 它之所以能保证内部的有序性,是因为同一时刻,只有一个线程能获得锁,就有点类似单线程那种效果
③ 此处,由于 synchronized{...} 外层还有一个 if 判断,这是不受它保护的;同时可以有多个线程可以执行的;那么此时,假如创建对象发生指令重排,就会出现问题
④ 当共享资源不是完全受 synchronized 保护时,就需要考虑到会不会受到指令重排影响;需要时,使用 volatile
happens - before
定义
规定了对共享变量的写操作对其他线程可见,抛开这些规则,JMM 不能保证一个线程对共享变量写操作对其他线程可见
规则总结
① synchronized
② volatile
③ 线程 start 前,对变量进行写操作,线程开始后对该变量读可见
static int x; x = 10; new Thread(() -> { System.out.println(x); }, "t").start();
④ 线程 t1 等待 线程 t2 结束;t2 结束前对变量进行写操作, t1 对该变量读可见
Thread t2 = new Thread(() -> { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } x = 200; }, "t2"); Thread t1 = new Thread(() -> { try { t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } //while(t2.isAlive()){} log.debug("x : {}", x); }, "t1"); t2.start(); t1.start();
⑤ 线程 t2 被 t1 打断,并且 t1 修改了变量;主线程对变量读可见
Thread t2 = new Thread(() -> { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }, "t2"); Thread t1 = new Thread(() -> { try { Thread.sleep(1500); x = 300; // 写 t2.interrupt(); // 打断 t2 } catch (InterruptedException e) { e.printStackTrace(); } }, "t1"); t2.start(); t1.start(); while(!t2.isInterrupted()){} log.debug("x : {}", x);
⑥ 对变量默认值(0,false,null)的写,其他线程对这些变量读可见
⑦ 传递性(volatile 读写屏障)
volatile 场景使用
① 多个线程进行读取,单个线程进行修改(如:标记的可见性)
② 锁保护不到的范围,防止指令重排使线程不安全