本文主要介绍 Thread 类中线程的六种状态以及各状态之间转换的方法,如 wait()、start()、run()、sleep() 等。在阐述线程的生命周期的同时,解释各个方法是如何使线程的状态进行转换的,以及在转换的过程中是否需要释放锁等问题。

进程和线程

一开始的时候并没有进程线程之类的概念,计算机就是完成很简单的输入、计算、输出操作。然而,我们将数据输入到计算机中的这个过程,其实计算机一直在那里处于等待用户输入的状态,这显然效率很低。然后人们开始将需要执行的指令写入到硬盘等存储介质中,将这些指令一次性交给计算机去执行,这样的话,计算机就会不断的读取指令来进行相关的操作,这种操作方式称为批处理系统。

批处理系统虽然解决了等待用户输入时的性能问题,但计算机只能同时执行一个任务,如果现在有两个任务,一个任务在执行到一半的时候,需要进行大量的 I/O 操作,此时 CPU 只能等待该任务读取完数据之后才能继续执行,这个过程中的等待操作无疑是给白白的浪费了。那么有没有一种机制或者方式可以让一个任务在读取数据的时候让另一个任务去执行,即让另一个任务使用此时空闲下来的 CPU 去执行操作?还有,即使是让一个任务执行到一半的时候让另一个任务去执行,那么该任务执行以后如何再回到之前的任务继续执行下去呢?即如何回到之前执行到一半的时候的状态?

为了进一步提升计算机的运行效率,则引入了进程的概念。一个进行就对应计算机中的一个任务(程序)。每个进行都拥有自己独立的内存空间,各个进行之间互不干扰。当进程暂停时,它会保存当前进程的状态(如进程标识、进程的使用资源等),等待再次切换回来的时候,会根据之前的状态信息进行恢复,继而执行。

多个进程之间是可以进行通信的,这会使得程序设计的更加灵活。多进程的出现使得一个 CPU 可以同时执行多个不同的程序。这里的同时指的是:对于单个 CPU 来说,它在执行操作的时候会将时间片分给每一个进程,当某个进程执行完(时间片完)之后操作系统会负责调度 CPU 执行另一个进程。但由于时间片的非常小,使得给人的感觉就像是多个程序在同时执行一样。即从同一个时间段上看有多个任务在执行,这就是所谓的并发。因此,进程让操作系统的并发成为了可能

为了满足实时性的要求,线程就诞生了。也就是说,在很多情况下,一个程序的内部中的会存在多个子任务,这些子任务之间可以并行执行。线程是对子任务的抽象表示,一个进程可以有多个子任务在同时执行,这些子任务会共享同一个进程中的资源和地址空间等数据。也就是说,一个进程可以包含多个线程,每个线程可以负责一个独立的子任务。因此,线程让进程的内部并发成为可能

对于多核 CPU 来说,操作系统负责将不容的线程调度到多个 CPU 上执行,可以实现真正意义上的并行执行。

综上所述:

  • 进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位。
  • 进程是对运行时程序的封装,可以保存程序的运行状态,实现操作系统的并发,而线程是进程的子任务,能够保证程序的实时性。
  • 进程让操作系统的并发成为可能,而线程让进行的内部并发成为可能。

线程的状态

从线程的创建到消亡,需要经过若干个不同的状态:创建、就绪、运行、阻塞、等待、时间等待、消亡。而在java.lang.Thread里的内部枚举类State中定义了线程的 6 种状态,分别代表线程的创建(NEW)就绪/可运行(RUNNABLE)阻塞(BLOCKED)等待(WAITING)时间等待(TIMED_WAITING)以及消亡(TERMINATED),如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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.
    */
    BLOCKED,

    /**
    * Thread state for a waiting thread.
    */
    WAITING,

    /**
    * Thread state for a waiting thread with a specified waiting time.
    */
    TIMED_WAITING,

    /**
    * Thread state for a terminated thread.
    * The thread has completed execution.
    */
    TERMINATED;
}

线程在真正进入运行状态之前需要一些条件,在创建线程后,如果能满足线程运行所需要的条件(程序计数器、本地方法栈)的话,则会先进入就绪状态,进入就绪状态的线程如果能够获得 CPU 所分配的时间片的话,才会进入运行状态。如下图所示:

image.png

创建线程的方式

在之前的《Java 并发编程-更新中》文章中提到了创建线程的方式:

  • 一种是继承 Thread 类,重写其中的 run() 方法,在 run() 方法中定义需要执行的任务,然后调用 start() 方法执行线程;
    • 其中 Thread 类本身就实现了 Runnable 接口,该方式的局限性是不支持多继承。
  • 另一种是实现 Runnable 接口,重写 run() 方法,然后 new 一个 Runnable 实例作为参数传递给 Thread 类,最后还是通过 start() 方法启动线程。

如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestThread {

    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                System.out.println("Thread.");
            }
        }.start();

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Runnable.");
            }
        });
        thread.start();

        System.out.println("main.");
    }
}
// 程序最后输出的结果,与代码的运行顺序或调用顺序无关。

关于 run() 方法和 start() 方法的区别

线程的启动并不是简单的调用了 run() 方法, 而是由一个线程调度器来分别调用所有线程的 run() 方法。如果自己写一个普通的 run() 方法,该方法如果没有执行完是不会返回的,会一直执行下去,这就会造成 run() 方法下面的方法就不可能会执行了。而线程中的 run() 方法缺不一样,CPU 会给该线程分配时间片,该线程只有一定的 CPU 时间,执行完以后时间片就会交给其它线程,由于时间很短、切换速度快,所以我们就感觉像是很多线程在同时运行一样。

假如 new 了一个线程对象Thread thread = new Thread();,如果执行thread.run();,此时就相当于在主线程 main中调用的,该 run() 方法是运行在主线程 main上的,跟普通的方法没有什么区别,此时并不会创建一个新的线程来执行任务。

而执行thread.start()的话,会启动thread线程,thread线程启动后,会调用 run() 方法,此时的 run() 方法是运行在thread上的。所以说,start() 方法的作用是通知线程规划器(或线程调度器),该线程已经准备就绪了,以便让操作系统安排一个时间来调用其 run() 方法,使线程得到运行。

Thread 类

为了方便查看调用线程的某个方法后该线程会处于哪种状态,在此将线程状态转换图给出。虽然文章开头已经给出了更为详细的转换图,但下图更为简单直观一些:

image.png

Thread 类实现了 Runnable 接口,在 Thread 中有一些比较重要的属性,如被关键字 volatile 修饰的name表示线程的名字,priority表示线程的优先级(默认为 5,最小为 1,最大为 10),target表示要执行的任务等等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

    private volatile String name;
    private int            priority;
    private Thread         threadQ;
    private long           eetop;

    /* Whether or not to single_step this thread. */
    private boolean     single_step;

    /* Whether or not the thread is a daemon thread. */
    private boolean     daemon = false;

    /* JVM state */
    private boolean     stillborn = false;

    /* What will be run. */
    private Runnable target;

    /* The group of this thread */
    private ThreadGroup group;

    /* The context ClassLoader for this thread */
    private ClassLoader contextClassLoader;

    /* The inherited AccessControlContext of this thread */
    private AccessControlContext inheritedAccessControlContext;
}

start() 方法

由最上面的线程状态转换图所示,当调用了 start() 方法后,表示启动一个线程,该线程进入就绪(可运行)状态,此时系统才会开启一个新的线程来执行用户定义的子任务,为响应的线程分配所需要的资源。此方法被调用以后,该线程中的 run() 方法会在某个时机被调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
                it will be passed up the call stack */
        }
    }
}

run() 方法

run() 方法不需要用户来调用,当通过调用线程的 start() 方法后,一旦该线程获得了 CPU 给它分配的时间片,便会进入 run() 方法体去执行具体的任务。

1
2
3
4
5
6
7
// 创建线程时,必须重写此方法!!!
@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

sleep() 方法

有两个重载的 sleep() 方法,该方法的作用是在指定的毫秒时间内让当前正在运行的线程睡眠,交出 CPU 让其去执行其它任务。当线程睡眠时间满了以后,不一定会立即得到执行,还得看 CPU 有没有在执行其它任务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public static native void sleep(long millis) throws InterruptedException;

public static void sleep(long millis, int nanos) throws InterruptedException {
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException("nanosecond timeout value out of range");
    }

    if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
        millis++;
    }

    sleep(millis);
}

对于 sleep() 方法需要注意的是:

  • 如果存在任何线程中断了当前线程,则会抛出InterruptedException异常,而当该异常抛出的时候,当前线程的中断状态就会被清除。因此如果调用了 sleep() 方法,则需要捕获InterruptedException异常或向上抛出。
  • 此外,sleep() 方法不会释放锁,即如果当前线程持有某个对象的锁,即使调用 sleep() 方法,其它线程也无法访问该对象。

yield() 方法

yield 就是让步的意思,调用 yield() 后会让当前线程交出 CPU 资源,让 CPU 去执行其它线程。需要注意的是:

  • yield() 方法不能控制交出 CPU 的具体时间,只能让拥有相同优先级的线程具有获得 CPU 执行时间的机会;
  • 调用了 yield() 方法后,并不会让线程进入阻塞状态,而是让线程重新回到就绪/可运行状态,此时需要等待重新得到 CPU 的执行;
  • 调用 yield() 方法后不会释放锁。

yield() 是一个静态的本地方法:

1
public static native void yield();

join() 方法

当调用 join() 后,当前线程会从运行状态转换为等待状态,而调用有参的 join() 方法后,线程会在睡眠时间期限已满后进入就绪状态。join() 有三个重载的方法,其中需要注意的是:join() 调用的是 join(0) 方法,而在 join(0) 方法中,实际上调用的是 Object 类中的 wait(0) 方法,此时就意味着线程会永远等待,直至当前线程被唤醒。

也就是说,Object 类中的本地方法 wait() 会造成当前线程一直等待,直到另一个线程调用此对象的 notify() 或 notify() 方法,或者经过了指定的时间之后,当前线程才会进入就绪/可运行状态。此外,当前线程调用了 wait() 方法后会获得当前对象的锁。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public final void join() throws InterruptedException {
    join(0);
}


public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}


public final synchronized void join(long millis, int nanos) throws InterruptedException {
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException("nanosecond timeout value out of range");
    }

    if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
        millis++;
    }

    join(millis);
}

下面通过一个实例来理解 join() 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class WaitMethodInThreadTest {
    public static void main(String[] args) {
        System.out.println("进入线程: " + Thread.currentThread().getName());

        ThreadDemo thread1 = new ThreadDemo();
        thread1.start();

        try {
            System.out.println("线程:" + Thread.currentThread().getName() + " 等待.");
            thread1.join();
            System.out.println("线程:" + Thread.currentThread().getName() + " 继续执行.");

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadDemo extends Thread {
    @Override
    public void run() {
        System.out.println("进入线程: " + Thread.currentThread().getName());
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程: " + Thread.currentThread().getName() + " 执行完毕.");
    }
}

输出结果如下所示:

1
2
3
4
5
进入线程: main
线程:main 等待.
进入线程: Thread-0
线程: Thread-0 执行完毕.
线程:main 继续执行.

通过现象可以看到,当调用thread1.join();方法后,main 线程会进入等待,然后等待 thread1 执行完以后再继续执行。

也就是说,当 main 线程运行到thread1.join();的时候,main 线程会获得线程对象thread1的锁,意味着 join() 中的 wait() 方法会得到该对象的锁,只要thread1线程一直存活,就会调用该对象锁的 wait() 方法阻塞 main 线程,之后 main 线程会被 notity() 方法唤醒,至此结束。

因此,wait() 方法会让 main 线程进入阻塞状态,并且会释放 main 线程所占有的锁,让 main 线程交出 CPU 执行权限,此时 thread1 线程就会获得 CPU 的执行权,等到 thread1 执行完毕后,main 线程才继续执行。

interrupt() 方法

即中断此线程,该方法可以使处于阻塞状态的线程抛出一个异常,即该方法可以中断一个正在处于阻塞状态的线程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();  // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

下面通过一个示例说明 interrupt() 方法可以中断处于阻塞状态的线程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class InterruptMethodInThreadTest {

    public static void main(String[] args) {
        InterruptThread thread = new InterruptThread();
        thread.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
    }
}

class InterruptThread extends Thread {
    @Override
    public void run() {
        try {
            System.out.println("进入睡眠状态...");
            Thread.sleep(10000);
            System.out.println("睡眠完毕...");
        } catch (InterruptedException e) {
            System.out.println("发生中断异常...");
        }
        System.out.println("run 方法执行完毕...");
    }
}

输出结果如下所示:

1
2
3
进入睡眠状态...
发生中断异常...
run 方法执行完毕...

通过结果可以看出,当调用thread.start();以后,线程 thread 便进入了睡眠状态,然后开始睡眠 10 秒,而等到 2 秒之后便执行了thread.interrupt();中断操作。由于发生了中断,被 catch 捕获到了异常,最后 run 方法相继执行完毕。

下面通过一个示例,来看一看 interrupt() 方法是否可以中断处于非阻塞状态的线程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class InterruptMethodInThreadNoBlockingTest {

    public static void main(String[] args) {
        InterruptThreadNoBlocking thread1 = new InterruptThreadNoBlocking();
        thread1.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }

        thread1.interrupt();
    }
}


class InterruptThreadNoBlocking extends Thread {
    @Override
    public void run() {
        int i = 0;
        while (i < Integer.MAX_VALUE) {
            System.out.println(i + " while 在循环...");
            i++;
        }
    }
}

程序的输出会一直运行,直到 i 超过 Integer.MAX_VALUE。因此,调用 interrupt() 方法不能中断处于非阻塞状态的线程,但由于调用 interrupt() 方法相当于将中断标志设置为 true,所以可以通过调用 isInterrupted() 或 interrupted() 来判断中断标志是否被置位,以此来中断线程的执行。

但一般情况下不会通过上述的方式进行设置,可以在 MyThread 类中增加一个 volatile 属性的 isStop 来标志是否结束 while 循环,然后在 while 循环中判断 isStop 的值,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class MyThread extends Thread{
        private volatile boolean isStop = false;
        @Override
        public void run() {
            int i = 0;
            while(!isStop){
                i++;
            }
        }

        public void setStop(boolean stop){
            this.isStop = stop;
        }
    }
}

通过调用setStop()方法可以将 isStop 设置为 true,表明我要停止 while 循环了,从而可以将 while 语句结束循环。

其它方法

此外,还有一些已经废弃的方法,如 stop()、suspend()、resume() 等。

对于 stop() 方法来说,它是一个不安全的方法,调用 stop() 方法会将正在运行的线程转换为消亡状态。该方法会抛出一个 ThreadDeath 错误,如果线程持有某个对象锁的话,会完全释放锁,导致对象状态不一致。所以 stop() 方法基本是不会被用到的。

对于 suspend() 方法,字面意思是延迟、推迟,该方法可以暂停线程。而对于 resume() 方法,字面意思是重新开始、恢复,该方法可以恢复线程的执行。但这两个方法可能会造成死锁。

还有一些操作线程常用的方法

  • currentThread():返回代码段正在被哪个线程调用的信息;
  • isAlive():判断调用该方法的线程是否处于活动状态;
  • getId():获得线程的唯一标识;
  • getName() 和 setName():得到或设置线程的名字;
  • getPriority() 和 setPriority():获得或设置线程的优先级;
  • Daemon:Java 中的线程分为用户线程和守护线程,任何一个守护线程都是为整个 JVM 中所有非守护线程服务的,只要当前 JVM 实例中存在任何一个非守护线程没有结束,那么守护线程就在工作,只有当最后一个非守护线程结束的时候,守护线程才随着 JVM 一同结束工作。

参考

https://blog.csdn.net/justloveyou_/article/details/54347954

https://www.cnblogs.com/dolphin0520/p/3913517.html

https://www.cnblogs.com/dolphin0520/p/3920357.html

https://www.cnblogs.com/ITtangtang/p/7602363.html