读书笔记:多线程下的懒加载问题

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

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

1、非安全的延迟初始化

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

2、安全的延迟初始化

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

3、双重检查锁定

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

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


4、创建实例的具体步骤

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

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

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

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

方案一:


6、使用volatile类型

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

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

7、利用类的初始化锁

为什么这样没有任何同步方法修饰的代码却可以得到一个正确的执行结果呢?
看图7,我们在初始化类InstanceHolder的时候会自动加一个锁,也就是一个时间点只能有一个线程对InstanceHolder类进行初始化,Java语言规范定义了以下的几种类或接口类型会立刻进行初始化的情形

  1. T是一个类,而且一个T类型的实例被创建
  • T是一个类,且T中声明的一个静态方法被调用
  • T中声明的一个静态字段被赋值
  • T中声明的一个静态字段被使用,而且这个字段不是一个常量字段
  • T是一个顶级类,而且一个断言语句嵌套在T内部被执行

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

推荐阅读更多精彩内容

  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 28,504评论 10 316
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 74,695评论 12 116
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    会飞的鱼69阅读 21,819评论 18 389
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 1,215评论 1 14
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 1,742评论 1 17