当前位置:网站首页>多线程(基础)

多线程(基础)

2022-08-09 09:34:00 Living_Amethyst

多线程

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!");
           
    }
}

在这个代码中,虽然先启动的线程,后打印的 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. 创建一个类,实现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();
            }
        }
    }
}

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();
    }
}

在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. 使用lambda表达式,来定义任务(推荐)
public class Demo5 {
    
    Thread t = new Thread( ()->{
    
        while(true){
    
            System.out.println("hello thread! ");
            try {
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }
        }
    });
}

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");
    }
}

一些解释

  • 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过于闲置

原网站

版权声明
本文为[Living_Amethyst]所创,转载请带上原文链接,感谢
https://blog.csdn.net/living_amethyst/article/details/125936084