Java中的显示锁ReentrantLock使用与原理详解

考虑一个场景,轮流打印0-100以内的技术和偶数。通过使用 synchronize 的 wait,notify机制就可以实现,核心思路如下:
使用两个线程,一个打印奇数,一个打印偶数。这两个线程会共享一个数据,数据每次自增,当打印奇数的线程发现当前要打印的数字不是奇数时,执行等待,否则打印奇数,并将数字自增1,对于打印偶数的线程也是如此

//打印奇数的线程
private static class OldRunner implements Runnable{
  private MyNumber n;

  public OldRunner(MyNumber n) {
    this.n = n;
  }

  public void run() {
    while (true){
      n.waitToOld(); //等待数据变成奇数
      System.out.println("old:" + n.getVal());
      n.increase();
      if (n.getVal()>98){
        break;
      }
    }
  }
}
//打印偶数的线程
private static class EvenRunner implements Runnable{
  private MyNumber n;

  public EvenRunner(MyNumber n) {
    this.n = n;
  }

  public void run() {
    while (true){
      n.waitToEven();      //等待数据变成偶数
      System.out.println("even:"+n.getVal());
      n.increase();
      if (n.getVal()>99){
        break;
      }
    }
  }
}

共享的数据如下

private static class MyNumber{
  private int val;

  public MyNumber(int val) {
    this.val = val;
  }

  public int getVal() {
    return val;
  }
  public synchronized void increase(){
    val++;
    notify(); //数据变了,唤醒另外的线程
  }
  public synchronized void waitToOld(){
    while ((val % 2)==0){
      try {
        System.out.println("i am "+Thread.currentThread().getName()+" ,but now is even:"+val+",so wait");
        wait(); //只要是偶数,一直等待
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  public synchronized void waitToEven(){
    while ((val % 2)!=0){
      try {
        System.out.println("i am "+Thread.currentThread().getName()+" ,but now old:"+val+",so wait");
        wait(); //只要是奇数,一直等待
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

运行代码如下

MyNumber n = new MyNumber(0);
Thread old=new Thread(new OldRunner(n),"old-thread");
Thread even = new Thread(new EvenRunner(n),"even-thread");
old.start();
even.start();

运行结果如下

i am old-thread ,but now is even:0,so wait
even:0
i am even-thread ,but now old:1,so wait
old:1
i am old-thread ,but now is even:2,so wait
even:2
i am even-thread ,but now old:3,so wait
old:3
i am old-thread ,but now is even:4,so wait
even:4
i am even-thread ,but now old:5,so wait
old:5
i am old-thread ,but now is even:6,so wait
even:6
i am even-thread ,but now old:7,so wait
old:7
i am old-thread ,but now is even:8,so wait
even:8

上述方法使用的是 synchronize的 wait notify机制,同样可以使用显示锁来实现,两个打印的线程还是同一个线程,只是使用的是显示锁来控制等待事件

private static class MyNumber{
  private Lock lock = new ReentrantLock();
  private Condition condition = lock.newCondition();
  private int val;

  public MyNumber(int val) {
    this.val = val;
  }

  public int getVal() {
    return val;
  }
  public void increase(){
    lock.lock();
    try {
      val++;
      condition.signalAll(); //通知线程
    }finally {
      lock.unlock();
    }

  }
  public void waitToOld(){
    lock.lock();
    try{
      while ((val % 2)==0){
        try {
          System.out.println("i am should print old ,but now is even:"+val+",so wait");
          condition.await();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }finally {
      lock.unlock();
    }
  }
  public void waitToEven(){
    lock.lock(); //显示的锁定
    try{
      while ((val % 2)!=0){
        try {
          System.out.println("i am should print even ,but now old:"+val+",so wait");
          condition.await();//执行等待
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }finally {
      lock.unlock(); //显示的释放
    }

  }
}

同样可以得到上述的效果

显示锁的功能

显示锁在java中通过接口Lock提供如下功能

lock: 线程无法获取锁会进入休眠状态,直到获取成功

  • lockInterruptibly: 如果获取成功,立即返回,否则一直休眠到线程被中断或者是获取成功
  • tryLock:不会造成线程休眠,方法执行会立即返回,获取到了锁,返回true,否则返回false
  • tryLock(long time, TimeUnit unit) throws InterruptedException : 在等待时间内没有发生过中断,并且没有获取锁,就一直等待,当获取到了,或者是线程中断了,或者是超时时间到了这三者发生一个就返回,并记录是否有获取到锁
  • unlock:释放锁
  • newCondition:每次调用创建一个锁的等待条件,也就是说一个锁可以拥有多个条件

Condition的功能

接口Condition把Object的监视器方法wait和notify分离出来,使得一个对象可以有多个等待的条件来执行等待,配合Lock的newCondition来实现。

  • await:使当前线程休眠,不可调度。这四种情况下会恢复 1:其它线程调用了signal,当前线程恰好被选中了恢复执行;2: 其它线程调用了signalAll;3:其它线程中断了当前线程 4:spurious wakeup (假醒)。无论什么情况,在await方法返回之前,当前线程必须重新获取锁
  • awaitUninterruptibly:使当前线程休眠,不可调度。这三种情况下会恢复 1:其它线程调用了signal,当前线程恰好被选中了恢复执行;2: 其它线程调用了signalAll;3:spurious wakeup (假醒)。
  • awaitNanos:使当前线程休眠,不可调度。这四种情况下会恢复 1:其它线程调用了signal,当前线程恰好被选中了恢复执行;2: 其它线程调用了signalAll;3:其它线程中断了当前线程 4:spurious wakeup (假醒)。5:超时了
  • await(long time, TimeUnit unit) :与awaitNanos类似,只是换了个时间单位
  • awaitUntil(Date deadline):与awaitNanos相似,只是指定日期之后返回,而不是指定的一段时间
  • signal:唤醒一个等待的线程
  • signalAll:唤醒所有等待的线程

ReentrantLock

从源码中可以看到,ReentrantLock的所有实现全都依赖于内部类Sync和ConditionObject。

Sync本身是个抽象类,负责手动lock和unlock,ConditionObject则实现在父类AbstractOwnableSynchronizer中,负责await与signal

Sync的继承结构如下

Sync的两个实现类,公平锁和非公平锁

公平的锁会把权限给等待时间最长的线程来执行,非公平则获取执行权限的线程与线程本身的等待时间无关

默认初始化ReentrantLock使用的是非公平锁,当然可以通过指定参数来使用公平锁

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

当执行获取锁时,实际就是去执行 Sync 的lock操作:

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

对应在不同的锁机制中有不同的实现

1、公平锁实现

final void lock() {
  acquire(1);
}

2、非公平锁实现

final void lock() {
  if (compareAndSetState(0, 1)) //先看当前锁是不是已经被占有了,如果没有,就直接将当前线程设置为占有的线程
    setExclusiveOwnerThread(Thread.currentThread());
  else
    acquire(1); //锁已经被占有的情况下,尝试获取
}

二者都调用父类AbstractQueuedSynchronizer的方法

public final void acquire(int arg) {
  if (!tryAcquire(arg) &&
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //一旦抢失败,就会进入队列,进入队列后则是依据FIFO的原则来执行唤醒
    selfInterrupt();
}

当执行unlock时,对应方法在父类AbstractQueuedSynchronizer中

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

公平锁和非公平锁则分别对获取锁的方式tryAcquire 做了实现,而tryRelease的实现机制则都是一样的

公平锁实现tryAcquire

源码如下

protected final boolean tryAcquire(int acquires) {
  final Thread current = Thread.currentThread();
  int c = getState(); //获取当前的同步状态
  if (c == 0) {
    //等于0 表示没有被其它线程获取过锁
    if (!hasQueuedPredecessors() &&
      compareAndSetState(0, acquires)) {
      //hasQueuedPredecessors 判断在当前线程的前面是不是还有其它的线程,如果有,也就是锁sync上有一个等待的线程,那么它不能获取锁,这意味着,只有等待时间最长的线程能够获取锁,这就是是公平性的体现
      //compareAndSetState 看当前在内存中存储的值是不是真的是0,如果是0就设置成accquires的取值。对于JAVA,这种需要直接操作内存的操作是通过unsafe来完成,具体的实现机制则依赖于操作系统。
      //存储获取当前锁的线程
      setExclusiveOwnerThread(current);
      return true;
    }
  }
  else if (current == getExclusiveOwnerThread()) {
    //判断是不是当前线程获取的锁
    int nextc = c + acquires;
    if (nextc < 0)//一个线程能够获取同一个锁的次数是有限制的,就是int的最大值
      throw new Error("Maximum lock count exceeded");
    setState(nextc); //在当前的基础上再增加一次锁被持有的次数
    return true;
  }
  //锁被其它线程持有,获取失败
  return false;
}

非公平锁实现tryAcquire

获取的关键实现为nonfairTryAcquire,源码如下

final boolean nonfairTryAcquire(int acquires) {
  final Thread current = Thread.currentThread();
  int c = getState();
  if (c == 0) {
    //锁没有被持有
    //可以看到这里会无视sync queue中是否有其它线程,只要执行到了当前线程,就会去获取锁
    if (compareAndSetState(0, acquires)) {
      setExclusiveOwnerThread(current); //在判断一次是不是锁没有被占有,没有就去标记当前线程拥有这个锁了
      return true;
    }
  }
  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;
}

释放锁的机制

protected final boolean tryRelease(int releases) {
  int c = getState() - releases;
  if (Thread.currentThread() != getExclusiveOwnerThread()) //只能是线程拥有这释放
    throw new IllegalMonitorStateException();
  boolean free = false;
  if (c == 0) {
    //当占有次数为0的时候,就认为所有的锁都释放完毕了
    free = true;
    setExclusiveOwnerThread(null);
  }
  setState(c); //更新锁的状态
  return free;
}

从源码的实现可以看到

ReentrantLock获取锁时,在锁已经被占有的情况下,如果占有锁的线程是当前线程,那么允许重入,即再次占有,如果由其它线程占有,则获取失败,由此可见,ReetrantLock本身对锁的持有是可重入的,同时是线程独占的

公平与非公平就体现在,当执行的线程去获取锁的时候,公平的会去看是否有等待时间比它更长的,而非公平的就优先直接去占有锁

ReentrantLock的tryLock()与tryLock(long timeout, TimeUnit unit):

public boolean tryLock() {
//本质上就是执行一次非公平的抢锁
return sync.nonfairTryAcquire(1);
}

有时限的tryLock核心代码是 sync.tryAcquireNanos(1, unit.toNanos(timeout));,由于有超时时间,它会直接放到等待队列中,他与后面要讲的AQS的lock原理中acquireQueued的区别在于park的时间是有限的,详见源码 AbstractQueuedSynchronizer.doAcquireNanos

为什么需要显示锁

内置锁功能上有一定的局限性,它无法响应中断,不能设置等待的时间

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 详解Java多线程编程中互斥锁ReentrantLock类的用法

    0.关于互斥锁 所谓互斥锁, 指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 而现在, Lock提供了比synchronized机制更广泛的锁定操作, Lock和synchronized机制的主要区别: synchronized机制提供了对与每个对象相关的隐式监视器锁的访问, 并强制所有锁获取和释放均要出现在一个块结构中, 当获取了多个锁时, 它们必须以相反的顺序释放. synchronized机制对锁的释放是

  • 详解java并发之重入锁-ReentrantLock

    前言 目前主流的锁有两种,一种是synchronized,另一种就是ReentrantLock,JDK优化到现在目前为止synchronized的性能已经和重入锁不分伯仲了,但是重入锁的功能和灵活性要比这个关键字多的多,所以重入锁是可以完全替代synchronized关键字的.下面就来介绍这个重入锁. 正文 ReentrantLock重入锁是Lock接口里最重要的实现,也是在实际开发中应用最多的一个,我这篇文章更接近实际开发的应用场景,为开发者提供直接上手应用.所以不是所有方法我都讲解,有些冷门

  • Java多线程中ReentrantLock与Condition详解

    一.ReentrantLock类 1.1什么是reentrantlock java.util.concurrent.lock中的Lock框架是锁定的一个抽象,它允许把锁定的实现作为Java类,而不是作为语言的特性来实现.这就为Lock的多种实现留下了空间,各种实现可能有不同的调度算法.性能特性或者锁定语义.ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,但是添加了类似锁投票.定时锁等候和可中断锁等候的一些特性.此外,它还提供了在激烈争用情况下更

  • Java源码解析之可重入锁ReentrantLock

    本文基于jdk1.8进行分析. ReentrantLock是一个可重入锁,在ConcurrentHashMap中使用了ReentrantLock. 首先看一下源码中对ReentrantLock的介绍.如下图.ReentrantLock是一个可重入的排他锁,它和synchronized的方法和代码有着相同的行为和语义,但有更多的功能.ReentrantLock是被最后一个成功lock锁并且还没有unlock的线程拥有着.如果锁没有被别的线程拥有,那么一个线程调用lock方法,就会成功获取锁并返回.

  • Java并发系列之ReentrantLock源码分析

    在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可见性.在大多数情况下,这些机制都能很好地完成工作,但却无法实现一些更高级的功能,例如,无法中断一个正在等待获取锁的线程,无法实现限定时间的获取锁机制,无法实现非阻塞结构的加锁规则等.而这些更灵活的加锁机制通常都能够提供更好的活跃性或性能.因此,在Java5.0中增加了一种新的机制:Reentrant

  • Java并发编程之显示锁ReentrantLock和ReadWriteLock读写锁

    在Java5.0之前,只有synchronized(内置锁)和volatile. Java5.0后引入了显示锁ReentrantLock. ReentrantLock概况 ReentrantLock是可重入的锁,它不同于内置锁, 它在每次使用都需要显示的加锁和解锁, 而且提供了更高级的特性:公平锁, 定时锁, 有条件锁, 可轮询锁, 可中断锁. 可以有效避免死锁的活跃性问题.ReentrantLock实现了 Lock接口: 复制代码 代码如下: public interface Lock {  

  • 深入理解java内置锁(synchronized)和显式锁(ReentrantLock)

    synchronized 和 Reentrantlock 多线程编程中,当代码需要同步时我们会用到锁.Java为我们提供了内置锁(synchronized)和显式锁(ReentrantLock)两种同步方式.显式锁是JDK1.5引入的,这两种锁有什么异同呢?是仅仅增加了一种选择还是另有其因?本文为您一探究竟. // synchronized关键字用法示例 public synchronized void add(int t){// 同步方法 this.v += t; } public stati

  • Java并发之ReentrantLock类源码解析

    ReentrantLock内部由Sync类实例实现. Sync类定义于ReentrantLock内部. Sync继承于AbstractQueuedSynchronizer. AbstractQueuedSynchronizer继承于AbstractOwnableSynchronizer. AbstractOwnableSynchronizer类中只定义了一个exclusiveOwnerThread变量,表示当前拥有的线程. 除了Sync类,ReentrantLock内部还定义了两个实现类. No

  • java ReentrantLock详解

    介绍 ReentrantLock称为重入锁,比内部锁synchonized拥有更强大的功能,它可中断.可定时.设置公平锁 [注]使用ReentrantLock时,一定要释放锁,一般释放放到finnal里写. 提供以下重要的方法 lock():获得锁,如果锁已被占用,则等待 lockInterruptibly():获得锁,但有限响应中断 unlock():释放锁 tryLock():尝试获取锁.如果获得,返回true:否则返回false tryLock(long time, TimeUnit un

  • Java中的显示锁ReentrantLock使用与原理详解

    考虑一个场景,轮流打印0-100以内的技术和偶数.通过使用 synchronize 的 wait,notify机制就可以实现,核心思路如下: 使用两个线程,一个打印奇数,一个打印偶数.这两个线程会共享一个数据,数据每次自增,当打印奇数的线程发现当前要打印的数字不是奇数时,执行等待,否则打印奇数,并将数字自增1,对于打印偶数的线程也是如此 //打印奇数的线程 private static class OldRunner implements Runnable{ private MyNumber n

  • java 中同步方法和同步代码块的区别详解

    java 中同步方法和同步代码块的区别详解 在Java语言中,每一个对象有一把锁.线程可以使用synchronized关键字来获取对象上的锁.synchronized关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁). 问题的由来: 看到这样一个面试题: //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized (obj){} } synchronized用于解决同步问

  • Java中关于二叉树的概念以及搜索二叉树详解

    目录 一.二叉树的概念 为什么要使用二叉树? 树是什么? 树的相关术语! 根节点 路径 父节点 子节点 叶节点 子树 访问 层(深度) 关键字 满二叉树 完全二叉树 二叉树的五大性质 二.搜索二叉树 插入 删除 hello, everyone. Long time no see. 本期文章,我们主要讲解一下二叉树的相关概念,顺便也把搜索二叉树(也叫二叉排序树)讲一下.我们直接进入正题吧!GitHub源码链接 一.二叉树的概念 为什么要使用二叉树? 为什么要用到树呢?因为它通常结合了另外两种数据结

  • java 中mongodb的各种操作查询的实例详解

    java 中mongodb的各种操作查询的实例详解 一. 常用查询: 1. 查询一条数据:(多用于保存时判断db中是否已有当前数据,这里 is  精确匹配,模糊匹配 使用regex...) public PageUrl getByUrl(String url) { return findOne(new Query(Criteria.where("url").is(url)),PageUrl.class); } 2. 查询多条数据:linkUrl.id 属于分级查询 public Lis

  • java中 Set与Map排序输出到Writer详解及实例

     java中 Set与Map排序输出到Writer详解及实例 一般来说java.util.Set,java.util.Map输出的内容的顺序并不是按key的顺序排列的,但是java.util.TreeMap,java.util.TreeSet的实现却可以让Map/Set中元素内容以key的顺序排序,所以利用这个特性,可以将Map/Set转为TreeMap,TreeSet然后实现排序输出. 以下是实现的代码片段: /** * 对{@link Map}中元素以key排序后,每行以{key}={val

  • java 中基本算法之希尔排序的实例详解

    java 中基本算法之希尔排序的实例详解 希尔排序(Shell Sort)是插入排序的一种.也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本.希尔排序是非稳定排序算法.该方法因DL.Shell于1959年提出而得名. 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序:随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止. 基本思想:算法先将要排序的一组数按某个增量d(n/2,n为要排序数的个数)分成若干组,每组中记录的下标相差

  • java 中枚举类enum的values()方法的详解

    java 中枚举类enum的values()方法的详解 前言: 关于枚举,相信使用的已经很普遍了,现在主要写的是枚举中的一个特殊方法,values(), 为什么说特殊呢,因为在Enum 的 API 文档中也找不到这个方法.接下来就看看具体的使用. 理论上此方法可以将枚举类转变为一个枚举类型的数组,因为枚举中没有下标,我们没有办法通过下标来快速找到需要的枚举类,这时候,转变为数组之后,我们就可以通过数组的下标,来找到我们需要的枚举类.接下来就展示代码了. 首先是我们自己的枚举类. public e

  • Java中的引用和动态代理的实现详解

    我们知道,动态代理(这里指JDK的动态代理)与静态代理的区别在于,其真实的代理类是动态生成的.但具体是怎么生成,生成的代理类包含了哪些内容,以什么形式存在,它为什么一定要以接口为基础? 如果去看动态代理的源代码(java.lang.reflect.Proxy),会发现其原理很简单(真正二进制类文件的生成是在本地方法中完成,源代码中没有),但其中用到了一个缓冲类java.lang.reflect.WeakCache<ClassLoader,Class<?>[],Class<?>

  • 基于Java中最常用的集合类框架之HashMap(详解)

    一.HashMap的概述 HashMap可以说是Java中最常用的集合类框架之一,是Java语言中非常典型的数据结构. HashMap是基于哈希表的Map接口实现的,此实现提供所有可选的映射操作.存储的是对的映射,允许多个null值和一个null键.但此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 除了HashMap是非同步以及允许使用null外,HashMap 类与 Hashtable大致相同. 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性

  • Java中由substring方法引发的内存泄漏详解

    内存溢出(out of memory ) :通俗的说就是内存不够用了,比如在一个无限循环中不断创建一个大的对象,很快就会引发内存溢出. 内存泄漏(leak of memory) :是指为一个对象分配内存之后,在对象已经不在使用时未及时的释放,导致一直占据内存单元,使实际可用内存减少,就好像内存泄漏了一样. 由substring方法引发的内存泄漏 substring(int beginIndex, int endndex )是String类的一个方法,但是这个方法在JDK6和JDK7中的实现是完全

随机推荐