zoukankan      html  css  js  c++  java
  • Java原子变量

    概述

    多个线程操作共享变量(Java堆内存上的数据)会带来bug,Java提供了锁机制(Lock)来管理多线程并发,比如synchronized,但是会带来额外的性能开销(线程阻塞,上下文切换等)。为了提升性能,Java引入了原子变量,通过无锁算法(lock-free)实现多线程安全,比如CAS。
    原子变量只是实现多线程安全的一个手段,在对单个共享变量进行”读取-修改-写入“操作的场景下很适合,所以,其适用场景没有synchronized广泛。

    多线程问题

    首先,实现一个计数器,代码如下:

    public class Counter {
        private volatile int num;
    
        public void increment() {
            num++;
        }
    
        public static void main(String[] args) {
            Counter counter = new Counter();
            // 多线程递增计数器
            IntStream.range(0, 100).parallel().forEach(i -> counter.increment());
            // 打印结果
            System.out.println("counter: " + counter.num);
        }
    }
    

    多次运行上述代码,打印出来的值不是100,而是98,97等。
    这是一个典型的多线程问题,num++ 看似一行简单的代码,像是一个原子操作,其实则不然,递增操作可能会三个步骤进行:

    1. 读取当前num变量的值
    2. 执行num+1
    3. 将+1后的值赋值给num变量

    所以,多个线程更新后的值会出现覆盖的情况,比如两个线程同时拿到了num的值为50,在各自的线程中执行加法操作后为51,然后更新主存中的值为51,但是我们期望的值是52。

    通过synchronized解决

    给increment()方法增加synchronized关键字,如下:

    // ...
    public synchronized void increment() {
        num++;
    }
    // ...
    

    synchronized是Java中最常用的锁,保证被“监控”代码块在同一个时刻只能由一个线程执行,所以最终出来的结果为100,正确。
    但是,该方法会导致没获取锁的线程挂起,发生上下文切换,这就是重量级锁带来的性能开销。

    通过原子变量AtomicInteger解决

    atomic包下有AtomicInteger类,可以解决上述问题,代码如下:

    public class Counter {
        private AtomicInteger num = new AtomicInteger(0);
    
        public void increment() {
            while (true) {
                int oldValue = num.get();
                int newValue = oldValue + 1;
                if (num.compareAndSet(oldValue, newValue)) {
                    return;
                }
            }
        }
    
        public static void main(String[] args) {
            Counter counter = new Counter();
            // 多线程递增计数器
            IntStream.range(0, 100).parallel().forEach(i -> counter.increment());
            // 打印结果
            System.out.println("counter: " + counter.num);
        }
    }
    

    运行代码,输出结果100。

    CAS原子操作

    Java并发包下的原子变量利用了CAS机制,实现了原子操作。这儿说的原子操作,是指CPU对某一块内存的原子操作(Atomic memory operation),具备如下特点:

    • 串行化多个线程对同一块内存的更新操作(保证多线程更新数据时的安全)。
    • 读取-修改-写入这三个操作不可被中断,更新操作要不然成功,要不然失败,不会出现中间状态(保证数据完整性)。
    • 只有当内存中的值与期望值相同时,才会执行更新操作(保证正确的逻辑)。

    在并发编程中,CAS属于”乐观锁“,假设多线程竞争几率很小,或者在很短的时间内竞争状态会结束,如果多线程竞争非常频繁,会使CPU长时间空转(busy waiting),造成资源浪费。所以,没有银弹!根据场景选择技术方案。

    CAS(Compare And Swap)需要特定的CPU指令支持,所以并不是所有硬件平台都支持CAS。Java跨平台的特性要求API的行为一致性,所以在不支持CAS的硬件平台上,atomic会退化成重量级锁。

    总结

    实现多线程的手段很多,根据场景选择合理的技术方案可以提升程序的性能。本文简单讲述了Java中原子变量是如何解决多线程问题,以及CAS的一些概念。

    参考:
    [1] Why do we use atomic variables instead of a volatile in Java?
    [2] An Introduction to Atomic Variables in Java
    [3] When to use AtomicReference in Java?
    [4] Threads and Locks
    [5] Compare-and-swap
    [6] Understanding and Using Atomic Memory Operations

  • 相关阅读:
    OpenCV基本函数学习
    OpenCV:概述、结构和内容
    NS2典型例子简单分析
    NS2仿真过程中需要的语言及基本组件
    洛谷 P2393 yyy loves Maths II
    洛谷 P1922 女仆咖啡厅桌游吧
    洛谷 P2559 [AHOI2002]哈利·波特与魔法石
    洛谷 P1301 魔鬼之城
    洛谷 P2383 狗哥玩木棒
    洛谷 P2298 Mzc和男家丁的游戏
  • 原文地址:https://www.cnblogs.com/junejs/p/12686921.html
Copyright © 2011-2022 走看看