浅讲JUC下的原子类

JUC下原子类的详细讲解

原子整数 原子引用

原子整数,如AtomicInteger、AtomicLong等,它们的方法都是采用cas+volatile保证了原子性。

原子引用,如AtomicReference、AtomicStampReference、AtomicMarkableReference,用于引用类型,使用方法跟原子整数差不多,都是new出来,然后对象调用方法。

它们实际维护的值被放在属性value中。value被volatile修饰,确保读取值时到主存读取,拿到的值是最新值。

cas一般传入两个参数,这里拿AotmicInteger举例,expect是传入你获得到的value值,update传入要修改成的值(要更新,拿到value值,计算得到新值)。cas会拿expect去跟现在最新的value值对比,如果一致,证明value没有被其他线程修改过,那么修改成功;如果不一致,证明value已经被其他线程修改了,那么这次cas修改失败。

ABA

由于cas是对比原先值和最新值,只要一致,就可以修改。于是,会出现ABA问题。

ABA问题就是,比如value初始值是A,线程1拿到value为A,准备把value修改为C。在拿到A后,修改为C前,有线程2将value从A改为B,线程3又将value从B改为A,然后,线程1才接着执行,它要把value改为C,一看,它拿到的value是A,现在最新的value也是A,一致,于是cas修改成功。在这种情况下,线程A无法感知到其他线程对共享变量value从A改到B,又从B改回A的操作。

如果希望,线程修改value时,只要value被其他线程修改过,就cas失败,那么,可以加上一个版本号,只要value被修改一次,版本号就加1。线程修改时,不仅对比value值,还对比版本号,这样就可以避免ABA问题。

JUC也提供了带版本号的原子类AtomicStampedReference

它维护的值放在属性reference中,版本号是stamp。

它也提供了compareAndSet(cas)方法,比之前的类就是多了版本号。

但有时候,我们并不关心变量被修改了多少次,只关心是否被修改过,于是AtomicMarkableReference就登场了,它不像AtomicStampedReference维护整数版本号,而是维护一个boolean值。

原子数组

因为原子引用保护的是引用本身的线程安全,它修改的是引用本身地址,但它无法保护引用内部元素的变化,如数组。当线程不是想修改引用本身地址,而是想修改内部元素时,就要用到原子数组类了。如AtomicIntegerArray,该类还重写了toString()方法,可以直接打印。

一个使用原子数组和不安全数组进行自增的对比测试,使用了函数式接口编程,博主觉得是个非常好的例子,分享给大家。

public class Test39 {
    public static void main(String[] args) {
        //线程不安全数组
        demo(
                () -> new int[10],
                (array) -> array.length,
                (array,index) -> array[index]++,
                (array) -> System.out.println(Arrays.toString(array))
        );
        
        //线程安全数组
        demo(
                () -> new AtomicIntegerArray(10),  //原子数组
                (array) -> array.length(),
                (array,index) -> array.getAndIncrement(index),
                (array) -> System.out.println(array)
        );
    }

    /**
     * 
     * @param arraySupplier 提供数组,可以是线程不安全数组或线程安全数组
     * @param lengthFun 获取数组长度的方法
     * @param putConsumer   自增方法,回传array、index
     * @param printConsumer 打印数组的方法
     */
    //参数是函数式接口给你的,结果是调用者要给回来的
    //supplier  提供者   无中生有(无参,要结果)    () -> 结果
    //function  函数    一个参数一个结果      (参数)->结果            BiFunction  (参数1,参数2)->结果
    //consumer  消费者   一个参数没结果   (参数) -> void            BiConsumer  (参数1,参数2) -> void
    private static <T> void demo(
            Supplier<T> arraySupplier,
            Function<T, Integer> lengthFun,
            BiConsumer<T, Integer> putConsumer,
            Consumer<T> printConsumer ){
        List<Thread> ts = new ArrayList<>();    //线程集合,将线程加到集合里方便统一操作
        //拿到数组
        T array = arraySupplier.get();
        int length = lengthFun.apply(array);
        for (int i = 0; i < length; i++) {  //length个线程,即数组有多长,就拿多少个线程
            //每个线程对数组作10000次操作
            ts.add(new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    putConsumer.accept(array, j % length);  //j % length是拿下标,将操作均摊到数组的每个元素上
                }
            }));
        }
        
        //启动所有线程
        ts.forEach(t -> t.start());
        //等待所有线程结束
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        printConsumer.accept(array);
    }
}

输出结果:

[7730, 7745, 7732, 7696, 7704, 7725, 7764, 7730, 7721, 7756]
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

字段更新器

字段更新器保护的是对象中的属性,它能保证多个线程访问同一个对象的成员变量时,成员变量的线程安全性。(即原子修改对象的成员变量)如AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater。

要注意,被保护的属性不能被private修饰,因为字段更新器要访问它。同时,被保护的属性必须被volatile修饰,因为要用cas,如果不被volatile修饰,会报错。

@Slf4j(topic = "c.Test40")
public class Test40 {
    public static void main(String[] args) {
        Student s = new Student();
        //参数1:哪个类
        //参数2:哪个属性
        //参数3:属性名
        AtomicReferenceFieldUpdater updater = 
                AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
        System.out.println(updater.compareAndSet(s, null, "张三"));
        System.out.println(s);
    }
}

class Student {
    volatile String name;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }
}
true
Student{name='张三'}

原子累加器

虽然原子整数可以调用如getAndIncrement()等方法进行累加,但JUC提供LongAdder原子累加器,它利用了cells累加单元,提高了累加效率,比调用原子整数方法累加效率快了许多。

具体原因是原子整数类累加都是在一个共享变量上累加,当竞争比较激烈时,就会频繁出现cas失败重试的现象,影响效率。而LongAdder在有竞争时会创建累加单元,每个线程有自己的累加单元,线程都在自己的累加单元上累加,最后将结果汇总,这样就减少了cas重试次数,提高了效率。

increment()

LongAdder的自增方法:increment()

它是调用了add(1L)

add流程如下:

①初始时,累加单元数组cells为空,即(as = cells) == null。caseBase在base属性上累加(没有竞争,就在base属性上累加,等同于原子整数类在一个变量上累加),如果cas成功,累加完成,add方法结束;如果cas失败,证明有竞争了,进入第一个if块。

因为【as == null】成立(累加单元数组cells为空),进入第二个if块,执行longAccumulate(),里面会去创建累加单元数组。

②再次调用add(1L),累加单元数组不为空了(那证明已经有过竞争了,不再在base属性上自增,要直接去累加单元上自增了),进入第1个if块,【a = as[getProbe() & m]) == null】判断当前线程是否有累加单元,如果没有,也就是条件成立,进入第二个if块。进入longAccumulate() (它去为当前线程创建累加单元)。如果当前线程有累加单元,【!(uncontended = a.cas(v = a.value, v + x))】让当前线程在自己的累加单元上进行cas操作,如果成功,add方法结束;如果失败,进入longAccumulate() (证明累加单元不够,扩容)

总结流程:

先看有没有累加单元数组,判断是否发生过竞争,没有就在base属性上cas,成功了直接返回,失败了就去调用longAccumulate()。有累加单元数组了,存在过竞争看当前线程是否有,累加单元,如果有,在累加单元上累加,成功返回,失败证明累加单元不够,去扩容,走longAccumulate();如果当前线程还没有累加单元,执行longAccumulate()

流程图:

longAccumulate()

先贴个整体

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) {
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            if ((as = cells) != null && (n = as.length) > 0) {
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        Cell r = new Cell(x);   // Optimistically create
                        if (cellsBusy == 0 && casCellsBusy()) {
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        if (cells == as) {      // Expand table unless stale
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                h = advanceProbe(h);
            }
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

主要逻辑在for死循环中,用if/else分为三大块:

对应add()调用longAccumulate()的三种情况,分别来看:

①没有累加单元数组进来:

cells就是累加单元数组,当没有累加单元时,第一个if ((as = cells) != null && (n = as.length) > 0)判断失败,进入第二个if。cellsBusy == 0判断还没有线程对累加单元数组加锁,cells==as判断累加单元数组还是原来的那个,即没有被别的线程修改过(多线程情况下,可能当前线程判断了累加单元数组为空后,准备第二个if判断,另一个线程刚好改完释放了锁,当前线程就拿到了锁,所以还要判断一下cells是不是原来拿到的cells),casCellsBusy()是用cas对cells加锁,即将cellsBusy从0改为1。如果cas失败,会进入第三个if块,会去修改base的值,修改成功方法结束,修改失败继续循环。

如果三个条件都成立,进入第二个if块,准备创建累加单元数组

    else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    //再次判断cells还是原来的cells
                    if (cells == as) {
                        //刚开始创建累加单元数组,大小只有2,后面才会慢慢扩容。
                        Cell[] rs = new Cell[2];    //大小为2的数组
                        rs[h & 1] = new Cell(x);    //但只创建了一个累加单元
                        cells = rs;    //将数组赋值给cells
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;    //最后解锁
                }
                if (init)
                    break;
            }

②有累加单元数组,但当前线程还没有累加单元:

明显会进入死循环中第一个if块,然后会进入这个if块,因为a = as[(n - 1) & h]) == null表示当前线程没有自己的累加单元。

            if ((a = as[(n - 1) & h]) == null) {
                    //cells还没被加锁
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        //创建一个累加单元
                        Cell r = new Cell(x);   // Optimistically create
                        //再次判断没有被加锁,cas去对cells加锁
                        if (cellsBusy == 0 && casCellsBusy()) {
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                //再次检查cells不为空,并且当前线程累加单元为空
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    放入
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {    //解锁
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }

③有累加单元数组,当前线程也有累加单元,但对累加单元累加失败:

这个块是对当前线程的累加单元再次尝试累加,如果成功,方法结束;如果失败,不会进这个else if,接着往下走。

boolean collide表示是否要扩容,如果累加单元数组长度大于CPU核心数,那么扩容后,即再增加累加单元,是没有意义的。如果没有超过CPU核心数,到else if(!collide),证明有累加单元数组并且在当前线程的累加单元上累加失败了,collide为false,于是进入这个else if块,将collide改为true,表示要扩容。

然后会继续循环,下一次循环会走扩容逻辑。

扩容逻辑进入下面这个块,如果cas能够成功加锁,并且cells还是原来的cells,创建一个长度为原来2倍的累加单元数组,将原数组元素拷贝到新数组,再将新数组赋值给cells,最后解锁。

<think>好的,用户想了解Java JUC包下的常用及其详细说明。首先,我需要回忆JUC包的主要组成部分,包括原子、并发集合、锁、同步器、线程池相关等。记得JUCjava.util.concurrent的缩写,是处理并发编程的重要工具。 用户提供的引用内容提到了乐观锁、ConcurrentHashMap、CopyOnWriteArrayList和synchronized方法,这些都属于JUC的一部分。需要将这些内容整合到回答中,确保引用标注正确。比如引用[1]关于乐观锁和CAS算法,可以对应到原子部分。引用[2]提到的ConcurrentHashMap是并发集合的重要例子,而引用[3]的synchronizedList属于Collections工具,但可能和JUC中的并发集合形成对比。 接下来要确定回答的结构,按照不同别列出常用,每个别下给出具体和说明,并结合示例代码。需要确保每个引用正确对应到相关部分,比如原子部分引用[1],并发集合部分引用[2]和[3]。 需要注意的是,用户可能不仅需要名,还需要详细说明和使用场景。例如,ConcurrentHashMap的性能优势,CopyOnWriteArrayList的适用场景,以及锁和同步器的区别等。示例代码要简洁,展示基本用法,比如使用ReentrantLock或CountDownLatch。 另外,用户示例中的Python代码可能只是示例结构,这里需要用Java代码示例。要检查每个的典型用法,确保语法正确,比如ConcurrentHashMap的实例化和方法调用。 最后,生成相关问题时要围绕JUC包扩展,可能涉及使用场景、性能比较、底层原理等,帮助用户深入理解。比如线程池配置参数的意义,或CAS算法的实现细节。 总结步骤: 1. 分JUC包下的常用。 2. 每个别下列举具体,详细说明功能和使用方法。 3. 插入对应的引用标注,确保引用与内容匹配。 4. 提供Java代码示例,正确展示用法。 5. 生成相关问题,覆盖使用、原理、比较等方面。</think>### Java JUC包常用及说明 Java的`java.util.concurrent`(JUC)包提供了高效、线程安全的并发编程工具,以下是其核心组件及详细说明: --- #### 一、原子(`java.util.concurrent.atomic`) 1. **AtomicInteger/AtomicLong** 支持原子操作的整型/长整型变量,底层通过CAS(Compare-And-Swap)实现无锁线程安全[^1]。 ```java AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // 原子自增 ``` 2. **AtomicReference** 用于原子更新对象引用,适用于无锁数据结构设计。 ```java AtomicReference<String> ref = new AtomicReference<>("initial"); ref.compareAndSet("initial", "updated"); // CAS操作 ``` --- #### 二、并发集合 1. **ConcurrentHashMap** 高并发场景下替代`Hashtable`,采用分段锁(JDK 8后改为CAS+节点锁)提升性能[^2]。 ```java ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); map.put("key", 1); ``` 2. **CopyOnWriteArrayList** 写操作时复制新数组,读操作无锁,适用于读多写少的场景。 ```java CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("element"); ``` --- #### 三、锁与同步器 1. **ReentrantLock** 可重入锁,支持公平/非公平模式,替代`synchronized`关键字。 ```java ReentrantLock lock = new ReentrantLock(); lock.lock(); try { /* 临界区 */ } finally { lock.unlock(); } ``` 2. **CountDownLatch** 线程等待计数器归零后继续执行,适用于多线程任务同步。 ```java CountDownLatch latch = new CountDownLatch(3); latch.countDown(); // 计数器减1 latch.await(); // 等待计数器归零 ``` 3. **CyclicBarrier** 所有线程到达屏障点后继续执行,支持重置,适用于分阶段任务。 ```java CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All threads arrived")); barrier.await(); ``` --- #### 四、线程池相关 1. **ExecutorService** 线程池接口,常用实现如`ThreadPoolExecutor`。 ```java ExecutorService executor = Executors.newFixedThreadPool(4); executor.submit(() -> System.out.println("Task executed")); ``` 2. **Future与CompletableFuture** `Future`表示异步计算结果,`CompletableFuture`支持链式异步编程。 ```java CompletableFuture.supplyAsync(() -> "Result") .thenApply(s -> s.toUpperCase()) .thenAccept(System.out::println); ``` --- #### 五、其他工具 1. **Semaphore** 控制同时访问资源的线程数量,适用于限流场景。 ```java Semaphore semaphore = new Semaphore(5); // 允许5个线程同时访问 semaphore.acquire(); semaphore.release(); ``` 2. **BlockingQueue** 线程安全的阻塞队列,如`ArrayBlockingQueue`和`LinkedBlockingQueue`。 ```java BlockingQueue<String> queue = new ArrayBlockingQueue<>(10); queue.put("item"); // 阻塞直到队列有空位 String item = queue.take(); // 阻塞直到队列有元素 ``` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值