首页 > 代码库 > java 线程(三) :对象的共享

java 线程(三) :对象的共享

可见性:

     我们希望确保一个线程修改了对象的状态后,其他线程能够看到发生的状态变化。
     例:在没有同步的情况下共享变量
public class NoVisibility {
     private static boolean ready;
     private static int number;
     public static class ReaderThread extends Thread {
         public void run(){
         while(!ready)
            Thread.yield();
         System.out.println(number);
         }
     }
     public static void main(String []args) {
         new ReaderThread( ).start();
         number = 42;
         ready = true;
     }
}
     以上程序在运行时,可能会持续循环下去,也有可能会输出令人匪夷所思的0。原因是在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。
     加锁的含义不仅仅局限于互斥行为,还包括内存的可见性。为了确保所有线程都能看到该共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
     非原子的64位操作:当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性。最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(double和long)。java内存模型要求,变量的读取操作盒写入操作都必须是原子操作,但对于非volatile类型的long和double变量,jvm允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能读取到某个值的高32位和另一个值的低32位,因此在多线程程序中使用共享且可变的long和double等类型时变量也是不安全的,除非用关键字volatile来声明它们,或者用锁保护起来。
     volatile: 把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
     例: 使用volatile的一种典型用法:检查某个状态标记以判断是否退出循环
     volatile boolean asleep;
     ...
          while(!asleep)
               countSomeSheep();
      我们也可以使用锁来确保asleep更新操作的可见性,但这将使代码变得更加复杂。
      加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。(确保每一次修改都直接修改到内存之中)
        在使用volatile要格外小心,例如:volatile的语义不足以确保递增操作(count++)的原子性,除非你能确保只有一个线程对变量执行写操作。(原子变量提供了"读-改-写"的原子操作,并且通常用做一种"更好的volatile变量")
      当且仅当满足以下所有条件时,才应该使用volatile变量:
  • 对变量的写入操作不依赖变量的当前值(如count++),或者你能确保只有单个线程更新变量的值
  • 该变量不会与其他状态变量一起纳入不变性条件中
  • 在访问变量时不需要加锁(因为volatile只确保可见性)

发布和逸出:

     "发布"一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。其实就是对象的引用被散播开去。当某个不应该发布的对象被发布时,这种情况就称为逸出。
     逸出的一个例子:
      不要在构造过程中使this引用逸出,因为此时对象的状态还不完全,而且不清楚外界会对此对象做什么。
       错误的示范:隐式的使用this引用逸出
     public class ThisEscape {
          public ThisEscape(EventSource source) {
               source.registerListener(
                    new EventListener() {
                         public void onEvent(Event e) {
                              doSomething(e);
                         }
                    } );
     }
} 
     如果我们想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法(Factory Method),从而避免不正确的构造过程。
     public class SafeListener {
          private final EventListener listner;
          private SafeListener() {
               listner = new EventListener() {
                    public void onEvent(Event e) {
                         doSomething( e );
                    }
               };
          }
          public static SafeListener newInstance( EventSource source ) {
               SafeListener safe = new SafeListener( );
               souce.registerListener( safa.listener );
               return safe;
          }
     }

线程封闭:

     当某个对象封闭在一个线程之中时,这种做法将自动实现线程安全性。这是实现线程安全性的最简单方式之一。
     Ad-hoc线程封闭:维护线程封闭性的职责完全由程序实现来承担(完全靠程序员自觉),没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。这种方式是非常脆弱的,很多时候线程的状态都是外界传入的,你不能保证外界对该状态做了什么。volatile可以实现一种特殊的线程封闭(可以起到一个标示Ad-hoc线程封闭的作用,但不是强制性的),只要能确定只有一个线程对该对象执行写的操作,那么就可以安全地在这些共享的volatile变量上执行"读取-修改-写入"的操作。在这种情况下,相当于将修改操作封闭在单个线程中以防止发生竟态条件,并且volatile变量的可见性还确保了其他线程能看到最新的值。
     栈封闭:使用局部变量的对象。     
     ThreadLocal:会为每一个线程创建一个独立的副本,并在线程终止后,这些值会作为垃圾回收。例子如下:
private static ThreadLocal<Connection> connectionHolder
     = new ThreadLocal<Connnection>( ) {
          public Connection initialValue( ) {
               return DriverManager.getConnection(DB_URL);
          } ;
     public static Connection getConnection( ) {
          return connectionHolder.get( );
     }
}

不变性:

     如果某个对象在创建后其状态就不能被修改,那么这个对象就称为不可变对象。
     不可变对象一定是线程安全的。
     当满足以下条件时,对象才是不可变的:
  • 对象是正确创建的(在对象的创建期间,this引用没有逸出)
  • 对象创建之后其状态就不能修改
  • 对象的所有域都是final类型
     Final域:final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步。即使对象是可变的,通过将对象的某些域声明为final类型,仍然可以简化对状态的判断,因此限制对象的可变性也就相当于限制了该对象可能的状态集合。
     使用volatile类型发布不可变对象:
     我们第2章中曾使用的因式分解Servlet将执行两个原子操作:更新缓存的结果,以及通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的因数分解结果。每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据。
@Immutable
class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;
    public OneValueCache(BigInteger i, BigInteger[] factors) {
        lastNumber = i;
        lastFactors = factors;
    }
    public BigInteger[] getFactors( BigInteger i ) {
         if(lastNumber == null || !lastNumber.equals(i)) {
              return null;
         } else {
              return Arrays.copyOf(lastFactors, lastFactors.length );
         }
    }
}
public class VolatileCachedFactorizer implements Servlet {
    private volatile OneValueCache cache = new OneValueCache(null, null);
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors( i );
        if(factors == null) {
             factors = factor(i);
             cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse(resp, factors);
    }
}
     通过将多个状态定义为final并且独立出来绑定在一个类中,来保持其状态的不变性条件,然后在外围使用时,通过volatile来保证其可见性。这样的话, 可以避免锁的使用。

java 线程(三) :对象的共享