AQS:阻塞队列 全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
getState - 获取 state 状态
setState - 设置 state 状态
compareAndSetState - cas 机制设置 state 状态
独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
tryAcquire
tryRelease
tryAcquireShared
tryReleaseShared
isHeldExclusively
AQS 要实现的功能目标
阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire
获取锁超时机制
通过打断取消机制
独占机制及共享机制
条件不满足时的等待机制
AQS 的基本思想其实很简单
while(state 状态不允许获取) {
if(队列中还没有此线程) {
入队并阻塞
}
}
当前线程出队
释放锁的逻辑
if(state 状态允许了) {
恢复阻塞的线程(s)
}
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
AQS由三部分组成,state同步状态、Node组成的CLH队列、ConditionObject条件变量(包含Node组成的条件单向队列)
AQS组件总结:
Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,
Semaphore(信号量)可以指定多个线程同时访问某个资源。
CountDownLatch (倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,
它可以让某一个线程等待直到倒计时结束,再开始执行。
CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。
主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,
让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier
我已经到达了屏障,然后当前线程被阻塞。
读写锁:读锁是共享锁加的的共享节点,获取读锁成功后进行后续节点的唤醒操作因为读锁是共享锁,这也是共享锁和排他锁的区别
如果是独占锁如果获取锁成功直接设置head节点结束了
StampedLock:它的特点是在使用读锁、写锁时都必须配合【戳】使用
乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通
过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全
缺点:StampedLock 不支持条件变量
StampedLock 不支持可重入
Semaphore:信号量,用来限制能同时访问共享资源的线程上限Semaphore semaphore = new Semaphore(3);
使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机
线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现)
CountdownLatch:用来进行线程同步协作,等待所有线程完成倒计时。
其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一 归0后主线程运行
CyclicBarrier:循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执
行到某个需要“同步”的时刻调用 await()
CyclicBarrier cb = new CyclicBarrier(2); // 个数为2时才会继续执行 cb.await();当个数不足时,等待
注意 CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的 CyclicBarrier 可以被比喻为『人满发车』
线程数要和任务数保持一致,假如有三个线程同时运行 会有额外的那个线程任务做减一操作
ConcurrentHashMap:
ConcurrentLinkedQueue:是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,
当我们获取一个元素时,它会返回队列头部的元素。
CopyOnWriteArrayList:增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。
模式篇:
同步模式之保护性暂停:即 Guarded Suspension,用在一个线程等待另一个线程的执行结果,JDK 中,join 的实现、Future 的实现,采用的就是此模式因为要等待另一方的结果,因此归类到同步模式
同步模式之 Balking():Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回
同步模式之顺序控制:固定运行顺序
异步模式之生产者/消费者:生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据,消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
JDK 中各种阻塞队列,采用的就是这种模式
异步模式之工作线程:让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池
终止模式之两阶段终止模式:在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会
interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行
线程安全单例:
享元模式:在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了 valueOf 方法,例如 Long 的
valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象 或者 String 串池等
非公平锁:
a)刚进来设置state从0变为1,设置成功后把该线程设置为锁的拥有者
b)tryAcquire流程:若设置不成功再尝试获取一次重复上述的步骤,还没设置成功的话看该线程有没有重入,重入的话state值加1,流程结束
c)若不是该线程,addWaiter流程则创建一个线程引用的node节点 然后尝试快速的将当前线程构造的node结点添加做为tail结点
d)即便在多线程状况下,enq方法仍是可以保证每一个线程结点会被安全的添加到同步队列中,由于enq经过CAS方式将结点添加到同步队列以后才会返回,
不然就会不断尝试添加(这样实际上就是在并发状况下,把向同步队列添加Node变得串行化了)
e)acquire()方法还有一步acquireQueued,这个方法的主要做用就是在同步队列中嗅探到本身的前驱结点,若是前驱结点是头节点的话就会尝试取获取同步状态,
不然会先设置本身的waitStatus为-1,而后调用LockSupport的方法park本身
流程:获取前驱结点 --> 若是前驱结点是头节点,就尝试取获取同步状态,这里的tryAcquire方法至关于仍是调用NofairSync的tryAcquire方法 -->
若是前驱结点是头节点而且tryAcquire返回true,那么就从新设置头节点为node --> 将原来的头节点的next设置为null,交由GC去回收它
--> 若是不是头节点(状态也是-1),或者虽然前驱结点是头节点可是尝试获取同步状态失败就会将node结点的waitStatus设置为-1(SIGNAL),
而且park本身,等待前驱结点的唤醒
①首先确定是调用ReentrantLock.lock()方法去尝试加锁;
②由于是非公平锁,因此就会转到调用NoFairSync.lock()方法;
③在NoFairSync.lock()方法中,会首先尝试设置state的值,由于已经被占有那么确定就是失败的。这时候就会调用AQS的模板方法AQS.acquire(1)。
④在AQS的模板方法acquire(1)中,实际首先会调用的是子类的tryAcquire()方法,而在非公平锁的实现中即Sync.nofairTryAcquire()方法。
⑤显然tryAcquire()会返回false,因此acquire()继续执行,即调用AQS.addWaiter(),就会将当前线程构造称为一个Node结点,初始情况下waitStatus为0。
⑥在addWaiter方法中,会首先尝试直接将构建的node结点以CAS的方式(存在多个线程尝试将本身设置为tail)设置为tail结点,若是设置成功就直接返回,失败的话就会进入一个自旋循环的过程。即调用enq()方法。最终保证本身成功被添加到同步队列中。
⑦加入同步队列以后,就须要将本身挂起或者嗅探本身的前驱结点是否为头结点以便尝试获取同步状态。即调用acquireQueued()方法。
⑧在这里thread3的前驱结点不是head结点,因此就直接调用shouldParkAfterFailedAcquire()方法,该方法首先会将刚刚的thread2线程结点中的waitStatue的值改变为-1(初始的时候是没有改变这个waitStatus的,每一个新节点的添加就会改变前驱结点的waitStatus值)。
⑨thread2所在结点的waitStatus改变后,shouldParkAfterFailedAcquire方法会返回false。因此以后还会在acquireQueued中进行第二次循环。并再次调用shouldParkAfterFailedAcquire方法,而后返回true。最终调用parkAndCheckInterrupt()将本身挂起。
非公平锁的释放流程:
从下面的unlock方法中咱们能够看出,其实是调用AQS的release()方法,其中传递的参数为1,表示每一次调用unlock方法都是释放所得到的一次state。重入的状况下会屡次调用unlock方法,也保证了lock和unlock是成对的
①获取当前AQS的state,并减去1;
②判断当前线程是否等于AQS的exclusiveOwnerThread,若是不是,就抛IllegalMonitorStateException异常,这就保证了加锁和释放锁必须是同一个线程;
③若是(state-1)的结果不为0,说明锁被重入了,须要屡次unlock,这也是lock和unlock成对的缘由;
④若是(state-1)等于0,咱们就将AQS的ExclusiveOwnerThread设置为null;
⑤若是上述操做成功了,也就是tryRelase方法返回了true;返回false表示须要屡次unlock。
调用release()方法中的unparkSuccessor()方法
①获取head节点的waitStatus,若是小于0,就经过CAS操做将head节点的waitStatus修改成0
②寻找head节点的下一个节点,若是这个节点的waitStatus小于0,就唤醒这个节点,不然遍历下去,找到第一个waitStatus<=0的节点,并唤醒
(5)下面咱们应该分析的是释放掉state以后,唤醒同步队列中的结点以后程序又是是怎样执行的。按照上面的同步队列示意图,那么下面会执行这些
①thread1(获取到锁的线程)调用unlock方法以后,最终执行到unparkSuccessor方法会唤醒thread2结点。因此thread2被unpark。
②再回想一下,当时thread2是在调用acquireQueued方法以后的parkAndCheckInterrupt里面被park阻塞挂起了,因此thread2被唤醒以后继续执行acquireQueued方
法中的for循环(到这里能够往前回忆看一下acquireQueued方法中的for循环作了哪些事情);
③for循环中作的第一件事情就是查看本身的前驱结点是否是头结点(按照上面的同步队列状况是知足的);
④前驱结点是head结点,就会调用tryAcquire方法尝试获取state,由于thread1已经释放了state,即state=0,因此thread2调用tryAcquire方法时候,
以CAS的方式去将state从0更新为1是成功的,因此这个时候thread2就获取到了锁
⑤thread2获取state成功,就会从acquireQueued方法中退出。注意这时候的acquireQueued返回值为false,因此在AQS的模板方法的acquire中会直接从if条件退出,
最后执行本身锁住的代码块中的程序。
公平锁和非公平锁的区别在于:非公平锁有新的线程进入会直接执行compareAndSetState(0, 1)方法试图获取锁
ConcurrentHashMap8原理: put流程: cas创建链表 --> 创建链表的头结点 --> 桶下标没冲突直接添加 --> 判断是否需要扩容 --> 冲突后,锁住链表头结点(synchronized)
--> 便利链表找到相同的key进行添加 --> 判断链表长度是否变为红黑树
ConcurrentHashMap7原理: 内部维护了一个segment数组, 每个segment对应一把锁 (缺点: segment数组默认为16,初始化后就不能改变了,并且不是懒惰初始化)
--> 每个segment数组的元素对应一个hashEntry数组, 每个hashEntry又是数组加链表的结构
hashmap(8):数组+链表+红黑树 --> 通过hash码计算桶下标,
hashmap(7)并发死链问题[七上八下]法生OOM: 发生在扩容的时候,重新计算桶下标(四分之三) 复现:找几扩容前后桶下标仍然相同的key,两个线程并发扩容中
========================================================================
FutureTask futureTask = new FutureTask<>(new Callable<Object>() {
@Override
public Object call() throws Exception {
TimeUnit.SECONDS.sleep(5);
return "111";
}
});
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get(3,TimeUnit.SECONDS));
System.out.println("等待输出...");
未在规定的时间范围内返回结果,get()会抛异常 后续主线程内容会不执行
可以使用isDone轮训,查询任务执行的状态
注: Future对于结果的获取不是很友好,只能通过阻塞或轮训的方式得到任务的结果
====================================================================================================================
CompletableFuture方式:提供了一种观察者模式类似的机制,可以让任务完成后通知监听的一方
.whenComplete((v, e) -> {
v为上一步的值, e为上一步是否有异常
}).exceptionally(e -> {
System.out.println(e.getMessage());
return null;
});;
异步任务发生异常后 exceptionally 相当于catch住了 不会造成别的线程继续执行
.exceptionally(throwable -> {
System.out.println("发生异常: " + throwable.getMessage());
return null;
}) 如果不加这段的话异步任务的异常会阻塞主线程逻辑的执行