Java的wait(), join(), sleep, lockSupport.park, 线程阻塞,等待,挂起等状态详解

在java中,很多时候我们忽略的基本的知识,这是很致命的,只有搞懂Thread的基础知识,才能进一步探索:reentrantLock,AQS等。

1:Thread的线程状态到底有几种? 

很多博文写的并不准确,我的答案是6种:(注意:没有所谓的就绪状态(prepare),这个状态是很多人在推断多线程执行过程自己yy的,臆想的)

public enum State {
        /**
         * Thread state for a thread which has not yet started.
            尚未启动的线程的线程状态。
         */
        NEW,

        /**
         * Thread state for a runnable thread. 可运行线程的线程状态。
         */
        RUNNABLE,

        /**
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * 等待获取锁的状态,或者等待获取重入锁的状态
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread. 线程等待状态
           调用以下方法都可以使一个线程进入等待状态 -》等待后必须通过notify(),notifyAll()才能唤醒,唤醒后才能重新去尝试获取CPU的执行权
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * 有期限的等待,超时等待。以下方法:
         *  sleep Thread.sleep
         *  Object#wait(long) 
         *  join(long) Thread.join
         *  LockSupport#parkNanos LockSupport.parkNanos
         *  LockSupport#parkUntil LockSupport.parkUntil
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * 线程结束
         */
        TERMINATED;
    }

以上源码就解释了什么是阻塞,等待,但是没有挂起,为什么没有挂起状态,因为线程挂起这种操作已经过时,不建议使用了,这里不做过多讨论,有人愿意研究的,请自行搜索:线程挂起。不要搜跟阻塞,等待的区别,很多都说错了。

2:到底如何区分阻塞,等待?

一切回归原始:假设一个场景:多个线程任务执行在同一个单核cpu上,在我们获取临界资源的时候,为了线程安全,是要加锁的,假设锁是synchronized,那么同一时刻,只有一个线程任务能获取锁资源,那么其它的线程就进入了:blocked状态,等待获取锁的状态,如上代码BLOCKED,  此时,这些没有获取到锁的线程有一些条件:1:他们都是可以随时被CPU上下文切换获取到执行权的。2:他们虽然可以获取CPU执行的时间片,但是他们无法获取锁。所以被阻塞在这里了。

基于以上两点,那么他们处于阻塞状态。而第一个获取到锁的线程,是runnable状态。

如果有大量线程为了竞争同一把锁(同一临界资源)而发生大面积阻塞,就会形成线程饥饿。

那什么是等待?还是上面的场景,加入有一个线程(第一个线程)获取到了锁,那么其它任何线程尝试获取锁都会失败,一旦失败,调用方法让他们进入等待的话,他们就不能再获取到CPU执行权了,跟上面所说的阻塞特征的第一点冲突,另外,他们也不会一直尝试去获取锁了,因为他们拿不到CPU执行权,相当于一个瘫痪的人没有CPU的帮扶,是无法站起来的。 那么他们如何才能站起来,继续有机会获取CPU执行权,进而获取锁呢? --唤醒-》notify。 所以,假如一个线程是wait状态已经,如果没有任何线程去唤醒它,那它永远是死的,如果大量的线程一旦永无机会被唤醒的话,将会占用大量内存。

引申思考:lockSupport的park()与unpark()详解:

翻看源码我们可以知道:lockSupport类里面的方法都是两个一起出现的,一个有参,一个无参的。(重载),那么为什么呢?

public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);

有参函数都有一个blocker,这个blocker是用来记录线程被阻塞时被谁阻塞的。用于线程监控和分析工具来定位原因的。我们分析dump文件的时候,如果有这个blocker就可以定位到具体那个地方,而没有就不一样。

与wait()和notify()、notifyAll() 区别:

(1)wait和notify都是Object中的方法,在调用这两个方法前必须先获得锁对象,但是park不需要获取某个对象的锁就可以让此线程进入等待状态。

(2)notify只能随机选择一个线程唤醒,无法唤醒指定的线程,unpark却可以唤醒一个指定的线程。

  (3)根据第一条wait在获取到锁对象以后,再执行,再自动释放锁的。而park如果在锁内部(即同步代码块内)进行执行,不会释放锁,因为它跟sleep一样,不涉及锁。验证如下:

public class ThreadWaitDemo {

    private static final Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t01 = new Thread(() -> {
            synchronized (obj) {
                System.out.println("运行中: Thread.currentThread().getName() = " + Thread.currentThread().getName() + "获取到了锁");
                LockSupport.parkNanos(9000000000L);//9秒
                System.out.println("t01线程运行结束,释放锁----");
            }
        }, "t01");

        t01.start();

        TimeUnit.SECONDS.sleep(1);
        System.out.println("t01.getState() = " + t01.getState());

        System.out.println("main线程尝试去获取锁----------");
        synchronized (obj) {
            System.out.println("main线程获取到锁");
        }

        System.out.println("结束运行: Thread.currentThread().getName() = " + Thread.currentThread().getName());

    }


运行结果:

运行中: Thread.currentThread().getName() = t01获取到了锁
t01.getState() = TIMED_WAITING
main线程尝试去获取锁----------
t01线程运行结束,释放锁----
main线程获取到锁
结束运行: Thread.currentThread().getName() = main

所以,通过以上代码验证,我们知道在同步代码块,也就是在锁中使用locksupport是不合理的,应尽量避免这样的操作,以免产生线程永远无法唤醒的bug。

3:wait() 和 join() 的区别?

从第一段博文我们知道,这两个方法可以让线程进入wait状态,他们的区别?基于以上两点,查看API,不难学会如何使用。这里不啰嗦讲解,不懂的朋友可以去查。

这里主要说:join方法的底层执行? 看如测试用例: -> 

两个红色的框分别说明了sleep方法前后test01的状态,这个不是重点,只是一个验证而已。

重要的是test02的状态,调用join方法后,test02变成了WAITING状态,为什么? 

看join()源码:

    public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                 //注意这里:join方法的底层还是调用了wait()
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

join方法加了锁,锁的是谁?test01,谁持有锁?test02,搞清楚这两个地方后,那么:

我们可以看出:是test02线程在调用 join() -> wait(), 所以test02会进入WAITING状态。直到test01执行完,自动调用Thread.exist()方法,在exist()方法中,test02会被唤醒。具体细节如何,请自行百度exist()即可。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值