Java 多线程并发【8】LockSupport


概览

这部分内容来自于这个类的注释,简单翻译了下。

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

它的实现思想是给每个使用它的线程颁发一个许可,当许可是可用状态时(线程有许可),调用 park 方法会消耗一个许可,方法立即返回(与信号量的作用类似),线程可以继续执行 park 方法后面的逻辑;如果调用 park 方法前,许可处于不可用状态(线程没有许可),park 方法不会立即返回,从而导致线程阻塞。而此时,可以通过调用 unpark 方法使许可恢复到可用状态(但与信号量不同,许可不会累积。最多有一个)。

方法 park 和 unpark 提供了阻塞和解除阻塞线程的有效方法,这些线程不会遇到 Thread.suspendThread.resume 存在的问题(因suspend 容易导致死锁,这俩个方法因为这个原因已经被弃用 ),因为 park 和 unpark 调用的线程,不存在锁竞争。

而如果调用 park 方法的线程被中断,park 方法将会立即 return ,并且有设置超时版本的 park 方法。

park 方法也可以在任何其他时间没有原因的 return,因此通常必须在返回时重新检查条件的循环中调用。

从这个意义上说,park 是对“繁忙等待”的优化,它不会浪费太多时间旋转,但必须与 unpark 配对才能有效。

park 方法有对应带有 blocker 对象参数的重载方法, blocker 对象在线程被阻塞时被记录,以允许监视和诊断工具识别线程被阻塞的原因。 (此类工具可以使用 getBlocker(Thread) 方法访问阻止程序。)强烈建议使用这些表单而不是没有此参数的原始表单。 在锁实现中作为阻塞器提供的正常参数是 this。

本质上 LockSupport 实现了一种自旋,构造类似于:

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

其中 canProceed 或在调用之前的任何其他操作都不会导致锁定或阻塞。 因为每个线程只有一个许可,所以任何对 park 的中间使用都可能会干扰其预期效果。

示例用法。 这是一个先进先出不可重入锁类的草图:

 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(falsetrue)) {
       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 类的代码量还算少,去掉注释仅有 100 行,所有的属性和方法都是静态的,并且备注明确说明了 LockSupport 无法实例化:

public class LockSupport {
    private LockSupport() {} // 无法实例化
    // ...
}

LockSupport 中包含了几个私有的内部静态属性:

public class LockSupport {
    // 通过内部 API 实现 Hotspot
    private static final Unsafe U = Unsafe.getUnsafe();
    private static final long PARKBLOCKER= U.objectFieldOffset(Thread.class, "parkBlocker");
    private static final long TID = U.objectFieldOffset(Thread.class, "tid");
    // ...    
}

从这些内部私有的静态属性可以看出,最重要的就是 U 了,LockSupport 中的方法,本质上也是调用U 提供的能力。U 在 CAS 与原子类中有介绍,是 JDK 中提供的一些非阻塞线程安全的实现能力的类,它的大多数方法都是 native 方法。

静态方法

LockSupport 基本上就是个静态工具类,它的主要能力,集中在它的静态方法中。

public class LockSupport {
    public static void unpark(Thread thread)
      
    public static void park(Object blocker)
    public static void parkNanos(Object blocker, long nanos)
    public static void parkUntil(Object blocker, long deadline)
      
    public static Object getBlocker(Thread t)
    public static void setCurrentBlocker(Object blocker)
    private static void setBlocker(Thread t, Object arg)
      
    public static void park()
    public static void parkNanos(long nanos)
    public static void parkUntil(long deadline)

    static final long getThreadId(Thread thread)
}

从方法名就可以看出,主要分为三个:

  • unpark
  • park
  • getBlocker

Blocker

public static Object getBlocker(Thread t) {
    if (t == null)
        throw new NullPointerException();
    return U.getReferenceOpaque(t, PARKBLOCKER);
}

private static void setBlocker(Thread t, Object arg) {
    U.putReferenceOpaque(t, PARKBLOCKER, arg);
}
// 在 JDK 14 之前是不存在该方法的, setBlocker 只能从内部进行
public static void setCurrentBlocker(Object blocker) {
    U.putReferenceOpaque(Thread.currentThread(), PARKBLOCKER, blocker);
}

setCurrentBlocker 的作用是,设置当前线程调用 getBlocker 返回的对象。 在 JDK 14 后暴露这个方法的用途,是用来配合park() 的无参数版本设置 Blocker ,它可以实现 park(blocker) 的效果:

 setCurrentBlocker(b); 
 park(); 
 setCurrentBlocker(null);

而私有静态方法 setBlocker 是在 park 的有参数方法中封装使用的。

对于 blocker 的保存,本质是通过 UnsafeputReferenceOpaque 方法保存和 getReferenceOpaque 方法读取的。

@IntrinsicCandidate
public final void putReferenceOpaque(Object o, long offset, Object x) {
    putReferenceVolatile(o, offset, x);
}
// 使用 volatile 存储语义将引用值存储到给定的 Java 变量中。 否则等同于 putReference(Object, long, Object)    
@IntrinsicCandidate
public native void putReferenceVolatile(Object o, long offset, Object x);

@IntrinsicCandidate
public final Object getReferenceOpaque(Object o, long offset) {
    return getReferenceVolatile(o, offset);
}
// 从给定的 Java 变量中获取引用值,具有可变加载语义。 否则等同于 getReference(Object, long)
@IntrinsicCandidate
public native Object getReferenceVolatile(Object o, long offset);

src/hotspot/share/opto/library_call.cpp 中发现了 Java 到 native 方法的映射:

case vmIntrinsics::_putReferenceVolatile: return inline_unsafe_access( is_store, T_OBJECT,   Volatile, false);

native 最终调用到是老朋友 inline_unsafe_access 。(这里不详细展开了。。我也没搞明白这个方法,和汇编指令相关)

需要注意的是,最终的都是将对象设置为了 volatile 。充分说明 LockSupport 也是一套非阻塞同步方案。

而上面提到的 putReference(Object, long, Object)的作用是:将引用值存储到给定的 Java 变量中。

@IntrinsicCandidate
public native void putReference(Object o, long offset, Object x);

除非存储的引用 x 为 null 或与字段类型匹配,否则结果是未定义的。 如果引用 o 不为空,则更新该对象的卡片标记或其他存储屏障(如果 VM 需要它们)。

unpark

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

解除调用 park 的线程的阻塞状态,或者如果调用 park 的线程没有阻塞,则会导致后续调用 park 不会造成阻塞。

注意:这个操作是不安全的,调用者必须确保线程没有被销毁。

这是什么意思呢?通过下面这个例子可以感受到先调用 unpark 后,指定参数中的线程参数对象调用 park 不会造成阻塞:

class LockSupportDemo {
    fun check() {
        val thread1 = Thread {
            Thread.sleep(1000)
            println("thread1 start + ${Date(System.currentTimeMillis())}")
            LockSupport.park()
            println("thread1 end +  ${Date(System.currentTimeMillis())}")
        }

        val thread2 = Thread {
            println("thread2 start +  ${Date(System.currentTimeMillis())}")
            LockSupport.unpark(thread1)
            println("thread2 end +  ${Date(System.currentTimeMillis())}")
        }
        thread1.start()
        thread2.start()
    }
}

打印日志

thread2 start +  Fri Jun 03 02:19:34 CST 2022
thread2 end +  Fri Jun 03 02:19:34 CST 2022
thread1 start + Fri Jun 03 02:19:35 CST 2022
thread1 end +  Fri Jun 03 02:19:35 CST 2022

thread2 优先执行,thread1 在 thread2 开始执行 1s 后执行,thread1 调用了 LockSupport.park() ,并没有造成自身阻塞。

Unsafe 的 unpark 方法

LockSupport.unpark(thread) 内部实际只调用了 Unsafe#unpark(thread);

public native void unpark(Object thread);

又是一个 native 方法。在 JDK 中发现 Parker::unpark 的定义在 os_posix.cppon_windows.cpp 中:

Java 多线程并发【8】LockSupport
image-20220603023039761.png

可以看出这个 native 方法,应该在不同的平台会有不同的实现。

park

park 方法有两组重载方法:

// 不带 blocker
public static void park()     
public static void parkNanos(long nanos)
public static void parkUntil(long deadline) 
// 需要 blocker 参数
public static void park(Object blocker)
public static void parkNanos(Object blocker, long nanos)
public static void parkUntil(Object blocker, long deadline)

不带 blocker 参数的分组

没有 blocker 参数的一组本质上的逻辑是:

U.park(boolean, long);

这一点可以从三个方法中看出:

public static void park() {
    U.park(false0L);
}
    
public static void parkNanos(long nanos) {
    if (nanos > 0)
        U.park(false, nanos);
}

public static void parkUntil(long deadline) {
    U.park(true, deadline);
}

这三个方法的区别是:

  • park

    方法的作用是:除非许可处于可用状态,否者关闭当前线程的线程调度的意图。如果许可可用,则许可被使用掉,并立即返回。否则当前线程因为线程调度的意图被关闭而导致阻塞。直到发生以下三种情况之一:

    这个方法不会报告是哪一种原因导致的 return。调用者应该重新检查导致线程第一次停止的条件。 例如,调用者还可以确定线程在返回时的中断状态。

    1. 其他线程以当前线程为目标调用 unpark
    2. 其他线程中断当前线程
    3. 调用虚假地 return(即 no reason)
  • parkNanos

    禁用当前线程的线程调度意图,直到指定的等待时间,除非许可可用。如果参数 nanos 为 0 或为负数,这个方法将不会做任何事情。

  • parkUntil

    禁用当前线程的线程调度意图,直到指定的最后期限,除非许可可用。参数 deadline 是截止日期——从 Epoch 开始等待的绝对时间,以毫秒为单位。

需要 blocker 参数的分组

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    U.park(false0L);
    setBlocker(t, null);
}

public static void parkNanos(Object blocker, long nanos) {
    if (nanos > 0) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        U.park(false, nanos);
        setBlocker(t, null);
    }
}
    
public static void parkUntil(Object blocker, long deadline) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    U.park(true, deadline);
    setBlocker(t, null);
}

带有 blocker 参数的这组函数,它们和对应的不带参数的方法意义是一样的,多了一个设置对象的操作。而这个对象 blocker 是负责此线程 parking 的同步对象 。

这组重载方法有共同的逻辑:

  1. 获取当前线程对象。
  2. blocker 对象的引用值存储到线程对象中。
  3. 调用 Unsafe 对象的 park(boolean, long) 方法。
  4. 将当前线程对象存储的引用值设置为 null 。

让我困惑的是,这个存储引用值操作有什么作用。但是联想到线程操作和对象,惊奇的发现我们常用的 API Object.wait()Object.notify() ,好像和这个场景很像,都用到了一个 Object ,也都造成了线程阻塞唤醒。

park/unpark 和 Object 的 wait/notify

public class LockSupportJava {
    
    Object obj = new Object();

    public static void main(String[] args) {
        LockSupportJava lock = new LockSupportJava();
        lock.waitAndNotify();
    }

    void waitAndNotify() {
        Thread thread1 = new Thread(() -> {
            synchronized(obj) {
                try {
                    System.out.println("thread1 start + " + new Date(System.currentTimeMillis()));
                    Thread.sleep(1000);
                    obj.notify();
                    System.out.println("thread1 end + " + new Date(System.currentTimeMillis()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized(obj) {
                try {
                    thread1.start();
                    Thread.sleep(3000);
                    System.out.println("thread2 start + " + new Date(System.currentTimeMillis()));
                    obj.wait();
                    System.out.println("thread2 end + " + new Date(System.currentTimeMillis()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread2.start();
    }
}

这是一个简单的 Demo ,创建了两个线程 thread1thread2

线程 2 从主线程先执行,持有了 obj对象的锁,在它的执行逻辑中:

  1. 先启动线程 1
  2. 线程 2 睡眠 3 秒
  3. 三秒后打印 start,obj 对象调用 wait ,使当前线程让出对象的锁,并进入阻塞状态。

此时,线程 1 可以获取到 obj 对象的锁了,它的执行逻辑:

  1. 线程 1 开始打印 start
  2. 睡眠 1 秒
  3. obj 对象调用 notify ,线程 2 开始尝试获取 obj 的锁。
  4. 打印 end ,执行结束,让出 obj 的锁

最后,线程 2 重新获取到了 obj 的锁,继续执行打印 end 。

从打印日志中,验证打印顺序:

thread2 start + Fri Jun 03 04:02:17 CST 2022
thread1 start + Fri Jun 03 04:02:17 CST 2022
thread1 end + Fri Jun 03 04:02:18 CST 2022
thread2 end + Fri Jun 03 04:02:18 CST 2022

注意:使用wait/notify实现同步时,必须先调用wait,后调用notify,如果先调用notify,再调用wait,会导致线程一直阻塞。

而如果使用 park / unpark 实现一个阻塞唤醒效果:

    Thread thread;

    void parkAndUnpark() {
        Thread thread2 = new Thread(() -> {
            System.out.println("thread2 start + " + new Date(System.currentTimeMillis()));
            thread.start();
            LockSupport.park(obj);
            System.out.println("Blocker info " + LockSupport.getBlocker(Thread.currentThread()));
            System.out.println("thread2 end + " + new Date(System.currentTimeMillis()));
        });
        // 唤起 thread2
        Thread thread1 = new Thread(() -> {
            try {
                System.out.println("thread1 start + " + new Date(System.currentTimeMillis()));
                Thread.sleep(3000);
                System.out.println("Blocker info " + LockSupport.getBlocker(thread2));
                LockSupport.unpark(thread2);
                System.out.println("thread1 end + " + new Date(System.currentTimeMillis()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread = thread1;
        thread2.start();
    }

parkAndUnpark() 方法中,先启动了线程 2,线程 2 打印完 start 后就启动了线程 1 。而此时线程 2 继续执行:

  1. 调用 park 进入阻塞状态

在线程 2 执行上面两个逻辑的同时,线程 1 也在同时执行:

  1. 线程 1 启动后先打印 start
  2. 线程 1 睡眠 3 秒
  3. 检查线程 2 调用 park(Object)方法设置的 Blocker 信息
  4. 调用 LockSupport.unpark(thread2) 解除线程 2 的阻塞
  5. 线程 1 打印 end 执行结束

线程 2 在被线程 1 唤醒后,继续执行打印信息:

  1. 打印当前线程的 Blocker 信息,为 null
  2. 打印 end 执行结束

打印日志:

thread2 start + Fri Jun 03 04:24:32 CST 2022
thread1 start + Fri Jun 03 04:24:32 CST 2022
Blocker info java.lang.Object@7a3acdd9
thread1 end + Fri Jun 03 04:24:35 CST 2022
Blocker info null
thread2 end + Fri Jun 03 04:24:35 CST 2022

注意:先调用 unpark 后调用 park 也不会导致阻塞,更加灵活。

可以发现,blocker 在线程恢复后变成了 null ,个人理解它的作用就是用来做阻塞标记的,可以用来在线程阻塞状态下,将一个对象设置上,然后在其他线程中读取这个对象。blocker 与线程的单次阻塞状态绑定,可以用于对线程状态的排查和线程状态的监控。

区别

从 wait/notify 和 park/unpark 两种阻塞线程和唤起线程的方式,能够感受出两者的不同之处:

  1. wait/notify 需要配合 synchronized 进行;park/unpark 虽然设置了 blocker 但全程没有锁。
  2. wait/notify 需要保证 wait 在前,notify 在后,否则会阻塞线程;park/unpark 对调用顺序没有限制。不会造成阻塞。
  3. wait/notify 方法在 Object 中定义,用于对象锁的资源让出;park/unpark 来自于 LockSupport ,是静态方法,用于对线程本身进行挂起唤醒。并可以通过 blocker 绑定线程阻塞状态下的一些信息。

个人感觉 park/unpark 更像是 挂起/恢复,而 Thread.suspendThread.resume 都已废弃,是很好的替代方案。

suspend,挂起线程,但是不会释放类似锁这样的资源。

resume,恢复线程,如果之前没有使用suspend暂停线程,则不起作用。

Thread.stop() 由于其固有的风险而被逐步淘汰。当你停止一个线程时,它会解锁它锁定的所有监视器。如果以前受这些监视器保护的任何对象处于不一致状态,其他线程可能会看到这些对象处于不一致状态。 作用在受损物体上的线可能会有意或无意地行为不规律。与其他不受控制的异常不同,ThreadDeath 会静默地杀死线程,不会向用户发出程序可能已损坏的警告。损坏发生后,损坏可能会在无法预料的时刻出现。此外,在多线程环境中使用 DBMS – JDBC 时,终止线程会产生问题。

Thread.suspend() 已被弃用,因为它本质上容易死锁。因此,Thread.resume() 也必须被弃用。当目标线程被挂起时,它会在监视器上锁定一个保护关键系统资源的锁,并且在目标线程恢复之前没有其他线程可以访问它。如果将重新启动目标线程的线程在调用 resume() 之前尝试锁定此监视器,则会发生死锁。



原文始发于微信公众号(八千里路山与海):Java 多线程并发【8】LockSupport

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/85113.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!