并发编程学习(十):共享模式无锁、原子整数、原子引用类型

这篇具有很好参考价值的文章主要介绍了并发编程学习(十):共享模式无锁、原子整数、原子引用类型。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1、volatile 

        获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。

        它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存,即一个线程对volatile变量的修改,对另一个线程可见。

        注意:

        volatile仅仅保证了共享变量的可见性,让其它线程能够开到新值,但不能解决指令交错问题(不能保证原子性)。

        CAS必须借助volatile才能读取到共享变量的最新值来实现【比较和交换】的效果。

2、cas为什么无锁效率高?

  • 无锁情况下,即使重试失败,线程始终在告诉运行,没有停歇,而synchronized会让线程在没有获取到锁的时候,发生上线问切换,进而阻塞。
  • 但无锁情况下,因为线程要保持运行,需要额外的CPU支持,如果没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

3、CAS的特点

        结合CAS和volatile可以实现无锁并发,适应于线程数少、多核CPU的场景下。

  • CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,会重试。
  • synchronized是基于悲观锁的思想:最悲观的估计,得防着其他线程来修改共享变量,上来先锁住,改完后再解开锁。 
  • CAS体现的是无锁并发、无阻塞并发。
    • 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
    • 但如果竞争激励,可以想到重试必然频繁发生,反而效率会受到影响。

4、原子整数

J.U.C并发包提供了:AtomicBoolean、AtomicInteger、AtomicLong.

4.1、AtomicInteger

package com.example.test.java.juc;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntUnaryOperator;

public class AtomicTest {
    public static void main(String[] args) {

        AtomicInteger i = new AtomicInteger();

        // 【原子操作】,先获取再自增(i=0,结果i=1,返回0),类似于i++
        System.out.println(i.getAndIncrement());

        // 【原子操作】,先自增再获取(i=0,结果i=1,返回1),类似于++i
        System.out.println(i.incrementAndGet());

        // 【原子操作】,先自减再获取(i=0,结果i=-1,返回-1),类似于--i
        System.out.println(i.decrementAndGet());

        // 【原子操作】,先获取再自减(i=0,结果i=-1,返回0),类似于i--
        System.out.println(i.getAndDecrement());

        // 【原子操作】,先获取再加值(i=0,结果i=5,返回0)
        System.out.println(i.getAndAdd(5));

        // 【原子操作】,先加值再获取(i=0,结果i=5,返回5)
        System.out.println(i.addAndGet(5));

        // 【原子操作】,先更新再获取(i=5,结果i=5*10 ,返回50), x 代表操作前的i值。
        System.out.println(i.updateAndGet(x -> x *10));

        // 【原子操作】,先获取再更新(i=5,结果i=5*10,返回5), x 代表操作前的i值。
        System.out.println(i.getAndUpdate(x -> x *10));

        // 原子操作实现原理:
        updateAndGet(i, o -> o * 2,1);

        System.out.println(i.get());
    }

    /* 原子操作实现原理(自定义实现)
     *
     * IntUnaryOperator : 接口函数。唯一的抽象方法:applyAsInt
     *
     * preOrNext : 返回处理前 或 处理后的 结果,1-前,2-后
     */
    public static void updateAndGet(AtomicInteger i, IntUnaryOperator o,int preOrNext) {
        int returni = 0;
        while (true) {
            int pre = i.get();
            int next = o.applyAsInt(pre);
            // compareAndSet为cas的原子操作
            if (i.compareAndSet(pre,next)) {
                if (1 == preOrNext) {
                    returni = pre;
                } else {
                    returni = next;
                }
                break;
            }
        }
    }
}

5、原子引用类型

        AtomicReference、 AtomicMarkableReference、AtomicStampedReference

5.1、AtomicReference

package com.example.test.java.juc;
import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicReference;
/*
    原子引用类型: 
         AtomicReference
         AtomicMarkableReference
         AtomicStampedReference

         AtomicReference:
            主线程仅能判断出共享变量的值与最初值A是否相同,不能感知到这种从A改为B又改回A的情况,
            如果主线程希望:只要有其他线程【动过了】共享变量,那么自己的cas就算失败,这时仅比较值就不够用了,需要再加一个版本号。
             如:AtomicStampedReference
 */
public class AtomicReferenceTest {
    /*
      DecimalAccount 接口类
     */
    interface DecimalAccount {
        // 获取金额
        BigDecimal getBalance();

        // 取款
        void withdraw(BigDecimal amount);

    }

    /*
      DecimalAccountCas 通过cas 实现DecimalAccount接口的提款方法的withdraw原子操作
     */
    class DecimalAccountCas implements DecimalAccount {

        // 通过原子引用类型保护Bigdecimal类型的余额:balance
        private AtomicReference<BigDecimal> balance;

        // 有参构造方法
        public DecimalAccountCas(BigDecimal balance) {
            this.balance = new AtomicReference<BigDecimal>(balance);
        }

        // 获取当前余额
        @Override
        public BigDecimal getBalance() {
            return balance.get();
        }

        // 取款
        @Override
        public void withdraw(BigDecimal amount) {
            while(true) {
                // 当前余额
                BigDecimal pre = balance.get();
                // 取款后的余额
                BigDecimal next = pre.subtract(amount);
                // 通过cas原子操作,将取款后的值复制给余额
                if (balance.compareAndSet(pre,next)) {
                    // 修改成功
                    break;
                }
                // 如果修改失败,代表pre已经被别人修改过了,需要继续循环重新获取
            }
        }
    }
}

5.2、AtomicStampedReference (多了版本号) 

        AtomicStampedReference可以给原子引用加上版本号,追踪原子引用真个的变化过程,如:A->B->A->C, 通过AtomicStampedReference我们可以知道 引用变量中途被变更了几次。

package com.example.test.java.juc;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicStampedReference;
/*
  AtomicStampedReference 比 AtomicReference 多了一个版本号的概念。
  AtomicReference:
            主线程仅能判断出共享变量的值与最初值A是否相同,不能感知到这种从A改为B又改回A的情况,
            如果主线程希望:只要有其他线程【动过了】共享变量,那么自己的cas就算失败,这时仅比较值就不够用了,需要再加一个版本号。这时就可以使用 AtomicStampedReference。

             AtomicStampedReference【只有当前值 、版本号都对,才可以修改成功】。
 */
@Slf4j(topic = "com.test.AtomicStampedReference")
public class AtomicStampedReferenceTestABA {

    // 共享变量
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);

    public static void main(String[] args) throws InterruptedException {

        // 获取未修改前的值
        String pre = ref.getReference();
        // 获取未修改前的版本号
        int stamp = ref.getStamp();
        log.debug("未修改前的值: {},版本号: {}",pre,stamp);


        // 新开两个线程将A改为B, B改为A
        new Thread(() -> {
            int stamp1 = ref.getStamp();
            String reference1 = ref.getReference();
            log.debug("当前值:{},版本号: {}",reference1,stamp1);
            boolean flag = ref.compareAndSet(ref.getReference(), "B", stamp1, stamp1 + 1);
            log.debug("A改为B: {}",flag);
        },"线程1").start();

        new Thread(() -> {
            int stamp2 = ref.getStamp();
            String reference2 = ref.getReference();
            log.debug("当前值:{},版本号: {}",reference2,stamp2);
            boolean flag = ref.compareAndSet(reference2, "A", stamp2, stamp2 + 1);
            log.debug("B改为A: {}",flag);
        },"线程2").start();

        Thread.sleep(1000);

        // 尝试改为C
        boolean flag = ref.compareAndSet(pre, "C", stamp, stamp + 1);
        log.debug("A尝试改为C: {}",flag);
    }
}

输出结果:
15:17:25.611 [main] DEBUG com.test.AtomicStampedReference - 未修改前的值: A,版本号: 0
15:17:25.702 [线程1] DEBUG com.test.AtomicStampedReference - 当前值:A,版本号: 0
15:17:25.702 [线程1] DEBUG com.test.AtomicStampedReference - A改为B: true
15:17:25.702 [线程2] DEBUG com.test.AtomicStampedReference - 当前值:B,版本号: 1
15:17:25.702 [线程2] DEBUG com.test.AtomicStampedReference - B改为A: true
15:17:26.704 [main] DEBUG com.test.AtomicStampedReference - A尝试改为C: false  

但有时候,并不关系引用变量更改次数,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference。

5.3、AtomicMarkableReference(是否更改过)

 AtomicMarkableReference 是通过 boolean 型的标识来判断数据是否有更改过。

package com.example.test.java.juc;

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicMarkableReference;

@Slf4j(topic = "com.test.AtomicMarkableReference")
public class AtomicMarkableReferenceTest {
    static AtomicMarkableReference<String> ref = new AtomicMarkableReference("tom",false);

    public static void main(String[] args) {
        boolean oldMarked = ref.isMarked();
        String oldReference = ref.getReference();

        log.debug("初始化之后的标记:{}", oldMarked);
        log.debug("初始化之后的值:{}", oldReference);

        String newReference = "jerry";

        boolean b = ref.compareAndSet(oldReference, newReference, true, false);
        if (!b) {
            log.debug("Mark不一致,无法修改Reference的值");
        }
        b = ref.compareAndSet(oldReference, newReference, false, true);
        if (b) {
            log.debug("Mark一致,修改reference的值为jerry");
        }
        log.debug("修改成功之后的Mark:{}" , ref.isMarked());
        log.debug("修改成功之后的值:{}" , ref.getReference());
    }
}

输出结果:

15:42:45.757 [main] DEBUG com.test.AtomicMarkableReference - 初始化之后的标记:false
15:42:45.769 [main] DEBUG com.test.AtomicMarkableReference - 初始化之后的值:tom
15:42:45.769 [main] DEBUG com.test.AtomicMarkableReference - Mark不一致,无法修改Reference的值
15:42:45.770 [main] DEBUG com.test.AtomicMarkableReference - Mark一致,修改reference的值为jerry
15:42:45.770 [main] DEBUG com.test.AtomicMarkableReference - 修改成功之后的Mark:true
15:42:45.770 [main] DEBUG com.test.AtomicMarkableReference - 修改成功之后的值:jerry文章来源地址https://www.toymoban.com/news/detail-439505.html

到了这里,关于并发编程学习(十):共享模式无锁、原子整数、原子引用类型的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • 并发编程08:原子操作类

    Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 AtomicInteger :整型原子类 At

    2024年02月04日
    浏览(31)
  • 并发编程-JUC-原子类

    JUC 整体概览 原子类 基本类型-使用原子的方式更新基本类型 AtomicInteger:整形原子类 AtomicLong:长整型原子类 AtomicBoolean :布尔型原子类 引用类型 AtomicReference:引用类型原子类 AtomicStampedReference:原子更新引用类型里的字段原子类 AtomicMarkableReference :原子更新带有标记位的引

    2024年02月21日
    浏览(32)
  • JUC并发编程之原子类

    目录 1. 什么是原子操作 1.1 原子类的作用 1.2 原子类的常见操作 原子类的使用注意事项 并发编程是现代计算机应用中不可或缺的一部分,而在并发编程中,处理共享资源的并发访问是一个重要的问题。为了避免多线程访问共享资源时出现竞态条件(Race Condition)等问题,J

    2024年02月13日
    浏览(37)
  • 原子操作:并发编程的守护者

    并发编程的守护者在多线程或者并发编程中,我们经常需要处理一些共享资源,这时候就需要保证这些共享资源的操作是线程安全的。而原子操作就是一种能够保证线程安全的重要手段。本文将详细介绍原子操作的定义、重要性、实现原理以及应用场景。 原子操作可以被视为

    2024年01月16日
    浏览(27)
  • 《C++并发编程实战》读书笔记(4):原子变量

    标准原子类型的定义位于头文件 atomic 内。原子操作的关键用途是取代需要互斥的同步方式,但假设原子操作本身也在内部使用了互斥,就很可能无法达到期望的性能提升。有三种方法来判断一个原子类型是否属于无锁数据结构: 所有标准原子类型( std::atomic_flag 除外,因为

    2024年02月10日
    浏览(27)
  • C++并发编程 | 原子操作std::atomic

    目录 1、原子操作std::atomic相关概念 2、不加锁情况 3、加锁情况  4、原子操作 5、总结 原子操作: 更小的代码片段,并且该片段必定是连续执行的,不可分割。 1.1 原子操作std::atomic与互斥量的区别 1) 互斥量 :类模板,保护一段共享代码段,可以是一段代码,也可以是一个

    2023年04月26日
    浏览(28)
  • C++11原子变量:线程安全、无锁操作的实例解析

      在 C++11 中,原子变量( std::atomic )提供了一种线程安全的方式来操作共享变量。下面是一个简单的例子,演示了C++11原子变量的用法。 原子性操作:  原子变量提供了原子性操作,避免了多线程同时访问共享变量时的竞争条件。 无锁:  使用原子变量的操作是无锁的,因

    2024年01月20日
    浏览(33)
  • JUC并发编程学习笔记(十七)彻底玩转单例模式

    单例中最重要的思想-------构造器私有! 恶汉式、懒汉式(DCL懒汉式!) 恶汉式 懒汉式 DCL懒汉式 完整的双重检测锁模式的单例、懒汉式、DCL懒汉式 但是有反射!只要有反射,任何的代码都不安全,任何的私有都是摆设 正常的单例模式: 反射破坏单例: 怎么去解决这

    2024年02月05日
    浏览(34)
  • Java并发编程——伪共享和缓存行问题

    在Java并发编程中,伪共享(False Sharing)和缓存行(Cache Line)是与多线程访问共享数据相关的两个重要概念。 伪共享指的是多个线程同时访问同一个缓存行中的不同变量或数据,其中至少一个线程对其中一个变量进行写操作。由于处理器缓存行的一致性协议要求缓存行中的数

    2024年02月02日
    浏览(26)
  • 《C++并发编程实战》读书笔记(2):线程间共享数据

    在C++中,我们通过构造 std::mutex 的实例来创建互斥量,调用成员函数 lock() 对其加锁,调用 unlock() 解锁。但通常更推荐的做法是使用标准库提供的类模板 std::lock_guard ,它针对互斥量实现了RAII手法:在构造时给互斥量加锁,析构时解锁。两个类都在头文件 mutex 里声明。 假设

    2024年02月10日
    浏览(33)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包