线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的
java语言中各种操作共享的数据分为以下5类:
- 不可变
一定是线程安全的。例如final,被final修饰的基础数据类型,被正确构造后是不可变的,亦为线程安全的。如果是一个对象,需要保证它的行为不会对状态产生印象,例如String、Integer、Long、Double等,内部值是final的,所以不可变。 - 绝对线程安全
一个类要想达到绝对线程安全,需要做到无论什么时候,都不需要额外的同步措施。这需要很大的代价。大多Java API标记的线程安全类,都不是绝对线程安全。比如Vector,它的get、add、size等方法都是被synchronized修饰的,单独调用时是安全的,但以一些特殊顺序调用一系列的方法时,就不安全了。 - 相对线程安全
相对线程安全其实就是我们一般意义上的线程安全,它需要保证对这个对象的单独操作是安全的。但是对于特定的顺序,需要一些方法保证线程安全。例如Vector、HashTable等。 - 线程兼容
线程兼容是指对象本身并不是线程安全的,单可以通过在调用时正确使用同步手段来保证对象在并发环境中可以安全使用,我们平时说一个类不是线程安全的,通常是指这种情况。Java API中大部分类都属于线程兼容,如ArrayList、HashMap等。 - 线程对立
线程对立指无论调用断是否采取了同步措施,都无法在多线程环境中并发使用的对象。如Thread类的suspend和resume方法,会产生死锁。
线程安全的实现方法:
- 互斥同步
保证共享数据在同一个时刻制备一个线程使用。互斥是因,同步是果;互斥是方法,同步时目的。
在java中,最基本的互斥同步手段就是synchronized关键字,或者用java.util.concurrent包中的ReentrantLock(重入锁)。 - 非阻塞同步
互斥同步因为会阻塞线程,称为阻塞同步,开销很大。这是一种悲观的并发策略,认为不同步就会出问题。随着硬件发展,出现了非阻塞同步的方法,这是一种乐观的策略。它采用先行模式,就是先进行数据操作,如果没有竞争,那操作就成功了。如果发生冲突,再补救,通常是循环重试。这就避免了线程的挂起。 - 无同步方案
要保证线程安全,并不是一定要进行同步,同步只是保证共享数据争用时的正确性的手段。如果一个方法本来就不涉及共享数据,自然就无须任何同步措施。
锁优化:
- 自旋锁与自适应自旋
线程的挂起和恢复代价很大,自旋锁让竞争锁的线程自循环等待一会,看能不能很快获得锁。省去了线程切换代价,但是自循环白白消耗cpu,所以它会自适应,收集数据判断等待是否值得,如果预测值得就会加大自旋次数,如果预测不值得,可能会直接挂起。 - 锁消除
利用逃逸分析的结果,去掉不必要的锁。 - 锁粗化
如果小范围内频繁对一个对象加锁、释放锁,开销很大,会将锁范围扩大,用更少量的锁代替,减少开销。 - 轻量级锁
使用对象头的Mark Word中锁标志位代替操作系统互斥量实现的锁。轻量级锁并不是用来代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。轻量级锁是在无竞争的情况下使用CAS(Compare-and-Swap)操作去消除同步使用的互斥量。 - 偏向锁
和轻量级锁原理基本一致,但偏向锁在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。