线程和线程的六种状态

Published on 2024-08-27 16:25 in 分类: 博客 with 狂盗一枝梅
分类: 博客

一、线程简介

线程是操作系统能够进行运算调度的最小单位,同时也被称为轻量级进程(lightweight processes)。线程存在于进程中,是进程的实际运作单位。一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程的作用主要包括:

  1. 提高程序的执行效率:多线程允许程序中不同的执行流同时运行,这意味着程序可以更快地完成更多的工作。
  2. 充分利用多核处理器:在多核处理器上,多线程程序可以分配到多个处理器核心上并行执行,显著提升程序运行速度。
  3. 改善用户体验:例如,在图形用户界面(GUI)程序中,通过将耗时的任务放在一个单独的线程中执行,可以避免界面冻结,从而提升用户体验。
  4. 更好的资源共享:在同一进程中的多个线程可以共享进程所拥有的资源,如内存空间和文件句柄等。

具体的线程操作包括创建、启动、暂停、恢复、停止和终止线程。在如Java等高级语言中,可以通过实现Runnable接口或继承Thread类来创建线程,通过调用start()方法启动线程,通过run()方法来定义线程的执行体。

需要注意的是,多线程的使用应该谨慎,因为错误的线程操作可能会导致程序的性能反而下降,比如过多的线程切换开销,或者由于数据不一致导致的程序错误。因此在设计和实现多线程程序时,需要仔细考虑程序逻辑和线程安全。

二、线程启动

线程有两种启动方式:实现Runnable接口;继承Thread类并重写run()方法。

1、方法一:实现Runnable接口

public class Main1 {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程启动方式1:实现Runnable接口");
            }
        }).start();
    }
}

这种方式创建Runnable实例并重写Rnnable接口的run方法,并以构造函数的方式传给Thread实例,调用Thread实例的start方法启动线程。

通常情况下,实现Runnable接口然后启动线程是最好的选择,这可以提高程序的灵活性和扩展性,并且用Runnable接口描述任务也更容易理解。

2、方式二:继承Thread类

public class Main2 {
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                System.out.println("线程启动方式2:继承Thread类");
            }
        }.start();
    }
}

这种方式一般不推荐使用。

  • Java是单继承的语言:Java 是单继承的语言,这意味着如果您选择继承 Thread 类来创建线程,就无法再继承其他类。这可能会导致设计上的限制,特别是在需要扩展其他类的情况下。
  • 耦合性高:通过继承 Thread 类创建线程会将线程的执行逻辑和线程本身的定义耦合在一起,这使得代码难以维护和扩展。将任务和线程分离是更清晰和灵活的设计方式。
  • 推荐使用接口:Java 推荐使用实现 Runnable 接口的方式来创建线程。这种方式更符合面向对象设计原则中的“组合优于继承”原则,将任务和线程分离,提高了代码的灵活性和可维护性。
  • 线程池的管理:当使用线程池来管理线程时,更适合使用实现 Runnable 接口的方式。线程池可以更好地管理和重用线程,而继承 Thread 类则无法很好地与线程池结合使用。

因此,虽然可以通过继承 Thread 类来创建线程,但为了保持代码的清晰性、可维护性和可扩展性,推荐使用实现 Runnable 接口的方式来创建线程

3、Thread类和Runnable接口

Thread类默认实现了Runnable接口,并且其构造方法的重载形式允许传入Runnable接口对象作为任务。

image-20240826111152100

线程的两种启动方式,其本质都是实现Thread类中的run()方法。而实现Runnable接口,然后传递给Thread类的方式,比Thread子类重写run()方法更加灵活。

三、线程的六种状态

Thread类有个内部类:State,它描述了在java中线程的六种状态:NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED

image-20240826231029875

调用getState方法可以获取线程的当前状态。

1、新建状态:NEW

NEW代表着线程新建状态,一个已创建但是未启动(start)的线程处于NEW状态。

public class ThreadStartState {

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            //空方法
        });
        System.out.println(thread.getState());
    }
}

输出:

NEW

2、运行状态:RUNNABLE

RUNNABLE状态表示一个线程正在Java虚拟机中运行,但是这个线程是否获得了处理器分配资源并不确定,因此有可能它并没有在执行任务,这时候也可以称它为 READY 状态,该状态并没有纳入java的状态枚举中,但是它可以准确的描述出当前线程“虽然是Runnable状态,但是并没有在运行”这一状态。调用Thread的start()方法后,线程从NEW状态切换到了RUNNABLE状态。

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ThreadRunnableState {

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            log.info("线程{}正在执行任务", Thread.currentThread().getName());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                log.error("", e);
            }
            log.info("线程{}结束执行任务", Thread.currentThread().getName());
        });
        log.info("线程未执行任务状态:{}", thread.getState());
        thread.start();
        log.info("线程执行任务中状态:{}", thread.getState());
    }
}

运行结果:

2024-08-26 23:00:41.502 [INFO ] [main] - 线程未执行任务状态:NEW
2024-08-26 23:00:41.503 [INFO ] [main] - 线程执行任务中状态:RUNNABLE
2024-08-26 23:00:41.503 [INFO ] [Thread-1] - 线程Thread-1正在执行任务
2024-08-26 23:00:46.510 [INFO ] [Thread-1] - 线程Thread-1结束执行任务

3、阻塞状态:BLOCKED

BLOCKED为阻塞状态,表示当前线程正在阻塞等待获得监视器锁。当一个线程要访问被其他线程synchronized锁定的资源时,当前线程需要阻塞等待。

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ThreadBlockedState {

    private static final Object obj = new Object();

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                log.info("t1开始执行任务");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    log.error("", e);
                }
                log.info("t1线程结束执行任务");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (obj) {
                log.info("t2开始执行任务");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    log.error("", e);
                }
                log.info("t2线程结束执行任务");
            }
        });

        log.info("【1】t1线程状态:{}", t1.getState());
        log.info("【1】t2线程状态:{}", t2.getState());
        log.info("启动t1线程");
        t1.start();
        log.info("【2】t1线程状态:{}", t1.getState());
        log.info("【2】t2线程状态:{}", t2.getState());
        log.info("启动t2线程");
        t2.start();
        log.info("【3】t1线程状态:{}", t1.getState());
        log.info("【3】t2线程状态:{}", t2.getState());
        log.info("主线程等待1秒钟");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            log.error("", e);
        }
        log.info("【4】t1线程状态:{}", t1.getState());
        log.info("【4】t2线程状态:{}", t2.getState());
        log.info("主线程等待6秒钟");
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            log.error("", e);
        }
        log.info("【5】t1线程状态:{}", t1.getState());
        log.info("【5】t2线程状态:{}", t2.getState());
    }
}

执行结果如下

2024-08-26 23:37:36.415 [INFO ] [main      ] - 【1】t1线程状态:NEW
2024-08-26 23:37:36.417 [INFO ] [main      ] - 【1】t2线程状态:NEW
2024-08-26 23:37:36.417 [INFO ] [main      ] - 启动t1线程
2024-08-26 23:37:36.417 [INFO ] [main      ] - 【2】t1线程状态:RUNNABLE
2024-08-26 23:37:36.417 [INFO ] [main      ] - 【2】t2线程状态:NEW
2024-08-26 23:37:36.417 [INFO ] [main      ] - 启动t2线程
2024-08-26 23:37:36.417 [INFO ] [main      ] - 【3】t1线程状态:RUNNABLE
2024-08-26 23:37:36.417 [INFO ] [main      ] - 【3】t2线程状态:RUNNABLE
2024-08-26 23:37:36.417 [INFO ] [main      ] - 主线程等待1秒钟
2024-08-26 23:37:36.417 [INFO ] [Thread-1  ] - t1开始执行任务
2024-08-26 23:37:37.426 [INFO ] [main      ] - 【4】t1线程状态:TIMED_WAITING
2024-08-26 23:37:37.426 [INFO ] [main      ] - 【4】t2线程状态:BLOCKED
2024-08-26 23:37:37.426 [INFO ] [main      ] - 主线程等待6秒钟
2024-08-26 23:37:41.426 [INFO ] [Thread-1  ] - t1线程结束执行任务
2024-08-26 23:37:41.426 [INFO ] [Thread-2  ] - t2开始执行任务
2024-08-26 23:37:43.431 [INFO ] [main      ] - 【5】t1线程状态:TERMINATED
2024-08-26 23:37:43.431 [INFO ] [main      ] - 【5】t2线程状态:TIMED_WAITING
2024-08-26 23:37:44.435 [INFO ] [Thread-2  ] - t2线程结束执行任务

t1线程的线程状态:NEW->RUNNABLE->TIME_WAITING->TERMINATED

t2线程的线程状态:NEW->RUNNABLE->BLOCKED->TIME_WAITING->TERMINATED

t2线程在变成RUNNABLE状态之后尝试去执行任务,所以要先拿到监视器锁,此时监视器锁还在t1线程手里还未释放,所以t2变成了BLOCKED状态等待t1释放锁。

4、等待状态:WAITING

WAITING状态表示线程处于等待状态。根据源码中的注释说明,调用以下方法之一,会使当前线程变成WAITING状态

  • Object类的wait方法(不带超时设置)
  • Thread类的join方法(不带超时设置)
  • LockSupport类的park方法

处于等待状态的线程,正在等待另外一个线程去完成某个特殊操作。例如,在某个线程中调用了Object对象的wait()方法,它会进入等待状态,等待Object对象调用notify()或notifyAll()方法。一个线程对象调用了join()方法,则会等待指定的线程终止任务。

4.1 Object类的wait()/nofify()

下面用Object对象的wait方法为例,测试线程的WAITING状态。需要注意的是,调用Object对象的wait方法,必须先用synchronized关键字锁定Object对象。

线程进入WAITING状态之后,后续代码不再执行,直到Object对象调用了notify方法,线程才继续运行。

import lombok.extern.slf4j.Slf4j;

/**
 * @author kdyzm
 * @date 2024/8/27
 */
@Slf4j
public class ThreadWaitingState {

    private static final Object obj = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                log.info("t1开始执行任务");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    log.error("", e);
                }
                log.info("t1继续执行任务");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (obj) {
                log.info("t2发送notify通知");
                obj.notify();
            }
        });

        log.info("启动t1线程");
        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            log.error("", e);
        }
        log.info("t1线程状态:{}", t1.getState());
        log.info("t2线程状态:{}", t2.getState());
        log.info("启动t2线程");
        t2.start();
    }
}

运行结果:

2024-08-27 14:35:04.708 [INFO ] [main      ] - 启动t1线程
2024-08-27 14:35:04.709 [INFO ] [Thread-1  ] - t1开始执行任务
2024-08-27 14:35:05.717 [INFO ] [main      ] - t1线程状态:WAITING
2024-08-27 14:35:05.718 [INFO ] [main      ] - t2线程状态:NEW
2024-08-27 14:35:05.718 [INFO ] [main      ] - 启动t2线程
2024-08-27 14:35:05.719 [INFO ] [Thread-2  ] - t2发送notify通知
2024-08-27 14:35:05.719 [INFO ] [Thread-1  ] - t1继续执行任务

需要注意两点:

  • 调用wait和notify方法必须先获取对象锁,也就是说需要用synchronized关键字锁定Object对象
  • t1线程调用wait方法之后就会自动释放锁,这时候t2就可以进入同步代码块了。

4.2 LockSupport类的park()/unpark()

可以通过park()/unpark()实现同样的功能,代码示例如下

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
/**
 * @author kdyzm
 * @date 2024/8/27
 */
@Slf4j
public class ThreadWaitingState {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.info("t1开始执行任务");
            LockSupport.park();
            log.info("t1继续执行任务");
        });

        Thread t2 = new Thread(() -> {
            log.info("t2发送notify通知");
            LockSupport.unpark(t1);
        });

        log.info("启动t1线程");
        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            log.error("", e);
        }
        log.info("t1线程状态:{}", t1.getState());
        log.info("t2线程状态:{}", t2.getState());
        log.info("启动t2线程");
        t2.start();
    }
}

运行结果如下:

2024-08-27 14:45:27.862 [INFO ] [main      ] - 启动t1线程
2024-08-27 14:45:27.863 [INFO ] [Thread-1  ] - t1开始执行任务
2024-08-27 14:45:28.868 [INFO ] [main      ] - t1线程状态:WAITING
2024-08-27 14:45:28.872 [INFO ] [main      ] - t2线程状态:NEW
2024-08-27 14:45:28.872 [INFO ] [main      ] - 启动t2线程
2024-08-27 14:45:28.872 [INFO ] [Thread-2  ] - t2发送notify通知
2024-08-27 14:45:28.873 [INFO ] [Thread-1  ] - t1继续执行任务

可以看到和上面的wait()/notify()方法的运行结果是一样的。但是和wait()/notify()不一样的是

  • park()/unpark()方法调用不需要监视器锁,直接调用即可。

  • park()/unpark()是基于线程的,而wait()/notify()是基于对象锁(监视器锁)的

  • 传统的等待唤醒机制场景一般选择wait()/notify()组合,有些高度定制的场景则可以选择park()/unpark组合。

4.3 Thread类的join()

join方法的使用场景:一个线程等待另外一个线程结束,比如thread.join(),表示当前线程要等待thread线程结束后才能继续自己的任务。

代码示例如下

import lombok.extern.slf4j.Slf4j;

/**
 * @author kdyzm
 * @date 2024/8/27
 */
@Slf4j
public class ThreadWaitingStateJoinExample {

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.info("t1开始执行任务...");
            try {
                Thread.sleep(2000); // Simulate some work
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("t1结束执行任务.");
        });

        Thread t2 = new Thread(() -> {
            log.info("t2等待t1执行完任务...");
            try {
                t1.join();
            } catch (InterruptedException e) {
                log.error("", e);
            }
            log.info("t2结束执行任务.");
        });

        t1.start();
        t2.start();
        log.info("t1线程状态:{}",t1.getState());
        log.info("t2线程状态:{}",t2.getState());
        
    }
}

运行结果:

2024-08-27 15:17:28.004 [INFO ] [Thread-1  ] - t1开始执行任务...
2024-08-27 15:17:28.004 [INFO ] [Thread-2  ] - t2等待t1执行完任务...
2024-08-27 15:17:28.004 [INFO ] [main      ] - t1线程状态:RUNNABLE
2024-08-27 15:17:28.005 [INFO ] [main      ] - t2线程状态:WAITING
2024-08-27 15:17:30.008 [INFO ] [Thread-1  ] - t1结束执行任务.
2024-08-27 15:17:30.009 [INFO ] [Thread-2  ] - t2结束执行任务.

5、定时等待状态:TIMED_WAITING

这种状态之前的案例中已经出现过了,在线程中调用Thread.sleep方法,线程的状态就会变成TIMED_WAITING

根据源码中的注释说明,共有以下几种情况会让线程的状态变成TIMED_WAITING

  • Object类的wait()方法(有超时设置)
  • Thread类的join()方法(有超时设置)
  • Thread类的sleep()方法
  • LockSupport类的parkNanos ()方法
  • LockSupport类的parkUntil()方法

Object类的wait()方法和Thread类的join、sleep方法之前的例子都有提过,不再演示,这里说下LockSupport类的parkNanos方法和parkUntil方法。

parkNanos和parkUntil方法都是用于暂停线程一段时间用的

  • parkNanos方法用于暂停线程一段时间,参数是纳秒,表示过了多长时间之后再继续执行任务
  • parkUntil方法是"暂停到什么时间",参数是个时间戳,表示到了那个时间再继续执行任务

这里有必要科普下“纳秒”的概念:

1秒 = 1000毫秒

1毫秒 = 1000微秒

1微妙 = 100,0000纳秒

1纳秒 = 1000皮秒

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
/**
 * @author kdyzm
 * @date 2024/8/27
 */
@Slf4j
public class ThreadWaitingStateJoinExample {

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.info("t1开始执行任务...");
            LockSupport.parkNanos(1000000 * 2000);
            log.info("t1结束执行任务.");
        });

        Thread t2 = new Thread(() -> {
            log.info("t2开始执行任务...");
            LockSupport.parkUntil(System.currentTimeMillis() + 2000);
            log.info("t2结束执行任务.");
        });

        t1.start();
        t2.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            log.error("", e);
        }
        log.info("t1线程状态:{}", t1.getState());
        log.info("t2线程状态:{}", t2.getState());
    }
}

运行结果:

2024-08-27 15:44:53.468 [INFO ] [Thread-1  ] - t1开始执行任务...
2024-08-27 15:44:53.468 [INFO ] [Thread-2  ] - t2开始执行任务...
2024-08-27 15:44:54.471 [INFO ] [main      ] - t1线程状态:TIMED_WAITING
2024-08-27 15:44:54.474 [INFO ] [main      ] - t2线程状态:TIMED_WAITING
2024-08-27 15:44:55.482 [INFO ] [Thread-1  ] - t1结束执行任务.
2024-08-27 15:44:55.482 [INFO ] [Thread-2  ] - t2结束执行任务.

注意写法:LockSupport.parkNanos(1000000 * 2000); 以及LockSupport.parkUntil(System.currentTimeMillis() + 2000);都是暂停两秒的意思。

6、完成状态:TERMINATED

TERMINATED表示线程为完结状态。当线程完成其run()方法中的任务,或者因为某些未知的异常而强制中断时,线程状态变为TERMINATED。这个前面的例子中也有所体现。

import lombok.extern.slf4j.Slf4j;

/**
 * @author kdyzm
 * @date 2024/8/27
 */
@Slf4j
public class ThreadTerminatedState {

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            log.info("线程执行任务");
        });
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            log.error("", e);
        }
        log.info("线程的当前状态:{}", thread.getState());
    }
}

运行结果:

2024-08-27 15:51:27.638 [INFO ] [Thread-1  ] - 线程执行任务
2024-08-27 15:51:27.639 [INFO ] [main      ] - 线程的当前状态:TERMINATED

四、线程状态转化

根据上面三的详细线程状态分析,整理出线程状态转化图

java线程状态转化图-一枝梅的博客

这里需要注意的是RUNNABLE状态在这里被拆分成了RUNNING状态和READY状态,但这在JAVA中并没有体现,实际上将RUNNABLE状态拆分为RUNNINGREADY状态可以更细致地描述线程的行为,更好地反映线程在系统中的执行情况。

RUNNABLE状态下的线程可以被操作系统调度为RUNNING状态(正在执行)或者READY状态(准备好执行但还未获得CPU执行时间)。

  • RUNNING状态表示线程正在执行指令,占用CPU资源。
  • READY状态表示线程已经准备好执行,但尚未获得CPU执行时间。线程在READY状态下等待操作系统的调度器将其调度为RUNNING状态,以便真正执行代码。

RUNNING状态和READY状态转换:

当线程处于READY状态时,如果被调度器选中,它会转换为RUNNING状态开始执行。

当线程处于RUNNING状态时,它可能会由于时间片用尽、等待I/O操作等原因,而转换为READY状态,等待下一次调度;也可能会因为某些原因(如被其他高优先级线程抢占)而转换为READY状态。


#java #多线程编程
目录
复制 复制成功