(二)Java并发学习笔记--安全发布对象

2022-04-13 14:02:04 浏览数 (3)

逸出的方式

上边关于逸出的概念讲述的很是模糊,下面列举几个逸出的示例。

  1. 通过静态变量引用逸出
代码语言:javascript复制
public static Set<Secret> knownSecrets;
public void initialize() {
    knowsSecrets = new HashSet<Secret>();
}

上边代码示例中,调用initialize方法,发布了knowSecrets对象。当你向knowSecrets中添加一个Secret时,会同时将Secret对象发布出去,原因是可以通过遍历knowSecrets获取到Secret对象的引用,然后进行修改。

2. 通过非静态(私有)方法

代码语言:javascript复制
class UnsafeStates {
    private String[] states = new String[]{"AK", "AL"};
    public String[] getStates() {
        return states;
    }
}

以这种方式发布的states会出问题,任何一个调用者都能修改它的内容。数组states已经逸出了它所属的范围,这个本应该私有的数据,事实上已经变成共有的了。

3. this逸出

代码语言:javascript复制
public class ThisEscape {
  public ThisEscape(EventSource source) {
        source.registerListener(new EventListener() {
              public void onEvent(Event e) {
                    doSomething(e);
              }
        });
  }
}

在上边代码中,当我们实例化ThisEscape对象时,会调用source的registerListener方法时,便启动了一个线程,而且这个线程持有了ThisEscape对象(调用了对象的doSomething方法),但此时ThisEscape对象却没有实例化完成(还没有返回一个引用),所以我们说,此时造成了一个this引用逸出,即还没有完成的实例化ThisEscape对象的动作,却已经暴露了对象的引用,使其他线程可以访问还没有构造好的对象,可能会造成意料不到的问题。

通过上述示例,个人理解,对逸出的概念应该定义为:

一个对象,超出了它原本的作用域,而可以被其它对象进行修改,而这种修改及修改的结果是无法预测的。换句话说:一个对象发布后,它的状态应该是稳定的,修改是可被检测到的。如果在其它线程修改(或做其它操作)一个对象后导致对象的状态未知,就可以说这个对象逸出了。

总之,一个对象逸出后,不论其它线程或对象是否使用这个逸出的对象都不重要,重要的是,被误用及被误用后的未知结果的风险总是存在的。

PS 书中给出了避免this逸出的方法:

代码语言:javascript复制
public class SafeListener {
  private final EventListener listener;

  private SafeListener() {
    listener = new EventListener() {
          public void onEvent(Event e) {
                doSomething(e);
          }
    };
     }

  public static SafeListener newInstance(EventSource source) {
    SafeListener safe = new SafeListener();
    source.registerListener(safe.listener);
    return safe;
  }
}

在这个构造中,我们看到的最大的一个区别就是:当构造好了SafeListener对象之后,我们才启动了监听线程,也就确保了SafeListener对象是构造完成之后在使用的SafeListener对象。

对于这样的技术,书里面也有这样的注释: 具体来说,只有当构造函数返回时,this引用才应该从线程中逸出。构造函数可以将this引用保存到某个地方,只要其他线程不会在构造函数完成之前使用它。

安全发布对象

  • 在静态初始化函数中初始化一个对象引用
  • 将对象的应用保存到volatile类型的域或者AtomicReferance对象中
  • 将对象的引用保存到某个正确构造对象的final类型域中
  • 将对象的引用保存到一个由锁保护的域中。
代码语言:javascript复制
/**
 * 懒汉模式(线程不安全)
 * 单例实例在第一次使用时进行创建
 */
@NotThreadSafe
public class SingletonExample1 {

    // 私有构造函数
    private SingletonExample1() {

    }

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

    // 静态的工厂方法
    public static SingletonExample1 getInstance() {
        // 这里同时有两个线程进入就可能同时初始化两个对象
        if (instance == null) {
            instance = new SingletonExample1();
        }
        return instance;
    }
}

懒汉模式本身是线程不安全的,如果想要实现线程安全可以通过synchronized关键字实现:

代码语言:javascript复制
/**
 * 懒汉模式
 * 单例实例在第一次使用时进行创建
 */
@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;
    }
}

但此中方式不推荐使用,应该它通过同一时间内只允许一个线程来访问的方式实现线程安全,但是却带来了性能上面的开销。

我们可以通过以下方式来实现线程安全:

懒汉模式 -》 volatile 双重同步锁单例模式

代码语言:javascript复制
/**
 * 懒汉模式 -》 双重同步锁单例模式
 * 单例实例在第一次使用时进行创建
 */
@ThreadSafe
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() 初始化对象

    // 单例对象 volatile   双重检测机制 -> 禁止指令重排
    private  volatile  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;
    }
}
代码语言:javascript复制
/**
 * 饿汉模式
 * 单例实例在类装载时进行创建
 */
@ThreadSafe
public class SingletonExample2 {

    // 私有构造函数
    private SingletonExample2() {

    }

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

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

饿汉模式不会有线程问题,但是在类加载时实例化对象。使用时要考虑两点:

  1. 私有构造函数在使用时没有过多的逻辑处理(销毁性能,慢)
  2. 这个对象一定会被使用(浪费资源)

在静态代码块中实例化一个对象:

代码语言:javascript复制
/**
 * 饿汉模式
 * 单例实例在类装载时进行创建
 */
@ThreadSafe
public class SingletonExample6 {

    // 私有构造函数
    private 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());
        System.out.println(getInstance().hashCode());
    }
}

枚举模式:

代码语言:javascript复制
/**
 * 枚举模式:最安全
 */
@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;
        }
    }
}

0 人点赞