01_本课程前提要求和说明
教学视频
https://www.bilibili.com/video/BV18b411M7xz
一些大厂的面试题
蚂蚁花呗一面:
Java容器有哪些?哪些是同步容器,哪些是并发容器?
ArrayList和LinkedList的插入和访问的时间复杂度?
java反射原理,注解原理?
新生代分为几个区?使用什么算法进行垃圾回收?为什么使用这个算法?
HashMap在什么情况下会扩容,或者有哪些操作会导致扩容?
HashMap push方法的执行过程?
HashMap检测到hash冲突后,将元素插入在链表的末尾还是开头?
1.8还采用了红黑树,讲讲红黑树的特性,为什么人家一定要用红黑树而不是AVL、B树之类的?
https和http区别,有没有用过其他安全传输手段?
线程池的工作原理,几个重要参数,然后给了具体几个参数分析线程池会怎么做,最后问阻塞队列的作用是什么?
linux怎么查看系统负载情况?
请详细描述springmvc处理请求全流程?spring 一个bean装配的过程?
讲一讲AtomicInteger,为什么要用CAS而不是synchronized?
美团一面:
最近做的比较熟悉的项目是哪个,画一下项目技术架构图。
JVM老年代和新生代的比例?
YGC和FGC发生的具体场景?
jstack,jmap,jutil分别的意义?如何线上排查JVM的相关问题?
线程池的构造类的方法的5个参数的具体意义?
单机上一个线程池正在处理服务如果忽然断电怎么办(正在处理和阻塞队列里的请求怎么处理)?
使用无界阻塞队列会出现什么问题?接口如何处理重复请求?
百度一面:
介绍一下集合框架?
hashmap hastable 底层实现什么区别?hashtable和concurrenthashtable呢?
hashmap和treemap什么区别?低层数据结构是什么?
线程池用过吗都有什么参数?底层如何实现的?
sychnized和Lock什么区别?sychnize 什么情况情况是对象锁?什么时候是全局锁为什么?
ThreadLocal 是什么底层如何实现?写一个例子呗?
volitile的工作原理?
cas知道吗如何实现的?
请用至少四种写法写一个单例模式?
请介绍一下JVM内存模型?用过什么垃圾回收器都说说呗线上发送频繁full gc如何处理?CPU使用率过高怎么办?如何定位问题?如何解决说一下解决思路和处理方法
知道字节码吗?字节码都有哪些?Integer x =5,int y =5,比较x =y 都经过哪些步骤?讲讲类加载机制呗都有哪些类加载器,这些类加载器都加载哪些文件?
手写一下类加载Demo
知道osgi吗?他是如何实现的?
请问你做过哪些JVM优化?使用什么方法达到什么效果?
classforName(“java.lang.String”)和String classgetClassLoader() LoadClass(“java.lang.String”)什么区别啊?
今日头条
HashMap如果一直put元素会怎么样? hashcode全都相同如何?
ApplicationContext的初始化过程?
GC 用什么收集器?收集的过程如何?哪些部分可以作为GC Root?
Volatile关键字,指令重排序有什么意义 ?synchronied,怎么用?
Redis数据结构有哪些?如何实现sorted set?
并发包里的原子类有哪些,怎么实现?
MvSql索引是什么数据结构? B tree有什么特点?优点是什么?
慢查询怎么优化?
项目: cache,各部分职责,有哪些优化点
京东金融面试
Dubbo超时重试;Dubbo超时时间设置
如何保障请求执行顺序
分布式事务与分布式锁(扣款不要出现负数)
分布式Session设置
执行某操作,前50次成功,第51次失败a全部回滚b前50次提交第51次抛异常,ab场景分别如何设计Spring (传播特性)
Zookeeper有却些作用
JVM内存模型
数据库垂直和水平拆分
MyBatis如何分页;如何设置缓存;MySQL分页
蚂蚁金服二面
自我介绍、工作经历、技术栈
项目中你学到了什么技术?(把三项目具体描述了很久)
微服务划分的粒度
微服务的高可用怎么保证的?
常用的负载均衡,该怎么用,你能说下吗?
网关能够为后端服务带来哪些好处?
Spring Bean的生命周期
HashSet是不是线程安全的?为什么不是线程安全的?
Java 中有哪些线程安全的Map?
Concurrenthashmap 是怎么做到线程安全的?
HashTable你了解过吗?
如何保证线程安全问题?
synchronized、lock
volatile 的原子性问题?为什么i++这种不支持原子性﹖从计算机原理的设计来讲下不能保证原子性的原因
happens before 原理
cas操作
lock和 synchronized 的区别?
公平锁和非公平锁
Java读写锁
读写锁设计主要解决什么问题?
- JUC(java.util.concurrent)
- 进程和线程
- 进程:后台运行的程序(我们打开的一个软件,就是进程)
- 线程:轻量级的进程,并且一个进程包含多个线程(同在一个软件内,同时运行窗口,就是线程)
- 进程和线程
- 并发和并行
- 并发:同时访问某个东西,就是并发
- 并行:一起做某些事情,就是并行
- JUC 下的三个包
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
02_volatile是什么
volatile是JVM提供的轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排(保证有序性)
03_JMM内存模型之可见性
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
三大特性:
可见性
原子性
有序性
可见性
通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。
这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
另一种
具体的 JMM 规定如下:
- 所有 共享变量 储存于 主内存 中;
- 每条线程拥有自己的工作内存,保存了被线程使用的变量的副本拷贝;
- 线程对变量的所有操作(读,写)都必须在自己的 工作内存 中完成,而不能直接读写 主内存 中的变量;
- 不同线程之间也不能直接访问对方工作内存中的变量,线程- 间变量值的传递需要通过主内存中转来完成
数据传输速率:硬盘 < 内存 < < cache
< CPU
两个概念:主内存 和 工作内存
-
主内存:就是计算机的内存,也就是经常提到的 8G 内存,16G 内存
-
工作内存:但我们实例化
new student
,那么age = 25
也是存储在主内存中- 当同时有三个线程同时访问
student
中的age
变量时,那么每个线程都会拷贝一份,到各自的工作内存,从而实现了变量的拷贝 。
即:JMM
内存模型的可见性,指的是当主内存区域中的值被某个线程写入更改后,其它线程会马上知晓更改后的值,并重新得到更改后的值。
- 当同时有三个线程同时访问
04_可见性的代码验证说明
可见性,如果不加 volatile
关键字,则主线程会进入死循环,加 volatile
则主线程能够退出,说明加了 volatile
关键字变量,当有一个线程修改了值,会马上被另一个线程感知到,当前值作废,从新从主内存中获取值。对其他线程可见,这就叫可见性。
import java.util.concurrent.TimeUnit;
/**
* 假设是主物理内存
*/
class MyData {
//volatile int number = 0;
int number = 0;
public void addTo60() {
this.number = 60;
}
}
/**
* 验证volatile的可见性
* 1. 假设int number = 0, number变量之前没有添加volatile关键字修饰
*/
public class VolatileDemo {
public static void main(String args []) {
// 资源类
MyData myData = new MyData();
// AAA线程 实现了Runnable接口的,lambda表达式
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
// 线程睡眠3秒,假设在进行运算
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改number的值
myData.addTo60();
// 输出修改后的值
System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
}, "AAA").start();
// main线程就一直在这里等待循环,直到number的值不等于零
while(myData.number == 0) {}
// 按道理这个值是不可能打印出来的,因为主线程运行的时候,number的值为0,所以一直在循环
// 如果能输出这句话,说明AAA线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
System.out.println(Thread.currentThread().getName() + "\t mission is over");
}
}
由于没有volatile
修饰MyData
类的成员变量number
,main
线程将会卡在while(myData.number == 0) {}
,不能正常结束。若想正确结束,用volatile
修饰MyData
类的成员变量number
吧。
volatile类比
没有volatile
修饰变量效果,相当于A同学拷贝了老师同一课件,A同学对课件进一步的总结归纳,形成自己的课件,这就与老师的课件不同了。
有volatile
修饰变量效果,相当于A同学拷贝了老师同一课件,A同学对课件进一步的总结归纳,形成自己的课件,并且与老师分享,老师认可A同学修改后的课件,并用它来作下一届的课件。
详细过程就是:
- 线程 a 从主内存读取 共享变量 到对应的工作内存
- 对共享变量进行更改
- 线程 b 读取共享变量的值到对应的工作内存
- 线程 a 将修改后的值刷新到主内存,失效其他线程对 共享变量的副本
- 线程 b 对共享变量进行操作时,发现已经失效,重新从主内存读取最新值,放入到对应工作内存。
05_volatile不保证原子性
原子性指的是什么意思?
不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整要么同时成功,要么同时失败。
volatile
不保证原子性案例演示:
class MyData2 {
/**
* volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
*/
volatile int number = 0;
public void addPlusPlus() {
number ++;
}
}
public class VolatileAtomicityDemo {
public static void main(String[] args) {
MyData2 myData = new MyData2();
// 创建10个线程,线程里面进行1000次循环
for (int i = 0; i < 20; i++) {
new Thread(() -> {
// 里面
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
}
}, String.valueOf(i)).start();
}
// 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
// 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
while(Thread.activeCount() > 2) {
// yield表示不执行
Thread.yield();
}
// 查看最终的值
// 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
}
}
最后的结果总是小于20000。
详细过程是:
-
假设现在共享变量的值是 100 ,线程 A 需要对变量进行自增 1,首先它从主内存中读取变量值,由于 CPU 切换关系,此时切换到 B线程;
-
B 线程也从主内存中读取变量值,此时读取到的变量值还是 100,然后在自己的工作内存中进行了 + 1 操作,但是还未刷新回主内存;
-
此时,CPU 又切换到了 A线程,由于 B 线程还未将工作内存中的值刷新回主内存,因此 A 线程中的值还是 100,A 线程对工作内存中的变量进行 + 1 操作;
-
线程 B 刷新 新的值 101 到主内存 ;
-
线程 A 刷新 新的值 101 到主内存;
结果就是:两次 +1 操作,却只进行了 1 次修改
06_volatile不保证原子性理论解释
number++在多线程下是非线程安全的。
我们可以将代码编译成字节码,可看出number++被编译成3条指令。
假设我们没有加 synchronized
那么第一步就可能存在着,三个线程同时通过getfield
命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd
命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile
的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd
命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于20000
。
07_volatile不保证原子性问题解决
可加synchronized
解决,但它是重量级同步机制,性能上有所顾虑。
如何不加synchronized
解决number++
在多线程下是非线程安全的问题?使用AtomicInteger
。
import java.util.concurrent.atomic.AtomicInteger;
class MyData2 {
/**
* volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
*/
volatile int number = 0;
AtomicInteger number2 = new AtomicInteger();
public void addPlusPlus() {
number ++;
}
public void addPlusPlus2() {
number2.getAndIncrement();
}
}
public class VolatileAtomicityDemo {
public static void main(String[] args) {
MyData2 myData = new MyData2();
// 创建10个线程,线程里面进行1000次循环
for (int i = 0; i < 20; i++) {
new Thread(() -> {
// 里面
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
myData.addPlusPlus2();
}
}, String.valueOf(i)).start();
}
// 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
// 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
while(Thread.activeCount() > 2) {
// yield表示不执行
Thread.yield();
}
// 查看最终的值
// 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
System.out.println(Thread.currentThread().getName() + "\t finally number2 value: " + myData.number2);
}
}
输出结果为:
main finally number value: 18766
main finally number2 value: 20000
08_volatile指令重排案例1
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
重排案例
public void mySort{
int x = 11;//语句1
int y = 12;//语句2
× = × + 5;//语句3
y = x * x;//语句4
}
可重排序列:
- 1234
- 2134
- 1324
问题:请问语句4可以重排后变成第一个条吗?答:不能。
重排案例2
int a,b,x,y = 0
线程1 | 线程2 |
---|---|
x = a; | y = b; |
x = 0; y = 0 |
如果编译器对这段程序代码执行重排优化后,可能出现下列情况:
线程1 | 线程2 |
---|---|
b = 1; | a = 2; |
x = a; | y = b; |
x = 2; y = 1 |
这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。
09_volatile指令重排案例2
观察以下程序:
public class ReSortSeqDemo{
int a = 0;
boolean flag = false;
public void method01(){
a = 1;//语句1
flag = true;//语句2
}
public void method02(){
if(flag){
a = a + 5; //语句3
}
System.out.println("retValue: " + a);//可能是6或1或5或0
}
}
多线程环境中线程交替执行method01()和method02(),由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
禁止指令重排小总结
volatile
实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
先了解一个概念,内存屏障(Memory Barrier
)又称内存栅栏,是一个CPU指令,它的作用有两个:
- 保证特定操作的执行顺序,
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier
则会告诉编译器和CPU
,不管什么指令都不能和这条Memory Barrier
指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU
的缓存数据,因此任何CPU
上的线程都能读取到这些数据的最新版本。
对volatile
变量进行写操作时,会在写操作后加入一条store
屏障指令,将工作内存中的共享变量值刷新回到主内存。
对Volatile
变量进行读操作时,会在读操作前加入一条load
屏障指令,从主内存中读取共享变量。
线性安全性获得保证
-
工作内存与主内存同步延迟现象导致的可见性问题 - 可以使用
synchronized
或volatile
关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。 -
对于指令重排导致的可见性问题和有序性问题 - 可以利用
volatile
关键字解决,因为volatile
的另外一个作用就是禁止重排序优化。
你在哪些地方用到过 volatile?单例
10_单例模式在多线程环境下可能存在安全问题
懒汉单例模式
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
// 这里的 == 是比较内存地址
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
}
}
输出结果:
main 我是构造方法singletonDemo
true
true
true
true
但是,在多线程环境运行上述代码,能保证单例吗?
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
输出结果:
4 我是构造方法SingletonDemo
2 我是构造方法SingletonDemo
5 我是构造方法SingletonDemo
6 我是构造方法SingletonDemo
0 我是构造方法SingletonDemo
3 我是构造方法SingletonDemo
1 我是构造方法SingletonDemo
显然不能保证单例。
解决方法之一:用synchronized
修饰方法getInstance()
,但它属重量级同步机制,使用时慎重。
public synchronized static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
11_单例模式volatile分析
解决方法之二:DCL
(Double Check Lock
双端检锁机制)
public class SingletonDemo{
private SingletonDemo(){}
private volatile static SingletonDemo instance = null;
public static SingletonDemo getInstance() {
if(instance == null) {
synchronized(SingletonDemo.class){
if(instance == null){
instance = new SingletonDemo();
}
}
}
return instance;
}
}
DCL中volatile解析
DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile
可以禁止指令重排
原因在于某一个线程执行到第一次检测,读取到的instance
不为null
时,instance
的引用对象可能没有完成初始化。instance = new SingletonDemo();
可以分为以下3步完成(伪代码):
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,
此时instance != null
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); //1.分配对象内存空间
instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! =null,但是对象还没有初始化完成!
instance(memory);//2.初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问instance
不为null
时,由于instance
实例未必已初始化完成,也就造成了线程安全问题。
另一种
public class Singleton6 {
//2.提供静态变量保存实例对象
private volatile static Singleton6 INSTANCE;
//1.私有化构造器
private Singleton6(){}
//3.提供获取对象的方法
public static Singleton6 getInstance(){
//第一重检查:针对很多个线程同时想要创建对象的情况
if(INSTANCE == null){
//同步代码块锁定
synchronized (Singleton6.class){
//第二重锁检查(针对比如A,B两个线程都为null,第一个线程创建完对象,第二个等待锁的线程拿到锁的情况)
if(INSTANCE == null){
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
请你说说为什么要在这里加上 volatile 呢?
因为创建对象分为 3 步:
- 分配内存空间;
- 初始化对象
- 设置实例执行刚分配的内存地址【正常流程走:
instance ! = null
】
但是,由于这 3 步不存在数据依赖关系 ,所以可能进行重排序优化,造成下列现象:
- 分配内存空间
- 设置实例执行刚分配的内存地址【
instance ! = null
有名无实,初始化并未完成!】 - 初始化对象
所有当另一条线程访问 instance
时 不为null
,但是 instance
实例化未必已经完成,也就造成线程安全问题!
12_CAS是什么
Compare And Set
CAS
的全称是 Compare-And-Swap
,它是 CPU
并发原语
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的
CAS
并发原语体现在 Java
语言中就是 sun.misc.Unsafe
类的各个方法。调用UnSafe
类中的 CAS
方法,JVM
会帮我们实现出 CAS
汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作,再次强调,CAS 是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS
是一条 CPU
的原子指令,不会造成所谓的数据不一致的问题,也就是说 CAS
是线程安全的 。
示例程序
public class CASDemo{
public static void main(String[] args){
AtomicInteger atomicInteger = new AtomicInteger(5);// mian do thing. . . . ..
/**
* 一个是期望值,一个是更新值,但期望值和原来的值相同时,才能够更改
* 假设三秒前,我拿的是5,也就是expect为5,然后我需要更新成 2019
*/
System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data: "+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t current data: "+atomicInteger.get());
}
}
输出结果为
true 2019
false 2019
首先调用 AtomicInteger
创建了一个实例, 并初始化为 5
AtomicInteger atomicInteger = new AtomicInteger(5);
然后调用 CAS
方法,企图更新成 2019,这里有两个参数,一个是 5,表示期望值,第二个就是我们要更新的值
atomicInteger.compareAndSet(5, 2019)
然后再次使用了一个方法,同样将值改成 1024
atomicInteger.compareAndSet(5, 1024)
原因: 这是因为我们执行第一个的时候,期望值和原本值是满足的,因此修改成功,但是第二次后,主内存的值已经修改成了 2019,不满足期望值,因此返回了 false,本次写入失败!!!
13_CAS底层原理-上
Cas
底层原理?如果知道,谈谈你对UnSafe
的理解
public void addPlusPlus2() {
number2.getAndIncrement();
}
2--->
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
3--->
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
atomiclnteger.getAndIncrement();
源码
从这里能够看到,底层又调用了一个 unsafe
类的 getAndAddInt
方法
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicInteger with initial value {@code 0}.
*/
public AtomicInteger() {
}
...
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
...
}
UnSafe
- Unsafe
是CAS
的核心类,由于Java
方法无法直接访问底层系统,需要通过本地(native
)方法来访问,Unsafe
相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java
中CAS
操作的执行依赖于Unsafe
类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
- 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
- 变量
value
用volatile
修饰,保证了多线程之间的内存可见性。
CAS是什么
CAS
的全称为Compare-And-Swap
,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS
并发原语体现在JAVA
语言中就是sun.misc.Unsafe
类中的各个方法。调用UnSafe
类中的CAS
方法,JVM
会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。(原子性)
var5
:就是我们从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到自己的本地内存,然后执行 compareAndSwapInt()
在再和主内存的值进行比较。因为线程不可以直接越过高速缓存,直接操作主内存,所以执行上述方法需要比较一次,在执行加1操作)
那么操作的时候,需要比较工作内存中的值,和主内存中的值进行比较
假设执行 compareAndSwapInt
返回 false
,那么就一直执行 while
方法,直到期望的值和真实值一样
14_CAS底层原理-下
继续上一节
UnSafe.getAndAddInt()
源码解释:
val1
:AtomicInteger
对象本身var2
:该对象值得引用地址var4
:需要变动的数量var5
:用var1
和var2
找到的内存中的真实值- 用该对象当前的值与
var5
比较 - 如果相同,更新
var5 + var4
并返回true
- 如果不同,继续取值然后再比较,直到更新完成
- 用该对象当前的值与
这里没有用 synchronized
,而用 CAS
,这样提高了并发性,也能够实现一致性,是因为每个线程进来后,进入的 do while
循环,然后不断的获取内存中的值,判断是否为最新,然后在进行更新操作 。
假设线程 A 和线程 B 同时执行 getAndInt
操作(分别跑在不同的 CPU
上)
AtomicInteger
里面的value
原始值为 3,即主内存中AtomicInteger
的value
为 3,根据JMM
模型,线程 A 和线程 B 各自持有一份价值为 3 的副本,分别存储在各自的工作内存- 线程 A 通过
getIntVolatile(var1 , var2)
拿到value
值3,这时线程 A 被挂起(该线程失去 CPU 执行权) - 线程 B 也通过
getIntVolatile(var1, var2)
方法获取到value
值也是3,此时刚好线程 B 没有被挂起,并执行了compareAndSwapInt
方法,比较内存的值也是 3,成功修改内存值为 4,线程B打完收工,一切OK - 这是线程 A 恢复,执行
CAS
方法,比较发现自己手里的数字 3 和主内存中的数字 4 不一致,说明该值已经被其它线程抢先一步修改过了,那么 A 线程本次修改失败,只能够重新读取后在来一遍了,也就是在执行do while
- 线程 A 重新获取
value
值,因为变量value
被volatile
修饰,所以其它线程对它的修改,线程 A 总能够看到,线程 A 继续执行compareAndSwapInt
进行比较替换,直到成功。
Unsafe 类 + CAS 思想: 也就是自旋,自我旋转!!!
补充: 上面说到的 Unsafe
类中的 compareAndSwapInt
是一个本地方法,该方法的实现位于unsafe.cpp
中
底层汇编
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)
UnsafeWrapper("Unsafe_CompareAndSwaplnt");
oop p = JNlHandles::resolve(obj);
jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e))== e;
UNSAFE_END
//先想办法拿到变量value在内存中的地址。
//通过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值。
小结
CAS
- 先想办法拿到变量
value
在内存中的地址 - 通过
Atomic::cmpxchg
实现比较替换,其中参数 X 是即将更新的值,参数 e 是原内存的值
CAS应用
CAS
有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
15_CAS缺点
循环时间长开销很大
// ursafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4){
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
}while(!this.compareAndSwapInt(varl, var2, var5,var5 + var4));
return var5;
}
我们可以看到getAndAddInt
方法执行时,有个do while
,如果CAS
失败,会一直进行尝试。如果CAS
长时间一直不成功,可能会给CPU
带来很大的开销。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
引出来ABA问题
CAS
的核心思想是通过比较内存值和预期值是否一样而判断内存值是否被更改过,但此判断逻辑不严谨,假如内存值为 A,后来一条线程修改为 B,最后又被另一个线程改成了 A,则 CAS
认为内存值并没有发生过改变,但实际情况是有被其他线程修改,这种情况对依赖过程值的情景的运算结果影响很大。
解决办法:引入版本号,每次变量更新都把版本号【时间戳】加一。
16_ABA问题
原子类 AtomicInteger
的ABA问题
连环套路
从 AtomicInteger
引出下面的问题:
CAS -> Unsafe -> CAS 底层思想 -> ABA -> 原子引用更新 -> 如何规避 ABA 问题
ABA问题怎么产生的
ABA 问题是什么
假设现在有两个线程,分别是 T1 和 T2,然后 T1 执行某个操作的时间为10 秒,T2 执行某个时间的操作是 2 秒,最开始 AB 两个线程,分别从主内存中获取 A 值,但是因为 B 的执行速度更快,他先把A的值改成B,然后在修改成A,然后执行完毕,T1线程在10秒后,执行完毕,判断内存中的值为A,并且和自己预期的值一样,它就认为没有人更改了主内存中的值,就快乐的修改成B,但是实际上 可能中间经历了 ABCDEFA 这个变换,也就是中间的值经历了狸猫换太子。
所以 ABA 问题就是,在进行获取主内存值的时候,该内存值在我们写入主内存的时候,已经被修改了 N 次,但是最终又改成原来的值了 。
CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
17_AtomicReference原子引用
import java.util.concurrent.atomic.AtomicReference;
class User{
String userName;
int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return String.format("User [userName=%s, age=%s]", userName, age);
}
}
public class AtomicReferenceDemo {
public static void main(String[] args){
User z3 = new User( "z3",22);
User li4 = new User("li4" ,25);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
}
}
输出结果
true User [userName=li4, age=25]
false User [userName=li4, age=25]
另一种
原子引用其实和原子包装类是差不多的概念,就是将一个 java 类,用原子引用类进行包装起来,那么这个类就具备了原子性 。
class User {
String userName;
int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User z3 = new User("z3", 22);
User l4 = new User("l4", 25);
// 创建原子引用包装类
AtomicReference<User> atomicReference = new AtomicReference<>();
// 现在主物理内存的共享变量,为z3
atomicReference.set(z3);
// 比较并交换,如果现在主物理内存的值为z3,那么交换成l4
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t " + atomicReference.get().toString());
// 比较并交换,现在主物理内存的值是l4了,但是预期为z3,因此交换失败
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t " + atomicReference.get().toString());
}
}
18_AtomicStampedReference版本号原子引用
原子引用 + 新增一种机制,那就是修改版本号(类似时间戳),它用来解决ABA问题。
思路如下:
新增一种机制,也就是修改版本号,类似于[ 时间戳 ]的概念
T1: 100 1 2019 2
T2: 100 1 101 2 100 3
如果 T1 修改的时候,版本号为 2,落后于现在的版本号 3,所以要重新获取最新值,这里就提出了一个使用时间戳版本号,来解决 ABA 问题的思路 。
19_ABA问题的解决
ABA问题程序演示及解决方法演示:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
/**
* 普通的原子引用包装类
*/
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
// 传递两个值,一个是初始值,一个是初始版本号
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("============以下是ABA问题的产生==========");
new Thread(() -> {
// 把100 改成 101 然后在改成100,也就是ABA
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
try {
// 睡眠一秒,保证t1线程,完成了ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把100 改成 101 然后在改成100,也就是ABA
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "t2").start();
/
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
/
System.out.println("============以下是ABA问题的解决==========");
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
// 暂停t3一秒钟
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 传入4个值,期望值,更新值,期望版本号,更新版本号
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp());
}, "t3").start();
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
// 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 当前最新实际版本号:"
+ atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值" + atomicStampedReference.getReference());
}, "t4").start();
}
}
输出结果
============以下是ABA问题的产生==========
true 2019
============以下是ABA问题的解决==========
t3 第一次版本号1
t4 第一次版本号1
t3 第二次版本号2
t3 第三次版本号3
t4 修改成功否:false 当前最新实际版本号:3
t4 当前实际最新值100
基于原子引用的 ABA 问题
我们首先创建了两个线程,然后 T1 线程,执行一次 ABA 的操作,T2 线程在一秒后修改主内存的值
public class ABADemo {
/**
* 普通的原子引用包装类
*/
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
new Thread(() -> {
// 把100 改成 101 然后在改成100,也就是ABA
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
try {
// 睡眠一秒,保证t1线程,完成了ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把100 改成 101 然后在改成100,也就是ABA
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "t2").start();
}
}
我们发现,它能够成功的修改,这就是 ABA 问题
20_集合类不安全之并发修改异常
单线程环境下
单线程环境的 ArrayList
是不会有问题的
public class ArrayListNotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
for(String element : list) {
System.out.println(element);
}
}
}
多线程环境
为什么 ArrayList
是线程不安全的 ?因为在进行写操作的时候,方法上为了保证并发性,是没有添加 synchronized
修饰,所以并发写的时候,就会出现问题 。
ArrayList 不安全的案列
当我们同时启动 100个线程去操作 List 的时候
public class Demo5 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for(int i = 0;i < 100;i++){
new Thread(() ->{
//往集合中添加元素【add 方法并未 synchronized 修饰】
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
这个时候出现了错误,也就是java.util.ConcurrentModificationException
[ 并发修改的异常 ]
上述程序会抛java.util.ConcurrentModificationException
解决方法之一:Vector
解决方法之二:Collections.synchronizedList()
21_集合类不安全之写时复制
导致原因
并发修改导致:一个人正在写入,另一个人过来抢夺,导致数据不一致异常 !
解决方案
方案一:Vector
第一种方法,就是不用 ArrayList
这种不安全的 List
实现类,而采用 Vector
,线程安全的
关于 Vector
如何实现线程安全的,而是在方法上加了锁,即synchronized
这样就每次只能够一个线程进行操作,所以不会出现线程不安全的问题,但是因为加锁了,导致并发性下降 。
方案二:Collections.synchronizedList()
List<String> list = Collections.synchronizedList(new ArrayList<>());
采用 Collections
集合工具类,在 ArrayList
外面包装一层 同步 机制 。
方案三:使用 JUC 工具类中的 CopyOnWriteArrayList 类
CopyOnWriteArrayList
:写时复制,主要是一种读写分离的思想
CopyOnWrite
容器即写时复制的容器。往一个容器添加元素的时候,不会直接往当前容器添加,而是现将当前容器 Object [ ]
进行 Copy
,复制出一个新的容器 Object [ ]
,然后再往新的容器中添加元素,添加完元素后,再将原容器的引用指向新的容器;这样做的好处是可以对 CopyOnWrite
容器进行并发读,而不需要加锁,因为当前容器并不需要添加元素。所以 CopyOnWrite
容器也是一种读写分离的思想!
就是写的时候,把 ArrayList
扩容一个出来,然后把值填写上去,在通知其他的线程,ArrayList
的引用指向扩容后的
查看底层 add 方法源码
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
首先需要加锁
final ReentrantLock lock = this.lock;
lock.lock();
然后在末尾扩容一个单位
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
然后在把扩容后的空间,填写上需要 add 的内容
newElements[len] = e;
最后把内容 set
到 Array
中
22_集合类不安全之Set
HashSet
也是非线性安全的。(HashSet
内部是包装了一个HashMap
的)
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
public class SetNotSafeDemo {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
//Set<String> set = Collections.synchronizedSet(new HashSet<>());
//Set<String> set = new CopyOnWriteArraySet<String>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(set);
}, String.valueOf(i)).start();
}
}
}
解决方法:
Collections.synchronizedSet(new HashSet<>())
CopyOnWriteArraySet<>()
(推荐)
使用 JUC 工具类中的 CopyOnWriteArraySet 类
底层还是使用 CopyOnWriteArrayList
进行实例化
public class CopyOnWriteArraySet<E> extends AbstractSet<E>
implements java.io.Serializable {
private static final long serialVersionUID = 5457747651344034263L;
private final CopyOnWriteArrayList<E> al;
/**
* Creates an empty set.
*/
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
public CopyOnWriteArraySet(Collection<? extends E> c) {
if (c.getClass() == CopyOnWriteArraySet.class) {
@SuppressWarnings("unchecked") CopyOnWriteArraySet<E> cc =
(CopyOnWriteArraySet<E>)c;
al = new CopyOnWriteArrayList<E>(cc.al);
}
else {
al = new CopyOnWriteArrayList<E>();
al.addAllAbsent(c);
}
}
//可看出CopyOnWriteArraySet包装了一个CopyOnWriteArrayList
...
public boolean add(E e) {
return al.addIfAbsent(e);
}
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
//暴力查找
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {//还要检查多一次元素存在性,生怕别的线程已经插入了
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
...
}
补充:
同理 HashSet
的底层结构就是 HashMap
思考: 但是为什么我调用 HashSet.add()
的方法,只需要传递一个元素,而 HashMap
是需要传递 key-value
键值对 ?
首先我们查看 hashSet
的 add
方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
我们能发现但我们调用 add
的时候,存储一个值进入map
中,只是作为key
进行存储,而 value
存储的是一个Object
类型的常量,也就是说 HashSet
只关心key
,而不关心 value
。
23_集合类不安全之Map
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class MapNotSafeDemo {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
// Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
// Map<String, String> map = new ConcurrentHashMap<>();
// Map<String, String> map = new Hashtable<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
}
解决方法:
HashTable
与Vector
类似,属于HashMap
线程安全的实现类,里面方面同样加了synchronized
修饰,效率较低 。Collections.synchronizedMap(new HashMap<>())
ConcurrencyMap<>()
(推荐)
Map<String, String> map = new ConcurrentHashMap<>();
24_TransferValue醒脑小练习
Java的参数传递是值传递,不是引用传递。
下面程序体验下上一句的含义:
class Person {
private Integer id;
private String personName;
public Person(String personName) {
this.personName = personName;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getPersonName() {
return personName;
}
public void setPersonName(String personName) {
this.personName = personName;
}
}
public class TransferValueDemo {
public void changeValue1(int age) {
age = 30;
}
public void changeValue2(Person person) {
person.setPersonName("XXXX");
}
public void changeValue3(String str) {
str = "XXX";
}
public static void main(String[] args) {
TransferValueDemo test = new TransferValueDemo();
// 定义基本数据类型
int age = 20;
test.changeValue1(age);
System.out.println("age ----" + age);
// 实例化person类
Person person = new Person("abc");
test.changeValue2(person);
System.out.println("personName-----" + person.getPersonName());
// String
String str = "abc";
test.changeValue3(str);
System.out.println("string-----" + str);
}
}
输出结果:
age ----20
personName-----XXXX
string-----abc
changeValue1 的执行过程
八种基本数据类型,在栈里面分配内存,属于值传递
栈管运行,堆管存储
当们执行 changeValue1 的时候,因为 int 是基本数据类型,所以传递的是 int = 20 这个值,相当于传递的是一个副本,main 方法里面的 age 并没有改变,因此输出的结果 age 还是20,属于值传递 。
changeValue2 的执行过程
因为 Person 是属于对象,传递的是内存地址,当执行changeValue2的时候,会改变内存中的 Person 的值,属于引用传递,两个指针都是指向同一个地址 。
changeValue3 的执行过程
String 不属于基本数据类型,但是为什么执行完成后,还是 abc 呢?
这是因为 String 的特殊性,当我们执行 String str = “abc” 的时候,它会把 abc 放入常量池中
当我们执行 changeValue3
的时候,会重新新建一个 xxx
,并没有销毁 abc
,然后指向xxx,然后最后我们输出的是 main
中的引用,还是指向的 abc
,因此最后输出结果还是abc
。
25_java锁之公平和非公平锁
是什么
-
公平锁―是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
-
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后中请的线程比先中请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象
并发包中ReentrantLock
的创建可以指定构造函数的boolean
类型来得到公平锁或非公平锁,默认是非公平锁。
The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order. Programs using fair locks accessed by many threads may display lower overall throughput (i.e., are slower; often much slower) than those using the default setting, but have smaller variances in times to obtain locks and guarantee lack of starvation.
Note however, that fairness of locks does not guarantee fairness of thread scheduling. Thus, one of many threads using a fair lock may obtain it multiple times in succession while other active threads are not progressing and not currently holding the lock. Also note that the untimed tryLock() method does not honor the fairness setting. It will succeed if the lock is available even if other threads are waiting.
此类的构造函数接受可选的公平性参数。当设置为true时,在争用下,锁有利于向等待时间最长的线程授予访问权限。否则,此锁不保证任何特定的访问顺序。与使用默认设置的程序相比,使用由许多线程访问的公平锁的程序可能显示出较低的总体吞吐量(即,较慢;通常要慢得多),但是在获得锁和保证没有饥饿的时间上差异较小。
但是请注意,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的多个线程中的一个线程可以连续多次获得公平锁,而其他活动线程则没有进行并且当前没有持有该锁。还要注意,不计时的 tryLock()方法不支持公平性设置。如果锁可用,即使其他线程正在等待,它也会成功。
reentrant
英 [riːˈɛntrənt] 美 [ˌriˈɛntrənt]
a. 可重入;可重入的;重入;可再入的;重进入
如何创建
synchronized
只能是非公平锁 。
并发包中 ReentrantLock
的创建可以指定析构函数的 boolean
类型来得到公平锁或者非公平锁,默认是非公平锁
/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(true);
两者区别
- 公平锁
Threads acquire a fair lock in the order in which they requested it.
公平锁就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。 - 非公平锁
a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lockhappens to be available when it is requested.
非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
题外话
Java ReentrantLock
而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。
非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized
而言,也是一种非公平锁
26_java锁之可重入锁和递归锁理论知识
可重入锁(也叫做递归锁)
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。
⽐如⼀个线程获得了某个对象的锁 [ 第一层方法 ],此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的 [ 第二层方法 ]
ReentrantLock
/synchronized
就是一个典型的可重入锁。
可重入锁最大的作用是避免死锁。
实例
可重入锁就是,在一个 method1
方法中加入一把锁,方法 2 也加锁了,那么他们拥有的是同一把锁
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
也就是说我们只需要进入 method1 后,那么它也能直接进入method2 方法,因为他们所拥有的锁,是同一把 。
作用
可重入锁的最大作用就是避免死锁
27_java锁之可重入锁和递归锁代码验证
证明 Synchronized
/**
* 资源类
*/
class Phone {
/**
* 发送短信
* @throws Exception
*/
public synchronized void sendSMS() throws Exception{
System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");
// 在同步方法中,调用另外一个同步方法
sendEmail();
}
/**
* 发邮件
* @throws Exception
*/
public synchronized void sendEmail() throws Exception{
System.out.println(Thread.currentThread().getId() + "\t invoked sendEmail()");
}
}
public class ReenterLockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
// 两个线程操作资源列
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "t2").start();
}
}
在这里,我们编写了一个资源类 phone
,拥有两个加了 synchronized
的同步方法,分别是 sendSMS
和 sendEmail
,我们在 sendSMS 方法中,调用 sendEmail。最后在主线程同时开启了两个线程进行测试,最后得到的结果为:
t1 invoked sendSMS()
t1 invoked sendEmail()
t2 invoked sendSMS()
t2 invoked sendEmail()
这就说明当 t1
线程进入sendSMS
的时候,拥有了一把锁,同时 t2
线程无法进入,直到 t1
线程拿着锁,执行了sendEmail
方法后,才释放锁,这样 t2
才能够进入
t1 invoked sendSMS() t1线程在外层方法获取锁的时候
t1 invoked sendEmail() t1在进入内层方法会自动获取锁
t2 invoked sendSMS() t2线程在外层方法获取锁的时候
t2 invoked sendEmail() t2在进入内层方法会自动获取锁
证明ReentrantLock
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 资源类
*/
class Phone implements Runnable{
Lock lock = new ReentrantLock();
/**
* set 进去的时候,就加锁,调用set方法的时候,能否访问另外一个加锁的set方法
*/
public void getLock() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t get Lock");
setLock();
} finally {
lock.unlock();
}
}
public void setLock() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t set Lock");
} finally {
lock.unlock();
}
}
@Override
public void run() {
getLock();
}
}
public class ReenterLockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
/**
* 因为Phone实现了Runnable接口
*/
Thread t3 = new Thread(phone, "t3");
Thread t4 = new Thread(phone, "t4");
t3.start();
t4.start();
}
}
现在我们使用 ReentrantLock
进行验证,首先资源类实现了 Runnable
接口,重写 Run
方法,里面调用 get
方法,get
方法在进入的时候,就加了锁
public void getLock() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t get Lock");
setLock();
} finally {
lock.unlock();
}
}
然后在方法里面,又调用另外一个加了锁的 setLock
方法
public void setLock() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t set Lock");
} finally {
lock.unlock();
}
}
最后输出结果我们能发现,结果和加 synchronized
方法是一致的,都是在外层的方法获取锁之后,线程能够直接进入里层
t3 get Lock
t3 set Lock
t4 get Lock
t4 set Lock
注意 1: 当我们在 getLock 方法加两把锁会是什么情况呢?(阿里面试)
public void getLock() {
lock.lock();
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t get Lock");
setLock();
} finally {
lock.unlock();
lock.unlock();
}
}
最后得到的结果也是一样的,因为里面不管有几把锁,其它他们都是同一把锁,也就是说用同一个钥匙都能够打开 。
注意 2: 当我们在 getLock 方法加两把锁,但是只解一把锁会出现什么情况呢 ?
public void getLock() {
lock.lock();
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t get Lock");
setLock();
} finally {
lock.unlock();
lock.unlock();
}
}
得到结果
t3 get Lock
t3 set Lock
也就是说程序直接卡死,线程不能出来,也就说明我们申请几把锁,最后需要解除几把锁
注意 3: 当我们只加一把锁,但是用两把锁来解锁的时候,又会出现什么情况呢 ?
public void getLock() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t get Lock");
setLock();
} finally {
lock.unlock();
lock.unlock();
}
}
这个时候,运行程序会直接报错
t3 get Lock
t3 set Lock
t4 get Lock
t4 set Lock
Exception in thread "t3" Exception in thread "t4" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
at com.moxi.interview.study.thread.Phone.getLock(ReenterLockDemo.java:52)
at com.moxi.interview.study.thread.Phone.run(ReenterLockDemo.java:67)
at java.lang.Thread.run(Thread.java:745)
java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
at com.moxi.interview.study.thread.Phone.getLock(ReenterLockDemo.java:52)
at com.moxi.interview.study.thread.Phone.run(ReenterLockDemo.java:67)
at java.lang.Thread.run(Thread.java:745)
28_java锁之自旋锁理论知识
自旋锁(Spin Lock
)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
提到了互斥同步对性能最大的影响阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程
“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
优缺点
优点: 循环比较获取直到成功为止,没有类似于 wait
的阻塞
缺点: 当不断自旋的线程越来越多的时候,会因为执行 while
循环不断的消耗 CPU
资源
29_java锁之自旋锁代码验证
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class SpinLockDemo {
// 现在的泛型装的是Thread,原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock() {
// 获取当前进来的线程
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t come in ");
// 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
while(!atomicReference.compareAndSet(null, thread)) {
//摸鱼
}
}
public void myUnLock() {
// 获取当前进来的线程
Thread thread = Thread.currentThread();
// 自己用完了后,把atomicReference变成null
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
// 启动t1线程,开始操作
new Thread(() -> {
// 开始占有锁
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 开始释放锁
spinLockDemo.myUnLock();
}, "t1").start();
// 让main线程暂停1秒,使得t1线程,先执行
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 1秒后,启动t2线程,开始占用这个锁
new Thread(() -> {
// 开始占有锁
spinLockDemo.myLock();
// 开始释放锁
spinLockDemo.myUnLock();
}, "t2").start();
}
}
30_java锁之读写锁理论知识
-
独占锁:指该锁一次只能被一个线程所持有。对
ReentrantLock
和Synchronized
而言都是独占锁 -
共享锁:指该锁可被多个线程所持有。
多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。但是,如果有一个线程想去写共享资源来,就不应该再有其它线程可以对该资源进行读或写。
对ReentrantReadWriteLock
其读锁是共享锁,其写锁是独占锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
- 读-读:能共存
- 读-写:不能共存
- 写-写:不能共存
31_java锁之读写锁代码验证
实现一个读写缓存的操作,假设开始没有加锁的时候,会出现什么情况
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/**
* 资源类
*/
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
// private Lock lock = null;
/**
* 定义写操作
* 满足:原子 + 独占
* @param key
* @param value
*/
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成");
}
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 线程操作资源类,5个线程写
for (int i = 0; i < 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}
// 线程操作资源类, 5个线程读
for (int i = 0; i < 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
}
}
我们分别创建5个线程写入缓存
// 线程操作资源类,5个线程写
for (int i = 0; i < 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}
5个线程读取缓存,
// 线程操作资源类, 5个线程读
for (int i = 0; i < 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
最后运行结果:
0 正在写入:0
4 正在写入:4
3 正在写入:3
1 正在写入:1
2 正在写入:2
0 正在读取:
1 正在读取:
2 正在读取:
3 正在读取:
4 正在读取:
2 写入完成
4 写入完成
4 读取完成:null
0 写入完成
3 读取完成:null
0 读取完成:null
1 写入完成
3 写入完成
1 读取完成:null
2 读取完成:null
我们可以看到,在写入的时候,写操作都没其它线程打断了,这就造成了,还没写完,其它线程又开始写,这样就造成数据不一致!!!
解决方法
上面的代码是没有加锁的,这样就会造成线程在进行写入操作的时候,被其它线程频繁打断,从而不具备原子性,这个时候,我们就需要用到读写锁来解决了
/**
* 创建一个读写锁
* 它是一个读写融为一体的锁,在使用的时候,需要转换
*/
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
当我们在进行写操作的时候,就需要转换成写锁
// 创建一个写锁
rwLock.writeLock().lock();
// 写锁 释放
rwLock.writeLock().unlock();
当们在进行读操作的时候,在转换成读锁
// 创建一个读锁
rwLock.readLock().lock();
// 读锁 释放
rwLock.readLock().unlock();
这里的读锁和写锁的区别在于,写锁一次只能一个线程进入,执行写操作,而读锁是多个线程能够同时进入,进行读取的操作
完整代码:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 资源类
*/
class MyCache {
/**
* 缓存中的东西,必须保持可见性,因此使用volatile修饰
*/
private volatile Map<String, Object> map = new HashMap<>();
/**
* 创建一个读写锁
* 它是一个读写融为一体的锁,在使用的时候,需要转换
*/
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
/**
* 定义写操作
* 满足:原子 + 独占
* @param key
* @param value
*/
public void put(String key, Object value) {
// 创建一个写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 写锁 释放
rwLock.writeLock().unlock();
}
}
/**
* 获取
* @param key
*/
public void get(String key) {
// 读锁
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 读锁释放
rwLock.readLock().unlock();
}
}
/**
* 清空缓存
*/
public void clean() {
map.clear();
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 线程操作资源类,5个线程写
for (int i = 1; i <= 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}
// 线程操作资源类, 5个线程读
for (int i = 1; i <= 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
}
}
运行结果:
1 正在写入:1
1 写入完成
2 正在写入:2
2 写入完成
3 正在写入:3
3 写入完成
4 正在写入:4
4 写入完成
5 正在写入:5
5 写入完成
2 正在读取:
3 正在读取:
1 正在读取:
4 正在读取:
5 正在读取:
2 读取完成:2
1 读取完成:1
4 读取完成:4
3 读取完成:3
5 读取完成:5
从运行结果我们可以看出,写入操作是一个一个线程进行执行的,并且中间不会被打断,而读操作的时候,是同时5个线程进入,然后并发读取操作 。
补充:
为什么Synchronized无法禁止指令重排,却能保证有序性
前言
首先我们要分析下这道题,这简单的一个问题,其实里面还是包含了很多信息的,要想回答好这个问题,面试者至少要知道一下概念:
- Java内存模型
- 并发编程有序性问题
- 指令重排
- synchronized锁
- 可重入锁
- 排它锁
- as-if-serial语义
- 单线程&多线程
标准解答
为了进一步提升计算机各方面能力,在硬件层面做了很多优化,如处理器优化和指令重排等,但是这些技术的引入就会导致有序性问题。
先解释什么是有序性问题,也知道是什么原因导致的有序性问题
我们也知道,最好的解决有序性问题的办法,就是禁止处理器优化和指令重排,就像volatile中使用内存屏障一样。
表明你知道啥是指令重排,也知道他的实现原理
但是,虽然很多硬件都会为了优化做一些重排,但是在Java中,不管怎么排序,都不能影响单线程程序的执行结果。这就是as-if-serial语义,所有硬件优化的前提都是必须遵守as-if-serial语义。
as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担心重排序会 干扰他们,也无需担心内存可见性问题。
重点!解释下什么是as-if-serial语义,因为这是这道题的第一个关键词,答上来就对了一半了
再说下synchronized,他是Java提供的锁,可以通过他对Java中的对象加锁,并且他是一种排他的、可重入的锁。
所以,当某个线程执行到一段被synchronized修饰的代码之前,会先进行加锁,执行完之后再进行解锁。在加锁之后,解锁之前,其他线程是无法再次获得锁的,只有这条加锁线程可以重复获得该锁。
介绍synchronized的原理,这是本题的第二个关键点,到这里基本就可以拿满分了。
synchronized通过排他锁的方式就保证了同一时间内,被synchronized修饰的代码是单线程执行的。所以呢,这就满足了as-if-serial语义的一个关键前提,那就是单线程,因为有as-if-serial语义保证,单线程的有序性就天然存在了。
来源
https://mp.weixin.qq.com/s/Pd6dOXaMQFUHfAUnOhnwtw
32_CountDownLatch
让一线程阻塞直到另一些线程完成一系列操作才被唤醒。
CountDownLatch
主要有两个方法(await()
,countDown()
)。
当一个或多个线程调用await()
时,调用线程会被阻塞。其它线程调用countDown()
会将计数器减1(调用countDown
方法的线程不会阻塞),当计数器的值变为零时,因调用await
方法被阻塞的线程会被唤醒,继续执行。
latch
英 [lætʃ] 美 [lætʃ]
n. 门闩;插销;碰锁;弹簧锁
v. 用插销插上;用碰锁锁上
场景:班长关门
假设一个自习室里有7个人,其中有一个是班长,班长的主要职责就是在其它6个同学走了后,关灯,锁教室门,然后走人,因此班长是需要最后一个走的,那么有什么方法能够控制班长这个线程是最后一个执行,而其它线程是随机执行的
解决方案
这个时候就用到了 CountDownLatch,计数器了。我们一共创建 6 个线程,然后计数器的值也设置成 6
// 计数器
CountDownLatch countDownLatch = new CountDownLatch(6);
然后每次学生线程执行完,就让计数器的值减 1
for (int i = 0; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
最后我们需要通过CountDownLatch的await方法来控制班长主线程的执行,这里 countDownLatch.await()可以想成是一道墙,只有当计数器的值为0的时候,墙才会消失,主线程才能继续往下执行
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t 班长最后关门");
不加 CountDownLatch 的执行结果,我们发现 main 线程提前已经执行完成了
1 上完自习,离开教室
0 上完自习,离开教室
main 班长最后关门
2 上完自习,离开教室
3 上完自习,离开教室
4 上完自习,离开教室
5 上完自习,离开教室
6 上完自习,离开教室
引入CountDownLatch后的执行结果,我们能够控制住 main 方法的执行,这样能够保证前提任务的执行
0 上完自习,离开教室
2 上完自习,离开教室
4 上完自习,离开教室
1 上完自习,离开教室
5 上完自习,离开教室
6 上完自习,离开教室
3 上完自习,离开教室
main 班长最后关门
完整代码
package com.moxi.interview.study.thread;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 计数器
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室");
//计数器 -1
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
//设置屏障
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t 班长最后关门");
}
}
温习枚举
枚举 + CountDownLatch
程序演示秦国统一六国
import java.util.Objects;
public enum CountryEnum {
ONE(1, "齐"), TWO(2, "楚"), THREE(3, "燕"), FOUR(4, "赵"), FIVE(5, "魏"), SIX(6, "韩");
private Integer retcode;
private String retMessage;
CountryEnum(Integer retcode, String retMessage) {
this.retcode = retcode;
this.retMessage = retMessage;
}
public static CountryEnum forEach_countryEnum(int index) {
CountryEnum[] myArray = CountryEnum.values();
for(CountryEnum ce : myArray) {
if(Objects.equals(index, ce.getRetcode())) {
return ce;
}
}
return null;
}
public Integer getRetcode() {
return retcode;
}
public void setRetcode(Integer retcode) {
this.retcode = retcode;
}
public String getRetMessage() {
return retMessage;
}
public void setRetMessage(String retMessage) {
this.retMessage = retMessage;
}
}
import java.util.concurrent.CountDownLatch;
public class UnifySixCountriesDemo {
public static void main(String[] args) throws InterruptedException {
// 计数器
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "国被灭了!");
countDownLatch.countDown();
}, CountryEnum.forEach_countryEnum(i).getRetMessage()).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + " 秦国统一中原。");
}
}
输出结果:
齐国被灭了!
燕国被灭了!
楚国被灭了!
魏国被灭了!
韩国被灭了!
赵国被灭了!
main 秦国统一中原。
33_CyclicBarrierDemo
CyclicBarrier的字面意思就是可循环(Cyclic)使用的屏障(Barrier)。它要求做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await方法。
CyclicBarrier与CountDownLatch的区别:CyclicBarrier可重复多次,而CountDownLatch只能是一次。
程序演示集齐7个龙珠,召唤神龙
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class SummonTheDragonDemo {
public static void main(String[] args) {
/**
* 定义一个循环屏障,参数1:需要累加的值,参数2 需要执行的方法
*/
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("召唤神龙");
});
for (int i = 1; i <= 7; i++) {
final Integer tempInt = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 收集到 第" + tempInt + "颗龙珠");
try {
// 先到的被阻塞,等全部线程完成后,才能执行方法
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
输出结果:
2 收集到 第2颗龙珠
6 收集到 第6颗龙珠
1 收集到 第1颗龙珠
7 收集到 第7颗龙珠
5 收集到 第5颗龙珠
4 收集到 第4颗龙珠
3 收集到 第3颗龙珠
召唤神龙
34_SemaphoreDemo
信号量主要用于两个目的
- 一个是用于多个共享资源的互斥使用
- 另一个用于并发线程数的控制。
semaphore
英 [ˈseməfɔː®] 美 [ˈseməfɔːr]
n. 信号标;旗语
v. 打旗语;(用其他类似的信号系统)发信号
正常的锁(concurrency.locks或synchronized锁)在任何时刻都只允许一个任务访问一项资源,而 Semaphore允许n个任务同时访问这个资源。
模拟一个抢车位的场景,假设一共有6个车,3个停车位
通过acquire()
和 release()
实现 。
案例:抢车位
我们模拟一个抢车位的场景,假设一共有6个车,3个停车位
那么我们首先需要定义信号量为3,也就是3个停车位
/**
* 初始化一个信号量为3,默认是false 非公平锁, 模拟3个停车位
*/
Semaphore semaphore = new Semaphore(3, false);
然后我们模拟6辆车同时并发抢占停车位,但第一个车辆抢占到停车位后,信号量需要减1
// 代表一辆车,已经占用了该车位
semaphore.acquire(); // 抢占
同时车辆假设需要等待3秒后,释放信号量
// 每个车停3秒
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
最后车辆离开,释放信号量
// 释放停车位
semaphore.release();
完整代码
public class SemaphoreDemo {
public static void main(String[] args) {
/**
* 初始化一个信号量为3,默认是false 非公平锁, 模拟3个停车位
*/
Semaphore semaphore = new Semaphore(3, false);
// 模拟6部车
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
// 代表一辆车,已经占用了该车位
semaphore.acquire(); // 抢占
System.out.println(Thread.currentThread().getName() + "\t 抢到车位");
// 每个车停3秒
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t 离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放停车位
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
运行结果
0 抢到车位
2 抢到车位
1 抢到车位
2 离开车位
1 离开车位
3 抢到车位
0 离开车位
4 抢到车位
5 抢到车位
4 离开车位
3 离开车位
5 离开车位
看运行结果能够发现,0 2 1 车辆首先抢占到了停车位,然后等待3秒后,离开,然后后面 3 4 5 又抢到了车位 。
35_阻塞队列理论
-
阻塞队列有没有好的一面
-
不得不阻塞,你如何管理
36_阻塞队列接口结构和实现类
阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示:
线程1 通过 Put 往阻塞队列中添加元素,而线程 2 通过 Take 从阻塞队列中移除元素:
-
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞
-
当阻塞队列是满时,从队列中添加元素的操作将会被阻塞
[ 类比 1 ]: -
当蛋糕店的柜子空的时候,无法从柜子里面获取蛋糕
-
当蛋糕店的柜子满的时候,无法继续向柜子里面添加蛋糕了
[ 即 ]: -
试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其它线程往空的队列插入新的元素
-
试图往已经满的阻塞队列中添加新元素的线程,直到其它线程往满的队列中移除一个或多个元素,或者完全清空队列后,使队列重新变得空闲起来,并后续新增
为什么要用 ?
去海底捞吃饭,大厅满了,需要进候厅等待,但是这些等待的客户能够对商家带来利润,因此我们非常欢迎他们阻塞
在多线程领域:所 谓 的 阻 塞 , 在 某 些 清 空 下 会 挂 起 线 程 ( 即 阻 塞 ) , 一 旦 条 件 满 足 , 被 挂 起 的 线 程 又 会 自 动 唤 醒 \color{red}{所谓的阻塞,在某些清空下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动唤醒}所谓的阻塞,在某些清空下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动唤醒
为什么需要 BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切 BlockingQueue 都帮你一手包办了
在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须自己取控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度 [ 减少程序员的负担 :不需要手动阻塞或者唤醒 ] 。
架构
[ 面试 ]
// 你用过List集合类
// ArrayList集合类熟悉么?
// 还用过 CopyOnWriteList 和 BlockingQueue
BlockingQueue 阻塞队列是属于一个接口,底下有七个实现类:
ArrayBlockQueue
: 由数组结构组成的有界阻塞队列LinkedBlockingQueue
:由链表结构组成的有界(但是默认大小 Integer.MAX_VALUE)的阻塞队列- [ 有界,但是界限非常大,相当于无界,可以当成无界 ]
- PriorityBlockQueue:支持优先级排序的无界阻塞队列
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列
SynchronousQueue
:不存储元素的阻塞队列,也即单个元素的队列- [ 生产一个,消费一个,不存储元素,不消费不生产 ]
- LinkedTransferQueue:由链表结构组成的无界阻塞队列
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列
这里需要掌握的是:ArrayBlockQueue
、LinkedBlockingQueue
、SynchronousQueue
BlockingQueue的核心方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
性质 | 说明 |
---|---|
抛出异常 | 当阻塞队列满时:在往队列中add插入元素会抛出 IIIegalStateException:Queue full当阻塞队列空时:再往队列中remove移除元素,会抛出NoSuchException |
特殊性 | 插入方法,成功true,失败false移除方法:成功返回出队列元素,队列没有就返回空 |
一直阻塞 | 当阻塞队列满时,生产者继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出。当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。 |
超时退出 | 当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出 |
37_阻塞队列api之抛出异常组
但执行 add 方法,向已经满的 ArrayBlockingQueue 中添加元素时候,会抛出异常
// 阻塞队列,[ 需要填入默认值 ]
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
System.out.println(blockingQueue.add("XXX"));
运行后:
true
true
true
Exception in thread "main" java.lang.IllegalStateException: Queue full
at java.util.AbstractQueue.add(AbstractQueue.java:98)
at java.util.concurrent.ArrayBlockingQueue.add(ArrayBlockingQueue.java:312)
at com.moxi.interview.study.queue.BlockingQueueDemo.main(BlockingQueueDemo.java:25)
同时如果我们多取出元素的时候,也会抛出异常,我们假设只存储了 3 个值,但是取的时候,取了 4 次
// 阻塞队列,[ 需要填入默认值 ]
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
//此时队列为空
System.out.println(blockingQueue.remove());
那么出现异常
true
true
true
a
b
c
Exception in thread "main" java.util.NoSuchElementException
at java.util.AbstractQueue.remove(AbstractQueue.java:117)
at com.moxi.interview.study.queue.BlockingQueueDemo.main(BlockingQueueDemo.java:30)
完整代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueExceptionDemo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
try {
//抛出 java.lang.IllegalStateException: Queue full
System.out.println(blockingQueue.add("XXX"));
} catch (Exception e) {
System.err.println(e);
}
System.out.println(blockingQueue.element());
///
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
try {
//抛出 java.util.NoSuchElementException
System.out.println(blockingQueue.remove());
} catch (Exception e) {
System.err.println(e);
}
try {
//element()相当于peek(),但element()会抛NoSuchElementException
System.out.println(blockingQueue.element());
} catch (Exception e) {
System.err.println(e);
}
}
}
输出结果:
true
true
true
a
java.lang.IllegalStateException: Queue full
a
b
c
java.util.NoSuchElementException
java.util.NoSuchElementException
38_阻塞队列api之返回布尔值组
我们使用 offer 的方法,添加元素时候,如果阻塞队列满了后,会返回 false ,否者返回 true
同时在取的时候,如果队列已空,那么会返回 null [ 避免了产生异常,叫停程序 ]
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueBooleanDemo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("d"));
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
}
}
输出结果:
true
true
true
false
a
b
c
null
39_阻塞队列api之阻塞和超时控制
我们使用 put 的方法,添加元素时候,如果阻塞队列满了后,添加消息的线程,会一直阻塞,直到队列元素减少,会被清空,才会唤醒
一般在消息中间件,比如 RabbitMQ 中会使用到,因为需要 [ 保证消息百分百不丢失,因此只有让它阻塞 ]
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
System.out.println("================");
blockingQueue.take();
blockingQueue.take();
blockingQueue.take();
blockingQueue.take();
完整代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class BlockingQueueBlockedDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
new Thread(()->{
try {
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
blockingQueue.put("c");//将会阻塞,直到主线程take()
System.out.println("it was blocked.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(2);
try {
blockingQueue.take();
blockingQueue.take();
blockingQueue.take();
blockingQueue.take();
System.out.println("Blocking...");
blockingQueue.take();//将会阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
同时使用 take 取消息的时候,如果内容不存在的时候,也会被阻塞,直到队列有元素时,才唤醒
[ 如果一直队列一直没有元素呢 ,让它一直阻塞吗 ? ]
不见不散组
在 offer( ) , poll 的基础上 [ 加时间 ]
使用 offer 插入的时候,需要指定时间,如果 2 秒还没有插入,那么就放弃插入
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("b", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("c", 2L, TimeUnit.SECONDS));
//尝试加入元素,如果 2s 都没有成功,放弃,返回 false
System.out.println(blockingQueue.offer("d", 2L, TimeUnit.SECONDS));
同时 poll 取的时候也进行判断
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
//尝试取元素,如果 2s 都未取到,返回 null
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
如果 2 秒内取不出来,那么就返回 null
完整代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class BlockingQueueTimeoutDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println("Offer.");
System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("b", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("c", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("d", 2L, TimeUnit.SECONDS));
System.out.println("Poll.");
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
}
}
输出结果:
Offer.
true
true
true
false
Poll.
a
b
c
null
40_阻塞队列之同步SynchronousQueue队列
SynchronousQueue没有容量。
与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。
每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
实例
首先我们创建了两个线程,一个线程用于生产,一个线程用于消费
生产的线程分别 put 了 A、B、C这三个字段
//定义一个[不储存元素的]阻塞队列
BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "\t put A");
synchronousQueue.put("A");
System.out.println(Thread.currentThread().getName() + "\t put B");
synchronousQueue.put("B");
System.out.println(Thread.currentThread().getName() + "\t put C");
synchronousQueue.put("C");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
消费线程使用 take,消费阻塞队列中的内容,并且每次消费前,都等待 5 秒
new Thread(() -> {
try {
try {
//睡眠5s
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronousQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take A");
try {
//睡眠5s
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronousQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take B");
try {
//睡眠5s
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronousQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take C");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
最后结果输出为:
t1 put A
5秒后...
t2 take A
t1 put B
5秒后...
t2 take B
t1 put C
5秒后...
t2 take C
我们从最后的运行结果可以看出,每次 t1 线程向队列中添加阻塞队列添加元素后,t1 输入线程就会等待 t2 消费线程,t2 消费后,t2 处于挂起状态,等待 t1 在 存入,从而周而复始,形成 一存一取的状态 。
完整版:
package blockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
/**
* @Description: SynchronousQueue使用演示
*/
public class SynchronousQueueDemo {
public static void main(String[] args) {
//定义一个[不储存元素的]阻塞队列
BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "\t put A");
synchronousQueue.put("A");
System.out.println(Thread.currentThread().getName() + "\t put B");
synchronousQueue.put("B");
System.out.println(Thread.currentThread().getName() + "\t put C");
synchronousQueue.put("C");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(() -> {
try {
try {
//睡眠5s
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronousQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take A");
try {
//睡眠5s
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronousQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take B");
try {
//睡眠5s
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronousQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take C");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
}
}
41_线程通信之生产者消费者传统版
阻塞队列用在哪里?
- 生产者消费者模式
- 传统版(synchronized, wait, notify)
- 阻塞队列版(lock, await, signal)
- 线程池
- 消息中间件
实现一个简单的生产者消费者模式
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ShareData {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() throws Exception{
// 同步代码块,加锁
lock.lock();
try {
// 判断
while(number != 0) {
// 等待不能生产
condition.await();
}
// 干活
number++;
System.out.println(Thread.currentThread().getName() + "\t " + number);
// 通知 唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws Exception{
// 同步代码块,加锁
lock.lock();
try {
// 判断
while(number == 0) {
// 等待不能消费
condition.await();
}
// 干活
number--;
System.out.println(Thread.currentThread().getName() + "\t " + number);
// 通知 唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class TraditionalProducerConsumerDemo {
public static void main(String[] args) {
ShareData shareData = new ShareData();
// t1线程,生产
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
shareData.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "t1").start();
// t2线程,消费
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
shareData.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "t2").start();
}
}
输出结果:
t1 1
t2 0
t1 1
t2 0
t1 1
t2 0
t1 1
t2 0
t1 1
t2 0
t1 1
t2 0
t1 1
t2 0
t1 1
t2 0
t1 1
t2 0
t1 1
t2 0
注意,increment()和decrement()内的
// 判断
while(number != 0) {
// 等待不能生产
condition.await();
}
不能用
// 判断
if(number != 0) {
// 等待不能生产
condition.await();
}
否则会出现虚假唤醒,出现异常状况。
42_Synchronized和Lock有什么区别
- synchronized属于JVM层面,属于java的关键字
- monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象 只能在同步块或者方法中才能调用 wait/ notify等方法)
- Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁
- 使用方法:
- synchronized:不需要用户去手动释放锁,当synchronized代码执行后,系统会自动让线程释放对锁的占用。
- ReentrantLock:则需要用户去手动释放锁,若没有主动释放锁,就有可能出现死锁的现象,需要lock() 和 unlock() 配置try catch语句来完成
- 等待是否中断
- synchronized:不可中断,除非抛出异常或者正常运行完成。
- ReentrantLock:可中断,可以设置超时方法
- 设置超时方法,trylock(long timeout, TimeUnit unit)
- lockInterrupible() 放代码块中,调用interrupt() 方法可以中断
- 加锁是否公平
- synchronized:非公平锁
- ReentrantLock:默认非公平锁,构造函数可以传递boolean值,true为公平锁,false为非公平锁
- 锁绑定多个条件Condition
- synchronized:没有,要么随机,要么全部唤醒
- ReentrantLock:用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized那样,要么随机,要么全部唤醒
43_锁绑定多个条件Condition
针对刚刚提到的区别的第 5 条,我们有下面这样的一个场景
题目:多线程之间按顺序调用,实现 A-> B -> C 三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
紧接着
AA打印5次,BB打印10次,CC打印15次
..
来10轮
我们会发现,这样的场景在使用 synchronized 来完成的话,会非常的困难,但是使用 lock 就非常方便了
也就是我们需要实现一个链式唤醒的操作
当 A 线程执行完后,B 线程才能执行,然后 B 线程执行完成后,C 线程才执行
首先我们需要创建一个重入锁
// 创建一个重入锁
private Lock lock = new ReentrantLock();
然后定义三个条件,也可以称为 [ 锁的钥匙 ],通过它就可以获取到锁,进入到方法里面
// 这三个相当于备用钥匙
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
然后开始记住锁的三部曲: [ 判断 干活 唤醒 ]
这里的判断,为了 [ 避免虚假唤醒,一定要采用 while ]
干活就是把需要的内容,打印出来
唤醒的话,就是修改资源类的值,然后精准唤醒线程进行干活:这里A 唤醒B, B唤醒C,C又唤醒A
public void print5() {
lock.lock();
try {
// 判断
while(number != 1) {
// 不等于1,需要等待
condition1.await();
}
// 干活
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t " + number + "\t" + i);
}
// 唤醒 (干完活后,需要通知B线程执行)
number = 2;
// 通知2号去干活了
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
/******************* 资源类 *******************/
class ShareResource {
// A 1 B 2 c 3
private int number = 1;
// 创建一个重入锁
private Lock lock = new ReentrantLock();
// 这三个相当于备用钥匙
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void print5() {
//同步代码块:加锁
lock.lock();
try {
// 判断
while(number != 1) {
// 不等于1,需要等待
condition1.await();
}
// 干活
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t " + number + "\t" + i);
}
// 唤醒 (干完活后,需要通知B线程执行)
number = 2;
// 通知2号去干活了
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
// 判断
while(number != 2) {
// 不等于1,需要等待
condition2.await();
}
// 干活
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "\t " + number + "\t" + i);
}
// 唤醒 (干完活后,需要通知C线程执行)
number = 3;
// 通知2号去干活了
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
// 判断
while(number != 3) {
// 不等于1,需要等待
condition3.await();
}
// 干活
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + "\t " + number + "\t" + i);
}
// 唤醒 (干完活后,需要通知C线程执行)
number = 1;
// 通知1号去干活了
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/******************* 测试类 *******************/
public class SyncAndReentrantLockDemo {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.print5();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.print10();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.print15();
}
}, "C").start();
}
}
最终结果
A 1 0
A 1 1
A 1 2
A 1 3
A 1 4
B 2 0
B 2 1
B 2 2
B 2 3
B 2 4
B 2 5
B 2 6
B 2 7
B 2 8
B 2 9
C 3 0
C 3 1
C 3 2
C 3 3
C 3 4
C 3 5
C 3 6
C 3 7
C 3 8
C 3 9
C 3 10
C 3 11
C 3 12
C 3 13
C 3 14
.....
执行 10 轮
44_线程通信之生产者消费者阻塞队列版
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
class MyResource {
// 默认开启,进行生产消费
// 这里用到了volatile是为了保持数据的可见性,也就是当TLAG修改时,要马上通知其它线程进行修改
private volatile boolean FLAG = true;
// 使用原子包装类,而不用number++
private AtomicInteger atomicInteger = new AtomicInteger();
// 这里不能为了满足条件,而实例化一个具体的SynchronousBlockingQueue
BlockingQueue<String> blockingQueue = null;
// 而应该采用依赖注入里面的,构造注入方法传入
public MyResource(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
// 查询出传入的class是什么
System.out.println(blockingQueue.getClass().getName());
}
public void myProducer() throws Exception{
String data = null;
boolean retValue;
// 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
// 当FLAG为true的时候,开始生产
while(FLAG) {
data = atomicInteger.incrementAndGet() + "";
// 2秒存入1个data
retValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
if(retValue) {
System.out.println(Thread.currentThread().getName() + "\t 插入队列:" + data + "成功" );
} else {
System.out.println(Thread.currentThread().getName() + "\t 插入队列:" + data + "失败" );
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "\t 停止生产,表示FLAG=false,生产结束");
}
public void myConsumer() throws Exception{
String retValue;
// 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
// 当FLAG为true的时候,开始生产
while(FLAG) {
// 2秒存入1个data
retValue = blockingQueue.poll(2L, TimeUnit.SECONDS);
if(retValue != null && retValue != "") {
System.out.println(Thread.currentThread().getName() + "\t 消费队列:" + retValue + "成功" );
} else {
FLAG = false;
System.out.println(Thread.currentThread().getName() + "\t 消费失败,队列中已为空,退出" );
// 退出消费队列
return;
}
}
}
/**
* 停止生产的判断
*/
public void stop() {
this.FLAG = false;
}
}
public class ProducerConsumerWithBlockingQueueDemo {
public static void main(String[] args) {
// 传入具体的实现类, ArrayBlockingQueue
MyResource myResource = new MyResource(new ArrayBlockingQueue<String>(10));
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 生产线程启动\n\n");
try {
myResource.myProducer();
System.out.println("\n");
} catch (Exception e) {
e.printStackTrace();
}
}, "producer").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 消费线程启动");
try {
myResource.myConsumer();
} catch (Exception e) {
e.printStackTrace();
}
}, "consumer").start();
// 5秒后,停止生产和消费
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("\n\n5秒中后,生产和消费线程停止,线程结束");
myResource.stop();
}
}
输出结果:
java.util.concurrent.ArrayBlockingQueue
producer 生产线程启动
consumer 消费线程启动
producer 插入队列:1成功
consumer 消费队列:1成功
producer 插入队列:2成功
consumer 消费队列:2成功
producer 插入队列:3成功
consumer 消费队列:3成功
producer 插入队列:4成功
consumer 消费队列:4成功
producer 插入队列:5成功
consumer 消费队列:5成功
5秒中后,生产和消费线程停止,线程结束
producer 停止生产,表示FLAG=false,生产结束
consumer 消费失败,队列中已为空,退出
45_Callable接口
前言
获取多线程的方法,我们都知道有三种,还有一种是实现 Callable 接口
- 实现 Runnable 接口
- 实现 Callable 接口
- 实例化 Thread 类
- 使用 线程池 获取
Callable 接口
Callable 接口,是一种让线程执行完成后,[ 能够返回结果的 ]
在说到 Callable 接口的时候,我们不得不提到 Runnable 接口 [ 两种写法 ]
/**
* 实现Runnable接口
*/
class MyThread implements Runnable {
@Override
public void run() {
}
}
/***** 实现 Runnable 接口*******/
new Thread(()->{
System.out.println("线程方法体");
},"t1").start();
实现 Runnable 接口的时候,需要重写 run 方法,也就是线程在启动的时候,会自动调用的方法
使用
同理,我们实现 Callable 接口,也需要实现 call 方法,但是这个时候我们还需要有返回值,这个Callable 接口的应用场景一般就在于批处理业务,比如 [ 转账的时候,需要给一会返回结果的状态码回来,代表本次操作成功还是失败 ]
/**
* Callable有返回值
* 批量处理的时候,需要带返回值的接口(例如支付失败的时候,需要返回错误状态)
*
*/
class MyThread2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("come in Callable");
return 1024;
}
}
最后我们需要做的就是通过 Thread 线程, 将 MyThread2 实现 Callable 接口的类包装起来
这里需要用到的是 FutureTask 类,他实现了 Runnable 接口,并且还需要传递一个实现 Callable 接口的类作为构造函数
// FutureTask:实现了Runnable接口,构造函数又需要传入 Callable接口
// 这里通过了FutureTask接触了Callable接口
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
然后在用 Thread 进行实例化,传入实现 Runnabnle 接口的 FutureTask 的类
Thread t1 = new Thread(futureTask, "aaa");
t1.start();
最后通过 utureTask.get() 获取到返回值
// 输出FutureTask的返回值
System.out.println("result FutureTask " + futureTask.get());
这就相当于原来我们的方式是 main 方法一条龙之心,后面在引入 Callable 后,对于执行比较久的线程,可以单独新开一个线程进行执行,最后在进行汇总输出
最后需要注意的是: 最后获得 Callable 线程的计算结果,如果没有计算完成就要去强求获得,会导致阻塞,直到计算完成
就是说: [ futureTask.get() 需要放在最后执行,这样不会导致主线程阻塞 ]
也可以使用下面算法,使用类似于自旋锁的方式来进行判断是否运行完毕
也可以使用下面算法,使用类似于 [ 自旋锁 ] 的方式来进行判断是否运行完毕
// 判断futureTask是否计算完成
while(!futureTask.isDone()) {
}
System.out.println("result FutureTask " + futureTask.get());
注意
多个线程执行 一个 FutureTask 的时候,只会计算一次
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
// 开启两个线程计算futureTask
new Thread(futureTask, "AAA").start();
new Thread(futureTask, "BBB").start();
如果我们要两个线程同时计算任务的话,那么需要这样写,需要定义两个 futureTask
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
FutureTask<Integer> futureTask2 = new FutureTask<>(new MyThread2());
// 开启两个线程计算futureTask
new Thread(futureTask, "AAA").start();
new Thread(futureTask2, "BBB").start();
46_线程池使用及优势
为什么用线程池
线程池做的主要工作就是控制运行的线程的数量,处理过程中,将任务放入到队列中,然后线程创建后,启动这些任务,如果线程数量超过了最大数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:[ 线程复用、控制最大并发数、管理线程 ]
线程池中的任务是放入到阻塞队列中的
线程池的好处
多核处理的好处是:[ 省略的上下文的切换开销 ]
原来我们实例化对象的时候,是使用 new 关键字进行创建,到了 Spring 后,我们学了 IOC 依赖注入,发现Spring 帮我们将对象已经加载到了 Spring 容器中,只需要通过 @Autowrite 注解,就能够自动注入,从而使用
因此使用多线程有下列的好处
- 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就立即执行
- 提高线程的可管理性。线程是稀缺资源,如果无线创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
47_线程池3个常用方式
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
创建线程池
- Executors.newFixedThreadPool(int i) :创建一个拥有 i 个线程的线程池
- 执行长期的任务,性能好很多
- 创建一个 [ 定长线程池 ],可控制线程数最大并发数,超出的线程会在队列中等待
- Executors.newSingleThreadExecutor:创建一个只有 1 个线程的 [ 单线程池 ]
- 一个任务一个任务执行的场景
- 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行
- Executors.newCacheThreadPool(); 创建一个 [ 可扩容的线程池 ]
- 执行很多短期异步的小程序或者负载教轻的服务器
- 创建一个可缓存线程池,如果线程长度超过处理需要,可灵活回收空闲线程,如无可回收,则新建新线程
- Executors.newScheduledThreadPool(int corePoolSize):线程池支持定时以及周期性执行任务,创建一个corePoolSize为传入参数,最大线程数为整形的最大数的线程池
具体使用
首先我们需要使用 [ Executors 工具类 ],进行创建线程池,这里创建了一个拥有 5 个线程的线程池
// 一池5个处理线程 [ 用池化技术,一定要记得关闭 ]
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 创建一个只有一个线程的线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 创建一个拥有N个线程的线程池,根据调度创建合适的线程
ExecutorService threadPool = Executors.newCacheThreadPool();
然后我们执行下面的的应用场景
模拟10个用户来办理业务,每个用户就是一个来自外部请求线程
我们需要使用 [ threadPool.execute ] 执行业务,execute 需要传入一个实现了 Runnable 接口的线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 给用户办理业务");
});
然后我们使用完毕后关闭线程池
threadPool.shutdown();
完整代码:
public class MyThreadPoolDemo {
public static void main(String[] args) {
// Array Arrays(辅助工具类)
// Collection Collections(辅助工具类)
// Executor Executors(辅助工具类)
// 一池5个处理线程(用池化技术,一定要记得关闭)
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 模拟10个用户来办理业务,每个用户就是一个来自外部请求线程
try {
// 循环十次,模拟业务办理,让5个线程处理这10个请求
for (int i = 0; i < 10; i++) {
final int tempInt = i;
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
最后结果:
pool-1-thread-1 给用户:0 办理业务
pool-1-thread-5 给用户:4 办理业务
pool-1-thread-1 给用户:5 办理业务
pool-1-thread-4 给用户:3 办理业务
pool-1-thread-2 给用户:1 办理业务
pool-1-thread-3 给用户:2 办理业务
pool-1-thread-2 给用户:9 办理业务
pool-1-thread-4 给用户:8 办理业务
pool-1-thread-1 给用户:7 办理业务
pool-1-thread-5 给用户:6 办理业务
我们能够看到,一共有 5 个线程,在给 10 个用户办理业务
创建周期性执行任务的线程池
Executors.newScheduledThreadPool(int corePoolSize):
线程池支持定时以及周期性执行任务,创建一个corePoolSize为传入参数,最大线程数为整形的最大数的线程池
底层使用 ScheduledThreadPoolExecutor 来实现 ScheduledThreadPoolExecutor 为ThreadPoolExecutor子类
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
执行方法
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
* command:执行的任务 Callable或Runnable接口实现类
* delay:延时执行任务的时间
* unit:延迟时间单位
*/
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit)
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
* @throws IllegalArgumentException {@inheritDoc}
* command:执行的任务 Callable或Runnable接口实现类
* initialDelay 第一次执行任务延迟时间
* period 连续执行任务之间的周期,从上一个任务开始执行时计算延迟多少开始执行下一个任务,但是还会等上一个任务结束之后。
* unit:延迟时间单位
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit)
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
* @throws IllegalArgumentException {@inheritDoc}
* command:执行的任务 Callable或Runnable接口实现类
* initialDelay 第一次执行任务延迟时间
* delay:连续执行任务之间的周期,从上一个任务全部执行完成时计算延迟多少开始执行下一个任务
* unit:延迟时间单位
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit)
底层实现
我们通过查看源码,点击了 Executors.newSingleThreadExecutor 和 Executors.newFixedThreadPool 能够发现底层都是使用了 ThreadPoolExecutor
我们可以看到线程池的内部,还使用到了 LinkedBlockingQueue 链表阻塞队列
同时在查看 Executors.newCacheThreadPool 看到底层用的是 SynchronousBlockingQueue 阻塞队列
最后查看一下,完整的三个创建线程的方法
48_线程池7大参数入门简介
public class ThreadPoolExecutor extends AbstractExecutorService {
...
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
...
}
49_线程池7大参数深入介绍
线程池在创建的时候,一共有7大参数
- corePoolSize:[ 核心线程数 ],线程池中的常驻核心线程数
- 在创建线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
- 当线程池中的线程数目达到 corePoolSize 后,就会把到达的队列放到缓存队列中
- maximumPoolSize:线程池能够容纳同时执行的 [ 最大线程数 ],此值必须大于等于1、
- 相当有扩容后的线程数,这个线程池能容纳的最多线程数
- keepAliveTime:多余的 [ 空闲线程存活时间 ]
- 当线程池数量超过 corePoolSiz e时,当空闲时间达到 keepAliveTime 值时,多余的空闲线程会被销毁,直到只剩下 corePoolSize 个线程为止
- 默认情况下,只有当线程池中的线程数大于 corePoolSize 时,keepAliveTime 才会起作用
- unit:keepAliveTime 的单位
- workQueue:任务队列,被提交的但未被执行的任务(类似于银行里面的候客区)
- LinkedBlockingQueue:链表阻塞队列
- SynchronousBlockingQueue:同步阻塞队列
- threadFactory:表示生成线程池中工作线程的 [ 线程工厂 ],用于创建线程池 一般用默认即可
- handler:[ 拒绝策略 ] ,表示当队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize3)时,如何来拒绝请求执行的 Runnable 的策略
今日值班窗口有 2 个,现在 2 个窗口都有人在办理业余,此时又进来 3 个客人需要办理业务,他们就需要去等待区 [ 阻塞队列 ]等待
此时,又进来 3 个客人需要办理业务,但当值窗口已满,等待区已满,就需要 加班窗口
今天 人特别多,不知为何,又来了 2 个人,课堂经理就需要到门口,对来的进行阻拦了,因为此时网点已容不下更多人
拒绝策略
以下所有拒绝策略都实现了 RejectedExecutionHandler接口
- AbortPolicy:默认,直接抛出 RejectedExcutionException 异常,阻止系统正常运行
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常,如果运行任务丢失,这是一种好方案
- CallerRunsPolicy:该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
50_线程池底层工作原理
文字说明:
- 在创建了线程池后,等待提交过来的任务请求
- 当调用 execute() 方法添加一个请求任务时,线程池会做出如下判断
- 如果正在运行的线程池数量小于 corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入 [ 阻塞队列 ]
- 如果这时候队列满了,并且正在运行的线程数量还小于 maximumPoolSize,那么还是创建非核心线程 like 运行这个任务;
- 如果队列满了并且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和 [ 拒绝策略 ]来执行
- 当一个线程完成任务时,它会从队列中取下一个任务来执行
- 当一个线程无事可做操作一定的时间 (keepAliveTime) 时,线程池会判断:
- 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉
- 所以线程池的所有任务完成后,它会最终收缩到 corePoolSize 的大小
[ 以顾客去银行办理业务为例,谈谈线程池的底层工作原理:]
- 最开始假设来了两个顾客,因为 corePoolSize为 2,因此这两个顾客直接能够去窗口办理
- 后面又来了三个顾客,因为 corePool 已经被顾客占用了,因此只有去候客区,也就是阻塞队列中等待
- 后面的人又陆陆续续来了,候客区可能不够用了,因此需要申请增加处理请求的窗口,这里的窗口指的是线程池中的线程数,以此来解决线程不够用的问题
- 假设受理窗口已经达到最大数,并且请求数还是不断递增,此时候客区和线程池都已经满了,为了防止大量请求冲垮线程池,已经需要开启拒绝策略
- 临时增加的线程会因为超过了最大存活时间,就会销毁,最后从最大数削减到核心数
51_线程池的4种拒绝策略理论简介
等待队列也已经排满了,再也塞不下新任务了同时,线程池中的max线程也达到了,无法继续为新任务服务。
这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK拒绝策略:
- AbortPolicy(默认):直接抛出 RejectedExecutionException异常阻止系统正常运知。
- CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
以上内置拒绝策略均实现了RejectedExecutionHandler接口。
52_线程池实际中使用哪一个
(超级大坑警告)你在工作中单一的/固定数的/可变的三种创建线程池的方法,你用那个多?
答案是一个都不用,我们生产上只能使用自定义的
Executors 中JDK已经给你提供了,为什么不用?
3.【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。
如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。4.【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为
Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
《Java开发手册》–阿里巴巴
53_线程池的手写改造和拒绝策略
采用默认拒绝策略
从上面我们知道,因为默认的 Executors 创建的线程池,底层都是使用 LinkBlockingQueue 作为阻塞队列的,而LinkBlockingQueue 虽然是有界的,但是它的界限是 Integer.MAX_VALUE 大概有 20 多亿,可以相当是无界的了,因此我们要使用 ThreadPoolExecutor 自己手动创建线程池,然后指定阻塞队列的大小
下面我们创建了一个 核心线程数为 2,最大线程数为 5,并且阻塞队列数为 3 的线程池
// 手写线程池
final Integer corePoolSize = 2;
final Integer maximumPoolSize = 5;
final Long keepAliveTime = 1L;
// 自定义线程池,只改变了LinkBlockingQueue的队列大小
ExecutorService executorService = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
然后使用 for 循环,模拟 10 个用户来进行请求
// 模拟10个用户来办理业务,每个用户就是一个来自外部请求线程
try {
// 循环十次,模拟业务办理,让5个线程处理这10个请求
for (int i = 0; i < 10; i++) {
final int tempInt = i;
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
但是在用户执行到第 9 个的时候,触发了异常,程序中断 [ 最大只能运行 8 个 ]
java.util.concurrent.RejectedExecutionException: Task threadPoll.MyThreadPoolExecutorsDemo$$Lambda$14/0x0000000840064c40@5b6f7412 rejected from java.util.concurrent.ThreadPoolExecutor@27973e9b[Running, pool size = 5, active threads = 5, queued tasks = 3, completed tasks = 0]
at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2055)
at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:825)
at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1355)
at threadPoll.MyThreadPoolExecutorsDemo.main(MyThreadPoolExecutorsDemo.java:32)
pool-1-thread-5 给用户:8 办理业务
pool-1-thread-3 给用户:6 办理业务
pool-1-thread-1 给用户:1 办理业务
pool-1-thread-2 给用户:2 办理业务
pool-1-thread-4 给用户:7 办理业务
pool-1-thread-2 给用户:5 办理业务
pool-1-thread-1 给用户:4 办理业务
pool-1-thread-5 给用户:3 办理业务
这是因为触发了拒绝策略,而我们设置的拒绝策略是默认的 AbortPolicy,也就是抛异常的
触发条件是,请求的线程大于 阻塞队列大小 + 最大线程数 = 8 的时候,也就是说第9个线程来获取线程池中的线程时,就会抛出异常从而报错退出。
采用CallerRunsPolicy拒绝策略
当我们更好其它的拒绝策略时,采用CallerRunsPolicy拒绝策略,也称为回退策略,就是把任务丢回原来的请求开启线程着,我们看运行结果
pool-1-thread-1 给用户:0 办理业务
pool-1-thread-5 给用户:7 办理业务
pool-1-thread-4 给用户:6 办理业务
main 给用户:8 办理业务
pool-1-thread-3 给用户:5 办理业务
pool-1-thread-2 给用户:1 办理业务
pool-1-thread-3 给用户:9 办理业务
pool-1-thread-4 给用户:4 办理业务
pool-1-thread-5 给用户:3 办理业务
pool-1-thread-1 给用户:2 办理业务
我们发现,输出的结果里面出现了main线程,因为线程池出发了拒绝策略,把任务回退到main线程,然后main线程对任务进行处理
采用 DiscardPolicy 拒绝策略
pool-1-thread-1 给用户:0 办理业务
pool-1-thread-3 给用户:5 办理业务
pool-1-thread-1 给用户:2 办理业务
pool-1-thread-2 给用户:1 办理业务
pool-1-thread-1 给用户:4 办理业务
pool-1-thread-5 给用户:7 办理业务
pool-1-thread-4 给用户:6 办理业务
pool-1-thread-3 给用户:3 办理业务
采用 DiscardPolicy 拒绝策略会,线程池会自动把后面的任务都直接丢弃,也不报异常,当任务无关紧要的时候,可以采用这个方式
采用DiscardOldestPolicy拒绝策略
pool-1-thread-1 给用户:0 办理业务
pool-1-thread-4 给用户:6 办理业务
pool-1-thread-1 给用户:4 办理业务
pool-1-thread-3 给用户:5 办理业务
pool-1-thread-2 给用户:1 办理业务
pool-1-thread-1 给用户:9 办理业务
pool-1-thread-4 给用户:8 办理业务
pool-1-thread-5 给用户:7 办理业务
这个策略和刚刚差不多,会把最久的队列中的任务替换掉
完整代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class MyThreadPoolExecutorDemo {
public static void doSomething(ExecutorService executorService, int numOfRequest) {
try {
System.out.println(((ThreadPoolExecutor)executorService).getRejectedExecutionHandler().getClass() + ":");
TimeUnit.SECONDS.sleep(1);
for (int i = 0; i < numOfRequest; i++) {
final int tempInt = i;
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
});
}
TimeUnit.SECONDS.sleep(1);
System.out.println("\n\n");
} catch (Exception e) {
System.err.println(e);
} finally {
executorService.shutdown();
}
}
public static ExecutorService newMyThreadPoolExecutor(int corePoolSize,
int maximumPoolSize, int blockingQueueSize, RejectedExecutionHandler handler){
return new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
1,//keepAliveTime
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(blockingQueueSize),
Executors.defaultThreadFactory(),
handler);
}
public static void main(String[] args) {
doSomething(newMyThreadPoolExecutor(2, 5, 3, new ThreadPoolExecutor.AbortPolicy()), 10);
doSomething(newMyThreadPoolExecutor(2, 5, 3, new ThreadPoolExecutor.CallerRunsPolicy()), 20);
doSomething(newMyThreadPoolExecutor(2, 5, 3, new ThreadPoolExecutor.DiscardOldestPolicy()), 10);
doSomething(newMyThreadPoolExecutor(2, 5, 3, new ThreadPoolExecutor.DiscardPolicy()), 10);
}
}
输出结果:
class java.util.concurrent.ThreadPoolExecutor$AbortPolicy:
pool-1-thread-1 给用户:0 办理业务
pool-1-thread-3 给用户:5 办理业务java.util.concurrent.RejectedExecutionException: Task com.lun.concurrency.MyThreadPoolExecutorDemo$$Lambda$1/303563356@eed1f14 rejected from java.util.concurrent.ThreadPoolExecutor@7229724f[Running, pool size = 5, active threads = 0, queued tasks = 0, completed tasks = 8]
pool-1-thread-2 给用户:1 办理业务
pool-1-thread-5 给用户:7 办理业务
pool-1-thread-3 给用户:3 办理业务
pool-1-thread-4 给用户:6 办理业务
pool-1-thread-1 给用户:2 办理业务
pool-1-thread-2 给用户:4 办理业务
class java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy:
pool-2-thread-1 给用户:0 办理业务
pool-2-thread-2 给用户:1 办理业务
pool-2-thread-1 给用户:2 办理业务
pool-2-thread-3 给用户:5 办理业务
pool-2-thread-3 给用户:7 办理业务
pool-2-thread-3 给用户:9 办理业务
pool-2-thread-4 给用户:6 办理业务
pool-2-thread-2 给用户:3 办理业务
pool-2-thread-5 给用户:8 办理业务
main 给用户:10 办理业务
pool-2-thread-1 给用户:4 办理业务
pool-2-thread-3 给用户:11 办理业务
pool-2-thread-4 给用户:13 办理业务
main 给用户:14 办理业务
pool-2-thread-1 给用户:12 办理业务
pool-2-thread-5 给用户:15 办理业务
pool-2-thread-2 给用户:17 办理业务
main 给用户:18 办理业务
pool-2-thread-3 给用户:16 办理业务
pool-2-thread-4 给用户:19 办理业务
class java.util.concurrent.ThreadPoolExecutor$DiscardOldestPolicy:
pool-3-thread-1 给用户:0 办理业务
pool-3-thread-2 给用户:1 办理业务
pool-3-thread-1 给用户:2 办理业务
pool-3-thread-2 给用户:3 办理业务
pool-3-thread-3 给用户:5 办理业务
pool-3-thread-5 给用户:8 办理业务
pool-3-thread-2 给用户:7 办理业务
pool-3-thread-4 给用户:6 办理业务
pool-3-thread-1 给用户:4 办理业务
pool-3-thread-3 给用户:9 办理业务
class java.util.concurrent.ThreadPoolExecutor$DiscardPolicy:
pool-4-thread-1 给用户:0 办理业务
pool-4-thread-2 给用户:1 办理业务
pool-4-thread-1 给用户:2 办理业务
pool-4-thread-2 给用户:3 办理业务
pool-4-thread-3 给用户:5 办理业务
pool-4-thread-3 给用户:9 办理业务
pool-4-thread-1 给用户:4 办理业务
pool-4-thread-5 给用户:8 办理业务
pool-4-thread-4 给用户:6 办理业务
pool-4-thread-2 给用户:7 办理业
54_线程池配置合理线程数
生产环境中如何配置 corePoolSize 和 maximumPoolSize
这个是根据具体业务来配置的,分为 CPU 密集型和 IO 密集型
CPU密集型
-
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行
-
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程)
-
而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些
-
CPU密集型任务 [ 配置尽可能少的线程数量 ] :
一般公式: CPU核数 + 1个线程数
IO密集型
-
由于 IO 密集型任务线程并不是一直在执行任务,则可能多的线程,如 CPU核数 * 2
-
IO 密集型,即该任务需要大量的 IO 操作,即大量的阻塞
-
在单线程上运行 IO 密集型的任务会导致浪费大量的 CPU 运算能力花费在等待上
-
所以 IO 密集型任务中使用多线程可以大大的加速程序的运行,即使在单核 CPU 上,这种加速主要就是利用了被浪费掉的阻塞时间。
-
IO密集时,大部分线程都被阻塞,故需要 [ 多配置线程数 ]:
参考公式: CPU核数 / (1 - 阻塞系数) [ 阻塞系数在0.8 ~ 0.9左右 ]
例如:8核CPU:8/ (1 - 0.9) = 80个线程数
55_死锁编码及定位分析
概念
死锁是指两个或多个以上的进程在执行过程中,因争夺资源而造成一种互相等待的现象,若无外力干涉那他们都将无法推进下去。如果资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
产生死锁的原因
- 系统资源不足
- 进程运行推进的顺序不对
- 资源分配不当
死锁产生的四个必要条件
- 互斥
- 解决方法:把互斥的共享资源封装成可同时访问
- 占有且等待
- 解决方法:进程请求资源时,要求它不占有任何其它资源,也就是它必须一次性申请到所有的资源,这种方式会导致资源效率低。
- 非抢占式
- 解决方法:如果进程不能立即分配资源,要求它不占有任何其他资源,也就是只能够同时获得所有需要资源时,才执行分配操作
- 循环等待
- 解决方法:对资源进行排序,要求进程按顺序请求资源。
死锁代码
我们创建了一个资源类,然后让两个线程分别持有自己的锁,同时在尝试获取别人的,就会出现死锁现象
/*************** 资源类 **************/
public class HoldLockThread implements Runnable {
private String lockA;
private String lockB;
public HoldLockThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName() + "\t 自己持有锁:" + lockA);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t 等待获取锁" + lockB);
synchronized (lockB){
System.out.println(Thread.currentThread().getName() + "\t 已获得锁" + lockB);
}
}
}
}
/*********************** 测试类 ****************/
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldLockThread(lockA,lockB),"t1").start();
new Thread(new HoldLockThread(lockB,lockA),"t2").start();
}
}
运行结果,main线程无法结束
t2 自己持有锁:lockB
t1 自己持有锁:lockA
t1 等待获取锁lockB
t2 等待获取锁lockA
如何排查死锁
当我们出现死锁的时候,首先需要使用 jps 命令查看运行的程序
jps -l
在使用 jstack 查看堆栈信息
jstack 6212 # 后面参数是 jps输出的该类的pid
得到的结果
通过查看最后一行,我们看到 Found 1 deadlock,即存在一个死锁
原文链接
Java开发常见面试题详解(并发,JVM)
https://blog.csdn.net/u011863024/article/details/114684428
并发
https://blog.csdn.net/xj0927/category_10403440.html
其它博客
java并发编程
http://blog.cuzz.site/2019/04/16/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/
并发
https://blog.csdn.net/xj0927/category_10403440.html
码云
https://gitee.com/moxi159753/LearningNotes/tree/master/%E6%A0%A1%E6%8B%9B%E9%9D%A2%E8%AF%95/JUC
GitHub
https://github.com/MrJian8/ThreadDemo
谷粒学院
https://www.gulixueyuan.com/goods/show/31?targetId=96&preview=0
视频资料
https://www.bilibili.com/video/BV1zb411M7NQ?from=search&seid=6874417574046272447