单例设计模式和Java内存模型

使用双检索的单例设计模式延迟加载存在的问题

这篇文章介绍了使用双检索延迟加载的单例模式存在的问题,以下的代码由于指令重排序可能会无法正常工作。

正常的执行顺序是

  1. 执行构造函数
  2. 构造函数执行完毕
  3. 将新构造的对象赋值到引用

但由于指令的乱序执行,代码的执行顺序可能变为

  1. 执行构造函数
  2. 将对象赋值到引用
  3. 构造函数执行完毕

由此,线程可能获取到一个没有初始化完毕的对象。

 1 class Foo {
 2   private Helper helper = null;
 3   public Helper getHelper() {
 4     if (helper == null)
 5       synchronized(this) {
 6         if (helper == null)
 7           helper = new Helper();
 8       }
 9     return helper;
10     }
11   // other functions and members...
12   }

然后给出了几种修复方案,这其实是一个安全发布的问题,能够解决问题的方案不外乎以下情况(链接的文章中没有出现第3种):

  1. 在静态初始化函数中初始化一个对象的引用。
  2. 将对象引用保存到volatile类型的域中或者AtomicReference对象中。
  3. 将对象引用保存到某个正确构造对象的final类型域中。
  4. 将对象的引用保存到一个由锁保护的域中。

对于第1种修复方案,因为静态初始化函数在类加载的初始化阶段执行,这部分的代码由JVM保证同步,因此是行之有效的。

1 class HelperSingleton {
2   static Helper singleton = new Helper();
3   }

我们先跳过第2种和第3种修复方案。 对于第4种修复方案,因为synchronized的代码段或者函数是同步的,具有原子性和可见性,因此也是能够工作的。

class Foo {
  private Helper helper = null;
  public synchronized Helper getHelper() {
    if (helper == null)
        helper = new Helper();
    return helper;
    }
  // other functions and members...
  }

我先给出第2种修复方案的代码,但不急着去分析,我们需要先了解一些其他的知识。因为仅仅根据之前的知识是无法解决问题的。

 1   class Foo {
 2         private volatile Helper helper = null;
 3         public Helper getHelper() {
 4             if (helper == null) {
 5                 synchronized(this) {
 6                     if (helper == null)
 7                         helper = new Helper();
 8                 }
 9             }
10             return helper;
11         }
12     }

之前的知识:

happens-before:

  • An unlock on a monitor happens-before every subsequent lock on that monitor.
  • A write to a volatile field (§8.3.1.4happens-before every subsequent read of that field.
  • A call to start() on a thread happens-before any actions in the started thread.
  • All actions in a thread happen-before any other thread successfully returns from a join() on that thread.
  • The default initialization of any object happens-before any other actions (other than default-writes) of a program.

对于一个volatile字段的写happens-before对一个volatile字段的读,也就是线程A的volatile写能够被线程B的volatile读所感知到。

但仅凭这点,对于乱序执行导致线程获取到一个没有初始化完毕的对象没有一点帮助。

我们还需要内存屏障相关的知识。

Java Memory Model Cookbook

内存屏障相关的知识可以该链接的文章中获取,也有一些人已经翻译过其中的内容发布到自己的博客上。

在这里,我们需要引用该文章中的两张表格和StoreStore内存屏障的知识。

Required barriers 2nd operation
1st operation Normal Load Normal Store Volatile Load
MonitorEnter
Volatile Store
MonitorExit
Normal Load LoadStore
Normal Store StoreStore
Volatile Load
MonitorEnter
LoadLoad LoadStore LoadLoad LoadStore
Volatile Store
MonitorExit
StoreLoad StoreStore
Java Instructions
class X {
  int a, b;
  volatile int v, u;
  void f() {
    int i, j;
   
    i = a;
    j = b;
    i = v;
   
    j = u;
   
    a = i;
    b = j;
   
    v = i;
   
    u = j;
   
    i = u;
   
   
    j = b;
    a = i;
  }
}

     

load a
load b
load v

LoadLoad
load u
   LoadStore
store a

store b

StoreStore
store v

StoreStore
store u

StoreLoad
load u

LoadLoad
   LoadStore
load b
store a

StoreStore Barriers

The sequence: Store1; StoreStore; Store2
ensures that Store1‘s data are visible to other processors (i.e., flushed to memory) before the data associated with Store2 and all subsequent store instructions. In general, StoreStore barriers are needed on processors that do not otherwise guarantee strict ordering of flushes from write buffers and/or caches to other processors or main memory.

观察第一张表,我们可以发现,对于volatile存储操作,不管上一条指令时什么操作,编译器会在volatile存储指令和上一条指令中间插入内存屏障指令。

对于第2种修复方案,当我们向volatile引用存储对象的时候,编译器会插入一条StoreStore屏障。对于 Store1; StoreStore; Store2 指令序列,(在这里Store2指令就是volatile存储引用操作),Store1存储的数据会先于Store2存储的数据和其后续的存储数据对其他处理器可见(也就是刷新到内存)。在其他线程第一次获取到对象引用的时候,必定能够获取到对象引用中的字段。

也就是volatile修饰的对象赋值时,能够保证之前对volatile对象字段的编辑都被写入到主内存中。

最后,对于第3种情况。将对象保存到正确构造的对象的final域中。为什么这样能够保证对象的安全发布?final字段的基本语义是不可变更的字段,它除此之外还有着一些其他的语义。在 Java Language Specification 第17章 第5小节有这样几句话,

An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object‘s final fields.

The usage model for final fields is a simple one: Set the final fields for an object in that object‘s constructor; and do not write a reference to the object being constructed in a place where another thread can see it before the object‘s constructor is finished. If this is followed, then when the object is seen by another thread, that thread will always see the correctly constructed version of that object‘s final fields. It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are.

对于final修饰的字段(也就是我们构造的对象),只有在持有该final字段的对象构造函数完成之后,持有该final字段的对象才可以被其他线程可见,这是为了保证其他线程能够访问final字段(也就是我们构造的对象)。更重要的是,其他线程也能够看到final字段赋值时的字段引用的数组或者对象。也就是能够看到构造函数中赋值的对象。 当然这个的前提是对象是安全发布的,也就是在构造函数调用的过程中没有暴露给其他线程。

原文地址:https://www.cnblogs.com/notUlnix/p/notUlnix.html

时间: 2024-10-01 04:52:19

单例设计模式和Java内存模型的相关文章

【设计模式】单例设计模式的N中Java实现方法

特点 单例模式的特点: 1.只能有一个实例: 2.必须自己创建自己的一个实例: 3.必须给所有其他对象提供这一实例. 饿汉式单例模式 也称为预先加载法,实现方式如下: [java] view plaincopy class Single { private Single()( Syustem.out.println("ok"); ) private static Single instance = new Single(); public static Single getInstan

Java——单例设计模式

设计模式:解决某一类问题最行之有效的方法.Java中23种设计模式:单例设计模式:解决一个类在内存中只存在一个对象. 想要保证对象唯一.1,为了避免其他程序过多建立该类对象.先禁止其他程序建立该类对象2,还为了让其他程序可以访问到该类对象,只好在本类中,自定义一个对象.3,为了方便其他程序对自定义对象的访问,可以对外提供一些访问方式 这三部怎么用代码体现呢?1,将构造函数私有化.2,在类中创建一个本类对象.3,提供一个方法可以获取到该类对象. 对于事物该怎么描述,还怎么描述.当需要将该事物的对象

java设计模式之单例设计模式

设计模式:解决某一类问题最行之有效的方法. java中23种设计模式. 单例设计模式:解决一类在内存中只存在一个对象. Runtime()方法就是单例设计模式进行设计的. 解决的问题:保证一个类在内存中的对象唯一性. 比如:多程序读取一个配置文件时,建议配置文件封装成对象.会方便操作其中数据,又要保证多个程序读到的是同一个配置文件对象,就需要该配置文件对象在内存中是唯一的. 1.为了避免其他程序过多建立该类对象,先禁止其他程序建立该类对象. 2.还为了让其他程序可以访问该类对象,只好在本类中自定

黑马程序员--Java基础学习笔记【单例设计模式、网络编程、反射】

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- 设计模式分类: 创建模式:是对类的实例化过程的抽象化,又分为类创建模式和对象创建模式 类创建模式:工厂方法模式 对象-:简单工厂(静态工厂方法)模式.抽象工厂模式.单例模式.建造模式- 结构模式:描述如何将类或者对象结合在一起形成更大的结构 适配器模式.缺省模式.合成模式.装饰模式(包装模式).门面模式- 行为模式:对不同的对象之间划分责任和算法的抽象化 不变模式.策略模式.迭代子模式.命令模

java设计模式_single(单例设计模式)

设计模式:解决某一类问题最行之有效的方法,java中有23种设计模式 一.单例设计模式概述: 1.解决一个类在内存中只有一个对象(保证一个类仅有一个实例,并提供一个访问他的全局访问点)  2.要保证对象的唯一: 1.为了避免其他程序过多的建立该类对象,先禁制其他程序建立该类对象 2.为了让其他程序可以访问到该类对象,只好在本类中,自定义一个对象 3.为了 方便其他程序对自定义对象的访问,可以对外提供一些访问方式 3.代码实现步骤: 1.将构造函数私有化 2.在类中创建一个本类对象 3.给外部提供

Java: 单例设计模式

设计模式: * 设计模式:解决某一类问题最行之有效的方法:* Java有23中设计模式* 单例设计模式:解决一个类在内存只存在一个对象:* * 想要保证对象唯一* 1.为了避免其他程序过多建立该类对象.先控制禁止其他程序建立该类对象* 2.还为了让其他程序可以访问到该类对象,只好在本类中自定义一个对象* 3.为了方便其他程序对自定义对象的访问,可以对外提供一些访问方式* * 这三部如何用代码体现呢?* 1. 将构造函数私有化* 2. 在类中建立一个本类对象* 3. 提供一个方法可以获取到该对象

JAVA学习第十课(单例设计模式)

杂谈: 如果一个类里的方法都是静态的,那么就没有必要创建对象,为了不让其他程序创建对象,就可以把当前类的构造函数私有化. class MAN { private MAN() { } } 文档注释:命令:javadoc 只能解析/** 开始至 */结束的内容:/* */则不行 路径设置问题: 当要运行,两个以上路径中的class文件时,路径都要设置,命令:classpath=.;c:\myhelp或者:classpath=.;%myhelp% 清除路径: set classpath= 设计模式:对

java笔记之静态修饰附和单例设计模式

 第六天笔记 静态修饰符static: 一.static修饰成员变量: static用来修饰成员变量叫静态成员变量,没有static修饰的成员变量叫非静态成员变量 静态成员的访问方式: (1)   用对象进行访问:对象名.变量名 (2)   通过类名来访问:类名.变量名; 使用注意点: (1)   只有需要数据共享才需要用static修饰 //公有属性        String name;        //这个变量就变成了一个共享数据 ,单独存放在一个地方        static Str

Java设计模式之一 单例设计模式

1.什么叫设计模式: 设计模式的概念首先来源于其它行业:建筑业,在早起建房子的时候,肯定是经验缺乏.显得杂乱无序的,这就会造成很多问题,在行业发展过程,通过不断的经验积累,前辈们针对这些问题提出了合理解决方案,这就是设计模式,参照设计模式往往可以解决很多问题,在计算机编程方面,也会出现类似问题,所以牛人们把这些解决问题的方案进行归类和总结,形成了面向对象编程的23种设计模式. 2.单例模式(特点): Java中单例模式定义:"一个类有且仅有一个实例,并且自行实例化向整个系统提供."通过