reentrantlock非公平锁不会随机挂起线程?_还在用synchronized?来试试ReentrantLock吧

bbec9672f75541c776a060ec92214243.png

ReentrantLock和synchronized的区别

开门见山,先说结论,在1.6之前的synchronized是一个重量级锁,这个锁性能属实拉跨,因为重量锁不管怎么样都要调用CPU内核态,在内核态和用户态中切换是需要耗费大量的性能的。

d98c8542e1187078eedb666b60160a82.png

所以Doug Lea为了解决这个问题,写出了ReentrantLock这个类,试图利用CAS+AQS尽量在用户态把问题解决,用过的程序员纷纷直呼真香,sun公司就看不下去了啊,凭啥我写的亲生儿子不如你这个外来的,于是在1.8之后借鉴ReentrantLock思想之后在内存模型中优化了synchronized,使其也拥有锁膨胀、锁消除、锁粗化、自旋锁等特性。

并发与并行

因为最近经常有小伙伴问,你这个线程又不是同一时间运行的,怎么叫并发呢?所以在说到锁这个概念,得先科普一下并发与并行。

你在吃饭中,电话打进来,你吃完饭再接电话,这叫不支持并发与并行;

你再吃饭中,电话打进来,你放下筷子聊完电话再吃饭,这叫并发;

你再吃饭中,电话打进来,你边吃饭边聊电话,这叫并行。

所以说,如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。只要把线程看出一个动作就能明白了。

应用场景

在绝大多数场景下可以选择synchronized,但synchronized也是有缺陷的,比如锁退化、条件锁、公平锁,读写锁等很难用synchronized来实现,这种需要考虑灵活性的情况下选择ReentrantLock会更好。

ReentrantLock原理

ReentrantLock是基于Lock之上对其进行补充完善了一些特性的一个类。

  • 基本锁的特性

1、加锁

2、解锁

  • ReentrantLock的补充特性

1、可重入

2、公平与非公平锁

让我们来看下ReentrantLock是如何实现这些特性的。

1、AQS

在JDK文档中这样描述AbstractQueuedSynchronizer:

Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues

大意是提供了一个用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器的框架,而ReentrantLock则是基于AQS实现的可重入锁。

60ffee7e90a3d5da6b599ab03452791d.png

在图中可以看到ReentrantLock中有一个内部类叫做Sync,它是用来实现这个锁的同步控制功能的基础类,这个类继承了以AQS类为基础实现了各种特性。

2、可重入锁

ReentrantLock和synchronized都是可重入锁,那什么是可重入锁呢,让我们看如下代码:

public class Test{

     Lock lock = new Lock();

     public void methodA(){
         lock.lock();
         methodB();
         lock.unlock();
     }

     public void methodB(){
         lock.lock();
         ...........;
         lock.unlock();
     }
}

当A方法获取lock锁去锁住一段需要做原子性操作的B方法时,如果这段B方法又需要锁去做原子性操作,那么A方法就必定要与B方法出现死锁,说人话就是:我要拿到锁才能执行B方法的同步代码块,但是A方法没执行完我又拿不到锁。以上,说明这就个锁无法实现可重入,一个可重入的锁在遇到这种情况的时候会进行判断,如果来抢锁的还是持有锁的线程,则它依旧可以拿到锁。

而在ReentrantLock中是如何实现可重入特性的呢?

AQS类中在获取锁之前,会进行一次判断,其中一个判断条件就是如果有线程持有锁,且当前线程为持有锁的线程,则获取锁。

3f5b3ff5379ee108de54139d313113a4.png

3、公平锁与非公平锁

大家想象一个场景,在一个售票窗口购买火车票的时候大家都不排队,每一个人买完火车票大家就一拥而上去抢着买票,而你每次都没抢到,甚至比你后来的人比你还先买到票了,你会不会非常的生气感觉非常不公平。上面这个例子抽象一下就是非公平锁的体现,把票当成需要争抢的资源,一个人比作一个线程,一个窗口比作一个锁,即每次获取锁的线程是随机的,这样是不公平的,synchronized就是这样的非公平锁。

而在ReentrantLock中,则在构造方法中提供了公平锁和不公平锁两种实现。

9e568d1c5e9b3cb3919f364772dd9cb8.png

公平锁也就是在售票窗口前放一个安保人员,强制大家排队(AQS),分个先来后到,这样就是公平的了。在接下来讲解加锁/解锁的流程使用就是的ReentrantLock实现的公平锁来举例。

4、加锁流程

74e49407aa977785b5ac79a5b962f753.png

在阅读源码之前首先要了解几个参数:

  • state:持有锁的状态,没人持有则为0,有人持有则为1,大于1则为该锁重入次数,例如state=3,说明该锁被当前持有锁的线程重入两次。
  • Node.waitStatus:当前等待线程的状态,为-1则是沉睡状态,0为唤醒状态,大于0则是线程取消,线程死亡等状态。
  • compareAndSet开头的方法:通过自旋方法来设置参数,例如compareAndSetState,通过自旋来设置State参数

首先让我们看下加锁的核心代码如下:

public final void acquire(int arg) {
        //先尝试获得锁,如果没有获得则继续执行入队代码。具体代码看tryAcquire方法
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //如果在入队过程中被其他线程打断则把当前线程打断,具体代码看acquireQueued方法
            selfInterrupt();
    }

tryAcquire方法:

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //判断当前锁是否被持有,没被持有则为0
            if (c == 0) {
                //如果不需要入队且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;
        }
    }

acquireQueued方法:

final 

值得注意的是,AQS队列头Node永远为空,因为头节点代表的持有锁的线程的状态,如果现在线程二已经拿到锁了,那么该线程所在的node就不用排队了,node对Thread的引用也就没有意义了,所以队列的head里面的Thread永远为null。其次就是自己的状态必须下一个入队的线程来设置,因为自己是无法判断自己的状态。

5、解锁流程

4737b20cfbd0eee8338b65b4b047bef6.png

解锁核心代码如下:

public 

有两个点值得注意,为了实现可重入锁,解锁的时候也并不是全解锁,每次解锁将state-1,直到减成0才是成功解锁。还有就是在unparkSuccessor方法中考虑到线程安全的复杂情况,有时候不一定能拿到等待的第一个元素,可能出现线程被取消,线程死亡等情况,这时就不是从队首开始拿元素,而是从队尾开始拿。代码如下:

private void unparkSuccessor(Node node) {
        /*
         * 设置等待线程状态为0
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * 如果等待线程为空或者等待线程状态不为正常状态(0或1)
         * 则说明该线程可能取消或者死亡
         * 则从队尾开始获得沉睡线程
         */
        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);
    }

6、打断

786dd435db67f1550afc7a048dd7f62e.png

直接使用lock也可以打断,但是使用这个方法可以执行打断策略,在catch里写入打断策略。

7、读写锁和条件锁

JUC中提供了内置的读写锁类ReentrantReadWriteLock,所谓读写锁也就是读和写使用两把不同的锁,读的锁较写的锁更为轻量。

想实现条件锁的话ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。

总结

ReentrantLock提供了对死锁,线程饥饿、线程中断等问题的处理方式,并提供多种读写锁和条件锁的实现。使用起来较synchronized更为灵活,使用的好的话性能也会有所提升。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值