深入了解ReentrantLock


在介绍Lock的时候,首先我们要先了解锁的释放和获取,在java内存中究竟怎么处理的。

锁的释放和获取的内存语义

当线程释放锁时,java内存模型(以下简称JMM)会把线程对应的本地内存中的共享变量刷新到主内存中。在这里插入图片描述
当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得监视器保护的临界区代码必须从主内存中读取共享变量。
在这里插入图片描述
对比锁释放-获取的内存语义与volatile写-读的内存语义可以看出:锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。

总结:

  • 线程A释放一个锁,实际是线程A向接下来将要获取这个锁(线程B)的某个线程发出了(线程A对共享变量修改的)消息;
  • 线程B获取一个锁,实际是线程B接收了之前某个线程(线程A)发出的(在释放这个锁之前对共享变量所做修改的)消息;
  • 线程A释放锁,线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

锁内存语义的实现

借助ReentrantLock源代码,来分析锁内存语义具体的实现机制。

    public void test(){
        int a = 0;
        Lock lock = new ReentrantLock();
        lock.lock();
        try{
            a++;
        }finally {
            lock.unlock();
        }
    }

ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。

ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(简称之为AQS,AQS使用一个整型的volatile变量(命名为state)来维护同步状态,volatile修饰的state保证了变量可见性,volatile变量是ReentrantLock内存语义实现的关键。

AQS

AQS是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法getState()setState(int newState)compareAndSetState(int expect,int update)来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,AQS既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLockReentrantReadWriteLockCountDownLatch等)。
AQS是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用AQS实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;AQS面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和AQS很好地隔离了使用者和实现者所需关注的领域。
AQS的设计是基于模板方法模式的,写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态

  1. getState():获取当前同步状态。
  2. setState(int newState):设置当前同步状态。
  3. compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

AQS的模板方法提供了独占式获取与释放同步状态、共享式获取与释放同步状态。

独占锁

独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。

我们下边要说的无论是公平锁或非公平锁,都是通过ReentrantLock去定义的,。都是独占式锁。

lock() 获取锁(公平锁)

ReentrantLock分为公平锁和非公平锁,也就是FairSyncNonfairSync,同时继承自Sync

  • 如果锁未被另一个线程持有,则获取该锁并立即返回,将锁持有计数设置为1。
  • 如果当前线程已经持有锁,那么持有计数将增加1,并且方法立即返回。
  • 如果锁被另一个线程持有,则当前线程将因线程调度而禁用,并处于休眠状态,直到获得锁为止,此时锁持有计数设置为1。
	//公平锁的同步对象
    static final class FairSync extends Sync {
        final void lock() {
            acquire(1);
        }
    }

Sync继承AQS,而公平锁FairSync继承自Sync。

acquire(int arg) 主要负责同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作。其主要逻辑是:首先调用自定义AQS实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。

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

通过调用AQS的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //Node为空时,头部添加
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                //尾部添加
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

上述代码compareAndSetTail(Node expect,Node update)方法来确保节点能够被线
程安全添加。在enq(final Node node)方法中,AQS通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。

节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)

公平锁的tryAcquire方法,真正的加锁
除非递归调用或没有服务生或是第一个,否则不要授予访问权限。

    protected final boolean tryAcquire(int acquires) {
          final Thread current = Thread.currentThread();
          //获取锁的开始,首先读通过volatile修饰的state变量
          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;
      }

AQS的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);
        }
    }

acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态。
原因:

  1. 头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
  2. 维护同步队列的FIFO原则

由于非首节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自
己的前驱是否是头节点,如果是则尝试获取同步状态。
acquire(int arg)方法调用流程,同时也是独占式同步状态获取流程,大致是这样的:
在这里插入图片描述
前驱节点为头节点且能够获取同步状态的判断条件和线程进入等待状态是获
取同步状态的自旋过程。当同步状态获取成功之后,当前线程从acquire(int arg)方法返回,如果对于锁这种并发组件而言,代表着当前线程获取了锁

同步状态
private volatile int state;

使用公平锁时,加锁方法lock()调用流程:

  1. ReentrantLock:lock()
  2. FairSync:lock()
  3. AbstractQueuedSynchronizer:acquire(int arg)
  4. ReentrantLock:tryAcquire(int acquires)

unlock()解锁

当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用AQS的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)

//释放锁
public class ReentrantLock implements Lock, java.io.Serializable{
    public void unlock() {
       sync.release(1);
    }
}
  • 如果当前线程是这个锁的持有者,那么计数递减。
  • 如果当前计数为零,则释放锁。
  • 如果当前线程不是此线程的持有者,则抛异常IllegalMonitorStateException
//以独占的方式释放
//tryRelease()方法如果返回true,则通过解锁一个或多个线程来实现。
public final boolean release(int arg) {
     if (tryRelease(arg)) {
         Node h = head;
         if (h != null && h.waitStatus != 0)
             unparkSuccessor(h);
         return true;
     }
     return false;
 }

该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用LockSupport来唤醒处于等待状态的线程

 //释放锁
 protected final boolean tryRelease(int releases) {
 	//volatile修饰state 减去需要释放的锁
     int c = getState() - releases;
     if (Thread.currentThread() != getExclusiveOwnerThread())
         throw new IllegalMonitorStateException();
     boolean free = false;
     //等于0,说明没有需要同步的,锁释放成功
     if (c == 0) {
         free = true;
         setExclusiveOwnerThread(null);
     }
     //释放锁最后,写volatile变量state
     setState(c);
     return free;
 }

在使用公平锁时,解锁方法unlock()调用过程:

  1. ReentrantLock:unlock()
  2. AbstractQueuedSynchronizer:release(int arg)
  3. Sync:tryRelease(int releases)

公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。根据volatilehappens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。

非公平锁的释放和公平锁是一样的,我们来看下非公平锁的获取。

lock() 获取锁(非公平锁)

    static final class NonfairSync extends Sync {
    
        final void lock() {
        	//通过CAS,期望值为0 ,更新值 1
            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;
     }

如果当前状态值等于期望值,则以原子方式将同步状态设置为给定的更新值。

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

使用非公平锁时,加锁方法lock()调用流程

  1. ReentrantLock:lock()
  2. NonfairSync:lock()
  3. AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)

编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序。组合这两个条件,意味着为了同时实现volatile读和volatile写的内存语义,编译器不能对CAS与CAS前面和后面的任意内存操作重排序。

公平锁和非公平锁的区别

现在对公平锁和非公平锁做个总结。

  • 公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
  • 公平锁和非公平锁释放时,最后都要写一个volatile变量state。
  • 公平锁获取时,首先会去读volatile变量。
  • 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同。
  • 公平性锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁会出现一个线程连续获取锁的情况。
  • 公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

公平锁tryAcquire,非公平锁nonfairTryAcquire,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

ReentrantLock的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种
方式。

  1. 利用volatile变量的写-读所具有的内存语义。
  2. 利用CAS所附带的volatile读和volatile写的内存语义。

tryLock()

获取锁(如果有)并立即返回true。 如果锁不可用,则此方法将立即返回值false

    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
    
    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;
    }

该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。

成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值,释放锁时tryRelease()方法,上边已经贴过源码

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

如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

独占式同步状态获取和释放总结

在获取同步状态时,AQS维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,AQS调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

共享式锁

上边我们提到独占式锁和共享式锁,讲了那么多,现在知道了ReentrantLock是独占式锁,那什么是共享式锁呢?它又是这么获取和释放的呢?

在java并发编程中,ReentrantReadWriteLock为共享式锁,我们一般也称为读写锁。
但也仅仅ReadLock 是共享锁,而WriteLock 依旧是独占锁

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。

    ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();

共享式获取与独占式获取最主要的区别在于:同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况。
在这里插入图片描述
左半部分,共享式访问资源时,其他共享式的访问均被允许,而独占式访问被
阻塞,右半部分是独占式访问资源时,同一时刻其他访问均被阻塞。

共享锁获取

独占式获取锁是acquire(int arg)方法,共享式锁是acquireShared(int arg)

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。

独占锁是tryAcquire,而共享锁是tryAcquireShared,同时它也分为公平锁和非公平锁,他们的区别也是hasQueuedPredecessors()方法,我们就已公平锁为例吧

  	protected int tryAcquireShared(int acquires) {
        for (;;) {
            if (hasQueuedPredecessors())
                return -1;
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }

acquireShared(int arg)方法中,AQS调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0。

    private void doAcquireShared(int arg) {
    	//创建共享Node
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。

共享锁的释放

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程

参考书籍:java并发编程的艺术

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章