对象的共享(第三章)

对象的共享

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的语义不足以确保递增(++)操做的原子性。ide

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

使用volatile变量的时机:函数

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

2.发布与逸出

  • 发布(Publish)一个对象:使对象可以在当前做用域以外的代码中使用。this

    • 当发布一个对象时,在该对象的非私有域中引用的全部对象一样会被发布
    • 当发布某个对象时,可能会间接发布其余对象,如发布一个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变量的可见性保证还确保了其余线程能看到最新的值。code

  • 栈封闭:只能经过局部变量访问对象
  • 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. 将对象的引用保存到一个由锁保护的域中

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

静态初始化器由JVM在类的初始化阶段执行,因为JVM内部存在着同步机制,所以经过这种方式初始化的任何对象均可以被安全地发布。

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

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

Conclusion

在并发程序中使用和共享对象时,可使用一些实用策略:
    1. 线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,而且只能由这个线程修改
    2. 只读共享。在没有额外同步的状况下,共享的只读对象能够由多个线程并发访问,任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象
    3. 线程安全共享。线程安全的对象在其内部实现同步,所以多个线程能够经过对象的公有接口来进行访问而不须要进一步的同步
    4. 保护对象。被保护的对象只能经过持有特定的锁来访问。保护对象包括封装在其余线程安全对象中的对象,以及已发布的而且由某个特定锁保护的对象。
相关文章
相关标签/搜索