(Java实习生)每日10道面试题打卡——Java多线程篇
Posted 兴趣使然的草帽路飞
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了(Java实习生)每日10道面试题打卡——Java多线程篇相关的知识,希望对你有一定的参考价值。
- 临近秋招,备战暑期实习,祝大家每天进步亿点点!
- 本篇总结的是Java多线程知识相关的面试题,后续会每日更新~
1、什么是进程、线程、协程,他们之间的关系是怎样的?
- 进程: 本质上是一个独立执行的程序,进程是操作系统资源分配的基本单位。
- 线程:线程是任务调度和执行的基本单位。。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程执行不同的任务,切换受系统控制。
- 协程:又称为微线程,是一种用户态的轻量级线程,协程不像线程和进程需要进行系统内核上的上下文切换,协程的上下文切换是由用户自己决定的,有自己的上下文,所以说是轻量级的线程,也称之为用户级别的线程就叫协程,一个线程可以多个协程,线程进程都是同步机制,而协程则是异步 。Java的原生语法中并没有实现协程,目前python、Lua和GO等语言支持
- 三者的关系:
- 一个进程可以有多个线程,它允许计算机同时运行两个或多个程序。线程是进程的最小执行单位,CPU的调度切换的是进程和线程,进程和线程多了之后调度会消耗大量的CPU,CPU上真正运行的是线程,线程可以对应多个协程:
2、请你说下并发和并行的区别,并举例说明?
- 并发
concurrency
:一核CPU,模拟出来多条线程,快速交替执行。 - 并行
parallellism
:多核CPU ,多个线程可以同时执行;- eg: 线程池!
- 并发指在一段时间内宏观上去处理多个任务。并行指同一个时刻,多个任务确实真的同时运行。
举例:
并发:是一心多用,听课和看电影,但是CPU大脑只有一个,所以轮着来
并行:火影忍者中的影分身,有多个你出现,可以分别做不同的事情
3、请问java实现多线程有哪几种方式,有什么不同,比较常用哪种?
3.1 继承Thread
- 继承 Thread,重写里面
run()
方法,创建实例,执行start()
方法。 - 优点:代码编写最简单直接操作。
- 缺点:没返回值,继承一个类后,没法继承其他的类,拓展性差。
public class ThreadDemo1 extends Thread {
@Override
public void run() {
System.out.println("继承Thread实现多线程,名称:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ThreadDemo1 threadDemo1 = new ThreadDemo1();
threadDemo1.setName("demo1");
// 执行start
threadDemo1.start();
System.out.println("主线程名称:"+Thread.currentThread().getName());
}
3.2 实现Runnable接口
- 自定义类实现 Runnable,实现里面
run()
方法,创建 Thread 类,使用 Runnable 接口的实现对象作为参数传递给Thread 对象,调用strat()
方法。 - 优点:线程类可以实现多个几接口,可以再继承一个类。
- 缺点:没返回值,不能直接启动,需要通过构造一个 Thread 实例传递进去启动。
public class ThreadDemo2 implements Runnable {
@Override
public void run() {
System.out.println("通过Runnable实现多线程,名称:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ThreadDemo2 threadDemo2 = new ThreadDemo2();
Thread thread = new Thread(threadDemo2);
thread.setName("demo2");
// start线程执行
thread.start();
System.out.println("主线程名称:"+Thread.currentThread().getName());
}
// JDK8之后采用lambda表达式
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("通过Runnable实现多线程,名称:"+Thread.currentThread().getName());
});
thread.setName("demo2");
// start线程执行
thread.start();
System.out.println("主线程名称:"+Thread.currentThread().getName());
}
3.3 实现Callable接口
- 创建 Callable 接口的实现类,并实现
call()
方法,结合 FutureTask 类包装 Callable 对象,实现多线程。 - 优点:有返回值,拓展性也高
- 缺点:Jdk5以后才支持,需要重写
call()
方法,结合多个类比如 FutureTask 和 Thread 类
public class MyTask implements Callable<Object> {
@Override
public Object call() throws Exception {
System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName());
return "这是返回值";
}
}
public static void main(String[] args) {
// JDK1.8 lambda表达式
FutureTask<Object> futureTask = new FutureTask<>(() -> {
System.out.println("通过Callable实现多线程,名称:" +
Thread.currentThread().getName());
return "这是返回值";
});
// MyTask myTask = new MyTask();
// FutureTask<Object> futureTask = new FutureTask<>(myTask);
// FutureTask继承了Runnable,可以放在Thread中启动执行
Thread thread = new Thread(futureTask);
thread.setName("demo3");
// start线程执行
thread.start();
System.out.println("主线程名称:"+Thread.currentThread().getName());
try {
// 获取返回值
System.out.println(futureTask.get());
} catch (InterruptedException e) {
// 阻塞等待中被中断,则抛出
e.printStackTrace();
} catch (ExecutionException e) {
// 执行过程发送异常被抛出
e.printStackTrace();
}
}
3.4 通过线程池创建线程
- 自定义 Runnable 接口,实现
run()
方法,创建线程池,调用执行方法并传入对象。 - 优点:安全高性能,复用线程。
- 缺点: Jdk5后才支持,需要结合 Runnable 进行使用。
public class ThreadDemo4 implements Runnable {
@Override
public void run() {
System.out.println("通过线程池+runnable实现多线程,名称:" +
Thread.currentThread().getName());
}
}
public static void main(String[] args) {
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
for(int i=0;i<10;i++){
// 线程池执行线程任务
executorService.execute(new ThreadDemo4());
}
System.out.println("主线程名称:"+Thread.currentThread().getName());
// 关闭线程池
executorService.shutdown();
}
- 一般常用的 Runnable 和 第 四种线程池 + Runnable,简单方便扩展,和高性能 (池化的思想)。
3.5 Runable Callable Thread 三者区别?
- Thread 是一个抽象类,只能被继承,而 Runable、Callable 是接口,需要实现接口中的方法。
- 继承 Thread 重写
run()
方法,实现Runable接口需要实现run()
方法,而Callable是需要实现call()
方法。 - Thread 和 Runable 没有返回值,Callable 有返回值。
- 实现 Runable 接口的类不能直接调用
start()
方法,需要 new 一个 Thread 并发该实现类放入 Thread,再通过新建的 Thread 实例来调用start()
方法。 - 实现 Callable 接口的类需要借助 FutureTask (将该实现类放入其中),再将 FutureTask 实例放入 Thread,再通过新建的 Thread 实例来调用
start()
方法。获取返回值只需要借助 FutureTask 实例调用get()
方法即可!
4、请你说一下线程的几个状态(生命周期)?
线程通常有五种状态,新建、就绪、运行、阻塞和死亡状态:
- 新建状态(New):线程刚被创建,但尚未启动。如:
Thread t = new Thread();
- 就绪状态(Runnable):当调用线程对象的
start()
方法后,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()
此线程立即就会执行。 - 运行状态(Running):当 CPU 开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
- 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 等待阻塞 :运行的线程执行
wait()
方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()
或notifyAll()
方法才能被唤 醒,wait()
是 Object 类的方法。 - 同步阻塞:当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中。
- 其他阻塞状态:当前线程执行了
sleep()
方法,或者调用了其他线程的join()
方法,或者发出了 I/O 请求时,就会进入这个状态。线程会进入到阻塞状态。当sleep()
状态超时、join()
等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入就绪状态。
- 等待阻塞 :运行的线程执行
- 死亡状态(Dead):线程执行完了或者因异常退出了
run()
方法,该线程结束生命周期。
5、请你说一下线程状态转换相关方法:sleep/yield/join wait/notify/notifyAll 的区别?
Thread下的方法:
sleep()
:属于线程 Thread 的方法,让线程暂缓执行,等待预计时间之后再恢复,交出CPU使用权,不会释放锁,抱着锁睡觉!进入超时等待状态TIME_WAITGING,睡眠结束变为就绪Runnableyield()
:属于线程 Thread 的方法,暂停当前线程的对象,去执行其他线程,交出CPU使用权,不会释放锁,和sleep()
类似,让相同优先级的线程轮流执行,但是不保证一定轮流,- 注意:不会让线程进入阻塞状态 BLOCKED,直接变为就绪 Runnable,只需要重新获得CPU使用权。
join()
:属于线程 Thread 的方法,在主线程上运行调用该方法,会让主线程休眠,不会释放锁,让调用join()
方法的线程先执行完毕,再执行其他线程。类似让救护车警车优先通过!!
Object下的方法:
wait()
:属于 Object 的方法,当前线程调用对象的wait()
方法,会释放锁,进入线程的等待队列,需要依靠notify()
或者notifyAll()
唤醒,或者wait(timeout)
时间自动唤醒。notify()
:属于 Object 的方法,唤醒在对象监视器上等待的单个线程,随机唤醒。notifyAll()
:属于Object 的方法,唤醒在对象监视器上等待的全部线程,全部唤醒
线程状态转换流程图
6、Thread 调用 start() 方法和调用 run() 方法的区别
run()
:普通的方法调用run()
函数,在主线程中执行,不会新建一个线程来执行。
start()
:新启动一个线程,这时此线程处于就绪(可运行)状态,并没有真正运行,一旦得到 CPU 时间片,就调用 run()
方法执行线程任务。
7、线程池的核心属性有哪些?
使用线程池的好处:
重用存在的线程,减少对象创建销毁的开销,有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,且可以定时定期执行、单线程、并发数控制,配置任务过多任务后的拒绝策略等功能。
类别:
newFixedThreadPool
:一个定长线程池,可控制线程最大并发数。newCachedThreadPool
:一个可缓存线程池。newSingleThreadExecutor
:一个单线程化的线程池,用唯一的工作线程来执行任务。newScheduledThreadPool
:一个定长线程池,支持定时/周期性任务执行。
【阿里巴巴编码规范】 线程池不允许使用 Executors 去创建,要通过 ThreadPoolExecutor的方式原因?
Executors创建的线程池底层也是调用 ThreadPoolExecutor,只不过使用不同的参数、队列、拒绝策略等如果使用不当,会造成资源耗尽问题。直接使用ThreadPoolExecutor让使用者更加清楚线程池允许规则,常见参数的使用,避免风险。
常见的线程池问题:
1.newFixedThreadPool和newSingleThreadExecutor:
队列使用LinkedBlockingQueue,队列长度为 Integer.MAX_VALUE,可能造成堆积,导致OOM
2.newScheduledThreadPool和newCachedThreadPool:
线程池里面允许最大的线程数是Integer.MAX_VALUE,可能会创建过多线程,导致OOM
ThreadPoolExecutor构造函数里面的参数,能否解释下各个参数的作用?
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
corePoolSize
:核心线程数,线程池也会维护线程的最少数量,默认情况下核心线程会一直存活,即使没有任务也不会受存 keepAliveTime 控制!
坑:在刚创建线程池时线程不会立即启动,到有任务提交时才开始创建线程并逐步线程数目达到 corePoolSize。 -
maximumPoolSize
:线程池维护线程的最大数量,超过将被阻塞!
坑:当核心线程满,且阻塞队列也满时,才会判断当前线程数是否小于最大线程数,才决定是否创建新线程 -
keepAliveTime
:非核心线程的闲置超时时间,超过这个时间就会被回收,直到线程数量等于 corePoolSize。 -
unit
:指定 keepAliveTime 的单位,如 TimeUnit.SECONDS、TimeUnit.MILLISECONDS -
workQueue
:线程池中的任务队列,常用的是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。 -
threadFactory
:创建新线程时使用的工厂 -
handler
:RejectedExecutionHandler 是一个接口且只有一个方法,线程池中的数量大于 maximumPoolSize,对拒绝任务的处理策略,默认有 4 种策略:AbortPolicy
CallerRunsPolicy
DiscardOldestPolicy
DiscardPolicy
8、你知道线程池有哪些拒绝策略吗?
-
AbortPolicy
:中止策略。默认的拒绝策略,直接抛出 RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。 -
DiscardPolicy
:抛弃策略。什么都不做,直接抛弃被拒绝的任务。 -
DiscardOldestPolicy
:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。 -
CallerRunsPolicy
:调用者运行策略。在调用者线程中执行该任务。该策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务。
9、请你简单描述一下线程池的运作流程
图片参考:https://joonwhee.blog.csdn.net/article/details/115364158
10、请问Java中可以有哪些方法来保证线程安全?
- 加锁:比如synchronize/ReentrantLock。
- 使用 volatile 声明变量,轻量级同步,不能保证原子性(需要解释)。
- 使用线程安全类,例如原子类 AtomicXXX等。
- 使用线程安全集合容器,例如:CopyOnWriteArrayList/ConcurrentHashMap等。
- ThreadLocal本地私有变量/信号量 Semaphore等。
总结的面试题也挺费时间的,文章会不定时更新,有时候一天多更新几篇,如果帮助您复习巩固了知识点,还请三连支持一下,后续会亿点点的更新!
为了帮助更多小白从零进阶 Java 工程师,从CSDN官方那边搞来了一套 《Java 工程师学习成长知识图谱》,尺寸 870mm x 560mm
,展开后有一张办公桌大小,也可以折叠成一本书的尺寸,有兴趣的小伙伴可以了解一下,当然,不管怎样博主的文章一直都是免费的~
以上是关于(Java实习生)每日10道面试题打卡——Java多线程篇的主要内容,如果未能解决你的问题,请参考以下文章
(Java实习生)每日10道面试题打卡——Java简单集合篇