0
点赞
收藏
分享

微信扫一扫

单例模式与Volatile的学习

迎月兮 2022-01-26 阅读 84

单例模式与Volatile的学习

哔哩哔哩链接:https://www.bilibili.com/video/BV15b4y117RJ?p=56

要求

  • 掌握五种单例模式的实现方式
  • 理解为何 DCL(双检锁) 实现时要使用 volatile 修饰静态变量
  • 了解 jdk 中用到单例的场景

一、五种实现方式

1.饿汉式

类一初始化就会被创建

实现要求:

1.构造私有,所有单例的实现都要求,因为如果不是私有,则其他类有机会调用构造方法来创建实例对象,这就会导致多个实例的发生

2.提供一个静态的成员变量,成员变量类型就是单例类型,值就是用私有构造创建的唯一实例

3.静态变量一般是私有的,一般会提供一个公共的静态方法,用于返回静态成员变量

package com.singleton.test;

import java.io.Serializable;

// 1. 饿汉式
public class Singleton1 implements Serializable {
    // 要求一:构造私有
    private Singleton1() { System.out.println("private Singleton1()"); }

    // 要求二:提供一个静态的成员变量
    private static final Singleton1 INSTANCE = new Singleton1();

    // 要求三:静态变量一般是私有的,一般会提供一个公共的静态方法,用于返回静态成员变量
    public static Singleton1 getInstance() {
        return INSTANCE;
    }

    // 提供这个静态方法的意图:其他类调用这个方法时就会触发Singleton1类的加载初始化,就会导致Singleton1这个单例对象被创建,便于测试
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

编写测试代码,

package com.singleton.test;

public class Test {

    @org.junit.Test
    public void test1(){
        // 调用这个静态方法的意图:触发Singleton1类的加载初始化,就会导致Singleton1这个单例对象被创建
        Singleton1.otherMethod();
        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        System.out.println(Singleton1.getInstance());
        System.out.println(Singleton1.getInstance());
    }
}

发现使用的是同一个对象,

image-20220124194949444

反射破坏单例

我们使用反射破坏单例模式,

private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    Constructor<?> constructor = clazz.getDeclaredConstructor(); // 得到无参构造方法
    constructor.setAccessible(true); // 暴力反射,私有的构造方法也可被使用
    System.out.println("反射创建实例:" + constructor.newInstance()); // 创建实例
}

@org.junit.Test
public void test2() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
    // 调用这个静态方法的意图:触发Singleton1类的加载初始化,就会导致Singleton1这个单例对象被创建
    Singleton1.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton1.getInstance());
    System.out.println(Singleton1.getInstance());

    // 反射破坏单例
    reflection(Singleton1.class);
}

发现通过反射又再次调用构造方法创建了一个新的对象,

image-20220124195636562

预防反射破坏单例模式:修改构造方法,如果对象已经创建则不能再次创建

image-20220124195923348

再次测试,发现已经不能再创建对象

image-20220124200049180

反序列化破坏单例

我们通过反序列化破坏单例模式,注意,前提是这个单例需要实现序列化接口

补充:保存在磁盘、网络传输都需要实现序列化接口

image-20220124200328571

测试代码如下:

private static void serializable(Object instance) throws IOException, ClassNotFoundException {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(instance); // 将对象变为一个字节流
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
    // 把字节流还原成一个对象,反序列化会创建出一个新的对象,且反序列化不走构造方法
    System.out.println("反序列化创建实例:" + ois.readObject());
}

@org.junit.Test
public void test3() throws IOException, ClassNotFoundException {
    // 调用这个静态方法的意图:触发Singleton1类的加载初始化,就会导致Singleton1这个单例对象被创建
    Singleton1.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton1.getInstance());
    System.out.println(Singleton1.getInstance());

    // 反序列化破坏单例
    serializable(Singleton1.class);
}

发现反序列化会创建出一个新的对象,且反序列化不走构造方法

image-20220124202248373

预防反序列化破坏单例

需要我们在单例的类中书写一个特殊的readResolve方法,方法中将单例对象返回即可。

原理:如果在反序列化过程中,如果重写了readResolve方法,就会利用这个方法的返回值作为结果返回,就不会用字节数组反序列化生成的对象了,这样就保证了单例模式。

image-20220124201809044

再次测试,发现反序列化返回的还是我们原来的单例对象

image-20220124202102540

Unsafe破坏单例

private static void unsafe(Class<?> clazz) throws InstantiationException {
    /*
     Unsafe是jdk内置的一个类,不能直接访问,我们通过反射拿到unsafe实例,
     UnsafeUtils这是一个工具类,getUnsafe拿到unsafe实例,allocateInstance可以根据类型创建这个类型的实例,
     且这个实例也是一个新的实例,他也不会走构造方法
    */
    Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);
    System.out.println("Unsafe 创建实例:" + o);
}

@org.junit.Test
public void test4() throws InstantiationException {
    // 调用这个静态方法的意图:触发Singleton1类的加载初始化,就会导致Singleton1这个单例对象被创建
    Singleton1.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton1.getInstance());
    System.out.println(Singleton1.getInstance());

    // 反序列化破坏单例
    unsafe(Singleton1.class);
}

发现Unsafe会创建出一个新的对象,且Unsafe不走构造方法

image-20220124203300121

目前还没有找到预防Unsafe破坏单例的方法

2.枚举饿汉式

枚举类是jdk5加入的语法,用枚举类可以很方便的控制枚举类的对象个数,枚举类最终还是会被编译器编译为class,本质还是一个类,如

enum Sex {
    MALE, FEMALE
}

在被编译后就变为了二进制字节码,我们将其翻译成java代码如下,

final class Sex extends Enum<Sex> {
    public static final Sex MALE;
    public static final Sex FEMALE;

    private Sex(String name, int ordinal) {
        super(name, ordinal);
    }

    static {
        MALE = new Sex("MALE", 0);
        FEMALE = new Sex("FEMALE", 1);
        $VALUES = values();
    }

    private static final Sex[] $VALUES;

    private static Sex[] $values() {
        return new Sex[]{MALE, FEMALE};
    }

    public static Sex[] values() {
        return $VALUES.clone();
    }

    public static Sex valueOf(String value) {
        return Enum.valueOf(Sex.class, value);
    }
}

final表示不能被继承,对应两个静态final成员变量,且类型为Sex,且都为public(公共的)。私有构造表示不能通过new关键字为这个枚举类创建新的对象,枚举变量的名称,序号,从0开始,逐一递增,传给父类构造,静态代码块为MALE,FEMALE进行了初始化,他们都分别创建了一个Sex对象,调用了自己的私有构造,创建了两个唯二的实例赋给MALE,FEMALE这两个变量。以后用这两个变量就是用的两个Sex对象

image-20220124205048047

所以我们就可以用枚举类创建单例模式,即只声明一个变量

package com.singleton.test;

// 2. 枚举饿汉式
public enum Singleton2 {
    // 控制枚举类只有一个唯一的实例,枚举变量都是公共的
    INSTANCE;

    // 默认枚举类的构造就是private,不写也可以
    private Singleton2() {
        System.out.println("private Singleton2()");
    }

    // 为了打印hashCode,看是否是同一个对象,不写默认打印枚举类变量的名字
    @Override
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

    // 提供静态的公共方法获取静态变量,因为枚举变量都是公共的,所有不提供也能使用
    public static Singleton2 getInstance() {
        return INSTANCE;
    }

    // 为了测试是饿汉式还是懒汉式
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

之后开始测试,

@org.junit.Test
public void test5(){
    // 调用这个静态方法的意图:触发Singleton2类的加载初始化,就会导致Singleton2这个单例对象被创建
    Singleton2.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton2.getInstance());
    System.out.println(Singleton2.getInstance());
}

发现使用的是同一个对象,

image-20220124210149565

好处一:预防反序列化破坏单例

枚举类使用单例有两个好处,一是它不怕通过反序列化破坏单例,编写测试,

private static void serializable(Object instance) throws IOException, ClassNotFoundException {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(instance); // 将对象变为一个字节流
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
    // 把字节流还原成一个对象,反序列化会创建出一个新的对象,且反序列化不走构造方法
    System.out.println("反序列化创建实例:" + ois.readObject());
}

@org.junit.Test
public void test6() throws IOException, ClassNotFoundException {
    // 调用这个静态方法的意图:触发Singleton2类的加载初始化,就会导致Singleton2这个单例对象被创建
    Singleton2.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton2.getInstance());
    System.out.println(Singleton2.getInstance());

    // 反序列化破坏单例
    serializable(Singleton2.class);
}

发现我们的Singleton2枚举饿汉式并没有书写readResolve方法,反序列化创建出来的还是同一个对象

image-20220125143109903

好处二:预防反射破坏单例

二是它也不怕通过反射破坏单例,编写测试,

private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<?> constructor = clazz.getDeclaredConstructor(); // 得到无参构造方法
        constructor.setAccessible(true); // 暴力反射,私有的构造方法也可使用
        System.out.println("反射创建实例:" + constructor.newInstance()); // 创建实例
}

@org.junit.Test
public void test7() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
    // 调用这个静态方法的意图:触发Singleton2类的加载初始化,就会导致Singleton2这个单例对象被创建
    Singleton2.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton2.getInstance());
    System.out.println(Singleton2.getInstance());

    // 反射破坏单例
    reflection(Singleton2.class);
}

同时我们也没有在Singleton2枚举饿汉式中改写构造方法

image-20220125145301914

发现报错,找不到一个无参的Singleton2的构造方法,

image-20220125143751588

前面我们也已分析枚举的构造方法,枚举的构造是有两个参数的,一个是枚举变量的名字,一个是序号。所以我们修改代码,得到有参构造,并试图创建一个新的枚举对象

private static void reflection1(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, Integer.class); // 得到有参构造方法
    constructor.setAccessible(true); // 暴力反射,私有的构造方法也可被使用
    System.out.println("反射创建实例:" + constructor.newInstance("OTHER", 1)); // 试图创建一个新的枚举对象实例
}

@org.junit.Test
public void test8() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
    // 调用这个静态方法的意图:触发Singleton2类的加载初始化,就会导致Singleton2这个单例对象被创建
    Singleton2.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton2.getInstance());
    System.out.println(Singleton2.getInstance());

    // 反射破坏单例
    reflection1(Singleton2.class);
}

发现还是不行,不能通过反射去创建一个枚举对象

image-20220125144845736

Unsafe破坏单例

private static void unsafe(Class<?> clazz) throws InstantiationException {
    /*
     Unsafe是jdk内置的一个类,不能直接访问,我们通过反射拿到unsafe实例,
     UnsafeUtils这是一个工具类,getUnsafe拿到unsafe实例,allocateInstance可以根据类型创建这个类型的实例,
     且这个实例也是一个新的实例,他也不会走构造方法
    */
    Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);
    System.out.println("Unsafe 创建实例:" + o);
}

@org.junit.Test
public void test9() throws InstantiationException {
    // 调用这个静态方法的意图:触发Singleton2类的加载初始化,就会导致Singleton2这个单例对象被创建
    Singleton2.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton2.getInstance());
    System.out.println(Singleton2.getInstance());

    // Unsafe破坏单例
    unsafe(Singleton2.class);
}

发现创建了一个新的对象

image-20220125145639177

目前还没有找到预防Unsafe破坏单例的方法

扩展:通过Unsafe创建一个新的枚举对象

package com.singleton.test;

import org.springframework.objenesis.instantiator.util.UnsafeUtils;
import sun.misc.Unsafe;

// 示例:通过 Unsafe 造出一个 Enum 对象
public class EnumCreator {
    public static void main(String[] args) throws Exception {
        Unsafe unsafe = UnsafeUtils.getUnsafe();
        long nameOffset = unsafe.objectFieldOffset(Enum.class.getDeclaredField("name"));
        long ordinalOffset = unsafe.objectFieldOffset(Enum.class.getDeclaredField("ordinal"));
        Sex o = (Sex) unsafe.allocateInstance(Sex.class);
        unsafe.compareAndSwapObject(o, nameOffset, null, "阴阳人");
        unsafe.compareAndSwapInt(o, ordinalOffset, 0, 2);
        System.out.println(o.name());
        System.out.println(o.ordinal());
    }
}

3.懒汉式

package com.singleton.test;

import java.io.Serializable;

// 3. 懒汉式单例
public class Singleton3 implements Serializable {
    private Singleton3() {
        System.out.println("private Singleton3()");
    }

    private static Singleton3 INSTANCE = null;

    // 调用getInstance方法时对象未创建时才进行创建
    public static Singleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

编写测试代码,

@org.junit.Test
    public void test10(){
    // 调用这个静态方法的意图:触发Singleton1类的加载初始化,查看构造是否被调用(区分是懒汉式还是饿汉式)
    Singleton3.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton3.getInstance());
    System.out.println(Singleton3.getInstance());
}

发现刚开始调用otherMethod这个静态方法时我们的构造方法并未被调用,只有调用了getInstance方法时对象未创建时才进行调用构造创建对象

image-20220125151230631

线程安全问题

同时如果我们的这个类如果运行在多线程环境下,则必须考虑线程安全问题。即如果我们的多个线程都执行到getInstance这个方法时,都判断INSTANCE这个对象为空,那么则会创建多个对象。

我们可以在getInstance这个方法上直接添加上synchronized进行线程安全的保护,加上synchronized的静态方法,即相当于在这个类上(Singleton3.class)加上一把锁

当某个线程执行这个方法时就会尝试获得Singleton3.class这个对象的锁,如果此时并没有被其他线程所持有,那么这个线程就会获得这把锁并执行方法中代码,同时其他线程在这个时候就需要等待,需要等这个线程执行完毕,退出这个方法并释放这把锁之后才能再次重新获取这把锁并执行这个方法内的代码

image-20220125153136889

不过这种方法性能并不高,因为我们只需要在这个对象未创建时才需要同步,如果这个对象已经创建完毕后,我们就不需要同步,互斥保护了。所以,我们希望是首次创建这个对象时才提供线程安全的保护,后续这个对象创建之后就不再需要了。这就引出了我们的第四种单例的创建方式。

4.DCL(双检锁)懒汉式

是对我们第三种单例模式的优化。在加锁之前先判断,创建了对象则直接返回,没有创建对象才考虑线程之间竞争的问题

package com.singleton.test;

import java.io.Serializable;

// 4. 懒汉式单例 - DCL
public class Singleton4 implements Serializable {
    private Singleton4() {
        System.out.println("private Singleton4()");
    }

    private static volatile Singleton4 INSTANCE = null; // 可见性,有序性

    public static Singleton4 getInstance() {
        // 在加锁之前先判断,创建了对象则直接返回,没有创建对象才考虑线程之间竞争的问题
        if (INSTANCE == null) {
            synchronized (Singleton4.class) {
                // 继续判断,只有INSTANCE为null时进行创建对象
                if (INSTANCE == null) {
                    INSTANCE = new Singleton4();
                }
            }
        }
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

同时需要注意的是,我们的INSTANCE这个成员变量上面必须添加上volatile进行修饰

为何要加上volatile修饰符

为何要添加上volatile进行修饰,我们需要先理解INSTANCE = new Singleton4()这行代码,我们通过反编译Singleton4这个二进制字节码文件。

首先找到Singleton4的字节码文件,之后右键选择Open in Terminal

image-20220125154903462

我们输入反编译指令,javap -c -v -p Singleton4.class ,之后找到getIntance方法,发现对应四个指令:

new指令表示创建一个新的对象(Singleton4),创建对象时会计算这个对象的成员变量,需要占多少内存空间,创建对象即把内存空间分配出来。

invokespecial指令表示调用方法,表示构造方法,构造方法和创建对象时两步操作,创建对象是把内存空间分配出来,而对象创建之后,里面可能存在很多成员变量,而这些成员变量的赋值是在构造方法中进行的。所以,一步是分配空间,一步是对成员变量做初始化。

putstatic指令表示给静态变量赋值,即给我们的INSTANCE赋值。

image-20220125160042890

我们知道,cpu在执行过程中可能会对我们的指令进行优化(重排序),以方便更快的处理。如果我们没有对指令作出因果关系,cpu可能会调换他们的执行顺序。而我们的构造方法初始化和为静态变量赋值的操作都是初始化赋值的操作,所以cpu可能会调换他们的顺序,这在单线程下并没有问题,但是在多线程就可能会存在问题了。如下:

image-20220125162900185

cpu调换了构造方法和为静态变量赋值的操作,此时只完成了对象的初始化操作和静态变量的赋值,构造方法还没有执行,即构造方法中的其他成员变量还未进行初始化,其他的线程就直接将INSTANCE对象返回了,这显然是不对的。

而加上volatile进行修饰INSTANCE之后,它会在为INSTANCE这个共享变量赋值的时候,会在这个变量的赋值语句之后添加上一层内存屏障,他的作用是防止在这语句之前的一些赋值操作(写操作)越过它跑到它的后面去。所以构造方法就不能跑到它的后面去。

补充:读操作的话就不能跑到他的前面去(详情请看后面的Volatile讲解)

image-20220125162306677

所以出现以下的情况都不会有问题:

情况一:

持有的线程还未完成为静态变量的赋值操作,所以其他线程只能等待其完成,所以不会有问题

image-20220125163336254

情况二:

拿到的是一个经过完整构造的对象,所以也不会有问题

image-20220125163455054

所以,我们要加上volatile进行修饰INSTANCE

饿汉式会有线程安全的问题吗?

不会,因为饿汉式是将创建的对象赋值给了静态成员变量,给静态成员变量,最终会放在这个类的静态代码块中执行,而静态代码块中的线程安全,是由java虚拟机进行保证这个对象创建、代码执行的线程安全。

所以,如果我们将对象的创建放入静态代码块,那么它就是线程安全的,所以,这就引出了我们的第五种单例模式的创建。

5.内部类懒汉式

这种方法既兼备了懒汉的特性,又保证在创建线程时的线程安全,推荐使用

package com.singleton.test;

import java.io.Serializable;

// 5. 懒汉式单例 - 内部类
public class Singleton5 implements Serializable {
    private Singleton5() {
        System.out.println("private Singleton5()");
    }

    // 内部类可以访问外部类的私有变量、私有构造
    private static class Holder {
        // 将创建的对象赋值给内部类的静态变量,最终会在静态代码块中执行,所有是线程安全的
        static Singleton5 INSTANCE = new Singleton5();
    }

    // 使用内部类访问它的变量,就会触发内部类的加载初始化,即创建对象
    // 既兼备了懒汉的特性,又保证在创建线程时的线程安全,推荐使用
    public static Singleton5 getInstance() {
        return Holder.INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

编写测试,

@org.junit.Test
public void test11(){
    // 调用这个静态方法的意图:触发Singleton5类的加载初始化,查看构造是否被调用
    Singleton5.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton5.getInstance());
    System.out.println(Singleton5.getInstance());
}

发现我们调用otherMethod静态方法时并没有触发单例对象的创建,只触发了外面这个类的加载初始化,并没有加载内部类,当调用getInstance静态方法时才会触发内部类的初始化,同时初始化的时候又能用静态代码块保证其创建对象的线程安全。

image-20220125165732103

二、单例模式在JDK中的体现

JDK 中单例的体现

  • Runtime 体现了饿汉式单例
  • Console 体现了双检锁懒汉式单例
  • Collections 中的 EmptyNavigableSet 内部类懒汉式单例
  • ReverseComparator.REVERSE_ORDER 内部类懒汉式单例
  • Comparators.NaturalOrderComparator.INSTANCE 枚举饿汉式单例

image-20220125170047712

Runtime类

image-20220125170147198

System类

image-20220125170539443

三、Volatile

哔哩哔哩链接:https://www.bilibili.com/video/BV15b4y117RJ?p=74

前面在DCL(双检锁)懒汉式中我们也已经初始了解了Volatile的作用,接下来我们将详细学习。

面试题:volatile能保证线程安全吗?不能,他能解决共享变量的可见性和有序性,但是不能解决原子性。

可见性:一个线程对共享变量的修改,另一个线程能看到最新的结果

有序性:一个线程内代码按编写顺序执行

原子性:一个线程内多行代码以一个整体运行,期间不能有其他线程的代码插队

下面我们将依次对其进行分析,首先创建一个log日志对象工具类LoggerUtils,用来输出日志

package com.volatiles.test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;

public class LoggerUtils {
    public static final Logger logger1 = LoggerFactory.getLogger("A");
    public static final Logger logger2 = LoggerFactory.getLogger("B");
    public static final Logger logger3 = LoggerFactory.getLogger("C");
    public static final Logger logger4 = LoggerFactory.getLogger("D");
    public static final Logger logger5 = LoggerFactory.getLogger("E");
    public static final Logger logger6 = LoggerFactory.getLogger("F");
    public static final Logger main = LoggerFactory.getLogger("G");

    private static final Map<String, Logger> map = new HashMap<>();

    static {
        map.put("1", logger1);
        map.put("2", logger2);
        map.put("3", logger3);
        map.put("4", logger4);
        map.put("5", logger5);
        map.put("6", logger6);
        map.put("0", logger6);
        map.put("main", main);
    }

    public static Logger get() {
        return get(null);
    }

    public static Logger get(String prefix) {
        String name = Thread.currentThread().getName();
        if(!name.equals("main")) {
            int length = name.length();
            name = name.substring(length - 1);
        }
        return map.getOrDefault(name, logger1);
    }

    public static void main(String[] args) {
        logger1.debug("hello");
        logger2.debug("hello");
        logger3.debug("hello");
        logger4.debug("hello");
        logger5.warn("hello");
        logger6.info("hello");
        main.error("hello");
    }
}

原子性演示

以下代码大意如下:有一个共享变量balance,初始值值为5,使用volatile进行修饰,subtract方法对其进行减5的操作,add方法对其进行加5的操作,先用两个线程分别执行这两个方法,等这两个线程都运行结束之后,最后查看balance的结果

package com.volatiles.test;

import java.util.concurrent.CountDownLatch;

public class AddAndSubtract {

    static volatile int balance = 10;

    public static void subtract() {
        balance -= 5;
    }

    public static void add() {
        balance += 5;
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);
        new Thread(() -> {
            subtract();
            latch.countDown();
        }).start();
        new Thread(() -> {
            add();
            latch.countDown();
        }).start();
        latch.await();
        LoggerUtils.get().debug("{}", balance);
    }
}

我们运行代码,发现结果是10,但是这个代码在我们多线程下是存在问题的,结果还有两种可能,一种是5,一种是15。因为我们的balance -= 5balance += 5并不是一个原子操作。

表面的一行命令,底层可能对应着多行命令,在多线程的情况下,如果他们的执行顺序没有发生交换,那么就不会有问题。但是,一旦他们发生执行顺序发生交换,则会存在着问题。

继续反编译该类(javap -c -v -p AddAndSubtract.class),发现balance += 5已经变为了4行命令,

getstatic指令表示读取静态变量的值,iconst_5指令表示准备了一个数字5,iadd指令表示相加,之后putstatic指令将运行结果写回刚才读取的静态变量

image-20220125174922211

我们知道,多个线程下,cpu执行是在这些线程之下来回切换,cpu可能会调换他们的执行顺序(未加Volatile进行修饰的变量),最终导致执行结果错误

image-20220125175219150

image-20220125175437556

我们修改代码,将balance -= 5balance += 5修改为多行代码的方式,并打上断点进行调试

image-20220125175731517

首先执行subtract方法的int b = balance 此时b的值为10,之后切换为add方法,执行完add方法的所有代码,此时balance的值为15,之后再切换为subtract方法,此时b为10的数据已经为脏数据了,继续往下执行,结果为5,这就导致了结果的错误。

image-20220125180516896

最终得出的结果为5,而正确的结果为10,所以Volatile并不能解决原子性的问题

image-20220125183202482

可见性演示

以下代码大意如下:我们运行一个线程,0.1s后将stop改为true(此时并未对stop变量用volatile进行修饰),当stop为true的时候会停止while循环并输出运行了多少次

package com.volatiles.test;


// 可见性例子
public class ForeverLoop {
    static boolean stop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            LoggerUtils.get().debug("modify stop to true...");
        }).start();

        foo();
    }

    static void foo() {
        int i = 0;
        while (!stop) {
            i++;
        }
        LoggerUtils.get().debug("stopped... c:{}", i);
    }
}

我们运行,发现0.1s后java虚拟机并没有停止运行,且也没有输出结果,明明我们启动一个线程将stop在0.1s后设置为true了,但是他还没有停止运行,这是为什么呢?

image-20220125185301922

难道是我们并没有修改stop的值为true吗?我们修改代码,书写多个线程在0.2s后输出stop的状态

image-20220125185519682

我们发现stop的值为true,明明修改了stop的值,为什么他还没有停止运行呢?

image-20220125185728617

这是因为stop的值初始为false,存储在物理内存当中,线程1执行while循环代码,线程2执行在0.1s后将stop修改为true,我们知道,所有的代码最终都交由cpu进行执行。

image-20220125192517599

首先cpu到物理内存中读取stop的值,第一次读到的值为false,所以它继续循环读取stop的值,这个读取操作非常的快速,在0.1s内可以读取上千万次,我们知道,内存的读写效率是比较低的,每一次大约是几百纳秒。而cpu读取这么多次,发现stop的值还是false,这时java的即时编译器JIT(是java虚拟机的组成部分,主要负责代码的优化)就会发起作用了。

我们的任何一条java代码都会被翻译成java的字节码指令,但是字节码指令还不能交由cpu去执行,他还要通过解释器,解释器会将我们的java字节码指令逐行翻译成机器码,cpu才能认识并执行。但是这个将java字节码指令逐行翻译成机器码这个效率比较低,所以JIT就会对一些热点的字节码进行优化,即一些频繁调用执行的代码。

所以我们的这个while循环在超过JIT所限定的界限时就会被触发代码优化的操作。所以为了减少cpu与物理内存之间的操作,他会自动将我们的代码进行更改,即将stop的值修改为false。原来我们的字节码被JIT替换为了编译后的机器码并将其缓存起来,之后在运行这个代码,就会直接找到这个缓存的机器码交由cpu进行运行,从而减少了中间这个解释的过程,提高效率。当然,如果我们还需要之前的代码,它还可以替换为原来的代码。

所以,即使当其他线程将stop变量修改为了true,线程1也无法停止运行,因为他的代码已经被JIT替换掉了。

我们可以通过设置java虚拟机参数,-Xint,表示只用解释执行java字节码,即不用JIT了。

image-20220125192915891

之后重新运行,发现已经成功输出循环次数

image-20220125193018256

但是这种做法我们并不推荐,因为不使用JIT会影响整个系统的性能。所以,我们并不推荐这样做,我们直接使用volatile修饰这个变量就能解决问题,因为JIT一旦发现我们的变量使用volatile进行修饰,就不会对这个变量进行优化。

有序性演示

必须经过大量的测试才能暴露出来指令重排序这种现象,此处我们使用jcstress-core进行测试,其maven坐标

<dependency>
    <groupId>org.openjdk.jcstress</groupId>
    <artifactId>jcstress-core</artifactId>
    <version>0.14</version>
</dependency>

编写测试代码,x和y都未使用volatile修饰,打包后运行,

package com.volatiles.test;

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.II_Result;

// 有序性例子
// 运行指令:java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -jar jcstress.jar -t day02.threadsafe.Reordering.Case1

public class Reordering {
    @JCStressTest
    @Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    // 出现1,0的情况即表示出现了指令重排序
    @Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
    @State
    public static class Case1 {
        int x;
        int y;

        // @Actor表示在进行测试的过程中都会被一个线程进行执行,这里的两个表示会启动两个线程来执行这两个被@Actor修饰的方法
        // 赋值的操作
        @Actor
        public void actor1() {
            x = 1;
            y = 1;
        }
        // @Actor表示在进行测试的过程中都会被一个线程进行执行,这里的两个表示会启动两个线程来执行这两个被@Actor修饰的方法
        // 获取值的操作,这个结果会与我们的预期值作对比,II_Result表示收集结果,r.r1和r.r2这两个变量会与上方的@Outcome
        // 中的数据做对比(多组用逗号隔开),查看实际值是否与预期值相符,r.r1与每组中的第一个值做比较,r.r2与每组中的第二个值做比较,
        @Actor
        public void actor2(II_Result r) {
            r.r1 = y;
            r.r2 = x;
        }
    }
}

发现出现了指令重排序的情况,

image-20220125195829152

继续编写第二个测试,使用volatile修饰y变量,阻止指令重排序的发生,打包后运行,

package com.volatiles.test;

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.II_Result;

// 有序性例子
// 运行指令:java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -jar jcstress.jar -t day02.threadsafe.Reordering.Case2

public class Reordering {
    @JCStressTest
    @Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    // Expect.FORBIDDEN 表示如果出现指令重排序,则直接报错
    @Outcome(id = "1, 0", expect = Expect.FORBIDDEN, desc = "FORBIDDEN")
    @State
    public static class Case2 {
        int x;
        // 使用volatile修饰y变量,阻止指令重排序的发生
        volatile int y;

        @Actor
        public void actor1() {
            x = 1;
            y = 1;
        }

        @Actor
        public void actor2(II_Result r) {
            r.r1 = y;
            r.r2 = x;
        }
    }
}

发现并未报错,即没有出现指令重排序的情况,且吞吐量相对于前面不加volatile时减少了(M表示千万),这是因为使用volatile之后就不能用JITvolatile修饰的变量做优化了。

image-20220125200808138

继续编写第三个测试,使用volatile修饰x变量,阻止指令重排序的发生,打包后运行,发现报错了。这是为什么呢?

volatile位置不同影响

volatile是使用内存屏障来解决指令重排序的,会为添加上volatile修饰的变量的写和读操作分别加上不同的内存屏障,对volatile的写操作会添加上一个向上的屏障,阻止上面的代码排下来,而对volatile的读取操作会添加上箭头一个向下的屏障,阻止下面的代码跑到上面去。

所以我们如果用volatile修饰x变量的话,

写操作:x=1上面的语句下不来,但是y=1是有可能到上面去的,

读操作:r.r2=x之后的语句不能越过屏障跑上去,但是r.r1=y却是可以下去的

image-20220125214034703

喜欢请关注我

image-20211108230322493

举报

相关推荐

0 条评论