Java资深架构师详解大厂多线程面试题,细谈并发编程深造历程

简介: 多线程、线程池多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。
img_e04edd499b0f35d1db94ef2b6467a54e.jpe

多线程、线程池

img_49f7a4f3f00b995285a16706bfd34109.jpe

多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。所谓多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程。进程与线程的区别如图所示:

img_d3330f2c633126d098febffc549c0a2b.jpe

多线程面试题及答案

这里例举20道面试题以及答案,由于篇幅过长,就不一一发出答案了,如果有需要的话可以参考结尾的获取方式。

img_097e9c207acceafc650ef07c133a1415.jpe

1.多线程有什么用?

1)发挥多核CPU 的优势

随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4 核、8 核甚至 16 核的也都不少见,如果是单线程的程序,那么在双核 CPU 上就浪费了 50%, 在 4 核 CPU 上就浪费了 75%。单核 CPU 上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核 CPU 上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU 的优势来,达到充分利用CPU 的目的。

2)防止阻塞

从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

3)便于建模

这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成几个小任务,任务B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

2.线程和进程的区别是什么?

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

3.Java 实现线程有哪几种方式?

1)继承 Thread 类实现多线程

2)实现 Runnable 接口方式实现多线程

3)使用 ExecutorService、Callable、Future 实现有返回结果的多线程

4.启动线程方法 start()和 run()有什么区别?

只有调用了 start()方法,才会表现出多线程的特性,不同线程的 run()方法里面的代码交替执行。如果只是调用 run()方法,那么代码还是同步执行的,必须等待一个线程的 run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其 run()方法里面的代码。

5.怎么终止一个线程?如何优雅地终止线程?

stop 终止,不推荐。

6.一个线程的生命周期有哪几种状态?它们之间如何流转的?

NEW:毫无疑问表示的是刚创建的线程,还没有开始启动。

RUNNABLE: 表示线程已经触发 start()方式调用,线程正式启动,线程处于运行中状态。

BLOCKED:表示线程阻塞,等待获取锁,如碰到 synchronized、lock 等关键字等占用临界区的情况,一旦获取到锁就进行 RUNNABLE 状态继续运行。

WAITING:表示线程处于无限制等待状态,等待一个特殊的事件来重新唤醒,如通过wait()方法进行等待的线程等待一个 notify()或者 notifyAll()方法,通过 join()方法进行等待的线程等待目标线程运行结束而唤醒,一旦通过相关事件唤醒线程,线程就进入了 RUNNABLE 状态继续运行。

TIMED_WAITING:表示线程进入了一个有时限的等待,如 sleep(3000),等待 3 秒后线程重新进行 RUNNABLE 状态继续运行。

TERMINATED:表示线程执行完毕后,进行终止状态。需要注意的是,一旦线程通过 start 方法启动后就再也不能回到初始 NEW 状态,线程终止后也不能再回到 RUNNABLE 状态

7.线程中的 wait()和 sleep()方法有什么区别?

这个问题常问,sleep 方法和 wait 方法都可以用来放弃 CPU 一定的时间,不同点在于如果线程持有某个对象的监视器,sleep 方法不会放弃这个对象的监视器,wait 方法会放弃这个对象的监视器

img_12c300c9f1f4b07274a8dfda5d1dcc67.jpe

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

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

9.什么是死锁?如何避免死锁?

死锁就是两个线程相互等待对方释放对象锁。

10.多线程之间如何进行通信?

wait/notify

11、线程怎样拿到返回结果?

实现Callable 接口。

12、violatile 关键字的作用?

一个非常重要的问题,是每个学习、应用多线程的 Java 程序员都必须掌握的。理解 volatile关键字的作用的前提是要理解 Java 内存模型,这里就不讲 Java 内存模型了,可以参见第31 点,volatile 关键字的作用主要有两个:

1)多线程主要围绕可见性和原子性两个特性而展开,使用 volatile 关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到 volatile 变量,一定是最新的数据

2)代码底层执行不像我们看到的高级语言----Java 程序这么简单,它的执行是 Java 代码-->字节码-->根据字节码执行对应的 C/C++代码-->C/C++代码被编译成汇编语言-->和硬件电路交互,现实中,为了获取更好的性能 JVM 可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用 volatile 则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。

13、新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?

用 join 方法。

14、怎么控制同一时间只有 3 个线程运行?

用 Semaphore。

15、为什么要使用线程池?

我们知道不用线程池的话,每个线程都要通过 new Thread(xxRunnable).start()的方式来创建并运行一个线程,线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的 CPU 和内存资源,也会造成 GC频繁收集和停顿,因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。所以,线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。

img_7eb6e434e5de5af449e90a501e5f2abc.jpe

16、常用的几种线程池并讲讲其中的工作原理。

什么是线程池?

很简单,简单看名字就知道是装有线程的池子,我们可以把要执行的多线程交给线程池来处理,和连接池的概念一样,通过维护一定数量的线程池来达到多个线程的复用。

线程池的好处

我们知道不用线程池的话,每个线程都要通过 new Thread(xxRunnable).start()的方式来创建并运行一个线程,线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的 CPU 和内存资源,也会造成 GC频繁收集和停顿,因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。所以,线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。

线程池核心类

在 java.util.concurrent 包中我们能找到线程池的定义,其中 ThreadPoolExecutor 是我们线程池核心类,首先看看线程池类的主要参数有哪些。

如何提交线程

如可以先随便定义一个固定大小的线程池ExecutorServicees=Executors.newFixedThreadPool(3);

提交一个线程es.submit(xxRunnble);

es.execute(xxRunnble);

submit 和 execute 分别有什么区别呢?

execute 没有返回值,如果不需要知道线程的结果就使用 execute 方法,性能会好很多。

submit 返回一个 Future 对象,如果想知道线程结果就使用 submit 提交,而且它能在主线程中通过 Future 的 get 方法捕获线程中的异常。

如何关闭线程池es.shutdown();

不再接受新的任务,之前提交的任务等执行结束再关闭线程池。

es.shutdownNow();

不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程list 列表。

17、线程池启动线程 submit()和 execute()方法有什么不同?

execute 没有返回值,如果不需要知道线程的结果就使用 execute 方法,性能会好很多。

submit 返回一个 Future 对象,如果想知道线程结果就使用 submit 提交,而且它能在主线程中通过 Future 的 get 方法捕获线程中的异常。

18、CyclicBarrier 和 CountDownLatch 的区别?

两个看上去有点像的类,都在 java.util.concurrent 下,都可以用来表示代码运行到某个点上,二者的区别在于:

1.CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch 则不是,某线程运行到某个点上之后,只是给某个数值-1 而已,该线程继续运行

2.CyclicBarrier 只能唤起一个任务,CountDownLatch 可以唤起多个任务

3.CyclicBarrier 可重用,CountDownLatch 不可重用,计数值为 0 该 CountDownLatch就不可再用了

img_ba1ad0643bc5b4c42be157a44ee81d48.jpe

19、什么是活锁、饥饿、无锁、死锁?

死锁、活锁、饥饿是关于多线程是否活跃出现的运行阻塞障碍问题,如果线程出现了这三种情况,即线程不再活跃,不能再正常地执行下去了。

死锁

死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。

举个例子,A 同学抢了 B 同学的钢笔,B 同学抢了 A 同学的书,两个人都相互占用对方的东西,都在让对方先还给自己自己再还,这样一直争执下去等待对方还而又得不到解决,老师知道此事后就让他们相互还给对方,这样在外力的干预下他们才解决,当然这只是个例子没有老师他们也能很好解决,计算机不像人如果发现这种情况没有外力干预还是会一直阻塞下去的。

活锁

活锁这个概念大家应该很少有人听说或理解它的概念,而在多线程中这确实存在。活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。

饥饿

我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿。当然还有一种饥饿的情况,一个线程一直占着一个资源不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如那个占用资源的线程结束了并释放了资源。

无锁

无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。之前的文章我介绍过 JDK 的 CAS 原理及应用即是无锁的实现。

可以看出,无锁是一种非常良好的设计,它不会出现线程出现的跳跃性问题,锁使用不当肯定会出现系统性能问题,虽然无锁无法全面代替有锁,但无锁在某些场合下是非常高效的。

20、什么是原子性、可见性、有序性?

原子性、可见性、有序性是多线程编程中最重要的几个知识点,由于多线程情况复杂,如何让每个线程能看到正确的结果,这是非常重要的。

原子性

原子性是指一个线程的操作是不能被其他线程打断,同一时间只有一个线程对一个变量进行操作。在多线程情况下,每个线程的执行结果不受其他线程的干扰,比如说多个线程同时对同一个共享成员变量 n++100 次,如果 n 初始值为 0,n 最后的值应该是 100,所以说它们是互不干扰的,这就是传说的中的原子性。但 n++并不是原子性的操作,要使用 AtomicInteger 保证原子性。

可见性

可见性是指某个线程修改了某一个共享变量的值,而其他线程是否可以看见该共享变量修改后的值。在单线程中肯定不会有这种问题,单线程读到的肯定都是最新的值,而在多线程编程中就不一定了。每个线程都有自己的工作内存,线程先把共享变量的值从主内存读到工作内存,形成一个副本,当计算完后再把副本的值刷回主内存,从读取到最后刷回主内存这是一个过程,当还没刷回主内存的时候这时候对其他线程是不可见的,所以其他线程从主内存读到的值是修改之前的旧值。像 CPU 的缓存优化、硬件优化、指令重排及对 JVM 编译器的优化,都会出现可见性的问题。

有序性

我们都知道程序是按代码顺序执行的,对于单线程来说确实是如此,但在多线程情况下就不是如此了。为了优化程序执行和提高 CPU 的处理性能,JVM 和操作系统都会对指令进行重排,也就说前面的代码并不一定都会在后面的代码前面执行,即后面的代码可能会插到前面的代码之前执行,只要不影响当前线程的执行结果。所以,指令重排只会保证当前线程执行结果一致,但指令重排后势必会影响多线程的执行结果。虽然重排序优化了性能,但也是会遵守一些规则的,并不能随便乱排序,只是重排序会影响多线程执行的结果。

img_e30590eadcbc6eadc5725c3bd99cc6c3.jpe

由于篇幅过长,就不一一发出答案了,如果有需要的话可以参考结尾的获取方式。

img_3f7a02e49eb8c388d8d1fdf39e33dbdf.jpe
相关文章
|
8天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第11天】 在Java中,高效的并发编程是提升应用性能和响应能力的关键。本文将探讨Java并发的核心概念,包括线程安全、锁机制、线程池以及并发集合等,同时提供实用的编程技巧和最佳实践,帮助开发者在保证线程安全的前提下,优化程序性能。我们将通过分析常见的并发问题,如竞态条件、死锁,以及如何利用现代Java并发工具来避免这些问题,从而构建更加健壮和高效的多线程应用程序。
|
1天前
|
安全 Java
java多线程(一)(火车售票)
java多线程(一)(火车售票)
|
1天前
|
安全 Java
就只说 3 个 Java 面试题 —— 02
就只说 3 个 Java 面试题 —— 02
12 0
|
1天前
|
存储 安全 Java
就只说 3 个 Java 面试题
就只说 3 个 Java 面试题
7 0
|
2天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
2天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。
|
2天前
|
缓存 分布式计算 监控
Java并发编程:深入理解线程池
【4月更文挑战第17天】在Java并发编程中,线程池是一种非常重要的技术,它可以有效地管理和控制线程的执行,提高系统的性能和稳定性。本文将深入探讨Java线程池的工作原理,使用方法以及在实际开发中的应用场景,帮助读者更好地理解和使用Java线程池。
|
3天前
|
负载均衡 Java 开发者
细解微服务架构实践:如何使用Spring Cloud进行Java微服务治理
【4月更文挑战第17天】Spring Cloud是Java微服务治理的首选框架,整合了Eureka(服务发现)、Ribbon(客户端负载均衡)、Hystrix(熔断器)、Zuul(API网关)和Config Server(配置中心)。通过Eureka实现服务注册与发现,Ribbon提供负载均衡,Hystrix实现熔断保护,Zuul作为API网关,Config Server集中管理配置。理解并运用Spring Cloud进行微服务治理是现代Java开发者的关键技能。
|
3天前
|
Java 开发者
Java中多线程并发控制的实现与优化
【4月更文挑战第17天】 在现代软件开发中,多线程编程已成为提升应用性能和响应能力的关键手段。特别是在Java语言中,由于其平台无关性和强大的运行时环境,多线程技术的应用尤为广泛。本文将深入探讨Java多线程的并发控制机制,包括基本的同步方法、死锁问题以及高级并发工具如java.util.concurrent包的使用。通过分析多线程环境下的竞态条件、资源争夺和线程协调问题,我们提出了一系列实现和优化策略,旨在帮助开发者构建更加健壮、高效的多线程应用。
3 0
|
4天前
|
缓存 监控 Java
Java并发编程:线程池与任务调度
【4月更文挑战第16天】Java并发编程中,线程池和任务调度是核心概念,能提升系统性能和响应速度。线程池通过重用线程减少创建销毁开销,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。任务调度允许立即或延迟执行任务,具有灵活性。最佳实践包括合理配置线程池大小、避免过度使用线程、及时关闭线程池和处理异常。掌握这些能有效管理并发任务,避免性能瓶颈。