• 多线程(基础)


    多线程

    1.为啥要有多进程

    因为并发编程的刚需!

    CPU单个核心已经发挥到极致了,要想提升算力,就得使用多个核心

    引入并发编程,最大的目的就是为了能够充分的利用好CPU的多核资源

    使用多进程这种编程模型,是完全可以做到并发编程的,并且也能使CPU多核被充分利用

    但是,在 有些场景 下,会存在问题:

    如果需要频繁地创建/销毁进程,这个时候就会比较低效

    因为,创建/销毁进程本身就是一个比较低效的操作,具体需要完成:

    1. 创建PCB
    2. 分配系统资源(尤其是内存资源) 这个比较消耗时间,因为是在系统内核资源管理模块,进行一系列遍历操作的
    3. 把PCB加入到内核的双向链表当中

    那么为了提高这个场景下的效率,就引入了”线程“

    2.进程和线程之间的区别

    • 进程是**包含**线程的,线程是在进程内部的,每个进程至少有一个线程存在,即主线程。

    • 进程和进程之间不共享内存空间. 同一个进程的线程之间共用同一份系统资源。(每个进程有独立的虚拟地址空间,也有自己独立的文件描述符集,同一个进程的多个线程之间,则共用这一份虚拟地址空间和文件描述符集)

    • 进程是操作系统中资源分配的基本单位,线程是操作系统中调度执行的基本单位image-20220722154916233

    • 多个进程同时执行的时候,如果一个进程挂了,一般不会影响到别的进程;同一个进程里的多个线程之间,如果一个线程挂了,很可能把整个进程带走,其它同进程中的线程也就没了

    每个线程其实也都有自己的 PCB,一个进程里面就可能对应多个PCB

    同一个进程的线程之间共用同一份系统资源(意味着:新创建的线程,不必重新给它分配系统资源,只需要复用前面的即可)

    因此,比起创建进程,创建线程只需要:

    1. 创建PCB
    2. 把PCB加入到内核的链表中

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

    3.观察线程

    image-20220722160246701

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

    image-20220722160349604

    光创建了这个类,还不算创建线程,还得创建实例

    image-20220722160620354

    class MyThread extends Thread {
        @Override
        public void run() {
                System.out.println("hello thread! ");
        }
    }
    public class Demo1 {
        //创建于1个线程
        //Java中 创建线程,离不开 thread 类
        //一个创建线程的 朴素方法,学一个子类,继承Thread,重写其中的 run 方法
        public static void main(String[] args) {
            Thread t = new MyThread();   //向上转型的写法
            t.start(); //这才是真正开始创建线程(在操作系统内核中,创建出对应线程的PCB,然后让这个PCB加入到系统链表中,参与调度)
                
            System.out.println("hello main!");
               
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这个代码中,虽然先启动的线程,后打印的 hello main

    但是实际执行的时候,看到的却是先打印了 hello main 后打印了 hello thread

    这说明什么呢?

    1. 每个线程是独立的执行流! main对应的线程是一个执行流, MyThread是另一个执行流。这两个执行流之间是并发的执行关系。

    2. 此时两个线程执行的先后顺序,取决于操作系统调度器具体实现(我们可以认为是随机调度的),因此先打印哪个,是随机的,虽然咱们反复运行多次,可能打印的结果一样,但是顺序仍然是不可确定的!当前看到的先打印 hello main,大概率是受到创建线程自身的开销影响 (哪怕1000次都是先打印main,也不能保证1001次还是这个结果)

    此处不想让进程结束这么快,我们就可以这么做

    image-20220722161700918

    此时就可以查看 Java 里进行的线程

    image-20220722161807680

    双击运行

    image-20220722161838306

    如果不显示进程列表,别担心,关闭之后,右键,以管理员身份运行

    image-20220722162144011

    image-20220722162232932

    这里的调用栈非常有用!

    未来调试一个"卡死"的程序的时候,就可以看下每个线程的调用栈是啥,就可以初步的确认卡死的原因。

    刚才的死循环代码,打印的太多太快

    有的时候不希望它们打这么快(不方便来观察),可以使用sleep来让线程适当的"休息"一下

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

    4.一个经典面试题

    谈谈 Thread 的 run 和 start 的区别

    image-20220722162620138
    • 使用start,可以看到两个线程并发的执行,两组打印交替出现。

    • 使用run,可以看到只是在打印thread,没有打印main。

    • 直接调用run,并没有创建新的线程,而只是在之前的线程中,执行了run 里的内容.

    • 使用start,则是创建新的线程,新的线程里面会调用run (新线程和旧线程之间是并发执行的关系)

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

    1. 创建一个类继承Thread,重写run(这个写法,线程和任务内容是绑定在一起的)
    class MyThread 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 {
        //创建于1个线程
        //Java中 创建线程,离不开 thread 类
        //一个创建线程的 朴素方法,学一个子类,继承Thread,重写其中的 run 方法
        public static void main(String[] args) {
            Thread t = new MyThread();   //向上转型的写法
            t.start(); //这才是真正开始创建线程(在操作系统内核中,创建出对应线程的PCB,然后让这个PCB加入到系统链表中,参与调度)
    
            while(true) {
                System.out.println("hello main!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    1. 创建一个类,实现Runnable接口,重写 run
    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 Demo2 {
        public static void main(String[] args) {
            //创建线程
            //第二种方法  创建一个类,实现 Runnable接口,重写run
            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();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    image-20220722163127300

    此处创建的Runnable ,相当于是定义了一个"任务" (代码要干啥),还是需要Thread实例,把任务交给Thread,还是Thread.start来创建具体的线程

    这个写法,线程和任务是分开的(更好的解耦合)【咱们写代码的时候要追求:低耦合,高内聚】

    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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

    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();
                        }
                    }
                }
            };
    
             */
    
            //  2.
            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();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    1. 使用lambda表达式,来定义任务(推荐)
    public class Demo5 {
        Thread t = new Thread( ()->{
            while(true){
                System.out.println("hello thread! ");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    6.使用多线程带来的好处

    使用多线程,能够更充分的利用CPU多核资源

    看一个代码 (完成 20 亿次自增)

    public class Demo6 {
        //1.单个线程,串行的,完成 20 亿次自增
        //2.两个线程,并发的,完成 20 亿次自增
    
        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");
        }
    
        /**
         * 并发的
         */
        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();
    
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            long end = System.currentTimeMillis();
            System.out.println("并发执行消耗的时间:" + (end-beg) + "ms");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    一些解释

    • concurrency的代码设计到三个线程:t1,t2,main,三个线程都是并发执行的

    • 如果没有调用join,只调用start,虽然 t1、t2 是会开始执行,同时不等它们执行完,main线程就往下走了,于是就结束计时

    • 正确的计时,应该是等到 t1和t2 都执行完,才停止!

    • join是等待线程结束(等待线程把自己的run方法执行完),在主线程中调用 t1.join ,意思就是让main线程等待t1执行完

    下面我们运行程序,测一下单线程和多线程运行的时间

    image-20220722164717280

    相比之下,多线程的效率确实提高不少!

    但为什么时间不刚好是单线程的一半呢?

    1. 创建线程自身,也是有开销的!
    2. 两个线程在CPU上不一定是纯并行,也可能是并发,部分时间里是并行了,部分时间里是并发的。
    3. 线程的调度,也是有开销的。 (但是当前场景中,开销应该是非常小的)

    7.多线程的使用场景

    1.在CPU密集型场景

    代码中大部分工作,都是在使用CPU进行运算(就像刚才这个反复自增)

    使用多线程,就可以更好的利用CPU多核计算资源,从而提高效率!

    2.在 I0密集型场景

    读写硬盘,读写网卡…这些操作都算I0,这些场景里, 就需要花很大的时间等待!

    像这些I0操作,都是几乎不消耗CPU就能完成快速读写数据的操作。

    既然CPU在摸鱼,就可以给他找点活干,也可以使用多线程,避免CPU过于闲置

  • 相关阅读:
    简单shell脚本的编写
    2023最新UI酒桌喝酒游戏小程序源码 娱乐小程序源码 带流量主
    pytorch之torch.zeros_like,torch.ones_like和tensor按索引赋值
    mycat2
    调用 xlwings 创建多线程时报错 pywintypes.com_error: ( ‘应用程序调用一个已为另一线程整理的接口)解决方法
    极光推送SDK引起的内存泄露排查
    跨境电商指南:如何处理客户投诉
    离散数学 学习 之一阶逻辑的前束范式
    【HTML入门】第四课 - 换行、分割横线和html的注释
    react笔记-03react-router篇
  • 原文地址:https://blog.csdn.net/Living_Amethyst/article/details/125936084