当前位置: 主页 > JAVA语言

java单例模式-java单例模式线程安全

发布时间:2023-02-09 10:48   浏览次数:次   作者:佚名

GoF的23种设计模式中,单例模式是比较简单的一种。 但是,有时候越简单的事情越容易出问题。 下面详细讨论单例设计模式。

所谓单例模式,简单来说就是保证一个类的实例在整个应用中只存在一个。 就像Java Web中的应用一样,它提供了一个全局变量,它的用途非常广泛,比如保存全局数据,实现全局操作等。

1.最简单的实现

首先,能想到的最简单的实现方式就是把类的构造函数写成private,这样可以保证其他类无法实例化这个类,然后在类中提供一个静态实例返回给用户. 这样用户就可以通过这个引用来使用这个类的实例了。

公共类 SingletonClass {

private static final SingletonClass instance = new SingletonClass();

公共静态 SingletonClass getInstance() {

返回实例;

}

私人单例类(){

}

}

如上例,如果外部用户需要使用SingletonClass的实例,只能使用getInstance()方法,其构造方法是private的,保证了只有一个对象存在。

2.性能优化——懒加载

上面的代码虽然简单,但是有一个问题——无论是否使用这个类,都会创建一个实例对象。 如果这个创建过程比较耗时,比如需要连接数据库10000次(夸张的。。。:-)),而且不一定会用到这个类,那么这个创建过程就白搭了。 怎么做?

为了解决这个问题,我们想出了一个新的解决方案:

公共类 SingletonClass {

私有静态 SingletonClass 实例 = null;

公共静态 SingletonClass getInstance() {

如果(实例==空){

instance = new SingletonClass();

}

返回实例;

}

私人单例类(){

java单例模式的作用_java单例模式_java单例模式线程安全

}

}

代码有两处改动——首先将实例初始化为null,直到第一次使用时通过判断是否为null来创建对象。 因为创建过程不在声明处,所以最后修改必须去掉。

让我们想象一下这个过程。 要使用 SingletonClass,请调用 getInstance() 方法。 第一次发现实例为null时,新建一个对象并返回; 第二次使用时,因为实例是静态的,不再为null,所以不会再创建对象,直接返回。

这个过程就变成了lazy loaded,即惰性加载——直到用到才加载。

3.同步

上面的代码简单明了。 然而,正如那句名言:“80%的错误是由20%的代码优化造成的”。 在单线程下,这段代码没有问题,但是如果是多线程,麻烦就来了。 我们来分析一下:

线程 A 要使用 SingletonClass 并调用 getInstance() 方法。 因为是第一次调用,A发现实例为null,于是开始创建实例。 这时CPU有时间片切换,线程B开始执行。 需要用到SingletonClass,调用getInstance()方法,还要检测Instance是否为null——注意这个是在检测到A之后切换的,也就是说A还没有来得及创建对象——所以B开始创造。 B创建完成后,切换到A继续执行,因为已经检测过了,所以A不会再检测,直接创建对象。 这样线程A和B各有一个SingletonClass对象——单例失败!

解决方法也很简单,就是加锁:

公共类 SingletonClass {

私有静态 SingletonClass 实例 = null;

公共同步静态 SingletonClass getInstance() {

如果(实例==空){

instance = new SingletonClass();

}

返回实例;

}

私人单例类(){

}

}

需要给getInstance()加同步锁。 一个线程必须等待另一个线程创建后才能使用该方法,保证了单例的唯一性。

4.再次表现

上面的代码非常清晰简单,然而简单的东西往往并不理想。 毫无疑问这段代码存在性能问题——synchronized修饰的synchronized块比一般代码段慢了好几倍! 如果调用getInstance()的次数很多,那就得考虑性能问题了!

java单例模式_java单例模式线程安全_java单例模式的作用

我们来分析一下,是整个方法都要加锁java单例模式,还是只加其中一个语句就够了? 为什么我们需要锁定它? 分析造成懒加载情况的原因。 原因是检测null的操作与创建对象的操作是分开的。 如果这两个操作都可以原子执行,那么单例就已经有保障了。 于是,我们开始修改代码:

公共类 SingletonClass {

私有静态 SingletonClass 实例 = null;

公共静态 SingletonClass getInstance() {

同步(SingletonClass.class){

如果(实例==空){

instance = new SingletonClass();

}

}

返回实例;

}

私人单例类(){

}

}

先去掉getInstance()的同步操作,然后在if语句上加载同步锁。 但是这样修改没有效果:因为每次调用getInstance()都要同步,性能问题依然存在。 如果……如果我们提前判断是否为nulljava单例模式,然后同步呢?

公共类 SingletonClass {

私有静态 SingletonClass 实例 = null;

公共静态 SingletonClass getInstance() {

如果(实例== null){

同步(SingletonClass.class){

如果(实例== null){

instance = new SingletonClass();

}

java单例模式_java单例模式线程安全_java单例模式的作用

}

}

返回实例;

}

私人单例类(){

}

}

任何问题? 首先判断实例是否为null,如果为null则用lock初始化; 如果不为空,则直接返回实例。

这就是实现单例模式的双重检查锁设计。 到目前为止,一切都很完美。 我们以一种非常聪明的方式实现了单例模式。

5. 从源头上检查

先从编译原理说起。 编译是将源代码“翻译”成目标代码(主要是机器代码)的过程。 对于Java来说,它的目标代码不是本机代码,而是虚拟机代码。 编译原理中很重要的一部分是编译器优化。 所谓编译器优化,是指在不改变原有语义的情况下,通过调整语句的顺序,使程序运行得更快。 此过程称为重新排序。

要知道,JVM只是一个标准,而不是一个实现。 JVM 中没有提供编译器优化,也就是说,JVM 实现可以自由执行编译器优化。

我们想一想,创建一个变量有哪些步骤? 一种是申请一块内存,调用构造函数进行初始化操作,另一种是分配指向这块内存的指针。 这两个操作哪个在前面,谁在后面? JVM 规范没有规定这一点。 那么就有这样一种情况,JVM先开辟一块内存,然后把指针指向这块内存,最后调用构造函数进行初始化。

现在我们考虑这样一种情况:线程A开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断实例是否为null。 根据我们上面提到的内存模型,A已经将实例指向了那块内存,但是还没有调用构造函数,所以B检测到实例不为null,所以直接返回了实例——问题来了,虽然instance不是null,而是没有建造,就像房子给了你钥匙,但是你住不进去,因为里面还没有收拾。 这时候如果B在A完成实例构造之前就使用了这个实例,程序就会出错!

因此,我们想出了以下代码:

公共类 SingletonClass {

私有静态 SingletonClass 实例 = null;

公共静态 SingletonClass getInstance() {

如果(实例== null){

单例类 sc;

同步(SingletonClass.class){

sc =实例;

如果(sc == null){

java单例模式_java单例模式线程安全_java单例模式的作用

同步(SingletonClass.class){

如果(sc == null){

sc = new SingletonClass();

}

}

实例= sc;

}

}

}

返回实例;

}

私人单例类(){

}

}

我们在第一个同步块中创建一个临时变量,然后使用这个临时变量创建对象,最后将实例指针指向临时变量的内存空间。 写这种代码是基于以下思路,即synchronized会起到代码屏蔽的作用,同步块内部的代码与外部代码没有任何联系。 所以外部同步块中临时变量sc的操作不会影响到实例,所以当外部类检测到instance=sc;之前的实例时,结果实例还是null。

然而,这种想法是完全错误的! synchronized块的释放保证了在此之前——也就是synchronized块内部的操作一定要完成,但不保证synchronized块之后的操作不能切换到synchronized块结束之前,因为编译器优化。 因此,编译器完全可以把instance=sc;这句话搬过来。 到内部同步块执行。 这样,程序又出错了!

6.解决方案

说了这么多,难道单例在Java中就没有办法实现了吗? 其实并不是!

在 JDK 5 之后,Java 使用了一种新的内存模型。 volatile关键字语义清晰——在JDK1.5之前,volatile是一个关键字,但其用途没有明确定义——write被volatile修饰的变量不能用之前的读写代码调整,读变量不能用read和write调整写代码调整! 因此,我们只要在实例中简单地加上volatile关键字即可。

公共类 SingletonClass {

private volatile static SingletonClass instance = null;

公共静态 SingletonClass getInstance() {

如果(实例== null){

java单例模式的作用_java单例模式_java单例模式线程安全

同步(SingletonClass.class){

如果(实例==空){

instance = new SingletonClass();

}

}

}

返回实例;

}

私人单例类(){

}

}

不过这只是JDK1.5以后的Java的解决方案,那么之前的版本呢? 其实还有一个解决方案是不会受Java版本影响的:

公共类 SingletonClass {

私有静态类 SingletonClassInstance {

private static final SingletonClass instance = new SingletonClass();

}

公共静态 SingletonClass getInstance() {

返回 SingletonClassInstance.instance;

}

私人单例类(){

}

}

在这个版本的单例模式实现代码中,我们使用了Java的静态内部类。 该技术由 JVM 明确指定,因此没有歧义。 在这段代码中,因为SingletonClass没有静态属性,所以不会被初始化。 在调用 getInstance() 之前,将首先加载 SingletonClassInstance 类。 这个类有一个静态的SingletonClass实例,所以需要调用SingletonClass的构造函数,然后getInstance()将这个内部类的实例返回给用户。 由于这个实例是静态的,所以不会多次构造。

由于 SingletonClassInstance 是一个私有的静态内部类,它不会被其他类知道。 同样,静态语义也要求不会有多个实例。 而且JSL规范中定义类的构造必须是原子的、非并发的,所以不需要加同步块。 此外,由于此构造是并发的,因此 getInstance() 不需要同步。

至此,当我们对Java语言中的单例模式有了完整的了解后,我们提出了两种解决方案。 我个人比较喜欢第二种方式,这种方式也是Effective Java推荐的。