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;
}
}
在两个线程之间共享访问变量是不安全的,执行结果顺序没法预料。
我们常用的get和set方法有时是非线程安全的
@NotThreadSafe
public class MutableInteger{
private int value;
public int get(){return value;}
public void set(int value){this.value = value;}
}
如果某个线程调用了set,那么另一个正在调用get的线程可能会看到更新后的value值,也可能看不到
将其变成线程安全的类
@ThreadSafe
public class SynchronizedInteger{
@GuardBy("this") private int value;
public synchronized int get(){return value;}
public synchronized void set(int value){this.value = value;}
}
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
Volatile变量
用来确保将变量的更新操作通知到其他线程。volatile变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。
Volatile变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile变量的最新值。Volatile变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。
但如果在代码中依赖Volatile变量来控制状态的可见性,通常比使用锁的代码更加脆弱,也更难以理解。
仅当volatile变量能简化代码实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,就不要使用volatile变量。volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标示一些重要的程序生命周期事件的发生(例如,初始化或关闭)
典型用法:检查某个状态标记以判断是否退出循环。
volatile boolean asleep;
...
while(!asleep)
countSomeSheep();
volatile不足以确保递增操作的原子性。
加锁机制既可以确保可见性又可以确保原子性。而volatile变量只能确保可见性。
当且仅当满足以下条件时,才应该使用volatile变量
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
- 该变量不会与其他状态的变量一起纳入不变性条件中
- 访问变量时不需要加锁
class UnSafeStates{
private String[] states={"AK","AL"};
public String[] getStates(){ return states; }
public static void main(String[] args) {
UnSafeStates safe = new UnSafeStates();
System.out.println(Arrays.toString(safe.getStates()));
safe.getStates()[1] = "c";
System.out.println(Arrays.toString(safe.getStates()));
}
}
public class Escape{
private int thisCanBeEscape = 0;
public Escape(){
new InnerClass();
}
private class InnerClass {
public InnerClass() {
//这里可以在Escape对象完成构造前提前引用到Escape的private变量
System.out.println(Escape.this.thisCanBeEscape);
}
}
public static void main(String[] args) {
new Escape();
}
}
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;
}
}
private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>(){
public Connection initialValue(){
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection(){
return connectionHolder.get();
}
将JDBC的连接保存到ThreadLocal对象中,调用get方法使得每个线程都拥有属于自己的连接副本。
可以将ThreadLocal<T>看做Map<Thread,T>对象。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾回收。
@Immutable
public final class ThreeStooges{
private final Set<String> stooges = new HashSet<String>();
public ThreeStooges(){
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Ted");
}
public boolean isStooge(String name){
return stooges.contains(name);
}
}
不可变性并不等于将对象所有的域都声明为final类型,即使对象中所有的域都是final类型,这个对象仍然也可以是可变的,因为在final域中可以保持对可变对象的引用。
对象创建出来后(通过构造方法)无论如何对此对象进行非暴力操作(不用反射),此对象的状态(成员变量的值)都不会发生变化,那么此对象就是不可变的,相应类就是不可变类,跟是否用 final 修饰没关系,final 修饰类是防止此类被继承。
final域
final域不能修改的(尽管如果final域指向的对象是不可变的,这个对象仍然可被修改),然而它在Java内存模式中还有着特殊语义。final域使得确保被始化安全性成为可能,初始化安全性让不可变性对象不需要同步就能*地被访问和共享。
正如“将所有的域声明为私有的,除非它们需要更高的可见性”一样“将所有的域声明为final型,除非它们是可变的”,也是一条良好的实践。
示例:使用volatile发布不可变的对象
下面是一个不可变对象,进(构造时传进的参数)出(使用时)都对状态进行了拷贝。因为BigInteger是不可变的,所以直接使用了Arrays.copyOf来进行拷贝了,如果状态所指引的对象不是不可变对象时,就不能使用这项技术了,因为外界可以对这些状态所指引的对象进行修改,如果这样只能使用new或深度克隆技术来进行拷贝了。
下面开始发布上面不可变对象,其中volatile起关键作用,如果没有它,即使OneValueCache是不可变类,其最新的状态也无法被其他线程可见。
不可变对象与初始化安全
Java内存模型为共享不可变对象提供了特殊的初始化安全性的保证,即对象在完全初始化之后才能被外界引用。
即使发布对象引用进没有使用同步,不可变对象仍然可以被安全地访问。为了获得这种初始化安全性的保证上,应该满足所有不可变性的条件:不可修改的状态、所有域都是final类型的以及正确的构造。
不可变对象呆以在没有额外同步的情况下,安全地用于任意线程;甚至发布它们时亦不需要同步。
可变对象与安全发布
如果一个对象不是不可变的,它就必须要被安全的发布,通常发布线程与消费线程都必须同步化。我们要确保消费线程能够看到处于发布当时的对象状态。
为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确创建的对象可以通过下列条件安全地发布:
1、 通过静态初始化器初始化对象引用;
2、 将它的引用存储到volatile域或AtomicReference;
3、 将它的引用存储到正确创建的对象的Final域中;
4、 或者将它的引用存储到由锁正确保护的域中。
线程安全中的容器提供了线程安全保证,正是遵守了上述最后一条要求。
通常,以最简单和最安全的方式发布一个被静态创建的对象,就是使用静态初始化器:
public static Holder holder = new Holder(42);
静态初始化器由JVM在类的初始阶段执行,由于JVM内在的同步,该机制确保了以这种方式初始化的对象可以被安全地发布。
对象可变性与安全发布
发布对象的必要条件依赖于对象的可变性:
1、 不可变对象可以通过任意机制发布;
2、 高效不可变对象(指对象本身是可变的,但只要发布后状态不再做修改)必须要安全发布;
3、 可变对象必须要安全发布,同时必须要线程安全或者是被锁保护;