本篇文章介绍了进程线程的基本概念、线程安全相关知识、线程池以及相关的并行并发高并发的详细讲解以及应用。同时扩展了数据库事务和锁等相关的知识、扩展了Java内存分析还有实际开发过程中好用的类,到最后列出来从基础到进阶到高阶的近100道线程并发常见面试题
线程从入门到精通一步到位
什么是进程、线程
-
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。(进程建立在操作系统上 一个操作系统上可以同时运行多个进程 这种技术叫做多进程技术)
-
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
多进程: 操作系统可以同时运行多个进程,多个进程之间时一种并行的关系 -
多进程的实现原理:
单核cpu : cpu分时间片,将时间片分配给每个进程,实现的效果为 宏观并行 微观串行
多核cpu: cpu分时间片,将时间片分配给每个进程,实现的效果为 宏观并行 微观并行
多线程: 实现原理 cpu分时间片,将时间片分配给每个线程,实现的效果为 宏观并行 微观串行。多线程之所以快,是因为可以尽可能的压栈cpu的算力,不让cpu空闲,速度比较快
并行、并发、高并发
介绍
- 并发: 指两个或多个事件同时请求执行,但是在某个瞬间有些事件是不执行的,同一时间应对多件事情的能力,多个线程轮流使用一个或多个 CPU。简单来说就是一时刻或者一瞬间只有一个线程获取到时间片执行任务。
举例:一家银行 一个办理业务的窗口 多个人排队
宏观:窗口办理了多个业务 微观:某一时刻只给一个人办理业务 - 并行: 指两个或多个事件在同一时刻发生(同时发生)。同一时间动手做多件事情的能力,双核 cpu 同时执行 2 两个线程。简单来说就是一时刻执行多个任务。
举例:一个银行 两个办理业务的窗口 多个人排队
宏观:窗口办理了多个业务 微观:某一时刻多个窗口办理多个业务 - 高并发: 指的是系统能够同时处理大量用户请求或事务的能力,在Web项目中可以理解为同时处理很多请求并响应的能力。一个请求到服务器的时候,从线程池中获取一个线程处理该请求并作出响应。
高并发扩展
- 常见的高并发场景,淘宝双十一秒杀、微博热搜、DDoS攻击。
- DDoS攻击是指同时向目标发送大量数据包或请求,以压垮目标的网络带宽、服务器资源或应用程序逻辑,从而使目标系统变得响应缓慢或完全无法访问。
*
提高系统高并发能力一般分为物理层面的扩展和系统层面的扩展
物理层面:增加服务器的数量和性能,包括CPU、内存、磁盘。提高网络带宽和优化网络架构,减少延迟吞吐量。
系统层面:
(1)负载均衡,将请求分发到多个服务器,避免单点过载。比如Nginx
(2)缓存机制,使用多级缓存,将热点数据放入缓存中。比如Redis
(3)异步处理,使用消息队列,进行异步处理和削峰填谷。比如RabbitMQ、Kafka
(4)数据库优化,读写分离(正常读取数据比较多需要进行同步数据)、数据库分片(水平分片和垂直分片)、索引优化(面试重点
加快数据检索)。
(5)架构优化,使用微服务架构的方式(根据服务需求量,部署)
线程的生命周期和状态
-
新建(New): 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。
-
就绪(Runnable): 在新建线程调用 start()方法后,线程进入就绪状态。此时在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度。
-
运行(Running): 在就绪状态下,线程获取了 CPU 的执行权限,进入运行状态。此时,线程真正开始执行其中的任务代码。在某个时刻,可能会因为时间片用完、被其他高优先级线程抢占或者主动调用 、 wait()或sleep()方法而暂停执行。
-
阻塞(Blocked):当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
-
等待(WAITING):一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。 ----> 无限期等待状态
-
计时等待(TIMED_WAITING):一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。–> 有限期等待状态
-
Terminated(终止): 线程可以以多种形式进入终止状态。包括线程执行完毕、异常导致线程终止、调用 方法等。一旦线程进入死亡状态,它的生命周期就结束了,不可再重新启动。
通过 Thread 类中的 getState()方法来获取线程状态
线程相关概念
-
线程休眠: 线程休眠本质: 主动释放cpu的执行权,进入到一个限时等待状态,在这个状态中,不会参与cpu时间片的分配.等到时间一到,重新回到初始就绪状态,等待cpu的选中.
-
线程优先级: 优先级越高,cpu优先的选中这个线程
final int getPriority() 返回此线程的优先级
final void setPriority(int i)更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10 -
线程礼让: 当一个线程礼让其他的线程,就会从运行状态,直接到达就绪状态.
static void yield()线程让步,暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程若队列中没有同优先级的线程,忽略此方法|。 -
线程守护: 守护的线程结束守护线程全部结束,陪葬制度
|public final void setDaemon(boolean on)将此线程标记为daemon线程或用户线程。当运行的唯一线程都是守护进程线程时,Java虚拟机将退出。
线程池
介绍
线程池: 简单来说线程池是存储线程的池子,线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。避免了频繁创建线程和销毁线程,提高资源利用率。
简单来说:不用线程池 —— 一个学校一个老师只能带一个班。今年招生两个班,同时招两个老师带这两个班,学生毕业把老师辞退。
用线程池—— 一个学校创建的时候招聘三个老师在办公室,今年招生两个班,那么学校在办公室找两个没有代课的老师带这两个班,学生毕业老师回办公室。下一年如果招生四个班级,在办公室找没有代课的老师,发现不够,再招老师。
创建线程池
线程七大核心参数:
- corePoolSize:线程池的基本大小,在任何时间都会维持这么多线程。
- maximumPoolSize:线程池最大线程数,限制了最多可以同时运行的线程数量。
- keepAliveTime:当线程池中的线程数目大于
corePoolSize
时,如果这时没有新的任务提交,核心线程外的线程会等待新任务到达此时间后终止。 - workQueue:用于保存等待执行的任务的阻塞队列。
- threadFactory:用于创建新线程的工厂。
- handler:饱和策略,用于处理超出队列和最大线程数的任务。
- allowCoreThreadTimeOut:是否允许核心线程超时,即当没有任务时,核心线程是否会被回收。
创建线程池并且使用:
(1) ThreadPoolExecutor
是java.util.concurrent
包下的一个类,它提供了对线程池的高级控制,包括核心参数的配置
示例代码:
import java.util.concurrent.*;
public class ThreadPoolExecutorExample {
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize: 核心线程数为5
10, // maximumPoolSize: 最大线程数为10
60L, // keepAliveTime: 非核心线程空闲后的存活时间为60秒
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // workQueue: 阻塞队列容量为100
Executors.defaultThreadFactory(), // threadFactory: 使用默认的线程工厂
new ThreadPoolExecutor.AbortPolicy() // handler: 当任务无法处理时采取的策略,这里是直接抛出异常
);
// 提交任务
for (int i = 0; i < 15; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task ID: " + taskId + " is running by " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
}
}
在这个例子中,我们创建了一个线程池,其中核心线程数为5,最大线程数为10,非核心线程的空闲存活时间为60秒,队列容量为100,使用默认的线程工厂,并且当任务无法被处理时,采用抛出异常的策略。
(2)Spring框架中,ThreadPoolTaskExecutor
是一个非常实用的工具,用于创建和管理线程池。下面是一个如何在Spring中配置和使用ThreadPoolTaskExecutor
的例子:
首先,我们需要在Spring配置类中定义ThreadPoolTaskExecutor
bean。假设你有一个Spring配置类:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
public class TaskExecutorConfig {
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 设置核心线程数
executor.setMaxPoolSize(10); // 设置最大线程数
executor.setQueueCapacity(20); // 设置队列大小
executor.setThreadNamePrefix("MyExecutor-"); // 设置线程名前缀
executor.setWaitForTasksToCompleteOnShutdown(true); // 等待所有任务完成后关闭线程池
executor.setAwaitTerminationSeconds(60); // 设置等待时间
executor.initialize(); // 初始化线程池
return executor;
}
}
一旦配置了ThreadPoolTaskExecutor
,你就可以在你的业务逻辑中使用它了。你可以将它注入到任何需要异步执行任务的类中:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Service
public class MyService {
private final ThreadPoolTaskExecutor taskExecutor;
@Autowired
public MyService(ThreadPoolTaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
public void executeAsyncTask(Runnable task) {
taskExecutor.execute(task);
}
}
在这个例子中,MyService
类注入了ThreadPoolTaskExecutor
,并提供了一个executeAsyncTask
方法,该方法接受一个Runnable
对象作为参数,并使用线程池异步执行这个任务。
*
使用线程池执行异步任务现在,你可以在控制器或任何其他需要异步执行的地方使用MyService
:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyController {
private final MyService myService;
@Autowired
public MyController(MyService myService) {
this.myService = myService;
}
@GetMapping("/run-task")
public String runTask() {
myService.executeAsyncTask(() -> {
try {
Thread.sleep(5000);
System.out.println("Task completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
return "Task submitted";
}
}
在这个控制器中,runTask
方法将一个异步任务提交给MyService
,然后立即返回。任务将在配置的线程池中异步执行。
线程的创建
在Java中,创建线程主要有以下三种方式:
-
继承Thread类:创建一个线程的最简单方法是定义一个Thread类的子类并重写其run()方法。在run()方法中编写线程要执行的代码。
示例:
public class MyThread extends Thread { public void run() { for (int i = 0; i < 5; i++) { System.out.println("Child thread: " + i); } } public static void main(String[] args) { MyThread t = new MyThread(); t.start(); // 启动线程 for (int i = 0; i < 5; i++) { System.out.println("Main thread: " + i); } } }
-
实现Runnable接口:定义一个实现了Runnable接口的类,并实现其run()方法。然后将这个Runnable对象传递给Thread类的构造函数来创建和启动线程。
示例:
public class MyRunnable implements Runnable { public void run() { for (int i = 0; i < 5; i++) { System.out.println("Runnable thread: " + i); } } public static void main(String[] args) { Thread t = new Thread(new MyRunnable()); t.start(); // 启动线程 for (int i = 0; i < 5; i++) { System.out.println("Main thread: " + i); } } }
-
使用Callable和FutureTask:这种方式可以为线程定义一个返回值,使用Callable接口替代Runnable接口,Callable接口的call()方法可以返回一个结果,并且可能抛出异常。
示例:
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class MyCallable implements Callable<String> { public String call() throws Exception { for (int i = 0; i < 5; i++) { System.out.println("Callable thread: " + i); } return "Result"; } public static void main(String[] args) { FutureTask<String> ft = new FutureTask<>(new MyCallable()); Thread t = new Thread(ft); t.start(); // 启动线程 try { String result = ft.get(); // 获取返回值 System.out.println(result); } catch (Exception e) { e.printStackTrace(); } } }
4.线程池方式创建: 可以避免频繁创建和销毁线程,提高资源利用率。Java中,可以使用java.util.concurrent.ExecutorService接口和它的实现类ThreadPoolExecutor或Executors工具类来创建线程池。
示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池,这里设为3个线程
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务到线程池
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task ID: " + taskId + " is running by " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
while (!executor.isTerminated()) {
// 等待所有任务完成
}
System.out.println("All tasks completed.");
}
}
第三种方式是可以获取返回值或者异常的,以上这几种方式底层都是基于 Runnable
线程安全(重要)
原子操作
不可分割的操作,一个原子操作从开始到结束不会被其他线程干扰,是一个完整的单位。
解决线程安全
使用synchronized
关键字,主要用于控制多线程对共享资源的访问,防止数据不一致的情况发生。它有以下几种用法:
-
修饰实例方法:当
synchronized
用于实例方法时,其锁对象是该方法所属的对象(this
)。这意味着如果有两个线程同时调用同一个对象的同步方法,那么在任意时刻只有一个线程能够进入这个方法。public class MyClass { public synchronized void myMethod() { // 代码块 } }
-
修饰静态方法:当
synchronized
用于静态方法时,其锁对象是该类的class对象。这意味着如果有多个线程同时调用同一个类的静态同步方法,那么在任意时刻只有一个线程能够进入这个方法。public class MyClass { public static synchronized void myStaticMethod() { // 代码块 } }
-
修饰代码块:可以指定锁对象,这样可以更灵活地控制同步范围。
public class MyClass { private Object lock = new Object(); public void myMethod() { synchronized (lock) { // 需要同步的代码块 } } }
使用synchronized
关键字时,需要注意以下几点:
- 锁住的对象必须是可变的,否则会导致死锁。
- 锁的粒度越小,性能越高,但实现起来可能更复杂。
synchronized
会自动释放锁,即如果一个线程获得了锁,并且在执行过程中抛出了未捕获的异常,那么锁将被自动释放,避免了死锁的发生。
使用java.util.concurrent.locks.Lock
接口
- 创建可重入锁:
import java.util.concurrent.locks.ReentrantLock; public class MyClass { private final ReentrantLock lock = new ReentrantLock(); public void myMethod() { lock.lock(); try { // 执行代码 } finally { lock.unlock(); } } }
使用Lock
接口需要显式地获取和释放锁,通常建议在try-finally
语句块中使用,以确保即使出现异常也能正确释放锁。
Synchronized 锁和 Lock 锁的区别
Synchronized
和 Lock
是 Java 中用于实现线程同步的两种不同机制。它们各自具有独特的特性和用途,下面是它们之间的一些主要区别:
-
类型:
Synchronized
是 Java 语言的关键字,直接在 JVM 层面实现。Lock
是一个接口,位于java.util.concurrent.locks
包中,提供了比synchronized
更多的控制手段。
-
锁的获取与释放:
Synchronized
是自动管理的,不需要明确的锁获取和释放操作。当线程离开同步块或方法时,锁会被自动释放。Lock
需要显式地调用lock()
方法获取锁,并且需要在不再需要锁时调用unlock()
方法释放锁。这通常意味着lock()
和unlock()
必须在try
/finally
块中使用,以确保即使在异常情况下锁也能被正确释放。
-
锁的状态:
Synchronized
不提供检查锁状态的能力,即无法知道是否已锁定。Lock
接口提供了检查锁状态的方法,如isLocked()
和hasQueuedThreads()
等。
-
可中断的锁:
Synchronized
不可响应中断,如果一个线程正在等待锁,它不能被中断。Lock
可以响应中断,通过lockInterruptibly()
方法可以允许等待线程响应中断。
-
公平性:
Synchronized
默认是非公平锁,不保证等待锁的线程按先进先出 (FIFO) 顺序获取锁。Lock
可以选择实现公平锁或非公平锁。
-
重入性:
- 两者都支持重入,即一个已经获取锁的线程可以再次获取同一个锁而不阻塞。
-
读写锁:
Synchronized
没有区分读锁和写锁。Lock
接口的实现如ReentrantReadWriteLock
提供了读锁和写锁,允许多个线程同时读取共享资源,但写操作是独占的。
-
性能:
- 传统上
Synchronized
因其开销较小而在简单的同步需求下表现较好。 - 对于复杂的同步需求,
Lock
提供了更高的灵活性和更好的性能,尤其是在需要高级同步功能的情况下。
- 传统上
-
适用场景:
Synchronized
适用于简单、快速的同步需求,如短小的临界区。Lock
更适用于需要高级同步控制的复杂场景,如长时间运行的同步代码块或需要更细粒度控制的场合。
选择哪种锁取决于具体的应用场景和对锁的需求。在现代 Java 中,Lock
被认为更加灵活和强大,但在简单场景下 Synchronized
的简洁性和自动管理特性使其仍然非常有用。
死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
产生死锁的几种情况
死锁是在计算机科学中,特别是在操作系统和数据库管理中,出现的一种特殊状况,其中两个或更多进程永久阻塞,每个进程都在等待另一个进程释放资源。这种情况发生时,没有外部干预,这些进程将无法继续执行。产生死锁的四个必要条件是:
-
互斥条件(Mutual Exclusion):至少有一个资源必须处于非共享模式,即一次只有一个进程可以使用该资源。如果另一个进程请求该资源,那么请求进程必须等待直到该资源被释放。
-
持有并等待(Hold and Wait):一个已经保持了至少一个资源的进程可能请求新的资源,但新资源已经被其他进程占有,此时该进程会等待资源释放,但不会释放自己已经持有的资源。
-
不可抢占条件(No Preemption):已经分配给进程的资源不能被抢占,只能由该进程显式地释放。
-
循环等待(Circular Wait):存在一组进程 {P1, P2, …, PN},P1 正在等待 P2 占有的资源,P2 正在等待 P3 占有的资源,…,PN 正在等待 P1 占有的资源,形成一个循环依赖链。
解决策略
- 避免使用嵌套锁:尽量避免在一个锁内部再次申请其他锁资源,减少死锁可能性
- 统一锁申请顺序:对于需要多个锁的场景,确保所有线程以相同的顺序请求锁,避免出现循环等待的情况
- 加锁超时或自动释放锁:在申请锁时,设置一个等待时间或使用可重入锁,避免线程长时间等待而导致死锁。同时在使用完锁之后,及时释放资源,避免持有锁时间过长
- 死锁检测和恢复:通过死锁检测算法,定期检查系统中的死锁情况,并尝试解决死锁,然后
恢复运行 - 资源分配策略优化:评估和优化资源的分配策略,避免资源竞争和瓶颈情况的发生
- 避免长时间持有锁
总的来说,解决死锁问题需要注意锁的申请顺序、资源分配策略、超时设置等,通过优化设计和避免资源竞争,可以减少死锁的可能性。在发生死锁时,通过死锁检测和恢复等方法解决死锁问题。
Synchronized扩展Synchronized的底层原理(面试)
synchronized
关键字在 Java 中用于实现线程之间的同步,确保同一时刻只有一个线程能够访问临界区(critical section),即被 synchronized
保护的代码段或方法。其底层实现涉及 JVM(Java 虚拟机)的多个层面,特别是与对象头、Monitor 和锁升级机制相关。
对象头和 Mark Word:
每个 Java 对象在内存中都有一个对象头(Header),其中包含元数据,如哈希码、GC 分代年龄、锁标志位等。Mark Word 是对象头的一部分,存储了对象的运行时数据,如锁状态信息,锁状态标志是实现 Synchronized 同步机制的关键部分,它用于记录当前对象锁的状态。
Monitor:
synchronized
的核心是 Monitor(监视器锁)。Monitor 是一个抽象概念,它包含了锁记录、条件队列、等待队列等结构。每个对象都有一个 Monitor 与之关联。当一个线程获得了对象的 Monitor,它就拥有了该对象的锁,其他线程必须等待直到锁被释放。
通俗来说当一个线程尝试进入一个由 Synchronized 修饰的方法或代码块时,它会尝试获取对象关联的 Monitor。如果 Monitor 处于无锁状态,该线程会成功获取锁,并在对象头中标记为已锁定。如果 Monitor 已被其他线程锁定,那么尝试进入的线程将被阻塞,直到获取到 Monitor 的锁
锁的获取和释放:
JVM 使用 monitorenter 和 monitorexit 字节码指令来实现锁的获取和释放。当线程遇到 monitorenter 指令时,它会尝试获取对象的 Monitor,如果成功则继续执行,否则会被阻塞。monitorexit 指令用于释放 Monitor。
锁升级:
从 Java 6 开始,JVM 引入了锁升级机制,Synchronized 的锁升级是指在不同的情况下,Synchronized 锁的状态会从偏向锁、轻量级锁、重量级锁等级别逐步升级的过程。。
-
偏向锁:当一个线程首次访问同步块时,如果该同步块尚未被锁定,JVM 会将锁状态设为偏向锁,并将线程 ID 记录在对象头的 Mark Word 中。后续该线程再次访问时,只需判断当前线程ID是否与对象头中Mark Word记录的线程ID相同,如果相同,就可以直接进入同步块,无需进行额外的同步操作可以直接进入同步块,不需要进行额外的锁获取操作。如果其他线程试图访问,偏向锁会失效,升级为轻量级锁。
-
轻量级锁:当多个线程竞争锁时,JVM 使用 CAS(Compare and Swap)操作尝试获取锁。如果 CAS 成功,线程可以继续执行;如果失败,线程可能需要在 Monitor 上自旋(自旋锁),等待锁的释放。自旋锁是一种消耗 CPU 资源但避免了线程挂起和唤醒开销的方法。当一个线程尝试获取对象的轻量级锁时,JVM会首先检查对象的锁状态。
如果对象当前未被锁定(即处于无锁状态),线程会尝试使用CAS操作将对象头部的Mark Word(一个用于存储对象元数据的字段)标记为“锁定”状态,并将指向当前线程栈帧的指针保存在Mark Word中。如果CAS操作成功,表示该线程成功获取了轻量级锁,可以执行同步代码块。如果CAS操作失败,说明有其他线程已经持有该锁,当前线程会进行自旋操作,即不断尝试重新获取锁,直到获取成功或自旋次数达到某个阈值。 -
重量级锁:如果轻量级锁的竞争过于激烈,或者自旋锁超时,锁将升级为重量级锁,当线程尝试进入同步代码块时,会执行monitorenter指令,尝试获取对象的锁。如果锁已经被其他线程持有,则当前线程会进入阻塞状态,直到锁被释放。当持有锁的线程执行完同步代码块后,会执行monitorexit指令,释放锁,并唤醒等待队列中的线程来竞争锁。这个过程确保了同一时间只有一个线程能够执行同步代码块中的代码。此外,重量级锁还涉及到线程的阻塞与唤醒,这些操作都需要操作系统的支持,因此重量级锁的开销相对较大。
可重入性:
synchronized
支持可重入性,这意味着持有锁的线程可以再次进入同一临界区而不会被阻塞。锁的可重入性由 Monitor 内部的计数器实现,每次线程获取锁时计数器加一,释放锁时减一,直到计数器为零,锁才完全释放。
锁的类型:
- 对象级锁:当
synchronized
用于实例方法或代码块时,锁的是对象本身。 - 类级锁:当
synchronized
用于静态方法时,锁的是类的 Class 对象,这影响到所有该类的实例。
锁的拓展之数据库锁
数据库锁的介绍
数据库锁是一种用于管理数据库中并发事务的重要机制,其主要目标是保证数据的一致性和隔离性,防止多个事务同时修改相同的数据导致的冲突。数据库锁可以分为几种不同类型,每种类型的锁都有其特定的用途和影响范围
数据库常见的锁
- 共享锁(Shared Locks):
共享锁允许一个事务读取数据,但阻止其他事务修改数据直到锁被释放。
多个事务可以同时对同一数据拥有共享锁。
如果一个事务尝试获取排他锁(例如,为了修改数据),则必须等待所有共享锁被释放。 - 排他锁(Exclusive Locks):
排他锁阻止其他事务读取或修改被锁定的数据。
一次只有一个事务可以对同一数据拥有排他锁。
排他锁常用于数据的修改操作,如插入、更新或删除。 - 行级锁(Row-Level Locks):
行级锁只锁定被访问的具体行,而不是整个表或页面。
这种锁提供了较高的并发度,因为它们只影响数据集的一部分。
InnoDB 存储引擎在 MySQL 中默认使用行级锁。 - 表级锁(Table-Level Locks):
表级锁锁定整个表,限制了并发操作,因为它们阻止其他事务访问整个表。
这种锁开销小,但并发度低。
MyISAM 和 Memory 存储引擎在 MySQL 中使用表级锁。 - 页级锁(Page-Level Locks):
页级锁锁定数据库页面,页面包含多行数据。
这种锁的粒度和开销介于行级锁和表级锁之间。 - 间隙锁(Gap Locks):
间隙锁锁定一个范围内的行,即使这些行目前不存在。
这种锁用于防止在索引的间隙中插入新的行,有助于保持事务的隔离级别。 - 自增锁(Auto-Increment Locks):
自增锁用于确保自增字段的唯一性和连续性,通常在插入操作时使用。 - 乐观锁(Optimistic Locking):
乐观锁通常不使用数据库锁,而是依靠版本号或时间戳来检测数据的并发修改。
如果数据在事务提交时已被其他事务修改,乐观锁会失败并回滚事务。 - 悲观锁(Pessimistic Locking):
悲观锁假设并发冲突是不可避免的,因此在事务开始时就锁定数据,直到事务完成。 - 意向锁(Intent Locks):
意向锁用于表明一个事务想要在某个层次上获取锁,例如,意向共享锁表明事务可能需要在表中获取共享锁。
这些锁并不直接锁定数据,而是表明事务的意图,帮助数据库管理系统决定是否授予锁。
数据库事务介绍
事务(Transaction)是数据库管理系统中执行一系列操作的一个逻辑单元,这些操作要么全部成功完成,要么全部失败并回滚。事务的特性通常用ACID(原子性、一致性、隔离性、持久性)来描述:
原子性(Atomicity): 事务中的所有操作要么全部成功,要么一个也不做。如果事务中的任何操作失败,整个事务都会被回滚到初始状态。
一致性(Consistency): 事务的执行将数据库从一个一致性状态带入另一个一致性状态。事务开始前和结束后,数据都必须符合所有定义好的规则。
隔离性(Isolation): 事务看起来好像独立执行,不受其他事务的影响。不同的事务在并发执行时,彼此之间是隔离的,就好像它们单独执行一样。
持久性(Durability): 一旦事务提交,它对数据库的更改就是永久的,即使在系统崩溃之后也应保持不变。
并发事务常见问题
事务在数据库系统中扮演着至关重要的角色,以确保数据的一致性和可靠性。然而,在实现和使用事务的过程中,可能会遇到各种问题,特别是当多个事务并发执行时。以下是一些事务中常见的问题:
-
脏读(Dirty Reads):
- 当一个事务正在对数据进行修改但尚未提交时,另一个事务读取到了这部分未提交的数据。如果第一个事务最终回滚,那么第二个事务将会基于无效数据进行操作,这会导致数据不一致。
-
不可重复读(Non-Repeatable Reads):
- 这发生在当一个事务读取一行数据后,另一个事务修改了该行数据并提交,导致第一个事务再次读取同一行数据时得到不同的结果。这违反了事务的隔离性原则。
-
幻读(Phantom Reads):
- 当一个事务两次读取同一范围的数据时,另一个事务在这两次读取之间插入了一些新行,导致第一次和第二次读取的结果集不同。这是在可重复读隔离级别下可能发生的现象。
-
死锁(Deadlocks):
- 发生在两个或多个事务互相等待对方释放资源,而实际上没有一方能继续执行下去的情况。死锁需要通过检测和手动或自动解除来处理。
事务的隔离级别
事务的隔离级别(Isolation Levels)是为了处理多个事务并发执行时可能出现的问题,如脏读、不可重复读和幻读。主要的隔离级别有:
-
读未提交(Read Uncommitted):最低的隔离级别,事务可以看到其他事务未提交的更改,可能导致脏读、不可重复读和幻读。
-
读已提交(Read Committed):一个事务只能看到其他事务提交后的更改。这可以防止脏读,但仍然可能有不可重复读和幻读。
-
可重复读(Repeatable Read):在一个事务中多次读取同样的数据将返回相同的结果,即使其他事务在此期间进行了更改。这可以防止脏读和不可重复读,但可能有幻读。
-
串行化(Serializable):最高级别的隔离,事务被串行执行,就像它们一个接一个地执行一样,可以防止所有并发问题,但可能造成性能瓶颈,因为需要更多的锁。
一般数据库的隔离级别选择可重复读,因为并发问题相对较少效率相对较快
隔离级别对应的锁
-
读未提交(Read Uncommitted):基本上不使用锁,因为事务不需要保证读取到的数据是已提交的。
-
读已提交(Read Committed):每次读取数据时,读取点之后的更新将被忽略,直到读取的事务结束。这通常意味着读取操作不加锁,写操作使用排他锁。
-
可重复读(Repeatable Read):在事务中,读取操作可能使用共享锁来保证读取数据的一致性,而写操作使用排他锁。
-
串行化(Serializable):使用最严格的锁机制,可能在读取的每一行上加锁,或者锁定整个表,以确保事务的串行执行。
在实际应用中,数据库系统通常使用多种锁来实现这些隔离级别,包括但不限于:
- 共享锁(S锁):允许多个事务同时读取数据,但阻止其他事务写入数据。
- 排他锁(X锁):阻止所有其他事务读取或写入数据,用于写操作。
- 意向锁:表明事务可能需要在某个层次上获取锁,用于辅助管理锁的层次结构。
不同的数据库管理系统可能使用不同的锁机制来实现事务的隔离级别,而且锁的实现细节也可能有所不同。例如,InnoDB 存储引擎在 MySQL 中使用行级锁和多版本并发控制(MVCC)来实现隔离级别。
多版本并发控制(MVCC,Multi-Version Concurrency Control)是一种在数据库系统中用于实现高并发性和事务隔离性的技术。MVCC 允许读取和写入操作在不互相阻塞的情况下并行执行,通过维护数据项的多个版本来达到这一目的。下面是 MVCC 的一些基本原理和工作方式:
MVCC扩展
MVCC 基础原理
-
数据版本化:每当一个事务修改了某个数据项时,数据库系统不会直接在原位置修改数据,而是保存一个新版本的数据,并保留旧版本。这样,不同事务可以读取到不同版本的数据。
-
事务隔离性:通过给每个事务分配一个事务标识符(通常是一个递增的整数),系统可以确定哪些数据版本对特定事务是可见的。这有助于实现事务的隔离级别。
-
一致性读:读取操作可以访问数据的一个历史快照,这个快照是在事务开始时的数据状态。这被称为“一致性读”或“快照读”。
-
当前读:当一个事务需要获取数据的最新状态时,它会进行“当前读”,这通常涉及到锁定数据,以确保读取到的数据是最新的。
MVCC 的具体实现
以 MySQL 的 InnoDB 引擎为例:
-
隐藏列:InnoDB 在每行数据中维护几个隐藏列,包括
DB_TRX_ID
(最后修改该行的事务ID)、DB_ROLL_PTR
(指向 undo 日志记录的指针)和DB_ROW_ID
(行的内部ID)。 -
版本链:每次更新数据时,旧版本会被存储在一个 undo 日志中,形成一个版本链。这样,即使数据行被更新,旧版本仍然可以通过
DB_ROLL_PTR
访问。 -
Read View:这是事务开始时创建的一致性视图,它决定了哪些事务的更改对当前事务是可见的。Read View 包含一系列规则,用于判断一个数据版本是否对当前事务可见。
-
快照读与当前读:快照读是默认的读取类型,它使用 Read View 来决定哪些数据版本是可见的。当前读则需要显式锁定数据,通常在需要获取最新数据或进行更新操作时使用。
MVCC 的优点
- 提高并发性:读取操作不会阻塞写入操作,反之亦然。
- 减少锁竞争:由于数据版本化,减少了需要锁定资源的数量,从而减少了锁的竞争。
MVCC 的局限性
- 存储开销:维护多个数据版本会占用更多的存储空间。
- 查询复杂性:确定正确的数据版本可能会增加查询的复杂性。
快照读和当前读(面试)
在数据库系统中,特别是像 MySQL 使用 InnoDB 存储引擎这样的系统,快照读(Snapshot Read)与当前读(Current Read)是两种不同的读取数据的方式,它们在事务隔离性和并发控制方面有着显著的区别。这两种读取方式与数据库的多版本并发控制(MVCC)机制紧密相关。
快照读(Snapshot Read)
快照读是指读取数据时,事务看到的是在事务开始时数据的状态,而不是最新的数据。换句话说,事务在其生命周期内看到的数据是一致的,不会因为其他事务的提交而改变。这种读取方式不加锁,因此不会阻止其他事务对同一数据进行修改。
快照读通常发生在以下情况:
- 使用 SELECT 语句进行读取时,如果不包含锁定提示(如
FOR UPDATE
或LOCK IN SHARE MODE
)。 - 在
REPEATABLE READ
隔离级别下,事务看到的数据是其开始时的快照,不会被后续提交的事务影响。 - 在
READ COMMITTED
隔离级别下,虽然每次读取都是最新的提交数据,但在没有显式加锁的情况下,读取操作也是快照读。
当前读(Current Read)
当前读是指读取最新的数据,即记录的最新版本。当进行当前读的操作时,系统会对读取的记录进行加锁,以确保其他并发事务不能修改当前记录,直到当前事务完成或释放锁。这种读取方式适用于需要读取最新数据并可能进行更新的场景。
当前读通常发生在以下情况:
- 使用 SELECT … FOR UPDATE 或 SELECT … LOCK IN SHARE MODE 这样的语句。
- 执行 UPDATE、INSERT 或 DELETE 语句时。
区别总结
- 读取的数据版本:快照读读取的是事务开始时的数据版本,而当前读读取的是数据的最新版本。
- 锁定机制:快照读不加锁,而当前读会加锁,以确保数据的一致性和事务的隔离性。
- 并发性能:快照读因为不加锁,所以有更高的并发性能,而当前读由于加锁,可能会影响并发性能,尤其是在高并发环境下。
- 隔离级别:在
SERIALIZABLE
隔离级别下,所有的读取都会变成当前读,因为在该级别下,所有的读取操作都会被加锁。
扩展Java内存及存储的元素线程共享的内存
JVM内存分析
Java虚拟机内存区域可以简单分为栈(后进先出,调用方法时会入栈)、堆(先进先出)、方法区、程序计数器、本地方法栈。以下是Java中各种元素的存储位置概览:
-
栈 (Stack)
- 局部变量: 包括方法参数和方法内部声明的变量。这些变量在方法调用时入栈,方法返回后出栈,生命周期与方法调用相关联。
- 对象引用: 存储在栈中的并不是对象本身,而是指向堆中对象的引用。
-
堆 (Heap)
- 对象实例: 当你使用
new
关键字创建一个对象时,对象的实例存储在堆中。堆是用于动态分配内存的区域,由垃圾回收器管理。 - 数组: 数组同样在堆上分配内存。
- 对象实例: 当你使用
-
方法区 (Method Area)
- 静态变量: 类中的静态变量(使用
static
关键字声明的变量)存储在方法区,这是一个线程共享的内存区域,用于存储类信息、常量、静态变量等。 - 类信息和方法信息: 包括编译后的代码、字段、方法、构造器的描述符和属性等。
- 静态变量: 类中的静态变量(使用
-
程序计数器 (Program Counter Register)
- 每个线程都有一个独立的程序计数器,用于指示当前线程正在执行的字节码指令的位置。
-
本地方法栈 (Native Method Stack)
- 用于支持本地方法的调用,与Java栈类似,但它处理的是本地方法调用。
线程内存共享
在Java Development Kit (JDK) 1.7中,Java Virtual Machine (JVM) 的内存结构可以分为多个部分,其中一些是所有线程共享的,而另一些则为每个线程所私有。以下是JDK 1.7中JVM内存区域的概述:
线程共享区域:
-
堆 (Heap)
- 所有线程共享的区域,用于存放对象实例和数组。
- 分为新生代 (Young Generation) 和老年代 (Old Generation)。
- 新生代又包括Eden空间和两个Survivor空间(S0, S1)。
-
方法区 (Method Area) 或 永久代 (PermGen)
- 在JDK 1.7中,方法区通常被称为永久代。
- 存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等。
- 这个区域也是所有线程共享的。
线程私有区域:
-
程序计数器 (Program Counter Register)
- 每个线程都有自己的程序计数器,用于指示当前线程正在执行的字节码指令的位置。
- 当线程执行的是Java方法时,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Native方法,这个计数器值为空 (Undefined)。
-
虚拟机栈 (Virtual Machine Stack)
- 每个线程拥有自己的虚拟机栈,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 每个方法执行时都会创建一个栈帧,用来存储该方法的信息。
-
本地方法栈 (Native Method Stack)
- 与虚拟机栈类似,但为虚拟机使用到的Native方法服务。
- 有的JVM实现可能将本地方法栈和虚拟机栈合并。
-
线程本地存储 (Thread Local Storage, TLS) 或者线程本地变量 (Thread Local Variables)
- 某些变量或资源可能需要在线程之间隔离,TLS提供了这种机制。
其他:
在JDK 1.7中,方法区还是以永久代的形式实现,而在JDK 1.8中,永久代被Metaspace所替代,后者使用的是本地内存,不再受JVM堆大小的限制。
------------------------------------------------------------------------------------------------------------
在JDK 1.8中,Java虚拟机(JVM)的内存模型有所变化,特别是与早期版本相比,方法区的实现方式有了显著的不同。以下是JDK 1.8中JVM内存区域的概览,区分了哪些是线程共享的,哪些是线程私有的:
线程共享区域:
-
堆内存(Heap):
- 所有线程共享的区域,用于存储对象实例和数组。这是JVM管理的最大内存区域。
-
元数据区(Metaspace):
- JDK 1.8中代替了JDK 1.7的永久代(PermGen space)。元数据区存储类信息、常量、静态变量、即时编译后的代码等数据。在JDK 1.8中,元数据区使用的是本地内存,而不是堆内存,这意味着它不受堆大小的限制。
-
运行时常量池(Runtime Constant Pool):
- 作为方法区的一部分,用于存储类或接口的常量信息,如字段名、方法名和符号引用等。由于元数据区取代了永久代,运行时常量池现在也位于元数据区中。
线程私有区域:
-
程序计数器(Program Counter Register):
- 每个线程都有自己的程序计数器,记录当前线程执行的字节码行号。当执行native方法时,它的值为
null
。
- 每个线程都有自己的程序计数器,记录当前线程执行的字节码行号。当执行native方法时,它的值为
-
虚拟机栈(Java Virtual Machine Stack):
- 每个线程都有自己的虚拟机栈,用于存储局部变量、操作数栈、动态链接和方法退出信息。每个Java方法被调用时会创建一个栈帧,方法结束后栈帧出栈。
-
本地方法栈(Native Method Stack):
- 与虚拟机栈类似,但服务于native方法的调用。在某些实现中,虚拟机栈和本地方法栈的功能可能合并在一起。
-
线程本地存储(Thread Local Storage,TLS):
- 提供了线程私有的数据存储空间,用于存储线程特有的数据。
扩展线程相关好用的类
CountDownLatch
CountDownLatch介绍
CountDownLatch是Java并发库中的一个同步辅助类,位于java.util.concurrent包下,它是在JDK 1.5以后加入的。CountDownLatch被称为“门阀”或“计数器”,它能够使一个或多个线程等待一系列指定操作的完成。CountDownLatch 初始化时带有一个指定的计数器,每当一个操作完成时,就会调用countDown()方法将计数器减一。当计数器的值减至零时,所有因调用await()方法而在CountDownLatch上等待的线程都会被释放,继续执行。
CountDownLatch主要方法及示例
主要方法
- 构造函数:public CountDownLatch(int count),接收一个int类型的参数,表示要等待的工作线程个数。
- await():调用await()方法的线程会被阻塞,直到计数器达到零。这意味着所有指定的操作都完成后,所有等待的线程会同时被释放。
- await(long timeout, TimeUnit unit):这是await()的一个重载方法,它允许设置超时时间。如果超时时间到达而计数器还未减至零,则等待的线程会被唤醒并返回。
- countDown():将计数器的值减一。当计数器的值减至零时,所有等待的线程会被唤醒。
示例代码
假设我们有一个场景,需要等待多个线程完成数据收集后进行数据汇总:
import java.util.concurrent.CountDownLatch;
public class DataCollector {
private static final int numWorkers = 5;
private static final CountDownLatch latch = new CountDownLatch(numWorkers);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < numWorkers; i++) {
new Thread(new Worker(latch)).start();
}
// 等待所有工作线程完成
System.out.println("Main thread waiting for workers...");
latch.await();
// 所有工作线程已完成,进行数据汇总
System.out.println("All workers finished. Starting data aggregation.");
}
static class Worker implements Runnable {
private final CountDownLatch latch;
public Worker(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
System.out.println("Worker " + Thread.currentThread().getName() + " started collecting data.");
try {
// 模拟数据收集耗时
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Worker " + Thread.currentThread().getName() + " finished collecting data.");
latch.countDown();
}
}
}
在这个例子中,DataCollector
类创建了五个工作线程,每个工作线程完成数据收集后调用 countDown()
方法。主方法中的 await()
方法会阻塞,直到所有工作线程完成数据收集,然后继续执行数据汇总。
CountDownLatch使用场景
- 一个线程等待其他多个线程都执行完毕,再继续自己的工作:
在实际开发场景中,很多情况下需要初始化一系列的前置操作,比如数据库先建立连接,所有bean都加载完毕,在这些准备条件都完成之前,是不能进行下一步工作的。这时,可以使用CountDownLatch来确保所有前置操作都完成后,主线程再继续执行。
示例场景:如启动一个服务时,需要等待多个组件和服务加载完毕,才能继续执行主逻辑。 - 多个线程等待某一个线程的信号,同时开始执行:
这种场景与第一种相反,是多个线程等待某个信号,一旦信号发出,所有等待的线程同时开始执行。
示例场景:如运动会上,运动员等待裁判员发令枪响后同时起跑。 - 在并行计算中,主线程等待所有计算任务完成后再继续:
在使用分治算法或并行流处理数据时,主线程可能需要等待所有的计算任务完成后才能继续执行后续逻辑。 - 在单元测试中,模拟并发情况:
使用CountDownLatch来模拟并发情况,确保所有的子线程完成任务后,再进行断言检查。 - 在分布式系统中,等待多个微服务的响应:
主线程可能需要等待来自多个微服务的响应,才能继续执行后续逻辑。 - 同时启动多个线程来执行初始化任务:
如预热缓存、加载静态数据等,主线程等待所有初始化任务完成后才继续执行。 - 初始化完成通知:
在应用启动时,可能有多个服务或组件需要初始化,使用 CountDownLatch 可以确保所有组件初始化完成后再继续后续的启动过程。 - 并行任务的同步:当一个任务依赖于多个并行执行的子任务时,可以使用 CountDownLatch 来等待所有子任务完成。
- 数据汇总:在多线程处理数据后,汇总数据前需要确保所有线程完成数据处理。
CompletableFuture
CompletableFuture 介绍
CompletableFuture 是 Java 8 引入的一个新特性,它是 Java 并发编程模型中的一个重要组成部分,用于处理异步计算和简化多线程编程CompletableFuture 实现了 Future 和 CompletionStage 接口,它不仅具有 Future 的基本功能(即获取异步操作的结果),还提供了更加丰富的功能,如组合多个异步任务、处理异常情况、以及在任务完成时执行回调函数等。
CompletableFuture 主要特点
- 异步编程:CompletableFuture 允许以非阻塞的方式编写并发代码,提高了程序的响应性和性能。
- 链式操作:支持链式调用,可以方便地将多个异步任务组合在一起,实现复杂的异步逻辑。
- 异常处理:提供了多种方式来处理异步任务执行过程中出现的异常,如使用 exceptionally 方法来提供异常处理逻辑。你可以为 CompletableFuture 设置异常处理器,当计算过程中出现异常时进行处理。
- 功能强大:比传统的 Future 接口提供了更多的功能和灵活性,使得异步编程变得更加简单和高效。
- 灵活的取消机制:支持异步操作的取消,并且可以通过 .cancel() 方法请求取消。
- 并行化:可以将多个 CompletableFuture 结果合并或转换,利用并发执行提高性能。
CompletableFuture 核心方法
- supplyAsync(): 异步执行任务,并返回结果。
- runAsync(): 异步执行任务,但不返回结果。
- thenApply(): 当异步操作完成时,对结果进行转换处理,并返回新的结果。
- thenAccept(): 当异步操作完成时,消费结果但不返回任何值。
- thenRun(): 当异步操作完成时,执行某个动作,但不接收结果。
- exceptionally(): 当异步操作抛出异常时,提供异常处理逻辑,并返回结果。
- handle(): 当异步操作完成时(无论正常还是异常),执行某些操作,并可以返回结果。
- join(): 等待异步操作完成,并返回结果。
- get(): 等待异步操作完成,并返回结果,但如果在等待期间发生中断,会抛出 InterruptedException。
CompletableFuture 使用场景及代码示例
CompletableFuture
在各种异步和并发场景下都有广泛的应用。以下是一些典型的应用场景及其对应的示例代码:
场景一:异步获取数据并进行后续处理
假设有一个耗时的异步方法 fetchDataAsync
用于从远程服务器获取数据,然后我们想要对数据进行一些处理,比如解析和格式化。
示例代码:
public CompletableFuture<String> fetchDataAsync() {
return CompletableFuture.supplyAsync(() -> {
// 模拟耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Some data from remote server";
});
}
public void processData() {
CompletableFuture<String> future = fetchDataAsync();
future.thenApply(data -> {
// 处理数据,例如解析
String processedData = data.toUpperCase(); // 转换为大写
System.out.println("Processed Data: " + processedData);
return processedData;
}).exceptionally(ex -> {
System.err.println("Error processing data: " + ex.getMessage());
return null;
});
}
场景二:并发执行多个任务并汇总结果
假设有两个耗时的异步任务,我们需要等待它们都完成,然后将结果合并。
示例代码:
public CompletableFuture<String> taskA() {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Task A completed";
});
}
public CompletableFuture<String> taskB() {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Task B completed";
});
}
public void executeTasks() {
CompletableFuture<String> result = CompletableFuture.allOf(taskA(), taskB())
.thenApply(v -> {
return taskA().join() + " and " + taskB().join();
});
System.out.println(result.join());
}
场景三:根据条件选择性地执行后续任务
假设我们有两个异步任务,但是我们只希望在第一个任务成功完成的情况下才执行第二个任务。
示例代码:
public CompletableFuture<String> firstTask() {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "First Task completed";
});
}
public CompletableFuture<String> secondTask(String input) {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return input + " with Second Task";
});
}
public void conditionalExecution() {
firstTask().thenCompose(result -> {
if (result.contains("completed")) {
return secondTask(result);
} else {
return CompletableFuture.completedFuture(result);
}
}).thenAccept(System.out::println);
}
场景四: 并行执行多个独立任务
当你需要并行执行多个不相互依赖的任务时,可以使用 CompletableFuture 的并行流或显式创建多个 CompletableFuture 实例。
示例代码:
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
// 模拟耗时任务1
System.out.println("Task 1 started");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Task 1 completed");
});
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
// 模拟耗时任务2
System.out.println("Task 2 started");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Task 2 completed");
});
CompletableFuture.allOf(future1, future2).join(); // 等待所有任务完成
System.out.println("All tasks completed");
场景五:链式处理异步结果
当你需要基于一个异步操作的结果来执行另一个异步操作时,可以使用 CompletableFuture 的链式调用功能。
示例代码:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 模拟获取数据的异步操作
return "Initial data";
}).thenApply(data -> {
// 基于上一步的结果进行处理
return data.toUpperCase();
}).thenApply(upperData -> {
// 再次处理
return "Processed: " + upperData;
});
future.thenAccept(result -> System.out.println(result))
.exceptionally(e -> {
System.err.println("Error processing data: " + e.getMessage());
return null;
});
场景六:异步操作完成后执行某些动作
有时,你可能只想在异步操作完成后执行某些不返回值的动作。
示例代码:
CompletableFuture.runAsync(() -> {
// 模拟耗时任务
System.out.println("Task is running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).thenRun(() -> {
// 任务完成后执行的动作
System.out.println("Task completed, performing cleanup");
});
场景七:异步处理集合
当你需要对集合中的每个元素进行异步处理时,可以结合 Java 8 的 Stream API 和 CompletableFuture。
示例代码(注意:直接对 Stream 使用异步操作需要谨慎,因为 Stream 是为顺序和并行处理设计的,而非异步。这里是一个简化的示例,实际使用时可能需要额外的逻辑来管理异步性):
List<String> dataList = Arrays.asList("a", "b", "c");
CompletableFuture.allOf(dataList.stream()
.map(data -> CompletableFuture.runAsync(() -> {
// 对每个元素进行异步处理
System.out.println("Processing " + data);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}))
.toArray(CompletableFuture[]::new))
.join(); // 等待所有异步操作完成
System.out.println("All data processed");
注意:上面的示例中,CompletableFuture.allOf 实际上并不是直接用于处理集合中每个元素的异步操作,因为 allOf 需要的是 CompletableFuture 类型的数组。上面的代码主要是为了演示如何结合使用 Stream 和 CompletableFuture,但在实际应用中,你可能需要采用不同的策略来处理集合中的异步操作。
对于集合的异步处理,通常建议使用如 CompletableFuture.supplyAsync() 结合集合的遍历,或者使用第三方库如 Reactor 或 RxJava,它们提供了更丰富的异步和响应式编程功能。
这些示例展示了 CompletableFuture
如何简化异步编程,并允许你以声明式方式组织和处理异步任务。请注意,在实际应用中,你可能需要使用 ExecutorService
或者 ForkJoinPool
来提供线程池,而不是默认的 ForkJoinPool.commonPool()
。
小知识点
面试题大全从简单到困难
基础概念
-
并行与并发的区别:
- 并发(Concurrency):指的是多个任务在同一个时间段内“看起来”是同时进行的。并发不一定需要多核处理器,单核处理器通过时间片轮转也可以实现并发。
- 并行(Parallelism):指的是多个任务在物理上同时进行,通常需要多核处理器支持。并行是并发的一种特殊形式,它强调的是任务的物理同时执行。
-
Java中创建线程的四种方法:
- 继承
Thread
类:通过创建一个继承自Thread
类的子类,并重写其run()
方法来定义线程的行为。 - 实现
Runnable
接口:创建一个实现了Runnable
接口的类,并定义其run()
方法。然后可以使用Thread
类的构造函数来创建线程对象,并将Runnable
对象作为参数传递给线程。 - 实现Callable接口(结合FutureTask和ExecutorService):Callable接口类似于Runnable,但它可以返回一个结果,并且可以抛出一个异常。要实现它,你通常需要与FutureTask一起使用,将FutureTask提交给ExecutorService来执行。
- 使用ExecutorService框架 :虽然这不是直接创建线程的方法,但它是Java并发编程中常用的高级抽象,用于管理线程池。通过Executors工厂类可以方便地创建不同类型的线程池,然后提交Runnable或Callable任务给线程池执行。这种方式使得线程的管理(如线程的创建、调度、销毁等)更加简单和高效。
- 继承
-
Java内存模型(JMM):
- Java内存模型定义了Java程序中各种变量(线程共享变量)的访问规则。它规定了在多线程环境中,变量的读写操作如何与内存中的变量进行同步,确保了多线程程序的内存一致性。
-
线程局部变量(ThreadLocal):
- 线程局部变量是每个线程独有的变量,它为每个使用该变量的线程提供了一个独立的副本。使用
ThreadLocal
类可以创建线程局部变量。 - 何时使用:当需要在线程之间隔离数据时,比如用户会话信息、数据库连接等。
- 线程局部变量是每个线程独有的变量,它为每个使用该变量的线程提供了一个独立的副本。使用
-
死锁:
- 死锁是指两个或多个线程因为互相等待对方持有的资源而无法继续执行的状态。
- 预防方法:避免循环等待、使用超时锁定、使用顺序锁定等。
-
活锁:
- 活锁是指两个或多个线程在尝试获取资源时,不断改变策略,导致它们无法取得进展的状态。
- 与死锁不同:死锁是完全停滞,而活锁是看似在运行但实际上没有进展。
-
线程饥饿:
- 线程饥饿是指某些线程因为优先级低或其他线程持续占用资源,导致它们长时间无法获得执行机会。
- 避免方法:合理设置线程优先级,避免长时间占用资源,使用公平锁等。
-
线程优先级:
- 线程优先级是Java中用于控制线程调度顺序的机制。优先级高的线程更有可能被优先执行。
- 设置和影响:可以通过
Thread
类的setPriority(int)
方法设置线程优先级。优先级范围是1到10。
-
synchronized关键字:
synchronized
关键字用于实现线程同步,确保多个线程在访问共享资源时的互斥性。它可以修饰方法或代码块。
-
volatile关键字:
volatile
关键字用于确保变量的读写操作对所有线程都是可见的,即一个线程修改了变量后,其他线程能够立即看到这个变化。
-
final关键字在并发中的作用:
final
关键字可以确保变量一旦被初始化后,其值不会被改变。在并发环境中,使用final
可以避免一些不必要的同步。
-
原子操作:
- 原子操作是指在执行过程中不会被其他线程中断的操作。Java中可以通过
synchronized
关键字或使用java.util.concurrent
包中的原子类(如AtomicInteger
)来实现原子操作。
- 原子操作是指在执行过程中不会被其他线程中断的操作。Java中可以通过
高级进阶
-
解释一下AbstractQueuedSynchronizer(AQS):
- AQS是一个用于构建锁和其他同步器的框架,它使用一个int成员变量来表示同步状态。
- 它通过内置的FIFO队列来管理线程的等待状态,支持独占和共享两种模式。
- AQS提供了模板方法,如
tryAcquire
和tryRelease
,供子类实现具体的获取和释放逻辑。
-
CAS操作(Compare and Swap):
- CAS是一种原子操作,用于实现无锁的同步机制。
- 它包含三个参数:内存地址(V)、预期值(A)和新值(B)。
- 操作的基本思想是:如果内存地址V的当前值等于预期值A,则将V的值更新为新值B,否则不做任何操作。
- 通过硬件指令实现,保证了操作的原子性。
-
synchronized底层原理:
synchronized
关键字在Java中用于实现同步,它可以通过对象锁和类锁来控制线程的访问。- 在JVM层面,
synchronized
通过监视器(Monitor)来实现,每个对象都有一个监视器。 - 当线程进入同步代码块时,会尝试获取监视器的锁,获取成功则继续执行,失败则阻塞等待。
-
Java锁的升级过程:
- 偏向锁:初始阶段,锁偏向第一个获取它的线程,后续该线程再次获取锁时无需额外操作。
- 轻量级锁:当锁被其他线程获取时,使用轻量级锁,通过CAS操作尝试获取锁。
- 重量级锁:如果轻量级锁失败,会升级为重量级锁,此时会阻塞线程,进入阻塞状态。
-
StampedLock与ReadWriteLock的比较:
- ReadWriteLock:允许多个读线程同时访问,但写线程是独占的。
- StampedLock:提供了更细粒度的控制,支持乐观读和悲观读,写锁同样是独占的。
- StampedLock通过版本号来避免ABA问题,读写锁则依赖于锁的重入。
-
Java 8中并行流(Parallel Streams)的工作原理:
- 并行流是Java 8引入的,允许将数据流的处理并行化,以提高处理效率。
- 它通过将数据源分割成多个部分,然后在不同的线程中并行处理这些部分。
- 并行流的执行依赖于Fork/Join框架,该框架将任务分解为更小的任务,递归地并行执行,最后合并结果。
-
start和run的区别:
- start()方法:用于启动线程,它会使线程进入就绪状态(Runnable),等待CPU的调度执行。当线程获得CPU时间片时,会自动调用该线程的run()方法。只能被调用一次。如果尝试多次调用同一个线程的start()方法,会抛出IllegalThreadStateException异常。调用后,线程进入就绪状态,等待CPU调度执行,并不保证立即执行run()方法中的代码。
- run()方法:是Thread类的一个方法,也称为线程体,包含线程要执行的具体任务代码。但直接调用run()方法并不会启动新线程,而是在当前线程中顺序执行run()方法中的代码。可以被多次调用,每次调用都会执行run()方法中的代码,但请注意,这并不是并发执行,而是在当前线程中顺序执行。如果当前线程的时间片没有用完,调用run()方法会立即执行其中的代码。
-
notify() 和 notifyall() 有什么区别:
- notifyAll:唤醒所有的 wait 的线程
- notify:只随机唤醒一个 wait 线程
-
sleep() 和 wait() 有什么区别:
- 共同:都能让当前线程暂停使用 cpu 的执行权,进入阻塞状态
- sleep()方法是Thread类的一个静态方法。 它使当前正在执行的线程暂停执行一段时间(以毫秒为单位),让出CPU给其他线程。sleep()的签名是public static void sleep(long millis) throws InterruptedException。
wait()方法是Object类的一个实例方法。 它用于让当前线程等待直到另一个线程调用此对象的notify()方法或notifyAll()方法。这意味着wait()必须在同步方法或同步块中被调用。wait()的签名是public final void wait() throws InterruptedException,它还有另外两个重载版本,允许指定等待时间和纳秒级时间。 - sleep() 方法不释放当前线程持有的锁(如果有的话)。它仅仅是让线程暂停执行,不会影响到其他线程对共享资源的访问。
wait() 方法会释放当前线程持有的锁,使其他线程能够访问该对象。当线程被唤醒(通过notify()或notifyAll())并重新获得锁后,它才能继续执行。 - sleep() 方法会按照指定的时间自动醒来,或者通过中断来提前结束睡眠。
wait() 方法必须等待其他线程调用同一对象的notify()或notifyAll()方法才能醒来,或者通过中断来提前结束等待。
-
为什么使用线程池
-
线程池核心参数
并发集合
-
ConcurrentHashMap与HashTable的区别:
- 线程安全:ConcurrentHashMap是线程安全的,而HashTable也是线程安全的,但实现方式不同。
- 锁策略:ConcurrentHashMap通过分段锁(Segment)来减少锁竞争,HashTable则使用全局锁。
- 性能:ConcurrentHashMap在高并发场景下性能更好,因为它减少了锁竞争。
- 迭代器:ConcurrentHashMap的迭代器是弱一致的,而HashTable的迭代器是强一致的。
-
Vector与ArrayList在多线程环境下的表现:
- Vector:是同步的,即其方法被synchronized关键字修饰,适合多线程环境,但性能较低。
- ArrayList:不是线程安全的,如果在多线程环境下使用需要手动同步,例如使用Collections.synchronizedList。
-
BlockingQueue的典型使用场景:
- 用于生产者-消费者模型,其中生产者将元素添加到队列,消费者从队列中移除元素。
- 常用于任务调度和负载均衡,例如在多线程环境中分配任务给不同的线程。
-
CopyOnWriteArrayList的内部实现:
- 它通过在每次修改操作时复制底层数组来实现线程安全。
- 读操作是无锁的,写操作需要加锁,并且复制当前数组。
- 适用于读多写少的场景,因为写操作成本较高。
-
ConcurrentLinkedQueue的原理:
- 它是一个基于链表的无界线程安全队列,支持高并发。
- 内部使用节点(Node)来存储数据,每个节点包含数据和指向下一个节点的指针。
- 通过CAS操作来实现节点的添加和移除,确保操作的原子性。
锁与同步
-
ReentrantLock的公平性和非公平性:
- 公平性:公平锁会按照线程请求锁的顺序来分配锁,即先到先得。
- 非公平性:非公平锁则不保证线程获取锁的顺序,可能会优先给等待时间较短的线程。
- 公平锁在多线程竞争较少时性能较好,但在高并发场景下可能会导致饥饿问题。
-
如何使用LockSupport暂停和恢复线程:
LockSupport
提供了一些静态方法来控制线程的阻塞和唤醒。LockSupport.park()
:使当前线程暂停,直到被其他线程通过LockSupport.unpark(Thread t)
唤醒。LockSupport.unpark(Thread t)
:唤醒指定的线程,使其从park()
状态恢复。
-
解释StampedLock的用途:
StampedLock
是一种新型的锁,提供了乐观读和悲观读的机制。- 它通过版本号来避免ABA问题,同时允许多个读线程同时访问资源。
- 适用于读操作多于写操作的场景,可以提高并发性能。
-
解释Thread.interrupt()的作用:
Thread.interrupt()
用于中断一个线程,设置线程的中断状态。- 当线程处于阻塞状态(如等待、睡眠)时,中断会触发中断异常(InterruptedException),从而可以响应中断。
- 它是一种协作机制,需要被中断的线程检查并响应中断状态。
-
如何在多线程环境下安全地使用StringBuilder和StringBuffer:
- StringBuilder:在单线程环境下性能更好,因为它不是线程安全的。
- StringBuffer:是线程安全的,通过内部同步机制来保证并发访问的安全性。
- 在多线程环境下,如果需要频繁修改字符串,建议使用
StringBuffer
来避免线程安全问题。 - 如果使用
StringBuilder
,则需要手动同步访问,例如使用synchronized
关键字。 StringBuffer
的所有公开方法都被声明为synchronized
所以它是线程安全的
核心工具
-
ExecutorService和ThreadPoolExecutor的区别:
- ThreadPoolExecutor:是一个具体的线程池执行器,提供了丰富的配置选项,如核心线程数、最大线程数、工作队列等。
- ExecutorService:是一个接口,ThreadPoolExecutor是其实现之一。ExecutorService提供了更高层次的抽象,简化了任务的提交和执行。
-
Future与CompletableFuture的使用场景:
- Future:表示异步计算的结果,可以查询计算是否完成,获取结果或取消任务。
- CompletableFuture:是对Future的扩展,提供了更多功能,如组合多个Future的结果、处理异常、转换结果等。适用于复杂的异步编程场景。
-
Callable和Runnable接口的主要区别:
- Runnable:没有返回值,只能通过传递给run方法的参数来传递结果。
- Callable:可以返回结果,并且可以抛出异常。通常用于需要返回值或处理异常的异步任务。
-
CountDownLatch、CyclicBarrier、Semaphore各自的用途:
- CountDownLatch:用于一个或多个线程等待一组事件发生。
- CyclicBarrier:允许一组线程相互等待,直到所有线程都到达一个公共屏障点。
- Semaphore:用于控制同时访问某个特定资源的线程数量。
-
ReentrantLock和synchronized关键字的比较:
- ReentrantLock:是一个可重入锁,提供了比synchronized更灵活的锁机制,如尝试非阻塞获取锁、可中断的锁获取等。
- synchronized:是Java内置的同步机制,使用简单,但功能有限,不支持中断和超时。
-
ReadWriteLock的实现和应用场景:
- ReadWriteLock:允许多个读线程同时访问资源,但写线程是独占的。适用于读多写少的场景,可以提高并发性能。
-
Atomic类(如AtomicInteger)如何实现原子操作:
- AtomicInteger:利用CAS(Compare-And-Swap)操作来保证操作的原子性。
- 通过硬件支持的原子指令,避免使用锁,提高了性能。
-
ConcurrentHashMap的工作原理:
- ConcurrentHashMap:通过分段锁(Segment)来减少锁竞争,每个Segment维护一部分数据。
- 支持并发访问和修改,适用于高并发场景。
-
BlockingQueue的实现有哪些?分别在什么场景下使用?
- ArrayBlockingQueue:基于数组的有界队列,适用于固定大小的缓冲区。
- LinkedBlockingQueue:基于链表的无界队列,适用于动态扩展的缓冲区。
- PriorityBlockingQueue:基于优先级的无界队列,适用于需要按优先级处理任务的场景。
- SynchronousQueue:不存储元素,每个插入操作必须等待一个移除操作,适用于线程间直接传递数据。
-
CopyOnWriteArrayList和Vector的区别:
- CopyOnWriteArrayList:在每次修改操作时复制底层数组,读操作是无锁的,适合读多写少的场景。
- Vector:是同步的,适用于需要线程安全且读写操作频繁的场景。
设计模式
-
如何实现线程安全的单例模式:
- 饿汉式:在类加载时就初始化单例对象,天然线程安全。
public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } }
- 懒汉式:第一次调用
getInstance
时才创建实例,需要同步方法或代码块。public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
- 双重检查锁定:减少同步的开销,只在实例化时同步。
public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
- 饿汉式:在类加载时就初始化单例对象,天然线程安全。
-
解释双重检查锁定(Double-Checked Locking):
- 双重检查锁定是一种优化单例实现的技术,减少同步的开销。
- 首先检查实例是否存在,如果不存在再进入同步块进行实例化。
- 使用
volatile
关键字确保实例引用的可见性。 - 适用于延迟初始化且需要线程安全的场景。
-
如何在多线程环境中使用工厂模式:
- 工厂模式可以用于创建线程安全的实例,通过同步方法或代码块来确保线程安全。
- 可以使用单例模式的实现方式来确保工厂类本身是线程安全的。
- 也可以使用线程安全的集合类(如
ConcurrentHashMap
)来存储和管理实例。
-
解释生产者-消费者模式:
- 生产者-消费者模式是一种并发编程模式,用于处理多个线程之间的数据交换。
- 生产者线程生成数据,消费者线程消费数据。
- 使用阻塞队列(如
BlockingQueue
)来协调生产者和消费者之间的数据传递。 - 可以处理并发访问和数据同步的问题。
-
如何在并发环境中实现观察者模式:
- 观察者模式是一种设计模式,用于实现对象间的一对多依赖关系。
- 在并发环境中,需要确保观察者和被观察者之间的交互是线程安全的。
- 可以使用同步方法或代码块来保护数据的访问和修改。
- 也可以使用并发集合类(如
CopyOnWriteArrayList
)来存储观察者列表,提高并发访问的性能。
框架应用
-
Spring框架中@Async注解的使用:
@Async
注解用于在Spring应用程序中创建异步方法。- 它需要在方法上使用,并在Spring配置中定义一个
Async
注解的执行器(Executor)。 - 调用异步方法时,Spring会将任务提交到一个专门的线程池中执行,从而不阻塞调用线程。
-
如何在Spring中配置TaskExecutor:
- 在Spring配置中,可以使用
<task:executor>
或@Bean
方法定义一个TaskExecutor
。 - 可以配置线程池的大小、队列容量等参数,以满足不同的并发需求。
- 例如:
@Bean public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(2); executor.setQueueCapacity(500); executor.setThreadNamePrefix("TaskExecutor-"); return executor; }
- 在Spring配置中,可以使用
-
Quartz Scheduler的主要特点:
- Quartz是一个开源的作业调度库,用于在Java应用程序中管理定时任务。
- 支持复杂的调度策略,如cron表达式。
- 可以持久化作业和触发器,确保在系统重启后任务可以恢复执行。
- 提供了丰富的监听器和插件,可以扩展其功能。
-
Akka框架中Actor模型的实现:
- Akka是一个构建并发、分布式和容错应用程序的工具包和运行时。
- 它基于Actor模型,每个Actor是一个并发执行的实体,拥有自己的状态和行为。
- Actor之间通过消息传递进行通信,避免了共享状态和同步问题。
- Akka提供了高度可扩展和容错的系统架构。
-
Hazelcast的分布式锁实现:
- Hazelcast是一个分布式内存数据结构和计算平台。
- 它提供了分布式锁,可以在集群中的多个节点上同步访问共享资源。
- Hazelcast的分布式锁基于其内部的原子操作和一致性协议,确保锁的一致性和可靠性。
- 适用于需要跨多个节点管理资源访问的场景。
并发实现细节
- JAVA如何实现一个线程池?
在Java中,可以通过实现java.util.concurrent
包中的ExecutorService
接口来创建一个线程池。以下是实现线程池的基本步骤:
- 创建线程池:使用Executors
类提供的静态工厂方法来创建不同类型的线程池,如 newFixedThreadPool
、newCachedThreadPool
、newSingleThreadExecutor
等。
- 提交任务:使用execute(Runnable command)
方法提交任务到线程池中。
- 管理线程池:使用shutdown()
方法来关闭线程池,等待正在执行的任务完成。shutdownNow()
方法可以尝试停止所有正在执行的任务并返回等待执行的任务列表。
示例代码:
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new Runnable() {
public void run() {
// 任务代码
}
});
executorService.shutdown();
-
如何设计一个高性能的并发数据结构?
- 无锁设计:尽可能使用无锁编程技术,如原子变量(
AtomicInteger
、AtomicLong
等)。 - 分段锁:将数据结构分成多个段,每个段使用不同的锁,以减少锁的竞争。
- 读写分离:使用读写锁(
ReadWriteLock
)来允许多个读操作同时进行,但写操作是排他的。 - 使用并发集合:Java提供了多种线程安全的并发集合,如
ConcurrentHashMap
、ConcurrentLinkedQueue
等。 - 减少共享:尽量减少共享状态,使用局部变量或线程局部存储(
ThreadLocal
)。
- 无锁设计:尽可能使用无锁编程技术,如原子变量(
-
如何实现一个线程安全的发布/订阅模式?
- 使用线程安全的数据结构:存储订阅者列表和发布的消息。
- 同步发布操作:在发布消息时使用同步代码块或锁来确保线程安全。
- 异步通知订阅者:使用线程池来异步处理订阅者的通知,避免阻塞发布者线程。
- 使用并发工具:利用Java的并发工具,如
BlockingQueue
、ExecutorService
等。
-
如何在并发环境中实现一个线程安全的工厂模式?
- 同步方法:在工厂方法上使用
synchronized
关键字或同步代码块来确保线程安全。 - 使用静态内部类:利用Java的类加载机制,每个静态内部类只会加载一次,可以避免多线程环境下的实例化问题。
- 使用单例模式:如果工厂类是单例的,确保单例的实现是线程安全的,如使用双重检查锁定模式。
- 同步方法:在工厂方法上使用
-
如何设计一个线程安全的装饰模式?
- 使用不可变对象:确保被装饰的对象是不可变的,这样在多线程环境下就不会有状态改变的问题。
- 同步装饰者:在装饰者类的方法上使用
synchronized
关键字或同步代码块来确保线程安全。 - 使用线程安全的数据结构:如果装饰者需要持有状态,使用线程安全的数据结构来存储这些状态。
并发编程的实践
-
如何在实际项目中使用CompletableFuture进行异步处理:
CompletableFuture
是Java 8引入的,用于异步编程。- 通过
supplyAsync
方法可以异步执行任务,并返回一个CompletableFuture
对象。 - 使用
thenApply
、thenAccept
等方法链式处理异步任务的结果。 - 可以组合多个
CompletableFuture
,处理复杂的异步逻辑。
-
如何在Web应用中使用ExecutorService:
ExecutorService
是Java线程池的接口,用于管理线程池。- 在Web应用中,可以通过
ExecutorService
来异步处理耗时任务,避免阻塞主线程。 - 例如,可以使用
Executors.newFixedThreadPool
创建一个固定大小的线程池,处理后台任务。 - 需要确保在应用关闭时,正确地关闭线程池,释放资源。
-
如何在数据库连接池中处理并发:
- 数据库连接池(如HikariCP、Apache DBCP)管理数据库连接,提高并发访问数据库的效率。
- 连接池通过限制最大连接数和最小空闲连接数来控制资源使用。
- 使用连接池时,需要确保连接的及时释放和回收,避免资源泄露。
- 可以通过配置连接池参数(如超时时间、验证查询)来优化性能和稳定性。
-
如何在消息队列中处理并发消息消费:
- 消息队列(如RabbitMQ、Kafka)常用于实现异步消息传递和解耦。
- 可以配置多个消费者实例并行消费消息,提高消息处理的吞吐量。
- 使用消息队列的消费者API来订阅队列,并处理接收到的消息。
- 需要考虑消息的幂等性、顺序性以及异常处理机制,确保消息的可靠传递。
-
如何在分布式系统中实现会话管理:
- 分布式会话管理是分布式系统中的一个挑战,需要在多个节点间共享用户会话信息。
- 可以使用集中式会话存储(如Redis)来存储会话数据,所有节点通过访问存储来获取会话信息。
- 也可以使用粘性会话(Sticky Sessions),将用户的请求定向到同一个节点,但需要考虑单点故障的问题。
- 需要考虑会话数据的一致性、可用性和安全性,确保系统的稳定性和可靠性。
性能调试与优化
-
如何使用JFR(Java Flight Recorder)进行性能分析:
- JFR是Java的一个性能分析工具,可以记录应用程序的运行时行为。
- 通过JFR,可以收集关于方法调用、异常、锁争用等的详细数据。
- 使用
jcmd
命令或Java Mission Control工具来启动和控制JFR的记录。
-
VisualVM在监控线程状态中的应用:
- VisualVM是一个多合一的Java性能分析工具,可以监控应用程序的线程状态。
- 它提供了线程活动视图,显示线程的运行状态、锁争用情况等。
- 可以用于识别死锁、线程阻塞和线程效率低下的问题。
-
如何使用ThreadMXBean:
ThreadMXBean
是一个管理接口,提供了线程和线程系统的操作和信息。- 可以通过MBeans或直接编程方式访问
ThreadMXBean
,获取线程的堆栈跟踪、锁信息等。 - 常用于监控和诊断多线程应用程序的性能问题。
-
如何使用JConsole或jstack诊断线程死锁:
- JConsole是一个图形界面的监控工具,可以连接到运行中的Java应用程序。
- 通过JConsole的线程视图,可以查看线程的状态和堆栈跟踪,识别死锁。
jstack
是一个命令行工具,可以生成线程的堆栈跟踪,帮助分析死锁。
-
如何在多线程环境中正确处理异常:
- 在多线程中,异常处理尤为重要,因为异常可能影响线程的执行。
- 需要确保每个线程都有适当的异常处理机制,避免线程因未捕获的异常而意外终止。
- 可以考虑使用
try-catch
块或Future
的get()
方法来处理异常。
-
如何在并发程序中识别性能瓶颈:
- 性能瓶颈通常表现为资源争用、线程阻塞或过多的上下文切换。
- 可以使用性能分析工具(如JProfiler、JVM自带的JFR)来识别瓶颈。
- 监控CPU、内存和I/O的使用情况,找出资源消耗最大的部分。
-
如何优化多线程程序的性能:
- 优化线程池的大小,避免过多的线程创建和销毁。
- 减少锁的使用,使用无锁数据结构或减少锁的粒度。
- 使用并发集合类(如
ConcurrentHashMap
)来提高并发访问的性能。 - 避免在高并发的代码中使用同步块,考虑使用
volatile
或原子类。
-
如何在多线程环境中避免过多的上下文切换:
- 上下文切换通常发生在线程切换时,减少线程的数量和切换频率可以降低上下文切换。
- 使用线程池管理线程,避免频繁创建和销毁线程。
- 优化锁的使用,减少锁争用和锁的持有时间。
-
如何在高并发环境下实现缓存策略:
- 使用缓存可以减少对数据库或外部资源的访问,提高系统响应速度。
- 可以使用本地缓存(如
ConcurrentHashMap
)或分布式缓存(如Redis)。 - 考虑缓存失效策略,如LRU(最近最少使用)、TTL(时间到期)等。
-
如何在并发程序中实现负载均衡:
- 负载均衡可以通过分散请求到多个服务器或线程来实现。
- 在分布式系统中,可以使用负载均衡器(如Nginx)来分配请求。
- 在多线程环境中,可以使用任务队列和线程池来均衡任务的分配。
并发与分布式系统
-
如何在分布式系统中实现数据一致性?
- 强一致性:通过同步复制数据,确保所有节点上的数据在任何时间点都是一致的。这可以通过两阶段提交协议(2PC)或三阶段提交协议(3PC)来实现。
- 最终一致性:允许系统在短暂的时间内不一致,但最终会达到一致状态。这可以通过版本控制、向量时钟等技术实现。
- 分布式事务:使用分布式事务协议如Paxos或Raft来保证跨多个节点的事务操作的一致性。
- 数据一致性协议:如CAP定理,选择一致性(C)和可用性(A)之间的平衡点,根据应用需求决定。
- 数据复制:通过数据复制策略(如主从复制、多主复制)来提高数据的一致性和可用性。
-
解释如何在分布式系统中实现数据分区。
- 哈希分区:通过哈希函数将数据均匀地分布到不同的节点上。
- 范围分区:根据数据的范围将数据分配到不同的节点上。
- 列表分区:将数据项分配到不同的节点上,每个节点负责维护一个数据项列表。
- 一致性哈希:通过一致性哈希算法将数据分布到节点上,减少数据迁移和重新分布的成本。
- 分片:将数据表分成多个片,每个片存储在不同的节点上,通过分片键来确定数据存储位置。
-
如何在分布式系统中实现负载均衡?
- 轮询:将请求按顺序分配到每个节点上。
- 随机分配:随机选择一个节点来处理请求。
- 最少连接:将请求分配给当前连接数最少的节点。
- 权重分配:根据节点的处理能力或权重来分配请求。
- 地理位置:根据用户的地理位置将请求分配到最近的节点上。
- 负载均衡器:使用专门的负载均衡器设备或软件来实现请求的分配。
-
如何在分布式系统中实现故障恢复?
- 冗余:通过在多个节点上复制数据和应用来提高系统的容错能力。
- 故障检测:定期检查节点的状态,及时发现故障。
- 自动切换:当检测到故障时,自动将请求切换到健康的节点上。
- 数据备份和恢复:定期备份数据,并在发生故障时恢复数据。
- 故障模拟:通过模拟故障来测试系统的恢复能力。
-
如何在分布式系统中实现容错机制?
- 冗余设计:通过增加额外的硬件或软件组件来提供备用资源。
- 错误检测和纠正:使用错误检测和纠正码(如CRC、RAID)来检测和纠正数据错误。
- 故障转移:当一个节点发生故障时,自动将任务转移到其他节点上。
- 数据复制:通过数据复制来确保数据的持久性和一致性。
- 服务冗余:在多个节点上运行相同的服务,确保一个服务节点发生故障时,其他节点可以接管其任务。
错误处理与案例分析
-
如何在多线程环境中处理异常?
- 捕获异常:在每个线程的
run
方法中捕获并处理可能发生的异常。 - 使用异常处理器:实现
Thread.UncaughtExceptionHandler
接口,为线程设置异常处理器。 - 集中处理:将异常信息发送到日志系统或监控系统,进行集中处理。
- 捕获异常:在每个线程的
-
解释如何避免线程间的错误传播。
- 局部化异常:确保异常不会从线程传播到其他线程。
- 使用隔离机制:如使用隔离的线程池,避免一个线程的异常影响到其他线程。
- 错误恢复:设计错误恢复机制,如重试或回退到安全状态。
-
如何在并发程序中实现重试机制?
- 重试策略:定义重试策略,如固定次数重试或指数退避重试。
- 使用重试库:利用现有的重试库,如Spring Retry或Guava Retry。
- 线程安全:确保重试机制在多线程环境下是线程安全的。
-
如何在多线程环境中处理失败的事务?
- 事务管理器:使用事务管理器来协调事务的提交和回滚。
- 隔离级别:设置适当的事务隔离级别,减少事务冲突。
- 超时机制:设置事务超时时间,避免事务长时间占用资源。
-
如何在并发环境中实现回滚和补偿机制?
- 事务日志:记录事务的每一步操作,以便在失败时进行回滚。
- 补偿操作:设计补偿操作,当事务失败时执行这些操作来恢复到原始状态。
- 分布式事务:使用两阶段提交或三阶段提交协议来处理分布式事务的回滚和补偿。
-
如何设计一个线程安全的缓存?
- 使用并发集合:如
ConcurrentHashMap
,提供线程安全的缓存实现。 - 锁机制:使用读写锁来控制缓存的读写操作。
- 原子操作:使用原子操作来更新缓存内容。
- 使用并发集合:如
-
如何设计一个高并发的计数器?
- 使用原子类:如
AtomicInteger
或LongAdder
,提供无锁的并发计数器。 - 分段锁:将计数器分成多个段,每个段使用不同的锁。
- 乐观锁:使用CAS(比较并交换)操作来实现无锁的计数器。
- 使用原子类:如
-
如何设计一个线程安全的队列?
- 使用并发队列:如
ConcurrentLinkedQueue
或BlockingQueue
,提供线程安全的队列实现。 - 锁机制:使用锁来控制队列的入队和出队操作。
- 无锁设计:利用原子操作和CAS来实现无锁的队列。
- 使用并发队列:如
-
如何在并发环境下实现一个线程安全的发布/订阅模式?
- 使用线程安全的数据结构:存储订阅者和消息。
- 同步通知:在发布消息时使用同步代码块或锁来确保线程安全。
- 异步处理:使用线程池来异步处理订阅者的通知。
-
如何设计一个线程安全的代理模式?
- 同步代理方法:在代理类的方法上使用
synchronized
关键字或同步代码块。 - 使用并发工具:利用Java的并发工具,如
ExecutorService
,来管理并发访问。 - 代理对象隔离:每个线程使用独立的代理对象,避免共享状态。
- 同步代理方法:在代理类的方法上使用