深入探究 ReentrantLock 的应用和原理

简介: 深入探究 ReentrantLock 的应用和原理

博主介绍: ✌博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家✌

Java知识图谱点击链接:体系化学习Java(Java面试专题)

💕💕 感兴趣的同学可以收藏关注下不然下次找不到哟💕💕

1687699556285.jpg

1、ReentrantLock 简介

ReentrantLock 是 Java 中的一个锁实现,它提供了与 synchronized 关键字类似的功能,但是它更加灵活和可扩展。ReentrantLock 是一个可重入锁,也就是说同一个线程可以多次获得同一个锁而不会发生死锁。

ReentrantLock 的主要方法包括:

  1. lock():获取锁,如果锁已经被其他线程获取,则当前线程会被阻塞。
  2. unlock():释放锁。
  3. tryLock():尝试获取锁,如果锁已经被其他线程获取,则立即返回 false。
  4. tryLock(long timeout, TimeUnit unit):尝试获取锁,如果锁已经被其他线程获取,则当前线程会被阻塞,直到超时或者获取到锁。
  5. newCondition():创建一个 Condition 对象,用于实现等待/通知机制。

ReentrantLock 的优点之一是它提供了更细粒度的锁控制,可以通过 lockInterruptibly() 方法实现可中断的锁获取,也可以通过 tryLock() 方法实现非阻塞的锁获取。此外,ReentrantLock 还支持公平锁和非公平锁,可以通过构造函数来指定锁的类型。

需要注意的是,使用 ReentrantLock 时需要手动释放锁,否则可能会导致死锁。通常使用 try-finally 语句块来确保锁一定会被释放。

2、ReentrantLock 的使用

2.1、基本使用

首先看一段没有加锁的代码:

package com.pany.camp.reentrantLock;

import java.util.concurrent.locks.ReentrantLock;

/**
 *
 * @description:  ReentrantLock 基本使用案例
 * @copyright: @Copyright (c) 2022 
 * @company: Aiocloud
 * @author: pany
 * @version: 1.0.0 
 * @createTime: 2023-06-25 20:47
 */
public class ReentrantLockDemo {
   
   

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
   
   
        Thread t1 = new Thread(() -> {
   
   
            try {
   
             
                System.out.println("Thread 1 acquired the start.");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            } finally {
   
    
                System.out.println("Thread 1 released the end.");
            }
        });

        Thread t2 = new Thread(() -> {
   
   
            try {
   
   
                System.out.println("Thread 2 acquired the start.");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            } finally {
   
   
                System.out.println("Thread 2 released the end.");
            }
        });

        t1.start();
        t2.start();
    }
}

执行后的结果如下:

Thread 2 acquired the start.
Thread 1 acquired the start.
Thread 2 released the end.
Thread 1 released the end.

Process finished with exit code 0

两个线程同时开始,同时结束。接下来我们使用 ReentrantLock 加上锁,看看效果。

package com.pany.camp.reentrantLock;

import java.util.concurrent.locks.ReentrantLock;

/**
 *
 * @description:  ReentrantLock 基本使用案例
 * @copyright: @Copyright (c) 2022 
 * @company: Aiocloud
 * @author: pany
 * @version: 1.0.0 
 * @createTime: 2023-06-25 20:47
 */
public class ReentrantLockDemo {
   
   

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
   
   
        Thread t1 = new Thread(() -> {
   
   
            try {
   
   
                lock.lock();
                System.out.println("Thread 1 acquired the start.");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            } finally {
   
   
                lock.unlock();
                System.out.println("Thread 1 released the end.");
            }
        });

        Thread t2 = new Thread(() -> {
   
   
            try {
   
   
                lock.lock();
                System.out.println("Thread 2 acquired the start.");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            } finally {
   
   
                lock.unlock();
                System.out.println("Thread 2 released the end.");
            }
        });

        t1.start();
        t2.start();
    }
}

输出结果如下:

Thread 1 acquired the start.
Thread 1 released the end.
Thread 2 acquired the start.
Thread 2 released the end.

Process finished with exit code 0

我们创建了一个 ReentrantLock 对象,并在两个线程中使用它来控制共享资源的访问。在每个线程的 run 方法中,我们首先使用 lock() 方法获取锁,然后执行一些临界区代码,最后使用 unlock() 方法释放锁。由于 ReentrantLock 是可重入锁,因此同一个线程可以多次获取同一个锁,不会发生死锁。

2.2、tryLock 的使用

接下来我们看下 tryLock 的使用:

package com.pany.camp.reentrantLock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 *
 * @description:  tryLock 的使用
 * @copyright: @Copyright (c) 2022 
 * @company: Aiocloud
 * @author: pany
 * @version: 1.0.0 
 * @createTime: 2023-06-25 20:55
 */
public class TryLockDemo {
   
   
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
   
   
        Thread t1 = new Thread(() -> {
   
   
            try {
   
   
                if (lock.tryLock(2000, TimeUnit.MILLISECONDS)) {
   
   
                    try {
   
   
                        System.out.println("Thread 1 acquired the lock.");
                        Thread.sleep(3000);
                    } finally {
   
   
                        lock.unlock();
                        System.out.println("Thread 1 released the lock.");
                    }
                } else {
   
   
                    System.out.println("Thread 1 failed to acquire the lock.");
                }
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            }
        });

        Thread t2 = new Thread(() -> {
   
   
            try {
   
   
                if (lock.tryLock(2000, TimeUnit.MILLISECONDS)) {
   
   
                    try {
   
   
                        System.out.println("Thread 2 acquired the lock.");
                        Thread.sleep(1000);
                    } finally {
   
   
                        lock.unlock();
                        System.out.println("Thread 2 released the lock.");
                    }
                } else {
   
   
                    System.out.println("Thread 2 failed to acquire the lock.");
                }
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            }
        });

        t1.start();
        t2.start();
    }
}

我们首先创建了一个 ReentrantLock 对象,并在两个线程中使用它来控制共享资源的访问。然后,在每个线程的 run 方法中,我们使用 tryLock 方法尝试获取锁,等待 2000 毫秒。如果获取锁成功,就执行临界区代码,然后释放锁。如果获取锁失败,就输出一条失败信息。
输出结果如下:

Thread 1 acquired the lock.
Thread 2 failed to acquire the lock.
Thread 1 released the lock.

Process finished with exit code 0

这表明线程 1 成功获取了锁并执行了临界区代码,然后释放了锁。而线程 2 在等待 2000 毫秒后仍然无法获取锁,因此输出了一条失败信息。需要注意的是,在使用 tryLock 方法时,如果获取锁失败,线程并不会一直等待,而是会立即返回。这使得我们可以避免线程因为长时间等待锁而被阻塞的情况。

2.3、Condition 实现等待/通知机制

案例如下:

package com.pany.camp.reentrantLock;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 *
 * @description:  Condition 实现等待/通知机制
 * @copyright: @Copyright (c) 2022 
 * @company: Aiocloud
 * @author: pany
 * @version: 1.0.0 
 * @createTime: 2023-06-25 20:59
 */
public class ConditionDemo {
   
   
    private static final ReentrantLock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();
    private static boolean flag = false;

    public static void main(String[] args) {
   
   
        Thread t1 = new Thread(() -> {
   
   
            lock.lock();
            try {
   
   
                while (!flag) {
   
   
                    System.out.println("Thread 1 is waiting.");
                    condition.await();
                }
                System.out.println("Thread 1 is notified.");
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            } finally {
   
   
                lock.unlock();
            }
        });

        Thread t2 = new Thread(() -> {
   
   
            lock.lock();
            try {
   
   
                flag = true;
                System.out.println("Thread 2 is notifying.");
                condition.signal();
            } finally {
   
   
                lock.unlock();
            }
        });

        t1.start();
        t2.start();
    }
}

我们首先创建了一个 ReentrantLock 对象和一个 Condition 对象。然后,在线程 1 中,我们使用 while 循环来等待条件 flag 的满足,并在每次循环中使用 condition.await() 方法来释放锁并等待通知。在线程 2 中,我们设置条件 flag 为 true,并使用 condition.signal() 方法来通知线程 1,然后释放锁。

输出结果如下:

Thread 1 is waiting.
Thread 2 is notifying.
Thread 1 is notified.

Process finished with exit code 0

线程 1 在等待 flag 条件满足时被阻塞,并在线程 2 通知后被唤醒。需要注意的是,在使用 Condition 实现等待/通知机制时,必须在获取锁之后才能使用 await 和 signal 方法,否则会抛出 IllegalMonitorStateException 异常。

2.4、多 Condition 使用

代码如下:

package com.pany.camp.reentrantLock;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 *
 * @description:  多 Condition 使用
 * @copyright: @Copyright (c) 2022 
 * @company: Aiocloud
 * @author: pany
 * @version: 1.0.0 
 * @createTime: 2023-06-25 21:06
 */
public class MultiConditionDemo {
   
   
    private static final ReentrantLock lock = new ReentrantLock();
    private static final Condition condition1 = lock.newCondition();
    private static final Condition condition2 = lock.newCondition();
    private static boolean flag = false;

    public static void main(String[] args) {
   
   
        Thread t1 = new Thread(() -> {
   
   
            lock.lock();
            try {
   
   
                while (!flag) {
   
   
                    System.out.println("Thread 1 is waiting for condition 1.");
                    condition1.await();
                }
                System.out.println("Thread 1 is notified for condition 1.");
                condition2.signal();
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            } finally {
   
   
                lock.unlock();
            }
        });

        Thread t2 = new Thread(() -> {
   
   
            lock.lock();
            try {
   
   
                flag = true;
                System.out.println("Thread 2 is notifying for condition 1.");
                condition1.signal();
                while (!flag) {
   
   
                    System.out.println("Thread 2 is waiting for condition 2.");
                    condition2.await();
                }
                System.out.println("Thread 2 is notified for condition 2.");
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            } finally {
   
   
                lock.unlock();
            }
        });

        t1.start();
        t2.start();
    }
}

我们创建了一个 ReentrantLock 对象和两个 Condition 对象。在线程 1 中,我们使用 while 循环来等待条件 flag 的满足,并在每次循环中使用 condition1.await() 方法来释放锁并等待通知。在线程 2 中,我们设置条件 flag 为 true,并使用 condition1.signal() 方法来通知线程 1,然后等待条件 flag2 的满足,使用 condition2.await() 方法来释放锁并等待通知。在线程 1 中,我们使用 condition2.signal() 方法来通知线程 2。

输出如下:

Thread 1 is waiting for condition 1.
Thread 2 is notifying for condition 1.
Thread 2 is notified for condition 2.
Thread 1 is notified for condition 1.

Process finished with exit code 0

线程 1 在等待条件 flag1 满足时被阻塞,并在线程 2 通知后被唤醒。然后线程 1 通知线程 2 等待的条件 flag2,线程 2 在等待条件 flag2 满足时被阻塞,并在线程 1 通知后被唤醒。需要注意的是,在使用多个 Condition 实现等待/通知机制时,每个 Condition 都应该有对应的等待和通知逻辑,并且必须在获取锁之后才能使用 await 和 signal 方法,否则会抛出 IllegalMonitorStateException 异常。

2.5、公平锁

公平锁是一种锁的实现方式,它可以保证多个线程按照它们请求锁的顺序获取锁。也就是说,如果一个线程请求锁,但是锁已经被其他线程占用了,那么这个线程将被放置在一个等待队列中,直到它的请求被处理并且锁被释放。然后,等待队列中的下一个线程才能获取锁。这种方式可以保证线程获取锁的公平性,避免线程饥饿现象的发生。但是,公平锁的实现可能会导致性能下降,因为它需要维护等待队列并进行线程切换。因此,在实现锁时需要根据具体情况来选择公平锁或非公平锁。

以下就是公平锁的使用:

package com.pany.camp.reentrantLock;

import java.util.concurrent.locks.ReentrantLock;

/**
 *
 * @description:  公平锁
 * @copyright: @Copyright (c) 2022 
 * @company: Aiocloud
 * @author: pany
 * @version: 1.0.0 
 * @createTime: 2023-06-25 21:09
 */
public class FairLockDemo {
   
   
    private static final ReentrantLock lock = new ReentrantLock(true);

    public static void main(String[] args) {
   
   
        Thread t1 = new Thread(() -> {
   
   
            lock.lock();
            try {
   
   
                System.out.println("Thread 1 acquired the lock.");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            } finally {
   
   
                lock.unlock();
            }
        });

        Thread t2 = new Thread(() -> {
   
   
            lock.lock();
            try {
   
   
                System.out.println("Thread 2 acquired the lock.");
            } finally {
   
   
                lock.unlock();
            }
        });

        t1.start();
        t2.start();
    }
}

2.6、可重入锁

可重入锁是一种线程同步机制,也称为递归锁。它允许线程多次获取同一个锁,而不会出现死锁的情况。当一个线程获取了锁后,它可以再次获取锁,而不会被阻塞。当线程释放锁时,它必须释放相同次数的锁,否则其他线程将无法获取锁。可重入锁通常用于解决递归函数或多个方法之间的互斥访问问题。Java 中的 ReentrantLock 就是一种可重入锁的实现。

以下是可重入锁的代码:

package com.pany.camp.reentrantLock;

import java.util.concurrent.locks.ReentrantLock;

/**
 *
 * @description:  可重入锁
 * @copyright: @Copyright (c) 2022 
 * @company: Aiocloud
 * @author: pany
 * @version: 1.0.0 
 * @createTime: 2023-06-25 21:11
 */
public class ReentrantLockDemo1 {
   
   
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
   
   
        Thread t1 = new Thread(() -> {
   
   
            lock.lock();
            try {
   
   
                System.out.println("Thread 1 acquired the lock.");
                Thread.sleep(1000);
                lock.lock();
                try {
   
   
                    System.out.println("Thread 1 acquired the lock again.");
                } finally {
   
   
                    lock.unlock();
                }
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            } finally {
   
   
                lock.unlock();
            }
        });

        Thread t2 = new Thread(() -> {
   
   
            lock.lock();
            try {
   
   
                System.out.println("Thread 2 acquired the lock.");
            } finally {
   
   
                lock.unlock();
            }
        });
        t1.start();
        t2.start();
    }
}

3、究根问底 - ReentrantLock 源码分析

  1. 构造函数

ReentrantLock 的构造函数有两个重载版本,分别是无参构造函数和带公平性参数的构造函数。其中,公平性参数用于指定是否使用公平锁,默认为非公平锁。

public ReentrantLock() {
   
   
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
   
   
    sync = fair ? new FairSync() : new NonfairSync();
}
  1. 加锁与解锁

ReentrantLock 的加锁和解锁操作都是通过内部类 Sync 来实现的。Sync 是一个抽象类,它有两个子类 NonfairSync 和 FairSync,分别对应非公平锁和公平锁。

abstract static class Sync extends AbstractQueuedSynchronizer {
   
   
    // 加锁
    abstract void lock();
     // 解锁
    protected final boolean tryRelease(int releases) {
   
   
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
   
   
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
}

static final class NonfairSync extends Sync {
   
   
    // 加锁
    final void lock() {
   
   
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
     // 尝试加锁
    protected final boolean tryAcquire(int acquires) {
   
   
        return nonfairTryAcquire(acquires);
    }
}

static final class FairSync extends Sync {
   
   
    // 加锁
    final void lock() {
   
   
        acquire(1);
    }
     // 尝试加锁
    protected final boolean tryAcquire(int acquires) {
   
   
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
   
   
            if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
   
   
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) {
   
   
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

在 NonfairSync 中,加锁操作首先尝试使用 compareAndSetState 方法将 state 从 0 修改为 1,如果成功则表示获取锁成功,否则就调用 acquire 方法进入等待队列。在 FairSync 中,加锁操作直接调用 acquire 方法进入等待队列,如果前面有等待的线程,则当前线程会进入等待队列,直到前面所有线程都执行完毕后才会获取锁。

在解锁操作中,会判断当前线程是否为持有锁的线程,如果不是则抛出 IllegalMonitorStateException 异常,否则就将 state 减去 releases,并判断是否需要释放锁。如果 state 减去 releases 后等于 0,则表示当前线程已经释放了锁,需要将 exclusiveOwnerThread 置为 null。

  1. 其他方法

除了加锁和解锁操作之外,ReentrantLock 还提供了一些其他方法,如 tryLock、tryLock(long time, TimeUnit unit)、lockInterruptibly、newCondition 等。

其中,tryLock 方法用于尝试获取锁,如果成功则返回 true,否则返回 false。tryLock(long time, TimeUnit unit) 方法用于在指定的时间内尝试获取锁,如果成功则返回 true,否则返回 false。lockInterruptibly 方法用于可中断地获取锁,如果当前线程被中断则会抛出 InterruptedException 异常。newCondition 方法用于创建一个 Condition 对象,用于实现线程间的协作。

4、ReentrantLock 实际应用

ReentrantLock是Java中的一个锁实现类,它可以用于多线程并发控制,常用于以下场景:

  1. 临界区控制
    当多个线程需要同时访问共享资源时,需要使用锁来保证线程安全。ReentrantLock可以用于控制临界区的访问,避免多个线程同时访问共享资源导致的数据竞争和线程安全问题。

  2. 死锁避免
    ReentrantLock支持可重入性,即同一个线程可以多次获取该锁。这种特性可以避免死锁的发生,因为如果一个线程已经持有了该锁,再次获取该锁时不会被阻塞,而是直接返回。

  3. 公平锁控制
    ReentrantLock提供了公平锁和非公平锁两种模式。公平锁可以保证多个线程按照先后顺序获取锁,避免线程饥饿问题。非公平锁则不保证线程获取锁的顺序,可能会导致某些线程一直无法获取到锁。

  4. 条件变量控制

ReentrantLock还提供了Condition接口,可以用于实现线程间的协作。通过Condition接口的await()和signal()方法,可以实现线程的等待和唤醒操作,从而更加灵活地控制线程的执行顺序。

总之,ReentrantLock是一个非常实用的多线程并发控制工具,可以用于各种场景下的线程同步和互斥操作。

1686494501743.jpg

💕💕 本文由激流丶创作,原创不易,感谢支持!
💕💕喜欢的话记得点赞收藏啊!

目录
相关文章
|
6月前
|
存储 监控 安全
吃透synchronized实现原理
吃透synchronized实现原理
86 0
|
3月前
|
Java
【多线程面试题十六】、谈谈ReentrantLock的实现原理
这篇文章解释了`ReentrantLock`的实现原理,它基于Java中的`AbstractQueuedSynchronizer`(AQS)构建,通过重写AQS的`tryAcquire`和`tryRelease`方法来实现锁的获取与释放,并详细描述了AQS内部的同步队列和条件队列以及独占模式的工作原理。
【多线程面试题十六】、谈谈ReentrantLock的实现原理
|
2月前
|
Java
JAVA并发编程ReentrantLock核心原理剖析
本文介绍了Java并发编程中ReentrantLock的重要性和优势,详细解析了其原理及源码实现。ReentrantLock作为一种可重入锁,弥补了synchronized的不足,如支持公平锁与非公平锁、响应中断等。文章通过源码分析,展示了ReentrantLock如何基于AQS实现公平锁和非公平锁,并解释了两者的具体实现过程。
|
5月前
|
算法 安全 Java
Java并发基石ReentrantLock:深入解读其原理与实现
Java并发基石ReentrantLock:深入解读其原理与实现
|
6月前
|
安全 Java 开发者
Java并发编程中的线程安全性探究
在Java编程中,线程安全性是一个至关重要的问题,涉及到多线程并发访问共享资源时可能出现的数据竞争和不一致性问题。本文将深入探讨Java并发编程中的线程安全性,介绍常见的线程安全性问题以及解决方法,帮助开发者更好地理解和应对在多线程环境下的挑战。
|
存储 安全 Java
synchronized原理详解(通俗易懂超级好)
当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
synchronized原理详解(通俗易懂超级好)
|
存储 安全 Java
java并发原理实战(5)--线程安全性问题和synchronized原理理解
java并发原理实战(5)--线程安全性问题和synchronized原理理解
java并发原理实战(5)--线程安全性问题和synchronized原理理解
|
Java 调度
JUC系列(三) | Lock 锁机制详解 代码理论相结合
JUC系列(三) | Lock 锁机制详解 代码理论相结合
159 0
JUC系列(三) | Lock 锁机制详解 代码理论相结合
ReentrantLock核心原理,绝对干货
那我们开始吧ReentrantLock 中文我们叫做可重入互斥锁,可重入的意思是同一个线程可以对同一个共享资源重复的加锁或释放锁,互斥就是 AQS 中的排它锁的意思,只允许一个线程获得锁。简单应用ReentrantLock 的使用相比较 synchronized 会稍微繁琐一点,所谓显示锁,也就是你在代码中需要主动的去进行 lock 操作。一般来讲我们可以按照下面的方式使用 ReentrantLocklock.lock () 就是在显式的上锁。上锁后,下面的代码块一定要放到 try 中,并且要结合 finally 代码块调用lock.unlock ()来释放锁,否则一定 doSomething