Thread.sleep(time)

  • 该方法必须传入指定的时间,线程将进入休眠状态,通过jstack输出线程快照的话此时该线程的状态应该是TIMED_WAITING,表示休眠一段时间。
  • 该方法会抛出InterruptedException异常,这是受检查异常,调用者必须处理。
  • 通过sleep方法进入休眠的线程不会释放持有的锁,因此,在持有锁的时候调用该方法需要谨慎。

Object.wait() 方法

java的每个对象都隐式的继承了Object类。因此每个类都有自己的wait()方法。我们通过object.wait()方法也可以让线程进入休眠。wait()有3个重载方法:

public final void wait() throws InterruptedException;

public final native void wait(long timeout) throws InterruptedException;

public final native void wait(long timeout) throws InterruptedException;

如果不传timeout,wait将会进入无限制的休眠当中,直到有人唤醒他。使用wait()让线程进入休眠的话,无论有没有传入timeout参数,线程的状态都将是WAITING状态。

如果传入了timeout,但是到时间后未获取到锁,或未竞争过其它的wait(),则如何处理 [todo]

另外,必须获得对象上的锁后,才可以执行该对象的wait方法。否则程序会在运行时抛出IllegalMonitorStateException异常。

再调用wait()方法后,线程进入休眠的同时,会释放持有的该对象的锁,这样其他线程就能在这期间获取到锁了。

调用Object对象的notify()或者notifyAll()方法可以唤醒因为wait()而进入等待的线程。

wait唤醒

  • 当wait指定时间时,到时间后会所有线程自动唤醒,唤醒时竞争到锁的继续执行代码,如果唤醒时未竞争到锁,则进入sychronized竞争锁的过程,竞争到锁后继续执行之后的代码
  • 当wait指定时间时,未到时间,其它线程执行notify时,虽然未到timeout时间,仍会唤醒一个wait中的线程,如果执行notifyAll方法,则会唤醒所有的线程,这些线程依次竞争锁
  • 当wait未指定时间,notify会唤醒一个wait中的线程,其它的线程仍然wait中;notifyall唤醒所有的线程,并竞争锁然后执行后续代码

LockSupport.park() 方法

通过LockSupport.park()方法,我们也可以让线程进入休眠。它的底层也是调用了Unsafe类的park方法:

//Unsafe.java类

//唤醒指定的线程

public native void unpark(Thread jthread);

//isAbsolute表示后面的时间是绝对时间还是相对时间,time表示时间,time=0表示无限阻塞下去

public native void park(boolean isAbsolute, long time);

调用park方法时,还允许设置一个blocker对象,主要用来给监视工具和诊断工具确定线程受阻塞的原因。

调用park方法进入休眠后,线程状态为WAITING。

实现原理

LockSupport.park() 的实现原理是通过二元信号量做的阻塞,要注意的是,这个信号量最多只能加到1。我们也可以理解成获取释放许可证的场景。unpark()方法会释放一个许可证,park()方法则是获取许可证,如果当前没有许可证,则进入休眠状态,知道许可证被释放了才被唤醒。无论执行多少次unpark()方法,也最多只会有一个许可证。

和wait的不同

park、unpark方法和wait、notify()方法有一些相似的地方。都是休眠,然后唤醒。但是wait、notify方法有一个不好的地方,就是我们在编程的时候必须能保证wait方法比notify方法先执行。如果notify方法比wait方法晚执行的话,就会导致因wait方法进入休眠的线程接收不到唤醒通知的问题。而park、unpark则不会有这个问题,我们可以先调用unpark方法释放一个许可证,这样后面线程调用park方法时,发现已经许可证了,就可以直接获取许可证而不用进入休眠状态了。

另外,和wait方法不同,执行park进入休眠后并不会释放持有的锁。

另外wait是对某个对象进行加锁,存在多个线程竞争的情况,则park只对当前线程挂起

和sleep的不同

sleep是只能到时间后自动醒来,而park可以在其它程序中通过unpark唤醒

对中断的处理

park方法不会抛出InterruptedException,但是它也会响应中断。当外部线程对阻塞线程调用interrupt方法时,park阻塞的线程也会立刻返回。

WX20220228-153757@2x

上面的demo最终会输出

park begin

main over

thread over.true

说明因park进入休眠的线程收到中断通知后也会立刻返回,并且可以手动通过Thread.currentThread().isInterrupted()获取到中断位。

unpark处理

因为在unpark时是指定thread的,所以不存在唤醒多个线程的情况,只会唤醒当前指定的线程,而Object wait时是通过object实例wait的,所以通过notify唤醒时可能会存在多个wait线程竞争的情况

LockSupport的park与unpark调用顺序验证

unpark在thread start之前调用

public static void main(String[] args) throws InterruptedException{
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("thread1 start");

            try{
                Thread.sleep(2000);
            } catch (Exception e){
                e.printStackTrace();
            }

            System.out.println("thread1 run before park");

            LockSupport.park();

            System.out.println("thread1 run after park");
        }
    });
    LockSupport.unpark(thread1);
    thread1.start();

    System.out.println("main thread");

}

执行结果:

main thread
thread1 start
thread1 run before park
(程序没有退出,线程1一直挂起)

park在park之前执行

public static void main(String[] args) throws InterruptedException{
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("thread1 start");

            try{
                Thread.sleep(2000);
            } catch (Exception e){
                e.printStackTrace();
            }

            System.out.println("thread1 run before park");

            LockSupport.park();

            System.out.println("thread1 run after park");
        }
    });
    thread1.start();
    LockSupport.unpark(thread1);

    System.out.println("main thread");

}

执行结果:

thread1 start
main thread
thread1 run before park
thread1 run after park

unpark在park之后执行

public static void main(String[] args) throws InterruptedException{
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("thread1 run before park");

            LockSupport.park();

            System.out.println("thread1 run after park");
        }
    });

    thread1.start();

    System.out.println("main thread");
    Thread.sleep(3000);
    LockSupport.unpark(thread1);
}

执行结果:

main thread
thread1 run before park
thread1 run after park

unpark总结

1.unpark必须在thread start之后才有用,之前调用没有任何效果;

2.thread start之后,unpark在park之前还是之后,作用是一样的,都会重新唤醒线程;

java进程的关闭

在linux中,我们通常用kill命令来关闭一个进程。众所周知,kill有-9和-15两种参数,默认是-15。如果是-15参数,系统就发送一个关闭信号给进程,然后等待进程关闭。在这个过程中,目标进程可以释放手中的资源,以及进行一些关闭操作。

正是有了这个概念,我曾经很大一段时间对java进程的关闭流程有所误解。在我原先的理解中,java进程接收到关闭信号后,会逐一给阻塞中的进程发送中断信号,并等待线程处理完。但其实这是错误的。

java进程收到关闭信号后,不会去关心运行中的那些线程是否运行完,也不会给阻塞中的线程发送中断信号。我们只能通过绑定关闭钩子来中断目标线程并等待线程执行完。

WX20220228-154431

java进程在收到关闭信号后,会执行所有绑定了shutdownHook的线程,确保这些绑定的线程都执行完了才真正关闭。因此,我们要释放资源就要在shutdownHook的线程内操作,然后在线程内等待其他释放资源的线程执行完成。

注意,所有绑定了shutdownHook的线程也是并行执行的,不是顺序执行。另外,用-9参数的kill不会等shutdownHook线程执行完就退出。

总结

5bff9535e4b04dd2799a6ae8

参考