高级并发编程系列十(可重入锁、公平锁、非公平锁案例)

162 阅读9分钟

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。即允许插队:这就是非公平锁的行为

以上我们一起回顾了可重入锁、公平锁、非公平锁。关于公平锁与非公平锁,我把之前的图也贴过来,方便你再次理解:

image.png

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任务内部,通过加锁保证线程安全,且两次执行任务,两次任务独立加锁、释放锁
  • 此时公平锁排队示例图

image.png

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任务内部,通过加锁保证线程安全,且两次执行任务,两次任务独立加锁、释放锁
  • 此时非公平锁排队示例图

image.png

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