美文网首页
四、Java高级特性(多线程基础概念篇)

四、Java高级特性(多线程基础概念篇)

作者: 大虾啊啊啊 | 来源:发表于2021-05-26 10:01 被阅读0次

一、进程和线程的概念

1、进程

进程是操作系统进行资源分配的最小单元,资源例如:cpu,内存,磁盘IO。进程之间是独立的。例如我们在windows上安装的一个PPT程序叫应用程序,当我们启动我们的PPT程序,操作系统中就会存在一个PPT的应用程序进程。

2、线程

线程是CPU调度的最小单位,必须依赖于进程存在,不能独立存在。一个进程可以拥有多个线程,线程可以共享进程的资源,包括内存、磁盘IO。

二、CPU核心数和线程数的关系

通过下图看到我们计算机的一个配置信息:内核是4个,逻辑处理器是8个,也就意味着CPU核心数是4个,并且我们的计算机同时可以跑8个线程。

image.png

三、并行和并发的概念

1、并行

并行就是指可以同时运行的任务数。可以理解为操作系统同时可以运行的线程数。例如以上我们计算机的配置,核心线程数是8,意思就是并行数是8。


image.png

2、并发

并发指的是:某个时间内可以执行的任务数。时间片轮转其实就是一种并发机制。例如我们的并行数虽然是8个,但是由于CPU时间片的轮转,我们1S内执行了100个线程,那我们就可以理解成我们的1S内的并发是100。

image.png

3、并发编程的好处

采用多线程编程的好处:充分利用CPU的资源,加快响应用户的时间。代码模块化,异步化。
需要注意:线程共享进程的资源,并发编程就导致线程抢占资源。使用锁,也可能会导致死锁等等。

四、线程的基本状态

线程基本状态有7种。


image.png
  • 创建一个线程Thread的时候,线程处于初始状态
  • 调用Thread.start的时候,线程处于就绪、运行状态,就绪状态指的是等待CPU分配时间片,当拿到时间片的时候,就处于运行状态
  • 调用wait、join、LockSupport.park方法后,线程处于等待状态
  • 调用waite、sleep相关传入时间,线程处于等待超时状态
  • 当线程调用notify被唤醒,或者线程等待超时结束之后,线程继续进入就绪、运行状态
  • 当线程调用synchronized修饰的代码块,没有拿到锁的时候,线程进入了阻塞状态
  • 当线程执行完毕,线程处于终止状态

六、上下文切换

1、什么是上下文切换

我们知道在处理多线程并发任务的时候,处理器会为每一个线程分配CPU时间片,线程在各自的时间片内执行任务。每个时间片大概几十毫秒,所以在1秒钟内可能会发生几十上百次的线程相互切换。因为切换的速度比较快,所以我们会感觉线程是同时执行的。
线程只在CPU分配的时间片内执行任务,当一个线程的时间片用完了,或者自身因素被迫暂停运行的时候,就会有另外一个线程来占用这个CPU。这种一个线程让出CPU使用权,另外一个线程获取占用CPU使用权的过程就叫做上下文切换。
一个线程让出CPU使用权就是切出,另外一个线程获取占用CPU使用权就是切入。这个切出切入的过程中,操作系统会保存和恢复线程的相关进度的信息。这个进度信息我们就称之为上下文。上下文一般包含了寄存器存储的内容和程序计数器的指令内容。

2、上下文切换的原因

在多线程编程中我们知道上下文的切换会造成性能问题,那么是什么原因造成上下文切换呢?


image.png

我们先来回顾线程的几种状态:
(1)当一个线程从就绪状态切换到运行中状态的时候,就是上下文的一次切换。
(2)从运行中状态到阻塞状态、等待状态、等待超时状态,从这些状态再到就绪状态,再从就绪态再到运行中状态,又是一次上下文的切换。

参考右图,线程从运行中状态到阻塞状态、等待状态、等待超时状态的时候,我们叫做线程暂停。当线程暂停的时候,就会让出CPU的使用权,这个CPU就会有别的线程来占用,这个时候操作系统就会保存上下文的信息,方便这个线程再次运行的时候在原来的进度执行。当线程从暂停到就绪状态的时候,我们称为线程的唤醒,此时操作系统就会恢复之前保存的上下文信息,当线程拿到时间片后在原来的上下文继续执行,变为运行中状态。

3、上下文切换的类型

(1)自发性上下文切换
自发性上下文切换是由JAVA程序调用导致切出的,一般是在编码的时候,调用以下几个方法:

sleep()
wait()
yield()
join()
park();
synchronized
lock

(2)非自发性上下文切换
非自发性上下文切换一般包含:线程分配的时间片用完、虚拟机垃圾回收、或者线程的执行优先级。

4、多线程执行速度一定大于单线程吗?

代码演示线程的上下文切换以及性能测试
package cn.enjoyedu.concurrent.cotext;


public class TreadConcurrentTest {
    private static final long count = 10000;

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

    }

    /**
     * 多线程并行执行
     * @throws Exception
     */
    private static void current() throws Exception {
        long start = System.currentTimeMillis();
        Thread thread = new Thread(new Runnable() {
            public void run() {
                int a = 0;
                for (int i = 0; i < count; i++) {
                    a += 5;
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                int b = 0;
                for (long i = 0; i < count; i++) {
                    b--;
                }
            }
        });
        //启动线程1 和2
        thread.start();
        thread2.start();
        //阻塞主线程
        thread.join();
        thread2.join();
        long time = System.currentTimeMillis() - start;
        System.out.println("发执行时间:" + time + "ms");
    }

    /**
     * 单线程串行执行
     */
    private static void serial() {
        long start = System.currentTimeMillis();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a += 5;
        }
        int b = 0;
        for (int i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        System.out.println("串行执行时间:" + time + "ms");
    }
}
发执行时间:2ms
串行执行时间:1ms

以上结果并发执行时间大于单线程串行执行时间。
在之前我们提到了并发编程的好处,但是并发编程优点并不是绝对的。多线程并发意味着要对线程的创建和销毁,对操作系统带来一定的开销。同时多线程带来的上下文切换也会带来性能的开销,主要体现在保存和恢复上下文信息。
一般情况下执行任务较少的时候,我们采用单线程,执行任务较多的时候,可以考虑采用多线程。

5、如何避免频繁的上下文切换?

(1)在实际开发中,我们有时候需要加锁,所以对于加锁我们需要注意,加锁和释放锁会导致比较多的上下文切换以及调度延时。引起性能问题。实际开发中需要注意。
(2)创建过多的线程,如果创建线程太多,线程切换的速度大于线程的执行速度。一般情况下创建的数量控制在CPU的核心线程数。

相关文章

网友评论

      本文标题:四、Java高级特性(多线程基础概念篇)

      本文链接:https://www.haomeiwen.com/subject/mttksltx.html