线程安全
按照线程安全的“安全程度”由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
- **不变性。**不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障措施。如:Java中final关键字修饰的变量。
- **绝对线程安全。**绝对线程安全要求“不管运行时环境如何,调用者都不需要任何额外开销的同步措施”。即使Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。
- **相对线程安全。**保证对象单次的操作是线程安全的。
- **线程兼容。**指对象本身并不是线程安全的,但是可以通过调用端正确地同步手段来保证对象再并发环境中可以安全的使用。
- **线程对立。**不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。
线程安全的实现方法
-
互斥同步。互斥同步是一种最常见也是最主要的并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一条线程使用,而互斥时实现同步的一种手段,临界区、互斥量、信号量都是最常见的互斥方式。
在Java中,最基本的互斥同步手段就是synchronized关键字,这是一种块结构的同步语法。(由于Java线程对于线程的操作是用操作系统内核实现的,所以需要频繁地在用户态和内核态之间进行转换)
- 被synchronized修饰的同步块**对同一条线程来说时可重入的。**这意味者同一线程反复进入同步块也不会出现自己把自己锁死的情况。
- 被synchronized修饰的同步块在持有锁的线程释放之前,会无条件地阻塞后面其他线程的进入。
重入锁(ReentrantLock)与synchronized相比增加了一些高级功能(等待可中断、可实现公平锁以及绑定多个条件)。
-
**非阻塞同步。**顾名思义,不阻塞线程(省去内核态和用户态之间的频繁转换)。比如基于冲突检测的乐观并发策略,通俗的说就是不管风险,先进行操作,如果没有其他线程争用,那就操作成功;如果共享数据发生冲突则进行其他的补偿措施(最常用的措施是不断重试,直到出现没有竞争的共享数据为止)。该操作需要保证操作与冲突检测者两个步骤具有原子性。该操作使用硬件来实现,常用的指令有:
- 测试并设置
- 获取并增加
- 交换
- 比较并交换(CAS)。CAS有三个操作数V:内存地址,A:旧的预期值,B:准备设置的新值/
- 加载链接/条件存储
如:AtomicInteger等原子基本包装类。
-
无同步方案。
- 可重入代码:这种代码又称纯代码,是指在代码执行的任何时刻中断它,转而去执行另一段代码,原来的程序都不会出现任何错误,也不会对结果有任何影响
- 线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,拿就看看这些共享数据是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无需同步也能保证线程之间不出现数据争用的问题(可以理解为session,session存储在服务器中,但每个session对应一个用户,可以理解为服务端中存放的一个副本)。