原理:volatile的state变量,有个CLH队列加cas操作
独占模式:ReenTrankLock
共享模式:计时器,信号量,共享栅栏,读写锁等
什么时候初始化队列:当有线程来抢锁抢不到,然后要进队列的时候,就会初始化。
AQS源码中static静态代码块中封装了很多CAS操作,底层是unsafe的cas方法。
如果有个线程park阻塞,然后interrupt打断这个线程,那么这个线程就再也阻塞不住了,除非把中断标记清除。
流程:线程过来,会先调用tryacquire获取锁,即判断state是不是0,,然后判断队列中有没有元素,如果(公平锁)那么就加入队列,如果第一次或者非公平,那么一来就去抢锁。
如果抢锁失败,那么就要开始排位,acquireQueued里面的addWaiter、
addWaiter方法是把线程加入等待队列
AQS是一个双向链表,节点保存前后指针和当前thread和一个waitStatus:是否要唤醒后一个节点,这个值是由后一个节点来设置。
第一个进入队列的时候初始化,创建一个节点,然后,首尾指针指向它。队首节点的Thread属性永远为null
然后把要加入的节点加入到队尾。(每一个加入的节点都会判断自己是不是队首后一个元素,是的话就会再去tryAcquire获取锁,获取不到就park。park之前会把前一个节点的waitStatus属性设置为-1.
(第一个节点入队他要自旋3次)
小结:
1.在第二个线程加锁时,由于第一个线程已经加锁,所以第一次尝试加锁一定失败
2.由于没有队列,他在入队的时候要先初始化队列
3.加入队列之后有两次循环,每次循环尝试加锁一次,也就是额外自旋2次
4.这两次循环第一次会把前一个节点的ws设置为-1,第二次才阻塞自己
第三个节点:它也拿不到锁,然后要addwaiter进入队列,,把它排到线程二后面的节点。然后进去acquireQueued方法。然后判断自己不是队首第一个元素,他就不会去tryAcquire。(这个tryAcquire也是为了非公平锁的逻辑),然后把前一个节点的ws设置为-1,然后把自己park.
非公平锁和公平锁唯一区别:非公平锁一来就直接去tryAcquire,失败就去排队
公平锁一来先去检查一下队列有没有人,有人就排队,没人再去抢
细节:非公平锁效率高
如果t1拿到锁,它执行结束了,然后需要去唤醒队列中的t2,在唤醒的途中,t3来了,很快就拿到了锁,这时候t2醒了,如果t3执行结束,那t2就直接执行任务,如果t3没结束,t2也会因为tryAcquire失败,从而再次阻塞,只不过这次他就跑到队尾了。(线程的唤醒和阻塞比较耗时)
(以上就是上锁逻辑)
释放锁
tryRelease,state-1,如果减到0,那么就得去队列中看。
ws不为0,表示后面有人需要唤醒。所以state为0之后,就会判断自己的ws是不是不等于0.
释放锁之后,唤醒后面线程,后面线程会判断自己是不是头节点后一个,如果是那么setHead(node),把自己设置成头节点,老的头节点.next=null,便于GC,在setHead中会把当前节点的pre,thread属性设置为null。懂了吧。
也就是说,队列里面的头节点中thread永远为null,即当前持有锁的线程,不会保存在队列中
扩展
如果在lock.lock()之后被interrupe打断会怎么样。
lock会继续阻塞,不会有反应。
为什么:
interrupt原理:仅仅给线程设置了一个中断标记。
这个标记可以被interrupted方法捕获,并去除标记
如果线程park的话,调用interrupt的话,那么确实会唤醒,并且因为中断标记,再也无法阻塞,除非擦除。
所以AQS这么处理的,如果你interrupe,那么线程确实会醒来,然后他会调用interrupted方法擦除状态,然后tryacquire获取锁,因为不是正常唤醒。所以获取锁失败然后入队列阻塞。这里面会有一个变量记录是否被中断过,就是她擦除之后线程阻塞,会有一个线程记录它曾经被中断过,然后等到该线程被正常唤醒的时候,就会去后面selfInterrupt方法中自我中断一下。|=运算符保证标记一旦获取就无法更改
lock()被interrupt小结
1.阻塞中被interrupt自然会醒来
2.但是这个时候属于非正常唤醒,所以先获取并抹除这个线程的interrupt标记,让它在下一个循环过后继续阻塞
3.用|=运算符保证一旦标记被获取就无法覆盖
4.最后线程正常获取锁之后,重新给他interrupt一下
以上是一般的lock.lock()被interrupe的逻辑
lock还有一个可中断的上锁方法
lock.lockInterruptibly();
可中断方法的逻辑
前面逻辑一样,如果节点被唤醒,那么lock()方法中是会用|=记录标记,然后让线程重新入队阻塞
但是在这个方法,它是会抛异常,但是抛异常他会先自己处理,cancelAcquire(node)看名字就是删除的意思。
如果单单抛出异常给用户。那么这个线程还在队列,就会出问题啊,他会去执行自己的方法,因为异常,跳出方法,所以它不会循环,循环也阻塞不住,他会执行自己的线程逻辑,然后等到真正要唤醒它的时候,就会出问题,导致死锁。
因此在这里先捕获异常,自己处理队列,然后再抛异常让用户处理。
那它是怎么处理队列的呢
失效节点的调整
调整思路:让前面的节点跳过这些失效的节点,唤醒后面有效的节点
清除这些失效节点。清除节点的逻辑其实并不是主动清除,而是通过设置参数为null,然后因为头节点出队之后会setHead,这时候就断开了。所以不要怕失效节点导致内存泄漏。