Hi,大家好,我是王二蛋。
金三银四求职季,特地为大家整理出12个 Java 线程面试题,希望能为正在准备或即将参与面试的小伙伴们提供些许帮助。
后续还会整理关于Web、并发编程等Java相关面试题,敬请各位持续关注。
这12个线程面试题先给大家罗列出来

1.创建线程有几种方法
创建线程主要有四种方法:
-
继承Thread类 -
实现Runnable接口 -
实现Callable接口 -
使用线程池
继承Thread:通过继承Java的Thread类并重写其run()方法,可以创建线程。

实现Runnable接口:通过实现Java的Runnable接口并重写其run()方法,也可以创建线程。与继承Thread类相比,实现Runnable接口的方式更加灵活,因为Java不支持多继承,但可以实现多个接口。

实现Callable接口:Callable接口是Java 5中新增的一个接口,它是有返回值的,并且可以声明抛出异常。

使用线程池:线程池是用于管理和复用线程的机制,可以避免频繁地创建和销毁线程,从而提高系统的性能。

在实际开发中,通常推荐使用实现Runnable接口或使用线程池的方式来创建线程,因为它们更加灵活且可以更有效地管理资源。
2.介绍下线程的生命周期
线程的生命周期是指线程从创建到销毁所经历的一系列状态变化。以下是线程生命周期中的主要状态:
-
新建状态:当创建一个新的线程对象时,它处于新建状态。此时,线程对象只是被分配了内存,但还没有开始执行。 -
就绪状态:当线程对象调用了start()方法后,线程就进入了就绪状态。这表示线程已经具备了运行的条件,等待CPU的调度来执行。 -
运行状态:当线程获得CPU时间片,开始执行其run()方法中的代码时,线程就进入了运行状态。 -
阻塞状态:线程因为某种原因(竞争锁、sleep等)而暂停执行,进入阻塞状态。 -
等待状态:线程通过调用Object类的wait()、wait(long timeout)方法进入等待状态。如果超过了timeout,线程就会返回就绪状态。 -
终止状态:当线程的run()方法执行完毕,或者因为异常退出了run()方法,线程就进入了终止状态,也意味着线程的生命周期结束。

3.如何停止正在运行的线程
有几种策略可以优雅地停止一个线程:
-
使用标志位 -
使用中断 -
使用Future和Callable -
使用阻塞队列和线程池
使用标志位:在你的线程运行逻辑中,设置一个共享的布尔变量作为标志位。当需要停止线程时,改变这个标志位的状态。

使用中断:每个线程都有一个中断状态,可以通过**调用线程的interrupt()**方法来设置这个状态。

使用Future和Callable:如果使用ExecutorService和Future来管理线程和任务,可以通过调用Future.cancel(boolean mayInterruptIfRunning)
来尝试停止任务。

使用阻塞队列和线程池:如果使用线程池和阻塞队列来处理任务,可以通过不再向队列中添加新任务,完成“停止”线程池。
注意!!!即使使用上述方法,也不能保证线程会立即停止。因为线程可能正在执行一个不能中断的阻塞操作,比如Thread.sleep()或Object.wait()。
4.什么是线程安全
线程安全就是多线程访问同一段代码,不会产生错误的结果。在多线程的情况下,通常需要使用同步机制,如synchronized、Lock来保证线程安全。
在Java中,像String、Integer、Long都是final修饰的类,任何一个线程都改变不了它们的值,除非新创建一个,所以这些不可变对象不需要任何同步手段就可以直接在多线程下使用。
还有一些线程安全的类,比如CopyOnWriteArrayList、Vector等。
5.线程安全需要保证几个基本特性?
-
原子性:相关操作不会中途被其他线程干扰,一般通过同步机制实现。 -
可见性:一个线程修改了某个共享变量,其状态能够立即被其他线程知晓。volatile就是负责保证可见性的。 -
有序性:程序执行的顺序按照代码的先后顺序执行,且不受编译器的重排序影响,避免指令重排。
6.为什么wait和notify方法要在同步块中调用?
主要原因涉及到线程安全和Java对象锁的机制。
当线程调用对象的 wait() 方法时,它会释放该对象的锁,并进入该对象的等待集合(wait set)。同样,当线程调用对象的 notify() 或 notifyAll() 方法时,它必须持有该对象的锁。
如果在没有同步的情况下调用这些方法,就可能导致不一致的锁状态,从而引发并发问题。
7.Synchronized和Lock的区别
synchronized和Lock都是Java中用于控制多个线程对共享资源的并发访问控制机制。以下是主要区别:
-
来源:synchronized是Java的关键字。Lock是Java中的一个接口,通常通过实现类(如ReentrantLock)来使用。 -
锁的释放:synchronized锁在方法或代码块执行完毕后自动释放。Lock在使用完毕后需要调用unlock()方法来释放锁。 -
锁的公平:synchronized锁是非公平的,等待时间最长的线程不一定能优先获得锁。Lock可以是公平的,也可以是非公平的,公平锁会按照线程请求锁的顺序来分配锁。 -
锁的中断:synchronized是不可中断的,一个线程在等待获取synchronized锁时,不能被其他线程中断。Lock接口提供了lockInterruptibly()方法,允许在等待获取锁的过程中被中断。 -
锁的粒度:synchronized只能作用于整个方法或指定的代码块。Lock可以控制锁定粒度,例如,可以锁定对象的某个特定部分,而不是整个对象。
8.常用的线程池有哪些
-
FixedThreadPool:线程池中的线程数量在创建时就被设定,并且是固定的。当有新的任务提交到线程池时,如果线程池中有空闲线程,则立即执行,否则被放入队列中等待。适用于并发处理任务数量有限的情况。 -
CachedThreadPool:线程池会根据需要创建新的线程,理论上可以无限制地增长。适用于并发处理任务数量较大的场景。 -
ScheduledThreadPool:同样是一个固定长度的线程池,提供定时任务的功能。 -
SingleThreadExecutor:线程池中只有一个线程来执行所有的任务,任务之间按照提交顺序进行串行执行。适用于需要顺序执行的任务场景。
9.为什么需要线程池
线程池的出现主要是为了解决频繁创建和销毁线程所带来的性能开销问题。具体体现在以下几点:
-
降低资源消耗:线程的创建和销毁都需要消耗系统资源,包括内存和时间。如果应用程序频繁地创建和销毁线程,将会造成大量的资源浪费。 -
提高响应速度:当执行一个任务时,如果创建一个新线程来执行,可能需要花费一定的时间。如果使用线程池中预先创建的线程,可以立即执行任务,从而提高了系统的响应速度。 -
管理线程更加方便:线程池可以对线程进行统一调度、监控。通过线程池,开发人员可以更加方便地控制线程的数量、优先级、生命周期等,以满足不同的业务需求。 -
支持并发控制:线程池通常提供了一些并发控制机制,如任务队列、拒绝策略等,可以帮助开发人员更好地控制任务的并发执行。
10. 简述一下线程池的工作原理
了解线程池的工作原理之前,先看一下构建线程池的几个重要参数。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
参数含义:
corePoolSize:线程池核心线程数量
maximumPoolSize:线程池最大线程数量
keepAliverTime:空闲线程存活时间
unit:keepAliveTime 时间的单位
workQueue:存放任务的队列
handler:拒绝策略
通过这几个参数就可以知道线程池的工作原理:
-
如果当前线程数小于核心线程数,线程池会创建新的线程来执行提交的任务。 -
如果当前线程数已经达到核心线程数,但任务队列未满,新提交的任务会被放入任务队列中等待执行。 -
如果任务队列也满了,但没达到最大线程数,那么就创建新的线程执行。 -
如果线程数已经达到最大线程数,那就执行拒绝策略。

11.线程池的拒绝策略有哪些?
-
AbortPolicy:这是线程池默认的拒绝策略。当线程池无法处理新任务时,该策略会直接抛出异常,阻止新任务的提交。 -
CallerRunsPolicy:当线程池无法处理新任务时,将任务交给提交任务的线程去执行。 -
DiscardPolicy:当线程池无法处理新任务时,默默地丢弃该任务。在实际应用中需要谨慎使用。 -
DiscardOldestPolicy:当线程池无法处理新任务时,丢弃队列中等待时间最长的任务,然后尝试重新提交新任务。
12.说说ThreadLocal的原理
ThreadLocal对象都相当于一个容器,用于存储线程本地的变量副本。每个线程都只能访问自己的副本,从而避免了线程安全问题。
每个Thread线程都会拥有自己的一个成员变量ThreadLocalMap,该变量默认为空。

当获取数据时,会拿到当前线程的ThreadLocalMap,并以当前ThreadLocal实例为key从里面拿出数据。这个键是ThreadLocalMap的静态内部类,而值是Map的内部接口,对应的就是线程中ThreadLocal实例的变量副本。
因此,当调用ThreadLocal的set()方法设置值时,实际上是在当前线程的ThreadLocalMap中以ThreadLocal实例为键,将值存储在对应的位置。这样,每个线程就可以通过自己的ThreadLocalMap访问和修改自己的变量副本,而不会影响到其他线程。
在使用ThreadLocal时,必须确保在线程结束时通过调用remove()方法清除该变量在当前线程中的副本,以避免内存泄漏。
原文始发于微信公众号(Hi程序员):挑战你的Java线程知识:12道精选面试题
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/260563.html