单例模式是我们面试中经常会遇到的一道题,看似简单,实则有很多玄机,今天来好好总结一下:
1. 饿汉模式
饿汉模式的单例模式是一种较为简单的写法,代码如下:
public class Singleton{
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
优点
虽然写法简单,但不要小瞧它,它是线程安全的绝对单例,当面试时被要求写一个线程安全的单例模式时完全可以用这个写法,还可以引申出一些其他的问题。
缺点
这种写法的缺点也比较明显,就是在有其他静态方法时,实例会在还没有使用它们的时候就加载好了,当对象很大时会造成对内存资源的浪费。
还有就是它不能防止反序列化和反射时产生新的实例。
PS:
-反序列化: 序列化是指把JAVA对象转换为字节流的过程,与之相对反序列化就是将字节流重构为JAVA对象的过程。
-反射:指程序在运行过程中可以获取到类的属性和方法,实现动态创建对象。
2. 懒汉模式
懒汉模式的代码如下:
public class Singleton{
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
优点
解决了饿汉模式下出现的问题,实例在未被使用时不会被创建,采用了延迟加载的办法
缺点
非线程安全,当两个线程发现instance都为null时,就会都去执行初始化操作。
不能防止反序列化和反射产生新的实例。
3.方法锁
public class Singleton{
private static Singleton instance = null;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
优点
线程安全的绝对单例,面试时可写。
缺点
由于方法体中可能还存在其他功能,方法加锁会导致并发性能下降。
无法防止反序列化和反射产生新的实例。
4.双重检查锁
public class Singleton{
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if(null == instance){// one
synchronized(Singleton.class){
if(null == instance){// two
instance = new Singleton();// three
}
}
}
return instance;
}
}
优点
与方法锁相比,并发性能更高。
缺点
并非绝对线程安全。
无法防止反序列化和反射产生新的实例。
线程不安全的原因:
java指令乱序执行,代码中three部分执行时分为三部分:
1 在内存中分配一块内存
2 调用构造方法
3 将内存地址指向instance(此时instance不为null)
正常是123的执行顺序,但是由于java指令乱序执行的特点,有可能出现132的情况,例如两个线程A和B,线程A在获得时间片按照132的指令顺序执行,当执行到2时,线程B获得到时间片,此时instance已经不为null,直接返回了instance,但此时构造方法还没有执行,返回的instance是未被初始化的对象,会产生错误。
5.双重检查锁与volatile联用
public class Singleton{
private static volatile Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if(null == instance){
synchronized(Singleton.class){
if(null == instance){
instance = new Singleton();
}
}
}
return instance;
}
}
优点
线程安全并且绝对单例。
线程安全的原因:
volatile不仅可以每次都从主存中读取变量,而且还屏蔽了指令重排,所以通过volatile修饰后一定是指令一定按照顺序执行的。
缺点
无法防止反序列化和反射产生新的实例。
6.静态内部类
public class Singleton{
private static class InnerHolder{
static final Singleton instance = new Singleton();
}
private Singleton(){
}
public static Singleton getInstance(){
return InnerHolder.instance;
}
}
优点
只有在使用的时候才会加载实例,这是这种写法与饿汉模式的区别,而且这种写法也是线程安全并且绝对单例。
线程安全的原因:
static修饰的类只会被初始化一次。
缺点
无法防止反序列化和反射产生新的实例。
7.枚举类
public class SingletonFactory{
private enum EnumSingleton{
factory;
private Singleton instance;
private EnumSingleton(){
instance = new Singleton();
}
public static Singleton getInstance(){
return instance;
}
}
public static Singleton getInstance(){
return EnumSingleton.factory.getInstance();
}
}
class Singleton{
public Singleton(){
}
}
优点
枚举类的构造方法是在类加载时被实例化,因此也是线程安全并且绝对单例,同时防止反射和反序列化产生的新的实例。
缺点
与饿汉模式类似,会影响并发性能。
写在最后
今天老大让和一个有9年工作经验的来面试的人聊聊技术,我就让他写一个线程安全的单例模式,结果他写了一个方法锁的写法,里面用了两次synchronized,我问他问什么要用两次,结果他说这是在网上看到的云云,具体原因也没说出来,感觉就是死记硬背下来的,关键是还没写对。
我觉得通过这事也给自己敲响了警钟,如果做事情不求甚解,这样子混个5年甚至10年又有什么用呢,和两三年工作经验的人水平差不了多少。一定要趁着年轻多积累多研究,加油!