jdk1.5开始增加了一种新的锁,原有的锁是通过对象头中的一个锁标识符来实现的,这里给大家分享一篇文章,http://www.jianshu.com/p/c5058b6fe8e5。
这里的实现我们且看且分析,先来看看类关系结构图,直接实现了Lock。
内部持有了一个Sync对象,根据描述可知,这个对象才是真正提供锁实现机制的核心。
这个对象继承自AbstractQueuedSynchronizer,子类支持公平和非公平两种模式,使用的是AQS状态去描述锁的持有数量。AQS是并发包下一个非常重要的类,之后会专门写一篇来讲解。
默认使用的是非公平锁,可以通过传入布尔值true来使用公平锁。
当我们调用lock命令的时候,其实真正调用的是sync对象的lock。
如果是非公平锁,我们来看看实现,首先调用一个CAS将一个状态从0变为1
这个方法是AQS里的一个方法,调用底层的UNSAFE的CASI更新状态,那么这个stateOffset是什么呢?
从AQS的静态初始化里可以看到这个是一个代表状态的内存偏移量,说直白点就是表示一种状态的值。
当更新成功后就进入了setExclusiveOwnerThread方法并传入了当前线程。然后这个方法也是AQS里面实现的,在AQS里面直接赋值给一个从名字分析来看叫唯一的线程的变量。
如果在上一步修改失败了进入acquire方法,这个方法里首先调用了一个treAcquire方法,并把传入的参数传进去,
TryAcquire方法在AQS里直接抛出了不支持操作的异常,这里调用的呢实际是nonFairSync里面重写的方法,这里又直接调用了nonfairTryAcquire方法,
在这个方法里首先调用了getState方法获得一个状态变量,直接调用了AQS里的方法,返回一个代表锁定次数的值。如果返回状态为0,则说明当前无线程锁定,则继续调用前面的CAS去修改状态为1,修改成功则设置当前线程为排他的主线程,然后返回true,或者返回false,也就是获取锁失败,如果状态不为0,则判断当前线程与排他的主线程是否相等,则改变加锁次数,如果这个次数小于0则抛出错误,然后调用setState方法把这个锁次数设置为状态,从这里可以理解为是一个可重入锁的实现过程
在前面如果调用tryAcquire失败后接着会调用acquireQueued方法,第一个参数里面先调用了一个addWaiter方法,并传入了一个代表排他的node节点,,这里面先对线程和这个代表着线程阻塞模式的状态包装为node,然后先拿到node节点的尾节点,如果这个尾节点不为null,则把这个为节点设置为新建立的node的前节点形成一个链条,然后通过CAS把尾节点替换为新建的node,然后把前面节点的下一个节点指向新建节点,然后返回。如果为节点为null或者替换失败则调用enq方法
enq方法里面调用一个线程循环,首先拿到尾节点,如果为尾节点为空,则先建立一个空的null节点并设置为Head,然后把尾节点也指向head,这相当于对node链的一个初始化,然后再次进入for循环。这时候头尾节点不为Null了,或者如果不为null,则把传入的新建node的前节点指向尾节点,然后通过CAS替换传入的node为尾节点,最后把之前的尾节点的next指向新传入的节点就完成了链条,这里其实仍然是一次重试node节点链条的建立,最后总是返回新建的节点。
写到这里我们看到这里利用node构建了一个链条,虽然目前不知道这个链条的作用,但是我们可以猜测到这个链条应该是用于阻塞等待的。在这个方法里面首先初始化了两个布尔值,一个用于判断获取结果是成功还是失败,一个用于判断线程状态。然后进入一个for循环。通过调用新传入的node.predecessor()方法获取这个Node的前一个node,如果前一个是整个链条的head,线程的阻塞等待是通过这个头节点的状态为-1实现的,然后就会再次尝试调用TraAcquire方法去获取,如果获取成功,则说明线程获得了锁,之后把获得锁的node(它持有当前线程)设置为头node,然后持有的线程设置为null,,然后把node的前一个node置为null,获取是否失败的标志变为false,然后返回是否中断的状态。如果前面的判断失败则调用一个shouldParkAfterFailedAcquire方法,从名字分析是一个获取失败后暂存线程的操作。真正的线程阻塞在parkAnCheckInterrupt里面调用locksupport.park方法实现的。
这个方法里第一个参数是新建Node的前一个Node,第二个参数是我们新建的node,他的主要目的是判断或者找到当前某一个node的等待状态为-1,这里如果node大于0,说明已经持有锁了,数值越大,说明重入次数越多,如果为0则说明在竞争锁。首先判断前一个node的线程状态是什么,如果是signal(-1)状态,则直接返回true,就可以安全的寄存线程了
如果不是则首先遍历node链条找到状态是0的节点,然后把我们新加进来的node变为这个节点的下一个节点,然后更新这个0的节点状态为-1.
在这个方法里如果返回true,则执行parkAndCheckInterrupt()方法:这里首先调用LockSupport的park方法把线程寄存,然后在判断线程的状态,使用interrupted和interrupt方法的区别还记得吗?如果调用了interrupted方法,会取消线程的中断状态。如果成功,则线程安全的寄存,如果寄存失败,则返回线程的中断状态并取消这个中断状态。
到这里就完成了线程的等待,这里的核心是调用了park方法实现的。
下面来看下unlock的实现。在这里调用了AQS的release
在release方法里首先调用了一个tryRelease的方法,如果这个返回true,则先拿到头节点,然后判断头节点不为null且他的等待状态不为0,则调用unparkSuccessor方法。我们先看tryRelease方法
tryRelease(),首先对该线程的状态进行一个线程锁次数减操作,然后判断当前线程是不是被设置为排他的线程,如果不是就抛出异常,如果是则先初始化一个是否成功的布尔值,如果此时锁定次数为0,则直接设置这个布尔值为true,同时设置排他线程为null,最后调用setState改变这个线程的状态,最后返回状态,这里的状态有2种,一种是锁定次数变为0代表解锁成功,如果不为0则代表重入了多次,需要多次调用来释放。
返回true后我们看看unparkSuccessor()方法里面的实现,进来后首先拿到这个线程的等待状态,如果小于0,先变更为0
然后拿到这个节点的下一个节点,判断如果下一个节点不为Null且等待状态为-1或者0,直接调用LockSupport.unpark()方法,这里注意传入的对象是下一个节点,此时我们使用的是非公平锁,那么这个下一个节点s是由怎么决定的呢?之后我们会详细描述整个链条的构造过程。这里看如果等待状态>0呢,首先把s设置为null,然后从整个链条的尾部开始遍历,找到一个线程node的等待状态为0或者-1的然后把这个节点,然后释放这个节点。这里的意思是如果解锁失败的话会直接去尾部开始找一个等待的线程node去释放。
由这个方法里面我们可以推断出,解锁的顺序是按照获得锁的顺序进行操作的。测试代码如下:
上面代码通过休眠100毫秒保证我们现场顺序入锁队列。经过多次运行打印顺序按照入队顺序。
入队顺序是通过addWaiter方法来实现整个链条的构建的。头是一个线程为null的节点,状态为-1,然后下一个指向第一个去抢锁的线程Node,依次用next指向下一个。
tryLock();方法顾名思义是尝试获取一个锁。这里调用了nonfairTryAcquire方法,前面已经介绍过了,如果获取失败则返回false。也就是获取失败。
tryLock还有一个带时间参数的,底层调用的是一个带参数的tryAcquireNanos方法,第一个是Long,第二个是代表时间单位。
这个方法里首先判断线程是否被中断,如果中断抛出异常,如果没中断则取调用tryAcquire去获取锁,如果获取成功直接返回,如果获取失败则调用doAcquireNanos方法,
doAcquireNanos方法里首先拿到排他的这个node,然后在for循环里去尝试获取锁
如果失败则调用parkNanos方法指定阻塞一定时间,然后继续进入循环,知道我们的等待时间为0,然后获取失败去调用cancelAcquire方法,
这个方法里首先判断这个node状态,如果已经为Null则直接返回。如果不为null,则先把node的线程设置为null,然后沿着线程的前指针寻找直到一个等待状态大于0的一个node,
然后把当前取消的节点和获取到锁的这个节点建立管理关系,然后将取消的节点状态设置为-1,然后看这时候是不是尾节点,如果是则把前面的那个节点设置为尾节点,然后把前面节点的下一个节点设置为Null。如果不是尾节点先判断是不是头节点,如果不是则判断这个Node状态是不是-1,如果不是则继续判断是不是0或者是-1 如果满足然后把这个节点状态先更新为-1,且这个节点的前置节点不为null,然后取到我们要取消的节点的下一个节点,如果下一个节点不为null则把前面节点和下一个节点直接建立关系。如果上面条件不满足则直接对要取消的节点执行解除寄存的操作,最后把要取消的节点的下一个节点指向自己,这样就不存在引用问题可以回收了。
到此为止我们等待阻塞获取也就看完啦。
lockInterruptibly方法之前在阻塞队列里见到了很多个这个尝试获取锁的方式,下面来看看他的实现,他也是调用了sync的acquireInterruptibly方法,
这里也会通过TryAcquire方法去尝试获取锁,如果获取成功直接返回,如果获取失败则调用doAcquireInterruptibly方法,
这个方法里首先把当前线程包装成一个排他的的Node节点,然后去尝试获取锁,这里与前面多线程竞争锁唯一不一致的地方就是当调用parkAndCheckInterrupt方法成功后直接抛出了异常。而前面的操作是如果调用这个成功后,则把interrupted是否中断的状态改为true返回。
这里我们需要理解,调用Thread.interrupted(),如果线程被中断则返回true,且释放中断,如果未被中断则返回false, 所以这种方式获取锁必须保证Thread未被中断才能获取到锁。如果Thread执行过中断,则获取锁失败。
还剩最后一个newCondition方法,这里就不列代码了,它总是调用对应的sync对象取获取一个condition。
至此我们新的锁的实现都可以很直观的明了了。这就是一个线程竞争入队的过程
这里补充一下,公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己
非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
下面一篇会看公平锁。