最近在看《java并发编程实践》这本书,看到关于不可变对象的介绍,以前并没有接触过,感觉不错。在这里介绍一下。
volatile简介
volatile是java的一种削弱的同步,volatile的功能只是能够保证对于变量修改时能够保证立即写入内存。被声明的变量能够保证可见性,但是并不能够满足原子性,整个过程中还是可以同时被其他的线程改变。所以volatile使用有一定的局限性,对于volatile的详细介绍可以参考一下这里,我就不班门弄斧了,这里主要介绍一下一种新学习的处理同步的方法:不可变对象。
不可变对象
不可变对象是指对象中的所有成员在创建的时候就不能更改。满足下面的条件才是不可变对象:
- 对象创建之后不能更改
- 对象的所有域都是final类型
- 对象是正确创建的,即创建过程中没有this逸出
这里需要注意的是对于final的引用,需要其所指的对象一样是不可变的。例如在TreeStooges类中的所有成员在创建对象时就不可变了,所以是一个不可变类。
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);
}
}
使用volatile类型发布一个不可变对象
一个不可变对象一定是线程安全的,所以对于不可变对象处理同步的提供了一个新的思路,之前经常考虑对共享变量的同步,现在可以通过不可变对象完成同步。如下例子:
public class CachedFactorizer extends GenericServlet implements Servlet {
private BigInteger lastNumber;
private BigInteger[] lastFactors;
private long hits;
private long cacheHits;
public synchronized long getHits() {
return hits;
}
public synchronized double getCacheHitRatio() {
return (double) cacheHits / (double) hits;
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);//通过request获得请求的整数i
BigInteger[] factors = null;
synchronized (this) {
++hits;
if (i.equals(lastNumber)) {
++cacheHits;
factors = lastFactors.clone();//如果有缓存,直接获取结果
}
}
if (factors == null) {
factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors.clone();//对该整数i进行因式分解
}
}
encodeIntoResponse(resp, factors);//返回response
}
}
这里CachedFactorizer类是Servlet请求,对对整数i进行因式分解,并且缓存了上一次请求的数据,如果两次请求相同,那么可以直接返回结果。由于对于缓存的结果不同的请求线程都会做相应的更改,所以需要对缓存lastNumber和lastFactors采用sychronized同步,保证每一个线程都只能单一处理缓存。
修改成不可变对象如下:
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);
}
}
建立不可变对象OneValueCache,用于缓存数据。注意Arrays.copyOf(),对于数组的拷贝,如果直接引用数组的话,OneValueCache就不是不可变对象了。
servelet修改如下:
public class VolatileCachedFactorizer extends GenericServlet implements Servlet {
private volatile OneValueCache cache = new OneValueCache(null, null);
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);//通过request获得请求的整数i
BigInteger[] factors = cache.getFactors(i);//对该整数i进行因式分解
if (factors == null) {
factors = factor(i);
cache = new OneValueCache(i, factors);//对于不同的请求生成不同的缓存
}
encodeIntoResponse(resp, factors);//返回response
}
}
对于不同的请求生成不同不可变对象缓存,对于其他线程请求只能访问不能修改。如果有新的缓存,则将其替换掉,这里cache变量需要声明为volatile类型,保证其可见性。在java的并发类CopyOnWriteArrayList和CopyOnWriteArraySet就是使用这个思想,即“写入时复制”。有兴趣可以看其源码。
总结
这种方式比较于使用sychronized同步机制有很强的并发性,volatile本来就是sychronized的削弱机制,需要的额外消耗较少,所以这种方式的同步效果较好。但是一般情况下,只有在遍历比修改频繁的时候才会考虑用这种方式,主要原因是如果数据庞大,频繁的申请和回收得不偿失。