java线程-深入理解AQS原理

96 阅读26分钟

Java语言本身就已经提供了Sychronized的语法糖,使用起来比较简单,并且在jdk1.5以后已经得到了优化,性能也是不差的,为什么java还需要提供较为复杂的java sdk来重复造轮子?他们的区别在哪?J.U.C是java.util.concurrent包的简称,在JDK1.5添加,目的是为了开发者能够灵活的运用多线程编程。本文主要讲解Lock和Condition,Lock和Condition就是java.util.concurrent包下的接口

Lock概述

类图:

JUC.Lock类图.png

我们先来回答为什么有了Synchroniezed还需要Lock呢?根据java线程-synchronized详解中了解到synchronized的缺点

  1. synchronized不能设置公平锁
  2. synchronized不能响应中断
  3. synchronized不支持超时
  4. synchronized不支持非阻塞获取锁
  5. synchronized不支持读写锁精细化管理
  6. synchronized不支持乐观锁

但这些缺点Lock锁都可以解决,其中Lock解决了1-4的缺点,ReadWriteLock解决了缺点5,StampedLock解决了缺点6。当然ReadWriteLock也包含解决了缺点1-4,StampedLock包含解决了缺点1-5。

Lock结构:

public interface Lock{
  void lock();
  void lockInterruptibly() throws InterruptedException; // 支持中断的 API
  boolean tryLock(); // 支持非阻塞获取锁的API
  boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 支持超时的APi
  void unlock();
  Condition newCondition(); // 条件变量
} 

ReentrantLock:

public class ReentrantLock implements Lock, java.io.Serializable{
    public ReentrantLock() {   // 默认是非公平锁
        sync = new NonfairSync();
    }
  
    public ReentrantLock(boolean fair) { // 使用构造器来判断是否使用公平锁
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

使用Lock锁的最佳实践:

class X {
  private final Lock rtl = new ReentrantLock();
  int value;
  public void addOne() {
    // 获取锁
    rtl.lock();  
    try {
      value+=1;
    } finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
}

ReadWriteLock结构:

public interface ReadWriteLock {
​
    Lock readLock();  // 读锁
​
    Lock writeLock(); // 写锁
}

StampedLock简介:

StampedLock是比ReentrantReadWriteLock更快的一种锁,支持乐观读、悲观读锁和写锁。和ReentrantReadWriteLock不同的是,StampedLock支持多个线程申请乐观读的同时,还允许一个线程申请写锁。

乐观读并不加锁

StampedLock的底层并不是基于AQS的。

ReentrantLock概述

使用Lock锁的示例:

class X {
  private final Lock rtl = new ReentrantLock();
  int value;
  public void addOne() {
    // 获取锁
    rtl.lock();  
    try {
      value+=1;
    } finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
}

Lock保证可见性的基本原理是利用volatile以及Happens-Before的规则,其中ReentrantLock内部维护了一个内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值。利用的规则如下:

  • 顺序性规则:对于线程 T1,value+=1 Happens-Before 释放锁的操作 unlock();
  • volatile 变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作 Happens-Before 线程 T2 的 lock() 操作;
  • 传递性规则:线程 T1 的 value+=1 Happens-Before 线程 T2 的 lock() 操作。

具体实现原理还是通过AQS,AQS的具体原理后面会有讲解。

可重入锁

ReentrantLock翻译过来就是可重入锁,可重入锁指的是可以被同一个线程多次加锁的锁。注意,这里说的多次加锁,并不是说解锁之后再次加锁,而是在锁没有解锁前再次加锁。

class X {
  private final Lock rtl = new ReentrantLock();
  int value;
  public void addOne() {
    // 获取锁
    rtl.lock();  
    try {
      increment(); // 重入锁
    } finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
}
​
public void increment(){
  rtl.lock();
  try{
    value+=1;
  }catch(){
    rtl.unlock();
  }
}

JUC提供的锁都是可重入锁。实际上,Java synchronized内置锁也是可重入锁。从侧面上,我们也可以得出,可重入是对锁的基本要求。为了实现可重入特性,可重入锁中需要有一个变量来记录重入的次数。每重入一次,变量就增一;每解锁一次(调用unlock()或退出synchronized代码块),变量就减一,直到变量值为0时,才会释放锁唤醒其他线程执行。

公平锁

ReentrantLock有两个构造函数,一个是无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的公平策略,如果传入 true 就表示需要构造一个公平锁,反之则表示要构造一个非公平锁。

前面我们在介绍Synchronized实现原理的时候,多个线程竞争Monitor锁的时候,如果一个线程没有获得锁,就会进入等待队列_cxq,并且还需要调用park()函数阻塞自己。当有线程释放锁的时候,会从_EntryList中唤醒一个等待的线程,取消阻塞状态,让这个线程和其他线程一起竞争Monitor锁,而不是直接将锁给它。

想要实现公平锁,新的线程想要获取锁,如果_EntryList_cxq队列中只要有排队等待的线程,那么不管有没有线程释放,只要是新来的线程,直接将线程放入到_cxq队列中,按照FIFO的顺序来等待锁,以免出现插队的现象。

只不过非公平锁的实现是在jvm层实现的,但公平锁的实现是在jdk层通过AQS实现的。

如果是公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,很公平;如果是非公平锁,则不提供这个公平保证,有可能等待时间短的线程反而先被唤醒。

可中断锁

对于synchronized锁来说,线程在阻塞等待synchronized锁时是无法响应中断的。而JUC Lock接口提供了lockInterruptibly()函数,支持可响应中断的方式来请求锁。示例代码如下所示。

public class LockTest {
​
    private Lock lock = new ReentrantLock();
​
    public void doBussiness() {
        String name = Thread.currentThread().getName();
​
        try {
            lock.lockInterruptibly();
            for (int i=0; i<5; i++) {
                Thread.sleep(1000);
                System.out.println(name + " : " + i);
            }
        } catch (InterruptedException e) {
          
            System.out.println(name + " 做些别的事情");
        } finally {
            try {
                lock.unlock();
                System.out.println(name + " 释放锁");
            } catch (Exception e) {
                System.out.println(name + " : 没有得到锁的线程运行结束");
            }
        }
    }
​
​
    public static void main(String[] args) throws InterruptedException {
​
        LockTest lockTest = new LockTest();
​
        Thread t0 = new Thread(
                new Runnable() {
                    public void run() {
                        lockTest.doBussiness();
                    }
                }
                );
​
        Thread t1 = new Thread(
                new Runnable() {
                    public void run() {
                        lockTest.doBussiness();
                    }
                }
                );
​
        // 启动线程t1
        t0.start();
        Thread.sleep(10);
        // 启动线程t2
        t1.start();
        Thread.sleep(100);
        // 线程t1没有得到锁,中断t1的等待
        t1.interrupt();
    }
}

可中断锁一般用于线程管理中,方便关闭正在执行的线程。比如,Nginx服务器采用多线程来执行请求。当我们调用stop命令关闭Nginx服务器时,Nginx服务器可以采用中断的方式,将阻塞等待锁的线程中止,然后,合理的释放资源和妥善处理未执行完成的请求,以实现服务器的优雅关闭。

非阻塞锁

锁已经被另一个线程获取,那么,这个线程就需要阻塞等待。JUC Lock接口提供了tryLock()函数,支持非阻塞的方式获取锁。如果锁已经被其他线程获取,那么,调用tryLock()函数会直接返回错误码而非阻塞等待。

可超时锁

还提供给了带时间参数的tryLock()函数,支持非阻塞获取锁的同时设置超时时间。也就是说,一个线程在请求锁时,如果这个锁被其他线程持有,那么这个线程会阻塞等待一段时间。如果超过了设定的超时时间,线程仍然没有获取到锁,那么tryLock()函数将会返回错误码而不再阻塞等待。

ReentrantLock与Synchronized的区别

ReentrantLockSynchronized
锁实现机制依赖AQS(JDK层)依赖ObjectMonitor(JVM层),Monitor锁
灵活性支持响应中断,非阻塞获取锁,超时不支持响应中断,不支持非阻塞获取锁,不支持超时
释放方式显式调用unlock()隐式自动释放Monitor锁
锁类型支持公平与非公平锁仅支持非公平锁
可重入性可重入可重入
条件队列可关联多个条件队列仅能关联一个条件队列

ReadWriteLock概述

ReentrantReadWriteLock是ReadWriteLock的实现类,ReentrantLock具有的特性,它也具有。

实际上,在多线程环境下,读操作是可以并发执行的,但是读和写,写和写不能并发执行。为了提高读操作的效率,将读写锁分离,JUC提供ReadWriteLock接口和ReentrantReadWriteLock实现类。

public interface ReadWriteLock {
    Lock readLock();
​
    Lock writeLock();
}

其中读锁是共享锁,多个读操作可以并发执行。写锁是排它锁,写锁同时只能由一个线程持有。读锁与写锁之间也是排它的。因此读写锁的使用场景就是读多写少的场景。

ReadWriteLock基本使用

代码使用示例:

public class ReentrantReadWriteLockDemo {
​
  private final List<String> strList = new LinkedList<>();
  private final ReadWriteLock lock = new ReentrantReadWriteLock();
  private final Lock readLock = lock.readLock();
  private final Lock writeLock = lock.writeLock();
​
  public void add(int idx, String str) {
    writeLock.lock();
    try {
      strList.add(idx, str);
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      writeLock.unlock();
    }
  }
​
  public String get(int idx) {
    readLock.lock();
    try {
      return strList.get(idx);
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      readLock.unlock();
    }
  }
  
}

ReentrantReadWriteLock同样也支持公平锁和非公平锁的指定,也是通过构造器参数来区分的,默认是非公平锁。

public ReentrantReadWriteLock() {
    this(false);
}
​
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

读写锁的升降级

如何使用ReentrantReadWriteLock实现一个Cache?代码如下:

public class Cache<K, V> {
​
  private final Map<K, V> m = new HashMap<>();
  private final ReadWriteLock rwl = new ReentrantReadWriteLock();
  private final Lock r = rwl.readLock();
  private final Lock w = rwl.writeLock();
​
  // 错误示例
  public final V get(K key) {
    V v = null;
    // 读缓存
    r.lock();
    try {
      v = m.get(key);
      if (v == null) { // 如果v为null
        w.lock();      // 写锁加锁
        try {
          // v = 省略代码
          m.put(key, v);
        } finally {
          w.unlock(); // 写锁解锁
        }
      }
    } finally {
      r.unlock();
    }
    return v;
  }
}

上述代码中可以发现,先获取读锁,如果v为null,就会再次获取写锁,这样看上去好像是没有问题的,这个是锁的升级。可惜 ReadWriteLock 并不支持这种升级。在上面的代码示例中,读锁还没有释放,此时获取写锁,会导致写锁永久等待,最终导致相关线程都被阻塞,永远也没有机会被唤醒。锁的升级是不允许的,这个你一定要注意。这是因为一个线程获取到读锁,可能其他线程同时也获取到读锁,如果这个线程升级到了写锁,那么其他线程的读锁和写锁就不互斥了。

虽说锁的升级不允许,但是锁的降级是允许的。锁的降级就是从写锁降级到读锁。

参考官方示例代码:

public class CacheData {
​
  Object data;
​
  volatile boolean cacheValid;
​
  final ReadWriteLock rwl = new ReentrantReadWriteLock();
​
  // 读锁
  final Lock r = rwl.readLock();
​
  // 写锁
  final Lock w = rwl.writeLock();
​
  void processCacheData() {
    // 获取读锁
    r.lock();
    if (!cacheValid) {
      // 释放读锁,因为不允许读锁的升级
      r.unlock();
      // 获取写锁
      w.lock();
      try {
        // 再次检查状态
        if (!cacheValid) {
          // 处理数据
          cacheValid = true;
        }
        // 释放写锁前,降级为读锁
        r.lock();
      } finally {
        w.unlock();
      }
    }
    // 此处任有读锁
    try {
      // 使用数据
    } finally {
      r.unlock();
    }
  }
}

StampedLock概述

StampedLock是对ReadWriteLock的进一步优化,在读锁和写锁的基础之上,又提供了乐观读锁。实际上,乐观读锁并没有加任何锁。在读多写少的应用场景中,大部分读操作都不会被写操作干扰,因此,我们甚至可以将读锁也省略掉。只有验证读操作真正有被写操作干扰的情况下,线程再加读锁重复执行读操作。

StampedLock基本使用

ReadWriteLock 支持两种模式:一种是读锁,一种是写锁。而 StampedLock 支持三种模式,分别是:写锁、悲观读锁和乐观读。其中,写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。相关的示例代码如下。

public class StampedLockDemo {
​
  private final StampedLock stampedLock = new StampedLock();
  private final List<String> list = new LinkedList<>();
​
  public void add(int idx, String str) {
    long stamped = stampedLock.writeLock();
    try {
      list.add(idx, str);
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      stampedLock.unlockWrite(stamped);
    }
  }
​
  public String get(int idx) {
    long stamped = stampedLock.tryOptimisticRead(); // 乐观读
    String res = list.get(idx);
    if (stampedLock.validate(stamped)) { // 如果没有写操作干扰
      return res;
    }
    // 有写操作干扰,重新获取读锁
    stamped = stampedLock.readLock();
    try {
      return list.get(idx);
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      stampedLock.unlockRead(stamped);
    }
  }
}

StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式。ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。注意这里,我们用的是“乐观读”这个词,而不是“乐观读锁”,是要提醒你,乐观读这个操作是无锁的,所以相比较 ReadWriteLock 的读锁,乐观读的性能更好一些。

StampedLock使用注意事项

  1. StampedLock 不支持重入。
  2. StampedLock 的悲观读锁、写锁都不支持条件变量。
  3. 如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。如果需要中断,建议使用readLockInterruptibly() 和写锁 writeLockInterruptibly()。

AQS具体实现原理

AQS简介

AQS是抽象类AbstractQueuedSynchronizer的简称,中文翻译为抽象队列同步器。Synchronized主要依赖JVM中的ObjectMonitor类来实现,但JUC中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AQS实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。

AQS成员变量

private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
private transient Thread exclusiveOwnerThread; // 父类AbstractOwnableSynchronizer中的成员变量
  1. state

    我们在使用synchronized,当多个线程竞争锁的时候,jvm会让他们通过CAS操作来设置ObjectMonitor中的_owner字段,谁设置成功,谁就获取了这个锁。

    AQS中的state的作用就类似于_owner字段,但不同的是_owner存储的是获取锁的线程,而state是一个int类型的变量,存储的是0,1等整数值,0表示锁没有被占用,1表示已经被占用,大于1的数表示重入的次数。当多个线程竞争锁的时候,它们会通过Unsafe类提供的CAS函数(保证了原子性)来更新state值。这里CAS指的是先检查state的值是否为0,如果是的话,将state设置为1.谁设置成功,谁就获取到锁。

    不过独占锁和共享锁state的设置逻辑会有些不一样

    独占模式:

    独占模式.png

    共享模式:

    共享模式.png

  2. exclusiveOwnerThread

    存储持有锁的线程,它配合state成员变量,可以实现锁的重入机制。

  3. head和tail

    在ObjectMonitor中,_cxq_EntryList用来存储等待锁的线程,_WaitSet用来存储调用了wait()函数的线程。

    在AQS中只有一个等待队列,既用来存储等待锁的线程,也用来存储等待条件变量的线程。这个等待队列是个双向链表,它还有个名称,叫做CLH队列。

    CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

    AQS中的队列是通过双向链表实现的,头节点为虚拟节点,其实并不存储任何信息,只是占位。真正的第一个数据节点,是在第二个节点开始的。

AQS数据结构

static final class Node {
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
  
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    volatile int waitStatus;
​
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;
​
    final Node predecessor() {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
}

解释一下几个方法和属性值的含义:

方法和属性值含义
waitStatus当前节点在队列中的状态
thread表示处于该节点的线程
prev前驱指针
predecessor返回前驱节点,没有的话抛出npe
nextWaiter指向下一个处于CONDITION状态的节点
next后继指针

线程两种锁的模式:

模式含义
SHARED表示线程以共享的模式等待锁
EXCLUSIVE表示线程正在以独占的方式等待锁

waitStatus有下面几个枚举值:

枚举含义
0当一个Node被初始化的时候的默认值
CANCELLED为1,表示线程获取锁的请求已经取消了
CONDITION为-2,表示节点在等待队列中,节点线程等待唤醒
PROPAGATE为-3,当前线程处在SHARED情况下,该字段才会使用
SIGNAL为-1,表示线程已经准备好了,就等资源释放了

AQS成员方法

AQS是通过模板方法实现的,AQS定义了8个模版方法,4个抽象方法,这几个方法可以分为2组,分别用于AQS的两种工作模式:独占模式和共享模式,其中不带Shared后缀的为独占模式,带Shared后缀的为共享模式。

Lock为排它锁,因此Lock的底层实现只会用到AQS的独占模式。ReadWriteLock中的读锁为共享锁,写锁为排它锁。Semaphore、CountDownLatch这些同步工具只会用到AQS的共享模式。

8个模版方法

public final void acquire(int arg) {
  if (!tryAcquire(arg) &&
      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}
​
public final void acquireInterruptibly(int arg)
  throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();
  if (!tryAcquire(arg))
    doAcquireInterruptibly(arg);
}
​
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
  throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();
  return tryAcquire(arg) ||
    doAcquireNanos(arg, nanosTimeout);
}
​
public final boolean release(int arg) {
  if (tryRelease(arg)) {
    Node h = head;
    if (h != null && h.waitStatus != 0)
      unparkSuccessor(h);
    return true;
  }
  return false;
}
​
public final void acquireShared(int arg) {
  if (tryAcquireShared(arg) < 0)
    doAcquireShared(arg);
}
​
public final void acquireSharedInterruptibly(int arg)
  throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();
  if (tryAcquireShared(arg) < 0)
    doAcquireSharedInterruptibly(arg);
}
​
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
  throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();
  return tryAcquireShared(arg) >= 0 ||
    doAcquireSharedNanos(arg, nanosTimeout);
}
​
public final boolean releaseShared(int arg) {
  if (tryReleaseShared(arg)) {
    doReleaseShared();
    return true;
  }
  return false;
}

四个抽象方法:

这四个方法都提供了默认实现,抛出UnsupportedOperationException异常。这样做是为了减少开发量,即我们不需要在子类中实现所有的抽象方法。

其中四个方法的参数含义为:获取锁的次数,尝试获取资源

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

还有一个方法:该线程是否正在独占资源。只有用到Condition才需要去实现它。

protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

AQS具体实现类

AQS具体实现类有:

AQS类结构图.png

  • ReentrantLock里的NonfairSync和FairSync,这两个类又有共同的父类Sync。
  • ReentrantReadWriteLock的NonfairSync和FairSync,这两个类又有共同的父类Sync。
  • Semaphore里的NonfairSync和FairSync,这两个类又有共同的父类Sync。
  • CountDownLatch里的Sync
  • ThreadPoolExecutor里的Worker

ReentrantLock实现原理

先来分析一下ReentrantLock里的实现,看一下具体实现流程是如何的。

加锁流程

非公平锁加锁流程:

AQS-ReentrantLock-非公平锁流程.png

公平锁加锁流程:

AQS-ReentrantLock-公平锁流程.png

  1. 通过ReentrantLock的加锁方法Lock进行加锁操作,实际上是调用Sync#lock(),由于Sync是抽象类,具体实现类还得看ReentrantLock初始化的时候选择的是公平锁和非公平锁。

  2. 非公平锁,会先进行CAS更新state,如果更新成功,表示当前线程已经被占用,设置独占线程就是当前线程。如果更新失败,再调用AQS.acquire(1)。

    公平锁,则会直接调用AQS.acquire(1)。

    AQS.acquire方法如下:

    public final void acquire(int arg) {
      if (!tryAcquire(arg) &&
          acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
    }
    
  3. AQS.acquire(1)会执行tryAcquire()方法,tryAcquire也是需要子类来实现。

    非公平锁的实现逻辑是调用其父类Sync#nonfairTryAcquire()方法

    final boolean nonfairTryAcquire(int acquires) {
      final Thread current = Thread.currentThread();
      int c = getState();
      if (c == 0) {//1. 锁没有被其他线程占用
        if (compareAndSetState(0, acquires)) { // CAS设置state=1
          setExclusiveOwnerThread(current); // 设置独占线程为当前线程
          return true; // 获取锁成功
        }
      }
      else if (current == getExclusiveOwnerThread()) { // 2. 锁可重入
        int nextc = c + acquires;
        if (nextc < 0) // overflow, 重入次数太多,超过了int的最大值会溢出,为负数
          throw new Error("Maximum lock count exceeded");
        setState(nextc); // state记录重入的次数,解锁的时候会用到
        return true; // 获取锁成功
      }
      return false; // 3. 锁被其他线程占用
    }
    

    公平锁的实现逻辑是如下:

    protected final boolean tryAcquire(int acquires) {
      final Thread current = Thread.currentThread();
      int c = getState();
      if (c == 0) {//1. 锁没有被其他线程占用
        if (!hasQueuedPredecessors() && // 等待队列中没有线程时才能获取锁
            compareAndSetState(0, acquires)) { // CAS设置state=1
          setExclusiveOwnerThread(current);// 设置独占线程为当前线程
          return true;// 获取锁成功
        }
      }
      else if (current == getExclusiveOwnerThread()) {// 2. 锁可重入
        int nextc = c + acquires;
        if (nextc < 0)// 重入次数太多,超过了int的最大值会溢出,为负数
          throw new Error("Maximum lock count exceeded");
        setState(nextc);  // state记录重入的次数,解锁的时候会用到
        return true; // 获取锁成功
      }
      return false; // 3. 锁被其他线程占用
    }
    
  4. 如果获取锁失败,会将线程加入到等待队列里,会调用addWaiter()方法

    private Node addWaiter(Node mode) {
      Node node = new Node(Thread.currentThread(), mode);
      // 快速入队
      Node pred = tail;
      if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
          pred.next = node;
          return node;
        }
      }
      enq(node);
      return node;
    }
    ​
    private Node enq(final Node node) {
      // 自旋执行CAS操作,直到成功为止
      for (;;) {
        Node t = tail;
        if (t == null) { // 链表为空,添加虚拟头节点
          // CAS操作解决添加虚拟头节点的线程安全问题
          if (compareAndSetHead(new Node()))
            tail = head;
        } else { // 链表不为空
          node.prev = t;
          // CAS操作解决了同时往链表尾部添加节点时的线程安全问题
          if (compareAndSetTail(t, node)) {
            t.next = node;
            return t;
          }
        }
      }
    }
    
  5. 最后acquireQueued,主要包含两个逻辑:使用tryAcquire()函数来竞争锁和使用park()函数来阻塞线程,并使用for循环来交替执行这两个逻辑。

    final boolean acquireQueued(final Node node, int arg) {
      boolean failed = true;
      try {
        boolean interrupted = false;
        // 自旋(竞争锁+阻塞),因为被唤醒之后不一定能竞争到锁,所以要自旋
        for (;;) {
          final Node p = node.predecessor();
          // 如果线程是被中断唤醒的,那么p 就不一定等于head,也就不能去竞争锁
          if (p == head && tryAcquire(arg)) {
            setHead(node); // 把node设置为虚拟头节点,也就相当于将它删除
            p.next = null; // help GC
            failed = false;
            return interrupted;
          }
          // 调用park()函数来阻塞线程,线程被唤醒有以下两种情况:
          // 1. 其他线程调用unpark()来唤醒,此时,节点位于虚拟头节点的下一个,p == head
          // 2. 被中断唤醒,此时,节点不一定就是虚拟节点的下一下,p不一定等于head
          if (shouldParkAfterFailedAcquire(p, node) &&
              parkAndCheckInterrupt())
            interrupted = true;
        }
      } finally {
        // 以上过程只要出现异常,都要将这个节点标记为cancelled,等待被删除
        if (failed)
          cancelAcquire(node);
      }
    }
    ​
    private final boolean parkAndCheckInterrupt() {
      LockSupport.park(this);
      return Thread.interrupted();
    }
    ​
    

解锁流程

AQS-ReentrantLock-解锁流程.png

  1. 通过ReentrantLock的unlock方法解锁
  2. unlock会调用AQS#release()方法
  3. AQS#release()会调用tryRelease()方法,这个方法是ReentrantLock.Sync的公共的方法,并不区分是公平锁和非公平锁
  4. 释放锁的时候会调用JVM#unpark()函数。

具体代码如下:

// AQS.release()
public final boolean release(int arg) {
  if (tryRelease(arg)) {
    Node h = head;
    if (h != null && h.waitStatus != 0)
      unparkSuccessor(h); // JVM#unpark()
    return true;
  }
  return false;
}
​
// ReentrantLock.Sync.tryRelease()
protected final boolean tryRelease(int releases) {
  int c = getState() - releases;
  // 如果当前线程不是独占线程,抛出异常
  if (Thread.currentThread() != getExclusiveOwnerThread())
    throw new IllegalMonitorStateException();
  boolean free = false;
  if (c == 0) { // state - 1 == 0解锁
    free = true;
    setExclusiveOwnerThread(null);
  }
  setState(c); // != 0表示锁被多次重入,还不能解锁
  return free;
}

中断机制和超时机制

之前说过Lock支持线程中断和超时机制,对应的方法是分别是

public void lockInterruptibly() throws InterruptedException {
  sync.acquireInterruptibly(1);
}
​
public boolean tryLock(long timeout, TimeUnit unit)
  throws InterruptedException {
  return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

中断机制

public final void acquireInterruptibly(int arg)
  throws InterruptedException {
  if (Thread.interrupted()) // 如果线程中断
    throw new InterruptedException(); // 则抛出异常
  if (!tryAcquire(arg)) // 如果获取锁成功直接返回,获取锁失败就调用doAcquireInterruptibly
    doAcquireInterruptibly(arg);
}
​
private void doAcquireInterruptibly(int arg) // 与acquireQueued()函数实现类似
  throws InterruptedException {
  final Node node = addWaiter(Node.EXCLUSIVE);
  boolean failed = true;
  try {
    for (;;) {
      final Node p = node.predecessor();
      if (p == head && tryAcquire(arg)) {
        setHead(node);
        p.next = null; // help GC
        failed = false;
        return;
      }
      if (shouldParkAfterFailedAcquire(p, node) &&
          parkAndCheckInterrupt())
        throw new InterruptedException(); // 与acquireQueued()不同的地方是这里:这里直接抛出异常
    }
  } finally {
    if (failed)
      cancelAcquire(node);
  }
}

超时机制

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted()) // 如果线程中断
        throw new InterruptedException(); // 抛出异常
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout); // 如果成功获取锁,则直接返回,否则调用doAcquireNanos函数
}
​
// 在acquireInterruptibly基础上添加了对超时的处理机制
private boolean doAcquireNanos(int arg, long nanosTimeout)
  throws InterruptedException {
  if (nanosTimeout <= 0L)
    return false;
  final long deadline = System.nanoTime() + nanosTimeout;
  final Node node = addWaiter(Node.EXCLUSIVE);
  boolean failed = true;
  try {
    for (;;) {
      final Node p = node.predecessor();
      if (p == head && tryAcquire(arg)) {
        setHead(node);
        p.next = null; // help GC
        failed = false;
        return true;
      }
      nanosTimeout = deadline - System.nanoTime();
      if (nanosTimeout <= 0L)
        return false;
      if (shouldParkAfterFailedAcquire(p, node) &&
          nanosTimeout > spinForTimeoutThreshold) // 不着急阻塞,先自旋一下
        LockSupport.parkNanos(this, nanosTimeout); // 超时阻塞
      if (Thread.interrupted())
        throw new InterruptedException();
    }
  } finally {
    if (failed)
      cancelAcquire(node);
  }
}
​

ReentrantReadWriteLock实现原理

ReentrantReadWriteLock是ReadWriteLock的实现类,跟ReentrantLock的结构类似,内部也有Sync类继承自AQS,以及Sync的两个子类,NonfairSync和FairSync来实现读锁(ReadLock)和写锁(WriteLock)。ReadLock和WriteLock均实现了Lock接口,实现了Lock接口中的所有加锁和解锁函数,且使用相同的AQS。

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    private final ReentrantReadWriteLock.ReadLock readerLock;
    private final ReentrantReadWriteLock.WriteLock writerLock;
    final Sync sync;
​
    public ReentrantReadWriteLock() {
        this(false);
    }
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
​
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
  
    abstract static class Sync extends AbstractQueuedSynchronizer {
        abstract boolean readerShouldBlock();
        abstract boolean writerShouldBlock();
        //偏移位数
        static final int SHARED_SHIFT = 16;
        //读锁计数基本单位
        static final int SHARED_UNIT = (1 << SHARED_SHIFT);
        //读锁、写锁可重入最大数量
        static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
        //获取低16位的条件
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
      
        protected final boolean tryRelease(int releases) {}
        protected final boolean tryAcquire(int acquires) {}
        protected final boolean tryReleaseShared(int unused) {}
        protected final int tryAcquireShared(int unused) {}
      
        final boolean tryWriteLock() {}
        final boolean tryReadLock() {}
    }
​
    static final class NonfairSync extends Sync {
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
    }
​
    static final class FairSync extends Sync {
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }
​
    public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        private final Sync sync;
        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
        public void lock() {
            sync.acquireShared(1);
        }
​
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireSharedInterruptibly(1);
        }
        public boolean tryLock() {
            return sync.tryReadLock();
        }
        public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
        }
        public void unlock() {
            sync.releaseShared(1);
        }
        public Condition newCondition() {
            throw new UnsupportedOperationException();
        }
    }
​
    public static class WriteLock implements Lock, java.io.Serializable {
        private final Sync sync;
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
​
        public void lock() {
            sync.acquire(1);
        }
​
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }
​
        public boolean tryLock() {
            return sync.tryWriteLock();
        }
        public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }
​
        public void unlock() {
            sync.release(1);
        }
        public Condition newCondition() {
            return sync.newCondition();
        }
    }
}

通过对ReentrantLock实现原理的分析的值,AQS中的state值是用来表示加锁的情况的,0表示没有加锁,1表示已经加锁,大于1的值表示重入的次数。但是读写锁还是需要区分是读锁还是写锁,因为处理逻辑是不一样的。

state变量中的低16位表示写锁的使用情况,高16位表示读锁的使用情况。

低16位所表示的数,值为0表示没有加写锁,值为1表示已经加写锁,值大于1表示写锁的重入次数。

高16位所表示的数,值为0表示没有加读锁,值为1表示已经加读锁,值大于1表示读锁总共被获取了多少次。

写锁原理

我们先来看WriteLock#lock()函数的实现,实际上还是调用的是AQS#acquire(1),与ReentrantLock的区别还在于tryAcquire()函数,写锁的实现,不管是公平锁还是非公平锁,都在父类的Sync#tryAcquire()中实现。

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState(); // c为state的值
    int w = exclusiveCount(c); // 低16位的值,也就是写锁的加锁情况
    // 1. 已经加读锁或者写锁(state!=0)
    if (c != 0) {
        // 已加读锁(w==0)或者当前加写锁的线程不是自己
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 更新写锁的重入次数
        setState(c + acquires);
        // 获取到了锁
        return true;
    }
    // 2. 没有加锁(state == 0)
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires)) // 去排队
        return false;
    setExclusiveOwnerThread(current);
    return true; // 获取到了锁
}
​
static final class FairSync extends Sync {
  private static final long serialVersionUID = -2274990926593161451L;
  final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
  }
  final boolean readerShouldBlock() {
    return hasQueuedPredecessors();
  }
}
​
static final class NonfairSync extends Sync {
  private static final long serialVersionUID = -8159625535654395037L;
  final boolean writerShouldBlock() {
    return false; // writers can always barge
  }
  final boolean readerShouldBlock() {
    return apparentlyFirstQueuedIsExclusive();
  }
}

unlock()的实现逻辑也跟ReentrantLock.unlock类似,但是tryRelease()会有区别,代码如下:

protected final boolean tryRelease(int releases) {
    // tryRelease()是AQS工作在独占模式下的函数,只能用于排它锁,也就是写锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 更新state值,写入的重入次数-release,对于锁来说,release总是等于1
    int nextc = getState() - releases;
    // 只有更新之后的state值为0时,才可以将写锁释放。
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

读锁原理

再来看看ReadLock#lock()方法。

// ReadLock#lock()
public void lock() {
    sync.acquireShared(1);
}
​
// AQS#acquireShared(int arg)
// 如果竞争锁成功,直接返回
// 如果竞争失败,则去排队
public final void acquireShared(int arg) {
  if (tryAcquireShared(arg) < 0) // 竞争读锁
    doAcquireShared(arg);        // 竞争失败去排队
}
​
// ReentrantReadWriteLock.Sync#tryAcquireShared(int unused)
protected final int tryAcquireShared(int unused) {
  Thread current = Thread.currentThread();
  int c = getState();
  // 已加写锁
  // 如果加写锁的线程不是当前线程,那么读锁也加不成,直接返回-1
  // 否则,读写锁支持锁降级,加了写锁的线程可以再加读锁
  if (exclusiveCount(c) != 0 &&
      getExclusiveOwnerThread() != current)
    return -1;
  // 理论上讲,如果没有加写锁,不管有没有加读写锁,都可以去竞争读锁,毕竟读锁是共享锁
  // 但是,存在两个特殊情况:
  // 1. 对于公平锁来说,如果等待队列不为空,并且当前线程没有持有读锁(重入加锁),那么,线程就去排队。
  // 2. 对于非公平锁来说,如果等待队列队首线程是写线程,那么线程就去排队。
  int r = sharedCount(c);
  if (!readerShouldBlock() &&
      r < MAX_COUNT &&
      compareAndSetState(c, c + SHARED_UNIT)) {
    if (r == 0) {
      firstReader = current;
      firstReaderHoldCount = 1;
    } else if (firstReader == current) {
      firstReaderHoldCount++;
    } else {
      HoldCounter rh = cachedHoldCounter;
      if (rh == null || rh.tid != getThreadId(current))
        // readHolds是ThreadLocal变量,保存跟这个线程的读取重入次数
        // 如果重入次数为0,表示没有加读锁,返回-1去排队
        // 如果重入次数>0,表示已加读锁,可以继续重入,不用排队。
        cachedHoldCounter = rh = readHolds.get();
      else if (rh.count == 0)
        readHolds.set(rh);
      // 竞争读锁成功,更新线程重入次数
      rh.count++;
    }
    return 1; // 成功获取读锁
  }
  // 上述的方式是为了让程序更快获取到写锁
  // 如果获取不到则使用自旋的方式,再次尝试去竞争读锁。代码逻辑上述的类似。
  return fullTryAcquireShared(current);
}
​
​
// AQS.doAcquireShared(),负责排队和等待唤醒。跟acquireQueued()函数类似,主要区别有两点
private void doAcquireShared(int arg) {
  // 区别一:标记此线程等待的是共享锁。
  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);
  }
}

ReadLock#unlock实现原理如下:

// ReadLock#unlock()
public void unlock() {
    sync.releaseShared(1);
}
​
// AQS#releaseShared
public final boolean releaseShared(int arg) {
  if (tryReleaseShared(arg)) { // 释放读锁,当所有的读锁都释放成功,state=0
    doReleaseShared(); // 唤醒等待队列中位于队首的线程
    return true; // 释放成功
  }
  return false;
}
​
// ReentrantReadWriteLock.Sync#tryReleaseShared()
protected final boolean tryReleaseShared(int unused) {
  Thread current = Thread.currentThread();
  if (firstReader == current) {
    // assert firstReaderHoldCount > 0;
    if (firstReaderHoldCount == 1)
      firstReader = null;
    else
      firstReaderHoldCount--;
  } else {
    HoldCounter rh = cachedHoldCounter;
    if (rh == null || rh.tid != getThreadId(current))
      rh = readHolds.get();
    int count = rh.count;
    if (count <= 1) {
      readHolds.remove();
      if (count <= 0)
        throw unmatchedUnlockException();
    }
    --rh.count;
  }
  // 以上代码,主要就是更新本线程对读锁的重入次数
  // 因为有可能多个线程同时释放读锁,通过CAS更新state,所以通过自旋+CAS方式保证线程安全
  for (;;) {
    int c = getState();
    // c-SHARED_UNIT; 相当于读锁的加锁次数-1
    int nextc = c - SHARED_UNIT;
    if (compareAndSetState(c, nextc))
      return nextc == 0; // state == 0 才会返回true,才会去唤醒等待中的线程
  }
}