LockSupport秘籍:新手入门,高手精通,玩转同步控制

简介: LockSupport秘籍:新手入门,高手精通,玩转同步控制

一、LockSupport是什么

java.util.concurrent.locks.LockSupport 是 Java 并发编程中的一个非常有用的线程阻塞工具类,它包含可以阻塞和唤醒线程的方法。这个类是Java并发编程中的基础工具之一,通常用于构建锁或其他同步组件。LockSupport的所有方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。


LockSupport中的park()和unpark()方法分别用于阻塞线程和解除线程的阻塞状态。调用park()方法后,当前线程会被挂起,直到它被其他线程通过unpark()方法唤醒,或者线程被中断,或者调用该方法的线程从调用park()方法开始已经过了一个不可预知的时间。需要注意的是,LockSupport不会释放任何锁资源,因此在调用park()之前应确保当前线程没有持有任何可能导致死锁的锁。


LockSupport实际上是对线程等待/通知机制(即Object类中的wait/notify/notifyAll方法)的一种改进和扩展。与传统的等待/通知机制相比,LockSupport提供了更精确的线程阻塞和唤醒控制,以及更好的可移植性和可维护性。此外,LockSupport还可以与Java的并发锁(如ReentrantLock)和条件变量(如Condition)等高级并发工具结合使用,以实现更复杂的线程同步和协作模式。


LockSupport提供了一种挂起和恢复线程的机制,通常与线程同步原语(如锁和条件)一起使用。但是,与 Object.wait() 和 Object.notify() 或 Object.notifyAll() 相比,LockSupport 提供了一种更灵活的线程挂起和恢复方法。

二、主要方法

  1. LockSupport.park()
  • 调用此方法会导致当前线程被挂起(或阻塞),直到它被 unpark,线程被中断,或者调用该方法的线程从调用 park 方法开始已经过了一个不可预知的时间。
  • 它是一个非阻塞性的挂起操作,不会释放任何锁资源。因此,通常与 java.util.concurrent.locks 包中的锁一起使用。
  1. LockSupport.unpark(Thread thread)
  • 此方法用于解除线程的挂起状态。如果线程没有被挂起,调用此方法没有效果。
  • 只有之前通过 LockSupport.park() 挂起的线程才能被 unpark() 恢复。

三、 使用场景

  • 自定义锁实现:当你需要实现自己的锁或同步原语时,可以使用 LockSupport 来挂起和恢复线程。这比使用内置的等待/通知机制更加灵活和高效。
  • java.util.concurrent.locks 一起使用:这个包中的锁(如 ReentrantLock)和条件(如 Condition)通常与 LockSupport 一起使用来实现复杂的线程同步模式。
  • 实现响应式编程模型:在某些响应式编程场景中,线程可能需要等待某个事件或条件发生。在这种情况下,可以使用 LockSupport 来挂起线程,直到事件或条件满足。

四、LockSupport的使用

下面代码使用 LockSupport 来挂起和恢复线程:

import java.util.concurrent.locks.LockSupport;

public class LockSupportExample {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("线程开始运行...");
            // 做一些工作...
            
            System.out.println("线程挂起...");
            LockSupport.park(); // 挂起线程
            
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("线程被中断...");
            } else {
                System.out.println("线程被恢复...");
            }
            
            // 继续执行其他任务...
        });
        
        thread.start();
        // 让主线程睡眠一段时间,确保子线程已经挂起
        Thread.sleep(2000);
        System.out.println("尝试恢复线程...");
        LockSupport.unpark(thread); // 恢复线程
        // 让主线程再睡眠一段时间,确保子线程有机会执行完毕
        Thread.sleep(2000);
    }
}

五、三种方法让线程等待和唤醒

让线程等待和唤醒是并发编程中的常见需求。以下是三种常用的方法,分别是使用Object类的wait()/notify()/notifyAll()方法、使用Lock和Condition接口,以及使用LockSupport类的park()和unpark()方法。

1. 使用Object类的wait()/notify()/notifyAll()方法

这是Java中最早提供的线程等待和唤醒机制。wait()方法会使当前线程等待,直到其他线程调用同一个对象的notify()notifyAll()方法。

public class WaitNotifyExample {
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("线程1等待");
                    lock.wait(); // 线程等待
                    System.out.println("线程1被唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程2唤醒其他线程");
                lock.notify(); // 唤醒等待的线程
            }
        });

        thread1.start();
        Thread.sleep(1000); // 确保thread1先执行
        thread2.start();
    }
}

2. 使用LockCondition接口

java.util.concurrent.locks.Lock接口和它的实现类(如ReentrantLock)提供了比synchronized更灵活的锁机制。Condition接口与Lock一起使用,可以实现等待/通知模式。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockConditionExample {
    private static Lock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("线程1等待");
                condition.await(); // 线程等待
                System.out.println("线程1被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        Thread thread2 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("线程2唤醒其他线程");
                condition.signal(); // 唤醒等待的线程
            } finally {
                lock.unlock();
            }
        });

        thread1.start();
        Thread.sleep(1000); // 确保thread1先执行
        thread2.start();
    }
}

3. 使用LockSupport类的park()unpark()方法

LockSupport它可以在线程中的任何位置阻塞线程的执行。park()方法用于阻塞线程,unpark()方法用于解除线程的阻塞状态。

import java.util.concurrent.locks.LockSupport;

public class LockSupportExample {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            System.out.println("线程1开始运行");
            LockSupport.park("等待唤醒"); // 线程等待
            System.out.println("线程1被唤醒");
        });

        thread1.start();
        Thread.sleep(1000); // 确保thread1先执行并等待

        System.out.println("主线程唤醒线程1");
        LockSupport.unpark(thread1); // 唤醒线程1
    }
}

在使用LockSupport.park()时,建议检查线程的中断状态,因为park()不会响应中断,除非显式地检查并处理中断。在实际应用中,可以将park()放在一个循环中,并在循环条件中检查中断状态。

六、LockSupport源码分析

实际上 LockSupport 类的实现非常简单,并且它的很多功能都是依赖于 JVM 的底层支持的。它的核心方法是 parkunpark,分别用于阻塞和解除阻塞线程。

下面是 LockSupport 类的一些关键部分的源码分析:

public class LockSupport {
    private LockSupport() {} // Cannot be instantiated.

    // Blocks until unparked, interrupted, or the caller spuriously returns.
    public static void park() {
        Unsafe.getUnsafe().park(false, 0L);
    }

    // Blocks until unparked, interrupted, the caller spuriously returns,
    // or the deadline passes, whichever comes first.
    public static void parkNanos(long nanos) {
        if (nanos > 0)
            Unsafe.getUnsafe().park(false, nanos);
    }

    // Blocks until unparked, interrupted, the caller spuriously returns,
    // or approximately deadline milliseconds have passed, whichever comes first.
    public static void parkUntil(long deadline) {
        Unsafe.getUnsafe().park(true, deadline);
    }

    // Unblocks the thread blocked by park().
    public static void unpark(Thread thread) {
        if (thread != null)
            Unsafe.getUnsafe().unpark(thread);
    }

    // Hotspot implementation of park/unpark may need some help to find
    // threads blocked on the LockSupport objects created by this class.
    // These methods are natively implemented in the JDK.
    private static native void setBlocker(Thread t, Object arg);
    private static native Object getBlocker(Thread t);
    
    // Disables the current thread for thread scheduling purposes, for up to the
    // specified waiting time, unless the `permit` is available.
    // This method is designed to be used as a tool for creating higher-level
    // synchronization utilities, and is not intended for general use.
    // It supports a single permit, which is initially unavailable, and
    // which becomes available when `unpark` is invoked, or if the caller
    // spuriously returns.
    // ... additional methods related to permit (not shown here)
}

从上面的源码片段中,我们可以看到以下几点:

  1. LockSupport 是一个工具类,不能被实例化(构造函数是私有的)。
  2. 它提供了三个 park 方法和一个 unpark 方法。这些方法都是静态的,可以直接通过类名调用。
  3. park 方法用于阻塞当前线程,直到它被 unpark,线程被中断,或者出现其他未指定的情况(即“spurious wakeup”,虽然在现代 JVM 中这种情况很少见)。
  4. parkNanosparkUntil 方法允许设置一个时间限制,在此时间之后线程会自动解除阻塞,即使没有被其他线程显式地 unpark
  5. unpark 方法用于解除指定线程的阻塞状态。如果线程没有被阻塞,或者由于某种原因已经解除阻塞,调用 unpark 不会有任何效果。
  6. 底层的阻塞和解除阻塞操作是通过 Unsafe 类来实现的,这是一个提供低级别、非安全、操作系统级别访问的类。Unsafe 类中的 parkunpark 方法是本地方法(通过 JNI 调用),它们直接与 JVM 的线程调度器交互。
  7. setBlocker 和 getBlocker 方法是本地方法,用于在 JVM 内部跟踪由 LockSupport 阻塞的线程。这些方法通常不用于应用程序代码。

七、 注意事项

  • 当使用 LockSupport.park() 挂起线程时,应该确保有一个明确的机制来恢复(通过 unpark())或中断线程,以避免线程永久挂起。
  • 由于 LockSupport 不会释放任何锁资源,因此在调用 park() 之前应该确保线程没有持有任何可能导致死锁的锁。
  • 与 Object.wait() 和 Thread.sleep() 不同,LockSupport.park() 不会响应中断,除非你在调用 park() 之前或之后显式检查中断状态。但是,如果线程在 park() 期间被中断,那么从 park() 返回后,中断状态将被清除(设置为 false),除非你在此之前调用了 Thread.interrupted() 来检查并清除中断状态。因此,通常建议在调用 park() 的循环中检查中断状态。

八、关于 LockSupport 的一些常见面试题

面试题1:LockSupport.park()Thread.sleep() 有什么区别?

答案

LockSupport.park():它会暂停当前线程的执行,直到它被 unpark,线程被中断,或者出现虚假唤醒(spurious wakeup,但在现代JVM中很少见)。park 不需要处理 InterruptedException,并且它不会保持任何锁。

Thread.sleep():它会使当前线程休眠指定的时间,时间到了之后线程会变为就绪状态。sleep 期间线程不会释放任何锁,而且必须处理 InterruptedException。

面试题2:LockSupport 的主要用途是什么?

答案

LockSupport 主要用于创建锁和其他同步工具的基础类。它提供了一种机制,允许线程等待某个条件成立,而不需要轮询。这种等待是通过 parkunpark 方法来实现的,它们是构建高效锁和其他同步工具的关键。

面试题3:LockSupport.park() 为什么比传统的线程等待方式更高效?

答案

传统的线程等待方式通常涉及到轮询(polling)或者使用 Thread.sleep(),这些方法都会浪费CPU资源,因为它们要么不断地检查条件,要么使线程进入睡眠状态,而在条件可能变为真时不会立即唤醒。LockSupport.park() 提供了一种更有效的方式,它允许线程在条件不满足时进入无消耗等待状态,直到它被 unpark 或中断,这样可以减少CPU的占用和上下文切换的开销。

面试题4:LockSupport 是如何工作的?

答案

LockSupport 中的 park 和 unpark 方法是通过底层的 Unsafe 类来实现的,这是一个提供低级别、非安全、操作系统级别访问的类。这些方法直接与JVM的线程调度器交互,将线程置于一种特殊的等待状态,在这种状态下线程不会消耗CPU资源,直到它被 unpark 或中断。


面试题5:使用 LockSupport 时需要注意什么?

答案

使用 LockSupport 时需要注意以下几点:

  1. park 方法可能会导致线程进入无限期等待,因此需要确保有相应的机制(如中断或 unpark)来唤醒线程。
  2. LockSupport 本身不提供锁或其他同步机制,它通常与其他同步原语(如 ReentrantLock)一起使用。
  3. 由于 LockSupport 是基于底层的 Unsafe 类实现的,因此在使用时需要谨慎,避免在不适当的上下文中使用它。
  4. 在多线程编程中,正确地处理中断和 InterruptedException 非常重要,尤其是在使用 LockSupport 时。即使 park 方法本身不会抛出 InterruptedException,但在使用它构建的同步工具中可能需要处理中断。

九、结语

LockSupport 提供了 park 和 unpark 方法,用于阻塞和解除阻塞线程,是构建锁和其他同步工具的基础。与传统的 Thread.sleep() 或Thread.yield() 不同,LockSupport 不需要线程处理 InterruptedException,而且它不会保持任何锁,因此在构建高效、响应式的并发系统时特别有用。


LockSupport.park() 方法允许线程等待某个条件成立,而不会消耗CPU资源,直到它被其他线程 unpark 或被中断。这种等待方式比轮询或使用 sleep() 方法更高效,因为它减少了CPU的占用和上下文切换的开销。


但lockSupport 本身不提供锁或其他同步机制,它通常与其他同步原语(如 ReentrantLock)一起使用,以实现更复杂的同步需求。此外,在使用 LockSupport 时,需要确保有相应的机制来唤醒等待的线程,避免线程进入无限期等待。


相关文章
|
安全 Java
并发编程系列教程(完) - 终章总结
并发编程系列教程(完) - 终章总结
50 0
|
7月前
|
设计模式 算法 安全
Java多线程编程实战:从入门到精通
【4月更文挑战第30天】本文介绍了Java多线程编程的基础,包括线程概念、创建线程(继承`Thread`或实现`Runnable`)、线程生命周期。还讨论了线程同步与锁(同步代码块、`ReentrantLock`)、线程间通信(等待/通知、并发集合)以及实战技巧,如使用线程池、线程安全设计模式和避免死锁。性能优化方面,建议减少锁粒度和使用非阻塞算法。理解这些概念和技术对于编写高效、可靠的多线程程序至关重要。
|
Linux 数据库 数据安全/隐私保护
C++实战-Linux多线程(入门到精通)(三)
C++实战-Linux多线程(入门到精通)(三)
59 0
|
NoSQL Linux 调度
C++实战-Linux多线程(入门到精通)(一)
C++实战-Linux多线程(入门到精通)(一)
178 0
|
Linux 调度 C语言
C++实战-Linux多线程(入门到精通)(二)
C++实战-Linux多线程(入门到精通)(二)
44 0
|
安全 Java 调度
java多线程使用教程
java多线程使用教程
124 0
|
Java
JAVA练手小游戏——多线程知识点小练习——文字版勇者斗恶龙
JAVA练手小游戏——多线程知识点小练习——文字版勇者斗恶龙
149 0
|
存储 监控 安全
Java并发编程实战专栏(八)
Java并发编程实战(8)
82 0
Java并发编程实战专栏(八)
|
Java 编译器
java并发编程专栏(十一)
java并发编程专栏(十一)
69 0
|
存储 编译器 C++
(黑马)C++核心编程笔记(下)
(黑马)C++核心编程笔记
110 0
(黑马)C++核心编程笔记(下)