Java多线程奇幻之旅——Synchronized方式和CAS方式实现线程安全性能思考

前言

在上一篇文章中《Java多线程奇幻之旅——CAS算法实现线程安全》,我们介绍了Synchronized和CAS方式实现线程安全类的方法,两种方式一个是锁定阻塞方式,一个是非阻塞方式。本文专注于两种实现方式效率问题。本文是上篇文章的延续,会借用到上文中的代码,如果没有阅读前文请先前往阅读。

旅程开始

1.大胆假设

在设计试验方法之前,针对Synchronized和CAS两种方式的特点,我们先来思考一下两种方式效率如何?
首先,我们在回顾一下两种方式是如何保证线程安全的。Synchronized方式通过大家应该很熟悉,他的行为非常悲观,只要有一个线程进入Synchronized临界区域(确保不被多线程并发访问的区域),其他线程均不能进入,直到早先进入的线程退出临界区域。和Synchronized相比CAS算法则显得乐观多了,他不限制其他线程进入临界区域,但是当一个线程退出临界区域的时候,他必须检查临界区域内数据是否被其他线程修改,一旦被修改,此线程就要做重试操作。

我们举一个生活化的例子加深理解:
我们把线程比作在马路上行驶的汽车,临界区比作道路交叉的十字路口。
如果所有马路上只有一辆车(单线程情况),那么我们无需任何处理。如果马路上不只一辆车要通过十字路口(多线程情况),并且我们不允许车辆在十字路口相撞(线程冲突情况),那么我们必须需要做出一些限制来避免同时通过十字路口的车辆相互碰撞(保证线程安全)。Synchronized方式相当于在路口设置红绿灯,用“红灯停,绿灯行”的基本原则限制两侧路口的汽车同时进入十字路口。而CAS方式就要评司机自觉了,一旦一辆汽车进入十字路口后发现已经有另一辆汽车进入十字路口,他需要退出十字路口重新进入。
我们用生活经验想象一下两种方式的车辆通行效率,我们经常看到在车流不高的路口汽车白白等待红绿灯,显然在车辆比较少的路口设置红绿灯很有可能影响通行效率,所有晚上一旦车流下降,某些路口红绿灯会关闭以调高通过效率。我们也看到在某个高峰时段由于路口红绿灯损坏造成的车辆拥堵,这说明在车流量较多的情况下,红绿灯的使用恰恰能避免拥堵发生。
通过红绿灯的例子我们可以假设,当线程竞争比较少的情况下,CAS算法效率较高,反之,Synchronized方式效率较高。

2.小心求证

借用上文中两种“栈”的代码,构建测试方法:

public static void main(String[] args) {
    long amount = 0;
    int max = 1000;
    for (int k = 0; k < max; k++) {
        long start =System.nanoTime();
        int loops = 1000;
        //分别运行不同的进程数1、2、、4、8、16、32、64...
        int threads =1;
        //分别运行不同的Stack类。
        //SynchronizedStack stack = new SynchronizedStack();
        TreiberStack  stack=new TreiberStack();
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int j = 0; j < threads; j++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < loops; i++) {
                        stack.push("a");
                    }
                }
            });
        }
        pool.shutdown();
        try {
            pool.awaitTermination(1, TimeUnit.HOURS);
        } catch (InterruptedException e) {
        }
        long end = System.nanoTime();
        System.out.println("每次用时:" + (end - start));

        amount += end - start;
    }
    System.out.println("平均用时:" + amount / max);
}

设置不同的threads的值并切换SynchronizedStack类或者TreiberStack类后运行结果如下:

threads/stack SynchronizedStack TreiberStack
1 259130 263106
2 414647 409145
4 596424 534784
8 1087788 1098736
16 1502044 1713802
32 2524017 3345929
64 4573564 7033072
128 8469581 14803696
256 17661089 30156804
512 35128364 63126440

在线程数较少,竞争较少的情况下TreiberStackSynchronizedStack运行结果差距很小,但是随着线程数的增多,竞争加剧,TreiberStackSynchronizedStack执行时间明显延长。

为什么在线程数较少的情况下TreiberStackSynchronizedStack没有明显差别?
在JDK1.6以后对synchronized关键字做了优化,导致加锁的效率提升,所以和非阻塞方式相比效率也不会相差很多。

为什么在线程数较多的情况下TreiberStackSynchronizedStack差别越来越大?
主要原因在于TreiberStack在高并发的情况下会产生大量的竞争,造成大量重试操作。
我们改造一下TreiberStack类,演示这种情况:

public class TreiberStack {
    private AtomicReference> headNode = new AtomicReference<>();
    //记录实际执行次数
    public static final LongAdder adder=new LongAdder();
    public void push(E item) {
        Node newHead = new Node<>(item);
        Node oldHead;
        do {
            adder.increment();
            oldHead = headNode.get();
            newHead.next = oldHead;
        } while (!headNode.compareAndSet(oldHead, newHead));
    }
    public E pop() {
        Node oldHead;
        Node newHead;
        do {
            oldHead = headNode.get();
            if (oldHead == null)
                return null;
            newHead = oldHead.next;
        } while (!headNode.compareAndSet(oldHead, newHead));
        return oldHead.item;
    }
    private static class Node {
        public final E item;
        public Node next;

        public Node(E item) {
            this.item = item;
        }
    }
}

运行测试方法:

public static void main(String[] args) {
    int loops = 1000;
    //分别运行不同的进程数1、2、、4、8、16、32、64...
    int threads =1;
    TreiberStack  stack=new TreiberStack();
    ExecutorService pool = Executors.newCachedThreadPool();
    for (int j = 0; j < threads; j++) {
        pool.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < loops; i++) {
                    stack.push("a");
                }
            }
        });
    }
    pool.shutdown();
    try {
        pool.awaitTermination(1, TimeUnit.HOURS);
    } catch (InterruptedException e) {
    }
    System.out.println("希望执行次数:"+ loops*threads +";希望执行次数:"+ stack.adder.longValue());
}

执行结果如下:

threads/times 希望执行次数 实际执行次数
1 1000 1000
2 2000 2000
4 4000 4038
8 8000 8334
16 16000 16390
32 32000 32688
64 64000 65115
128 128000 138662
256 256000 286673
512 512000 898106

通过结果我们可以发现,随着线程数增多,实际执行结果数越来越多,说明冲突增多重试次数增多。

后记

通过“提出假设——验证假设——证明假设”这一过程,我们确定Synchronized方式和CAS方式在竞争较少的时候性能相差不大,后者略优于前者,而随着冲突加剧,后者性能较前者显著下降。
如果你亲自运行文中测试方法,你还会发现一个现象,无论是TreiberStack类的运行时间还是实际执行次数,在同一线程数下每次运行结果差别较大,而SynchronizedStack类的结果较稳定,可见CAS方式执行的随机性比较大,而Synchronized方式相对稳定

你可能感兴趣的:(Java多线程奇幻之旅——Synchronized方式和CAS方式实现线程安全性能思考)