1.锁
锁用来控制多线程按照顺序访问临界资源,通常用锁限制同一时间内只有一个线程能访问到临界资源;读写锁对于临界资源的读取操作同一个时间内允许多个线程同时访问,写资源必须互斥执行。
Java提供两种锁
synchronized
Lock锁(实现Lock接口的锁)
Lock锁的特点
(1)能尝试非阻塞的获取锁;
(2)能响应中断,Lock.lockInterruptibly()阻塞等待锁时能响应中断,Lock.lock()不响应中断
(3)能超时获取锁,如果超时会返回,不会一直阻塞等待
Lock锁的实现基本都是通过聚合一个AQS(队列同步器)的子类来控制线程同步访问临界资源(让线程按照顺序访问临界资源)。
2.AQS队列同步器
AQS全称AbstractQueuedSynchronizer,抽象队列同步器,是构建锁的基础框架,是实现Java多线程同步的基础。
AQS是实现锁(任意同步组件)的关键,通常在内部聚合同步器来实现锁。
锁和AQS的关系,锁面向使用者,锁为使用者屏蔽了内部实现的细节;AQS面向实现者,AQS为实现者屏蔽了同步状态管理、线程排队、等待和唤醒等底层操作,简化了锁的实现。
AQS内部包含一个volatile int型state成员变量,用于表示锁的状态;还包含一个FIFO先进先出的线程队列,用于控制线程获取锁的顺序。AQS整体结构如下图。
AQS可重写的方法
方法 | 描述 |
protected boolean tryAcquire(int arg) | 独占式获取同步状态,需要CAS操作 |
protected boolean tryRelease(int arg) | 独占式释放同步状态 |
protected int tryAcquireShared(int arg) | 共享式获取同步状态 |
protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 |
protected boolean isHeldExclusively() | 独占模式下,本线程是否占有同步状态 |
AQS提供的模板方法
方法 | 描述 |
void acquire(int arg) | 独占式获取同步状态,未获取到时线程进入同步队列等待,不响应中断 |
void acquireInterruptibly(int arg) | 独占式获取同步状态,未获取到时线程进入同步队列等待,响应中断抛出InterruptedException |
boolean tryAcquireNanos(int arg, long nanosTimeout) | 在acquireInterruptibley()基础上增加了超时等待 |
void acquireShared(int arg) | 共享式获取同步状态,未获取到时线程进入同步队列等待,不响应中断,该模式同一时间多个线程可以同时获取同步状态 |
void acquireSharedInterruptibly(int arg) | 共享式获取同步状态,未获取到是线程进入同步队列等待,响应中断抛出InterruptedException |
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) | 在acquireShardedInterruptibly()基础上增加了超时等待 |
boolean release(int arg) | 独占式释放同步状态,释放同步状态之后,将同步队列中第一个节点唤醒 |
boolean releaseShared(int arg) | 共享式释放同步状态 |
Collection<Thread> getQueuedThreads() | 获取在同步队列中等待的线程集合 |
2.1 AQS的acquire()方法
AQS的acquire()方法,独占式获取同步状态。
让线程入队列的过程如下图。头结点是一个占位的节点,并没有线程与之对应。为了保证线程操作安全,tail节点必须用CAS操作来指向新的尾结点。
对于入队的线程,只有处在第1位的线程才能获取state所有权。
acquire()方法不响应中断。
2.2 AQS的acquireInterruptibly()方法
与AQS的acquire()方法执行过程基本相同,不同的是对线程中断的处理方式。
acquire()方法检测到线程中断后不会抛出InterruptedException异常,会缓存中断状态,然后清除掉线程的中断状态,当获取到同步状态之后才调用selfInterrupt()方法中断本线程(但不会抛出中断异常)。
acquireInterruptibly()方法检测到线程中断后,先清除掉线程的中断状态,然后抛出InterruptedException异常。
2.3 AQS的release()方法
先调用tryRelease()方法释放锁,然后唤醒同步队列中第一个节点的线程。通过LockSupport.unpark()唤醒指定的线程。
被唤醒的线程出队列的过程如下图。
被唤醒的线程出队列成功之后,占位节点变为垃圾对象会被自动回收,被唤醒的线程对应的节点被改成占位节点。
注意:同步队列中第一个节点被唤醒后,不一定能获取到同步状态。这是因为在竞争很激烈的情况下,此时可能存在大量其他未入队的线程来获取同步状态。如果同步状态被未入队的线程获取,这个被唤醒的线程又会阻塞等待。
参考资料
《Java并发编程的艺术》 机械工业出版社 方腾飞 魏鹏 程晓明