文章目录

  • 1. 什么是线程和进程
  • 1.1 进程
  • 1.2 线程
  • 1.3 小节
  • 2. 线程和进程的联系
  • 2.1 图解进程和线程的关系
  • 2.2 程序计数器为什么是私有的?
  • 2.3 虚拟机栈和本地方法栈为什么是私有的?
  • 2.4 ⼀句话简单了解堆和方法区
  • 3. 并发与并行
  • 4. 为何使用多线程
  • 5. 使用多线程可能带来什么问题
  • 5.1 上下文切换
  • 5.2 死锁
  • 5.3 资源限制


1. 什么是线程和进程


1.1 进程

进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。

程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列

进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

Java 线程进程 java进程与线程_线程

进程是程序的⼀次执⾏过程,因此进程是动态的。系统运⾏⼀个程序即是⼀个进程从创建,运⾏到消亡的过程。

在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,⽽ main 函数所在的线程就是这个进程中的⼀个线程,也称主线程。

如下图所示,在 windows 中通过查看任务管理器的⽅式,我们就可以清楚看到 window 当前运⾏的进程(.exe ⽂件的运⾏)。

Java 线程进程 java进程与线程_进程_02


1.2 线程

线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位。

一个进程可以由很多个线程组成,线程间共享进程的所有资源(堆和⽅法区资源)。每个线程有自己的有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈。所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程

线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

Java 线程进程 java进程与线程_开发语言_03

Java 程序天⽣就是多线程程序,我们可以通过 JMX 来看⼀下⼀个普通的 Java 程序有哪些线程,代码如下。

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class MultiThread {
    public static void main(String[] args) {
        // 获取 Java 线程管理 MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false,
                false);
        // 遍历线程信息,仅打印线程 ID 和线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] " +
                    threadInfo.getThreadName());
        }
    }
}

运行结果:

Java 线程进程 java进程与线程_进程_04


1.3 小节

进程是资源分配的基本单位,它是程序执行时的一个实例,在程序运行时创建;线程是程序执行的最小单位,是进程的一个执行流,一个进程由多个线程组成的。


2. 线程和进程的联系

从 JVM 角度说进程和线程之间的关系


2.1 图解进程和线程的关系

下图是 Java 内存区域,通过下图我们从 JVM 的⻆度来说⼀下线程和进程之间的关系。

Java 线程进程 java进程与线程_进程_05

从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。

总结: 线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。


2.2 程序计数器为什么是私有的?

程序计数器主要有下⾯两个作⽤:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从⽽实现代码的流程控制,如:顺序执⾏、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置,从⽽当线程被切换回来的时候能够知道该线程上次运⾏到哪⼉了。

需要注意的是,如果执⾏的是 native ⽅法,那么程序计数器记录的是 undefined 地址,只有执⾏的是 Java 代码时程序计数器记录的才是下⼀条指令的地址。

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执⾏位置。


2.3 虚拟机栈和本地方法栈为什么是私有的?

虚拟机栈: 每个 Java ⽅法在执⾏的同时会创建⼀个栈帧⽤于存储局部变量表、操作数栈、常量池引⽤等信息。从⽅法调⽤直⾄执⾏完成的过程,就对应着⼀个栈帧在 Java 虚拟机栈中⼊栈和出栈的过程。

本地方法栈: 和虚拟机栈所发挥的作⽤⾮常相似,区别是: 虚拟机栈为虚拟机执⾏ Java ⽅法(也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地⽅法栈是线程私有的。


2.4 ⼀句话简单了解堆和方法区

堆和方法区是所有线程共享的资源,其中堆是进程中最⼤的⼀块内存,主要⽤于存放新创建的对象 (所有对象都在这⾥分配内存),⽅法区主要⽤于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。


3. 并发与并行

  • 并发: 同⼀时间段,多个任务都在执⾏ (单位时间内不⼀定同时执⾏);
  • 并行: 单位时间内,多个任务同时执⾏。

4. 为何使用多线程

先从总体上来说:

  • 从计算机底层来说: 线程可以⽐作是轻量级的进程,是程序执⾏的最⼩单位,线程间的切换和调度的成本远远⼩于进程。另外,多核 CPU 时代意味着多个线程可以同时运⾏,这减少了线程上下⽂切换的开销。
  • 从当代互联⽹发展趋势来说: 现在的系统动不动就要求百万级甚⾄千万级的并发量,⽽多线程并发编程正是开发⾼并发系统的基础,利⽤好多线程机制可以⼤⼤提⾼系统整体的并发能⼒以及性能。

再深⼊到计算机底层来探讨:

  • 单核时代:在单核时代多线程主要是为了提⾼ CPU 和 IO 设备的综合利⽤率。举个例⼦:当只有⼀个线程的时候会导致 CPU 计算时,IO 设备空闲;进⾏ IO 操作时,CPU 空闲。我们可以简单地说这两者的利⽤率⽬前都是 50%左右。但是当有两个线程的时候就不⼀样了,当⼀个线程执⾏ CPU 计算时,另外⼀个线程可以进⾏ IO 操作,这样两个的利⽤率就可以在理想情况下达到100%了。
  • 多核时代:多核时代多线程主要是为了提⾼ CPU 利⽤率。举个例⼦:假如我们要计算⼀个复杂的任务,我们只⽤⼀个线程的话,CPU 只会⼀个 CPU 核⼼被利⽤到,⽽创建多个线程就可以让多个 CPU 核⼼被利⽤到,这样就提⾼了 CPU 的利⽤率。

5. 使用多线程可能带来什么问题

并发编程的⽬的就是为了能提⾼程序的执⾏效率提⾼程序运⾏速度,但是并发编程并不总是能提⾼程序运⾏速度的,⽽且并发编程可能会遇到很多问题,⽐如:上下⽂切换、死锁还有受限于硬件和软件的资源限制问题。


5.1 上下文切换

并发执行并不一定比串行快?这是因为线程有创建和上下文切换的开销
因而,累加操作较小,即不超过百万次的时候,并发执行的优势不明显。


5.2 死锁

可以认为是两个线程或进程在请求对方占有的资源。

Java 线程进程 java进程与线程_开发语言_06

出现以下四种情况会产生死锁:

  • 情况一:相互排斥。一个线程或进程永远占有共享资源,比如,独占该资源。
  • 情况二:循环等待。例如,进程A在等待进程B,进程B在等待进程C,而进程C又在等待进程A。
  • 情况三:部分分配。资源被部分分配,例如,进程A和B都需要访问一个文件,同时需要用到打印机,进程A得到了这个文件资源,进程B得到了打印机资源,但两个进程都不能获得全部的资源了。
  • 情况四:缺少优先权。一个进程获得了该资源但是一直不释放该资源,即使该进程处于阻塞状态。

5.3 资源限制

  • 资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。
  • 硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。
  • 软件资源限制有数据库的连接数和socket连接数等。

资源限制引发的问题:

在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。

意思就是一段代码过于庞大,很有可能要执行许多次才可以执行结束,相比于单线程顺序执行,增加了许多上下文切换和资源调度的时间

有数据库操作时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库连接。