LockSupport详解

# LockSupport详解

# 1、LockSupport简介

LockSupport 是 Java 并发包(java.util.concurrent)中的一个工具类,提供了基本的线程阻塞和唤醒机制。它主要用于实现锁和其他同步类,如AQS、 ReentrantLock、Semaphore、CountDownLatch 等。

LockSupport 提供的主要方法是 parkunpark,它们分别用于挂起和唤醒线程(LockSupport.park()会让线程进入WATING状态)。 AQS中对线程进行挂起和唤醒操作最终使用的就是LockSupport.park(xxx);LockSupport.unpark(xxx)。 AQS相关内容可以参考 我的上一篇博客 AQS详解 (opens new window)

# LockSupport 类的构造方法

// 构造方法是私有的,意味着LockSupport类不能被实例化。
// 这个设计表明LockSupport类只是一个工具类,提供静态方法供外部调用。
private LockSupport() {}

# LockSupport 类的属性

// 这个静态常量持有一个Unsafe类的实例。Unsafe类提供了一些底层操作的能力。
// 如直接内存访问、CAS操作等。由于其强大的功能和潜在的危险性,Unsafe类的使用受到严格限制,通常只在JDK内部使用。
private static final sun.misc.Unsafe UNSAFE;

//parkBlockerOffset常量保存了Thread类中parkBlocker字段的内存偏移量。parkBlocker字段用于记录调用park方法时的阻塞对象。
private static final long parkBlockerOffset;
// 这些静态常量分别保存了Thread类中
// threadLocalRandomSeed
// threadLocalRandomProbe
// threadLocalRandomSecondarySeed字段的内存偏移量。
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;

static {
    try {
        // 获取 Unsafe 实例
        UNSAFE = sun.misc.Unsafe.getUnsafe();

        // 获取 Thread 类的相关字段的偏移量
        Class<?> tk = Thread.class;
        
        // 获取 parkBlocker 字段的偏移量
        parkBlockerOffset = UNSAFE.objectFieldOffset(tk.getDeclaredField("parkBlocker"));
        
        // 获取 threadLocalRandomSeed 字段的偏移量
        SEED = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomSeed"));
        
        // 获取 threadLocalRandomProbe 字段的偏移量
        PROBE = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomProbe"));
        
        // 获取 threadLocalRandomSecondarySeed 字段的偏移量
        SECONDARY = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
    } catch (Exception ex) {
        throw new Error(ex);
    }
}

总结:
LockSupport类提供了线程阻塞和唤醒的基础设施,通过使用Unsafe类进行底层内存操作来实现。其核心是利用park和unpark方法来管理线程的状态,确保并发编程中的高效和安全。
通过获取Thread类中关键字段的偏移量,LockSupport类能够直接操作这些字段,实现对线程状态的控制。 关于sun.misc.Unsafe UNSAFE类,后续再另写一篇博客分析。

# Thread类的parkBlocker属性

volatile Object parkBlocker;

Thread类的parkBlocker属性是一个volatile的Object类型变量,用于记录线程在调用LockSupport.park(Object blocker)时被阻塞的原因或对象。通过parkBlocker,我们可以在调试或分析时更容易地了解线程的阻塞原因。这种设计有助于提高并发编程的可调试性和可维护性。

比如有一个锁对象,线程在获取锁时被阻塞,可以通过parkBlocker记录这个锁对象,以便在调试或分析时知道线程因为什么原因被阻塞。

为啥这样设计呢?
因为在JDK1.5的时候 LockSupport 没设计Thread 类的parkBlocker来记录阻塞信息,这导致分析线程变得困难。所以1.6加入了这个特性。 此外虽然 synchronized 本身不支持像 parkBlocker 这样的灵活机制但是在 JVM 内部,synchronized 会记录阻塞线程的锁对象,这也有利于调试。

下面的park(Object blocker)方法就是这么用的。

# LockSupport 类的常用方法

# 挂起线程的相关方法

①、park(Object blocker)方法:
挂起当前线程,并设置阻塞对象。阻塞对象通常用于调试或监视线程状态。挂起结束后清除阻塞对象。

public static void park(Object blocker) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 设置阻塞对象
    setBlocker(t, blocker);
    // 挂起当前线程
    UNSAFE.park(false, 0L);
    // 注意: (这里因为上面的  UNSAFE.park(false, 0L) 会让线程挂起)  
    // 当线程被唤醒的时候必须要清除唤醒线程的blocker对象 或者下面直接跟了setBlocker(t, null); 
    // 这就保证了当线程被唤醒之后能够确保清除唤醒线程的blocker对象
    
    // 挂起结束后清除阻塞对象 
    setBlocker(t, null);
}

// 通过 UNSAFE.putObject 方法将阻塞对象 arg 设置到线程 t 中的 parkBlocker 字段中。
private static void setBlocker(Thread t, Object arg) {
    UNSAFE.putObject(t, parkBlockerOffset, arg);
}

// park 方法是一个本地方法,具体实现依赖于底层操作系统
public native void park(boolean isAbsolute, long time);

②、parkNanos(Object blocker, long nanos)方法:
挂起当前线程指定的纳秒数,并设置阻塞对象。挂起结束后清除阻塞对象。

public static void parkNanos(Object blocker, long nanos) {
    if (nanos > 0) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 设置阻塞对象
        setBlocker(t, blocker);
        // 挂起当前线程指定的纳秒数
        UNSAFE.park(false, nanos);
        // 挂起结束后清除阻塞对象
        setBlocker(t, null);
    }
}

③、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);
}

④、park()方法:
挂起当前线程,直到其他线程调用unpark或线程被中断。

public static void park() {
    // 挂起当前线程
    UNSAFE.park(false, 0L);
}

⑤、parkNanos(long nanos)方法:
挂起当前线程指定的纳秒数。

public static void parkNanos(long nanos) {
    if (nanos > 0) {
        // 挂起当前线程指定的纳秒数
        UNSAFE.park(false, nanos);
    }
}

⑥、parkUntil(long deadline)方法:
挂起当前线程直到指定的时间。

public static void parkUntil(long deadline) {
    // 挂起当前线程直到指定的时间
    UNSAFE.park(true, deadline);
}

# 唤醒线程的相关方法

unpark(Thread thread)方法:
unpark方法用于唤醒被park方法挂起的线程。

public static void unpark(Thread thread) {
    // 检查传入的线程是否为 null
    if (thread != null) {
        // 使用 UNSAFE 类的 unpark 方法唤醒指定的线程
        UNSAFE.unpark(thread);
    }
}

# unpark(Thread thread)方法注意点

这个方法上注释有一句话: This operation is not guaranteed to have any effect at all if the given thread has not been started. 意思是:如果给定的线程尚未启动(也就是线程调用start方法之前的状态),则无法保证此操作有效。

比如对一个处于NEW状态的线程 调用unpark,再让线程start,当线程处于RUNNABLE状态后再调用park方法,那么这个线程可能会被挂起。也就是 unpark方法不一定生效。

这里先留个印象,下面第2节 分析 LockSupport原理 还会提到。

看下面的例子:

import java.util.concurrent.locks.LockSupport;

public class TestA {

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // ③、t1此时的状态是 RUNNABLE ,再 park t1线程
            LockSupport.park();  // 可能会阻塞  因为 unpark方法不保证 对未启动的线程生效
            System.out.println("123");
        }, "t1");

        // ①、先 unpark  NEW状态的 线程 t1
        LockSupport.unpark(t1);
        // ②、启动 t1线程
        t1.start();

    }
}

上面代码, t1线程尚未启动(也就是线程调用start方法之前的状态) LockSupport.unpark(t1) 未生效。

# LockSupport使用示例

利用LockSupport,实现线程间通信,交替打印 牛 马 人

import java.util.concurrent.locks.LockSupport;

public class TestA {
    // t1负责打印牛
    // t2负责打印马
    // t3负责打印人
    private static Thread t1, t2, t3;

    // 控制打印顺序的状态变量 0表示执行t1 唤醒t2
    // 控制打印顺序的状态变量 1表示执行t2 唤醒t3
    // 控制打印顺序的状态变量 2表示执行t3 唤醒t1
    private static volatile int state = 0;

    public static void main(String[] args) {
        t1 = new Thread(() -> {
            for (int i = 0; i < 3; ++i) {
                while (state != 0) {
                    LockSupport.park(); // 挂起 t1 线程,直到被唤醒
                }
                System.out.print("牛 ");
                state = 1; // 设置为 1,表示下一个打印马
                LockSupport.unpark(t2); // 唤醒 t2
            }
        });

        t2 = new Thread(() -> {
            for (int i = 0; i < 3; ++i) {
                while (state != 1) {
                    LockSupport.park(); // 挂起 t2 线程,直到被唤醒
                }
                System.out.print("马 ");
                state = 2; // 设置为 2,表示下一个打印人
                LockSupport.unpark(t3); // 唤醒 t3
            }
        });

        t3 = new Thread(() -> {
            for (int i = 0; i < 3; ++i) {
                while (state != 2) {
                    LockSupport.park(); // 挂起 t3 线程,直到被唤醒
                }
                System.out.print("人 ");
                state = 0; // 设置为 0,表示下一个打印牛
                LockSupport.unpark(t1); // 唤醒 t1
            }
        });

        t1.start();
        t2.start();
        t3.start();

    }
}

运行结果:

牛 马 人 牛 马 人 牛 马 人 

# 判断park的条件建议使用while而不是if

LockSupport源码中注释也是这么建议的:

while (!canProceed()) { ... LockSupport.park(this); }

为什么这么用呢?
如果使用 if 条件,线程被唤醒后只检查一次条件。如果是条件不满足的唤醒,但线程已经继续执行,这会导致错误的行为。 而while 循环在每次被唤醒时都会重新检查条件。如果条件仍然不满足,线程会继续等待。这确保了线程在条件未满足时不会继续执行。

比如上面打印牛马人示例中:


new Thread(() -> {
            for (int i = 0; i < 3; ++i) {
                while (state != 2) {  // 这里的while 换成两次if也可以
                    LockSupport.park(); // 挂起 t3 线程,直到被唤醒
                }
                System.out.print("人 ");
                state = 0; // 设置为 0,表示下一个打印牛
                LockSupport.unpark(t1); // 唤醒 t1
            }
});

// 上代码可以用下面代码替换   因为条件比较简单 
new Thread(() -> {
            for (int i = 0; i < 3; ++i) {
                if (state != 2) {
                    LockSupport.park(); // 挂起 t3 线程,直到被唤醒
                }

                if (state == 2) {
                    System.out.print("人 ");
                    state = 0; // 设置为 0,表示下一个打印牛
                    LockSupport.unpark(t1); // 唤醒 t1
                }else {
                    LockSupport.park();
                }
            }
        });

明显可以看出,使用while更加简洁明了。 实际上即使是很简单的state != 2 的条件用if看着也很臃肿了,如果条件比较复杂就更不推荐多个if检查条件了,会让代码变得臃肿且难以理解和维护。

# 引出Guarded Suspension模式

在《图解Java多线程设计模式》这本书中第82页就有介绍这个模式。下面引用一下原书的内容。

Guarded是被守护、被保卫、被保护的意思Suspension则是“暂停”的意思。如果执行现在的处理会造成问题,就让执行处理的线程进行等待--这就是 Guarded Suspension 模式。

当你正在家换衣服时,门铃突然响了,原来是邮递员来送邮件了。这时,因为正在换衣服出不去,所以只能先喊道“请稍等一下”,让邮递员在门口稍等一会儿。换好衣服后,才说着“让您久等了”并打开门。

这个例子很日式呀,让您久等了~ 因为这本书作者就是日本人 。 非常推荐大家看看这本书《图解Java多线程设计模式》 。

再拿上面打印牛马人的例子说明一下问题,其中利用 while来判断park条件 就是为了保证线程只打印该打印的东西,不该打印的时候就挂起。

# 总结

LockSupport类提供了多种挂起线程的方法,主要是通过Unsafe类的park方法实现。挂起线程的方法分为带阻塞对象的和不带阻塞对象的,带阻塞对象的方法在挂起线程时会记录阻塞的原因或对象,以便调试和监视。每种方法还支持不同的挂起时长,包括无限期挂起、挂起指定的纳秒数和挂起直到某个时间点。通过这些方法,LockSupport类为高级并发控制提供了基础设施。比如AQS以及依赖AQS为基础实现的锁或者同步器,最终都是通过LockSupport提供的parkunpark实现的线程挂起和唤醒。

# 2、LockSupport原理分析

先思考一个问题
看下面代码:

import java.util.concurrent.locks.LockSupport;

public class TestA {

    public static void main(String[] args) throws InterruptedException {

        LockSupport.unpark(Thread.currentThread());
        LockSupport.park();
        System.out.println("LockSupport  先unpark 后park");

        TestA testA = new TestA();
        synchronized (testA) {
            testA.notify();
            testA.wait();
            System.out.println("synchronized  先notify 后wait");
        }

    }
}

运行结果:

mixureSecure

根据代码执行的结果可以看出:
代码调用 LockSupport.unpark 来唤醒当前线程。即使当前线程没有被阻塞,这个操作也会起作用。 当前线程已经调用过一次 LockSupport.unpark 后,再调用LockSupport.park()就不会被阻塞了,因为已经提前唤醒了一次。

synchronized 同步代码块中 先调用 notify没有任何作用,因为没有线程在 testA锁对象 上等待。
当前线程已经调用过一次 testA.notify(); 后,再调用testA.wait(); 线程仍然会被阻塞。

总结:
LockSupport 提供了更灵活的阻塞和唤醒机制,能够在任何时候调用 unpark 而不必担心线程是否已经阻塞。

下面这段内容来自Java官方的JDK1.6中文版文档。 (如果英语掌握的很好,还是推荐自己去阅读源码上的英文注释比较好,虽然下面是官方的JDK1.6中文版文档,但是翻译的还是比较生硬~)

用来创建锁和其他同步类的基本线程阻塞原语。 

此类以及每个使用它的线程与一个许可关联(从 Semaphore 类的意义上说)。如果该许可可用,并且可在进程中使用,则调用 park 将立即返     
回;否则可能 阻塞。如果许可尚不可用,则可以调用 unpark 使其可用。(但与 Semaphore 不同的是,许可不能累积,并且最多只能有一个    
许可。) 

park 和 unpark 方法提供了阻塞和解除阻塞线程的有效方法,并且不会遇到导致过时方法 Thread.suspend 和 Thread.resume 因为以  
下目的变得不可用的问题:由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。此外,如果调用者线  
程被中断,并且支持超时,则 park 将返回。park 方法还可以在其他任何时间“毫无理由”地返回,因此通常必须在重新检查返回条件的循环里  
调用此方法。从这个意义上说,park 是“忙碌等待”的一种优化,它不会浪费这么多的时间进行自旋,但是必须将它与 unpark 配对使用才更高  
效。   

三种形式的 park 还各自支持一个 blocker 对象参数。此对象在线程受阻塞时被记录,以允许监视工具和诊断工具确定线程受阻塞的原因。  
(这样的工具可以使用方法 getBlocker(java.lang.Thread) 访问 blocker。)建议最好使用这些形式,而不是不带此参数的原始形式。  
在锁实现中提供的作为 blocker 的普通参数是 this。   

这些方法被设计用来作为创建高级同步实用工具的工具,对于大多数并发控制应用程序而言,它们本身并不是很有用。park 方法仅设计用于以下  
形式的构造: 

while (!canProceed()) { ... LockSupport.park(this); }在这里,在调用 park 之前,canProceed 和其他任何动作都不会锁定  
或阻塞。因为每个线程只与一个许可关联,park 的任何中间使用都可能干扰其预期效果。 

示例用法。 以下是一个先进先出 (first-in-first-out) 非重入锁类的框架。 

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);

     // Block while not first in queue or cannot acquire lock
     while (waiters.peek() != current ||
            !locked.compareAndSet(false, true)) {
        LockSupport.park(this);
        if (Thread.interrupted()) // ignore interrupts while waiting
          wasInterrupted = true;
     }

     waiters.remove();
     if (wasInterrupted)          // reassert interrupt status on exit
        current.interrupt();
   }

   public void unlock() {
     locked.set(false);
     LockSupport.unpark(waiters.peek());
   }
 }

# 总结下LockSupport类设计思路的关键点:

  • ①、许可机制: LockSupport通过一个许可的概念来控制线程的阻塞和恢复。这个许可类似于Semaphore中的许可,但需要注意的是它不支持累积,即最多只有一个许可可用。这意味着即使多次调用unpark也只会保留一个许可。

  • ②、阻塞操作: LockSupport.park()方法会尝试获取许可。如果许可存在,线程会立即返回并继续执行。如果没有许可,线程将被阻塞,直到另一个线程调用LockSupport.unpark(Thread)方法来释放许可,或者线程接收到中断信号。

  • ③、非累积性: 与Semaphore不同,LockSupport的许可不能累积。这意味着即使一个线程被多次unpark,它也只能被唤醒一次,多余的unpark操作不会产生额外的效果。 例如:

import java.util.concurrent.locks.LockSupport;

public class TestA {
    
    public static void main(String[] args) {

        // unpark 主线程两次
        LockSupport.unpark(Thread.currentThread());
        LockSupport.unpark(Thread.currentThread());

        // park主线程两次
        LockSupport.park();
        System.out.println("park第一次");  // 正常打印
        LockSupport.park();   // 在这就会阻塞
        System.out.println("park第二次"); // 不打印

    }
}

  • ④线程关联: 每个线程都有一个与之关联的LockSupport许可。当一个线程调用unpark时,它必须指定要唤醒的线程。这允许了更细粒度的控制,因为可以精确地选择哪个线程应该被唤醒。

  • ⑤、中断处理: 当一个线程被阻塞时,它可以被中断。LockSupport.park()会检查中断状态,并在检测到中断时抛出InterruptedException。这使得LockSupport可以安全地与其他Java中断机制协同工作。

至于再底层的实现就是通过Unsafe类,以及操作系统提供的原语了。
例如,在 Linux 上,LockSupport 可能会利用 pthread 库中的 pthread_mutex_lock 和 pthread_cond_wait 等函数来实现这些功能。这里就不再细说了,水平有限~

# 3、park\unpark VS wait\notify\sleep\await\singnal

直接列个表格简单直观:

方法 描述 对比
park/unpark park 阻塞当前线程,直到被 unpark 唤醒。unpark 唤醒指定线程。 直接操作线程,不依赖锁或条件。
可以在非同步块中使用。
提供更细粒度的控制。
park 可以指定阻塞时间。
可以先 unparkpark
由于park不依赖于锁或者其他条件,所以如果一个线程持有锁,调用了park,那么该线程挂起后并不会释放锁
wait/notify wait 使当前线程等待,直到被 notifynotifyAll 唤醒。 需要在synchronized同步代码块中使用。
wait 会释放持有的对象锁。
notify 唤醒单个等待线程,notifyAll 唤醒所有等待线程。
notifywait之前调用没有效果
sleep 使当前线程休眠指定时间。 不释放锁。
主要用于时间控制。
休眠时间结束后自动恢复。
await/signal 用于 Condition 对象上的线程等待和唤醒。 必须在锁条件下使用。
await 会释放锁并进入等待状态,直到被 signalsignalAll 唤醒。
用于更复杂的同步需求。