关于java:通俗易懂的JUC源码剖析LockSupport

38次阅读

共计 3352 个字符,预计需要花费 9 分钟才能阅读完成。

前言

LockSupport 是 rt.jar 下的工具类,它的作用是挂起和唤醒线程,它在 JUC 很多同步组件中都会用到,比方 AQS。LockSupport 类与每个应用它的线程都会关联一个许可证,在默认状况下调用 LockSupport 类办法的线程是没有许可证的。LockSupport 外部是用 Unsafe 的 park 和 unpark 办法实现的。

次要办法

(1)void park()

调用 park()办法会阻塞以后线程,除非以后线程曾经拿到了与 LockSupport 关联的许可证。当其余线程调用 unpark(Thread thread)办法并且将以后线程作为参数时,或者调用以后线程的 interrupt()办法时,以后线程会从阻塞状态返回。然而,须要留神的是,与调用 sleep(),wait()等办法不同,调用 park()办法被中断返回时,不会抛出 InterruptedException 异样。还有须要留神的是,park()办法返回时不会通知你是什么起因返回(unpark 或 interrupt),但咱们能够依据调用前后的中断标记是否扭转来判断是否因为 interrupt 返回的。

(2)void unpark(Thread thread)

线程调用 unpark 办法时,如果参数线程 thread 没有持有许可证,则让其持有,如果 thread 在此前因调用 park 而被阻塞,则调用 unpark 后,该线程会被唤醒。如果 thread 之前没有调用 park,则调用 unpark 后,再调用 park,线程会立刻返回,而不会阻塞挂起。用代码来阐明:

import java.util.concurrent.locks.LockSupport;
public class UnparkDemo {public static void main(String[] args) {System.out.println("give current thread a permit");
        LockSupport.unpark(Thread.currentThread());
        System.out.println("current thread begin park");
        LockSupport.park();
        System.out.println("current thread end park");
    }
}

输入后果如下:
以后线程调用 park 办法后并没有阻塞挂起,而是立刻返回了,因为此前调用 unpark 给了它许可证。

(3)void parkNanos(long nanos)

和 park 办法相似,但有个超时参数,如果期待了 nanos(单位纳秒)工夫后,以后线程没有被唤醒,会主动返回。

(4)void park(Object blocker)

以后线程调用 park(blocker)被挂起时,blocker 对象会被记录在以后线程外部,它的作用是当咱们应用诊断工具 (如 jstack 等)dump 线程时,能够察看到线程被阻塞在哪个类下面。诊断工具是通过 getBlocker() 拿到 blocker 对象的。因而 JDK 举荐咱们应用带有 blocker 参数的 park 办法,例如 LockSupport.park(this)。同样用代码来阐明下:

import java.util.concurrent.locks.LockSupport;
public class ParkWithBlockerDemo {public void parkWithBlocker() {LockSupport.park(this);
    }
    public static void main(String[] args) {ParkWithBlockerDemo demo = new ParkWithBlockerDemo();
        demo.parkWithBlocker();}
}

程序运行后,线程会被阻塞挂起,此时先用 jps 命令找到它的 pid,而后用 jstack pid 执行,会有如下后果:

不加 blocker 参数的话,是不会有这个标记的,感兴趣的同学能够去试试。
park(blocker)办法源码:

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

代码逻辑很清晰明了,原理就是真正 park 之前,将 blocker 对象绑定在以后 Thread 外部的某个变量中,等 park 返回后,解除绑定。其中 setBlocker 源码如下:

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

其中 parkBlockerOffset 是 Thread 类 parkBlocker 变量的内存偏移量。

private static final long parkBlockerOffset;
parkBlockerOffset = UNSAFE.objectFieldOffset
    (tk.getDeclaredField("parkBlocker"));

(5)parkUntil(long deadline)

与 parkNanos 不同的是,deadline 是个相对工夫戳,示意到某个工夫点后,以后线程会被唤醒。

结束语

最初再来看个栗子来加深 LockSupport 的了解。

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
public class FIFOMutex {private final AtomicBoolean locked = new AtomicBoolean(false);
    private final Queue<Thread> waiters = new ConcurrentLinkedDeque<>();
    public void lock() {
        boolean wasInterrupted = false;
        Thread current = Thread.currentThread();
        // 把以后线程放到队列开端
        waiters.add(current);
        // 只有队列头部的线程能力获取锁
        while (waiters.peek() != current || locked.compareAndSet(false, true)) {LockSupport.park(this);
            // 记录是否被中断过,并重置中断标记,这里其实就是疏忽中断
            if (Thread.interrupted()) {wasInterrupted = true;}
        }
        // 移除队列头部元素,即胜利获取锁的以后线程
        waiters.remove();
        // 从新响应之前的中断
        if (wasInterrupted) {current.interrupt();
        }
    }
    public void unlock() {locked.set(false);
        // 唤醒本来处于队列第二,当初处于头部的线程,让它尝试获取锁
        LockSupport.unpark(waiters.peek());
 }
}

这是一个先入先出的锁,也就是只有队列的头部线程能力获取锁,如果以后线程不是头部元素,或者锁曾经被其余线程获取,则调用 park 办法阻塞本人。

这里的 wasInterrupted 怎么了解呢?如果 park 办法是因为被中断而返回的,则疏忽中断,只记录 wasInterrupted=true。因为这阐明不是其余线程调用理解锁办法 unlock()外面的 unpark 让它返回的,它还不能获取锁,得持续阻塞。尽管以后线程对中断标记不感兴趣,但不代表其余线程也不敢趣味,所以在胜利获取锁后,从新对本人调用 interrupt,复原下中断标记的值。

参考资料:
《Java 并发编程之美》

正文完
 0