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