线程 本身是 操作系统 提供的概念,且 操作系统提供 API 供程序员调用。不同的系统,提供的 API 是不同的(Windows 创建线程的 API 和 Linux 差别非常大...)
Java (JVM) 把这些系统 API 封装好了,不需要关注系统原生 API, 只需要了解好 Java 提供的这一套 API就行了 --> Thread 标准库
一. 线程的创建
1.通过继承Thread类
class MyThread extends Thread{
@Override
public void run(){
//这里写的代码,就是即将创建出的线程,要执行的逻辑
while(true){
System.out.println("hello thread");
//循环中,加上 休眠 操作,让循环每执行一次,都休息一会儿,避免 CPU 消耗过大
try {
Thread.sleep(1000);//sleep是Thread中的类方法,static修饰
} catch (InterruptedException e) {
//throw new RuntimeException(e);//抛出新的异常,且打印异常信息
e.printStackTrace();//只是打印异常信息
//实际开发中处理异常的方法有很多,不只上面两种
}
}
}
}
public class Main1 {
//调用main方法的线程称为主线程 一个进程至少一个线程-->主线程
public static void main(String[] args) {
//创建一个线程对象
MyThread t = new MyThread();
//创建并启动线程,执行run里面的代码
t.start();
//t.run()不会创建线程,依旧是在主线程中执行逻辑
//主线程
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
结果解析: 多个线程之间,谁先去CPU上调度执行,这个过程是“不确定的”(不是数学意义的随机),这个调度顺序,取决于 操作系统 内核里的 “调度器”。调度器里有一套规则,但是作为应用程序的开发者 是感受不到的,也没法进行干预。决定调度执行哪一个线程的过程,是一个“抢占式执行” 的过程。
我们可以通过Java jdk 中的 bin包 下的 jconsole 查看线程情况。
堆栈跟踪 是一个程序在运行时生成的诊断信息,它显示了程序执行到当前点时,调用栈上所有活动的帧(Frame)。每个帧代表一个方法调用,堆栈跟踪能够展示哪些方法被调用,以及调用它们的顺序。当异常发生时,堆栈跟踪尤其有用,因为它可以帮助开发者追踪异常的来源,即确定异常是在哪个方法中首次被抛出的。
除了手动创建的线程和主线程, 剩下的线程,都是起到了一些辅助作用:a. 垃圾回收 (合适时机,释放不适用的对象,后面专门章节会介绍) b. 统计信息/调试信息 比如 现在通过jconsole能够查看到一个Java进程的详情
类似的信息也可以再IDEA的调试器看到:
注意:
- run方法中 处理的异常 不能 以throws的方式抛出。因为run是重写的方法,父类的run方法没有throws的声明,因此重写不可以增加throws,必须 try-catch 处理。
- 上述代码中,run方法并没有手动调用,但是最终也执行了。像这种用户手动定义,但是没有手动调用,最终这个方法被系统/库/框架进行调用了 --> 就称为“回调函数” (callback)
回调函数:
在之前的学习中已经出现过:
- C语言中 函数指针 主要有两个用途:
a. 作为回调函数 b. 实现转移表--> 降低代码复杂度- Java数据结构的优先级队列(堆)必须先定义好对象的“比较规则”
Comparable中的compareTo方法 Comparator中的compare方法
自己定义了,但是没有手动调用,此时都是由 标准库 本身内部逻辑负责调用的。然而,过度使用回调函数也可能导致代码难以理解和维护,这就是所谓的“回调地狱”。js里面的回调太多了,为了解决这个问题,提供Promises、async/await等。
2.通过实现Runnable接口
//public interface Runnable{}
class MyRunnable implements Runnable{
@Override
public void run() {
//描述了线程需要完成的逻辑
System.out.println("hello thread");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
throw new RuntimeException(e);
}
}
}
public class Main2 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t = new Thread(myRunnable);
t.start();
while(true){
System.out.println("hello main");
try{
sleep(1000);
}catch (InterruptedException e){
throw new RuntimeException(e);
}
}
}
}
通过 Thread创建线程,传入实现Runnable接口的对象,Runnable就是用来描述“要执行的任务是什么”,不是通过Thread自己来描述。
有的人认为 Runnable 这种做法 更有利于“解耦合”。因为 Runnable 只是一个任务,并不是和“线程”这样的概念强相关的,后续执行这个任务的载体可以是线程,也可以是其他的东西:比如,后续会介绍 “线程池” (ThreadPool)来执行任务,再比如,可以通过“虚拟线程”(“协程”Coroutine )来执行
3.通过匿名内部类创建
public class Main3 {
public static void main(String[] args) {
Thread t1 = new Thread(){
public void run(){
while(true){
System.out.println("hello thread1");
try {
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
};//匿名内部类,是Thread的子类,重写父类的run方法
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("hello thread2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}//匿名内部类,是Runnable接口的子类,重写父类的run方法
);
t2.start();
}
}
匿名内部类 一般是“一次性”使用的类,用完就丢了,内聚性 更好一些 。
4.Lambda表达式改写匿名内部类
public class Main4 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while(true){
System.out.println("hello thread1");
try {
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
});
t.start();
}
}
lambda表达式是最常用的方法
二.线程调度常用方法
构造方法
其中 通过起名的方法 可便于调试
ThreadGroup线程组 --> 把多个线程放到一个线程组里,方便统一设置线程的一些属性(现在会很少用到线程组,线程的相关属性用的不太多,用到更多的是“线程池”)
成员方法
isDaemon():判断是否是后台线程
后台线程:如果这个线程执行过程中,不能阻止进程结束(虽然线程在执行着,但是进程要结束了,此时这个线程也会随之被带走),这样的线程就是"后台线程”,也叫“守护线程”(daemon)
前台线程:如果某个线程在执行过程中,能够阻止进程结束,此时这个线程就是"前台线程”。一般main线程和 创建的线程 默认为 前台线程,当新创建的线程 调用 setDaemon(true) 方法会 转为 后台线程
前台线程和后台线程,除了影响进程退出之外,其他的没啥别的区别了...
通俗来讲:
- 后台线程
1)前台线程宣布结束,此时进程就结束,后台线程也会随之结束
2)前台线程不宣布结束,后台线程结束了也不受影响 - 前台线程
1)进程要结束(前台线程要结束) --> 无力阻止
2)后台线程先结束了,不影响 进程的结束(其他前台线程的结束)
一个进程中,前台线程可以有多个(创建的线程默认就是前台线程),必须所有的前台线程都结束,进程才结束。若后台线程没有执行完也会“强制结束”....
public class Main5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
},"自定义线程");
//把t设置为守护线程,不能阻止主线程的结束
t.setDaemon(true);
t.start();
//主线程不写循环,执行完就结束,t线程也会随之结束
}
}
运行结果:
操作系统中 还有"前台进程”"后台进程"这个概念,Java 不做过多介绍,C++会介绍,和前台线程、后台线程没有关系。
isAlive():为 true 表示内核的线程存在;为 false 表示内核的线程不存在了。
代码中,创建的 Thread 对象的 生命周期 和 内核中实际的线程 是不一定一样的。可能会出现:Thread 对象仍然存在,但是内核中的线程不存在了这样的情况:
- 实例了Thread对象,没调用 start ,此时内核中 还没创建线程
- 线程的 run 执行完毕了,内核的线程就没了,但是 Thread 对象 仍然存在
(注:不会出现 Thread 对象不存在,线程还存在这种情况)
public class Main6 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 3; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//未调用start,线程未创建状态
System.out.println(t.isAlive());//false
t.start();
System.out.println(t.isAlive());//true
//等待4s,此时线程已经运行结束
Thread.sleep(4000);
System.out.println(t.isAlive());//false
}
}
运行结果:
注:由于线程之间的调度顺序是不确定的,如果两个线程都是sleep(3000),当时间到,谁先执行 谁后执行 不一定(不是指双方概率均等,实际上这里的两种情况的概率,可能会随着系统的不同、代码运行环境的不同而存在差异)