线程的常用方法和作用(02.关于线程你必须知道的8个问题(上))
大家好 ,我是王有志 ,欢迎来到《Java面试都问啥?》的第一篇技术文章 。
这个系列会从Java部分开始 ,接着是MySQL和Redis的内容 ,同时会继续更新数据结构与算法的部分 ,这样在第一阶段 ,我们就完成了面试“三幻神 ”的挑战 。
Java的部分从并发编程开始 ,接着是Java虚拟机 ,最后是集合框架。至于Java基础 ,因为大部分只是API的使用,所以只提供整理好的题目 ,而涉及到反射 ,动态代理等内容,会在集合框架完成后补充 。
那么话不多说 ,我们直接开始吧 。
并发编程都问啥?
每个模块开始时 ,我都会放出这一模块中知识点的统计数据,供大家参考 。
统计中 ,我将并发编程分为了5个知识点:
线程基础:线程的基本概念 ,Thread类的使用等; 线程池:线程池的原理 ,线程池的使用等; synchronized:原理 ,锁升级 ,优化等; volatile:原理 ,指令重排 ,JMM相关等; ThreadLocal:原理 ,使用方法 ,内存泄漏等; JUC:Lock接口,并发容器 ,CAS ,AQS等 。统计到并发编程关键词174次,线程出现37次 ,线程池出现22次 ,synchronized出现30次,volatile出现12次 ,ThreadLocal出现8次 ,JUC出现44次 ,剩余21次仅提到多线程/并发编程 。
从图中看 ,ThreadLocal和volatile出现概率较低 ,但个人建议面试准备中 ,并发编程的部分要全量准备 。
数据大家都看到了 ,接下来看看各大公司都会问哪些关于线程的问题 。这部分题目主要收集自某准网面经 ,浅紫色底色的题目是我和小伙伴在面试过程遇到过的 。
MarkDown的表格实在太丑了 ,偷个懒使用图片代替了,文末附上整理后Excel的获取方式 。
关于线程你必须知道的8个问题
涉及到概念性的题目就不过多赘述了 ,这些可以通过百度百科获取到答案。在这里我挑选了8道比较有代表性的问题 ,和大家分享我的理解 。
并发编程的3要素
并发编程的3要素:
原子性:操作不可分割,要么不间断的全部执行 ,要么全部不执行; 有序性:指程序按照代码的顺序结构执行; 可见性:当一个线程修改了共享变量后 ,其它线程也是立即可见的 。概念很简单,我们写一些代码展示下有序性和可见性的问题(原子性实在没有想到很好的例子 ,有没有小伙伴提供示例呢)。
有序性问题 public static class Singleton { private Singleton instance; public Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } private Singleton() { } }这是有序性问题的经典案例--未做同步控制的单例模式 。当instance还未初始化时 ,多个线程同时调用getInstance方法 ,很容易出现其中一个线程获取到的instance为NULL 。
这里涉及Java创建对象的操作 ,CPU时间片分配的问题 ,解决它的办法也有很多 ,暂时按下不表 ,放到volatile关键字的内容中详细解释。
可见性问题 private static boolean flag = true; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (flag) { } System.out.println("线程:" + Thread.currentThread().getName() + " ,flag:" + flag); }, "block_thread").start(); TimeUnit.MICROSECONDS.sleep(500); new Thread(() -> { flag = false; System.out.println("线程:" + Thread.currentThread().getName() + " ,flag:" + flag); }, "change_thread").start(); }很明显,在change_thread中修改了flag ,并不会使block_thread得到解脱 ,这就是共享变量在线程间不可见的问题 。
Java创建线程的方式
通常网上的资料会给出4种创建线程的方式:
继承Thread类 实现Runnable接口 实现Callable接口 通过线程池创建先不评价这个答案的正确性,我们先来看看继承Thread类 ,实现Runnable接口和实现Callable接口是如何使用的 。
继承Thread类 public class ByThread { public static void main(String[] args) throws InterruptedException { System.out.println("main的线程:" + Thread.currentThread().getName()); MyThread myThread = new MyThread(); myThread.start(); } static class MyThread extends Thread { @Override public void run() { System.out.println("MyThread的线程:" + Thread.currentThread().getName()); } } }继承Thread类要实现run方法 ,用于完成业务逻辑,该方法来自于Runnable接口 。启动线程通过Thread.start方法 ,方法内通过调用native方法start0来启动线程 。
实现Runnable接口 public class ByRunnable { public static void main(String[] args) { System.out.println("main的线程:" + Thread.currentThread().getName()); new Thread(new MyRunnable()).start(); } static class MyRunnable implements Runnable { @Override public void run() { System.out.println("MyRunnable的线程:" + Thread.currentThread().getName()); } } }实现Runnable接口同样要实现run方法 ,启动线程依旧是通过Thread.start方法 。
实质上继承Thread类和实现Runnable接口没有差别 ,只不过是隔代实现run方法还是直接实现run方法 。
实现Callable接口 public class ByCallable { public static void main(String[] args) throws ExecutionException, InterruptedException { System.out.println("main的线程:" + Thread.currentThread().getName()); Callable<String> callable = new MyCallable(); FutureTask <String> futureTask = new FutureTask<>(callable); new Thread(futureTask).start(); System.out.println("MyCallable的执行线程:" + futureTask.get()); } static class MyCallable implements Callable<String> { @Override public String call() { System.out.println("MyCallable的线程:" + Thread.currentThread().getName()); return Thread.currentThread().getName(); } } }实现Callable接口看起来会复杂一些 ,但通过代码可以看出来 ,最终还是回归到Thread.start方法 ,根据经验 ,这种方式是不是和Runnable有关系?
另外 ,我们注意到这种方式中借助到了FutureTask类 ,来看看FutureTask的继承关系:
不出所料,FutureTask同样要实现Runnable.run方法 ,只不过这次由FutureTask实现 ,FutureTask在run方法中调用Callable.call方法来执行业务逻辑 。
我们来回顾下这3种方式的特点,启动线程都是通过Thread.start方法 ,start方法的基本执行单位是Runnable接口 ,它们直接的差异在于如何实现Runnable.run方法 。另一个差异就是Callable.call方法是有返回值的,而Runnable.run方法没有返回值 。
使用线程池 public class ByThreadPool { public static void main(String[] args) { System.out.println("main的线程:" + Thread.currentThread().getName()); ExecutorService executorService = Executors.newSingleThreadExecutor(); Runnable runnable = new Runnable() { @Override public void run() { System.out.println("线程池的线程:" + Thread.currentThread().getName()); } }; executorService.execute(runnable); executorService.shutdown(); } }使用线程池依旧离不开Runnable.run方法 ,会不会和Callable一样本质上还是Thread.start?
如果不熟悉ThreadPoolExecutor源码的话 ,可以采用断点的方式去跟踪源码 ,重点关注ThreadPoolExecutor.execute和ThreadPoolExecutor.addWorker两个方法。
我们可以在addWorker方法中发现两行关键代码:
final Thread t = w.thread; t.start();这证实了关于ThreadPoolExecutor底层调用的猜想 ,最终依旧是通过Thread.start方法启动 。
回到最初的问题 ,Java有几种创建线程的方式?
如果从Java的层面来看 ,可以认为创建Thread类的实例对象就完成了线程的创建 ,而调用Thread.start0可以认为是操作系统层面的线程创建和启动 。
至于网上说的4种创建线程的方式 ,个人认为将它们归类到线程中业务逻辑的实现方式更合理。
Java的线程状态
Java中定义了6种线程状态(与OS的线程状态有差别) ,线程状态的枚举类被定义为Thread的内部类State 。
public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; }需要注意,Java中并未定义线程的RUNNING状态 ,而是通过RUNNABLE包含了RUNNABLE(可运行)和RUNNING(运行中) 。
建议大家阅读源码中的注释 ,很清晰的解释了每个状态的场景。下面我还是通过几段代码展示线程的不同状态 。
Tips:代码中出现的TimeUnit.MILLISECONDS.sleep是为了确保线程已经进入期望的状态,如果不能很好的理解 ,文末附有Gitee地址 ,工程中的代码有注释 。
常规状态的转换这里指的是线程从创建后(NEW),到启动后(RUNNABLE) ,再到最后终止(TERMINATED)的一种无竞争的线程状态转换 。
NEW(新建):创建线程后尚未启动(未调用start方法); RUNNABLE(可运行):可运行状态的线程在Java虚拟机中等待调度线程选中获取CPU时间片; TERMINATED(终止):线程执行结束 。写一段简单的代码来看下:
public class NormalStateTransition { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { try { TimeUnit.MILLISECONDS.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } }); System.out.println("线程[" + thread.getName() + "]创建 ,状态:[" + thread.getState() + "]"); thread.start(); System.out.println("线程[" + thread.getName() + "]启动 ,状态:[" + thread.getState() + "]"); TimeUnit.SECONDS.sleep(2); System.out.println("线程[" + thread.getName() + "]结束 ,状态:[" + thread.getState() + "]"); } }代码非常简单 ,这里就不再解释了 。
常规状态的转换:
阻塞状态的转换阻塞状态是一种“异常 ”的状态 ,通常是在等待资源 。
BLOCKED(阻塞):等待监视器锁而阻塞的线程状态 ,处于阻塞状态的线程正在等待监视器锁进入同步的代码块/方法 ,或者在调用Object.wait之后重新进入同步的代码块/方法 。
再写一段代码:
public class BlockedStateTransition { public static void main(String[] args) throws InterruptedException { AtomicBoolean locker = new AtomicBoolean(false); new Thread(() -> { synchronized (locker) { try { TimeUnit.MILLISECONDS.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); Thread t = new Thread(() -> { synchronized (locker) { try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程[" + Thread.currentThread().getName() + "]阻塞后 ,状态:[" + Thread.currentThread().getState() + "]"); } }); System.out.println("线程[" + t.getName() + "]创建,状态:[" + t.getState() + "]"); t.start(); System.out.println("线程[" + t.getName() + "]启动 ,状态:[" + t.getState() + "]"); System.out.println("线程[" + t.getName() + "]阻塞中 ,状态:[" + t.getState() + "]"); TimeUnit.MILLISECONDS.sleep(5000); System.out.println("线程[" + t.getName() + "]结束,状态:[" + t.getState() + "]"); } }首先是匿名线程持有locker ,接着线程t启动 ,进入RUNNABLE状态,线程t尝试获取locker ,进入BLOCKED状态 ,等待后获取到locker ,进入RUNNABLE状态 ,最后执行结束 ,进入TERMINATED状态 。
阻塞状态转换:
等待状态的转换关于等待状态 ,Java源码的注释有详细描述如何进入等待状态 ,以及如何唤醒处于等待状态的线程:
Thread state for a waiting thread. A thread is in the waiting state due to calling one of the following methods:
Object.wait with no timeout
Thread.join with no timeout
LockSupport.park
A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.join() is waiting for a specified thread to terminate.WAITING(等待):线程处于等待状态 ,处于等待状态的线程正在等待另一个线程执行的特定操作(通知或中断) 。
再再写一段代码:
public class WaitingStateTransition { public static void main(String[] args) throws InterruptedException { AtomicBoolean locker = new AtomicBoolean(false); Thread t = new Thread(() -> { synchronized (locker) { try { TimeUnit.MILLISECONDS.sleep(100); locker.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程[" + Thread.currentThread().getName() + "]唤醒 ,状态:[" + Thread.currentThread().getState() + "]"); } }); System.out.println("线程[" + t.getName() + "]创建,状态:[" + t.getState() + "]"); t.start(); System.out.println("线程[" + t.getName() + "]启动 ,状态:[" + t.getState() + "]"); TimeUnit.MILLISECONDS.sleep(150); System.out.println("线程[" + t.getName() + "]等待 ,状态:[" + t.getState() + "]"); new Thread(() -> { synchronized (locker) { locker.notify(); } }).start(); TimeUnit.MILLISECONDS.sleep(100); System.out.println("线程[" + t.getName() + "]结束,状态:[" + t.getState() + "]"); } }线程t创建后 ,进入NEW状态 ,启动后,进入RUNNABLE状态 ,locker.wait后 ,进入WAITING状态 ,匿名线程启动 ,locker.notify后 ,唤醒线程t ,进入RUNNABLE状态 ,最后线程执行结束 ,进入TERMINATED状态。
等待状态的转换:
限时等待状态的转换Java源码的注释上 ,也很详细的解释了如何进入限时等待:
Thread state for a waiting thread with a specified waiting time. A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time:
Thread.sleep
Object.wait with timeout
Thread.join with timeout
LockSupport.parkNanos
LockSupport.parkUntilTIMED_WAITING(限时等待):线程处于限时等待状态,与等待状态不同的是 ,在指定时间后 ,线程会被自动唤醒 。
Tips:也有翻译成超时等待的,但是我觉得不太合适 。
再再再写一段代码:
public class TimedWaitingStateTransition { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程[" + Thread.currentThread().getName() + "]限时等待后 ,状态:[" + Thread.currentThread().getState() + "]"); }); System.out.println("线程[" + t.getName() + "]创建 ,状态:[" + t.getState() + "]"); t.start(); System.out.println("线程[" + t.getName() + "]启动,状态:[" + t.getState() + "]"); TimeUnit.MILLISECONDS.sleep(50); System.out.println("线程[" + t.getName() + "]限时等待中 ,状态:[" + t.getState() + "]"); TimeUnit.MILLISECONDS.sleep(100); System.out.println("线程[" + t.getName() + "]结束 ,状态:[" + t.getState() + "]"); } }线程t创建后 ,进入NEW状态 ,启动后进入RUNNABLE状态 ,线程休眠100ms ,进入TIMED_WAITING状态 ,休眠时间结束后 ,进入RUNNABLE状态 ,最后线程执行结束,进入TERMINATED状态。
限时等待状态的转换:
线程状态转换总结上面我们通过4段代码了解了线程状态的转换 ,下面我们通过一张图来总结下线程的状态转换 。
结语
今天分享了并发编程的统计数据 ,因此面试题目较少,不过还是希望对你有帮助 。
下一篇内容是剩余的5个知识点(如果一篇能够写完的话):
Thread类核心方法 同步与互斥 Java线程调度方式 死锁的产生与解决 多线程的优点本篇文章的代码仓库:
并发3要素问题 线程实现方式 线程状态转换关注王有志 ,回复面试题合集获取整理后的面试题(正在同步更新 ,可以持续关注)。
好了,今天就到这里了 ,Bye~~
创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!