Java开发之高并发必备篇——线程的状态调度和操作方法

Posted weixin_43802541

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java开发之高并发必备篇——线程的状态调度和操作方法相关的知识,希望对你有一定的参考价值。

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

  1. Java线程的状态

我们知道当我们创建了Thread对象,并调用start方法之后,我们的线程就运行起来了,但是线程运行起来之后处于一个什么样的状态,我们又如何对线程的状态进行转换呢?其实呢Java中对于线程总共设定了5个状态,分别为:新建状态、就绪状态、运行状态、阻塞状态、死亡状态。并且在任意一时间点一个线程只能有一个状态。线程5种状态介绍如下:

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

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

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

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

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

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

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

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

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

  1. Java线程的调度

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

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

· 协同式线程调度

该调度策略模式下线程的执行时间由线程本身来控制,某一线程执行完了之后,会主动通知系统切换到另外一个线程上执行(可以想象下类似排队一样的场景)。协同式多线程的最大好处是实现简单,而且由于线程获取执行时间和切换由自己控制,切换操作对线程自己是可知的,所以没有什么线程同步的问题。但是它缺点很明显:如果一个线程编写有问题,那么线程运行了一部分之后就一直堵塞,一直不告诉系统进行线程切换,进程一直不让CPU执行时间严重时可能导致整个系统崩溃。

· 抢占式线程调度

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

  1. 线程操作的主要方法

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 等;

(3)线程的优先级-Priority

线程同样也有优先级设定,线程的优先级从1到10,数字越大则优先级越高,如果我们创建线程而没有设置优先级的话,那么优先级就是默认的5;值得注意的是并不是优先级高的线程就一定会比优先级低的线程先执行,线程的优先级越高表示的是获取到的CPU执行时间片越多,跟优先级低的线程比抢夺到CPU执行时间的几率就越高。我们可以通过Thread的setPriority()方法来设置线程的优先级,同样也可以通过getPriority();方法获取线程的优先级。案例如下:

图片运行结果:

运行多次程序结果分析我们会发现线程2获取到的机会更大一些。

(4)线程的礼让-yeild

Thread.yeild() 是Thread类的静态方法可以直接调用表示线程的礼让,线程的礼让指的是当前线程暂时放弃CPU的执行时间礼让一下给其他线程,而礼让的同时自身进入就绪状态。因为礼让之后自身也进入就绪状态,所以yeild礼让之后自身还是有几率抢夺到CPU的执行时间,只不过一旦礼让之后优先级越高的线程抢夺到的几率越高。举个例子:好比排队买菜,某一天轮到张三买菜了,但是张三礼让了一下说“大家公平竞争,谁今天先到卖菜的地方谁就先买”而张三这么一礼让明天买菜的人就看谁先到了,有可能买菜的是他也有可能是别人。

案例如下:

运行结果:


我们发现并不是每次都能礼让成功的。虽然yeild方法可以做到礼让,但是其实在开发中用的场合很少。更多是用于调式和测试的作用,源代码说明如下:

(5)线程的插队-join

Java线程面试的时候很多人遇到过这样的问题,“Java中如何让多个线程按照自己指定的顺序执行?”没错,这个问题最简单的实现就是使用Thread的join()方法来实现了。

thread.join()的意思表示当前线程会从运行状态变为阻塞状态,需要等待插入的线程执行完终止之后,才会从thread.join的阻塞状态变为可运行状态。案例如下:

运行下代码我们就会发现,无论运行多少次,一直就是线程t1的代码先执行完毕然后才执行到t2的代码。

当然join()方法并不是用来保证线程的顺序性的。查看下join的源代码我们会发现其实底层还是使用的Object类的wait()方法,如图:

join方法中当通过isAlive()方法判断当前线程是运行状态的时候,就进行阻塞主线程让出cpu的资源给t1线程,而t1可以一直抢夺cpu资源到执行完成是因为join方法被synchronized修饰了,之前介绍过synchronized关键字的一些意思,这里我们先简单说明下被synchronized修饰的方法调用线程就会获取到同步锁,获取同步锁的线程可以一直执行完毕才会释放锁给其他线程执行。

join()方法主要用于一些多线程协调完成一个任务的执行,或者可以顺序执行的场景。

(6)线程的中断

之前我们在使用Thread.sleep()和Thread.join()的时候发现都需要抛出异常InterruptedException即线程被中断异常。为什么要抛出这个异常?

在Java中,一个线程是不能终止另一个线程的,除非线程自己程序想退出或者程序结束了。以前的时候Thread类提供了stop()、destroy()等方法可以强制结束一个线程,但是现在这些方法都没有得到保留下来。

那如何结束一个线程呢?其实每个线程都拥有一个flag,标志着线程的中断标识。如果一个线程A想让线程B退出,则A将B的中断标示(interrupt flag)置为true,我们说“线程A向线程B发了中断信号”。此时如果B检查到了中断标识为true,说明有线程想让它中断,线程B通过自己判断是否需要自愿退出(也可以不退出,不能强制)。

而在执行一些耗时操作的时候,例如sleep()、join()、wait()等,需要经常check interrupt的状态,并且一旦发现为true,就会立刻抛出InterruptedException告诉你其他线程向你发送了中断信号。Java中通过Thread的interrupt(true)来设置一个线程的中断信号为true,interrupted()和isInterrupted()方法可以获取线程的中断标识是否为true,不同的是interrupted()在获取的标识后会清除标识即把标识改为false。我们看下面结束线程的案例:

运行代码,当主线程调用t.interrupt();的时候子线程t执行结束。

(7)守护线程(后台线程)

守护线程又叫“服务线程”,它是后台线程,守护线程最显著的特点就是JVM中如果没有用户创建的前台线程的时候就会自动退出,所以守护线程一般都是给程序中其他对象和线程提供一些公共服务或者进行一些后台任务执行。

守护线程的优先级都很低,典型的守护线程就是GC(垃圾回收器),都知道GC主要负责我们JVM堆和方法区中内存的回收,如果JVM中除了GC线程外其他的都运行完没有了,那么GC也就不需要回收垃圾所以作为守护线程就自动退出了。

Java中通过setDaemon(true)来设置线程为“守护线程”,案例代码如下:

运行之后我们会发现当i=99之后,上面的线程运行完毕,然后后台线程也会退出程序。

以上就是我们线程中的一些基本的操作方法使用和讲解了,通过这些方法的运用希望大家对于线程的了解和使用更加的详细,下一篇中我们讲开始讲解我们如何保证线程并发安全的问题。

以上是关于Java开发之高并发必备篇——线程的状态调度和操作方法的主要内容,如果未能解决你的问题,请参考以下文章

Java开发之高并发必备篇——线程安全操作之synchronized

Java开发之高并发必备篇——线程安全操作之synchronized

Java开发之高并发必备篇——线程池

Java开发之高并发必备篇:Lock和ReentrantLock

Java开发之高并发必备篇——Lock和ReentrantLock

Java开发之高并发必备篇——Lock和ReentrantLock