Java并发包基础元件LockSupport

前言

LockSupport 和 CAS 是Java并发包中很多并发工具控制机制(Lock和同步器框架的核心 AQS: AbstractQueuedSynchronizer)的基础,它们底层其实都是依赖Unsafe实现。

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。其主要的核心就是提供了park()和unpark()方法来实现阻塞线程和解除线程阻塞。

其基本原理类似于二元信号量(只有1个许可证"permit"可供使用),当执行park()的时候,如果这个唯一许可证还没有被占用,当前线程则获取该唯一许可继续往下执行,如果许可已经被占用,则当前线程阻塞,等待获取许可。当执行unpark()的时候,将释放对应线程的许可。

park/unpark方法详解

首先看LockSupport 中相关方法的源码(此处只列举了最基本的park方法,其他带超时时间参数的park方法就不再一一介绍)

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

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

public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
}

其他带超时时间的变种park方法便不再一一列举。

通过源码可以发现,park/unpark的底层都是通过调用unsafe类的相关实现,对于park的其他增加了超时时间的变种park方法也是根据unsafe的park方法的第一个参数是否是绝对时间来扩展的。

关于park/unpark方法在openJDK里的C++实现,主要是利用了Posix的mutex,condition来实现的,至于什么是Posix、什么是mutex,condition,就不在深入探索了,只要知道这种实现是和平台紧密相关的,大概就是借助了操作系统的某些实现。总之其内部维护了一个volatile修饰的int类型的_counter变量来记录所谓的“许可”。当park时,这个变量置为了0,当unpark时,这个变量置为1。

值得注意的是,由于park的底层调用的是unsafe的park实现,所以当调用LockSupport的park()方法时如果没有立即获得许可,那么当前线程阻塞之后,也只有出现如下几种情况才会退出阻塞状态,立即返回:

1)其他线程执行了当前线程的unpark()方法2)其他线程打断了当前线程3)如果park方法带的超时时间不为0,当超时时间到达时4)无理由的虚假的唤醒(也就是传说中“Spurious wakeup”,和Object类的wait()方法类似)。

前三种情况都很好理解,第四种情况似乎有点让人无法接受,为什么会存在无缘无故的就被唤醒的情况?这样如何保证我们的应用不出现错误?Google了很多关于Spurious wakeup的文章,大概有如下几种解释:

第一种解释:通过分析源码发现底层的pthread_cond_wait方法并不是放在一个while循环中,而是if判断中,这样当pthread_cond_wait被唤醒之后,并不会再次进行条件判断,而是立即返回至上层应用。我认为这其实并不能称之为一种解释,最多算一种最肤浅最表面的原因,更重要的应该是为何pthread_cond_wait方法会在条件不成立的情况下返回。

第二种解释:这种解释认为这是出于性能考虑的原因“Spurious wakeups may sound strange, but on some multiprocessor systems, making condition wakeup completely predictable might substantially slow all condition variable operations”。但这看起来也像是一种模棱两可的解释。

第三种解释:认为这是操作系统本身的一种策略“Each blocking system call on Linux returns abruptly with EINTR when the process receives a signal. ... pthread_cond_wait() can't restart the waiting because it may miss a real wakeup in the little time it was outside the futex system call.”

总而言之,这种无理由的虚假的唤醒是存在的,但是几率应该是比较少的,到底的出于什么原因导致的,我们可以不用深究。针对这种情况,如何保证我们的应用不受这种虚假唤醒的影响,网络上的答案到是一致的,那就是:将park()方法的调用置于循环检查是否满足条件的代码块中:

while (<condition does not hold>)
     LockSupport.park();
    ... //执行适合条件的动作 

park/unpark特性详解

1. 许可默认是被占用的,也就是说如果在没有先执行unpark的情况下,直接执行park()将获取不到许可,从而被阻塞。示例如下:

public static void main(String[] args)
{
     LockSupport.park();//许可默认已经被占用,此处将阻塞
     System.out.println("block.");//这里将不会得到执行
}

2. LockSupport不可重入,但unpark可以多次调用。 

public static void main(String[] args)
{
     Thread thread = Thread.currentThread();
     LockSupport.unpark(thread);//释放许可
	 System.out.println("a");
	 LockSupport.unpark(thread);//再次释放许可,也是可以的。
	 System.out.println("b");
     LockSupport.park();// 获取许可
     System.out.println("c");
	 LockSupport.park();//不可重入,导致阻塞
     System.out.println("d");
}
 以上代码中只会打印出:a,b,c。不会打印出c。因为第二次调用park的时候,线程无法获取许可从而导致阻塞。

3. 支持被中断

public static void main(String[] args) throws Exception {
		Thread t = new Thread(new Runnable() {
			private int count = 0;

			@Override
			public void run() {
				long start = System.currentTimeMillis();
				long end = 0;

				while ((end - start) <= 1000) {
					count++;
					end = System.currentTimeMillis();
				}

				System.out.println("before park.count=" + count);

				LockSupport.park();//被阻塞
				System.out.println("thread over." + Thread.currentThread().isInterrupted());

			}
		});

		t.start();

		Thread.sleep(5000);

		t.interrupt();

		System.out.println("main over");
}
当线程t执行 LockSupport.park()的时候,由于许可默认被占用,所以被阻塞,但是主线程在5秒只后对t线程进行了打断,导致LockSupport.park()被唤醒,打印出thread over.true。由此可见线程如果因为调用park而阻塞的话,能够响应中断请求(中断状态被设置成true),但是不会抛出InterruptedException 。

4. 唤醒信号不担心丢失

在wait/notify/notifyAll模式的阻塞/唤醒机制中,我们必须要考虑 notify和wait调用的时序性,避免在wait方法调用之前调用了notify,从而导致错过唤醒信号,使应用永远等待。而LockSupport的unpark()方法可以在park()方法调用之前、之后甚至同时执行,都可以达到唤醒线程的目的。并且park和Object.wait()本质实现机制不同,两者的阻塞队列并不交叉,object.notifyAll()不能唤醒LockSupport.park()阻塞的线程。

5. 方便线程监控与工具定位 

在LockSupport类中存在parkBlocker的getter、setter方法, 可以看到它是通过unsafe运用Thread类的实例成员属性parkBlocker的偏移地址获取对应线程的parkBlocker成员属性的值。

private static final long parkBlockerOffset;

static{
try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> tk = Thread.class;
            parkBlockerOffset = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("parkBlocker"));
        } catch (Exception ex) { throw new Error(ex); }

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

private static void setBlocker(Thread t, Object arg) {
        // Even though volatile, hotspot doesn't need a write barrier here.
        UNSAFE.putObject(t, parkBlockerOffset, arg);
}

 这个parkBlocker对象是用来记录线程被阻塞时被谁阻塞的。可以通过LockSupport的getBlocker获取到阻塞的对象.用于线程监控和分析工具来定位原因的。

猜你喜欢

转载自pzh9527.iteye.com/blog/2416459