百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

ReentrantLock原理详解

toyiye 2024-05-25 20:11 12 浏览 0 评论

ReentrantLock是一个可重入且独占式的锁,它具有与使用synchronized监视器锁相同的基本行为和语义,但与synchronized关键字相比,它更灵活、更强大,增加了轮询、超时、中断等高级功能。ReentrantLock,顾名思义,它是支持可重入锁的锁,是一种递归无阻塞的同步机制。除此之外,该锁还支持获取锁时的公平和非公平选择。

ReentrantLock的类图如下:


ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。如果在绝对时间上,先对锁进行获取的请求你一定先被满足,那么这个锁是公平的,反之,是不公平的。公平锁的获取,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock的公平与否,可以通过它的构造函数来决定。

事实上,公平锁往往没有非公平锁的效率高,但是,并不是任何场景都是以TPS作为唯一指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求越能够得到优先满足。

下面我们着重分析ReentrantLock是如何实现重进入和公平性获取锁的特性,并通过测试来验证公平性对性能的影响。

实现重进入

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的首先需要解决以下两个问题:

  • 线程再次获取锁:所需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次获取成功;
  • 锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其它线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前线程被重复获取的次数,而被释放时,计数自减,当计数为0时表示锁已经成功释放。

ReentrantLock是通过自定义同步器来实现锁的获取与释放,我们以非公平锁(默认)实现为例,对锁的获取和释放进行详解。

获取锁

ReentrantLock的默认构造函数为:

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

即内部同步组件为非公平锁,获取锁的代码为:

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

通过简介中的类图可以看到,Sync类是ReentrantLock自定义的同步组件,它是ReentrantLock里面的一个内部类,它继承自AQS,它有两个子类:公平锁FairSync和非公平锁NonfairSync。ReentrantLock的获取与释放锁操作都是委托给该同步组件来实现的。下面我们来看一看非公平锁的lock()方法:

final void lock() {    if (compareAndSetState(0, 1))        setExclusiveOwnerThread(Thread.currentThread());    else        acquire(1);}

该程序首先会通过compareAndSetState(int, int)方法来尝试修改同步状态,如果修改成功则表示获取到了锁,然后调用setExclusiveOwnerThread(Thread)方法来设置获取到锁的线程,该方法是继承自AbstractOwnableSynchronizer类,AQS继承自AOS类,它的主要作用就是记录获取到独占锁的线程,AOS类的定义很简单:

public abstract class AbstractOwnableSynchronizer    implements java.io.Serializable {    private static final long serialVersionUID = 3737899427754241961L;     protected AbstractOwnableSynchronizer() { }     // The current owner of exclusive mode synchronization.    private transient Thread exclusiveOwnerThread;     protected final void setExclusiveOwnerThread(Thread thread) {        exclusiveOwnerThread = thread;    }     protected final Thread getExclusiveOwnerThread() {        return exclusiveOwnerThread;    }}

如果同步状态修改失败,则表示没有获取到锁,需要调用acquire(int)方法,该方法定义在AQS中,如下:

public final void acquire(int arg) {    if (!tryAcquire(arg) &&        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))        selfInterrupt();}

tryAcquire(int)是子类需要重写的方法,在非公平锁中的实现如下:

protected final boolean tryAcquire(int acquires) {    return nonfairTryAcquire(acquires);}
final boolean nonfairTryAcquire(int acquires) {    // 获取当前线程    final Thread current = Thread.currentThread();    // 获取同步状态    int c = getState();    // 同步状态为0,表示没有线程获取锁    if (c == 0) {        // 尝试修改同步状态        if (compareAndSetState(0, acquires)) {            // 同步状态修改成功,获取到锁            setExclusiveOwnerThread(current);            return true;        }    }    // 同步状态不为0,表示已经有线程获取了锁,判断获取锁的线程是否为当前线程    else if (current == getExclusiveOwnerThread()) {        // 获取锁的线程是当前线程        int nextc = c + acquires;        if (nextc < 0) // overflow            throw new Error("Maximum lock count exceeded");        setState(nextc);        return true;    }    // 获取锁的线程不是当前线程    return false;}

nonfairTryAcquire(int)方法首先判断同步状态是否为0,如果是0,则表示该锁还没有被线程持有,然后通过CAS操作获取同步状态,如果修改成功,返回true。如果同步状态不为0,则表示该锁已经被线程持有,需要判断当前线程是否为获取锁的线程,如果是则获取锁,成功返回true。成功获取锁的线程再次获取该锁,只是增加了同步状态的值,这也就实现了可重入锁。

释放锁

成功获取锁的线程在完成业务逻辑之后,需要调用unlock()来释放锁:

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

unlock()调用NonfairSync类的release(int)方法释放锁,release(int)方法是定义在AQS中的方法:

public final boolean release(int arg) {    if (tryRelease(arg)) {        Node h = head;        if (h != null && h.waitStatus != 0)            unparkSuccessor(h);        return true;    }    return false;}

tryRelease(int)是子类需要实现的方法:

protected final boolean tryRelease(int releases) {    // 计算新的状态值    int c = getState() - releases;    // 判断当前线程是否是持有锁的线程,如果不是的话,抛出异常    if (Thread.currentThread() != getExclusiveOwnerThread())        throw new IllegalMonitorStateException();    boolean free = false;    // 新的状态值是否为0,若为0,则表示该锁已经完全释放了,其他线程可以获取同步状态了    if (c == 0) {        free = true;        setExclusiveOwnerThread(null);    }    // 更新状态值    setState(c);    return free;}

如果该锁被获取n次,那么前(n-1)次tryRelease(int)方法必须返回false,只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当状态为0时,将占有线程设为null,并返回true,表示释放成功。

公平锁与非公平锁

公平性与否是针对锁获取顺序而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合FIFO原则。我们在前面介绍了非公平锁NonfairSync调用的nonfairTryAcquire(int)方法,在该方法中,只要通过CAS操作修改同步状态成功,则当前线程就获取到了锁,而公平锁则不同,公平锁FairSync的tryAcquire(int)方法如下所示:

protected final boolean tryAcquire(int acquires) {    // 获取当前线程    final Thread current = Thread.currentThread();    // 获取同步状态    int c = getState();    // 同步状态为0,表示没有线程获取锁    if (c == 0) {        if (!hasQueuedPredecessors() &&            compareAndSetState(0, acquires)) {            setExclusiveOwnerThread(current);            return true;        }    }    // 同步状态不为0,表示已经有线程获取了锁,判断获取锁的线程是否为当前线程    else if (current == getExclusiveOwnerThread()) {        // 获取锁的线程是当前线程        int nextc = c + acquires;        if (nextc < 0)            throw new Error("Maximum lock count exceeded");        setState(nextc);        return true;    }    return false;}

该方法与nonfairTryAcquire(int)方法比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,该方法定义如下:

public final boolean hasQueuedPredecessors() {    // The correctness of this depends on head being initialized    // before tail and on head.next being accurate if the current    // thread is first in queue.    // 同步队列尾节点    Node t = tail; // Read fields in reverse initialization order    // 同步队列头节点    Node h = head;    Node s;    return h != t &&        ((s = h.next) == null || s.thread != Thread.currentThread());}

该方法主要是对同步队列中当前节点是否有前驱节点进行判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

下面我们编写一个测试程序来观察公平锁和非公平锁在获取锁时的区别:

public class FairAndUnfairTest {    private static CountDownLatch start;        private static class MyReentrantLock extends ReentrantLock {        public MyReentrantLock(boolean fair) {            super(fair);        }         public Collection<Thread> getQueuedThreads() {            List<Thread> arrayList = new ArrayList<Thread>(super.getQueuedThreads());            Collections.reverse(arrayList);            return arrayList;        }    }        private static class Worker extends Thread {        private Lock lock;         public Worker(Lock lock) {            this.lock = lock;        }         @Override        public void run() {            try {                start.await();            } catch (InterruptedException e) {            	e.printStackTrace();            }            // 连续两次打印当前的Thread和等待队列中的Thread            for (int i = 0; i < 2; i++) {                lock.lock();                try {                    System.out.println("Lock by [" + getName() + "], Waiting by " + ((MyReentrantLock) lock).getQueuedThreads());                } finally {                    lock.unlock();                }            }        }         public String toString() {            return getName();        }    }     public static void main(String[] args) {		Lock fairLock = new MyReentrantLock(true);		Lock unfairLock = new MyReentrantLock(false);				testLock(fairLock);//		testLock(unfairLock);	}     private static void testLock(Lock lock) {        start = new CountDownLatch(1);        for (int i = 0; i < 5; i++) {            Thread thread = new Worker(lock);            thread.setName("" + i);            thread.start();        }        start.countDown();    }}

testLock(fairLock)运行结果(不唯一):

Lock by [0], Waiting by [4, 1, 2, 3]
Lock by [4], Waiting by [1, 2, 3, 0]
Lock by [1], Waiting by [2, 3, 0, 4]
Lock by [2], Waiting by [3, 0, 4, 1]
Lock by [3], Waiting by [0, 4, 1, 2]
Lock by [0], Waiting by [4, 1, 2, 3]
Lock by [4], Waiting by [1, 2, 3]
Lock by [1], Waiting by [2, 3]
Lock by [2], Waiting by [3]
Lock by [3], Waiting by []

testLock(unfairLock)运行结果(不唯一):

Lock by [0], Waiting by [1]
Lock by [0], Waiting by [1, 2, 3, 4]
Lock by [1], Waiting by [2, 3, 4]
Lock by [1], Waiting by [2, 3, 4]
Lock by [2], Waiting by [3, 4]
Lock by [2], Waiting by [3, 4]
Lock by [3], Waiting by [4]
Lock by [3], Waiting by [4]
Lock by [4], Waiting by []
Lock by [4], Waiting by []

从上述结果可以看到,公平锁每次都是队列中的第一个节点获取到锁,而非公平锁出现了一个线程连续获取锁的情况。

为什么会出现连续获取锁的情况呢?因为在nonfairTryAcquire(int)方法中,每当一个线程请求锁时,只要获取了同步状态就成功获取了锁。在此前提下,刚刚释放锁的线程再次获取到同步状态的几率很大,而其他线程只能在同步队列中等待。

非公平锁有可能使线程饥饿,那为什么还要将它设置为默认模式呢?我们再次观察上面的运行结果,如果把每次不同线程获取到锁定义为1次切换,公平锁在测试中进行了10次切换,而非公平锁只有5次切换,这说明非公平锁的开销更小。我们下面再进行一个测试(还是用上面的程序,不过使用了10个线程,每个线程获取2000次锁,程序运行环境为Centos7.3 E5-2682 2.50GHz 单核 2GB),通过vmstat统计测试程序上下文切换次数,运行结果如下所示:

公平锁

程序运行总耗时为5308毫秒

非公平锁

程序运行总耗时为3176毫秒

从结果中可以看到,公平锁与非公平锁相比,耗时更多,线程上下文切换次数更多。可以看出,公平锁保证了锁的获取按照FIFO原则,而代价则是进行大量的线程切换。非公平锁虽然可能导致线程饥饿,但却有极少的线程切换,保证了其更大的吞吐量

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码