java多线程中的锁分类多种多样,其中有一种主要的分类方式就是乐观和悲观进行划分的。这篇文章主要介绍如何自己手写一个乐观锁代码。不过文章为了保证完整性,会从基础开始介绍。
一、乐观锁概念
说是写乐观锁的概念,但是通常乐观锁和悲观锁的概念都要一块写。对比着来才更有意义。
1、悲观锁概念
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁。

数据库里面也用到了这种悲观锁的机制。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。这样其他的线程就不能同步操作,必须要等到他释放才可以。
2、乐观锁概念
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。

3、乐观锁实现案例
java并发机制中主要有三个特性需要我们去考虑,原子性、可见性和有序性。AtomicInteger的作用就是为了保证原子性。如何保证原子性呢?我们使用案例说明:

对于a++的操作,其实可以分解为3个步骤。
(1)从主存中读取a的值
(2)对a进行加1操作
(3)把a重新刷新到主存
比如说有的线程已经把a进行了加1操作,但是还没来得及重刷入到主存,其他的线程就重新读取了旧值。这才造成了错误。解决办法就可以使用AtomicInteger:

4、乐观锁案例分析
AtomicInteger是一个乐观锁,也就是说我们只要看一下AtomicInteger是如何实现这样的机制和原理,我们就可以找出其他乐观锁实现的一般机制。想要找出来答案我们还要从AtomicInteger的incrementAndGet方法说起。因为这个方法实现了锁一样的功能。这里使用的是jdk1.8的版本,不同的版本会有出入。

(1)Unsafe:Unsafe是位于sun.misc包下的一个类,Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力。也就是说我们直接操作了内存空间进行了加1操作。
(2) unsafe.getAndAddInt:其内部又调用了Unsafe.compareAndSwapInt方法。这个机制叫做CAS机制,
CAS 即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。
我们使用一个例子来解释相信你会更加的清楚。
比如说给你儿子订婚。你儿子就是内存位置,你原本以为你儿子是和杨贵妃在一起了,结果在订婚的时候发现儿子身边是西施。这时候该怎么办呢?你一气之下不做任何操作。如果儿子身边是你预想的杨贵妃,你一看很开心就给他们订婚了,也叫作执行操作。现在你应该明白了吧。
但是这样的CAS机制会带来一个比较常见的问题。那就是ABA问题,举个例子,你看到桌子上有100块钱,然后你去干其他事了,回来之后看到桌子上依然是100块钱,你就认为这100块没人动过,其实在你走的那段时间,别人已经拿走了100块,后来又还回来了。这就是ABA问题。

5、乐观锁思想
OK,上面说了这么多,其实就是想说一句话那就是乐观锁可以由CAS机制+版本机制来实现。
乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
(1)CAS机制:当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败。CAS 有效地说明了“ 我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可“。
(2)版本机制:CAS机制保证了在更新数据的时候没有被修改为其他数据的同步机制,版本机制就保证了没有被修改过的同步机制(意思是上面的ABA问题)。
基于这个思想我们就可以实现一个乐观锁。下面我们写一下代码。这个代码在我自己电脑上亲测通过。
二、实现一个乐观锁
第一步:定义我们要操作的数据



第四步:输出结果
这个结果可以看到在读数据的时候只要发现没有变化即可,但是更新数据的时候要判断当前的版本号和预期的版本号是否一致,如果一致那就更新,如果不一致,那就说明更新失败。
