我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Java 多线程并发LockSupport

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Java 多线程并发LockSupport

概览

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

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

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

方法 park 和 unpark 提供了阻塞和解除阻塞线程的有效方法,这些线程不会遇到 Thread.suspend 和 Thread.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(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 类的代码量还算少,去掉注释仅有 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 的保存,本质是通过 Unsafe 的 putReferenceOpaque 方法保存和 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);

在 class="lazy" data-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.cpp 和 on_windows.cpp 中:

可以看出这个 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(false, 0L);
}
public static void parkNanos(long nanos) {
    if (nanos > 0)
        U.park(false, nanos);
}
public static void parkUntil(long deadline) {
    U.park(true, deadline);
}

这三个方法的区别是:

park

方法的作用是:除非许可处于可用状态,否者关闭当前线程的线程调度的意图。如果许可可用,则许可被使用掉,并立即返回。否则当前线程因为线程调度的意图被关闭而导致阻塞。

直到发生以下三种情况之一:

其他线程以当前线程为目标调用 unpark

其他线程中断当前线程

调用虚假地 return(即 no reason)

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

parkNanos

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

parkUntil

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

需要 blocker 参数的分组

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    U.park(false, 0L);
    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 的同步对象 。

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

  • 获取当前线程对象。
  • 将 blocker 对象的引用值存储到线程对象中。
  • 调用 Unsafe 对象的 park(boolean, long) 方法。
  • 将当前线程对象存储的引用值设置为 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 ,创建了两个线程 thread1 和 thread2 。

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

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

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

  • 线程 1 开始打印 start
  • 睡眠 1 秒
  • obj 对象调用 notify ,线程 2 开始尝试获取 obj 的锁。
  • 打印 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 继续执行:

  • 调用 park 进入阻塞状态

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

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

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

  • 打印当前线程的 Blocker 信息,为 null
  • 打印 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 两种阻塞线程和唤起线程的方式,能够感受出两者的不同之处:

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

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

  • suspend,挂起线程,但是不会释放类似锁这样的资源。
  • resume,恢复线程,如果之前没有使用suspend暂停线程,则不起作用。
  • Thread.stop() 由于其固有的风险而被逐步淘汰。当你停止一个线程时,它会解锁它锁定的所有监视器。如果以前受这些监视器保护的任何对象处于不一致状态,其他线程可能会看到这些对象处于不一致状态。 作用在受损物体上的线可能会有意或无意地行为不规律。与其他不受控制的异常不同,ThreadDeath 会静默地杀死线程,不会向用户发出程序可能已损坏的警告。损坏发生后,损坏可能会在无法预料的时刻出现。此外,在多线程环境中使用 DBMS – JDBC 时,终止线程会产生问题。
  • Thread.suspend() 已被弃用,因为它本质上容易死锁。因此,Thread.resume() 也必须被弃用。当目标线程被挂起时,它会在监视器上锁定一个保护关键系统资源的锁,并且在目标线程恢复之前没有其他线程可以访问它。如果将重新启动目标线程的线程在调用 resume() 之前尝试锁定此监视器,则会发生死锁。

到此这篇关于Java 多线程并发LockSupport的文章就介绍到这了,更多相关Java  LockSupport内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Java 多线程并发LockSupport

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Java LockSupport与线程中断

LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),permit只有两个值1和零,默认是零。

Java多线程并发之ReentrantLock

这篇文章主要介绍了Java 多线程并发ReentrantLock,文中有非常详细的代码示例,对正在学习java的小伙伴们有非常好的帮助,需要的朋友可以参考下
2023-05-18

如何实现 Java 多线程并发控制?(Java多线程并发控制怎样实现)

在Java编程中,多线程并发控制是一个重要的概念和技术。它允许程序同时执行多个线程,以提高程序的性能和响应性。然而,多线程并发也带来了一些挑战,如线程安全、竞态条件和死锁等。本文将介绍Java中实现多线程并发控制的方法和技巧。一、线程同步机制
如何实现 Java 多线程并发控制?(Java多线程并发控制怎样实现)
Java2024-12-13

Java多线程并发、并行、线程与进程实例分析

本篇内容介绍了“Java多线程并发、并行、线程与进程实例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!一、并发与并行并发:指两个或多个事
2023-07-02

怎么在Java中利用LockSupport类实现并发编程

今天就跟大家聊聊有关怎么在Java中利用LockSupport类实现并发编程,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。一、LockSupport类的属性private stati
2023-06-15

理解Java多线程之并发编程

这篇文章主要介绍了理解Java多线程之并发编程的相关资料,需要的朋友可以参考下
2023-02-02

Java并发编程中LockSupport的用法是怎样的

Java并发编程中LockSupport的用法是怎样的,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。1、什么是LockSupport?LockSupport是用于创建锁和其他同
2023-06-25

java多线程中如何实现线程并发库

本篇文章给大家分享的是有关java多线程中如何实现线程并发库,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。多线程之线程并发库原子性操作类java.util.concurrent
2023-06-19

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录