什么是悲观锁?

在高并发编程中,锁机制是保证线程安全的重要手段。悲观锁是一种对共享资源进行保护的锁机制,它假设最坏的情况,总是认为共享资源每次被访问时都会发生冲突。因此,在操作资源前,悲观锁会对资源进行加锁,确保其他线程无法访问该资源,直到锁被释放。

悲观锁的概念

悲观锁总是假设最坏的情况,认为共享资源每次被访问时都会出现问题(如共享数据被修改),所以每次在获取资源操作时都会上锁。这样,其他线程想要访问该资源时会被阻塞,直到锁被持有的线程释放。也就是说,共享资源每次只允许一个线程使用,其他线程阻塞等待,用完后再将资源转让给其他线程。

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方法使用悲观锁确保生产和消费操作的顺序性,防止数据竞争。

总结

悲观锁通过在资源访问前加锁,确保了线程安全和数据一致性,适用于以下场景:

  1. 数据库锁定:防止脏读和并发更新问题。
  2. 分布式系统中的资源锁定:确保多个节点对共享资源的访问一致性。
  3. 高并发场景下的资源访问控制:防止多个线程同时修改共享资源。
  4. 需要严格控制资源访问顺序的场景:确保资源按顺序被访问。

虽然悲观锁能有效防止数据不一致问题,但也带来了性能开销。在选择锁机制时,需要根据具体应用场景权衡性能和安全性,以便做出最佳决策。

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值