多线程Thread(初阶一:认识线程)

绝地灬酷狼 2024-02-18 12:13 170阅读 0赞

目录

一、引用线程的原因

二、线程的概念

三、进程和线程的区别

四、操作系统的内核

五、多线程编程

1、线程创建的几种方式

(1)、继承Thread类,重写run

(2)、实现Runneble接口,重写run

要怎么查看进程的情况呢?

(3)继承Thread类,重写run,但使用匿名内部类

(4)、实现Runneble接口,重写run,使用内部类

六、star()和run()的区别

都看到这了,点个赞再走吧,谢谢谢谢谢!!!


一、引用线程的原因

多任务操作系统,希望系统能同时运行多个任务。所以会涉及到进程,需要对进程进行管理、调度等。

而单任务操作系统,就完全不涉及到进程,也不需要管理、调度了。

而进程,就是解决并发编程的这样问题,事实上,进程也能解决大部分并发编程的问题(Java不提倡多进程编程)。但有些情况就很乏力了,如下:

网站 / Web开发,是一种服务器程序,我们知道,一个网站服务器在同一时刻,会受到很多请求,针对这些请求,会创建进程,一个请求创建一个进程,创建完一个进程,又要销毁这个进程,这意味着这个网站服务器要频繁的创建和释放资源,这个操作开销是比较大的,

原因:

我们知道,进程是资源(CPU,硬盘,内存,网络带宽)分配的基本单位,而一个进程,刚启动的时候,首当其冲的就是申请内存资源,因为进程需要把依赖的代码 / 数据,从磁盘加载到内存中。

而从系统分配一个内存,并非是件容易的事,一般来说,申请内存的时候需要指定一个大小,系统内部就要把各自大小的空闲内存,通过一定的数据结构,给组织起来,实际申请的时候,就需要去这样的空间中进行查找,找到一个大小合适的空闲内存,进行分配。

结论:进程在创建和销毁的时候,开销比较大,主要体现在申请和释放资源上。

这时,我们就引入线程,来解决开销比较大的问题。


二、线程的概念

线程也可以理解成“轻量级进程”,基于进程做的一些改进和调整,使其变得开销(资源的申请和释放)不那么大。

因为进程的独立性,一个进程在内存中申请一块资源时,那个块资源只能让那个进程使用,其他的进程不能使用。而一个线程在内存中申请了一块资源,其他不同的线程也可以使用这块资源,这样就避免了多次的资源申请和释放。PCB可以表示进程,也可以表示线程。

进程在内存中的使用范围,如图

27eabed133174ec18ccddd0430a73303.png

04ab5ea6831948b2ad61fa160ef10229.png

PCB有个属性,是内存指针

多线程的PCB,也有内存指针,但可以指的是同一块内存空间,以及进程有的pid、状态、

上下文、优先级等,线程也有。


三、进程和线程的区别

1、进程包含线程,进程可以理解成多个线程的组合,这些线程称为线程组。

关系图如下:

0a83b253ed494b19ab62afc3426688df.png

2、进程扮演的角色是申请内存空间,而线程扮演的角色是调度数据 / 执行代码。

3、1个进程至少有1个线程,每个进程有自己的资源空间,而进程里的线程共用这块资源空间。

4、进程和进程之间不会相互影响,但是进程中的某个线程出问题了,可能会影响到这个进程中的其他线程,导致这个进程也出问题。

5、同一个进程中的线程之间,可能会相互干扰,引起线程安全问题。

6、线程不是越多越好,应该要合适,如果线程太多了,调度开销可能非常明显。


四、操作系统的内核

内核,是操作系统最核心的功能模块,操作系统 = 内核 + 配套的应用程序,操作系统大概可以分为两个模块,用户空间(用户态),内核空间(内核态),操作系统理解为银行,银行大概分为两个区域,如图:

25848a60411349fd9d6d169b70813b70.png

办事窗口里面的区域是做一些比较重要的事,一般人都在大堂,只能在大堂里活动,办事窗口里面能做一些核心,重要的事,外面的人也不能闯进来,这时,我们可以理解为内核空间是办事窗口,用户空间是大堂,如图:

173aa4e8486d491ba0b319bfdfdef3dd.png

那如果用户空间里的程序,想要使用内核资源,需要针对系统内的软硬件资源进行操作,要怎么办呢,这时,Windows操作系统就站出来了,提供系统的api给这些程序使用,调用内核资源,进一步在内核中完成这样的操作。

为什么要划分出内核空间(内核态)和用户空间(用户态)呢?

我们想想,如果一个应用程序可以直接调用硬件资源,如果出现一个bug了呢?不直接把硬件给干烧了,所以,划分出这两模块,主要目的还是为稳定,系统会封装一些api,这些api都是安全的操作,供应用程序使用,就不至于对系统 / 硬件设备造成危害了。


五、多线程编程

1、线程创建的几种方式

(1)、继承Thread类,重写run

一个简单的线程创建,代码如下:

  1. class MyThread extends Thread {
  2. @Override
  3. public void run() {
  4. //run方法是该线程的入口方法
  5. System.out.println("Hello World");
  6. }
  7. }
  8. public class SystemCode {
  9. public static void main(String[] args) {
  10. //2、根据刚才的类,创建一个实例
  11. Thread t = new MyThread();
  12. //3、调用Thread的start方法,才会真正调用系统的api,在系统内核中创建线程
  13. t.start();
  14. }
  15. }

执行结果:

ce2c447bbe9b491bb6fed371b6dc4a13.png

(2)、实现Runneble接口,重写run

代码如下:

  1. class MyThread implements Runnable {
  2. @Override
  3. public void run() {
  4. while (true) {
  5. System.out.println("Hello thread");
  6. try {
  7. Thread.sleep(1000);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. }
  13. }
  14. public class ThreadDemo1 {
  15. public static void main(String[] args) {
  16. Thread t = new Thread(new MyThread());
  17. t.start();
  18. while (true) {
  19. System.out.println("Hello main");
  20. try {
  21. Thread.sleep(1000);
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. }
  26. }
  27. }

执行效果:

bb76cfe4597b449fb0d2426d6adaf41f.png

要怎么查看进程的情况呢?

1、使用jdk自带的jconsole.exe文件。

4850012168704fe9b9bcc08f1c7d2fc1.png

dc61a6c5f42546deb43f876bb6619812.png

bfbea66cab464b27bb9b7cbe6f16f023.png

看到这里有这么多的线程,我们发现,其实一个java的进程,包含的线程也挺多的。

这里除了main线程和我们自己创建的线程,其余的线程,都是 jvm 自带的线程,用来完成垃圾回收(gc自动帮你释放内存),监控统计各种指标(如果我们的代码出问题了,这些指标就可以给我们提供一些参考和线索),把统计指标通过网络的方式,传输给其他程序。

5a1d40b54787471091703424e5edd1b6.png

堆栈跟踪信息:是很有意义的,描述了线程的调用栈,线程里当前执行道路哪个方法的第几行代码了,这个方法是怎么一层一层调用过去的。

和我们所写的代码行序是对应的

34a8cce5687148f798fd1d5cceee9004.png

2、使用idea查看线程

debug调试我们的代码,然后打断点到这:

bda994810fa047db952187b4ee7d9221.png

43668a4f976a45e79bfd04c5c320a98b.png

这有个下拉窗,我们就可以看到现有的线程了:

8846aea5972448c59121c0e112e5787d.png

(3)继承Thread类,重写run,但使用匿名内部类

代码:

  1. public class ThreadDemo2 {
  2. public static void main(String[] args) {
  3. Thread t = new Thread() {
  4. @Override
  5. public void run() {
  6. while (true) {
  7. System.out.println("hello thread");
  8. try {
  9. Thread.sleep(1000);
  10. } catch (InterruptedException e) {
  11. throw new RuntimeException(e);
  12. }
  13. }
  14. }
  15. };
  16. t.start();
  17. while (true) {
  18. System.out.println("hello main");
  19. try {
  20. Thread.sleep(1000);
  21. } catch (InterruptedException e) {
  22. throw new RuntimeException(e);
  23. }
  24. }
  25. }
  26. }

执行效果和上面的一样

说明:内部类的最大用途,也就是匿名内部类,没有名字,意味着不能重复使用,用一次就不要了。new Thread()后面 { }里的意思是要定义一个类,这个类继承自Thread,此处的 { } 中可以定义子类的属性和方法,此处的 { } 最主要目的就是重写run方法。

这个代码还创造出了子类的示例

a55c1975249a448f917f019208ecaabb.png

这里的 t 指向的实例,并非单纯的Thread,而是Thread的子类,因为我们不知道这个子类叫啥,因为是匿名的。

(4)、实现Runneble接口,重写run,使用内部类

代码:

  1. public class ThreadDemo3 {
  2. public static void main(String[] args) {
  3. Thread t = new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. while (true) {
  7. System.out.println("hello thread");
  8. try {
  9. Thread.sleep(1000);
  10. } catch (InterruptedException e) {
  11. throw new RuntimeException(e);
  12. }
  13. }
  14. }
  15. });
  16. t.start();
  17. while (true) {
  18. System.out.println("hello main");
  19. try {
  20. Thread.sleep(1000);
  21. } catch (InterruptedException e) {
  22. throw new RuntimeException(e);
  23. }
  24. }
  25. }
  26. }

注意:匿名内部类是在()里面的。

(5)使用lambada(推荐)

代码:

  1. public class ThreadDemo4 {
  2. public static void main(String[] args) {
  3. Thread t = new Thread(() -> {
  4. while (true) {
  5. System.out.println("hello thread");
  6. try {
  7. Thread.sleep(1000);
  8. } catch (InterruptedException e) {
  9. throw new RuntimeException(e);
  10. }
  11. }
  12. });
  13. t.start();
  14. while (true) {
  15. System.out.println("hello main");
  16. try {
  17. Thread.sleep(1000).;
  18. } catch (InterruptedException e) {
  19. throw new RuntimeException(e);
  20. }
  21. }
  22. }
  23. }

注意:lambda表达式来代替功能接口,所以要代替的是实现接口的功能,重写的是run方法,所以要写在Thread()的括号里面。


六、star()和run()的区别

看到这,是不是觉得对这两个区别优点疑惑?但事实上,这两个概念是毫不相干的两个东西,创建出来的线程实例,只有调用 star,才会真正调用系统的api,在系统内核创建出线程;而 run 是线程执行的入口。这里,就有老铁疑惑了,我用 run 也可以调用这个重写的方法啊,和用 start 一样,哎,这就不对了,举个以下例子:

两个代码分别单独执行star 和 run

代码:

run:

  1. class MyThread extends Thread {
  2. @Override
  3. public void run() {
  4. while (true) {
  5. System.out.println("这是我的线程,正在工作");
  6. try {
  7. Thread.sleep(1000);
  8. } catch (InterruptedException e) {
  9. throw new RuntimeException(e);
  10. }
  11. }
  12. }
  13. }
  14. public class ThreadDemo4 {
  15. public static void main(String[] args) {
  16. Thread t = new MyThread();
  17. t.run();
  18. //t.start();
  19. }
  20. }

start:

  1. class MyThread extends Thread {
  2. @Override
  3. public void run() {
  4. while (true) {
  5. System.out.println("这是我的线程,正在工作");
  6. try {
  7. Thread.sleep(1000);
  8. } catch (InterruptedException e) {
  9. throw new RuntimeException(e);
  10. }
  11. }
  12. }
  13. }
  14. public class ThreadDemo4 {
  15. public static void main(String[] args) {
  16. Thread t = new MyThread();
  17. //t.run();
  18. t.start();
  19. }
  20. }

执行效果:

c0c9f29e9bc444a6a2cb2fcb87ab04f9.png

e86aa322cce34211975955d24b5e20a7.png

这两代码的执行效果是一样的,但我们现在改一下,如下:

run:

  1. class MyThread extends Thread {
  2. @Override
  3. public void run() {
  4. while (true) {
  5. System.out.println("这是我的线程,正在工作");
  6. try {
  7. Thread.sleep(1000);
  8. } catch (InterruptedException e) {
  9. throw new RuntimeException(e);
  10. }
  11. }
  12. }
  13. }
  14. public class ThreadDemo4 {
  15. public static void main(String[] args) throws InterruptedException {
  16. Thread t = new MyThread();
  17. t.run();
  18. //t.start();
  19. while (true) {
  20. System.out.println("这是主线程,正在工作");
  21. Thread.sleep(1000);
  22. }
  23. }
  24. }

start:

  1. class MyThread extends Thread {
  2. @Override
  3. public void run() {
  4. while (true) {
  5. System.out.println("这是我的线程,正在工作");
  6. try {
  7. Thread.sleep(1000);
  8. } catch (InterruptedException e) {
  9. throw new RuntimeException(e);
  10. }
  11. }
  12. }
  13. }
  14. public class ThreadDemo4 {
  15. public static void main(String[] args) throws InterruptedException {
  16. Thread t = new MyThread();
  17. //t.run();
  18. t.start();
  19. while (true) {
  20. System.out.println("这是主线程,正在工作");
  21. Thread.sleep(1000);
  22. }
  23. }
  24. }

分别执行效果:

run:

a7a2c0d831824173afd41ab3b2a19ad2.png

start:

8d758bec32a0416c8a368d8cf10bbade.png

可以看到不同了吧,这是因为star之后,这里有两个线程(t 线程和主线程),他们都是各自调度各自的线程,就有两个循环在执行了,但是如果使用 run 的话就只有一个线程了,只在一个循环里执行,所以之后在当前方法里面循环,不会执行下面的循环。


都看到这了,点个赞再走吧,谢谢谢谢谢!!!

发表评论

表情:
评论列表 (有 0 条评论,170人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java之线

    目录 一.进程和线程 1.什么是进程 2.并发,并行和串行 3.线程和多线程 4.进程和线程的区别(重点) 5.多线程的优点 6.多线程的缺点 二.线程的创建

    相关 Java线-线池总结

    一、什么是线程池? 线程池是一种管理线程的机制,用于复用线程资源,减少线程创建和销毁的开销,从而提高程序性能;线程池中的线程在完成任务后不会立即销毁,而是被放回线程池,等

    相关 线Thread

    -------------------- \\java中的线程分类 User Thread 用户线程 运行在前台,执行具体任务: 比如程序的主线程、连接网络的子线程等等