关于单例、关于DCL:Double Check Lock、关于volatile

Java程序员面试都会被问到单例模式,有的公司(如1.)还会问单例模式的各种实现。

结论

(1)单例的实现请直接看第4和第5。
(2)volatile总共实现了两个功能:
a.多线程间的可见性问题
b.对象实例化时的完整性问题
下面阐述下具体写法。

1.

public class Singleton {
    private Singleton() {
        //must
    }

    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }
}

上面的写法最简单最有效。但是没有实现延迟初始化。
注意
(1)上面 私有的构造方法 必须要有,否则JVM会自动添加默认的构造方法,这个是public的。
(2)不要将成员变量instance设置为public的,这违反了成员私有的原则。而且instance成员变量必须是static的。如果不是static的,getInstance方法无法访问非静态成员。
(3)成员方法必须是static的。否则必须要有Singleton对象才能调用,这是一个悖论。本来getInstance方法就是要拿到Singleton对象。
不延迟加载有什么问题
(1)如果不延迟加载的话,在JVM加载Singleton这个类时,由于instance是static的,所以会进行初始化,new的过程很耗时的话(可能有关于对象的其他初始化操作),应用程序启动的会很慢,降低体验的友好性。
(2)如果不延迟加载的话,假设Singleton这个类还有其他的功能,如提供了public static void sayHello()的方法,当初次调用sayHello方法时,会实例化Singleton对象。然而,在这里可能并用不到这个对象,造成了浪费。
注意,只有当应用程序访问到Singleton时,类加载器才会加载Singleton class。
静态的成员变量肯定是在类加载时就进行了初始化操作

2.

public class Singleton {
    private Singleton() {
        //must
    }

    private static Singleton instance = null;

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

注意
(1)instance = null; 这样即使加载了类,也不会出现创建对象的耗时。
(2)synchronized关键字的使用。如果没有synchronized关键字,在高并发情况下,会new出多个instance实例。
上面的代码实现了延迟加载。但是好像有一些问题。如果getInstance的调用频率很高的话,每次都synchronized同步访问,效率岂不是很低。因此,要降低锁的力度。

3.

public class Singleton {
    private Singleton() {
        //must
    }

    private static Singleton instance = null;

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

上面的代码其实就是大名鼎鼎的DCK了。这个大大提高了并发访问性能,而且实现了延迟初始化。如果第一个instance不为null的话,就可以直接返回了,减少了同步开销。只有当instance是null的时候,才会进行同步操作。这个时候需要在进行一次instance是否为null的检查。因为,有可能两个线程都判断instance为null,一个线程加锁,对instance进行了实例化,释放锁,另外一个线程拿到锁后,不进行instance是否为null的再次判断,会再次进行instance的实例化。
上面代码有一个问题:instance不是volatile的。看new操作的反汇编代码,其实他包含3条汇编指令:new、dup、init。这样当一个线程执行了new汇编指令后,被唤出。一个新的线程到来,在第一个判断instance是否为null时,发现instance已经不为null了,直接返回。这时的instance其实是不完整的。而且,即使一个线程实例化了instance,由于每个线程都有自己的working缓存,可能另一个线程看不到前一个线程对instance的操作。

4.

public class Singleton {
    private Singleton() {
        //must
    }

    private static volatile Singleton instance = null;

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

5.

上面的实现多少有些复杂。请看终极实现方式:

public class Singleton {
    private Singleton() {
        //must, otherwise JVM create default constructor(public)
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }
}

内部的静态类SingletonHolder,只有当getInstance方法被调用时才会被加载。注意SingletonHolder这个内部类是static的,否则其成员变量不能声明成static的。JVM在类加载时保证了线程安全性问题,以及instance的唯一性。

6.DCL的应用

在有些场景下,我们通常会将RPC调用的实例放在一个Map中,当我们需要一个服务时,从这个map中拿,如果实例是null的,我们会new出一个服务实例然后放在map中。在应用中new一个服务实例往往会很耗时,因此要考虑降低这种new的频率。通常的写法如下:

public class Service {
    private static final Map<String, Service> serviceMap = new ConcurrentHashMap<>();

    public Service getService(String serviceName) {
        Service service = serviceMap.get(serviceName);
        if (service == null) {
            synchronized (Service.class) {
                service = serviceMap.get(serviceName);
                if (service == null) {
                    service = new Service();
                    serviceMap.put(serviceName, service);
                }
            }
        }
        return service;
    }
}

即使serviceMap是ConcurrentHashMap,也必须要有synchronized (Service.class)。否则会new出多个Service对象。

上面的代码,IDEA会提示我这么做:

public class Service {
    private static final Map<String, Service> serviceMap = new ConcurrentHashMap<>();

    public Service getService(String serviceName) {
        Service service = serviceMap.get(serviceName);
        if (service == null) {
            synchronized (Service.class) {
                service = serviceMap.computeIfAbsent(serviceName, k -> new Service());
            }
        }
        return service;
    }
}

逼格一下高太多!懵圈了!我用的JDK1.8。这个其实是JDK1.8的新特性,Function接口和lambda表达式。有空再写写吧。争取下周写。

参考资料:
1. 《Java程序性能优化:让你的Java程序更快、更稳定》
2. 《Java并发编程实战(中文版)》

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值