Java 锁 实现原理总结
背景
总结一下 java 里面的锁
- Synchronized
- ReentrantLock
- LockSupport
synchronized
Java中的每一个对象都可以作为锁
- 同步普通方法,锁是当前实例对象。
- 同步静态方法,锁是当前类的Class对象。
- 同步方法块,锁是Synchonized括号里配置的对象。
实现原理
同步方法, JVM 采用 ACC_SYNCHRONIZED 标记符来实现同步
方法级的同步是隐式的。同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。
当某个线程要访问某个方法的时候,会检查是否有 ACC_SYNCHRONIZED,
如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。
这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。
值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。同步代码块, JVM采用 monitorenter、monitorexit 两个指令来实现同步
可以把执行 monitorenter 指令理解为加锁,执行 monitorexit 理解为释放锁。
每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0
当一个线程获得锁(执行 monitorenter )后,该计数器自增变为 1,当同一个线程再次获得该对象的锁的时候,计数器再次自增。
当同一个线程释放锁(执行 monitorexit 指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。锁的信息维护在Java头对象里面, Java 对象头包括
- Mark Word
- 指向类的指针
- 数组长度(只有数组对象才有)
这里只重点说一下 Mark Word, Mark Word 记录了对象和锁有关的信息,当这个对象被 synchronized 关键字当成同步锁时,围绕这个锁的一系列操作都和 Mark Word 有关
锁优化
每一个线程在准备获取共享资源时:
- 检查 MarkWord 里面是不是放的自己的 ThreadId, 如果是, 表示当前线程是处于 “偏向锁”
- 如果 MarkWord 不是自己的 ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据 MarkWord 里面现有的 ThreadId,通知之前线程暂停,之前线程将 Markword 的内容置为空
- 两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作把共享对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord
- 第三步中成功执行CAS的获得资源,失败的则进入自旋
- 自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败
- 进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己
ReentrantLock
1 | private final ReentrantLock lock = new ReentrantLock(); |
实现原理
- new ReentrantLock() 初始化
- 公平锁
- 非公平锁
基于 AQS AbstractOwnableSynchronizer
lock.lock() 仅包含非公平锁的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}- 如果之前无线程获取锁,则标志当前线程获取独占锁, 否则,进入 acquire(1)
- tryAquire 的逻辑为 获取 state 值(父类中 volatile 修饰的整型属性)
- 如果 state = 0,则标志当前线程获取独占锁
- 如果 state != 0, 并且获得独占锁的线程是当前线程,则 state 值+1(这也是可重入的原因)
以上条件都返回 true,拿到了锁,可继续往下执行
- 如果没拿到独占锁,则将当前线程包装到 Node 里面,acquireQueued 加入等待队列(Node组成的链表)
并且不断的用 Node 链表的尾节点 tryAcquire 尝试获取锁
lock.unlock()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}unlock 释放锁 tryRelease 对 state 值(父类中 volatile 修饰的整型属性)进行自减操作(重入机制会导致 state 值 > 1)
直到 state = 0 锁完全释放
LockSupport
通过 Unsafe 类里的函数实现的锁
实现原理
1 | public static void park(Object blocker) { |
所有线程共享一个 permit
- park 拿到 permit,获得锁继续执行,其他线程只能阻塞, 因为不支持重入,一个线程多次 park 会一直阻塞下去
- unpark 释放 permit,释放锁,可唤醒其他线程继续执行