单例模式

157 阅读3分钟

记一次学习单例模式,仅作为笔记存储。

一、使用场景:

  • 需要频繁实例化然后销毁的对象;
  • 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
  • 有状态的工具类对象;
  • 频繁访问数据库或文件的对象;

二、实现方式:

1.懒汉式经典实现

public class Singleton {

	private static Singleton new_instance = null;

	private Singleton(){

	}

	public static Singleton getInstance(){		if (new_instance == null){			new_instance = new Singleton ();		}
		return new_instance;	}
}

单例模式的经典实现方式存在不少弊端,比如多线程情况下,两个线程同时调用 getInstance 方法,这样将创建两个实例。

2.同步锁

public class Singleton {
    private static Singleton new_instance = null;    
    private Singleton() {
        
    }
   
    public static synchronized Singleton getInstance() { 
        if(new_instance == null) {            new_instance = new Singleton();        }
        return new_instance ;    }
}

此方法是直接在方法上加同步锁,多线程并发时,线程B只有在线程A执行完后才会执行getInstance()方法,保证线程安全。弊端是如果频繁调用getInstance()方法的话,效率会很糟糕,毕竟大多数情况下是不需要同步操作。

3.饿汉式

public class Singleton {
    private static Singleton new_instance = new Singleton();      
    private Singleton() {
        
    }
    
    public static Singleton getInstance() {        
        return new_instance ;    }
}

饿汉式避免了多线程同步问题,因为对象在类装载的时候就被实例化了,所以即使出现多线程并发,类里面已经存在new_instance对象了,就不存在线程安全问题了。缺点是此种方式在类装载的时候就初始化实例了,如果程序自始至终不需要用到该实例的话,就会造成内存资源浪费。

4.双重检查加锁

public class Singleton {

    private volatile static Singleton new_instance = null; //注意volatile关键词    
    private Singleton() {
        
    }
    
    public static Singleton getInstance() {
        if(new_instance == null) {     //1.第一次检查            synchronized (Singleton.class) {        
                if(new_instance == null) { //2.第二次检查                    new_instance = new Singleton(); //3.创建实例                }
            }
        }
        return new_instance ;    }
}

如果上边的new_instance对象没有加volatile关键字修饰的话,会导致一个问题:

在线程执行到第一次检查的地方的时候,代码读取到new_instance不为null时,new_instance引用的对象有可能还没有完成初始化。主要的原因是重排序。重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

在第3创建对象的时候,可以分解成三个动作。

memory = allocate();  // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
new_instance = memory;  // 3:设置new_instance指向刚分配的内存地址

根源在于2和3之间,有可能会被重排序,例如:

memory = allocate();  // 1:分配对象的内存空间
instance = memory;  // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化
ctorInstance(memory); // 2:初始化对象

这在多线程环境下会出现问题,B线程会看到一个还没有被初始化的对象,判断不为null,直接返回一个未被完全初始化的对象。

如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的,同时还会禁止指令重排序。

5.静态内部类

public class Singleton {  
    private static class SingletonHolder {          //SingletonHolder类
        private static final Singleton new_instance = new Singleton();  
    }  

    private Singleton (){

    }  

    public static final Singleton getInstance() {  //调用getInstance方法将装载SingletonHolder类
        return SingletonHolder.new_instance ;     }  
}

前面提到的饿汉式在类装载时,对象就被实例化,没有达到懒加载的效果。
此种模式下,即使 Singleton 类被装载了, new_instance 对象也不一定被实例化。因为 SingletonHolder 类并没有被主动使用,只有调用 getInstance 方法才会装载 SingletonHolder 类,从而实例化 new_instance 对象。所以,此模式下不会在类装载时就直接构造实例,比饿汉式要合理一些。

三、总结

  • 懒汉式代码书写相对复杂,而且需要用同步锁,比较耗费资源。
  • 普通饿汉式代码书写简单,但浪费内存空间。
  • 静态内部类是对普通饿汉式的改进,较好地克服了内存空间浪费问题。