文章目录
- 1、管程
- 2、AQS
- 3、ReentrantLock
- 3.1、lock/unlock
- 3.1.1、lock
- 3.1.2、unlock
- 3.2、一些思考
1、管程
什么是管程?
管理协调多个线程对共享资源的访问,是一种高级的同步机制。
有哪些管程模型?
hansen:唤醒其他线程的代码必须在当前线程的最后执行,以确保其他线程被唤醒时,当前线程已经执行完。
hoare:唤醒其他线程的代码可以在任意位置,且唤醒其他线程后,当前线程立即阻塞,当被唤醒线程执行完后,再继续执行当前线程。
mesa:唤醒其他线程的代码可以在任意位置,且被唤醒线程不会立即执行,而是加入一个队列,当当前线程执行完后,再从队列中获取并执行其他线程。(需要注意的是,由于存在时间差,所以真正执行的时候有可能已经不满足条件)
其中最常用的管程模型就是mesa,在java中实现的也是该模型。如下图是AQS的实现:
其实管程模型在java里的实现不止有AQS,synchronized 在jvm的底层实现像什么 cxq,entryList,waitSet也是mesa管程模型中的概念,同理object.wait()/object.notify()也是管程模型的实现。
2、AQS
所以具体什么是aqs?
aqs就是java代码对管程模型的一个抽象实现。把volatile state字段定义成共享资源,并且实现了同步等待队列和条件等待队列的入队/出队以及线程的阻塞/唤醒等公共操作,至于具体锁的获取/释放则交由各自实现类,不同的实现类可以定义不同的锁,比如公平锁/非公平锁,重入锁/不可重入锁,独占锁/共享锁等以满足不同的场景。
简单的看个印象
共享资源:
同步等待队列:
条件等待队列:
获锁:
释放锁:
阻塞:
唤醒:
3、ReentrantLock
AQS最经典的实现莫过于 ReentrantLock。接下来看看是 ReentrantLock 如何通过AQS实现 lock/unlock 的。
3.1、lock/unlock
3.1.1、lock
AbstractQueuedSynchronizer.acquire
获取共享资源(aqs实现)
ReentrantLock .tryAcquire
获锁逻辑(ReentrantLock 自己实现的获锁方法)
在ReentrantLock中实现了公平/非公平两种获锁方式(默认为非公平)
以为非公平为例:
@ReservedStackAccessfinal boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {//cas修改共享资源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;}
AbstractQueuedSynchronizer.addWaiter
获锁失败后入队同步队列(aqs实现)
private Node addWaiter(Node mode) {Node node = new Node(mode);for (;;) {Node oldTail = tail;if (oldTail != null) {//cas加入队列node.setPrevRelaxed(oldTail);if (compareAndSetTail(oldTail, node)) {oldTail.next = node;return node;}} else {//初始化队列initializeSyncQueue();}}}
AbstractQueuedSynchronizer.acquireQueued
入队后阻塞之前,根据前节点的状态进行一定次数的自旋获锁(aqs实现)
final boolean acquireQueued(final Node node, int arg) {boolean interrupted = false;try {//这个循环保证一定会获取到锁for (;;) {final Node p = node.predecessor();//如果前一个节点是头结点,再次尝试获锁if (p == head && tryAcquire(arg)) {//如果获锁成功就出队头结点setHead(node);p.next = null;return interrupted;}//如果获锁失败或者前节点不是head的节点就根据前节点的状态来看是否需要阻塞//需要阻塞就调用 LockSupport.park() 阻塞线程if (shouldParkAfterFailedAcquire(p, node))interrupted |= parkAndCheckInterrupt();}} catch (Throwable t) {cancelAcquire(node);if (interrupted)selfInterrupt();throw t;}}private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;//如果前驱节点状态为 SIGNAL 直接返回 true 阻塞线程if (ws == Node.SIGNAL)return true;//如果前驱节点状态大于0,说明线程已被返回,剔除无用节点前驱节点if (ws > 0) {do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} //cas替换前驱节点状态为 SIGNALelse {pred.compareAndSetWaitStatus(ws, Node.SIGNAL);}return false;}private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}
也就是说,获锁失败进入同步等待队列进行阻塞,而在实际阻塞之前会自旋再次尝试获锁。
实际上在自旋途中只有前节点是head的节点才会尝试获锁
如果前节点的 waitStatus 为 signal 则停止自旋
为避免无限自旋,自旋的同时会尝试修改前节点的状态为 signal
3.1.2、unlock
AbstractQueuedSynchronizer.release
释放共享资源(aqs实现)
h.waitStatus == 0 说明后面没有等待唤醒的节点
ReentrantLock .tryRelease
释放锁逻辑(ReentrantLock 自己实现的释放锁方法)
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);}//直接修改state值(不需要cas或者什么操作,因为释放锁不可能有并发)setState(c);return free;}
AbstractQueuedSynchronizer.unparkSuccessor
唤醒节点(aqs实现)
private void unparkSuccessor(Node node) {//清除node节点的waitStatus,重置为0int ws = node.waitStatus;if (ws < 0)node.compareAndSetWaitStatus(ws, 0);//如果node节点的后继节点被取消或者为空,就从尾部向前遍历找到实际的未取消后继节点。Node s = node.next;if (s == null || s.waitStatus > 0) {s = null;for (Node p = tail; p != node && p != null; p = p.prev)if (p.waitStatus <= 0)s = p;}//LockSupport.unparkif (s != null)LockSupport.unpark(s.thread);}
3.2、一些思考
1、公平锁和非公锁的区别在哪?
2、如何实现可中断的?
3、如何实现可超时的?
4、一开始没有线程获取锁,第一获取锁的线程进来直接获锁成功返回,没有入队操作,如何唤醒后继的线程?
答:虽然获锁线程没有入队,但是如果后续有等待线程需要用到队列的话还是会new一个node用于表示之前获锁线程的(相当于之前的获锁线程入队了)。
5、被唤醒的节点如何出队?