线程(一)——初识线程

server/2024/11/20 19:59:25/

概念:

  1、线程是什么

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

        用银行来举例子:一个人去银行办理业务,多个业务一个人跑肯定是效率不高,这时候就需要多个人来跑业务,而每一个跑业务的人就是一个“执行流”。

2、为什么需要线程

 ①并发线程已经成为刚需

        单核CPU的发展有瓶颈,想要提高算力就必须要有多核CPU,并发变成能更充分利用多核CPU资源,而且在遇到有IO的任务场景,在IO时间内能去做其他的事更能提高效率,这也需要并发编程

②虽然多进程能实现,但是线程比进程更加轻量

        线程在创建、销毁和调度上都要比进程更快。为满足开发需求,后续还有“线程池”以及“协程”

  3、线程与进程的区别

①进程是包含线程的。一个进程必定有一个线程(即主线程)

进程与进程之间不共享内存空间,同一个进程的线程之间共享同一个内存空间

进程是系统分配资源的最小单位,线程是系统调度的最小单位

  一个进程挂了一般不会影响其他进程,但是一个进程中的一个线程挂了的话是有可能影响到同一个进程中的其他线程(让整个进程也跟着崩掉)。

线程是操作系统中的概念,操作系统内核实现了线程这种机制,也提供了这样的API给用户层的用户使用。Java中的Thread类可以视为是对操作系统提供的API进行了进一步的抽象和封装


 创建线程

 通过观察Thread的源文件代码我们可以知道,大佬们在实现线程的过程中也是调用了Runnable接口。

因此,创建线程有两种方式:继承Thread类和实现Runnable接口,这两种方法都需要重写其中的run()方法。

继承Thread类:

java">class MyThread extends Thread{@Overridepublic void run() {System.out.println("这是继承Thread类重写过后的线程run方法");}
}

实现Runnable接口:

java">public class MyRunnable implements Runnable{@Overridepublic void run() {System.out.println("这是实现Runnable接口重写过后的线程run方法");}}

在创建一个线程之后用start()方法启动线程

java">//实现Runnable接口public static void main1(String[] args) {Thread t1 = new Thread(new MyRunnable());t1.start();
}
//继承Thread类public static void main (String[] args)
{MyThread thread = new MyThread();thread.start();
}

创建线程的其他变形:

java">//内部类创建Runnable的子类public static void main(String[] args){Thread thread  = new Thread(new Runnable(){@Overridepublic void run(){System.out.print("通过内部类重写Runnabler子类的run方法创建的线程");}});thread.start();}
java">Thread thread  = new Thread(){@Overridepublic void run(){System.out.print("通过内部类重写Thread子类的run方法创建的线程");}};

多线程的优势——加快运行速度

        还是拿银行的例子来解释,一个人跑事务和几个人跑事务的速度是不同的。多线程的并发执行决定了这种运行方法在一些场景下是能提高整体代码的运行效率的。

 例:concurrency()中用一个子线程计算a,主线程计算b,而serial()中则是计算a和b

java">public class ThreadSAdvantage {public static  final long count  = 10_0000_0000;public static void main(String[] args) throws InterruptedException{concurrency();serial();}private  static void concurrency ()throws InterruptedException {{long begin = System.nanoTime();Thread thread = new Thread(new Runnable() {@Overridepublic void run() {int a = 0;for (long i= 0; i < count; i++) {a--;}}});thread.start();//到此处是用一个线程计算a的值int b = 0;for (long i= 0; i < count; i++) {b--;}//统计计时long end = System.nanoTime();double time = (end-begin)*1.0 /1000/1000;System.out.printf("并发:%f 毫秒%n",time);}}private  static void serial (){long begin = System.nanoTime();int a =0;for (long i= 0; i < count; i++) {a--;}int b = 0;for (long i= 0; i < count; i++) {b--;}//统计时长long end = System.nanoTime();double time  = (end - begin)*1.0/1000/1000;System.out.printf("串行:%f 毫秒%n",time);
}}

 通过结果我们可以发现:并行的运行时间是要比串行执行快很多的


线程的常见构造

 线程的构造方法参数有:Runnable target,String name

常见属性:

线程id是线程的唯一标识,不同线程不会出现id重复的情况(例如你们班有两个人叫张三,身份证号码是辨认这两个张三身份的唯一标识)

名称应用于各种调试工具 

优先级高的线程会比较先被调度到

是否存活简单理解为run方法是否运行结束

自定义线程优先级=建议,系统如果不按优先级调度那这个优先级意义就不大(系统的意思就是:你的建议很好,但我不接受!!!)

再单独说一说是否后台线程:

        与后台线程对应的还有一个前台线程。前台线程与后台线程的差别就在于线程的结束会不会导致java进程的结束。

        后台进程就是即使还在执行也阻止不了进程结束的线程,相反,前台线程就是如果不结束执行就不会人java进程结束的线程。(前台线程:干完活了之后只要还在工作,就不下班。后台线程:就算还在干活,我依旧要下班)

tips:前台线程可以有多个,有多个前台线程的话必须最后一个前台线程结束执行才会让java进程结束。

        java程序中,main线程就是前台线程,另外,程序员创建的线程默认情况下是前台线程,通过setDaemon方法将线程设置为后台线程。

注意:设置线程为后台线程的操作必须要在启动线程之前,也就是setDaemon()要放在start()之前,不然线程依旧默认是前台线程

启动一个线程:

        实例化一个线程之后,用.调用start()方法就是启动一个线程(实例化一个线程对象并不意味着线程已经开始执行)

区分run()和start():

        run()是线程的入口,交代线程的执行内容(run相当于事物清单)

        start()是在内核中创建线程,让线程跑起来(start相当于告诉线程——按run()里面的内容去给我跑腿干活吧!)

一个线程只能调用start()一次,否则会抛异常 

中断一个线程:

       Thread类中有一个内制的标志位isinterrupted,通过调用interrupt()改变位置量的值来决定是否中断线程。

        将一个处于工作状态的线程停止工作,常见的方式有两种:1.模仿Thread类的内置标志位,自定义布尔变量来决定线程是否中断;2.直接调用interrupt()方法

       interrupt()的默认值是true,这个true的意义就是中断线程

看例子:

java">public class Threadtest{
private static class Thread1 {private static class MyRunnable implements Runnable{public volatile boolean quit = false;@Overridepublic void run() {while(!quit){System.out.println(Thread.currentThread().getName()+"我忙着干活呢!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("什么?!没钱发工资?那我卖啥命呢!");}}public static void main(String[] args)throws InterruptedException {MyRunnable target = new MyRunnable();Thread thread = new Thread(target,"李四");System.out.println(Thread.currentThread().getName()+"让李四开始干活!");thread.start();thread.sleep(10000);System.out.println(Thread.currentThread().getName()+"告诉李四别干了,老板跑路了发不出工资!");thread.sleep(1000);target.quit = true;}}
}

 在线程休眠了十秒之后,修改自定义的标志位让线程结束

 操作系统中针对多个线程的特性:随机调度,抢占执行

看例子:
 

java">public class Threadtest2 {public static void main(String[] args)throws InterruptedException {Thread thread = new Thread(()->{System.out.println(Thread.currentThread().getName()+" is working");},"Lisi");Thread thread2 = new Thread(()->{System.out.println(Thread.currentThread().getName()+" is coming!");},"Wangwu");thread.start();thread2.start();thread2.join();thread.join();}
}

通过例子我们可以验证这一点:操作系统对于多个线程的调度是随机的,而且线程之间是抢占式执行,不是说先谁调用start()方法就是先执行


阻塞一个线程:

在有多个线程执行时,调用.join()方法阻塞一个线程就是让另一个线程稍后再工作(这句话有点含糊)

例如:有a和b两个线程,在a线程中将b阻塞,就是让a线程等b线程完成工作之后再工作,也就是说——b.join()其实是b线程在执行,a线程是在等待的那个

看实例:

java">public class Threadtest3 {public static void main(String[] args) {Thread t = new Thread(()->{System.out.println("this is Thread t");for (int i = 0; i < 5; i++) {System.out.println("t is working...");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("Thread t is finished!!!");});System.out.println("The main thread is waitiing!!!");t.start();try {t.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("The main thread is after waiting!!!");}
}

这是线程t,我们在主线程中阻塞线程t

从结果来看,我们就能体会到上面那句看起来很含糊的话的意思——main线程中阻塞了t线程,那就要等t忙完才轮到main线程继续执行

小结:

        1.阻塞为的是确保目标线程先执行完毕,当线程已经执行完再进行阻塞并没有用

        2.线程之间是可以相互等待的,多个线程可以同时阻塞相互等待

        3.无参数的join()就是死等模式,当前阻塞线程没有执行完就会让其他的线程一直等待下去

休眠当前线程: 

调用sleep()就是休眠一个线程

重点:

        中断线程会唤醒sleep(),当sleep()被唤醒抛出异常,此时内置的标志位就给清空了。

        调用sleep()的线程不会参与cpu调度,会把cpu资源让出来给其他线程去干事情。这种也称之为“放权”,即放弃使用cpu的权利。因为在开发中会有很多的线程,线程之间又存在着轻重缓急,所以就有了“放权”。


线程的状态:

NewThread对象有了,但是还没有在cpu内核上创建线程——还没调用start()(安排了工作,还没执行)

Runnable:处于就绪状态,已经在cpu上工作  /  准备好在cpu上工作 

Blocked:阻塞(因为锁竞争导致的阻塞)

Waiting:阻塞(没有超过时间限制的等待)

Timed_waiting:阻塞(超过时间限制的等待)

Terminated:Thread类对象虽然还在,但是线程已经从内核销毁了(工作完了)

顺带一提:类的对象在程序结束时销毁

(参考前面博客,JVM内存分布一栏:JavaSE—数组的定义以及应用_javase数组的定义与使用-CSDN博客


http://www.ppmy.cn/server/143531.html

相关文章

【1】猫眼娱乐后端开发面试题整理

[1]. 全量索引和增量索引的区别 全量索引&#xff1a;检索系统在启动时一次性读取当前数据库中的所有数据&#xff0c;建立索引。 增量索引&#xff1a;系统运行过程中&#xff0c;监控数据库的变化&#xff0c;即增量&#xff0c;实时加载更新&#xff0c;构建索引。 [2]. …

3D Gaussian Splatting 代码层理解之Part1

2023 年初,来自法国蔚蓝海岸大学和 德国马克斯普朗克学会的作者发表了一篇题为“用于实时现场渲染的 3D 高斯泼溅”的论文3d_gaussian_splatting。该论文提出了实时神经渲染的重大进步,超越了NeRF等以往方法的实用性。高斯泼溅不仅减少了延迟,而且达到或超过了 NeRF 的渲染质…

5个Midjourney技巧,让你的图片更自然真实,没有“AI味”

您是否觉得有些AI生成的图像看起来过于完美&#xff1f;有股AI味&#xff1f;MidJourney 可以创建非常高质量的逼真图像&#xff0c;然而画面完美无瑕、栩栩如生&#xff0c;让人感觉完美得令人不安&#xff0c;几乎不真实。 比如这个&#xff0c;有点夸张&#xff1a; 大多数…

32.3 mmap的在io提速上的应用和prometheus的应用

本节重点总结 : mmap的在io提速上的应用prometheus 中mmap的应用 mmap 减少copy次数 传统IO 在开始谈零拷贝之前&#xff0c;首先要对传统的IO方式有一个概念。基于传统的IO方式&#xff0c;底层实际上通过调用read()和write()来实现。通过read()把数据从硬盘读取到内核缓…

第三十七章 如何清理docker 日志

如何清理docker 日志 目标 掌握docker 日志设置掌握docker日志的清理办法背景 在现代软件开发和部署环境中,Docker 容器技术因其轻量级、可移植性和高效资源利用的特点,已成为许多企业和开发团队的首选。Docker 容器在运行过程中会产生大量的日志信息,这些日志对于监控容器…

电子电气架构 --- 传统刷写流程怎么用在SOC上就不适用呢?

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 所有人的看法和评价都是暂时的,只有自己的经历是伴随一生的,几乎所有的担忧和畏惧,都是来源于自己的想象,只有你真的去做了,才会发现有多快乐。…

安装pytest失败ModuleNotFoundError: No module named ‘distutils‘

下载一下即可解决 pip install setuptools 下载完成后&#xff0c;再进行下载 pip install pytest

【1】流利说面经整理

[1].介绍一下CAP理论 CAP理论是指在分布式系统设计中&#xff0c;没有一种设计可以同时满足Consistency&#xff08;一致性&#xff09;、 Availability&#xff08;可用性&#xff09;、Partition tolerance&#xff08;分区容错性&#xff09;3个特性&#xff0c;最多只能同…