设计模式:单例模式
定义:在程序运行时,确保某一个类在内存中只有一个实例。
懒汉式
线程安全的懒汉式单例,但是此种效率低下,静态方法上添加synchronized锁会锁住整个类,不管有没有实例化lazySingleton都加锁。
public class LazySingleton { private static LazySingleton lazySingleton = null; private LazySingleton(){ } public synchronized static LazySingleton getInstance(){ if(lazySingleton == null){ lazySingleton = new LazySingleton(); } return lazySingleton; } }
Double check形式,会提高性能,因为锁不会加在静态方法上,而是在方法中
public class LazyDoubleCheckSingleton { private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; private LazyDoubleCheckSingleton(){ } public static LazyDoubleCheckSingleton getInstance(){ if(lazyDoubleCheckSingleton == null){ synchronized (LazyDoubleCheckSingleton.class){ if(lazyDoubleCheckSingleton == null){ lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); //1.分配内存给这个对象 // //3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址 //2.初始化对象 // intra-thread semantics // ---------------//3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址 } } } return lazyDoubleCheckSingleton; } }
该方式先判断一下lazyDoubleCheckSingleton有没有被实例化,有就直接返回了,没有实例化才会进入锁,然后再判断一次,,没有被实例化再new
但引申出一个问题:
对象创建lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();一行代码,却分为3个步骤
//1.分配内存给这个对象
//3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
//2.初始化对象
此处,2、3步骤的顺序是可以互换的,而第三部恰巧是第一个判断if(lazyDoubleCheckSingleton == null)的依据,
即分配了内存地址,java虚拟机就会认为对象已经被创建
但是步骤2还没有进行,此时其它线程进来并通过了(lazyDoubleCheckSingleton == null)判断,就将未完成的对象return,导致JVM报错
解决方案(一):
在被创建的对象上添加修饰符volatile,意思是该对象创建时,顺序不可更改。
解决方案(二):
利用静态内部类
思路:
对象的创建由私有的静态内部类来完成,这样确保了实例的创建是由另一个类来完成。
线程0进入该类,请求创建实例,须等待该类的内部类来创建后返回给线程0,如果此时
线程1到来,也请求创建实例,线程1并非对象的构造线程(该实例的构造线程是内部类的线程),并不能访问到实例创建的步骤
这样实例创建的过程好比黑盒,只有内部类可以知晓,而内部类是私有的,只能由这个上级类来调用,就不存在上述误判的情况了。
饿汉式
都是静态的,在类一加载就完成了对象的创建,而且没有线程安全问题,就是会可能造成资源浪费
public class HangrySingleton { private final static HangrySingleton hangrySingleton ; static{ hangrySingleton = new HangrySingleton(); } private HangrySingleton(){ } public static HangrySingleton getInstance(){ return hangrySingleton; } }
单例模式的反射攻击
class文件如果被其他程序获取到,其他程序可以用反射机制将私有的构造方法的权限修改之后创建对象,从而破坏单例模式。
防止反射攻击
在私有的构造器中添加一段代码
private HungrySingleton(){ if(hungrySingleton != null){ throw new RuntimeException("单例构造器禁止反射调用"); } }
但是,此种方式只适用于饿汉式,因为饿汉式的对象声明是静态的,
private final static HungrySingleton hungrySingleton;
而且对象的创建也是在静态代码块中完成的
static{
hungrySingleton = new HungrySingleton();
}
即,饿汉式的单例创建于类加载时,而反射机制发生于类运行时,类加载,对象就已创建,反射无法再次创建对象。
而懒汉式,对象创建于运行时,这就给了反射以机会,两者都在运行时创建对象就会带来线程不安全问题,没有同一把锁安在他们身上。
反射可以在"getInstance"方法没有创建完对象时,将对象创建完毕(没有锁),而 "getInstance"方法创建完对象又返回了一个对象
将对象序列化也会造成对单例模式的破坏
破坏过程:将对象序列化后,再将其反序列化回来就不再是同一个对象了(内存地址不一样)。
因为序列化就是利用反射机制,在反序列化时又新建了一个对象
解决办法:
在一个需要被序列化的类中添加一个方法
private Object readResolve(){ return hungrySingleton; }
其实就是告诉序列化的实现类,反序列化时,别创建新对象了,还是返回我这个对象hungrySingleton吧。
枚举<->单例
枚举单例的实现,就这么简单
public enum EnumInstance{ INSTANCE; public static EnumInstance getInstance(){ return INSTANCE; } }
在枚举类中对单例的实现类似于饿汉式,如果想在单例对象中添加方法,如下:
public enum EnumInstance { INSTANCE{ protected void printTest(){ System.out.println("Edward Print Test"); } }; protected abstract void printTest(); public static EnumInstance getInstance(){ return INSTANCE; } public static void main(String[] args) { EnumInstance instance = EnumInstance.getInstance(); instance.printTest(); //运行结果:Edward Print Test } }
其中如果想外界能调用到该方法,需要添加高亮代码
反编译EnumInstance.java生成的EnumInstance.class文件,会看到如下代码
import java.io.PrintStream; public abstract class EnumInstance extends Enum { public static EnumInstance[] values() { return (EnumInstance[])$VALUES.clone(); } public static EnumInstance valueOf(String s) { return (EnumInstance)Enum.valueOf(EnumInstance, s); } private EnumInstance(String s, int i) { super(s, i); } protected abstract void printTest(); public static EnumInstance getInstance() { return INSTANCE; } public static final EnumInstance INSTANCE; private static final EnumInstance $VALUES[]; static { INSTANCE = new EnumInstance("INSTANCE", 0) { protected void printTest() { System.out.println("Geely Print Test"); } } ; $VALUES = (new EnumInstance[] { INSTANCE }); } }
容器单例模式
比如维护一个HashMap,将所要用到的单例对象都放入容器内,然后通过key获取对象,适用于系统中单例对象比较多的情况下
public class ContainerSingleton { private ContainerSingleton(){ } private static Map<String,Object> singletonMap = new HashMap<String,Object>(); public static void putInstance(String key,Object instance){ if(StringUtils.isNotBlank(key) && instance != null){ if(!singletonMap.containsKey(key)){ singletonMap.put(key,instance); } } } public static Object getInstance(String key){ return singletonMap.get(key); } }
HashMap线程不安全,可能会出现多个对象抛异常的现象。HashTable线程安全,但效率低下。
折中一下,使用concurrent,线程虽然不是绝对安全,但是在不考虑序列化和反射的情况下,是可以使用concurrent做容器单例模式的。
ThreadLocal线程单例
ThreadLocal对象是基于线程的单例,并不保证线程之外的单例,即在线程内部只有一个对象,避免了多线程访问时线程之间的干扰
Spring内的单例其作用范围是Spring应用程序内,并不是一个真正意义上的单例,如果启动多个Spring容器即使每个容器都是单例的,依然可以将对象都拿出来。
而单例设计模一般的作用域是整个Java的类加载器的管理空间