ReentrantLock

简介: ReentrantLock

一、概述

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。

相对于 synchronized 它具备如下特点:

  • 可中断(等待获取锁的过程中可以被打断)
  • synchronized锁加上去不能中断,a线程应用锁,b线程不能取消掉它
  • 可以设置超时时间
  • synchronized它去获取锁时,如果对方持有锁,那么它就会进入entryList一直等待下去。而ReentrantLock可以设置超时时间,规定时间内如果获取不到锁,就放弃锁。
  • 可以设置为公平锁
  • 防止线程饥饿的情况,即先到先得。如果争抢的人比较多,则可能会发生永远都得不到锁。
  • 支持多个条件变量(相当于有多个EntryList)
  • synchronized只支持同一个waitset。
  • 与 synchronized 一样,都支持可重入

二、基本语法

步骤

(1)创建一个ReentrantLock对象;

(2)调用ReentrantLock对象的lock()方法;

(3)将临界区的代码写在try代码块中;

(4)将ReentrantLock对象的unlock()方法写在finally代码块中。

private Lock lock = new ReentrantLock();
// 获取锁
lock.lock();
try {
    // 临界区
} finally {
    // 释放锁
    lock.unlock();
}
  • synchronized是在关键字的级别来保护临界区,而reentrantLock是在对象的级别保护临界区。临界区即访问共享资源的那段代码。
  • finally中表明不管将来是否出现异常,都会释放锁,释放锁即调用unlock方法。否则无法释放锁,其它线程就永远也获取不了锁。

注意:lock.lock();与try代码块之间不要有空行或者其它逻辑,且lock.unlock();要写在finally代码块的第一行

三、可重入

3.1 概述

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

3.2 代码示例

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @author xiaowei
 * @date 2022-10-25
 * @description ReentrantLock 可重入
 **/
@Slf4j(topic = "c.Test03")
public class Test03 {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        method1();
    }
    public static void method1(){
        lock.lock();
        try {
            log.debug("execute method1");
            method2();
        } finally {
            lock.unlock();
        }
    }
    public static void method2(){
        lock.lock();
        try {
            log.debug("execute method2");
            method3();
        }finally {
            lock.unlock();
        }
    }
    public static void method3(){
        lock.lock();
        try {
            log.debug("execute method3");
        }finally {
            lock.unlock();
        }
    }
}

运行结果

21:58:48.257 c.Test03 [main] - execute method1
21:58:48.259 c.Test03 [main] - execute method2
21:58:48.259 c.Test03 [main] - execute method3

从运行结果可以看出,当前线程在执行时多次获取锁, 并不会被锁挡住, 而是正常运行

注意:加锁与解锁是必须匹配的,只有当解锁次数等于加锁次数时,锁才会被正确释放。

四、可打断

4.1 概述

可打断是指, 当前线程在等待锁的时候, 可以被其他的线程使用 interrupt() 方法打断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。

方法说明

  • lock.lockInterruptibly():尝试获取锁,如果获取不到锁,进入等待;等待过程中可以被打断。
  • lock.lock():等待锁的过程是不可以被打断的。

4.2 代码示例

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
import static com.lilinchao.thread.utils.Sleeper.sleep;
/**
 * @author lilinchao
 * @date 2022-10-25
 * @description 打断ReentrantLock锁
 **/
@Slf4j(topic = "c.Test04")
public class Test04 {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("等锁的过程中被打断");
                return;
            }
            try {
                log.debug("获得了锁");
            }finally {
                lock.unlock();
            }
        },"t1");
        lock.lock();
        log.debug("获得了锁");
        t1.start();
        try {
            sleep(1);
            t1.interrupt();
            log.debug("执行打断");
        } finally {
            lock.unlock();
        }
    }
}

运行结果

22:21:02.967 c.Test04 [main] - 获得了锁
22:21:02.969 c.Test04 [t1] - start...
22:21:03.984 c.Test04 [main] - 执行打断
java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at com.lilinchao.thread.demo06.Test04.lambda$main$0(Test04.java:22)
    at java.lang.Thread.run(Thread.java:748)
22:21:03.985 c.Test04 [t1] - 等锁的过程中被打断

说明

main线程首先获得锁,因此被创建出的线程t1启动后无法获得锁,之后,main线程打断线程t1,使得线程t1结束等待。

4.3 不可中断模式

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

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
import static site.weiyikai.thread.utils.Sleeper.sleep;
/**
 * @author xiaowei
 * @date 2022-10-25
 * @description 不可中断模式
 **/
@Slf4j(topic = "c.Test05")
public class Test05 {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            lock.lock();
            try {
                log.debug("获得了锁");
            }finally {
                lock.unlock();
            }
        },"t1");
        lock.lock();
        log.debug("获得了锁");
        t1.start();
        try {
            sleep(1);
            t1.interrupt();
            log.debug("执行打断");
            sleep(1);
        } finally {
            log.debug("释放了锁");
            lock.unlock();
        }
    }
}

运行结果

22:24:52.756 c.Test05 [main] - 获得了锁
22:24:52.758 c.Test05 [t1] - start...
22:24:53.771 c.Test05 [main] - 执行打断
22:24:54.777 c.Test05 [main] - 释放了锁
22:24:54.777 c.Test05 [t1] - 获得了锁

五、锁超时

5.1 概述

可打断,是一种被动的打断,需要其他的线程来进行打断。

而锁超时可以通过主动方式,来解决线程无限制的等待下去。如果当前线程在等待了一段时间之后,还没有获取锁,将不在继续等待,继续向下执行。

通过设置获得锁的等待时间,当不能在等待时间内获得锁的时候释放锁,就能够避免死锁的问题。

5.2 设置超时时间API

ReetrantLock提供了两个获取锁并快速返回的方法,不会一直等待,无论成功失败都将立即返回。

5.3 代码示例

  • 无参tryLock()方法
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
import static site.weiyikai.thread.utils.Sleeper.sleep;
/**
 * @author xioawei
 * @date 2022-10-25
 * @description 锁超时 - 立刻失败
 **/
@Slf4j(topic = "c.Test06")
public class Test06 {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            if (!lock.tryLock()) {
                log.debug("获取立即失败,返回");
                return;
            }
            try {
                log.debug("获得了锁");
            }finally {
                lock.unlock();
            }
        },"t1");
        lock.lock();
        log.debug("获得了锁");
        t1.start();
        try {
            sleep(2);
        }finally {
            lock.unlock();
        }
    }
}

运行结果

22:40:21.990 c.Test06 [main] - 获得了锁
22:40:21.992 c.Test06 [t1] - start...
22:40:21.992 c.Test06 [t1] - 获取立即失败,返回
  • 带参tryLock方法
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import static site.weiyikai.thread.utils.Sleeper.sleep;
/**
 * @author xiaowei
 * @date 2022-10-25
 * @description 超时失败
 **/
@Slf4j(topic = "c.Test07")
public class Test07 {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    log.debug("获取等待 1s 后失败,返回");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                log.debug("获得了锁");
            }finally {
                lock.unlock();
            }
        },"t1");
        lock.lock();
        log.debug("获得了锁");
        t1.start();
        try {
            //主线程等待2s
            sleep(2);
        }finally {
            lock.unlock();
        }
    }
}

运行结果

22:42:41.537 c.Test07 [main] - 获得了锁
22:42:41.539 c.Test07 [t1] - start...
22:42:42.544 c.Test07 [t1] - 获取等待 1s 后失败,返回

说明

代码执行时,main线程先获得了锁,进入到2s的睡眠当中,此时t1线程启动,执行到lock.tryLock(1, TimeUnit.SECONDS),等待获取到lock锁后继续向下执行。1s后t1线程未获得所,将放弃继续获取锁,t1线程退出。

5.4 锁超时解决哲学家就餐问题

import site.weiyikai.thread.utils.Sleeper;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @author xiaowei
 * @date 2022-10-25
 * @description
 **/
public class Test08 {
    public static void main(String[] args) {
        Chopstick2 c1 = new Chopstick2("1");
        Chopstick2 c2 = new Chopstick2("2");
        Chopstick2 c3 = new Chopstick2("3");
        Chopstick2 c4 = new Chopstick2("4");
        Chopstick2 c5 = new Chopstick2("5");
        new Philosopher2("苏格拉底", c1, c2).start();
        new Philosopher2("柏拉图", c2, c3).start();
        new Philosopher2("亚里士多德", c3, c4).start();
        new Philosopher2("赫拉克利特", c4, c5).start();
        new Philosopher2("阿基米德", c5, c1).start();
    }
}
@Slf4j(topic = "c.Philosopher2")
class Philosopher2 extends Thread {
    Chopstick2 left;
    Chopstick2 right;
    public Philosopher2(String name, Chopstick2 left, Chopstick2 right) {
        super(name);
        this.left = left;
        this.right = right;
    }
    @Override
    public void run() {
        while (true) {
            // 尝试获得左手筷子
            if (left.tryLock()) {
                try {
                    // 尝试获得右手筷子
                    if (right.tryLock()) {
                        try {
                            eat();
                        } finally {
                            right.unlock();
                        }
                    }
                } finally {
                    // 如果没有获得右手的筷子,则释放自己手里的筷子,破坏了死锁的请求和保持条件
                    left.unlock();
                }
            }
        }
    }
    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(1);
    }
}
class Chopstick2 extends ReentrantLock {
    String name;
    public Chopstick2(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

运行结果

22:56:17.080 c.Philosopher2 [亚里士多德] - eating...
22:56:17.080 c.Philosopher2 [苏格拉底] - eating...
22:56:18.093 c.Philosopher2 [柏拉图] - eating...
22:56:18.093 c.Philosopher2 [赫拉克利特] - eating...
22:56:19.093 c.Philosopher2 [苏格拉底] - eating...
22:56:19.093 c.Philosopher2 [亚里士多德] - eating...
22:56:20.101 c.Philosopher2 [亚里士多德] - eating...
22:56:20.101 c.Philosopher2 [阿基米德] - eating...
...........
程序会一直向下执行,不会产生死锁

说明

可以看到,需要使用 tryLock 方法去获取左筷子和右筷子, 如果获取失败直接结束, 另外在成功获取锁后,要在 finally 里释放锁。

六、公平锁

6.1 概念

  • 公平锁:是指多个线程按照申请锁的顺序来获取锁,通过队列FIFO,先进先出,类似排队打饭,先来后到。
  • 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。

6.2 语法

ReentrantLock默认是非公平锁

ReentrantLock lock = new ReentrantLock(true);  // true:公平锁
lock.lock();
try {
    // todo
} finally {
    lock.unlock();
}

说明

  • 初始化构造函数入参,选择是否为初始化公平锁。
  • 其实一般情况下并不需要公平锁,除非你的场景中需要保证顺序性。
  • 使用 ReentrantLock 切记需要在 finally 中关闭,lock.unlock()。
公平锁和非公平锁的选择

一点源码

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
  • 构造函数中选择公平锁(FairSync)、非公平锁(NonfairSync)。

6.3 代码示例

  • 非公平锁
import java.util.concurrent.locks.ReentrantLock;
/**
 * Created by xiaowei
 * Date 2022/10/25
 * Description 公平锁/非公平锁
 */
public class FairLockDemo {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock(false);
        lock.lock();
        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " running...");
                } finally {
                    lock.unlock();
                }
            }, "t" + i).start();
        }
        // 1s 之后去争抢锁
        Thread.sleep(1000);
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start...");
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " running...");
            } finally {
                lock.unlock();
            }
        }, "强行插入").start();
        lock.unlock();
    }
}

强行插入,有机会在中间输出

运行结果

改为公平锁后

ReentrantLock lock = new ReentrantLock(true);

强行插入,总是在最后输出

说明

开启公平锁后,所有的线程在entrylist中按照开始的时间顺序执行,不会出现插队现象,所以公平锁能够解决饥饿现象。不开启公平锁,当上一个线程结束后,随机从entrylist中执行一个线程。

6.4 总结

公平锁

  • 优点:吞吐率较高。
  • 缺点:从申请者个体的角度来看,这些申请者获得相应资源的独占权所需时间的偏差可能比较大,即有的线程很快就能申请到资源,而有的线程则要经历若干次暂停与唤醒才能成功申请到资源,极端情况下可能导致饥饿现象。(注意:对于非公平锁:在高并发的情况下,有可能会造成优先级反转或者饥饿现象

非公平锁

  • 优点:从个体申请者的角度来看,这些申请者获得相应资源的独占权所需时间的偏差可能比较小,即每个资源申请者申请到资源所需的时间基本相同,并且不会导致饥饿现象。
  • 缺点:吞吐率较低,这是其维护资源独占权的授予顺序的开销比较大(主要是线程的暂停与唤醒所导致的上下文切换)的结果。

七、条件变量

7.1 概述

  • 关键字synchronized中也有条件变量,就是waitSet,可以理解为条件不满足时进入waitSet等待,一个synchronized只能对应一个waitSet。
  • ReentrantLock可以支持多个条件变量,因此可以将不同条件的线程放入等待集合中,以便于后续进行专门的唤醒。在ReentrantLock中使用条件变量需要使用await()方法。

ReentrantLock的条件变量比synchronized强大之处在于,它支持多个条件变量(对象)。

使用流程
  • 使用ReentrantLock对象创建条件变量condition;
  • 执行condition.await()前需要先获取锁;
  • 执行condition.await()后,线程会释放锁,并进入conditionObject中等待;
  • 其它线程执行condition.signal()或者condition.signalAll()唤醒conditionObject中等待的线程;
  • 被唤醒后会重新竞争锁
  • 竞争锁成功后,会从await()后的代码处开始执行

函数await()调用方式

7.2 代码示例

  • t1需要等待烟过来, 否则就一直等待
  • t2需要等待早餐, 否则就一直等待
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import static site.weiyikai.concurrent.utils.Sleeper.sleep;
/**
 * Created by xiaowei
 * Date 2022/10/25
 * Description 条件变量
 */
@Slf4j(topic = "c.ConditionTest")
public class ConditionTest {
    static ReentrantLock lock = new ReentrantLock();
    static Condition waitCigaretteQueue = lock.newCondition();
    static Condition waitbreakfastQueue = lock.newCondition();
    static volatile boolean hasCigrette = false;
    static volatile boolean hasBreakfast = false;
    public static void main(String[] args) {
        new Thread(() -> {
            try {
                // 如果没有拿到锁的话, 线程就会阻塞在这, 不会向下执行
                lock.lock();
                while (!hasCigrette) {
                    // 不满足条件就到对应的 waitSet 等待
                    try {
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("等到了它的烟");
            } finally {
                lock.unlock();
            }
        },"t1").start();
        new Thread(() -> {
            try {
                lock.lock();
                while (!hasBreakfast) {
                    try {
                        waitbreakfastQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("等到了它的早餐");
            } finally {
                lock.unlock();
            }
        },"t2").start();
        sleep(1);
        sendBreakfast();
        sleep(1);
        sendCigarette();
    }
    private static void sendCigarette() {
        lock.lock();
        try {
            log.debug("送烟来了");
            hasCigrette = true;
            waitCigaretteQueue.signal();
        } finally {
            lock.unlock();
        }
    }
    private static void sendBreakfast() {
        lock.lock();
        try {
            log.debug("送早餐来了");
            hasBreakfast = true;
            waitbreakfastQueue.signal();
        } finally {
            lock.unlock();
        }
    }
}

运行结果

23:52:23.927 c.ConditionTest [main] - 送早餐来了
23:52:23.930 c.ConditionTest [t2] - 等到了它的早餐
23:52:24.931 c.ConditionTest [main] - 送烟来了
23:52:24.931 c.ConditionTest [t1] - 等到了它的烟
目录
相关文章
|
7月前
|
安全 Java
ReentrantLock、ReentrantReadWriteLock、StampedLock讲解
ReentrantLock、ReentrantReadWriteLock、StampedLock讲解
|
4月前
|
存储 设计模式 安全
理解 AQS 和 ReentrantLock
在多线程编程中,同步机制是确保线程安全的关键。AQS(AbstractQueuedSynchronizer)和ReentrantLock是Java中两种常见的同步机制,它们各自具有不同的特性和适用场景。了解和掌握这两种机制对于编写高效、安全的并发程序至关重要。这篇文章将带你取了解和掌握这两种机制!另外值得一提的是:公平锁的实现与非公平锁是很像的,只不过在获取锁时不会直接尝试使用CAS来获取锁。只有当队列没节点并且state为0时才会去获取锁,不然都会把当前线程放到队列中。
116 1
|
7月前
ReentrantLock和Synchronized简单比较
ReentrantLock和Synchronized简单比较
20 0
|
11月前
|
Java
16.ReentrantLock全解读
大家好,我是王有志。今天和大家一起聊聊ReentrantLock,它是我们最常见的基于AQS实现的互斥锁。
75 0
|
缓存 Java Linux
ReentrantLock、ReentrantReadWriteLock、StampedLock
ReentrantLock、ReentrantReadWriteLock、StampedLock
ReentrantLock、ReentrantReadWriteLock、StampedLock
ReentrantLock介绍
ReentrantLock介绍
126 0
|
Java
彻底理解ReentrantLock可重入锁的使用
java除了使用关键字synchronized外,还可以使用ReentrantLock实现独占锁的功能。而且ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。这篇文章主要是从使用的角度来分析一下ReentrantLock。
150 0
彻底理解ReentrantLock可重入锁的使用
|
存储 设计模式 Java
深入理解ReentrantLock
同步锁synchronized和重入锁ReentrantLock都是用于并发程序设计必不可少的手段,在JDK 5.0早期版本中,同步锁性能远远低于重入锁,但是在6.0版本之后,jdk对同步锁做了大量的优化,使得同步锁跟重入锁性能差距并不大,并且jdk团队表示,同步锁还有进一步升级优化的空间
深入理解ReentrantLock
|
调度
ReentrantLock的使用
ReentrantLock的使用
222 0
ReentrantLock的使用
|
Java 容器 安全
ReentrantLock详解
本博客主要讲述ReentrantLock的实现原理,主要内容包括: AQS原理以及实现过程。 ReenetrantLock获取锁、释放锁流程,以及原理。 ReenetrantLock源码分析。
6074 1