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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值