技术文章

高并发必备篇(四):线程的状态、调度和操作方法(上)

之前的文章中我们已经介绍了线程的创建方式,以及线程并发的现象和原理结构,我们已经对于解决线程并发问题有了了解,但是在做线程并发安全的问题之前呢,我们先了解下Java中线程的几个状态、线程的调度以及线程的一些操作方法。

 

1. Java线程的状态

我们知道当我们创建了Thread对象,并调用start方法之后,我们的线程就运行起来了,但是线程运行起来之后处于一个什么样的状态,我们又如何对线程的状态进行转换呢?

 

其实呢Java中对于线程总共设定了5个状态,分别为:新建状态、就绪状态、运行状态、阻塞状态、死亡状态。并且在任意一时间点一个线程只能有一个状态。线程5种状态介绍如下:

 

● 新建状态(New):顾名思义就是我们通过new Thread() 创建了线程,但是还并未启动线程。

 

● 就绪状态(Runnable):当其他线程调用start 方法启动该线程的时候,线程首先会进入准备就绪状态也被称为“可运行状态”,随时等待线程调度程序获取CPU的执行时间(即CPU时间片)。

 

● 运行状态(Running):,线程调度程序一旦获取到了CPU的执行时间线程就进入运行状态并执行线程的程序代码。

 

● 阻塞状态(Blocked):阻塞状态是指线程因为某种原因放弃了cpu 使用权,让出了cpu的执行时间。直到线程进入“可运行状态”,才有机会再次获得cpu 执行时间 转到“运行状态”。导致线程阻塞主要有三种情况:

 

01.无限等待:

当调用了没有时间参数的Object.wait()、Thread.join()、

LockSupport.park()等方法,当前线程就会处于无限等待状态,这种等待需要其他线程显示的唤醒才能重新获取CPU执行时间进入运行状态,例如:调用Object.notify()可以唤醒调用Object.wati()阻塞的线程。

 

02.限时等待:

当调用了

Thread.sleep()、Object.wait(timeout)、

Thread.join(millis)LockSupport.parkNanos(nanos)、LockSupport.parkUnit(deadline)等方法,当前线程也会处于等待状态,但是无须等待被其他线程显示的唤醒,在一定时间后它们会由系统自动唤醒。

 

03.同步等待:

运行的线程在获取对象的synchronized同步锁时,若该同步锁被别的线程占用则获取失败,JVM会把该线程放入锁池(lock pool)中,它会进入同步阻塞状态。等占用同步锁的线程释放了同步锁之后,线程就会再次尝试获取同步锁,如果获取成功则进入运行状态。

 

● 死亡状态(Dead):当线程执行代码运行完之后或者因为异常退出了run方法,那么该线程都会结束其生命周期。

 

下面这张图就很好的介绍了线程的5个状态以及转换过程:

 

高并发必备篇(四):线程的状态、调度和操作方法(上)

 

2. Java线程的调度

之前提到了线程获取到CPU执行时间的时候就会进入运行状态,否则就会再可运行状态或者阻塞状态,那么系统是如何分配线程时间片以及实现线程的调度的呢?下面我们就来讲讲线程的调度策略。

 

线程调度是指系统为线程分配CPU执行时间片的策略方式,主要调度方式有两种:协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)。

 

● 协同式线程调度

该调度策略模式下线程的执行时间由线程本身来控制,某一线程执行完了之后,会主动通知系统切换到另外一个线程上执行(可以想象下类似排队一样的场景)。

 

协同式多线程的最大好处是实现简单,而且由于线程获取执行时间和切换由自己控制,切换操作对线程自己是可知的,所以没有什么线程同步的问题。

 

但是它缺点很明显:如果一个线程编写有问题,那么线程运行了一部分之后就一直堵塞,一直不告诉系统进行线程切换,进程一直不让CPU执行时间严重时可能导致整个系统崩溃。

 

高并发必备篇(四):线程的状态、调度和操作方法(上)

 

● 抢占式线程调度

该调度策略模式下每个线程的执行时间以及线程的切换都将有系统分配和控制;在这种实现线程调度的方式下,线程的执行时间是系统可控的,可能一个线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片,这种调度策略下如果某个线程阻塞了也不会导致整个进程阻塞的问题,Java使用的线程调度方式就是抢占式调度。

 

高并发必备篇(四):线程的状态、调度和操作方法(上)

 

3. 线程操作的主要方法

Java中既然是抢占式线程调度模式,那么我们哪些操作方法可以让线程获取CPU的执行时间片呢?又有哪些操作可以阻塞线程呢?下面我们就来看看线程常见的一些操作方法。

 

(1)获取当前线程及其Name信息

Thread.currentThread(); 表示获取当前正在执行的线程,这样我们就可以操作当前的线程。例如获取当前线程的名称,或者设置名称:

 

高并发必备篇(四):线程的状态、调度和操作方法(上)

 

Java程序运行的话都是执行的主线程main线程,而其他线程都是在主线程中new出来的也被称为“子线程”,上面的代码我们在main方法中执行的,所以返回的结果为主线程的名字“main”。同样也可以通过setName 方法修改线程的名称。

 

高并发必备篇(四):线程的状态、调度和操作方法(上)

 

(2) 线程的睡眠-sleep

Thread.sleep(millis);  sleep方法是Thread类中的静态方法所以可以直接调用,调用了sleep方法,那么当前线程会让出CPU的执行时间而进入阻塞等待状态,等待的时间一到就会再次进入就绪状态抢夺CPU的执行时间片。使用案例如下:

 

高并发必备篇(四):线程的状态、调度和操作方法(上)

 

代码的意思就是i会每隔50 毫秒输出一次。

分析:Thread.sleep()的好处在于短时间内可以让出cpu资源给其他线程运行,并且睡眠时间一到就会自动苏醒到就绪状态然后到运行状态,并且Thread.sleep()只能睡眠当前的线程。

 

Thread.sleep(0)的妙用:

很多人一看到sleep(0)就认为线程睡眠0秒那不是毫无意义吗?其实sleep(0)并不是阻塞0秒的意思,Thread.sleep(0)表示当前的线程暂时放弃CPU的执行时间让给其他的线程或进程使用CPU资源,而自身线程马上进入就绪状态而不是阻塞等待状态。

 

这样既可以使得系统可以做到适当切换执行的线程,也不影响当前线程的竞争从而可以提示系统执行的效率。

 

TimeUnit 线程睡眠的便利使用:

当我们希望我们的线程睡眠时间特别久的时候,如果我们使用Thread.sleep()方法发现时间计算比较麻烦,并且不直观,比如我们需要线程睡眠3个小时10分钟,如果我们使用Thread.sleep()那么就需要计算3个小时10分钟换算成毫秒值为多少,太麻烦了。怎么办?Java中的TimeUnit提供了优雅简单的调用方式,如下:

高并发必备篇(四):线程的状态、调度和操作方法(上)

 

同样方法还有

TimeUnit.DAYS、TimeUnit.SECONDS 等。

 

OK,今天的干货分享就先讲到这里,下期文章我们继续为大家带来高并发必备篇之线程的操作方法(下)。

 

【未完待续…】