什么是进程?进程是程序的⼀次执⾏过程,是系统运⾏程序的基本单位,因此进程是动态的。系统运行和关闭⼀个程序即是⼀个进程从创建,运⾏到消亡的过程。在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,⽽ main 函数所在的线程就是这个进程中的⼀个线程,也称主线程
什么是线程?线程与进程相似,但线程是⼀个⽐进程更⼩的执⾏单位。⼀个进程在其执⾏的过程中可以产⽣多个线程。与进程不同的是同类的多个线程共享进程的堆和⽅法区资源,但每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈,所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程
二者之间的区别与联系如下:
- 使用定位,进程是分配资源的基本单位,线程是独立运行和独立调度的基本单位,多进程是指操作系统能同时运行多个任务(程序)。多线程是指在同一程序中有多个顺序流在并发执行
- 地址空间和资源:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见,每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含n个线程;线程没有独立的代码和数据空间,一个进程下的多个线程需要共享进程的资源,线程间切换开销小,比进程切换快的多
- 状态阶段:线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
- 通信方式:进程间通信IPC(IPC是intent-Process Communication的缩写,含义为进程间通信或者跨进程通信),线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
对应于我们的JVM模型如下:
启动多少个Java程序,就会创建多少个JVM进程,也称之为JVM实例。而每一个JVM实例都是独立的,它们互不影响。这也是前面所说的一个程序可以被多个进程共用的情况
程序计数器为什么是私有的?
- 字节码解释器通过改变程序计数器来依次读取指令,从⽽实现代码的流程控制,如:顺序执⾏、选择、循环、异常处理。
- 在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置,从⽽当线程被切换回来的时候能够知道该线程上次运⾏到哪⼉了。需要注意的是,如果执⾏的是 native ⽅法,那么程序计数器记录的是 undefined 地址,只有执⾏的是 Java 代码时程序计数器记录的才是下⼀条指令的地址。
程序计数器私有为了保证线程切换后能恢复到正确的执⾏位置。
虚拟机栈和本地⽅法栈为什么是私有的?
- 虚拟机栈: 每个 Java ⽅法在执⾏的同时会创建⼀个栈帧⽤于存储局部变量表、操作数栈、常量池引⽤等信息。从⽅法调⽤直⾄执⾏完成的过程,就对应着⼀个栈帧在 Java 虚拟机栈中⼊栈和出栈的过程。
- 本地⽅法栈: 和虚拟机栈所发挥的作⽤⾮常相似,区别是: 虚拟机栈为虚拟机执⾏ Java ⽅法(也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。
虚拟机栈和本地⽅法栈私有为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地⽅法栈是线程私有的。
堆和⽅法区为什么是公有的?
- 堆是所有线程共享的资源,其中堆是进程中最⼤的⼀块内存,主要⽤于存放新创建的对象 (所有对象都在这⾥分配内存)
- ⽅法区主要⽤于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
堆和⽅法区公有为了保证线程都能共享到堆中创建的对象以及方法区中的类型。
多线程的设计原则
在进行多线程编程的时候我们需要遵循如下的设计原则,如果违反该原则,则有可能导致错误:
- 原子性,原子是世界上最小的单位,具有不可分割性。在我们编程的世界里,某个操作如果不可分割我们就称之为该操作具有原子性。例如:i = 0,这个操作是不可分割的,所以该操作具有原子性。如果某个操作可以分割,那么该操作就不具备原子性,例如i++。非原子操作都存在线程安全问题,这个时候我们需要使用同步机制来保证这些操作变成原子操作,来确保线程安全。
- 可见性,线程可见性是指线程之间的可见性**,即一个线程对状态的修改对另一个线程是可见的**,也就是一个线程修改的结果,另外一个线程立马就知道了。比如volitile修饰的变量,就具备可见性。
- 有序性,有序性指的是数据不相关的变量在并发的情况下,实际执行的结果和单线程的执行结果是一样的,不会因为重排序的问题导致结果不可预知。volatile, final, synchronized,显式锁都可以保证有序性。
遵循以上设计原则进行编程,才能正确的进行并发编程。
多线程的优势
为什么我们要使用多线程的设计呢?换句话说我们为什么要使用多线程的设计呢?
- 并发能力强: 现在的系统动辄要求百万级甚⾄千万级的并发量,⽽多线程并发编程正是开发⾼并发系统的基础,利⽤好多线程机制可以⼤⼤提⾼系统整体的并发能⼒以及性能。
- 切换和调度成本低: 线程可以⽐作是轻量级的进程,是程序执⾏的最⼩单位,线程间的切换和调度的成本远远⼩于进程。另外,多核 CPU 时代意味着多个线程可以并行,这减少了线程上下⽂切换的开销。
- 提高 CPU 利⽤率,假如我们要计算⼀个复杂的任务,我们只⽤⼀个线程的话,CPU 只会⼀个 CPU 核⼼被利⽤到,⽽创建多个线程就可以让多个 CPU 核⼼被利⽤到,这样就提⾼了 CPU 的利⽤,这样提高了并行性能,如果一个子任务在执行过程中阻塞,例如等待文件A从磁盘读取,此时CPU是闲置的,但是CPU如果开启了两个线程,那么第二个线程可以开启读取文件B的任务,CPU不会闲置,等到B文件从磁盘读取时,A文件读取好了CPU再调度到A线程上。
- 更快的响应速度,当再服务端的时候,如果客户端发起一个下载任务,那么服务端主线程执行下载,显示进度的线程显示下载进度,二者交替执行,才能给客户实施的响应,如果没有多线程,那么下载结果不可预知。
其实也可以认为是多线程和多进程的优势,因为这里我们是针对Java程序这一个运行的进程来看的,所以重点讨论关注范围就是一个进程下的多线程并发编程。
多线程的问题
我们可以从多线程的设计原则中可以看到,多线程虽然并发能力强、调度成本低,CPU利用率高,但是因为其存在对共享和可变状态的资源进行访问,所以存在一定的线程安全问题。
- 共享就意味着变量可以被多个线程同时访问。我们知道系统中的资源是有限的,不同的线程对资源都是具有着同等的使用权。有限、公平就意味着竞争,竞争就有可能会引发线程竞争问题。
- 可变是指变量的值在其生命周期内是可以发生改变的。“可变”对应的是“不可变”。我们知道不可变的对象一定是线程安全的,并且永远也不需要额外的同步(因为一个不可变的对象只要构建正确,其外部可见状态永远都不会发生改变)。所以可变意味着存在线程不安全的风险。
所以基于多线程的设计原则和产生的问题,我们可以归结: 要通过一些方式解决共享可变带来的不能满足多线程设计原则(原子、可见、有序)的线程安全问题
- 多线程的执行顺序不可重现,但是必须要求执行结果必须可以重现
- 线程的共享数据操作不完整性就一定会出现数据被破坏,而导致结果无法预知的问题,线程的安全问题
要解决线程安全问题,就需要使用线程同步
线程同步
首先看下什么是线程安全?其核心概念就是正确性。所谓正确性就是某类的行为与其规范完全一致,当多个线程访问某各类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的,当然大多数类不一定能满足线程安全的条件,此时就需要线程同步来实现。
什么是线程同步?其核心就在于一个同。所谓“同”就是协同、协助、配合,也就是按照预定的先后顺序进行运行,即你先,我等, 你做完,我再做。
- 线程同步,就是当线程发出一个功能调用时,在没有得到结果之前,该调用就不会返回,其他线程也不能调用该方法。就一般而言,我们在说同步、异步的时候,特指那些需要其他组件来配合或者需要一定时间来完成的任务。
- 在多线程编程里面,一些较为敏感的数据时不允许被多个线程同时访问的,使用线程同步技术,确保数据在任何时刻最多只有一个线程访问,保证数据和操作的完整性。
所以我们需要掌握线程同步的机制方式,线程同步的机制主要有:临界区、互斥量、事件、信号量四种方式
- 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
- 互斥量:用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享。
- 信号量:它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。
- 事 件: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。
所以其实以上的几种机制都是需要实现对公共资源的控制来达到目的。
Java多线程实现
Java线程模型基于操作系统原生线程模型实现。因此操作系统支持怎么样的线程模型,很大程度上决定了Java虚拟机的线程如何映射,这一点在不同的平台上没有办法达成一致,虚拟机规范中也未限定Java线程需要使用哪种线程模型来实现。线程模型只对线程的并发规模和操作成本产生影响,对于Java程序来说,这些差异是透明的:
也就是一条
- Java线程就映射到一条轻量级进程(Light Weight Process)中,而一条轻量级线程又映射到一条内核线程(Kernel-Level Thread)。
- 我们平时所说的线程,往往就是指轻量级进程(或者通俗来说我们平时新建的java.lang.Thread就是轻量级进程实例的一个"句柄",因为一个java.lang.Thread实例会对应JVM里面的一个JavaThread实例,而JVM里面的JavaThread就应该理解为轻量级进程)。
- 我们在应用程序中创建或者操作的java.lang.Thread实例最终会映射到系统的内核线程
如果我们恶意或者实验性无限创建java.lang.Thread
实例,最终会影响系统的正常运行甚至导致系统崩溃
Java线程优先级
在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通setPriority(int)
方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定
Java线程调度
线程调度方式包括两种,协同式线程调度和抢占式线程调度
线程调度方式 | 描述 | 优势 | 劣势 |
协同式线程调度 | 线程的执行时间由线程本身控制,执行完毕后主动通知操作系统切换到另一个线程上 | 某个线程如果不让出CPU执行时间可能会导致整个系统崩溃 | 实现简单,没有线程同步的问题 |
抢占式线程调度 | 每个线程由操作系统来分配执行时间,线程的切换不由线程自身决定 | 实现相对复杂,操作系统需要控制线程同步和切换 | 不会出现一个线程阻塞导致系统崩溃的问题 |
Java线程最终会映射为系统内核原生线程,所以Java线程调度最终取决于系操作系统,而目前主流的操作系统内核线程调度基本都是使用抢占式线程调度。那么Java中线程的生命周期和调度方式是怎样的呢?
Java线程同步
Java中实现线程同步的方式有以下几种,以具体使用的方式来举例,第一类是使用锁,使用锁会使运行效率降低:
- 关键字synchronized,可以修饰同步代码块和同步方法,Java的每个对象都有一个内置锁,当用此关键词修饰方法时,内置锁会保护整个方法,在调用该方法前,需要获取内置锁,否则就处于阻塞状态,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
- JUC包-使用重入锁类实现线程同步,在JavaSE5.0中新增了一个
java.util.concurrent
包来支持同步。ReentrantLock类可重入、互斥,实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力
第二类是使用一些特殊机制避免使用锁,但是可能需要额外开销或功能阉割。
- 关键字volatile及JMM内存模型,修饰共享变量,保持数据可见性,volatile关键字为域变量的访问提供了一种免锁机制。要了解具体用法,需要掌握JMM内存。
- 使用局部变量实现线程同步,如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响
第三类是在实现层面上考量而非底层实现机制上考量:
- JUC包-使用并发集合类实现线程同步,使用
LinkedBlockingQueue<E>
来实现线程的同步LinkedBlockingQueue<E>
是一个基于已连接节点的,范围任意的blocking queue。队列是先进先出的顺序,类似Redis的list实现方式
第四类是我们干脆使用操作原子性的线程安全的类来使用就不用考虑同步问题了
- JUC包-原子类,需要使用线程同步的根本原因在于对普通变量的操作不是原子的。那么什么是原子操作呢?原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,即-这几种行为要么同时完成,要么都不完成
依据以上的线程同步方式,接下来本系列的行文布局会分为三大块:
- 锁机制,Java中的锁分类以及synchronized关键字,它的实现原理和机制、Java中锁的种类切换、死锁的现象和避免,JUC并发包下的高级锁的使用方式
- JMM内存模型,JMM内存模型、volatile关键字、ThreadLocal的实现方式
- JUC并发包下 并发集合类的使用方式、线程池的使用方式、原子类的使用方式
那么接下来正式开启线程同步实现方式的探索之旅吧!
总结
本篇Blog初步介绍了什么是进程和线程、线程的优势和问题;基于这些抽象的概念,我们来看Java中的多线程具体实现方式和线程同步的实现方式,所以我们需要掌握Java线程调度的实现以及Java同步方法,这也是后续几篇Blog的行文逻辑。