多线程的初识和创建

这篇具有很好参考价值的文章主要介绍了多线程的初识和创建。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇
✨每日一语:知不足而奋进,望远山而前行。

💤一. 认识线程(Thread)

🍎1. 线程的引入

  • 根据我们前面学的进程,为什么要有多个进程呢?为了并发编程,他的 CPU 单个核心已经发挥到极致了,想要提升算力,就得使用多个核心。
  • 引入并发编程,最大的目的就是为了能够充分的利用好 CPU 的多核资源。使用多进程这种编程模型,是完全可以做到。并发编程,并且也能够使 CPU 多核被充分利用,但是在有些情急下,会存在问题,例如:需要频繁的创建 / 销毁进程,这个时候就会比较低效!!!
  • 例如,你写了个服务器程序,服务器要同一时刻给很多客户提供服务的,就需要用到并发编程,典型的做法,就是每个客户端给他分配一个进程,提供一对一服务。

创建 / 销毁进程,本身就是一个比较低效的操作
1. 创建 PCB
2. 分配系统资源(尤其是内存资源)分配资源,就比较消耗时间了!(这个是在系统内核资源管理模块进行一系列操作的...)
3. 把 PCB 加入到内核的双向链表中

为了提高这个场景下的效率,就引入了 " 线程 ",线程其实也叫做 " 轻量级进程 "

🍏2. 线程是什么?

一个线程其实是包含在进程中的(一个进程里面可以有很多个线程),每个线程其实也有自己的 PCB (一个进程里面可能就对应多个 PCB),同一个进程里面的多个线程之间,共用一份系统资源(意味着,新创建的线程,不必重新给他分配系统资源,只需要复用之前的即可!)

因此,创建线程只需要
1. 创建 PCB
2. 把 PCB 加入到内核的链表中

这就是线程相对于进程做出的重大改进!!!也就是线程更轻量的原因!

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

线程,是包含在进程内部的 " 逻辑执行流 "(线程可以执行一段单独的代码,多个线程之间,是并发执行的)

操作系统进行调度的时候,其实也是以 " 线程为单位 " 来进行调度的(系统内核不认进程 / 线程,只认 PCB)

如果把进程比作工厂,线程就是工厂内部的流水线

🍋3. 进程和线程之间的区别

  • 进程是包含线程的,线程是在进程内部的
  • 每个进程有独立的虚拟地址空间(进程之间的资源是独立的,进程的独立性),也有自己独立的文件描述符表;同一个进程(不同进程里面的不同线程,则没有共享的资源)的多个线程之间,则共用这一份虚拟地址空间和文件描述符表(线程之间系统资源是共享的)
  • 进程是操作系统中资源分配的基本单位,线程是操作系统中,调度执行的基本单位
  • 多个进程同时执行的时候,如果一个进程挂了,一般不会影响到别的进程;同一个进程内的多个线程之间,如果一个线程挂了,很可能把整个进程带走,其他同进程里的线程也就没了

 

💨二. 第一个多线程程序

🏐1. Java中线程的认识和基本操作

在 Java 中即使是一个最简单的 “hello”,其实在运行的时候也涉及到线程了,一个进程里至少会有一个线程

public static void main(String[] args) {
    System.out.println("hello");
}

运行上面的程序,操作系统就会创建一个 Java 进程,在这个 Java 进程里就会有一个线程(主线程)调用 main 方法;虽然在上述代码中,我们并没有手动的创建其他线程,但是 Java 进程在运行的时候,内部也会创建多个线程。

注:

谈到多进程的时候,经常会谈到 “父进程” “子进程”。进程 A 里面创建了进程 B,A 是 B 的父进程,B 是 A 的子进程。但是在多线程里面,没有 “父线程” “子线程” 这种说法,线程之间的地位是对等的!

创建一个线程,Java 中创建线程,离不开一个关键的类,Thread。一种比较朴素的创建线程的方式,是写一个子类,继承 Thread,重写其中的 run 方法。

class MyTread extends Thread {
    @Override
    public void run() {
        System.out.println("hello Thread!");
    }
}

这个 run 方法重写的目的,是为了明确咱们新创建出来的线程,要干啥活儿

光创建了这个类,还不算创建线程,还得创建实例,在主函数中写如下代码

Thread t = new MyTread();
t.start();
  • System.out.println(“hello Thread!”); --> 先把新员工要做的任务安排好
  • Thread t = new MyTread(); --> 招聘进来一个新员工,把任务交给他
  • t.start(); --> 告诉新员工,你开始干活!

t.start(); --> 这才是真正开始创建线程(在操作系统内核中,创建出对应线程的 PCB,然后让这个 PCB 加入到系统链表中,参与调度)

Thread t = new MyTread();
t.start();

System.out.println("hello main!");

多线程的初识和创建,Java EE初阶,java,jvm,开发语言
在这个代码中,虽然先启动的线程,后打印的 hello main,但是实际执行的时候,看到的却是先打印了 hello main 后打印了 hello thread。

  1. 每个线程是独立的执行流!main 对应的线程是一个执行流,MyThread 是另一个执行流,这两个执行流之间是并发(并发+并行)的执行关系!
  2. 此时两个线程执行的先后顺序,取决于操作系统调度具体实现(程序猿可以把这里的调度规则简单的视为是 “随机调度”)

因此执行的时候看到,是先打印 hello main 还是先打印 hello thread 是不能确定的。无论反复运行多少次看起来好像都是先打印 hello main,但是顺序仍然是不能确定的,当前看到的先打印 main,大概率是受到创建线程自身的开销影响的。

编写多线程代码的时候,一定要注意到!!!默认情况下,多个线程的执行顺序,是 “无序”,是 "随机调度"的。

我们还有一些手段能影响到线程执行的先后顺序的,但是调度器自身的行为修改不了,调度器仍然是随机调度,咱们最多是让某个线程先等待,等待另一个线程执行完了自己再执行

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

  • Process 进程
  • exit code 0 进程的退出码

操作系统中用 进程的退出码 来表示 “进程的运行结果”

  • 使用 0 表示进程执行完毕,结果正确
  • 使用非 0 表示进程执行完毕,结果不正确
  • 还有个情况 main 还没返回,进程就崩溃,此时返回的值很可能是一个随机值

要是我们不想要这个程序结束的这么快咋办?
就可以想办法让这个进程别结束这么快,好让咱们看下这里都有啥线程

while (true) {
    System.out.println("hello Thread!");
}
while (true){
    System.out.println("hello main!");
}

把他们都套上一个死循环之后看到打印结果是 hello main! 和 hello Thread! 交替打印

🥎2. 线程的查看

(注意不要关闭上述程序运行,查看线程需要)此时就可以查看当前 Java 进程里的线程有哪些(打开任务管理器可以观看到程序运行状态)

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

  1. 找出 JDK 安装的目录,找到 bin 文件里,在 JDK 里提供了 jconsole这样的工具,可以看到 Java 进程里的线程详情

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

  1. 点击运行(如果运行界面没有这样的选项,我们在打开的时候需要以管理员方式打开!)

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

  1. 点击进去即可(不用害怕hhh)

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

  1. 打开线程所在窗口

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

  1. 找到左下角,会有显示具体的线程

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

这个列表就是当前 Java 进程中的线程信息
其他的线程则是 JVM 中的辅助功能,有的线程负责垃圾回收,有的线程负责处理调试信息,比如咱们使用的 jconsole 能够连上这个 JVM ,也是靠对应的线程提供的服务

  1. 点击某个线程的时候,右侧会显示线程的详细情况

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

上面两张表里都描述了当前线程这里的各种方法之间调用的关联关系

这里的调用栈非常有用,未来调试一个 “卡死” 的程序的时候,就可以看下面每个线程的调用栈是啥,就可以初步的确认卡死的原因。

上面的死循环代码打印的太快太多,有时候我们不希望它这么快,不利于我们观察,于是用 sleep 来让线程适当休息一下

使用 Thread.sleep 的方式进行休眠,sleep 是Thread 的静态成员方法,sleep 的参数是一个时间,单位 ms

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

这个异常在多线程中经常能见到,interrupted 中断

通过 try-catch 抓捕异常后的运行结果

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

加上 sleep 之后,无序调度这个事情就被更加清楚的观察到了。

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

一个经典面试题:
谈谈 Thread 的 run 和 start 的区别:

  • 使用 start 可以看到两个线程并发的执行,两组打印交替出现
  • 使用 run 可以看到只是在打印 thread,没有打印 main
     

直接调用 run 并没有创建新的线程,而只是在之前的线程中,执行了 run 里的内容;使用 start,则是创建新的线程,新的线程里面会调用 run,新线程和旧线程之间是并发执行的关系

上述总的代码:

class MyTread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello Thread!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        //创建一个线程
        //Java 中创建线程,离不开一个关键的类,Thread
        //一种比较朴素的创建线程的方式,是写一个子类,继承 Thread,重写其中的 run 方法。

        Thread t = new MyTread();
        //t.start();
        t.run();

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

 

🎶三. 创建线程的几种常见写法

  1. 创建一个类继承 Thread,重写 run。(上面已经写过)

这个写法,线程和任务内容是绑定在一起的!

  1. 创建一个类,实现 Runnable 接口,重写 run。
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
  • 此处创建的 Runnable,相当于是定义了一个 "任务"(代码要干啥)
  • 需要 Thread 实例,把任务交给 Thread
  • 还是 Thread.start 来创建具体的线程
  • 这个写法,线程和任务是分离开的(更好的解耦合)(耦合的意思和表面差不多,模块 / 代码之间,关联关系越紧密,就认为耦合性越高,关联关系越不紧密,认为耦合性越低)
  • 写代码的时候追求 “高耦合” “低内聚”,让每个模块之间耦合性降低, 好处就是一个模块出了问题对另外的模块影响不大
  • 这种写法把任务内容和线程本身给分离开了,就把耦合性降低了(任务的本身和线程关系不大),假设这个任务不想通过多线程执行了,而是换成别的方式执行,这个时候代码的改动也不大。

一个问题:

为啥刚才使用 Thread,Runnable,interruptedException 都不需要 import ??啥样的类,不需要 import 就能直接使用??

  • 要么是同一个类中,要么这个类是在 java.lang 中(String)
  1. 仍然是使用继承 Thread 类,但是不再显式继承,而是使用 “匿名内部类”。
public class Demo3 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello Thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
    }
}
Thread t = new Thread() {

此处就是在创建了一个匿名内部类(没有名字),这个匿名内部类是 Thread 的子类,同时前面 new 关键字,就给这个匿名内部类创建出了一个实例

这一套操作实现了继承,方法重写,实例化

在 start 之前,线程只是准备好了,并没有真正被创建出来,执行了 start 方法,才真正在操作系统中创建了线程!!

Thread 实例是 Java 中对于线程的表示,实际上想要真正的跑起来,还是需要操作系统里面的线程,创建好了 Thread,此时系统里面还没有线程,直到调用 start 方法,操作系统才真的创建了线程(1.创建 PCB;2.把 PCB 加入到链表里)并进行运行。

  1. 使用 Runnable,是匿名内部类的方式使用
  • 方法一:
public class Demo4 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        Thread t = new Thread(runnable);
        t.start();
    }
}
  • 方法二:(推荐)
public class Demo4 {
    public static void main(String[] args) {
	Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hello Thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
    }
}

中间的匿名内部类的实例,是 Thread 的构造方法的参数!

  1. 使用 lambda 表达式,来定义任务。(推荐做法)
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();
    }
}

使用 lambda 表达式,其实是更简单的写法,也是推荐写法,形如 lambda 表达式这样的,能够简化代码编写的语法规则,称为 “语法糖”。

lambda 表达式本质上就仅仅是一个 “匿名函数”(没有名字的,只使用一次的函数)

实际上,线程还有其他的创建方式,后续介绍后面的创建方式!

  • 6.基于 Callable / FutureTask 的方式创建
  • 7.基于线程池的方式创建

至少有七种创建方式~

 

💦四. 多线程的优势

  1. 单个线程,串行的,完成 20 亿次自增。
public class Demo6 {
    private static final long COUNT = 20_0000_0000;

    private static void serial(){
        //需要把方法执行的时间给记录下来
        //记录当前的毫秒级时间戳
        long beg = System.currentTimeMillis();

        int a = 0;
        for(long i = 0; i < COUNT; i++){
            a++;
        }
        a = 0;
        for(long i = 0; i < COUNT; i++){
            a++;
        }

        long end = System.currentTimeMillis();
        System.out.println("单线程消耗的时间:" + (end - beg) + " ms");
    }

    public static void main(String[] args) {
        serial();
    }
}

运行结果:

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

使用一个线程串执行花了大约 1200 ms

  1. 两个线程,并发的,完成 20 亿次自增
public class Demo6 {
    private static void concurrency(){
        long beg = System.currentTimeMillis();

        Thread t1 = new Thread(()->{
            int a = 0;
            for(long i = 0; i < COUNT; i++){
                a++;
            }
        });

        Thread t2 = new Thread(()->{
            int a = 0;
            for(long i = 0; i < COUNT; i++){
                a++;
            }
        });

        t1.start();
        t2.start();

        long end = System.currentTimeMillis();
        System.out.println("并发执行的时间:" + (end - beg) + " ms");
    }


    public static void main(String[] args) {
        concurrency();
    }
}

这个代码涉及到三个线程 t1,t2,main(调用 concurrency 方法的线程),三个线程都是并发执行的

t1.start(); t2.start(); 表示 t1,t2 是会开始执行的,同时,不等 t1,t2 执行完毕,main 线程就往下走了,于是就结束计时。此处的计时,是为了衡量 t1,t2 的执行时间,正确的做法应该是等到 t1,t2 都执行完,才停止计时。

在上述代码 t1.start(); t2.start(); 之后加上如下代码即可

try {
    t1.join();
    t2.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}

t1.join(); t2.join(); --> join 是等待线程结束(等待线程把自己的 run 方法执行完),在主线程中调用 t1.join,意思就是让 main 线程等待 t1 执行完,这两个 join 操作谁先谁后不影响。

运行结果:

多线程的初识和创建,Java EE初阶,java,jvm,开发语言

如果没有 join 的限制,main 和 t1,t2 都是同时往下走的!多个线程并发执行的(同时在往下走),走的过程中,调度顺序不确定!调度在这个代码执行过程中不是只有一次,会有很多次,即使是上面的简单代码,也可能会出现成千上万次的调度!绝对不是说一个线程执行完了再调度给第二个线程!!!

上面的结果相较之下对比体现出两个线程并发执行确实快了不少,但是为什么两个线程并发执行不是单个线程串行执行的耗时一半呢?

  • 创建线程自身,也是有开销的
  • 两个线程在 CPU 上不一定是纯并行,也可能是并发(一部分时间并行了,一部分时间并发了)
  • 线程的调度,也是有开销的

 

❄️五. 多线程的使用场景

  1. 在 CPU 密集型

代码中大部分工作,都是在使用 CPU 进行运算(就像上面的反复++),使用多线程,就可以更好的利用 CPU 多核计算资源,从而提高效率!

  1. 在 IO 密集型场景

input output ,读写硬盘,读写网卡…这些操作都算 IO,像这些 IO 操作,几乎都是不消耗 CPU 就能完成快速读写数据的操作,既然 CPU 在摸鱼,就可以给他找点活儿干,也可以使用多线程,避免 CPU 过于闲置。文章来源地址https://www.toymoban.com/news/detail-810879.html

到了这里,关于多线程的初识和创建的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Java EE初阶十七】网络原理(二)

    2.2.2 关于可靠传输 4.滑动窗口         前面的三个机制,都是在保证 tcp 的可靠性;         TCP 的可靠传输,是会影响传输的效率的.(多出了一些等待 ack 的时间,单位时间内能传输的数据就少了);         滑动窗口,就让可靠传输对性能的影响,更少一些.TCP 只要引入了可

    2024年02月20日
    浏览(43)
  • 【Java EE初阶十六】网络原理(一)

            在网络原理中主要学习TCP/IP四层模型中的重点网络协议         应用层是和程序员接触最密切的;         应用程序:在应用层这里,很多时候都是程序员自定义应用层协议(步骤:1、根据需求,明确要传输的信息,2、约定好信息按照什么样的格式来组织)的

    2024年02月20日
    浏览(38)
  • (第六天)初识Spring框架-SSM框架的学习与应用(Spring + Spring MVC + MyBatis)-Java EE企业级应用开发学习记录

    ​ 昨天我们已经把Mybatis框架的基本知识全部学完,内容有Mybatis是一个半自动化的持久层ORM框架,深入学习编写动态SQL,Mybatis的关联映射,一对一、一对多、多对多、Mybatis的缓存机制,一二级缓存的开启和设置,缓存命中率、如何使用idea链接数据库自动生成pojo类等。我们学

    2024年02月10日
    浏览(67)
  • (第十一天)初识SpringMVC SSM框架的学习与应用(Spring + Spring MVC + MyBatis)-Java EE企业级应用开发学习记录

    今天我们要来学习一下SSM框架的最后一个框架SpringMVC 一、初认SpringMVC 基本概念: ​ Spring MVC(Model-View-Controller)是一个用于构建Java Web应用程序的开源框架,它提供了一种基于MVC架构的方式来开发Web应用 。 ​ SpringMVC是Spring Framework的一部分,它是一种基于模型-视图-控制器(

    2024年02月07日
    浏览(72)
  • 【Java EE 初阶】TCP协议的安全效率机制

    目录 1.应用层协议 2.传输层协议 3.UDP协议格式 4.TCP协议格式 5.TCP的安全效率机制 1.确认应答机制 2.超时重传机制 但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了; 3.连接管理机制 ​编辑 面试题:会不会有可能变成三次挥手? 面试题:第二个FIN丢包了如何处理?

    2024年02月09日
    浏览(45)
  • 【Java EE】初识Spring Web MVC

    Spring Web MVC 是 基于 Servlet API 构建的原始 Web 框架 ,从⼀开始就包含在Spring框架中。它的正式名称“Spring Web MVC”来⾃其源模块的名称(Spring-webmvc),但它通常被称为)Spring MVC). Servlet是⼀种实现动态⻚⾯的技术.准确来讲Servlet是⼀套Java Web 开发的规范,或者说是⼀套Java Web 开发的

    2024年04月10日
    浏览(56)
  • 【Java EE初阶二十二】https的简单理解

             当前网络上,主要都是 HTTPS 了,很少能见到 HTTP.实际上 HTTPS 也是基于 HTTP.只不过 HTTPS 在 HTTP 的基础之上, 引入了\\\"加密\\\"机制;引入 HTTPS 防止你的数据被黑客篡改 ;         HTTPS 就是一个重要的保护措施.之所以能够安全, 最关键的在于\\\"加密”;         明文:

    2024年02月22日
    浏览(53)
  • 【Java EE初阶二十一】http的简单理解(二)

            Referer 描述了当前页面是从哪个页面跳转来的,如果是直接在地址栏输入 url(或者点击收藏夹中的按钮) 都是没有 Referer。如下图所示:         HTTP 最大的问题在于\\\"明文传输”,明文传输就容易被第三方获取并篡改.         HTTPS 针对 HTTP 数据进行了加密 (h

    2024年02月22日
    浏览(39)
  • 【Java EE初阶十五】网络编程TCP/IP协议(二)

            tcp的socket api和U大片的socket api差异很大,但是和前面所讲的文件操作很密切的联系         下面主要讲解两个关键的类:         1、ServerSocket:给服务器使用的类,使用这个类来绑定端口号         2、Socket:即会给服务器使用,又会给客户端使用;         

    2024年02月20日
    浏览(51)
  • 【Java EE】SpringBoot的创建与简单使用

    如果你的IDEA是专业版,则哪个版本都可以。 如果你是社区版,则请下载 2021.1 - 2022.1.4 如果个⼈电脑安装的 idea 不在这个范围,需要卸载重新安装 卸载参考:《IDEA卸载和删除注册表》 一定要删除注册表 关于Maven相关知识与国内源配置可以参考博主写的【Java EE】关于Maven 在学

    2024年04月11日
    浏览(74)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包