一个线程修改了对象状态后,其他线程能够看到发生的状态变化
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
如上,子线程判断ready和输出number,主线程修改后,可能出现
数据在多个线程共享时,就应该使用同步确保可见性
对于如下类,可能出现一个线程在set过程中,另外一个线程调用get获取失效数据
class MutableInteger {
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
对于可共享变量,应需要同时对get和set同步
class MutableInteger {
@GuardedBy("this")
private int value;
public synchronized int getValue() {
return value;
}
public synchronized void setValue(int value) {
this.value = value;
}
}
加锁不仅仅局限于互斥,还包含内存可见性,为了所有线程都能获得共享数据的最新值,其get和set都必须在同一个锁上同步
非volatile类型的64位数值变量(double和long)读写操作分为两个32位操作,多线程下可能出现失效数据,应该用volatile或锁保护
经典用法:检测某个状态标志位以判断是否退出循环,此时比用锁更简便
class A {
volatile boolean isSleep;
public void check() {
while (!isSleep) {
//......
}
}
}
加锁和原子变量能确保可见性和原子性,volatile只能确保可见性
使用volatile变量的条件:
发布指的是对象在当前作用域之外的代码中使用,当某个不应该发布的对象被发布时称为逸出,常见的逸出有
第三种常见形式是this在构造函数中逸出,如下构造函数还未结束,线程就持有了对象实例,这会导致未知状态
class A {
public A() {
new Thread(new Runnable() {
@Override
public void run() {
A.this.test();
}
}).start();
//...
}
private void test() {
System.out.println(getClass());
}
}
只有构造函数返回时,对象才处于可预测和一致的状态,构造函数创建线程不应立即启动,如下等构造完后再启动线程
class A {
private void test() {
System.out.println(getClass());
}
private final Runnable mRunnable;
private A() {
mRunnable = new Runnable() {
@Override
public void run() {
A.this.test();
}
};
}
public static A getInstance() {
A a = new A();
new Thread(a.mRunnable).start();
return a;
}
}
仅在单线程内访问数据,就不需要同步,称为线程封闭
局部变量封闭在执行线程的栈中,其他线程无法访问这个栈
class A {
public int test() {
int num = 0;
//...
Set set = new HashSet<>();
return num;
}
}
如上,基本数据类型num无引用,可封闭在线程内,若是返回set则封闭性被破坏导致对象逸出
使线程中的某个值与保存值的对象关联起来,每个使用该变量的线程都存有一个独立的副本,ThreadLocal
当将单线程应用移植到多线程环境中,可以将共享的全局变量转换为ThreadLocal对象,以维持线程安全性
不可变对象一定是线程安全的,不可变对象需满足
可通过volatile和不可变对象确保线程安全性和可见性
public class A {
@GuardedBy("this")
private long count;
@GuardedBy("this")
private boolean isOdd;
public long getCount() {
return count;
}
public void test() {
//....
synchronized (this) {
count++;
isOdd = getCount() % 2 == 0;
}
}
}
对于如上加锁代码,提取不可变类,用volatile确保数据更新后的可见性,每次更新创建新的不可变对象
public class A {
private volatile Counter mCounter = new Counter(0);
private long currentCount = 0;
public void test() {
//....
currentCount++;
if (currentCount != mCounter.getCount()) {
mCounter = new Counter(currentCount);
}
}
}
class Counter {
private final long count;
private final boolean isOdd;
Counter(long count) {
this.count = count;
this.isOdd = count % 2 == 0;
}
public long getCount() {
return count;
}
}
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见
一个正确构造的对象可以通过以下方式来安全地发布:
线程安全库中的容器类提供了以下的安全发布保证:
如果对象从技术上来看是可变的,但其状态在发布后不会再改变,如下将可变的Date放入Map就不会改变,访问时不需要额外的同步
public Map lastLogin = Collections.synchronizedMap(new HashMap<>());