饿汉式
1 | /** |
测试代码:
1 | import java.util.ArrayList; |
某次运行结果:
1 | ... |
最后一行打印出来的size是3,难道饿汉式单例模式非线程安全?再查看前面100行打印的显示对象地址,均相同。其实,size计数不正确是HashSet
导致的。HashSet
不是线程安全的,可以用ConcurrentHashMap
来替代相关代码:
1 | // Set<Singleton> singletonSet = new HashSet<>(); |
懒汉式
- 懒汉式-非线程同步
1 | /** |
再修改测试代码的这一行后再测试:
1 | // SingletonType1 singleton = SingletonType1.getInstance(); |
某次结果如下:
1 | Thread-2:cn.lzzeng.home.singleton.SingletonType2@11f7d35 |
从打印的前2行来看,确实出现了获取实例不唯一的现象,可见是非线程安全的。
- 懒汉式-线程同步
1 | /** |
此法使用synchronized
修饰static方法getInstance
,实测未见异常。
如果改用类的class对象作为同步监视器对象,即如下:
1 | /** |
这样看起来似乎没问题,但实测出现如下结果:
这是为什么?上一种synchronized修饰方法的方式中,加锁后,进入方法执行时,首先会判断一次是否已存在单类的实例,而后一种方式synchronized后没有这个判断步骤。也就是说,要想只创建一个实例,加锁的区域必须包含判断是否已实例化的步骤。否则,仅仅是保证了多个线程通过new创建实例的过程在时序上不会发生重叠。
正确的写法:
1 | public static SingletonType3 getInstance() { |
双重校验锁
1 | /** |
双重校验锁方式采用双锁机制,安全且在多线程情况下能保持高性能。与第二种懒汉式的区别只在于多了一个volatile
。
静态内部类式
1 | /** |
这种方式同样利用了类加载机制来保证初始化实例时只有一个线程,它跟饿汉式不同的是:饿汉式只要 Singleton类(即SingletonType1
)被加载了,就会实例化instance
,没有达到懒加载的效果。而这种方式是Singleton类被加载后,只有显式地调用getInstance
方法时,才会加载SingletonHolder
类,从而实例化instance
。
枚举式
1 | /** |
枚举方式是Effective Java作者Josh Bloch提倡的方式。特点:
- 避免了多线程同步问题,线程安全
- 支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化
- 可以防止利用反射强行构建单例对象
- 利用内部类实现懒加载
(End)