• 确保JAVA线程安全应该遵循哪些使用规则?
  • 发布于 2个月前
  • 83 热度
    0 评论
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 实现上,可能就有问题了。
用户评论