• JUC原子类


    JUC原子类

    CAS

    实现线程安全的方法:

    • 互斥同步:synchronized,Lock.
    • 非阻塞同步:CAS,Atomic.
    • 无同步方法:栈封闭,ThreadLocal,可重入代码

    CAS介绍

    • CAS(Compare-And-Swap):对比并交换,是一个原子操作.
    • 先比较旧值是否发生变化,若没有,则交换成新值;否则不进行交换.
    • CAS是原子的,多线程使用CAS更新数据时,可以不使用锁.

    示例:

    public AtomicInteger i = new AtomicInteger(0);
    
    public int add(){
        return i.addAndGet(1);
    }
    

    存在的问题:

    • ABA问题:CAS只检查值是否发生变化,若一个值由A变为B后又变回为A,则认为没有变化而正常进行操作.
    • 循环时间长开销大:自旋CAS若长时间不成功,则会给CPU带来很大的执行开销.
    • 只能保证一个共享变量的原子操作:当对一个共享变量进行操作时,可以使用CAS保证原子操作.但是要需要对多个共享变量进行操作,则无法保证多个变量的原子性.

    解决:

    • ABA问题:使用版本号,在变量上加入版本号,每次变量变更后更新对应的版本号,若版本号一致才认为没有改变.(JUC提供AtomicStampedReference,其先检查当前引用是否等于预期引用,若等于才设置为新值)
    • 自旋问题:使用处理器提供的pause指令==>延迟流水线执行指令,避免循环时因内存顺序冲突引起的流水线被清空.
    • 多变量问题:使用AtomicReference类将多个变量放在一个对象中进行CAS操作.

    UnSafe类

    提供的功能:

    • 内存操作
    • CAS
    • Class相关
    • 对象操作
    • 线程调度
    • 系统信息获取
    • 内存屏障
    • 数组操作

    CAS原理:使用自旋调用UnSafe中CAS更新,若失败则重试.


    AtomicInteger

    常用API

    public final int get(); // 获取值
    public final int getAndSet(int newValue); // 获取当前值,并设为新值
    public final int getAndIncrement(); // 获取旧值,并自增
    public final int getAndDecrement(); // 获取旧值,并自增
    public final int getAndAdd(int delta); // 获取旧值,并加指定值
    void lazySet(int newValue); // 设为指定值,但非即时
    

    原理:使用volatile和CAS保证原子操作.

    • volatile:保证可见性,多线程并发时,一个线程修改,其他线程可见.
    • CAS:保证原子性.

    原子类小结

    原子更新基本类型

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

    原子更新数组

    • AtomicIntegerArray:原子更新整型数组.
    • AtomicLongArray:原子更新长整型数组.
    • AtomicReferenceArray:原子更新引用类型数组.
      • get(int index):获取指定位置上的元素.
      • compareAndSet(int i,E except,E update):若当前值等于预期值,则原子方式设置为新值.

    原子更新引用类型

    • AtomicReference:原子更新引用类型.
    • AtomicStampedReference:原子更新引用类型,使用Pair存储元素值和版本号.
    • AtomicMarkableReference:原子更新带有标记的引用类型.

    原子更新字段类

    • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器.
    • AtomicLongFieldUpdater:原子更新长整型的字段的更新器.
    • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器.

    示例:

    // 1. 由AtomicIntegerFieldUpdater的newUpdater获取更新器(传入对应的类和待更新的字段名)
    // 2. 使用getAndAdd方法,传入待更新的对象实例和更新值
    // 注: 更新的字段类型要和更新器一致,否则抛出异常
    //      字段必须是volatile,保证线程间的可见性
    //      字段的修饰符要和操作对象的字段类型一致
    //      只能操作实例对象的字段,不能操作static字段
    //      不能修改final字段,不可对不可变字段更新
    //      Integer/Long包装类要使用AtomicReferenceUpdater进行修改
    public class Test{
        public void test(){
            newClass nc = new newClass();
            AtomicIntegerFieldUpdater<newClass> updater1 = AtomicIntegerFieldUpdater.newUpdater(newClass.class, "publicVal");
            int res1 = updater1.getAndAdd(nc, 2);
            System.out.println(res1);
            System.out.println(nc.publicVal);
    
            // 更新器类型与更新字段类型不一致,抛出异常
            AtomicIntegerFieldUpdater<newClass> updater2 = AtomicIntegerFieldUpdater.newUpdater(newClass.class, "integerVal");
            int res2 = updater2.getAndAdd(nc, 1);
            System.out.println(res2);
            System.out.println(nc.integerVal);
        }
    }
    
    class newClass{
        public volatile int publicVal = 1;
        protected volatile int protectedVal = 2;
        private volatile int privateVal = 3;
        public volatile static int staticVal = 4;
        public volatile Integer integerVal = 5;
        public volatile Long longVal = 6l;
    }
    

    AtomicStampedReference原理

    解决ABA问题

    静态私有类Pair包含两个域:

    • reference:维护对象引用.
    • stamp:标志版本.

    更新流程:

    • 若元素值和版本号没有改变,且更新值不变,返回true.
    • 若元素值和版本号未变,而更新值不同,则构造新的Pair对象并进行CAS更新Pair.

    小结:

    • 使用版本号控制.
    • 不重复使用Pair的引用,每次新建Pair作为CAS对象.

    示例:

    private static AtomicStampedReference stamp = new AtomicStampedReference(0,0); // 初始的reference和stamp
    
        @Test
        public void test() throws InterruptedException {
            Thread t1 = new Thread(() -> {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean isSuccess = stamp.compareAndSet(0, 1, 0, 1);
                System.out.println(isSuccess);
            });
    
            // 线程2的stamp不匹配,无法设置成功
            Thread t2 = new Thread(() -> {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean isSuccess = stamp.compareAndSet(0, 2, 1, 2); // 若设为(0,2,0,2)则有可能更新成功
                System.out.println(isSuccess);
            });
    
            t1.start();
            t2.start();
            TimeUnit.SECONDS.sleep(1);
            System.out.println("reference is: " + stamp.getReference() + " stamp: "+stamp.getStamp());
        }
    

    参考:

  • 相关阅读:
    一个爬虫的练习(妹子图)
    安装模块出现的编译问题(解决)
    基于套接字通信的简单练习(FTP)
    Python3 之选课系统
    数据爬取后台(PHP+Python)联合作战
    让pip 使用国内镜像源
    为啥学蛇和python10年后的变化
    模拟登陆百度以及Selenium 的基本用法
    冒泡排序及其效率分析(无聊搞来玩玩)
    LLVM编译器
  • 原文地址:https://www.cnblogs.com/truestoriesavici01/p/13214017.html
Copyright © 2020-2023  润新知