java ‘i++’ 计数器的性能测试
- 所谓的volatile
- Synchronized同步原语
- JDK1.5的AtomicLong
- Java8 LongAdder Vs AtomicLong
- 总结:比较,该用哪个 ?
- CAS(compare and swap)
前言
在写多线程中,我们免不了使用到计数器,今天就来分析下java中提供给我们的计数器以及它们的性能测试。
1.所谓的volatile
上一篇文章我也写到了volatile的作用:当我们写一个变量时,它会被立刻刷新到主内存中去,保证了变量对其他线程的可见性,不会发生线程在自己的私有内存中更新了数据却没有同步到主内存中。并且在后来的JDK版本volatile语义被增强,它会限定部分内存重排的规则来保证线程安全性
虽然volatile用来保证线程安全性,但是我们要注意volatile修饰的变量并不是线程安全的。也就是说volatile只是起 辅助 作用,它不保证修饰的变量是原子的。
来看段简单的代码:
public class JavaVolatile {
volatile static int i = 0;
public static void main(String args[]) throws InterruptedException{
class NewThread extends Thread {
public void run() {
for (int j = 0; j < 1000000; ++j) {
++i;
}
}
}
double start = System.currentTimeMillis();
NewThread nt1 = new NewThread();
NewThread nt2 = new NewThread();
NewThread nt3 = new NewThread();
nt1.start();
nt2.start();
nt3.start();
nt1.join();
nt2.join();
nt3.join();
double end = System.currentTimeMillis();
System.out.println(i);
System.out.println("time:" + (end-start));
}
}
从图片我们可以看出volatile修饰的变量是不保证原子性的(正确结果应该是3000000),原因比如线程A和线程B同时操作i时,A,B线程同时更新了本地,然后两个线程同时刷新到内存中问题就出现了。但是随着JVM的优化,volatile的用处也越来越少了,我们可以使用后面将要说的几种,然而一般用volatile修饰boolean类型是个不错的主意。
注意:volatile修饰和自己本身无关的变量操作时是原子的,n++不是,但若n = m+1或者 n = ture是原子的,原因可以想想volatile的原理。
2.Synchronized同步原语
Synchronized可以修饰一个方法或者一个代码块,并且在某一时刻保证只有一个线程可以访问这个方法或者代码块。
Synchronized原理其实就是java中每个对象都有一个监视器,或叫做锁,当访问该对象Synchronized方法或代码块是就会上锁,直到访问完毕或者抛出异常才会释放锁。
由此可见Synchronized是保证能同步的,毕竟涉及到了lock锁机制,但是效率相对来说也是比较低的,毕竟涉及到加锁和解锁,而且在加锁的情况下其他线程访问也会被阻塞。
代码
public class JavaSynchronize {
public static int i = 0;
static synchronized void incre(){
++i;
}
public static void main(String args[]) throws InterruptedException {
class NewThread extends Thread{
public void run(){
for(int j = 0; j < 1000000; ++j){
incre();
}
}
}
double start = System.currentTimeMillis();
NewThread nt1 = new NewThread();
NewThread nt2 = new NewThread();
NewThread nt3 = new NewThread();
nt1.start();
nt2.start();
nt3.start();
nt1.join();
nt2.join();
nt3.join();
double end = System.currentTimeMillis();
System.out.println(i);
System.out.println(end-start);
}
}
结果:
3.JDK1.5的AtomicLong
java se5引入了AtomicLong这个原子类,为我们封装了类似i++的操作,所以我们可以直接简单的使用。
public class JavaThread {
public static void main(String args[]) throws InterruptedException {
//AtomicInteger i = new AtomicInteger(0);
AtomicLong i = new AtomicLong(0);
class NewThread extends Thread{
public void run(){
for(int j = 0; j < 1000000; ++j) {
i.addAndGet(1);
}
}
}
double start = System.currentTimeMillis();
NewThread nt1 = new NewThread();
NewThread nt2 = new NewThread();
NewThread nt3 = new NewThread();
nt1.start();
nt2.start();
nt3.start();
nt1.join();
nt2.join();
nt3.join();
double end = System.currentTimeMillis();
System.out.println(i.get());
System.out.println(end-start);
}
}
代码中我注释了AtomicInteger,是原子整型,AtomicLong是长整型,在实际测试过程中AtomicInteger速度要比AtomicLong快
下图是AtomicInteger
所以,我们是选择计数器时还是要根据实际情况选择并且根据自己的机器情况来选择效率最高的。
4.Java8 LongAddr Vs AtomicLong
最新的java8更新了不少东西,其中就包括新的原子计数器LongAddr,既然它能更新进来说明它的效率是更好的^_^,使用和前面的AtomicLong没什么区别,而且它其实就是用来代替AtomicLong的。
看代码:
public class JavaLongAddr {
public static void main(String args[]) throws InterruptedException {
LongAdder la = new LongAdder();
class CountThread extends Thread{
public void run(){
for(int i = 0; i < 1000000; ++i){
la.increment();
}
}
}
double start = System.currentTimeMillis();
CountThread ct1 = new CountThread();
CountThread ct2 = new CountThread();
CountThread ct3 = new CountThread();
ct1.start();
ct2.start();
ct3.start();
ct1.join();
ct2.join();
ct3.join();
double end = System.currentTimeMillis();
System.out.println(la);
System.out.println(end-start);
}
}
我测试了多次取了个平均的是50ms左右,实际上最快达到了30ms,说明了LongAdder的效率和AtomicLong的效率相比是非常高的,快了不只一倍。但java8并没有所谓的IntegerAdder,只增加了LongAdder和DoubleAdder,如果感兴趣为什么变高效了可在网上搜索,有很多^_^。
如果想进一步了解AtomicLong和LongAddr请见http://www.importnew.com/9560.html
分析的很清楚
5.总结:比较,该用哪个?
以我的观点来说,还是具体情况具体分析,每个人需求不同,所处环境也不一样,如果我们想要自己的程序达到最大的效率话,那就动手测试吧!亲自找到效率最高的吧!,我也只能提供个思路和大致的情况。
下面是我的测试,每种取 5组数据(单位ms):
ThreadNum | Synchronzied | AtomicLong | LongAddr |
---|---|---|---|
1 | 36/42/39/33/32 | 29/52/26/30/28 | 23/28/29/37/24 |
2 | 60/68/104/69/51 | 66/97/80/96/100 | 57/42/46/37/46 |
4 | 153/281/276/178/235 | 144/160/145/149/121 | 63/62/59/65/71 |
8 | 497/587/168/245/531 | 447/385/240/351/310 | 116/103/93/99/91 |
16 | 366/549/767/724/568 | 623/890/835/554/886 | 174/157/153/176/163 |
对上面的数据进行平均值计算,方便大家能更好的看出来
ThreadNum | Synchronized | AtomicLong | LongAdder |
---|---|---|---|
1 | 36 | 33 | 28 |
2 | 70 | 88 | 46 |
4 | 225 | 144 | 64 |
8 | 406 | 347 | 100 |
16 | 595 | 758 | 165 |
结果很明显了,LongAdder的效率明显高过AtomicLong和Synchronized,但是在单线程的情况下差别并不大,偶尔另外两个还要高于LongAdder,多线程下 计数器肯定用LongAdder了,效率差了不是一点点,但是AtomicInteger还有待于测试。
另外如果我们想多线程同步的操作不仅仅是计数器,还有其他操作等,可以考虑选择Synchronized。
6.CAS(compare and swap)
前面说了JDK1.5加入了AtomicLong这个原子计数器,且其实JDK1.5引入了java.util.concurrent这个并发编程的包,它其中包括了许多和并发相关的东西,感兴趣的可以自己查下,但是下面我们要介绍的CAS,可以说是如果没有CAS就不会有这个包存在^_^
CAS是什么:compare and swap 一种策略,当A线程访问一个变量时,先取出旧值并且保存,然后保存变化后的新值,此时在比较旧值和内存中的值是否一样(一样说明没有被其他的线程所更改),如果一样则更新新值。
CAS是原子性的,它已经被硬件化到CPU上了,所以它的性能是非常好的。但是它也存在ABA问题,即比较时情况可能是修改后又修改会来这种,那么可能会认为它没有改变。但是一般情况ABA问题影响不大。
关于CAS更加详细可以参考这篇博文
http://www.bdqn.cn/news/201312/12579.shtml