浅谈ReentrantLock中公平锁的加锁和解锁

前言

 作为一个程序员,相信大家或多或少了解或听说过锁的概念。锁,在现实中,我们的锁是用来保护我们想要保护的东西的,比如说隐私、财产...。在程序中,锁是用来实现线程安全的(多线程操作共享数据,保证数据的准确)。作为Java程序员,相信大家见过最多的一个锁应该是synchronized(Java的一个关键字,这里笔者不对synchronized做讲解)。Java在1.5的版本,推出了一系列并发的东西,其中包含一个Lock接口,以及一个抽象类AbstractQueuedSynchronizer(AQS)。Lock下有一个实现类,名为ReentrantLock,笔者本次主要讨论ReentrantLock中的公平锁。

问题

在此,笔者抛出一个疑问,为什么Java已经有synchronized来保证线程安全的锁,为什么后面还推出ReentrantLock?

1.什么是ReentrantLock

ReentrantLock是一个可重入互斥锁,具有与使用synchronized方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。

2.ReentrantLock的结构

 

3.FairSync分jie析

1.FairSync的加锁过程

以下是FairSync加锁一个大概的流程图解,建议有兴趣的去仔细阅读源码,有很多细节未在图上体现。

 接下来上代码

1.首先调用acquire方法(加锁的入口方法)

//加锁的入口方法
public final void acquire(int arg) {
     if (!tryAcquire(arg) &&
         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         selfInterrupt();
}

2. 调用 tryAcquire(int acquires):尝试获取锁

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //hasQueuedPredecessors() 判断自己是否需要排队
        //compareAndSetState 加锁,只有当队列中没有元素才会尝试加锁
        // setExclusiveOwnerThread设置当前持有锁的线程
        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;
}
------------------------------------以上hasQueuedPredecessors()方法的内部----------

public final boolean hasQueuedPredecessors() {
    //t:获取AQS的队列尾
    //h:获取AQS的队列头
    //s:临时变量
    Node t = tail; 
    Node h = head;
    Node s;

    //h != t:判断队列是否有等待的线程
    //(s = h.next) == null :判断队列第二个元素是否存在
    //s.thread != Thread.currentThread(): 第二个线程是否为当前线程
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

3.如果获取锁失败调用addWaiter:初始化(维护)队列

//mode=null
private Node addWaiter(Node mode) {
    //创建一个Node: 设置当前线程,以及下一个服务员为null
    /** Node(Thread thread, Node mode) {     // Used by addWaiter
                    this.nextWaiter = mode;
                    this.thread = thread;}
               **/ 
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

---------------------------以上代码enq()方法的内部------------------------------------------
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        //如果尾为null就初始化头、尾
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //如果队列已初始化,此线程的Node的上一个指向队尾,队尾的下一个指向此线程的Node
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

入队并可能进行睡眠(此方法里的细节能推测出本章开头的问题)

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        //Theard.interrup才会有作用
        boolean interrupted = false;
        for (;;) {
            //获取改线程节点的上一个节点
            final Node p = node.predecessor();
            //如果上一个节点是对头,则尝试获取一次锁(只有第一个排队的才有资格去获取锁)
            if (p == head && tryAcquire(arg)) {
                //拿到锁后,重置头节点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //shouldParkAfterFailedAcquire:睡眠前将前一个的waitStatus标识改为-1,方便解锁
            //parkAndCheckInterrupt睡眠
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

2.FairSync的解锁过程

 代码演示

1.调用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;
    }

2.调用tryRelease尝试释放锁

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    //当前线程不是持有锁的线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();

    //free锁自由标识
    boolean free = false;
    if (c == 0) {
        free = true;
        //清除当前持有锁的线程
        setExclusiveOwnerThread(null);
    }
    //可重入锁的释放
    setState(c);
    return free;
}

3.假设队列中有等待的线程,调用unparkSuccessor唤醒后面等待的线程,并重置头节点的waitStatus状态为0

private void unparkSuccessor(Node node) {
     /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
     int ws = node.waitStatus;
     if (ws < 0)
         compareAndSetWaitStatus(node, ws, 0);

     /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         * 
         */
     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);
 }

4.问题解答

为什么Java已经有synchronized来保证线程安全的锁,为什么后面还推出ReentrantLock?

在多线程并发时,所有的线程在某些特定情况下并不是竞争执行,而是顺序执行。

举个例子,现在有个 T1,T2两个线程,假设T1比T2先执行,大部分情况下T2是在T1执行完的情况下再去执行的,所以这里没有对锁的争夺。而synchronized(1.6以前的版本)只要线程一进来就直接加锁,而这个关键字则会调用的底层的操作系统来进行加锁,这里会有资源的开销(用户态->内核态),而ReentrantLock在Java层面就将这类情况解决了。

ReentrantLock在让线程睡眠时,会让线程再次(一共2次)尝试获取锁,笔者从这可以推测,Doug Lea尽可能不想让线程去睡眠。

当然,在一些场景下,比如线程竞争执行的情况特别多,ReentrantLock相较于synchronized(1.6以前的版本)的性能又低了很多。

至此公平锁讲解完毕,由于笔者文字功底不是很扎实,所以有些细节没有体现,本文的初衷是希望读者能大概了解ReentrantLock中的一些过程,并强烈要求自己去亲自阅读源码,感受一下Doug Lea的魅力。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值