文章目录

  • 脑图
  • 概述
  • 安全发布对象的4种方式
  • 示例
    • 懒汉模式(线程不安全)
    • 饿汉模式 静态域(线程安全)
    • 改造线程不安全的懒汉模式方式一 静态方法使用synchronized修饰 (线程安全)
    • 改造线程不安全的懒汉模式方式二双重检查机制(线程不安全)
    • 改造线程不安全的懒汉模式方式二双重检查机制优化-volatile + 双重检测机制 (线程安全)
    • 饿汉模式的第二种写法 静态代码块 (线程安全)
    • 饿汉模式的第三种写法 静态内部类 (线程安全)
    • 小结
  • 枚举模式 推荐 ( 线程安全,防止反射构建)
  • 代码

脑图


概述

上篇文章并发编程-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会被初始化两次,所以在判断中加入synchronizedif (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();}/*** 由枚举类创建单例对象*/@Getterprivate enum Singleton {INSTANCE;/*** 单例对象*/private SingletonEum instance;/*** JVM保证这个方法绝对只调用一次*/Singleton() {instance = new SingletonEum();}}}

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

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


代码

https://github.com/yangshangwei/ConcurrencyMaster

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

  1. 【Java并发编程】安全发布对象

    文章目录 安全发布对象 一.对象发布 二.对象逸出 三.安全发布的方法 安全发布对象 一.对象发布 在介绍安全发布对象之前,应该首先聊一聊什么是发布对象.发布对象是"使一个对象能够被当前范围 ...

  2. 并发编程-08安全发布对象之发布与逸出

    文章目录 脑图 概念 示例 不安全的发布对象Demo 对象逸出Demo 小结 代码 脑图 概念 发布对象: 使一个对象能够被当前范围之外的代码所使用,日常开发中比较常见的比如通过类的非私有方法返回对象 ...

  3. 多线程与并发编程入门基础,多图详解

    目录 1  概述 1.1 多线程概念 1.2 多线程的应用场景 2 线程与进程 2.1 概念 2.2 进程与线程的联系与区别: 3 并行并发 3.1 并行并发概念​ 3.2 并行并发 4 程序运行,线 ...

  4. Java并发编程:线程封闭和ThreadLocal详解

    什么是线程封闭 当访问共享变量时,往往需要加锁来保证数据同步.一种避免使用同步的方式就是不共享数据.如果仅在单线程中访问数据,就不需要同步了.这种技术称为线程封闭.在Java语言中,提供了一些类库和机 ...

  5. java并发编程之线程的生命周期详解

    java线程从创建到销毁,一共会有6个状态,不一定都经历,有可能只经历部分: NEW:初始状态,线程被创建,但是还没有调用start方法. RUNNABLED:运行状态,java线程把操作系统中的就绪 ...

  6. Java并发编程系列之CountDownLatch用法及详解

    背景 前几天一个同事问我,对这个CountDownLatch有没有了解想问一些问题,当时我一脸懵逼,不知道如何回答.今天赶紧抽空好好补补.不得不说Doug Lea大师真的很牛,设计出如此好的类. 1. ...

  7. java connection 单例_Java设计模式之单例模式详解

    Java设计模式之单例模式详解 什么是设计模式 设计模式是在大量的实践中总结和理论之后优选的代码结构,编程风格,以及解决问题的思考方式.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可 ...

  8. 深入浅出多线程编程实战(五)ThreadLocal详解(介绍、使用、原理、应用场景)

    深入浅出多线程编程实战(五)ThreadLocal详解(介绍.使用.原理.应用场景) 文章目录 一.ThreadLocal简介 二.ThreadLocal与Synchronized区别 三.Threa ...

  9. 64位JVM的Java对象头详解

    关注"Java艺术"一起来充电吧! 我们编写一个Java类,编译后会生成.class文件,当类加载器将class文件加载到jvm时,会生成一个Klass类型的对象(c++),称为类 ...

最新文章

  1. 研发应该懂的binlog知识(下)
  2. 性能监测与优化命令free
  3. QT的QQmlPropertyMap类的使用
  4. 每天一道LeetCode-----数独盘求解
  5. 为什么使用Deque而不使用Stack构造栈
  6. 手动给64位centos6.3版本linux的firefox安装Adobe flash player
  7. Bootstrap 3之美03-独立行,文字环绕,图片自适应,隐藏元素
  8. RAD Studio 2010 环境设置(转)
  9. python 3d游戏编程入门_用python写游戏 - 从入门到精通16
  10. 数字图像处理合集终章——车流量统计(后附源码)
  11. Windows批处理:命令echo 和 @
  12. SPSS/PROCESS-中介检验
  13. 南水北调工程简介及线路图
  14. 智课雅思短语---一、be no exception
  15. python地理位置聚类_python实现地理位置的聚类
  16. Oracle range分区values less than代表的是小于
  17. android7.0 360os,360 OS 2.0评测 安全与体会的全部升级
  18. 一个IP账号,为啥通过路由器就可供多人同时使用?
  19. 爸爸妈妈儿子女儿吃水果问题以及五个哲学家吃饭问题
  20. excel服务器项目管理软件,用excel做项目管理系统

热门文章

  1. mysql存储netcdf数据_关于NetCDF与HDF5存储科学数据的观点?
  2. 图分区技术基本概念【1】
  3. wget命令出现Unable to establish SSL connection.错误
  4. python浮点数运算问题_python基础教程之. 浮点数运算:问题和局限
  5. MATLAB编程经典程序 素数的判断,求0~100素数之和
  6. 机器学习算法源码全解析(四)-人工神经网络关键核心知识点汇总
  7. 塞尔达传说gba_回顾 | 猹鱼主题速写:塞尔达传说 英国绅士
  8. python怎么画两幅图_python matplotlib模块: Subplots(在同一个figure里绘制多个图)
  9. 机器学习基本概念-阿里云大学
  10. 今日头条算法原理(全文)【转】