Java并发编程专题之LockSupport

LockSupport

概述

LockSupport是一个编程工具类,主要是为了阻塞和唤醒线程。它的所有方法都是静态方法,它可以让线程在任意位置阻塞,也可以在任意位置唤醒。

它可以在阻塞线程时为线程设置一个blocker,这个blocker是用来记录线程被阻塞时被谁阻塞的,用于线程监控和分析工具来定位原因。

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

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
        System.out.println("线程开始执行");
        LockSupport.park();
        System.out.println("线程执行结束");
    });
    thread.start();
    TimeUnit.SECONDS.sleep(3);
    System.out.println("执行unpark");
    LockSupport.unpark(thread);
}

和wait/notify区别

  1. wait和notify都必须先获得锁对象才能调用,但是park不需要获取某个对象的锁就可以锁住线程。
  2. notify只能随机选择一个线程唤醒,无法唤醒指定的线程,unpark却可以唤醒一个指定的线程。

重要方法

这些方法都是调用Unsafe类的native方法

private static final sun.misc.Unsafe UNSAFE;

public final class Unsafe {
    public native void park(boolean isAbsolute, long time);
    public native void unpark(Thread jthread);
}

park(Object blocker)

setBlocker记录了当前线程是被blocker阻塞的,当线程在没有持有许可证的情况下调用park方法而被阻塞挂起时,这个blocker对象会被记录到该线程内部。使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用getBlocker(Thread)方法来获取blocker对象的,所以推荐使用LockSupport.park(this);

如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.park()时会马上返回,否则调用线程会被阻塞挂起。在其他线程调用unpark(Thread thread) 方法并且将当前线程作为参数时,调用park方法而被阻塞的线程会返回。另外,如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者被虚假唤醒,则阻塞线程也会返回。

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null); // 线程被激活后清除blocker变量
}
private static void setBlocker(Thread t, Object arg) {
    UNSAFE.putObject(t, parkBlockerOffset, arg);
}

unpark(Thread thread)

如果thread之前因调用park()而被挂起,则调用unpark后,该线程会被唤醒。

如果thread之前没有调用park,则让thread持有一个许可证,之后再调用park方法,则会立即返回。

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

parkNanos(Object blocker, long nanos)

如果没有拿到许可证,则阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回

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

阻塞当前线程,直到deadline;

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

原理浅析

只讲部分重点,非重点代码略去了

概述

每个java线程都有一个Parker实例,Parker类定义:

class Parker {
private:
  volatile int _counter; // 记录许可
public:
  void park(bool isAbsolute, jlong time);
  void unpark();
}

LockSupport通过控制_counter进行线程的阻塞/唤醒,原理类似于信号量机制的PV操作,其中Semaphore初始为0,最多为1。

形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个。

_counter只能在0和1之间取值:当为1时,代表该类被unpark调用过,更多的调用,也不会增加_counter的值,当该线程调用park()时,不会阻塞,同时_counter立刻清零。当为0时, 调用park()会被阻塞。

  • 为什么可以先唤醒线程后阻塞线程?
    因为unpark获得了一个凭证,之后调用park因为有凭证消费,故不会阻塞。
  • 为什么唤醒两次后阻塞两次会阻塞线程。
    因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证。

park()

未命名文件
  • 检查_counter是否大于零(之前调用过unpark),则通过原子操作将_counter设置为0。线程不用阻塞并返回。
  • 检查该线程是否有中断信号,如果有则清除该中断信号并返回(不抛出异常)。
  • 尝试通过pthread_mutex_trylock对_mutex加锁来达到线程互斥。
  • 检查park是否设置超时时间, 若设置了通过safe_cond_timedwait进行超时等待; 若没有设置,调用pthread_cond_wait进行阻塞等待。 这两个函数都在阻塞等待时都会放弃cpu的使用。 直到别的线程去唤醒它(调用pthread_cond_signal)。safe_cond_timedwait/pthread_cond_wait在执行之前肯定已经获取了锁_mutex, 在睡眠前释放了锁, 在被唤醒之前, 首先再去获取锁。
  • 将_counter设置为零
  • 通过pthread_mutex_unlock释放锁
void Parker::park(bool isAbsolute, jlong time) {
  if (Atomic::xchg(0, &_counter) > 0) return; // 调用过unpark
    
  if (Thread::is_interrupted(thread, false)) return; // 中断过
  
  // 对_mutex加锁
  if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
        return;
  }
 
  // 进行超时等待或者阻塞等待,直到被signal唤醒
  if (time == 0) {
    status = pthread_cond_wait (&_cond[_cur_index], _mutex); 
  } else {
    status = os::Linux::safe_cond_timedwait (&_cond[_cur_index], _mutex, &absTime);
  }
  _counter = 0; // 唤醒后消耗掉这个凭证
  status = pthread_mutex_unlock(_mutex); // 解锁
}

unpark()

  • 首先获取锁_mutex。
  • 不管之前是什么值,都将_counter置为1,所以无论多少函数调用unpark(),都是无效的,只会记录一次。
  • 检查线程是否已经被阻塞了,阻塞则调用pthread_cond_signal唤醒
  • 最后释放锁_mutex。
void Parker::unpark() {
  status = pthread_mutex_lock(_mutex);   
  s = _counter;
  _counter = 1; // 将_counter置1
  if (s < 1)  status = pthread_cond_signal (&_cond[_cur_index]); // 进行线程唤醒
  pthread_mutex_unlock(_mutex);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容