Java-单例模式
Posted 骑着乌龟去看海
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java-单例模式相关的知识,希望对你有一定的参考价值。
什么是单例模式?
单例对象的类必须保证只有一个实例存在;
单例模式要考虑的几个方面?
线程安全,延迟加载,序列化与反序列化安全
几种实现方式:
第一种:简单的懒汉或恶汉模式
/** * 单例模式 */ public class SingletonTest { private static SingletonTest singletonTest = null; private SingletonTest() { } public static SingletonTest instance() { if (singletonTest == null) { singletonTest = new SingletonTest(); } return singletonTest; } }
第二种:方法上添加synchronized关键字,可以实现线程安全,但由于锁加在了方法上,性能较低;
/** * 单例模式 */ public class SingletonTest { private static SingletonTest singletonTest = null; private SingletonTest() { } public static synchronized SingletonTest instance() { if (singletonTest == null) { singletonTest = new SingletonTest(); } return singletonTest; } }
第三种:双重检测机制
双重检测机制,主要是为了防止一个线程进入锁之后,另一个线程也已经过了instance==null的校验之后出现的问题;
/** * 单例模式 */ public class SingletonTest { private static SingletonTest singletonTest = null; private SingletonTest() { } public static SingletonTest instance() { if (singletonTest == null) { synchronized (SingletonTest.class) { if (singletonTest == null) { singletonTest = new SingletonTest(); } } } return singletonTest; } }
第四种:valatile机制
首先要明白两个点:原子操作,指令重排;
1. 原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。
比如,简单的赋值是一个原子操作:m = 6; 假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。
而声明并赋值就不是一个原子操作:int n = 6; 对于这个语句,至少有两个操作:
①声明一个变量n
②给n赋值为6
——这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。
——这样,在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能会导致不稳定的结果出现。
2. 指令重排:
指令重排:Java中,是JVM为了提交执行效率做的一些优化,即在不影响结果的情况下,可以能会对一些语句的执行顺序进行调整;
int a ; // 语句1 a = 8 ; // 语句2 int b = 9 ; // 语句3 int c = a + b ; // 语句4
正常来说,对于顺序结构,执行的顺序是自上到下,也即1234;但是,由于指令重排的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。
由于语句3和4不是原子操作,所以语句3和语句4也可能会拆分成原子操作,再重排。也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。
而在单例中,由于singleton = new Singleton()这句并非是一个原子操作,所以会被编译器编译成如下JVM指令:
a. 给对象singleton分配内存空间 :memory=allocate();
b. 调用构造函数初始化对象singleton:ctorInstance(memory);
c. 设置singleton 指向刚分配的内存空间(这一步执行完singleton 就不是null了);
但是由于JVM中存在指令重排的优化,上面第二步和第三步的执行顺序不是固定的,所以最终执行的顺序可能是abc,也可能是acb。如果是后者,则在c执行完,而b没执行之前,另一个线程2在读到第一个singleton ==null时,这时候singleton 已经不是null(但却没有初始化),所以线程2会返回singleton ,但该singleton 直接使用就会报错了;也就是说,存在一个【singleton 已经不是null但还没有初始化】的中间状态;
这里的关键在于线程1对singleton 的写操作还没完成,线程2就执行了读操作;
而要避免这种操作,可以通过关键字volatile关键字来实现;
volatile关键字的一个作用就是禁止指令重排,保证了指令的顺序执行,这样在线程2看来,singleton 对象的引用要么指向null,要么指向一个初始化完成的singleton ,而不会出现某个中间状态;
volatile的一个问题是反射,不过可以把类设置为抽象类;
/** * 单例模式 */ public class SingletonTest { private static volatile SingletonTest singletonTest = null; private SingletonTest() { } public static SingletonTest instance() { if (singletonTest == null) { synchronized (SingletonTest.class) { if (singletonTest == null) { singletonTest = new SingletonTest(); } } } return singletonTest; } }
第五种:Effective Java推荐,内部类的懒加载模式
这种写法非常巧妙:
-
对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。
-
同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。
- 它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现。
/** * 单例模式 */ public class SingletonTest { private static class SingletonHolder { private static final SingletonTest INSTANCE = new SingletonTest(); } private SingletonTest() { } public static SingletonTest instance() { return SingletonHolder.INSTANCE; } }
第六种:Effective Java推荐,枚举;
由于创建枚举实例的过程是线程安全的,所以枚举的写法也没有同步的问题;
public enum SingleInstance { INSTANCE; public void fun1() { // do something } } // 使用 SingleInstance.INSTANCE.fun1();
参考自:https://www.cnblogs.com/dongyu666/p/6971783.html
公众号:《算法爱好者》
以上是关于Java-单例模式的主要内容,如果未能解决你的问题,请参考以下文章