高并发之——如何安全的发布对象(含各种单例代码分析)

简介: 首先,来介绍两个概念:发布对象:使一个对象能够被当前范围之外的代码所使用。对象溢出:是一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见。接下来,给出一个不安全的发布示例代码和对象溢出示例代码。

首先,来介绍两个概念:

发布对象:使一个对象能够被当前范围之外的代码所使用。

对象溢出:是一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见。

接下来,给出一个不安全的发布示例代码和对象溢出示例代码。

不安全的发布示例代码:

package io.binghe.concurrency.example.publish;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
@Slf4j
public class UnsafePublish {
    private String[] states = {"a", "b", "c"};
    public String[] getStates(){
        return states;
    }
    public static void main(String[] args){
        UnsafePublish unsafePublish = new UnsafePublish();
        log.info("{}", Arrays.toString(unsafePublish.getStates()));
        unsafePublish.getStates()[0] = "d";
        log.info("{}", Arrays.toString(unsafePublish.getStates()));
    }
}

其中,每个线程都能获取到UnsafePublish类的私有成员变量states,并修改states数组的元素值,造成其他线程获取的states元素值不确定。

对象溢出示例代码

package io.binghe.concurrency.example.publish;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Escape {
    private int thisCanBeEscape = 0;
    public Escape(){
        new InnerClass();
    }
    private class InnerClass{
        public InnerClass(){
            log.info("{}", Escape.this.thisCanBeEscape);
        }
    }
    public static void main(String[] args){
        new Escape();
    }
}

其中,内部类InnerClass的构造方法中包含了对封装实例Escape的隐含的引用(体现在InnerClass的构造方法中引用了Escape.this),在对象没有被正确构造完成之前,就会被发布,有可能存在不安全的因素。

一个导致this在构造期间溢出的错误:在构造函数中,启动一个线程,无论是隐式的启动还是显式的启动,都会造成this引用的溢出(因为新线程总是在所属对象构造完毕之前就已经看到this引用了)。所以,如果要在构造函数中创建线程,则不要在构造函数中启动线程,可以使用一个专有的start()方法或者一个初始化方法,来统一启动线程,可以采用工厂方法和私有构造函数来完成对象的创建和监听器的注册,之后统一启动线程,来避免溢出。

注意:在对象未构造完成之前,不可以将其发布

如何安全的发布对象:

(1)在静态初始化函数中初始化一个对象引用

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

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

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

接下来,看几个单例对象的示例代码,其中有些代码是线程安全的,有些则不是线程安全的,需要大家细细品味,这些代码也是冰河本人在高并发环境下测试验证过的。

代码一:SingletonExample1

这个类是懒汉模式,并且是线程不安全的

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 懒汉模式,单例实例在第一次使用的时候进行创建,这个类是线程不安全的
 */
public class SingletonExample1 {
    private SingletonExample1(){}
    private static SingletonExample1 instance = null;
    public static SingletonExample1 getInstance(){
        //多个线程同时调用,可能会创建多个对象
        if (instance == null){
            instance = new SingletonExample1();
        }
        return instance;
    }
}

代码二:SingletonExample2

饿汉模式,单例实例在类装载的时候进行创建,是线程安全的

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 饿汉模式,单例实例在类装载的时候进行创建,是线程安全的
 */
public class SingletonExample2 {
    private SingletonExample2(){}
    private static SingletonExample2 instance = new SingletonExample2();
    public static SingletonExample2 getInstance(){
        return instance;
    }
}

代码三:SingletonExample3

懒汉模式,单例实例在第一次使用的时候进行创建,这个类是线程安全的,但是这个写法不推

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 懒汉模式,单例实例在第一次使用的时候进行创建,这个类是线程安全的,但是这个写法不推荐
 */
public class SingletonExample3 {
    private SingletonExample3(){}
    private static SingletonExample3 instance = null;
    public static synchronized SingletonExample3 getInstance(){
        if (instance == null){
            instance = new SingletonExample3();
        }
        return instance;
    }
}

代码四:SingletonExample4

懒汉模式(双重锁同步锁单例模式),单例实例在第一次使用的时候进行创建,但是,这个类不是线程安全的!

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 懒汉模式(双重锁同步锁单例模式)
 *              单例实例在第一次使用的时候进行创建,这个类不是线程安全的
 */
public class SingletonExample4 {
    private SingletonExample4(){}
    private static SingletonExample4 instance = null;
    //线程不安全
    //当执行instance = new SingletonExample4();这行代码时,CPU会执行如下指令:
    //1.memory = allocate() 分配对象的内存空间
    //2.ctorInstance() 初始化对象
    //3.instance = memory 设置instance指向刚分配的内存
    //单纯执行以上三步没啥问题,但是在多线程情况下,可能会发生指令重排序。
    // 指令重排序对单线程没有影响,单线程下CPU可以按照顺序执行以上三个步骤,但是在多线程下,如果发生了指令重排序,则会打乱上面的三个步骤。
    //如果发生了JVM和CPU优化,发生重排序时,可能会按照下面的顺序执行:
    //1.memory = allocate() 分配对象的内存空间
    //3.instance = memory 设置instance指向刚分配的内存
    //2.ctorInstance() 初始化对象
    //假设目前有两个线程A和B同时执行getInstance()方法,A线程执行到instance = new SingletonExample4(); B线程刚执行到第一个 if (instance == null){处,
    //如果按照1.3.2的顺序,假设线程A执行到3.instance = memory 设置instance指向刚分配的内存,此时,线程B判断instance已经有值,就会直接return instance;
    //而实际上,线程A还未执行2.ctorInstance() 初始化对象,也就是说线程B拿到的instance对象还未进行初始化,这个未初始化的instance对象一旦被线程B使用,就会出现问题。
    public static SingletonExample4 getInstance(){
        if (instance == null){
            synchronized (SingletonExample4.class){
                if(instance == null){
                    instance = new SingletonExample4();
                }
            }
        }
        return instance;
    }
}

线程不安全分析如下:

当执行instance = new SingletonExample4();这行代码时,CPU会执行如下指令:

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

2.ctorInstance() 初始化对象

3.instance = memory 设置instance指向刚分配的内存

单纯执行以上三步没啥问题,但是在多线程情况下,可能会发生指令重排序。

指令重排序对单线程没有影响,单线程下CPU可以按照顺序执行以上三个步骤,但是在多线程下,如果发生了指令重排序,则会打乱上面的三个步骤。


如果发生了JVM和CPU优化,发生重排序时,可能会按照下面的顺序执行:

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

3.instance = memory 设置instance指向刚分配的内存

2.ctorInstance() 初始化对象


假设目前有两个线程A和B同时执行getInstance()方法,A线程执行到instance = new SingletonExample4(); B线程刚执行到第一个 if (instance == null){处,如果按照1.3.2的顺序,假设线程A执行到3.instance = memory 设置instance指向刚分配的内存,此时,线程B判断instance已经有值,就会直接return instance;而实际上,线程A还未执行2.ctorInstance() 初始化对象,也就是说线程B拿到的instance对象还未进行初始化,这个未初始化的instance对象一旦被线程B使用,就会出现问题。

代码五:SingletonExample5

懒汉模式(双重锁同步锁单例模式)单例实例在第一次使用的时候进行创建,这个类是线程安全的,使用的是 volatile + 双重检测机制来禁止指令重排达到线程安全

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 懒汉模式(双重锁同步锁单例模式)
 *              单例实例在第一次使用的时候进行创建,这个类是线程安全的
 */
public class SingletonExample5 {
    private SingletonExample5(){}
    //单例对象  volatile + 双重检测机制来禁止指令重排
    private volatile static SingletonExample5 instance = null;
    public static SingletonExample5 getInstance(){
        if (instance == null){
            synchronized (SingletonExample5.class){
                if(instance == null){
                    instance = new SingletonExample5();
                }
            }
        }
        return instance;
    }
}

代码六:SingletonExample6

饿汉模式,单例实例在类装载的时候(使用静态代码块)进行创建,是线程安全的

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 饿汉模式,单例实例在类装载的时候进行创建,是线程安全的
 */
public class SingletonExample6 {
    private SingletonExample6(){}
    private static SingletonExample6 instance = null;
    static {
        instance = new SingletonExample6();
    }
    public static SingletonExample6 getInstance(){
        return instance;
    }
}

代码七:SingletonExample7

枚举方式进行实例化,是线程安全的,此种方式也是线程最安全的

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 枚举方式进行实例化,是线程安全的,此种方式也是线程最安全的
 */
public class SingletonExample7 {
    private SingletonExample7(){}
    public static SingletonExample7 getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
    private enum Singleton{
        INSTANCE;
        private SingletonExample7 singleton;
        //JVM保证这个方法绝对只调用一次
        Singleton(){
            singleton = new SingletonExample7();
        }
        public SingletonExample7 getInstance(){
            return singleton;
        }
    }
}


相关文章
|
4月前
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
65 0
使用队列解决高并发下使用Client对象调用webService接口
使用队列解决高并发下使用Client对象调用webService接口
|
6月前
|
SQL 安全 算法
在高并发情况下,如何做到安全的修改同一行数据?
在高并发情况下,如何做到安全的修改同一行数据?
|
算法 安全 应用服务中间件
温故知新-互联网高并发与安全篇
温故知新-互联网高并发与安全篇
44 0
|
存储 Java 数据安全/隐私保护
项目实战典型案例15——高并发环境下由于使用全局变量导致数据混乱 高并发环境下对象被大量创建,导致GC并是CPU飙升
项目实战典型案例15——高并发环境下由于使用全局变量导致数据混乱 高并发环境下对象被大量创建,导致GC并是CPU飙升
153 0
项目实战典型案例15——高并发环境下由于使用全局变量导致数据混乱 高并发环境下对象被大量创建,导致GC并是CPU飙升
|
XML 安全 Java
Spring 框架(Spring Framework)之事务管理、单元测试、单例的高并发安全问题等
Spring 框架(Spring Framework)之事务管理、单元测试、单例的高并发安全问题等
270 0
|
存储 资源调度 安全
ATC'22顶会论文RunD:高密高并发的轻量级 Serverless 安全容器运行时 | 龙蜥技术
RunD可以在88毫秒内启动,并且在104核384GB内存的单节点上每秒启动超过200个安全容器。
ATC'22顶会论文RunD:高密高并发的轻量级 Serverless 安全容器运行时 | 龙蜥技术
|
SQL Java
解决单例模式中懒汉式不支持高并发,饿汉式不支持懒加载问题最简单方法——基于枚举类型的单例实现
解决单例模式中懒汉式不支持高并发,饿汉式不支持懒加载问题最简单方法——基于枚举类型的单例实现
161 0
解决单例模式中懒汉式不支持高并发,饿汉式不支持懒加载问题最简单方法——基于枚举类型的单例实现
|
缓存 安全 NoSQL
阿里一面:Spring Bean 默认是单例的,高并发情况下,如何保证并发安全?
阿里一面:Spring Bean 默认是单例的,高并发情况下,如何保证并发安全?
176 0
|
Java 容器
Java——多线程高并发系列之ArrayList、HashSet、HashMap集合线程不安全的解决方案
Java——多线程高并发系列之ArrayList、HashSet、HashMap集合线程不安全的解决方案
Java——多线程高并发系列之ArrayList、HashSet、HashMap集合线程不安全的解决方案
下一篇
无影云桌面