JAVA多线程系列之LockSupport

简介: 多线程中LockSupport相信大家都用过,那么底层原理大家有详细了解吗?今天就来带大家瞧瞧

基本使用

// 暂停当前线程LockSupport.park(); 
// 恢复某个线程的运行LockSupport.unpark(暂停线程对象)

特点

与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify


先Park后Unpark

packagecom.gzczy.concurrent.week3;
importlombok.extern.slf4j.Slf4j;
importjava.util.concurrent.TimeUnit;
importjava.util.concurrent.locks.LockSupport;
/*** @Description Park和Unpark* @Author chenzhengyu* @Date 2020-11-05 19:15*/@Slf4j(topic="c.ParkAndUnParkDemo")
publicclassParkAndUnParkDemo {
publicstaticvoidmain(String[] args) {
//        demo1();demo2();
    }
publicstaticvoiddemo1(){
Threadt1=newThread(() -> {
log.debug("start...");
try {
TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedExceptione) {
e.printStackTrace();
            }
log.debug("park...");
LockSupport.park();
log.debug("resume...");
        },"t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
log.debug("unpark...");
LockSupport.unpark(t1);
    }
//先Unpark再Parkpublicstaticvoiddemo2(){
Threadt1=newThread(() -> {
log.debug("start...");
try {
TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedExceptione) {
e.printStackTrace();
            }
log.debug("park...");
LockSupport.park();
log.debug("resume...");
        }, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
log.debug("unpark...");
LockSupport.unpark(t1);
    }
}

源码解析

为线程调度目的禁用当前线程,除非许可可用。

如果许可证是可用的,那么它将被消耗,调用将立即返回。否则,当前线程会因为线程调度的目的而被暂停,并处于休眠状态,直到以下三种情况之一发生:

  • 其他一些线程以当前线程为目标调用unpark
  • 其他线程打断当前线程
  • 不符合逻辑的调用

park方法不会告知你是哪一种情况的发生而导致的。调用者应该重新检查导致线程停在第一个位置的条件。调用者也可以确定,例如,线程返回时的中断状态。

/*** Disables the current thread for thread scheduling purposes unless the* permit is available.** <p>If the permit is available then it is consumed and the call returns* immediately; otherwise* the current thread becomes disabled for thread scheduling* purposes and lies dormant until one of three things happens:** <ul>* <li>Some other thread invokes {@link #unpark unpark} with the* current thread as the target; or** <li>Some other thread {@linkplain Thread#interrupt interrupts}* the current thread; or** <li>The call spuriously (that is, for no reason) returns.* </ul>** <p>This method does <em>not</em> report which of these caused the* method to return. Callers should re-check the conditions which caused* the thread to park in the first place. Callers may also determine,* for example, the interrupt status of the thread upon return.** @param blocker the synchronization object responsible for this*        thread parking* @since 1.6*/publicstaticvoidpark(Objectblocker) {
Threadt=Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
    }

unpark通过传入当前线程进行取消park

/*** Makes available the permit for the given thread, if it* was not already available.  If the thread was blocked on* {@code park} then it will unblock.  Otherwise, its next call* to {@code park} is guaranteed not to block. This operation* is not guaranteed to have any effect at all if the given* thread has not been started.** @param thread the thread to unpark, or {@code null}, in which case*        this operation has no effect*/publicstaticvoidunpark(Threadthread) {
if (thread!=null)
UNSAFE.unpark(thread);
    }

对比Wait & Notify 优势

LockSupport的主要作用就是让线程进入阻塞等待和唤醒状态

我们常见的三种线程唤醒的方法

  • 方式1:  使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程
  • 方式2:  使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
  • 方式3:  LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程


Object类中的wait和notify方法实现线程等待和唤醒

packagecom.gzczy.concurrent.heima.b.wait;
importlombok.extern.slf4j.Slf4j;
importjava.util.concurrent.TimeUnit;
/*** @Description wait notify 测试阻塞* @Author chenzhengyu* @Date 2021年01月31日 14:12:08*/@Slf4j(topic="c.WaitNotifyDemo")
publicclassWaitNotifyDemo2 {
finalstaticObjectobj=newObject();
publicstaticvoidmain(String[] args) throwsException {
newThread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedExceptione) {
e.printStackTrace();
            }
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去                } catch (InterruptedExceptione) {
e.printStackTrace();
                }
log.debug("其它代码....");
            }
        },"t1").start();
newThread(() -> {
synchronized (obj) {
log.debug("执行....");
obj.notify(); // 让线程在obj上一直等待下去log.debug("其它代码....");
            }
        },"t2").start();
    }
}

异常1:wait方法和notify方法,两个都去掉同步代码块,抛出IllegalMonitorStateException

image.png

ibary/Java/JavavintuaLyachines/jk1.8..26.k/m/in/ava

c.waitNotifyDemo-执行....

14:13:08.483[t2]

javaLangLALMontxceptioncerakoint

Exceptioninthread"t2"java.1

atjava.tang.@bjectnotifyNatieM)

internalcaii>

t8om.2c2y.m.

14:13:10.484[t1]Wii.

javaagotxceerakint

ninthread"t1"j

Exception

山兰

atjava.Lang.objectwaitNativeMethod

atjava.tang.object.waitcobicta2

tcon.gzczyor.he

internaZcali>

Processfinishedwithexitcode

异常2:将notify放在wait方法前面,程序将无法执行,无法进行唤醒

image.png

WaitNotifyDemo2

Run:

Library/Java/bavavirtuaLMachin/jk1.86on

14:12:34.336[t2]cWaitoifeo行...

cWaitotifyDemo其它代码....

14:12:34.339[t2]

$

14:12:36.334[t1]

CWaitnotifyDemo-执行....

力Q引巾

总结:wait和notify方法必须要在同步代码块中或者方法里面成对进行出现才能使用,而且必须先wait后notify之后才OK


通过Park和Unpark进行实现线程等待和唤醒

packagecom.gzczy.concurrent.heima.b.wait;
importlombok.extern.slf4j.Slf4j;
importjava.util.concurrent.TimeUnit;
importjava.util.concurrent.locks.LockSupport;
/*** @Description LockSupport Demo 进行线程阻塞* @Author chenzhengyu* @Date 2021-01-31 14:22*/@Slf4j(topic="c.LockSupportDemo")
publicclassLockSupportDemo {
publicstaticvoidmain(String[] args) throwsInterruptedException {
//默认是permit 0Threadt1=newThread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedExceptione) {
e.printStackTrace();
            }
log.debug("执行....是否打断--->"+Thread.currentThread().isInterrupted());
// 调用一次park就会消费permit 由刚刚的1变为0 你继续运行下去吧LockSupport.park();
log.debug("其它代码,是否打断--->"+Thread.currentThread().isInterrupted());
//再次打住 目前没有许可了 许可为0 那就不放你继续运行啦LockSupport.park();
log.debug("再次park,是否打断--->"+Thread.currentThread().isInterrupted());
//被打断后会发现无法park住,打断标记已经为true 线程已经被打断了LockSupport.park();
        }, "t1");
t1.start();
newThread(() -> {
log.debug("执行....");
//调用一次unpark就加1变成1,线程还在运行 没毛病老铁 你继续运行吧LockSupport.unpark(t1);
log.debug("其它代码....");
        }, "t2").start();
try {
TimeUnit.SECONDS.sleep(4);
log.debug("打断t1线程....");
t1.interrupt();
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
    }
}

运行结果

image.png

LockSupportDemo

Run:

Library/Java/JavavirtuaLMachins/dk1.8..26.k//

15:03:12.156[t2]c.Lueo行..

15:03:12.159[t2]c.Lockuppoemo它....

15:83141551].m打

口粽兄

个少

15:8314.156[1.m,否打断-->lse

15:03:16.158[main].u打断程

15:0316.1打

finishedwithexitcode

Process1

优势对比

  • 以前的等待唤醒通知机制必须synchronized里面有一个wait和notify;Lock里面有await和signal。LockSupport不用持有锁块,不用加锁,程序性能好
  • 先后顺序,不容易导致卡死,虚假唤醒。通过许可证进行授权


解析

每个线程都有自己的一个 Parker 对象(底层C代码实现),由三部分组成 _counter , _cond 和 _mutex 打个比喻

  • 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)
  • 调用 park 就是要看需不需要停下来歇息
  • 如果备用干粮耗尽,那么钻进帐篷歇息
  • 如果备用干粮充足,那么不需停留,继续前进
  • 调用 unpark,就好比令干粮充足
  • 如果这时线程还在帐篷,就唤醒让他继续前进
  • 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
  • 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮


当前线程调用UnSafe.park

image.png

Unsafepark

(1)

Parker-0

Thread-o

Thread-o

mutex

cond

(3)

(4).counter-0

counter三0

(2)


  1. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
  2. 线程进入 _cond 条件变量阻塞
  3. 设置 _counter = 0


当前线程调用UnSafe.unpark(许可为0)

image.png

Unsafe.unpark(Thread0)

Parker-0

Thread-0

Thread-o

mutex

cond

(3)

counter1

黑马程序员

witheima.com

(4)_counter-0

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 唤醒 _cond 条件变量中的 Thread_0
  3. Thread_0 恢复运行
  4. 设置 _counter 为 0

当前线程调用UnSafe.unpark(许可为1)

image.png

Unsafe.unpark(Threado

Unsafe.park

(2)

Parker-0

Thread-o

mutex

cond

(4).counter-0

counter-1

(3)


1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1

2. 当前线程调用 Unsafe.park() 方法

3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行

4. 设置 _counter 为 0

总结

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,它使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(Permit),Permit只有两个值:1和0,默认是0。我们可以把许可看成是一种信号量(Semaphore),但是与Semaphore不同的是,许可的累加上限是1


LockSupport是一个线程阻塞工具,所有的方法都是静态方法,可以让线程在任意位置进行阻塞。阻塞之后也有对应的唤醒方法,归根结底,LockSupport调用的是UnSafe里面的native代码


LockSupport提供了Park和Unpark的方法实现阻塞线程和解除线程阻塞的过程

LockSupport和每个使用它的线程都有一个许可(Permit)进行关联,Permit相当于1,0的开关,默认是0

调用一次unpark就加1变成1,调用一次park就会消费permit,也就是将1变成0的这个过程,同时将park立即返回

如果再次调用park会变成阻塞(因为Permit为0,所以会阻塞在这里,直至Permit变为1),如果这个时候调用unpark会把permit设置为1。每个线程都有一个相关的Permit,Permit最多只有1个,重复调用unpark也不会积累凭证

面试题


为什么可以先唤醒线程后阻塞线程?

答:因为UnPark获得了一个凭证,之后调用Park方法,就可以名正言顺的凭证消费,故不会阻塞


为什么唤醒两次后阻塞两次,但是最终的结果还是会阻塞线程?

答:因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark的效果是一样的,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行






相关文章
|
13天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
11天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
13天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
7天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
7天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
24 3
|
8天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
13天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
39 5
|
11天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
21 2
|
11天前
|
监控 Java 开发者
Java线程管理:守护线程与本地线程的深入剖析
在Java编程语言中,线程是程序执行的最小单元,它们可以并行执行以提高程序的效率和响应性。Java提供了两种特殊的线程类型:守护线程和本地线程。本文将深入探讨这两种线程的区别,并探讨它们在实际开发中的应用。
17 1
|
13天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
49 1