Java并发编程之LockSupport源码详解

Java并发编程之LockSupport源码详解,第1张

Java并发编程之LockSupport源码详解 什么是LockSupport

LockSupport是JUC包下的一个工具类,主要作用是用于阻塞和唤醒线程,底层基于Unsafe类实现。LockSupport类的所有方法都被static修饰,可以在任意位置阻塞或唤醒某线程。
JUC包下的队列同步器AQS的阻塞和唤醒 *** 作就是使用LockSupport实现。接下来关注LockSupport的源码。

LockSupport源码分析 构造方法与成员变量

LockSupport类只提供了一个被private修饰的构造方法,意味着LockSupport不能在任何地方被实例化,但所有方法都是静态方法,可以在任意地方被调用。

	private LockSupport() {}

成员变量中重点关注parkBlockerOffset变量,即blocker的偏移量,下文讲解。

	private LockSupport() {}
	
	private static final sun.misc.Unsafe UNSAFE;
    private static final long parkBlockerOffset;
    private static final long SEED;
    private static final long PROBE;
    private static final long SECONDARY;
    static {
        try {
        	
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            
            Class tk = Thread.class;
            parkBlockerOffset = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("parkBlocker"));
            SEED = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSeed"));
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
            SEConDARY = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception ex) { throw new Error(ex); }
    }
关键方法 blocker的作用

在分析LockSupport源码前,先来解决上一部分遗留的问题,什么是blocker,blocker的作用是什么?
在Thread类的源码中,可以找到这样一个被volatile修饰的变量,LockSupport类中所有blocker的相关变量以及方法都是为这个parkBlocker变量服务的。

volatile Object parkBlocker;

当线程被阻塞时,如果该线程的parkBlocker变量不为空,则在打印堆栈异常时,控制台会打印输出具体阻塞对象的信息,方便错误排查,后文会对此进行演示

blocker相关方法

首先关注blocker相关方法:

	
	private static void setBlocker(Thread t, Object arg) {
        UNSAFE.putObject(t, parkBlockerOffset, arg);
    }
    
    public static Object getBlocker(Thread t) {
        if (t == null)
            throw new NullPointerException();
        return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
    }

源码中可以清晰的看到方法中使用了parkBlockerOffset,即在类初始化时获取的parkBlocker在内存中的偏移量,putObject和getObjectVolatile方法采用地址加偏移量的方式从内存直接设置或获取parkBlocker(Unsafe包下的方法可以直接 *** 作内存,因此该类被命名为Unsafe),这样做的原因是因为线程被阻塞时无法被赋值或取值。

park与unpark方法

LockSupport为阻塞 *** 作提供了两组三类方法
一组是不设置blocker的方法,另一组是设置blocker方法的,JDK推荐为Thread设置blocker方便调试。其余分析见注释。

	
	public static void park(Object blocker) {
		
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        
        UNSAFE.park(false, 0L);
        
        setBlocker(t, null);
    }
	
    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);
        }
    }
	
    public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
    }

    public static void park() {
        UNSAFE.park(false, 0L);
    }

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

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

unpark方法如下,UNSAFE.unpark为唤醒线程的核心方法,后文讨论。

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

blocker测试

(blocker学习自Java并发包源码学习系列:挂起与唤醒线程LockSupport工具类)

阻塞时不设blocker

首先测试不设置blocker阻塞线程时,用jstack指令打印结果(首先在idea的terminal窗口输入jsp,找到当前运行类的pid,之后输入jstack pid即可打印进程堆栈):

public class LockSupportTest {

    public void park(){
        LockSupport.park();
    }

    public static void main(String[] args) {
        new LockSupportTest().park();
    }

}

主线程堆栈打印结果如下,可见并未打印阻塞信息

阻塞时设置blocker
public class LockSupportTest {

    public void park(){
        LockSupport.park(this);
    }

    public static void main(String[] args) {
        new LockSupportTest().park();
    }

}

主线程堆栈打印结果如下,可见打印了阻塞信息,便于错误调试

park与unpark在底层C++的实现

在Unsafe的源码中可见,park与unpark都是native方法(意味着由C++底层实现),具体实现细节需要阅读C++源码

park和unpark阻塞和唤醒的实现思路

阅读C++源码前首先简述一下使用park和unpark阻塞和唤醒的思想,park/unpark的核心是一个抽象概念——许可。许可的具体实现为一个二元整型变量,即该变量只有0和1两个状态,默认值为0。当一个线程调用了park的时候,如果许可仍有剩余(为1),则当前线程不阻塞(持有许可),如果许可无剩余(为0),则当前线程阻塞等待获取许可,相应的调用unpark时会释放许可(置1)。

park在C++的源码实现

由于作者C++水平有限且本文关注重点在java,所以省略部分非关键代码,只看核心实现。

void Parker::park(bool isAbsolute, jlong time) {
  // 先原子的将_counter的值设为0,并返回_counter的原值,如果原值>0说明有通行证,直接返回
  // 首先尝试能否获取许可
  // 利用原子 *** 作设_counter(许可)为0,同时返回_counter原值
  // 原值为1时获取许可成功 直接return;
  if (Atomic::xchg(0, &_counter) > 0) return;

  Thread* thread = Thread::current();
  assert(thread->is_Java_thread(), "Must be JavaThread");
  JavaThread *jt = (JavaThread *)thread;
  //如果线程被中断 直接返回
  if (Thread::is_interrupted(thread, false)) {
    return;
  }

  timespec absTime;
  // 如果出现time小于0 或
  // 调用了parkUntil方法(只用调用parkUntil时isAbsolute为true)且time为0的情况
  // 意味着不需要尝试获取许可 直接返回
  if (time < 0 || (isAbsolute && time == 0) ) {
    return;
  }
  // 只有在java种调用了parkNanos或parkUntil方法才会进入此分支
  if (time > 0) {
    // 定时唤醒
    unpackTime(&absTime, isAbsolute, time);
  }

  ThreadBlockInVM tbivm(jt);

  // 如果线程被中断,直接返回
  // 如果没有被中断,且获取互斥锁失败,直接返回
  if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
    return;
  }

  int status ;
  // 如果_counter > 0, 不需要等欧拉欧拉待,这里再次检查_counter的值
  if (_counter > 0)  {
    _counter = 0;
    status = pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant") ;
    // 插入写屏障
    OrderAccess::fence();
    return;
  }


  OSThreadWaitState osts(thread->osthread(), false);
  // 暂停Java线程
  jt->set_suspend_equivalent();

  assert(_cur_index == -1, "invariant");
  if (time == 0) {
    _cur_index = REL_INDEX; // arbitrary choice when not timed
    // 线程进入阻塞状态 并等待_cond[_cur_index]信号
    status = pthread_cond_wait (&_cond[_cur_index], _mutex) ;
  } else {
    _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
    // 线程进入限时阻塞状态
    status = os::Linux::safe_cond_timedwait (&_cond[_cur_index], _mutex, &absTime) ;
    if (status != 0 && WorkAroundNPTLTimedWaitHang) {
      pthread_cond_destroy (&_cond[_cur_index]) ;
      pthread_cond_init    (&_cond[_cur_index], isAbsolute ? NULL : os::Linux::condAttr());
    }
  }
  _cur_index = -1;
  _counter = 0 ;
  // 互斥锁释放
  status = pthread_mutex_unlock(_mutex) ;
  assert_status(status == 0, status, "invariant") ;
  OrderAccess::fence();

}

unpark在C++的源码实现
void Parker::unpark() {
  int s, status ;
  status = pthread_mutex_lock(_mutex);
  assert (status == 0, "invariant") ; 
  //保存原始许可值 用于后续判断
  s = _counter; 
  //许可置1
  _counter = 1;
  if (s < 1) {
     if (WorkAroundNPTLTimedWaitHang) {
        //唤醒等待线程
        status = pthread_cond_signal (_cond) ;
        assert (status == 0, "invariant") ;
        //释放锁操作
        status = pthread_mutex_unlock(_mutex);
        assert (status == 0, "invariant") ;
     } else {
        status = pthread_mutex_unlock(_mutex);
        assert (status == 0, "invariant") ;
        status = pthread_cond_signal (_cond) ;
        assert (status == 0, "invariant") ;
     }
  } else {
   	//释放锁
    pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant") ;
  }
}
park/unpark的特点及与wait/notify的区别
  • 先唤醒再阻塞 *** 作,由LockSupport实现,即先调用unpark再调用park,则该线程不会被阻塞;由Object实现,即先调用notify再调用wait,则该线程会被阻塞。
  • LockSupport允许在任意地方阻塞唤醒线程,Object的wait/notify必须在synchronized同步代码块内调用。因为park/unpark依赖许可量,wait/notify依赖锁。
  • LockSupport允许唤醒指定线程,notify只能唤醒随机线程,notifyAll唤醒全部阻塞线程。

以上便是本篇文章的全部内容
作者才疏学浅,如文中出现纰漏,还望指正

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5583734.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-14
下一篇 2022-12-14

发表评论

登录后才能评论

评论列表(0条)

保存