如果一个对象可以被多线程同时使用,那他就是线程安全的。
01、线程安全的定义
当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
02、Java语言中的线程安全
按照线程安全的安全程度由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
1、不可变
如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行。最简单的就是把对象中带有状态的变量都声明为final。
2、绝对线程安全
不管运行时环境如何,调用者都不需要任何额外的同步措施。
3、相对线程安全
需要保证对这个对象单独的操作时安全的,我们在调用的时候不需要做额外的保障措施。比如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合。
4、线程兼容
线程兼容是指对象并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证在并发环境中可以安全地使用。与前面的Vector和HashTable向对应的集合类ArrayList和HashTable等。
5、线程对立
线程独立是指无论调用端是否采用了同步措施,都无法在多线程环境中并发使用的代码。
03、线程安全的实现方法
线程安全的实现,与(1)代码编写(2)虚拟机提供的同步和锁机制都有非常重要的作用。
1、互斥同步(也叫阻塞同步)
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。临界区、互斥量和信号量都是主要的互斥实现方式。互斥是因,同步是果;互斥是方法,同步是目的。synchronized关键字、java.util.concurrent(J.U.C)包中的重入锁(ReentrantLock)。
2、非阻塞同步
互斥同步属于一种悲观锁的并发策略,随着硬件指令集的发展,现在有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施,这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
CAS指令需要3个操作数,分别是内存位置(V)、旧的预期值(A)、新值(B)。CAS指令执行时,当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新,但是无论是否更新了V的值,都会返回V的旧值。J.U.C包中提供了一个带有标记的原子引用类“AtomicStampedReferance”
3、无同步方案
同步只是保证共享数据争用的正确性手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
(1)可重入代码
(2)线程本地存储
04、锁优化
从JDK 1.5到JDK 1.6,HotSpot虚拟机开发团队花费了大量精力实现了各种适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁。
1、自旋锁与自适应自旋
互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队注意到许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能够让两个或以上的线程同时执行,我们就可以让后面请求锁的那个线程“稍等一会”,我们只需要让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
2、锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步但是被检测到不可能存在共享数据竞争的锁进行消除。
3、锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
4、轻量级锁
轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在争用的”,这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。
5、偏向锁
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要进行同步。