线程安全: 当多线程访问一个对象时, 如果不用考虑这些线程在运行时环境下的调度和交替执行, 也不需要额外的同步, 或者在调用方进行任何其他的协调操作, 调用这个对象的行为都可以获得正确的结果, 那么这个对象就是线程安全的.
Java 中, 线程安全体现在多个线程访问同一个共享数据, 如果一段代码中根本不会与其他线程共享数据, 可以说不存在线程安全的问题.
线程安全的安全程度, 由强至弱排序, 可以分为以下 5 类.
不可变
不可变的对象一定是线程安全的, final 关键字可以实现可见性. Java 中如果共享数据时基本数据类型, 加上 final 关键字就能保证它不可变, 如果是对象, 只需保证它是个不可变对象就行, 比如 String 对象, 对其的任何操作都不会改变原来的值, 而是返回一个新的字符串对象.
绝对线程安全
满足一开始的线程安全定义, 即: 当多线程访问一个对象时, 如果不用考虑这些线程在运行时环境下的调度和交替执行, 也不需要额外的同步, 或者在调用方进行任何其他的协调操作, 调用这个对象的行为都可以获得正确的结果, 那么这个对象就是线程安全的.
相对线程安全
通常意义上的线程安全, 需要保证这个对象单独操作是线程安全的, 在调用时不需要做额外的保障措施, 但是对于一些特定顺序的连续调用, 就可能使用额外的同步手段. Java API 中大多数线程安全类都是这种类型, 比如 Vector,HashTable 等.
线程兼容
指对象本身不是线程安全, 但是可以通过使用同步手段保证在并发环境下安全使用. 通常说的线程不安全就是指这个类型, Java 中大多数类都是, 比如 ArrayList,HashMap 等.
线程对立
无论是否采取了同步措施, 都无法在多线程环境中并发安全使用. 在 Java 中很少见.
线程安全的实现方法
互斥同步
同步指在多线程并发访问共享数据时, 保证共享的那个数据在同一时刻只能被一个线程使用.. 在 Java 中使用 synchronized 或者 concurrent 包中的重入锁可以实现同步. 后者相比前者, 有一些更高级的功能:
等待可中断. 当持有锁的线程长时间不释放锁, 正在等待的线程可以不在放弃等待, 从而可以处理其他任务.
公平锁. 多个线程等待同一个锁, 按照先来后到的顺序依次得到锁. 而不是任意一个线程都有机会在这一次能得到锁. synchronized 中的锁时非公平的, 重入锁 ReentrantLock 默认非公平, 不过可以指定参数设置为公平锁.
锁绑定多个条件. 一个 ReentrantLock 可以和多个 Condition 对象绑定.
性能上, 两者差不多.
非阻塞同步
同步会进行线程阻塞, 属于悲观策略, 即无论是否真的共享数据竞争, 都要加锁. 而 CAS 操作属于乐观策略, 先进行操作, 如果没有共享数据竞争, 就操作成功; 否则产生冲突, 那么就不断重试, 直到成功为止.
CAS 操作有 3 个操作数, 分别是内存位置 V(Java 中可简单理解为变量的内存地址), 旧的预期值 A, 和新值 B, 当且仅当 V 符合旧的预期值 A 时, 才会有心智 B 更新 V 的值, 否则不更新, 说明这个变量已经被别的线程修改过了.
CAS 有个问题: 如果变量 V 一开始被读取到是 A 值, 中途被修改成 B, 最后又被修改回 A,CAS 操作会误认为变量 V 没有被改变过, 这称为 CAS 操作的 "ABA 问题".
无同步
如果不涉及共享数据, 就无需进行同步. 可重入代码: 可以在代码执行的任何时刻中断, 转而去执行其他代码, 在控制权返回后, 原来的程序不会出现任何错误. 可重入代码都是线程安全的.
锁优化
自旋锁
如果共享数据的锁定状态只有很短的一段时间, 为了这段时间去挂起和恢复线程 (都需要转入内核态) 并不值得, 所以此时让后面请求锁的那个线程稍微等待以下, 但不放弃处理器的执行时间. 这里的等待其实就是执行了一个忙循环, 这就是所谓的自旋.
自旋等待有一定的限度, 默认值是 10 次, 如果超过这个次数, 就会使用传统方式挂起线程. JDK1.6 中引入了自适应的自旋锁. 如果在同一个对象上, 自旋等待刚刚成功获得了锁, 且持有的锁正在运行中, 虚拟机就认为这次自旋也会成功, 进而它被允许有更长时间的自旋等待; 相反, 如果对于某个锁, 自旋很少成功过, 那在之后获取这个锁时很可能省略掉自旋过程.
锁消除
虚拟机即时编译时, 对一些代码上要求同步, 但被检测到不可能存在共享数据竞争的锁进行消除. 锁消除的依据来源于 "逃逸分析" 技术. 堆上的所有数据都不会逃逸出去被其他线程访问到, 就可以把它们当栈上的数据对待, 认为它们是线程私有的, 同步加锁就是没有必要的.
锁粗化
如果连续对某个对象反复枷锁或解锁, 甚至加锁操作出现在循环中. 虚拟机探测到这种情况, 会把锁粗化到操作的外部. 举个例子
- public static int j = 0;
- for (int i = 0; i < 100; i++) {
- synchronized (this) {
- j++;
- }
- }
- // 锁粗化
- synchronized (this) {
- for (int i = 0; i < 100; i++) {
- j++;
- }
- }
轻量级锁
传统的锁机制称为 "重量级" 锁, 轻量级锁用于在没有多线程竞争的前提下, 减少传统的重量级锁使用操作系统互斥量产生的性能消耗.
轻量级锁的加锁和解锁都是通过 CAS 操作完成的, 如果有两个以上的线程争用同一个锁, 轻量级锁将膨胀为重量级锁.
轻量级锁能提升同步性能主要因为: 对于大多数锁, 在整个同步周期内都是不存在竞争的.
偏向锁
轻量级锁噪无竞争的情况下使用 CAS 操作消除同步使用的互斥量, 偏向锁是在无竞争的情况下吧整个同步都消除掉, CAS 操作也没有了.
偏向锁会偏向第一个获得它的线程, 如果在接下来的执行过程中, 该锁没有被其他线程获取, 则持有偏向锁的线程永远也不需要再进行同步.
- by @sunhaiyu
- 2018.6.20
来源: http://www.bubuko.com/infodetail-2651782.html