• java并发编程学习六——乐观锁CAS


    一、CAS原理

    CAS全称CompareAndSet或者CompareAndSwap,其中【比较-交换】的操作是原子的,下面先一个具体的案例

    1.1 无锁保护共享变量

    定义一个接口

    interface Acount {
    
        void withdraw(int amount);
    
        Integer getBalance();
    	//设置余额为10000,一千个线程每个线程减少10,正确结果应该为0
        static void demo(Acount acount) {
            List<Thread> threadList = new ArrayList<>();
            for (int i = 0; i < 1000; i++) {
                threadList.add(new Thread(() -> acount.withdraw(10)));
            }
            long start = System.currentTimeMillis();
            threadList.forEach(Thread::start);
    
            threadList.forEach(thread -> {
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            long end = System.currentTimeMillis();
            
            System.out.printf("执行时间:%d,余额:%d", end - start, acount.getBalance());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    1.1.1 不安全模式实现

    不安全的实现

    class AcountUnsafe implements Acount {
        private Integer balance;
    
        public AcountUnsafe(Integer balance) {
            this.balance = balance;
        }
    
        @Override
        public void withdraw(int amount) {
            this.balance -= amount;
        }
    
        public Integer getBalance() {
            return balance;
        }
    
        public void setBalance(Integer balance) {
            this.balance = balance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    测试

    public class Test1 {
        public static void main(String[] args) {
            Acount acount = new AcountUnsafe(10000);
            Acount.demo(acount);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    结果:
    在这里插入图片描述

    1.1.2 有锁安全实现

    给withdraw加锁即可

     @Override
        public void withdraw(int amount) {
            synchronized(this){
                this.balance -= amount;
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    结果:
    在这里插入图片描述

    1.1.3 无锁安全实现

    无锁采用原子整数AtomicInteger来实现

    class AcountSafe implements Acount {
        private AtomicInteger balance;
    
        public AcountSafe(AtomicInteger balance) {
            this.balance = balance;
        }
    
        @Override
        public void withdraw(int amount) {
            while (true) {
                int prev = this.balance.get();
                int value = prev - amount;
                // compareAndSet是JVM提供的本地方法,该方法具有原子性
                if (this.balance.compareAndSet(prev, value)) {
                    break;
                }
            }
        }
    
        public Integer getBalance() {
            return balance.get();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    结果
    在这里插入图片描述
    比较执行时间发现,无锁比有锁实现要快一点

    1.2 CAS工作方式

    先分析下上述代码的时序图
    在这里插入图片描述
    CAS底层使用的lock compxchg指令(X86架构),在单核CPU和多核CPU下都能保证【比较-交换】的原子性。CAS必须借助volatile才能读到共享变量的最新值,从而完成【比较-交换】的效果,来看看AtomicInteger的源码,其中初始值value就是用volatile修饰的
    在这里插入图片描述
    注意,volatile只能保证可见性,让其他线程看到最新值,但不能保证原子性即不能解决多个线程的指令交错问题。

    1.3 CAS的效率和特点

    CAS的作用是用无锁来实现多线程对共享变量操作的安全性,由于需要不停重试它也不是一定能提高效率。
    CAS的效率:

    • 无锁的情况下,即使重试失败,线程仍然在高速运行。synchronized加锁会让线程进入阻塞,发生上下文切换。
    • 线程上下文切换,好比赛车在跑动高速运行时,需要先刹车减速停下之后再被唤醒之后重新启动、加速,代价比较大。
    • 无锁情况下,线程要保持运行,需要CPU支援。在单核CPU下,线程不加锁也会由于时间片使用完,发生上下文切换。因此,CAS需要在多核下才能发挥优势,而且线程数最好不要超过CPU核数。

    CAS的特点:

    • CAS基于乐观锁的思维实现,不怕别人修改共享变量,修改了没关系,自己再重试
    • synchronized基于悲观锁的思维,加锁之后不允许别人修改共享变量,除非自己修改完释放锁,别人才有机会
    • CAS体现的是无锁并发、无阻塞并发。由于没有加锁,不会发生阻塞,从而提高效率。但是在竞争激烈的情况下,会发生大量的无效重试,反而会影响效率。

    二、原子整数

    2.1 AtomicInteger、AtomicBoolean、AtomicLong

    AtomicInteger、AtomicBoolean、AtomicLong,这三个比较类似,都可以看做是对一个整数的封装。

    public class AtomicIntegerTest {
        public static void main(String[] args) {
            AtomicInteger i = new AtomicInteger(0);
            //自增等价于++i
            System.out.println(i.incrementAndGet());
            //i++
            System.out.println(i.getAndIncrement());
            System.out.println(i.get());
            //类似的还有--i,i--
    
            //增加指定的数
            System.out.println(i.addAndGet(3));
            System.out.println(i.getAndAdd(3));
            System.out.println(i.get());
    
            //其他计算,乘法等复杂运算
            System.out.println(i.updateAndGet(x -> x * 2 - 1));
            System.out.println(i.getAndUpdate(x -> x * 2 - 1));
            System.out.println(i.get());
    
            //自己实现updateAndGet
            System.out.println(updateAndGet(i,x->(x-1)*2));
        }
    
        private static AtomicInteger updateAndGet(AtomicInteger i, IntUnaryOperator intUnaryOperator) {
            int prev;
            do {
                prev = i.get();
            } while (!i.compareAndSet(prev, intUnaryOperator.applyAsInt(prev)));
            return i;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    2.2 Unsafe模拟AtomicInteger

    public class MyAtomicIntegerTest implements Acount {
    
        public static void main(String[] args) {
            Acount test = new MyAtomicIntegerTest(10000);
            Acount.demo(test);
        }
    
        MyAtomicInteger balance;
    
        public MyAtomicIntegerTest(int i) {
            this.balance = new MyAtomicInteger(i);
        }
    
        @Override
        public void withdraw(int amount) {
            balance.incrementAndGet(-amount);
        }
    
        @Override
        public Integer getBalance() {
            return balance.get();
        }
    }
    
    class MyAtomicInteger {
        volatile int value;
    
        private static Unsafe UNSAFE;
    
        private static long valueOffset;
    
        static {
            try {
                //利用反射获取Unsafe的实例
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                UNSAFE = (Unsafe) field.get(null);
                //获取字段value相对类对象内存开始位置的偏移量
                valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    
        public MyAtomicInteger(int value) {
            this.value = value;
        }
    
        public int get() {
            return value;
        }
    
        public void incrementAndGet(int i) {
            while (true) {
                if (UNSAFE.compareAndSwapInt(this, valueOffset, this.get(), this.get() + i)) {
                    break;
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    测试结果:
    在这里插入图片描述

    三、原子引用

    3.1 AtomicReference

    使用AtomicReference实现Account

    public class AtomicReferenceTest {
        public static void main(String[] args) {
            Acount acount = new AccountBigDecimalSafe(new BigDecimal("10000"));
            Acount.demo(acount);
        }
    }
    
    class AccountBigDecimalSafe implements Acount {
        private AtomicReference<BigDecimal> balance;
    
        AccountBigDecimalSafe(BigDecimal balance) {
            this.balance = new AtomicReference<>(balance);
        }
    
        @Override
        public void withdraw(int amount) {
            while (true) {
                BigDecimal prev = balance.get();
                BigDecimal next = prev.subtract(new BigDecimal(amount));
                if (balance.compareAndSet(prev, next)) {
                    break;
                }
            }
        }
    
        @Override
        public Integer getBalance() {
            return balance.get().intValue();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    3.2 ABA问题

    字符串A,先改成了B又改回A,主线程发现字符串还是A,就改成了C。一般ABA问题不会有什么影响,但在实际工作中还是注意。

    public class AbaTest {
        public static void main(String[] args) {
            AtomicReference atomicReference = new AtomicReference("A");
            System.out.println(atomicReference);
            new Thread(() -> {
                if (atomicReference.compareAndSet("A", "B")) {
                    System.out.println(atomicReference);
                }
                if (atomicReference.compareAndSet("B", "A")) {
                    System.out.println(atomicReference);
                }
            }).start();
    
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            if (atomicReference.compareAndSet("A", "C")) {
                System.out.println(atomicReference);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    3.3 AtomicMarkableReference

    可以使用AtomicMarkableReference解决ABA问题

    public class AbaTest2 {
        public static void main(String[] args) {
            AtomicMarkableReference<String> atomicReference = new AtomicMarkableReference("A",false);
            System.out.println(atomicReference.getReference());
            new Thread(() -> {
                if (atomicReference.compareAndSet("A", "B",false,true)) {
                    System.out.println(atomicReference.getReference());
                }
                if (atomicReference.compareAndSet("B", "A",false,true)) {
                    System.out.println(atomicReference.getReference());
                }
            }).start();
    
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            if (atomicReference.compareAndSet("A", "C",false,true)) {
                System.out.println(atomicReference.getReference());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    AtomicMarkableReference增加了是否改动过的标记,只要改动过,并被标记上,后面的改动就无法生效。

    四、原子累加器

    原子累加器,指的是累加操作是原子的,java8之前只能使用原子整数类的incrementAndGet方法,java8新增了LongAdder(并发大神狗哥,Doug Lea编写),专门用于累加操作,效率提升5倍。

    public class LongAddrTest {
    
        public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                addr(() -> new AtomicLong(0), (AtomicLong::incrementAndGet));
            }
            for (int i = 0; i < 5; i++) {
                addr(LongAdder::new, LongAdder::increment);
            }
    
        }
    
        static <T> void addr(Supplier<T> supplier, Consumer<T> action) {
            T t = supplier.get();
            List<Thread> threads = new ArrayList<>(4);
            long start = System.currentTimeMillis();
            for (int i = 0; i < 4; i++) {
                threads.add(new Thread(() -> {
                    for (int j = 0; j < 500000; j++) {
                        action.accept(t);
                    }
                }));
            }
            threads.forEach(Thread::start);
            threads.forEach(thread -> {
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            long end = System.currentTimeMillis();
            System.out.println("耗时:" + (end - start));
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    测试结果:
    在这里插入图片描述
    LongAddr性能提升的原因是拆分了多个累加单元[cell0]…,当竞争激烈的时候,累加分散到多个cell减少失败重试,最后将结果汇总,最终减少竞争提高效率。

  • 相关阅读:
    Zookeeper的六个要点问题
    详解CAN总线:高速CAN总线和低速CAN总线的特性
    操作系统:处理机调度与死锁 练习题
    ElementUI之动态树+数据表格+分页
    关于使用pytorch-lightning版本过低的一些问题
    df命令:显示系统上可使用的磁盘空间
    ROS的程序编写流程
    ESP8266-Arduino编程实例-深度休眠与唤醒
    ORACLE XXX序列 goes below MINVALUE 无法实例化的处理办法
    json-server|0编码实现REST API
  • 原文地址:https://blog.csdn.net/yx444535180/article/details/126931257