目录
  • 正文
  • 应用场景
  • 公平和非公平锁代码演示
  • 执行流程分析
    • 公平锁执行流程
    • 非公平锁执行流程
  • 优缺点分析
    • 总结

      前言:

      从公平的角度来说,Java 中的锁总共可分为两类:公平锁和非公平锁。但公平锁和非公平锁有哪些区别?孰优孰劣呢?在 Java 中的应用场景又有哪些呢?接下来我们一起来看。

      正文

      公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。
      非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。

       举个例子:公平锁就像开车经过收费站一样,所有的车都会排队等待通过,先来的车先通过,

      如下图所示:

      ​​​​​​​Java公平锁和非公平锁的区别

      通过收费站的顺序也是先来先到,分别是张三、李四、王五,这种情况就是公平锁。 而非公平锁相当于,来了一个强行加塞的老司机,它不会准守排队规则,来了之后就会试图强行加塞,如果加塞成功就顺利通过,当然也有可能加塞失败,如果失败就乖乖去后面排队,这种情况就是非公平锁。 

      ​​​​​​​Java公平锁和非公平锁的区别

      应用场景

      在 Java 语言中,锁 synchronized 和 ReentrantLock 默认都是非公平锁,当然我们在创建 ReentrantLock 时,可以手动指定其为公平锁,但 synchronized 只能为非公平锁

       ReentrantLock 默认为非公平锁可以在它的源码实现中得到验证,如下源码所示: 

      ​​​​​​​Java公平锁和非公平锁的区别

       当使用 new ReentrantLock(true) 时,可以创建公平锁,如下源码所示: 

      ​​​​​​​Java公平锁和非公平锁的区别

      公平和非公平锁代码演示

      接下来我们使用 ReentrantLock 来演示一下公平锁和非公平锁的执行差异,首先定义一个公平锁,开启 3 个线程,每个线程执行两次加锁和释放锁并打印线程名的操作,

      如下代码所示:

      import java.util.concurrent.locks.Lock;
      import java.util.concurrent.locks.ReentrantLock;
      
      public class ReentrantLockFairTest {
          static Lock lock = new ReentrantLock(true);
          public static void main(String[] args) throws InterruptedException {
              for (int i = 0; i < 3; i++) {
                  new Thread(() -> {
                      for (int j = 0; j < 2; j++) {
                          lock.lock();
                          System.out.println("当前线程:" + Thread.currentThread()
                                  .getName());
                          lock.unlock();
                      }
                  }).start();
              }
          }
      }

      以上程序的执行结果如下图所示: 

      ​​​​​​​Java公平锁和非公平锁的区别

       接下来我们使用非公平锁来执行上面的代码,具体实现如下:

      import java.util.concurrent.locks.Lock;
      import java.util.concurrent.locks.ReentrantLock;
      
      public class ReentrantLockFairTest {
          static Lock lock = new ReentrantLock();
          public static void main(String[] args) throws InterruptedException {
              for (int i = 0; i < 3; i++) {
                  new Thread(() -> {
                      for (int j = 0; j < 2; j++) {
                          lock.lock();
                          System.out.println("当前线程:" + Thread.currentThread()
                                  .getName());
                          lock.unlock();
                      }
                  }).start();
              }
          }
      }

      以上程序的执行结果如下图所示: 

      ​​​​​​​Java公平锁和非公平锁的区别

      从上述结果可以看出,使用公平锁线程获取锁的顺序是:A -> B -> C -> A -> B -> C,也就是按顺序获取锁。而非公平锁,获取锁的顺序是 A -> A -> B -> B -> C -> C,原因是所有线程都争抢锁时,因为当前执行线程处于活跃状态,其他线程属于等待状态(还需要被唤醒),所以当前线程总是会先获取到锁,所以最终获取锁的顺序是:A -> A -> B -> B -> C -> C。

      执行流程分析

      公平锁执行流程

      获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。

      非公平锁执行流程

      当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。

      公平锁和非公平锁的性能测试结果如下:

      ​​​​​​​Java公平锁和非公平锁的区别

       从上述结果可以看出,使用非公平锁的吞吐率(单位时间内成功获取锁的平均速率)要比公平锁高很多。

      优缺点分析

      公平锁的优点是按序平均分配锁资源,不会出现线程饿死的情况,它的缺点是按序唤醒线程的开销大,执行性能不高。 非公平锁的优点是执行效率高,谁先获取到锁,锁就属于谁,不会“按资排辈”以及顺序唤醒,但缺点是资源分配随机性强,可能会出现线程饿死的情况。

      总结

      在 Java 语言中,锁的默认实现都是非公平锁,原因是非公平锁的效率更高,使用 ReentrantLock 可以手动指定其为公平锁。非公平锁注重的是性能,而公平锁注重的是锁资源的平均分配,所以我们要选择合适的场景来应用二者。

      声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。