多线程面试题及答案(面试官必问39题)

多线程面试题及答案(面试官必问39题)-mikechen

1、并发编程三要素?

1)原子性

原子性是指一个操作是不可中断的,要么全部执行成功,要么全部不执行。

在并发编程中,原子性可以通过使用同步机制,比如:synchronized关键字或Lock锁来保证。

2)可见性

可见性是指当一个线程对共享资源进行修改后,其他线程能够立即看到这个修改。

在多线程环境下,每个线程都有自己的工作内存,对共享资源的修改首先会在工作内存中进行,然后才会刷新到主内存中,其他线程才能看到这个修改。

为了确保可见性,可以使用volatile关键字或显式地使用锁来进行同步。

3)有序性

有序性是指程序执行的结果是按照一定的顺序来保证的。

在多线程环境下,由于线程调度的不确定性,不同线程之间的操作顺序是无法确定的。

为了确保有序性,可以使用同步机制,比如:synchronized关键字或Lock锁来保证线程的顺序执行。

 

2、实现可见性的方法有哪些?

使用volatile关键字,将共享变量声明为volatile可以确保对该变量的写操作立即可见于其他线程。

使用synchronized关键字或Lock锁,使用同步机制可以确保线程在进入同步块或同步方法时,会先获取锁,而在释放锁之前会将对共享变量的修改刷新到主内存中,保证可见性。

 

3、多线程的价值?

1)提高程序的并发性

多线程可以使程序同时执行多个任务,从而充分利用多核处理器的计算能力,提高程序的整体性能和吞吐量。

多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的,采用多线程的方式去同时完成几件事情而不互相干扰。

2)提升程序的响应性

通过多线程可以实现异步操作和并行处理,使得程序能够更快地响应用户的请求或事件,提升用户体验。

3)改善资源利用率

多线程可以充分利用系统资源,如CPU、内存、I/O设备等,提高资源的利用效率。

 

4.创建线程的三种方式的对比?

1)实现Runnable接口

class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的代码
    }
}

Thread thread = new Thread(new MyRunnable());
thread.start();

 

优势是

线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是:

编写稍微复杂一些,需要额外的类来实现Runnable接口。

 

2)继承Thread类

class MyThread extends Thread {
    public void run() {
        // 线程执行的代码
    }
}

MyThread thread = new MyThread();
thread.start();

 

优势是:

简单易用,适用于简单的线程需求。

劣势是:

由于Java是单继承的,如果已经继承了其他类,则无法使用该方式创建线程。

3)使用匿名内部类

Thread thread = new Thread(new Runnable() {
    public void run() {
        // 线程执行的代码
    }
});
thread.start();

 

  • 优点:简洁、快速,适用于简单的线程需求。
  • 缺点:可读性稍差,不适用于复杂的线程需求。

 

5、线程的状态流转图

线程的生命周期及五种基本状态,如下图所示:

多线程面试题及答案(面试官必问39题)-mikechen

6.Java线程具有五中基本状态

线程的生命周期包括以下状态:

新建(New):创建线程对象。

就绪(Runnable):线程被创建并调用了start()方法,但还没有被调度执行。

运行(Running):线程被调度执行。

阻塞(Blocked):线程在等待某个条件(如锁)释放。

终止(Terminated):线程执行完毕或异常终止。

 

7.什么是线程池?有哪几种创建方式?

线程池是一种用于管理和复用线程的机制,它维护了一个线程的集合,可以动态地创建、销毁和重用线程,以提高线程的利用效率和性能。

Java提供了ThreadPoolExecutor类作为线程池的实现,同时也提供了Executors工具类来快速创建不同类型的线程池。

 

8.四种线程池的创建

(1)newCachedThreadPool:创建一个可缓存线程池;

(2)newFixedThreadPool :创建一个定长线程池,可控制线程最大并发数;

(3)newScheduledThreadPool: 创建一个定长线程池,支持定时及周期性任务执行;

(4)newSingleThreadExecutor :创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。

 

9.线程池的优点?

  1. 降低线程创建和销毁的开销:线程的创建和销毁是一项昂贵的操作,涉及到操作系统的系统调用和资源分配。
  2. 提高系统资源的利用率:线程池可以限制线程的数量,通过合理配置线程池的大小,可以避免过多的线程竞争系统资源,提高系统资源的利用率。
  3. 提高程序的响应性和吞吐量:线程池能够并发执行多个任务,当有新任务到达时,可以从线程池中取出空闲的线程来处理任务,从而减少任务的等待时间,提高程序的响应性和处理能力。
  4. 提供线程的管理和监控:线程池提供了对线程的管理和监控功能,可以方便地查看线程的状态、统计信息和调整线程池的配置。

 

10.常用的并发工具类有哪些?

  • CountDownLatch;
  • CyclicBarrier;
  • Semaphore;
  • Exchanger;

11.线程池的核心参数有哪些?

线程池的核心参数,会包含如下几种,如下图所示:

多线程面试题及答案(面试官必问39题)-mikechen

12.synchronized的作用?

Synchronized 是 Java 中的关键字,用于实现线程间的同步和互斥。

具有以下几个主要的作用:

  1. 实现线程安全:synchronized 关键字可以确保在同一时刻只有一个线程可以执行被修饰的方法或代码块,从而避免多个线程同时修改共享数据而引发的竞态条件和数据不一致的问题。
  2. 保护共享资源:当多个线程需要访问共享资源时,可以使用 synchronized 来保护共享资源的访问。
  3. 保证可见性:synchronized 关键字不仅提供了互斥性,还保证了线程间的可见性。
  4. 顺序性:synchronized 关键字还可以保证代码的顺序性执行。在同一个线程中,对于 synchronized 块或方法的访问是按照代码的书写顺序执行的,保证了代码的有序性。

 

13.volatile关键字的作用

对于可见性,Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

14.什么是CAS

CAS 是 Compare and Swap(比较并交换)的缩写,是一种无锁的原子操作。

CAS 操作涉及三个操作数:

  1. 内存地址(或变量的引用):需要进行原子操作的内存位置。
  2. 期望值(旧值):用于比较的值。
  3. 更新值(新值):希望将内存位置更新为的新值。

CAS 操作的过程如下:

  1. 读取内存地址的当前值。
  2. 将当前值与期望值进行比较。
  3. 如果相等,则将内存位置的值更新为新值。
  4. 如果不相等,则表示其他线程已经修改了内存位置的值,当前操作失败。

 

15. CAS的问题

1)CAS容易造成ABA问题。一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。

2) 不能保证代码块的原子性

CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

3)CAS造成CPU利用率增加。之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。

 

16.什么是Future?

Java Future 是 Java 多线程编程中用于表示异步计算结果的接口。

它代表了一个可能还没有完成的计算任务,并提供了一些方法来检查任务是否已完成、获取计算结果和取消任务等操作。

 

17.什么是AQS

AQS(AbstractQueuedSynchronizer)就是一个抽象的队列同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它。

AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,比如大家熟知的:

  • ReentrantLock
  • Semaphore
  • CountDownLatch
  • CyclicBarrier

等并发类均是基于AQS来实现的。

 

18. AQS的数据模型

AQS 使用下图的资源变量 state来表示同步状态,通过内置的 CLH FIFO 队列来完成获取资源线程的排队工作。

如下图所示:

多线程面试题及答案(面试官必问39题)-mikechen

19.ReadWriteLock是什么

所谓的读写锁(Readers-Writer Lock),顾名思义就是将一个锁拆分为读锁和写锁两个锁。

ReentrantReadWriteLock适合读多写少的场景:

读锁ReentrantReadWriteLock.ReadLock可以被多个线程同时持有, 所以并发能力很高。

写锁ReentrantReadWriteLock.WriteLock是独占锁, 在一个线程持有写锁时候, 其他线程都不能在抢占, 包含抢占读锁都会阻塞。

ReentrantReadWriteLock的使用场景总结:其实就是 读读并发、读写互斥、写写互斥而已,如果一个对象并发读的场景大于并发写的场景,那就可以使用 ReentrantReadWriteLock来达到保证线程安全的前提下提高并发效率。

20.FutureTask是什么

Java FutureTask 是 Java 多线程编程中的一个实现类,实现了 Future 接口和 Runnable 接口,用于表示一个异步计算任务,并可以获取计算结果。

21.synchronized和ReentrantLock的区别

它们有以下几个区别:

1)锁的获取方式

synchronized 通过关键字修饰方法或代码块,当线程进入 synchronized 修饰的代码块时,会自动获取锁,当线程退出 synchronized 修饰的代码块时,会自动释放锁。

ReentrantLock 是通过 Lock 接口的实现类,需要手动调用 lock() 方法来获取锁,并在合适的时机调用 unlock() 方法释放锁。需要手动管理锁的获取和释放。

2)锁的灵活性

synchronized:synchronized 是 JVM 内置的机制,具有一些固定的特性,例如不可中断、不可限时地等待锁的释放。

ReentrantLock:ReentrantLock 是 JDK 提供的显示锁,具有更丰富的特性。它可以实现可中断的锁获取、可限时的锁等待、公平性选择等。

3)性能

synchronized 是 JVM 内置的机制,在 JDK 的不断优化下,其性能在很多情况下已经相当高效。

ReentrantLock 是基于 Lock 接口的实现类,相对于 synchronized,它提供了更细粒度的控制和额外的特性,但由于其复杂性,性能上略低于 synchronized。

22.什么是乐观锁和悲观锁

乐观锁和悲观锁是并发编程中两种不同的锁策略,用于处理多线程环境下的数据访问冲突问题。

1)乐观锁

乐观锁的设计思想是,认为在整个操作过程中不会发生冲突,因此在访问共享资源之前不会进行加锁操作,直接进行操作。

2)悲观锁

悲观锁的设计思想是,认为在整个操作过程中会发生冲突,因此在访问共享资源之前,先假设其他线程会修改该资源,所以会悲观地加锁,防止其他线程对该资源的访问和修改。

23.线程B怎么知道线程A修改了变量

  • volatile修饰变量
  • synchronized修饰修改变量的方法
  • wait/notify
  • while轮询

24.synchronized、volatile、CAS比较

  • synchronized是悲观锁,属于抢占式,会引起其他线程阻塞。
  • volatile提供多线程共享变量可见性和禁止指令重排序优化。
  • CAS是基于冲突检测的乐观锁(非阻塞)

25.sleep方法和wait方法有什么区别?

sleep方法和wait方法都可以用来放弃CPU一定的时间;

不同点在于如果线程持有某个对象的监视器;

sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

26.ThreadLocal是什么?有什么用?

ThreadLocal是一个本地线程副本变量工具类,主要用于将私有线程和该线程存放的副本对象做一个映射。

各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

27.为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁

 

28.什么是并行?

并发,如下图所示:

多线程面试题及答案(面试官必问39题)-mikechen
并行:指的是多个cpu实例,或者多台机器,同时执行一段处理逻辑,是真正的同时。

 

29.什么是并发?

并发,如下图所示:

多线程面试题及答案(面试官必问39题)-mikechen

并发: 指单个cpu同时处理多个线程任务,通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。

 

30.ConcurrentHashMap的并发度是什么

ConcurrentHashMap的并发度就是segment的大小,默认为16。

这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势。

 

31.Linux环境下如何查找哪个线程使用CPU最长

(1)获取项目的pid,jps或者ps -ef | grep java

(2)top -H -p pid,顺序不能改变

 

32.Java死锁?

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。

当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁,如图所示:

多线程面试题及答案(面试官必问39题)-mikechen

 

33.死锁的原因

死锁的发生通常需要满足以下四个条件,称为死锁的必要条件:

  1. 互斥条件:至少有一个资源必须处于被独占状态,即一次只能被一个线程占用。
  2. 请求与保持条件:一个线程已经持有了一个资源,同时又提出了对其他资源的请求。
  3. 不可剥夺条件:已经分配的资源不能被强制性地剥夺。
  4. 环路等待条件:存在一个等待循环,即线程 A 等待线程 B 持有的资源,线程 B 等待线程 C 持有的资源,以此类推,直到线程 N 等待线程 A 持有的资源。

 

34.怎么唤醒一个阻塞的线程

如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它。

如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

 

35.synchronized的底层实现?

synchronized的底层实现是完全依赖JVM虚拟机的,所以谈synchronized的底层实现,就不得不谈数据在JVM内存的存储:Java对象头,以及Monitor对象监视器。

 

36.什么是多线程的上下文切换

多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

 

37.Java中用到的线程调度算法是什么

抢占式,一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

38.什么是自旋?

很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。

既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。

如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

39.创建线程的有哪些方式?

1)继承Thread类创建线程类

2)通过Runnable接口创建线程类

3)通过Callable和Future创建线程

4)通过线程池创建

 

mikechen

mikechen睿哥,10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。

关注「mikechen」公众号,获知最新一线技术干货!

评论交流
    说说你的看法