LockSupport工具类
LockSupport是JDk rt.jar下面的一个工具类,主要作用是 挂起和唤醒线程 ,该工具类是 创建锁和其他同步类的基础 。

LockSupport与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport的方法的线程是不持有许可证的。LockSupport是使用Unsafe类实现的。
LockSupport中的 void park() 方法
如果调用park()的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.park()时会马上返回,否则调用线程会被禁止参与线程的调度,也就是会被阻塞挂起。
如果其他线程调用 unpark(Thread thread) 并将该线程作为参数传入, 此时因为调用 park()方法而被阻塞的线程会返回,另外如果其他线程调用了阻塞线程的Interrupt()方法, 设置了中断标志或者线程被虚假唤醒,则阻塞线程也会返回,需要注意的是返回的时候不会抛出InterruptedException异常。
LockSupport中的 void unpark(Thread thread) 方法
当一个线程调用unpark方法,其中的参数线程如果没有持有与LockSupport类相关的许可证,那么会立即持有, 如果线程在之前已经调用了park()方法而被阻塞,那么该线程会立即返回,如果没有调用park()方法,那么调用unpark()之后调用park()会直接返回(因为通过unpark()获取了许可证)
LockSupport中的 void parkNanos(long nanos) 方法
和park()方法差不多,只不过如果调用线程没有许可证,那么会被阻塞指定时间而返回。
LockSupport中的 park(Object blocker) 方法
1 | public static void park(Object blocker) { |
Thread类中有一个volatile Object parkBlocker用来存放传递的blocker,也就是把blocker变量存放到了线程本地。一般使用LockSupport.park(this),在打印线程堆栈的时候就可以获得相关阻塞类的信息了。
LockSupport中的 void parkNanos(Object blocker, long nanos)
比上面方法多一个超时时间。
LockSupport中的 void parkUntil(Object blocker, long deadline)
多设置一个最终时间
例子
1 | public class FIFOMutex { |
抽象同步队列 AQS 概述
AbstractQueuedSynchronizer 抽象同步队列简称 AQS。它是实现同步器的基本组件, 并发包中锁的底层就是使用AQS实现的。

我们大致能看到AQS是一个FIFO的双向队列,其内部通过head和tail记录头尾节点,队列中元素类型为Node,其中Node中的thread变量用来存放AQS队列中的线程。Node节点内部的SHARED用来标记该线程是获取共享资源时被阻塞挂起进入AQS队列的,EXCLUSIVE用来标记该线程是获取独占资源时被阻塞挂起进入AQS队列的。wiatStatus用来记录当前线程的等待状态,可以为CANCELLED(线程被取消了),SIGNAL(线程需要被唤醒),CONDITION(线程在条件队列里等待),PROPAGATE(释放共享资源的时候需要通知其他节点);prev用来记录当前节点的前驱节点,next记录当前节点的后继节点。
在AQS中维持一个单一的状态信息state,可以通过getState,setState,compareAndSetState函数来操作, 对于 ReentrantLock 的实现来说,state可以用来表示当前线程获取锁的可重入次数;对于读写锁 ReentrantReadWriteLock 来说,state的高16位表示读状态,低16位用来表示获取到写锁的线程的可重入次数。对于 semaphore 来说,state用来表示当前可用信号的个数,对于 CountDownlatch 来说,state用来表示计数器当前的值 。
AQS有一个内部类 ConditionObject ,用来结合锁实现线程同步。ConditionObject可以直接访问AQS的内部变量,比如state状态值和AQS队列,ConditionObject是一个条件变量,每一个条件变量对应着一个条件队列(单向链表队列)。其用来存放调用条件变量的await()方法后被阻塞的线程,条件队列的头尾元素分别为firstWaiter和lastWaiter。
对于AQS来说, 线程同步的关键在于对state的操作 。独占方式下获取和释放资源使用的方法是acquire(int), void acquireInterruptibly(int), boolean release(int)。共享方式:acquireShared(int),acquireSharedInterruptibly(int),boolean releaseShared(int)方法。
共享方式指的是能有多个线程获取资源(只要符合要求),独占指的是只能有一个线程获取资源。
独占方式获取和释放
1 | // 在调用acquire获取的时候首先会调用tryAcquire来设置当前状态值 |
共享方式获取和释放
对于共享方式和独占方式基本差不多,只不过在获取资源失败的时候线程会被标记为SHARED并放入AQS队列。还需要注意的是对于acquire系列的方法有一些是带Interruptibly的,这些带有Interruptibly的需要对中断进行相应,即其他线程中断了次挂起线程后,这个线程需要抛出InterruptedException然后返回。
条件变量的支持
对于 synchronized 来说只能支持一个共享变量的notify或wait方法, 而 AQS的锁能对应多个条件变量 ,和synchronized一样在调用条件变量的await和signal方法之前必须先获取条件变量对应的锁。
1 | // 创建一个ReentrantLock独占可重入锁 |
在其中有一个 lock.newCondition() 这个其实是new了一个在AQS中内部声明的一个ConditionObject对象,可以上去查看刚刚的类图。这个ConditionObject是AQS的一个内部类,每个条件变量维护了一个条件队列(我们可以看到ConditionObject中有firstWaiter lastWaiter等)用来存放调用相应condition的await()方法而被阻塞的线程。
我们来看condition的await方法
1 | public final void await() throws InterruptedException { |
我们再来看看signal方法
1 | public final void signal() { |
总结一下: 当多个线程调用lock.lock()方法的时候,未竞争到锁的线程会被转换成一个NOde节点插入AQS队列中,如果获取到了锁的线程(因为某种条件不满足)调用了await()方法的时候,会被转换成Node节点并插入相应的Condition条件队列中并且释放锁,这个时候未获取到锁的线程(在AQS队列中)就可以重新竞争锁了,如果另外一个线程调用条件变量的signal()或者signalAll()方法,会把条件变量队列中的一个或全部Node移动到AQS阻塞队列里。
独占锁 ReentrantLock 的原理
类图结构

1 | // 默认非公平锁 |
其中 ReentrantLock 中有一个内部类Sync 这个类继承了AQS,并且扩展了公平锁(FairSync)和非公平锁(NonFairSync)
在Sync中AQS的state状态值表示 线程的可重入次数。
获取锁
- void lock()
1 | // 在ReentrantLock中的lock方法调用了sync的lock |
我们来回顾一下非公平机制和公平机制到底是怎么实现的。
比如这个时候有两个线程竞争ReentrantLock 线程A和线程B并且线程C已经获取了ReentrantLock,当线程A进入lock()方法然后进入acquire()方法再进入nonfairTryAcquire()方法(非公平策略)会因为这个锁已经被其他线程占有而被阻塞进入AQS队列,线程B也同时竞争锁,进入进入lock()方法然后进入acquire()方法再进入nonfairTryAcquire()方法(非公平策略),而这个时候恰恰线程C释放了锁,那么线程B就会直接判断state值发现为0然后线程B就抢占了。我们可以发现,线程B是后来的但是先获得了锁,这就是非公平的体现。
而公平策略中会在抢占锁之前判断前面有没有线程在等待,如果有那么该线程直接放弃竞争。所以公平锁的资源浪费会比非公平高。
void lockInterruptibly() 方法
和lock()方法类似,但是它会对中断进行响应,就是当前线程在调用该方法的时候,其他线程如果调用了当前线程的interrupt()方法,该线程会抛出InterruptException异常然后返回。
释放锁
void unlock() 方法
尝试释放锁,如果当前线程持有锁则将AQS的state值减一,如果减下来为0那么该线程会释放锁,否则仅仅减一。如果当前线程没有持有锁而调用该方法会抛出IllegalMonitorStateException异常。
1 | public void unlock() { |
读写锁 ReentrantReadWriteLock 原理
读写锁类图结构

读写锁内部维护了一个ReadLock和WriteLock,他们 依赖Sync来实现具体功能, 而Sync继承AQS并且提供了公平和非公平锁的实现。
AQS中只维护了一个state变量,而读写锁中通过将state一分为二控制读状态和写状态。它巧妙的 将state的高16位表示读状态(也就是读锁的次数),低16位表示写状态也就是写锁的可重入次数。
1 | static final int SHARED_SHIFT = 16; |
其中 firstReader 用来获取第一个获取到读锁的线程, firstReaderHoldCount 则记录第一个获取到读锁的线程获取读锁的可重入次数, cachedHoldCounter 用来记录最后一个获取读锁的线程获取读锁的可重入次数。
1 | // 内部类 里面维护了count和线程id |
readHolds 是 ThreadLocal变量,用来存放出去第一个获取读锁线程外的其他线程获取读锁的可重入次数, ThreadLocalHoldCounter 继承了ThreadLocal。
1 | static final class ThreadLocalHoldCounter |
写锁的获取与释放
写锁通过WriteLock来实现。
- void lock()
1 | // WriteLock的lock() |
我们来看一下writerShouldBlock的实现
1 | // 非公平实现 |
- boolean tryLock()
1 | public boolean tryLock( ) { |
- void unlock()
1 | public void unlock() { |
读锁的获取域释放
- void lock()
1 | public void lock() { |
- void unlock()
1 | public void unlock() { |
JDK8中新增的 StampedLock 探究
概述
StampedLock是JDK8中新增的锁,它提供了三种模式的读写锁
- 写锁。 是一个独占锁而且是不可重入锁,请求该锁成功后会返回一个stamp参数来表示该锁的版本,当释放该锁的时候需要调用unlockWrite并传递获取锁是的stamp参数,并且它提供一个非阻塞的 tryWriteLock 方法。
- 悲观读锁。 是一个共享锁,在没有线程占有写锁的情况下可以获取。如果有线程占用写锁,那么竞争该锁的线程会被阻塞(和读写锁的读锁很像 但是他是不可重入锁) 这里悲观指 需要先将数据加锁, 这是在读少写多的去情况的一种考虑。 该锁请求成功后也会返回一个stamp值,并且在调用unlockRead方法时需要传入获取该锁时的stamp来释放锁。并且它提供了一个非阻塞的 tryReadLock 方法。
乐观读锁。 在操作数据之前没有通过CAS设置锁的装填,仅仅通过位运算测试,如果当前没有线程持有写锁,则简单返回一个非0的stamp版本信息。获取该stamp后再具体操作数据前还需要调用validate方法验证该stamp是否已经不可用,也就是看调用tryOptimisticRead返回stamp后当当前是否有其他线程持有了写锁,如果是则返回0,否则就可以通过该stamp版本的锁对数据进行操作,适用于读多写少的情况。
这三种锁还能在一定条件下相互转换。
最佳实践
我们可以巧妙的利用三种锁的转换和三种锁性能差异来设计一套最佳实践模板 这也是官方推荐的。
1 | // 读模板 |