线程安全
线程安全:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下 的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对 象的行为都可以获得正确的结果,那就称这个对象是线程安全的。
操作共享的数据:
- 不可变:对象一定是线程安全的,final修饰的基本数据类型
-
绝对线程安全:对象的方法被synchronized修饰,不一定代表对象线程安全,还是需要在调用端做同步处理
-
相对线程安全:保证对对象的单次操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需 要在调用端使用额外的同步手段来保证调用的正确性。
在Java语言中,大部分声称线程安全的类都属于这种类型,例如Vector、HashTable、Collections的 synchronizedCollection()方法包装的集合等。
-
线程兼容:线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对 象在并发环境中可以安全地使用。我们平常说一个类不是线程安全的,通常就是指这种情况。Java类 库API中大部分的类都是线程兼容的,如与前面的Vector和HashTable相对应的集合类ArrayList和 HashMap等。
-
线程对立:线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。由于Java 语言天生就支持多线程的特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害 的,应当尽量避免。 一个线程对立的例子是Thread类的suspend()和resume()方法。如果有两个线程同时持有一个线程对 象,一个尝试去中断线程,一个尝试去恢复线程,在并发进行的情况下,无论调用时是否进行了同 步,目标线程都存在死锁风险——假如suspend()中断的线程就是即将要执行resume()的那个线程,那就 肯定要产生死锁了。也正是这个原因,suspend()和resume()方法都已经被声明废弃了。常见的线程对立 的操作还有System.setIn()、Sytem.setOut()和System.runFinalizersOnExit()等。
线程安全的实现方法
-
互斥同步:最常见也是最主要的并发正确性保障手 段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些, 当使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量 (Mutex)和信号量(Semaphore)都是常见的互斥实现方式。互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的性能开销,因此这种同步也被称为阻塞 同步,悲观锁
- 在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。synchronized关键字经过Javac编译之后,会在同步块的前后分别形成 monitorenter和monitorexit这两个字节码指令这两个字节码指令都需要一个reference类型的参数来指明 要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作 为reference;如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来 决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。
- 根据《Java虚拟机规范》的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁。如果 这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行 monitorexit指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象 锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。
- 被synchronized修饰的同步块对同一条线程来说是可重入的。这意味着同一线程反复进入同步块 也不会出现自己把自己锁死的情况。
- 笔者仍然推荐在synchronized与ReentrantLock都可满足需要时优先使用synchronized:
- synchronized是在Java语法层面的同步,足够清晰,也足够简单。每个Java程序员都熟悉 synchronized,但J.U.C中的Lock接口则并非如此。因此在只需要基础的同步功能时,更推荐 synchronized。 ·Lock应该确保在finally块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不 会释放持有的锁。这一点必须由程序员自己来保证,而使用synchronized的话则可以由Java虚拟机来确 保即使出现异常,锁也能被自动释放。
-
非阻塞同步:乐观锁,不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数 据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试,直到出现 没有竞争的共享数据为止,也常被称为无锁编程
- 使用CAS避免阻塞同步
CAS
- CAS指令需要有三个操作数,分别是内存位置(在Java中可以简单地理解为变量的内存地址,用V 表示)、旧的预期值(用A表示)和准备设置的新值(用B表示)
- CAS指令执行时,当且仅当V符合 A时,处理器才会用B更新V的值,否则它就不执行更新。但是,不管是否更新了V的值,都会返回V的 旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断。
- CAS从语义上来说并不是真正完美的,它存在一个逻辑漏洞:如果一个变量V初次读取的时候是A 值,并且在准备赋值的时候检查到它仍然为A值,那就能说明它的值没有被其他线程改变过了吗?这 是不能的,因为如果在这段期间它的值曾经被改成B,后来又被改回为A,那CAS操作就会误认为它从 来没有被改变过。这个漏洞称为CAS操作的“ABA问题”。J.U.C包为了解决这个问题,提供了一个带有 标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性
-
无同步方案
- 同步只是保障存在共享数据争用时正确性的手段,如果能让一个方法本来就不涉及共享数据,那它自 然就不需要任何同步措施去保证其正确性,因此会有一些代码天生就是线程安全的,
锁优化
如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁膨胀(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)等,这些技术都是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的执行 效率
自旋锁
如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,我们 就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很 快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自 旋锁。
自旋等待的时间必须有一定的限 度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。自旋次数 的默认值是十次,用户也可以使用参数-XX:PreBlockSpin来自行更改。
自适应锁
自旋成功越多,则允许自旋等待持续相对更长的时间,如果很少成功获得锁,则可能直接忽略掉自选过程,以避免浪费处理器资源
锁消除
对一些代码要求同步,但是对被检测到不可能存在共享 数据竞争的锁进行消除
如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可 以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。
锁粗化
如果虚拟机探测到有这样一串零碎的操作 都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,以代码清单13-7 为例,就是扩展到第一个append()操作之前直至最后一个append()操作之后,这样只需要加锁一次就可 以了。
轻量级锁
轻量级锁是JDK 6时加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实 现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。不过,需要强调一点,轻量级锁并不是 用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系 统互斥量产生的性能消耗。
线程安全
线程安全:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下 的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对 象的行为都可以获得正确的结果,那就称这个对象是线程安全的。
操作共享的数据:
- 不可变:对象一定是线程安全的,final修饰的基本数据类型
-
绝对线程安全:对象的方法被synchronized修饰,不一定代表对象线程安全,还是需要在调用端做同步处理
-
相对线程安全:保证对对象的单次操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需 要在调用端使用额外的同步手段来保证调用的正确性。
在Java语言中,大部分声称线程安全的类都属于这种类型,例如Vector、HashTable、Collections的 synchronizedCollection()方法包装的集合等。
-
线程兼容:线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对 象在并发环境中可以安全地使用。我们平常说一个类不是线程安全的,通常就是指这种情况。Java类 库API中大部分的类都是线程兼容的,如与前面的Vector和HashTable相对应的集合类ArrayList和 HashMap等。
-
线程对立:线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。由于Java 语言天生就支持多线程的特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害 的,应当尽量避免。 一个线程对立的例子是Thread类的suspend()和resume()方法。如果有两个线程同时持有一个线程对 象,一个尝试去中断线程,一个尝试去恢复线程,在并发进行的情况下,无论调用时是否进行了同 步,目标线程都存在死锁风险——假如suspend()中断的线程就是即将要执行resume()的那个线程,那就 肯定要产生死锁了。也正是这个原因,suspend()和resume()方法都已经被声明废弃了。常见的线程对立 的操作还有System.setIn()、Sytem.setOut()和System.runFinalizersOnExit()等。
线程安全的实现方法
-
互斥同步:最常见也是最主要的并发正确性保障手 段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些, 当使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量 (Mutex)和信号量(Semaphore)都是常见的互斥实现方式。互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的性能开销,因此这种同步也被称为阻塞 同步,悲观锁
- 在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。synchronized关键字经过Javac编译之后,会在同步块的前后分别形成 monitorenter和monitorexit这两个字节码指令这两个字节码指令都需要一个reference类型的参数来指明 要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作 为reference;如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来 决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。
- 根据《Java虚拟机规范》的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁。如果 这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行 monitorexit指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象 锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。
- 被synchronized修饰的同步块对同一条线程来说是可重入的。这意味着同一线程反复进入同步块 也不会出现自己把自己锁死的情况。
- 笔者仍然推荐在synchronized与ReentrantLock都可满足需要时优先使用synchronized:
- synchronized是在Java语法层面的同步,足够清晰,也足够简单。每个Java程序员都熟悉 synchronized,但J.U.C中的Lock接口则并非如此。因此在只需要基础的同步功能时,更推荐 synchronized。 ·Lock应该确保在finally块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不 会释放持有的锁。这一点必须由程序员自己来保证,而使用synchronized的话则可以由Java虚拟机来确 保即使出现异常,锁也能被自动释放。
-
非阻塞同步:乐观锁,不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数 据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试,直到出现 没有竞争的共享数据为止,也常被称为无锁编程
- 使用CAS避免阻塞同步
CAS
- CAS指令需要有三个操作数,分别是内存位置(在Java中可以简单地理解为变量的内存地址,用V 表示)、旧的预期值(用A表示)和准备设置的新值(用B表示)
- CAS指令执行时,当且仅当V符合 A时,处理器才会用B更新V的值,否则它就不执行更新。但是,不管是否更新了V的值,都会返回V的 旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断。
- CAS从语义上来说并不是真正完美的,它存在一个逻辑漏洞:如果一个变量V初次读取的时候是A 值,并且在准备赋值的时候检查到它仍然为A值,那就能说明它的值没有被其他线程改变过了吗?这 是不能的,因为如果在这段期间它的值曾经被改成B,后来又被改回为A,那CAS操作就会误认为它从 来没有被改变过。这个漏洞称为CAS操作的“ABA问题”。J.U.C包为了解决这个问题,提供了一个带有 标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性
-
无同步方案
- 同步只是保障存在共享数据争用时正确性的手段,如果能让一个方法本来就不涉及共享数据,那它自 然就不需要任何同步措施去保证其正确性,因此会有一些代码天生就是线程安全的,
锁优化
如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁膨胀(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)等,这些技术都是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的执行 效率
自旋锁
如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,我们 就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很 快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自 旋锁。
自旋等待的时间必须有一定的限 度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。自旋次数 的默认值是十次,用户也可以使用参数-XX:PreBlockSpin来自行更改。
自适应锁
自旋成功越多,则允许自旋等待持续相对更长的时间,如果很少成功获得锁,则可能直接忽略掉自选过程,以避免浪费处理器资源
锁消除
对一些代码要求同步,但是对被检测到不可能存在共享 数据竞争的锁进行消除
如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可 以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。
锁粗化
如果虚拟机探测到有这样一串零碎的操作 都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,以代码清单13-7 为例,就是扩展到第一个append()操作之前直至最后一个append()操作之后,这样只需要加锁一次就可 以了。
轻量级锁
轻量级锁是JDK 6时加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实 现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。不过,需要强调一点,轻量级锁并不是 用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系 统互斥量产生的性能消耗。
偏向锁
消除数据在无竞争情况下的同步原语, 进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互 斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。
偏向锁中的“偏”,就是偏心的“偏”、偏袒的“偏”。它的意思是这个锁会偏向于第一个获得它的线 程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需 要再进行同步。
偏向锁
消除数据在无竞争情况下的同步原语, 进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互 斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。
偏向锁中的“偏”,就是偏心的“偏”、偏袒的“偏”。它的意思是这个锁会偏向于第一个获得它的线 程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需 要再进行同步。