线程安全
“当多个线程访问一个对象时 如果不用考虑这些线程在运行时环境下的调度和交替执行 也不需要进行额外的同步 或者在调用方进行任何其他的协调操作 调用这个对象的行为都可以获得正确的结果 那这个对象是线程安全的”
Java语言中的线程安全
按照线程安全的“安全程度”由强至弱来排序 我们可以将Java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立
1.不可变
在Java语言中(特指JDK 1.5以后,即Java内存模型被修正之后的Java语言) 不可变(Immutable)的对象一定是线程安全的 无论是对象的方法实现还是方法的调用者 都不需要再采取任何的线程安全保障措施 只要一个不可变的对象被正确地构建出来(没有发生this引用逃逸的情况) 那其外部的可见状态永远也不会改变 永远也不会看到它在多个线程之中处于不一致的状态 “不可变”带来的安全性是最简单和最纯粹的
Java语言中 如果共享数据是一个基本数据类型 那么只要在定义时使用final关键字修饰它就可以保证它是不可变的 如果共享数据是一个对象 那就需要保证对象的行为不会对其状态产生任何影响才行
2.绝对线程安全
绝对的线程安全完全满足Brian Goetz给出的线程安全的定义 这个定义其实是很严格的 一个类要达到“不管运行时环境如何 调用者都不需要任何额外的同步措施”通常需要付出很大的 甚至有时候是不切实际的代价 在Java API中标注自己是线程安全的类 大多数都不是绝对的线程安全
3.相对线程安全
相对的线程安全就是我们通常意义上所讲的线程安全 它需要保证对这个对象单独的操作是线程安全的 我们在调用的时候不需要做额外的保障措施 但是对于一些特定顺序的连续调用 就可能需要在调用端使用额外的同步手段来保证调用的正确性
在Java语言中 大部分的线程安全类都属于这种类型 例如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等
4.线程兼容
线程兼容是指对象本身并不是线程安全的 但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用 我们平常说一个类不是线程安全的 绝大多数时候指的是这一种情况 Java API中大部分的类都是属于线程兼容的 如Vector和HashTable相对应的集合类ArrayList和HashMap等
5.线程对立
线程对立是指无论调用端是否采取了同步措施 都无法在多线程环境中并发使用的代码 由于Java语言天生就具备多线程特性 线程对立这种排斥多线程的代码是很少出现的 而且通常都是有害的 应当尽量避免
线程安全的实现方法
1.互斥同步
互斥同步(Mutual Exclusion&Synchronization)是常见的一种并发正确性保障手段 同步是指在多个线程并发访问共享数据时 保证共享数据在同一个时刻只被一个(或者是一些 使用信号量的时候)线程使用 而互斥是实现同步的一种手段 临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式 因此 在这4个字里面 互斥是因 同步是果 互斥是方法 同步是目的
2.非阻塞同步
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题 因此这种同步也称为阻塞同步(Blocking Synchronization) 从处理问题的方式上说 互斥同步属于一种悲观的并发策略 总是认为只要不去做正确的同步措施(例如加锁) 那就肯定会出现问题 无论共享数据是否真的会出现竞争 它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁) 用户态核心态转换 维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作 随着硬件指令集的发展 我们有了另外一个选择:基于冲突检测的乐观并发策略 通俗地说 就是先进行操作 如果没有其他线程争用共享数据 那操作就成功了;如果共享数据有争用 产生了冲突 那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试 直到成功为止) 这种乐观的并发策略的许多实现都不需要把线程挂起 因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)
3.无同步方案
要保证线程安全 并不是一定就要进行同步 两者没有因果关系 同步只是保证共享数据争用时的正确性的手段 如果一个方法本来就不涉及共享数据 那它自然就无须任何同步措施去保证正确性 因此会有一些代码天生就是线程安全的
可重入代码(Reentrant Code):这种代码也叫做纯代码(Pure Code) 可以在代码执行的任何时刻中断它 转而去执行另外一段代码(包括递归调用它本身) 而在控制权返回后 原来的程序不会出现任何错误 相对线程安全来说 可重入性是更基本的特性 它可以保证线程安全 即所有的可重入的代码都是线程安全的 但是并非所有的线程安全的代码都是可重入的
线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享 那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证 我们就可以把共享数据的可见范围限制在同一个线程之内 这样 无须同步也能保证线程之间不出现数据争用的问题
锁优化
自旋锁与自适应自旋
前面我们讨论互斥同步的时候 提到了互斥同步对性能最大的影响是阻塞的实现 挂起线程和恢复线程的操作都需要转入内核态中完成 这些操作给系统的并发性能带来了很大的压力 同时 虚拟机的开发团队也注意到在许多应用上 共享数据的锁定状态只会持续很短的一段时间 为了这段时间去挂起和恢复线程并不值得 如果物理机器有一个以上的处理器 能让两个或以上的线程同时并行执行 我们就可以让后面请求锁的那个线程“稍等一下” 但不放弃处理器的执行时间 看看持有锁的线程是否很快就会释放锁 为了让线程等待 我们只需让线程执行一个忙循环(自旋) 这项技术就是所谓的自旋锁
在JDK 1.6中引入了自适应的自旋锁 自适应意味着自旋的时间不再固定了 而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定 如果在同一个锁对象上 自旋等待刚刚成功获得过锁 并且持有锁的线程正在运行中 那么虚拟机就会认为这次自旋也很有可能再次成功 进而它将允许自旋等待持续相对更长的时间 比如100个循环 另外 如果对于某个锁 自旋很少成功获得过 那在以后要获取这个锁时将可能省略掉自旋过程 以避免浪费处理器资源 有了自适应自旋 随着程序运行和性能监控信息的不断完善 虚拟机对程序锁的状况预测就会越来越准确 虚拟机就会变得越来越“聪明”了
锁消除
锁消除是指虚拟机即时编译器在运行时 对一些代码上要求同步 但是被检测到不可能存在共享数据竞争的锁进行消除 锁消除的主要判定依据来源于逃逸分析的数据支持 如果判断在一段代码中 堆上的所有数据都不会逃逸出去从而被其他线程访问到 那就可以把它们当做栈上数据对待 认为它们是线程私有的 同步加锁自然就无须进行
锁粗化
原则上 我们在编写代码的时候 总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步 这样是为了使得需要同步的操作数量尽可能变小 如果存在锁竞争 那等待锁的线程也能尽快拿到锁
大部分情况下 上面的原则都是正确的 但是如果一系列的连续操作都对同一个对象反复加锁和解锁 甚至加锁操作是出现在循环体中的 那即使没有线程竞争 频繁地进行互斥同步操作也会导致不必要的性能损耗
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁 将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
轻量级锁
轻量级锁是JDK 1.6之中加入的新型锁机制 它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的 因此传统的锁机制就称为“重量级”锁 首先需要强调一点的是 轻量级锁并不是用来代替重量级锁的 它的本意是在没有多线程竞争的前提下 减少传统的重量级锁使用操作系统互斥量产生的性能消耗
偏向锁
偏向锁也是JDK 1.6中引入的一项锁优化 它的目的是消除数据在无竞争情况下的同步原语 进一步提高程序的运行性能 如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量 那偏向锁就是在无竞争的情况下把整个同步都消除掉 连CAS操作都不做了
偏向锁的“偏” 就是偏心的“偏” 偏袒的“偏” 它的意思是这个锁会偏向于第一个获得它的线程 如果在接下来的执行过程中 该锁没有被其他的线程获取 则持有偏向锁的线程将永远不需要再进行同步