前言

关于线程安全一提到可能就是加锁

那锁本身是怎么去实现的呢?又有哪些加锁的方式呢?

今天就简单聊一下乐观锁和悲观锁,他们对应的实现 CAS ,Synchronized,ReentrantLock

正文

一、乐观锁—CAS

1、什么是乐观锁?
答:乐观锁其实就是总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制CAS算法实现。

2、那CAS又是神马?它又是怎么实现线程安全的?

答:CAS(Compare And Swap 比较并且替换)进行比较后再更新数据,流程图

java lock syc 和 多线程 java多线程加锁的三种方式_多线程


举例说明:现在一个线程要修改数据库的name,修改前我会先去数据库查name的值,发现name=“小明”,拿到值了,我们准备修改成name=“大明”,在修改之前我们判断一下,原来的name是不是等于“小明”,如果被其他线程修改就会发现name不等于“小明”,我们就不进行操作,如果原来的值还是小明,我们就把name修改为“大明”,至此,一个流程就结束了。

3、那CAS是不是存在问题?
答:是的,会变成死循环,要是结果一直不满足要求就一直循环了,例如上述例子如果name一直不是小明那就会一直循环下去,CUP开销是个问题,还有ABA问题只能保证一个共享变量原子操作的问题。

4、什么是ABA问题?
答:如下流程图:

  1. 线程1读取了数据A
  2. 线程2读取了数据A
  3. 线程2通过CAS比较,发现值是A没错,可以把数据A改成数据B
  4. 线程3读取了数据B
  5. 线程3通过CAS比较,发现数据是B没错,可以把数据B改成了数据A
  6. 线程1通过CAS比较,发现数据还是A没变,就写成了自己要改的值

java lock syc 和 多线程 java多线程加锁的三种方式_加锁_02


在这个过程中任何线程都没做错什么,但是值被改变了,线程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,没有的话,锁定调用者,然后直接运行。

从对对象、方法和代码块三方面加锁,去介绍他怎么保证线程安全的:

  1. synchronized 对对象进行加锁,在 JVM 中,对象在内存中分为三块区域:对象头(Header)、实例数据(Instance
    Data)和对齐填充(Padding)。
  • 对象头:我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass
    Pointer(类型指针)。
  • Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark
    Word里存储的数据会随着锁标志位的变化而变化。
  • Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 你可以看到在对象头中保存了锁标志位和指向 monitor 对象的起始地址,如下图所示,右侧就是对象对应的 Monitor 对象。

java lock syc 和 多线程 java多线程加锁的三种方式_数据_03



  • 当 Monitor 被某个线程持有后,就会处于锁定状态,如图中的 Owner 部分,会指向持有 Monitor 对象的线程。

另外 Monitor 中还有两个队列分别是EntryList和WaitList,主要是用来存放进入及等待获取锁的线程。

如果线程进入,则得到当前对象锁,那么别的线程在该类所有对象上的任何操作都不能进行。
2.同步方法和同步代码块底层都是通过monitor来实现同步的,不再一一介绍。

两者的区别:同步方式是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现,同步代码块是通过monitorenter和monitorexit来实现。

我们知道了每个对象都与一个monitor相关联,而monitor可以被线程拥有或释放

三、知识点总结

java lock syc 和 多线程 java多线程加锁的三种方式_数据_04