> 文章列表 > java面试题-JUC工具类

java面试题-JUC工具类

java面试题-JUC工具类

1.什么是CountDownLatch?

CountDownLatch是Java中的一个同步工具类,用于协调多个线程之间的执行顺序。它的工作原理是主线程创建一个CountDownLatch对象,并将计数器初始化为等待的线程数。每个等待的线程在完成自己的任务后,都会调用CountDownLatch的countDown()方法,将计数器减1。当计数器的值变为0时,主线程就可以继续执行下一步操作了。常用于等待一组线程完成后再执行主线程操作。

下面是一个简单的例子,演示了如何使用CountDownLatch实现线程协作:

import java.util.concurrent.CountDownLatch;public class Example {public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(3);// 创建三个工作线程Thread worker1 = new Thread(() -> {System.out.println("Worker 1 is working...");latch.countDown();});Thread worker2 = new Thread(() -> {System.out.println("Worker 2 is working...");latch.countDown();});Thread worker3 = new Thread(() -> {System.out.println("Worker 3 is working...");latch.countDown();});// 启动工作线程worker1.start();worker2.start();worker3.start();// 等待工作线程完成latch.await();// 所有工作线程完成后,主线程继续执行System.out.println("All workers have finished.");}
}

2.CountDownLatch底层实现原理?

CountDownLatch的底层实现原理主要依赖于AQS(AbstractQueuedSynchronizer)。

在CountDownLatch中,AQS维护了一个共享的state变量,表示需要等待的线程数。当一个线程调用CountDownLatch的await()方法时,它会获取AQS的同步状态,并检查state的值是否为0。如果state的值不为0,则当前线程会被挂起,等待其他线程调用countDown()方法将state的值减1。

当一个线程调用CountDownLatch的countDown()方法时,它会通过AQS的acquireShared()方法来获取同步状态,并将state的值减1。如果state的值变为0,则所有被挂起的线程会被唤醒,继续执行。

需要注意的是,AQS中的同步状态是以“共享锁”的方式实现的。在CountDownLatch中,所有等待线程共享同一个锁。当锁的状态被释放时,所有等待线程都可以获得锁并继续执行。

总的来说,CountDownLatch的底层实现依赖于AQS提供的同步机制,通过共享状态和挂起/唤醒等操作来实现线程之间的协作和同步。

3.CountDownLatch一次可以唤醒几个任务?

在CountDownLatch中,每次调用countDown()方法只会将计数器减1,而不会唤醒任何线程。唤醒线程的操作是在计数器的值变为0时自动触发的,会唤醒所有等待的线程。

因此,在CountDownLatch中,一次可以唤醒所有等待的线程,也就是唤醒的数量等于计数器的初始值。例如,如果计数器的初始值为5,那么在计数器的值变为0时,所有等待的5个线程都会被唤醒,继续执行后续操作。

4.CountDownLatch有哪些主要方法?

CountDownLatch是Java中的一个同步工具类,主要包含以下几个方法:

  1. public CountDownLatch(int count):构造一个CountDownLatch对象,指定需要等待的线程数量(计数器初始值)。

  1. public void await() throws InterruptedException:使当前线程等待,直到计数器的值变为0。如果计数器的值已经为0,则立即返回。

  1. public boolean await(long timeout, TimeUnit unit) throws InterruptedException:使当前线程等待,直到计数器的值变为0,或者超时时间到达。如果计数器的值已经为0,则立即返回true。如果超时时间到达仍然没有到达0,返回false。

  1. public void countDown():将计数器的值减1,表示有一个线程完成了任务。

  1. public long getCount():获取当前计数器的值。

5.写道题:实现一个容器,提供两个方法,add,size 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束?

wait()/notify()的写法:

import java.util.ArrayList;
import java.util.List;public class Container {private List<Object> list = new ArrayList<>();public void add(Object obj) {synchronized (list) {list.add(obj);list.notify(); // 唤醒等待线程}}public int size() {synchronized (list) {return list.size();}}
}public class Main {public static void main(String[] args) {Container container = new Container();// 线程1:添加10个元素到容器中new Thread(() -> {for (int i = 0; i < 10; i++) {container.add(new Object());System.out.println("添加了一个元素");try {Thread.sleep(1000); // 等待1秒钟} catch (InterruptedException e) {e.printStackTrace();}}}).start();// 线程2:监控元素的个数,当个数到5个时,给出提示并结束new Thread(() -> {synchronized (container) {while (container.size() != 5) {try {container.wait(); // 等待元素个数变化} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("容器中元素个数为5,线程2结束");container.notify(); // 唤醒线程1,使其结束}}).start();}
}

下面是使用 CountDownLatch 实现的代码:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;public class Container {private List<Object> list = new ArrayList<>();private CountDownLatch latch = new CountDownLatch(1);public void add(Object obj) {list.add(obj);if (list.size() == 5) {latch.countDown(); // 计数器减一}}public int size() {return list.size();}public CountDownLatch getLatch() {return latch;}
}public class Main {public static void main(String[] args) throws InterruptedException {Container container = new Container();// 线程1:添加10个元素到容器中new Thread(() -> {for (int i = 0; i < 10; i++) {container.add(new Object());System.out.println("添加了一个元素");try {Thread.sleep(1000); // 等待1秒钟} catch (InterruptedException e) {e.printStackTrace();}}}).start();// 线程2:监控元素的个数,当个数到5个时,给出提示并结束new Thread(() -> {try {container.getLatch().await(); // 等待计数器归零} catch (InterruptedException e) {e.printStackTrace();}System.out.println("容器中元素个数为5,线程2结束");}).start();}
}

add() 方法中,当添加一个元素后,如果元素个数达到5个,则通过 CountDownLatchcountDown() 方法将计数器减一。

在监控线程中,使用 CountDownLatchawait() 方法等待计数器归零,当元素个数达到5个时,计数器减一后变为0,此时线程2就会被唤醒。

相对于使用 wait()notify() 方法,使用 CountDownLatch 更加简单,不需要手动处理锁和条件变量,同时也可以避免虚假唤醒问题。

6.什么是CyclicBarrier?

CyclicBarrier 是一个同步工具类,它可以让一组线程在到达某个屏障点时被阻塞,直到所有线程都到达该屏障点,然后继续执行。

CountDownLatch 不同的是,CyclicBarrier 可以重复使用。当所有线程都到达屏障点后,屏障就会被打开,所有线程都可以继续执行,而 CyclicBarrier 的计数器会被重置,可以被再次使用。

CyclicBarrier 的主要方法是 await(),每个线程调用该方法后会被阻塞,直到所有线程都到达屏障点。当最后一个线程到达屏障点时,它会执行一个可选的回调函数,然后所有线程都会被释放。当然,如果在所有线程到达屏障点之前,任何一个线程被中断或者超时,那么所有线程都会被释放,并抛出 BrokenBarrierException 异常。

CyclicBarrier 还有一个构造函数,可以指定一个回调函数,在所有线程到达屏障点时被执行。例如:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;public class CyclicBarrierExample {public static void main(String[] args) throws InterruptedException {int n = 4;CyclicBarrier barrier = new CyclicBarrier(n, () -> {System.out.println("所有线程都到达屏障点,执行回调函数");});for (int i = 0; i < n; i++) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 到达屏障点");try {barrier.await();} catch (InterruptedException | BrokenBarrierException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 继续执行");}).start();}}
}

在这个示例中,有4个线程参与同步,它们会分别执行一个任务,然后在某个点上等待其他线程。这个点就是 CyclicBarrier 的屏障点,所有线程到达屏障点后,就会执行回调函数,然后所有线程都可以继续执行自己的任务。

在每个线程中,首先输出自己到达屏障点的信息,然后调用 CyclicBarrierawait() 方法等待其他线程。当所有线程都到达屏障点后,会执行回调函数,然后所有线程都会被释放,输出继续执行的信息。

在这个示例中,输出的结果可能是这样的:

Thread-0 到达屏障点
Thread-3 到达屏障点
Thread-2 到达屏障点
Thread-1 到达屏障点
所有线程都到达屏障点,执行回调函数
Thread-2 继续执行
Thread-0 继续执行
Thread-1 继续执行
Thread-3 继续执行

可以看到,所有线程都先到达屏障点,然后执行回调函数,最后继续执行。注意到,每个线程在到达屏障点后,会一直等待,直到所有线程都到达,这就是 CyclicBarrier 的特点之一。

7.CountDownLatch和CyclicBarrier对比?

  1. 计数器的初始化方式不同:CountDownLatch 的计数器是在创建对象时初始化的,而 CyclicBarrier 的计数器是在调用 reset() 方法时初始化的。

  1. 计数器的使用方式不同:CountDownLatch 的计数器只能使用一次,计数器减到 0 后就不能再使用了;而 CyclicBarrier 的计数器可以重复使用。

  1. 阻塞的方式不同:CountDownLatch 的线程等待是一直阻塞的,直到计数器为 0;而 CyclicBarrier 的线程等待可以是阻塞或者超时等待,具体可以通过 await() 方法的参数来指定。

  1. 回调函数的使用方式不同:CountDownLatch 没有回调函数的概念,而 CyclicBarrier 可以在所有线程到达屏障点时执行一个回调函数。

  1. 应用场景不同:CountDownLatch 适用于一个线程需要等待多个线程完成某些任务的情况,而 CyclicBarrier 适用于多个线程之间需要相互等待,直到所有线程都到达某个屏障点后再一起继续执行的情况。

8.什么是Semaphore?

Semaphore 是 Java 中的一个同步工具类,它可以用来控制同时访问某个资源的线程数量。它维护了一个计数器,该计数器表示当前可用的许可证的数量。当线程要访问某个资源时,需要首先获取一个许可证,如果许可证数量已经达到了限制,那么线程就需要等待,直到有其他线程释放了许可证。

9.Semaphore内部原理?

Semaphore总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。

java面试题-JUC工具类

Semaphore 的底层原理是基于 AQS 的共享模式同步器实现的,通过计数器和等待队列等机制,实现了线程之间的同步和互斥访问。

10.Semaphore常用方法有哪些? 如何实现线程同步和互斥的?

  1. acquire():尝试获取一个许可证,如果没有可用许可证,则阻塞当前线程。

  1. acquire(int permits):尝试获取指定数量的许可证,如果没有足够的可用许可证,则阻塞当前线程。

  1. release():释放一个许可证,增加可用许可证的数量,并唤醒等待队列中的一个线程。

  1. release(int permits):释放指定数量的许可证,增加可用许可证的数量,并按照等待队列中的顺序唤醒等待线程。

  1. availablePermits():返回当前可用的许可证数量。

Semaphore 可以实现线程的同步和互斥访问。当我们在创建 Semaphore 对象时,需要指定许可证的数量,也就是最多允许多少个线程同时执行关键代码段。当某个线程需要执行关键代码段时,它需要先通过 acquire() 方法获取许可证,如果没有可用的许可证,则会被阻塞,直到有其他线程释放许可证。当线程执行完关键代码段时,需要通过 release() 方法释放许可证,使得其他线程可以继续执行关键代码段。

通过限制可用许可证的数量,Semaphore 可以实现线程的互斥访问,即同一时间只有一个线程可以执行关键代码段,其他线程需要等待许可证释放后才能继续执行。同时,通过调整许可证的数量,我们也可以实现线程的同步,即在某个时间点只有指定数量的线程可以执行关键代码段,其他线程需要等待。

11.单独使用Semaphore是不会使用到AQS的条件队列?

不同于CyclicBarrier和ReentrantLock,单独使用Semaphore是不会使用到AQS的条件队列的,其实,只有进行await操作才会进入条件队列,其他的都是在同步队列中,只是当前线程会被park。

12.Semaphore初始化有10个令牌,11个线程同时各调用1次acquire方法,会发生什么?

具体来说,最先执行 acquire() 方法的10个线程,都可以成功获取到一个令牌,然后继续执行下面的逻辑。而第11个线程在调用 acquire() 方法时,会发现当前可用的令牌数量为0,于是会被阻塞,并被加入到 Semaphore 的同步队列中等待。

随后,如果有其他线程调用 release() 方法,释放了一个令牌,那么 Semaphore 就会将同步队列中的一个线程唤醒,并将其移动到同步队列的头部,然后该线程就可以获取到令牌并继续执行了。如果没有其他线程释放令牌,则第11个线程会一直等待,直到有其他线程释放令牌或者线程被中断。

13.Semaphore初始化有10个令牌,一个线程重复调用11次acquire方法,会发生什么?

这种情况和上面类似,当线程第11次调用 acquire() 方法时,会发现当前可用的令牌数量为0,于是会被阻塞,并被加入到 Semaphore 的同步队列中等待其他线程调用 release() 方法释放令牌。并不会有类似锁重入的概念

14.Semaphore初始化有1个令牌,1个线程调用一次acquire方法,然后调用两次release方法,之后另外一个线程调用acquire(2)方法,此线程能够获取到足够的令牌并继续运行吗?

能,原因是release方法会添加令牌,并不会以初始化的大小为准。

15.Semaphore初始化有2个令牌,一个线程调用1次release方法,然后一次性获取3个令牌,会获取到吗?

能,原因是release会添加令牌,并不会以初始化的大小为准。Semaphore中release方法的调用并没有限制要在acquire后调用。

16.Phaser主要用来解决什么问题?

Phaser 主要用来解决多线程任务分阶段执行的问题。在多线程并发执行任务的时候,有时候需要将任务分成多个阶段,每个线程只能在当前阶段完成任务后才能继续执行下一阶段的任务,而且所有线程都必须等待所有线程完成当前阶段的任务后才能继续执行下一阶段的任务。

Phaser 提供了类似于 CyclicBarrier 和 CountDownLatch 的功能,但是 Phaser 更加灵活,可以动态地注册和注销参与者,而且可以在每个阶段结束后执行特定的操作。此外,Phaser 还可以用于任务分治的场景,可以将多个子任务分别执行,最后将它们的结果合并成一个结果。

17.Phaser与CyclicBarrier和CountDownLatch的区别是什么?

Phaser、CyclicBarrier 和 CountDownLatch 都是用于多线程同步的工具,它们的作用都是等待一组线程执行完毕后再继续执行。

下面是它们的区别:

  1. 功能不同:Phaser 可以支持分阶段执行任务,并且可以动态注册和注销参与者,每个阶段结束后可以执行特定的操作。CyclicBarrier 和 CountDownLatch 只能等待所有线程都到达一个点后再继续执行。

  1. 参与者动态性:Phaser 可以动态地添加和删除参与者,而 CyclicBarrier 和 CountDownLatch 的参与者是固定的,一旦设置好参与者数量就不能再进行修改。

  1. 重复使用性:Phaser 可以重复使用,而 CyclicBarrier 和 CountDownLatch 在一次使用之后就不能再次使用。

  1. 代码复杂度:使用 Phaser 可以使代码更简洁,因为它可以支持多阶段任务,而 CyclicBarrier 和 CountDownLatch 只能支持单一任务。

总之,Phaser 是一种更加灵活、功能更强大的多线程同步工具,但它相对于 CyclicBarrier 和 CountDownLatch 也更加复杂一些,需要在具体应用场景中权衡使用。

18.Phaser运行机制是什么样的?

java面试题-JUC工具类

Phaser内部使用了一个数字表示当前阶段数(phase),每个线程在执行任务时需要到达指定的阶段才能继续往下执行。Phaser的运行机制主要有以下几个步骤:

  1. 初始化:在创建Phaser对象时,需要指定参与同步的线程数。当所有线程都到达同步点时,Phaser会进入下一阶段。初始阶段数默认为0。

  1. 注册:线程需要先注册才能参与同步。可以通过Phaser的register()方法来实现。

  1. 到达同步点:在执行任务前,线程需要到达同步点。可以通过Phaser的arrive()或arriveAndAwaitAdvance()方法来实现。其中,arrive()表示当前线程已经到达同步点,继续执行下一步任务,而arriveAndAwaitAdvance()表示当前线程已经到达同步点,等待其他线程也到达同步点后,一起继续执行下一步任务。

  1. 阶段跨越:当所有线程都到达同步点时,Phaser会进入下一阶段,阶段数加1。可以通过Phaser的arriveAndDeregister()方法来表示当前线程已经完成当前阶段的任务,并注销不再参与下一阶段的同步。

  1. 动态注册和注销:在执行过程中,线程可以动态地注册和注销。可以通过Phaser的register()和deregister()方法来实现。

  1. 终止:Phaser可以设置终止状态,表示同步已经完成,不再等待新的线程注册。可以通过Phaser的forceTermination()方法来强制终止同步。

19.给一个Phaser使用的示例?

模拟了100米赛跑,10名选手,只等裁判一声令下。当所有人都到达终点时,比赛结束。

public class Match {// 模拟了100米赛跑,10名选手,只等裁判一声令下。当所有人都到达终点时,比赛结束。public static void main(String[] args) throws InterruptedException {final Phaser phaser=new Phaser(1) ;// 十名选手for (int index = 0; index < 10; index++) {phaser.register();new Thread(new player(phaser),"player"+index).start();}System.out.println("Game Start");//注销当前线程,比赛开始phaser.arriveAndDeregister();//是否非终止态一直等待while(!phaser.isTerminated()){}System.out.println("Game Over");}
}
class player implements Runnable{private  final Phaser phaser ;player(Phaser phaser){this.phaser=phaser;}@Overridepublic void run() {try {// 第一阶段——等待创建好所有线程再开始phaser.arriveAndAwaitAdvance();// 第二阶段——等待所有选手准备好再开始Thread.sleep((long) (Math.random() * 10000));System.out.println(Thread.currentThread().getName() + " ready");phaser.arriveAndAwaitAdvance();// 第三阶段——等待所有选手准备好到达,到达后,该线程从phaser中注销,不在进行下面的阶段。Thread.sleep((long) (Math.random() * 10000));System.out.println(Thread.currentThread().getName() + " arrived");phaser.arriveAndDeregister();} catch (InterruptedException e) {e.printStackTrace();}}
}

20.Exchanger主要解决什么问题?

Exchanger 主要用于在两个线程之间进行数据交换,即一个线程通过 exchange() 方法向另一个线程交换数据,当两个线程都调用了 exchange() 方法时,数据才会真正地交换。

通常情况下,两个线程通过 Exchanger 交换数据可以解决以下问题:

  1. 线程之间需要直接交换数据,但是不能使用共享变量或者其他线程通信机制。

  1. 两个线程需要相互通信协作,但是不能直接互相访问。

例如,在一个生产者-消费者的场景中,生产者线程需要将生产的数据传递给消费者线程进行处理,而且要求消费者线程处理完数据之后需要将处理结果返回给生产者线程,这时可以使用 Exchanger 实现两个线程之间的数据交换。

21.对比SynchronousQueue,为什么说Exchanger可被视为 SynchronousQueue 的双向形式?

Exchanger 和 SynchronousQueue 都是用来实现两个线程之间的数据交换,但两者的实现方式略有不同。

SynchronousQueue 是一种容量为 0 的阻塞队列,生产者线程在队列中插入元素时会被阻塞直到有消费者线程从队列中取走该元素。反之,消费者线程在队列中取元素时也会被阻塞直到有生产者线程向队列中插入元素。

Exchanger 也是用来实现两个线程之间的数据交换,但它是一种双向的同步机制,可以让两个线程互相交换数据。每个线程在调用 Exchanger 的 exchange 方法时会被阻塞,直到另一个线程也调用了 exchange 方法,然后两个线程就可以交换数据,然后各自继续执行。

可以说 Exchanger 是 SynchronousQueue 的双向形式,因为它们都是实现两个线程之间的数据交换,只是 Exchanger 可以实现双向交换数据,而 SynchronousQueue 只能实现单向交换数据。

22.Exchanger在不同的JDK版本中实现有什么差别?

  • 在JDK5中Exchanger被设计成一个容量为1的容器,存放一个等待线程,直到有另外线程到来就会发生数据交换,然后清空容器,等到下一个到来的线程。

  • 从JDK6开始,Exchanger用了类似ConcurrentMap的分段思想,提供了多个slot,增加了并发执行时的吞吐量

23.Exchanger实现举例

import java.util.concurrent.Exchanger;public class ExchangerExample {public static void main(String[] args) {Exchanger<String> exchanger = new Exchanger<>();new Thread(() -> {String data = "Hello from Thread 1";try {System.out.println("Thread 1 before exchange: " + data);data = exchanger.exchange(data);System.out.println("Thread 1 after exchange: " + data);} catch (InterruptedException e) {e.printStackTrace();}}).start();new Thread(() -> {String data = "Hello from Thread 2";try {System.out.println("Thread 2 before exchange: " + data);data = exchanger.exchange(data);System.out.println("Thread 2 after exchange: " + data);} catch (InterruptedException e) {e.printStackTrace();}}).start();}
}

在这个例子中,我们创建了一个Exchanger对象,并创建了两个线程,每个线程都有一个字符串数据,它们在Exchanger对象上进行交换。当线程1调用exchange()方法时,它会等待线程2到达Exchanger对象上进行交换。线程2调用exchange()方法时也会等待线程1到达Exchanger对象上进行交换。当两个线程都到达时,Exchanger会交换它们所传递的字符串数据,并将交换后的数据返回给它们。这里需要注意的是,exchange()方法会抛出InterruptedException异常,因此需要处理异常情况。

运行上述代码,输出如下:

Thread 1 before exchange: Hello from Thread 1
Thread 2 before exchange: Hello from Thread 2
Thread 1 after exchange: Hello from Thread 2
Thread 2 after exchange: Hello from Thread 1

可以看到,线程1和线程2的字符串数据被成功交换了。

24.什么是ThreadLocal? 用来解决什么问题的?

ThreadLocal是Java中的一个线程局部变量,它提供了一种在多线程环境下,保证变量各自独立互不干扰的方式。每个ThreadLocal对象都维护了一个独立的变量副本,且只能被对应的线程访问和修改,因此可以有效避免多线程并发时的变量访问冲突问题。

ThreadLocal常用于解决多线程环境下的上下文切换问题。在一些场景中,线程之间需要共享一些数据,但是这些数据不适合作为全局变量使用,因为不同线程之间需要访问和修改的数据是不同的。此时就可以使用ThreadLocal来维护每个线程的数据副本,从而避免了线程之间的竞争和同步开销。

另外,ThreadLocal还可以用来实现一些资源的延迟绑定,比如数据库连接、Session等,这些资源在使用时需要绑定到当前线程上,但是在实际调用时可能并没有被初始化,因此可以通过ThreadLocal来实现懒加载和延迟初始化。

25.说说你对ThreadLocal的理解

ThreadLocal是Java中一种线程级别的变量隔离机制,它可以为每个线程存储一份独立的变量副本,保证线程之间互不干扰。通常情况下,ThreadLocal对象被定义为static类型的,以便于在不同的线程中共享访问。

下面是一个简单的示例,使用ThreadLocal为每个线程保存一个计数器,当线程执行完毕后,输出计数器的值:

public class ThreadLocalDemo {private static ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {@Overrideprotected Integer initialValue() {return 0;}};public static void main(String[] args) throws InterruptedException {Runnable task = () -> {int c = counter.get();counter.set(c + 1);System.out.println(Thread.currentThread().getName() + ": " + c);};Thread[] threads = new Thread[5];for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(task);threads[i].start();}for (int i = 0; i < threads.length; i++) {threads[i].join();}}
}

在这个示例中,我们定义了一个静态的ThreadLocal变量counter,并重写了它的initialValue方法,以便初始值为0。在任务中,我们使用counter.get()获取计数器的值,然后使用counter.set()将计数器加1。每个线程都有自己的计数器,互相独立。

运行结果如下:

Thread-0: 0
Thread-1: 0
Thread-2: 0
Thread-3: 0
Thread-4: 0

从结果可以看出,每个线程都有自己的计数器,并且互不干扰。

26.ThreadLocal是如何实现线程隔离的?

一个ThreadLocalMap的映射表来实现线程之间的隔离。ThreadLocalMap是一个由Entry数组构成的哈希表,每个Entry对应一个ThreadLocal对象和对应线程的变量副本。每个线程持有一个ThreadLocalMap的引用,当线程需要访问ThreadLocal变量时,ThreadLocal会根据当前线程获取对应的ThreadLocalMap,并在ThreadLocalMap中查找对应的Entry。

27.为什么ThreadLocal会造成内存泄露? 如何解决

当一个线程结束时,如果它所持有的ThreadLocal变量没有被垃圾回收,那么这个变量的值就会一直存在于内存中,直到应用程序退出或者线程池被销毁。如果这个变量是一个对象,它所持有的引用也会一直存在于内存中,导致内存泄漏问题。

为了解决这个问题,可以采取以下措施:

  1. 及时清理ThreadLocal变量。在使用完ThreadLocal变量后,需要调用其remove()方法或者设置为null,以便让垃圾回收器回收相关的内存空间。

  1. 使用弱引用ThreadLocal变量。可以使用ThreadLocal的静态内部类WeakReference来包装ThreadLocal变量,以便让垃圾回收器可以回收它所引用的对象。

  1. 避免创建过多的ThreadLocal变量。每个ThreadLocal变量都会占用一定的内存空间,因此不应该创建过多的ThreadLocal变量。在设计应用程序时需要考虑到线程池的大小和内存使用情况,避免过度使用ThreadLocal变量。

总之,要解决ThreadLocal造成的内存泄漏问题,需要及时清理ThreadLocal变量,使用弱引用ThreadLocal变量以及避免创建过多的ThreadLocal变量。

28.还有哪些使用ThreadLocal的应用场景?

public class ThreadLocalDemo {private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {@Overrideprotected Integer initialValue() {return 0;}};public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 5; i++) {int num = threadLocal.get();num++;threadLocal.set(num);System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());}}}, "Thread-1");Thread thread2 = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 5; i++) {int num = threadLocal.get();num += 10;threadLocal.set(num);System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());}}}, "Thread-2");thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("Main Thread: " + threadLocal.get());}
}

该示例中定义了一个ThreadLocal对象,用于存储整型变量。在两个线程中,分别对ThreadLocal对象的值进行操作,每个线程中操作5次。最后在主线程中输出ThreadLocal对象的值。

在每个线程中,首先获取ThreadLocal对象的值,然后对其进行加法或者加法赋值操作,最后将结果设置回ThreadLocal对象中。由于每个线程都有自己的ThreadLocal对象,所以它们对ThreadLocal对象的操作不会互相影响。最终输出的结果也能够验证ThreadLocal的线程封闭效果。

Thread-1: 1
Thread-2: 10
Thread-1: 2
Thread-2: 20
Thread-1: 3
Thread-2: 30
Thread-1: 4
Thread-2: 40
Thread-1: 5
Thread-2: 50
Main Thread: 0

阿里巴巴 java 开发手册中推荐的 ThreadLocal 的用法:

import java.text.DateFormat;
import java.text.SimpleDateFormat;public class DateUtils {public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){@Overrideprotected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");}};
}

然后我们再要用到 DateFormat 对象的地方,这样调用:

DateUtils.df.get().format(new Date());