一、重入锁 ReentrantLock
- 重入锁可以完全替代关键字 synchronized。在 JDK 1.5 之前,重入锁的性能远高于关键字 synchronized;JDK 1.6 开始,两者的性能差距并不大
- 重入锁有着显式的操作过程,对逻辑控制有更强的灵活性
1 | public class ReenterLock implements Runnable { |
- 重入锁可以在一个线程反复进入
1 | lock.lock(); |
- 重入锁可以中断响应:等待锁的过程中,程序可以根据需要取消对锁的请求
1 | public class IntLock implements Runnable { |
- 锁申请等待限时:给定一个等待时间,让线程自动放弃请求锁
1 | public class TimeLock implements Runnable { |
tryLock()
可以不带参数运行:当前线程会尝试获得锁,如果锁未被其他线程占用,则申请成功,并立即返回 true,否则立即返回 false
1 | public class TryLock implements Runnable { |
- 大多数情况下,锁的申请都是非公平的:线程 A 和 B 同时申请锁,系统会从这个锁的等待队列中随机挑选一个
- 根据系统的调度,一个线程会倾向于再次获取己经持有的锁,这种分配方式是高效的,但是无公平性可言
- 公平锁会按照申请时间的先后顺序,依次赋予锁,保证了不会产生饥饿现象
- 要实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能也非常低下
1 | public class FairLock implements Runnable { |
- 在重入锁的实现中,主要包含三个要素:
- 原子状态:原子状态使用 CAS 操作来存储当前锁的状态,判断锁是否己经被别的线程持有了
- 等待队列:所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作
- 阻塞原语:
park()
和unpark()
,用来挂起和恢复线程。没有得到锁的线程将会被挂起
二、重入锁的等待 Condition
- Condition 对象与
wait()
和notify()
方法的作用大致相同,可以让线程在合适的时间等待,或在某一个特定的时刻得到通知,它与重入锁相关联
1 | public class ReenterLockCondition implements Runnable { |
- Condition 接口提供的基本方法如下:
1 | // 使当前线程等待,同时释放当前锁。可被中断。使用前当前线程需获取锁 |
- JDK 内部,重入锁和 Condition 对象被广泛使用,如 ArrayBlockingQueue
1 | final ReentrantLock lock; |
三、信号量 Semaphore
- 信号量是对锁的扩展:无论是 synchronized 还是 ReentrantLock,一次只允许一个线程访问一个资源,而信号量可以指定多个线程同时访问某一个资源
1 | // 信号量的准入数, 是否公平 |
- 案例:
1 | public class SemapTest implements Runnable { |
四、读写锁 ReadWriteLock
- ReadWriteLock 是 JDK 5 提供的读写分离锁,可以有效地减少锁竞争
- 读写锁允许多个线程同时读,但写写操作和读写操作间依然是需要相互等待和持有锁的
1 | public class ReadWriteLockTest { |
五、倒计数器 CountDownLatch
- 倒计数器 CountDownLatch 通常用来控制线程等待,它可以让某一个线程等待直到倒计数结束,再开始执行
1 | public class CountDownLatchTest implements Runnable { |
六、循环栅栏 CyclicBarrier
- 循环栅栏 CyclicBarrier 通常用来阻止线程继续执行,让线程在栅栏外等待,达到一定数量后再执行。其计数器可以反复使用
1 | public class CyclicBarrierTest implements Runnable { |
七、线程阻塞工具 LockSupport
- LockSupport 可以在线程内任意位置让线程阻塞。与
Thread.suspend()
方法相比,它弥补了resume()
方法导致线程无法继续执行的情况;和Object.wait()
方法相比,它不需要先获得某个对象的锁,也不会抛出InterruptedException
异常 - LockSupport 有
parkNanos()
、parkUntil()
等方法,可以实现限时的等待 - LockSupport 内部使用类似信号量的机制,它为每个线程准备了一个许可,默认不可用。如果许可可用,
park()
方法会立即返回,并消费这个许可;如果许可不可用,就会阻塞;unpark()
方法会使这个许可变为可用(和信号量不同,只能拥有一个许可,不能累加)
1 | public class LockSupportTest { |
park()
方法挂起的线程不会像suspend()
方法那样显示 RUNNABLE 状态- 如果使用
park(Object)
方法,可以为当前线程设置一个阻塞对象,该对象也会出现在 Dump 中
1 | // park(); |
park()
方法支持中断,但不会抛出 InterruptedException 异常,而是直接返回
1 | public class LockSupportTest2 { |
八、RateLimiter 限流
- Guava 是 Google 下的一个核心库,提供了一大批设计精良、使用方便的工具类
- 任何应用和模块组件都有一定的访问速率上限,如果请求速率突破了这个上限,不但多余的请求无法处理,甚至会压垮系统使所有的请求均无法有效处理。因此,对请求进行限流是非常必要的。RateLimiter 正是这么一款限流工具
- 一种简单的限流算法就是给出一个单位时间,然后使用一个计数器 counter 统计单位时间内收到的请求数量,当请求数量超过上限时,余下的请求丢弃或者等待。但这种算法很难控制边界时间上的请求
- 常用的限流算法有两种:漏桶算法和令牌桶算法
- 漏桶算法:利用一个缓存区,当有请求进入系统时,无论请求的速率如何,都先在缓存区内保存,然后以固定的流速流出缓存区进行处理
- 漏桶算法的特点:无论外部请求压力如何,漏桶算法总是以固定的流速处理数据。漏桶的容积和流出速率是该算法的两个重要参数
- 令牌桶算法:是一种反向的漏桶算法,桶中存放的不再是请求,而是令牌。处理程序只有拿到令牌后,才能对请求进行处理。如果没有令牌,那么处理程序要么丢弃请求,要么等待可用的令牌
- 为了限制流速,该算法会在每个单位时间产生一定量的令牌存入桶中(不会超过桶的容量上限)
- RateLimiter 采用了令牌桶算法
1 | public class RateLimiterTest { |