在高并发编程中,锁机制是保证线程安全的重要手段。悲观锁是一种对共享资源进行保护的锁机制,它假设最坏的情况,总是认为共享资源每次被访问时都会发生冲突。因此,在操作资源前,悲观锁会对资源进行加锁,确保其他线程无法访问该资源,直到锁被释放。
悲观锁的概念
悲观锁总是假设最坏的情况,认为共享资源每次被访问时都会出现问题(如共享数据被修改),所以每次在获取资源操作时都会上锁。这样,其他线程想要访问该资源时会被阻塞,直到锁被持有的线程释放。也就是说,共享资源每次只允许一个线程使用,其他线程阻塞等待,用完后再将资源转让给其他线程。
Java中的悲观锁实现
在Java中,synchronized和ReentrantLock是悲观锁思想的具体实现。
synchronized 关键字
synchronized关键字是一种隐式锁机制,使用简单方便,适合在方法或代码块级别进行同步。
java
public class SynchronizedExample {
public void performSynchronisedTask() {
synchronized (this) {
// 需要同步的操作
System.out.println("Thread " + Thread.currentThread().getName() + " is performing synchronized task.");
}
}
}
ReentrantLock 类
ReentrantLock是一个显式锁机制,功能更加灵活和强大,适合复杂的同步需求。
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
public void performLockTask() {
lock.lock();
try {
// 需要同步的操作
System.out.println("Thread " + Thread.currentThread().getName() + " is performing lock task.");
} finally {
lock.unlock();
}
}
}
synchronized
synchronized关键字在Java字节码中是通过 monitorenter 和 monitorexit 指令实现的。每个对象都有一个监视器(monitor),当线程进入同步块或同步方法时,需要获取该对象的监视器。
java
public class SynchronizedExample {
public synchronized void synchronizedMethod() {
// 需要同步的操作
}
}
反编译后的字节码:
plaintext
0: aload_0
1: monitorenter
2: // 需要同步的操作
3: aload_0
4: monitorexit
5: return
当线程执行到 monitorenter 指令时,会尝试获取对象的监视器,如果成功则进入同步块,否则阻塞等待。
ReentrantLock
ReentrantLock的内部实现依赖于AQS(AbstractQueuedSynchronizer),通过AQS实现了锁的获取和释放。
java
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
public void performLockTask() {
lock.lock();
try {
// 需要同步的操作
} finally {
lock.unlock();
}
}
}
ReentrantLock的锁获取过程:
java
public void lock() {
sync.lock();
}
sync 是 ReentrantLock 内部的一个同步器(Sync),它继承自 AbstractQueuedSynchronizer。
java
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
compareAndSetState 方法尝试将状态从0变为1,表示获取锁成功。如果失败,则调用 acquire 方法进行自旋或阻塞。
性能对比
在高并发场景下,激烈的锁竞争会导致线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。以下是 synchronized 和 ReentrantLock 的性能对比示例:
java
public class LockPerformanceTest {
private static final int THREAD_COUNT = 100;
private static final int LOOP_COUNT = 100000;
private static int counter = 0;
private static final Object lock = new Object();
private static final Lock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
long startTime, endTime;
// Test synchronized
startTime = System.currentTimeMillis();
Thread[] synchronizedThreads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
synchronizedThreads[i] = new Thread(() -> {
for (int j = 0; j < LOOP_COUNT; j++) {
synchronized (lock) {
counter++;
}
}
});
synchronizedThreads[i].start();
}
for (Thread t : synchronizedThreads) {
t.join();
}
endTime = System.currentTimeMillis();
System.out.println("Synchronized: " + (endTime - startTime) + " ms");
// Reset counter
counter = 0;
// Test ReentrantLock
startTime = System.currentTimeMillis();
Thread[] reentrantLockThreads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
reentrantLockThreads[i] = new Thread(() -> {
for (int j = 0; j < LOOP_COUNT; j++) {
reentrantLock.lock();
try {
counter++;
} finally {
reentrantLock.unlock();
}
}
});
reentrantLockThreads[i].start();
}
for (Thread t : reentrantLockThreads) {
t.join();
}
endTime = System.currentTimeMillis();
System.out.println("ReentrantLock: " + (endTime - startTime) + " ms");
}
}
在上述测试中,两个线程分别使用 synchronized 和 ReentrantLock 进行计数操作,并记录执行时间。可以看到在某些场景下,ReentrantLock 可能会表现得更好,因为它提供了更多的功能和更细粒度的控制。
悲观锁的应用场景
1. 数据库锁定
在数据库操作中,悲观锁常用于防止脏读和并发更新问题。比如,在银行转账的场景中,从一个账户转出资金并存入另一个账户,需要确保这两个操作的原子性。
示例:银行转账
java
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class BankTransfer {
private Connection connection;
public BankTransfer(Connection connection) {
this.connection = connection;
}
public void transfer(int fromAccountId, int toAccountId, int amount) throws SQLException {
try {
connection.setAutoCommit(false);
// Lock the rows to prevent concurrent updates
PreparedStatement lockStmt = connection.prepareStatement(
"SELECT * FROM accounts WHERE account_id IN (?, ?) FOR UPDATE");
lockStmt.setInt(1, fromAccountId);
lockStmt.setInt(2, toAccountId);
ResultSet rs = lockStmt.executeQuery();
if (rs.next()) {
int fromBalance = rs.getInt("balance");
if (fromBalance < amount) {
throw new SQLException("Insufficient funds");
}
// Perform the transfer
PreparedStatement updateStmt = connection.prepareStatement(
"UPDATE accounts SET balance = balance - ? WHERE account_id = ?");
updateStmt.setInt(1, amount);
updateStmt.setInt(2, fromAccountId);
updateStmt.executeUpdate();
updateStmt = connection.prepareStatement(
"UPDATE accounts SET balance = balance + ? WHERE account_id = ?");
updateStmt.setInt(1, amount);
updateStmt.setInt(2, toAccountId);
updateStmt.executeUpdate();
}
connection.commit();
} catch (SQLException e) {
connection.rollback();
throw e;
} finally {
connection.setAutoCommit(true);
}
}
}
在这个例子中,FOR UPDATE子句会锁定选定的行,确保在事务完成之前,其他事务无法修改这些行。
2. 分布式系统中的资源锁定
在分布式系统中,多个节点可能需要访问同一个共享资源。为了避免数据不一致问题,可以使用分布式锁。常见的实现方式包括基于数据库、Redis或Zookeeper的分布式锁。
示例:基于Redis的分布式锁
java
import redis.clients.jedis.Jedis;
public class RedisDistributedLock {
private Jedis jedis;
private String lockKey;
public RedisDistributedLock(Jedis jedis, String lockKey) {
this.jedis = jedis;
this.lockKey = lockKey;
}
public boolean tryLock(String lockValue, int expireTime) {
String result = jedis.set(lockKey, lockValue, "NX", "PX", expireTime);
return "OK".equals(result);
}
public void unlock(String lockValue) {
String currentValue = jedis.get(lockKey);
if (lockValue.equals(currentValue)) {
jedis.del(lockKey);
}
}
}
在这个例子中,tryLock方法尝试获取锁,如果成功则返回true,否则返回false。unlock方法用于释放锁。
3. 高并发场景下的资源访问控制
在高并发场景下,如果多个线程频繁访问共享资源,可能会导致数据不一致问题。悲观锁可以确保每次只有一个线程访问资源,从而保证数据一致性。
示例:库存扣减
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Inventory {
private int stock;
private final Lock lock = new ReentrantLock();
public Inventory(int stock) {
this.stock = stock;
}
public boolean reduceStock(int amount) {
lock.lock();
try {
if (stock >= amount) {
stock -= amount;
return true;
} else {
return false;
}
} finally {
lock.unlock();
}
}
public int getStock() {
return stock;
}
}
在这个例子中,reduceStock方法使用悲观锁来确保库存扣减操作的原子性,防止多个线程同时修改库存。
4. 需要严格控制资源访问顺序的场景
某些场景下,资源访问顺序需要严格控制,比如生产者-消费者模式中的队列操作,通过悲观锁可以确保资源按顺序被访问。
示例:生产者-消费者模式
java
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public ProducerConsumer(int capacity) {
this.capacity = capacity;
}
public void produce(int value) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await();
}
queue.offer(value);
notEmpty.signalAll();
} finally {
lock.unlock();
}
}
public int consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
int value = queue.poll();
notFull.signalAll();
return value;
} finally {
lock.unlock();
}
}
}
在这个例子中,produce和consume方法使用悲观锁确保生产和消费操作的顺序性,防止数据竞争。
总结
悲观锁通过在资源访问前加锁,确保了线程安全和数据一致性,适用于以下场景:
- 数据库锁定:防止脏读和并发更新问题。
- 分布式系统中的资源锁定:确保多个节点对共享资源的访问一致性。
- 高并发场景下的资源访问控制:防止多个线程同时修改共享资源。
- 需要严格控制资源访问顺序的场景:确保资源按顺序被访问。
虽然悲观锁能有效防止数据不一致问题,但也带来了性能开销。在选择锁机制时,需要根据具体应用场景权衡性能和安全性,以便做出最佳决策。