单例模式是最为人熟知的一种设计模式,看似是最简单一种设计模式,其实一点不简单,光单例模式就不光一种,而且期间还会引出一些其他方面的问题,我们下面就来看一下。
## 一. 饿汉模式
```java
public class HungrySingleton {
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
```
第一种是饿汉模式,这种模式很好理解,就是当类加载的时候就初始化这个单例类,不管是不是真正用到,所以存在着浪费的可能,如何改进呢?我们看下懒汉模式。
## 二. 懒汉模式
```java
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
```
第二种懒汉模式单例,是在第一次调用getInstance方法的时候去初始化这个单例,同时在方法上加了synchronized关键字保证线程安全,所以解决了上面饿汉模式浪费的问题,但是它也是有问题的,虽然没有饿汉模式的初始化浪费的问题,但是我们看到由于synchronized关键字的存在,导致同一时间只能有一个线程进入getInstance这个方法,如果在多线程并发的情况下,必然会导致性能的下降(当然,懒汉模式还有一个缺点,会被反射破坏单例,这个后续文章会降到这点,目前先忽略这个),要解决这个问题我们看下面第三种方式。
## 三. Double Check
```java
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
//1.分配内存给这个对象
//2.初始化对象
//3.设置lazyDoubleCheckSingleton 指向刚分配的内存
}
}
}
return lazyDoubleCheckSingleton;
}
}
```
以上就是DoubleCheck的单例实现,首先它解决了懒汉模式中对整个方法加锁导致性能下降的问题,当调用这个单例的时候会首先检查是否为null,如果不是null,说明已经被初始化了,就会直接返回,从而多线程并发的时候也不会被锁住,但是如果当这个类还没有被初始化的时候还是有懒汉模式那样的性能下降问题,但是这只会出现在第一次初始化的时候,应该说已经好很多了,而且DoubleCheck也没有饿汉模式那样浪费内存的问题,延迟了对象的初始化,所以说还是比较完美的解决了饿汉和懒汉模式的问题。
可以看到这种模式在变量上面有个volatile这个关键字,之所以要用到这个关键字,我们看到以上代码中在lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();这里下面有3行注释中,我们注意要new出一个对象赋给一个变量一般就是这3步,但是jvm虚拟机在实际执行中会有自己的优化步骤,所以上面3步的第二和第三步有时候是会调换顺序的,所以导致先执行第三步,再执行第二步,如果按照这个顺序的话,在多线程并发的时候,有概率会是正好一个新线程进行到这个方法,同时另一个线程正好执行完了第三步,还没有执行完第二步,这时这个新线程就会判断lazyDoubleCheckSingleton不是null,从而执行执行了最后的return,而实际上这个单例还没有初始化完毕,最终导致程序失败,这就是jvm的重排序导致了,volatile这个关键字就是禁止重排序(当然volatile还是多线程内存可见性的功能,这个另一个它的重要特性),从而防止多线程中出现的这个问题。当然还有一种方法也可以解决这个问题,我们接着往下看。
## 四. 静态内部类
```java
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
}
```
这是静态内部类的单例模式,它也不会受到上面说的jvm重排序问题的影响。我们看到这里的类有个私有的静态内部类,和一个对外的public静态方法getInstance,获取单例就是从这个getInstance获取的,那么这种写法为什么可以做到和DoubleCheck一样的作用呢?首先我们先说明一下,一个类什么时候会被加载,有以下五种条件的时候,一个类会被加载:
1.实例被创建
2.静态方法被调用
3.静态成员被赋值
4.静态成员被使用,并且不是常量成员
5.是顶级类,并且类中有嵌套的断言语句
在这5种条件被满足时,一个类会被加载,而一个类被加载的时候会有有个Class对象的初始化锁,这个锁也只运行一个线程进入。我们看上面代码,比如当线程0调用getInstance的时候,相当于满足条件2,从而外部类StaticInnerClassSingleton被加载,而这个方法的return返回的是内部静态类的静态成员,从而满足条件3和4,从而这个单例开始被初始化,由于类初始化是有锁的,所以只有线程0才能看到,如果这时还有1个线程1进行这个方法,如果这时候还没被初始化完成,它进不去的,自然也就不存在什么重排序问题,如果已经初始化完成,由于是静态成员,所以会直接返回,自然这也是一种线程安全的单例模式。

> 多线程访问Class初始化模型
## 五.枚举单例
```java
public enum EnumInstance {
INSTANCE{
protected void printTest(){
System.out.println("hello");
}
};
protected abstract void printTest();
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
```
调用如下:
```java
EnumInstance instance = EnumInstance.getInstance();
instance.getData();
```
以上是枚举单例,可能一般平时我们用的不多,用到可能也是比较简单的,但是枚举类本身就是一个单例,从底层保证的单例的特性,所以具体使用看我们平时的需求。
以上就是一些常用的单例模式,还有些比如容器单例,就是把对象放入一个map类中,再次取出的时候还是从这个map来取,也可以看成是一个缓存的对象用作单例,还有利用ThreadLocal线程特性来模拟单例。所有这些单例可能都有适用的场景,我们需要根据自己的场景来适用。
单例模式