多线程——ReentranLock重入锁

版权声明:本文为博主原创文章,欢迎转载哦 https://blog.csdn.net/wgp15732622312/article/details/81276037

   前言

  对于阅读源码来说,能够提高自己的理解里,根据源码逆推代码的功能和目的。对于理解项目需求来说,能够更加的快速。

   阅读源码,也能够提高自己的见识,对设计模式有更加深刻的体会。

  第三点,也是最终要的一点,阅读源码,得带着问题去阅读,首先罗列自己的几点问题,然后再去理解。这点对于纯粹看源码来说要有效率多了。一开始本人阅读时,就没有一个问题,结果读的发怵了,产生了一种对编程极其厌烦的地步,在工位上就做不住,知道今天才想通了一点。最近阅读了两个锁对象,重入锁,读写锁。先拿重入锁开刀。

1、重入锁时怎样实现重入的

2、公平锁与非公平锁的区别

3、ReentranLock和AQS的关系

3、加锁和放锁的原理。

先对这四个问题进行猜想:

解决重入应该是这样:对于获得锁的线程,重新进入加锁的代码区域时,不需要获得锁,但是计数器得加1。每次释放锁的时候计数器减1。减完了,这个线程才释放锁。

第2个根据了解,公平锁获取时,必须是等待最长的一个线程,也就是说等待获得锁的线程必须排队,排在最前面的线获得锁。

非公平锁获得锁时,不用排队,能抢到锁就获得锁。

3、AQS和ReentranLock有模板方法设计模式的关系,那么我们就注重AQS与AQS子类的关系。

4、猜想加锁是对一个变量进行判断,如果成功了,就获得锁,否则就获得锁失败。

应用

先来看看,如何应用重入锁。

    Lock lock = new ReentrantLock();
    public void methodA() {
        lock.lock();
        System.out.println("methodA");
        try {
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        lock.unlock();
    }

在应用来,对于重入锁的代码,有以下几步

a)实例化锁对象

b) lock方法,获得锁。防止其他线程进入该代码块。

c) unlock方法,释放锁。其他线程可以进入。

对于a来说看源码,从源码中,我们可以看到java默认是非公平锁,如果想要公平锁的话,加上参数fair为true即可。

   /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

再看lock方法

在源码里重入锁实际上是调用的sync的lock方法,sync类是一个内部类,它继承了AQS类。而sync的lock方法呢,它是个抽象方法,具体的实现交给了FairSync类和NonfairSync。对于AQS类和Sync类,以及Sync的子类,从这里呢,我们可以看到一个模板方法模式,把具体变化的部分交给子类去实现。属于不会变化的一部分由父类实现。具体分析写到下面,先解决我们lock获得锁的实现。先看下公平锁的实现

    //sync子类的方法    
final void lock() {
   acquire(1);
}

//AQS的acquire方法,但是tryAcquire是个抽象方法,具体实现由子类完成

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

上图代码的语义是:如果获取锁失败,就把当前线程变为一个节点添加到等待队列里,然后再调用 acquireQueued方法进入获得锁阻塞等待状态。

对上面的三个方法:tryAcquire和addWaiter还有acquireQueued方法,第一个属于Sync类,其实Sync类的tryAcquire也是一个抽象方法,具体实现交给了Sync的子类。其余两个属于AQS类。到这里我们就可以清楚的看到AQS和Sync类是一个模板方法模式的应用。在《多线程并发编程的艺术》一书中,有说过利用AQS创建属于自己的同步器组件,其实tryAcquire就是创建同步器组件的关键,对于锁的实现来说,最终重要的三点:加锁,加锁不成功放入等待队列,放锁,放锁成功通知等待队列里的线程。其中

加锁不成功AQS帮我们完成了,放锁不成功AQS也帮我们完成了。所以我们实现加锁和放锁的方法就能够创建出一个同步器组件。

说到这里,来看看重入锁的公平锁对象的获得锁的策略

 protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                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;
        }
    }

1、判断计数器是否为0,如果为0表示没有线程获得锁,那么直接获取锁,直接进行cas操作,修改state的值。如果是线程重入的情况的话,那么直接记性计数器加accquires操作。注意前面还有一个hasQueuedPredecessors方法。来看看这个方法

    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

这个方法大意就是说头结点和尾节点相同时,即第一个获得锁时,头结点和尾节点相同都为null,或者等待队列里没有节点时,头结点和尾节点是同一个节点。返回false,另一个就是头结点有后继节点,即有线程在等待队列里。

这两种情况分别说明了,公平锁实现获锁策略时,第一次获得锁时,可以直接获取锁对象,但是一旦出现第二个线程获得锁时,要想获得锁,必须进入等待队列里。

再来看看非公平锁的策略:

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        protected final boolean tryAcquire(int acquires) {
            return 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;
        }

这个方法说明了,进入lock方法的线程,将会直接进行CAS操作来获取锁。tryAcquire方法调用的是nonfairTryAcquire方法,而且也缺少了hasQueuedPredecessors方法的判断,这就说明了,采用非公平锁策略的线程,进入lock方法时,直接尝试获取锁,是一种抢占式获得锁。

到这里我们基本上解决了我们最开始的问题,state变量来标识获得锁的状态,如果重入的话,state进行加1操作。tryAcquire方法说明了AQS和Sync类是一种模板方法的实现。对于加锁的实现也基本上有了了解。最后看下放锁的步骤

    public void unlock() {
        sync.release(1);
    }
  public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
 protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

对于上面的步骤,无非就是对state变量进行减1操作,如果state减为0了,那么久释放重入锁当前线程,置空,等待GC。在这里我们要注意的是,公平锁和非公平锁的放锁方法是一样的。都是Sync类的放锁策略。所以公平锁和费公平锁的区别即使线程刚进入获锁策略的那段代码。如果线程一旦进入等待队列里,那么就无所谓公平锁和非公平锁了。unparkSuccessor方法在AQS里实现,会释放等待队列里的头结点方法。到这里放锁的大概过程,我们也了解了。

最后说一个特别有意思的一个事情 ,就是在唤醒头结点时,是利用尾节点从后往前遍历获得头结点,而不是利用head直接获取后继节点。有人说在往等待队列里添加节点时,刚设置完尾节点,但是pred.next还没有设置,如果此时通过head.next获得节点时会得到null。而利用尾节点的tail.prev将会得到node节点,有节点而获得null的情况,看官你怎么看呢?

猜你喜欢

转载自blog.csdn.net/wgp15732622312/article/details/81276037