03.JAVAEE之线程1

1. 认识线程(Thread)

1.1 概念

一个线程就是一个 "执行流". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 "同时" 执行着多份代码

1.2 引入线程的原因

引入多个进程,初心?

实现并发编程 => 多核 CPU 的时代=>可以同时执行多个任务

进程可以满足并发编程,但是效率很低,这时候我们引入了线程。

【进程太重量,效率不高.

创建一个进程,消耗时间比较多

【消耗在申请资源上的.进程是资源分配的基本单位.

分配内存操作,就是一个大活~

操作系统内部有一定的数据结构,把空闲的内存分块管理好,当我们去进行申请内存的时候,系统就会从这样的数据结构中找到一个大小合适的空闲内存,返回给对应的进程

这里虽然通过此处的数据结构,可以一定程度提高效率,整体来说,管理的空间比较多,相比之下还是一个耗时操作.】

销毁一个进程,消耗时间也比较多

调度一个进程消耗时间也比较多】

如果需要频繁的创建/销毁进程,这个时候,开销就不能忽视

为了解决上述问题,就引入了"线程"(Thread)
线程也叫做"轻量级进程"

创建线程,比创建进程,更快;

销毁线程,比销毁进程, 更快,

调度线程,比调度进程, 更快

【线程不能独立存在,而是要依附于进程,(进程包含线程)

进程可以包含一个线程,也可以包含多个线程(一个进程,最开始的时候,至少要有一个线程这个线程负责完成执行代码的工作.也可以根据需要, 创建出更多的线程,从而使当前实现"并发编程"的效果)】

【结构】一个进程,使用 PCB 表示,一个进程可能使用一个 PCB 表示,也可能使用多个 PCB 表示每个 PCB 对应到一个线程 上(状态,优先级,上下文,记账信息....每个线程都有这些信息,辅助调度)(除此之外, 前面谈到的 pid,是相同的.内存指针, 文件描述符表,也是共用同一份的)

上述结构,决定了,线程的特点:

1.每个线程都可以独立的去 CPU 上调度执行,

2.同一个进程的多个线程之间,共用同一份内存空间,和文件资源...

创建线程的时候,不需要重新申请资源了直接复用之前已经分配给进程的资源.省去了资源分配的开销,于是创建效率就更高了
进程是资源分配的基本单位

线程是调度执行的基本单位
一个系统中,可以有很多进程
每个进程, 都有自己的资源.
一个进程中,可以有很多线程

每个线程都能独立调度,共享内存/硬盘资源

总结:

1.首先, "并发编程" 成为 "刚需".

  • 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.
  • 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.

2.其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.

  • 创建线程比创建进程更快.
  • 销毁线程比销毁进程更快.
  • 调度线程比调度进程更快.

3.最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池"(ThreadPool) 和 "协程"
(Coroutine)

1.3 进程和线程的区别)(总结)

  • 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。进程同时可以有多个线程。
  • 进程和线程, 都是用来实现 并发编程 场景的,但是线程比进程更轻量,更高效
  • 同一个进程的线程之间,共用同一份的资源(内存+硬盘),省去了申请资源的开销
  • 进程和进程之间,是具有独立性的,一个进程挂了,不会影响到别人线程和线程之间(前提是同一个进程内),是可能会相互影响的.(线程安全问题 + 线程出现异常)
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。

1.4 Java 的线程 和 操作系统线程 的关系

线程是操作系统中的概念.

操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).

Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.

2.多线程程序

感受多线程程序和普通程序的区别:

每个线程都是一个独立的执行流

多个线程之间是 "并发" 执行的.

  • java.lang下的类不用import
  • run是线程的入口方法

每个线程都是一个独立的执行流~~

每个线程都可以执行一系列的逻辑(代码)

一个线程跑起来,从哪个代码开始执行?

就是从它的入口方法入口

一个 Java 程序的 入囗是 main 方法一样~~

【运行 Java 程序,就是跑起来一个 java 进程,这个进程里面至少会有一个线程,主线程,主线程的入口方法就是main 方法. 】

//Thread.sleep

//sleep是Thread的静态方法

  • 把上述代码改成 while (true)可以看到, 这两个 while 循环在"同时执行看到的结果,是两边的日志都在交替打印
  • 每个线程,都是一个独立执行的逻辑.(独立的执行流)
  • 兵分两路,并发执行(并行+并发)->达到并发编程的效果->更好的利用多核
  • 这俩线程都是休眠 1000ms, 当时间到了之后,这俩线程谁先执行, 谁后执行,不一定!!
    这个过程可以视为是"随机"的.
    操作系统,对于多个线程的调度顺序,是不确定的,"随机"的.(此处的随机,不是数学上"概率均等"这种随机,取决于 操作系统 对于线程调度的模块 (调度器)具体实现)
    把 t.start 改成 t.run此时,代码中不会创建出新的线程,只有一个 主线程.这个主线程里面只能依次执行循环执行完一个循环再执行另一个

t.start兵分两路,一部分往下(main自动创建的线程,与别的线程相比没有什么特殊的,一个Java进程至少有一个main线程),一部分创建新的线程
多线程程序运行的时候,可以使用 IDEA 或者jconsole(jdk带有的程序) 来观察到该进程里的多线程情况

找到jdk所在路径

  • 在jconsole,可以看到一个 Java 进程即使是最简单的,里面也包含了很多的线程
  • 只有Thread-0是Thread t = new MyThread();自己手动创建的,其他的线程都是 JVM 自动创建的
  • 一个 Java 进程启动之后,NM 会在后面,默默的帮咱们做很多的事情(比如,垃圾回收,资源统计, 远程方法调用...)
  • 线程的详细信息
  • 未来写一些多线程程序的时候,就可以借助这个功能能看到该线程实时的运行情况比如,写的程序"卡死了

2.1 Thread 类的其他用法

创建线程,其他的写法

1.继承 Thread, 重写 run

2.实现 Runnable, 重写 run (Runnable是接口不是类)

  1. 实现 Runnable 接口

class MyRunnable implements Runnable {

@Override

public void run() {

System.out.println("这里是线程运行的代码");

}

}

  1. 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.

Thread t = new Thread(new MyRunnable());

  1. 调用 start 方法

t.start(); // 线程开始运行

java 复制代码
class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Test01 {
    public static void main(String[] args) {
        Runnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
        t.start();

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Runnable 表示的是一个"可以运行的任务这个任务是交给线程负责执行,还是交给其他的实体来执行...... Runnable 本身并不关心~~

使用 Runnable 的写法, 和 直接继承 Thread 之间的区别, 主要就是解耦合
创建一个线程,需要进行两个关键操作:
1.明确线程要执行的任务

任务本身, 不一定和线程概念强相关的这个任务只是单纯的执行一段代码,这个任务是使用单个线程执行,还是多个线程执行,还是通过其他的方式(信号处理函数/协程/线程池.....)都没啥区别~~(可以把任务本身给提取出来~此时就可以随时把代码改成使用其他方式来执行这个任务)
2. 调用系统 api 创建出线程

3.匿名内部类创建 Thread 子类对象

匿名内部类创建 Thread 子类对象

// 使用匿名类创建 Thread 子类对象

Thread t1 = new Thread() {

@Override

public void run() {

System.out.println("使用匿名类创建 Thread 子类对象");

}

};

4.匿名内部类创建 Runnable 子类对象

// 使用匿名类创建 Runnable 子类对象

Thread t2 = new Thread(new Runnable() {

@Override

public void run() {

System.out.println("使用匿名类创建 Runnable 子类对象");

}

});

5.lambda 表达式创建 Runnable 子类对象(lambda自身就是run方法,所以不用重写run)

// 使用 lambda 表达式创建 Runnable 子类对象

Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));

Thread t4 = new Thread(() -> {

System.out.println("使用匿名类创建 Thread 子类对象");

});
lambda 表达式,本质上是一个匿名函数,主要用来实现回调函数"的效果

Java 中不允许函数独立存在的(其他语言叫函数 function, Java 这里叫方法 method)
lambda 本质函数式接口.(本质上还是没有脱离类)

java 复制代码
public class Demo5 {
    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.start();

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2. Thread 类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。

2.1 Thread 的常见构造方法

|--------------------------------------|--------------------------|
| 方法 | 说明 |
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
| Thread(String name) | 创建线程对象,并命名 |
| Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |

创建线程的时候,可以去指定 namename 不影响线程的执行,

只是给线程起个名字后续在调试的时候,比较方便区分
Thread t1 = new Thread();

Thread t2 = new Thread(new MyRunnable());

Thread t3 = new Thread("这是我的名字");

Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

2.2 Thread 的几个常见属性

|--------|-----------------|
| 属性 | 获取方法 |
| ID | getId() |
| 名称 | getName() |
| 状态 | getState() |
| 优先级 | getPriority() |
| 是否后台线程 | isDaemon() |
| 是否存活 | isAlive() |
| 是否被中断 | isInterrupted() |

  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。(后台线程是否结束,不影响整个进程的接收,前台线程会影响进程结束)(默认情况下一个线程是前台线程)(t.setDaemon(true)即就设置为后台线程了)
  • 是否存活,即简单的理解,为 run 方法是否运行结束了(Thread 对象的生命周期,要比系统内核中的线程更长一些~~Thread 对象还在,内核中的线程已经销毁了这样的情况~~)
  • 线程的中断问题

2.3 启动一个线程(start)

start 方法,start 方法内部,是会调用到系统 api,来在系统内核中创建出线程,

run 方法,就只是单纯的描述了该线程要执行啥内容.(会在 start 创建好线程之后自动被调用的)

二者之间的差别就是是否创建了新的线程

2.4 中断一个线程(interrupt)

常见的有以下两种方式:

1. 通过共享的标记来进行沟通

java 复制代码
// 线程的打断
public class Demo8 {
    private static boolean isQuit = false;

    public static void main(String[] args) throws InterruptedException {
        // boolean isQuit = false;

        Thread t = new Thread(() -> {
            while (!isQuit) {
                // 此处的打印可以替换成任意的逻辑来表示线程的实际工作内容
                System.out.println("线程工作中");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程工作完毕!");
        });

        t.start();
        Thread.sleep(5000);

        isQuit = true;
        System.out.println("设置 isQuit 为 true");
    }
}

当前咱们这个代码,是使用了一个 成员变量 isQuit,来作为标志位如果把 isQuit 改成 main 方法中的局部变量,是否可以呢??

不可以

lambda 表达式,有一个语法规则,变量捕获,lambda 表达式里面的代码,是可以自动捕获到上层作用域中涉及到的局部变量的~~

所谓的变量捕获, 其实就是让 lambda 表达式把当前作用域中的变量在 lambda 内部复制了一份!!(此时,外面是否销毁, 就无所谓了)

变量捕获

Java 中,变量捕获语法, 还有一个前提限制,就是必须只能,捕获一个 final 或者是实际上是 final 的变量

变量虽然没有使用 final,但是却没有修改内容就是"事实上的 final'。

上述方案,不够优雅.

1.需要手动创建变量.

2.当线程内部在 sleep 的时候, 主线程修改变量,新线程内部不能及时响应.

2. 调用 interrupt() 方法来通知

java 复制代码
// 线程终止
public class Demo9 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            // Thread 类内部, 有一个现成的标志位, 可以用来判定当前的循环是否要结束.
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("线程工作中");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // 1. 假装没听见, 循环继续正常执行.
                    e.printStackTrace();
                    // 2. 加上一个 break, 表示让线程立即结束.
                    // break;
                    // 3. 做一些其他工作, 完成之后再结束.
                    // 其他工作的代码放到这里.
                    break;
                }
            }
        });
        t.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("让 t 线程终止. ");
        t.interrupt();
    }
}

使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记

|-------------------------------------|-------------------------------------|
| 方法 | 说明 |
| public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
| public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
| public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |

t.interrupt();//这个操作,就是把上述 Thread 对象内部的标志位设置为 true 了.

即使线程内部的逻辑出现阻塞(sleep)也是可以使用这个方法唤醒的~~

正常来说,sleep 会休眠到时间到, 才能唤醒,此处给出的 interrupt 就可以使 sleep 内部触发一个异常,从而提前被唤醒,(这是手动设置标志位无法实现的)

//但存在一个问题

异常确实是出现了.sleep 确实是唤醒了但是上述 t仍然在继续工作!! 并没有真的结束!!
interrupt 唤醒线程之后,此时sleep 方法抛出异常,同时会自动清除刚才设置的 标志位这样就使"设置标志位"这样的效果就好像没有生效一样~~

【这么设定的原因】
Java 是期望, 当线程收到"要中断"这样的信号的时候,他能够自由决定,接下来怎么处理~~

就可以让咱们有更多的"可操作性空间

可操作性空间的前提,是通过"异常"的方式唤醒的

如果没有 sleep,则没有上述操作空间,(此时没有异常,目的就非常明确,如果有 异常,就需要在出现异常之后,再确认一下)

2.5 线程等待join()

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。

让一个线程,等待另一个线程执行结束,再继续执行.本质上就是控制线程结束的顺序

join 实现线程等待效果,

主线程中,调用 t.join()

1.此时就是主线程等待 t线程先结束

java 复制代码
package thread;

public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("t 线程工作中!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();

        // 让主线程来等待 t 线程执行结束.
        // 一旦调用 join, 主线程就会触发阻塞. 此时 t 线程就可以趁机完成后续的工作.
        // 一直阻塞到 t 执行完毕了, join 才会解除阻塞, 才能继续执行
        System.out.println("join 等待开始");
        t.join();
        System.out.println("join 等待结束");
    }
}

t.join 工作过程:
1)如果t线程正在运行中,此时调用 join 的线程就会阻塞,一直阻塞到t线程执行结束为止

2)如果t线程已经执行结束了,此时调用 join 线程, 就直接返回了.不会涉及到阻塞~~

3)可以设置超时时间(一般来说不建议死等)

|------------------------------------------|----------------------|
| 方法 | 说明 |
| public void join() | 等待线程结束 |
| public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
| public void join(long millis, int nanos) | 同理,但可以更高精度 |

2.6 获取当前线程引用

java 复制代码
public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
   }
}

2.7 休眠当前线程

也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的

|------------------------------------------------------------------------------|-----------------|
| 方法 | 说明 |
| public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis毫秒 |
| public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |

相关推荐
gopher95114 分钟前
go语言 数组和切片
开发语言·golang
ymchuangke5 分钟前
线性规划------ + 案例 + Python源码求解(见文中)
开发语言·python
gopher95116 分钟前
go语言Map详解
开发语言·golang
Python私教9 分钟前
Go语言现代web开发15 Mutex 互斥锁
开发语言·前端·golang
zyt.com20 分钟前
线程池总结
jvm
小电玩27 分钟前
JAVA SE8
java·开发语言
努力的布布1 小时前
Spring源码-从源码层面讲解声明式事务的运行流程
java·spring
程序员大金1 小时前
基于SpringBoot的旅游管理系统
java·vue.js·spring boot·后端·mysql·spring·旅游
小丁爱养花1 小时前
记忆化搜索专题——算法简介&力扣实战应用
java·开发语言·算法·leetcode·深度优先
大汉堡~1 小时前
代理模式-动态代理
java·代理模式