Java:volatile变量,synchronized和AtomicInteger的性能比较

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/diangangqin/article/details/100780983

对于Java中volatle型变量的介绍,《深入理解Java虚拟机-JVM高级特性与最佳实践(周志明 著)》介绍的比较全面和易懂,当一个变量定义为volatile之后,将具备一种特性是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的,而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,先变量值才会对线程B可见。volatile变量虽然对所有线程是立即可见的,但是基于volatile变量的运算在并发下是不安全的,因为Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全。对于普通变量想达到马上可见需要使用synchronized。
并举了下面的例子。

public class VolatileTest {
    public static volatile int race = 0;
    public static void increase() {
        race++;
    }
    private static final int THREADS_COUNT = 20;
    private static final int INCREASE_COUNT = 10000;
    
    public static void main (String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        System.out.println(System.currentTimeMillis());
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run () {
                    for (int i = 0; i < INCREASE_COUNT; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 1) {
             Thread.yield();
        }
        System.out.println(System.currentTimeMillis());
        System.out.println(race);
    }
}

这段代码发起了20个线程,每个线程对race变量进行10000次自增操作,如果这段代码能够正确并发的话,最终输出的结果应该是200000。我使用JDK的版本是"1.8.0_222",运行环境是"1.8.0_222-8u222-b10-1ubuntu1~18.04.1-b10",实际测试的结果的确都是比200000小,而且小多了,我的实际结果是96080,89060,54169,58840,71617,52860,59977,60540,连200000一半都没有。如果想得出正确结果200000,只需将increase()添加一个synchronized即可。如下:

public synchronized static void increase() {
    race++;
}

修改后的实际测试结果都是200000,就没有任何问题了。这时候我想到变量race不需要volatile修饰,结果应该也是正确的,试验后的确没有问题,结果仍然是200000。对于上面这个例子,有没有volatile对结果都没有影响。但是有没有volatile对性能是否有影响呢,我本身想根据理论猜测一下,但是奈何理论底子比较弱,对JVM理解没有那么深刻,很难推测出来,但是第一感觉应该是性能基本一样,但是感觉不一定正确,所以需要做一下实验了,比对一下有volatile修饰和没有volatile修饰,哪一个性能更好,跑得更快,volatile在synchronized下,性能如何呢?
另外最近在看Android code的时候,发现大量采用了Atomic的变量AtomicInteger,了解了Atomic的基本含义和意义后,认为将变量race修改为AtomicInteger,不使用synchronized就能得到正确结果。code如下:

import java.util.concurrent.atomic.AtomicInteger;
public class VolatileTest {
    public static AtomicInteger race = new AtomicInteger(0);
    public static void increase() {
        race.getAndIncrement();
    }
    private static final int THREADS_COUNT = 20;
    private static final int INCREASE_COUNT = 10000;
    
    public static void main (String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        System.out.println(System.currentTimeMillis());
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run () {
                    for (int i = 0; i < INCREASE_COUNT; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 1) {
             Thread.yield();
        }
        System.out.println(System.currentTimeMillis());
        System.out.println(race);
    }
}

测试结果的确是200000,没有问题。现在就需要测试Atomic和synchronized的性能哪个高,哪个低。从事务发展规律来看Atomic的性能应该比synchronized高,不然的话就没有必要JDK1.5之后引入atomic包了, 另外synchronized本身就是一个悲观锁,性能就是有些担忧,尤其是multi thread出现竞争锁的情况。而Atomic变量是底层使用了处理器提供的原子指令,在无锁的情形下进行原子操作,效率上应该是好一点。那我们就做三个基本测试在synchronized下没有volatile修饰变量的测试称为non-volatile-sync test,在synchronized下有volatile修饰变量的测试称为volatile-sync test以及最后一个Atomic test,另外对THREADS_COUNT和INCREASE_COUNT取不同的值进行测试。结果如下(单位是毫秒):

THREADS_COUNT=20/INCREASE_COUNT=10000                 
non-volatile-sync test   46, 49, 40, 41, 35, 45, 43, 46, 38, 38, 39, 39, 42, 45, 37, 37, 46, 42, 47, 46
volatile-sync test       46, 44, 49, 57, 44, 47, 46, 37, 43, 40, 56, 41, 42, 43, 42, 41, 39, 43, 42, 38
Atomic test              23, 20, 26, 26, 23, 25, 23, 23, 24, 18, 24, 23, 25, 25, 25, 20, 22, 25, 27, 23

THREADS_COUNT=200/INCREASE_COUNT=10000  
non-volatile-sync test  121, 133, 122, 141, 125, 231, 124, 219, 112, 129, 128, 113, 104, 145, 140, 128, 148, 104, 111, 159
volatile-sync test      182, 183, 225, 238, 165, 155, 205, 215, 226, 177, 237, 162, 162, 134, 167, 172, 200, 149, 173, 164
Atomic test             92, 87, 87, 85, 93, 87, 74, 94, 113, 73, 92, 85, 98, 83, 82, 90, 94, 90. 96, 94

THREADS_COUNT=20/INCREASE_COUNT=100000  
non-volatile-sync test  133, 121, 130, 132, 127, 121, 150, 127, 154, 124, 121, 112, 120, 122, 118, 119, 119, 105, 118, 130
volatile-sync test      134, 115, 133, 137, 127, 143, 129, 130, 138, 139, 116, 104, 129, 126, 141, 123, 130, 132, 135, 113
Atomic test             107, 102, 116, 105, 109, 110, 104, 120, 88, 98, 113, 110, 109, 108, 92, 86, 102, 107, 99, 112

THREADS_COUNT=200/INCREASE_COUNT=100000  
non-volatile-sync test  561, 512, 467, 481, 492, 650, 549, 550, 449, 493, 588, 810, 563, 558, 579, 611, 493, 615, 632, 603
volatile-sync test      704, 730, 769, 731, 686, 728, 742, 795, 717, 783, 753, 749, 737, 763, 868, 767, 766, 766, 801, 743
Atomic test             362, 324, 338, 348, 343, 343, 337, 322, 348, 329, 330, 333, 341, 340, 326, 337, 334, 323, 335, 330

这里就不计算均值和方差了,因为测试的次数不多,计算出的均值和方差也不是很准确,我们只能看数据的大体趋势,如果想得到准确数据,必须要增加测试样本的数量,才有统计意义。
从上面的几次结果看,Atomic test的结果都要优于另两个测试,并且比较稳定,符合Atomic的猜测,毕竟Atomic使用了底层处理器的原子指令,性能上有比较明显的优势。而volatile-sync test和non-volatile-sync test结果比较就稍微复杂些了。在THREADS_COUNT=20的两个测试,两个的结果从数据看是基本一样的,没有明显的高低区别,但是THREADS_COUNT=200的时候性能上差异就可以看出来了,尤其是INCREASE_COUNT=100000的时候volatile-sync test的平均值明显高于non-volatile-sync test,虽然non-volatile-sync test也出现过"810"这种比较大的值,但总体上volatile-sync test性能上要比non-volatile-sync test差。我们基本上可以准确推测随着thread数量的增多,volatile-sync test的性能会越来越差。原因可能是volatile需要保证所有线程的可见性造成的性能消耗,而保证可见性在synchronized的修饰下其实是没有必要的,因为synchronized也会保证可见性,相当于volatile白做了一次;除了这种原因,也有可能是volatile第二个语义禁止指令重排序优化造成的。但是我更倾向于第一种猜测因为随着thread的数量的增多,volatile-sync test性能在变差,如果是指令重排序优化造成的性能差异应该在thread数量比较少的时候应该也能体现出来。
那是否是说我们以后都全部使用Atomic变量呢?当然不是了。Atomic支持的类型是有限的,如果是和上面例子相近的,当然用Atomic变量最好,也就是多个thread会对同一变量进行修改,那就可以用Atomic变量,如果只是单一线程修改变量的值还是可以用volatile。如果函数已经用synchronized修饰了,那请不要用volatile再次修饰变量了,性能可能会变差。

最后再提及一下volatile的使用场景,《深入理解Java虚拟机-JVM高级特性与最佳实践(周志明 著)》有介绍由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过枷锁(使用synchronized或者java.util.concurrent中的原子类)来保证原子性。
1 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2 变量不需要与其他的状态变量共同参与不变约束。

猜你喜欢

转载自blog.csdn.net/diangangqin/article/details/100780983