Java并发编程-线程基础

本文详细讲解了Java并发编程的基础知识,包括进程与线程的区别、并发与并行的差异、线程的创建方式(如继承Thread、实现Runnable和Callable接口)、线程状态以及线程方法如start、run、sleep、yield、wait。还探讨了线程的上下文切换、线程池的使用以及守护线程和用户线程的概念。此外,文章还涉及了CPU占用过高时的分析思路和如何查看线程信息。
摘要由CSDN通过智能技术生成
  • 哈喽,大家好,这篇文章主要讲解Java并发编程,线程基础的知识点,讲解知识点的同时也会穿插面试题的讲解.

主要讲解以下内容

  • 进程,线程基础常见的一些区别比较
  • 如何使用和查看线程
  • 理解线程如何运行以及线程上下文切换的知识
  • 线程方法
  • 线程状态

希望能给大家带来帮助.加油~~~

目录

进程,线程基础常见的一些区别比较

进程与线程的区别

并发与并行的区别

Java中守护线程和用户线程的区别?

​编辑

说下同步、异步、阻塞和非阻塞

JDK19中的虚拟线程了解么? /了解协程么?

线程的使用与查看

线程创建的方式有哪些?/Java中线程的实现方式?

继承Thread类

普通类继承Thread类

匿名内部类

继承Thread类的缺点

实现Runnable接口

普通类实现Runnable接口

匿名Runnable实现类

使用lambda表达式

实现Runnable接口的缺点

实现Callable接口创建线程

普通类实现Callable接口

匿名callable实现类

实现Callable接口来创建线程分析

使用线程池创建线程

Runnable与Callable的区别?

Thread 与 Runnable 的关系

线程类的构造方法,静态块是被哪个线程调用的?

cpu占用过高的分析思路

如何查看进程线程的方法

线程上下文切换/如何理解线程的运行

栈与栈帧

 多线程运行的原理

多线程运行原理

什么是线程上下文切换

发生上下文切换的原因

线程数越多越好么 ?

线程方法

start与run

start与run的区别

为什么调用start()方法时会执行run()方法,而不直接执行run()方法?

为什么start方法不能重复调用?而run方法却可以?

可以直接调用 Thread 类的 run 方法吗?

sleep,yield,wait

sleep与yield的区别

sleep与wait的区别

为什么 wait() 方法不定义在 Thread 中?

为什么 sleep() 方法定义在 Thread 中?

线程的优先级

Thread.sleep(0)的作用是什么?

join方法

interrupt方法

如何停止一个正在运行的线程? /如何优雅的关闭线程

错误停止线程的方式

interrupt, isInterrupted, interrupted的区别是什么

notify和notifyAll有什么区别

线程的状态

线程的5种状态

线程的六种状态

线程状态之间的变化

线程死亡的几种情况

线程阻塞的几种情况


进程,线程基础常见的一些区别比较

进程与线程的区别

什么是进程 ? 

比如我们打开一个程序,就相当于开启一个进程,我们知道程序是由指令和数据构成的,这些指令要运行,数据要读写就必须将指令加载到CPU,数据加载到内存中,指令运行的过程中需要用到磁盘,网络等设备,进程就是用来加载指令,管理内存,管理IO的.

总结一句话 : 当一个程序被运行,从磁盘加载这个程序的代码到内存,这时就开启了一个进程.

什么是线程 ? 

个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行.

一个进程包含了多个线程,Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。

总结一下进程与线程的区别:

  • 进程是资源分配的基本单位,线程是程序执行/系统调度的基本单位;
  • 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
  • 进程拥有独立的内存空间和资源,而线程则共享进程的内存和资源
  • 进程之间的通信比较复杂,而线程通信相对简单,他们共享进程中的资源
  • 线程更加轻量,线程的上下文切换成本一般要比进程的上下文切换低

并发与并行的区别

对于单核CPU : 线程实际上还是串行执行的.

由于操作系统有一个组件叫做任务调度器,他会将CPU的时间片分给不同的程序使用,只是由于CPU在线程间的切换速度非常快,所以我们人总感觉是同时执行的.

即 : 微观串行,宏观并行.

我们把 对于线程轮流使用CPU的这种做法叫做并发.

在多核CPU下,才能真正的可以做到并行执行,也就是多个CPU核心都可以调度运行线程.

总结一下 :

  • 并发是同一时间应对多件事情的能力,比如多个线程轮流使用一个或者多个CPU
  • 并行是同一时间动手做多件事情的能力,比如4核CPU同时执行4个线程

现在大多数都是多核CPU,在多核CPU下 , 没有绝对的并发和绝对的并行,大多数情况下都是既有并发又有并行.

Java中守护线程和用户线程的区别?

用户线程 : 默认情况下我们创建的线程或线程池都是用户线程,所以用户线程也被称之为普通线程。
守护线程 : 守护线程是为用户线程服务的,一般是指后台线程. 

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。当程序中的用户线程全部执行结束之后,守护线程也会结束.

  • 垃圾回收器线程:  垃圾回收就是会将堆中的没有被引用的对象回收掉,当程序停止的时候,垃圾回收线程也会停止
  • 守护线程的设置 setDaemon(true) 必须要放在线程的 start() 之前,否则程序会报错。也就是说在运行线程之前,一定要先确定线程的类型,并且线程运行之后是不允许修改线程的类型的。
@Slf4j(topic ="c.TestDemo1")
public class TestDemo1 {
    public static void main(String[] args) throws InterruptedException {
        log.debug("开始运行...");
        Thread t1 = new Thread(() -> {
            log.debug("开始运行...");
            try {
                sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("运行结束...");
        }, "daemon");

        // 设置该线程为守护线程
        t1.setDaemon(true);
        t1.start();
        sleep(1);
        log.debug("运行结束...");
    }
}

说下同步、异步、阻塞和非阻塞

JDK19中的虚拟线程了解么? /了解协程么?

线程的使用与查看

线程创建的方式有哪些?/Java中线程的实现方式?

线程的创建/实现方式有四种

  • 继承Thread类
    • 普通类继承Thread类
    • 匿名内部类
  • 实现Runnable接口
    • 普通类实现Runnable接口
    • 匿名Runnable实现类
    • 使用lambda表达式
  • 实现Callable接口
    • 普通类实现Callable接口
    • 匿名callable实现类
  • 使用线程池创建线程

继承Thread类

普通类继承Thread类
//使用普通类继承Thread
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("我使用的是普通类来继承Thread类");
    }
}
public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
    }
}
匿名内部类
public class ThreadDemo2 {
    //使用匿名内部类的方式创建线程
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("我是run方法");
            }
        };
        //创建线程
        t.start();
    }
}
继承Thread类的缺点
  • 使用继承Thread类的方式创建新线程,最大的局限就是不支持多继承,因为Java语言的特点就是单继承.继承了Thread类就不能继承其他类了

实现Runnable接口

普通类实现Runnable接口
class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("我使用普通类实现Runnable接口来创建线程");
    }
}
public class ThreadDemo3 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread t = new Thread(myRunnable);
        t.start();
    }
}
匿名Runnable实现类
public static void main(String[] args) {
    //使用匿名的Runnable实现类
    Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("我是run方法");
        }
    });
    //启动线程
    t.start();
}
使用lambda表达式
public static void main(String[] args) {
    //使用Lambda表达式
    Thread t = new Thread(()->{
        System.out.println("我是Lambda创建的线程");
    });
    //启动线程
    t.start();
}
实现Runnable接口的缺点

实现Runnable接口和继承Thread类的缺点都是不能获取线程执行的结果.但是如果是在不要求得到线程执行的返回结果的情况下就使用Lambda表达式最简洁

FutureTask也要需要与callable进行配合使用,futureTask参数要加上callable

实现Callable接口创建线程

普通类实现Callable接口
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for(int i =0;i<100;++i){
            sum += i;
        }
        return sum;
    }
}
public class ThreadDemo6 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        int res = futureTask.get();
        System.out.println(res);
    }
}
匿名callable实现类
public static void main(String[] args) throws ExecutionException, InterruptedException {
    //使用匿名callable实现类
    FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            int sum = 0;
            for(int i =0;i<100;++i){
                sum += i;
            }
            return sum;
        }
    });
    Thread t = new Thread(futureTask);
    t.start();
    int res = futureTask.get();
    System.out.println(res);
}
实现Callable接口来创建线程分析

callable接口要作为参数放到FutureTask对象中.

根据源码分析 FutureTask 是间接实现了 Runnable接口,所以FutureTask也可以作为一个任务对象,他还有一个future接口,这个接口可以用于得到线程执行的结果,通过futureTask.get()就可以获取到线程执行结果交给其他线程来使用. 而对于Runnable是不能得到线程执行的结果

  • 首先要创建好Callable实例(或者使用匿名内部类的方式),将Callable实例传入FutureTask的构造方法参数中
  • 然后创建线程对象,将FutureTask对象传入到线程对象构造参数中.
  • 当启动线程后,就会执行callable的call方法,然后将结果返回给FutureTask对象保存起来
  • 然后线程执行完毕之后,可以通过futureTask.get()得到线程执行的结果

如果需要线程的执行结果,那就可以使用实现Callable接口的方式

使用线程池创建线程

//使用线程池来创建线程
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("使用线程池来创建线程!!!");
    }
});

Runnable与Callable的区别?

首先实现Runnable接口和实现Callable接口都可以创建线程

两者的区别 :

  • Callable的任务执行后有返回值,而Runnable的任务是没有返回值的. , 运行Callable任务可以拿到一个Future对象, 可以得到异步计算的结果. 对于Runnable不能获取线程的执行结果
  • Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()
  • Callable接口的call() 方法允许抛出异常;而Runnable接口的run方法的异常只能内部消化,不能继续上抛.

Thread 与 Runnable 的关系

Thread t1 = new Thread(() ->{
    System.out.println("xxxx");
}
);
Runnable runnable = ()->{
    System.out.println("xxx");
};
Thread t2 = new Thread(runnable);
  • 通过源码我们可以发现,他们最终都是调用的是run方法,有Runnable就调用Runnable的run方法
  • 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
  • Runnable 更容易与线程池等高级 API 配合
  • Runnable 让任务类脱离了 Thread 继承体系,更灵活 ,组合优于继承

线程类的构造方法,静态块是被哪个线程调用的?

线程类的构造方法和静态块是由主线程调用的。

在Java程序中,当我们创建一个线程对象时,实际上是在主线程中创建了一个新的线程对象。这个新线程的构造方法和静态块都是在主线程中被调用执行的。

具体来说,在主线程中创建线程对象时,会调用线程类的构造方法来初始化线程对象,并执行构造方法中的逻辑。如果线程类中有静态块,主线程在加载这个类的时候会执行静态块中的代码,完成静态资源的初始化工作。

一旦创建完线程对象,我们可以通过调用start()方法启动线程,此时主线程会调用新线程的run()方法,新线程开始独立执行,并与主线程并发运行。

cpu占用过高的分析思路

  • top -H -p 进程id  -->查看进程中的线程信息 ,先使用top命令找出CPU占比最高的 到底是哪一个进程CPU过高
  • 查看线程信息 : ps H -eo pid,tid,%cpu | grep 进程pid 进一步知道进程中的哪一个线程CPU过高

  • 可以使用jstack 进程id->pid  得到该进程的所有线程栈的信息 ->线程的具体信息

  • 因为上一步之前得到的tid是10进制的,所以需要把cpu过高的那个线程的tid转换成16进制,在线程栈信息中找该16进制的tid

如何查看进程线程的方法

windows
  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程
linux
  • ps -ef | grep 进程名称
  • 查看该进程及其所有子线程:ps -efL|grep PID

  • kill 杀死进程
  • 用于查看系统中所有线程,比如:top -H

  • top -H -p <PID> 查看某个进程(PID)的所有线程
Java
  • jps 命令查看所有 Java 进程,以及进程pid
  • jstack <PID> 查看某个 Java 进程(PID)的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

线程上下文切换/如何理解线程的运行

先理解栈帧的概念,我们一步一步了解线程的运行原理

栈与栈帧

JVM是由堆,栈,方法区所组成.我们的栈内存其实就是给我们线程来使用的.每一个线程启动后虚拟机就会为其分配一块栈内存

每一个栈是由多个栈帧(Frame)组成的,栈帧就是一次方法调用所占用的内存

每一个线程只能有一个活动栈帧,就是对应着当前正在执行的方法

每一个栈帧对应一个方法的调用

我们启动main方法的时候==>对应着就启动一个主线程,主线程一启动,虚拟机就会为主线程分配一块栈内存(由许多栈帧组成),调用main方法就会为main方法产生一个栈帧

在调用方法就会对其方法在产生一个栈帧(压栈),方法执行完之后,栈内存就会被释放,同时调用的时候记录返回地址,执行完就回到返回地址处,然后继续执行

我们可以发现每调用以此方法都会有栈帧入栈,当方法运行结束之后,栈帧会弹出 , 同时调用的时候记录返回地址,执行完就回到返回地址处,然后继续执行

 多线程运行的原理

多线程运行原理

public class TestThreadDemo1 {
    public static void main(String[] args) {
        Thread t = new Thread("t1"){
            @Override
            public void run() {
                method(10);//t1线程调用的方法
            }
        };
        t.start();
        method(10);//主线程调用的方法
    }
    public static void method(int x){
        int y = x + 1;
        Object m = method2();
        System.out.println(m);
    }
    public static Object method2(){
        Object n = new Object();
        return n;
    }
}

总结一下就是 :

多线程运行的时候,每一个线程都会有自己的栈帧,线程的栈内存相互独立(每个线程拥有自己的独立的栈内存,栈内存里面有多个栈帧),互不干扰,当系统内没有任何线程执行的时候,整个程序就结束了

什么是线程上下文切换

在多线程运行的时候,CPU过时间片分配算法来循环执行任务 , 任务调度器会给每一个线程分配CPU时间片,当一个线程执行完一个时间片的任务之后就会切换到下一个任务,就会把CPU的使用权交给其他线程,在切换前我们也要保存上一个任务的状态,以便下一次切换回这个任务的时候,可以加载这个任务的状态,继续向下运行,该线程从使用CPU->不使用CPU 就是一次线程的上下文切换(从保存到再加程就是一次上下文切)

发生上下文切换的原因

被动发生上下文切换 

  • 时间片用完

也就是当任务调度器将CPU时间片分配给每一个线程运行,这个时间片总有用完的时候,时间片用完的时候,CPU的使用权就会交给其他线程,这个时候对于当前线程就是发生一次上下文切换.

  • 垃圾回收

垃圾回收会暂停当前工作的线程(就会出现上下文切换),就会让垃圾回收的线程开始回收垃圾

  • 更高优先级的线程要运行

CPU的使用权就要交给优先级更高的线程来使用,当前线就会暂停(就是一次线程的上下文切换),当这个优先级更高的线程运行完了之后,由任务调度器会重新为当前线程分配CPU时间片

主动发生上下文切换

  • 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法

当完成一次线程的上下文切换的时候,操作系统就要保存当前线程的状态(包括需要使用程序计数器记住下一条JVM的指令执行地址,还有一些状态信息,虚拟机栈的栈帧信息,如局部变量,操作数栈,返回地址....),并且恢复另一个线程的状态

为啥要保存当前线程的状态呢 ?

原因是当我们下次在运行的时候,CPU就知道要从哪开始运行了.

使用程序计数器就可以保存当前代码执行到哪里了(也就 是记住下一跳JVM指令的地址).

频繁的上下文切换会影响性能.

并不是线程数越多越好,当CPU核心数 < 线程数,线程的上下文切换的成本就会提高.频繁的上下文切换就会影响性能.

线程数越多越好么 ?

不是的,当线程数超过CPU核心数的时候,CPU就要在这么多线程轮流切换, 线程的上下文切换成本会提高,频繁的上下文切换影响程序的性能.

线程方法

start与run

start与run的区别

  • 调用start方法才是真正的启动一个新的线程执行,调用run方法只是执行了一个普通方法不没有真正的创建线程让线程执行,run方法只是封装了要被线程执行的任务
  • 调用start方法不会立即执行任务 , 调用run就可以立即执行. 

因为调用start方法的时候不是立即执行线程任务,而是从新建状态变为就绪状态,就绪状态就是已经获取到了除CPU资源以外的其他资源,等待任务调度器分配给当前线程CPU时间片,才会真正的执行任务. 所以调用start方法不会立即执行任务. 但直接调用run,run相当于是普通方法,可以立即执行

  • 重复调用start方法就会抛出异常,对于run方法为普通方法可以重复调用

为什么调用start()方法时会执行run()方法,而不直接执行run()方法?

首先我们使用多线程的原因就是提高速度,创建多个线程来执行任务.

  • 调用start方法才会启动一个新的线程,并在新线程中执行run方法的代码.因为start方法会做一些准备工作,包括线程分配资源,初始化线程上下文,然后等到获取到CPU时间片, 调用操作系统相关API创建新的线程
  • 直接调用run方法,会在当前线程中执行run方法代码,并不会启动一个新的线程去执行任务

为什么start方法不能重复调用?而run方法却可以?

当我们重复调用start方法的时候就会出现异常...

可以看源码进行分析 :

0->表示NEW状态.

调用start方法的时候,首先会先判断线程状态(threadStatus)是不是等于0(是不是新建状态),如果不是新建状态了,那就会抛出异常.原因是当调用第一次start方法的时候,线程状态从NEW变为了Runnable状态,由于线程状态是不可逆的(也就是说不能在从Runnable->NEW了),所以JVM在判断当前线程状态已经不是NEW状态了,他就会抛出异常.

即 : 重复调用 start()会抛出异常

可以直接调用 Thread 类的 run 方法吗?

可以通过Thread类对象直接调用run方法,run方法就是一个普通方法,用于封装线程执行的任务, 如果直接调用run方法会直接在当前线程(一般为主线程)执行,不会新启动一个线程去执行任务.

sleep,yield,wait

sleep与yield的区别

  • sleep()方法使线程进入阻塞状态,暂停执行指定的时间,在时间结束后重新进入就绪状态. yield()方法使线程放弃当前获得的CPU时间片,重新回到就绪状态
  • sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会,yield()方法只会给相同或更高优先级的线程以运行的机会;
  • 线程执行sleep()方法后进入阻塞(blocked)状态,而执行yield()方法后进入就绪(ready)状态;
  • sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;

sleep与wait的区别

共同点:两者都可以暂停线程的执行.

两者的区别 :

  • wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
  • sleep 不需要强制与 synchronized 配合使用, 但是 wait 必须和 synchronized 一起使用.调用wait方法前必须先获取wait对象的锁 
  • 调用sleep方法后不会释放对象锁, 但调用wait方法之后就会释放对象锁,允许其他线程获取锁
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法.或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。sleep()方法执行完成后,线程会自动苏醒 并且他们都可以被打断唤醒
  • sleep()方法调用后, 线程进入TIMED_WAITING状态, wait()方法调用后未被唤醒前是WATING状态。 wait(n),sleep(n)两者共同点就是 : 线程的状态都为TIMEDWAITING
  • sleep是Thread类的静态方法,而wait是所有对象都有的方法,是Object的方法

为什么 wait() 方法不定义在 Thread 中?

因为调用wait方法前必须获取对象锁,让获取对象锁的线程等待,然后就会自动释放当前线程占有的对象锁.

每一个对象都拥有对象锁,要想让获取到对象锁的线程进入WAITING状态,操作的是对应的对象,而不是线程.

为什么 sleep() 方法定义在 Thread 中?

sleep方法是让当前线程暂停执行,没有涉及到对象类,就不需要获取对象锁.

线程的优先级

线程优先级会提示( hint )调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它  
如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

Thread.sleep(0)的作用是什么?

Thread的sleep会让线程暂时释放CPU资源,然后进入TIMED_WAITING状态,等待指定时间之后在尝试获取CPU时间片.

Thread.sleep(0),就是让当前线程释放一下CPU时间片,然后重新开始争抢.

可以用来做线程调度,比如某个线程长时间占用CPU资源,这个时候sleep(0)让线程主动释放CPU时间片,让其他线程可以进行一次公平的竞争

join方法

  • join()方法的作用是什么?

join方法就是用于一个线程等待另外一个线程执行完毕,然后自己可以利用那个线程执行完的结果做一些其他事情.

  • join()方法与sleep()方法有什么区别?
  1. join方法用于一个线程等待另外一个线程执行完毕, sleep方法用于让当前线程暂停一段时间
  2. join方法必须在线程对象上调用,而 sleep() 是Thread类的静态方法,通过Thread类调用
  3. join方法进入WAITING状态, sleep方法进入TIMED_WAITING状态
  • 多个线程之间如何正确使用 join() 方法来协同工作?/如何使用 join() 方法来保证某些线程在其它线程执行完毕后再执行?

多个线程之间使用join方法,主要是确保线程之前的顺序执行.

我们需要创建多个协作的线程,哪个线程需要等待另外一个线程结束,就在该线程内部调用另外一个线程的join方法

比如 main线程等待t1,t2结束,那么就在主线程调用t1.join,t2.join

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        System.out.println("t1");
    }, "t1");
    Thread t2 = new Thread(() -> {
        System.out.println("t2");
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println("main");
}

如果t1要想等待t2结束,那么就在t1线程中调用t2.join方法

public static void main(String[] args) throws InterruptedException {
    Thread t2 = new Thread(() -> {
        System.out.println("t2");
    }, "t2");
    Thread t1 = new Thread(() -> {
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("t1");
    }, "t1");
    t1.start();
    t2.start();
    t1.join();
    System.out.println("main");
}
  • 如果一个线程已经完成了它的任务,再次调用它的 join() 方法会发生什么?

如果一个线程已经完成了它的任务,再次调用它的 join() 方法将不会发生任何变化,因为已经没有需要等待的内容了。

interrupt方法

如何停止一个正在运行的线程? /如何优雅的关闭线程

有三种方式可以用来停止线程

  • 使用标志位,来终止其他线程的run方法
@Slf4j(topic = "c.TestInterrupt")
public class InterruptTest1 {
    //1.使用自带标记位
    private static volatile boolean flg = false;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (!flg) {
                log.debug("test...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t1").start();
        Thread.sleep(2000);
        flg = true;
    }
}
  • 使用stop方法强行终止
  • @Slf4j(topic = "c.InterruptTest2")
    public class InterruptTest2 {
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                while(true) {
                    log.debug("test...");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }, "t1");
            t1.start();
            Thread.sleep(2000);
            t1.stop();
        }
    }
  • 使用interrupt方法打断该线程
  1. 打断正在阻塞的线程.(比如调用了sleep,wait,join方法,都会进入阻塞状态这些方法都要抛出InterrupedException)
public static void main(String[] args) throws InterruptedException {
    //1.打断阻塞的线程
    Thread t1 = new Thread(() -> {
        log.debug("sleep...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "t1");
    t1.start();
    Thread.sleep(600);
    t1.interrupt();
    log.debug(t1.isInterrupted()+"");
}

被阻塞的线程打断标记会被设置为false,将来打断标记可以用来判断这个线程是继续运行还是就此终止.

  1. 打断正常运行的线程.
@Slf4j(topic = "c.InterruptTest4")
public class InterruptTest4 {
    public static void main(String[] args) throws InterruptedException {
        //1.打断正在运行的线程
        Thread t1 = new Thread(() -> {
            while(true) {
                log.debug("sleep");
                boolean interrupted = Thread.currentThread().isInterrupted();
                if(interrupted) {
                    //由被打断线程自己去决定,是继续运行还是停止
                   break;
                }
            }
        }, "t1");
        t1.start();
        t1.interrupt();
    }
}

打断正在运行的线程也是使用t.interrupt()方法来进行打断, 会把打断标记设置为true,被打断线程可以根据打断标记来判断,接下来是继续运行还是就此停止.

错误停止线程的方式

  • 使用stop方法强行终止
@Slf4j(topic = "c.InterruptTest2")
public class InterruptTest2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(true) {
                log.debug("test...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "t1");
        t1.start();
        Thread.sleep(2000);
        t1.stop();
    }
}

不建议使用stop方法来终止线程,这样不是优雅的终止,而是强制终止.

interrupt, isInterrupted, interrupted的区别是什么

  • void interrupt() :interrupt() 方法只是给当前线程设置了一个打断标记,把打断标记置为true,至于后续是打断还是继续运行,由当前线程自己决定(料理完后事在退出).对于打断阻塞的线程(调用了wait方法,sleep方法,join方法),会认为是异常终止,会把打断标记置为false
  • boolean isInterrupted() 方法: 检测当前线程是否被中断。
  • boolean interrupted() 方法:检测当前线程是否被中断, 与isInterrupted 不同的是,该方法如果发现当前线程被中断, 则会清除中断标志.

notify和notifyAll有什么区别

  • notify和notifyAll都是操作对象的,所以都被定义在Object中,都是Object的方法
  • notify 是把在等待队列(WaitSet)中等待的线程,随机唤醒一个
  • notifyAll : 是把在等待队列(WaitSet)中等待的线程全部唤醒

还有一点要注意的是,从等待队列中被唤醒并不能立即获取到CPU的使用权,只是表示他们可以竞争锁了,竞争到锁之后才有机会被CPU调度

notifyAll虽然唤醒的是waitset中的所有线程,但是最终只能有一个线程获取/竞争到锁.

线程的状态

线程有5种状态和6种状态两种说法

线程的5种状态是指操作系统层面上的 , 线程的6种状态是指JavaAPI层面上的

线程的5种状态

线程的5种状态是指操作系统层面上的

  • 初始状态 : 仅仅是语言层面创建了线程对象,还没有与操作系统相关联.比如new 了一个Thread对象还没有调用start方法
  • 可运行状态 : 也可以理解为就绪状态,该线程已经被创建了,与操作系统相关联,可以由CPU调度,但是线程调度器还没有给该线程分配CPU时间片
  • 运行状态 : 给该线程分配了CPU时间片,线程处于正在运行的状态
    • 这里运行状态和可运行状态是能进行上下文切换的,当CPU时间片用完了,线程就会从运行状态变为可运行状态.,导致线程上下文切换
  • 阻塞状态 : 该线程调用了一些阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,就会导致上下文切换,从运行状态变为阻塞状态.线程调度器不会分配给[阻塞状态线程]CPU时间片的,但是会分配给可运行状态的线程. 当读写文件操作完毕,会由操作系统来唤醒阻塞状态的线程, 就会从阻塞状态变为可运行状态
  • 终止状态 : 当当前线程的所有代码执行完成之后,表示线程已经执行完毕,生命周期已经结束了,不会再转换为其他状态.

线程的六种状态

线程的6种状态是JavaAPI层面上的.

根据Thread.State 枚举 ,分为6种状态.

  • NEW : 仅仅是语言层面创建了线程对象,还没有与操作系统相关联.比如new 了一个Thread对象还没有调用start方法
  • RUNNABLE : JavaAPI层面上状态涵盖了操作系统层面的[可运行状态],[运行状态] 和[阻塞状态]
  • 阻塞状态在Java里面细分为 BLOKED,WAITING,TIMED_WAITING
  • BLOKED : 当t线程调用synchronized(obj) 获取对象锁时竞争失败,会进入BLOCKED状态
  • WAITING : 当调用线程的join方法,LockSupport.park()方法或者用synchronized(obj)获取了对象锁后,调用obj.wait()方法时.
  • TIMED_WAITING : 当用synchronized获取对象锁后,调用wait(long n)方法时候,当调用join(long n) ,调用sleep(long n),或者调用LockSupport的有时限的方法
  • TERMINATED : 当当前线程的所有代码执行完成之后,表示线程已经执行完毕,生命周期已经结束了,不会再转换为其他状态.

线程状态之间的变化

  • NEW --> RUNNABLE

当调用了 t.start()方法的时候,由NEW --> RUNNABLE

  • RUNNABLE <--> WAITING

调用wait方法,join方法,LockSupport.park() 线程就从 RUNNABLE --> WAITING

  1. 对于join方法和park方法 当其他线程调用interrupt方法打断该线程就会 从 WAITING-->RUNNABLE状态
  2. 对于wait方法
  • 调用了 obj.notify() ,obj.notifyAll() ,t.interrupt() 时候
    • 竞争锁成功,t线程从 WAITING --> RUNNABLE
    • 竞争锁失败,t线程从 WAITING --> BLOCKED

比如两个线程竞争同一把锁,其中主线程使用notifyAll把它们全唤醒,那必然有一个线程竞争锁成功从WAITING --> RUNNABLE,必然有一个线程竞争锁失败从WAITING --> BLOCKED

  • RUNNABLE <--> TIMED_WAITING
  1. 当前线程调用 t.join(long n) 方法, Thread.sleep(long n) , LockSupport.parkNanos(long nanos) obj.wait(long n)方法 ,就会进入 TIMED_WAITING状态

     2. 对于t.join(long n) 方法, Thread.sleep(long n) , LockSupport.parkNanos(long nanos) 当到达指定时间就会从 TIMED_WAITING --> RUNNABLE

        对于 obj.wait(long n)方法

  • 当t线程等待时间超过了n毫秒,或调用obj.notify(),obj.notifyAll(),t.interrupt() 时
    • 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
  • RUNNABLE <--> BLOCKED
  • 当t线程用synchronized(obj)获取了对象锁时如果竞争失败就会从 RUNNABLE-->BLOCKED
  • 持有obj锁线程的同步代码块执行完毕,会唤醒该对向上的所有BLOKED的线程,如果其中t线程竞争成功,从BLOKED-->RUNNABLE ,其他失败的线程仍然是 BLOCKED.

RUNNABLE --> TERMINATED

当当前线程的所有代码执行完成之后,表示线程已经执行完毕,生命周期已经结束了,不会再转换为其他状态.

线程死亡的几种情况

  1. 线程正常执行完毕:当线程执行完自己的任务后,线程会自然结束并死亡。

  2. 异常终止:如果线程中抛出未捕获的异常并且没有对该异常进行处理,线程将会异常终止并死亡。这种情况下,可以通过设置异常处理程序来捕获并处理线程中的异常,避免线程死亡。

  3. 调用stop()方法:stop()方法是一个被废弃的方法,它会强制终止一个线程的执行。这种方式容易引发线程安全问题和资源泄漏,不推荐使用。

线程阻塞的几种情况

  • 阻塞状态在Java里面细分为 BLOKED,WAITING,TIMED_WAITING
  • BLOKED : 当t线程调用synchronized(obj) 获取对象锁时竞争失败,会进入BLOCKED状态
  • WAITING : 当调用线程的join方法,LockSupport.park()方法或者用synchronized(obj)获取了对象锁后,调用obj.wait()方法时.
  • TIMED_WAITING : 当用synchronized获取对象锁后,调用wait(long n)方法时候,当调用join(long n) ,调用sleep(long n),或者调用LockSupport的有时限的方法
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值