深入理解ReentrantLock

锁的分类

按照Java虚拟机对锁的实现方式划分,Java平台中锁可分为内部锁显示锁

  • 内部锁:也被称为监视器锁,是一种排他锁,也就是独占锁,是通过synchronized关键字实现,在我们之前 深入理解synchronized关键字文章中有详细分析
  • 显示锁:自jdk1.5开始引入的排他锁,是通过java.concurrent.locks.Lock接口的实现类,起作用与内部锁相同。它提供了一些内部锁不具备的特性。但并不是内部锁的替代品

Lock

Lock接口的实现类:

  • ReentrantLock:重入锁,也是Lock接口的默认实现类
  • ReentrantReadWriteLock:重入读写锁,排他锁的改进版
  • StampedLock:Java 8引入了新的读写锁

今天我们只要来讲一下Lock接口的默认实现类ReentrantLock

ReentrantLock的使用

public static int count=0;
static Lock lock=new ReentrantLock();
public static void incr(){
	lock.lock();//获得锁
    try {
        Thread.sleep(1);
         count++;
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally{
    	lock.unlock();//释放锁
    }
       
}
public static void main( String[] args ) throws InterruptedException {
    for(int i=0;i<1000;i++){
        new Thread(()->App.incr()).start();
    }
    Thread.sleep(3000); //保证线程执行结束
    System.out.println("运行结果:"+count);
}

显示锁ReentrantLock的用法也很简单,通过Lock接口定义的lock方法和unlock方法分别用于申请和释放相应Lock实例所代表的锁。

ReentrantLock的分类

ReentrantLock 分为公平锁非公平锁,可以通过构造方法来指定具体类型,默认为非公平锁:

 public ReentrantLock() {
     sync = new NonfairSync();//默认构造非公平锁
 }

 /**
  * Creates an instance of {@code ReentrantLock} with the
  * given fairness policy.
  *
  * @param fair {@code true} if this lock should use a fair ordering policy
  */
 public ReentrantLock(boolean fair) {
     sync = fair ? new FairSync() : new NonfairSync();
 }
  • 公平锁:指多个线程按照申请锁的顺序来获取锁,类似于排队打饭,先来后到,不能插队。
  • 非公平锁:多个线程情况下获取锁的顺序并不是按照申请锁的顺序,谁抢到就是谁的。

ReentrantLock源码分析

ReentrantLock 默认为非公平锁,那么我们先来看一下非公平锁是如何实现的:

非公平锁获得锁

不同于公平锁按照顺序依次获得。非公平锁是抢占模式,一进来,就先去抢占锁。

final void lock() {
if (compareAndSetState(0, 1))
    setExclusiveOwnerThread(Thread.currentThread());
else
    acquire(1);
}

通过compareAndSetState(CAS)执行原子操作,通过地址拿到当前锁的state值,如果是0,就变成1,表示抢占到了锁,同时宣布主权,将当前占有该锁的线程修改为当前线程setExclusiveOwnerThread(Thread.currentThread())

 private volatile int state;//0表示锁没有被占
 
 protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

否则,进入队列acquire(1)

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

里面有三个方法:

  • tryAcquire(arg):实现抢占和重入锁
  • addWaiter(Node.EXCLUSIVE):添加到队列
  • acquireQueued(node,arg):将没有获得锁的线程阻塞
    我们来一个个分析
tryAcquire(arg)
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;
}

先拿到当前线程,然后再去看当前锁的状态,如果为0,就去抢占。
如果占有锁的线程与当前线程相同时,将state+1,并返回true,即lock方法执行完毕,这就是重入锁的实现

addWaiter(Node.EXCLUSIVE)

Node.EXCLUSIVE表示独占,与之相反的还有Node.SHARED共享

static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
private Node addWaiter(Node mode) {
	//初始化一个node
   	Node node = new Node(Thread.currentThread(), mode);
    //最后一个node
    Node pred = tail;
    //链表不为空,直接将节点添加到链表后面
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

队列是通过双向链表实现的,所以每个node都会有一个next指向下一个节点,一个pred指向上一个节点,这里就是把新来的节点插入到链表

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { 
        	//创建一个假的head节点,后面会用到
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

这里需要注意的一个点:当tail为空的时候,创建一个虚拟节点同时指向head和tail,为什么会有一个虚拟节点呢?我们后面会讲到。

acquireQueued(node,arg)
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        //采用自旋方式,一直循环
        for (;;) {
        	//获得当前线程节点的前一个节点
            final Node p = node.predecessor();
            // 如果前一个节点是 head ,tryAcquire()就尝试获取锁
            // 如果 获取成功,就将当前节点设置为 head,注意 head 节点是永远不会唤醒的。
            //因为head没有前一个node
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //获取不到锁,就需要阻塞当前线程
            //阻塞之前,需要改变前一个节点的状态。如果是 SIGNAL 就阻塞,否则就改成 SIGNAL
            //这是为了提醒前一个节点释放锁完后能够叫醒自己
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

采用自旋的方式一致循环。拿到当前节点的前一个节点,如果前一个节点是head节点,再次执行tryAcquire(arg)
如果成功占有锁,则将当前节点变成head节点,同时,将与之前head节点的链接断开

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;
    //前一个节点状态为SIGNAL,可以阻塞
    if (ws == Node.SIGNAL)
        return true;
    //前一个节点状态为取消,需要跳过前一级,再试
    if (ws > 0) {
        do {
        	//将前一级的前一级赋值给当前节点的前一级
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
         //通过cas操作将状态改为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

AbstractQueuedSynchronizer(AQS)通过waitStatus状态去管理每个节点,默认值为0

  • SIGNAL = -1 :表示后续线程需要释放
  • CANCELLED = 1:表示线程已取消

所以,每个节点在休眠前,需要将前一个节点的waitStatus设置为SIGNAL,否则,自己将无法被唤醒

所以,我们才要在没有队列的时候去创建一个虚拟的head节点。

公平锁获得锁

公平锁与非公平锁的差异主要在获取锁:

公平锁就相当于买票,后来的人需要排到队尾依次买票,不能插队。所以,在第一部尝试获得锁的时候需要去判断有没有队列,而不是直接去抢占

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

第一次尝试获得锁:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //锁没有被占
    if (c == 0) {
    	//还要去判断是否有队列,如果没有线程在排队,才能去抢占
        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;
}

需要去判断队列是否为空:

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

其他的就和非公平锁的实现方式差不多,这里就不展示了。接下来,我们再看看释放锁的方法

释放锁

公平锁和非公平锁的释放流程都是一样的:

public void unlock() {
 sync.release(1);
}
public final boolean release(int arg) {
	//将锁的状态减一
   if (tryRelease(arg)) {
       Node h = head;
       //因为所有的节点在将自己挂起之前,都会将前置节点设置成 SIGNAL,希望前置节点释放的时候,唤醒自己。
        // 如果前置节点是 0 ,说明前置节点已经释放过了。
       if (h != null && h.waitStatus != 0)
           unparkSuccessor(h);
       return true;
   }
   return false;
}

通过tryRelease方法,先将锁的state值减一

protected final boolean tryRelease(int releases) {
	//将锁的state-1
	//因为有重入锁,所以state可能大于1
    int c = getState() - releases;
    //如果当前线程不是当前拥有锁的线程,报异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //当拥有锁的线程取消后,将拥有锁线程的值变为null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

再去获得head节点,去查看head的waitStatus状态,因为所有的节点在将自己挂起之前,都会将前置节点设置成 SIGNAL,所以如果有后续需要被唤醒的线程的话,head的waitStatus一定是为1的。如果前置节点是 0 ,说明前置节点已经释放过了,不需要重复操作。
接着执行unparkSuccessor方法,将节点的waitStatus恢复为0初始状态,防止重复操作

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

   //判断当前节点的下一个节点
   //如果节点为空,或者状态为取消,则需要向后移动以找到实际的非取消后继者。
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //唤醒下一个线程
    if (s != null)
        LockSupport.unpark(s.thread);
}

最后,唤醒下一个休眠的线程。唤醒之后呢,在接着将自己设置为head,并将前任head与自己的链接断掉。

总结

由于公平锁每次在占有锁之前都需要去判断一下是否存在队列。如果有线程在等待,需要将自己的挂起,同时唤醒最前面的线程,造成了大量的线程切换,而非公平锁则没有这个限制。这也导致非公平锁的效率会被公平锁更高。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值