Java 线程安全:synchronized 与 Lock 的区别、代码示例及高并发场景适配
·
Java 线程安全:synchronized 与 Lock 的区别、代码示例及高并发场景适配
摘要:在Java中,
synchronized和Lock都是实现线程同步的机制,用于解决多线程并发访问共享资源时的线程安全问题。但它们在实现原理、功能特性和使用场景上有显著区别,下面从多个维度详细对比并结合示例说明
一、核心区别总览
| 维度 | synchronized |
Lock(以ReentrantLock为例) |
|---|---|---|
| 实现层面 | Java关键字,JVM原生实现 | 接口(java.util.concurrent.locks.Lock),API层面实现(如ReentrantLock) |
| 锁的获取与释放 | 自动获取/释放(代码块结束或异常时) | 手动获取(lock())/释放(unlock()),需在finally中确保释放 |
| 中断响应 | 不可中断(等待锁的线程会一直阻塞) | 可中断(lockInterruptibly()) |
| 超时获取 | 不支持 | 支持(tryLock(long timeout, TimeUnit unit)) |
| 公平性 | 仅支持非公平锁 | 可选择公平/非公平锁(构造函数参数指定) |
| 绑定条件 | 与wait()/notify()绑定,仅一个条件 |
可通过Condition绑定多个条件 |
| 读写分离 | 不支持 | 可通过ReadWriteLock实现读写分离 |
| 锁状态查询 | 无法查询 | 可查询(如isLocked()、hasQueuedThreads()) |
二、详细区别及示例
1. 实现层面与基本用法
synchronized是Java关键字,由JVM直接管理,无需手动处理锁的释放。Lock是接口,需通过实现类(如ReentrantLock)使用,锁的释放必须手动完成。
示例1:synchronized的基本用法
// 同步方法
class SynchronizedCounter {
private int count = 0;
// 同步方法(锁为当前对象)
public synchronized void increment() {
count++;
}
// 同步代码块(锁为指定对象)
public void decrement() {
synchronized (this) {
count--;
}
}
public synchronized int getCount() {
return count;
}
}
示例2:Lock的基本用法
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class LockCounter {
private int count = 0;
private final Lock lock = new ReentrantLock(); // 实例化Lock
public void increment() {
lock.lock(); // 手动获取锁
try {
count++; // 临界区
} finally {
lock.unlock(); // 必须在finally中释放锁,避免死锁
}
}
public void decrement() {
lock.lock();
try {
count--;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
2. 锁的释放机制
synchronized:当代码块执行完毕或抛出异常时,JVM会自动释放锁,无需手动干预。Lock:必须通过unlock()手动释放锁,且通常需要在finally块中执行(否则可能因异常导致锁未释放,引发死锁)。
3. 中断响应能力
synchronized:等待锁的线程无法被中断,会一直阻塞直到获取到锁。Lock:支持中断等待锁的线程(通过lockInterruptibly()),可避免线程无限期阻塞。
示例3:Lock的中断响应
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockInterruptExample {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
// 尝试获取锁,允许被中断
lock.lockInterruptibly();
try {
System.out.println("线程1获取到锁,开始休眠");
Thread.sleep(10000); // 持有锁10秒
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("线程1等待锁时被中断");
}
});
Thread t2 = new Thread(() -> {
try {
lock.lockInterruptibly(); // 尝试获取锁(此时t1持有锁)
try {
System.out.println("线程2获取到锁");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("线程2等待锁时被中断");
}
});
t1.start();
Thread.sleep(1000); // 确保t1先获取锁
t2.start();
Thread.sleep(2000); // 让t2进入等待锁状态
t2.interrupt(); // 中断t2的等待
}
}
输出:
线程1获取到锁,开始休眠
线程2等待锁时被中断
- 说明:t2在等待锁时被中断,
lockInterruptibly()会抛出InterruptedException,避免t2无限期阻塞。
4. 超时获取锁
synchronized:不支持超时,线程会一直阻塞直到获取锁。Lock:通过tryLock(long timeout, TimeUnit unit)实现超时获取,若超时未获取到锁则返回false,可避免死锁。
示例4:Lock的超时获取
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTimeoutExample {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
lock.lock();
try {
System.out.println("线程1获取到锁,休眠5秒");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread t2 = new Thread(() -> {
try {
// 尝试在2秒内获取锁
boolean acquired = lock.tryLock(2, TimeUnit.SECONDS);
if (acquired) {
try {
System.out.println("线程2获取到锁");
} finally {
lock.unlock();
}
} else {
System.out.println("线程2超时未获取到锁,放弃等待");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(1000);
t2.start();
}
}
输出:
线程1获取到锁,休眠5秒
线程2超时未获取到锁,放弃等待
- 说明:t2尝试在2秒内获取锁,但t1持有锁5秒,因此t2超时后放弃,避免无限阻塞。
5. 公平性选择
synchronized:仅支持非公平锁(线程获取锁的顺序不保证与请求顺序一致,可能有"插队"现象)。Lock:ReentrantLock的构造函数可指定fair参数(true为公平锁,false为非公平锁),公平锁会按线程请求顺序分配锁(性能略低,但避免饥饿)。
示例5:Lock的公平锁
import java.util.concurrent.locks.ReentrantLock;
public class FairLockExample {
// 公平锁(按请求顺序分配)
private static final ReentrantLock fairLock = new ReentrantLock(true);
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int j = 0; j < 2; j++) { // 每个线程尝试获取2次锁
fairLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取到锁");
} finally {
fairLock.unlock();
}
}
}, "线程" + i).start();
}
}
}
输出(大致顺序):
线程0 获取到锁
线程1 获取到锁
线程2 获取到锁
线程3 获取到锁
线程4 获取到锁
线程0 获取到锁
线程1 获取到锁
...(按请求顺序执行)
- 说明:公平锁保证线程按请求顺序获取锁,而非公平锁可能出现后请求的线程先获取锁。
6. 绑定多个条件
synchronized:与Object的wait()/notify()/notifyAll()绑定,一个锁只能关联一个条件队列(所有等待线程共享)。Lock:通过newCondition()创建多个Condition对象,可实现多个条件队列(不同线程等待不同条件,唤醒时更精准)。
示例6:Lock的多条件绑定
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// 生产者-消费者模型(多条件)
public class MultiConditionExample {
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition(); // 非空条件
private final Condition notFull = lock.newCondition(); // 非满条件
private final Object[] buffer = new Object[10];
private int count = 0, putIndex = 0, takeIndex = 0;
// 生产
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == buffer.length) {
notFull.await(); // 缓冲区满,等待非满条件
}
buffer[putIndex] = x;
if (++putIndex == buffer.length) putIndex = 0;
count++;
notEmpty.signal(); // 唤醒等待非空条件的消费者
} finally {
lock.unlock();
}
}
// 消费
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 缓冲区空,等待非空条件
}
Object x = buffer[takeIndex];
if (++takeIndex == buffer.length) takeIndex = 0;
count--;
notFull.signal(); // 唤醒等待非满条件的生产者
return x;
} finally {
lock.unlock();
}
}
}
- 说明:通过
notEmpty和notFull两个条件,生产者和消费者可分别等待不同条件,唤醒时仅通知相关线程,比synchronized的notifyAll()更高效。
7. 读写分离锁
synchronized:不支持读写分离,读操作和写操作互斥(即使多个读线程也无法并发)。Lock:通过ReentrantReadWriteLock实现读写分离,多个读线程可并发访问(读锁共享),写线程独占(写锁排他),适合读多写少场景。
示例7:ReadWriteLock的读写分离
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private int data = 0;
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock(); // 读锁(共享)
private final Lock writeLock = rwLock.writeLock(); // 写锁(排他)
// 读操作(共享)
public int read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 读取数据: " + data);
return data;
} finally {
readLock.unlock();
}
}
// 写操作(排他)
public void write(int newData) {
writeLock.lock();
try {
data = newData;
System.out.println(Thread.currentThread().getName() + " 写入数据: " + data);
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
ReadWriteLockExample example = new ReadWriteLockExample();
// 3个读线程并发
for (int i = 0; i < 3; i++) {
new Thread(example::read, "读线程" + i).start();
}
// 1个写线程
new Thread(() -> example.write(100), "写线程").start();
}
}
输出(可能):
读线程0 读取数据: 0
读线程1 读取数据: 0
读线程2 读取数据: 0
写线程 写入数据: 100
- 说明:多个读线程可同时获取读锁并发执行,而写线程必须等待所有读锁释放后才能获取写锁,提高了读操作的并发效率。
三、性能对比
- 低并发场景:
synchronized性能更好(JVM对其有优化,如偏向锁、轻量级锁)。 - 高并发场景:
Lock(如ReentrantLock)性能更稳定(减少了synchronized的重量级锁竞争开销)。
四、适用场景
- 优先用
synchronized:简单同步场景(如单一路径的锁竞争),代码简洁,无需手动管理锁释放。 - 优先用
Lock:- 需要中断等待锁的线程、超时获取锁。
- 需要公平锁或多条件绑定。
- 读多写少场景(用
ReadWriteLock)。 - 需要查询锁状态(如
isLocked())。
总结
synchronized是JVM原生的简单同步机制,适合基础场景;Lock是更灵活的API级同步工具,支持中断、超时、公平性等高级特性。实际开发中需根据场景选择,两者并非替代关系,而是互补关系。
更多推荐
所有评论(0)