目录
FairSyn公平锁 = lock() + tryAcquire();这两个方法互相利用,lock中使用了tryAcquire();
2-1-1 进到acquire方法里的tryAcquire(1)
2-1-2-1 进到acquire方法里的addWaiter(Node.EXCLUSIVE,1)
2-1-2-3 进到acquire方法里的acquireQueued(1)
2-1-3 进acquire方法里的selfInterrupt()
ReentrantLock简介
ReentrantLock是JUC包下的最常见的锁。
本篇文章由使用到原理分析ReentrantLock:以公平锁为例
ReentrantLock的使用
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
//被锁住的仅允许一个线程访问的业务代码块
reentrantLock.unlock();
三行,起到的作用就是: synchronized(){ //被锁住的仅允许一个线程访问的业务代码块 }
那么问题来了:
- ReentrantLock如何实现synchronized的功能的?
- ReentrantLock又比synchronized好或者差在哪里呢?
ReentrantLock底层实现
首先带着问题
前提:如果同时来了1000个线程T0,T1,T2,T3....同时走到第二行reentrantLock.lock();
- 如何做到只有一个放行?
- 放行的这个线程执行的时候,其他线程在干什么?
- 放行的线程执行完毕,如何通知其他线程的?
- 那么多线程谁来第二个执行下面代码?
其次带着猜测
- 做个加锁成功的判断,谁拿到锁,谁放行。难道用synchronize?
- 其他线程可能一直持续试探拿锁(1000个线程开销实在太大),那睡眠等着被唤醒(睡多久呢?),可能让出CPU(让给谁呢?),可能阻塞在那里等着解封(什么时候解封?)。
- 可以修改个状态,或者解封/唤醒其他全部/指定线程再次竞争锁。
- 按排队顺序先来后到(那需要个先进先出的队列存储线程们?)?还是随机?
ReentrantLock源码
就看开篇那三行!
1. new ReentrantLock();
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
意味着:
- 直接new ReentrantLock(); 是NonfairSync非公平锁。
- 带参new ReentrantLock(true); 可得FairSyn公平锁。
什么是公平锁,什么是非公平锁?
- 公平:排队获取锁;
- 非公平:插队获取锁;
FairSyn公平锁 = lock() + tryAcquire();这两个方法互相利用,lock中使用了tryAcquire();
static final class FairSync extends Sync {
final void lock() {
acquire(1);
//这里的acquire方法如下
/**public final void acquire(int arg) {
* //这里的tryAcquire在下面那段
* if (!tryAcquire(arg) &&
* acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
* selfInterrupt();
*}
*/
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
}
FairSync 公平锁源码 大致翻译成人话
公平锁类继承Sync类 {
加锁方法{
有个神奇隐秘的方法;
}
尝试加锁设置为的方法{
抓住当前线程,命名current线程
获取状态值,命名为c
如果c状态值是0就进这里{
如果 ( 队列里没有东西 && CAS把0给写成1 )
把current线程设置为独家占有线程
返回成功
}
}
如果c状态值不是0 并且current线程 是 独家占有线程 就{
状态值+1
如果加完一小于零抛异常
如果加完一不小于零再把状态设置为加一后的那个数
返回成功
}
前面两个如果都没满足,走到这步直接返回失败
}
}
涉及到的state是什么?
//同步状态。
private volatile int state;
//返回当前状态
protected final int getState() {
return state;
}
//如果当前状态值等于期望值,则自动将同步状态设置为给定的更新值
protected final boolean compareAndSetState(int expect, int update) {
// 这里的unsafe.compareAndSwapInt是C++代码
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
总结一下重点:
- 那个隐秘的方法重点分析。
- tryAcquire()方法像是辅助,负责独占这件事(就不管他了!)。
2. 重点lock()方法分析
2-1 进到 lock()里唯一的acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
2-1-1 进到acquire方法里的tryAcquire(1)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
解释:
- 试图以独占模式获取。这个方法应该查询对象的状态是否允许以独占模式获取它,如果允许就获取它。
- 执行acquire的线程总是调用此方法。如果此方法报告失败,则acquire方法可能会将尚未排队的线程排队,直到其他线程发出释放信号。这可以用来实现Lock.tryLock()方法。
- 默认实现抛出UnsupportedOperationException。
参数:
- Arg—获取参数。这个值总是传递给acquire方法的值,或者是在进入wait条件时保存的值。否则,该值是未解释的,可以表示您喜欢的任何值。
返回:
- 如果成功。一旦成功,这个对象就被获得了。
- 如果失败:IllegalMonitorStateException -如果获取会将此同步器置于非法状态。必须以一致的方式抛出此异常,才能使同步正常工作。
- UnsupportedOperationException -如果排他模式不被支持
总结来说公平锁这里一定返回false,再取反。
2-1-2-1 进到acquire方法里的addWaiter(Node.EXCLUSIVE,1)
private Node addWaiter(Node mode) {
//当前线程构建一个节点
Node node = new Node(Thread.currentThread(), mode);
//尾插法将当前线程CAS方式插入队列尾部
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
总结来说:想得到锁,先进到队列queue中去!
队列什么样子?
这样一个队列,head出去,进来放tail,保证原子操作,保证先进先出实现公平
+------+ prev +-----+ +-----+
head | | <---- | | <---- | | tail
+------+ +-----+ +-----+
- 这一个一个node节点是有thread构成的。 部分解答了第二个问题:放行的这个线程执行的时候,其他线程在干什么?未竞争到锁的线程们,进到了一个队列
2-1-2-2 借此介绍一下node
/**
* 不管是条件队列,还是CLH等待队列
* 都是基于Node类
*
* AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人
* 发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列,Java中的
* CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。
*/
static final class Node {
/** 标记节点未共享模式*/
static final Node SHARED = new Node();
/** 标记节点为独占模式 */
static final Node EXCLUSIVE = null;
/** 在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待 */
static final int CANCELLED = 1;
/** 后继节点的线程处于等待状态,而当前的节点如果释放了同步状态或者被取消,
* 将会通知后继节点,使后继节点的线程得以运行。 */
static final int SIGNAL = -1;
/** 节点在等待队列中,节点的线程等待在Condition上,
*。 当其他线程对Condition调用了signal()方法后,
* 该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中 */
static final int CONDITION = -2;
/** 表示下一次共享式同步状态获取将会被无条件地传播下去 */
static final int PROPAGATE = -3;
/**
* 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态
* 使用CAS更改状态,volatile保证线程可见性,高并发场景下,
* 即被一个线程修改后,状态会立马让其他线程可见。
*/
volatile int waitStatus;
/** 前驱节点,当前节点加入到同步队列中被设置 */
volatile Node prev;
/** 后继节点 */
volatile Node next;
/** 节点同步状态的线程 */
volatile Thread thread;
/**
* 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量,
* 也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段。
*/
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回前驱节点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//空节点,用于标记共享模式
Node() { // Used to establish initial head or SHARED marker
}
//用于同步队列CLH
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//用于条件队列
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
/**
2-1-2-3 进到acquire方法里的acquireQueued(1)
/**
* 已经在队列当中的Thread节点,准备阻塞等待获取锁
*/
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)) {//如果前驱结点是头结点,才tryAcquire,其他结点是没有机会tryAcquire的。
setHead(node);//获取同步状态成功,将当前结点设置为头结点。
p.next = null; // help GC
failed = false;
return interrupted;
}
/**
* 如果前驱节点不是Head,通过shouldParkAfterFailedAcquire判断是否应该阻塞
* 前驱节点信号量为-1,当前线程可以安全被parkAndCheckInterrupt用来阻塞线程
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
总结来说:如果是头节点就去tryAcquire(1-2中介绍),否则被park阻塞。
-
parkAndCheckInterrupt方法中:LockSupport.park(this);
至此第二个问题:放行的这个线程执行的时候,其他线程在干什么?未竞争到锁的线程们,进到了一个尾插法的队列(先进先出),并且被park阻塞 。
2-1-3 进acquire方法里的selfInterrupt()
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
中断当前线程
总结一下破案线索:
- 如何做到只有一个放行?
- 放行的这个线程执行的时候,其他线程在干什么?
- 放行的线程执行完毕,如何通知其他线程的?未知
- 那么多线程谁来第二个执行下面代码?
答案:分公平和非公平两种情况:公平锁时:多线程执行到lock方法时会尾插法进到队列中死循环获取锁,如果是头节点则有机会用CAS修改同步器状态+1,并获取锁成功,放行执行下面代码,而其他线程则会被park阻塞;
3. unlock();方法
/**
* 尝试去释放锁
*/
public void unlock() {
sync.release(1);
}
3-1unlock方法中的release方法
/**
* 释放独占模式持有的锁
*/
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;
}
private void unparkSuccessor(Node node) {
//获取wait状态
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);// 将等待状态waitStatus设置为初始值0
/**
* 若后继结点为空,或状态为CANCEL(已失效),则从后尾部往前遍历找到最前的一个处于正常阻塞状态的结点
* 进行唤醒
*/
Node s = node.next; //head.next = Node1 ,thread = T3
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);//唤醒线程
}
总结一下破案线索:
除去独占锁的考虑,unlock就是LockSupport.unpark(s.thread);//唤醒下一节点线程
总结:
主要功能 | 主要内容 | |
---|---|---|
new ReentrantLock() | 可实现公平/非公平锁 | state同步状态;queue队列;CAS; |
lock(); | 竞争锁 | CAS标识同步状态表示获取锁的状态, queue队列尾插,只有头节点才有机会竞争锁,其他未获得锁的线程会park阻塞 |
unlock(); | 唤醒下一节点的线程 | 找到队列头节点unpark |