多线程系列-1 线程的状态

前言:

本文是多线程体系的第三篇,主要介绍线程的状态以及线程状态的切换过程。涉及的知识点属于线程的基础知识,涉及线程调度的知识本文一带而过,在后续多线性体现博客中讲解。

1.线程的状态

线程的生命周期中会经历以下几个过程:
(1) new状态:线程实例被创建之后。
(2) runable状态:线程实例调用了start方法之后。
(3) running状态:处于runable状态的线程得到了cpu资源,开始运行。
(4) blocked状态:线程处于阻塞状态。
(5) dead状态:线程运行完成或者遇到异常退出。

上述的5种状态可以用下图表示:
在这里插入图片描述
上图不仅包括了线程的状态,也包括了各个状态的相互切换过程。
不妨用thread_new 表示new状态的线程,thread_runnable表示new状态的线程,以此类推。
thread_new在调用start方法后称为runnabel, thread_running将程序运行完成或者抛出异常时退出运行;这两个过程一目了然,比较简单。

下面详细介绍一下Runnabel,Running,Blocked这三者之间的切换。
Runnabel,Running,Blocked之间的切换涉及到两个概念:锁池(Entry Set)、等待队列(Wait Set)。

(1)所有期待获得锁的线程,在锁已经被其它线程拥有的时候,这些期待获得锁的线程就进入了Object Lock的entry set区域。
(2)所有曾经获得过锁,但是由于其它必要条件不满足而需要wait的时候,线程就进入了Object Lock的wait set区域 。
(3)在wait set区域的线程获得Notify/notifyAll通知的时候,随机的一个Thread(Notify)或者是全部的Thread(NotifyALL)从ObjectLock的wait set区域进入了entry set中。
(4)在当前拥有锁的线程释放掉锁的时候,处于该Object Lock的entryset区域的线程都会抢占该锁,但是只能有任意的一个Thread能取得该锁,而其他线程依然在entry set中等待下次来抢占到锁之后再执行。

对于每一个锁对象,都有一个Entry Set(锁池)和一个Wait Set(等待队列),用于存放线程。
上图可以表述为以下几个场景:
场景一:线程A获取锁Lock的时候,如果此时Lock已被线程B获取,A就会存放在Lock对象的Entry Set中而处于Blocked状态,直到B释放了锁,才有可能获得Lock锁,从而进去Runnable状态,等待被调度。
在这里插入图片描述
场景二:对于调用wait方法的线程,该线程会先处于锁对象的Wait Set,从而处于Blocked状态;当调用notify时会随机从等待池中选择一个线程放入锁对象的Entry set, 线程之后的流程与场景一种相同。
在这里插入图片描述
因为线程调用wait方法后,会释放锁,所以线程不是直接离开wait set变成Runnabel, 而是进入Entry set去竞争锁。与之相反,sleep休眠时不释放锁,所以休眠完成后线程可以直接恢复成Runnable状态。

场景三:线程执行IO操作时,或者调用了threadB.join(),或者调用了Thread.sleep()时,会暂时释放CPU资源,线程临时处于Blocked状态;等到这些操作执行完成后,线程再度回到Runnable状态,等待调度。
这么设计是提高程序的效率,进一步压榨cpu:当程序IO或者sleep时,是不需要cpu参与的,必须等到操作完成了,才会被调度,进而得到cpu的使用权。如果IO时仅仅让出了cpu,并没有将线程置于Blocked且等IO完成才转为Runnable,会导致该线程即使处于IO状态,也仍然会被调度到,然后由于IO继续让出CPU,导致频繁地上下文切换。

2.线程的调度

当线程处于Runnable时,怎么才能运行呢?即获取cpu的时间片的方式是怎样的。
线程调度分为了两种:

(1) 协调式调度 :cpu的执行时长取决于线程自己,执行完成后才会让出cpu资源;
(2) 抢占式调度: 系统控制各个线程获取cpu时间片的长度,通过对线程设置优先级来区别不同线程对cpu占用时长。

协调式会因为一个线程的阻塞而影响整个进程内的所有线程,但实现机制比较简单。

java中使用的是抢占式,线程根据系统调度来获取cpu的时间片;相对而言,优先级越高,获得cpu的可能性就越大,占用cpu的时间就越长。但是jvm的线程归根到底还是需要映射到具体操作系统上的线程。但是具体平台的优先级和jvm中定义的不一定完全一致,涉及到一个集合映射到另一个集合,因此代码的逻辑不能依赖于线程的优先级。
在这里插入图片描述

3.使用JVM Tools观察线程状态

通过jstack可以得到程序中线程状态的快照,一下通过一个例子进行介绍。

package com.shengyu;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class Application {
    private static Object obj1 = new Object();

    private static Object obj2 = new Object();

    public static void main(String[] args) throws SQLException {
        testThreadLock();
    }

    private static void testThreadLock() {
        new Thread(() -> {
            synchronized (obj1) {
                doSleep(1000); //加入sleep让死锁现象更明显 
                System.out.println("sy1 step1");
                synchronized (obj2) {
                    System.out.println("sy1 step2");
                }
            }
            System.out.println("sy1 step3");
        },"sy1").start();

        new Thread(() -> {
            synchronized (obj2) {
                doSleep(1000);
                System.out.println("sy2 step1");
                synchronized (obj1) {
                    System.out.println("sy2 step2");
                }
            }
            System.out.println("sy2 step3");
        },"sy2").start();
    }

    private static void doSleep(int mills) {
        try {
            Thread.sleep(mills);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上述代码中:
开启了两个线程sy1,sy2,sy1和sy2都需要同时获取obj1和obj2这两把锁才可以执行完成。
sy1先获取obj1,再获取oj2;
sy2先获取obj2,再获取oj1;
这样就形成了一个相互要挟的局势,造成死锁线程。
可以通过jconsole和jstack两种方式来查看线程的状态:

jconsole

在这里插入图片描述
jconsole有个好处是直接可以得到pid;此时是32160(下午的jstack需要用到)
进入线程tab栏可以看到jvm进程中的线程数量以及每个线程的状态。
在这里插入图片描述

在这里插入图片描述
比较关心的是用户线程sy1和sy2,这两个线程因为需要锁而处于BLOCKED状态;

jstack

在这里插入图片描述
jstack用法比较简单,直接jstack pid

得到线程状态的详细信息:

"sy2" #21 prio=5 os_prio=0 tid=0x00000000216f3000 nid=0x69c0 waiting for monitor entry [0x0000000021f5f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.shengyu.Application.lambda$testThreadLock$1(Application.java:33)
	- waiting to lock <0x000000076f8a82f0> (a java.lang.Object)
	- locked <0x000000076f8a8300> (a java.lang.Object)
	at com.shengyu.Application$$Lambda$2/990368553.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)

"sy1" #20 prio=5 os_prio=0 tid=0x00000000216f0800 nid=0xfcac waiting for monitor entry [0x0000000021e5f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.shengyu.Application.lambda$testThreadLock$0(Application.java:22)
	- waiting to lock <0x000000076f8a8300> (a java.lang.Object)
	- locked <0x000000076f8a82f0> (a java.lang.Object)
	at com.shengyu.Application$$Lambda$1/295530567.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)

以sy1线程为例:
#20是sy1线程的排序; prio=5表示此线程在jvm中的优先级(不指定时,默认是5); os_prio表示映射到具体的操作系统上线程的优先级(此时对于window上线程的优先级为0); tid表示jvm的线程id;
nid表示操作系统线程的线程id;waiting for monitor entry :表示此线程位于Entry set中等待获取锁。
除此之外,还详细地指出了已获得的锁,追求的锁,线程的堆栈信息,可以方便开发者快速定位。

Found one Java-level deadlock:
=============================
"sy2":
  waiting to lock monitor 0x000000001d08de78 (object 0x000000076f8a82f0, a java.lang.Object),
  which is held by "sy1"
"sy1":
  waiting to lock monitor 0x000000001d0904f8 (object 0x000000076f8a8300, a java.lang.Object),
  which is held by "sy2"

Java stack information for the threads listed above:
===================================================
"sy2":
	at com.shengyu.Application.lambda$testThreadLock$1(Application.java:33)
	- waiting to lock <0x000000076f8a82f0> (a java.lang.Object)
	- locked <0x000000076f8a8300> (a java.lang.Object)
	at com.shengyu.Application$$Lambda$2/990368553.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)
"sy1":
	at com.shengyu.Application.lambda$testThreadLock$0(Application.java:22)
	- waiting to lock <0x000000076f8a8300> (a java.lang.Object)
	- locked <0x000000076f8a82f0> (a java.lang.Object)
	at com.shengyu.Application$$Lambda$1/295530567.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)

Found 1 deadlock.

以上是jstack诊断出的死锁信息。当只有两个线程相互劫持两把锁时,可以通过上面的线程状态和持有的锁来判断线程是否有死锁现象;当用户线程数量较多时,且锁的数量较多时(A需要持有L1,L2;B需要持有L2,L3;C需要持有L3,L1…),人工判断就显得很吃力了,感谢jstack的友好型(确实,做一款软件,开发一个需求归根到底还是为了用户使用,就需要站在用户的角度–怎么设计最好用!)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值