单例模式
单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)
1.饿汉模式
饿汉模式是一种比较常见且简洁的写法。在类加载的时候就实例化了类,也没有线程安全问题,并且没有使用锁,效率较高。但是如果这个类整个应用都没使用到的话,白白浪费了内存。
public class SingletonDemo {
//饿汉模式
private static SingletonDemo sInstanceDemo = new SingletonDemo();
private SingletonDemo() {}
public static SingletonDemo getInstance() {
return sInstanceDemo;
}
}
2.懒汉模式
懒汉模式也就是懒加载,需要使用的时候再加载。 懒汉模式中有两种情况,一种是线程不安全的,一种是加了锁线程安全的,具体情况具体使用。
线程非安全
public class SingletonDemo {
//懒汉模式(线程不安全)
private static SingletonDemo sInstanceDemo;
private SingletonDemo() {}
public static SingletonDemo getInstance() {
if(sInstanceDemo == null) {
sInstanceDemo = new SingletonDemo();
}
return sInstanceDemo;
}
}
这种写法在面对单线程的时候可以适用,但是当出现多线程情况调用的时候,比如线程调用跑到sInstance == null的时候,另一个线程也跑到了null,并且初始化了实例,但是还没来得及同步到内存,这个时候就导致内存中存在两个实例,就违背了单例的原则。所以我们需要下面那种线程安全的写法,增加锁。
线程安全
public class SingletonDemo {
//懒汉模式(线程安全)
private static SingletonDemo sInstanceDemo;
private SingletonDemo() {}
public static synchronized SingletonDemo getInstance() {
if(sInstanceDemo == null) {
sInstanceDemo = new SingletonDemo();
}
return sInstanceDemo;
}
}
这种写法保证了线程安全问题,但是每次调用都需要进行同步(锁),效率会比较低。
3.双重检测
双重检测实际上是懒加载两种方式的结合体,既保证了线程安全,又一定程度上提高了效率。是平时用到最多的单例写法。
第一步if(sInstanceDemo == null)不需要锁同步,直接进入判断,提升了效率。
第二步synchronized则是进行锁同步,保证线程安全。
第三步if(sInstanceDemo == null)非空检查是因为同时存在多个线程调用时,线程a获得锁并创建实例之后释放锁,之前一起竞争的其他线程获得锁,首先会判断实例是否非空,如果已经创建了,就不会继续去创建实例
public class SingletonDemo {
//双重检测
//volatile 保证变量可见性 禁止指令重排
private static volatile SingletonDemo sInstanceDemo;
private SingletonDemo() {}
public static SingletonDemo getInstance() {
//第一次检测
if(sInstanceDemo == null) {
//锁判断
synchronized (SingletonDemo.class) {
//第二次检测
if(sInstanceDemo == null) {
sInstanceDemo = new SingletonDemo();
}
}
}
return sInstanceDemo;
}
}
这里使用了volatile,需要讲一下:
在真正new对象的时候,即上面代码sInstanceDemo = new SingletonDemo(); 这个操作不是原子性的。这句话正常来说是执行三个步骤
1.给 sInstanceDemo 分配内存
2.调用 SingletonDemo 的构造函数来初始化对象
3.将sInstanceDemo对象指向刚刚分配的内存
正常情况下按照1-2-3的顺序执行,但是JVM编译的时候有可能指令重排,也就是变成1-3-2的顺序,这个时候sInstanceDemo已经是非空的了,但是没有初始化(这个时候如果其他线程再次调用,就会直接拿未初始化的状态去使用就会出问题),导致return之后会有问题。所以我们需要使用volatile来保证这句话是原子操作,禁止指令重排,保证执行完初始化后再调用读的操作if(sInstanceDemo == null)。
4.静态内部类
public class SingletonDemo {
private SingletonDemo() {}
//静态内部类
private static class SingleHolder{
static SingletonDemo sInstanceDemo = new SingletonDemo();
}
public static SingletonDemo getInstance() {
return SingleHolder.sInstanceDemo;
}
}
静态内部类是只有被调用的时候才会初始化,只有当线程第一次调用getInstance方法后,SingleHolder会被初始化。另外不同线程调用getInstance方法都是调用的SingleHolder的同一个对象,所以也是线程安全的。
5.枚举
public class MyApplication {
public static void main(String args[]) {
SingletonEnum.INSTANCE.fun();
}
}
enum SingletonEnum{
INSTANCE;
public void fun() {
}
}
比较少见,但是也是线程安全的。
总结
模式 | 优点 | 缺点 |
---|---|---|
饿汉式 | 效率高,线程安全 | 非懒加载 |
懒汉式 | 懒加载,线程安全 | 效率低 |
双重检测 | 懒加载,线程安全,效率高 | 无 |
静态内部类 | 懒加载,线程安全,效率高 | 无 |
枚举 | 线程安全,效率高 | 无 |
这么多单例模式,都需要根据实际情况使用不同的方式,最常使用的还是双重检测和静态内部类。达到的结果是一样的,还有就是如果你的项目中100%使用到这个实例,那么饿汉式也没啥问题,因为迟早是需要实例化的,双重检测的锁同步反而降低了效率。