java多线程中的锁分类多种多样,其中有一种主要的分类方式就是乐观和悲观进行划分的。这篇文章主要介绍如何自己手写一个乐观代码。不过文章为了保证完整性,会从基础开始介绍。
一、乐观锁概念
说是写乐观锁的概念,但是通常乐观锁和悲观锁的概念都要一块写。对比着来才更有意义。
1、悲观锁概念
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁。
就比如说java里面的同步机制synchronized关键字就是一个悲观锁,当一个变量或者是方法使用了synchronized修饰时,其他的线程想要拿到这个变量或者是方法的时候将就需要等到别的线程释放。
数据库里面也用到了这种悲观锁的机制。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。这样其他的线程就不能同步操作,必须要等到他释放才可以。
2、乐观锁概念
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。
注意“在此期间”的含义是拿到数据到更新数据的这段时间。因为没有加锁,所以别的线程可能会更改。还有一点那就是乐观锁其实是不加锁的。
今天我们要实现的就是一个乐观锁机制,既然乐观锁是不加锁的,而且还要保证数据的一致性。如何来实现呢?举个例子:java中的Atomic包下的一系列类就是使用了乐观锁机制。我们挑出来一个看看官方是如何实现的,然后按照这样的实现机制我们自己就可以实现。
3、乐观锁实现案例
java并发机制中主要有三个特性需要我们去考虑,原子性、可见性和有序性。AtomicInteger的作用就是为了保证原子性。如何保证原子性呢?我们使用案例说明:
这个例子很简单:我们定义了一个变量a,初始值是0,然后使用5个线程去增加,每个线程增加10,按道理来说5个线程一共增加了50,但是运行一下就知道答案不到50,原因就在于里面那个加一操作:a++;
对于a++的操作,其实可以分解为3个步骤。
(1)从主存中读取a的值
(2)对a进行加1操作
(3)把a重新刷新到主存
比如说有的线程已经把a进行了加1操作,但是还没来得及重刷入到主存,其他的线程就重新读取了旧值。这才造成了错误。解决办法就可以使用AtomicInteger:
现在我们使用AtomicInteger定义a,然后使用incrementAndGet进行自增操作,最后的结果就总是50了。为了什么AtomicInteger有这样的特点呢?我们来分析一下:
4、乐观锁案例分析
AtomicInteger是一个乐观锁,也就是说我们只要看一下AtomicInteger是如何实现这样的机制和原理,我们就可以找出其他乐观锁实现的一般机制。想要找出来答案我们还要从AtomicInteger的incrementAndGet方法说起。因为这个方法实现了一样的功能。这里使用的是jdk1.8的版本,不同的版本会有出入。
这里我们可以看到自增操作主要是使用了unsafe的getAndAddInt方法。因为不是专门介绍AtomicInteger,所以不会对源码进行相信的分析。
(1)Unsafe:Unsafe是位于sun.misc包下的一个类,Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力。也就是说我们直接操作了内存空间进行了加1操作。
(2)unsafe.getAndAddInt:其内部又调用了Unsafe.