2023年Java核心技术第十二篇(篇篇万字精讲)

这篇具有很好参考价值的文章主要介绍了2023年Java核心技术第十二篇(篇篇万字精讲)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

22. AtomicInteger 底层实现原理是什么?如何在自己的项目代码中应用CAS操作?

22.1 典型回答

22.1.1 CAS详细解释:

22.1.1.1 预期值的选取:

22.1.2 CAS的弊端

22.1.2.1 ABA问题:

22.1.2.2 自旋次数限制:

22.1.2.3 只能保证一个共享变量的原子操作:

22.1.3 CAS操作失败的原因以及解决方案

22.1.3.1 CAS操作失败可能有以下几个原因:

22.1.3.1.1 竞争条件:

22.1.3.1.2共享变量被修改多次:

22.1.3.1.3自旋次数过少:

22.1.3.1.4并发度太高:

22.1.3.2 解决CAS操作一直失败的方法

22.1.3.2.1 调整自旋次数:

22.1.3.2.2 使用其他同步机制:

22.1.3.2.3 优化并发逻辑:

22.1.4 AtomicInteger的使用:

22.1.4.1  incrementAndGet()和decrementAndGet()解析

22.1.4.2  incrementAndGet()和decrementAndGet()底层伪代码进行解析

22.1.5 小结

22.1.5 ABA问题的解决方案

22.1.5.1 AtomicStampedReference补充解释


22. AtomicInteger 底层实现原理是什么?如何在自己的项目代码中应用CAS操作?

这一篇我们将走进各种同步结构,线程池,是基于什么原理来进行设计和实现。

22.1 典型回答

AtomicIntger 是对int类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于CAS技术。

22.1.1 CAS详细解释:

CAS,即Compare and Swap(比较并交换),是一种用于实现多线程同步的原子操作。

它在并发编程中广泛应用,用于解决多线程访问共享数据时可能出现的竞态条件问题。

CAS操作包含三个基本参数:内存地址V、旧的预期值A和新的目标值B。CAS操作通过比较内存地址V处的值与预期值A是否相等,如果相等,则将内存地址V处的值更新为目标值B;如果不相等,则说明其他线程已经修改了内存地址V处的值,当前线程的操作失败。

CAS操作可以简单理解为以下流程:

  1. 读取内存地址V处的值,记为当前值C。
  2. 判断C是否等于预期值A。如果相等,则继续执行;如果不相等,则说明其他线程已经修改了该值,当前线程的操作失败。
  3. 将目标值B写入内存地址V处。如果写入成功,则操作完成;如果写入失败,则返回第1步重新进行。

CAS操作是原子的,因为它在执行过程中,不会被其他线程中断或修改数据。

CAS的优势在于避免了使用锁机制带来的性能开销。相比于锁机制,CAS不需要阻塞线程,只需在操作失败时进行重试,从而减少了线程切换和上下文切换的开销。尤其在低竞争情况下,CAS能够提供较好的性能。

22.1.1.1 预期值的选取:

预期值(Expected Value)是在CAS操作中作为比较的参考值,用于判断内存地址中的值是否与预期值相等。预期值可以由程序员事先指定或者根据具体的上下文确定。

通常情况下,预期值是通过读取共享变量的当前值来获取的。在CAS操作执行之前,程序会先读取共享变量的当前值,并将其作为预期值传入CAS操作中。CAS操作会比较内存地址中的值与预期值是否相等,如果相等,则将新的目标值写入内存地址中;如果不相等,则说明其他线程已经修改了共享变量的值,当前线程的CAS操作失败。

需要注意的是,由于并发环境下共享变量的值可能会被其他线程修改,所以预期值只是一个参考值,并不能保证CAS操作一定成功。当多个线程同时进行CAS操作时,可能会出现竞争条件和重试的情况。

为了提高CAS操作的成功率,有时候可以根据上下文的需求,在选择预期值时采取一些策略,例如基于某种约束条件的判断、根据先前的操作结果等等。具体的预期值选择策略需要根据具体的业务场景和需求来确定。

22.1.2 CAS的弊端

 CAS存在一些限制和问题:

22.1.2.1 ABA问题:

如果内存地址V处的值在操作过程中经历了从A到B再到A的变化,CAS无法察觉到这种变化,可能导致数据不一致。

22.1.2.2 自旋次数限制:

为了避免出现无限循环,CAS通常会设置一个自旋次数的上限。如果达到了自旋次数的上限仍然没有成功,那么CAS操作将会失败,需要采取其他的同步机制。

22.1.2.3 只能保证一个共享变量的原子操作:

CAS只能对单个变量进行原子操作,对于多个变量之间的复合操作,CAS无法保证原子性。

CAS是一种基于比较和交换的原子操作,用于实现并发编程中的线程同步。它具有高效性和无锁特点,但也需要注意处理ABA问题和自旋次数限制等潜在问题。

22.1.3 CAS操作失败的原因以及解决方案

22.1.3.1 CAS操作失败可能有以下几个原因:
22.1.3.1.1 竞争条件:

多个线程同时执行CAS操作时,如果其中一个线程成功地更新了共享变量的值,那么其他线程的CAS操作就会失败。这是由于并发环境下存在竞争条件导致的。

22.1.3.1.2共享变量被修改多次:

CAS操作是基于共享变量当前的值进行比较和交换的。如果在CAS操作执行期间,共享变量被其他线程多次修改,那么CAS操作会失败,因为当前的值已经发生了变化。

22.1.3.1.3自旋次数过少:

如果设置的自旋次数过少,那么CAS操作可能没有足够的机会重试,从而直接失败。在高并发场景下,自旋次数的设置需要根据实际情况进行调优。

22.1.3.1.4并发度太高:

当并发度极高时,多个线程同时执行CAS操作,会增加CAS操作失败的概率。这是由于多个线程同时修改共享变量,导致彼此之间的竞争过于激烈。

22.1.3.2 解决CAS操作一直失败的方法
22.1.3.2.1 调整自旋次数:

增加自旋次数,使得CAS操作有更多的机会进行重试。但要注意自旋次数也不能设置过大,否则会占用过多的CPU资源。

22.1.3.2.2 使用其他同步机制:

如果CAS操作一直失败,可以考虑使用其他的同步机制,如互斥锁、信号量等。这些机制可以保证临界区的互斥访问,避免竞争条件的发生。

22.1.3.2.3 优化并发逻辑:

分析并发逻辑是否能够减少竞争条件的发生。例如,通过细粒度锁来减小锁的粒度,或者使用无锁数据结构等。这些优化措施可以减少竞争条件,从而降低CAS操作失败的概率。

22.1.4 AtomicInteger的使用:

当多个线程同时访问和更新一个普通的int类型变量时,可能会导致数据竞争(data race)的问题,从而引发并发访问的不一致性。为了解决这个问题,Java提供了AtomicInteger类,它是对int类型的一个封装,并提供了原子性的访问和更新操作。

CAS(compare-and-swap)是一种并发原语,用于实现多线程环境下的无锁同步。它包含三个操作数:内存地址(V)、期望值(A)和新值(B)。CAS的操作是原子的,当且仅当内存地址V上的值与期望值A相等时,才会将新值B写入到内存地址V上,并返回原来的值。

进行例子解析:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
    private static AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.decrementAndGet();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Counter: " + counter.get()); // 输出结果应为0
    }
}

我们创建了一个AtomicInteger对象counter,初始值为0。然后创建两个线程thread1thread2,分别对counter进行自增和自减操作。在每次操作中,我们直接调用AtomicInteger提供的原子性方法,如incrementAndGet()decrementAndGet(),无需手动进行CAS操作。

由于AtomicInteger的操作是原子的,多个线程并发访问时不会出现数据竞争的问题,保证了计数器的正确性。最终,通过get()方法获取最终的结果。

22.1.4.1  incrementAndGet()和decrementAndGet()解析

incrementAndGet()decrementAndGet()是Java中AtomicInteger类提供的两个原子操作方法,用于对AtomicInteger对象进行自增和自减操作,并返回最新的值。

  • incrementAndGet(): 这个方法将当前值加1,并返回更新后的结果。它执行以下操作:

    1. 从内存中获取当前值。
    2. 将当前值增加1。
    3. 将新值写回内存。
    4. 返回最新的值。

    请注意,这个方法由于是原子操作,因此不会受到其他线程的干扰,即使多个线程同时调用incrementAndGet()也可以保证最终结果的正确性。

  • decrementAndGet(): 这个方法将当前值减1,并返回更新后的结果。它执行以下操作:

    1. 从内存中获取当前值。
    2. 将当前值减少1。
    3. 将新值写回内存。
    4. 返回最新的值。

    同样地,decrementAndGet()方法也是原子操作,保证了并发环境下的正确性。

这两个方法在并发编程中非常有用,特别适用于需要对共享计数器进行操作的场景。使用AtomicInteger类的incrementAndGet()decrementAndGet()方法,可以避免数据竞争和并发访问的问题,确保计数器的正确性和一致性。

如果只需要对计数器进行自增或自减操作,并不需要返回最新的值,可以使用incrementAndGet()decrementAndGet()方法的对应方法incrementAndGet()decrementAndGet()。这些方法只执行自增或自减操作,并不返回结果,因此更加高效。

例子:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
    private static AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.decrementAndGet();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Counter: " + counter.get()); // 输出结果应为0
    }
}

我们创建了一个AtomicInteger对象counter,初始值为0。然后创建两个线程thread1thread2,分别对counter进行自增和自减操作。在每次操作中,我们直接调用AtomicInteger提供的原子性方法,如incrementAndGet()decrementAndGet()

由于AtomicInteger的操作是原子的,多个线程并发访问时不会出现数据竞争的问题,保证了计数器的正确性。最终,通过get()方法获取最终的结果。

以上代码的输出结果应该为0,因为thread1counter进行了1000次自增操作,thread2counter进行了1000次自减操作,两者抵消了。如果没有使用AtomicInteger类,而是直接操作普通的int变量,那么输出结果可能会不为0,因为数据竞争会导致错误的计算结果。

22.1.4.2  incrementAndGet()和decrementAndGet()底层伪代码进行解析
// 伪代码示例

public int incrementAndGet() {
    while (true) {
        int current = getValue(); // 获取当前值
        int next = current + 1; // 计算下一个值
        
        if (compareAndSwap(current, next)) { // 比较并交换
            return next; // 返回更新后的值
        }
        // 如果比较并交换失败,则重新尝试
    }
}

public int decrementAndGet() {
    while (true) {
        int current = getValue(); // 获取当前值
        int next = current - 1; // 计算下一个值
        
        if (compareAndSwap(current, next)) { // 比较并交换
            return next; // 返回更新后的值
        }
        // 如果比较并交换失败,则重新尝试
    }
}

getValue()方法用于获取当前存储的值。compareAndSwap(current, next)方法用于比较当前值和期望的旧值,并将新值写入内存中。

compareAndSwap(current, next)方法的实现:

private boolean compareAndSwap(int expect, int update) {
    if (currentValue == expect) { // 当前值和期望的旧值相等
        currentValue = update; // 将新值写入内存
        return true; // 操作成功
    } else {
        return false; // 操作失败
    }
}

CAS算法通过比较当前值和期望的旧值是否相等来判断是否发生了其他线程的修改。如果相等,说明没有发生竞争,将新值写入内存并返回操作成功;如果不相等,说明其他线程已经修改过值,需要重新尝试。

通过循环不断尝试,直到成功执行了比较并交换操作,incrementAndGet()decrementAndGet()方法保证了原子性的自增和自减操作。

22.1.5 小结

CAS其实就是Java的lock-free的基础,怎么进行理解。

CAS(Compare and Swap)是Java中实现无锁(lock-free)算法的基础。

理解CAS可以从以下几个方面:

  1. 原子性:CAS是一种原子操作,它能够在并发环境下实现无锁的原子更新操作。原子性意味着CAS操作要么完全执行成功,要么不执行,不会出现中间状态。这对于处理共享数据的并发访问非常重要。

  2. 比较和交换:CAS操作包括两个阶段:比较和交换。在比较阶段,CAS首先读取目标内存地址中的旧值,并和期望的旧值进行比较;如果相等,则进入交换阶段,CAS将新值写入到目标内存地址中;如果不相等,则说明该变量已经被其他线程修改过,CAS操作失败,需要重新尝试。

  3. 无锁算法:CAS是一种无锁算法,与传统的基于锁的同步方式不同。在使用锁的同步方式中,线程需要获取锁来访问共享资源,而CAS操作是无锁的,不需要获取锁。相比锁的方式,CAS操作避免了线程的阻塞和唤醒,减少了上下文切换的开销,提高了并发性能。

  4. ABA问题:尽管CAS是一种强大的原子操作,但它也存在ABA问题。ABA问题指的是如果一个值在操作期间被修改了两次,并恢复原值,那么CAS操作无法检测到这个过程。为了解决ABA问题,通常需要使用版本号或标记位等机制来追踪变量的变化。

CAS作为一种乐观锁技术,通过比较和交换来实现无锁的原子操作。它可以提高并发性能,减少锁带来的开销和竞争,但也需要注意处理ABA问题。CAS在Java中广泛应用于各种并发数据结构和算法的实现中,如Atomic类、ConcurrentHashMap、AQS等

22.1.5 ABA问题的解决方案

要解决ABA问题,可以使用版本号或标记位等机制来追踪变量的变化。这样可以在进行CAS操作时,不仅要比较值是否相等,还需要检查版本号或标记位是否发生变化。

例子:

有一个共享变量sharedValue,初始值为"A",并且有两个线程并发修改这个变量。

import java.util.concurrent.atomic.AtomicStampedReference;

public class ABAExample {
    private static AtomicStampedReference<String> sharedValue = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            int stamp = sharedValue.getStamp(); // 获取当前版本号
            sharedValue.compareAndSet("A", "B", stamp, stamp + 1); // 尝试将值从"A"更新为"B"
            sharedValue.compareAndSet("B", "A", stamp + 1, stamp + 2); // 尝试将值从"B"更新回"A"
        });

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(1000); // 等待一段时间,让thread1完成ABA操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int stamp = sharedValue.getStamp(); // 获取当前版本号
            sharedValue.compareAndSet("A", "C", stamp, stamp + 1); // 尝试将值从"A"更新为"C"
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Final value: " + sharedValue.getReference()); // 输出最终的共享变量值
    }
}

使用AtomicStampedReference来包装共享变量,并附上一个版本号。线程1先将共享变量从"A"更新为"B",然后再将其更新回"A",这个过程相当于一个ABA操作。而线程2在一段时间后将共享变量从"A"更新为"C"。

由于使用了版本号,CAS操作时不仅会比较值是否相等,还会比较版本号是否相等。在这个例子中,线程2尝试将值从"A"更新为"C"时,由于版本号已经发生变化,所以CAS操作会失败,不会误认为共享变量从"B"更新为"C",从而成功避免了ABA问题。

通过使用版本号或标记位等机制来追踪变量的变化,可以有效地解决ABA问题。

22.1.5.1 AtomicStampedReference补充解释

AtomicStampedReference是Java中的原子类,它提供了一种在进行CAS操作时同时维护一个版本号(stamp)的机制。

AtomicStampedReference可以用来解决ABA问题。ABA问题指的是如果一个值在操作期间被修改了两次,并恢复原值,那么CAS操作无法检测到这个过程,可能产生意外的结果。通过使用AtomicStampedReference,我们可以将变量的值和版本号一起进行比较和交换,从而避免误认为变量的值没有发生变化。

AtomicStampedReference的构造方法如下:

public AtomicStampedReference(V initialRef, int initialStamp)
  • initialRef:初始的引用值。
  • initialStamp:初始的版本号。

AtomicStampedReference提供了以下常用方法:

  • boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp):如果当前引用和版本号与期望的引用和版本号相等,则将引用和版本号更新为新值。返回是否更新成功。

  • V getReference():获取当前的引用值。

  • int getStamp():获取当前的版本号。

  • void set(V newReference, int newStamp):设置引用和版本号为新值。

AtomicStampedReference还提供了其他一些方法来支持对引用和版本号的操作,例如weakCompareAndSet()attemptStamp()等。

通过使用AtomicStampedReference,我们可以在进行CAS操作时同时比较和交换变量的值和版本号,从而解决ABA问题。文章来源地址https://www.toymoban.com/news/detail-691939.html

到了这里,关于2023年Java核心技术第十二篇(篇篇万字精讲)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java多线程&并发篇----第十二篇

    前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。 ReentantLock 继承接口 Lock 并实现了接口中定义的方法, 他是一种可重入锁, 除了能完成 synchronized 所能完成的所有

    2024年01月20日
    浏览(52)
  • 《MySQL》第十二篇 数据类型

    MySQL 支持多种数据类型,学习好数据类型,才能更好的学习 MySQL 表的设计,让表的设计更加合理。 类型 大小 SIGNED(有符号) UNSIGNED(无符号) 说明 tinyint 1字节 -128~127 0~255 小整型 smallint 2字节 -32768~32767 0~65535 小整型

    2024年02月15日
    浏览(52)
  • 【MySQL数据库 | 第十二篇】:约束

    在MySQL中, 约束是一种限制数据表中列值的规定 。保证数据库中的数据正确,有效性和完整性。MySQL中的约束有以下几种: 1. 主键约束(Primary Key Constraint) :主键是用于唯一标识表中每行记录的列。主键约束要求 每个主键列的值都是唯一的,且不能为NULL 。一个表只能有一

    2024年02月08日
    浏览(50)
  • 干翻Dubbo系列第十二篇:Dubbo协议介绍

    文章目录 文章说明 一:Dubbo协议 1:Dubbo协议简介 2:Dubbo协议优点  3:Dubbo协议帧的组成 (一):幻数 (二):2Way (三):event (四):Serilization ID (五):status (六):RequestID (七):数据长度         Rpc过程当中三个核心的要素就是: 协议、序列化 、通信方式         Dubbo协议

    2024年02月12日
    浏览(39)
  • 第十二篇-Tesla P40+ModelScope+Gradio+QWen+MindChat

    本文主要实现Qwen-7B-Chat与MindChat-Qwen-7B的简单部署与测试 通过ModelScope下载模型,下载速度还是很好的,下载模型完成,需要一些时间 1.Qwen-7B-Chat 大概15G 2.MindChat-Qwen-7B 大概14G 可以使用体验了 本文参考 https://github.com/X-D-Lab/MindChat/blob/main/webui_demo.py

    2024年02月10日
    浏览(43)
  • 【夜深人静学习数据结构与算法 | 第十二篇】动态规划——背包问题

      目录  前言:  01背包问题: 二维数组思路: 一维数组思路: 总结:       在前面我们学习动态规划理论知识的时候,我就讲过要介绍一下背包问题,那么今天我们就来讲解一下背包问题。 在这里我们只介绍 01背包 ,至于分组背包和混合背包这种的已经属于竞赛级别的

    2024年02月12日
    浏览(53)
  • 第十二篇 1+X考证 Web前端测试题PHP篇(新)

      1、 以下关于PHP面向对象的说法错误的是( A ) A、PHP可以多重继承,一个类可以继承多个父类 B、PHP使用new运算符来获取一个实例对象 C、一个类可以在声明中用extends继承另一个类的方法和属性 D、PHP默认将var解释为public [ 知识点:php不支持多重继承,但是

    2024年02月08日
    浏览(47)
  • RK3568驱动指南|第十二篇 GPIO子系统-第128章 GPIO入门实验

    瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网

    2024年01月21日
    浏览(43)
  • 第十二篇【传奇开心果系列】Ant Design Mobile of React开发移动应用:内置组件实现酷炫CSS 动画

    第一篇【传奇开心果系列】Ant Design Mobile of React 开发移动应用:从helloworld开始 第二篇【传奇开心果系列】Ant Design Mobile of React 开发移动应用:天气应用 第三篇【传奇开心果系列】Ant Design Mobile of React 开发移动应用:健身追踪 第四篇【传奇开心果系列】Ant Design Mobile of React 开发移

    2024年01月20日
    浏览(60)
  • 【从零开始学习JAVA | 第二十二篇】BigInteger

    目录  前言:  BigInterger: BigInteger常见的方法: 总结:         本篇我们将介绍BigInteger这个比较实用一点的API,这个API在我们实际写项目中都是很实用的API,因此大家应该对这个API有更加熟练的掌握。 在Java中,整数类型int和long的取值范围是有限的,超出该范围的整数无法

    2024年02月15日
    浏览(69)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包