[Java] 多线程初识

多线程

    • 前言
    • 内核相关
    • 线程
    • 一、线程概念及简单操作
      • 多线程编程
        • 第一个多线程程序
        • 真正体现多线程程序
          • 一些问题
          • jconsole工具
      • 创建线程的方式
        • 方法一:继承 Thread 类
        • 方法二:实现 `Runnable` 接口
        • 方法三:继承 Thread,但是使用 匿名内部类
        • 方法四:实现 `Runnable` 接口,但是使用 匿名内部类
        • 方法五:\[常用/推荐] 使用 lambda 表达式
    • 二、Thread 类及常见方法
      • 1)Thread 常见构造方法
      • 2)Thread 的几个常见属性
      • 3)提前终止一个线程
        • 方法一:添加一个 标志符
        • 方法二:方法一的优雅版本
      • 线程须知
        • 关于线程,异常处理方案
      • 4)等待线程 - `join()`
        • 使用 `join()`
        • `join()` 的种类
      • 5)获取当前线程引用
        • 通过 this 拿到线程实例
        • `Thread.currentThread()` 方法,获取当前线程引用
      • 6)休眠当前线程
    • 三、线程的状态

前言

上一章节中,我们知道,进程是可以很好的解决并发编程这样的问题,但在一些特定的情况下,进程的表现是不尽人意的。比如:有些场景下,需要频繁的创建和销毁进程,此时使用多进程编程,系统开销就会很大

  • 举一个例子:

    在早期Web开发中,就是基于多进程的编程模式来开发的。服务器同一时刻会收到很多请求,针对每个请求,它都会创建出一个进程,给这个请求提供一定的服务,然后返回对应的响应,一旦这个请求处理完了,此时这个进程就要销毁掉

    • 但是如果请求很多,意味着服务器就需要不停地创建新的进程和销毁旧的进程,这样的操作,开销就会很大

开销比较大最关键的原因

  • 资源的申请和释放
    • 进程是资源分配的基本单位,一个进程在刚刚启动时,首先就是对 内存资源 的一个分配。进程需要将所依赖的代码和数据,从磁盘加载到内存中
  • 从系统分配一个内存,并非一件易事
    • 一般来说,申请内存时,需要指定一个大小。系统内部就把各种大小的空闲内存,通过一定的数据结构给组织起来,实际申请的时候,就需要去这样的空间中进行查找,找到个大小合适的空闲内存,然后分配
  • 问题:如果一个很大的进程,找不到一个合适的空闲内存,此时系统会怎样?
  • 答:系统会报错,启动不了这个进程
    • 这在Windows上不明显,在Linux上是很明显的

结论:进程在进行频繁创建和销毁时,开销是很大的,所以引进了线程这一概念


内核相关

  • 前面提到在操作系统内核中创建出线程,但什么是内核呢?

  • 内核,是操作系统中,最核心的功能模块,其作用是:管理硬件,给软件提供稳定的运行环境

    举一个例子,张三去银行办理业务,他只能通过工作人员去代办他的需求,他不能进入办事窗口自己办理业务(如果自己能进去,那警察叔叔可就要上门请你喝茶了)

  • 上述例子中,工作人员所待的办事窗口,在操作系统中称为 ”内核空间(内核态)“,而张三所待的大堂,在操作系统中称为 ”用户空间(用户态)“

  • 我们平时运行的普通程序如:QQ音乐,微信等程序都是运行在 用户态 的,这些程序在有的场景下,需要针对一些系统提供的 软硬件资源 进行操作

    如:QQ音乐在播放音乐时,需要对扬声器进行操作;打微信视频电话时,需要对摄像头进行操作

  • 这些操作,都 不是应用程序直接操作 的,此时需要调用系统所提供的 api,进一步在内核中完成这样的操作

问题:为什么要划分出内核态和用户态呢?
答:最主要的目的还是为了 “稳定”。如果给应用程序的权限太大,使它可以直接操作你的硬件,如果运行过程中出现了bug,就很可能导致将硬件给干烧了,直接用不了了
所以,操作系统封装了一些 api,这些 api 属于 “合法” 操作,应用程序只能调用这些 ”合法“ 的 api,这样就不至于对系统/硬件设备造成危害


线程

线程,也可以称为 “轻量级进程” ,其优点在于:
保持了独立调度执行,但同时省去了分配资源” 和 “释放资源” 带来的额外开销

一、线程概念及简单操作

  1. 一个线程就是一个 “执行流”,每个线程之间都可以按照顺序执行自己的代码。多个线程之间 “同时” 执行着多份代码

    设想如下场景:
    一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得缴社保
    如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责⼀个事情,分别申请⼀个号码进⾏排队,⾃此就有了三个执⾏流共同完成任务,但本质上他们都是为了办理同一个业务。此时,我们就把这种情况称为多线程,将⼀个⼤任务分解成不同⼩任务,交给不同执⾏流就分别排队执⾏。其中李四、王五都是张三叫来的,所以张三⼀般被称为主线程(Main Thread)

  2. 为啥要有线程

  • 首先,“并发编程” 成为 “刚需”
    • 单核 CPU 的发展遇到了瓶颈,要想提高算力,就需要多核 CPU。而并发编程能更充分利用多核 CPU 资源
    • 有些任务场景需要 “等待IO“,为了让等待 IO 的时间能够去做一些其他的工作,也需要用到并发编程
  • 其次,虽然多进程也能实现 并发编程,但是线程比进程更轻量
    • 创建线程比创建进程更快
    • 销毁线程比销毁进程更快
    • 调度线程比调度进程更快
  1. 进程和线程的区别

    • 进程是包含线程的,每个进程至少有一个线程存在,即主线程
    • 进程和进程之间不共享内存空间。同一个进程的线程之间共享同一个内存空间
    • 进程是系统分配资源的最小单位,线程是系统调度的最小单位
      • 有线程之前,进程需要扮演两个角色(资源分配的基本单位和调度执行的基本单位)
      • 有线程之后,进程专注于资源分配线程专注于调度执行
    • 一个进程挂了一般不会影响到其他进程。但是一个线程挂了可能会把同进程的其他线程一起带走(整个进程崩溃)

      比如,张三、李四、王五,其中李四挂了,李四所负责的任务就要落到其他两人身上,其他两人不堪重负,也纷纷挂掉了,这个业务就没法执行了!

  2. 线程多了也不是一件好事

    • 当线程数量太多,线程之间就会相互竞争CPU的资源(毕竟CPU核心数是有限的),那么这样非但不会提高效率,反而还会增加调度的开销
    • 线程之间可能会起冲突,就可能会导致代码中出现一些逻辑上的错误(线程安全问题,重点,难点)
  3. Java 的线程 和 操作系统线程 的关系

    • 线程是操作系统中的概念。操作系统内核实现了线程这样的机制,并且对用户层提供了一些API供用户使用(例如 Linux 的 pthread 库)
    • Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进一步的抽象和封装
      • 由于操作系统都是C/C++实现的,提供的api也是C/C++风格的,JDK就对这些api进行了封装,封装成了Java风格的api(一个Java程序也是可以调用C/C++函数的,这涉及到了Java中的JNI技术)

多线程编程

第一个多线程程序
  • 题外话:
    • 一般是把 “跑起来” 的程序,称为 “进程
    • 没有运行起来的程序(.exe),称为 “可执行文件

代码:

class MyThread extends Thread {  
    @Override  
    public void run() {  
        // run 方法就是该线程的入口方法  
        System.out.println("hello world");  
    }  
}  
  
public class ThreadDemo1 {  
    public static void main(String[] args) {  
        // 根据刚才的类,创建出实例(线程实例,才是真正的线程)  
        Thread thread = new MyThread();  
        // 调用 Thread 的 start 方法,才会真正调用系统 api,在系统内核中创建出 线程  
        thread.start();
    }  
}
  • 注意事项:
  1. run() 方法,不需要程序员手动调用,它会在合适的时机(线程创建好之后),由 jvm 自动调用执行。这种风格的函数,称为 “回调函数(callback)”
    • 回调函数:比如Java中的 PriorityQueue 中的 compareTo()compare 就属于 “回调函数”,它会在我们插入元素时,自动调用这些方法
    • 个人理解回调函数:就是在A方法中包含了另一个B方法,在调用 A 方法时,A方法会自动调用B方法,而不需要我们手动去调用B方法
  2. run() 方法,类似于 main 方法,是一个 Java 进程的入口方法
  3. 线程 是要 创建出来才有的
  4. 调用 start() 方法时,就会自动调用 run() 方法

真正体现多线程程序

代码:

class MyThread2 extends Thread {  
    @Override  
    public void run() {  
        while (true) {  
            System.out.println("hello world");  
        }  
    }  
}  

public class ThreadDemo2 {  
    public static void main(String[] args) {  
        Thread thread = new MyThread2();  
        thread.start();  
        while (true) {  
            System.out.println("hello main");  
        }  
    }  
}
  • 两个 while(true) 死循环
  • 在之前的学习中,我们知道,当陷入一个死循环时,该循环下面的代码是无法继续执行,但是当我们真正运行时,我们会发现:
  • 两句打印语句是 “交替进行” 的,速度非常快!这就是 “并发编程“ 的体现

如图:主线程 main 和其 子线程thread0,此时就是互不干扰,各搞各的
[Java] 多线程初识_第1张图片

注意! 当有多个线程时,这些线程执行的先后顺序是不确定的! 因为在操作系统内核中,有一个 “调度器” 模块,这个模块的实现方式,会呈现出一种 “随机调度” 的效果

  • 什么叫随机调度
    1. 一个线程,什么时候被调度到CPU上执行时机是不确定的
    2. 一个线程,什么时候从CPU上 下来,给别人让位,这个时机也是不确定
  • 这种 ”抢占式执行“,也给后面多线程的线程安全问题埋下伏笔

一些问题

上述代码中,俩循环都是死循环,而且没有加任何条件,一旦程序运行起来,这俩循环就会执行得飞快,导致CPU占用率比较高,所以我们可以在循环中加上 sleep() 方法来降低循环速度
C语言中用的是 Windows api 中提供的 Sleep 函数
在Java中,我们使用的是封装后的版本,是Thread类提供的静态方法
![[Pasted image 20231217230746.png]]

注意:1s = 1000ms,这个方法本身也没有非常精确,精度误差就在毫秒级,所以用第一个方法就好
但是,直接用的话,会报异常,如图:这个异常,意味着 sleep(1000) 的过程中,可能会被提前唤醒
![[Pasted image 20231217230948.png]]

代码:

class MyThread2 extends Thread {  
    @Override  
    public void run() {  
        while (true) {  
            System.out.println("hello world");  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  
  
public class ThreadDemo2 {  
    public static void main(String[] args) throws InterruptedException {  
        Thread thread = new MyThread2();  
        thread.start();  
  
        while (true) {  
            System.out.println("hello main");  
            Thread.sleep(1000);  
  
        }  
    }  
}

运行结果:可以看到,两个语句是同时出现,也就是说可以认为两个循环是同时执行,即两个线程是同时执行的,而且打印也是 “随机” 的
但是!即便是 “随机” 的,也是主线程 main 先打印出语句
这是因为:主线程main在调用 start() 方法后,就立即往下执行打印语句了;于此同时,内核就要通过 刚才线程的 api 构建出线程,然后执行 run(),由于创建线程本身也有开销,所以在第一轮打印时子线程要稍慢一些
[Java] 多线程初识_第2张图片

  • 细节问题:为什么 run() 方法中只有一个 try() catch() 改错方案,不能有 throws ,但是下面的 main() 方法中就可以有两种改错方案?
  • 答:因为如果 run() 方法加上 throws ,就修改了方法签名,此时就无法构成 “重写”,父类的 run() 没有 throws 这个异常,子类重写的时候,就也不能 throws 异常

jconsole工具

在 JDK 中,有一个 jconsole 工具,可以更直观地看到多个线程,地址在:Java-jdk-bin-jconsole.exe
如果打开后,发现进程一栏是空,那么需要以管理员方式打开
[Java] 多线程初识_第3张图片

当然,使用IDEA调试器,也可以看线程情况(打断点)


创建线程的方式

方法一:继承 Thread 类

该方法就是上面的写法


方法二:实现 Runnable 接口

代码:

class MyThread3 implements Runnable {  
    @Override  
    public void run() {  
        while (true) {  
            System.out.println("hello runnable");  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  
  
public class ThreadDemo3 {  
    public static void main(String[] args) {  
        // 两种写法  
  
        // 1.  
        /*Runnable runnable = new MyThread3();       
        Thread thread = new Thread(runnable);*/  
        // 2.        
        Thread thread = new Thread(new MyThread3());  
  
        thread.start();  
  
        while (true) {  
            System.out.println("hello main");  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
  
        }  
    }  
}
  • 说明:
  • 如图,这种写法,其实就是把 线程要执行的任务 进行 解耦合,让他们的关联性没那么强
    [Java] 多线程初识_第4张图片

方法三:继承 Thread,但是使用 匿名内部类

内部类:在一个类里面定义的类,没有名字,不能重复使用,用一次就扔了

[Java] 多线程初识_第5张图片


方法四:实现 Runnable 接口,但是使用 匿名内部类

代码:

public class ThreadDemo4 {  
    public static void main(String[] args) {  
        Thread thread = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                while (true) {  
                    System.out.println("runnable");  
                    try {  
                        Thread.sleep(1000);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
            }  
        });  
        
        thread.start();  
        
        while (true) {  
            System.out.println("main");  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}

方法五:[常用/推荐] 使用 lambda 表达式
  • lambda表达式,其实就是 匿名函数
  • 在Java中,方法必须要依赖类,所以在使用方法时,必须要给它套一层类
  • 所以就引入了 lambda 表达式,在不破坏原有的规则下,对调用方法进行简化

代码:

public class ThreadDemo5 {  
    public static void main(String[] args) throws InterruptedException {  
        Thread thread = new Thread(() -> {  
            while (true) {  
                System.out.println("lambda");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        });  
  
        thread.start();  
  
        while (true) {  
            System.out.println("main");  
            Thread.sleep(1000);  
        }  
    }  
}

细节问题:
[Java] 多线程初识_第6张图片

为什么编译器正好知道我们要重写的是 run() 方法?
因为Thread 中的构造方法有好几个版本,在编译器编译的时候,就会一个个往里面匹配,其中匹配到 Runnable 这个版本时,发现这里有个 run() 方法,无参数,正好能和现在的 lambda 对上


二、Thread 类及常见方法

1)Thread 常见构造方法

[Java] 多线程初识_第7张图片

如:此时在 jconsole 工具中就能看到我们所命名的线程

Thread thread = new Thread(new Runnable() {  
    @Override  
    public void run() {  
        System.out.println("runnable");  
    }  
},"这是一个线程");

注意:第五个构造方法,Java中的线程组和系统内核中的线程组不是一个东西,了解即可


2)Thread 的几个常见属性

[Java] 多线程初识_第8张图片

  1. getId()
    • jvm 自动分配的身份标识,保证唯一性
  2. getState()
    • 进程之前介绍有 就绪状态和阻塞状态
    • 线程也有状态,Java中对线程的状态又进行了进一步的区分
  3. getPriority()
    • 线程的优先级
    • 在Java中设置优先级,效果不是很明显(对 内核 调度器的调度过程产生一些影响,但由于系统的随机调度,影响比较小)
  4. isDaemon() :是否是 ”后台线程“(和手机上的 前台app,后台app概念是不同的)
    • 前台线程:会阻止进程结束
    • 后台线程:不会阻止进程结束
  • 如图:当前代码执行时,thread 子线程一直在执行,但是 main 主线程已经结束了,但是编译器依旧在打印子线程语句

  • 所以说,该代码创建的线程,默认是前台线程, 会阻止进程结束,只要前台线程没执行完,进程就不会结束,即使主线程已经执行完毕
    [Java] 多线程初识_第9张图片

  • 所以我们就可以设置,线程为 ”前台线程“ 或 ”后台线程“

  • 如图:setDaemon ,设置为 true 时,就是将该线程设置为 “后台线程”,此时主线程默认是前台线程,主线程结束后该子后台线程也只能结束

  • 这个设置满足这样的需求:主线程结束后,该进程就需要直接关闭,在这种场景下,就可以将其他子线程设置为 “后台线程”
    ![[Pasted image 20231218161755.png]]

  1. isAlive() :表示,内核中的线程是否还存在

3)提前终止一个线程

  • 如何让线程 提前终止
方法一:添加一个 标志符

在之前的代码中,我们循环条件里填的就是 true ,直接设定成死循环,现在我们添加一个 boolean 类型的变量,来操控这个循环的终止
代码:通过添加一个 布尔类型的变量 isQuit,当我们想要结束该线程时,就在主线程main中将这个 isQuit 修改

public static boolean isQuit = false;  
  
public static void main(String[] args) throws InterruptedException {  
  
    Thread thread = new Thread(() -> {  
        while (!isQuit) {  
            System.out.println("thread");  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
        System.out.println("线程执行完毕");  
    },"测试线程1");  
  
    thread.start();  
    // 运行三秒  
    Thread.sleep(3000);  
    System.out.println("提前终止线程");  
    
    isQuit = true;  
}

问题1isQuit 变量,为什么放在 main 方法当中就不可行?写作 成员变量就可以了呢?
:涉及到变量捕获;lambda表达式本质上是 “函数式接口” => 匿名内部类,内部类访问外部类成员这个事情本身就是可以的,就不受到变量捕获的影响了

问题2:为什么 Java 对于变量捕获有 final 限制?
每个线程都有其独立的栈帧,各自栈帧的生命周期不一样。这就可能导致主线程执行完,栈帧销毁了,但子线程还在,还想用主线程里面的变量。---- Java 中的做法就非常的简单粗暴,变量捕获本质上就是 “传参 ,换句话说,就是让 lambda 表达式在自己的栈帧中创建一个新的 isQuit 并把外面的 isQuit 值拷贝过来(为了避免 里外 的 isQuit 的值不同步,Java干脆就不让修改了
相比之下:JS 里的变量捕获就很复杂,JS改变了变量的生命周期:某个局部变量被其他 “匿名函数” 捕获,此时这个变量就脱离原有的函数级别的生命周期了(这背后就涉及到一个非常复杂的 “作用域链” 问题/闭包)


方法二:方法一的优雅版本

方法一,代码不够简洁,还需要我们手动添加一个布尔类型变量
Thread 类中,就内置了这样一个变量,如图
[Java] 多线程初识_第10张图片

[Java] 多线程初识_第11张图片

代码:

public class ThreadDemo8 {  
    public static void main(String[] args) {  
        Thread thread = new Thread(() -> {  
            while (!Thread.currentThread().isInterrupted()) {  
                System.out.println("thread");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        });  
  
        thread.start();  
  
        try {  
            Thread.sleep(3000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
  
        System.out.println("我要终止这个线程");  
        thread.interrupt();  
    }  
}
  • 但是注意,运行之后,如图:进程并没有真正结束,而是抛了一个异常后,继续执行
    [Java] 多线程初识_第12张图片

  • 观察发现这里抛一个 InterruptedException 异常,说 sleep interrupted,就是 sleep 被提前唤醒了,可见是 sleep() 引起的问题

  • sleep() 被提前唤醒会做两件事:

    1. 抛出异常
    2. 清除 Thread 实例的 isInterrupted 标志位
  • 我们通过 thread.interrupt() 方法已经把标志位设为 true

  • 但是 sleep 提前唤醒,操作之后,又把 标志位改回了 false ,所以循环又继续了(注意,不止是 sleep 有这样一个操作,其他方法也会这样)

注意! 在Java中,线程的终止是一种 “软性” 操作,必须要对应的线程配合,才能把该线程给提前终止;相比之下,系统原生的 api 还提供了 “强制终止线程” 的操作,无论线程是否愿意配合,无论该线程执行到哪个代码,都能强行把这个线程给干掉
强行干掉线程,在Java中是没有提供相应 api 的,这种操作弊大于利,如果强行干掉一个线程,很可能线程执行到一半,就会出现一些残留的临时性质的“错误”数据

  • 问题:为什么 sleep 会清除标志位呢?
  • :为了给程序员更多的 “可操作空间”
    • 前一个代码,写的是 睡眠1秒 sleep(1000),但现在还没到1s呢,就要终止线程了
    • 这就相当于两个前后矛盾的操作,在计算机眼里,这一步出现了问题,所以需要我们程序员来对这样的情况进行具体的处理
    • 处理方法有:
      1. 让线程立即结束:加 break
      2. 让线程不结束,继续执行:不加 break
      3. 让线程执行一些逻辑之后,再结束:写一些其他代码,再 break

线程须知

  1. 一个线程对象只能调用一次 start() 方法,多次调用会抛异常!所以要想启动更多线程,就是需要创建更多新的线程实例
    • 本质上 start() 会调用系统的 api,来完成 创建线程 的操作

关于线程,异常处理方案

线程中我们经常需要去处理抛的异常,一般有以下处理方法

  1. 尝试自动恢复
    • 能自动恢复的,就尽量自动恢复,如:出现一个网络连接失败,就可以在 catch 中尝试重连网络
  2. 记录日志(将异常信息记录到 文件 中)
    • 有些情况并非是很严重的问题,只需要把这个问题记录下来即可,等之后程序员有空再解决
  3. 发出报警
    • 针对一些比较严重的问题(程序无法继续执行)
    • 包括但不限于:给程序员 发邮件、短信
  4. [少数/非常规用法]:依赖 catch
    • 比如文件操作中有的方法,就是要通过 catch 来结束循环之类的

4)等待线程 - join()

让一个线程等待另一个线程的结束

  • 注意:多个线程的执行顺序是不确定的!(随机调度抢占式执行

  • 虽然线程底层的调度是无序的,但可以在应用程序中,通过一些api来影响到线程执行的顺序

  • join() 就是一种方式。比如:

    • t2线程 等待 t1线程,此时,一定是 t1 先结束,t2后结束
    • 因为 join 是可能会使 t2线程 阻塞

使用 join()

在 main 线程中调用 thread.join() ,就是让 main 线程等待 thread 线程结束。哪个线程调用 join() 方法,那么调用这个方法的线程就进入阻塞等待状态

代码:

public class ThreadDemo9 {  
    public static void main(String[] args) throws InterruptedException {  
        Thread thread = new Thread(() -> {  
            for(int i = 0; i < 5; i++) {  
                System.out.println("子线程正在工作中...");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        });  
  
        thread.start();  
  
        thread.join();  
  
        System.out.println("这是主线程,等待子线程结束后,该日志才能打印");  
    }  
}

打印结果:可以看到,主线程 main 调用了 thread,所以 主线程 main 处于阻塞等待状态(下面简称为 “等待状态” )
[Java] 多线程初识_第13张图片

直接上结论:thread.join() 方法,只会使主线程(或者调用thread.join()的线程)进入等待池,并等待 thread 线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程
打个比方,join有连接、汇合的意思,主线程main调用子线程的join()方法时,好比用一条绳子将两个线程连在一块了,主线程的速度是比子线程快的,这条绳子就相当于把主线程捆住了,需要等子线程跑完,然后主线程才能继续跑


join() 的种类

如图:可以看到有 不带参数 和 带参数的 join() 方法
![[Pasted image 20231219135408.png]]

  1. 死等(不带参数的)

    • 这种方式容易出问题,如果代码中因为死等,导致程序卡住,无法处理后续的逻辑,这就是一个非常严重的bug
  2. 带有超时时间的等(等,但是有时间限制)

    • 等待一定时间,超过了这个时间,就不等了,继续往下走
  3. 当然,interrupt() 方法,就可以把 等待池 里的线程提前唤醒

  • 如图:join也会抛一个 InterruptedException 异常,就是因为 interrupt() 操作可以将它提前唤醒
    ![[Pasted image 20231219135823.png]]

5)获取当前线程引用

通过 this 拿到线程实例

代码:

class MyThread5 extends Thread {  
    @Override  
    public void run() {  
        System.out.println(this.getId() + " " + this.getName());  
    }  
}  
  
public class ThreadDemo10 {  
    public static void main(String[] args) throws InterruptedException {  
        MyThread5 thread1 = new MyThread5();  
        MyThread5 thread2 = new MyThread5();  
  
        thread1.start();  
        thread2.start();  
  
        Thread.sleep(1000);  
        System.out.println(thread1.getId() + " " + thread1.getName());  
        System.out.println(thread2.getId() + " " + thread2.getName());  
    }  
}

运行结果:可见,如果是继承 Thread 的类,就可以直接使用 this 拿到线程实例
![[Pasted image 20231219140628.png]]

但如果是 Runnable 或 lambda 的方式,this 就拿不到了,因为此时 this 已经不再指向 Thread 实例了,就只能用以下方法


Thread.currentThread() 方法,获取当前线程引用

代码:

public static void main(String[] args) {  
    Thread thread1 = new Thread(() -> {  
        System.out.println(Thread.currentThread().getName());  
    });  
  
    Thread thread2 = new Thread(() -> {  
        System.out.println(Thread.currentThread().getName());  
    });  
  
    thread1.start();  
    thread2.start();  
}

运行结果:如果在 lambda 表达式中改为 this 去引用,势必会报错
![[Pasted image 20231219141112.png]]


6)休眠当前线程

已经很熟悉的方法了,就是我们的 sleep() 方法
![[Pasted image 20231219141908.png]]


三、线程的状态

Java中,线程有以下六种状态:

  1. NEW:Thread 实例创建好了,但是还没有调用 start() 方法,系统中**还没有创建出线程**
  2. TERMINATED:系统内部的**线程已经执行完毕**,但是Thread 对象仍然存在
  3. RUNNABLE:就绪状态。表示这个线程正在CPU上执行或者已经准备就绪,随时都可以去CPU上执行
  4. TIMED_WAITING:指定时间的阻塞,在到达一定时间后,线程会被自动唤醒
    • 如使用 sleep() 或 带有超时时间的 join() 方法,就会进入这个状态
  5. WAITING:也就是 死等,必须满足一定条件后,才会唤醒该线程
  6. BLOCKED锁竞争引起的阻塞

画个图来表示这六个状态的流程:
[Java] 多线程初识_第14张图片

  • 当程序卡住时,就可以使用 jconsole 之类的工具,观察线程的状态,找出问题所在

你可能感兴趣的:(java,开发语言)