对象的共享(第三章)

对象的共享

1.可见性

在多线程程序中,我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。如果没有同步,那么这种情况就无法实现。

  • 重排序:在没有同步的情况下,编译器、处理器以及运行时都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确地结论。
public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        @Override
        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。因为在缺少同步的情况下,Java内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中,它还允许CPU对操作顺序进行重排序,并将数值缓存在特定的缓存中。

  • 非原子类的64位操作
    Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作,从而破坏了原子性,除非用关键字“volatile”来声明它们或者使用锁来保护它们。
  • 内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。

        加锁的含义不仅仅局限于互斥行为,还包括内存可见性,为了确保所有线程都能看到共享变量的最新值,所有执行读操作或写操作的线程都必须在同一个锁上同步。
    
  • volatile变量
    Java提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与jre都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。

但volatile并不会加锁,因此也就不会产生阻塞行为。所以,volatile变量是一种比synchronized更轻量级的同步机制。
volatile变量通常用作某个操作完成、发生中断或者正太改变的标志,在使用时应非常小心,例如,volatile的语义不足以确保递增(++)操作的原子性。

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

使用volatile变量的时机:

  1. 对变量的写入操作不依赖变量的当前值(避免竞态条件),或者你能确保只有单个线程更新变量的值
  2. 该变量不会与其他状态变量一起纳入不变性条件中
  3. 在访问变量时不需要加锁

2.发布与逸出

  • 发布(Publish)一个对象:使对象能够在当前作用域之外的代码中使用。

    • 当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布
    • 当发布某个对象时,可能会间接发布其他对象,如发布一个List,包含在这个List中的对象也会被发布,如下代码所示
    class UnsafeStates {
        private String[] states = new String[] {"AK","AL"...};
        public String[] getStates() {return states;}
    }
  • 逸出(Escape):一个不该被发布的对象被发布

    • 不要在构造过程中使this引用逸出
      当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态,因此,当对象从构造函数中发布时(如返回一个匿名内部类),只是发布了一个尚未构造完成的对象。这造成了不正确构造。

      常见的使this逸出的操作:
      1. 在构造函数中启动一个线程
        如果想在构造函数中注册一个事件监听器或启动线程,可以使用一个私有的构造函数和一个公共的工厂方法
    class SafeListener {
        private final EventListener listener;
        private SafeListener {
            listener = new EventLIstener() {
            public void onEvent(Event e) {doSomething(e);
            };
        }
        static public SafeListener newInstance(EventSource source) {
            ...//构造、返回
        }                
    }
    2. 在构造函数中调用一个可改写的实例方法(既不是私有方法,也不是final方法)
    
    ### 3.线程封闭
    将数据或对象封闭在一个线程中的技术叫做“线程封闭”。线程封闭将自动实现线程安全性,即使被封闭的对象不是线程安全的。
    Java提供了一些机制来帮助实现线程封闭性,如局部变量和ThreadLocal类,但使用时要确保封闭在线程中的对象不会从线程中逸出。
        在volatile变量上存在一种特殊的线程封闭,只要确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在这些共享的volatile变量上执行“读取--修改--写入”的操作,在这种情况下,相当于将修改操作封闭在单个线程中以防止发生竞态条件,并且volatile变量的可见性保证还确保了其他线程能看到最新的值。
  • 栈封闭:只能通过局部变量访问对象
  • ThreadLocal类:这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal类提供了get与set等方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回当前执行线程在调用set时设置的最新值

    4.不变性

    满足同步需求的另一种方法是使用不可变对象。
    不可变对象总是线程安全的。

    不可变对象不等于将对象中所有的域都声明为final类型,即使对象中所有的域都是final类型的,这个对象也仍然是可变的,因为在final类型的域中可以保存对可变对象的引用。
    不可变对象满足的条件:
    1. 对象创建以后其状态不能修改
    2. 对象的所有域都是final类型(Java中,final除了表示不可变,还表示对象初始化过程是安全的)
    3. 对象是正确创建的(创建是this没有逸出)
    

    5.安全发布

    要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。

    安全发布的常用模式:
    1. 在静态初始化函数中初始化一个对象引用
    2. 将对象的引用保存到volatile类型的域或者AtomicReference对象中
    3. 将对象的引用保存到某个正确构造对象的final类型域中
    4. 将对象的引用保存到一个由锁保护的域中
    
  • 通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器:

  1. static Holder holder = new Holder(42);
静态初始化器由JVM在类的初始化阶段执行,由于JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布。

- 事实不可变对象:如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象(Effectively Immutable Object)”
在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

        对象的发布需求取决于它的可变性:
        1. 不可变对象可以通过任意机制来发布
        2. 事实不可变对象必须通过安全方式来发布
        3. 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来

### Conclusion
        在并发程序中使用和共享对象时,可以使用一些实用策略:
        1. 线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改
        2. 只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象
        3. 线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步
上一篇:取消与关闭(第七章)


下一篇:Python 清理HTML标签类似PHP的strip_tags函数功能(二)