AQS是Java多线程编程的重入锁,管程,工具类的基础类,是必须要掌握的。不掌握这个类,根本不能称之为合格的Java程序员。
即使是把这个类所有的代码都背会,也是值得的。
如何标识已经有线程在执行呢?
有两个变量,一个state变量,一个exclusiveOwnerThread变量,为什么需要两个变量呢?用一个exclusiveOwnerThread变量不也可以吗?
state变量用来支持重入,exclusiveOwnerThread变量用来支持互斥。
state变量标识是否已经有线程获得锁。但是这里为什么只是用cas尝试设置一次呢?如下代码所示:
复制代码 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);
}
}
复制代码 如果说这里使用CAS尝试修改无限循环的设置,那么能够就能简单的实现锁了呢?如下代码所示:
final void lock() { int state = getState(); while (!compareAndSetState(state, 1)){
}
}
这里使用CAS设置state成员变量明显不行,那能够设置当前线程呢?这里是不行的,CAS只能保证多个线程并发修改数据的线程安全特性,只有一个线程修改成功,但无法保证这类数据被一个线程锁占用时,另一个线程的CAS操作会失败的情况!!!所以,这里只能尝试设置一次状态,这里判断的用意是看当前线程执行到这里,有没有其他的线程已经设置过状态,若设置过,那么就只能加入到队列,否则,自己就直接加锁不就得了。
acquire方法为何最后要自我打断?
分析情况应该分多钟情况,这样比较好分析。
情景1:节点类型
1.节点全部都是独占模式的情景模拟
2.节点全部都是共享模式的情景模拟
3.节点既有独占模式,又有共享模式的情景模拟。
情景2:
1.当有一个线程加入队列时,此时持有锁的线程释放。。。
2.当有2个线程加入队列时,此时持有锁的线程释放。。。
为什么会同时存在addWaiter方法和enq方法呢?只使用addWatier方法不就行了吗?
addWatier方法适用于只有一个线程加入队列,此时没有竞争,很快就能加入队列。
enq方法同时考虑了两种情况:
1.多个线程都要加入队列
此时为了提高效率,先尝试一次CAS设置,至此有一个线程成功设置,让其他线程后进入enq方法,让所有要加入队列的线程自旋CAS加入队列。
2.队列为空
此时需要初始化一个头结点,而且,为了只能设置一个头结点,运用CAS来操作。
复制代码 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; } } } } 复制代码 这里比较经典,只有一个线程会走Must initialize,而且只会发生一次!!!然后,大家都会走下面的分支来依次添加到队尾。思考,这个Head什么都没有,如何处置他呢?
我感觉下面会竞争执行权。
结点状态深入理解!!!结点有好几种状态,必须理解一下。
节点状态有:
1.被取消
-1 等待通知
-2 等待条件
-3 ???
什么时候会出现被取消的节点呢?
当调用tryLock(TimeUinit)这样的方法时,如果尝试获得锁失败,则会将节点变成取消状态。
什么时候会出现取消的节点呢?
1.当超过等待时间依然没有获得锁,则退出方法时,会设置成取消状态
2.当等待过程过,出现打断异常,则退出方法时,会设置取消状态
为什么会保留这样的取消的节点呢?
因为如果要线程自己移除这个取消的节点,还是要保证线程安全,花费的时间是不确定的,所以,不如简单的设置一下已取消状态,让其他的线程帮自己移除,而自己尽快返回,不浪费时间。
使用AQS组件的锁的线程什么时候会自旋?
感觉没有地方会自旋。
为什么要用双向链表存储被阻塞的线程,而不是使用单链表?
1.节省寻找前驱节点的时间
为何ConditionObject是使用单链表,而不是双向链表呢?
够用就好
ReentantLock是否是公平锁?如果是,如何实现公平锁的?
通过一个boolean类型参数的构造函数来决定是否公平,公平锁保证等待的所有线程都有平等的机会获执行权,实现方式就是有一个线程获得锁之后,在这个线程没有获得锁之前,其他线程一直自旋等待加入队列,公平锁比较浪费CPU计算资源。此时等待队列就相当于退化到了只有一个节点的队列。。。。
非公平锁,是先来先服务的锁,谁先能够加入的等待队列,则谁就先被通知执行,非公平锁是阻塞锁,相较于公平锁,节省CPU计算资源。
针对这个类的每个方法,都仔细思考思考,切实理解每个方法的实现思路和原因?为什么要这样做比这个方法做了什么更重要。
每个方法的原因###############
释放互斥锁的操作
release方法,内部调用tryRelease方法和unpakcSuccessor(唤醒后继节点)方法。
tryRelease方法返回true说明完全释放了锁,所以其他等待的线程将会调用acquire方法获得锁。(其实是正在等待和正在调用acquire方法的线程)
unpackSuccessor方法唤醒后继节点,这里比较的有意思。
这个方法先找到第一个,是从双链表的尾部项头部遍历,找到哨兵节点的后继节点,然后unpack操作。
思考,谁把当前的这个节点从链表当中移除的呢?因为此时unpack操作,一定唤醒的是在睡眠的线程,所以,应该是要看做pack操作的后面的代码,因为此时会从这里被唤醒然后继续执行!!!
从acquireQueued方法可以看出,是线程自己把自己设置为头结点???哨兵节点不是头结点吗?
自己设置为头结点之后,就放弃了成为等待节点的能力,因为setHead方法会设置必要的字段为空,这样就成为哨兵节点了。此时,是放弃了之前的哨兵节点了!!!
获得互斥锁的操作
可中断的获得互斥锁的操作
超时获得互斥锁的操作
尝试一次获得互斥锁的操作
注意:哨兵节点的加入,其实也是惰性的,只有当有线程需要加入阻塞队列的时候才会创建哨兵节点,
哨兵节点来自3个操作:
1.第一个要加入阻塞队列的线程追加的
2.当获得互斥锁的线程要释放互斥锁,那么就会unpack后继节点,后继节点线程醒来,就会设置自己所在的节点为哨兵节点,那么,此时,哨兵节点的status字段就标识了当前线程之前所处的状态了。
3.释放共享锁,跟释放互斥锁类似。
#######################################################################################
释放共享锁的操作
可中断获得共享锁的操作
获得共享锁的操作
超时获得共享锁的操作
尝试一次获得互斥锁的操作
##########condition对象
为什么condition对象的wait和single类方法使用之前需要加锁,使用之后需要解锁?
因为这类方法都不是线程安全的方法,需要借助加锁来保证线程安全,保证自己追加Waiter能够线程安全。
包括增加等待节点,取消等待节点。
这里不是线程安全的方法,正好可以借助ReentantLock来保证线程安全。这样做,简化了Condition接口实现类的写法,比较容易理解。
conditionObject是condition的实现类,作为一个内部类,用意是能够访问外部类对象AQS的一些属性。
condition对象的本质依然是对等待对列的操作,两种操作,入队,出队,提供等待和通知两种比较实用的方法的工具组件。
可以实现管程。
condition对象的singleAll是一个一个通知的。先来先通知。
condition构建自己的等待队列不久OK了吗?为何还要让描述自己的节点加入AQS的同步队列呢?