【面试真题拆解06】Java锁机制:synchronized、ReentrantLock、锁升级、可重入锁

四季读书网 3 0
【面试真题拆解06】Java锁机制:synchronized、ReentrantLock、锁升级、可重入锁

一句话:

Java 锁机制核心是 synchronized 和 ReentrantLock,两者都是可重入锁;

JDK 1.6+ 后 synchronized 引入锁升级(无锁➡️偏向锁➡️轻量级锁➡️重量级锁),性能大幅提升;

简单场景优先用 synchronized,需要高级功能(比如可中断、可超时、公平锁)时用 ReentrantLock。

为什么需要锁?

在并发场景下,多个线程同时修改同一个共享变量,会出现线程安全问题。

举个例子说明一下:

publicclassUnsafeCounter{privateint count = 0;publicvoidincrement(){        count++; // 这行代码不是原子操作!    }publicintgetCount(){return count;    }}

如果1000个线程同时调用 increment(),最终 count 的值很可能小于1000。

因为 count++ 不是原子操作,它分为读取countcount+1写回count三步,多线程同时执行会互相覆盖。

锁的作用就是把这段代码加锁,让同一时间只有一个线程能执行,从而保证操作的原子性。

synchronized

synchronized 是JVM内置的关键字,不需要手动释放锁。

用法
锁的对象
示例
锁实例方法
锁当前对象 this
public synchronized void increment()
锁静态方法
锁当前类的 Class 对象
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):

锁状态
25位
4位
1位(是否偏向锁)
2位(锁标志位)
无锁
对象的哈希码
分代年龄
0
01
偏向锁
偏向线程ID
分代年龄
1
01
轻量级锁
指向栈中锁记录的指针
00
重量级锁
指向重量级锁(monitor)的指针
10

锁升级的四个阶段

1. 无锁

没有线程竞争锁,对象处于无锁状态。

2. 偏向锁

只有一个线程多次获取同一把锁,没有其他线程竞争。第一个线程获取锁时,会在对象头的 Mark Word 里记录【偏向线程ID】,这个线程以后再次获取锁时,只需要检查偏向线程ID是不是自己,如果是,直接获取锁。当有第二个线程来竞争这把锁时,偏向锁会撤销,升级为轻量级锁。

3. 轻量级锁

多个线程交替获取锁,但是竞争不太激烈(比如线程A获取锁,执行完释放了,线程B再获取)。

线程在自己的栈帧里创建一个锁记录(Lock Record),用 CAS(Compare And Swap,比较并交换)操作,尝试把对象头的 Mark Word 替换成指向自己栈中锁记录的指针。

如果 CAS 成功,就获取到了轻量级锁。

当有多个线程同时竞争锁,CAS 失败多次,就会升级为重量级锁。

4. 重量级锁

多个线程同时竞争锁,竞争激烈。锁升级为重量级锁后,对象头的 Mark Word 会指向一个monitor(监视器)对象。

没有获取到锁的线程会进入阻塞队列,被操作系统挂起,等待持有锁的线程释放锁后唤醒。

抱歉,评论功能暂时关闭!