1、 死锁的发生原因和怎么避免
死锁,简单来说就是两个或者两个以上的线程在执行的过程中,争夺同一个共享
资源造成的相互等待的现象。
如果没有外部干预,线程会一直阻塞无法往下执行,这些一直处于相互等待资源
的线程就称为死锁线程。
导致死锁的条件有四个,也就是这四个条件同时满足就会产生死锁。
互斥条件,共享资源 X 和 Y 只能被一个线程占用;
请求和保持条件,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不
释放共享资源 X;
不可抢占条件,其他线程不能强行抢占线程 T1 占有的资源;
循环等待条件,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的
资源,就是循环等待。
导致死锁之后,只能通过人工干预来解决,比如重启服务,或者杀掉某个线程。
所以,只能在写代码的时候,去规避可能出现的死锁问题。
按照死锁发生的四个条件,只需要破坏其中的任何一个,就可以解决,但是,互
斥条件是没办法破坏的,因为这是互斥锁的基本约束,其他三方条件都有办法来
破坏:
- 对于“请求和保持”这个条件,我们可以一次性申请所有的资源,这样就不存在等
待了。 - 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申
请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。 - 对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资
源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,
这样线性化后自然就不存在循环了。
2、讲一下 wait 和 notify 这个为什么要在synchronized 代码块中?
wait 和 notify 用来实现多线程之间的协调,wait 表示让线程进入到阻塞状态,
notify 表示让阻塞的线程唤醒。
wait 和 notify 必然是成对出现的,如果一个线程被 wait()方法阻塞,那么必然需
要另外一个线程通过 notify()方法来唤醒这个被阻塞的线程,从而实现多线程之
间的通信。
在多线程里面,要实现多个线程之间的通信,除了管道流以外,只能通过共享变
量的方法来实现,也就是线程 t1 修改共享变量 s,线程 t2 获取修改后的共享变
量 s,从而完成数据通信。
但是多线程本身具有并行执行的特性,也就是在同一时刻,多个线程可以同时执
行。在这种情况下,线程 t2 在访问共享变量 s 之前,必须要知道线程 t1 已经修
改过了共享变量 s,否则就需要等待。
同时,线程 t1 修改过了共享变量 S 之后,还需要通知在等待中的线程 t2。
所以要在这种特性下要去实现线程之间的通信,就必须要有一个竞争条件控制线
程在什么条件下等待,什么条件下唤醒。
而 Synchronized 同步关键字就可以实现这样一个互斥条件,也就是在通过共享
变量来实现多个线程通信的场景里面,参与通信的线程必须要竞争到这个共享变
量的锁资源,才有资格对共享变量做修改,修改完成后就释放锁,那么其他的线
程就可以再次来竞争同一个共享变量的锁来获取修改后的数据,从而完成线程之
前的通信。
所以这也是为什么 wait/notify 需要放在 Synchronized 同步代码块中的原因,有
了 Synchronized 同步锁,就可以实现对多个通信线程之间的互斥,实现条件等
待和条件唤醒。
3、如果一个线程两次调用 start(),会出现什么问题?
在Java 里面,一个线程只能调用一次 start() 方法,第二次调用会抛出IllegalThreadStateException。
一个线程本身是具备一个生命周期的。
在 Java 里面,线程的生命周期包括 6 种状态。
- NEW,线程被创建还没有调用 start 启动
- RUNNABLE,在这个状态下的线程有可能是正在运行,也可能是在就绪队列里
面等待操作系统进行调度分配 CPU 资源。 - BLOCKED,线程处于锁等待状态。
- WAITING,表示线程处于条件等待状态,当触发条件后唤醒,比如 wait/notify。
- TIMED_WAIT,和 WAITING 状态相同,只是它多了一个超时条件触发。
- TERMINATED,表示线程执行结束。
当我们第一次调用 start()方法的时候,线程的状态可能处于终止或者非 NEW 状
态下的其他状态。再调用一次 start(),相当于让这个正在运行的线程重新运行,不管从线程的安全性角度,还是从线程本身的执行逻辑,都是不合理的。
因此为了避免这个问题,在线程运行的时候会先判断当前线程的运行状态。
4、线程池有哪些参数,各个参数的作用是什么?
线程池主要有如下7个参数:
-
corePoolSize(核心工作线程数):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时。
-
maximumPoolSize(最大线程数):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
-
keepAliveTime(多余线程存活时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
-
workQueue(队列):用于传输和保存等待执行任务的阻塞队列。
-
unit:指定KeepAliveTime参数的时间单位。
-
threadFactory(线程创建工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
-
handler(拒绝策略):当线程池和队列都满了,再加入线程会执行此策略
5、线程池都有哪些状态?
线程池一共有五种状态, 分别是:
RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务。
SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。
STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态。
TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。进入TERMINATED的条件如下:
-
线程池不是RUNNING状态;
-
线程池状态不是TIDYING状态或TERMINATED状态;
-
如果线程池状态是SHUTDOWN并且workerQueue为空;
-
workerCount为0;
-
设置TIDYING状态成功。
6、谈谈线程池的拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
1、AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
2、DiscardPolicy:也是丢弃任务,但是不抛出异常。
3、DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)。
4、CallerRunsPolicy:由调用线程处理该任务。文章来源:https://www.toymoban.com/news/detail-401013.html
7、线程生命周期
文章来源地址https://www.toymoban.com/news/detail-401013.html
到了这里,关于多线程面试的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!