前言
关于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来修饰和定义的,因此枚举的本质经过编译之后,就是一个静态的对象,在我们使用这种单例的时候才会加载,其实也是一种懒加载。同时也会避免反射和反序列化对单例的破坏
针对单例模式其实可考察的点很多,尤其是双重检测,这个考察的最多。
针对使用场景:如果程序一开始要加载的资源太多,那么就应该使用懒加载,饿汉式的单例不适用于对象的创建依赖于配置文件的场景。懒汉式会增加程序的复杂性。
总结
简单梳理了下什么是原子性,对单例模式重新梳理了一下。