最近工作上遇到一个需求:需要根据nginx日志去统计每个域名的qps(Query Per Second,每秒查询率)数据。

解决了日志读取等问题之后,为了写一个尽可能高效的统计模块,我决定用多线程去计数,然后将统计结果保存在Map中。用多线程去计数的需求还是比较常见的。

HashMap 线程不安全,操作时只能加synchronized,结果还是单线程的计数,效率太低。ConcurrentHashMap是线程安全的,就用它了。

先看第一版代码:

     // 先定义一个全局的Map
private Map<String, Integer> counter = new ConcurrentHashMap<>(); // 统计方法
private static final String separator = "|-|";
public void countLog(NginxLog nginxLog) {
String key = nginxLog.getHost() + separator + nginxLog.getDate();
// 先取一下之前的值,然后再加一插入进去
Integer oldValue = counter.putIfAbsent(key, 1);
if (oldValue != null) {
counter.put(key, oldValue.intValue() + 1);
}
}

这段统计代码显然是不行的,ConcurrentHashMap虽然是线程安全类,并且也能保证所提供的方法是线程安全的,但是这并不代表使用它你的程序就是线程安全的。

在这段代码中counter.putIfAbsent()操作是原子性操作,counter.put()也是原子操作。但两者组合起来这就产生问题了。

我们举个例子:比如说现在有两个线程先后运行到counter.putIfAbsent()方法,然后两个线程都取到了同样的oldValue值,假设此值为10,然后两个线程都将执行counter.put()方法,此时两个线程都是在执行counter.put(key, 11)。这显然是不合理的,计数次数理应为12的。

为了解决这个问题,我想到了两种思路:

1、给countLog方法加上synchronized同步,如此使用ConcurrentHashMap就没有多大必要了,改成HashMap好了,这就是最开始的思路,代码如下:

     private Map<String, Integer> counter = new HashMap<>();

     public synchronized void countLog(NginxLog nginxLog) {
String key = nginxLog.getHost() + separator + nginxLog.getDate();
// 先取一下之前的值,然后再加一插入进去
Integer oldValue = counter.putIfAbsent(key, 1);
if (oldValue != null) {
counter.put(key, oldValue.intValue() + 1);
}
}

执行测试运行结果为:

2017-11-28 14:43:17,292  INFO NginxLogCountTest - count: 100000, costTime: 23 ms

因为加了同步锁,相当于计数都是单线程在进行的,因此统计结果也是正确的,耗时23ms

2、第二种思路,使用AtomicInteger类计数。ConcurrentHashMap和AtomicInteger类组合。代码如下:

     private Map<String, AtomicInteger> counter = new ConcurrentHashMap<>();

     public void countLog(NginxLog nginxLog) {
String key = nginxLog.getHost() + separator + nginxLog.getDate();
AtomicInteger oldValue = counter.putIfAbsent(key, new AtomicInteger(1));
if (oldValue != null) {
oldValue.incrementAndGet();
}
}

执行测试运行结果为:

2017-11-28 14:53:14,655  INFO NginxLogCountTest - count: 100000, costTime: 11 ms

这种解决方案里面将AtomicInteger和ConcurrentHashMap组合到一起,counter.putIfAbsent()执行后可以获得当前值的AtomicInteger对象,这个时候使用AtomicInteger对象的incrementAndGet方法。这种组合相当于将两步操作分担给两个线程安全类来处理了。

从执行时间来看相对于单线程计数也还是有一定优势的。

最后附上测试用的代码:

     @Resource
private NginxLogCount nginxLogCount; @Test
public void testCountLog() {
NginxLog nginxLog = new NginxLog();
nginxLog.setHost("test.com");
nginxLog.setDate("2017-11-28T11:43:46+08:00");
String key = nginxLog.getHost() + "|-|" + nginxLog.getDate();
long startTime = System.currentTimeMillis();
for (int j = 0; j < 10; j++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
nginxLogCount.countLog(nginxLog);
}
}
});
thread.setDaemon(false);
thread.start();
}
long endTime = System.currentTimeMillis();
try {
// 等待运行结束
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("count: {}, costTime: {} ms", nginxLogCount.getValue(key), endTime - startTime);
} // getValue方法
public int getValue(String key) {
AtomicInteger value = counter.get(key);
return value == null ? 0 : value.intValue();
}

JAVA多线程统计日志计数时的线程安全及效率问题的更多相关文章

  1. Java多线程系列--“基础篇”06之 线程让步

    概要 本章,会对Thread中的线程让步方法yield()进行介绍.涉及到的内容包括:1. yield()介绍2. yield()示例3. yield() 与 wait()的比较 转载请注明出处:ht ...

  2. Java多线程系列--“基础篇”07之 线程休眠

    概要 本章,会对Thread中sleep()方法进行介绍.涉及到的内容包括:1. sleep()介绍2. sleep()示例3. sleep() 与 wait()的比较 转载请注明出处:http:// ...

  3. Java多线程系列--“基础篇”10之 线程优先级和守护线程

    概要 本章,会对守护线程和线程优先级进行介绍.涉及到的内容包括:1. 线程优先级的介绍2. 线程优先级的示例3. 守护线程的示例 转载请注明出处:http://www.cnblogs.com/skyw ...

  4. Java多线程(三)如何创建线程

    点我跳过黑哥的卑鄙广告行为,进入正文. Java多线程系列更新中~ 正式篇: Java多线程(一) 什么是线程 Java多线程(二)关于多线程的CPU密集型和IO密集型这件事 Java多线程(三)如何 ...

  5. Java多线程(一) 什么是线程

    声明:本系列大多是翻译自https://www.javatpoint.com,加上自己的增删改,尽力写的系统而通俗易懂,后文不再重复声明. 点我跳过黑哥的卑鄙广告行为,进入正文. Java多线程系列更 ...

  6. “全栈2019”Java多线程第十四章:线程与堆栈详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  7. “全栈2019”Java多线程第十章:Thread.State线程状态详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  8. “全栈2019”Java多线程第七章:等待线程死亡join()方法详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  9. “全栈2019”Java多线程第六章:中断线程interrupt()方法详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

随机推荐

  1. Vue之彻底理解自定义组件的v-model

    最近在学习vue,今天看到自定义事件的表单输入组件,纠结了一会会然后恍然大悟...官方教程写得不是很详细,所以我决定总结一下. v-model语法糖 v-model实现了表单输入的双向绑定,我们一般是 ...

  2. ASP.NET异常处理机制

    try{ //获取并使用资源,可能出现异常}catch(DivideByZeroException de){}catch(ArithmeticException ae){}catch(Exceptio ...

  3. 《java.util.concurrent 包源码阅读》28 Phaser 第二部分

    这一部分来分析Phaser关于线程等待的实现.所谓线程等待Phaser的当前phase结束并转到下一个phase的过程.Phaser提供了三个方法: // 不可中断,没有超时的版本 public in ...

  4. C++ UTF8和GB2312相互转换

    #include <Windows.h> #include <string> using std::string; void UTF8_to_GB2312(const char ...

  5. Android 异步消息处理机制终结篇 :深入理解 Looper、Handler、Message、MessageQueue四者关系

    版权声明:本文出自汪磊的博客,转载请务必注明出处. 一.概述 我们知道更新UI操作我们需要在UI线程中操作,如果在子线程中更新UI会发生异常可能导致崩溃,但是在UI线程中进行耗时操作又会导致ANR,这 ...

  6. 51nod 1020 逆序排列 DP

    在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序.一个排列中逆序的总数就称为这个排列的逆序数. 如2 4 3 1中,2 1,4 3,4 1,3 1是逆序 ...

  7. c++ 中 pair 的 使用方法

    原转载地址:点击打开链接 pair的类型: pair 是 一种模版类型.每个pair 可以存储两个值.这两种值无限制.也可以将自己写的struct的对象放进去.. pair<string,int ...

  8. FastDFS教程IV-文件服务器集群搭建

    1.简介     本文主要介绍FastDFS文件服务器的集群搭建,在阅读本文之前,您需具备FastDFS文件服务器单节点安装,扩容,迁移等方面的知识.同时,您还需了解Keepalived,nginx方 ...

  9. Python 学习(1) 简单的小爬虫

    最近抽空学了两天的Python,基础知识都看完了,正好想申请个联通日租卡,就花了2小时写了个小爬虫,爬一下联通日租卡的申请页面,看有没有好记一点的手机号~   人工挑眼都挑花了. 用的IDE是PyCh ...

  10. Ubuntu使用之Svn命令小技巧

    注: [svn Path]:是指要代替码分支的server绝对路径 [Path]:是指终端相对当前文件夹的相对路径.假设是在当前文件夹下,就省略路径 ①.取svnserver的代码: svn co [ ...