欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

juc - ReentrantLock源码解读(一)

程序员文章站 2022-04-19 08:23:16
...

ReentrantLock,翻译过来叫做重入锁,是实现线程安全的一个方式,和synchronized的作用类似,但是他的实现原理是什么呢,在查看了很多的博客之后,我决定自己写一篇,形成自己的理解。从ReentrantLock的方法一个一个的来吧。

补充:博客中会不停的提到一个叫做标记的概念,是我自己给起的名字,就是AbstractQueuedSynchronizer的private volatile int state;对象,在ReentrantLock中,如果当前对象为0则表示锁没有被任何线程占有,否则表示被占有了,只有当前占有锁的线程可以继续获得锁(通过调用lock或者trylock方法),继续占有锁会继续增加这个属性的值(没调用一次lock或者trylock就会加一)

 

1、new ReentrantLock() 

    构造方法,在这个方法里面会形成一个NonfairSync,也就是不公平的同步器,即并不是公平的,先来的线程不一定先获得锁(当然还有一个含有参数的构造方法,可以形成一个公平的同步器,先不看这个。在这篇博客中都是以不公平的锁来做说明的。)。NonfairSync是ReentrantLock.Sync的子类,而ReentrantLock.Sync是AbstractQueuedSynchronizer(也就是常说的aqs)的子类,用来做同步器。

 

2、lock()

    关键方法,尝试获得锁的操作。在javadoc中已经说明了,如果当前线程没有获得锁且当前的锁没有被其他线程捕获,就会获得锁,并将标记置为1;如果已经获得锁了会继续获得锁,并且将标记加一;如果当前的锁被其他线程获得,则当前线程被挂起。他的内部实现是使用之前形成的sync对象,调用的是sync的lock方法,

final void lock() {
   if (compareAndSetState(0, 1))//如果当前的标记为是0(即没有线程占有当前的锁),那么原子性的将标记为设置为1。
       setExclusiveOwnerThread(Thread.currentThread());//如果上面返回true,则设置持有锁的线程为当前的线程。这个方法比较简单,不做过多的介绍
    else
       acquire(1);//如果当前的锁被占有(可能是当前的线程,也可能不是当前的线程)
}

 下面挨个方法的分析:

2.1 comapreAndSetState(int expect,int value):这个方法在AbstractQueuedSynchronizer中,是CAS的操作,CAS是一个机器操作命令,使用非java语言实现,可以黑盒的理解为他是原子性的,意思是如果标记的值是expect的值,那么就原子性的将标记置为vlaue的值,如果标记的不是则gai

2.2 acquire(1),方法很重要,存在于aqs中

public final void acquire(int arg) {//这里的arg是1,
   if (!tryAcquire(arg) &&
       acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
       selfInterrupt();
}

 这个方法会先调用tryAcquire,即尝试获得锁,如果获得成功,则返回true,否则返回false,如果返回true的话就结束改方法,此时已经获得了锁;如果返回false,则调用acquireQueued。我们先看看tryAcquire方法的实现:

 final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();//获得标记
            if (c == 0) {//如果标记为0,表示当前的锁没有被占用,
                if (compareAndSetState(0, acquires)) {//使用cas将标记置为1,这个和上面的lock方法的if分支是一个道理。
                    setExclusiveOwnerThread(current);
                    return true;//成功获得锁,返回true
                }
            }else if (current == getExclusiveOwnerThread()) {//如果当前的锁被占用了且占用锁的线程是当前的线程,
                int nextc = c + acquires;//增加标记的值,增大1,即上面说的,在已经占用了锁的情况下没调用一次lock或者trylock都会增大标记,增大1.
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);//这个方法并不是cas的,为什么呢?不害怕并发的问题吗?  原因很简单,因为当前的锁已经被当前的线程持有,也就是只有一个线程在运行,所以不会出现并发问题。
                return true;//成功获得锁,返回true。
            }
            return false;//没有获得锁,返回false。
}

 看完了tryAcquire方法,继续看一下addWaiter方法,这个方法用于将调用tryAcquire方法并返回false,也就是没有获得锁的线程加入到等待队列中,Node只是封装了线程

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;//队列的尾巴
        if (pred != null) {//当不是第一次进入的时候,pred不是null,则进入if,
            node.prev = pred;//将新的node的prev指向尾巴
            if (compareAndSetTail(pred, node)) {//原子性的更新tail,如果成功进入if,这里有点绕,如果我进入if后,其他线程又更新成功了呢?会不会出错啊?不会的,因为这里仅仅是更新tail,即使更新了100个tail,前面进入if的线程已经获得了更新tail之前的tail(也就是pred),仍然能将链表串联起来。
                pred.next = node;//将原先的tail的尾巴指向最新的tail
                return node;
            }
        }
        enq(node);//当第一次进入的时候tail是null,进入这个方法。这个方法很简单,就是原子性的设置tail和head的Node,这个node的状态为0,不再贴代码了。
        return node;
    }

 再看一下acquireQueued方法:这个方法用于将没有获得锁的线程挂起。

 final boolean acquireQueued(final Node node, int arg) {//node表示封装了没有获得锁的线程的对象,arg为1.
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {//这个是死循环的原因是因为现在没有获得锁的node的状态时exclusive,在下面的shouldParkAfterFailedAcquire方法中会检查这个状态,然后更新为signal,这样线程就能被挂起了
                final Node p = node.predecessor();//node的上一个节点
                if (p == head && tryAcquire(arg)) {//如果上一个节点是head,即当前的线程是要马上获得锁的线程,并且尝试获得了锁,则进入if,此时当前的线程不被挂起。
                    setHead(node);//从这个地方可以得出这样的结论:head有两种情况,一个是无意义的,也就是第一次有阻塞的线程的时候,第二种是之前被阻塞的线程获得锁之后就会被设置为head。
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&  parkAndCheckInterrupt())//在这个方法中会更新node的状态,将head的状态变为signal(-1),默认在创建head的时候是0,
                                                                                        然后将当前的线程挂起。这个方法很重要,当被挂起的线程又运行后,还是从这里运行,进入下一轮for循环,会进入到上面的if,这样锁就可以被下一个线程捕获了。
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

没有获得锁的线程在acquireQueued之后,有两种可能:一是被挂起,也就是进入了上面的if(shouldPa....分支,二是当前的线程获得锁。 至此,获得锁的过程就完了用几句话总结下就是:如果当前的标记是0,则当前的线程获得锁,如果当前的标记不为0 ,则看当前的线程是否是已经获得锁的线程,如果是则将标记加一,如果不是,则将当前的线程加入到一个队列中,并将当前的线程挂起。从这里可以得出,如果调用了m次lock,而调用了m次的unlock,就会造成其他线程获取不到锁。

 

3、lockInterruptibly():这个和Lock很像,不同之处在于,如果当前的线程如果在获取锁之前或者获取不到在加入到队列之后再重新获取到锁之后被interrupt,就会抛一个InterruptedException。代码我就不贴了,基本类似上面的代码。

4、tryLock():这个方法更简单,尝试获取锁,如果获取不到返回false,不会加入到队列中等待,如果获取得到,则返回true,如果当前线程已经获取了锁,也返回true。

5、getHoldCount():去的标记的大小,也就是当前线程加锁的次数。如果是0表示当前的线程没有获得锁。

6、isHeldByCurrentThread():判断当前的锁是不是由当前的线程持有。

7、isLocked:判断当前的锁是否加锁了

8:、hasQueuedThreads:判断当前的锁是否是多个线程在获取,他的内部实现是判断队列的head和tail是否相等,也就是判断是否有其他的线程加入了争夺锁的竞争中。

9、unlock:释放所,如果当前的线程只 加了一次锁,则会释放,否则仍然会持有锁,但是标记一定减1.最终调用的是NonFairSync的release方法,参数是1:

public final boolean release(int arg) {
        if (tryRelease(arg)) {//当标记为0的时候返回true,也就是锁当前没有被任何线程获得
            Node h = head;
            if (h != null && h.waitStatus != 0)//这里的head可能不存在,即在当前的线程获得了锁之后没有其他的线程加入到队列中。当head不是null的话,唤醒head后面阻塞的线程(head也可能是封装当前的线程的node)
                unparkSuccessor(h);//unpark方法会唤醒head的下一个节点,下一个节点在acquireQueued中阻塞了,唤醒后继续做for循环,可以参考acquireQueued方法。
            return true;
        }
        return false;
    }

 

这样就算是看完了ReentrantLock的所有的关键点了,不过还没有看公平的锁,下一个博客中介绍这个。 

 

相关标签: ReentrantLock aqs