多线程共享变量问题
非线程安全代码举例
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的值,这种现象称为“重排序”。
使用同步可以保证可见性
@NotThreadSafe
public class MutableInteger {
private int value;
public int get() {
return value;
}
public void set(int value) {
this.value = value;
}
}
@ThreadSafe
public class SynchronizedInteger {
@GuardedBy("this")
private int value;
public synchronized int get() {
return value;
}
public synchronized void set(int value) {
this.value = value;
}
}
对get()方法进行同步,保证了value的可见性。
非原子的64位操作
当线程没有同步的情况下读取变量,可能会得到一个失效的值,但是至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性。
最低安全性适合绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(double和long)。JVM允许64位的读操作或写操作分解为两个32位的操作。
因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非用关键字volatile来声明,或者用锁保护起来。
volatile关键字
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
发布与溢出
发布:使对象能够在除当前作用域之外的地方使用。
溢出:指某个对象不应该发布却被发布了。
public class PublishAndEscape {
//发布status
public static String status = "status";
private Object[] objects;
// 内部的可变状态溢出,导致外部可以直接访问并修改object
public Object[] getObjects() {
return objects;
}
}
改进代码
public class PublishAndEscape {
public static final String STATUS = "status";
private Object[] object;
//参见ArrayList、CopyOnWriteList
public Object[] getObject() {
return Arrays.copyOf(object, object.length);
}
}
线程封闭
当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术叫做线程封闭(Thread Confinement)。
栈封闭
栈限制是线程限制的一种特例,在栈限制中,只能通过本地变量才可以触及对象。正如封装使不变约束更容易被保持,本地变量使对象更容易被限制在线程本地中。本地变量本身就被限制在执行线程中;它们存在于执行线程栈。其他线程无法访问这个栈。栈限制(也称线程内部或者线程本地用法,但是不要与核心库类的ThreadLocal混淆)
public int loadTheArk(Collection<Animal> candidates) {
SortedSet<Animal> animals;
int numPairs = 0;
Animal candidate = null;
//animals被封装在方法中,不要使它们溢出
animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
for(Animal a:animals){
if(candidate==null || !candidate.isPotentialMate(a)){
candidate = a;
}else{
ark.load(new AnimalPair(candidate,a));
++numPairs;
candidate = null;
}
}
return numPairs;
}
ThreadLocal
这个类能使线程中的某个值与保存值的对象关联起来。
ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
当某个线程终止后,与线程对应的对象会作为垃圾回收。
ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。
public class ConnectionDispenser {
static String DB_URL = "jdbc:mysql://localhost/mydatabase";
private ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
try {
return DriverManager.getConnection(DB_URL);
} catch (SQLException e) {
throw new RuntimeException("Unable to acquire Connection, e");
}
};
};
public Connection getConnection() {
return connectionHolder.get();
}
}