一、线程安全
- 当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象为线程安全
- 共同特征
- 代码本身封装了所有必要的正确性保障手段(互斥同步等),令调用者无须关心多线程下的调用问题,更无须自己实现任何措施来保障多线程环境下的正确调用
- 白话文:代码本身已经有了必要的正确性保障手段,像什么互斥同步这些,调用的时候就不需要自己实现任何操作保障多线程下正确调用
- Java 语言中的线程安全
- Java 语言中各种操作共享的数据分为以下五类
- 不可变
- 一定是线程安全的,把对象里面带有状态的变量都声明为 final
- 绝对线程安全
- 要达到不管运行时环境如何,调用者都不需要任何额外的同步措施,需要付出高昂的,甚至不切实际的代价
- Vector 时一个线程安全的容器,所有方法都是 synchronized 修饰的
- 相对线程安全
- 通常意义上的线程安全,需要保证单次操作时线程安全的,调用时候不需要额外的操作
- 线程兼容
- 对象本身不安全,调用的时候使用同步手段来保证安全
- 线程对立
- 不可变
- Java 语言中各种操作共享的数据分为以下五类
- 线程安全的实现方法
- 互斥同步
- 同步是指多个线程并发访问共享数据时,保证同一时刻只有一条线程使用,互斥时实现同步的手段
- 互斥是因,同步是果
- 最基本的就是 synchronized,编译后形成 monitorenter 和 monitorexit 两个字节码指令
- 执行前者时,锁的计数器加一,后者减一,值为零就是无锁
- 在执行时会无条件阻塞后面其他线程的进入
- 重入锁(ReentrantLock)
- 和 synchronized 一样时可重入的,多了三个高级功能
- 等待可中断:持有锁长期不释放,其他线程可以放弃等待
- 公平锁:按照申请锁的时间顺序来依次获得锁
- 非公平是任何一个等待锁的线程都有机会获得(两个都是,Lock 可以改)
- 锁绑定多个条件:ReentrantLock 可以同时绑定多个 Condition 对象
- 同步是指多个线程并发访问共享数据时,保证同一时刻只有一条线程使用,互斥时实现同步的手段
- 非阻塞同步
- 先进行操作,没有线程竞争操作就成功,有竞争会进行补偿措施(不断重试,直到没有竞争)
- 这种乐观并发策略的实现不需要把线程阻塞挂起,这就是非阻塞同步
- 无同步方案
- 有些代码天生就是线程安全
- 可重入代码:可以任意时刻中断去执行另外一段代码,返回后程序不出现错误
- 不依赖全局变量、存储在堆上的数据和公用的系统资源
- 线程本地存储:如果数据需要共享,需要看看共享数据的代码是否能保证在同一个线程中执行。
- 互斥同步
二、锁优化
- 自旋锁与自适应自旋
- 为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁
- 如果锁占用时间很短,自旋等待的效果就会非常好,反之如果锁占用的时间很长,那么自旋只会白白消耗处理器资源
- 自旋次数的默认值是十次,用户也可以使用参数 -XX:PreBlockSpin 来更改
- 自适应意味着自旋的时间不再是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定的
- 对于某个锁,自旋很少成功获得过,可以直接忽略自旋过程
- 锁消除
- 指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争对锁进行消除
- 锁粗化
- 如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列到外部
- 轻量级锁
- 设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
- 对象除了未被锁定的正常状态外,还有轻量级锁定、重量级锁定、GC 标记、可偏向等不同状态
- 如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁
- 轻量级锁通过 CAS 操作成功避免了使用互斥量的开销
- 偏向锁
- 目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能
- 偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不去做了
- 偏就是偏心的偏,这个锁会偏向第一个获得它的线程
- 一旦出现另外一个线程去尝试获取这个锁,偏向模式就马上宣告结束
- 如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的