Java Review - 并发编程_LockSupport

简介: Java Review - 并发编程_LockSupport

195d03d17afc4a928bc581f313b01dfe.png

概述


位于rt.jar包的java.util.concurrent.locks目录中, 主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。

LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的。


主要方法


void park()


如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.park()时会马上返回,否则调用线程会被禁止参与线程的调度,也就是会被阻塞挂起。

    public static void main(String[] args) {
        System.out.println("begin park");
        LockSupport.park();
        System.out.println("end park");
    }

97938c0e0e4042c89a3196f18cbaf372.png

直接在main函数里面调用park方法,最终只会输出begin park!,然后当前线程被挂起,这是因为在默认情况下调用线程是不持有许可证的。


在其他线程调用 unpark(Thread thread)方法并且将当前线程作为参数时,调用park方法而被阻塞的线程会返回。


另外,如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者线程被虚假唤醒,则阻塞线程也会返回。


所以在调用park方法时最好也使用循环条件判断方式。


需要注意的是,因调用park()方法而被阻塞的线程被其他线程中断而返回时并不会抛出InterruptedException异常。


void unpark(Thread thread)


当一个线程调用unpark时,如果参数thread线程没有持有thread与LockSupport类关联的许可证,则让thread线程持有。


如果thread之前因调用park()而被挂起,则调用unpark后,该线程会被唤醒。如果thread之前没有调用park,则调用unpark方法后,再调用park方法,其会立刻返回。修改代码如下。

   public static void main(String[] args) {
        System.out.println("begin park");
        LockSupport.unpark(Thread.currentThread());
        LockSupport.park();
        System.out.println("end park");
    }

83ccf8a3e0724922a9c7d5b7a629e176.png


小示例 park() & unpark(Thread thread)

 public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            System.out.println("child thread begin to park");
            LockSupport.park();
            System.out.println("child thread unpark");
        });
        // 启动子线程
        thread.start();
        // 主线程休眠1s
        TimeUnit.SECONDS.sleep(1);
        // 调用unpark方法让thread持有许可证,然后park方法返回
        System.out.println("main thread begin unpark ");
        LockSupport.unpark(thread);
    }


610c317864b146239b8721aad0884fd5.png


首先创建了一个子线程thread,子线程启动后调用park方法,由于在默认情况下子线程没有持有许可证,因而它会把自己挂起。


主线程休眠1s是为了让主线程调用unpark方法前让子线程输出child thread begin park!并阻塞


主线程然后执行unpark方法,参数为子线程,这样做的目的是让子线程持有许可证,然后子线程调用的park方法就返回了。


park方法返回时不会告诉你因何种原因返回,所以调用者需要根据之前调用park方法的原因,再次检查条件是否满足,如果不满足则还需要再次调用park方法。


例如,根据调用前后中断状态的对比就可以判断是不是因为被中断才返回的。


为了说明调用park方法后的线程被中断后会返回,我们修改上面的例子代码,删除LockSupport.unpark(thread);,然后添加thread.interrupt();,

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
 * @author 小工匠
 * @version 1.0
 * @description: TODO
 * @date 2021/12/5 8:53
 * @mark: show me the code , change the world
 */
public class LockSupportDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            System.out.println("child thread begin to park");
            // 调用park方法,挂起自己,只有被中断才推出循环
            while(!Thread.currentThread().isInterrupted()){
                LockSupport.park();
            }
            System.out.println("child thread unpark");
        });
        // 启动子线程
        thread.start();
        // 主线程休眠1s
        TimeUnit.SECONDS.sleep(1);
        // 中断子线程
        thread.interrupt();
    }
}

在如上代码中,只有中断子线程,子线程才会运行结束,如果子线程不被中断,即使你调用unpark(thread)方法子线程也不会结束。


void parkNanos(long nanos)

和park方法类似,如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.parkNanos(long nanos)方法后会马上返回。该方法的不同在于,如果没有拿到许可证,则调用线程会被挂起nanos时间后修改为自动返回。


另外park方法还支持带有blocker参数的方法void park(Object blocker)方法,当线程在没有持有许可证的情况下调用park方法而被阻塞挂起时,这个blocker对象会被记录到该线程内部。


使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用getBlocker(Thread)方法来获取blocker对象的,所以JDK推荐我们使用带有blocker参数的park方法,并且blocker被设置为this,这样当在打印线程堆栈排查问题时就能知道是哪个类被阻塞了。


public class TestPark {
    public static void main(String[] args) {
        TestPark testPark = new TestPark();
        testPark.parkTest();
    }
    private void parkTest() {
        // 调用park 挂起自己
        LockSupport.park();
    }
}

运行代码后,使用jstack pid命令查看线程堆栈时可以看到如下输出结果。


66560640bb2f4e10be38c0333ca8f233.png

7188227c31364a0a93813542d2064c5b.png

748b5240cfcf404e8fd44e441356add7.png

使用带blocker参数的park方法,线程堆栈可以提供更多有关阻塞对象的信息。


park(Object blocker)

来看下源码

public static void park(Object blocker) {
    // 当前线程
        Thread t = Thread.currentThread();
        // 设置线程的blocker变量
        setBlocker(t, blocker);
        // 挂起线程
        UNSAFE.park(false, 0L);
        // 线程被激活后,清除blocker变量,因为一般都是在线程阻塞的时候才分析原因
        setBlocker(t, null);
    }


Thread类里面有个变量volatile Object parkBlocker,用来存放park方法传递的blocker对象,也就是把blocker变量存放到了调用park方法的线程的成员变量里面。

dd000d3cee894a1db5f58987040cf2eb.png


void parkNanos(Object blocker, long nanos)

相比park(Object blocker) 方法多了个超时时间。


void parkUntil(Object blocker, long deadline)


    public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
    }

其中参数deadline的时间单位为ms,该时间是从1970年到现在某一个时间点的毫秒值。这个方法和parkNanos(Object blocker, long nanos)方法的区别是,后者是从当前算等待nanos秒时间,而前者是指定一个时间点,比如需要等到2021.12.05日 12:00:00,则把这个时间点转换为从1970年到这个时间点的总毫秒数。


示例

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
/**
 * @author 小工匠
 * @version 1.0
 * @description: TODO
 * @date 2021/12/5 9:42
 * @mark: show me the code , change the world
 */
public class FIFOMutex {
    private final AtomicBoolean locked = new AtomicBoolean(false);
    private final Queue<Thread> waiters = new ConcurrentLinkedQueue<Thread>();
    public void lock(){
        boolean wasInterrupted = false ;
        // 当期线程
        Thread current = Thread.currentThread();
        waiters.add(current);
        // 1 只有队首的线程可以获取锁
        while(waiters.peek() != current || !locked.compareAndSet(false,true)){
                LockSupport.park(this);
                if (Thread.interrupted()) { // 2
                    wasInterrupted = true;
                }
        }
        waiters.remove();
        if (wasInterrupted){ // 3
            current.interrupt();
        }
    }
    public void unlock(){
        locked.set(false);
        LockSupport.unpark(waiters.peek());
    }
}


这是一个先进先出的锁,也就是只有队列的首元素可以获取锁。


在代码(1)处,如果当前线程不是队首或者当前锁已经被其他线程获取,则调用park方法挂起自己。


然后在代码(2)处判断,如果park方法是因为被中断而返回,则忽略中断,并且重置中断标志,做个标记,然后再次判断当前线程是不是队首元素或者当前锁是否已经被其他线程获取,如果是则继续调用park方法挂起自己。


然后在代码(3)中,判断标记,如果标记为true则中断该线程,这个怎么理解呢?其实就是其他线程中断了该线程,虽然我对中断信号不感兴趣,忽略它,但是不代表其他线程对该标志不感兴趣,所以要恢复下。


相关文章
|
12月前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
161 0
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
839 6
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
387 1
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
188 2
|
Java 编译器 开发者
深入理解Java内存模型(JMM)及其对并发编程的影响
【9月更文挑战第37天】在Java的世界里,内存模型是隐藏在代码背后的守护者,它默默地协调着多线程环境下的数据一致性和可见性问题。本文将揭开Java内存模型的神秘面纱,带领读者探索其对并发编程实践的深远影响。通过深入浅出的方式,我们将了解内存模型的基本概念、工作原理以及如何在实际开发中正确应用这些知识,确保程序的正确性和高效性。
|
Java 开发者
深入探索Java中的并发编程
本文将带你领略Java并发编程的奥秘,揭示其背后的原理与实践。通过深入浅出的解释和实例,我们将探讨Java内存模型、线程间通信以及常见并发工具的使用方法。无论是初学者还是有一定经验的开发者,都能从中获得启发和实用的技巧。让我们一起开启这场并发编程的奇妙之旅吧!
117 5
|
算法 安全 Java
Java中的并发编程是如何实现的?
Java中的并发编程是通过多线程机制实现的。Java提供了多种工具和框架来支持并发编程。
98 1
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
安全 Java 测试技术
掌握Java的并发编程:解锁高效代码的秘密
在Java的世界里,并发编程就像是一场精妙的舞蹈,需要精准的步伐和和谐的节奏。本文将带你走进Java并发的世界,从基础概念到高级技巧,一步步揭示如何编写高效、稳定的并发代码。让我们一起探索线程池的奥秘、同步机制的智慧,以及避免常见陷阱的策略。
|
C# 开发者 数据处理
WPF开发者必备秘籍:深度解析数据网格最佳实践,轻松玩转数据展示与编辑大揭秘!
【8月更文挑战第31天】数据网格控件是WPF应用程序中展示和编辑数据的关键组件,提供排序、筛选等功能,显著提升用户体验。本文探讨WPF中数据网格的最佳实践,通过DevExpress DataGrid示例介绍其集成方法,包括添加引用、定义数据模型及XAML配置。通过遵循数据绑定、性能优化、自定义列等最佳实践,可大幅提升数据处理效率和用户体验。
225 0