给大伙讲讲并发编程里的悲观锁和乐观锁

简介: 什么是悲观锁?什么是乐观锁?

思维导图

文章已收录Github精选,欢迎Starhttps://github.com/yehongzhi/learningSummary

悲观锁

悲观锁是平时开发中经常用到的一种锁,比如ReentrantLocksynchronized等就是这种思想的体现,它总是假设别的线程在拿线程的时候都会修改数据,所以每次拿到数据的时候都会上锁,这样别的线程想拿这个数据就会被阻塞。如图所示:

synchronized是悲观锁的一种实现,一般我们都会有这样使用:

private static Object monitor = new Object();

public static void main(String[] args) throws Exception {
    //锁一段代码块
    synchronized (monitor){

    }
}
//锁实例方法,锁对象是this,即该类实例本身
public synchronized void doSome(){

}
//锁静态方法,锁对象是该类,即XXX.class
public synchronized static void add(){

}

我们以最简单的同步代码块来分析,其实就是将synchronized作用于一个给定的实例对象monitor,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有monitor实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行synchronized内包裹的代码块

从上面的分析中可以看出,悲观锁是独占和排他的,只要操作资源都会对资源进行加锁。假设读多写少的情况下,使用悲观锁的效果就不是很好。这时就引出了接下来要讲的乐观锁。

乐观锁

乐观锁,顾名思义它总是假设最好的情况,线程每次去拿数据时都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。如图所示:
在这里插入图片描述
一般乐观锁在java中是通过无锁编程实现的,最常见的就是CAS算法,比如Java并发包中的原子类的递增操作就是通过CAS算法实现的。

CAS算法,其实就是Compare And Swap(比较与交换)的意思。目的就是将内存的值更新为需要的值,但是有个条件,内存值必须与期待的原内存值相同。展开来说,我们有三个变量,内存值M,期望的内存值E,更新值U,只有当M==E时,才会将M更新为U

CAS算法实现的乐观锁在很多地方有应用,比如并发包的原子类AtomicInteger类。在自增的时候就使用到CAS算法。

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

//var1 是this指针
//var2 是偏移量
//var4 是自增量
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        //获取内存,称之为期待的内存值E
        var5 = this.getIntVolatile(var1, var2);
        //var5 + var4的结果是更新值U
        //这里使用JNI方法,每个线程将自己内存中的内存值M与var5期望值比较,
        //如果相同则更新为var5 + var4,返回true跳出循环。
        //如果不相同,则把内存值M更新为最新的内存值,然后自旋,直到更新成功为止
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    //返回更新后的值
    return var5;
}

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

所以可以看出CAS算法其实是无锁的。好处是在读多写少的情况下,性能是比较好的。那么CAS算法的缺点其实也是很明显的。

  • ABA问题。线程C将内存值A改成了B后,又改成了A,而线程D会认为内存值A没有改变过,这个问题就称为ABA问题。解决办法很简单,在变量前面加上版本号,每次变量更新的时候变量的版本号都+1,即A->B->A就变成了1A->2B->3A
  • 在写多读少的情况下,也就是频繁更新数据,那么会导致其他线程经常更新失败,那么就会进入自旋,自旋时会占用CPU资源。如果资源竞争激烈,多线程自旋的时间长,导致消耗资源

使用场景

读多写少的场景下,更新时很少发生冲突,使用乐观锁,减少了上锁和释放锁的开销,可以有效地提升系统的性能。

相反,在写多读少的场景下,如果使用乐观锁会导致更新时经常产生冲突,然后线程会循环重试,这样会增大CPU的消耗。在这种情况下,建议可以使用悲观锁

总结

在日常的开发中,悲观锁和乐观锁应该是见得最多,用得最多的锁,比如最常见的synchronizedReentrantLock是悲观锁,并发包中的原子类和ConcurrentHashMap则用了乐观锁。锁的实现并不复杂,关键是搞懂这两种锁的思想,这样才能在合适的地方使用合适的锁。

这篇文章就讲到这里了,希望看完后能有所收获,感谢你的阅读。

觉得有用就点个赞吧,你的点赞是我创作的最大动力~

我是一个努力让大家记住的程序员。我们下期再见!!!
在这里插入图片描述

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!

相关文章
|
1月前
|
算法 Java 关系型数据库
何为乐观锁和悲观锁
何为乐观锁和悲观锁
|
3月前
|
设计模式 消息中间件 安全
面试官:说说读写锁的实现原理?
面试官:说说读写锁的实现原理?
62 1
|
3月前
|
算法 Java
【多线程面试题十八】、说一说Java中乐观锁和悲观锁的区别
这篇文章讨论了Java中的乐观锁和悲观锁的区别,其中悲观锁假设最坏情况并在访问数据时上锁,如通过`synchronized`或`Lock`接口实现;而乐观锁则在更新数据时检查是否被其他线程修改,适用于多读场景,并常通过CAS操作实现,如Java并发包`java.util.concurrent`中的类。
|
3月前
|
Java
【多线程面试题二十二】、 说说你对读写锁的了解
这篇文章讨论了读写锁(ReadWriteLock)的概念和应用场景,强调了读写锁适用于读操作远多于写操作的情况,并介绍了Java中`ReentrantReadWriteLock`实现的读写锁特性,包括公平性选择、可重入和可降级。
|
6月前
|
Java 大数据 程序员
Java并发编程中的锁机制探究
传统的Java并发编程中,锁机制一直是保证多线程程序安全性的重要手段之一。本文将深入探讨Java中常见的锁机制,包括synchronized关键字、ReentrantLock类以及java.util.concurrent包下的各种锁实现,分析它们的特点、适用场景以及性能表现,帮助开发者更好地理解和应用这些锁机制。
36 1
|
6月前
|
算法 Java 关系型数据库
面试必备之何谓悲观锁与乐观锁
乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。
53 1
|
设计模式 Java 容器
一篇神文就把java多线程,锁,JMM,JUC和高并发设计模式讲明白了
今天给大家分享一篇一线开发大牛整理的java高并发核心编程神仙文档,里面主要包含的知识点有:多线程、线程池、内置锁、JMM、CAS、JUC、高并发设计模式、Java异步回调、CompletableFuture类等。
|
存储 Java
【一文读懂】 Java并发 - 锁升级原理
Java对象头,锁升级的原因,重量级锁、轻量级锁、偏向锁的原理
344 0
【一文读懂】 Java并发 - 锁升级原理
|
算法 Java 关系型数据库
同步锁、死锁、乐观锁、悲观锁 (高薪常问)
同步锁、死锁、乐观锁、悲观锁 (高薪常问)
93 2
|
数据库
乐观锁和悲观锁的底层原理
乐观锁和悲观锁是并发编程中常用的两种锁机制,用于解决多线程或多进程环境下的并发访问问题。它们的底层原理和适用场景有所不同。
151 0