先赞后看,Java进阶一大半
eginnovations网站在一篇Java线程文章中介绍道:Java 程序的多个线程拥有自己的堆栈,但共享 JVM 的堆内存。
各位好,我是南哥。
⭐⭐⭐收录在《Java学习/进阶/面试指南》:https://github/JavaSouth
1. 线程池
1.1 如何配置线程池大小
面试官:你说下线程池的大小要怎么配置?
这个问题要看业务系统执行的任务更多的是计算密集型任务,还是I/O密集型任务。大家可以从这两个方面来回答面试官。
(1)如果是计算密集型任务,通常情况下,CPU个数为N,设置N + 1个线程数量能够实现最优的资源利用率。因为N + 1个线程能保证至少有N个线程在利用CPU,提高了CPU利用率;同时不设置过多的线程也能减少线程状态切换所带来的上下文切换消耗。
(2)如果是I/O密集型任务,线程的主要等待时间是花在等待I/O操作上,另外就是计算所花费的时间。一般可以根据这个公式得出线程池合适的大小配置。
线程池大小
=
C
P
U
数量
∗
C
P
U
期望的利用率
∗
(
1
+
I
O
操作等待时间
/
C
P
U
计算时间
)
线程池大小 = CPU数量 * CPU期望的利用率 * (1 + IO操作等待时间/CPU计算时间)
线程池大小=CPU数量∗CPU期望的利用率∗(1+IO操作等待时间/CPU计算时间)
1.2 创建线程池
面试官:那线程池怎么创建?
可以使用ThreadPoolExecutor自定义创建线程池,这也是创建线程池推荐的创建方式。
public ThreadPoolExecutor(int corePoolSize, // 要保留在池中的线程数
int maximumPoolSize, // 池中允许的最大线程数
long keepAliveTime, // 当线程数大于corePoolSize时,多余的空闲线程在终止之前等待新任务的最长时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 在执行任务之前用于保存任务的队列
ThreadFactory threadFactory) { // 执行程序创建新线程时使用的工厂
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
另外Executors类也提供了一些静态工厂方法,可以用来创建一些预配置的线程池。
newFixedThreadPool可以设置线程池的固定线程数量。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newSingleThreadExecutor可以让线程按序执行,适用于需要确保所有任务按序执行的场景。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
大家看下以下源码,newCachedThreadPool的线程数没有上限限制,同时空闲线程的存活时间是60秒。newCachedThreadPool更适合系统负载不太高、线程执行时间短的场景下,因为线程任务不需要经过排队,直接交给空闲线程就可以。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newScheduledThreadPool可以安排任务在给定的延迟后运行,或者定期执行。
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
1.3 预配置线程池弊端
面试官:你说的这些预配置线程池会有什么问题?
小伙伴要记得上述静态工厂方法在使用过程中可能会出现OOM内存溢出的情况。
newFixedThreadPool
、newSingleThreadExecutor
:因为线程池指定的请求队列类型是链表队列LinkedBlockingQueue<Runnable>()
,故允许的请求队列长度是无上限的,可能会出现OOM内存溢出。newCachedThreadPool
、newScheduledThreadPool
:线程池指定的线程数上限是Integer.MAX_VALUE,故允许创建的线程数量是无上限的Integer.MAX_VALUE,可能会出现OOM内存溢出。
1.3 Spring创建线程池
面试官:你们项目线程池用的这种创建方式?
一般Spring工程创建线程池不直接使用ThreadPoolExecutor。
Spring框架提供了以Bean形式来配置线程池的ThreadPoolTaskExecutor
类,ThreadPoolExecutor类的底层实现还是基于JDK的ThreadPoolExecutor。
# 示例代码
@Bean(name = "testExecutor")
public ThreadPoolTaskExecutor testExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 配置核心线程数
executor.setCorePoolSize();
// 配置最大线程数
executor.setMaxPoolSize();
// 配置队列大小
executor.setQueueCapacity();
executor.initialize();
return executor;
}
1.4 线程池拒绝策略
面试官:线程池请求队列满了,有新的请求进来怎么办?
大家如果有看ThreadPoolExecutor源码就知道,ThreadPoolExecutor类实现了setRejectedExecutionHandler
方法,顾名思义意思是设置拒绝执行处理程序。
# ThreadPoolExecutor源码
/**
* Sets a new handler for unexecutable tasks. // 为无法执行的任务设置新的处理程序
*
* @param handler the new handler
* @throws NullPointerException if handler is null
* @see #getRejectedExecutionHandler
*/
public void setRejectedExecutionHandler(RejectedExecutionHandler handler) {
if (handler == null)
throw new NullPointerException();
this.handler = handler;
}
该方法可以为线程池设置拒绝策略,目前JDK8一共有四种拒绝策略,也对应入参RejectedExecutionHandler的四种子类实现。
- AbortPolicy:默认的拒绝策略,直接抛出RejectedExecutionException异常。
- CallerRunsPolicy:直接在execute方法的调用线程中运行被拒绝的任务。
- DiscardPolicy:直接丢弃被拒绝的任务。
- DiscardOldestPolicy:丢弃最旧的未处理请求,然后重试execute 。
另外如果线程池拒绝策略设置为DiscardOldestPolicy,线程池的请求队列类型最好不要设置为优先级队列PriorityBlockingQueue。因为该拒绝策略是丢弃最旧的请求,也就意味着丢弃优先级最高的请求。
1.5 线程工厂的作用
面试官:线程池的入参ThreadFactory有什么作用吗?
ThreadFactory定义了创建线程的工厂,回答这个问题我们就要结合实际场景了。
ThreadFactory线程工厂能够为线程池里每个线程设置名称、同时设置自定义异常的处理逻辑,可以方便我们通过日志来定位bug的位置。
以下是一个代码示例。
@Slf4j
public class CustomGlobalException {
public static void main(String[] args) {
ThreadFactory factory = r -> {
String threadName = "线程名称";
Thread thread = new Thread(r, threadName);
thread.setUncaughtExceptionHandler((t, e) -> {
log.error("{}执行了自定义异常日志", threadName);
});
return thread;
};
ExecutorService executor = new ThreadPoolExecutor(6,
6,
0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(66),
factory);
executor.execute(() -> {
throw new NullPointerException();
});
executor.shutdown();
}
}
控制台打印:2024-04-26 22:04:45[ ERROR ]线程名称执行了自定义异常日志
2. 线程通信
2.1 线程的等待/通知机制
面试官:Java线程的等待/通知机制知道吧?
Java线程的等待/通知机制指的是:线程A获得了synchronized同步方法、同步方法块的锁资源后,调用了锁对象的wait()方法,释放锁的同时进入等待状态;而线程B获得锁资源后,再通过锁对象的notify()或notifyAll()方法来通知线程A恢复执行逻辑。
其实Java的所有对象都拥有等待/通知机制的本领,大家可以在JDK源码package java.lang`下找到Java.lang.Object里提供的五个与等待/通知机制相关的方法。
一、等待。
(1)使当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法。
public final void wait() throws InterruptedException {
wait(0);
(2)使当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或者指定的毫秒timeout过去。
public final native void wait(long timeout) throws InterruptedException;
(3)使当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或者指定的毫秒timeout过去,另外nanos是额外时间,以纳秒为单位。
public final void wait(long timeout, int nanos) throws InterruptedException {
}
所以其实wait()、watit(0)、watit(0, 0)执行后都是同样的效果。
二、通知。
(1)唤醒在此对象监视器上等待的单个线程。
public final native void notify();
(2)唤醒在此对象监视器上等待的所有线程。
public final native void notifyAll();
大家有没听说过消费者生产者问题呢?消费者生产者之间要无限循环生产和消费物品,解决之道就是两者形成完美的等待、通知机制。而这套机制就可以通过上文的wait、notify方法来实现。
2.2 线程通信方式
面试官:还有没有其他线程通信方式?
(1)利用Condition进行线程通信。
如果大家的程序直接采用的是Lock对象来同步,则没有了上文synchronized锁带来的隐式同步器,也就无法使用wait()、notify()方法。
此时的线程可以使用Condition对象来进行通信。例如下文的示例代码: condition0的await()阻塞当前线程,同时释放、等待获取锁资源;接着等待其他线程调用condition0的signal()来通知其获取锁资源继续执行。
@Slf4j
public class UseReentrantLock {
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition condition0 = lock.newCondition();
private static final Condition condition1 = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
try {
lock.lock();
for (int i = 1; i < 4; i++) {
log.info(i + "");
condition1.signal();
condition0.await();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
for (int i = 65; i < 68; i++) {
log.info((char) i + "");
condition0.signal();
condition1.await();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
}
}
# 程序执行结果
2024-05-30 10:30:30[ INFO ]1
2024-05-30 10:30:30[ INFO ]A
2024-05-30 10:30:30[ INFO ]2
2024-05-30 10:30:30[ INFO ]B
2024-05-30 10:30:30[ INFO ]3
2024-05-30 10:30:30[ INFO ]C
(2)Thread采用join方法进行通信。
线程Thread对象还提供了join方法,也是一种通信的方式。当某个程序的执行流调用了某个thread对象的join方法,调用线程将会被阻塞,等到thread对象终止后才通知调用线程继续执行。
public final void join() throws InterruptedException {
join(0);
}
(3)volatile共享变量。
volatile的出现,大家是不是有些意外呢?虽然volatile适用的多线程场景不多,但它也是线程通信的一种方式。被volatile修饰的变量如果更新了值,则会通过主内存这条消息总线通知所有使用该变量的线程,让其把主内存同步到工作内存里,则所有线程都会获取共享变量最新值。
2.3 更加灵活的ReentrantLock
面试官:你说的Lock对象说下你的理解?
在线程同步上,JDK的Lock接口提供了多个实现子类,如下所示。下面我按面试官面试频率高的ReentrantLock来讲解。
ReentrantLock相比synchronized来说使用锁更加灵活,可以自由进行加锁、释放锁。ReentrantLock类提供了lock()、unlock()来实现以上操作。具体实操代码可以看上一个面试官问题关于Condition的示例代码。
// ReentrantLock源码
package java.util.concurrent.locks;
public class ReentrantLock implements Lock, java.io.Serializable {
// 获取锁
public void lock() {
sync.lock();
}
// 尝试释放此锁
public void unlock() {
sync.release(1);
}
}
另外ReentrantLock和synchronized都是可重入锁,即线程获取锁资源后,下一步如果进入相同锁资源的同步代码块,不需要再获取锁。
ReentrantLock也可以实现公平锁,即成功获取锁的顺序与申请锁资源的顺序一致。我们在创建对象时进行初始化设置就可以设置为公平锁。
ReentrantLock lock = new ReentrantLock(true);
2.4 ThreadLocal作用
面试官:ThreadLocal知道吧?
上文我们讨论的都是在多个线程对共享资源进行通信的业务场景上,例如商城业务秒杀的库存要保证数据安全性。而如果在多个线程对共享资源进行线程隔离的业务场景上,则可以使用ThreadLoccal来解决。
ThreadLocal可以保存当前线程的副本值,提供了set、get方法,通过set方法可以把指定值设置到当前线程副本;而通过get方法可以返回此当前线程副本中的值。
例如要实现一个功能,每个线程打印当前局部变量:局部变量 + 10
,我们就可以利用ThreadLocal保存共享变量i,来避免对变量i的共享冲突。
public class UseThreadLocal {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(20);
for (int i = 0; i < 3; i++) {
int number = i;
es.execute(() -> System.out.println(number + ":" + new intUtil().addTen(number)));
}
}
private static class intUtil {
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); // 使用threadLocal保存线程保存的当前共享变量num
public static int addTen(int number) {
threadLocal.set(number);
try { // 休息1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return threadLocal.get() + 10;
}
}
}
# 程序执行结果
0:10
2:12
1:11
2.5 线程生命周期
面试官:那线程生命周期都有什么?
在校招笔试、或面试中,这道面试题还是比较常见的,大家简单记忆下就可以。
-
初始状态。创建了线程对象还没有调用start()。
-
就绪或运行状态。执行了start()可能运行,也可能进入就绪状态在等待CPU资源。
-
阻塞状态 。一直没有获得锁。
-
等待状态。等待其他线程的通知唤醒。
-
超时状态。
-
终止状态。
我是南哥,南就南在Get到你的点赞点赞点赞。
创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️