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

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

2019080611330380.jpg


脑图


20190222165920930.png


概述

上篇文章并发编程-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修饰即可,但是缺点也很明显,线程阻塞,效率较低


20190221003514612.png


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个步骤


memory = allocate() // 分配对象的内存空间

ctorInstance() // 初始化对象

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 + 双重检测机制 (线程安全)


20190221010416654.png

经过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://github.com/yangshangwei/ConcurrencyMaster

相关文章
|
2月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
60 1
|
1月前
|
设计模式 SQL 安全
单例模式大全:细说七种线程安全的Java单例实现,及数种打破单例的手段!
设计模式,这是编程中的灵魂,用好不同的设计模式,能使你的代码更优雅/健壮、维护性更强、灵活性更高,而众多设计模式中最出名、最广为人知的就是Singleton Pattern单例模式。通过单例模式,我们就可以避免由于多个实例的创建和销毁带来的额外开销,本文就来一起聊聊单例模式。
|
2月前
|
设计模式 缓存 安全
Java面试题:工厂模式与内存泄漏防范?线程安全与volatile关键字的适用性?并发集合与线程池管理问题
Java面试题:工厂模式与内存泄漏防范?线程安全与volatile关键字的适用性?并发集合与线程池管理问题
45 1
|
2月前
|
设计模式 存储 缓存
Java面试题:结合单例模式与Java内存模型,设计一个线程安全的单例类?使用内存屏障与Java并发工具类,实现一个高效的并发缓存系统?结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
Java面试题:结合单例模式与Java内存模型,设计一个线程安全的单例类?使用内存屏障与Java并发工具类,实现一个高效的并发缓存系统?结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
25 0
|
4月前
|
设计模式 安全 Java
Java多线程基础-8:单例模式及其线程安全问题(二)
单例模式是软件设计模式之一,确保一个类只有一个实例并提供全局访问点。
42 0
|
4月前
|
设计模式 安全 Java
Java多线程基础-8:单例模式及其线程安全问题(一)
本文介绍了设计模式中的单例模式,它是软件开发中的经典模式,确保某个类在程序运行期间只有一个实例。
45 0
|
4月前
|
设计模式 安全 Java
多线程案例-单例模式
多线程案例-单例模式
255 0
|
设计模式 Java 数据库连接
多线程案例(1)-单例模式
多线程案例(1)-单例模式
67 0
|
设计模式 安全 Java
【Java|多线程与高并发】设计模式-单例模式(饿汉式,懒汉式和静态内部类)
设计模式是一种在软件开发中常用的解决复杂问题的方法论。它提供了一套经过验证的解决方案,用于解决特定类型问题的设计和实现。设计模式可以帮助开发人员提高代码的可重用性、可维护性和可扩展性。
|
设计模式 SQL 安全
【设计模式学习笔记】单例模式详解(懒汉式遇上多线程问题解析基于C++实现)
【设计模式学习笔记】单例模式详解(懒汉式遇上多线程问题解析基于C++实现)
324 0
【设计模式学习笔记】单例模式详解(懒汉式遇上多线程问题解析基于C++实现)