JUC
Volatile 使用场景

单一赋值可以,但是含复合运算赋值不可以(i++ 之类)

UseVolatileDemo.java
volatile int a = 10;
volatile boolean flag = false;

状态标志,判断业务是否结束

UseVolatileDemo.java
/**
 * 使用:作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束
 * 理由:状态标志并不依赖于程序内任何其他状态,且通常只有一种状态转换
 * 例子:判断业务是否结束
 */
public class UseVolatileDemo {
    private volatile static boolean flag = true;
 
    public static void main(String[] args) {
        new Thread(() -> {
            while(flag) {
                //do something......
            }
        },"t1").start();
 
        try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }
 
        new Thread(() -> {
            flag = false;
        },"t2").start();
    }
}

开销较低的读,写锁策略

UseVolatileDemo.java
public class UseVolatileDemo {
    /**
     * 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
     * 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
     */
    public class Counter {
        private volatile int value;
 
        public int getValue() {
            return value;   //利用volatile保证读取操作的可见性
        }
        public synchronized int increment() {
            return value++; //利用synchronized保证复合操作的原子性
        }
    }
}

DCL 双端锁

问题

SafeDoubleCheckSingleton.java
public class SafeDoubleCheckSingleton {
    private static SafeDoubleCheckSingleton singleton;
    //私有化构造方法
    private SafeDoubleCheckSingleton() {
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance() {
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}
  • 单线程环境下会执行如下操作,保证能获取到已完成初始化的实例

    memory = allocate();    // 1:分配对象的内存空间
    ctorInstance(memory);   // 2:初始化对象
    instance = memory;      // 3:设置 instance 指向刚分配的内存地址
  • 多线程环境下,由于重排序导致 2,3 乱序,导致其他线程得到的是 null 而不是完成初始化的对象

    memory = allocate();    // 1:分配对象的内存空间
    instance = memory;      // 3:设置 instance 指向刚分配的内存地址
                            // 注意,此刻对象还没有初始化
    ctorInstance(memory);   // 2:初始化对象

    这种场景在著名的双重检锁(double-checked-locking)中会出现:

    // 注意 volatile
    private volatile static Singleton instance;
     
    public static Singleton getInstance {
        // 第一次 null 检查
        if (instance == null) {
            synchronized(Singleton.class) {      // 1
                // 第二次 null 检查
                if (instance == null) {          // 2
                    instance = new Singleton();  // 3
                }
            }
        }
        return instance;
    }

    其中第3步中实例化 Singleton 分多步执行(分配内存空间、初始化对象、将对象指向分配的内存空间)某些编译器为了性能原因,会将第二步和第三步进行重排序(分配内存空间、将对象指向分配的内存空间、初始化对象)。这样某个线程可能会获得一个未完全初始化的实例。

解决方案

  • 加 volatile 修饰
SafeDoubleCheckSingleton.java
public class SafeDoubleCheckSingleton {
    //通过volatile声明,实现线程安全的延迟初始化。
    private volatile static SafeDoubleCheckSingleton singleton;
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
 
                                      //原理:利用volatile,禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}
  • 采用静态内部类的方式实现
SingletonDemo.java
public class SingletonDemo {
    private SingletonDemo() { }
 
    private static class SingletonDemoHandler {
        private static SingletonDemo instance = new SingletonDemo();
    }
 
    public static SingletonDemo getInstance() {
        return SingletonDemoHandler.instance;
    }
}

静态内部类知识补充

静态内部类的实现方式具有几个特点:

  1. 独立性:静态内部类可以独立于外部类的实例存在,即使外部类没有被实例化,也可以创建静态内部类的对象。
  2. 访问权限:静态内部类可以访问外部类的所有静态成员,包括私有的静态成员。但是,它不能直接访问外部类的非静态成员。
  3. 使用场景:静态内部类通常用于当内部类的行为不依赖于外部类实例时,或者用于构造单例模式等。
OuterClass.java
public class OuterClass {
    private static String staticOuterVariable = "外部类的静态变量";
    private String instanceOuterVariable = "外部类的实例变量";
    
    // 静态内部类
    public static class StaticInnerClass {
        public void display() {
            // 访问外部类的静态变量
            System.out.println(staticOuterVariable);
            // 不能直接访问外部类的实例变量
            // System.out.println(instanceOuterVariable); // 这会产生编译错误
        }
    }
    
    public static void main(String[] args) {
        // 直接创建静态内部类的实例,无需外部类的实例
        OuterClass.StaticInnerClass innerObject = new OuterClass.StaticInnerClass();
        innerObject.display();
    }
}

在这个例子中,StaticInnerClass是一个静态内部类,它可以访问外部类OuterClass的静态成员staticOuterVariable,但是不能访问实例成员instanceOuterVariable。要创建静态内部类的实例,我们不需要OuterClass的实例,直接使用OuterClass.StaticInnerClass即可创建。