要编写正确的并发程序,关键在于:
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(); new ReaderThread().start(); new ReaderThread().start(); number = 42; ready = true; } } }
这段代码可能出现的结果
在没有同步的情况下,编译器,处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整,在缺乏足够同步的多线程程序中,要想对内存操作执行顺序进行判断,几乎无法得出正确的结论。
当ReaderThread查看ready变量时,可能会得到一个已经失效的值,而且失效值可能不会同时出现:一个线程可能获得了某个变量的最新值,而获得了另一个变量的失效值。
最低安全性:在没有进行同步时读取某个变量,可能会得到一个失效值,但这个值至少是由之前某个线程设置的,而非随机值。这种安全性保证也被称为最低安全性。
非volatile类型的64位数值变量(double和long)
由于Java内存模型要求,变量的读取操作和写入操作必须都是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位操作。
读取volatile相当于进入同步代码块,写入volatile变量相当于退出同步代码块。
确保它们自身状态的可见性
, 确保它们所引用对象状态的可见性
,以及 表示一些重要的声明周期事件的发生
(例如初始化,关闭,循环退出条件等。) /** * 数绵羊 */ volatile boolean asleep; while(!asleep){ countSomeSheep(); }
加锁机制既能保证可见性,又可以确保原子性。而volatile变量只能保证可见性
。 当且仅当满足以下所有条件时,才应该使用volatile变量:
发布(publish)
一个对象,指的是对象能够在当前作用域之外的代码中使用。
例如,将一个指向该对象的引用
但如果在发布时要确保线程安全性,则可能需要同步。发布内部状态可能会破坏封装性,并使程序难以维持不变性条件。
逸出(Escape)
public class UnsafeState { private String[] states = new String[]{ "A","B","C","D","E" }; public String[] getStates(){ return states; } }
线程封闭(Thread Confinement)
JDBC的Connection对象就使用了线程封闭技术。在典型的服务器应用程序中,线程从JDBC连接池中获得一个Connection对象,并且用该对象来处理请求,使用完后再将对象返回给连接池。
Ad-hoc线程封闭是指,维护线程封闭性的职责全部由程序实现来承担。
栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。局部变量的特性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。
维护线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get/set等访问接口和方法,
ThreadLocal是一个创建线程局部变量的类。
使用了ThreadLocal创建的变量只能被当且线程访问,其他线程无法访问和修改。
private void testThreadLocal() { Thread t = new Thread() { ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>(); @Override public void run() { super.run(); mStringThreadLocal.set("123"); mStringThreadLocal.get(); } }; t.start(); }
为ThreadLocal设置初始值的话,则需要重写 initialValue
方法:
ThreadLocal<String> mThreadLocal = new ThreadLocal<String>() { @Override protected String initialValue() { return Thread.currentThread().getName(); } };
本质上ThreadLocal是在堆上创建对象,但是将对象引用持有在线程的栈内存上。
许多事务性的框架功能,通过将事务的上下文保存在静态的ThreadLocal对象中,当需要判断是哪一个事务时,只需要从ThreadLocal对象中读取事务上下文即可。
满足同步需求的另一种方法是使用 不可变对象(Immutable Object)
,之前的例如得到失效数据,丢失更新操作或者观察到某个对象处于不一致的状态等问题,都与多线程试图同时访问一个可变变量有关,如果这个变量是不可变的,那么这些问题也就自然消失了。
不可变对象一定是线程安全的
。
当满足以下条件时,对象才是不可变的:
关键字final用于构造不可变对象。final类型的域是不可修改的,但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的。
在java的内存模型中,final域还有特殊的语义:final域能确保初始化过程的安全性,从而不受限制的访问不可变对象,并在共享这些对象时无需同步。
除非需要更高的可见性,否则应将所有的域都声明为私有域
是个优秀的编程习惯一样, 除非需要某个域是可变的,否则都应该声明为final域
也是一个良好的编程习惯。 在某些情况下,我们需要在多个线程之间共享对象,此时必须确保安全地进行共享
不能指望一个未被完全创建的对象拥有完整性。
public class Holder { private int n; public Holder(int n) { this.n = n; } public void assertSanity() { if (n != n) { throw new AssertionError("this statement is false"); } } }
在发布Holder的线程发布完成之前,Holder域是个失效值,此时的n可能是空引用。
Java内存模型对不可变对象的共享提供了一种特殊的初始化安全性保证。
任何线程都可以在不需要额外同步的情况下安全的访问不可变对象,即使在发布这些对象的时候没有使用同步
如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍需同步。
要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全的发布:
Hashtable
, synchonizedMap
, ConcurrentMap
中,可以安全的将它发布给任何从这些容器访问它的线程(不论直接访问还是迭代器访问) Vector
, CopyOnWriteArrayList
, CopyOnWriteArraySet
, SynchonizedList
, SynchonizedSet
中,可以将元素安全地发布到任何从这些容器中访问该元素的线程。 BlockingQueue
或者 ConcurrentLinkedQueue
中,可以将元素安全地发布到任何从这些队列中访问该元素的线程。 通常,要发布一个静态构造的对象,最简单和最安全的方式就是使用静态的初始化构造器。
public static Holder holder = new Holder(1);
由于静态初始化构造器由JVM在类的初始化阶段执行,在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全的发布。
事实不可变对象(Effectively Immutable Object)
如果对象在构造后可以被修改,那么安全发布只能保证发布当时的可见性。对象的发布需求取决于它的可变性:
在并发程序中使用和共享对象的时候,可以使用一些实用的策略,包括:
线程封闭 线程安全共享 保护对象