锁
多个线程或多个进程在同时改变某个共享变量时,需要对变量或代码块做同步(锁),使其在修改这种变量时能够线性执行
怎么加锁
CAS:(原子操作)将预期值与内存实际值比较,当相等时,把内存实际值修改为期望值
CAS 的 ABA问题,内存值 0 ,+1,再-1 ,再进行CAS操作比较的时候,是符合的,单实际上该内存值已经改变过了
原子操作:不会被线程调度打断的操作
Redis的操作之所以是原子性的,是因为Redis是单线程的。
重量级锁:JVM在JAVA1.6以前使用操作系统自身的互斥锁(重量级锁),这种锁需要操作系统内核态与用户态来回切换比较耗时间。
自旋锁:CAS比较后不符合,循环比较
自适应自旋锁:自旋时间短的,更容易自旋,时间长的,更少自旋甚至跳过
轻量级锁:不申请互斥量,只有在大于两个线程发生锁竞争后才改为重量级锁,在那之前,仅仅将Mark Word中的锁状态字节CAS更新为锁记录的地址(指针),如果更新成功,则轻量级锁获取成功
偏向锁:不进行CAS更新指向…,只有在两个线程发生锁竞争后才改为轻量级锁,仅仅将Mark Word中的偏向锁标志置1,如果更新成功,则偏向锁获取成功。
总结上面几种锁的区别,锁越轻量,消耗越小,操作越轻微,当发生线程竞争后不断向更重量的锁去变化。
java对象头里一般包含,HashCode、GC分代年龄、锁状态标志、是否偏向锁标记位、偏向线程ID
上文的Mark Word对应这里的 锁状态、锁标志、是否偏向锁标记位,共占一个字节,32位系统是32位,64位系统是64位。
由此可知,一般锁的实现是采用一种具有bool性质的数据做标志,比如某个数据的有无,比如某段内存是否指向某段地址,比如对象中的锁标志位置1置0,比如因为redis里的key是唯一的,一个 key的有和无 ,比如因为数据库表里的主键是唯一的,一条记录的有和无,所以只要能实现标志加和 解锁两种状态的方式都可以实现加锁。
多线程锁
死锁
死锁分为两种,
-
一个线程获取到锁,没释放的时候再次获取该锁,造成死锁
可以通过可重入锁避免
可重入锁:线程获取一个资源的锁,可以再次获取该资源的锁,建立一个标志数,每获取一次,标志数加一,每释放一次,标志数减一,标志数为0,解锁 -
线程a获取资源1的锁同时等待资源2,线程b获取资源2的锁同时等待资源1,这样线程a和b进入死锁
可以通过修改资源访问顺序的方式避免死锁,把线程a和b都修改为先获取资源1再获取资源2
基本所有的死锁问题都可以通过加一个锁失效时间来解决
活锁
是一系列线程在轮询地等待某个不可能为真的条件为真。活锁的时候进程是不会blocked,这会导致耗尽CPU资源
如线程a从队列中取出任务1来执行,如果任务执行失败,那么将任务重新加入队列,继续执行。假设任务总是执行失败,那么线程一直在繁忙却没有任何结果
如线程a请求资源1,如失败请求资源2,线程b用同样的逻辑请求资源1,2,a和b不断的同时失败,同时改变逻辑,再同时失败…
可以通过加随机等待时间,如果检测到冲突,那么就暂停随机的一定时间进行重试解决
处于活锁的实体是在不断的改变状态, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
悲观锁
代码里每次数据操作都加锁,同时只能有一个线程操作数据
乐观锁
乐观锁与CAS类似,代码不加锁,默认不会出现资源竞争问题
- 为表中加一个 version 字段;
- 当读取数据时,连同这个 version 字段一起读出;
- 数据每更新一次就将此值加一;(此种方式可以避免ABA问题)
- 当提交更新时,判断数据库表中对应记录的当前版本号是否与之前取出来的版本号一致,如果一致则可以直接更新,如果不一致则表示是过期数据需要重试或者做其它操作
分布式锁
CAP :一致性(Consistency):各个分布式系统数据是否一致、可用性(Availability):各个分布式系统功能是否齐全。分区容错性(Partition tolerance):分布式某个系统挂掉了,系统是否还能继续用,最多只能同时满足两项。
为了保证数据的最终一致性,需要用分布式事务、分布式锁等
分布式事务
单进程事务一般使用注解就可以,分布式系统基本都是保证最终一致性,可以采用
- 补偿机制
程序员编写补偿代码,在分布式系统事务操作进行异常捕捉,然后根据是否异常,确定是否进行补偿
它分为三个阶段:
Try 尝试
Confirm 提交,只要Try成功,Confirm一定成功。
Cancel 回滚或业务取消 - 本地消息表
生产者A新建一个消息表,当要进行事务操作时,存一个带状态的消息到消息表,利用MQ发给消费者,消费者B事务成功,则修改消息表的状态为成功,A定时检查消息表,把失败的事务重新发送,直到状态被B改为成功,对于一直不成功的设置一个次数限制,超过限制了A的事务操作进行回滚。
分布式锁
分布式锁研究的是多个进程之间的锁,一般在多线程中,可以采取内存中的对象上的锁标志加锁解锁和标记要锁的数据的地址(指针),但是多线程,一般互相的内存是不互通的,所以要采取使用第三方的工具
一般可以采取
- 乐观锁(版本号)的方式
- 利用 Redis Key唯一性(如下单系统用每个订单的编号做key,下单成功存入Redis,如果该key存在,则说明锁着,其余进程无法操作该订单)
- 数据表的主键唯一性等(同上)
方式实现分布式锁