目录
- 01_本课程前提要求和说明
- 02_volatile是什么
- 03_JMM内存模型之可见性
- 04_可见性的代码验证说明
- 05_volatile不保证原子性
- 06_volatile不保证原子性理论解释
- 07_volatile不保证原子性问题解决
- 08_volatile指令重排案例1
- 09_volatile指令重排案例2
- 10_单例模式在多线程环境下可能存在安全问题
- 11_单例模式volatile分析
- 12_CAS是什么
- 13_CAS底层原理-上
- 14_CAS底层原理-下
- 15_CAS缺点
- 16_ABA问题
- 17_AtomicReference原子引用
- 18_AtomicStampedReference版本号原子引用
- 19_ABA问题的解决
- 20_集合类不安全之并发修改异常
- 21_集合类不安全之写时复制
- 22_集合类不安全之Set
- 23_集合类不安全之Map
- 24_TransferValue醒脑小练习
- 25_java锁之公平和非公平锁
- 26_java锁之可重入锁和递归锁理论知识
- 27_java锁之可重入锁和递归锁代码验证
- 28_java锁之自旋锁理论知识
- 29_java锁之自旋锁代码验证
- 30_java锁之读写锁理论知识
- 31_java锁之读写锁代码验证
- 32_CountDownLatch
- 33_CyclicBarrierDemo
- 34_SemaphoreDemo
- 35_阻塞队列理论
- 36_阻塞队列接口结构和实现类
- 37_阻塞队列api之抛出异常组
- 38_阻塞队列api之返回布尔值组
- 39_阻塞队列api之阻塞和超时控制
- 40_阻塞队列之同步SynchronousQueue队列
- 41_线程通信之生产者消费者传统版
- 42_Synchronized和Lock有什么区别
- 43_锁绑定多个条件Condition
- 44_线程通信之生产者消费者阻塞队列版
- 45_Callable接口
- 46_线程池使用及优势
- 47_线程池3个常用方式
- 48_线程池7大参数入门简介
- 49_线程池7大参数深入介绍
- 50_线程池底层工作原理
- 51_线程池的4种拒绝策略理论简介
- 52_线程池实际中使用哪一个
- 53_线程池的手写改造和拒绝策略
- 54_线程池配置合理线程数
- 55_死锁编码及定位分析
- 56_JVMGC下半场技术加强说明和前提知识要求
- 57_JVMGC快速回顾复习串讲
- 58_谈谈你对GCRoots的理解
- 59_JVM的标配参数和X参数
- 60_JVM的XX参数之布尔类型
- 61_JVM的XX参数之设值类型
- 62_JVM的XX参数之XmsXmx坑题
- 63_JVM盘点家底查看初始默认值
- 64_JVM盘点家底查看修改变更值
- 65_堆内存初始大小快速复习
- 66_常用基础参数栈内存Xss讲解
- 67_常用基础参数元空间MetaspaceSize讲解
- 68_常用基础参数PrintGCDetails回收前后对比讲解
- 69_常用基础参数SurvivorRatio讲解
- 70_常用基础参数NewRatio讲解
- 71_常用基础参数MaxTenuringThreshold讲解
- 72_强引用Reference
- 73_软引用SoftReference
- 74_弱引用WeakReference
- 75_软引用和弱引用的适用场景
- 76_WeakHashMap案例演示和解析
- 77_虚引用简介
- 78_ReferenceQueue引用队列介
- 79_虚引用PhantomReference
- 80_GCRoots和四大引用小总结
- 81_SOFE之StackOverflowError
- 82_OOM之Java heap space
- 83_OOM之GC overhead limit exceeded
- 84_OOM之Direct buffer memory
- 85_OOM之unable to create new native thread故障演示
- 86_OOM之unable to create new native thread上限调整
- 87_OOM之Metaspace
- 88_垃圾收集器回收种类
- 89_串行并行并发G1四大垃圾回收方式
- 90_如何查看默认的垃圾收集器
- 91_JVM默认的垃圾收集器有哪些
- 92_GC之7大垃圾收集器概述
- 93_GC之约定参数说明
- 94_GC之Serial收集器
- 95_GC之ParNew收集器
- 96_GC之Parallel收集器
- 97_GC之ParallelOld收集器
- 98_GC之CMS收集器
- 99_GC之SerialOld收集器
- 100_GC之如何选择垃圾收集器
- 101_GC之G1收集器
- 102_GC之G1底层原理
- 103_GC之G1参数配置及和CMS的比较
- 104_JVMGC结合SpringBoot微服务优化简介
- 原文链接
- 其它博客
- 视频资料
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,即存在一个死锁
56_JVMGC下半场技术加强说明和前提知识要求
以后补充
57_JVMGC快速回顾复习串讲
Java gc 主要回收的是 方法区 和 堆 中的内容
类加载器
常考考点:
- 类加载器是什么
- 双亲委派机制
- Java类加载的沙箱安全机制
常见的垃圾回收算法
引用计数
复制算法
复制算法在年轻代
的时候,进行使用,复制时候有交换
优点:没有内存碎片
缺点:消耗内存
标记清除
先标记,后清除。用于老年代
多一些
优点:不会消耗那么多内存
缺点:会产生内存碎片
标记整理
也叫标记清除整理,多用于老年代
优点:不会消耗太多内存;不会有内存碎片
缺点:比较耗时
总结:没有完美的算法,只有合适的算法,一般采用分代收集算法
58_谈谈你对GCRoots的理解
题目1:JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots
什么是垃圾
简单来说就是:内存中已经不再被使用的空间就是垃圾
要进行垃圾回收,如何判断一个对象是否可以被回收?
-
引用计数法
-
枚举根节点做可达性分析(根搜索路径)
如何判断一个对象是否可以被回收
引用计数法
Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法就是通过引用计数来判断一个对象是否可以回收
简单说,给对象中添加一个引用计数器
,每当有一个地方引用它,计数器值加1,每当有一个引用失效,计数器值减1,任何时刻计数器值为零
的对象就是不可能再被使用的,那么这个对象就是可回收对象。
那么为什么主流的Java虚拟机里面都没有选用这个方法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题
。该算法存在但目前无人用了,解决不了循环引用的问题,了解即可。
枚举根节点做可达性分析
根搜索路径算法
:为了解决引用计数法的循环引用个问题,Java使用了可达性分析的方法
所谓 GC Roots 或者说 Tracing Roots的“根集合” 就是一组必须活跃的引用
基本思路:就是通过一系列名为 GC Roots的对象作为起始点
,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,没有被遍历到的对象就被判定为死亡
举例1说明:
必须从GC Roots对象开始,这个类似于linux的 / 也就是根目录:
- 蓝色部分是从GC Roots出发,能够循环可达
- 而白色部分未从GC Roots出发,无法到达
举例2说明:
假设我们现在有三个实体,分别是 人,狗,毛衣
然后他们之间的关系是:人 牵着 狗,狗穿着毛衣,他们之间是强连接的关系
有一天人消失了,只剩下狗狗 和 毛衣,这个时候,把人想象成 GC Roots,因为 人 和 狗之间失去了绳子连接,那么狗可能被回收,也就是被警察抓起来,被送到流浪狗寄养所
假设狗和人有强连接的时候,狗狗就不会被当成是流浪狗
哪些对象可以当做GC Roots
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中的引用对象
- 本地方法栈中的JNI(Native方法)的引用对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
代码说明:
/**
* 在Java中,可以作为GC Roots的对象有:
* - 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中的引用对象
* - 方法区中的类静态属性引用的对象
* - 方法区中常量引用的对象
* - 本地方法栈中的JNI(Native方法)的引用对象
*/
public class GCRootDemo {
// 方法区中的类静态属性引用的对象
// private static GCRootDemo2 t2;
// 方法区中的常量引用,GC Roots 也会以这个为起点,进行遍历
// private static final GCRootDemo3 t3 = new GCRootDemo3(8);
public static void m1() {
// 第一种,虚拟机栈中的引用对象
GCRootDemo t1 = new GCRootDemo();
System.gc();
System.out.println("第一次GC完成");
}
public static void main(String[] args) {
m1();
}
}
59_JVM的标配参数和X参数
官方文档
JVM的参数类型:
-
标配参数
- -version
- -help
- java -showversion
-
X参数(了解)
- -Xint:解释执行
- -Xcomp:第一次使用就编译成本地代码
- -Xmixed:混合模式
- XX参数(下一节)
60_JVM的XX参数之布尔类型
Boolean类型
公式:
-XX:+ 或者-某个属性
+表示开启,-表示关闭
Case
-XX:-PrintGCDetails:表示关闭了GC详情输出
jps:查看java的后台进程
jinfo:查看正在运行的java程序
查看运行的Java程序,JVM参数是否开启,具体值为多少?
首先我们运行一个HelloGC的 Java 程序
/**
* @Author: xj0927
* @Date Created in 2021-01-27 13:29
*/
public class HelloGC {
public static void main(String[] args) throws InterruptedException {
System.out.println("hello GC");
Thread.sleep(Integer.MAX_VALUE);
}
}
然后查看java的后台进程:
# 得到进程号
jps -l
然后查看是否开启PrintGCDetails这个参数:
# + 表示开启,-号 表示没开启
jinfo -flag PrintGCDetails 9936
结果表明:-号表示关闭,即没有开启PrintGCDetails这个参数
最后:需要在启动HelloGC的时候,增加 PrintGCDetails这个参数,需要在运行程序的时候配置JVM参数
在VM Options中加入下面的代码,现在+号表示开启:
-XX:+PrintGCDetails
重启启动程序,使用 jps -l得到进程号,使用jinfo -flag PrintGCDetails 进程号得到配置结果:
-XX:+PrintGCDetails
我们看到原来的-号变成了+号,说明我们通过 VM Options配置的JVM参数已经生效了
也可以使用下列命令,会把jvm的全部默认参数输出
jinfo -flags 进程号
是否打印GC收集细节
-XX:-PrintGCDetails
-XX:+PrintGCDetails
是否使用串行垃圾回收器
-XX:-UseSerialGC
-XX:+UserSerialGC
61_JVM的XX参数之设值类型
KV设值类型
-XX:属性key=值value
Case
-XX:MetaspaceSize=128m 调整元空间大小
-XX:MaxTenuringThreshold=15 调整存活年龄
还是刚刚那个程序,首先我们查看默认的元空间大小:
# 查看进程号
jps -l
# 查看默认空间(此时是键值对类型)
jinfo -flag MetaspaceSize 11080
配置元空间大小:
# 配置元空间大小为1024m
-XX:MetaspaceSize=1024m
重新启动程序,查看:
配置生效!
同样操作:配置存活年龄
-XX:MaxTenuringThreshold=10
jinfo:如何查看当前程序运行的配置
jinfo -flag 某个参数 pid
或者
jinfo -flags pid 显示所有参数
Case1
# 查看堆内存初始化大小
jinfo -flag InitialHeapSize 15276
Case2
# 查看全部配置信息
jinfo -flags 15276
# 结果:
VM Flags:
-XX:CICompilerCount=12 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4278190080 -XX:MaxNewSize=1426063360
-XX:MetaspaceSize=1073741824 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=89128960 -XX:OldSize=179306496 -XX:+
UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndivi
dualAllocation -XX:+UseParallelGC
62_JVM的XX参数之XmsXmx坑题
两个经典参数:-Xms和 -Xmx,这两个参数 如何解释?
这两个参数,还是属于XX参数,因为取了别名
- -Xms 等价于 -XX:InitialHeapSize :初始化堆内存(默认只会用最大物理内存的64分1)
- -Xmx 等价于 -XX:MaxHeapSize :最大堆内存(默认只会用最大物理内存的4分1)
与机器本身配置有关:
我们进行重新配置:
# 设置初始化大小200m,最大3g
-Xms200m -Xmx3g
63_JVM盘点家底查看初始默认值
查看JVM默认参数
-XX:+PrintFlagsInitial
方式一:查看初始默认值
公式:
java -XX:+PrintFlagsInitial -version
java -XX:+PrintFlagsInitial(重要参数)
方式二:查看修改后的,最终的值
(可能是JVM,也可能是人为修改)
公式:
-XX:+PrintFlagsFinal -version
会将JVM的各个结果都进行打印
如果有 := 表示修改过的, = 表示没有修改过的
以初始化堆内存为例:我的机器是16G内存,所以JVM会划内存的1/64给堆初始化,即270m左右
实例2:运行java命令的同时打印出参数
java -XX:+PrintFlagsFinal -Xss128k javaClass
方式三:查看一些常见的参数
-XX:+PrintCommandLineFlags
java -XX:+PrintCommandLineFlags -version
该命令主要是用来查看当前使用的垃圾回收器
64_JVM盘点家底查看修改变更值
PrintFlagsFinal举例,运行java命令的同时打印出参数
java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512m HelloWorld
...
size_t MetaspaceSize := 536870912 {pd product} {default}
...
打印命令行参数
-XX:+PrintCommandLineFlags
C:\Users\abc>java -XX:+PrintCommandLineFlags -version
-XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=266613056 -XX:MarkStackSize=4
194304 -XX:MaxHeapSize=4265808896 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+Seg
mentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
openjdk version "15.0.1" 2020-10-20
OpenJDK Runtime Environment (build 15.0.1+9-18)
OpenJDK 64-Bit Server VM (build 15.0.1+9-18, mixed mode)
65_堆内存初始大小快速复习
题目3:用过的JVM常用基本配置参数有哪些?
知识复习:
JDK 1.8之后将最初的永久代取消了,由元空间取代。
在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。
元空间(Java8)与永久代(Java7)之间最大的区别在于:永久带使用的JVM的堆内存,但是Java8以后的元空间并不在虚拟机中而是使用本机物理内存。
因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
查看堆内存
查看JVM的初始化堆内存 -Xms
和最大堆内存 -Xmx
代码方式查看:
/**
* @Author: xj0927
* @Date Created in 2021-01-27 13:29
*/
public class HelloGC {
public static void main(String[] args) throws InterruptedException {
// 返回Java虚拟机中内存的总量
long totalMemory = Runtime.getRuntime().totalMemory();
// 返回Java虚拟机中试图使用的最大内存量
long maxMemory = Runtime.getRuntime().maxMemory();
System.out.println("TOTAL_MEMORY(-Xms) = " + totalMemory + "(字节)、" + (totalMemory / (double) 1024 / 1024) + "MB");
System.out.println("MAX_MEMORY(-Xmx) = " + maxMemory + "(字节)、" + (maxMemory / (double) 1024 / 1024) + "MB");
}
}
运行结果:
-Xms 初始堆内存为:物理内存的1/64 -Xmx 最大堆内存为:系统物理内存的 1/4
66_常用基础参数栈内存Xss讲解
常用参数
-Xms
-Xms:初始化堆内存,默认为物理内存的1/64,
等价于 -XX:initialHeapSize
-Xmx
-Xmx:最大堆内存,默认为物理内存的1/4,
等价于-XX:MaxHeapSize
-Xss
-Xss:设计单个线程栈的大小,一般默认为512K~1024K,
等价于 -XX:ThreadStackSize
使用 jinfo -flag ThreadStackSize 会发现 -XX:ThreadStackSize = 0
原因:
# 官网解释(JDK8)
Linux/x64:1024KB
OS X:1024KB
Oracle Solaris:1024KB
Windows:取决于虚拟内存的大小
67_常用基础参数元空间MetaspaceSize讲解
-Xmn:设置年轻代大小
-XX:MetaspaceSize 设置元空间大小
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制
典型设置案例
-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails-XX:+UseSerialGC
-XX:MetaspaceSize
设置元空间大小。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
元空间与永久代的最大区别在于,永久代在虚拟机中(占用堆内存),而元空间在本地内存中(占用物理内存)。
为了防止在频繁的实例化对象的时候,让元空间出现OOM,因此可以把元空间设置的大一些
# 查看默认配置大小(大约21m)
java -XX:+PrintFlagsFinal -version
# 设置元空间大小
-XX:+MetaspaceSize=1024m
68_常用基础参数PrintGCDetails回收前后对比讲解
-XX:+PrintGCDetails
输出GC收集日志信息
分为:
GC
Full GC
我们使用一段代码,制造出垃圾回收的过程
首先我们设置一下程序的启动配置: 设置初始堆内存为10M,最大堆内存为10M
-Xms10m -Xmx10m -XX:+PrintGCDetails
然后用下列代码,创建一个 非常大空间的byte类型数组
import java.util.concurrent.TimeUnit;
public class PrintGCDetailsDemo {
public static void main(String[] args) throws InterruptedException {
byte[] byteArray = new byte[10 * 1024 * 1024];
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
}
}
运行后,发现会出现下列错误,这就是OOM:java内存溢出,也就是堆空间不足
同时还打印出了GC垃圾回收时候的详情
问题发生的原因:因为们通过 -Xms10m 和 -Xmx10m 只给Java堆栈设置了10M的空间,但是创建了50M的对象,因此就会出现空间不足,而导致出错
同时在垃圾收集的时候,我们看到有两个对象:GC 和 Full GC
GC
GC垃圾回收发生在新生代。
参数图:
# GC信息(GC (Allocation Failure):表示分配失败,那么就需要触发年轻代空间中的内容被回收)
[GC (Allocation Failure) [PSYoungGen: 1580K->504K(2560K)] 1580K->764K(9728K), 0.0167268 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]
Full GC垃圾回收
Full GC大部分发生在养老区
参数图:
# Full GC
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 592K->574K(7168K)] 592K->574K(9728K), [Metaspace: 3158K->3158K(1056768K)], 0.0040996 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
规律:
[名称: GC前内存占用 -> GC后内存占用 (该区内存总大小)]
当我们出现了老年代都扛不住的时候,就会出现OOM异常
综合案例
VM综合配置:
-Xss1024k -Xms200m -Xmx4g -XX:MetaspaceSize=1024m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC
- -Xss1024k 栈大小
- -Xms200m 堆初始化大小
- -Xmx4g 堆最大值
- -XX:MetaspaceSize=1024m 元空间大小
- -XX:+PrintCommandLineFlags 打印常用配置
- -XX:+PrintGCDetails 打印日志信息
- -XX:+UseSerialGC 使用垃圾回收器的名称(串行回收器)
69_常用基础参数SurvivorRatio讲解
-XX:SurvivorRatio
调节新生代中eden 和 S0、S1的空间比例,
默认为 -XX:SuriviorRatio=8,Eden:S0:S1 = 8:1:1
我们设置如下参数:
# 分配堆内存为10m(此时新生代大约占2~3m)
-Xms10m -Xmx10m -XX:+PrintGCDetails
假如设置成 -XX:SurvivorRatio=4,则为 Eden:S0:S1 = 4:1:1
SurvivorRatio值就是设置eden区的比例占多少?S0和S1相同
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:SurvivorRatio=4
70_常用基础参数NewRatio讲解
-XX:NewRatio(了解)
配置年轻代new 和老年代old 在堆结构的占比
默认: -XX:NewRatio=2 新生代占1,老年代2,年轻代占整个堆的1/3
-XX:NewRatio=4:新生代占1,老年代占4,年轻代占整个堆的1/5,NewRadio值就是设置老年代的占比,剩下的1个新生代
新生代特别小,会造成频繁的进行GC收集
71_常用基础参数MaxTenuringThreshold讲解
-XX:MaxTenuringThreshold
设置垃圾最大年龄
SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区,部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认为15),最终如果还是存活,就存入老年代。
这里就是调整这个次数的,默认是15,并且设置的值 在 0~15之间
# 查看默认进入老年代年龄:
jinfo -flag MaxTenuringThreshold 进程号
# 设置年龄:
-XX:MaxTenuringThreshold=15
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻对象不经过Survivor区,直接进入老年代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大的值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。
72_强引用Reference
在原来的时候,我们谈到一个类的实例化
Person p = new Person()
在等号的左边,就是一个对象的引用,存储在栈中
而等号右边,就是实例化的对象,存储在堆中
其实这样的一个引用关系,就被称为强引用
整体架构
强引用
当内存不足的时候,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,打死也不回收~!
强引用是我们最常见的普通对象引用,只要还有一个强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。
在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收,因此强引用是造成Java内存泄漏的主要原因之一。
对于一个普通的对象,如果没有其它的引用关系,只要超过了引用的作用于或者显示地将相应(强)引用赋值为null,一般可以认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾回收策略)
案例:
public class StrongReferenceDemo {
public static void main(String[] args) {
// 这样定义的默认就是强应用
Object obj1 = new Object();
// 使用第二个引用,指向刚刚创建的Object对象
Object obj2 = obj1;
// 置空
obj1 = null;
// 垃圾回收
System.gc();
System.out.println(obj1);
System.out.println(obj2);
}
}
输出结果我们能够发现,即使 obj1 被设置成了null,然后调用gc进行回收,但是也没有回收实例出来的对象,obj2还是能够指向该地址,也就是说垃圾回收器,并没有将该对象进行垃圾回收
null
java.lang.Object@14ae5a5
73_软引用SoftReference
软引用是一种相对弱化了一些的引用,需要用Java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集,对于只有软引用的对象来讲:
- 当系统内存充足时,它不会被回收
- 当系统内存不足时,它会被回收
软引用通常在对内存敏感的程序中,比如高速缓存就用到了软引用,内存够用 的时候就保留,不够用就回收
代码演示:
public class SoftReferenceDemo {
/**
* 内存够用的时候
*/
public static void softRefMemoryEnough() {
// 创建一个强应用
Object o1 = new Object();
// 创建一个软引用
SoftReference<Object> softReference = new SoftReference<>(o1);
System.out.println(o1);
System.out.println(softReference.get());
o1 = null;
// 手动GC
System.gc();
System.out.println(o1);
System.out.println(softReference.get());
}
/**
* JVM配置,故意产生大对象并配置小的内存,让它的内存不够用了导致OOM,看软引用的回收情况
* -Xms5m -Xmx5m -XX:+PrintGCDetails
*/
public static void softRefMemoryNoEnough() {
System.out.println("========================");
// 创建一个强应用
Object o1 = new Object();
// 创建一个软引用
SoftReference<Object> softReference = new SoftReference<>(o1);
System.out.println(o1);
System.out.println(softReference.get());
o1 = null;
// 模拟OOM自动GC
try {
// 创建30M的大对象
byte[] bytes = new byte[30 * 1024 * 1024];
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(o1);
System.out.println(softReference.get());
}
}
public static void main(String[] args) {
softRefMemoryEnough();
softRefMemoryNoEnough();
}
}
情况一:
我们写了两个方法,一个是内存够用的时候,一个是内存不够用的时候
我们首先查看内存够用的时候,首先输出的是 o1 和 软引用的 softReference,我们都能够看到值
然后我们把o1设置为null,执行手动GC后,我们发现softReference的值还存在,说明内存充足的时候,软引用的对象不会被回收
java.lang.Object@14ae5a5
java.lang.Object@14ae5a5
[GC (System.gc()) [PSYoungGen: 1396K->504K(1536K)] 1504K->732K(5632K), 0.0007842 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 228K->651K(4096K)] 732K->651K(5632K), [Metaspace: 3480K->3480K(1056768K)], 0.0058450 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
null
java.lang.Object@14ae5a5
情况二:
下面我们看当内存不够的时候,我们使用了JVM启动参数配置,给初始化堆内存为5M
-Xms5m -Xmx5m -XX:+PrintGCDetails
但是在创建对象的时候,我们创建了一个30M的大对象
// 创建30M的大对象
byte[] bytes = new byte[30 * 1024 * 1024];
这就必然会触发垃圾回收机制,这也是中间出现的垃圾回收过程,最后看结果我们发现,o1 和 softReference都被回收了,因此说明,软引用在内存不足的时候,会自动回收
java.lang.Object@7f31245a
java.lang.Object@7f31245a
[GC (Allocation Failure) [PSYoungGen: 31K->160K(1536K)] 682K->811K(5632K), 0.0003603 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 160K->96K(1536K)] 811K->747K(5632K), 0.0006385 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 96K->0K(1536K)] [ParOldGen: 651K->646K(4096K)] 747K->646K(5632K), [Metaspace: 3488K->3488K(1056768K)], 0.0067976 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 646K->646K(5632K), 0.0004024 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 646K->627K(4096K)] 646K->627K(5632K), [Metaspace: 3488K->3488K(1056768K)], 0.0065506 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
null
null
74_弱引用WeakReference
不管内存是否够,只要有GC操作就会进行回收
弱引用需要用 java.lang.ref.WeakReference 类来实现,它比软引用生存期更短
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的空间。
/**
* 弱引用
*/
public class WeakReferenceDemo {
public static void main(String[] args) {
Object o1 = new Object();
WeakReference<Object> weakReference = new WeakReference<>(o1);
System.out.println(o1);
System.out.println(weakReference.get());
o1 = null;
System.gc();
System.out.println(o1);
System.out.println(weakReference.get());
}
}
我们看结果,能够发现,我们并没有制造出OOM内存溢出,而只是调用了一下GC操作,垃圾回收就把它给收集了
java.lang.Object@14ae5a5
java.lang.Object@14ae5a5
[GC (System.gc()) [PSYoungGen: 5246K->808K(76288K)] 5246K->816K(251392K), 0.0008236 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 808K->0K(76288K)] [ParOldGen: 8K->675K(175104K)] 816K->675K(251392K), [Metaspace: 3494K->3494K(1056768K)], 0.0035953 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
null
null
75_软引用和弱引用的适用场景
场景:假如有一个应用需要读取大量的本地图片
- 如果每次读取图片都从硬盘读取则会严重影响性能
- 如果一次性全部加载到内存中,又可能造成内存溢出 说明:下面方式使用弱引用也可以完成!
此时使用软引用可以解决这个问题
设计思路: 使用HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占的空间,从而有效地避免了OOM的问题
Map<String, SoftReference<String>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
76_WeakHashMap案例演示和解析
WeakHashMap
比如一些常常和底层打交道的,mybatis等,底层都应用到了WeakHashMap
WeakHashMap和HashMap类似,只不过它的Key是使用了弱引用的,也就是说,当执行GC的时候,HashMap中的key会进行回收,
下面我们使用例子来测试一下
我们使用了两个方法,一个是普通的HashMap方法
我们输入一个Key-Value键值对,然后让它的key置空,然后在查看结果
private static void myHashMap() {
Map<Integer, String> map = new HashMap<>();
Integer key = new Integer(1);
String value = "HashMap";
map.put(key, value);
System.out.println(map);
key = null;
System.gc();
System.out.println(map);
}
第二个是使用了WeakHashMap,完整代码如下
public class WeakHashMapDemo {
public static void main(String[] args) {
myHashMap();
System.out.println("==========");
myWeakHashMap();
}
private static void myHashMap() {
Map<Integer, String> map = new HashMap<>();
Integer key = new Integer(1);
String value = "HashMap";
map.put(key, value);
System.out.println(map);
key = null;
System.gc();
System.out.println(map);
}
private static void myWeakHashMap() {
Map<Integer, String> map = new WeakHashMap<>();
Integer key = new Integer(1);
String value = "WeakHashMap";
map.put(key, value);
System.out.println(map);
key = null;
System.gc();
System.out.println(map);
}
}
最后输出结果为:
{1=HashMap}
{1=HashMap}
==========
{1=WeakHashMap}
{}
从这里我们看到,对于普通的HashMap来说,key置空并不会影响,HashMap的键值对,因为这个属于强引用,不会被垃圾回收。
但是WeakHashMap,在进行GC操作后,弱引用的就会被回收
77_虚引用简介
虚引用需要java.lang.ref.PhantomReference
类来实现。
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue
)联合使用。
虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。
PhantomReference的gei方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比fihalization机制更灵活的回收操作。
换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
这个就相当于Spring AOP里面的后置通知
78_ReferenceQueue引用队列介
场景
一般用于在回收时候做通知相关操作
软引用,弱引用,虚引用在回收之前,需要在引用队列保存一下
我们在初始化的弱引用或者虚引用的时候,可以传入一个引用队列
Object o1 = new Object();
// 创建引用队列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// 创建一个弱引用
WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);
那么在进行GC回收的时候,弱引用和虚引用的对象都会被回收,但是在回收之前,它会被送至引用队列中
完整代码如下:
/**
* 虚引用
*/
public class PhantomReferenceDemo {
public static void main(String[] args) {
Object o1 = new Object();
// 创建引用队列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// 创建一个弱引用
WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);
// 创建一个弱引用
// PhantomReference<Object> weakReference = new PhantomReference<>(o1, referenceQueue);
System.out.println(o1);
System.out.println(weakReference.get());
// 取队列中的内容
System.out.println(referenceQueue.poll());
o1 = null;
System.gc();
System.out.println("执行GC操作");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(o1);
System.out.println(weakReference.get());
// 取队列中的内容
System.out.println(referenceQueue.poll());
}
}
运行结果
java.lang.Object@14ae5a5
java.lang.Object@14ae5a5
null
执行GC操作
null
null
java.lang.ref.WeakReference@7f3124
从这里我们能看到,在进行垃圾回收后,我们弱引用对象,也被设置成null,但是在队列中还能够导出该引用的实例,这就说明在回收之前,该弱引用的实例被放置引用队列中了,我们可以通过引用队列进行一些后置操作
79_虚引用PhantomReference
Java提供了4种引用类型,在垃圾回收的时候,都有自己各自的特点。
ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样可以运行。
创建引用的时候可以指定关联的队列,当Gc释放对象内存的时候,会将引用加入到引用队列,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动这相当于是一种通知机制。
当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVW允许我们在对象被销毁后,做一些我们自己想做的事情。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class PhantomReferenceDemo {
public static void main(String[] args) throws InterruptedException {
Object o1 = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(o1, referenceQueue);
System.out.println(o1);
System.out.println(phantomReference.get());
System.out.println(referenceQueue.poll());
System.out.println("==================");
o1 = null;
System.gc();
Thread.sleep(500) ;
System.out.println(o1);
System.out.println(phantomReference.get());
System.out.println(referenceQueue.poll());
}
}
输出结果:
java.lang.Object@15db9742
java.lang.Object@15db9742
null
==================
执行GC操作
null
null
java.lang.ref.WeakReference@6d06d69c
80_GCRoots和四大引用小总结
- 红色部分在垃圾回收之外,也就是强引用的
- 蓝色部分:属于软引用,在内存不够的时候,才回收
- 虚引用和弱引用:每次垃圾回收的时候,都会被干掉,但是它在干掉之前还会存在引用队列中,我们可以通过引用队列进行一些通知机制
81_SOFE之StackOverflowError
JVM中常见的两种错误
StackoverFlowError :栈溢出
OutofMemoryError: java heap space:堆溢出
除此之外,还有以下的错误
-
StackoverFlowError
- java.lang.StackOverflowError
-
OutofMemoryError
- java.lang.OutOfMemoryError:java heap space
- java.lang.OutOfMemoryError:GC overhead limit exceeeded
- java.lang.OutOfMemoryError:Direct buffer memory
- java.lang.OutOfMemoryError:unable to create new native thread
- java.lang.OutOfMemoryError:Metaspace
架构:
OutOfMemoryError和StackOverflowError是属于Error,不是Exception
StackoverFlowError
堆栈溢出,我们有最简单的一个递归调用,就会造成堆栈溢出,也就是深度的方法调用
栈一般是512K,不断的深度调用,直到栈被撑破
public class StackOverflowErrorDemo {
public static void main(String[] args) {
stackOverflowError();
}
/**
* 栈一般是512K,不断的深度调用,直到栈被撑破
* Exception in thread "main" java.lang.StackOverflowError
*/
private static void stackOverflowError() {
stackOverflowError();
}
}
运行结果
Exception in thread "main" java.lang.StackOverflowError
at com.moxi.interview.study.oom.StackOverflowErrorDemo.stackOverflowError(StackOverflowErrorDemo.java:17)
82_OOM之Java heap space
java heap space
创建了很多对象,导致堆空间不够存储
public class OOMEJavaHeapSpaceDemo {
/**
*
* -Xms10m -Xmx10m
*
* @param args
*/
public static void main(String[] args) {
byte[] array = new byte[80 * 1024 * 1024];
}
}
我们创建一个80M的数组,会直接出现Java heap space
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
83_OOM之GC overhead limit exceeded
GC回收时间过长时会抛出OutOfMemoryError,过长的定义是,超过了98%的时间用来做GC,并且回收了不到2%的堆内存
连续多次GC都只回收了不到2%的极端情况下,才会抛出。假设不抛出GC overhead limit 错误会造成什么情况呢?
那就是GC清理的这点内存很快会再次被填满,迫使GC再次执行,这样就形成了恶性循环,CPU的使用率一直都是100%,而GC却没有任何成果。
代码演示:
为了更快的达到效果,我们首先需要设置JVM启动参数
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
这个异常出现的步骤就是,我们不断的向list中插入String对象,直到启动GC回收
/**
* GC 回收超时
* JVM参数配置: -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
*/
public class GCOverheadLimitDemo {
public static void main(String[] args) {
int i = 0;
List<String> list = new ArrayList<>();
try {
while(true) {
list.add(String.valueOf(++i).intern());
}
} catch (Exception e) {
System.out.println("***************i:" + i);
e.printStackTrace();
throw e;
} finally {
}
}
}
运行结果
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7106K->7106K(7168K)] 9154K->9154K(9728K), [Metaspace: 3504K->3504K(1056768K)], 0.0311093 secs] [Times: user=0.13 sys=0.00, real=0.03 secs]
[Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7136K->667K(7168K)] 9184K->667K(9728K), [Metaspace: 3540K->3540K(1056768K)], 0.0058093 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 2560K, used 114K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 5% used [0x00000000ffd00000,0x00000000ffd1c878,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 667K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 9% used [0x00000000ff600000,0x00000000ff6a6ff8,0x00000000ffd00000)
Metaspace used 3605K, capacity 4540K, committed 4864K, reserved 1056768K
class space used 399K, capacity 428K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)
at com.moxi.interview.study.oom.GCOverheadLimitDemo.main(GCOverheadLimitDemo.java:18)
我们能够看到 多次Full GC,并没有清理出空间,在多次执行GC操作后,就抛出异常 GC overhead limit
84_OOM之Direct buffer memory
Direct buffer memory
Netty + NIO:
这是由于NIO引起的
写NIO程序的时候经常会使用ByteBuffer来读取或写入数据,这是一种基于通道(Channel) 与
缓冲区(Buffer)的I/O方式,它可以使用Native
函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。
这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
- ByteBuffer.allocate(capability):第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢
- ByteBuffer.allocteDirect(capability):第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存的拷贝,所以速度相对较快
但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那么程序就奔溃了。
一句话说:本地内存不足,但是堆内存充足的时候,就会出现这个问题
我们使用 -XX:MaxDirectMemorySize=5m 配置能使用的堆外物理内存为5M
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
然后我们申请一个6M的空间
// 只设置了5M的物理内存使用,但是却分配 6M的空间
ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);
这个时候,运行就会出现问题了
配置的maxDirectMemory:5.0MB
[GC (System.gc()) [PSYoungGen: 2030K->488K(2560K)] 2030K->796K(9728K), 0.0008326 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 488K->0K(2560K)] [ParOldGen: 308K->712K(7168K)] 796K->712K(9728K), [Metaspace: 3512K->3512K(1056768K)], 0.0052052 secs] [Times: user=0.09 sys=0.00, real=0.00 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:693)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.moxi.interview.study.oom.DIrectBufferMemoryDemo.main(DIrectBufferMemoryDemo.java:19)
程序:
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
public class OOMEDirectBufferMemoryDemo {
/**
* -Xms5m -Xmx5m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
*
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
System.out.println(String.format("配置的maxDirectMemory: %.2f MB",//
sun.misc.VM.maxDirectMemory() / 1024.0 / 1024));
TimeUnit.SECONDS.sleep(3);
ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);
}
}
[GC (Allocation Failure) [PSYoungGen: 1024K->504K(1536K)] 1024K->772K(5632K), 0.0014568 secs] [Times: user=0.09 sys=0.00, real=0.00 secs]
配置的maxDirectMemory: 5.00 MB
[GC (System.gc()) [PSYoungGen: 622K->504K(1536K)] 890K->820K(5632K), 0.0009753 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 316K->725K(4096K)] 820K->725K(5632K), [Metaspace: 3477K->3477K(1056768K)], 0.0072268 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Exception in thread "main" Heap
PSYoungGen total 1536K, used 40K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
eden space 1024K, 4% used [0x00000000ffe00000,0x00000000ffe0a3e0,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 4096K, used 725K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
object space 4096K, 17% used [0x00000000ffa00000,0x00000000ffab5660,0x00000000ffe00000)
Metaspace used 3508K, capacity 4566K, committed 4864K, reserved 1056768K
class space used 391K, capacity 394K, committed 512K, reserved 1048576K
java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.lun.jvm.OOMEDirectBufferMemoryDemo.main(OOMEDirectBufferMemoryDemo.java:20)
85_OOM之unable to create new native thread故障演示
不能够创建更多的新的线程了,也就是说
创建线程的上限达到了
在高并发场景的时候,会应用到
高并发请求服务器时,经常会出现如下异常java.lang.OutOfMemoryError:unable to create new native thread
,准确说该native thread异常与对应的平台有关
导致原因:
- 应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
- 服务器并不允许你的应用程序创建这么多线程,linux系统默认运行单个进程可以创建的线程为1024个,如果应用创建超过这个数量,就会报
java.lang.OutOfMemoryError:unable to create new native thread
解决方法:
- 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
- 对于有的应用,确实需要创建很多线程,远超过linux系统默认1024个线程限制,可以通过修改linux服务器配置,扩大linux默认限制
/**
* 无法创建更多的线程
*/
public class UnableCreateNewThreadDemo {
public static void main(String[] args) {
for (int i = 0; ; i++) {
System.out.println("************** i = " + i);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
这个时候,就会出现下列的错误,线程数大概在 900多个
Exception in thread "main" java.lang.OutOfMemoryError: unable to cerate new native thread
86_OOM之unable to create new native thread上限调整
非root用户登录Linux系统(CentOS)测试
服务器级别调参调优
如何查看线程数
ulimit -u
修改配置方法:
87_OOM之Metaspace
Metaspace
元空间内存不足,Matespace元空间应用的是本地内存
-XX:MetaspaceSize 的初始化化大小为20M
元空间是什么
元空间就是我们的方法区,存放的是类模板,类信息,常量池等
Metaspace是方法区HotSpot中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟内存中,而是使用本地内存,也即在java8中,class metadata(the virtual machines internal presentation of Java class),被存储在叫做Matespace的native memory
永久代(java8后背元空间Metaspace取代了)存放了以下信息:
- 虚拟机加载的类信息
- 常量池
- 静态变量
- 即时编译后的代码
模拟Metaspace空间溢出,我们不断生成类 往元空间里灌输,类占据的空间总会超过Metaspace指定的空间大小
在模拟异常生成时候,因为初始化的元空间为20M,因此我们使用JVM参数调整元空间的大小,为了更好的效果
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
代码
首先添加CGLib依赖
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.10</version>
</dependency>
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class OOMEMetaspaceDemo {
// 静态类
static class OOMObject {}
/**
* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*
* @param args
*/
public static void main(final String[] args) {
// 模拟计数多少次以后发生异常
int i =0;
try {
while (true) {
i++;
// 使用Spring的动态字节码技术
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, args);
}
});
enhancer.create();
}
} catch (Throwable e) {
System.out.println("发生异常的次数:" + i);
e.printStackTrace();
} finally {
}
}
}
输出结果
发生异常的次数:569
java.lang.OutOfMemoryError: Metaspace
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
at com.lun.jvm.OOMEMetaspaceDemo.main(OOMEMetaspaceDemo.java:37)
88_垃圾收集器回收种类
问题5:GC垃圾回收算法和垃圾收集器关系?分别是什么请你谈谈?
- 天上飞的理念,要有落地的实现(垃圾收集器就是GC垃圾回收算法的实现)
- GC算法是内存回收的方法论,垃圾收集器就是算法的落地实现
GC算法主要有以下几种:
- 引用计数(几乎不用,无法解决循环引用的问题)
- 复制拷贝(用于新生代)
- 标记清除(用于老年代)
- 标记整理(用于老年代)
- 因为目前为止还没有完美的收集器出现,更没有万能的收集器,只是针对具体应用最合适的收集器,进行;
四种主要的垃圾收集器:
- Serial:串行回收 -XX:+UseSeriallGC
- Parallel:并行回收 -XX:+UseParallelGC
- CMS:并发标记清除
- G1
- ZGC:(java 11 出现的)
89_串行并行并发G1四大垃圾回收方式
Serial
串行垃圾回收器,它为单线程环境设计且值使用一个线程进行垃圾收集,会暂停所有的用户线程,只有当垃圾回收完成时,才会重新唤醒主线程继续执行。所以不适合服务器环境
Parallel
并行垃圾收集器,多个垃圾收集线程并行工作,此时用户线程也是阻塞的,适用于科学计算 / 大数据处理等弱交互场景,也就是说Serial 和
Parallel其实是类似的,不过是多了几个线程进行垃圾收集,但是主线程都会被暂停,但是并行垃圾收集器处理时间,肯定比串行的垃圾收集器要更短
CMS
并发标记清除,用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程,互联网公司都在使用,适用于响应时间有要求的场景。
并发是可以有交互的,也就是说可以一边进行收集,一边执行应用程序。
串行/并行/并发 垃圾收集器总结
注意:并行垃圾回收在单核CPU下可能会更慢
G1
G1垃圾回收器将堆内存分割成不同区域,然后并发的进行垃圾回收.
Java 7 和 Java 8 开始使用G1,Java 11开始换成了ZGC
90_如何查看默认的垃圾收集器
问题7:怎么查看服务器的默认GC收集器?生产上如何配置GC收集器?谈谈你的GC收集器的理解?
查看默认垃圾收集器
使用下面JVM命令,查看配置的初始参数
java -XX:+PrintCommandLineFlags -version
输出结果
C:\Users\abc>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=266613056 -XX:MaxHeapSize=4265808896 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)
移动到最后一句,就能看到 -XX:+UseParallelGC
说明使用的是并行垃圾回收。
或者
jps -l
得出Java程序号
jinfo -flags (Java程序号)
91_JVM默认的垃圾收集器有哪些
Java中一共有7大垃圾收集器
- UserSerialGC:串行垃圾收集器
- UserParallelGC:并行垃圾收集器
- UseConcMarkSweepGC:(CMS)并发标记清除
- UseParNewGC:年轻代的并行垃圾回收器
- UseParallelOldGC:老年代的并行垃圾回收器
- UseG1GC:G1垃圾收集器
- UserSerialOldGC:串行老年代垃圾收集器(已经被移除)
底层源码:
92_GC之7大垃圾收集器概述
各垃圾收集器的使用范围
新生代使用的:
- Serial Copying: UserSerialGC,串行垃圾回收器
- Parallel Scavenge:UserParallelGC,并行垃圾收集器
- ParNew:UserParNewGC,新生代并行垃圾收集器
老年区使用的:
- Serial Old:UseSerialOldGC,老年代串行垃圾收集器
- Parallel Compacting(Parallel Old):UseParallelOldGC,老年代并行垃圾收集器
- CMS:UseConcMarkSwepp,并行标记清除垃圾收集器
各区都能使用的:
G1:UseG1GC,G1垃圾收集器
垃圾收集器就来具体实现这些GC算法并实现内存回收,不同厂商,不同版本的虚拟机实现差别很大
HotSpot中包含的收集器如下图所示:
93_GC之约定参数说明
- DefNew:Default New Generation
- Tenured:Old
- ParNew:Parallel New Generation
- PSYoungGen:Parallel Scavenge
- ParOldGen:Parallel Old Generation
Java中的Server和Client模式
使用范围:一般使用Server模式,Client模式基本不会使用
操作系统
- 32位的Window操作系统,不论硬件如何都默认使用Client的JVM模式
- 32位的其它操作系统,2G内存同时有2个cpu以上用Server模式,低于该配置还是Client模式
- 64位只有Server模式
94_GC之Serial收集器
串行GC(Serial)
串行GC(Serial)(Serial
Copying):是一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。
串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在垃圾收集过程中可能会产生较长的停顿(Stop-The-World 状态)。 虽然在收集垃圾过程中需要暂停所有其它的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是Java虚拟机运行在Client模式下默认的新生代垃圾收集器
对应JVM参数是:-XX:+UseSerialGC
开启后会使用:Serial(Young区用) + Serial Old(Old区用) 的收集器组合
表示:新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法
public class GCDemo {
public static void main(String[] args) throws InterruptedException {
Random rand = new Random(System.nanoTime());
try {
String str = "Hello, World";
while(true) {
str += str + rand.nextInt(Integer.MAX_VALUE) + rand.nextInt(Integer.MAX_VALUE);
}
}catch (Throwable e) {
e.printStackTrace();
}
}
}
VM参数:(启用UseSerialGC)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
输出结果:
-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC
[GC (Allocation Failure) [DefNew: 2346K->320K(3072K), 0.0012956 secs] 2346K->1030K(9920K), 0.0013536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2888K->0K(3072K), 0.0013692 secs] 3598K->2539K(9920K), 0.0014059 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2065K->0K(3072K), 0.0011613 secs] 4604K->4550K(9920K), 0.0011946 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2056K->0K(3072K), 0.0010394 secs] 6606K->6562K(9920K), 0.0010808 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2011K->2011K(3072K), 0.0000124 secs][Tenured: 6562K->2537K(6848K), 0.0021691 secs] 8574K->2537K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0024399 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2059K->2059K(3072K), 0.0000291 secs][Tenured: 6561K->6561K(6848K), 0.0012330 secs] 8620K->6561K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0012888 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 6561K->6547K(6848K), 0.0017784 secs] 6561K->6547K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0018111 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOfRange(Arrays.java:3664)
at java.lang.String.<init>(String.java:207)
at java.lang.StringBuilder.toString(StringBuilder.java:407)
at com.lun.jvm.GCDemo.main(GCDemo.java:23)
Heap
def new generation total 3072K, used 105K [0x00000000ff600000, 0x00000000ff950000, 0x00000000ff950000)
eden space 2752K, 3% used [0x00000000ff600000, 0x00000000ff61a7c8, 0x00000000ff8b0000)
from space 320K, 0% used [0x00000000ff8b0000, 0x00000000ff8b0000, 0x00000000ff900000)
to space 320K, 0% used [0x00000000ff900000, 0x00000000ff900000, 0x00000000ff950000)
tenured generation total 6848K, used 6547K [0x00000000ff950000, 0x0000000100000000, 0x0000000100000000)
the space 6848K, 95% used [0x00000000ff950000, 0x00000000fffb4c30, 0x00000000fffb4e00, 0x0000000100000000)
Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 286K, capacity 386K, committed 512K, reserved 1048576K
- DefNew:Default New Generation
- Tenured:Old
95_GC之ParNew收集器
并行GC(ParNew)
并行收集器,使用多线程进行垃圾回收,在垃圾收集,会 Stop-the-World 暂停其他所有的工作线程直到它收集结束
ParNew收集器其实就是Serial收集器新生代的并行多线程版本,最常见的应用场景时配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多Java虚拟机运行在Server模式下新生代的默认垃圾收集器。
常见对应JVM参数:-XX:+UseParNewGC 启动ParNew收集器,只影响新生代的收集,不影响老年代
开启上述参数后,会使用:ParNew(Young区用) + Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法
但是,ParNew+Tenured这样的搭配,Java8已经不再被推荐
Java HotSpot™64-Bit Server VM warning:
Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release.
备注:-XX:ParallelGCThreads限制线程数量,默认开启和CPU数目相同的线程数。
复用上一节的GCDemo
VM参数:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC
输出结果:
-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC
[GC (Allocation Failure) [ParNew: 2702K->320K(3072K), 0.0007029 secs] 2702K->1272K(9920K), 0.0007396 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 2292K->37K(3072K), 0.0010829 secs] 3244K->2774K(9920K), 0.0011000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 2005K->9K(3072K), 0.0008401 secs] 4742K->5624K(9920K), 0.0008605 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 1974K->1974K(3072K), 0.0000136 secs][Tenured: 5615K->3404K(6848K), 0.0021646 secs] 7589K->3404K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0022520 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 1918K->2K(3072K), 0.0008094 secs] 5322K->5324K(9920K), 0.0008273 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 1970K->1970K(3072K), 0.0000282 secs][Tenured: 5322K->4363K(6848K), 0.0018652 secs] 7292K->4363K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0019205 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 4363K->4348K(6848K), 0.0023131 secs] 4363K->4348K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0023358 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
par new generation total 3072K, used 106K [0x00000000ff600000, 0x00000000ff950000, 0x00000000ff950000)
eden space 2752K, 3% used [0x00000000ff600000, 0x00000000ff61a938, 0x00000000ff8b0000)
from space 320K, 0% used [0x00000000ff8b0000, 0x00000000ff8b0000, 0x00000000ff900000)
to space 320K, 0% used [0x00000000ff900000, 0x00000000ff900000, 0x00000000ff950000)
tenured generation total 6848K, used 4348K [0x00000000ff950000, 0x0000000100000000, 0x0000000100000000)
the space 6848K, 63% used [0x00000000ff950000, 0x00000000ffd8f3a0, 0x00000000ffd8f400, 0x0000000100000000)
Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 286K, capacity 386K, committed 512K, reserved 1048576K
Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release
96_GC之Parallel收集器
并行回收GC(Parallel)/ (Parallel Scavenge)
因为Serial 和 ParNew都不推荐使用了,因此现在新生代默认使用的是Parallel Scavenge,也就是新生代和老年代都是使用并行
Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。一句话:串行收集器在新生代和老年代的并行化
它关注的重点是:
可控制的吞吐量(Thoughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间) ),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99%。高吞吐量意味着高效利用CPU时间,它多用于在后台运算而不需要太多交互的任务。
自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间( -XX:MaxGCPauseMills))或最大的吞吐量。
常用JVM参数:-XX:+UseParallelGC 或 -XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器
开启该参数后:新生代使用复制算法,老年代使用标记-整理算法
多说一句:-XX:ParallelGCThreads=数字N 表示启动多少个GC线程
-
cpu>8 N= 5/8
-
cpu<8 N=实际个数
复用上一节GCDemo
VM参数:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
输出结果:
-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
[GC (Allocation Failure) [PSYoungGen: 2009K->503K(2560K)] 2009K->803K(9728K), 0.7943182 secs] [Times: user=0.00 sys=0.00, real=0.79 secs]
[GC (Allocation Failure) [PSYoungGen: 2272K->432K(2560K)] 2572K->2214K(9728K), 0.0020218 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2448K->352K(2560K)] 4230K->3122K(9728K), 0.0017173 secs] [Times: user=0.11 sys=0.02, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1380K->0K(2560K)] [ParOldGen: 6722K->2502K(7168K)] 8102K->2502K(9728K), [Metaspace: 2657K->2657K(1056768K)], 0.0039763 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 2016K->0K(2560K)] [ParOldGen: 6454K->6454K(7168K)] 8471K->6454K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0049598 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 6454K->6454K(9728K), 0.0008614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 6454K->6440K(7168K)] 6454K->6440K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0055542 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOfRange(Arrays.java:3664)
at java.lang.String.<init>(String.java:207)
at java.lang.StringBuilder.toString(StringBuilder.java:407)
at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
PSYoungGen total 2560K, used 82K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd14810,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 6440K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 89% used [0x00000000ff600000,0x00000000ffc4a1c8,0x00000000ffd00000)
Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 286K, capacity 386K, committed 512K, reserved 1048576K
97_GC之ParallelOld收集器
老年代下的垃圾收集器
并行GC(Parallel Old)/ (Parallel MSC)
Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6才开始提供。
在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配老年代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。在JDK1.6以前(Parallel Scavenge + Serial Old)
Parallel Old正是为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可以考虑新生代Parallel Scavenge和老年代Parallel Old 收集器的搭配策略。在JDK1.8及后(Parallel Scavenge + Parallel Old)
JVM常用参数:-XX +UseParallelOldGC:使用Parallel Old收集器,设置该参数后,新生代Parallel+老年代 Parallel Old
复用上一节GCDemo
VM参数:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC
输出结果:
-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelOldGC
[GC (Allocation Failure) [PSYoungGen: 1979K->480K(2560K)] 1979K->848K(9728K), 0.0007724 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2205K->480K(2560K)] 2574K->2317K(9728K), 0.0008700 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2446K->496K(2560K)] 4284K->3312K(9728K), 0.0010374 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1499K->0K(2560K)] [ParOldGen: 6669K->2451K(7168K)] 8168K->2451K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0043327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1966K->0K(2560K)] [ParOldGen: 6304K->6304K(7168K)] 8270K->6304K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0021269 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 6304K->6304K(9728K), 0.0004841 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 6304K->6290K(7168K)] 6304K->6290K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0058149 secs] [Times: user=0.11 sys=0.00, real=0.01 secs]
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOfRange(Arrays.java:3664)
at java.lang.String.<init>(String.java:207)
at java.lang.StringBuilder.toString(StringBuilder.java:407)
at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
PSYoungGen total 2560K, used 81K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 3% used [0x00000000ffd00000,0x00000000ffd14768,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 6290K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 87% used [0x00000000ff600000,0x00000000ffc24b70,0x00000000ffd00000)
Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 286K, capacity 386K, committed 512K, reserved 1048576K
98_GC之CMS收集器
并发标记清除GC(CMS)
CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以最短回收停顿时间为目标的收集器适合应用在互联网或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。
CMS非常适合堆内存大,CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。
Concurrent Mark Sweep:并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行
开启该收集器的JVM参数: -XX:+UseConcMarkSweepGC 开启该参数后,会自动将 -XX:+UseParNewGC打开,开启该参数后,使用ParNew(young 区用)+ CMS(Old 区用) + Serial Old 的收集器组合,Serial Old将作为CMS出错的后备收集器
实例:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC
四个步骤
- 初始标记(CMS initial mark)
- 只是标记一个GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程
- 并发标记(CMS concurrent mark)和用户线程一起
- 进行GC Roots跟踪过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象
- 重新标记(CMS remark)
- 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程,由于并发标记时,用户线程依然运行,因此在正式清理前,在做修正
- 并发清除(CMS concurrent sweep)和用户线程一起
- 清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象,由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。
- 清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象,由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。
优缺点
优点:
- 并发收集低停顿
缺点:
- 并发执行,对CPU资源压力大,采用的标记清除算法会导致大量碎片
由于并发进行,CMS在收集与应用线程会同时增加对堆内存的占用,也就是说,CMS必须在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW方式进行一次GC,从而造成较大的停顿时间
标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩,CMS也提供了参数 -XX:CMSFullGCSBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC
99_GC之SerialOld收集器
Serial Old是Serial垃圾收集器老年代版本,它同样是一个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默认的Java虚拟机中默认的老年代垃圾收集器
在Server模式下,主要有两个用途(了解,版本已经到8及以后)
- 在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用(Parallel Scavenge + Serial Old)
- 作为老年代版中使用CMS收集器的后备垃圾收集方案。
实例:
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseSerialOldlGC
该垃圾收集器,目前已经不推荐使用了!!!
为什么新生代采用复制算法,老年代采用标整算法
新生代使用复制算法
因为新生代对象的生存时间比较短,80%的都要回收的对象,采用标记-清除算法则内存碎片化比较严重,采用复制算法可以灵活高效,且便与整理空间。
老年代采用标记整理
标记整理算法主要是为了解决标记清除算法存在内存碎片的问题,又解决了复制算法两个Survivor区的问题,因为老年代的空间比较大,不可能采用复制算法,特别占用内存空间
100_GC之如何选择垃圾收集器
组合的选择
- 单CPU或者小内存,单机程序
- -XX:+UseSerialGC
- 多CPU,需要最大的吞吐量,如后台计算型应用
- -XX:+UseParallelGC(这两个相互激活)
- -XX:+UseParallelOldGC
- 多CPU,追求低停顿时间,需要快速响应如互联网应用
- -XX:+UseConcMarkSweepGC
- -XX:+ParNewGC
参数 | 新生代垃圾收集器 | 新生代算法 | 老年代垃圾收集器 | 老年代算法 |
---|---|---|---|---|
-XX:+UseSerialGC | SerialGC | 复制 | SerialOldGC | 标记整理 |
-XX:+UseParNewGC | ParNew | 复制 | SerialOldGC | 标记整理 |
-XX:+UseParallelGC | Parallel [Scavenge] | 复制 | Parallel Old | 标记整理 |
-XX:+UseConcMarkSweepGC | ParNew | 复制 | CMS + Serial Old的收集器组合,Serial Old作为CMS出错的后备收集器 | 标记清除 |
-XX:+UseG1GC | G1整体上采用标记整理算法 | 局部复制 |
101_GC之G1收集器
问题8:G1垃圾收集器
开启G1:
复用上一节GCDemo
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
输出结果:
-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
[GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0015787 secs]
[Parallel Time: 0.8 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 106.4, Avg: 106.5, Max: 106.5, Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.2, Avg: 0.3, Max: 0.5, Diff: 0.4, Sum: 2.2]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.0, Avg: 0.3, Max: 0.3, Diff: 0.3, Sum: 2.1]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4]
[Termination Attempts: Min: 1, Avg: 5.3, Max: 10, Diff: 9, Sum: 42]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.3]
[GC Worker Total (ms): Min: 0.6, Avg: 0.6, Max: 0.7, Diff: 0.1, Sum: 4.9]
[GC Worker End (ms): Min: 107.1, Avg: 107.1, Max: 107.1, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.3 ms]
[Other: 0.5 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.3 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 4096.0K(4096.0K)->0.0B(4096.0K) Survivors: 0.0B->1024.0K Heap: 7073.4K(10.0M)->2724.8K(10.0M)]
[Times: user=0.02 sys=0.02, real=0.00 secs]
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0004957 secs]
[GC concurrent-mark-start]
[GC concurrent-mark-end, 0.0001071 secs]
[GC remark [Finalize Marking, 0.0001876 secs] [GC ref-proc, 0.0002450 secs] [Unloading, 0.0003675 secs], 0.0011690 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[GC cleanup 4725K->4725K(10M), 0.0004907 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[GC pause (G1 Humongous Allocation) (young), 0.0009748 secs]
[Parallel Time: 0.6 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 111.8, Avg: 111.9, Max: 112.2, Diff: 0.5]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.8]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.0, Avg: 0.2, Max: 0.3, Diff: 0.3, Sum: 1.7]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
[Termination Attempts: Min: 1, Avg: 3.3, Max: 5, Diff: 4, Sum: 26]
[GC Worker Other (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.0, Sum: 0.8]
[GC Worker Total (ms): Min: 0.1, Avg: 0.5, Max: 0.6, Diff: 0.5, Sum: 3.6]
[GC Worker End (ms): Min: 112.3, Avg: 112.3, Max: 112.4, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 1024.0K(4096.0K)->0.0B(4096.0K) Survivors: 1024.0K->1024.0K Heap: 6808.1K(10.0M)->2595.2K(10.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0006211 secs]
[Parallel Time: 0.2 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 113.3, Avg: 113.3, Max: 113.4, Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 1.0]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.1, Max: 1, Diff: 1, Sum: 1]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.3]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 0.1, Avg: 0.2, Max: 0.2, Diff: 0.1, Sum: 1.4]
[GC Worker End (ms): Min: 113.5, Avg: 113.5, Max: 113.5, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.3 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 0.0B(4096.0K)->0.0B(2048.0K) Survivors: 1024.0K->1024.0K Heap: 4595.9K(10.0M)->4557.3K(10.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[GC concurrent-root-region-scan-start]
[GC pause (G1 Humongous Allocation) (young)[GC concurrent-root-region-scan-end, 0.0001112 secs]
[GC concurrent-mark-start]
, 0.0006422 secs]
[Root Region Scan Waiting: 0.0 ms]
[Parallel Time: 0.2 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 114.2, Avg: 114.3, Max: 114.4, Diff: 0.2]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.7]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.1, Max: 1, Diff: 1, Sum: 1]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.9]
[GC Worker End (ms): Min: 114.4, Avg: 114.4, Max: 114.4, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.3 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 0.0B(2048.0K)->0.0B(2048.0K) Survivors: 1024.0K->1024.0K Heap: 4557.3K(10.0M)->4547.6K(10.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) 4547K->4527K(10M), 0.0023437 secs]
[Eden: 0.0B(2048.0K)->0.0B(3072.0K) Survivors: 1024.0K->0.0B Heap: 4547.6K(10.0M)->4527.6K(10.0M)], [Metaspace: 2658K->2658K(1056768K)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) 4527K->4513K(10M), 0.0021281 secs]
[Eden: 0.0B(3072.0K)->0.0B(3072.0K) Survivors: 0.0B->0.0B Heap: 4527.6K(10.0M)->4514.0K(10.0M)], [Metaspace: 2658K->2658K(1056768K)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[GC concurrent-mark-abort]
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
garbage-first heap total 10240K, used 4513K [0x00000000ff600000, 0x00000000ff700050, 0x0000000100000000)
region size 1024K, 1 young (1024K), 0 survivors (0K)
Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 286K, capacity 386K, committed 512K, reserved 1048576K
以前收集器的特点:
- 年轻代和老年代是各自独立且连续的内存块
- 年轻代收集使用单eden + S0 + S1 进行复制算法
- 老年代收集必须扫描珍整个老年代区域
- 都是以尽可能少而快速地执行GC为设计原则
G1是什么
G1:Garbage-First
收集器,是一款面向服务端应用的收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能满足垃圾收集暂停时间的要求。
另外,它还具有一下特征:
- 像CMS收集器一样,能与应用程序并发执行
- 整理空闲空间更快
- 需要更多的时间来预测GC停顿时间
- 不希望牺牲大量的吞吐量性能
- 不需要更大的Java Heap
G1收集器设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色
- G1是一个有整理内存过程的垃圾收集器,
不会产生很多内存碎片
。 - G1的Stop The World(STW)更可控,
G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。
CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器-G1垃圾收集器
G1是在2012才在JDK1.7中可用,Oracle官方计划在JDK9中将G1变成默认的垃圾收集器以替代CMS,它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换Java8以前的CMS收集器
主要改变时:Eden,Survivor 和 Tenured 等内存区域不再是连续了,而是变成一个个大小一样的region,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。
特点
- G1能充分利用多CPU,多核环境硬件优势,尽量缩短STW
- G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
- 宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘
- G1收集器里面将整个内存区域都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但他们不再是物理隔离的,而是通过一部分Region的集合且不需要Region是连续的,也就是说依然会采取不同的GC方式来处理不同的区域
- G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代和老年代的区别,也不需要完全独立的Survivor(to space)堆做复制准备,
G1只有逻辑上的分代概念
,或者说每个分区都可能随G1的运行在不同代之间前后切换。
102_GC之G1底层原理
Region区域化垃圾收集器,化整为零,打破了原来新生区和老年区的壁垒,
避免了全内存扫描,只需要按照区域来进行扫描即可。
区域化内存划片Region,整体遍为了一些列不连续的内存区域,避免了全内存区的GC操作。
核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置子区域大小
在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n
可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为:32MB*2048 = 64G内存
Region区域化垃圾收集器
Region区域化垃圾收集器
G1将新生代、老年代的物理空间划分取消了
同时对内存进行了区域划分
G1算法将堆划分为若干个区域(Reign),它仍然属于分代收集器,这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间
这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片的问题存在了。
在G1中,还有一种特殊的区域,叫做Humongous(巨大的)区域,如果一个对象占用了空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象,这些巨型对象默认直接分配在老年代,但是如果他是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响,为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H区来存储,为了能找到连续的H区,有时候不得不启动Full GC。
回收步骤
针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集 + 形成连续的内存块,避免内碎片
- Eden区的数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据会晋升到Old区
- Survivor区的数据移动到新的Survivor区,部分数据晋升到Old区
- 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行
回收完成后
小区域收集 + 形成连续的内存块,最后在收集完成后,就会形成连续的内存空间,这样就解决了内存碎片的问题
四步过程
- 初始标记:只标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing(链路扫描)的过程
- 最终标记:修正并发标记期间,因为程序运行导致标记发生变化的那一部分对象
- 筛选回收:根据时间来进行价值最大化回收
103_GC之G1参数配置及和CMS的比较
参数配置
开发人员仅仅需要申明以下参数即可
三步归纳:-XX:+UseG1GC -Xmx32G -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=n:最大GC停顿时间单位毫秒,这是个软目标,JVM尽可能停顿小于这个时间
常用配置(了解):
- -XX:UseG1GC:使用G1垃圾回收器
- -XX:G1HeapRegionSize=n:划分region大小
- -XX:MaxGCPauseMillis:最大GC停顿时间,是个软目标,尽量达到。
- -XX:InitiatingHeapOccupancyPercent:堆占用了多少执行GC,默认45%
- -XX:ConcGCThreads:并发GC使用的线程数
- -XX:G1ReservePercent:设置为空闲空间的预留内存比
G1和CMS比较
- G1不会产生内碎片
- 是可以精准控制停顿。该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。
104_JVMGC结合SpringBoot微服务优化简介
- IDEA开发微服务工程。
- Maven进行clean package。
- 要求微服务启动的时候,同时配置我们的JVM/GC的调优参数。
- 公式:
java -server jvm的各种参数 -jar 第1步上面的jar/war包名
。
例如:
java -server -Xms1024m -Xmx1024 -XX:UseG1GC -jar xxx.jar
可以使用jps -l
查看端口号,使用jinfo -flags
端口号查看我们配置的参数
原文链接
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