使用volatile类型发布不可变对象

最近在看《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的削弱机制,需要的额外消耗较少,所以这种方式的同步效果较好。但是一般情况下,只有在遍历比修改频繁的时候才会考虑用这种方式,主要原因是如果数据庞大,频繁的申请和回收得不偿失。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值