一、可重入锁
可重入锁是指同一个线程连续调用lock.lock()不会被阻塞。重入锁的一种实现方式是为锁关联一个计数器和拥有线程。当计数器为0时,表示锁没有被任何线程占有。初次获取锁时,锁时计数器加1,并且拥有线程设置为当前线程。释放锁时计数器减1,当计数器为0时释放锁并清空关联线程。java 的synchronized 和ReentrantLock都是可重入锁。
二、独占锁,共享锁
- 独占锁
独占锁是指同一时间锁只能被一个线程占有,任何竞争锁的线程都会失败,直至持有锁的线程释放资源,独占锁也被称为排他锁。synchronized是独占锁。
- 共享锁
共享锁是指同一时间可以被多个线程占有,假设锁最多允许n个线程共享,当线程竞争锁如果持有锁的线程数小于n,则线程可以获取共享锁,当持有锁的线程数为n时,获取锁的线程需要排队,直至持有锁的线程释放资源。
三、锁降级
这里的锁降级并不是指synchronized的锁降级,synchronized只能按照偏向锁–>轻量级锁–>重量级锁升级。事实上锁降级和synchronized的锁升级也不是一个维度的。
读写锁:读锁与读锁之间共享,读写之间排他。
锁降级是指写锁降级成读锁。如果当前线程获取到了写锁,随后释放了写锁,然后获取到读锁,这个过程不是锁降级。锁降级是指线程拥有写锁,获取读锁,释放写锁的过程。
四、J.U.C源码分析
Lock接口是1.5开始出现的,由Java并发大师Doug Lea撰写,主要记录下AbstractQueuedSynchronizer,ReentrantLock,ReentrantReadWriteLock的源码学习记录。
1. AbstractQueuedSynchronizer
几乎所有Lock下锁的实现都聚合了一个AbstractQueuedSynchronizer(以下称AQS)实现自己特定的功能,Doug Lea也希望AQS能应用于大部分自定义锁的场景,通常只需要定义一个静态内部类Sync继承AQS,根据具体的需求重写相应的模板方法,如tryRelease(释放锁),tryAcquire(获取锁)等方法就够了。AQS没有实现任何Lock相关的接口,仅提供了共享锁,独占锁获取的模板方法,内部持有一个类似CLH的队列,用来存放等待获取锁的线程(当然如果锁竞争不激烈,就没有这个队列),维护线程的状态,及
当线程获取不到锁时,会创造一个Node节点,从尾部进入AQS的同步队列,独占锁和共享锁共用一个队列。当入队时,为了保证线程安全,采用自旋+CAS更新尾节点的方式。出队时,直接简单地更新头节点就行了,因为出队的Node(线程)必然是持有锁的线程。AQS内部的Node类主要的实例域:
// 当前节点的状态
volatile int waitStatus;
// 当前node的前驱节点
volatile Node prex;
// 当前node的后继节点
volatile Node next;
// 当前节点线程对象
volatile Thread thread;
// 表明当前节点模式,或用于Condition
Node nextWaiter;
Node状态 waitStatus说明:
状态 | 值 | 说明 |
---|---|---|
CANCELLED | 1 | 由于中断或者等待超时,线程取消了对锁的争夺,马上要脱离等待队列了 |
SIGNAL | -1 | 后继节点的线程处于等待状态,前面的线程释唤醒它 |
CONDITION | -2 | 节点在等待队列中,线程等待在Condition上,当Condition#signal()或signalAll()会将节点从等待队列转移到同步队列中 |
PROPAGATE | -3 | 下一次的共享状态会无条件的传播下去。 |
Node在AQS同步队列结构
AQS内部存储了两个节点:头节点、尾节点,Node两个指针(prev、next)共同构成了一个队列。头尾节点默认延迟初始化,当第一次有节点入队时进行初始化。获取锁的方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg)需要由子类重写,且tryAcquire方法要求是非阻塞的、线程安全的、实现尽量简单。当子类获取不到锁时会入队。方法如下:
addWaiter(Node.EXCLUSIVE):构造Node节点并加入等待队列
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速插入,如果tail尾节点不为空直接尝试CAS从尾部插入新的节点
// 如果失败说明 尾节点还没初始化或有其他线程参与竞争需要自旋插入直至成功
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
// 快速插入失败会自旋插入
private Node enq(final Node node) {
// 代表线程会直至插入成功才会退出
for (;;) {
Node t = tail;
if (t == null) {
// 初始化头尾节点,CAS失败了也没关系,
// 其他线程已经初始化好了
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued(addWaiter(Node.EXCLUSIVE), arg),加入成功后会自旋获取锁,并不会一直自旋消耗CPU,如果满足条件shouldParkAfterFailedAcquire(p, node),会调用LockSuport#park();使当前线程进入等待状态。当线程被唤醒时,有可能是头节点释放锁,也有可能是前驱节点因为中断或者取消离开队列。当线程被唤醒时,如果前驱节点是头节点,则会开始竞争锁;否则检查,前驱节点prex是否被取消了,prex会跳过取消的节点 “挂在”最近的一个未取消的节点上面,以便前面的兄弟节点释放锁时能够成功的唤醒它。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取前驱节点,只有当前node的前驱节点是头节点时才会参与锁的竞争,FIFO
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// shouldParkAfterFailedAcquire 从方法名可以看出来,
// 是否需要在此次获取锁失败的时候park当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 如果当前节点的前驱节点已经被设置为SIGNAL(-1)时,可以安心的park了,
// 前面的线程会在合适的时候唤醒它。
return true;
if (ws > 0) {
// 状态大于0,说明当前的线程是CANCELLED (1)前驱节点就要离队了
// 这个时候就不能安心的park了,需要做点事情,前驱节点指向第一个没有被取消的node
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 说明是 0 或 -3(前节点没有初始化或者其他情况),需要设置为-1
// 使得前节点能够叫醒自己
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 会进行acquireQueued 方法中的自旋,如果前驱节点已经是头节点了,
// 就可以争夺锁了,否则下一次进入此方法就有能被park()而进入等待了{
return false;
}
释放锁:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
//
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 后继节点不为空则唤醒后继节点
// 否则会从tail尾部找一个没有被取消的node(线程)进行唤醒
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
AQS获取锁的流程总结:
![](https://i-blog.csdnimg.cn/blog_migrate/dfe8a0a5bb9717c91e0824a93886e30f.png)
总结:
- 获取锁失败会构造一个node节点插入至同步队列尾部
- 如果前驱节点是头节点则会参与锁的竞争,如果不是则会将其前驱节点状态改为-1,保证前驱节点释放锁或者取消时能够唤醒它。(其实这种说法并不严谨,当释放锁时后继节点如果被取消了,则会从tail尾部遍历唤醒一个非取消状态的节点(1),不过能保证前驱节点设置为-1,总有线程能够唤醒它。
- 释放锁时有需要则会唤醒一个线程,如果后继节点没有取消,则是后继节点,否则会从尾部找一个非取消状态的线程,来唤醒它。这并不违反FIFO,因为唤醒的线程会判断前驱节点是否为头节点,如果不是则会继续睡眠,这次唤醒只是通知它前面有节点取消了,prev->prev这个链需要跳过这些取消的节点。
- 共享锁和排他锁流程相似,这里就不分析了
2. ReentrantLock
可重入锁式独占锁,内部聚合了一个自定义的Sync继承了AQS实现了自己的功能。默认是非公平锁,公平锁是严格的FIFO,先获取锁的线程一定开获取到锁,这样会损失一些行能,因为获取锁的时候要判断是否有更早的线程获取锁,有的话没有竞争资格,在后面排队。非公平锁则不管这些,线程来了直接参与竞争,一定程度上避免了线程的切换,性能更好。
分析下可重入的实现原理:
// 非公平锁获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 获取锁的线程 是否和当前线程相等 是的话就将AQS的status +1
// 释放锁的时候也不直接释放,判断 status 为 1 时才进行释放
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;
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 非公平锁会判断有没有 ‘前辈’在等着,有的话!hasQueuedPredecessors()为
// false ,直接丧失竞争资格,去AQS的同步队列后面排队
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
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) {
// 前 n - 1次都不释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
3. ReentrantReadWriteLock
待更新(2020-08-08 17:41:00)