我相信大家都很熟悉 DCL, 对于缺少实践经验的程序开发人员来说, DCL 的学习基本限制在单例模式, 但我发现在高并发场景中会经常遇到需要用到 DCL 的场景, 但并非用做单例模式, 其实 DCL 的核心思想和 CopyOnWrite 很相似, 就是在需要的时候才加锁; 为了说明这个观点, 我先把单例的经典代码防止如下:
先说明几个关键词:
volatile: 保证线程的可见性, 有序性; 这两点非常重要, 可见性让线程可以马上获释主存变化, 二有序性避免指令重排序出现问题;
- public class Singleton {
- // 通过 volatile 关键字来确保安全
- private volatile static Singleton singleton;
- private Singleton(){}
- public static Singleton getInstance(){
- if(singleton == null){
- synchronized (Singleton.class){
- if(singleton == null){
- singleton = new Singleton();
- }
- }
- }
- return singleton;
- }
- }
大家可以知道, 这段代码是没有性能瓶颈的线程安全 (当然, 用了 volatile 是有一定的性能影响, 但起码不需要竞争锁); 这代码只会在需要的时候才加锁, 这就是 DCL 的需要时加锁的特性, 由第一个检查 check 保证 (也就是 if (singleton == null));
但 DCL 的需要时才加锁的魅力不仅仅如此场景而已, 我们看一个需求: 一个不要求实时性的更新, 所有线程公用一个资源, 而且只有满足某个条件的时候才更新, 那么多线程需要访问缓存时, 是否需要加锁呢? 不需要的, 看如下代码:
- private static volatile JSONArray cache = new JSONArray(Collections.synchronizedList(new LinkedList<>()));
- public static int updateAeProduct(JSONObject aeProduct,String productId,boolean isFlush){
- JSONObject task = new JSONObject();
- String whereStr ="{\"productId\": {\"operation\": \"eq\", \"value\":\""+productId+"\" },\"provider\":{\"operation\": \"eq\", \"value\":\"aliExpress\" }}";
- task.put("where",JSON.parseObject(whereStr));
- task.put("params",aeProduct);
- cache.add(task);
- if(cache.size()>2 ||isFlush){
- // 争夺更新权
- JSONArray temp=cache;
- synchronized (updateLock){
- if(temp==cache&&cache.contains(task)){
- cache = new JSONArray(Collections.synchronizedList(new LinkedList<>()));
- }else {
- return 1;
- }
- }
- // 拥有更新权的继续更新
- try {
- Map<String,String> headers = new HashMap<>();
- headers.put("Content-Type","application/json");
- String response = HttpUtils.post(updateapi,temp.toJSONString(),headers);
- JSONObject result = JSON.parseObject(response);
- if(result!=null&&"Success".equals(result.getString("msg"))){
- // System.out.println("========================= 完成一次批量存储, 成功 Flush:"+temp.size());
- }
- } catch (Exception e) {
- System.out.println("更新丢失, 策略补救");
- e.printStackTrace();
- }
- }
- return 1;
- }
这样保证了性能, 也做到了缓存的线程安全; 这就是单例的厉害; 我在项目中经常遇到该类场景, 下面给出一个任务计时器的代码:
- package com.mobisummer.spider.master.component;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.concurrent.ScheduledExecutorService;
- import java.util.concurrent.atomic.AtomicLong;
- public class RateCalculator {
- ConcurrentHashMap<String,AtomicLong> taskInfo = new ConcurrentHashMap();
- volatile boolean isStart =false;
- Object lock = new Object();
- AtomicLong allCount = new AtomicLong();
- private ScheduledExecutorService scheduledThreadPool;
- public void consume(Long num,String taskId){
- if(taskInfo.containsKey(taskId)){
- taskInfo.get(taskId).addAndGet(num);
- }else {
- calculateTask(num,taskId);
- }
- allCount.addAndGet(num);
- calculateAll(num,taskId);
- }
- /**
- * 计算任务
- * @param num
- * @param taskId
- */
- private void calculateTask(Long num,String taskId){
- synchronized (lock){
- if(taskInfo.containsKey(taskId)){
- return;
- }else {
- taskInfo.put(taskId,new AtomicLong());
- Thread countor = new Thread(new Runnable() {
- @Override
- public void run() {
- while (true){
- double startTime =System.currentTimeMillis();
- double startCount = taskInfo.get(taskId).get();
- try {
- Thread.sleep(10000);
- } catch (InterruptedException e) {
- System.out.println("计数器失效");
- }
- double endTime =System.currentTimeMillis();
- double endCount = taskInfo.get(taskId).get();
- double percent =(endCount-startCount)/((endTime - startTime)/1000);
- // System.out.println("目前总成功爬取速率:==========="+percent+"======= 目前处理总数 ========:"+allCount);
- System.out.println("目前"+taskId+"成功爬取速率:==========="+percent+"======= 目前"+taskId+"处理总数 ========:"+endCount);
- }
- }
- });
- countor.start();
- }
- }
- }
- /**
- * 计算所有任务
- * @param num
- * @param taskId
- */
- private void calculateAll(Long num,String taskId){
- if(isStart){
- return;
- }else {
- synchronized (this){
- if(isStart){
- return;
- }else {
- isStart =true;
- Thread countor = new Thread(new Runnable() {
- @Override
- public void run() {
- while (true){
- double startTime =System.currentTimeMillis();
- double startCount = allCount.get();
- try {
- Thread.sleep(10000);
- } catch (InterruptedException e) {
- System.out.println("计数器失效");
- }
- double endTime =System.currentTimeMillis();
- double endCount = allCount.get();
- double percent =(endCount-startCount)/((endTime - startTime)/1000);
- System.out.println("目前总成功爬取速率:==========="+percent+"======= 目前处理总数 ========:"+allCount);
- // System.out.println("目前"+taskId+"成功爬取速率:==========="+percent+"======= 目前"+taskId+"处理总数 ========:"+allCount);
- }
- }
- });
- countor.start();
- }
- }
- }
- }
- }
同样的, 线程安全的双重检测, 这就是 DCL 的魅力;
来源: http://www.bubuko.com/infodetail-2741085.html