美文网首页多线程
让你不再害怕JAVA的锁(二)

让你不再害怕JAVA的锁(二)

作者: Top2_头秃 | 来源:发表于2019-05-11 15:18 被阅读42次

    让你不再害怕JAVA的锁(一)中我们介绍了java的synchronized锁。
    本篇文章将会介绍一下 Lock锁

    Lock与synchronized的对比

    类别 synchronized Lock
    存在层面 语法层面;JVM实现 API层面,是一个类,JDK实现
    锁的获取 A获得锁,B等待 A获得锁,B可以由多种情况,具体后文介绍
    锁的释放 JVM自动释放,不存在死锁 finally中必须释放,否则容易造成死锁
    锁的状态 无法判断 可以判断
    锁类型 可重入,不可中断,非公平 可重入,可中断,可公平
    性能 少量线程同步 可大量同步

    竞争资源不激烈时,两者的性能是差不多的;当有大量线程同时竞争,此时Lock的性能要远远优于synchronized

    Lock需要用lock与unlock显示指明,并在finally中unlock以防死锁

    synchronized是一种悲观锁,当有很多线程竞争锁的时候,会引起cpu频繁的切换上下文环境,导致效率很低。
    Lock是一种乐观锁,使用了CAS机制(需要CPU的支持),如果查看Lock源码,会发现一个比较重要的获得锁的方法就是compareAndSetState

    Lock与synchronized使用场景对比

    一般大部分情况下,两者是都可以使用的。但是在一些非常复杂的同步应用中,建议使用ReentrantLock,尤其是以下两种case:

    • 线程在等待一个锁的这段时间需要中断
    • 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify到哪个线程,即ReentrantLock控制粒度比synchronized要细
    • 需要用到公平锁

    ReentrantLock源码分析

    ReentrantLock是Lock的一种实现方式,相比与synchronized的非公平锁,Lock是可公平可非公平。
    公平锁(Fair):加锁前检查是否有排队等待的线程,优先排队等待的线程,有个先来后到
    非公平锁(Nonfair):加锁时压根就不会考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
    ReentrantLock默认的lock方法采用的是非公平锁

    下面我们逐步分析一下公平锁的实现原理
    1 abstract static class Sync extends AbstractQueuedSynchronizer在ReentrantLock内部定义了一个static类型的AbstractQueuedSynchronizer(这个就是传说中的AQS这个类提供了对操作系统层面线程的一些操作方法的封装调用)的子类Sync,我们看一下AQS的源码,看一下里面究竟有什么东西

    private volatile int state;
    

    CAS操作(compareAndSetState函数)其实就是在操作这个volatile类型的state,设置state+1返回true,就说明获取到了锁。
    volatile的作用说白了就是能让所有线程能够获取最新的volatile值,这是什么意思呢?请看下图的说明

    主存变量和方法栈中变量关系

    我们知道对象的成员跟对象是在堆(即主存)中的,而方法运行是在栈中的,
    如果state没有volatile的修饰,线程1和线程2同时执行CAS方法(即expect=0,update=1)来更改state的值为1,线程1更改成功后回写了堆,但是线程2并没有感知到这个变化,还认为expect=0,此时线程2update state也会成功,即与线程1一样,也获取到了锁,这显然是不对了。

    如果加了volatile修饰,线程1更改state成功后,其它线程中的state副本就会失效,线程2就会重新从主存load state值,此时在用(expect=0和update=1)的条件去更新时,就会失败,因为此时的state expect是1,就不会获取到锁

    2 看一下ReentrantLock带参数的构造方法,我们可以通过传入参数来构造公平的可重入锁

    public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
    }
    

    3 ReentrantLock中的lock调用的就是sync中的lock(),如果步骤2中的fair=true,则sync变量被初始化为 Sync的公平锁子类FairSync
    4下面我们看一下FairSync中的lock实现

    // 把公平锁和非公平锁放一块,我们看一下这俩实现到底有啥不一样
    // 公平锁的lock实现
          final void lock() {
               acquire(1);
           }
    //非公平锁的lock实现
            final void lock() {
                if (compareAndSetState(0, 1))
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
            }
    

    看见了吧,非公平锁上来就进行一次CAS,即上来就进行获取锁的操作,压根就不会考虑排队等待的问题,就没有先来后到这个意思。现在看一下这个acquire(1)是个什么沙雕函数吧 ,acquire是AQS中的一个模版方法

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

    5让我们先看一下非公平锁 NonfairSync中tryAcquire的实现逻辑

            protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);
            }
    

    nonfairTryAcquire(acquires)的源码

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

    如果当前状态state=0,就通过CAS(之后我会专门写一篇文章来介绍)操作将state的值update为1,如果成功(即获取锁成功)就执行当前线程。如果当前执行线程就是该线程,就把state++,这也是重入锁的表现所以这里无需在进行CAS的操作,看见了没这里有个nextc<0的判断,说明锁的重入也是有一定次数的限制的。

    我们再回到acquire函数源码,如下

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

    可知,如果tryAcquire失败,就acquireQueued(addWaiter(Node.EXCLUSIVE), arg),如果acquireQueued成功,就acquireQueued 这块没看明白,待之后详细研究

    通过上述分析,我们暂时知道了公平锁与非公平锁的实现原理

    acquireQueued(addWaiter(Node.EXCLUSIVE), arg)是个什么鬼?

    通过Node.EXCLUSIVE参数我们知道这是独占模式,addWaiter源码如下,看看到底干了点啥

        private Node addWaiter(Node mode) {
            Node node = new Node(Thread.currentThread(), mode);
            // Try the fast path of enq; backup to full enq on failure
            Node pred = tail;
            if (pred != null) {
                node.prev = pred;
                if (compareAndSetTail(pred, node)) {
                    pred.next = node;
                    return node;
                }
            }
            enq(node);
            return node;
        }
    

    其实就是一个双向链表,获取到链表的最后一个节点(tail)作为新加入节点Node的前驱节点,通过CAS的操作将当前线程的Node节点(其实就是当前线程的一个占位符)放入链表最后,如果CAS失败,就调用enq不停的重复CAS操作直至Node放入队列成功。

    在看一下acquireQueued()函数的源码

        final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                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;
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    

    在分析这个函数的逻辑之前,我们应该知道链表的结构如下
    Node链表的结构


    链表结构

    如果当前节点的前驱节点是head,并且可以通过casupdate state的状态,代表当前节点占有锁。此时就把当前节点设置为head,设置next为null(即从当前链表中剔除) 否则进入等待状态(即将当前线程从线程调度器上摘下)。

    示例代码

    说了这么多,我们来用实际代码看一下ReentrantLock的使用吧

    // 待更新
    

    首先我们需要知道:

    synchronized一般与Object的wait 、notify、notifyAll绑定使用,而Lock可以使用Condtion的await,signal和signalAll,一般使用ReentrantLock类作为锁

    相关文章

      网友评论

        本文标题:让你不再害怕JAVA的锁(二)

        本文链接:https://www.haomeiwen.com/subject/mjvxaqtx.html