如何安全的发布一个对象

要正确的发布一个对象首先要解决3个问题:
1.发布的对象只需要被它需要被看见的线程或其它对象看见
2.避免逸出问题
3.避免其它线程拿到未初始化完全的对象

什么是发布

发布一个对象是指,使对象能够在当前作用域之外的代码中使用。比如,将创建的对象保存到容器中,也可能通过某个方法返回对象的引用,或者将引用传递到其他类的方法中。

什么是逸出

逸出是指某个不应该发布的对象被发布,被其他线程或对象看见。

对于问题1:

我们提出线程封闭的概念:

如果仅在单线程内访问数据,就不需要同步,这种技术被称为线程封闭

栈封闭

使用局部变量,利用局部变量的天然属性,其本身就是封闭在运行线程中,但是注意一点就是在编码过程中避免通过方法或者容器将对象逸出。

ThreadLocal类(重点)

这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get和set等方法或访问接口,这些方法为每个使用该变量的线程存有一份独立的副本,因此get总是返回由当前线程在调用set时设置的新值。在JDBC连接对象,以及Mybatis中ErrorContext等都是ThreadLocal类。

对于问题2:

1.如果在编码中不希望一个变量被其它使用,就不要通过方法或者容器让对象逸出

private User user;
public User getUser(){
   return this.user;
}//如果本意不想让外部获取user,那么这个方法就会让对象逸出
pubic Map getUsers(){
   map.put("u1",user);
   return map;
}//不经意发布包含user的map,在实际情况中可能

2.隐式的使this逸出

  1. 在构造函数中启动一个线程
  2. 在构造函数中发布一个内部的类实例
    隐式的使this逸出问题在于考虑到对象未初始化完全,使得其它对象或现程未取得完整初始化的对象而发生未知错误。
//摘自于Java 并发编程实战
public class ThisEscape {  
    private String name = null;  
    public ThisEscape(EventSource source) {  
        source.registerListener(new EventListener() {  
        /*在构造函数注册监听器,监听器属于另一个线程*/
            public void onEvent(Event event) {  
                doSomething(event);//等效于this.doSomething(event),this就是在这里逸出的
            }  
          });  
        name = "TEST";  
    }  
    protected void doSomething(Event event) {  
        System.out.println(name.toString());  
    }  
}  

故避免此类问题的最好方式就是不要在构造函数里做这些事情。当然如果需要也不要在类似于监听器这样的操作里调用外部类的方法。

对于问题3:

经典双重检查问题讨论:
DCL的真正问题在于:当在没有同步的情况下读取一个共享对象时,可能发生的最糟糕的事情是看到一个失效值,这也就是为什么很多时候强调在读操作和写操作都应加上锁的原因

class Foo {
  private static Helper helper = null;
  public static Helper getHelper() {
    if (helper == null)
      synchronized(Foo.class) {
        if (helper == null)
          helper = new Helper();
      }
    return helper;
    }
  // other functions and members...
  }

假设两个线程A,B,A线程执行写操作,由于helper没有读操作与写操作并不满足Happen-before操作,对于helper = new Helper(),有可能先进行读操作获取对象的地址,然后进行初始化操作,此时若线程B在未初始化话之前读取helper则有可能读到不完整的对象。产生隐患。
解决的方法就是使读写满足Happen-before
方案一:volatile规则
volatile变量规则:对volatile变量的写入操作必须在对该变量的读操作之前

class Foo {
  private static volatile Helper helper = null;//使用volatile修饰使resource的读操作与写操作具有happen-before规则
  public static Helper getHelper() {
    if (helper == null)
      synchronized(Foo.class) {
        if (helper == null)
          helper = new Helper();
      }
    return helper;
    }
  // other functions and members...
  }

方案二:同步读写操作(源操作只同步了写操作)

class Foo {
  private static Helper helper = null;
  public static synchronized Helper getHelper() {
    if (helper == null) {
        if (helper == null)
          helper = new Helper();
      }
    return helper;
    }
  // other functions and members...
  }

方案三:利用JVM锁机制提前初始化

在初始器中采用了特殊的方式来处理静态域,并提供了额外的线程安全性保证。静态初始化器是由JVM在类的初始化阶段执行即在类被加载后并且在被线程使用之前,由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。因此无论是在构造期间还是被引用时,静态初始化的对象都不需要显式的同步。然而,这个规则只适用于构造时的状态,也就是说保证构造的完整性,并不保证之后的操作的线程安全。如果这个类不是线程安全的,仍需要适当的同步。

class Foo {
  private static Helper helper = new Helper();//在很多框架中都会这么用
  public static Helper getHelper() {
    return helper;
    }
  }

方案4:利用JVM锁机制延迟初始化占位类模式

class Foo {

  private static class ResourceHolder(){
     private static Helper helper = new Helper();//在很多框架中都会这么用
  }
  public static Helper getHelper() {
    return ResourceHolder.helper;
    }
  }

补充:利用初始化过程中的安全性

初始化过程中的安全性保证,对于被正确构造的对象,所有的线程都能看到由构造函数为对象给各个final域设置的正确值,而不管采用何种方式来发布对象。而且,对于可以通过被正确构造对象中的某个final域到达的任意变量(例如某个final数组中的元素,或者由一个final域引用的HashMap的内容)将同样对于其它线程是可见的。对于含有final域对象,初始化安全性可以防止对对象的初始引用被重排序到构造过程之前。

public class SafeState{
  private final Map<String,String> states;
  public SafeState(){
     states = new HashMap<String,String>();//final域写入操作
     state.put("a","a");//final域可达的变量,仍然具备可见性,并且不会被重排序到构造函数之后

  }
}

根据上面所讨论的,可以总结出以下安全发布的常用模式:

  1. 在静态初始化函数中初始化一个对象引用(private static Helper helper = new Helper())
  2. 将对象的引用保存到volatile类型的域或者AtomicReferance对象中(利用volatile happen-before规则)
  3. 将对象的引用保存到某个正确构造对象的final类型域中(初始化安全性)
  4. 将对象的引用保存到一个由锁保护的域中(读写都上锁)

猜你喜欢

转载自blog.csdn.net/chenbinkria/article/details/79887244