《java并发编程实战笔记》
第三章 对象的共享
从第三章开始,(这本书就开始变态起来了),如何共享和发布对象,从而使它们能安全的被多个线程同时访问。
可见性
同步代码块和同步方法可以确保以原子方式执行操作,一种常见的误解,认为关键字synchronized只能用于实现原子性或者确定“临界区”。不仅是只有原子性功能,还有内存可见性,某个线程正在使用对象状态,而另一个线程正在同时修改该状态,有了内存可见性,两个线程之间的对象状态能及时同步更新。
public class NoVisibilty{
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;
}
}
由于重排序现象,ReaderThread 线程很有可能输出的number为0。
失效数据
失效数据更好的解释是:过时的数据。以上面NoVisibilty类为例,main主线程更改number值后,更改后的值只是停留在主线程的高速缓存中,在number值从高速缓存区更新至内存,再更新到ReaderThread 线程这段时间中,number的值相对于ReaderThread 就是过时的、失效的。失效数据可能导致一些令人困惑的故障,例如意料之外的异常、被破坏的数据结构、不精确的计算、无线循环等。
典型的失效数据出现的情况:
@NotThreadSafe
public class MutableInteger{
private int value;
public int get(){
return value;}
public void set(int value){
this.value = value;}
}
由于get和set都是在没有同步的情况下访问value,某个线程调用了set,另外正在调用get的线程可能看到更新后的value,也可能看不到。
正确的代码:
@ThreadSafe
public class MutableInteger{
@GuardedBy("this") private int value;
public synchronized int get(){
return value;}
public synchronized void set(int value){
this.value = value;}
}
最低安全性
当线程在没有同步情况下读取变量,可能会得到一个失效值,但是至少也是某个线程设置的值,而不是随机的。这种安全性保证也称为最低安全性,即至少读这个过程总是原子的、可靠的。最低安全性适用于绝大多数的变量,但是对非volatile类型的64位数值变量(double和long)例外。对于非volatile类型的64位数值变量(double和long)变量,JVM会将64位的读、写操作分解成两个32位操作。当读取一个非volatile类型的long变量时,如果对该变量的读和写操作在不同的线程中,那么很有可能会读取到某个值的高32位和低32位。除非用关键字volatile或者锁保护起来。
volatile变量
当把共享变量声明为volatile类型后,编译器与允许时都会注意到这个变量是共享的,因此不会讲该变量上的操作与其他内存变量操作仪器重排序。相比于加锁机制,volatile不会使用执行线程堵塞,因此volatile变量是一种比sychronized关键字更加轻量化的同步机制。
volatile的典型用法:检查其他线程是否达到自己线程想达到的状态并标记。通常用作某个操作是否完成、终端、或者状态的标志。
例如:
volatile boolean wakeFlag;
...
while(!wakeFlag)
{
.....}
但是需要注意:!!
volatile变量只能保证可见性,并不能保证原子性。使用时不能使用count++这种操作,原子变量有提供自己的“读-改-写”操作方式。举个例子,A线程在执行count++。在读取count值时,B线程也执行了读取count操作,B线程也想执行count++操作 ,最后A B线程执行完后count也只加了1。当且仅当满足下面所有条件是,才可用volatile变量:
- 对变量读写操作不依赖变量本身当前值
- 该变量不会和其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
对象的发布
书上是这么定义:将一个指向该对象的引用保存至其他方法可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。
例如:将新建的HashSet<>对象的引用保存至其他代码可以访问的地方。
public static Set<Secret> knownSecrets;
public void initialize(){
knownSecrets = new HashSet<Secret>();
}
再例如:
对象的逸出
定义:某个不应该发布的对象被发布时,这种情况被称为逸出。
例如:在非私有的方法返回私有变量的引用,内部可变状态逸出了
class UnsafeStates{
private String[] states = new String[] {
"AK","AL",....
};
public String[] getStates() {
return states;}
}
例如:隐式的适用this引用逸出
public class ThisEscape{
public ThisEscape(EventSource source){
source.registerListener(
new EventListener(){
public void onEvent(Event e){
doSomething(e);
}
});
}
}
内部类在编译完成后会隐含保存一个它外围类的引用,"ThisEscape.this”,然而构造函数还没完成,ThisEscape在执行构造函数,其本身对象还没构造完,this引用就立刻间接被传递到其他类的方法中,这当然是不应该的,所以是隐式的逸出。正确的做法是构造函数返回时,this引用才能从线程中逸出,才能在该线程中被其他类的方法使用。
public class SafeListener{
private final EventListener listener;
private SafeListener(){
listener = new EventListener(){
public void onEvent(Event e)
{
doSomething(e);
}; //在构造函数结束之前,外部类this的引用并没有被其他类的方法引用,并没有被发布,所以没有逸出
}
public static SafeListener newInstance(EventSource source){
SafeListener safe = new SafeListener();
souce registerListener(safe.listener);
return safe;
}
}
只有构造函数返回后,外部类的this才能被引用,此时外部类的对象已经构造完整。
线程封闭
当访问共享的可变数据一般都需要使用同步,避免使用同步的方式就是不共享数据,即在单线程中访问数据,这种技术称为线程封闭。线程安全性实现的最简单方式之一。
1、Ad-hoc线程封闭:
指维护线程封闭性的职责完全由程序实现承担,书中没有举例如何实现,一脸懵逼,鬼知道怎么怎么承担会非常脆弱。实际上没有一种特定的修饰符,可以将线程封闭到目标线程上。虽然有volatile变量上有一种特殊的线程封闭,但是是确保只有单个线程对共享的volatile变量进行写入操作,并不能封闭到指定的线程上。
2、栈封闭:
书中原话,只能通过局部变量才能访问对象。言下之意,只用局部变量访问对象,而且这个对象也是单个线程中的局部对象。关键在于确保某个对象只能由单个线程访问。书中举了个找animals中可能凑一对的数目,懵逼了半天看的我。自己的理解写的程序
public class StudentDao {
public String