Java的两把锁浅析

笔者最近在学习Java多线程的一些基础知识,浅谈一些自己关于Java锁的一些理解

Java锁是用来干什么的?

我们做一个程序,终归的目的,就是想让程序按我们想要的方式来,但是在多线程场景下,很多自己很难预计的到的事情会发生,导致数据的不安全,这时候我们会想到一些方法来解决数据不一致的问题:

  1. 避免数据不一致(ThreadLocal)
  2. 排队
  3. 投票

而加锁就是排队的一种实现方式。

Java锁都有什么?

Java的锁大概分为两种

  1. Synchronized
  2. Lock

从JVM角度看Synchronized

今天我们避过Synchronized关键字的三种使用方法,避开monitorenter指令和monitorexit指令,避开ACC_SYNCHRONIZED标志这些话题不谈,只说说Synchronized关键字到底做了什么。

本质上来说,Synchronized底层是基于Lock-Free队列的,我们从无到有再到优化来剖析一下Synchronized关键字。

首先我们必须要明确,在给对象监视器加上Synchronized关键字以后,当有多个线程同时来请求这个对象监视器的时候,对象监视器会将所有的线程分成几类来处理 大家可以想象一个场景:就是很多人同时去抢一个Offer的时候,会根据你的流程状态把面试人员分成很多批

  1. 竞争队列(笔面试流程中)

首先我们会把所有线程放进竞争队列,这个竞争队列严格意义上并不是Queue,而是一个基于Node和next指针的一个链表结构,所有入队新进线程会放在链表头节点的位置,而所有出队的线程则是在链表尾节点的位置进行CAS的出队操作,很明显这是一个Lock-Free队列,而能从竞争队列中拿走线程的线程只有Owner线程,Owner线程就是正在拿着锁的线程,它会选择合适的候选人线程,然后把它们放进Entry-List。

  1. Entry-List(备胎池)

就像某公司的录用排序中一样,进了该队列并不是说一定能拿到资源,让线程进去这个队列只是为了避免线程频繁的在竞争队列队尾冲突,然后进入录用池以后,会 (非公平) 随机的拿一个人的简历进行录用(也就是设置成Ready线程),这个线程如果拿到了Offer就变成Owner线程 (上岸) ,如果没有拿到那就回到录用池,碍于公平的情面,会把这个线程放在Entry-List的队头。

  1. WaitSet(考虑Offer的池子)

如果拿到Offer的人(Owner线程)说wait!wait!我要考虑一下,(调用wait()方法)那这个线程便会被扔进waitSet队列,当你考虑清楚以后会被重新塞进Entry-List进行流程。

  1. OnDeck(就是Ready线程)(口头Offer)

正在竞争锁的线程就是Ready线程(非公平)。

  1. Owner线程(拿到Offer的人)
  2. !Owner线程(毁Offer)

自旋锁

我们必须要想清楚一点,倘若将和HR交流/联系不上看作是用户态/阻塞态,竞争队列、备胎池、考虑Offer的池子这三个批次的人归根到底是没有Offer的,处于这些批次的人是处于阻塞态的(一般联系不上HR的),为了让自己的应聘流程没有那么慢,我们要经常催促HR,也就是轮询HR到底Offer轮不轮得到我,这个轮询周期非常值得考究,因为轮询会占住HR不放,所以当轮询很多次结果以后,会断开联系,也就是进入阻塞态。

翻译成锁就是,为了避免线程进入阻塞态,得不到锁的线程先自旋,但是自旋一段时间后如果获得不了锁,就进入阻塞态

当然面试过程一定是想要公平,却非公平的

不公平的地方在哪里呢?

经常询问HR的人可能会引起注意,直接被录取,这对一直在竞争队列排队的人不公平,甚至有可能直接抢走Offer,对处于口头Offer状态的人不公平

线程进入队列前先尝试自旋,如果直接获得锁,对等待队列的线程和Ready线程不公平

偏向锁

当然上面的面试场景一般都是大厂场景,很多小厂愿意去面试的人没几个,甚至只有你一个(开心吗?),当你第一次能面试过这家以后,可以再来面试, (可重入) ,再来面试总是能过的(当然我们的假设是别人不要面子),也就是无竞争下,希望你不要再走面试流程了,直接过就可以了,这时候就设计了偏向锁。

偏向锁直接去掉了进入流程 加锁/解锁 的过程,因为可重入锁虽然很好,但是加锁/解锁过程中设计的CAS操作其实是很影响性能的。

CAS操作为什么影响性能呢?

首先CAS在失败的时候的自旋操作会占住CPU资源,其次,CAS会造成一些本地延迟,因为在多处理器场景下,每个核会有自己的L1缓存,然后通过总线和主存连接起来,如果Core1改变了一些值,Core2拿到这个数据的时候数据会失效,最新的数据同步通信过程会产生缓存一致性流量,太大的缓存一致性流量会导致总线的压力太大,成为性能的瓶颈。

从JVM角度来看Lock

今天我们避开Lock的使用方法,Lock从其实现来看主要是通过实现Lock接口,而Lock接口的所有操作都放在了Sync类中,Sync类则是AQS的一个子类,所以其基本思想完全是继承自AQS的,那么AQS的思想又是什么呢?

浅谈AQS

AQS的基本思想是当线程请求一个资源的时候,如果资源空闲,就直接给这个线程,并锁住资源,如果资源已经被锁,则将线程加入一个基于阻塞的CLH队列,CLH队列是一个虚拟的双向队列,锁释放的时候会唤醒线程,AQS可以实现成独占式,比如ReentrantLock,也可以实现成共享式,比如信号量、读写锁、倒计时器等。

当我们每次调用Lock()方法的时候,默认的会进行非公平锁获取的方法,先会判断当前锁的状态,如果当前锁状态c==0,也就是空着的话,就直接获得该锁。然后该锁的acquire属性会+1,unlock()的时候会-1,当为0的时候为空,如果当前锁状态不是0,要判断是否是自己占有锁,如果是自己的话,就给值++,避免CAS操作,也就是实现了偏向锁。

得不到锁的线程会被包装成Node调用addWriter()进等待队列,如果有队尾的话,会CAS将当前线程更新为队尾,如果没有队尾的话,就循环CAS知道加入队尾,addWriter()返回的线程进行阻塞,阻塞前先尝试tryAcquire()能否获得锁,每个节点根据询问前继节点是否阻塞来决定是否阻塞。

说了这么多,我们就来比较一下Synchronized和Lock锁吧

  • Synchronized

这是一个基于Lock-Free等待队列的关键字,JVM分的更加仔细,将等待队列分成了好几个部分,为了加快出列的速度,并且Synchronized实现了自旋锁,但这是基于JVM的指令实现的。

  • Lock

这是一个基于阻塞的CLH等待队列,队列内的所有操作都是基于CAS的,并且对已经获得了锁的线程可以实现偏向锁,但是并没有实现自旋锁,只能僵硬的等待,好的一点是Lock更适应于扩展,可以扩展成读写锁、公平锁、非公平锁等,另外区别于wait/notify()机制的是Condition机制更加的灵活。

Synchronized 和 Lock 锁在JVM中的实现原理以及代码解析

原文地址:https://www.cnblogs.com/alkaid96/p/13160064.html