AQS的介绍
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
AQS原理
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态(解释:就是通过volatile int state这个变量来判断该资源是否可以被当前发起资源请求的线程占用,如果state>0,那么就说明已经有线程在占用该资源了,其他线程就不能进行访问,除非state变成0,才可以申请访问该资源),如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例(解释:不是真的用队列Queue去存储这些节点,而是通过某种方式维持节点之间的关系,从而使得看起来像队列),仅存在节点之间的关联关系。
看个AQS(AbstractQueuedSynchronizer)原理图:
模板方法模式链接:
https://blog.csdn.net/qq_40241957/article/details/84898404
解释: ReentrantReadWriteLock是对“读操作”共享,“写操作”互斥的
自定义同步器
同步器代码实现
上面大概讲了一些关于AQS如何使用的理论性的东西,接下来,我们就来看下实际如何使用,直接采用JDK官方文档中的小例子来说明问题
解释:下面的Sync类对象是上面定义的静态内部类
同步器代码测试
测试下这个自定义的同步器,我们使用之前文章中做过的并发环境下a++的例子来说明问题(a++的原子性其实最好使用原子类AtomicInteger来解决,此处用Mutex有点大炮打蚊子的意味,好在能说明问题就好)
package juc;
import java.util.concurrent.CyclicBarrier;
/**
* Created by chengxiao on 2017/7/16.
*/
public class TestMutex {
private static CyclicBarrier barrier = new CyclicBarrier(31);
private static int a = 0;
private static Mutex mutex = new Mutex();
public static void main(String []args) throws Exception {
//说明:我们启用30个线程,每个线程对i自加10000次,同步正常的话,最终结果应为300000;
//未加锁前
for(int i=0;i<30;i++){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10000;i++){
increment1();//没有同步措施的a++;
}
try {
barrier.await();//等30个线程累加完毕
} catch (Exception e) {
e.printStackTrace();
}
}
});
t.start();
}
barrier.await();
System.out.println("加锁前,a="+a);
//加锁后
barrier.reset();//重置CyclicBarrier
a=0;
for(int i=0;i<30;i++){
new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10000;i++){
increment2();//a++采用Mutex进行同步处理
}
try {
barrier.await();//等30个线程累加完毕
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
barrier.await();
System.out.println("加锁后,a="+a);
}
/**
* 没有同步措施的a++
* @return
*/
public static void increment1(){
a++;
}
/**
* 使用自定义的Mutex进行同步处理的a++
*/
public static void increment2(){
mutex.lock();
a++;
mutex.unlock();
}
}
TestMutex
这运行结果充分说明了i++不是原子操作,可能发生线程安全问题
源码详解
解释:为什么会存在争夺的情况,原因是因为不仅仅是有后继节点线程会去抢资源,还可能会有新产生的线程也会去抢资源,所以会产生争夺现象
提示:下面的源码可能看不懂,下图第2张图是源码的翻版
3.1 acquire(int)
小疑问:上述为什么会是被中断呢?
3.1.2 addWaiter(Node)
小疑问:下面addWaiter()方法里面还有enq()是入列(enqueue)的意思
——————————————————————————————————
3.1.2.1 enq(Node)
提示: 要注意下面的enq()方法体里面有一个for死循环,这个就是自旋,一直做for循环,直到CAS,执行成功return t为止
①因为存在争用,可能多个线程入队,所以CAS操作来保证入队的原子性
②tai节点又是volatile的,保证可见性
3.1.3 acquireQueued(Node, int)
**解释:**上述说的“如果自己可以休息”就是说本线程是否被请求中断不是自身控制的,是由外在对象改变的
到这里了,我们先不急着总结acquireQueued()的函数流程,先看看shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()具体干些什么。
3.1.3.1 shouldParkAfterFailedAcquire(Node, Node)
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。
3.1.3.2 parkAndCheckInterrupt()
如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。
提示: 需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。
对acquireQueue()大总结:
3.2 release(int)
上一小节已经把acquire()说完了,这一小节就来讲讲它的反操作release()吧。此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()。下面是release()的源码:
逻辑并不复杂。它调用tryRelease()来释放资源。有一点需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()的时候要明确这一点!!
3.2.1 tryRelease(int)
下面是AQS的方法源码,需要我们去重写之
跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。
3.2.2 unparkSuccessor(Node)
此方法用于唤醒等待队列中下一个线程。下面是源码:
小疑问:可以看到下面的for循环是从同步队列的尾部往前逐个遍历的,然后逐个往前遍历,直到找出最前的那个未放弃的线程(有效的)
这个函数并不复杂。一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了!!And then, DO what you WANT!
3.2.3 小结
release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。
共享模式下的方法
3.3 acquireShared(int)
此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。下面是acquireShared()的源码:
3.3.1 doAcquireShared(int)
此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。下面是doAcquireShared()的源码:
解释: 从上面的这段话就可以看出,并发就比较低效,是要在代码中处理这个死局问题的
3.3.1.1 setHeadAndPropagate(Node, int)
小疑问: 可能这里不是很明白为什么要加上 h ==null和h.waitStatus<0?从上图可以看出p.next=null(解释: p是原头节点,p.next=null目的是为了回收原头节点)是在setHeadAndPropagate()函数之后执行的,说明在setHeadAndPropagate()函数体里面原头节点肯定不为null,那么为什么要在if()里面加上h ==null和h.waitStatus<0呢?
此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享
doReleaseShared()我们留着下一小节的releaseShared()里来讲。
3.4 releaseShared()
上一小节已经把acquireShared()说完了,这一小节就来讲讲它的反操作releaseShared()吧。此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。下面是releaseShared()的源码:
小疑问: 我感觉这样做的思路岂不是互斥了吗
3.4.1 doReleaseShared()