concurrent包的结构层次
Lock简介
Lock lock = new ReentrantLock(); lock.lock(); try{ ....... }finally{ lock.unlock(); }需要注意的是synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁,因此在finally块中释放锁。
Lock接口API
void lock(); //获取锁public class ReentrantLock implements Lock, java.io.Serializable查看源码时你会惊讶的发现ReentrantLock并没有多少代码,另外有一个很明显的特点是:基本上所有的方法的实现实际上都是调用了其静态内存类Sync中的方法,而Sync类继承了AbstractQueuedSynchronizer(AQS)。可以看出要想理解ReentrantLock关键核心在于对队列同步器AbstractQueuedSynchronizer(简称同步器)的理解。
初识AQS
同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成等待队列。它的子类必须重写AQS的几个protected修饰的用来改变同步状态的方法,其他方法主要是实现了排队和阻塞机制。状态的更新使用getState,setState以及compareAndSetState这三个方法。
子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以方便的实现不同类型的同步组件。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者的关系:锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作。锁和同步器很好的隔离了使用者和实现者所需关注的领域。
AQS的模板方法设计模式
AQS的设计是使用模板方法设计模式,它将一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法。举个例子,AQS中需要重写的方法tryAcquire:protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }ReentrantLock中NonfairSync(继承AQS)会重写该方法为:
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }而AQS中的模板方法acquire():
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
会调用tryAcquire方法,而此时当继承AQS的NonfairSync调用模板方法acquire时就会调用已经被NonfairSync重写的tryAcquire方法。这就是使用AQS的方式,在弄懂这点后会lock的实现理解有很大的提升。可以归纳总结为这么几点:
- 同步组件(这里不仅仅值锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内存类;
- AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法;
- AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义;
- 在重写AQS的方式时,使用AQS提供的getState(),setState(),compareAndSetState()方法进行修改同步状态
AQS提供的模板方法可以分为3类:
- 独占式获取与释放同步状态;
- 共享式获取与释放同步状态;
- 查询同步队列中等待线程情况;
同步组件通过AQS提供的模板方法实现自己的同步语义。
下面使用一个例子来进一步理解下AQS的使用。class Mutex implements Lock, java.io.Serializable { // Our internal helper class // 继承AQS的静态内存类 // 重写方法 private static class Sync extends AbstractQueuedSynchronizer { // Reports whether in locked state protected boolean isHeldExclusively() { return getState() == 1; } // Acquires the lock if state is zero public boolean tryAcquire(int acquires) { assert acquires == 1; // Otherwise unused if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // Releases the lock by setting state to zero protected boolean tryRelease(int releases) { assert releases == 1; // Otherwise unused if (getState() == 0) throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null); setState(0); return true; } // Provides a Condition Condition newCondition() { return new ConditionObject(); } // Deserializes properly private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // reset to unlocked state } } // The sync object does all the hard work. We just forward to it. private final Sync sync = new Sync(); //使用同步器的模板方法实现自己的同步语义 public void lock() { sync.acquire(1); } public boolean tryLock() { return sync.tryAcquire(1); } public void unlock() { sync.release(1); } public Condition newCondition() { return sync.newCondition(); } public boolean isLocked() { return sync.isHeldExclusively(); } public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } }MutexDemo:
public class MutextDemo { private static Mutex mutex = new Mutex(); public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(() -> { mutex.lock(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } finally { mutex.unlock(); } }); thread.start(); } } }
上面的这个例子实现了独占锁的语义,在同一个时刻只允许一个线程占有锁。MutexDemo新建了10个线程,分别睡眠3s。从执行情况也可以看出来当前Thread-6正在执行占有锁而其他Thread-7,Thread-8等线程处于WAIT状态。按照推荐的方式,Mutex定义了一个继承AQS的静态内部类Sync,并且重写了AQS的tryAcquire等等方法,而对state的更新也是利用了setState(),getState(),compareAndSetState()这三个方法。在实现实现lock接口中的方法也只是调用了AQS提供的模板方法(因为Sync继承AQS)。从这个例子就可以很清楚的看出来,在同步组件的实现上主要是利用了AQS,而AQS“屏蔽”了同步状态的修改,线程排队等底层实现,通过AQS的模板方法可以很方便的给同步组件的实现者进行调用。而针对用户来说,只需要调用同步组件提供的方法来实现并发编程即可。同时在新建一个同步组件时需要把握的两个关键点是:
- 实现同步组件时推荐定义继承AQS的静态内存类,并重写需要的protected修饰的方法;
- 同步组件语义的实现依赖于AQS的模板方法,而AQS模板方法又依赖于被AQS的子类所重写的方法。
通俗点说,因为AQS整体设计思路采用模板方法设计模式,同步组件以及AQS的功能实际上别切分成各自的两部分:
同步组件实现者的角度:
通过可重写的方法:独占式: tryAcquire()(独占式获取同步状态),tryRelease()(独占式释放同步状态);共享式 :tryAcquireShared()(共享式获取同步状态),tryReleaseShared()(共享式释放同步状态);告诉AQS怎样判断当前同步状态是否成功获取或者是否成功释放。同步组件专注于对当前同步状态的逻辑判断,从而实现自己的同步语义。这句话比较抽象,举例来说,上面的Mutex例子中通过tryAcquire方法实现自己的同步语义,在该方法中如果当前同步状态为0(即该同步组件没被任何线程获取),当前线程可以获取同时将状态更改为1返回true,否则,该组件已经被线程占用返回false。很显然,该同步组件只能在同一时刻被线程占用,Mutex专注于获取释放的逻辑来实现自己想要表达的同步语义。
AQS的角度
而对AQS来说,只需要同步组件返回的true和false即可,因为AQS会对true和false会有不同的操作,true会认为当前线程获取同步组件成功直接返回,而false的话就AQS也会将当前线程插入同步队列等一系列的方法。
总的来说,同步组件通过重写AQS的方法实现自己想要表达的同步语义,而AQS只需要同步组件表达的true和false即可,AQS会针对true和false不同的情况做不同的处理
AbstractQueuedSynchronizer(AQS)简介
独占式锁:
void acquire(int arg):独占式获取同步状态,如果获取失败则插入同步队列进行等待; void acquireInterruptibly(int arg):与acquire方法相同,但在同步队列中进行等待的时候可以检测中断; boolean tryAcquireNanos(int arg, long nanosTimeout):在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false; boolean release(int arg):释放同步状态,该方法会唤醒在同步队列中的下一个节点
共享式锁:
首先了解下AQS中的同步队列是一种什么样的数据结构,因为同步队列是AQS对同步状态的管理的基石。void acquireShared(int arg):共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态; void acquireSharedInterruptibly(int arg):在acquireShared方法基础上增加了能响应中断的功能; boolean tryAcquireSharedNanos(int arg, long nanosTimeout):在acquireSharedInterruptibly基础上增加了超时等待的功能; boolean releaseShared(int arg):共享式释放同步状态
同步队列
当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。就数据结构而言,队列的实现方式无外乎两者一是通过数组的形式,另外一种则是链表的形式。AQS中的同步队列则是通过链式方式进行实现。接下来,很显然我们至少会抱有这样的疑问:**1. 节点的数据结构是什么样的?2. 是单向还是双向?3. 是带头结点的还是不带头节点的?**我们依旧先是通过看源码的方式。在AQS有一个静态内部类Node,其中有这样一些属性:
volatile int waitStatus //节点状态volatile Node prev //当前节点/线程的前驱节点volatile Node next; //当前节点/线程的后继节点volatile Thread thread;//加入同步队列的线程引用Node nextWaiter;//等待队列中的下一个节点
节点的状态有以下这些:
节点的数据结构类型,并且每个节点拥有其前驱和后继节点,很显然这是一个双向队列。同样的我们可以用一段demo看一下。int CANCELLED = 1//节点从同步队列中取消int SIGNAL = -1//后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行;int CONDITION = -2//当前节点进入等待队列中int PROPAGATE = -3//表示下一次共享式同步状态获取将会无条件传播下去int INITIAL = 0;//初始状态
public class LockDemo { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { for (int i = 0; i < 5; i++) { Thread thread = new Thread(() -> { lock.lock(); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }); thread.start(); } } }实例代码中开启了5个线程,先获取锁之后再睡眠10S中,实际上这里让线程睡眠是想模拟出当线程无法获取锁时进入同步队列的情况。通过debug,当Thread-4(在本例中最后一个线程)获取锁失败后进入同步时,AQS时现在的同步队列如图所示:
Thread-0先获得锁后进行睡眠,其他线程(Thread-1,Thread-2,Thread-3,Thread-4)获取锁失败进入同步队列,同时也可以很清楚的看出来每个节点有两个域:prev(前驱)和next(后继),并且每个节点用来保存获取同步状态失败的线程引用以及等待状态等信息。另外AQS中有两个重要的成员变量:
private transient volatile Node head; private transient volatile Node tail;也就是说AQS实际上通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁时对同步队列中的线程进行通知等核心方法。其示意图如下:
通过对源码的理解以及做实验的方式,现在我们可以清楚的知道这样几点:
- 节点的数据结构,即AQS的静态内部类Node,节点的等待状态等信息;
- 同步队列是一个双向队列,AQS通过持有头尾指针管理同步队列;
那么,节点如何进行入队和出队是怎样做的了?实际上这对应着锁的获取和释放两个操作:获取锁失败进行入队操作,获取锁成功进行出队操作。