synchronized的底层实现原理

同步锁。可以修饰实例方法(当前实例加锁) 修饰静态方法(当前类加锁),修饰代码块(指定加锁对象)

使用synchronized编译后的字节码文件中会有monitorenter和monitorexit指令。分别对应着获取锁和释放锁。

而每一个同步对象都有一个自己的Monitor监视器锁。加锁的时候,会先尝试获取monitor的所有权,

  • 如果monitor的进入数为0,则将进入数设置为1并进入monitor。该线程即为monitor的所有者
  • 如果线程已经占有该monitor,那么就重新进入,将进入数加1
  • 如果其他线程占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0。重新获取monitor的所有权

如果monitorexit出现了两次,第一次为同步正常释放锁,第二次为发生异常退出锁

Synchronized和Lock的区别

Synchronized是java的关键字,而Lock是一个接口

synchronized不会产生死锁,而lock必须在finally中释放锁,不然容易造成死锁

synchonized不可以判断锁的状态,lock(通过trylock)可以判断

synchonized可重入,不可中断,是一个非公平锁 lock则是可以自行调整

Thread.sleep()和Object.wait()的区别

Thread.sleep不会释放锁、Object.wait会释放锁

Thread.sleep到了时间自动唤醒、wait如果没有指定时间,则必须手动使用notify唤醒

如果wait设置了时间并到了时间被唤醒,如果获得了锁,则继续执行,没获得锁则进入锁池等待

锁升级

Java 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了 “偏向锁” 和 “轻量级锁”:锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。

偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入同步块时先判断对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果存在就直接获取锁。

轻量级锁:当其他线程尝试竞争偏向锁时,锁升级为轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,标识其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

重量级锁:锁在原地循环等待的时候,是会消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循环反而会消耗CPU资源。默认情况下锁自旋的次数是10 次,可以使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。10次后如果还没获取锁,则升级为重量级锁。

发生死锁怎么办

一般发生死锁的原因:系统资源竞争 进程推进顺序非法

产生死锁的四个必要条件:

​ 互斥条件: 一个资源每次只能被一个线程使用

​ 请求与保持条件: 一个线程因请求资源而堵塞时,对自己获得的资源保持不放

​ 不剥夺条件 进程已经获得的资源,在未使用完之前,不能被其他线程强行剥夺

​ 循环等待条件 若干个线程形成一种头尾相接的循环等待资源关系

那么如何避免死锁呢?我们可以针对加锁顺序,让程序按照我们指定好顺序执行。还有加锁时限,也就是线程获取锁的时候加上一定的时限,超过这个时限,放弃请求并释放自己的锁。

CAS(Compare And Swap)

CAS,比较并替换。有三个操作数,内存地址V 旧值A 新值B

CAS执行操作的时候,只有当内存地址V的值和旧值A的值相等时,才将V修改为B,否则什么都不做,是一个原子性操作

为什么是原子性的呢?

  • 因为CAS是一种系统原语。系统原语属于操作系统用于范畴,由若干个指令组成。由于原语的执行必须是连续的,不允许被打断,所以具有原子性操作

synchronized和CAS区别

  • synchronized加锁,同一时间只能有一个线程访问,一致性得到了保障,但是降低了并发性。而CAS用的是do whilte,没有加锁,反复通过CAS比较,直到成功。既保证了一致性又提高了并发性。

CAS三大缺点

  • 循环时间长:

    CAS一般和死循环配合使用,如果CAS失败,会一直尝试,如果一直不成功,会给CPU带来很大开销

  • 只能保证一个变量的原子操作:

    对一个变量进行操作的时候,我们可以通过循环CAS来保证原子操作。

    但是多个不可以保证操作的原子性。可以通过使用互斥锁来保证原子性或者使用AtomicReference

  • ABA问题:

    假设我们现在要修改内存值2为4,第一步我们查出来A的值,然后发现和旧值A是相等的。准备修改值为4,就在这时候,来了一个操作,将2改为4又改为2.CAS会判断值没有变,修改完成。但其实这个值已经被变了

AtomicStampedReference与AtomicMarkableRederence类

为了解决ABA问题。java提供了以上2个类。

AtomicStampedReference是利用版本戳的形式记录了每次改变以后的版本号

举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。这就是AtomicStampedReference的解决方案。

AQS

AbstractQueuedSynchronizer(AQS)是JDK实现并发编程的核心,ReentrantLock就是基于AQS实现的。AQS是一个多线程同步器,是JUC包多个组件的底层实现。有三个特点。

第一点,AQS提供了2种锁机制(共享锁/排他锁)。排他锁就是重量级锁。共享锁也是读锁。表示资源可以同时被多个线程锁读取。

第二点,AQS提供了可重入锁。也就是获取到锁资源的线程可以再次获取锁。

第三点,锁竞争的公平性和非公平性。也就是公平锁和非公平锁。AQS内部有一个核心变量state(volatle修饰)。表示锁的状态。 初始状态下是0。

如果现在有一个线程调用了lock方法进行加锁。 State会被设置为1,同时AQS内部还有一个关键变量,用来记录加锁的是哪个线程。默认值null。加锁的过程是将state的值从0变为1。同时将加锁线程设置为自己。如果没有人加过锁的话,到这里就加锁成功了。

reentrantLock是一个可重入锁,也就是对同一个锁加多次。每一次重入加锁,会先判断当前线程是不是自己,是自己的话进行重入加锁,将state的值累加1.

如果说线程A加锁成功。线程B进来加锁会发生什么? 首先线程B会查看锁的状态state,发现是1,接着去看加锁线程,发现也不是自己。那么加锁就会失败。但是并没有结束。线程B会将自己放入队列中等待,使用的是CAS机制。等待线程A释放锁,自己就可以重新尝试加锁了。这个地方叫锁池。 如果线程A执行完了自己的业务逻辑,就会释放锁。首先将state的值-1.如果不是0的话,说明自己还持有锁。继续执行逻辑。直到减到0。这个时候真正释放锁。同时加锁线程设置为null。这个时候线程B就尝试进行加锁。如果没有其他线程进行竞争。线程B当然可以加锁成功。将state变为1 线程变量设置为自己。将自己从锁池中移除。加锁成功

分布式锁

数据库分布式锁: 也就是锁表,我们可以创建一个专门用来记录锁的状态一张表。获取锁插入一条数据。逻辑执行完成删除这条数据。通过这条数据来获取锁

悲观锁的实现,需要先把MySQL的自动提交给关闭了。set autocommit=0就可以。然后查询语句可以后面跟for update 来锁定。被锁定的数据必须等待本次事务执行提交之后才可以被操作。保证了数据不被其他事务修改。但是我们必须要基于索引来实现。也就是说必须走索引。如果不走索引的话,会把查询时扫描到的其他行全部上锁,极大的影响了效率。

乐观锁的实现,我们可以给数据初始化的时候指定一个版本号,每次对数据操作都对版本号+1 并判断当前数据是不是该数据的最新版本号。比如基于乐观锁的SQL语句就是:

redis的分布式锁

redisson其实就是控制分布式系统不同进程共同访问共享资源的一种锁的实现 保证一致性,一般使用场景有秒杀下单,抢红包这种高并发的场景。

分布式锁具有的特征:

互斥性: 任意时刻,只有一个客户端可以持有锁

锁超时释放: 设置超时时间,可以防止不必要的资源浪费,同时防止死锁

可重入性 一个线程如果获取了锁之后,可以再次对其请求加锁

高性能和高可用: 加锁和解锁开销尽可能低,同时保证高可用 避免分布式锁失效

安全性: 锁只能被持有的客户端删除,不能被其他客户端删除

通过setnx实现。如果加锁成功返回1

redis红锁

假设我们有N个redis主节点,这些节点是完全独立的。我们不使用复制或任何其他隐式卸掉系统。为了取到锁。客户端要做一些操作。

  1. 获取当前时间,以毫秒为单位
  2. 依次尝试从5个实例,使用相同key和随机值获取锁。获取锁的尝试时间要远远小于锁的超时时间。访问某个masterDown了。
  3. 只要大多数节点获取了锁,而且总获取时间小于锁的超时时间的情况下,认为锁获取成功
  4. 如果锁获取成功 锁的超时时间就是最初的锁超时时间减去获取锁的总耗时时间
  5. 如果锁获取失败 不管是因为获取成功的数目没有过半,还是因为获取锁的耗时时间超过了锁的释放时间,都会将已经设置了key的master上的key删除