java乐观锁和悲观锁

原文链接

乐观锁和悲观锁

乐观锁和悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。

乐观锁:对于并发操作产生的线程安全问题持乐观态度,认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则执行其他操作,如报错或自动重试。乐观锁最常用的是CAS算法,java java.util.concurrent包下的原子类(例如AtomicBooleanAtomicIntegerAtomicLong)中的递增操作通过CAS自旋来实现。

悲观锁:它是对于并发操作时产生的线程安全持悲观心态,悲观锁认为竞争总会发生,因此每次对于某些进行操作时,都会持有一个独占的锁,像synchronied和lock的实现类都是悲观锁。

 

适用场景:
悲观锁适用于写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁可以使读操作的性能大幅提升。

什么是CAS?


CAS:比较与交换 ,是一种无锁算法。在不适用锁(没有线程阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包的原子类就是通过cas来实现乐观锁。

CAS算法涉及到三个操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

看个例子:

1.在内存地址V当中,存储着值为10的变量。

2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

4、线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。

5、线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

6、这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。线程1可以进行SWAP,把地址V的值替换为B,也就是12。

 

前面我们说到java java.util.concurrent包下的原子类的自增操作是利用了CAS思想。我们查看AtomicInteger的自增函数incrementAndGet(),发现自增函数底层调用的是unsafe.getAndAddInt()。进一步查看unsafe.getAndAddInt() 的实现。

从上图中的源码中,我们可以看到getAndAddInt()循环获取给定对象O中的偏移量处的值v,然后判断内存值是否等于v, 如果相等则将内存值设置为v+delta,否则返回false,继续循环进行充实,知道设置成功才能推出循环,并且将旧值返回。整个“比较和更新”操作封装在compareAndSwapInt(),在JNI里借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。
 

CAS的缺点:

1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证多个的原子性。
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用独占锁了。

3、 ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。

一个使用AtomicInteger的例子:


 
 
  1. import java.util.concurrent.atomic.AtomicInteger;
  2. class Untitled {
  3. private static AtomicInteger count = new AtomicInteger( 0);
  4. public static void main(String[] args) {
  5. for ( int i = 0; i < 2; i++) {
  6. new Thread( new Runnable() {
  7. @Override
  8. public void run() {
  9. try {
  10. Thread.sleep( 10);
  11. } catch (Exception e) {
  12. e.printStackTrace();
  13. }
  14. //每个线程让count自增100次
  15. for ( int i = 0; i < 100; i++) {
  16. count.incrementAndGet();
  17. }
  18. }
  19. }).start();
  20. }
  21. try{
  22. Thread.sleep( 2000);
  23. } catch (Exception e){
  24. e.printStackTrace();
  25. }
  26. System.out.println(count);
  27. }
  28. }

换成Synchronized:


 
 
  1. class Untitled {
  2. private static int count = 0;
  3. public static void main( String[] args) {
  4. for ( int i = 0; i < 2; i++) {
  5. new Thread( new Runnable() {
  6. @Override
  7. public void run() {
  8. try {
  9. Thread.sleep( 10);
  10. } catch ( Exception e) {
  11. e.printStackTrace();
  12. }
  13. //每个线程让count自增100次
  14. for ( int i = 0; i < 100; i++) {
  15. synchronized (Untitled. class){
  16. count++;
  17. }
  18. }
  19. }
  20. }).start();
  21. }
  22. try{
  23. Thread.sleep( 2000);
  24. } catch ( Exception e){
  25. e.printStackTrace();
  26. }
  27. System.out.println(count);
  28. }
  29. }

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值