乐趣区

关于java:并发王者课铂金1探本溯源为何说Lock接口是Java中锁的基础

欢送来到《并发王者课》,本文是该系列文章中的 第 14 篇

黄金 系列中,咱们介绍了并发中一些问题,比方死锁、活锁、线程饥饿等问题。在并发编程中,这些问题无疑都是须要解决的。所以,在铂金系列文章中,咱们会从并发中的问题登程,摸索 Java 所提供的 锁的能力 以及它们是如何解决这些问题的。

作为铂金系列文章的第一篇,咱们将从 Lock 接口 开始介绍,因为它是 Java 中锁的根底,也是并发能力的根底。

一、了解 Java 中锁的根底:Lock 接口

在青铜系列文章中,咱们介绍了通过 synchronized 关键字实现对办法和代码块加锁的用法。然而,尽管 synchronized 十分好用、易用,然而它的灵便度却非常无限,不能灵便地管制加锁和开释锁的机会。所以,为了更灵便地应用锁,并满足更多的场景须要,就须要咱们可能自主地定义锁。于是,就有了Lock 接口

了解 Lock 最直观的形式,莫过于间接在 JDK 所提供的并发工具类中找到它,如下图所示:

能够看到,Lock 接口提供了一些能力 API,并有一些具体的实现,如 ReentrantLock、ReentrantReadWriteLock 等。

1. Lock 的五个外围能力 API

  • void lock():获取锁。如果以后锁不可用,则会被阻塞直至锁开释
  • void lockInterruptibly():获取锁并容许被中断。这个办法和 lock() 相似,不同的是,它容许被中断并抛出中断异样
  • boolean tryLock():尝试获取锁。会立刻返回后果,而不会被阻塞
  • boolean tryLock(long timeout, TimeUnit timeUnit):尝试获取锁并期待一段时间。这个办法和 tryLock(),然而它会依据参数期待–会, 如果在规定的工夫内未能获取到锁就会放弃
  • void unlock():开释锁。

2. Lock 的常见实现

在 Java 并发工具类中,Lock 接口有一些实现,比方:

  • ReentrantLock:可重入锁;
  • ReentrantReadWriteLock:可重入读写锁;

除了列举的两个实现外,还有一些其余实现类。对于这些实现,暂且不用具体理解,前面会具体介绍。在目前阶段,你须要了解的是 Lock 是它们的根底

二、自定义 Lock

接下来,咱们基于后面的示例代码,看看如何将 synchronized 版本的锁用 Lock 来实现。

 public static class WildMonster {
   private boolean isWildMonsterBeenKilled;
   
   public synchronized void killWildMonster() {String playerName = Thread.currentThread().getName();
     if (isWildMonsterBeenKilled) {System.out.println(playerName + "未斩杀野怪失败...");
       return;
     }
     isWildMonsterBeenKilled = true;
     System.out.println(playerName + "斩获野怪!");
   }
 }

1. 实现一把简略的锁

创立类 WildMonsterLock 并实现 Lock 接口,WildMonsterLock 将是取代 synchronized 的要害:

// 自定义锁
public class WildMonsterLock implements Lock {
    private boolean isLocked = false;

    // 实现 lock 办法
    public void lock() {synchronized (this) {while (isLocked) {
                try {wait();
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
            isLocked = true;
        }
    }
    
    // 实现 unlock 办法
    public void unlock() {synchronized (this) {
            isLocked = false;
            this.notify();}
    }
}

在实现 Lock 接口时,你须要实现它上述的所有办法。不过,为了简化代码不便展现,咱们移除了 WildMonsterLock 类中的 tryLock 等办法。

对于 waitnotify办法的时候,如果你不相熟的话,能够查看青铜系列的文章。这里须要揭示的是,notify在应用时务必要和 wait 是同一个监视器

基于方才定义的 WildMonsterLock,创立 WildMonster 类,并在办法 killWildMonster 中应用 WildMonsterLock 对象,从而取代 synchronized.

// 应用方才自定义的锁
 public static class WildMonster {
   private boolean isWildMonsterBeenKilled;

   public void killWildMonster() {
     // 创立锁对象
     Lock lock = new WildMonsterLock(); 
     // 获取锁
     lock.lock(); 
     try {String playerName = Thread.currentThread().getName();
       if (isWildMonsterBeenKilled) {System.out.println(playerName + "未斩杀野怪失败...");
         return;
       }
       isWildMonsterBeenKilled = true;
       System.out.println(playerName + "斩获野怪!");
     } finally {
       // 执行完结后,无论如何不要遗记开释锁
       lock.unlock();}
   }
}

输入后果如下:

哪吒斩获野怪!典韦未斩杀野怪失败...
兰陵王未斩杀野怪失败...
铠未斩杀野怪失败...

Process finished with exit code 0

从后果中能够看到:只有哪吒一人斩获了野怪,其余几个英雄均以失败告终,后果合乎预期 。这阐明,WildMonsterLock 达到了和synchronized 统一的成果。

不过,这里有细节须要留神 。在应用synchronized 时咱们无需关怀锁的开释,JVM 会帮忙咱们主动实现。然而,在应用自定义的锁时,肯定要应用 try...finally 来确保锁最终肯定会被开释,否则将造成后续线程被阻塞的严重后果。

2. 实现可重入的锁

synchronized 中,锁是能够重入的 所谓锁的可重入,指的是锁能够被线程反复或递归调用。比方,加锁对象中存在多个加锁办法时,当线程在获取到锁进入其中任一办法后,线程应该能够同时进入其余的加锁办法,而不会呈现被阻塞的状况。当然,前提条件是这个加锁的办法用的是同一个对象的锁(监视器)。

在上面这段代码中,办法 A 和 B 都是同步办法,并且 A 中调用 B. 那么,线程在调用 A 时曾经取得了以后对象的锁,那么线程在 A 中调用 B 时能够间接调用,这就是锁的可重入性。


public class WildMonster {public synchronized void A() {B();
    }
    
    public synchronized void B() {doSomething...}
}

所以,为了让咱们自定义的 WildMonsterLock 也反对可重入,咱们须要对代码进行一点改变。

public class WildMonsterLock implements Lock {
    private boolean isLocked = false;
   
    // 重点:减少字段保留以后取得锁的线程
    private Thread lockedBy = null;
    // 重点:减少字段记录上锁次数
    private int lockedCount = 0;

    public void lock() {synchronized (this) {Thread callingThread = Thread.currentThread();
            // 重点:判断是否为以后线程
            while (isLocked && lockedBy != callingThread) {
                try {wait();
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
            isLocked = true;
            lockedBy = callingThread;
            lockedCount++;
        }
    }

    public void unlock() {synchronized (this) {
            // 重点:判断是否为以后线程
            if (Thread.currentThread() == this.lockedBy) {
                lockedCount--;
                if (lockedCount == 0) {
                    isLocked = false;
                    this.notify();}
            }
        }
    }
}

在新的 WildMonsterLock 中,咱们减少了 this.lockedBylockedCount字段,并在加锁和解锁时减少对线程的判断。在加锁时,如果以后线程曾经取得锁,那么将不用进入期待。而在解锁时,只有以后线程能解锁

lockedCount字段则是为了保障解锁的次数和加锁的次数是匹配的,比方加锁了 3 次,那么相应的也要 3 次解锁。

3. 关注锁的公平性

在黄金系列文章中,咱们提到了线程在竞争中可能被饿死,因为竞争并不是偏心的。所以,咱们在自定义锁的时候,也该当思考锁的公平性

三、小结

以上就是对于 Lock 的全部内容。在本文中,咱们介绍了 Lock 是 Java 中各类锁的根底。它是一个接口,提供了一些能力 API,并有着残缺的实现。并且,咱们也能够依据须要自定义实现锁的逻辑。所以,在学习 Java 中各种锁的时候,最好先从 Lock 接口开始。同时,在代替 synchronized 的过程中,咱们也能感触到 Lock 有一些 synchronized 所不具备的劣势:

  • synchronized 用于办法体或代码块,而 Lock 能够灵便应用,甚至能够逾越办法
  • synchronized 没有公平性,任何线程都能够获取并长期持有,从而可能饿死其余线程。而基于 Lock 接口,咱们能够实现偏心锁,从而防止一些线程活跃性问题
  • synchronized 被阻塞时只有期待,而 Lock 则提供了 tryLock 办法,能够疾速试错,并能够设定工夫限度,应用时更加灵便
  • synchronized 不能够被中断,而 Lock 提供了 lockInterruptibly 办法,能够实现中断

另外,在自定义锁的时候,要思考锁的公平性。而在应用锁的时候,则须要思考锁的平安开释。

夫子的试炼

  • 基于 Lock 接口,自定义实现一把锁。

延长浏览与参考资料

  • Locks in Java
  • 《并发王者课》纲要与更新进度总览

对于作者

关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶然也聊聊生存和现实。不贩卖焦虑,不做题目党。

如果本文对你有帮忙,欢送 点赞 关注 监督 ,咱们一起 从青铜到王者

退出移动版