一句话:
Java 锁机制核心是 synchronized 和 ReentrantLock,两者都是可重入锁;
JDK 1.6+ 后 synchronized 引入锁升级(无锁➡️偏向锁➡️轻量级锁➡️重量级锁),性能大幅提升;
简单场景优先用 synchronized,需要高级功能(比如可中断、可超时、公平锁)时用 ReentrantLock。
为什么需要锁?
在并发场景下,多个线程同时修改同一个共享变量,会出现线程安全问题。
举个例子说明一下:
publicclassUnsafeCounter{privateint count = 0;publicvoidincrement(){ count++; // 这行代码不是原子操作! }publicintgetCount(){return count; }}
如果1000个线程同时调用 increment(),最终 count 的值很可能小于1000。
因为 count++ 不是原子操作,它分为读取count、count+1、写回count三步,多线程同时执行会互相覆盖。
锁的作用就是把这段代码加锁,让同一时间只有一个线程能执行,从而保证操作的原子性。
synchronized
synchronized 是JVM内置的关键字,不需要手动释放锁。
| | |
|---|
| | public synchronized void increment() |
| | public static synchronized void increment() |
| | synchronized (lock) { ... } |
可以用synchronized给之前的例子上锁:
publicclassSafeCounter{privateint count = 0;// 锁实例方法:同一时间只有一个线程能执行publicsynchronizedvoidincrement(){ count++; }publicintgetCount(){return count; }}
ReentrantLock
ReentrantLock 是 java.util.concurrent(JUC)包下的锁,是基于代码实现的锁,比 synchronized 更灵活,但需要手动释放锁。
也可以用ReentrantLock给之前的例子上锁:
import java.util.concurrent.locks.ReentrantLock;publicclassReentrantLockCounter{privateint count = 0;// 创建 ReentrantLock 实例private ReentrantLock lock = new ReentrantLock();publicvoidincrement(){ lock.lock(); // 加锁try { count++; } finally { lock.unlock(); // 必须在 finally 里释放锁,防止死锁 } }publicintgetCount(){return count; }}
ReentrantLock 有三个 synchronized 没有的功能:
| | |
|---|
| | lock.lockInterruptibly() |
| | lock.tryLock(1, TimeUnit.SECONDS) |
| 按线程请求锁的顺序分配锁(先来先得),默认是非公平锁 | new ReentrantLock(true) |
可重入锁
synchronized 和 ReentrantLock 都是可重入锁。
那什么是可重入呢?
可重入的意思就是同一个线程,可以多次获取同一把锁,不会被自己阻塞。
那又为什么需要可重入呢?
举个递归调用的例子:
publicclassReentrantExample{// synchronized 是可重入的publicsynchronizedvoidmethodA(){ System.out.println("执行 methodA"); methodB(); // 调用 methodB,methodB 也需要同一把锁 }publicsynchronizedvoidmethodB(){ System.out.println("执行 methodB"); }publicstaticvoidmain(String[] args){ ReentrantExample example = new ReentrantExample(); example.methodA(); }}
如果锁是不可重入的,那么线程在执行 methodA() 时已经拿到了锁,调用 methodB() 时会因为拿不到锁而被自己阻塞,导致死锁。
synchronized 的锁升级
在 JDK 1.6 之前,synchronized 是重量级锁,性能很差;
JDK 1.6 之后,引入了锁升级机制,提升了 synchronized 的性能。
锁升级的意思是:
随着多线程竞争的加剧,锁会从【无锁】➡️【偏向锁】➡️【轻量级锁】➡️【重量级锁】逐步升级,而且升级是单向的,只能升不能降。
Java对象在内存中分为3部分:
对象头、实例数据、对齐填充,其中对象头里的 Mark Word 会记录锁的状态。
而synchronized 的锁是存放在【对象头】里的。
Mark Word 的结构(32 位 JVM):
锁升级的四个阶段
1. 无锁
没有线程竞争锁,对象处于无锁状态。
2. 偏向锁
只有一个线程多次获取同一把锁,没有其他线程竞争。第一个线程获取锁时,会在对象头的 Mark Word 里记录【偏向线程ID】,这个线程以后再次获取锁时,只需要检查偏向线程ID是不是自己,如果是,直接获取锁。当有第二个线程来竞争这把锁时,偏向锁会撤销,升级为轻量级锁。
3. 轻量级锁
多个线程交替获取锁,但是竞争不太激烈(比如线程A获取锁,执行完释放了,线程B再获取)。
线程在自己的栈帧里创建一个锁记录(Lock Record),用 CAS(Compare And Swap,比较并交换)操作,尝试把对象头的 Mark Word 替换成指向自己栈中锁记录的指针。
如果 CAS 成功,就获取到了轻量级锁。
当有多个线程同时竞争锁,CAS 失败多次,就会升级为重量级锁。
4. 重量级锁
多个线程同时竞争锁,竞争激烈。锁升级为重量级锁后,对象头的 Mark Word 会指向一个monitor(监视器)对象。
没有获取到锁的线程会进入阻塞队列,被操作系统挂起,等待持有锁的线程释放锁后唤醒。