详述重入锁-ReentrantLock

什么是重入锁?

锁主要用来控制多线程访问的行为,对于同一个线程,如果连续两次对同一把锁进行lock,那么这个线程会被卡死在那里,这样的特性很不好,在实际的开发中,方法之间的调用方式错综复杂,如果不小心可能在多个不同的方法中,反复调用 lock(),这样就会把自己卡死。

所以,重入锁就是用来解决这个问题的,重入锁使同一个线程可以对同一把锁在不释放的前提下,反复的加锁不会导致线程的卡死,唯一的一点就是需要保证 unlock() 的次数和 lock()一样的多。

重入锁的实现

Java中的锁都来自与 Lock 接口,而 ReadWriteLoc k实现的 lock 接口,本文主要分析这两个接口的几个子类的实现细节。
详述重入锁-ReentrantLock_第1张图片
而重入锁最重要的方法就是lock()。

  • lock():加锁,如果锁已经被别人占用了,就无限等待
  • tryLock(long timeout, TimeUnit unit) throws InterruptedException:尝试获取锁,等待timeout时间。同时,可以响应中断
  • unlock() :释放锁
  • tryLock():不会进行任何等待,如果能够获得锁,直接返回true,如果获取失败,就返回false
  • lockInterruptibly():可以响应中断,lock方法会阻塞线程直到获取到锁
  • newCondition():返回一个条件变量,一个条件变量也可以做线程间通信来同步线程。

重入锁实现原理

重入锁实现的主要类如下图:

详述重入锁-ReentrantLock_第2张图片
重入锁的核心功能委托给内部类 Sync 实现,并且根据是否是公平锁有 FairSync 和 NonfairSync 两种实现。这是一种典型的策略模式。

实现重入锁的方法很简单,就是基于一个状态变量 state。这个变量保存在AbstractQueuedSynchronizer对象中

private volatile int state;

当 stat e== 0 时,表示锁是空闲的,大于零表示锁已经被占用, 它的数值表示当前线程重复占用这个锁的次数。因此,lock() 的最简单的实现是:

final void lock() {
        // CAS设置共享状态,返回true表示成功获取共享状态
        if (compareAndSetState(0, 1))
            // 设置当前线程为共享状态的持有线程
            setExclusiveOwnerThread(Thread.currentThread());
        else
            // 否则调用AQS中的acquire(int arg)尝试获取同步状态,失败则加入等待队列,自旋获取共享状态
            acquire(1);
    }

下面是acquire() 的实现:

 public final void acquire(int arg) {
 //tryAcquire() 再次尝试获取锁,
 //如果发现锁就是当前线程占用的,则更新state,表示重复占用的次数,
 //同时宣布获得所成功,这正是重入的关键所在
 if (!tryAcquire(arg) &&
     // 如果获取失败,那么就在这里入队等待
     acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
     //如果在等待过程中 被中断了,那么重新把中断标志位设置上
     selfInterrupt();
}

下面我们说一下公平锁 和 非公平锁

公平的重入锁

初始化时, state = 0,表示无人抢占了锁。这时候,这时线程 A 请求锁,获得了锁,把 state + 1,如下所示:

详述重入锁-ReentrantLock_第3张图片

线程 A 取得了锁,把 state +1,这时候 state 改为 1,线程 A 继续执行其他任务,此时线程B请求锁,线程 B 无法获取锁,生成节点进行排队,如下图所示:

详述重入锁-ReentrantLock_第4张图片
初始化的时候,会生成一个空的头节点,然后才是线程 B 节点,这时候,如果线程 A 又请求锁,是否需要排队?答案当然是否定的,否则就直接死锁了。当 A 再次请求锁,这时候的状态如下图所示:

详述重入锁-ReentrantLock_第5张图片
到了这里,相信大家应该明白了什么是可重入锁了吧。就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加。如果线程 A 释放了一次锁,如下图所示:

详述重入锁-ReentrantLock_第6张图片
仅仅是把状态值减了,只有线程 A 把此锁全部释放了,状态值减到 0 了,其他线程才有机会获取锁。当线程 A 把锁完全释放后,state 恢复为 0,然后会通知队列唤醒线程 B 节点,使B可以再次竞争锁。当然,如果线程 B 后面还有线程 C,线程 C 继续休眠,除非 B 执行完了,通知了线程 C。注意,当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除。

非公平的重入锁

理解了公平锁的话,那非公平锁就容易理解了,当线程 A 执行完之后,要唤醒线程 B 是需要时间的,而且线程 B 醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程 C,那么线程 C 是有可能获取到锁的,如果线程 C 获取到了锁,线程 B 就只能继续等待休眠了。

那公平锁和非公平锁实现的核心区别在哪里呢?如下所示:

//非公平锁 
 final void lock() {
     //直接抢了再说
     if (compareAndSetState(0, 1))
         setExclusiveOwnerThread(Thread.currentThread());
     else
         //抢不到,就进队列慢慢等着
         acquire(1);
 }

 //公平锁
 final void lock() {
     //直接进队列等着
     acquire(1);
 }

我们从代码中可以看到,非公平锁如果第一次争抢失败,后面的处理和公平锁是一样的,都是进入等待队列慢慢等。

而对应tryLock()方法也非常类似的:

 //非公平锁 
final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        // 如果当前共享状态未被其他线程占用
        if (c == 0) {
            // 尝试通过CAS占有当前共享状态
            if (compareAndSetState(0, acquires)) {
                // 设置共享状态持有线程为当前线程
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 如果共享状态已被占用,则判断当前占用共享状态的线程是否就是当前线程
        else if (current == getExclusiveOwnerThread()) {
            // 如果是则自增获取次数,设值state
            int nextc = c + acquires;
            if (nextc < 0) 
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

//非公平锁 
protected final boolean tryAcquire(int acquires) {
        // 获取到当前线程
        final Thread current = Thread.currentThread();
        // 获取当前同步状态
        int c = getState();
        // 如果同步状态为0,则说明当前同步状态已完全释放
        if (c == 0) {
            // 1、hasQueuedPredecessors判断当前节点是否存在前驱节点
            // 2、如果不存在则CAS设置state的值
            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;
    }

引入 Condition

Condition 的作用与 Object.wait() 和 Object.notify() 的作用大致是相同的。但是 wait() 和 notify() 方法是与synchronized 关键字合作使用的,而 Condition 是与重入锁相关联的。通过 Lock 接口(重入锁实现了这一接口)的new Condition() 方法可以生成一个与当前重入锁绑定的 Condition 实例。利用 Condition 对象,可以让线程在合适的时间等待,或者在某一个特定的时间得到通知。

Condition 接口提供的方法

/**
  * 使当前线程进入等待状态直到被通知(signal)或中断
  * 当其他线程调用 singal() 或 singalAll() 方法时,该线程将被唤醒
  * 当其他线程调用 interrupt() 方法中断当前线程
  * await() 相当于 synchronized 等待唤醒机制中的 wait() 方法
  */
  void await() throws InterruptedException;
//当前线程进入等待状态,直到被唤醒,该方法不响应中断要求
void awaitUninterrruptibly();
 //调用该方法,当前线程进入等待状态,直到被唤醒或被中断或超时
 //其中 nanosTimeout 指的等待超时时间,单位纳秒
long awaitNanos(long nanosTimeout) throws InterruptedException;
//同 awaitNanos,但可以指明时间单位
boolean await(long time, TmeUnit unit) throws InterruptedException;
 //调用该方法当前线程进入等待状态,直到被唤醒、中断或到达某个时
 //间期限(deadline),如果没到指定时间就被唤醒,返回 true,其他情况返回 false
boolean await(Date deadline) throws InterruptedException;
//唤醒一个等待在 Condition 上的线程,该线程从等待方法返回前必须
//获取与 Condition 相关联的锁,功能与notify()相同
void signal();
//唤醒所有等待在 Condition 上的线程,该线程从等待方法返回前必须
//获取与 Condition 相关联的锁,功能与 notifyAll() 相同
void signalAll();

代码演示一下 Condition 的使用:

public class ReentrantLockCondition implements Runnable{

	public static ReentrantLock lock = new ReentrantLock();
    //通过 ReentrantLock 创建 Condition 实例,并与之关联
    public static Condition  condition = lock.newCondition();

    @Override
    public void run()
    {
        try
        {
            lock.lock();
            condition.await();
            System.out.println("Thread is going on");
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        finally
        {
            lock.unlock();
        }
    }
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		ReentrantLockCondition condition1 = new ReentrantLockCondition();
        Thread thread= new Thread(condition1 );
        thread.start();
        Thread.sleep(2000);
        lock.lock();
        condition.signal();
        lock.unlock();
	}

}

与 Object.wait() 和 Object.notify() 方法类似,当前线程使用 Condition.await() 时,要求线程持有相关的重入锁,在Condition.await() 调用后,这个线程会释放这把锁。同理,在 Condition.signal() 方法调用时,也要求线程先获得相关的锁。在 signal() 方法调用后,系统会从当前 Condtion 对象的等待队列中,唤醒一个线程。一旦线程被唤醒,它会重新尝试获得与之绑定的重入锁,一旦成功获取,就可以继续执行了。因此,在 signal() 方法调用之后,一般需要释放相关的锁,谦让给被唤醒的线程,让它可以继续执行。

总结

对于重入锁,这里我们需要知道几点:

  • 对于同一个线程,重入锁允许你反复获得通一把锁,但是,申请和释放锁的次数必须一致
  • 默认情况下,重入锁是非公平的,公平的重入锁性能差于非公平锁
  • 重入锁的内部实现是基于 CAS 操作的
  • 重入锁的伴生对象 Condition 提供了 await() 和 singal() 的功能,可以用于线程间消息通信

你可能感兴趣的:(多线程,java,java,开发语言,后端)