java教程笔记(十三)-synchronized和ReentrantLock
1.synchronized-线程同步详解
在 Java 中,synchronized
是一种用于实现线程同步的关键字。它的主要作用是确保多个线程在访问共享资源时的互斥性和可见性,从而避免并发问题(如数据不一致、竞态条件等)。
1. synchronized 的作用
- 互斥性(Mutual Exclusion):确保同一时间只有一个线程可以执行某个代码块或方法。
- 可见性(Visibility):保证共享变量的修改对其他线程立即可见。
2. synchronized 的使用方式
//语法机制 synchronized后面小括号() 中传的这个“数据”是相当关键的。这个数据必须是 多线程共享 的数据。
/*
()中写什么?
那要看你想让哪些线程同步。假设t1、t2、t3、t4、t5,有5个线程,你只希望t1 t2 t3排队,t4 t5不需要排队。怎么办?你一定要在()中写一个t1 t2 t3共享的对象。而这个对象对于t4 t5来说不是共享的。
*/
synchronized(){// 线程同步代码块。
}
2.1 修饰实例方法
synchronized出现在实例方法上,一定锁的是 this
。只能是this。
多个线程调用同一个对象的同步方法时,必须排队执行。
若多个实例存在,锁对象不同,无法同步(引用1的“this锁”说明)
public synchronized void method() {// 同步代码
}
class DiningTable {private String tableId;private int maxSeats = 4;private int availableSeats;// 锁对象:每个餐桌独立一把锁private final Object lock = new Object();public DiningTable(String tableId) {this.tableId = tableId;this.availableSeats = maxSeats;}// 分配座位的方法public void bookSeats(int numberOfSeats) {synchronized (lock) { // 使用当前餐桌的锁对象if (availableSeats >= numberOfSeats) {System.out.println(Thread.currentThread().getName() + " 正在为桌号:" + tableId + " 预订 " + numberOfSeats + " 个座位");try {Thread.sleep(500); // 模拟处理时间} catch (InterruptedException e) {e.printStackTrace();}availableSeats -= numberOfSeats;System.out.println("预订成功!剩余座位:" + availableSeats);} else {System.out.println("桌号:" + tableId + " 当前无足够座位!");}}}public int getAvailableSeats() {return availableSeats;}
}class BookingThread extends Thread {private DiningTable table;private int seatsToBook;public BookingThread(DiningTable table, int seatsToBook, String name) {super(name);this.table = table;this.seatsToBook = seatsToBook;}@Overridepublic void run() {table.bookSeats(seatsToBook);}
}public class RestaurantBookingSystem {public static void main(String[] args) {// 创建两个餐桌对象DiningTable table1 = new DiningTable("Table1");DiningTable table2 = new DiningTable("Table2");// 启动多个线程并发预订BookingThread t1 = new BookingThread(table1, 2, "服务员A");BookingThread t2 = new BookingThread(table1, 3, "服务员B");BookingThread t3 = new BookingThread(table2, 2, "服务员C");BookingThread t4 = new BookingThread(table2, 2, "服务员D");t1.start();t2.start();t3.start();t4.start();try {t1.join();t2.join();t3.join();t4.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("最终状态:");System.out.println("Table1 剩余座位: " + table1.getAvailableSeats());System.out.println("Table2 剩余座位: " + table2.getAvailableSeats());}
}
/*
t1(服务员A) 对 Table1 预订 2 个座位 成功,剩余 2 个座位
t2(服务员B) 对 Table1 预订 3 个座位 失败,剩余座位不足
t3(服务员C) 对 Table2 预订 2 个座位 成功,剩余 2 个座位
t4(服务员D) 对 Table2 预订 2 个座位 成功,剩余 0 个座位
*/
- 锁的是当前对象 (
this
)。 - 同一时间只有一个线程可以调用该对象的
synchronized
方法。
2.2 修饰静态方法
类锁永远只有1把。
就算创建了100个对象,那类锁也只有1把。
- 对象锁:1个对象1把锁,100个对象100把锁。
- 类锁:100个对象,也可能只是1把类锁。
public class Counter {
private static int count = 0;
public static synchronized void resetCount() {
count = 0;
}
}
- 锁的是类的 Class 对象(
Counter.class
)。 - 所有该类的实例共享这个锁。
2.3 修饰代码块(推荐)
public class Counter {
private int count = 0;private final Object lock = new Object();public void increment() {
synchronized (lock) {
count++;
}
}
}
- 更加灵活,可以指定任意对象作为锁。
- 推荐使用这种方式,以减少锁的粒度和提高并发性能。
3.死锁
在 Java 中,使用 synchronized
实现线程同步时,死锁(Deadlock)是常见的并发问题。其核心在于多个线程因争夺锁资源而陷入相互等待的循环状态。
1. 死锁的四个必要条件
死锁的形成需同时满足以下条件:
- 互斥:锁资源只能被一个线程持有。
- 不可抢占:锁只能由持有线程主动释放。
- 请求与保持:线程持有锁的同时申请新锁。
- 循环等待:多个线程形成等待环(如线程A等线程B持有的锁,线程B等线程A持有的锁)
线程1持有 lockA
并等待 lockB
,线程2持有 lockB
并等待 lockA
,形成死锁
// 线程1:先锁A再锁B
synchronized (lockA) {Thread.sleep(1000);synchronized (lockB) { /* ... */ }
}// 线程2:先锁B再锁A
synchronized (lockB) {Thread.sleep(1000);synchronized (lockA) { /* ... */ }
}
4. synchronized 底层原理
Java 中的 synchronized
是基于 JVM 内部的 Monitor(监视器)机制 实现的:
- 每个 Java 对象都有一个与之关联的 Monitor。
- 当线程进入
synchronized
块时,它会尝试获取对象的 Monitor 锁。 - 如果锁未被占用,则线程获得锁并继续执行;如果已被其他线程持有,则当前线程阻塞等待。
JVM 在底层通过 monitorenter
和 monitorexit
字节码指令来实现这一过程。
可重入、支持读写锁(ReadWriteLock ) |
5. 使用注意事项
- 避免死锁:多个线程按不同顺序获取多个锁可能导致死锁。
- 锁的粒度控制:尽量缩小
synchronized
代码块范围,以提高并发性能。 - 不要锁 String 常量池对象:因为字符串常量池可能被多个地方复用,容易引发意外竞争。
- 避免锁升级失败:在高并发场景下,JVM 会对
synchronized
进行锁优化(偏向锁 → 轻量级锁 → 重量级锁),但过度竞争会导致性能下降。
6. 示例代码
示例:多线程计数器
public class SyncExample {
private int count = 0;
public synchronized void add() {
count++;
}
public static void main(String[] args) throws InterruptedException {
SyncExample example = new SyncExample();
for (int i = 0; i < 1000; i++) {
new Thread(example::add).start();
}
Thread.sleep(3000); // 等待所有线程执行完成
System.out.println("Final count: " + example.count);
}
}
输出:
Final count: 1000
如果没有 synchronized
,由于并发问题,最终结果可能小于 1000。
7. 总结
特性 | 描述 |
---|---|
线程安全 | ✅ 提供线程安全保障 |
易用性 | ✅ 简单易用,适合基础同步需求 |
性能 | ⚠️ 高并发下性能略逊于 ReentrantLock |
控制能力 | ❌ 不支持超时、尝试锁等高级功能 |
synchronized
是 Java 并发编程中最基础、最常用的同步机制之一。对于大多数简单的线程同步场景,它是首选方案。而对于需要更细粒度控制的场景,建议使用 ReentrantLock
或 ReadWriteLock
。
2.ReentrantLock
ReentrantLock
是 Java 提供的一种可重入的互斥锁(Mutual Exclusion Lock),它比内置的 synchronized
更加灵活、功能更强大。它是 java.util.concurrent.locks
包下的核心类之一。
1.什么是 ReentrantLock?
- 可重入:同一个线程可以多次获取同一把锁;
- 显式锁:需要手动加锁和解锁;
- 支持尝试获取锁、超时机制、公平锁等高级特性;
- 适用于高并发场景下的细粒度控制。
import java.util.concurrent.locks.ReentrantLock;public class Counter {private int count = 0;private ReentrantLock lock = new ReentrantLock();public void increment() {lock.lock(); // 手动加锁try {count++;} finally {lock.unlock(); // 必须在 finally 中释放锁}}public int getCount() {lock.lock();try {return count;} finally {lock.unlock();}}
}
2.主要特性详解
特性 | 描述 |
---|---|
可重入 | 同一线程可以多次调用 lock() 而不会死锁 |
尝试加锁 | tryLock() 可以避免阻塞,适合做资源探测 |
超时机制 | tryLock(long time, TimeUnit unit) 支持等待一段时间后放弃 |
公平锁 | 构造时传参 new ReentrantLock(true) ,保证先请求的线程优先获得锁 |
锁绑定条件 | 可配合 Condition 实现更复杂的线程通信逻辑 |
可重入示例:
public class ReentrantExample {
private ReentrantLock lock = new ReentrantLock();
public void methodA() {
lock.lock();
try {
System.out.println("methodA");
methodB(); // 可以再次进入}
finally {
lock.unlock();}}
public void methodB() {lock.lock();try {
System.out.println("methodB");}
finally { lock.unlock();
}
}}
同一个线程多次获取锁是允许的。
尝试加锁 + 超时机制
if (lock.tryLock()) {try {
// 执行操作 }
finally {
lock.unlock();}
}
else {
System.out.println("无法获取锁");}
或者带超时时间:
if (lock.tryLock(1, TimeUnit.SECONDS)) {try { // 成功获取锁 }
finally { lock.unlock();}
}
else { System.out.println("等待锁超时");
}
和 synchronized
的对比
对比项 | synchronized | ReentrantLock |
---|---|---|
自动释放锁 | 是 | 需手动释放 |
可尝试获取锁 | 否 | 是 (tryLock ) |
支持超时 | 否 | 是 |
支持中断响应 | 否 | 是 |
是否公平 | 默认非公平 | 可设置为公平锁 |
条件变量支持 | 不支持 | 支持 Condition |
性能 | JDK1.6+优化后性能接近 | 稍复杂但更可控 |
使用建议
场景 | 推荐方式 |
---|---|
简单同步方法/代码块 | 使用 synchronized |
需要尝试加锁、超时、公平锁 | 使用 ReentrantLock |
高并发数据结构、任务调度器等 | 使用 ReentrantLock 或 ReadWriteLock |
注意事项
- 必须在
finally
块中释放锁,否则可能造成死锁; - 不要在构造函数中调用
lock()
,容易导致死锁; - 避免嵌套锁顺序混乱,防止发生死锁;
- 合理设置公平性,虽然公平锁能避免饥饿,但性能较低。
总结
功能 | 是否支持 |
---|---|
可重入 | 是 |
尝试加锁 | 是 |
超时机制 | 是 |
公平锁 | 是 |
条件变量 | 是 |
高并发适用性 | 是 |
3.volatile
volatile
是 Java 中的一个关键字,用于多线程编程中确保变量的可见性和有序性,但不保证操作的原子性。
1. 可见性(Visibility)
当一个变量被 volatile
修饰时,它会确保该变量在多个线程之间是可见的。也就是说:
- 当一个线程修改了
volatile
变量的值,其他线程可以立即看到这个修改。 - 这是因为
volatile
会禁止JVM对变量进行缓存到寄存器或CPU高速缓存中,强制读写都发生在主内存中。
示例:
public class VolatileExample {
private volatile boolean flag = false;
public void toggleFlag() {
flag = true;
}
public boolean isFlag() {return flag;
}
}
当一个线程调用 toggleFlag()
修改 flag 后,其他线程能立即感知到这个变化。
2. 禁止指令重排序(Ordering / Happens-Before)
Java 编译器和 JVM 在执行时可能会对指令进行优化重排以提高性能,但在多线程环境下这种行为可能导致不可预测的问题。使用 volatile
可以阻止编译器和 JVM 对该变量的读写操作进行重排序。
例如,在单例模式双重检查锁定中,volatile
非常关键:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 禁止重排序
}
}
}
return instance;}
}
如果不用 volatile
,则 new Singleton()
的初始化过程可能被重排序,导致其他线程拿到一个未构造完成的对象。
3. 不保证原子性
volatile
不能保证复合操作的原子性,例如 i++
这种操作需要读、加、写三步:
private volatile int count = 0;
public void increment() {
count++; // 非原子操作}
这种情况下仍需配合锁(如 synchronized
或 AtomicInteger
)来实现线程安全。
总结对比
特性 | volatile | synchronized |
---|---|---|
可见性 | ✅ | ✅ |
原子性 | ❌ | ✅(代码块原子化) |
有序性 | ✅ | ✅ |
线程阻塞 | ❌ | ✅ |
使用场景
- 状态标志(如开关、是否初始化等)
- 单例模式双重检查锁定
- 事件监听器机制
- 多线程环境中共享的基础类型变量(int、boolean 等)且不需要复合操作
小结
volatile
是一种轻量级同步机制,适用于变量仅被多个线程读取和单次赋值的情况。若涉及复杂操作或多步骤逻辑,仍需使用锁机制来保证线程安全。
4.Java中有三大变量以及线程安全问题
在 Java 中,三大变量通常指的是:
- 局部变量(Local Variables)
- 实例变量(Instance Variables / 成员变量)
- 静态变量(Static Variables / 类变量)
这三类变量在多线程环境下是否线程安全,取决于它们的使用方式。下面我们将详细分析每种变量的特点以及在多线程环境下的线程安全性。
1.Java 三大变量详解
1. 局部变量(Local Variables)
- 定义位置:定义在方法内部或代码块中。
- 生命周期:随方法调用而创建,方法执行结束则销毁。
- 存储位置:保存在栈帧中的局部变量表中(每个线程有自己的栈空间)。
- 线程安全性:线程安全,因为每个线程都有自己的副本,不存在共享问题。
public void method() { int localVar = 10; // 局部变量 }
结论:局部变量是线程安全的,无需额外同步。
2. 实例变量(Instance Variables)
- 定义位置:定义在类中、方法外,属于对象的一部分。
- 生命周期:随着对象的创建而存在,对象被回收时销毁。
- 存储位置:保存在堆内存中,多个线程可以访问同一个对象的实例变量。
- 线程安全性: 非线程安全,需要通过
synchronized
、volatile
或并发工具类进行保护。
public class MyClass {private int instanceVar; // 实例变量
public void increment() { instanceVar++; } }
注意:如果多个线程同时操作同一个
MyClass
实例,会出现竞态条件,导致数据不一致。
3. 静态变量(Static Variables)
- 定义位置:定义在类中、方法外,并使用
static
修饰。 - 生命周期:随着类的加载而存在,类卸载时销毁。
- 存储位置:保存在方法区(JDK 8 及以前)或元空间(JDK 8+)中,全局唯一。
- 线程安全性: 非线程安全,多个线程共享同一个类的静态变量,需手动加锁或使用并发工具类。
public class Counter {
private static int count = 0; // 静态变量
public static synchronized void increment() { count++; } }
注意:必须使用
synchronized
或其他并发机制来确保线程安全。
2.总结对比表
变量类型 | 存储位置 | 是否共享 | 线程安全 | 推荐做法 |
---|---|---|---|---|
局部变量 | 栈 | 否 | ✅ 是 | 无需处理 |
实例变量 | 堆 | 是 | ❌ 否 | 加锁或使用原子类 |
静态变量 | 方法区/元空间 | 是 | ❌ 否 | 加锁或使用 volatile 、AtomicInteger |
3.最佳实践建议
- 优先使用局部变量:避免共享,减少同步开销。
- 尽量避免使用静态变量:除非确实需要全局共享。
- 使用并发工具类:如
AtomicInteger
、ConcurrentHashMap
。 - 合理控制锁粒度:避免粗粒度锁影响性能。
- 避免死锁:按固定顺序加锁,避免嵌套锁。
5.关于Object类的wait()、notify()、notifyAll()方法
Object
类是 Java 中所有类的根类,它提供了一些与线程同步和协作密切相关的基础方法:wait()
、notify()
和 notifyAll()
。这些方法用于实现线程间的等待/通知机制,通常与 synchronized
配合使用。此三个方法饿调用者必须是synchronized
(同步监视器)里面的同步监视器对象
1.方法详解
1. wait()
- 作用:使当前线程进入等待状态,并释放持有的对象锁。
- 调用前提:必须在
synchronized
同步块或方法中调用,否则抛出IllegalMonitorStateException
。 - 行为:
- 线程会释放对象的锁,进入该对象的等待队列(wait set)。
- 直到其他线程调用此对象的
notify()
或notifyAll()
方法,或者指定超时时间。
常见重载形式:
void wait() throws InterruptedException void wait(long timeout) throws InterruptedException void wait(long timeout, int nanos) throws InterruptedException
2. notify()
- 作用:唤醒在此对象监视器上等待的一个线程(具体哪一个由 JVM 决定)。
- 调用前提:必须在
synchronized
块中调用。 - 注意:不会立即释放锁,要等到当前同步块执行完毕后才会释放锁。
3. notifyAll()
- 作用:唤醒在此对象监视器上等待的所有线程。
- 优势:避免因只唤醒一个线程而导致死锁或效率低下的问题。
2.使用规范与注意事项
正确使用方式:
synchronized (lockObj) {
while (条件不满足) {lockObj.wait(); // 让正在lockObj对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。
}
// 条件满足,执行业务逻辑}
synchronized (lockObj) {
// 修改共享资源
lockObj.notify(); // 或 notifyAll(); 唤醒正在lockObj对象上等待的线程。
}
注意事项:
项目 | 说明 |
---|---|
必须配合 synchronized 使用 | 否则抛异常 |
wait() 必须在循环中使用 | 防止虚假唤醒 |
不要对常量字符串加锁 | 如 "LOCK".intern() ,容易引发死锁 |
notify() 可能导致死锁 | 推荐优先使用 notifyAll() |
3.生产者消费者示例
- 生产线程负责生产,消费线程负责消费。
- 生产线程和消费线程要达到均衡。
- 这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。
class SharedQueue<T> {
private Queue<T> queue = new LinkedList<>();
private int capacity;
public SharedQueue(int capacity) {
this.capacity = capacity;
}
public synchronized void put(T item) throws InterruptedException { w
hile (queue.size() == capacity) {
wait(); // 等待有空位
}
queue.add(item);
notifyAll(); // 通知消费者可以消费了
}
public synchronized T take() throws InterruptedException {while (queue.isEmpty()) { wait(); // 等待有数据 }
T item = queue.poll();
notifyAll(); // 通知生产者可以继续生产 return item; } }
6. Condition
Condition
允许线程在某个条件不满足时挂起,并在其他线程改变状态后被唤醒。相比 wait/notify
,它有以下优势:
- 一个
Lock
可以绑定多个Condition
,实现多路等待/通知 - 更细粒度的控制线程阻塞和唤醒
- 支持尝试等待、超时等高级功能
Lock
和Condition
通常结合使用,以实现更复杂的线程同步机制。
1.常用方法
方法名 | 描述 |
---|---|
await() | 当前线程进入等待状态,直到被唤醒或中断 |
awaitUninterruptibly() | 当前线程接受到信号前一直处于等待状态,不响应中断的等待 |
await(long time, TimeUnit unit) | 等待,当前线程接受到信号前、或中断前、或到达指定等待时间之前一直处于等待状态,返回boolean类型,表示是否在指定时间内获取到接受信号,false表示超时。 |
awaitUntil(Date deadline) | 当前线程在接收到信号前、被中断或到达指定最后唤醒一个等待线程。如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁。期限之前一直处于等待状态 |
signal() | 唤醒一个等待的线程 |
signalAll() | 唤醒所有等待的线程 |
2.基本用法示例
public static void main(String[] args) throws InterruptedException {//创建lock对象Lock lock = new ReentrantLock();//新建conditionCondition condition = lock.newCondition();//创建线程AThread threadA = new Thread(()->{System.out.println("A尝试获取锁...");lock.lock();try {System.out.println("A获取锁成功!");TimeUnit.SECONDS.sleep(1);System.out.println("A开始释放锁,并开始等待...");//线程A开始等待condition.await();System.out.println("A被通知继续运行...");} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();System.out.println("A线程释放了锁,执行结束!");}});//创建线程BThread threadB = new Thread(()->{System.out.println("B尝试获取锁...");lock.lock();try {System.out.println("B获取锁成功!");TimeUnit.SECONDS.sleep(3);//线程B开始唤醒线程Acondition.signal();System.out.println("B随机通知lock对象的等待队列中某个线程!");} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();System.out.println("B线程释放了锁,执行结束!");}});//启动线程AthreadA.start();//这里为了是线程A先执行,避免B先执行了notify导致A永远无法被唤醒TimeUnit.SECONDS.sleep(1);//启动线程BthreadB.start();
}//执行结果
A尝试获取锁...
A获取锁成功!
A开始释放锁,并开始等待...
B尝试获取锁...
B获取锁成功!
B随机通知lock对象的等待队列中某个线程!
B线程释放了锁,执行结束!
A被通知继续运行...
A线程释放了锁,执行结束!
3.Condition 与 wait/notify 对比
功能 | wait/notify | Condition |
---|---|---|
所属 API | Object 类 | java.util.concurrent.locks.Condition |
锁机制 | 必须配合 synchronized 使用 | 必须配合 Lock 使用 |
多条件支持 | ❌(只能有一个等待队列) | ✅(可创建多个 Condition 实例) |
响应中断 | wait() 可能抛出 InterruptedException | 支持 awaitUninterruptibly() |
超时支持 | ✅ | ✅ 更灵活 |
4.使用注意事项
Condition
必须在加锁之后调用(否则会抛异常)await()
返回后仍需重新检查条件是否满足(防止虚假唤醒)signal()
是随机唤醒一个等待线程,而signalAll()
唤醒所有线程,根据业务需要选择- 注意避免死锁,确保唤醒逻辑正确
5.典型应用场景
- 生产者-消费者模型
- 缓冲区管理
- 事件驱动系统中的等待/通知机制
- 线程池任务调度
- 自定义并发数据结构(如阻塞队列)
6.总结
Condition
提供了更强大、更灵活的线程通信机制,是构建高并发程序的重要工具之一。相比于传统的 wait/notify
,它的优势在于:
- 可绑定多个条件变量
- 控制更精细
- 支持超时、中断等特性
建议在使用 ReentrantLock
时搭配 Condition
来实现复杂的线程同步逻辑。