1.引子
到此,锁的基本概念、锁分类、Lock接口我们都已经认识了。那么接下来我们通过两篇内容案例演示常见锁类型,加深你对锁的理解,它们有:
- 可重入锁
- 公平锁
- 非公平锁
- 共享锁(读锁)
- 排它锁(写锁)
这一篇我们先来看:可重入锁、公平锁、非公平锁。关于共享锁与排它锁,我们将在下一篇再一起来看。在写实际案例前,你都还记得各类锁的分类含义吗?让我们一起再来回顾一下:
-
可重入锁
- 可重入锁是指一个线程获取到锁A以后,可以在释放锁A以前,多次再获取锁A,那么我们说锁A是可重入锁。简单概括:重入,等价于不释放的前提下,多次获取。
- juc包中提供的ReentrantLock就是可重入锁,我们稍后通过案例来演示
-
公平锁
- 公平锁是指在多个线程并发获取锁L的时候,需要讲究先来后到
- 如果线程A获取到了锁L,那么需要获取同一把锁L的线程B、线程C需要在等待队列中进行排队等待
- 线程A释放锁以后,根据排队顺序,线程B、线程C依次获取锁L。那么我们说:锁L是公平锁
-
非公平锁
- 非公平锁是指在多个线程并发获取锁L的时候,允许插队的现象发生
- 比如说线程A、线程B、线程C并发获取锁L,假设线程A获取到了锁L,那么线程B、线程C需要在等待队列中排队等待
- 当线程A释放锁L的瞬间,正好来了一个线程D,它也需要获取锁A
- 此时线程调度可以选择:将线程D放到等待队列中进行排队,唤醒等待队列中的线程B,让线程B获取锁L。这是公平锁的行为,不允许插队
- 另外线程调度还有另外一个选择:线程调度觉得唤醒等待队列中的线程B,有时间成本,会影响执行效率。不如直接让线程D持有锁L,这个时候我们看到,线程D后来,不经过排队直接拿到了锁L。即允许插队:这就是非公平锁的行为
以上我们一起回顾了可重入锁、公平锁、非公平锁。关于公平锁与非公平锁,我把之前的图也贴过来,方便你再次理解:
2.案例
2.1.可重入锁
案例描述:
- 我们通过构造ReentrantLock锁对象,在主线程main中多次加锁,多次释放锁
- 通过记录持有锁的次数,来观察可重入锁的特征
2.1.1.案例代码
package com.anan.edu.common.newthread.lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 可重入锁案列
*
* @author ThinkPad
* @version 1.0
* @date 2020/11/1 11:45
*/
public class MayReentrantLockDemo {
/**
* 锁对象
*/
private static ReentrantLock lock = new ReentrantLock();
/**
* 通过主线程main,多次获取同一个lock锁
* @param args
*/
public static void main(String[] args) {
// 1.未获取锁前打印
System.out.println("[" + Thread.currentThread().getName() + "]1.未获取锁前,拥有锁次数:" + lock.getHoldCount());
// 2.第一次获取锁
lock.lock();
System.out.println("[" + Thread.currentThread().getName() + "]2.第一次获取锁,拥有锁次数:" + lock.getHoldCount());
// 3.第二次获取锁
lock.lock();
System.out.println("[" + Thread.currentThread().getName() + "]3.第二次获取锁,拥有锁次数:" + lock.getHoldCount());
// 4.第三次获取锁
lock.lock();
System.out.println("[" + Thread.currentThread().getName() + "]4.第三次获取锁,拥有锁次数:" + lock.getHoldCount());
// 5.第一次释放锁
lock.unlock();
System.out.println("[" + Thread.currentThread().getName() + "]5.第一次释放锁后,拥有锁次数:" + lock.getHoldCount());
// 6.第二次释放锁
lock.unlock();
System.out.println("[" + Thread.currentThread().getName() + "]6.第二次释放锁后,拥有锁次数:" + lock.getHoldCount());
// 7.第三次释放锁
lock.unlock();
System.out.println("[" + Thread.currentThread().getName() + "]7.第三次释放锁后,拥有锁次数:" + lock.getHoldCount());
}
}
2.1.2.执行结果
D:\02teach\01soft\jdk8\bin\java com.anan.edu.common.newthread.lock.MayReentrantLockDemo
[main]1.未获取锁前,拥有锁次数:0
[main]2.第一次获取锁,拥有锁次数:1
[main]3.第二次获取锁,拥有锁次数:2
[main]4.第三次获取锁,拥有锁次数:3
[main]5.第一次释放锁后,拥有锁次数:2
[main]6.第二次释放锁后,拥有锁次数:1
[main]7.第三次释放锁后,拥有锁次数:0
Process finished with exit code 0
2.2.公平锁
案例描述:
- 我们通过构造ReentrantLock锁对象,在构造方法中传入:true,即构造一个公平锁
- 创建三个线程,编程编号依次是:Thread[0]、Thread[1]、Thread[2]
- 三个线程并行执行任务:doByFairLock()
- doByFairLock任务内部,通过加锁保证线程安全,且两次执行任务,两次任务独立加锁、释放锁
- 此时公平锁排队示例图
2.2.1.案例代码
package com.anan.edu.common.newthread.lock;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
/**
* 公平锁案例
*
* @author ThinkPad
* @version 1.0
* @date 2020/11/1 11:57
*/
public class FairLockDemo {
/**
* 锁对象
*/
private static ReentrantLock fairLock = new ReentrantLock(true);
/**
* 公平锁案例
* @param args
*/
public static void main(String[] args) {
// 创建 3 个线程,并行执行任务
Thread [] threads = new Thread[3];
for(int i = 0; i < threads.length; i++){
threads[i] = new Thread(() ->{doByFairLock();});
threads[i].start();
}
}
/**
* 演示公平锁执行机制
*/
public static void doByFairLock(){
// 1.第1次加锁,执行任务
fairLock.lock();
try{
System.out.println("线程:" + Thread.currentThread().getName() +
"正在执行第【1】次任务.");
// 随机休眠,模拟执行任务耗时
Random random = new Random();
int r = random.nextInt(10);
Thread.sleep(r * 1000);
System.out.println("线程:" + Thread.currentThread().getName() +
"执行第【1】次任务结束.耗时:" + (r * 1000) + "毫秒.");
}catch (Exception ex){
ex.printStackTrace();
}finally {
// 释放锁
fairLock.unlock();
}
// 2.第2次加锁,执行任务
fairLock.lock();
try{
System.out.println("线程:" + Thread.currentThread().getName() +
"正在执行第【2】次任务.");
// 随机休眠,模拟执行任务耗时
Random random = new Random();
int r = random.nextInt(10);
Thread.sleep(r * 1000);
System.out.println("线程:" + Thread.currentThread().getName() +
"执行第【2】次任务结束.耗时:" + (r * 1000) + "毫秒.");
}catch (Exception ex){
ex.printStackTrace();
}finally {
// 释放锁
fairLock.unlock();
}
}
}
2.2.2.执行结果
通过观察执行结果,我们发现尽管线程Thread-0、Thread-1、Thread-2线程两次执行任务,依次按照:
- 第1次执行任务:Thread-0、Thread-1、Thread-2顺序执行
- 第2次执行任务:Thread-0、Thread-1、Thread-2顺序执行
- 这就是公平锁,要讲究先来后到,排队执行
D:\02teach\01soft\jdk8\bin\java com.anan.edu.common.newthread.lock.FairLockDemo
线程:Thread-0正在执行第【1】次任务.
线程:Thread-0执行第【1】次任务结束.耗时:9000毫秒.
线程:Thread-1正在执行第【1】次任务.
线程:Thread-1执行第【1】次任务结束.耗时:5000毫秒.
线程:Thread-2正在执行第【1】次任务.
线程:Thread-2执行第【1】次任务结束.耗时:9000毫秒.
线程:Thread-0正在执行第【2】次任务.
线程:Thread-0执行第【2】次任务结束.耗时:0毫秒.
线程:Thread-1正在执行第【2】次任务.
线程:Thread-1执行第【2】次任务结束.耗时:6000毫秒.
线程:Thread-2正在执行第【2】次任务.
线程:Thread-2执行第【2】次任务结束.耗时:8000毫秒.
Process finished with exit code 0
2.3.非公平锁
案例描述:
- 我们通过构造ReentrantLock锁对象,在构造方法中传入:false,即构造一个非公平锁
- 创建三个线程,编程编号依次是:Thread[0]、Thread[1]、Thread[2]
- 三个线程并行执行任务:doByNoFairLock()
- doByNoFairLock任务内部,通过加锁保证线程安全,且两次执行任务,两次任务独立加锁、释放锁
- 此时非公平锁排队示例图
2.3.1.案例代码
友情提示,非公平锁的案例代码,与公平锁案例代码几乎一样,唯一的差异,仅是构造锁对象,构造非公平锁时,构造方法中传入了false。
package com.anan.edu.common.newthread.lock;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
/**
* 非公平锁案例
*
* @author ThinkPad
* @version 1.0
* @date 2020/11/1 11:57
*/
public class NoFairLockDemo {
/**
* 锁对象
*/
private static ReentrantLock noFairLock = new ReentrantLock(false);
/**
* 非公平锁案例
* @param args
*/
public static void main(String[] args) {
// 创建 3 个线程,并行执行任务
Thread [] threads = new Thread[3];
for(int i = 0; i < threads.length; i++){
threads[i] = new Thread(() ->{doNoByFairLock();});
threads[i].start();
}
}
/**
* 演示非公平锁执行机制
*/
public static void doNoByFairLock(){
// 1.第1次加锁,执行任务
noFairLock.lock();
try{
System.out.println("线程:" + Thread.currentThread().getName() +
"正在执行第【1】次任务.");
// 随机休眠,模拟执行任务耗时
Random random = new Random();
int r = random.nextInt(10);
Thread.sleep(r * 1000);
System.out.println("线程:" + Thread.currentThread().getName() +
"执行第【1】次任务结束.耗时:" + (r * 1000) + "毫秒.");
}catch (Exception ex){
ex.printStackTrace();
}finally {
// 释放锁
noFairLock.unlock();
}
// 2.第2次加锁,执行任务
noFairLock.lock();
try{
System.out.println("线程:" + Thread.currentThread().getName() +
"正在执行第【2】次任务.");
// 随机休眠,模拟执行任务耗时
Random random = new Random();
int r = random.nextInt(10);
Thread.sleep(r * 1000);
System.out.println("线程:" + Thread.currentThread().getName() +
"执行第【2】次任务结束.耗时:" + (r * 1000) + "毫秒.");
}catch (Exception ex){
ex.printStackTrace();
}finally {
// 释放锁
noFairLock.unlock();
}
}
}
2.3.2.执行结果
通过观察执行结果,我们发现线程Thread-0、Thread-1、Thread-2线程两次执行任务,依次按照:
- Thread-0 执行第1次任务,执行第2次任务
- Thread-1 执行第1次任务,执行第2次任务
- Thread-2 执行第1次任务,执行第2次任务
我们结合线程在等待队列中的排队情况,原本应该是:
- Thread-0第1次执行任务
- Thread-1、Thread-2线程已经在等待队列中排队
- 当Thread-0第1次执行完成任务,释放锁的瞬间,准备获取锁执行第2次任务
- 我们说这个时候,线程调度并没有将锁给到等待队列中的Thread-1,而是直接将锁再次给了Thread-0。即Thread-0因为插队,从而再次获取到了锁
- 这里我们主要模拟了插队的现象,如果你觉得不好理解,建议你将代码执行一遍,对照结果分析一下。案例都比较简单,相信你定会有所收获
D:\02teach\01soft\jdk8\bin\java com.anan.edu.common.newthread.lock.NoFairLockDemo
线程:Thread-0正在执行第【1】次任务.
线程:Thread-0执行第【1】次任务结束.耗时:7000毫秒.
线程:Thread-0正在执行第【2】次任务.
线程:Thread-0执行第【2】次任务结束.耗时:8000毫秒.
线程:Thread-1正在执行第【1】次任务.
线程:Thread-1执行第【1】次任务结束.耗时:2000毫秒.
线程:Thread-1正在执行第【2】次任务.
线程:Thread-1执行第【2】次任务结束.耗时:8000毫秒.
线程:Thread-2正在执行第【1】次任务.
线程:Thread-2执行第【1】次任务结束.耗时:7000毫秒.
线程:Thread-2正在执行第【2】次任务.
线程:Thread-2执行第【2】次任务结束.耗时:0毫秒.
Process finished with exit code 0