参考自:http://www.cnblogs.com/waterystone/p/4920797.html,
https://blog.csdn.net/qq_30572275/article/details/80297047
过了一段时间我又来修改了。。。
在开头加一句话,CAS的原子性是因为说cpu在执行cas时对这一块内存是独占排他的。
AQS全名AbstractQueuedSynchronizer,就和字面上一样,是维护了一个队列的同步器框架。
AQS类里有一个Node类,对线程进行了封装。看看里面的字段:
static final class Node {
// 共享模式下等待的标记
static final Node SHARED = new Node();
// 独占模式下等待的标记
static final Node EXCLUSIVE = null;
// 线程的等待状态 表示线程已经被取消
static final int CANCELLED = 1;
// 线程的等待状态 表示后继线程需要被唤醒
static final int SIGNAL = -1;
// 线程的等待状态 表示线程在Condtion上
static final int CONDITION = -2;
// 表示下一个acquireShared需要无条件的传播
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
/**
* 当前节点的线程,初始化后使用,在使用后失效
*/
volatile Thread thread;
/**
* 链接到等在等待条件上的下一个节点,或特殊的值SHARED,因为条件队列只有在独占模式时才能被访问,
* 所以我们只需要一个简单的连接队列在等待的时候保存节点,然后把它们转移到队列中重新获取
* 因为条件只能是独占性的,我们通过使用特殊的值来表示共享模式
*/
Node nextWaiter;
/**
* 如果节点处于共享模式下等待直接返回true
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回当前节点的前驱节点,如果为空,直接抛出空指针异常
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // 用来建立初始化的head 或 SHARED的标记
}
Node(Thread thread, Node mode) { // 指定线程和模式的构造方法
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // 指定线程和节点状态的构造方法
this.waitStatus = waitStatus;
this.thread = thread;
}
}
然后是AQS类里的其他几个字段(volatile和CAS可以在博客里的其他文章看)
好了,接下来就是开始获取里,直接上源码:
注释里说的很清楚,以独占模式尝试获取锁,忽略里中断。里面的tryAcquire()方法是用来给我们自己实现的,毕竟AQS是一个框架(可以去我的博客里看ReentrentLock的tryAcquire()是怎么实现的)。后面接着的是addWaiter()方法:
然后看acquireQueued()方法:
shouldParkAfterFailedAcquire和parkAndCheckInterrupt两个方法用来让当前节点找到一个合适的地方开始等待
最后总结下流程吧,参考大佬的图:
1.调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
2.没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
3.acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
4.如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
接下来看如何释放:
改写tryRelease()方法,成功后,释放资源,用unparkSuccessor(h)来释放并唤醒下一个节点
至于共享模式的话,其实就是在获取成功后会唤醒后面的节点,共享模式释放的时候,也会唤醒后继。(这个时候PROPAGATE这个值才会被用到).
最后,个人觉得的话,这东西就是一个队列,内部一直循环请求,再如ReentrantLock,其实内部就是一个acquire(1),只获取一个资源,本身有一个state,state=0的话就是锁没被占用,!=0就表示锁被占用里。
另外,有关读写锁ReentrantReadWriteLock的详解,可以参考大佬的文章:
https://blog.csdn.net/qq_19431333/article/details/70568478
顺嘴提一句,关于setHeadAndPropagate()方法,countDownLatch里也用到里这个,countdownLatch就是开始时候赋一个值,然后内部循环查询是否这个是归零里,如果归零里就会调用这个方法将后续等待着的节点都唤醒,就实现里所谓的等大家都准备好了再行动。
最后再附一张用户态,内核态,磁盘的I/O图,简略版,可以明白为什么线程切换时候会有成本了: