多线程编程-设计模式之不可变对象模式
Immutable Object设计模式
适用场景:
1.被建模对象的状态变化不频繁:设置一个专门的线程用于被建模对象状态发生变化时创建新的不可变对象。而其他线程只是读取不可变对象的状态。此场景下一个小技巧就是Manipulator对不可变对象的引用使用volatile关键字进行修饰,既可以避免使用显示锁比如synchronize,又可以保证多线程间的内存可见性。
2.同时对一组相关的数据进行写操作,因此需要保证原子性
此场景下为了保证操作的原子性,通常的做法是使用显示锁,但若采用Immutable Object模式,将这一组相关的数据组合成一个不可变对象,则对这一组数据的操作就可以无需加显示锁也能保证原子性,这样既简化了编程,又提高了代码运行效率。
3.使用某个对象作为hashmap的key:一个对象作为HashMap的key被放入到HashMap中,若该对象的状态发生变化导致Hashcode发生变化,则会导致后面同样1的对象作为key去get的时候无法获取到关联的值,尽管改hashMap中的确存在已该对象作为key的条目。相反,由于不可变对象的状态不变,因此其HashCode也不变,这使得不可变对象非常适用于作为HashMap的key。
设计思路:
代码设计:手机号前缀与彩信中心之间的路由表关系 路由表可能发生变化 但是变化的频率并不高,并且不希望对这些数据的访问进行加锁等并发访问控制,以免产生不必要的开销和问题。
使用不可变对象维护路由表
package com.immutableobject.design;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* 彩信中心路由规则管理器
*
* @author zhouhy
* @create 2017 -09-29 上午11:03
*/
public final class MMSCRouter {
//用volatile修饰保证多线程环境下该变量的可见性
private static volatile MMSCRouter instance = new MMSCRouter();
private final Map<String,MMSCInfo> routeMap;
public MMSCRouter(){
//把数据库中的数据加载到内存中 存为map
this.routeMap = MMSCRouter.retrieveRouteMapFromDB();
}
private static Map<String,MMSCInfo> retrieveRouteMapFromDB(){
Map<String,MMSCInfo> map = new HashMap<String,MMSCInfo>();
//从数据库中查出数据并存到map里
return map;
}
public static MMSCRouter getInstance(){
return instance;
}
/**
*
* 根据手机号前缀获取对象的彩信中心信息
* @param msisdnPrefix
* @return
*/
public MMSCInfo getMMSC(String msisdnPrefix){
return routeMap.get(msisdnPrefix);
}
public static void setInstance(MMSCRouter newInstance){
instance = newInstance;
}
private static Map<String,MMSCInfo> deepCopy(Map<String,MMSCInfo> m){
Map<String,MMSCInfo> result = new HashMap<>();
for (String key:m.keySet()){
result.put(key,new MMSCInfo(m.get(key)));
}
return result;
}
//会发生变化的数据要进行防御性复制
public Map<String,MMSCInfo> getRouteMap(){
//防御性复制
return Collections.unmodifiableMap(deepCopy(routeMap));
}
}
使用不可变对象表示彩信中心数据:
package com.immutableobject.design;
/**
* 使用不可变对象表示彩信中心信息
*
* @author zhouhy
* @create 2017 -09-29 上午11:08
*/
public final class MMSCInfo {
/**
* 设备编号
*/
private final String deviceId;
/**
* 彩信中心url
*/
private final String url;
/**
* 该彩信中心所允许的最大附件大小
*/
private final int maxAttachmentSizeInBytes;
public MMSCInfo(String deviceId,String url,int maxAttachmentSizeInBytes) {
this.deviceId = deviceId;
this.url = url;
this.maxAttachmentSizeInBytes = maxAttachmentSizeInBytes;
}
public MMSCInfo(MMSCInfo prototype){
this.deviceId = prototype.deviceId;
this.url = prototype.url;
this.maxAttachmentSizeInBytes = prototype.maxAttachmentSizeInBytes;
}
public String getDeviceId() {
return deviceId;
}
public String getUrl() {
return url;
}
public int getMaxAttachmentSizeInBytes() {
return maxAttachmentSizeInBytes;
}
}
参与者实例监控路由表数据变化:
package com.immutableobject.design;
/**
* 与运维中心对接的类;Manipulator
*
* @author zhouhy
* @create 2017 -09-29 上午11:23
*/
public class OMCAgent extends Thread {
@Override
public void run() {
boolean isTableModificationMsg = false;
String updateTableName = null;
while (true){
/**
* 从与OMC连接的socket中读取消息并解析
* 解析到路由表更新消息后,重置MMSCInfo实例
*/
if (isTableModificationMsg){
if ("MMSCInfo".equals(updateTableName)){
MMSCRouter.setInstance(new MMSCRouter());
}
}
}
}
}
使用注意问题:
1.被建模对象的状态变更比较频繁:
频繁创建不可变对象,增加垃圾回收。
2.防御性复制:如果不可变对象本身包含一些状态需要对该暴露,而相应的字段本身又是可变的(比如HashMap),那么返回这些字段方法的时候需要做防御性复制,以避免外部代码修改其内部状态。
CopyOnWriteArrayList:遍历时为线程安全的 应用了不可变对象模式
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//复制原数组 并在此基础上将数组的最后一个元素设置为要添加的元素
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}