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调试方法了。
如上,在多线程的run方法中,先打断点(具体位置根据业务需求确定,这里我们在创建对象时打断点),然后右键,可弹出如上图所示的设置页面,具体含义为:
1、默认会选择enabled 和suspend 表示是否可用和挂起的方式(all 和 thtead)
2、挂起方式(suspend)默认会选择All,而我们需要选择挂起方式为Thread方式,这样就可以通过打断点的方式,阻塞到这里,等待我们手动处理,实现手动控制几个线程运行的顺序和节奏,模拟不同的多线程运行可能性的目的。而all 只会debug到本线程的断点,所以此处一定要选择thread 。
3、当选择thread时,后面会产生一个make default按钮,选中(变暗)情况下,表示以后默认情况下,都采用thread方式。
注:了解了如上内容,在此,我们在T 类(创建对象) 、单例模式的创建(判断是否为空时)、测试类(打印最后一句时)这几个位置都打上断点,并设置如上debug方式。然后开始debug 调试运行。
如上图,左侧大圈框住部分frame,点击可以看到三个线程 main、t1、t2,都已经运行,点击倒三角可以下拉,在此处可以切换线程,右侧values查看具体某个线程的信息,同时当左侧选中某个线程时,下一步可以单独改变该线程的进度,在不断切换线程,改变其进度的条件下,就能认为创建我们想要模拟的状况。
如下左下角,可以看到我们在选择到theread0 线程上,改变它的运行进度
此处具体可以根据上方担心的安全问题,通过控制多线程的方式,调节到描述的情形,验证是否存在我们所担心的问题
为了更好的帮助自己看这篇,此处附上几张模拟如上情形时的几张关键图片,尽可能配以注释。
第一,直接debug运行Test.main 到下图处,注意红色笔圈住部分
第二、切换到Thread0,F6单步运行,让其阻塞到即将创建单例对象的位置,右侧可看到 lazySingleton=null 的位置,如下图
第三,再切换到Thread1 上,同样单步执行到此状态。如下图
第四、再次切换到Thread0,单步运行,执行到如下情形,注意右侧this,即对象的值465,创建的lazySingleton值为479
第五、再次切换线程到Thread1,单步运行一次,执行到如下情形 ,再次注意右侧的值480,此时可看到两个值不一样
第六、再次切换线程到thread0 ,继续单步运行,查看lazySingleton的值
此刻,我们可以看到之前thread0 创建的值还是479,而现在返回的值是480,暂时先放一下,我们全部执行完毕看下结果,回头再分析为什么此处会有这样的变化。
最后的执行结果就是两个线程返回了同一个对象。
分析,首先要了解一个点:
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
第二,切换线程到thread0,执行到即将创建对象的那一行。
第三、将线程切换到thread1,单步运行,查看状态,注意这里同之前的代码运行时的变化,此处,我们无法将其运行到类似第一次的情形,因为这里,thread1,状态不是running,而是monitor,即监控,具体看文字,表示当前线程阻塞。
因为加了同步锁安全防范机制,所以此处无法进入,所以全部执行,会看到效果是有用的,达到了实际创建一个对象的目的。
但此方案有什么特点呢?
优点就是通过加同步锁,避免了安全性问题,但因为对整个类加锁,对性能影响较大。如果对性能要求不高,可采用此方案,否则该方案不可行。那还有其他更好的方案吗? 有的,请看后续更改方案。
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;
}
}
如上,展示了该方案的做法,其中嵌入了可能存在的问题的描述,下面详细分析。摘取图片如下:
上图解释了上方所写的初级的使用双重检查锁机制可能存在的问题。图上左右为两个线程,各个线程从上往下执行,而多线程中,两个线程执行先后顺序的差异,尤其是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语言中有介绍,详细请查阅更多资料。
如上图,线程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;
}
}
延迟加载初始化的方式降低开销,提升性能的方案
如上介绍了两种对于基础的懒汉式单例设计模式的增强改进方案。