并发指在宏观上的同一时间内同时执行多个任务. 为了满足这一需求, 现代的操作系统都抽象出线程的概念, 供上层应用使用.
这篇博文不打算详细展开分析, 而是对 java 并发中的概念和工具做一个梳理.
沿着并发模型, 并发要解决的问题, 基本工具, 衍生工具这一思路展开.
<!-- more -->
线程
首先线程是什么? 线程是由 OS 抽象并实现的, 我们知道 OS 的职责是管理并合理分配硬件资源, 那么 OS 为了更好的管理, 分配 CPU 资源, 同时也为了满足同时执行任务这一需求, 设计了线程这一概念.
虽然 java 程序运行在 JVM 虚拟机上, 但是 java 的线程仍然是对操作系统原生线程的封装, 同时, jvm 对线程实现时也将 jvm 的运行栈设计成线程私有内存, 因此, java 线程和原生线程在理解上实际上没太大区别.
线程的五种状态:
graph LR
新建 --> 就绪;
就绪 --> 运行;
运行 --> 就绪;
运行 --> 阻塞;
阻塞 --> 就绪;
运行 --> 死亡;
先来看上面的就绪状态和运行状态. 我们知道线程虽然宏观上是同时执行的, 但是微观上使用如时间片轮转算法使得线程依次执行. 那么, 同一时间只有一个线程执行, 其它需要执行的线程处于就绪队列中, 等待自己被调度到.
而如果线程想要暂时放弃在 CPU 上运行的权利, 就会阻塞自己. 这时对应着阻塞状态, 同时线程会从就绪队列中移除, 进入等待队列.
很显然, 阻塞线程被唤醒肯定是进入就绪队列等待调度, 而不可能是直接分配到 CPU 上运行.
在线程同步时, 线程可能由于以下情况被阻塞:
同步阻塞. 就是被锁阻塞.
等待阻塞. 被条件变量阻塞.
其它. 调用 sleep(), join() 或等待 IO 操作时的阻塞.
并发需要解决的问题
功能性问题
线程同步面临两个问题, 想象下有两个线程在协作工作完成某项任务. 那么需要解决以下问题:
线程两个线程之间交互数据, 必然涉及到数据共享. 而某些数据资源无法被多个线程同时使用 (临界区), 这时需要, 即线程互斥问题.
假如一个线程进行的太快, 另外一个线程就需要等等它, 即线程同步问题.
性能和可用性问题
在多线程程序的性能问题上, 如果是对于同样一段临界区的多线程访问, 那么则有以下几个思路:
互斥锁. 互斥锁即保证同一时间只有一个线程访问临界区并完整执行完, 其它线程在临界区外面等待.
无障碍或无锁. 线程们一开始直接进入临界区执行, 注意其中不能修改共享数据. 执行完后再判断刚才这段时间是否有其它线程执行, 没有的话才修改共享数据, 如果有的话就回滚重来.
降低锁粒度. 也即将这个大的临界区拆分成几个小的临界区, 分别加互斥锁控制, 这样提高了线程同时访问的临界区的机会变多, 性能提高. 显然这要对代码仔细推敲, 考虑如何拆分锁粒度而不影响整体的语义.
以上三种思路的性能优劣没有一个普适的结果, 和具体的场景相关.
并发中还会出现以下几种情况导致系统不可用:
死锁. 不解释.
饥饿. 线程调度算法如果不是平等分配的, 那么就可能出现优先级高的线程长时间占用 CPU, 导致优先级低的线程无法得到执行机会.
活锁. 这个我解释不来...
并发代码的几个性质
并发编程中需要考虑的几个概念:
原子性: 指某个操作一旦被某个线程执行, 直到该操作执行完毕都不会有其它线程来干扰.
可见性: 指某个变量或某块内存如果被 A 线程修改, B 线程能否马上读取到修改后的值.
有序性: A 线程执行的代码序列, 在 B 线程看来是否是有序的.
从我个人的理解来看, 原子性属于由并发和线程这一理论概念自然而然推导衍生而来的概念, 而可见性和有序性是具体的工程实践中产生的.
实际中, jvm 并不能实现的特别完美, 总会有工程上的妥协. 理论模型与实际模型无法完美契合, 总存在一定的偏差.
比如说, jvm 为了向性能妥协使用了缓存机制, 牺牲了数据一致性, 这就产生了可见性的概念, 需要程序员编程时自己控制.
jvm 为了指令更高效率的执行进行了指令重排优化, 则产生了有序性的问题. 印象里以前大学里学过的 CPU 的流水线技术, 为了指令能够更好的被 CPU 流水线利用, 减少流水线的空闲时间, 编译器编译时也会在不影响串行语义的前提下, 进行指令重排.
总而言之, 这是在性能和理论模型完整性之间的一种妥协.
并发的工具
技术上的工具, 概念繁多复杂, 但是如果我们能理解技术设计上无时无刻的不运用抽象和分层的手段,
那么, 我们可以把技术上的工具分为两种:
最基本的, 原生的工具.
在原生提供的工具上, 进行封装得到的更高层次的工具.
更高层次的工具对基础工具进行了抽象和封装, 屏蔽了其中的实现细节.
这里想强调的是, 工具的接口和实现是分开的, 两者可以没有关系.
如 java 的监视器锁从接口上来看, 其语义和互斥锁一样. 然而它并不一定使用互斥锁实现, 而是可以为了性能存在优化, 只要最终的行为与接口相同即可.
基本工具
锁, 条件变量, 信号量
有三种用于线程同步的工具:
锁. 锁可用于规定一个临界区, 同一时间临界区内仅能由一个线程访问. 其他线程则在临界区外等待 (阻塞).
互斥锁. 使用信号量实现. 临界区外等待的线程会被阻塞.
自旋锁. 临界区外等待的线程会忙等.
条件变量 (Condition). 线程在某种条件不满足时阻塞自己, 等待其它的线程条件满足时再唤醒它们. 很显然所有等待的线程要放入一个数据结构中, 这个数据结构就在条件变量内.
信号量. 操作系统原生的机制. 实际上, 锁 + 条件变量可完成所有信号量可以完成的逻辑.
在 java 中, Object 类有 wait(),notify() 和 notifyAll() 之类的方法.
这些方法可以认为每个对象都内置了一个条件变量, 而这些方法是对这些条件变量的操作, 因此, 可以使用这些方法将对象当作条件变量使用, 从而做到线程的同步.
来源: http://www.jianshu.com/p/6b89b8a4b835