ReentrantLock — 可重入锁

 J.U.C

Java.util.concurrent(java并发应用包) 是在并发编程中比较常用的工具类,里面包含很多用来在并发场景中使用的组件。比如线程池、阻塞队列、计时器、同步器、并发集合等等。并发包的作者是大名鼎鼎的 Doug Lea。

ReentrantLock

Lock在J.U.C中是最核心的组件,J.U.C 包中的所有组件绝大部分的组件都有用到了 Lock。在 Lock 接口出现之前,Java中的应用程序对于多线程的并发安全处理只能基于synchronized关键字来解决。ReentrantLock的诞生并不是用来取代synchronized的,而是应该在synchronized无法满足的场景下再使用。

1、重入锁概念及基本方法

重入的概念是指在同一个线程内部,这种锁是可以反复进入的。一个线程可多次获取锁,但同时也要释放相同的次数,否则该线程将持续拥有锁,其他线程将无法进入临界区。

lock.lock();
lock.lock();
try{
    // do something
}finally{
    lock.unlock();
    lock.unlock();
}

方法签名方法描述
void lock();获取锁(如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放)
void lockInterruptibly();和lock()方法相似, 获取锁,但阻塞的线程可中断,抛出java.lang.InterruptedException 异常
boolean tryLock()非阻塞获取锁;尝试获取锁,不等待;如果成功返回 true
boolean tryLock(long timeout, TimeUnit timeUnit)在一定时间内尝试获取锁(过时不候)
void unlock()释放锁
//当前线程T1 tryLock() 成功后就获取到了锁。此时T2也tryLock() 返回false,那么T2就会执行到else,如果没有else 就会直接结束incr方法。
//同理tryLock(long time, TimeUnit unit) 差不多,只是获取不到锁就会进入到TIMED_WAITING状态,持续时间为指定时间。
public class ReentrantLockDemo2 {
    public static Integer count = 0;
​
    static ReentrantLock lock = new ReentrantLock();
​
    public static void incr() throws InterruptedException {
        try {
            if (lock.tryLock(10, TimeUnit.SECONDS)){
                System.out.println(Thread.currentThread().getName()+"获取到锁");
                count++;
                Thread.sleep(5000);
            }else {
                System.out.println(Thread.currentThread().getName()+"没有获取到锁,没有执行+1 操作就结束了结束incr方法");
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock.isHeldByCurrentThread()) {//查询当前线程是否保持此锁定
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "线程释放锁");
            }
            System.out.println(Thread.currentThread().getName() + "线程退出");
        }
    }
​
    public static void main(String[] args) throws InterruptedException {
​
        System.out.println("=============================================");
        new Thread(()->{
            try {
                incr();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T1").start();
​
        Thread.sleep(1000);
​
        new Thread(()->{
            try {
                System.out.println("T2 start..");
                incr();
                System.out.println("T2 end..");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T2").start();
​
        Thread.sleep(10000);
        System.out.println(count);
    }
}
​
​
​
​
//lockInterruptibly()
public class ReentrantLockDemo3 implements Runnable{
    private ReentrantLock lock = new ReentrantLock();
​
    @Override
    public void run() {
        try {
            lock.lockInterruptibly();//当一个线程通过 lockInterruptibly()方法获取锁时,如果该线程处于等待阻塞状态,则该线程可以响应中断
            System.out.println(Thread.currentThread().getName() + " running");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + " finished");
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " interrupted");
        }finally {
            if (lock.isHeldByCurrentThread()) {//查询当前线程是否保持此锁定
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + " 释放锁");
            }
        }
    }
​
    public static void main(String[] args) {
        ReentrantLockDemo3 reentrantLockDemo = new ReentrantLockDemo3();
        Thread thread01 = new Thread(reentrantLockDemo, "thread01");
        Thread thread02 = new Thread(reentrantLockDemo, "thread02");
​
        thread01.start();
        thread02.start();
​
        thread02.interrupt();
    }
​
}

1.1 中断响应

对于synchronized来说,一个线程要么获取到锁开始执行,要么继续等待。但是对于重入锁来说,提供了更灵活的一种机制,那就是在等待锁的过程中,可以取消对锁的请求,这样可以有效避免死锁的可能。

1.2 锁申请等待时间

中断响应是一种通过外部通知中断对锁的请求,从而避免死锁的一种机制。除此之外,还有一种机制,那就是等待限时。

1.3 公平锁、非公平锁

重入锁默认是非公平的。可以通过构造函数实现公平锁。如果是非公平锁,在并发场景下,系统会随机从等待队列中挑选一个线程。如果是公平锁,系统会维护一个有序队列,会按照进入队列的次序有序执行,因此公平锁虽然避免了饥饿现象,但是会需要更高的成本来维护这个有序队列。

//无参构造函数,默认非公平锁
public ReentrantLock() {
    sync = new NonfairSync();//通过内置的sync对象加锁
}
​
//传入布尔值,true-公平锁 false-非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

2.重入锁的实现原理

ReentrantLock定义了内部类SyncSync继承自AbstractQueuedSynchronizer(简称AQS),是一个同步等待队列本质上是一个带有头尾指针的双向链表。sync并不具备业务功能,所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能。Sync 有两个具体的实现类,分别是:

  • NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁

  • FailSync: 表示所有线程严格按照 FIFO 来获取锁

FairSync和NonfairSync都继承自Sync,他们的继承关系如下图,都是最终继承自AbstractQueuedSynchronizer:

img

AQS依赖先进先出队列实现了阻塞锁和相关的同步器。AQS内部有一个volatile类型的state属性,实际上多线程对锁的竞争体现在对 state 值写入的竞争。一旦 state 从 0 变为 1,代表有线程已经竞争到锁,那么其它线程则进入等待队列。等待队列是一个链表结构的 FIFO 队列,这能够确保公平锁的实现。同一线程多次获取锁时,如果之前该线程已经持有锁,那么对 state 再次加 1。释放锁时,则会对 state-1。直到减为 0,才意味着此线程真正释放了锁。

AQS 的功能分为两种:独占和共享锁,每次只能有一个线程持有锁,ReentrantLock 就是以独占方式实现的互斥锁;共享锁,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock。

private volatile int state;

重入锁的加锁和解锁过程主要有AQS完成,AQS维护了一个双向链表,每个节点Node存储一个线程及线程的状态,Head节点代表正在持有锁的线程。

当线程获取锁失败之后,就通过addWaiter加入到同步队列中(加入到尾部),自旋判断自己是否是链表的头节点,如果是头节点,就不断参试获取资源,获取成功后则退出同步队列。

2.1 公平锁加锁过程

sync.lock时调用AQS的acquire方法,这是一种模板设计模式,即AQS中定义了整体的处理流程,但是具体的实现细节会根据锁类型的不同,放到子类方法中执行。

public void lock() {
      sync.lock();
}
​
//以公平锁为例
final void lock() {
    acquire(1);
}
​
//AQS --  FairSync并没有重写acquire方法代码,因此调用的是AQS中的
public final void acquire(int arg) {
    //首先尝试调用一次tryAcquire方法,1)若返回true,则立刻返回 
    //2)若为false,则会先调用addWaiter()方法,将线程包装成一个Node,加入到等待队列;
    //再调用acquireQueued()尝试排队获取锁,如果成功后发现自己被中断过,则返回true,否则false,立刻返回
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

acquire()方法:

  • 通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false

  • 如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加到 AQS 队列尾部

  • acquireQueued,将 Node 作为参数,通过自旋去尝试获取锁

tryAcquire(int acquires)在FairSync和NonfairSync中有不同的实现。

//ReentrantLock
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {//表明目前锁空闲,没有线程占用
        if (!hasQueuedPredecessors() && //1.判断队列前是否已有线程在排队,若没有,进行第二个判断。
            compareAndSetState(0, acquires)) { //2.CAS,比较当前状态是否为0,若是,则修改为acquires
            setExclusiveOwnerThread(current);//3.修改当前锁属于的线程
            return true;
        }
    }
    //若锁被占用,判断拥有锁的线程是否为当前线程(可重入)
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;//若是,则将state + 1(acquires)
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);//设置state
        return true;
    }
    return false;
}

addWaiter(Node.EXCLUSIVE):

//AQS
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);//把当前线程封装为 Node
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;//tail 是 AQS 中表示同步队列队尾的属性,默认是 null
    if (pred != null) {//tail 不为空的情况下,说明队列中存在节点
        node.prev = pred;//把当前线程的 Node 的 prev 指向 tail
        if (compareAndSetTail(pred, node)) {//通过 cas 把 node加入到 AQS 队列,也就是设置为 tail
            pred.next = node;//设置成功以后,把原 tail 节点的 next指向当前 node
            return node;
        }
    }
    enq(node);//通过自旋操作把当前节点加入到队列中
    return node;
}
​
​
//AQS
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {//自旋
            final Node p = node.predecessor();//获取当前节点的 prev 节点
            if (p == head && tryAcquire(arg)) {//如果是 head 节点,说明有资格去争抢锁“如果是第一个排队的,那么就再去尝试获取锁”
                setHead(node);//获取锁成功,也就是ThreadA 已经释放了锁,然后设置 head 为 ThreadB 获得执行权限
                p.next = null; // help GC  -- 把原 head 节点从链表中移除
                failed = false;
                return interrupted;
            }
            //ThreadA 可能还没释放锁,使得ThreadB 在执行 tryAcquire 时会返回 false
            //检查是否应该被中断
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true; //并且返回当前线程在等待过程中有没有中断过。
        }
    } finally {
        if (failed)
            cancelAcquire(node);//取消获得锁的操作
    }
}
假设有三个线程来争抢锁,AQS中的链表结构图如下:

img

2.2公平锁与非公平锁的区别:

锁的公平性是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是 FIFO。 在上面分析的例子来说,只要CAS 设置同步状态成功,则表示当前线程获取了锁,而公平锁则不一样,差异点有两个:

首先是lock中的实现

  1. 非公平锁和公平锁最大的区别在于,在非公平锁中抢占锁的逻辑是,不管有没有线程排队,先 cas 去抢占一下

  2. CAS 成功,就表示成功获得了锁

  3. CAS 失败,调用 acquire(1)走锁竞争逻辑

//非公平锁
final void lock() {
     if (compareAndSetState(0, 1)){
        setExclusiveOwnerThread(Thread.currentThread());
     } else{
        acquire(1);
     }
}
​
//公平锁
final void lock() {
 acquire(1);
}
​

compareAndSetState()

protected final boolean compareAndSetState(int expect, int update) {
     // See below for intrinsics setup to support this
     return unsafe.compareAndSwapInt(this,stateOffset, expect, update);
}

通过CAS乐观锁的方式来做比较并替换,如果当前内存中的state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返回 false。这个操作是原子的,不会出现线程安全问题,这里面涉及到Unsafe这个类的操作,以及涉及到 state 这个属性的意义。

state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入锁的实现来说,表示一个同步状态。它有两个含义的表示

  1. 当 state=0 时,表示无锁状态

  2. 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候,state 会递增,比如重入 5 次,那么 state=5。而在释放锁的时候,同样需要释放 5 次直到 state=0 其他线程才有资格获得锁 。

Unsafe 类:Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、Hadoop、Kafka 等;Unsafe 可认为是 Java 中留下的后门,提供了一些层次操作,如直接内存访问、线程的挂起和恢复、CAS、线程同步、内存屏障,而 CAS 就是 Unsafe 类中提供的一个原子操作,第一个参数为需要改变的对象,第二个为偏移量(即之前求出来的 headOffset 的值),第三个参数为期待的值,第四个为更新后的值。整个方法的作用是如果当前时刻的值等于预期值 var4 相等,则更新为新的期望值 var5,如果更新成功,则返回 true,否则返回 false;

tryAcquire()实现:

//公平锁
protected final boolean tryAcquire(int acquires) {
 final Thread current = Thread.currentThread();
 int c = getState();
 if (c == 0) {
    //公平锁多了一个!hasQueuedPredecessors() 这个方法
    if (!hasQueuedPredecessors() &&  compareAndSetState(0, acquires)) {
         setExclusiveOwnerThread(current);
         return true;
    }
   ...
}
​
//非公平锁
protected final boolean tryAcquire(int acquires) {
 return nonfairTryAcquire(acquires);
}
​
final boolean nonfairTryAcquire(int acquires) {
   final Thread current = Thread.currentThread();
   int c = getState();
   if (c == 0) {
     if (compareAndSetState(0, acquires)) {
       setExclusiveOwnerThread(current);
       return true;
     }
     ...
}
  • 非公平锁在获取锁的时候,会先通过 CAS 进行抢占,而公平锁则不会

  • 公平锁在tryAcquire方法中多了hasQueuedPredecessors()方法,也就是加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回 true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁

2.3ReentrantLock.unlock 释放锁

在 unlock 中,会调用 release 方法来释放锁

public void unlock() {
    sync.release(1);
}
​
//AQS
public final boolean release(int arg) {
  if (tryRelease(arg)) { //释放锁成功
      Node h = head; //得到 AQS 中 head 节点
      if (h != null && h.waitStatus != 0)//如果 head 节点不为空并且状态!=0.调用 
          unparkSuccessor(h);//唤醒后续节点
      return true;
  }
  return false;
}


protected final boolean tryRelease(int releases)
{
     int c = getState() - releases; //减1(releases)
     if (Thread.currentThread() != getExclusiveOwnerThread()) //判断当前线程是否为拥有锁的线程
        throw new IllegalMonitorStateException();
     boolean free = false;//是否释放成功
     if (c == 0) {//state值减为0,表示锁释放完了(可重入)
        free = true;
        setExclusiveOwnerThread(null);//设置当前锁空闲,不被线程拥有
     }
     setState(c);//设置state
     return free;
}

tryRelease()是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值(参数是 1),如果结果状态为 0,就将排它锁(独占)的 Owner 设置为 null,以使得其它的线程有机会进行执行。

在排它(独占)锁中,加锁的时候状态会增加 1(可以修改这个值),在解锁的时候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、3、4 这些值,只有 unlock()的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true。

unparkSuccessor(h);//唤醒后续节点

private void unparkSuccessor(Node node) {
 int ws = node.waitStatus;//获得 head 节点的状态
 if (ws < 0)
     compareAndSetWaitStatus(node, ws, 0);// 设置 head 节点状态为 0
 Node s = node.next;//得到 head 节点的下一个节点
 if (s == null || s.waitStatus > 0) {
     //如果下一个节点为 null 或者 status>0 表示 cancelled 状态.
     //通过从尾部节点开始扫描,找到距离 head 最近的一个waitStatus<=0 的节点
     s = null;
     for (Node t = tail; t != null && t != node; t = t.prev){
         if (t.waitStatus <= 0)
             s = t;
     }
     if (s != null) //next 节点不为空,直接唤醒这个线程即可
         LockSupport.unpark(s.thread);
  }
}

通过锁的释放,原本的结构就发生了一些变化。head 节点的 waitStatus 变成了 0,ThreadB 被唤醒。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值