Java Concurrency in Practice ---对象的共享

这一小节主要讲如何共享和发布对象。


2.1 可见性

可见性指的是在多线程中,如果一个线程对某个变量做出了改变,其他线程要能够看得见这种改变。

首先,来看一个没有同步机制的共享变量的例子:

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private 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;
    }
}

这个例子有主线程和读线程,主线程启动读线程后,并给两个变量赋值。这两个变量主线程和读线程都可以访问。读线程被启动后,等到 ready 为 true 时,将 number 输出。

乍一看可能以为这个例子理所当然应该输出 42,其实不然。由于没有使用同步机制,所以无法保证主线程写入的 number = 42 以及 ready = true 对读线程来说是可见的。所以程序的结果很可能是输出 0 或者根本无法终止。

读线程可能看不到 ready 的值为 true,因此程序可能无法终止。
读线程可能看到了 ready 的值,但没有看到 number 的值,这种现象是编译器的重排序造成的,所以也可能输出 0。

所以,想要程序具有正确的可见性,或者说想要线程读到的数据不是失效的数据,那么就要采取同步措施。


2.2 Volatile 变量

volatile 变量的作用是用来确保将变量的更新操作通知到其他线程。

当使用 volatile 变量后,编译器与运行时都会注意到这个变量是共享的,而不会将该变量上的操作与其他内存操作一起重排序,保证在读取 volatile 类型的变量时总会返回最新写入的值。

因此,可以将 volatile 变量理解为一种比 synchronized 关键字更轻量级的同步机制。

volatile 变量也有局限,它不足以保证操作的原子性,只能确保可见性。

因此,当且仅当满足以下条件时,才应该使用 volatile 变量:

  • 对变量的写入不依赖变量的当前值,或者你能确保只能单个线程更新变量的值。
  • 该变量不会与其他状态一起纳入不变性条件。
  • 在访问变量时不需要加锁。

2.3 发布与逸出

发布是指使对象能够在当前作用域之外的代码中使用。
逸出是指某个不该发布的对象被发布。

最简单的一种发布对象方法:将对象的引用保存到一个共有的静态变量中

public static Set knownSecrets;

public void initialize() {
    knownSecrets = new HashSet();
}

上面的操作中 initialize 方法实例化一个新的 HashSet 对象,并将对象的引用保存到 knownSecrets 中以发布该对象。

下面看一个逸出的例子,也是另外一种发布对象的例子:

class UnSafeStates {
    private String[] states = new String[] {"a", "b", "c"};

    public string[] getStates() { return states; }
}

上面的操作通过返回一个对象的引用,来发布该对象。在这里返回的 states 可能会被其他的调用者修改,而它本身是 private 域的,因此我们发布了一个不该发布的对象,会产生逸出。

接下来看最后一种发布对象的方法:发布一个内部的类实例

public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(new EventListener() {
            public void onEvent(Event e) {
                dosomething(e);
            } 
        });
    }
}

上面的操作中,当 ThisEscape 发布 EventListener 时,也隐含地发布了 ThisEscape 实例本身,因为在这个内部类的实例中包含了对 ThisEscape 实例的隐含引用,即外部的 ThisEscape 实例逸出了。

看一个使用公共的工厂方法和私有的构造函数来发布对象并且不会造成逸出的例子:

public class SafeListener {
    private final EventListener listener;

    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
                dosomething(e);
            }
        }
    }

    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }
}

2.4 线程封闭

线程封闭是指在单线程中访问数据,这样不需要同步,同时也是线程安全的。

线程封闭的一种常见应用是 JDBC 的 Connection 对象。

有几种线程封闭技术:

  • Ad-hoc 线程封闭:是指维护线程封闭性的职责完全由程序实现来承担。少使用。
  • 栈封闭:只有通过局部变量才能访问对象。
  • ThreadLocal 类:该类能够使线程中的某个值与保存值的对象关联起来,也是最常用的线程封闭技术。

2.5 不变性

如果某个对象在被创建后其状态就不能被修改,这个对象就称为不可变对象。不可变对象一定是线程安全的。

当满足以下条件时,对象是不可变的:

  • 对象创建后其状态就不能修改。
  • 对象的所有域都是 final 类型的。
  • 对象时正确创建的,即在对象创建期间,this 引用没有逸出。

2.6 安全发布的常用模式

可变对象必须通过安全的方式来发布,这意味着在发布和使用该对象的线程时都必须同步。

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:

  • 在静态初始化函数中初始化一个对象引用。
  • 将对象的引用保存到 volatile 类型的域或者 AtomicReferance 对象中。
  • 将对象的引用保存到某个正确构造对象的 final 域中。
  • 将对象的引用保存到一个由锁保护的域中。

通常,要发布一个静态构造的对象,最简单的方法是使用静态的初始化器。

public static Holder holder = new Holder(42);

静态的初始化器由 JVM 在类的初始化阶段执行。

你可能感兴趣的:(Java,java,并发,多线程,线程)