Java多线程之深入理解原子变量

1 CAS原理?

1.1 你知道什么是 CAS 吗?

答:CAS的英文全称是Compare-And-Swap,中文叫做"比较并交换",它是一种思想、一种算法。在多线程的情况下,各个代码的执行顺序是不能确定的,所以为了保证并发安全,我们可以使用互斥锁(悲观锁)。而 CAS的特点是避免使用互斥锁,当多个线程同时使用CAS更新同一个变量时,只有其中一个线程能够操作成功,而其他线程都会更新失败。不过和同步互斥锁不同的是,更新失败的线程并不会被阻塞,而被告知这次由于竞争而导致的操作失败,但还可以再次尝试。

1.2 CAS 的思路?第64讲:你知道什么是 CAS 吗?

答:(1)CAS指令保证原子性:在大多数处理器的指令中,都会实现CAS相关的指令,这一条指令就可以完成“比较并交换”的操作,也正是由于这是一条(而不是多条)CPU指令,所以CAS相关的指令是具备原子性的,这个组合操作在执行期间不会被打断,这样就能保证并发安全。由于这个原子性是由CPU保证的,所以无需我们程序员来操心。

(2)CAS最核心的思路就是,仅当 当前的内存值 V 和 预期值 P 相等时,才将内存值修改为 结果值 RCAS有三个操作数:当前内存值 V(value)、预期值 P(通过偏移地址计算的)、要修改的结果值 R
在这里插入图片描述
(3)实现了乐观锁:利用CAS实现的无锁算法,就像我们谈判的时候,用一种非常乐观的方式去协商,彼此之间很友好,这次没谈成,还可以重试。CAS的思路和之前的互斥锁是两种完全不同的思路,如果是互斥锁,不存在协商机制,多个线程都会尝试抢占资源,如果抢到了,在操作完成前,会把这个资源牢牢的攥在自己的手里,直到执行完毕才轮到其他线程执行。当然,利用CAS和利用互斥锁,都可以保证并发安全,它们是实现同一目标的不同手段。

1.3 CAS 的应用场景?第65讲:CAS 和乐观锁的关系,什么时候会用到 CAS?

答:Doug Lea 大神在 JUC 包中大量使用了 CAS 技术,该技术既能保证安全性,又不需要使用互斥锁,能大大提升工具类的性能。应用场景包括:ConcurrentHashMap,ConcurrentLinkedQueue,原子类。因此,CAS 被广泛应用在并发编程领域中,以实现那些不会被打断的数据交换操作,从而就实现了无锁的线程安全。

1.4 CAS 有什么缺点?第66讲:CAS 有什么缺点?

答:CAS 的三个缺点,分别是 ABA 问题、自旋时间过长以及线程安全的范围不能灵活控制

1.4.1 CAS 最大的缺点就是 ABA 问题?

答:(1)原因:CAS 是否进行 swap 的判断标准是“当前的值和预期的值是否一致”,并不检查值有没有发生过变化,如果变量的值从旧值 A 变成了新值 B 再变回旧值 A,由于最开始的值 A 和现在的值 A 是相等的,所以 CAS 会认为变量的值在此期间没有发生过变化。所以,CAS 并不能检测出在此期间值是不是被修改过,它只能检查出现在的值和最初的值是不是一样。

(2)例子:假设第一个线程拿到的初始值是 100,然后进行计算,在计算的过程中,有第二个线程把初始值改为了 200,然后紧接着又有第三个线程把 200 改回了 100。等到第一个线程计算完毕去执行 CAS 的时候,它会比较当前的值是不是等于最开始拿到的初始值 100,此时会发现确实是等于 100,所以线程一就认为在此期间值没有被修改过,就理所当然的把这个 100 改成刚刚计算出来的新值,但实际上,在此过程中已经有其他线程把这个值修改过了,这样就会发生 ABA 问题。

(3)如何解决 ABA 这个问题呢?添加一个版本号就可以解决在变量值自身之外,再添加一个版本号,那么这个值的变化路径就从 A→B→A 变成了 1A→2B→3A,可以通过对比版本号来判断值是否变化过,这比直接去对比两个值是否一致要更靠谱,所以通过这样的思路就可以解决 ABA 的问题了。

(4)解决 ABA 问题的工具类:在 atomic 包中提供了 AtomicStampedReference 这个类,它是专门用来解决 ABA 问题的,解决思路正是利用版本号。AtomicStampedReference 会维护一种类似 <Object,int> 的数据结构,其中的 int 就是用于计数的,也就是版本号,它可以对这个对象和 int 版本号同时进行原子更新,从而也就解决了 ABA 问题。因为我们去判断它是否被修改过,不再是以值是否发生变化为标准,而是以版本号是否变化为标准,即使值一样,它们的版本号也是不同的

1.4.2 自旋时间过长

答:由于单次 CAS 不一定能执行成功,所以 CAS 往往是配合着循环来实现的,有的时候甚至是死循环,不停地进行重试,直到线程竞争不激烈的时候,才能修改成功。

可是如果我们的应用场景本身就是高并发的场景,就有可能导致 CAS 一直都操作不成功,这样的话,循环时间就会越来越长。而且在此期间,CPU 资源也是一直在被消耗的,这会对性能产生很大的影响。所以这就要求我们,要根据实际情况来选择是否使用 CAS,在高并发的场景下,通常 CAS 的效率是不高的详细分析请看2.7—>

1.4.3 不能灵活控制线程安全的范围

答:(1)原因:通常我们去执行 CAS 的时候,是针对某一个,而不是多个共享变量的,这个变量可能是 Integer 类型,也有可能是 Long 类型、对象类型等等,但是我们不能针对多个共享变量同时进行 CAS 操作,因为这多个变量之间是独立的,简单的把原子操作组合到一起,并不具备原子性。因此如果我们想对多个对象同时进行 CAS 操作并想保证线程安全的话,是比较困难的。

(1)一个解决方案:那就是利用一个新的类,来整合刚才这一组共享变量,这个新的类中的多个成员变量就是刚才的那多个共享变量,然后再利用 atomic 包中的 AtomicReference 来把这个新对象整体进行 CAS 操作,这样就可以保证线程安全

(2)另一个解决方案:如果我们使用其他的线程安全技术,那么调整线程安全的范围就可能变得非常容易,比如我们用 synchronized 关键字时,如果想把更多的代码加锁,那么只需要把更多的代码放到同步代码块里面就可以了

1.5 什么是非阻塞同步?

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

2 原子类是如何利用 CAS 保证线程安全的?第39讲:原子类是如何利用 CAS 保证线程安全的?

2.1 什么是原子变量?原子类有什么作用?

答:(1)原子性意味着“一组操作要么全都操作成功,要么全都失败,不能只操作成功其中的一部分”。而 java.util.concurrent.atomic 下的类,就是具有原子性的类,可以原子性地执行添加、递增、递减等操作。比如之前多线程下的线程不安全的 i++ 问题,到了原子类这里,就可以用功能相同且线程安全的 getAndIncrement 方法来优雅地解决。

(2)原子类的作用和锁有类似之处,是为了保证并发情况下线程安全。不过原子类相比于锁,有一定的优势

  • 粒度更细:原子变量可以把竞争范围缩小到变量级别,通常情况下,锁的粒度都要大于原子变量的粒度。
  • 效率更高:除了高度竞争的情况之外,使用原子类的效率通常会比使用同步互斥锁的效率更高,因为原子类底层利用了 CAS 操作,不会阻塞线程

2.2 原子类6种类型?各有什么特点?

答:
在这里插入图片描述

2.3 Atomic* 基本类型原子类

第一类 Atomic*,我们把它称为基本类型原子类,它包括三种,分别是 AtomicInteger、AtomicLong 和 AtomicBoolean

我们来介绍最为典型的 AtomicInteger,它是对于 int 类型的封装,并且提供了原子性的访问和更新。也就是说,我们如果需要一个整型的变量,并且这个变量会被运用在并发场景之下,我们可以不用基本类型 int,也不使用包装类型 Integer,而是直接使用 AtomicInteger,这样一来就自动具备了原子能力,使用起来非常方便。

2.3.1 AtomicInteger 类常用方法?
// 第一个构造方法给定了一个初始值,第二个的初始值为0。
public AtomicInteger(int initialValue)
public AtomicInteger()
public final int get() // 获取当前的值
public final void set(int newValue) 

public final int getAndSet(int newValue) // 获取当前的值,并设置新的值
// 接下来的几个方法和它平时的操作相关:
public final int getAndIncrement() // 获取当前的值,并自增
public final int getAndDecrement() // 获取当前的值,并自减
// 如果不能满足需求,我们就可以使用  getAndAdd 方法
public final int getAndAdd(int delta) // 获取当前的值,并加上预期的值,直接一次性地加减我们想要的数值

public final int incrementAndGet() // 以原子方式给当前值加1并获取新值
public final int decrementAndGet() // 以原子方式给当前值减1并获取新值
public final int addAndGet(int delta) // 以原子方式给当前值加delta并获取新值

boolean compareAndSet(int expect, int update) // 如果输入的数值等于预期值,则以原子方式将该值更新为输入值(update)

2.4 Atomic*Array 数组类型原子类

AtomicArray 数组类型原子类,数组里的元素,都可以保证其原子性,比如 AtomicIntegerArray 相当于把 AtomicInteger 聚合起来,组合成一个数组。这样一来,我们如果想用一个每一个元素都具备原子性的数组的话, 就可以使用 AtomicArray。它一共分为 3 种,分别是:

AtomicIntegerArray:整形数组原子类;
AtomicLongArray:长整形数组原子类;
AtomicReferenceArray :引用类型数组原子类。

2.5 Atomic*Reference 引用类型原子类

AtomicReference 类的作用和AtomicInteger 并没有本质区别, AtomicInteger 可以让一个整数保证原子性,而AtomicReference 可以让一个对象保证原子性。这样一来,AtomicReference 的能力明显比 AtomicInteger 强,因为一个对象里可以包含很多属性。在这个类别之下,除了 AtomicReference 之外,还有:

AtomicStampedReference:它是对 AtomicReference 的升级,在此基础上还加了时间戳,用于解决 CAS 的 ABA 问题。
AtomicMarkableReference:和 AtomicReference 类似,多了一个绑定的布尔值,可以用于表示该对象已删除等场景。

2.6 Atomic*FieldUpdater 升级类型原子类

Atomic*FieldUpdater称为原子更新器,一共有三种,分别是:

AtomicIntegerFieldUpdater:原子更新整形的更新器;
AtomicLongFieldUpdater:原子更新长整形的更新器;
AtomicReferenceFieldUpdater:原子更新引用的更新器。
2.6.1 原子更新器的应用场景?

如果我们之前已经有了一个变量,比如是整型的 int,实际它并不具备原子性。可是木已成舟,这个变量已经被定义好了,此时我们有没有办法可以让它拥有原子性呢?就是利用 Atomic*FieldUpdater,如果它是整型的,就使用 AtomicIntegerFieldUpdater 把已经声明的变量进行升级,这样一来这个变量就拥有了 CAS 操作的能力

2.6.2 为什么需要原子更新器?

答:第一种情况是出于历史原因考虑,那么如果出于历史原因的话,之前这个变量已经被声明过了而且被广泛运用,那么修改它成本很高,所以我们可以利用升级的原子类。

另外一种情况是:如果我们在大部分情况下并不需要使用到它的原子性,只在少数情况,比如每天只有定时一两次需要原子操作的话,我们其实没有必要把原来的变量声明为原子类型的变量,因为 AtomicInteger 比普通的变量更加耗费资源。所以如果有成千上万个原子类的实例的话,它占用的内存也会远比成千上万个普通类型占用的内存高。所以在这种情况下,我们可以利用 AtomicIntegerFieldUpdater 作为类的静态成员使用,进行合理升级,更节约内存

2.6.3 使用例子

(1)AtomicLong

class Counter {
    private val counter: AtomicLong = AtomicLong()
    fun increase() {
        counter.incrementAndGet()
    }
    fun get(): Long {
        return counter.get()
    }
}

(2)AtomicLongFieldUpdater

class CompactCounter {
    @Volatile
    private var counter: Long = 0
    /**
     * 注意:如果变量的名称不存在,会直接报错
     * private val updater: AtomicLongFieldUpdater<CompactCounter> = AtomicLongFieldUpdater.newUpdater(CompactCounter::class.java, "countersssss")
     * 报错Caused by: java.lang.RuntimeException: java.lang.NoSuchFieldException: No field countersssss in class Lcom/seniorlibs/thread/atomic/CompactCounter;
     */
    private val updater: AtomicLongFieldUpdater<CompactCounter> =
        AtomicLongFieldUpdater.newUpdater(CompactCounter::class.java, "counter")
        
    fun increase() {
        updater.incrementAndGet(this)
    }
    fun get(): Long {
        return counter
    }
}

(3)测试

object AtomicLongFieldUpdaterTest {
    private const val TAG = "AtomicLongFieldUpdaterTest"
    var normalInt: Int = 0
    var counter: Counter = Counter()
    var compactCounter: CompactCounter = CompactCounter()
    fun mainTest() {
        val r = AtomicRunnable()
        val t1 = Thread(r)
        val t2 = Thread(r)
        t1.start()
        t2.start()
        t1.join()
        t2.join()
        println("$TAG 普通变量的结果:$normalInt")
        println("$TAG 原子变量的结果:${counter.get()}")
        println("$TAG 原子更新器的结果:${compactCounter.get()}")
//        System.out: AtomicLongFieldUpdaterTest 普通变量的结果:199947
//        System.out: AtomicLongFieldUpdaterTest 原子变量的结果:200002
//        System.out: AtomicLongFieldUpdaterTest 原子更新器的结果:200002
    }
    class AtomicRunnable : Runnable {
        override fun run() {
            for (i in 0..100000) {
                normalInt++
                counter.increase()
                compactCounter.increase()
            }
        }
    }
}

(4)结果分析
普通变量由于不具备线程安全性,所以在多线程操作的情况下,它虽然看似进行了 200000 次操作,但有一些操作被冲突抵消了,所以最终结果小于 200000。可是使用 AtomicIntegerFieldUpdater 这个工具之后,就可以做到把一个普通类型的 counter 变量进行原子的自加操作,最后的结果也和加的次数是一样的,也就是 200000。

2.6.4 迁移案例

答:Kotlin的by lazy有多种线程安全模式,如果在程序中写出类成千上万个by lazy(LazyThreadSafetyMode.PUBLICATION),如果使用AtomicReference的话意味着每使用一次lazy,都需要创建一个原子类的实例,也是不可以被接受的。如果将AtomicReferenceFieldUpdater作为类的静态成员使用,全程只有一个实例,会比较节省内存

同理,Kotlin的协程内部也是使用了AtomicReferenceFieldUpdater对结果做原子操作。

private val case: Int by lazy(LazyThreadSafetyMode.PUBLICATION) { 1 }
// 指定[Lazy]实例如何在多个线程之间同步初始化
public enum class LazyThreadSafetyMode {
    // 锁用于确保只有一个线程可以初始化[Lazy]实例。
    SYNCHRONIZED,
    // 并发访问未初始化的[Lazy]实例值时,可以多次调用Initializer函数,但是,只有第一个返回的值将用作[Lazy]实例的值。
    PUBLICATION,
    // 不使用锁来同步对[Lazy]实例值的访问
    NONE,
}
private class SafePublicationLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
    override val value: T
        get() {
            val newValue = initializerValue()
            if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) {
            	return newValue
            }
        }
    companion object {
        private val valueUpdater = java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater(
            SafePublicationLazyImpl::class.java, Any::class.java, "_value")
    }
}

2.7 Adder累加器 第40讲:AtomicInteger 在高并发下性能不好,如何解决?为什么?

它里面有两种累法器,分别叫作 LongAdder 和 DoubleAdder

2.7.1 AtomicInteger在高并发下性能不好,为什么?如何解决?

答:
在这里插入图片描述
答:(1)AtomicLong 内部的 value 属性是被 volatile 修饰的,它保证了自身可见性。但是,volatile 具备以下2点特性:①当写一个volatile共享变量时,JMM会把该线程对应的更新的后的本地内存中的值强制刷新到主内存中②当读一个volatile共享变量时,JMM会把该线程对应的本地内存内存置为无效,然后线程会从主内存中读取最新的值到工作内存中

(2)这样一来,每一次它的数值有变化的时候,它都需要进行 刷新 和 读取。由于竞争很激烈,这样的 刷新 和 读取 操作耗费了很多资源,而且 CAS 也会经常失败

(3)在Java8中新增了LongAdder这个类,这是一个针对Long类型的操作工具类,LongAdder比AtomicLong效率更高。因此,在高并发下,建议使用LongAdder

2.7.2 为什么需要Adder累加器?

答:既然已经有了AtomicLong,为何又要新增LongAdder这一个类呢?因为高并发下LongAdder比AtomicLong效率更高

2.7.3 为什么高并发下LongAdder比AtomicLong效率更高?

答:(1)因为 LongAdder 引入了分段累加的概念,内部一共有两个参数参与计数:第一个叫作 base,它是一个变量,第二个是 Cell[] ,是一个数组

(2)在竞争不激烈的时候,可以直接把累加结果改到 base 变量上;

(3)当竞争激烈的时候:LongAdder 会通过计算出每个线程的 hash 值来给线程分配到 Cell[] 数组中不同的 Cell对象 上去,而不会大家共用同一个。每个 Cell对象 相当于是一个独立的计数器,就不会和其他的计数器干扰,Cell 之间并不存在竞争关系。这样一来,不同线程对应到不同的 Cell 上进行修改,所以在自加的过程中,就大大减少了刚才的 刷新 和 读取,以及降低了冲突的概率本质是空间换时间,因为它有多个计数器同时在工作,所以占用的内存也要相对更大一些

2.7.4 LongAdder最终是如何实现多线程计数的呢?

答:在最后一步的求和 sum 方法,执行 LongAdder.sum() 的时候,会把各个线程里的 Cell 累计求和,并加上 base,形成最终的总和。代码如下:

public long sum() {
   Cell[] as = cells; Cell a;
   long sum = base;
   if (as != null) {
       for (int i = 0; i < as.length; ++i) {
           if ((a = as[i]) != null)
               sum += a.value;
       }
   }
   return sum;
}

**先取 base 的值,然后遍历所有 Cell,把每个 Cell 的值都加上去,形成最终的总和。**由于在统计的时候并没有进行加锁操作,所以这里得出的 sum 不一定是完全准确的,因为有可能在计算 sum 的过程中 Cell 的值被修改了。

2.7.5 如何选择AtomicLong和LongAdder进行计数?

答:(1)在低竞争的情况下,都可以选择。因为竞争不高,AtomicLong和LongAdder这两个类具有相似的特征,吞吐量也是相似的。(2)是在高并发下,优先选择LongAdder。因为LongAdder的预期吞吐量要高得多,经过试验,LongAdder的吞吐量大约是AtomicLong的十倍,不过凡事总要付出代价,LongAdder在保证高效的同时,也需要消耗更多的空间。

2.7.6 AtomicLong 可否被 LongAdder 替代?

答:(1)LongAdder只提供了add、increment等简单的方法,适合的是统计求和计数的场景,场景比较单一。(2)AtomicLong还具有compareAndSet等高级方法,可以应对除了加减之外的更复杂的需要CAS的场景。(3)所以:如果我们的场景仅仅是需要用到加和减操作的话,可以直接使用更高效的LongAdder,但如果需要利用CAS比如compareAndSet等操作的话,就需要使用AtomicLong来完成

2.8 Accumulator积累器 第43讲:Java 8 中 Adder 和 Accumulator 有什么区别?

最后一种叫 Accumulator 积累器,分别是 LongAccumulator 和 DoubleAccumulator

答:Accumulator 和 Adder 非常相似,实际上 Accumulator 就是一个更通用版本的 Adder,比如 LongAccumulator 是 LongAdder 的功能增强版,因为 LongAdder 的 API 只有对数值的加减,而 LongAccumulator 提供了自定义的函数操作。

3 以 AtomicInteger 为例,从源码角度分析在 Java 中如何利用 CAS 实现原子操作?

在充分了解了原子类的作用和种类之后,我们来看下 AtomicInteger 是如何通过 CAS 操作实现并发下的累加操作的,以其中一个重要方法 incrementAndGet() 方法为突破口。

3.1 incrementAndGet()方法

    /**
     * 在 Java 1.8 中的实现如下:
     * 以原子方式将当前值增加一
     * @return the 更新值
     */
    public final int incrementAndGet() {
        return U.getAndAddInt(this, VALUE, 1) + 1;
    }

可以看出,里面使用了 Unsafe 这个类,并且调用了 unsafe.getAndAddInt 方法。所以这里需要简要介绍一下 Unsafe 类。

3.1.1 Unsafe类是什么?有什么作用?

答:Unsafe 其实是 CAS 的核心类。由于 Java 无法直接访问底层操作系统,而是需要通过 native 方法来实现。不过尽管如此,JVM 还是留了一个后门,在 JDK 中有一个 Unsafe 类,它提供了硬件级别的原子操作,我们可以利用它直接操作内存数据

3.2 AtomicInteger 的一些重要代码
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
    private static final long VALUE;
    
    static {
        try {
            VALUE = U.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }

    // 当前内存值 V
    private volatile int value;
}

在数据定义的部分,首先获取了 Unsafe 实例 U,并且定义了 VALUE,在执行时我们会调用 Unsafe 的 objectFieldOffset 方法,从而得到当前这个原子类的 value 的偏移量,并且赋给 VALUE 变量,它的含义是在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据的原值的,这样我们就能通过 Unsafe 来实现 CAS 了。

当前内存值 value 是用 volatile 修饰的,它就是我们原子类存储的值的变量,由于它被 volatile 修饰,我们就可以保证在多线程之间看到的 value 是同一份,保证了可见性

3.3 继续看Unsafe 的 getAndAddInt 方法的实现
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            // 获取当前时刻下的原子类的数值,即是 "期望值"
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

(1)首先我们看一下结构,它是一个 do-while 循环,所以这是一个死循环,直到满足循环的退出条件时才可以退出。

(2)do 后面的这一行代码 var5 = this.getIntVolatile(var1, var2) 是什么意思。这是个 native 方法,作用就是:获取在 当前原子类 中的 在内存中的偏移地址 处的值,并且保存到 var5 里面,此时 var5 实际上代表 当前时刻下的原子类的数值(即"期望值")

// 补充各个参数的作用
var1:object,当前原子类this,也就是 atomicInteger 这个对象本身;;
var2:offset,在内存中的偏移地址,通过它获取当前时刻下的原子类的数值(即"期望值");
var4:delta,希望原子类所改变的数值,比如+1,-1
var5:expectedValue,"期望值V",当前时刻下的原子类的数值
var5+var4:newValue,"新值N",是希望修改的数值

(3)现在再来看 while 的退出条件,也就是compareAndSwapInt(var1, var2, var5, var5 + var4)) 这个方法,为了方便理解,给它们取了新了变量名,分别 object、offset、expectedValue、newValue。所以 compareAndSwapInt 方法的作用就是:判断如果现在原子类里 当前值E(value) 和 期望值V 相等的话,那么就把结果修改为 新值,所以说这行代码就实现了 CAS 的过程

(4)一旦 CAS 操作成功,就会退出这个 while 循环,但是也有可能操作失败。如果操作失败就意味着在获取到 当前时刻下的原子类的数值(即"期望值") 之后,并且在 CAS 操作之前,value 的数值已经发生变化了,证明有其他线程修改过这个变量

(5)这样一来,就会再次执行循环体里面的代码,重新获取 当前时刻下的原子类的数值(即"期望值") 的值,并且再次利用 CAS 去尝试更新,直到更新成功为止,所以这是一个死循环。

(6)总结一下,Unsafe 的 getAndAddInt 方法是通过 循环 + CAS 的方式来实现的,在此过程中,它会通过 compareAndSwapInt 方法来尝试更新 value 的值,如果更新失败就重新获取,然后再次尝试更新,直到更新成功

3.4 深入挖Unsafe的底层实现

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

3.4.1 Atomic:comxchg()方法的作用

最后调用的是Atomic:comxchg这个方法,这个方法的实现放在hotspot下的os_cpu包中,说明这个方法的实现和操作系统、CPU都有关系,以linux的X86处理器的实现为例来进行分析:
在这里插入图片描述
CAS 底层会根据操作系统和处理器的不同来选择对应的调用代码,以 Windows 和 X86 处理器为例,如果是多处理器,通过(一条)带 lock 前缀的 cmpxchg 指令对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作如果是单处理器,通过(一条) cmpxchg 指令完成原子操作

3.4.2 学习链接

第10讲:深入理解 AQS 和 CAS 原理

4 原子类和volatile有什么异同?第41讲:原子类和 volatile 有什么异同?

答:volatile 和原子类的使用场景是不一样的,如果我们有一个可见性问题,那么可以使用 volatile 关键字;但如果我们的问题是一个组合操作,需要用同步来解决原子性问题的话,那么可以使用原子变量,而不能使用 volatile 关键字

(1)volatile 可以用来修饰 boolean 类型的标记位,因为对于标记位来讲,直接的赋值操作本身就是具备原子性的,再加上 volatile 保证了可见性,那么就是线程安全的了。
(2)而对于会被多个线程同时操作的计数器++的场景,它不仅仅是一个简单的赋值操作,而是需要先读取当前的值,然后在此基础上进行一定的修改,再把它给赋值回去。这样一来,我们的 volatile 就不足以保证这种情况的线程安全了,需要使用原子类来保证线程安全。

5 原子类和synchronized有什么异同? 第42讲:AtomicInteger 和 synchronized 的异同点

答:
(1)虽然原子类和 synchronized 都能保证线程安全,但是其实现原理是大有不同的
①原子类保证线程安全的原理是利用了 CAS 操作。
②synchronized 背后的 monitor 锁,同步方法和同步代码块的背后原理会有少许差异,但总体思想是一致的:在执行同步代码之前,需要首先获取到 monitor 锁,执行完毕后,再释放锁。

(2)使用范围的不同
①原子类的使用范围是比较局限的。因为一个原子类仅仅是一个对象,不够灵活。
②synchronized 的使用范围要广泛得多。比如说 synchronized 既可以修饰一个方法,又可以修饰一段代码,相当于可以根据我们的需要,非常灵活地去控制它的应用范围。
③仅有少量的场景,例如计数器等场景,可以使用原子类。而在其他更多的场景下,如果原子类不适用,就可以考虑用 synchronized 来解决这个问题。

(3)粒度的区别
①原子变量的粒度是比较小的,它可以把竞争范围缩小到变量级别。
②synchronized 锁的粒度都要大于原子变量的粒度。如果只把一行代码用 synchronized 给保护起来的话,有一点杀鸡焉用牛刀的感觉。

(4)它们性能的区别,同时也是悲观锁和乐观锁的区别
①因为 synchronized 是一种典型的悲观锁;而原子类恰恰相反,它利用的是乐观锁。所以,我们在比较 synchronized 和 AtomicInteger 的时候,其实也就相当于比较了悲观锁和乐观锁的区别。

②从性能上来考虑的话,悲观锁的操作相对来讲是比较重量级的。因为 synchronized 在竞争激烈的情况下,会让拿不到锁的线程阻塞,而原子类是永远不会让线程阻塞的。

不过,虽然 synchronized 会让线程阻塞,但是这并不代表它的性能就比原子类差。因为悲观锁的开销是固定的,也是一劳永逸的,随着时间的增加,这种开销并不会线性增长;而乐观锁虽然在短期内的开销不大,但是随着时间的增加,它的开销也是逐步上涨的

③所以从性能的角度考虑,它们没有一个孰优孰劣的关系,而是要区分具体的使用场景。在竞争非常激烈的情况下,推荐使用 synchronized;而在竞争不激烈的情况下,使用原子类会得到更好的效果

④值得注意的是,synchronized 的性能随着 JDK 的升级,也得到了不断的优化。synchronized 会从无锁升级到偏向锁,再升级到轻量级锁,最后才会升级到让线程阻塞的重量级锁。因此synchronized 在竞争不激烈的情况下,性能也是不错的。

6 其他学习链接

原子操作类AtomicInteger详解

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

Java中的CAS实现原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值