从单例模式的实现到jvm的类加载机制

先上答案,懒加载的最佳实践

public class Singleton {
    private static class SingletonHolder {
        private static Singleton singleton = new Singleton();
    }
    
    private Singleton() {
    }

    public static Singleton getSingleton() {
        return SingletonHolder.singleton;
    }
}
// 防new,防反射,防序列化,并且也是懒加载
public enum MySingleton3 implements MySingleton {
    SINGLETON,
    ;
    
    public static MySingleton3 getInstance(){
        return SINGLETON;
    }
}

最佳实践的起源

在没有内部类的情况下,类的初始化顺序为:

类的静态成员变量【类所有】-->静态代码块-->成员变量【对象所有】-->代码块-->构造函数实例化【对象所有】

依据单例的性质,只持有一个实例,外部无法使用构造函数进行初始化,很容易想到以下实现方式1。

public class Singleton1 {
    private static Singleton1 singleton;
    private Singleton1() {
    }

    public static Singleton1 getSingleton() {
        if (null == singleton) {
            singleton = new Singleton1();
        }
        return singleton;
    }

}

实现方式1,在单线程场景下使用没有问题,但是在并发场景下:多个线程同时调用getSingleton( ),并判断 null == singleton ,依然会造成多次实例化,带来并发场景下的线程安全问题,很容易想到加锁的方式来保证线程安全。

单例模式实现方式2

public class Singleton1 {
    private static Singleton1 singleton;
    private Singleton1() {
    }

    public synchronized static Singleton1 getSingleton() {
        if (null == singleton) {
            singleton = new Singleton1();
        }
        return singleton;
    }

}

实现方式2 的这种加锁方式,粒度太大【类锁,其实是xxx.class作为锁,在同步方法时加锁】,并且当getSingleton( )方法高频,高并发场景下多线程同时等待获取锁进入方法,在该处由并行变成串行,性能下降。

由于实现方式2中的加锁粒度过大,导致性能下降,可以减小锁的粒度,于是有了double check lock的方式

单例模式实现方式3

public class Singleton1 {
    private static Singleton1 singleton;
    private Singleton1() {
    }

    public static Singleton1 getSingleton() {
        if (null == singleton) { // 1
            synchronized (Singleton1.class) { // 2
                if (null == singleton) { // 3
                    singleton = new Singleton1();  // 4 实例化
                }
            }
        }
        return singleton;
    }

}

实现方式3看起来完美,但是忽略了一个问题:就是JVM 通过 new 实例化对象的时候【上述代码中步骤4】,可能会进行指令重排。比如:

其实是有三个步骤的:

memory = allocate();        // 指令1:分配对象的内存空间
ctorInstance(memory);       // 指令2:初始化对象
instance = memory;        // 指令3:设置instance指向刚分配的内存地址

JVM 为了性能方面的考虑,可能会进行指令重排,比如,指令3 会在 指令2 之前执行,导致了实现方式3 在步骤 3 进行 if (null == singleton)的判断时,singleton 并没有初始化完成而直接返回了一个空对象,引发问题。

单例懒加载实现方案4:

public class Singleton1 {
    private static volatile Singleton1 singleton;
    private Singleton1() {
    }

    public static Singleton1 getSingleton() {
        if (null == singleton) {
            synchronized (Singleton1.class) {
                if (null == singleton) {
                    singleton = new Singleton1();
                }
            }
        }
        return singleton;
    }
}

volatile关键字,无法保证线程安全【即无法保证操作的原子性】,但是可以防止指令重排【保证操作有序性】,并且能够保证volatile关键字修饰的共享变量的可见性【即,线程A操作完之后会将操作后的值刷回主存,另一个线程B识别到该共享变量,不会从线程B的CPU高速缓存中读取,而从主存中直接读取】,加上synchronized关键字,可以保证线程安全性。下图是volatile 和 synchronized 关键字的语义。

volatile 和 synchronized 关键字的语义

需要详细理解volatile关键字,请参考 https://blog.csdn.net/justloveyou_/article/details/53672005

单例懒加载实现5:

类的加载机制保证: 引用<深入理解JVM>

1、遇到 new、getstatic、setstatic 或 invokestatic 这 4 个字节码指令时,分别对应如下Java代码场景:

new : new一个实例化对象

getstatic 读取一个静态字段(final修饰、已在编译期把结果放入常量池的除外)

setstatic 设置一个静态字段(同上)

invokestatic 调用一个类的静态方法

2、使用 java.lang.reflect 包中的方法,对类进行反射调用时,如果类没有初始化过,会触发初始化之。

3、当初始化一个类时,如果父类未初始化,会先触发父类的初始化。

4、当虚拟机启动时,用户需要制定一个要执行的主类(含 main 方法的类),虚拟机会先初始化这个类。

5、当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle 
实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,
并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上5种情况属于主动引用,其他情况下属于被动引用。

静态内部类就是被动引用的类型。即,内部类的加载不会随着外部类的加载而加载,而是在使用的时候进行加载。
当 getInstance() 方法被调用时,SingletonHolder 才在 Singleton 的运行时常量池里,把符号引用替换为直接引用,这时静态对象 singleton 也才最终被创建,然后再被 getInstance() 方法返回。

<深入理解JVM> 引用

JVM 保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁、同步。

如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>() 方法,
其他线程都需要阻塞等待,直到活动线程执行完 <clinit>() 方法。

当某个线程第一次执行了<clinit>() 这个初始化方法之后,其他线程就不会重复执行初始化了。

同一个加载器下,一个类只会被初始化一次。

由以上两点,就可以保证单例的延迟加载和线程安全。

单例懒加载实现方式5

public class Singleton {
    private static class SingletonHolder {
        private static Singleton singleton = new Singleton();
    }
    
    private Singleton() {
    }

    public static Singleton getSingleton() {
        return SingletonHolder.singleton;
    }
}

说到了类的加载机制,考虑到enum的特性,便有了单例实现方式6

单例懒加载实现方式6:

// 防new,防反射,防序列化
public enum MySingleton3 {
    SINGLETON,
    ;
    
    public static MySingleton3 getInstance(){
        return SINGLETON;
    }
}

枚举类里面的对象都是static final 的成员变量,final修饰的成员变量,不会触发实例初始化。枚举的初始化时机,在引用枚举的时候,在静态代码块里面初始化的【这个可以通过反编译看出来】,并且,只会只会实例化一次。所以是懒加载,并且是线程安全的。

Week 枚举类反编译示例代码

public final class Week extends Enum
{
    private Week(String s, int i)
    {
        super(s, i);
    }

    public static Week[] values()
    {
        Week aweek[];
        int i;
        Week aweek1[];
        System.arraycopy(aweek = ENUM$VALUES, 0, aweek1 = new Week[i = aweek.length], 0, i);
        return aweek1;
    }

    public static Week valueOf(String s)
    {
        return (Week)Enum.valueOf(learnbymaven/single/Week, s);
    }

    public static final Week Monday;
    public static final Week Tuesday;
    private static final Week ENUM$VALUES[];
    
    // 静态代码块初始化变量
    static 
    {
        Monday = new Week("Monday", 0);
        Tuesday = new Week("Tuesday", 1);
        ENUM$VALUES = (new Week[] {
            Monday, Tuesday
        });
    }
}

鸣谢

感谢BenYoung 对于类的加载机制的总结:https://juejin.cn/post/6844903877326667789

感谢书呆子Rico 对于volatile的关键字的详细总结:https://blog.csdn.net/justloveyou_/article/details/53672005

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容