单例模式

 

在谈单例设计模式的时候,有必要谈一谈设计模式

(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;
    }

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值