Lock和synchronized是什么?
在Java中,Lock
和synchronized
是用于管理多线程环境中对资源的访问,以保证线程安全的两种机制。本文将介绍一下他们的基本原理以及代码中的实际用法示例。
简单介绍一下Lock
当谈到Java中的Lock
机制,特别是如ReentrantLock
这样的具体实现,我们可以从几个不同的层面深入理解其原理和工作方式。Lock
接口提供了比内置的synchronized
关键字更复杂的锁定机制,允许更精细的控制和扩展的功能。
1. Lock
接口基础
Lock
接口定义了锁定操作必须遵守的基本协议,主要方法包括lock()
, unlock()
, tryLock()
, 和 lockInterruptibly()
。这些方法提供了显示的锁定和解锁功能,相比于synchronized
的自动锁管理,开发者需要明确地指示何时获取锁和释放锁。
2. ReentrantLock
作为Lock
的实现
ReentrantLock
是Lock
接口的一个实现,提供了可重入的互斥锁功能。可重入意味着同一个线程可以多次获取同一个锁,而不会导致死锁。
锁的状态管理
- 状态表示:
ReentrantLock
使用一个state
变量来表示锁的持有计数。当线程首次获得锁时,state
被设置为1。如果同一个线程再次获取这个锁,state
就会增加。每次释放锁时,state
递减,直到0,这时锁被完全释放。 - 所有者追踪:
ReentrantLock
还追踪当前持有锁的线程,这允许它实现可重入性和检查锁的所有权。
锁获取
- 公平性选择:
ReentrantLock
可以配置为公平或非公平。在公平模式下,锁倾向于授予等待时间最长的线程,而在非公平模式下,锁可能会被新请求的线程抢占。 - 锁机制:线程试图通过调用
lock()
获得锁。如果锁已被其他线程持有,则当前线程会被挂起(阻塞),直到锁可用。 - 条件变量支持:
ReentrantLock
允许创建一个或多个Condition
实例,这些实例与锁绑定,提供类似Object
监视器方法(wait
,notify
和notifyAll
)的功能。
锁的释放
- 释放锁:当线程完成其临界区内的操作后,必须调用
unlock()
来释放锁。这是与synchronized
明显不同的地方,后者会在方法或代码块结束时自动释放锁。
3. 底层实现细节
ReentrantLock
的实现依赖于底层的同步器框架,即AbstractQueuedSynchronizer
(AQS)。AQS使用一个双向队列来管理等待锁的线程,这是实现锁定和同步机制的核心。
- 节点和队列:AQS内部包含一个节点(Node)类和一个队列。每个节点包含一个线程引用,表示等待锁的线程,以及关于线程等待状态的信息。
- 独占模式:
ReentrantLock
在AQS的独占模式下工作,这意味着一次只有一个线程能持有锁。 - CAS操作:AQS使用比较并交换(CAS)操作来管理其状态字段,这提供了非阻塞的状态管理,是实现高效同步的关键。
通过这些机制,ReentrantLock
提供了一种比synchronized
更灵活和功能丰富的锁机制,适用于更复杂的并发场景。
简单介绍一下synchronized
synchronized
是Java中的一个基本同步机制,它内置于Java语言和JVM中。它主要用于控制多线程对共享资源的访问,防止出现数据不一致的情况。synchronized
可以用于方法或者特定代码块,确保同一时间只有一个线程执行特定的代码段。
原理概述
synchronized
关键字实现了对一个对象的监视器(monitor)的锁定和解锁。在Java中,每个对象都隐含关联一个监视器,这个监视器可以帮助实现对临界区的互斥访问。
- 方法锁:当
synchronized
用于实例方法或静态方法时,锁定的是对象实例(this
)或类的Class
实例。 - 块锁:当
synchronized
用于代码块时,需要指定一个锁对象。
锁的状态
JVM为每个对象和类维护一个锁(或监视器锁),这个锁有几个状态:
- 无锁状态
- 偏向锁:这是一种优化手段,假设一个锁主要被一个线程访问,将锁的所有权偏向这个线程,避免了真正的争夺。
- 轻量级锁:当锁对象被不同的线程访问,但没有竞争出现时,使用轻量级锁来优化。
- 重量级锁:当有真正的锁竞争时,锁会升级为重量级锁,此时会涉及到操作系统级的互斥原语。
工作原理
当一个线程尝试获取一个由synchronized
保护的锁时,它需要检查锁的状态:
- 如果锁是偏向锁且已偏向于调用线程,线程将直接进入同步块。
- 如果锁是可用的(无锁状态),JVM会尝试通过CAS操作将锁的状态设置为当前线程所有,变为轻量级锁。
- 如果锁已被其他线程持有,当前线程会根据锁的状态进行相应的处理:
- 如果锁是轻量级锁,JVM可能会自旋,尝试获取锁几次。
- 如果自旋失败,或锁已是重量级锁,线程将被阻塞直到锁被释放。
底层实现
在底层,synchronized
关键字的实现依赖于JVM的内部机制,具体如下:
- 对象头:在HotSpot JVM中,每个对象头包含两部分信息,标记字(mark word)和类型指针。标记字中存储了对象的锁状态信息、哈希码、偏向线程ID、年龄等信息。
- 监视器(Monitor):JVM内部使用Monitor对象来支持
synchronized
,该对象包含两个基本结构:WaitSet和EntryList。被阻塞的线程会加入到EntryList,等待获取锁;等待对象的线程会加入WaitSet。
synchronized
的使用和实现在JVM层面是高度优化的,包括锁升级和自旋锁等机制,使得其在不同情况下提供不同级别的性能优化。通过这些复杂的策略,JVM尽可能地减少同步的开销,同时保持线程安全。
使用Lock的代码示例
在Spring Boot项目中,使用Lock
的案例通常涉及到复杂的业务逻辑或需要更细粒度控制的并发场景。下面,我将提供一个使用ReentrantLock
的实际示例,这是一个常见的可重入锁,用于保证多线程环境下数据的一致性和线程的安全。
案例背景
假设我们有一个在线电商平台,需要在高并发环境下处理库存更新。为了避免多个用户同时下单时导致的库存超卖问题,我们可以使用ReentrantLock
来确保在更新库存数量时的线程安全。
步骤和代码示例
- 依赖配置
首先,确保你的Spring Boot项目已经设置了适当的依赖。通常,Spring Boot Starter已包含了你需要的大部分依赖。
- 服务层实现
我们在服务层实现库存更新的逻辑,使用ReentrantLock
来同步访问临界区代码:
import org.springframework.stereotype.Service;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
public class InventoryService {
private final Lock lock = new ReentrantLock();
public void updateInventory(String productId, int quantity) {
lock.lock();
try {
// 模拟获取当前库存,实际中应从数据库或缓存中获取
int currentInventory = getCurrentInventory(productId);
// 检查库存是否足够
if (currentInventory >= quantity) {
// 执行库存更新操作,实际中应写入数据库
int newInventory = currentInventory - quantity;
saveInventory(productId, newInventory);
} else {
throw new IllegalStateException("Not enough inventory for product: " + productId);
}
} finally {
lock.unlock();
}
}
private int getCurrentInventory(String productId) {
// 这里只是一个示例,实际应从数据库获取
return 100; // 假设始终返回100件库存
}
private void saveInventory(String productId, int newInventory) {
// 这里只是一个示例,实际应更新数据库
System.out.println("Inventory updated for product " + productId + ": " + newInventory);
}
}
- 调用服务
在你的控制器或其他业务逻辑中调用这个服务方法:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/inventory")
public class InventoryController {
@Autowired
private InventoryService inventoryService;
@PostMapping("/update")
public String updateInventory(@RequestParam String productId, @RequestParam int quantity) {
try {
inventoryService.updateInventory(productId, quantity);
return "Inventory updated successfully!";
} catch (IllegalStateException e) {
return e.getMessage();
}
}
}
解释
在这个例子中,ReentrantLock
被用来保护库存更新的代码段。当多个线程尝试执行updateInventory
方法时,ReentrantLock
确保一次只有一个线程可以执行库存减少的操作。这样可以防止多线程同时操作同一数据导致的数据不一致问题,如库存的超卖现象。
通过这种方式,你可以在Spring Boot应用中安全地处理复杂的并发数据修改操作,而不仅限于库存管理,同样的策略也可以应用于订单处理、财务事务等需要高度并发控制的业务场景。
使用sychronized的代码示例
在Spring Boot项目中,使用synchronized
关键字的场景通常比较简单,适用于保护临界区以避免多线程同时访问同一资源导致的问题。下面是一个具体的例子,展示如何在Spring Boot中使用synchronized
来同步访问和修改一个简单的计数器。
案例背景
假设我们的Spring Boot应用需要跟踪某个特定操作(比如用户的登录尝试)的次数。为了确保计数器的正确性,在多线程环境下(例如多个用户同时登录),我们需要使用synchronized
来防止并发访问导致的计数错误。
步骤和代码示例
- 创建一个服务类
首先,创建一个服务类来封装计数逻辑,使用synchronized
关键字确保线程安全。
import org.springframework.stereotype.Service;
@Service
public class CounterService {
private int loginAttemptCounter = 0;
public synchronized void incrementLoginAttempt() {
loginAttemptCounter++;
}
public synchronized int getLoginAttemptCount() {
return loginAttemptCounter;
}
}
在这个服务类中,我们定义了两个方法:incrementLoginAttempt
和getLoginAttemptCount
。这两个方法都被标记为synchronized
,这意味着同时只有一个线程能够执行这些方法中的任何一个。这保证了loginAttemptCounter
这个共享变量的访问和修改是线程安全的。
- 在控制器中使用服务
接下来,我们在一个REST控制器中使用这个服务。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
@Autowired
private CounterService counterService;
@PostMapping("/login")
public String login() {
// 假设这里有一些登录逻辑
counterService.incrementLoginAttempt();
return "Login Attempt Recorded";
}
@GetMapping("/login/attempts")
public String getAttempts() {
int attempts = counterService.getLoginAttemptCount();
return "Total Login Attempts: " + attempts;
}
}
代码解释
在这个简单的示例中,每当用户尝试登录时,login
方法就会被调用,并通过调用CounterService
中的incrementLoginAttempt
方法来增加计数器。getAttempts
方法可以返回到目前为止的登录尝试次数。
使用synchronized
确保了无论何时调用incrementLoginAttempt
和getLoginAttemptCount
,对loginAttemptCounter
的读写操作都是互斥的,从而避免了并发错误。
使用场景
这个使用synchronized
的例子虽然简单,但是非常适合处理小规模的并发控制。对于复杂的并发场景或者高性能需求,可能需要考虑使用更高级的并发控制机制,如java.util.concurrent
包中的锁机制或其他并发工具。
转载自:https://juejin.cn/post/7371649020123824128