线程安全是程序开发中很是须要咱们注意的一环,当程序存在并发的可能时,若是咱们不作特殊的处理,很容易就出现数据不一致的状况。面试
一般状况下,咱们能够用加锁的方式来保证线程安全,经过对共享资源 (也就是要读取的数据) 的加上"隔离的锁",使得多个线程执行的时候也不会互相影响,而悲观锁和乐观锁正是并发控制中较为经常使用的技术手段。算法
什么是悲观锁?什么是乐观锁?其实从字面上就能够区分出二者的区别,通俗点说,sql
悲观锁就好像一个有迫害妄想症的患者,老是假设最坏的状况,每次拿数据的时候都觉得别人会修改,因此每次拿数据的时候都会上锁,直到整个数据处理过程结束,其余的线程若是要拿数据就必须等当前的锁被释放后才能操做。数据库
使用案例编程
悲观锁的使用场景并很多见,数据库不少地方就用到了这种锁机制,好比行锁,表锁,读锁,写锁等,都是在作操做以前先上锁,悲观锁的实现每每依靠数据库自己的锁功能实现。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的全称是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知识的文章出来,欢迎你们多来踩踩!
做者:鄙人薛某,一个不拘于技术的互联网人,技术三流,吹水一流,想看更多精彩文章能够关注个人公众号哦~~~
原创不易,您的 【三连】 将是我创做的最大动力,感谢各位的支持!