多线程面试题
1 Java创建线程之后,直接调用start()方法和run()方法的区别?
- 启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。run()方法可以产生必须退出的标志来停止一个线程。
2 线程B怎么知道线程A修改了变量?
- Volatile 修饰变量
- synchronized 修饰修改变量的方法
- wait/notify
- while 轮询
3 synchronized 和 volatile , CAS比较?
- synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
- volatile 提供多线程共享变量可见性和禁止指令重排序优化。
- CAS 是给予冲突检测的乐观锁(非阻塞)。
4 线程间通信,wait 和 notify 的理解和使用?
- wait 和 nofity 必须配合 synchronized 关键字使用。
- wait 方法释放锁,notify 方法不释放锁。
- 涉及到线程之间的通信,就肯定会用到 validate 修饰
5 定时线程的使用?
- 普通线程死循环
- 使用定时器timer
- 使用定时调度线程池 ScheduledExecutorService
6 线程同步的方法?
- wait() :使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep() :使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕获
InterrupyedException异常
notify() :唤醒一个处于等待状态的线程,注意的是在调用此方法的石昊,并不能确切的唤醒某一
个等待状态的线程,而是由 JVM 确定唤醒,而且不是按优先级。
notifyAll() :唤醒所有处于等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它
们竞争
7 进程和线程的区别?
- 调度:线程作为调度和分配的 基本单位,进程作为拥有资源的基本单位。
- 并发性 :不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
- 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源
- 系统开销:在创建或撤销进程的时候,由于系统都要为之分配和回收资源,导致进程的明显大于创建或撤销线程时的开销。但进程有独立的地址空间,进程崩溃后,在保护模式下不会对其他的进程产生影响,而线程只是一个进程中的不同的执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但是在进程切换时,耗费的资源较大,效率要差些。
8 什么叫线程安全?
- 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而其它的变量的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然可以将集合类分成两组,线程安全和非线程安全。
9 线程的几种状态?
- 新建状态(New)
新创建了一个线程对象。 - 就绪状态(Runnable)
线程对象创建后,其它线程调用了该对象的start() 方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。即在就绪状态的线程除CPU之外,其它的运行所需要的资源都已全部获得。 - 运行状态(Running)
就绪状态的线程获取了CPU,执行程序代码。 - 阻塞状态(Blocked)
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。知道线程进入就绪状态,才有机会转到运行状态。
阻塞的情况分为三种:- 等待阻塞:
运行的线程执行 wait() 方法,该线程会释放占用的所有资源,JVM 会把该线程放入"等待池"中。进入这个状态后,是不能自动唤醒的,必须依靠其它线程调用 notify() 或 notifyAll() 方法才能被唤醒。 - 同步阻塞:
运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入"锁池"中。 - 其它阻塞:
运行的线程执行 sleep() 或 join() 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep() 状态超市,join() 等待线程终止或者超时,或者 I/O 处理完毕时,线程重新转入就绪状态。
- 等待阻塞:
- 死亡状态(Dead)
线程执行完了或者因异常退出了run() 方法,该线程结束生命周期。
10 说一下 atomic 的原理?
- atomic 主要利用 CAS (Compare And Wwap) , volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
11 volatile 变量和 atomic 变量有什么不同
- volatile 变量和 atomic 变量看起来很像,但功能却不一样。
- Volatile 变量可以确保先行关系,即写操作发生在后续的读操作之前,但他并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不上原子性的。
- 而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性,如 getAndIncrement() 方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
12 Java中什么是竞态条件?
- 竞态条件会导致程序在并发情况下出现一些bugs。多线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的bugs。这种 bugs 很难发现而且会重复出现,因为线程间的随机竞争。
13 Java中怎么停止一个线程?
- Java 提供了很丰富的ApI 但没有为停止线程提供 API 。JDK1.0 本来有一些像 stop(), suspend() 和 resume() 的控制方法;但是由于潜在的死锁威胁,因此在后续的 JKD 版本中它们被弃用了,之后 JavaAPI 的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当 run() 或 call() 方法执行完的时候线程就会自动结束,如果要手动结束一个线程,可以用 volatile 布尔变量来退出 run() 方法的循环或者取消任务来中断线程。
14 线程池的优点?
- 重用存在的线程,减少对象创建销毁的开销。
- 可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
- 提供定时执行,定期执行,单线程,并发数控制等功能。
15 volatile 的理解?
- 一旦一个共享变量(类的成员变量,类的静态成员变量)被 volatile 修饰后,就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
- 用 volatile 修饰后,变量的操作:
- 使用 volatile 关键字会强制将修改的值立即写入主存;
- 使用 volatile 关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量 stop 的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
- 由于线程1的工作内存中缓存变量 stop 的缓存行无效,所以线程1再次读取变量 stop 的值时会去主存读取。
16 实现多线程有几种方式
- 在语言层面有两种方式。
java.lang.Thread 类的实例就是一个线程,但是他需要调用 java.lang.Runnable 接口来执行,由于线程类本身就是调用的 Runnable 接口,所以可以继承 java.lang.Thread 类或者直接调用 Runnable 接口来重写 run() 方法实现线程。
17 线程的创建方式
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口
- 使用线程池的方式
18 Java 中 notify 和 notifyAll 有什么区别?
- notify
notify() 方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候才使用。 - notifyAll
notifyAll() 唤醒所有线程并允许它们争夺锁,确保了至少有一个线程能继续运行。
19 什么是乐观锁和悲观锁?
- 乐观锁
就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该由相应的重试逻辑。 - 悲观锁
对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像 synchronized ,直接上了锁才操作资源。
20 线程池的作用?
- 创建线程要花费昂贵的资源和时间,如果任务来了才创建线程,那么响应时间会变长,而且一个进行能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从 JDK1.5 开始, JavaAPI 提供了 Executor 框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。
21 wait 和 sleep 的区别?
- wait
wait 是 Object 类的方法,对此对象调用wait 方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出 notify 方法(或者 notifyAll) 后本线程才进入对象锁定池准备获取对象锁进入运行状态。 - sleep
sleep 是线程类(Thread) 的方法,导致此线程暂停执行指定时间,把执行机会给其它线程,到那时监控状态依然保持,到时后会自动恢复。调用 sleep 不会释放对象锁。
22 产生死锁的条件?
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件: 进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
23 请写出实现线程安全的几种方式
- 使用同步代码块
- 使用同步方法
- 使用 ReentrantLock
24 守护线程是什么?它和非守护线程的区别?
- 守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某
种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线
程。 - 程序运行完毕,JVM 会等待非守护线程完成后关闭,但是 JVM 不会等待守护线程。
25 什么是多线程的上下文切换?
- 多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。
26 Callable 和 Runnable 的区别是什么?
- 实现 Callable 接口的任务线程能返回执行结果,而实现 Runnable 接口的任务线程不能返回结果
- Callable 通常需要和 Future/FutureTask 结合使用,用户获取异步计算结果。
27 线程阻塞有哪些原因?
- sleep() 允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到 CPU 时间,指定的时间一过,线程重新进入可执行状态。
典型地,sleep() 被用在等待某个资源就绪的情形;测试发现条件不满足后,让线程阻塞一段时间后重新测试,知道条件满足为止。 - suspend() 和 resume() 两个方法配套使用, suspend() 使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。
典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生结果后,调用 resume() 使其恢复。 - yield() 使当前线程放弃当前已经分得的 CPU 时间,但不使当前线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
- wait() 和 notify() 两个方法配套使用,wait() 使得线程进入阻塞状态,他有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间后线程重新进入可执行状态,后者则必须对应的 notify() 被调用。
28 说一下 synchronized 底层实现原理?
- synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
29 synchronized 和 ReentrantLock 区别是什么?
- synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都 相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
- 主要区别如下:
ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动 释放和开启锁;
ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
30 synchronized 和 Lock 的区别?
- 主要相同点
Lock 能完成 synchronized 所实现的所有功能 - 主要不同点
Lock 有比 synchronized 更精确的线程语义和更好的性能。
synchronized 会自动释放锁,而 Lock 一定要求程序员手动释放,并且必须在 finally 语句中释放。
31 ThreadLocal 是什么? 有什么作用?
- ThreadLocal 是一个本地线程副本变量工具类。
- 主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不同的变量值完成操作的场景。
- 简单说ThreadLocal 就是一种以空间换时间的做法,在每一个 Thread 里面维护了一个以开地址法实现的 ThreadLocal.ThreadLocalMap ,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题。
32 交互方式分为同步和异步两种?
- 同步交互
指发送一个请求,需要等待返回,然后才能够发送下一个请求,有一个等待过程; - 异步交互
指发送一个请求,不需要等待返回,随时可以发送下一个请求,即不需要等待; - 区别
一个需要等待,一个不需要等待,在部分情况下,我们的项目开发中都会优先选择不需要等待的异步交互方式。
33 什么是线程?
- 线程是操作系统能够进行运算的调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集任务提速。
34 什么是FutureTask?
- 在Java并发程序中 FutureTask 表示一个可以取消的异步运算。 它有启动和取消运算,查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是调用了 Runnable 接口,所以它可以提交给 Executor 来执行。
35 Java中 interrupted 和 isInterrupted 方法的区别?
- interrupted() 和 isInterrupted() 的主要区别是前者会将中断状态清除而后者不会。
- Java 多线程的中断机制是用内部标识来实现的;
调用 Thread.interrupt() 来中断一个线程就会设置中断标识为 true。
当中断线程调用静态方法 Thread.interrupted() 来检查中断状态时,中断状态会被清零。
而非静态方法 isInterrupted() 用来查询其它线程的中断状态且不会改变中断状态标识。 - 任何抛出InterruptedException异常的方法都会将中断状态清零。一个线程的中断状态可以被其他线程调用中断来改变。
36 死锁的原因?
- 是多个线程涉及到多个锁,这些所存在着交叉,所以可能会导致了一个锁依赖的闭环。
例如: 线程在获得锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。 - 默认的锁申请操作是阻塞的。
所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。
37 什么是自旋?
- 很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。 既然 synchronized 里面的代码执行的非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。 如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
38 怎么唤醒一个阻塞的线程?
- 如果线程是因为调用了 wait() ,sleep() 或者 join() 方法而导致的阻塞,可以中断线程,并且通过抛出 InterruptedException 来唤醒它。
- 如果线程遇到了 IO 阻塞,无法唤醒,因为 IO 是操作系统实现的, Java 代码并没有办法直接接触到操作系统。
39 如果提交任务时,线程池队列已满,这时会发生什么?
- 如果提交任务时线程池队列已满(如果一个任务不能被调度执行),那么ThreadPoolExecutor’s submit() 方法将会抛出一个 RejectedExecutionException 异常。
40 什么是线程局部变量?
- 线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程见共享。 Java 体供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如Web服务器)使用线程局部变量的时候需要特别的小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。 任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄漏的风险。
41 使用volatile 关键字的场景?
- synchronized 关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而 volatile 关键字在某些情况下性能优于 synchronized ,但是要注意 volatile 关键字是无法替代 synchronized 关键字的,因为volatile 关键字无法保证操作的原子性。通常来说,使用 volatile 必须具备以下两个条件的:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其它变量的不变式中
42 线程池的工作原理,几个重要参数?
- ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue workQueue, ThreadFactory trheadFactory,
RejectedExecutionHandler handler) - 参数说明:
- corePoolSize 核心线程数
- maximumPoolSize 最大线程数,一般大于等于核心线程数
- keepAliveTime 线程存活时间(针对最大线程数大于核心线程数时,非核心线程)
- unit 存活时间单位,和现场存活时间配套使用
- workQueue 任务队列
- trheadFactory 创建线程的工程
- handler 拒绝策略
43 线程池的阻塞队列有哪些?
- 三种阻塞队列:
BlockingQueue workQueue = null;
- workQueue = new ArrayBlockingQueue<>(5);
基于数组的先进先出队列,有界 - workQueue = new LinkedBlockingQueue<>();
基于链表的先进先出队列,无界 - workQueue = new SynchronousQueue<>();
无缓冲的等待队列,无界
44 线程池的拒绝策略有哪些?
- 四种拒绝策略
- 等待队列已经排满了,再也塞不下新任务,同时线程池中线程也已经达到 maximumPoolSize 数量,无法继续为新任务服务,这个时候就需要使用拒绝策略来处理。
RejectedExecutionHandler rejected = null;
- rejected = new ThreadPoolExecutor.AbortPolicy();
默认,队列满了丢任务抛出异常 - rejected = new ThreadPoolExecutor.DiscardPolicy();
队列满了丢任务不异常 - rejected = new ThreadPoolExecutor.DiscardOldestPolicy();
将最早进入队列的任务删,之后再尝试加入队列 - rejected = new ThreadPoolExecutor.CallerRunsPolicy();
如果添加到线程池失败,那么主线程会自己去执行该任务
- 等待队列已经排满了,再也塞不下新任务,同时线程池中线程也已经达到 maximumPoolSize 数量,无法继续为新任务服务,这个时候就需要使用拒绝策略来处理。
45 线程池的类型?
- 五种线程池:
ExecutorService threadPool = null;- threadPool = Executors.newCachedThreadPool();
有缓冲的线程池,线程数 JVM 控制 - threadPool = Executors.newFixedThreadPool(3);
固定大小的线程池,可控制线程最大并发数,超出的线程会在队列中等待 - threadPool = Executors.newScheduledThreadPool(2);
固定大小的线程池,支持定时及周期性任务执行 - threadPool = Executors.newSingleThreadExecutor();
单线程的线程池,只有一个线程在工作 - threadPool = new ThreadPoolExecutor();
默认线程池,可控制参数比较多
- threadPool = Executors.newCachedThreadPool();