不可变对象
线程安全问题
如果在电商中需要扣减库存,通常情况下,写出来的代码可能是这样的。
/**
* 库存计数器
*
*/
public class InventoryCounter {
/**
* 库存数量
*
*/
private int counter = 100;
public void access() {
counter--;
}
}
每当有新的已支付订单时,库存数量就减一,直到库存数量等于零为止。
显然,在多线程中,这种代码一定会产生超卖
或者幽灵订单
现象。
产生这个问题的根本原因还是由于JMM(Java Memory Model,Java内存模型)引起的(至于什么是JMM,我已经在《Java深度探索:开发基础》的《第3章 多线程》
中有过讲解,这里就不罗嗦了)。
扣减库存的过程应该是这样的三步操作。
从主内存中读取
counter
的值。将
counter
的值减1。将
counter
的值写回主内存。
在单个线程的时候执行结果没问题,而当多个线程同时执行时,它就变成了下面这样。
两个线程都会从主内存中复制counter
的值到自己的工作内存中。

然后两个线程都执行了计算步骤,分别得到同样的值,都是99
。

最后执行了关键的第三步:这两个线程相继将计算结果写回到主内存中,实现数据同步。
这种操作是没有先后顺序的,不管是那种顺序都不影响它的最终结果是99
——相当于是其中一个线程覆盖
了另一个线程的执行结果。

这就是让人头疼的线程安全问题。
解决办法
解决线程安全问题的办法有两类:使用锁
或者不使用锁
。
无锁
的方式,使用局部变量
、不可变对象
、ThreadLocal
和CAS原子类
这四种方法来实现。有锁
的方式,通过synchronized
关键字或者ReentrantLock
类来实现。
不可变对象
这个好理解,线程既然改不了它们的值,那就自然不会有冲突,这个需要单独来说。
而ThreadLocal
这个在很多多线程的实践中都是不推荐的,所以不用它。
所谓局部变量
说的是这个意思。
public int getValue() {
int localValue = 0;
localValue++;
return localValue;
}

因为localValue
根本就不会出现在主内存中,当然也就不会有线程安全问题了。
不过这种方式只提供解决问题的思路,直接套用它是不行的。
至于CAS原子类
,在《Java深度探索:开发基础》的《第3章 多线程》
中也有相关讲解。
可以直接将之前的库存变量声明为原子类,就不会有问题了。
/**
* 库存数量
*
*/
private AtomicInteger counter = new AtomicInteger(100);
public void access() {
counter.decrementAndGet();
System.out.println("库存数量:" + counter.get());
}
原子类是通过底层的Unsafe
类调用硬件级别的原子操作
来实现的。
在早些年计算机性能普遍不高的情况下,锁
的方式比较影响程序的执行效率。但随着Java本身不断迭代新的版本及计算机硬件的升级,这个问题已经没有之前那么大了。所以可以放心大胆地使用synchronized
关键字或者ReentrantLock
类。
/**
* 库存数量
*
*/
private int counter = 100;
private ReentrantLock lock = new ReentrantLock();
/**
* 通过ReentrantLock方式加锁
*
*/
public void lockAccess1() {
lock.lock();
try {
counter--;
} finally {
lock.unlock();
}
}
/**
* 通过synchronized关键字方式加锁
*
*/
public synchronized void lockAccess2() {
counter--;
}
Immutable Object
既然Immutable Object(不可变对象)
能解决线程安全问题,就来试试看。
假设有这样一个用户安全信息类,用来核实用户身份。
/**
* 用户安全信息类
*
*/
public class UserSecurityInfo {
private String username;
private String password;
public UserSecurityInfo(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public void setInfo(String username, String password) {
this.username = username;
this.password = password;
}
}
通过一个追踪器来获取和更新它。
/**
* 用户安全信息追踪
*
*/
public class SecurityInfoTracker {
private Map<String, UserSecurityInfo> userSecurityInfoMap = new ConcurrentHashMap<>();
/**
* 更新用户安全信息
*
*/
public void updateUserSecurityInfo(String id, String username, String password) {
UserSecurityInfo userSecurityInfo = userSecurityInfoMap.get(id);
userSecurityInfo.setInfo(username, password);
}
/**
* 获取用户安全信息
*
*/
public UserSecurityInfo getUserSecurityInfo(String id) {
return userSecurityInfoMap.get(id);
}
}
但是UserSecurityInfo
类中的setInfo()
方法是有问题的。

也就是说,如果线程1
只来得及更新username
,就会导致线程2
获取到错误的用户数据。
所以将UserSecurityInfo
改造为不可变类
,这样就避免了上面的问题。
/**
* 用户安全信息类
*
*/
public final class UserSecurityInfo {
private final String username;
private final String password;
public UserSecurityInfo(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
这样一来,UserSecurityInfo
类既不能被继承,也没有了setInfo()
方法,解决了线程问题。
如果需要更新追踪器中的用户安全信息,就只能用UserSecurityInfo
类整个来替换了。
/**
* 用不可变对象UserSecurityInfo更新用户安全信息
*
*/
public void updateUserSecurityInfo(String id, UserSecurityInfo userSecurityInfo) {
userSecurityInfoMap.put(id, userSecurityInfo);
}
这种方式就是被称为Immutable Object(不可变对象)
的模式。
支付网关
有一个日交易量达到百万笔的支付网关系统,它会调用第三方支付系统,例如,支付宝、微信支付、度小满等。

它包括支付服务商的基本信息和支付网关路由这两个核心类。
支付服务商基本信息PaymentInfo
类。
/**
* 支付服务商基本信息
*
*/
public class PaymentInfo {
private String appkey;
// 签名方式
private String sign;
public PaymentInfo(String appkey, String sign) {
this.appkey = appkey;
this.sign = sign;
}
public String getAppkey() {
return appkey;
}
public void setAppkey(String appkey) {
this.appkey = appkey;
}
public String getSign() {
return sign;
}
public void setSign(String sign) {
this.sign = sign;
}
}
支付网关路由PaymentRouter
类。
/**
* 支付网关路由
*
*/
public class PaymentRouter {
private static volatile PaymentRouter instance = new PaymentRouter();
/**
* key表示支付服务的优先级
*
*/
private final Map<Integer, PaymentInfo> routerMap;
/**
* 获取服务商的map
*
*/
public Map<Integer, PaymentInfo> getRouterMap() {
return routerMap;
}
/**
* 初始化支付网关的路由信息
*
*/
public PaymentRouter() {
// 初始化路由表
this.routerMap = this.loadRouterMapFromMySQL();
}
/**
* 从数据库中加载第三方支付信息
*
*/
private Map<Integer, PaymentInfo> loadRouterMapFromMySQL() {
Map<Integer, PaymentInfo> routerMap = new ConcurrentHashMap<>();
routerMap.put(1, new PaymentInfo("alipay", "123"));
routerMap.put(2, new PaymentInfo("wxpay", "456"));
routerMap.put(3, new PaymentInfo("dupay", "789"));
return routerMap;
}
/**
* 将 dupay 改为 jdpay
*
*/
public void changeRouterMap() {
Map<Integer, PaymentInfo> routerMap = this.getRouterMap();
PaymentInfo paymentInfo = routerMap.get(3);
paymentInfo.setAppkey("jdpay");
paymentInfo.setSign("abc");
routerMap.put(3, paymentInfo);
}
}
为了防止之前在用户安全信息类UserSecurityInfo
中出现的问题,这里也要把支付服务商基本信息PaymentInfo
类改造为不可变类。
同样使用final
关键字,将它作用在类和成员变量上。
public final class PaymentInfo {
private final int id;
private final String appkey;
// 签名方式
private final String sign;
public int getId() {
return id;
}
public String getAppkey() {
return appkey;
}
public String getSign() {
return sign;
}
public PaymentInfo(int id, String appkey, String sign) {
this.id = id;
this.appkey = appkey;
this.sign = sign;
}
public PaymentInfo(PaymentInfo paymentInfo) {
this.id = paymentInfo.getId();
this.appkey = paymentInfo.getAppkey();
this.sign = paymentInfo.getSign();
}
}
但这里和之前用户信息安全类UserSecurityInfo
稍有不同的是,要将获取服务商Map
的代码进行一番深度
改造。
/**
* 获取服务商的map
*
*/
public Map<Integer, PaymentInfo> getRouterMap() {
// 防止对支付路由信息进行更改,使用了 Collections 工具类中的 unmodifiableMap() 方法
return Collections.unmodifiableMap(deepCopy(this.routerMap));
}
/**
* 实现深拷贝操作
*
*/
private Map<Integer, PaymentInfo> deepCopy(Map<Integer, PaymentInfo> routerMap) {
Map<Integer, PaymentInfo> result = new ConcurrentHashMap<Integer, PaymentInfo>(routerMap.size());
for (Map.Entry<Integer, PaymentInfo> entry : routerMap.entrySet()) {
result.put(entry.getKey(), new PaymentInfo(entry.getValue()));
}
return result;
}
因为Java没有Python中那种比较方便的deepCopy()
方法,所以就自己手动实现。
理论上来说,其实还应该让Map
中的每个PaymentInfo
类都实现Cloneable
和Serializable
接口,这样才是真正的Java深拷贝
,只不过这里做了简化。
然后提供一个替换支付网关路由PaymentRouter
的方法,用来刷新服务商的信息。
public static PaymentRouter getInstance() {
return instance;
}
public static void setInstance(PaymentRouter newInstance) {
instance = newInstance;
}
当支付服务商发生变更时,可以这样更新。
public void changeRouterInfo() {
// 先更新数据库中的支付服务商
updatePaymentRouterInfo2MySQL();
// 再更新内存中的支付服务商路由
PaymentRouter.setInstance(new PaymentRouter());
}
通过changeRouterInfo()
,它会先将数据保存到数据库中,然后再调用PaymentRouter
类的构造方法,从数据库中读出信息并更新到内存,并替换整个PaymentRouter
实例。
这里可能有点绕:先保存到数据库,然后再从数据库中读取到内存,是不是可以不从数据库读而直接保存到内存?
当然可以这么做,但是必须要考虑到多线程的影响,并采取相应措施。
感谢支持
更多内容,请移步《超级个体》。