最简单又最复杂的单例模式
文章目录
- 定义
- 类型
- 适用场景
- 优点
- 缺点
- 重点
- 私有构造器
- 线程安全
- 延迟加载
- 序列化和反序列化安全
- 反射
- codnig
- 懒汉式
- 非线程安全的
- 线程安全的
- 第一种:加synchronized关键字
- Double-check双重检查方式(有隐患)
- Double-check双重检查方式(volatile禁止重排序)
- 静态内部类延迟加载(基于类初始化)
- 饿汉式
- 反序列化破坏单例模式及解决方案
- 反射攻击和解决方案
- 枚举单例
- 序列化影响测试
- 反射攻击测试
- 反编译工具Jad
- 枚举类中使用方法
- 总结
- 容器单例模式
- ConcurrentHashMap不是绝对安全
- ThreadLoacl线程单例
- 单例模式源码分析
- Runtime饿汉式
- java.awt.Desktop 容器单例
- spring中AbstractFactoryBean 单例的影子
- Mybatis中ErrorContext 基于ThreadLocal的单例模式
- 最后
定义
保证一个类仅有一个实例,并提供一个全局访问点
类型
创建型
适用场景
想确保任何情况下都绝对只有一个实例
优点
- 在内存里只有一个实例,减少了内存开销
- 可以避免对资源的多重占用
- 设置了全局访问点,严格控制访问
缺点
没有接口,扩展困难
重点
私有构造器
这个是为了禁止从单例外部调用构造函数来创建这个对象,为了达到这个目的,必须设置构造函数的权限为private
线程安全
线程安全在单例模式中是非常重要的
延迟加载
我们想使用的时候再创建
序列化和反序列化安全
一旦对单例进行序列化和反序列化的话,就会对单例进行破坏
反射
单例模式也要防止反射攻击,虽然在平常写代码的时候并不会刻意这么做,但是基于工程师的思想我们也要考虑下这个点
codnig
懒汉式
非线程安全的
/*** 懒汉式:*/
public class LazySingleton {/** 可以说比较懒,在初始化的时候是没有创建的*/private static LazySingleton lazySingleton=null;/*** 构造器是private的,为了在外部不让调用*/private LazySingleton(){}/*** 线程不安全的,* @return*/public static LazySingleton getInstance(){if(lazySingleton==null){lazySingleton=new LazySingleton();}return lazySingleton;}}
懒汉式重在懒,在加载的时候没有初始化。但是对于线程安全是没有考虑的,比如两个线程都到断点处
线程1到24行后没有执行呢,线程2在23行执行时候判断到是true,也进入24行,这样LazySingleton就被初始化了两次。我们可以通过多线程debug的方式让这种现象出现,具体操作是在Idea中右键断点,弹出对话框
默认是选中All的,也就是主线程中,我们可以选择thread,即为线程模式,并且右边还有个MakeDefault的按钮,这个可以设置下次设置断点时候默认选择的模式
线程安全的
有两种改进懒汉式非线程安全的方式
第一种:加synchronized关键字
public synchronized static LazySingleton getInstance(){if(lazySingleton==null){lazySingleton=new LazySingleton();}return lazySingleton;}
如果在静态方法上加synchronized 关键字,那么锁的是这个类的class文件,不是静态方法,相当于锁的是在堆内存中生成的对象。还有一种写法,和上面的是一样的
public static LazySingleton getInstance(){synchronized(LazySingleton.class) {if (lazySingleton == null) {lazySingleton = new LazySingleton();}}return lazySingleton;}
我们知道同步锁是比较消耗资源,而且synchronized修饰static方法的时候,锁的是这个class,锁的范围是非常大的,对性能会有一定影响。
Double-check双重检查方式(有隐患)
这种方式兼顾了性能和线程安全,而且也是懒加载的
/*** 懒加载双重检查*/
public class LazyDoubleCheckSingleton {private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;private LazyDoubleCheckSingleton() {}public static LazyDoubleCheckSingleton getInstance() {if (lazyDoubleCheckSingleton == null) {synchronized (LazyDoubleCheckSingleton.class) {if (lazyDoubleCheckSingleton == null) {lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();}}}return lazyDoubleCheckSingleton;}
}
我们在if里面锁住类,那么就代表if还是可以进来的,需要在进来后的创建对象代码上加锁,这样比起第一种情况,锁的范围就缩小了,性能也影响变小了,同时也保证了线程安全。
但是上面的代码还有隐患,出在if (lazyDoubleCheckSingleton == null) {
和lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
,为什么呢?
首先在if (lazyDoubleCheckSingleton == null) {
行时候,虽然判断了是否为空,但是如果不为空,但是对象又没有完成初始化,也就是lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
行没有执行结束,这是什么意思呢?
再来看下new的这行代码,看起来是一行,实际上经历了3个步骤
- 分配内存给这个对象
- 初始化这个对象
- 设置lazyDoubleCheckSingleton指向刚分配的内存地址
也就是说new这行代码实际上执行了3行操作,在2、3步骤时候有可能出现重排序,顺序颠倒变成
- 分配内存给这个对象
- 设置lazyDoubleCheckSingleton指向刚分配的内存地址
- 初始化这个对象
这就要说下java规范里的,有说必须要遵守intra-thread semantics(线程内语义)这样的一个规定,它保证重排序不会改变单线程内的程序执行结果,比如1、2、3互换位置,不会改变单线程的执行结果,也就是说上面的2、3互换是允许的。
上图可以看出,2、3怎么换顺序,4的访问结果都是一样的,所以2、3的重排序对结果并没有影响,当然这个重排序并不是100%命中的,是有一定概率的,但是这种隐患我们也要消除,左边是单线程没有什么问题。我们看右边线程1,假设线程0重排序了,走了3,这时候线程1从上至下开始判断instance是否为null,这个时候判断出来了,instance并不为null,因为它有指向内存空间,然后线程1开始访问对象,也就是说线程1比线程0更早的访问对象,所以线程1访问到的对象是在线程0中还没有初始化完成的对象,这个对象并没有被完整的初始化上,系统就要报异常了。
那我们怎么办呢?可以有两种方法,可以阻止重排序,或者允许线程0重排序但线程1不能看到这个重排序
Double-check双重检查方式(volatile禁止重排序)
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();}}}return lazyDoubleCheckSingleton;}
}
这样重排序就会被禁止,在多线程的时候cpu也有共享内存,我们在加了volatile关键字之后,所有的线程就都能看到共享内存的最新状态,保证了内存的可见性,这里就和共享内存有关了,在加了volatie关键字后,在共享变量进行写操作的时候,会多出一些汇编代码,起到两个作用,第一是将当前当前处理器缓存的数据写到系统内存,这个写会内存的操作会使在其它cpu里缓存了该内存地址的数据无效,因为其它cpu缓存的数据无效了,所以它们又从共享内存同步数据,这样就保证了内存的可见性。这里面是使用的缓存一致性协议,当处理器发现我这个缓存已经无效了,所以我在进行操作的时候会重新从系统内存中把数据读到处理器的缓存里。
静态内部类延迟加载(基于类初始化)
这种方式是让线程1看不到线程0的重排序,是通过静态内部类来解决
/*** 静态内部类延迟加载单例*/
public class StaticInnerClassSingleton {private static class InnerClass{private static StaticInnerClassSingleton staticInnerClassSingleton=new StaticInnerClassSingleton();}public static StaticInnerClassSingleton getInstance(){return InnerClass.staticInnerClassSingleton;}private StaticInnerClassSingleton(){}
}
我们看下原理
jvm在类的初始化阶段,也就是class被加载后,并且被线程使用之前,都是类的初始化阶段。在这个阶段,会执行类的初始化,那在执行类的初始化期间,jvm会去获取一个锁,这个锁可以同步多个线程对一个类的初始化,也就是上图绿色的部分。基于这个特性我们可以实现基于静态内部类的,并且是线程安全的,延迟初始化方案,看上图,右边框中的2、3指令重排序,对于线程1并不会看到,也就是说非构造线程,是不允许看到这个重排序的,因为之前我们讲的是线程0来构造这个单例对象,初始化一个类,包括执行这个类的静态初始化,还有初始化在这个类中声明的静态变量,根据java语音规范主要分为5种情况:
- 首次发生的时候,一个类将被立刻初始化,这个类所说的类泛指包括接口也是一个类。
- 包括类中声明的一个静态方法被调用
- 类中声明的静态成员被赋值
- 类中声明的一个静态成员被使用,并且这个静态成员不是一个常量成员
- 类是顶级类,并且这个类中有断言语句
只要发生上面说的5种情况,这个类都会被初始化。再看上图,线程0、线程1同时获得绿色框的锁的时候,假设线程0获取了锁,线程0执行静态内部类的初始化,对于静态内部类的2、3存在重排序,但是线程1是无法看到这个重排序的,因为这里面有一个class对象的初始化锁,因为有锁所以对于线程0而言,初始化这个静态内部类的时候,把这个instance new出来,我们线程1看不到,因为线程1在绿色区等待,所以静态内部类是基于类初始化的延迟加载解决方案。
饿汉式
/*** 饿汉式*/
public class HungrySingleton {private final static HungrySingleton hungrySingleton=new HungrySingleton();private HungrySingleton(){}public static HungrySingleton getInstance(){return hungrySingleton;}}
优点是在类加载的时候就完成了初始化,避免了线程的同步问题。
缺点也是由于类在加载的时候就完成了初始化,没有延迟加载的效果,如果这个类从始至终我们系统都没用过,还会造成内存的浪费,我们也可以将对象的初始化放在静态代码块里。
/*** 饿汉式*/
public class HungrySingleton {private final static HungrySingleton hungrySingleton ;static{hungrySingleton =new HungrySingleton();}private HungrySingleton(){}public static HungrySingleton getInstance(){return hungrySingleton;}}
由于final修饰的变量必须在类加载时完成初始化,因此在懒汉式中静态私有单例类型变量不能加final,恶汉式中就可加可不加了。
反序列化破坏单例模式及解决方案
我们以懒汉式为例,进行演示
public class Test {public static void main(String[] args) throws IOException, ClassNotFoundException {HungrySingleton instance=HungrySingleton.getInstance();ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("singleton_file"));oos.writeObject(instance);//再读取出来File file=new File("singleton");ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));//和写入时的对象进行对比,看是否是同一个对象HungrySingleton newInstace= (HungrySingleton) ois.readObject();System.out.println(instance);System.out.println(newInstace);System.out.println(instance == newInstace);}
}
运行结果:
Exception in thread "main" java.io.NotSerializableException: com.design.pattern.creational.singleton.HungrySingletonat java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)at com.design.pattern.creational.singleton.Test.main(Test.java:25)
这是因为HungrySingleton
中没有实现Serializable
接口,实现后再运行
com.design.pattern.creational.singleton.HungrySingleton@7530d0a
com.design.pattern.creational.singleton.HungrySingleton@3d494fbf
false
可以发现结果是不相等的,这就违背了单例模式的初衷,通过序列化和反序列化得到了不同的对象,我们希望得到同一个对象,这个事情怎么解决呢?
在HungrySingleton
中写个方法
public class HungrySingleton implements Serializable {private final static HungrySingleton hungrySingleton=new HungrySingleton();private HungrySingleton(){System.out.println("HungrySingleton");}public static HungrySingleton getInstance(){return hungrySingleton;}/*** 增加该方法,返回这个单例对象* @return*/private Object readResolve(){return hungrySingleton;}}
再运行下,看看结果
com.design.pattern.creational.singleton.HungrySingleton@7530d0a
com.design.pattern.creational.singleton.HungrySingleton@7530d0a
true
为什么加了readResolve
方法就可以了呢?从HungrySingleton
的父类里找,根本就没有找到该方法
说明它并不是Object这个对象的方法,方法名字为什么又叫readResolve
呢?我们较个真,继续看下ois.readObject()
,在它里面有一行if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod())
,判断了如果反序列化的对象有readResolve
方法,就反射调用该方法得到该方法的返回值,并替换掉反序列化时实例化的对象
反射攻击和解决方案
我们写下反射攻击的代码
public class Test {public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {Class objectClass=HungrySingleton.class;Constructor constructor=objectClass.getDeclaredConstructor();HungrySingleton instance=HungrySingleton.getInstance();HungrySingleton newInstance= (HungrySingleton) constructor.newInstance();System.out.println(instance);System.out.println(newInstance);System.out.println(instance == newInstance);}
}
运行结果
Exception in thread "main" java.lang.IllegalAccessException: Class com.design.pattern.creational.singleton.Test can not access a member of class com.design.pattern.creational.singleton.HungrySingleton with modifiers "private"at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
看到报错,是说HungrySingleton 的构造方法是私有的,那我们改成可访问的,加一行代码constructor.setAccessible(true);
,再运行后
com.design.pattern.creational.singleton.HungrySingleton@7440e464
com.design.pattern.creational.singleton.HungrySingleton@49476842
falseProcess finished with exit code 0
可以看到反射和单例模式创建的对象是不一样的,这就违背了单例模式的初衷,我们怎么办呢?
看下饿汉式有个特点,它是在类加载的时候就初始化了,那我们可以在构造器中进行判断
private HungrySingleton(){if(hungrySingleton!=null){throw new RuntimeException("单例构造器禁止反射调用");}}
再运行就会报异常,这样对反射攻击进行了保护,但是这种方式有个特点,对类加载时候就实例化是OK的,但是对于懒加载的单例就不是都ok了,例如先反射方式创建,再单例方式创建,就会创建两个不同的对象,并且增加一个是否懒加载过的标识,反射都可以修改该标识,所以这种方式对于懒加载的单例是不能阻挡反射攻击的
/*** 懒汉式:*/
public class LazySingleton2 {/** 可以说比较懒,在初始化的时候是没有创建的*/private static LazySingleton2 lazySingleton=null;private static boolean flag = true;private LazySingleton2(){if(flag){flag= false;}else {throw new RuntimeException("单例构造器禁止反射调用");}}public static LazySingleton2 getInstance(){synchronized(LazySingleton2.class) {if (lazySingleton == null) {lazySingleton = new LazySingleton2();}}return lazySingleton;}public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {Class objectClass=LazySingleton2.class;Constructor c=objectClass.getDeclaredConstructor();c.setAccessible(true);LazySingleton2 o1=LazySingleton2.getInstance();LazySingleton2 o2= (LazySingleton2) c.newInstance();System.out.println(o1);System.out.println(o2);System.out.println(o1==o2);}}
运行结果
Exception in thread "main" java.lang.reflect.InvocationTargetExceptionat sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)at java.lang.reflect.Constructor.newInstance(Constructor.java:423)at com.design.pattern.creational.singleton.LazySingleton2.main(LazySingleton2.java:44)
Caused by: java.lang.RuntimeException: 单例构造器禁止反射调用at com.design.pattern.creational.singleton.LazySingleton2.<init>(LazySingleton2.java:19)... 5 more
看到我们加的flag是生效了,没有问题,是好使的,阻止了反射攻击。但是我们可以破坏它,怎么做呢?
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {Class objectClass=LazySingleton2.class;Constructor c=objectClass.getDeclaredConstructor();c.setAccessible(true);LazySingleton2 o1=LazySingleton2.getInstance();//破坏阻止反射攻击Field flag=o1.getClass().getDeclaredField("flag");flag.setAccessible(true);flag.set(o1,true);LazySingleton2 o2= (LazySingleton2) c.newInstance();System.out.println(o1);System.out.println(o2);System.out.println(o1==o2);}
运行结果
com.design.pattern.creational.singleton.LazySingleton2@49476842
com.design.pattern.creational.singleton.LazySingleton2@78308db1
false
可以看到反射攻击成功,也就是说我们通过反射把flag改为true了,无论里面加多么复杂的逻辑,都是没有用的,可以通过反射进行任意修改。
到这里,你可能认为单例模式是最简单的,看上去呢确实是最简单的,但是说复杂也是最复杂的
枚举单例
可保证防止反射攻击,有可以保证不被序列化破坏。这也是Effective Java书里推荐的单例模式。
/*** 枚举类单例模式*/
public enum EnumInstance {INSTANCE;/**可以多个,这里演示只写一个data*/private Object data;public Object getData() {return data;}public void setData(Object data) {this.data = data;}public static EnumInstance getInstance(){return INSTANCE;}
}
我们主要关注的是枚举单例的序列化机制,和反射攻击。
枚举类天然的可序列化机制,可以保证不会出现多次实例化的情况,即使在复杂的序列化和反射攻击情况下枚举类型的单例模式都没有问题,只不过现在的写法可能看起来不太自然,但是枚举类实现单例可能是实现单例中的最佳实践,Effective java中也是强烈推荐。
序列化影响测试
我们测试下序列化,先测试枚举持有的INSTANCE
/*** 应用类*/
public class Test {public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {EnumInstance instance=EnumInstance.getInstance();//再读取出来File file=new File("singleton_file");ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));//和写入时的对象进行对比,看是否是同一个对象EnumInstance newInstace= (EnumInstance) ois.readObject();System.out.println(instance);System.out.println(newInstace);System.out.println(instance == newInstace);}
}
运行结果
INSTANCE
INSTANCE
true
结果已经出来了,输出的时候是两个INSTANCE,并且是相等的。我们主要测试的是枚举类持有的data对象做实验,我们继续使用data做实验
/*** 应用类*/
public class Test {public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {EnumInstance instance=EnumInstance.getInstance();//测试枚举类持有的data初始化好,再序列化,之后看看data是不是同一个instance.setData(new Object()); //再读取出来File file=new File("singleton_file");ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));//和写入时的对象进行对比,看是否是同一个对象EnumInstance newInstace= (EnumInstance) ois.readObject();System.out.println(instance.getData());System.out.println(newInstace.getData());System.out.println(instance.getData() == newInstace.getData());}
}
运行结果
java.lang.Object@b1bc7ed
java.lang.Object@b1bc7ed
true
枚举类就是这么强大,那么序列化和反序列化枚举类是怎么处理的呢?我们打开ObjectInputStream
里面有一个方法叫readEnum(boolean unshard)
private Enum<?> readEnum(boolean unshared) throws IOException {//各种校验开始if (bin.readByte() != TC_ENUM) {throw new InternalError();}ObjectStreamClass desc = readClassDesc(false);if (!desc.isEnum()) {throw new InvalidClassException("non-enum class: " + desc);}int enumHandle = handles.assign(unshared ? unsharedMarker : null);ClassNotFoundException resolveEx = desc.getResolveException();if (resolveEx != null) {handles.markException(enumHandle, resolveEx);}//重点开始//获取到枚举对象的名name,String name = readString(false);Enum<?> result = null;Class<?> cl = desc.forClass();if (cl != null) {try {@SuppressWarnings("unchecked")//通过类型和名称获取枚举常量,因为name是惟一的,并且对应唯一一个枚举常量//取到的肯定是唯一的常量对象,没有创建新的对象,维持了对象的单例属性Enum<?> en = Enum.valueOf((Class)cl, name);result = en;} catch (IllegalArgumentException ex) {throw (IOException) new InvalidObjectException("enum constant " + name + " does not exist in " +cl).initCause(ex);}if (!unshared) {handles.setObject(enumHandle, result);}}handles.finish(enumHandle);passHandle = enumHandle;return result;}
可以看到,枚举对于序列化的破坏是不受影响的
反射攻击测试
/*** 应用类*/
public class Test {public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {//枚举单例 反射攻击测试Class objectClass=EnumInstance.class;Constructor constructor=objectClass.getDeclaredConstructor();constructor.setAccessible(true);EnumInstance instance=EnumInstance.getInstance();EnumInstance newInstance= (EnumInstance) constructor.newInstance();System.out.println(instance);System.out.println(newInstance);System.out.println(instance == newInstance);}
}
运行结果
Exception in thread "main" java.lang.NoSuchMethodException: com.design.pattern.creational.singleton.EnumInstance.<init>()at java.lang.Class.getConstructor0(Class.java:3082)at java.lang.Class.getDeclaredConstructor(Class.java:2178)at com.design.pattern.creational.singleton.Test.main(Test.java:69)
看到运行报异常了,报的是NoSuchMethodException,这个异常出现在这一行Constructor constructor=objectClass.getDeclaredConstructor();
获取构造器时没有获取无参构造器,这个是为什么呢?我们来看下枚举类java.lang.Enum的源码,只有117行有个有参构造方法
protected Enum(String name, int ordinal) {this.name = name;this.ordinal = ordinal;}
我们改下反射攻击的代码
/*** 应用类*/
public class Test {public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {//枚举单例 反射攻击测试Class objectClass=EnumInstance.class;Constructor constructor=objectClass.getDeclaredConstructor(String.class,int.class);constructor.setAccessible(true);EnumInstance instance=EnumInstance.getInstance();EnumInstance newInstance= (EnumInstance) constructor.newInstance("test",666);System.out.println(instance);System.out.println(newInstance);System.out.println(instance == newInstance);}
}
运行结果
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objectsat java.lang.reflect.Constructor.newInstance(Constructor.java:417)at com.design.pattern.creational.singleton.Test.main(Test.java:72)
看到反射获取构造器成功了,但是构造器在构造时候又报异常了,Cannot reflectively create enum objects
不能反射创建枚举类对象,在Constructor.java:417
if ((clazz.getModifiers() & Modifier.ENUM) != 0)throw new IllegalArgumentException("Cannot reflectively create enum objects");
看到通过反射攻击失败了
反编译工具Jad
jad下载是一个超级有用的反编译工具,我们在命令行中使用
>jad EnumInstance.class
Parsing EnumInstance.class... Generating EnumInstance.jad
我们打开反编译的jad文件
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumInstance.javapackage com.design.pattern.creational.singleton;//类是final的,我们在代码中是看不出来的
public final class EnumInstance extends Enum
{public static EnumInstance[] values(){return (EnumInstance[])$VALUES.clone();}public static EnumInstance valueOf(String name){return (EnumInstance)Enum.valueOf(com/design/pattern/creational/singleton/EnumInstance, name);}//私有构造器,非常符合单例模式的要求private EnumInstance(String s, int i){super(s, i);}public Object getData(){return data;}public void setData(Object data){this.data = data;}public static EnumInstance getInstance(){return INSTANCE;}//static final的,在什么时候实例化呢?public static final EnumInstance INSTANCE;private Object data;private static final EnumInstance $VALUES[];//静态块的方式实例化它static {INSTANCE = new EnumInstance("INSTANCE", 0);$VALUES = (new EnumInstance[] {INSTANCE});}
}
看到一个枚举类其实是非常符合单例模式的要求的,final代表这个类不能够被继承,私有构造器不允许外部实例化,类变量时静态的并且没有延迟初始化,通过静态代码块来初始化,同时也是线程安全的。除此之外,还有反序列化类、反射来为枚举类保驾护航,适当的抛出异常,所以枚举类实现单例模式还是比较优雅的。
枚举类中使用方法
/*** 枚举类中使用方法*/
public enum EnumInstance {INSTANCE{@Overrideprotected void printTest(){System.out.println("print test");}};/**声明一个同名的抽象方法,不然没法调用{}中声明的方法*/protected abstract void printTest();private Object data;public Object getData() {return data;}public void setData(Object data) {this.data = data;}public static EnumInstance getInstance(){return INSTANCE;}
}
测试代码
EnumInstance instance=EnumInstance.getInstance();instance.printTest();
运行结果
print test
反编译后枚举类的代码
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumInstance.javapackage com.design.pattern.creational.singleton;import java.io.PrintStream;public abstract class EnumInstance extends Enum
{public static EnumInstance[] values(){return (EnumInstance[])$VALUES.clone();}public static EnumInstance valueOf(String name){return (EnumInstance)Enum.valueOf(com/design/pattern/creational/singleton/EnumInstance, name);}private EnumInstance(String s, int i){super(s, i);}protected abstract void printTest();public Object getData(){return data;}public void setData(Object data){this.data = data;}public static EnumInstance getInstance(){return INSTANCE;}public static final EnumInstance INSTANCE;private Object data;private static final EnumInstance $VALUES[];static {INSTANCE = new EnumInstance("INSTANCE", 0) {protected void printTest(){System.out.println("print test");}}
;$VALUES = (new EnumInstance[] {INSTANCE});}
}
总结
单例模式实现的方案很多,每一种都有自己的优缺点,所以我们要根据不同的业务场景来选择不同的单例模式的实现方案。还有这一种模式是如何演进的,这几种单例方式都解决了什么问题,又存在什么问题,单例模式无论在校招还是社招,如果问设计模式的话99%都会问单例这个模式,单例模式看起来非常简单,对于初学者来说也是很简单的,但是就是隔着一层窗户纸,只要一捅破小伙伴会发现单例模式是这些设计模式中最复杂的一个模式。如果要深入研究的话,这里面的知识点还是非常有意思的。相信小伙伴们通过学习单例模式,对于提高自己的思维能力、编码能力、实操能力,这些肯定会有所提高的。还有看到这里了,希望小伙伴们在以后的面试中,千万不要在单例模式上丢分,一定要在这里加分
容器单例模式
public class ContainerSingleton {private static Map<String,Object> singletonMap=new HashMap<>();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会不会就变线程安全的,当然是线程安全的,但会影响性能,频繁去取的时候都有同步锁,会影响性能。我们可以折中使用ConcurrentHashMap,但是我们使用了静态的ConcurrentHashMap,而且直接操作了这个Map,在这种场景下,ConcurrentHashMap并不是绝对的线程安全。
所以我们不考虑反射序列化这种机制的话,这种容器单例模式也是有一定的使用场景的,在安卓的SDK源码中使用的也比较多,JDK中也有这种模式使用的影子。这个在使用过程中,不建议使用HashTable,这个HashMap也是做了一个平衡,如果一个业务中,单例对象特别多,我们就可以考虑使用一个容器把这些单例对象统一管理,优点就是统一管理节省资源相当于一个缓存,缺点线程不安全
ConcurrentHashMap不是绝对安全
转载
public class ThreadSafeTest {public static Map<Integer,Integer> map=new ConcurrentHashMap<>();
public static void main(String[] args) {ExecutorService pool1 = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {pool1.execute(new Runnable() {@Overridepublic void run() {Random random=new Random();int randomNum=random.nextInt(10);if(map.containsKey(randomNum)){map.put(randomNum,map.get(randomNum)+1);}else{map.put(randomNum,1);}}});}
}
}
这段代码是用10个线程测试10以内各个整型随机数出现的次数,表面上看采用ConcurrentHashMap进行contain和put操作没有任何问题。但是仔细想下,尽管 containsKey和 put 两个方法都是原子的,但在jvm中并不是将这段代码做为单条指令来执行的,例如:假设连续生成2个随机数1,map的 containsKey 和 put 方法由线程A和B 同时执行 ,那么有可能会出现A线程还没有把 1 put进去时,B线程已经在进行if 的条件判断了,也就是如下的执行顺序:
A: map 正在放置随机数 1 进去
A 被挂起
B: 执行 map.containsKey(1) 返回false
B: 将随机数 1 放进 map
A: 将随机数 1 放进 map
map 中key 为1 的value值 还是为 1
这样会导致虽然生成了2次随机数 1 ,它的value值还是1,我们期望的结果应该是2,这并不是我们想要的结果。概括的说就是两个线程同时竞争map, 但他们对map访问顺序必须是先 containsKey 然后再 put 对象进去,即产生了竞态条件。解决方法当然就是同步了,现在我们将代码改成如下:
public class ThreadSafeTest {public static Map<Integer,Integer> map=new ConcurrentHashMap<>();
public static void main(String[] args) {ExecutorService pool1 = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {pool1.execute(new Runnable() {@Overridepublic void run() {Random random=new Random();int randomNum=random.nextInt(10);countRandom(randomNum);}});}
}
public static synchronized void countRandom(int randomNum){if(map.containsKey(randomNum)){map.put(randomNum,map.get(randomNum)+1);}else{map.put(randomNum,1);}
}
}
上述代码在当前类中没有线程安全的问题,但依然有线程安全的危险,成员变量map依然有可能会在其他地方被更改,在java并发中属于无效的同步锁
ThreadLoacl线程单例
这个单例可能要画一个引号了,因为并不能保证整个应用全局唯一,但是可以保证线程唯一,这么理解呢?
public class ThreadLoaclInstance {private static final ThreadLocal<ThreadLoaclInstance> THREAD_LOACL_INSTANCE_THREAD_LOCAL=new ThreadLocal<ThreadLoaclInstance>(){@Overrideprotected ThreadLoaclInstance initialValue() {return new ThreadLoaclInstance();}};private ThreadLoaclInstance(){}public static ThreadLoaclInstance getInstance(){return THREAD_LOACL_INSTANCE_THREAD_LOCAL.get();}
}
测试类
/*** 应用类*/
public class Test {public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {Thread t1=new Thread(()->{ThreadLoaclInstance instance=ThreadLoaclInstance.getInstance();System.out.println(Thread.currentThread().getName()+" "+instance);});Thread t2=new Thread(()->{ThreadLoaclInstance instance=ThreadLoaclInstance.getInstance();System.out.println(Thread.currentThread().getName()+" "+instance);});t1.start();t2.start();System.out.println("program end");}
}
运行结果
program end
Thread-1 com.design.pattern.creational.singleton.ThreadLoaclInstance@2831008d
Thread-0 com.design.pattern.creational.singleton.ThreadLoaclInstance@59a30351
看到两个线程运行拿到的对象并不是同一个。现在想象下main本身是一个线程,里面又开了两个线程,如果我们在main里面拿呢?
/*** 应用类*/
public class Test {public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {System.out.println("main Thread"+ThreadLoaclInstance.getInstance());System.out.println("main Thread"+ThreadLoaclInstance.getInstance());System.out.println("main Thread"+ThreadLoaclInstance.getInstance());System.out.println("main Thread"+ThreadLoaclInstance.getInstance());System.out.println("main Thread"+ThreadLoaclInstance.getInstance());System.out.println("main Thread"+ThreadLoaclInstance.getInstance());Thread t1=new Thread(()->{ThreadLoaclInstance instance=ThreadLoaclInstance.getInstance();System.out.println(Thread.currentThread().getName()+" "+instance);});Thread t2=new Thread(()->{ThreadLoaclInstance instance=ThreadLoaclInstance.getInstance();System.out.println(Thread.currentThread().getName()+" "+instance);});t1.start();t2.start();System.out.println("program end");}
}
运行结果
main Threadcom.design.pattern.creational.singleton.ThreadLoaclInstance@78308db1
main Threadcom.design.pattern.creational.singleton.ThreadLoaclInstance@78308db1
main Threadcom.design.pattern.creational.singleton.ThreadLoaclInstance@78308db1
main Threadcom.design.pattern.creational.singleton.ThreadLoaclInstance@78308db1
main Threadcom.design.pattern.creational.singleton.ThreadLoaclInstance@78308db1
main Threadcom.design.pattern.creational.singleton.ThreadLoaclInstance@78308db1
program end
Thread-1 com.design.pattern.creational.singleton.ThreadLoaclInstance@735037e6
Thread-0 com.design.pattern.creational.singleton.ThreadLoaclInstance@1894a1d6
看到main线程中拿到的都是相同的对象,和两个线程拿到的都不相同,我们看下,ThreadLocal会为每个线程提供一个独立的副本,源码里面是基于
static class ThreadLocalMap {...
做的。
ThreadLoacl维持了线程间的隔离,隔离了对数据访问的冲突,对于多线程资源共享的情况下,我们想象下使用同步锁,其实就是以时间换空间的方式,因为要排队;ThreadLoacl 就是空间换时间的方式,它会创建很多对象,在一个线程下是唯一的,为每个线程创建了一个对象,线程相互不会影响,这就是基于ThreadLocal的“单例模式”实现方案
单例模式源码分析
Runtime饿汉式
public class Runtime {private static Runtime currentRuntime = new Runtime();public static Runtime getRuntime() {return currentRuntime;}/** Don't let anyone else instantiate this class */private Runtime() {}
java.awt.Desktop 容器单例
public static synchronized Desktop getDesktop(){if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();if (!Desktop.isDesktopSupported()) {throw new UnsupportedOperationException("Desktop API is not " +"supported on the current platform");}sun.awt.AppContext context = sun.awt.AppContext.getAppContext();Desktop desktop = (Desktop)context.get(Desktop.class);if (desktop == null) {desktop = new Desktop();context.put(Desktop.class, desktop);}return desktop;}
spring中AbstractFactoryBean 单例的影子
public final T getObject() throws Exception {if (isSingleton()) {return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());}else {return createInstance();}
}private T getEarlySingletonInstance() throws Exception {Class<?>[] ifcs = getEarlySingletonInterfaces();if (ifcs == null) {throw new FactoryBeanNotInitializedException(getClass().getName() + " does not support circular references");}if (this.earlySingletonInstance == null) {this.earlySingletonInstance = (T) Proxy.newProxyInstance(this.beanClassLoader, ifcs, new EarlySingletonInvocationHandler());}return this.earlySingletonInstance;
}
Mybatis中ErrorContext 基于ThreadLocal的单例模式
package org.apache.ibatis.executor;public class ErrorContext {private static final String LINE_SEPARATOR = System.lineSeparator();private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal();private ErrorContext stored;private String resource;private String activity;private String object;private String message;private String sql;private Throwable cause;private ErrorContext() {}public static ErrorContext instance() {ErrorContext context = (ErrorContext)LOCAL.get();if (context == null) {context = new ErrorContext();LOCAL.set(context);}return context;}...
最后
单例模式在面试中必考点,很多面试官喜欢用单例模式来挖掘面试者的深度,希望小伙伴们把单例模式实现方案、优缺点、应用场景、在源码中的使用、序列化的破坏、反射攻击的防御等理解透,拿到满意的offer。
最简单又最复杂的单例模式相关推荐
- 简单介绍工厂模式和单例模式
工厂模式: 介绍: 工厂模式主要是为创建对象提供过渡接口,以便将创建对象的具体过程(new 关键字和具体的构造器)隐藏起来.用一个工厂方法来替代,对外提供的只是一个工厂方法,达到提高灵活性的目的. ...
- 单例模式 -- 简单
单例模式(Singleton) 1. 简单单例 单例模式是一种常用的设计模式.在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在.这样的模式有几个好处: 1. 系统开销小.某些类创 ...
- socket可以写成单例嘛_精读《设计模式 - Singleton 单例模式》
Singleton(单例模式) Singleton(单例模式)属于创建型模式,提供一种对象获取方式,保证在一定范围内是唯一的. 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点. 其实单例模 ...
- filter java 是单例的吗_JAVA 设计模式之 单例模式详解
单例模式:(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点.单例模式是创建型模式.单例模式在现实生活中应用也非常广泛. 在 J2EE 标准中,S ...
- Java使用简单工厂模式对面向接口编程模式的深度解耦实现
在Java和C#的编程世界里,并没有出现像C++那样的多脉继承,它们只支持单一的继承,或者多级继承,这一变化最大的影响,我觉得是大大的降低了编程的难度,因为没有了C++的多级多脉继承,所以接口出现了, ...
- 设计模式 (3) : 单例模式的几种方法
定义: 确保一个类只有一个实例, 并提供一个全局访问点. 原理: c# 中用 new 方法创建一个实例需要调用类的构造函数(注: 每一个类都必须有至少一个构造函数, 当我们未定义构造函数时,编译时编译 ...
- 单例模式的5种实现方法及优缺点
单例模式是GoF23种设计模式中创建型模式的一种,也是所有设计模式中最简单的一种. 单例模式是用来保证系统中某个资源或对象全局唯一的一种模式,比如系统的线程池.日志收集器等.它保证了资源在系统中只有一 ...
- javascript --- 手写Promise、快排、冒泡、单例模式+观察者模式
手写promise 一种异步的解决方案, 参考 Promise代码基本结构 function Promise(executor){this.state = 'pending';this.value = ...
- java 窗口 单例_java单例模式实现面板切换
本文实例为大家分享了java单例模式实现面板切换的具体代码,供大家参考,具体内容如下 1.首先介绍一下什么是单例模式: java单例模式是一种常见的设计模式,那么我们先看看懒汉模式: public c ...
- 从一个简单的Java单例示例谈谈并发
一个简单的单例示例 单例模式可能是大家经常接触和使用的一个设计模式,你可能会这么写 public class UnsafeLazyInitiallization { private static Un ...
最新文章
- [转]ASP.NET页面生命周期描述
- ffmpeg连接超时与解码超时
- curl http header_PHP如何解析header头部信息
- 服务器上的电脑登不上oracle,Oracle服务器改计算机名后报错之解决方法
- R语言第四讲 之R语言数据类型
- C语言libcurl例程:multi 多线程,多任务
- python将excel导入生成矩阵_Python导入数值型Excel数据并生成矩阵操作
- hive外部表/内部表路径知识点
- 斐波那契序列 Fibonacci
- 银行软件业务开发分类杂谈-多年前的旧文
- Yii Framework2.0开发教程(7)账户注册开发
- 使用Excel背单词-高效-简单
- 用while输出1到100的偶数python_用while语句,求1到100的偶数之和
- 原生HTML:img 相关属性详解(alt属性,onerror事件,以及其他基本属性),css中的object-fit
- android自适应屏幕方向,Android 屏幕自适应方向尺寸与分辨率-Fun言
- 微信小程序实现图片预览的功能
- metaWRAP bin_refine 模块如何优化分箱结果
- Js与Jq实战:第七讲:jQuery基础
- 2022年应届大学毕业生就业分析报告
- 2022.7.19 防火墙知识点