一、概述
单例模式属于一种创建型设计模式。单例模式的定义就是某一个类在系统中只需要有一个实例对象,而且对象是由这个类自行实例化并提供给系统其它地方使用,这个类称为单例类。单例模式是GOF 23种设计模式中最简单的一种,但同时也是在项目中接触最多的一种。
二、优缺点
优点:由于单例模式只生成了一个实例,避免了到处new对象,所以能够节约系统资源,减少性能开销,提高系统效率,同时也能够严格控制客户对它的访问。
缺点:也正是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,这样扩展起来有一定的困难。
三、实现方式
单例模式在多线程环境下,肯定要考虑线程安全问题。下面围绕「线程安全」,下面一一列举单例模式的五种实现方式:「饿汉式」、「懒汉式」、「双重检测锁式」、「静态内部类式」和「枚举单例」。
1. 饿汉式
线程安全,这种写法是最常用,简单实用,唯一的缺点就是当类加载的时候会生成一个对象,但是这种写法是线程安全的。类加载到内存后,会实例化一个单例,保证线程安全。
public class SingleTon1 {
private static final SingleTon1 instance = new SingleTon1();
private SingleTon1(){
}
public static SingleTon1 getInstance(){
return instance;
}
}
测试
public void test1(){
SingleTon1 instance1 = SingleTon1.getInstance();
SingleTon1 instance2 = SingleTon1.getInstance();
System.out.println(instance1.hashCode() == instance2.hashCode());
//true
}
2. 懒汉式
线程不安全,该实现方式在运行时加载对象,这样带来了线程不安全问题。
public class SingleTon2 {
private static SingleTon2 instance;
private SingleTon2(){
}
public static SingleTon2 getInstance(){
if(null == instance){
//休眠一毫秒来模拟线程挂起
try{
Thread.sleep(1);
}catch (Exception e){
e.printStackTrace();
}
instance = new SingleTon2();
}
return instance;
}
}
为了更直观观察开启100个线程,测试线程安全问题,通过休眠一毫秒来模拟线程挂起,为初始化完instance。
public void test2(){
for(int i=0;i<100;i++){
new Thread(()->
System.out.println(SingleTon2.getInstance().hashCode())
).start();
}
}
日志输出>>
1060934507
992953204
711038951
1943663771
992953204
40773580
1075978764
1199869364
1942038659
992953204
1775468882
681753486
.......
造成线程不安全的原因
当并发访问的时候,第一个调用getInstance方法的线程t1,在判断完instance是null的时候,线程A就进入了if块准备创造实例,但是同时另外一个线程B在线程A还未创造出实例之前,就又进行了instance是否为null的判断,这时instance依然为null,所以线程B也会进入if块去创造实例,这时问题就出来了,有两个线程都进入了if块去创造实例,结果就造成单例模式并非单例。
3. 懒汉式(synchronized修饰方法)
线程安全,锁的粒度比较大,对getInstance()方法加了 synchronized 来保证多线程下的线程安全,但是锁住了整个方法,导致其它线程堵塞等待,效率比较低。
public class SingleTon3 {
private static SingleTon3 instance;
private SingleTon3(){
}
public static synchronized SingleTon3 getInstance(){
if(null == instance){
instance = new SingleTon3();
}
return instance;
}
}
4. 懒汉式(synchronized修饰代码块)
线程不安全,锁的粒度虽然小,对代码块加了 synchronized 来保证多线程下的线程安全,但是依旧还是会出现和第2种未加锁的问题一样,线程不安全。
public class SingleTon4 {
private static SingleTon4 instance;
private SingleTon4(){
}
public static SingleTon4 getInstance(){
if(null == instance){
synchronized(SingleTon4.class){
instance = new SingleTon4();
}
}
return instance;
}
}
5. 懒汉式(双重检测锁式)
线程安全,所谓“双重检测锁式”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
public class SingleTon5 {
private static volatile SingleTon5 instance;
private SingleTon5(){
}
public static SingleTon5 getInstance(){
if(null == instance){
synchronized(SingleTon5.class){
if(null == instance){
instance = new SingleTon5();
}
}
}
return instance;
}
}
(1)为么两层判空?
第一层:为了减少锁了粒度,因为只有在instance == null的时候才需要上锁,其他情况可以直接返回,这样就节省了很多无谓的线程等待时间。
第二层:获取锁后还需要判断 instance == null是因为instance可能已经被改变,所以要再次判断。
例如:两个线程都在等待获取锁,线程A获取到后实例化了instance后释放了锁,instance现在不是null;之后线程B获取到锁,如果不判断instance == null的话便会又重新创建一个instance。
(2)为么使用volatile修饰?
因为虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创建一个新的对象并非是原子性操作。
创建一个新的对象,需要3个步骤:
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间
JVM存在指令重排序优化,有可能上述步骤变为1-3-2。假设现在的执行顺序是1-3-2,现在有两个线程A和B。A获取锁后,执行new对象,执行步骤是1-3,还未执行2。需要注意的是,执行完3后jinstance就未非null了,而第2步还没有执行,对象不是完整的对象,此时如果判断instance == null 将返回false。此时线程B执行判断同步代码块外的 instance == null 判断,得到结果false,直接返回了这个不完整的对象。因此这里volatile是为了避免指令重排序,而不是可见性。
6. 静态内部类式
线程安全,类加载的机制来保证初始化实例时只有一个线程,类的静态属性只会在第一次加载类的时候初始化,所以在这里jvm帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
public class SingleTon6 {
private static class StaticSingleTon{
private static final SingleTon6 instance = new SingleTon6();
}
public static SingleTon6 getInstance(){
return StaticSingleTon.instance;
}
}
7. 枚举单例
线程安全,使用枚举的方法来实现单例时很简单的一种方法,同时也可以防止反序列化攻击。枚举类型以及其定义的枚举变量在JVM中都是唯一的,外部无法通过构造器创建枚举类的实例,因此反序列化后的实例也会和之前被序列化的对象实例相同,所以枚举本身就是单例的。
public enum SingleTon7 {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
四、常见应用场景
- 网站计数器
- 项目中用于读取配置文件的类
- 数据库连接池
- Spring中,每个Bean默认都是单例的,这样便于Spring容器进行管理。
- Windows中任务管理器,回收站。