1、简介
LockSupport是一个线程工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,也可以在任意位置唤醒。
它的内部其实两类主要的方法:park(停车阻塞线程)和unpark(启动唤醒线程)。
/**
* Basic thread blocking primitives for creating locks and other
* synchronization classes.
*
* <p>This class associates, with each thread that uses it, a permit
* (in the sense of the {@link java.util.concurrent.Semaphore
* Semaphore} class). A call to {@code park} will return immediately
* if the permit is available, consuming it in the process; otherwise
* it <em>may</em> block. A call to {@code unpark} makes the permit
* available, if it was not already available. (Unlike with Semaphores
* though, permits do not accumulate. There is at most one.)
*/
public class LockSupport {
/**
* Makes available the permit for the given thread, if it
* was not already available. If the thread was blocked on
* {@code park} then it will unblock. Otherwise, its next call
* to {@code park} is guaranteed not to block. This operation
* is not guaranteed to have any effect at all if the given
* thread has not been started.
*
* @param thread the thread to unpark, or {@code null}, in which case
* this operation has no effect
*/
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
/**
* Disables the current thread for thread scheduling purposes unless the
* permit is available.
*
* <p>If the permit is available then it is consumed and the call
* returns immediately; otherwise the current thread becomes disabled
* for thread scheduling purposes and lies dormant until one of three
* things happens:
*
* <ul>
*
* <li>Some other thread invokes {@link #unpark unpark} with the
* current thread as the target; or
*
* <li>Some other thread {@linkplain Thread#interrupt interrupts}
* the current thread; or
*
* <li>The call spuriously (that is, for no reason) returns.
* </ul>
*
* <p>This method does <em>not</em> report which of these caused the
* method to return. Callers should re-check the conditions which caused
* the thread to park in the first place. Callers may also determine,
* for example, the interrupt status of the thread upon return.
*/
public static void park() {
UNSAFE.park(false, 0L);
}
}
我们知道wait/notify也是用来阻塞和唤醒线程的,那么与它相比,LockSupport有什么优点呢?
2、与wait/notify对比
(1)wait/notify代码
public class WaitNotifyTest {
public static void main(String[] args){
final Object lock = new Object();
Thread t0 = new Thread(()->{
synchronized (lock){
System.out.println(Thread.currentThread().getName()+" wait");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" end");
}
});
Thread t1 = new Thread(()->{
synchronized (lock){
System.out.println(Thread.currentThread().getName()+" notify");
lock.notify();//执行notify,当前线程不会释放锁
System.out.println(Thread.currentThread().getName()+" end");
}
});
// 保证t0先启动,锁对象必须先执行wait后执行notify
t0.start();
t1.start();
}
}
执行结果
Thread-0 wait
Thread-1 notify
Thread-1 end
Thread-0 end
(2)LockSupport代码
public class LockSupportTest {
public static void main(String[] args){
Thread t1 = new Thread(()->{
System.out.println(Thread.currentThread().getName()+" start");
System.out.println(Thread.currentThread().getName()+" park");
LockSupport.park();
System.out.println(Thread.currentThread().getName()+" unpark");
System.out.println(Thread.currentThread().getName()+ " end");
});
t1.start();
System.out.println(Thread.currentThread().getName()+" start");
try {
System.out.println(Thread.currentThread().getName()+" sleep");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" end");
LockSupport.unpark(t1);//唤醒指定线程
}
}
执行结果
Thread-0 start
main sleep
Thread-0 park
----------------1s后执行下面的输出-------------------
main end
Thread-0 unpark
Thread-0 end
结论:
使用wait,notify来实现等待唤醒功能的特点如下:
1)wait和notify都是Object中的方法,在调用这两个方法前必须先获得锁对象,这限制了其使用场合:只能在同步代码块中。
2)当对象的等待队列中有多个线程时,notify只能随机选择一个线程唤醒,无法唤醒指定的线程。
而使用LockSupport的话,我们可以在任何场合使线程阻塞,同时也可以指定要唤醒的线程,相当的方便。
3、LockSupport的其他特点
(1)可以先唤醒线程,再阻塞线程。先unpark(),在park()
public static void main(String[] args) {
Thread t = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println(i);
if(i == 5) {
LockSupport.park();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
LockSupport.unpark(t);
}
程序功能:线程t每隔1s打印一个数字,当i==5时阻塞,等待唤醒继续执行。上面的线程在执行LockSupport.park();之前,先执行了 LockSupport.unpark(t);
运行结果如下:
0
1
2
3
4
5
6
7
8
9
i==5时并未阻塞而是继续执行
为什么?
线程阻塞需要消耗许可证(permit),这个凭证最多只有1个。当调用park方法时,如果有许可证,则会直接消耗掉这个凭证继续执行;但是如果没有凭证,就必须阻塞等待许可证可用;
而unpark则相反,它会增加一个许可证,但凭证最多只能有1个。但是每次park都需要消耗一个许可证
原理浅析
class Parker : public os::PlatformParker {
private:
volatile int _counter ;
...
public:
void park(bool isAbsolute, jlong time);
void unpark();
...
}
class PlatformParker : public CHeapObj<mtInternal> {
protected:
pthread_mutex_t _mutex [1] ;
pthread_cond_t _cond [1] ;
...
}
LockSupport就是通过控制变量_counter来对线程阻塞唤醒进行控制的。原理有点类似于信号量机制。
- 当调用park()方法时,先尝试直接能否直接拿到“许可”,即_counter>0时,如果成功,则把_counter设置为0,并返回。
如果不成功,则构造一个ThreadBlockInVM,然后检查_counter是不是>0,如果是,则把_counter设置为0,unlock mutex并返回:
否则,再判断等待的时间,然后再调用pthread_cond_wait函数等待,如果等待返回,则把_counter设置为0,unlock mutex并返回:
这就是整个park的过程,总结来说就是消耗“许可”的过程
- 当调用unpark()方法时,会将_counter置为1,同时判断前值,小于1会进行线程唤醒,否则直接退出。
Q&A
(1)为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个许可证,之后调用park因为有许可证消费,故不会阻塞。
(2)为什么唤醒两次后阻塞两次会阻塞线程。
因为许可证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个许可证;而调用两次park却需要消费两个许可证。