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)。
另外重写了拒绝策略,出现拒绝任务时,直接关闭线程池。