面试问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编译生成多个.class文件的原理和作用

《Java编译生成多个.class文件的原理和作用》作为一名经验丰富的开发者,在Java项目中执行编译后,可能会发现一个.java源文件有时会产生多个.class文件,从技术实现层面详细剖析这一现象... 目录一、内部类机制与.class文件生成成员内部类(常规内部类)局部内部类(方法内部类)匿名内部类二、

Go标准库常见错误分析和解决办法

《Go标准库常见错误分析和解决办法》Go语言的标准库为开发者提供了丰富且高效的工具,涵盖了从网络编程到文件操作等各个方面,然而,标准库虽好,使用不当却可能适得其反,正所谓工欲善其事,必先利其器,本文将... 目录1. 使用了错误的time.Duration2. time.After导致的内存泄漏3. jsO

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

Springboot @Autowired和@Resource的区别解析

《Springboot@Autowired和@Resource的区别解析》@Resource是JDK提供的注解,只是Spring在实现上提供了这个注解的功能支持,本文给大家介绍Springboot@... 目录【一】定义【1】@Autowired【2】@Resource【二】区别【1】包含的属性不同【2】@

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

Elasticsearch 在 Java 中的使用教程

《Elasticsearch在Java中的使用教程》Elasticsearch是一个分布式搜索和分析引擎,基于ApacheLucene构建,能够实现实时数据的存储、搜索、和分析,它广泛应用于全文... 目录1. Elasticsearch 简介2. 环境准备2.1 安装 Elasticsearch2.2 J

Java中的String.valueOf()和toString()方法区别小结

《Java中的String.valueOf()和toString()方法区别小结》字符串操作是开发者日常编程任务中不可或缺的一部分,转换为字符串是一种常见需求,其中最常见的就是String.value... 目录String.valueOf()方法方法定义方法实现使用示例使用场景toString()方法方法

Java中List的contains()方法的使用小结

《Java中List的contains()方法的使用小结》List的contains()方法用于检查列表中是否包含指定的元素,借助equals()方法进行判断,下面就来介绍Java中List的c... 目录详细展开1. 方法签名2. 工作原理3. 使用示例4. 注意事项总结结论:List 的 contain

Java实现文件图片的预览和下载功能

《Java实现文件图片的预览和下载功能》这篇文章主要为大家详细介绍了如何使用Java实现文件图片的预览和下载功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... Java实现文件(图片)的预览和下载 @ApiOperation("访问文件") @GetMapping("