上文已经总结了AQS的前世今生,有了这个基础我们就可以来进一步学习并发工具类。首先我们要学习的就是ReentrantLock,本文将从ReentrantLock的产生背景、源码原理解析和应用来学习ReentrantLock这个并发工具类。

1、 产生背景

  前面我们已经学习过了synchronized,这个关键字可以确保对象在并发访问中的原子性、可见性和有序性,这个关键字的底层交由了JVM通过C++来实现,既然是JVM实现,就依赖于JVM,程序员就无法在Java层面进行扩展和优化,肯定就灵活性不高,比如程序员在使用时就无法中断一个正在等待获取锁的线程,或者无法在请求一个锁时无限的等待下去。基于这样一个背景,Doug Lea构建了一个在内存语义上和synchronized一样效果的Java类,同时还扩展了其他一些高级特性,比如定时的锁等待、可中断的锁等待和公平性等,这个类就是ReentrantLock。

2、 源码原理解析

2.1 可重入性原理

  在synchronized一文中,我们认为synchronized是一种重量级锁,它的实现对应的是C++的ObjectMonitor,代码如下:

ObjectMonitor() {_header       = NULL;_count        = 0; //记录线程获取锁的次数_waiters      = 0;_recursions   = 0; //锁的重入次数_object       = NULL;_owner        = NULL;//指向持有ObjectMonitor对象的线程_WaitSet      = NULL; //等待条件队列 类似AQS的ConditionObject_WaitSetLock  = 0 ;_Responsible  = NULL ;_succ         = NULL ;_cxq          = NULL ;FreeNext      = NULL ;_EntryList    = NULL ; //同步队列 类似AQS的CLH队列_SpinFreq     = 0 ;_SpinClock    = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0;}

  从代码中可以看到synchronized实现的锁的重入依赖于JVM,JVM为每个对象的锁关联一个计数器_count和一个所有者线程_owner,当计数器为0的时候就认为锁没有被任何线程持有,当线程请求一个未被持有的锁时,JVM就记下锁的持有者,并将计数器的值设置为1,如果是同一个线程再次获取这个锁,计数器的值递增,而当线程退出时,计数器的值递减,直到计数器为0时,锁被释放。

  ReentrantLock实现了在内存语义上的synchronized,固然也是支持可重入的,那么ReentrantLock是如何支持的呢,让我们以非公平锁的实现看下ReentrantLock的可重入,代码如下:

final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();//当前线程int c = getState();if (c == 0) {//表示锁未被抢占if (compareAndSetState(0, acquires)) {//获取到同步状态setExclusiveOwnerThread(current); //当前线程占有锁return true;}}else if (current == getExclusiveOwnerThread()) {//线程已经占有锁了 重入int nextc = c + acquires;//同步状态记录重入的次数if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}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;//只有线程全部释放才返回truesetExclusiveOwnerThread(null); //同步队列的线程都可以去获取同步状态了
            }setState(c); return free;}

  看到这也就明白了上文说的ReentrantLock类使用AQS同步状态来保存锁重复持有的次数。当锁被一个线程获取时,ReentrantLock也会记录下当前获得锁的线程标识,以便检查是否是重复获取,以及当错误的线程试图进行解锁操作时检测是否存在非法状态异常。

2.2 获取和释放锁

  如下是获取和释放锁的方法:

public void lock() {sync.lock();//获取锁
}
public void unlock() {sync.release(1); //释放锁
}

  获取锁的时候依赖的是内部类Sync的lock()方法,该方法又有2个实现类方法,分别是非公平锁NonfairSync和公平锁FairSync,具体咱们下一小节分析。再来看下释放锁,释放锁的时候实际调用的是AQS的release方法,代码如下:

public final boolean release(int arg) {if (tryRelease(arg)) {//调用子类的tryRelease 实际就是Sync的tryReleaseNode h = head;//取同步队列的头节点if (h != null && h.waitStatus != 0)//同步队列头节点不为空且不是初始状态unparkSuccessor(h);//释放头节点 唤醒后续节点return true;}return false;
}

  Sync的tryRelease就是上一小节的重入释放方法,如果是同一个线程,那么锁的重入次数就依次递减,直到重入次数为0,此方法才会返回true,此时断开头节点唤醒后续节点去获取AQS的同步状态。

2.3 公平锁和非公平锁

  公平锁还是非公平锁取决于ReentrantLock的构造方法,默认无参构造方法是NonfairSync,含参构造方法,入参true为FairSync,入参false为NonfairSync。

public ReentrantLock() {sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}

  再分别来看看非公平锁和公平锁的实现。

static final class NonfairSync extends Sync {private static final long serialVersionUID = 7316153563782823691L;/*** Performs lock.  Try immediate barge, backing up to normal* acquire on failure.*/final void lock() {if (compareAndSetState(0, 1))//通过CAS来获取同步状态 也就是锁setExclusiveOwnerThread(Thread.currentThread());//获取成功线程占有锁elseacquire(1);//获取失败 进入AQS同步队列排队等待 执行AQS的acquire方法
        }protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}

  在AQS的acquire方法中先调用子类tryAcquire,也就是nonfairTryAcquire,见2.1小节。可以看出非公平锁中,抢到AQS的同步状态的未必是同步队列的首节点,只要线程通过CAS抢到了同步状态或者在acquire中抢到同步状态,就优先占有锁,而相对同步队列这个严格的FIFO队列来说,所以会被认为是非公平锁。

static final class FairSync extends Sync {private static final long serialVersionUID = -3000897897090466540L;final void lock() {acquire(1);//严格按照AQS的同步队列要求去获取同步状态
        }/*** Fair version of tryAcquire.  Don't grant access unless* recursive call or no waiters or is first.*/protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();//获取当前线程int c = getState();if (c == 0) {//锁未被抢占if (!hasQueuedPredecessors() &&//没有前驱节点compareAndSetState(0, acquires)) {//CAS获取同步状态
                    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;}}

  公平锁的实现直接调用AQS的acquire方法,acquire中调用tryAcquire。和非公平锁相比,这里不会执行一次CAS,接下来在tryAcquire去抢占锁的时候,也会先调用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());//头节点的后续节点为空或者不是当前线程}

  虽然公平锁看起来在公平性上比非公平锁好,但是公平锁为此付出了大量线程切换的代价,而非公平锁在锁的获取上不能保证公平,就有可能出现锁饥饿,即有的线程多次获取锁而有的线程获取不到锁,没有大量的线程切换保证了非公平锁的吞吐量。

3、 应用

3.1普通的线程锁

  标准形式如下:

ReentrantLock lock = new ReentrantLock();
try {lock.lock();//……}finally {lock.unlock();}

  这种用法和synchronized效果是一样的,但是必须显示的声明lock和unlock。

3.2 带限制的锁

public boolean tryLock()// 尝试获取锁,立即返回获取结果 轮询锁
public boolean tryLock(long timeout, TimeUnit unit)//尝试获取锁,最多等待 timeout 时长 超时锁
public void lockInterruptibly()//可中断锁,调用线程 interrupt 方法,则锁方法抛出 InterruptedException  中断锁

  具体可查看github链接里面的ReentrantLockTest。

3.3 等待/通知模型

  内置队列存在一些缺陷,每个内置锁只能关联一个条件队列(_WaitSet),这导致多个线程可能会在同一个条件队列上等待不同的条件谓词,如果每次使用notify唤醒条件队列,可能会唤醒错误的线程导致唤醒失败,但是如果使用notifyAll的话,能唤醒到正确的线程,因为所有的线程都会被唤醒,这也带来一个问题,就是不应该被唤醒的在被唤醒后发现不是自己等待的条件谓词转而又被挂起。这样的操作会带来系统的资源浪费,降低系统性能。这个时候推荐使用显式的Lock和Condition来替代内置锁和条件队列,从而控制多个条件谓词的情况,达到精确的控制线程的唤醒和挂起。具体后面再来分析下JVM的内置锁、条件队列模型和显式的Lock、Condition模型,实际上在AQS里面也提到了Lock、Condition模型。

3.4 和synchronized比较

  两者的区别大致如下:

synchronized

ReentrantLock

使用Object本身的wait、notify、notifyAll调度机制

与Condition结合进行线程的调度

显式的使用在同步方法或者同步代码块

显式的声明指定起始和结束位置

托管给JVM执行,不会因为异常、或者未释放而发生死锁

手动释放锁

  Jdk1.6之前,ReentrantLock性能优于synchronized,不过1.6之后,synchronized做了大量的性能调优,而且synchronized相对程序员来说,简洁熟悉,如果不是synchronized无法实现的功能,如轮询锁、超时锁和中断锁等,推荐首先使用synchronized,而针对锁的高级功能,再使用ReentrantLock。

 

参考资料:

https://github.com/lingjiango/ConcurrentProgramPractice

https://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html

https://www.ibm.com/developerworks/java/library/j-jtp10264/

转载于:https://www.cnblogs.com/iou123lg/p/9535710.html

Java并发编程-ReentrantLock相关推荐

  1. Java并发编程-ReentrantLock源码分析

    一.前言 在分析了 AbstractQueuedSynchronier 源码后,接着分析ReentrantLock源码,其实在 AbstractQueuedSynchronizer 的分析中,已经提到 ...

  2. Java并发编程-ReentrantLock可重入锁

    目录 1.ReentrantLock可重入锁概述 2.可重入 3.可打断 4.锁超时 5.公平锁 6.条件变量 Condition 1.ReentrantLock可重入锁概述 相对于 synchron ...

  3. Java并发编程 ReentrantLock 源码分析

    ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大. 这个类主要基于AQS(Abst ...

  4. Java 并发编程CAS、volatile、synchronized原理详解

    CAS(CompareAndSwap) 什么是CAS? 在Java中调用的是Unsafe的如下方法来CAS修改对象int属性的值(借助C来调用CPU底层指令实现的): /*** * @param o ...

  5. reentrantlock非公平锁不会随机挂起线程?_【原创】Java并发编程系列16 | 公平锁与非公平锁...

    本文为何适原创并发编程系列第 16 篇,文末有本系列文章汇总. 上一篇提到重入锁 ReentrantLock 支持两种锁,公平锁与非公平锁.那么这篇文章就来介绍一下公平锁与非公平锁. 为什么需要公平锁 ...

  6. JUC里面的相关分类|| java并发编程中,关于锁的实现方式有两种synchronized ,Lock || Lock——ReentrantLock||AQS(抽象队列同步器)

    JUC分类 java并发编程中,关于锁的实现方式有两种synchronized ,Lock AQS--AbstractQueuedSynchronizer

  7. Java并发编程71道面试题及答案

    Java并发编程71道面试题及答案 1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User). 任何线程都可以设置为守护线程和用户线程,通过方 ...

  8. Java并发编程:CopyOnWrite容器的实现

    Java并发编程:并发容器之CopyOnWriteArrayList(转载) 原文链接: http://ifeve.com/java-copy-on-write/ Copy-On-Write简称COW ...

  9. Java 并发编程——Executor框架和线程池原理

    Java 并发编程系列文章 Java 并发基础--线程安全性 Java 并发编程--Callable+Future+FutureTask java 并发编程--Thread 源码重新学习 java并发 ...

最新文章

  1. 程序员如何切入区块链去中心化应用开发
  2. NOIP2018提高组模拟题(六)
  3. Android学习(二十)Notification通知栏
  4. python查询缺失值所在位置_Python Pandas找到缺失值的位置方法
  5. 地铁框架保护的原理_继电保护的基础知识和原理(地铁)
  6. Error running tomcat8 Address localhost:1099 is already in use 错误解决
  7. GRE 隧道配置案例(静态、动态路由)
  8. 不会框架不要紧,我带你自定义框架
  9. 字节序(byte order)和位序(bit order)
  10. quartz spring 时间配置
  11. BinaryViewer(二进制查看器)使用教程(附下载)
  12. Android项目实战(八):列表右侧边栏拼音展示效果
  13. 计算机运行慢提速小技巧,教你为Win7系统加速的五个技巧
  14. 亲手制作:超级DOS工具+Vista+加强版WindowsXP Lite5.8集成
  15. JVM学习笔记(12) 垃圾回收-垃圾回收相关算法
  16. c语言课程设计报告停车系统,停车场管理系统C语言课程设计
  17. U盘产品如何做好软文推广利用软文来打造为产品引流宣传
  18. 【剑指Offer】个人学习笔记_41_数据流中的中位数
  19. 美国姑娘项美丽与邵洵美的跨国恋
  20. CoreAnimation动画(CALayer动画)

热门文章

  1. 漫步数学分析十七——连续映射上的运算
  2. 深度学习-Tensorflow1.x-CNN中的padding参数
  3. arduino运行java_IC之路(一)Proteus-Arduino仿真环境搭建
  4. CIF进口货物流程图_广州进口报关公司阿根廷红酒上海进口清关成本选择聚海
  5. matlab中double 和single数据类型的差别
  6. 【Tensorflow】Tensorflow中的卷积函数(conv2d、slim.conv2d、depthwise_conv2d、conv2d_transpose)
  7. Git使用中报错fatal: The current branch master has no upstream branch.解决方案
  8. 从JVM的角度看JAVA代码1
  9. CSS属性和值--备份
  10. mysql alidata_linux下安装mysql | 学步园