文章目录
- 1.什么是进程和线程?进程与线程的关系,区别与优缺点?
- 2.为什么要使用多线程呢?
- 3.什么是上下文切换?
- 4.什么是线程死锁?如果避免死锁?
- 5.说说 sleep() 方法和 wait() 方法区别和共同点?
- 6.乐观锁和悲观锁是什么?怎么实现乐观锁?
- 7.JMM(Java内存模式)详细说一下?volatile 关键字解决了什么问题?
- 8.Java内存区域和JMM有什么区别?
- 9.happens-before原则?
- 10.synchronized 关键字的作用?
- 11.synchronized 和 ReentrantLock 的区别?
- 12.synchronized 和 volatile 的区别?
- 13.synchronized 关键字的底层原理?
- 14.ThreadLocal 关键字的作用?
- 15.线程是什么?有什么用?为什么不推荐使用内置线程池?
- 16.Java线程池有哪些?阻塞队列有几种?拒绝策略有几种?
- 17.手写一个线程池?
- 18.实现 Runnable 接口和 Callable 接口的区别?
- 19.如何给线程池命名?为什么建议给线程池命名?
- 20.如何动态修改线程池参数?
- 21.AQS原理了解吗?组件有哪些?
- 22.Semaphore 有什么用?原理是什么?
- 23.CountDownLatch 有什么用?原理是什么?
- 24.CyclicBarrier有什么用?原理是什么?
- 25.多个任务的编排可以怎么做?项目用到了 CompletableFuture 吗?
1.什么是进程和线程?进程与线程的关系,区别与优缺点?
进程(Process):
- 进程是程序的一次执行过程,是程序运行时的一个实例。
- 每个进程拥有独立的内存空间,包括代码、数据、堆栈等,相互之间不能直接访问对方的内部数据。
- 进程间通信的成本较高,通常需要通过进程间通信(IPC)机制来实现,如管道、信号量、消息队列等。
- 每个进程都有自己的资源和状态,相互之间不会影响。
线程(Thread):
- 线程是进程的一个执行流程,是操作系统能够进行运算调度的最小单位。
- 多个线程可以共享同一个进程的资源,包括代码段、数据段等。
- 线程间通信相对简单,可以通过共享内存等方式进行通信。
- 线程的创建、销毁和切换开销较小,可以更高效地利用系统资源。
进程与线程的关系:
- 一个进程可以包含多个线程,这些线程共享进程的资源。
- 每个线程拥有自己的栈空间,但共享进程的堆空间和静态存储区域。
区别与优缺点:
区别:
- 进程是程序的一次执行过程,拥有独立的内存空间,而线程是进程的一个执行流程,共享进程的资源。
- 进程间通信成本较高,线程间通信相对简单。
- 进程之间相互独立,线程共享进程的资源,因此线程之间的切换开销较小。
优点:
- 进程:独立性强,一个进程崩溃不会影响其他进程;安全性高;利于资源的管理和保护。
- 线程:创建、销毁和切换开销小;可以更高效地利用系统资源;适合并发执行的场景。
缺点:
- 进程:资源开销大,创建、销毁和切换开销较大。
- 线程:线程间共享资源需要考虑同步与互斥问题,容易引发死锁、竞态条件等并发问题;一个线程崩溃可能影响整个进程的稳定性。
在Java中,可以通过Java提供的多线程机制来实现多线程编程,例如使用Thread类或者实现Runnable接口。下面是一个简单的Java多线程示例:
public class MyThread extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread: " + i);
try {
Thread.sleep(1000); // 线程休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
for (int i = 0; i < 5; i++) {
System.out.println("Main: " + i);
try {
Thread.sleep(1000); // 主线程休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这个示例中,我们定义了一个继承自Thread类的线程类MyThread,重写了run方法,在run方法中实现了线程的具体逻辑。在main方法中,我们创建了一个MyThread实例,并调用start方法启动线程,同时主线程也在执行一个简单的循环。
2.为什么要使用多线程呢?
使用多线程主要是为了提高程序的并发性和效率,以及改善用户体验。以下是使用多线程的一些主要原因:
- **提高程序的响应速度:**在图形界面、网络通信等需要及时响应用户操作的场景中,使用多线程可以使程序能够同时执行多个任务,提高了程序的响应速度。
- **提高系统资源利用率:**多线程可以更充分地利用多核处理器和多任务操作系统的特性,使得系统资源得到更有效的利用,提高系统整体的效率。
- **改善用户体验:**通过多线程实现后台任务的异步执行,可以避免因为长时间的计算或者IO等待导致的界面卡顿,提升用户体验。
- **实现并发编程:**多线程可以使得程序能够同时执行多个任务,从而更好地处理并发请求,提高系统的并发处理能力。
- **提高程序的可伸缩性:**多线程可以使得程序能够更好地适应不同的负载情况,提高了系统的可伸缩性和稳定性。
- **实现复杂任务的分解与协作:**通过多线程可以将复杂的任务分解成多个子任务并行执行,然后再将结果合并,从而提高了程序的可维护性和扩展性。
总的来说,使用多线程可以更好地利用系统资源,提高程序的效率和并发处理能力,同时改善用户体验,是现代软件开发中非常重要的一种技术手段。
3.什么是上下文切换?
上下文切换是指操作系统在进行多任务调度时,从一个任务(线程或进程)切换到另一个任务时所执行的一系列操作。在进行上下文切换时,操作系统需要保存当前任务的执行状态(称为上下文),包括寄存器的值、程序计数器的值、内存页表等信息,并加载下一个任务的执行状态,使其能够继续执行。
上下文切换的过程包括以下几个步骤:
- **保存当前任务的上下文:**操作系统会保存当前任务的执行状态,包括CPU寄存器的值、程序计数器的值、堆栈指针等。
- **切换到另一个任务的上下文:**操作系统会根据调度算法选择下一个要执行的任务,并加载其执行状态,将CPU寄存器的值、程序计数器的值等恢复到合适的状态。
- **切换内存映射(可选):**如果任务切换涉及到不同的内存空间(例如不同的进程),操作系统可能还需要调整内存映射,将当前任务的内存空间切换到下一个任务的内存空间。
- **恢复执行:**完成上述步骤后,CPU开始执行下一个任务,继续其执行流程。
上下文切换是操作系统进行多任务调度的基本操作之一。尽管上下文切换是必要的,但它也会带来一定的开销,包括保存和恢复任务上下文的时间开销以及可能导致CPU缓存失效等额外开销。因此,在设计和优化多任务系统时,需要尽量减少上下文切换的次数,以提高系统的性能和效率。
4.什么是线程死锁?如果避免死锁?
线程死锁是指两个或多个线程在互相等待对方释放资源的情况下,都无法继续执行的一种状态。在死锁状态下,每个线程都在等待某个资源被释放,但同时又不释放自己持有的资源,导致所有线程都无法继续执行,形成了相互等待的僵局。
通常发生死锁的条件有四个,也被称为死锁的必要条件:
- **互斥条件(Mutual Exclusion):**一个资源每次只能被一个线程使用。
- **占有且等待(Hold and Wait):**一个线程持有至少一个资源,并且正在等待获取其他线程持有的资源。
- **不可抢占(No Preemption):**线程无法强行抢占其他线程持有的资源,只能在持有资源的线程主动释放后才能获取。
- **循环等待(Circular Wait):**一组线程相互之间形成一个循环等待其他线程持有的资源。
要避免死锁,可以采取以下一些措施:
- **避免使用多个锁:**尽量减少使用多个锁,或者在使用多个锁的情况下确保线程按照相同的顺序获取锁,从而避免循环等待的情况发生。
- **使用定时锁(TryLock):**在获取锁时,使用带有超时参数的尝试锁机制(TryLock),避免线程长时间等待,超时后可以释放资源或进行其他处理。
- **破坏循环等待条件:**对资源进行编号,要求线程按照编号顺序获取资源,从而破坏循环等待条件。
- **加锁顺序:**在获取多个锁的情况下,保持一致的锁获取顺序,避免不同线程之间出现交叉加锁导致死锁的可能。
- **资源分配策略:**尽量保证线程在获取资源时,不会因为资源不可用而长时间等待,可以采用资源预分配或者动态资源分配策略。
- **死锁检测与恢复:**实现死锁检测机制,当检测到死锁发生时,进行相应的恢复措施,如强制终止一些线程、释放资源等。
通过合理的设计和编码,以及对系统的监控和调优,可以有效地避免死锁的发生,提高系统的稳定性和可靠性。
5.说说 sleep() 方法和 wait() 方法区别和共同点?
sleep() 方法和 wait() 方法都是Java中用于线程控制的方法,它们有一些区别和共同点。
共同点:
- **都是线程控制方法:**sleep() 方法和 wait() 方法都可以用于线程的控制和调度。
- **都可以让线程进入等待状态:**无论是调用 sleep() 还是 wait() 方法,都可以使得线程进入等待状态,暂时停止执行。
区别:
调用方式不同:
- sleep() 方法是 Thread 类的静态方法,直接通过线程对象调用,例如 Thread.sleep()。
- wait() 方法是 Object 类的实例方法,需要在同步代码块或同步方法中通过锁对象调用,例如 synchronized(obj) { obj.wait(); }。
使用范围不同:
- sleep() 方法可以在任何地方调用,不需要在同步代码块或同步方法中使用。
- wait() 方法通常用于线程间的通信,需要在同步代码块或同步方法中使用,等待其他线程对同一个对象发出的通知。
释放锁的不同:
- 在调用 sleep() 方法后,线程不会释放持有的锁,其他线程无法获取该锁。
- 在调用 wait() 方法后,线程会释放持有的锁,允许其他线程获取该对象的锁,并且进入等待队列中等待被唤醒。
被唤醒的方式不同:
- sleep() 方法在指定的时间到期或者被中断时会自动返回,不需要其他线程的干预。
- wait() 方法需要其他线程调用相同对象上的 notify() 或 notifyAll() 方法来唤醒等待的线程。
综上所述,sleep() 方法主要用于线程休眠一段时间,不释放锁;而 wait() 方法主要用于线程间的通信,等待其他线程发出的通知,并且在等待时释放锁。
6.乐观锁和悲观锁是什么?怎么实现乐观锁?
乐观锁和悲观锁是两种并发控制的思想,用于解决多线程环境下的数据一致性问题。
悲观锁:
- 悲观锁的思想是假设在并发情况下,会发生数据冲突,因此在访问数据时先获取锁,确保其他线程无法同时访问,从而保证数据的一致性。
- 悲观锁的代表是数据库中的行级锁或表级锁,在并发高的情况下可能会导致大量的线程等待锁的释放,性能较差。
乐观锁:
- 乐观锁的思想是假设在并发情况下,数据不会发生冲突,因此不加锁进行操作,而是在更新数据时检查数据的版本号或者时间戳等,如果发现数据被其他线程修改,则认为操作失败,需要重新尝试。
- 乐观锁的代表是CAS(Compare And Swap)操作,通过比较当前值与期望值是否相等,如果相等则进行更新,否则重试或者放弃操作。
乐观锁的实现:
- 在Java中,乐观锁可以通过CAS操作来实现,主要依赖于java.util.concurrent.atomic包中的原子类,如AtomicInteger、AtomicLong等。
- CAS操作的基本思想是:先读取当前值,然后比较是否与期望值相等,如果相等则更新为新值,否则重新读取当前值并重试,直到更新成功或者达到重试次数上限。
- 下面是一个简单的示例,展示了如何使用AtomicInteger实现乐观锁:
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLockExample {
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
int oldValue;
int newValue;
do {
oldValue = counter.get();
newValue = oldValue + 1;
} while (!counter.compareAndSet(oldValue, newValue));
}
public int getValue() {
return counter.get();
}
public static void main(String[] args) {
OptimisticLockExample example = new OptimisticLockExample();
// 创建多个线程并发执行increment操作
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
example.increment();
}
}).start();
}
// 等待所有线程执行完毕
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出最终结果
System.out.println("Final value: " + example.getValue());
}
}
在这个示例中,多个线程并发执行increment()方法来增加计数器的值,通过CAS操作来保证线程安全。
7.JMM(Java内存模式)详细说一下?volatile 关键字解决了什么问题?
JMM(Java内存模型)定义了Java程序中多线程之间的内存交互规则,它规定了在多线程环境下,线程如何与主内存进行数据交互、如何进行内存可见性、如何保证指令重排序的一致性等。JMM主要解决了多线程并发访问共享变量时可能出现的内存可见性、指令重排序和原子性等问题。
下面是JMM的几个主要特性:
-
**内存可见性(Visibility):**当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。如果没有采取特殊的同步机制,多个线程之间对共享变量的修改可能不会及时同步到主内存和其他线程的工作内存,导致数据不一致的问题。
-
**原子性(Atomicity):**在JMM中,一些特定的操作,如读取、写入一个共享变量,是不可分割的,要么执行完整,要么不执行,不存在中间状态。对于一些复合操作,JMM通过加锁机制或者原子类来保证其原子性。
-
**有序性(Ordering):**在JMM中,虽然编译器和处理器可能会对指令进行重排序优化,但是JMM通过禁止特定类型的指令重排序,保证了在多线程环境下的执行顺序符合程序的预期。
-
** volatile **关键字是Java语言中用于保证内存可见性的一种机制,它解决了在多线程环境下共享变量的可见性问题。具体来说,使用volatile关键字修饰的变量具有如下特性:
-
**内存可见性:**被volatile修饰的变量对所有线程都是可见的。当一个线程修改了volatile变量的值后,这个变化对其他线程立即可见,即使这些线程使用的是不同的CPU缓存。
-
**禁止指令重排序:**volatile变量的写操作在多线程环境下会插入内存屏障(Memory Barrier),保证了写操作的原子性和有序性。这意味着在写入volatile变量之前的所有操作都会先行发生于写入操作,写入操作都会先行发生于读取该变量的操作。
-
volatile关键字并不能解决所有的并发问题,它只能保证可见性,而无法保证原子性。因此,在需要保证原子性的操作(如递增操作)时,还需要使用其他同步机制,如synchronized关键字或者java.util.concurrent.atomic包中的原子类。
8.Java内存区域和JMM有什么区别?
ava内存区域(Java Memory Model)和JMM(Java内存模型)是两个相关但不同的概念。
Java内存区域:
- Java内存区域指的是Java虚拟机在执行Java程序时所管理的内存的逻辑划分。
- Java内存区域包括了方法区、堆、虚拟机栈、本地方法栈和程序计数器等几个主要部分,每个部分负责不同的功能和存储不同类型的数据。
- Java内存区域是Java虚拟机的内存管理模型,用于描述Java虚拟机如何划分和管理内存空间,以及在运行Java程序时如何存储和访问数据。
Java内存模型(JMM):
- Java内存模型指的是Java程序中多线程之间的内存交互规则,定义了Java程序中线程如何与主内存进行数据交互、如何进行内存可见性、如何保证指令重排序的一致性等。
- Java内存模型描述了在多线程环境下,Java虚拟机如何处理线程之间共享变量的访问和修改,以及如何保证多线程之间的数据一致性和正确性。
- Java内存模型是Java程序员编写多线程程序时需要遵循的规范,用于指导程序员如何正确地编写线程安全的代码,避免出现数据竞争和内存可见性问题。
总的来说,Java内存区域是Java虚拟机在运行时管理内存的逻辑划分,而Java内存模型是规定了Java程序中多线程之间如何进行内存交互和数据访问的规范。Java内存区域是Java虚拟机的一部分,而Java内存模型是Java编程语言的一部分
9.happens-before原则?
“happens-before” 原则是 Java 内存模型中的一个重要概念,用于规定程序中操作之间的执行顺序和可见性。具体来说,如果一个操作 “happens-before” 另一个操作,那么第一个操作的执行结果对第二个操作是可见的,且第一个操作发生在第二个操作之前。
“happens-before” 原则的几个规则如下:
- 程序顺序规则(Program Order Rule):在一个线程内,按照程序代码的顺序,前面的操作 “happens-before” 后面的操作。
- 监视器锁规则(Monitor Lock Rule):对一个锁的解锁操作 “happens-before” 于后续对同一个锁的加锁操作。
- volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作 “happens-before” 后续对同一个 volatile 变量的读操作。
- 传递性(Transitive):如果 A “happens-before” B,B “happens-before” C,那么 A “happens-before” C。
- 线程启动规则(Thread Start Rule):一个线程 A 启动另一个线程 B,那么 A 线程启动操作 “happens-before” 于 B 线程的任意操作。
- 线程终止规则(Thread Termination Rule):一个线程的所有操作都 “happens-before” 于其它线程检测到该线程已经终止。
- 线程中断规则(Thread Interruption Rule):对线程 interrupt() 方法的调用 “happens-before” 于被中断线程的代码检测到中断的发生。
- 对象终结规则(Finalizer Rule):一个对象的初始化完成 “happens-before” 它的 finalize() 方法的开始。
“happens-before” 原则为编写多线程程序提供了重要的指导,通过正确理解和遵守这些规则,可以帮助开发人员编写出正确、高效的多线程程序,避免出现数据竞争、内存可见性等并发问题。
10.synchronized 关键字的作用?
synchronized 关键字是 Java 中用于实现线程同步的机制,它可以应用于方法或代码块。synchronized 的作用包括以下几个方面:
- **实现线程安全:**通过在关键代码段或方法前加上 synchronized 关键字,可以确保同一时间只有一个线程可以访问这段代码或方法,从而避免了多线程环境下的数据竞争和并发访问的问题,保证了程序的线程安全性。
- **实现互斥访问:**synchronized 保证了同一时间只有一个线程可以获得锁并执行关键代码段,其他线程必须等待该线程释放锁之后才能获得锁进入临界区执行,从而实现了对共享资源的互斥访问。
- **保护共享数据的完整性:**当多个线程同时访问共享数据时,synchronized 关键字可以确保在同一时间只有一个线程对共享数据进行修改,避免了数据的不一致性。
- **保证内存可见性:**除了实现互斥访问外,synchronized 还能确保对共享变量的修改操作对其他线程可见。当一个线程释放锁时,它会把对变量的修改刷新到主内存,使得其他线程在获取锁时能够看到最新的值。
总的来说,synchronized 关键字是 Java 中实现线程同步的基本手段,它能够确保在多线程环境下对共享资源的安全访问,保护数据的完整性和一致性,避免了竞态条件和并发访问问题,是编写多线程程序时常用的重要技术之一。
11.synchronized 和 ReentrantLock 的区别?
synchronized 关键字和 ReentrantLock 是 Java 中用于实现线程同步的两种机制,它们都可以保证线程安全,但在使用方式、功能特性和灵活性等方面有一些区别。
下面是它们之间的主要区别:
实现方式:
- synchronized 是 Java 语言内置的关键字,通过在方法或代码块前加上 synchronized 关键字来实现线程同步。
- ReentrantLock 是一个基于 Java API 的可重入锁,它是一个类,在使用时需要显式地创建一个 ReentrantLock 对象,并调用其方法来实现线程同步。
锁的获取方式:
- synchronized 获取锁的过程是隐式的,当线程进入同步代码块或同步方法时,会自动获取锁,并在退出同步代码块或方法时释放锁。
- ReentrantLock 的锁获取方式是显式的,需要调用 lock() 方法来获取锁,并在使用完毕后调用 unlock() 方法释放锁。
可重入性:
- synchronized 是可重入锁,同一线程可以多次获取同一个对象上的锁,而不会导致死锁。
- ReentrantLock 也是可重入锁,但是它提供了更强大的重入特性,例如可以指定公平性、可中断性等。
条件变量支持:
- ReentrantLock 提供了 Condition 接口,可以通过 newCondition() 方法创建多个条件变量,从而实现更复杂的线程通信和等待/通知机制。
- synchronized 不直接支持条件变量,但是可以通过 wait()、notify() 和 notifyAll() 方法来实现简单的线程通信。
性能:
- 在某些情况下,ReentrantLock 的性能可能比 synchronized 更好,因为它提供了更多的灵活性和功能,例如公平锁、可中断锁等。
- 在其他情况下,synchronized 可能比 ReentrantLock 的性能更好,因为 synchronized 是 JVM 内置的机制,不需要额外的开销。
综上所述,synchronized 是 Java 内置的实现线程同步的机制,使用简单方便,适合大多数情况下的同步需求;而 ReentrantLock 则提供了更多的功能特性和灵活性,适合对同步控制有更高要求的场景。选择使用哪种方式取决于具体的需求和性能要求。
12.synchronized 和 volatile 的区别?
synchronized 关键字和 volatile 关键字都是 Java 中用于实现多线程之间内存可见性和线程安全的机制,但它们之间有几点明显的区别:
作用范围:
- synchronized 关键字可以应用于代码块或方法上,用于对一段代码或整个方法进行同步,从而实现线程安全。
- volatile 关键字主要用于修饰变量,用于保证变量的可见性,确保一个线程对该变量的修改对其他线程是立即可见的。
实现机制:
- synchronized 通过获取对象的监视器锁来实现线程之间的同步,当一个线程进入 synchronized 代码块或方法时,它会尝试获取锁,如果获取失败则被阻塞,直到获取锁为止。
- volatile 通过强制线程从主内存中读取变量的值,而不是使用线程的本地缓存,以确保所有线程都能看到最新的值。同时,对 volatile 变量的写操作会立即刷新到主内存,保证可见性。
适用场景:
- synchronized 适用于复杂的临界区同步和复合操作的原子性保证,可以保证多个线程对共享资源的安全访问。
- volatile 适用于单一变量的读写操作,可以保证对该变量的读取和修改是原子的,并且对其他线程立即可见。
内存语义:
- synchronized 除了具有保证可见性外,还具有原子性和有序性。当一个线程获取到锁后,它对共享变量的修改操作是原子的,并且在释放锁之前的操作不会被重排序。
- volatile 仅具有保证可见性的特性,对于复合操作的原子性和有序性没有保证,因此在一些复杂场景下可能不适用。
综上所述,synchronized 适用于复杂的同步操作和临界区保护,可以确保线程安全性,而 volatile 适用于保证单个变量的可见性,用于简单的变量读写操作。在选择使用时,需要根据具体的需求和场景进行考虑。
13.synchronized 关键字的底层原理?
synchronized 关键字是 Java 中用于实现线程同步的关键字,它可以应用于方法或代码块。底层原理涉及到 Java 对象头和 Monitor(监视器)的概念。
下面是 synchronized 关键字的底层原理:
Java 对象头:
- 每个 Java 对象在内存中都有一个对象头,用于存储对象的元信息,包括对象的哈希码、GC 相关信息、锁状态等。
- 在 HotSpot 虚拟机中,对象头的存储结构包括标记字(Mark Word)、类型指针(Klass Pointer)等字段。
Monitor(监视器):
- 每个对象都与一个 Monitor 相关联,Monitor 用于实现 synchronized 关键字的语义。
- Monitor 可以视为一个互斥锁,用于保护对象的临界区域,确保同一时间只有一个线程可以执行临界区代码。
进入同步代码块的过程:
- 当一个线程进入一个使用 synchronized 关键字修饰的同步代码块时,它会尝试获取对象关联的 Monitor。
- 如果 Monitor 的锁状态为无锁状态(空闲),则该线程会成功获取锁,并将 Monitor 锁状态置为锁定状态,然后执行同步代码块。
- 如果 Monitor 的锁状态为锁定状态(被其他线程持有),则该线程会被阻塞,直到持有锁的线程释放锁为止。
释放锁的过程:
- 当一个线程执行完一个使用 synchronized 关键字修饰的同步代码块时,会释放对象关联的 Monitor,将其锁状态置为无锁状态,唤醒等待在该 Monitor 上的其他线程。
锁的重入性:
- synchronized 关键字是可重入锁,即同一个线程可以多次获得同一个对象上的锁,不会造成死锁。
- 在进入 synchronized 方法或代码块时,会尝试获取对象关联的 Monitor,如果当前线程已经持有该 Monitor,则可直接进入临界区,不会再次阻塞。
总的来说,synchronized 关键字通过对象的 Monitor 实现了线程的互斥访问,确保了多线程环境下的数据安全性。每个对象的 Monitor 在 JVM 中有一个对应的数据结构与之关联,用于管理对象的锁状态和线程的等待队列,从而实现了对临界区的控制。
14.ThreadLocal 关键字的作用?
ThreadLocal 关键字的作用是创建线程局部变量。线程局部变量是指每个线程都有自己的变量副本,不同线程之间互不干扰,每个线程都可以独立地修改自己的副本,而不会影响其他线程的副本。ThreadLocal 实例通常被声明为静态变量,每个线程通过 ThreadLocal 实例可以访问自己的局部变量。
ThreadLocal 关键字的主要作用有以下几点:
- **实现线程封闭:**ThreadLocal 可以实现线程封闭,将数据与线程绑定,确保数据在同一个线程中的访问安全,不会被其他线程访问或修改。
- **线程上下文传递:**ThreadLocal 可以方便地将数据在同一个线程中传递,而不需要显式地传递参数或通过全局变量等方式。
- **减少线程同步:**使用 ThreadLocal 可以避免在多线程环境下进行显式的线程同步,因为每个线程都有自己的局部变量副本,不需要考虑多线程并发访问的问题。
- **线程上下文信息保存:**在一些场景下,比如 Web 开发中的用户认证、事务管理等,可以使用 ThreadLocal 来保存线程上下文信息,方便在整个线程执行过程中进行访问和修改。
总的来说,ThreadLocal 关键字提供了一种简单且有效的方式来实现线程局部变量,能够有效地解决多线程环境下数据共享和访问安全的问题,是 Java 中处理线程封闭和线程上下文传递的重要工具之一
15.线程是什么?有什么用?为什么不推荐使用内置线程池?
线程(Thread)是程序执行流的最小单元,它是操作系统能够进行运算调度的最小单位。线程是进程中的实际运作单位,一个进程可以拥有多个线程,这些线程可以并发执行,共享进程的资源,但每个线程拥有独立的执行流程。
线程的主要作用包括:
- **并发执行:**线程使得程序能够并发执行,提高了程序的执行效率和资源利用率。在多核处理器上,多个线程可以同时执行,从而充分利用了多核处理器的性能。
- **异步编程:**线程可以实现异步编程,使得程序能够同时执行多个任务,提高了程序的响应速度和用户体验。
- **提高程序的并发能力:**线程使得程序能够处理多个任务,实现多个功能模块之间的并发执行,提高了程序的并发能力和处理能力。
- **实现复杂的任务分解:**线程可以将复杂的任务分解成多个子任务,并发执行这些子任务,从而提高了任务执行的效率。
- **资源共享:**线程可以共享进程的资源,包括内存空间、文件句柄等,使得程序能够更加高效地利用系统资源。
尽管线程提供了很多好处,但是在使用内置线程池时也需要注意以下几点,因此有时不推荐直接使用内置线程池:
- **固定的线程数量:**内置线程池通常是固定大小的线程池,一旦达到最大线程数,新任务将被放入队列中等待执行。如果应用场景需要动态调整线程数量,使用固定大小的线程池可能不太合适。
- **任务队列容量有限:**内置线程池的任务队列通常是有限大小的,当任务数量超过队列容量时,新任务可能会被拒绝或者触发异常,导致任务丢失。
- **无法定制线程池参数:**内置线程池的参数通常是固定的,无法定制化,如线程池的核心线程数、最大线程数、任务队列类型、拒绝策略等。
- **缺乏监控和扩展功能:**内置线程池通常缺乏监控和扩展功能,无法方便地获取线程池的运行状态、监控线程池的运行情况、进行线程池的动态调整等。
综上所述,尽管内置线程池提供了简单易用的方式来管理线程,但在某些场景下,特别是对线程池的动态调整、监控和扩展等功能有要求时,可能需要自定义线程池或使用第三方线程池实现。
16.Java线程池有哪些?阻塞队列有几种?拒绝策略有几种?
Java 中常用的线程池有以下几种:
FixedThreadPool(固定大小线程池):
- FixedThreadPool 是一个固定大小的线程池,在线程池初始化时就会创建固定数量的线程。当线程池中的线程达到最大数量时,新任务会被放入任务队列中等待执行。
CachedThreadPool(缓存线程池):
- CachedThreadPool 是一个可以根据需要创建新线程的线程池。当有新任务提交时,如果线程池中有空闲线程,则会重用空闲线程执行任务;如果没有空闲线程,则会创建新线程。如果线程在60秒内没有被使用,则会被终止并从线程池中移除。
SingleThreadPool(单线程线程池):
- SingleThreadPool 是一个只有一个工作线程的线程池,适用于需要保证顺序执行的场景,所有任务都将在同一个线程中顺序执行。
ScheduledThreadPool(定时任务线程池):
- ScheduledThreadPool 是一个支持定时执行任务的线程池,可以按照固定的频率或者固定的延迟执行任务。
WorkStealingPool(工作窃取线程池):
- WorkStealingPool 是 Java 7 新增的线程池类型,它是一种支持工作窃取的线程池。每个线程都有自己的任务队列,当自己的队列为空时,会从其他线程的队列中窃取任务来执行,从而提高了线程的利用率。
阻塞队列是一种特殊的队列,当队列为空时,尝试从队列中获取元素的线程将会被阻塞,直到队列中有元素可以获取;当队列已满时,尝试向队列中添加元素的线程将会被阻塞,直到队列中有空间可以添加元素。
Java 中常用的阻塞队列有以下几种:
- **ArrayBlockingQueue:**基于数组实现的有界阻塞队列,内部使用固定大小的数组存储元素。当队列满时,尝试向队列中添加元素的线程将被阻塞。
- **LinkedBlockingQueue:**基于链表实现的有界或无界阻塞队列,当构造时指定了容量,则为有界队列;否则为无界队列。当队列满时(有界队列)或者空时,尝试向队列中添加或获取元素的线程将被阻塞。
- **PriorityBlockingQueue:**基于堆实现的无界阻塞优先队列,可以根据元素的优先级顺序进行获取,当队列为空时,尝试获取元素的线程将被阻塞。
拒绝策略用于处理线程池队列满时无法接受新任务的情况。
Java 中常用的拒绝策略有以下几种:
- **AbortPolicy(默认策略):**直接抛出 RejectedExecutionException 异常,阻止系统正常运行。
- **CallerRunsPolicy:**将任务交由调用线程(提交任务的线程)来执行。
- **DiscardPolicy:**直接丢弃新提交的任务,不做任何处理。
- **DiscardOldestPolicy:**丢弃队列中最旧的任务,然后尝试将新任务添加到队列中。
每种拒绝策略都有各自的适用场景,开发者可以根据实际情况选择合适的拒绝策略来处理任务提交失败的情况。
17.手写一个线程池?
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class CustomThreadPool {
private final int poolSize;
private final WorkerThread[] threads;
private final BlockingQueue<Runnable> taskQueue;
public CustomThreadPool(int poolSize) {
this.poolSize = poolSize;
taskQueue = new LinkedBlockingQueue<>();
threads = new WorkerThread[poolSize];
// 创建并启动工作线程
for (int i = 0; i < poolSize; i++) {
threads[i] = new WorkerThread();
threads[i].start();
}
}
public void execute(Runnable task) {
synchronized (taskQueue) {
taskQueue.offer(task);
taskQueue.notify(); // 唤醒一个工作线程
}
}
private class WorkerThread extends Thread {
public void run() {
Runnable task;
while (true) {
synchronized (taskQueue) {
// 如果任务队列为空,则等待任务的到来
while (taskQueue.isEmpty()) {
try {
taskQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 从任务队列中取出任务执行
task = taskQueue.poll();
}
// 执行任务
task.run();
}
}
}
// 关闭线程池
public void shutdown() {
for (WorkerThread thread : threads) {
thread.interrupt(); // 中断线程
}
}
public static void main(String[] args) {
CustomThreadPool threadPool = new CustomThreadPool(5); // 创建一个线程池,包含5个工作线程
// 提交任务
for (int i = 0; i < 10; i++) {
final int taskId = i;
threadPool.execute(() -> {
System.out.println("Task " + taskId + " is running.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
threadPool.shutdown();
}
}
该示例中实现了一个简单的线程池,包括以下几个主要步骤:
- 创建 CustomThreadPool 类,包含线程池的基本结构和成员变量。
- 创建 WorkerThread 内部类作为工作线程,用于执行任务。
- 在线程池初始化时创建并启动指定数量的工作线程。
- 提供 execute(Runnable task) 方法,用于向线程池提交任务。
- 在工作线程中循环从任务队列中取出任务执行。
- 提供 shutdown() 方法,用于关闭线程池,中断所有工作线程。
该示例实现了一个简单的线程池,但并不具备线程池的高级功能,如任务拒绝策略、线程池扩展、监控等。在实际应用中,可以根据需要进行扩展和优化。
18.实现 Runnable 接口和 Callable 接口的区别?
Runnable 接口和 Callable 接口都是 Java 中用于描述可在线程中执行的任务的接口,它们的主要区别在于返回值、异常处理和使用方式等方面。
返回值:
- Runnable 接口的 run() 方法没有返回值,通常用于执行无返回结果的任务。
- Callable 接口的 call() 方法可以返回一个结果,通常用于执行需要返回结果的任务。
异常处理:
- Runnable 接口的 run() 方法不能抛出任何受检查异常,只能捕获并处理异常,因为 run() 方法的签名不允许抛出异常。
- Callable 接口的 call() 方法可以抛出受检查异常,但调用者必须显式捕获或者在调用时使用 throws 声明来处理异常。
返回结果:
- Runnable 接口的 run() 方法没有返回值,因此无法直接获取任务的执行结果。
- Callable 接口的 call() 方法可以返回一个结果,可以通过 Future 对象来获取任务的执行结果。
使用方式:
- Runnable 接口通常与 Thread 类一起使用,通过创建一个 Thread 对象并将 Runnable 对象传入 Thread 的构造方法来创建一个新的线程执行任务。
- Callable 接口通常与 ExecutorService 接口一起使用,通过调用 ExecutorService 的 submit(Callable) 方法来提交任务,返回一个 Future 对象,通过 Future 对象可以获取任务的执行结果。
总的来说,Runnable 接口适用于执行无返回结果的任务,而 Callable 接口适用于执行需要返回结果的任务,并且 Callable 接口提供了更丰富的异常处理和返回结果的功能,因此在需要获取任务执行结果的情况下,通常使用 Callable 接口。
19.如何给线程池命名?为什么建议给线程池命名?
在 Java 中,通常使用 ThreadFactory 接口来为线程池中的线程命名。通过自定义 ThreadFactory 实现类,可以在创建线程池时指定命名规则,从而为线程池中的线程设置有意义的名称。
下面是一个简单的示例代码,演示如何通过自定义 ThreadFactory 实现类为线程池中的线程命名:
import java.util.concurrent.ThreadFactory;
public class NamedThreadFactory implements ThreadFactory {
private final String namePrefix;
private final ThreadGroup group;
private int threadCount;
public NamedThreadFactory(String namePrefix) {
this.namePrefix = namePrefix;
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + "-" + threadCount++, 0);
if (t.isDaemon()) {
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}
使用自定义的 NamedThreadFactory 类,可以在创建线程池时通过构造方法指定线程名的前缀,然后使用这个 ThreadFactory 实例来创建线程池。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
// 创建一个带有命名线程的线程池
ExecutorService executor = Executors.newFixedThreadPool(5, new NamedThreadFactory("MyThreadPool"));
// 提交任务给线程池执行
executor.execute(() -> {
// 任务内容
System.out.println("Task executed by thread: " + Thread.currentThread().getName());
});
// 关闭线程池
executor.shutdown();
}
}
建议给线程池命名的原因包括:
- **方便识别和排查问题:**给线程池和线程命名可以方便地识别线程池和线程的作用,有助于定位和排查线程相关的问题。
- **提高可读性:**线程池中的线程如果有有意义的命名,可以提高代码的可读性,使代码更易于理解和维护。
- **监控和管理:**给线程池命名可以方便地对线程池进行监控和管理,例如通过监控工具查看线程池的运行状态、线程数量等信息。
综上所述,给线程池命名是一种良好的编程习惯,有助于代码的可读性、排查问题和监控管理。
20.如何动态修改线程池参数?
在 Java 中,动态修改线程池参数可以通过以下步骤实现:
- **创建可调整参数的线程池:**在创建线程池时,使用可调整参数的方式来指定线程池的参数,例如通过构造方法或者相应的设置方法来设置线程池的核心线程数、最大线程数、线程存活时间等参数。
- **提供修改参数的方法:**在线程池的管理类中,提供相应的方法来修改线程池的参数,例如提供设置核心线程数、设置最大线程数、设置线程存活时间等方法。
- **调用修改参数的方法:**在需要修改线程池参数的地方调用相应的方法,动态地修改线程池的参数。
下面是一个简单的示例代码,演示了如何通过自定义的线程池管理类来动态修改线程池的核心线程数和最大线程数:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class CustomThreadPoolManager {
private ThreadPoolExecutor threadPool;
public CustomThreadPoolManager(int corePoolSize, int maximumPoolSize) {
threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(corePoolSize);
threadPool.setMaximumPoolSize(maximumPoolSize);
}
// 提供设置核心线程数的方法
public void setCorePoolSize(int corePoolSize) {
threadPool.setCorePoolSize(corePoolSize);
}
// 提供设置最大线程数的方法
public void setMaximumPoolSize(int maximumPoolSize) {
threadPool.setMaximumPoolSize(maximumPoolSize);
}
// 提交任务给线程池执行
public void executeTask(Runnable task) {
threadPool.execute(task);
}
// 关闭线程池
public void shutdown() {
threadPool.shutdown();
}
public static void main(String[] args) {
CustomThreadPoolManager threadPoolManager = new CustomThreadPoolManager(5, 10);
// 执行任务
for (int i = 0; i < 20; i++) {
final int taskId = i;
threadPoolManager.executeTask(() -> {
System.out.println("Task " + taskId + " is running by thread: " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 动态修改线程池参数
threadPoolManager.setCorePoolSize(10);
threadPoolManager.setMaximumPoolSize(20);
// 关闭线程池
threadPoolManager.shutdown();
}
}
在上述示例中,我们通过自定义的线程池管理类 CustomThreadPoolManager 提供了设置核心线程数和最大线程数的方法,并在执行任务和需要修改线程池参数的地方调用相应的方法,实现了动态修改线程池参数的功能。
21.AQS原理了解吗?组件有哪些?
AQS(AbstractQueuedSynchronizer)是 Java 中用于构建同步器的抽象基类,它提供了一种框架,使得构建自定义的同步器变得更加容易。AQS 是 Java 并发包中许多同步器的基础,如 ReentrantLock、Semaphore、CountDownLatch 等。
AQS 的核心思想是使用一个双向链表(CLH 队列)来管理等待获取锁的线程,通过状态标志来表示资源的占用情况,从而实现对共享资源的安全访问。
AQS 主要包含以下几个组件:
- **state(状态):**AQS 通过一个整型变量 state 来表示同步状态,可以被子类修改和访问。state 的含义和使用方式由子类自行定义,常用的方式包括表示资源的可用数量、锁的占用状态等。
- **双向链表(CLH 队列):**AQS 使用一个双向链表来管理等待获取锁的线程。每个节点表示一个等待线程,它包含了一个状态标志、一个指向前驱节点的引用和一个指向后继节点的引用。
- **acquire(获取锁)方法:**acquire 方法用于获取锁,它是 AQS 实现同步功能的核心方法之一。acquire 方法会尝试获取锁,如果获取成功则返回,否则将当前线程加入到等待队列中,并挂起线程直到获取锁成功。
- **release(释放锁)方法:**release 方法用于释放锁,它也是 AQS 实现同步功能的核心方法之一。release 方法会释放锁,并唤醒等待队列中的一个线程(通常是队列头部的线程)。
- **ConditionObject(条件对象):**AQS 还提供了 Condition 条件对象,用于实现基于条件的等待和通知机制。ConditionObject 内部维护了一个等待队列,通过 await() 方法等待条件满足,通过 signal() 或 signalAll() 方法通知等待线程。
通过对这些组件的合理组合和实现,可以构建出各种基于 AQS 的同步器,实现复杂的并发控制逻辑。 AQS 的设计使得 Java 并发包中的同步器能够统一使用一套基础的实现逻辑,简化了同步器的实现和使用。
22.Semaphore 有什么用?原理是什么?
Semaphore 是 Java 中的一个同步工具类,用于控制同时访问某个资源的线程数量。它可以看作是一个计数器,表示可以同时允许多少个线程访问某个资源。Semaphore 提供了两个主要的方法:acquire() 和 release()。
- acquire() 方法尝试获取一个许可,如果没有可用的许可(计数器为0),则线程将被阻塞,直到有许可可用或者线程被中断。
- release() 方法释放一个许可,将计数器加一,释放一个阻塞在 acquire() 方法上的线程。
Semaphore 的主要用途包括:
- **控制并发线程数量:**可以限制同时执行的线程数量,防止过多的线程访问共享资源,从而避免资源的过度竞争和性能下降。
- **实现资源池:**可以用 Semaphore 来实现资源池,例如数据库连接池、线程池等,通过控制资源池中的资源数量,防止资源被过度占用。
- **实现有界队列:**可以用 Semaphore 来实现有界队列,例如生产者-消费者模式中的阻塞队列,通过控制队列的许可数量来限制队列的大小。
Semaphore 的原理是基于共享锁(Lock)和条件队列(Condition)来实现的。Semaphore 内部维护了一个计数器,表示可用的许可数量,线程调用 acquire() 方法时会尝试获取许可,如果计数器大于0,则成功获取许可并将计数器减一;如果计数器等于0,则线程会阻塞并加入条件队列等待许可的释放。当有线程调用 release() 方法释放许可时,会唤醒一个或多个等待线程,使其从条件队列中移出并重新尝试获取许可。
总的来说,Semaphore 是一种用于控制并发线程数量的同步工具,通过控制许可的数量来限制对共享资源的访问,可以有效地管理和优化多线程程序的并发访问。
23.CountDownLatch 有什么用?原理是什么?
CountDownLatch 是 Java 中的一个同步工具类,用于实现线程间的等待。它允许一个或多个线程等待其他线程完成操作后再继续执行。CountDownLatch 内部维护了一个计数器,初始化时指定计数器的值,每次调用 countDown() 方法将计数器减一,调用 await() 方法的线程将阻塞,直到计数器值减为0。
CountDownLatch 的主要用途包括:
- **实现线程协调:**可以用 CountDownLatch 实现线程之间的协调,例如在主线程等待多个子线程完成任务后再继续执行。
- **控制任务的启动顺序:**可以用 CountDownLatch 控制多个任务的启动顺序,例如某个任务依赖于其他任务的完成后才能启动。
- **等待外部事件的发生:**可以用 CountDownLatch 等待外部事件的发生,例如等待某个服务启动完成或者某个资源准备就绪后再继续执行。
CountDownLatch 的原理是基于共享锁(Lock)和条件队列(Condition)来实现的。CountDownLatch 内部维护了一个计数器,每次调用 countDown() 方法将计数器减一,当计数器值为0时,所有等待的线程将被唤醒。调用 await() 方法的线程会阻塞,直到计数器值为0。当计数器值为0时,所有等待的线程将被唤醒并继续执行。
总的来说,CountDownLatch 是一种用于实现线程间等待的同步工具,通过控制计数器的值来实现线程间的协调和等待。
24.CyclicBarrier有什么用?原理是什么?
CyclicBarrier 是 Java 中的一个同步工具类,用于实现线程间的同步。它允许一组线程在达到某个共同点后相互等待,然后同时继续执行。与 CountDownLatch 不同,CyclicBarrier 的计数器可以被重置并且是循环使用的。
CyclicBarrier 的主要用途包括:
- **分阶段任务的并行计算:**可以将任务划分为多个阶段,每个阶段的任务在完成后等待其他线程,然后再同时继续执行下一阶段的任务。
- **分解任务的计算过程:**可以将大任务拆分为多个子任务,并行地进行计算,然后在某个临界点合并计算结果。
- **流水线操作:**可以模拟流水线操作,每个工人负责一道工序,当所有工序完成后,流水线上的产品就完成了。
CyclicBarrier 的原理是基于共享锁(Lock)和条件队列(Condition)来实现的。CyclicBarrier 内部维护了一个计数器和一个等待队列,每个线程调用 await() 方法将计数器减一,并将自己加入等待队列,当计数器值为0时,所有等待的线程将被唤醒并继续执行。同时,计数器的值会被重置为初始值,等待下一轮的使用。
总的来说,CyclicBarrier 是一种用于实现线程间同步的同步工具,通过控制计数器的值和等待队列来实现线程间的等待和同步。
25.多个任务的编排可以怎么做?项目用到了 CompletableFuture 吗?
多个任务的编排可以使用 CompletableFuture 来实现。CompletableFuture 是 Java 中用于异步编程的工具类,它可以用于管理多个任务的执行顺序、依赖关系和结果组合等。
CompletableFuture 提供了一系列方法来实现多个任务的编排,包括:
- **thenApply()、thenAccept()、thenRun():**这些方法用于在一个任务完成后执行下一个任务,并且可以使用上一个任务的结果作为下一个任务的输入。
- **thenCombine()、thenAcceptBoth()、runAfterBoth():**这些方法用于组合多个任务的结果,等待所有任务完成后执行下一步操作。
- **thenCompose()、thenComposeAsync():**这些方法用于组合多个任务的结果,并且可以使用前一个任务的结果来触发下一个任务。
- **anyOf()、allOf():**这些方法用于等待任意一个任务完成或者所有任务完成后执行下一步操作。
- **exceptionally()、handle():**这些方法用于处理任务执行过程中的异常情况,例如捕获异常并返回默认值。
通过使用 CompletableFuture,可以轻松实现多个任务的编排,提高程序的并发性能和可维护性。
关于项目是否使用 CompletableFuture,这取决于项目的具体需求和技术栈。CompletableFuture 是 Java 8 中引入的功能,适用于需要进行异步编程和任务编排的场景,如果项目需要进行异步操作或者多个任务的编排,那么可以考虑使用 CompletableFuture 来实现。