在谈单例设计模式的时候,有必要谈一谈设计模式
(1)设计模式:被反复使用,多数人知晓,经过实践的"代码设计经验"的总结,是解决某种特定问题的手段
(2)目的:提高代码的重用性、可读性、可维护性
参考:点击打开链接
下面进入本节的重点
一、单例模式(Singleton Model)
(1)概念:顾名思义,一个类只有一个实例,并且自行实例化(需求),整个项目系统都能访问该实例
模式的需求来源:
(1)对于那些比较耗内存的类,只实例化一次可以大大提高性能,尤其是在移动开发中。
(2)保持程序运行的时候该中始终只有一个实例存在内存中(2)单例模式的分类:懒汉模式、饿汉模式
(3)二者的区别(创建时机):
懒汉模式:延迟加载,在实例的第一次使用时创建
饿汉模式:实例在类装(加)载时创建
(4)二者的问题:
饿汉模式---如果初始化太早,而没有及时使用的话,会占用资源(内存),造成资源浪费
懒汉模式---线程不安全
---------------------------------------------------------------------------------------
首先分析饿汉模式
顾名思义,饥不择食,需不需要都先创建在内存中存在,随类的加载而加载
例1
package www.wzj.singleton;
public class SingletonHungry {
//饿汉式:需不需要都先创建---创建时机是在类加载时
/**
* 分析:
* (1)由于只有一个实例,因此外界不能创建该对象,将构造方法私有化
* (2)由于构造方法自由化,所以只能在该类的"内部"创建该类的唯一对象(实例)
* (3)由于在整个项目系统上都能访问该实例,因此将该实例设置成"静态成员变量",
* 并且通过给外界提供一个接口(静态的getXXX的方法)供外界访问
*/
private SingletonHungry(){
}
private static SingletonHungry instance=new SingletonHungry();
public static SingletonHungry getInstance(){
return new SingletonHungry();//new 的时候完成了两个操作,会调用构造方法,这里调用私有构造方法
}
}
缺点:创建的开销较大,如果不使用比较耗资源
再分析一下懒汉模式
顾名思义,火烧眉毛了才知道创建,也即:在要使用的时候才去创建对象
例2
package www.wzj.singleton;
public class SingletonLazy {
/**
* (1)外界不能创建对象,构造方法"私有化",只能在"类内"创建
* (2)由于要在真个项目系统上可以访问,因此设置为"静态"成员变量(但不初始化)---类的层面
* (3)外界访问的话,由于不能创建对象,可以给予其一个接口(通过"静态"的getXXX方法获取)
*
* 懒加载代码设计思想:
* 每次获取instance之前,要先进行判断;
* 如果instance已经存在,直接返回已经存在的instance
* 如果不存在说明,是首次使用,则需要创建
*
*
*/
private SingletonLazy(){
}
private static SingletonLazy instance=null;//不初始化(不需要随着类的加载而加载)
public static SingletonLazy getInstance(){
if(instance==null){
instance= new SingletonLazy();
}
return instance;
}
}
貌似上面的很完美,但是如果在多线程的环境下呢?
试分析一下:
问题:如果有两个线程来访问该实例,两个线程都执行完if(instance!=null)语句, 此时线程B抢到了CPU的执行权,执行下一条语句"instance= new SingletonLazy()"此时对象已经创建,但是线程A又抢到了CPU的执行权,虽然instance已经创建,但由于已经进行if判断,又会重新(new)创建新的对象,即:两个对象不一致,不是单例;
由此,多线程下,可能会出现数据安全性问题(对象不是自己想要的)
解决方案:加锁(将多条语句操作的共享数据保护起来,保证此代码某个时间段处于"单线程"的环境
例3
package www.wzj.singleton;
public class SingletonLazySafe {
private SingletonLazySafe(){
//构造方法私有化
}
private static SingletonLazySafe instance=null;
public static SingletonLazySafe getInstance(){
synchronized (SingletonLazySafe.class){ //重点:静态的锁对象是字节码文件对象--类名.class
if(instance==null){
instance=new SingletonLazySafe();
}
return instance;
}
}
}
例3似乎已经很完美了,线程安全的问题完美解决了,但是你是否思考过效率问题?
我们知道线程的加锁会使线程的效率降低至少100倍,如果更复杂的情况呢?
问题描述:给getInstance方法加锁,避免了线程安全的问题,但是会"强制"除了获取锁的线程的其它线程处于等待状态,
对程序的执行效率造成负面影响
明确一点:效率和安全总是相悖的
引申一点:算法的复杂度(时间和空间),耗时和存储的问题
解决思路:双重检查(Double Check)
例4
package www.wzj.singleton;
public class SingletonPerforance {
/**
* 说明:只有"第一次"才会同步(解决了线程安全的问题),多次(第2..)的话,其余线程直接通过If(instance!=null)的判断,直接
*
* 跳过synchronized()的代码块,不再处于等待状态,大大提高了执行效率
* 第一个if判断是保证线程的效率,而synchronized中的第二个if判断是保证功效数据的的(同步),解决线程安全的问题
*/
private SingletonPerforance(){
}
private static SingletonPerforance instance=null;//类层面,整个系统都能访问
public static SingletonPerforance getInstance(){
if(instance==null){
synchronized (SingletonPerforance.class){
if(instance==null){
instance=new SingletonPerforance();
}
}
}
return instance;
}
}
例3、例4的简单说明
SingletonLazySafe代码:为了解决1%几率的问题(线程安全),使用了100%的防护盾
SingletonPerforance代码优化思路:把100%出现的防护盾,改为1%几率的问题
即:将保护盾精确到"导致多个实例出现的地方"
详细说明:只有"第一次创建对象的时候"才会同步(解决了线程安全的问题),多次(第2、3..)的话,其余线程直接通过
If(instance!=null)的判断,直接跳过synchronized()的代码块,不再处于等待状态,大大提高了执行效率
----------------------------------
上面的似乎已经很完美了,但是是不是还有什么问题(如果你对操作系统有一定的了解的话)?
我们先来了解两个概念:原子操作和指令重排
原子操作
赋值操作"a=0":是原子操作
声明并赋值不是一个原子操作:int n=6
原因:完成了至少两个操作(1)声明一个变量(分配内存空间)(2)赋值(n=6)
出现问题的地方:会出现一个"中间状态":n被声明了但是未被赋值的状态
此处:多线程执行环境中,线程执行顺序的不确定性,如果两个线程都使用n,可能会出现结果的不稳定
例5
package www.wzj.singleton;
public class SingletonPerfect {
/**
* 说明:SingletonPerforance"看起来"似乎非常完美
*
* 缺陷:涉及原子操作和指令重排
*
*原子操作:不会随线程调度被打断的操作,
*
* 例如:赋值操作"a=0"是原子操作
*
* 声明并赋值不是一个原子操作:int n=6
*
* 原因:完成了至少两个操作(1)声明一个变量(分配内存空间)(2)赋值(n=6)
*
* 出现问题的地方:会出现一个"中间状态":n被声明了但是未被赋值的状态
*
* 此处:多线程执行环境中,线程执行顺序的不确定性,如果两个线程都使用n,可能会出现结果的不稳定
*
* ---------------------------
*
* 指令重排:计算机为了提高执行效率,会做一些优化:"不影响结果"的前提下,对一些语句的执行顺序进行调整
*
* 举例:
* int a;
* a=8;
* int b=9;
* int c=a+b;
*
* 指令重排,可能实际执行顺序为1324或3124(正常的话为1234)
*
* 出现问题:由于指令重排,语句3和4也会被拆分成原子操作,再重排
*
* 总结:对于非原子性的操作,在不影响最终结果的情况下,"其拆分的原子操作"可能会被指令重排
*
* ------------------------------------------
*
* SingletonPerforance类的问题
*
* 问题:出现在 instance=new SingletonPerforance();
*
* 说明:不是原子操作,JVM大致完成的事情(正常的顺序)
*
* (1)给instance分配内存
* (2)调用SingletonPerforance的构造函数初始化成员变量
* (3)将instance对象(堆中)"指"向分配的内存空间
*
* 只有第(3)步执行完了,instance才是非空,由于JVM的即时编译存在指令重排的优化
*
* 即:(2)、(3)步的执行顺序可能会发生变化,(1)必须先完成(指令重排的前提)
*
* 导致现象:(3)执行完毕而(2)未执行,被线程2抢到,此时instance已经是非null(但却没有被初始化)---停留在中间状态
*
* 而线程2完成if(instance!=null)的判断,直接把中间状态的instance拿去用了,会出现问题
*
* 也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。
* 如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),
* 所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
* 再稍微解释一下,就是说,由于有一个(instance已经不为null但是仍没有完成初始化)的中间状态,
* 而这个时候,如果有其他线程刚好运行到第一层if (instance ==null)这里,
* 这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。
* 这里的关键在于线程T1对instance的写操作没有完成,线程T2就执行了读操作。
*
* 解决方法:给instance赋上关键字:volatile
*
* volatile作用:禁止指令重排,变量声明volatile之后,保证写一个操作(1、2、3)之前不会调用读操作(if(instance==null))
*
* 注意1:
*
* volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障,
*
* 这样,在它的赋值(写操作 instance=new SingletonPerfect())完成之前,就不用会调用读操作((if(instance==null)))
*
* 注意2:
* volatile阻止的不是singleton = new Singleton()这句话内部[1-2-3]的指令重排,
*
* 而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。
*/
private SingletonPerfect(){
}
private static volatile SingletonPerfect instance=null;
public static SingletonPerfect getInstance(){
if(instance==null){
synchronized (SingletonPerfect.class){
if(instance==null){
instance=new SingletonPerfect();
}
}
}
return instance;
}
}
是不是只有这一种单例模式的完美写法,当然不是,还有一种更简单的
例6
package www.wzj.singleton;
public class SingletonOther {
/**
* 静态内部类的形式(懒汉和饿汉的兼容)
*
* 这种写法的巧妙之处在于:
*
* 对于内部类SingletonHolder,它是一个饿汉式的单例实现,
* 在SingletonHolder初始化的时候会由ClassLoader来"保证同步",使INSTANCE是一个真单例。
* 同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,
* 它才会被加载的(加载的时机:也就是在getInstance()方法第一次被调用的时候)--保证了使用时才创建
* 它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。
* 从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现
*
* 疑问:final的含义
*/
private static class SingletonHolder{
private static final SingletonOther INSTANCE=new SingletonOther();
}
private SingletonOther(){
}
//只有在使用的时候才创建,并且线程安全由类加载器保证同步
public static final SingletonOther getInstance(){
return SingletonHolder.INSTANCE;
}
}