发布对象:使对象可以在当前作用域以外的代码中使用。比如将一个对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中
逸出:当某个不应该发布的对象被发布时,就称为逸出
逸出
内部状态逸出
public class UnsafeStates {
private String[] states = {"a","b","c"};
public String[] getStates() {
return states;
}
}
- 在这个示例中,数组states 已经逸出了它所在的作用域,因为这个本应是私有的变量已经被发布了。
- 如果按照上述方式来发布states,就会出现问题,因为任何调用者都能修改这个数组的内容。
对象逸出
public class Escape {
private int thisCannBeEscape = 0;
public Escape(){
new InnerClass();
}
private class InnerClass{
public InnerClass(){
log.info(Escape.this.thisCannBeEscape);
}
}
public static void main(String[] args) {
new Escape();
}
}
- 这个内部类的实例里面包含了对封装实例的私有域(thisCannBeEscape)对象的引用,在对象没有被正确构造完成之前就会被发布,可能有不安全的因素在里面,会导致this引用在构造期间溢出
- 上述代码在函数构造过程中启动了一个线程(打印日志log方法)。在构造方法中无论是隐式的启动还是显式的启动,this引用都会被新线程共享。
- 如果要在构造函数中创建线程那么不要立即启动它,而是应该采用一个专有的start或者初始化的方法统一启动线程
- 可以采用工厂方法和私有构造函数来完成对象创建和监听器的注册等等,这样才可以避免错误
发布对象
线程不安全的对象发布
1.懒汉式(未加锁)
public class UnsafeLazyInitialization {
private static Resource resource;
public static Resource getInstance() {
if (resource == nu11)
resource = new Resource(); // 不安全的发布
return resource;
}
}
- 当线程A调用getInstance后,线程B随后调用getInstance,但是因为没有同步线程B可能会读取到一个失效的值或者A刚判断完resource==null还未执行赋值时到B紧接进行判断那么A、B都会进行赋值,就会产生错误
2.懒汉式(双重检查加锁)
public class DoubleCheckedLocking {
private static Resource resource;
private DoubleCheckedLocking(){
}
public static Resource getInstance() {
if (resource == null) {
synchronized (DoubleCheckedLocking.class) {
if (resource == null) {
resource = new Resource();
}
}
}
return resource;
}
}
- 当线程A调用getInstance后,在执行resource = new Resource();是可能会重排导致线程B可能会读取到一个失效的值就会产生错误
线程安全的发布
1.懒汉式(加锁)
public class UnsafeLazyInitialization {
private static Resource resource;
public static synchronized Resource getInstance() {
if (resource == nu11)
resource = new Resource();
return resource;
}
}
- synchronized可以使所有线程以串行方式执行getInstance,并保证可见性
2.懒汉式(双重检查加锁+volatile)
public class DoubleCheckedLocking {
private static volatile Resource resource;
private DoubleCheckedLocking(){
}
public static Resource getInstance() {
if (resource == null) {
synchronized (DoubleCheckedLocking.class) {
if (resource == null) {
resource = new Resource();
}
}
}
return resource;
}
}
- volatile可以防止重排序
3.饿汉式
public class EagerInitialization {
// 私有构造函数
private EagerInitialization() {
}
private static Resource resource = new Resource() ;
public static Resource getResource(){ return resource;}
}
- 单例实例是在类装载的时候进行创建,只会被执行一次,所以它是线程安全的。
- 缺陷:如果构造函数中有着大量的事情操作要做,那么类的装载时间会很长,影响性能。如果只是做的类的构造,却没有引用,那么会造成资源浪费
- 适用场景为:(1)私有构造函数在实现的时候没有太多的处理(2)这个类在实例化后肯定会被使用
4.枚举式
public class SingletonExample {
private SingletonExample() {
}
public static SingletonExample getInstance() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private SingletonExample singleton;
Singleton() {
singleton = new SingletonExample();
}
public SingletonExample getInstance() {
return singleton;
}
}
}
- 由于枚举类的特殊性,枚举类的构造函数Singleton方法只会被实例化一次,且是这个类被调用之前。这个是JVM保证的。
- 对比懒汉与饿汉模式,它的优势很明显。
5.占位类模式
public class ResourceFactory{
private static class ResourceHolder{
public static Resource resource = new Resource () ;
}
public static Resource getResource () {
return ResourceHolder.resource;
}
}
-
在初始器中采用了特殊的方式来处理静态域(或者在静态初始化代码块中初始化的值),并提供了额外的线程安全性保证。静态初始化器是由JVM在类的初始化阶段执行,即在类被加载后并且被线程使用之前。由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间, 内存写入操作将自动对所有线程可见。因此无论是在被构造期间还是被引用时,静态初始化的对象都不需要显式的同步。然而,这个规则仅适用于在构造时的状态,如果对象是可变的,那么在读线程和写线程之间仍然需要通过同步来确保随后的修改操作是可见的,以及避免数据破坏。
-
JVM将推迟ResourceHolder的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化Resource,因此不需要额外的同步。当任何一个线程第一次调用getResource时,都会使ResourceHolder被加载和被初始化,此时静态初始化器将执行Resource的初始化操作。