最近在做接口限流时涉及到了一个有意思问题,牵扯出了关于concurrentHashMap的一些用法,劳务派遣管理系统,以及CAS的一些观念。限流算法许多,我主要就以最简朴的计数器法来做引。先抽象化一下需求:统计每个接口会见的次数。一个接口对应一个url,也就是一个字符串,每挪用一次对其举办加一处理惩罚。大概呈现的问题主要有三个:
但这次的博客并不是想描写怎么去实现接口限流,而是主要想描写一下碰着的问题,所以,第二点临时不思量,即不利用Redis。
说到并发的字符串统计,当即让人遐想到的数据布局即是ConcurrentHashpMap<String,Long> urlCounter;
假如你方才打仗并发大概会写出如代码清单1的代码
代码清单1:
public class CounterDemo1 { private final Map<String, Long> urlCounter = new ConcurrentHashMap<>(); //接口挪用次数+1 public long increase(String url) { Long oldValue = urlCounter.get(url); Long newValue = (oldValue == null) ? 1L : oldValue + 1; urlCounter.put(url, newValue); return newValue; } //获取挪用次数 public Long getCount(String url){ return urlCounter.get(url); } public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(10); final CounterDemo1 counterDemo = new CounterDemo1(); int callTime = 100000; final String url = "http://localhost:8080/hello"; CountDownLatch countDownLatch = new CountDownLatch(callTime); //模仿并发环境下的接口挪用统计 for(int i=0;i<callTime;i++){ executor.execute(new Runnable() { @Override public void run() { counterDemo.increase(url); countDownLatch.countDown(); } }); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } executor.shutdown(); //期待所有线程统计完成后输出挪用次数 System.out.println("挪用次数:"+counterDemo.getCount(url)); } } console output: 挪用次数:96526
都说concurrentHashMap是个线程安详的并发容器,所以没有显示加同步,实际结果呢并不如所愿。
问题就出在increase要领,劳务派遣管理系统,concurrentHashMap能担保的是每一个操纵(put,get,delete…)自己是线程安详的,可是我们的increase要领,对concurrentHashMap的操纵是一个组合,先get再put,所以多个线程的操纵呈现了包围。假如对整个increase要领加锁,那么又违背了我们利用并发容器的初志,因为锁的开销很大。我们有没有要领改进统计要领呢?
代码清单2摆列了concurrentHashMap父接口concurrentMap的一个很是有用可是又经常被忽略的要领。
代码清单2:
/** * Replaces the entry for a key only if currently mapped to a given value. * This is equivalent to * <pre> {@code * if (map.containsKey(key) && Objects.equals(map.get(key), oldValue)) { * map.put(key, newValue); * return true; * } else * return false; * }</pre> * * except that the action is performed atomically. */ boolean replace(K key, V oldValue, V newValue);
这其实就是一个最典范的CAS操纵,except that the action is performed atomically.这句话真是帮了大忙,我们可以担保较量和配置是一个原子操纵,当A线程实验在increase时,旧值被修改的话就回导致replace失效,而我们只需要用一个轮回,不绝获取最新值,直到乐成replace一次,软件开发,即可完成统计。
改造后的increase要领如下
代码清单3:
public long increase2(String url) { Long oldValue, newValue; while (true) { oldValue = urlCounter.get(url); if (oldValue == null) { newValue = 1l; //初始化乐成,退出轮回 if (urlCounter.putIfAbsent(url, 1l) == null) break; //假如初始化失败,说明其他线程已经初始化过了 } else { newValue = oldValue + 1; //+1乐成,退出轮回 if (urlCounter.replace(url, oldValue, newValue)) break; //假如+1失败,说明其他线程已经修悔改了旧值 } } return newValue; } console output: 挪用次数:100000