记一次线程安全的i++操作

问题产生

设计一个程序,统计服务器接口的访问次数,有可能会这样写:

public class AccessCounter {
    private int accessCount;
    
    public void access() {
        accessCount++;
    }
}


上面的代码没有考虑JMM,在并发环境统计的结果并不准确。

img

img



问题产生于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;
    }
}

参考阅读:

为什么volatile修饰的i++是线程不安全的?

设计一个不可变对象

volatile底层之缓存一致性协议MESI

posted @ 2023-11-14 21:27  OraCat  阅读(18)  评论(0编辑  收藏  举报