Java多线程初阶(下)------ synchronized、volatile、wait/notify与经典并发工具
一、解决线程不安全的神器:synchronized(监视器锁)
synchronized能保证原子性 和可见性。
1. 核心特性:互斥与可重入
- 互斥 :当线程A进入
synchronized代码块(相当于加锁),其他试图进入该锁保护的代码块的线程(如线程B)必须阻塞等待,直到A执行完毕释放锁(解锁)。 - 可重入 :同一个线程可以多次获取同一把锁,不会把自己锁死。
synchronized内部记录了"锁的持有者"和"计数器",每次重入计数器+1,完全退出时计数器归零才真正释放锁。
java
synchronized (locker) {
synchronized (locker) { // 同一个线程可以再次进入,不会死锁
count++;
}
}
2. 三种使用方式
- 修饰代码块 :需要手动指定锁对象(
synchronized(locker) {})。 - 修饰普通方法 :锁的是当前实例对象
this。 - 修饰静态方法 :锁的是当前类的
Class对象(全局锁)。
二、保证可见性的轻量级方案:volatile
- 作用 :被
volatile修饰的变量,读写时强制绕过工作内存,直接操作主内存。保证一个线程修改后,其他线程立刻可见。 - 误区 :
volatile不保证原子性 。比如volatile int count,两个线程执行count++依然会数据错乱,因为"读-改-写"不是原子操作。它适合作为标志位 (如boolean isQuit)使用。
三、线程间的协作:wait() 与 notify()
synchronized解决了竞争问题,但有时我们需要线程间主动协调顺序(如线程A传完球,线程B才能扣篮)。wait/notify定义在Object类中,必须配合synchronized使用。
1. wait() 做了什么?
- 释放当前对象锁。
- 让当前线程进入等待队列(WAITING状态)。
- 直到被
notify唤醒或超时,才重新竞争锁。
2. notify() 与 notifyAll()
notify():随机唤醒一个在该对象上等待的线程。notifyAll():唤醒所有等待的线程,它们再去竞争锁。
经典生产者消费者模型示例(伪代码):
java
synchronized (lock) {
while (队列已满) { lock.wait(); } // 生产者等待
生产数据;
lock.notifyAll(); // 唤醒消费者
}
synchronized (lock) {
while (队列为空) { lock.wait(); } // 消费者等待
消费数据;
lock.notifyAll(); // 唤醒生产者
}
注意 :wait必须在while循环中调用,因为被唤醒后不一定满足条件(可能又被别的线程抢占了资源)。
wait 与 sleep 的区别(高频面试题)
wait属于Object,必须配合synchronized,会释放锁;sleep属于Thread,不涉及锁释放。wait需要被显式唤醒;sleep到时间自动醒来。
四、必会的并发编程案例
1. 单例模式(双重检查锁 DCL)
懒汉模式(单线程版)线程不安全,多线程版加上synchronized后虽然安全但效率低。最终优化版(面试必考):
java
class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查:避免每次都加锁
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查:防止多线程同时创建
instance = new Singleton();
}
}
}
return instance;
}
}
为什么加volatile? 因为new Singleton()这一步可能会发生指令重排序(分配内存、初始化、赋值),加上volatile禁止重排序,保证其他线程看到的是完整的对象。
2. 阻塞队列(BlockingQueue)
Java标准库提供了LinkedBlockingQueue、ArrayBlockingQueue。核心方法:
put(e):队列满则阻塞等待。take():队列空则阻塞等待。
利用阻塞队列可以轻松实现"生产者-消费者模型",起到削峰填谷和解耦的作用。
3. 定时器(Timer)
java
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("3秒后执行");
}
}, 3000);
原理:内部维护一个优先级队列(按执行时间排序),后台线程不断扫描队首元素,判断当前时间是否达到执行时间。
4. 线程池(ThreadPoolExecutor)
频繁创建/销毁线程开销大,线程池复用线程。
- 标准库创建 :
Executors.newFixedThreadPool(10)(固定数量)等。 - 核心参数(线程池7大参数) :
corePoolSize:核心线程数(正式工)。maximumPoolSize:最大线程数(正式工 + 临时工)。keepAliveTime:临时工空闲多久会被辞退。workQueue:任务阻塞队列。handler:拒绝策略(当队列满且线程数达到最大时执行)。
- 自定义线程池:核心逻辑就是维护一个阻塞队列存放任务,多个Worker线程不断从队列取任务执行。
总结 :多线程编程的核心是访问共享资源时保证线程安全 。优先考虑volatile(轻量级标志位),其次使用synchronized保证原子性,线程间协调使用wait/notify,最后不要重复造轮子,多用标准库提供的线程池、阻塞队列等组件。