并发与高并发(十一)-安全发布对象

前言

什么是发布对象呢?简要来说,就是让我们定义的对象能够被其他范围之外的范围使用。

主体概要

  • 对象的发布与逸出
  • 对象安全发布的四种方法

主体内容

一、对象的发布与逸出

1.概念

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

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

2.平时我们会通过一些类的非私有方法返回对象的引用或者公有静态变量发布对象。接下来,看一下非私有方法返回对象的引用的不安全发布的例子。

@Slf4j
public class UnsafePublish {
    private String[] states = {"a","b","c"};
    
    public String[] getStates(){
        return states;
    }
    
    public static void main(String[] args){
        UnsafePublish up = new UnsafePublish();
        log.info("states:{}",Arrays.toString(up.getStates()));
        //对它私有属性的数组进行修改
        up.getStates()[0]="d";
        log.info("states:{}",Arrays.toString(up.getStates()));

    }
}

这样的结果会是:

23:19:37.949 [main] INFO com.controller.publish.UnsafePublish - states:[a, b, c]
23:19:37.952 [main] INFO com.controller.publish.UnsafePublish - states:[d, b, c]

例子很简单,但是还是要看一下,因为非私有方法(public访问级别),类的外部的线程都可以访问这个域,这样的发布对象其实是不安全的,因为我们无法保证其他线程会不会修改这个域,从而造成类里面的states的错误。

3.接下来,介绍一下对象发布的逸出,演示一个例子。

@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();
    }
}

结果:

23:35:56.093 [main] INFO com.controller.publish.Escape - 0

看代码中的内部类实例里面包含了对封装实例的引用,这样可能导致Escape对象没有被完全构造完成就会被发布,可能会导致隐患。

具体解释:在Escape的构造函数中相当于是启动了一个线程,这会造成内部类构造方法中的this引用的逸出,新线程总是会在所属对象构造完毕之前就能够看到this的引用了。

二、安全发布对象的四种方法

  • 在静态初始化函数中初始化一个对象的引用
  • 将对象的引用保存到volatile类型域或者AtomicReference对象中
  • 将对象的引用保存到某个正确构造对象的 final类型域中
  • 将对象的引用保存到某个由锁保护的域中

这四种方法该如何理解呢,下面通过四个例子来诠释一下。

1.首先,我们通过一个单例设计模式(懒汉式)的代码看一下其在多线程场景下会出现怎样的问题。

public class Singleton {
    //私有构造函数
    private Singleton() {

    }
    //单例对象
    private static Singleton instance = null;
    //静态的工厂方法
    public static Singleton getInstance(){
        if(instance==null){
            instance = new Singleton();
        }
        return instance;
    }
}

观察以上的代码,单线程下没有问题,那么多线程下,当个两个线程同时判断instance==null的条件成立后,都进入了创建对象的过程,那么私有构造函数相当于被调用了两边,如果私有构造函数中是数值加减的操作,这个数值是不是会被加或减了两次,那一定是不正确的。以上的代码是线程不安全的。

2.懒汉单例模式看过了,下面看一下饿汉式的单例模式。

public class Singleton2 {
    //私有构造函数
        private Singleton2() {

        }
        //单例对象
        private static Singleton2 instance = new Singleton2();
        //静态的工厂方法
        public static Singleton2 getInstance(){
            return instance;
        }
}

无论如何,饿汉式都会直接创建类对象,无论这个对象是否被使用,它都会被创建,这就导致性能问题,造成资源的浪费。因此,我们在使用饿汉式的时候,一定要考虑到它的私有构造函数是否有过多的处理逻辑。

综上两个例子所述,饿汉式虽然是线程安全的,却容易造成性能问题,那么懒汉式能不能改造成线程安全的呢?答案是可以的。接下来看一下如何改造吧!

猜想一下,要是能保证创建对象的方法只能由一个线程访问就不会造成线程不安全了,于是在静态的工厂方法上加上synchronized

//静态的工厂方法
        public synchronized static Singleton3 getInstance(){
            if(instance==null){
                instance = new Singleton3();
            }
            return instance;
        }

虽然这样保证了线程安全性,但是却造成了性能上的开销,所以这种写法是不推荐的!!!

那我们要是将synchronized下沉到创建对象上呢?

public class Singleton4 {
    //私有构造函数
            private Singleton4() {

            }
            //单例对象
            private static Singleton4 instance = null;
            //静态的工厂方法
            public static Singleton4 getInstance(){
                if(instance==null){
                    synchronized(Singleton4.class){
                        if(instance==null){//双重检测机制
                            instance = new Singleton4();
                        }
                    }
                }
                return instance;
            }
}

这种方法其实又变成了线程不安全了。这里复习一下对象创建的过程,(1)memory=allocate() 分配对象的内存空间 (2)ctorInstance() 初始化对象 (3)instance=memory 设置instance指向刚分配的内存。这里的JVM和CPU发生了指令重排(指令重排请参考并发与高并发(十)),这就导致以上的三步不是按照以上的顺序来执行了,而是变成了(1)(3)(2)这种顺序。这种情况发生的场景需要解释一下:如以下代码所示,在指令重排的前提下,当线程A,线程B各自执行到代码中标识的位置时,A开始执行第(3)步,那么线程B执行判断的时候,发现有值了,便会直接return instance。而实际的线程A第(2)步初始化对象还没做。虽然这种问题出现的概率很小,但是这种方法仍然是线程不安全的。

public class Singleton4 {
    //私有构造函数
            private Singleton4() {

            }
            //单例对象
            private static Singleton4 instance = null;
            //静态的工厂方法
            public static Singleton4 getInstance(){
                if(instance==null){//线程B
                    synchronized(Singleton4.class){
                        if(instance==null){//双重检测机制
                            instance = new Singleton4(); //线程A-3
                        }
                    }
                }
                return instance;
            }
}

既然是因为指令重排的原因导致的问题,那么我们就不让JVM和CPU发生指令重排。

想起来之前学过的一个关键字:volatile。在以上代码的基础上,将volatile加到单例对象声明上。

private volatile static Singleton4 instance = null;

这样这个懒汉式单例模式就变成线程安全的了。

这里一定一定记住,因为平时写代码的时候很容易被忽略。这就是volatile的使用场景之一:volatile+双重检测机制=禁止指令重排。

3.另外提到一种线程安全,性能可靠的枚举模式:

public class Singleton5 {
    private Singleton5(){
        
    }
    public static Singleton5 getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
    private enum Singleton{
        INSTANCE;
        
        private Singleton5 singleton;
        //JVM保证这个方法绝对只被调用一次
        Singleton(){
            singleton = new Singleton5();
        }
        
        public Singleton5 getInstance(){
            return singleton;
        }
    }
}

猜你喜欢

转载自www.cnblogs.com/jmy520/p/12078856.html
今日推荐