Java多线程之—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<String> stack = new SynchronizedStack();
        TreiberStack<String>  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/stackSynchronizedStackTreiberStack
1259130263106
2414647409145
4596424534784
810877881098736
1615020441713802
3225240173345929
6445735647033072
128846958114803696
2561766108930156804
5123512836463126440

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

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

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

public class TreiberStack<E> {
    private AtomicReference<Node<E>> headNode = new AtomicReference<>();
    //记录实际执行次数
    public static final LongAdder adder=new LongAdder();
    public void push(E item) {
        Node<E> newHead = new Node<>(item);
        Node<E> oldHead;
        do {
            adder.increment();
            oldHead = headNode.get();
            newHead.next = oldHead;
        } while (!headNode.compareAndSet(oldHead, newHead));
    }
    public E pop() {
        Node<E> oldHead;
        Node<E> 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<E> {
        public final E item;
        public Node<E> 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<String>  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希望执行次数实际执行次数
110001000
220002000
440004038
880008334
161600016390
323200032688
646400065115
128128000138662
256256000286673
512512000898106

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

结果

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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值