1:ReentrantLock简介
ReentrantLock是java并发包里面的可重入锁,它是基于AQS(AbstractQueuedSynchronizer)实现的,它有两种锁,公平和非公平锁,本文主要用来记录我个人学习的总结。
1.1:ReetrantLock的小Demo说明
public class LockExample {
private ReentrantLock lock = new ReentrantLock(true);
public void modifyResources(String threadName){
System.out.println("通知吴亦凡:--->"+threadName+"准备说唱");
lock.lock();
System.out.println("舞台:--->"+threadName+"第一次说唱");
lock.lock();
System.out.println("舞台:"+threadName+"第二次说唱");
lock.unlock();
System.out.println("舞台:"+threadName+"你不行");
lock.unlock();
System.out.println("舞台:"+threadName+"还是不行");
}
public static void main(String[] args){
LockExample tp = new LockExample();
new Thread(()->{
String threadName = Thread.currentThread().getName();
tp.modifyResources(threadName);
},"Rapper:阿Giao").start();
new Thread(()->{
String threadName = Thread.currentThread().getName();
tp.modifyResources(threadName);
},"Rapper:药水哥").start();
}
}
我写了一个Rapper小demo代码很简单就不介绍了,下面为运行结果,可以发现阿Giao必须要释放锁后,吴亦凡才会去听药水哥说唱。
通知吴亦凡:--->Rapper:阿Giao准备说唱
舞台:--->Rapper:阿Giao第一次说唱
通知吴亦凡:--->Rapper:药水哥准备说唱
舞台:Rapper:阿Giao第二次说唱
舞台:Rapper:阿Giao你不行
舞台:Rapper:阿Giao还是不行
舞台:--->Rapper:药水哥第一次说唱
舞台:Rapper:药水哥第二次说唱
舞台:Rapper:药水哥你不行
舞台:Rapper:药水哥还是不行
2:ReentrantLock lock = new ReentrantLock(true)创建一个公平锁
当我们创建lock的时候,给它带上参数true则会创建一个公平锁,反之如果传false则为非公平锁。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
我们可以发现一个特别的类sync那么,这个类是什么呢?我们进入到代码看看
2.1:sync类
可以发现该类继承了AbstractQueuedSynchronizer也就是AQS类,到这里我们找到了ReentrantLock里面最重要的类也是我们需要学习的一个类。
abstract static class Sync extends AbstractQueuedSynchronizer
2.2:AbstractQueuedSynchronizer学习
这里附上lock所有的参数,先从外层解析一下,首先lock有4个参数
1:head#头部节点
2:tail#尾部节点
3:state#加锁次数
4:exclusiveOwnerThread#当前的加锁线程
整体的参数构成就是这样。
接下来主要讲一下sync里面的head和tail,要想讲清楚它们首先要来认识一下CLH队列,这个队列主要就是负责记录阻塞线程,以及线程节点的状态。
进入到Sync里面我们有几个重要的参数负责记录信息,主要在内部类Node中
1:waitStatus#队列里面线程的状态分为(-3,-2,-1,0,1)
2:prev#前驱节点
3:next#后驱节点
4:thread#节点同步状态的线程
5:nextWaiter#等待队列中的后继节点
waitStatus的状态类型分别对应
static final int CANCELLED = 1;#线程等待超时或者被中断
static final int SIGNAL = -1;#后继节点的线程处于等待状态
static final int CONDITION= -2;#节点在等待队列中,节点的线程等待在Condition上
static final int PROPAGATE= -3;#表示下一次共享式同步状态获取将会被无条件地传播下去
3:加锁过程
final void lock() {
acquire(1);
}
通过调用lock方法进入获取锁
public final void acquire(int arg) {
#首先尝试获取锁如果获取不到尝试将线程加入队列
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) {
#判断当前节点是否有前驱节点
#如果没有前驱节点,则通过CAS比较将加锁次数置为1
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//将锁给当前线程
setExclusiveOwnerThread(current);
return true;
}
}
#如果当前线程等于拥有锁的线程
else if (current == getExclusiveOwnerThread()) {
#将锁的次数加1(也就是为什么我们截图锁的次数为2的原因)
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
public final boolean hasQueuedPredecessors() {
#获取尾节点
Node t = tail; // Read fields in reverse initialization order
#获取头结点
Node h = head;
Node s;
#如果尾节点与头结点相等,队列为空队列
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
到这里如果我们获取锁失败了则会进入队列方法
private Node addWaiter(Node mode) {
#构造一个当前线程的Node节点
Node node = new Node(Thread.currentThread(), mode);
#获取尾部节点Node
Node pred = tail;
#判断尾部节点是否为空
if (pred != null) {
#将当前节点的前驱节点置为当前队列尾部节点
node.prev = pred;
#通过CAS比较算法将尾部节点与当前节点替换实现尾部插入当前节点
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) { // Must initialize
#创建空的头节点
if (compareAndSetHead(new Node()))
#尾部同时指向该空node(类似于一个空节点被head和tail指向)
tail = head;
} else {
#当前传入节点的前驱节点指向head也就是刚才的空节点
node.prev = t;
#CAS设置尾部节点
if (compareAndSetTail(t, node)) {
#前驱节点的next指针指向当前节点
t.next = node;
return t;
}
}
}
}
到此线程已经成功被加入到了CHL队列当中
#尝试阻塞队列
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
#自旋死循环
for (;;) {
#获取当前节点的前驱节点
final Node p = node.predecessor();
#如果当前节点的前驱节点是head节点证明队列中我就是下一个执行的线程,尝试获取锁
if (p == head && tryAcquire(arg)) {
//获取成功,将当前结点设置为头结点。
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
#如果当前节点前驱节点不是head,则尝试阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
#获取当前节点的前驱节点的状态值
int ws = pred.waitStatus;
#若前驱结点是SIGNAL也就是-1,意味着当前结点可以被安全地park
if (ws == Node.SIGNAL)
return true;
#若前驱节点的状态值为CANCELLED -1则前驱节点可能中断或超时了需要移除
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
#如果尾0或者其他状态会被现行替换为-1然后在呗阻塞
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
接下来让我们看看解锁过程,以及怎么样唤醒CHL中的节点取拿锁,其实在阻塞的方法中,线程一直在死循环中,也就是只能获取到锁才会跳出死循环。
4:解锁过程
public final boolean release(int arg) {
#尝试解锁
if (tryRelease(arg)) {
#获取头结点
Node h = head;
#如果头结点不为空,并且头结点的状态不为0,所有结点初始为0
if (h != null && h.waitStatus != 0)
#解锁当前的头结点,也就是这次线程结束后,后面队列的线程
unparkSuccessor(h);
return true;
}
return false;
protected final boolean tryRelease(int releases) {
#当前锁的数量-1
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) {
#获取头结点的状态
int ws = node.waitStatus;
if (ws < 0)
#将头结点的状态置为0
compareAndSetWaitStatus(node, ws, 0);
#获取头结点的下一个节点
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);
}
到此解锁过程结束