07-Java多线程、原子操作和CAS

文章目录

  • 一、原子操作
    • 1.1 Java中原子操作实现方式
      • 1.1.1 悲观锁(阻塞同步)
      • 1.1.2 乐观锁(非阻塞同步)
    • 1.2 原子操作示例
      • 1.2.1 JDK原子操作类型
      • 1.2.1 示例
  • 二、CAS
    • 2.1 CAS的实现原理
      • 2.1.1 总线加锁
      • 2.1.1 缓存加锁
    • 2.2 CAS的缺陷
      • 2.2.1 循环时间长
      • 2.2.2 只能对一个变量操作
      • 2.2.3 ABA问题
    • 2.3 解决ABA问题
      • 2.3.1 代码
  • 三、小结
  • 四、参考

一、原子操作

  • 原子操作是一个不能再切分的操作。如果一个操作满足原子性和可见性,那么就是线程安全的。

1.1 Java中原子操作实现方式

1.1.1 悲观锁(阻塞同步)

  • synchronized或者显示锁

1.1.2 乐观锁(非阻塞同步)

  • 乐观的解决方案,顾名思义,就是很大度乐观,每次操作数据的时候,都认为别的线程不会参与竞争修改,也不加锁。如果操作成功了那最好;如果失败了,比如中途确有别的线程进入并修改了数据(依赖于冲突检测),也不会阻塞,可以采取一些补偿机制,一般的策略就是反复重试。很显然,这种思想相比简单粗暴利用锁来保证同步要合理的多。

1.2 原子操作示例

1.2.1 JDK原子操作类型

类型 描述
AtomicBoolean 原子布尔
AtomicInteger 原子整型
AtomicIntegerArray 原子整型数组
AtomicLong 原子长整型
AtomicLongArray 原子长整型数组
AtomicReference 原子引用
AtomicReferenceArray 原子引用数组
AtomicMarkableReference 原子布尔版本戳(判断是否被修改)
AtomicIntegerFieldUpdater 原子整型字段更新
AtomicLongFieldUpdater 原子长整型字段更新
AtomicReferenceFieldUpdater 原子引用字段更新
AtomicStampedReference 原子整型版本戳(判断被修改过几次)

1.2.1 示例

  • 我们看到,如果使用原子类型,多线程之间的操作自增是安全的,每一次都可以获得正确的结果,如果只是使用volatile的话,是不行的,每次结果都是不一样的
public class AtmoTest {

    //切换volatile和原子类型
    static AtomicInteger auto = new AtomicInteger(0);
    //static volatile int  auto = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(new Modify()).start();
        }
        SleepTools.second(5);
        System.out.println("修改后的值是:" + auto);
    }

    static class Modify implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                //切换volatile和原子类型
                auto.getAndIncrement();
                //auto++;
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

二、CAS

  • CAS是Compare And Swap,即先比较再设置。CAS是java中原子操作乐观锁的一种实现。在java.util.concurrent.atomic包下的原子类都是基于CAS来实现的。
  • 更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
  • 更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  • 更新引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
  • 原子更新字段类: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater

2.1 CAS的实现原理

  • JAVA中的CAS操作都是通过sun包下Unsafe类实现,而Unsafe类中的方法都是native方法,由JVM本地实现,具体和不同的OS和硬件CPU都有关系。在Linux的X86下主要是通过cmpxchgl这个指令在CPU级完成CAS操作的,但在多处理器情况下必须使用lock指令加锁来完成,具体有下面2种处理办法。

2.1.1 总线加锁

总线加锁就是就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。但是这种处理方式显得有点儿霸道,不厚道,他把CPU和内存之间的通信锁住了,在锁定期间,其他处理器都不能其他内存地址的数据,其开销有点儿大,所以就有了缓存加锁。

2.1.1 缓存加锁

其实针对于上面那种情况,我们只需要保证在同一时刻,对某个内存地址的操作是原子性的即可。缓存加锁,就是缓存在内存区域的数据如果在加锁期间,当它执行锁操作写回内存时,处理器不再输出LOCK# 信号,而是修改内部的内存地址,利用缓存一致性协议来保证原子性。缓存一致性机制可以保证同一个内存区域的数据仅能被一个处理器修改,也就是说当CPU1 修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。

2.2 CAS的缺陷

2.2.1 循环时间长

  • 循环时间太长。如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。在JUC中,有些地方就限制了CAS自旋的次数,例如: BlockingQueue的SynchronousQueue 。

2.2.2 只能对一个变量操作

  • 只能对一个变量做原子操作。看了 CAS 的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了,当
    然如果你有办法把多个变量整成一个变量,利用 CAS 也不错。例如读写锁中 state 的高低位。

2.2.3 ABA问题

  • CAS可以有效的提升并发的效率,但同时也会引入ABA问题。CAS需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是 A,变成了 B,然后又变成了 A,那么在 CAS检查的时候会发现没有改变,但是实质
    上它已经发生了改变,这就是所谓的ABA问题。对于 ABA 问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次
    改变时加 1 ,即 A —> B —> A ,变成1A —> 2B —> 3A 。 如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,
    并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程是有问题的。比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。

2.3 解决ABA问题

  • 原子包中提供了AtomicStampedReference/AtomicMarkableReference来处理会发生ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。前者可以记录版本号,通过版本号可知变量被修改了几次,后者内部通过一个布尔类型来记录变量是否被修改了,不记录修改的次数。

2.3.1 代码

public class ABA {

    private static AtomicInteger atomicInteger = new AtomicInteger(100);
    private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100, 1);

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

        // 1.线程at1悄悄的将atomicInteger改变了,然后又改了回来,
        Thread at1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInteger.compareAndSet(100, 110);
                atomicInteger.compareAndSet(110, 100);
            }
        });

        // 2.线程at1悄悄的将atomicInteger改变了,然后又改了回来,at2是感觉不到的
        Thread at2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(2);      // at1,执行完
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("使用AtomicInteger无法避免AB问题,我们修改成功了,说明我们没有意识到AtomicInteger已经被修改了: " + atomicInteger.compareAndSet(100, 120));
            }
        });

        at1.start();
        at2.start();

        at1.join();
        at2.join();

        // AtomicStampedReference
        // 3.线程tsf1悄悄的将atomicStampedReference改变了,然后又改了回来,但是版本号递增了
        Thread tsf1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //让 tsf2先获取stamp,导致预期时间戳不一致
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 预期引用:100,更新后的引用:110,预期标识getStamp() 更新后的标识getStamp() + 1
                atomicStampedReference.compareAndSet(100, 110, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
                atomicStampedReference.compareAndSet(110, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            }
        });

        // 4.线程tsf2悄悄的将atomicStampedReference改变了,然后又改了回来,但是版本号递增了,因此tsf2能够感知到,
        //因为他在sleep期间发现版本号被修改了
        Thread tsf2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedReference.getStamp();

                try {
                    TimeUnit.SECONDS.sleep(2);      //线程tsf1执行完
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("使用AtomicStampedReference可以避免AB问题,我们修改失败了,因为我们发现版本戳被修改了: " + atomicStampedReference.compareAndSet(100, 120, stamp, stamp + 1));
            }
        });

        tsf1.start();
        tsf2.start();
    }
}
  • 输出
使用AtomicInteger无法避免AB问题,我们修改成功了,说明我们没有意识到AtomicInteger已经被修改了: true
使用AtomicStampedReference可以避免AB问题,我们修改失败了,因为我们发现版本戳被修改了: false

三、小结

  • 在AtomicInteger中的主要方法都是一些先增在获取(addAndGet),或者先获取再自增(getAndIncrement)之类的API,这些操作其实包含2个步骤,拿getAndIncrement来举例,其实包括2个步骤:
1.是获取到这个变量在内存中的最新的,
2.将值自增1
3.将自增后的新的值写回内存保证其他线程看到最新结果
  • 这里实际上第一步和第三步是一个可见性的问题,在内存模型中,线程都有一个自己的私有内存空间,整个进程有一个主存,私有内存空间对其他线程是不可见的,因此我们写操作完成之后要刷到主内存,其他线程才能看到,读也是一样,我们读私有内存就可以读到脏值,需要去读主内存。
  • 第二步是一个原子性的问题,就是说自增的过程对这个变量来说是原子的,也就是这个过程是不可能被打断的。
  • 上面的3个过程是借助于CPU指令来实现的,简单描述就在2.1小节。借助于CPU指令级别的锁机制来保证线程之间的可见性和操作的原子性。
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
  • 疑问:CAS自旋会不会进入一个永远的死循环,如下所示如果期望将变量从11修改为15,如果这个变量永远都不会是11岂不是永远修改不成功,当然这个方法是立即返回的。但是在CAS的模式下编程会不会出现这样的死循环呢?这个要更多看JUC包下的源码中是如何使用CAS的。在AtomicInteger中类似于自增的操作是不会有这样的问题是,因为那些操作都是在现有值的基础上加1,不过仔细想想其实也有一个先读,再写的过程,具体可能需要研究更底层的实现机制。
    atomicInteger.compareAndSet(11, 15);
  • 补充
补充一个,在for循环中不断循环实现线程安全的自增方法,因为一个线程获取到之后,再去设置,这个过程中可能会有别的线程修改了,假如说
有多个线程,初始时为1,那么多个线程都获取到1,只会有一个线程将其设置为2,那么其他的线程在尝试将其设置为2的时候,发现他的值已经不
是1了,因此会失败,只能循环一次,再来获取一把,在尝试将其由2设置为3,因此类推,因此需要在循环中来实现。

四、参考

  • Java中CAS的实现原理
  • 面试必问的CAS,你懂了吗?

你可能感兴趣的:(并发编程)