前情提要
需要有一定的多线程基础,需要的小伙伴可以回到这个帖子来复习Java多线程
何为JUC?
java.util.concurrent包
回顾基础
回顾线程和进程的区别
进程:一个程序,QQ.exe Music.exe 程序的集合;
一个进程往往可以包含多个线程,至少包含一个!
Java默认有几个线程? 2 个 mian、GC
线程:开了一个进程 Typora,写字,自动保存(线程负责的)
对于Java而言:Thread、Runnable、Callable
Java 真的可以开启线程吗? 开不了,因为底层调用还是jvm,无法直接操作系统
回顾并发、并行
并发编程:并发、并行
并发(多线程操作同一个资源)
同一时刻多个线程在访问同一个资源,多个线程对一个点
例子:春运抢票 电商秒杀…
并行(多个人一起行走)
多项工作一起执行,之后再汇总
例子:泡方便面,电水壶烧水,一边撕调料倒入桶中
Java代码查看CPU可用的物理线程数
package com.kuang.demo01;
public class Test1 {
public static void main(String[] args) {
// 获取cpu的线程数 // CPU 密集型,IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
并发编程的本质:充分利用CPU的资源
回顾线程的有几个状态
在源码里有个可以根据枚举进行查看
一共有六大状态
- NEW 新生
- RUNNABLE 运行
- BLOCKED 阻塞
- WAITING 等待(死等)
- TIMED_WAITING 超时等待(过期不候)
- TERMINATED 死亡
wait和sleep的区别
-
来自不同的类
wait -> Object
sleep -> Thread
-
关于锁的释放
wait:释放锁(醒着的,可以放锁,只是在等待),wait时别人可以获取锁
sleep:不释放锁(抱着锁睡的~)
-
使用的范围不同
wait只能在synchronized中使用
sleep可以在任意地方使用
-
是否需要捕获异常
wait需要因为会有线程中断的异常
sleep要捕获异常,因为会有超时等待的情况(过期不候了之后该怎么去处理)
管程
管程(monitor)是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现).但是这样并不能保证进程以设计的顺序执行
JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁
执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程
管程对象进入要加锁,退出要解锁。
用户线程和守护线程
-
用户线程:平时我们用的都是用户线程,自定义的线程
-
守护线程:GC线程
用户线程:平时用到的普通线程,自定义线程
守护线程:运行在后台,是一种特殊的线程,比如垃圾回收
当主线程结束后,用户线程还在运行,JVM 存活
如果没有用户线程,都是守护线程,JVM 结束
注意:设置守护线程要在start方法之前做好,start之后再去设置守护线程就不对了
Lock锁
传统 Synchronized
回顾Sychronized用法
synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,因此,synchronized 关键字不能被继承。如果在父类中的某个方法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
- 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修改一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象
抢票的示例
如果没加Sychronized就会出现并发安全的问题
sychronized是一个全自动的锁
线程就是一个单独的资源类,没有任何附属的操作!
并发:多线程操作同一个资源类, 把资源类丢入线程
多线程编程步骤(上)
- 创建资源类,创建属性和操作方法
- 创建多线程调用资源类的方法
Lock锁
Lock是一个接口,位于java.util.concurrent.locks
包下
其有多个实现类。
Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允
许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对
象。Lock 提供了比 synchronized 更多的功能。
Lock 与的 Synchronized 区别
• Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内置特性。Lock 是一个类,通过这个类可以实现同步访问;
• Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
可重入锁ReentrantLock
可重入锁,也是递归锁,好比进了自己家的门,在去别的房间就不需要钥匙了
可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
synchronized 和 ReentrantLock 都是可重入锁。
可重入锁的意义之一在于防止死锁。
默认情况下,ReentrantLock的默认无参构造方法是new一个不公平(unfair)锁
但是在构造方法中传入一个boolean,即可控制new的锁是否为公平锁
true:公平锁(fair lock)
false:不公平锁(unfair lock)
为什么默认要用非公平锁?
因为公平。因为如果使用公平锁,会有可能导致执行耗时长的线程优先执行,会导致CPU使用效率下降。
jdk帮助文档看一下介绍
公平锁:十分公平:可以先来后到
非公平锁:十分不公平:可以插队 (默认)
Lock三部曲
- 1、 new ReentrantLock();
- 2、 lock.lock(); // 加锁
- 3、 finally里=> lock.unlock(); // 解锁
一定要以try finally的情况来写lock和unlock,为什么?
所以一定要在finally中加入unlock来确保可以解锁
Sychronized和Lock的区别
- Synchronized是内置关键字,Lock是一个类
- Synchronized无法判断是否获取到了锁,Lock可判断是否获得到了锁
- Synchronized会自动获取和释放锁
- Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去
- Synchronized 可重入锁,不可以中断的,非公平 Lock :可重入锁,可以判断锁,非公平(可以自己设置)
- Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码
线程间通信
多线程编程步骤(中)
多线程编程步骤(下)
最后一点:防止虚假唤醒问题
多线程编程步骤总结
线程间通信例子
生产者消费者模式Synchronized版本
需求:两个线程交替修改资源,当前线程如果不符合条件就会停下等待被唤醒,一个线程+1,另一个线程-1
按照预先的步骤来
先写资源类
//第一步,创建资源类,定义属性和操作方法
class Share{
private int num = 0;
//+1的方法
public synchronized void increse() throws InterruptedException {
//第二步,判断 干活 通知
if (num!=0){
//不是0当前线程就需要等待
this.wait();
}
//干活
++num;
System.out.println(Thread.currentThread().getName()+"当前数量"+num);
//唤醒其他线程
this.notifyAll();
}
//-1的方法
public synchronized void decrease() throws InterruptedException {
//第二步,判断 干活 通知
if (num!=1){
//判断,不等于1就需要等待
this.wait();
}
//干活
--num;
System.out.println(Thread.currentThread().getName()+"当前数量"+num);
//通知
this.notifyAll();
}
}
再写主线程进行调用
public class ThreadDemo1 {
public static void main(String[] args) {
//创建多个线程,调用资源类的方法
//先创建资源类
Share share = new Share();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.increse();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程1,增加方法").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程2,减少方法").start();
}
}
运行测试
产生的问题:线程间虚假通信
如果再引入线程3和线程4两个线程就会造成虚假唤醒的问题
解决虚假唤醒的问题
wait有一个特点,在哪里睡就会在哪里醒
如果是if做判断的时候:当wait醒来就不做判断了直接从醒来的地方往下走就会出错
这个时候改为while:当wait醒来的时候,就会重复进行一次条件判断。
将if换成while,解决了虚假唤醒的问题
生产者消费者模式Lock版本
Lock下的方法
Lock接口要创建钥匙对象
Condition condition = lock.newCondition();这个对象才能操作await和signal方法(下面的一系列方法都是钥匙方法)
Share(资源类)
class Share2{
//声明Lock锁对象
private Lock lock = new ReentrantLock();
//声明钥匙对象来进行等待和唤醒操作,同属Lock接口(Lock的钥匙)
private Condition condition = lock.newCondition();
private int num = 0;
public void increase() throws InterruptedException {
//上锁
lock.lock();
try {
while (num!=0){
//一定要在while里判断,防止虚假唤醒,不等于0就是没到+1的时候就需要继续等待
condition.await();
}
num++;
condition.signalAll();//操作完了一定要唤醒!!!
System.out.println(Thread.currentThread().getName()+"当前num:"+num);
}finally {
//一定要解锁
lock.unlock();
}
}
public void decrease() throws InterruptedException {
lock.lock();
try {
while (num!=1){
//一定要在while里判断,防止虚假唤醒,不等于1就是没到-1的时候就需要继续等待
condition.await();
}
num--;
condition.signalAll();//操作完了一定要唤醒!!!
System.out.println(Thread.currentThread().getName()+"当前num:"+num);
}finally {
lock.unlock();
}
}
}
主启动类
public class ThreadDemo2 {
public static void main(String[] args) {
Share2 share2 = new Share2();
new Thread(()->{
for (int i = 0; i < 30; i++) {
try {
share2.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程1增加方法").start();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
try {
share2.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程2减少方法").start();
new Thread(()->{
for (int i = 0; i < 30; i++) {
try {
share2.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程3增加方法").start();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
try {
share2.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程4减少方法").start();
}
}
启动测试,交替执行可以解决虚假唤醒的问题
线程间定制化通信
之前的输出方式是无序的。1、2、3、4线程谁都有可能先执行。
我们可以按照约定的顺序进行输出,这就叫线程间定制化通信
业务介绍
问题: A 线程打印 5 次 A,B 线程打印 10 次 B,C 线程打印 15 次 C,按照此顺序循环 10 轮
具体思路
Share3资源类
class Share3{
//直接赋值给1标志位,AA线程看到1的标志位直接开始工作
private int flag = 1;
private Lock lock = new ReentrantLock();
//定义AA的钥匙1、BB的钥匙2、CC的钥匙3
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void AAprint() throws InterruptedException {
lock.lock();
try {
while (flag!=1){
//如果标志位没到AA线程专属的1时,就需要进行等待
condition1.await();
}
System.out.println("AA线程开始打印任务");
//干活,正常输出
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"线程输出了!");
}
//更改标志位,准备唤醒BB线程
flag = 2;
//干完活就需要唤醒下一个要干活的线程
condition2.signal();
}finally {
lock.unlock();
}
}
public void BBprint() throws InterruptedException{
lock.lock();
try {
while (flag!=2){
condition2.await();
}
System.out.println("线程BB开始打印任务");
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"线程输出了!");
}
//任务完成就得更改标志位准备唤醒下一个工作的线程
flag = 3;
//叫醒CC线程
condition3.signal();
}finally {
lock.unlock();
}
}
public void CCprint() throws InterruptedException{
lock.lock();
try {
while (flag!=3){
condition3.await();
}
System.out.println("线程CC开始打印任务");
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName()+"线程输出了!");
}
//通知唤醒AA线程
flag = 1;
condition1.signal();
}finally {
lock.unlock();
}
}
}
主启动类
public class ThreadDemo3 {
public static void main(String[] args) {
Share3 share3 = new Share3();
new Thread(()->{
try {
share3.AAprint();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AA").start();
new Thread(()->{
try {
share3.BBprint();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"BB").start();
new Thread(()->{
try {
share3.CCprint();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"CC").start();
}
}
启动测试,按照预先的想法进行输出
集合线程安全问题
ArrayList的线程不安全演示
首先看源码,以ArrayList对象的add方法为例**(并发修改异常)**
测试一下
多次运行就会报并发修改的异常
得出结论:ArrayList是线程不安全的
解决方案1:Vector
Vector的方法都是用Sychronized关键字修饰的,很古老,效率低。已经不常用
Vector 是矢量队列,它是 JDK1.0 版本添加的类。继承于 AbstractList,实现
了 List, RandomAccess, Cloneable 这些接口。 Vector 继承了 AbstractList,
实现了 List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功
能。 Vector 实现了 RandmoAccess 接口,即提供了随机访问功能。
RandmoAccess 是 java 中用来被 List 实现,为 List 提供快速访问功能的。在
Vector 中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访
问。 Vector 实现了 Cloneable 接口,即实现 clone()函数。它能被克隆。
==和 ArrayList 不同,Vector 中的操作是线程安全的。==
解决方案2:Collections接口
把new出来的List对象传入接口,也可以解决并发安全问题
以上方法都比较古老,下面的JUC中的方法才是重头戏
终极解决方案:CopyOnWriteArrayList(重点)
CopyOnWriteArrayList也叫写时复制技术 既兼顾了并发读,又照顾了独立写
他允许并发的读取,写的时候只能独立的写(一个人写)
== 当发生写操作时,会先复制一份和之前集合相同大小的内容。往里面写入新的内容,写完内容之后,新的集合内容会和老的集合内容做一个合并(覆盖掉老集合),此时再进行并发读取,读到的就是新集合的内容。 ==
HashSet异常演示
首先是HashSet
== 回顾一下HashSet,底层实际上就是k-v形式的HashMap。因为HashSet是无序的,且不能放重复元素,往HashSet放元素put的时候就直接放在HashMap的Key位置上,HashMap的Value就是空的。==
演示
多次运行后依然会存在并发修改的异常
查看一下源码,不难发现,没有Synchronized修饰,因此无法保证线程安全
解决方案:CopyOnWriteArraySet
改为CopyOnWriteArraySet进行处理
HashMap异常演示
和上面一样,多次运行出现了线程并发修改的问题
解决方案:ConcurrentHashMap
new出来的对象改用ConcurrentHashMap来创建HashMap对象就可以很好的解决HashMap的线程不安全问题
Sychronized锁的八种情况
有个大佬写的很好:Sychronized锁的八种情况演示
多线程锁
公平锁和非公平锁
公平锁和非公平锁都是在ReentrantLock下的
-
公平锁:先来后到(必须是先来的先执行)
-
非公平锁:非前来后到,可插队(根据CPU进行调度)
先看ReentrantLock的源码
可重入锁
可重入,意思可以再次进入
之前的Sychronized和Lock都是可重入锁。
二者的区别就是Sychronized是隐式可重入锁,Lock是显式可重入锁
举一个例子:面前有一个屋子,屋子有个大门,只要你有钥匙能进入大门,那么大门后面的各个房间里面就可以随便进入不需要再获得钥匙进入。这个就是可重入锁,也可以叫递归锁。内层是可以自由穿梭的
演示代码
打印输出
死锁
何为死锁
两个或者两个以上的进程,在执行过程中因为争夺资源而造成互相等待的现象,如果没有外力干涉,他们无法执行下去。
产生死锁的原因
手写死锁演示
public class DeadLockDemo {
static Object objectA = new Object();
static Object objectB = new Object();
public static void main(String[] args) {
/**
* A线程持有锁A,试图获取锁B
* A线程持有锁B,试图获取锁A
* 两个线程互相等待对方放锁造成死锁
*/
new Thread(()->{
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"线程A持有锁A,试图获取锁B");
try {//加个延迟放大问题
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectB){
System.out.println("线程A获取锁B");
}
}
},"A线程").start();
new Thread(()->{
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"线程B持有锁B,试图获取锁A");
try {//加个延迟放大问题
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectA){
System.out.println("线程B获取锁A");
}
}
},"B线程").start();
}
}
运行起来就会互相等待对方放锁
验证是否死锁的方法
- 1、jps 类似Linux中的ps -ef命令
- 2、jstack jvm自带的堆栈跟踪工具
通过两个工具对死锁进行跟踪
jps是jdk自带的一个小工具
先运行jps -l 查看死锁找到对应编号,再用jstack 编号的命令对这个死锁进行跟踪,并且验证
回车查看输出内容
Callable接口
目前我们学习了有两种创建线程的方法-一种是通过创建 Thread 类,另一种是实现 Runnable接口 创建线程。但是,Runnable 缺少的一项功能是,当线程终止时(即 run()完成时),我们无法使线程返回结果。为了支持此功能,
Java 中提供了 Callable 接口。
• 为了实现 Runnable,需要实现不返回任何内容的 run()方法,而对于Callable,需要实现在完成时返回结果的 call()方法。
• call()方法可以引发异常,而 run()则不能。
• 为实现 Callable 而必须重写 call 方法
• 不能直接替换 runnable,因为 Thread 类的构造方法根本没有 Callable
这个类就是FutureTask
Future接口
当 call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用 Future 对象。将 Future 视为保存结果的对象–它可能暂时不保存结果,但将来会保存(一旦Callable 返回)。Future 基本上是主线程可以跟踪进度以及其他线程的结果的一种方式。要实现此接口,必须重写 5 种方法,这里列出了重要的方法,如下:
• public boolean cancel(boolean mayInterrupt):用于停止任务。
如果尚未启动,它将停止任务。如果已启动,则仅在 mayInterrupt 为 true时才会中断任务。
• public Object get()抛出 InterruptedException,ExecutionException:用于获取任务的结果。
如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果。
• public boolean isDone():如果任务完成,则返回 true,否则返回 false
可以看到 Callable 和 Future 做两件事Callable 与 Runnable 类似,因为它封装了要在另一个线程上运行的任务,而 Future 用于存储从另一个线程获得的结果。实际上,future 也可以与 Runnable 一起使用。要创建线程,需要 Runnable。为了获得结果,需要 future。
FutureTask类
Java 库具有具体的 FutureTask 类型,该类型实现 Runnable接口 和 Future接口,并方便地将两种功能组合在一起。 可以通过为其构造函数提供 Callable 来创建FutureTask。然后,将 FutureTask 对象提供给 Thread 的构造函数以创建Thread 对象。因此,间接地使用 Callable 创建线程。
FutureTask实现了RunnableFuture接口,而RunnableFuture继承了Runnable和Future,也就是说FutureTask既是Runnable,也是Future。
FutureTask核心原理:(重点)
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给 Future 对象在后台完成
- 当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态
- 一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
- 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法
- 一旦计算完成,就不能再重新开始或取消计算
- get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常
- get 只计算一次,因此 get 方法放到最后
== Runnable接口有实现类FutureTask,FutureTask里又有构造方法可以传递Callable ==
构造方法传Callable对象或者Runnable对象
创建FutureTask对象
为什么叫未来任务
主线程遇到计算量大的任务时,这个时候我单开一个线程来做一个计算(提前计算提前准备),等我主线程忙完了,就回头来找这个单独的线程来拿计算结果。
开启子线程计算,存放计算完成后的结果,等需要的时候直接获取,而不需要每次都去计算(用未来任务只会计算一次,下一次再调用就不用计算了直接返回值了)
现实举例:
Callable接口创建线程
== callable通过适配的方式和runnable子类futuretask产生依赖关系,最后还得要thread类来start ==
//Lambda表达式
FutureTask futureTask2 = new FutureTask(() -> {
System.out.println(Thread.currentThread().getName()+"进入Callable接口");
return "FutureTask返回值";
});
//创建线程
new Thread(futureTask2, "callable接口").start();
System.out.println(futureTask2.get());
小结
• 在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给 Future 对象在后台完成, 当主线程将来需要时,就可以通过 Future对象获得后台作业的计算结果或者执行状态
• 一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果
• 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异
常。
JUC辅助类
JUC 中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过
多时 Lock 锁的频繁操作。这三种辅助类为:
减少计数CountDownLatch
CountDownLatch 类可以设置一个计数器,设置一个初始值,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句。
• CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞
• 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程不会阻塞)
• 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行
场景Demo: 6 个同学陆续离开教室后值班同学才可以关门。
普通版本不用CountDownLatch ,六个线程进行操作
CountDownLatch 版本,快速实现功能,创建一个CountDownLatch类
CountDownLatch countDownLatch = new CountDownLatch(希望的初始值);
循环栅栏CyclicBarrier
CyclicBarrier 看英文单词可以看出大概就是循环阻塞的意思,在使用中CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier 理解为加 1 操作
构造方法以及普通方法
场景Demo:集齐 7 颗龙珠就可以召唤神龙
是跟cyclicbarrier await线程数相关 只要await达到的线程数刚好符合 就唤醒所有线程并执行runnable方法
信号灯Semaphore
Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方法获得许可证,release 方法释放许可
场景Demo: 抢车位, 6 部汽车 3 个停车位
输出结果
对应的现实生活就是同学轮流上黑板写题,6个同学上黑板,但是黑板只能3个人写,剩下的3个人只能等待前面三个写完了才能上去写
JUC读写锁
概述
悲观锁和乐观锁
悲观锁:解决了并发编程问题,缺点就是得挨个排队很慢,并且得等当前线程处理完毕才能放锁
乐观锁:谁先修改谁就更新版本号,后面的线程再想更改就得核对版本号,一看版本号是被之前的线程修改过的,对不上了就得放弃更改,涉及到先后读写加锁更新版本号的问题。
表锁和行锁
表锁锁住整张表,不会出现死锁
行锁锁住当前行,会出现死锁
读写锁
读锁:共享锁
写锁:独占锁
读写锁都会出现死锁。
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,
就不应该允许其他线程对该资源进行读和写的操作了。针对这种场景,JAVA 的并发包提供了读写锁 ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁
- 线程进入读锁的前提条件:
• 没有其他线程的写锁
• 没有写请求, 或者有写请求,但调用线程和持有锁的线程是同一个(可重入锁)。 - 线程进入写锁的前提条件:
• 没有其他线程的读锁
• 没有其他线程的写锁
而读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
读写锁案例
//创建读写锁
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//加写锁
reentrantReadWriteLock.writeLock().lock();
//解写锁
reentrantReadWriteLock.writeLock().unlock();
//解读锁
reentrantReadWriteLock.readLock().unlock();
//加读锁
reentrantReadWriteLock.readLock().lock();
具体读写的案例
class Cache{
private volatile Map hashMap = new HashMap();
public void put(Object key,Object o) {
System.out.println("开始存储"+Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
hashMap.put(key, o);
System.out.println("存放完成"+Thread.currentThread().getName());
}
public Object get(Object o) {
System.out.println("开始拿取"+Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object result=hashMap.get(o);
System.out.println("拿取完成"+Thread.currentThread().getName());
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
//创建读写锁
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//创建资源类
Cache cache = new Cache();
//创建10个线程5个存5个取
//写
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(()->{
try {
//加写锁
reentrantReadWriteLock.writeLock().lock();
cache.put(num + "", num + "");
}finally {
//解写锁
reentrantReadWriteLock.writeLock().unlock();
}
},String.valueOf(i)).start();
}
//读
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(()->{
try {
//加读锁
reentrantReadWriteLock.readLock().lock();
System.out.println(cache.get(num + ""));
}finally {
//解读锁
reentrantReadWriteLock.readLock().unlock();
}
},String.valueOf(i)).start();
}
}
}
最后结果就是按顺序写读输出
读写锁的演变过程
读的时候不能写就是读锁不能升级成写锁的情况
而写的时候可以读就是写锁降级为读锁的体现
读写锁降级
将写入锁降级为读锁的权限
锁的升级就是越来越严格
锁的降级就是越来越宽松
注意,读锁不能升级为写锁。写锁降级是为了提高数据可见性
如果先有读锁再有写锁怎么办?
只能是先等读操作结束,读锁释放了,写锁才能拿到开始写入操作
图为写锁降级为读锁的过程
小结(重要)
• 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
• 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
原因: 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
BlockingQueue阻塞队列
阻塞队列概述
Concurrent 包中,BlockingQueue 很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。本文详细介绍了 BlockingQueue 家庭中的所有成员,包括他们各自的功能以及常见使用场景。
阻塞队列,顾名思义,首先它是一个队列, 通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;
- 当队列是空的,从队列中获取元素的操作将会被阻塞(空的取不出来)
- 当队列是满的,从队列中添加元素的操作将会被阻塞(满的放不进去)
试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
为什么需要 BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue 都给你一手包办了
在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
阻塞队列分类
ArrayBlockingQueue(常用)
基于数组的阻塞队列实现,在 ArrayBlockingQueue 内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue 内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
一句话总结: 由数组结构组成的有界阻塞队列。
LinkedBlockingQueue(常用)
基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue 可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
ArrayBlockingQueue 和 LinkedBlockingQueue 是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个类足以。
一句话总结: 由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
其他阻塞队列
看不懂,不多说了
DelayQueue、PriorityBlockingQueue、SynchronousQueue、 LinkedTransferQueue
阻塞队列核心方法
这里方法的类型就是报错的类型,打个比方,如果设置队列长度为3,这个时候用插入方法插入了第4个元素就会报错,不同的方法错误的类型也完全不同,如果是抛出异常:那么插入第四个元素就会抛异常出来。其他的都是如此依次类推,阻塞报错的时候就线程阻塞…
线程池Thread Pool
线程池概述
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
例子: 10 年前单核 CPU 电脑,假的多线程,像马戏团小丑玩多个球,CPU 需要来回切换。 现在是多核电脑,多个线程各自跑在独立的 CPU 上,不用切换效率高。
线程池的优势: 线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:
• 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
• 提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行。
• 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
• Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类
线程池实现架构
使用方法和实现原理
一池多线程
Executors.newFixedThreadPool(线程数量)
一次性建好线程放好,一池N线程
作用:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
特征:
• 线程池中的线程处于一定的量,可以很好的控制线程的并发量
• 线程可以重复被使用,在显示关闭之前,都将一直存在
• 超出一定量的线程被提交时候需在队列中等待
public static void main(String[] args) {
//一池5线程
ExecutorService threadPool=Executors.newFixedThreadPool(5);
//一共五个线程,一共来了十个请求
try {
for (int i = 1; i <=10 ; i++) {
//执行,execute里面是Runnable接口,直接lamdba表达式实现!
threadPool.execute(() -> {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭线程池
threadPool.shutdown();
}
}
类似银行柜台只有五个窗口,一次只能办五个人的业务,多的人只能等待其他人办完了才能继续
源码:
一池一线程
Executors.newSingleThreadExecutor()
一个任务一个任务执行,一池一线程
作用:创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的newFixedThreadPool 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。
特征: 线程池中最多执行 1 个线程,之后提交的线程活动将会排在队列中以此执行
//一池一线程
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
源码:
可扩容缓存线程池
Executors.newCachedThreadPool()
线程池根据需求创建线程,可扩容,遇强则强
作用:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.
特点:
• 线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE) • 线程池中的线程可进行缓存重复利用和回收(回收默认时间为 1 分钟)
• 当线程池中,没有可用线程,会重新创建一个线程
创建可扩容线程池
ExecutorService threadCachePool = Executors.newCachedThreadPool();
//一共五个线程,一共来了十个请求
try {
for (int i = 1; i <=10 ; i++) {
//执行,execute里面是Runnable接口,直接lamdba表达式实现!
threadCachePool.execute(() -> {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭线程池
threadCachePool.shutdown();
}
一共十个需要处理的线程,自动创建了多个线程处理。
但是注意,这个只是少量的情况,多了的情况不会有多少创建多少,例如20个线程只会创建13个左右个线程,这个是动态的,不固定!
源码:
工作中以上三种我们都不用,到时候都是自定义一个线程池。
线程池七个参数
具体含义
线程池工作流程以及拒绝策略
工作流程
- 1、在创建了线程池后,线程池中的线程数为零
- 2、当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
2.2 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。 - 3、当一个线程完成任务时,它会从队列中取下一个任务来执行
- 4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
4.1 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。
4.2 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
拒绝策略
- new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异 常
- new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
- new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
- new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会 抛出异常!
自定义线程池
public static void main(String[] args) {
//自定义线程池
ExecutorService threadPool= new ThreadPoolExecutor(
2,//线程池的核心线程数
5,//能容纳的最大线程数
2,//空闲线程存活时间
TimeUnit.SECONDS,//存活的时间单位
new ArrayBlockingQueue<>(3),//阻塞队列类型以及长度
Executors.defaultThreadFactory(),//创建线程的工厂类
new ThreadPoolExecutor.AbortPolicy()//拒绝策略类型
);
创建完之后
threadPool.各种线程操作方法就可以使用了
}
分支合并框架
概述
Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。Fork/Join 框架要完成两件事情:
-
Fork:把一个复杂任务进行分拆,大事化小
-
Join:把分拆任务的结果进行合并
- 任务分割:首先 Fork/Join 框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割
- 执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。
在 Java 的 Fork/Join 框架中,使用两个类完成上述操作
• ForkJoinTask:我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。该类提供了在任务中执行 fork 和 join 的机制。通常情况下我们不需要直接集成 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:
a.RecursiveAction:用于没有返回结果的任务
b.RecursiveTask:用于有返回结果的任务
• ForkJoinPool:ForkJoinTask 需要通过 ForkJoinPool 来执行
• RecursiveTask: 继承后可以实现递归(自己调自己)调用的任务
Fork方法
实现结构图
分支合并框架案例
1到1000的加和,拆分出来每份的范围是1~100,不满足条件就继续递归拆分
package com.cc.juc.forkJoin;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
class MyTask extends RecursiveTask<Integer>{
private int start;
private int end;
private int sum;
//构造方法,为了递归用
public MyTask(int start, int end){
this.start = start;
this.end = end;
}
@Override
protected Integer 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 个小任务
MyTask taskExample1 = new MyTask(start, middle);
MyTask taskExample2 = new MyTask(middle + 1, end);
//执行:异步
taskExample1.fork();
taskExample2.fork();
//同步阻塞获取执行结果
sum = taskExample1.join() + taskExample2.join();
}
//加完返回
return sum;
}
}
public class forkJoinDemo {
/**
* 生成一个计算任务,计算 1+2+3.........+1000
* 拆分运算,每份之间的差值不能超过100
*/
public static void main(String[] args) {
//定义任务
MyTask taskExample = new MyTask(1, 1000);
//定义执行对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
//加入任务执行
ForkJoinTask<Integer> result = forkJoinPool.submit(taskExample);
//输出结果
try {
System.out.println(result.get());
}catch (Exception e){
e.printStackTrace();
}finally {
forkJoinPool.shutdown();
}
}
}
输出结果
异步回调
Future 设计的初衷: 对将来的某个事件的结果进行建模
没太听懂,以后再补上吧~