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

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

时间2025-09-03 03:15:22分类IT科技浏览4921
导读:大家好,我是王有志,欢迎来到《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
c++中返回值是什么意思(C++11:返回值类型后置(跟踪返回值类型)) win10任务栏假死根除(如何解决Win10系统任务栏假死?)