谈谈对ReentrantLock的理解

什么是ReentrantLock?

ReentrantLock 是 Java 的 JUC(java.util.concurrent)包中提供的一种可重入锁,是一种递归无阻塞的同步机制。ReentrantLock 等同于synchronized关键字,但是 ReentrantLock 提供了比 synchronized 更强大,更灵活的锁机制,可以减少死锁发生的概率

ReentrantLock与synchronized的区别?

ReentrantLock 和 synchronized 在基本用法、行为、语义上都是类似的,同样具有可重入性。只不过相比原生的 synchronized,ReentrantLock 增加了一些高级的扩展功能,如公平锁、绑定多个 Condition(公平锁和Condition下面会进行讲解)。虽然 ReentrantLock 等同于 synchronized,而且性能也差不多。但是 ReentrantLock 相比 synchronized 而言功能更加丰富,而且使用起来更加方便。为了更好地理解,画个表

ReentrantLocksynchronized
灵活性支持响应中断,超时,尝试获取锁不灵活
锁类型公平锁 & 非公平锁非公平锁
条件队列可关联多个条件队列关联一个条件队列
锁实现机制依赖AQS监视器模式
可重入性可重入可重入
释放形式必须显示调用unlock()方法释放锁自动释放监视器

什么是可重入性?

可重入性就是说可以支持一个线程对锁的重复获取。在原生的 synchronized 就具有可重入性,比如一个 synchronized 修饰的递归方法,当线程在执行期间,它是可以反复获取到锁的,不会出现死锁的情况。ReentrantLock 也是一样,在调用lock() 方法的时候,已经获取到锁的线程,能够再次调用 lock()方法获取锁反而不会阻塞

什么是公平锁?什么是非公平锁?

ReentrantLock 还提供了公平锁(fair)和非公平锁(unfair)。所谓的公平锁就是指锁的获取策略相对公平,当多个线程在获取同一个锁的时候,必须按照锁的申请时间来一次获得锁,也就是按顺序来;非公平锁就不一样了,当锁被释放的时候,等待中的线程均有机会获得锁。synchronized是非公平锁,而 ReentrantLock 也是一样。但是 ReentrantLock 可以通过构造方法接收一个可选的fair参数(默认是非公平锁),当传入的值为 true 时则表示是公平锁,源码如下:

// 使用以下代码实现公平策略
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

实现公平锁只需编写以下代码即可:

// 传入布尔值true实现公平锁
ReentrantLock lock = new ReentrantLock(true);

关于FairSync和NonfairSync等下会说

ReentrantLock实现了哪些接口?

public class ReentrantLock implements Lock, java.io.Serializable {
    
}

Lock

什么是Lock?

在jdk1.5以后,增加了juc并发包且提供了Lock接口用来实现锁的功能,它除了提供了与synchroinzed关键字类似的同步功能,还提供了比synchronized更灵活api实现

Lock有哪些方法?

从源码可以看到 ReentrantLock 实现了 Lock 接口和 Serializable,Serializable是序列化接口,这个不懂得朋友可以自行查阅一下,这里不做详细介绍,我们来看下 Lock 这个接口:

public interface Lock {

    /**
      * 尝试获取锁,获取不到则阻塞等待,不响应中断
      */
    void lock();

    /**
      * 尝试获取锁,获取不到则阻塞等待,响应中断
      */
    void lockInterruptibly() throws InterruptedException;

    /**
      * 尝试获取锁,立即返回,获取成功返回true,失败则返回false
      */
    boolean tryLock();

    /**
      * 超时获取锁,获取到则返回,获取不到知道超时时间过,返回false
      */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    /**
     * 释放锁,需要在持有锁的线程中调用,否则会抛IllegalMonitorStateException
     * 一般放到finally块中执行,确保在碰到任何异常,都正确能释放锁
     */
    void unlock();

    /**
     * 新建一个同步等待条件
     */
    Condition newCondition();
}

ReentrantLock的静态内部类——Sync

从源码可以看到Sync继承了一个 AbstractQueuedSynchronizer 类(简称AQS)

private final Sync sync;

// Sync继承了AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    // 主要为了子类可以尝试快速非公平的获取锁
    abstract void lock();

    // 非公平尝试获取锁,公平与非公平实现的tryLock方法都会调用这个来尝试非公平的获取一次
    final boolean nonfairTryAcquire(int acquires) {
        
        // 获取当前线程
        final Thread current = Thread.currentThread();
        
        //  获取状态码
        int c = getState();
        
        // 如果当前状态为0代表锁没有被获取
        if (c == 0) {
            // CAS设置状态,成功后调用setExclusiveOwnerThread方法设置持有锁的线程
            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;
        
        // 释放锁的当前线程必须是只有锁的线程,否则无法释放,会抛出IllegalMonitorStateException异常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        
        // 因为是可重入锁,所以状态为0的时候才需要setExclusiveOwnerThread(null)
        // 用于清空持有锁的线程,并且返回布尔值true
        // 返回true时会在release方法触发唤醒等待锁的线程
        // 只有当前线程完全释放锁,其他的线程才可以去得到锁
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

    // 判断当前线程是否是持有锁的线程
    protected final boolean isHeldExclusively() {
        // 虽然在一般情况下我们必须先读取所有者状态,
	    // 我们不需要检查当前线程是否为所有者
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

    // ConditionObject是AQS中的等待队列,类似于Object类中的wait和notify,这里暂时不介绍
    final ConditionObject newCondition() {
        return new ConditionObject();
    }

	// 获取当前持有锁的线程
    final Thread getOwner() {
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }

    // 获取重入持有锁的次数
    final int getHoldCount() {
        return isHeldExclusively() ? getState() : 0;
    }

    // 判断当前线程是否被锁
    final boolean isLocked() {
        return getState() != 0;
    }

    // 反序列化
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        // 需要重置锁状态为0
        setState(0); // reset to unlocked state
    }
}

setExclusiveOwnerThread

setExclusiveOwnerThread方法如下:

// 独占模式同步的当前所有者
private transient Thread exclusiveOwnerThread;

// exclusiveOwnerThread属性是AQS从父类AbstractOwnableSynchronizer中继承的属性
// 用来保存当前占用同步状态的线程
protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

getExclusiveOwnerThread

方法如下:

可以发现这个getExclusiveOwnerThread跟上面的setExclusiveOwnerThread是对应的,一个设置一个获取

// 返回由setExclusiveOwnerThread方法设置的最后的一个线程
protected final Thread getExclusiveOwnerThread() {
    return exclusiveOwnerThread;
}

setState

setState方法如下:

// 设置一个新的状态码
protected final void setState(int newState) {
    state = newState;
}

getState

getState方法如下:

可以看到这个也是跟上面的setState方法也对应的,一个设置一个获取

// 获取状态码
protected final int getState() {
    return state;
}

哪些类继承了Sync?

在文章开头我们讲过ReentrantLock的实现有公平锁和非公平锁,我们先来看下公平锁——FairSync

FairSync

// 公平锁实现
static final class FairSync extends Sync {
    // 序列ID
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        // 调用AQS的acquire方法,直接执行 AQS 的正常的同步状态获取逻辑
        acquire(1);
    }

    // 尝试公平获取锁
    protected final boolean tryAcquire(int acquires) {
        // 获取当前线程
        final Thread current = Thread.currentThread();
        
        // 获取当前状态码
        int c = getState();
        
        // 如果状态码为0则代表可以获取锁
        if (c == 0) {
            
            // 公平锁和非公平锁的主要区别在于:
            // 检查是否队列中没有等待的线程,如果没有等待的线程才尝试CAS修改锁状态,目的是保证先来获取的一定先获			   取到
            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;
    }
}

说明:公平锁和非公平锁都调用了acquire()方法。而acquire()方法是公平锁(FairSync)与非公平锁(UnfairSync)的父类AQS中的核心方法

hasQueuedPredecessors

hasQueuedPredecessors 方法源码如下,该方法是位于AQS:

在比较公平与非公平锁获取同步状态的过程,会发现两者唯一的区别在于公平锁在获取童虎状态的时候多了一个限制条件——hasQueuedPredecessors(),是否有前序节点,如果有则返回true,表示自己不是首个等待获取同步状态的节点

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; // 尾节点
    Node h = head; // 头节点
    Node s;
    
    // 如果 头节点 != 尾节点
    return h != t &&
        // 同步队列第一个节点不为null,当前线程是同步队列第一个节点
        ((s = h.next) == null || s.thread != Thread.currentTh read());
}

NonfairSync

既然有公平锁就有非公平锁,来看下非公平锁的实现

// 非公平锁的实现
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    final void lock() {
        // 非公平锁实现会尝试快速获取一次,获取失败则调用acquire方法
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    // 非公平的去尝试获取锁
    protected final boolean tryAcquire(int acquires) {
        // 调用的nonfairTryAcquire上面讲到过,不会的朋友往上翻翻
        return nonfairTryAcquire(acquires);
    }
}

acquire方法如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
    	// addWaiter(Node.EXCLUSIVE)加入等待队列
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire方法如下:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

如果该方法返回了True,则说明当前线程获取锁成功,就不用往后执行了;如果获取失败,就需要加入到等待队列中。加入队列方法如下:

private Node addWaiter(Node mode) {
	// 通过当前的线程和锁模式新建一个节点
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
	// pred指针指向尾节点tail
    Node pred = tail;
    if (pred != null) {
    	// 将Node中的prev指针指向pred
        node.prev = pred;
        
        // 通过compareAndSetTail方法完成尾节点的设置。这个方法主要是对tailOffset和Expect进行比较,如果tailOffset的Node节点和Expect的Node节点地址是相同的,那么设置Tail的值为修改后的值
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

acquireQueued方法如下:

final boolean acquireQueued(final Node node, int arg) {
	// 标记是否成功拿到锁
    boolean failed = true;
    try {
    	// 标记等待过程中是否中断过
        boolean interrupted = false;
		// 开启自旋(自旋的简单理解是要么获取锁,要么中断)
        for (;;) {
	
			// 获取当前节点的前驱节点
            final Node p = node.predecessor();

			// 如果p是头节点,则说明当前节点在真实数据队列的首部,尝试获取锁
			// (注意:头节点是虚节点)
            if (p == head && tryAcquire(arg)) {
            	// 成功获取锁,头指针移动到当前node
                setHead(node);

				// 方便GC
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
			
			// 说明p为头节点并且当前没有获取到锁(有可能是非公平锁被抢占了)或者是p不为头节点
			// 这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

setHead方法如下:

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

shouldParkAfterFailedAcquire方法如下:

// 靠前驱节点判断当前线程是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

	// 获取头节点的节点状态
   int ws = pred.waitStatus;
	
	// 表示头节点处于唤醒状态
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;

	// 通过枚举值知道 waitStatus 是取消状态
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
        	// 循环向前查找取消节点,把取消节点从队列中去掉
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
         // 设置前驱节点等待状态为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

enq方法如下:

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

说明:如果没有被初始化,需要进行初始化一个头结点出来。但请注意,初始化的头结点并不是当前线程节点,而是调用了无参构造函数的节点。如果经历了初始化或者并发导致队列中有元素,则与之前的方法相同。其实,addWaiter就是一个在双端链表添加尾节点的操作,需要注意的是,双端链表的头结点是一个无参构造函数的头结点

AQS中的静态代码块

static {
    try {
         stateOffset = unsafe.objectFieldOffset
             (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
         headOffset = unsafe.objectFieldOffset
             (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
         tailOffset = unsafe.objectFieldOffset
             (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
         waitStatusOffset = unsafe.objectFieldOffset
             (Node.class.getDeclaredField("waitStatus"));
         nextOffset = unsafe.objectFieldOffset
             (Node.class.getDeclaredField("next"));

     } catch (Exception ex) { throw new Error(ex); }
 }

说明:

  • 如果通过CAS设置变量State(同步状态)成功,也就是说获取锁成功,则将当前线程设置为独占线程
  • 如果通过CAS设置变量State(同步状态)失败,也就是说获取锁失败,则进入acquire()方法进行后续的处理

当某个线程获取锁失败之后的后续流程有以下两个可能

  • 将当前线程获取锁的结果设置为失败,获取锁流程结束。这种设计会极大地降低系统的并发度,但是并不满足我们的实际需求。所以就需要AQS框架的处理
  • 存在某种排队等候机制的时候,线程继续等待,仍然保留获取锁的可能,获取锁的流程仍在继续

示例:

public ReentrantLock() {
     sync = new NonfairSync();//默认是非公平的
}

或者这样也可以:

在创建 ReentrantLock 的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁

ReentrantLock lock = new ReentrantLock(true);

ReentrantLock如果与AQS关联的?(以非公平锁为例)

加锁
  • 通过ReentrantLock的加锁方法(Lock)进行加锁的操作
  • 调用内部类Sync的Lock方法,由于Sync的lock方法是抽象的,根据ReentrantLock初始化选择的公平锁与非公平锁执行相关的内部类的Lock方法,本质上都会执行AQS中的acquire()方法
  • AQS的acquire()方法会执行tryAcquire()方法,但是由于tryAcquire()需要自定义同步器实现,因此执行了ReentrantLock中的tryAcquire方法,由于ReentrantLock是通过公平锁与非公平锁内部类实现的tryAcquire()方法,所以会根据锁类型的不同,来执行不同的tryAcquire()方法
  • tryAcquire是获取锁逻辑,获取失败后,会执行AQS框架的后续逻辑,与ReentrantLock自定义的同步器无关
加锁解锁
apilockunlock
AQS核心方法acquirerelease
自定义同步器实现的方法tryAcquire、nonfairTryAcquiretryRelease

解锁

  • 通过ReentrantLock的解锁方法(unlock)进行解锁
  • unlock方法会调用内部类Sync的release方法,该方法来自于继承的AQS
  • release中会调用tryRelease方法,tryRelease方法需要自定义同步器实现,tryRelease只在ReentrantLock中的Sync实现,因此可以看出,释放锁的过程,并不区分是否为公平锁
  • 释放成功后,所有处理都由AQS完成,与自定义同步器无关

总结

OK,先来总结一下公平锁与非公平锁

  • FairSync:lock()方法相当于少了插队的环节(简单地说就是少了CAS尝试将state从0设置为1的过程,从而获得锁的过程)

  • FairSync:tryAcquire(int acquires)则多了需要判断当前线程是否在等待队列首部(简单地说就是少了再次插队的环节,但是CAS获取还是要的)

    公平锁是指当锁可用的时候 ,在锁上等待时间最长的线程将获得锁的使用权。然而非公平则是随机分配这种使用权的。与synchronized一样,默认的ReentrantLock的实现是非公平锁,因为相比公平锁,非公平锁性更更好一点。当然也不是说公平锁不好,公平锁能防止饥饿,在某些情况下情况下也是很有用的。

    说了那么多没点实战性的东西怎么理解?OK,现在就来介绍下ReentrantLock的简单使用,先来实现一下公平锁是怎样的:

    public class DemoString{
    
        private static final Lock lock = new ReentrantLock(true);
    
        public static void main(String[] args) {
            // 创建线程
            new Thread(() -> test(),"Thread1").start();
            new Thread(() -> test(),"Thread2").start();
            new Thread(() -> test(),"Thread3").start();
            new Thread(() -> test(),"Thread4").start();
            new Thread(() -> test(),"Thread5").start();
        }
    
        public static void test(){
            for (int i = 0; i < 2; i++) {
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + " 获得锁");
                    TimeUnit.SECONDS.sleep(2);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        }
    }
    

    这段代码相信聪明的你们应该都看得懂,上面的代码中我们开启了5个线程,让每个线程都获取释放两次。为了能够更好的观察到结果,在每次获取锁之前让线程休眠10ms。从下面的结果可以看出线程几乎是轮流获取到了锁

在这里插入图片描述

我们再来看下非公平锁的实现:

public class DemoString{

    private static final Lock lock = new ReentrantLock(true);

    public static void main(String[] args) {
        new Thread(() -> test(),"Thread1").start();
        new Thread(() -> test(),"Thread2").start();
        new Thread(() -> test(),"Thread3").start();
        new Thread(() -> test(),"Thread4").start();
        new Thread(() -> test(),"Thread5").start();
    }

    public static void test(){
        for (int i = 0; i < 2; i++) {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " 获得锁");
                TimeUnit.SECONDS.sleep(2);
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
}

上面的代码只是修改了一个布尔值而已,至于为什么贴全部代码也是为了让初学ReentrantLock的朋友更好的理解,可以看到非公平锁的线程会重复获取锁。如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁,这也就是非公平锁的饥饿问题
在这里插入图片描述

所以说在大部分情况下我们会选择使用非公平锁,因为它的性能比公平锁好很多。但是公平锁能够避免线程饥饿问题,所以某些情况下也是很有用的(看场景)

ReentrantLock简单使用方法

讲了那么多都没说怎么使用,那怎么搞?不慌,现在就来使用一下:

这里有一点需要注意的是 :unlock必需在finally块中,以保证锁的释放;lock必需在try{}finally外面,防止未获取到锁仍然做额外的释放

public class Demo {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try {
            // 代码块......
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值