Java并发编程详解之 线程安全和对象共享 (三)

2014-11-24 10:41:04 · 作者: · 浏览: 2
加锁机制

当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁

三、对象的共享
1、加锁与可见性

如右图,当线程A执行某个同步代码块时,线程B随后进入由同一个锁保护的同步代码块,在这种情况下可以保证,在锁被释放之前,A看到的变量值在B获得锁后同样可以由B看到

我们可以进一步理解为什么在访问某个共享的变量时要求所有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其他线程来说都是可见的。否则,如果一个线程在未持有正确锁的情况下读某个变量,那么读到的可能是一个失败值

加锁的含义不仅仅局限于互斥行为,还包括内存的可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步

2、Volatile变量
Java语言提供了一个稍弱的同步机制,即volatile变量,当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值

volatile的使用场景:

仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。

下面是volatile的典型用法

[java]
public class CountingSheep {
volatile boolean asleep;

void tryToSleep() {
while (!asleep)
countSomeSheep();
}

void countSomeSheep() {
// One, two, three...
}
}

public class CountingSheep {
volatile boolean asleep;

void tryToSleep() {
while (!asleep)
countSomeSheep();
}

void countSomeSheep() {
// One, two, three...
}
}
虽然volatile变量很方便,但是存在一些局限性。volatile变量通常用做某个操作完成、发生中断或者状态的标志。但是volatile的语义不足以确保递增操作(count++)的原子性,那些需要依赖于前一个值的变化不适合用volatile

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性

当且仅当满足以下所有条件时,才应该使用volatile变量:

(1)对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值

(2)该变量不会与其他状态变量一起纳入不变性条件中

(3)在访问变量时不需要加锁

3、发布与逸出
“发布”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。例如将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中

例如下面代码

[java]
class Secrets {
public static Set knownSecrets;

public void initialize() {
knownSecrets = new HashSet();
}
}

class Secrets {
public static Set knownSecrets;

public void initialize() {
knownSecrets = new HashSet();
}
}
上面的knownSecrets就被发布了,这样就导致了逸出。当发布某个对象时,可能会间接发布其他对象,如果将一个Secret对象添加到集合knownSecrets中,那么同样会发布这个对象

再列举两种逸出的例子

1、使内部的可变状态逸出

[java]
class UnsafeStates {
private String[] states = new String[]{
"AK", "AL" /*...*/
};

public String[] getStates() {
return states;
}
}

class UnsafeStates {
private String[] states = new String[]{
"AK", "AL" /*...*/
};

public String[] getStates() {
return states;
}
}
2、隐式地使this引用逸出

[java]
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}

public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
注意:内部类也是导致内存泄露的一个方面

安全的对象构造过程

不要在构造过程中使this引用逸出。(下面都是围绕这条展开的)

当从对象的构造函数中发布一个对象时,只是发布了一个尚未构造完成的对象。即使发布对象的语句位于构造函数的最后一行也是如此。如果this引用在构造过程中逸出,那么这种对象就被认为是不正确构造

基于上面,在构造过程中使用this引用逸出的一个常见的错误是,在构造函数中启动一个线程。在构造函数中创建线程本身没有错误,但最好不要立即启动它,而是通过一个start或initialize方法来启动。

如果想在构造函数中注册一个时间监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法,从而避免不正确的构造过程,避免this引用在构造过程中逸出

例如下面代码:

[java]
public class SafeListener {
private final EventListener listener;

private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSome