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

你真的了解ReentrantLock吗?公平锁和非公平锁获取和释放相同吗?

程序员文章站 2024-03-24 08:50:22
...


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

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

当线程释放锁时,java内存模型(以下简称JMM)会把线程对应的本地内存中的共享变量刷新到主内存中。你真的了解ReentrantLock吗?公平锁和非公平锁获取和释放相同吗?
当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得监视器保护的临界区代码必须从主内存中读取共享变量。
你真的了解ReentrantLock吗?公平锁和非公平锁获取和释放相同吗?
对比锁释放-获取的内存语义与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内存语义实现的关键。

ReentrantLock分为公平锁和非公平锁,FairSyncNonfairSync,同时继承自Sync,我们先来说公平锁。

lock() 获取锁(公平)

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

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

    final void lock() {
         acquire(1);
     }

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    /**
     * 公平锁的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;
      }

同步状态
private volatile int state;

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

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

unlock()解锁

//释放锁
public class ReentrantLock implements Lock, java.io.Serializable{
    public void unlock() {
       sync.release(1);
    }
}
  • 如果当前线程是这个锁的持有者,那么计数递减。
  • 如果当前计数为零,则释放锁。
  • 如果当前线程不是此线程的持有者,则抛异常IllegalMonitorStateException
//以独占的方式释放
//tryRelease()方法如果返回true,则通过解锁一个或多个线程来实现。
abstract class AbstractQueuedSynchronizer{
    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) {
    	//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变量。根据volatile的happens-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,表示释放成功。

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

相关标签: 线程 并发编程