浅谈JAVA并发之ReentrantLock

作者:ukyu 时间:2022-10-22 10:03:33 

1. 介绍

浅谈JAVA并发之ReentrantLock

结合上面的ReentrantLock类图,ReentrantLock实现了Lock接口,它的内部类Sync继承自AQS,绝大部分使用AQS的子类需要自定义的方法存在Sync中。而ReentrantLock有公平与非公平的区别,即'是否先阻塞就先获取资源',它的主要实现就是FairSync与NonfairSync,后面会从源码角度看看它们的区别。

2. 源码剖析

Sync是ReentrantLock控制同步的基础。它的子类分为了公平与非公平。使用AQS的state代表获取锁的数量


abstract static class Sync extends AbstractQueuedSynchronizer {
   private static final long serialVersionUID = -5179523762034025860L;

/**
       * Performs {@link Lock#lock}. The main reason for subclassing
       * is to allow fast path for nonfair version.
       */
   abstract void lock();

...
}

我们可以看出内部类Sync是一个抽象类,继承它的子类(FairSync与NonfairSync)需要实现抽象方法lock。

下面我们先从非公平锁的角度来看看获取资源与释放资源的原理

故事就从就两个变量开始:


// 获取一个非公平的独占锁
/**
* public ReentrantLock() {
*    sync = new ReentrantLock.NonfairSync();
* }
*/
private Lock lock = new ReentrantLock();
// 获取条件变量
private Condition condition = lock.newCondition();

2.1 上锁(获取资源)


lock.lock()

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

static final class NonfairSync extends Sync {
   private static final long serialVersionUID = 7316153563782823691L;

// 获取资源
   final void lock() {
       // 若此时没有线程获取到资源,直接设置当前线程独占访问资源。
       if (compareAndSetState(0, 1))
           setExclusiveOwnerThread(Thread.currentThread());
       else
           // AQS的方法
           acquire(1);
   }

protected final boolean tryAcquire(int acquires) {
       // 实现在父类Sync中
       return nonfairTryAcquire(acquires);
   }
}

AQS的acquire


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

// Sync实现的非公平的tryAcquire
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) // overflow
           throw new Error("Maximum lock count exceeded");
       setState(nextc);
       return true;
   }
   return false;
}

尝试获取资源的过程是非常简单的,这里再贴一下acquire的流程

浅谈JAVA并发之ReentrantLock

2.2 释放资源


lock.unlock();

public void unlock() {
   // AQS的方法
   sync.release(1);
}

AQS的release


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

release的流程已经剖析过了,接下来看看tryRelease的实现


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;
       setExclusiveOwnerThread(null);
   }
   setState(c);
   return free;
}

tryRelease的实现也很简单,这里再贴一下release的流程图

浅谈JAVA并发之ReentrantLock

2.3 公平锁与非公平锁的区别

公平锁与非公平锁,即'是否先阻塞就先获取资源', ReentrantLock中公平与否的控制就在tryAcquire中。下面我们看看,公平锁的tryAcquire


static final class FairSync extends Sync {
       private static final long serialVersionUID = -3000897897090466540L;

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

protected final boolean tryAcquire(int acquires) {
           final Thread current = Thread.currentThread();
           int c = getState();
           if (c == 0) {
               // (2.3.1)
               // sync queue中是否存在前驱结点
               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;
       }
   }

区别在代码(2.3.1)

hasQueuedPredecessors

判断当前线程的前面有无其他线程排队;若当前线程在队列头部或者队列为空返回false


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());
}

结合下面的入队代码(enq), 我们分析hasQueuedPredecessors为true的情况:

1.h != t ,表示此时queue不为空; (s = h.next) == null, 表示另一个结点已经运行了下面的步骤(2),还没来得及运行步骤(3)。简言之,就是B线程想要获取锁的同时,A线程获取锁失败刚好在入队(B入队的同时,之前占有的资源的线程,刚好释放资源)

2.h != t 且 (s = h.next) != null,表示此时至少有一个结点在sync queue中;s.thread != Thread.currentThread(),这个情况比较复杂,设想一下有这三个结点 A -> B C, A此时获取到资源,而B此时因为获取资源失败正在sync queue阻塞,C还没有获取资源(还没有执行tryAcquire)。

时刻一:A释放资源成功后(执行tryRelease成功),B此时还没有成功获取资源(C执行s = h.next时,B还在sync queue中且是老二)

时刻二: C此时执行hasQueuedPredecessors,s.thread != Thread.currentThread()成立,此时s.thread表示的是B


private Node enq(final Node node) {
   for (;;) {
       Node t = tail;
       if (t == null) { // Must initialize
           if (compareAndSetHead(new Node())) // (1) 第一次初始化
               tail = head;
       } else {
           node.prev = t;
           if (compareAndSetTail(t, node)) { // (2) 设置queue的tail
               t.next = node; // (3)
               return t;
           }
       }
   }
}

Note that 1. because cancellations due to interrupts and timeouts may occur at any time, a true return does not guarantee that some other thread will acquire before the current thread(虚假true). 2. Likewise, it is possible for another thread to win a race to enqueue after this method has returned false, due to the queue being empty(虚假false).

这位大佬对hasQueuedPredecessors进行详细的分析,他文中解释了虚假true以及虚假false。我这里简单解释一下:

1.虚假true, 当两个线程都执行tryAcquire,都执行到hasQueuedPredecessors,都返回true,但是只有一个线程执行compareAndSetState(0, acquires)成功

2.虚假false,当一个线程A执行doAcquireInterruptibly,发生了中断,还没有清除掉该结点时;此时,线程B执行hasQueuedPredecessors时,返回true

来源:https://www.cnblogs.com/ukyu/p/14802757.html

标签:Java,并发,ReentrantLock
0
投稿

猜你喜欢

  • springboot 参数格式校验操作

    2023-10-16 08:53:32
  • Spring定时任务使用及如何使用邮件监控服务器

    2023-01-12 16:38:58
  • Android使用MulticastSocket实现多点广播图片

    2023-01-16 06:13:57
  • Android 自定义标题栏 显示网页加载进度的方法实例

    2023-10-11 09:51:22
  • 浅谈Spring中单例Bean是线程安全的吗

    2023-07-12 23:42:21
  • Spring实战之使用注解实现声明式事务操作示例

    2021-08-19 07:16:07
  • 一文详解如何在Flutter中使用导航Navigator

    2022-06-15 05:52:32
  • Unity实现汽车前后轮倒车轨迹计算

    2022-12-18 17:49:47
  • Android中方法数超限问题与启动优化详解

    2023-03-05 17:55:41
  • Java使用Maven BOM统一管理版本号的实现

    2023-08-30 00:46:00
  • 单例模式 分析代码优化方法

    2021-07-28 15:49:51
  • Java解析XML的四种方法详解

    2022-07-02 23:39:33
  • c# 方法可变数量的参数

    2023-10-06 07:31:21
  • Android实现拍照截图功能

    2023-06-21 04:43:23
  • C#获取系统版本信息方法

    2022-12-13 04:45:20
  • Struts2实现对action请求对象的拦截操作方法

    2023-06-08 01:54:13
  • 使用maven开发springboot项目时pom.xml常用配置(推荐)

    2022-09-19 23:33:48
  • C# 6.0 的知识梳理

    2021-07-21 22:08:17
  • SpringBoot读取资源目录中JSON文件的方法实例

    2023-04-26 02:00:42
  • spring boot 全局异常处理方法汇总

    2021-07-06 22:44:04
  • asp之家 软件编程 m.aspxhome.com