最近在作接口限流时涉及到了一个有意思问题,牵扯出了关于concurrentHashMap的一些用法,以及CAS的一些概念。限流算法不少,我主要就以最简单的计数器法来作引。先抽象化一下需求:统计每一个接口访问的次数。一个接口对应一个url,也就是一个字符串,每调用一次对其进行加一处理。可能出现的问题主要有三个:html
但此次的博客并非想描述怎么去实现接口限流,而是主要想描述一下遇到的问题,因此,第二点暂时不考虑,即不使用Redis。java
说到并发的字符串统计,当即让人联想到的数据结构即是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是个线程安全的并发容器,因此没有显示加同步,实际效果呢并不如所愿。shell
问题就出在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
再次调用后得到了正确的结果,上述方案看上去比较繁琐,由于第一次调用时须要进行一次初始化,因此多了一个判断,也用到了另外一个CAS操做putIfAbsent,他的源代码描述以下:
代码清单4:
/** * If the specified key is not already associated * with a value, associate it with the given value. * This is equivalent to * <pre> {@code * if (!map.containsKey(key)) * return map.put(key, value); * else * return map.get(key); * }</pre> * * except that the action is performed atomically. * * @implNote This implementation intentionally re-abstracts the * inappropriate default provided in {@code Map}. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with the specified key, or * {@code null} if there was no mapping for the key. * (A {@code null} return can also indicate that the map * previously associated {@code null} with the key, * if the implementation supports null values.) * @throws UnsupportedOperationException if the {@code put} operation * is not supported by this map * @throws ClassCastException if the class of the specified key or value * prevents it from being stored in this map * @throws NullPointerException if the specified key or value is null, * and this map does not permit null keys or values * @throws IllegalArgumentException if some property of the specified key * or value prevents it from being stored in this map */ V putIfAbsent(K key, V value);
简单翻译以下:“若是(调用该方法时)key-value 已经存在,则返回那个 value 值。若是调用时 map 里没有找到 key 的 mapping,返回一个 null 值”。值得注意点的一点就是concurrentHashMap的value是不能存在null值的。实际上呢,上述的方案也能够把Long替换成AtomicLong,能够简化实现, ConcurrentHashMap
private AtomicLongMap<String> urlCounter3 = AtomicLongMap.create(); public long increase3(String url) { long newValue = urlCounter3.incrementAndGet(url); return newValue; } public Long getCount3(String url) { return urlCounter3.get(url); }
看一下他的源码就会发现,其实和代码清单3思路差很少,只不过功能更完善了一点。
和CAS很像的操做,我以前的博客中提到过数据库的乐观锁,用version字段来进行并发控制,其实也是一种compare and swap的思想。
杂谈:网上不少对ConcurrentHashMap的介绍,众所周知,这是一个用分段锁实现的一个线程安全的map容器,可是真正对他的使用场景有介绍的少之又少。面试中能知道这个容器的人也确实很多,问出去,也就回答一个分段锁就没有下文了,但我以为吧,有时候只知其一;不知其二反而会比不知道更可怕。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
https://www.awaimai.com/348.html
网站大规模并发处理方案:电商秒杀与抢购
——————————
今天分享的主题最后的讨论很好,最后部分同窗可能仍是会有疑惑,到底高并发或电商场景下该用乐观锁仍是悲观锁呢?
建议感兴趣的同窗先看下上面那篇文章,对整个背景有个大致了解,再回来我们的问题:
悲观锁和乐观锁是数据库用来保证数据并发安全防止更新丢失的两种方法,PPT列举的例子在select ... for update 前加个事务就能够防止更新丢失。悲观锁和乐观锁大部分场景下差别不会不大,一些独特场景下有一些差异,通常咱们能够从以下几个方面来判断:
1.响应速度:若是须要很是高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不须要等待其余并发去释放锁;
2.冲突频率:若是冲突频率很是高,建议采用悲观锁,保证成功率,若是冲突频率大,乐观锁会须要屡次重试才能成功,资源消耗代价比较大;
3.重试代价:若是重试代价大,建议采用悲观锁,好比付款的时候调用第三方外部接口;
总结起来就是:
秒杀活动是一个并发写的过程,同时也是一个随机性很高的事件,并不须要去关注事务失败率高这个问题,因此采用乐观锁是合适的。但若是要保证事务成功率的话,显然使用乐观锁是一个糟糕的方案。因此到底该用悲观锁仍是乐观锁仍是得看场景和业务需求,还有架构。
[1] 非阻塞同步算法与CAS(Compare and Swap)无锁算法
http://www.cnblogs.com/Mainz/p/3546347.html
小白科普:悲观锁和乐观锁
并发一枝花之 ConcurrentLinkedQueue
[2] ConcurrentHashMap使用示例
https://my.oschina.net/mononite/blog/144329
[3] 深度剖析ConcurrentHashMap源码
http://blog.csdn.net/xiaoxian8023/article/details/49249091
[4] CAS下ABA问题及优化方案 | 架构师之路
库存扣多了,到底怎么整 | 架构师之路
http://chuansong.me/n/1921434646119
库存扣减还有这么多方案? | 架构师之路
http://chuansong.me/n/1921434546720
[5] Java并发编程——锁与可重入锁
http://www.jianshu.com/p/007bd7029faf
java的可重入锁用在哪些场合?
https://www.zhihu.com/question/23284564
java自旋锁
http://www.jianshu.com/p/dfbe0ebfec95
java锁的种类以及辨析(一):自旋锁
http://ifeve.com/java_lock_see1/
[6] Disruptor简介
http://blog.csdn.net/winwill2012/article/details/71718809
高性能队列——Disruptor
https://zhuanlan.zhihu.com/p/23863915
并发框架DISRUPTOR译文
http://coolshell.cn/articles/9169.html
[7] Java并发编程-原子性变量