以对用户ID加锁为例
错误案例一
public void sync1(String uid){
synchronized(uid){
// 业务处理
}
}
案例分析:这里原本想实现对相同用户的业务处理进行同步,但在多线程环境下,uid 并不是同一个对象,所以这里并不能实现代码同步。
改进案例一
public void sync2(String uid){
synchronized(uid.intern()){
// 业务处理
}
}
案例分析:这里通过使用字符串的 intern 方法实现对同一个对象加锁,虽然可以实现代码同步,但这种方案存在一些问题:
- 后续如果用户ID和商品ID都需要同步处理,那么相同用户ID和商品ID无法并发处理(可以通过加个前缀标识处理);
- 常量池中的数据不好清楚(考虑使用 Map 代替)
改进案例二
private static final ConcurrentMap<String, Object> UID_MAP = new ConcurrentHashMap<>();
public void sync3(String uid){
Object mutex = UID_MAP.computeIfAbsent(uid, k -> new Object());
synchronized(mutex){
// 业务处理
}
}
案例分析:通过使用 ConcurrentMap 来代替字符串常量池。
注意:清除时需保证没有线程阻塞在对应 pid 的mutex 对象上,否则保证不了线程安全。
错误案例二
private static final ConcurrentMap<String, Object> UID_MAP = new ConcurrentHashMap<>();
public void sync4(String uid){
Object mutex = UID_MAP.computeIfAbsent(uid, k -> new Object());
synchronized(mutex){
// 业务处理
}
UID_MAP.remove(uid);
}
案例分析:清除时需没有线程阻塞在对应 pid 的mutex 对象上,这里没有额外处理所以保证不了线程安全。
例如:线程 A、B 同时进入 sync4,线程 A 获取到锁后进入同步代码块进行业务处理,线程 B 阻塞。当线程 A 执行完业务处理后移除 uid 对应的 mutex 对象,线程 C 进入 sync4,线程 C 成功发现不存在 uid 则创建新的 mutex 对象,此时线程 B、C 就会同时执行业务处理。
改进案例三
private static final ConcurrentMap<String, Object> UID_MAP = new ConcurrentHashMap<>();
public void sync4(String uid){
for( ; ;){
Object mutex = UID_MAP.computeIfAbsent(uid, k -> new Object());
synchronized(mutex){
if(mutex != UID_MAP.get(uid)){
continue;
}
// 业务处理
}
UID_MAP.remove(uid);
break;
}
}
案例分析:使用双重校验的思想来解决错误案例二的问题。