文章目录
一、可重入锁ReentrantLock
1.1 什么是可重入锁
- 重入锁使用
java.util.concurrent.locks.ReentrantLock
类来实现 - 重入锁使用的简单例子:
public class ReenterLock implements Runnable {
private static ReentrantLock lock = new ReentrantLock();
private static int i = 0;
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
// 加锁
lock.lock();
try {
i++;
} finally {
// 释放锁
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLock demo = new ReenterLock();
Thread t1 = new Thread(demo);
Thread t2 = new Thread(demo);
t1.start();
t2.start();
// 等待t1、t2运行完再运行主线程打印和
t1.join();
t2.join();
System.out.println("sum: " + i);
}
}
注意:在使用
lock()
方法时,一定要有相应的unlock()
方法,并且放在finally
块中,防止忘记释放锁,导致其它线程无法进入临界区
1.2 为什么叫做可重入
- 可重入:即一个线程可以反复进入同一个锁
lock.lock();
lock.lock();
try{
i++;
}finally {
lock.unlock();
lock.unlock();
}
注意:如果一个线程多次获取锁,那么在释放锁的时候,也必须释放相同次数的锁。
- 如果释放次数多了,会得到一个异常
- 如果释放次数少了,就会继续持有锁,这会导致其它线程无法进入临界区
1.3 可重入锁的高级功能
1.3.1 中断响应
- 对于
synchronized
来说,一个线程等待锁只有两种情况- 获得锁并继续执行
- 未获得锁继续等待
- 而对于可重入锁而言,提供了被中断的功能,即:线程在等待锁的过程中,可以响应中断,取消对锁的请求
lockInterruptibly()
方法:等待锁的过程中可以响应中断- 代码示例:
/**
* @Title: 可重入锁的中断响应
* @Description: 两个线程启动后,分别占有一个锁,等待获取对方的锁;主线程被唤醒后,将线程“B”中断,线程"B"中断后释放锁并退出,而A获取锁
* @author: QianYi
* @date: 2020/9/9 - 21:13
*/
public class InterruptLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
private boolean flag;
public InterruptLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
try {
if (flag) {
try {
// 先申请lock1(可对中断响应)
lock1.lockInterruptibly();
// 睡眠
Thread.sleep(1000);
// 再申请lock2(可对中断响应)
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " get lock");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
try {
// 先申请lock2(可对中断响应)
lock2.lockInterruptibly();
// 睡眠
Thread.sleep(1000);
// 再申请lock2(可对中断响应)
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " get lock");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
// 释放锁
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
System.out.println(Thread.currentThread().getName() + " 线程退出");
}
}
public static void main(String[] args) throws InterruptedException {
InterruptLock task1 = new InterruptLock(true);
InterruptLock task2 = new InterruptLock(false);
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.start();
t2.start();
// 主线程睡眠2秒,等待线程t1、t2启动完毕
Thread.sleep(2000);
t2.interrupt();
}
}
执行结果:
线程 t1 启动后,先请求 lock1,再请求 lock2;线程 t2 启动后,先请求 lock2,再请求 lock1。
main 线程启动后,处于休眠状态,t1、t2 处于死锁状态(此时 t1 占用 lock1,t2 占用 lock2)。
当对 t2 进行中断后,t2 会放弃请求 lock1,同时会释放 lock2,线程 t1 此时可以继续占有 lock2,从而继续执行下去。
1.3.2 限时等待
- 概念:线程在等待锁时,可以给它指定一个等待时间,超过这个等待时间仍未获取锁,那就返回失败
tryLcok()
方法:可以进行限时等待- 带参数:两个参数,①等待时长;②计时单位。超过这个时间为获取锁返回
false
,否则返回true
- 不带参数:线程申请锁时,不会等待。若申请锁成功立即返回true,否则立即返回false
- 带参数:两个参数,①等待时长;②计时单位。超过这个时间为获取锁返回
- 代码(有参数、限时)
/**
* @Title: 可重入锁的限时等待(限时)
* @Description: A线程先拿到锁,睡4秒;而获取锁的时间只有3秒,所以B等待3秒后返回false
* @author: QianYi
* @date: 2020/9/9 - 21:25
*/
public class TimeLock implements Runnable {
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
// 若获取到锁,睡眠4秒
if (lock.tryLock(3, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + ":get lock success!");
Thread.sleep(4000);
} else {
// 未获取到,输出信息
System.out.println(Thread.currentThread().getName() + ":get lock failed!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
// 这里要加判断,因为不持有锁的线程释放锁会报异常
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
public static void main(String[] args) {
TimeLock task = new TimeLock();
new Thread(task, "A").start();
new Thread(task, "B").start();
}
}
获取锁的时间只有3秒,而获取锁后会睡眠4秒
A、B线程启动后,A先获取锁并睡眠4秒;所以B会等待获取锁,但只会等待3秒时间,所以B会申请锁失败
- 代码(无参数、立即返回)
/**
* @Title: 可重入锁的限时等待(立即返回)
* @Description: 只要等待一段时间,就会成功
* @author: QianYi
* @date: 2020/9/9 - 21:32
*/
public class TryLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
private boolean flag;
public TryLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
// 按顺序获取lock1、lock2
while (true) {
// 先获取lock1
if (lock1.tryLock()) {
try {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再获取lock2
if (lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " is over!");
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
}
} else {
// 按顺序获取lock2、lock1
while (true) {
// 先获取lock1
if (lock2.tryLock()) {
try {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再获取lock2
if (lock1.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " is over!");
return;
} finally {
lock1.unlock();
}
}
} finally {
lock2.unlock();
}
}
}
}
}
public static void main(String[] args) {
TryLock task1 = new TryLock(true);
TryLock task2 = new TryLock(false);
new Thread(task1, "A").start();
new Thread(task2, "B").start();
}
}
t1 先获取 lock1,t2 获取 lock2;然后 t1 申请 lock2,t2 申请 lock1。
一般情况下,这会导致 t1 和 t2 的相互等待,形成死锁。
但使用了 tryLock()
后,由于获取锁失败后会不断地进行尝试,直至成功
1.3.3公平锁
- 概念:按照线程到达的先后顺序获取锁;它不会产生饥饿现象。
public ReentrantLock(boolean fair)
- 当参数为false时,是非公平锁
- 当参数为true时,是公平锁;实现公平锁需要维护一个有序队列,实现成本高,但是性能低下
/**
* @Title: 可重入锁的公平锁
* @Description: 设置可重入锁为公平锁,所以它们是交替获取锁的
* @author: QianYi
* @date: 2020/9/9 - 21:46
*/
public class FairLock implements Runnable {
// 公平锁
private static ReentrantLock fairLock = new ReentrantLock(true);
// 非公平锁
// private static ReentrantLock fairLock = new ReentrantLock(false);
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
fairLock.lock();
System.out.println(Thread.currentThread().getName() + " get lock!");
} finally {
fairLock.unlock();
}
}
}
public static void main(String[] args) {
FairLock task = new FairLock();
new Thread(task, "A").start();
new Thread(task, "B").start();
}
}
1.4 相关方法
lock()
:申请锁,若锁被占用则等待。lockInterruptibly()
:申请锁,等待锁的过程中优先响应中断。tryLock()
:尝试获取锁,不等待,立即返回。若成功,立即返回true;若失败,立即返回false。tryLock(long time,TimeUnit unit)
:在指定时间内尝试获取锁。若成功,立即返回true;若失败,立即返回false。unlock()
:释放锁
1.5 可重入的三要素
- 原子状态:原子状态使用CAS操作来存储当前锁的状态,判断锁是否被别的线程持有了
- 等待队列:没有获取锁的线程会进入一个等待队列,当有线程释放锁后,就可以从等待队列中唤醒一个线程继续工作
- 阻塞原语park()和unpark():它们用来挂起和恢复线程。没有得到锁的线程会被挂起
二、 Condition重入锁搭档
2.1 介绍
Condition
与 wait()
方法和 notify()
方法大致相同。但是 wait()
方法和 notify()
方法是与关键字 synchronized
关键字组合使用的,而 Condition
是与重入锁相关联的
2.2 相关方法
void await() throws InterruptedException
:使当前线程等待,同时释放锁,在等待锁的过程中可以响应中断void awaitUninterruptibly()
:和await()方法大致相同,不同点在于它在等待锁的过程中无法响应中断boolean await(long time, TimeUnit unit)
:使当前线程等待一段时间(需要设置时间单位)long awaitNanos(long nanosTimeout)
:使当前线程等待一段时间(时间单位默认:纳秒)boolean awaitUntil(Date deadline)
:使当前线程等待至某一时刻void signal()
:唤醒一个在等待中的线程void signalAll()
:唤醒所有在等待中的线程
2.3 代码示例
/**
* @Title: Condition 的使用
* @Description:
* @author: QianYi
* @date: 2020/9/9 - 23:26
*/
public class ReenterLockConditon implements Runnable {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
@Override
public void run() {
try {
lock.lock();
// 等待、释放锁
condition.await();
System.out.println("线程正在运行!");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLockConditon task = new ReenterLockConditon();
Thread t1 = new Thread(task);
t1.start();
System.out.println("等待2秒...");
Thread.sleep(2000);
// 通知线程t1继续执行
lock.lock();
// 唤醒线程
condition.signal();
lock.unlock();
}
}
三、Semaphore信号量
3.1 信号量和锁的区别
- 内部锁synchronized和重入锁ReentrantLock,同时只允许一个线程访问一个资源
- 信号量同一时刻可以允许多个线程访问一个资源
3.2 主要方法
public Semaphore(int permits)
:构造函数,指定许可数量(默认非公平)public Semaphore(int permits, boolean fair)
:构造函数,指定许可数量,指定是否公平public void acquire()
:尝试获取一个许可。若无法获得,则会等待,直到有线程释放一个许可或者当前线程被中断public void acquireUninterruptibly()
:和acquire类似,但是不响应的中断public boolean tryAcquire()
:尝试获取一个许可,不等待,立即返回结果。若成功则立即返回false,若失败立即返回truepublic boolean tryAcquire(long timeout, TimeUnit unit)
:在一段时间内尝试获取一个许可。若成功则立即返回false,若失败立即返回truepublic void release()
:释放一个许可
3.3 代码示例
/**
* @Title: 信号量Demo
* @Description:
* @author: QianYi
* @date: 2020/9/9 - 23:40
*/
public class SemaphoreDemo implements Runnable {
// 定义信号量
private static Semaphore semaphore = new Semaphore(5);
@Override
public void run() {
try {
// 获取许可
semaphore.acquire();
// 操作
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " get it!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放许可
semaphore.release();
}
}
public static void main(String[] args) {
SemaphoreDemo semaphoreDemo = new SemaphoreDemo();
ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 20; i++) {
pool.submit(semaphoreDemo);
}
// 关闭线程池
pool.shutdown();
}
}
四、ReadWriteLock读写锁
4.1 读写锁和普通锁的区别
若线程A1、A2、A3进行写操作,线程B1、B2、B3进行读操作。如果使用重入锁ReentreantLock或内部锁synchronized,所有的读-读、读-写、写-写之间都是互斥的。然而读-读并不会破坏数据的完整性,所以读-读情况下的互斥不合理。这种情况下,可以使用读写锁,通过读写分离机制,使得读-读之间不再互斥
4.2 读写锁的访问约束情况
读 | 写 | |
---|---|---|
读 | 非互斥 | 互斥 |
写 | 互斥 | 互斥 |
4.3 适用情况
读次数>写次数。读次数远远大于写次数,则读写锁就可以发挥最大的作用
4.4 代码示例
/**
* @Title: 读写锁的使用
* @Description:
* @author: QianYi
* @date: 2020/9/9 - 23:55
*/
public class ReadWriteLockDemo {
private static ReentrantLock lock = new ReentrantLock();
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
private int value;
// 读操作
public int read(Lock lock) {
// 加锁
lock.lock();
try {
// 睡眠2秒后打印值
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " 的读操作: " + value);
return value;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
return 0;
}
// 写操作
public void write(Lock lock, int newValue) {
lock.lock();
try {
// 睡眠2秒后修改值并打印
Thread.sleep(2000);
value = newValue;
System.out.println(Thread.currentThread().getName() + " 的写操作: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
// 读任务
Runnable r1 = new Runnable() {
@Override
public void run() {
demo.read(readLock);
}
};
// 写任务
Runnable r2 = new Runnable() {
@Override
public void run() {
demo.write(writeLock, new Random().nextInt());
}
};
// 写线程
for (int i = 0; i < 5; i++) {
new Thread(r2).start();
}
// 读线程
for (int i = 0; i < 20; i++) {
new Thread(r1).start();
}
}
}
读写操作都会先睡眠2秒,然后再进行相关操作。
这段代码共启用5个写线程,20个读线程。
若使用普通锁,理论上需要50秒;但使用了读写锁,只需12秒
五、CountDownLatch:倒计数器
5.1 概念
CountDownLatch
可以让一个线程等待,直到倒计数器结束,这个线程才会继续执行
5.2 相关方法
public CountDownLatch(int count)
:构造函数,接收一个整数作为参数,即计数器的计数个数public void await()
:直到倒计数器变为0 之前一直等待public boolean await(long timeout, TimeUnit unit)
:直到倒计数器变为0 之前一直等待或者时间耗尽public void countDown()
:当计数器的计数大于0时,减1
5.3 代码示例
/**
* @Title: CountDownLatch 的使用
* @Description:
* @author: QianYi
* @date: 2020/9/10 - 0:13
*/
public class CountDownLatchDemo implements Runnable {
// 定义CountDownLatch,计数器设为 10
private static final CountDownLatch LATCH = new CountDownLatch(10);
@Override
public void run() {
try {
// 操作
Thread.sleep(500);
System.out.println("完成!");
// 计数器-1
LATCH.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
CountDownLatchDemo demo = new CountDownLatchDemo();
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
pool.submit(demo);
}
// 等待
LATCH.await();
System.out.println("全部完成!!!");
// 关闭线程池
pool.shutdown();
}
}
六、CyclicBarrier:循环栅栏
6.1 概念
CyclicBarrier
可以理解为循环栅栏,意味着这个计数器可以反复使用。
比如:我们可以把这个计数器设置为10,那么凑齐第一批10 个线程后,计数器归0,然后继续凑齐下一批线程。
6.2 代码示例
/**
* @Title: CyclicBarrier 的使用
* @Description:
* @author: QianYi
* @date: 2020/9/10 - 0:30
*/
public class CyclicBarrierDemo {
public static class Soldier implements Runnable {
private String soldier;
private CyclicBarrier cyclic;
public Soldier(String soldier, CyclicBarrier cyclic) {
this.soldier = soldier;
this.cyclic = cyclic;
}
@Override
public void run() {
try {
// 等待
cyclic.await();
doSomething();
// 等待
cyclic.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
public void doSomething() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(soldier + "任务完成!");
}
}
public static class BarrierRun implements Runnable {
private boolean flag;
private int N;
public BarrierRun(boolean flag, int N) {
this.flag = flag;
this.N = N;
}
@Override
public void run() {
if (flag) {
System.out.println("司令:[士兵" + N + "个,任务完成!]");
} else {
System.out.println("司令:[士兵" + N + "个,集合完毕!]");
// 修改标志
flag = true;
}
}
}
public static void main(String[] args) {
final int N = 10;
boolean flag = false;
Thread[] allSoldiers = new Thread[N];
CyclicBarrier cyclic = new CyclicBarrier(N, new BarrierRun(flag, N));
System.out.println("集合队伍!");
for (int i = 0; i < N; i++) {
System.out.println("士兵" + i + "报道!");
allSoldiers[i] = new Thread(new Soldier("士兵" + i, cyclic));
allSoldiers[i].start();
// if (i == 5) allSoldiers[i].interrupt();
}
}
}
6.3 两个异常
InterruptException
:在等待过程中,线程被中断BrokenBarrierException
:表示当前CyclicBarrier已经破损了,系统没办法等待所有线程到齐了
例如在73 行加入如下代码
if (i == 5) allSoldiers[i].interrupt();
会得到一个 InterruptException
和9 个 BrokenBarrierException
,InterruptException
是被线程中断抛出的,而其它的 BrokenBarrierException
则是等待在当前 CyclicBarrier
上的线程抛出的,这个异常可以防止其它的线程进行永久且无谓的等待
七、 LockSupport:线程阻塞工具类
7.1 LockSupport与suspend()、wait()的对比
LockSupport
可以在线程内任意位置让线程阻塞
- 相比于
Thread.suspend()
,它弥补了由于resume()
方法先执行而导致线程无法继续执行的情况 - 相比于
Objec.wait()
,它不需要先获得某个对象的锁,也不会抛出InterruptException
异常
7.2 LockSupport的原理
LockSupport
类使用类似于信号量的机制。它为每个线程准备了一个许可,
- 对于
park()
方法- 如果许可可用,那么就会消费这个许可(即把许可变为不可用),并立即返回
- 如果许可不可用,那么就会阻塞
- 对于
unpark()
方法,使得一个许可变为可用(和信号量不同,许可不能累加,你最多只能拥有一个许可)
7.3 park()和suspend()的区别
- 若
resume()
方法发生在suspend()
方法前,系统可能就无法继续往下执行了;但即使unpark()
方法操作发生在park()
方法之前,它也可以使下一次得park()
方法操作立即返回 suspend()
挂起线程会给出一个Runnable
状态,而park()
方法挂起线程会给出WAITING
状态,还会标注是park()
方法引起的
7.4 支持中断
LockSupport.park()
还能支持中断影响,但它不会抛出 InterrruptException
异常,只会默默返回,不过我们可以从 Thread.interrupted()
方法中获得中断标记
八、Guava和RateLimiter限流
8.1 漏桶算法
- 基本思想:利用一个缓存区,当有请求进入系统时,无论请求得速率如何,都会在缓存区内保存,然后以固定得速率流出缓存区进行处理
- 特点:无论请求得速率如何,漏桶算法总是以恒定得速率处理数据。漏桶得容积和速率是该算法得两个重要参数
8.2 令牌桶算法
基本思想:在令牌桶算法中,桶中存放得不是请求,而是令牌。处理程序只有拿到令牌后,才能对请求进行处理。如果没有令牌,处理程序要么丢弃请求,要么等待可用的令牌。为了限时流速,系统会在每个单位之间内产生一定量得令牌放入桶中。一般,桶的容量是有限的,令牌数量超过桶容量时就不再增加。