1、并发编程的优缺点为什么要使用并发编程(并发编程的优点)
2、并发编程有什么缺点
并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,
比如:内存泄漏、上下文切换、线程安全、死锁等问题。
3、并发编程三要素是什么?在 Java 程序中怎么保证多线程的运行安全?
并发编程三要素(线程的安全性问题体现在):
出现线程安全问题的原因:
解决办法:
4、并行和并发有什么区别?
做一个形象的比喻:
并发 = 两个队列和一台咖啡机。
并行 = 两个队列和两台咖啡机。
串行 = 一个队列和一台咖啡机。
5、什么是多线程,多线程的优劣?
多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个 不同的线程来执行不同的任务。
多线程的好处: 可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可 以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单 个程序创建多个并行执行的线程来完成各自的任务。
多线程的劣势:
6、线程和进程区别 什么是线程和进程?
进程 :
一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进 程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进 程。
线程:
进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至 少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
进程与线程的区别 :
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process), 它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有 若干个线程,至少包含一个线程。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执 行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切 换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空 间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开 销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线 (线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻 量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空 间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个 线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控 制,两者均可并发执行
7、什么是上下文切换?
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任 意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的 策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就 会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存 自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在 每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换 对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一 项就是,其上下文切换和模式切换的时间消耗非常少。
8、守护线程和用户线程有什么区别呢?
守护线程和用户线程
main 函数所在的线程就是一个用户线程,main 函数启动的同时在 JVM 内部 同时还启动了好多守护线程,比如垃圾回收线程。 比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线 程运行。而守护线程不会影响 JVM 的退出。
注意事项:
9、如何在 Windows 和 Linux 上查找哪个线程cpu利用率最高?
windows上面用任务管理器看,linux下可以用 top 这个工具看。
10、 什么是线程死锁 ?
百度百科:死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资 源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推 进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进 程(线程)称为死锁进程(线程)。
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线 程被无限期地阻塞,因此程序不可能正常终止。
线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方 的资源,所以这两个线程就会互相等待而进入死锁状态。
形成死锁的四个必要条件是什么
如何避免线程死锁
我们只要破坏产生死锁的四个条件中的其中一个就可以了。
破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源 需要互斥访问)。
破坏请求与保持条件
一次性申请所有的资源。
破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占 有的资源。
破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环 等待条件。
11、 创建线程有哪几种方式?
创建线程有四种方式:
12、 线程的 run()和 start()有什么区别?
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的, run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。 start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。 start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待 run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
13、 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
**14、 什么是 Callable 和 Future?
**Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说Callable用于产生结果,Future 用于获取结果。
15、什么是 FutureTask?
FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。
16、说说线程的生命周期及五种基本状态?
网上对线程状态的描述很多,有5种,6种,7种,都可以接受。
5中状态一般是针对传统的线程状态来说(操作系统层面):
Java中给线程准备的6种状态:
在Java代码中验证一下效果
NEW
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
});
System.out.println(t1.getState());
}
RUNNABLE
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true){
}
});
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
BLOCKED
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread t1 = new Thread(() -> {
// t1线程拿不到锁资源,导致变为BLOCKED状态
synchronized (obj){
}
});
// main线程拿到obj的锁资源
synchronized (obj) {
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
}
WAITING
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread t1 = new Thread(() -> {
synchronized (obj){
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
TIMED_WAITING
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
TERMINATED
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(1000);
System.out.println(t1.getState());
}
17、 sleep() 和 wait() 有什么区别?
两者都可以暂停线程的执行
18、 如何停止一个正在运行的线程?
在java中有以下3种方法可以终止正在运行的线程:
19、 Java 中 interrupted 和 isInterrupted 方法的区别?
interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。
注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
isInterrupted:查看当前中断信号是true还是false
20、 notify() 和 notifyAll() 有什么区别?
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。
notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。如何在两个线程间共享数据?在两个线程间共享变量即可实现共享。
21、 Java 线程数过多会造成什么异常?
22、synchronized 的作用?
synchronized 可以修饰类、方法、变量。
在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。
庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
23、 synchronized、volatile、CAS 比较?
(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
(3)CAS 是基于冲突检测的乐观锁(非阻塞)
24、synchronized 和 Lock 有什么区别?
25、 synchronized 和 ReentrantLock 区别是什么?
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
相同点:
两者都是可重入锁两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0 时才能释放锁。主要区别如下:
26、 volatile 关键字的作用
在 Java 中,volatile 是一个类型修饰符,用于修饰变量,主要用于保证变量的可见性和禁止指令重排序。以下是其核心作用的详细解释:
1)保证可见性(Visibility)
当一个变量被声明为 volatile 时,它会保证以下两点:
示例场景:
public class VolatileExample {
private volatile boolean flag = false; // 使用volatile保证可见性
public void writer() {
flag = true; // 写操作立即刷新到主内存
}
public void reader() {
while (!flag) { // 读操作强制从主内存获取最新值
// 循环等待
}
System.out.println("Flag is now true");
}
}
解释:若 flag 未被声明为 volatile,reader 线程可能永远看不到 writer 线程对 flag 的修改,导致无限循环。
2)禁止指令重排序(Happens-Before Ordering)
Java 编译器和处理器为了优化性能,可能会对指令进行重排序,但 volatile 变量会禁止特定类型的重排序。具体规则是:
示例场景:
public class Singleton {
private static volatile Singleton instance; // 使用volatile禁止重排序
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 禁止指令重排序
}
}
}
return instance;
}
}
解释:若 instance 未被声明为 volatile,可能会出现指令重排序导致的问题:instance = new Singleton() 可能被分解为:
若另一个线程在此时读取 instance,会得到一个未完全初始化的对象,导致程序崩溃。
3)不保证原子性(Atomicity)
volatile 不保证变量操作的原子性,即无法防止多个线程同时修改变量时的竞态条件。例如:
private volatile int count = 0;
public void increment() {
count++; // 非原子操作,即使count被声明为volatile
}
解释:count++ 实际上是三个操作的组合(读取、加 1、写入),多个线程同时执行时仍可能导致数据不一致。若需要原子性,应使用 AtomicInteger 等原子类。
4)适用场景
总结:
volatile 的核心作用是:
但它不能替代 synchronized 或原子类,仅适用于一个线程写、多个线程读的场景,或作为轻量级的同步机制。
27、什么是AQS?
在 Java 中,AQS(AbstractQueuedSynchronizer)是一个位于java.util.concurrent.locks包下的抽象类,它是构建锁和其他同步组件(如ReentrantLock、Semaphore、CountDownLatch)的基础框架。AQS 通过一个 FIFO 队列来管理线程的阻塞和唤醒,并使用一个int类型的状态变量(state)来表示同步状态,极大地简化了并发编程的实现。
核心原理
AQS 的核心思想是通过一个状态变量(state)和一个CLH 队列(FIFO 双向队列)来管理线程的同步状态:
1)状态变量(state):
由子类通过getState()、setState()和compareAndSetState()方法操作。
例如,ReentrantLock用state表示锁的重入次数,Semaphore用state表示剩余许可数。
2)CLH 队列:
当多个线程竞争同一个锁时,未获取到锁的线程会被封装成Node节点加入队列。
队列中的线程会按照 FIFO 顺序等待获取锁。
关键方法
AQS 提供了两类方法:
1)模板方法(由使用者调用):
2)钩子方法(由子类实现):
独占模式 vs 共享模式
AQS 的典型应用
1)ReentrantLock:
2)Semaphore:
3)CountDownLatch:
4)ReentrantReadWriteLock:
示例:自定义同步器(基于 AQS)
下面是一个简单的自定义同步器示例,实现一个不可重入的独占锁:
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class SimpleLock {
private static class Sync extends AbstractQueuedSynchronizer {
// 锁被占用时状态为1,空闲时为0
@Override
protected boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int releases) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
}
AQS 的优势
总结
AQS 是 Java 并发包的核心基石,通过状态变量和 FIFO 队列提供了一套通用的同步机制。理解 AQS 有助于深入掌握 Java 中的锁、信号量等并发工具的工作原理,并能灵活扩展自定义同步组件。
AQS内部结构和属性:
28、ThreadLocal 是什么?有哪些使用场景?
hreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。
核心作用:
ThreadLocal的主要作用是:
基本用法:
以下是ThreadLocal的基本使用示例:
public class ThreadLocalExample {
// 创建ThreadLocal变量,初始值为0
private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
// 创建两个线程
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
int value = threadLocal.get(); // 获取当前线程的副本值
threadLocal.set(value + 1); // 修改当前线程的副本值
System.out.println("Thread 1: " + threadLocal.get());
}
threadLocal.remove(); // 建议在finally块中调用,防止内存泄漏
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 2: " + threadLocal.get());
threadLocal.set(threadLocal.get() - 1);
}
threadLocal.remove();
});
t1.start();
t2.start();
}
}
输出示例(线程间数据隔离):
plaintext
Thread 1: 1
Thread 2: 0
Thread 1: 2
Thread 2: -1
Thread 1: 3
Thread 2: -2
实现原理:
ThreadLocal的核心机制是:
1)每个线程维护一个ThreadLocalMap:
2)ThreadLocal作为键:
内存泄漏问题:
ThreadLocal可能导致内存泄漏的原因:
解决方案:
使用后及时调用remove():在try-finally块中确保调用,避免残留值。
典型应用场景:
数据库连接管理、Session 管理、避免参数传递。
经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。
注意事项:
不可继承性:ThreadLocal中的值不能被子线程继承。若需要子线程继承父线程的值,可使用InheritableThreadLocal。
线程安全:ThreadLocal本身是线程安全的,但如果多个线程操作同一个ThreadLocal中的可变对象(如ArrayList),仍需同步。
总结:
ThreadLocal通过为每个线程创建独立的变量副本,实现了线程间的数据隔离,适用于需要避免共享变量的场景。使用时需注意内存泄漏问题,建议在每次使用后调用remove()。它是简化并发编程的重要工具,常见于数据库连接、事务管理、用户会话等场景。
29、 ThreadLocal造成内存泄漏的原因?
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key 为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完ThreadLocal方法后手动调用remove()方法。
ThreadLocal内存泄漏解决方案?
30、 线程池有什么优点?
31、 线程池都有哪些状态?
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
32、 Executors和ThreaPoolExecutor创建线程池的区别
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 各个方法的弊端:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
主要问题是线程数 大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定。
33、ThreadPoolExecutor参数说明?
1、corePoolSize:核心线程数
* 核心线程会一直存活,及时没有任务需要执行
* 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
* 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
2、queueCapacity:任务队列容量(阻塞队列)
* 当核心线程数达到最大时,新任务会放在队列中排队等待执行
3、maxPoolSize:最大线程数
* 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
* 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
4、 keepAliveTime:线程空闲时间
* 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
* 如果allowCoreThreadTimeout=true,则会直到线程数量=0
5、allowCoreThreadTimeout:允许核心线程超时
6、rejectedExecutionHandler:任务拒绝处理器
* 两种情况会拒绝处理任务:
- 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
- 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
* 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
* ThreadPoolExecutor类有几个内部实现类来处理这类情况:
- AbortPolicy 丢弃任务,抛运行时异常
- CallerRunsPolicy 执行任务
- DiscardPolicy 忽视,什么都不会发生
- DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
* 实现RejectedExecutionHandler接口,可自定义处理器
34、 ThreadPoolExecutor执行顺序
线程池按以下行为执行任务
1. 当线程数小于核心线程数时,创建线程。
2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
3. 当线程数大于等于核心线程数,且任务队列已满
-1 若线程数小于最大线程数,创建线程
-2 若线程数等于最大线程数,抛出异常,拒绝任务
35、 并发工具
1)CycliBarriar 和 CountdownLatch 有什么区别?
CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的:
CountDownLatch是不能复用的,而CyclicLatch是可以复用的。
2)Semaphore
Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。
Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码 多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。
Semaphore(信号量)-允许多个线程同时访问: synchronized 和
ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量) 可以指定多个线程同时访问某个资源。
3)Exchanger
Exchanger是一个用于线程间协作的工具类,用于两个线程间交换数据。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。交换数据是通过 exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据。
36、常用的并发工具类有哪些?