Java多线程面试题(面试必备)

文章目录一、多线程基础基础知识1.并发编程1.1并发编程的优缺点1.2并发编程的三要素1.3并发和并行有和区别1.4什么是多线程,多线程的优劣?2.线程与进程2.1什么是线程与进程2.2线程与进程的区别2.3用户线程与守护线程2.4什么是线程死锁2.5形成死锁的四个必要条件2.6如何避免死锁3.创建线程的四种方式4.线程状态和基本操作一、多线程基础基础知识1.并发编程1.1并发编程的优缺点优点:充分利用多核CPU的计算能力,通过并发编程的形式将多核CPU的计算.

大家好,又见面了,我是你们的朋友全栈君。

能力有限,初级菜?,多线程可是一块庞大的知识块,慢慢总结吧!

文章目录

一、多线程基础基础知识

1. 并发编程

1.1 并发编程的优缺点

优点

  • 充分利用多核CPU的计算能力,通过并发编程的形式将多核CPU的计算能力发挥到极致,性能得到提升。
  • 方面进行业务的拆分。提高系统并发能力和性能:高并发系统的开发,并发编程会显得尤为重要,利用好多线程机制可以大大提高系统的并发能力及性能;面对复杂的业务模型,并行程序会比串行程序更适应业务需求,而并发编程更适合这种业务拆分。cai

缺点

  • 并发编程的目的是为了提高程序的执行效率,提高程序运行速度,但并发编程并不是总能提高性能,有时还会遇到很多问题,例如:内存泄漏,线程安全,死锁等。

1.2 并发编程的三要素

并发编程的三要素:(也是带来线程安全所在)

  1. 原子性:原子是不可再分割的最小单元,原子性是指一个或多个操作要么全部执行成功,要么全部执行失败。
  2. 可见性:一个线程对共享变量的修改,另一个线程能看到(synchronized,volatile)
  3. 有序性:程序的执行顺序按照代码的先后顺序

线程安全的问题原因有:

1. 线程切换带来的原子性问题
2. 缓存导致的可见性问题
3. 编译优化带来的有序性问题

解决方案:

  • JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
  • synchronized、volatile、LOCK,可以解决可见性问题
  • Happens-Before 规则可以解决有序性问题

1.3 并发和并行有和区别

并发:多个任务在同一个CPU上,按照细分的时间片轮流交替执行,由于时间很短,看上去好像是同时进行的。
并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的同时进行。
串行:有n个任务,由一个线程按照顺序执行。

1.4 什么是多线程,多线程的优劣?

定义:多线程是指程序中包含多个流,即在一个程序中可以同时进行多个不同的线程来执行不同的任务
优点:

  • 可以提高CPU的利用率,在多线程中,一个线程必须等待的时候,CPU可以运行其它线程而不是等待,这样就大大提高了程序的效率,也就是说单个程序可以创建多个不同的线程来完成各自的任务。
    缺点:
  • 线程也是程序,线程也需要占内存,线程也多内存也占的也多。
  • 多线程需要协调和管理,所以需要CPU跟踪线程。
  • 线程之间共享资源的访问会相互影响,必须解决禁用共享资源的问题。

2. 线程与进程

2.1 什么是线程与进程

进程:内存中运行的运用程序,每个进程都有自己独立的内存空间,一个进程可以由多个线程,例如在Windows系统中,xxx.exe就是一个进程。
线程:进程中的一个控制单元,负责当前进程中的程序执行,一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可以共享数据。

2.2 线程与进程的区别

根本区别:进程是操作系统资源分配的基本单元,而线程是处理器任务调度的和执行的基本单位。
资源开销:每个进程都有自己独立的代码和空间(程序上下文),程序之间的切换会有较大的开销;线程可以看作轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行的过程不是一条线的,而是多条线(多个线程),共同完成;线程是进程的一部分,可以把线程看作是轻量级的进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。

2.3 用户线程与守护线程

用户(User)线程:运行在前台,执行具体任务,如程序的主线程,连接网络的子线程都是用户线程。
守护(Daemon)线程:运行在后台,为其它前台线程服务,也可以说守护线程是JVM非守护线程的”佣人“,一旦所有线程都执行结束,守护线程会随着JVM一起结束运行。
main函数就是一个用户线程,main函数启动时,同时JVM还启动了好多的守护线程,如垃圾回收线程,比较明显的区别时,用户线程结束,JVM退出,不管这个时候有没有守护线程的运行,都不会影响JVM的退出。

2.4 什么是线程死锁

死锁是指两个或两个以上进程(线程)在执行过程中,由于竞争资源或由于彼此通信造成的一种堵塞的现象,若无外力的作用下,都将无法推进,此时的系统处于死锁状态。
如图,线程A拥有的资源2,线程B拥有的资源1,此时线程A和线程B都试图去拥有资源1和资源2,但是它们的?还在,因此就出现了死锁。
在这里插入图片描述

2.5 形成死锁的四个必要条件

  1. 互斥条件:线程(进程)对所分配的资源具有排它性,即一个资源只能被一个进程占用,直到该进程被释放。
  2. 请求与保持条件:一个进程(线程)因请求被占有资源而发生堵塞时,对已获取的资源保持不放。
  3. 不剥夺条件:线程(进程)已获取的资源在未使用完之前不能被其他线程强行剥夺,只有等自己使用完才释放资源。
  4. 循环等待条件:当发生死锁时,所等待的线程(进程)必定形成一个环路,死循环造成永久堵塞。

2.6 如何避免死锁

我们只需破坏形参死锁的四个必要条件之一即可。
破坏互斥条件:无法破坏,我们的?本身就是来个线程(进程)来产生互斥
破坏请求与保持条件:一次申请所有资源
破坏不剥夺条件:占有部分资源的线程尝试申请其它资源,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件:按序来申请资源。

2.7 什么是上下文的切换

当前任务执行完,CPU时间片切换到另一个任务之前会保存自己的状态,以便下次再切换会这个任务时可以继续执行下去,任务从保存到再加载执行就是一次上下文切换。

3. 创建线程

3.1 创建线程的四种方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. Executors工具类创建线程池

3.2 Runnable接口和Callable接口有何区别

相同点

  1. Runnable和Callable都是接口
  2. 都可以编写多线程程序
  3. 都采用Thread.start()启动线程

不同点

  1. Runnable接口run方法无返回值,Callable接口call方法有返回值,是个泛型,和Futrue和FutureTask配合用来获取异步执行结果。
  2. Runable接口run方法只能抛出运行时的异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息。

:Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会堵塞主线程继续往下执行,如果不调用就不会堵塞。

3.2 run()方法和start()方法有和区别

每个线程都是通过某个特定的Thread对象对于的run()方法来完成其操作的,run方法称为线程体,通过调用Thread类的start方法来启动一个线程。
start()方法用于启动线程,run()方法用于执行线程的运行代码,run()可以反复调用,而start()方法只能被调用一次。

start()方法来启动一个线程,真正实现了多线程的运行。调用start()方法无需等待run()方法体代码执行结束,可以直接继续执行其它的代码;调用start()方法线程进入就绪状态,随时等该CPU的调度,然后可以通过Thread调用run()方法来让其进入运行状态,run()方法运行结束,此线程终止,然后CPU再调度其它线程。

3.3 为什么调用start()方法会执行run()方法,为什么不能直接调用run()方法

这是一个常问的面试题,new Thread,线程进入了新建的状态,start方法的作用是使线程进入就绪的状态,当分配到时间片后就可以运行了。start方法会执行线程前的相应准备工作,然后在执行run方法运行线程体,这才是真正的多线程工作。
如果直接执行了run方法,run方法会被当作一个main线程下的普通方法执行,并不会在某个线程中去执行它,所以这并不是多线程工作。
小结
调用start方法启动线程可使线程进入就绪状态,等待运行;run方法只是thread的一个普通方法调用,还是在主线程里执行。

3.4 什么是Callable和Future

Callable接口也类似于Runnable接口,但是Runnable不会接收返回值,并且无法抛出返回结果的异常,而Callable功能更强大,被线程执行后,可有返回值,这个返回值可以被Future拿到,也就是说Future可以拿到异步执行任务的返回值。
Future接口表示异步任务,是一个可能没有完成的异步任务结果,所以说Callable用于产生结果,Future用于接收结果。

3.5 什么是FutureTask

FutureTask是一个异步运算的任务,FutureTask里面可以可以传入Callable实现类作为参数,可以对异步运算任务的结果进行等待获取,判断是否已经完成,取消任务等操作。只有当结果完成之后才能取出,如果尚未完成get方法将堵塞。一个Future对象可以调用Callable和Runable的对象进行包装,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

4. 线程状态和基本操作

4.1 线程声明周期的6种状态

很多地方说线程有5种状态,但实际上是6中状态,可以参考Thread类的官方api

public enum State { 
   
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

如图:
在这里插入图片描述
新创建:又称初始化状态,这个时候Thread才刚刚被new出来,还没有被启动。
可运行状态:表示已经调用Thread的start方法启动了,随时等待CPU的调度,此状态又被称为就绪状态。
被终止:死亡状态,表示已经正常执行完线程体run()中的方法了或者因为没有捕获的异常而终止run()方法了。
计时状态:调用sleep(参数)或wait(参数)后线程进入计时状态,睡眠时间到了或wait时间到了,再或者其它线程调用notify并获取到锁之后开始进入可运行状态。另一种情况,其它线程调用notify没有获取到锁或者wait时间到没有获取到锁时,进入堵塞状态。
无线等待状态:获取锁对象后,调用wait()方法,释放锁进入无线等待状态
锁堵塞状态:wait(参数)时间到或者其它线程调用notify后没有获取到锁对象都会进入堵塞状态,只要一获取到锁对象就会进入可运行状态。

堵塞状态的详解:
在这里插入图片描述

4.2 Java用到的线程调度算法是什么?

计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获取到CPU的使用权才能执行指令,所谓多线程的并发运行,其实从宏观上看,各线程轮流获取CPU的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待,CPU的调度,JVM有一项任务就是负责CPU的调度,线程调度就是按照特定的机制为多个线程分配CPU的使用权。
有两种调度模型:分时调度和抢占式调度
分时调度就是让所有的线程轮流获得CPU的使用权,并且平均分配到各个线程占有CPU的时间片。
抢占式调度:Java虚拟机采用抢占式调度模型,是指优先让线程池中优先级高的线程首先占用CPU,如果线程池中优先级相同,那么随机选择一个线程,使其占有CPU,处于这个状态的CPU会一直运行,优先级高的分的CPU的时间片相对会多一点。

4.2 Java线程调度策略

线程调度优先选择优先级高的运行,但是如果出现一下情况,就会终止运行(不是进入死亡状态):

  1. 线程调用了yield方法让出CPU的使用权,线程进入就绪状态。
  2. 线程调用sleep()方法,使其进入计时状态
  3. 线程由于IO受阻
  4. 另一个更高的优先级线程出现
  5. 在支持的时间片系统中,改线程的时间片用完。

4.3 什么是线程调度(Thread Scheduler)和时间分片(Time Slicing )

线程调度是一个操作系统服务,它负责为储在Runnable状态的线程分配CPU时间片,一旦我们创建一个线程并启动它,它的执行便依赖线程调度器的实现。
时间分片是指CPU可用时间分配给Runnable的过程,分配的时间可以根据线程优先级或线程等待时间。

4.4 Java线程同步和线程调度的相关方法

  1. wait():调用后线程进入无限等待状态,并释放所持对象的锁
  2. sleep():使一个线程进入休眠状态(堵塞状态),带有对象锁,是一个静态方法,需要处理InterruptException异常。
  3. notify():唤醒一个处于等待状态的线程(无线等待或计时等待),如果多个线程在等待,并不能确切的唤醒一个线程,与JVM确定唤醒那个线程,与其优先级有关。
  4. notityAll():唤醒所有处于等待状态的线程,但是并不是将对象的锁给所有的线程,而是让它们去竞争,谁先获取到锁,谁先进入就绪状态。

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

两者都可以使线程进入等待状态

  • 类不同:sleep()是Thread下的静态方法,wait()是Object类下的方法
  • 是否释放锁:sleep()不释放锁,wait()释放锁
  • 用处不同:wait()常用于线程间的通信,sleep()常用于暂停执行。
  • 用法不同:wait()用完后,线程不会自动执行,必须调用notify()或notifyAll()方法才能执行,sleep()方法调用后,线程经过过一定时间会自动苏醒,wait(参数)也可以传参数使其苏醒。它们苏醒后还有所区别,因为wait()会释放锁,所以苏醒后没有获取到锁就进入堵塞状态,获取到锁就进入就绪状态,而sleep苏醒后之间进入就绪状态,但是如果cpu不空闲,则进入的是就绪状态的堵塞队列中。

4.6 你是如何调用wait()方法的,使用if还是循环

处以等待状态的线程可能会收到错误警告或伪唤醒,如果不在循环中检查等待条件,程序可能会在没有满足条件的时候退出。

synchronized (monitor) { 
   
    // 判断条件谓词是否得到满足
    while(!locked) { 
   
        // 等待唤醒
        monitor.wait();
    }
    // 处理其他的业务逻辑
}

4.7 为什么线程通信方法wait(),notify(),notifyAll()要被定义到Object类中

Java中任何对象都可以被当作锁对象,wait(),notify(),notifyAll()方法用于等待获取唤醒对象去获取锁,Java中没有提供任何对象使用的锁,但是任何对象都继承于Object类,所以定义在Object类中最合适。

有人会说,既然是线程放弃对象锁,那也可以把wait()放到Thread类中,新定义线程继承Thread类,也无需重新定义wait(),
然而,这样做有一个很大的问题,因为一个线程可以持有多把锁,你放弃一个线程时,到底要放弃哪把锁,当然了这种设计不能不能实现,只是管理起来比较麻烦。
综上:wait(),notify(),notifyAll()应该要被定义到Object类中。

4.8 为什么线程通信方法wait(),notify(),notifyAll()要在同步代码块或同步方法中被调用?

wait(),notify(),notifyAll()方法都有一个特点,就是对象去调用它们的时候必须持有锁对象。
如对象调用wait()方法后持有的锁对象就释放出去,等待下一个线程来获取。
如对象调用notifyAll()要唤醒等待中的线程,也要讲自身用于的锁对象释放,让就绪状态中的线程竞争获取锁。
由于这些方法都需要线程持有锁对象,这样只能通过同步来实现,所以它们只能在同步块或同步方法中被调用。

4.9 Thread的yiele方法有什么作用?

让出CPU的使用权,使当前线程从运行状态进入就绪状态,等待CPU的下次调度。

4.10 为什么Thread的sleep和yield是静态的?

Thread类的sleep()和yield()方法将在当前正在运行的线程上工作,所以其它处于等待状态的线程调用它们是没有意义的,所以设置为静态最合适。

4.11 线程sleep和yield方法有什么区别

  • 线程调用sleep()方法进入堵塞状态,醒来后因为(没有释放锁)后直接进入了就绪状态,运行yield后也没有释放锁,于是进入了就绪状态。
  • sleep()方法使用时需要处理InterruptException异常,而yield没有。
  • sleep()执行后进入堵塞状态(计时等待),醒来后进入就绪状态(可能是堵塞队列),而yield是直接进入就绪状态。

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

  1. 使用stop方法终止,但是这个方法已经过期,不被推荐使用。
  2. 使用interrupt方法终止线程
  3. run方法执行结束,正常退出

4.13 如何在两个线程间共享数据?

两个线程之间共享变量即可实现共享数据。
一般来说,共享变量要求变量本身是线程安全的,然后在线程中对变量使用。

4.14 同步代码块和同步方法怎么选?

同步块是更好的选择,因为它不会锁着整个对象,当然你也可以然它锁住整个对象。同步方法会锁住整个对象,哪怕这个类中有不关联的同步块,这通常会导致停止继续执行,并等待获取这个对象锁。
同步块扩展性比较好,只需要锁住代码块里面相应的对象即可,可以避免死锁的产生。
原则:同步范围也小越好。

4.15 什么是线程安全?Servlet是线程安全吗?

线程安全是指某个方法在多线程的环境下被调用时,能够正确处理多线程之间的共享变量,能程序能够正确完成。
Servlet不是线程安全的,它是单实例多线程的,当多个线程同时访问一个方法时,不能保证共享变量是安全的。
Struts2是多实例多线程的,线程安全,每个请求过来都会new一个新的action分配这个请求,请求完成后销毁。
springMVC的controller和Servlet一样,属性单实例多线程的,不能保证共享变量是安全的。
Struts2好处是不用考虑线程安全问题,springMVC和Servlet需要考虑。
如果想既可以提升性能又可以不能管理多个对象的话建议使用ThreadLocal来处理多线程。

4.16 线程的构造方法,静态块是被哪个线程类调用的?

线程的构造方法,静态块是被哪个线程类调用的?
该线程在哪个类中被new出来,就是在哪个被哪个类调用,而run方法是线程类自身调用的。
例子:mian函数中new Thread2,Thread2中new Thread1
在这里插入图片描述
thread1线程的构造方法,静态块是thread2线程调用的,run方法是thread1调用的。
thread2线程的构造方法,静态块是main线程调用的,run方法是thread2调用的。

4.17 Java中是如何保证多线程安全的?

  1. 使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
  2. 使用自动锁,synchronized锁
  3. Lock lock = new ReentrantLock(),使用手动锁lock .lock(),lock.unlock()方法

4.18 线程同步和线程互斥的区别

线程同步:当一个线程对共享数据进行操作的时候,在没有完成相关操作时,不允许其它的线程来打断它,否则就会破坏数据的完整性,必然会引起错误信息,这就是线程同步。
线程互斥
而线程互斥是站在共享资源的角度上看问题,例如某个共享资源规定,在某个时刻只能一个线程来访问我,其它线程只能等待,知道占有的资源者释放该资源,线程互斥可以看作是一种特殊的线程同步。
实现线程同步的方法

  1. 同步代码块:sychronized(对象){} 块
  2. 同步方法:sychronized修饰的方法
  3. 使用重入锁实现线程同步:reentrantlock类的锁又互斥功能,Lock lock = new ReentrantLock(); Lock对象的ock和unlock为其加锁

4.19 你对线程优先级有什么理解?

每个线程都具有优先级的,一般来说,高优先级的在线程调度时会具有优先被调用权。我们可以自定义线程的优先级,但这并不能保证高优先级又在低优先级前被调用,只是说概率有点大。
线程优先级是1-10,1代表最低,10代表最高。
Java的线程优先级调度会委托操作系统来完成,所以与具体的操作系统优先级也有关,所以如非特别需要,一般不去修改优先级。

4.20 谈谈你对乐观锁和悲观锁的理解?

乐观锁:每个去拿数据的时候都认为别人不会修改,所以不会都不会上锁,但是在更新的时候会判断一下在此期间有没有去更新这个数据。所以乐观锁使用了多读的场合,这样可以提高吞吐量,像数据库提供的类似write_condition机制,都是用的乐观锁,还有那个原子变量类,在java.util.concurrent.atomic包下
悲观锁:总是假设最坏的情况,每次去拿数据的时候都会认为有人会修改,所以每次在拿数据的时候都会上锁。这样别的对象想拿到数据,那就必须堵塞,直到拿到锁。传统的关系型数据库用到了很多这种锁机制,比如读锁,写锁,在操作之前都会先上锁,再比如Java的同步代码块synchronized/方法用的也是悲观锁。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/126154.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • 此av非彼”AV”

    此av非彼”AV”作者:王亨 ,R语言中文社区专栏作者,跟着菜鸟一起一步步学习R语言,争做R语言高手。个人公众号:跟着菜鸟一起学R语言(微信ID:learn_R) 最近发现一个特别有意思的…

  • 嵌入式Linux–menuconfig详解

    嵌入式Linux–menuconfig详解menuconfig工作原理menuconfig是一套图像化配置工具,由ncurses库提供软件支持。ncurses库提供了一系列的函数以便使用者调用它们去生成基于文本的用户界面。menuconfig本身的软件只负责提供menuconfig工作的这一套逻辑,比如说通过上下左右调整光标,Enter选中等,并不负责提供内容。menuconfig运行之后会读取Kconfig、读取/写入….

  • linux查看redis命令,linux查看redis版本怎么操作?具体示例

    linux查看redis命令,linux查看redis版本怎么操作?具体示例对于有相关开发经验的朋友来说,linux作为一套免费使用和自由传播的类UNIX操作系统,相信你们肯定是比较亲切的,那么今天我们一起了解的是,怎么用linux查看redis版本号?工具/原料:linux,redis方法/步骤:登录Linux服务器,使用命令:whereisredis查找到redis的安装目录。用cd命令进入该目录。进入该目录下的bin目录。使用ls命令列出该目录下的文件结构,可以发…

  • uCOSII操作系统移植笔记

    uCOSII操作系统移植笔记笔记一:今天粗略的看了一下周立功关于uc/osII在lpc2104上的移植方面的说明,这之中印象最深的应该是irq中断和软中断方面的处理,由于arm芯片的特殊性(拥有7种处理器模式),即每种处理器模式都有自己的堆栈,这样在处理堆栈的时候就会相应的麻烦一些。在响应异常时,该移植计划在初始代码里面比在没有操作系统的初始代码多了irq的处理,移植里面的irq处理多了由汇编语言编写的对任务环境的保存,

  • Activiti流程引擎_activiti工作流原理

    Activiti流程引擎_activiti工作流原理Activiti框架提供的流程引擎配置类ProcessEngineConfiguration的类图如下:下面的图是流程引擎的架构图:由上图我们可以很清楚地从全局角度了解ProcessEngineConfiguration类:1)EngineServices:该接口中定义了获取各种服务类实例对象的方法。2)ProcessEngine:继承EngineServices接口,并增…

    2022年10月20日
  • 欧拉函数最全总结

    欧拉函数最全总结文章目录欧拉函数的内容一、欧拉函数的引入二、欧拉函数的定义三、欧拉函数的性质四、欧拉函数的计算方法(一)素数分解法(二)编程思维1.求n以内的所有素数2.求φ(n)3.格式化输出0-100欧拉函数表(“x?”代表十位数,“x”代表个位数)五、欧拉函数相关定理以及证明(一)定理1:缩系与欧拉函数的关系(二)定理2:缩系的充要条件(三)定理3:缩系拓展1.简单证明:(a,m)=1,(x,m)=1,故(ax,m)=1。(四)定理4:设m>1,(a,m)=1,则aφ(m)≡1(modm).1.**若ac≡bc

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号