面试问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中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

一篇文章彻底搞懂macOS如何决定java环境

《一篇文章彻底搞懂macOS如何决定java环境》MacOS作为一个功能强大的操作系统,为开发者提供了丰富的开发工具和框架,下面:本文主要介绍macOS如何决定java环境的相关资料,文中通过代码... 目录方法一:使用 which命令方法二:使用 Java_home工具(Apple 官方推荐)那问题来了,

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

Java中的.close()举例详解

《Java中的.close()举例详解》.close()方法只适用于通过window.open()打开的弹出窗口,对于浏览器的主窗口,如果没有得到用户允许是不能关闭的,:本文主要介绍Java中的.... 目录当你遇到以下三种情况时,一定要记得使用 .close():用法作用举例如何判断代码中的 input

Spring Gateway动态路由实现方案

《SpringGateway动态路由实现方案》本文主要介绍了SpringGateway动态路由实现方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随... 目录前沿何为路由RouteDefinitionRouteLocator工作流程动态路由实现尾巴前沿S