0%

  • synchronized
  • wait-notify
  • park-unpark
  • ReentrantLock

★synchronized

  • **在成员方法上加锁等于锁住当前对象 this**:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Test {
    public synchronized void test() {

    }
    }
    等价于
    class Test {
    public void test() {
    synchronized(this) {

    }
    }
    }
  • **在静态方法上加锁等于锁住类对象 clazz**:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Test {
    public synchronized static void test() {

    }
    }
    等价于
    class Test {
    public static void test() {
    synchronized(Test.class) {

    }
    }
    }

Monitor(重量级锁)

  • Monitor 被翻译为监视器或管程。

    • 每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例存储在堆中,如果使用 synchronized 给对象上锁(重量级)之后,该对象的对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁(Heavyweight Locked)。

    • Mark Word 结构:最后两位是锁标志位

      img

      64 位虚拟机 Mark Word:

      img

  • synchronized 锁的原理:

    1. 开始时 Monitor 中 Owner 为 null。

    2. 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,obj 对象的 Mark Word 指向 Monitor,把对象原有的 MarkWord 内容存入线程栈中的锁记录中(轻量级锁部分详解)。

      img

    3. 在 Thread-2 上锁后,Thread-1、Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),它们发现 obj 的 Mark Work 已经指向了一个 Monitor,并进一步发现该 Monitor 的 Owner 已经被 Thread-2 占用了,那这些线程就会进入 EntryList 等待,状态为 BLOCKED。

    4. Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 Mark Word。

    5. 唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞。

    6. WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制,后面再说)。

      img

      • 注意:
        • synchronized 必须是进入同一个对象的 Monitor 才有上述的效果
        • 不加 synchronized 的对象不会关联监视器,不遵从以上规则
  • synchronized 是可重入、不公平的重量级锁,所以可以对其进行优化。

    1
    无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁	// 随着竞争的增加,只能锁升级,不能降级

img

轻量级锁

  • 一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),可以使用轻量级锁来优化,轻量级锁对使用者是透明的(不可见)

    • 轻量级锁在没有竞争时(锁重入时),每次重入仍然需要执行 CAS 操作,Java 6 才引入的偏向锁来优化
  • 轻量级锁加锁流程:

    1. 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的 Mark Word。

      img

    2. 让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入线程栈中的锁记录

    3. 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁。

      img

      如果 CAS 失败,有两种情况:

      • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程。
      • 如果是线程自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数。

      img

    4. 当退出 synchronized 代码块(解锁时):

      • 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1。
      • 如果锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
        • 成功,则解锁成功
        • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀(轻量级锁->重量级锁)
  • 在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

    1. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁。

      img

    2. Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,通过 Object 对象头获取到持锁线程,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED。

      img

    3. 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。

自旋锁(重量级锁竞争时)

  • 重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用自旋(默认 10 次)来进行优化,采用循环的方式去尝试获取锁。如果当前线程自旋成功,即此时持锁线程已经退出了同步块,释放了锁,这时当前线程就可以避免阻塞。

    注意:

    • 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势。
    • 自旋失败的线程会进入阻塞状态。

    优点:不会进入阻塞状态,减少线程上下文切换的消耗

    缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源。

  • 自旋锁情况:

    • 自旋成功的情况:

      img

    • 自旋失败的情况:

      img

    注意:

    • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能
    • Java 7 之后不能控制是否开启自旋功能,由 JVM 控制

偏向锁

  • 偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作

    • 当锁对象第一次被线程获得的时候进入偏向状态,标记为 101,同时使用 CAS 操作将线程 ID 记录到 Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作

      img

    • 当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定或轻量级锁状态

  • 一个对象创建时:

    • 如果开启了偏向锁(默认开启),那么对象创建后,MarkWord 值为 0x05 即最后 3 位为 101,thread、epoch、age 都为 0

      如果没有开启偏向锁,那么对象创建后,mark word 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为0,第一次用到 hashcode 时才会赋值

      • 当一个对象已经计算过 hashCode,就再也无法进入偏向状态了,从对象头的格式可以看出来,如果计算过 hashCode,那 54 位的线程 ID 已经存不下了,所以该对象不能再用偏向锁了。
    • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低

    • 添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

撤销偏向锁
  • 撤销偏向锁的状态:
    • 调用对象的 hashCode:偏向锁的对象 MarkWord 中存储的是线程 id,调用 hashCode 将导致偏向锁被撤销,偏向锁升级为轻量级锁
    • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
    • 调用 wait/notify,需要申请 Monitor,进入 WaitSet,偏向锁升级为重量级锁
  • 批量撤销:如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
    • 批量重偏向:当撤销偏向锁阈值超过 20 次后,JVM 会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程
    • 批量撤销:当撤销偏向锁阈值超过 40 次后,JVM 会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

锁消除

  • 锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM 即时编译器的优化
    • 锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 逃逸分析)

wait-notify

基本使用

  • 需要获取对象锁后才可以调用 锁对象.wait(),notify 随机唤醒一个线程,notifyAll 唤醒所有线程去竞争 CPU。

    • 说明:wait 是挂起线程,需要唤醒的都是挂起操作,阻塞线程可以自己去争抢锁,挂起的线程需要唤醒后再去争抢锁。

    Object 类 API:

    1
    2
    3
    4
    public final void notify():唤醒正在等待对象监视器的单个线程。
    public final void notifyAll():唤醒正在等待对象监视器的所有线程。
    public final void wait():导致当前线程等待,直到另一个线程调用该对象的 notify() 方法或 notifyAll()方法。
    public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待,或是被唤醒
    • 它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法,也就是说线程必须成为 Monitor 的 Owner 之后,上面的方法调用才有效
  • 对比 sleep():

    • 原理不同:sleep() 方法是属于 Thread 类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait() 方法属于 Object 类,用于线程间通信
    • 锁的处理机制不同:调用 sleep() 方法的过程中,线程不会释放对象锁,当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池(不释放锁其他线程怎么抢占到锁执行唤醒操作),但是都会释放 CPU
    • 使用区域不同:wait() 方法必须放在同步控制方法和同步代码块(先获取锁)中使用,即需要和 synchronized 一起使用,而 sleep() 方法可以放在任何地方使用

底层原理

  • Owner 线程发现当前继续向下执行的条件不满足,则调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态

  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片

    • BLOCKED 线程会在 Owner 线程释放锁时唤醒
    • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,唤醒后并不意味者立刻获得锁,需要进入 EntryList 重新竞争

    img

虚假唤醒

  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程

    • 解决方法:采用 notifyAll

    • notifyAll 仅解决某个线程的唤醒问题,使用 if + wait 判断仅有一次机会,一旦条件不成立,无法重新判断

      • 解决方法:用 while + wait,当条件不成立,再次 wait

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        private static final Object lock = new Object();
        synchronized(lock) {
        while (条件不成立) {
        lock.wait();
        }
        // 向下执行代码...
        }

        // 另一个线程
        synchronized(lock) {
        lock.notifyAll();
        }

park-unpark

基本使用

  • LockSupport 是用来创建锁和其他同步类的线程原语

    • LockSupport 类方法:
      • LockSupport.park():暂停当前线程,挂起原语
      • LockSupport.unpark(暂停的线程对象):恢复某个线程的运行

    LockSupport 出现就是为了增强 wait & notify 的功能:

    • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park、unpark 不需要
    • park & unpark 以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程
    • park & unpark 可以先 unpark,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费
    • wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放 CPU

底层原理

  • 先 park 再 unpark:

    1. 当前线程调用 Unsafe.park() 方法
    2. 检查 _counter ,本情况为 0,这时获得 _mutex 互斥锁
    3. 线程进入 _cond 条件变量挂起
    4. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
    5. 唤醒 _cond 条件变量中的 Thread_0,Thread_0 恢复运行,设置 _counter 为 0

    img

  • 先 unpark 再 park:

    1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
    2. 当前线程调用 Unsafe.park() 方法
    3. 检查 _counter ,本情况为 1,这时线程无需挂起,继续运行,设置 _counter 为 0

    img

线程状态转换

  • 假设有线程 t

情况 1 NEW —> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW —> RUNNABLE

情况 2 RUNNABLE —> WAITING

  • t 线程用 synchronized(obj) 获取了对象锁后
    • 调用 obj.wait() 方法时,t 线程从 RUNNABLE —> WAITING
    • 调用 obj.notify()obj.notifyAll()t.interrupt()
      • 竞争锁成功,t 线程从 WAITING —> RANNABLE
      • 竞争锁失败,t 线程从 WAITING —> BLOCKED

情况 3 RUNNABLE —> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE —> WAITING
    • 注意是当前线程在 t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING —> RUNNABLE

情况 4 RUNNABLE —> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE —> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程的 interrupt(),会让目标线程从 WAITING —> RUNNABLE

情况 5 RUNNABLE —> TIMED_WAITING

  • t 线程用 synchronized(obj) 获取了对象锁后
    • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE —> TIMED_WAITING
    • t 线程等待时间超过了 n 毫秒,或者调用 obj.notify()obj.notifyAll()t.interrupt()
      • 竞争锁成功,t 线程从 TIMED_WAITING —> RANNABLE
      • 竞争锁失败,t 线程从 TIMED_WAITING —> BLOCKED

情况 6 RUNNABLE —> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE —> TIMED_WAITING
    • 注意是当前线程在 t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或者 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING —> RUNNABLE

情况 7 RUNNABLE —> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n),当前线程从 RUNNABLE —> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING —> RUNNABLE

情况 8 RUNNABLE —> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos)LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE —> TIMED_WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程的 interrupt(),或是等待超时,会让目标线程从 TIMED_WAITING —> RUNNABLE

情况 9 RUNNABLE —> BLOCKED

  • t 线程用 synchronized(obj) 获取对象锁时如果竞争失败,则从 RUNNABLE —> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,则从 BLOCKED —> RUNNABLE,其它失败的线程仍然 BLOCKED

情况 10 RUNNABLE —> TERMINATED

  • 当前线程所有代码运行完毕,进入TERMINATED

★ReentrantLock

  • ReentrantLock 相对于 synchronized 具备如下特点:
    1. 锁的实现:synchronized 是 JVM 实现的,使用关键字 synchronized 来使用。而 ReentrantLock 是 JDK 实现的,要创建 ReentrantLock 对象来使用。
    2. 性能:新版本 Java 对 synchronized 进行了很多优化,synchronized 与 ReentrantLock 大致相同
    3. 使用:ReentrantLock 需要手动解锁,synchronized 执行完代码块自动解锁
    4. 可中断:ReentrantLock 可中断,而 synchronized 不行
    5. 公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
      • ReentrantLock 可以设置公平锁,synchronized 中的锁是非公平的
      • 不公平锁的含义是阻塞队列内公平,队列外非公平
    6. 锁超时:尝试获取锁,超时获取不到直接放弃,不进入阻塞队列
      • ReentrantLock 可以设置超时时间,synchronized 会一直等待
    7. 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象,更细粒度的唤醒线程
    8. 两者都是可重入锁

基本使用

  • 构造方法:ReentrantLock lock = new ReentrantLock();

    ReentrantLock 类 API:

    • public void lock():获得锁
      • 如果锁没有被另一个线程占用,则将锁定计数设置为 1
      • 如果当前线程已经保持锁定,则保持计数增加 1
      • 如果锁被另一个线程保持,则当前线程被禁用线程调度,并且在锁定已被获取之前处于休眠状态
    • public void unlock():尝试释放锁
      • 如果当前线程是该锁的持有者,则保持计数递减
      • 如果保持计数现在为零,则锁定被释放
      • 如果当前线程不是该锁的持有者,则抛出异常
    1
    2
    3
    4
    5
    6
    7
    8
    9
    ReentrantLock reentrantLock = new ReentrantLock();
    try {
    // 获取锁
    reentrantLock.lock();
    // 临界区
    } finally {
    // 释放锁
    reentrantLock.unlock();
    }

可重入

  • 可重入是指同一个线程如果首次获得了这把锁,那么它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住,直接造成死锁。
  • 用 lock 方法加两把锁会是什么情况呢?
    • 加锁两次解锁两次:正常执行
    • 加锁两次解锁一次:程序直接卡死,线程不能出来,也就说明申请几把锁,最后需要解除几把锁
    • 加锁一次解锁两次:运行程序会直接报错

可打断

  • public void lockInterruptibly():获得可打断的锁

    • 如果没有锁竞争,此方法就会获取 lock 对象锁
    • 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 打断
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public static void main(String[] args) throws InterruptedException {    
    ReentrantLock lock = new ReentrantLock();
    Thread t1 = new Thread(() -> {
    try {
    System.out.println("尝试获取锁");
    lock.lockInterruptibly();
    } catch (InterruptedException e) {
    System.out.println("没有获取到锁,被打断,直接返回");
    return;
    }
    try {
    System.out.println("获取到锁");
    } finally {
    lock.unlock();
    }
    }, "t1");
    lock.lock();
    t1.start();
    Thread.sleep(2000);
    System.out.println("主线程进行打断锁");
    t1.interrupt();
    }
    • 注意:如果是不可中断模式 lock.lock(),那么即使使用了 interrupt 也不会让等待状态中的线程中断

锁超时

  • public boolean tryLock():尝试获取锁,获取到返回 true,获取不到直接放弃,不进入阻塞队列

    public boolean tryLock(long timeout, TimeUnit unit):在给定时间内获取锁,获取不到就退出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();
    Thread t1 = new Thread(() -> {
    try {
    if (!lock.tryLock(2, TimeUnit.SECONDS)) {
    System.out.println("获取不到锁");
    return;
    }
    } catch (InterruptedException e) {
    System.out.println("被打断,获取不到锁");
    return;
    }
    try {
    log.debug("获取到锁");
    } finally {
    lock.unlock();
    }
    }, "t1");
    lock.lock();
    System.out.println("主线程获取到锁");
    t1.start();

    Thread.sleep(1000);
    try {
    System.out.println("主线程释放了锁");
    } finally {
    lock.unlock();
    }
    }
    • 注意:tryLock 期间也可以被打断

公平锁

  • 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁

    • ReentrantLock 可以设置公平锁,synchronized 中的锁是非公平的
    • 不公平锁的含义是阻塞队列内公平,队列外非公平
  • 构造方法:ReentrantLock lock = new ReentrantLock(true)

    1
    2
    3
    public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    }

    ReentrantLock 默认是不公平的

    1
    2
    3
    public ReentrantLock() {
    sync = new NonfairSync();
    }

    说明:公平锁一般没有必要,会降低并发度。

条件变量

  • synchronized 的条件变量,是当条件不满足时进入 WaitSet 等待;ReentrantLock 的条件变量比 synchronized 强大之处在于支持多个条件变量。

  • ReentrantLock 类获取 Condition 对象:public Condition newCondition()

    Condition 类 API:

    • void await():当前线程从运行状态进入等待状态,释放锁
    • void signal():唤醒一个等待在 Condition 上的线程,但是必须获得与该 Condition 相关的锁

    使用流程:

    • await / signal 前需要获得锁
    • await 执行后,会释放锁进入 ConditionObject 等待
    • await 的线程被唤醒(或打断,或超时)去重新竞争 lock 锁
    • 线程在条件队列被打断会抛出中断异常
    • 竞争 lock 锁成功后,从 await 后继续执行
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public static void main(String[] args) throws InterruptedException {    
    ReentrantLock lock = new ReentrantLock();
    //创建一个新的条件变量
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    new Thread(() -> {
    try {
    lock.lock();
    System.out.println("进入等待");
    //进入休息室等待
    condition1.await();
    System.out.println("被唤醒了");
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    lock.unlock();
    }
    }).start();
    Thread.sleep(1000);
    //叫醒
    new Thread(() -> {
    try {
    lock.lock();
    //唤醒
    condition1.signal();
    // condition2.signal(); // 用condition2是不能唤醒的,因为之前的线程是在condition1中等待
    } finally {
    lock.unlock();
    }
    }).start();
    }

---------------The End---------------