java多线程之原子类及并发工具

原子类

原子类的实现原理

原子类的原理主要是使用了volatile+cas锁来实现原子性更新

以下就是原子类这一类java类中共有的核心代码:

private static final Unsafe U = Unsafe.getUnsafe();
private volatile int value;

public final boolean compareAndSet(int expectedValue, int newValue) {
        return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}

可以看到原子类中有一个Unsafe类型的对象,Unsafe类是java不对外开放的可以直接操作内存的类,其实现的compareAndSet()就是cas操作,也就是说cas操作是底层硬件支持的,其工作原理如下:

当线程执行CAS操作时,它会检查主内存中的值是否仍然与预期原值相匹配。如果匹配,则更新内存位置的值;如果不匹配,则操作失败。

示例说明:

假设有一个共享变量 x,其初始值为 0,存储在主内存的某个位置。

  1. 线程1 读取 x 的值为 0,并想要将其更新为 1
  2. 线程2 也读取 x 的值为 0,并想要将其更新为 2

在执行CAS操作之前,两个线程都会持有 x 的值的副本,即 0。当线程1尝试执行CAS操作时,它会检查主内存中 x 的值是否仍然是 0。如果是,则线程1将 x 的值更新为 1。如果线程2随后尝试执行CAS操作,它会发现主内存中 x 的值不再是 0(已经被线程1更新为 1),因此CAS操作失败。

同时在原子类中使用了volatile关键词修饰使得在执行对共享数据操作的时候,线程的副本会更新,同时共享内存下的数据也会更新,Cas在比对时就是共享内存下新的数据,从而让其它线程修改失败。

ABA问题

假设我们有两个线程:线程A、线程B。还有一个共享变量 value,其初始值为 A

  1. 线程A 读取 value 的值为 A,并打算将其更新为 B
  2. 线程B 也读取 value 的值为 A,并将其更新为 C
  3. 线程B 再次将 value 的值从 C 更新回 A
  4. 线程A 执行CAS操作,尝试将 value 的值从 A 更新为 B

由于线程A读取 value 的值时为 A,并且在线程A尝试执行CAS操作时,value 的值仍然是 A,因此CAS操作成功。但实际上,value 的值已经从 A 变为 C 再变回 A。这就产生了误导性的结果,线程A以为没有任何其他线程修改过 value 的值。

通常可以采取以下几种策略:

  1. 使用版本号:为共享变量添加一个版本号或时间戳,每次修改变量时都更新版本号。这样即使值相同,版本号也会不同,从而避免了ABA问题。
  2. 使用带有版本号的原子类:Java中提供了 AtomicStampedReference 类,它允许跟踪一个引用和一个版本号(也称为标记)。这样即使引用本身没有变化,版本号也会随着每次更新而改变,从而解决了ABA问题。

为什么引入LongAdder

使用CAS这类乐观锁不会阻塞线程,减少了上下文的开销,故而使用原子类在高并发场景下有很大的性能提升,但由于使用了volatile,每个线程增加了一个刷新到共享内存的开销,而且在并发程度高的时候线程容易CAS失败,于是就引入了LongAdder类,当并发度不高时默认使用base来自增,当并发度高的时候会使用一个cell数组,同时根据每个线程的hashcode来分配给线程各自的cell位置,每个线程只用负责自增自身内存的数据,然后LongAdder会使用一个sum来将base和cell值累加起来。

并发工具

Exchanger

概念

Exchanger 是 Java 中的一个并发工具类,它允许两个线程在一个特定的点上交换数据。当两个线程都到达这个交换点时,它们会交换各自携带的数据并继续执行。

代码演示
import java.util.concurrent.Exchanger;

public class ExchangerExample {

    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();

        Thread threadA = new Thread(() -> {
            String dataA = "Data from A";
            try {
                String dataB = exchanger.exchange(dataA);
                System.out.println("Thread A received: " + dataB);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread threadB = new Thread(() -> {
            String dataB = "Data from B";
            try {
                String dataA = exchanger.exchange(dataB);
                System.out.println("Thread B received: " + dataA);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        threadA.start();
        threadB.start();

        try {
            threadA.join();
            threadB.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
场景描述

假设你需要在两个线程之间同步交换数据,比如一个线程负责收集数据,另一个线程负责处理这些数据。你可以使用 Exchanger 来确保数据的同步交换。


CountDownLatch

概念

CountDownLatch 是一个同步辅助类,允许一个或多个线程等待其他线程完成操作。它通过一个计数器来控制线程的等待行为,当计数器减到0时,等待的线程会被释放。

代码演示
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);

        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " started");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " finished");
                latch.countDown(); // 减少计数器
            }).start();
        }

        System.out.println("Main thread waiting...");
        latch.await(); // 等待计数器减到0
        System.out.println("All tasks completed.");
    }
}
场景描述

假设你有一个主程序,需要等待所有子线程完成一些任务后再继续执行。你可以使用 CountDownLatch 来确保主程序不会提前继续执行,直到所有的子线程都完成了它们的任务。


Semaphore

概念

Semaphore 是一个用于控制对有限数量的资源的访问的并发工具类。它可以用于模拟信号量的行为,限制同时访问共享资源的线程数量。

代码演示
import java.util.concurrent.Semaphore;

public class SemaphoreExample {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3); // 最多允许3个线程同时访问

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire(); // 请求许可
                    System.out.println(Thread.currentThread().getName() + " is accessing a resource");
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " finished accessing");
                    semaphore.release(); // 释放许可
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
场景描述

假设你有一个服务,需要限制同时处理请求的客户端数量,以防止资源过度消耗。你可以使用 Semaphore 来限制同时处理请求的线程数量。


CyclicBarrier

概念

CyclicBarrier 是一个让一组线程相互等待,直到到达某个公共屏障点的工具。当所有参与线程到达屏障点时,它们会被释放并继续执行。

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {

    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3);

        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " started");
                try {
                    Thread.sleep(1000);
                    barrier.await(); // 等待所有线程到达
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " passed the barrier");
            }).start();
        }
    }
}
场景描述

假设你需要在多个线程之间同步执行某项操作,比如在多线程环境中启动一个任务,要求所有线程都准备好了才能一起开始。你可以使用 CyclicBarrier 来确保所有线程都到达了某个公共的屏障点后再继续执行。

ps:CountDownLatch和CyclicBarrier前者专注于count的数量是否达标,也就是无论是一个线程执行的countDown还是一个任务执行的countDown只要将count减为零被await()的线程就能被唤醒,而后者专注于线程数,每个线程中需要使用await来等待线程数到规定的数量,同时前者是不可复用的,计数器的数字不会重置,而后者是可复用的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值