Java互联网科技Java

简述悲观锁和乐观锁及其非阻塞算法--CAS原理

2019-08-16  本文已影响0人  晴栀吖

首先,这个悲观锁和乐观锁都是针对数据库操作而言的。产生的原因是为了解决互联网上的高并发问题。

那么,悲观锁有啥特点?

利用了数据库内部提供的锁机制
在并发过程中一旦有一个事务持有了数据库记录的锁,其他线程就不能再对数据库进行更新
所以,对于悲观锁而言,当一条线程抢占了资源后,其他的线程得不到资源,那么这个时候,cpu就会将这些得不到资源的线程挂起,挂起的线程也会消耗cpu的资源,尤其是高并发的请求中。如果大量的线程被挂起和恢复,就会非常消耗资源,所以悲观锁的性能不佳。悲观锁又称独占锁、阻塞锁。这个就很像我们平常在java并发中使用的synchronized,但是这里的悲观锁是数据库使用的,数据库有他的sql语句来实现悲观锁,比如说for update,这就是给相应的数据加上悲观锁(其实就是mysql中的排他锁,用于更新的),多个线程同时申请操作的话就会引起堵塞。

和悲观锁相反,乐观锁是一种不会阻塞其它线程并发的机制,它不会使用数据库的锁进行实现。所以就不会引起线程的频繁挂起和恢复,这样效率就提高了。它的实现关键在于CAS原理。

那么啥是CAS呢?全写为Compare and Swap,即比较和交换。具体是指:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。(这个好像是网上的通俗概念,所以直接复制粘贴,而且默默表示简短精炼的说明了啥是CAS)。用我的话说就是:一个小黑屋内有五颗棒棒糖放在桌面上,然后我记住了这里是有五颗棒棒糖,然后我离开了小黑屋。然后我再进小黑屋,发现这里还是只有五棵棒棒糖,因为现在的5和我记忆中的5是一样的,所以,我认为没有人动过这些棒棒糖。然后,聪明的我们发现其中有猫腻,如果在我出去的这个期间,有人进来吃了一个棒棒糖,然后另一个人又拿了一个棒棒糖放回来,最后两个人都出去了,那不还是操作了这些棒棒糖。所以我之前的判断不太对。然后可能有人又会想,这还是五颗啊,有影响吗?那我会这样回答,如果只有前面五颗棒棒糖才能组成打开幸运宝盒的钥匙,一旦缺少一颗或者变化了一颗都会让钥匙失效,并且触发潘多拉魔盒的打开,那你还会觉得这种变化无关紧要吗?这就是在CAS中常常说到的ABA问题:线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题。

所以,如何解决呢?聪明的我们会联想起图书馆的借书记录,对啊,找个本子放在桌面上记录啊,谁操作了这棒棒糖就把本子上的数据加一就行。我们只需要记住我们操作时本子上的数据是多少,然后看看我们回来时本子上的数据是否是我们原来记录下来的,如果一样,那不就说明没人动过了吗?然后再进行我们所需要的操作。所以这就是加入一个版本号解决了问题。这就需要在数据库存储中加入一个version类的字段,然后再sql语句中每操作一次都改变一下这版本号。然后再java程序中进行相应的逻辑判断就行了。

但是,这样虽然并发错误(比如超发现象)不会产生了,但是在高并发过程中,由于大量修改这个版本号,乐观锁导致请求服务失败的概率大大增高,如果客户端只能请求一次的话,那么就会存在大量的无效请求,实现不了相应的服务。所以这样就有了相应的解决办法:乐观锁的重入机制,即使用时间戳或者按次数限定。就比如你只允许进三次小黑屋,如果三次都争取不到,那么你就得出来先,然后等下次有机会再上,不然你一直都抢不到,但是又占了位置,那不是很难受??为了更进一步地提高效率,就用上了我上一篇文章说的redis。你数据库操作1万次的时间甚至都可以让我redis操作10万次了,你说你在干啥子勒?不过由于redis并不是一个长久存储数据的地方,它存储的数据是非严格和安全的环境,更多的时候只是为了提供更为快速的缓存。所以当达到稳定状态的时候还是需要存储进数据库当中,这样才能保证数据的安全性和严格性。

然后,回到书本上讲的抢红包的高并发案例,这里建议是用firefox浏览器去访问,因为chrome是真的会丢失一些ajax请求,导致数据很奇怪,比如乐观锁中3万个请求都结束了,可是2万个红包都没分发完,然后我把数据打印到文本文件中才发现,这个chrome很多请求都没发送到tomcat里来。

先看没有任何措施时的抢红包,此时会产生超发现象,也就是说明明只有一万个红包,可是却发出了10008个红包,原因在于程序判断红包数大于0时没有加锁,也没有采取任何措施,导致并发情况下没有红包时也发出了红包。

然后看悲观锁情况下,由于加了mysql(使用innodb引擎进行存储)的排他锁,所以,没有超发现象的产生,但是由于线程的挂起等待恢复,消耗了很多时间,所以时间效率比较低,而且消耗cpu的资源。感觉运行其他软件也会有些卡。不过它可以保证前2万个人进来,一定是这两万个人拿到红包,,而不会是后面的人。

接着看乐观锁,当没有重入机制时,比起悲观锁来说,运行起来还好,不是那么卡,但是由于没有重入机制,也没有锁,所以一个人抢不到红包的概率很大,得看cpu的运行了。而且这就很不科学了,我明明先抢的红包,可是却没抢到,反倒是我后面的人抢到红包了。运行结束后是3万个人抢2万个红包,最后居然没有抢完,还留有大量红包,可是他们又不能再抢了。

然后看乐观锁使用时间戳进行重入,我取得时间戳是100ms,也就是说你这个线程在100ms内只要cpu分配到给你机会你就可以抢红包。出了这100ms你就没机会了。运行起来消耗还好。然后最后的结果是第2万3百个人抢完最后一个红包。也就是说前面有300个人虽然提前抢了红包,但是由于cpu在100ms内给你的机会了你把握不住或者根本没给你机会(第一次还是有的)导致你没抢到红包。这个人数还是有点大,不过比起单纯的乐观锁而言已经好了很多了。

最后看乐观锁使用次数进行重入,我取的次数是3次,也就是说每个线程都有三次机会,如果三次都有其他线程修改了你的版本号(version),那么你就凉凉。不过这个概率很低。然后最终结果是2万零1个人抢到最后一个红包,也就是说只有一个人三次机会都失败了。这已经很不错了,概率非常之低了。

前面的实验都是基于我的4000块钱的联想电脑,有挺多插件的eclipse(日常打开花一分钟),火狐浏览器,盗版win10操作系统而言的。所以时间那些东西就不去算了,很多不确定性。电脑配置太差了。

建议还是自己去实现一波,不然无法理解里面的具体操作是啥,怎么去实现。在实现的同时,还可以好好地把数据库,redis方面的知识好好学习。里面的程序是真的精髓,理解了一种思想,而且还好好学习了一波sql。后面会开文章将redis的。
如果觉得有用的话,可以帮忙点个小赞嘛?


谢谢

参考《java EE互联网轻量级框架整合开发ssm+redis》

更新:注意,这里说的乐观锁是利用了sql里面的update语句的原子性实现的,在update语句里加上检查version的值是否改变。

上一篇 下一篇

猜你喜欢

热点阅读