DCL并非单例模式专用

Posted icanhua

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DCL并非单例模式专用相关的知识,希望对你有一定的参考价值。

  我相信大家都很熟悉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的魅力;


以上是关于DCL并非单例模式专用的主要内容,如果未能解决你的问题,请参考以下文章

DCL 单例模式是否需要volatile?

DCL单例模式你不知道的秘密

DCL_单例模式

DCL单例模式浅析

Java枚举单例模式比DCL和静态单例要好?

单例模式双重检查(DCL)引发的多线程问题