面试题004-Java-Java多线程(下)
这里写目录标题
- 面试题004-Java-Java多线程(下)
- 题目自测
- 题目答案
- 1. synchronized 关键字的作用?
- 2. volatile 关键字的作用?
- 3. synchronized 和 volatile 的区别?
- 4. synchronized 和 ReentrantLock 的区别?
- 5. ThreadLocal有什么用?
- 6. 线程池有什么用?为什么不推荐使用内置线程池?
- 7. 如何自定义线程池?
- 8. Java线程池有哪些参数?阻塞队列有几种?拒绝策略有几种?
- 9. 线程池处理任务的流程了解吗?
- 10. 如何给线程池命名?为什么建议给线程池命名?
- 参考资料
题目自测
- 1. synchronized 关键字的作用?
- 2. volatile 关键字的作用?
- 3. synchronized 和 volatile 的区别?
- 4. synchronized 和 ReentrantLock 的区别?
- 5. ThreadLocal有什么用?
- 6. 线程池有什么用?为什么不推荐使用内置线程池?
- 7. 如何自定义线程池?
- 8. Java线程池有哪些参数?阻塞队列有几种?拒绝策略有几种?
- 9. 线程池处理任务的流程了解吗?
- 10. 如何给线程池命名?为什么建议给线程池命名?
题目答案
1. synchronized 关键字的作用?
答:在Java中synchronized关键字用于实现线程同步,主要解决是多个线程之间访问相同资源不会发生数据不一致的问题。它可以作用于方法或代码块上,以保证同一时间只有一个线程可以执行被同步的方法或代码块。
-
同步实例方法: 表示该方法在同一时间只能由一个线程访问同一个实例。
public class SynchronizedExample {public synchronized void synchronizedMethod() {// 同步代码} }
-
同步静态方法:表示该方法在同一时间只能由一个线程访问同一个类的所有实例。
public class SynchronizedExample {public static synchronized void synchronizedStaticMethod() {// 同步代码} }
-
同步代码块:表示该代码块在同一时间只能由一个线程访问该对象。
public class SynchronizedExample {private final Object lock = new Object();public void synchronizedBlock() {synchronized (this) {// 同步代码}} }
2. volatile 关键字的作用?
答:在Java中volatile关键字用于声明变量的可见性和防止指令重排,从而提供一种轻量级的同步机制。
在可见性方面,用volatile修饰的变量,来确保一个线程对该变量的修改对其他线程立即可见。
在防止指令重排方面,volatile会禁止JVM对变量操作的指令进行重新排序。
3. synchronized 和 volatile 的区别?
答:synchronized和volatile两个关键字都用于实现线程同步的机制。
- volatile是线程同步的轻量级实现,volatile性能比synchronized要好。volatile关键字只能用于变量,而synchronized关键字可以修饰方法和代码块。
- volatile能保证数据的可见性,但不能保证数据的原子性。synchronized两者都能保证。
- volatile主要解决变量在多个线程之间的可见性,而synchronize解决的是多个线程之间访问资源的同步性。
4. synchronized 和 ReentrantLock 的区别?
答:synchronized和ReentrantLock都是Java中用于实现线程同步的机制,并且都是可重入锁。
- synchronized:
- 是Java语言的内置关键字,用于对代码块或方法进行同步。
- 简单易用,直接在方法或代码块上使用即可。
- 由JVM实现,使用方便,但功能较为有限。
- ReentrantLock:
- ReentrantLock是java.util.concurrent.locks包中的类,提供了更灵活和丰富的锁机制(响应中断、尝试获取锁、使用条件变量等)。
- 需要显示的加锁和解锁,通过代码来控制锁的获取和释放。
- 由Java库实现,功能强大,灵活性高。
5. ThreadLocal有什么用?
答:ThreadLocal在Java中用于创建线程局部变量,确保每个变量都有自己的独立副本变量,从而避免多线程共享一个变量带来的线程安全问题,同时提高了并发性能。然而在使用ThreadLocal时需要注意内存泄露问题,由于ThreadLocal变量的生命周期与线程线相同,如果线程池中线程长时间不被销毁,而ThreadLocal变量没有被正确移除,可能会导致内存泄露问题,因此建议在不再使用ThreadLocal变量时显示调用remove()方法。
它的使用场景有用户会话管理、数据库连接管理、事物管理等。
6. 线程池有什么用?为什么不推荐使用内置线程池?
答:Java中的线程池是一种基于池化技术设计用于执行异步任务的框架,它维护了一定数量的线程,避免频繁地创建和销毁线程带来的性能开销和资源浪费。他的主要作用是提高资源复用、提高系统稳定性、便于管理和提供灵活的并发策略。
不推荐使用内置线程池的主要原因是内置线程池等配置选项有限、不能满足所有的应用场景的需求。Java库中提供了几种预定义线程的实现,如Executors类中的newFixedThreadPool、newCacheThreadPool、newSingleThreadExecutor等。
- FixedThreadPool 和 SingleThreadExecutor 使用无界队列(LinkedBlockingQueue),在任务提交速度超过执行速度时,任务队列可能无限增长,导致内存耗尽。
- CacheThreadPool 使用无界线程池,在高并发下可能会创建大量线程、导致系统资源耗尽。未被回收的线程可能会长时间占用资源,造成线程泄漏问题。
7. 如何自定义线程池?
答:在Java中,自定义线程池可以通过ThreadPoolExecutor类来实现。ThreadPoolExecutor提供了丰富的配置选项,如核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、任务队列(workQueue)和拒绝策略(rejectedExecutionHandler)等,可以根据具体需求进行灵活配置。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; public class CustomThreadPoolExample { public static void main(String[] args) { // 核心线程数 :线程池在空闲时保留的线程数,即使没有任务需要处理。int corePoolSize = 5; // 最大线程数 :线程池允许创建的最大线程数。int maximumPoolSize = 10; // 非核心线程空闲存活时间 :当线程数超过核心线程数时,多余的空闲线程的存活时间。long keepAliveTime = 1L; // 时间单位 : 空闲线程存活时间的时间单位。TimeUnit unit = TimeUnit.SECONDS; // 任务队列 :用于保存等待执行任务的队列。ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); // 线程工厂 : 用于创建新线程。ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 拒绝策略 : 当线程池和队列都满时,如何处理新任务RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 创建ThreadPoolExecutor ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler ); // 提交任务 for (int i = 0; i < 15; i++) { int taskId = i; executor.execute(() -> { System.out.println(Thread.currentThread().getName() + " is processing " + taskId); try { // 模拟任务执行时间 Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } // 关闭线程池(不再接受新任务,但已提交的任务会继续执行) executor.shutdown(); // 等待所有任务完成 while (!executor.isTerminated()) { // 等待一段时间 } System.out.println("All tasks completed."); }
}
8. Java线程池有哪些参数?阻塞队列有几种?拒绝策略有几种?
答:
线程池的参数:
- corePoolSize(核心线程数):线程池中始终保留的线程数量,即使这些线程处于空闲状态。
- maximumPoolSize(最大线程数):线程池中允许创建的最大线程数量。当任务队列已满且当前线程数小于最大线程数时,会创建新线程来处理任务。
- keepAliveTime(空闲线程存活时间):当线程池中的线程数超过核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。
- timeUnit(时间单位):keepAliveTime 参数的时间单位。常见值包括 TimeUnit.SECONDS、TimeUnit.MILLISECONDS 等。
- workQueue(任务队列):用于保存等待执行任务的队列l。
- threadFactory(线程工厂):用于创建新线程。
- rejectedExecutionHandler(拒绝策略):当线程池和队列都满时,如何处理新任务。
阻塞队列:
- ArrayBlockingQueue:一个基于数组的有界阻塞队列。按FIFO(先进先出)顺序保存任务。
- LinkedBlockingQueue:一个基于链表的可选有界阻塞队列。通常用于无限制的任务队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等待一个相应的移除操作。
- PriorityBlockingQueue:一个基于优先级的无限阻塞队列。任务按照优先级顺序执行。
- DelayQueue:一个基于优先级队列的无界阻塞队列,只有在延迟期满时才能从队列中取走元素。
拒绝策略:
- AbortPolicy(默认策略):抛出 RejectedExecutionException 异常,阻止系统正常工作。
- CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。这种策略会降低新任务的提交速度,从而减轻线程池的负载。
- DiscardPolicy:直接丢弃任务,不抛出异常。如果允许任务丢失,这种策略可以用于避免系统过载。
- DiscardOldestPolicy:丢弃队列中最旧的任务(即即将执行的任务),然后重新尝试提交新任务。
9. 线程池处理任务的流程了解吗?
答:线程池处理任务的流程可以总结为:提交任务、检查核心线程数、任务队列处理、非核心线程创建、执行任务以及线程回收。
- 通过submit()或execute()方法提交任务
- 检查核心线程数,如果当前线程数少于核心线程数,那么就创建新的核心线程来执行任务。如果当前线程数已达到核心线程数,将任务放入任务队列。
- 如果任务队列没有满,将任务队列继续放入队列。如果任务队列已满,检查当前线程数是否小于最大线程数。
- 如果当前线程数小于最大线程数,创建新的非核心线程执行任务,如果当前线程已经达到了最大线程数,执行拒绝策略。
- 线程从任务队列取出任务执行。
- 线程池中的非核心线程在完成任务后不会立即销毁,进入保持存活状态,只有当这些线程在空闲时间超过keepAliveTime后被回收。
10. 如何给线程池命名?为什么建议给线程池命名?
答:要给线程池中的线程命名,可以自定义一个 ThreadFactory,在创建线程时设置线程名称。
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;/*** 线程工厂,它设置线程名称,有利于我们定位问题。*/
public final class NamingThreadFactory implements ThreadFactory {private final AtomicInteger threadNum = new AtomicInteger();private final String name;/*** 创建一个带名字的线程池生产工厂*/public NamingThreadFactory(String name) {this.name = name;}@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(r);t.setName(name + " [#" + threadNum.incrementAndGet() + "]");return t;}
}
在 Java 中,为线程池中的线程命名是一种良好的实践。命名线程有助于调试和监控,使得可以轻松识别和跟踪线程的行为和状态。
参考资料
- JavaGuide
- 牛客网-Java面试宝典