【讲义】第2讲:AQS和JUC

⼀、ReentrantLock重⼊锁

1.1> 概述

1.2> 中断响应 lockInterruptibly()

1.3> 锁申请等待限时 tryLock(long time, TimeUnit unit)

1.4> 公平锁和⾮公平锁

1.5> AQS源码解析

⼆、Condition重⼊锁的搭配类

三、Semaphore信号量

四、ReadWriteLock读写锁

五、CountDownLatch倒计时器

六、CyclicBarrier循环栅栏

七、LockSupport线程阻塞⼯具类

⼀、ReentrantLock重⼊锁

1.1> 概述

重⼊锁可以完全替代synchronized关键字。在JDK5.0的早期版本中,重⼊锁的性能远远好于

synchronized,但从JDK6.0开始,JDK在synchronized上做了⼤量的优化,使得两者的性能差距并

不⼤。重⼊锁对逻辑控制的灵活性要远远好于synchronized。

重⼊锁常⽤⽅法

void lock():获得锁,如果锁已经被占⽤,则等待。

void lockInterruptibly():获得锁,但优先响应中断。

boolean tryLock():尝试获得锁,如果成功,返回true;如果失败则返回false;获得不到锁,则不进

⾏等待,⽴即返回。

boolean tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。

boolean isHeldByCurrentThread():判断担⼼线程是否持有锁。

void unlock():释放锁。

下⾯是使⽤重⼊锁的简单示例:

1之所以称之为重⼊锁,就是⼀个线程允许反复进⼊。当然,这⾥的反复仅仅局限于⼀个线程;如

果同⼀个线程多次获锁,那么在释放锁的时候,也必须释放相同次数。如果释放锁的次数多,那么

会得到⼀个java.lang.IllegalMonitorStateException异常,反之,如果释放锁的次数少,那么相当

于线程还持有这个锁。如下所示:

21.2> 中断响应 lockInterruptibly()

如果使⽤synchronized,要么获得锁,要么保持等待。⽽如果使⽤了重⼊锁,则提供了另⼀种可

能,那就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。即:

如果⼀个线程正在等待锁,那么它依然可以收到⼀个通知,被告知⽆须再等待,可以停⽌⼯作了

可以很好的应对死锁问题。示例如下所示:

341.3> 锁申请等待限时 tryLock(long time, TimeUnit unit)

除了等待外部通知之外,要避免死锁还有另外⼀种⽅式,就是限时等待。以下⾯为例,线程尝试

获得锁,如果没有获得锁,则等待5秒钟。如果5秒钟之后依然没有获得锁,则返回false,表示获得

锁失败。

tryLock()⽅法也可以不带参数直接运⾏。在这种情况下,当前线程会尝试获得锁,如果锁并未被其

他线程占⽤,则申请锁会成功,并⽴即返回true。如果锁被其他线程占⽤,则当前线程不会进

⾏等待,⽽是⽴即返回false。这种模式不会引起线程等待,因此也不会产⽣死锁。

51.4> 公平锁和⾮公平锁

在⼤多数情况下,锁的申请都是⾮公平锁。系统只是会从这个锁的等待线程中随机选择⼀个。类似

⼤家买票不去排队,乱哄哄的围在售票窗⼝前,售票员忙得焦头烂额,也顾不及谁先谁后,随便找

⼀个⼈出票就完事⼉了。

6当⼊参为true时,则采⽤公平锁⽅式。要求系统维护⼀个有序队列,因此公平锁的实现成本⽐较

⾼,性能相对也⾮常低下。因此,默认情况下,锁是⾮公平的。如果没有特别的需求,也不需要使

⽤公平锁。

71.5> AQS源码解析

详情请⻅:【源码解析】AbstractQueuedSynchronizer.pdf

⼆、Condition重⼊锁的搭配类

相同点

Condition和Object的wait()、notify()⽅法的作⽤是⼤致相同的,对应关系如下所示:

Condition.await()--->Object.wait()

Condition.signal()--->Object.notify()

Condition.signalAll()--->Object.notifyAll()

不同点

Object.wait()和Object.notify()⽅法是和Synchronized关键字合作使⽤的;⽽Condition是与

ReentrantLock相关联的。

Condition常⽤⽅法

8void await()

会使当前线程等待,同时释放当前锁,当其他线程中使⽤signal()或者signalAll()⽅法时,线程会

重新获得锁并继续执⾏。或者当线程被中断是,也能跳出等待。这和Object.wait()⽅法很相似。

void awaitUninterruptibly()

与await()⽅法基本相同,区别是它不会在等待过程中响应中断。

long awaitNanos(long nanosTimeout)

如果nanosTimeout时间内,没有被执⾏signal,则解除等待状态。

boolean await(long time, TimeUnit unit)

如果time时间内,没有被执⾏signal,则解除等待状态。

boolean awaitUntil(Date deadline)

如果deadline时间内,没有被执⾏signal,则解除等待状态。

void signal()

⽤于唤醒⼀个正在等待中的线程。

void signalAll()

⽤于唤醒所有正在等待中的线程。

Condition使⽤示例:

三、Semaphore信号量

⼴义上说,Semapore信号量是对锁的⼀种扩展;因为⽆论是内部锁synchronized,还是重⼊锁

910

ReentrantLock,⼀次都只允许⼀个线程访问某⼀资源,⽽信号量却可以指定多个线程同时访

问某⼀个资源

信息量主要提供了⼀下构造函数,必须要指定信号量的准⼊数,即:同时能申请多少个许可

信息量主要⽅法如下所示:

申请了4个准⼊,循环10个线程,那么将会以4个线程⼀组为单位进⾏执⾏输出

ReadWriteLock是JDK5中提供的读写分离锁。它允许多个线程同时读。但是考虑到数据的完整性,

写写操作和读写操作间依然是需要相互等待和持有锁的。读写锁的访问约束情况如下所示:

public Semaphore(int permits); // permits:准⼊数

public Semaphore(int permits, boolean fair); // permits:准⼊数,fair:是否公平获得锁

public void acquire();

尝试获得⼀个准⼊的许可。若⽆法获得,则线程会等待,直到有线程释放

⼀个许可或者当前线程被中断。

public void acquireUninterruptibly();

具有acquire⼀样的功能,但是不响应中断。

public void tryAcquire(); 尝试获得⼀个许可,如果成功就返回true,失败则返回false。

public void tryAcquire(long timeout, TimeUnit unit); 在指定时间内,尝试获得⼀个许可,如果成功

就返回true,失败则返回false。

public void release();

资源访问结束后,释放⼀个许可。

四、ReadWriteLock读写锁

●● 下⾯例⼦中,我们使⽤普通锁,执⾏读写操作:

下⾯例⼦中,我们使⽤读写锁(只需要将上⾯例⼦中openRWLock=true即可)执⾏读写操作,如

11下所示:

所以,如果在系统中,读操作的次数远远⼤于写操作,那么读写锁就可以发挥最⼤的效果,提升系

统的性能。

五、CountDownLatch倒计时器

CountDownLatch是⼀个多线程控制⼯具。⽤来控制线程的等待。设置需要countDown的数量

num,然后每⼀个线程执⾏完毕后,调⽤countDown()⽅法,⽽主线程调⽤await()⽅法执⾏等待,

直到num个⼦线程执⾏了countDown()⽅法 ,则主线程开始继续执⾏。

12六、CyclicBarrier循环栅栏

CyclicBarrier与CountDownLatch⾮常类似,它⽀持计数器的反复使⽤,CyclicBarrier可以

理解为循环栅栏。CyclicBarrier可以接收⼀个参数作为Runnable barrierAction,每当计数器

⼀次计数完成后——CyclicBarrier.await()时,系统会执⾏的动作。

13CyclicBarrier.await()⽅法可能会抛出两种异常:⼀个是InterruptedException,也就是

在等待过程中,线程被中断,应该说这是⼀个⾮常通⽤的异常,⼤部分迫使线程等待的⽅法都可能

会抛出这个异常,使得线程在等待时依然可以响应外部紧急事件。另外⼀个异常则是CyclicBarrier

特有的BrokenBarrierException,⼀旦遇到这个异常,则表示当前的CyclicBarrier已经破损

了,可能系统已经没有办法等待所有线程到⻬了。如果继续等待,可能就是徒劳⽆功的,因此就此

结束吧。

14七、LockSupport线程阻塞⼯具类

LockSupport是⼀个⾮常⽅便实⽤的线程阻塞⼯具,它可以在线程内任意位置让线程阻塞。和

Thread.suspend()相⽐,它弥补了由于resume()在前发⽣,导致线程⽆法继续执⾏的情况。和

Object.wait()⽅法相⽐,它不需要先获得某个对象的锁,也不会抛出InterruptedException

异常。

park()可以阻塞当前线程,其中每⼀个线程都有⼀个许可,该许可默认为[不可⽤]。如果该许可

是[可⽤]状态,那么park()⽅法会⽴即返回,消费这个许可,将该许可变更为[不可⽤]状态,流

程代码可以继续执⾏。如果该许可是[不可⽤]状态,那么park()⽅法将会阻塞

unpark⽅法,将指定线程的⼀个许可变为[可⽤]状态。如下表所示:

15示例⼀:先执⾏unpark()⽅法再执⾏park()⽅法,也不会造成永久卡死线程。如下所示:

示例⼆:LockSupport.park()还能⽀持中断。但是它不会抛InterruptedException异常

它只会默默的返回,但是我们可以从Thread.interrupted()等⽅法获得中断标记。

1617