Java经典面试(二)

  • 1、volatile是什么
  • 2、JMM
    • JMM可见性
  • 3、volatile不保证原子性
    • volatile不保证原子性理论解释
    • 解决volatile不保证原子性问题
  • 4、volatile禁止指令重排
    • volatile指令重排案例1
    • volatile指令重排案例2
  • 5、单例模式在多线程环境下可能存在安全问题
    • 单例模式volatile分析
  • 6、CAS
    • CAS底层原理-上
      • CAS到底是什么
    • CAS底层原理-下
    • CAS缺点
  • 7、ABA问题
    • ABA问题是怎么产生的?
    • 原子引用
      • AtomicReference 原子引用
    • 时间戳原子引用
  • 8、集合类不安全之并发修改异常
    • 集合类不安全之写时复制
    • 集合类不安全之Set
    • 集合类不安全之Map
  • 9、TransferValue小练习
  • 10、java锁之公平和非公平锁
    • 公平和非公平锁
    • 可重入锁(又名递归锁)
    • 自旋锁
    • 独占锁(写锁)/共享锁(读锁)/互斥锁
      • 读写锁代码验证:
  • 11、CountDownLatch/CyclicBarrier/Semaphore类
    • CountDownLatch
    • CyclicBarrier
    • Semaphore
  • 12、阻塞队列理论
    • 阻塞队列api之抛出异常组
    • 阻塞队列api之返回布尔值组
    • 阻塞队列api之阻塞
    • 阻塞队列api之超时控制
    • 阻塞队列之同步SynchronousQueue队列
    • 线程通信之生产者消费者传统版
    • Synchronized和Lock有什么区别
    • 锁绑定多个条件Condition
    • 线程通信之生产者消费者阻塞队列版
  • 13、线程池
    • Callable接口
    • 线程池使用及优势
    • 线程池3个常用方式
    • 线程池7大参数入门简介
    • 线程池底层工作原理
    • 线程池的4种拒绝策略理论简介
    • 线程池实际中使用哪一个
    • 线程池的手写改造和拒绝策略
    • 线程池配置合理线程数
  • 14、死锁编码及定位分析
  • 15、JVM + GC
    • 快速回顾复习
    • GC Roots
    • JVM的标配参数和X参数
    • JVM的XX参数之布尔类型
    • JVM的XX参数之设置类型
    • JVM盘点家底查看初始默认值
    • JVM盘点家底查看修改变更值
    • 堆内存初始大小快速复习
    • 常用基础参数栈内存Xss讲解
    • 常用基础参数元空间MetaspaceSize讲解
    • 常用基础参数PrintGCDetails回收前后对比讲解
    • 常用基础参数SurvivorRatio讲解
    • 常用基础参数NewRatio讲解
    • 常用基础参数MaxTenuringThreshold讲解
  • 16、强引用、软引用、弱引用、虚引用
    • 强引用Reference
    • 软引用SoftReference
    • 弱引用WeakReference
    • 软引用和弱引用的适用场景
    • WeakHashMap案例演示和解析
    • 虚引用简介
    • ReferenceQueue引用队列介
    • 虚引用PhantomReference
    • GCRoots和四大引用小总结
  • 17、OOM
    • SOFE之StackOverflowError
    • OOM之Java heap space
    • OOM之GC overhead limit exceeded
    • OOM之Direct buffer memory
    • OOM之unable to create new native thread故障演示
    • OOM之unable to create new native thread上限调整
    • OOM之Metaspace
  • 18、GC垃圾收集器
    • 垃圾收集器回收种类
    • 串行并行并发G1四大垃圾回收方式
  • 19、垃圾收集器
    • 如何查看默认的垃圾收集器
    • JVM默认的垃圾收集器有哪些
    • GC之7大垃圾收集器概述
    • GC之约定参数说明
    • GC之Serial收集器
    • GC之ParNew收集器
    • GC之Parallel收集器
    • GC之ParallelOld收集器
    • GC之CMS收集器
    • GC之SerialOld收集器
    • GC之如何选择垃圾收集器
  • 20、G1垃圾收集器
    • GC之G1收集器
    • GC之G1底层原理
    • GC之G1参数配置及和CMS的比较
    • JVMGC结合SpringBoot微服务优化简介
  • 21、Linux命令
    • Linux命令之top
    • Linux之cpu查看vmstat
    • Linux之cpu查看pidstat
    • Linux之内存查看free和pidstat
    • Linux之硬盘查看df
    • Linux之磁盘IO查看iostat和pidstat
    • Linux之网络IO查看ifstat
  • 22、CPU占用过高的定位分析思路
  • 23、GitHub操作
    • GitHub操作之常用词
    • GitHub操作之in限制搜索
    • GitHub操作之star和fork范围搜索
    • GitHub操作之awesome加强搜索
    • GitHub操作之#L数字
    • GitHub操作之T搜索
    • GitHub操作之搜索区域活跃用户

视频连接

1、volatile是什么

volatile是java虚拟机(JVM)提供的轻量级的同步机制

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排(保证有序性)

2、JMM

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定:
  1、线程解锁前,必须把共享变量的值刷新回主内存
  2、线程加锁前,必须读取主内存的最新值到自己的工作内存
  3、加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

主内存是共享内存可见性的

JMM特性:

  • 可见性
  • 原子性
  • 有序性

VolatileDemo代码演示可见性 + 原子性代码

class MyData{int number = 0;public void addTo60(){this.number = 60;}}/*** 1 验证volatile的可见性*  1.1 假设int number = 0, number变量之前没有添加volatile关键字修饰,没有可见性**/
public class VolatileDemo {public static void main(String[] args) { // main 是一切方法的运行入口MyData myData = new MyData(); // 资源类new Thread(()->{System.out.println(Thread.currentThread().getName() + "\t come in");// 暂停一会线程 3秒try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }myData.addTo60(); // 把 number该成 60System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.number);},"AAA").start();//  AAA 线程名称// 第2个线程就是我们的main线程while(myData.number == 0) {// main线程就一直在这里等待循环,直到number的值不等于零}System.out.println(Thread.currentThread().getName() + "\t mission is over");}
}

控制台输出:

AAA   come in
AAA  update number value: 60

但是这里程序还一直执行,没有执行最后一行 System.out.println(Thread.currentThread().getName() + "\t mission is over");语句,说明这里没有加 volatile关键字修饰,没有可见性

JMM可见性

添加 number 修饰符 volatile

class MyData{volatile int number = 0;public void addTo60(){this.number = 60;}
}
public static void main(String[] args) {...System.out.println(Thread.currentThread().getName() + "\t mission is over, get number value: " + myData.number);
}

再重新执行,控制台输出:

AAA   come in
AAA  update number value: 60
main     mission is over, get number value: 60

可见性:
通过前面对JMM的介绍,我们知道
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。

这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,
这种工作内存与主内存同步延迟现象就造成了可见性问题

3、volatile不保证原子性

volatile是不保证原子性的:代码示例

class MyData{volatile int number = 0;public void addTo60(){this.number = 60;}// 此时number前面是加了volatile关键字修饰的,volatile不保证原子性public void addPlusPlus(){number++;}
}/*** 1 验证volatile的可见性*  1.1 假设int number = 0, number变量之前没有添加volatile关键字修饰,没有可见性*  1.2 添加了volatile,可以解决可见性问题。** 2 验证volatile不保证原子性*  2.1 原子性指的是什么意思?*      不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整*      要么同时成功,要么同时失败。**  2.2 volatile不保证原子性的案例演示**/
public class VolatileDemo {public static void main(String[] args) {// main 是一切方法的运行入口MyData myData = new MyData();for(int i=1; i<=20; i++){new Thread(()->{for (int j = 0; j < 1000; j++) {myData.addPlusPlus();}},String.valueOf(i)).start();//  AAA 线程名称}// 需要等待上面20个线程都全部计算完后,再用main线程获取最终的结果值看是多少?while (Thread.activeCount() > 2){ // 默认后台有两个线程 1 main线程 2 GC线程Thread.yield();//使当前线程由执行状态,变成为就绪状态}System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);}// volatile可以保证可见性,及时通知其它线程,主物理内存的值已被修改。public static void seeOkByVolatile() {MyData myData = new MyData(); // 资源类new Thread(()->{System.out.println(Thread.currentThread().getName() + "\t come in");// 暂停一会线程 3秒try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }myData.addTo60(); // 把 number该成 60System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.number);},"AAA").start();//  AAA 线程名称// 第2个线程就是我们的main线程while(myData.number == 0) {// main线程就一直在这里等待循环,直到number的值不等于零}System.out.println(Thread.currentThread().getName() + "\t mission is over, get number value: " + myData.number);}
}

执行,控制台输出:

main  finally number value: 18758

这里的输出值和自己计算的值不一致

volatile不保证原子性理论解释

number++在多线程下是非线程安全的。
addPlusPlus添加synchronized

 public synchronized void addPlusPlus(){number++;}

再测试:

main  finally number value: 20000

是正确的,添加synchronized保证了原子性,不过这里并不合适

我们可以将代码编译成字节码,可看出number++被编译成3条指令。

解决volatile不保证原子性问题

使用AtomicInteger

class MyData{ // MyData ==> MyData.class ==> JVM字节码volatile int number = 0;public void addTo60(){this.number = 60;}// 此时number前面是加了volatile关键字修饰的,volatile不保证原子性public void addPlusPlus(){number++;}// 原子性整型AtomicInteger atomicInteger = new AtomicInteger();public void addAtomic(){atomicInteger.getAndIncrement();}
}
public class VolatileDemo {public static void main(String[] args) {// main 是一切方法的运行入口MyData myData = new MyData();for(int i=1; i<=20; i++){new Thread(()->{for (int j = 0; j < 1000; j++) {myData.addPlusPlus();myData.addAtomic();}},String.valueOf(i)).start();//  AAA 线程名称}// 需要等待上面20个线程都全部计算完后,再用main线程获取最终的结果值看是多少?while (Thread.activeCount() > 2){ // 默认后台有两个线程 1 main线程 2 GC线程Thread.yield();//使当前线程由执行状态,变成为就绪状态}System.out.println(Thread.currentThread().getName() + "\t int type,finally number value: " + myData.number);System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type,finally number value: " + myData.atomicInteger);}
}

执行:

main  int type,finally number value: 18197
main     AtomicInteger type,finally number value: 20000

4、volatile禁止指令重排

volatile指令重排案例1

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。

处理器在进行重排序时必须要考虑指令之间的数据依赖性

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

重排案例1

public void mySort{int x = 11;//语句1int y = 12;//语句2× = × + 5;//语句3y = x * x;//语句4
}

可重排序列:

1234
2134
1324

问题:请问语句4可以重排后变成第一个条吗?答:不能。
因为:处理器在进行重排序时必须要考虑指令之间的数据依赖性

重排案例2
int a,b,x,y = 0

线程1 线程2
x = a; y = b;
b = 1; a = 2;
x = 0; y = 0;

如果编译器对这段程序代码执行重排优化后,可能出现下列情况:

线程1 线程2
b = 1; a = 2;
x = a; y = b;
x = 2; y = 1

这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。

volatile指令重排案例2

观察以下代码:

public class ReSortSeqDemo{int a = 0;boolean flag = false;public void method01(){a = 1;//语句1flag = true;//语句2}public void method02(){if(flag){a = a + 5; //语句3}System.out.println("retValue: " + a);}
}

如果在,多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。 int a的值可能是6或1或5或0

禁止指令重排小总结
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象

先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
 一、保证特定操作的执行顺序,
 二、保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本

怎么禁:在关键的变量前加volatile关键字修饰

线性安全性获得保证
工作内存与主内存同步延迟现象导致的可见性问题
可以使用synchronizedvolatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

对于指令重排导致的可见性问题和有序性问题
可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。

5、单例模式在多线程环境下可能存在安全问题

懒汉单例模式:

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) {// 这里的 == 是比较内存地址 判断 new SingletonDemo()是否为同一个对象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

上面使用的是单线程(main线程的操作动作)

如果是并发多线程的情况下,又会怎么样?

public static void main(String[] args) {// 并发多线程后,情况发生了很大的变化for (int i = 1; i <= 10; i++) {new Thread(() ->{SingletonDemo.getInstance();},String.valueOf(i)).start();}
}

执行,输出:

3     我是构造方法SingletonDemo
6    我是构造方法SingletonDemo
1    我是构造方法SingletonDemo
5    我是构造方法SingletonDemo
2    我是构造方法SingletonDemo
4    我是构造方法SingletonDemo

说明这里的懒汉单例模式在多线程的情况下是不安全的

或者使用synchronized关键字修饰getInstance()可以保证多线程的情况下是安全的,但是在高并发的情况下并不适合加synchronized,因为每次获取单例对象时都会加锁,这样就会带来性能损失

单例模式volatile分析

使用:
DCL(Double Check Lock 双端检锁机制)

public class SingletonDemo {private SingletonDemo () {// 如果这里是单例模式 ,应该打印一次System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");}private static SingletonDemo instance = null;// DCL(Double Check Lock 双端检锁机制)public static  SingletonDemo getInstance() {if(instance == null) {synchronized (SingletonDemo.class){if(instance == null){instance = new SingletonDemo();}}}return instance;}public static void main(String[] args) {// 并发多线程后,情况发生了很大的变化for (int i = 1; i <= 10; i++) {new Thread(() ->{SingletonDemo.getInstance();},String.valueOf(i)).start();}}
}

执行:

1     我是构造方法SingletonDemo

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实例未必已初始化完成,也就是造成了线程安全问题。

添加volatile禁止指令重排:

public class SingletonDemo {private static volatile SingletonDemo instance = null;....
}

6、CAS

CAS是什么?
CompareAndSet(比较并交换)

/*** 1 CAS是什么? ===>compareAndSet*    比较并交换**/
public class CASDemo {public static void main(String[] args) {AtomicInteger atomicInteger = new AtomicInteger(5);//new AtomicInteger() 括号内的值默认为0// compareAndSet(5,2021) 5:是主内存存的值5(期望值) 2021:是修改的值  就是说 期望主内存存的值是5 就更新为2021System.out.println(atomicInteger.compareAndSet(5, 2021)+"\t current data: "+atomicInteger.get());System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t current data: "+atomicInteger.get());}
}

执行:

true  current data: 2021
false    current data: 2021

如果线程的期望值跟物理内存的真实值一样,就修改为更新值,返回ture,修改成功
如果线程的期望值跟物理内存的真实值不一样,直接返回false,修改失败

CAS底层原理-上

查看AtomicInteger底层源码:

1、什么是 Unsafe
Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sum.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为JavaCAS操作的执行依赖于Unsafe类方法。

注意Unsafe类中的所有方法都是native修饰,也就是说Unsafe类中的方法都是直接调用操作系统底层执行相对应任务

2、 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

3、变量valuevolatile修饰,保证了多线程之间的内存可见性。

CAS到底是什么

CAS的全称为Compare-And-Swap,它是一条CPU并发原语。

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

CAS底层原理-下

UnSafe.getAndAddInt()源码解释:

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;
}

var1 AtomicInteger对象本身。
var2 该对象值得引用地址。
var4 需要变动的数量。
var5 是用过var1,var2找出的主内存中真实的值。
用该对象当前的值与var5 [this.compareAndSwapInt(var1, var2, var5, var5 + var4)] 比较:
 如果相同,更新var5+var4并且返回true,
 如果不同,继续取值然后再比较,直到更新完成。

假设线程A和线程B两个线程同时执行getAndAddInt() 操作(分别跑在不同CPU上) :
  1、Atomiclnteger里面的value原始值为3,即主内存中Atomiclntegervalue3,根据JMM模型,线程A和线程B各自持有一份值为3value的副本分别到各自的工作内存。

  2、线程A通过getIntVolatile(var1, var2) 拿到value3,这时线程A被挂起。

  3、线程B也通过getintVolatile(var1, var2) 方法获取到value3,此时刚好线程B没有被挂起并执行compareAndSwapInt() 方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK

  4、这时线程A恢复,执行compareAndSwapInt() 方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。

  5、线程A重新获取value值,因为变量valuevolatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt() 进行比较替换,直到成功。

底层汇编
Unsafe类中的compareAndSwapInt(),是一个本地方法,该方法的实现位于unsafe.cpp

小结
CAS(CompareAndSwap)
比较当前工作内存中的值和主内存的值,如果相同则执行规定操作,
否则继续比较到主内存和工作内存的值一直为止。

CAS应用
CAS3个操作数,内存值V,旧的预期值A,要修改的更新值B
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

CAS缺点

  • 循环时间长开销很大

     使用do while,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

  • 只能保证一个共享变量的原子操作
     当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

  • 引出来ABA问题??

7、ABA问题

ABA问题是怎么产生的?

CAS会导致“ABA问题”。

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。

尽管线程oneCAS操作成功,但是不代表这个过程就是没有问题的。

原子引用

AtomicReference 原子引用

使用AtomicReference<T>(包装)代码引用:

@Data
@AllArgsConstructor
class User{String name;int age;}public class AtomicIntegerDemo {public static void main(String[] args) {User user1 = new User("user1",21);User user2 = new User("user2",22);// 原子泛型引用 -- UserAtomicReference<User> atomicReference = new AtomicReference<>();// 主物理内存共享 user1atomicReference.set(user1);// 如果期望值是user1 则替换为user2System.out.println(atomicReference.compareAndSet(user1,user2) + "\t " + atomicReference.get().toString());System.out.println(atomicReference.compareAndSet(user1,user2) + "\t " + atomicReference.get().toString());}
}

执行:

true  User(name=user2, age=22)
false    User(name=user2, age=22)

时间戳原子引用

AtomicStampedReference版本号原子引用
原子引用 + 新增一种机制,那就是修改版本号(类似时间戳),它用来解决ABA问题。
使用AtomicStampedReference<V>包装类

public class ABADemo { // ABA问题的解决  AtomicStampedReferencestatic 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(() ->{atomicReference.compareAndSet(100,101);atomicReference.compareAndSet(101,100);},"t1").start();new Thread(() ->{// 暂停1秒线程t2 保存上面线程完成一次ABA操作try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(atomicReference.compareAndSet(100, 2021) + "\t "+atomicReference.get());},"t2").start();// 暂停一会线程try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println("============以下是ABA问题的解决==========");new Thread(() ->{// 获取版本号int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName() + "\t 第一次版本号:" +stamp);// 暂停1秒线程t3try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }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);// 暂停3秒线程t4 保存上面t3线程完成一次ABA操作try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }boolean result = atomicStampedReference.compareAndSet(100, 2021, stamp, stamp + 1);System.out.println(Thread.currentThread().getName() + "\t 当前最新版本号:" + atomicStampedReference.getStamp());System.out.println(Thread.currentThread().getName() + "\t 修改结果:" + result);System.out.println(Thread.currentThread().getName() + "\t 当前最新值:" + atomicStampedReference.getReference());},"t4").start();}
}

执行:

============以下是ABA问题的产生==========
true     2021
============以下是ABA问题的解决==========
t3   第一次版本号:1
t4   第一次版本号:1
t3   第二次版本号:2
t3   第三次版本号:3
t4   当前最新版本号:3
t4   修改结果:false
t4   当前最新值:100

8、集合类不安全之并发修改异常

ArrayList是线程不安全的集合

/*** 集合类不安全的问题*  ArrayList*/
public class ContainerNotSafeDemo {public static void main(String[] args) {//        List<String> list = Arrays.asList("a","b","c");
//        list.forEach(System.out::println);//输出list各个各值List<String> list = new ArrayList<>();for (int i = 0; i < 3; i++) {new Thread(() ->{list.add(UUID.randomUUID().toString().substring(0,8));System.out.println(list);},String.valueOf(i)).start();}// java.util.ConcurrentModificationException}
}

执行,抛出异常

java.util.ConcurrentModificationException(并发修改异常)
解决方法之一:使用集合的 new Vector()

List list = new Vector();
因为Vector().add()是加锁的;

Vector() 虽然保证了数据的一致性,但是由于加了锁 并发性 降低了 不推荐

解决方法之二:使用集合的 Collections.synchronizedList(new ArrayList<>())

List list = Collections.synchronizedList(new ArrayList<>());

集合类不安全之写时复制

限制不可以使用VectorCollections工具类解决方案
使用 new CopyOnWriteArrayList()(写时复制策略)(推荐)

List<String> set = new CopyOnWriteArrayList<>();

查看CopyOnWriteArrayList源码

使用了volatile轻量级同步机制

CopyOnWriteArrayList思想:
写值复制CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将容器Object[]进行Copy,复制出一个新的容器Object[] newElements ,然后新的容器Object[] newElements 里添加元素,添加完元素之后,
再将原容器的引用指向新的容器setArray(newElements);。这样做的好处是可以对CopyOnWrite容器进行并发的读,
而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

集合类不安全之Set

HashSet()也是线程不安全的

public static void main(String[] args) {Set<String> set = new HashSet<>();for (int i = 0; i < 31; i++) {new Thread(() ->{set.add(UUID.randomUUID().toString().substring(0,8));System.out.println(set);},String.valueOf(i)).start();}}

执行,抛出异常:

java.util.ConcurrentModificationException异常
解决方法之一:使用 Collections.synchronizedSet()

Set<String> set = Collections.synchronizedSet(new HashSet<>());

解决方法之二:使用 CopyOnWriteArraySet() (推荐)

Set<String> set = new CopyOnWriteArraySet<>();

CopyOnWriteArraySet()底层源码也是使用了CopyOnWriteArrayList

new HashSet<>();底层就是 new HashMap<>()

但是HashSet()是传一个值,HashMap()是传两个值?
看源码:

这里HashMap()的key就是HashSet()的值,value是 PRESENT, PRESENT是一个Object的常量

集合类不安全之Map

public static void main(String[] args) {Map<String,String> map = new HashMap<>();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();}
}

执行,报异常:

java.util.ConcurrentModificationException异常
解决方法之一:使用 Collections.synchronizedMap()

Map<String,String> map = Collections.synchronizedMap(new HashMap<>());

解决方法之二:使用 ConcurrentHashMap() (推荐)

Map<String,String> map = new ConcurrentHashMap<>();

9、TransferValue小练习

TransferValue(传值)
Person 类:

@Data
public class Person {private Integer id;private String personName;public Person(String personName){this.personName = personName;}}

TestTransferValue类:

public class TestTransferValue {public void changeValue(int age){age = 30;}public void changeValue2(Person person){person.setPersonName("xxx");}public void changeValue3(String str){str = "xxx";}public static void main(String[] args) {TestTransferValue test = new TestTransferValue();int age = 20;test.changeValue(age);System.out.println("age=" +age);Person person = new Person("abc");test.changeValue2(person);System.out.println("persionName="+person.getPersonName());String str = "abc";test.changeValue3(str);System.out.println("String="+str);}}

执行,输出:

age=20
persionName=xxx
String=abc

JavaTransferValue中,传基本类型的值原值不变,传引用类型的参数,参数变化,原引用的值也会发生改变。但是在常量池中的引用类型变量除外。

  • 基本类型传的是复印件,原值不变
  • 引用类型传的是地址
  • 基本类型值不变,引用类型值会变

10、java锁之公平和非公平锁

公平和非公平锁

公平锁: 是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后中请的线程比先中请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象

公平/非公平锁
并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁

关于两者区别:
公平锁**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而言,也是一种非公平锁。

可重入锁(又名递归锁)

可重入锁(也叫做递归锁)
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,
在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。

ReentrantLock/synchronized就是一个典型的可重入锁。

可重入锁最大的作用是避免死锁

synchronized是可重入锁,看以下代码:

class phone{public synchronized void sendSMS(){System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");sendEmail();}public synchronized void sendEmail(){System.out.println(Thread.currentThread().getName() + "\t ------- invoked sendEmail()");}
}/***  可重入锁(也叫做递归锁)**  指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,*  在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。**  也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。**/
public class ReenterLockDemo {public static void main(String[] args) {phone phone = new phone();new Thread(() ->{phone.sendSMS();},"t1").start();new Thread(() ->{phone.sendSMS();},"t2").start();}
}

执行:

t1    invoked sendSMS()
t1   ------- invoked sendEmail()
t2   invoked sendSMS()
t2   ------- invoked sendEmail()

t1 invoked sendSMS()   # t1线程在外层方法获取锁的时候
t1 ------- invoked sendEmail()   # t1在进入内层方法会自动获取锁

ReentrantLock也是可重入锁,看以下代码:

class phone implements Runnable{Lock lock = new ReentrantLock();@Overridepublic void run()  {get();}public void get(){lock.lock();try {System.out.println(Thread.currentThread().getName() + "\t invoked get()");set();}finally {lock.unlock();}}public void set(){lock.lock();try {System.out.println(Thread.currentThread().getName() + "\t ------- invoked set()");}finally {lock.unlock();}}
}/***  可重入锁(也叫做递归锁)**  指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,*  在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。**  也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。**/
public class ReenterLockDemo {public static void main(String[] args) {phone phone = new phone();Thread t3 = new Thread(phone,"t3");Thread t4 = new Thread(phone,"t4");t3.start();t4.start();}
}

执行:

t3    invoked get()
t3   ------- invoked set()
t4   invoked get()
t4   ------- invoked set()

注意:使用Lock锁试,比如:lock.lock();lock.lock();加了两把锁,解锁必须也是lock.unlock();lock.unlock();两把,少了一把锁会卡死程序。

自旋锁

自旋锁(spinlock)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

代码验证

/***  实现一个自旋锁*  自旋锁好处:循环比较获取直到成功为止,没有类似wait的堵塞。**  通过CAS操作完成自旋锁,A线程先进来调用myLock 方法自己持有5秒锁,B线程随后进来发现*  当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到。*/
public class SpinLockDemo {// 原子引用线程 初始值nullAtomicReference<Thread> atomicReference = new AtomicReference<>();public void myLock(){//当前进来的线程Thread thread = Thread.currentThread();System.out.println(Thread.currentThread().getName() + "\t come in...");// 当第一个线程进来时while (!atomicReference.compareAndSet(null,thread)){}}public void myUnlock(){Thread thread = Thread.currentThread();atomicReference.compareAndSet(thread,null);System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");}public static void main(String[] args) {SpinLockDemo spinLockDemo = new SpinLockDemo();new Thread(() ->{spinLockDemo.myLock();try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }spinLockDemo.myUnlock();},"AA").start();try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }new Thread(() ->{spinLockDemo.myLock();try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }spinLockDemo.myUnlock();},"BB").start();}
}

执行:

独占锁(写锁)/共享锁(读锁)/互斥锁

独占锁:指该锁一次只能被一个线程所持有。对ReentrantLockSynchronized而言都是独占锁

共享锁:指该锁可被多个线程所持有。

ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。

读锁的共享锁可保证并发读是非常高效的,读写,写读,写的过程是互斥的

读写锁代码验证:

不加锁演示:

class MyCache{ //资源类private volatile Map<String,Object> map = new HashMap<>();// 写public void put(String key, Object value){System.out.println(Thread.currentThread().getName() + "\t 正在写入key: "+ key);// 暂停一会儿 300毫秒try { TimeUnit.MICROSECONDS.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 正在都取key: "+ key);// 暂停一会儿 300毫秒try { TimeUnit.MICROSECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }Object result = map.get(key);System.out.println(Thread.currentThread().getName() + "\t 都取完成: " + result);}}/***  多个线程同时读取一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行*  但是*  如果有一个线程想去写共享资源来,就不应该再有其它线程可以对该资源进行读或写*  小总结:*       读-读能共享*       读-写不能共享*       写-写不能共享**       写操作:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断*/
public class ReadWriteLockDemo {public static void main(String[] args) {MyCache myCache = new MyCache();for (int i = 1; i <= 5; i++) {final int tempInt = i;new Thread(() ->{myCache.put(tempInt+"",tempInt+"");},String.valueOf(i)).start();}for (int i = 1; i <= 5; i++) {final int tempInt = i;new Thread(() ->{myCache.get(tempInt+"");},String.valueOf(i)).start();}}
}

执行:

1     正在写入key: 1
3    正在写入key: 3
2    正在写入key: 2
5    正在写入key: 5
1    正在都取key: 1
2    正在都取key: 2
3    写入完成:
3    正在都取key: 3
1    写入完成:
2    写入完成:
4    正在都取key: 4
4    正在写入key: 4
5    正在都取key: 5
5    写入完成:
1    都取完成: 1
3    都取完成: 3
4    写入完成:
2    都取完成: null
4    都取完成: 4
5    都取完成: 5

这里的1号线程做写入操作时,其它线程不应该也进来做写操作

使用ReentrantReadWriteLock读写锁

class MyCache{ //资源类private volatile Map<String,Object> map = new HashMap<>();private ReentrantReadWriteLock rwLocak = new ReentrantReadWriteLock();// 写public void put(String key, Object value){rwLocak.writeLock().lock();try{System.out.println(Thread.currentThread().getName() + "\t 正在写入key: "+ key);// 暂停一会儿 300毫秒try { TimeUnit.MICROSECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }map.put(key,value);System.out.println(Thread.currentThread().getName() + "\t 写入完成: ");}catch (Exception e){e.printStackTrace();}finally{rwLocak.writeLock().unlock();}}// 读public void get(String key){rwLocak.readLock().lock();try{System.out.println(Thread.currentThread().getName() + "\t 正在都取key: "+ key);// 暂停一会儿 300毫秒try { TimeUnit.MICROSECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }Object result = map.get(key);System.out.println(Thread.currentThread().getName() + "\t 都取完成: " + result);}catch (Exception e){e.printStackTrace();}finally{rwLocak.readLock().unlock();}}// 清除public void cleanMap(){map.clear();}}/***  多个线程同时读取一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行*  但是*  如果有一个线程想去写共享资源来,就不应该再有其它线程可以对该资源进行读或写*  小总结:*       读-读能共享*       读-写不能共享*       写-写不能共享**       写操作:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断*/
public class ReadWriteLockDemo {public static void main(String[] args) {MyCache myCache = new MyCache();for (int i = 1; i <= 5; i++) {final int tempInt = i;new Thread(() ->{myCache.put(tempInt+"",tempInt+"");},String.valueOf(i)).start();}for (int i = 1; i <= 5; i++) {final int tempInt = i;new Thread(() ->{myCache.get(tempInt+"");},String.valueOf(i)).start();}}
}

执行:

2     正在写入key: 2
2    写入完成:
4    正在写入key: 4
4    写入完成:
5    正在写入key: 5
5    写入完成:
1    正在写入key: 1
1    写入完成:
3    正在写入key: 3
3    写入完成:
2    正在都取key: 2
1    正在都取key: 1
3    正在都取key: 3
5    正在都取key: 5
4    正在都取key: 4
2    都取完成: 2
3    都取完成: 3
1    都取完成: 1
4    都取完成: 4
5    都取完成: 5

这里就可以看到效果,只容许一个一个操作写操作

11、CountDownLatch/CyclicBarrier/Semaphore类

CountDownLatch

背景

  • CountDownLatch是在java1.5被引入,跟它一起被引入的工具类还有CyclicBarrierSemaphoreconcurrentHashMapBlockingQueue
  • 存在于 java.util.cucurrent`包下。

概念

  • CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行
  • 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

不加CountDownLatch前代码示例:

public class CountDownLatchDemo {public static void main(String[] args) {for (int i = 1; i <= 6 ; i++) {new Thread(() ->{System.out.println(Thread.currentThread().getName() +"\t 上完班,离开公司");},String.valueOf(i)).start();}System.out.println(Thread.currentThread().getName() + "\t ----->保安最后锁门走人");}
}

执行:

2     上完班,离开公司
4    上完班,离开公司
main     ----->保安最后锁门走人
1    上完班,离开公司
3    上完班,离开公司
5    上完班,离开公司
6    上完班,离开公司

这里我们希望保安是应该最后执行

CountDownLatch后代码示例:

public class CountDownLatchDemo {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() +"\t 上完班,离开公司");countDownLatch.countDown();},String.valueOf(i)).start();}countDownLatch.await();System.out.println(Thread.currentThread().getName() + "\t ----->保安最后锁门走人");}
}

执行:

2     上完班,离开公司
1    上完班,离开公司
3    上完班,离开公司
5    上完班,离开公司
6    上完班,离开公司
4    上完班,离开公司
main     ----->保安最后锁门走人

让一线程阻塞直到另一些线程完成一系列操作才被唤醒。

CountDownLatch主要有两个方法, 当一个或多个线程调用await()时,调用线程会被阻塞。
其它线程调用countDown()会将计数器减1(调用countDown()方法的线程不会阻塞),
当计数器的值变为零时,因调用await()方法被阻塞的线程会被唤醒,继续执行。

CyclicBarrier

CyclicBarrier的字面意思就是可循环(Cyclic)使用的屏障(Barrier)。它要求做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrierawait()方法。

代码演示:

public class CyclicBarrierDemo {public static void main(String[] args) {// CyclicBarrier(int parties, Runnable barrierAction)CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { System.out.println("----->>>召唤神龙"); });for (int i = 1; i <= 7 ; i++) {final int 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龙珠
5    收集到第:5龙珠
6    收集到第:6龙珠
1    收集到第:1龙珠
3    收集到第:3龙珠
4    收集到第:4龙珠
7    收集到第:7龙珠
----->>>召唤神龙

Semaphore

Semaphore 是一个计数信号量,必须由获取它的线程释放。

常用于限制可以访问某些资源的线程数量,例如通过 Semaphore 限流。

信号量主要两个目的,一个用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

代码演示:

public class SemaphoreDemo {public static void main(String[] args) {Semaphore semaphore = new Semaphore(3);//模式3个车位for (int i = 1; i <= 6; i++) { // 模式6部汽车new Thread(() ->{try{semaphore.acquire();// 占车位System.out.println(Thread.currentThread().getName() +"\t 抢到车位");// 暂停一会儿线程try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(Thread.currentThread().getName() +"\t 停车3秒后,离开车位");} catch (InterruptedException e){e.printStackTrace();}finally{semaphore.release();//释放车位}},String.valueOf(i)).start();}}
}

执行:

1     抢到车位
3    抢到车位
2    抢到车位
1    停车3秒后,离开车位
3    停车3秒后,离开车位
2    停车3秒后,离开车位
5    抢到车位
6    抢到车位
4    抢到车位
5    停车3秒后,离开车位
6    停车3秒后,离开车位
4    停车3秒后,离开车位

12、阻塞队列理论

队列 + 阻塞队列
顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示:

  • 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
    当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。

  • 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。

    同样试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从列中移除一个或者多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增

为什么用?有什么好处?
在多线程领域:所谓阻塞,在某些情况下余挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒

为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了

Concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

架构介绍

种类分析:

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列。
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
  • SynchronousQueue:不存储元素的阻塞队列,即单个元素的队列
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

BlockingQueue的核心方法

方法类型 抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time,unit)
检查 element() peek() 不可用 不可用
性质 说明
抛出异常 当阻塞队列满时:在往队列中add(e)插入元素会抛出 IllegalStateException: Queue full当阻塞队列空时:
再往队列中remove()移除元素,会抛出 NoSuchElementException
特殊性 插入方法,成功true,失败false
移除方法:成功返回出队列元素,队列没有就返回空
一直阻塞 当阻塞队列满时,生产者继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出。
当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。
超时退出 当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出。

阻塞队列api之抛出异常组

add(e)抛出异常示例:

public static void main(String[] args) {// 设置阻塞队列为3BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);System.out.println(queue.add("a"));System.out.println(queue.add("b"));System.out.println(queue.add("c"));System.out.println(queue.add("d"));}

执行:

remove()抛出异常示例:

public static void main(String[] args) {// 设置阻塞队列为3BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);System.out.println(queue.add("a"));System.out.println(queue.remove());//移除一个元素System.out.println(queue.remove());//移除一个元素
}

执行;

remove()抛出异常示例:

public static void main(String[] args) {// 设置阻塞队列为3BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);System.out.println(queue.add("a"));System.out.println(queue.add("b"));System.out.println(queue.element());System.out.println(queue.remove());System.out.println(queue.remove());System.out.println(queue.element());
}

执行:

remove()检查队首元素,如果阻塞队列为空也会抛出NoSuchElementException异常

阻塞队列api之返回布尔值组

offer(e)特殊值示例:

public static void main(String[] args) {// 设置阻塞队列为3BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);System.out.println(queue.offer("a"));System.out.println(queue.offer("b"));System.out.println(queue.offer("c"));System.out.println(queue.offer("d"));
}

执行:

true
true
true
false

poll(e)特殊值示例:

public static void main(String[] args) {// 设置阻塞队列为3BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);System.out.println(queue.offer("a"));System.out.println(queue.offer("b"));System.out.println("--------");System.out.println(queue.poll());System.out.println(queue.poll());System.out.println(queue.poll());
}

执行:

true
true
--------
a
b
null

poll(e)特殊值示例:

public static void main(String[] args) {// 设置阻塞队列为3BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);System.out.println(queue.offer("a"));System.out.println(queue.offer("b"));System.out.println("--------");System.out.println(queue.peek());System.out.println("--------");System.out.println(queue.poll());System.out.println(queue.poll());System.out.println("--------");System.out.println(queue.peek());}

执行:

true
true
--------
a
--------
a
b
--------
null

阻塞队列api之阻塞

put(e)特殊值示例:

public static void main(String[] args) throws Exception {// 设置阻塞队列为3BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);queue.put("a");queue.put("b");queue.put("c");System.out.println("--------");queue.put("d");
}

执行:

当阻塞队列满时,生产者继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出

take()特殊值示例:

public static void main(String[] args) throws Exception {// 设置阻塞队列为3BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);queue.put("a");queue.put("b");queue.put("c");System.out.println("--------");queue.take();queue.take();queue.take();queue.take();
}

执行:

当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。

阻塞队列api之超时控制

offer(e,time,unit)特殊值示例:

public static void main(String[] args) throws Exception {// 设置阻塞队列为3BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);System.out.println(queue.offer("a", 2L, TimeUnit.SECONDS));System.out.println(queue.offer("b", 2L, TimeUnit.SECONDS));System.out.println(queue.offer("c", 2L, TimeUnit.SECONDS));//2秒钟后插不进 撤回System.out.println(queue.offer("d", 2L, TimeUnit.SECONDS));System.out.println("d 插入队列撤回");}

执行:

true
true
true
false
d 插入队列撤回

阻塞队列之同步SynchronousQueue队列

理论:
SynchronousQueue没有容量。
与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue
每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

代码演示:

public static void main(String[] args) {BlockingQueue<String> blockingQueue = new SynchronousQueue();new Thread(()->{try{System.out.println(Thread.currentThread().getName() + "\t put:1");blockingQueue.put("1");System.out.println(Thread.currentThread().getName() + "\t put:2");blockingQueue.put("2");System.out.println(Thread.currentThread().getName() + "\t put:3");blockingQueue.put("3");} catch (InterruptedException e){e.printStackTrace();}},"AAA").start();new Thread(()->{try{try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(Thread.currentThread().getName() + "\t take:" +blockingQueue.take());try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(Thread.currentThread().getName() + "\t take:" +blockingQueue.take());try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(Thread.currentThread().getName() + "\t take:" +blockingQueue.take());} catch (InterruptedException e){e.printStackTrace();}},"BBB").start();}

执行:

AAA   put:1
BBB  take:1
AAA  put:2
BBB  take:2
AAA  put:3
BBB  take:3

线程通信之生产者消费者传统版

传统版(synchronized, wait, notify)
阻塞队列版(lock, await, signal)

代码示例:

class ShareData{ //资源类private int number = 0;private Lock lock = new ReentrantLock();private Condition condition = lock.newCondition();public void increment() throws InterruptedException {lock.lock();try{// 1 判断while (number != 0){// 等待,不能生产condition.await();}// 干活number++;System.out.println(Thread.currentThread().getName() + "\t" + number);// 3 通知唤醒condition.signalAll();}catch (Exception e){e.printStackTrace();}finally {lock.unlock();}}public void decrement() throws InterruptedException {lock.lock();try{// 1 判断while (number == 0){// 等待,不能生产condition.await();}// 干活number--;System.out.println(Thread.currentThread().getName() + "\t" + number);// 3 通知唤醒condition.signalAll();}catch (Exception e){e.printStackTrace();}finally {lock.unlock();}}
}/***  题目:一个初始值为零的变量,两个线程对其操作,一个加1一个减1,来5轮*      1 线程 --> 操作(方法) ---> 资源类*      2 判断 --> 干活 ---> 通知*      3 防止虚假唤醒机制*/
public class ProdConsumer_TraditionDemo {public static void main(String[] args) {ShareData shareData = new ShareData();new Thread(() ->{for (int i = 1; i <= 5; i++) {try {shareData.increment();} catch (InterruptedException e) {e.printStackTrace();}}},"AA").start();new Thread(() ->{for (int i = 1; i <= 5; i++) {try {shareData.decrement();} catch (InterruptedException e) {e.printStackTrace();}}},"BB").start();}
}

执行:

AA   1
BB  0
AA  1
BB  0
AA  1
BB  0
AA  1
BB  0
AA  1
BB  0

**注意:**使用锁后,判断语句应使用while不能使用ifif判断会出现虚假唤醒,出现异常状况

Synchronized和Lock有什么区别

1.原始构成

  • synchronized关键字 属于JVM层面
    monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象 只能在同步块或者方法中才能调用 wait/notify等方法)
    monitorexit(释放)
  • lock是具体类(java.util.concurrent.locks.Lock)是API层面的锁

2.使用方法

  • synchronized:不需要用户去手动释放锁,当synchronized代码执行后,系统会自动让线程释放对锁的占用
  • ReentrantLock:则需要用户去手动释放锁,若没有主动释放锁,就有可能出现死锁的现象,需要lock()unlock() 配置try/finally语句来完成

3.等待是否可中断

  • synchronized:不可中断,除非抛出异常或者正常运行完成。
  • ReentrantLock:可中断,可以设置超时方法,
    1 设置超时方法,trylock(long timeout, TimeUnit unit)
    2 lockInterrupible() 放代码块中,调用interrupt() 方法可以中断

4.加锁是否公平

  • synchronized:非公平锁
  • ReentrantLock:两者都可以,默认非公平锁,构造函数可以传递boolean值,true为公平锁,false为非公平锁

5.锁绑定多个条件Condition

  • synchronized:没有,要么随机,要么全部唤醒
  • ReentrantLock:用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized那样,要么随机,要么全部唤醒

锁绑定多个条件Condition

代码演示:

class ShareResource{//资源类private int number = 1;// A:1 B:2 C:3private Lock lock = new ReentrantLock();private Condition c1 = lock.newCondition();// A线程private Condition c2 = lock.newCondition();// B线程private Condition c3 = lock.newCondition();// C线程public void print5(){lock.lock();try {// 1 判断while (number != 1){c1.await();}// 2 干活for (int i = 1; i <= 5 ; i++) {System.out.print(Thread.currentThread().getName() + i + " ");}System.out.println("");// 3 通知 B线程number = 2;c2.signal();}catch (Exception e){e.printStackTrace();}finally {lock.unlock();}}public void print10(){lock.lock();try {// 1 判断while (number != 2){c2.await();}// 2 干活for (int i = 1; i <= 10; i++) {System.out.print(Thread.currentThread().getName() + i + " ");}System.out.println("");// 3 通知 C线程number = 3;c3.signal();}catch (Exception e){e.printStackTrace();}finally {lock.unlock();}}public void print15(){lock.lock();try {// 1 判断while (number != 3){c3.await();}// 2 干活for (int i = 1; i <= 15; i++) {System.out.print(Thread.currentThread().getName() + i + " ");}System.out.println("");System.out.println("---------");// 3 通知 A线程number = 1;c1.signal();}catch (Exception e){e.printStackTrace();}finally {lock.unlock();}}
}/***  题目:多线程之间按顺序调用,实现 A -> B -> C 三个线程启动,要求如下:*      AA打印5次,BB打印10次,CC打印15次*      禁接着*      AA打印5次,BB打印10次,CC打印15次*      ...*      来10轮*/
public class SyncAndReentrantLockDemo {public static void main(String[] args) {ShareResource shareResource = new ShareResource();new Thread(() ->{for (int i = 1; i <= 10; i++) {shareResource.print5();}},"A").start();new Thread(() ->{for (int i = 1; i <= 10; i++) {shareResource.print10();}},"B").start();new Thread(() ->{for (int i = 1; i <= 10; i++) {shareResource.print15();}},"C").start();}
}

执行:

A1 A2 A3 A4 A5
B1 B2 B3 B4 B5 B6 B7 B8 B9 B10
C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 C11 C12 C13 C14 C15
---------
A1 A2 A3 A4 A5
B1 B2 B3 B4 B5 B6 B7 B8 B9 B10
C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 C11 C12 C13 C14 C15
---------
A1 A2 A3 A4 A5
B1 B2 B3 B4 B5 B6 B7 B8 B9 B10
C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 C11 C12 C13 C14 C15
---------
A1 A2 A3 A4 A5
B1 B2 B3 B4 B5 B6 B7 B8 B9 B10
C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 C11 C12 C13 C14 C15
---------
A1 A2 A3 A4 A5
B1 B2 B3 B4 B5 B6 B7 B8 B9 B10
C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 C11 C12 C13 C14 C15
---------
A1 A2 A3 A4 A5
B1 B2 B3 B4 B5 B6 B7 B8 B9 B10
C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 C11 C12 C13 C14 C15
---------
A1 A2 A3 A4 A5
B1 B2 B3 B4 B5 B6 B7 B8 B9 B10
C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 C11 C12 C13 C14 C15
---------
A1 A2 A3 A4 A5
B1 B2 B3 B4 B5 B6 B7 B8 B9 B10
C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 C11 C12 C13 C14 C15
---------
A1 A2 A3 A4 A5
B1 B2 B3 B4 B5 B6 B7 B8 B9 B10
C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 C11 C12 C13 C14 C15
---------
A1 A2 A3 A4 A5
B1 B2 B3 B4 B5 B6 B7 B8 B9 B10
C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 C11 C12 C13 C14 C15
---------

线程通信之生产者消费者阻塞队列版

代码演示:

class MyResource{ // 资源类// 高并发 加 volatile 可见性private volatile boolean FLAG = true; // 默认开启,进行生产 + 消费private AtomicInteger atomicInteger = new AtomicInteger();BlockingQueue<String> blockingQueue = null;// 构造注入public MyResource(BlockingQueue<String> blockingQueue) {this.blockingQueue = blockingQueue;System.out.println(blockingQueue.getClass().getName());}public void myProd() throws Exception {String data = null;boolean retValue;while (FLAG){data = atomicInteger.incrementAndGet() + "";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 +"失败");}TimeUnit.SECONDS.sleep(1);}System.out.println(Thread.currentThread().getName() + "\t 生产停止,表示FLAG=false,生产动作结束");}public void myConsumer() throws Exception {String result = null;while (FLAG){result = blockingQueue.poll(2L, TimeUnit.SECONDS);if(null == result || result.equalsIgnoreCase("")){FLAG = false;System.out.println(Thread.currentThread().getName() + "\t超过2秒钟没有取到蛋糕,消费退出");return;}System.out.println(Thread.currentThread().getName() + "\t 消费队列蛋糕" + result +"成功");}}public void stop(){this.FLAG = false;}}/****  volatile/CAS/atomicInteger/blockQueue/线程交互/原子引用*/
public class ProdConsumer_BlockQueueDemo {public static void main(String[] args) throws InterruptedException {MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));new Thread(() ->{System.out.println(Thread.currentThread().getName() + "\t生产线程启动");try {myResource.myProd();} catch (Exception e){e.printStackTrace();}},"Prod").start();new Thread(() ->{System.out.println(Thread.currentThread().getName() + "\t消费线程启动");System.out.println();System.out.println();try {myResource.myConsumer();} catch (Exception e){e.printStackTrace();}},"Consumer").start();TimeUnit.SECONDS.sleep(5);System.out.println();System.out.println();System.out.println();System.out.println("5秒时间到,mian线程-->停止生产");myResource.stop();}
}

执行:

java.util.concurrent.ArrayBlockingQueue
Prod    生产线程启动
Prod     插入队列1成功
Consumer    消费线程启动Consumer   消费队列蛋糕1成功
Prod     插入队列2成功
Consumer     消费队列蛋糕2成功
Prod     插入队列3成功
Consumer     消费队列蛋糕3成功
Prod     插入队列4成功
Consumer     消费队列蛋糕4成功
Prod     插入队列5成功
Consumer     消费队列蛋糕5成功5秒时间到,mian线程-->停止生产
Prod     生产停止,表示FLAG=false,生产动作结束
Consumer    超过2秒钟没有取到蛋糕,消费退出

13、线程池

Callable接口

Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其他线程执行的任务。

CallableRunnable的区别如下:

  • Callable定义的方法是call,而Runnable定义的方法是run

  • Callablecall方法可以有返回值,而Runnablerun方法没有返回值。

  • Callablecall方法可抛出异常,而Runnablerun方法不能抛出异常。

代码示例:

/*** 获取线程方法一:继承Thread类*/
class MyThread extends Thread{}/*** 获取线程方法二:实现Runnable接口*/
class MyRunnabl implements Runnable{@Overridepublic void run() {}
}/*** 获取线程方法三:实现Runnable接口*/
class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {System.out.println(Thread.currentThread().getName() + " come in Callable");TimeUnit.SECONDS.sleep(2);return 1024;}
}/***  多线程*/
public class CallableDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {// 两个线程 一个main线程 一个futureTask线程(A)FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());new Thread(futureTask, "AA").start();//多个线程执行 抢一个FutureTask的时候,只会计算一次 因为都是使用一个FutureTask,会复用上一个执行 除了再new FutureTasknew Thread(futureTask, "BB").start();System.out.println(Thread.currentThread().getName() + "********");int result01 = 100;//        // 自旋锁概念
//        while (!futureTask.isDone()){//
//        }// 要求获得 Callable 线程的计算结果。如果没有计算完成就要去强求,会导致堵塞,直到计算完成int result02 = futureTask.get();// 输出FutureTask的返回值System.out.println("--------result:" + (result01 +  result02));}
}

执行:

main********
AA come in Callable
--------result:1124

线程池使用及优势

查看本机CPU核数:

System.out.println(Runtime.getRuntime().availableProcessors());

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

它的主要特点为:线程复用,控制最大并发数,管理线程。

优势:

  • 第一:降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
  • 第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池3个常用方式

Java中的线程池是通过Executor框架实现的,该框架中用到了ExecutorExecutorsExecutorServiceThreadPoolExecutor这几个类。


了解:

  • Executors.newCachedThreadPool();时间调度线程
  • Executors.newWorkStealingPool(int):Java8新增,使用目前机器上可用的处理器作为它的并行级别

重点:

  • Executors.newFixedThreadPool():执行长期任务,性能好很多
    主要特点如下:
    创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

    newFixedThreadPool创建的线程池corePoolSizemaximumPoolSize值是相等的,它使用的LinkedBlockingQueue
    源码:

代码演示:

/***  第4种获取/使用java多线程的方式,线程池*/
public class SingleThreadDemo {public static void main(String[] args) {ExecutorService threaPool = Executors.newFixedThreadPool(5);// 一池5个处理线程// 模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程try {for (int i = 1; i <= 10; i++) {threaPool.execute(() -> {System.out.println(Thread.currentThread().getName() +"\t 办理业务");});}}catch (Exception e){e.printStackTrace();}finally {threaPool.shutdown();}}
}

执行:

pool-1-thread-2   办理业务
pool-1-thread-5  办理业务
pool-1-thread-2  办理业务
pool-1-thread-4  办理业务
pool-1-thread-3  办理业务
pool-1-thread-1  办理业务
pool-1-thread-3  办理业务
pool-1-thread-2  办理业务
pool-1-thread-4  办理业务
pool-1-thread-5  办理业务
  • Executors.newSingleThreadExecutor():一个任务一个任务执行的场景
    主要特点如下:
    创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。

    newSingleThreadExecutorcorePoolSizemaximumPoolSize都设置为1,它使用的LinkedBlockingQueue
    源码:

代码演示:

public static void main(String[] args) {ExecutorService threaPool = Executors.newSingleThreadExecutor();// 一池1个处理线程// 模拟5个用户来办理业务,每个用户就是一个来自外部的请求线程try {for (int i = 1; i <= 5; i++) {threaPool.execute(() -> {System.out.println(Thread.currentThread().getName() +"\t 办理业务");});}}catch (Exception e){e.printStackTrace();}finally {threaPool.shutdown();}}

执行:

pool-1-thread-1   办理业务
pool-1-thread-1  办理业务
pool-1-thread-1  办理业务
pool-1-thread-1  办理业务
pool-1-thread-1  办理业务
  • Executors.newCachedThreadPool():适用:执行很多短期异步的小程序或者负载较轻的服务器
    主要特点如下:
    创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

    newCachedThreadPoolcorePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
    源码:

代码演示:

 public static void main(String[] args) {ExecutorService threaPool = Executors.newCachedThreadPool() ;// 一池n个处理线程// 模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程try {for (int i = 1; i <= 10; i++) {threaPool.execute(() -> {System.out.println(Thread.currentThread().getName() +"\t 办理业务");});}}catch (Exception e){e.printStackTrace();}finally {threaPool.shutdown();}}

执行:

pool-1-thread-3   办理业务
pool-1-thread-5  办理业务
pool-1-thread-4  办理业务
pool-1-thread-1  办理业务
pool-1-thread-2  办理业务
pool-1-thread-6  办理业务
pool-1-thread-9  办理业务
pool-1-thread-10     办理业务
pool-1-thread-7  办理业务
pool-1-thread-8  办理业务

线程池7大参数入门简介

源码:

7个参数:

1、corePoolSize:线程池中的常驻核心线程数
  ①在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
  ②当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中

2、maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1

3、keepAliveTime:多余的空闲线程的存活时间。
 当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止

4、unit:keepAliveTime的单位。

5、workQueue:任务队列,被提交但尚未被执行的任务。

6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。

7、handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数( maximumPoolSize)。

线程池底层工作原理



1、在创建了线程池后,等待提交过来的任务请求。

2、当调用execute()方法添加一个请求任务时,线程池会做如下判断:
 2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
 2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
 2.3 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
 2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

3、当一个线程完成任务时,它会从队列中取下一个任务来执行。

4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
 所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。

线程池的4种拒绝策略理论简介

等待队列也已经排满了,再也塞不下新任务了
同时,
线程池中的max线程也达到了,无法继续为新任务服务。

这时候我们就需要拒绝策略机制合理的处理这个问题。

  • AbortPolicy(默认) :直接抛出 RejectedExecutionException异常阻止系统正常运知。

  • CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。

  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。

  • DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。

以上内置拒绝策略均实现了RejectedExecutionHandler接口。

线程池实际中使用哪一个

Executors.newFixedThreadPool(int)
Executors.newSingleThreadExecutor()
Executors.newCachedThreadPool()

上面三个创建线程一个都不能用,因为它们底层使用了LinkedBlockingQueue阻塞队列

LinkedBlockingQueue阻塞队列是由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列。 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
SynchronousQueue阻塞队列是不存储元素的阻塞队列。允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM

线程池的手写改造和拒绝策略

演示代码:

public class MyThreadPoolDemo {public static void main(String[] args) {ExecutorService threadPool = new ThreadPoolExecutor(2,5,2L,TimeUnit.SECONDS,new LinkedBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());// 模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程try {for (int i = 1; i <= 9; i++) {threadPool.execute(() -> {System.out.println(Thread.currentThread().getName() +"\t 办理业务");});}}catch (Exception e){e.printStackTrace();}finally {threadPool.shutdown();}}
}

执行:报出异常RejectedExecutionException

这里设置处理最大线程数为8个(5+3),但是这里使用了9个线程数,并且使用默认的拒绝策略AbortPolicy(直接抛出 RejectedExecutionException异常阻止系统正常运知。

修改threadPool的拒绝策略为CallerRunsPolicy("调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。)

执行:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。

修改threadPool的拒绝策略为DiscardOldestPolicy(抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。)

执行:抛弃队列中等待最久的任务

修改threadPool的拒绝策略为DiscardPolicy(直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。)

执行:直接丢弃任务,不予任何处理也不抛出异常

线程池配置合理线程数

CPU密集型

获取本机CPU核数:

System.out.println(Runtime.getRuntime().availableProcessors());

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个线程数

14、死锁编码及定位分析

是什么:
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够碍到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

产生死锁主要原因:
系统资源不足
进程运行推进的顺序不合适
资源分配不当

代码演示死锁:

class holdLockThread implements Runnable{private String lockA;private String lockB;public holdLockThread(String lockA, String lockB) {this.lockA = lockA;this.lockB = lockB;}@Overridepublic void run() {synchronized (lockA){System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockA + "\t 尝试获得:" + lockB);// 暂停一会儿线程try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }synchronized (lockB){System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockB + "\t 尝试获得:" + lockA);}}}
}/*** 死锁是指两个或两个以上的进程在执行过程中,(自己占用锁,然后又去抢其他线程的锁)* 因争夺资源而造成的一种互相等待的现象,* 若无外力干涉那它们都将无法推进下去*/
public class DeadLockDemo {public static void main(String[] args) {String lockA = "lockA";String lockB = "lockB";new Thread(new holdLockThread(lockA,lockB),"ThreadAAA").start();new Thread(new holdLockThread(lockB,lockA),"ThreadBBB").start();}
}

执行:程序还在一直在执行…

查看是否死锁工具:

linux 下查询进程命令
ps -ef | grep 进程名
#ls(英文全拼:list files)命令用于显示指定工作目录下之内容 -l 除文件名称外,亦将文件型态、权限、拥有者、文件大小等资讯详细列出
ls -l  

window下的java运程程序 也有类似ps的查看进程命令,但是目前我们需要查看的只是java
  jps = java ps
  jps -l

1、jps命令定位进程号

2、jstack找到死锁查看


15、JVM + GC

快速回顾复习

JVM内存结构

Java8以后的JVM


GC的作用域:

常见的垃圾回收算法:
1、引用计数

2、复制算法


      MinorGC的过程(复制->清空->互换)
 a、Eden、SurvivorFrom复制到SurvivorTo,年龄+1
 首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1

 b、清空eden、SurvivorErom
 然后,清空EdenSurvivor From中的对象,也即复制之后有交换,谁空谁是To

 c、Survivor To和 Survivor From互换
 最后,SurvivorToSurvivorFrom互换,原SurvivorTo成为下一次GC时的Survivor From区。部分对象会在FromTo区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。

3、标记清除

4、标记整理

GC Roots

什么是垃圾:

简单的说就是内存中已经不再被使用到的空间就是垃圾。

要进行垃圾回收,如何判断一个对象是否可以被回收?
1、引用计数法
2、枚举根节点做可达性分析(根搜索路径)

引用计数法

Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。

因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器,
每当有一个地方引用它,计数器值加1
每当有一个引用失效时,计数器值减1。

任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。

那为什么主流的Java虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。

枚举根节点做可达性分析(根搜索路径)
为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。

所谓 “GC roots” 或者说tracing GC的 “根集合” 就是一组必须活跃的引用。

基本思路就是通过一系列名为 “GC Roots” 的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活;没有被遍历到的就自然被判定为死亡。

Java中可以作为GC Roots的对象

  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。

  • 方法区中的类静态属性引用的对象。

  • 方法区中常量引用的对象。

  • 本地方法栈中JNI(Native方法)引用的对象。

示例:

/*** 在java中,可作为GC Roots的对象有:*  1、虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。*  2、方法区中的类静态属性引用的对象。*  3、方法区中常量引用的对象。*  4、本地方法栈中JNI(Native方法)引用的对象。--Thread().start()方法*/
public class GCRootDemo {private byte[] bytesArrys = new byte[100 * 1024 * 1024];//    private static GCRootDemo2 t2; // 2、方法区中的类静态属性引用的对象。
//    private static final GCRootDemo3 t3 = new GCRootDemo3(8); // 3、方法区中常量引用的对象。public static void m1() {GCRootDemo t1 = new GCRootDemo();// 1、虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。System.gc();System.out.println("第一次GC完成");}public static void main(String[] args) {m1();}
}

JVM的标配参数和X参数

JVM的参数类型:

  • 标配参数
  • X参数(了解)
  • XX参数

标配参数

java -version
-help
java -showversion

X参数

-Xint:解释执行
-Xcomp:第一次使用就编译成本地代码
-Xmixed:混合模式

JVM的XX参数之布尔类型

  • Boolean类型
  • KV设置类型

Boolean类型
公式: -XX:+ 或者 - 某个属性值 (+ 表示开启 - 表示关闭)

如何查看一个正在运行中的java程序,它的某个jvm参数是否开启?具体值是多少?

jps -l  #查看一个正在运行中的java程序,得到Java程序号。
jinfo -flag PrintGCDetails xxx(java进程编号)  #查看java 进程的某个jvm参数(如PrintGCDetails 配置项)是否开启。
jinfo -flags xxx(java进程编号)  #查看它的所有jvm参数

是否打印GC收集细节

演示:

运行程序:查看java的程序

F:>jinfo -flag PrintGCDetails 15480
-XX:-PrintGCDetails   # - 未开启

添加:-XX:+PrintGCDetails 参数

执行,再查看java的程序

是否使用串行垃圾回收器

参数:-XX:+UseSerialGC / -XX:-UseSerialGC

跟上述的PrintGCDetails修改一样操作

JVM的XX参数之设置类型

KV设置类型
公式:-XX:属性key=属性值value

比如:
-XX:MetaspaceSize=128m   #MetaspaceSize元数据空间,专门用来存元数据的

-XX:MaxTenuringThreshold=15   #MaxTenuringThreshold进入老年代阈值设置

开启垃圾回收细节打印

查看MetaspaceSize元数据空间(默认值21807104=21兆)

设置MetaspaceSize的值

-XX:MetaspaceSize=1024m


重启,再查询

查看MaxTenuringThreshold默认值

查看当前进程编号所有参数

jinfo -flags xxx

两个经典参数:
-Xms  等价于:-XX:InitialHeapSize (初始化堆内存)
-Xmx  等价于:-XX:MaxHeapSize (最大堆内存)

JVM盘点家底查看初始默认值

第一种:

jps -l
jinfo -flag 具体参数 java进程编号
jinfo -flags  java进程编号

第二种:

-XX:+PrintFlagsInitial
主要查看初始默认值

公式: java -XX:+PrintFlagsInitial -version
    java -XX:+PrintFlagsInitial

-XX:+PrintFlagsFinal
主要查看修改更新

公式: java -XX:+PrintFlagsFinal -version
    java -XX:+PrintFlagsFinal -version


注:= 和 := 是两种不同的概念 = 代表是初始值没有动过; := 代表是人为改过或者jvm加载时修改过

JVM盘点家底查看修改变更值

运行java命令的同时打印出参数:


修改本次运行的元数据空间

java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512m Test.java



-XX:+PrintCommandLineFlags
打印命令行参数

堆内存初始大小快速复习

JDK 1.8之后将最初的永久代取消了,由元空间取代。

在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。

元空间(Java8)与永久代(Java7)之间最大的区别在于:永久带使用的JVM的堆内存,但是Java8以后的元空间并不在虚拟机中而是使用本机物理内存。

因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。

public static void main(String[] args)  {long totalMemory = Runtime.getRuntime().totalMemory(); // 返回 java 虚拟机中内存数量long maxMemory = Runtime.getRuntime().maxMemory(); // 返回 java 虚拟机试图使用的最大内存量System.out.println("TOTAL_MEMORY(-Xms) = " + totalMemory + " (字节)、" + (totalMemory / (double)1024/1024) +"MB");System.out.println("MAX_MEMORY(-Xmx) = " + maxMemory + " (字节)、" + (maxMemory/ (double)1024/1024) +"MB");}

执行:

TOTAL_MEMORY(-Xms) = 124780544 (字节)、119.0MB
MAX_MEMORY(-Xmx) = 1821376512 (字节)、1737.0MB

常用基础参数栈内存Xss讲解

-Xms
初始大小内存,默认为物理内存1/64
等价于 -XX:InitialHeapSize

-Xmx
最大分配内存,默认为物理内存1/4
等价于 -XX:MaxHeapSize

-Xss
设置单个线程栈的大小,一般默认为512~1024k
等价于 -XX:ThreadStackSize

查看ThreadStackSize默认值 为 0

设置 -Xss128k

重启,再次查看

文档
官网文档
ThreadStackSize表面是0,实际是使用出厂默认值

常用基础参数元空间MetaspaceSize讲解

-Xmn
设置年轻代大小(一般使用默认值)

-XX:MetaspaceSize
设置元空间大小
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
不过元空间与永久代之间最大的区别在于:
元空间并不在虚拟机中,而是使用本地内存。
因此,默认情况下,元空间的大小仅受本地内存限制

典型设置案例 (按本地实际内存配置,这里只做参看)

-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC

-XX:+UseSerialGC 串行垃圾回收器
-XX:+UseParalleGC 并行垃圾回收器

常用基础参数PrintGCDetails回收前后对比讲解

-XX:+PrintGCDetails
输出详细GC收集日志信息

设置参数:-Xms10m -Xmx10m -XX:+PrintGCDetails

运行:因为 new byte[10 * 1024 * 1024] 超过 -Xms10m -Xmx10m 设定的参数 抛出异常


GC

Full GC

常用基础参数SurvivorRatio讲解

-XX:SurvivorRatio
设置新生代中 eden 和 S0/S1 空间的比例

默认
-XX:SurvivorRatio=8, Eden:S0:S1 = 8:1:1

-XX:+PrintGCDetails -XX:+UseSerialGC -Xms10m -Xmx10m -XX:SurvivorRatio=8

假如
-XX:SurvivorRatio=4, Eden:S0:S1 = 4:1:1
SurvivorRatio值就是 eden 区的比例占多少,S0/S1相同。

-XX:+PrintGCDetails -XX:+UseSerialGC -Xms10m -Xmx10m -XX:SurvivorRatio=4

常用基础参数NewRatio讲解

-XX:NewRatio
配置年轻代 new 和老年代 old 在堆结构的占比
默认:-XX:NewRatio=2 新生代占1,老年代2,年轻代占整个堆的1/3
-XX:NewRatio=4:新生代占1,老年代占4,年轻代占整个堆的1/5

NewRadio值就是设置老年代的占比,剩下的1个新生代。

默认
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseSerialGC -XX:NewRatio=2

修改
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseSerialGC -XX:NewRatio=4

新生代特别小,会造成频繁的进行GC收集。

常用基础参数MaxTenuringThreshold讲解

-XX:MaxTenuringThreshold
设置垃圾的最大年龄(young区到old区)

查看默认进入老年代年龄:

-XX:MaxTenuringThreshold=0; 设置垃圾最大年龄。如果设置为0的话,则年轻对象不经过Survivor区,直接进入老年代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大的值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概念。

注:-XX:MaxTenuringThreshold 的值必须在 0~15之间
示例:

执行:

16、强引用、软引用、弱引用、虚引用

整体架构

强引用Reference

强引用(默认支持模式)
当内存不足,JVM开始回收垃圾,对于强引用对象,就算是出现了 OOM 也不会对该对象进行回收,死都不改。

强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还"活着",垃圾收集器不会碰这种对象。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远不会用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或显式地将相应(强)引用赋值为 null
一般认为就是可被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。

示例:

public class StrongRefereceDemo {public static void main(String[] args) {Object obj1 = new Object();// 这样定义的默认是 强引用Object obj2 = obj1;// obj2 引用赋值obj1 = null; // 置空System.gc();System.out.println(obj2);}
}

执行:

软引用SoftReference

软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。

对于只有软引用的对象来说,
当系统内存充足时它  不会  被回收,
当系统内存不足时它  会  被回收。

软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

内存够用的时候就保留,代码示例:

public class SoftReferenceDemo {/*** 内存够用的时候就保留,不够用就回收*/public static void softRefMemoryEnough(){Object obj1 = new Object();SoftReference<Object> softReference = new SoftReference<>(obj1);System.out.println("obj1 = " + obj1);System.out.println("softReference = " + softReference.get());obj1 = null;System.gc();System.out.println("--------------");System.out.println("obj1 = " + obj1);System.out.println("softReference = " + softReference.get());}public static void main(String[] args) {softRefMemoryEnough();}
}

运行:没有被回收

obj1 = java.lang.Object@7591083d
softReference = java.lang.Object@7591083d
--------------
obj1 = null
softReference = java.lang.Object@7591083d

内存不够用就回收,代码示例:

public class SoftReferenceDemo {/***  JVM配置,故意产生大对象并配置小的内存,让内存不够用了导致 OOM,看软引用的回收情况*  -Xms5m -Xmx5m -XX:+PrintGCDetails*/public static void softRefMemoryNotEnough(){Object obj1 = new Object();SoftReference<Object> softReference = new SoftReference<>(obj1);System.out.println("obj1 = " + obj1);System.out.println("softReference = " + softReference.get());obj1 = null;System.out.println("----------------");try{byte[] bytes = new byte[30 * 1024 * 1024];}catch (Throwable e){e.printStackTrace();}finally{System.out.println("obj1 = " + obj1);System.out.println("softReference = " + softReference.get());}}public static void main(String[] args) {softRefMemoryNotEnough();}
}

配置:-Xms5m -Xmx5m -XX:+PrintGCDetails

运行:内存不够用,软引用被回收

弱引用WeakReference

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,

对于只有弱引用的对象来说,只要垃圾回收机制一运行不管JVM的内存空间是否足够,都会回收该对象占用的内存

代码演示:

public class WeakReferenceDemo {public static void main(String[] args) {Object obj1 = new Object();WeakReference<Object> obj2 = new WeakReference<>(obj1);System.out.println("obj1 = " + obj1);System.out.println("obj2 = " + obj2.get());obj1 = null;System.gc();System.out.println("---------------");System.out.println("obj1 = " + obj1);System.out.println("obj2 = " + obj2.get());}
}

运行:

软引用和弱引用的适用场景

场景:假如有一个应用需要读取大量的本地图片

  • 如果每次读取图片都从硬盘读取则会严重影响性能
  • 如果一次性全部加载到内存中,又可能造成内存溢出

此时使用软引用可以解决这个问题。

  设计思路:使用HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占的空间,从而有效地避免了OOM的问题 (也可以使用弱引用)

示例:

 Map<String,SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

WeakHashMap案例演示和解析

代码演示:

public class WeakHashMapDemo {public static void main(String[] args) {myHashmap();System.out.println("==============");myWeakHashMap();}private static void myWeakHashMap() {WeakHashMap<Integer,String> map = new WeakHashMap<>();Integer key = new Integer(2);String value = "HashMap";map.put(key, value);System.out.println("map = " + map);key = null;System.out.println("---------");System.out.println("map = " + map);System.gc();System.out.println("map = " + map +"\t"+ map.size());}// 强引用private static void myHashmap() {HashMap<Integer,String> map = new HashMap<>();Integer key = new Integer(1);String value = "HashMap";map.put(key, value);System.out.println("map = " + map);key = null;System.out.println("---------");System.out.println("map = " + map);System.gc();System.out.println("map = " + map +"\t"+ map.size());}
}

运行:

map = {1=HashMap}
---------
map = {1=HashMap}
map = {1=HashMap} 1
==============
map = {2=HashMap}
---------
map = {2=HashMap}
map = {}   0Process finished with exit code 0

虚引用简介

虚引用需要java.lang.ref.PhantomReference类来实现。

顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。

虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。
PhantomReferenceget方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比fihalization机制更灵活的回收操作。

换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

ReferenceQueue引用队列介

代码演示:

public class ReferenceQueueDemo {public static void main(String[] args) throws InterruptedException {Object obj1 = new Object();ReferenceQueue<Object> referenceQueue = new ReferenceQueue();WeakReference<Object> obj2 = new WeakReference<>(obj1,referenceQueue);System.out.println("obj1 = " + obj1);System.out.println("obj2 = " + obj2.get());System.out.println("referenceQueue = " + referenceQueue.poll());System.out.println("================");obj1 = null;System.gc();Thread.sleep(500);System.out.println("obj1 = " + obj1);System.out.println("obj2 = " + obj2.get());System.out.println("referenceQueue = " + referenceQueue.poll());}
}

运行:

obj1 = java.lang.Object@7591083d
obj2 = java.lang.Object@7591083d
referenceQueue = null
================
obj1 = null
obj2 = null
referenceQueue = java.lang.ref.WeakReference@77a567e1

referenceQueue被回收前需要被引用队列保存下。

虚引用PhantomReference

代码演示:

/*** java通过了4种引用类型,在垃圾回收的时候,都有自己各自的特点* ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样可以运行。** 创建引用的时候可以指定关联的队列,当GC释放对象内存的时候,会将引用加入到引用队列,* 如果程序发现某个虚引用已经加入到引用队列,那么就可以在所引用的对象的内存被回收之前采不要的行动* 这相当于是一种通知机制** 当关联的引用队列中有数据的时候,意味者引用指向的堆内存的对象被回收。通过这种方式,JVM允许我们在对象被销毁后,* 做一些我们自己想做的事情。*/
public class PhantomReferenceDemo {public static void main(String[] args) throws InterruptedException {Object obj1 = new Object();ReferenceQueue<Object> referenceQueue = new ReferenceQueue();PhantomReference<Object> obj2 = new PhantomReference<>(obj1,referenceQueue);System.out.println("obj1 = " + obj1);System.out.println("obj2 = " + obj2.get());System.out.println("referenceQueue = " + referenceQueue.poll());System.out.println("================");obj1 = null;System.gc();Thread.sleep(500);System.out.println("obj1 = " + obj1);System.out.println("obj2 = " + obj2.get());System.out.println("referenceQueue = " + referenceQueue.poll());}
}

运行:

obj1 = java.lang.Object@7591083d
obj2 = null
referenceQueue = null
================
obj1 = null
obj2 = null
referenceQueue = java.lang.ref.PhantomReference@77a567e1

GCRoots和四大引用小总结

17、OOM

Java内存溢出(OOM)异常

  • java.lang.StackOverflowError
  • java.lang.OutOfMemoryError: java heap space
  • java.lang.OutOfMemoryError: GC overhead limit exceeded
  • java.lang.OutOfMemoryError: Direct buffer memory
  • java.lang.OutOfMemoryError: unable to create new native thread
  • java.lang.OutOfMemoryError: Metaspace

SOFE之StackOverflowError

代码演示:

public class StackOverflowErrorDemo {public static void main(String[] args) {stackOverflowError();}private static void stackOverflowError() {stackOverflowError();}
}

运行:栈默认值为512~1024,使用递归添加方法到栈里,使其撑爆,Exception in thread "main" java.lang.StackOverflowError


StackOverflowErrorOutOfMemoryError是属于Error错误

OOM之Java heap space

堆内存错误

代码演示:

public class JavaHeapSpaceDemo {// -Xms5m -Xmx5mpublic static void main(String[] args) {//        byte[] bytes = new byte[80 * 1024 * 1024];String str = "zzpp";while (true){str += str + new Random().nextInt(11111111) + new Random().nextInt(22222222);str.intern();}}
}

配置:-Xms5m -Xmx5m

运行:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

OOM之GC overhead limit exceeded

GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC 都只回收了不到2%的极端情况下才会抛出。

假如不抛出GC overhead limit错误会发生什么情况呢?那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行。这样就形成恶性循环,

CPU使用率一直是100%,而GC却没有任何成果。

代码演示:

/*** JVM配置参数演示*  -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m*/
public class GCOverheadDemo {public static void main(String[] args){int a = 0;List<String> list = new ArrayList<>();try{while(true){list.add(String.valueOf(++a).intern());}}catch (Exception e){System.out.println("********a="+a);e.printStackTrace();throw e;}}
}

JVM配置参数:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m

-XX:MaxDirectMemorySize:设置java堆外内存的峰值

运行:抛出Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

OOM之Direct buffer memory

导致原因:
NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,
它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。
这样能在一些场景中显著提高性能,因为避兔了在Java堆Native堆中来回复制数据。

ByteBuffer.allocate(capability) 第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。
ByteBuffer.allocateDirect(capability) 第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。

但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GCDirectByteBuffer对象们就不会被回收,
这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了。

代码演示:

/*** 配置参数:*  -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m**  故障现象*   Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory*/
public class DirectBufferMemoryDemo {public static void main(String[] args) throws InterruptedException {System.out.println("配置的maxDirectMemory:" + (sun.misc.VM.maxDirectMemory() / (double)1024 / 1024) + "MB");TimeUnit.SECONDS.sleep(3);// -XX:MaxDirectMemorySize=5m 分配OS本地内存(堆外内存) 我们配置为5m。但实际使用6mByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);}
}

配置参数:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m

运行:

OOM之unable to create new native thread故障演示

高并发请求服务器时,经常会出现异常java.lang.OutOfMemoryError:unable to create new native thread
准确说该native thread异常与对应的平台有关(线程资源不够)

导致原因:
1、应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
2、你的服务器并不允许你的应用程序创建这么多线程,linux系统默认运行单个进程可以创建的线程为1024个,你的应用创建超过这个数量,就会报 java.lang.OutOfMemoryError:unable to create new native thread

解决方法:
1、想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
2、对于有的应用,确实需要创建很多线程,远超过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 { Thread.sleep(Integer.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); }},""+i).start();}}
}

本次在虚拟机Linux OS(CentOS)操作

javac -d . UnableCreateNewThreadDemo.java #编译

java com.zzp.demo4.UnableCreateNewThreadDemo
会出现下列的错误,线程数大概在900多个

OOM之unable to create new native thread上限调整

非root用户登录Linux系统(CentOS)测试
服务器级别调参调优
查看系统线程限制数目

ulimit -u

修改系统线程限制数目

vim /etc/security/limits.d/20-nproc.conf
打开后发现除了root,其他账户都限制在1024个

假如我们想要zzp这个用卢运行,希望他生成的线程多一些,我们可以如下配置

OOM之Metaspace

查看Metaspace本机的初始化参数,-XX:MetaspaceSize为21810376B(大约20.8M)

java -XX:+PrintFlagsInitial

Java 8及之后的版本使用Metaspace来替代永久代。

Metaspace是方法区在Hotspot 中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟机内存中而是使用本地内存也即在Java8中, classe metadata(the virtual machines internal presentation of Java class),被存储在叫做Metaspace native memory

永久代(Java8后被原空向Metaspace取代了)存放了以下信息:

  • 虚拟机加载的类信息
  • 常量池
  • 静态变量
  • 即时编译后的代码

代码演示:

/*** JVM参数*  -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m** 模拟Metaspace空间溢出,我们不断生成类往元空间灌,类占据的空间总是会超过Metaspace指定的空间大小的。*/
public class MetaspaceSizeDemo {static class OOMTest{ }public static void main(String[] args) {int i = 0;//模拟计数多少次以后发送异常try{while (true){i++;Enhancer enhancer = new Enhancer();//cglib中的字节码增强器enhancer.setSuperclass(OOMTest.class);enhancer.setUseCache(false);enhancer.setCallback(new MethodInterceptor() {@Overridepublic 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();}}
}

JVM参数:-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m

运行:

18、GC垃圾收集器

垃圾收集器回收种类

GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现。

因为目前为止还没有完美的收集器出现,更加没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集

4种主要垃圾收集器(以java8示例):

  • Serial(串行垃圾回收器)
  • Paraller(并行垃圾回收器)
  • CMS(并发垃圾回收器)
  • G1(G1垃圾回收器)

串行并行并发G1四大垃圾回收方式

  • 1、串行垃圾回收器(Serial
    它为单线程环境设计且只使用一个线程进行垃圾收集,会暂停所有的用户线程。所以不适合服务器环境

  • 2、并行垃圾回收器(Parallel)
    多个垃圾收集线程并行工作,此时用户线程也是暂停的,适用于科学计算 / 大数据处理首台处理等弱交互场景,SerialParallel类似,一个是单个另一个是多个,时间和性能比Serial较好

  • 3、并发垃圾回收器(CMS)
    用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程,互联网公司都在使用,适用于响应时间有要求的场景。

上述3个小总结:

  • 4、G1垃圾回收器(G1)
    G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。

19、垃圾收集器

如何查看默认的垃圾收集器

怎么查看默认的垃圾收集器是哪个

java -XX:+PrintCommandLineFlags -version

JVM默认的垃圾收集器有哪些

Javagc回收的类型主要有几种?

  • UseSerialGC:串行回收器
  • UseParallelGC:串行收集器
  • UseConcMarkSweepGC(CMS):并发标记清除
  • UseParNewGC:新生代并行回收
  • UseParallelOldGC:老年代并行收集器
  • UseG1GC:G1垃圾收集器

GC之7大垃圾收集器概述


上图是垃圾收集器就来具体实现这些GC算法并实现内存回收。

不同厂商、不同版本的虚拟机实现差别很大HotSpot中包含的收集器如下图所示:

GC之约定参数说明

部分参数说明:

  • DefNew:Default New Generation(默认新生代)
  • Tenured:Old(老年代)
  • ParNew:Parallel New Generation(新生代并行回收)
  • PSYoungGen:Parallel Scavenge(新生代并行清除)
  • ParOldGen:Parallel Old Generation(老年代并行回收)

Server/Client模式分别是什么意思?
1、使用范围:一般使用Server模式,Client模式基本不会使用

2、操作系统
  2.1、32位的Window操作系统,不论硬件如何都默认使用ClientJVM模式
  2.2、32位的其它操作系统,2G内存同时有2cpu以上用Server模式,低于该配置还是Client模式
  2.3、64位only Server模式

GC之Serial收集器

新生代:

  • 串行GC(Serial)/(Serial Copying)
  • 并行GC(ParNew)
  • 并行回收GC(Parallel)/(Parallel Scavenge)

串行GC(Serial)/(Serial Copying):
串行收集器:Serial收集器
一句话:一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。

串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在进行垃圾收集过程中可能会产生较长的停顿(Stop-The-World状态)。虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。

对应JVM参数是:-XX:+UseSerialGC

开启后会使用:Serial(Young区用) + Serial Old(Old区用)的收集器组合

表示:新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法

-Xms10mm -Xmx10m -XX:+PrintGCDetails -XX:+UseSerialGC

演示:

/*** -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC  (DefNew + Tenured)*/
public class GCDemo {public static void main(String[] args) {System.out.println("*********GCDemo start");try{String str = "zzp";while (true){str += str + new Random().nextInt(77777777) + new Random().nextInt(8888888);str.intern();}}catch (Throwable e){e.printStackTrace();}}
}

JVM参数:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC

运行:

GC之ParNew收集器

并行GC(ParNew):

ParNew (并行)收集器
一句话:使用多线程进行垃圾回收,在垃圾收集时,会Stop-The-World暂停其他所有的工作线程直到它收集结束。

ParNew 收集器其实就是 Serial 收集器新生代的并行多线程版本,最常见的应用场景是配合老年代的 CMS GC 工作,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多 Java 虚拟机运行在 Server 模式下新生代的默认垃圾收集器。

常用对应JVM参数:-XX:+UseParNewGC启用ParNew收集器,只影响新生代的收集,不影响老年代。

开启上述参数后,会使用:ParNew(Young区)+ Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法

-Xms10mm -Xmx10m -XX:+PrintGCDetails -XX:+UseParNewGC

演示:

/*** -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC  (ParNew + Tenured)*/
public class GCDemo1 {public static void main(String[] args) {System.out.println("*********GCDemo start");try{String str = "zzp";while (true){str += str + new Random().nextInt(77777777) + new Random().nextInt(8888888);str.intern();}}catch (Throwable e){e.printStackTrace();}}
}

JVM参数:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC

运行:

但是,ParNew+Tenured这样的搭配,Java8已经不再被推荐
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:Java热点(TM)64位服务器VM警告:不推荐使用带有串行旧收集器的ParNew young collector,并且可能在以后的版本中删除

备注:
-XX:ParallelGCThreads 限制线程数量,默认开启和CPU数目相同的线程数。

GC之Parallel收集器

并行回收GC(Parallel)/(Parallel Scavenge):

Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。一句话:串行收集器在新生代和老年代的并行化。

它重点关注的是:
可控制的吞吐量(Thoughput=运行用户代码时间/(运行用户代码时间+垃圾收集时间),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99% )。高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。

自适应调节策略也是 Parallel Scavenge 收集器与 ParNew 收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量。

常用JVM参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器。

开启该参数后:新生代使用复制算法,老年代使用标记-整理算法。

多说一句:-XX:ParallelGCThreads=数字N 表示启动多少个GC线程
cpu>8   N= 5/8
cpu<8   N=实际个数

-Xms10mm -Xmx10m -XX:+PrintGCDetails -XX:+UseParallelGC
-Xms10mm -Xmx10m -XX:+PrintGCDetails -XX:+UseParallelOldGC

演示:

/*** -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC  (PSYoungGen + ParOldGen)*/
public class GCDemo2 {public static void main(String[] args) {System.out.println("*********GCDemo start");try{String str = "zzp";while (true){str += str + new Random().nextInt(77777777) + new Random().nextInt(8888888);str.intern();}}catch (Throwable e){e.printStackTrace();}}
}

JVM参数:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC

运行:

GC之ParallelOld收集器

老年代:

  • 串行GC(Serial Old)/(Serial MSC)
  • 并行GC(Parallel Old)/(Parallel MSC)
  • 并发标记清除GC(CMS)

并行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

演示:

/*** -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC  (PSYoungGen + ParOldGen)* 不加就是默认 UseParallelGC* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags                        (PSYoungGen + ParOldGen)*/
public class GCDemo3 {public static void main(String[] args) {System.out.println("*********GCDemo start");try{String str = "zzp";while (true){str += str + new Random().nextInt(77777777) + new Random().nextInt(8888888);str.intern();}}catch (Throwable e){e.printStackTrace();}}
}

JVM参数:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC

运行:

GC之CMS收集器

并发标记清除GC(CMS)
CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。

适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。

CMS非常适合地内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。

Concurrent Mark Sweep并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行

开启该收集器的JVM参数:  -XX:+UseConcMarkSweepGC   开启该参数后会自动将 -XX:+UseParNewGC打开。

开启该参数后,使用 ParNewYoung区用)+ CMSOld区用)+ Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器。

-Xms10m -Xmx10m -XX:printGCDetails -XX:+UseConcMarkSweepGC

4步过程:
1、初始标记(CMS initial mark): 只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

2、并发标记(CMS concurrent mark)和用户线程一起 : 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象。

3、重新标记(CMS remark):为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正。

4、 并发清除(CMS concurrent sweep)和用户线程一起 :清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看 CMS 收集器的内存回收和用户线程是一起并发地执行。

优缺点:
优点:并发收集低停顿

缺点:并发执行,对CPU资源压力大,采用的标记清除算法会导致大量碎片

并发执行,对CPU资源压力大:
由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间

采用的标记清除算法会导致大量碎片:
标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数 -XX:CMSFullGCsBeForeCompaction (默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC

演示:

/*** -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC  (Par new generation + concurrent mark-sweep generation)*/
public class GCDemo4 {public static void main(String[] args) {System.out.println("*********GCDemo start");try{String str = "zzp";while (true){str += str + new Random().nextInt(777777777) + new Random().nextInt(888888888);str.intern();}}catch (Throwable e){e.printStackTrace();}}
}

JVM参数:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC

运行:

GC之SerialOld收集器

串行GC(Serial Old)/(Serial MSC)

Serial OldSerial 垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在 Client 默认的java虚拟机默认的年老代垃圾收集器。

Server模式下,主要有两个用途(了解,版本已经到8及以后):

  1. JDK1.5之前版本中与新生代的Parallel Scavenge 收集器搭配使用。(Parallel Scavenge + Serial Old )
  2. 作为老年代版中使用CMS收集器的后备垃圾收集方案。

演示:

/*** (理论知道即可,实际中java8已经被优化掉了,没有了)* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC*/
public class GCDemo5 {public static void main(String[] args) {System.out.println("*********GCDemo start");try{String str = "zzp";while (true){str += str + new Random().nextInt(777777777) + new Random().nextInt(888888888);str.intern();}}catch (Throwable e){e.printStackTrace();}}
}

JVM参数:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC

运行:

在Java8中,-XX:+UseSerialOldGC不起作用。

GC之如何选择垃圾收集器

组合的选择:

  • CPU或者小内存,单机程序
    -XX:+UseSerialGC

  • CPU,需要最大的吞吐量,如后台计算型应用
    -XX:+UseParallelGC 或者(两者只配一个,相互激活)
    -XX:+UseParallelOldGC

  • CPU,追求低停顿时间,需要快速响应如互联网应用
    -XX:+UseConcMarkSweepGC
    -XX:+ParNewGC

参数 新生代垃圾收集器 新生代算法 老年代垃圾收集器 老年代算法
-XX:+UseSerialGC SerialGC 复制 SerialOldGC 标记整理
-XX:+UseParNewGC ParNew 复制 SerialOldGC 标记整理
-XX:+UseParallelGC/-XX:+UseParallelOldGC Parallel[Scavenge] 复制 Parallel Old 标记整理
-XX:+UseConcMarkSweepGC ParNew 复制 CMS + Serial Old的收集器组合(Serial Old作为CMS出错的后备收集器) 标记清除
-XX:+UseG1GC G1整体上采用标记-整理算法 局部是通过复制算法,不会产生内存碎片

20、G1垃圾收集器

GC之G1收集器

演示:

/***  -Xmx10m -Xms10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC*/
public class GCDemo6 {public static void main(String[] args) {System.out.println("*********GCDemo start");try{String str = "zzp";while (true){str += str + new Random().nextInt(777777777) + new Random().nextInt(888888888);str.intern();}}catch (Throwable e){e.printStackTrace();}}
}

JVM参数:-Xmx10m -Xms10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC

运行:


以前收集器特点:

  • 年轻代和老年代是各自独立且连续的内存块;
  • 年轻代收集使用单 eden+S0+S1 进行复制算法;
  • 老年代收集必须扫描整个老年代区域;
  • 都是以尽可能少而快速地执行GC为设计原则。

G1是什么

G1 (Garbage-First)收集器,是一款面向服务端应用的收集器:
官网介绍

翻译:

CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器 -G1 垃圾收集器。

G1是在2012年才在jdk1.7u4中可用。oracle官方计划在JDK9中将G1变成默认的垃圾收集器以替代CMS。它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换java8以前的CMS收集器。

主要改变是EdenSurvivorTenured等内存区域不再是连续的了,而是变成了一个个大小一样的region ,
每个region1M32M不等。一个region有可能属于EdenSurvivor或者Tenured内存区域。

特点:
1:G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW

2:G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片。

3:宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘。

4:G1收集器里面讲整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但它们不再是物理隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域。

5:G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;

GC之G1底层原理

Region区域化垃圾收集器
最大好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可。

区域化内存划片Region,整体编为了一些列不连续的内存区域,避免了全内存区的GC操作。

核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小,
在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数 -XX:G1HeapRegionSize=n 可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为: 32 MB ∗ 2048 = 65536MB = 64G内存

Humongous: 超大对象区

G1算法将堆划分若干区域(Region),它仍然属于分代收集器

这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。

这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。

G1中,还有一种特殊的区域,叫Humongous区域。

如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC

回收步骤
G1收集器下的Young GC
针对Eden区进行收集,Eden区耗尽后会被触发,主要是 小区域收集形成连续的内存块,避免内存碎片

  • Eden区的数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据会部会晋升到Old区。
  • Survivor区的数据移动到新的Survivor区,部会数据晋升到Old区。
  • 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行。

4步过程:

  • 初始标记:只标记GC Roots能直接关联到的对象
  • 并发标记:进行GC Roots Tracing的过程
  • 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
  • 筛选回收:根据时间来进行价值最大化的回收

GC之G1参数配置及和CMS的比较

常用配置参数:

  • -XX:+UseG1GC
  • -XX:G1HeapRegionSize=n:设置的G1区域的大小。值是2的幂,范围是1MB32MB。目标是根据最小的Java堆大小划分出约2048个区域。
  • -XX:MaxGCPauseMillis=n:最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间。
  • -XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45
  • -XX:ConcGCThreads=n;:并发GC使用的线程数。
  • -XX:G1ReservePerecent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%

开发人员仅仅需要声明以下参数即可:
三步归纳:开始G1 + 设置最大内存 + 设置最大停顿时间
-XX:+UseG1GC
-Xmx32g
-XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=n:最大GC停顿时间单位毫秒,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间

G1和CMS比较

  • G1不会产生内碎片
  • 是可以精准控制停顿。该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。

JVMGC结合SpringBoot微服务优化简介

公式:

java -server   jvm的各种参数 -jar  xxx.jar/war包名字

例如:

java -server -Xms512m -Xmx512m -XX:+UseG1GC -jar  xxx.jar

21、Linux命令

  • 整体:top
  • CPU:vmstat
  • 内存: free
  • 硬盘:df
  • 磁盘IO:iostat
  • 网络:ifstat

Linux命令之top

top  #整机性能查看

主要看load average, CPU, MEN三部分
load average表示系统负载均衡 三个数值平均代表: 1分钟、5分钟、15分;
如果三个值相加 除以3 乘以100% 大于 60% --说明系统压力重

uptime  #系统性能命令的精简版

Linux之cpu查看vmstat

vmstate -n 2 3

一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔单位是秒,第二个参数是采样的次数

-procs
r:运行和等待的CPU时间片的进程数,原则上1核的CPU的运行队列不要超过2,整个系统的运行队列不超过总核数的2倍,否则代表系统压力过大
b:等待资源的进程数,比如正在等待磁盘I/O、网络I/O
-cpu
us:用户进程消耗CPU时间百分比,us值高,用户进程消耗CPU时间多,如果长期大于50%,优化程序
sy:内核进程消耗的CPU时间百分比
us + sy:参考值为80%,如果us + sy 大于80%,说明可能存在CPU不足。
id:处于空闲的CPU百分比
wa:系统等待IO的CPU时间百分比
st:来自于一个虚拟机偷取的CPU时间比

Linux之cpu查看pidstat

查看看所有cpu核信息

mpstat -P ALL 2

%idle : CPU的空闲率 越高越好 低于60%CPU繁忙

每个进程使用cpu的用量分解信息

pidstat -u 1 -p 进程编号 #每1秒收集一次

Linux之内存查看free和pidstat

应用程序可用内存数

free

free -g/-m :-g按GB内存显示 -m按MB内存显示 单独是按字节显示

输出简介
Mem: 行(第二行)是内存的使用情况。
Swap: 行(第三行)是交换空间的使用情况。
total: 列显示系统总的可用物理内存和交换空间大小。
used: 列显示已经被使用的物理内存和交换空间。
free: 列显示还有多少物理内存和交换空间可用使用。
shared: 列显示被共享使用的物理内存大小。
buff/cache: 列显示被 buffer 和 cache 使用的物理内存大小。
available: 列显示还可以被应用程序使用的物理内存大小。

-经验值:

  • 应用程序可用内存 / 系统物理内存 >70% 内存充足
  • 应用程序可用内存 / 系统物理内存 <20% 内存不足,需要增加内存
  • 20%< 应用程序可用内存 / 系统物理内存 <70% 内存基本够用

查看额外:

pidstat -p 进程号 -r 采样间隔秒数

Linux之硬盘查看df

查看磁盘剩余空间数(显示文件系统信息)

df -h

Filesystem:表示该文件系统位于哪个分区,因此该列显示的是设备名称;
Size:容量大小
Used:表示用掉的磁盘空间大小;
Avail:表示剩余的磁盘空间大小;
Use%:磁盘空间使用率;
Mounted on:文件系统的挂载点,也就是磁盘挂载的目录位置

Linux之磁盘IO查看iostat和pidstat

磁盘I/O性能评估

iostat -xdk 2 3 # 每间隔2秒 采样3次

磁盘块设备分布

  • rkB/s每秒读取数据量kB;
  • wkB/s每秒写入数据量kB;
  • svctm lO请求的平均服务时间,单位毫秒;
  • await l/O请求的平均等待时间,单位毫秒;值越小,性能越好;
  • util 一秒中有百分几的时间用于I/O操作。接近**100%**时,表示磁盘带宽跑满,需要优化程序或者增加磁盘;
  • rkB/s、wkB/s根据系统应用不同会有不同的值,但有规律遵循: 长期、超大数据读写,肯定不正常,需要优化程序读取。
  • svctm的值与await的值很接近,表示几乎没有IO等待,磁盘性能好。
  • 如果await的值远高于svctm的值,则表示IO队列等待太长,需要优化程序或更换更快磁盘。

查看额外:

pidstat -d 采样间隔秒数 -p 进程号

Linux之网络IO查看ifstat

默认本地没有,下载安装ifstat

wget http://distfiles.macports.org/ifstat/ifstat-1.1.tar.gz
tar xzvf ifstat-1.1.tar.gz
cd ifstat-1.1
./configure
make && make install

查看网络IO

ifstat 1

各个网卡的in、out

观察网络负载情况程序

网络读写是否正常

  • 程序网络I/O优化
  • 增加网络I/O带宽

22、CPU占用过高的定位分析思路

结合Linux和JDK命令一块分析
案例步骤
1、先用top命令找出CPU占比最高的

2、ps -e f或者 jps 进一步定位,得知是一个怎么样的一个后台程序给我们惹事

jps -l
ps -ef|grep java|grep -v grep

3、定位到具体线程或者代码

ps -mp 进程 -o THREAD,tid,time

ps -mp 4872 -o THREAD,tid,time

-m 显示所有的线程
-p pid进程使用cpu的时间
-o 该参数后是用户自定义格式

4、将需要的线程ID转换为16进制格式(英文小写格式)

使用计算器:转换为16进制
4873(十进制)线程转换1309(16进制)

或者命令使用:转换为16进制
命令: printf “%x\n” 有问题的线程

printf “%x\n” 4873

5、jstack 进程ID | grep tid(16进制线程ID小写英文)-A60 (-A60打印之后的60行)

jstack 4872 | grep 1309 -A60

23、GitHub操作

GitHub操作之常用词

常用词含义:
watch:会持续收到该项目的动态
fork:复制其个项目到自己的Github仓库中
star:可以理解为点赞
clone:将项目下载至本地
follow:关注你感兴趣的作者,会收到他们的动态

GitHub操作之in限制搜索

in关键词限制搜索范围:
公式 :xxx(关键词) in:name或description或readme

  • xxx in:name 项目名包含xxx的
  • xxx in:description 项目描述包含xxx的
  • xxx in:readme 项目的readme文件中包含xxx的组合使用

比如搜索秒杀:seckill in:name

组合使用:

  • 搜索项目名或者readme中包含秒杀的项目
  • seckill in:name,readme

比如: seckill in:name,readme

比如: seckill in:name,readme,description

GitHub操作之star和fork范围搜索

公式:

  • xxx关键字 stars 通配符 :> 或者 :>=
  • 区间范围数字 数字1…数字2

案例:

  • 查找stars数大于等于5000的springboot项目:
    搜索命令:springboot stars:>=5000

  • 查找forks数大于500的springcloud项目:
    搜索命令:springcloud forks:>500

    组合使用:

  • 查找fork在100到200之间并且stars数在80到100之间的springboot项目:
    搜索命令:springboot forks:100…200 stars:80…100

GitHub操作之awesome加强搜索

公式:

  • awesome关键字
    awesome系列,一般用来收集学习、工具、书籍类相关的项目
  • 搜索优秀的redis相关的项目,包括框架,教程等 awesome redis

例如:awesome redis

GitHub操作之#L数字

高亮显示某一行代码

公式:

  • 一行:地址后面紧跟 #L 数字
    例如:https://github.com/codingXiaxw/seckill/blob/master/src/main/java/cn/codingxiaxw/dao/SeckillDao.java#L13
  • 多行:地址后面紧跟 #L数字-L数字2
    例如:https://github.com/codingXiaxw/seckill/blob/master/src/main/java/cn/codingxiaxw/dao/SeckillDao.java#L13-L23

GitHub操作之T搜索

进入项目,并按下T键,进行项目内搜索
更多github快捷键

GitHub操作之搜索区域活跃用户

公式:

  • location:地区
  • language:语言

深圳地区的java方向的用户
搜索:location:shenzhen language:java

Java经典面试(二)相关推荐

  1. Java经典面试套路讲解:Java Killer系列

    Java经典面试套路讲解:Java Killer系列 java编程语言是目前应用较为广泛的一门计算机编程语言,目前java市场需求量有增无减.java作为目前IT软件开发行业的重要技术之一,人才市场出 ...

  2. java经典面试题目

    java经典面试题目 网页静态化 1.什么是网页静态化(定义)? 网页静态化是指通过动态网站静态化将动态网站以静态化的方式展现出来 2.网页静态化的优点和好处? 1.静态页面相对于动态页面更容易被搜索 ...

  3. Java经典面试:源码解读及如何保证线程安全

    一面 正式批(别看了都是正式批,提前批就没让我面!)一面.面试时间 08-18,19:53 - 21:08,全程1个小时15分钟.涉及内容:项目.网络.数据库.算法题 1. 自我介绍 2. 项目中的有 ...

  4. Java经典面试:完美世界java开发待遇

    前言 消息中间件作为分布式系统的重要成员,各大公司及开源均有许多解决方案.目前主流的开源解决方案包括RabbitMQ.RocketMQ.Kafka.ActiveMQ等.消息这个东西说简单也简单,说难也 ...

  5. Java经典面试宝典及答案(持续收录)

    文章目录 前言 一.Java基础 1.Java中有几种基本数据类型? 2.equals 和 == 的区别? 3. Integer a =128; Integer b = Integer.valueOf ...

  6. JAVA经典例题二(10 examples)

    古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? (兔子的规律为数列:2,2,4,6,10,16-) pu ...

  7. Java经典试题(二)

    单选题 1:关于选择结构下列( )说法是正确的.( 2 分) A:if语句和else语句必须成对出现 B:if语句可以没有else语句对应 C:switch结构中每个case语句中必须用break语句 ...

  8. 大厂经典面试系列之二:京东Java面试【建议关注收藏订阅,你早晚会用上】

    前言 上一篇文章总结了大厂经典面试系列之一:百度Java面试.接下来的文章我将陆续献上大厂的经典面试系列,所列的题目不代表所有,只是从面试者的口中总结的几次面试题,也是万千面试题的缩影,诣在帮助即将面 ...

  9. java经典50题_JAVA经典算法50题(3)【面试+工作】

    原标题:JAVA经典算法50题(3)[面试+工作] JAVA经典算法50题(3)[面试+工作] [程序21] 题目:求1+2!+3!+...+20!的和. 1.程序分析:此程序只是把累加变成了累乘. ...

最新文章

  1. lr如何监控linux,LoadRunner如何监控Linux系统资源
  2. 单例测试模式中【饿汉式】与【懒汉式】的区别
  3. (转)海量数据面试题集锦
  4. 汇编语言基础教程-寄存器
  5. rest spring_Spring REST:异常处理卷。 1个
  6. sublime自动保存(失去焦点自动保存)
  7. 代码阅读 | 孪生网络目标跟踪的尺度(Scale)计算 (In ATOM)
  8. photoshop cs6破解
  9. 网络传输大文件使用什么软件可以高速传输?
  10. 2009年英国大学综合排名
  11. CTF 小白教程《从0到1:CTFer成长之路》SQL - 2 解题过程
  12. HarryPotter第一部—Aragog-1.0.2
  13. JAVA实现无损word转pdf文件完整代码教程
  14. Android6.0 源码修改之Settings音量调节界面增加通话音量调节
  15. 轻巧的jQuery提示框插件Tipso
  16. 30个CSS使用技巧
  17. OSChina 周四乱弹 ——你老婆在我的硬盘里
  18. day11_ContentProvider动态获取权限
  19. 计算机应用课堂教学结题汇报,课题结题验收总结报告.ppt
  20. 小白项目初尝试——全民飞机大战初期

热门文章

  1. 艾美捷Cy5.5单琥珀酰亚基酯 Cy5.5 NHS酯解决方案
  2. 2023-03-08 Incident: Infrastructure connectivity issue affecting multiple regions
  3. PDF去除水印(1)-根据文本移除对象
  4. AES加密解密(java、web、app)
  5. 源码赏析 - 1K的Firewatch游戏
  6. NOIP2021 游记
  7. 【Leetcode】Number of Islands
  8. 在 Jupyter Notebook 文档页面中添加目录索引及错误解决办法
  9. 第24章 JVM监控及诊断工具-GUI篇
  10. 数字IC设计入门书单