本文主要是介绍J.U.C — Locks — ReentrantLock(一),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
整体结构
公平锁和非公平锁
如果获取一个锁是按照请求的顺序得到的,那么就是公平锁,否则就是非公平锁。
公平锁保证一个阻塞的线程最终能够获得锁,因为是有序的,所以总是可以按照请求的顺序获得锁。
不公平锁意味着后请求锁的线程可能在其前面排列的休眠线程恢复前拿到锁,这样就有可能提高并发的性能。
这是因为通常情况下挂起的线程重新开始与它真正开始运行,二者之间会产生严重的延时。因此非公平锁就可以利用这段时间完成操作。这是非公平锁在某些时候比公平锁性能要好的原因之一。
- 初始化
public ReentrantLock() {sync = new NonfairSync(); // 默认是非公平锁
}public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}
- 公平锁 和 非公平锁 : Lock
ReentrantLock lock = new ReentrantLock(true); // 声明公平锁
lock.lock();
try {// method body
} finally {lock.unlock();
}lock.lock() -->// 公平锁: ReentrantLock.FairSyncfinal void lock() {acquire(1);}// 非公平锁: ReentrantLock.NonfairSync(Sync默认)final void lock() {if (compareAndSetState(0, 1)) // 一定程度上不够公平setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}-->// AQSpublic final void acquire(int arg) {if (!tryAcquire(arg) && // 1. 如果tryAcquire(arg)成功,那就没有问题拿到锁;若失败则2acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 创建一个独占的节点(Node.EXCLUSIVE),并插入CLH队尾;自旋获取锁selfInterrupt(); // 3. 因为在获得锁之前线程会被中断,这个地方会清楚中断标记位}-->公平锁1.protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState(); // 获取当前锁的状态if (c == 0) { // 若为0,说明没有线程持有锁if (!hasQueuedPredecessors() && // 若有先序节点,退出;若无则获取锁。 确保公平性,即前面的节点先获取锁compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) { // 若持有锁的线程就是当前线程,则直接state++int nextc = c + acquires; if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}非公平锁1.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) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}-->2.自旋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)) { // 如果当前节点是AQS队列的头结点(如果第一个节点是DUMP节点也就是傀儡节点,那么第二个节点实际上就是头结点了),就尝试在此获取锁tryAcquire(arg)。如果成功就将头结点设置为当前节点(不管第一个结点是否是DUMP节点),返回中断位setHead(node);p.next = null; // help GC ?没有把node的prev指针设为null,怎么GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}Node node = new Node(Thread.currentThread(), mode);Node pred = tail;if (pred != null) { // 若队列不为空,则将当前节点设为tailnode.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) { // Must initializeif (compareAndSetHead(new Node())) // Dump Nodetail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}/*** CANCELLED = 1: 节点操作因为超时或者对应的线程被interrupt。节点不应该留在此状态,一旦达到此状态将从CHL队列中踢出。* SIGNAL = -1: 节点的继任节点是(或者将要成为)BLOCKED状态(例如通过LockSupport.park()操作),因此一个节点一旦被释放(解锁)或者取消就需要唤醒(LockSupport.unpack())它的继任节点。* CONDITION = -2:表明节点对应的线程因为不满足一个条件(Condition)而被阻塞。* 0: 正常状态,新生的非CONDITION节点都是此状态。*/private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL)return true;if (ws > 0) { // 如果前一个节点的等待状态waitStatus>0,也就是前一个节点被CANCELLED了,那么就将前一个节点去掉,递归此操作直到所有前一个节点的waitStatus<=0do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else { // 前一个节点等待状态waitStatus=0,修改前一个节点状态位为SINGAL,表示后面有节点等待你处理compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 这个地方没有返回true,即中断,是为了执行下一次循环先尝试获取锁return false;}// 3. static void selfInterrupt() {Thread.currentThread().interrupt();}
公平锁和非公平锁:UnLock
lock.unlock(); -->// AQSpublic final boolean release(int arg) {if (tryRelease(arg)) { // 1. 先尝试释放Node h = head;if (h != null && h.waitStatus != 0) // 这个是前面将节点状态设为signal的原因,如果不设置的话,在这个地方释放锁后不会唤醒后继节点unparkSuccessor(h); // 2. 唤醒后继节点return true;}return false;}// ReentrantLock.Syncprotected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread()) // 判断持有线程的锁是不是当前线程,若不是,则报异常throw new IllegalMonitorStateException();boolean free = false;if (c == 0) { // 若状态位为0,表示没有线程持有锁,将独自线程清空;若不等于0,可能是一个线程多次获取锁,因为ReentrantLock是可重入锁,所以不等于0的时候,只是将状态位减少,并不清空独占线程,也不唤醒下一个节点free = true;setExclusiveOwnerThread(null);}setState(c);return free;}private void unparkSuccessor(Node node) {int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);Node s = node.next; // 很关键,第一次释放锁的时候,Dump节点在这被跳过去的if (s == null || s.waitStatus > 0) {s = null;for (Node t = tail; t != null && t != node; t = t.prev) // 从tail -> head 找到最后一个有效的节点if (t.waitStatus <= 0)s = t;}if (s != null)LockSupport.unpark(s.thread); // 唤醒节点,唤醒之后的节点会进入acquireQueued操作}
举例
假设有3个线程陆续执行lock.lock():
- 开始状态:
state = 0, 所有的线程都停留在lock前,这里的CPU是指正在执行lock与unlock之间的任务的线程
- thread-1执行lock,由于当前没有线程正在运行,所以thread-1获取锁,直接开始运行,并将state设置1和独占线程设置为thread-1
- thread-2执行lock:
由于当前state为1且是thread-1,故而thread-2进入sync队列
当前sync队列为空,先创建Dump节点,并将head和tail设置为Dump节点;
创建thread-2的节点,并插入队列尾部,并将tail设置为thread-2;
thread-2节点自旋获取锁,同时因为前驱节点的waitstatus为0,更改为-1(Signal),在自旋循环的下一次若还无法获取锁,则中断
- thread-3执行lock:
由于当前state为1且是thread-1,故而thread-3进入sync队列
当前队列不为空,直接将创建的thread-3插入队列,并将tail设置为thread-3;
thread-3节点自旋获取锁,同时由于前驱节点的waitstatus为0,设置为-1(Signal),在自旋循环的下一次若还无法获取锁,则中断
- 若此时threa-1再次执行lock, 只会将state增加1,同时之后释放锁也需要执行相应次数的unlock操作。threa-1执行unlock:
从head.next节点开始遍历,找到第一个waitstatus <= 0的节点唤醒,在这里Dump节点感觉相当于正在运行的线程thread-1;
此时thread-2被唤醒,并执行acquired获取锁,并将Head设置为thread-2
- thread-2执行unlock:
- thread-3执行unlock:
因为thread-3节点的waitStatus为0,故而不会唤醒下一个节点
这篇关于J.U.C — Locks — ReentrantLock(一)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!