首页 > 面试必会!Java后端开发岗位并发部分高频面试题分享
头像
已加入本公司人才库
编辑于 2021-04-19 10:30
+ 关注

面试必会!Java后端开发岗位并发部分高频面试题分享

问题来源于多篇面经,答案都是自己手动收集整理的,有任何错漏之处欢迎指正~


进程和线程的区别?

进程是系统资源调度的最小单位;线程是CPU调度的最小单位
进程是比线程更大的概念,进程之间彼此独立,一个进程里可以包含多个线程,多线程共享一部分资源,因此彼此相互影响比多进程更大
多线程共享一部分进程的内存区域,也有自己私有的区域
进程在执行时拥有独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。



进程和线程消耗什么系统资源?

线程共享的包括:
进程ID,优先级
代码区:代码编译后的可执行机器指令。
数据区:进程地址空间中的数据区,这里存放的是全局变量
堆区:new出来的数据就存放在这个区域

线程独立拥有:
线程id:用于区分每个线程
线程优先级:用于调度
1.栈区:栈帧中保存了函数的调用嵌套、返回值、调用其它函数的参数、该函数使用的局部变量以及该函数使用的寄存器信息
2.程序计数器:接下来执行那一条指令
3.寄存器:进程运行时的计算数据,进程切换时必须保存这些数据

i++是原子性的么?怎么保证原子性?

i++不具有原子性,因为在+1之前可能其他线程改变了i的值
通过JUC中的Atomic,或者使用锁保证原子性


什么是上下⽂切换?

⼀般线程的个数都⼤于 CPU 核⼼的个数,⽽⼀个 CPU 核⼼在任意时刻只能被⼀个线程使⽤,为了让这些线程都能得到执⾏, CPU 轮流给每个线程分配时间⽚。当⼀个线程的时间⽚⽤完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就是⼀次上下⽂切换。
当前任务在执⾏完 CPU 时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。 任务从保存到再加载的过程就是⼀次上下⽂切换。


如何减少上下文切换?

1.避免锁的使用。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
2.CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。使用最少线程。
3.避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
4.协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。


线程通信的方式和区别?

1.共享对象。在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。
例如synchronized关键字进行线程同步,实现通信
2.while轮询,不断检测某个条件是否成立,比较浪费CPU资源,
此外存在可见性问题,线程都是先把变量读取到本地线程栈空间,然后再去再去修改的本地变量。因此,如果线程B每次都在取本地的 条件变量,那么尽管另外一个线程已经改变了轮询的条件,它也察觉不到,这样也会造成死循环。
3.wait/notify机制
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify()。缺点是当一个线程还未开始执行时,过早的notify是无效的。
4管道通信。就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信

Java线程生命周期和切换过程

状态类型:
1.new        初始,线程被创建,未调用start()
2.runnable    运行(java将os中的就绪和运行状态统称:运行中)
3.blocked    阻塞(被锁阻塞)
4.waiting    等待(等待其他线程通知或中断,wait()进入notify()或者notifyAll()可以唤醒,a.join()会在进程a结束后唤醒)
5.time_waiting    超时等待(超过指定时间自行唤醒,sleep(),设置了Timeout参数的Object.wait(),Thread.join() )
6.terminated    终止(执行完毕或者发生异常后结束)

状态转换:
1.线程创建之后它将处于 NEW状态,调⽤ start() ⽅法后开始运⾏,进入Runnable(可运⾏) 状态。
2.当线程获得了 cpu 时间⽚(timeslice)后就处于 RUNNING(运⾏) 状态。
(os隐藏了就绪和运行状态,Java统称这两种状态为RUNNABLE)
3.当线程执⾏ wait() ⽅法之后,线程进⼊ WAITING(等待)状态。进⼊等待状态的线程需要依靠其他线程的通知才能够返回到RUNNABLE状态,⽽ TIME_WAITING(超时等待) 设置了等待时间,当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。
4.当线程调⽤同步⽅法时,在没有获取到锁的情况下,线程将会进⼊到 BLOCKED(阻塞) 状态。
5.线程执行完毕后会进⼊到 TERMINATED(终⽌) 状态。


线程状态切换的方式

object类提供的方法,只能在synchronized同步代码块中使用
wait() notify() notifyAll()
Thread类提供的方法
sleep()
join():b线程内调用a.join()会让a先执行完毕,然后b继续执行
yield(),表示当前线程优先级较低,可以切换到其他线程,是对线程调度的建议,而非强制
thraed.setDaemon(true) 方法可以将一个线程设置为守护线程,所有非守护线程结束时会杀死守护线程
juc提供的方法,配合ReentrantLock使用
await() signal() signalAll()
condition.await()可以指定等待的条件,更加灵活


sleep wait的区别和共同点?

sleep来自Thread类,可以在任何地方使用
让线程暂停一段时间,释放cpu,但是不会释放锁,暂停结束后重新等待cpu分配时间片
sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(longtimeout) 超时后线程会⾃动苏醒。
注:
Thread.Sleep(0)的作用,就是触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。
wait是object的方法,主要用于线程通信,wait只能在同步代码块中使用(拿到锁之后才能使用)
让线程进入锁对象的等待池中,进入等待状态,在指定时间内释放cpu和锁,时间结束后恢复,进入阻塞状态,重新竞争锁,没有指定时间就需要其他线程notify


notify()和 notifyAll()有什么区别?

1.如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
2.当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
3.优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。


为什么wait方法和notify方法要在基类,在其他类行不行?

因为 wait 和 notify 方法必须要在同步代码块中才能调用,而同步代码块锁的是对象,调用该对象的 wait 方法,线程会进入该对象的等待池中,和 Thread 无关。

waiting和blocked的区别?Wait超时后会怎样?

进入waiting状态时,会释放锁,并且之后不会主动获取锁,唤醒之后进入阻塞状态,重新尝试获取锁
blocked状态会主动尝试获取锁

Java线程的三种启动方式,优缺点?

1.实现Runable接口,重写run方法
new Thread(new MyRunnable()).start();

2.实现Callable接口,重写call方法,可以获取计算结果
FutureTask<Integer> ft = new FutureTask<>(new Th3());
new Thread(ft).start();
System.out.println(ft.get());

3.继承Thread,覆盖 run() 方法,直接start即可,不需要new Thread
mt.start()

4.匿名内部类创建线程
new Thread(new Runnable() {
public void run() {}
}
}).start();
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。
继承Thread可以理解为一个线程,直接调用start即可
实现接口会更好一些,因为:
Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
类可能只要求可执行就行,继承整个 Thread 类开销过大。


线程run和start的区别,为什么我们调⽤ start() ⽅法时会执⾏ run() ⽅法,为什么我们不能直接调⽤ run() ⽅法?

new ⼀个 Thread,线程进⼊了新建状态。调⽤ start() ⽅***启动⼀个线程并使线程进⼊了就绪状态,当分配到时间⽚后就可以开始运⾏了。
start() 会执⾏线程的相应准备⼯作,然后⾃动执⾏ run() ⽅法的内容,这是真正的多线程⼯作。 但是,直接执⾏ run() ⽅***把 run()⽅法当成⼀个 main 线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。

总结: 调⽤ start() ⽅法⽅可启动线程并使线程进⼊就绪状态,直接执⾏ run() ⽅法的话不会以多线程的⽅式执⾏。

如何结束线程?

interrupt():
如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
thread.start();
thread.interrupt();

interrupted():
如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
但是调用 interrupt() 方***设置线程的中断标记,此时调用 interrupted() 方***返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。

其他用于线程管理的类?

CountDownLatch:
维护一个计数器,每次调用countDown()时计数器-1,当计数器为0时,因调用 await() 方法而在等待的线程就会被唤醒。
CountDownLatch countDownLatch = new CountDownLatch(1);
countDownLatch.await();
countDownLatch.countDown(); //计数器-1
countdownlatch实现原理

CyclicBarrier
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
循环屏障,当阻塞线程达到指定个数时将会全部唤醒它们,然后继续开始计数。
有两个构造函数,其中 parties 指示计数器的初始值,第二个构造方法有一个 Runnable 参数,这个参数的意思是最后一个到达线程要做的任务
CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
cyclicBarrier.await(); //每次调用await时计数器-1,计数器为0时唤醒之前阻塞的线程


Semaphore
可以控制对互斥资源的访问线程数。
Semaphore semaphore = new Semaphore(3); //最大同时访问数3
semaphore.acquire(); //获取,获取不到则阻塞
semaphore.tryAcquire(); //尝试获取
semaphore.release(); //释放当前资源,计数器+1
semaphore.availablePermits() //输出剩余可访问数


LockSupport
可以让线程在任意位置阻塞和唤醒线程。和wait notify的区别有两点:
(1)wait和notify都是Object中的方法,在调用这两个方法前必须先获得锁对象,但是park不需要获取某个对象的锁就可以锁住线程。
(2)notify只能随机选择一个线程唤醒,无法唤醒指定的线程,unpark却可以唤醒一个指定的线程。
LockSupport.park(); //阻塞当前线程
LockSupport.unpark(t2); //唤醒指定线程

线程池是什么,好处?

1.降低资源消耗。通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗。
2.提⾼响应速度。当任务到达时,任务可以不需要的等到线程创建就能⽴即执⾏。
3.提⾼线程的可管理性。线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统⼀的分配,调优和监控。

重复创建线程为什么会开销过大?

1.必须为线程堆栈分配并初始化一大块内存。
2.需要进行系统调用,系统从用户态切换到内核态,创建和注册新线程。
3.需要创建,初始化描述符并将其添加到JVM内部数据结构中。


线程池的创建方法和参数,等待队列,拒绝策略

我们可以使用ThreadPoolExecutor来创建线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();

我们可以看出创建线程池有七个参数,而上述我们通过Executors工具类来创建的线程池就一两个参数,其他参数它都帮我们默认写死了,我们只有真正理解了这几个参数才能更好的去使用线程池。下面我们来看看这七个参数(线程池参数)

1.corePoolSize
核心线程数(线程池的基本大小)当我们提交一个任务到线程池时就会创建一个线程来执行任务.当我们需要执行的任务数大于核心线程数了就不再创建, 如果运行的线程少于 corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;如果我们调用了prestartAllCoreThreads()方法线程池就会为我们提前创建好所有的基本线程。

2.maximumPoolSize
最大线程数:线程池允许创建的最大线程数。如果队列已经满了,且已创建的线程数小于最大线程数,则线程池就会创建新的线程来执行任务。这里有个小知识点,如果我们的队列是用的无界队列,这个参数是不会起作用的,因为我们的任务会一直往队列中加,队列永远不会满(内存允许的情况)。

3.keepAliveTime
空闲线程最大生存时间。当前线程数大于核心线程数时,结束多余的空闲线程等待新任务的最长时间,直到线程池中的线程数不超过核心线程数。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。

4.unit
线程存活时间的的单位。可选的单位有days、hours等。

5.workQueue
任务队列。主要分为三种:
1.直接握手队列: 队列容量为0,,如果没有线程立即可用来运行它,将不会进入队列,而是直接构建新的线程来运行它。一般最大线程数无限制,因此要注意线程数无限增长的问题。( 此策略在处理可能具有内部依赖关系的请求集时避免锁定)
2.无界队列:队列容量为无限,当核心线程都在运行任务时,新任务都将进入队列等待,此时最大线程数的设置是无效的。当任务的处理速度小于增加速度时,可能出现队列无限增长的问题。(可以用于平滑瞬时大量请求)
3.有界队列:队列容量有限,有助于防止资源耗尽,但是难以控制。队列大小和最大线程数需要相互权衡

大队列和较小的最大线程数可以地减少CPU使用率,操作系统资源和上下文切换开销,但会导致人为的低吞吐量。如果任务经常被阻塞(比如I/O限制),那么系统可以调度比我们允许的更多的线程。
使用小队列通常需要较大的maximumPoolSizes,这会使CPU更繁忙,但可能会遇到不可接受的调度开销,这也会降低吞吐量。

ArrayBlockingQueue:数组实现的有界阻塞队列,先进先出,支持公平锁和非公平锁
LinkedBlockingQueue:链表实现,先进先出,默认长度为Integer.MAX_VALUE,默认长度有OOM风险
LinkedBlockingDeque:链表实现的双向阻塞队列,队列头尾可以增删元素,并发环境下可以将锁的竞争最多降低到一半
PriorityBlockingQueue:支持优先级排序的无界队列,默认自然顺序,重写compareTo()方法定义排序规则,不保证同优先级元素的顺序
DelayQueue:实现PriorityBlockingQueue实现延迟获取的无界队列,创建元素时可以指定间隔多久才能从队列中取出该元素。
SynchronousQueue:不存储元素的阻塞队列,每个put操作必须等待take操作,否则无法添加元素,支持公平锁和非公平锁,一个使用场景是Executors.newCachedThreadPool()会在新任务到来时创建新线程,有空闲线程会重复使用,空闲超过60s后回收
LinkedTransferQueue:链表实现的无界阻塞队列,多了transfer和tryTransfer方法

6.threadFactory
用户设置创建线程的工厂,我们可以通过这个工厂来创建有业务意义的线程名字。我们可以对比下自定义的线程工厂和默认的线程工厂创建的名字。
默认产生线程的名字:pool-5-thread-1
自定义线程工厂产生名字:testPool-1-thread-1

7.RejectedExecutionHandler
线程池拒绝策略。线程池关闭或队列和最大线程数已满时都会触发。当队列和线程池都满了说明线程池已经处于饱和状态。必须要采取一定的策略来处理新提交的任务。jdk默认提供了四种拒绝策略:
ThreadPoolExecutor.AbortPolicy:默认策略,丢弃任务并抛出RejectedException异常。适合比较关键的业务,及时发现并发容量上界
ThreadPoolExecutor.DiscardPolicy:丢失任务且不抛出异常,适合无关紧要的业务
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,重新提交被拒绝的任务。
ThreadPoolExecutor.CallerRunsPolicy:由线程池所在的线程处理该任务

参数如何设置?

1.一般我们如果任务为耗时IO型比如读取数据库、文件读写以及网络通信的的话这些任务不会占据很多cpu的资源但是会比较耗时:线程数设置为2倍CPU数以上,充分的来利用CPU资源。
2.一般我们如果任务为CPU密集型的话比如大量计算、解压、压缩等这些操作都会占据大量的cpu。所以针对于这种情况的话一般设置线程数为:1倍cpu+1。为啥要加1,很多说法是备份线程。又说留个空的做切换。
3.如果既有IO密集型任务,又有CPU密集型任务,这种该怎么设置线程大小?这种的话最好分开用线程池处理,IO密集的用IO密集型线程池处理,CPU密集型的用cpu密集型处理。以上都只是理算情况下的估算而已,真正的合理参数还是需要看看实际生产运行的效果来合理的调整的。

核心线程,最大线程数,判断执行流程?

任务提交时,判断的顺序为 corePoolSize --> workQueue --> maximumPoolSize。
1.如果运行线程少于核心线程数,则创建新线程来处理任务,即使线程池中的其他线程是空闲的
如果我们调用了prestartAllCoreThreads()方法线程池就会为我们提前创建好所有的基本线程。
2.如果线程池中的线程数量大于等于核心线程数且小于最大线程数(动态修改了coreSize才会出现这种情况),任务来了首先放在队列中, 只有当队列满时才创建新的线程处理任务。如果设置的核心线程数 和 最大线程数 相同,则创建的线程池的大小是固定的,这时如果有 新任务提交,若工作队列未满,则将请求放入工作队列中,等待有空闲的线程去从工作队列中取任务并处理;
3.如果运行的线程数量大于等于最大线程数,这时如果工作队列已经满了,则通过handler所指定的拒绝策略来处理任务;


自带的4种线程池为什么不推荐使用?

一般不使用Executors创建线程池,
1.FixedThreadPool和SingleThreadPool,允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2.CachedThreadPool和ScheduledThreadPool允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
建议使用 new ThreadPoolExecutor(),自定义7个参数。

1.SingleThreadExecutor
单线程线程池,可以保证任务顺序执行,还可以进行生命周期管理。
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
final int j = i;
executorService.execute(() -> {
System.out.println(j + " " + Thread.currentThread().getName());
});
}
executorService.shutdown();

2.CachedThreadPool
CachedThreadPool是一个没有核心线程的线程池,当新任务来临时,如果非核心线程在忙碌,则直接新创建一个线程。SynchronousQueue是一个手把手传递的一个阻塞队列,可见上面的教程。特点是任务基本不需要等待?
//核心线程数0,最大线程数无限,等待时间60s,不存储元素的等待队列
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
ExecutorService service = Executors.newCachedThreadPool();

3.FixedThreadPool
FixedThreadPool是一个固定线程数的线程池。
ExecutorService service = Executors.newFixedThreadPool(cpuCoreNum);

4.ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor是定时任务线程池,其本质上依然是ThreadPoolExecutor。
ScheduledExecutorService service = Executors.newScheduledThreadPool(4);


线程池如何保证核心线程不被销毁?线程池的线程是如何做到复用的?

线程内部有while循环不停的执行getTask方法从阻塞队列中获取任务调用task.run()来执行任务,这样的话就达到了线程复用的目的。
//有任务,或者从队列中获取到了任务,则执行,否则
while (task != null || (task = getTask()) != null)
getTask从队列中的take方法获取任务,如果为空,take方法则一直阻塞当前线程,保证线程不会挂掉。

线程池里的某个线程发生了异常,会发生什么?

当一个线程池里面的线程异常后:
当执行方式是execute时,可以看到堆栈异常的输出。
当执行方式是submit时,堆栈异常没有输出。但是调用Future.get()方法时,可以捕获到异常。
不会影响线程池里面其他线程的正常执行。
线程池会把这个线程移除掉,并创建一个新的线程放到线程池中。

全部评论

(3) 回帖
加载中...
话题 回帖

推荐话题

相关热帖

热门推荐