在JDK5之前Java语言靠的是synchronized关键字保证同步的,这会导致有锁。
锁机制又存在以下问题:
(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁会导致其他需要此锁的线程挂起。
(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。
独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其他所有需要锁的线程挂起,等待持有锁的线程释放锁。
而另一个更加有效的锁就是乐观锁。
所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
无锁的概念
在谈论无锁概念时,总会关联起乐观派与悲观派。对于乐观派而言,他们认为事情总会往好的方向发展,总是认为坏的情况发生的概率特别小,可以无所顾忌地做事。但对于悲观派而言,他们总会认为发展势态如果不及时控制,以后就无法挽回了即使无法挽回的局面几乎不可能发生。这两种派系映射到并发编程中就如同加锁和无锁的策略,即加锁是一种悲观策略,无锁是一种乐观策略。因为对于加锁的并发程序来说,它们认为每次访问共享资源时都会发生冲突,因此必须对每一次数据操作实施加锁策略。而无锁则总是假设对共享资源的访问没有冲突,线程可以不停的执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,这项CAS计数就是无锁策略的关键,下面我们进一步了解CAS技术的奇妙之处。
CAS操作
上面乐观锁用到的机制就是CAS,Compare and Swap
执行函数:CAS(V,E,N)
原理图如下:
由于CAS操作属于乐观派,他总认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出并更新成功,其余均会失败,但失败的线程并不会被挂起,而是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作,这点从图中也可以看出来。基于这样的原理,CAS操作即使没有锁,同样知道其他线程对共享资源操作影响,并执行相应的处理措施。同时从这点也可以看出,由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说无锁操作天生免疫死锁。
CAS有3个操作数,内存值V,旧的预期值E,要修改的新值N。当且仅当预期值E和内存值V相同时,将内存值V修改为N否
则什么也不做。
CAS 存在以下四个问题
(1)ABA问题。如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时发现它的值没有发生变化,但是实际上却 发生了。ABA的解决思路就是适用版本号,在变量前面增加版本号,那么ABA就变成了1A2B3A。
(2)循环时间长,开销大。
(3)只能保证一个共性变量的原子操作。也就是多个共享变量操作时,循环CAS就无法保证操作的原子性了,就可以把多个变量放在一个对象里来进行CAS操作。
(4)数据不一致问题。CAS是一种系统原语,原语属于操作系统用语范筹,是由若干条指令组成的,用于完成某个功能的一个程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
PS:
原子性
举个例子:
A想要从自己的帐户中转1000块钱到B的帐户里。那个从A开始转帐,到转帐结束的这一个过程,称之为一个事务。在这个事务里,要做如下操作:
- 1. 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。
- 2. 在B的帐户里加1000块钱。如果B的帐户如果原来有2000块钱,现在则变成3000块钱了。
如果在A的帐户已经减去了1000块钱的时候,忽然发生了意外,比如停电什么的,导致转帐事务意外终止了,而此时B的帐户里还没有增加1000块钱。那么,我们称这个操作失败了,要进行回滚。回滚就是回到事务开始之前的状态,也就是回到A的帐户还没减1000块的状态,B的帐户的原来的状态。此时A的帐户仍然有3000块,B的帐户仍然有2000块。
我们把这种要么一起成功(A帐户成功减少1000,同时B帐户成功增加1000),要么一起失败(A帐户回到原来状态,B帐户也回到原来状态)的操作叫原子性操作。
如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性。