再谈重入锁--ReentrantLock

入锁(ReentrantLock)是一种递归无阻塞的同步机制。以前一直认为它是synchronized的简单替代,而且实现机制也不相差太远。不过最近实践过程中发现它们之间还是有着天壤之别。

以下是官方说明:一个可重入的互斥锁定 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁定相同的一些基本行为和语义,但功能更强大。ReentrantLock 将由最近成功获得锁定,并且还没有释放该锁定的线程所拥有。当锁定没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁定并返回。如果当前线程已经拥有该锁定,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。

它提供了lock()方法:
如果该锁定没有被另一个线程保持,则获取该锁定并立即返回,将锁定的保持计数设置为 1。
如果当前线程已经保持该锁定,则将保持计数加 1,并且该方法立即返回。
如果该锁定被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁定之前,该线程将一直处于休眠状态,此时锁定保持计数被设置为 1。

最近在研究Java concurrent中关于任务调度的实现时,读了延迟队列DelayQueue的一些代码,比如take()。该方法的主要功能是从优先队列(PriorityQueue)取出一个最应该执行的任务(最优值),如果该任务的预订执行时间未到,则需要wait这段时间差。反之,如果时间到了,则返回该任务。而offer()方法是将一个任务添加到该队列中。

后来产生了一个疑问:如果最应该执行的任务是一个小时后执行的,而此时需要提交一个10秒后执行的任务,会出现什么状况?还是先看看take()的源代码:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) {
            E first = q.peek();
            if (first == null) {
                available.await();
            else {
                long delay =  first.getDelay(TimeUnit.NANOSECONDS);
                if (delay > 0) {
                    long tl = available.awaitNanos(delay);
                else {
                    E x = q.poll();
                    assert x != null;
                    if (q.size() != 0)
                        available.signalAll()// wake up other takers
                    return x;
                }
            }
        }
    finally {
        lock.unlock();
    }
}

而以下是offer()的源代码:

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        E first = q.peek();
        q.offer(e);
        if (first == null || e.compareTo(first0)
            available.signalAll();
        return true;
    finally {
        lock.unlock();
    }
}

 

如代码所示,take()和offer()都是lock了重入锁。如果按照synchronized的思维(使用诸如synchronized(obj)的方法),这两个方法是互斥的。回到刚才的疑问,take()方法需要等待1个小时才能返回,而offer()需要马上提交一个10秒后运行的任务,会不会一直等待take()返回后才能提交呢?答案是否定的,通过编写验证代码也说明了这一点。这让我对重入锁有了更大的兴趣,它确实是一个无阻塞的锁。

下面的代码也许能说明问题:运行了4个线程,每一次运行前打印lock的当前状态。运行后都要等待5秒钟。

public static void main(String[] argsthrows InterruptedException {
  final ExecutorService exec = Executors.newFixedThreadPool(4);
  final ReentrantLock lock = new ReentrantLock();
  final Condition con = lock.newCondition();
  final int time = 5;
  final Runnable add = new Runnable() {
    public void run() {
      System.out.println("Pre " + lock);
      lock.lock();
      try {
        con.await(time, TimeUnit.SECONDS);
      catch (InterruptedException e) {
        e.printStackTrace();
      finally {
        System.out.println("Post " + lock.toString());
        lock.unlock();
      }
    }
  };
  for(int index = 0; index < 4; index++)
    exec.submit(add);
  exec.shutdown();
}

 

这是它的输出:
Pre ReentrantLock@a59698[Unlocked]
Pre ReentrantLock@a59698[Unlocked]
Pre ReentrantLock@a59698[Unlocked]
Pre ReentrantLock@a59698[Unlocked]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-2]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-3]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-4]

每一个线程的锁状态都是“Unlocked”,所以都可以运行。但在把con.await改成Thread.sleep(5000)时,输出就变成了:
Pre ReentrantLock@a59698[Unlocked]
Pre ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Pre ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Pre ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-2]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-3]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-4]

以上的对比说明线程在等待时(con.await),已经不在拥有(keep)该锁了,所以其他线程就可以获得重入锁了。

有必要会过头再看看Java官方的解释:“如果该锁定被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁定之前,该线程将一直处于休眠状态”。我对这里的“保持”的理解是指非wait状态外的所有状态,比如线程Sleep、for循环等一切有CPU参与的活动。一旦线程进入wait状态后,它就不再keep这个锁了,其他线程就可以获得该锁;当该线程被唤醒(触发信号或者timeout)后,就接着执行,会重新“保持”锁,当然前提依然是其他线程已经不再“保持”了该重入锁。

总结一句话:对于重入锁而言,"lock"和"keep"是两个不同的概念。lock了锁,不一定keep锁,但keep了锁一定已经lock了锁。

<think>好的,用户让我介绍一下乐观锁和悲观锁。我需要先回忆一下这两个概念的基本定义和应用场景。乐观锁和悲观锁都是并发控制的方法,但它们的实现方式和适用情况有所不同。 首先,悲观锁,应该是指在对数据操作时,默认会有冲突发生,所以会先加锁再访问。比如数据库中的行锁、表锁,还有Java里的synchronized关键字和ReentrantLock。这种情况下,一旦某个线程获得了锁,其他线程就必须等待,直到锁被释放。这样确实能保证数据的一致性,但可能带来性能问题,尤其是在高并发的情况下,等待时间可能会很长,影响系统吞吐量。 然后是乐观锁,它假设冲突很少发生,所以在操作数据时不会加锁,而是在提交更新时检查是否有其他线程修改过数据。常见的实现方式有版本号机制或CAS(Compare and Swap)。比如在数据库中更新数据时,会检查版本号是否和之前读取的一致,如果一致则更新,否则重试或报错。Java中的AtomicInteger类就是用了CAS机制。乐观锁的优点是减少了加锁的开销,提高了性能,适合读多写少的场景。但如果冲突频繁发生,重试的次数可能会增多,反而影响效率。 接下来要考虑用户可能的背景。用户可能是在学习并发编程或者数据库事务处理,遇到了这两个概念需要理解。他们可能想知道两者的区别、适用场景以及优缺点。此外,用户可能没有说出的是实际应用中如何选择这两种锁,或者具体的实现例子。 需要确保解释清晰,对比两者的不同,比如在加锁时机、冲突处理、性能影响等方面。还要提到实际应用中的例子,比如数据库和编程语言中的实现,这样用户能更容易理解。 可能需要注意的点是,避免使用过于专业的术语,或者如果用了术语需要简单解释。例如CAS可能需要稍微说明一下,或者版本号机制如何工作。同时,需要指出乐观锁可能存在的ABA问题,但这个问题可能对初学者来说比较复杂,可以简单提及或省略,视情况而定。 总结来说,结构应该是先分别介绍乐观锁和悲观锁,再对比它们的区别和适用场景,最后给出使用建议。这样用户能够有一个全面的了解,并知道在什么情况下选择哪种锁机制。</think>乐观锁和悲观锁是两种常见的并发控制机制,用于解决多线程或多用户场景下的数据竞争问题。它们的核心区别在于**对冲突的预期和处理方式**。 --- ### **一、悲观锁(Pessimistic Locking)** **核心思想**:默认认为并发操作会发生冲突,因此**先加锁再操作**,确保同一时间只有一个线程/用户能访问资源。 #### **特点**: 1. **主动加锁**:操作数据前先获取锁(如数据库的行锁、表锁,Java的`synchronized`关键字)。 2. **阻塞等待**:若锁被占用,其他线程必须等待,可能降低系统吞吐量。 3. **强一致性**:保证数据绝对安全,但可能牺牲性能。 #### **常见实现**: - **数据库**:`SELECT ... FOR UPDATE`(行级锁)。 - **Java**:`synchronized`、`ReentrantLock`。 - **应用场景**:写操作频繁、冲突概率高的场景(如账户扣款)。 --- ### **二、乐观锁(Optimistic Locking)** **核心思想**:默认认为并发冲突概率低,因此**先操作,提交时再检查冲突**。若冲突发生,则通过重试或回滚解决。 #### **特点**: 1. **无锁设计**:操作时无需加锁,仅在提交时检查版本号或时间戳。 2. **非阻塞**:线程无需等待,适合高并发读操作。 3. **冲突检测**:通过版本号(Version)、CAS(Compare and Swap)等机制实现。 #### **常见实现**: - **数据库**:通过`version`字段,更新时检查版本是否匹配(例如:`UPDATE table SET value=1, version=version+1 WHERE id=2 AND version=old_version`)。 - **Java**:`AtomicInteger`(基于CAS)、`StampedLock`的乐观读。 - **应用场景**:读多写少、冲突概率低的场景(如库存查询)。 --- ### **三、对比与选择** | **维度** | **悲观锁** | **乐观锁** | |----------------|-----------------------------------|-----------------------------------| | **冲突预期** | 认为冲突频繁发生 | 认为冲突较少发生 | | **加锁方式** | 操作前显式加锁 | 无锁,依赖版本号或CAS机制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值