一、什么是线程安全
线程安全:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,即这个对象是线程安全的。
Java中的线程安全,就是多个线程之间访问共享数据时,能够正确访问。将Java中访问共享数据的操作由线程安全的“安全程度”由强至弱分为:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
(1)不可变:不可变对象一定是线程安全的,如final关键字修饰的基本数据类型,都是不可变的;还有String,Enum、Long,Double、AtomicInteger等也都是不可变等数据类型
(2)绝对线程安全:不管运行时环境如何,调用者都不需要额外的同步措施。在Java API中标注为线程安全的类,大多都不是绝对线程安全。
(3)相对线程安全:通常意义上的线程安全,保证这个对象单独的操作是线程安全的,不需要额外的保障措施,对于一些特定顺序的联系调用,可能需要使用额外的同步手段来保证调用的正确性。
(4)线程兼容:通常意义上的线程不安全,即对象本身并不是线程安全的,但是可以通过调用端正确地使用同步手段保证对象在并发环境中的安全使用。
(5)线程对立:无论调用端是否采取同步措施,都无法在多线程环境中并发使用的代码。Java天生具备多线程特性,因此很少出现,也应尽量避免。其中,Thread类的suspend()和resume()方法就是一个线程对立的。
二、如何实现线程安全
1、互斥同步:是指在多线程并发访问共享数据时,保证共享数据在用一个时刻只被一个(或一些,使用信号量的时候)线程使用。常见的包括:临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)、事件(Event)。其中互斥是因,同步是果,互斥是方法,同步是目的。在Java中,最基本的互斥同步手段就是synchronized关键字和ReentrantLock对象。
synchronized表现为原生语法层面的互斥锁,编译后在同步块前后分别形成monitorenter和monitorexit两个字节码指令,它们都需要一个reference类型的参数来指明药品锁定和解锁的对象(未指定就是对象实例和Class对象),因为Java的线程是映射到操作系统的原生线程之上,如果要阻塞和唤醒一个线程,都需要操作系统来完成,就需要从用户态转换到核心态,即消耗更多的时间和性能。因此synchronized在Java中是一个重量级操作(重量级锁)。
ReentrantLock表现为API层面的互斥锁,通过lock()和unlcok()方法配合try/finally语句块来完成,相比于synchronized,ReentrantLock增加了一些高级功能:等待可中断(放弃等待),可实现公平锁(时间顺序),锁定条件(多个Condition对象)。
2、非阻塞同步:即基于冲突检测的乐观并发策略,就是先进行操作,如果没有竞争,那么操作就成功了,若产生冲突,再采取补偿措施(常见的补偿措施就是不断重试,直到成功)。其中需要保证操作和冲突检测具备原子性,因此需要硬件指令集的发展来完成,这类指令集常用的有:测试并设置(Test-and-Set)、获取并增加(Fetch-and-Increment)、交换(Swap)、比较并交换(Compare-and-Swap)和加载链接/条件存储(Load-Linked/Store-Conditional)。CAS也存在“ABA”问题,可通过带有标记的原子引用类“AtomicStampedReference”解决。
3、无同步方案:同步只是保证共享数据争用时的正确性手段,如果一个方法本身不涉及共享数据,就无需任何同步措施。如:可重入代码和线程本地存储。
可重入代码:不依赖存储在堆上的数据和公用的系统资源、用到的状态量都是由参数传入、不调用非可用重入的方法等,即纯代码(Pure Code)。
线程本地存储:如果共享数据的代码在同一线程执行,也无须同步能保证线程之间不出现数据争用的问题。
三、锁优化
为实现线程安全,需要通过同步互斥锁机制实现,而常用的锁机制性能较差,因此从JDK1.6后进行了优化,提出了:适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。
自适应自旋:互斥同步最大的性能问题在于阻塞,线程的阻塞与唤醒需要内核态参与。因此,加入自旋(忙循环),即让线程不挂起,而执行一段无操作循环,等待有锁线程释放锁。若锁被占用时间很短,自旋效果很好,相反若占用时间很长,则反而带来性能浪费。则利用自适应自旋,通过前一次在同一锁上的自旋时间接锁的拥有者状态决定自旋时间。
锁消除:是指虚拟机(JVM)即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判断依据来源于逃逸分析的数据支持。
锁粗化:原则上需要将同步块的作用方位限制得尽量小——只在共享数据的实际作用域进行同步。但如果一系列连续操作都对同一个对象反复加锁和解锁,甚至加锁出现在循环体中,频繁进行互斥同步操作会导致不必要的性能消耗。因此,可以将加锁同步的范围扩展(粗化)到整个操作序列外部。
轻量级锁:相对于传统锁机制而言,其本意是在没有多线程竞争的前提下,减少传统的重量级锁使用系统互斥量产生的性能消耗。在HotSpot虚拟机中,对象(对象头)内存布局分为两个部分:存储对象自身运行时数据(Mark Word)与存储指向方法区对象类型数据的指针(Class Metadata Address)。由于对象头信息是与对象自身定义的数据无关的额外存储成本,因此Mark Word被设计为非固定的数据结构,以便存储更多的有效数据。
(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(Displaced Mark Word)。
(2)拷贝对象头中的Mark Word复制到锁记录(Lock Record)中,拷贝成功后,虚拟机将使用CAS操作尝试将锁对象的Mark Word更新为指向Lock Record的指针,并将线程栈帧中的Lock Record里的owner指针指向Object的 Mark Word。
(3)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态;如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
偏向锁:目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。当锁对象第一次被线程获取的时候,线程使用CAS操作把这个线程的ID记录在对象Mark Word之中,同时置偏向标志位1。以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的ID。如果测试成功,表示线程已经获得了锁。当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定或轻量级锁定状态。