多线程和并发编程(2)—CAS和Atomic实现的非阻塞同步

简介: 多线程和并发编程(2)—CAS和Atomic实现的非阻塞同步

在并发编程中实现原子操作可以使用锁,锁机制满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁。

这里会有些问题,首先,如果被阻塞的线程优先级很高很重要怎么办?其次,如果获得锁的线程一直不释放锁怎么办?同时,还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重。为了解决这个问题,Java提供了Atomic系列的原子操作类。

image-20230912100455360

一、CAS的原理

CAS(compare and swap)是一种非阻塞同步的实现方式,如其名字含义,他的核心思想就是先比较再替换,在AtomicInteger和其他原子操作的工具类中运用的比较多。

核心思路是比较三个值,CAS(V,E,N),其中V是要更新的值的地址,E是预期查询出来的值,N是更新后的值,只有当要更新的V查到的值和预期值E一致的时候,才能正常更新到N。他的底层是利用Unsafe类来进行操作,该类是对内存进行直接操作,保障指令的原子性,主要是执行native方法compareAndSwapObject()、compareAndSwapInt()、compareAndSwapLong()等方法。

//Unsafe.java

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

二、Atomic原子操作类

1.基本类型

AtomicInteger

主要提供基本数据类型包装类的原子操作:

  1. int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
  2. boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
  3. int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
  4. int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。

AtomicIntegerArray

主要是提供原子的方式更新数组里的整型,其常用方法如下。

  1. int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
  2. boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。
  3. 需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。

2.引用类型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类。

AtomicReference

原子更新引用类型。

AtomicStampedReference

利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题了。这就是AtomicStampedReference的解决方案。AtomicMarkableReference跟AtomicStampedReference差不多, AtomicStampedReference是使用pair的int stamp作为计数器使用,AtomicMarkableReference的pair使用的是boolean mark。 还是那个水的例子,AtomicStampedReference可能关心的是动过几次,AtomicMarkableReference关心的是有没有被人动过,方法都比较简单。

AtomicMarkableReference:

原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。

3.AtomicInteger详解

以下详细说明AtomicInteger的使用:

常见方法:

addAndGet()// 以原子方式将给定值添加到当前值,并在添加后返回新值。
getAndAdd() // 以原子方式将给定值添加到当前值并返回旧值。
incrementAndGet()// 以原子方式将当前值递增1并在递增后返回新值。它相当于i ++操作。
getAndIncrement() // 以原子方式递增当前值并返回旧值。它相当于++ i操作。
decrementAndGet()// 原子地将当前值减1并在减量后返回新值。它等同于i-操作。
getAndDecrement() // 以原子方式递减当前值并返回旧值。它相当于-i操作。
boolean compareAndSet(int expect, int update) //比较和交换操作将内存位置的内容与给定值进行比较,并且只有它们相同时,才将该内存位置的内容修改为给定的新值

代码示例:

public class AtomicTest {
   
   
    public static void main(String[] args) {
   
   
        AtomicInteger atomic = new AtomicInteger(0);
        int i = atomic.addAndGet(3);
        System.out.println("i = " + i);

        int i1 = atomic.getAndAdd(5);
        System.out.println("i1 = " + i1);

        int i2 = atomic.incrementAndGet();
        System.out.println("i2 = " + i2);

        int i3 = atomic.getAndIncrement();
        System.out.println("i3 = " + i3);

        int i4 = atomic.getAndDecrement();
        System.out.println("i4 = " + i4);

        int i5 = atomic.decrementAndGet();
        System.out.println("i5 = " + i5);


        //1、默认初始值
        AtomicInteger atomicInteger = new AtomicInteger(100);
        //2、默认初始值和给定值,都是100,所以会更改成功
        boolean isSuccess = atomicInteger.compareAndSet(100,110);   //current value 100
        //3、返回true
        System.out.println(isSuccess);      //true
        //4、默认初始值是110,给定值是100,所以会更改失败
        isSuccess = atomicInteger.compareAndSet(100,120);       //current value 110
        //5、返回false
        System.out.println(isSuccess);      //false
    }
}

三、CAS实现原子性面临的问题

1.ABA问题

ABA问题是当我查询到V所对应的值A后,又有其他线程将该值改为B,后又改回A的情况,这种情况对于CAS操作来说是分辨不出来的。一种常见解决类似ABA的问题的思路是加入了version字段用来标识每次更新的版本信息,如果更新完成之后version发生了变更,就表明被其他线程进行了更改。在CAS中,是通过Atomic类下的AtomicStampedReference解决了ABA的问题,是通过加入了时间戳(stamp)来区分是否中途有更改,获取到新值之后,再通过时间戳比较是否中途被其他线程修改过。

//AtomicStampedReference.java 源码

public class AtomicStampedReference<V> {
   
   

    private static class Pair<T> {
   
   
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
   
   
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
   
   
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;
  //...
}

2.循环时间长开销大。

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

针对并发处理时间较长的场景,可以考虑使用synchronize进行加锁操作,竞争共享资源的线程在资源竞争不到的时候是进入阻塞状态,不会一直占用CPU,相比CAS自旋更合适。

3.只能保证一个共享变量的原子操作。

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

解决办法就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

四、总结

CAS是一种非阻塞同步方法处理并发问题,相较于synchronize加锁操作,其操作上更轻量级,不会阻塞线程,但针对并发量很大并且竞争非常激烈的场景,可能使用CAS就不太合适,由于竞争到锁的概率很低,这就会造成CPU空转,导致资源浪费。

目录
相关文章
|
3月前
|
人工智能 安全 调度
Python并发编程之线程同步详解
并发编程在Python中至关重要,线程同步确保多线程程序正确运行。本文详解线程同步机制,包括互斥锁、信号量、事件、条件变量和队列,探讨全局解释器锁(GIL)的影响及解决线程同步问题的最佳实践,如避免全局变量、使用线程安全数据结构、精细化锁的使用等。通过示例代码帮助开发者理解并提升多线程程序的性能与可靠性。
126 0
|
3月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
153 0
|
5月前
|
Java 开发者 Kotlin
华为仓颉语言初识:并发编程之线程的基本使用
本文详细介绍了仓颉语言中线程的基本使用,包括线程创建(通过`spawn`关键字)、线程名称设置、线程执行控制(使用`get`方法阻塞主线程以获取子线程结果)以及线程取消(通过`cancel()`方法)。文章还指出仓颉线程与Java等语言的差异,例如默认不提供线程名称。掌握这些内容有助于开发者高效处理并发任务,提升程序性能。
178 2
|
12月前
|
编解码 数据安全/隐私保护 计算机视觉
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
如何使用OpenCV进行同步和异步操作来打开海康摄像头,并提供了相关的代码示例。
721 1
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
|
9月前
|
安全 Java 程序员
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
250 11
|
12月前
多线程通信和同步的方式有哪些?
【10月更文挑战第6天】
786 61
|
11月前
|
Java 调度
Java 线程同步的四种方式,最全详解,建议收藏!
本文详细解析了Java线程同步的四种方式:synchronized关键字、ReentrantLock、原子变量和ThreadLocal,通过实例代码和对比分析,帮助你深入理解线程同步机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Java 线程同步的四种方式,最全详解,建议收藏!
|
11月前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
781 6
|
11月前
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
370 1
|
11月前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####

热门文章

最新文章