对象的共享
线程之间对象的共享不仅仅需要有原子性和临界区,还有一个重要方面:内存可见性
1 可见性
读操作的线程并非可以一直获取到写线程写入的最新值,例如:
代码示例
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;
}
以上实例代码可能存在如下问题:
- 一直循环下去,因为读线程可能看不到写线程写入了ready。
- 在读线程中输出0,因为主线程可能对number和ready的赋值顺序进行了改变。
这是由于线程之间没有正确使用同步使得数据在多个线程中共享出现错误导致的。常见的问题包括:
- 读取到的数据已经失效
- 没有对64位数据同步读写
失效的数据
以上述例子来说,当读线程读取ready变量的时候,很可能该变量已经失效。失效的数据通常包括两类数据:
- 值数据
- 引用数据
值数据的失效例如一个计数器应用,如果不对count变量进行同步处理的话可能会导致计数不准的问题。如果对象的引用失效可能会导致一些莫名其妙的问题,比如意料之外的异常,被破坏的数据结果,不精确的计算及无限循环等。
非原子的64位操作
根据Java内存模型的要求,变量的读写操作必须是原子操作。对于非volatile类型的long和double变量,JVM允许将64位读写操作分解为两个32为的操作,这样在多线程环境中共享可变的long和double变量也是不安全的。
一般的解决方式可以通过加锁或者声明变量为volatile变量。
内置锁可以用于确保某个线程以一种可预测的方式看到另一个线程执行的结果。加锁的含义不仅仅局限于互斥行为,还包括了内存可见性。
Java同时提供了一种较弱的同步机制,即volatile变量。volatile变量不同于加锁机制,加锁机制可以保证原子性和可见性,但是volatile只能保证可见性。volatile变量会确保将变量的更新结果通知到其他线程,被声明volatile变量会有两个效果:
- 不会对该变量重排序;
- 不会缓存在寄存器或者其他处理器不可见的地方。
使用volatile变量可以不执行加锁操作,也不会阻塞线程,开销要比同步要低。但是只能确保可见性,不能确保原子性,更不能保证在volatile变量递增操作的原子性,在使用的时候要注意。当满足以下所有条件时才应该使用volatile变量:
- 对变量写入不依赖当前值,或者确保只有单个线程更新变量的值;
- 该变量不被要求与其他变量同步变化;
- 在访问该变量时不需要加锁,因为加上锁就没必要使用volatile了。
2 发布与逸出
发布一个对象是指使一个对象能在当前作用域之外的代码中使用。一般是将成员变量或静态变量公开。
对于一个类的外部方法是不完全由该类规定的方法,包括该类以外定义的方法及由该类定义但可以被改写的方法。当把一个对象传递给一个外部方法,就相当于发布了这个对象。
逸出一个对象是指某个不应该发布的对象被发布
安全构造对象
错误的构造对象方式
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
在EventListener中保留了this的引用,因此会将this引用逸出。
一定不要在构造过程中使this引用逸出。在构造对象的过程中常见的问题:
- 构造函数中将自己的引用传递给一个线程并将该线程启动;
- 构造函数中调用一个可被改写的方法
如果构造函数中需要注册事件监听器或者启动线程,一个办法是将构造函数私有化,用工厂模式初始化。例如:
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
3 线程间的变量
对于多线程相关的变量,使用的时候有两种策略,一种不希望被共享,使用线程封闭技术将该变量的使用范围限制在一个线程中,如果需要在多个线程之间进行共享,跟据该变量是否可变,需要有不同的共享策略。
3.1 线程封闭
线程封闭是指仅在单线程内访问数据,将访问的数据封闭在一个线程中。常见场景:
- JDBC的Connection对象
- Command模式
Java中提供一些机制可用来实现线程封闭,例如局部变量和ThreadLocal。
3.1.1 Ad-hoc线程封闭
Ad-hoc线程封闭是指维护线程封闭性的职责完全由程序实现来承担。非常脆弱,一般不建议使用,建议使用其他诸如栈封闭和ThreadLocal。
3.1.2 栈封闭
在栈封闭中,只有通过局部变量才能访问对象。但程序员需要确保该局部引用不会逸出。
3.1.3 ThreadLocal类
一种更规范的保证线程封闭的方法就是使用ThreadLocal,set入ThreadLocal的变量每个线程都只有一份独立的副本。当某个线程第一次调用Thread.get的时候,就会通过initialValue获取初始值:
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();
}
以上代码可以为每个线程维持一个数据库连接。
3.2 线程共享的对象
3.2.1不可变对象
线程安全是不可变对象的固有属性之一
Java中,不可变不代表对象的所有域都声明为final类型,当满足以下条件的时候,对象才是不可变的:
- 对象创建以后其状态不能修改
- 对象的所有域都是final类型
- 对象正确被创建,没有在构造期间this引用逸出
不可变对象的内部仍可以使用可变对象来管理他们的状态,例如:
@Immutable
public final class ThreeStooges {
private final Set<String> stooges = new HashSet<String>();
public ThreeStooges() {
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge(String name) {
return stooges.contains(name);
}
public String getStoogeNames() {
List<String> stooges = new Vector<String>();
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
return stooges.toString();
}
}
虽然用于管理ThreeStooges对象的Set对象时可变的,但是ThreeStooges对象是不可变的。
不可变对象可以提供弱原子性。当需要同时更新多个域时,可以将多个域放入一个不可变对象中保持原子性。例如:
public class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache(BigInteger i,
BigInteger[] factors) {
lastNumber = i;
lastFactors = Arrays.copyOf(factors, factors.length);
}
public BigInteger[] getFactors(BigInteger i) {
if (lastNumber == null || !lastNumber.equals(i))
return null;
else
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
每次更新Cache都需要更新lastNumber和lastFactors,更新这两个域需要以原子方式执行某个操作。
调用的时候:
private volatile OneValueCache cache = new OneValueCache(null, null);
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
}
void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[]{i};
}
将cache设置为volatile使的更新了该域后其他线程会立即看到新缓存的数据。
3.2.2 事实不可变对象
事实不可变对象是指那些从技术上看是可变的,但是其状态在发布之后不会在改变的对象。
在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。
3.2.3 可变对象
对于可变对象,不仅发布对象的时候需要使用同步,而且每次访问对象同样需要使用同步确保后续操作修改的可见性。
3.3 安全发布
三类对象
- 不可变对象:可以通过任意机制发布;
- 事实可变对象:必须通过安全方式发布;
- 可变对象:可变对象必须通过安全方式发布,而且必须是线程安全或者进行锁保护。
并发共享对象时的策略
- 线程封闭:栈封闭或者ThreadLocal。
- 只读共享:共享的对象包括不可变对象和事实不可变对象。
- 线程安全共享:线程安全对象在其内部实现同步。
- 保护对象:锁保护