Java | 多线程 | 史上最全Java多线程面试题及答案(持续更新中)

1、Synchronized和Violatile的本质区别?

1)Synchronized底层实现依赖于jvm的监视器monitor,对编译后的代码指令加锁。所以会出现阻塞情况。而Violatile底层实现则是借助于寄存器,也就是工作内存,在读取Violatile变量时,jvm会告诉寄存器,该变量的值需要同步到主内存,不会出现阻塞的情况。

2)线程池安全主要涉及两个方面:原子性和可见性。Synchronized既可以保证原子性,也可以保证可见性。而Violatile只能保证可见性,所以会存在线程安全问题。

3)Synchronized可以加锁级别可以是变量,方法,和类级别。而Violatile只能是变量级别。

2、线程池启动线程execute()和submit()方法有什么不同?

1)execute方法只能执行线程,而不能获取线程执行结果。而submit通过future对象可以获取执行结果的。
2)submit方法最终还是通过execute方法来执行线程,所以可以将submit方法是对execute方法的一层封装。

3、如何正确、优雅、合理的中断线程?

1)通过全局标识变量,一般是violatile变量,通过判断标识变量,来退出线程。这也是violatile使用最多的场景。
2)调用isInterrupted()方法,修改线程中断标识位,线程逻辑执行结束后,会自行中断。

4、线程池底层实现?

线程池面试时涉及的问题比较多,我这里将线程池的覆盖范围做了总结,共五类问题:

  • 线程池优势
  • 线程池底层执行流程
  • 线程池的三种队列
  • 线程池的四种拒绝策略
  • 线程池的最佳配置

一般初中级面试官会问一些线程池优势或底层执行流程,再高级的话就是队列原理和拒绝策略,以及项目中的实际配置。具体来看一下:

一)线程池优势:线程池最大的优势就是线程复用,避免了线程创建和销毁带来的性能损耗。特别是在数量多逻辑少的请求场景中,更能体现出优势。

二)线程池底层执行流程:首先初始化一定数量的线程,task任务进来,若没有线程执行,则放入队列。如果有线程,就将task任务和thread 封装成 worker,然后在worker启动线程执行task。当task执行完毕,worker中的线程不会马上销毁,而是去队列中获取task任务,如果有task存在,则执行,没有的话则看线程池设置策略。如果核心允许超时,则线程会一直阻塞,等待队列中有task进来,如果不允许超时,则核心线程执行销毁逻辑。

三)线程池的三种队列:
1)同步队列:SynchronousQueue,一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,该队列不是一种真正意义上的容器,其内部不维护task队列,不存储task任务,而是采用了传递的方式,由生产者将task传递给消费者。但也不能说SynchronousQueue没有维护队列,其内部维护的是一个线程队列,有线程队列,也就意味着有公平和不公平两种方式。在并发不大的情况下,该队列吞吐量通常要高于LinkedBlockingQueue,而在并发比较大的情况下,建议尽量不要该队列。容易引发内存溢出。静态工厂方法Executors.newCachedThreadPool使用了这个队列。

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }


2)LinkedBlockingQueue,队列默认大小this(Integer.MAX_VALUE);,两个重要参数:corePoolSize和maximumPoolSize。也是线程池工具类用的最多的一种队列,当corePoolSize和maximumPoolSize相等时,也就是创建了固定数量的线程池,没有核心线程和普通线程之分。

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }


3)ArrayBlockingQueue,该队列同LinkedBlockingQueue类似,只是基于数组实现,构造方法必须指定队列大小。最大的区别是ArrayBlockingQueue的生产者和消费者用的是相同的条件队列锁,在性能上可能略逊于LinkedBlockingQueue。


四、线程池的四种拒绝策略:
1)AbortPolicy,默认的一种拒绝策略,也是最简单的一种,采用了直接抛异常的方式。

   private static final RejectedExecutionHandler defaultHandler =
        new AbortPolicy();

  public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                    " rejected from " +
                    e.toString());
        }

2)CallerRunsPolicy,直接运行被拒绝的任务,注意,这里是在线程池之外,脱离线程池运行的。有可能会导致性能下降,不建议使用。

       public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }

3)DiscardPolicy,默默的丢弃task任务,不做任何处理。这会引起任务丢失,而开发人员又无从查起。当然,如果允许task丢失,可以选择此策略。

 public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }

4)DiscardOldestPolicy,从名字可以看出,该策略会丢弃最老的一个task,然后重新提交。

  public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }


五、线程池的最佳配置:

ExecutorService pool = new ThreadPoolExecutor(
        Runtime.getRuntime().availableProcessors(),
        2 * Runtime.getRuntime().availableProcessors(),
        0L,
        TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024),
        namedThreadFactory,
        new ThreadPoolExecutor.AbortPolicy());
pool.execute(()-> System.out.println(Thread.currentThread().getName()));
pool.shutdown();//gracefully shutdown 优雅关闭

其中Runtime.getRuntime().availableProcessors() 为服务器可用的CPU数量,corePoolSize取CPU数量,而maximumPoolSize取CPU数量的2倍(网上也有这种配置方案:cpu密集型:CPU+1,IO密集型:2CPU+1)。
另外重写了拒绝策略,出现拒绝任务时,直接关闭线程池。

猜你喜欢

转载自blog.csdn.net/woshilijiuyi/article/details/81208117