读书笔记-----Java并发编程实战(二)对象的共享

 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变量而非锁时,某些习惯用法(idiom)更加易于编码和阅读。此外,volatile变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile变量还可以提供优于锁的性能优势。

但如果在代码中依赖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()));
}
}
输出结果[AL,KL] 
            [AL,c]
 
 
    以上代码通过public访问级别发布了类的域,在类的外部任何线程都可以访问这些域,这样发布对象是不安全的,因为我们无法假设,其他线程不会修改这些域,从而造成类状态的错误。还有一种逸出是在构造对象时发生的,它会使类的this引用发生逸出,从而使线程看到一个构造不完整的对象,如下面代码所示:
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();
}
}
上面的内部类的实例包含了对封装实例隐含的引用,这样在对象没有被正确构造之前,就会被发布,有可能会有不安全因素。 
 
一个导致this引用在构造期间逸出的错误,是在构造函数中启动一个线程,无论是隐式启动线程,还是显式启动线程,都会造成this引用逸出,新线程总会在所属对象构造完毕前看到它。所以如果要在构造函数中创建线程,那么不要启动它,而应该采用一个专有的start或initialize方法来统一启动线程。我们可以采用工厂方法和私有构造函数来完成对象创建和监听器的注册,这样就可以避免不正确的创建。记住,我们的目的是,在对象未完成构造之前,不可以将其发布。 
 
正确的方式
 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;
}
}
不要在构造过程中使用this逸出。构造函数可以将this引用保存到某个地方,只要其他线程不会在构造函数完成之前使用它。
 
线程封闭
1.栈封闭。
即使用局部变量,对于基本类型的局部变量一定不会破坏栈封闭性。由于任何方法都无法获得对基本类型的引用,确保了基本类型的局部变量始终封闭在线程内。
在维持对象引用的栈封闭时,需要额外的操作防止引用的对象不被逸出。
2.ThreadLocal类
这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set方法为每个使用该变量的线程都存有一份独立的副本。
ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。
例子:
 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对象中,当线程终止后,这些值会作为垃圾回收。

不变性
不可变的对象一定是线程安全的。
不可变对象一定满足以下条件:
1.对象创建后其状态(成员变量)就不能修改。
2.对象的所有域都是final类型
3.对象是正确创建的(this引用没有逸出)
例子:
@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、  可变对象必须要安全发布,同时必须要线程安全或者是被锁保护;

 
上一篇:Blend4 的安装和配置


下一篇:Paxos算法小结