Java内存模型基础学习(三)——最后说说原子性

前言

关于JMM的内容其实并不多,指令重排,可见性,原子性,就这三大块,这次的简单总结,并没有过多深入总结,也只是总结面试上的内容,本篇博客简单说一下原子性,并总结一下JMM中的相关面试问题

原子性

要说到什么是原子性,其实这个应该学过计算机的同学都应该知道,每次聊到原子性,都会老生常谈的几个实例也就是那几个,无非就是转账要么全部成功,要么全部失败,其操作组合是一个原子性的。

其实通俗点理解就是一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,其操作组合是不可分割的。

Java中的原子操作

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中原子性的面试问题,最常见的就是单例模式,针对单例模式我们之前有所总结,但是并没有结合线程的只是进行梳理。

单例模式

单例模式的写法有很多种

1、饿汉式

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

}

2、懒汉式(线程不安全的版本)

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

3、懒汉式(线程安全的版本)

第二种单例模式,明显的不是线程安全的,如果要让其满足线程安全的要求,最简单的方式就是在方法上加上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的基础上提出优化

4、懒汉式(局部的synchronized)

/**
 * 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的判断无法保证线程安全。

5、双重检测

/**
 * 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来修饰,这个在上述代码注释中都有解释。同时无法保证反序列化和反射对单例的破坏

6、静态内部类

/**
 * 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的类加载又保证了线程安全,因此这种方式是线程安全的。同时也属于懒汉式,真正使用中较为推荐。

7、枚举式的单例模式

/**
 * autor:liman
 * createtime:2021-10-29
 * comment:单例模式 枚举类型
 */
public enum EnumSingleton {
    
    INSTANCE;
    
}

极为简单,这种方式也是大神推荐的。在《Effective Java》中明确表述过,“使用枚举实现单例的方法虽然没有广泛使用,但是单元素的枚举类型已经成为实现单例模式的最佳方法”。

枚举是一种特殊的类,经过反编译之后枚举会被编译成一个final修饰的class,枚举会继承父类Enum,这个父类的实例都是通过static来修饰和定义的,因此枚举的本质经过编译之后,就是一个静态的对象,在我们使用这种单例的时候才会加载,其实也是一种懒加载。同时也会避免反射和反序列化对单例的破坏

针对单例模式其实可考察的点很多,尤其是双重检测,这个考察的最多。

针对使用场景:如果程序一开始要加载的资源太多,那么就应该使用懒加载,饿汉式的单例不适用于对象的创建依赖于配置文件的场景懒汉式会增加程序的复杂性。

总结

简单梳理了下什么是原子性,对单例模式重新梳理了一下。

上一篇:羽毛球球速详解,76速、77速、2号球、3号球的含义


下一篇:python – PyQt QTreeWidget.clear()导致崩溃