线程的基础小结

70 阅读16分钟

单例模式

什么是单例模式?

Gof23设计模式之一,单例模式是保证一个类只有一个实例

好处:

- 节约内存资源
- 便于维护
- 满足某些特定的业务需要,如:太阳类只有一个实例

怎么实现单例模式?

1) 将构造方法私有化

2) 定义静态的实例

3) 定义静态方法返回实例

单例模式分为几种?

饿汉式

定义静态实例时就创建对象

懒汉式

定义静态实例不创建对象,调用静态方法判断对象为空再创建对象

/**
 * 单例模式
 */
public class MySingleton {
​
    //静态实例
    private static MySingleton instance = null;
​
    //私有构造方法
    private MySingleton(){
        System.out.println("执行了构造方法");
    }
​
    //返回静态实例的方法
    public static MySingleton getInstance(){
        if(instance == null){ //问题所在行
            instance = new MySingleton();
        }
        return instance;
    }
​
    public static void main(String[] args) {
        //模拟多线程调用单例模式
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                MySingleton instance = MySingleton.getInstance();
                System.out.println(instance.hashCode());
            }).start();
        }
    }
}

出现问题:

多线程环境下创建多个对象

分析问题:

第18行代码判断对象为空,A线程进入,判断对象为空,准备创建对象,B线程抢占CPU进入,A线程阻塞,B线程判断为空,创建对象,A线程获得cpu后继续创建对象

多线程的调度是抢占式的,无法保证程序代码能完整执行

并发编程的三大特性

并发编程的三大特性:

  • 原子性

    线程执行的程序指令能完整的执行,全部执行,或全部不执行

  • 可见性

    对于某个数据,某个线程进行的修改,其它所有线程都可见

  • 有序性

    线程中的程序指令是按最初编写的顺序执行的

原子性

线程安全问题:多线程的调度是抢占式的,无法保证程序代码能完整执行,则会出现数据不一致的问题

解决线程安全问题:通过上锁机制

synchronized关键字

翻译过来是:同步

可以给方法或代码上锁,让线程有序的完整的执行

方法上锁(同步方法),当第一个线程执行方法时,持有锁,其它线程无法进入方法,线程执行完后,自动释放锁,其它线程才能进入

public synchronized 返回值 方法名(...){
   
}

代码上锁(同步代码块)

public 返回值 方法名(...){
   synchronized(锁对象){
       代码块....
   }
}

任何成员变量对象都可以作为锁对象

  • 可以创建新对象作为锁
  • 成员方法还可以使用this作为锁
  • 静态方法可以使用 类名.class 作为锁

同步方法如果是非静态的,默认将this作为锁,如果是静态,默认将类名.class作为锁

synchronized能保证线程安全,单会带来性能上的损失

双检锁单例模式

/**
 * DCL double check lock 双检锁单例模式
 */
public class MySingleton {
​
    //静态实例
    private static volatile MySingleton instance = null;
​
    //私有构造方法
    private MySingleton(){
        System.out.println("执行了构造方法");
    }
​
    //返回静态实例的方法
    public static MySingleton getInstance(){
        //判断当对象不为空时,不执行同步块,从而提高性能
        if(instance == null) {
            //保证判断和创建对象原子执行
            synchronized (MySingleton.class) {
                if (instance == null) {
                    instance = new MySingleton();
                }
            }
        }
        return instance;
    }
}

为什么synchronized会降低性能

synchronized属于互斥锁,一个线程持有锁时,会阻塞其它线程

线程的两个状态:

  • 用户态

    JVM能够管理的状态

  • 内核态

    JVM不能管理,由操作系统管理

上下文切换

线程在抢占资源时,发现资源上锁,线程从用户态转为内核态进行等待,线程获得锁,重新执行前会从内核态转换为用户,转换过程会降低性能,

切换的过程中需要保存或读取程序计数器的代码行数和寄存器的数据,比较消耗时间

image.png

synchronized的优化

jdk1.6对synchronized关键字进行了优化

  • 锁消除

    如果jvm发现同步方法或同步块中没有线程竞争的资源,会消除锁

    public class Demo1 {
        
        public synchronized void test(){
            System.out.println("hello world");
        }
    }
    
  • 锁膨胀

    如果jvm发现在大量循环中使用锁,会优化将锁放到循环外部

    public class Demo1 {
    ​
        public void test2(){
            for (int i = 0; i < 100; i++) {
                synchronized (this){
                    //......
                }
            }
            //jvm优化 --> 锁膨胀
    //        synchronized (this){
    //            for (int i = 0; i < 100; i++) {
    //                    //.....
    //            }
    //        }
        }
    }
    
  • 锁升级

    锁的状态:

    • 无锁

      没有任何线程竞争情况下,不会加锁

    • 偏向锁

      如果只有一个线程使用锁,锁会在对象头中记录线程的id,如果是这个线程就直接放行

    • 轻量级锁

      出现少量竞争情况下,会通过CAS乐观锁机制进行线程的调度,不会出现上下文切换,会出现自旋等待(消耗cpu)

    • 重量级锁

      出现大量竞争情况下,会转换为重量级锁(互斥锁),线程出现上下文切换

    synchronized在1.6后,上锁的过程叫锁升级: 无锁 ---> 偏向锁 ---> 轻量级锁 ---> 重量级锁

    只能升不能降级

synchronized的原理

image.png 自动上锁和释放锁实现的原理,一旦给方法或代码块加synchronized,JVM会启动Monitor监视器监控上锁的代码,线程进入后,监视器中计数器加1,其它线程进入时,监视器的计数器不为0,就不允许其它线程进入,线程执行完代码后,计数器减1,监视器再让其它线程进入

javap -c 类名

ReentantLock 类

是java.util.concurrent.lock 包提供工具类

ReentantLock 重入锁(递归锁)

重入锁: 发生方法递归情况下,持有锁的线程,可以重新持有该锁

非重入锁:方法递归的情况,持有锁的线程,不能重新持有该锁

创建方法1:

ReentantLock lock = new ReentrantLock();

创建方法2:

ReentantLock lock = new ReentrantLock(true/false);

布尔值由于指定该锁是公平或非公平锁,true公平,false非公平(默认)

公平锁: 会维护等待线程的队列,锁释放后,优先让等待时间长的线程拿到锁,降低线程的饥饿,也会降低程序的效率

非公平锁: 所有线程都去抢锁,谁抢到谁执行,有的线程会一直饥饿,效率高

使用方法:需要手动上锁和释放锁

lock.lock(); //上锁
try{
    上锁的业务代码
}finally{
    lock.unlock(); //释放锁
}

使用案例

public class LockDemo{
​
    //创建重入锁
    private ReentrantLock lock = new ReentrantLock();
​
    public void testLock(){
        //上锁
        lock.lock();
        try {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }finally {
            //释放锁
            lock.unlock();
        }
    }
​
    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                lockDemo.testLock();
            }).start();
        }
    }
}

主要方法

方法名作用
lock()上锁
unlock()释放锁
getQueuedLength()获得公平锁线程排队长度
boolean tryLock()上锁并获得上锁是否成功
boolean tryLock(long,TimeUnit)在一定时间内上锁并获得上锁是否成功
Condition newCondition()获得条件对象
boolean isFair()是否公平锁

面试题:synchronized 和 ReentrantLock的区别

1) 上锁机制不同:synchronized 是JVM自动上锁和释放锁,ReentrantLock需要代码手动上锁和释放锁

2) 锁的类型不同:synchronized 是非公平锁,ReentrantLock 可以设置公平锁或非公平锁

3) 性能不同:ReentrantLock高于synchronized

4) 功能不同:ReentrantLock提供非常丰富的方法,功能大大强于synchronized

5) 编程难度不同:synchronized 更加简单,ReentrantLock更复杂

可见性

对于共享资源,一个线程修改后,其它的线程可以看到修改后的状态

原因:CPU有多个内核,每个内核中都有独立的存储单元(寄存器、L1L2L3缓存),每个内核都执行线程,线程中的数据会从主内存中缓存到不同的内核中,线程修改一个内核中的数据,另一个内核不能及时修改

image.png

volatile关键字的作用:用于修饰变量,保证变量的可见性

被修饰的变量只保存在主内存中,所有线程都直接读写主内存,避免了可见性问题

image.png

MESI 内存一致性协议

面试题: volatile和synchronized的区别

1) synchronized能实现原子性、可见性、有序性;volatile能实现可见性和有序性

2) synchronized更加重量级,消耗更多资源;volatile更加轻量级

3) synchronized用在方法或代码块上;volatile 只能用于变量

有序性

程序指令是按编写的顺序执行的

JVM会对程序指令进行优化,可能导致程序指令重排序

做菜: 买菜、洗碗、洗菜、切菜、炒菜 ----> 买菜、切菜、洗菜、炒菜、洗碗

Object obj = new Object();

创建对象的过程:

1) 分配内存创建对象

2) 对属性初始化

3) 将内存地址赋值给引用

指令重排可能出现: 1) 3) 2) 可能将没有完成初始化的对象交给用户,导致问题

//静态实例 volatile 防止指令重排
private static volatile MySingleton instance = null;

原子类

变量的++和--

分为三个步骤:

  1. 读取原始值

  2. 计算新值

  3. 保存为新值

多线程环境下,不能完整执行,可能导致线程安全问题

public class AtomicDemo {
​
    static int count = 0;
​
    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
                count++; // 1) 读取原始值 2) 计算原始值+1 3) 保存为新值
            }).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count=" + count);
    }
}

解决方案:

1) 上锁 (效率太低)

2) 使用原子类

常用的原子类:

  • AtomicInteger 原子整数
  • AtomicLong 原子长整数
  • AtomicBoolean 原子布尔

AtomicInteger 用法

AtomicInteger count = new AtomicInteger(初始值);

自增: getAndIncrement() 或 IncrementAndGet()

自减: getAndDecrement() 或 DecrementAndGet()

原子类实现的原理:

使用乐观锁实现的

悲观锁,认为线程竞争比较激烈,会给代码上锁,线程会出现上下文切换,效率比较低

乐观锁,认为线程竞争比较少,不给代码上锁,线程不会出现上下文切换,效率高

Java中的乐观锁机制是CAS: 比较和交换 Compare And Swap

对变量进行修改时,先读取变量的原始值,要修改时,再读取变量当前内存中的值,如果当前值和原始值相同,就用新值覆盖原始值;如果当前值和原始值不同,就表示出现其它线程修改了该值,放弃修改。

CAS机制可能出现ABA问题:假设原始值是A,线程1将其改为B,线程2将其改为A,前面的线程发现值相同,以为没有线程并发问题出现

如何解决ABA问题:引入版本号机制,给变量加版本号,每次修改版本号加1,比较时判断原始值和当前值是否相同还要判断版本号是否改变

线程的等待和通知

并发编程中可以通过锁对象控制线程,如:让线程等待,通知线程执行

注意:线程的等待和通知必须由锁对象完成,否则出现线程状态异常 IllegalMonitorStateException

任何对象都可以作为锁,方法是Object类定义,等待和通知必须是同一个锁对象完成

方法名说明
wait()让当前持有锁的线程等待,直到被锁的notify唤醒(自动释放锁)
wait(long)线程等待一定时间,到时候会自动唤醒
notify()随机选择一个等待的线程唤醒
notifyAll()唤醒所有等待该锁的线程

等待和通知案例

public class WaitNotifyDemo {
​
    public synchronized static void testWait(){
        try {
            System.out.println(Thread.currentThread().getName() +"开始等待");
            //让线程等待
            WaitNotifyDemo.class.wait();
            System.out.println(Thread.currentThread().getName() +"执行完毕");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
​
    public static void main(String[] args) {
//        WaitNotifyDemo demo = new WaitNotifyDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                WaitNotifyDemo.testWait();
            }).start();
        }
        //过5s主线程通知子线程执行
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //通知需要上上锁的代码内执行
        synchronized (WaitNotifyDemo.class) {
            WaitNotifyDemo.class.notifyAll();
        }
    }
}
​

线程交替输出: A线程循环输出A,B线程循环输出B,要求两个线程交替输出ABABAB.....

public class ThreadPrintDemo {
​
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (ThreadPrintDemo.class) {
                for (int i = 0; i < 10; i++) {
                    System.out.println("A");
                    try {
                        //通知对方线程执行
                        ThreadPrintDemo.class.notify();
                        //当前线程等待
                        ThreadPrintDemo.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        new Thread(() -> {
            synchronized (ThreadPrintDemo.class) {
                for (int i = 0; i < 10; i++) {
                    System.out.println("B");
                    try {
                        //通知对方线程执行
                        ThreadPrintDemo.class.notify();
                        //当前线程等待
                        ThreadPrintDemo.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        try {
            Thread.sleep(500);
            synchronized (ThreadPrintDemo.class){
                ThreadPrintDemo.class.notifyAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

生产者消费者模式

多线程环境下有的线程用于生产数据就属于生产者,有的线程用于使用数据就属于消费者,可能会出现问题:

  • 耦合性高,生产者和消费者直接交互,相互影响,不利于代码维护
  • 速度不一致,生产者线程太快消费者来不及消费,数据过量造成资源浪费,反之消费者速度过快,消费者会一直等待

实现方法:

  • 需要定义一个缓冲区,缓冲区有临界值
  • 生产者将数据存入缓冲区,缓冲区满了,就让生产者等待,通知消费者消费
  • 消费者从缓冲区取出数据,缓冲区空了,就让消费者等待,通知生产者生产
/**
 * 包子
 */
public class Baozi {
​
    private long id;
​
    public Baozi(long id) {
        this.id = id;
    }
​
    @Override
    public String toString() {
        return "包子{" +
                "id=" + id +
                '}';
    }
}
/**
 * 包子铺
 */
public class BaoziShop {
​
    //临界值
    public static final int MAX_COUNT = 100;
    //缓冲区
    private List<Baozi> baozis = new ArrayList<>();
​
    /**
     * 做包子
     */
    public synchronized void makeBaozi(){
        //判断缓冲区是否满了
        if(baozis.size() >= MAX_COUNT){
            System.out.println("包子铺满了,师傅等待 " + Thread.currentThread().getName());
            try {
                //满了,生产者等待
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            //没有满,通知消费者来买
            this.notifyAll();
        }
        //做包子放缓冲区
        Baozi baozi = new Baozi(baozis.size());
        baozis.add(baozi);
        System.out.println("师傅" + Thread.currentThread().getName() + "做了" + baozi);
    }
​
    /**
     * 卖包子
     */
    public synchronized void sellBaozi(){
        //判断缓冲区是否空了
        if(baozis.size() == 0){
            System.out.println("包子铺空了,消费者等待 " + Thread.currentThread().getName());
            try {
                //空了,消费者等待
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            //没有空,通知师傅来做
            this.notifyAll();
        }
        //从缓冲区拿包子,卖给消费者
        if(baozis.size() > 0){
            Baozi baozi = baozis.remove(0);
            System.out.println("消费者" + Thread.currentThread().getName() + "吃了" + baozi);
        }
    }
}
public class BaoziShopDemo {
​
    public static void main(String[] args) {
        BaoziShop shop = new BaoziShop();
        //两个师傅分别做了50包子
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 50; j++) {
                    shop.makeBaozi();
                }
            }).start();
        }
        //10个消费者吃10个包子
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    shop.sellBaozi();
                }
            }).start();
        }
    }
}

JUC工具类

java.util.concurrent 并发包

JUC下提供大量的并发开发工具类

常用的有:

  • ReentrantLock 重入锁
  • 阻塞队列
  • CountDownLatch类
  • Semaphone类
  • CirrrBar... 类

ReentrantLock 重入锁

的等待和通知

使用Condition接口 ,翻译为条件

创建方法

Condition condition = Lock对象.newCondition()

下面的方法必须包含在 lock的try-finally中使用

方法名说明
await()让当前持有锁的线程等待,直到被锁的singal唤醒(自动释放锁)
await(long)线程等待一定时间,到时候会自动唤醒
singal()随机选择一个等待的线程唤醒
singalAll()唤醒所有等待该锁的线程
/**
 * 等待和通知案例
 */
public class WaitNotifyDemo2 {
​
    //创建锁对象
    private static Lock lock = new ReentrantLock();
    //获得条件对象
    private static Condition condition = lock.newCondition();
​
    public static void testWait(){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() +"开始等待");
            //让线程等待
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() +"执行完毕");
        } finally {
            lock.unlock();
        }
    }
​
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                WaitNotifyDemo2.testWait();
            }).start();
        }
        //过5s主线程通知子线程执行
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //通知需要上上锁的代码内执行
        lock.lock();
        try{
            //唤醒所有线程
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }
}

阻塞队列

BlockingQueue 是一系列特殊的集合,这种集合会有临界值,达到临界值后自动让线程等待,也会自动唤醒线程

常用实现类:

ArrayBlockingQueue 数组结构的阻塞队列

LinkedBlockingQueue 链表结构的阻塞队列

.....

创建

new ArrayBlockingQueue(临界值)

用法

方法说明
put(T)添加数据到末尾,达到临界值后自动阻塞线程,小于临界值后会自动唤醒线程
T take()从头部删除一个数据,如果空了自动阻塞线程,非空后会自动唤醒线程
int size()数据个数
/**
 * 包子铺 阻塞队列版
 */
public class BaoziShop2 {
​
    //临界值
    public static final int MAX_COUNT = 100;
    //阻塞队列
    private BlockingQueue<Baozi> baozis = new ArrayBlockingQueue<>(MAX_COUNT);
​
    /**
     * 做包子
     */
    public void makeBaozi(){
        Baozi baozi = new Baozi(baozis.size());
        try {
            //做包子放缓冲区
            baozis.put(baozi);
            System.out.println("师傅" + Thread.currentThread().getName() + "做了" + baozi);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
​
    /**
     * 卖包子
     */
    public void sellBaozi(){
        try {
            Baozi baozi = baozis.take();
            System.out.println("消费者" + Thread.currentThread().getName() + "吃了" + baozi);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

CountDownLatch 类 闭锁

作用:一个或多个线程等待其它线程工作执行完再执行自己的任务

创建:

new CountDownLatch(倒数次数)

主要方法:

方法说明
await()让当前线程等待
countDown()倒数一次,次数-1,当次数为0,自动唤醒等待的线程
int getCount()获得当前倒数次数

PS: CountDownLatch对象只能使用一次

/**
 * 倒数案例
 */
public class CountDownLatchDemo {
​
    public static void main(String[] args) {
        //创建倒数对象
        CountDownLatch countDownLatch = new CountDownLatch(3);
        //创建三个线程,分别倒数一次
        Thread thread = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "把饭吃了,准备好了!!" + countDownLatch.getCount());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //倒数一次
            countDownLatch.countDown();
        });
        Thread thread2 = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "把垃圾倒了,准备好了!!" + countDownLatch.getCount());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            countDownLatch.countDown();
        });
        Thread thread3 = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "把女朋友哄了,准备好了!!" + countDownLatch.getCount());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            countDownLatch.countDown();
        });
        thread.start();
        thread2.start();
        thread3.start();
        try {
            System.out.println("等等,我去找我兄弟去!!!");
            //阻塞当前线程
            countDownLatch.await();
            System.out.println("我兄弟都来了!!!上啊!!!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Semaphore类 信号量

信号量,可以用于控制执行任务的数量,主要用于限流(限制并发量)

创建方法:

new Semaphore(信号量大小)
new Semaphore(信号量大小,是否公平锁)

用法

方法说明
void accquired()请求信号量,信号量会减1,为0时就会阻塞
void release()释放信号量,信号量会加1,会唤醒一个阻塞的线程

限流案例

/**
 * 信号量案例
 */
public class SemaphoneDemo {
​
    public static void main(String[] args) {
        //创建信号量对象
        Semaphore semaphore = new Semaphore(10);
        //限制只有10个线程能执行
        for (int i = 0; i < 100; i++) {
            final int n = i;
            new Thread(() -> {
                //消耗一个信号量
                try {
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "消耗了信号量" + n);
            }).start();
        }
        try {
            Thread.sleep(3000);
            //释放信号量
            semaphore.release(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

CyclicBarrier类

循环栏栅,让多个线程等待一个线程,线程准备好后一起执行,类似CountDownLatch,区别是:可以重复使用

/**
 * 循环栏栅
 */
public class CyclicBarrierDemo {
​
    public static void main(String[] args) {
        //创建栏栅对象
        CyclicBarrier barrier = new CyclicBarrier(3,() -> {
            System.out.println("发令枪响了!~!!");
        });
//        for (int j = 0; j < 2; j++) {
//            //重置数量
//            barrier.reset();
            for (int i = 0; i < 3; i++) {
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() + "准备好了!!");
                    try {
                        Thread.sleep(2000);
                        //开始等待, parties减1,到0就全部唤醒执行
                        barrier.await();
                        System.out.println(Thread.currentThread().getName() + "冲啊!!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
//        }
    }
}

AQS

AbstractQueuedSynchronizer 抽象队列同步器

大量并发包中的工具类使用了AQS,基于AQS实现工具类功能的开发

使用了AQS的类:

  • CountDownLatch
  • Semaphone
  • ReentrantLock

AQS是一个抽象类,用于开发并发编程的类,包含:

1) 成员变量

    /**
     * The synchronization state.
     */
    private volatile int state; //同步器状态

对于CountDownLatch来说,state就是倒数数量

对于Semaphore来说,就是信号量数

对于ReentrantLock,是上锁的状态

2) 双向链表

保存处于等待状态的线程

对于Semaphore和ReentrantLock来说,可以保存公平锁的等待顺序