关于JMM的内容其实并不多,指令重排,可见性,原子性,就这三大块,这次的简单总结,并没有过多深入总结,也只是总结面试上的内容,本篇博客简单说一下原子性,并总结一下JMM中的相关面试问题
要说到什么是原子性,其实这个应该学过计算机的同学都应该知道,每次聊到原子性,都会老生常谈的几个实例也就是那几个,无非就是转账要么全部成功,要么全部失败,其操作组合是一个原子性的。
其实通俗点理解就是一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,其操作组合是不可分割的。
Java中本身的原子操作并不多,只有如下几个
1、基本类型的赋值,int,byte,boolean,short,char,float的赋值操作,float和double,如果在32位JVM虚拟机上运行,很难保证原子性。
2、所有引用reference的赋值操作,不管是32位还是64的虚拟机
3、java.concurrent.Atomic.*包下的所有类的原子操作
只有这几种操作是原子性的。至于long和double由于其变量本身在8个字节,64位,因此在32的虚拟机上的赋值操作是分两步进行的。因此会造成线程安全问题,官方文档对于这种32位错位赋值的变量的时候,建议加上volatile关键字进行修饰。实际开发中,商用版本的JVM其实是已经解决了这个问题。
但是,简单的将各个原子操作组合在一起的操作,并不是原子性的。比如HashMap。
关于JMM中原子性的面试问题,最常见的就是单例模式,针对单例模式我们之前有所总结,但是并没有结合线程的只是进行梳理。
单例模式的写法有很多种
/** * autor:liman * createtime:2021-10-28 * comment:饿汉式单例设计模式(静态常量) */ @Slf4j public class HungarySingleton { //类加载的时候,JVM会保证线程安全(这里可以用静态代码块初始化,也是一样的) private final static HungarySingleton INSTANCE = new HungarySingleton(); private HungarySingleton(){ } public static HungarySingleton getInstance(){ return INSTANCE; } }
/** * autor:liman * createtime:2021-10-29 * comment:懒汉式 线程不安全 */ public class LazySingleton { private static LazySingleton instance; private LazySingleton(){ } //在需要获取的时候去实例化,这明显是不满足线程安全要求的 public static LazySingleton getInstance(){ if(null == instance){ instance = new LazySingleton(); } return instance; } }
第二种单例模式,明显的不是线程安全的,如果要让其满足线程安全的要求,最简单的方式就是在方法上加上synchronized关键字
/** * autor:liman * createtime:2021-10-29 * comment:懒汉式 简单的线程安全 */ public class LazySingletonSynchronize { private static LazySingletonSynchronize instance; private LazySingletonSynchronize(){ } public synchronized static LazySingletonSynchronize getInstance(){ if(null == instance){ instance = new LazySingletonSynchronize(); } return instance; } }
线程安全了,但是慢,很慢,效率很低。因此我们在此基础上引申出另一种写法,在3的基础上提出优化
/** * autor:liman * createtime:2021-10-29 * comment:懒汉式 线程不安全 */ public class LazySingletonSynchronizeLettle { private static LazySingletonSynchronizeLettle instance; private LazySingletonSynchronizeLettle(){ } public synchronized static LazySingletonSynchronizeLettle getInstance(){ if(null == instance){ //方法级别的synchronized影响性能,就将关键的代码用synchronized包围。 synchronized (LazySingletonSynchronizeLettle.class) { instance = new LazySingletonSynchronizeLettle(); } } return instance; } }
看上去是对的。但是依旧无法保证完全的单例,因为……if的判断无法保证线程安全。
/** * autor:liman * createtime:2021-10-29 * comment: 单例模式,双重检测 */ public class SingletonDoubleCheck { //由于新建对象的操作并不是原子的,而是有三步,而这三步指令,可能被CPU重排序 //这三步重排序可能发生NPE问题 //因此要用volatile修饰,一个是保证可见性,同时防止指令重排序 private static volatile SingletonDoubleCheck instance; private SingletonDoubleCheck(){ } public static SingletonDoubleCheck getInstance(){ if(null == instance){//如果没有这个判断,就等同于直接在方式上加synchronized关键字,效率是很低的。 synchronized (SingletonDoubleCheck.class){ if(null == instance){//由于多个线程可能在外部判断中出现线程安全问题,因此内部也需要做一个判断 instance = new SingletonDoubleCheck(); } } } return instance; } }
这种方式是面试中被问到最多的,关于为什么要加双重检测,少一重行不行。为什么要用volatile来修饰,这个在上述代码注释中都有解释。同时无法保证反序列化和反射对单例的破坏
/** * autor:liman * createtime:2021-10-29 * comment:单例模式 静态内部类 */ public class InnerClassSingleton { private InnerClassSingleton(){ } //内部类 private static class SingleInnerInstance{ private static final InnerClassSingleton instance = new InnerClassSingleton(); } public static InnerClassSingleton getInstance(){ return SingleInnerInstance.instance; } }
这种方式其实相当于一种懒汉式的单例模式。根据JVM的规定,在加载类的时候,是不会加载其内部类的,而真正的实例化操作其实在内部类中,也是在调用getInstance方法的时候,才进行加载,而JVM的类加载又保证了线程安全,因此这种方式是线程安全的。同时也属于懒汉式,真正使用中较为推荐。
/** * autor:liman * createtime:2021-10-29 * comment:单例模式 枚举类型 */ public enum EnumSingleton { INSTANCE; }
极为简单,这种方式也是大神推荐的。在《Effective Java》中明确表述过,“使用枚举实现单例的方法虽然没有广泛使用,但是单元素的枚举类型已经成为实现单例模式的最佳方法”。
枚举是一种特殊的类,经过反编译之后枚举会被编译成一个final修饰的class,枚举会继承父类Enum,这个父类的实例都是通过static来修饰和定义的,因此枚举的本质经过编译之后,就是一个静态的对象,在我们使用这种单例的时候才会加载,其实也是一种懒加载。同时也会避免反射和反序列化对单例的破坏
针对单例模式其实可考察的点很多,尤其是双重检测,这个考察的最多。
针对使用场景:如果程序一开始要加载的资源太多,那么就应该使用懒加载,饿汉式的单例不适用于对象的创建依赖于配置文件的场景。懒汉式会增加程序的复杂性。
简单梳理了下什么是原子性,对单例模式重新梳理了一下。