Loading... ### 线程的概念 **进程与线程** - **进程**:是一个可执行的程序,是系统分配资源的基本单位。一个进程往往可以包含多个线程,至少包含一个线程。一个JAVA进程中至少会启动线程,一个是main线程,一个是GC线程。进程与进程之间是相互独立的。 - **线程**:线程是进程内部相对独立的可执行单元,是任务调度的基本单位。一个进程内部的各个线程之间并不完全独立,可以共享进程的方法区内存、堆内存、系统资源。 **CPU时间片** **CPU的计算频率非常高,每秒可达数十亿次,因此可以将CPU的时间从毫秒的维度进行分段,每一个小段叫作一个时间片。**操作系统中主流的线程调度方式是基于CPU时间片方式进行线程调度。线程只有得到CPU时间片才能执行指令,处于执行状态,没有得到时间片的线程处于就绪状态,等待系统分配下一个CPU时间片。由于时间片非常短,在各个线程之间快速切换,因此表现出来的特征是很多个线程在“同时执行”或者“并发执行”。 ### Java创建线程的方式 在未了解线程池之前,Java中线程的创建方式有以下几种: #### 继承Thread类 - 定义Thread类的子类,并重写run方法 - 创建Thread子类的对象实例 - 调用start方法启动线程 ```java public class ThreadTest { public static void main(String[] args) { //创建线程对象 MyThread1 myThread1 = new MyThread1(); //调用start方法启动线程 myThread1.start(); } } class MyThread1 extends Thread { @Override public void run() { System.err.println(Thread.currentThread().getName()); } } ``` #### 实现Runnable接口 - 定义Runnable接口的实现类,实现run方法 - 创建Runnable接口实现类的对象实例,并通过Thread类的有参构造创建Thread对象 - 调用Thread对象的start方法启动线程 ```java public class ThreadTest { public static void main(String[] args) { //创建Runnable接口实现类实例 Runnable target = new MyThread2(); //创建线程对象 Thread myThread2 = new Thread(target); //调用start方法启动线程 myThread1.start(); } } class MyThread2 implements Runnable { @Override public void run() { System.err.println(Thread.currentThread().getName()); } } ``` - 通过Runnable函数式接口的Lambda表达式,本质上还是实现Runnable接口。 ```java public class ThreadTest { public static void main(String[] args) { Thread myThread = new Thread(() -> { System.out.println(Thread.currentThread().getName()); }); myThread.start(); } } ``` **Thread.start()方法和Thread.run()方法的区别?** - Thread.start()方法会调用native方法创建一个新的线程,并在新的线程执行run()方法 - Thread.run()方法只是一个普通的方法,直接调用run方法,会在当前线程直接执行run方法的内容。 **Thread继承和Runnable接口实现的区别?** - 实现Runnable接口可以避免Java单继承带来的局限性。 - 实现Runnable接口可以使逻辑和数据更好分离,在同一个资源被多个线程逻辑一部、并行处理的场景。实现Runnable接口作为Thread的target构造参数进行线程创建,各个线程拥有的是同一份资源。而Thread类创建线程,各个线程拥有的是不同的资源。 #### 实现Callable接口 Callable接口位于java.util.concurrent包中,是泛型接口,也是一个函数式接口。且允许方法内部的异常直接抛出。 Thread的target只能属于Runnable接口,Runnable和Callable可以通过FutureTask类作为搭桥。 - 创建Callable接口实现类,实现call()方法,即异步执行的具体逻辑。 - 使用Callable实现类的实例构造FutureTask实例。 - 使用FutureTask实例作为Thread构造器的target入参。 - 启动Thread实例类,FutureTask在调用run方法时,会调用call方法。 ```java public class ThreadTest { private static int count = 0; public static void main(String[] args) { ReturnableTask task = new ReturnableTask(); FutureTask<Boolean> futureTask = new FutureTask<Boolean>(task); Thread thread = new Thread(futureTask); thread.start(); try { futureTask.get();//获取执行结果 } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } class ReturnableTask implements Callable<Boolean> { @Override public Boolean call() throws Exception { //具体的异步执行逻辑,允许返回值,允许抛出异常 return true; } } ``` ### 线程的优先级 Java中使用抢占式调度模型进行线程调度。系统会按照线程优先级分配CPU时间片,优先级高的线程优先分配CPU时间片,如果所有就绪线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。 Thread类中有一个实例属性专门用于设置线程的优先级,默认是5,最大值为10,最小值为1。 ```java private int priority; ``` Thread类设置的值越大,优先级越高,线程获得CPU时间片的机会越多,但是不代表绝对优先分配CPU时间片。 ### Java线程的生命周期 在操作系统中线程的生命周期包含5个阶段: - 新建:一个新的线程被创建,等待该线程被调用执行; - 就绪:表示线程正在等待系统调度分配CPU使用权,或者时间片已用完,此线程被强制暂停,等待下一个属于它的时间片; - 运行:表示线程获得了CPU使用权,正在占用时间片进行运算。 - 等待:表示线程等待,等待某一事件执行完,让出CPU资源给其他线程使用。 - 退出:一个线程完成任务或者其他终止条件发生,该线程进入退出状态,退出状态释放该线程所分配的资源。 Java中在Thread类中定义了一个内部枚举类State,其中定义了以下六种状态 - NEW:新建状态,尚未启动的线程的线程状态。 - RUNNABLE:可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行,但它可能正在等待CPU分配时间片。Java线程中把操作系统中就绪和运行两种状态统一称为”运行中“。 - Blocked:表示线程进入阻塞状态,通常与锁有关系,比如正在获取有锁控制的资源,如进入synchronized代码块,获取ReentryLock等。 - WAITING:等待状态,进入该状态的线程,等待被显示唤醒,比如其他线程执行notigy或notifyAll,或者另外的线程终止如join的情况,否则处于无限期等待。 - TIMED_WAITING:超时等待,在指定的时间后自动唤醒。比如Thread.sleep()方法。 - TERMINATED:终止状态,表示该线程已执行完毕。终止的线程不允许在调用Thread.start()方法。 他们的对应关系可以从以下图反应: ![](https://qncloud.smalleyes.wang/blog/lifecircle.png) ### Thread类基本操作 #### 设置线程名称 可以通过构造器或者Thread实例的setName()方法设置线程名称,获取线程名称则可通过getName()方法完成。 - 线程名称应该在线程启动前设置。 - 允许两个Thread对象有相同的名称,但是应该避免。 - 如果没有为线程指定名称,系统会自动为线程设置名称,格式为Thread-{nextThreadNum},如Thread-0。 ```java Thread t1 = new Thread("thread1"); t1.setName("thread1"); //构造函数中自动设置名称 public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); } ``` #### sleep操作 sleep的作用是让目前正在执行的线程休眠,让CPU去执行其他的任务。线程状态从执行状态变成限时等待状态。从RUNNABLE状态变为TIMED_WAITING状态。当线程睡眠时间结束后,线程不一定会立即执行,处于就绪状态,等待分配CPU时间片以便有机会执行。 sleep方法是Thread的静态方法,有两个重载版本。 ```java public static native void sleep(long millis) throws InterruptedException; public static void sleep(long millis, int nanos) throws InterruptedException; ``` 但你仍然可以粗略的认为只有一个方法实现,因为大部分操作系统并不能足够惊喜的分辨一次睡眠几纳秒。 ```java 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"); } //仅仅在这里判断了nanos的值,且精度都直接在毫秒,类似四舍五入。 if (nanos >= 500000 || (nanos != 0 && millis == 0)) { millis++; } sleep(millis); } ``` #### interrupt操作 Thread的interrupt方法,可以将线程设置为中段状态。 - 如果线程处于阻塞状态,就会立马退出阻塞,抛出InterruptedException。 - 如果想编程正在处于运行之中,线程不受任何影响,继续运行,仅仅是线程的中段标记被设置为true。程序可以通过调用isInterrupted方法确认是否中断,并执行退出操作。 #### join操作 join指的是线程的合并。比如有两个线程A和B,线程A对线程B的执行有依赖,需要将线程B的执行流程合并到自己的执行流程中,这就是线程合并,被动方线程可以叫作被合并线程。 join在Thread类中有三个重载方法: ```java //此方法会把当前线程变为WAITING,直到被合并线程执行结束 public final void join() throws InterruptedException; //此方法会把当前线程变为TIMED_WAITING,直到被合并线程执行结束或者等待被合并线程执行millis的时间。 public final synchronized void join(long millis) throws InterruptedException; //此方法会把当前线程变为TIMED_WAITING,直到被合并线程执行结束或者等待被合并线程执行millis的时间。同sleep方法,并未完全实现nanos public final synchronized void join(long millis, int nanos) throws InterruptedException; ``` ![](https://qncloud.smalleyes.wang/blog/join.png) - join()方法是实例方法,需要使用被合并线程的实例变量调用,如threadb.join()。执行threadb.join()这行代码的当前线程被称为合并线程(甲方),进入TIMED_WAITING或WAITING状态,让出CPU资源,等待Threadb执行结束。 - 如果设置了被合并线程的执行时间,并不能保证当前线程一定会在该时间后变为Runnable。 - 如果主动方合并线程在等待时被中段,就会抛出InterruptedException受检异常。 - 调用join方法的语句可以理解为合并点,合并的本质是:线程A需要再合并点等待线程B执行完成或者超时。 #### yield操作 线程的yield(让步)操作的作用是让目前正在执行的线程放弃当前的执行,让出CPU的执行权限,使得CPU去执行其他的线程,处于让步状态的JVM层面的线程状态仍然是RUNNABLE状态,在操作系统层面的线程状态会从执行状态变成就绪状态。 yield操作是不确定的,线程放弃和重占CPU的时间是不确定的,可能刚放弃,就又获得执行权限。 yield方法是Thread类的静态方法,不会阻塞调用的当前线程,只会让当期那线程暂停以下,让系统的线程调度器重新调度。 - 从运行状态变为就绪状态 - 不能保证状态的迅速切换 - 即使完成了迅速切换,系统通过线程调度机制从所有就绪线程中挑选下一个执行线城时,可能被选中,也可能不被选中。其调度过程受优先级或其他因素的影响。 #### 守护线程 Java中的线程分为两类:守护线程与用户线程。守护线程也称为后台线程,在程序运行过程中,在后台提供某种通用服务的线程。比如GC线程就是守护线程。 JVM中只要存在用户线程,守护线程就能执行自己的工作。当最后一个用户线程结束,守护线程随着JVM一同结束工作。 设置方法:thread实例对象,setDaemon(true)。 - 必须在启动之前设置守护状态为true,才能生效。启动后设置会抛出InterruptedException。 - 守护线程创建的线程也是守护线程,除非显示指定创建线程的daemon值为false。 - 守护线程存在被JVM强行终止的风险,尽量不要在守护线程中访问系统资源,如文件句柄、数据库连接等。 #### Thread状态总结 Java线程6种状态进入条件总结: - NEW状态 通过new Thread(...)已经创建线程,但未调用start()方法启动线程。 - RUNNABLE状态 是线程Ready(就绪)和Running(运行)两种状态的合并状态。 - 就绪状态:线程具备运行资格,没有被操作系统的调度程序挑选中。 - 调用线程的start()方法,此线程就会先进入就绪状态。 - 当前线程的执行时间片用完。等待下一个时间片的分配。 - 线程睡眠(sleep)操作结束。 - 对其他线程合入(Join)操作结束。 - 等待用户输入结束 - 线程争抢到对象锁。 - 当前线程调用了yield方法让出CPU执行权限 - 执行状态:线程调度程序从就绪状态的线程中选择一个线程,被选中的线程获取到CPU时间片,开始执行,变为执行状态。 - BLOCKED状态 处于BLOCKED阻塞状态的线程并不会占用CPU资源,以下情况会让线程进入阻塞状态: - 线程等待获取锁,该锁被其他线程持有。 - IO阻塞:线程发起了一个阻塞式IO操作,比如线程等待用户输入内容后继续执行。 - WAITING状态 处于WAITING(无限期等待)状态的线程不会被分配CPU时间片,需要被其他线程显示地唤醒,才会进入就绪状态。 - Object.wait()方法。 - Thread.join()方法。 - LockSupport.park()方法。 - TIMED_WAITING状态 处于限时等待状态,线程需要被其他线程显示唤醒或者等待时间结束系统自动唤醒,进入就绪状态。 - Thread.sleep(time)。 - Object.wait(time)。 - LockSupport.parkNanos(time)/parkUntil(time) 进入BLOCKED、WAITING、TIMED_WAITING状态的线程都会让出CPU的使用权。等待或者阻塞状态的线程被唤醒后,进入Ready状态,需要重新获取时间片才能接着运行。 - TERMINATED状态 - 线程结束任务之后,将会正常进入TERMINATED(终止/死亡)状态。 - 线程执行过程中发生了未被处理的异常 ![](https://qncloud.smalleyes.wang/blog/lc.png) © Allow specification reprint Support Appreciate the author AliPayWeChat Like 1 If you think my article is useful to you, please feel free to appreciate