java学习杂记
什么是上下文
所谓的上下文,就是使用它来存储一些(初始化的)信息。 例如Spring的ApplicationContext就是作为IOC容器存储Bean、请求上下文RequestContext就是存储客户端请求服务器时传来的一些信息。 项目启动,就可以将配置文件中的数据加载存储到context中。
ThreadLocal
ThreadLocal 是 Java 中提供的一种线程绑定机制,用于在多线程环境下为每个线程提供独立的实例副本。不同线程之间的数据相互隔离,互不干扰。ThreadLocal 的核心思想是将值存储在每个线程的本地存储中,从而实现线程安全。
为什么需要ThreadLocal
在多线程环境中,实现线程安全通常需要用到同步机制,例如sychronized、lock,同步操作可能会导致性能问题或者复杂的代码逻辑。ThreadLocal提供了一种替代方案,通过给每个线程提供一个独立的变量副本,避免了线程间的共享竞争,从而简化了线程安全的实现。
工作原理
ThreadLocal 的实现基于每个线程的本地存储(ThreadLocalMap)。当一个线程调用 ThreadLocal 的 set 或 get 方法时,实际上是在当前线程的 ThreadLocalMap 中存储或检索值。ThreadLocalMap 是一个定制的哈希表,使用弱引用(WeakReference)来存储键值对。
ThreadLocalMap 的作用:
每个线程有一个独立的 ThreadLocalMap 实例。
ThreadLocal 对象作为键(ThreadLocal 子类实例),线程特定的值作为值存储。
弱引用机制:
ThreadLocal 的键使用弱引用,这意味着如果 ThreadLocal 对象被垃圾回收,其对应的键值对也会被清除,避免内存泄漏。
核心方法
- set(Object value)
将值绑定到当前线程的 ThreadLocal 实例中。如果当前线程之前没有设置过值,ThreadLocalMap 会自动创建一个新的条目。 - get()
从当前线程的 ThreadLocal 实例中获取值。如果当前线程尚未设置值,get 方法会调用 initialValue 方法(默认返回 null)。 - remove()
从当前线程的 ThreadLocal 实例中移除值。这有助于减少内存泄漏的风险。 - initialValue()
可重写的方法,用于设置 ThreadLocal 的初始值,默认返回 null。
使用示例
- 基本用法
public class ThreadLocalExample {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 线程 1
Thread t1 = new Thread(() -> {
threadLocal.set("Thread 1");
System.out.println("Thread 1: " + threadLocal.get());
threadLocal.remove();
});
// 线程 2
Thread t2 = new Thread(() -> {
threadLocal.set("Thread 2");
System.out.println("Thread 2: " + threadLocal.get());
threadLocal.remove();
});
t1.start();
t2.start();
}
}
输出:
Thread 1: Thread 1
Thread 2: Thread 2
- 初始值设置
public class ThreadLocalWithInitialValue {
private static ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "Default Value";
}
};
public static void main(String[] args) {
Thread t1 = new Thread(() -> System.out.println("Thread 1: " + threadLocal.get()));
Thread t2 = new Thread(() -> System.out.println("Thread 2: " + threadLocal.get()));
t1.start();
t2.start();
}
}
输出:
Thread 1: Default Value
Thread 2: Default Value
内存泄漏问题
- 为什么会发生内存泄漏?
ThreadLocal 的键使用弱引用,而值使用强引用。
如果 ThreadLocal 对象被回收(键为 null),但值未被清理,ThreadLocalMap 中的条目会残留,导致值被永久保留,无法被回收,从而引发内存泄漏。 - 如何避免内存泄漏?
严格遵循 try-finally 原则,确保在使用完 ThreadLocal 后调用 remove() 方法清理值。
在线程池环境中,特别注意清理 ThreadLocal 的值,防止线程复用导致的残留问题。
应用场景
- 线程上下文管理
在多线程环境中,用于存储线程相关的上下文信息(如事务 ID、用户 ID、请求链路追踪 ID 等)。 - 日志记录
在日志记录中,通过 ThreadLocal 存储线程相关的日志上下文(如请求 ID、用户信息等),便于日志分析和追踪。 - 数据库连接池
在某些情况下,可以使用 ThreadLocal 将数据库连接绑定到线程,确保线程安全。 - 避免频繁创建对象
在性能敏感的代码中,通过 ThreadLocal 避免多次创建和销毁临时对象,减少开销。
ThreadLocalMap 深入
- 数据结构
ThreadLocalMap 是一个定制化的哈希表,用于存储 ThreadLocal 对象及其对应的值。它的键是 ThreadLocal 子类的实例,值是线程特定的值。 - 弱引用
ThreadLocal 的键使用弱引用,是因为 ThreadLocal 对象可能在某些情况下被回收,而值需要保留到线程结束。
弱引用允许垃圾回收器在必要时回收 ThreadLocal 对象,从而避免内存泄漏。 - 哈希冲突处理
ThreadLocalMap 使用线性探测法来解决哈希冲突。如果发生冲突,新键值对会存储到下一个可用的位置。
九、InheritableThreadLocal
InheritableThreadLocal 是 ThreadLocal 的子类,它允许子线程继承父线程的 ThreadLocal 值。子线程启动时会复制父线程的 ThreadLocal 值,但子线程对值的修改不会影响父线程。
示例
public class InheritableThreadLocalExample {
private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
inheritableThreadLocal.set("Main Thread");
Thread t1 = new Thread(() -> {
System.out.println("Thread 1: " + inheritableThreadLocal.get());
inheritableThreadLocal.set("Thread 1");
});
t1.start();
}
}
输出:
Thread 1: Main Thread
为什么一个线程会有多个 ThreadLocal 实例?
不同的 ThreadLocal 变量:
在实际应用中,一个线程可能需要存储多个线程本地变量(例如,用户 ID、事务 ID、请求上下文等)。每个变量都可以通过一个独立的 ThreadLocal 实例来管理。
例如:
ThreadLocal<String> userThreadLocal = new ThreadLocal<>();
ThreadLocal<Integer> transactionThreadLocal = new ThreadLocal<>();
在这种情况下,线程需要同时存储 userThreadLocal 和 transactionThreadLocal 的值。
线程本地存储的隔离性:
每个 ThreadLocal 实例为线程提供独立的存储空间。即使多个 ThreadLocal 实例存在,它们的值也不会互相干扰。
例如,线程 A 可能设置了 userThreadLocal 和 transactionThreadLocal 的值,而线程 B 设置了不同的值,彼此独立。
总结
ThreadLocal 提供了一种简单而有效的线程隔离机制,适用于多线程环境下需要避免共享数据竞争的场景。然而,使用时需要注意内存泄漏问题,并确保在不再需要时清理值。
jedis
Jedis 简介
Jedis 是 Redis 官方推荐的 Java 客户端,用于与 Redis 数据库进行交互。它提供了一系列简单易用的 API,使得在 Java 应用程序中使用 Redis 变得非常方便。
Jedis 的特点
易于使用:Jedis 提供了简单直接的 API,使得在 Java 中操作 Redis 变得非常方便。
性能高效:Jedis 旨在提高性能,与 Redis 2.8.x、3.x.x 及更高版本完全兼容。
功能全面:Jedis 支持大部分 Redis 命令,包括字符串、列表、集合、有序集合、哈希表等。
连接池支持:Jedis 支持连接池,可以有效管理 Redis 连接,提高性能。
Jedis 的基本使用
添加 Maven 依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.0</version>
</dependency>
连接 Redis:
Jedis jedis = new Jedis("localhost", 6379);
jedis.set("key", "value");
String value = jedis.get("key");
System.out.println(value);
jedis.close();
使用连接池:
JedisPool pool = new JedisPool("localhost", 6379);
try (Jedis jedis = pool.getResource()) {
jedis.set("key", "value");
}
Jedis 的高级功能
支持 Redis 集群:Jedis 支持连接到 Redis 集群,可以使用 JedisCluster 类。
支持事务:Jedis 支持 Redis 事务,可以使用 jedis.multi() 和 jedis.exec() 方法。
支持管道:Jedis 支持管道操作,可以减少网络延迟,提高性能。
Jedis 的注意事项
资源管理:使用 Jedis 时,务必确保在操作完成后关闭连接或归还连接到连接池,以避免资源泄漏。
序列化:Jedis 本身不支持序列化,可以使用第三方序列化工具(如 Protostuff、Jackson 等)进行对象的序列化和反序列化。
版本兼容性:Jedis 与 Redis 的版本兼容性需要注意,不同版本的 Jedis 可能支持不同的 Redis 版本。
Jedis 与其他 Redis 客户端的比较
Lettuce:Lettuce 是一个高级 Redis 客户端,支持异步和响应式编程,适合高并发场景。
Redisson:Redisson 提供了分布式和可扩展的 Java 数据结构,支持分布式锁、分布式集合等高级功能。
异步和同步
异步(Asynchronous)和同步(Synchronous)是计算机编程和系统架构中用于描述任务执行方式的两个重要概念。
同步
任务按顺序执行,只有前一个任务完成,后一个任务才能开始。在同步操作中,调用方会等待被调用方完成任务并返回结果,整个流程是线性的,直到所有任务都按顺序完成。
原理: 在同步模式下,任务执行的流程是严格按顺序进行的。例如,假设有一个简单的同步函数调用序列,函数 A 调用函数 B,函数 B 调用函数 C。在这种情况下,函数 A 会等待函数 B 完成,函数 B 又会等待函数 C 完成,之后函数 B 才能返回结果给函数 A。整个过程是一个线性的控制流,每个任务都必须等待前一个任务的完成。
优点:
逻辑简单直观,易于理解和实现。
调试方便,因为任务的执行顺序是确定的,出现问题时更容易定位和修复。
缺点:
效率低下。如果一个任务需要等待外部资源(如网络请求、数据库查询等),整个流程会被阻塞,直到资源可用。这可能导致系统性能下降,尤其是在高并发场景下。
不适合处理耗时的任务或需要频繁交互的场景,因为这些场景下同步模式会导致系统响应变慢。
应用场景:
简单的线性流程,任务之间没有复杂交互。
安全要求较高的场景,需要确保任务按严格顺序执行。
测试和调试阶段,因为同步流程更容易跟踪和发现问题。
异步
任务可以在多个任务之间并行执行,一个任务的启动不需要等待另一个任务完成。在异步模式下,调用方发起任务后,不会等待被调用方的即时响应,而是继续执行后续任务。被调用方在完成任务后,通常会通过回调函数、事件或通知等方式通知调用方。
原理: 异步操作的核心是通过事件驱动或回调机制来处理任务完成后的结果。例如,在一个异步的网络请求场景中,当客户端发起请求后,不会阻塞线程等待响应,而是继续执行其他任务。当服务器返回响应时,客户端会通过回调函数或事件监听器来处理响应数据。这种模式允许在等待任务完成时,系统可以利用这段时间来处理其他任务,从而提高资源利用率和系统性能。
优点:
高效利用资源。在等待任务完成期间,系统可以执行其他任务,避免了线程阻塞和资源浪费。
适合高并发和交互频繁的场景,如 Web 服务器处理大量用户请求、前端 JavaScript 处理用户交互等。
提高系统响应速度。由于任务可以并行执行,用户不需要等待一个任务完成就可以开始另一个任务。
缺点:
逻辑复杂。异步模式下,任务的执行顺序可能不是线性的,这增加了代码的复杂性和理解难度。
调试困难。由于任务之间没有明确的顺序关系,出现问题时难以定位和复现。
需要处理并发问题。异步模式下,多个任务可能同时访问共享资源,需要采取措施保证线程安全。
应用场景:
需要处理大量并发请求的场景,如 Web 服务器、消息队列等。
涉及 I/O 操作(如文件读写、网络通信)的场景,可以利用异步模式提高 I/O 操作的效率。
需要实时响应的场景,如用户交互、游戏开发等,以提高系统的响应速度。
耗时任务的处理,如数据处理、图像渲染等,可以将耗时任务放在后台异步执行,避免阻塞主线程。
对比总结
线程池相关
Future类
Future 类是一个接口,用于表示异步计算的结果。它主要用于处理并发编程中的异步任务,允许你在提交任务后获取任务的执行结果。以下是 Future 类的主要功能和用途:
- Future 类的主要功能
表示异步计算的结果:Future 表示一个可能尚未完成的异步任务的结果。你可以通过 Future 对象检查任务是否已完成,或者等待任务完成并获取结果。
检查任务状态:Future 提供了方法来检查任务是否已完成(isDone())、是否被取消(isCancelled())等。
获取任务结果:通过 get() 方法可以获取任务的执行结果。如果任务尚未完成,get() 方法会阻塞,直到任务完成并返回结果。
取消任务:可以通过 cancel() 方法取消任务的执行。 - Future 类的使用场景
异步任务处理:当你需要在一个线程中提交一个任务,并在另一个线程中获取任务的执行结果时,可以使用 Future。
并发编程:在多线程或多核处理器的环境中,Future 可以帮助你更好地管理异步任务的执行和结果获取。 - 示例代码
以下是一个简单的示例,展示了如何使用 Future:
import java.util.concurrent.*;
public class FutureExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
// 提交一个 Callable 任务
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(2000); // 模拟耗时任务
return "Hello, Future!";
}
});
try {
// 获取任务结果(如果任务未完成,会阻塞在这里)
String result = future.get();
System.out.println("Task result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executor.shutdown(); // 关闭线程池
}
}
}
- FutureTask 类
FutureTask 是 Future 接口的一个实现类,同时也实现了 Runnable 接口。它既可以作为 Future 的实现类来获取任务的执行结果,也可以作为线程执行的任务来执行异步操作。FutureTask 是线程池中常用的实现类之一。 - CompletableFuture 类
从 Java 8 开始,CompletableFuture 类被引入,提供了更强大和灵活的异步编程功能。它允许你链式地组合多个异步操作,处理异常情况,并在任务完成时执行特定的回调函数。CompletableFuture 实现了 Future 接口,并扩展了其功能。 - 总结
Future 类是 Java 并发编程中用于处理异步任务的重要工具。它允许你在提交任务后获取任务的执行结果,并提供了检查任务状态和取消任务的功能。在实际开发中,Future 和其实现类(如 FutureTask 和 CompletableFuture)可以帮助你更好地管理异步任务和提高程序的并发性能。
微服务架构中的分布式事务
在微服务架构中,分布式事务是一个关键的挑战,因为一个业务操作可能涉及多个微服务,而这些服务之间的数据一致性需要得到保证。以下是几种常见的解决方案及其详细解释:
- 基于消息队列的最终一致性方案
原理:
异步通信:通过消息队列(如 Kafka、RabbitMQ)实现服务之间的异步通信。当一个服务完成其操作后,它会向消息队列发送一条消息,而不是等待其他服务同步处理。
最终一致性:虽然数据在短时间内可能不一致,但最终会达到一致状态。例如,支付服务在完成支付后,向消息队列发送一条消息,积分服务接收到消息后,再进行积分更新操作。即使积分服务暂时不可用,消息队列也会保存消息,直到积分服务恢复后处理。
优点:
解耦服务:服务之间通过消息队列解耦,提高了系统的灵活性和可扩展性。
提高性能:异步通信减少了服务之间的直接依赖,提高了系统的整体性能。
容错性强:即使某个服务暂时不可用,消息队列也能确保消息不会丢失,系统最终会恢复一致状态。
缺点:
数据一致性延迟:数据在短时间内可能不一致,需要业务能够容忍这种延迟。
复杂性增加:需要处理消息的重复消费、消息丢失等问题,增加了系统的复杂性。 - Saga 模式
原理:
事务分解:将一个长事务分解为多个短小的本地事务,每个本地事务由一个微服务处理。
补偿机制:每个本地事务都有一个对应的补偿操作(如回滚操作),当某个本地事务失败时,通过执行补偿操作来恢复系统状态。
编排和协同:Saga 模式有两种实现方式:
编排型:由一个中央协调者(如Saga协调器)来管理事务的流程,协调各个服务的操作和补偿操作。
协同型:各个服务通过事件通知(如消息队列)来协同事务的执行和补偿操作。
优点:
灵活性高:可以根据业务需求灵活选择编排型或协同型实现方式。
支持异步操作:通过异步通信提高系统的性能和可扩展性。
容错性强:通过补偿机制确保数据的一致性,即使某个服务失败,也能通过补偿操作恢复系统状态。
缺点:
实现复杂:需要设计和实现补偿操作,增加了系统的复杂性。
事务管理:需要管理事务的状态和流程,增加了开发和维护的难度。 - 两阶段提交(2PC)
原理:
准备阶段:事务协调者向所有参与者发送准备请求,参与者准备执行事务操作,并将结果(成功或失败)返回给协调者。
提交阶段:如果所有参与者都准备成功,协调者发送提交请求,参与者执行事务操作。如果任何一个参与者准备失败,协调者发送回滚请求,参与者执行回滚操作。
优点:
强一致性:确保所有参与者在事务提交或回滚时保持一致状态。
适用于关键业务:适用于对数据一致性要求极高的业务场景,如金融交易。
缺点:
阻塞问题:如果协调者或某个参与者在准备阶段失败,其他参与者会处于阻塞状态,影响系统的可用性。
单点故障:协调者是单点,如果协调者失败,整个事务流程将无法继续。 - 基于消息的最终一致性方案
原理:
消息确认:服务在完成操作后,向消息队列发送消息,并等待消息队列的确认。如果消息发送失败,服务会重试发送。
消息消费:其他服务从消息队列中消费消息,并执行相应的操作。如果消费失败,消息队列会重新投递消息,直到消费成功。
优点:
解耦服务:服务之间通过消息队列解耦,提高了系统的灵活性和可扩展性。
提高性能:异步通信减少了服务之间的直接依赖,提高了系统的整体性能。
容错性强:即使某个服务暂时不可用,消息队列也能确保消息不会丢失,系统最终会恢复一致状态。
缺点:
数据一致性延迟:数据在短时间内可能不一致,需要业务能够容忍这种延迟。
复杂性增加:需要处理消息的重复消费、消息丢失等问题,增加了系统的复杂性。 - 使用分布式事务框架
原理:
框架支持:使用分布式事务框架(如 Narayana、DTF)来管理分布式事务。这些框架提供了事务管理、协调和补偿机制,简化了分布式事务的实现。
多种事务模型:支持多种事务模型,如两阶段提交、基于消息的事务、Saga 模式等,可以根据业务需求选择合适的模型。
优点:
简化实现:框架提供了事务管理、协调和补偿机制,减少了开发和维护的复杂性。
灵活性高:支持多种事务模型,可以根据业务需求灵活选择。
可靠性高:框架通常提供了完善的错误处理和补偿机制,确保事务的一致性和可靠性。
缺点:
依赖框架:需要依赖特定的分布式事务框架,增加了系统的依赖性。
学习成本:需要学习和掌握框架的使用和配置,增加了开发人员的学习成本。
总结
在微服务架构中,解决分布式事务问题的方法有多种,每种方法都有其优缺点和适用场景。选择合适的解决方案需要根据具体的业务需求、系统架构和性能要求来决定。常见的解决方案包括基于消息队列的最终一致性方案、Saga 模式、两阶段提交和使用分布式事务框架。