Java的锁—彻底理解重入锁(ReentrantLock)

本文详细介绍了ReentrantLock的工作原理及使用方式,包括重入特性、线程中断响应、有限时间等待锁、公平锁与非公平锁的区别以及Condition条件变量的应用。

重入锁简单理解就是对同一个线程而言,它可以重复的获取锁。例如这个线程可以连续获取两次锁,但是释放锁的次数也一定要是两次。下面是一个简单例子:

public class ReenterLock {

    private static ReentrantLock lock = new ReentrantLock();

    private static int i = 0;

    // 循环1000000次
    private static Runnable runnable = () -> IntStream.range(0, 1000000).forEach((j) -> {
        lock.lock();
        try {
            i++;
        } finally {
            lock.unlock();
        }
    });

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        // 利用join,等thread1,thread2结束后,main线程才继续运行,并打印 i
        thread1.join();
        thread2.join();
        // 利用lock保护的 i,最终结果为 2000000,如果不加,则值肯定小于此数值
        System.out.println(i);
    }
}

从上面的代码可以看到,相比于synchronized,开发者必须手动指定锁的位置和什么时候释放锁,这样必然增加了灵活性。

线程中断响应

如果线程阻塞于synchronized,那么要么获取到锁,继续执行,要么一直等待。重入锁提供了另一种可能,就是中断线程。下面的例子是利用两个线程构建一个死锁,然后中断其中一个线程,使另一个线程获取锁的例子:

public class ReenterLockInterrupt {
    private static ReentrantLock lock = new ReentrantLock();

    private static Runnable runnable = () -> {
        try {
            // 利用 lockInterruptibly 申请锁,这是可以进中断申请的申请锁操作
            lock.lockInterruptibly();
            // 睡眠20秒,在睡眠结束之前,main方法里要中断thread2的获取锁操作
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            String threadName = Thread.currentThread().getName();
            // 中断后抛出异常,最后要释放锁
            // 如果是线程1则释放锁,因为线程2就没拿到锁,所以不用释放
            if ("Thread-1".equals(threadName)) lock.unlock();
            System.out.println(threadName+" 停止");
        }
    };

    public static void main(String[] args) {
        Thread thread1 = new Thread(runnable, "thread-1");
        Thread thread2 = new Thread(runnable, "thread-2");
        thread1.start();

        // 让主线程停一下,让thread1获取锁后再启动thread2
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // 这里什么也不做
        }

        thread2.start();
        thread2.interrupt();
    }
}

thread-1拿到锁之后,线程即持有锁并等待20秒,然后thread-2启动,并没有拿到锁,这时候中断thread-2线程,线程2退出。

有限时间的等待锁

顾名思义,简单理解就是在指定的时间内如果拿不到锁,则不再等待锁。当持有锁的线程出问题导致长时间持有锁的时候,你不可能让其他线程永远等待其释放锁。下面是一个例子:

public class ReenterTryLock {
    private static ReentrantLock reenterLock = new ReentrantLock();

    private static Runnable runnable = () -> {
        try {
            // tryLock()方法会返回一个布尔值,获取锁成功则为true
            if (reenterLock.tryLock(3, TimeUnit.SECONDS)) {
                Thread.sleep(5000);
            } else {
                System.out.println(Thread.currentThread().getName() + "获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 最后,如果当前前程在持有锁,则释放锁
            if (reenterLock.isHeldByCurrentThread()) {
                System.out.println(Thread.currentThread().getName() + "释放锁了");
                reenterLock.unlock();
            }
        }
    };

    public static void main(String[] args) {
        Thread thread1 = new Thread(runnable, "thread-1");
        Thread thread2 = new Thread(runnable, "thread-2");

        thread1.start();
        thread2.start();
    }
}

这里使用tryLock()第一个获取锁的线程,会停止5秒。而获取锁的设置为3秒获取不到锁则放弃,所以第二个去尝试获取锁的线程是获取不到锁而被迫停止的。如果tryLock()方法不传入任何参数,那么获取锁的线程不会等待锁,则立即返回false。

公平锁与非公平锁

当一个线程释放锁时,其他等待的线程则有机会获取锁,如果是公平锁,则分先来后到的获取锁,如果是非公平锁则谁抢到锁算谁的,这就相当于排队买东西和不排队买东西是一个道理。Java的synchronized关键字就是非公平锁

那么重入锁ReentrantLock()是公平锁还是非公平锁?

重入锁ReentrantLock()是可以设置公平性的,可以参考其构造方法:

// 通过传入一个布尔值来设置公平锁,为true则是公平锁,false则为非公平锁
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

构建一个公平锁需要维护一个有序队列,如果实际需求用不到公平锁则不需要使用公平锁。下面用一个例子来演示公平锁与非公平锁的区别:

public class ReenterTryLockFair {
    // 分别设置公平锁和非公平锁,分析打印结果
    private static ReentrantLock lock = new ReentrantLock(true);

    private static Runnable runnable = () -> {
        while (true) {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " 获取了锁");
            } finally {
                lock.unlock();
            }
        }
    };

    public static void main(String[] args) {
        Thread thread1 = new Thread(runnable, "thread---1");
        Thread thread2 = new Thread(runnable, "thread---2");
        Thread thread3 = new Thread(runnable, "thread---3");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

当设置为true即公平锁的时候,可以看到打印非常规律,截取一段儿打印结果:

thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁

可以看到,都是thread–1,thread–2,thread–3,无限循环下去,如果设置的为非公平锁,打印结果就混乱没有规律了:

thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---1 获取了锁

Condition

同jdk中的等待/通知机制类似,只不过Condition是用在重入锁这里的。有了Condition,线程就可以在合适的时间等待,在合适的时间继续执行。

Condition接口包含以下方法:

// 让当前线程等待,并释放锁
void await() throws InterruptedException;
// 和await类似,但在等待过程中不会相应中断
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒等待中的线程
void signal();
// 唤醒等待中的所有线程
void signalAll();

下面是一个简单示例:

public class ReenterLockCondition {
    private static ReentrantLock lock = new ReentrantLock();

    private static Condition condition = lock.newCondition();

    private static Runnable runnable = () -> {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "进入等待。。");
            condition.await();
            System.out.println(Thread.currentThread().getName() + "继续执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    };

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(runnable, "thread--1");
        thread.start();

        Thread.sleep(2000);

        lock.lock();
        condition.signal();
        System.out.println("主线程发出信号");
        lock.unlock();
    }
}

thread–1启动,拿到锁,然后进入等待并且释放锁,2秒后,主线程拿到锁,然后发出信号并释放锁,最后,thread–1继续执行。下面是打印结果:

thread--1进入等待。。
主线程发出信号
thread--1继续执行
<think>嗯,我现在需要谈谈对AQS的理解,以及AQS如何实现可重入锁。首先,我得回忆一下AQS是什么。AQS全称是AbstractQueuedSynchronizer,是Java并发包中的一个核心框架,很多同步器比如ReentrantLock、Semaphore都是基于它实现的。对吧? 那AQS的基本原理是什么呢?我记得它内部维护了一个状态变量state和一个CLH队列,这个队列用来管理等待获取资源的线程。state的具体含义由子类决定,比如在可重入锁中,state表示重入次数。当线程尝试获取时,如果state为0,说明未被占用,线程可以获取并将state加1。如果state不为0,且当前线程是持有线程,那么state会继续增加,这就是可重入的实现。如果不是当前线程,那么线程会被放入队列中等待。 不过,这里可能有些细节需要确认。比如,CLH队列具体是怎么工作的?是不是一个双向链表,每个节点代表一个等待的线程?节点可能有不同的模式,比如独占模式或共享模式。对于可重入锁来说,应该是独占模式,因为同一时间只能有一个线程持有。 然后,关于可重入性。可重入锁允许同一个线程多次获取,每次获取state就递增,释放的时候递减,直到state为0才完全释放。这里的关键是,AQS如何识别当前线程是否是的持有者。应该是通过某种方式记录当前持有线程,比如在AbstractOwnableSynchronizer中有一个exclusiveOwnerThread变量。当线程尝试获取时,会检查当前线程是否是exclusiveOwnerThread,如果是,就允许重入,state加1。 具体到ReentrantLock的实现,它的内部类Sync继承了AQS,并实现了tryAcquire和tryRelease方法。在tryAcquire中,首先获取当前state,如果是0,则尝试CAS设置state为1,并设置当前线程为持有者。如果state不为0,检查当前线程是否是持有者,如果是,state加1,否则获取失败。tryRelease则是每次释放时state减1,直到减到0时,才将持有者线程置为null。 可能还需要提到公平和非公平的区别。非公平在尝试获取时直接尝试CAS,而公平则会先检查队列中是否有等待的线程,如果有则排队。这也涉及到AQS中的acquire方法,可能调用tryAcquire的不同实现。 但有没有哪里理解错了?比如,AQS的acquire方法是如何处理中断的?或者可重入锁重入次数有没有限制?应该是理论上可以重入Integer.MAX_VALUE次,因为state是int类型。不过实际中不太可能达到这个次数。 总结一下,AQS通过state和CLH队列管理线程的同步状态,而可重入锁利用state记录重入次数,并通过检查当前线程是否为持有者来实现重入ReentrantLock中的Sync类扩展AQS,实现tryAcquire和tryRelease来控制的获取和释放。</think>AQS(AbstractQueuedSynchronizer)是Java并发包的核心同步框架,其实现可重入锁的核心逻辑如下: --- ### **1. AQS核心机制** - **同步状态**:通过`volatile int state`字段记录同步状态(如重入次数) - **CLH队列**:通过双向链表实现的线程等待队列(FIFO结构) - **模板方法**:子类需实现`tryAcquire`和`tryRelease`等钩子方法 数学描述: $$ \text{state} = \begin{cases} 0 & \text{未被占用} \\ n & \text{被同一线程重入n次} \end{cases} $$ --- ### **2. 可重入锁实现原理** #### **(1) 获取(lock())** ```java final void lock() { if (compareAndSetState(0, 1)) // 尝试直接获取 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // 进入AQS队列 } ``` - **非公平尝试**:直接通过CAS(CompareAndSwap)修改state - **重入逻辑**:若当前线程是持有者,则state递增 $$ \text{newState} = \text{currentState} + 1 $$ #### **(2) 释放(unlock())** ```java 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); // 不需要CAS(只有持有线程能调用释放) return free; } ``` - **状态递减**:每次释放state减1,直到归零时彻底释放 $$ \text{state} \leftarrow \text{state} - 1 $$ --- ### **3. 关键设计细节** 1. **线程标识**:通过`AbstractOwnableSynchronizer`的`exclusiveOwnerThread`字段记录持有者 2. **重入上限**:state使用int类型,最大支持$2^{31}-1$次重入(实际场景不可能达到) 3. **公平性控制**: - **非公平**:新线程可直接插队尝试获取 - **公平**:必须严格按队列顺序获取(通过`hasQueuedPredecessors()`检查) --- ### **4. 数学验证可重入性** 假设线程T连续获取3次: 1. 首次获取:$\text{state} = 0 \xrightarrow{\text{CAS}} 1$ 2. 第二次获取:$\text{state} = 1 + 1 = 2$(重入) 3. 第三次获取:$\text{state} = 2 + 1 = 3$(再次重入) 每次释放操作使state递减,直到$\text{state}=0$时唤醒后续等待线程。 --- ### **5. 典型应用** ```java ReentrantLock lock = new ReentrantLock(); lock.lock(); try { // 可重入代码块 lock.lock(); // 再次获取(state=2) // ... } finally { lock.unlock(); // state=1 lock.unlock(); // state=0(完全释放) } ``` --- ### **总结** AQS通过`state`状态和CLH队列实现了同步基础框架,可重入锁通过以下设计实现: 1. **状态递增/递减**:记录线程重入次数 2. **线程持有者检查**:确保只有拥有者能重入 3. **CAS原子操作**:保证状态修改的线程安全 这种设计使得ReentrantLock等同步工具在保证线程安全的同时,具备高效的性能表现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值