并发
- 1.并发编程的优缺点?
- 2.并发编程三要素?
- 3.什么叫指令重排?
- 4.如何避免指令重排?
- 5.并发?并行?串行?
- 6.线程和进程的概念和区别?
- 7.什么是上下文切换?
- 8.守护线程和用户线程的定义?
- 9.什么是线程死锁?
- 10.形成死锁的四个条件?
- 11.怎么避免死锁?
- 12.创建线程的四种方式?
- 13.runable和callable区别?
- 14.run()和start()的区别?
- 15.什么是futureTask?
- 16.为什么我们调用start()方法会执行run()方法,为什么我们不能直接调用run()方法?
- 17.线程生命周期及五种状态的转换?
- 18.线程调度的几种模型?
- 19.线程调度策略?
- 20.什么是线程调度器和时间分片?
- 21.wait、sleep、yield区别?notify、notifyAll区别?
- 22.sleep、yield为什么是静态的?
- 23.如何调用wait()?使用if块还是循环块?
- 24.为什么线程通信方法wait()、notify()、notifyAll()定义在Object中?
- 25.为什么wait()、notify()、notifyAll()必须在同步方法或者同步块中被调用?
- 26.如何停止一个正在运行的线程?
- 27.interrupt、interrupted和isInterrupt方法的区别?
- 28.怎么唤醒阻塞线程?
- 29.什么是阻塞式方法?
- 30.实现线程同步的方法?
- 31.同步方法和同步块是什么?
- 32.线程池的工作原理?
- 33.创建线程池都有哪些方式?
- 34.线程池常用的几个参数?
- 35.线程池的拒绝策略有哪些?
- 36.线程池都有哪些状态?
- 37.线程池中submit()和execute()的区别?
- 38.当你提交任务时,线程池队列已满,这时会发生什么?
- 39.synchronized使用方式?
- 40.synchronized的锁升级的过程
- 41.synchronized底层原理?
- 42.什么是自旋?
- 43.synchronized可重入的原理?
- 44.synchronized和volicate区别?
- 45.Lock和synchronized区别?
- 46.synchronized和ReentrantLock可重入锁的区别?
- 47. ReentrantLock是什么?
- 48.synchronized为什么不能集群操作?如果想集群操作用什么?
- 49.线程池用完扔回线程池是什么状态?
- 50.CAS和ABA的问题?
- 51.线程池的线程数是怎么确定的?
- 52.ThredLocal是什么?以及使用场景?
- 53.什么是临界区?
- 53.ab同时提交线程完成任何一个就去执行C,用什么来完成?
- 54.ab同时提交线程,需要判断结果,就去执行C,用什么来完成?
- 55.CountDownLatch是什么?
- 56.ExecutorService是什么?
- 55.AQS简单介绍一下?
- 55.CLH是什么?
- 55.线程池的应用场景?
- 54.为什么要使用并发编程?
- 55.双重校验锁实现对象单例模式?
- 56.多线程的应用场景?
1.并发编程的优缺点?
优点:
- 充分利用多核CPU的计算能力,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升;
- 方便业务拆分,提升系统并发能力和性能;
缺点:
- 内存泄漏;
- 线程安全问题;
- 复杂程度增加:比如死锁;
- 资源消耗增加:比如频繁的上下文切换也可能导致额外的性能开销;
2.并发编程三要素?
- 原子性:原子,就是一个不可再被分割的颗粒。原子性就是指一个或多个操作要么全部执行成功要么全部执行失败;
- 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到;
- 有序性:程序执行的顺序按照代码先后顺序执行。(处理器可能会对指令进行重排序)
3.什么叫指令重排?
指令重排(也称为指令重排序)是指在程序执行过程中,指令的执行顺序可能与它们在代码中的顺序不一致的现象。编译器和处理器为了提高程序的执行效率,可能会根据一些规则和优化策略对指令进行重新排序。但是,这种重排序必须保证最终的执行结果与不进行重排时的执行结果保持一致,以确保程序的正确性;存在数据依赖关系的也不允许指令重排
指令重排主要是基于处理器的特性,如多级缓存、多核等,来优化指令的执行顺序。这种优化可以使程序在保证业务运行的同时,充分利用CPU的资源,发挥最大的性能。然而,指令重排也可能会导致线程安全问题,特别是在多线程环境下。因此,在编写并发程序时,需要特别注意指令重排的影响,并采取相应的措施来确保程序的正确性和性能。
4.如何避免指令重排?
- 使用volatile关键字:在Java中,volatile关键字可以确保多线程环境下变量的可见性和有序性。当一个变量被声明为volatile时,它会禁止指令重排,确保所有线程看到的变量值都是一致的。volatile关键字还可以防止JVM的指令重排优化,确保代码的执行顺序与预期一致。
- 使用synchronized关键字:synchronized关键字可以用来保证代码块或方法的原子性,即在同一时刻只能有一个线程执行被保护的代码。通过synchronized块或方法,可以确保指令按照预期的顺序执行,避免指令重排导致的线程安全问题。
- 使用Lock接口及其实现类:Java中的Lock接口及其实现类(如ReentrantLock)也可以用来控制并发访问,保证代码的正确执行顺序。与synchronized相比,Lock接口提供了更灵活的控制方式,可以更好地避免指令重排带来的问题。
- 避免使用final关键字修饰引用类型变量:在Java中,final关键字修饰的引用类型变量在初始化后不能被改变。但是,如果final变量指向的对象是可变的,那么其他线程仍然可以修改该对象的内容。因此,在使用final关键字时,需要特别注意避免指令重排导致的线程安全问题。
- 了解并遵循Happens-Before规则:Happens-Before规则是Java内存模型定义的一组规则,用于确定多线程环境中哪些操作是有序的。遵循这些规则可以确保指令按照预期的顺序执行,避免指令重排导致的线程安全问题。
5.并发?并行?串行?
- 并发:多个任务在同一个cpu上,按细分的时间片轮流执行,从逻辑上来看那些任务是同事执行的;(两个队列一台咖啡机)
- 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的同时进行;(两个队列一两台咖啡机)
- 串行:有n个任务,由一个线程按顺序执行,犹豫任务,方法都在一个线程执行,所以不存在线程不安全情况,也就不存在临界区的问题;(一个队列一台咖啡机)
6.线程和进程的概念和区别?
进程:是操作系统资源分配的基本单位,一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程可以有多个线程。
线程:处理器任务调度和执行的基本单位,又叫做轻型进程;进程中的一个执行任务,负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据;
区别:
- 资源开销:进程是系统分配资源的基本单位,它拥有独立的内存空间和系统资源,因此创建和销毁一个进程需要较大的开销,包括内存分配、上下文切换等。而线程是进程内的一条执行路径,多个线程共享同一个进程的内存空间和资源,因此创建和销毁一个线程的开销相对较小。
- 执行方式:进程是独立执行的,拥有自己的地址空间和资源,相互之间通过进程间通信(IPC)进行交互。而线程是进程内的一条执行路径,多个线程之间共享进程的资源,因此它们之间的通信和同步更为直接和高效。
- 并发性:进程在并发执行时具有更高的稳定性,因为每个进程都是独立的执行单元,拥有自己的调度算法。而线程之间的调度和同步相对复杂,需要更多的注意,以避免出现竞态条件、死锁等问题。
- 独立性:进程是独立的,一个进程出现问题不会影响其他进程的执行。而线程是进程的一部分,一个线程的错误可能导致整个进程的崩溃。
7.什么是上下文切换?
CPU采取的策略是为每个线程分配时间片并轮训的形式,当前任务在执行完CPU时间片切换到另一个任务之前会保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载过程就是一次上下文切换。
上下文切换消耗大量的CPU时间,可能是操作系统中时间消耗最大的操作。
8.守护线程和用户线程的定义?
守护线程:运行在后台,为其他前台线程服务。也可以说守护线程是JVM中非守护线程的"佣人",一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作;
用户线程:运行在前台,执行具体任务,如程序的主线程,连接网络的子线程等都是用户线程。
守护线程不能依靠finally块的内容来确保执行关闭或清理资源的逻辑,因为用户线程结束,守护线程就跟着结束,所以守护线程中的finally语句块可能无法被执行;
9.什么是线程死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成一种阻塞的现象,若无外力作用,他们都将无法推进下去。
10.形成死锁的四个条件?
- 互斥条件:线程对于所分配到的资源具有排他性,即一个资源只能被一个线程占用,直到该线程释放;
- 请求与保持条件:一个线程因请求被占用资源而发生阻塞时,对已获得的资源保持不放;
- 不可剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完后才释放资源;
- 循环等待条件:当发生死锁,所等待的线程必定会形成一个环路,造成永久阻塞。
11.怎么避免死锁?
破坏产生死锁的四个条件中的其中一个:
- 破坏互斥条件:
- 破坏请求与保持条件:
- 破坏不可剥夺条件:
- 破坏循环等待条件:
12.创建线程的四种方式?
继承Thread类:
- 定义一个Thread类的子类,重写run方法,run方法里就是相关业务逻辑;
- 创建自定义的线程子类对象;
- 调用子类实例的start方法启动线程;
实现runable接口:
- 定义runnable接口实现类MyRunnable,并重新run方法;
- 创建MyRunnable实例myRunnable,以myRunnable作为target创建Thread对象,该Thread对象才是真正的线程对象;
- 调用线程对象的start方法;
实现callable接口:
- 创建实现callable接口的类myCallable;
- 以myCallable为参数创建FutureTask对象;
- 将FutureTask作为参数创建Thread对象;
- 调用线程对象的start方法;
使用Excutors工具类创建线程池:
Excutors提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
13.runable和callable区别?
相同点:
- 都是接口;
- 都可以编写多线程;
- 都采用Thread.start()启动线程;
不同点:
- Runnable接口run方法只能抛出异常,不能捕获异常,没有返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果;
14.run()和start()的区别?
run()方法称为线程体,通过调用Thread类的start()方法来启动一个线程;run()可以重复调用,start()只能调用一次;
15.什么是futureTask?
表示一个异步运算的任务,里面可以传一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候才能取回结果,如果尚未完成运算,get方法将会阻塞。
16.为什么我们调用start()方法会执行run()方法,为什么我们不能直接调用run()方法?
首先,当你调用一个线程的 start() 方法时,Java虚拟机(JVM)会为这个线程创建一个新的调用栈,并将该线程标记为可运行状态。然后,JVM会调度这个线程执行,当线程获得CPU时间片时,就会执行该线程的 run() 方法。这种方式允许线程在操作系统级别进行调度,从而能够充分利用多核CPU和操作系统提供的线程调度机制,实现真正的并发执行。
而如果你直接调用 run() 方法,那么这个方法就会在当前的调用栈中执行,它只是一个普通的方法调用,并不会启动一个新的线程。也就是说,run() 方法会在当前线程中同步执行,不会创建新的线程,也就无法实现并发。
因此,start() 方法和 run() 方法的区别在于:start() 方法用于启动一个新的线程来执行 run() 方法,而 run() 方法本身只是一个普通的方法调用,不会创建新的线程。
17.线程生命周期及五种状态的转换?
18.线程调度的几种模型?
- 分时调度模型:这种模型让所有线程轮流获得CPU的使用权,并且平均分配每个线程占用CPU的时间片。这种方式下,每个线程都会得到一定的执行时间,但也可能因为时间片过短而无法完成复杂的任务。
- 抢占式调度模型:这种模型优先让可运行池中优先级高的线程占用CPU。如果线程的优先级相同,那么就随机选择一个线程使其占用CPU。当线程丢失了CPU的使用权后,再随机选择其他线程获取CPU的使用权。这种方式下,优先级高的线程会获得更多的执行机会。
需要注意的是,线程的调度不是跨平台的,它不仅取决于JVM(Java虚拟机),还依赖操作系统。在Java中,抢占式调度模型被采用作为默认的线程调度模型。
19.线程调度策略?
线程调度器选择优先级最高的线程运行,但是遇到下面几种情况,就会终止线程的运行:
- 调用yield方法,让出cpu的占有权;
- 调用sleep方法使线程进入睡眠状态;
- 另一个更高优先级的线程出现;
- 在支持时间片的系统中,该线程的时间片用完。
20.什么是线程调度器和时间分片?
线程调度器:是一个操作系统服务,他负责为Runnable状态的线程分配CPU时间;
时间分片:是指将可用的CPU时间分配给Runnable线程的过程。
21.wait、sleep、yield区别?notify、notifyAll区别?
wait():使一个线程处于等待阻塞状态,并且释放所持对象的锁,Object类的方法,不会自动苏醒,需要调用notify、notifyAll;
sleep():使一个正在运行的状态处于睡眠状态,不释放锁,静态方法,会自动苏醒,或者等超时后就会自动苏醒;
yield():由运行状态变为就绪状态,静态方法;
notify():唤醒一个处于等待状态的线程,并不能确定唤醒哪一个线程,而是与JVM确定唤醒哪个线程,而且与优先级无关,Object类的方法;
notifyAll():唤醒所有处于等待状态的线程,该方法不是将对象的锁给所有线程,而是让他们竞争,只有获得锁才能进入就绪状态,Object类的方法;
22.sleep、yield为什么是静态的?
sleep()和yield()方法都是Thread类中的静态方法,这意味着可以直接通过类名来调用它们,而不需要创建Thread类的实例。这两个方法的行为不依赖于特定线程的状态或属性。相反,它们影响的是调用它们的线程本身。因此,将它们设计为静态方法可以使代码更加简洁,并且更符合它们的使用场景。
23.如何调用wait()?使用if块还是循环块?
应该在循环块中调用,因为,当线程获取到Cpu开始执行的时候,其他条件可能还没有满足,所以在处理之前,循环检测条件是否满足会更好。
24.为什么线程通信方法wait()、notify()、notifyAll()定义在Object中?
这几个方法都在同步代码块中调用,在java中,任何对象都可以作为锁,并且wait()、notify()等方法用于等待对象的锁或者唤醒线程,在Java的线程中并没有可供对象使用的锁,所以任意对象调用方法一定定义在Object类中。
25.为什么wait()、notify()、notifyAll()必须在同步方法或者同步块中被调用?
-
线程安全性:这些方法的设计初衷是为了实现线程间的安全通信。调用这些方法涉及到线程状态的改变(例如,从运行状态变为等待状态,或从等待状态变为可运行状态),以及线程间对共享资源的访问。为了确保这些操作的原子性和一致性,避免竞态条件,它们必须在同步块或同步方法中调用。
-
对象锁:
wait()
,notify()
, 和notifyAll()
方法与对象的内部锁(也称为监视器锁或互斥锁)紧密相关。当一个线程调用某个对象的wait()
方法时,它会释放该对象的锁,使得其他线程可以获取这个锁并执行同步块或同步方法中的代码。同样,当线程调用notify()
或notifyAll()
方法时,它会唤醒正在等待该对象锁的线程,并重新获取该对象的锁。这些操作都依赖于对象锁的存在,因此必须在同步块或同步方法中执行。 -
等待/通知机制:
wait()
、notify()
和notifyAll()
方法是等待/通知机制的一部分,该机制允许线程在等待某个条件成立时进入等待状态,并在条件满足时被其他线程唤醒。由于这个机制依赖于对象锁来同步线程间的通信,因此必须在同步块或同步方法中调用这些方法。 -
如果这些方法在非同步环境中被调用,会导致
IllegalMonitorStateException
异常,因为调用它们的线程没有持有对象的锁,无法安全地执行这些操作。
总的来说,将
wait()
,notify()
, 和notifyAll()
方法限制在同步方法或同步块中调用是为了确保线程间的安全通信和正确的同步行为。这是Java语言设计的一部分,旨在防止竞态条件和死锁等并发问题。
26.如何停止一个正在运行的线程?
- 使用interrupt方法中断线程;
- 使用stop方法强行终止,但是不推荐,因为stop已经作废了;
- run方法完成后线程终止;
27.interrupt、interrupted和isInterrupt方法的区别?
interrupt:用于中断线程,线程状态变为”中断“状态;但是线程不会停止,需要用户自己去监视线程状态并做处理;
interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号,如果一个线程被中断了,第一次调用interrupted返回true,第二次以后就是fasle了;
isInterrupt:查看当前中断信号是true还是false;
28.怎么唤醒阻塞线程?
wait、notify方法都是针对对象的,调用wait方法都将导致线程阻塞,阻塞的同时也会释放该对象的锁,notify也会唤醒阻塞线程,但是需要重新获取对象的锁,才能够往下执行;
这俩方法必须在synchronized块或者方法块中调用,并且保证同步块或方法的锁对象与调用wait、notify方法的对象是同一个,如此一来在调用wait之前线程就已经成功获取某对象的锁,执行wait阻塞后当前线程就将之前获取的对象锁释放。
29.什么是阻塞式方法?
就是指程序会一直等待该方法完成期间不做其他事情。
30.实现线程同步的方法?
- 同步代码方法:synchronized关键字修饰的方法;
- 同步代码块:synchronized关键字修饰的代码块;
- volatile:为域变量的访问提供了一种免锁机制;
- 使用重入锁实现线程同步:reentrantlock类是可冲入,互斥,实现了lock接口的锁;
31.同步方法和同步块是什么?
- 同步块:不会锁住整个对象,更符合开放调用的原则,只锁住需要用的代码块上,这样也可以避免死锁;
- 同步方法:会锁住整个对象,哪怕这个类中有多个不相关联的同步块,会导致他们停止执行并需要等待获得这个对象上的锁。
32.线程池的工作原理?
- 线程池在创建的时候会创建核心线程数,corepoolsize = 5;
- 当线程池满了,不会被立即扩容,而是放到阻塞队列中,当阻塞队列满了之后才会继续扩容;
- 如果队列满了,线程数达到最大的线程数会执行拒绝策略;
- 当线程数大于核心线程数,超过了限制时间,线程会被回收,最终保持corepoolsize数;
33.创建线程池都有哪些方式?
手动创建:通过手动编写代码来创建线程池,包括创建线程、管理线程的运行以及终止线程等操作。
使用ThreadPoolExecutor类:Java 提供了ThreadPoolExecutor类来简化线程池的创建和管理,通过该类可以设置线程池的大小、线程池中任务队列的大小以及拒绝策略等。
使用Executors类:Java提供了Executors类,该类提供了几个静态工厂方法,可以根据具体需求来创建不同类型的线程池,如固定大小的线程池、可缓存的线程池、可以执行延迟任务的线程池等。
以下是 Executors
类中常用的几个线程池方法:
-
newFixedThreadPool(int nThreads)
创建一个固定大小的线程池,当有新任务提交时,如果线程池中有空闲线程,则立即执行。如果没有,则新任务会在一个队列中等待,直到有线程空闲出来。
-
newCachedThreadPool()
创建一个可缓存的线程池,如果线程池大小超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这个线程池会对线程进行缓存,如果线程一段时间没有被使用就会处于空闲状态,因此它非常适合执行大量的异步任务。
-
newSingleThreadExecutor()
创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照提交顺序(FIFO)执行。
-
newScheduledThreadPool(int corePoolSize)
创建一个可以执行定时任务或周期性任务的线程池。核心线程池的大小在创建时设定,当有新任务提交时,如果线程池中有空闲线程,则立即执行。如果没有,则新任务会在一个队列中等待,直到有线程空闲出来。这个线程池特别适合需要多个后台线程执行定时任务或周期性任务的情况。
-
newWorkStealingPool(int parallelism)
从Java 8开始引入,创建一个支持工作窃取算法的线程池。工作窃取算法是一种提高线程利用率的算法,当某个线程处理完自己的任务后,它会随机从其他线程的队列中“窃取”一个任务来执行。这个线程池适合有大量小任务需要执行的情况,可以充分利用多核处理器的优势。
34.线程池常用的几个参数?
- corePoolSize:线程池中的常驻核心线程数。
- maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值大于等于1。
- keepAliveTime:多余的空闲线程的存活时间,当空闲时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下- corePoolSize个线程为止。
- unit:keepAliveTime的单位。
- workQueue:任务队列,被提交但尚未被执行的任务。
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可。
- handler:拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示数(maxnumPoolSize)时如何来拒绝请求执行的runnable的策略。
35.线程池的拒绝策略有哪些?
线程池的拒绝策略是指在线程池已经关闭或达到最大容量时,新提交的任务将被拒绝执行的策略。以下是 Java 线程池中的四种内置拒绝策略:
-
AbortPolicy(默认策略):直接抛出
RejectedExecutionException
异常来阻止系统正常运行。这是线程池默认的拒绝策略。当任务添加到线程池中被拒绝时,它会直接抛出异常。这种策略适用于一些比较重要的业务场景,因为抛出异常可以让开发者及时发现并处理。 -
CallerRunsPolicy:调用执行自己的线程运行任务。当任务添加到线程池中被拒绝时,不是抛出异常,而是将任务回退到调用者,由调用者所在的线程来执行这个任务。这种策略既不会抛弃任务,也不会抛出异常,而是将某些任务退回,从而降低新任务的流量。
-
DiscardPolicy:不处理,直接丢弃掉。当任务添加到线程池中被拒绝时,线程池会丢弃该任务,且不抛出任何异常。这种策略适用于一些不重要的业务场景,例如统计一些无关紧要但又需要的数据。
-
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试提交被拒绝的任务。当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。这种策略可以理解为“杀熟”,即先丢弃最早的任务,以尝试为新任务腾出空间。
36.线程池都有哪些状态?
线程池的状态可以通过ThreadPoolExecutor类的几个方法查询,它主要有以下几种状态:
- RUNNING:这是最正常的状态,表示线程池正在运行,能够接收新的任务并处理等待队列中的任务。当线程池被创建后,它就处于RUNNING状态,此时线程池中的任务数为0。
- SHUTDOWN:表示线程池不再接收新的任务提交,但会继续处理等待队列中的任务。当调用线程池的shutdown()方法时,线程池会由RUNNING状态转变为SHUTDOWN状态。
- STOP:表示线程池不再接收新的任务提交,并且会中断正在执行的任务,同时还会中断正在等待的任务的处理。当调用线程池的shutdownNow()方法时,线程池会由RUNNING或SHUTDOWN状态转变为STOP状态。
- TIDYING:表示线程池中的所有任务都已经销毁,workCount为0,线程池的状态在转换为TIDYING状态时,会执行钩子方法terminated()。当SHUTDOWN状态下,任务数为0,或者STOP状态下线程池中执行中任务为空时,线程池会由这些状态转变为TIDYING状态。
- TERMINATED:表示线程池彻底终止。当线程池在TIDYING状态下执行完terminated()方法后,就会由TIDYING状态转变为TERMINATED状态。
37.线程池中submit()和execute()的区别?
线程池中的 submit()
和 execute()
方法在功能和使用上存在一些关键的区别,主要体现在以下三个方面:
- 接收的参数不同:
execute()
方法接收的参数是Runnable
类型的任务,而submit()
方法则可以接收Runnable
或Callable
类型的任务,这使得submit()
方法在处理任务时更加灵活。 - 返回值不同:
execute()
方法没有返回值,它仅用于执行给定的任务。这使得我们无法判断任务是否成功执行或获取任务的执行结果。相比之下,submit()
方法有返回值,它返回一个Future
对象,这个对象表示异步计算的结果。通过调用Future
对象的get()
方法,我们可以获取任务的执行结果,并可以在任务完成时处理异常。 - 异常处理:
execute()
方法在任务执行过程中抛出异常后,线程会终止,这可能导致线程池中出现无意义的线程,因为线程没有得到重用。而submit()
方法在执行过程中不会抛出异常,而是将异常保存在成员变量中。当调用Future.get()
方法时,如果任务执行过程中有异常发生,get()
方法会抛出异常,这样我们就可以在外部捕获并处理这些异常。
总结来说,
execute()
方法适用于不需要返回值且不需要处理异常的任务,而submit()
方法则适用于需要返回值或需要处理异常的任务。
38.当你提交任务时,线程池队列已满,这时会发生什么?
- 如果使用的是无界队列LinkedBlockedQueue,会继续往里添加等待执行,因为他被认为是一个近乎无穷大的队列,可以无限存放任务;
- 如果使用的是有界队列ArrayBlockedQueue,任务首先会被添加到ArrayBlockedQueue中,如果满了,会根据maximumPoolSize的值增加线程数量,如果数量处理不过来,满了,那么会使用拒绝策略,默认是AbortPolicy。
39.synchronized使用方式?
- 修饰实例方法,锁的是当前实例对象;
- 修饰静态方法,锁的是当前类的class对象;
- 修饰代码块,同步方法块锁的是括号里面的对象;
40.synchronized的锁升级的过程
synchronized的锁升级过程主要涉及三种锁状态:无锁状态、偏向锁状态和自旋锁状态(轻量级锁)。以下是具体的升级过程:
- 无锁状态:这是对象的初始状态,此时没有线程获取到锁。
- 偏向锁状态:当线程首次访问同步代码块并获取到锁时,锁会进入偏向锁状态。此时,锁会记录下获取到该锁的线程ID,以便该线程下次直接获取锁,而无需进行CAS操作。这种机制有助于提高程序的性能,因为它避免了不必要的CAS操作。如果线程A已经持有偏向锁,但尚未执行完同步代码,此时线程B来请求锁,会导致CAS失败,偏向锁会升级为轻量级锁。
- 自旋锁状态(轻量级锁):当偏向锁升级为轻量级锁时,如果有其他线程来竞争锁,那么当前线程会尝试使用CAS算法获取锁。如果获取成功,则当前线程获得锁并执行同步代码块。如果获取失败,那么当前线程会进行自旋等待,直到获取到锁为止。自旋等待的次数是有限制的,当自旋次数达到一定值(默认是10次)时,如果仍未获取到锁,那么轻量级锁会升级为重量级锁。
- 重量级锁:当轻量级锁升级为重量级锁时,未获取到锁的线程会被阻塞并进入等待状态,等待操作系统唤醒并重新尝试获取锁。
41.synchronized底层原理?
- 1.5之前是一个重量级锁,1.6之后进行了优化;
- 原子性、可见性、有序性(程序按照代码先后执行);
- 通过对象内部一个叫监视器锁monitor实现的,且每个对象都会有一个与之对应的monitor对象,该对象存储着当前持有锁的线程和等待锁队列,获取锁的时候是monitorenter,释放锁的时候是monitorexit,mark word会记录关于锁的信息,其加锁依赖的是操作系统中的互斥指令,有用户态和内核态的切换性能消耗极为明显。
42.什么是自旋?
synchronized里面的代码比较简单时,执行都比较快,没必要上锁,就会在边界做忙循环,如果多次循环之后还没获得锁,再去阻塞是一种更好的策略。
43.synchronized可重入的原理?
重入锁是值一个线程获取到该锁之后,该线程可以继续获得该锁,底层维护了一个计数器,当线程获取到该锁时,计数器+1,再次获取到该锁时再+1,释放锁时-1,知道为0的时候,释放锁。
44.synchronized和volicate区别?
- synchronized悲观锁,属于抢占式,会引起线程阻塞;
- volicate提供多线程共享变量可见性和禁止指令重排,当一个共享变量被volicate修饰,会保证修改的值会立即被更新到主存中,当有其他线程需要读取时,回去内存中读取新值。
45.Lock和synchronized区别?
- Lock是个Java类;synchronized是关键字;
- Lock只能给代码块加锁;synchronized可以修饰类、方法、变量;
- Lock必须手动获取释放锁,synchronized不需要手动来操作;
- Lock可以知道有没有获取锁成功,synchronized不能做到;
- Lock可重入、可判断、可公平;synchronized可重入、不可中断、非公平;
- Lock适合有大量同步代码的同步问题,synchronized适合代码少量的同步问题;
46.synchronized和ReentrantLock可重入锁的区别?
- synchronized是关键字;
- ReentrantLock是一个类,比synchronized更灵活,可以被继承,可以有方法,但是必须有释放锁的动作;
- ReentrantLock必须手动获取释放锁,synchronized不需要手动来操作;
- ReentrantLock只适用于代码块锁,synchronized可以修饰类、方法、变量;
- 二者都是可重入锁;
47. ReentrantLock是什么?
ReentrantLock的原理基于AQS(AbstractQueuedSynchronizer)框架,它实现了独占锁的功能。ReentrantLock通过维护一个内部状态来表示锁是否被占用,以及等待获取锁的线程队列。
当一个线程尝试获取ReentrantLock时,会调用lock()方法。如果锁当前没有被占用(即内部状态为0),那么该线程会成功获取锁,并将内部状态设置为1。如果锁已经被其他线程占用,那么该线程会被添加到等待队列中,进入自旋等待状态,不断检查锁是否可用。
当一个线程释放ReentrantLock时,会调用unlock()方法。该方法会将内部状态减1,表示锁被释放。如果此时有等待队列中的线程在等待获取锁,那么会从队列中取出一个线程来获取锁。如果没有等待的线程,那么锁就处于可用状态,等待下一个线程来获取。
ReentrantLock是一种可重入的互斥锁,也被称为“独占锁”。它是JDK中的一种线程并发访问的同步手段,功能类似于synchronized,但提供了比synchronized更强大、灵活的锁机制,可以减少死锁发生的概率。ReentrantLock的实现基于AQS(AbstractQueuedSynchronizer)框架,它支持手动加锁与解锁,以及加锁的公平性设置。
ReentrantLock的主要特性包括:
- 可重入:ReentrantLock锁可以被同一个线程多次获取,只要该线程持有锁,就可以再次获取该锁而不会被阻塞。
- 公平性:ReentrantLock支持公平锁和非公平锁。在公平锁机制下,线程会依次排队获取锁,确保先请求的线程先获取锁。而在非公平锁机制下,即使一个线程在队列的末尾,也有可能在它前面有线程正在持有锁时获取到锁。
- 中断:ReentrantLock支持中断功能,即当线程在等待获取锁的过程中可以被中断。
- 超时:ReentrantLock支持设置超时时间,如果线程在等待获取锁的过程中超过了设定的超时时间,那么线程会放弃获取锁并继续执行后续操作。
48.synchronized为什么不能集群操作?如果想集群操作用什么?
synchronized 是 Java 中用于实现线程同步的关键字,它保证了同一时刻只有一个线程可以执行被 synchronized 修饰的代码块或方法。然而,synchronized 的同步机制是基于 JVM 内部的锁机制实现的,这意味着它只能保证单个 JVM 进程内的线程同步,而无法跨多个 JVM 进程实现线程同步
在集群环境下实现线程同步,通常需要使用分布式锁机制。分布式锁是一种跨多个进程或机器的锁机制,它可以保证在分布式系统中,同一时刻只有一个节点可以执行某个任务或访问某个资源。常见的分布式锁实现方式包括基于数据库、Redis、ZooKeeper 等的锁机制。
49.线程池用完扔回线程池是什么状态?
线程池中的线程在完成任务后,会被放回线程池中,并标记为空闲状态,等待下一次任务的到来。这种机制使得线程池可以避免频繁地创建和销毁线程,减少了系统开销和内存消耗。
然而,线程池中的线程如果长时间处于空闲状态,可能会占用系统资源,导致性能下降。因此,线程池通常会设定一个线程的空闲时间阈值,当一个线程在空闲状态下超过这个阈值后,线程池会判断该线程不再被需要,从而销毁该线程,释放系统资源
50.CAS和ABA的问题?
CAS(Compare-and-Swap)是一种无锁机制,用于实现多线程之间的同步。CAS操作包含三个操作数——内存位置(V)、期望的原值(A)和新值(B)。执行CAS操作时,会将内存位置V的值与期望的原值A进行比较。如果相匹配,那么处理器会自动将该内存位置V的值更新为新值B。否则,处理器不做任何操作。
ABA问题是指在CAS操作过程中,由于时间差导致数据的变化。具体来说,当多个线程对同一个原子类进行操作时,某个线程将原值A改成了B,然后又改回了A。此时,另一个线程也对该值进行操作,发现它的值仍然是A,就会认为它没有被修改过,从而执行CAS操作。但实际上,这个值已经被其他线程修改过了,只是最后又被改回了A,这就是ABA问题。
为了解决ABA问题,可以采用版本号或时间戳等机制来标识数据的变化。例如,在Java中,JUC包提供的AtomicStampedReference类和AtomicMarkableReference类就可以解决CAS的ABA问题。AtomicStampedReference类使用一个标记(stamp)来记录对象的版本号,当对象发生变化时,版本号会自动增加。通过比较对象引用和版本号来判断对象是否发生过变化,从而避免了ABA问题。AtomicMarkableReference类类似于AtomicStampedReference,但它使用一个布尔标记(mark)来表示对象的状态是否发生过改变。同样地,通过比较对象引用和标记来判断对象是否发生过变化,以避免ABA问题。
51.线程池的线程数是怎么确定的?
CPU密集型任务:这类任务执行大量的计算,但很少进行I/O操作。对于这类任务,线程数量通常设置为CPU核心数加一(Ncpu+1),以减少线程上下文切换的开销。
I/O密集型任务:这类任务大部分时间都在等待I/O操作完成,如数据库查询或网络请求。对于这类任务,线程数量通常设置为CPU核心数的两倍(2*Ncpu),甚至更多,以充分利用等待I/O的时间。
混合型任务:在I/O优化中,线程等待时间所占比例越高,需要线程越多,线程CPU时间所占比例越高,需要线程越少。估算公式:最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU数目;
52.ThredLocal是什么?以及使用场景?
ThreadLocal是一个线程变量,它为每个线程创建了一个变量副本,这样每个线程可以访问自己内部的副本变量。这意味着ThreadLocal中填充的变量属于当前线程,并且该变量对其他线程是隔离的,因此可以看作是线程独有的变量。由于每个线程都有自己的实例副本,并且该副本只能由当前线程使用,因此不存在多线程间共享的问题。
ThreadLocal变量通常被声明为private static
,以便在类的方法中被访问。总的来说,ThreadLocal提供了一种在多线程环境中管理线程本地数据的方式。
一个线程内可以存在多个ThreadLocal对象,所以其实是ThreadLocal内部维护了一个Map,这个Map不是直接使用的HashMap,而是ThreadLocal实现的一个叫做ThreadLocalMap的静态内部类。而我们使用的get()、set()方法其实都是调用了这个ThreadLocalMap类对应的get()、set()方法。
ThreadLocal在多种场景中都有应用,主要包括以下几个方面:
- 保存线程独享的对象:每个线程都可以修改自己所拥有的副本,而不会影响其他线程的副本,确保了线程安全。这特别适用于保存线程不安全的工具类,例如SimpleDateFormat。
- 线程间数据隔离:在Web开发中,可以使用ThreadLocal存储当前请求的上下文信息,避免参数传递的复杂性。每个线程在其自己的线程中使用自己的局部变量,各线程间的ThreadLocal对象互不影响。
- 数据库连接管理:ThreadLocal可以为每个线程保持独立的数据库连接,提高并发性能。例如,Spring的事务管理器就使用了ThreadLocal来管理数据库连接。
- 日志记录:ThreadLocal可以将日志记录与当前线程关联起来,方便追踪和排查问题。
- 线程池:在线程池中,可以使用ThreadLocal为每个线程维护独立的上下文信息,避免线程间互相干扰。
- AOP缓存:在AOP中,可以将数据缓存到ThreadLocal中,以便在后续的控制器层中获取到当前变量。
总的来说,ThreadLocal适用于多线程的情况下,可以实现数据传递和线程隔离。通过正确使用ThreadLocal,可以提高程序的线程安全性和性能。然而,也需要避免内存泄漏等问题。
53.什么是临界区?
用来表示一种公共资源或者说是共享资源,可以被多个线程使用,但每个线程使用时,一旦临界资源被一个线程占用,其他线程必须等待。
53.ab同时提交线程完成任何一个就去执行C,用什么来完成?
这个场景是在考察对Java并发编程中的线程管理和线程同步的理解。具体来说,它涉及到以下几个关键点:
-
Callable和Future的使用:
Callable
是Java中的一个接口,它允许你定义一个返回结果的任务。Callable
的实例可以被提交给ExecutorService
去执行,并返回一个Future
对象。Future
对象代表了异步计算的结果。你可以通过Future
对象来获取异步计算的结果,或者检查计算是否已经完成。
-
线程同步:
- 在这个场景中,你需要确保当A或B中的任何一个线程完成时,C线程可以开始执行。这通常涉及到线程同步机制,比如使用
CountDownLatch
、CyclicBarrier
、Semaphore
等。 CountDownLatch
是一个计数器,允许一个或多个线程等待其他线程完成操作。在这个案例中,你可以设置一个CountDownLatch
的初始计数为2,然后每个线程A和B在完成任务后调用countDown()
方法,C线程在await()
方法上等待,直到计数减到0。
- 在这个场景中,你需要确保当A或B中的任何一个线程完成时,C线程可以开始执行。这通常涉及到线程同步机制,比如使用
-
线程池的使用:
- 为了有效地管理线程,通常会使用
ExecutorService
来创建一个线程池。线程池可以复用线程,减少线程创建和销毁的开销。
- 为了有效地管理线程,通常会使用
-
异常处理:
- 在使用
Callable
和Future
时,需要注意异常处理。如果Callable
任务抛出异常,它将被封装在一个ExecutionException
中,可以通过Future.get()
方法抛出。
- 在使用
-
资源释放:
- 在使用完
ExecutorService
后,需要调用shutdown()
或shutdownNow()
方法来关闭线程池,释放资源。
- 在使用完
54.ab同时提交线程,需要判断结果,就去执行C,用什么来完成?
可以使用Future和ExecutorService。同时,为了实现轮询(polling)以检查任务是否完成,你可以使用一个循环来定期检查Future的完成状态。
55.CountDownLatch是什么?
countDown() 是 CountDownLatch 类中的一个方法。CountDownLatch 是一个在 Java 中常用的同步工具类,它允许一个或多个线程等待其他线程完成操作。CountDownLatch 维护了一个内部计数器,该计数器的初始值在创建 CountDownLatch 对象时设定。
countDown() 方法会将这个计数器的值减一。如果计数器的当前值为正数,那么调用 countDown() 后,计数器的值会减一;如果计数器的当前值为零,那么 countDown() 调用将没有任何效果。
CountDownLatch 的另一个关键方法是 await(),它会让当前线程等待,直到计数器的值减为零。当计数器的值减为零时,所有在 await() 方法上等待的线程将被唤醒并继续执行。
CountDownLatch 通常用于控制并发线程的执行顺序。例如,如果你有一个任务需要多个线程共同完成,并且只有当所有线程都完成它们的工作后,主线程才能继续执行,那么你可以使用 CountDownLatch 来实现这个需求。每个工作线程在完成自己的任务后,调用 countDown() 方法,而主线程在调用 await() 方法后等待所有工作线程完成。
56.ExecutorService是什么?
ExecutorService是Java中的一个线程池服务,它是Executor的直接扩展接口,也是最常用的线程池接口。线程池是一种用于处理大量短小任务的机制,它避免了频繁创建和销毁线程所带来的开销,提高了系统的响应速度和资源利用率。
ExecutorService提供了灵活的线程池管理功能,包括控制最大并发线程数、定时执行、定期执行、单线程执行、并发数控制等。当我们有任务需要多线程来完成时,可以将任务(实现Runnable接口、Callable接口或继承Thread类的对象)提交给ExecutorService来执行。ExecutorService会负责线程的创建、管理和调度,从而简化了多线程编程的复杂性。
在服务器应用程序中,ExecutorService常用于处理来自远程来源的大量短小任务。通过将任务提交给线程池来执行,可以避免频繁创建和销毁线程所带来的开销,提高了服务器的性能。同时,ExecutorService还提供了线程池的关闭和资源释放功能,以确保资源的正确管理。
55.AQS简单介绍一下?
AQS,全称为AbstractQueuedSynchronizer,是一个抽象的队列式同步器,是Java并发编程中的核心组件。它定义了实现线程同步器的基础框架,主要用于协调多个线程对共享资源的访问。
AQS通过维护一个内部状态(state)来表示同步状态,这个状态是一个整数。当state大于0时,表示已经获取了锁;当state等于0时,表示锁已经被释放。AQS通过原子操作来更新这个状态,以确保线程安全。
AQS的核心思想是将请求共享资源的线程封装成一个节点(Node),并将这些节点加入到一个FIFO(先进先出)的队列中。当共享资源空闲时,将队列中的第一个节点设置为有效的工作线程,并将共享资源设置为锁定状态。如果共享资源被占用,那么线程会阻塞等待,直到获取到锁为止。这种机制是通过CLH队列锁实现的。
AQS提供了两种锁机制:独占锁和共享锁。独占锁用于多个线程竞争同一个共享资源的情况,同一时刻只允许一个线程访问该资源。例如,ReentrantLock就是一个基于AQS实现的独占锁。共享锁则允许多个线程同时访问共享资源,如CountDownLatch和Semaphore就是基于AQS实现的共享锁。
此外,AQS还支持公平锁和非公平锁。公平锁按照线程请求锁的顺序来分配锁,而非公平锁则允许线程抢占已经持有的锁。这种机制是通过在AQS中维护一个FIFO队列来实现的,队列中的节点按照线程请求锁的顺序排列。
55.CLH是什么?
CLH队列,全称是Craig,Landin,和Haqersten提出的锁队列,是一个FIFO(先进先出)的双向链表队列,用于存储被阻塞的线程信息。它是AQS(AbstractQueuedSynchronizer)内部维护的一个关键组件,用来实现线程之间的公平锁。
当一个线程尝试获取同步状态失败时,它会被封装成一个Node节点,并通过CAS原子操作插入到CLH队列的尾部。此时,该线程会被阻塞。当线程释放同步状态后,会唤醒当前节点的next节点,这个next节点会尝试抢占同步资源。如果抢占失败,它会重新阻塞;如果成功,它会将自己设置为当前线程的节点,并将之前的head节点废弃。
CLH队列具有以下优点:
- 先进先出的特性保证了公平性。
- 它是一个非阻塞的队列,通过自旋锁和CAS保证了节点插入和移除的原子性,实现了无锁快速插入。因此,CLH队列也是一种基于链表的可扩展、高性能、公平的自旋锁。