前言
关于线程安全一提到可能就是加锁。
那锁本身是怎么去实现的呢?又有哪些加锁的方式呢?
今天就简单聊一下乐观锁和悲观锁,他们对应的实现 CAS ,Synchronized,ReentrantLock
正文
一、乐观锁—CAS
1、什么是乐观锁?
答:乐观锁其实就是总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。
2、那CAS又是神马?它又是怎么实现线程安全的?
答:CAS(Compare And Swap 比较并且替换)进行比较后再更新数据,流程图
举例说明:现在一个线程要修改数据库的name,修改前我会先去数据库查name的值,发现name=“小明”,拿到值了,我们准备修改成name=“大明”,在修改之前我们判断一下,原来的name是不是等于“小明”,如果被其他线程修改就会发现name不等于“小明”,我们就不进行操作,如果原来的值还是小明,我们就把name修改为“大明”,至此,一个流程就结束了。
3、那CAS是不是存在问题?
答:是的,会变成死循环,要是结果一直不满足要求就一直循环了,例如上述例子如果name一直不是小明那就会一直循环下去,CUP开销是个问题,还有ABA问题和只能保证一个共享变量原子操作的问题。
4、什么是ABA问题?
答:如下流程图:
- 线程1读取了数据A
- 线程2读取了数据A
- 线程2通过CAS比较,发现值是A没错,可以把数据A改成数据B
- 线程3读取了数据B
- 线程3通过CAS比较,发现数据是B没错,可以把数据B改成了数据A
- 线程1通过CAS比较,发现数据还是A没变,就写成了自己要改的值
在这个过程中任何线程都没做错什么,但是值被改变了,线程1却没有办法发现,其实这样的情况出现对结果本身是没有什么影响的,但是我们还是要防范。
5、那要如何防范ABA问题呢?
答:这就用到了版本号机制,其实很简单,加标识符,如版本号;
举个例子:
现在我们去要求操作数据库,根据CAS的原则我们本来只需要查询原本的值就好了,现在我们一同查出他的标志位版本字段vision。
之前不能防止ABA的正常修改:
update table set value = newValue where value = #{oldValue}
//oldValue就是我们执行前查询出来的值
带版本号能防止ABA的修改:
update table set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision}
// 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样
二、悲观锁 - synchronized
1、什么是悲观锁?
答:悲观锁其实就是总是假设最坏的情况,每次去拿数据的时候都认为别人一定会修改,所以一定会上锁,如synchronized加锁,synchronized 是最常用的线程同步手段之一,上面提到的CAS是乐观锁的实现,synchronized就是悲观锁了。
2、synchronized是如何保证同一时刻只有一个线程进行操作的?
synchronized,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。
从对对象、方法和代码块三方面加锁,去介绍他怎么保证线程安全的:
- synchronized 对对象进行加锁,在 JVM 中,对象在内存中分为三块区域:对象头(Header)、实例数据(Instance
Data)和对齐填充(Padding)。
- 对象头:我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass
Pointer(类型指针)。
- Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark
Word里存储的数据会随着锁标志位的变化而变化。 - Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 你可以看到在对象头中保存了锁标志位和指向 monitor 对象的起始地址,如下图所示,右侧就是对象对应的 Monitor 对象。
当 Monitor 被某个线程持有后,就会处于锁定状态,如图中的 Owner 部分,会指向持有 Monitor 对象的线程。
另外 Monitor 中还有两个队列分别是EntryList和WaitList,主要是用来存放进入及等待获取锁的线程。
如果线程进入,则得到当前对象锁,那么别的线程在该类所有对象上的任何操作都不能进行。
2.同步方法和同步代码块底层都是通过monitor来实现同步的,不再一一介绍。
两者的区别:同步方式是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现,同步代码块是通过monitorenter和monitorexit来实现。
我们知道了每个对象都与一个monitor相关联,而monitor可以被线程拥有或释放
三、知识点总结