正确理解线程WAITING状态

正确理解线程WAITING状态

今天来学习下,Java的线程状态,重点讨论下thread.state.WAITING。讨论下线程如何进入此状态,以及它们之间的区别。最后,我们进一步了解java.util.concurrent.locks.LockSupport,它提供了几种用于同步的静态实用方法。

线程状态

在JDK Thread源代码中存在一个内部枚举定义,它定义了Java 线程的各种状态

public enum State {
        // 新建状态
        NEW,
        //运行状态 包含操作系统就绪、运行两种状态
        RUNNABLE,
        //阻塞状态
        BLOCKED,
        //等待状态
        WAITING,
				//限时等待
        TIMED_WAITING,
        //终止状态
        TERMINATED;
    }

img

线程状态之间的转换如上图(图片来源于互联网)。

进入WAITING态

JDK内部提供多种方式让线程进入WAITING状态,如

Object.wait()

Java开发者将线程状态置于WAITING状态的最标准方法之一就是调用wait()方法。当线程拥有一个对象的monitor锁时,开发者可以通过调用wait()方法暂停该线程让;其他线程执行,当其他线程执行完毕后,调用notify()、notifyAll()方法唤醒被暂停的线程。暂停期间,线程处于WAITING状态,这会在线程的DUMP文件中体现出来。如下

"WAITING-THREAD" #11 prio=5 os_prio=0 tid=0x000000001d6ff800 nid=0x544 in Object.wait() [0x000000001de4f000]
   java.lang.Thread.State: WAITING (on object monitor)

Thread.join()

另一种让线程暂停的方式是调用 join方法。当主线程需要等待工作线程执行完毕才执行时,开发者在主线程中通过调用工作线程实例的join()方法。此时主线程将进入WAITING状态。可以使用jstack命令查看线程状态

"MAIN-THREAD" #12 prio=5 os_prio=0 tid=0x000000001da4f000 nid=0x25f4 in Object.wait() [0x000000001e28e000]
   java.lang.Thread.State: WAITING (on object monitor)

注意,Thread.join 有重载方法,开发者可以指定线程等待时间,无参的join方法表示一直等待

public final synchronized void join(final long millis)
    throws InterruptedException {
    ...
 }
//该方法表示等待 millis(毫秒) + nanos(纳秒) 的时间
 public final synchronized void join(long millis, int nanos)
    throws InterruptedException {
 		...
 }
public final void join() throws InterruptedException {
        join(0);
}

LockSupport.park()

最后,开发者也可以调用LockSupport的静态park()方法将线程置于WAITING状态。调用park()方法将停止当前线程执行,并且线程状态为WAITING。同样可以使用jstack命令查看线程状态

"PARKED-THREAD" #11 prio=5 os_prio=0 tid=0x000000001e226800 nid=0x43cc waiting on condition [0x000000001e95f000]
   java.lang.Thread.State: WAITING (parking)

LockSupport

如之前所述,最优雅的方式是通过LockSupport类的相关方法来暂停、启动线程。该类是Unsafe的包装类, 对线程的操作底层委托Unsafe。只是由于Unsafe被认为是一个内部Java API,开发者不应该直接使用。因此LockSupport是官方推荐操作线程的方式。实际上很多GUC(java.util .concurrent)下面的很多类底层也都使用了LockSupport。

基本使用

使用LockSupport停止线程的执行非常简单,开发者只需要调用park()方法即可。不必跟之前的方式一致,需要提供对线程对象本身的引用——代码会停止调用它的线程。示例代码如下:

package com.andy.spring.boot.docker.thread;

import java.util.concurrent.locks.LockSupport;

public class Application {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            int acc = 0;
            for (int i = 1; i <= 100; i++) {
                acc += i;
            }
            System.out.println("Work finished");
            LockSupport.park();
            System.out.println(acc);
        });
        t.setName("PARK-THREAD");
        t.start();
    }
}

由于代码中调用了park()方法,工作线程暂停,因此程序永远不会退出。启动程序,接下来我们使用jstack命令看下线程的堆栈信息

#使用jcmd命令查看正在运行的java程序
AndydeMacBook-Pro:spring-boot-docker andy$ jcmd
57778 jdk.jcmd/sun.tools.jcmd.JCmd
57768 com.andy.spring.boot.docker.thread.Application
...
AndydeMacBook-Pro:spring-boot-docker andy$ 
# 根据上面的 pid(57768) 使用jstack查看堆栈信息
jstack 57768

在这里插入图片描述

如需要在工作线程执行完毕后,退出应用程序,也非常简单只需要在代码的最后加上 **LockSupport.unpark(t);**即可

t.setName("PARK-THREAD");
t.start();
// 停顿1秒是为了让工作线程有足够的时间执行
Thread.sleep(1000);
LockSupport.unpark(t);

底层原理

  • park API内部工作原理是使用单一许可,类似于唯一的一个信号量 - Semaphore。内部使用许可管理线程状态,park()方法使用需求,unpark()方法让许可重新生效
  • 由于每个线程只能生成一个许可,因此调用多次unpark()方法是没有意义的。任意一次调用park()方法就可以禁止线程。

方法重载

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

如上,LockSupport源码里面还有一个park的重载方法,方法包含一个blocker参数。该参数并不会影响线程的执行,但是会在线程的dump日志中体现出来,目的是便于开发者诊断并发问题。

//将之前的代码 稍作修改
String syncObj = "hello";
LockSupport.park(syncObj);

使用jstack命令重新抓取线程dump日志, 仔细观察,会发现多了一段报错日志

在这里插入图片描述

总结

  • Parking vs. Waiting 两种方式为开发者都提供了相似的功能,暂停线程。那么,在日常开发中如何使用呢?通常情况下LockSupport的相关类认为是低级的API,偏底层,需要使用者非常熟悉其原地和使用方式,否则滥用会造成难以理解的死锁。而且,绝大多数情况下,Thread类的 wait()、join()方法能够满足要求。

  • 使用parking的好处是,不需要同步代码块来禁用其他线程进入临界资源。这一点非常重要,因为同步代码块会产生 happens-before的现场,会强制刷新所有变量。极端情况下,可能会降低性能。因此需要慎重使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值