我们认为,由于思维定式原子变量总是比同步运行的速度更快,我想是这样也已经,直到实现了ID在第一次测试过程生成器不具有在这样一个迷迷糊糊的东西。

测试代码:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; public class ConcurrentAdder {
private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);
private static int I = 0;
private static final Object o = new Object();
private static volatile long start; public static void main(final String[] args) {
//每一个线程运行多少次累加
int round = 10000000;
//线程个个数
int threadn = 20;
start = System.currentTimeMillis();
atomicAdder(threadn, round);
//syncAdder(threadn, round);
} static void atomicAdder(int threadn, int addTimes) {
int stop = threadn * addTimes;
List<Thread> list = new ArrayList<Thread>();
for (int i = 0; i < threadn; i++) {
list.add(startAtomic(addTimes, stop));
}
for (Thread each : list) {
each.start();
} } static Thread startAtomic(final int addTimes, final int stop) {
Thread ret = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < addTimes; i++) {
int v = ATOMIC_INTEGER.incrementAndGet();
if (stop == v) {
System.out.println("value:" + v);
System.out.println("elapsed(ms):" + (System.currentTimeMillis() - start));
System.exit(1);
}
}
}
});
ret.setDaemon(false);
return ret;
} static void syncAdder(int threadn, int addTimes) {
int stop = threadn * addTimes;
List<Thread> list = new ArrayList<Thread>();
for (int i = 0; i < threadn; i++) {
list.add(startSync(addTimes, stop));
}
for (Thread each : list) {
each.start();
} } static Thread startSync(final int addTimes, final int stop) {
Thread ret = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < addTimes; i++) {
synchronized (o) {
I++;
if (stop == I) {
System.out.println("value:" + I);
System.out
.println("elapsed(ms):" + (System.currentTimeMillis() - start));
System.exit(1);
}
}
}
}
});
ret.setDaemon(false);
return ret;
}
}

这是一个非常easy的累加器,N个线程并发累加,每一个线程累加R次。

分别凝视

atomicAdder(threadn, round);//原子变量累加
syncAdder(threadn, round);//同步累加

中的一行运行还有一行

笔者机器的配置:i5-2520M 2.5G 四核

N=20

R=10000000

结果:

原子累加:15344 ms

同步累加:10647 ms

问题出来了,为什么同步累加会比原子累加要快50%左右?

@ 我们知道java加锁的过程是(内置sync和显式lock类似),要加锁的线程检查下锁是否被占用。假设被占用则增加到目标锁的等待队列。假设没有则。加锁。

这里我们每一个线程获取到锁累加之后就立刻又去获取锁,这时其它线程还没有被唤醒。锁又被当前线程拿到了。

这也就是非公平锁可能造成的饥饿问题。

可是这一个原因不能解释50%的性能提升?理论上。在一个绝对时间。总有一个线程累加成功,那么两种累加器的耗时应该近似才对。

那么是有什么提升了同步累加的性能。或者是什么减少了原子累加的性能?

@接下来笔者分别perf了一下两种累加器的运行过程:

第一次运行的是原子累加器,第二次运行的同步累加器。

wxf@pc:/data$ perf stat -e cs -e L1-dcache-load-misses java ConcurrentAdder
value:100000000
elapsed(ms):8580 Performance counter stats for 'java ConcurrentAdder 1 100 1000000': 21,841 cs
233,140,754 L1-dcache-load-misses
8.633037253 seconds time elapsed
wxf@pc:/data$ perf stat -e cs -e L1-dcache-load-misses java ConcurrentAdder
value:100000000
elapsed(ms):5749 Performance counter stats for 'java ConcurrentAdder 2 100 1000000': 55,522 cs
28,160,673 L1-dcache-load-misses
5.811499179 seconds time elapsed

我们能够看出,同步累加的上下文切换是要比原子累加多。这个能够理解,加锁本身就会添加线程的切换。

再看,原子累加器的L1缓存失效比同步累加器高一个数量级

笔者茅塞顿开,原子操作会导致缓存一致性问题。从而导致频繁的缓存行失效。缓存一致性协议MESI见:http://en.wikipedia.org/wiki/MESI_protocol

可是这时同步累加器在一个CPU周期内重复的获取锁操作。缓存并没有失效。

再把每次累加的线程ID输出来,会发现。原子累加的线程分布要分散非常多。

回到问题上来。为什么我们会一直觉得原子操作比加锁要快呢?文中的样例是非常特别非常特别的,正常业务场景下,我们累加过后,要经过非常多业务代码逻辑才会再次去累加,这里已经跨过非常多个CPU时间片了。从而同步累加器非常难一直获取到锁。这中情况下,同步累加器即会有等待加锁的性能损失还会有缓存一致性带来的性能损失。

所以在一般的情况下,同步累加器会慢非常多。

版权声明:本文博客原创文章。博客,未经同意,不得转载。

谈论Java原子变量和同步的效率 -- 颠覆你的生活的更多相关文章

  1. Java原子变量

    实现全局自增id最简单有效的方式是什么?java.util.concurrent.atomic包定义了一些常见类型的原子变量.这些原子变量为我们提供了一种操作单一变量无锁(lock-free)的线程安 ...

  2. Java原子变量类需要注意的问题

    在学习多线程时,遇到了原子变量类,它是基于 CAS 和 volatile 实现的,能够保障对共享变量进行 read-modify-write 更新操作的原子性和可见性.于是我就写了一段代码试试,自认为 ...

  3. 全面了解 Java 原子变量类

    目录   一.原子变量类简介  二.基本类型  三.引用类型  四.数组类型  五.属性更新器类型  参考资料

  4. 基础篇:JAVA原子组件和同步组件

    前言 在使用多线程并发编程的时,经常会遇到对共享变量修改操作.此时我们可以选择ConcurrentHashMap,ConcurrentLinkedQueue来进行安全地存储数据.但如果单单是涉及状态的 ...

  5. 用Java原子变量的CAS方法实现一个自旋锁

    为了防止无良网站的爬虫抓取文章,特此标识,转载请注明文章出处.LaplaceDemon/ShiJiaqi. http://www.cnblogs.com/shijiaqi1066/p/5999610. ...

  6. Java并发编程实战 第15章 原子变量和非阻塞同步机制

    非阻塞的同步机制 简单的说,那就是又要实现同步,又不使用锁. 与基于锁的方案相比,非阻塞算法的实现要麻烦的多,但是它的可伸缩性和活跃性上拥有巨大的优势. 实现非阻塞算法的常见方法就是使用volatil ...

  7. 《Java并发编程实战》第十五章 原子变量与非堵塞同步机制 读书笔记

    一.锁的劣势 锁定后假设未释放.再次请求锁时会造成堵塞.多线程调度通常遇到堵塞会进行上下文切换,造成很多其它的开销. 在挂起与恢复线程等过程中存在着非常大的开销,而且通常存在着较长时间的中断. 锁可能 ...

  8. java并发编程(8)原子变量和非阻塞的同步机制

    原子变量和非阻塞的同步机制 一.锁的劣势 1.在多线程下:锁的挂起和恢复等过程存在着很大的开销(及时现代的jvm会判断何时使用挂起,何时自旋等待) 2.volatile:轻量级别的同步机制,但是不能用 ...

  9. Java多线程并发编程之原子变量与非阻塞同步机制

    1.非阻塞算法 非阻塞算法属于并发算法,它们可以安全地派生它们的线程,不通过锁定派生,而是通过低级的原子性的硬件原生形式 -- 例如比较和交换.非阻塞算法的设计与实现极为困难,但是它们能够提供更好的吞 ...

随机推荐

  1. python urllib和urllib2 区别

    python有一个基础的库叫httplib.httplib实现了HTTP和HTTPS的客户端协议,一般不直接使用,在python更高层的封装模块中(urllib,urllib2)使用了它的http实现 ...

  2. UNIX 缩写风格

    构建于图形界面之上的操作系统,使用鼠标作为主输入设备, 是否使用缩写并不重要.比如 Windows 系统中的目录,几乎都是全称…… 点击两次鼠标进入文件夹 pf, 并不意味着点击13次才能进入文件夹  ...

  3. Nginx 开启 debug 日志的办法

    译序:一般来讲,Nginx 的错误日志级别是 error,作为 Nginx 用户来讲,你设置成 info 就足够用了.         但有时有些难以挖掘的 bug,需要看到更详细的 debug 级别 ...

  4. os.path.exists(path) 和 os.path.lexists(path) 的区别

    使用os.path.exists()方法可以直接判断文件是否存在.代码如下:>>> import os>>> os.path.exists(r'C:\1.TXT') ...

  5. (收藏)KMP算法的前缀next数组最通俗的解释

    我们在一个母字符串中查找一个子字符串有很多方法.KMP是一种最常见的改进算法,它可以在匹配过程中失配的情况下,有效地多往后面跳几个字符,加快匹配速度. 当然我们可以看到这个算法针对的是子串有对称属性, ...

  6. Jquery Ajax时 error处理 之 parsererror

    Jquery Ajax时 error处理 之 parsererror     01 $.ajax({ 02         type: "POST", 03         con ...

  7. win7 ShuipFCMS 配置 及问题

    随风下载地址:http://www.shuipfcms.com/download.shtml 碰到问题: 一.mb_strlen 函数不支持   到php.ini  开启 ;extension=php ...

  8. C++获取当前时间和计算程序运行时间的方法

    C++获取当前时间和计算程序运行时间的方法 获取当前时间: #include <iostream> #include <Windows.h> using namespace s ...

  9. ZOJ 3204 Connect them MST-Kruscal

    这道题目麻烦在输出的时候需要按照字典序输出,不过写了 Compare 函数还是比较简单的 因为是裸的 Kruscal ,所以就直接上代码了- Source Code : //#pragma comme ...

  10. 2014 HDU多校弟五场J题 【矩阵乘积】

    题意很简单,就是两个大矩阵相乘,然后求乘积. 用 Strassen算法 的话,当N的规模达到100左右就会StackOverFlow了 况且输入的数据范围可达到800,如果变量还不用全局变量的话连内存 ...