首页IT科技线程的常用方法和作用(02.关于线程你必须知道的8个问题(上))

线程的常用方法和作用(02.关于线程你必须知道的8个问题(上))

时间2025-07-09 02:20:43分类IT科技浏览3987
导读:大家好,我是王有志,欢迎来到《Java面试都问啥?》的第一篇技术文章。...

大家好              ,我是王有志                    ,欢迎来到《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.parkUntil

TIMED_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版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
vue3.0+ts打造企业级组件库(Vue3 企业级优雅实战 – 组件库框架 – 6 搭建example环境) 电脑开机出现startup interrupt menu(电脑开机弹出documents文件夹解决方法)