并发编程-09安全发布对象+单例模式详解

文章目录

在这里插入图片描述

脑图

在这里插入图片描述

概述

上篇文章并发编程-08安全发布对象之发布与逸出中简单的描述了下对象发布和逸出的概念,并通过demo演示了不安全发布对象对象逸出(this引用逸出)。 那该如何安全的发布对象呢?


安全发布对象的4种方式

  • 在静态初始化函数中初始化一个对象的引用

  • 将对象的引用保存到volatile类型域或者AtomicReference对象中

  • 将对象的引用保存到某个正确构造对象的final类型域中

  • 将对象的引用保存到一个由锁保护的域中


示例

上面所提到的几种方法都可以应用到单例模式中,我们将以单例模式为例,介绍如何安全发布对象,以及单例实现的一些注意事项。

以前写的一篇文章: 单例模式

懒汉模式(线程不安全)

package com.artisan.example.singleton;

import com.artisan.anno.NotThreadSafe;

/**
 * 懒汉模式 单例的实例在第一次调用的时候创建
 * 
 * 单线程下没问题,多线程下getInstance方法线程不安全
 * 
 * @author yangshangwei
 *
 */
@NotThreadSafe
public class SingletonLazyModel {

    // 私有构造函数
    // 如果要保证一个类只能被初始化一次,首先要保证的是构造函数是私有的,不允许外部类直接调用new方法
    private SingletonLazyModel() {
        // 可以初始化一些资源等
    }

    // static单例对象
    private static SingletonLazyModel instance = null;

    // 静态工厂方法 
    // public方法外部通过getInstance获取
    public static SingletonLazyModel getInstance() {

        // 多线程情况下,假设线程A和线程B同时获取到instance为null, 这时候instance会被初始化两次
        if (instance == null) {
            instance = new SingletonLazyModel();
        }
        return instance;
    }

}


饿汉模式 静态域(线程安全)

package com.artisan.example.singleton;

import com.artisan.anno.ThreadSafe;

/**
  *  饿汉模式 单例的实例在类装载的时候进行创建
 * 
  *  因为是在类装载的时候进行创建,可以确保线程安全
 * 
 * 
 * 饿汉模式需要注意的地方: 1.私有构造函数中不要有太多的逻辑,否则初始化会慢   2.确保初始化的对象能够被使用,否则造成资源浪费
 * 
 * @author yangshangwei
 *
 */
@ThreadSafe
public class SingletonHungerModel {

    // 私有构造函数
    // 如果要保证一个类只能被初始化一次,首先要保证的是构造函数是私有的,不允许外部类直接调用new方法
    private SingletonHungerModel() {
        // 可以初始化一些资源等
    }

    // static单例对象  静态域
    private static SingletonHungerModel instance = new SingletonHungerModel();

    // public方法外部通过getInstance获取
    public static SingletonHungerModel getInstance() {
        // 直接返回实例化后的对象
        return instance;
    }

}


改造线程不安全的懒汉模式方式一 静态方法使用synchronized修饰 (线程安全)

仅需要将静态的 getInstance方法使用synchronized修饰即可,但是缺点也很明显,线程阻塞,效率较低

在这里插入图片描述

synchronized修饰静态方法的作用域及demo见 并发编程-05线程安全性之原子性【锁之synchronized】


改造线程不安全的懒汉模式方式二双重检查机制(线程不安全)

改造线程不安全的懒汉模式方式一 静态方法使用synchronized修饰的缺点既然都清楚了,为了提高效率,那就把synchronized下沉到方法中的实现里吧

package com.artisan.example.singleton;

import com.artisan.anno.NotThreadSafe;

/**
 * 懒汉模式 单例的实例在第一次调用的时候创建
 * 
 * 对static getInstance方法 进行 双重检测
 * 
 * @author yangshangwei
 *
 */
@NotThreadSafe
public class SingletonLazyModelOptimize2 {

    // 私有构造函数
    // 如果要保证一个类只能被初始化一次,首先要保证的是构造函数是私有的,不允许外部类直接调用new方法
    private SingletonLazyModelOptimize2() {
        // 可以初始化一些资源等
    }

    // static单例对象
    private static SingletonLazyModelOptimize2 instance = null;

    // 静态工厂方法
    // public方法外部通过getInstance获取
    public static  SingletonLazyModelOptimize2 getInstance() {
        // 多线程情况下,假设线程A和线程B同时获取到instance为null, 这时候instance会被初始化两次,所以在判断中加入synchronized
        if (instance == null) {
            // synchronize修饰类 ,修饰范围是synchronized括号括起来的部分,作用于所有对象
            synchronized(SingletonLazyModelOptimize2.class) {
                if (instance == null) {
                    instance = new SingletonLazyModelOptimize2();
                }
            }

        }
        return instance;
    }

}

先说下结论: 上述代码是线程不安全的,可能会返回一个未被实例化的instance,导致错误。

这个就要从cpu的指令说起了。

问题主要出在实例化这一步

instance = new SingletonLazyModelOptimize2()

这个实例化的操作,对应底层3个步骤

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

对于单线程,肯定是没有问题的。但是对于多线程,CPU为了执行效率,可能会发生指令重排序。

经过JVM和CPU的优化,因为第2步和第2步本质上没有先后关系,指令可能会重排成下面的顺序 1—>3—>2:

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

假设按照这个指令顺序执行的话,那么当线程A执行完1和3时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if (instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象


改造线程不安全的懒汉模式方式二双重检查机制优化-volatile + 双重检测机制 (线程安全)

在这里插入图片描述

经过volatile的修饰,保证变量的可见性,当线程A执行instance = new SingletonLazyModelOptimize3的时候,JVM执行顺序会始终保证是下面的顺序:

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

这样的话线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了线程安全。


饿汉模式的第二种写法 静态代码块 (线程安全)

见注释

package com.artisan.example.singleton;

import com.artisan.anno.ThreadSafe;

/**
  *  饿汉模式 单例的实例在类装载的时候进行创建
 * 
  *  因为是在类装载的时候进行创建,可以确保线程安全
 * 
 * 
 * 饿汉模式需要注意的地方: 1.私有构造函数中不要有太多的逻辑,否则初始化会慢   2.确保初始化的对象能够被使用,否则造成资源浪费
 * 
 * @author yangshangwei
 *
 */
@ThreadSafe
public class SingletonHungerModel2 {

    // 私有构造函数
    // 如果要保证一个类只能被初始化一次,首先要保证的是构造函数是私有的,不允许外部类直接调用new方法
    private SingletonHungerModel2() {
        // 可以初始化一些资源等
    }

    // 注意:  static的顺序不要写反了,否则会抛空指针。 static的加载顺序是按顺序执行

    // static单例对象    静态域
    private static SingletonHungerModel2 instance = null;

    // 静态块
    static {
        instance = new SingletonHungerModel2();
    }

    // public方法外部通过getInstance获取
    public static SingletonHungerModel2 getInstance() {
        // 直接返回实例化后的对象
        return instance;
    }

}


饿汉模式的第三种写法 静态内部类 (线程安全)

package com.artisan.example.singleton;

import com.artisan.anno.ThreadSafe;

/**
 * 饿汉模式 单例的实例在类装载的时候进行创建
 * 
 * 使用静态内部类实现的单例模式-线程安全
 * 
 * 
 * 饿汉模式需要注意的地方: 1.私有构造函数中不要有太多的逻辑,否则初始化会慢 2.确保初始化的对象能够被使用,否则造成资源浪费
 * 
 * @author yangshangwei
 *
 */
@ThreadSafe
public class SingletonHungerModel3 {

    // 私有构造函数
    // 如果要保证一个类只能被初始化一次,首先要保证的是构造函数是私有的,不允许外部类直接调用new方法
    private SingletonHungerModel3() {
        // 可以初始化一些资源等
    }

    // 静态工厂方法-获取实例
    public static SingletonHungerModel3 getInstance() {
        // 直接返回实例化后的对象
        return InstanceHolder.INSTANCE;
    }

    // 用静态内部类创建单例对象 private 修饰
    private static class InstanceHolder {
        private static final SingletonHungerModel3 INSTANCE = new SingletonHungerModel3();
    }

}

注意事项

  • 从外部无法访问静态内部类InstanceHolder (private修饰的),只有当调用Singleton.getInstance方法的时候,才能得到单例对象instance。
  • instance对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类InstanceHolder 被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。

小结

小结: 以上所提到的单例实现方式并不能算是完全安全的,这里的安全不仅指线程安全还有发布对象的安全。因为以上例子所实现的单例模式,我们都可以通过反射机制去获取私有构造器更改其访问级别从而实例化多个不同的对象。

那么如何防止利用反射构建对象呢?这时我们就需要使用到内部枚举类了,因为JVM可以阻止反射获取枚举类的私有构造方法


枚举模式 推荐 ( 线程安全,防止反射构建)

package com.artisan.example.singleton;

import lombok.Getter;

public class SingletonEum {

    /**
     * 私有构造函数
     */
    private SingletonEum() {
    }

    /**
     * 静态工厂方法-获取实例
     *
     * @return instance
     */
    public static SingletonEum getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    /**
     * 由枚举类创建单例对象
     */

    @Getter
    private enum Singleton {
        INSTANCE;

        /**
         * 单例对象
         */
        private SingletonEum instance;

        /**
         * JVM保证这个方法绝对只调用一次
         */
        Singleton() {
            instance = new SingletonEum();
        }
    }

}

使用枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以保证线程安全,并且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。

上面代码中之所以使用内部枚举类的原因是为了让这个单例对象可以懒加载,相当于是结合了静态内部类的实现思想。若不使用内部枚举类的话,单例对象就会在枚举类被加载的时候被构建。

原文地址:https://artisan.blog.csdn.net/article/details/87833163

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

推荐阅读更多精彩内容