并发编程概述
1. 并发编程的基本概念
1.1 进程与线程
操作系统在启动一个程序时,往往会为这个程序创建一个进程。
进程是操作系统进行资源分配的最小单位,在一个进程中可以创建多个线程。
线程是比进程粒度更小的能够独立运行的基本单位,也是CPU调度的最小单元,被称为轻量级的进程。在一个进程中可以创建多个线程,多个线程各自拥有独立的局部变量、线程堆栈和程序计数器等,能够访问共享的资源。
进程&线程的区别:
- 进程是操作系统分配资源的最小单位,线程是CPU调度的最小单元。
- 一个进程中可以包含一个或多个线程,一个线程只能属于一个进程。
- 进程与进程之间是互相独立的,进程内部的线程之间并不完全独立。可以共享进程的堆内存、方法区内存和系统资源。
- 进程上下文切换要比线程的上下文切换慢得多。
- 进程是存在地址空间的,而线程本身没有地址空间,线程的地址空间是包含在进程中的。
- 某个进程发生异常不会对其他进程造成影响,某个线程发生异常可能会对所在进程中的其他线程造成影响。
1.2 守护线程
- 守护线程是一种特殊的线程,在系统后台完成相应的任务,例如JVM中的垃圾回收线程。
- 在程序运行的过程中,只要有一个非守护线程还在运行,守护线程就会一直运行。只有所有的非守护线程全部运行结束,守护线程才会退出。
- 在编写Java程序时,可以在调用start方法之前手动通过setDeamon方法指定当前线程是否是守护线程。
1.3 并行与并发
- 并行:多核CPU中的多个CPU核心同时执行多个线程。
- 并发:一个CPU核心在一段时间内交替执行了多个线程。
1.4 同步与异步
同步和异步主要是针对一次方法的调用来说的。以同步方式调用方法时,必须在方法返回后,才能执行后面的操作。以异步方式调用方法时,不需要等待方法返回,就可以执行后面的操作,当异步方法完成之后,会以通知或者回调的方式告知调用方。
1.5 共享和独享
共享:同一进程的多个线程在运行过程中共享某些系统资源,比如JVM中的方法区和堆空间。
独享:一个线程在运行过程中独占的某些系统资源,比如栈、本地方法栈和程序计数器。
1.6 临界区
临界区:能够被多个线程共享的资源或数据,每次只能提供给一个线程使用。
在并发编程中,临界区一般指受保护的对象或者程序代码片段。
1.7 阻塞与非阻塞
阻塞与非阻塞一般用来描述多个线程之间的相互影响。
阻塞:多个线程抢占临界区资源,抢占失败的线程必须阻塞等待。只有占用临界区资源的线程释放后,其他线程才可以再次抢占。
非阻塞:多个线程抢占临界区资源,线程之间不会相互影响,抢占失败的线程会继续执行。
2. 并发编程的风险
并发编程存在诸多优点,例如可以充分利用多核CPU的计算能力来提高应用性能。但是也存在一些风险,例如安全性问题、活跃性问题、性能问题。
2.1 安全性问题
多线程并发的情况下,程序的表现与期望不符。并发编程中一般叫做线程不安全。
2.2 活跃性问题
活跃性问题指的是:程序中某个操作无法正常执行下去了。死锁、饥饿与活锁都是典型的活跃性问题。
解决活跃性问题的方法就是在串行程序中避免出现无限循环的异常,在并发编程中避免出现死锁、饥饿和活锁等异常。
2.3 性能问题
一般来说,为了解决安全性问题,会为临界区添加锁,如果锁的粒度或者范围比较大,就会影响程序的执行性能。
另外,如果程序在运行过程中出现服务响应时间过长、资源消耗过高、系统吞吐量过低等问题,也会影响性能。
在并发编程中,尽量使用无锁的数据结构和算法,尽量减少锁的范围和持有时间,以提升程序的执行性能。
总之,提升程序的性能可以从三方面入手:提高吞吐量、降低延迟和提高并发量。
3. 并发编程中的锁
悲观锁和乐观锁
- 悲观锁:认为数据很容易被修改,线程每次进入临界区前,会尝试加锁。
- Java中的synchronized重量级锁是一种典型的悲观锁。
- 乐观锁:认为每次访问数据的时候其他线程都不会修改数据,需要更新数据时,检测数据是否被其他线程修改过,如果没有修改过则直接修改;如果修改过则尝试再次读取数据并修改,如此反复直到修改成功。
- Java中的synchronized轻量级锁属于乐观锁,基于CAS实现,采取版本号机制。
公平锁和非公平锁
- 公平锁:各个线程按照请求占用锁的顺序依次获取锁
- 非公平锁:当当前占用锁的线程释放锁之后,所有线程都可以获取到锁,不依赖请求顺序。
- ReentrantLock的默认实现是非公平锁,也可以实现为公平锁。
独占锁和共享锁
- 独占锁:也叫排它锁,采取悲观锁的机制,同一时刻只能有一个线程获取到锁。
- 共享锁:采取乐观锁的机制,允许多个线程同时访问临界区的资源
- ReentrantLock是一种独占锁,ReentrantReadWriteLock可以实现读/写锁分离,允许多个读操作同时获取读锁。
可重入锁
- 可重入锁:同一个线程可以多次占用同一个锁,后续占用时不需要再次加锁。
- ReentrantLock是一种可重入锁。
可中断锁&不可中断锁
- 可中断锁:锁被其他线程获取后,某个线程在阻塞等待的过程中,可能由于等待时间过长,而中断阻塞等待的状态,去执行其他任务。
- 不可中断锁:锁被其他线程获取后,某个线程如果想要获取这个锁,只能一直阻塞等待。
- ReentrantLock是一种可中断锁,synchronized是不可中断锁。
读/写锁
- 读/写锁分为读锁和写锁,持有读锁能够对共享资源进行读操作,持有写锁能够对共享资源进行写操作。读锁具有共享性,多个读锁可以同时存在。写锁具有排他性,写锁和写锁之间,写锁和读锁之间不能共存。
- ReadWriteLock是一种读/写锁。
自旋锁
- 自旋锁是指某个线程在没有获取到锁时,不会立即进入阻塞等待的状态,而是不断尝试获取锁,直到占用锁的线程释放锁。
- Java中的CAS是一种自旋锁。
死锁、饥饿与活锁
- 死锁:两个或多个线程互相持有对方所需要的资源,导致多个线程相互等待,无法继续执行后续任务的现象。
- 饥饿:一个或多个线程由于一直无法获得需要的资源而无法继续执行的现象。
- 活锁:两个或多个线程在同时抢占同一资源时,主动将资源让给其他线程使用,导致资源在多个线程间来回被占用,这些线程因无法获得所有资源而无法继续执行的现象。