单例模式是最常用也是最基础的一种模式,我们来好好地总结一下吧。
目录
1. 第一种实现方式-(饿汉,常用)
2. 第二种实现方式-(懒汉,不安全)
3. 第三种实现方式-(加锁的懒汉,性能低)
4. 第四种实现方式-(静态块,可以)
5. 第五种实现方式–(静态内部类,推荐)
6. 第六种实现方式-(枚举,推荐)
7. 第七种实现方式-(重要,面试)
1. 第一种实现方式-饿汉,常用
/**
* 单例模式,饿汉式,线程安全
*/
public static class Singleton01{
private static Singleton01 instance = new Singleton01();
private Singleton01(){}
public static Singleton01 getInstance(){
return instance;
}
}
饿汉,只要有就先加载出来让我吃。
首先是写明私有的构造方法防止被new,然后直接就实例化,最后调用,不存在线程安全问题。
2. 第二种实现方式-懒汉,不安全
/**
* 单例模式,懒汉式,线程不安全
*/
public static class Singleton02{
private Singleton02(){}
private static Singleton02 instance = null;
public static Singleton02 getInstance(){
if(instance == null){
instance = new Singleton02();
}
return instance;
}
}
懒汉,就是我饿的时候再找吃的,属于懒加载,在instance = new Singleton02();可能会出现线程安全问题,因为new一个对象在JVM底层做了如下工作:
给 instance 分配内存
调用 Singleton 的构造函数来初始化成员变量
将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
显然是不能保证原子性的。
3. 第三种实现方式-加锁的懒汉,性能低
/**
* 单例模式,懒汉式,线程安全,多线程环境下效率不高
*/
public static class Singleton03{
private Singleton03(){}
private static Singleton03 instance = null;
public static synchronized Singleton03 getInstance(){
if(instance == null){
instance = new Singleton03();
}
return instance;
}
}
既然提到不安全,那么就加锁,显然是可以解决线程安全性问题的,但是效率会比较低。
4. 第四种实现方式-静态块,可以
/**
* 单例模式,懒汉式,变种,线程安全
*/
public static class Singleton04{
private Singleton04(){}
private static Singleton04 instance = null;
static {
instance = new Singleton04();
}
public static Singleton04 getInstance(){
return instance;
}
}
当第一次引用getInstance()方法的时候,访问静态内部类中的静态成员变量,此时该内部类需要调用static代码块(因为首次访问该类)。而后再次访问getInstance()方法会直接返回instace引用。这种做法相对于传统做法更加巧妙。
5. 第五种实现方式–静态内部类,推荐
/**
* 单例模式,使用静态内部类,线程安全【推荐】
*/
public static class Singleton05{
private Singleton05(){}
private static final class SingletonHolder{
private static final Singleton05 instance = new Singleton05();
}
public static Singleton05 getInstance(){
return SingletonHolder.instance;
}
}
定义一个私有的内部类,在第一次用这个嵌套类时,会创建一个实例。而类型为SingletonHolder的类,只有在Singleton.getInstance()中调用,由于私有的属性,他人无法使用SingleHolder,不调用Singleton.getInstance()就不会创建实例。
优点:达到了lazy loading的效果,即按需创建实例。
6. 第六种实现方式-枚举,推荐
/**
* 静态内部类,使用枚举方式,线程安全【推荐】
*/
public enum Singleton06{
INSTANCE;
}
在《Effective Java》最后推荐了这样一个写法,简直有点颠覆,不仅超级简单,而且保证了现场安全。这里引用一下,此方法无偿提供了序列化机制,绝对防止多次实例化,及时面对复杂的序列化或者反射攻击。单元素枚举类型已经成为实现Singleton的最佳方法。
很多人会对枚举法实现的单例模式很不理解。这里需要深入理解的是两个点:
- 枚举类实现其实省略了private类型的构造函数
- 枚举类的域(field)其实是相应的enum类型的一个实例对象
对于第一点实际上enum内部是如下代码:
public enum Singleton {
INSTANCE;
// 这里隐藏了一个空的私有构造方法
private Singleton () {}
}
比较清楚的写法是:
public class SingletonExample5 {
private SingletonExample5(){}
public static SingletonExample5 getInstance(){
return Singleton.INSTANCE.getInstance();
}
private enum Singleton{
INSTANCE;
private SingletonExample5 singleton;
//JVM保证这个方法绝对只调用一次
Singleton(){
singleton = new SingletonExample5();
}
public SingletonExample5 getInstance(){
return singleton;
}
}
}
7. 第七种实现方式-重要,面试
/**
* 静态内部类,使用双重校验锁,线程安全【推荐】
*/
public static class Singleton07{
private Singleton07(){}
private volatile static Singleton07 instance = null;
public static Singleton07 getInstance(){
if(instance == null){
synchronized (Singleton07.class){
if(instance == null){
instance = new Singleton07();
}
}
}
return instance;
}
}
思想:先判断一下是不是null,然后加锁,再判断一下是否为null。如果还是null,则可以放心地new。
还是不大明白为什么要判断两次null?
第一次null很简单,如果不为null,直接return,不需要进锁了。如果为null,说明有可能是第一次进来。
这里第一次进来的线程可能不止一个,假设是两个,分别为线程A和线程B,那么进行排队,假设A先进来,如果没有Null判断,那么它就直接new,释放锁没这个时候B获得锁,还是直接new,不就出现了两个对象了吗?