• JUC之CAS


    CAS(全称为CompareAndSwap,也有说是CompareAndSet,都差不多)是一条CPU并发原语,它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,判断预期值和更改新值的整个过程是原子的。在JAVA中,CAS的实现全部在sun.misc.Unsafe类中的各个方法,调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能。

    在传统方式中实现并发的手段是加锁,JAVA中的锁有synchronized和Lock(jdk1.5才有)。Lock是基于AQS和CAS实现的,这里先跳过。对于synchronized锁,JVM在执行它的时候会依赖操作系统的临界区机制。这样的话,每次执行到synchronized锁,都会经历用户态和内核态之间的切换。这个过程的消耗是很大的。而且,大多数时候synchronized锁住的操作是很细粒度的。为了细粒度的操作去经历用户态和内核态之间的切换是低效的做法。

    其实最常见的就是我们需要并发修改某个变量值,举个常见的例子,窗口售票,不加锁的代码如下所示:

    public class Test {
    
        public static void main(String[] args) {
            Stock stock = new Stock(10);
            for (int i = 0; i < 5; i++) {
                new Thread(stock, "窗口" + (++i)).start();
            }
        }
    
        private static class Stock implements Runnable {
            private volatile int count;
    
            public Stock(int count) {
                this.count = count;
            }
    
            @Override
            public void run() {
                for (;;) {
                    count--;
                    if (count < 0) {
                        return;
                    }
                    System.out.println(Thread.currentThread().getName() + "售出一张票,剩余票数:" + count);
                    try {
                        Thread.sleep(100L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    得到的结果:
    (票数和线程太少,需要多跑几遍,多了几乎一遍就可以看出问题)

    得到的结果很明显,一共卖出了12张票,其中红框中出现的数字就可以说明问题。出现问题的原因很简单,因为--count这个算式表达式并不是原子的。在一个线程对count进行计算赋值后,但还没有将新值推送到内存中时,另一个线程获取的count值还是原来的值,当这个线程拿着这个值去进行计算,就会出现上面的问题。(这个涉及到Java的内存模型JMM,有兴趣的可以自行了解)
    在JDK1.5之前,我们想要解决这个问题,就只能使用synchronized进行加锁,如下:

    public void run() {
    for (;;) {
    synchronized (this) {
    count--;
    }
    if (count < 0) {
    return;
    }
    System.out.println(Thread.currentThread().getName() + "售出一张票,剩余票数:" + count);
    try {
    Thread.sleep(100L);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

    但是如果仅仅是类似于--count这种并发计数功能,需要进行同步的操作粒度很细时,使用synchronized就大材小用了,不高效(即便现在synchronized经过很多优化,不再想最初那样耗资源,但是它毕竟是个锁,而且多个线程进行竞争的时候还是会变成重量级锁),而使用CAS来实现就会更加的轻量级,性能更好。先上代码再说

    public class Test {
    
        private static Unsafe unsafe;
    
        public static void main(String[] args) throws Exception {
            Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafeField.setAccessible(true);
            unsafe = (Unsafe) theUnsafeField.get(null);
            Stock stock = new Stock(10);
            for (int i = 0; i < 5; i++) {
                new Thread(stock, "窗口" + (++i)).start();
            }
        }
    
        private static class Stock implements Runnable {
            private volatile int count;
            private static long countOffset;
            public Stock(int count) {
                this.count = count;
                try {
                    countOffset = unsafe.objectFieldOffset(this.getClass().getDeclaredField("count"));
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                }
            }
    
            @Override
            public void run() {
                for (;;) {
                    int x = this.count;
                    int y = x - 1;
                    if (!unsafe.compareAndSwapInt(this, countOffset, x, y)) {
                        continue;
                    }
                    if (y < 0) {
                        return;
                    }
                    System.out.println(Thread.currentThread().getName() + "售出一张票,剩余票数:" + y);
                    try {
                        Thread.sleep(100L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    像上面代码一样,当我们使用CAS进行操作时,出现的效果和使用synchronized一样的。代码中使用了JAVA提供的sun.misc.Unsafe进行CAS操作,而且在代码总我使用反射进行获取Unsafe实例,之所以这样做,是因为JDK不想让我们开发者去直接使用Unsafe这个类,而且使用起来比较繁琐,他们给我们提供了一些封装好的类来供我们开发者使用,比如常用的java.util.concurrent.atomic.AtomicInteger、java.util.concurrent.atomic.AtomicBoolean、java.util.concurrent.atomic.AtomicIntegerArray。这些类中都有相同的特点,就是使用sun.misc.Unsafe进行CAS操作,内部进行了一些类似上面代码的封装,我们就以AtomicInteger进行代码演示。

    public class Test {
    
        public static void main(String[] args) throws Exception {
            Stock stock = new Stock(10);
            for (int i = 0; i < 5; i++) {
                new Thread(stock, "窗口" + (++i)).start();
            }
        }
    
        private static class Stock implements Runnable {
            private volatile AtomicInteger count;
            public Stock(int count) {
                this.count = new AtomicInteger(count);
            }
    
            @Override
            public void run() {
                for (;;) {
                    int x = count.get();
                    int y = x - 1;
                    if (!count.compareAndSet(x, y)) {
                        continue;
                    }
                    if (y < 0) {
                        return;
                    }
                    System.out.println(Thread.currentThread().getName() + "售出一张票,剩余票数:" + y);
                    try {
                        Thread.sleep(100L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    就像上面的代码一样,我们不必进行反射获取Unsafe,然后在获取字段在class中的偏移量这些繁琐的操作了,下面我们就去看看AtomicInteger的源码 

      

    看上面AtomicInteger的源码我们可以很明显的看的出来,在AtomicInteger进行类加载的时候,会通过sun.misc.Unsafe获取value这个变量在类文件中的偏移量,进行保存,跟我们直接使用Unsafe的操作是一样的。我们找到刚才使用的compareAndSet(x, y)方法的源码,可以看到底层就是使用unsafe实例进行CAS操作。

    AtomicInteger还有一些别的方法,比如getAndIncrement、getAndDecrement、getAndAdd、incrementAndGet、decrementAndGet等等,底层实际上还是使用的unsafe实例进行CAS操作,有兴趣的同学可以自己翻下源码看看,这里就不多说了。

    总结:CAS的出现就是为了解决一些简单的并发操作,将比较、赋值作为一个原子操作记性处理,实现无锁化处理,节省资源开销。

  • 相关阅读:
    【android】手机增加一个新的实体按键
    python自动化错误解决方法
    java.lang.IllegalArgumentException: Buffer size too small. size = 262144 needed = 2205991
    50个开发工具
    mysql 查询json串
    软考题目单选题目
    软考操作系统基础知识
    使用DM将mysql具体表同步到tidb
    python日期处理
    安装部署dm
  • 原文地址:https://www.cnblogs.com/zzw-blog/p/12850730.html
Copyright © 2020-2023  润新知