对象的共享知识梳理
可见性
在多线程编程中,通常,我们无法确保读操作的线程能实时的看到其他线程写入的值。
为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
并发编程过程中可能会发生“重排序”等问题,如例1。
//例1
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;
}
}
Novisibility可能会持续循环下去,因为读线程可能永远都看不到ready的值。另一种情况,NoVisibility可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值。这种现象被称为“重排序”。
失效数据
在缺乏同步的程序中可能产生错误的一种情况:失效数据。当读线程查看ready变量时,可能会得到一个已经失效的值。除非每次访问变量时都使用同步,否则很可能得该变量的一个失效值。
非原子的64位操作
非volatile类型的64位数值变量(double和long)。Java内存模型要求,变量得到读取操作和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同线程中执行,那么很可能会读取到某个值得高32位和另一个值得低32位。
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
Volatile变量
这里还需要说明下volatile关键字的作用,可以说有2个作用,其一是,用volatile修饰的变量的读取和写入都是直接操作内存,以保证被其它线程读取到值都是最新的,或者称之为确保内存的可见性;其二是,保证变量的读取和写入操作都是原子操作,就是上面long和double的读取所遇到的问题,注意这里提到的原子性只是针对变量的读取和写入,并不包括对变量的复杂操作,比如i++就无法使用volatile来确保这个操作是原子操作。
使用volatile的需要满足的所有条件:
a、对变量的写入操作不依赖于变量当前的值,或者你能确保只有单个线程更新变量的值
b、该变量不会与其他状态变量一起纳入不变性条件中
c、在访问变量时不需要加锁
发布与逸出
“发布”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。
“逸出”是指某个不应该发布的对象被发布。
当“发布”一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。
以下示范一个错误案例:
当ThisEscape发布EventListener时,也隐含地发布了ThisEscape实例本身,因为在这个内部类的实例中包含了对ThisEscape实例的隐含引用。
public class ThisEscape{
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
不要在构过程中使this引用逸出。
在构造过程中使this引用逸出的一个常见错误是,在构造函数中启动一个线程。在构造函数中创建线程并没有错误,但最好不要立即启动它。
在构造函数中调用一个可改写的实例方法时,同样也会导致this引用在构造过程中逸出。
如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公有的工厂方法,从而避免不正确的构造过程。
线程封闭
当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭。
ThreadLocal类
维持线程封闭性的一种规范方法是使用ThreadLocal,这个类能使线程中某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本。
ThreadLocal对象通常用于防止可变的单实例变量或全局变量进行共享。
不变性
不可变对象举例:
//不可变对象
public final class ThreeStooges {
private final Set<String> stooges = new HashSet<>();
public ThreeStooges() {
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge(String name) {
return stooges.contains(name);
}
}
不可变对象一定是线程安全的。
当满足一下条件时,对象才是不可变的:
a、对象创建以后其状态就不能修改。
b、对象的所有域都是final类型。
c、对象时正确创建的(在对象创建期间,this引用没有逸出)。
安全发布
发布不可变对象的引用时没有使用同步,也仍然可以访问该对象。
可变对象必须通过安全的方式来发布。这意味着发布和使用该对象的线程时都必须使用同步。可变对象必须通过安全的方式来发布,并且必须是线程安全的或者由某个锁保护起来。
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。
一个正确构造的对象可以通过以下方式来安全地发布:
a、在静态初始化函数中初始化一个对象引用。
b、将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
c、将对象的引用保存到某个正确构造对象的final类型域中。
d、将对象的引用保存到一个由锁保护的域中。