一文搞懂面试必备问题-悲观锁和乐观锁

java常用锁synchronized和Lock

  • Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将
    unLock()放到finally{} 中;
  • synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  • Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  • 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  • Lock可以提高多个线程进行读操作的效率。

什么是悲观锁和乐观锁

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如(InnoDB)行锁,(myisam)表锁等,(readLock)读锁,(writLock)写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁的CAS机制

CAS:Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置,是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B
    当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作,原子是单个共享变量,如果是多个则无效)。一般情况下是一个自旋操作,即不断的重试。
volatile test = 12345; // volatile关键字修饰内存中读取的V值(内存中的值是共享的,在多线程环境下是变化的,volatile可以保证是可见和有序的)

/* 更新数据的线程会进行如下操作 */
flag = true;
while (flag) {
    Value= test; // 保存原始数据到A值
    newValue = dbgetValue(Value); //通过A值计算出需要设置的B值

    // 下面的部分为CAS操作,尝试更新test 的值
    if (test == Value ) { // 比较,内存中V值是否变化
        test = newValue; // 设置
        flag = false; // 结束
    } else {
	// 啥也不干,循环重试
    }
}

很明显,这样的代码根本不是原子性的,因为真正的CAS利用了CPU指令,这里只是为了展示执行流程,本意是一样的因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已!

CAS算法中,保存在内存中的V值必须用volatile关键字修饰
volatile是可以保证数据的可见性,即当对一个volatile数据进行修改的时候,可以保证此时缓存行的进行读入,并且立即将修改的值从自己的缓存中写入主存,缓存行会等待主存地址更改的通知后,才会读入数据 ,同时通过volatile关键字来保证一定的“有序性”,即volatile保证对一个变量的写操作先行发生于后面对这个变量的读操作。可以保证读取到最新的数据,但是不能保证变量自增的原子性。
后续会将volatile关键字特性单独开一篇博客:https://blog.csdn.net/qq_41714882/article/details/104166530

乐观锁的缺点

ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

为了解决这个问题,在每次进行操作的时候加上一个版本号,每次操作的就是两个值,JDK 1.5 以后提供了AtomicStampedReference类解决ABA问题,用Pair这个内部类实现,包含两个属性,分别代表版本号和引用,在compareAndSet中先对当前引用进行检查,再对版本号标志进行检查,只有全部相等才更新值。
循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 看起来CAS比锁的效率高,从阻塞机制变成了非阻塞机制,减少了线程之间等待的时间。每个方法不能绝对的比另一个好,在线程之间竞争程度大的时候,如果使用CAS,每次都有很多的线程在竞争,也就是说CAS机制不能更新成功。这种情况下CAS机制会一直重试,这样就会比较耗费CPU。因此可以看出,如果线程之间竞争程度小,使用CAS是一个很好的选择;但是如果竞争很大,使用锁可能是个更好的选择。
只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。

CAS与synchronized的使用情景

简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)

  • 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  • 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
发布了32 篇原创文章 · 获赞 53 · 访问量 2476

猜你喜欢

转载自blog.csdn.net/qq_41714882/article/details/104083939
今日推荐