数据结构论坛

首页 » 分类 » 分类 » 02关于线程你必须知道的8个问题上
TUhjnbcbe - 2023/9/8 20:59:00

大家好,我是王有志,欢迎来到《Java面试都问啥?》的第一篇技术文章。

这个系列会从Java部分开始,接着是MySQL和Redis的内容,同时会继续更新数据结构与算法的部分,这样在第一阶段,我们就完成了面试“三幻神”的挑战。

Java的部分从并发编程开始,接着是Java虚拟机,最后是集合框架。至于Java基础,因为大部分只是API的使用,所以只提供整理好的题目,而涉及到反射,动态代理等内容,会在集合框架完成后补充。

那么话不多说,我们直接开始吧。

并发编程都问啥?

每个模块开始时,我都会放出这一模块中知识点的统计数据,供大家参考。

统计中,我将并发编程分为了5个知识点:

线程基础:线程的基本概念,Thread类的使用等;

线程池:线程池的原理,线程池的使用等;

synchronized:原理,锁升级,优化等;

volatile:原理,指令重排,JMM相关等;

ThreadLocal:原理,使用方法,内存泄漏等;

JUC:Lock接口,并发容器,CAS,AQS等。

统计到并发编程关键词次,线程出现7次,线程池出现22次,synchronized出现0次,volatile出现12次,ThreadLocal出现8次,JUC出现44次,剩余21次仅提到多线程/并发编程。

从图中看,ThreadLocal和volatile出现概率较低,但个人建议面试准备中,并发编程的部分要全量准备。

数据大家都看到了,接下来看看各大公司都会问哪些关于线程的问题。这部分题目主要收集自某准网面经,浅紫色底色的题目是我和小伙伴在面试过程遇到过的。

MarkDown的表格实在太丑了,偷个懒使用图片代替了,文末附上整理后Excel的获取方式。

关于线程你必须知道的8个问题

涉及到概念性的题目就不过多赘述了,这些可以通过百度百科获取到答案。在这里我挑选了8道比较有代表性的问题,和大家分享我的理解。

并发编程的要素

并发编程的要素:

原子性:操作不可分割,要么不间断的全部执行,要么全部不执行;

有序性:指程序按照代码的顺序结构执行;

可见性:当一个线程修改了共享变量后,其它线程也是立即可见的。

概念很简单,我们写一些代码展示下有序性和可见性的问题(原子性实在没有想到很好的例子,有没有小伙伴提供示例呢)。

有序性问题

这是有序性问题的经典案例--未做同步控制的单例模式。当instance还未初始化时,多个线程同时调用getInstance方法,很容易出现其中一个线程获取到的instance为NULL。

这里涉及Java创建对象的操作,CPU时间片分配的问题,解决它的办法也有很多,暂时按下不表,放到volatile关键字的内容中详细解释。

可见性问题

很明显,在change_thread中修改了flag,并不会使block_thread得到解脱,这就是共享变量在线程间不可见的问题。

Java创建线程的方式

通常网上的资料会给出4种创建线程的方式:

继承Thread类

实现Runnable接口

实现Callable接口

通过线程池创建

先不评价这个答案的正确性,我们先来看看继承Thread类,实现Runnable接口和实现Callable接口是如何使用的。

继承Thread类

继承Thread类要实现run方法,用于完成业务逻辑,该方法来自于Runnable接口。启动线程通过Thread.start方法,方法内通过调用native方法start0来启动线程。

实现Runnable接口

实现Runnable接口同样要实现run方法,启动线程依旧是通过Thread.start方法。

实质上继承Thread类和实现Runnable接口没有差别,只不过是隔代实现run方法还是直接实现run方法。

实现Callable接口

实现Callable接口看起来会复杂一些,但通过代码可以看出来,最终还是回归到Thread.start方法,根据经验,这种方式是不是和Runnable有关系?

另外,我们注意到这种方式中借助到了FutureTask类,来看看FutureTask的继承关系:

不出所料,FutureTask同样要实现Runnable.run方法,只不过这次由FutureTask实现,FutureTask在run方法中调用Callable.call方法来执行业务逻辑。

我们来回顾下这种方式的特点,启动线程都是通过Thread.start方法,start方法的基本执行单位是Runnable接口,它们直接的差异在于如何实现Runnable.run方法。另一个差异就是Callable.call方法是有返回值的,而Runnable.run方法没有返回值。

使用线程池

使用线程池依旧离不开Runnable.run方法,会不会和Callable一样本质上还是Thread.start?

如果不熟悉ThreadPoolExecutor源码的话,可以采用断点的方式去跟踪源码,重点

1
查看完整版本: 02关于线程你必须知道的8个问题上