JUC从实战到源码:LockSupport

104 阅读13分钟

JUC从实战到源码:LockSupport

😄生命不息,写作不止

🔥 继续踏上学习之路,学之分享笔记

👊 总有一天我也能像各位大佬一样

🏆 博客首页   @怒放吧德德  To记录领地 @一个有梦有戏的人

🌝分享学习心得,欢迎指正,大家一起学习成长!

转发请携带作者信息  @怒放吧德德(掘金) @一个有梦有戏的人(CSDN)

LockSupport-title.png

引言

在多线程编程中,线程间的协调是一个复杂而又至关重要的话题。Java提供了多种机制来实现线程间的同步和通信,以确保数据的一致性和线程的安全执行。在这些机制中,LockSupport类提供了一种高效且灵活的方式来控制线程的阻塞和唤醒。本文将深入探讨LockSupport类及其与Java中其他线程控制机制的对比,包括传统的wait和notify方法以及基于Condition的await和signal方法。我们将通过代码示例和分析,详细解释这些机制的工作原理和使用场景,以及它们之间的差异和最佳实践。

LockSupport的基本概念

LockSupport 是 Java 中的一个工具类,位于 java.util.concurrent.locks 包中。它提供了一些低级别的线程阻塞和唤醒功能,主要用于实现其他高层并发结构,如锁和信号量。LockSupport 通过使用许可(permits)来控制线程的状态,使得开发者能够在自定义的锁或线程管理中灵活使用。例如,LockSupport.park() 方法可以使当前线程阻塞,而 LockSupport.unpark(Thread thread) 则可以唤醒一个被阻塞的线程。这种机制比传统的 waitnotify 方法更高效且更易于实现。

简单来说他就是用于创建和其他同步类的基本线程阻塞原语。

线程阻塞与唤醒机制

接下来我们用三个方式来实现对线程的阻塞与唤醒。

wait与notify方法

wait()notify() 方法是 Java 中用于线程间通信的重要机制,主要在对象监视器(也称为锁)中使用。这些方法允许线程在特定条件下等待和唤醒,从而实现更复杂的多线程交互。

首先我们通过API文档来了解这两个方法,这两个是Object类下的两个方法。

wait

public final void wait() throws InterruptedException

这个方法会导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法。

当前线程必须拥有这个对象的监视器。线程释放该监视器的所有权,并等待,直到另一个线程通过调用notify方法或notifyAll方法通知等待该对象监视器的线程唤醒。然后,线程等待,直到它可以重新获得监视器的所有权并继续执行。

notify

public final void notify()

唤醒正在等待对象监视器的单个线程。 如果任何线程正在等待这个对象,其中一个被选择被唤醒。 选择是任意的,并且由实施的判断发生。 线程通过调用wait方法之一等待对象的监视器。

唤醒的线程将无法继续,直到当前线程放弃此对象上的锁定为止。 唤醒的线程将以通常的方式与任何其他线程竞争,这些线程可能正在积极地竞争在该对象上进行同步; 例如,唤醒的线程在下一个锁定该对象的线程中没有可靠的权限或缺点。

该方法只能由作为该对象的监视器的所有者的线程调用。

线程以三种方式之一成为对象监视器的所有者:

  • 通过执行该对象的同步实例方法。
  • 通过执行在对象上synchronized语句的正文。
  • 对于类型为Class的对象,通过执行该类的同步静态方法。
案例

现在通过一个案例来学习,以下有两个线程,因为需要同一个监视器中执行,所以这里需要通过synchronized进行包裹。

public class LockWn {
    public static void main(String[] args) {
        // 同一把锁
        Object lockKey = new Object();
        new Thread(() -> {
            synchronized (lockKey) {
                System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");
                try {
                    // 阻塞
                    lockKey.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒...");
            }
        }, "T1").start();
        new Thread(() -> {
            // 线程T2释放
            synchronized (lockKey) {
                lockKey.notify();
                System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");
            }
        }, "T2").start();
    }
}

以上代码执行后就能够看到线程T1阻塞之后,线程T2发起通知去释放。

我们在以上代码加了synchronized,那么如果不加synchronized会怎样?

我们不妨自己动手试一下,将synchronized去掉试一下。

结果是会抛出java.lang.IllegalMonitorStateException异常。这个API文档中,就说过了**只能由拥有此对象监视器的线程调用,如果当前线程不是对象监视器的所有者则会抛出IllegalMonitorStateException**

我们也可以看一下代码上的注释。

这个案例是先执行了wait再去执行notify,那么如果换一下顺序效果将会是怎样的呢?

public class LockWn2 {
    public static void main(String[] args) {
        // 同一把锁
        Object lockKey = new Object();
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (lockKey) {
                System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");
                try {
                    // 阻塞
                    lockKey.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒...");
            }
        }, "T1").start();
        new Thread(() -> {
            // 线程T2释放
            synchronized (lockKey) {
                lockKey.notify();
                System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");
            }
        }, "T2").start();
    }
}

我们通过这个案例,在T1获取锁执行wait之前调用休眠1s,这是为了保证T2线程先执行。也就是限制性了notify,再去执行wait。这时候我们看一下输出结果。

我们可以看到,程序已经执行了T2中的notify,之后也执行了wait方法,但是最后结果确实程序还在消耗,T1依旧被阻塞住,并没有释放。这显然我们可以得出,将notify卸载wait之前,是不会导致被唤起的。

如果我们在LockWn2#main代码后面加上以下代码

// 如果在尾部加上T3来执行notify
try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}
new Thread(() -> {
    // 线程T3释放
    synchronized (lockKey) {
        lockKey.notify();
        System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");
    }
}, "T3").start();

最终T1是能够被唤醒的。

注:wait和notify都要放在synchronized代码块中,wait要先执行才执行notify才会有效果。

await与signal实现

第二种实现方式采用Condition接口中的await和signal方法实现线程的等待唤醒。

Condition接口是Java并发包java.util.concurrent.locks中的一部分,它为线程同步提供了更灵活的机制,类似于传统的Object类中的wait()、notify()和notifyAll()方法。Condition结合Lock接口使用,以实现复杂的线程间协调,主要依赖await()和signal()方法。

await

await()的作用类似于Object.wait(),当一个线程调用await()时:

  • 该线程会被挂起(阻塞),并且释放当前持有的锁。
  • 线程进入等待队列,直到被其他线程通过signal()signalAll()唤醒,或者被中断。

InterruptedException - 如果当前线程被中断(并且支持线程中断的中断)

signal

signal()的作用类似于Object.notify(),它用来唤醒等待该Condition的某个线程:

  • 只能唤醒一个等待的线程。
  • 被唤醒的线程仍需要重新获取锁,才能从await()后继续执行。

当调用此方法时,实现可能(通常是)要求当前线程保存与此Condition的锁。 执行必须记录此前提条件,如果不保持锁定,则采取任何措施。 通常情况下,一个异常如IllegalMonitorStateException将被抛出。

案例

我们还是一样通过案例来认识一下await与signal,我们是首先需要一把锁Lock lock = new ReentrantLock();,然后整体与Object.wait与Object.notify是差不多的。

public class LockAs {
    public static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(() -> {
            lock.lock();
            System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");
            try {
                condition.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
            System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒...");
        }, "T1").start();

        Thread.sleep(1000);

        new Thread(() -> {
            lock.lock();
            try {
                condition.signal();
            } finally {
                lock.unlock();
            }
            System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");
        }, "T2").start();
    }
}

从结果可以看出与Object.wait与Object.notify的结果是一样的。

我们还是老办法,先将锁给注释掉,结果如下

会抛出IllegalMonitorStateException异常,因为这两个方法都是要求当前线程保存与此Condition的锁。

接下来,我们还是把await与signal倒序执行。

public class LockAs2 {
    public static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            lock.lock();
            System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");
            try {
                condition.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
            System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒...");
        }, "T1").start();

        new Thread(() -> {
            lock.lock();
            try {
                condition.signal();
            } finally {
                lock.unlock();
            }
            System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");
        }, "T2").start();
    }
}

结果还是和wait与notify方法是一样,会出现T2通知唤起T1,但是T1却一直没有唤醒,线程一直阻塞状态。

park与unpark方法

接下来就到使用park与unpark两个方法。首先,我们要先了解一下LockSupport,他是用于创建锁和其他同步类的基本线程阻塞原语。 这个类与每个使用它的线程相关联,一个许可证(在Semaphore类的意义上)。 如果许可证可用,则呼叫park将park返回,在此过程中消耗它; 否则可能会阻止。 如果没提供许可证,则致电unpark获得许可。 (*与信号量不同,许可证不能累积,最多只有一个

方法park和unpark提供了阻止和解除阻塞线程的有效手段,该方法不会遇到导致不推荐使用的方法Thread.suspend和Thread.resume目的不能使用的问题:一个线程调用park和另一个线程之间的尝试unpark线程将保持活跃性,由于许可证。 另外,如果调用者的线程被中断, park将返回,并且支持超时版本。 park方法也可以在任何其他时间返回,因为“无理由”,因此一般必须在返回之前重新检查条件的循环中被调用。 在这个意义上, park作为一个“忙碌等待”的优化,不浪费时间旋转,但必须与unpark配对才能有效。

park

park()方法用于阻塞当前线程,禁用当前线程进行线程调度,直到指定的等待时间,除非许可证可用。也就是直到被其他线程调用unpark()方法唤醒。

使用park()时,线程将被放入一个等待状态,释放 CPU,直到以下条件之一发生:

  • 当前线程被调用unpark()唤醒。
  • 当前线程被中断。
  • 当前线程由于其他原因被唤醒(例如 JVM 的实现细节)。

通过代码可以看到,实际上是使用了UNSAFE的方法。

public static void park() {
    UNSAFE.park(false, 0L);
}
unpark

unpark(Thread thread)方法用于唤醒指定的线程。如果该线程正在等待(被调用了park()),则unpark()将使其继续执行。如果该线程没有被阻塞,unpark()会将其标记为可以运行,确保线程在下次调度时能够执行。(许可证不会累积!

public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}
案例

使用LockSupport.park();先将当前线程阻塞,再通过另一个线程执行LockSupport.unpark(t1);来将提供许可证,是的t1线程能够继续使用。我们从以下代码可以看出,使用LockSupport是不用再使用同步锁的。

public class LockPark {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");
            LockSupport.park();
            System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒...");
        }, "t1");
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(() -> {
            LockSupport.unpark(t1);
            System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");
        }, "t2");
        t2.start();
    }
}

从结果可以看出,线程的阻塞与唤醒能够正常达到目的。

接下来我们还是与上文一样,做了一些反例。我们将unpark与park顺序做一个对换。

public class LockPark2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");
            LockSupport.park();
            System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒...");
        }, "t1");
        t1.start();

        Thread t2 = new Thread(() -> {
            LockSupport.unpark(t1);
            System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");
        }, "t2");
        t2.start();
    }
}

通过运行结果我们可以得知,其效果是一致的。

但是,如果我们多次park呢?假如在一个线程中,我们park了两次,并且执行了多次unpark,那会是怎么样的结果呢?

public class LockPark2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");
            LockSupport.park();
            System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒1...");
            LockSupport.park();
            System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒2...");
        }, "t1");
        t1.start();
        Thread t2 = new Thread(() -> {
            LockSupport.unpark(t1);
            LockSupport.unpark(t1);
            System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");
        }, "t2");
        t2.start();
        Thread t3 = new Thread(() -> {
            LockSupport.unpark(t1);
            System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");
        }, "t3");
        t3.start();
    }
}

如上面代码,我们是先执行了unpark,先颁布许可证,我们在t2线程执行了2次unpark,在t3线程中执行了1次,我们运行一下。

我们可以看到,第一个park解开了,第二个并没有,也就是说明了许可证只会有一个,并不会进行积累。

转发请携带作者信息  @怒放吧德德(掘金) @一个有梦有戏的人(CSDN)

与其他线程控制机制的对比

这样我们可以得到以下的区别

wait与notify方法

  • 需要锁
  • 顺序不能颠倒(不能先唤醒后等待)

await与signal实现

  • 需要锁
  • 顺序不能颠倒(不能先唤醒后等待)

park与unpark方法

  • 不需要锁
  • 也不一定需要顺序执行(先唤醒后等待也能正常执行)
  • 许可证不会累计,最多只有一个

那么为什么可以调到顺序呢?

因为unpark是提供了一个许可证,等调用park的时候得到了凭证,就直接释放了,就不会阻塞。

为什么唤醒两次在阻塞两次却不会唤醒呢?

因为许可证最多只能有一个,连续调用两次的许可证是不会进行累计。

结论

通过对LockSupport类及其提供的park和unpark方法的深入分析,我们可以看到,这些方法为线程控制提供了一种无需锁的阻塞和唤醒机制。与wait和notify方法以及await和signal方法相比,park和unpark方法具有更高的灵活性和效率。它们不需要依赖于锁对象,也不受顺序执行的限制,使得它们在某些场景下更为适合。然而,需要注意的是,park和unpark方法中的许可证不会累积,这意味着即使多次调用unpark,也只有一个许可证可用,这与其他机制的行为有所不同。

总的来说,选择合适的线程控制机制需要根据具体的应用场景和需求来决定。LockSupport类提供了一种强大的工具,可以帮助开发者实现更高效和灵活的线程同步,但同时也要求开发者对线程的生命周期和状态有更深入的理解。通过本文的学习和实践,希望读者能够更好地掌握这些线程控制机制,并在实际开发中做出恰当的选择。


转发请携带作者信息  @怒放吧德德 @一个有梦有戏的人
持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。转载请携带链接,转载到微信公众号请勿选择原创,谢谢!
👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍
谢谢支持!