程序平安
线程平安是程序开发中十分须要咱们留神的一环,当程序存在并发的可能时,如果咱们不做非凡的解决,很容易就呈现数据不统一的状况。
通常状况下,咱们能够用加锁的形式来保障线程平安,通过对共享资源 (也就是要读取的数据) 的加上 ” 隔离的锁 ”,使得多个线程执行的时候也不会相互影响,而 乐观锁 和乐观锁 正是并发管制中较为罕用的技术手段。
乐观锁和乐观锁
什么是乐观锁?什么是乐观锁?其实从字面上就能够辨别出两者的区别,艰深点说,
乐观锁
乐观锁就如同一个有迫害妄想症的患者,总是假如最坏的状况,每次拿数据的时候都认为他人会批改,所以每次拿数据的时候都会上锁,直到整个数据处理过程完结,其余的线程如果要拿数据就必须等以后的锁被开释后能力操作。
应用案例
乐观锁的应用场景并不少见,数据库很多中央就用到了这种锁机制,比方行锁,表锁,读锁,写锁等,都是在做操作之前先上锁,乐观锁的实现往往依附数据库自身的锁性能实现。Java 程序中的 Synchronized 和 ReentrantLock 等实现的锁也均为乐观锁。
在数据库中,乐观锁的调用个别是在所要查问的语句前面加上 for update
,
select * from db_stock where goods_id = 1 for update
当有一个事务调用这条 sql 语句时,会对 goods_id = 1 这条记录加锁,其余的事务如果也对这条记录做 for update
的查问的话,那就必须等到该事务执行完后能力查出后果,这种加锁形式能对读和写做出排他的作用,保障了数据只能被以后事务批改。
当然,如果其余事务只是简略的查问而没有用 for update 的话,那么查问还是不会受影响的,只是说更新时一样要期待以后事务完结才行。
值得注意的是,MySQL 默认应用 autocommit 模式,也就是说,当你执行一个更新操作后,MySQL 会立即将后果进行提交,就是说,如果咱们不仅要读,还要更新数据的话,须要手动管制事务的提交,比方像上面这样:
set autocommit=0;
// 开始事务
begin;
// 查问出商品 id 为 1 的库存表数据
select * from db_stock where goods_id = 1 for update;
// 减库存
update db_stock set stock_num = stock_num - 1 where goods_id = 1 ;
// 提交事务
commit;
尽管乐观锁能无效保证数据执行的程序性和一致性,但在高并发场景下并不实用,试想,如果一个事务用乐观锁对数据加锁之后,其余事务将不能对加锁的数据进行除了查问以外的所有操作,如果该事务执行工夫很长,那么其余事务将始终期待,这无疑会升高零碎的吞吐量。
这种状况下,咱们能够有更好的抉择,那就是乐观锁。
乐观锁
乐观锁的思维和乐观锁相同,总是假如最好的状况,认为他人都是敌对的,所以每次获取数据的时候不会上锁,但更新数据那一刻会判断数据是否被更新过了,如果数据的值跟本人预期一样的话,那么就能够失常更新数据。
场景
这种思维利用到理论场景的话,能够用版本号机制和 CAS 算法实现。
CAS
CAS 是一种无锁的思维,它假如线程对资源的拜访是没有抵触的,同时所有的线程执行都不须要期待,能够继续执行。如果遇到抵触的话,就应用一种叫做 CAS (比拟替换) 的技术来甄别线程抵触,如果检测到抵触产生,就重试以后操作到没有抵触为止。
原理
CAS 的全称是 Compare-and-Swap,也就是比拟并替换,它蕴含了三个参数:V,A,B,V 示意要读写的内存地位,A 示意旧的预期值,B 示意新值
具体的机制是,当执行 CAS 指令的时候,只有当 V 的值等于预期值 A 时,才会把 V 的值改为 B,如果 V 和 A 不同,有可能是其余的线程批改了,这个时候,执行 CAS 的线程就会一直的循环重试,直到能胜利更新为止。
正是基于这样的原理,CAS 即时没有应用锁,也能发现其余线程对以后线程的烦扰,从而进行及时的解决。
毛病
CAS 算是比拟高效的并发管制伎俩,不会阻塞其余线程。然而,这样的更新形式是存在问题的,看流程就晓得了,如果 C 的后果始终跟预期的后果不一样的话,线程 A 就会始终一直的循环重试,重试次数太多的话对 CPU 也是一笔不小的开销。
而且,CAS 的操作范畴也比拟局限,只能保障一个共享变量的原子操作,如果须要一段代码块的原子性的话,就只能通过 Synchronized 等工具来实现了。
除此之外,CAS 机制最大的缺点就是 ”ABA” 问题。
ABA 问题
后面说过,CAS 判断变量操作胜利的条件是 V 的值和 A 是统一的,这个逻辑有个小小的缺点,就是如果 V 的值一开始为 A,在筹备批改为新值前的期间已经被改成了 B,起初又被改回为 A,通过两次的线程批改对象的值还是旧值,那么 CAS 操作就会误工作该变量素来没被批改过,这就是 CAS 中的“ABA”问题。
看完流程图置信也不必我说太多了吧,线程多发的状况下,这样的问题是十分有可能产生的,那么如何防止 ABA 问题呢?
加标记位,例如搞个自增的字段,没操作一次就加一,或者是一个工夫戳,每次更新比拟工夫戳的值,这也是数据库版本号更新的思维(上面会说到)
在 Java 中,自 JDK1.5 当前就提供了这么一个并发工具类AtomicStampedReference,该工具外部保护了一个外部类,在原有根底上保护了一个对象,及一个 int 类型的值(能够了解为版本号),在每次进行比照批改时,都会先判断要批改的值,和内存中的值是否雷同,以及版本号是否雷同,如果全副相等,则以原子形式将该援用和该标记的值设置为给定的更新值。
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {return new Pair<T>(reference, stamp);
}
}
实用场景
CAS 个别实用于读多写少的场景,因为这种状况线程的抵触不会太多,也只有线程抵触不重大的状况下,CAS 的线程循环次数能力无效的升高,性能也能更高。
版本号机制
版本号机制是数据库更新操作里十分实用的技巧,其实原理很简略,就是获取数据的时候会拿一个能对应版本的字段,而后更新的时候判断这个字段是否跟之前拿的值是否统一,统一的话证实数据没有被他人更新过,这时就能够失常实现更新操作。
还是下面的那张表为例,咱们加上一个版本号字段 version,而后每次更新数据的时候就把版本号加 1,
select goods_id,stock_num,version from db_stock where goods_id = 1
update db_stock set stock_num = stock_num - 1,version = version + 1 where goods_id = 1 and version = #{version}
这样的话,如果有两个事务同时对 goods_id = 1 这条数据做更新操作的话,肯定会有一个事务先执行实现,而后 version 字段就加 1,另一个事务更新的时候发现 version 曾经不是之前获取到的那个值了,就会从新执行查问操作,从而保障了数据的一致性。
这种锁的形式也不会影响吞吐量,毕竟大家都能够同时读和写,但高并发场景下,sql 更新报错的可能性会大大增加,这样对业务解决仿佛也不敌对。
这种状况下,咱们能够把锁的粒度放大,比如说减库存的时候,咱们能够这么解决:
update db_stock set stock_num = stock_num - 1 where goods_id = 1 and stock_num > 0
这样一来,sql 更新抵触的概率会大大降低,而且也不必去独自保护相似 version 的字段了。
最初
对于乐观锁和乐观锁的例子介绍就到这儿了,当然,本文也只是稍微解说,更多的知识点还要靠大家钻研,而且,除了这两种锁,并发管制中还有很多其余的管制伎俩,像什么 Synchronized、ReentrantLock、偏心锁,非偏心锁之类的都是很常见的并发常识,不论是为了日常开发还是应酬面试,把握这些知识点还是很有必要的,而且,并发编程的常识思维是共通的,晓得一块知识点后很容易就能延长去学习其余的知识点。
拿我本人来说,最近也在认真钻研 Java 并发编程的一些知识点,也因为要写乐观锁的缘故,顺道温习了一下 CAS 和它的应用案例,从而也理解到了 ReentrantLock 底层其实就是通过 CAS 机制来实现锁的,而且还理解了独占锁,共享锁,可重入锁等应用场景,由点到面,也让我常识体系储备更加的丰盛,近期也有打算撸几篇对于 ReentrantLock 常识的文章进去,欢送大家多来踩踩!
作者:鄙人薛某,一个不拘于技术的互联网人,技术三流,吹水一流,想看更多精彩文章能够关注我的公众号哦~~~
原创不易,您的【三连】将是我创作的最大能源,感激各位的反对!