java并发编程实战之理解显示锁ReentrantLock

版权声明:转载注明出处 https://blog.csdn.net/nobody_1/article/details/82793558

前面两篇博客分别介绍了通过synchronized关键字(内置锁机制)和volatile关键字实现同步机制。由于volatile实现的同步不能保证操作的原子性,因此一般常用内置锁实现同步机制,但java5.0版本的内置锁在功能上有很多缺陷:如无法中断一个正在等待获取锁的线程、无法在请求获取一个锁时无限地等待下去等,基于这些原因,ReentrantLock孕育而生。

1.ReentrantLock如何实现同步机制?

ReentrantLock类实现了Lock接口,接口把加锁操作抽象为一组方法。

public interface Lock{
	void lock();    // 获得锁,如果锁被占用,则被阻塞
	// 获得锁,如果锁被占用,则可以中断被阻塞的锁
    void lockInterruptibly() throws InterruptedException;  
	boolean tryLock();  // 尝试获得锁,如果成功则为true,否则为false
    // 在time时间内尝试获得锁
	boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 
	void unlock();   // 释放锁
	Condition newCondition();
}

从接口的方法可知,不单单提供简单的获得锁和释放锁的操作,还提供其他一些功能,这也是内置锁所不具备的特点。ReentrantLock类完全实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。

要理解ReentrantLock类实现同步的原理,就需要看一下源码的实现。

1.1ReentrantLock类的构造函数有两种方式:

    public ReentrantLock() {
        sync = new NonfairSync();
    }

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

ReentrantLock不带参数的构造方法生成一个(非公平的)sync对象;
ReentrantLock带参数构造方法根据传入的true或者false分别构造公平的、不公平的sync对象。

NonfairSync类和FairSync类都继承自抽象类Sync,它们都实现父类Sync的抽象方法:lock()和tryAcquire()。这里的lock()实现则是ReentrantLock对象实际调用的方法。

1.2 ReentrantLock类的lock(),即获得锁的过程:

  1. NonfairSync类的lock()方法
    final void lock() {
	      // 如果锁未被占有则设置锁的状态为1,并把当前线程置为锁的拥有者
	      if (compareAndSetState(0, 1))
	          setExclusiveOwnerThread(Thread.currentThread());
	      else
	           acquire(1);
    }

compareAndSetState()是抽象类AbstractQueuedSynchronizer类的方法,通过unsafe对象的本地方法compareAndSwapInt()来实现(比较并交换操作即CAS操作);AQS类内部有一个state的全局变量,该变量在ReentrantLock类中的含义是:锁获取操作的次数。因此compareAndSetState(0,1)的意思是:如果state值与方法的第一个参数相同,则设置state值为方法的第二个参数,这里表示state值是否为0,若为0则设置为1并返回true,表示该线程获得锁,否则返回false。返回true则执行方法setExclusiveOwnerThread(thread)把获得锁的线程设置为锁的拥有者。返回false则执行acquire(1)。即获取锁失败后的操作:

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

这个方法可以分三步来理解:
第一步:通过tryAcquire(arg)尝试获取锁,该方法是NonfairSync类从父类Sync类继承过来并自己实现的方法。【它和lock()方法组成了NonfairSync类】tryAcquire(arg)方法实则调用Sync类的nonfairTryAcquire(arg)方法:

    final boolean nonfairTryAcquire(int acquires) {
        // 获取当前请求锁的线程
        final Thread current = Thread.currentThread();
        // 获取当前锁的状态 state变量是AQS的全局变量
        int c = getState();
        // 若当前锁状态为0 表明锁处于空闲状态 则
        if (c == 0) {
            // 则通过CAS操作设置锁状态为acquires参数的值(默认传进来值为1) 表明锁已经被占有
            if (compareAndSetState(0, acquires)) {
                // 锁状态设置成功后 并把当前线程置为锁的拥有者
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 若锁状态不为0 表明锁已经被占有,则判断当前线程是否是该锁的拥有者,若不是则返回false,否则返回true 表明同一个线程请求锁 即重入性
        else if (current == getExclusiveOwnerThread()) {
            // state状态值加1 表明锁被重复请求,释放时需要多次释放
            int nextc = c + acquires;
            // 若锁状态小于0,则抛异常
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

如果当前锁未被占有,或者请求的线程是锁的拥有者线程(重入),则返回true;否则,返回false;
如果是重入则tryAcquire()返回true,acqure()方法直接结束;
如果不是重入,则tryAcquire()返回false,继续执行acquireQueued(addWaiter(Node.EXCLUSIVE),arg)。

第二步:既然无法获得锁,又不是重入,表明该线程要被阻塞,放入获取锁的等待队列中,通过调用addWaiter(Node mode)方法放入队列中:

    private Node addWaiter(Node mode) {
        // 把当前线程构造成一个node节点 参数mode表示Node节点类型
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        // 若tail节点不为空 tail节点为尾节点 enq方法的head节点为头结点
        if (pred != null) {
            // 则把tail节点设置为node节点的前节点
            node.prev = pred;
            // 设置node节点为tail节点  node节点的下节点则为tail节点
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 若tail节点为空,则执行enq
        enq(node);
        return node;
    }

enq():

    private Node enq(final Node node) {
        // 内旋循环
        for (;;) {
            Node t = tail;
            // 若tail节点为空,则把node节点初始化为head节点
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    // head节点同时设置为tail节点
                    tail = head;
            } else {
                // 若tail节点不为空,则把tail节点设置为node节点的前节点
                node.prev = t;
                // 设置node节点为tail节点 并把tail节点的下一节点指针指向node
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
          }
       }

addWaiter(Node. EXCLUSIVE)方法返回含有当前线程的node节点;

第三步:通过acquireQueued()方法阻塞请求锁失败后的线程:

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 获取node节点的前驱节点p
                final Node p = node.predecessor();
                // 如果前驱节点p为head节点,则node节点的线程尝试获取锁
                if (p == head && tryAcquire(arg)) {
                    // 获取成功则把node节点设置为head节点,前驱节点p的后节点断开,便于gc
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // 若前驱节点状态为SIGNAL,即为-1,则返回true
        if (ws == Node.SIGNAL)
            return true;
        // 如果前驱节点状态大于0,即前驱节点被取消了,且去掉被取消的前驱结点
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 如果前驱节点不大于0,则通过CAS设置前驱节点状态为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

如果前驱节点状态为SIGNAL,则返回true,并执行parkAndCheckInterrupt();其他状态返回false,继续循环直到返回true;

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

最后返回Thread类的interrupted()方法,即线程被阻塞

至此线程获取锁成功和失败后的流程分析完毕。总结一句话就是:某线程请求锁,如果锁没有被占有或者线程是锁的所有者则可以获得锁,即请求成功;否则请求锁的线程通过步骤二放入等待队列,通过步骤三实现线程阻塞,即请求失败。

  1. FairSync类的lock()方法:
 final void lock() {
     acquire(1);
 }

直接调用acquire(1)方法:

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

获取锁同样有三个步骤:

第一步:通过tryAcquire(1)重试获取锁;

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

在非公平锁第一步中,当锁的状态为0时,请求的线程直接获取锁;在公平锁中请求线程是否获得锁需要通过hasQueuedPredeceddors()方法进行判断:

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

结果1:若等待队列中head节点等于tail节点,即空队列,则返回fasle,从而请求线程获得锁
结果2:若head节点的后驱节点不为空,且后驱节点的线程等于当前请求锁的线程,即重入性,则返回false,从而请求线程获得锁
结果3:其他情况返回true,线程无法获得锁。’

第二步和第三步与非公平锁调用的方法相同,都是AQS类的方法。

公平锁的lock()和非公平锁的lock()的区别在于:请求的线程是否是直接获取锁。

1.3 ReentrantLock的unlock()过程,即释放锁

	public void unlock() {
	    sync.release(1);
	}

直接调用sync类的release(1)方法:

   public final boolean release(int arg) {
	    // tryRelease方法返回true则释放锁,否则不释放锁
	    if (tryRelease(arg)) {
	        Node h = head;
	        // 若head节点不为空 且head节点的状态值不为0,则
	        if (h != null && h.waitStatus != 0)
	            // 唤醒下一个node
	            unparkSuccessor(h);
	        return true;
	    }
	    return false;
	}

tryRelease()的返回值决定是否释放锁:

     protected final boolean tryRelease(int releases) {
            // 获取当前锁的状态值,并减去1
            int c = getState() - releases;
            // 若当前线程不是该锁的所有者则抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 若锁的状态值为0,则表明锁可以被释放,并清空锁的所有者
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
      }

主要根据锁的状态值来判断:
>若为0则可以释放并清空锁的所有者,并唤醒下一个等待的线程;
>若不为0则不释放锁,且修改当前锁的状态值。
ReentrantLock类关系继承图
注:更多关于AQS的内容可以参考博客(https://blog.csdn.net/pfnie/article/details/53191892)

ReentrantLock类是否可以替代Synchronized?

答案当然是不可以。ReentrantLock加锁和释放锁都是显示的,所以如果在加锁后的代码中发生异常,若没有正确的释放锁则导致该锁永远得不到释放,并且无法查找最初发生错误的位置。这一个巨大的隐患使得ReentrantLock永远无法替代Synchronized。

ReentrantLock性能 VS Synchronized性能?

java5中的ReentrantLock比内置锁提供更好的竞争性能;java6改进了内置锁的算法,使得两者性能差不多。使用只要考虑适合的场景即可,即内置锁无法满足需求时,才会考虑使用ReentrantLock。

总结

本篇博文首先分析了为什么会有ReentrantLock类,然后从源码的角度解释ReentrantLock加锁和释放锁的原理,从公平锁和非公平锁的角度分别阐述;最后简单的删除ReentrantLock和synchronized的性能区别。
注:源码分析过程中,有些术语可能不太好理解,具体可参考下面两篇博客:
https://blog.csdn.net/pfnie/article/details/53191892
https://blog.csdn.net/linxdcn/article/details/72844011
个人感觉这两篇博客都很清晰,从实际例子的角度阐述了代码的执行过程。

参考书籍:

《java并发编程实战》
《实战java高并发程序设计》

猜你喜欢

转载自blog.csdn.net/nobody_1/article/details/82793558
今日推荐