[笔记系列文章说明]: 该类型的文章是笔者学习过程中整理的学习笔记.
概念
这里抛开数据库来谈乐观锁和悲观锁,扯上数据库总会觉得和Java离得很远.
悲观锁: 一段执行逻辑加上悲观锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放.
乐观锁: 一段执行逻辑加上乐观锁,不同线程同时执行时,可以同时进入执行,在最后更新数据的时候要检查这些数据是否被其他线程修改了(版本和执行初是否相同),没有修改则进行更新,否则放弃本次操作.
从解释上可以看出,悲观锁具有很强的独占性,也是最安全的.而乐观锁很开放,效率高,安全性比悲观锁低,因为在乐观锁检查数据版本一致性时也可能被其他线程修改数据.从下面的例子中可以看出来这里说的安全差别.
乐观锁例子
1 | package note.com; |
测试结果
1 | A:1 |
从结果中看出,B线程在执行的时候最后发现自己的value和执行前不一致,说明被A修改了,那么放弃了本次执行.
多运行几次发现了下面的结果:
1 | A:1 |
从结果看A修改了value值,B却没有检查出来,利用错误的value值进行了操作. 为什么会这样呢?
这里就回到前面说的乐观锁是有一定的不安全性的,B在检查版本的时候A还没有修改,在B检查完版本后更新数据前(例子中的输出语句),A更改了value值,这时B执行更新数据(例子中的输出语句)就发生了与现存value不一致的现象.
针对这个问题,我觉得乐观锁要解决这个问题还需要在检查版本(解决ABA问题)与更新数据这个操作的时候能够使用悲观锁,比如加上synchronized,让它在最后一步保证数据的一致性.这样既保证多线程都能同时执行,牺牲最后一点的性能去保证数据的一致.
ABA: 三个线程同时操作数值, 仅比较值是否和获取的相同无法保证,值未被修改C检查A+ B-后值未变,此时也应放弃操作才对
解决ABA: 操作前增加版本号对比
补充
以前不知道cas(比较-交换)这个在java中的存在,找了找资料才发现java的concurrent包确实使用的cas实现乐观锁的数据同步问题.
下面是我对这两种方式的一点看法:
有两种方式来保证乐观锁最后同步数据保证它原子性的方法
- CAS方式:Java非公开API类Unsafe实现的CAS(比较-交换),由C++编写的调用硬件操作内存,保证这个操作的原子性,concurrent包下很多乐观锁实现使用到这个类,但这个类不作为公开API使用,随时可能会被更改.我在本地测试了一下,确实不能够直接调用,源码中Unsafe是私有构造函数,只能通过getUnsafe方法获取单例,首先去掉eclipse的检查(非API的调用限制)限制以后,执行发现报 java.lang.SecurityException异常,源码中getUnsafe方法中执行访问检查,看来java不允许应用程序获取Unsafe类. 值得一提的是反射是可以得到这个类对象的.
- 加锁方式:利用Java提供的现有API来实现最后数据同步的原子性(用悲观锁).看似乐观锁最后还是用了悲观锁来保证安全,效率没有提高.实际上针对于大多数只执行不同步数据的情况,效率比悲观加锁整个方法要高.特别注意:针对一个对象的数据同步,悲观锁对这个对象加锁和乐观锁效率差不多,如果是多个需要同步数据的对象,乐观锁就比较方便.
扩展:利用反射获得Unsafe对象
第一步:去掉eclipse受限制的API检查:
1 | 将Windows->Preferences->Java-Complicer->Errors/Warnings->Deprecated and restricted API,中的Forbidden references(access rules)设置为Warning,Unsafe可以编译通过。 |
第二步:利用反射跳过安全检查获取Unsafe对象:
1 | Class<Unsafe> s1 = (Class<Unsafe>) Class.forName("sun.misc.Unsafe"); |
关于Unsafe的使用方法给个参考地址,平时用不到,我没有去深入看.
地址:Java Magic. Part 4: sun.misc.Unsafe