- 算法:92 反转链表2
后端 社招 流程:
2. 自我介绍
3. 项目有关问题
4. 基础java八股文
5. 算法 反转链表||
八股文问题记录
Redis为啥快
Redis如果提前锁被释放了怎么办
JMM
线程池的核心参数,自己怎么用,最大线程数什么时候生效
MVCC怎么实现的
间隙锁了解过吗
写代码时候发生过死锁吗
redis为啥快
-
内存存储:Redis 将数据存储在内存中,相比于传统的磁盘存储,内存存储的读写速度要快得多。
-
单线程模型:Redis 使用单线程的事件循环机制处理请求,避免了多线程的上下文切换开销。尽管是单线程,Redis 能够通过非阻塞 I/O 和多路复用技术处理大量并发连接。
-
数据结构优化:Redis 提供了一系列高效的数据结构,如字符串(String)、列表(List)、集合(Set)、有序集合(Sorted Set)和哈希(Hash),这些数据结构的操作都经过精心优化,保证了高性能。
-
避免复杂操作:Redis 的设计避免了复杂的查询操作,它更专注于快速的读写和简单的数据操作,这种简化操作使其更高效。
-
持久化机制:虽然 Redis 是内存数据库,但它提供了 RDB 和 AOF 两种持久化机制,保证了数据的持久性,同时也不会影响其内存操作的高性能。
-
内存管理:Redis 使用了高效的内存管理策略,包括对象共享、惰性释放、对象压缩等,最大限度地利用内存。
-
网络 I/O 优化:Redis 使用了基于 epoll 的网络 I/O 模型,减少了 I/O 操作的开销,提高了响应速度。
通过上述多个方面的优化和设计,Redis 能够在处理大量数据和高并发请求时,依然保持极高的性能。
备注:IO事件通知机制 只关注活跃的文件描述符(针对大规模网络连接,面对数千/万个文件描述符的时候可快速有效检测)
Redis提前释放锁
在使用 Redis 实现分布式锁时,锁的提前释放可能会导致多个客户端同时持有锁,从而引发竞争条件和数据不一致等问题。为了防止这种情况,可以采取以下措施:
1. 锁续期(Lock Renewal)
在持有锁的过程中,定期续期锁的过期时间,确保锁在任务执行期间不会被意外释放。
实现方式:
- 启动一个独立的线程或定时任务,在锁快要过期时,延长锁的过期时间。
- 在续期时,要确保续期操作的原子性,可以使用 Redis 的
PEXPIRE
命令更新锁的过期时间。
2. 锁的唯一标识(Token)
为每个锁生成一个唯一标识(如 UUID),在释放锁时检查当前持有锁的唯一标识是否匹配,只有匹配时才释放锁。
实现方式:
-
设置锁时,附加一个唯一标识:
local lock_key = "my_lock" local lock_value = "unique_token" redis.call("SET", lock_key, lock_value, "NX", "PX", 30000)
-
释放锁时,使用 Lua 脚本保证操作的原子性:
local lock_key = "my_lock" local lock_value = "unique_token" local current_value = redis.call("GET", lock_key) if current_value == lock_value then redis.call("DEL", lock_key) end
3. 使用 Redlock 算法
Redlock 是 Redis 作者提出的一种用于实现分布式锁的算法,适用于 Redis 集群。Redlock 通过在多个 Redis 实例上同时获取锁,提高了锁的可靠性和安全性。
实现方式:
- 在多个(通常是 5 个) Redis 实例上同时尝试获取锁,只有在大多数(至少 3 个)实例上成功获取锁,才认为获取成功。
- 锁的过期时间要足够短,以防止因单个实例不可用而导致锁无法及时释放。
- 在任务执行过程中,可以定期续期锁的过期时间。
Redlock 的 Python 示例:
import redis
import time
import uuid
class Redlock:
def __init__(self, redis_nodes):
self.redis_nodes = [redis.StrictRedis(host=node[0], port=node[1]) for node in redis_nodes]
def acquire_lock(self, lock_key, ttl):
lock_value = str(uuid.uuid4())
quorum = len(self.redis_nodes) // 2 + 1
acquired = 0
for node in self.redis_nodes:
if node.set(lock_key, lock_value, nx=True, px=ttl):
acquired += 1
if acquired >= quorum:
return lock_value
else:
for node in self.redis_nodes:
node.delete(lock_key)
return None
def release_lock(self, lock_key, lock_value):
lua_script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
"""
for node in self.redis_nodes:
node.eval(lua_script, 1, lock_key, lock_value)
# 使用 Redlock
redis_nodes = [("127.0.0.1", 6379), ("127.0.0.1", 6380), ("127.0.0.1", 6381)]
lock_manager = Redlock(redis_nodes)
lock_key = "my_lock"
ttl = 10000 # 锁过期时间 10 秒
lock_value = lock_manager.acquire_lock(lock_key, ttl)
if lock_value:
try:
# 执行业务逻辑
pass
finally:
lock_manager.release_lock(lock_key, lock_value)
else:
print("Failed to acquire lock")
4. 超时和重试机制
在获取锁时设置获取超时和重试机制,如果在一定时间内无法获取到锁,则放弃操作或进行相应处理。
实现方式:
- 在获取锁的代码中加入重试逻辑,重试次数和间隔时间可以根据业务需求调整。
通过上述措施,可以有效防止 Redis 锁的提前释放问题,提高分布式锁的可靠性和安全性。
MVCC实现
通过上述多个方面的优化和设计,Redis 能够在处理大量数据和高并发请求时,依然保持极高的性能。
多版本并发控制(MVCC,Multi-Version Concurrency Control)是一种用于实现数据库事务并发控制的技术。它允许多个事务并发地读取和写入数据,而不相互阻塞,从而提高系统的性能和并发度。MVCC 的核心思想是通过保存数据的多个版本,实现对并发事务的隔离。下面是 MVCC 的实现原理:
基本概念
- 版本链:每个数据行都有多个版本,每个版本包含一个时间戳或事务 ID,以标识其创建时间。
- 快照读:读取操作不会阻塞写操作。读取操作会读取数据行的一个特定版本(通常是事务开始时的快照),即使其他事务正在修改该行。
- 版本控制:每个写操作都会创建数据行的新版本,并将其添加到版本链中,而不会立即覆盖旧版本。
实现步骤
- 事务开始:当一个事务开始时,数据库系统会为该事务分配一个唯一的时间戳或事务 ID,这个时间戳标识了事务开始的时间点。
- 读取操作:
- 快照读:事务会读取在其开始时间之前提交的最新版本数据。
- 当前读:某些特殊的读取(如
SELECT ... FOR UPDATE
)会读取最新版本的数据,同时可能会锁定该数据,以防其他事务修改。
- 写入操作:
- 写操作(如
INSERT
、UPDATE
、DELETE
)会创建数据行的新版本,新版本带有当前事务的时间戳或事务 ID。 - 旧版本会保留在版本链中,以供其他事务读取。
- 写操作(如
- 提交事务:当一个事务提交时,系统会将其时间戳或事务 ID 作为提交时间。这标识了该事务所做更改的生效时间。
- 版本清理:系统会定期清理不再需要的旧版本数据,以释放存储空间。这个过程通常称为垃圾回收(Garbage Collection)。
具体实现
不同的数据库系统在实现 MVCC 时可能有所不同。下面是两个流行数据库的 MVCC 实现简介:
PostgreSQL
- PostgreSQL 使用多版本数据行(MVCC 行),每个数据行都有两个隐藏字段:
xmin
和xmax
。xmin
表示创建该版本的事务 ID,xmax
表示删除该版本的事务 ID(如果已删除)。 - 事务在读取数据行时,会根据事务的时间戳和
xmin
、xmax
判断该行版本是否对当前事务可见。 - 写操作会创建新版本的行,并更新
xmax
字段标识旧版本已删除。
MySQL (InnoDB 引擎)
- InnoDB 使用隐藏的事务 ID 和回滚指针实现 MVCC。每个数据行都有一个隐藏的事务 ID,标识创建该行版本的事务。
- InnoDB 在写操作时,会记录旧版本的数据到回滚日志中,以供其他事务读取旧版本数据。
- 事务读取数据行时,通过比较事务 ID 和回滚指针,判断数据版本的可见性。
通过上述机制,MVCC 有效地提高了数据库的并发性能,避免了读写操作之间的直接冲突,同时保持了事务的隔离性。
分布式场景下如何保证消息消费的顺序
在并发场景下,保证消息消费的顺序是一个常见的挑战,特别是在分布式系统中。以下是几种常见的方法和策略,可以用来确保消息消费的顺序:
1. 基于分区的顺序消费
Kafka
Kafka 是一个流行的分布式消息系统,它通过以下方式保证消息的顺序:
- 主题分区:Kafka 中的主题被划分为多个分区,每个分区内的消息是有序的。为了保证顺序消费,可以将相关的消息发送到同一个分区。
- 消息键:消息生产者可以根据某个键(如订单ID)将消息发送到相同的分区。Kafka 使用消息键来计算分区的哈希值,从而保证相同键的消息进入同一分区。
- 消费者组:Kafka 中的消费者组保证每个分区只能由一个消费者实例消费,这样可以保证分区内消息的顺序。
2. 基于消息队列的顺序消费
RabbitMQ
RabbitMQ 可以通过以下方式保证消息顺序:
- 队列绑定:确保消息被发送到同一个队列中,可以通过使用路由键和队列绑定来实现。
- 消费者串行化:配置消费者以串行化处理消息,即每个消费者在处理完当前消息之前,不会处理下一个消息。可以通过设置
prefetch
值为 1 来实现。
3. 分布式锁
在某些情况下,可以使用分布式锁来保证消息处理的顺序:
- 分布式锁服务:使用分布式锁服务(如 Redis、Zookeeper)来确保只有一个消费者在特定时间段内处理特定类型的消息。
- 顺序控制:在消费消息时,获取对应的锁,处理完成后释放锁,从而保证同一类消息按顺序处理。
4. 本地事务
对于需要严格顺序的场景,可以结合本地事务来保证顺序:
- 事务队列:使用数据库事务和消息队列组合,将消息处理和数据库操作放在一个本地事务中,确保消息处理的原子性和顺序性。
- 消息表:使用一张消息表来存储待处理的消息,按照插入顺序消费,每次处理完一条消息后,标记该消息为已处理。
5. 逻辑顺序控制
在某些场景下,可以通过逻辑上的顺序控制来保证消费顺序:
- 序列号:每个消息带上一个序列号,消费者在消费时根据序列号排序处理。
- 缓存队列:使用缓存(如内存队列)来暂存消息,根据序列号顺序处理。
实践案例
Kafka 实践
-
生产者配置:
- 为每条消息设置一个键(如订单ID),确保相同键的消息发送到同一分区。
ProducerRecord<String, String> record = new ProducerRecord<>(topic, key, value); producer.send(record);
-
消费者配置:
- 使用同一个消费者组,确保每个分区由一个消费者处理。
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); consumer.subscribe(Collections.singletonList(topic)); while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); for (ConsumerRecord<String, String> record : records) { // Process record } }
通过上述方法,可以在并发场景下保证消息消费的顺序。具体选择哪种方法取决于系统的架构和业务需求。
线程池
间隙锁
间隙锁(Gap Lock)是数据库系统(尤其是 MySQL 的 InnoDB 存储引擎)中用于实现多版本并发控制(MVCC)和防止幻读的一种锁机制。它锁定的不是具体的行,而是行与行之间的间隙。间隙锁的主要作用是防止其他事务在当前事务操作范围内插入新的数据,从而保证事务的一致性。
间隙锁的工作原理
间隙锁是指锁住两个索引记录之间的间隙,以防止其他事务在这个间隙中插入新记录。它通常在范围查询(如 SELECT ... FOR UPDATE
)或范围更新(如 UPDATE ... WHERE
)时使用。
假设有如下索引记录:
1, 3, 5, 7
当一个事务执行如下查询时:
SELECT * FROM table WHERE id > 3 FOR UPDATE;
InnoDB 会在范围 (3, 5)
上加上间隙锁,防止其他事务在这个范围内插入新的记录。
间隙锁的类型
-
纯间隙锁(Gap Lock):只锁定间隙,不锁定具体的行。例如
SELECT * FROM table WHERE id > 3 FOR UPDATE
,只锁定(3, 5)
这个间隙。 -
临键锁(Next-Key Lock):同时锁定行和相邻的间隙。它是行锁(Record Lock)和间隙锁(Gap Lock)的组合。例如
SELECT * FROM table WHERE id = 3 FOR UPDATE
,不仅锁定id = 3
的记录,还锁定(2, 3)
和(3, 4)
之间的间隙。
间隙锁的应用场景
-
防止幻读:在可重复读(REPEATABLE READ)隔离级别下,使用间隙锁可以防止幻读问题。幻读是指在同一个事务中,两次相同的查询却得到了不同的结果,这是由于其他事务在查询范围内插入了新记录。
-
范围更新和删除:在执行范围更新或删除操作时,间隙锁可以防止其他事务在操作范围内插入新记录,确保数据的一致性。
例子
假设有一个表 employees
,包含如下数据:
id | name
-----------
1 | Alice
3 | Bob
5 | Charlie
7 | David
现在有两个事务:
事务1:
START TRANSACTION;
SELECT * FROM employees WHERE id > 3 FOR UPDATE;
此时,InnoDB 会在 (3, 5)
、(5, 7)
、(7, +∞)
这些间隙上加上间隙锁。
事务2:
START TRANSACTION;
INSERT INTO employees (id, name) VALUES (4, 'Eve');
由于 (3, 5)
间隙被事务1锁定,事务2会被阻塞,直到事务1提交或回滚。
需要注意的问题
- 死锁:由于间隙锁会锁定范围,如果多个事务尝试获取重叠的间隙锁,可能会导致死锁。数据库系统通常会自动检测死锁并回滚其中一个事务。
- 性能影响:频繁使用间隙锁可能会导致锁冲突和性能问题,因此在设计数据库和应用程序时需要谨慎使用。
间隙锁通过锁定行之间的间隙,有效防止了幻读问题,并在范围操作中保证了数据一致性,是保证数据库事务隔离性的重要机制之一。
线程池使用和最大线程池参数啥时候用
线程池是为了管理和复用一组工作线程而设计的,它可以提高应用程序的性能和资源使用效率通过合理配置和使用线程池,可以有效提高应用程序的并发处理能力和资源利用效率。Java 中的 java.util.concurrent
包提供了丰富的线程池实现,其中 ThreadPoolExecutor
是最常用的一个。以下是线程池的核心参数及其使用方法。
线程池核心参数
-
corePoolSize(核心线程数):
- 线程池中保持的最小线程数,即使这些线程是空闲的。
-
maximumPoolSize(最大线程数):
- 线程池允许创建的最大线程数。如果当前运行的线程数达到这个值,再有新任务到来时,线程池将拒绝任务或采取其他策略。
-
keepAliveTime(线程空闲时间):
- 当线程数超过
corePoolSize
时,多余的空闲线程在终止前等待新任务的最长时间。
- 当线程数超过
-
unit(时间单位):
keepAliveTime
的时间单位,可以是TimeUnit.SECONDS
、TimeUnit.MILLISECONDS
等。
-
workQueue(任务队列):
- 用于存放等待执行任务的队列,可以是
LinkedBlockingQueue
、SynchronousQueue
- 用于存放等待执行任务的队列,可以是
-
threadFactory(线程工厂):
- 用于创建新线程的工厂。通过提供自定义的
ThreadFactory
实现,可以为每个线程设置自定义的属性,如线程名、优先级等。
- 用于创建新线程的工厂。通过提供自定义的
-
handler(拒绝策略):
- 当任务无法被执行时的处理策略。Java 提供了以下几种内置的拒绝策略:
AbortPolicy
(默认):抛出RejectedExecutionException
。CallerRunsPolicy
:调用执行任务的线程直接执行该任务。DiscardPolicy
:直接丢弃任务,不抛出异常。DiscardOldestPolicy
:丢弃队列中最旧的任务,然后尝试重新提交任务。
- 当任务无法被执行时的处理策略。Java 提供了以下几种内置的拒绝策略:
创建和使用线程池
以下是一个使用 ThreadPoolExecutor
创建和配置线程池的示例:
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60, // keepAliveTime
TimeUnit.SECONDS, // unit
new LinkedBlockingQueue<>(100), // workQueue
Executors.defaultThreadFactory(), // threadFactory
new ThreadPoolExecutor.AbortPolicy() // handler
);
// 提交任务给线程池
for (int i = 0; i < 20; i++) {
executor.execute(new Task(i));
}
// 关闭线程池
executor.shutdown();
}
static class Task implements Runnable {
private final int taskId;
Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Executing task " + taskId + " by " + Thread.currentThread().getName());
try {
Thread.sleep(2000); // 模拟任务执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
线程池的常用静态工厂方法
Java 的 Executors
类提供了几个便捷的方法来创建常用的线程池:
-
newFixedThreadPool(int nThreads):
- 创建一个固定大小的线程池,重用指定数量的线程。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
-
newCachedThreadPool():
- 创建一个可缓存的线程池,调用
execute
将重用以前构造的线程(如果线程可用)。如果没有可用线程,则创建一个新线程并添加到池中,终止并从缓存中移除那些已有 60 秒未被使用的线程。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- 创建一个可缓存的线程池,调用
-
newSingleThreadExecutor():
- 创建一个使用单个工作线程的线程池,执行唯一任务,确保所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
-
newScheduledThreadPool(int corePoolSize):
- 创建一个支持定时及周期性任务执行的线程池。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
线程池的使用注意事项
-
合理配置线程池大小:
- 配置过大的线程池会导致资源浪费,而配置过小则可能导致任务处理延迟。一般根据任务性质(CPU 密集型或 IO 密集型)进行合理配置。
-
处理异常:
- 在线程池中执行任务时,任务中未捕获的异常不会抛出到调用者。可以通过设置
ThreadFactory
或在任务内部捕获并处理异常。
- 在线程池中执行任务时,任务中未捕获的异常不会抛出到调用者。可以通过设置
-
线程池的关闭:
- 使用
shutdown()
或shutdownNow()
方法来关闭线程池,前者会等待已提交的任务执行完毕后关闭,后者则会尝试停止所有正在执行的任务并关闭线程池。
- 使用
队列的选择
在 Java 中,ThreadPoolExecutor
使用 BlockingQueue
来存储等待执行的任务。不同类型的任务队列有不同的特性和适用场景。以下是常见的任务队列类型及其特性:
1. ArrayBlockingQueue
- 特点:有界队列,底层使用数组实现,必须在创建时指定队列的容量。
- 适用场景:适用于需要限制队列大小的场景,可以防止因为无限制的任务提交导致内存溢出。
- 示例:
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100); ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, queue);
2. LinkedBlockingQueue
- 特点:可以选择有界或无界,底层使用链表实现,如果没有指定容量,则为无界队列。
- 适用场景:适用于任务生产速度和消费速度不均衡的场景,通常用作默认的任务队列。
- 示例:
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(100); // 有界队列 BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(); // 无界队列 ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, queue);
3. SynchronousQueue
- 特点:无缓冲队列,每个插入操作必须等待相应的移除操作,反之亦然。
- 适用场景:适用于任务提交速度和处理速度大致相等的场景,或者需要立即处理提交的任务。
- 示例:
BlockingQueue<Runnable> queue = new SynchronousQueue<>(); ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, queue);
4. PriorityBlockingQueue
- 特点:无界优先级队列,任务按优先级顺序执行,任务必须实现
Comparable
接口,或者提供Comparator
。 - 适用场景:适用于任务有优先级区分的场景。
- 示例:
BlockingQueue<Runnable> queue = new PriorityBlockingQueue<>(); ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, queue);
5. DelayQueue
- 特点:无界延迟队列,只有在延迟期满后才能从队列中取出元素,任务必须实现
Delayed
接口。 - 适用场景:适用于需要延迟执行任务的场景,如定时任务。
- 示例:
BlockingQueue<Runnable> queue = new DelayQueue<>(); // Note: DelayQueue is usually used with ScheduledExecutorService rather than ThreadPoolExecutor
选择合适的任务队列
选择合适的任务队列类型是设计线程池的关键,取决于具体的应用场景和需求:
- 任务数量有限且需要限制内存使用:使用
ArrayBlockingQueue
或者LinkedBlockingQueue
(有界)。 - 任务生产和消费速度不均衡:使用
LinkedBlockingQueue
(无界),注意这种情况可能会导致内存占用过多。 - 需要立即处理提交的任务:使用
SynchronousQueue
。 - 需要根据任务优先级处理:使用
PriorityBlockingQueue
。 - 需要延迟执行任务:使用
DelayQueue
,通常配合ScheduledExecutorService
使用。
合理选择和配置任务队列可以有效提升线程池的性能和资源利用率,避免潜在的性能问题和资源浪费。
JMM java内存模型
Java Memory Model(JMM,Java 内存模型)是 Java 虚拟机规范的一部分,用于定义多线程环境下变量的访问规则和操作语义。JMM 解决了在并发编程中线程之间如何共享和传递数据的问题。它定义了变量的读/写操作在何种情况下是可见的,以保证多线程程序的正确性和一致性。
JMM 的关键概念
-
主内存和工作内存:
- 主内存:所有线程共享的内存区域,存储全局变量和对象实例。
- 工作内存:每个线程都有自己的工作内存(高速缓存),存储线程独有的数据副本。线程对变量的所有操作(读取、写入)必须在工作内存中进行,不能直接操作主内存中的变量。
-
可见性:
- 可见性指的是当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。JMM 提供了一些机制来保证变量修改的可见性,如
volatile
关键字、锁机制(如synchronized
、ReentrantLock
)等。
- 可见性指的是当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。JMM 提供了一些机制来保证变量修改的可见性,如
-
原子性:
- 原子性指的是一个操作不可中断,整个操作过程要么全部执行,要么全部不执行。Java 提供了一些原子操作,如
volatile
变量的读/写、synchronized
块内的代码执行等。
- 原子性指的是一个操作不可中断,整个操作过程要么全部执行,要么全部不执行。Java 提供了一些原子操作,如
-
有序性:
- 有序性指的是程序执行的顺序。Java 语言规范允许编译器和处理器对代码进行优化,改变指令执行的顺序以提高性能,但 JMM 提供了一些机制(如
happens-before
关系)来保证多线程环境下的有序性。
- 有序性指的是程序执行的顺序。Java 语言规范允许编译器和处理器对代码进行优化,改变指令执行的顺序以提高性能,但 JMM 提供了一些机制(如
happens-before 规则
happens-before
是 JMM 中用于描述操作间相互关系的重要概念。若一个操作 A
happens-before 操作 B
,则 A
的结果对 B
是可见的,并且 A
的执行顺序在 B
之前。以下是一些常见的 happens-before
规则:
- 程序次序规则:在一个线程内,按照代码顺序,前面的操作
happens-before
后面的操作。 - 监视器锁规则:一个
unlock
操作happens-before
于同一个锁上的后续lock
操作。 - volatile 变量规则:对一个
volatile
变量的写操作happens-before
于对这个volatile
变量的后续读操作。 - 传递性:如果操作
A
happens-before 操作B
,且操作B
happens-before 操作C
,则操作A
happens-before 操作C
。 - 线程启动规则:在主线程中启动子线程的
Thread.start()
操作happens-before
子线程中的任何操作。 - 线程终止规则:子线程中的所有操作
happens-before
主线程检测到子线程终止(通过Thread.join()
)的操作。
关键字和 JMM 相关的编程实践
-
volatile:
volatile
关键字保证了变量的可见性,即对一个volatile
变量的写操作对所有线程立即可见。volatile
还禁止指令重排序优化,保证了有序性。
-
synchronized:
synchronized
关键字保证了原子性和可见性。- 进入
synchronized
块前,线程必须获得锁,释放锁前,线程必须将对变量的修改刷新到主内存中,其他线程在获得同一锁后,能看到这些修改。
-
锁和并发工具类:
- Java 提供了丰富的并发工具类,如
ReentrantLock
、CountDownLatch
、CyclicBarrier
、Semaphore
等,这些工具类都基于 JMM 进行了设计,确保线程间通信和同步的正确性。
- Java 提供了丰富的并发工具类,如
JMM 中的示例
以下是一个简单的示例,展示了 volatile
关键字的作用:
public class VolatileExample {
private volatile boolean flag = false;
public void writer() {
flag = true; // 写操作
}
public void reader() {
if (flag) { // 读操作
System.out.println("Flag is true");
}
}
public static void main(String[] args) throws InterruptedException {
VolatileExample example = new VolatileExample();
Thread writerThread = new Thread(() -> example.writer());
Thread readerThread = new Thread(() -> example.reader());
writerThread.start();
readerThread.start();
writerThread.join();
readerThread.join();
}
}
在这个示例中,writer
方法中的写操作 flag = true
对 reader
方法中的读操作立即可见,保证了可见性。
总结
Java 内存模型是理解和编写正确并发程序的基础。通过了解 JMM 的核心概念、happens-before 规则以及相关关键字的使用,可以有效地避免多线程编程中的常见问题,如数据不一致、竞态条件等。