【Java EE】CAS原理和实现以及JUC中常见的类的使用

发布于:2024-04-30 ⋅ 阅读:(27) ⋅ 点赞:(0)

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱
ʕ̯•͡˔•̯᷅ʔ大家好,我是xiaoxie.希望你看完之后,有不足之处请多多谅解,让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客
本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如需转载还请通知˶⍤⃝˶
个人主页xiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客

系列专栏:xiaoxie的JAVAEE学习系列专栏——CSDN博客●'ᴗ'σσணღ
我的目标:"团团等我💪( ◡̀_◡́ ҂)" 

( ⸝⸝⸝›ᴥ‹⸝⸝⸝ )欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​+关注(互三必回)!

目录

​编辑一.CAS

1.什么是CAS

2.CAS的原理

例子:抢座位游戏

如何用CAS解决:

 3.CAS出现的问题

4.CAS与锁的区别

1. 实现方式:

2. 性能特性:

3. 可伸缩性:

4. 编程复杂性:

5. 死锁和饥饿:

6.总结:

二.JUC(Java.util.concurrent)中常见类的使用

 1.ReentrantLock

1.ReentrantLock 的主要特点:

2.ReentrantLock 的基本使用 

3.ReentrantLock vs synchronized  

4.注意事项 

 2.原子类(AtomicXXX)

 3.Semaphore(信号量)

1.Semaphore 的基本概念

2.Semaphore 的使用场景:

3.Semaphore 的基本使用: 

4.Semaphore 的公平性 

5.注意事项:

 4.CountDownLatch

1.CountDownLatch 的主要特点:

2.CountDownLatch 的基本使用: 

3.注意事项:

4.运用场景(重点)

5.FutureTask 和 Callable

1.Callable 接口:

2.FutureTask 类:

3.使用示例:

主要特点:

注意事项:

6.CyclicBarrier

1.CyclicBarrier 的主要特点:

2.CyclicBarrier 的基本使用:

3.注意事项:

三.ThreadLocal的底层实现

1.ThreadLocal 的底层实现原理:

2.示例代码: 

3.注意事项:


一.CAS

1.什么是CAS

在Java中,CAS代表“Compare-And-Swap”,即比较并交换。这是一种用于实现多线程中无锁编程的原子操作。CAS操作通常由三个参数:内存地址V旧的预期值A要更新的新值B。只有当内存地址V的当前值与预期值A相等时,才将内存地址V的值更新为B。如果不相等,则不进行任何操作。

CAS操作通常用于实现线程安全的计数器、栈等数据结构,以及在并发编程中实现同步机制,如锁。CAS操作可以避免使用传统的锁机制,从而减少线程间的上下文切换,提高程序的执行效率。

2.CAS的原理

CAS(Compare-And-Swap)的原理基于一个简单的思想:在多线程环境中,当多个线程尝试同时修改共享数据时,CAS提供了一种机制来确保只有一个线程能够成功修改数据。CAS操作通常由处理器的原子指令直接支持,这意味着在执行CAS操作时,不会受到其他线程的干扰

CAS操作的三个主要步骤如下:

  1. 比较:首先,CAS检查内存中的值是否与预期值(expected value)相等。这个预期值是线程在执行CAS操作之前所知道的值。

  2. 交换:如果内存中的值与预期值相等,CAS将执行交换操作,即将内存中的值更新为新值(new value)。

  3. 返回结果:CAS操作完成后,会返回一个布尔值,指示操作是否成功。如果内存中的值与预期值不相等,CAS操作失败,返回false;如果内存中的值与预期值相等,并且值已被成功更新,返回true

CAS操作通常用于实现无锁数据结构和算法,因为它们可以避免使用传统的锁机制,从而减少线程间的上下文切换,提高程序的执行效率。

在Java中,java.util.concurrent.atomic包提供了多个原子类,如AtomicIntegerAtomicLongAtomicReference等,它们内部都使用了CAS操作来实现线程安全的原子操作。

以上的原理内容,确实有点枯燥难懂,博主这里举个小例子来说明一下吧

例子:抢座位游戏

想象一下,你和一群朋友在玩一个抢座位的游戏。游戏规则如下:

  1. 开始游戏:所有玩家围成一个圈,但圈里比玩家人数少一个座位。
  2. 音乐开始:当音乐开始播放时,所有玩家围绕座位走动。
  3. 音乐停止:一旦音乐停止,每个玩家都要尽快找到一个座位坐下。
  4. 抢座位:如果两个玩家同时试图坐同一个座位,他们需要通过一种方式来决定谁可以坐下。

在这个游戏中,我们可以将每个座位看作是一个共享资源,玩家则代表不同的线程。现在,我们用CAS的原理来解决两个玩家同时试图坐同一个座位的问题:

  1. 比较(Compare):每个玩家都会记住自己之前坐过的座位(预期值A)。

  2. 尝试坐下(Swap):当音乐停止时,每个玩家都会尝试去坐他们记住的那个座位。如果座位上没有人(当前值与预期值相等),玩家就可以坐下(将新值B,即玩家自己,与当前值A进行交换)。

  3. 确认结果:如果玩家成功坐下(CAS操作成功),他们就安全了,不需要再移动。如果有另一个玩家也试图坐同一个座位,并且先坐下了(CAS操作失败),那么没有成功的玩家需要继续寻找其他座位。

如何用CAS解决:

  • 无锁机制:玩家不需要一个中央权威来告诉他们谁可以坐下,他们通过比较和尝试坐下的机制自行解决冲突,这类似于无锁编程中的CAS操作

  • 原子性:在CAS中,比较和交换是作为一个不可分割的步骤执行的,保证了操作的原子性。在游戏中,玩家尝试坐下的行为也是原子的,要么成功坐下,要么不坐下并继续寻找下一个座位。

  • 并发控制:CAS操作可以控制多个线程对共享资源的并发访问,而在游戏中,玩家通过CAS原则来解决座位的争抢问题,避免了混乱。

 3.CAS出现的问题

CAS操作虽然高效,但也存在一些问题,如:

  • ABA问题:如果一个值从A变为B,然后又变回A,CAS检查会认为值没有变化,因为当前值与预期值都为A,这可能导致一些逻辑错误。

  • 循环等待:如果多个线程同时尝试更新同一个值,可能会导致线程循环等待,因为CAS操作可能会连续失败。

  • 只能保证一个共享变量的原子操作:CAS只能确保单个共享变量的原子操作,如果需要原子地操作多个共享变量,CAS就显得无能为力了。

为了解决这些问题,Java提供了一些额外的原子类,如AtomicStampedReference通过引入版本号来解决ABA问题)和AtomicReferenceArray用于原子地操作数组中的元素)。

CAS操作是现代多线程编程中实现线程安全和高效并发访问共享资源的重要工具。通过理解CAS的原理和使用场景,开发者可以更好地利用这一机制来构建高性能的并发程序。

4.CAS与锁的区别

1. 实现方式:

  • CAS 是一种基于硬件原子操作的无锁编程技术。它通过比较内存中的当前值和预期值来决定是否进行交换操作,从而实现对共享资源的原子更新。

  • 是一种更为传统的并发控制机制,它通常依赖于操作系统的同步原语(如互斥锁、信号量等)来保证在任一时刻只有一个线程可以访问共享资源。

2. 性能特性:

  • CAS 通常在低竞争环境下表现更好,因为它避免了操作系统切换线程的开销。然而,在高竞争环境下,CAS可能会导致大量的上下文切换和CPU循环等待,从而降低性能。

  • 可以提供更强的保证,如公平性(按照请求锁的顺序授予锁)和可重入性(一个线程可以重复请求它已经持有的锁)。但锁的使用可能会增加上下文切换的开销,尤其是在锁竞争激烈的情况下。

3. 可伸缩性:

  • CAS 在写入操作较少时更为高效,因为它们不需要操作系统介入,减少了上下文切换。但是,如果多个线程频繁地竞争同一块内存,CAS可能会导致“活锁”(即线程不断地重试,但很少或从未成功)。

  • 可以更好地处理高并发情况,因为操作系统可以介入来调度线程,避免单个线程长时间占用资源。但这也意味着在高并发下,锁可能会成为性能瓶颈。

4. 编程复杂性:

  • CAS 需要开发者自己管理复杂的状态和错误情况,如ABA问题(一个值从A变为B,再变回A,CAS检查会认为它没有变化)。这增加了编程的复杂性。

  • 提供了更简单的编程模型,因为操作系统负责管理锁的状态。开发者通常只需要关心何时获取和释放锁。

5. 死锁和饥饿:

  • CAS 不会直接导致死锁,因为它们不涉及操作系统级别的资源分配。但是,如果不正确地使用CAS,可能会导致其他问题,如活锁。

  • 如果使用不当,可能会导致死锁和饥饿。死锁发生在两个或多个线程相互等待对方释放锁,而饥饿是指某个线程长时间无法获得锁。

6.总结:

CAS和锁各有优势和局限性。CAS在低竞争环境下提供了一种高效且细粒度的控制方式,而锁则在高竞争和需要严格顺序控制的场景下更为适用。在实际应用中,选择哪种机制取决于具体的性能要求、并发模式和编程复杂性的权衡。在某些情况下,结合使用CAS和锁(例如,使用锁来保护低级别的CAS操作)可以提供更好的性能和可伸缩性。

二.JUC(Java.util.concurrent)中常见类的使用

 1.ReentrantLock

ReentrantLock 是 Java 中的一个接口,属于 java.util.concurrent.locks 包,它提供了一种锁机制,可以被用来控制多个线程对共享资源的访问。

1.ReentrantLock 的主要特点:

  1. 重入性:正如其名字所暗示的,ReentrantLock 允许同一个线程多次获取锁,但必须确保获取和释放锁的次数相同。

  2. 公平性ReentrantLock 允许设置公平性。如果设置为公平锁,则尝试获取锁的线程将按照请求的顺序获得锁;如果设置为非公平锁(默认),则可能发生线程饥饿,因为任何线程都有机会获取锁。

  3. 可中断:当一个线程在尝试获取锁时,如果锁已被其他线程持有,它可以中断等待。

  4. 超时ReentrantLock 提供了一个带超时参数的获取锁的方法,如果在超时时间内锁没有被获取,方法将返回。

  5. 条件变量ReentrantLock 可以与 Condition 对象配合使用,实现等待/通知机制

2.ReentrantLock 的基本使用 

public static void main(String[] args) {
        //这里可以设为true 就变为公平锁
        ReentrantLock locker = new ReentrantLock(true);
        locker.lock();//上锁
        try{
            // 受保护的代码
        }finally {//使用try - finally 确保一定会执行locker.unlock();
            locker.unlock();//释放锁
        }
    }

3.ReentrantLock vs synchronized  

  • 性能:在某些情况下,ReentrantLock 可以提供比 synchronized 更好的性能,尤其是在竞争不激烈的情况下。

  • 灵活性ReentrantLocksynchronized 提供了更多的灵活性,例如可以设置公平性、尝试非阻塞地获取锁、可中断等待等。

  • 实现方式synchronized 是一种关键字,其使用更简单,而 ReentrantLock 是一个类,需要显式地管理锁的获取和释放。

  • 可中断性ReentrantLock 允许尝试获取锁的线程在等待时被中断,而 synchronized 不支持这一点。

  • 条件队列ReentrantLock 可以与条件变量配合使用,提供更复杂的线程间协调,而 synchronized 需要配合 Objectwaitnotify 方法实现条件等待。

4.注意事项 

  • 使用 ReentrantLock 时,必须确保在每个 lock() 对应一个 unlock(),避免死锁。

  • 在使用 ReentrantLocklockInterruptibly() 方法时,要注意处理 InterruptedException

  • synchronized 不同,ReentrantLock 不会自动释放锁,必须显式地调用 unlock() 方法。

  • 在设计锁时,应考虑使用 ReentrantLock 还是 synchronized,根据实际场景和需求选择最合适的锁机制。

 2.原子类(AtomicXXX)

 如AtomicInteger、AtomicLong、AtomicBoolean等,它们提供了原子性的递增、递减、设置值等操作,基于CAS实现,相比synchronized效率更高。

AtomicInteger counter = new AtomicInteger(0);
int newValue = counter.incrementAndGet(); // 原子性增加并获取新值

 3.Semaphore(信号量)

1.Semaphore 的基本概念

  • 计数器Semaphore 有一个计数器,表示可用的许可(permits)数量。线程可以从信号量获取(take)或释放(release)许可。

  • 获取许可:当线程需要访问受信号量保护的资源时,它首先尝试从信号量获取一个许可。如果信号量的计数器大于零,计数器减一,线程成功获取许可。

  • 释放许可:线程访问完资源后,将释放一个许可,信号量的计数器加一。

  • 阻塞等待:如果信号量的计数器为零,尝试获取许可的线程将被阻塞,直到其他线程释放一个许可。

  • 特殊的信号量:锁其实就是一个特殊的信号量,如果信号量的取值要么为0要么为1,那么锁就是一个特殊的信号量.

2.Semaphore 的使用场景:

  1. 限制资源访问数量:例如,限制同时访问数据库连接的线程数量

  2. 控制线程数:例如,限制同时执行任务的线程数量

  3. 实现信号量模型:用于解决生产者-消费者问题

3.Semaphore 的基本使用: 

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private Semaphore semaphore = new Semaphore(3);

    public void accessResource() {
        try {
            // 尝试获取一个许可,最多等待1秒
            if (semaphore.tryAcquire(1, 1, TimeUnit.SECONDS)) {
                try {
                    // 访问资源
                    System.out.println("访问资源");
                } finally {
                    // 释放许可
                    semaphore.release(1);
                }
            } else {
                System.out.println("无法获取许可,已超时");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

 在这个例子中,我们创建了一个初始许可数量为3的信号量。accessResource 方法尝试获取一个许可,如果成功,将访问资源,然后释放许可。如果获取许可失败(超时),则输出提示信息。

4.Semaphore 的公平性 

ReentrantLock 一样,Semaphore 也有公平性和非公平性之分:

  • 非公平性(默认):不保证等待时间最长的线程会最先获得许可。

  • 公平性:保证等待时间最长的线程会最先获得许可。

可以通过构造函数参数来设置信号量的公平性。

5.注意事项:

  • 避免永久阻塞:如果所有线程都释放了许可,但没有线程尝试获取许可,信号量的计数器将不会改变,导致等待的线程永久阻塞。

  • 避免资源泄露:确保线程在访问完资源后释放许可,否则可能导致其他线程永久等待。

  • 使用 try-finally 结构:在获取许可后,应使用 try-finally 结构确保许可最终被释放

Semaphore 是一个强大的同步工具,可以用于控制对资源的访问,但需要谨慎使用,以避免潜在的死锁和资源泄露问题。 

 4.CountDownLatch

CountDownLatch 是 Java java.util.concurrent 包中的一个同步辅助类,它允许一个或多个线程等待一组其他线程完成操作CountDownLatch 通过一个计数器来实现这一功能,该计数器的初始值在创建 CountDownLatch 时设定,每当一个等待的操作完成时,计数器的值就会递减,当计数器的值递减到0时,所有等待在这个 CountDownLatch 上的线程就会被唤醒

1.CountDownLatch 的主要特点:

  1. 初始化计数器创建 CountDownLatch 时需要指定一个整数,这个整数作为内部计数器的初始值。

  2. 等待操作:调用 CountDownLatchawait() 方法的线程将会阻塞,直到 CountDownLatch 的计数器达到0。

  3. 计数递减:每当一个需要等待的操作完成时,调用 CountDownLatchcountDown() 方法,这会将计数器的值递减。

  4. 一次性CountDownLatch一次性的,计数器一旦达到0,就不能再次重置和使用。

  5. 不可中断await() 方法的等待是不可中断的,除非线程被中断时它恰好在等待 CountDownLatch

2.CountDownLatch 的基本使用: 

import java.util.concurrent.CountDownLatch;

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

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " started.");
                // 模拟工作
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println(Thread.currentThread().getName() + " finished work.");
                latch.countDown(); // 通知工作完成
            }).start();
        }

        latch.await(); // 等待所有线程完成
        System.out.println("All threads have completed their work.");
    }
}

在这个例子中,主线程创建了一个 CountDownLatch,其初始计数设置为3,表示主线程将等待3个工作线程完成它们的任务。每个工作线程在完成其工作后调用 countDown() 方法递减计数器。主线程调用 await() 方法阻塞,直到所有工作线程都完成了它们的任务并且 CountDownLatch 的计数器达到0,此时主线程继续执行,打印所有线程已完成工作的提示信息。 

3.注意事项:

  • 确保递减:在使用 CountDownLatch 时,确保每个对应的 countDown() 调用与初始化的计数器值相匹配,否则 CountDownLatch 可能不会按预期工作。

  • 一次性使用:由于 CountDownLatch 不能重置,一旦使用完毕,如果需要类似的同步机制,必须创建一个新的 CountDownLatch 实例。

  • 避免无限等待:如果忘记了调用 countDown() 或者计数器的初始值设置错误,await() 方法调用的线程可能会无限期地等待

  • 线程中断:虽然 await() 方法的等待不可中断,但可以在等待期间检查中断状态,并采取适当的中断处理措施。

CountDownLatch 是实现线程间协调的有用工具,特别是在需要等待多个线程或事件完成特定任务时。

4.运用场景(重点)

  1. 初始化等待:在应用程序启动时,主线程可能需要等待多个初始化操作(如数据库连接初始化、资源加载等)完成。此时,可以使用 CountDownLatch 来确保这些初始化操作在主线程继续之前被执行完毕。

  2. 并发任务执行当需要执行多个并发任务,并且需要在所有任务完成后进行汇总或进一步处理时,可以使用 CountDownLatch。每个任务在执行完毕后调用 countDown() 方法,而主线程则在所有任务启动后调用 await() 方法等待它们全部完成。

  3. 模拟并发:在测试或某些特定场景下,可能需要模拟多个线程同时开始执行的操作。可以通过创建一个 CountDownLatch 并初始化为 1,然后在每个线程中调用 await() 方法,主线程调用 countDown() 后所有等待的线程将被唤醒。

  4. 多线程数据汇总在多线程处理数据后,需要在某个线程中汇总所有线程的处理结果。此时,可以为每个处理线程设置一个 countDown() 操作,汇总线程使用 await() 等待所有计数减完。

  5. 资源依赖等待在一些复杂的业务逻辑中,后续操作可能依赖于多个前置操作的结果。这时,可以使用 CountDownLatch 来确保所有前置操作完成后才能执行后续操作

  6. 死锁检测:在并发编程中,死锁是一个常见问题。使用 CountDownLatch 可以模拟多个线程访问共享资源的场景尝试产生死锁,并通过测试来检测和解决死锁问题

  7. 控制并发线程数:在需要限制同时执行的线程数量时,可以使用 CountDownLatch 来控制。例如,只有当一定数量的线程完成它们的任务后,新的线程才会被允许开始执行

5.FutureTask 和 Callable

FutureTask Callable 都是 Java java.util.concurrent 包中用于异步执行任务和获取结果的类。它们允许你将任务提交到后台线程执行,并且可以在任务完成时获取结果

1.Callable 接口:

Callable 是一个接口,它比 Runnable 接口更灵活,因为它可以返回结果并抛出异常

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Runnable 接口相比,Callablecall() 方法可以返回一个值,并且可以抛出任何类型的异常。

2.FutureTask 类:

FutureTask 是一个实现了 RunnableFuture 接口的类,它将任务封装起来,允许你启动任务,并在任务完成时获取结果。

public class FutureTask<V> implements RunnableFuture<V>, java.io.Serializable {
    private final Callable<V> callable;
    // ... 其他字段和方法
}

FutureTask 可以与 Callable 对象一起使用,也可以与实现了 Runnable 接口的对象一起使用,只需将 Runnable 对象包装在 Callable 中即可。

3.使用示例:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;

public class FutureTaskExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        Callable<String> callable = () -> {
            // 模拟长时间运行的任务
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return "Task completed";
        };

        FutureTask<String> futureTask = new FutureTask<>(callable);
        executorService.submit(futureTask);

        try {
            // 异步等待任务完成并获取结果
            String result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }
}

在这个示例中,我们创建了一个 Callable 任务,它简单地休眠2秒,然后返回一个字符串。然后,我们创建了一个 FutureTask 实例,将其提交到线程池中执行。通过调用futureTask.get()我们可以异步等待任务完成并获取结果。

主要特点:

  • 异步执行FutureTask 允许你在后台线程中异步执行任务。

  • 获取结果:通过 FutureTask.get() 方法,你可以获取 Callable 任务的返回值

  • 异常处理FutureTask.get() 会抛出 ExecutionException,如果 Callablecall() 方法抛出了异常,这个异常会被包装在 ExecutionException 中。

  • 任务取消FutureTask.cancel(boolean mayInterruptIfRunning) 方法允许你取消任务。如果参数 mayInterruptIfRunning true,并且任务正在运行,那么线程的中断状态将被设置。

  • 运行状态FutureTask 提供了方法来检查任务是否已完成、取消或未启动。

注意事项:

  • 异常处理:确保正确处理 FutureTask.get() 方法可能抛出的 InterruptedExceptionExecutionException

  • 任务取消:即使任务被取消,call() 方法仍然可能被执行,具体取决于任务的实现和执行环境。

  • 线程中断:如果任务在等待 I/O 操作或其他阻塞操作时被取消,可能需要额外的逻辑来处理中断。

FutureTaskCallable 提供了一种强大的机制来执行异步任务并处理结果,它们在需要执行长时间运行的任务或并行处理多个任务时非常有用。

6.CyclicBarrier

CyclicBarrier 是 Java java.util.concurrent 包中的一个同步辅助类,它允许一组线程互相等待,直到所有线程都达到了某个公共屏障点(barrier point)时,这些线程才会继续执行。这个类对多线程之间的协调非常有用,特别是当一组线程需要等待其他线程完成操作时

1.CyclicBarrier 的主要特点:

  1. 重复使用:与 CountDownLatch 不同,CyclicBarrier 在释放等待的线程后可以重置,因此可以重复使用

  2. 屏障作用CyclicBarrier 可以指定一个 Runnable 命令,当所有线程都到达屏障时,该命令会被执行一次,然后屏障重置,等待下一次线程集合。

  3. 等待超时:线程在等待 CyclicBarrier 时可以指定超时时间,如果在超时时间内其他线程没有到达屏障,等待的线程会被唤醒。

  4. 中断等待:如果等待 CyclicBarrier 的线程被中断,那么这个线程会抛出 InterruptedException,并且屏障将被重置。

2.CyclicBarrier 的基本使用:

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

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int workerCount = 3;
        final CyclicBarrier barrier = new CyclicBarrier(workerCount, 
            () -> System.out.println("All workers have completed their task."));

        for (int i = 0; i < workerCount; i++) {
            new Thread(() -> {
                System.out.println("Worker " + Thread.currentThread().getName() + " started.");
                doWork();
                try {
                    barrier.await(); // 等待其他工人
                } catch (InterruptedException | BrokenBarrierException e) {
                    Thread.currentThread().interrupt();
                }
            }).start();
        }
    }

    private static void doWork() {
        try {
            // 模拟工作
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

 在这个例子中,我们创建了一个 CyclicBarrier,它等待3个工作线程完成工作。每个线程在开始工作后调用 barrier.await() 方法,等待其他线程完成工作。当所有线程都到达屏障时,提供的 Runnable 命令被执行,屏障重置,所有线程继续执行。

3.注意事项:

  • 屏障破坏:如果在等待期间线程中断或超时,屏障将被破坏,并且 BrokenBarrierException 将被抛出。

  • 资源清理:如果 CyclicBarrier 被破坏,应该避免再次使用它,或者在捕获 BrokenBarrierException重置它。

  • 公平性CyclicBarrier 可以设置为公平或非公平模式,公平模式下将优先让等待时间最长的线程获取锁。

  • 超时:可以为 await() 方法设置超时时间,如果在超时时间内所有线程没有都到达屏障,TimeoutException 将被抛出。

CyclicBarrier 协调多个线程执行的有用工具,特别是在需要周期性地让一组线程相互等待的场景中。通过合理使用 CyclicBarrier,可以简化多线程程序的逻辑,提高程序的可读性和健壮性。

三.ThreadLocal的底层实现

加上这一个的底层实现,主要是因为博主再看到一个面经时,看到了这一个问题所以在这里记录一下

 ThreadLocal 是 Java 提供的一个线程局部变量类,它允许线程独立持有一个共享对象的局部副本。每个线程的 ThreadLocal 实例会持有一个线程局部变量的副本,这个副本仅对该线程可见,从而避免了线程间共享状态时的同步问题

1.ThreadLocal 的底层实现原理:

  1. 线程局部变量存储:每个线程 Thread 对象都有一个 ThreadLocalMap 类型的内部类,用于存储与该线程关联的 ThreadLocal 对象及其值

  2. 键值对存储ThreadLocalMap 是一个简单的键值对集合,其中键是 ThreadLocal 实例本身(通过 ThreadLocal ThreadLocalMap 内部类实现的弱引用),值是线程局部变量的副本

  3. 弱引用键ThreadLocalMap 中的键是 ThreadLocal 对象的弱引用,这意味着如果没有任何强引用指向 ThreadLocal 实例,该 ThreadLocal 实例将被垃圾回收,但它关联的 ThreadLocalMap 条目不会自动清除,可能导致内存泄漏

  4. 访问局部变量:当线程访问 ThreadLocal 提供的变量时,实际上是通过查询其内部的 ThreadLocalMap 来获取与当前 ThreadLocal 关联的值。

  5. 初始值获取:使用 ThreadLocal get() 方法时,如果没有为当前线程初始化过值ThreadLocal 会调用其 initialValue() 方法来提供一个初始值。

  6. 设置局部变量:使用 ThreadLocal set(T value) 方法时,会将当前 ThreadLocal 对象作为键,传入的值作为值,存入当前线程的 ThreadLocalMap 中。

  7. 移除局部变量ThreadLocalremove() 方法可以移除当前线程的 ThreadLocalMap 中与当前 ThreadLocal 对象关联的条目,有助于避免内存泄漏

2.示例代码: 

public class ThreadLocalExample {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0; // 提供一个初始值
        }
    };

    public static void main(String[] args) {
        new Thread(() -> {
            threadLocal.set(1); // 为当前线程设置值
            System.out.println(threadLocal.get()); // 输出 1
        }).start();

        new Thread(() -> {
            threadLocal.set(2);
            System.out.println(threadLocal.get()); // 输出 2
        }).start();
    }
}

在这个例子中,每个线程通过 set() 方法为 ThreadLocal 设置了一个值,并通过 get() 方法获取了属于自己的局部变量值。由于 ThreadLocal每个线程都有变量副本,所以两个线程的输出值是独立的。 

3.注意事项:

  • 内存泄漏:由于 ThreadLocalMap 的键是 ThreadLocal 实例的弱引用,如果没有外部强引用指向 ThreadLocal 对象,当 ThreadLocal 对象被垃圾回收后,ThreadLocalMap 中的条目不会自动清除,这可能导致内存泄漏。

  • 线程结束时的清理:线程结束后,其关联的 ThreadLocalMap 应该被清理,但实际上并不会自动清理。因此,应当在线程结束时或不再需要时使用 remove() 方法手动清理

  • 初始值提供ThreadLocal 提供了 initialValue() 方法,允许子类重写此方法来提供一个初始值。

通过理解 ThreadLocal 的底层实现,可以更好地利用这一特性来处理线程局部变量,同时避免潜在的内存泄漏问题。

感谢你的阅读,祝你一天愉快


网站公告

今日签到

点亮在社区的每一天
去签到