这是我参与更文挑战的第8天,活动详情查看: 更文挑战
前言
在学习乐观锁和悲观锁之前,先要了解一个名词【并发控制】。
在程序出现并发的情况下,即同一时间大量用户同时访问程序,这时候就要保证数据的准确性,用户和其他用户一起操作时,得到的数据和其单独操作时要一致,这就叫并发控制。
试想如果没有并发控制,那就会导致数据脏读、不可重复读等等问题。
这就引出今天的主题,实现并发控制的主要手段分为乐观并发控制和悲观并发控制两种。
乐观锁比较适用于读多写少的情况(多读场景),悲观锁比较适用于写多读少的情况(多写场景)。
悲观锁
顾名思义就是悲观的锁,当数据被用户A在修改时,系统会先锁住数据,等用户A修改之后,再打开锁,让下一个用户修改。悲观锁极具占有性,为了避免并发时多用户同时修改数据,直接上锁防止并发的操作。
这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。
悲观锁,具有强烈的独占性和排他性,它指的是对数据被外界修改持保守态度。在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制。
悲观锁实现过程:
现在有线程a和线程b同时都要对数据进行操作;
1、线程a尝试增加排他锁,线程b也尝试增加排他锁;
2、线程a加锁成功,线程b被阻塞;
3、线程a操作结束,增加释放锁,线程b继续增加排他锁;
4、线程b加锁成功,进行更新操作。
之所以称为悲观锁,就是对数据并发持悲观态度,认为某一线程在修改数据时,其他线程一定也会同时操作,为了防止这样的情况,对数据的操作过程全程上锁,当其他线程要访问时,都会被阻塞。
悲观锁的应用
1、传统的关系型数据库使用这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
2、Java 里面的同步synchronized关键字的实现。
悲观锁也分为两种:共享锁和排他锁。
共享锁,又名读锁,顾名思义读取数据时可以共享使用,但是只能读不能写。
排他锁,又名写锁,这就是在对数据进行操作时,已经获取到排他锁的事务可以对数据进行读写操作,未获得的事务不能操作数据。上面的例子讲到的就是排他锁。
说到悲观锁就再说下偏向锁。
偏向锁,当只有一个线程访问时,偏向于第一个访问锁的线程,就比较偏心的意思,为了在没有多线程竞争的情况下尽量减少不必要的轻量级锁执行。
因为轻量级锁的获取及释放需要多次CAS(Compare and Swap)原子操作,而偏向锁只需要在切换ThreadID时执行一次CAS原子操作,因此可以提高锁的运行效率。
偏向锁是始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用。一旦出现多线程竞争的情况就会升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁。
乐观锁
在每次读取数据时都认为别人不会修改该数据,所以不会上锁,但在更新时会判断在此期间别人有没有更新该数据,有的话会返回异常信息给用户,由用户决定如何操作。所以乐观锁更宽松,适用于读多写少的场景。
乐观锁的实现:
1、CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
2、版本号控制:一般会开一张数据表,记录数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会 +1。当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
CAS底层原理:
假如说有 3 个线程并发的要修改一个 UserAge的值,底层机制如下:
- 首先,这三个线程每个都会先获取到当前的值,接着走一个 CAS 操作。这个 CAS 操作一定是自己完整执行完的,不会被别人打断。
- 然后在 CAS 操作里,都会去比较一下,现在的age值是不是刚才获取到的那个值。如果是,说明没人改过这个值,然后设置成累加 1 之后的一个值。
- 同理,如果在执行 CAS 的时候,发现之前获取的值跟当前的值不一样,会导致 CAS 失败。失败之后,线程进入一个无限循环,会不断的重试,重新获取值,继续执行 CAS 操作,失败再重试,直到修改成功。
这里再顺便提一下自旋锁
自旋锁
在多线程高并发的情况下,如果持有锁的线程能在很短的时间内释放出锁资源,那么无需让其他线程做内核态和用户态的切换,进入阻塞、挂起状态而过多消耗时间,只需等一等(也叫作自旋),持有锁的线程释放锁后,即可立即获取锁。
但是线程在自旋时会占用一定的CPU,在线程长时间自旋获取不到锁时,将会产CPU的浪费,甚至有时线程永远无法获取锁而导致CPU资源被永久占用,所以需要设定一个自旋等待的最大时间。在线程执行的时间超过自旋等待的最大时间后,线程会退出自旋模式并释放其持有的锁。
特点:
减少CPU上下文切换,对于占用锁时间很短或者锁竞争不激烈的情况下,性能能够大幅度提升。自旋的CPU耗时明显少于线程阻塞、挂起、再唤醒时两次CPU上下文切换所用的时间。