JVM 的内存模型十分复杂,难以理解, <<Java 并发编程实战>>告诉我们,除非你对 JVM 的线程安全原理十分熟悉,否则应该严格遵守基本的 Java 线程安全规则,使用 Java 内置的线程安全的类及关键字。那我们应该怎么确保JAVA线程的安全呢?
一.熟练使用线程安全类
ConcurrentHashMap
反例:
map.get 以及 map.put 操作是非原子操作,多线程并发修改的情况下可能导致一致性问题。比如线程 A 调用 append 方法,在第 6 行时,线程 B 删除了 key。
public class ConcurrentHashMapExample {
private Map<String, String> map = new ConcurrentHashMap<>();
// 堆代码 duidaima.com
public void appendIfExists(String key, String suffix) {
String value = map.get(key);
if (value != null) {
map.put(key, value + suffix);
}
}
}
正例:
public class ConcurrentHashMapExample {
private Map<String, String> map = new ConcurrentHashMap<>();
public void append(String key, String suffix) {
// 使用 computeIfPresent 原子操作
map.computeIfPresent(key, (k, v) -> v + suffix);
}
}
二.保证变更的原子性
反例:
@Getter
public class NoAtomicDiamondParser {
private volatile int start;
private volatile int end;
public NoAtomicDiamondParser() {
Diamond.addListener("dataId", "groupId", new ManagerListenerAdapter() {
@Override
public void receiveConfigInfo(String s) {
JSONObject jsonObject = JSON.parseObject(s);
start = jsonObject.getIntValue("start");
end = jsonObject.getIntValue("end");
}
});
}
}
public class MyController{
private final NoAtomicDiamondParser noAtomicDiamondParser;
public void handleRange(){
// end 读取的旧值, start 读取的新值, start 可能大于 end
int end = noAtomicDiamondParser.getEnd();
int start = noAtomicDiamondParser.getStart();
}
}
正例:
@Getter
public class AtomicDiamondParser {
// 堆代码 duidaima.com
private volatile Range range;
public AtomicDiamondParser() {
Diamond.addListener("dataId", "groupId", new ManagerListenerAdapter() {
@Override
public void receiveConfigInfo(String s) {
range = JSON.parseObject(s, Range.class);
}
});
}
@Data
public static class Range {
private int start;
private int end;
}
}
public class MyController {
private final AtomicDiamondParser atomicDiamondParser;
public void handleRange() {
Range range = atomicDiamondParser.getRange();
System.out.println(range.getStart());
System.out.println(range.getEnd());
}
}
三.使用不可变对象
当一个对象是不可变的,那这个对象内就自然不存在线程安全问题,如果需要修改这个对象,那就必须创建一个新的对象,这种方式适用于简单的值对象类型,常见的例子就是 java 中的 String 和 BigDecimal。对于上面一个例子,我们也可以将 Range 设计为一个通用的值对象。
正例:
@Getter
public class AtomicDiamondParser {
private volatile Range range;
public AtomicDiamondParser() {
Diamond.addListener("dataId", "groupId", new ManagerListenerAdapter() {
@Override
public void receiveConfigInfo(String s) {
JSONObject jsonObject = JSON.parseObject(s);
int start = jsonObject.getIntValue("start");
int end = jsonObject.getIntValue("end");
range = new Range(start, end);
}
});
}
// 堆代码 duidaima.com
// lombok 注解会保证 Range 类的不变性
@Value
public static class Range {
private int start;
private int end;
}
}
四.正确性优先于性能
不要因为担心性能问题而放弃使用 synchronized,volatile 等关键字,或者采用一些非常规写法
反例 双重检查锁:
class Foo {
// 缺少 volatile 关键字
private Helper helper = null;
public Helper getHelper() {
if (helper == null)
synchronized(this) {
if (helper == null)
helper = new Helper();
}
return helper;
}
}
在上述例子中,在 helper 字段上增加 volatile 关键字,能够在 java 5 及之后的版本中保证线程安全。
正例:
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null)
synchronized(this) {
if (helper == null)
helper = new Helper();
}
return helper;
}
}
正例3(推荐):
class Foo {
private Helper helper = null;
public synchronized Helper getHelper() {
if (helper == null)
helper = new Helper();
}
return helper;
}
五.并不严谨的 Diamond Parser:
/**
* 省略异常处理等其他逻辑
*/
@Getter
public class DiamondParser {
// 缺少 volatile 关键字
private Config config;
public DiamondParser() {
Diamond.addListener("dataId", "groupId", new ManagerListenerAdapter() {
@Override
public void receiveConfigInfo(String s) {
config = JSON.parseObject(s, Config.class);
}
});
}
@Data
public static class Config {
private String name;
}
}
这种 Diamond 写法可能从来没有发生过线上问题,但这种写法也确实是不符合 JVM 线程安全原则。未来某一天你的代码跑在另一个 JVM 实现上,可能就有问题了。