目录
- 背景
- 前置知识
- 类加载运行全过程
- 单例模式的实现方式
- 一、饿汉式
- 基本介绍
- 源码
- 分析
- 二、懒汉式
- 基本介绍
- 源码
- 分析
- 改进
- 三、懒汉式单例终极解决方案(静态内部类)(推荐使用方案)
- 基本介绍
- 源码
- 分析
- 感谢
背景
最近学习了JVM之后,总感觉知识掌握不够深,所以想通过分析经典的【懒汉式单例】来加深一下理解。(主要是【静态内部类】实现单例的方式)。
如果小白想理解单例的话,也能看我这篇文章。我也通过了【前置知识】跟【普通懒汉式】、【双检锁懒汉】、【静态内部类】懒汉给大家分析了一下他们的线程安全性。但是,我这边没有完整的演进【懒汉式单例】历程。所以,会缺少思维上的递进。不过,我在最后的【感谢】名单里,提供了一个完整的【懒汉式单例演进】的链接,建议可以结合这个文章一起学习。
前置知识
类加载运行全过程
当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM。
package com.tuling.jvm;
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute() { //一个方法对应一块栈帧内存区域
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
通过Java命令执行代码的大体流程如下:
其中loadClass的类加载过程有如下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
- 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 验证:校验字节码文件的正确性
- 准备:给类的静态变量分配内存,并赋予默认值
- 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下节课会讲到动态链接
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块
总结一下,上面说的加载 >> 验证 >> 准备 >> 解析 >> 初始化过程是由JVM帮我们进行的,所以,对我们程序员来说,【天生】就具备线程安全性(这个由JVM帮我们保证,无需我们关心)。
单例模式的实现方式
单例模式,是我们Java中很常见的一个设计模式。所以有这么一种说法:遇事不决,单例解决。
Java单例通常有2种,分别为:饿汉式、懒汉式
一、饿汉式
基本介绍
饿汉式(Eager Initialization,急切的初始化),在类加载时就创建单例实例,并在需要时直接返回该实例。这种方式的实现是线程安全的,因为在类加载过程中实例已经创建好了。
源码
public class SingletonTest {
private static final SingletonTest me = new SingletonTest();
public static SingletonTest me() {
return me;
}
public static void main(String[] args) {
System.out.println(SingletonTest.me());
System.out.println(SingletonTest.me());
System.out.println(SingletonTest.me());
}
// 系统输出如下:
// org.tuling.juc.singleton.SingletonTest@12a3a380
// org.tuling.juc.singleton.SingletonTest@12a3a380
// org.tuling.juc.singleton.SingletonTest@12a3a380
}
分析
因为单例对象SingletonTest
是静态成员变量,所以,在JVM类加载过程中==(加载-》验证-》准备-》解析-》初始化)==的【解析】阶段已经被JVM初始化了,所以,由JVM保证了线程安全性。
二、懒汉式
基本介绍
懒汉式(Lazy Initialization),在首次调用时创建单例实例,存在线程安全问题。如果多个线程同时进入判断条件,可能会创建多个实例。
源码
public class SingletonTest {
private static SingletonTest me;
public static SingletonTest me() {
if(me == null) {
me = new SingletonTest();
}
return me;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(SingletonTest.me());
}).start();
}
}
}
输出结果如下
分析
为什么上面这段代码不是线程安全的呢?我们举一个极端的例子,如下图所示:
在没有锁机制的存在情况下,多线程环境里面可能会出现上述的并发执行情况。在线程1
判断完me == null
之后,即将开始执行new
之前,线程2
也刚好在判断me == null
,这是因为线程1
还没有执行new
操作,所以线程2
判断肯定是null
的,于是也开始new
。这就是线程安全问题所在。
(PS:小白们一定要理解上面这个图。虽然很简单,但是说它是你们迈向,或者培养【并发意识】的启蒙都不为过。)
改进
为了解决上面的问题,大牛们进行了改进,使用了【双检锁+volatile】机制,【双检锁】,即:双重检查锁。代码如下:
public class SingletonTest {
private static volatile SingletonTest me;
public static SingletonTest me() {
if(me == null) {
synchronized (SingletonTest.class) {
if (me == null) {
me = new SingletonTest();
}
}
}
return me;
}
}
上面的改进,关键点如下:
- 使用了
volatile
关键字修饰单例对象me
- 在获取单例对象的时候,判断了两次
if(me == null)
- 第二次判断
if(me == null)
之前,先加了锁
第二、三点我就不说了,大家可以看看最下面【感谢】的友链。这里重点说说第一点。
估计小白会很难理解,为什么一定要volatile
关键字修饰,不用可以吗?答案是:不可以。因为,volatile
能禁止重排序。什么是【重排序呢】?说的简单点,就是JVM,甚至是CPU为了性能,可能会在不改变语义的情况下修改我们的代码执行顺序。比如,当我们new SingletonTest()
的时候,你以为只有一步操作,实际上,它有3步,如下:
memory = allocate(); // 1.分配对象内存空间
instance(memory); // 2.初始化对象
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance!=null
但事实上,经过重排序之后可能会变成下面的执行顺序:
memory = allocate(); // 1.分配对象内存空间
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance!=null
instance(memory); // 2.初始化对象
然后大家再用上面的【并发启蒙】意识,自己画个图看下,还能线程安全吗?
所以,需要使用volatile
关键字,告诉底层JVM或者CPU,不要帮我重排序这个对象!于是就避免了上面的并发线程安全问题了。
三、懒汉式单例终极解决方案(静态内部类)(推荐使用方案)
基本介绍
这里通过利用JVM类加载【天生线程安全】的特性,来帮助实现【懒汉式】的单例。如何做到呢?答案是【静态内部类】。
源码
public class SingletonTest {
/** 单例对象,可以直接调用配置属性 */
private static class Holder {
private static SingletonTest me = new SingletonTest();
}
public static SingletonTest me() {
return Holder.me;
}
public static void main(String[] args) {
int threadCount = 10000;
for (int i = 0; i < threadCount; i++) {
new Thread(()->{
System.out.println(SingletonTest.me());
}).start();
}
}
}
上面的代码,新建了1W个线程来调用单例,我们发现,结果都是一样,同一个对象。
分析
为什么上面,通过静态内部类能保证线程安全性呢?这个我们在【前置知识】已经说过了,是由JVM保证了线程安全性。
如上图所示,只有当我们使用了SingletonTest.me()
的时候,才会去开始加载Holder
静态内部类,这就是它实现【懒汉式】的原因(延迟加载)。
感谢
感谢【作者:weixin_47196090】的深度好文,《懒汉式单例演进到DCL懒汉式 深度全面解析》