JUC基础
JUC概述
juc是并发编程的工具类
进程与线程
进程是系统进行资源分配和调度的基本单元,是系统中正在运行的一个应用程序。而线程是操作系统进行调度的最小单元,他被包含在进程中,是进程中的实际运作单元,是进程中独立运行的单元执行流。
线程的状态
- NEW(新建)
- RUNNABLE(准备就绪)
- BLOCKED(阻塞)
- WAITING(不见不散)
- TIMED_WAITING(过时不候)
- TERMINATED(终结)
wait()和sleep()的区别
- sleep 是 Thread 的静态方法,wait 是 Object 的方法,任何对象实例都能调用。
- sleep 不会释放锁,它也不需要占用锁。wait 会释放锁,但调用它的前提是当前线程占有锁(即代码要在 synchronized 中)。
- 它们都可以被 interrupted 方法中断。
并发与并行
串行
一次取一个任务,执行完再取下一个
并行
一次取多个任务,交替执行,每个任务走走停停(同一个时刻,多个线程访问一个资源,多个对一点。如抢票和秒杀)
并发
多个任务同时进行(如泡面中的煲水和拆料包)
管程
保证同一个时刻只有一个进程在管程内执行,这由编译器实现,但是不能保证进程以设计的顺序执行。在一个进程获取到管程后,只有该进程释放,其他进程才能拿到管程
用户线程和守护线程
- 用户线程是平时用到的普通线程(如普通线程),只要用户线程未结束,jvm就不会结束。
- 守护线程是运行在后台的线程(如垃圾回收),即使守护线程未结束,用户线程结束,jvm也会结束。
Lock接口
Synchronized
- 可修饰代码块,作用于调用这个代码块的对象。
- 可修饰方法,作用于调用这个方法的对象(当子类方法调用到到父类的同步方法时,该子类方法也相当于同步的)
- 可修饰静态方法,作用于这个类的所有对象。
- 可修饰一个类,作用于这个类的所有对象。
什么是Lock
Lock锁比同步代码块和方法提供了更广泛的锁操作。
Lock与Synchronized的区别
- synchronized是java内置的,是java关键字。而Lock是一个类,也可以实现同步访问
- Lock实现同步要手动上锁和释放锁,而Synchronized是自动上锁和释放锁,Lock若不释放锁则有可能会造成死锁
- Lock 可以提高多个线程进行读操作的效率。 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源 非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized。
Lock接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock()方法
lock()方法是用来获取锁的,通常用try catch finally修饰,并把unlock()方法放在finally快中执行为了保证锁的释放,避免死锁
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{lock.unlock(); //释放锁
}
newCondition()方法
关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock 锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式。
用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知,Condition 比较常用的两个方法:
- await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行。
- signal()用于唤醒一个等待的线程。 注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关 的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前 Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦 获得锁成功就继续执行。
ReentrantLock
ReentrantLock,意思是“可重入锁”,关于可重入锁的概念将在后面讲述。
ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更 多的方法。
线程间通信
线程间通信的模型有两种:共享内存和消息传递
线程间定制化通信
要添加标志位
案例如下: 问题: A 线程打印 5 次 A,B 线程打印 10 次 B,C 线程打印 15 次 C,按照此顺序循环 10 轮
class DemoClass{
//通信对象:0--打印 A 1---打印 B 2----打印 C
private int number = 0;
//声明锁
private Lock lock = new ReentrantLock();
//声明钥匙 A
private Condition conditionA = lock.newCondition();
//声明钥匙 B
private Condition conditionB = lock.newCondition();
//声明钥匙 C
private Condition conditionC = lock.newCondition();
/**
* A 打印 5 次
*/
public void printA(int j){
try {
lock.lock();
while (number != 0){
conditionA.await();
}
System.out.println(Thread.currentThread().getName() + "输出 A,第" + j + "
轮开始");
//输出 5 次 A
for (int i = 0; i < 5; i++) {
System.out.println("A");
}
//开始打印 B
number = 1;
//唤醒 B
conditionB.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
} /**
* B 打印 10 次
*/
public void printB(int j){
try {
lock.lock();
while (number != 1){
conditionB.await();
}
System.out.println(Thread.currentThread().getName() + "输出 B,第" + j + "
轮开始");
//输出 10 次 B
for (int i = 0; i < 10; i++) {
System.out.println("B");
}
//开始打印 C
number = 2;
//唤醒 C
conditionC.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
} /**
* C 打印 15 次
*/
public void printC(int j){
try {
lock.lock();
while (number != 2){
conditionC.await();
}
System.out.println(Thread.currentThread().getName() + "输出 C,第" + j + "
轮开始");
//输出 15 次 C
for (int i = 0; i < 15; i++) {
System.out.println("C");
}
System.out.println("-----------------------------------------");
//开始打印 A
number = 0;
//唤醒 A
conditionA.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
} }
}
测试类
/**
* volatile 关键字实现线程交替加减
*/
public class TestVolatile {
/**
* 交替加减
* @param args
*/
public static void main(String[] args){
DemoClass demoClass = new DemoClass();
new Thread(() ->{
for (int i = 1; i <= 10; i++) {
demoClass.printA(i);
}
}, "A 线程").start();
new Thread(() ->{
for (int i = 1; i <= 10; i++) { demoClass.printB(i);
}
}, "B 线程").start();
new Thread(() ->{
for (int i = 1; i <= 10; i++) {
demoClass.printC(i);
}
}, "C 线程").start();
}
}
注意
集合线程安全
List线程不安全解决?
-
Vector
Vector 是矢量队列,它是 JDK1.0 版本添加的类。继承于 AbstractList,实现 了 List, RandomAccess, Cloneable 这些接口。 Vector 继承了 AbstractList, 实现了 List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功 能。 Vector 实现了 RandmoAccess 接口,即提供了随机访问功能。 RandmoAccess 是 java 中用来被 List 实现,为 List 提供快速访问功能的。在 Vector 中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访 问。 Vector 实现了 Cloneable 接口,即实现 clone()函数。它能被克隆。 ==和 ArrayList 不同,Vector 中的操作是线程安全的。==
-
Collections
Collections 提供了方法 synchronizedList 保证 list 是同步线程安全的
-
CopyOnWriteArrayList(重点)
首先我们对 CopyOnWriteArrayList 进行学习,其特点如下: 它相当于线程安全的 ArrayList。和 ArrayList 一样,它是个可变数组;但是和 ArrayList 不同的时,它具有以下特性:
-
它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
-
它是线程安全的。
-
因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
-
迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
-
使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代 器时,迭代器依赖于不变的数组快照。
-
独占锁效率低:采用读写分离思想解决
-
写线程获取到锁,其他写线程阻塞
-
复制思想: 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容 器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素 之后,再将原容器的引用指向新的容器。 这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来 得及写会内存,其他的线程就会读到了脏数据。
==这就是 CopyOnWriteArrayList 的思想和原理。就是拷贝一份。==
原因分析(重点): ==动态数组与线程安全==
下面从“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList 的原理进行说明。
- “动态数组”机制
- 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile 数组”, 这就是它叫做 CopyOnWriteArrayList 的原因
- 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList 效率很低;但是单单只是进行遍历查找的话,效率比较高。
- “线程安全”机制
- 通过 volatile 和互斥锁来实现的。
- 通过“volatile 数组”来保存数据的。一个线程读取 volatile 数组时,总能看到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读取到的数据总是最新的”这个机制的保证。
- 通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥锁”,就达到了保护数据的目的。
Callable&Future 接口
为什么要用Callable接口创建线程
用于返回结果
引入FutureTask
java 库具有具体的 FutureTask 类型,该类型实现 Runnable 和 Future,并方 便地将两种功能组合在一起。 可以通过为其构造函数提供 Callable 来创建 FutureTask。然后,将 FutureTask 对象提供给 Thread 的构造函数以创建 Thread 对象。因此,间接地使用 Callable 创建线程。
- 当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态
- 一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
- 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法
- 一旦计算完成,就不能再重新开始或取消计算
- get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常
- get 只计算一次,因此 get 方法放到最后
JUC三大辅助类
JUC 中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过 多时 Lock 锁的频繁操作。这三种辅助类为:
- CountDownLatch: 减少计数
- CyclicBarrier: 循环栅栏
- Semaphore: 信号灯
减少计数CountDownLatch
CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行 减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法 之后的语句。它是一个同步工具类,允许一个或多个线程一直等待,直到其他线程运行完成后再执行。
- CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞
- 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程不会阻塞)
- 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行
应用举例:场景: 6 个同学陆续离开教室后值班同学才可以关门。
循环栅栏 CyclicBarrier
CyclicBarrier 的构造方法第一个参数是目标障碍数,第二个参数是当达到目标障碍数时要执行的线程。如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier.await() 理解为加 1 操作。
应用举例:场景: 集齐 7 颗龙珠就可以召唤神龙
CountDownLatch与CyclicBarrier
- CyclicBarrier 方法多,可以用reset()方法来重置CyclicBarrier,让栅栏可以反复用。而CountDownLatch如果count变为0了,那么只能保持在这个0的最终状态,不能重新再用。
- CyclicBarrier 是让一组线程等待某个事件发生,如果发生了,这组线程可以继续执行;CountDownLatch是一个线程或多个线程等待一组线程执行完毕。不同的点就在于当count变为0之后,CyclicBarrier是让这组线程不再阻塞,而继续执行;而CountDownLatch是让等待的线程不阻塞,继续执行。
信号灯 Semaphore
Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线 程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方 法获得许可证,release 方法释放许可.
应用场景:多个线程抢占有限资源。如(抢车位, 6 部汽车 3 个停车位)
读写锁
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那 么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以 应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源, 就不应该允许其他线程对该资源进行读和写的操作了。 针对这种场景,JAVA 的并发包提供了读写锁 ReentrantReadWriteLock, 它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称 为排他锁
线程进入读锁的前提条件:
- 没有其他线程的写锁
- 没有其他线程的写有写请求,但调用线程和持有锁的线程是同一个(可重入锁)。
线程进入写锁的前提条件
- 没有其他线程的写锁和读锁。
读写锁有以下三个重要的特性
- 公平选择性(但是非公平锁的吞吐量更高,因为减少了切换上下文的资源的消耗)
- 可重入性
- 写锁可降级,读锁不可升级。(写锁降级流程:获取写锁,再获取读锁,先释放写锁)
阻塞队列
Concurrent 包中,BlockingQueue 很好的解决了多线程中,如何高效安全 “传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建 高质量的多线程程序带来极大的便利。本文详细介绍了 BlockingQueue 家庭 中的所有成员,包括他们各自的功能以及常见使用场景。
为什么需要BlockingQueue
减少了程序员对多线程的工作,不要考虑什么时候阻塞线程和唤醒线程,使程序员能更加专注于逻辑代码。这都是有阻塞队列给包办了。
在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
BlockingQueue 核心方法
常见的BlockingQueue
- ArrayBlockingQueue(常用):由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue(常用):由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列。往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
- LinkedTransferQueue:由链表组成的无界阻塞队列。
- LinkedBlockingDequ:由链表组成的双向阻塞队列
ThreadPool 线程池
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
主要特点
- 降低了资源的消耗
- 提高了响应速度
- 提高了线程的可管理性
创建线程池的参数
- corePoolSize 线程池的核心线程数
- maximumPoolSize 能容纳的最大线程数
- keepAliveTime 空闲线程存活时间
- unit 存活的时间单位
- workQueue 存放提交但未执行任务的队列
- threadFactory 创建线程的工厂类
- handler 等待队列满后的拒绝策略 线程池中,有三个重要的参数,决定影响了拒绝策略:corePoolSize - 核心线 程数,也即最小的线程数。workQueue - 阻塞队列 。 maximumPoolSize - 最大线程数 当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻 塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池 的拒绝策略了。 总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。
拒绝策略(重点)
- CallerRunsPolicy: 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度快,可能导致程序阻塞,性能效率上必然的损失较大
- AbortPolicy: 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
- DiscardPolicy: 直接丢弃,其他啥都没有
- DiscardOldestPolicy: 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
线程池的种类与创建
创建
使用Executors工具类创建
种类
-
newCachedThreadPool(常用)
- 作用:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
- 特点:线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)。线程池中的线程可进行缓存重复利用和回收(回收默认时间为 1 分钟)。当线程池中,没有可用线程,会重新创建一个线程
- 场景:适用于创建一个可无限扩大的线程池,服务器负载压力较轻,执行时间较短,任务多的场景
-
newFixedThreadPool(常用)
- 作用:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
- 特征: 线程池中的线程处于一定的量,可以很好的控制线程的并发量。线程可以重复被使用,在显示关闭之前,都将一直存在。超出一定量的线程被提交时候需在队列中等待
- 场景: 适用于可以预测线程数量的业务中,或者服务器负载较重,对线程数有严格限制的场景
-
newSingleThreadExecutor(常用)
- 作用:创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的newFixedThreadPool 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。
- 特征: 线程池中最多执行 1 个线程,之后提交的线程活动将会排在队列中以此执行
- 场景: 适用于需要保证顺序执行各个任务,并且在任意时间点,不会同时有多个线程的场景
-
newScheduleThreadPool(了解)
- 作用: 线程池支持定时以及周期性执行任务,创建一个 corePoolSize 为传入参数,最大线程数为整形的最大数的线程池
- 特征:(1)线程池中具有指定数量的线程,即便是空线程也将保留 (2)可定时或者延迟执行线程活动
- 场景: 适用于需要多个后台线程执行周期任务的场景
-
newWorkStealingPool
- 概述:jdk1.8 提供的线程池,底层使用的是 ForkJoinPool 实现,创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用 cpu 核数的线程来并行执行任务
- 场景: 适用于大耗时,可并行执行的场景
为什么不允许使用 Executors.的方式手动创建线程池,如下图
Fork/Join
Fork/Join 框架简介
Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子 任务结果合并成最后的计算结果,并进行输出。Fork/Join 框架要完成两件事 情:
- Fork:将任务拆分成小任务
- Join:将小任务的计算结果合并
怎么用?
- ForkJoinTask:我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。该类提供了在任务中执行 fork 和 join 的机制。通常情况下我们不需要直接集成 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:
- RecursiveAction:用于没有返回结果的任务
- RecursiveTask:用于有返回结果的任务
- ForkJoinPool:ForkJoinTask 需要通过 ForkJoinPool 来执行
- RecursiveTask: 继承后可以实现递归(自己调自己)调用的任务
Fork/Join 框架的实现原理
ForkJoinPool 由 ForkJoinTask 数组和 ForkJoinWorkerThread 数组组成, ForkJoinTask 数组负责将存放以及将程序提交给 ForkJoinPool,而 ForkJoinWorkerThread 负责执行这些任务。
案例
/**
* 递归累加
*/
public class TaskExample extends RecursiveTask<Long> {
private int start;
private int end;
private long sum;
/**
* 构造函数
* @param start
* @param end
*/
public TaskExample(int start, int end){
this.start = start;
this.end = end;
}
/**
* The main computation performed by this task.
*
* @return the result of the computation
*/@Override
protected Long compute() {
System.out.println("任务" + start + "=========" + end + "累加开始");
//大于 100 个数相加切分,小于直接加
if(end - start <= 100){
for (int i = start; i <= end; i++) {
//累加
sum += i;
}
}else {
//切分为 2 块
int middle = start + 100;
//递归调用,切分为 2 个小任务
TaskExample taskExample1 = new TaskExample(start, middle);
TaskExample taskExample2 = new TaskExample(middle + 1, end);
//执行:异步
taskExample1.fork();
taskExample2.fork();
//同步阻塞获取执行结果
sum = taskExample1.join() + taskExample2.join();
}
//加完返回
return sum;
}
}
/**
* 分支合并案例
*/public class ForkJoinPoolDemo {
/**
* 生成一个计算任务,计算 1+2+3.........+1000
* @param args
*/
public static void main(String[] args) {
//定义任务
TaskExample taskExample = new TaskExample(1, 1000);
//定义执行对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
//加入任务执行
ForkJoinTask<Long> result = forkJoinPool.submit(taskExample);
//输出结果
try {
System.out.println(result.get());
}catch (Exception e){
e.printStackTrace();
}finally {
forkJoinPool.shutdown();
}
}
}
CompletableFuture(实现异步回调)
CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞, 可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可 以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。
CompletableFuture 实现了 Future, CompletionStage 接口,实现了 Future 接口就可以兼容现在有线程池框架,而 CompletionStage 接口才是异步编程 的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的 CompletableFuture 类。
但是业务中通常使用mq实现异步回调。