
目录Java 多线程线程安全、wait/notify 与单例模式详解一、线程安全问题1. 什么是线程安全2. 线程安全问题产生的原因1线程的随机调度2多个线程修改同一个变量3操作不是原子的4内存可见性问题5指令重排序二、wait 与 notifywait 与 join 的区别joinwait锁竞争问题三、线程饥饿Starvation四、wait 方法详解1. 当前线程进入等待状态2. 释放当前持有的锁注意事项wait 和 synchronized 必须使用同一个锁对象五、单例设计模式什么是设计模式什么是单例模式六、饿汉模式七、懒汉模式7.1 普通懒汉模式线程不安全7.2 同步方法7.3 双重检查锁DCL7.4 为什么需要 volatile总结Java 多线程线程安全、wait/notify 与单例模式详解一、线程安全问题1. 什么是线程安全线程安全指的是一段代码在多线程环境下运行时无论线程如何调度程序最终都能够得到符合预期的结果。例如count;假设两个线程同时执行这行代码。我们的预期是count count 2;但最终结果可能只增加了 1。这说明程序出现了线程安全问题。2. 线程安全问题产生的原因1线程的随机调度线程由操作系统负责调度。现代操作系统普遍采用抢占式调度当前线程运行过程中可能随时被暂停CPU 可能切换到其他线程执行程序员无法预测线程切换时机。因此多线程执行顺序是不确定的。2多个线程修改同一个变量当多个线程同时读写同一个变量时就可能产生数据竞争Race Condition。例如count;表面上看是一条语句但实际上对应多个CPU指令读取 count count 1 写回 count如果两个线程同时执行就可能出现线程A读取 count 0 线程B读取 count 0 线程A写回 1 线程B写回 1最终结果变成count 1而不是预期的count 23操作不是原子的原子性Atomicity指一个操作要么全部执行成功要么完全不执行中间不会被其他线程打断。例如count;并不是原子操作。因为它对应读取 计算 写回三个步骤。在执行过程中可能被其他线程插入执行从而导致线程安全问题。4内存可见性问题为了提高运行效率CPU 和编译器会进行优化。线程读取变量时boolean flag false;可能不会每次都从主内存读取而是缓存到自己的工作内存中。此时线程Awhile(!flag){ }线程Bflag true;虽然线程B已经修改了变量但线程A可能一直读取自己缓存中的旧值。导致循环无法结束。这就是内存可见性问题。解决方案volatile boolean flag;volatile能够保证变量修改后立即刷新到主内存并保证其他线程能够及时看到最新值。5指令重排序为了提高执行效率编译器和CPU可能调整指令执行顺序。例如new Singleton();理论执行过程1. 申请内存 2. 初始化对象 3. 引用指向对象实际可能被优化为1. 申请内存 2. 引用指向对象 3. 初始化对象此时其他线程可能拿到一个尚未初始化完成的对象。这就是指令重排序问题。二、wait 与 notifywait 和 notify 用于协调多个线程之间的执行顺序。它们无法决定CPU调度哪个线程但能够让线程主动进入等待状态。例如线程A先执行 线程B等待 线程A完成后 唤醒线程B这样就实现了线程之间的协作。wait 与 join 的区别joint1.join();含义当前线程必须等待 t1 完全执行结束。waitlock.wait();含义当前线程等待其他线程调用 notify() 或 notifyAll()。不要求对方线程结束运行。因此join等待线程结束 wait等待线程被唤醒锁竞争问题多个线程竞争同一把锁时synchronized(lock){ }锁释放后哪个线程获得锁是不确定的。例如A获得锁 A释放锁 下一次 可能B获得锁 也可能A再次获得锁完全由操作系统调度决定。三、线程饥饿Starvation线程饥饿指某个线程长期得不到CPU时间或锁资源导致一直无法执行。可以把线程看成鸟宝宝CPU是鸟妈妈锁资源是食物如果某只鸟长期抢不到食物就会发生线程饥饿。四、wait 方法详解当线程调用wait();会发生两件事1. 当前线程进入等待状态RUNNING ↓ WAITING2. 释放当前持有的锁这是wait最重要的特性。例如synchronized(lock){ lock.wait(); }执行wait后释放 lock 进入等待状态这样其他线程才能获得锁并执行notify。如果wait不释放锁线程A wait 线程B获取不到锁 无法notify 线程A永远等待程序将陷入死锁状态。注意事项wait 和 synchronized 必须使用同一个锁对象正确synchronized(lock){ lock.wait(); }错误synchronized(lock1){ lock2.wait(); }会抛出IllegalMonitorStateException五、单例设计模式什么是设计模式设计模式Design Pattern针对软件开发中的常见问题总结出来的一套成熟解决方案。可以理解为程序员的“棋谱”。什么是单例模式单例模式要求一个类在整个程序运行期间只能创建一个对象。核心思想构造方法私有化 ↓ 外部无法new ↓ 类内部自己创建对象 ↓ 统一对外提供访问入口六、饿汉模式饿汉模式类加载时立即创建对象。public class Singleton { private static final Singleton INSTANCE new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } }特点实现简单天然线程安全获取速度快缺点可能提前创建不需要使用的对象七、懒汉模式懒汉模式第一次使用时才创建对象。7.1 普通懒汉模式线程不安全public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if(instance null){ instance new Singleton(); } return instance; } }问题多个线程同时进入if(instance null)可能创建多个对象。7.2 同步方法public static synchronized Singleton getInstance(){ if(instance null){ instance new Singleton(); } return instance; }优点线程安全缺点每次获取对象都要加锁性能较低7.3 双重检查锁DCLpublic class Singleton { private static volatile Singleton instance; private Singleton(){} public static Singleton getInstance(){ if(instance null){ synchronized(Singleton.class){ if(instance null){ instance new Singleton(); } } } return instance; } }第一次判断if(instance null)避免频繁加锁。第二次判断if(instance null)防止多个线程重复创建对象。7.4 为什么需要 volatile对象创建过程可能发生指令重排序1. 分配内存 2. 初始化对象 3. 引用指向对象可能变成1. 分配内存 2. 引用指向对象 3. 初始化对象此时线程A刚执行到步骤2线程B发现instance ! null直接返回对象。但对象实际上还没有初始化完成。因此需要private static volatile Singleton instance;volatile能够禁止这部分重排序从而保证DCL模式正确运行。总结线程安全问题主要来源于线程随机调度 ↓ 共享资源竞争 ↓ 原子性问题 内存可见性问题 指令重排序问题而在单例模式中饿汉模式 ↓ 简单、安全 懒汉模式 ↓ 节省资源 DCL volatile ↓ 兼顾性能与线程安全