线程1
1 进程和线程
1.1 进程
在计算机中,进程代表了内存中正在运行的应用程序,计算机中的资源(cpu 内存 磁盘 网络等),会按照需求分配给每个进程,从而这个进程对应的应用程序就可以使用这些资源了。
在操作系统中,启动一个应用程序的时候,会有一个或多个进程同时被创建,这些进程其实就表示了当前这个应用程序,在系统中的资源使用情况以及程序运行的情况。如果关闭这个进程,那么对应的应用程序也就关闭了。
所以,进程就是在系统中,运行一个应用程序的基本单位。
1.2 线程
线程是进程中的一个代码执行单元,负责当前进程中代码程序的执行,一个进程中有一个或多个线程。
当一个进程中启动了多个线程去分别执行代码的时候,这个程序就是多线程程序。
例如,当前我们使用java命令去运行一个类的时候,会先启动JVM,这个JVM对于计算机来讲,就是一个应用程序,所以同时系统中也会启动一个进程和这个JVM对应。
在桌面新建文件Hello.java
public class Hello{
public static void main(String[] args)throws Exception{
System.out.println("hello");
long time = 1000*100L;
Thread.sleep(time);
System.out.println("world");
}
}
Thread.sleep方法,可以让当前执行代码的线程,暂时的休眠一会
使用命令编译运行Hello类:java Hello
注意,代码中,输出hello之后,JVM并没有直接结束,而是让当前线程去休眠了100秒,所以这个时候JVM还在运行着,我们可以在任务管理中,看到JVM对应的进程了
如果把这个进程强行关闭掉,那么JVM就停止了,那么程序的运行也就停止了。
注意,可以先关闭STS等开发工具,因为很工具运行时候的也需要启动一个JVM,所以这里可能会出现多个进程
除此之外,我们还可以使用JDK中提供的工具,来查看JVM当前的运行情况:
这里通过JDK自带的jconsole工具,可以检测到当前运行Hello这个类的时候,JVM的运行情况,包含内存的使用、线程的运行状态、类的加载等信息
例如,查看当前JVM中执行main方法线程信息
注意,这线程的名字就叫main,它是任务就是调用执行我们类中的main方法!
所以,是一个名字叫main的线程,调用执行我们代码中的main方法
2 并发和并行
-
线程的并发执行,是指在一个时间段内,俩个或多个线程,使用一个CPU,进行交替运行。
-
线程的并行执行,是指在同一时刻,俩个或多个线程,各自使用一个CPU,同时进行运行。
如果计算机是单核CPU的话,那么同一时刻只能有一个线程使用CPU来执行代码
如果计算机是多核CPU的话,那么同一时刻有可能是俩个线程同时使用不同的CPU执行代码
可以看出,多核CPU确定可以提供程序的运行速度。
但是,假如我们在程序中编写了俩个线程,启动并运行它们,那么我们是无法控制也无法知道,计算机中到底是使用了一个CPU去运行它们,还是使用俩个CPU去运行它们。这是计算机内核中对资源进行调度的事情,我们从应用程序的层面是无法干涉的。
所以,在一般情况下,我们编写多线程代码时,可以用单核CPU的情况来考虑问题,去设计并编写多线程的代码。
将来计算机如果真的使用多个CPU来运行我们的多线程代码的话,那么也只是提高了代码的执行效率,基本不会影响我们代码的设计和实现的。
3 时间片
3.1 概述
时间片,当前一个线程要使用CPU的时候,CPU会分配给这个线程一小段时间(毫秒级别),这段时间就叫做时间片,也就是该线程允许使用CPU运行的时间,在这个期间,线程拥有CPU的使用权。
如果在一个时间片结束时,线程还在运行,那么这时候,该线程就需要停止运行,并交出CPU的使用权,然后等待下一个CPU时间片的分配。
所以,在宏观上,一段时间内,我们感觉俩个线程在同时运行代码,其实在微观中,这俩个线程在使用一个CPU的时候,它们是交替着运行的,每个线程每次都是运行一个很小的时间片,然后就交出CPU使用权,只是它们俩个交替运行的速度太快了,给我们的感觉,好像是它们俩个线程在同时运行。
思考,生活中还有哪些是因为速度太快,从而通过我们的眼睛“欺骗”了我们的情况?
3.2 调度
当俩个或多个线程使用一个CPU来运行代码的时候,在操作系统的内核中,就会有相应的算法来控制线程获取CPU时间片的方式,从而使得这些线程可以按照某种顺序来使用cpu运行代码,这种情况被称为线程调用。
常见的调度方式:
-
时间片轮转
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
-
抢占式调度
系统会让优先级高的线程优先使用 CPU(提高抢占到的概率),但是如果线程的优先级相同,那么会随机选择一个线程获取当前CPU的时间片。
JVM中的线程,使用的为抢占式调度。
例如,
public static void main(String[] args) {
//创建线程对象t1
Thread t1 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("hello");
}
}
};
//创建线程对象t2
Thread t2 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("world");
}
}
};
//启动线程t1 t2
t1.start();
t2.start();
}
代码中,线程t1中循环输出100次hello,线程t2中循环输出100次world,启动t1和t2后,它们就开始公平的竞争(默认情况下优先级相等),抢占CPU的时间片(使用权),抢到之后就可以使用cpu运行代码了,所以可以看到最后的运行结果,hello和world是交替运行的,但是每次的运行结果都会有所不同。
4 main线程
使用java
命令来运行一个类的时候,首先会启动JVM(进程),JVM会在创建一个名字叫做main的线程,来执行类中的程序入口(main方法)
例如,
public class Test {
public static void main(String[] args) {
//获取执行当前方法的线程对象
Thread currentThread = Thread.currentThread();
System.out.println("执行当前方法的线程名字为:"+currentThread.getName());
}
}
//运行结果:
执行当前方法的线程名字为:main
所以,我们写在main方法中的代码,其实都是由名字叫做main的线程去执行的
Thread.currentThread();
可以写在任意方法中,返回就是执行这个方法的线程对象
上面代码使用java命令运行的过程是:
-
使用java命令运行Test类,会先启动JVM
-
应用类加载器通过CLASSPATH环境变量配置的路径,找到Test.class文件,并加载到方法区。
注意,这里会同时生产一个Class类型对象,来代表这个Test类型,并且会优先处理类中的静态代码(静态属性、静态方法、静态代码块)
-
JVM创建并启动一个名字叫做main的线程
-
main线程将Test中的main方法加载到栈区中
-
在栈里面,main线程就可以一行行的执行方法中的代码了
-
如果在执行代码中,遇到了方法调用,那么线程会继续把被调用的方法,加载到栈中(压栈操作),然后执行栈顶这个最新添加进来的方法,栈顶方法执行完,就释放(出栈操作),然后在进行执行当前最新的栈顶方法(之前我们画过栈里面的方法调用图,例如在异常的学习过程中)
-
代码执行过程输出执行结果
-
当前是单线程程序,main线程结束了,JVM就停止了,如果是多线程程序,那么JVM要等所有线程都结束了才会停止
5 线程的创建和启动
java.lang.Thread
是java中的线程类,所有的线程对象都必须是Thread类或其子类的实例。
每个线程的作用,就是完成我们给它指定的任务,实际上就是执行一段我们指定的代码。我们只需要在Thread
类的子类中重写run
方法,把执行的代码写入到run方法中即可,这就是线程的执行任务!
Java中通过继承Thread类来创建并启动一个新的线程的步骤如下:
- 定义
Thread
类的子类(可以是匿名内部类),并重写Thread
类中的run
方法,run
方法中的代码就是线程的执行任务 - 创建
Thread
子类的对象,这个对象就代表了一个要独立运行的新线程 - 调用线程对象的
start
方法来启动该线程
例如,
public class Test {
public static void main(String[] args) {
//2.创建线程类对象
Thread t = new MyThread();
//3.调用start方法启动线程
t.start();
}
}
//1.子类继承父类Thread,并重写run方法(指定线程的执行任务)
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello world");
try {
//可以让当前执行代码的线程睡眠1000毫秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
根据结果,可以看出,t线程启动开始执行的时候,每输出一次hello world都会睡眠1秒钟
例如,也可以使用匿名内部类的形式来完成
public class Test {
public static void main(String[] args) {
Thread t = new MyThread(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
}
}
在此过程中,main线程和t线程之间的关系是:
可以看出,main线程在执行main方法的过程中,创建并启动了t线程,并且t线程启动后,和main线程就没有关系了,这时候main线程和t线程都是自己独立的运行,并且他们俩个是要争夺CUP的时间片(使用权)的
以上代码在内存中的情况:
注意1,之前所提到的栈区,又被称为方法调用栈,是线程专门执行方法中代码的地方,并且每一个线程,都有自己独立的栈空间,和别的线程相互不影响
注意2,最先启动的线程是主线程(main线程),因为它要执行程序的入口main方法,在主线程中,创建并且启动了t线程,启动之后main线程和t线程将各自独立运行,并且争夺CPU的时间片
注意3,线程启动之后(调用start方法),会开始争夺CPU的时间片,然后自动执行run方法,如果子类对象重写了,那么就调用到重写后的run方法
注意4,堆区是对所以线程共享的,每个线程中如果创建了对象,那么对象就会存放到堆区中
注意5,线程对象t被创建出来的时候,它还只是一个普通的对象,但是当调用了t.start()方法之后,线程对象t可以说才真正的“现出原形”:开辟了单独的栈空间,供线程t调用方法使用
思考,多线程比单线程的优势在哪里?我们一般会把什么样的代码交给多先去执行处理?
6 Runnable接口
给一个线程对象指定要执行的任务,除了继承Thread类后重写run方法之外,还可以利于Runnable接口来完成线程任务的指定
java.lang.Runnable
,该接口中只有一个抽象方法run
public interface Runnable {
public abstract void run();
}
其实Thread
类也是Runnable
接口的实现类,其代码结构大致为:
public class Thread implements Runnable {
/* What will be run. */
private Runnable target;
public Thread() {
//...
}
public Thread(Runnable target) {
this.target = target;
//..
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
可以看出,子类重写Thread中的run方法,这个run方法其实也来自于Runnable接口
通过以上的代码结构,可以知道,我们还可以直接创建Thread
对象,在调用构造器的时候,传一个Runnable
接口的实现类对象进来,然后调用线程的对象run
方法,那么默认就会调用到Runnable接口实现类重写的run方法!
例如,使用Runnable
接口的匿名内部类,来指定线程的执行任务(重写接口中的run方法)
public class Test {
public static void main(String[] args) {
//Runnable接口的实现类中,重写了run方法,指定线程的执行任务
Runnable run = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//创建线程对象,指定执行任务
Thread t = new Thread(run);
t.start();
}
}
可以看出,这种方式也可以完成之前相同的功能
实现Runnable接口比继承Thread类所具有的优势:
- 可以把相同的一个执行任务(Runnable接口的实现),交给不同的线程对象去执行
- 可以避免java中的单继承的局限性。
- 线程和执行代码各自独立,实现代码解耦
7 线程的名字
通过Thread类中的currentThread方法,可以获取当前线程的对象,然后调用线程对象的getName方法,可以获取当前线程的名字。String name = Thread.currentThread().getName();
注意,这里说的当前线程,指定是执行当前方法的线程,因为获取线程名字的代码肯定是写在某个方法中的,并且这个方法一定是由某个线程调用执行的,它们的关系如下:
例如,
public class Test {
public static void main(String[] args) {
String name = Thread.currentThread().getName();
System.out.println("执行当前main方法的线程是:"+name);
Runnable run = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println("执行当前run方法的线程是:"+name);
}
};
Thread t = new Thread(run);
t.start();
}
}
//运行结果为:
执行当前main方法的线程是:main
执行当前run方法的线程是:Thread-0
注意,一定要记得,start方法启动线程后,线程会自动执行run方法
千万不要直接调用run方法,这样就不是启动线程执行任务,而是普通的方法调用,和调用sayHello没区别
默认情况下,主线程中,创建出的线程,它们的都会有一个默认的名字:
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
其中,"Thread-" + nextThreadNum()
就是在拼接出这个线程默认的名字,Thread-0 Thread-1 Thread-2等等
我们也可以创建线程对象的时候,给它设置一个指定的名字:
Thread t = new Thread("t线程");
//或者
Thread t = new Thread(new Runnable(){
public void run(){
//执行任务
}
},"t线程");
//或者
Thread t = new Thread();
t.setName("t线程");
8 线程的分类
java中,线程可以分为:
- 前台线程,又叫做执行线程、用户线程
- 后台线程,又叫做守护线程、精灵线程
前台线程:
这种线程专门用来执行用户编写的代码,地位比较高,JVM是否会停止运行,就是要看当前是否还有前台线程没有执行完,如果还剩下任意一个前台线程没有“死亡”,那么JVM就不能停止!
例如,执行程序入口的主线程(main),就是一个前台线程,在单线程程序中,main方法执行完,就代表main线程执行完了,这时候JVM就停止了
例如,我们在主线程创建并启动的新线程,默认情况下就是一个前台线程,用来执行用户编写的代码任务。
后台线程:
这种线程是用来给前台线程服务的,给前台线程提供一个良好的运行环境,地位比较低,JVM是否停止运行,根本不关心后台线程的运行情况和状态。
例如,垃圾回收器,其实就一个后台线程,它一直在背后默默的执行着负责垃圾回收的代码,为我们前台线程在执行用户代码的时候,提供一个良好的内存环境。
思考,前台线程和后台线程的关系,在生活中是否也存在类似的例子呢?
在主线程中,创建出来的线程对象,默认就是前台线程,在它启动之前,我们还可以给它设置为后台线程:
public class Test {
public static void main(String[] args) {
Thread t = new Thread("t线程"){
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
System.out.println(name+": hello "+i);
}
}
};
//在启动线程之前,可以将其设置为后台线程,否则默认是前台线程
t.setDaemon(true);
t.start();
}
}
注意,
t.setDaemon(true);
这句代码,注释掉和不注释,观察代码的运行结果,看是否一样?
9 线程优先级
线程类Thread中,有一个属性,表示线程的优先级,代码结果大致为:
public class Thread implements Runnable {
private int priority;
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
public final int getPriority() {
return priority;
}
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
private native void setPriority0(int newPriority);
}
可以看出,最终设置线程优先级的方法,是一个native方法,并不是java语言实现的
线程的优先级使用int类型数字表示,最大是10,最小是1,默认的优先级是5。
当俩个线程争夺CPU时间片的时候:
- 优先级相同,获得CPU使用权的概率相同
- 优先级不同,那么高优先级的线程有更高的概率获取到CPU的使用权
例如,t1和t2线程各自运行10000次循环,看哪个线程先运行完
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程"){
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 10000; i++) {
}
System.out.println(name+"线程执行完毕");
}
};
Thread t2 = new Thread("t2线程"){
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 10000; i++) {
}
System.out.println(name+"线程执行完毕");
}
};
// t1.setPriority(Thread.MAX_PRIORITY);
// t2.setPriority(Thread.MIN_PRIORITY);
System.out.println("t1线程的优先级:"+t1.getPriority());
System.out.println("t2线程的优先级:"+t2.getPriority());
t1.start();
t2.start();
}
}
注意1,默认情况下,俩个线程的优先级都是5,那个俩个线程争夺到CPU的使用权的概率一样,那么基本上俩个线程都有相同的概率先执行完10000次循环
注意2,其实t1先稍微占了那么一点点的优势,因为毕竟在主线程的代码中,先启动了t1先,然后又启动了t2线程
注意3,设置t1和t2优先级的之后,在运行查看结果,会明显看到优先级高的线程,有更高的概率先执行完代码
10 线程组
Java中使用java.lang.ThreadGroup
类来表示线程组,它可以对一批线程进行管理,对线程组进行操作,同时也会对线程组里面的这一批线程操作。
java.lang.ThreadGroup
:
public class ThreadGroup{
public ThreadGroup(String name){
//..
}
public ThreadGroup(ThreadGroup parent, String name){
//..
}
}
创建线程组的时候,需要指定该线程组的名字。
也可以指定其父线程组,如果没有指定,那么这个新创建的线程组的父线程组就是当前线程组。
例如,
public class Test {
public static void main(String[] args) {
//获取当前线程对象
Thread currentThread = Thread.currentThread();
//获取当前线程所属的线程组
ThreadGroup currentThreadGroup = currentThread.getThreadGroup();
System.out.println(currentThreadGroup);
}
}
//运行结果:
java.lang.ThreadGroup[name=main,maxpri=10]
可以看出,当前线程组的名字为main,并且线程组中的线程最大优先级可以设置为10
例如,用户在主线程中创建的线程,属于默认线程组(名字叫"main"的线程组)
public class Test {
public static void main(String[] args) {
Thread t = new Thread();
ThreadGroup threadGroup = t.getThreadGroup();
System.out.println(threadGroup);
}
}
//运行结果:
java.lang.ThreadGroup[name=main,maxpri=10]
可以看出,主线程中,创建一个线程对象,它的线程组默认就是当前线程的线程组
例如,
public class Test {
public static void main(String[] args) {
ThreadGroup group = new ThreadGroup("我的线程组");
//指定线程所属的线程组
Thread t = new Thread(group,"t线程");
ThreadGroup threadGroup = t.getThreadGroup();
System.out.println(threadGroup);
}
}
//运行结果:
java.lang.ThreadGroup[name=我的线程组,maxpri=10]
例如,
public class Test {
public static void main(String[] args) {
ThreadGroup group = new ThreadGroup("我的线程组");
Runnable run = new Runnable() {
@Override
public void run() {
try {
//让线程休眠一会,否则运行太快,死亡太快了
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t1 = new Thread(group,run,"t1线程");
Thread t2 = new Thread(group,run,"t2线程");
Thread t3 = new Thread(group,run,"t3线程");
//注意,启动后,三个线程都会进行休眠,等run方法运行完就“死亡”了
t1.start();
t2.start();
t3.start();
//返回当前线程组中还没有“死亡”的线程个数
System.out.println("线程组中还在存活的线程个数为:"+group.activeCount());
//准备好数组,保存线程组中还存活的线程
Thread[] arr = new Thread[group.activeCount()];
//将存活的线程集中存放到指定数组中,并返回本次存放到数组的存活线程个数
System.out.println("arr数组中存放的线程个数为:"+group.enumerate(arr));
//输出数组中的内容
System.out.println("arr数组中的内容为:"+Arrays.toString(arr));
}
}
//运行结果:
线程组中还在存活的线程个数为:3
arr数组中存放的线程个数为:3
arr数组中的内容为:[Thread[t1线程,5,我的线程组], Thread[t2线程,5,我的线程组], Thread[t3线程,5,我的线程组]]
注意,只有在创建线程对象的时候,才能指定其所在的线程组,线程运行中途不能改变它所属的线程组
11 线程状态
java.lang.Thread.State
枚举类型中(内部类形式),定义了线程的几种状态,其代码结果为:
public class Thread{
/* Java thread status for tools,
* initialized to indicate thread 'not yet started'
*/
private volatile int threadStatus = 0;
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* 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
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <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>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
//返回这个线程当前所处的状态
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
}
状态描述和解释如下:
线程状态 | 名称 | 描述 |
---|---|---|
NEW | 新建 | 线程刚被创建,还没调用start方法,或者刚刚调用了start方法,调用start方法不一定"立即"改变线程状态,中间可能需要一些步骤才完成一个线程的启动。 |
RUNNABLE | 可行行 | start方法调用结束,线程由NEW变成RUNNABLE,线程存活着,并尝试抢占CPU资源,或者已经抢占到CPU资源正在运行,这俩种情况的状态都显示为RUNNABLE |
BLOCKED | 锁阻塞 | 线程A和线程B都要执行方法test,而且方法test被加了锁,线程A先拿到了锁去执行test方法,线程B这时候需要等待线程A把锁释放。这时候线程B就是处理BLOCKED |
WAITING | 无限期等待 | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
TIMED_WAITING | 有限期等待 | 和WAITING状态类似,但是有一个时间期限,时间到了,自己也会主动醒来 |
TERMINATED | 终止(死亡) | run方法执行结束的线程处于这种状态。 |
注意,其实 BLOCKED,WAITING,TIMED_WAITING 这三种都属于线程阻塞,只是触发的条件不同,以及从阻塞状态中恢复过来的条件也不同而已。
线程在这三种情况的阻塞下,都具备相同的特点:
- 线程不执行代码
- 线程也不参与CPU时间片的争夺
线程状态变化的情况如下:
一个线程从创建到启动、到运行、到死亡,以及期间可能出现的情况都在上图中进行了描述。
在后面的学习中,会陆续认识到这里面的每一种情况!
一个线程,经历的最普通的过程如下:
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
}
}
};
System.out.println(t1.getState());
//启动t1线程
t1.start();
System.out.println(t1.getState());
System.out.println(t1.getState());
System.out.println(t1.getState());
System.out.println(t1.getState());
System.out.println(t1.getState());
System.out.println(t1.getState());
System.out.println(t1.getState());
System.out.println(t1.getState());
}
}
//运行结果: 注意需要多运行几次,因为可能每次运行的情况不一样
NEW
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
TERMINATED
TERMINATED
注意1,刚创建好的线程对象,就是出于NEW的状态
注意2,线程启动后,会出于RUNNABLE状态
注意3,其实这个RUNNABLE状态包含俩种情况:
- 就绪状态,此时这个线程没有运行,因为没有抢到CPU的执行权
- 运行状态,此时这个线程正在运行中,因为抢到CPU的执行权
注意4,JavaAPI中并没有定义就绪状态和运行状态,而是把这俩情况统一叫做RUNNABLE(可运行状态),但是一般我们为了能更加清楚的描述问题,会用上就绪状态和运行状态
注意5,在线程多次抢到CPU执行权,“断断续续”把run方法执行完之后,就变成了TERMINATED状态(死亡),之所以是“断断续续”的运行,是因为每次抢到CPU执行权的时候,只是运行很小的一个时间片,完了之后还要重新抢夺下一个时间片,并且中间还有可能抢不到的情况
注意6,死亡后的线程,不能重新启动
和上面描述对应的状态图如下:
从就绪状态到运行状态,之间会经过多次反复的CPU执行权的争夺(线程调度)
这就是一个线程经历的最基本的状态变化。
其他的状态都是线程在Running的时候,线程中调用了某些方法,或者触发了某些条件,导致这个线程进入到了阻塞状态(上面介绍的三种阻塞情况)