线程 & 多线程

Posted 一朵花花

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程 & 多线程相关的知识,希望对你有一定的参考价值。

线程

线程概念

所谓的"线程",可以理解成轻量级"进程",也是一种实现并发编程的方式

如果把一个进程,想象成是一个工厂,线程就是工厂中的若干个流水线

为啥要有线程?

可以实现并发编程

  • 单核 CPU 的发展遇到了瓶颈,要想提高算力,就需要多核 CPU,而并发编程能更充分利用多核 CPU 资源
  • 有些任务场景需要 “等待 IO”,为了让等待 IO 的时间能够去做一些其他的工作,也需要用到并发编程
  • 线程之间共享的资源多,完成一些操作时更方便

线程比进程更轻量

  • 创建一个线程,比创建一个进程成本低
    销毁一个线程,比销毁一个进程成本也低
    (成本低的原因:新创建一个线程,不需要给这个线程分配很多的资源
    如果新建一个进程,就需要给这个进程分配较多的资源)
  • 创建线程比创建进程更快
    销毁线程比销毁进程更快
    调度线程比调度进程更快

实际进行并发编程的时候,多线程方式要比多进程方式更常见,也效率更高

线程和进程的联系和区别

联系:

  1. 线程其实是包含在进程中的 (每个进程至少有一个线程存在,即主线程)
  2. 一个进程中可能会有多个线程
  3. 每个线程都有一段自己要执行的逻辑 / 指令
    (每个线程都是一个独立的"执行流")
  4. 同一个进程中的很多线程之间,共享了一些资源

同一个进程的多个线程之间共享的资源主要是两方面:
1.内存资源(两个不同进程之间,内存不能共享)
2.打开的文件
也有一些不是共享的资源:
1.上下文 / 状态 / 记账信息 / 优先级(每个进程要独立的参与 CPU 的调度)
2.内存中有一块特殊的区域:栈 (每个线程要独立一份)

区别:

  • 进程是操作系统分配资源的最小单位
    线程是造作系统进行调度和执行的最小单位 (所谓的操作系统进行进程调度,本质上就是操作系统针对这个进程的若干个线程进行调度)

  • 当创建一个进程的时候,就会自动随之创建一个线程(主线程)
    一个进程被创建出来的同时,至少会随之创建一个线程 (主线程)

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

线程管理

本质上和管理进程一样,先用PCB描述,再使用双向链表来组织

内核只认 PCB
即:一个线程和一个 PCB 对应,而一个进程可能和多个 PCB 对应


上述逻辑是在 Linux 中的实现方式

多线程

吃鸡例子: 滑稽吃100只鸡


思考:线程数量是越多越好吗?

不是,因为线程的调度是有开销的,随着线程数量的增多,线程调度的开销也就越大
线程数量太多,非但不会提高效率,反而会降低效率

那么:一个进程中,最多能有多少个线程呢?

  1. 和 CPU 个数有关
  2. 和线程执行的任务的类型也相关
    a) CPU 密集型:程序一直在执行计算任务
    b) IO 密集型:程序没咋进行计算,主要是进行输入输出操作

假设这个主机有 8核 CPU (两种极端情况:)
若任务纯是 CPU 密集型的,此时线程的数目大概就是 8 个左右
若任务纯是 IO 密集型的,理论上,有多少个线程多可以
现实中的情况是要介于两者之间,实践中一般需要通过 测试 的方式来找到合适的线程数,让这个程序效率够高,同时系统的压力也不会过大

多线程程序缺点

第一个多线程程序

Java 中如何使用多线程?
标准库中提供了一个类:Thread 类

public class ThreadDemo1 
    static class MyThread extends Thread
        @Override
        public void run() 
            System.out.println("Hello World, 我是一个线程");
        
    

    public static void main(String[] args) 
        // 创建线程需要使用 Thread 类,来创建一个 Thread 的实例
        // 另一方面还需要给这个线程指定 要执行哪些指令/代码
        // 指定指令的方式有很多种,此处先用一种简单的,直接继承Thread类,
        // 重写 Thread 类中的 run 方法

        // 当 Thread 对象被创建出来的时候,内核中并没有随之产生一个线程(PCB)
        Thread t = new MyThread();
        // 执行这个 start 方法,才是真的创建出一个线程
        // 此时内核中才随之出现了一个 PCB,这个 PCB 就会对应让 CPU 来执行该线程的代码(上面的run方法中的逻辑)
        t.start();
    

输出结果:


为了进一步观察当前确实是俩线程,可以借助第三方工具
JDK 中内置了一个 jconsole

但此时并不能看到线程信息,因为当前进程已经结束了


必须要想办法让进程不要那么快结束,才能看到线程信息

直接安排一个死循环~

public class ThreadDemo1 
    static class MyThread extends Thread
        @Override
        public void run() 
//            super.run();
            System.out.println("Hello World, 我是一个线程");
            while (true)
                //死循环
            
        
    
    public static void main(String[] args) 
        Thread t = new MyThread();
        t.start();
        while (true)
            // 死循环
        
    

输出结果:

此时运行程序,然后双击 jconsole.exe



多线程并发执行 和 单线程 的对比:
例: 针对一个整数进行大量的循环相加:

  • 串行
private static long count = 100_0000_0000L;

public static void main(String[] args) 
    serial();      //串行
//        concurrency(); // 并发


private static void serial() 
    // 获取当前时间戳
    long begin = System.currentTimeMillis();
    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.currentTimeMillis();
    System.out.println("time: " + (end - begin) + "ms");

输出结果:

即:串行针对两个整数累加100亿次,大概消耗5s

System.currentTimeMillis( ) — 获取到毫秒级的时间戳
时间戳:
以 1970 年 1月1日 0时0分0秒为基准时刻,计算当前时刻和基准时刻之间的秒数 / 毫秒数 / 微秒数 之差

  • 并发
private static long count = 100_0000_0000L;

public static void main(String[] args) 
//        serial();      //串行
    concurrency(); // 并发


private static void concurrency() 
    long begin = System.currentTimeMillis();
    // 匿名内部类
    Thread t1 = new Thread()
        @Override
        public void run() 
            int a = 0;
            for (long i = 0; i < count; i++) 
                a++;
            
        
    ;
    Thread t2 = new Thread()
        @Override
        public void run() 
            int b = 0;
            for (long i = 0; i < count; i++) 
                b++;
            
        
    ;
    // 启动线程
    t1.start();
    t2.start();

    try 
        // 线程等待,让main 线程等待 t1和t2 执行结束,然后再继续往下执行
        t1.join();
        t2.join();
     catch (InterruptedException e) 
        e.printStackTrace();
    

    // t1,t2,和 main 线程之间都是并发执行的
    // 调用了 t1.start 和 t2.start之后,两个新线程正在忙着进行计算
    // 此时 main线程仍然会继续执行,下面的 end 也就会被随之计算了
    // 正确做法: 应该是 t1 t2计算完毕后,再来计算 end 的时间戳
    long end = System.currentTimeMillis();
    System.out.println("time: " + (end - begin) + "ms");

输出结果:

两个线程并发执行的时候,时间大概是 2.7s 左右,时间缩短了很多~

创建线程的几种代码写法

1.通过显示继承一个 Thread 类的方式来实现
2.通过匿名内部类的方式继承 Thread 类
3.显式创建一个类,实现 Runnable 接口;然后把 Runnable实例 关联到一个 Thread 实例上
4.通过匿名内部类的方式,实现Runnable
5.使用 lambda 表达式,来指定线程执行的内容

代码示例:

public class ThreadDemo3 
    // Runnable 本质上就是描述了 一段要执行的任务代码是啥
    static class MyRunnable implements Runnable
        @Override
        public void run() 
            System.out.println("我是一个新线程~");
        
    
    public static void main(String[] args) 
        // 1.显式继承 Thread
        
        // 2.通过匿名内部类的方式,继承Thread来创建线程
//        Thread t = new Thread()
//            @Override
//            public void run() 
//
//            
//        ;
//        t.start();

        // 3.显式创建一个类,实现 Runnable 接口
        // 然后把 Runnable实例 关联到一个 Thread 实例上
//        Thread t = new Thread(new MyRunnable());
//        t.start();

        // 4.通过匿名内部类的方式,实现Runnable
//        Runnable runnable = new Runnable() 
//            @Override
//            public void run() 
//                System.out.println("我是一个新线程~~");
//            
//        ;
//        Thread t2 = new Thread(runnable);
//        t2.start();

        // 5.使用 lambda 表达式,来指定线程执行的内容
        Thread t = new Thread(()->
            System.out.println("我是一个新线程~~~");
        );
        t.start();
    

无论是哪种方式,没有本质上的区别 (站在操作系统的角度),核心都是依靠Thread类,只不过指定线程执行的任务的方式有所差异

细节上有点差别(站在代码耦合性角度):
通过 Runnable / lambda 的方式来创建线程 和 继承 Thread 类相比,代码耦合性要更小一些,在写 Runnable / lambda 的时候 run 中没有涉及到任何 Thread 相关的内容,这就意味着,很容易把这个逻辑从多线程中剥离出来,去搭配其他的并发编程的方式来执行,当然也可以很容易的改成不并发的方式执行

以上是关于线程 & 多线程的主要内容,如果未能解决你的问题,请参考以下文章

多个请求是多线程吗

多个用户访问同一段代码

线程学习知识点总结

多线程编程

python小白学习记录 多线程爬取ts片段

多线程二:jvm中的主线程&垃圾回收线程