面试问JUC(java.util.concurrent)的常见类你能答出来几句?

2023-10-20 03:28

本文主要是介绍面试问JUC(java.util.concurrent)的常见类你能答出来几句?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 1.Callable接口
  • 2. ReentrantLock
  • 3. 原子类(java.util.concurrent.atomic)
  • 4. 线程池
  • 5. 信号量 Semaphore
  • 6. CountDownLatch

1.Callable接口

Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算结果.通过下面两个代码实例,可以清晰的看到Callable的优势:

现在要求创建线程计算 1 + 2 + 3 + … + 1000的结果。

在没有Callable的情况下,我们想要在直接通过一个线程来获取到计算结果是不可能的,因为这种情况下,线程的执行不返回任何结果。Runnable接口的run方法定义了线程的执行逻辑,但该方法没有返回值。此时想要实现题目要求,就必须再线程中对一个全局变量进行修改,并且配合线程通信方法来实现:

public class Demo1 {static class Result {public int sum = 0;public Object lock = new Object();}public static void main(String[] args) throws InterruptedException {Result result = new Result();Thread t = new Thread() {@Overridepublic void run() {int sum = 0;for (int i = 1; i <= 1000; i++) {sum += i;}synchronized (result.lock) {result.sum = sum;result.lock.notify();}}};t.start();synchronized (result.lock) {while (result.sum == 0) {result.lock.wait();}System.out.println(result.sum);}}
}

可以看到, 上述代码需要一个辅助类 Result, 还需要使用一系列的加锁和 wait notify 操作, 代码复杂, 容易出错.

但是当有了Callable,就可以通过以下步骤直接在线程内计算然后返回一个值的方式来实现题目要求:

  • 创建一个类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
  • 重写 Callable的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
  • 把 callable 实例使用 FutureTask 包装一下.
  • 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的call方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
  • 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
	static class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {int result = 0;for (int i = 1; i <= 1000; i++) {result += i;}return result;}}public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> myCallable = new MyCallable();FutureTask<Integer> futureTask = new FutureTask<>(myCallable);Thread t = new Thread(futureTask);t.start();int result = futureTask.get();System.out.println(result);}

理解Callable

1.Callable 和 Runnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务。

2.Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.

3.FutureTask 就可以负责这个等待结果出来的工作。future部分源码:

	private volatile int state;private static final int NEW          = 0;private static final int COMPLETING   = 1;private static final int NORMAL       = 2;private static final int EXCEPTIONAL  = 3;private static final int CANCELLED    = 4;private static final int INTERRUPTING = 5;private static final int INTERRUPTED  = 6;private Object outcome; // non-volatile, protected by state reads/writes/** The underlying callable; nulled out after running */private Callable<V> callable;public FutureTask(Callable<V> callable) {if (callable == null)throw new NullPointerException();this.callable = callable;this.state = NEW;       // ensure visibility of callable}public V get() throws InterruptedException, ExecutionException {int s = state;if (s <= COMPLETING)s = awaitDone(false, 0L);return report(s);}private V report(int s) throws ExecutionException {Object x = outcome;if (s == NORMAL)return (V)x;if (s >= CANCELLED)throw new CancellationException();throw new ExecutionException((Throwable)x);}

2. ReentrantLock

ReentrantLock可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.

ReentrantLock 的用法:

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
  • unlock(): 解锁
ReentrantLock lock = new ReentrantLock();
-----------------------------------------
lock.lock();
try {
// working
} finally {
lock.unlock()
}

ReentrantLock 和 synchronized 的区别:

  1. synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).
  2. synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,但是也容易遗漏 unlock.
  3. synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
  4. synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.
ReentrantLock reentrantLock = new ReentrantLock(true);
// ReentrantLock 的构造方法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
  1. 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
class SharedResource {private final Lock lock = new ReentrantLock();private final Condition condition = lock.newCondition();private int value;public void produce(int newValue) {lock.lock();try {value = newValue;System.out.println("Producing: " + value);condition.signal(); // 唤醒等待的线程} finally {lock.unlock();}}public int consume() {lock.lock();try {while (value == 0) {try {condition.await(); // 等待,直到有数据被生产} catch (InterruptedException e) {Thread.currentThread().interrupt();}}int consumedValue = value;value = 0;System.out.println("Consuming: " + consumedValue);return consumedValue;} finally {lock.unlock();}}
}public class Demo2 {public static void main(String[] args) {SharedResource sharedResource = new SharedResource();// 生产者线程Thread producerThread = new Thread(() -> {sharedResource.produce(42);});// 消费者线程Thread consumerThread = new Thread(() -> {int value = sharedResource.consume();});producerThread.start();consumerThread.start();}}

3. 原子类(java.util.concurrent.atomic)

原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个

AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicLong
AtomicReference
AtomicStampedReference

这些原子类是非常有用的,因为它们可以确保在多线程环境中进行原子操作,避免了竞态条件和数据竞争。这些操作是线程安全的,不需要显式的同步措施。例如,你可以使用AtomicInteger来实现计数器,多个线程可以同时递增计数器的值而不会发生竞态条件。常用方法有:

addAndGet(int delta); i += delta;
decrementAndGet(); --i;
getAndDecrement(); i–;
incrementAndGet(); ++i;
getAndIncrement(); i++;

以下是一个示例,演示了如何使用AtomicInteger:

import java.util.concurrent.atomic.AtomicInteger;public class AtomicExample {public static void main(String[] args) {AtomicInteger counter = new AtomicInteger(0);// 多个线程同时递增计数器的值Runnable task = () -> {for (int i = 0; i < 1000; i++) {counter.incrementAndGet();//++i}};Thread thread1 = new Thread(task);Thread thread2 = new Thread(task);//同时启动两个线程对原子类的实力进行自增thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Counter: " + counter.get()); // 应该输出 2000}
}

4. 线程池

线程池在前面的博文有详细的讲解:线程池博文

5. 信号量 Semaphore

java.util.concurrent.Semaphore 是 Java 并发编程中的一个重要工具,用于控制并发访问资源的数量。它提供了一种计数信号机制,允许你控制同时访问某个共享资源的线程数。

Semaphore 维护了一个计数器,该计数器表示允许同时访问资源的线程数量。当线程要访问资源时,它必须先获取信号量,如果信号量计数大于零,则线程可以访问资源,同时信号量计数减一;如果计数为零则线程必须等待,直到有其他线程释放资源,增加信号量计数。

Semaphore 的核心方法包括:

acquire(): 当线程想要获得一个信号量时,可以调用这个方法。如果信号量计数大于零,线程将成功获得信号量,计数减一;否则,线程将阻塞,直到有信号量可用。((这个称为信号量的 P 操作))release(): 当线程使用完资源后,应该调用 release() 方法来释放信号量,这将使信号量计数加一,表示资源可供其他线程使用。((这个称为信号量的 V操作))

6. CountDownLatch

CountDownLatch 的主要思想是,一个线程(通常是主线程)等待其他一组线程执行完特定任务,然后再继续执行。它通过一个计数器来实现,该计数器初始化为一个正整数,每个线程在完成任务时会将计数器减一,当计数器的值达到零时,等待的线程将被唤醒继续执行。

举个例子

同时等待 N 个任务执行结束.—>好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。

理解:

  1. 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
  2. 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
  3. 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
public class Demo3 {public static void main(String[] args) {int taskCount = 10;CountDownLatch latch = new CountDownLatch(taskCount);Runnable task = () -> {// 模拟任务的执行System.out.println("任务正在执行。");// 任务完成后减少计数器latch.countDown();};// 启动 10 个线程执行任务for (int i = 0; i < taskCount; i++) {Thread thread = new Thread(task);thread.start();}try {// 等待所有任务完成latch.await();System.out.println("所有任务已完成。主线程可以继续执行。");} catch (InterruptedException e) {e.printStackTrace();}}
}

这篇关于面试问JUC(java.util.concurrent)的常见类你能答出来几句?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/244325

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2