资讯专栏INFORMATION COLUMN

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

Chaz / 1180人阅读

摘要:前言在上一篇文章中多线程奇幻之旅算法实现线程安全,我们介绍了和方式实现线程安全类的方法,两种方式一个是锁定阻塞方式,一个是非阻塞方式。

前言

在上一篇文章中《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方式相对稳定

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68994.html

相关文章

  • Java线程奇幻之旅——CAS算法实现线程安全

    摘要:大多数保证线程安全的方法是添加各种类型锁,使用各种同步机制,用限制对共享的可变的类变量并发访问的方式来保证线程安全。只有保证这两条语句及中间语句以原子方式执行,才能避免多线程覆盖问题。 前言 对于线程安全,我们有说不尽的话题。大多数保证线程安全的方法是添加各种类型锁,使用各种同步机制,用限制对共享的、可变的类变量并发访问的方式来保证线程安全。文本从另一个角度,使用比较交换算法(Comp...

    jasperyang 评论0 收藏0
  • i++ 是线程安全的吗?

    摘要:例子先来看下面的示例来验证下到底是不是线程安全的。上面的例子我们期望的结果应该是,但运行遍,你会发现总是不为,至少你现在知道了操作它不是线程安全的了。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。 例子 先来看下面的示例来验证下 i++ 到底是不是线程安全的。 1000个线程,每个线程对共享变量 count 进行 1000 次 ++ 操作。 showImg(https://s...

    RyanQ 评论0 收藏0
  • java高并发系列 - 第21天:java中的CAS操作,java并发的基石

    摘要:方法由两个参数,表示期望的值,表示要给设置的新值。操作包含三个操作数内存位置预期原值和新值。如果处的值尚未同时更改,则操作成功。中就使用了这样的操作。上面操作还有一点是将事务范围缩小了,也提升了系统并发处理的性能。 这是java高并发系列第21篇文章。 本文主要内容 从网站计数器实现中一步步引出CAS操作 介绍java中的CAS及CAS可能存在的问题 悲观锁和乐观锁的一些介绍及数据库...

    zorro 评论0 收藏0
  • Week 1 - Java 线程 - CAS

    摘要:前言学习情况记录时间子目标多线程记录在学习线程安全知识点中,关于的有关知识点。对于资源竞争严重线程冲突严重的情况,自旋的概率会比较大,从而浪费更多的资源,效率低于。 前言 学习情况记录 时间:week 1 SMART子目标 :Java 多线程 记录在学习线程安全知识点中,关于CAS的有关知识点。 线程安全是指:多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表...

    ZweiZhao 评论0 收藏0
  • 线程安全

    摘要:不可变在中,不可变的对象一定是线程安全的。在里标注自己是线程安全的类,大多都不是绝对线程安全,比如某些情况下类在调用端也需要额外的同步措施。无同步方案要保证线程安全,不一定就得需要数据的同步,两者没有因果关系。 在之前学习编程的时候,有一个概念根深蒂固,即程序=算法+数据结构。数据代表问题空间中的客体,代码就用来处理这些数据,这种思维是站在计算机的角度去抽象问题和解决问题,称之为面向过...

    fuyi501 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<