Java 面试复习_3
2019-5-19
作者:水不要鱼
(注:能力有限,如有说错,请指正!)
- 继承 Thread 类,并重写 run() 方法,这种方式有局限性,由于 Java 中是单继承的,
所以继承了 Thread 类就意味着不能再继承别的类了 - 实现 Runnable 接口,实现 run() 方法,这种方式不存在单继承的局限性,但是它没有返回值,
所以如果执行的任务需要返回数据,就只能使用数据共享的方式来获得执行结果了 - 实现 Callable 接口,实现 call() 方法,这种方式需要先将 Callable 对象包装成 FutureTask 对象,
因为 Thread 的构造器就是支持 Runnable 对象,但好处就是,它可以拿到返回值
class Test {
// 继承 Thread 类
static class Thread1 extends Thread {
@Override
public void run() {
// 获取当前线程:this
System.out.println("我是 " + this.getName());
}
}
// 实现 Runnable 接口
static class Thread2 implements Runnable {
@Override
public void run() {
// 获取当前线程:Thread.currentThread()
System.out.println("我是 " + Thread.currentThread().getName());
}
}
// 实现 Callable 接口
static class Thread3 implements Callable<Void> {
@Override
public Void call() throws Exception {
// 获取当前线程:Thread.currentThread()
System.out.println("我是 " + Thread.currentThread().getName());
return null;
}
}
public static void main(String[] args) throws Exception {
new Thread1().start();
new Thread(new Thread2()).start();
// 注意这里需要将 Callable 包装成 FutureTask
// 实际上,FutureTask 实现了 Runnable 接口,所以才能传给 Thread
FutureTask task = new FutureTask<>(new Thread3());
new Thread(task).start();
// 获得返回值,这是阻塞方法,
// 建议使用带等待时间的 get(long timeout, TimeUnit unit)
task.get();
}
}
扩展:线程池
我们知道线程的创建和销毁是需要消耗性能的,而且操作系统对于线程的数量一般都会有限制,
所以我们需要一种保障,它需要满足两点:1. 线程复用,减少创建和销毁的性能消耗;2. 限制线程数量,以免线程数量太多导致过多的线程上下文切换,甚至把操作系统搞崩。
这种保障就是线程池。从字面意思很好理解,就是一个装满线程的池子,需要的时候就从里面拿一个线程,
用完了就放回去,和共享有点像。JDK 中提供了一个线程池类:ThreadPoolExecutor,构造参数如下:
class ThreadPoolExecutor {
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 线程空闲存活时间
TimeUnit unit, // 存活时间单位
BlockingQueue<Runnable> workQueue, // 任务缓冲队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 线程池不能再处理任务的时候,执行的拒绝策略
) {
// 省略源码。。。
}
}
线程池内部是这样工作的:
- 当线程池线程数量还没达到核心线程数的时候,每来一个任务,如果没有空闲线程可以执行的话,就创建一个新线程去执行任务
- 当线程池线程数量已经达到核心线程数的时候,再来任务,如果没有空闲线程可以执行的话,就将任务放入任务缓冲队列
- 线程池中的线程会不断从任务缓冲队列中取任务执行,这个队列是阻塞的,也就是拿不到任务的话,就会等待新任务的到来
- 当任务缓冲队列也被填满了,就会再次创建线程,这部分线程将被标记为非核心数量线程,因为它们是介于核心线程数和最大线程数之间的线程
- 最终,线程数量达到了最大线程数,整个线程池便进入忙碌状态,这时候提交任务,就会执行拒绝策略
从上面我们可以看到,如果最大线程数是 Integer.MAX_VALUE,拒绝策略也就不会被执行,
因为不存在线程池满的情况。如果队列是无界的,就有可能撑爆服务器内存。
我们再来仔细看看上面的一些参数:
- keepAliveTime,线程空闲存活时间
上面说过,如果线程池数量已经达到核心线程数了,再创建出来的线程就会被标记为非核心线程,
当这些被标记为非核心线程的线程空闲的时间达到了 keepAliveTime 之后,就会被回收销毁。
那核心线程有没有可能被销毁呢?有的,在这个类中有一个方法:void allowCoreThreadTimeOut(boolean value),
可以设置核心线程也会被超时回收,也就是说核心线程和非核心线程的待遇一样了,这点从源码中可以看到:
class ThreadPoolExecutor {
/**
* If false (default), core threads stay alive even when idle.
* If true, core threads use keepAliveTime to time out waiting
* for work.
*/
private volatile boolean allowCoreThreadTimeOut;
}
- workQueue,任务缓冲队列
这个参数的类型是 BlockingQueue ,这是一个接口,而且带了泛型,也就是说最终放进队列的一定是 Runnable 对象,
另外,这是个 BlockingQueue 也就是阻塞队列。阻塞的意思就是当拿不到东西的时候就会等待,当放不进东西的时候也会等待。
这个接口有一些方法很有意思:
- boolean add(E e):这个方法很霸道,一旦队列满了,添加元素失败,就会爆出 IllegalStateException 异常
- boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException:
如果添加失败,会等待特定的时间,时间到了还是添加不成功就会直接返回了,润物细无声,你甚至不知道它还在等你哈哈 - E poll(long timeout, TimeUnit unit) throws InterruptedException:和 offer 类似,
一段时间等不到就不等了,我猜它心里肯定是这么想的:老娘又不是没人要,哼,渣男! - void put(E e) throws InterruptedException:这个方法也很霸道,因为你仔细看的话会发现,它没有返回值!
意思就是它只能成功,不能失败,所以返回值就没有意义了。保证成功的秘诀就是,死等,只要没有人中断,就等待。 - E take() throws InterruptedException:和 put 类似,死等直到能拿到元素为止
在 JDK 中提供了很多的 BlockingQueue 实现类:
-
ArrayBlockingQueue:底层使用数组来实现,并且提供了容量限制,一定要给初始化容量,这个值将根据业务的量来定,
开的太大会浪费内存,开的太小,很容易满,就会导致新创建线程甚至是执行拒绝策略 -
LinkedBlockingQueue:底层使用链表来实现,它提供了默认容量,大小为 Integer.MAX_VALUE,
这意味着如果不指定大小,它将会消耗大量的内存,如果超过了机器的内存大小,就会导致内存溢出。所以在使用的时候,
还是建议手动指定大小 -
SynchronousQueue:这是最诡异的一个队列,它的容量为 0,也就是你不能添加任何的元素进去,执行 offer 方法永远返回 false,执行 poll() 永远返回 null。从源码注释中,我发现了这样一段话:
Synchronous queues are similar to rendezvous channels used in CSP and Ada.
也就是说,这个队列是类似于 CSP 并发模型中的 channel 的,比如 Go 中的 channel。
在 Java 中,我搜索了一遍,发现除了 Executors 这些有用到它之外,还有 com.sun.webkit.network.SocketStreamHandle 这个类有用到,但也仅仅只是用来初始化线程池,没有拿来作 channel 使用。所以这个队列在 Java 中貌似就是空队列的存在
另外,这个队列支持公平和非公平,我们来看源码:
class SynchronousQueue {
/**
* Creates a {@code SynchronousQueue} with the specified fairness policy.
*
* @param fair if true, waiting threads contend in FIFO order for
* access; otherwise the order is unspecified.
*/
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
}
很明显,当设置为公平时,就是 TransferQueue 对象,也就是 FIFO 的队列,反之,非公平就是 TransferStack 对象
在源码中我还发现一堆很有趣的东西:
class SynchronousQueue {
/*
* To cope with serialization strategy in the 1.5 version of
* SynchronousQueue, we declare some unused classes and fields
* that exist solely to enable serializability across versions.
* These fields are never used, so are initialized only if this
* object is ever serialized or deserialized.
*/
@SuppressWarnings("serial")
static class WaitQueue implements java.io.Serializable { }
static class LifoWaitQueue extends WaitQueue {
private static final long serialVersionUID = -3633113410248163686L;
}
static class FifoWaitQueue extends WaitQueue {
private static final long serialVersionUID = -3623113410248163686L;
}
private ReentrantLock qlock;
private WaitQueue waitingProducers;
private WaitQueue waitingConsumers;
}
我跟踪使用的地方,发现只有两个地方有用到:
class SynchronousQueue {
/**
* Saves this queue to a stream (that is, serializes it).
* @param s the stream
* @throws java.io.IOException if an I/O error occurs
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
boolean fair = transferer instanceof TransferQueue;
if (fair) {
qlock = new ReentrantLock(true);
waitingProducers = new FifoWaitQueue();
waitingConsumers = new FifoWaitQueue();
}
else {
qlock = new ReentrantLock();
waitingProducers = new LifoWaitQueue();
waitingConsumers = new LifoWaitQueue();
}
s.defaultWriteObject();
}
/**
* Reconstitutes this queue from a stream (that is, deserializes it).
* @param s the stream
* @throws ClassNotFoundException if the class of a serialized object
* could not be found
* @throws java.io.IOException if an I/O error occurs
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
if (waitingProducers instanceof FifoWaitQueue)
transferer = new TransferQueue<E>();
else
transferer = new TransferStack<E>();
}
}
换而言之,就是序列化的时候要用到,一个空队列居然也要序列化??搞不懂 Doug Lea 的想法,
或许这个队列还有其他很大的作用,只是我们还没有发现
-
PriorityBlockingQueue:优先队列,底层使用数组模拟的堆结构实现,
和 java.util.PriorityQueue 的功能一样,只是它是阻塞的。可以指定初始化容量,而且内部还保存了一个 PriorityQueue 用于序列化,
也就是在 writeObject 中将数据写入内部这个的 PriorityQueue,然后序列化这个 PriorityQueue,
在 readObject 中从 PriorityQueue 中读取数据并初始化 PriorityBlockingQueue -
DelayQueue:延迟队列,底层使用优先队列来排序时间,也就是说,当你向这个队列添加元素的时候,它会根据这个元素的延迟时间来升序排序,
这样最先出队的就是延迟最低时间最短的那个元素了。那元素的延迟时间怎么设置?我们可以发现,这个类的定义是有泛型的:
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {}
这个泛型 E extends Delayed 就意味着入队的元素必须实现 java.util.concurrent.Delayed 接口,
进而实现 long getDelay(TimeUnit unit) 方法来设置元素的延迟时间。这个队列使用了 Leader-Follower 模式来减少等待的时间,
感兴趣的可以去搜相关内容
还有一个类和这个类很相似,那就是 java.util.concurrent.ScheduledThreadPoolExecutor.DelayedWorkQueue 队列,
但是这个队列没有直接使用优先队列,而是又重写了堆结构,同样要求元素实现 java.util.concurrent.Delayed 接口
- handler,拒绝策略
上面说过,当线程池满了的时候,就会执行拒绝策略。这个参数是 java.util.concurrent.RejectedExecutionHandler 接口,
里面有一个 void rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法,我们可以自己定义拒绝策略,
用于不同的业务需求。
比如说,你这个线程池的任务不允许有一丝的丢失,而线程池又不可能一直处理任务,
你就可以定义一个拒绝策略:
class NotRejectedExecutionHandler implements RejectedExecutionHandler {
// 重试次数
// 保存操作有可能失败,所以需要重试,但是不能一直重试,
// 必须要有个重试次数限制,否则就会导致资源浪费
private static final int RETRY_TIMES = 3;
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// Runnable r: 被拒绝的任务对象
// ThreadPoolExecutor executor: 当前执行任务的这个线程池
// 场景假设:不允许丢失任何一个任务
// 我们可以将任务对象持久化,比如保存在数据库中,
// 之后由定时任务来从数据库取出任务执行
int retryTimes = 0;
while (!save(r)) {
retryTimes++;
if (retryTimes > RETRY_TIMES) {
// 如果重试了三次都失败了,就需要采取应急措施
notifyWorker(r);
break;
}
}
private boolean save(Runnable task) {
// 数据库保存操作...
return true;
}
private void notifyWorker(Runnable r) {
// 日志记录,或者发邮件通知运维人员之类的...
// 由于这已经是应急操作了,所以日志记录是必须的
// 而且为了保证应急操作成功,最好执行一些失败率很低的操作
}
}
上面只是举了一个例子,你还可以将任务保存在缓存系统或者消息队列中,这些都需要根据实际业务来定。
如果你没有指定这个参数,就会使用默认的拒绝策略,也就是直接抛异常:
class ThreadPoolExecutor {
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
// 这里的 defaultHandler 就是下面的 AbortPolicy
}
/**
* The default rejected execution handler
*/
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
/**
* A handler for rejected tasks that throws a
* {@code RejectedExecutionException}.
*/
public static class AbortPolicy implements RejectedExecutionHandler {
/**
* Creates an {@code AbortPolicy}.
*/
public AbortPolicy() {}
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " + e.toString());
}
}
}
除了上面的 AbortPolicy 默认拒绝策略之外,还有几个内置的拒绝策略:
- CallerRunsPolicy:在调用者线程执行这个任务
public static class CallerRunsPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code CallerRunsPolicy}.
*/
public CallerRunsPolicy() {}
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
// 直接执行这个 run 方法
r.run();
}
}
}
- DiscardOldestPolicy:丢掉最老的元素,空出位置执行这个被拒绝的任务
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardOldestPolicy} for the given executor.
*/
public DiscardOldestPolicy() {}
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
// 将队列的第一个元素移除,空出位置来,然后再将任务加进去执行
e.getQueue().poll();
e.execute(r);
}
}
}
- DiscardPolicy:这是最简单的策略了,就是直接拒绝,也不会抛出异常,
就是很安静的抛弃掉任务,无声无息,悄悄地就走了
public static class DiscardPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardPolicy}.
*/
public DiscardPolicy() {}
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 啥也不干
}
}
在 JDK 中提供了 Executors 工具类,里面有几个工厂方法,可以快速创建几种线程池:
- newCachedThreadPool(),这个线程池从某种程度上来说不能算是线程池,我们来看方法源码:
class Executors {
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0, // 核心线程数
Integer.MAX_VALUE, // 最大线程数
60L, TimeUnit.SECONDS, // 非核心线程空闲 60 秒就被回收
// 任务缓冲队列
// 一旦达到核心线程数,这个队列又不接受元素,就会开始创建非核心线程
new SynchronousQueue<Runnable>()
// 默认的拒绝策略:AbortPolicy,也就是直接报异常
);
}
}
为什么我说这不能算是线程池?
它的核心线程数设置为 0,而任务缓冲队列又是 SynchronousQueue,
也就是说一旦有任务到来,就直接创建非核心线程。一旦每个任务的执行时间都超过 60 秒,就会导致一直创建新线程。
而由于创建的都是非核心线程,所以一旦空闲,又会被全部回收销毁。
这也就失去线程池的价值,资源控制和减少创建销毁性能消耗
那这个线程有什么意义吗?
如果你真的要说意义,那就是可以用来运行非常短小的任务,比如一个任务就几秒的运行时间,
由于 60 秒才会回收空闲线程,所以线程可以不断重用,这时候它还像个线程池
- newFixedThreadPool(int nThreads),固定线程数量的线程池,还是来看方法源码:
class Executors {
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads, // 核心线程数
nThreads, // 最大线程数
0L, TimeUnit.MILLISECONDS, // 非核心线程空闲立马被回收
// 任务缓存队列
// 使用了不指定大小的 LinkedBlockingQueue 就意味着队列长度是 Integer.MAX_VALUE,
// 所有任务太多就会占用很多的内存,也就有可能将内存耗尽,导致服务器崩溃
new LinkedBlockingQueue<Runnable>()
// 默认的拒绝策略:AbortPolicy,也就是直接报异常
);
}
}
我们可以留意到这个线程池的设置,核心线程数和最大线程数是一样的,也就是说队列满了,线程池就满了,
就不会创建非核心线程,所以这时候 0L, TimeUnit.MILLISECONDS 的设置相当于失效
- newSingleThreadExecutor(),单线程的线程池,废话不多说,上源码:
class Executors {
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(
1, // 核心线程数
1, // 最大线程数
0L, TimeUnit.MILLISECONDS, // 非核心线程空闲立马被回收
// 任务缓存队列
// 使用了不指定大小的 LinkedBlockingQueue 就意味着队列长度是 Integer.MAX_VALUE,
// 所有任务太多就会占用很多的内存,也就有可能将内存耗尽,导致服务器崩溃
new LinkedBlockingQueue<Runnable>())
// 默认的拒绝策略:AbortPolicy,也就是直接报异常
);
}
}
你可以把它当成是 newFixedThreadPool(1)
- newScheduledThreadPool(int corePoolSize),定时任务线程池
class Executors {
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
}
// 可以看到 ScheduledThreadPoolExecutor 的父类是 ThreadPoolExecutor
class ScheduledThreadPoolExecutor extends ThreadPoolExecutor
implements ScheduledExecutorService {
public ScheduledThreadPoolExecutor(int corePoolSize) {
// 下面的代码其实就是创建 ThreadPoolExecutor
super(
corePoolSize, // 核心线程数
Integer.MAX_VALUE, // 最大线程数
0, NANOSECONDS, // 非核心线程空闲立马被回收
// 任务缓存队列
// 与 DelayQueue 类似
// 要求元素必须实现 java.util.concurrent.Delayed 接口
new DelayedWorkQueue()
// 默认的拒绝策略:AbortPolicy,也就是直接报异常
);
}
}
定时任务线程池,可以说是 ThreadPoolExecutor 的子类,其实内部使用的队列是延迟队列,
这就意味着如果元素延迟时间还没到的话,就会阻塞,从而达到定时的效果。
这个线程池还可以定时间隔执行任务,它的原理其实很奇妙。就是把一个任务正常执行,然后判断它是否需要重复执行,
如果需要就把延迟时间更新为下一次执行的时间,再放回队列,这样就实现了重复间隔执行的效果
- newWorkStealingPool(int parallelism),会偷任务来做的线程池
class Executors {
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
}
看了这么多内置线程池的源码,终于有一个是不一样的了。内部使用的是 ForkJoinPool 实现,
感兴趣的可以搜索来看