多线程常见面试题(含常见项目遇到多线程问题解决及面试对话)

纯面试 纯文字 看起来乱但适合面试总结

多线程

什么是线程和进程?他们是什么关系?

进程:在操作系统中能够独立运行,并且作为资源分配的基本单位。它表示运行中的程序。系统运行一个程序就是一个进程从创建、运行到消亡的过程。

线程:是一个比进程更小的执行单位,能够完成进程中的一个功能,也被称为轻量级进程。一个进程在其执行的过程中可以产生多个线程。

  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
  • 进程要想执行任务,必须得有线程,进程至少要有一条线程

多线程有什么用?

  1. 发挥多核CPU的优势
  2. 防止阻塞
  3. 便于建模

创建线程有几种方式?

  1. 继承Thread类
  2. 实现Runnable接口
  3. 使用Callable和Future创建线程
  4. 使用Executor框架创建线程池

(推荐使用实现Runnable,因为继承只能继承一个,而实现了接口还能可以继承)

并发编程三要素

1)原子性

原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

2)可见性

可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

3)有序性

有序性,即程序的执行顺序按照代码的先后顺序来执行。

多线程同步有哪几种方法?

Synchronized关键字,Lock锁实现,分布式锁等。

Thread 类中的start() 和 run() 方法有什么区别?

这个问题经常被问到,但还是能从此区分出面试者对Java线程模型的理解程度。start()方法被用来启动新创建的线程,而且start()内部 调用了run()方法,这和直接调用run()方法 的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启 动,start()方法才会启动新线程。start()方法会将新创建的线程交给CPU去调度,CPU可以通过 轮转时间片去执行这个线程,至于是否分给该线程时间片那就看CPU了。

Java中线程的各种状态

针对操作系统来说,线程有五种状态

  1. 新建状态:即单纯地创建一个线程。
  2. 就绪状态:在创建了线程之后,调用Thread类的start()方法来启动一个线程,即表示线程进入就绪状态!
  3. 运行状态:当线程获得CPU时间片,线程才从就绪状态进入到运行状态!
  4. 阻塞状态:线程进入运行状态后,可能由于多种原因让线程进入阻塞状态,如:调用sleep()方法让线程睡眠,调用wait()方法让线程等待,调用join()方法、suspend()方法(它现已被弃用!)以及阻塞式IO方法。
  5. 死亡状态:run()方法的正常退出就让线程进入到死亡状态,还有当一个异常未被捕获而终止了run()方法的执行也将进入到死亡状态!

sleep()和wait()的区别,调用这两个函数后,线程状态分别作何改变

① 这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。 sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。

② 锁: 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。 sleep不让出系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用, 要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来, 如果时间不到只能调用interrupt()强行打断。 Thread.sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。

③ 使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。 synchronized(x){ x.notify() //或者wait() } 两者的线程状态都从运行—> 阻塞,不同的是sleep方法在阻塞的同时还带着锁,wait方法在阻塞

volatile的三大特性

  1. 禁止指令重排序 : 添加volatile的变量,在调用的时候,该变量之前的代码不会进行指令重排序,原理是加了一个写屏障。还有一个是读屏障,在调用该变量之后的代码不会进行 指令重排序,读屏障是对变量的读操作,写屏障是对变量的写操作。
  2. 不保证原子性 : 指令交错
  3. 保证可见性 : 一个线程对主存的数据进行了更改,对另外一个线程不可见,加了volatile关键字修饰的变量,会每次都从主内存中获取数据, 而不是去自己的工作内存中获取缓存数据,工作内存和主内存请见Java内存模型

多线程开发带来的问题与解决方法

(一)线程安全问题

线程安全问题指的是在某一线程从开始访问到结束访问某一数据期间,该数据被其他的线程所修改,那么对于当前线程而言,该线程就发生了线程安全问题,表现形式为数据的缺失,数据不一致等

线程安全问题发生的条件:

1)多线程环境下,即存在包括自己在内存在有多个线程。

2)多线程环境下存在共享资源,且多线程操作该共享资源。

3)多个线程必须对该共享资源有非原子性操作。

线程安全问题的解决思路:

1)尽量不使用共享变量,将不必要的共享变量变成局部变量来使用

2)使用synchronized关键字同步代码块,或者使用jdk包中提供的Lock为操作进行加锁

3)使用ThreadLocal为每一个线程建立一个变量的副本,各个线程间独立操作,互不影响

(二)性能问题

线程的生命周期开销是非常大的,一个线程的创建到销毁都会占用大量的内存。同时如果不合理的创建了多个线程,cup的处理器数量小于了线程数量,那么将会有很多的线程被闲置,闲置的线程将会占用大量的内存,为垃圾回收带来很大压力,同时cup在分配线程时还会消耗其性能。

解决思路:

利用线程池,模拟一个池,预先创建有限合理个数的线程放入池中,当需要执行任务时从池中取出空闲的先去执行任务,执行完成后将线程归还到池中,这样就减少了线程的频繁创建和销毁,节省内存开销和减小了垃圾回收的压力。同时因为任务到来时本身线程已经存在,减少了创建线程时间,提高了执行效率,而且合理的创建线程池数量还会使各个线程都处于忙碌状态,提高任务执行效率,线程池还提供了拒绝策略,当任务数量到达某一临界区时,线程池将拒绝任务的进入,保持现有任务的顺利执行,减少池的压力。

(三)活跃性问题

1)死锁假如线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。多个线程环形占用资源也是一样的会产生死锁问题。

解决方法:

  • 避免一个线程同时获取多个锁
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用 lock.tryLock(timeout) 来代替使用内部锁机制。

想要避免死锁,可以使用无锁函数(cas)或者使用重入锁(ReentrantLock),通过重入锁使线程中断或限时等待可以有效的规避死锁问题

2)饥饿,饥饿指的是某一线程或多个线程因为某些原因一直获取不到资源,导致程序一直无法执行。如某一线程优先级太低导致一直分配不到资源,或者是某一线程一直占着某种资源不放,导致该线程无法执行等。

解决方法:

与死锁相比,饥饿现象还是有可能在一段时间之后恢复执行的。可以设置合适的线程优先级来尽量避免饥饿的产生

3)活锁,活锁体现了一种谦让的美德,每个线程都想把资源让给对方,但是由于机器“智商”不够,可能会产生一直将资源让来让去,导致资源在两个线程间跳动而无法使某一线程真正的到资源并执行,这就是活锁的问题。

(四)阻塞

阻塞是用来形容多线程的问题,几个线程之间共享临界区资源,那么当一个线程占用了临界区资源后,所有需要使用该资源的线程都需要进入该临界区等待,等待会导致线程挂起,一直不能工作,这种情况就是阻塞如果某一线程一直都不释放资源,将会导致其他所有等待在这个临界区的线程都不能工作。当我们使用synchronized或重入锁时,我们得到的就是阻塞线程,如论是synchronized或者重入锁,都会在试图执行代码前,得到临界区的锁,如果得不到锁,线程将会被挂起等待,知道其他线程执行完成并释放锁且拿到锁为止。

解决方法:

可以通过减少锁持有时间,读写锁分离,减小锁的粒度,锁分离,锁粗化等方式来优化锁的性能

synchronized锁升级的过程

在jdk1.6之后引入了锁升级的机制,在1.6之前,synchronized只是一把重量级的锁,要么就是没有锁要么就是全部给,现在不是了,现在分成了四种情况,无锁,偏向锁,然后是轻量级锁和重量级锁,然后无锁就是没有锁的过程,然后这个偏向锁呢它主要是针对一种情况就是,你有一个资源,然后有一些线程去申请,但是这个一些他不是一些,它是只有一个线程去申请了,并且这个线程已经申请到了这个锁,没有其他的线程跟它去争抢这个锁,那么当它进行同步代码块的执行的时候,它就没有必要释放这个锁了,因为它释放锁的过程是需要从用户态转换到内核态的,然后这个过程是需要消耗很多资源的,就是说没有必要释放,然后为了提高性能呢就引入了这个偏向锁,于是当有一个线程去执行,像有资源去争夺这个资源的时候,它就不释放锁了,继续持有这个锁,然后另外的话它就轻量级锁可以从偏向锁去升级到轻量级锁,如果有两个线程去争夺这个资源的话,那么那个偏向锁就会升级的,升级到轻量级锁,轻量级锁它用的是CAS,CAS它先去判断一下这个资源还是否能抢占得到,如果抢占不到的话,它就会进行一段忙等待时间,也是自旋操作,自旋操作一段时候还没有办法获取这个锁的话,那么它就会升级到就是 一个重量级锁了,然后重量级锁就是用一种就像管程实现的,然后或者是那个底层操作什么的,这就是重量级的了,就完全阻塞了

那么CAS会有什么问题

如果CAS失败,会一直循环,如果CAS一直不成功,可能会给CPU带来很大的开销

CAS会有ABA的问题,CAS它也是一个缩写,是Compare and Swap,然后它是有三个值的,一个是预期值,然后还有一个是新的值,然后它先去判断一下当前的这个数据是否和自己的预期是一样的,如果是一样的话说明没有被改变过,然后它就把新的值写入进去,但是如果是一种情况就是,先把这个数据变成A然后变成B再变成A,其实它中间是有一个变成B的过程的,但是如果是用这个CAS判断的话,它判断的是两个A,它就会不能发现这个变幻的过程,这是它的一个问题。但是一般来讲的话CAS都会用版本号或者是时间戳来实现,就可能避免这个问题了,另外就是原子类它底层也是用这个CAS来实现的,所以说原子类它只有增长和减少,它都是自己封装好的并不会让你自己去实现的。(JUC后面对原子类有一个优化一个叫做LongAdder的,也是CAS但是有优化的地方)

notify和notifyAll有什么区别

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

Thread类中的start()和run()方法有什么不同?

start()方法被用来启动新创建的线程,而start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程

死锁的原因

1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。

例如:线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。

2)默认的锁申请操作是阻塞的。

所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。

怎么唤醒一个阻塞的线程

如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

Runnable接口和Callable接口的区别

两者最大的区别,实现Callable接口的任务线程能返回执行结果,而实现Runnable接口的任务线程不能返回执行结果

Callable接口实现类中run()方法允许将异常向上抛出,也可以直接在内部处理(try…catch); 而Runnable接口实现类中run()方法的异常必须在内部处理掉,不能向上抛出

Threadlocal

用过ThreadLocal吗?知道它什么原理吗?

Thread类里面都包含了一个Map,然后每个Map就对应了一个ThreadLocal,我觉得它写的特别厉害,然后这样的话它每次调用一个就是,ThreadLocal的一个类的话它就会调用那个ThreadMap里面的具体的那个ThreadLocal,这样的话它每个线程都对应自己的一个Local的那个对象,

它与synchronied是为了解决多线程下,同时访问共享变量而造成的数据不一致

ThreadLocalMap它的key跟value是什么

key是当前ThreadLocal对象 value是线程存入的对象

ThreadLocal在父子线程之间是可以继承的吗?

不可以,InheritableThreadLocal才可以

你项目里面是怎么使用多线程的?

在进行导入导出Excel时,可能会有1W多个表导出,这时用多线程同时导出到不同的表单

用多线程接收大量的json数据

小题提交、视频审核、错题库

MQ那个地方就是用到多线程

当你Excel大量导出导致堆栈溢出怎么办?

设置-Xss,就是栈内存的大小,设置的栈的大小决定了函数调用的最大深度

-Xss设置的大小决定了函数调用的深度,如果函数调用的深度大于设置的Xss大小,那么将会套

“java.lang.StackOver”

线程池

为什么要用线程池

线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处:

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

线程池的参数及要点

多线程常见面试题(含常见项目遇到多线程问题解决及面试对话)_第1张图片

实现Runnable接口和Callable接口的区别

如果想让线程池执行任务的话需要实现的Runnable接口或Callable接口。 Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果。

你可能感兴趣的:(面试,Java,面试,java,多线程)