JAVA学习笔记(并发编程 - 肆)- 安全发布对象

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/bingdianone/article/details/83414709

什么是发布对象和对象逸出

  • ◆发布对象:使一个对象能够被当前范围之外的代码所使用
  • ◆对象逸出:一种错误的发布。当一个对象还没有构造完成时,就使它被其他线程所见

不正确的发布可变对象导致的两种错误:

  • 发布线程意外的所有线程都可以看到被发布对象的过期的值
  • 线程看到的被发布对象的引用是最新的,然而被发布对象的状态却是过期的

不安全的示例

package com.mmall.concurrency.example.publish;

import com.mmall.concurrency.annoations.NotRecommend;
import com.mmall.concurrency.annoations.NotThreadSafe;
import lombok.extern.slf4j.Slf4j;

/**
 * 对象溢出
 *在对象构造没有完成之前就会被发布;有可能有不安全因素
 */
@Slf4j
@NotThreadSafe
@NotRecommend
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();
    }
}
/*
在以上这个例子中,内部类的构造器里包含了对封装实例的隐含引用,
这样在对象没有被正确构造完成之前就会被发布,由此会导致不安全的因素在里面。
其中一个就是导致this引用在构造期间逸出的错误,它是在构造函数构造过程中启动了一个线程,
无论是显式启动还是隐式启动,都会造成this引用的逸出。新线程总会在所属对象构造完毕之前
就已经看到它了,所以如果要在构造函数中创建线程,那么不要启动它,而是应该采用一个专有的
start,或是其他初始化的方式统一启动线程。这里其实我们可以使用工厂方法和私有构造函
数来完成对象创建和监听器的注册等等来避免不正确的发布。
*/

package com.mmall.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()));
    }
}
/*
15:15:21.725 [main] INFO com.mmall.concurrency.example.publish.UnSafePublish - [a, b, c]
15:15:21.734 [main] INFO com.mmall.concurrency.example.publish.UnSafePublish - [d, b, c]
 */

安全发布对象

想要安全的发布对象主要有四种方法

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

以上所提到的几种方法都可以应用到单例模式中,所以本文将以单例模式为例,介绍如何安全发布对象,以及单例实现的一些问题。

众所周知,单例模式是最常用的设计模式了。Spring容器中所管理的类的实例默认也是单例的,虽然单例看似简单,但也是有不少需要注意的地方,特别是在多线程环境下。示例代码如下:

懒汉模式发布对象

package com.mmall.concurrency.example.singleton;

import com.mmall.concurrency.annoations.NotThreadSafe;

/**
 * 懒汉模式
 * 在第一次使用时进行创建
 * 多线程模式下不安全
 */
@NotThreadSafe
public class SingletonExample1 {
    //私有构造函数
    private SingletonExample1(){

    }

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

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

饿汉模式发布对象

package com.mmall.concurrency.example.singleton;

import com.mmall.concurrency.annoations.ThreadSafe;

/**
 * 饿汉模式
 * 在类装载使用时进行创建
 * 多线程安全
 * 不足:如果构造方法中存在过多的处理会导致这个类加载的过慢;引起性能问题
 * 构造方法必须使用;否则会造成资源浪费
 */
@ThreadSafe
@NotRecommend
public class SingletonExample2 {
    //私有构造函数
    private SingletonExample2(){

    }

    //单例对象
    private static SingletonExample2 instance=new SingletonExample2();

    //静态的工厂方法
    public static SingletonExample2 getInstance(){
        return instance;
    }
}

懒汉模式改造1

package com.mmall.concurrency.example.singleton;

import com.mmall.concurrency.annoations.NotRecommend;
import com.mmall.concurrency.annoations.ThreadSafe;

/**
 * 懒汉模式
 * 在第一次使用时进行创建
 * 多线程安全但是不推荐使用
 * synchronized会有过多的开销
 */
@ThreadSafe
@NotRecommend
public class SingletonExample3 {
    //私有构造函数
    private SingletonExample3(){

    }

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

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

懒汉模式改造2

package com.mmall.concurrency.example.singleton;

import com.mmall.concurrency.annoations.NotRecommend;
import com.mmall.concurrency.annoations.NotThreadSafe;

/**
 * 懒汉模式-》双重同步锁单例模式
 * 在第一次使用时进行创建
 * 不安全是多线程下cpu指令重排造成
 *
 */
@NotThreadSafe
@NotRecommend
public class SingletonExample4 {
    //私有构造函数
    private SingletonExample4(){

    }

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

    // JVM和cpu优化,发生了指令重排.

    // 1, memory = allocate()分配对象的内存空间
    // 3, instance = memory设置instance指向刚分配的内存,
    // 2, ctorInstance()初始化对象
    //当A在重排后为3;B判断对象不为空

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

    //静态的工厂方法
    public static  SingletonExample4 getInstance(){
        if(instance==null){//双重检测机制   //B
            synchronized (SingletonExample4.class){//同步锁
                if(instance==null){
                    instance=new SingletonExample4();//A-3
                }
            }
        }
        return instance;
    }
}

懒汉模式改造3(使用volatile )

package com.mmall.concurrency.example.singleton;

import com.mmall.concurrency.annoations.ThreadSafe;

/**
 * 懒汉模式-》双重同步锁单例模式
 * 在第一次使用时进行创建
 * volatile可以不让指令重排(双重检测)
 */
@ThreadSafe
public class SingletonExample5 {
    //私有构造函数
    private SingletonExample5(){

    }

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

    //单例对象 volatile +双重检测机制->禁止指令重排
    //private static SingletonExample4 instance=null;
    //volatile可以不让指令重排(双重检测)
    private volatile static SingletonExample5 instance=null;

    //静态的工厂方法
    public static SingletonExample5 getInstance(){
        if(instance==null){//双重检测机制   //B
            synchronized (SingletonExample5.class){//同步锁
                if(instance==null){
                    instance=new SingletonExample5();//A
                }
            }
        }
        return instance;
    }
}

饿汉模式改造1

package com.mmall.concurrency.example.singleton;

import com.mmall.concurrency.annoations.ThreadSafe;

/**
 * 饿汉模式
 * 在类装载使用时进行创建
 * 当在写静态代码块和静态域的时候注意代码顺序
 */
@ThreadSafe
public class SingletonExample6 {
    //私有构造函数
    private SingletonExample6(){

    }
    /*放这里会先走静态代码块;报空指针
    static {
        instance=new SingletonExample6();
    }
     */

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

    static {
        instance=new SingletonExample6();
    }

    //静态的工厂方法
    public static SingletonExample6 getInstance(){
        return instance;
    }

    public static void main(String[] args) {
        System.out.println(getInstance().hashCode());
    }
}

饿汉模式改造2

package com.mmall.concurrency.example.singleton;

import com.mmall.concurrency.annoations.Recommend;
import com.mmall.concurrency.annoations.ThreadSafe;

@ThreadSafe
@Recommend
public class SingletonExample8 {
    /**
     * 私有构造函数
     */
    private SingletonExample8() {
    }

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

    /**
     * 用静态内部类创建单例对象
     */
    private static class LazyHolder {
        private static final SingletonExample8 INSTANCE = new SingletonExample8();
    }
}

/*
这里有几个需要注意的点:

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

以上所提到的单例实现方式并不能算是完全安全的,这里的安全不仅指线程安全还有发布对象的安全。因为以上例子所实现的单例模式,都可以通过反射机制去获取私有构造器更改其访问级别从而实例化多个不同的对象,虽然一般不会这么干,但也难免会有这种情况。那么如何防止利用反射构建对象呢?这时我们就需要使用到内部枚举类了,因为JVM可以阻止反射获取枚举类的私有构造方法。示例代码如下:

枚举模式 -推荐使用

package com.mmall.concurrency.example.singleton;

import com.mmall.concurrency.annoations.Recommend;
import com.mmall.concurrency.annoations.ThreadSafe;

@ThreadSafe
@Recommend
/**
 * 枚举模式;最安全的;
 * 推荐原因:
 * 对于懒汉模式在安全性方面更容易保证
 * 对于饿汉模式在实际调用的时候才开始做初始化;在后续使用也可以直接取到值;不会照成资源浪费
 */
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;
        }
    }
}

使用枚举实现的单例模式,是最为推荐的一种写法,因为这种实现方式不但可以防止利用反射强行构建单例对象,而且可以保证线程安全,并且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。这里之所以使用内部枚举类的原因是为了让这个单例对象可以懒加载,相当于是结合了静态内部类的实现思想。若不使用内部枚举类的话,单例对象就会在枚举类被加载的时候被构建。

单例模式实现 是否线程安全 是否懒加载 是否防止反射构建
双重锁检测volatile
静态内部类和块
枚举

猜你喜欢

转载自blog.csdn.net/bingdianone/article/details/83414709
今日推荐