Volatile

volatile是Java虚拟机提供的轻量级的同步机制

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

保证可见性

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

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

JMM关于同步的规定:

  • 线程解锁前,必须把共享变量的值刷新回主内存
  • 线程加锁前,必须读取主内存的最新值到自己的工作内存
  • 加锁解锁是同一把锁

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

不保证原子性

案例

原子性:不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败。

代码示例:(数据操作丢失)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyData{
volatile int number = 0;
public void addPlusPlus(){
number ++;
}
}
public class test {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(() ->{
for (int j = 1; j <= 1000; j++) {
myData.addPlusPlus();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println("result:" + myData.number);
}
}

问题解决

  • 加synchronized
1
2
3
public synchronized void addPlusPlus(){
number ++;
}
  • 使用JUC下的AtomicInteger
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MyData{
volatile int number = 0;
public void addPlusPlus(){
number ++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndIncrement();
}
}
public class test {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(() ->{
for (int j = 1; j <= 1000; j++) {
myData.addPlusPlus();
myData.addAtomic();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println("int type,result1:" + myData.number);
System.out.println("AtomicInteger type,result1:" + myData.atomicInteger);
}
}

禁止指令重排

有序性

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

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

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

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

小结

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

什么叫做内存屏障(Memory Barrier)?

又称内存栅栏,是一个cpu指令,它的作用有两个:

  • 保证特定操作的执行顺序
  • 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

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

线程安全性获得保证

工作内存和主内存同步延迟现象导致的可见性问题:

  • 使用synchronized或者volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见

对于指令重排序导致的可见性和有序性问题:

  • 可以利用volatile关键字解决,因为它可以禁止指令重排序

什么地方可以用volatile

多线程下会有安全问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton {
private static Singleton instance = null;
private Singleton(){
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法Singleton()");
}
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()-> {
Singleton.getInstance();
},String.valueOf(i)).start();
}
}
}
1
2
3
4
5
0	 我是构造方法Singleton()
2 我是构造方法Singleton()
1 我是构造方法Singleton()

Process finished with exit code 0

单例模式DCL

DCL(Double Check Lock,双端检锁机制)

1
2
3
4
5
6
7
8
9
10
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}

单例模式volatile分析

DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排序。

原因在于某一个线程执行到第一次检测时,读取到的instance不为null,但instance的引用对象可能没有完成初始化

instance = new Singleton();可以分为以下3步完成(伪代码)

1
2
3
memory = allocate(); //1、分配对象内存空间
instance(memory); //2、初始化对象
instance = memory; //3、设置instance指向刚分配的内存地址,此时instance!=null

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排序优化是允许的。

1
2
3
memory = allocate(); //1、分配对象内存空间
instance = memory; //3、设置instance指向刚分配的内存地址,此时instance!=null ,!!!但是对象还没有初始化完成
instance(memory); //2、初始化对象

指令重排只会保证串行语义的执行的一致性,并不会关心多线程之间的语义一致性。

所以当一条线程访问instance不为null时,由于instance实例未必已经初始化完成,所以会有线程安全问题。

解决:加入volatile关键字

1
private static volatile Singleton instance = null;

CAS

CAS Demo

CompareAndSwap,比较并交换

与期望值相同,才可以进行修改,返回布尔值。

1
2
3
4
5
6
7
8
9
import java.util.concurrent.atomic.AtomicInteger;

public class CAS {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2023) + "\t current data: " + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 1029) + "\t current data: " + atomicInteger.get());
}
}
1
2
3
4
true	 current data: 2023
false current data: 2023

Process finished with exit code 0

底层原理

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的原子指令,不会造成所谓的数据不一致问题。

atomicInteger.getAndIncrement()源码示例:

1
2
3
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
1
2
3
4
5
6
7
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
  • o:AtomicInteger对象本身
  • offset:该对象值的引用地址
  • delta:需要变动的数量
  • v:通过ooffset找出的主内存中真实的值

用该对象当前的值与v进行比较:

  • 相同:更新v + delta并返回true
  • 不同:继续取值,然后再比较,知道更新完成

过程示例:

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同的CPU上)

  1. AtomicInteger里面的value原始值为5,即主内存中AtomicInteger的value为5,根据JMM模型,线程A和线程B各自持有一份值为5的value的副本到各自的工作内存中。
  2. 线程A通过getIntVolatile(o, offset)方法,拿到value值5,这时线程A被挂起。
  3. 此时线程B也通过getIntVolatile(o, offset)方法,拿到value值5,此时刚好线程B没有被挂起,并执行compareAndSwapInt(o, offset, v, v + delta)方法比较内存值也为5,成功修改内存值为6,线程B打完收工,一切OK。
  4. 这时线程A恢复,执行compareAndSwapInt(o, offset, v, v + delta)方法比较,发现自己手里的值5和主内存值6不一致,说明该值已经被其他线程抢先一步修改过了,那A线程本次修改失败,智能重新读取重新再来一遍
  5. 线程A重新获取value值,因为变量value被volatile修饰,满足可见性,所以其他线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt(o, offset, v, v + delta)进行比较替换,直到成功。

CAS缺点

  1. 循环时间长,开销大
    • 如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
  2. 只能保证一个共享变量的原子操作
    • 对多个共享变量,只能通过加锁来保证原子性
  3. 引出ABA问题

ABA问题

如何引发

狸猫换太子~

CAS会导致“ABA问题”。CAS算法实现一个重要前提需要取出内存中某时刻的数据,并在当下时刻进行比较并替换,那么在这个时间差里可能会导致数据的变化。

比如说,一个线程T1从内存位置V中取出A,这时候另一个线程T2也从内存中取出A,并且线程T2进行了一些操作将值变为了B,然后线程T2又将V位置的数据变成A,这时候线程T1进行CAS操作发现内存中仍然是A,然后T1操作成功。尽管T1线程CAS操作成功,但并不代表这个过程就是没有问题的。

解决方法

原子引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 "User{" +
"username='" + username + '\'' +
", age=" + age +
'}';
}
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User zhangsan = new User("zhangsan",22);
User lisi = new User("lisi",22);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(zhangsan);
System.out.println(atomicReference.compareAndSet(zhangsan, lisi) + "\t" + atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(zhangsan, lisi) + "\t" + atomicReference.get().toString());
}
}

时间戳原子引用

新增一种机制,那就是修改版本号(类似于时间戳)——AtomicStampedReference

ABA问题演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class ABA {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
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秒钟,保证上面T1线程完成了一次ABA操作
try{ TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); }
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
},"T2").start();
}
}
1
2
3
4
========以下是ABA问题的产生========
true 2019

Process finished with exit code 0

ABA问题的解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABA {
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("========以下是ABA问题的解决========");
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);
//暂停1秒钟T3线程
try{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第2次版本号:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t第3次版本号:" + atomicStampedReference.getStamp());
},"T3").start();

new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);
//暂停3秒钟T4线程
try{TimeUnit.SECONDS.sleep(3);}catch (InterruptedException e){e.printStackTrace();}
boolean result = atomicStampedReference.compareAndSet(100, 2023, 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();
}
}
1
2
3
4
5
6
7
8
9
========以下是ABA问题的解决========
T3 第1次版本号:1
T4 第1次版本号:1
T3 第2次版本号:2
T3 第3次版本号:3
T4 修改成功否:false 当前最新实际版本号:3
T4 当前实际最新值:100

Process finished with exit code 0

集合类不安全问题

ArrayList线程安全问题

问题重现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class ContainerNotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}

故障现象

java.util.ConcurrentModificationException

导致原因

并发争抢修改导致,参考花名册签名情况(一个人正在写入,另一个人过来争抢,导致数据不一致异常)

解决方案

Vector

1
List<String> list = new Vector<>();

Collections

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

CopyOnWriteArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
}
}

写时复制:

CopyOnWrite容器,即写时复制容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制出一个新的容器Object[] newElements,然后新的容器Object[] newElements里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray(newElements)

这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

Set线程安全问题

故障现象

java.util.ConcurrentModificationException

解决方案

Collections

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

CopyOnWriteArraySet

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

补充:HashSet底层就是HashMap,至于为什么map有key和value两个值,而set只有一个,是因为调用HashSet调用add方法,实际上返回一个map,只不过添加进的数据是map里的key,value是一个固定的常量。

Map线程安全问题

故障现象

java.util.ConcurrentModificationException

解决方案

Collections

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

ConcurrentHashMap

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

Java锁

公平锁/非公平锁

是什么

1
Lock lock = new ReentrantLock();
1
2
3
4
5
6
7
public ReentrantLock() {
sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

通过构造函数指定该锁是否是公平锁,默认非公平锁。

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

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

区别

  • 公平锁,就是很公平,在并发环境下,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
  • 非公平锁,比较粗鲁,上来就直接占有锁,如果尝试失败,就在采用类似公平锁的那种方式。
  • 非公平锁的优点在于吞吐量比公平锁大,synchronized就是一种非公平锁

可重入锁(递归锁)

是什么

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

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

ReentrantLock/Synchronized就是典型的可重入锁。

可重入锁最大的作用:可以避免死锁。

代码验证

  • Synchronized是一个典型的可重入锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Phone {
public synchronized void sendSMS() throws Exception {
System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");
sendEmail();
}
public synchronized void sendEmail() throws Exception {
System.out.println(Thread.currentThread().getName() + "\t ###### invoked sendEmail()");
}
}
public class ReentrantLockDemo {
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();
}
}
1
2
3
4
5
6
T1	 invoked sendSMS()
T1 ###### invoked sendEmail()
T2 invoked sendSMS()
T2 ###### invoked sendEmail()

Process finished with exit code 0
  • ReentrantLock是一个典型的可重入锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Phone implements Runnable{
Lock lock = new ReentrantLock();
@Override
public 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 ReentrantLockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
Thread thread1 = new Thread(phone);
Thread thread2 = new Thread(phone);
thread1.start();
thread2.start();
}
}
1
2
3
4
5
6
Thread-0	 invoked get()
Thread-0 ###### invoked set()
Thread-1 invoked get()
Thread-1 ###### invoked set()

Process finished with exit code 0

注意:加双锁可不可以,编译能通过吗?能运行成功吗?

  • 可以,能编译成功且运行成功,但加锁解锁要对应。

自旋锁

是什么

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

CAS——自旋锁

循环比较获取直到成功为止,没有类似wait的阻塞。

1
2
3
4
5
6
7
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}

代码验证

通过CAS操作完成自旋锁,A线程先进来调用MyLock方法自己持有锁5秒钟,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B抢到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class SpinLockDemo {
//原子引用线程
AtomicReference<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 invoke 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();
},"T1").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();
},"T2").start();
}
}
1
2
3
4
5
6
T1	 come in ~
T2 come in ~
T1 invoke MyUnLock()
T2 invoke MyUnLock()

Process finished with exit code 0

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

是什么

  • 独占锁:指该锁一次只能被一个线程所持有。ReentrantLock和Synchronized都是独占锁。
  • 共享锁:指该锁可被多个线程所持有。

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

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

多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行,但是,如果有一个线程想去写共享资源表,就不应该再有其他线程可以对该资源进行读或者写。

小总结:

  • 读-读:可共存
  • 读-写:不可共存
  • 写-写:不可共存

代码验证

Before

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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);
//模拟网络拥堵
try {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 {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();
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1	 正在写入:1
3 正在写入:3
2 正在写入:2
5 正在写入:5
4 正在写入:4
1 正在读取
2 正在读取
3 正在读取
4 正在读取
5 正在读取
1 写入完成
4 读取完成:4
4 写入完成
3 读取完成:3
3 写入完成
2 写入完成
5 写入完成
2 读取完成:2
1 读取完成:1
5 读取完成:5

Process finished with exit code 0

显然,写操作被打断的一塌糊涂~

After

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key,Object value){
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
//模拟网络拥堵
try {TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成");
} finally {
rwLock.writeLock().unlock();
}
}
public void get(String key){
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在读取");
//模拟网络拥堵
try {TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
} finally {
rwLock.readLock().unlock();
}
}
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1	 正在写入:1
1 写入完成
2 正在写入:2
2 写入完成
4 正在写入:4
4 写入完成
3 正在写入:3
3 写入完成
5 正在写入:5
5 写入完成
1 正在读取
2 正在读取
3 正在读取
4 正在读取
5 正在读取
3 读取完成:3
4 读取完成:4
1 读取完成:1
2 读取完成:2
5 读取完成:5

Process finished with exit code 0

JUC其他类

CountDownLatch

情景复现

我们期望所有人走后,班长再关门离开

1
2
3
4
5
6
7
8
9
10
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###### 班长关门,走人 ######");
}
}
1
2
3
4
5
6
7
8
9
1	上完自习,离开教室
4 上完自习,离开教室
3 上完自习,离开教室
main ###### 班长关门,走人 ######
2 上完自习,离开教室
6 上完自习,离开教室
5 上完自习,离开教室

Process finished with exit code 0

解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
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###### 班长关门,走人 ######");
}
}
1
2
3
4
5
6
7
8
9
1	上完自习,离开教室
5 上完自习,离开教室
4 上完自习,离开教室
3 上完自习,离开教室
2 上完自习,离开教室
6 上完自习,离开教室
main ###### 班长关门,走人 ######

Process finished with exit code 0

采用枚举,限制线程名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public enum CountryEnum {
ONE(1,"齐"),
TWO(2,"楚"),
THREE(3,"燕"),
FOUR(4,"赵"),
FIVE(5,"魏"),
SIX(6,"韩");

private Integer Code;
private String Message;

CountryEnum(Integer code, String message) {
Code = code;
Message = message;
}

public Integer getCode() {
return Code;
}

public String getMessage() {
return Message;
}

public static CountryEnum forEach_CountryEnum(int index){
CountryEnum[] values = CountryEnum.values();
for (CountryEnum element : values){
if (index == element.getCode()){
return element;
}
}
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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() + "国,被灭");
countDownLatch.countDown();
},CountryEnum.forEach_CountryEnum(i).getMessage()).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t秦帝国,一统天下");
}
}
1
2
3
4
5
6
7
8
9
齐国,被灭
燕国,被灭
楚国,被灭
赵国,被灭
魏国,被灭
韩国,被灭
main 秦帝国,一统天下

Process finished with exit code 0

总结

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

CountDownLatch主要有两个方法:

  • 当一个或者多个线程调用await方法时,调用线程会被阻塞。
  • 其他线程调用countDown方法时,会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值为0时,调用await方法的线程才会被唤醒,继续执行。

CyclicBarrier

是什么

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

代码验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CyclicBarrierDemo {
public static void main(String[] args) {
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();
}
}
}
1
2
3
4
5
6
7
8
9
10
1	 收集到第:1龙珠
4 收集到第:4龙珠
5 收集到第:5龙珠
6 收集到第:6龙珠
7 收集到第:7龙珠
3 收集到第:3龙珠
2 收集到第:2龙珠
******召唤神龙******

Process finished with exit code 0

Semaphore

是什么

信号量:主要用于两个目的

  • 一个是用于多个共享资源的互斥使用
  • 一个用于并发线程数的控制

代码验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3); //模拟三个停车位
for (int i = 1; i <= 6; i++) { //模拟6辆汽车
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "\t抢到车位");
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "\t停车3秒后离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1	抢到车位
3 抢到车位
2 抢到车位
1 停车3秒后离开车位
2 停车3秒后离开车位
4 抢到车位
3 停车3秒后离开车位
5 抢到车位
6 抢到车位
6 停车3秒后离开车位
4 停车3秒后离开车位
5 停车3秒后离开车位

Process finished with exit code 0

阻塞队列

是什么

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

线程1往阻塞队列中添加元素,线程2从阻塞队列中移除元素。

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

好处

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会被自动唤醒。

为什么需要BlockingQueue?

  • 不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程

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

架构梳理+种类分析

List和BlockingQueue属于平级,都是Collection接口下的子接口~

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

阻塞队列api

SynchronousQueue

是什么

SynchronousQueue没有容量。

与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。

每一个put操作必须要等待一个take操作,否则不能添加元素,反之亦然。

代码验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

public class SynchronousQueueDemo {
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();
}
},"T1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "\t take" + blockingQueue.take());
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "\t take" + blockingQueue.take());
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "\t take" + blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T2").start();
}
}
1
2
3
4
5
6
7
8
T1	 put 1
T2 take1
T1 put 2
T2 take2
T1 put 3
T2 take3

Process finished with exit code 0

用在哪里

生产者消费者模式

传统版

一个初始值为0的变量,两个线程对其进行交替操作,一个加1一个减1,来5轮

  1. 线程 操作 资源类
  2. 判断 干活 通知
  3. 防止虚假唤醒

线程的判断,必须放到while循环中,不能用if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* @author liuwq
* @create 2023-04-08 09:29
*
* 一个初始值为0的变量,两个线程对其进行交替操作,一个加1一个减1,来5轮
*/
class ShareData{
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();

public void increase() throws InterruptedException {
lock.lock();
try {
// 1、判断
while (number != 0){
// 等待,不能生产
condition.await();
}
// 2、干活
number++;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3、唤醒
condition.signal();
} finally {
lock.unlock();
}
}

public void reduce() throws InterruptedException {
lock.lock();
try {
// 1、判断
while (number == 0){
// 等待,不能消费
condition.await();
}
// 2、干活
number--;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3、唤醒
condition.signal();
} finally {
lock.unlock();
}
}
}
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.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"T1").start();

new Thread(()->{
for (int i = 1; i <= 5; i++) {
try {
shareData.reduce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"T2").start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
T1	1
T2 0
T1 1
T2 0
T1 1
T2 0
T1 1
T2 0
T1 1
T2 0

Process finished with exit code 0

阻塞队列版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
* @author liuwq
* @create 2023-04-08 17:16
*/
class MyResource{
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 = true;
while (flag){
data = atomicInteger.incrementAndGet() + "";
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 超过2s没有取到蛋糕,消费退出");
System.out.println();
return;
}
System.out.println(Thread.currentThread().getName() + "\t 消费队列蛋糕" + result + "成功");
}
}

public void stop() throws Exception{
this.flag = false;
}
}
public class ProdConsumer_BlockQueueDemo {
public static void main(String[] args) throws Exception {
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 消费线程启动");
try {
myResource.MyConsumer();
} catch (Exception e) {
e.printStackTrace();
}
},"Consumer").start();

TimeUnit.SECONDS.sleep(5);

System.out.println();
System.out.println();

System.out.println("5s钟时间到,大老板main线程叫停,活动结束");

myResource.stop();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java.util.concurrent.ArrayBlockingQueue
Prod 生产线程启动
Consumer 消费线程启动
Prod 插入队列1成功
Consumer 消费队列蛋糕1成功
Prod 插入队列2成功
Consumer 消费队列蛋糕2成功
Prod 插入队列3成功
Consumer 消费队列蛋糕3成功
Prod 插入队列4成功
Consumer 消费队列蛋糕4成功
Prod 插入队列5成功
Consumer 消费队列蛋糕5成功


5s钟时间到,大老板main线程叫停,活动结束
Prod 大老板叫停,flag=false,生产动作结束
Consumer 超过2s没有取到蛋糕,消费退出


Process finished with exit code 0

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可以中断
      • 设置超时方法tryLock(long timeout, TimeUnit unit)
      • lockInterruptibly()放代码块中,调用interrupt()方法可以中断
  4. 加锁是否公平
    • Synchronized非公平锁
    • ReentrantLock两者都可以,默认非公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁
  5. 锁绑定多个条件
    • Synchronized没有
    • ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像Synchronized要么随机唤醒一个线程要么唤醒全部线程

Lock好处: 锁绑定多个条件Condition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* @author liuwq
* @create 2023-04-08 10:16
*
* 多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下:
* A打印5次,B打印10次,C打印15次
* 接着
* A打印5次,B打印10次,C打印15次
* ······
* 来10轮
*/
class ShareResource{
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 {
//1、判断
while (number != 1){
condition1.await();
}
//2、干活
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//3、通知
number = 2;
condition2.signal();
} catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print10(){
lock.lock();
try {
//1、判断
while (number != 2){
condition2.await();
}
//2、干活
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//3、通知
number = 3;
condition3.signal();
} catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print15(){
lock.lock();
try {
//1、判断
while (number != 3){
condition3.await();
}
//2、干活
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//3、通知
number = 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 = 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();
}
}

Callable接口

获取线程的另外一种方式,支持线程返回值。

线程池

优势

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

主要特点:

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

线程池3个常用方式

架构

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

了解

  1. Executors.newScheduledThreadPool():带调度的
  2. Java8新出:Executors.newWorkStealingPool(int)——Java8,使用目前机器上可用的处理器作为它的并行级别

重点

Executors.newFixedThreadPool(int)

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

主要特点如下:

  • 执行长期的任务,性能好很多
  • 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
  • newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue

Executors.newSingleThreadExecutor()

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

主要特点如下:

  • 一个任务一个任务执行的场景
  • 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有的任务按照指定顺序执
  • newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,使用LinkedBlockingQueue

Executors.newCachedThreadPool()

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

主要特点如下:

  • 适用:执行很多短期异步的小程序或者负载较轻的服务器
  • 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
  • newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用SynchronousQueue,也就说,来了任务就创建线程运行,当线程空闲超过60s,就销毁线程。

线程池7大参数

corePoolSize

线程池中的常驻核心线程数

  • 在创建了线程池后,当有请求任务来后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
  • 当线程池中的线程数达到corePoolSize后,就会把到达的任务放到缓存队列当中

maximumPoolSize

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

keepAliveTime

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

unit

keepAliveTime的单位

workQueue

任务队列,被提交但尚未被执行的任务

threadFactory

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

handler

拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数maximumPoolSize,就拒绝任务

线程池底层工作原理

  1. 在创建了线程池后,等待提交过来的任务请求
  2. 当调用execute()方法,添加一个请求任务时,线程池会做以下判断:
    1. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
    2. 如果正在运行的线程数量大于等于corePoolSize,那么将这个任务放入队列
    3. 如果队列满了,且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
    4. 如果队列满了,且正在运行的线程数量大于等于maximumPoolSize,那么线程池会启动饱和拒绝策略
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行
  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会做以下判断:
    1. 如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉
    2. 所有线程池的任务都完成后,会最终收缩到corePoolSize大小

线程池的拒绝策略

是什么

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

这个时候线程池就会采用一定的拒绝策略合理地处理这个问题。

JDK内置的拒绝策略

  • AbortPolicy(默认):直接抛出RejectedExecutionException异常组织系统正常运行
  • CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新的任务流量
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
  • DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案

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

工作中用哪个?

!!!!一个也不用,生产上我们只用自定义的。

因为不管是固定数量的、单一的还是有缓存的线程池,里面用到的阻塞队列都是无界的,2亿多呢。。。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.concurrent.*;

/**
* @author liuwq
* @create 2023-04-09 21:16
*/
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}

线程池合理配置线程数

CPU密集型

CPU密集的意思是:该任务需要大量的运算,而且没有阻塞,CPU一直全速运行

CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总运算能力就那些。

CPU密集型任务:配置尽可能少的线程数量(一般公式:CPU核数+1个线程的线程池)

IO密集型

  1. 由于IO密集型任务线程并不是一直在执行任务,所以应配置尽可能多的线程,例如:CPU核数*2
  2. IO密集型,即该任务需要大量IO,即大量阻塞。在单线程上运行IO密集型任务会导致浪费大量的CPU运算能力(等待),所以在IO密集型任务中使用多线程可以大大加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型时,大部分线程都阻塞,所以应该配置多个线程数:

参考公式:CPU核数/(1-阻塞系数) 阻塞系数在0.8-0.9之间

死锁编码分析及定位分析

是什么

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

产生死锁的原因

  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.concurrent.TimeUnit;

/**
* @author liuwq
* @create 2023-04-10 14:47
*/
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 + "\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),"Thread-A").start();
new Thread(new HoldLockThread(lockB,lockA),"Thread-B").start();
}
}

定位分析

jps -l

定位进程号

jstack 进程号

找到死锁查看

JVM/GC回顾

JVM内存结构

JVM体系概述

Java8以后的JVM

GC的作用域

常见的垃圾回收算法

引用计数

复制

Java堆从GC的角度还可以细分为:

  • 新生代(Eden区、From Survivor区和To Survivor区)
  • 老年代

标记清除

标记整理

JVM/GC面试题

GC Roots

题目:JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots?

什么是垃圾?——简单来说就是内存中已经不再被使用到的空间就是垃圾

要进行垃圾回收,如何判断一个对象是否可以被回收?

枚举根节点做可达性分析(根搜索路径)

为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。

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

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

Java中哪些可以作为GC Roots的对象

  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象

JVM

JVM的参数类型

标配参数

  • -version
  • -help
  • java -showversion

X参数

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

XX参数

Boolean类型

公式:

  • -XX:+或-某个属性值
  • +:表示开启;-表示关闭
  1. 是否打印GC收集细节
    • -XX:-PrintGCDetails
    • -XX:+PrintGCDetails
  2. 是否使用串行垃圾回收器
    • -XX:-UseSerialGC
    • -XX:+UseSerialGC
KV设值类型

公式:

  • -XX:属性key=属性值value

例如:

  • -XX:MetaspaceSize=128m
  • -XX:MaxTenuringThreshold=15
查看当前程序配置

jps:用来查看进程编号

查看单个配置:jinfo -flag 配置项 进程编号

查看所有参数:jinfo -flags 进程号

-Xms

等价于-XX:InitialHeapSize

-Xmx

等价于-XX:MaxHeapSize

查看JVM系统默认值

-XX:+PrintFlagsInitial

  • 查看初始默认值
  • java -XX:+PrintFlagsInitial

-XX:+PrintFlagsFinal

  • 查看修改更新
  • java -XX:+PrintFlagsFinal

默认值修改值区分

  • 有冒号:修改过的
  • 无冒号:默认值

边运行边修改并打印

java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512m 程序名

-XX:+PrintCommandLineFlags

常用基本配置参数

-Xms

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

-Xmx

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

-Xss

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

-Xmn

  • 设置年轻代大小

-XX:MetaspaceSize

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

-XX:+PrintGCDetails

  • 输出详细GC收集日志信息
  • GC

  • Full GC

-XX:SurvivorRatio

  • 设置新生代中eden和S0/S1空间的比例
  • 默认:-XX:SurvivorRatio=8,eden:S0:S1=8:1:1
  • 例如:-XX:SurvivorRatio=4,eden:S0:S1=4:1:1
  • SurvivorRatio值就是设置eden区的比例占多少,S0/S1相同

-XX:NewRatio

  • 配置年轻代与老年代在堆结构的占比
  • 默认:-XX:NewRatio=2:新生代占1,老年代2,年轻代占整个堆的1/3
  • 例如:`-XX:NewRatio=4:新生代占1,老年代4,年轻代占整个堆的1/5
  • NewRatio值就是设置老年代的占比,剩下的1给新生代

-XX:MaxTenuringThreshold

  • 设置垃圾最大年龄
  • 只能设置为0-15之间

四大引用

整体架构

强引用

默认支持模式

  • 当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author liuwq
* @create 2023-04-13 20:25
*/
public class StrongReferenceDemo {
public static void main(String[] args) {
Object o1 = new Object(); //强引用
Object o2 = o1; //o2引用赋值
o1 = null; //置空
System.gc();
System.out.println(o2);
}
}

回收o1,o2正常(不回收)

软引用

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

对于只有软引用的对象来说:

  • 当系统内存充足时,它,不会,被回收
  • 当系统内存不足时,它,会,被回收

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

弱引用

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

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

软/弱引用适用场景

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

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

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

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

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

WeakHashMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.util.HashMap;
import java.util.WeakHashMap;

/**
* @author liuwq
* @create 2023-04-13 21:10
*/
public class WeakHashMapDemo {
public 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);

key = null;
System.out.println(map);

System.gc();
System.out.println(map);
}
public 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);

key = null;
System.out.println(map);

System.gc();
System.out.println(map);
}
public static void main(String[] args) {
myHashMap();
System.out.println("==============");
myWeakHashMap();
}
}
1
2
3
4
5
6
7
8
9
{1=HashMap}
{1=HashMap}
{1=HashMap}
============
{2=HashMap}
{2=HashMap}
{}

Process finished with exit code 0

虚引用

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

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

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

虚引用的主要作用是跟踪对象被垃圾回收的状态,仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。

PhantomReference的get方法总是返回null,因此无法访问对应的引用对象,其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。

换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。

Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

总结:被回收前放到引用队列里保存下,做一些后续处理,类似于后置通知!

截屏2023-04-13 21.51.33

OOM

StackOverflowError

Error!方法太深,出不来

1
2
3
4
5
6
7
8
9
public class StackOverflowErrorDemo {
public static void main(String[] args) {
stackOverflowError();
}

private static void stackOverflowError() {
stackOverflowError();
}
}

OutOfMemory

Java heap space

Error!对象太多,撑爆了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.Random;

/**
* @author liuwq
* @create 2023-04-14 20:28
*/
public class JavaHeapSpaceDemo {
public static void main(String[] args) {
String str = "小新";
while (true){
str += str + new Random().nextInt(111) + new Random().nextInt(222);
str.intern();
}
}
}

GC overhead limit exceeded

GC回收时间过长时会抛出OutOfMemoryError。

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

假如不抛出GC overhead limit exceeded错误会发生什么情况呢?

  • GC清理的这么点内存很快就会再次被填满,迫使GC再次执行,这样就形成了恶性循环,CPU使用率一直都是100%,而GC却没有任何的效果。

加上:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.ArrayList;
import java.util.List;

/**
* @author liuwq
* @create 2023-04-14 20:53
*/
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 (Throwable e){
System.out.println("******** i:" + i);
e.printStackTrace();
throw e;
}
}
}

Direct buff memory

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

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

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

unable to create new native thread

高并发请求服务器时,经常会出现这个错误。准确地讲,该错误与对应的平台有关。

导致原因:

  • 你的应用创建了太多线程了,一个应用进程创建多个线程,超过系统承载的极限
  • 你的服务器并不允许你的应用程序创建这么多的线程,LInux系统默认允许单个进程可以创建的线程数是1024个,要是超过了,就会出现这个错误

解决方法:

  • 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多的线程,如果不是,改代码将线程数降到最低
  • 对于有的应用,确实需要创建很多的线程,远超过Linux系统的默认1024个线程的限制,可以通过修改Linux服务器配置,扩大Linux默认限制

Metaspace

Java8及以后的版本,使用Metaspace来替代永久代。

Metaspace是方法区在HotSpot中的实现,它与持久代最大的区别在于:

  • Metaspace并不在虚拟机内存中,而是在本地内存中

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

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

GC回收算法和收集器的关系

分别是什么

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

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

4种主要垃圾收集器

  1. 串行垃圾回收器(Serial)

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

  1. 并行垃圾回收器(Parallel)

多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理等弱交互场景

  1. 并发垃圾回收器(CMS)

用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程

互联网公司多用它,适用于对响应时间有要求的场景

  1. G1垃圾回收器

堆内存分割成不同的区域然后并发的对其进行垃圾回收

垃圾收集器

查看默认的垃圾收集器

java -XX:+PrintCommandLineFlags

默认的垃圾收集器

java的gc回收的类型主要有以下这几种:

  • UseSerialGC
  • UseParallelGC
  • UseConcMarkSweepGC
  • UseParNewGC
  • UseParallelOldGC
  • UseG1GC

7大垃圾收集器

概述

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

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

约定参数说明

  • DefNew:Default New Generation
  • Tenured:Old
  • ParNew:Parallel New Generation
  • PSYoungGen:Parallel Scavenge
  • ParOldGen:Parallel Old Generation

Server/Client

  1. 适用范围:只需要掌握Server模式即可,Client模式基本不会用
  2. 操作系统
    • 32位Windows操作系统,不论硬件如何,默认使用Client的JVM模式
    • 32位其他操作系统,2G内存同时有2个CPU以上用Server模式,低于该配置还是Client模式
    • 64位Only Server模式

新生代之Serial收集器

一个单线程的收集器,在进56tr行垃圾收集的时候,必须暂停其他所有的工作线程,直到它收集结束。

串行收集器是最古老、最稳定以及效率高的收集器,只使用一个线程去回收,但在其进行垃圾收集的过程中,可能会产生较长的停顿(Stop-The-World状态)。

虽然在垃圾收集的过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此,Serial垃圾收集器依然是Java虚拟机运行在Client模式下默认的新生代垃圾收集器。

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

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

表示:新生代和老年代都会使用串行的垃圾收集器,新生代使用复制算法,老年代使用标记-整理算法。

新生代之ParNew收集器

使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World暂停其他所有的工作线程直到它收集结束。

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

常用对应JVM参数:

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

新生代之Parallel收集器

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

它关注的重点是:

  • 可控制的吞吐量Thoughput=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。
  • 自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量)

常用对应JVM参数:

  • -XX:+UseParallelGC或-XX:+UseParallelOldGC(可以相互激活):使用Parallel Scanvenge收集器
  • 开启该参数后:新生代使用复制算法,老年代使用标记-整理算法

老年代之ParallelOld收集器

Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6才开始提供。

在JDK1.6之前,新生代使用Parallel Scavenge收集器,只能搭配老年代的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

老年代之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出错的后备收集器

优点:

  • 并发收集低停顿

缺点:

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

老年代之Serial Old收集器

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

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

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

如何选择垃圾收集器

组合的选择:

  • 单CPU或小内存,单机程序
    • -XX:+UseSerialGC
  • 多CPU,需要最大吞吐量,如后台计算型应用
    • -XX:+UseParallelGC
    • -XX:+UseParallelOldGC
  • 多CPU,追求低停顿时间,需快速响应,如互联网应用
    • -XX:+UseConcMarkSweepGC
    • -XX:+ParNewGC

G1垃圾收集器

以前收集器的特点

  • 年轻代和老年代是各自独立且连续的内存块
  • 年轻代收集使用单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.7u4中可用。oracle官方计划在jdk9中将G1变成默认的垃圾收集器以替代CMS。它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换jva8以前的CMS收集器。

主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。

特点

  1. G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW。
  2. G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
  3. 宏观上看G1之中不再区分年轻代和老年代。**把内存划分成多个独立的子区域(Rego)**,可以近似理解为一个围棋的棋盘。
  4. G1收集器里面讲整个的内存区都混合在一起了,但其木身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但它们不再是物理隔离的,而是一部分Regionl的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域。
  5. G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换:

底层原理

Region区域化垃圾收集器

最大的好处是:化整为零,避免全内存扫描,只需要按照区域来进行扫描即可。

区域化内存划片Region,整体编为了一些列不连续的内存区域,避免了全内存区的GC操作。
核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小。
在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB32MB,且必须是2的幂),默认将整堆分为2048个分区。
大小范围在1MB
32MB,最多能设置2048个区域,也即能够支持的最大内存为:32MB*2048=65536MB=64G内存

回收步骤

针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片

  • Eden区的数据移动到Survivor[区,假如出现Survivorl区空间不够,Eden区数据会部会晋升到Old区
  • Survivor区的数据移动到新的Survivor区,部会数据晋升到Old区
  • 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行。
4步过程
  • 初始标记:只标记GC Rootsf能直接关联到的对象
  • 并发标记:进行GC Roots Tracing的过程
  • 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
  • 筛选回收:根据时间来进行价值最大化的回收

常用配置参数

三步归纳:开始G1+设置最大内存+设置最大停顿时间

-XX:+UseG1GC

-Xmx32g

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

和CMS相比的优势

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

JVMGC结合SpringBoot微服务优化

JVMGC ——> 调优 ——> SpringBoot微服务的生产部署和调参优化

  • IDEA开发完微服务工程
  • Maven进行clean、package
  • 要求微服务启动的时候,同时配置JVM/GC的调优参数
    • 外(重点)
  • 公式:java -server jvm各种参数 -jar jar/war包名字

Linux命令

生产环境服务器变慢,诊断思路和性能评估

整机:top(uptime:系统性能命令精简版)

CPU:vmstat(查看CPU,包含但不限于)

  • vmstat -n 2 3(第一个参数:采样的时间间隔数,单位秒;第二个参数:采样的次数)
    • procs
      • r:运行和等待CPU时间片的进程数
      • b:等待资源的进程数
    • cpu
      • us:用户进程消耗CPU时间百分比
      • sy:内核进程消耗CPU时间百分比
  • mpstat -P ALL 2(查看所有CPU核的信息)
  • pidstat - u 1 -p 进程编号(每个进程使用CPU的用量分解信息)

内存:free(一般用free -m)

硬盘:df(查看磁盘剩余空间,一般用df -h)

磁盘IO:iostat

  • iostat -xdk 2 3
  • pidstat -d 采样间隔秒数 -p 进程号

网络IO:ifstat(默认本地没有,下载ifstat)

CPU占用过高定位分析

结合Linux和JDK命令一块分析

  1. 先用top命令找出CPU占比最高的
  2. ps -ef或者jps -l进一步定位,得知是一个怎么样的后台程序给我们惹事
  3. 定位到具体线程或者代码
    • ps -mp 进程号 -o THREAD,tid,time
      • -m:显示所有的线程
      • -p:pid进程使用CPU的时间
      • -o:该参数后是用户自定义格式
  4. 将需要的线程ID转换为16进制格式(英文小写格式):printf "%x\n" 线程ID
  5. jstack 进程ID|grep tid(16进制线程id小写英文) -A60

GitHub

常用词

  • watch:会持续收到该项目的动态
  • fork:复制某个项目到自己的GitHub仓库中
  • star:点赞
  • clone:将项目下载至本地
  • follow:关注你感兴趣的作者,会收到他们的动态

in限制搜索

xxx关键词 in:name或description或readme

star和fork范围搜索

xxx关键词 stars:>数量

xxx关键词 forks:>数量

区间范围:数字1..数字2

awesome搜索

awesome 关键词

一般用来收集学习、工具、书籍类相关的项目

#L数字

给别人指出关键代码的行号

地址#L行号

地址#L行号-L行号

T搜索

进入项目后,按下英文小写t,可以搜索源码

搜索区域活跃用户

location:地址 language:语言