前言:
在说锁之前,先聊一下,什么是锁,什么是锁资源,什么是保护资源。
其实,锁是一种概念,是用来在多线程环境中让某一资源同一时间不会被多个线程访问的手段。类比现实,锁其实就是 “防盗” 这个理念;
锁资源,也有人叫锁对象。它其实就是用来实现锁这个概念的对象,类比现实,就是为了防盗而买的一个物理上摸到的门锁;
保护资源,就是我们要用锁来实现保护的资源,比如一段代码,一个VO,数据库的一条数据。
具体看一下 java synchronized,java Lock, redssion 的Lock 这三种锁使用中,锁资源 保护资源都是什么。
对于Lock锁,其实new 出来这个lock对象就是一个锁对象
对于synchronized,有多种用法,但无论哪种用法,本质还是要把一个对象作为锁对象
synchronized(obj) 将obj对象当锁对象
synchronized(this) 将this指针指向的对象当锁对象
synchronized 将当前的类的class对象当锁对象
Redssion的lock将redis中对应key的值查出来,当锁对象
一.经常提到的锁的概念
1.重量级锁:线程间的互斥依赖于操作系统,没有获取到锁资源的线程会进入阻塞,同步过程重会涉及到线程用户态和内核态之间的切换,此种方式操作系统的开销大,故而称为重量级锁。
2.自旋锁:当线程竞争资源失败时,不直接阻塞自己,而是通过自旋(例如,一个的while循环中不断判断自己能否获取资源,这种判断通常会使用CAS)的方式来继续等资源
3.偏向锁:java中对象头有一个markWord字段,所谓偏向锁,就是让这个东西直接指向获取了当前资源对象的线程
以上三种锁是线程在争抢锁的时候的三种不同处理方式,可以说它们是有具体实现逻辑的锁定。还有一些我们常说的锁不一定指代某一种具体实现,而是一种方案,比如
a.公平锁,非公平锁:首先,公平锁和非公平锁是一种概念,公平锁指的是,争抢资源的线程是否会排队等待,而非直接抢占资源,非公平锁则相反,争抢资源的线程会先去尝试获取资源,获取不到的情况下才会排队等待
b.乐观锁,悲观锁:乐观锁就是认为访问资源时候冲突概率很低,通过对比资源的版本号或者时间戳之类的标识就可以判断出是否有冲突,具体的实现可以自己手写代码,也可以利用CAS机制。
悲观锁则是认为资源访问时候冲突概率很低,在访问之前要对资源进行可靠的同步,如利用.重量级锁 进行锁定资源等
c.阻塞锁,非阻塞锁:与上诉的a,b不同。阻塞锁,非阻塞锁也是两种具体的实现方式,但又与最初的1,2,3中提到的三种锁不同,阻塞锁,非阻塞锁的关注点不在如何获争抢资源上,而是资源
争抢失败的线程应该怎么办。
阻塞锁,就是争抢失败的线程要去进入阻塞态进行等待,当资源再度空闲时候,处于阻塞的线程接到通知,从新进入激活状态,然后再争抢资源。
非阻塞锁就是说,线程不去等待,可以由我们的代码控制一个循环,每次争抢失败后等一下再继续尝试抢占资源,也可以抢占失败直接就放弃不抢
二.CAS是什么
1.cas的含义
CAS(Compare And Swap) 比较 和 交换 **务必记住这个的英文,记住这个的英文全称,其实就理解了CAS**
从名字可以知道,CAS是一种思想或者说套路,是比较,交换两个步骤,它并不是一种具体的锁的实现方式。强调:CAS不是一种具体的锁实现!它只是一种比较+交换的思想
2.cas的具体实现
cas是比较和交换两个步骤,CAS中包含三个要素 (对象地址,预期值,修改后的新值) ,其原理就是,取地址对应的对象,判断对象的值是否等于预期值,如果等于,则将新值付给对象
其伪代码如下:
boolean CAS(addr, oldValue, newValue) {
if &addr == oldValue
&addr = newValue;
}
这段中,我们可以看出,其实如果我们自己写了代码,在 if 和实际执行之中,还是可能插入其他线程的时间片,导致if判断和赋值出现线程不安全。
所以,我们常说的线程安全中的CAS实现,是基于 硬件级的支持,是硬件支持对应的原子性的指令才能有CAS这种机制!强调:是有硬件支持,保证if和赋值之间的原子性,才有了CAS
3.java中使用CAS的案例
java中的Lock,AtomicXXX(比如AtomicInteger),都使用了CAS的方式。
AtomicInteger的底层调用了unsafe类的compareAndSwapInt,unsafe底层则是native方法,继续深入依旧是会使用硬件的支持达到CAS的效果
Lock同样也是调用了unsafe类的方法
三.java中的synchronized关键字的实现原理以及锁升级
1.前置知识:什么是Monitor对象,java对象头中部分内容
a.对象头:java中,为了实现类的一些增强功能,对象头会存放一些信息。我们不展开的去细看,只了解到对象头有一些数据,叫运行时元数据(Mark Word),
而在这个Mark Word中,又有锁状态标识,线程持有的锁,偏向线程ID这些信息。
b.Monitor:可以理解Monitor也是一个对象,java中每个对象可以关联一个Monitor。Monitor中有三个最重要的东西,分别是waiSet,EntryList,Owner。
给对象上锁之后,对象的mark word中,就有一个指针指向Monitor(用java的思想来理解,就是通过对象头能访问到一个Monitor对象)。Monitor中的Owneri表示
哪个线程得到了资源,没有得到资源的线程则会进入EntryList,而如果线程调用了资源对象的wait,则线程进入waitSet并从Owneri中退出。EntryList中的线程会在
owner空出的时候争抢资源,waiSet中的则不会,只会等待notify
2.synchronized重量级锁的实现原理
了解了Monitor,其实也就了解了重量级锁,java的重量级锁就是依赖于Monitor实现。java对象中,对象的mark word中有一个指针指向一个Monitor对象,当对象
被synchronized时候,如 synchronized(Object)。多个线程执行这段代码,成功获得了Object对象的线程,进入Monitor的owner中,没有获得的,进入Monitor的
EntryList中。此过程涉及到线程状态变更,更底层还涉及到用户态内核态的转换,故而称为重量级锁
3.synchronized锁升级过程
最低级:偏向锁,即在线程的markWord中记录一下哪个线程拥有了它
升级:自旋锁,当多个线程开始竞争资源,就会利用CAS机制继续尝试修改锁资源状态,没有得到资源的线程会使用以自旋的方式继续CAS
再升级:重量级锁,当资源争抢达到一定量,对象的锁会升级为重量级锁定,即使用Monitor那一套
**注**:有人把升级一次之后称为轻量级锁,我认为这种说法其实也没错,其实轻量级锁更多是一个概念,它表示没有通过切换线程状态就达到了线程安全目的锁实现方式
四.java中的Lock
lock锁如果希望详细了解,建议看源码,这里为了节省大家的理解时间,将lock锁的原理以一层层疑问解答的方法列出
a.lock锁和synchronized有什么不同?lock的具体实现有哪些?
从实现层面来讲,首先,lock是一个接口,它的实现有ReentrantLock,read/writeloc,它的底层使用AQS(AbstractQueuedSynchronizer)这种同步器和CAS机制进行加锁和线程管理
而synchronized从我们之前讲的可以知道,是通过对象的头信息及Monitor机制来完成加锁和线程管理
b.Lock的lock和tryLock有都是加锁,他们有什么区别?
tryLock()会直接返回获取锁的结果,如果获取到了锁,直接返回true,没有的话则false,它属于一种乐观锁的思想,一般我们写一些代码,希望不要阻塞可以使用tryLock(),甚至可以自己
使用tryLock()来实现一个自旋锁,比如
while(!tryLock()) { } ;
这段简短的代码中,tryLock尝试获取锁,没有得到返回false,一直在while中自旋等待。
lock()方法没有返回值,如果lock没有获取到锁定,会阻塞线程,它属于一种悲观锁的实现。
c.Lock和AQS是什么关系?
AQS(AbstractQueuedSynchronizer)是java的一个抽象类,它有如下几个特别重要的东西:
队列:AQS内部实现了一个双向队列,队列的每一Node结构简单理解为 pre| Thread对象 |next 。即数据域存放一个线程,有左右指针的一个队列。
acquire()方法:尝试获取资源,如果失败,则把线程置为阻塞加入队列
release()方法:释放资源,并通知队列中的线程,唤醒线程继续争抢资源
Lock的lock()方法则是借助AQS来完成的,以ReentrantLock为例,它的内部有一个对象叫 Sync,这个对象就继承了AQS,它的仍是一个抽象类,它的两个实现分别是FairSync,NonFairSync
分别以公平锁和非公平锁的思想实现了lock()方法,而他们的lock方法中,都是调用AQS的acquire方法
d.刚提到了公平锁和非公平锁,他们有什么不同
没啥不同,非公平锁调用acquire方法之前先尝试修改资源状态从而获取资源了一下,而公平锁直接就调用acquire了
e.一张图看Lock()
五.分布式锁
1.分布式锁的实现原理
说分布式锁之前,回顾一下锁,我们可以简单的说,实现一个锁要考虑两个事情
a.如何保证多个线程争抢锁资源时,只有一线程能获取到资源?
b.没有获取到线程的资源怎么办
想要实现a,又需要两个,第一,这个锁资源能被每一个线程都访问到,第二,有一个办法,能够让我们将 查询比较和修改 两个操作原子性的执行,在这两步之间不能让别的线程插进来。
单机情况下,达成第一个目标只需要随便定义一个变量,就能让所有线程访问,第二个目标则可以依靠CAS思想,借助硬件的支持完成。
但是在分布式中,多个服务在不同服务器,所以,想要达成第一个条件,就需要一个每个服务都能访问到的公共组件,而要达成第二个条件,则还需要这个组件能够支持原子性的
查询比较+修改操作。我们常见到的组件中,mysql,redis,Zookeeper都能做到这两点,所以,常见的分布式锁也就基于他们实现。注意,是因为他们能做到这两点才用他们做分布式
锁定,而不是只有他们才能做分布式锁,如果我们自己写一个服务,其他服务能访问这个服务,这个服务有一个接口,能原子性的支持其他服务对本服务中的某一个对象 查询比较+修改,
那么用我们这个服务,也完全能做分布式锁。
再说b,如果要实现的是一个非阻塞锁,即没有得到锁资源的线程直接拿到一个false返回值,该干啥干啥。但是如果要实现阻塞锁,则要考虑如何存放阻塞的线程,如果在锁释放
后唤醒被阻塞的线程继续争抢锁。单机中,我们依靠AQS或者monitor。但是,分布式中,则要考虑到,每一个不同服务器上的线程都可能阻塞,所以,每一个服务器都需要管理被阻塞
的线程,以及锁资源释放后,需要广播通知每一个服务器,唤醒阻塞的线程
2.redssion中的分布式锁
redssion的分布式锁是最常用的分布式锁,通过上面的理论,已经知道了实现锁会遇到问题,以下通过看redssion如何解决这些问题,从而理解redssion的锁
a.如何定义一个锁资源?
在redis里面定义一个变量就行了,这个变量所有服务都能访问,所以它可以做锁资源
b.如何确保同一时间只有一个服务的一个线程能获取锁资源?
redis支持setnx这种原子操作,也能原子性的执行一段lua表达式,redssion借用这个来实现了一个CAS,即原子性的 查询比较+修改变量
c.redis的对象是有存活时间的,redssion是如何保证锁对象生命周期大于锁保护的业务的执行时间?
redssion中有一个叫看门狗的机制,会自动给锁资源对象续期
d.redssion的lock方法如何处理没有拿到锁的线程?已经如何在锁释放后通知线程
redssion中有一个对象叫AsyncSemaphore(信号量模型),这个对象内部有一个队列,一个计数器,这东西类似单机的Lock锁中我们说的AQS,redssion在加锁时候调用AsyncSemaphore
的acquire,没有抢到锁的线程会进入队列等到。同时,redssion会启动一个channal用于沟通redis,当redis中的锁资源释放,则redssion得到通知,从而告知本服务中的等待的线程。