初识多线程

1. 前置知识——进程

在学习多线程前需要了解操作系统中的基本知识,这里简单回顾下。

1.1 进程控制块

一个进程对应着一个进程控制块PCB,PCB是一个用于管理和维护进程信息的数据结构,这个数据结构中大致包含下面内容(并不完整):

  1. 进程标识符PID:唯一标识进程的数值
  2. 进程分配的资源:如分配的内存(指向内存的指针)以及文件描述符表(该进程打开了什么文件)等
  3. 程序计数器:指向进程当前执行的指令的地址(由于进程需要经常被调度,因此需要寄存器记录该进程执行到哪了)

1.2 进程的五种状态

进程共有五种状态,它们分别为:

  1. 运行态:指进程上CPU运行的状态;
  2. 就绪态:指进程在就绪队列中等待CPU调度的状态;
  3. 阻塞态:指进程上CPU运行过程中,(由于某些原因)发现需要阻塞,知道满足条件后才能回到等待调度的状态;
  4. 创建态:操作系统为进程分配资源,创建PCB;
  5. 终止态:操作系统回收进程资源,撤销PCB;

状态间的关系如下:

Untitled Diagram.drawio.png

1.3 进程的调度

进程的调度是指一个进程由就绪态到运行态的过程,在引入多线程之前,调度的基本单元是进程,这里我们先了解一下进程的调度,以便后续了解多线程的调度。

我们可以把就绪队列简单的看作一个链式队列,就绪队列会根据PCB的优先级组织PCB,当CPU处于空闲状态的时候,调度器就会从就绪队列中取出PCB,此时进程就由就绪态转为运行态了。

image.png

在程序猿的视角中,调度器将进程由就绪态调度上CPU进行运行的过程是透明的,我们可以把这么一个过程看作调度器对就绪队列的进程的随机调度。

2. 线程的引入

引入线程后,调度的基本单位不再是进程,而是线程。也就是说前面我们讲到的进程的调度,此时基本单位是线程,也就是在

2.1 进程与线程的区别

  1. 一个进程可能包含一个或多个线程,而一个线程只能属于一个进程
  2. 每个进程都有独立的虚拟地址空间,也有自己独立的文件描述符,同一个进程的多个线程之间,则共用这一份虚拟地址空间和文件描述符表
  3. 进程是资源分配的基本单位,线程是调度的基本单位
  4. 一个进程挂了一般不会影响到其他进程,一个线程挂了很可能把整个进程带走,其他线程也就没了

2.2 为什么要使用多线程

为了提高CPU的资源利用率,可能会选择通过多进程以及多线程的方式来处理一段程序,然而在编程中为什么更加倾向于使用多线程呢,原因如下:

  1. 首先,由于进程的独立性,每个进程都有自己独立的虚拟地址空间,因此进程间进行通信的步骤较为麻烦;

  2. 更为重要的一点是创建进程需要涉及资源分配的工作,如分配内存空间以及创建文件描述符表,而同一个进程的多个线程共享资源,则省去了分配资源的步骤。


3. 第一个自定义线程

其实我们在程序开发的过程中早就涉及到多线程了:

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

即使是一个最简单的hello world程序,其实在运行的时候也设计到“线程”了,虽然我们没有手动在上面的代码中创建其他线程,JVM内部也会创建出多个线程,如:主线程,垃圾扫描线程等。

3.1 定义线程

通过继承Thread的方式自定义一个线程,run方法中描述的是线程执行的具体任务:

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

run方法描述的是线程的工作

3.2 创建线程

前面只是定义了一个线程需要完成的工作,我们需要在程序中实例化线程,并且调用它的start()方法才算是创建了一个线程:创建出线程的TCB,并加入到就绪队列中,参与调度。

public class Demo1 {

    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
        System.out.println("hello world!");
    }
}

运行结果:

hello world!
hello thread!

3.3 线程的随机调度

这里如果对调度器的随机调度理解不是很深,可能会提出一个疑问:明明我们是先调用了t.start()方法,为什么运行结果中先打印了hello world!呢?

线程中没有父线程和子线程的概念,多个线程涉及并发与并行,后续我们统称为并发

  1. 并发指的是当cpu忙碌的时候轮流调用线程,一般是一个时间片结束的时候,将cpu中运行的线程放入就绪队列,然后再随机调度就绪队列中的线程
  2. 并行指的是多核cpu能同时进行运行多个线程

原因:前面我们讲到,创建线程后会将TCB添加到就绪队列中等待调度器调度,然而调度的过程是随机的,不可预知的

我们在启动程序的时候就会有一个main线程,而当main进程在cpu中执行到t.start()语句后,会创建一个MyThread线程(在就绪态等待调度器调度),由于两个线程是并发的,因此打印的结果是随机的。


听了上面的解释,大家此时可能又有一个疑问:既然调度是随机的,为什么我执行了这么多次都是先打印的Hello world!

由于线程的创建是需要开销的,因此可能大家尝试了许多次都是先执行main线程中的语句,但谁也不能保证第n次运行程序的时候,顺序是否发生变化。

3.4 进程退出码

在console中不只打印了hello world!hello thread!,还打印了一句:

Process finished with exit code 0

操作系统中用进程的退出码来表示“进程的运行状态”,而上面的code 0就是进程的退出码,在C语言阶段的main函数有一个return值,都是写作了return 0

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

4. jconsole的使用

在jdk中,有一个叫jconsole的运行程序,通过该程序可以观察线程的基本信息以及调用方法栈,在多线程的开发中经常需要使用该工具来定位问题。

这里我们加上一个死循环观察线程的调度。

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

public class Demo1 {

    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
        while (true) {
            System.out.println("hello world!");
        }
    }
}

找到自己jdk的位置,并打开jconsole(如果是windows,需以管理员身份打开),我的系统是macos,位于jdk目录下的:jdk-1.8.jdk/Contents/Home/bin/jconsole:

在这里可以看到java的所有进程,由于我的启动类名为Demo1,因此直接连接thread.Demo1

image.png

直接点击不安全的连接:

image.png

重点关注这两个线程,分别为main线程,和我们刚才自定义的线程。

image.png

在列表的右边显示的就是当前线程的信息,上半部分为线程信息,下半部分为线程的函数调用栈,线程信息的内容如下:

  1. 线程名称Name(程序员可在创建时自定义)
  2. 状态State:此时状态为阻塞blocked,原因是main此时在使用打印机,因此被main线程阻塞
  3. 总阻塞次数

5. 创建线程的常见方式

  1. 创建一个类继承Thread,重写run,前面已经用过,不多赘述

  2. 创建一个类,实现runnable接口,重写run

    此时Runnable相当于定义了一个任务,还是需要实例化Thread实例,把任务交给Thread,这个写法,线程和任务是分开的,可以更好地解耦合。

//实现runnable接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello thread!");
        }
    }
}
public class Demo2 {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        //创建线程
        thread.start();
        while (true) {
            System.out.println("hello world!");
        }
    }
}
  1. 匿名内部类(Thread)
public class Demo3 {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread!");
                }
            }
        };
        thread.start();
        
        while (true) {
            System.out.println("hello world!");
        }
    }
}
  1. 匿名内部类(Runnable)
public class Demo4 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread");
                }
            }
        });

        thread.start();

        while (true) {
            System.out.println("hello world");
        }
    }
}
  1. [推荐]lambda表达式为方式4的简化版
public class Demo5 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("hello thread!");
            }
        });
        thread.start();
        while (true) {
            System.out.println("hello world");
        }
    }
}

6. 性能对比

对比单线程和双线程的情况下,变量自增20亿次所耗费时间

单线程:

public class Demo6 {
    private static final long COUNT = 10_0000_0000;
    private static void serial() {
        long begin = System.currentTimeMillis();

        int a = 0;
        for (int i = 0; i < COUNT; i++) {
            a++;
        }
        a = 0;
        for (int i = 0; i < COUNT; i++) {
            a++;
        }
        long end = System.currentTimeMillis();
        System.out.println("共花费了" + (end - begin) + "ms");
    }

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

多线程:这里需要用到join()方法来保证两个线程执行完毕才计时,并且该方法可能抛中断异常InterruptedException(当线程运行中断时会触发的异常)

private static void concurrency() {
    long begin = System.currentTimeMillis();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < COUNT; i++) {
            int a = 0;
            a++;
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < COUNT; i++) {
            int a = 0;
            a++;
        }
    });
    t1.start();
    t2.start();

    try {
        //使用join方法,保证等待t1,t2两个线程执行完毕
        t1.join();
        t2.join();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    long end = System.currentTimeMillis();
    System.out.println("共花费了" + (end - begin) + "ms");
}

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

在我的机器上,单线程的耗费时间大概在1500~1600ms区间,双线程的耗费时间大概在900~1000ms区间。

由此可见线程虽说可以提高效率,但并不是预想中的双线程就将性能提高两倍左右,因为多线程的场景涉及创建线程以及频繁的调度线程的开销。

7. 多线程的使用场景

  1. CPU密集型场景:代码中大部分工作都是在使用CPU进行运算(比如反复++的操作),使用多线程可以更好的利用CPU多个核心并行计算资源,从而提高效率。
  2. I/O密集型场景:读写磁盘,读写网卡这些操作都属于I/O,当线程在运行时遇到I/O操作就会由运行态转为阻塞态,串性执行程序的话,此时CPU就会处于空闲的状态,引入多线程可以避免CPU过于闲置。
  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

干脆面la

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值