阳哥大厂第二季

阳哥大厂第二季

第 1 章 课程概述

1、大厂面试题
蚂蚁花呗一面(一个小时)

1、Java容器有哪些?哪些是同步容器,哪些是并发容器?

2、ArayList和LinkedList的插入和访问的时间复杂度?

3、java反射原理,注解原理?

4、新生代分为几个区?使用什么算法进行垃圾回收?为什么使用这个算法?

5、HashMap在什么情况下会扩容,或者有哪些操作会导致扩容?

6、HashMap push方法的执行过程?

7、HashMap检测到hash冲突后,将元素插入在链表的末尾还是开头?

8、1.8还采用了红黑树,讲讲红黑树的特性,为什么人家一定要用红黑树而不是AVL、B树之类的?

9、https和http区别,有没有用过其他安全传输手段?

10、线程池的工作原理,几个重要参数,然后给了具体几个参数分析线程池会怎么做,最后问阻

11、塞队列的作用是什么?

12、linux怎么查看系统负载情况?

13、请详细描述springmvc处理请求全流程?

14、spring一个bean装配的过程?

15、讲一讲AtomicInteger,为什么要用CAS而不是synchronized?

美团一面经验

1、最近做的比较熟悉的项目是哪个,画一下项目技术架构图

2、JVM老年代和新生代的比例?·YGC和FGC发生的具体场景?

3、jstack.jmap.jul分别的意义?如何线上排查JVM的相关问题?

4、线程池的构造类的方法的5个参数的具体意义?

5、单机上一个线程池正在处理服务如果忽然断电怎么办(正在处理和阻塞队列里的请求怎么处理)?

6、使用无界阻塞队列会出现什么问题?

7、接口如何处理重复请求?

百度面试题
1、hashmap hastable 底层实现什么区别?hashtable和concurrenthashtable呢?

2、hashmap和treemap什么区别?底层数据结构是什么?

3、线程池用过吗?都有什么参数?底层如何实现的?

4、synchronized和Lock什么区别?synchronized什么情况情况是对象锁?什么时候是全局锁为什么?

5、ThrealdLocal 是什么底层如何实现?写一个例子呗?

6、volitile的工作原理?

7、cas知道吗如何实现的?

8、请用至少四种写法写一个单例模式?

9、请介绍一下JVW内存模型??用过什么垃圾回收器都说说呗

10、线上发送频繁Full GC如何处理?CPU使用率过高怎么办?

11、如何定位问题?如何解决说一下解决思路和处理方法

12、知道字节码吗?字节码都有哪些?Integer x=5,int y =5,比较x=y都经过哪些步骤?

13、讲讲类加载机制呗,都有哪些类加载器,这些类加载器都加载哪些文件?

14、手写一下类加载Demo,知道osgi吗?他是如何实现的???

15、请问你做过哪些TVW优化?使用什么方法达到什么效果???

16、classforlame(“java.lang.String”)和String classgetClassloader() LoadClass(“java.lang.String”)什么区别啊?

今日头条
1、HashMap如果一直put元素会怎么样?hashcode全都相同如何?

2、AppicationContex的初始化过程?

3、GC用什么收集器?收集的过程如何?哪些部分可以作为GC Roots?

4Volatile 关键字,指令重排序有什么意义?s/nchronied怎么用?

5、并发包里的原子类有哪些,怎么实现?cas在CPU级别用什么指令实现

6、Redis数据结构有哪些?如何实观sorted set?这种数据结构在极端情况树?

7、MySql索引提什么数据结构?B tree有什么特点?优点是什么?

8、慢查询怎么优化?

9、项目:cache,各部分职责,有哪些优化点

京东金融面试
1、Dubbo超时重试;Dubbo超时时间设置

2、如何保障请求执行顺序

3、分布式率务与分布式锁(扣款不要出现负数)

4、分布式session设置

5、执行某操作,前50次成功,第51次失败:a)全部回滚b)前50次提交第51次抛异常,a)b)场景分别如何设置

6、Spring(传播特性)

7、Zookeeper利部些作用

8、JVM内存模型

9、数据库重直和水平拆分

10、MyBais如何分页;如何设置缓存;MySQL分页

美团面试题汇总
一、jvm相关

1、对象在jvm中是怎么存储的?
2、对象头信息里面有哪些东西?
3、jvm内部如何划分?常量池在哪里?
4、写一段小程序使栈溢出,堆溢出?

二、GC

1、GCRoot如何确定,哪些对象可以作为GC Root?
2、GC如何分代的?每代用什么算法回收?
3、CMS过程是怎样的?内部使用什么算法做垃圾回收?
4、分代垃圾回收过程?

三、并发相关

1、java中有哪几种锁?
2、synchronized内部原理?
3、ReentrantLock内部实现?
4、HashMap,Hashtable,ConcurrentHashlap区别?内部实现?
5、原子类内部如何实现的?
6、ArrayBlockingQueue和LinkedBlockingQueue内部如何实现?

四、数据库相关

1、innoDB索引数据结构?
2、BTree B+Tree区别?为什么使用B+Tree?

五、算法

1、写程序判断一棵树是不是完全对称的二叉树?
2、写程序判断两颗二又树是不是相同?

六、其他

1、Comparable和Comparator区别?
2、内存溢出和内存泄露分别指什么?

二轮技术面(这轮面试全程懵逼-好多问题记不清了)

一、项目介绍

二、开源架构

RocketMQ?设计介绍?

三轮技术面

一、项目介绍二、开源框架

1、dubbo如何提供服务?有机器宿掉怎么检测出来?如何找到服务?
2、zk如何管理服务和配置的?
3、tair与redis 有什么区别?
4、redis是单例的吗?
5、mysql的整体架构是怎样的?
6、innodb索引?
7、innodb 主键索引和非主键索引区别?
8、了解java的nio吗?

三、基础

1、Hashlap与concurrentlHashMlap比较?
2、介绍一下java多线程?
3、线程间如何通信?

四、项目管理

项目开发流程?
如何推动了解整个项目情况?

蚂蚁金服电话二面

1、自我介绍、工作经历、技术栈

2、项目中你学到了什么技术?(把三项目具体描述了很久)

3、微服务划分的粒度

4、微服务的高可用怎么保证的?

5、常用的负载均衡,该怎么用,你能说下吗?

6、网关能够为后端服务带来哪些好处?

7、Spring Bean 的生命周期

8、HashSet 是不是线程安全的?为什么不是线程安全的?

9、Java中有哪些线程安全的Map?

10、Concurrenthashmap 是怎么做到线程安全的?

11、HashTable你了解过吗?

12、如何保证线程安全问题?

13、synchronized、lock

14、volatile 的原子性问题?为什么i++这种不支持原子性?从计算机原理的设计来讲下不能保证原子性的原因

15、happens before 原理cas 操作

16、lock 和synchronized的区别?

17、公平锁和非公平锁Java 读写锁

18、读写锁设计主要解决什么问题?

第 2 章 JUC

1、volatile 关键字

谈谈你对volatile的理解

1.1、volatile 三大特性

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

可以将 volatile 看作是乞丐版的 synchronized 锁

  1. 保证内存可见性
  2. 禁止指令重排
  3. 不保证原子性
1.2、JMM 内存模型
1.2.1、谈谈 JMM

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

JMM关于同步的规定:

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

内存可见性

1、由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域。

2、Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行。

3、一个线程如果想要修改主内存中的变量,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存。

4、线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:。
在这里插入图片描述

1.2.2、内存可见性

JMM volatile 的内存可见性:

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

2、这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作

3、但此时A线程工作内存中的共享变量X对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题

代码示例:内存可见性:

/**
 * @ClassName VolatileDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/7 10:59
 * @Version 1.0
 */
public class VolatileDemo {

    public static void main(String[] args) {
        volatileVisibilityDemo();
    }

    /*
    验证volatile的可见性
        1.1 加入int number=0,number变量之前根本没有添加volatile关键字修饰,没有可见性
        1.2 添加了volatile,可以解决可见性问题
     */
    private static void volatileVisibilityDemo() {
        System.out.println("可见性测试");
        MyData myData = new MyData();//资源类
        
        //启动一个线程操作共享数据
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
                TimeUnit.SECONDS.sleep(3);
                myData.setTo60();
                System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.number);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AAA").start();

        while (myData.number == 0) {
            //main线程持有共享数据的拷贝,一直为0
        }
        System.out.println(Thread.currentThread().getName() + "\t mission is over. main get number value: " + myData.number);
    }
}


class MyData {

    int number = 0;

    public void setTo60() {
        this.number = 60;
    }

}

程序运行结果:程序未能停下来
在这里插入图片描述
分析:
1、在上述程序中,两个线程:main 线程和 AAA 线程,同时对 myData 数据进行操作

2、由于 AAA 线程先睡眠了 3s ,所以 main 线程先拿到了 myData.number 的值,将该值拷贝回自己线程的工作内存,此时 myData.number = 0

3、AAA 线程 3s 后醒来,将 myData.number 拷贝回自己线程的工作内存,修改为 60 后,写回主内存

4、但 AAA 线程将 myData.number 的值写回主内存后,并不会去通知 main 线程,所以 main 线程一直拿着自己线程的工作内存中的 myData.number = 0 ,搁那儿 while 循环呢

代码示例 2 :volatile 保证线程间内存的可见性:

代码:number 变量加上 volatile 关键字

/**
 * @ClassName VolatileDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/7 10:59
 * @Version 1.0
 */
public class VolatileDemo {

    public static void main(String[] args) {
        volatileVisibilityDemo();
    }

    /*
    验证volatile的可见性
        1.1 加入int number=0,number变量之前根本没有添加volatile关键字修饰,没有可见性
        1.2 添加了volatile,可以解决可见性问题
     */
    private static void volatileVisibilityDemo() {
        System.out.println("可见性测试");
        MyData myData = new MyData();//资源类
        //启动一个线程操作共享数据
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
                TimeUnit.SECONDS.sleep(3);
                myData.setTo60();
                System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.number);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AAA").start();

        while (myData.number == 0) {
            //main 线程收到通知后,会修改自己线程内存中的值
        }
        System.out.println(Thread.currentThread().getName() + "\t mission is over. main get number value: " + myData.number);
    }
}


class MyData {

    // volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改
    volatile int number = 0;

    public void setTo60() {
        this.number = 60;
    }

}

程序运行结果:停下来了哦
在这里插入图片描述
分析:由于有volatile 关键字的存在,当 AAA 线程修改了 myData.number 的值后,main 线程会受到通知,从而刷新自己线程工作内存中的值

1.2.3、原子性
原子性是什么?

原子性是不可分割,完整性。也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割, 需要整体完成,要么同时成功,要么同时失败(类比数据库原子性)

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

代码

/**
 * @ClassName VolatileDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/7 10:59
 * @Version 1.0
 */
public class VolatileDemo {

    public static void main(String[] args) {
        atomicDemo();
    }

    /*
    2 验证volatile不保证原子性
        2.1 原子性是不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割。
            需要整体完成,要么同时成功,要么同时失败。

        2.2 volatile不可以保证原子性演示

        2.3 如何解决原子性
            1)加sync
            2)使用我们的JUC下AtomicInteger
     */
    private static void atomicDemo() {
        System.out.println("原子性测试");
        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();
        }
        /*
        需要等待上述20个线程都计算完成后,再用main线程去的最终的结果是多少?
        只要上述20个线程还有在执行的,main线程便礼让,让他们执行,直至最后只剩main线程
         */
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "\t int type finally number value: " + myData.number);
    }
}


class MyData {

    // volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改
    volatile int number = 0;

    public void setTo60() {
        this.number = 60;
    }

    //此时number前面已经加了volatile,但是不保证原子性
    public void addPlusPlus() {
        number++;
    }

}

程序运行结果

原子性测试
main	 int type finally number value: 19077
从字节码角度解释原子性
  • java 源代码
/**
* @ClassName T1
* @Description TODO
* @Author Heygo
* @Date 2020/8/7 12:54
* @Version 1.0
*/
public class T1 {

   volatile int n = 0;

   public void add() {
       n++;
   }

}

  • n++ 的字节码指令
 0 aload_0
 1 dup
 2 getfield #2 <com/Heygo/T1.n>
 5 iconst_1
 6 iadd
 7 putfield #2 <com/Heygo/T1.n>
10 return

  • n++ 分为三步
    第一步:执行 getfield 指令拿到主内存中 n 的值

第二步:执行 iadd 指令执行加 1 的操作(线程工作内存中的变量副本值加 1)

第三步:执行 putfield 指令将累加后的 n 值写回主内存

PS :iconst_1 是将常量 1 放入操作数栈中,准备执行 iadd 操作

分析多线程写值,值丢失的原因

1、两个线程:线程 A和线程 B ,同时拿到主内存中 n 的值,并且都执行了加 1 的操作

2、线程 A 先执行 putfield 指令将副本的值写回主内存,线程 B 在线程 A 之后也将副本的值写回主内存

3、此时,就会出现写覆盖、丢失写值的情况

**  解决原子性问题:**

** 两个解决办法:**

1、对 addPlusPlus() 方法加同步锁(加锁这个解决方法太重)

2、使用 Java.util.concurrent.AtomicInteger 类

代码:使用 AtomicInteger 类保证 i++ 操作的原子性:

/**
 * @ClassName VolatileDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/7 10:59
 * @Version 1.0
 */
public class VolatileDemo {

    public static void main(String[] args) {
        atomicDemo();
    }

    /*
    2 验证volatile不保证原子性
        2.1 原子性是不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割。
            需要整体完成,要么同时成功,要么同时失败。

        2.2 volatile不可以保证原子性演示

        2.3 如何解决原子性
            1)加sync
            2)使用我们的JUC下AtomicInteger
     */
    private static void atomicDemo() {
        System.out.println("原子性测试");
        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();
        }
        /*
        需要等待上述20个线程都计算完成后,再用main线程去的最终的结果是多少?
        只要上述20个线程还有在执行的,main线程便礼让,让他们执行,直至最后只剩main线程
         */
        while (Thread.activeCount() > 2) {
            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);
    }
}


class MyData {

    // volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改
    volatile int number = 0;

    public void setTo60() {
        this.number = 60;
    }

    //此时number前面已经加了volatile,但是不保证原子性
    public void addPlusPlus() {
        number++;
    }

    // Integer 原子包装类
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addAtomic() {
        atomicInteger.getAndIncrement();
    }

}

  • 程序运行结果
原子性测试
main	 int type finally number value: 17591
main	 AtomicInteger type finally number value: 20000
瞅瞅 AtomicInteger 源码

先获取再修改

  • getAndIncrement() 方法
public final int getAndIncrement() {
   return unsafe.getAndAddInt(this, valueOffset, 1);
}

  • getAndDecrement() 方法
public final int getAndDecrement() {
   return unsafe.getAndAddInt(this, valueOffset, -1);
}
  • getAndAdd() 方法
public final int getAndAdd(int delta) {
   return unsafe.getAndAddInt(this, valueOffset, delta);
}
  • 总结:以上方法都通过调用 unsafe.getAndAddInt() 实现

先修改再获取

  • incrementAndGet() 方法
public final int incrementAndGet() {
   return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
  • decrementAndGet() 方法
public final int decrementAndGet() {
   return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
  • addAndGet() 方法
public final int addAndGet(int delta) {
   return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

总结:以上方法都通过调用 unsafe.getAndAddInt() + delta 实现

1.2.4、代码重排
有序性

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种
在这里插入图片描述
理解指令重排序

1、指令重排序,就是出于优化考虑,CPU执行指令的顺序跟程序员自己编写的顺序不一致

2、就好比一份试卷,题号是老师规定的,是程序员规定的,但是考生(CPU)可以先做选择,也可以先做填空

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

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

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

重排代码示例

示例 1

  • 代码
public void mySort(){
   int x = 11; //语句1
   int y = 12; //语句2
   x = x + 5;  //语句3
   y = x * x;  //语句4
}

以上代码,可能出现的执行顺序有1234、2134、1342,这三个都没有问题,但是语句 4 不能变成第一条,因为存在数据依赖(y 依赖于 x)。

示例 2

1、在代码中定义了 a, b, x, y 四个整形变量

2、线程 1 原本的执行顺序为 x = a; b = 1; ,线程 2 原本的执行顺序为 y = b; a = 1;

3、但是经过指令重排后,指令执行顺序变化,导致程序执行结果变化

4、这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。
在这里插入图片描述
示例 3
在这里插入图片描述
分析:
1、变量 a 与 flag 并没有数据依赖性,所以 a = 1; 与 flag = true; 语句无法保证谁先谁后

2、线程操作资源类,线程1访问method1,线程2访问method2,正常情况顺序执行,a=6

3、多线程下假设出现了指令重排,语句2在语句1之前,当执行完flag=true后,另一个线程马上执行method2,则会输出 a=5

禁止指令重排案例小结

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

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

3、由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。

4、内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
在这里插入图片描述

1.3、线程安全性保证
如何使线程安全性获得保证

1、工作内存与主内存同步延迟现象导致的可见性问题可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

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

1.4、volatile 单例模式
1.4.1、DCL 单例模式
**DCL模式:Double Check Lock,即双端检索机制:在加锁前后都进行判断**
  • 代码
/**
 * @ClassName SingletonDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/7 15:14
 * @Version 1.0
 */
public class SingletonDemo {

    private static SingletonDemo singletonDemo = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 我是构造方法");
    }

    //DCL模式 Double Check Lock 双端检索机制:在加锁前后都进行判断
    public static SingletonDemo getInstance() {
        if (singletonDemo == null) {
            synchronized (SingletonDemo.class) {
                if (singletonDemo == null) {
                    singletonDemo = new SingletonDemo();
                }
            }
        }
        return singletonDemo;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i + 1)).start();
        }
    }

}

  • 这种写法在多线程条件下可能正确率为 99.999999%,但可能由于指令重排出错
1.4.2、单例volatile 分析
DCL 问题分析:

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

2、原因:可能出现某一个线程执行到第一次检测,读取到的instance不为null时,但是instance的引用对象可能没有完成初始化。原因如下:

3、实例化代码 instance=new SingletonDemo(); 可以分为以下3步完成(伪代码)

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

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

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

5、指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。

6、就比如说我们需要使用 instance 对象中的一个对象 heygo ,但是由于 instance 并未初始化完成,此时 heygo == null ,访问 instance.heygo 将抛出空指针异常

单例模式正确写法:

加上 volatile ,禁止指令重排

private static volatile SingletonDemo singletonDemo=null;

2、CAS 算法

CAS你知道吗?
2.1、CAS 概述
CAS:compare and set(比较并交换)

代码示例

  • 代码
/**
- @ClassName CASDemo
- @Description TODO
- @Author Heygo
- @Date 2020/8/7 15:43
- @Version 1.0
*/
public class CASDemo {
   public static void main(String[] args) {
       /*
        CAS是什么? ==>compareAndSet 比较并交换
       */
       AtomicInteger atomicInteger = new AtomicInteger(5);
       // 期望值与上次相同,修改成功
       System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data : " + atomicInteger.get());
       // 期望值与上次不同,修改失败
       System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t current data : " + atomicInteger.get());
   }
}
  • 程序运行结果
true	 current data : 2019
false	 current data : 2019

分析CAS:就拿 JMM 模型来说

1、现在有两个线程:线程 A 和线程 B ,同时操作主内存中的变量 i

2、线程 A 将变量 i 的副本拷贝回自己线程的工作内存,先记录变量 i 当前的值,记录为期望值

3、线程 A 修改值后,将 i 的值写回主内存前,先判断一下当前主内存的值是否与期望值相等,相等我才写回,不相等证明别的线程(线程 B)改过了,如果强行写,将出现写覆盖

2.2、CAS 原理
2.2.1、Unsafe 类
CAS底层原理?如果知道,谈谈你对Unsafe的理解

一句话总结:自旋锁 + Unsafe 类

AtomicInteger 类的底层源码
  • getAndIncrement() 方法
public final int getAndIncrement() {
   return unsafe.getAndAddInt(this, valueOffset, 1);
}
  • 分析参数含义
    1、this:当前对象
    2、valueOffset:内存偏移量(内存地址)
    3、为什么AtomicInteger能解决i++多线程下不安全的问题,靠的是底层的Unsafe类

  • AtomicInteger 类中维护了一个 Unsafe 实例,和一个 volatile 修饰的 value 值

public class AtomicInteger extends Number implements java.io.Serializable {
   private static final long serialVersionUID = 6214790243416807050L;

   // setup to use Unsafe.compareAndSwapInt for updates
   private static final Unsafe unsafe = Unsafe.getUnsafe();
   private static final long valueOffset;

   static {
       try {
           valueOffset = unsafe.objectFieldOffset
               (AtomicInteger.class.getDeclaredField("value"));
       } catch (Exception ex) { throw new Error(ex); }
   }

   private volatile int value;
Unsafe 类

1、Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。

2、Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,Java中CAS操作的执行依赖于Unsafe类的方法。

3、注意Unsafe类中的所有方法都是native修饰

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

5、变量value用volatile修饰,保证了多线程之间的内存可见性。

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
2.2.2、CAS 是什么
CAS 到底是个什么玩意儿?

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

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

3、CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。

4、再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

AtomicInteger 类 CAS 算法分析
  • 通过 AtomicInteger 类调用 getAndIncrement() 方法
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

1、atomicInteger.getAndIncrement() 方法调用 unsafe.getAndAddInt() 方法

2、this.getIntVolatile(var1,var2) 方法获取var1这个对象在var2地址上的值。

3、this.compareAndSwapInt(var1, var2, var5, var5 + var4) 方法判断 var5 变量是否与期望值相同:
如果 var5 与内存中的期望值相同,证明没有其他线程改过,则执行 +var 操作
如果 var5 与内存中的期望值不同,证明没有其他线程改过 var2 地址处的值,然后再重新获取 var2 地址处的值,重复 compare and set 操作

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;
}
  • 总结:getAndIncrement()方法底层调用的是Unsafe类的getAndAddInt()方法,底层是CAS思想
atomicInteger.getAndIncrement() 方法详解
  • AtomicInteger 类的 getAndIncrement() 方法
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

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

流程分析:
1、var1:Atomiclnteger对象本身。
2、var2:该对象值得引用地址。
3、var4:需要变动的数量。
4、var5:使用var1 var2找出的主内存中真实的值。
用该对象当前的值与var5比较:
如果相同,更新var5 + var4并且返回true,
如果不同,继续取值然后再比较,直到更新完成。

举例说明:
1、假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上):

2、AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本,分别拷贝到各自的工作内存。

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

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

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

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

底层汇编指令

1、Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中
2、Atomic:cmpxchg 指令:但凡带 Atomic 汇编指令都是不会被其他线程打断
在这里插入图片描述

CAS 简单小总结

CAS(CompareAndSwap)

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

CAS应用

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

2.3、CAS 缺点
1、循环时间长开销很大

我们可以看到getAndAddInt方法执行时,有个do while

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

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;
}
2、只能保证一个共享变量的原子操作

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

3、引出来ABA问题?
2.4、面试题
为什么用 CAS 而不用synchronized?

以下是我的理解

1、使用 synchronized 虽然能保证操作的原子性,但是将操作变成了串行操作,大大降低了程序的并发性

2、如果使用 synchronized 没有抢到同步锁,那么线程将处于阻塞状态,等待 CPU 的下一次调度

3、CAS 使用 Unsafe 类 + 自旋锁实现操作的原子性,Unsafe 类中使用 do while 循环实现 compare and set ,多个线程可以同时操作,大大提高了程序的并发性,并且不存在让线程等待的问题

3、ABA 问题

原子类AtomicInteger的ABA问题?原子更新引用知道吗?
3.1、ABA 问题的产生
面试坑爹套路

CAS —> UnSafe —> CAS底层思想 —> ABA —> 原子引用更新 —> 如何规避ABA问题

ABA问题是怎样产生的?

CAS会导致 ABA 问题

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

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

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

4、一句话总结:狸猫换太子

3.2、原子引用
原子引用代码示例
  • 代码:使用 AtomicReference 原子引用类封装我们自定义的 User 类
/**
 * @ClassName AtomicReferenceDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/7 18:45
 * @Version 1.0
 */
public class AtomicReferenceDemo {

    public static void main(String[] args) {
        AtomicReference<User> atomicReference = new AtomicReference<>();

        User z3 = new User("z3", 23);
        User l4 = new User("l4", 24);
        User w5 = new User("w5", 25);


        atomicReference.set(z3);
        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(z3, w5) + "\t" + atomicReference.get().toString());
    }

}

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 +
                '}';
    }
}
  • 程序运行结果
true	User{userName='l4', age=24}
false	User{userName='l4', age=24}
3.3、版本号原子引用
解决ABA问题:理解原子引用 + 新增一种机制,那就是修改版本号(类似时间戳)
  • 代码:使用带版本号的原子类 AtomicStampedReference 解决 ABA 问题
/**
 - @ClassName ABADemo
 - @Description TODO
 - @Author Heygo
 - @Date 2020/8/7 21:08
 - @Version 1.0
 */
public class ABADemo {
    // 初始值为 100
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    // 初始值为 100 ,初始版本号为 1
    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秒钟线程2,保证上面t1线程完成一次ABA操作
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
        }, "t2").start();

        // 保证上面的操作执行完成
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println("======以下是ABA问题的解决=====");
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + atomicStampedReference.getStamp());
            // 暂停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线程,保证上面t3线程完成一次ABA操作
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t修改成功否: " + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName() + "\t当前实际值:" + atomicStampedReference.getReference());
        }, "t4").start();
    }
}
  • 程序运行结果
======ABA问题的产生======
true	2019
======以下是ABA问题的解决=====
t3	第1次版本号:1
t4	第1次版本号:1
t3	第2次版本号:2
t3	第3次版本号:3
t4	修改成功否: false	当前最新实际版本号:3
t4	当前实际值:100
关于 AtomicStampedReference 的一些说明

1、AtomicStampedReference 的构造器
2、initialRef:初始值
3、initialStamp:初始版本号

public AtomicStampedReference(V initialRef, int initialStamp) {
    pair = Pair.of(initialRef, initialStamp);
}

1、compareAndSet() 方法
2、expectedReference:期望值
3、newReference:新值
4、expectedStamp:期望版本号
5、newStamp:新的版本号

    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

4、集合框架

我们知道ArrayList是线程不安全,请编码一个不安全的案例并给出解决方案
4.1、ArrayList 不安全
我们知道ArrayList是线程不安全,请编码一个不安全的案例并给出解决方案
  • 代码
/**
* @ClassName ContainerNotSafeDemo
* @Description TODO
* @Author Heygo
* @Date 2020/8/7 21:35
* @Version 1.0
*/
public class ContainerNotSafeDemo {

   /*
    * 1 故障现象
    *   java.util.ConcurrentModificationException
    *
    * 2 导致原因
    *   并发争抢修改导致,参考我们的花名册签名情况。
    *   一个人正在写入,另一个同学过来抢夺,导致数据不一致异常。并发修改异常。
    *
    * */
   public static void main(String[] args) {
       listNotSafe();
   }

   private static void listNotSafe() {
       List<String> list=new ArrayList<>();
       // java.util.ConcurrentModificationException
       for (int i = 1; i <= 30; i++) {
           new Thread(() -> {
               list.add(UUID.randomUUID().toString().substring(0, 8));
               System.out.println(Thread.currentThread().getName() + "\t" + list);
           }, String.valueOf(i)).start();
       }
   }

}
  • 程序运行结果:由于 ArrayList 类的 add() 方法没有加锁,所以存在多线程并发安全问题
java.util.ConcurrentModificationException
   at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
   at java.util.ArrayList$Itr.next(ArrayList.java:851)
   at java.util.AbstractCollection.toString(AbstractCollection.java:461)
   at java.lang.String.valueOf(String.java:2994)
   at java.lang.StringBuilder.append(StringBuilder.java:131)
   at com.Heygo.ContainerNotSafeDemo.lambda$listNotSafe$0(ContainerNotSafeDemo.java:26)
   at java.lang.Thread.run(Thread.java:748)

解决问题 ArrayList 线程不安全

1、使用 new Vector<>();(ArrayList所有方法加synchronized,太重)。

2、使用 Collections.synchronizedList(new ArrayList<>()); 转换成线程安全类。

3、使用 new java.concurrent.CopyOnWriteArrayList<>();(推荐)。

CopyOnWriteArrayList

CopyOnWriteArrayList 写时复制

1、写时复制:CopyOnWrite容器,即写时复制的容器。

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

3、这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。

4、所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList 代码示例

  • 代码:使用 CopyOnWriteArrayList 集合类,保证 ArrayList 并发修改安全性的同时,也保证了并发读取的效率
/**
* @ClassName ContainerNotSafeDemo
* @Description TODO
* @Author Heygo
* @Date 2020/8/7 21:35
* @Version 1.0
*/
public class ContainerNotSafeDemo {

   /*
    * 1 故障现象
    *   java.util.ConcurrentModificationException
    *
    * 2 导致原因
    *   并发争抢修改导致,参考我们的花名册签名情况。
    *   一个人正在写入,另一个同学过来抢夺,导致数据不一致异常。并发修改异常。
    *
    * 3 解决方案
    *   3.1 new Vector<>();
    *   3.2 集合工具类:Collections.synchronizedList(new ArrayList<>());
    *   3.3 new CopyOnWriteArrayList<>()
    *       写时复制:CopyOnWrite容器即写时复制的容器。
    *       往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前object[]进行Copy,
    *       复制出一个新的容器Object[] newElements,然后新的容器Object[] newElements里添加元素,
    *       添加完元素之后,再将原容器的引用指向新的容器setArray(newElements);
    *       这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
    *       所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
    *
    * 4 优化建议(同样的错误不犯两次)
    *
    * */
   public static void main(String[] args) {
       listNotSafe();
   }

   private static void listNotSafe() {
       List<String> list = new CopyOnWriteArrayList<>();
       for (int i = 1; i <= 30; i++) {
           new Thread(() -> {
               list.add(UUID.randomUUID().toString().substring(0, 8));
               System.out.println(Thread.currentThread().getName() + "\t" + list);
           }, String.valueOf(i)).start();
       }
   }

}
  • 程序运行结果
5	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72]
8	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9]
10	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9]
2	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9]
3	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72]
12	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02]
7	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e]
4	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72]
15	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a]
14	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e]
13	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b]
1	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e]
22	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9, a4cf1f6e, 5427a83a, 250c0143, 4c32f82d, a76fe96a]
11	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e]
6	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803]
9	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64]
27	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9, a4cf1f6e, 5427a83a, 250c0143, 4c32f82d, a76fe96a, e28f0c36, ee151ef4, 1ce730cc, 5807293f, 8163070f]
26	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9, a4cf1f6e, 5427a83a, 250c0143, 4c32f82d, a76fe96a, e28f0c36, ee151ef4, 1ce730cc, 5807293f]
25	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9, a4cf1f6e, 5427a83a, 250c0143, 4c32f82d, a76fe96a, e28f0c36, ee151ef4, 1ce730cc]
24	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9, a4cf1f6e, 5427a83a, 250c0143, 4c32f82d, a76fe96a, e28f0c36, ee151ef4]
23	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9, a4cf1f6e, 5427a83a, 250c0143, 4c32f82d, a76fe96a, e28f0c36]
21	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9, a4cf1f6e, 5427a83a, 250c0143, 4c32f82d]
20	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9, a4cf1f6e, 5427a83a, 250c0143]
19	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9, a4cf1f6e, 5427a83a]
18	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9, a4cf1f6e]
17	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9]
16	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3]
30	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9, a4cf1f6e, 5427a83a, 250c0143, 4c32f82d, a76fe96a, e28f0c36, ee151ef4, 1ce730cc, 5807293f, 8163070f, 8bc1cbfc, 58caaadd]
29	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9, a4cf1f6e, 5427a83a, 250c0143, 4c32f82d, a76fe96a, e28f0c36, ee151ef4, 1ce730cc, 5807293f, 8163070f, 8bc1cbfc, 58caaadd, ce11ccb2]
28	[ce629c0f, 195253cd, 9d98dc22, b3ed3b72, 1542d43e, 1494f2e9, aa7e9f64, 413cf9d9, ce7e5748, 1feaa74e, 6d40a803, 5fa45f02, 86971d8b, 14886d8e, 232c6e3a, ecf34ff3, 9aa964b9, a4cf1f6e, 5427a83a, 250c0143, 4c32f82d, a76fe96a, e28f0c36, ee151ef4, 1ce730cc, 5807293f, 8163070f, 8bc1cbfc]
ArrayList 源码分析
  • 初始化时,构造了一个空的 Object[] 数组
    /**
    * Shared empty array instance used for default sized empty instances. We
    * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
    * first element is added.
    */
   private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

   /**
    * Constructs an empty list with an initial capacity of ten.
    */
   public ArrayList() {
       this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
   }
  • ArrayList 中使用 Object[] 数组存放数据
    /**
    * The array buffer into which the elements of the ArrayList are stored.
    * The capacity of the ArrayList is the length of this array buffer. Any
    * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    * will be expanded to DEFAULT_CAPACITY when the first element is added.
    */
   transient Object[] elementData; // non-private to simplify nested class access
   ```
   

- 第一次添加元素时,初始化 Object[] 数组的大小为 DEFAULT_CAPACITY = 10
 /**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

/**
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}
```

1、扩容操作
2、每次扩容为旧容量的 1.5 倍
3、ArrayList 最大容量为 Integer.MAX_VALUE - 8
4、使用 Arrays.copyOf()方法扩容,并将将原来数组中的值拷贝到新数组中

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
Collections. synchronizedList() 源码
  • Collections.synchronizedList() 方法:由于 ArrayList 实现了 RandomAccess 接口,所以在方法内部创建了一个 SynchronizedRandomAccessList 的实例
    /**
    * Returns a synchronized (thread-safe) list backed by the specified
    * list.  In order to guarantee serial access, it is critical that
    * <strong>all</strong> access to the backing list is accomplished
    * through the returned list.<p>
    *
    * It is imperative that the user manually synchronize on the returned
    * list when iterating over it:
    * <pre>
    *  List list = Collections.synchronizedList(new ArrayList());
    *      ...
    *  synchronized (list) {
    *      Iterator i = list.iterator(); // Must be in synchronized block
    *      while (i.hasNext())
    *          foo(i.next());
    *  }
    * </pre>
    * Failure to follow this advice may result in non-deterministic behavior.
    *
    * <p>The returned list will be serializable if the specified list is
    * serializable.
    *
    * @param  <T> the class of the objects in the list
    * @param  list the list to be "wrapped" in a synchronized list.
    * @return a synchronized view of the specified list.
    */
   public static <T> List<T> synchronizedList(List<T> list) {
       return (list instanceof RandomAccess ?
               new SynchronizedRandomAccessList<>(list) :
               new SynchronizedList<>(list));
   }
  • SynchronizedRandomAccessList 类是 Collections 类的静态内部类
  • SynchronizedRandomAccessList 的父类为 SynchronizedList 类
  • super(list); 表示调用父类 SynchronizedList 的构造方法
static class SynchronizedRandomAccessList<E>
    extends SynchronizedList<E>
    implements RandomAccess {

    SynchronizedRandomAccessList(List<E> list) {
        super(list);
    }
  • SynchronizedList 类也是 Collections 类的静态内部类
  • super(list); 表示调用父类 SynchronizedCollection 的构造方法
  • SynchronizedList 内部维护了 ArrayList 的引用:this.list = list;
static class SynchronizedList<E>
    extends SynchronizedCollection<E>
    implements List<E> {
    private static final long serialVersionUID = -7754090372962971524L;

    final List<E> list;

    SynchronizedList(List<E> list) {
        super(list);
        this.list = list;
    }
  • SynchronizedCollection 类也是 Collections 类的静态内部类
  • 在 SynchronizedCollection 内部维护了 ArrayList 的引用:this.c = Objects.requireNonNull©;
  • 通过 final Object mutex 这把锁,给集合中的所有方法都加上锁,保证多线程并发的安全性
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
   private static final long serialVersionUID = 3053995032091335093L;

   final Collection<E> c;  // Backing Collection
   final Object mutex;     // Object on which to synchronize

   SynchronizedCollection(Collection<E> c) {
       this.c = Objects.requireNonNull(c);
       mutex = this;
   }

   SynchronizedCollection(Collection<E> c, Object mutex) {
       this.c = Objects.requireNonNull(c);
       this.mutex = Objects.requireNonNull(mutex);
   }

   public int size() {
       synchronized (mutex) {return c.size();}
   }
   public boolean isEmpty() {
       synchronized (mutex) {return c.isEmpty();}
   }
   public boolean contains(Object o) {
       synchronized (mutex) {return c.contains(o);}
   }
   public Object[] toArray() {
       synchronized (mutex) {return c.toArray();}
   }
   public <T> T[] toArray(T[] a) {
       synchronized (mutex) {return c.toArray(a);}
   }

   public Iterator<E> iterator() {
       return c.iterator(); // Must be manually synched by user!
   }

   public boolean add(E e) {
       synchronized (mutex) {return c.add(e);}
   }
   public boolean remove(Object o) {
       synchronized (mutex) {return c.remove(o);}
   }

   public boolean containsAll(Collection<?> coll) {
       synchronized (mutex) {return c.containsAll(coll);}
   }
   public boolean addAll(Collection<? extends E> coll) {
       synchronized (mutex) {return c.addAll(coll);}
   }
   public boolean removeAll(Collection<?> coll) {
       synchronized (mutex) {return c.removeAll(coll);}
   }
   public boolean retainAll(Collection<?> coll) {
       synchronized (mutex) {return c.retainAll(coll);}
   }
   public void clear() {
       synchronized (mutex) {c.clear();}
   }
   public String toString() {
       synchronized (mutex) {return c.toString();}
   }
   // Override default methods in Collection
   @Override
   public void forEach(Consumer<? super E> consumer) {
       synchronized (mutex) {c.forEach(consumer);}
   }
   @Override
   public boolean removeIf(Predicate<? super E> filter) {
       synchronized (mutex) {return c.removeIf(filter);}
   }
   @Override
   public Spliterator<E> spliterator() {
       return c.spliterator(); // Must be manually synched by user!
   }
   @Override
   public Stream<E> stream() {
       return c.stream(); // Must be manually synched by user!
   }
   @Override
   public Stream<E> parallelStream() {
       return c.parallelStream(); // Must be manually synched by user!
   }
   private void writeObject(ObjectOutputStream s) throws IOException {
       synchronized (mutex) {s.defaultWriteObject();}
   }
}
CopyOnWriteArrayList 源码分析
  • CopyOnWriteArrayList 内部维护了两个重要成员变量:
  • ReentrantLock 锁:保证多线程并发修改的安全性
  • Object[] array 数组:使用 volatile 修饰,保证内存的可见性
public class CopyOnWriteArrayList<E>
   implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
   private static final long serialVersionUID = 8673264195747942595L;

   /** The lock protecting all mutators */
   final transient ReentrantLock lock = new ReentrantLock();

   /** The array, accessed only via getArray/setArray. */
   private transient volatile Object[] array;

   /**
    * Gets the array.  Non-private so as to also be accessible
    * from CopyOnWriteArraySet class.
    */
   final Object[] getArray() {
       return array;
   }

   /**
    * Sets the array.
    */
   final void setArray(Object[] a) {
       array = a;
   }

   /**
    * Creates an empty list.
    */
   public CopyOnWriteArrayList() {
       setArray(new Object[0]);
   }

添加元素:
上锁{
数组长度扩容 1 个元素,将旧元素拷贝至新数组与中,将新元素放在数组末尾
修改 rivate transient volatile Object[] array; 的引用
}解锁

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
  • 由于采用了读写分离,所以读取集合无需加锁,提高了读的并发性
public String toString() {
   return Arrays.toString(getArray());
}
  • 代码
/**
- @ClassName ContainerNotSafeDemo
- @Description TODO
- @Author Heygo
- @Date 2020/8/7 21:35
- @Version 1.0
*/
public class ContainerNotSafeDemo {
   public static void main(String[] args) {
       setNoSafe();
   }

   private static void setNoSafe() {
       Set<String> set=new HashSet<>();
       for (int i = 1; i <= 30; i++) {
           new Thread(() -> {
               set.add(UUID.randomUUID().toString().substring(0, 8));
               System.out.println(Thread.currentThread().getName() + "\t" + set);
           }, String.valueOf(i)).start();
       }
   }
}
  • 程序运行结果:java.util.ConcurrentModificationException
java.util.ConcurrentModificationException
   at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
   at java.util.HashMap$KeyIterator.next(HashMap.java:1461)
   at java.util.AbstractCollection.toString(AbstractCollection.java:461)
   at java.lang.String.valueOf(String.java:2994)
   at java.lang.StringBuilder.append(StringBuilder.java:131)
   at com.Heygo.ContainerNotSafeDemo.lambda$setNoSafe$2(ContainerNotSafeDemo.java:71)
   at java.lang.Thread.run(Thread.java:748)
解决 HashSet 线程不安全问题
  • 使用 CollectionssynchronizedSet() 方法将 HashSet 转为线程安全版本
  • 使用 CopyOnWriteArraySet 类:读写分离

CopyOnWriteArraySet 代码示例

  • 代码
/**
 - @ClassName ContainerNotSafeDemo
 - @Description TODO
 - @Author Heygo
 - @Date 2020/8/7 21:35
 - @Version 1.0
 */
public class ContainerNotSafeDemo {
    public static void main(String[] args) {
        setNoSafe();
    }

    private static void setNoSafe() {
        Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(Thread.currentThread().getName() + "\t" + set);
            }, String.valueOf(i)).start();
        }
    }
}
  • 程序运行结果
1	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb]
7	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a]
16	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177]
18	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6, 23fbb23e]
15	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a]
21	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6, 23fbb23e, ec60cb89, 73bd78e7, 3f6ded3e]
4	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7]
3	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7]
2	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7]
13	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7]
24	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6, 23fbb23e, ec60cb89, 73bd78e7, 3f6ded3e, f995ef22, 866bc698, 22177284, 6252e23d]
8	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9]
14	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9]
30	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6, 23fbb23e, ec60cb89, 73bd78e7, 3f6ded3e, f995ef22, 866bc698, 22177284, 6252e23d, 85e01c3e, 0edf78f9, a0c65913]
5	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d]
11	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d]
9	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb]
12	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24]
26	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6, 23fbb23e, ec60cb89, 73bd78e7, 3f6ded3e, f995ef22, 866bc698, 22177284, 6252e23d, 85e01c3e, 0edf78f9, a0c65913, 9ae99682, 84b58b16]
25	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6, 23fbb23e, ec60cb89, 73bd78e7, 3f6ded3e, f995ef22, 866bc698, 22177284, 6252e23d, 85e01c3e, 0edf78f9, a0c65913, 9ae99682]
29	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6, 23fbb23e, ec60cb89, 73bd78e7, 3f6ded3e, f995ef22, 866bc698, 22177284, 6252e23d, 85e01c3e, 0edf78f9, a0c65913, 9ae99682]
28	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6, 23fbb23e, ec60cb89, 73bd78e7, 3f6ded3e, f995ef22, 866bc698, 22177284, 6252e23d, 85e01c3e, 0edf78f9]
27	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6, 23fbb23e, ec60cb89, 73bd78e7, 3f6ded3e, f995ef22, 866bc698, 22177284, 6252e23d, 85e01c3e]
22	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6, 23fbb23e, ec60cb89, 73bd78e7, 3f6ded3e, f995ef22, 866bc698]
23	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6, 23fbb23e, ec60cb89, 73bd78e7, 3f6ded3e, f995ef22]
20	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6, 23fbb23e, ec60cb89, 73bd78e7]
19	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6, 23fbb23e, ec60cb89]
17	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a, 25b9d177, e5567ae6]
6	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb]
10	[d9f8ebbc, 1d6205eb, 836575a9, ce40bd87, 7bacd0f6, 57d5347f, e1676b1c, c10c5256, 247ff963, 3a5b3feb, ce846b2d, 9e050a24, 0e7e56e9, bf76ebc7, 6072428a]
HashSet 源码分析
  • HashSet 的构造器:底层维护了一个负载因子为 0.75 的 HashMap
public class HashSet<E>
   extends AbstractSet<E>
   implements Set<E>, Cloneable, java.io.Serializable
{
   static final long serialVersionUID = -5024744406713321676L;

   private transient HashMap<E,Object> map;

   // Dummy value to associate with an Object in the backing Map
   private static final Object PRESENT = new Object();

   /**
    * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
    * default initial capacity (16) and load factor (0.75).
    */
   public HashSet() {
       map = new HashMap<>();
   }
  • add() 方法:
    • key 为待添加的元素
    • value 统一为 private static final Object PRESENT = new Object();
    /**
    * Adds the specified element to this set if it is not already present.
    * More formally, adds the specified element <tt>e</tt> to this set if
    * this set contains no element <tt>e2</tt> such that
    * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
    * If this set already contains the element, the call leaves the set
    * unchanged and returns <tt>false</tt>.
    *
    * @param e element to be added to this set
    * @return <tt>true</tt> if this set did not already contain the specified
    * element
    */
   public boolean add(E e) {
       return map.put(e, PRESENT)==null;
   }
Collections.synchronizedSet() 源码分析
  • Collections.synchronizedSet() 方法创建了一个 synchronizedSet 类的实例
    /**
    * Returns a synchronized (thread-safe) set backed by the specified
    * set.  In order to guarantee serial access, it is critical that
    * <strong>all</strong> access to the backing set is accomplished
    * through the returned set.<p>
    *
    * It is imperative that the user manually synchronize on the returned
    * set when iterating over it:
    * <pre>
    *  Set s = Collections.synchronizedSet(new HashSet());
    *      ...
    *  synchronized (s) {
    *      Iterator i = s.iterator(); // Must be in the synchronized block
    *      while (i.hasNext())
    *          foo(i.next());
    *  }
    * </pre>
    * Failure to follow this advice may result in non-deterministic behavior.
    *
    * <p>The returned set will be serializable if the specified set is
    * serializable.
    *
    * @param  <T> the class of the objects in the set
    * @param  s the set to be "wrapped" in a synchronized set.
    * @return a synchronized view of the specified set.
    */
   public static <T> Set<T> synchronizedSet(Set<T> s) {
       return new SynchronizedSet<>(s);
   }
   
  • SynchronizedSet 类是 Collections 的静态内部类
  • SynchronizedSet 类的父类是 SynchronizedCollection 类
  • super(s); 调用父类构造器
static class SynchronizedSet<E>
    extends SynchronizedCollection<E>
    implements Set<E> {
    private static final long serialVersionUID = 487447009682186044L;

    SynchronizedSet(Set<E> s) {
        super(s);
    }
  • SynchronizedSet 和 SynchronizedList 都继承自 SynchronizedCollection 类,均是通过 mutex 这把锁解决了多线程并发修改的安全问题
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
    private static final long serialVersionUID = 3053995032091335093L;

    final Collection<E> c;  // Backing Collection
    final Object mutex;     // Object on which to synchronize

    SynchronizedCollection(Collection<E> c) {
        this.c = Objects.requireNonNull(c);
        mutex = this;
    }

    SynchronizedCollection(Collection<E> c, Object mutex) {
        this.c = Objects.requireNonNull(c);
        this.mutex = Objects.requireNonNull(mutex);
    }

    public int size() {
        synchronized (mutex) {return c.size();}
    }
    public boolean isEmpty() {
        synchronized (mutex) {return c.isEmpty();}
    }
    public boolean contains(Object o) {
        synchronized (mutex) {return c.contains(o);}
    }
    public Object[] toArray() {
        synchronized (mutex) {return c.toArray();}
    }
    public <T> T[] toArray(T[] a) {
        synchronized (mutex) {return c.toArray(a);}
    }

    public Iterator<E> iterator() {
        return c.iterator(); // Must be manually synched by user!
    }

    public boolean add(E e) {
        synchronized (mutex) {return c.add(e);}
    }
    public boolean remove(Object o) {
        synchronized (mutex) {return c.remove(o);}
    }

    public boolean containsAll(Collection<?> coll) {
        synchronized (mutex) {return c.containsAll(coll);}
    }
    public boolean addAll(Collection<? extends E> coll) {
        synchronized (mutex) {return c.addAll(coll);}
    }
    public boolean removeAll(Collection<?> coll) {
        synchronized (mutex) {return c.removeAll(coll);}
    }
    public boolean retainAll(Collection<?> coll) {
        synchronized (mutex) {return c.retainAll(coll);}
    }
    public void clear() {
        synchronized (mutex) {c.clear();}
    }
    public String toString() {
        synchronized (mutex) {return c.toString();}
    }
    // Override default methods in Collection
    @Override
    public void forEach(Consumer<? super E> consumer) {
        synchronized (mutex) {c.forEach(consumer);}
    }
    @Override
    public boolean removeIf(Predicate<? super E> filter) {
        synchronized (mutex) {return c.removeIf(filter);}
    }
    @Override
    public Spliterator<E> spliterator() {
        return c.spliterator(); // Must be manually synched by user!
    }
    @Override
    public Stream<E> stream() {
        return c.stream(); // Must be manually synched by user!
    }
    @Override
    public Stream<E> parallelStream() {
        return c.parallelStream(); // Must be manually synched by user!
    }
    private void writeObject(ObjectOutputStream s) throws IOException {
        synchronized (mutex) {s.defaultWriteObject();}
    }
}
CopyOnWriteArraySet 源码分析
  • CopyOnWriteArraySet 内部维护了一个 CopyOnWriteArrayList 实例,典型的挂羊皮卖狗肉
public class CopyOnWriteArraySet<E> extends AbstractSet<E>
        implements java.io.Serializable {
    private static final long serialVersionUID = 5457747651344034263L;

    private final CopyOnWriteArrayList<E> al;

    /**
     * Creates an empty set.
     */
    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }
  • copyOnWriteArraySet.add() 方法:调用 copyOnWriteArrayList.addIfAbsent() 方法
	// CopyOnWriteArraySet 类的 add() 方法
	/**
     * Adds the specified element to this set if it is not already present.
     * More formally, adds the specified element {@code e} to this set if
     * the set contains no element {@code e2} such that
     * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
     * If this set already contains the element, the call leaves the set
     * unchanged and returns {@code false}.
     *
     * @param e element to be added to this set
     * @return {@code true} if this set did not already contain the specified
     *         element
     */
    public boolean add(E e) {
        return al.addIfAbsent(e);
    }
  • copyOnWriteArrayList.addIfAbsent() 方法:
    • 获取 Object[] 数组的快照
    • 将元素添加至 ArrayList(这里看不太懂)
    /**
     * Appends the element, if not present.
     *
     * @param e element to be added to this list, if absent
     * @return {@code true} if the element was added
     */
    public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
            addIfAbsent(e, snapshot);
    }

    /**
     * A version of addIfAbsent using the strong hint that given
     * recent snapshot does not contain e.
     */
    private boolean addIfAbsent(E e, Object[] snapshot) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // Optimize for lost race to another addXXX operation
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i] && eq(e, current[i]))
                        return false;
                if (indexOf(e, current, common, len) >= 0)
                        return false;
            }
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
4.3、HashMap 线程不安全
演示 HashMap 线程不安全
  • 代码
/**
- @ClassName ContainerNotSafeDemo
- @Description TODO
- @Author Heygo
- @Date 2020/8/7 21:35
- @Version 1.0
*/
public class ContainerNotSafeDemo {
   public static void main(String[] args) {
       mapNotSafe();
   }

   private static void mapNotSafe() {
       Map<String,String> map=new HashMap<>();
       for (int i = 1; i <= 30; i++) {
           new Thread(() -> {
               map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
               System.out.println(Thread.currentThread().getName() + "\t" + map);
           }, String.valueOf(i)).start();
       }
   }
}
  • 程序运行结果:java.util.ConcurrentModificationException
java.util.ConcurrentModificationException
	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
	at java.util.HashMap$EntryIterator.next(HashMap.java:1471)
	at java.util.HashMap$EntryIterator.next(HashMap.java:1469)
	at java.util.AbstractMap.toString(AbstractMap.java:554)
	at java.lang.String.valueOf(String.java:2994)
	at java.lang.StringBuilder.append(StringBuilder.java:131)
	at com.Heygo.ContainerNotSafeDemo.lambda$mapNotSafe$1(ContainerNotSafeDemo.java:60)
	at java.lang.Thread.run(Thread.java:748)
解决 HashMap 线程不安全
  • 代码:使用 ConcurrentHashMap 类
/**
 - @ClassName ContainerNotSafeDemo
 - @Description TODO
 - @Author Heygo
 - @Date 2020/8/7 21:35
 - @Version 1.0
 */
public class ContainerNotSafeDemo {
    public static void main(String[] args) {
        mapNotSafe();
    }

    private static void mapNotSafe() {
        Map<String,String> map=new ConcurrentHashMap<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
                System.out.println(Thread.currentThread().getName() + "\t" + map);
            }, String.valueOf(i)).start();
        }
    }
}
  • 程序运行结果
12	{11=ae36bafc, 12=16de3027, 13=dcb688cc, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
16	{11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
4	{11=ae36bafc, 12=16de3027, 1=bf7bc1a8, 13=dcb688cc, 2=d479c1fe, 14=ba2e61d8, 3=f036e44c, 15=f9c89bc1, 4=71eba0d9, 16=0c7d1777, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
19	{11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
6	{11=ae36bafc, 12=16de3027, 1=bf7bc1a8, 13=dcb688cc, 2=d479c1fe, 14=ba2e61d8, 3=f036e44c, 15=f9c89bc1, 4=71eba0d9, 16=0c7d1777, 5=8d4fab3b, 17=7f64e3ee, 6=ad508f93, 7=cb593b6f, 19=801d369c, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
7	{11=ae36bafc, 12=16de3027, 1=bf7bc1a8, 13=dcb688cc, 2=d479c1fe, 14=ba2e61d8, 3=f036e44c, 15=f9c89bc1, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
1	{11=ae36bafc, 12=16de3027, 1=bf7bc1a8, 13=dcb688cc, 2=d479c1fe, 14=ba2e61d8, 3=f036e44c, 15=f9c89bc1, 4=71eba0d9, 16=0c7d1777, 5=8d4fab3b, 17=7f64e3ee, 6=ad508f93, 18=3c3c1d41, 7=cb593b6f, 19=801d369c, 8=8c1542fa, 9=31d873ac, 20=6926e455, 10=8f6933de, 21=f8e65c72}
22	{11=ae36bafc, 22=3af2e92c, 12=16de3027, 23=e96165a6, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 17=7f64e3ee, 18=3c3c1d41, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 20=6926e455, 10=8f6933de, 21=f8e65c72}
25	{22=3af2e92c, 23=e96165a6, 25=71972304, 10=8f6933de, 11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 17=7f64e3ee, 18=3c3c1d41, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 20=6926e455, 21=f8e65c72}
2	{11=ae36bafc, 12=16de3027, 1=bf7bc1a8, 13=dcb688cc, 2=d479c1fe, 14=ba2e61d8, 3=f036e44c, 15=f9c89bc1, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
28	{22=3af2e92c, 23=e96165a6, 24=961abac9, 25=71972304, 26=a58c6246, 27=0b742cd8, 28=efe599df, 10=8f6933de, 11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 17=7f64e3ee, 18=3c3c1d41, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 20=6926e455, 21=f8e65c72}
3	{11=ae36bafc, 12=16de3027, 1=bf7bc1a8, 13=dcb688cc, 2=d479c1fe, 14=ba2e61d8, 3=f036e44c, 15=f9c89bc1, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
15	{11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
14	{11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
30	{22=3af2e92c, 23=e96165a6, 24=961abac9, 25=71972304, 26=a58c6246, 27=0b742cd8, 28=efe599df, 29=ecd71129, 30=7c2cc2a8, 10=8f6933de, 11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 17=7f64e3ee, 18=3c3c1d41, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 20=6926e455, 21=f8e65c72}
29	{22=3af2e92c, 23=e96165a6, 24=961abac9, 25=71972304, 26=a58c6246, 27=0b742cd8, 28=efe599df, 29=ecd71129, 10=8f6933de, 11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 17=7f64e3ee, 18=3c3c1d41, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 20=6926e455, 21=f8e65c72}
24	{22=3af2e92c, 23=e96165a6, 24=961abac9, 25=71972304, 26=a58c6246, 27=0b742cd8, 10=8f6933de, 11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 17=7f64e3ee, 18=3c3c1d41, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 20=6926e455, 21=f8e65c72}
26	{22=3af2e92c, 23=e96165a6, 25=71972304, 26=a58c6246, 10=8f6933de, 11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 17=7f64e3ee, 18=3c3c1d41, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 20=6926e455, 21=f8e65c72}
27	{22=3af2e92c, 23=e96165a6, 24=961abac9, 25=71972304, 26=a58c6246, 27=0b742cd8, 10=8f6933de, 11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 17=7f64e3ee, 18=3c3c1d41, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 20=6926e455, 21=f8e65c72}
23	{11=ae36bafc, 12=16de3027, 23=e96165a6, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 17=7f64e3ee, 18=3c3c1d41, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 20=6926e455, 10=8f6933de, 21=f8e65c72}
5	{11=ae36bafc, 12=16de3027, 1=bf7bc1a8, 13=dcb688cc, 2=d479c1fe, 14=ba2e61d8, 3=f036e44c, 15=f9c89bc1, 4=71eba0d9, 16=0c7d1777, 5=8d4fab3b, 17=7f64e3ee, 6=ad508f93, 18=3c3c1d41, 7=cb593b6f, 19=801d369c, 8=8c1542fa, 9=31d873ac, 20=6926e455, 10=8f6933de, 21=f8e65c72}
13	{11=ae36bafc, 12=16de3027, 1=bf7bc1a8, 13=dcb688cc, 2=d479c1fe, 14=ba2e61d8, 3=f036e44c, 15=f9c89bc1, 4=71eba0d9, 16=0c7d1777, 5=8d4fab3b, 17=7f64e3ee, 6=ad508f93, 18=3c3c1d41, 7=cb593b6f, 19=801d369c, 8=8c1542fa, 9=31d873ac, 20=6926e455, 10=8f6933de, 21=f8e65c72}
21	{11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 17=7f64e3ee, 18=3c3c1d41, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 20=6926e455, 10=8f6933de, 21=f8e65c72}
20	{11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 17=7f64e3ee, 18=3c3c1d41, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 20=6926e455, 10=8f6933de}
10	{11=ae36bafc, 12=16de3027, 1=bf7bc1a8, 13=dcb688cc, 2=d479c1fe, 14=ba2e61d8, 3=f036e44c, 15=f9c89bc1, 4=71eba0d9, 16=0c7d1777, 5=8d4fab3b, 17=7f64e3ee, 6=ad508f93, 18=3c3c1d41, 7=cb593b6f, 19=801d369c, 8=8c1542fa, 9=31d873ac, 20=6926e455, 10=8f6933de}
18	{11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 17=7f64e3ee, 18=3c3c1d41, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 20=6926e455, 10=8f6933de}
9	{11=ae36bafc, 12=16de3027, 1=bf7bc1a8, 13=dcb688cc, 2=d479c1fe, 14=ba2e61d8, 3=f036e44c, 15=f9c89bc1, 4=71eba0d9, 16=0c7d1777, 5=8d4fab3b, 17=7f64e3ee, 6=ad508f93, 18=3c3c1d41, 7=cb593b6f, 19=801d369c, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
17	{11=ae36bafc, 12=16de3027, 13=dcb688cc, 14=ba2e61d8, 15=f9c89bc1, 16=0c7d1777, 17=7f64e3ee, 19=801d369c, 1=bf7bc1a8, 2=d479c1fe, 3=f036e44c, 4=71eba0d9, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
8	{11=ae36bafc, 12=16de3027, 1=bf7bc1a8, 13=dcb688cc, 2=d479c1fe, 14=ba2e61d8, 3=f036e44c, 15=f9c89bc1, 4=71eba0d9, 16=0c7d1777, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 19=801d369c, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
11	{11=ae36bafc, 12=16de3027, 1=bf7bc1a8, 13=dcb688cc, 2=d479c1fe, 14=ba2e61d8, 3=f036e44c, 15=f9c89bc1, 4=71eba0d9, 16=0c7d1777, 5=8d4fab3b, 6=ad508f93, 7=cb593b6f, 8=8c1542fa, 9=31d873ac, 10=8f6933de}
HashMap 底层源码
  • 参考资料:https://www.cnblogs.com/iwenwen/p/11052708.html
  • HashMap 中维护了一个 KV 链表数组:Node<K,V>[]
transient Node<K,V>[] table;

构造方法

  • 默认构造函数:负载因子为 0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

   /**
    * Constructs an empty <tt>HashMap</tt> with the default initial capacity
    * (16) and the default load factor (0.75).
    */
   public HashMap() {
       this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
   }
  • 通过另一个 Map 创建 HashMap
// 包含另一个“Map”的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
   this.loadFactor = DEFAULT_LOAD_FACTOR;
   putMapEntries(m, false);//下面会分析到这个方法
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
   int s = m.size();
   if (s > 0) {
       // 判断table是否已经初始化
       if (table == null) { // pre-size
           // 未初始化,s为m的实际元素个数
           float ft = ((float)s / loadFactor) + 1.0F;
           int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                   (int)ft : MAXIMUM_CAPACITY);
           // 计算得到的t大于阈值,则初始化阈值
           if (t > threshold)
               threshold = tableSizeFor(t);
       }
       // 已初始化,并且m元素个数大于阈值,进行扩容处理
       else if (s > threshold)
           resize();
       // 将m中的所有元素添加至HashMap中
       for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
           K key = e.getKey();
           V value = e.getValue();
           putVal(hash(key), key, value, false, evict);
       }
   }
}
  • 指定初始容量大小的构造函数
// 指定“容量大小”的构造函数
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
  • 指定初始容量大小和负载因子的构造函数
// 指定“容量大小”和“加载因子”的构造函数
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

添加元素

  • put() 方法添加元素:
    • 如果 tab 数组为空或者长度为 0 ,则扩容
    • 如果定位到的数组位置没有元素 就直接插入
    • 如果定位到的数组位置有元素就和要插入的key比较
      • 如果key相同就直接覆盖
      • 如果key不相同,就判断p是否是一个树节点
    • -如果是就调用 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 将元素添加进 HashMap 中
    • -如果不是就遍历链表插入(插入在链表尾部)
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素
    else {
        Node<K,V> e; K k;
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
        // hash值不相等,即key不相等;为红黑树结点
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 为链表结点
        else {
            // 在链表最末插入结点
            for (int binCount = 0; ; ++binCount) {
                // 到达链表的尾部
                if ((e = p.next) == null) {
                    // 在尾部插入新结点
                    p.next = newNode(hash, key, value, null);
                    // 结点数量达到阈值,转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) { 
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
} 
  • put 方法流程图
    在这里插入图片描述
  • get 方法:如果数组对应位置为 null ,则直接返回 null ,否则去对应链表或者红黑树中去取
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 数组元素相等
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 桶中不止一个节点
        if ((e = first.next) != null) {
            // 在树中get
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 在链表中get
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}


  • resize方法:进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 超过最大值就不再扩充了,就只好随你碰撞去吧
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 没超过最大值,就扩充为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else { 
        // signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 计算新的resize上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 把每个bucket都移动到新的buckets中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { 
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 原索引
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
ConcurrentHashMap 底层源码

等我功力提升,再回来看

5、多线程各种锁

公平锁/非公平锁/可重入锁/递归锁/自旋锁,谈谈你的理解?请手写一个自旋锁
5.1、公平锁与非公平锁
是个啥玩意儿?
  1. 公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
  2. 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象
  3. 并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁
    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
公平锁与非公平锁的区别

公平锁

  1. 公平锁:Threads acquire a fair lock in the order in which they requested it
  2. 公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己

非公平锁

  1. 非公平锁:a nonfair lock permits barging:threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested.
  2. 非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
题外话
  1. Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
  2. 对于Synchronized而言,也是一种非公平锁
5.2、可重入锁
可重入锁(又名递归锁)是啥玩意儿?

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

2、就像有了家门的锁,厕所、书房、厨房就为你敞开了一样

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

4、ReentrantLock,synchronized 就是一个典型的可重入锁

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

可重入锁的代码示例

synchronized 示例

  • 代码
/**
* @ClassName RenenterLockDemo
* @Description TODO
* @Author Heygo
* @Date 2020/8/8 13:03
* @Version 1.0
*/

/*
* 可重入锁(也就是递归锁)
*
* 指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,
* 在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
*
* 也就是说,线程可以进入任何一个它已经拥有的锁所有同步着的代码块。
*
* t1   invoked sendSMS()      t1线程在外层方法获取锁的时候
* t1   invoked sendEmail()    t1在进入内层方法会自动获取锁
* t2   invoked sendSMS()
* t2   invoked sendEmail()
*
*/
public class RenenterLockDemo {
   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();
   }

}

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()");
   }
}
  • 程序运行结果
t1	 invoked sendSMS()
t1	 invoked sendEmail()
t2	 invoked sendSMS()
t2	 invoked sendEmail()

ReentrantLock 示例

  • 代码
/**
* @ClassName RenenterLockDemo
* @Description TODO
* @Author Heygo
* @Date 2020/8/8 13:03
* @Version 1.0
*/

/*
* 可重入锁(也就是递归锁)
*
* 指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,
* 在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
*
* 也就是说,线程可以进入任何一个它已经拥有的锁所有同步着的代码块。
*
*/
public class RenenterLockDemo {
   public static void main(String[] args) {
       Phone phone = new Phone();
       new Thread(() -> {
           try {
               phone.get();
           } catch (Exception e) {
               e.printStackTrace();
           }
       }, "t1").start();

       new Thread(() -> {
           try {
               phone.get();
           } catch (Exception e) {
               e.printStackTrace();
           }
       }, "t2").start();
   }

}

class Phone implements Runnable {
   //Reentrant TEST
   Lock lock = new ReentrantLock();

   @Override
   public void run() {
       get();
   }

   public void get() {
       lock.lock();
       try {
           System.out.println(Thread.currentThread().getName() + "\t" + "get()");
           set();
       } finally {
           lock.unlock();
       }
   }

   public void set() {
       lock.lock();
       try {
           System.out.println(Thread.currentThread().getName() + "\t" + "set()");
       } finally {
           lock.unlock();
       }
   }
}
  • 程序运行结果
t1	get()
t1	set()
t2	get()
t2	set()

锁两次,释放两次

  • 代码
class Phone implements Runnable {
    //Reentrant TEST
    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        get();
    }

    public void get() {
        lock.lock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "get()");
            set();
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }

    public void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "set()");
        } finally {
            lock.unlock();
        }
    }
}
  • 程序运行结果:正常执行
t1	get()
t1	set()
t2	get()
t2	set()

锁两次,释放一次

  • 代码
class Phone implements Runnable {
    //Reentrant TEST
    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        get();
    }

    public void get() {
        lock.lock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "get()");
            set();
        } finally {
            lock.unlock();
        }
    }

    public void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "set()");
        } finally {
            lock.unlock();
        }
    }
}
  • 程序运行结果:由于 t1 线程未释放 lock 锁,程序卡死(死锁)
    在这里插入图片描述
    结论:锁几次,就释放几次
5.3、自旋锁
什么是自旋锁?

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

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;
}
自旋锁代码示例
  • 代码:使用 AtomicReference 封装 Thread ,通过 CAS算法实现线程的自旋锁
/**
 - 写一个自旋锁
 - 自旋锁的好处:循环比较获取直到成功为止,没有类似wait的阻塞。
 -  - 通过CAS操作完成自旋锁:
 -  A线程先进来调用myLock方法自已持有锁5秒钟
 -  B随后进来后发现当前有线程持有锁,不是null,
 -  所以只能通过自旋等待,直至A释放锁后B随后抢到
 -  - @ClassName SpinLockDemo
 - @Description TODO
 - @Author Heygo
 - @Date 2020/8/8 13:37
 - @Version 1.0
 */
public class SpinLockDemo {
    // 泛型为 Thread
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock() {
        // 获取当前线程
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in ");
        /*
         自旋:
            期望值为 null 表示当前没有线程
            新值为 thread ,即 Thread.currentThread()
          */
        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();
    }
}
  • 程序运行结果:核心为 CAS 算法
    • 线程 A 先执行,此时期望值为 null ,线程 A 将获得锁,并将期望值设置为线程 A 自身
    • 线程 B 尝试获取锁,发现期望值并不是 null ,就在那儿原地自旋
    • 线程 A 释放锁之后,将期望值设置为 null ,此时线程 B 获得锁,将期望值设置为线程 B 自身
    • 最后线程 B 释放锁
AA	 come in 
BB	 come in 
AA	 invoked myUnLock()
BB	 invoked myUnLock()

5.4、读写锁
独占锁(写锁)、共享锁(读锁)、互斥锁
  • 独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和synchronized而言都是独占锁
  • 共享锁:指该锁可以被多个线程所持有
  • 对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。
  • 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
读写锁的代码示例
  • 代码:使用 ReentrantReadWriteLock 完成读锁、写锁分离
/**
 * 多个线程同时读一个资源类没有问题,所以为了满足并发量,读取共享资源应该可以同时进行。
 *
 * 但是写资源只能有一个线程。
 *
 * 写操作:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断。
 *
 * 小总结:
 * 读-读能共存
 * 读-写不能共存
 * 写-写不能共存
 *
 * 写操作:原子性+独占,整个过程必须是一个完整的统一体,中间不许被分隔,被打断
 *
 * @ClassName ReadWriteLockDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/8 14:13
 * @Version 1.0
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for (int i = 1; i <= 5; i++) {
            int tempInt = i;
            new Thread(() -> {
                myCache.put(tempInt + "", tempInt + "");
            }, String.valueOf(i)).start();
        }

        for (int i = 1; i <= 5; i++) {
            int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt + "");
            }, String.valueOf(i)).start();
        }
    }
}

class MyCache {
    // 凡缓存,一定要用 volatile 修饰,保证内存可见性
    private volatile Map<String, Object> map = new HashMap<>();
    // ReentrantReadWriteLock:读写锁
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {
        reentrantReadWriteLock.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 写入完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            reentrantReadWriteLock.writeLock().unlock(); // 释放写锁
        }
    }

    public void get(String key) {
        reentrantReadWriteLock.readLock().lock(); // 加读锁
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在读取:" + key);
            try {
                TimeUnit.MILLISECONDS.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 {
            reentrantReadWriteLock.readLock().unlock(); // 释放读锁
        }
    }
}
  • 程序运行结果:写操作没有被打断
2	 正在写入:2
2	 写入完成
3	 正在写入:3
3	 写入完成
1	 正在写入:1
1	 写入完成
4	 正在写入:4
4	 写入完成
5	 正在写入:5
5	 写入完成
1	 正在读取:1
2	 正在读取:2
3	 正在读取:3
4	 正在读取:4
5	 正在读取:5
2	 读取完成2
4	 读取完成4
1	 读取完成1
5	 读取完成5
3	 读取完成3
  • 在ReentrantReadWriteLock中分别维护了一把读锁和一把写锁
public class ReentrantReadWriteLock
       implements ReadWriteLock, java.io.Serializable {
   private static final long serialVersionUID = -6992448646407690164L;
   /** Inner class providing readlock */
   private final ReentrantReadWriteLock.ReadLock readerLock;
   /** Inner class providing writelock */
   private final ReentrantReadWriteLock.WriteLock writerLock;
   /** Performs all synchronization mechanics */
   final Sync sync;

6、线程通信

CountDownLatch、CyclicBarrier、Semaphore使用过吗?
6.1、CountDownLatch
CountDownLatch 的用法
  • 让一些线程阻塞,直到另一些线程完成一系列操作后才被唤醒
  • CountDownLatch 维护了一个计数器,有两个核心方法:countDown() 和 await()
    - 调用 countDown() 方法会将计数器减一
    - 当计数器的值不为零时,线程调用 await() 方法时,会被阻塞
    - 当计数器的值变为0时,因调用 await() 方法被阻塞的线程会被唤醒,继续执行
CountDownLatch 代码示例
  • 代码
/**
 - @ClassName CountDownLatchDemo
 - @Description TODO
 - @Author Heygo
 - @Date 2020/8/8 17:43
 - @Version 1.0
 */
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        leaveClassroom();
    }

    private static void leaveClassroom() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6); // 初始化次数为 6
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t上完自习,离开教室");
                countDownLatch.countDown(); // 计数器减 1
            }, String.valueOf(i)).start();
        }
        countDownLatch.await(); // 等待上述线程执行完成(等待计数减为 0)
        System.out.println(Thread.currentThread().getName() + "\t ******班长最后关门走人");
    }
}
  • 程序运行结果:班长等待所有同学都完成自习后,再锁门
2	上完自习,离开教室
1	上完自习,离开教室
3	上完自习,离开教室
4	上完自习,离开教室
5	上完自习,离开教室
6	上完自习,离开教室
main	 ******班长最后关门走人

CountDownLatch + 枚举类的使用
  • 定义枚举类:可以通过 CountryEnum.ONE 获得齐国对应的 CountryEnum 对象
/**
 - @ClassName CountryEnum
 - @Description TODO
 - @Author Heygo
 - @Date 2020/8/8 17:44
 - @Version 1.0
 */
public enum CountryEnum {
    ONE(1, "齐"), TWO(2, "楚"), THREE(3, "燕"), FOUR(4, "赵"), FIVE(5, "魏"), SIX(6, "韩");

    private Integer retCode;
    private String retMsg;

    CountryEnum(Integer retCode, String retMsg) {
        this.retCode = retCode;
        this.retMsg = retMsg;
    }

    public Integer getRetCode() {
        return retCode;
    }

    public void setRetCode(Integer retCode) {
        this.retCode = retCode;
    }

    public String getRetMsg() {
        return retMsg;
    }

    public void setRetMsg(String retMsg) {
        this.retMsg = retMsg;
    }

    public static CountryEnum list(int idx) {
        // 获取枚举类中的所有值
        CountryEnum[] countryEnums = CountryEnum.values();
        for (CountryEnum countryEnum : countryEnums) {
            if (idx == countryEnum.getRetCode()) {
                return countryEnum;
            }
        }
        return null;
    }
}
  • 秦灭六国,后一统华夏
/**
 - @ClassName CountDownLatchDemo
 - @Description TODO
 - @Author Heygo
 - @Date 2020/8/8 17:43
 - @Version 1.0
 */
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        county();
    }

    private static void county() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6); // 初始化次数为 6
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 国被灭");
                countDownLatch.countDown(); // 计数器减 1
            }, CountryEnum.list(i).getRetMsg()).start();
        }
        countDownLatch.await(); // 等待上述线程执行完成(等待计数减为 0)
        System.out.println(Thread.currentThread().getName() + "\t ******秦国一统华夏");
    }
}
  • 程序运行结果
齐	 国被灭
魏	 国被灭
燕	 国被灭
楚	 国被灭
韩	 国被灭
赵	 国被灭
main	 ******秦国一统华夏
6.2、CyclicBarrier
  • 代码
/**
 - @ClassName CyclicBarrierDemo
 - @Description TODO
 - @Author Heygo
 - @Date 2020/8/8 18:52
 - @Version 1.0
 */
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();
        }
    }
}
  • 程序运行结果(感觉像是线程完成后的回调函数)
2	 收集到第:2龙珠
3	 收集到第:3龙珠
1	 收集到第:1龙珠
6	 收集到第:6龙珠
5	 收集到第:5龙珠
4	 收集到第:4龙珠
7	 收集到第:7龙珠
召唤神龙

6.3、Semaphore
Semaphore 的使用
  1. Semaphore 即信号量,信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
  2. 构造器 Semaphore(int) 用于指定共享资源的数目,如果设定为 1 ,则 Semaphore 信号量退化为 Lock 锁或者 synchronized 锁
  3. 调用 semaphore.acquire() 方法获取对共享资源的使用,调用 semaphore.release() 释放对共享资源的占用
  4. 一句话讲明白:抢车位
Semaphore 代码示例
  1. 代码:使用 Semaphore 完成对共享资源的并发控制
/**
 2. @ClassName SemaphoreDemo
 3. @Description TODO
 4. @Author Heygo
 5. @Date 2020/8/8 19:02
 6. @Version 1.0
 */
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. 程序运行结果
2	抢到车位
1	抢到车位
3	抢到车位
3	停车3秒后离开车位
2	停车3秒后离开车位
1	停车3秒后离开车位
5	抢到车位
6	抢到车位
4	抢到车位
4	停车3秒后离开车位
6	停车3秒后离开车位
5	停车3秒后离开车位

7、阻塞队列

阻塞队列知道吗?
7.1、什么是阻塞队列
队列 + 阻塞队列
  1. 阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示
    当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。当队列是空时,从队列中取出元素的操作将被阻塞
  2. 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
  3. 试图往己满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从列中移除一个或者多个元素,或者4. 完全清空队列后使队列重新变得空闲起来并后续新增一些元素

在这里插入图片描述

7.2、阻塞队列好处
为什么用阻塞队列?有什么好处?

1、在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
2、为什么需要BlockingQueue?好处是我们不需要关心什么时候需要阻塞线程,不用我们自己去控制线程的唤醒(notify)和阻塞(wait)
3、什么时候需要唤醒线程?这一切BlockingQueue都给你一手包办了
4、在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度

7.3、阻塞队列分类
BlockingQueue 分类(看前三个即可)
  1. ArrayBlockingQueue:由数组结构组成的有界阻塞队列——对应于 ArrayList
  2. LinkedBlockingQueue:由链表结构组成的有界(大小默认值为Integer.MAX_VALUE)阻塞队列——对应于 LinkedList
  3. SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列——生产一个,消费一个
  4. PriorityBlockingQueue:支持优先级排序的无界阻塞队列
  5. DelayQueue:使用优先级队列实现的延迟无界阻塞队列
  6. LinkedTransferQueue:由链表结构组成的无界阻塞队列。
  7. LinkedBlockingDeque:由链表结构组成的双向阻塞队列
7.4、阻塞队列用法
BlockingQueue 的核心方法

在这里插入图片描述
抛出异常

  1. 当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full
  2. 当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException

返回布尔

  1. 插入方法,成功 ture 失败 false
  2. 移除方法,成功返回出队列的元素,队列里面没有就返回null

一直阻塞

  1. 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产线程直到put数据成功或者响应中断退出。
  2. 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。

超时退出
当阻塞队列满时,队列会阻塞生产者线程一定时间,超过后限时后生产者线程会退出

7.4.1、ArrayBlockingQueue

代码示例

/**
 * ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO原则对元素进行排序
 * LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量高于ArrayBlockingQueue
 * SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移出操作,否则插入操作一直处于阻塞状态,吞吐量通常要高
 *
 * 阻塞队列
 *   1.阻塞队列有没有好的一面
 *   2.不得不阻塞,你如何管理
 *
 * @ClassName BlockingQueueDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/8 19:11
 * @Version 1.0
 */
public class BlockingQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3);
        //addAndRemove(blockingQueue);
        //offerAndPoll(blockingQueue);
        //putAndTake(blockingQueue);
        outOfTime(blockingQueue);
    }

    private static void addAndRemove(BlockingQueue<String> blockingQueue) {
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        System.out.println(blockingQueue.add("e"));
        System.out.println(blockingQueue.element());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
    }

    private static void offerAndPoll(BlockingQueue<String> blockingQueue) {
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.offer("e"));
        System.out.println(blockingQueue.peek());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
    }

    private static void putAndTake(BlockingQueue<String> blockingQueue) throws InterruptedException {
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
        blockingQueue.put("d");
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
    }

    private static void outOfTime(BlockingQueue<String> blockingQueue) throws InterruptedException {
        System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
    }

}
7.4.2、SynchronousQueue
阻塞队列之 SynchronousQueue
  1. SynchronousQueue没有容量。与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。
  2. 每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
  3. 一句话总结:SynchronousQueue 时零库存阻塞队列

代码示例

  • 代码
/**
- 阻塞队列SynchronousQueue演示
-  - @ClassName SynchronousQueueDemo
- @Description TODO
- @Author Heygo
- @Date 2020/8/8 20:13
- @Version 1.0
*/
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();
           }
       }, "AAA").start();

       new Thread(() -> {
           try {
               try {
                   TimeUnit.SECONDS.sleep(5);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());

               try {
                   TimeUnit.SECONDS.sleep(5);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());

               try {
                   TimeUnit.SECONDS.sleep(5);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }, "BBB").start();
   }
}
  • 程序运行结果:线程 AAA 生产完新的数据后,必须等待线程 BBB 取走后,才能继续生产
AAA	 put 1
BBB	1
AAA	 put 2
BBB	2
AAA	 put 3
BBB	3

7.5、阻塞队列使用场景
阻塞队列使用场景

生产者消费者模式、线程池、消息中间件

传统版:消费者生产者模式
  • 代码
  • Lock 替代 synchronized
    • lock.await() 替代 object.wait()
    • lock.signalAll() 替代 object.notifyAll()
/**
 * 题目:一个初始值为0的变量,两个线程对其交替操作,一个加1,一个减1,来5轮
 *
 * 口诀:
 *  1.线程操作资源类     --> 编写方法
 *  2.判断 干活 通知     --> await() 和 signalAll()
 *  3.防止虚假唤醒机制   --> 使用 while 判断,而不是 if
 *
 * @ClassName ProdConsumer_TraditionDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/8 20:26
 * @Version 1.0
 */
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 (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "AAA").start();

        new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                try {
                    shareData.decrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "BBB").start();
    }
}

// 资源类
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 == 1) {
                //等待,不能生产
                condition.await();
            }
            //2 干活
            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();
            }
            //2 干活
            number--;
            System.out.println(Thread.currentThread().getName() + "\t" + number);
            //3 通知唤醒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
  • 程序运行结果
AAA	1
BBB	0
AAA	1
BBB	0
AAA	1
BBB	0
AAA	1
BBB	0
AAA	1
BBB	0
补充:synchronized 和Lock有什么区别?用新的Lock有什么好处?你举例说说

1、实现方式:https://www.cnblogs.com/lycroseup/p/7486860.html

1、synchronized是关键字属天JVM层面,synchronized的语义底层是通过一个monitor的对象来完成,其实wait()、notify()等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait()、notify()等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因

2、Lock是具体类(java.util.concurrent.Locks.Lock)是api层面的锁

2、使用方法
1、synchronized 不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用

2、ReentrantLock则需要用户去手动释放锁着没有主动放锁,就有可能导致出现死锁现象。需要lock()和unlock()方法配合try{ }finally{ }语句块来完成。

3、等待是否可中断
1、synchronized不可中断,除非抛出异常或者正常运行完成
2、ReentrantLock 可中断

  • 设置超时方法 trylock(long timeout,TimeUnit unit)
    或者
  • LockInterruptibly()放代码块中,调用interrupt()方法可中断

4、加锁是否公平

1、synchronized非公平锁

2、ReentrantLock两者都可以,默认公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁

5、锁绑定多个条件condition

1、synchronized没有

2、ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

从字节码来看 synchronized 和 Lock

  • 代码
public static void main(String[] args) {
    synchronized (new Object()) {
    }

    ReentrantLock lock = new ReentrantLock();
}
  • 字节码指令
    • 有一条 monitorenter 指令,表示获取锁
    • 有两条 monitorexit 指令,其中第二条 monitorexit 指令保证程序出了一场,照样能释放锁
    • 使用 ReentrantLock 类,在字节码层面就是 new 了一个对象
 0 new #2 <java/lang/Object>
 3 dup
 4 invokespecial #1 <java/lang/Object.<init>>
 7 dup
 8 astore_1
 9 monitorenter
10 aload_1
11 monitorexit
12 goto 20 (+8)
15 astore_2
16 aload_1
17 monitorexit
18 aload_2
19 athrow
20 new #3 <java/util/concurrent/locks/ReentrantLock>
23 dup
24 invokespecial #4 <java/util/concurrent/locks/ReentrantLock.<init>>
27 astore_1
28 return
题目:多线程交替打印,展示 Lock 的精确唤醒
  • 代码
/**
 * 题目:多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下:
 * A打印5次,B打印10次,C打印15次
 * 紧接着
 * A打印5次,B打印10次,C打印15次
 * ......
 * 打印10轮
 *
 * @ClassName SyncAndReenLockDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/8 21:01
 * @Version 1.0
 */
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();
    }
}

// 资源类
class ShareResource {
    private int number = 1;//A:1.B:2,C:3
    private Lock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    public void print5() {
        lock.lock();
        try {
            //1判断
            while (number != 1) {
                c1.await();
            }
            //2干活
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //3通知
            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.println(Thread.currentThread().getName() + "\t" + i);
            }
            //3通知
            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.println(Thread.currentThread().getName() + "\t" + i);
            }
            //3通知
            number = 1;
            c1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

  • 程序运行结果(一下结果仅是一轮打印的结果)
A	1
A	2
A	3
A	4
A	5
B	1
B	2
B	3
B	4
B	5
B	6
B	7
B	8
B	9
B	10
C	1
C	2
C	3
C	4
C	5
C	6
C	7
C	8
C	9
C	10
C	11
C	12
C	13
C	14
C	15
阻塞队列版:消费者生产者模式
  • 代码
/**
 * volatile/CAS/atomicInteger/BlockQueue/线程交互/原子引用
 *
 * @ClassName ProdConsumer_BlockQueueDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/8 21:26
 * @Version 1.0
 */
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();

        try {
            TimeUnit.SECONDS.sleep(5);
        }catch (InterruptedException e){

        }

        System.out.println("5秒钟到,main停止");
        myResource.stop();
    }
}

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;
        while (FLAG) {
            data = atomicInteger.incrementAndGet() + ""; // 原子整形对象加 1
            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生产停止");
    }

    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 + "成功");
        }
        System.out.println(Thread.currentThread().getName() + "\t消费停止");
    }

    public void stop() throws Exception {
        this.FLAG = false; // 停止生产与消费
    }
}

  • 程序运行结果
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成功
5秒钟到,main停止
Prod	生产停止
Consumer	 超过2秒,消费退出

8、线程池

线程池用过吗?ThreadPoolExecutor他谈你的理解?生产上你如何设置合理参数?
8.1、Callable 接口
Callable 与 Futuretask(适配器模式)
  • 代码
/**
 * @ClassName CallableDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/8 21:51
 * @Version 1.0
 */
public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
        new Thread(futureTask, "AA").start(); // 开始执行耗时计算
        int result01 = 100;
        // 等待线程执行完成
        while (!futureTask.isDone()){
            // Do something here
        }
        // 要求获得Callable线程的计算结果,如果没有计算完成就要去强求,会导致阻塞,直到计算完成
        int result02 = futureTask.get();
        System.out.println("result=" + (result01 + result02));
    }
}

class MyThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("callable come in ...");
        // 模拟耗时操作
        try {
            TimeUnit.SECONDS.sleep(2);
        }catch (InterruptedException e){

        }
        return 1024;
    }
}
  • 程序运行结果
callable come in ...
result=1124

多个线程共享 Futuretask

  • 代码:线程 A 和线程 B 共享 Futuretask
/**
* @ClassName CallableDemo
* @Description TODO
* @Author Heygo
* @Date 2020/8/8 21:51
* @Version 1.0
*/
public class CallableDemo {
   public static void main(String[] args) throws ExecutionException, InterruptedException {
       FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
       new Thread(futureTask, "AA").start(); // 开始执行耗时计算
       new Thread(futureTask, "BB").start(); // 开始执行耗时计算
       int result01 = 100;
       while (!futureTask.isDone()){
           // Do something here
       }
       // 要求获得Callable线程的计算结果,如果没有计算完成就要去强求,会导致阻塞,直到计算完成
       int result02 = futureTask.get();
       System.out.println("result=" + (result01 + result02));
   }
}

class MyThread implements Callable<Integer> {
   @Override
   public Integer call() throws Exception {
       System.out.println("callable come in ...");
       // 模拟耗时操作
       try {
           TimeUnit.SECONDS.sleep(2);
       }catch (InterruptedException e){

       }
       return 1024;
   }
}
  • 程序运行结果
callable come in ...
result=1124

8.2、线程池的优势
为什么要用线程池,线程池的优势是什么?

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

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

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
8.3、线程池如何使用
线程池架构说明
  1. Java 中的线程池是通过Executor框架实现的
  2. 该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类
  3. 线程池框架架构图如下:

在这里插入图片描述

线程池的编码实现

了解:

  1. Executor:newScheduledThreadPool(),带时间调度的线程池
  2. Executors.newWorkStealingPool(int):java8新增,使用目前机器上可用的处理器作为它的并行级别

重要:

  1. Executors.newFixedThreadPool(int):执行长期的任务,性能好很多
  2. Executors.newSingleThreadExecutor():一个任务一个任务执行的场景
  3. Executors.newCachedThreadPool():执行很多短期异步的小程序或者负载较轻的服务器

线程池使用步骤:

  1. 创建线程池
  2. 使用线程池
  3. 关闭线程池
Executors.newFixedThreadPool(int):固定线程数量的线程池

主要特点如下:

  • 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newFixedThreadPool() 创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue 阻塞队列
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

代码示例

  • 代码
/**
- 第四种使用Java多线程的方式:线程池
-  - @ClassName MyThreadPoolDemo
- @Description TODO
- @Author Heygo
- @Date 2020/8/8 22:34
- @Version 1.0
*/
public class MyThreadPoolDemo {
   public static void main(String[] args) {
       System.out.println("Fixed Thread Pool");
       fixedThreadPool();
   }

   private static void fixedThreadPool() {
       //一池5个线程
       ExecutorService threadPool = Executors.newFixedThreadPool(5);
       //一般常用try-catch-finally
       //模拟10个用户来办理业务,每个用户就是一个线程
       try {
           for (int i = 0; i < 9; i++) {
               threadPool.execute(() -> {
                   System.out.println(Thread.currentThread().getName() + "\t办理业务");
               });
           }
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           threadPool.shutdown();
       }
   }
}
  • 程序运行结果:线程池数量为 5,所以线程名最大为 pool-1-thread-5
Fixed Thread Pool
pool-1-thread-3	办理业务
pool-1-thread-4	办理业务
pool-1-thread-2	办理业务
pool-1-thread-1	办理业务
pool-1-thread-2	办理业务
pool-1-thread-4	办理业务
pool-1-thread-5	办理业务
pool-1-thread-3	办理业务
pool-1-thread-1	办理业务
Executors.newSingleThreadExecutor():单个线程的线程池

主要特点如下:

  • 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
  • newSingleThreadExecutor()将线程池的corePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue 阻塞队列
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

代码示例

  • 代码
/**
 * 第四种使用Java多线程的方式:线程池
 *
 * @ClassName MyThreadPoolDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/8 22:34
 * @Version 1.0
 */
public class MyThreadPoolDemo {
    public static void main(String[] args) {
        System.out.println("Single Thread Pool");
        singleThreadPool();
    }

    private static void singleThreadPool() {
        //一池1个线程
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        try {
            for (int i = 0; i < 9; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t办理业务");
                });
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }

}
  • 程序运行结果:线程池中只有一个线程
Single Thread Pool
pool-1-thread-1	办理业务
pool-1-thread-1	办理业务
pool-1-thread-1	办理业务
pool-1-thread-1	办理业务
pool-1-thread-1	办理业务
pool-1-thread-1	办理业务
pool-1-thread-1	办理业务
pool-1-thread-1	办理业务
pool-1-thread-1	办理业务
Executors.newCachedThreadPool():自适应线程数量的线程

主要特点如下:

  • 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newCachedThreadPool()将线程池的corePoolSize设置为0,将线程池的maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

代码示例

  • 代码
/**
 * 第四种使用Java多线程的方式:线程池
 *
 * @ClassName MyThreadPoolDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/8 22:34
 * @Version 1.0
 */
public class MyThreadPoolDemo {
    public static void main(String[] args) {
        System.out.println("Cached Thread Pool");
        cachedThreadPool();
    }

    private static void cachedThreadPool() {
        //不定量线程
        ExecutorService threadPool = Executors.newCachedThreadPool();
        try {
            for (int i = 0; i < 9; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t办理业务");
                });
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }

}
  1. 程序运行结果
Cached Thread Pool
pool-1-thread-1	办理业务
pool-1-thread-2	办理业务
pool-1-thread-3	办理业务
pool-1-thread-4	办理业务
pool-1-thread-5	办理业务
pool-1-thread-6	办理业务
pool-1-thread-7	办理业务
pool-1-thread-8	办理业务
pool-1-thread-3	办理业务
new ThreadPoolExecutor()

在这里插入图片描述

8.4、线程池 7 大参数
ThreadPoolExecutor 构造器
/**
 2. Creates a new {@code ThreadPoolExecutor} with the given initial
 3. parameters.
 4.  5. @param corePoolSize the number of threads to keep in the pool, even
 6.        if they are idle, unless {@code allowCoreThreadTimeOut} is set
 7. @param maximumPoolSize the maximum number of threads to allow in the
 8.        pool
 9. @param keepAliveTime when the number of threads is greater than
 10.        the core, this is the maximum time that excess idle threads
 11.        will wait for new tasks before terminating.
 12. @param unit the time unit for the {@code keepAliveTime} argument
 13. @param workQueue the queue to use for holding tasks before they are
 14.        executed.  This queue will hold only the {@code Runnable}
 15.        tasks submitted by the {@code execute} method.
 16. @param threadFactory the factory to use when the executor
 17.        creates a new thread
 18. @param handler the handler to use when execution is blocked
 19.        because the thread bounds and queue capacities are reached
 20. @throws IllegalArgumentException if one of the following holds:<br>
 21.         {@code corePoolSize < 0}<br>
 22.         {@code keepAliveTime < 0}<br>
 23.         {@code maximumPoolSize <= 0}<br>
 24.         {@code maximumPoolSize < corePoolSize}
 25. @throws NullPointerException if {@code workQueue}
 26.         or {@code threadFactory} or {@code handler} is null
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
七大参数
  1. corePoolSize:线程池中的常驻核心线程数
    • 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
    • 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
  2. maximumPoolSize:线程池能够容纳同时执行的最大线程数,此数值必须大于等于1
  3. keepAliveTime:多余的空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
  4. unit:keepAliveTime的单位
  5. workQueue:任务队列,被提交但尚未被执行的任务(阻塞队列)
  6. threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
  7. handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数时,如何来拒绝

理解:线程池的创建参数,就像一个银行。

  1. corePoolSize就像银行的“当值窗口“,比如今天有2位柜员在受理客户请求(任务)。如果超过2个客户,那么新的客户就会在等候区(等待队列workQueue)等待。
  2. 当等候区也满后,又来了几位客户,这个时候就要开启“加班窗口”,让其它3位柜员来加班,此时达到最大窗口maximumPoolSize,为5个。emmm …,但后来的客户会抢占等候区客户的机会,先办理业务
  3. 如果开启了所有窗口,等候区依然满员,此时就应该启动”拒绝策略“handler,告诉不断涌入的客户,叫他们不要进入,已经爆满了。
  4. 由于不再涌入新客户,加班窗口逐渐开始空闲,这个时候就通过keepAlivetTime将多余的3个”加班窗口“取消,恢复到2个”当值窗口“。

验证从core扩容到maximum后,立即运行当前到达的任务,而不是队列中的

  • 代码:线程池的核心线程数量为 2,最大线程数量为 5
/**
 - @ClassName T1
 - @Description TODO
 - @Author Heygo
 - @Date 2020/8/7 12:54
 - @Version 1.0
 */
public class T1 {

    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                100,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
        try {
            for (int i = 1; i <= 8; i++) {
                final int tempInt = i;
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "号窗口,服务顾客" + tempInt);
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}
  • 程序运行结果
pool-1-thread-2号窗口,服务顾客2
pool-1-thread-1号窗口,服务顾客1
pool-1-thread-3号窗口,服务顾客6
pool-1-thread-4号窗口,服务顾客7
pool-1-thread-5号窗口,服务顾客8
pool-1-thread-1号窗口,服务顾客3
pool-1-thread-2号窗口,服务顾客4
pool-1-thread-5号窗口,服务顾客5
  • 分析结果:core=2,所以1号窗口对应1号顾客,2号窗口对应2号顾客,但是接下来,3、4、5号顾客又来了,进入容量为3的队列中排队,接下来6、7、8号顾客又来了,1、2号窗口正在服务,且队列也满了,此时应该开启3、4、5号窗口来提供服务,为6、7、8号顾客提供服务,然后再由这5个窗口为3、4、5号顾客提供服务
8.5、线程池底层原理
  1. 在创建了线程池后,等待提交过来的任务请求。
  2. 当调用execute()方法添加一个请求任务时,线程池会做如下判断:
    • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
    • 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
    • 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    • 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
    • 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
    • 所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小

在这里插入图片描述

8.6、线程池面试题
8.6.1、线程池拒绝策略
拒绝策略是什么?
  1. 等待队列也已经排满了,再也塞不下新任务了,同时线程池中的max线程也达到了,无法继续为新任务服务。
  2. 这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK内置拒绝策略(以下内置策略均实现了RejectedExecutionHandler接口)
  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行

  2. CallerRunsPolicy:调用者运行,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量

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

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

8.6.2、创建线程池方法
单一的、固定的、可变的三种创建线程池的方法,用哪个多?

结论:一个都不用,答案来自于阿里开发手册

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

【强制】线程池不允许使用Executors 去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:

  • FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
  • CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
8.6.3、自己手写线程池
工作中如何使用线程池,是否自定义过线程池使用?
  • JDK 自带线程池的缺陷:底层使用了 LinkedBlockingQueue 阻塞队列,该阻塞队列默认是无界的,允许的请求队列长度为Integer.MAX_VALUE
/**
 * Creates a {@code LinkedBlockingQueue} with a capacity of
 * {@link Integer#MAX_VALUE}.
 */
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

代码示例:指令阻塞队列上限 + 使用默认的 AbortPolicy 策略

  • 代码
/**
 * 第四种使用Java多线程的方式:线程池
 *  * @ClassName MyThreadPoolDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/8 22:34
 * @Version 1.0
 */
public class MyThreadPoolDemo {
    public static void main(String[] args) {
        System.out.println("Custom Thread Pool\n");
        customThreadPool();

    }

    private static void customThreadPool() {
        ExecutorService threadPool =
                new ThreadPoolExecutor(2,
                        5,
                        1L,
                        TimeUnit.SECONDS,
                        new LinkedBlockingQueue<>(3),
                        Executors.defaultThreadFactory(),
                        new ThreadPoolExecutor.AbortPolicy()
                );
        try {
            for (int i = 1; i <= 8; i++) {
                final int temp = i;
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t办理业务" + temp);
                });
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}
  • for (int i = 1; i <= 8; i++) 时,线程池最大线程数 + 阻塞队列数量 = 8 ,所以程序不会抛异常,线程池 hold 得住
Custom Thread Pool

pool-1-thread-1	办理业务1
pool-1-thread-1	办理业务3
pool-1-thread-1	办理业务4
pool-1-thread-1	办理业务5
pool-1-thread-2	办理业务2
pool-1-thread-5	办理业务8
pool-1-thread-3	办理业务6
pool-1-thread-4	办理业务7
  • for (int i = 1; i <= 8; i++) 时,同时并发的最大线程数可能会超过 8 ,所以程序可能会抛异常,也可能不会,跑不跑异常,就看线程池能不能即时处理我们交给他的任务
Custom Thread Pool

java.util.concurrent.RejectedExecutionException: Task com.Heygo.MyThreadPoolDemo$$Lambda$1/990368553@7ba4f24f rejected from java.util.concurrent.ThreadPoolExecutor@3b9a45b3[Running, pool size = 5, active threads = 2, queued tasks = 0, completed tasks = 6]
   at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
   at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
   at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
   at com.Heygo.MyThreadPoolDemo.customThreadPool(MyThreadPoolDemo.java:40)
   at com.Heygo.MyThreadPoolDemo.main(MyThreadPoolDemo.java:23)
pool-1-thread-2	办理业务2
pool-1-thread-3	办理业务6
pool-1-thread-2	办理业务3
pool-1-thread-2	办理业务5
pool-1-thread-5	办理业务8
pool-1-thread-1	办理业务1
pool-1-thread-4	办理业务7
pool-1-thread-3	办理业务4

代码示例:使用 CallerRunsPolicy 策略

  • 代码
private static void customThreadPool() {
    ExecutorService threadPool =
            new ThreadPoolExecutor(2,
                    5,
                    1L,
                    TimeUnit.SECONDS,
                    new LinkedBlockingQueue<>(3),
                    Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.CallerRunsPolicy()
            );
    try {
        for (int i = 1; i <= 9; i++) {
            final int temp = i;
            threadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "\t办理业务" + temp);
            });
        }

    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        threadPool.shutdown();
    }
}
  • 程序运行结果:无法处理的任务会回退给调用线程
Custom Thread Pool

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

代码示例:使用 DiscardOldestPolicy 策略

  • 代码
private static void customThreadPool() {
    ExecutorService threadPool =
            new ThreadPoolExecutor(2,
                    5,
                    1L,
                    TimeUnit.SECONDS,
                    new LinkedBlockingQueue<>(3),
                    Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.DiscardOldestPolicy()
            );
    try {
        for (int i = 1; i <= 9; i++) {
            final int temp = i;
            threadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "\t办理业务" + temp);
            });
        }

    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        threadPool.shutdown();
    }
}
  • 程序运行结果:等待时间最长的业务 3 被抛弃了。。。
Custom Thread Pool

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

代码示例:使用 DiscardPolicy 策略

  • 代码
private static void customThreadPool() {
    ExecutorService threadPool =
            new ThreadPoolExecutor(2,
                    5,
                    1L,
                    TimeUnit.SECONDS,
                    new LinkedBlockingQueue<>(3),
                    Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.DiscardPolicy()
            );
    try {
        for (int i = 1; i <= 9; i++) {
            final int temp = i;
            threadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "\t办理业务" + temp);
            });
        }

    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        threadPool.shutdown();
    }
}
  • 程序运行结果:业务 9 直接来吃闭门羹
Custom Thread Pool

pool-1-thread-1	办理业务1
pool-1-thread-3	办理业务6
pool-1-thread-2	办理业务2
pool-1-thread-1	办理业务3
pool-1-thread-4	办理业务7
pool-1-thread-5	办理业务8
pool-1-thread-2	办理业务5
pool-1-thread-3	办理业务4
8.6.4、合理配置线程池
如何查看机器的逻辑处理器个数
System.out.println(Runtime.getRuntime().availableProcessors());
如何合理配置线程池?

CPU 密集型

  1. CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
  2. CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。
  3. CPU密集型任务配置尽可能少的线程数量,一般公式:CPU核数+1个线程的线程池

IO 密集型

  1. 由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2。
  2. IO密集型,即该任务需要大量的IO,即大量的阻塞。
  3. 在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
  4. 所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
  5. IO密集型时,大部分线程都阻塞,故需要多配置线程数:参考公式:CPU核数/(1-阻塞系数),阻塞系数在0.8~0.9之间,比如8核CPU:8/(1-0.9)=80个线程数

9、死锁及定位

死锁编码及定位分析
9.1、什么是死锁
产生死锁的主要原因

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

死锁产生原因
  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当
9.2、死锁示例代码
死锁代码 Demo
  • 代码:两个线程线程持有自己的锁,同时又去抢对方的锁,emmm。。。不是说不让用字符串作为线程同步锁吗
/**
* 死锁是指两个或者两个以上的进程在执行过程中,因抢夺资源而造成的一种互相等待的现象,
* 若无外力干涉它们将都无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,
* 死锁出现的可能性也就很低,否则就会因争夺有限的资源而陷入死锁。
*
* @ClassName DeadLockDemo
* @Description TODO
* @Author Heygo
* @Date 2020/8/9 12:48
* @Version 1.0
*/
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();

       /*
        * windows下的java运行程序,也有类似ps的查看进程的命令,但是目前我们需要查看的只是java
        *
        * linux
        *          ps -ef|grep xxxx    ls -l查看当前进程的命令
        *
        * windows
        *          jps = java ps      jps -l
        *          jstack
        * */
   }
}

class HoldLockThread implements Runnable {
   private String lockA;
   private String lockB;

   public HoldLockThread(String lockA, String lockB) {
       this.lockA = lockA;
       this.lockB = lockB;
   }

   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);
           }
       }
   }
}
  • 程序运行结果
9.3、死锁解决方案
如何排查死锁问题?
  • jps命令定位进程号
  • jstack找到死锁查看
操作示例
  • jps -l 查看产生死锁的进程号
C:\Users\Heygo\Desktop\Interview>jps -l
29808 sun.tools.jps.Jps
3824 org.jetbrains.jps.cmdline.Launcher
25220
8900 DeadLockDemo
  • jstack pid :找到死锁
Found one Java-level deadlock:
=============================
"ThreadBBB":
  waiting to lock monitor 0x000000001c403158 (object 0x000000076b518a18, a java.lang.String),
  which is held by "ThreadAAA"
"ThreadAAA":
  waiting to lock monitor 0x000000001c400ef8 (object 0x000000076b518a50, a java.lang.String),
  which is held by "ThreadBBB"

Java stack information for the threads listed above:
===================================================
"ThreadBBB":
        at HoldLockThread.run(DeadLockDemo.java:56)
        - waiting to lock <0x000000076b518a18> (a java.lang.String)
        - locked <0x000000076b518a50> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)
"ThreadAAA":
        at HoldLockThread.run(DeadLockDemo.java:56)
        - waiting to lock <0x000000076b518a50> (a java.lang.String)
        - locked <0x000000076b518a18> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

第 3 章 JVM 与 GC

1、JVM 复习串讲

JVM 内存结构
  • JVM 体系结构
    在这里插入图片描述
  • Java8以后的JVM
    在这里插入图片描述

在这里插入图片描述

GC 的作用域

在这里插入图片描述

常见的垃圾收集算法
  1. 引用计数算法
    在这里插入图片描述
  2. 复制算法
    在这里插入图片描述
    在这里插入图片描述
  3. 标记清除算法
    在这里插入图片描述
  4. 标记整理算法
    在这里插入图片描述

2、谈谈 GC Roots

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

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

引用计数算法

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

  2. 因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,给对象中

  3. 添加一个引用计数器,每当有一个地方引用它,计数器值加1,每当有一个引用失效时,计数器值减1。

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

  5. 那为什么主流的Java虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。
    在这里插入图片描述
    枚举根节点做可达性分析(根搜索路径)

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

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

  8. 基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。

  9. 也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活;没有被遍历到的就自然被判定为死亡。

在这里插入图片描述
在这里插入图片描述
那么问题来了,哪些可作为GC Roots的对象

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

3、JVM 系统默认值

JVM常用基本配置参数有哪些?
3.1、JVM参数类型
3.1.1、标配参数
在jdk各个版本之间稳定,很有大的变化
  • -version
  • -help
  • java -showversion

在这里插入图片描述

3.1.2、X 参数
X 参数了解即可
  • -Xint(修改编译模式)
  • -Xcomp
  • -Xmixed
    在这里插入图片描述
3.1.3、XX 参数
1、Boolean 类型

公式:-XX:+或者- 某个属性,+表示开启、-表示关闭

示例

  1. 是否打印GC收集细节:
  • -XX:-PrintGCDetails
  • -XX:+PrintGCDetails
  1. 是否使用串行垃圾回收器
  • -XX:-UseSerialGC
  • -XX:+UseSerialGC
2、KV键值对类型

公式:-XX:属性key=属性值value
示例

  • -XX:MetaspaceSize=128m
  • -XX:MaxTenuringThreshold=15
题外话:jinfo 如何查看当前运行程序的配置

公式 1:jinfo -flag 配置项 进程编号
公式 2:jinfo -flags 进程编号

示例

  • jps -l :得到 JVM 进程编号
  • jinfo -flag PrintGCDetails pid :查看 PrintGCDetails 属性是否开启(pid 为进程编号)
题外话:两个经典参数 -Xms和-Xmx
  • -Xms:等价于-XX:InitialHeapSize
  • -Xmx:等价于-XX:MaxHeapSize
3.2、查看 JVM 默认值
-XX:+PrintFlagsInitial:主要查看初始默认值

公式:

  • java -XX:+PrintFlagsInitial -version
  • java -XX:+PrintFlagsInitial
    示例
    在这里插入图片描述
-XX:+PrintFlagsfinal:主要查看修改更新

公式:java -XX:+PrintFlagsFinal -version

示例

  • = 表示没被修改过
  • := 表示 JVM 默认加载时修改过或人为修改过
    在这里插入图片描述
PrintFlagsFinal举例:运行Java命令的同时打印出参数
  • java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512m T(T 为 Java 类名)
    在这里插入图片描述
-XX:+PrintCommandLineFlags:打印命令行参数

公式:java -XX:+PrintCommandLineFlags -version

C:\Users\Heygo\Desktop\Interview>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=266620736 -XX:MaxHeapSize=4265931776 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocatio
n -XX:+UseParallelGC
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)


4、JVM 常用参数

基础知识复习
/**
 * @ClassName HelloGC
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 15:12
 * @Version 1.0
 */
public class HelloGC {

    public static void main(String[] args) {
        long totalMemory = Runtime.getRuntime().totalMemory(); // Java 虚拟机中的内存总量
        long maxMemory = Runtime.getRuntime().maxMemory(); // Java 虚拟机试图使用的最大内存量
        System.out.println("TOTAL_MEMORY:" + totalMemory / (double) 1024 / 1024 + "MB");
        System.out.println("MAX_MEMORY:" + maxMemory / (double) 1014 / 1024 + "MB");
    }

}
常用参数
  • -Xms:初始大小内存,默认为物理内存1/64,等价于-XX:InitialHeapSize

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

  • -Xss:设置单个线程栈的大小,等价于-XX:ThreadStackSize,一般默认为512~1024K;0代表默认出厂值

在这里插入图片描述

  1. -Xmn:设置年轻代大小

  2. -XX:MetaSpaceSize:设置元空间大小

    • 元空间本质和永久代类似,都是对JVM中方法区的实现。不过元空间与永久代之间最大的区别是:元空间并不在虚拟机中,而是使用本地内存。
    • 因此,默认情况下,元空间的大小仅受本地内存限制,默认大小约为 21M
    • -Xms10m -Xmx10m -XX:MetaSpaceSize=1024m -XX:+PrintFlagsFinal
  3. -XX:+PrintGCDetails:输出详细GC收集日志信息。
    在这里插入图片描述

  4. -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相同
  5. -XX:NewRatio:配置年轻代与老年代在堆结构的占比

    • 默认-XX:NewRatop=2,新生代占1,老年代占2,即新生代占整个堆的1/3
    • 假如设置参数-XX:NewRatop=4,新生代占1,老年代占4,即新生代占整个堆的1/5
    • NewRatio就是老年代占比,剩下的1给年轻代
  6. -XX:MaxTenuringThreshold:设置垃圾最大年龄

    • XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。
    • 如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加对象在年轻代被回收的概率
典型设置案例:可以和面试官闲聊的案例
-Xms4096m -Xmx4096m -Xss1024k -XX:MetaSpaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC

5、四中引用类型

强引用、软引用、弱引用、虚引用分别是什么?
5.1、整体架构

强、软、弱、虚
在这里插入图片描述

5.2、四种引用
5.2.1、强引用
强引用(默认支持模式)
  1. 当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。
  2. 强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象
  3. 在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达。状态,它是不可能被垃圾回收机制回收的,即使该对象以后远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一
  4. 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)
5.2.2、软引用
软引用的含义
  • 软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
  • 对于只有软引用的对象来说,当系统内存充足时它不会被回收,当系统内存不足时它会被回收。
  • 软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!
软引用代码示例

示例 1:内存够用不会回收软引用对象

  • 代码
/**
 * 内存够用的时候就保留,不够用就回收
 *
 * @ClassName SoftReferenceDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 21:54
 * @Version 1.0
 */
public class SoftReferenceDemo {

    public static void main(String[] args) {
        softRef_Memory_Enough();
    }

    public static void softRef_Memory_Enough() {
        Object o1 = new Object();
        SoftReference<Object> softReference = new SoftReference<>(o1);
        System.out.println(o1);
        System.out.println(softReference.get());

        o1 = null;
        System.gc(); // 进行 GC
        System.out.println(o1); // 强引用变量为 null 必被回收
        System.out.println(softReference.get()); // 内存够用不会回收软引用对象
    }
    
}

  • 程序运行结果:软引用对象并没有被回收
java.lang.Object@4554617c
java.lang.Object@4554617c
null
java.lang.Object@4554617c

示例 2:内存不够用必定回收软引用对象

  • 代码
/**
 * 内存够用的时候就保留,不够用就回收
 *
 * @ClassName SoftReferenceDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 21:54
 * @Version 1.0
 */
public class SoftReferenceDemo {

    public static void main(String[] args) {
        //softRef_Memory_Enough();
        softRef_Memory_NotEnough();
    }

    /*
        JVm配置,故意产生大对象并配置小的内存,让内存不够用,导致OOM,看软引用的回收情况
        -Xms5m -Xmx5m -XX:+PrintGCDetails
     */
    public static void softRef_Memory_NotEnough() {
        Object o1 = new Object();
        SoftReference<Object> softReference = new SoftReference<>(o1);
        System.out.println(o1);
        System.out.println(softReference.get());

        o1 = null;

        try {
            byte[] bytes = new byte[30 * 1024 * 1024];
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(o1);
            System.out.println(softReference.get());
        }
    }

}

  • 程序运行结果:OOM 之前,必定进行一次 Full GC ,此时会清除软引用对象
  • ``
[GC (Allocation Failure) [PSYoungGen: 1024K->504K(1536K)] 1024K->640K(5632K), 0.0008207 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
java.lang.Object@4554617c
java.lang.Object@4554617c
[GC (Allocation Failure) [PSYoungGen: 1403K->504K(1536K)] 1539K->752K(5632K), 0.0007773 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 504K->504K(1536K)] 752K->768K(5632K), 0.0005913 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 264K->637K(4096K)] 768K->637K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0055154 secs] [Times: user=0.09 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 637K->637K(5632K), 0.0002769 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) java.lang.OutOfMemoryError: Java heap space
	at com.Heygo.SoftReferenceDemo.softRef_Memory_NotEnough(SoftReferenceDemo.java:46)
	at com.Heygo.SoftReferenceDemo.main(SoftReferenceDemo.java:18)
[PSYoungGen: 0K->0K(1536K)] [ParOldGen: 637K->620K(4096K)] 637K->620K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0056716 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
null
null
Heap
 PSYoungGen      total 1536K, used 92K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 9% used [0x00000000ffe00000,0x00000000ffe173f8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 4096K, used 620K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 15% used [0x00000000ffa00000,0x00000000ffa9b138,0x00000000ffe00000)
 Metaspace       used 3487K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 382K, capacity 388K, committed 512K, reserved 1048576K

5.2.3、弱引用
弱引用的含义

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。

弱引用代码示例
  • 代码
/**
 * @ClassName WeakReferenceDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 22:09
 * @Version 1.0
 */
public class WeakReferenceDemo {
    public static void main(String[] args) {
        Object o1 = new Object();
        WeakReference<Object> weakReference = new WeakReference<>(o1);
        System.out.println(o1);
        System.out.println(weakReference.get());

        o1 = null;
        System.gc();
        System.out.println("...............");

        System.out.println(o1);
        System.out.println(weakReference.get());
    }
}

  • 程序运行结果:执行 GC,弱引用对象被回收
java.lang.Object@4554617c
java.lang.Object@4554617c
...............
null
null

你知道弱引用的话,谈谈WeakHashMap
  • 代码
/**
 * @ClassName WeakHashMapDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 22:14
 * @Version 1.0
 */
public class WeakHashMapDemo {
    public static void main(String[] args){
        myHashMap();
        System.out.println("========");
        myWeakHashMap();
    }

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

        // key 置为 null ,关 HashMap 毛事啊,HashMap 已经将数据保存至 Node 节点中了
        key = null;
        System.out.println(map);

        System.gc();
        System.out.println(map);
    }

    private static void myWeakHashMap(){
        WeakHashMap<Integer,String> map = new WeakHashMap<>();
        Integer key = new Integer(2);
        String value = "WeakHashMap";

        map.put(key,value);
        System.out.println(map);

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

        System.gc();
        System.out.println(map);
    }
}

  • 程序运行结果:如果 WeakHashMap 的 key 为 null ,GC 后该 KV 节点将被回收
{1=HashMap}
{1=HashMap}
{1=HashMap}
========
{2=WeakHashMap}
{2=WeakHashMap}
{}

5.2.4、虚引用
虚引用的作用
  • 虚引用需要java.lang.ref.PhantomReference类来实现。顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
  • 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列ReferenceQueue联合使用。
  • 虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。
  • PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。
  • 换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
  • Java 技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
    在这里插入图片描述
    引用队列:我(虚引用)被回收前需要被引用队列保存下
    在这里插入图片描述
    引用队列代码示例
  • 代码
/**
 * @ClassName ReferenceQueueDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 22:30
 * @Version 1.0
 */
public class ReferenceQueueDemo {
    public static void main(String[] args) throws InterruptedException{
        Object o1 = new Object();
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        WeakReference<Object> weakReference = new WeakReference<>(o1,referenceQueue);
        System.out.println(o1);
        System.out.println(weakReference.get());
        System.out.println(referenceQueue.poll());

        System.out.println("=============");
        o1 = null;
        System.gc();
        Thread.sleep(500);

        System.out.println(o1);
        System.out.println(weakReference.get());
        System.out.println(referenceQueue.poll());
    }
}

  • 程序运行结果:在 GC 回收之前,将待回收的对象放入引用队列中,有点类似 Spring AOP 的后置通知
java.lang.Object@4554617c
java.lang.Object@4554617c
null
=============
null
null
java.lang.ref.WeakReference@74a14482

虚引用代码示例

  • 代码
/**
 * @ClassName PhantomReferenceDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 22:30
 * @Version 1.0
 */
public class PhantomReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        Object o1 = new Object();
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        PhantomReference<Object> phantomReference = new PhantomReference<>(o1,referenceQueue);

        System.out.println(o1);
        System.out.println(phantomReference.get());
        System.out.println(referenceQueue.poll());

        System.out.println("=================");
        o1 = null;
        System.gc();
        Thread.sleep(500);

        System.out.println(o1);
        System.out.println(phantomReference.get());
        System.out.println(referenceQueue.poll());
    }
}

  • 程序运行结果:对象被回收时,将其放入了引用队列
java.lang.Object@4554617c
null
null
=================
null
null
java.lang.ref.PhantomReference@74a14482


5.3、引用总结
软引用、弱引用适用场景
  1. 假如有一个应用需要读取大量的本地图片:
    • 如果每次读取图片都从硬盘读取则会严重影响性能,
    • 如果一次性全部加载到内存中又可能造成内存溢出。
  1. 此时使用软引用可以解决这个问题。设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。
Map<String,SoftReference<Bitmap>> imageCache = new HashMap<String,SoftReference<Bitmap>>();
GC Roots 和四种引用类型的总结
  1. java提供了4种引用类型,在垃级回收的时候,都有自己各自的特点。
  2. ReferenceQueue是用来配合引用工作的,没有有ReferenceQueue一样可以运行。
  3. 创建引用的时候可以指定关联的队列,当GC释放对象内存的时候,会将引用加入到引用队列,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动这相当于是一种通知机制。
  4. 当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVM允许我们在对象被销毁后,做一些我们自己想做的事情。
    在这里插入图片描述

6、OOM

面试题:请谈谈你对OOM的认识

在这里插入图片描述

java.lang.StackOverflowError
  • 代码:递归调用无结束条件
/**
 * @ClassName StackOverflowErrorDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 22:48
 * @Version 1.0
 */
public class StackOverflowErrorDemo {
    public static void main(String[] args){
        stackOverflowError();
    }

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


  • 程序运行结果
Exception in thread "main" java.lang.StackOverflowError
java.lang.OutOfMemoryError:Java heap space
  • 代码
/**
 * @ClassName JavaHeapSpaceDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 22:53
 * @Version 1.0
 */
public class JavaHeapSpaceDemo {
    public static void main(String[] args){
        String str = "seu";

        while(true){
            str += str + new Random().nextInt(11111111)+new Random().nextInt(22222222);
            str.intern();
        }

    }
}

  • JVM 参数
-Xms10m -Xmx10m
  • 程序运行结果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
	at java.lang.StringBuilder.append(StringBuilder.java:208)
	at com.Heygo.JavaHeapSpaceDemo.main(JavaHeapSpaceDemo.java:17)
	java.lang.OutOfMemoryError:GC overhead limit exceeded
  • GC回收时间过长时会抛出0utOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存
  • 连续多次GC都只回收了不到2%的极端情况下才会抛出。假如不抛出GC overhead Limit 错误会发生什么情况呢?
  • 那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行,这样就形成恶性循环,CPU使用孩一直是100%,而GC却没有任何成效
    在这里插入图片描述
  • 代码:将生成的字符串放到字符串常量池中
/**
 - @ClassName GCOverheadDemo
 - @Description TODO
 - @Author Heygo
 - @Date 2020/8/9 23:02
 - @Version 1.0
 */
public class GCOverheadDemo {
    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;
        }

    }
}
  • JVM 参数
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
  • 程序运行结果:GC 说,老铁,我回收不动啊~~~
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7076K->7076K(7168K)] 9124K->9124K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0304580 secs] [Times: user=0.17 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7078K->7078K(7168K)] 9126K->9126K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0294555 secs] [Times: user=0.11 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7080K->7080K(7168K)] 9128K->9128K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0303605 secs] [Times: user=0.08 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7081K->7081K(7168K)] 9129K->9129K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0273443 secs] [Times: user=0.11 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7083K->7083K(7168K)] 9131K->9131K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0323289 secs] [Times: user=0.13 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7085K->7085K(7168K)] 9133K->9133K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0282554 secs] [Times: user=0.25 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7087K->7087K(7168K)] 9135K->9135K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0279839 secs] [Times: user=0.22 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7088K->7088K(7168K)] 9136K->9136K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0268396 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7090K->7090K(7168K)] 9138K->9138K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0275986 secs] [Times: user=0.14 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7092K->7092K(7168K)] 9140K->9140K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0279635 secs] [Times: user=0.14 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7094K->7094K(7168K)] 9142K->9142K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0259208 secs] [Times: user=0.22 sys=0.01, real=0.03 secs] 
***************i:145410
[Full GC (Ergonomics) java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.lang.Integer.toString(Integer.java:403)
	at java.lang.String.valueOf(String.java:3099)
	at com.Heygo.GCOverheadDemo.main(GCOverheadDemo.java:20)
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.lang.Integer.toString(Integer.java:403)
	at java.lang.String.valueOf(String.java:3099)
	at com.Heygo.GCOverheadDemo.main(GCOverheadDemo.java:20)
java.lang.OutOfMemoryError: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就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了

代码示例:查看 MaxDirectMemory

  • 代码
/**
 *
 * @ClassName DirectBufferMemoryDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 23:18
 * @Version 1.0
 */
public class DirectBufferMemoryDemo {
    public static void main(String[] args) {
        System.out.println("配置的maxDirectMemory: " + (sun.misc.VM.maxDirectMemory() / (double) 1024 / 1024) + "MB");
        try {
            Thread.sleep(300);
        } catch (Exception e) {
            e.printStackTrace();
        }
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6 * 1024 * 1024);
    }
}

  • 程序运行结果
配置的maxDirectMemory: 3618.0MB

演示 DirectBuffer OOM

  • 代码同上
  • JVM 参数
-Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
  • 程序运行结果
配置的maxDirectMemory: 5.0MB
[GC (Allocation Failure) [PSYoungGen: 2035K->488K(2560K)] 2035K->792K(9728K), 0.0007089 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (System.gc()) [PSYoungGen: 564K->496K(2560K)] 868K->912K(9728K), 0.0008965 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 416K->705K(7168K)] 912K->705K(9728K), [Metaspace: 3483K->3483K(1056768K)], 0.0051785 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:694)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at com.Heygo.DirectBufferMemoryDemo.main(DirectBufferMemoryDemo.java:22)

结论:
写 NIO 和 Netty 程序时要小心

java.lang.OutOfMemoryError: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默认限制

非root用户登录Linux系统测试

  • 代码
package com.atguigu.Interview.study..jvm.oom;

/**
 * @ClassName UnableCreateNewThreadDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/10 12:59
 * @Version 1.0
 */
public class UnableCreateNewThreadDemo {

    public static void main(String[] args) {
        for (int i = 1; ; i++) {
            System.out.println(i);
            new Thread(() -> {
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  • Linux 编译带包名的 Java源文件
java -d . UnableCreateNewThreadDemo.java
  • Linux 运行带包名的 Java 程序
java com.atguigu.Interview.study..jvm.oom.UnableCreateNewThreadDemo
  • 程序运行结果
    在这里插入图片描述

终止程序

  • 当前普通用户无法终止该进程,需要 root 用户帮忙
ps -ef|grep java
kill -9 pid

在这里插入图片描述
服务器级别调优参数

  • 查看当前用户的可创建的最大线程数
ulimit -u
  • 编辑配置文件:可以看到除了 root 用户无限制,其他用户所能创建的最大线程数为 1024
vim /etc/security/limits.d/90-nproc.conf

在这里插入图片描述

  • 我们给张三用户多分配一些线程数
    在这里插入图片描述
java.lang.OutOfMemoryError:Metaspace
  1. Metaspace是方法区在Hotspot中的实现,它与永久代最大的区别在于:Metaspace并不在虚拟机内存中而是使用本地内存,也即在java8中,classe metadata(the virtual machines internal presentation of Java class),被存储在叫做Metaspace的native memory
  2. Java 8及之后的版本使用Metaspace来替代永久代永久代,存放了以下信息:
    • 虚拟机加载的类信息
    • 常量池
    • 静态变量
    • 即时编译后的代码缓存

代码示例

  • 代码:利用 CGLIB 不停地往元空间中加载类
/**
* 模Metaspace空间溢出,我们不断生成类往元空间灌,类占据的空间总是会超过Metaspace指定的空间大小的
*
* @ClassName MetaspaceOOMTest
* @Description TODO
* @Author Heygo
* @Date 2020/8/10 13:28
* @Version 1.0
*/
public class MetaspaceOOMTest {

   static class OOMTest {

   }

   public static void main(String[] args) {
       int i = 0;//模拟多少次后发生异常

       try {
           while (true) {
               i++;
               Enhancer enhancer = new Enhancer();
               enhancer.setSuperclass(OOMTest.class);
               enhancer.setUseCache(false);
               enhancer.setCallback(new MethodInterceptor){
                   @Override
                   public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                       return methodProxy.invokeSupper(o, args);
                   }
                   enhancer.create();
               }
           }
       } catch (Throwable e) {
           System.out.println("********多少次后发生了异常:" + i);
           e.printStackTrace();
       }
   }

}
  • JVM 参数
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
  • 程序运行结果
    在这里插入图片描述

7、GC 垃圾回收

GC垃圾回收算法和垃圾收集器的关系?分别是什么?
  1. GC算法(引用计数/复制/标记清除/标记整理)是内存回收的方法论,垃圾收集器是算法的落地实现
  2. 目前为止还没有完美的收集器出现,更没有万能的收集器,只有针对具体应用最合适的收集器,进行分代收集
  3. 四种主要的垃圾收集器:串行、并行、并发、G1
    在这里插入图片描述
串行垃圾回收器(Serial)

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

程序 --> GC --> 程序 --> GC --> 程序 --> GC
并行垃圾回收器(Parallel)

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

     --> GC         --> GC          --> GC
程序 --> GC --> 程序 --> GC --> 程序 --> GC
     --> GC         --> GC          --> GC
并发垃圾回收器(Concurrent Mark Sweep)

用户线程和垃圾收集线程同时执行(不一定并行,可能交替执行)不需要停顿用户线程。互联网公司多用,适用对响应时间有要求的场景

     --> GC         --> 程序         --> 程序
程序 --> GC --> 程序 --> GC --> 程序 --> GC
     --> GC         --> GC          --> 程序
G1 垃圾回收器(Garbage first)

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

串行与并行、STW与并发
  • 串行与并行的垃圾回收器执行 GC 时,都会中断用户线程,STW 时间都会比较长(相对于并发来讲)
  • 并发垃圾回收器可与用户线程并发执行,降低了 STW 的时间
    在这里插入图片描述
    在这里插入图片描述

8、垃圾收集器

怎么看服务器默认的垃圾收集器是哪个?生产上如何配置垃圾收集器? 谈谈你对垃圾收集器的理解
8.1、查看默认垃圾收集器
命令行指令:java -XX:+PrintCommandLineFlags -version
  • -XX:+UseParallelGC:表示使用 UseParallelGC 垃圾收集器
C:\Users\Heygo\Desktop\Interview>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=266620736 -XX:MaxHeapSize=4265931776 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocatio
n -XX:+UseParallelGC
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)
8.2、默认的垃圾收集器
  1. java的GC回收的类型主要有几种:UseSerialGC,UseParallelGC,UseConcMarkSweepGC,UseParNewGC,UseParallelOldGC,UseG1GC
  2. UseSerialOldGC被淘汰了
  • 撕个 OpenJDK 源码

bool Arguments::check_gc_consistency() {
  bool status = true;
  // Ensure that the user has not selected conflicting sets
  // of collectors. [Note: this check is merely a user convenience;
  // collectors over-ride each other so that only a non-conflicting
  // set is selected; however what the user gets is not what they
  // may have expected from the combination they asked for. It's
  // better to reduce user confusion by not allowing them to
  // select conflicting combinations.

  uint i = 0;
  if (UseSerialGC)                       i++;
  if (UseConcMarkSweepGC || UseParNewGC) i++;
  if (UseParallelGC || UseParallelOldGC) i++;
  if (UseG1GC)                           i++;
  if (i > 1) {
    jio_fprintf(defaultStream::error_stream(),
                "Conflicting collector combinations in option list; "
                "please refer to the release notes for the combinations "
                "allowed\n");

    status = false;
  }
  return status;
}

8.3、垃圾收集器细讲
垃圾收集器概述

垃圾收集器就来具体实现这些GC算法并实现内存回收。不同厂商、不同版本的虚拟机实现差别很大,HotSpot中包含的收集器如下图所示:
在这里插入图片描述
垃圾收集器对应的组合关系
在这里插入图片描述

8.3.1、部分参数说明
  1. DefNew:Default New Generation
  2. Tenured:Old
  3. ParNew:Parallel New Generation
  4. PSYoungGen:Parallel Scavenge
  5. ParOldGen:Parallel Old Generation
8.3.2、Server & Client
  1. 适用范围:只需要掌握Server模式即可,Client模式基本不会用
  2. 根据操作系统以及硬件区分 Client 和 Server:
    • 32位Window操作系统,不论硬件如何都默认使用Client的JVM模式
    • 32位其它操作系统,2G内存同时有2个cpu以上用Server模式,低于该配置还是Client模式
    • 64位only server模式
8.3.3、新生代垃圾回收
串行GC(Serial)/(Serial Copying)

串行收集器:Serial收集器

  1. 一句话:一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它集结束。
  2. 串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在进行垃圾收集过程中可能会产生较长的停顿(Stop-The-World状态)。
  3. 虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。
    在这里插入图片描述
    参数配置:
  4. 对应JVM参数是:-XX:+UseSerialGC
  5. 开启后会使用:Serial(Young区用)+Serial Old(Old区用)的收集器组合
  6. 表示:新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:UseSerialGC

在这里插入图片描述

并行GC(ParNew)

ParNew(并行)收集器

  1. 一句话:使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World暂停其他所有的工作线程直到它收集结束。
  2. ParNew收集器其实就是Serial收集器新生代的并行多线程版本,最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Seria收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多java虚拟机运行在Server模式下新生代的默认垃圾收集器。
    在这里插入图片描述
    参数配置:
  3. 常用对应JVM参数:-XX:+UseParNewGC启用ParNew收集器,只影响新生代的收集,不影响老年代
  4. 开启上述参数后,会使用:ParNew(Young区用)+Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法
  5. 但是,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
  6. -XX:ParallelGCThreads 限制线程数量,默认开启和CPU数目相同的线程数
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC

在这里插入图片描述

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

Parallel 并行回收
7. Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。一句话:串行收集器在新生代和老年代的并行化
8. 它重点关注的是:可控制的吞吐量(Thoughput=运行用户代码时间/(运行用户代码时间+垃圾收集时间),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99%)。高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。
9. 自适应调节策略也是ParallelScavenge 收集器与ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMilis)或最大的吞吐量。
在这里插入图片描述
参数配置:

  1. 常用JVM参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器
  2. 开启该参数后:新生代使用复制算法,老年代使用标记-整理算法
  3. 多说一句:-XX:ParallelGCThreads=数字N表示启动多少个GC线程
    • cpu>8:N=5/8
    • cpu<8:N=实际个数
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
或者
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldlGC

在这里插入图片描述

8.3.4、老年代垃圾回收
串行GC(Serial Old)/(Serial MSC)
  1. Serial Old是 Serial垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默认的java虚拟机默认的年老代垃圾收集器。
  2. 在Server模式下,主要有两个用途(了解):
    • 在JDK1.5之前版本中与新生代的Parallel Scavenge 收集器搭配使用。即垃圾收集器组合为:Parallel Scavenge+Serial Old
    • 在JDK1.5之后作为老年代版中使用CMS收集器的后备垃圾收集方案。

参数配置:

-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldlGC
并行GC(Parallel Old)/(Parallel MSC)
  1. Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6才开始提供。
  2. 在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。即在JDK1.6之前的垃圾回收器的搭配方案为Parallel Scavenge+Serial Old
  3. Parallel Old正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可以优先考虑新生代。Parallel Scavenge和年老代Parallel Old 收集器的搭配策略。在JDK1.8的垃圾回收器的搭配方案为Parallel Scavenge+Parallel Old

参数配置:

  1. JVM常用参数:-XX:+UseParallelOldGC 使用Parallel Old收集器
  2. 设置该参数后,新生代Parallel+老年代Parallel Old
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldlGC
或者
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
并发标记清除GC(CMS)
  1. CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。
  2. 适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。
  3. CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。
  4. Concurrent Mark Sweep并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行

在这里插入图片描述
参数配置:

  1. 开启该收集器的JVM参数:-XX:+UseConcMarkSweepGC
  2. 开启该参数后会自动将-XX:+UseParNewGC打开开启该参数后,使用ParNew(Young区用)+CMS(Old区用)+Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC 

CMS 的 4 步过程

  1. 初始标记(CMS initial mark):只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
  2. 并发标记(CMS concurrent mark)和用户线程一起:进行GCRoots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象
  3. 重新标记(CMS remark):为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
  4. 并发清除(CMS concurrent sweep)和用户线程一起:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。

CMS 四步骤总结
在这里插入图片描述
优点
并发收集停顿时间低

缺点

  1. 并发执行,对CPU压力较大:由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间
  2. 采用的标记清除算法会产生大量碎片:标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFulIGCsBeforeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。
    Demo 代码
/*
1.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
2.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC
3.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
4.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC
5.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC
6.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
7.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC (已经没有了)
*/
public class GCDemo {

    public static void main(String[] args) {
        System.out.println("GCDemo...");
        try {
            String str = "bjtu";
            while (true) {
                str += str + new Random().nextInt(77777777) + new Random().nextInt(88888888);
                str.intern();
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}
8.4、如何选择垃圾收集器

在这里插入图片描述
在这里插入图片描述

9、G1 垃圾收集器

9.1、Before G1
以前收集器特点
  1. 年轻代和老年代是各自独立且连续的内存块

  2. 年轻代收集,使用单eden+S0+S1进行复制算法

  3. 老年代收集必须扫描整个老年代区域

  4. 都是以尽可能快速地执行GC为设计原则

9.2、G1 是什么

G1(Garbage-First)收集器,是一款面向服务端应用的收集器
在这里插入图片描述
从官网的描述中,我们知道G1是一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。另外,它还具有以下特性:

  1. 像CMS收集器一样,能与应用程序线程并发执行。
  2. 整理空闲空间更快。
  3. 需要更多的时间来预测GC停顿时间。
  4. 不希望牺牲大量的吞吐性能。
  5. 不需要更大的Java Heap。

G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:

  1. G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
  2. G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。
    为什么会出现 G1?
  3. CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器——G1垃圾收集器。
  4. G1是在2012年才在jdk1.7u4中可用。oracle官方计划在jdk9中将G1变成默认的垃圾收集器以替代CMS。它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换java8以前的CMS收
  5. 主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。
9.3、G1 特点
  1. G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW。
  2. G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片。
  3. 宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘。
  4. G1收集器里面讲整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但它们不再是物理隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域。
  5. G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换。
9.4、G1 底层原理
Region区域化垃圾收集器:最大的好处是化整为零,避免全内存扫描,只需要按区域来进行扫描即可
  1. 区域化内存划片Region,整体编为了一些列不连续的内存区域,避免了全内存区的GC操作。
  2. 核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小。
  3. 在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。
  4. 启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
  5. 每个Region 小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为:32MB*2048=65536MB=64G内存
G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器
  1. 这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。
  2. 这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。
在G1中,还有一种特殊的区域,叫Humongous(巨大的)区域
  1. 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。
  2. 为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
    在这里插入图片描述
G1 回收步骤

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

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

在这里插入图片描述

G1 工作的四大步骤

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

在这里插入图片描述

G1 Demo 代码
/**
 * @ClassName GCDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/10 18:40
 * @Version 1.0
 */
public class GCDemo {

    public static void main(String[] args) {
        System.out.println("GCDemo...");
        try {
            String str = "bjtu";
            while (true) {
                str += str + new Random().nextInt(77777777) + new Random().nextInt(88888888);
                str.intern();
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

  • JVM 参数
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
  • 程序运行结果
    • initial-mark:初始标记
    • GC concurrent-mark-start :并发标记
    • GC remark:最终标记
    • GC cleanup:筛选回收
[GC pause (G1 Humongous Allocation) (young) (initial-mark) (to-space exhausted), 0.0032657 secs]
   [Parallel Time: 1.8 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 116.9, Avg: 117.0, Max: 117.2, Diff: 0.2]
      [Ext Root Scanning (ms): Min: 0.1, Avg: 0.3, Max: 0.6, Diff: 0.5, Sum: 2.4]
      [Update RS (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.8]
         [Processed Buffers: Min: 0, Avg: 1.9, Max: 4, Diff: 4, Sum: 15]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 1.0, Avg: 1.2, Max: 1.3, Diff: 0.3, Sum: 9.8]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4]
         [Termination Attempts: Min: 1, Avg: 6.9, Max: 14, Diff: 13, Sum: 55]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
      [GC Worker Total (ms): Min: 1.5, Avg: 1.7, Max: 1.8, Diff: 0.2, Sum: 13.5]
      [GC Worker End (ms): Min: 118.7, Avg: 118.7, Max: 118.7, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.2 ms]
   [Other: 1.3 ms]
      [Evacuation Failure: 0.9 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.3 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 5120.0K(6144.0K)->0.0B(1024.0K) Survivors: 0.0B->1024.0K Heap: 7351.7K(10.0M)->6167.8K(10.0M)]
 [Times: user=0.09 sys=0.00, real=0.00 secs] 
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0009353 secs]
[GC concurrent-mark-start]
[GC pause (G1 Humongous Allocation) (young) (to-space exhausted), 0.0037669 secs]
   [Parallel Time: 2.7 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 122.1, Avg: 122.1, Max: 122.2, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 0.1, Avg: 0.4, Max: 2.6, Diff: 2.5, Sum: 3.3]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
         [Processed Buffers: Min: 0, Avg: 0.4, Max: 3, Diff: 3, Sum: 3]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.2, Diff: 0.2, Sum: 0.2]
      [Object Copy (ms): Min: 0.0, Avg: 1.1, Max: 1.5, Diff: 1.5, Sum: 9.0]
      [Termination (ms): Min: 0.0, Avg: 1.0, Max: 1.3, Diff: 1.3, Sum: 7.9]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 2.5, Avg: 2.6, Max: 2.6, Diff: 0.1, Sum: 20.6]
      [GC Worker End (ms): Min: 124.7, Avg: 124.7, Max: 124.7, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   [Other: 0.9 ms]
      [Evacuation Failure: 0.5 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.2 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 1024.0K(1024.0K)->0.0B(1024.0K) Survivors: 1024.0K->0.0B Heap: 7372.3K(10.0M)->6188.4K(10.0M)]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC concurrent-mark-end, 0.0043902 secs]
[GC remark [Finalize Marking, 0.0001364 secs] [GC ref-proc, 0.0002039 secs] [Unloading, 0.0004323 secs], 0.0008988 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC cleanup 7372K->7372K(10M), 0.0005463 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
  • G1 中堆分为两部分:garbage-first heap 和 Metaspace(Non-heap)
Heap
 garbage-first heap   total 10240K, used 4199K [0x00000000ff600000, 0x00000000ff700050, 0x0000000100000000)
  region size 1024K, 1 young (1024K), 0 survivors (0K)
 Metaspace       used 3510K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 384K, capacity 388K, committed 512K, reserved 1048576K

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

G1 常用配置:

  1. 开发人员仅仅需要声明以下参数即可:
  2. 三步归纳:开始G1+设置最大内存+设置最大停顿时间
-XX:MaxGCPauseMilis=n:最大GC停顿时间,单位毫秒,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间
9.6、G1 的优势
G1 相较于 CMS 的优势

比起CMS有两个优势:

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

10、生产调优

10.1、SpringBoot 调优步骤
SpringBoot 微服务的生产部署和调参优化步骤
  1. IDEA开发完微服务工程
  2. maven进行clean + package
  3. 要求微服务启动的时候,同时配置我们的JVM/GC的调优参数
  4. 公式:java -server jvm的各种参数 -jar jar/war包名字
java -server -Xms1024m -Xmx1024m -XX+UseG1GC -jar springboot2019-1.0-SNAPSHOT.war
10.2、调优思路和性能评估
整机:top(查看 CPU、内存使用率等,也可使用精简版系统性能命令:uptime)
  • 主要查看 CPU、MEM、load average(按 1 可详细查看 CPU 的每个核心状态)
    在这里插入图片描述
CPU:vmstat(查看 CPU 使用情况)

vmstat -n 2 3

  • 一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数单位是秒,第二个参数是采样的次数
    在这里插入图片描述
    参数含义
  1. -procs
    • r:运行和等待CPU时间片的进程数,原则上1核的CPU的运行队列不要超过2,整个系统的运行队列不能超过总核数的2倍,否则代表系统压力过大
    • b:等待资源的进程数,比如正在等待磁盘I/O、网络I/O等
  1. -cpu
    • us:用户进程消耗CPU时间百分比,us值高,用户进程消耗CPU时间多,如果长期大于50%,需要优化程序
    • sy:内核进程消耗的CPU时间百分比
      说明:us +sy参考值为80%,如果us+sy大于80%,说明可能存在CPU不足
    • id:处于空闲的CPU百分比
    • wa:系统等待IO的CPU时间百分比
    • st:来自于一个虚拟机偷取的CPU时间的百分比

其他的查看指令

  1. 查看所有cpu核信息:mpstat -P ALL 2 (每两秒采样一次)
    在这里插入图片描述
  2. 每个进程使用cpu的用量分解信息:pidstat -u 1 -p 进程编号
    在这里插入图片描述
内存:free(查看应用程序可用内存数)
  1. free:以字节为单位
  2. free -g:以 GB 为单位
  3. free -m:以 GB 为单位
    在这里插入图片描述
    经验值

1.、应用程序可用内存/系统物理内存>70%内存充足

2、 应用程序可用内存/系统物理内存<20%内存不足,需要增加内存

3、20%<应用程序可用内存/系统物理内存<70%内存基本够用

pidstat -p 进程号 -r 采样间隔秒数,可查看进程占用内存的情况

在这里插入图片描述
硬盘:df(查看磁盘剩余空间数)
在这里插入图片描述

磁盘io:iostat(磁盘IO性能评估)

磁盘块设备分布

  1. rkB/s每秒读取数据量kB
  2. wkB/s每秒写入数据量kB
  3. svctm I/O请求的平均服务时间,单位毫秒
  4. await I/O请求的平均等待时间,单位毫秒;值越小,性能越好
  5. util一秒中有百分几的时间用于I/O操作。接近100%时,表示磁盘带宽跑满,需要优化程序或者增加磁盘
  6. rkB/s、wkB/s根据系统应用不同会有不同的值,但有规律遵循:长期、超大数据读写,肯定不正常,需要优化程序读取。
  7. svctm的值与await的值很接近,表示几乎没有/O等待,磁盘性能好,如果await的值远高于svctm的值,则表示I/O队列等待太长,需要优化程序或更换更快磁盘。
    在这里插入图片描述
    pidstat -p 进程号 -r 采样间隔秒数,可查看进程磁盘的使用情况
    在这里插入图片描述
网络io:ifstat(查看网络占用情况)

默认本地没有,下载ifstat

wget http://gael.roualland.free.fr/ifstat/ifstat-1.1.tar.gz

tar xzvf ifstat-1.1.tar.gz 

cd ifstat-1.1

./configure

make 

make install

查看网络 I/O 情况
在这里插入图片描述

11、CPU 占用率高

假如生产环境出现CPU占用过高,请谈谈你的分析思路和定位

下面开始举例啦~~~

先用top命令找出cpu占比最高的,记下PID
  1. 第一个是控制台输出,不用管它,第二个才是我们要监控的进程(CPU 占用率高)
    在这里插入图片描述
    在这里插入图片描述
ps -ef或者jps进一步定位,得知是一个怎么样的一个后台程序

在这里插入图片描述

定位到具体的线程或代码

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

  1. -m:显示所有的线程
  2. -p:pid进程使用cpu的时间
  3. -o:该参数后是用户自定义的格式
    在这里插入图片描述
将需要的线程ID转换为16进制格式(英文小写格式)

printf “%x\n”:打印输出有问题的线程ID(用计算器算也可以)

jstack 进程ID | grep tid(16进制线程ID小写英文)-A60

-A60 表示打印出前 60 行

在这里插入图片描述

12、JVM 分析工具

对于JDK自带的JVM监控和性能分析工具用过哪些?一般怎么使用

下面开讲

自带的JVM监控和性能分析工具是什么
  1. 官方文档:https://docs.oracle.com/javase/8/docs/technotes/tools/
    在这里插入图片描述
自带的JVM监控和性能分析工具怎么用?
  1. jps(虚拟机进程状况工具)

  2. jinfo(java配置信息工具)

  3. jmap(内存映像工具)

  4. jstat(统计信息监视工具)

  5. jstack(堆栈异常跟踪工具)

  6. jvisualvm

  7. jconsole

第 4 章 GitHub 骚操作

1、常用词

常用词含义
  1. watch:会持续收到该项目的动态
    在这里插入图片描述
  2. fork:复制某个项目到自己的GitHub仓库
    在这里插入图片描述
  3. star:点赞
    在这里插入图片描述
  4. clone:将项目下载至本地
git clone https://github.com/996icu/996.ICU.git

在这里插入图片描述

  1. follow:关注作者,会收到他们的动态
    在这里插入图片描述

2、 in 关键词

in关键词限制搜索范围

xxx关键词 in:name或description或readme

搜索示例

xxx in:name:项目名包含xxx
在这里插入图片描述
xxx in:description:项目描述包含xxx
xxx in:readme:项目的readme文件包含xxx
组合使用

  1. seckill in:name,readme:搜索项目名或readme中包含秒杀的项目
    在这里插入图片描述

3、stars fork

stars或fork数量关键词去查找

公式:xxx关键词 stars 通配符

  1. 通配符包括::> 或 :>=
  2. 区间范围数据::数字1…数字2.
搜索示例

查找stars数大于等于5000的springboot项目:springboot stars:>=5000
在这里插入图片描述
查找forks数大于500的springcloud项目:springcloud forks:>500
在这里插入图片描述
组合使用查找fork在2000到4000之间并且star数大于6000的springboot项目:springboot forks:2000…4000 stars:>6000
在这里插入图片描述

4、awesome

awesome加强搜索

公式:awesome关键字;awesome系列一般是用来收集学习、工具、书籍等相关的项目

搜索示例

搜索优秀的redis相关项目,包括框架、教程等:awesome redis
在这里插入图片描述

5、高亮显示

高亮显示某一行代码

公式:

  1. 一行:地址后面紧跟#L数字
  2. 多行:地址后面紧跟#L数字-L数字2
示例:给别人指出关键代码的行数

单行高亮:地址 + #L行数
在这里插入图片描述
多行高亮:地址 + #L行数1-L行数2
在这里插入图片描述

6、项目内搜索

项目内搜索:在项目页面单击键盘 t

在这里插入图片描述

官网地址

https://docs.github.com/en/github/getting-started-with-github/keyboard-shortcuts

7、寻找大佬

搜索某个地区内大佬

公式:

  1. location:地区
  2. language:语言
搜索示例

北京地区java用户:location:beijing language:Java
在这里插入图片描述

  • 20
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值