java设计模式之单例模式——懒汉式(饱汉式)及多线程debug

1、懒汉式定义:

懒汉式设计模式:比较懒,在类创建时不创建对象,而是以延迟加载的方式,当需要使用时才创建。

2、懒汉式的优缺点:

3、懒汉式的基础创建方式:

package com.zxl.design.zxl.design.pattern.singleton;

/**
 * Created by Administrator on 2019/6/16.
 * 懒汉式是指初始化时是不创建的,在运行时延迟加载。
 * 单线程时没问题,但是多线程时会出现问题
 *
 */
public class LazySingleton {
    /**
     * 初级懒汉式单例设计模式
     * 线程不安全的,单线程时不会出问题,但是多线程时会出问题
     * 假设两个线程同时运行,第一个线程在判断完if (lazySingleton == null)为true,
     * 进入到方法内,但是还未执行创建对象或者还未执行完创建对象的步骤时,
     * 此时lazySingleton 还是null,第二个线程刚好走到判断是否为null时,则判断为null,
     * 之后线程二也会进入该方法。这样两个线程都进入,则都会正常创建对象, 造成创建两个对象。
     *即使最后lazySingleton返回的对象一样(因为lazySingleton被赋值两次,后一次将前一次覆盖)
     * 也同样是存在安全隐患。
     * */
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){

    }
    public static LazySingleton getInstance(){
        if (lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

1)该类的基本使用方案:

package com.zxl.design.zxl.design.pattern.singleton;

/**
 * Created by Administrator on 2019/6/16.
 */
public class Test {
    public static void main(String[] args) {
        //该方案为单例模式,证明目前没有问题,
        //因为LazySingleton的构造方法私有,外部无法调用,所以
        //只能通过提供的公共访问方法构建。注:大家习惯用getInstance()这个方法名
        //但并不是必须的。建议可读性好,一般用该方法名
        //该测试方案通过debug的方式可以说明,类加载时是没有创建对象的,
        //等到使用该类时,才创建了对象,也就是所说的延迟加载。
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.print("program end");
    }
}

2)如何验证该方案存在问题呢?

我们要创建一个多线程的环境,通过debug方式手动控制其运行节奏,模拟该可能性。

 首先创建一个线程类(后面还会用到),在其中通过不安全的懒汉式创建一个对象。并打印线程的名字和它的值

然后测试类中创建两个线程,并开启。具体如下:

package com.zxl.design.zxl.design.pattern.singleton;

import java.net.SocketPermission;

/**
 * Created by Administrator on 2019/6/23.
 */
public class T implements Runnable {
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName() +" "+ lazySingleton);
    }
}
package com.zxl.design.zxl.design.pattern.singleton;

/**
 * Created by Administrator on 2019/6/16.
 */
public class Test {
    public static void main(String[] args) {
//        //该方案为单例模式,证明目前没有问题,
//        //因为LazySingleton的构造方法私有,外部无法调用,所以
//        //只能通过提供的公共访问方法构建。注:大家习惯用getInstance()这个方法名
//        //但并不是必须的。建议可读性好,一般用该方法名
//        //该测试方案通过debug的方式可以说明,类加载时是没有创建对象的,
//        //等到使用该类时,才创建了对象,也就是所说的延迟加载。
//        LazySingleton lazySingleton = LazySingleton.getInstance();
//        System.out.print("program end");
        Thread t1 = new Thread(new T());
        Thread t2 = new Thread(new T());
        t1.start();
        t2.start();
        System.out.print("program end");

    }
}

在如上不加干预的情况下,看最终运行的结果,好像是没问题的,但是在多线程中呢?

"D:\Program Files\Java\jdk1.8.0_102\bin\java" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:62545,suspend=y,server=n -Dfile.encoding=UTF-8 -classpath "D:\Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\deploy.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\zipfs.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\javaws.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jfxswt.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\management-agent.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\plugin.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;C:\Users\Administrator\Desktop\gson-master\gson-master\DesignMode\target\classes;D:\InteliijIDea\IntelliJ IDEA 2017.1.4\lib\idea_rt.jar" com.zxl.design.zxl.design.pattern.singleton.Test
Connected to the target VM, address: '127.0.0.1:62545', transport: 'socket'
Thread-0 com.zxl.design.zxl.design.pattern.singleton.LazySingleton@54173bf4
Thread-1 com.zxl.design.zxl.design.pattern.singleton.LazySingleton@54173bf4
program endDisconnected from the target VM, address: '127.0.0.1:62545', transport: 'socket'

Process finished with exit code 0

如上,两个结果看似一样,但是如何通过debug手动干预多线程运行节奏呢?这个需要学习intelij idea工具的多线程debug调试方法了。

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第1张图片

如上,在多线程的run方法中,先打断点(具体位置根据业务需求确定,这里我们在创建对象时打断点),然后右键,可弹出如上图所示的设置页面,具体含义为:

1、默认会选择enabled 和suspend 表示是否可用和挂起的方式(all 和 thtead)

2、挂起方式(suspend)默认会选择All,而我们需要选择挂起方式为Thread方式,这样就可以通过打断点的方式,阻塞到这里,等待我们手动处理,实现手动控制几个线程运行的顺序和节奏,模拟不同的多线程运行可能性的目的。而all 只会debug到本线程的断点,所以此处一定要选择thread 。

3、当选择thread时,后面会产生一个make default按钮,选中(变暗)情况下,表示以后默认情况下,都采用thread方式。

注:了解了如上内容,在此,我们在T 类(创建对象) 、单例模式的创建(判断是否为空时)、测试类(打印最后一句时)这几个位置都打上断点,并设置如上debug方式。然后开始debug 调试运行。

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第2张图片

如上图,左侧大圈框住部分frame,点击可以看到三个线程 main、t1、t2,都已经运行,点击倒三角可以下拉,在此处可以切换线程,右侧values查看具体某个线程的信息,同时当左侧选中某个线程时,下一步可以单独改变该线程的进度,在不断切换线程,改变其进度的条件下,就能认为创建我们想要模拟的状况。

如下左下角,可以看到我们在选择到theread0 线程上,改变它的运行进度

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第3张图片

此处具体可以根据上方担心的安全问题,通过控制多线程的方式,调节到描述的情形,验证是否存在我们所担心的问题

为了更好的帮助自己看这篇,此处附上几张模拟如上情形时的几张关键图片,尽可能配以注释。

第一,直接debug运行Test.main 到下图处,注意红色笔圈住部分

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第4张图片

第二、切换到Thread0,F6单步运行,让其阻塞到即将创建单例对象的位置,右侧可看到 lazySingleton=null 的位置,如下图

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第5张图片

第三,再切换到Thread1 上,同样单步执行到此状态。如下图

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第6张图片

 第四、再次切换到Thread0,单步运行,执行到如下情形,注意右侧this,即对象的值465,创建的lazySingleton值为479

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第7张图片

第五、再次切换线程到Thread1,单步运行一次,执行到如下情形 ,再次注意右侧的值480,此时可看到两个值不一样

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第8张图片

 第六、再次切换线程到thread0 ,继续单步运行,查看lazySingleton的值

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第9张图片

 此刻,我们可以看到之前thread0 创建的值还是479,而现在返回的值是480,暂时先放一下,我们全部执行完毕看下结果,回头再分析为什么此处会有这样的变化。

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第10张图片

 最后的执行结果就是两个线程返回了同一个对象。

分析,首先要了解一个点:

LazySingleton lazySingleton = LazySingleton.getInstance();

该句执行了三步,包括在栈区创建一个引用,堆区实际创建一个对象,将对象的地址值给引用。

因为这一行要有三步,在多线程实际运行中,因为运行快,很有可能出现上方描述的情形,导致出现安全问题,因为最后返回的是lazySingleton 这个引用的值,所以返回的结果一致。但明确的是,这里堆区创建了两个对象。如果实际更多的线程,创建一个耗资源大的对象,虽然返回的值是同一个,但是至少也创建了多个对象,浪费了存储空间。

那针对懒加载模式存在的如此问题,如何解决呢?

4、针对上方分析,出现的问题,解决方案有如下几种:

1)在getInstanche() 方法前,加上synchronized 关键字,即加上锁,因为这个方法是static修饰的,所以这个锁相当于加在了这个单例类上。(扩展:如果方法没有加static修饰,相当于在堆内存中为这个对象加上了锁。注意区别)

package com.zxl.design.zxl.design.pattern.singleton;

/**
 * Created by Administrator on 2019/6/16.
 * 懒汉式是指初始化时是不创建的,在运行时延迟加载。
 * 单线程时没问题,但是多线程时会出现问题
 *
 */
public class LazySingleton {
    /**
     * 初级懒汉式单例设计模式
     * 线程不安全的,单线程时不会出问题,但是多线程时会出问题
     * 假设两个线程同时运行,第一个线程在判断完if (lazySingleton == null)为true,
     * 进入到方法内,但是还未执行创建对象或者还未执行完创建对象的步骤时,
     * 此时lazySingleton 还是null,第二个线程刚好走到判断是否为null时,则判断为null,
     * 之后线程二也会进入该方法。这样两个线程都进入,则都会正常创建对象, 造成创建两个对象。
     *即使最后lazySingleton返回的对象一样(因为lazySingleton被赋值两次,后一次将前一次覆盖)
     * 也同样是存在安全隐患。
     * */
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){

    }
    //如下加上同步方法,表示对类LazySingleton加了同步锁
    public synchronized static LazySingleton getInstance(){
        if (lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

//    该方法和上方实际道理相同,上方写法更简洁
//    public  static LazySingleton getInstance(){
//        synchronized(LazySingleton.class){
//            if (lazySingleton == null){
//                lazySingleton = new LazySingleton();
//            }
//        }
//        return lazySingleton;
//    }
}

单纯改成如上,会是什么情况呢?类似之前方式,重新跟踪一遍

第一、将之前代码运行完毕,更改成如上,debug Test.main 

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第11张图片

第二,切换线程到thread0,执行到即将创建对象的那一行。

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第12张图片

第三、将线程切换到thread1,单步运行,查看状态,注意这里同之前的代码运行时的变化,此处,我们无法将其运行到类似第一次的情形,因为这里,thread1,状态不是running,而是monitor,即监控,具体看文字,表示当前线程阻塞。

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第13张图片

因为加了同步锁安全防范机制,所以此处无法进入,所以全部执行,会看到效果是有用的,达到了实际创建一个对象的目的。

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第14张图片

但此方案有什么特点呢?

优点就是通过加同步锁,避免了安全性问题,但因为对整个类加锁,对性能影响较大。如果对性能要求不高,可采用此方案,否则该方案不可行。那还有其他更好的方案吗?  有的,请看后续更改方案。

2)双重锁检查内存机制。平衡性能和安全问题

package com.zxl.design.zxl.design.pattern.singleton;

/**
 * Created by Administrator on 2019/6/23.
 */
public class LazyDoubleCheckSingleton {
    /**
     * 懒汉式(饱汉式)单例设计模式增强版
     * 该方案通过双重检查锁的判断,缩小锁加载的范围,
     * 改善对类加锁影响范围较大的问题,平衡性能和安全
     * */
    private static LazyDoubleCheckSingleton lazyDoubleSingleton = null;
    private LazyDoubleCheckSingleton(){

    }
    //通过双重检查锁的判定,对方法加上锁
    //看似没问题,但实际该方案有个指令重排序问题隐患
    //lazyDoubleSingleton = new LazyDoubleCheckSingleton();
    // 正常如上一句代码需要完成如下1、2、3步骤
    //但如果在实际运行时,系统先处理第三步,再处理第二步,就可能出现问题了
    public static LazyDoubleCheckSingleton getDoubleSingletonInstance(){
        if (lazyDoubleSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if (lazyDoubleSingleton == null){
                    lazyDoubleSingleton = new LazyDoubleCheckSingleton();
                    //1、分配内存给这个对象
                    //------------------------//3、设置lazyDoubleSingleton 分配的内存地址
                    //2、初始化对象
                    //3、设置lazyDoubleSingleton 指向刚分配的内存地址
                    //intra-thread semantics (java语言规范,所有java线程在执行程序在时
                    //必须保证如上规则,即不得改变单线程内的代码运行顺序,但多线程中无法保证)
                }
            }
        }
        return lazyDoubleSingleton;
    }
}

如上,展示了该方案的做法,其中嵌入了可能存在的问题的描述,下面详细分析。摘取图片如下:

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第15张图片

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第16张图片

上图解释了上方所写的初级的使用双重检查锁机制可能存在的问题。图上左右为两个线程,各个线程从上往下执行,而多线程中,两个线程执行先后顺序的差异,尤其是2 、3两个步骤调换造成的多线程安全问题。

如何解决:

(1)不允许2、3两步进行重排序。方案就是为实例变量引用添加volatile关键字;

volatile 关键字可以实现线程安全的延迟初始化,禁止重排序,在多线程中,volatile可以实现所有多线程看到共享内存的东西,volatile修饰的共享变量在进行写操作时,汇编时会多出一些代码,该内容起到两个作用:

    a、将当前处理器缓存的数据缓存写回到内存,

    b、这个写回到内存时,会使其他CPU里缓存了该共享  数据的内容无效,从而使得其他CPU再从共享内存中读取数据以更新,这样就保证了其他CPU或者线程读取该值时,和当前处理器获取到的值一致。此处主要用了缓存一致性协议。

代码即

package com.zxl.design.zxl.design.pattern.singleton;

/**
 * Created by Administrator on 2019/6/23.
 */
public class LazyDoubleCheckSingleton {
    /**
     * 懒汉式(饱汉式)单例设计模式增强版
     * 该方案通过双重检查锁的判断,缩小锁加载的范围,通过volatile 关键字保证不发生重排序
     * 改善对类加锁影响范围较大的问题,平衡性能和安全
     * */
    private volatile static LazyDoubleCheckSingleton lazyDoubleSingleton = null;
    private LazyDoubleCheckSingleton(){

    }
    
    public static LazyDoubleCheckSingleton getDoubleSingletonInstance(){
        if (lazyDoubleSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if (lazyDoubleSingleton == null){
                    lazyDoubleSingleton = new LazyDoubleCheckSingleton();
                   
                }
            }
        }
        return lazyDoubleSingleton;
    }
}

(2)允许2、3两步重排序,但是该排序不能让第二个线程看到,方案就是基于类初始化的解决方案。基本原理为:jvm在类加载后,被使用之前,都是类的初始化阶段。在初始化阶段,jvm会获取一个锁,该锁可以同步多个线程对同一个类的初始化。也就是下面绿色的部分。

线程0 和线程1 ,假设线程0先进入进行初始化,那么线程1 对业务部分即橙色框部分的2和3 的重排序是不可见的,因为根据java

规范,非构造线程是无法看到构造线程即线程0(正在调用类的构造器进行初始化)的重排序过程。

根据java规范,类的静态初始化包括初始化该类的静态方法 和 静态方法内的静态变量。

类的初始化主要包括5种情形:首次发生时,一个类将被立即初始化;这里所说的类表示泛指,包括接口和类,比如一个普通的类A,导致其立刻被初始化的5种情形:

a、一个A类实例被创建;

b、A类中一个静态方法被调用;

c、A类中一个静态成员被赋值;

d、A类中一个静态成员被使用并且该成员不是常量。

e、A类如果是一个顶级类并且该类中有嵌套的断言语句。顶级类在java语言中有介绍,详细请查阅更多资料。

java设计模式之单例模式——懒汉式(饱汉式)及多线程debug_第17张图片

如上图,线程0获取初始化锁之后,进入初始化阶段,此时线程1无法看到该单例类的初始化阶段,即无法看到橙色框内重排序的过程。当线程0执行完毕,线程1再次初始化时,确定该初始化已经完成,代码如下:

package com.zxl.design.zxl.design.pattern.singleton;

/**
 * Created by Administrator on 2019/6/23.
 */
public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton( ){
        //注意该方法不能少,否则会从外面创建一个对象出来。
    }
    private static class InnerClass{
        //该方案重点就在于如下私有类的初始化锁,重点看谁先获取到该初始化锁,
        //一旦获取到该初始化锁,会立刻执行初始化,并且该初始化过程对于其他
        //线程是不可见的。
        private static StaticInnerClassSingleton innerClassSingleton=
                new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getStaticInnerClassInstance(){
        return InnerClass.innerClassSingleton;
    }

}

延迟加载初始化的方式降低开销,提升性能的方案

 如上介绍了两种对于基础的懒汉式单例设计模式的增强改进方案。

你可能感兴趣的:(java,计算机原理,设计模式)