并发编程【60道】
七、Java并发编程面试题
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/weixin_45483322/article/details/132363273
1.并行跟并发有什么区别?
从操作系统的角度来看,线程是CPU分配的最小单位。
并行就是同一时刻,两个线程都在执行。这就要求有两个CPU去分别执行两个线程。 并发就是同一时刻,只有一个执行,但是一个时间段内,两个线程都执行了。并发的实现依赖于CPU切换线程,因为切换的时间特别短,所以基本对于用户是无感知的。
就好像我们去食堂打饭,并行就是我们在多个窗口排队,几个阿姨同时打菜;并发就是我们挤在一个窗口,阿姨给这个打一勺,又手忙脚乱地给那个打一勺。
2.说说什么是进程和线程?
要说线程,必须得先说说进程。
进程:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。 线程:线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。 操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是 CPU分配的基本单位。
比如在Java中,当我们启动main函数其实就启动了一个JVM进程,而main函数在的线程就是这个进程中的一个线程,也称主线程。
一个进程中有多个线程,多个线程共用进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈。
3.说说线程有几种创建方式?
Java中创建线程主要有三种方式,分别为继承Thread类、实现Runnable接口、实现Callable接口。
- 继承Thread类,重写run() 方法,调用start() 方法启动线程
public class ThreadTest {
/**
* 继承Thread类
*/
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("This is child thread");
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
- 实现Runnable接口,重写run() 方法
public class RunnableTask implements Runnable {
public void run() {
System.out.println("Runnable!");
}
public static void main(String[] args) {
RunnableTask task = new RunnableTask();
new Thread(task).start();
}
}
上面两种都是没有返回值的,但是如果我们需要获取线程的执行结果,该怎么办呢?
- 实现Callable接口,重写call() 方法,这种方式可以通过FutureTask获取任务执行的返回值
public class CallerTask implements Callable<String> {
public String call() throws Exception {
return "Hello,i am running!";
}
public static void main(String[] args) {
//创建异步任务
FutureTask<String> task = new FutureTask<String>(new CallerTask());
//启动线程
new Thread(task).start();
try {
//等待执行完成,并获取返回结果
String result = task.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
4.为什么调用start()方法时会执行run()方法,那怎么不直接调用run()方法?
JVM执行start方法,会先创建一条线程,由创建出来的新线程去执行thread的run方法,这才起到多线程的效果。
为什么我们不能直接调用run() 方法?也很清楚,如果直接调用Thread的run() 方法,那么run方法还是运行在主线程中,相当于顺序执行,就起不到多线程的效果。
5.线程有哪些常用的调度方法?
线程等待与通知
在Object类中有一些函数可以用于线程的等待与通知。
wait():当一个线程A调用一个共享变量的 wait()方法时,线程A会被阻塞挂起,发生下面几种情况才会返回 (1)线程A调用了共享对象notify()或者 notifyAll()方法; (2)其他线程调用了线程A的interrupt()方法,线程A抛出InterruptedException异常返回。
wait(long timeout):这个方法相比wait()方法多了一个超时参数,它的不同之处在于,如果线程A调用共享对象的wait(long timeout)方法后,没有在指定的timeout ms时间内被其它线程唤醒,那么这个方法还是会因为超时而返回。
wait(long timeout, int nanos),其内部调用的是 wait(long timout)函数。 上面是线程等待的方法,而唤醒线程主要是下面两个方法:
notify():一个线程A调用共享对象的notify()方法后,会唤醒一个在这个共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
notifyAll():不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。 Thread类也提供了一个方法用于等待的方法:
join():如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。
线程休眠
- sleep(long millis):Thread类中的静态方法,当一个执行中的线程A调用了Thread的sleep方法后,线程A会暂时让出指定时间的执行权,但是线程A所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,接着参与CPU的调度,获取到CPU资源后就可以继续运行。
让出优先权
- yield():Thread类中的静态方法,当一个线程调用 yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU,但是线程调度器可以无条件忽略这个暗示。
线程中断
Java中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
- void interrupt():中断线程,例如,当线程A运行时,线程B可以调用线程interrupt()方法来设置线程的中断标志为true 并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,会继续往下执行。
- boolean isInterrupted()方法:检测当前线程是否被中断。
- boolean interrupted()方法:检测当前线程是否被中断,与 isinterrupted 不同的是,该方法如果发现当前线程被中断,则会清除中断标志。
6.线程有几种状态?
在Java中,线程共有六种状态:
线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java线程状态变化如图示:
7.什么是线程上下文切换?
使用多线程的目的是为了充分利用CPU,但是我们知道,并发其实是一个CPU来应付多个线程。
为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换。
8.守护线程了解吗?
Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。
用户线程是虚拟机启动的线程中的普通线程,当所有用户线程结束运行后,虚拟机才会停止运行,即使还有一些守护线程在运行。
守护线程是在程序中创建的线程,它的作用是为其他线程提供服务。当所有的用户线程结束运行后,守护线程也会随之结束,而不管它是否执行完毕。守护线程通常用于执行一些辅助性任务,如垃圾回收、缓存清理等,它们不需要等待所有的任务完成后再退出。
它们之间的区别在于虚拟机在何时结束进程。
那么守护线程和用户线程有什么区别呢?
当最后一个非守护线程束时,JVM会正常退出,而不管当前是否存在守护线程,也就是说守护线程是否结束并不影响JVM退出。
换而言之,只要有一个用户线程还没结束,正常情况下JVM就不会退出。
9.线程间有哪些通信方式?
volatile和synchronized关键字:
关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
等待/通知机制:
可以通过Java内置的等待/通知机制(wait()/notify())实现一个线程修改一个对象的值,而另一个线程感知到了变化,然后进行相应的操作。
管道输入/输出流:
- 管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。
- 管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream,PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。
使用Thread.join():
Thread.join():join()的作用是“等待该进程终止”,也就是在子线程调用了join()方法后,主线程后面的代码要等到子线程结束了才能执行。一般应用于一个线程的输入可能依赖于另一个或者多个线程的输出,此时这个线程就需要等待依赖线程执行完毕才能继续执行。
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。
线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。
使用ThreadLocal:
- ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
- 可以通过set(T) 方法来设置一个值,在当前线程下再通过get() 方法获取到原先设置的值。
关于多线程,其实很大概率还会出一些笔试题,比如交替打印、银行转账、生产消费模型等等
ThreadLocal
ThreadLocal其实应用场景不是很多,但却是被炸了千百遍的面试老油条,涉及到多线程、数据结构、JVM,可问的点比较多,一定要拿下。