单例模式是最简单的一个设计模式,来看其定义:
定义:Ensure a class has only one instance, and provide a global point of access
to it.
翻译过来就是确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实
例。 那么如何保证一个类只有一个实例呢?我们通过代码来研究一下。
public class Singleton {
//必须是静态的
private static Singleton instance;
//让构造函数为private,这样该类就不会被实例化
private Singleton (){}
//必须是静态的
public static Singleton getInstance() {
//判断系统是否已经有这个单例,如果有则返回,如果没有则创建
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
所以关键代码就是保证*静态的变量、静态的方法、私有的构造方法,按照这个套路,我们容易就写出单例模式的代码,但是这个代码不安全!
多线程问题
上述的这种方式称为懒汉式,这种实现最大的问题就是不支持多线程。因为没有加锁,所以严格意义上它并不算单例模式,当多个线程去获取实例时,这个类可就不仅只有一个实例了。
如何解决?
怎样既要保证lazy loading(懒加载)又要保证线程安全呢?我们通常有3种方法。
1. 使用synchronized(加锁)
synchronized可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
public class Singleton {
private static Singleton instance;
private Singleton (){}
//对方法使用synchronized关键字使其线程安全
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方法虽然简单,但是效率低,其原因就是进行线程阻塞和唤醒的代价较高。究其深层原因,那就是线程的阻塞和唤醒,需要操作系统在用户态与内核态之间切换。
2. 使用volatile-双重验证加锁
volatile
一句话概括这个关键字的功能:被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象
所以,volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。
代码
public class Singleton {
//使用volatile修饰
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
上述代码完美的解决了同时保证线程安全和高性能的两大要求,原理也很简单,我们知道,线程不安全往往发生在第一次调用时,那么我们只要在第一次调用时才访问锁,第一次后都不用锁,就可以解决问题了,即使用volatile来修饰静态变量,保持其可见性。
过程:当多个线程第一次调用getSingleton()方法时,singleton为null,这时获得锁的线程拿到了实例,由于被volatile修饰的变量提供了可见性,任何一个线程对其的修改将立即对其他线程可见,所以其他线程知道了该实例已存在,就不会挨个去获得锁去new实例了,线程会直接读取已有的实例。
3. 静态内部类
public class Singleton {
//静态内部类
private static class SingletonFD {
private static Singleton instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return SingletonFD.instance;
}
}
原理
外部类初次加载,会初始化静态变量、静态代码块、静态方法,但不会加载内部类和静态内部类,这是其优点,内部类不被加载则不去初始化instance,故而不占内存,只有第一次显式调用getInstance() 方法时,才会显式装载SingletonFD类,从而实例化instance,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
单例模式中最常用的写法是“饿汉式”,它不是懒加载的,而是在类加载时就实例化对象,并且是线程安全的。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
缺点:容易产生垃圾对象
优点:没有加锁,执行效率高
原理
基于类加载机制规避了线程安全问题,但是类加载时就初始化实例会浪费内存。
说了这么多,单例模式的使用场景是啥?
1.要求生成唯一序列号的环境
2. 在整个项目中需要一个共享访问点或共享数据
3.创建一个对象需要消耗的资源过多
4.需要定义大量的静态常量和静态方法(如Utils类)的环境
5.session会话
… …