Java多线程——不变性与安全发布

1、不变性

  某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象,不可变对象一定是线程安全的。不可变对象很简单。他们只有一种状态,并且该状态由构造函数来控制。

  当满足以下条件时,对象才是不可变的:(1)、对象创建以后其状态就不能改变;(2)、对象的所有域都是final类型;(3)、对象是正确创造的(在对象创建期间,this引用没有溢出)。

1.1 final域

  关键字final用于构造不可变对象,final类型的域是不能修改的(但是final域所引用的对象是可变的,那么这些引用的对象是可以修改的),即使对象是可变的,通过将可变对象的某些域声明为final类型,相当于告诉维护人员这些域是不可变化的。

2、正确发布一个对象

  正确发布一个对象遇到的两个问题:(1)引用本身要被其他线程看到;(2)对象的状态要被其他线程看到。

  在多线程编程中,首要的原则,就是要避免对象的共享,因为如果没有对象的共享,那么多线程编写要轻松得多,但是,如果要共享对象,那么除了能够正确的将构造函数书写正确外,如何正确的发布也是一个很重要的问题。

  我们看下面的代码:

 1 public class Client {
 2     public Holder holder;
 3     
 4     public void initialize(){
 5         holder = new Holder(42);
 6     }
 7 }
 8 
 9 
10 public class Holder {
11     int n;
12     public Holder(int n) {
13         this.n = n;
14     }
15     public void assertSanity() {
16         if(n != n)
17              throw new AssertionError("This statement is false.");
18     }
19 }
View Code

  在Client类中,Holder对象被发布了,但是这是一个不正确的发布。由于可见性问题,其他线程看到的Holder对象将处于不一致的状态,即使在该对象的构成构函数中已经正确的该构建了不变性条件,这种不正确的发布导致其他线程看到尚未创建完成的对象。主要是Holder对象的创建不是原子性的,可能还未构造完成,其他线程就开始调用Holder对象。

由于没有使用同步的方法来却确保Holder对象(包含引用和对象状态都没有)对其他线程可见,因此将Holder成为未正确发布。问题不在于Holder本身,而是其没有正确的发布。上面没有正确发布的可能导致的问题:

  • 别的线程对于holder字段,可能会看到过时的值,这样就会导致空引用,或者是过时的值(即使holder已经被设置了)(引用本身没有被别的线程看到)
  • 更可怕的是,对于已经更新holder,及时能够看到引用的更新,但是对于对象的状态,看到的却可能是旧值,对于上面的代码,可能会抛出AssertionError异常

主要是holder = new Holder(42);这个代码不是原子性的,可能在构造未完成时,其他线程就会调用holder对象引用,从而导致不可预测的结果。

2.1安全发布常用模式

  要安全的发布一个对象,对象的引用和对象的状态必须同时对其他线程可见。一般一个正确构造的对象(构造函数不发生this逃逸),可以通过如下方式来正确发布:

  (1)、在静态初始化函数中初始化一个对象引用

  (2)、将一个对象引用保存在volatile类型的域或者是AtomicReference对象中

  (3)、将对象的引用保存到某个正确构造对象的final类型的域中。

  (4)、将对象的引用保存到一个由锁保护的域。

  

  在线程安全容器内部同步意味着,在将对象放到某个容器中,比如Vector中,将满足上面的最后一条需求。如果线程A将对象X放到一个线程安全的容器中,随后线程B读取这个对象,那么可以确保可以确保B看到A设置的X状态,即便是这段读/写X的应用程序代码没有包含显示的同步。下面容器内提供了安全发布的保证:

  (1)、通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全将它发布给任何从这些容器中访问它的线程。

  (2)、通过将某个元素放到Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchroizedList,可以将该元素安全的发布到任何从这些容器中访问该元素的线程。

  (3)、通过将元素放到BlockingQueue或者是ConcrrentLinkedQueue中,可以将该元素安全的发布到任何从这些访问队列中访问该元素的线程。

  通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态初始化器: public static Holder = new Holder(42);

  静态初始化器由JVM在类的初始化阶段执行,由于JVM内部存在同步机制,所以这种方式初始化对象都可以被安全的发布。对于可变对象,安全的发布之时确保在发布当时状态的可见性,而在随后的每次对象的访问时,同样需要使用同步来确保修改操作的可见性。

  

原文地址:https://www.cnblogs.com/Hxinguan/p/7471461.html