并发编程-03线程安全性之原子性(Atomic包)及原理分析

简介: 并发编程-03线程安全性之原子性(Atomic包)及原理分析

2019080611330380.jpg



线程安全性文章索引


并发编程-03线程安全性之原子性(Atomic包)及原理分析

并发编程-04线程安全性之原子性Atomic包的4种类型详解

并发编程-05线程安全性之原子性【锁之synchronized】

并发编程-06线程安全性之可见性 (synchronized + volatile)

并发编程-07线程安全性之有序性


脑图


20190216205207715.png



线程安全性的定义


当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何进行交替,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现正确的行为,那么这个类就是线程安全的。


线程安全性的体现


线程安全性主要体现在一下三个方面

1. 原子性

2. 可见性

3. 有序性


原子性:提供互斥访问,同一时刻只能有一个线程来对它进行操作


可见性:一个线程对主内存的修改可以及时被其他线程观察到


有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序


我们逐个来看下这3个特性,首先来学习下线程安全的原子性JDK中提供的类以及原理。


原子性


提到原子性,就不得不提jdk1.5开始提供的juc中的Atomic包,Atomic包中的原子操作类提供了一种用法简单、性能高效、线程安全的更新一个变量的方式。

先回顾下线程不安全的写法

20190216220354959.png


使用AtomicInteger改造线程不安全的变量


下面我们通过示例来演示下Atomic包中的原子类是如何线程安全的更新一个变量的方式


20190216222136196.png



incrementAndGet源码分析-UnSafe类 compareAndSwapInt (CAS)

AtomicInteger#incrementAndGet 是如何实现线程安全的呢?

看下源码 (JDK1.8


20190216230655125.png


调用了Unsafe类中的getAndAddInt()方法,该方法执行一个CAS操作,保证线程安全

UnSafe的getAndAddInt方法实现:

//Unsafe类中的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {        
    int v;        
    do {            
        v = getIntVolatile(o, offset);        
    } while (!compareAndSwapInt(o, offset, v, v + delta));        
    return v;
}



getAndAddInt通过一个while循环不断的重试更新要设置的值,直到成功为止,调用的是Unsafe类中的compareAndSwapInt方法,是一个CAS操作方法。 【CAS操作是基于系统原语的(原语的执行必须是连续的,操作期间不会被系统中断,是一条CPU的原子指令),因此是一个不需要加锁的锁,也因此不可能出现死锁的情况。】


CAS方法的主要实现逻辑


CAS:Compare and Swap

CAS(V, E, N)


V:要更新的变量

E:预期值

N:新值


如果V值等于E值,则将V值设为N值;如果V值不等于E值,说明其他线程做了更新,那么当前线程什么也不做。(放弃操作或重新读取数据)


在JDK中的实现为,加入了个偏移量offset

Unsafe里的CAS 操作相关:

//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                   
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x); 
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);


上面的incrementAndGet 源码分析是基于JDK1.8的,如果是1.8之前的方法,1.8之前的方法则是通过for的死循环实现的:

//JDK 1.7的源码,由for的死循环实现,并且直接在AtomicInteger实现该方法,
//JDK1.8后,该方法实现已移动到Unsafe类中,直接调用getAndAddInt方法即可
public final int incrementAndGet() {    
    for (;;) {        
        int current = get();        
        int next = current + 1;        
        if (compareAndSet(current, next))            
            return next;    
    }
}


CAS操作中可能会带来的ABA问题


当第一个线程执行CAS(V,E,U)操作,在获取到当前变量V,准备修改为新值U前,另外两个线程已连续修改了两次变量V的值,使得该值又恢复为旧值。

2019021700330114.png

ABA问题的解决办法


  • AtomicStampedReference类 时间戳


一个带有时间戳的对象引用,每次修改时,不但会设置新的值,还会记录修改时间。在下一次更新时,不但会对比当前值和期望值,还会对比当前时间和期望值对应的修改时间,只有二者都相同,才会做出更新。


底层实现为:一个键值对Pair存储数据和时间戳,并构造volatile修饰的私有实例;两者都符合预期才会调用Unsafe的compareAndSwapObject方法执行数值和时间戳替换

  • AtomicMarkableReference类

一个boolean值的标识,true和false两种切换状态表示是否被修改。不推荐使用。

原理参考:Java中的CAS和Unsafe类


AtomicLong 和 LongAdder


上面的AtomicInteger也可以改成AtomicLong ,其他地方都无需调整,效果是一样的。 这里我们要引出的JDK8中对AtomicLong的改进类LongAdder.

20190217014250438.png


多次执行,结果总是10000.


LongAdder的优化思路


LongAdder所使用的思想就是热点分离,这一点可以类比一下ConcurrentHashMap的设计思想。就是将value值分离成一个数组,当多线程访问时,通过hash算法映射到其中的一个数字进行计数。而最终的结果,就是这些数组的求和累加。这样一来,就减小了锁的粒度

20190217013426566.png

LongAdder的优缺点

优点:

  • LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。 如果仅仅是需要做形如count++的操作,如果使用的JDK8的话,推荐使用LongAdder代替AtomicLong。

缺点:

  • LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。


AtomicReference 和 AtomicIntegerFieldUpdater


因篇幅原因 AtomicReference 和 AtomicIntegerFieldUpdater 的使用见另外一篇博客

并发编程-04线程安全性之原子性Atomic包详解

原子更新引用类型: AtomicReference

原子更新字段类型: AtomicIntegerFieldUpdater


代码

https://github.com/yangshangwei/ConcurrencyMaster

相关文章
|
2月前
|
存储 NoSQL Redis
Redis 新版本引入多线程的利弊分析
【10月更文挑战第16天】Redis 新版本引入多线程是一个具有挑战性和机遇的改变。虽然多线程带来了一些潜在的问题和挑战,但也为 Redis 提供了进一步提升性能和扩展能力的可能性。在实际应用中,我们需要根据具体的需求和场景,综合评估多线程的利弊,谨慎地选择和使用 Redis 的新版本。同时,Redis 开发者也需要不断努力,优化和完善多线程机制,以提供更加稳定、高效和可靠的 Redis 服务。
41 1
|
2月前
线程CPU异常定位分析
【10月更文挑战第3天】 开发过程中会出现一些CPU异常升高的问题,想要定位到具体的位置就需要一系列的分析,记录一些分析手段。
66 0
|
16天前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
2月前
|
数据挖掘 程序员 调度
探索Python的并发编程:线程与进程的实战应用
【10月更文挑战第4天】 本文深入探讨了Python中实现并发编程的两种主要方式——线程和进程,通过对比分析它们的特点、适用场景以及在实际编程中的应用,为读者提供清晰的指导。同时,文章还介绍了一些高级并发模型如协程,并给出了性能优化的建议。
31 3
|
3月前
|
负载均衡 Java 调度
探索Python的并发编程:线程与进程的比较与应用
本文旨在深入探讨Python中的并发编程,重点比较线程与进程的异同、适用场景及实现方法。通过分析GIL对线程并发的影响,以及进程间通信的成本,我们将揭示何时选择线程或进程更为合理。同时,文章将提供实用的代码示例,帮助读者更好地理解并运用这些概念,以提升多任务处理的效率和性能。
60 3
|
2月前
|
Java 编译器 程序员
【多线程】synchronized原理
【多线程】synchronized原理
59 0
|
2月前
|
Java 应用服务中间件 API
nginx线程池原理
nginx线程池原理
37 0
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
48 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
20 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
19 2