美文网首页多线程
让你不再害怕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