记一次线程安全的i++操作
问题产生
设计一个程序,统计服务器接口的访问次数,有可能会这样写:
public class AccessCounter {
private int accessCount;
public void access() {
accessCount++;
}
}
上面的代码没有考虑JMM,在并发环境统计的结果并不准确。
问题产生于i++执行的操作:
1) 从主存中读取i的值到工作内存
2 )i的值加1
3)把i的值写入主存
上面的操作在并发下并不保证原子性。
解决上面的问题,可以使用:
- 原子类:通过cas保证原子性。
- 同步:加锁保证原子性、原子性。线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。
- 不可变对象。
- 局部变量、ThreadLocal。
volatile不能保证i++线程安全!
在多线程的情况下,一个线程修改了i的值,由于缓存一致性协议,其他的线程获取的i的值会失效。但是,可能会出现这么一种情况。在这个线程修改i的值的同时,其他线程已经把i从缓存行中加载到了cpu寄存器,缓存一致性协议只对缓存行有效,对寄存器不起作用。所以会继续执行i++的操作。最后导致线程安全问题。
不可变对象demo:
public final class SmsInfo {
/**
* 设备编号
*/
private final String deviceCode;
/**
* 短信中心的url
*/
private final String url;
/**
* 短信内容最多多少个字节
*/
private final Long maxSizeInBytes;
public SmsInfo(String deviceCode, String url, Long maxSizeInBytes) {
this.deviceCode = deviceCode;
this.url = url;
this.maxSizeInBytes = maxSizeInBytes;
}
/**
* 初始化
*
* @param smsInfo 短信中心
*/
public SmsInfo(SmsInfo smsInfo) {
this.deviceCode = smsInfo.getDeviceCode();
this.url = smsInfo.getUrl();
this.maxSizeInBytes = smsInfo.getMaxSizeInBytes();
}
public String getDeviceCode() {
return deviceCode;
}
public String getUrl() {
return url;
}
public Long getMaxSizeInBytes() {
return maxSizeInBytes;
}
}
public class SmsRouter {
/**
* 短信网关对象,通过volatile修饰来保证其他线程的可见性
*/
private static volatile SmsRouter instance = new SmsRouter();
/**
* 短信中心路由信息的map
*/
private final Map<String, SmsInfo> smsInfoRouteMap;
/**
* 初始化短信网关路由信息
*/
public SmsRouter() {
// 从数据库中维护的路由信息加载到jvm内存中
this.smsInfoRouteMap = this.loadSmsInfoRouteMapFromDb();
}
/**
* 短信路由中心表变更,更新短信网关
*
* @param newInstance 短信网关
*/
public static void setInstance(SmsRouter newInstance) {
instance = newInstance;
}
/**
* 从数据库加载短信中心的路由信息
*
* @return 短信中心的路由信息
*/
private Map<String, SmsInfo> loadSmsInfoRouteMapFromDb() {
// 初始化 模拟db的数据
Map<String, SmsInfo> routeMap = new HashMap<>();
routeMap.put("180", new SmsInfo("001", "http://www.baidu.com", 180L));
routeMap.put("181", new SmsInfo("002", "http://www.biying.com", 181L));
routeMap.put("182", new SmsInfo("003", "http://www.google.com", 182L));
return routeMap;
}
/**
* 获取短信网关对象
*
* @return
*/
public static SmsRouter getInstance() {
return instance;
}
/**
* 根据手机号前缀来获取短信中心
*
* @param phoneNumberPrefix 手机号的前缀
* @return 短信中心
*/
public SmsInfo getSmsInfoByPhoneNumberPrefix(String phoneNumberPrefix) {
return smsInfoRouteMap.get(phoneNumberPrefix);
}
/**
* 获取路由信息
*
* @return 路由信息 key:手机号前缀 value:短信中心
*/
public Map<String, SmsInfo> getRouteMap() {
// 防止对短信路由信息更改 进行防御性的复制
return Collections.unmodifiableMap(deepCopy(smsInfoRouteMap));
}
private Map<String, SmsInfo> deepCopy(Map<String, SmsInfo> smsInfoRouteMap) {
HashMap<String, SmsInfo> result = new HashMap<>();
for (String key : smsInfoRouteMap.keySet()) {
result.put(key, new SmsInfo(smsInfoRouteMap.get(key)));
}
return result;
}
}
参考阅读:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)