用不可变类防止并发问题的思路进行实战
1 基于不可变模式解决并发问题
1.1 业务介绍
- 需求:发短信给客户
- 问题:短信厂商众多,需要实时择优
- 业务流程
- 定时进行短信厂商PK,最终决定一家服务商。
- 短信网关,通过这个被选定的服务商,发送短信。
1.2 服务商代码
public class SmsInfo{
// text url
private String url;
// the max length of a text
private Long maxSizeInBytes;
public SmsInfo(String url, Long maxSizeInBytes){
this.url = url;
this.maxSizeInBytes = maxSizeInBytes;
}
set\get 方法省略
}
1.3 PK短信服务商代码
public class SmsRouter{
// 短信网关对象,通过volatile修饰保证其他线程的可见性
private static volatile SmsRouter instance = new SmsRouter();
// 记录服务商优劣
private final Map<Integer, SmsInfo> smsInfoRouterMap;
public SmsRouter{
this.smsInfoRouterMap = this.loadSmsInfoRouterMapFromDb();
}
private Map<Integer, SmsInfo> loadSmsInfoRouterMapFromDb(){
Map<Integer, SmsInfo> routerMap = new HashMap<>();
routerMap.put(1, new SmsInfo("https://www.aliyun.com", 180L));
routerMap.put(2, new SmsInfo("https://www.cloud.tencent.com", 181L));
routerMap.put(3, new SmsInfo("https://www.cloud.baidu.com", 182L));
}
}
在构造函数中调用了loadSmsInfoRouterMapFromDb方法,把数据库中的数据(此处是模拟的)加载到内存之中。
其中这个Map的Key就是排名。
1.4 短信服务商更新数据代码
public void changeRouteInfo(){
Map<Integer, SmsInfo> smsInfoRouteMap = smsRouter.getSmsInfoRouteMap();
SmsInfo smsInfo = smsInfoRouteMap.get(3);
smsInfo.setUrl("https://www.jiguang.cn");
smsInfo.setMaxSizeInBytes(183L);
}
1.5 问题出现
注意,changeRouteInfo()并不是一个原子操作。
因此完全可能出现,刚刚设置了新的URL,还没来得及设置大小,结果别的线程就抢先一步访问数据库,读取数据。
结果读取到了新的url和旧的大小。
1.6 改造
思路就是把SmsInfo改造成一个不可变类。
// 把smsinfo改造成不可变类
public class SmsInfo{
private final String url;
private final Long maxSizeInBytes;
private final Long id;
public SmsInfo(Long id, String url, Long maxSizeInBytes){
this.id = id;
this.url = url;
this.maxSizeInBytes = maxSizeInBytes;
}
public SmsInfo(SmsInfo smsInfo){
this.id = smsInfo.getId();
this.url = smsInfo.getUrl();
this.maxSizeInBytes = smsinfo.getMaxSizeInBytes();
}
}
// 更改短信供应商信息代码
public void changeRouteInfo(){
// 进行防御性复制
public Map<Integer, SmsInfo> getSmsInfoRouteMap(){
return Collections.unmodifiableMap(deepCopy(smsInfoRouteMap));
}
private Map<Integer, SmsInfo> deepCopy(Map<Integer, SmsInfo> smsInfoRouteMap){
Map<Integer, SmsInfo> resultMap = new HashMap<>(smsInfoRouteMap.size());
for (Map.Entry<Integer, Sminfo> entry : smsInfoRouteMap){
resultMap.put(entry.getKey(), new SmsInfo(entry.getValue()));
}
return resultMap;
}
}
public class SmsRouter{
private static volatile SmsRouter instance = new SmsRouter();
public static SmsRouter getInstance(){
return instance;
}
public static void setInstance(SmsRouter newInstance){
instance = newInstance;
}
public static void setInstance(SmsRouter newInstance){
instance = newInstance;
}
}
// 更新短信服务商代码
public void changeRunteInfo(){
updateSmsRouteInfoLists();
SmsRouter.setInstance(new SmsRouter())
}