java并发编程 11:JUC之ReentrantLock使用与原理

news2025/2/3 9:12:51

目录

  • 使用
    • 可重入
    • 可打断
    • 锁超时
    • 公平锁
    • 条件变量
  • 原理
    • 非公平锁实现原理
      • 源码
      • 流程
    • 锁重入原理
    • 可打断原理与不可打断原理
    • 公平锁原理
    • 条件变量原理
      • await流程
      • signal流程

使用

ReentrantLock是可冲入锁,与 synchronized 一样,都支持可重入。但是相对于 synchronized 它具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

ReentrantLock实现了Lock接口。

基本语法

// 获取锁
reentrantLock.lock();
try {
 // 临界区
} finally {
 // 释放锁
 reentrantLock.unlock();
}

注意:锁【lock.lock】必须紧跟try代码块,且unlock要放到finally第一行。

下面来一一看下他的几个特点

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

示例:

package up.cys.chapter03;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

@Slf4j
public class ReentrantLockTest01 {
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        method1();
    }

    public static void method1() {
        lock.lock();
        try {
            log.info("execute method1");
            method2();
        } finally {
            lock.unlock();
        }
    }
    public static void method2() {
        lock.lock();
        try {
            log.info("execute method2");
            method3();
        } finally {
            lock.unlock();
        }
    }
    public static void method3() {
        lock.lock();
        try {
            log.info("execute method3");
        } finally {
            lock.unlock();
        }
    }
}

输出:

2023-06-03 15:59:23,694 - 0    INFO  [main] up.cys.chapter03.ReentrantLockTest01:23  - execute method1
2023-06-03 15:59:23,701 - 7    INFO  [main] up.cys.chapter03.ReentrantLockTest01:32  - execute method2
2023-06-03 15:59:23,701 - 7    INFO  [main] up.cys.chapter03.ReentrantLockTest01:41  - execute method3

可打断

可打断的意思是,再获取锁的过程中,可以打断,不再去获取锁。

获取锁时需要使用lock.lockInterruptibly()代替lock.lock()

示例:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

@Slf4j
public class ReentrantLockTest02 {
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.info("子线程启动...");
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.info("等锁的过程中被打断");
                return;
            }
            try {
                log.info("子线程获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        try {
            log.info("主线程获得了锁");
            t1.start();
            Thread.sleep(1000);
            t1.interrupt();
            log.info("主线程执行打断");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }


    }
}

输出如下:

2023-06-03 17:10:36,817 - 0    INFO  [main] up.cys.chapter03.ReentrantLockTest02:35  - 主线程获得了锁
2023-06-03 17:10:36,823 - 6    INFO  [t1] up.cys.chapter03.ReentrantLockTest02:18  - 子线程启动...
2023-06-03 17:10:37,830 - 1013 INFO  [main] up.cys.chapter03.ReentrantLockTest02:39  - 主线程执行打断
2023-06-03 17:10:37,833 - 1016 INFO  [t1] up.cys.chapter03.ReentrantLockTest02:23  - 等锁的过程中被打断
java.lang.InterruptedException
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:944)
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1263)
	at java.base/java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:317)
	at up.cys.chapter03.ReentrantLockTest02.lambda$main$0(ReentrantLockTest02.java:20)
	at java.base/java.lang.Thread.run(Thread.java:834)

注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断。

锁超时

获取锁时需要使用lock.tryLock()代替lock.lock(),意思是尝试获取锁,如果获取不到,则立即放弃。

还可以使用带参数的方法lock.tryLock(long timeout, TimeUnit unit)来设置尝试获取锁等待的时间。

示例:

import lombok.extern.slf4j.Slf4j;

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


@Slf4j
public class ReentrantLockTest03 {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            log.info("启动...");
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    log.info("获取等待 1s 后失败,返回");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                log.info("获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");
        
        lock.lock();
        try {
            log.info("获得了锁");
            t1.start();
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}

输出如下:

2023-06-03 17:15:54,031 - 0    INFO  [main] up.cys.chapter03.ReentrantLockTest03:38  - 获得了锁
2023-06-03 17:15:54,040 - 9    INFO  [t1] up.cys.chapter03.ReentrantLockTest03:20  - 启动...
2023-06-03 17:15:55,048 - 1017 INFO  [t1] up.cys.chapter03.ReentrantLockTest03:23  - 获取等待 1s 后失败,返回

公平锁

公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序。对于非公平锁,则允许线程“插队”。

synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。

ReentrantLock 默认是不公平的,即有些线程可能一直获取不到锁,出现饥饿。

改为公平锁:

ReentrantLock lock = new ReentrantLock(true);

公平锁会有个队列维护等待线程。公平锁一般没有必要,会降低并发度。

条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet ,当条件不满足时进入 waitSet 等待。ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的。synchronized 是那些不满足条件的线程都在一个条件变量等消息,而 ReentrantLock 支持多个条件变量t,唤醒时也只唤醒自己条件变量下等待的线程。

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行

示例:

package up.cys.chapter03;

import lombok.extern.slf4j.Slf4j;

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

@Slf4j
public class ReentrantLockTest03 {
    static ReentrantLock lock = new ReentrantLock();
    // 条件变量1:等待送烟
    static Condition waitCigaretteQueue = lock.newCondition();
    // 条件变量2:等待早餐
    static Condition waitbreakfastQueue = lock.newCondition();
    static volatile boolean hasCigrette = false;
    static volatile boolean hasBreakfast = false;

    public static void main(String[] args) {
        // 线程1:等到烟才继续执行
        new Thread(() -> {
            lock.lock();
            try {
                log.info("线程1等待烟");
                while (!hasCigrette) {
                    try {
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.info("线程1等到了它的烟");
            } finally {
                lock.unlock();
            }
        }).start();

        // 线程2:等到外卖才继续执行
        new Thread(() -> {
            lock.lock();
            try {
                log.info("线程2等早餐");
                while (!hasBreakfast) {
                    try {
                        waitbreakfastQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.info("线程2等到了它的早餐");
            } finally {
                lock.unlock();
            }
        }).start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 开始送早餐
        sendBreakfast();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 开始送烟
        sendCigarette();
    }

    private static void sendCigarette() {
        lock.lock();
        try {
            log.info("送烟来了");
            hasCigrette = true;
            waitCigaretteQueue.signal();
        } finally {
            lock.unlock();
        }
    }

    private static void sendBreakfast() {
        lock.lock();
        try {
            log.info("送早餐来了");
            hasBreakfast = true;
            waitbreakfastQueue.signal();
        } finally {
            lock.unlock();
        }
    }

}

输出如下:

2023-06-03 17:34:44,999 - 0    INFO  [Thread-0] up.cys.chapter03.ReentrantLockTest03:29  - 线程1等待烟
2023-06-03 17:34:45,010 - 11   INFO  [Thread-1] up.cys.chapter03.ReentrantLockTest03:47  - 线程2等早餐
2023-06-03 17:34:46,004 - 1005 INFO  [main] up.cys.chapter03.ReentrantLockTest03:92  - 送早餐来了
2023-06-03 17:34:46,005 - 1006 INFO  [Thread-1] up.cys.chapter03.ReentrantLockTest03:55  - 线程2等到了它的早餐
2023-06-03 17:34:47,011 - 2012 INFO  [main] up.cys.chapter03.ReentrantLockTest03:81  - 送烟来了
2023-06-03 17:34:47,013 - 2014 INFO  [Thread-0] up.cys.chapter03.ReentrantLockTest03:37  - 线程1等到了它的烟

原理

![]](https://img-blog.csdnimg.cn/4d4dccdf5b084b31a6ad324e5d8b5997.png)

看下上面类图,ReentrantLock实现了Lock接口,同时内部维护了一个同步器Sync

Sync 继承了 AbstractQueuedSynchronizer ,所以 Sync 就具有了锁的框架,根据 AQS 的框架,Sync 只需要实现 AQS 预留的几个方法即可,但 Sync 也只是实现了部分方法,还有一些交给子类 NonfairSync(非公平锁) 和 FairSync(公平锁) 去实现了。

ReentrantLock内部主要利用CAS+AQS队列来实现。

说明:以下源代码版本为JDK11。

非公平锁实现原理

源码

先从构造器开始看,ReentrantLock默认构造器是非公平锁实现:

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

NonfairSync实现的加锁Lock方法实现了加锁,代码如下:

public void lock() {
    sync.acquire(1);
}

然后调用了同步器Sync的acquire(1)方法,源代码如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&  // tryAcquire尝试获取锁,如果失败
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  // 尝试创建一个Node对象,加到等待队列中,如果成功
        selfInterrupt();  // 获取失败并且加入队列成功,就调用自己的Interrupt方法
}

然后下面主要看下获取锁tryAcquire方法,tryAcquire是AQS里的抽象方法,找到非公平锁的实现,其源代码如下:

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

然后就是进入nonfairTryAcquire方法,源代码如下:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
  	// 获取同步器的状态,如果为0,说明锁是空闲的,没有被持有
    int c = getState();
    if (c == 0) {
      	// 使用CAS让当前线程持有锁
        if (compareAndSetState(0, acquires)) {
          	// 如果成功,设置exclusiveOwnerThread为当前线程,返回true
            setExclusiveOwnerThread(current);
            return true;
        }
    }
  	// 如果锁已经被占用,比较是否是当前线程占用的,如果是,准备重复加锁,这就是重入锁的原理
    else if (current == getExclusiveOwnerThread()) {
      	// 让计数器加1,表示重入锁上的加锁次数
        int nextc = c + acquires;
      	// int 是有最大值的,如果小于0,说明超过了最大值,直接报错
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
      	// 设置同步器的状态为nextc
        setState(nextc);
        return true;
    }
  	// 最后返回加锁失败
    return false;
}

回头继续看Sync的acquire(1)方法,当加锁失败,则执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法。

首先是addWaiter(Node.EXCLUSIVE), arg)方法,他返回一个Node对象,内部维护的是一个双向链表

private Node addWaiter(Node mode) {
  	// 初始化节点
    Node node = new Node(mode);

  	// 死循环,知道把新的node添加到了队列中
    for (;;) {
      	// 获取队列尾节点
        Node oldTail = tail;
      	// 如果为节点不为null,则把新的node节点添加到尾部
        if (oldTail != null) {
            node.setPrevRelaxed(oldTail);
          	// 设置新节点为尾节点
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return node;
            }
        // 如果尾结点是null,则调用初始化队列的方法,并把新节点放入尾部
        } else {
            initializeSyncQueue();
        }
    }
}

队列创建完之后作为方法acquireQueued的参数,

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
      	// 有一个死循环一直尝试获取锁
        for (;;) {
          	// 获取前驱节点p
            final Node p = node.predecessor();
          	// 如果前驱节点是头节点,说明自己是第二个节点,那么便有资格去尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node);  // 获取成功,将当前节点设置为head节点
                p.next = null; // help GC
                return interrupted;  //返回interrupted中断过状态
            }
          	// 判断获取失败后是否可以挂起
            // 注意第一个参数是前驱节点,方法会前驱节点的状态设为-1,表示他可以用来唤醒后继节点
            if (shouldParkAfterFailedAcquire(p, node))
                // 若可以,则挂起,并设置interrupted中断状态
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

上面是获取锁的源码。

下面看看释放锁的源码。

首先是unlock方法,调用了syncrelease方法:

public void unlock() {
    sync.release(1);
}

release方法代码如下:

public final boolean release(int arg) {
  	// 调用tryRelease方法来释放锁
    if (tryRelease(arg)) {
        Node h = head;
      	// 如果释放成功,则检查头部是否不为null,并且头部节点不为0
        if (h != null && h.waitStatus != 0)
          	// 如果是,则唤醒下一个节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

里面主要调用了tryRelease方法:

@ReservedStackAccess
protected final boolean tryRelease(int releases) {
  	// 计算当前的状态和要释放的数字的差,主要是因为可重入锁可以多次获取锁,释放时也要释放和加锁的次数一样
    int c = getState() - releases;
  	// 如果当前线程不是锁的持有者就报错
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;  // 是否释放成功了,默认为fasle
  	// 如果c为0,说明锁可以被释放了
    if (c == 0) {
        free = true;  // 释放成功了
      	// 把锁Owner设为null
        setExclusiveOwnerThread(null);
    }
  	// 设置status
    setState(c);
    return free;
}

流程

根据上面的源码,看下整个加锁解锁流程是什么样的。

流程如下:

  1. 第一次加锁

第一个线程Thread-0加锁时,没有竞争,直接调用Lock方法成功,会把exclusiveOwnerThread为Thread-0

  1. 第二个线程Thread-1想要加锁时,如下

在这里插入图片描述

  • 进入nonfairTryAcquire方法
  • getState()获取状态,发现结果是1
  • 然后比较当前线程不是持有锁的线程,加锁失败
  • 接下来进入 addWaiter 逻辑,构造 Node 队列。图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态;Node 的创建是懒惰的;其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程。

在这里插入图片描述

  1. 当前线程进入 acquireQueued 逻辑
  • acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
  • 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
  • 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false

在这里插入图片描述

  1. shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时state 仍为 1,失败

  2. 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次方法返回true

  3. 进入 parkAndCheckInterrupt,就挂起当前线程并检查Interrupt状态, 线程Thread-1 状态就为park了(灰色表示)

在这里插入图片描述

  1. 再次有多个线程经历上述过程竞争失败,变成下面这个样子

在这里插入图片描述

  1. 当Thread-0 释放锁,进入 tryRelease 流程,如果成功,设置 exclusiveOwnerThread 为 null,state = 0

在这里插入图片描述

  1. 当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程,找到队列中离 head 最近的一个 Node(没取消的,取消的为-1),unpark 恢复其运行,本例中即为 Thread-1。
  2. 回到 Thread-1 的 acquireQueued 流程

在这里插入图片描述

如果加锁成功(没有竞争),会设置:

  • exclusiveOwnerThread 为 Thread-1,state = 1
  • head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
  • 原本的 head 因为从链表断开,而可被垃圾回收
  1. 如果这时候有其它线程来竞争(非公平的体现)

    例如这时有 Thread-4 来了

在这里插入图片描述

如果不巧又被 Thread-4 占了先:

  • Thread-4 被设置为 exclusiveOwnerThread,state = 1
  • Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞

锁重入原理

锁的重入原理在上面的源码上就可以看到,主要体现在以下两个方面。

第一个是在上锁的时候,回顾下源码如下:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
  	// 获取同步器的状态,如果为0,说明锁是空闲的,没有被持有
    int c = getState();
    if (c == 0) {
      	// 使用CAS让当前线程持有锁
        if (compareAndSetState(0, acquires)) {
          	// 如果成功,设置exclusiveOwnerThread为当前线程,返回true
            setExclusiveOwnerThread(current);
            return true;
        }
    }
  	// 如果锁已经被占用,比较是否是当前线程占用的,如果是,准备重复加锁,这就是重入锁的原理
    else if (current == getExclusiveOwnerThread()) {
      	// 让计数器加1,表示重入锁上的加锁次数
        int nextc = c + acquires;
      	// int 是有最大值的,如果小于0,说明超过了最大值,直接报错
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
      	// 设置同步器的状态为nextc
        setState(nextc);
        return true;
    }
  	// 最后返回加锁失败
    return false;
}

重点在执行else if时,也就是:

上锁时比较了当前线程是不是锁的持有者,如果是,则会把状态加上acquires,从而记录了锁重入的次数。

然后看下释放锁的时候,回顾下源码如下:

@ReservedStackAccess
protected final boolean tryRelease(int releases) {
  	// 计算当前的状态和要释放的数字的差,主要是因为可重入锁可以多次获取锁,释放时也要释放和加锁的次数一样
    int c = getState() - releases;
  	// 如果当前线程不是锁的持有者就报错
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;  // 是否释放成功了,默认为fasle
  	// 如果c为0,说明锁可以被释放了
    if (c == 0) {
        free = true;  // 释放成功了
      	// 把锁Owner设为null
        setExclusiveOwnerThread(null);
    }
  	// 设置status
    setState(c);
    return free;
}

重点看下方法的第一行,和上锁时相反:

在释放锁时,会把状态status减去releases,也就是获取的次数减去要释放的次数,差就是还剩余的重入的次数

可打断原理与不可打断原理

首先上我们上面的源码Lock是默认是不可打断的,回顾acquireQueued方法源码如下:

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
      	// 有一个死循环一直尝试获取锁
        for (;;) {
          	// 获取前驱节点p
            final Node p = node.predecessor();
          	// 如果前驱节点是头节点,说明自己是第二个节点,那么便有资格去尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node);  // 获取成功,将当前节点设置为head节点
                p.next = null; // help GC
                return interrupted;  //返回interrupted中断过状态
            }
          	// 判断获取失败后是否可以挂起
            // 注意第一个参数是前驱节点,方法会前驱节点的状态设为-1,表示他可以用来唤醒后继节点
            if (shouldParkAfterFailedAcquire(p, node))
                // 若可以,则挂起,并设置interrupted中断状态
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

首先如果获取锁失败,县城会被park住,然后并设置interrupted中断状态,但是此时并没有返回interrupted的状态,还会继续进入循环,直到获取到了锁,才把interrupted状态返回了。

执行完acquireQueued方法返回true后,执行了selfInterrupt方法,才产生了中断。代码如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&  // tryAcquire尝试获取锁,如果失败
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  // 尝试创建一个Node对象,加到等待队列中,如果成功
        selfInterrupt();  // 获取失败并且加入队列成功,就调用自己的Interrupt方法,才产生了中断
}

前面我们知道,可打断,需要获取锁时需要使用lock.lockInterruptibly()代替lock.lock()

lock.lockInterruptibly代码如下:

public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

调用了sync的acquireInterruptibly方法,代码如下:

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
  			// 如果打断标记为真,抛出打断异常
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
          	// 获取锁
            doAcquireInterruptibly(arg);
    }

其中doAcquireInterruptibly方法代码如下:

private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return;
                }
              	// 上面代码与不可打断模式相同,关键在下面
              	// 如果判断获取锁失败后可以挂起,并且检查状态是可打断,就直接抛出InterruptedException异常了
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }

可以看到大部分代码与不可打断模式一样,唯一不一样是当线程获取锁失败后可以挂起,并且可打断,就直接抛出异常了。

公平锁原理

首先看下我们上面说的非公平锁的源码:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
  	// 获取同步器的状态,如果为0,说明锁是空闲的,没有被持有
    int c = getState();
    if (c == 0) {
      	// 使用CAS让当前线程持有锁
        if (compareAndSetState(0, acquires)) {
          	// 如果成功,设置exclusiveOwnerThread为当前线程,返回true
            setExclusiveOwnerThread(current);
            return true;
        }
    }
  	// 如果锁已经被占用,比较是否是当前线程占用的,如果是,准备重复加锁,这就是重入锁的原理
    else if (current == getExclusiveOwnerThread()) {
      	// 让计数器加1,表示重入锁上的加锁次数
        int nextc = c + acquires;
      	// int 是有最大值的,如果小于0,说明超过了最大值,直接报错
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
      	// 设置同步器的状态为nextc
        setState(nextc);
        return true;
    }
  	// 最后返回加锁失败
    return false;
}

非公平主要体现在当c==0时:

使用CAS让当前线程尝试持有锁,而不会检查AQS队列

然后看下公平锁的TryAcquire方法源码:

@ReservedStackAccess
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
              	// 这里是主要代码
              	// 上锁时,会先检查是否有前驱节点,没有的话使用CAS尝试竞争锁
                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;
        }

看上面的源码中的关键代码注释:

公平锁上锁时,会使用hasQueuedPredecessors方法先检查是否有前驱节点,没有的话使用CAS尝试竞争锁

hasQueuedPredecessors方法是从AQS继承而来的,看下源码:

public final boolean hasQueuedPredecessors() {
        Node h, s; 
        if ((h = head) != null) {  // 如果头不是null
          	// 再看下第二个节点是否为null
          	// 如果第二个节点是为null,或者第二个节点的状态为大于0,说明被取消了
            if ((s = h.next) == null || s.waitStatus > 0) {
                s = null; // traverse in case of concurrent cancellation
              	// 否则按照FIFO原则寻找最先入队列的并且没有被Cancel的Node ,赋值给s
                for (Node p = tail; p != h && p != null; p = p.prev) {
                    if (p.waitStatus <= 0)
                        s = p;
                }
            }
          	// 如果s节点不是null,并且不是当前的线程,则返回true,说明有前驱节点
            if (s != null && s.thread != Thread.currentThread())
                return true;
        }
  			// 如果头是null,说明队列为空,返回fasle,说明无前驱节点
        return false;
    }

条件变量原理

每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject

public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    /** First node of condition queue. */
    private transient Node firstWaiter;
    /** Last node of condition queue. */
    private transient Node lastWaiter;

    /**
     * Creates a new {@code ConditionObject} instance.
     */
    public ConditionObject() { }
 	
  	// 省略代码
}

其内部也维护了两个变量,firstWaiterlastWaiter,用来维护在条件变量上等待的对列的头和尾。

await流程

await方法用来把线程加入到条件变量的等待队列。

源码如下:

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
  	// 添加一个 Node 至等待队列
    Node node = addConditionWaiter();
  	// 释放节点持有的锁,因为可能有重入
    int savedState = fullyRelease(node);
    int interruptMode = 0;
  	// 如果该节点还没有转移至 AQS 队列, 就阻塞
    while (!isOnSyncQueue(node)) {
      	// park当前线程,等待被唤醒
        LockSupport.park(this);
      	// 如果被打断, 退出等待队列
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
  	// 退出等待队列后, 还需要获得 AQS 队列的锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) 
      	// 所有已取消的 Node 从队列链表删除,
        unlinkCancelledWaiters();
    if (interruptMode != 0)
      	// 应用打断模式
        reportInterruptAfterWait(interruptMode);
}

其中addConditionWaiter源码如下:

private Node addConditionWaiter() {
  	// 如果不是锁的持有者,直接报错
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node t = lastWaiter;
    // 如果最后一个节点是null,就从队列清除
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
		
  	// 创建一个关联当前线程的新 Node,
    Node node = new Node(Node.CONDITION);

  	// 如果为节点是null,说明队列是空的,则把新节点作为头节点,否则把新节点加到t的下个节点
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
  	// 把新节点设为尾节点
    lastWaiter = node;
    return node;
}

整个流程如下;

  1. 开始 Thread-0 持有锁,调用 await

    进入 ConditionObject 的 addConditionWaiter 流程,创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部

在这里插入图片描述

  1. 接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁

在这里插入图片描述

  1. unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功

在这里插入图片描述

  1. park阻塞 Thread-0线程,等待被唤醒

在这里插入图片描述

signal流程

signal用来唤醒在当前条件变量等待的线程。

源码如下:

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

其主要方法是执行doSignal,源码如下:

private void doSignal(Node first) {
    do {
      	// 已经是尾节点了
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&  // 将等待队列中的 Node 转移至 AQS 队列, 如果不成功
             (first = firstWaiter) != null);  // 且还有节点则继续循环
}

其中主要是transferForSignal方法,用来将等待队列中的 Node 转移至 AQS 队,源码如下:

final boolean transferForSignal(Node node) {
    // 如果状态已经不是 Node.CONDITION, 说明节点的线程已经被取消了
    if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
        return false;

    /*
     * Splice onto queue and try to set waitStatus of predecessor to
     * indicate that thread is (probably) waiting. If cancelled or
     * attempt to set waitStatus fails, wake up to resync (in which
     * case the waitStatus can be transiently and harmlessly wrong).
     */
  	// 加入 AQS 队列尾部,返回的p是原来的尾部,即现在的尾节点的上一个节点
    Node p = enq(node);
    int ws = p.waitStatus;  // 获取p的状态
  	// 如果ws > 0 ,即上一个节点被取消,或者上一个节点p不能设置状态为 Node.SIGNAL
    if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
      	// 那么就使用unpark 取消当前线程阻塞, 让线程重新同步状态
        LockSupport.unpark(node.thread);
    return true;
}

整个流程如下:

  1. 假设 Thread-1 要来唤醒 Thread-0

在这里插入图片描述

  1. 进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node

在这里插入图片描述

  1. 执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的waitStatus 改为 -1

改为-1是让他有资格唤醒下个节点

在这里插入图片描述

  1. 最后Thread-1 释放锁,进入 unlock 流程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/762105.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

kafka第三课-可视化工具、生产环境问题总结以及性能优化

一、可视化工具 https://pan.baidu.com/s/1qYifoa4 密码&#xff1a;el4o 下载解压之后&#xff0c;编辑该文件&#xff0c;修改zookeeper地址&#xff0c;也就是kafka注册的zookeeper的地址&#xff0c;如果是zookeeper集群&#xff0c;以逗号分开 vi conf/application.conf 启…

如何刻录光盘

如何刻录光盘 1 、将光盘放入光驱&#xff0c;选择“用于CD/DVD播放机” &#xff0c;该模式下&#xff0c;刻录在光盘的文件无法进行编辑和删除 2 、将需要刻录的文件拷贝至光盘内&#xff0c;则会在“准备好写入光盘中的文件”下显示拷贝进去的文件&#xff0c;此时文件还没…

EDI 工作流操作指南

一个完整的EDI工作流中&#xff0c;起始端为通常为文件传输端口&#xff1a;如AS2、OFTP等&#xff0c;末端为数据库端口。此前的文章中我们对AS2端口以及数据库端口已做了详细介绍&#xff0c;本文主要介绍 EDI 文件的格式转换以及映射。 如下图所示&#xff0c;工作流界面中…

安装blissOS重启后无法进入图形化界面

重启blissOS 重启时&#xff0c;按e键两下 进入 上图是一个可编辑页面&#xff0c;不要删除修改前面的内容&#xff0c;移动光标前往quiet&#xff0c;然后删除quiet输入“ nomodeset xforceseva ”&#xff0c;然后按下回车 然后按回车&#xff0c;按b键进入系统 在set-…

class组件constructor方法

class组件constructor方法 https://blog.csdn.net/m0_37557930/article/details/116228217 https://blog.csdn.net/qq_39207948/article/details/113143131 ​ 为何我们使用子类继承父类&#xff0c;就必须在 constructor( ) 方法中调用 super( ) 方法&#xff0c;否则新建实…

【C】文件操作详解

这里写目录标题 文件操作什么是文件文件名文件类型文本文件二进制文件 文件缓冲区文件指针文件的打开和关闭fopenfclose 文件的顺序读写fgetcfputcfgetsfputcfscanffprintffwritefread比较scanf/fscanf/sscanfsscanf 比较printf/fprintf/sprintfsprintf 文件的随机读写fseekfte…

周考一之重做

输入一个学生的成绩&#xff0c;如果学习成绩>90分的同学用A表示&#xff0c;60-89分之间用B表示&#xff0c;60分以下的用C表示(10) public static void main(String[] args){ Scanner scanner new Scanner(System.in); System.out.println(“请输入学生成绩&#xff1a;…

linux图形界面总结---X、Xorg、WM、QT、GTK、KDE、GNOME的区别与联系

文章目录 一、 linux图形界面二、X协议三、Xfree86 Xorg四、WM(window manager:窗口管理器)五、X协议的Client端实现六、KDE、GNOME、QT和GTK直接关系七、参考&#xff1a; 一、 linux图形界面 linux本身没有图形界面&#xff0c;linux现在的图形界面的实现只是linux下的应用程…

值类型与引用类型

常见的值类型&#xff1a;int&#xff0c;long&#xff0c; short&#xff0c; float&#xff0c; double&#xff0c; byte&#xff0c; char&#xff0c; enum&#xff0c; struct...... 常见的引用类型&#xff1a;字符串&#xff0c; 数组&#xff0c; 类...... 区别&…

附录10-3.JS正则常见案例

目录 1 某一个字符串重复出现两次 2 多行字符串转变为数组套对象 3 多个正则表达式匹配一个字符 4 在指定的字符前加一些什么东西 1 某一个字符串重复出现两次 match的方式如果你使用小括号分组&#xff0c;第一个结果是符合正则的结果&#xff0c;也就是我查出来了 l…

Excel表格怎么样转换成PDF?分享这3个方法免费转换!

在日常办公和学习中&#xff0c;我们常常需要将Excel表格转换为PDF格式以便分享、打印或存档。本文将介绍三种简便的免费方法。方法一介绍了记灵在线工具&#xff0c;方法二使用办公软件&#xff08;WPS或Office&#xff09;&#xff0c;方法三则使用Adobe软件。 方法一&#…

大二毕设.2-自研Spring框架

目录 项目描述&#xff1a; 基本演示 提取标记类 IOC容器的装载 IOC容器的操作 DI依赖注入 Aspect排序 AOP MVC 功能实现讲解 项目描述&#xff1a; 为了更好地学习 Spring 的核心&#xff0c;参考 Spring 源码实现的一个简易框架当前已实现 IOC&#xff0c;DI依赖注…

交叉编译gRPC初实践

快速开始 一、创建android编译目录&#xff0c;在grpc源码根目录下运行&#xff1a; mkdir -p cmake/build_android && cd cmake/build_android 二、cmkae生成对应Makefile等编译所需的文件 cmake -DCMAKE_TOOLCHAIN_FILE/zhuyazhou/DDS/tools_dds/android-ndk-r25/b…

Python(Conda)环境迁移(从win10到macos12.5)笔记

文章目录 背景环境 1、通过conda迁移2、通过python迁移3、最后&#xff08;逐一安装&#xff09; 背景环境 win10是以前安装的conda和py。目前需要导出的环境的版本为py3.10.4。macos是重新安装的conda&#xff0c;目前有的环境是py3.11.4。我是先进conda用刚安装好的base创建…

idea 有时提示找不到类或者符号,日志报java: 找不到符号的解决

解决一&#xff1a; idea maven编译成功&#xff0c;运行失败提示找不到符号&#xff0c;主要是get和set方法找不到符号&#xff0c;此时就是idea的lombok版本冲突 IDEA版本导致的Lombok失效&#xff0c;需要更新lombok版本到1.18.14及之后版本得到解决 <dependency>&…

计划、逻辑与智能

有计划性是指基于目标、目的或问题&#xff0c;通过制定计划、设立步骤和执行策略来达成预期结果的思维和行为。有计划的智能强调理性、逻辑和目标导向&#xff0c;它能够帮助人们更好地组织和管理资源&#xff0c;解决复杂的问题&#xff0c;并实现预期的目标。 无计划性则代表…

Seal AppManager如何基于Terraform简化基础设施管理

作者简介 陈灿&#xff0c;数澈软件Seal 后端研发工程师&#xff0c;曾在腾讯负责敏捷研发体系建设以及 DevOps 解决方案的敏捷实践。在敏捷研发和产品效能提升有着丰富的经验&#xff0c;致力于构建一站式研发友好的平台工程解决方案。现在是 Seal 平台工程团队核心研发人员。…

配置spark

配置spark Yarn 模式Standalone 模式Local 模式 Yarn 模式 tar -zxvf spark-3.0.0-bin-hadoop3.2.tgz -C /opt/module cd /opt/module mv spark-3.0.0-bin-hadoop3.2 spark-yarn修改 hadoop 配置文件/opt/module/hadoop/etc/hadoop/yarn-site.xml, 并分发 <!--是否启动一…

【板栗糖GIS】——如何安装ffmpeg

【板栗糖GIS】——如何安装ffmpeg 目录 1. 解压安装包 2. 把bin路径放在环境变量中 3. 检测是否安装成功 下载软件包&#xff0c;我已经准备好资源&#xff0c;只是审核还未通过&#xff0c;过两天会加上安装包链接 1. 解压安装包 2. 把bin路径放在环境变量中 3. 检测是否…

Anaconda安装和激活

一、Anaconda下载地址 https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/?CM&OD 说明&#xff1a;使用paddlepaddle需要先安装python环境&#xff0c;这里我们选择python集成环境Anaconda工具包 Anaconda是1个常用的python包管理程序安装完Anaconda后&#xff0c…