时间很贪婪——有时候,它会独自吞噬所有的细节。——《追风筝的人》


内容



1.线程状态

一个线程在某一时刻只能处于一种状态,这里讨论的线程状态特指Java虚拟机的线程状态,并不反映线程在特定操作系统下的状态,也就是说Java虚拟机的线程状态和操作系统的状态并不是一一对应的。

Java在Thread类中定义了State枚举类,列举了6种线程状态:

 public class Thread implements Runnable {
//线程的状态以枚举的方式定义在Thread类的内部
public enum State {

NEW, // 新建

RUNNABLE, // 运行

BLOCKED, // 阻塞

WAITING, // 无限期等待

TIMED_WAITING, // 限期等待

TERMINATED; // 终止
}
}

因此,Java中的线程状态分别是:

  • 新建(NEW):线程刚被创建,还未调用start启动。
  • 运行(RUNNABLE):调用了 strat 方法。该状态下,线程可能正在运行,也可能处于就绪状态,等待CPU调度。
  • 无限期等待(WAITING):让出CPU执行权,线程休眠,等待其他线程显式地唤醒。
  • 限期等待(TIMED_WAITING):让出CPU执行权,线程休眠,在一定时间内,即使没有其他线程显式地唤醒该线程,该线程也能被自动唤醒。
  • 阻塞(BLOCKED):线程被阻塞于锁,这个事件将在另外一个线程获得锁的时候可能发生。
  • 终止(TERMINATED):终止状态,线程已结束运行。

注意

  • Java中的运行(RUNNABLE)状态对应了操作系统中的就绪态(Ready)和运行态(Running),因此在Java中,处于RUNNABLE状态的线程不一定正在被执行,也可能处于就绪状态,等着被CPU调度。
  • 一个线程想被运行(Running)的先决条件是他要处于就绪状态(Ready)。一个处于就绪状态的线程意味着,该线程已经拿到了除CPU资源的其他任何需要的资源,他一拿到CPU的执行权就可以立即被执行。
  • 当进行阻塞IO操作时,线程的状态是RUNNABLE,而非WAITING或BLOCKED。
  • 在操作系统层面的阻塞和Java线程的阻塞含义相差甚远。

2.线程状态转化

每一个线程在他的生命周期内有以上6中不同的状态,这也是进程动态性的表现,随着CPU的调度和特定的事件发生,线程的状态会发生转换。

Java线程状态转化图:

Java并发深度总结:基础线程机制_线程状态

2.1 NEW ----> RUNNABLE

当一个线程刚被创建出来时,就处于新建状态(NEW),调用Thread.start()方法,开启一个线程,该线程进入运行状态(RUNNABLE)。

运行状态(RUNNABLE)包含了正在运行(Running)和就绪状态(Ready):

  • Ready --> Running : CPU为该线程分配时间片,线程拿到CPU执行权,由就绪(Ready)状态转化为正在运行(Running)。
  • Running --> Ready :该线程的CPU时间片使用完了或该线程将CPU的执行权礼让(Thread.yield)给了其他线程,该线程由正在运行(Running)转化为就绪(Ready)状态。

注意:一个线程从新建(NEW) ---- > 运行(RUNNABLE)只会发生一次,也就是说一个线程被创建出来之后只能调用一次start()方法,多次调用start()方法启动一个线程会抛出IllegalThreadStateException,即使该线程已经运行结束了(TERMINATED)也不能再次启动。

import java.util.concurrent.TimeUnit;

public class ThreadTest {

public static void main(String[] args) throws Exception{

Thread t = new Thread(() -> {
System.out.println("run end !");
});

t.start();
Thread.sleep(1000l);
System.out.println(t.getState()); // TERMINATED
t.start(); // 再次启动线程:IllegalThreadStateException
}
}

输出:
run end !
TERMINATED
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
...

2.2 RUNNABLE <----> WAITING

运行状态(RUNNABLE)和无限期等待(WAITING)两种状态可以相互转换:

RUNNABLE --> WAITING:

  • 调用了没有设置Timeout参数的Object.wait()方法。
  • 调用了没有设置Timeout参数的Thread.join()方法。
  • LockSupport.park()方法。

WAITING --> RUNNABLE :

  • 其他线程调用了Object.notify() 或 Object.notifyAll() 将处于WAITING 状态的线程唤醒。
  • LockSupport.unpark(Thread)方法。

注意:

  • RUNNABLE --> WAITING状态转换中,准确的说是运行状态中的正在运行(Running) 向 WAITING状态转换,因为处于就绪状态的线程没有CPU的执行权,因此也就不能直接由就绪(Ready)转化为 无限期等待(WAITING)状态。
  • 同样的,WAITING --> RUNNABLE状态转换中,实际上是无限等待(WAITING )转化为运行状态中的就绪(Ready)状态。
2.3 RUNNABLE <----> TIMED_WAITING

与运行状态和无限期等待的相互转换类似:

RUNNABLE --> TIMED_WAITING:

  • 调用了没有设置Timeout参数的Object.wait()方法。
  • 调用了没有设置Timeout参数的Thread.join()方法。
  • Thread.sleep(long millis)方法。
  • LockSupport.parkNanos()方法。
  • LockSupport.parkUntil()方法。

TIMED_WAITING --> RUNNABLE :

  • 其他线程调用了Object.notify() 或 Object.notifyAll() 将处于WAITING 状态的线程唤醒。
  • LockSupport.unpark(Thread)方法。
  • 达到设置的超时时间,该线程被自动唤醒。
2.4 RUNNABLE <----> BLOCKED

Java中的阻塞状态(BLOCKED)指:线程被阻塞于锁,也就是线程要进入同步方法或同步代码块时,其他的线程已经持有同步方法或同步代码块的锁,因此该线程等待其他线程释放锁而处于阻塞状态。

RUNNABLE --> BLOCKED:

  • 线程进入同步方法或同步代码块时,其他线程未释放锁。

BLOCKED --> RUNNABLE:

  • 线程被阻塞后,竞争锁成功。
2.5 RUNNABLE ----> TERMINATED

当发生以下情况时,线程会由运行状态转换为终止状态:

  • 线程run()方法正常运行结束。
  • 线程run()方法抛出未被捕获的异常。
  • 调用了Thread.stop()方法,强制地终止了线程。

再次说明一下:处于终止状态(TERMINATED)的线程无法再次启动(调用start()方法),即使线程对象还在。

3.线程控制

3.1 线程休眠:Thread.sleep

Thread类的静态方法:线程休眠,需指定当前线程的休眠时间,可能抛出异常。该方法让线程由运行状态(RUNNABLE)进入限期等待(TIMED_WAITING),让出CPU执行权,不释放对象锁(持锁等待),即线程在获得锁后调用该方法不会释放已获得的锁,其他线程不能访问共享数据。

该方法常用来暂停当前线程的执行,让其他线程执行:

public class ThreadSleep {

public static void main(String[] args) {
Thread t = new Thread(()->{
while (true) {
try {
//do something...
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

t.start();
}
}

当前线程可能是一个服务程序,为了防止单处理器下,CPU占用率100%,可以设置当前线程休眠,让其他线程工作。

注意:sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。

public class ThreadSleep {

public static void main(String[] args) throws Exception{

Thread t = new Thread(()->{
});

t.start();

System.out.println("main...");
t.sleep(2000); // main 线程睡眠 2000毫秒 而不是t线程 不建议
System.out.println("main end...");
}
}

从控制台输出情况可以看到,两个输出语句大约间隔2s,所以是main线程休眠了,而非 t 线程。

3.2 线程礼让:Thread.yield

Thead类的静态方法:线程礼让,让当前线程释放CPU的执行权,当前线程从正在运行(Running)状态转换到就绪状态(Ready),重新竞争CPU的执行权。不释放当前线程已持有的锁

注意:

  • yield()方法,用于暗示操作系统调度其他线程,仅仅是暗示,没有任何机制保证他会被执行,也就是说yield()方法的实现依赖于操作系统的任务调度器,调用yield()方法并不一定能保证线程礼让一定能够成功。
  • 通常线程优先级比当前线程更高的、处于就绪状态的线程,更有可能获得执行的机会。
public class ThreadYield {

public static void main(String[] args) {
// Thread-0
new Thread(()->{
int count = 0;
while (true) {
count++;
System.out.println(Thread.currentThread().getName() + "---" + count);
}

}).start();

// Thread-1
new Thread(()->{
int count = 0;
while (true) {
Thread.yield();
count++;
System.out.println(Thread.currentThread().getName() + "---" + count);
}

}).start();
}
}

运行截图:

Java并发深度总结:基础线程机制_java_02

从运行结果中可以看到,Thread-1 线程礼让后,其执行的次数远远小于Thread-0。

3.3 线程中断:Thread.interrupt

Thread类的普通成员方法:interrupt()方法会设置线程的中断标记为true。注意:interrupt()方法仅会设置该线程的中断状态位为true,并不会真正的让线程中断,设置线程中断标记不影响线程的继续执行

public class ThreadInterrupt {

public static void main(String[] args) throws Exception{
Thread t = new Thread(() -> {

while (true) {
System.out.println(Thread.currentThread().getName()+ "---Running...");
}

});

t.start();

t.interrupt();
System.out.println("调用interrupt()"); //Thread-0 一直在运行,并没有被中断
}
}

上面的代码,展示了interrupt()方法的调用不会对线程的运行带来影响。那么如何通过interrupt()方法实现线程的中断呢,下面给出了两种方式:

  • 捕获InterruptException异常:线程设置休眠后(wait、join、sleep),调用线程的interrupt()方法,会抛出 InterruptedException,且中断标志被清除,重新设置为false。
  • 使用isInterrupted()判断线程的中断标志。
public class ThreadInterrupt {

public static void main(String[] args) {

Thread t1 = new Thread(()->{
while (!Thread.currentThread().isInterrupted()) {
// do something
}

System.out.println(Thread.currentThread().getName() + "---线程结束...");
});
t1.start();
t1.interrupt();

Thread t2 = new Thread(()->{
while (true) {
try {
// do something...
Thread.sleep(2000l);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "---线程结束...");
break;
}
}
});
t2.start();
t2.interrupt();
}
}

输出:
Thread-0---线程结束...
Thread-1---线程结束...

除了使用中断的方式来终结一个线程,还可以通过:

  • Thread.stop:已过时。调用该方法即刻停止run()方法中剩余的全部工作,因此这种终结线程的方式不会保证线程的资源正常释放,如文件、数据库连接的释放等,此外,stop()方法会释放所有的锁,从而导致原子逻辑受损。因此不建议使用。
  • 通过自定义标记位终结线程:这种方式与通过中断标志位终结线程类似。
public class ThreadInterrupt {

public static void main(String[] args) throws Exception{
// 1.调用 thread.stop()
Thread t = new Thread(() -> {
while (true) {
// do something...
System.out.println(Thread.currentThread().getName() + "---Running...");
}
});

t.start();
Thread.sleep(10);
t.stop();

// 2.自定义标志位
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
t1.start();
Thread.sleep(10);
myRunnable.cancel();
}
}

class MyRunnable implements Runnable {

private volatile boolean on = true;

@Override
public void run() {
while (on) {
// do something...
}
}

public void cancel() {
this.on = false;
}
}

Thread.interrupted() 和 Thread.isInterrupted()

  • Thread.isInterrupted()仅仅检测线程对象中断标志的状态。
  • Thread.interrupted() 也可以检测线程对象中断标志的状态,并且还会将中断标志位清除,即重新设置为false。因此请谨慎使用。
  • Thread.isInterrupted()是普通成员方法,Thread.interrupted() 是静态方法。Thread.interrupted()与Thread.sleep类似,他返回并清除的是当前线程的中断标志,而不是调用这个方法的线程。
public class Interrupted {
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> {
while (true) {}
});
thread.start();
thread.interrupt();

//获取中断标志并重置,虽然是thread.interrupted(),但实际上是获取主线程的中断标志,因为在主线程中调用的
System.out.println("interrupted:" + thread.interrupted());

//获取中断标志并重置,也是获取主线程的中断标志
System.out.println("interrupted:" + Thread.interrupted());
}
}

3.4 线程等待与唤醒:Object.wait & notify & notifyAll
3.4.1 Object.wait

Object类的普通方法:线程等待,该方法使当前线程让出CPU的执行权,并释放锁,让其他线程可以进入同步方法或同步代码块,当前线程被放入对象等待队列中,等待被notify或notifyAll唤醒。

该方法有三个重载方法,一个是没有timeout 参数的方法,其他两个是有 timeout 参数的方法 。

  • Object.wait():使当前线程进入无限期等待状态(WAITING),直到被notify或notifyAll唤醒。
  • Object.wait(long):使当前线程进入限期等待状态(TIMED_WAITING),在一定时间内,即使没有notify或notifyAll也能自动被唤醒。
  • Object.wait(long,int):对于超时时间更细力度的控制,单位为纳秒。(实际上并不能控制到超时时间到纳秒级别,而是在原来毫秒级别上+1)

注意:wait、notify、notifyAll 虽然是Object的一部分,但他们只能在同步方法或同步代码块中被调用,否则在运行时会抛出IllegalMonitorStateException异常。也就是说调这些方法前必须持有对象的锁。

3.4.3 Object.notify & notifyAll
  • notify():随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,也就是notify()方法仅通知一个线程,唤醒的线程再重新竞争锁。
  • notifyAll():使所有正在等待队列中等待同一共享资源的全部线程退出等待队列,唤醒的线程再重新竞争锁。

注意:

  • notify() & notifyAll 并不是唤醒任何处于wait()状态的线程,而是唤醒具有同一锁对象的线程。
  • 调用notify() & notifyAll后,当前线程并不会马上释放该对象锁,要等到执行notify()方法的线程执行完才会释放对象锁。
  • 被唤醒的线程也不会立即执行而是尝试获得锁(他必须先重新获得他wait时释放的锁,因此刚被唤醒的线程处于阻塞状态)。

问题:写两个线程,一个线程打印1-52,另一个线程打印A-Z,打印结果为12A34B…5152Z

class Print {

private int flag = 1;//信号量。当值为1时打印数字,当值为2时打印字母
private int count = 1;

// 同步方法的锁对象为this
public synchronized void printNum() {
// 不打印数字就进入等待
if (flag != 1) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//打印数字
System.out.print(2 * count - 1);
System.out.print(2 * count);
flag = 2;
notify();
}

public synchronized void printChar() {
// 不打印字母就进入等待
if (flag != 2) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//打印字母
System.out.print((char) (count - 1 + 'A'));
count++;//当一轮循环打印完之后,计数器加1
flag = 1;
notify();
}
}

public class Test {
public static void main(String[] args) {

Print print = new Print();

new Thread(() -> {
for (int i = 0; i < 26; i++) {
print.printNum();
}
}).start();

new Thread(() -> {
for (int i = 0; i < 26; i++) {
print.printChar();
}
}).start();
}
}

3.4.3 线程休眠(sleep)与线程等待(wait)的区别
  • sleep是Thread类的静态方法;wait是Object的普通方法。
  • sleep不会释放对象锁;而wait会释放对象锁。
  • wait的执行必须在同步方法或同步代码块中进行,而sleep则不需要。
  • wait和sleep都可以使当前线程休眠,并且在此期间被中断(调用interrupt())都会抛出InterruptedException。
3.5 线程合并:Thread.join

Thread类的普通方法:线程合并,即当前线程会等待调用join()的线程结束。join()方法底层通过 wait()/notifyAll() 实现。

public class ThreadJoin {

static int r = 0;

public static void main(String[] args) throws Exception{

Thread t1 = new Thread(() -> {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
});
t1.start();
t1.join();
System.out.println("r=" + r);
}
}

3.6 LockSupport 类

LockSupport是一个线程休眠工具类,所有的方法都是静态方法,可以让线程在任意位置被等待和唤醒,在等待和唤醒时不需要获取对象锁,因此比wait / notify 更加灵活。

  • void park():使当前线程进入无限期等待(WAITING)状态。
  • void parkNanos(long nanos) :使当前线程进入限期等待(TIMED_WAITING)状态。超过时间后自动被唤醒。
  • void unpark(Thread) :唤醒指定处于等待状态的线程。

4.线程优先级

Java中的优先级有10级,从1-10,数字越大,优先级越高。

  • Thread.MIN_PRIORITY = 1 :最小优先级
  • Thread.MAX_PRIORITY = 10 :最大优先级
  • Thread.NORM_PRIORITY = 5 :默认优先级

在线程启动之前(start),通过setPriority()可以设置线程的优先级。同样的,线程优先级越高只能代表该线程被操作系统调度的概率越大,并不是优先级高的线程一定会在优先级低的线程之前被执行。因此线程的调度依然具有不确定性和随机性。

5.守护线程(Daemon )

Java 中的线程分为两类:守护线程和用户线程。守护线程又被称为协程、后台线程。在JVM启动时会调用main 函数, main 函数所在的线程就是一个用户线程,JVM内的垃圾回收线程就是一个守护线程。

在线程启动之前(start),通过setDaemon(true)将线程设置为守护线程。当守护线程的用户线程结束运行,则用户线程不会等待守护线程运行结束,而是直接终止守护线程。

public class ThreadTest {

public static void main(String[] args) throws Exception{

Thread t = new Thread(() -> {

try {
System.out.println("start ADaemon...");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e){
System.out.println("catch InterruptedException...");
}finally {
System.out.println("finally...");
}
});

t.setDaemon(true);
t.start();

Thread.sleep(1);

System.out.println(Thread.currentThread().getName() + "---运行结束!");
}
}

输出:
start ADaemon...
main---运行结束!

上面的例子说明了当用户线程结束后,守护线程自动终止。并且守护线程的finally 代码块并没有被执行。因此,在构建守护线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。这也同时说明了finally代码块并不是一定会执行(守护线程被提前终止)。