要编写正确的并发程序,关键在于:
在访问共享的可变状态时,须要进行正确的管理。
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; } }
这段代码可能出现的结果java
在没有同步的状况下,编译器,处理器以及运行时等均可能对操做的执行顺序进行一些意想不到的调整,在缺少足够同步的多线程程序中,要想对内存操做执行顺序进行判断,几乎没法得出正确的结论。
当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是在堆上建立对象,可是将对象引用持有在线程的栈内存上。ide
许多事务性的框架功能,经过将事务的上下文保存在静态的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)
。若是对象在构造后能够被修改,那么安全发布只能保证发布当时的可见性。对象的发布需求取决于它的可变性:
在并发程序中使用和共享对象的时候,可使用一些实用的策略,包括:
线程封闭
:线程封闭的对象只能由一个线程拥有,对象封闭在该线程中,而且只能由这个线程修改。线程安全共享
:线程安全的对象在其内部实现同步,所以多个线程能够经过对象的公有接口来进行访问而不须要进一步的同步。保护对象
:被保护的对象只有经过持有特定的锁来访问。保护对象包括封装在其余线程安全对象中的对象,以及发布的而且由某个特定锁保护的对象。