Java多线程面试八股文

105 阅读9分钟

Java中线程的实现方式

一. 线程的创建(4种)

1.1 继承Thread类,重写run方法

1.2 实现Runnable接口,重写run方法(更好)

1.3 实现Callable接口,重写call方法,配合FutureTask

Callable一般用于有返回结果的非阻塞的执行方法。

同步非阻塞

runnable 和 callable 有什么区别?

runnable 没有返回值,callable 可以拿到有返回值,callable 可以看作是 runnable 的补充。

1.4 基于线程池构建

追其底层,其实只有一种,实现Runnable。

线程池创建有七种方式,最核心的是:

ThreadPoolExecutor():是最原始的线程池创建,其他创建方式都是对ThreadPoolExecutor的封装。

二. Java中线程的状态

2.1 线程的状态

5种、6种、7种都可以接受

5种状态一般是针对传统的线程状态来说(操作系统层面):

新建态(new)、就绪态(ready)、运行态(running)、等待态(WAITING)、结束态(TERMINATED)

Java中给线程准备的6种状态(重点):

NEW、RUNNABLE(包含就绪和运行两种状态)、BLOCKED(拿synchronized失败)、WAITING、TIMED_WAITING、TERMINATED

7种状态:

RUNNABLE中就绪和运行分开

三. Java中如何停止线程

3.1 stop方法(不用)(过时了)

3.2 使用共享变量(很少会用)

修改共享变量破坏死循环,让线程退出循环,结束run方法

3.3 interrupt方式

共享变量方式

四. Java中sleep和wait方法的区别

  • sleep属于Thread类汇总的static方法,wait属于Object类的方法
  • sleep属于TIMED_WAITING,自动被唤醒、wait输入WAAITING,需要手动唤醒
  • sleep方法在持有锁时,执行,不会释放锁资源,wait在执行后会释放锁资源
  • sleep可以在持有锁和不持有锁时,执行,wait方法必须在持有锁时才可以执行

wait方法会将持有锁的线程从owner扔到WaitSet集合中,这个操作是在修改ObjectMonitor对象,如果没有持有synchronized锁的话,是无法操作ObjectMonitor对象的。

五. 并发编程的三大特性

5.1 原子性

一个操作不可分割不可中断,一个线程在执行时,另一个线程不会影响到他。

保证并发编程的原子性:

  1. synchronized
  2. CAS(compare and swap比较和交换,是CPU的并发原语
  3. Lock锁(和优化过的synchronized性能差别不大)
  4. ThreadLocal

5.2 可见性

基于CPU位置,提供L1,L2,L3三级缓存提高效率,但是问题是,CPU都是多核,CPU三级缓存都是独立的,会告知每个线程中做修改时,只改自己的工作内存,没有及时的同步到主内存,导致数据不一致问题。

解决方案:volatile(常用)

5.3 有序性

解决方案:volatile(常用)

六. Java中的四种引用类型

强、软、弱、虚

强引用(使用最多):把一个对象赋给一个引用变量,这个引用变量就是一个强引用。它永远不可能被垃圾回收机制回收,因此强引用是造成Java内存泄漏的主要原因之一

User user = new User();

软引用(使用较少)SoftReference:当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中,作为缓存使用。

弱引用:比软引用生存期更短。弱只有弱引用,只要GC,立即被回收。解决内存泄漏问题。

虚引用:不能单独使用。

七. Java中锁的分类

7.1 可重入锁、不可重入锁

Java中提供的synchronized是可重入锁,大多数都是可重入锁

ThreadPoolExecutor中的Worker是不可重入锁

7.2 乐观锁、悲观锁

Java中提供的synchronized是悲观锁。

Java中提供的CAS操作(Atomic原子性类),是乐观锁。

7.3 公平锁、非公平锁

Java中提供的synchronized只能是非公平锁。

7.4 互斥锁、共享锁

Java中提供的synchronized是互斥锁。同一时间点,只有一个线程持有当前互斥锁。

八. ConcurrentHashMap

ConcurrentHashMap是线程安全的HashMap。

ConcurrentHashMap在JDK1.8中是以CAS+synchronized(锁头结点)实现的线程安全。

CAS:在没有hash冲突时。

synchronized:出现hash冲突时。

存储的结构:数组+链表+红黑树

当链表长度>8且数组长度>=64时,链表转为红黑树。

九. Java线程相关常见面试问法

  1. 哪些集合类是线程安全的?

Vector、Hashtable、Stack 都是线程安全的,而像 HashMap 则是非线程安全的,不过在 JDK 1.5 之后随着 Java.util.concurrent 并发包的出现,它们也有了自己对应的线程安全类,比如 HashMap 对应的线程安全类就是 ConcurrentHashMap。

  1. 守护线程是什么?

守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。

sleep() 和 wait() 有什么区别?

类的不同:sleep() 来自 Thread,wait() 来自 Object。
释放锁:sleep() 不释放锁;wait() 释放锁。
用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。

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

notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程。notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。

  1. 线程的 run() 和 start() 有什么区别?

start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

  1. 线程池都有哪些状态?

RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。

  1. 线程池中 submit() 和 execute() 方法有什么区别?

execute():只能执行 Runnable 类型的任务。
submit():可以执行 Runnable 和 Callable 类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。

  1. 线程池中 submit() 和 execute() 方法有什么区别?

execute():只能执行 Runnable 类型的任务。
submit():可以执行 Runnable 和 Callable 类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。

  1. 多线程中 synchronized 锁升级的原理是什么?

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

  1. 什么是死锁?

当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

  1. 怎么防止死锁?

尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
尽量使用 Java. util. concurrent 并发类代替自己手写锁。
尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
尽量减少同步的代码块。

  1. ThreadLocal 是什么?有哪些使用场景?

ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal 的经典使用场景是数据库连接和 session 管理等。

  1. 说一下 synchronized 底层实现原理?

synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

  1. synchronized 和 volatile 的区别是什么?

volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

  1. synchronized 和 Lock 有什么区别?

synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

  1. synchronized 和 ReentrantLock 区别是什么?

synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。

主要区别如下:

ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

  1. 说一下 atomic 的原理?

atomic 主要利用 CAS (Compare And Wwap) 和 volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。