java place inner holder 懒加载_读书笔记:多线程下的懒加载问题

本文探讨了Java多线程中延迟初始化的挑战,特别是通过单例模式实现延迟初始化时可能遇到的问题。分析了非安全的延迟初始化、安全的延迟初始化以及双重检查锁定(DCL)模式,并揭示了DCL潜在的JIT编译器优化导致的隐患。提出了解决方案,包括使用volatile关键字和利用类初始化的隐式锁,确保线程安全的延迟初始化。
摘要由CSDN通过智能技术生成

最近看完了《Java并发编程的艺术》一书,差不多看明白了,做了很多的笔记,也敲完了书本上的所有代码。但是看完书本再合起来一想,就老是觉得什么都没学到,我觉得这是因为我只是在看书的当时理解了书上的内容,但是离把书本上的知识转化为我的知识储备这一过程还是没有做到,所以把自己的一些笔记整理成博文,本人的水准有限,还是希望被指出错误。

在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销,让我们来看下面一段代码:

7412f5d7c85f?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

1、非安全的延迟初始化

这是一个简单的单例模式的代码,它的功能是实现UnsafeLazyInitialization类中Instance对象的延迟初始化,可以看到,如果有2个线程A和B同时调用UnsafeLazyInitialization类中的getInstance方法,假如A执行到了步骤3,而B还在执行步骤2,那么,A线程执行getInstance的返回值将是B创建的实例对象,显然在一个多线程的程序中发生这样的事情不是我们想要的,那么如何使得A执行getInstance的结果是A创建的实例对象呢?我们首先想到的就是给getInstance方法加锁,如下所示:

7412f5d7c85f?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

2、安全的延迟初始化

貌似问题到这里就轻松的解决了,但是慢着,我们来看一下这段代码有什么问题。这段代码本质上是对于任何线程对Instance类的实例都采取加锁访问的方式,假如这个这个类的实例被访问地非常频繁,那么这种频繁加锁和释放锁方式就会产生严重的效率问题,既然如此,我们不如对这段代码进行一下优化,代码如下所示:

7412f5d7c85f?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

3、双重检查锁定

正如上图标识的,这种方法被称为“双重检查锁定”,让我们来分析一下这个代码,在Instance对象为空的情况下,假设两个线程A和B同时执行getInstance方法,二者都执行到步骤1时,假如A获取了锁,那么B就会在获取锁的入口等待,在A创建完实例对象之后,A走出了同步块,并返回A刚刚创建的实例对象,此时B再进入同步块,发现实例已经被创建了,那么B同样会走出同步快,返回的是A创建的实例对象,后续的线程在步骤1时发现实例已经被创建,那么都会返回线程A创建的实例,而且都不需要进行同步了。多么愉快的而巧妙的解决方案啊!!!

但是,这样的解决方案存在一个隐蔽的问题,那就是JIT编译器的优化可能会使这个方法执行的过程会发生一些问题,那就是在A进入同步块创建实例的时候,线程B会返回一个没有初始化的Instance对象。为什么会发生这样的情况呢?这是由于JIT编译器会在编译时会发生“重排序”的状况,让我们看一下上面的步骤4,步骤4可以分解为下面几个具体的步骤,如下:

7412f5d7c85f?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

4、创建实例的具体步骤

上图是编译器在创建一个类对象时的步骤分解,首先为一个对象分配一个空间,然后初始化这个空间,最后把这个初始化后的空间赋值给一个引用即instance,但是编译器偏偏不这么干,它可能把2和3给颠倒过来,如下所示:

7412f5d7c85f?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

5、编译器的一个分解顺序

Java语言规范规定编译器的优化不会改变单线程的执行结果,但是并没有对多线程做出这样的保证。好了让我们来看一看“双重检查锁定”会发生一种情况,假如编译器是按照图5进行优化的,那么一种执行的情形就是这样的(看图3):假设有两个线程A和B,假如A执行完4之后退出同步块,而B刚刚执行到步骤1,根据编译器的优化,instance指向了刚分配的地址,但是还没被初始化,A会在返回之前会等到初始化完成(Java语言的intra-thread semantics),但是B此时发现instance不为null,于是直接返回instance引用,假设这个初始化持续的时间有一点长,而线程B又马上会使用instance的内容,那么程序的运行就会发生不可预料的错误。

针对这个问题,解决方案有以下两种

方案一:

7412f5d7c85f?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

6、使用volatile类型

如上图,我们把instance对象变成了volatile类型,java编译器会阻止上图5的编译器重排序操作(这种方案只对JDK5或以上的版本有效,因为JDK5增强了volatile类型的内存语义,内存语义以后再开篇说明),所以我们会得到正确的执行结果

方案二:

方案二是利用类初始化时JVM会获取一个锁,相当于一个加在类上隐形的锁,代码如下:

7412f5d7c85f?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

7、利用类的初始化锁

为什么这样没有任何同步方法修饰的代码却可以得到一个正确的执行结果呢?

看图7,我们在初始化类InstanceHolder的时候会自动加一个锁,也就是一个时间点只能有一个线程对InstanceHolder类进行初始化,Java语言规范定义了以下的几种类或接口类型会立刻进行初始化的情形

T是一个类,而且一个T类型的实例被创建

T是一个类,且T中声明的一个静态方法被调用

T中声明的一个静态字段被赋值

T中声明的一个静态字段被使用,而且这个字段不是一个常量字段

T是一个顶级类,而且一个断言语句嵌套在T内部被执行

图7中的代码属于上述的第3和第4中情况,因此在访问InstanceHolder类的内部的instance对象时会自动地给获取该对象的线程加锁。

写的不好,还望海涵

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值