单例模式
定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
为什么要用单例模式
在系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
单例模式带来的好处
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
为什么不使用全局变量确保一个类只有一个实例
静态变量也可以保证该类的实例只存在一个。只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。 但是,如果说这个对象非常消耗资源,而且程序某次的执行中一直没用,这样就造成了资源的浪费。利用单例模式的话,我们就可以实现在需要使用时才创建对象,这样就避免了不必要的资源浪费。 不仅仅是因为这个原因,在程序中我们要尽量避免全局变量的使用,大量使用全局变量给程序的调试、维护等带来困难。
单例模式写法
饿汉(可用)
public class Singleton {
private static Singleton instance=new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
优缺点
优:在类加载的时候就完成了实例化,避免了多线程的同步问题,另外在每次调用时,无须对对象进行null判断
缺:因为类加载时就实例化了,没有达到
Lazy Loading(懒加载) 的效果,如果该实例没被使用,便浪费了内存空间。
懒汉
懒汉第一版(不可用)
public class Singleton {
private Singleton() {} //私有构造函数
private static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
问题:
会出现多线程安全问题
懒汉第二版(不可用)
public class Singleton {
private Singleton() {} //私有构造函数
private static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized(Singleton.class){ //同步锁
if(null == instance){ //双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
说明
- 1.为了防止new Singleton被执行多次,因此在new操作之前加上Synchronized 同步锁,锁住整个类(注意,这里不能使用对象锁)。
- 2.进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。
问题
会因重排序导致多线程问题,不是绝对的线程安全
在java中,对于instance = new Singleton,会被编译器编译成如下jvm指令
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。
懒汉第三版/双重检查DCL(可用)
public class Singleton {
private Singleton() {} //私有构造函数
private volatile static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized(Singleton.class){ //同步锁
if(null == instance){ //双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
说明:
1.添加volatitle,用于禁止指定重排序
静态内部类
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
说明:
1.从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。
2.INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
枚举
enum SingletonDemo{
INSTANCE;
public void otherMethods(){
System.out.println("Something");
}
}
使用
public class Hello {
public static void main(String[] args){
SingletonDemo.INSTANCE.otherMethods();
}
}
说明
《Effective Java 》以及《Java与模式》的作者推荐的方式。
优缺点:
优:可以阻止反射获取枚举类的私有构造方法,同时可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象
缺:A非懒加载模式,B当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。
总结
上述方法实现存在的问题
除了枚举实现外,其它实现方法均存在以下问题
- 无法防止利用反射来重复构建对建
//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));
- 反序列化的问题
如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。
解决方式:添加readResolve()方法:
public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {
}
private Object readResolve() {
return INSTANCE;
}
}
为什么反序列化会出现这样的问题:
www.hollischuang.com/archives/11…
对比(还没写)
| 方法 | ||
|---|---|---|
| 饿汉 | ||