Java 面试复习_3


2019-5-19
作者:水不要鱼

(注:能力有限,如有说错,请指正!)

  • 创建线程的方式

  1. 继承 Thread 类,并重写 run() 方法,这种方式有局限性,由于 Java 中是单继承的,
    所以继承了 Thread 类就意味着不能再继承别的类了
  2. 实现 Runnable 接口,实现 run() 方法,这种方式不存在单继承的局限性,但是它没有返回值,
    所以如果执行的任务需要返回数据,就只能使用数据共享的方式来获得执行结果了
  3. 实现 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 // 线程池不能再处理任务的时候,执行的拒绝策略
            ) {
        // 省略源码。。。
    }
}

线程池内部是这样工作的:

  1. 当线程池线程数量还没达到核心线程数的时候,每来一个任务,如果没有空闲线程可以执行的话,就创建一个新线程去执行任务
  2. 当线程池线程数量已经达到核心线程数的时候,再来任务,如果没有空闲线程可以执行的话,就将任务放入任务缓冲队列
  3. 线程池中的线程会不断从任务缓冲队列中取任务执行,这个队列是阻塞的,也就是拿不到任务的话,就会等待新任务的到来
  4. 当任务缓冲队列也被填满了,就会再次创建线程,这部分线程将被标记为非核心数量线程,因为它们是介于核心线程数和最大线程数之间的线程
  5. 最终,线程数量达到了最大线程数,整个线程池便进入忙碌状态,这时候提交任务,就会执行拒绝策略

从上面我们可以看到,如果最大线程数是 Integer.MAX_VALUE,拒绝策略也就不会被执行,
因为不存在线程池满的情况。如果队列是无界的,就有可能撑爆服务器内存。

我们再来仔细看看上面的一些参数:

  1. 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;
}
  1. 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 接口

  1. 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 内置线程池

在 JDK 中提供了 Executors 工具类,里面有几个工厂方法,可以快速创建几种线程池:

  1. 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 秒才会回收空闲线程,所以线程可以不断重用,这时候它还像个线程池

  1. 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 的设置相当于失效

  1. 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)

  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 的子类,其实内部使用的队列是延迟队列,
这就意味着如果元素延迟时间还没到的话,就会阻塞,从而达到定时的效果。

这个线程池还可以定时间隔执行任务,它的原理其实很奇妙。就是把一个任务正常执行,然后判断它是否需要重复执行,
如果需要就把延迟时间更新为下一次执行的时间,再放回队列,这样就实现了重复间隔执行的效果

  1. newWorkStealingPool(int parallelism),会偷任务来做的线程池
class Executors {
    public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool(parallelism,
                 ForkJoinPool.defaultForkJoinWorkerThreadFactory,
                 null, true);
    }
}

看了这么多内置线程池的源码,终于有一个是不一样的了。内部使用的是 ForkJoinPool 实现,
感兴趣的可以搜索来看

今天就到这里!晚安!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值