首页 > 编程语言 >Java 多线程面试题

Java 多线程面试题

时间:2024-10-28 11:20:52浏览次数:8  
标签:面试题 执行 Java 对象 一个 线程 进程 多线程 方法

什么是线程和进程?

进程(Process)

定义:进程是一个正在运行的程序的实例。每个进程都有自己的内存空间、数据栈和其他用于跟踪进程执行的辅助数据。

特性:

        进程之间是相互独立的,拥有各自的地址空间。

        进程间通信(IPC)相对复杂,通常需要使用管道、套接字或共享内存等方式。

        每个进程都有自己的资源(如文件句柄、内存等),需要操作系统进行管理。

线程(Thread)

定义:线程是进程中的一个执行单元,是程序执行的基本单元。一个进程可以包含多个线程,它们共享该进程的内存和资源。

特性:

        线程之间共享进程的内存空间和资源,使得线程间通信更加高效。

        线程的创建和销毁比进程轻量级,通常更快。

        由于共享内存,线程间的同步和协调变得更加重要,以防止数据竞争和不一致。

总结

        内存分配:进程有独立的内存空间,线程共享进程的内存。

        创建和销毁:创建线程的开销比创建进程小。

        通信:进程间通信更复杂,线程间通信更简单。

做个简单的比喻:进程=火车,线程=车厢

线程在进程下行进(单纯的车厢无法运行)

一个进程可以包含多个线程(一辆火车可以有多个车厢)

不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)

同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)

进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)

进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)

进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)

进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"

进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

为什么要有线程?

线程的存在主要是为了提高程序的并发性和资源利用效率,具体原因包括:

并发执行:线程允许在同一进程内同时执行多个任务,提高程序的响应能力和处理速度。例如,在用户界面中,一个线程可以处理用户输入,而另一个线程执行后台任务。资源共享同一进程内的线程共享内存和资源,使得数据交换更加高效,减少了内存开销和上下文切换的成本。

响应性:多线程可以提高应用程序的响应性,尤其在需要进行长时间操作时(如网络请求或文件处理),可以保持用户界面的流畅性。

提高性能:在多核处理器上,线程可以充分利用硬件资源,实现真正的并行执行,提高计算性能。

简化程序设计:某些应用逻辑(如事件处理、任务调度等)可以通过多线程设计得更简单和清晰。

总之,线程的使用可以显著提升程序的效率、性能和用户体验。

每个进程都有自己的地址空间,即进程空间。一个服务器通常需要接收大量并发请求,为每一个请求都创建一个进程系统开销大、请求响应效率低,因此操作系统引进线程。

线程和进程的关系

一个程序至少一个进程,一个进程至少一个线程,进程中的多个线程是共享进程的资源。

Java 中当我们启动 main 函数时候就启动了一个 JVM 的进程,而 main 函数所在线程就是这个进程中的一个线程,也叫做主线程。

一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器,栈区域。

进程和线程区别

本质:进程是操作系统资源分配的基本单位;线程是任务调度和执行的基本单位

内存分配:系统在运行的时候会为每个进程分配不同的内存空间,建立数据表来维护代码段、堆栈段和数据段;除了 CPU 外,系统不会为线程分配内存,线程所使用的资源来自其所属进程的资源

资源拥有:进程之间的资源是独立的,无法共享;同一进程的所有线程共享本进程的资源,如内存,CPU,IO 等

开销:每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行程序计数器和栈,线程之间切换的开销小

通信:进程间以IPC(管道,信号量,共享内存,消息队列,文件,套接字等)方式通信 ;同一个进程下,线程间可以共享全局变量、静态变量等数据进行通信,做到同步和互斥,以保证数据的一致性

调度和切换:线程上下文切换比进程上下文切换快,代价小

执行过程:每个进程都有一个程序执行的入口,顺序执行序列;线程不能够独立执行,必须依存在应用程序中,由程序的多线程控制机控制

健壮性:每个进程之间的资源是独立的,当一个进程崩溃时,不会影响其他进程;同一进程的线程共享此线程的资源,当一个线程发生崩溃时,此进程也会发生崩溃,稳定性差,容易出现共享与资源竞争产生的各种问题,如死锁等

可维护性:线程的可维护性,代码也较难调试,bug 难排查

并发编程的缺点?

  1. Java 中的线程对应是操作系统级别的线程,线程数量控制不好,频繁的创建、销毁线程和线程间的切换,比较消耗内存和时间。
  2. 容易带来线程安全问题。如线程的可见性、有序性、原子性问题,会导致程序出现的结果与预期结果不一致。
  3. 多线程容易造成死锁、活锁、线程饥饿等问题。此类问题往往只能通过手动停止线程、甚至是进程才能解决,影响严重。
  4. 对编程人员的技术要求较高,编写出正确的并发程序并不容易。
  5. 并发程序易出问题,且难调试和排查;问题常常诡异地出现,又诡异地消失。

什么是守护线程?

  1. Java线程分为用户线程和守护线程。
  2. 守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。
  3. Java中把线程设置为守护线程的方法:在 start 线程之前调用线程的 setDaemon(true) 方法。

什么是线程死锁?

“线程死锁是指由于两个或者多个线程互相持有所需要的资源,导致这些线程一直处于等待其他线程释放资源的状态,无法前往执行,如果线程都不主动释放所占有的资源,将产生死锁。”

如何避免死锁

避免一个线程同时获取多个锁。

避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源

尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。

对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

并发和并行

并行:指两个或两个以上事件或活动在同一时刻发生。如多个任务在多个 CPU 或 CPU 的多个核上同时执行,不存在 CPU 资源的竞争、等待行为。

并发:通过多个执行器同时执行一个任务来缩短执行时间、提高执行效率的方法。

并行与并发的区别是什么?

  1. 并行指多个事件在同一个时刻发生;并发指在某时刻只有一个事件在发生,某个时间段内由于 CPU 交替执行,可以发生多个事件。
  2. 并行没有对 CPU 资源的抢占;并发执行的线程需要对 CPU 资源进行抢占。
  3. 并行执行的线程之间不存在切换;并发操作系统会根据任务调度系统给线程分配线程的 CPU 执行时间,线程的执行会进行切换。

java创建线程的四种方式

继承 Thread 类创建线程

通过 Runnable 接口创建线程类

使用 Callable 接口和 FutureTask 类实现创建

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

ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。返回结果的线程是在JDK1.5中引入的新特征,确实很实用,有了这种特征我就不需要再为了得到返回值而大费周折了,而且即便实现了也可能漏洞百出。可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须实现Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。

Runnable接口和Callable接口有何区别

最大的区别,runnable 没有返回值,而实现 callable 接口的任务线程能返回执行结果

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

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

如何停止一个正在运行的线程?

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  2. 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
  3. 使用interrupt方法中断线程。

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

线程通常有五种状态

新建状态(New):新创建了一个线程对象。

就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。

阻塞的情况又分为三种

等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法

同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。

其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法。

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

  1. notify唤醒一个处于等待状态的线程,notifyAll()唤醒所有处入等待状态的线程;
  2. notify可能会导致死锁,而notifyAll则不会,任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码;
  3. notify并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。notifyAll可以理解为把他们排进一个队列;只不过只有头部的线程获得了锁,才能运行;注意!!并不是给所有唤醒线程一个对象的锁,而是让它们竞争,当其中一个线程运行完就开始运行下一个已经被唤醒的线程,因为锁已经转移了。

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

  1. 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
  2. sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
  3. 当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。

volatile 是什么?

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

(1) 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。

(2) 禁止进行指令重排序。

Thread 类中的start() 和 run() 方法有什么区别?

  1. 通过start()方法来启动一个线程,此时线程处于就绪状态,可以被JVM来调度执行,在调度过程中,JVM通过调用线程类的run()方法来完成实际的业务逻辑,当run()方法结束后,此线程就会终止,所以通过start()方法可以达到多线程的目的。
  2. 如果直接调用线程类的run()方法,会被当做一个普通的函数调用,程序中仍然只有主线程这一个线程,即start()方法呢能够异步的调用run()方法,但是直接调用run()方法确实同步的,无法达到多线程的目的。

为什么wait, notify 和 notifyAll这些方法不在thread类里面?

原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object中因为锁属于对象。

为什么wait和notify方法要在同步块中调用?

  1. 只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法。
  2. 如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。
  3. 还有一个原因是为了避免 wait 和 notify 之间产生竞态条件。

Java中interrupted 和 isInterrupted方法的区别?

  1. interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法。
  2. isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。

synchronized关键字最主要的三种使用方式

  1. 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前获得当前对象实例的锁
  2. 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例
  3. 对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
  4. 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因 为JVM中,字符串常量池具有缓存功能!

什么是线程安全?Vector是一个线程安全类吗?

1. 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量 的值也和预期的是一样的,就是线程安全的。

2. 一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分 成两组,线程安全和非线程安全的。

3. Vector 是用同步方法来实现线程安全的。

什么是线程池?

线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。

为什么要用线程池?

1. 线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

2. 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

3. 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。

4. 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

Spring 中 Bean 是线程安全的吗?

Spring本身并没有针对Bean做线程安全的处理,所以:

  1. 如果 Bean 是无状态的,那么 Bean 则是线程安全的
  2. 如果 Bean 是有状态的,那么 Bean 则不是线程安全的

无状态Bean(Stateless Bean):就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。

有状态Bean(Stateful Bean) :就是有实例变量的对象,可以保存数据,是非线程安全的。

另外,Bean 是不是线程安全,根 Bean 的作用域没有关系,Bean 的作用域只是表示 Bean 的生命周期范围,对于任何生命周期的 Bean 都是一个对象,这个对象是不是线程安全的,还是得看这个 Bean 对象本身。

在多线程中,什么是上下文切换(context-switching)?

上下文切换是储存和恢复CPU状态的过程,它能让线程执行从中断点恢复执行

上下文切换是多任务操作系统和多线程环境的基本特征

如何强制启动一个线程?

目前是没有绝对的解决方法的 即使你用System.gc()回收 也不能保证成功 在java里没办法强制启动一个线程 因为它是被线程调度器控制

如何创建守护线程?

使用Thread类的setDaemon(true)方法可以将线程设置为守护线程,需要注意的是,需要在调用start()方法前调用这个方法,否则会抛出IllegalThreadStateException异常

同步方法和同步块,哪个是更好的选择?

同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁

什么是ThreadGroup?为什么不建议使用它?

ThreadGroup是一个类,它的目的是提供关于线程组的信息。

ThreadGroup API比较薄弱,它并没有比Thread提供了更多的功能。它有两个主要的功能:一是获取线程组中处于活跃状态线程的列表;二是为线程设置未捕获异常处理器。但在Java 1.5中Thread类也添加了,所以ThreadGroup是已经过时的,不建议继续使用。

对线程优先级的理解是什么?

每一个线程都是有优先级的,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的。

Java中的wait和sleep的区别与联系?

sleep是线程中的方法,但是wait是Object中的方法。

sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。

sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。

sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要(不指定时间需要被别人中断)。

什么是线程安全?

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

Vector是一个线程安全类吗?

Vector是一个线程安全的类

在add()等操作上添加synchronized关键字实现同步,但是并非是绝对的线程安全类.

当进行迭代遍历时,如果在另一个线程执行add(),remove()操作,仍然会有机率抛出异常ConcurrentModificationException.

在 java 程序中怎么保证多线程的运行安全?

使用自动锁 synchronized。

使用手动锁 Lock。

使用线程安全的类 如:java.util.concurrent下的类,Vector.HashTable、StringBuffer。

保证一个或者多个操作在CPU执行的过程中不被中断。

保证一个线程对共享变量的修改,另外一个线程能够立刻看到。

保证程序执行的顺序按照代码的先后顺序执行。

什么是线程池?

线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。

四种线程池的创建

newCachedThreadPool 创建一个可缓存线程池。

newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。

newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

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

线程池的优点

重用存在的线程,减少对象创建销毁的开销。

可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

提供定时执行、定期执行、单线程、并发数控制等功能。

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

execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多submit返回一个Future对象,如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常。

线程数过多会造成什么异常?

线程的生命周期开销非常高消耗过多的CPU资源,线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销降低稳定性。

标签:面试题,执行,Java,对象,一个,线程,进程,多线程,方法
From: https://blog.csdn.net/m0_54144956/article/details/143281163

相关文章