什么都无法舍弃的人,什么也改变不了!!!
1.ReentrantLock特性
A:支持公平加锁和非公平加锁方式
B:独占模式加锁
C:支持手动解锁
D:支持锁的重入
2.ReentrantLock的核心原理
A:自旋——等待加锁和解锁
B:park——线程阻塞(第一次尝试阻塞不会直接阻塞,而是先标记head节点的waitStatus为-1,为了下一个节点状态为可唤醒状态)
C:cas——保证加锁的原子性
D:队列——先进先出的数据结构,保证加锁的公平性(双向链表实现)
3.ReentrantLock结构
4.什么是CAS操作
Conmpare And Swap 比较与交换:
/** * CAS head field. Used only by enq. */ private final boolean compareAndSetHead(Node update) { return unsafe.compareAndSwapObject(this, headOffset, null, update); } /** * CAS tail field. Used only by enq. */ private final boolean compareAndSetTail(Node expect, Node update) { return unsafe.compareAndSwapObject(this, tailOffset, expect, update); } /** * CAS waitStatus field of a node. */ private static final boolean compareAndSetWaitStatus(Node node, int expect, int update) { return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update); } /** * CAS next field of a node. */ private static final boolean compareAndSetNext(Node node, Node expect, Node update) { return unsafe.compareAndSwapObject(node, nextOffset, expect, update); }
上述代码,都是AQS中的CAS方法,调用的都是Unsafe类中的方法,都是native方法,原理就不深纠了,这个是CPU硬件实现。
一共四个参数,第一个参数是需要CAS操作的对象(目标对象),第二个参数目标对象的属性偏移量(jvm的安全手段,获取的是属性在元空间的地址段),第三个参数比较值(与目标对象的属性的内存值做比较的值),第四个参数期望值(如果目标对象的属性的值,跟第三个参数一样,那么就将目标对象的属性的内存值改为期望值)。
这些方法在高并发场景下,只有一个会更改成功,保证了原子操作。
5.AQS的CLH队列实现
private Node addWaiter(Node mode) { //入队列方法 Node node = new Node(Thread.currentThread(), mode); //构建一个新的节点,绑定当前线程,第二个参数此时是null,也就是说这个节点的nextWaiter是null // Try the fast path of enq; backup to full enq on failure Node pred = tail; //获取尾节点 if (pred != null) { //如果队列已经初始化,此时尾节点不为空 node.prev = pred; //把新建的节点的上一个节点指向尾节点(末尾插入队列) if (compareAndSetTail(pred, node)) { //cas操作,把尾结点设置成这个新的节点 pred.next = node; //如果成功了,在把原来的尾节点的下一个节点指向这个新的节点,然后把这个节点返回 return node; } } enq(node); //能跑到这来说明此时队列还没初始化,亦或者说是上面的尾插入失败了 return node; }
在来看enq方法(讲节点入队列,如果节点还没初始化,先进行初始化):
private Node enq(final Node node) { for (;;) { //先来一个自旋 Node t = tail; //获取尾节点 if (t == null) { // Must initialize 如果为节点是空的,说明此时队列还未初始化 if (compareAndSetHead(new Node())) //cas操作,设置一个新的头结点 tail = head; //讲尾节点也设置成这个新的头结点 } else { node.prev = t; //跑到这来说明此时队列初始化已经完成;将参数目标节点的上一个节点指向尾节点 if (compareAndSetTail(t, node)) { //cas操作设置尾节点为参数节点 t.next = node; //如果成功了,把原来的尾节点的下一个节点指向参数节点 return t; //返回队列中的最后一个节点,也就是原尾节点; 如果设置尾节点失败了,继续自旋,直到在末尾插入到队列成功为止 } } } }
这里有点绕,画个图吧:
初始状态的AQS中的CLH队列:
第一个线程尝试去加锁入队列:
第一步,初始化一个空的头节点(CAS操作,防止别的线程也在初始化操作):
第二步,将头结点指向尾节点:
第三步,开始自旋,防止此时有别的线程想要入队列,代码进入else里面,如果入队cas失败,一直自旋下去,直到插入到尾节点为止,入队成功enq方法会返回这个去尝试入队的节点:
同理,第二个线程入队列:
我们继续往下看acquireQueued方法:
final boolean acquireQueued(final Node node, int arg) { //入参为入队列以后返回的线程,上面两个方法返回,arg为1,lock方法过来一直是1 boolean failed = true; try { boolean interrupted = false; //是否中断标记 for (;;) { //自旋,直到线程获取到锁,要么出现异常状态 final Node p = node.predecessor(); //入参节点如果是非头结点的第一个节点,那么返回的就是头节点(只有头结点的下一个节点才会被唤醒后面会讲到,如果不是的话,就会走下面的if) if (p == head && tryAcquire(arg)) { //如果入参节点上一个节点是头节点,会再去尝试获取一次锁, setHead(node); //如果获取到了锁,设置参数节点为头结点,后续节点都往前挪;如果抢锁失败了(为啥会失败,因为有可能非公平加锁模式下,别的线程上来就拿到锁了)走下面的if判断 p.next = null; // help GC //断开与尾节点的引用 failed = false; return interrupted; //返回false } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //如果前驱节点不是头结点,或者说是头结点,获取锁失败了,会走到这里。先去判断前驱节点的等待状态是不是-1 interrupted = true; //(接上面)如果是-1就阻塞队列,等待唤醒,如果不是-1,就把前驱节点的等待状态改为-1,在把线程阻塞
} } finally { if (failed) cancelAcquire(node); } } //异常处理
//入队列等待唤醒
/////
都会先去更改前驱节点的waitStatus = -1,为了后面unpark操作,细节!细节!细节。unpark会将waitStatus
在改回0(为了支持非公平加锁外部新来线程拿到了锁,一直在-1和0之间反复横跳),就是为了acquireQueued方法中阻塞在parkAndCheckInterrupt里面的线程唤醒继续去抢锁。
整个CLH队列中,只有非head节点的第一个节点才会被唤醒,直到获取锁以后,唤醒下一个节点,成为新的head节点,然后出列。
///
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; //前驱节点的等待状态 if (ws == Node.SIGNAL) //如果是-1,表示下一个节点可以被唤醒,直接返回true /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { //如果前驱节点的等待状态比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); //如果不是-1,就把前驱节点的等待状态改为-1 } return false; }
private final boolean parkAndCheckInterrupt() { //park住线程,等待去唤醒 LockSupport.park(this); return Thread.interrupted(); }
6.ReentrantLock的公平与非公平加锁
先看ReentrantLock的构造函数:
内部类sync,aqs的实现,默认是非公平加锁,我们再来看NonfairSync这个实现类:
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { //加锁的方法 if (compareAndSetState(0, 1)) //上来就直接去抢锁,并不会直接入队列 setExclusiveOwnerThread(Thread.currentThread()); //如果抢锁成功,把当前线程设置为获取锁的线程 else acquire(1); //如果抢锁失败,正常入队列,走公平加锁方式 } protected final boolean tryAcquire(int acquires) { //入队列之前,还会再去抢一次锁 return nonfairTryAcquire(acquires); } } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //获取当前队列线程持有状态,如果是0,那就表示还未有线程占有锁 if (c == 0) { if (compareAndSetState(0, acquires)) { //把state设置为1 setExclusiveOwnerThread(current); //如果state成功设置成1,这个线程获取了锁,把当前线程设置为持有锁的线程 return true; } } else if (current == getExclusiveOwnerThread()) { //如果持有锁的已经是当前线程了,这个时候就是锁的重入 int nextc = c + acquires; //state+1 if (nextc < 0) // overflow //小于1,异常状态 throw new Error("Maximum lock count exceeded"); setState(nextc); //设置state为新的值,1 return true; } return false; }
我们继续来看公平加锁实现,FairSync:
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { //加锁方法,直接走入队列方法 acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { //入队列之前调用的方法 final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //如果state为0,并且队列中没有别的在等待锁的线程,然后去更改state,把获取锁的线程设置为当前线程 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; } }
7.独占模式加锁
这个不过多解释了,aqs的属性exclusiveOwnerThread,标志为当前获取锁的线程是哪个,解锁以后,引用会置为空,重新等待新的线程去获取锁,再把引用指向为新的线程。
8.手动解锁
public void unlock() { sync.release(1); }
调用的是aqs的release方法:
public final boolean release(int arg) { if (tryRelease(arg)) { //尝试去解锁 Node h = head; //如果成功了,获取头结点 if (h != null && h.waitStatus != 0) //如果头结点不为空,并且头节点的等待状态不为0(上面自旋的时候会把头结点的等待状态改为-1),就是为了这里去手动释放锁 unparkSuccessor(h); //解锁 return true; } return false; }
protected final boolean tryRelease(int releases) { //入参为1 int c = getState() - releases; //获取aqs中的state,在-1 if (Thread.currentThread() != getExclusiveOwnerThread()) //如果获取锁的线程不是当前线程, 抛出异常 throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { //如果-1以后为0,表示释放锁成功 free = true; setExclusiveOwnerThread(null); //把当前获取锁的属性指向为空 } setState(c); //设置state为0 return free; }
//整个方法来说,如果线程是多次重入过,就需要多次解锁,最终让state为0才能释放锁
private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; //获取头结点的等待状态 if (ws < 0) //如果是-1,将等待状态改为0 compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; //获取第一个有效节点 if (s == null || s.waitStatus > 0) { //为空,或者等待状态>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); }
over~