Java进阶之深入理解原子变量

1 什么是原子变量?为什么需要它们呢?

(1)对于count++这种操作来说,通常使用synchronized关键字保证原子更新操作,synchronized会保证多线程不会同时更新变量count。但是,使用synchronzied成本太高了,需要先获取锁,最后还要释放锁,获取不到锁的情况下还要等待,还会有线程的上下文切换,这些都需要成本。
(2)而Java从Jdk 1.5开始提供了java.util.concurrent.atomic包(并发包),这个包中的原子操作类提供了一种用法简单、性能高效、线程安全的更新一个变量的方式。并发包基本原子变量类型有:原子更新基本类型、原子更新数组、原子更新引用、和原子更新属性(字段)
(3)之所以称为原子变量,是因为其包含一些以原子方式实现组合操作的方法。
(4)JAVA1.5开始引入了CAS,主要代码都放在JUC的atomic包下,如下图:
在这里插入图片描述

2 原子更新基本类型

2.1 AtomicInteger详解

2.1.1 基本用法

// 第一个构造方法给定了一个初始值,第二个的初始值为0。
public AtomicInteger(int initialValue)
public AtomicInteger()
// 可以直接获取或设置AtomicInteger中的值,方法是:
public final int get()
public final void set(int newValue) 
// 以原子方式获取旧值并设置新值
public final int getAndSet(int newValue)
// 以原子方式获取旧值并给当前值加1
public final int getAndIncrement()
// 以原子方式获取旧值并给当前值减1
public final int getAndDecrement()
// 以原子方式获取旧值并给当前值加delta
public final int getAndAdd(int delta)
// 以原子方式给当前值加1并获取新值
public final int incrementAndGet()
// 以原子方式给当前值减1并获取新值
public final int decrementAndGet()
// 以原子方式给当前值加delta并获取新值
public final int addAndGet(int delta)

2.1.2 为什么需要AtomicInteger原子操作类?

(1)测试源码

public class AtomicIntegerTest {

    public static void mainTest() {
//        testAtomicInteger(true); // 测试int
        testAtomicInteger(false);  // 测试AtomicInteger
    }

    // 数量达到一定才容易看效果
    private static final int THREADS_COUNT = 10000;
    private static final int NUM = 100;
    private static int sCount = 0;
    private static AtomicInteger sAtomicCount = new AtomicInteger(0);

    private static void testAtomicInteger(final boolean isAtomicInteger) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < NUM; i++) {
                        try {
                            Thread.sleep(10);
                            if (isAtomicInteger) {
                                sAtomicCount.incrementAndGet();
                            } else {
                                sCount++;
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            threads[i].start();
        }

        // 让调用join()的线程先执行
        for (int i = 0; i < THREADS_COUNT; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 再执行主线程
        System.out.println(isAtomicInteger ? "AtomicInteger:" + sAtomicCount.get() : "Int:" + sCount);
    }
}

(2)对于Java中的运算操作,例如自增或自减,若没有进行额外的同步操作,在多线程环境下就是线程不安全的。count++解析为count=count+1,明显,这个操作不具备原子性,多线程并发共享这个变量时必然会出现问题。结果如下:

2019-08-25 21:18:20.159 16917-16917/com.seniorlibs.thread I/System.out: Int:999323
2019-08-25 21:19:14.190 27071-27071/com.seniorlibs.thread I/System.out: Int:997635

(3)用了AtomicInteger类后会变成什么样子呢?

2019-08-25 21:17:46.887 6623-6623/com.seniorlibs.thread I/System.out: AtomicInteger:1000000
2019-08-25 21:17:46.887 6623-6623/com.seniorlibs.thread I/System.out: AtomicInteger:1000000

结果每次都输出1000000,程序输出了正确的结果,这都归功于AtomicInteger.incrementAndGet()方法的原子性。

2.1.3 CAS机制

2.1.3.1 什么是非阻塞同步?

(1)同步:多线程并发访问共享数据时,保证共享数据再同一时刻只被一个或一些线程使用。
(2)阻塞同步和非阻塞同步都是实现线程安全的两个保障手段,非阻塞同步对于阻塞同步而言主要解决了阻塞同步中线程阻塞和唤醒带来的性能问题。
(3)那什么叫做非阻塞同步呢?在并发环境下,某个线程对共享变量先进行操作,如果没有其他线程争用共享数据那操作就成功;如果存在数据的争用冲突,那就才去补偿措施,比如不断的重试机制,直到成功为止,因为这种乐观的并发策略不需要把线程挂起,也就把这种同步操作称为非阻塞同步(操作和冲突检测具备原子性)。

2.1.3.2 什么是CAS?

(1)在硬件指令集的发展驱动下,使得 “操作和冲突检测” 这种看起来需要多次操作的行为只需要一条处理器指令便可以完成,这些指令中就包括非常著名的CAS指令(Compare-And-Swap比较并交换)
(2)将当前值E和期望结果值N比较,在相等时,将该内存位置的值更新为结果值V;不相等时写入失败,重新读取当前值E进行再次比较,直到成功为止,如图所示:
在这里插入图片描述

2.1.3.3 CAS机制的缺陷——ABA问题?

(1)CAS可以有效的提升并发的效率,但同时也会引入ABA问题,简单描述:你女朋友分手后,又经历了其他男人,又和你复合了,你不知道她经历了什么。

(2)具体问题:如线程1从内存X中取出A,这时线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的。比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。

(3)解决办法:Java中提供了AtomicStampedReference/AtomicMarkableReference来处理会发生ABA问题的场景,主要是在对象中额外再增加一个版本号/标记来标识对象是否有过变更,除了比较值还比较版本号。基础类型简单值不需要版本号。

2.1.4 源码分析

2.1.4.1 incrementAndGet()方法

    /**
     * incrementAndGet()方法在一个无限循环体内,不断尝试将一个比当前值大1的新值赋给自己,
     * 如果失败则说明在执行"CAS"操作的时已经被其它线程修改过了,于是便再次进入循环下一次操作,直到成功为止
     * @return the updated value
     */
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next)) // compareAndSet()
                return next;
        }
    }

2.1.4.2 compareAndSet()方法

/**
 * 如果内存位置的内容{@code ==}是给期望结果值,则原子性地将该值设置为给定的更新结果值。
 *
 * @param 期望结果值
 * @param 更新结果值
 * @成功返回true,false表示内存位置的内容不等于期望结果值。
 */
 public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

2.1.4.3 Unsafe

JAVA中的CAS操作都是通过sun包下Unsafe类实现,而Unsafe类中的方法都是native方法,由JVM本地实现,,查看了openJDK7的源码,下面就稍作分析:
在这里插入图片描述

2.1.4.4 Atomic:comxchg()

最后调用的是Atomic:comxchg这个方法,这个方法的实现放在hotspot下的os_cpu包中,说明这个方法的实现和操作系统、CPU都有关系,以linux的X86处理器的实现为例来进行分析:
在这里插入图片描述
linux的X86下主要是通过lock cmpxchgl 指令在CPU级完成CAS操作的,在多处理器情况下,CAS操作操作必须使用lock指令加锁来完成原子性操作,从这个例子就可以比较清晰的了解CAS的底层实现了。

2.1.4.5 lock指令的几个作用

(1)锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放。现在处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁缓存期间其他CPU没法访问内存
(2)lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据;
(3)不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序;
(4)硬件层面:lock指令在执行后面指令的时候锁定一个北桥信号;

2.1.5 学习链接

原子操作类AtomicInteger详解

Java编程的逻辑(70) - 原子变量和CAS

Java中的CAS实现原理

发布了185 篇原创文章 · 获赞 207 · 访问量 59万+

猜你喜欢

转载自blog.csdn.net/chenliguan/article/details/100068960
今日推荐