线程安全-读笔记

1. 概述

线程安全定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调动作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。摘至《Java并发编程实践》
编写的线程安全的代码,本质上就是管理对状态(state)的访问,而且通常都是共享的、可变的状态。共享指一个变量可以被多个线程访问;可变变量的值在生命周期内可以改变。
无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。
保证对象线程安全的三种措施:

  • 不跨线程共享变量
  • 使状态变量不可变
  • 在任何访问状态变量的时候使用同步

2. Java线程安全

2.1 线程安全强弱等级

Java语言中操作各种共享的数据可根据安全程度分为以下5类:

2.1.1 不可变

在Java线程里面,不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何的线程安全保障措施。

Java语言中,如果共享数据是一个基本数据类型,只要在定义时使用final关键字修饰它就可以保证它是不可变的;如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行。只要一个不可变的对象被正确构建出来(没有发生this引用逃逸的情况),那么其外部的可见状态也不会改变。保证对象的行为不影响自己状态的途径有很多种,最简单的就是把对象中带有状态的变量都声明为final。

2.1.2 绝对线程安全

不管运行时环境如何,调用者都不需要任何额外的同步措施。

2.1.3 相对线程安全

对象单独的操作时线程安全的,我们在调用的时候不需要做额外的保障措施,对于特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。例如:Vector、ConcurrentHashMap等。

2.1.4 线程兼容

线程兼容是指对象本身不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中安全地使用。例如:HashMap等。

2.1.5 线程对立

线程对立指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。例如:Thread类中的suspend(),resume()方法。

2.2 Java线程安全实现

2.2.1 互斥同步

互斥同步(Mutual Exclusion & Synchronization)是最常见的一种并发正确性保障手段,同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条线程使用。是一种悲观的并发策略,无论是否并发都需要加锁。
手段:synchronized、ReentrantLock。
缺点:线程阻塞和唤醒带来的性能问题,因此互斥同步也称为阻塞同步(Blocking Synchronization)。

2.2.2 非阻塞同步

非阻塞同步(Non-Blocking Synchronization)是基于冲突检测的乐观并发策略,通俗的讲就是先进行操作,如果没有其他线程争用共享数据,则操作成功,否则产生冲突,然后进行补偿措施(最常见的就是不断的重试,知道试成功为止)。
要求:硬件指令集的发展,需要操作和检测两个步骤具备原子性。
常见指令:测试并设置(Test-and-Set)、获取并增加(Fetch-and-Increment)、交换(Swap)、比较并交换(Compare-and-Swap,CAS)、加载链接/条件存储(Load-linked/Store-Conditional,LL/SC)。

2.2.3 无同步方案

要保证线程同步,不一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时正确性的手段,如果有些代码不涉及共享数据,自然无需同步保证共享数据争用时的正确性。

  • 可重入代码(Reentrant Code):这种代码也叫纯代码(Pure Code),可以在代码执行的任何时刻中断它,转去执行另外一段代码,而控制权回来后继续执行代码,程序不会出现任何错误。
  • 线程本地存储(Thread Local Storage):如果一段代码所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行,如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,无需同步也能实现线程之间不出现数据争用的问题。

参考资料

深入理解Java虚拟机-JVM高级特性与实践
Java并发编程实践
Java并发编程艺术