Java多线程——对象的共享
可见性
一个线程修改了对象状态后,其他线程能够看到发生的状态变化
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,主线程修改后,可能出现
- 子线程一直循环(未看到ready的值)
- number输出0(子线程可能先看到写入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都必须在同一个锁上同步
非原子的64位操作
非volatile类型的64位数值变量(double和long)读写操作分为两个32位操作,多线程下可能出现失效数据,应该用volatile或锁保护
volatile变量
- 用于确保变量的更新操作通知到其他线程
- 对该变量的操作不会重排序,不会被缓存在寄存器,
- 可以理解对其get和set都声明为synchronized,但实际实现不会加锁也不会导致线程阻塞,更为轻量级
经典用法:检测某个状态标志位以判断是否退出循环,此时比用锁更简便
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<String> set = new HashSet<>();
return num;
}
}
如上,基本数据类型num无引用,可封闭在线程内,若是返回set则封闭性被破坏导致对象逸出
ThreadLocal
使线程中的某个值与保存值的对象关联起来,每个使用该变量的线程都存有一个独立的副本,ThreadLocal<T>相当于Map<Thread, T>
当将单线程应用移植到多线程环境中,可以将共享的全局变量转换为ThreadLocal对象,以维持线程安全性
不变性
不可变对象一定是线程安全的,不可变对象需满足
- 对象创建后其状态不能修改
- 对象所有域都是final
- 对象创建期间,this引用没有逸出
可通过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;
}
}
安全发布
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见
常用模式
一个正确构造的对象可以通过以下方式来安全地发布:
- 在静态初始化函数中初始化一个对象引用,如pubic static A a = new A()
- 将对象的引用保存到volatile类型的域或者AtomicReferance对象
中 - 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中,如系统自带的线程安全库
线程安全库中的容器类提供了以下的安全发布保证:
- 通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)
- 通过将某个元素放入 Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程
- 通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程
事实不可变对象
如果对象从技术上来看是可变的,但其状态在发布后不会再改变,如下将可变的Date放入Map就不会改变,访问时不需要额外的同步
public Map<String, Date> lastLogin = Collections.synchronizedMap(new HashMap<>());