今天遇到一个很有趣的问题,由于业务要求,需要懒初始化一个实例变量。

简单方法

很顺手就写出下面的代码。

public class LazyFieldInitializer {
    private Object obj = null;

    public LazyFieldInitializer(){

    }

    public void someOp(){
        if(obj == null){
            obj = new Object();
        }
    }

    public void otherOp(){

    }

    public static void main(String[] args) {
        LazyFieldInitializer instance = new LazyFieldInitializer();
        instance.someOp();
    }
}

但这种方法存在问题,线程不安全,当两个线程同时调用someOp方法,obj变量被初始化了两次。

加个锁吧

public class LazyFieldInitializer {
    private Object obj = null;

    public LazyFieldInitializer(){

    }

    public void someOp(){
        synchronized (this) {
            if (obj == null) {
                obj = new Object();
            }
        }
    }

    public void otherOp(){

    }

    public static void main(String[] args) {
        LazyFieldInitializer instance = new LazyFieldInitializer();
        instance.someOp();
    }
}

这种方法虽说没问题,就是效率不高,每次执行someOp方法都会锁this。

双重校验

好了,来个双重校验吧

public class LazyFieldInitializer {
    private Object obj = null;

    public LazyFieldInitializer(){

    }

    public void someOp(){
        if (obj == null) {
            synchronized (this) {
            	if(obj == null) {
                	obj = new Object();
                }
            }
        }
    }

    public void otherOp(){

    }

    public static void main(String[] args) {
        LazyFieldInitializer instance = new LazyFieldInitializer();
        instance.someOp();
    }
}

这次没问题了吧。很可惜还是有问题,关键在obj = new Object();这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给obj分配内存
  2. 调用Object的构造函数来初始化成员变量
  3. 将obj对象指向分配的内存空间(执行完这步obj就为非null了)

这个就是JVM很有特色的指令重排序优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在3执行完毕、2 未执行之前,被另一个线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),这个线程拿着这个obj引用去干活,自然就会出问题。

规避指令重排序优化的办法

  1. 使用volatile关键字禁止指令重排序优化

    public class LazyFieldInitializer {
    private volatile Object obj = null;
    
    public LazyFieldInitializer(){
    
    }
    
    public void someOp(){
        if (obj == null) {
            synchronized (this) {
            	if (obj == null) {
                	obj = new Object();
                }
            }
        }
    }
    
    public void otherOp(){
    
    }
    
    public static void main(String[] args) {
        LazyFieldInitializer instance = new LazyFieldInitializer();
        instance.someOp();
    }
    }
    

volatile关键字在这里有两层含义,一个是禁止JVM对该变量的指令重排序优化,另一个是使这个变量的修改对其它线程可见。

Java单例

查阅JVM的指令重排序优化相关文章,还看到Java单例写法的文章,这里小小总结一下。

/**
 * Created by jeremy on 16/6/11.
 * 懒汉模式, 线程不安全
 */
public class Singleton1 {
    private static Singleton1 instance;
    private Singleton1(){

    }

    public void sayHello(){
        System.out.println("hello");
    }

    public static Singleton1 getInstance(){
        if(instance == null){
            instance = new Singleton1();
        }
        return instance;
    }

    public static void main(String[] args) {
        Singleton1.getInstance().sayHello();
    }
}
/**
 * Created by jeremy on 16/6/11.
 * 懒汉模式, 线程安全
 */
public class Singleton2 {
    private static Singleton2 instance;
    private Singleton2(){

    }

    public void sayHello(){
        System.out.println("hello");
    }

    public static synchronized Singleton2 getInstance(){
        if(instance == null){
            instance = new Singleton2();
        }
        return instance;
    }

    public static void main(String[] args) {
        Singleton2.getInstance().sayHello();
    }
}
/**
 * Created by jeremy on 16/6/11.
 * 饿汉模式, 类变量类加载时初始化, 线程安全
 */
public class Singleton3 {
    private static Singleton3 instance = new Singleton3();
    private Singleton3(){

    }

    public void sayHello(){
        System.out.println("hello");
    }

    public static Singleton3 getInstance(){
        return instance;
    }

    public static void main(String[] args) {
        Singleton3.getInstance().sayHello();
    }
}
/**
 * Created by jeremy on 16/6/11.
 * 饿汉模式, 类变量类加载时在类的静态初始化块里初始化, 线程安全
 */
public class Singleton4 {
    private static Singleton4 instance = null;
    static {
        instance = new Singleton4();
    }
    private Singleton4(){

    }

    public void sayHello(){
        System.out.println("hello");
    }

    public static Singleton4 getInstance(){
        return instance;
    }

    public static void main(String[] args) {
        Singleton4.getInstance().sayHello();
    }
}
/**
 * Created by jeremy on 16/6/11.
 * 懒汉模式, 采用内部静态类,线程安全
 */
public class Singleton5 {
    private static class Singleton5Holder {
        private static final Singleton5 instance = new Singleton5();
    }
    private Singleton5(){

    }

    public void sayHello(){
        System.out.println("hello");
    }

    public static Singleton5 getInstance(){
        return Singleton5Holder.instance;
    }

    public static void main(String[] args) {
        Singleton5.getInstance().sayHello();
    }
}
/**
 * Created by jeremy on 16/6/11.
 * 懒汉模式, 枚举实现, 线程安全
 */
public enum Singleton6 {
    INSTANCE;

    Singleton6(){

    }

    public void sayHello(){
        System.out.println("hello");
    }

    public static void main(String[] args) {
        Singleton6.INSTANCE.sayHello();
    }
}
/**
 * Created by jeremy on 16/6/11.
 * 懒汉模式, 双重校验, 采用volatile规避指令重排序优化, 线程安全
 */
public class Singleton7 {
    private static volatile Singleton7 instance;

    private Singleton7(){

    }

    public void sayHello(){
        System.out.println("hello");
    }

    public static Singleton7 getInstance(){
        if(instance == null){
            synchronized (Singleton7.class){
                if(instance == null){
                    instance = new Singleton7();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        Singleton7.getInstance().sayHello();
    }
}

总结

Java还是有不少暗坑的,写代码时得小心了。记得大学时看过Java Puzzlers,那上面列举过不少这样的Java陷阱,抽时间要重读一读这本书了。