AQS系列
1、AQS核心原理
2、ReentrantLock 原理及示例
3、CountDownLatch / Semaphore 示例及使用场景
4、BlockingQueue 示例及使用场景
文章目录
一、ReentrantLock原理
ReentrantLock 基于 AQS 框架应用的实现,是 JDK1.5 提供的线程并发控制的手段,和 synchronized 关键字的功能类似,是一种互斥锁,可以保证线程的安全。
那么我们在实例化的时候可以发现,构造方法 ReentrantLock(boolean fair) 是带了 fair 参数的,参数为true 是公平锁,false 是非公平锁。
而且 ReentrantLock 是可重入锁,意思是我们可以多次调用 lock() 方法,重复加锁,当然这样的话就需要多次调用 unlock() 方法,多次解锁了。
ReentrantLock 是一种互斥锁,那是如何实现公平锁和非公平锁的呢?
在 ReentrantLock 内部是定义了一个 Sync 内部类,该类继承了 AbstractQueuedSynchronizer,实现了部分方法,这也是模板模式的实现。
还定义了 FairSync(公平锁) 和 NonfairSync(非公平锁) 类,这两个类都是继承自 Sync,也就间接基础了 AbstractQueuedSynchronizer ,所以 ReentrantLock 也就实现了 公平锁 和 非公平锁。
那 ReentrantLock 呢本身是实现了 Lock 接口来实现等待与唤醒的。
二、上公共厕所(Demo)
这里用一个小的 Demo 来使用一下 ReentrantLock 加锁功能,好多都是卖票的例子,多个窗口或者多个接口卖一张票,但是不能卖重复,那我这里就换一个场景,一个公共厕所多个门,每次只能有一个人使用。
场景是这样的,商场里面可以有多条道路通往卫生间,但是每次只能有一个人使用,除非别人同意和你一起使用 ^ _ ^,估计没人会。
现如今都是有素质的人们,如果某个时刻有多个人来了,看到有人,那就排队了,这就是公平锁。
2.1 公平锁
public class ReentrantLockTest {
//表示排到第几个人了
volatile static int index = 0;
//卫生间门上有个锁,人民素质高: true, 素质低: false
static ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) {
start();
}
/**
* 开始活动
*/
private static void start(){
//下面实例化来卫生间的路线,有3个路线可以过来
new Thread(() -> {
for(;;){
doing(index++);
}
}).start();
new Thread(() -> {
for(;;){
doing(index++);
}
}).start();
new Thread(() -> {
for(;;){
doing(index++);
}
}).start();
}
/**
* 上厕所这个动作
*/
private static void doing(int person){
lock.lock();
System.out.println("第 " + person + " 个人去了" + Thread.currentThread().getName() + " 坑位!");
try {
System.out.println("上卫生间中,顺便抽烟!");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第 " + person + " 个人完事了!");
System.out.println("----------------------------");
lock.unlock();
}
}
执行结果如下:
第 0 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!
第 0 个人完事了!
----------------------------
第 1 个人去了Thread-1 坑位!
上卫生间中,顺便抽烟!
第 1 个人完事了!
----------------------------
第 2 个人去了Thread-2 坑位!
上卫生间中,顺便抽烟!
第 2 个人完事了!
----------------------------
第 3 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!
第 3 个人完事了!
----------------------------
第 4 个人去了Thread-1 坑位!
上卫生间中,顺便抽烟!
第 4 个人完事了!
----------------------------
第 5 个人去了Thread-2 坑位!
上卫生间中,顺便抽烟!
从执行结果可以看出来,Thread-0、Thread-1、Thread-2交替执行,很有规律,每次都是上一个人的后面,1 2 3 4 5 有顺序的,这就是公平锁。
2.2 非公平锁
我们把实例化 ReentrantLock 的参数改为 false 来看看。
static ReentrantLock lock = new ReentrantLock(false);
执行结果如下:
第 0 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!
第 0 个人完事了!
----------------------------
第 3 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!
第 3 个人完事了!
----------------------------
第 4 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!
第 4 个人完事了!
----------------------------
第 5 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!
第 5 个人完事了!
----------------------------
第 6 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!
从执行结果可以看出,第1个人和第2个人没抢到坑位,第4、5、6个人也是 Thread-0 路线来的,所以 Thread-1 和 Thread-2 的人基本上 “憋死了”。
三、ReentrantLock 和 Synchronized 区别
不同点:
ReentrantLock | Synchronized |
---|---|
ReentrantLock 是类实现加锁 | Synchronized 是JDK内部实现,关键词加锁 |
ReentrantLock 是使用 tryLock() 尝试获取锁,避免死锁,更安全 | Synchronized 会有死锁问题 |
ReentrantLock 需要考虑异常情况,在 finally{} 中释放锁 | Synchronized不需要考虑异常情况 |
ReentrantLock 自己提供了加锁解锁功能,在特殊条件下加锁解锁需要借助 Condition对象来实现 | Synchronized需要Object提供的 wait(),notify() 方法在特殊条件下来实现加锁解锁 |
ReentrantLock锁的粒度更细,使用更方便 | Synchronized 只能在 方法上或代码块加锁 |
相同点:
- ReentrantLock 和 Synchronized 都是一个线程可以多次获取同一个锁。
- 都是同步方式加锁。
四、总结
其实到现在的 JDK8 之后 Synchronized 就做了优化,在并发了并不是特别高的情况下,和 ReentrantLock 差不多了。Synchronized 从开始时无锁状态,随着线程数量的增加变为 偏向锁,在升级为 轻量级锁,最后升级到 重量级锁。不过一般的电商项目或者广告项目,快递等都是高并发的项目,那种量级的情况下单体应用肯定是不行的了,不过在一般的场景下还是需要用 ReentrantLock 更灵活方便一点的。
五、Synchronized(面试扩展)
前面说到了 Synchronized 关键字加锁,但 Synchronized 关键字能单独作为一个 代码块 使用,也能添加到方法上使用,添加到代码块上面则就是锁住了这一段代码,那添加到方法上锁的是什么呢?
我们知道,Synchronized 同步代码块每次只有一个线程执行,做个小Demo来试试。
下面是一个静态方法 mF(),打印 5 次,开始和结尾打印 start 和 end.
public static void mF(){
System.out.println("start...");
synchronized (Object.class){
for(int i=0; i<=5; i++){
System.out.println("method F!" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("end...");
}
用两个线程调用 mF() 方法。
new Thread(() -> {
SynchronizedTest.mF();
}).start();
new Thread(() -> {
SynchronizedTest.mF();
}).start();
执行结果:
start...
start...
method F!Thread-0
method F!Thread-0
method F!Thread-0
method F!Thread-0
method F!Thread-0
method F!Thread-0
end...
method F!Thread-1
method F!Thread-1
method F!Thread-1
method F!Thread-1
method F!Thread-1
method F!Thread-1
end...
可以发现打印了两个 “start…”, Thread-0 执行完,Thread-1 才执行。
那如果是 Synchronized 关键字加载 方法上呢,我们知道,一个类可以有 静态方法 和 实例方法,那作用在这两种方法上有什么不同呢?
答案:实例锁与类锁之间互不干扰!写个例子来看看。
public synchronized static void mA(){
for(;;){
System.out.println("method A!");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static synchronized void mB(){
for(;;){
System.out.println("method B!");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void mC(){
for(;;){
System.out.println("method C!");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void mD(){
for(;;){
System.out.println("method D!");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
method A!
method D!
method C!
method C!
method D!
method A!
method D!
method A!
method C!
从执行结果上可以看出,方法 A D C 都执行了,那 B 没有执行,总结如下:
方法 A 和 B 都是静态方法,在静态方法用 Synchronized 关键字加锁,方法是同一把锁,所以只能有一个方法执行,而 方法 C 和 D 都是实例方法,和 静态方法 A 互不干涉,所以都在执行。那另个实例方法为啥能通知执行呢?因为 实例方法 上加 Synchronized 关键字加锁,锁是 this 对象,所以都会执行。