当多个线程共享一个变量时,考虑到线程安全,要保证下面3
个要素:
- 原子性(
synchronized
、Lock
) - 可见性(
volatile
、synchronized
、Lock
) - 有序性(
volatile
、synchronized
、Lock
)
由于synchronized
和Lock
可以保证某个时刻只有一个线程执行同步代码,所以是线程安全的,但是会影响效率。
当在处理并发编程的时候,只要程序满足了原子性,可见性和有序性,就不会发生脏数据的问题。
1 有序性
有序性:程序执行的顺序按照代码的先后顺序执行。在Java
内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。可以通过volatile
关键字来保证一定的“有序性”。
2 可见性
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其它线程能够立即看得到修改的值。
当一个共享变量被volatile
修饰时,它会保证修改的值会立即被更新到内存,当有其它线程需要读取共享变量时,它会去内存中读取新值。
普通的共享变量不能保证可见性,因为普通共享变量被修改后,什么时候被写入内存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
更新内存的步骤:当前线程将其它线程的工作内存中的缓存变量的缓存行设置为无效,然后当前线程将变量的值跟新到内存,更新成功后将其它线程的缓存行更新为新的内存地址。其它线程读取变量时,发现自己的缓存行无效,它会等待缓存行对应的内存地址被更新后,然后去对应的内存读取最新的值。
3 原子性
原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。在Java
中,基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
atomic [əˈtɑːmɪk] 原子的,原子能的;微粒子的 operation [ˌɑːpəˈreɪʃn] 操作;经营;[外科] 手术;运算
相关的术语:
3.1 Java
中的原子操作
在Java
中可以通过锁和循环CAS
的方式来实现原子操作
3.1.1 使用循环CAS
实现原子操作
自旋CAS
实现的基本思路就是循环进行CAS
操作直到成功为止。
JDK 1.5
的并发包里提供了一些类来支持原子操作,如tomicBoolean
(用原子方式更新的boolean
值)、AtomicInteger
(用原子方式更新的int
值)和AtomicLong
(用原子方式更新的long
值)。这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增1
和自减1
。
CAS
虽然很高效地解决了原子操作,但是CAS
仍然存在三 大问题:
ABA
问题。 因为CAS
需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A
,变成了B
,又变成了A
,那么使用CAS
进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA
问题的解决思路就是使用版本号。在变量前面 追加上版本号,每次变量更新的时候把版本号加1
,那么A→B→A
就会变成1A→2B→3A
。从JDK 1.5
开始,Atomic
包里提供了一个类AtomicStampedReference
来解决ABA
问题。这个类的compareAndSet
方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值;- 循环时间长开销大。 自旋
CAS
如果长时间不成功,会给CPU
带来非常大的执行开销; - 只能保证一个共享变量的原子操作。 当对一个共享变量执行操作时,可以使用循环
CAS
的方式来保证原子操作,但是对多个共享变量操作时,循环CAS
就无法保证操作的原子性,这个时候就可以用锁。还有一个办法,就是把多个共享变量合并成一个共享变量。比如,有两个共享变量i = 2,j = a
,合并一下ij = 2a
,然后用CAS
来操作ij
。JDK 1.5
提供了AtomicReference
类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS
操作;
3.1.2 使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM
内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。除了偏向锁,JVM
实现锁的方式都用了循环CAS
,即当一个线程想进入同步块的时候使用循环CAS
的方式来获取锁,当它退出同步块的时候使用循环CAS
释放锁。
3.2 Atomic
类
我们经常使用的i++
操作并不是线程安全的,这时通常会使用synchroized
关键字来处理并发操作,在并发量不大的情况使用synchroized
性能并不是特别高。
在JDK 1.6
以前synchroized
是重量级锁,无论有没有资源竞争都会对变量加锁,在JDK 1.6
之后引入了偏向锁和轻量级锁,效率才有了很大的提升。
atomic
类使用了CAS
的思想,只有真正资源竞争的时候才会有资源消耗,而且Atomic
是通过底层硬件指令集实现的,所以并发量不大的情况下性能更高。
主要原理就是CAS
(比较和交换),涉及到三个值(V
、O
、 N
),V
是内存中真正的值,O
是加载到线程中的预期值,N
是计算后的目标结果值,当计算出目标结果值时比较V
和O
是否相等,不相等代表V
被其他线程改写过,那么将V
重新赋值给O
,然后重新计算目标值,再次重复上述步骤,这个称为自旋操作。
缺点:
- 存在
ABA
问题,因为每次都比较O
和V
的值,如果在比较之前V
被多次改写过,最终的值还是之前的V
,那么仅仅比较最终的V
和O
是无法知道这种情况的; - 只能针对一个共享变量进行原子操作;
- 可以看到当
V
和O
不等的时候就需要自旋操作,当并发数量很多,资源竞争激烈时,进行自旋操作等待的时间会很长,性能会大幅度降低,这时候使用其他锁会比较合适;