Java并发编程之 无锁(CAS)

共享模型之无锁

一、CAS 与 volatile

AtomicInteger 的使用举例

//创建实例
AtomicInteger balance = new AtomicInteger(int n);

//CAS操作
while(true) {
    
    // 获取实例的最新值
    int prev = balance.get();
    
    // 修改后的实例的值
    int next = prev - amount;
    
    // 真正修改,将修改后的数据同步到主存中
    // compareAndSet参数1是获取到的最新值,参数2是修改后的值,将参数2写往主存中
    if(balance.compareAndSet(prev, next)) { //修改成功返回true
        break;
    }
}

//上述循环的代码可以使用如下方法替换
balance.getAndAdd(-1 * amount);

1. CAS

AtomicInteger内部并没有用锁来保护共享变量的线程安全

它的机制是 compareAndSet方法,简称就是 CAS ,它有两个操作:比较和设置值

如下图①的位置,线程2已经将余额修改为90,但线程1获取到的余额还是100,此时cas操作会将第一个参数(线程1拿到的最新值100)与主存中的数据(已被修改为90)进行比较

  • 如果不一致,cas操作会返回fasle,表示修改主存的值失败,再次进入循环,重新获取主存中最新的值

  • 如果一致,执行set,cas操作返回true,表示成功修改主存的值

    核心思想在于采用不断比较并不断获取新值的方式保证了线程安全

2. volatile

AtomicInteger的值通过value属性保存,此属性使用了volatile关键字 ,即 private volatile int value;

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

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

3. 为什么无锁效率高

  1. 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞
  2. 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持

4. CAS 的特点

  • 结合 CAS 和 volatile 可以实现无锁并发、无阻塞并发,适用于线程数少、多核 CPU 的场景

    • 如果竞争激烈(写操作多),可以想到重试必然频繁发生,反而效率会受影响
  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,即使改了也可以再重载

  • synchronized 是基于悲观锁的思想:最悲观的估计,防止其它线程修改共享变量,释放锁,别的线程才有机会

二、原子整数

JUC并发包提供了一些并发工具类,这里把它分成五类:

  1. 原子整数
  2. 原子引用
  3. 原子数组
  4. 字段更新器
  5. 原子累加器

下面先讨论原子整数类

对类中保存的整数类型的数据的操作具有原子性

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean :布尔型原子类

上面三个类提供的方法几乎相同,以 AtomicInteger 为例讨论它的api接口:通过观察源码可以发现,AtomicInteger 内部都是通过cas的原理来实现的,也就是说所有方法都可以保证原子性,不会有线程安全的问题

public static void main(String[] args) {

    //1. 创建AtomicInteger对象
    AtomicInteger i = new AtomicInteger(0); 
    //参数表示初始值,保存在value属性中

    //2. get方法拿到最新值

    //3. compareAndSet方法通常需要与while循环结合使用,使用起来不方便

    //4. 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
    System.out.println(i.getAndIncrement());

    //5. 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
    System.out.println(i.incrementAndGet());

    //6. 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
    System.out.println(i.decrementAndGet());

    //7. 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
    System.out.println(i.getAndDecrement());

    //8. 获取并加值(i = 0, 结果 i = 5, 返回 0)
    System.out.println(i.getAndAdd(5));

    //9. 加值并获取(i = 5, 结果 i = 0, 返回 0)
    System.out.println(i.addAndGet(-5));

    //10. 获取并更新(形参p为i的当前值, 函数体中写对p的操作;i = 0, 结果i = -2, 返回 0)
    // 参数是函数式编程接口,其中函数中的操作能保证原子
    System.out.println(i.getAndUpdate(p -> p - 2));

    //11. 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
    // 参数是函数式编程接口,其中函数中的操作能保证原子
    System.out.println(i.updateAndGet(p -> p + 2));

    //12. 获取并计算(i = 0, p 为 i 的当前值, x 为参数1的值, 结果 i = 10, 返回 0)
    // 函数式编程接口,其中函数中的操作能保证原子
    System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));

    //13. 计算并获取(i = 10, p 为 i 的当前值, x 为参数1值, 结果 i = 0, 返回 0)
    // 函数式编程接口,其中函数中的操作能保证原子
    System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));
}

三、原子引用

保证引用类型的共享变量是线程安全的

常用的有以下三种:

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:带有版本号的引用类型原子类
  • AtomicMarkableReference:带有标记的引用类型原子类

1. AtomicReference

用法与原子整数一致,也要通过循环CAS操作保证线程安全,泛型中声明此实例对应的类型

private AtomicReference<BigDecimal> balance ;

2. ABA 问题

cas操作中,某一线程仅能判断出共享变量此时的值与最初获取到的值 A 是否相同,不能感知到别的线程将共享变量从 A 改为 B 又改回 A 的情况,如果某一线程希望:只要有其它线程修改过共享变量,那么自己的 cas 操作就算失败,这时,仅比较值是不够的,需要再加一个版本号

使用 AtomicStampedReference 来解决

3. AtomicStampedReference

代码示例

@Slf4j(topic = "c.Test36")
public class Test36 {

    //参数1表示初始值,参数2表示初始版本号
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        log.debug("主线程启动");
        //获取初始值A
        String prev = ref.getReference();
        //获取版本号
        int stamp = ref.getStamp();
        log.debug("主线程获取到的版本 {}", stamp);
        other(); //other方法中定义了两个新的线程,制造ABA问题
        log.debug("主线程睡眠1秒");
        Thread.sleep(1000);
        // 主线程尝试将A修改为C
        // compareAndSet第三个参数是获取到的版本号,第四个参数表示修改后的版本号
        log.debug("主线程尝试将 A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }

    //定义other方法
    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("新线程1尝试将 A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));
            log.debug("新线程1更新后的版本为 {}", ref.getStamp());
        }, "t1").start();
        Thread.sleep(1000); //睡眠1秒后再启动第二个线程
        new Thread(() -> {
            log.debug("新线程2尝试将 B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));
            log.debug("新线程2更新后的版本为 {}", ref.getStamp());
        }, "t2").start();
    }
}
//主线程获取的A的版本号和目前的A的版本号不同,修改失败

4. AtomicMarkableReference

如果只是单纯的关心共享变量是否更改过,而不关心更改过几次,可以使用 AtomicMarkableReference

代码示例

//1. 创建对象
//参数1表示初始值,参数2表示初始标记
AtomicMarkableReference<T> ref = new AtomicMarkableReference<>("T类型的初始值", true);

//2. 获取值
T prev = ref.getReference();

//3. 修改主存中的值
//参数3表示期望的标记,参数4表示修改之后的标记,如果主存的数据不是期望的标记则修改失败
ref.compareAndSet("获取到的值", "修改后的值", true, false);

//如果有别的线程对主存中的数据进行修改,则标记变为false,此线程会修改失败

四、原子数组

使用原子的方式操作数组里的某个元素(不会产生线程安全问题),而使用普通的数组无法保证原子性

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray :引用类型数组原子类
//创建数组方式一,定义数组长度
AtomicIntegerArray array = new AtomicIntegerArray(10);

//创建数组方式二,定义数组内容
int[] a = new int[]{1, 2, 3};
AtomicIntegerArray array = new AtomicIntegerArray(a);

其中常用的方法:i参数表示索引值

五、原子更新器

保证操作类中的属性时的原子性

  • AtomicReferenceFieldUpdater:表示类类型的字段(属性)

  • AtomicIntegerFieldUpdater:表示整型的字段

  • AtomicLongFieldUpdater:表示长整型的字段

@Slf4j(topic = "c.Test40")
public class Test40 {
    public static void main(String[] args) {
        Student stu = new Student();

        //参数1表示属性所属的类
        //参数2表示属性的类型
        //参数3表示属性的名称
        AtomicReferenceFieldUpdater updater =
                AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");

        //参数1表示要修改哪个对象中的属性
        //参数2表示期望属性的初始值是什么
        //参数3表示要把属性的值修改为什么
        //初始值为期望才可以修改成功
        System.out.println("是否修改成功:" + updater.compareAndSet(stu, null, "张三"));
        System.out.println(stu);
    }
}

class Student {
    //属性使用volatile来修饰
    volatile String name;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }
}
  • 运行结果:

  • 注意:字段更新器,只能配合 volatile 修饰的字段使用,否则会出现异常

  • 如果把name属性的volatile关键字去掉,运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gmRNpk1S-1616746939670)(图片/5. 共享模型之无锁/image-20210319171438925.png)]

六、原子累加器LongAdder

  • LongAdder累加器的使用

    LongAdder adder = new LongAdder(); //创建一个累加器,初始值为0
    adder.increment(); //对累加器的值加1
    adder.decrement(); //对累加器的值减一
    adder.add(5); //对累加器的值增加指定的数值
    
  • 相比于原子整数的 getAndIncrement() 性能有了很大的提升(二者都可以保证累加的原子性)

  • 原子整数的getAndIncrement()会在多个线程有竞争的情况下,循环的去比较,线程较多竞争较激烈时,循环的次数会越多,必然会造成性能的下降

  • LongAdder性能提升的原因很简单,就是在有竞争时,设置多个累加单元(不是在一个共享变量Cell上累加),Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总

  • 这样它们在进行累加操作时,操作不同的 Cell 变量,因此减少了 CAS 失败重试的次数,从而提高性能

  • 累加单元数不会超过CPU的核心数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Nice2cu_Code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值