PART1:安全性问题与活跃性问题、性能问题
- 线程的活跃性:死锁(见Part1、Part6-1)、活锁、饥饿
- 死锁(见Part1、Part6-1)
- 活锁:两个没有获取到锁,但是改变了对方的结束条件使得双方都停止不了
- 饥饿:饥饿定义为,一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束
PART2:Fork/Join(分支合并计算)~这货就是一个JDK1.7加入的新的线程池实现,没什么大不了,看这里先:了解一下线程池
- ForkJoin(分支合并计算):(底层维护一个双向队列)。Fork/Join框架是Java7提供的一个
用于并行执行任务
的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。- Fork/Join是JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想, 适用于
能够进行任务拆分
的cpu密集型运算【所谓的任务拆分,是将一个大任务拆分为算法上相同
的小任务,直至不能拆分可以直接求解
。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解】- 分而治之
- 分而治之
- Fork/Join在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率。
Fork/Join默认会创建与cpu核心数大小相同的线程池
- Fork/Join是JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想, 适用于
public class ForkJoinExample extends RecursiveTask<Integer>{
private final int threshod = 5;
private int first;
private int last;
public ForkJoinExample(int first, int last){
this.first = first;
this.last = last;
}
@Override
protected Integer computer(){
int result = 0;
if(last - first <= threshold){
//任务足够小则直接计算
for(int i = first; i < last; i++){
result += i;
}
} else{
//否则,代表任务太大,需拆分成小任务
int middle = first + (last - first) / 2;
ForkJoinExample leftTask = new ForkJoinExample(first, middle);
ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
leftTask.fork();
rightTask.fork();
result = leftTask.join() + rightTask.join();
}
return result;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinExample example = new ForkJoinExample(1, 10000);
ForkJoinPool forkJoinPool = new ForkJoinPool();
Future result = forkJoinPool.submit(example);
System.out.println(result.get());
}
- 工作窃取算法(某个线程从其他队列中窃取任务进行执行的过程。一般就是指做得快的线程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程各在一端。)
- 把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~
- Fork/Join使用步骤:分两步
- 第一步:
- 第二步:
- 第一步:
- 把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~
PART3:JUC
- JUC包提供了一系列的原子性操作类,这些类都是使用**非阻塞算法CAS实现的** ,相比使用锁实现原子性操作这在性能上有很大提高。有时候咱们明知道是只读操作时多个线程同时调用不会存在线程安全问题,但是不得不给这个只读操作对应的方法上加个synchronized关键字,原因是咱们要靠synchronized 来实现 value的内存可见性。这样一比较为了靠锁来实现变量的内存可见性而让我们背上了锁引来的上下文切换的问题(当一个线程没有获取到锁时这个线程会被阻塞挂起,这会导致线程上下文的切换和重新调度开销,虽然Java 提供了非阻塞的volatile关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,但是volatile只能保证共享变量的可见性,不能解决读->改->写等操作之间的原子性问题),还有没有更好的方法呀不用锁用能实现 value的内存可见性---------
使用java.util.concurrent.atomic.AtomicInteger(JUC包下的原子性操作类
,(在内部使用非阻塞 CAS算法
实现的原子性操作类 AtomicLong)类主要利用 CAS和 volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升
。)
PART3-1:JUC(java.util.concurrent)
挑一些比较常用的在下面逐一介绍:java.util.concurrent…实操更多见线程安全篇
- ConcurrentHashMap
这里经常会出现这种错误:
解决方法一:
解决方法二:
Map<String, String> map = new ConcurrentHashMap<>();
- CopyOnWriteArrayList:使用
写时复制CopyOnWrite【写之前先复制一个主角的副本,然后在副本上写】
的策略来保证 list一致性。而获取一>修改一>写入三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁来保证在某个时间只有一个线程能对list数组进行修改- 当 List 需要被修改的时候,
我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据
,这样就可以保证写操作不会影响读操作了。- 所谓 CopyOnWrite 也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了
- 每个CopyOnWriteArrayList对象里面有一个array数组对象用来存放具体元素(ReentrantLock独占锁对象用来保证同时只有一个线程对array进行修改)
- CopyOnWriteArrayList是一个线程安全 ArrayList:(对其进行的修改操作都是在底层的一个复制的数组(快照,也就是咱们经常说的副本)上进行的,也就是使用了写时复制策略。)
- ReentrantLock独占锁对象用来保证同时只有一个线程对array进行修改
- 另外CopyOnWriteArrayList提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对list 修改是不可见的, 迭代器遍历的数组是一个快照
- 上面咱们看了线程安全的ArrayList,也就是CopyOnWriteArrayList,如果让咱们自己设计一个,先不说能不能设计出来,咱们应该思考什么东西呢?
首先咱们要搞一个集合,那这个集合初始化时咱们应该让集合多大呢?,这个多大是能装多少个元素呢?然后什么时候初始化呢?
,哪个阶段呢?~类加载阶段详细阶段- 那不就是在内部创建了一个大小为0的Object数组作为array的初始值
- 那不就是在内部创建了一个大小为0的Object数组作为array的初始值
那咱们有多个线程进行读写时该咋保证线程安全呢?
如何保证使用迭代器遍历list时的数据一致性,要不就叫做稳定性嘛(数据结构与算法中不是也有稳定这一说嘛)
- 获取迭代器后使用该迭代器元素时,其他线程对该list进行的增删改不可见(因为他们操作的是不同的数组,这就是弱一致性)
- 除了上面这几个,还有CopyOnWriteArrayList中常用的方法:
- 用来添加元素的:
比如add(E e):add()方法在添加元素时首先复制了一个快照然后在快照上进行添加而不是直接在原来数组上进行【CopyOnWriteArrayList 写入操作 add()方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来
】
再比如使用E get(int index)获取下标为index的元素,如果元素不存在就抛出IndexOutOfBoundsException异常。写时复制策略产生的弱一致性问题
:
如上当线程A调用get()方法获取指定位置的元素时会分两步走(当这两个步骤或者说整个过程中没有加锁同步):- step1:首先通过getArray()方法获取array数组
- step2:然后通过下标访问指定位置的元素
再比如使用E set(int index, E element)修改list中指定元素的值,不存在则抛出IndexOutOfBoundsException异常。过程是这样的:首先获取了独占锁,从而阻止其他线程对 array 数组进行修改,然后获取当前数组并调用 get 方法获取指定位置的元素,如果指定位置的元素值与新值不一致则创建新数组井复制元素,然后在新数组上修改指定位置的元素值并设置新数组到array。如
果指定位置的元素值与新值一样 ,则为 了保证volatile语义还是需要重新设置array ,虽然array的内容并没有改变。
再比如使用E remove(int index)等方法删除list里面指定的元素:首先获取独占锁以保证删除数据期间其他线程不能对 array 进行修改,然后获取数组中要被删除的元素,并把剩余的元素复制到新数组,之后使用新数组替换原来的数组,最后在返回前释放锁
- 用来添加元素的:
- 当 List 需要被修改的时候,
- CountDownLatch:
倒计时锁【用来进行线程同步协作,等待所有线程完成倒计时。其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown) 用来让计数减一
】。原理:每次有线程调用countDown()这个方法后,计数器就会数量-1,假设计数器数量==0,然后await()方法就会被唤醒并被执行- AQS 组件:
- Semaphore(信号量)-允许多个线程同时访问:
synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源
Semaphore 与 CountDownLatch 一样,也是共享锁的一种实现。它默认构造 AQS 的 state 为 permits
。当执行任务的线程数量超出 permits,那么多余的线程将会被放入阻塞队列 Park,并自旋判断 state 是否大于 0。只有当 state 大于 0 的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执行 release() 方法,release() 方法使得 state 的变量会加 1,那么自旋的线程便会判断成功。 如此,每次只有最多不超过 permits 数量的线程能自旋成功,便限制了执行任务线程的数量
- CountDownLatch (倒计时器): CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个CountDownLatch 工具通常用来控制线程等待,
CountDownLatch 可以让某一个线程等待直到倒计时结束,再开始执行【也就是允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕】
。- CountDownLatch 是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用 countDown() 方法时,其实使用了tryReleaseShared方法以 CAS 的操作来减少 state,直至 state 为 0 。当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。然后,CountDownLatch 会自旋 CAS 判断 state == 0,如果 state == 0 的话,就会释放所有等待的线程,await() 方法之后的语句得到执行
- 那具体的使用场景比如说,有一个
使用多线程读取多个文件处理
的场景用到了 CountDownLatch,比如要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理,为此可以定义一个线程池和 count 为 6 的CountDownLatch对象 。使用线程池处理读取任务,每一个线程处理完之后就将 count-1,调用CountDownLatch对象的 await()方法,直到所有文件读取完之后,才会接着执行后面的逻辑
。。
- CountDownLatch 的不足:CountDownLatch 是一次性的,
计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值
,当 CountDownLatch 使用完毕后,它不能再次被使用
- CountDownLatch 的不足:CountDownLatch 是一次性的,
- 可以使用 CompletableFuture 类来改进!Java8 的 CompletableFuture 提供了很多对多线程友好的方法,使用它可以很方便地为我们编写多线程程序,什么异步、串行、并行或者等待所有线程执行完任务什么的都非常方便。
- CompletableFuture 同时实现了 Future 和 CompletionStage 接口【
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {}
】。CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,CompletableFuture 还提供了函数式编程的能力
。- Future 接口有 5 个方法:
- CompletableFuture 的函数式能力是CompletionStage 接口给予的。
- Future 接口有 5 个方法:
- CompletableFuture常见操作:
- 创建 CompletableFuture 对象的方法:
- new:通过 new 关键字创建 CompletableFuture 对象这种使用方式可以看作是将 CompletableFuture 当做 Future 来使用
- 基于 CompletableFuture 自带的静态工厂方法:runAsync() 、supplyAsync()
- 处理异步结算的结果:当我们获取到异步计算的结果之后,还可以对其进行进一步的处理,比较常用的方法有下面几个:
- thenApply() 方法接受一个 Function 实例,用它来处理结果。
- 异常处理:可以通过 handle() 方法来处理任务执行过程中可能出现的抛出异常的情况。
- 组合 CompletableFuture:可以使用 thenCompose() 按顺序链接两个 CompletableFuture 对象。在实际开发中,这个方法还是非常有用的。比如说,我们先要获取用户信息然后再用用户信息去做其他事情。
- 并行运行多个 CompletableFuture:
可以通过 CompletableFuture 的 allOf()这个静态方法来并行运行多个 CompletableFuture,实际项目中,我们经常需要并行运行多个互不相关的任务,这些任务之间没有依赖关系,可以互相独立地运行。比说我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。像这种情况我们就可以使用并行运行多个 CompletableFuture 来处理。
- 创建 CompletableFuture 对象的方法:
- CompletableFuture 同时实现了 Future 和 CompletionStage 接口【
- 当任务过多的时候,把每一个 task 都列出来不太现实,可以考虑通过循环来添加任务。
- CountDownLatch 是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用 countDown() 方法时,其实使用了tryReleaseShared方法以 CAS 的操作来减少 state,直至 state 为 0 。当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。然后,CountDownLatch 会自旋 CAS 判断 state == 0,如果 state == 0 的话,就会释放所有等待的线程,await() 方法之后的语句得到执行
- CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,CyclicBarrier 它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,
CyclicBarrier 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活
。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞
CountDownLatch 的实现是基于 AQS 的
- 对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。
- CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减
CycliBarrier 是基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的
- 对于 CyclicBarrier,
重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待
。 - CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行
- 对于 CyclicBarrier,
- Semaphore(信号量)-允许多个线程同时访问:
- CountDownLatch是使用 AQS 实现的。使用 AQS 的状态变量来存放计数器的值。首先在初始化CountDownLatch 时设置状态值(计数器值),当多个线程调用 countdown方法时实际是原子性递减 AQS 的状态值。当线程调用await方法后当前线程会被放入AQS的阻塞队列等待计数器为0再返回。其他线程调用 countdown 方法让计数器值递减1,当计数器值变为0时,当前线程还要调用AQS的doReleaseShared方法来激活由于调用await() 方法而被阻塞的线程
- CountDownLatch 的任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,
每个子线程执行完后 countDown() 一次,state 会 CAS(Compare and Swap) 减 1
。等到所有子线程都执行完后(即 state=0 ),会 unpark() 主调用线程,然后主调用线程就会从 await() 函数返回,继续后余动作。
- CountDownLatch 的任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,
- 在日常开发中经常会遇到需要在主线程中开启多线程去并行执行任务 ,并且主线程需要等待所有子线程执行完后再进行汇总的场景。在 CountDownLatch出现之前一般都使用线程的 join()方法来实现这一点,但是join 方法不够灵活不能够满足不同场景的需要,所以 JDK 开发组提供了CountDownLatch这个类
- 常用情景:
- 等待多(个)线程准备完毕,倒计时
- 等待多个远程调用结束
- CountDownLatch和join方法的区别:
- 一个区别是,调用一个子线程的join()方法后,该线程会一直被阻塞直到子线程运行完毕,而 CountDownLatch使用计数器来允许子线程运行完毕或者在运行中递减计数,也就是CountDownLatch可以在子线程运行的任何时候让 await 方法返回而不一定必须等到线程结束
- 另外, 用线程池来管理线程一般都是直接添加 Runnable 到线程池,这时候就没有办法再调用线程的 join方法了,就是说 countDownLatch 相比 join 方法让我们对线程同步有更灵活的控制
- 常用情景:
- AQS 组件:
- CyclicBarrier(回环屏障):
- CountDownLatch 在解决多线程同步方面相对于调用线程的 join方法己经有了不少优化,但CountDownLatch计数器是一次性的,要再次计数你就得重新创建一个锁对象,也就是等到计数器值变为0后才会再调用CountDownLatch的await方法和countdown 方法都会立刻返回,这就起不到线程同步的效果了。所以为了满足计数器可以重置的需要, JDK开发组提供了CyclicBarrier。CyclicBarrier类的功能并不限于CountDownLatch 的功能。从字面意思理解,CyclicBarrier 是回环屏障的意思 ,
它可以让一组线程全部达到一个状态后再全部同时执行
。这里之所以叫作回环是因为当所等待线程执行完毕,并重置 CyclicBarrier 的状态后它可以被重用
。之所以叫作屏障是因为线程调用await 方法后就会被阻塞,这个阻塞点就称为屏障点,等所有线程都调用了 await 方法后,线程就会冲破屏障,继续向下运行。
- CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
- CyclicBarrier 内部通过一个 count 变量作为计数器,
count 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务
- CyclicBarrier 内部通过一个 count 变量作为计数器,
- 用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行,和CountdownLaych相似,都是通过维护计数器来实现的。线程执行await()方法之后计数器会-1,并进行等待,直到计数器减为0,所有await()方法执行完后,之前再等待的线程才能继续执行,CyclicBarrier类
- CycleBarrier和CountDownLatch 的不同在于CycleBarrier是可以复用的,并且CycleBarrier特适合分段任务有序执行的场景。CycleBarrier通过独占锁 ReentrantLock实现计数器原子性更新,并使用条件变量队列来实现线程同步。
- CyclicBarrier 的应用场景:
- CyclicBarrier 可以
用于多线程计算数据,最后合并计算结果
的应用场景。比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个 sheet 里的银行流水,都执行完之后,得到每个 sheet 的日均银行流水,最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水
- CyclicBarrier 可以
- CountDownLatch 在解决多线程同步方面相对于调用线程的 join方法己经有了不少优化,但CountDownLatch计数器是一次性的,要再次计数你就得重新创建一个锁对象,也就是等到计数器值变为0后才会再调用CountDownLatch的await方法和countdown 方法都会立刻返回,这就起不到线程同步的效果了。所以为了满足计数器可以重置的需要, JDK开发组提供了CyclicBarrier。CyclicBarrier类的功能并不限于CountDownLatch 的功能。从字面意思理解,CyclicBarrier 是回环屏障的意思 ,
- Semaphore(信号量):信号量,
用来限制能同时访问共享资源的线程上限
。- Semaphore 信号量也是 Java中的一个同步器,与CountDownLatch与CycleBarrier不同的是,它内部的计数器是递增的,并且在开始初始化Semaphore时可指定一个初始值,但是并不需要知道需要同步的线程个数,而是在需要同步的地方调用 acquire 方法时指定需要同步的线程个数。
- 执行 acquire() 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;
- 除了 acquire() 方法之外,另一个比较常用的与之对应的方法是 tryAcquire() 方法,该tryAcquire方法如果获取不到许可就立即返回 false。
每个 release 方法增加一个许可证,这可能会释放一个阻塞的 acquire() 方法
。然而,其实并没有实际的许可证这个对象,Semaphore 只是维持了一个可获得许可证的数量
。
- 执行 acquire() 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;
- Semaphore完全可以达到CountDownLatch的效果,但是Semaphore的计数器是不可以自动重置的。不过通过变相地改变acquire方法的参数还是可以实现CycleBarrier的功能的。Semaphore 也是使用 AQS实现的。并且获取信号量时有公平策略和非公平策略之分。
- 单机版的限流的时候用(Semaphore类似于OS中的信号量,可以控制对互斥资源的访问线程数,而不是资源数(比如连接数)(例子代码中模拟了对某个服务的并发请求,每次只能有3个客户端同时访问,请求总数为10))
- Semaphore应用:
Semaphore 经常用于限制获取某种资源的线程数量
。使用Seimaphore限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数( 例如连接数,请对比Tomcat LimitI atch的实现) - 用Semaphore 实现简单连接池,对比享元模式下的实现(用wait notify),性能和可读性显然更好
- Semaphore应用:
- Semaphore 有两种模式,公平模式和非公平模式:
- 公平模式: 调用 acquire() 方法的顺序就是获取许可证的顺序,遵循 FIFO
- 非公平模式: 抢占式的
- 当一个线程通过acquire()方法获取信号量时,会首先看当前信号量个数是否满足需要, 不满足则把当前线程放入阻塞队列,如果满足则通过自旋CAS获取信号量。
- 加锁解锁流程:
- acquire()
- release()
- acquire()
- Semaphore 信号量也是 Java中的一个同步器,与CountDownLatch与CycleBarrier不同的是,它内部的计数器是递增的,并且在开始初始化Semaphore时可指定一个初始值,但是并不需要知道需要同步的线程个数,而是在需要同步的地方调用 acquire 方法时指定需要同步的线程个数。
- BlockingQueue
- 阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是
BlockingQueue 提供了可阻塞的插入和移除的方法
。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。
3 个常见的 BlockingQueue 的实现类
:- ArrayBlockingQueue:
- ArrayBlockingQueue 是 BlockingQueue 接口的
有界队列
实现类,底层采用数组来实现 ArrayBlockingQueue 一旦创建,容量不能改变
。其并发控制采用可重入锁 ReentrantLock ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时
,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。- ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。可以通过
private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true)
;获得公平性的 ArrayBlockingQueue
- ArrayBlockingQueue 是 BlockingQueue 接口的
- LinkedBlockingQueue:
- LinkedBlockingQueue 底层基于
单向链表实现
的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE
- LinkedBlockingQueue 底层基于
- PriorityBlockingQueue:
- PriorityBlockingQueue 是一个支持优先级的无界阻塞队列。默认情况下元素
采用自然顺序进行排序,也可以通过自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则
。 - PriorityBlockingQueue 并发控制采用的是可重入锁 ReentrantLock,队列为无界队列(ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 也可以通过在构造函数中传入 capacity 指定队列最大的容量,但是 PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容),相当于
PriorityBlockingQueue 就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞
)。
- PriorityBlockingQueue 是一个支持优先级的无界阻塞队列。默认情况下元素
- ArrayBlockingQueue:
- 阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是
- SynchronousQueue:(同步队列)容量为1,就是每次进如队列一个元素后,队列就满了,必须等待取出来那一个之后,才能再往里面放入一个元素
Notes:
凡是多线程总是会遇到java.util.ConcurrentModificationException并发修改异常
解决方法一:
List<String> list = new Vector<>();//Vector默认是线程安全的
解决方法二:用Collections工具类里面的方法
或者这个例子形式:
解决方法三:用JUC下面的类
用的是Lock模板圈实现锁
或者这个例子:
Set<String> set = new CopyOnWriteArraySet<>();
解决方法三(用CopyOnWriteXxx):比解决方法一(用Vector)牛逼在哪里,源码里面CopyOnWrite的add()方法用的是lock模板圈,Vector的add()方法用的是synchronized,synchronized比lock模板圈慢一点
-
CopyOnWriteArraySet
-
并发编程之美这本好书上讲解了很多并发原理的类图结构和原理等,有需要的可以看看:
- java.util.concurrent.ConcurrentLinkedQueue
Java 提供的线程安全的 Queue 可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是 ConcurrentLinkedQueue
,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现
- ConcurrentLinkedQueue这个队列使用链表作为其数据结构.
ConcurrentLinkedQueue 应该算是在高并发环境中性能最好的队列了
。它之所有能有很好的性能,是因为其内部复杂的实现,ConcurrentLinkedQueue 主要使用 CAS 非阻塞算法来实现线程安全
- ConcurrentLinkedQueue 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代
- java.util.concurrent.DelayQueue
- java.util.concurrent.ConcurrentLinkedQueue
PART3-2:java.util.concurrent.atomic(实操见线程安全篇)【Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰,原子类说简单点就是具有原子/原子操作特征的类。并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic下
。】
-
JUC 包中的4类原子类:
- 基本类型:使用原子的方式更新基本类型
- AtomicInteger : 整型原子类
- AtomicLong:长整型原子类~AtomicLong详细见这篇
- LongAddr:性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加Cell[0],而Thread-1累加Cell[1]…最后将结果汇总。这样它们在累加时操作的不同的Cell变量,因此减少了CAS重试失败,从而提高性能。
- LongAddr:性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加Cell[0],而Thread-1累加Cell[1]…最后将结果汇总。这样它们在累加时操作的不同的Cell变量,因此减少了CAS重试失败,从而提高性能。
- AtomicBoolean: 布尔型原子类
- 基本数据类型原子类的优势:一比较不就出来了嘛
- 多线程环境不使用原子类保证线程安全(基本数据类型)
- 多线程环境使用原子类保证线程安全(基本数据类型)
- 多线程环境不使用原子类保证线程安全(基本数据类型)
- 数组类型 使用原子的方式更新数组里的某个元素:
- AtomicIntegerArray: 整型数组原子类
- AtomicLongArray: 长整型数组原子类
- AtomicReferenceArray: 引用类型数组原子类
- 引用类型 使用原子的方式更新引用类型
- AtomicReference: 引用类型原子类
- AtomicStampedReference: 原子更新
带有版本号
的引用类型。该类将整型数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题
。- AtomicStampedReference可以给原子弓|用
加上版本号
,追踪原子引用整个的变化过程,如:A -> B -> A -> C,通过AtomicStampedReference, 我们可以知道,引用变量中途被更改了几次。就可以解决ABA问题
- AtomicStampedReference可以给原子弓|用
- AtomicMarkableReference: 原子更新带有标记位的引用类型。对象属性修改类型
- 但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference
- 字段更新器:保护对象属性赋值操作的原子性。对象的属性修改类型
- AtomicIntegerFieldUpdater: 原子更新整型字段的更新器
- AtomicLongFieldUpdater: 原子更新长整型字段的更新器
- AtomicMarkableReference: 原子更新带有标记位的引用类型
- 基本类型:使用原子的方式更新基本类型
-
以AtomicLong类为例,探究一下细节:(AtomicLong类是原子性递增或者递减类,内部使用Unsafe实现)
publc class AtomicLong extends Number implements java.io.Serializable { private static final long serialVersionUID = 1927816293512124184L; //(1)获取Unsafe实例,(这是因为AtomicLong也是在rt.jar包下面的,AtomicLong类就是通过BootStarp类加载器进行加载的) private static final Unsafe unsafe = Unsafe.getUnsafe(); //(2)存放变量value的偏移量 private static final long valueOffset; //(3)判断JVM是否支持Long类型无锁CAS static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8() ; private static native boolean VMSupportsCSB() ; static{ try { //(4)获取value在AtomicLong中的偏移量 valueOffset = unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField(”value")); } catch (Exception ex) { throw new Error(ex); } } //(5)实际变量值,value声明为volatile是为了在多线程下保证内存可见性,value是具体存放计数的变量 private volatile long value; public AtomicLong(long initialValue) { value = initialValue; } ...... }
/** *下面的方法内部都是调用Unsafe的getAndAddLong方法来实现操作,这个函数或者方法是个原子性操作,这里第一个参数是AtomicLong实例的引用、第二个参数是value变量AtomicLong中的偏移值,第三个参数是要设置的第二个变量的值。 */ //(6)调用unsafe方法,原子性设置value值为原始值+1,返回值为递增后的值 public final long incrementAndGet(){ return unsafe.getAndAddLog(this, valueOffset, 1L) + 1L; } //(7)调用unsafe方法,原子性设置value值为原始值-1,返回值为递减之后的值 public final long decrementAndGet() { return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L; } //(9)调用unsafe方法,原子性设置value值为原始值+1,返回值为原始值 public final long getAndincrement() { return unsafe.getAndAddLong(this, valueOffset, 1L); } /** *getAndlncrement方法在JDK7中的实现逻辑: - *public final long getAndincrement(){ while(true){ long current = get(); //每个线程是先拿到变量的当前值(因为上面代码中咱们的value变量已经被volatile修饰也就是保证了内存的可见性),所以咱们这里拿到的是最新值 long next = current + 1; //然后在线程私有的工作内存中对变量的当前值进行增1操作 if(compareAndSet(current , next)){//而后使用CAS修改变量的值,用了CAS的好处就是如果设置失败,则循环继续尝试直到设置成功 return current; } } } */ /** *getAndlncrement方法在JDK8中的实现逻辑: - *public final long getAndIncrement(){ return unsafe.getAndAddLong(this, valueOffset, 1L); } */ //(9)调用unsafe方法,原子性设置value值为原始值,返回位为原始值 public final long getAndDecrement() { return unsafe.getAndAddLong(this, valueOffset, -1L); }
public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2){ Long l; do{ l = getLongvolatile(paramObject, paramLong1); )while(!compareAndSwapLong(param0bject, paramLong1, 1, 1 + paramLong2)){ return 1; } }
public final boolean compareAndSet(long expect, long update){ //如果原子变量中的value值等于expect,则使用update值更新该值并返回true,否则返回false return unsafe.compareAndSwapLong(this, valueOffset, expect, update); }
-
再以AtomicInteger为例:
- AtomicInteger 类常用方法
public final int get() //获取当前的值 public final int getAndSet(int newValue)//获取当前的值,并设置新的值 public final int getAndIncrement()//获取当前的值,并自增 public final int getAndDecrement() //获取当前的值,并自减 public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
- AtomicInteger 类的使用:使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。
class AtomicIntegerTest { private AtomicInteger count = new AtomicInteger(); //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。 public void increment() { count.incrementAndGet(); } public int getCount() { return count.get(); } }
AtomicInteger 类主要利用 CAS (compare and swap:CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升
- AtomicInteger 类常用方法
PART3-3:JUC之java.util.concurrent.locks
- LockSupport :JDK 中的 rt.jar 里面的LockSupport 是个工具类,
主要作用是挂起和唤醒线程
- LockSupport 类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的。
- LockSupport 是使用 Unsafe 类实现的~Unsafe类,往下翻,使劲翻
- LockSupport工具类的几个常用方法:
- public static void park():如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用Locksupport. park()时会马上返回,否则调用线程会被禁止参与线程的调度, 也就是没拿到与LockSupport关联的许可证的调用park方法的线程会被阻塞挂起。
- 在其他线程调用unpark(Thread thread)方法并且将当前线程作为参数时,调用park方法而被阻塞的线程会返回。
- 另外,如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者线程被虚假唤醒则阻塞线程会返回。
所以在调用park方法时最好也用while等循环条件判断方式防止一下虚假唤醒,里面写的是咱们的打断标记,可以用Thread. interrupted()获取到打断标记
。- 调用interrupt()方法会打断正在执行park()方法的线程
- 调用park()方法而被阻塞的线程被其他线程中断而返回时并不会抛出InterruptedException异常。
- public static void park(Object blocker):
JDK 推荐我们使用带有 bocker 数的 park 方法
,并且blocker被设置为 this。这样当在打印线程堆栈排查问题时就能知道是哪个类被阻塞了。
- 当钱程在没有持有许可证的情况下调用 park 方法而被阻塞挂起时 ,这个 blocker对象会被记录到该线程内部
- 当钱程在没有持有许可证的情况下调用 park 方法而被阻塞挂起时 ,这个 blocker对象会被记录到该线程内部
- public static void unpark(Thread thread)
- 当一个线程调用unpark时,如果参数thread线程没有持有thread与LockSupport类关联的许可证,则让这个参数thread线程持有。
- 如果thread之前因调用 park()而被挂起,则调用unpark后该线程会被唤醒
- 如果thread之前没有调用 park,则调用unpark方法后调用 park方法其会立刻返回
- public static void parkNanos(long nanos)
- 和park 方法类似,如果调用park方法的线程已经拿到了与 LockSupport 关联的许可证,则调用 LockSupport.parkNanos(long nanos)方法后会马上返回。
- ,如果没有拿到许可证,则调用线程会被挂起nanos 时间后修改为自动返回。
- public static void parkNanos(Object blocker, long nanos)
- 相比 park(Object blocker )方法多了个超时时间
- public static void parkUntil(Object blocker, long deadline)
- 这个方法和 parkNanos(Object blocker, long nanos)方法的区别是
- parkNanos(Object blocke r, long nanos)方法从当前算等待nanos秒时间
- parkUntil(Object blocker, long deadline)是指定一个时间点,比如需要等 2017.12.11的12:00:00 ,则把这个时间点转换为从1970年到这个时间点的总毫秒数
- 这个方法和 parkNanos(Object blocker, long nanos)方法的区别是
- public static void park():如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用Locksupport. park()时会马上返回,否则调用线程会被禁止参与线程的调度, 也就是没拿到与LockSupport关联的许可证的调用park方法的线程会被阻塞挂起。
- Condition(条件队列或条件变量 ):java.util.concurrent.locks 类中提供了Condition类来实现线程之间的协调
- synchronized((synchronized见这篇))同时只能与一个共享变量的notify或wait方法(线程的常用方法见这篇)实现同步,而AQS的一个锁可以对应多个条件变量
- 在每个条件变量内部都维护了一个条件队列,用来存放调用条件变量await()方法时被阻塞的线程。注意这个条件队列和 AQS 队列不是一回事。
- 在调用条件变量的 await 方法前也必须先获取条件变量对应的锁
下面例子里面干货多多哦。
//下面例子里面干货多多哦。
ReentrantLock lock = new ReentrantLock(); //(1),创建一个独占锁ReentrantLock对象(ReentrantLock是基于AQS实现的锁,是因为人家AQS帮忙维护队列来着)
Condition condition = lock.newCondition(); //(2),使用创建的(独占锁)Lock对象的newCondition()方法创建了一个ConditionObject变量,这个变量就是Lock锁对应的一个条件变量(一个Lock对象可以创建多个条件变量)。(其实这里的Lock对象等价于synchronized加上共享变量)。
//lock.newCondition()的作用其实是new一个在AQS内部声明的ConditionObject。ConditionObject是AQS的内部类,可以访问AQS 内部的变量(例如状态变state)和方法。
lock.lock();//(3),获取独占锁,,调用 lock.lock()方法就相当于进入了synchronized块(获取了共享变量的内置锁)。当多个线程同时调用 lock.lock()方法获取锁时,只有一个线程获取到了锁,其他线程会被转换为Node节点插入到lock锁对应的AQS阻塞队列里面并做自旋CAS尝试获取锁。如果获取到锁的线程又调用了对应的条件变量的await()方法,则该线程会释放获取到的锁,并被转换为Node节点插入到条件变量对应的条件队列里面。这时候因为调用lock.lock()方法被阻塞到AQS队列里面的一个线程会获取到被释放的锁,如果该线程也调用了条件变量await()方法则该线程会被放入条件变量的条件队列里面
try {
System.out.println("begin wait");
condition.await(); //(4),调用条件变量的await()方法阻塞挂起了当前线程,当其他线程调用条件变量的signal方法时被阻塞的线程才会从await()处返回。和调用Object的wait方法一样如果在没有获取到锁之前调用了条件变量的await()方法则会抛出java.lang.IllegalMonitorStateException异常。(调用条件变 await()方法就相当于调用共享变 wait()方法,)
System.out.println("end wait");
} catch (Exception e) {
e.printStackTrace ();
} finally {
lock.unlock();//(5),释放了获取的锁,,调用lock.unLock()方法就相当于退出synchronized块
}
lock.lock();//(6)
try {
System.out.println("begin signal");
condition.signal();//(7),调用条件变量的signal方法就相当于调用共享变量的notify()方法。调用条件变量signalAll()方法就相当于调用共享变量 notifyAll()方法。当另外一个线程调用条件变量的signa ()或者signalAll()方法时,会把条件队列里面一个或者全部Node节点移动到AQS的阻塞队列里面,等待时机获取锁
System.out.println("end signal");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//(8)
}
并发编程之美里面有个“基于AQS实现自定义同步器”,做波推荐,有兴趣自取。
- ReadWriteLock
写个缓存,没有用锁之前,读写操作出错了,本来咱们实例化了五个线程出来,虽然读操作可以多线程同时执行,但是写操作必须第一个写完第二个再写,你看咱们打印出的结果,说明不用锁时明显多线程写入时出错了
将刚才咱们写的缓存,加上读写锁,控制一下写入的顺序(用读写锁将写入操作保护为一个原子性的操作)
- ReentrantLock ,ReentrantLock见线程安全篇
- ReentrantReadWriteLock :继承自 AQS (也就是说ReentrantReadWriteLock底层是使用AQS 实现的)实现的读写锁ReentrantReadWriteLock里面的读锁在重写tryAcquireShared 时,首先查看写锁是否被其他线程持有,如果是则直接返回false 否则使用 CAS 递增state的高16位(在 ReentrantReadWriteLock 中, state的高16位为获取读锁的次数)。同样的,继承自 AQS 实现的读写锁ReentrantReadWriteLock里面的读锁在重写tryReleaseShared时在内部需要使用CAS算法把当前state值的高16位减1然后返回true,如果CAS失败则返回false
- ReentrantReadWriteLock 可以看成是组合式,
因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读【读写锁 ReentrantReadWriteLock 可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。】
。 - 当读操作远远高于写操作时,这时候使用
读写锁
让读-读可以并发
,提高性能。类似于数据库中的select … from … lock in share mode。提供一个数据容器类内部分别使用读锁保护数据的read()方法,写锁保护数据的write()方法读读可以并发,读写可以互斥
。
- 解决线程安全问题使用 ReentrantLock 就可以。但是 ReentrantLock 是独占锁。某时只有一个线程可以获取该锁,而实际中会有写少读多的场景,显然ReentrantLock满足不了这个需求,所以 ReentrantReadWriteLock应运而生。ReentrantReadWriteLock 采用
读写分离
的策略,允许多个线程可以同时获取读锁
- 读写锁的内部维护了一个ReadLock和WriteLock ,它们依赖Sync实现具体功能。Sync继承自AQS ,并且也提供了公平和非公平的实现
- AQS 中只维护了state状态,而ReentrantReadWriteLock需要维护读状态和写状态,一个state怎么表示写和读两种状态呢?ReentrantReadWriteLock巧妙地使用state的高 16 位表示读状态,也就是获取到读锁的次数;使用低 16 位表示获取到写锁的线程的可重入次数
- 读写锁的原理:
读写锁用的是同一个Sycn同步器,因此等待队列、state等也是同一个
ReentrantReadWriteLock中写锁使用 WriteLock 来实现
- void lock():写锁是个独占锁,某时只有一个线程可以获取该锁。如果当前没有线程获取到读锁和写锁, 当前线程可获取到写锁然后返回。如果当前已经有线程获取到读锁和写锁,则当前请求写锁的线程会被阻塞挂起。另外,写锁是可重入锁,如果当前线程已经获取了该锁,再次获取只是简单地把可重入次数+1后直接返回
- unlock():读读并发
然后,另一种情况:
- void locklnterruptibly():似于Lock()方法,它的不同之处在与它会对中断进行响应 ,也就是当其他线程调用了该线程inerrupt()方法中断了当前线程时当前线程会抛出异常 interruptedException异常。
- boolean Lock():尝试获取写锁,如果当前没有其他线程持有写锁或者读锁,则当前线程获取写锁会成功然后返回true。如果当前已经有其他线程持有写锁或者读锁则该方法直接返回false,且当前线程并不会被阻塞。 如果当前线程已经持有了该写锁则简单增加AQS的状态值后直接返回true
- 重入时升级不支持:
持有读锁的情况下去获取写锁,会导致获取写锁永久等待
ReentrantReadWriteLock 中的读锁是使 ReadLock实现的
- 读锁不支持条件变量
- 重入时降级支持:
持有写锁的情况下去获取读锁
- 一个例子
- 一个例子
- 应用场景:缓存更新策略,更新时是先更新数据库还是先清理缓存,加了锁就可以视为原子性操作,所以谁先谁后没影响
- 先清理缓存,后续查询查到的一直就是旧值,所以先清缓存不行
- 先更新数据库再清理缓存【加个锁更好一点,数据一致性好一点,虽然性能会受影响】,查询次数上来时,能够及时纠正
- 先清理缓存,后续查询查到的一直就是旧值,所以先清缓存不行
- ReentrantReadWriteLock 可以看成是组合式,
static final int SHARED_SHIFT = 16 ;
//共享锁(读锁)状态单位值65536
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//共享锁线程最大个数65535
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//排它锁(写锁)掩码,二进制, 15个1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** 返回读锁线程数 */
static int sharedCount(int c) {
return c >>> SHARED_SHIFT;
}
/** 返回写锁可重入个数*/
static int exclusiveCount(int c) {
return c & EXCLUSIVE_MASK;
}
- java.util.concurrent.locks.StampedLock :该锁提供了三种模式的读写控制【
java.util.concurrent.locks.StampedLock类自JDK8加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合[戳]使用
】,当调用获取锁的系列函数时,会返回 long 型的变量,我们称之为戳记 stamp), 这个戳记代表了锁的状态。其中try系列获取锁的函数,当获取锁失败后会返回为0的stamp值。当调用释放锁和转换锁的方法时需要传入获取锁时返回的 stamp值。
- StampedLock提供的读写锁与 ReentrantReadWriteLock类似,只是StampedLock提供的是不重入锁。但是StampedLock通过提供乐观读锁在多线程多读的情况下提供了更好的性能。这是因为获取乐观读锁时不需要进行 CAS 操作设置锁的状态,而只是简单的测试状态。
- StampedLock不支持条件变量
- StampedLock不支持可重入
- StampedLock提供的三种读写模式的锁分别如下
- StampedLock 支持这三种锁在一定条件下进行相互转换。例如 long tryConvertToWriteLock(long tamp 期望把 tamp标示的锁升级为写锁。这个函数会在下面几种情况下返回一个有效的stamp 也就是晋升写锁成功)
- StampedLock提供的读写锁与 ReentrantReadWriteLock类似,只是StampedLock提供的是不重入锁。但是StampedLock通过提供乐观读锁在多线程多读的情况下提供了更好的性能。这是因为获取乐观读锁时不需要进行 CAS 操作设置锁的状态,而只是简单的测试状态。
- AbstractQueuedSynchronizer(AQS,抽象同步队列,,这个类在java.util.concurrent.locks包下面。):并发包中锁的底层就是使用AQS实现的。
阻塞式的锁
,【AQS是一个抽象队列同步器,通过维护一个状态标志位state和一个先进先出的(FIFO)的线程等待队列来实现一个多线程访问共享资源的同步框架
】。AQS的类图结构
- AQS是一个用来构建锁和同步器的框架,
使用 AQS 能简单且高效地构造出大量应用广泛的同步器,比如我们提到的 ReentrantLock、Semaphore、CountDownLatch
,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的
。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器
- AQS 是一个锁框架,
AQS它定义了锁的实现机制,并开放出扩展的地方,让子类去实现
,比如我们在 lock 的时候,AQS 开放出 state 字段,让子类可以根据 state 字段来决定是否能够获得锁,对于获取不到锁的线程 AQS 会自动进行管理,无需子类锁关心,这就是 lock 时锁的内部机制,封装的很好,又暴露出子类锁需要扩展的地方;
- AQS 是一个锁框架,
- AQS的原理:
AQS的原理大概是这样的,给每个共享资源都设置一个共享锁, 线程在需要访问共享资源时首先需要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该共享资源,如果没有获取到共享锁,该线程被放入到等待队列中,等待下一次资源调度
。
- AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
- AQS 底层是由同步队列 + 条件队列联手组成:
- 同步队列管理着获取不到锁的线程的排队和释放
- 条件队列是在一定场景下,对同步队列的补充,比如获得锁的线程从空队列中拿数据,肯定是拿不到数据的,这时候条件队列就会管理该线程,使该线程阻塞;
- 同步队列管理着获取不到锁的线程的排队和释放
- AQS 底层是由同步队列 + 条件队列联手组成:
AQS只是一一个框架(模板模式),只定义了一个接口,具体资源的获取、释放都交由自定义同步器去实现。不同的自定义同步器争取用共享资源的方式也不同,自定义同步器在实现时只需实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护,如获取资源失败入队、唤醒出队等,AQS已经在顶层实现好,不需要具体的同步器在做处理
。- 一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
- AQS 底层使用了模板方法模式,同步器的设计是基于模板方法模式的,
如果需要自定义同步器一般的方式是这样
(模板方法模式很经典的一个应用):使用者继承 AbstractQueuedSynchronizer 并重写指定的方法
。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放,比如自定义同步器时需要重写下面几个 AQS 提供的钩子方法:)protected boolean tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 protected boolean tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 protected int tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 protected boolean tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 protected boolean isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
钩子方法是一种被声明在抽象类中的方法,它可以是空方法(由子类实现),也可以是默认实现的方法。模板设计模式通过钩子方法控制固定步骤的实现
。- 除了上面提到的钩子方法之外,AQS 类中的其他方法都是 final ,所以无法被其他类重写。
- 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法
- 捡田螺的小男孩老师关于实现自定义同步器和AQS派生出如ReentrantLock、Semaphore的使用案例的文章
- AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,
这个机制 AQS 是用 CLH 队列锁实现的
,即将暂时获取不到锁的线程加入到队列中
。- CLH(Craig,Landin and Hagersten)队列是一个虚拟的FIFO双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
- CLH(Craig,Landin and Hagersten)队列中的Node节点:
node节点作为CLH队列的一个节点,有着5条属性,分别是waitStatus 、prev、next、thread、nextWater
。
- Node结点是对每一个访问同步代码的线程的封装,如是否被阻塞,是否等待唤醒,是否已经被取消等。CLH同步队列中,一个节点表示一个线程,它保存着需要同步的线程本身以及线程的状态线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next),condition队列的后续节点(nextWaiter)
- node节点的属性:状态(waitStatus)。
- node节点的属性:状态(waitStatus)。
- CLH队列入列以及出列
- CLH队列
入列就是tail指向新节点、新节点的prev指向当前最后的节点,当前最后一个节点的next指向当前节点
。addWaiter设置尾节点失败的话,调用enq(Node node)方法设置尾节点
- 首节点的线程释放同步状态后,将会唤醒它的后继节点(next),而后继节点将会在获取同步状态成功时将自己设置为首节点
- CLH队列
- Node结点是对每一个访问同步代码的线程的封装,如是否被阻塞,是否等待唤醒,是否已经被取消等。CLH同步队列中,一个节点表示一个线程,它保存着需要同步的线程本身以及线程的状态线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next),condition队列的后续节点(nextWaiter)
- CLH(Craig,Landin and Hagersten)队列中的Node节点:
- CLH(Craig,Landin and Hagersten)队列是一个虚拟的FIFO双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
- AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
- 在AQS中维持了一个单一的状态信息state(可以通过getState、setState、compareAndSetState等方法修改这个state值),
对于AQS来说线程同步的关键也是对状态值state进行相应操作
private volatile int state;//共享变量,使用volatile修饰保证线程可见性 ... //返回同步状态的当前值 protected final int getState() { return state; } //设置同步状态的值 protected final void setState(int newState) { state = newState; } //原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
- 对于ReentrantLock的实现来说state可用来表示当前线程获取锁的可重入次数
- 对于读写锁ReentrantReadWriteLock来说,state的高16位表示读状态(其实也就是获取该读锁的次数),低16位表示获取到写锁的线程的可重入次数
- 对于semaphore来说,state用来表示当前可用信号的个数
- 对于CountDownlatch来说state用来表示计数器当前值
- AQS特点:
图中黄色三角表示该Node的waitStatus状态,其中0为默认正常状态。Node的创建是懒惰的
,其中第一个Node称为Dummy (哑元)或哨兵,用来占位,并不关联线程
- 在AQS中维持了一个单一的共享状态state,来实现同步器同步。根据
state是否属于一个线程可以把操作state的方式分为独占方式和共享方式
:或者说AQS定义了两种资源共享方式:独享式和共享式
- 独占方式:使用独占方式获取的资源是与具体线程绑定的【
只有一个线程可以执行或者说享用该资源
】,意思就是说如果一个线程获取到了资源,这个资源就会标记是这个线程获取到了,其他线程再尝试操作 state 获取资源时会发现当前该资源不是自己持有的,就会在获取失败后被阻塞,具体的实现比如~独占锁ReentrantLock更多见线程安全篇- Exclusive(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁:
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁
,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面
。如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
。相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 独占方式下获取和释放资源使用的方法为:void acquire(int args)、void acquireInterruptibly(int args)、boolean release(int args),在独占方式下获取与释放资源的流程如下图
- acquire(int arg)是独占式获取同步状态的方法:【比如,以ReentranLock的非公平锁为例。我们主要关注的方法是lock(),和unlock()。】
- acquire(int arg)是独占式获取同步状态的方法:【比如,以ReentranLock的非公平锁为例。我们主要关注的方法是lock(),和unlock()。】
- AQS 类并没有提供可用的tryAcquire 和tryRelease 方法。
tryAcquire和tryRelease 需要由具体的子类来实现
。子类在实现tryAcquire和tryRelease时要根据具体场景使用 CAS 算法尝试修改state状态值,成功则返 true ,否则返回 false。子类还需要定义在调用acquire和release方法时state状态值的增减代表什么含义。在重写 tryAcquire 时,在内部需要使用 CAS算法查看当前 state是否为0 ,如果为0使用 CAS设置为1,并设置当前锁的持有者为当前线程,而后返回true,如果CAS失败则返回false
- Exclusive(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁:
- 共享方式:对应共享方式的资源与具体线程是不相关的【
多个线程可同时执行,具体的Java实现有Semaphore和CountDownLatch
】,当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的需要,则当前线程只需要使用CAS方式进行资源的获取即可~比如Semaphore- Share(共享):多个线程可同时执行,如 CountDownLatch、Semaphore、CyclicBarrier、ReadWriteLock。
- 在共享方式下获取和释放资源的方法为void acquireShared(int args)、void acquireSharedInterruptibly(int args)、boolean releaseShared(int args)。共享方式下获取与释放资源的流程如下图:
- acquireShared(long arg)是共享式获取同步状态的方法:
- acquireShared(long arg)是共享式获取同步状态的方法:
- AQS 类并没有提供可用的tryAcquireShared 和tryReleaseShared 方法。tryAcquireShared 和tryReleaseShared需要由具体的子类来实现。子类在实现tryAcquireShared 和tryReleaseShared时要根据具体场景使用 CAS 算法尝试修改state状态值,成功则返 true ,否则返回 false。
- 独占方式:使用独占方式获取的资源是与具体线程绑定的【
- AQS有个内部类ConditionObject是用来结合锁实现线程同步的(这个ConditionObject内部类可以直接访问AQS对象内部的变量,比如state状态值和AQS队列)
- conditionObject是条件变量,
ConditionObject实现了Condition接口
,给AQS提供条件变量的支持 。每个条件变量对应一个条件队列 (单向链表队列)
,其用来存放调用条件变量的await方法后被阻塞的线程
- conditionObject是条件变量,
- 如何维护 AQS 提供的队列:
- 入队操作:当一个线程获取锁失败后该线程会被转换为Node节点,然后就会使用enq(final Node node)方法将该节点插入到 AQS 的阻塞队列
private Node enq(final Node node){ //码在第一次循环中,当要在AQS队列尾部插入元素时,AQS队列头、尾节点都指向null for(;;){ Node t = tail; //(1),这一句代码执行完后节点t指向了尾部节点(这时候t也为null) if(t == null){ // Must initialize if(compareAndSetHead(new Node())){//(2),使用CAS算法设置一个哨兵节点为头节点,如果CAS设置成功则让尾部节点也指向哨兵节点,此时tail和head节点都指向哨兵节点 tail = head; } }else{ node.prev = t;//(3) if(compareAndSetTail(t, node)){//(4) t.next = node; return t; } } } }
- 入队操作:当一个线程获取锁失败后该线程会被转换为Node节点,然后就会使用enq(final Node node)方法将该节点插入到 AQS 的阻塞队列
- AQS是一个用来构建锁和同步器的框架,
PART3-4:上面说了java.util.concurrent包下的AQS(AbstractQueuedSynchronizer抽象同步队列)的基本。现在举个例子,基于AQS实现一个类,那咱们该怎么考虑
- 举个例子,基于AQS实现一个类
public class HuLock{
//获取锁
public void lock(){}
//释放锁
public void unlock(){}
}
HuLock huLock = new HuLock();
public void doSomething(){
//获取锁,表示同一时刻只允许一个线程执行这个方法
huLock.lock();
try{
...
} finally{
//优雅,在finally中释放锁
huLock.unlock();
}
}
来个测试代码测试一下:
//构造一个,可能发生线程安全问题的共享变量
private static long count = 0;
//让两个线程,并发对count++
public static void main(String[] args) throws Exception{
Thread thread1 = new Thread(() -> add());
Thread thread2 = new Thread(() -> add());
//启动两个线程
thread1.start();
thread2.start();
//等待两个线程执行结束
thread1.join();
thread2.join();
System.out.println(count);
private static ExampleLock example
= new ExampleLock();
//循环执行count++;,进行100000次
private static void add(){
exampleLock.lock();
for(int i = 0; i < 100000; i++){
count++;
}
add2();
//要是没啥异常,我就直接释放锁了
exampleLock.unlock();
}
}
@Override
public boolean tryAcquire(int acquires) {
// CAS 方式尝试获取锁,成功返回true,失败返回false
if (compareAndSetState(0, 1)) {
return true;
}
return false;
}
这段代码在尝试获取锁时一上来判断并开始抢锁,一旦抢锁成功就返回true。那我可以这样呀,就是加入一些机制,让线程不要一有机会就抢锁,先考虑一下其他线程的感受再抢锁,公平一点,哥们,你是不是从来没抢到呀,那要不你先来这种
//此时就得研究一下AQS的内部实现逻辑了,也就是原理了,干他丫的
public abstract class AbstractQueuedSynchronizer {
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
static final class Node {}
}
static final class Node {
// ... 省略一些暂不关注的
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
//想要公平锁,调用的时候就传true进来
public HuLock(boolean fair){
sync = fair ? new FairSync() : new NonFairSync();
}
巨人的肩膀:
Java并发编程之美
B站的狂神说
低并发编程