程序平安
线程平安是程序开发中十分须要咱们留神的一环,当程序存在并发的可能时,如果咱们不做非凡的解决,很容易就呈现数据不统一的状况。
通常状况下,咱们能够用加锁的形式来保障线程平安,通过对共享资源 (也就是要读取的数据) 的加上"隔离的锁",使得多个线程执行的时候也不会相互影响,而乐观锁和乐观锁正是并发管制中较为罕用的技术手段。
乐观锁和乐观锁
什么是乐观锁?什么是乐观锁?其实从字面上就能够辨别出两者的区别,艰深点说,
乐观锁
乐观锁就如同一个有迫害妄想症的患者,总是假如最坏的状况,每次拿数据的时候都认为他人会批改,所以每次拿数据的时候都会上锁,直到整个数据处理过程完结,其余的线程如果要拿数据就必须等以后的锁被开释后能力操作。
应用案例
乐观锁的应用场景并不少见,数据库很多中央就用到了这种锁机制,比方行锁,表锁,读锁,写锁等,都是在做操作之前先上锁,乐观锁的实现往往依附数据库自身的锁性能实现。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 = 1update 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常识的文章进去,欢送大家多来踩踩!
作者:鄙人薛某,一个不拘于技术的互联网人,技术三流,吹水一流,想看更多精彩文章能够关注我的公众号哦~~~
原创不易,您的 【三连】 将是我创作的最大能源,感激各位的反对!