AQS AbstractQueuedSynchronizer同步器,这也是ReentranLock实现锁的基础,如果不知道AQS,就很难理解锁到底是怎么实现的,当然这其中不包括通过synchronize获取的锁,当然我们入手点当然是ReentrantLock,通过ReentranLock lock = new ReentrantLock();
一般来讲我们通过lock.lock()来获取锁,
public void lock() { sync.lock(); } |
他调用的是同步器的lock方法,所以下面我们看看到底是怎样实现的
private final Sync sync; static final class NonfairSync extends Sync public ReentrantLock() { |
所以根据上面的代码可以知道,在默认构造方法里,实例化的非公平同步器NonfairSync,lock里调用的是nonfairSync里面的lock方法,
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } |
首先他会通过CAS来吧state从0变成1,如果成功了,就说明以前state的状态为0,由于state是有volatile修饰的,对其他线程是可见的,一修改,别的线程就知道,一旦compareAndSetState(0, 1)返回的是true,意味着其中线程不会执行setExclusiveOwnerThread(Thread.currentThread());知道state变为0,所以执行setExclusiveOwnerThread(Thread.currentThread());是线程安全安全的,而该方法只要是记录当前线程为执行线程。
如果compareAndSetState(0, 1)返回false就会走acquire(1);这个方法是AbstractQueueSynchronizer的
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } |
首先会判断!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg),首先会执行tryAcquire(1)
这个方法在nonfairSync里面
protected final boolean tryAcquire(int acquires) { final boolean nonfairTryAcquire(int acquires) { |
他会执行nonfairTryAcquire,然后再一次尝试获取锁,当然如果有线程获取锁,这里hi不可能成功的,如果正好有线程执行完,他就能获得锁,不用去等待队列了,然后会判断当前线程是不是正在执行独占锁的线程,如果是就是锁重入,虽然此时state大于0,但是还是会加1,然后返回true,这样的!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)就是false,acquire方法就不会干啥了,如果state != 0,而且此时执行的线程不是当前线程,就会直接返回false,这样的话就会走acquireQueued(addWaiter(Node.EXCLUSIVE), 1);首先他会执行addWaiter(Node.EXCLUSIVE)。
|
把当前线程包装成一个节点,如果当前等待队列的尾节点不为空,并把节点插入等待队列末尾。
在多多线程环境下,就会有两种情况没有解决,一种是首尾节点都为null,另一种是多个线程执行到node.prev = pred;这就会造成多个节点的pred指向尾节点,但是只有一个成功,还有许多没有成功的,这两种情况就会进入enq(node);
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } |
在这个方法里,就会干两件事,对于首尾节点为空的情况,就会通过一个空节点把首尾节点关联起来,然后进行第二步,第二步就是通过无限循环的情况,把节点插入等待队列的末尾,这样就解决的上述的两种情况。然后把现在尾节点返回。
然后就会执行acquireQueued(node,1)当然,第一个参数就是尾节点,
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)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } |
如果尾节点是首节点的下一个节点,就会再一次的获取锁,如果能获取锁,等待队列就不需要记录该节点的内容,就会把该节点设置为当前首节点,然后把前首节点的引用都设置为空,好让他被jvm回收,然后返回false,
如果当前尾节点不是首节点的下一个节点,就会进入
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } |
判断当前线程包装的结点的前节点的waitStatus,如果前节点的等待状态是SIGNAL,就返回true,当他的状态大于0,就会把前节点干掉,直接把前节点的前节点和当前线程的包装节点关联起来,如果前面都没有走前面的,就会把前节点的等待状态改为SIGNAL,其实只要前节点的等待状态不为SIGNAL,就会返回false。只要前节点的等待状态为SIGNAL,就进入parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } |
把当前线程进入阻塞,返回他的中断标志,如果中断了,就会取消该线程,不会执行。
总结
当一个线程在访问Lock的同步代码块是,首先会去尝试获取锁(CAS把state从0设置为1,如果成功,就会把当前线程设置为独占线程,获取执行权。CAS失败的话就会执行acquire(1),这是线程第一次尝试获取锁),没有获取锁就会执行acquire(1),首先线程会再次尝试获取一次锁tryAcquire(1)(CAS设置state,成功把当前线程设置为独占,如果失败,就会判断是不是重入,重入就会state++,这是第二次尝试获取锁),如果失败就会把他加入到等待队列。
加入到等待队列,当前节点不为空,就会先把当前节点的前节点引用设置为tail,通过CAS把当前节点设置为尾节点。不成功就会执行enq方法,在enq方法里,定义了当head=null的情况,包括了第一次加入节点的情况,他会创造一个空节点来连接首尾节点,同时通过无限死循环来吧节点放进队列里。放进队列的节点不会立刻阻塞。会执行tryqueue(node,1)方法。如果当前的节点的前节点是首节点,就会再一次的获取锁tryacqire()。(这是可能发生的第三次尝试获取锁),成功就返回false,失败的话吧那些没有中断的线程阻塞。