package com.lizba.p1;
/**
-
-
实例对象
-
@Author: Liziba
-
@Date: 2021/6/12 22:42
*/
public class Instance {
public Instance() {
System.out.println(“init…”);
}
}
package com.lizba.p1;
/**
-
-
延迟初始化
-
@Author: Liziba
-
@Date: 2021/6/12 22:40
*/
public class UnsafeLazyInitialization {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) { // 1、线程A执行
instance = new Instance(); // 2、线程B执行
}
return instance;
}
}
在UnsafeLazyInitialization类中,假设线程A执行1的同时线程B执行2,此时线程A可能会看到Instance对象未完成初始化(后续会讲问题根源)。
同步处理解决方法:
package com.lizba.p1;
/**
-
-
@Author: Liziba
-
@Date: 2021/6/12 22:46
*/
public class SafeLazyInitialization {
private static Instance instance;
public synchronized static Instance getInstance() {
if (instance == null) { // 线程A执行
instance = new Instance(); // 线程B执行
}
return instance;
}
}
给getInstance()方法做了同步处理,synchronized会带来性能开销。在getInstance()调用不频繁的情况下,这种解决方案是可以接收的,但是如果getInstance()被频繁调用,程序的整体性能将会下降。(尤其是在早期JVM中,没有锁升级策略的时候)。
双重检查锁解决方法:
package com.lizba.p1;
/**
-
-
双重检查锁
-
@Author: Liziba
-
@Date: 2021/6/12 22:51
*/
public class DoubleCheckedLocking {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) { // 第一次检查
synchronized (DoubleCheckedLocking.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new Instance(); // 仍然存在问题的代码
}
}
}
return instance;
}
}
如上代码,如果第一次检查instance不为null,那么久不需要执行加锁和初始化工作,可以极大的减少synchronized带来的性能开销,但是双重检查锁也存在一个问题,就是判断instance == null这行代码可能会在Instance未正确初始化的时候成立,这个问题产生的原因是指令重拍,下面会详细讲述,也可以看我往期的文章哈!因此这是一个错误的不完美的解决方案。
2、问题的根源
2.1 分析 instance = new Instance();
instance = new Instance(); 这行代码在可以理解为三行伪代码(JVM中的指令):
-
memory = allocate(); // 分配对象的内存空间
-
ctorInstance(memory); // 初始化对象
-
instance = memory; // 设置instance指向刚分配的内存地址
上述代码2和3可能会被重排序(部分JIT编译器真实存在),重排序后如下所示:
-
memory = allocate(); // 分配对象的内存空间
-
instance = memory; // 设置instance指向刚分配的内存地址 (未初始化完成)
-
ctorInstance(memory); // 初始化对象
由于上述重排序,遵守Java程序执行时必须遵守的intra-thread semantics,重排序并未改变在单线程中程序执行结果,且如果该重排序能带来性能优化则是被Java语言规范《The Java Language Specification》允许的。
2.2 分析什么是intra-thread semantics
单线程内instance = new Instance(); 执行时序图:
线程执行时序图
多线程内instance = new Instance(); 可能存在的一种执行时序图:
多线程执行时序图
由于单线程内要遵守intra-thread semantics,从而保证线程A的执行结果不会被改变;但是在上图多线程执行中,线程B可能读到一个未正确完成初始化的Instance对象。
回到DoubleCheckedLocking这个示例代码中,线程B可能在第一次instance == null判断时为真,线程B接下来将访问instance引用指向的对象,但是此时这个对象并没有初始化完成。
多线程执行时序表:
| 时间 | 线程A | 线程B |
| — | — | — |
| t1 | A1:分配对象的内存空间 | |
| t2 | A3:设置instance指向内存空间 | |
| t3 | | B1:判断instance是否为null |
| t4 | | B2:由于instance不为null,线程B将访问instance引用的对象 |
| t5 | A2:初始化对象 | |
| t6 | A4:访问instance引用的对象 | |
2.3 分析问题关键点
有上述的时序图表和解释我们不难发现,出现的问题是对象instance实例化时指令重排序导致对象“逸出”了,因此我们有如下两种解决思路:
-
不允许2和3重排序
-
运行2和3重排序,但是不允许其他线程“看到”这个重排序
下面讲述具体实现方案。
3、基于volatile的解决方案
在DoubleCheckedLocking上做小修改即可(需要基于JDK1.5及以上)
package com.lizba.p1;
/**
-
-
双重检查锁正确示例,JDK1.5及以上
-
@Author: Liziba
-
@Date: 2021/6/12 22:51
*/
public class DoubleCheckedLocking {
// private static Instance instance;
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) { // 第一次检查
synchronized (DoubleCheckedLocking.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new Instance(); // instance为volatile,问题得以解决
}
}
}
return instance;
}
}
声明instance为volatile引用变量时,2和3的重排序会被禁止,执行时序图如下:
多线程执行时序图
该方案是通过禁止重排序来实现。
4、基于类初始化的解决方案
JVM在类的初始化阶段(即在Class被加载后,且被线程使用前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
基于这个特性实现的方案被称之为(Initialization On Demand Holder idiom)。
示例代码:
package com.lizba.p1;
/**
-
-
实例工厂
-
@Author: Liziba
-
@Date: 2021/6/12 23:52
*/
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance;
}
}
假设线程A和线程B同时执行getInstance()方法,下面是执行示意图:
这个方案实质上是运行重排序,但是不允许非构造线程B看到未实例化完成的对象,利用了JVM类初始化的特性。
初始化一个类包括执行这个类的静态初始化和初始化在这个类中声明的静态字段。
那么类什么时候会被初始化呢?在Java语言规范中,首次发生如下情况中的任意一种,一个类或者一个接口类型T将会被立即初始化:
-
T是一个类,而且一个T类型的实例被创建
-
T是一个类,且T中声明的一个静态方法被调用
-
T中声明的一个静态字段被赋值
-
T中声明的一个静态字段被使用,而且这个字段不是一个常量字段
-
T是一个顶级类(Top Level Class),而且一个断言语句嵌套在T内部被执行
在InstanceFactory示例代码中,符合情况4,InstanceHolder中静态字段instance被使用,导致触发InstanceHolder对象的初始化,从而初始化Instance对象。
在Java代码执行过程中,会存在多线程同时尝试去初始化一个类或者一个接口,因此在Java语言规范中,会要求具体的JVM实现对这个过程做同步处理。(实现规范是每个类或者接口有一个唯一的初始化锁LC与之对应。从C到LC的映射,由JVM去实现)。
5、Java初始化类或接口的具体过程
我们来看看《Java并发编程艺术》的作者是如何通过5个步骤阐述这个过程的。
5.1 第一阶段
通过在Class对象上同步(获取Class对象的初始化锁),来控制类或接口的初始化。这个获取锁的线程会一直等待,知道当前线程能够获取到这个Class对象的初始化锁。
假设线程A和线程B同时初始化一个未被初始化的Class对象(初始化状态state,此时被标记为state=noInitialization),图示如下:
类初始化-第一阶段
类初始化-第一阶段执行时序表:
| 时间 | 线程A | 线程B |
| — | — | — |
| t1 | A1:尝试获取Class对象的初始化锁。这里假设线程A获取到初始化锁。 | B1:尝试获取Class对象的初始化锁,由于线程A获取到了锁,线程B等待获取初始化锁 |
| t2 | A2:线程A看到对象还未被初始化(state=noInitialization),线程设置state=noInitializating | |
| t3 | A3:线程A释放初始化锁 | |
5.2 第二阶段
线程A执行类的初始化,同时线程B在初始锁对应的condition上等待。
图示如下:
类初始化-第2阶段
类初始化-第二阶段执行时序表:
| 时间 | 线程A | 线程B |
| — | — | — |
| t1 | A1:执行类的静态初始化和初始化类中声明的静态字段 | B1:获取到初始化锁 |
| t2 | | B2:读取到state=initializing |
| t3 | | B3:释放初始化锁 |
| t4 | | B4:在初始化锁的condition中等待 |
5.3 第三阶段
最后
小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
如果你需要这些资料, ⬅ 专栏获取
n中等待 |
5.3 第三阶段
最后
小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
[外链图片转存中…(img-mSbMAXze-1719340963690)]
[外链图片转存中…(img-vtPEmPGl-1719340963690)]
[外链图片转存中…(img-rnOaEyd7-1719340963691)]
[外链图片转存中…(img-x07NK3uO-1719340963692)]
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
如果你需要这些资料, ⬅ 专栏获取