• Java设计模式——单例模式


      Java中有许多设计模式,总体分为3大类:创建型模式、结构型模式和行为型模式。

    分类 设计模式 关注点
    创建型模式 工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式 关注于对象的创建,同时隐藏创建逻辑
    结构性模式 适配器模式、过滤器模式、装饰模式、享元模式、代理模式、外观模式、组合模式、桥接模式 关注类和对象之间的组合
    行为型模式 责任链模式、命令模式、中介者模式、观察者模式、状态模式、策略模式、模板模式、空对象模式、备忘录模式、迭代器模式、解释器模式、访问者模式 关注对象之间的通信

      创建型模式最常见也最简单的就是单例模式。单例模式,顾名思义就是一个类只能有一个对象(实例)。

      单例模式总结有3个特点:

    • 单例类只能有一个对象实例。

    • 该类必须自己创建的唯一的实例。

    • 该类必须向系统中所有其他对象提供这个实例。

      单例模式的初代版本(懒汉模式):

    public class Singleton {
    	private Singleton() { // 构造方法私有化
    	}
     
    	private static Singleton instance = null; // 单例对象
     
    	// 静态工厂方法
    	public static Singleton getInstance() {
    		if (instance == null) {
    			instance = new Singleton();
    		}
    		return instance;
    	}
    }
    

      上面代码有以下3点解释:

    1. 一个类进行new操作时会默认执行其构造方法,要使其只能有一个对象自然不能随便new,故要将构造方法私有化。
    2. getInstance是获取单例对象的方法,其是静态工厂方法,返回值为单例对象。
    3. 上面方法中如果单例对象初值设为null,即没有创建,就创建单例对象并返回该值。

      以上版本的单例模式其实存在一个问题:不能保证其线程安全。

      为什么呢?现在有一这种情况:当Singleton类刚被初始化时,有两个线程同时访问getInstance方法,因为instance的值为空,故这两个线程都满足条件,最终的结果就是new语句被执行了两次。显然这不是我们需要的结果。

      那么如何改进呢?这里我们使用同步synchronized来解决。getInstance方法属于临界区,临界区的内容需要互斥访问,将故获取单例对象的方法加上synchronized,保证每次操作只有一个线程访问。

      单例模式线程安全版:

    public class Singleton {
    	private Singleton() { // 构造方法私有化
    	}
     
    	private static Singleton instance = null; // 单例对象
     
    	// 静态工厂方法
    	public static Singleton getInstance() {
    		if (instance == null) {// 第一次检测
    			synchronized (Singleton.class) {// 同步锁(锁住整个类)
    				if (instance == null) {// 第二次检测
    					instance = new Singleton();
    				}
    			}
    		}
    		return instance;
    	}
    }
    

      该版本解释以下几点:

    1. synchronized同步锁必须使用类锁,锁住整个类。
    2. 判断条件检测了两次,保证只会创建一个对象实例。

      当两个线程同时访问时,第一次检测都符合条件,故继续执行,但有同步锁,故线程1执行,执行完毕 instance 对象已经被创建并返回。这时候线程2进入临界区,先进行第二次检测,不为空故不进行new操作。最终结果保证了只有一个对象实例。

      到这里觉得单例模式应该没什么问题了吧。其实加了synchronized同步锁还不能保证绝对的安全。为什么呢?这里和JVM编译器的指令重排有关。

      在Java中代码 instance = new Singleton( ); 编译器进行编译成3句JVM指令:

    memory = allocate( );//1. 给对象分配内存空间
    ctorSingleton(memory);//2. 对象初始化
    instance = memory;//3. 将singleton对象指向为其分配的内存空间
    

      这3条指令的执行顺序不是确定的,也就是说会发生指令重排的现象。若现在发生指令重排,3条指令的执行顺序变成如下顺序:

    memory = allocate( );//1. 给对象分配内存空间
    instance = memory;//3. 将singleton对象指向为其分配的内存空间
    ctorSingleton(memory);//2. 对象初始化
    

      这时候当线程1执行完1和3,线程2又抢占到CPU资源,执行检测语句if(singleton == null)。但这时候得到的结果会是什么?false并返回一个没有初始化的对象。因为线程1虽执行了1和3指令,instance 的值已经不为null,但没有完成初始化。线程2过来判断时不满足为null的条件,所以是false。

      所以虽然保证是一个对象,但这个对象是残缺的。这显然不是我们要的结果。解决办法就是利用关键字volatile。

    简单说明一下,被volatile修饰的共享变量有两点重要的特性:

    1. 保证不同线程对这个变量操作的内存可见性。
    2. 禁止指令重排。

      所以这里的改进办法就是只需在对象singleton前加上它,就可防止指令重排,问题得到解决。

      单例模式线程安全改进版:

    public class Singleton {
    	private Singleton() { // 构造方法私有化
    	}
     
    	private volatile static Singleton instance = null; // 单例对象
     
    	// 静态工厂方法
    	public static Singleton getInstance() {
    		if (instance == null) {// 第一次检测
    			synchronized (Singleton.class) {// 同步锁(锁住整个类)
    				if (instance == null) {// 第二次检测
    					instance = new Singleton();
    				}
    			}
    		}
    		return instance;
    	}
    }
    

      被volatile修饰的变量在被编译成JVM的3条指令时始终保持顺序执行,即防止了指令重排的发生。

      现在当线程1先执行,对线程2来说,其会指向一个完成了初始化的对象 instance ,不会出现对象“残缺”的现象。

  • 相关阅读:
    导出excel
    JS一些记录
    Concat
    (二)《SQL进阶教程》学习记录--GROUP BY、PARTITION BY
    PostgreSQL 时间转换
    vlc+flv.js 摄像头 H5 直播
    echarts label formatter params backgroundColor rich 标签设置背景图并传参
    异步、多线程、Await/Async、Task
    “2+3”等于我的自白
    SignalR:React + ASP.NET Core Api
  • 原文地址:https://www.cnblogs.com/flyingrun/p/13578574.html
Copyright © 2020-2023  润新知