锁策略及优化总结

发布时间:2024-12-23 22:41

定期复盘总结,不断优化交易策略。 #生活技巧# #理财投资建议# #炒汇技巧#

锁策略

锁在加锁/解锁/遇到锁冲突的时候,都会怎么做。

常见锁策略:

悲观锁vs乐观锁:

加锁的时候,预测当前锁冲突的概率是大还是小:

预测当前锁冲突概率大,后续要做的工作往往就会更多.加锁开销就更大(时间,系统资源)=>悲观锁.预测当前锁冲突概率不大,后续要做的工作往往就更少.加锁开销就更小(时间,系统资源)=>乐观锁.

重量级锁vs轻量级锁:

一般来说,悲观锁往往就是重量级锁-》加锁过程做的事情多,重量

一般来说,乐观锁往往就是轻量级锁-》加锁过程做的事情少,轻量.

实际交流过程中,这两组概念可能会混着用,悲观乐观,是先预测一下, 锁冲突概率高还是低,又因为,一般锁冲突概率高的时候就得多做处理.

自旋锁vs挂起等待锁:

自旋锁,是轻量级锁的一种典型实现方式:

void lock(){

while (true) {

if (锁是否被占用) {

continue;

}

获取到锁

break;

}

}

cpu在空转,忙等,消耗了更多的cpu资源,但是一旦锁被释放,就能第一时间拿到锁,拿到锁的速度更快,消耗cpu。

挂起等待锁,是重最级锁的一种典型实现方式:

借助系统中的线程调度机制,当尝试加锁,并且锁被占用了,出现锁冲突,就会让当前这个尝试加锁的线程被挂起. (阻塞状态),此时这个线程就不会参与调度了,直到这个锁被释放,然后系统才能唤醒这个线程,去尝试重新获取锁.

消耗的时间更长,一旦线程被阻塞 了,啥时候被唤醒,这个过程是不可控的,可能会经历很长很长的时间,拿到锁的速度更慢,节省cpu。

synchronized轻量级锁部分,基于自旋锁实现;重量级锁部分,基于挂起等待锁实现。

可重入锁Vs不可重入锁:

synchronized就是可重入锁,一个线程针对这把锁,连续加锁两次不会死锁.

比如C++的std:mutex就是不可重入锁一个线程针对这把锁连续加锁两次,会死锁.

公平锁Vs非公平锁:

公平锁:严格按照先来后到的顺序来获取锁,哪个线程等待的时间长,哪个线程就拿到锁.

非公平锁:若干个线程,各凭本事,随机的获取到锁,和线程等待时间无关了。

synchronized属于非公平锁,多个线程,尝试获取这个锁,此时是按照概率均等的方式来进行获取的。

互斥锁vs读写锁:

synchronized本身就是普通的互斥锁 :加锁 和解锁

读写锁,则是-一个更特殊的锁:加读锁 加写锁, 解锁:

        java的读写锁是这样设定的:

        1)读锁和读锁之间,不会产生互斥

        2)写锁和写锁之间,会产生互斥

        3)读锁和写锁之间,会产生互斥

        突出体现的是"读操作和读操作"之间是共享的(不会互斥的),有利于降低锁冲突的概率,提高并发能力。

synchronized优化

synchronized实现原理:

synchronized即使悲观锁,也是乐观锁,既是轻量级锁,也是重量级锁.轻量级锁是自旋锁实现,重量级锁是挂起等待锁实现,是可重入锁不是读写锁,是非公平锁.

synchronized的优化策略

1、synchronized的"自适应":

锁升级的过程:

1)未加锁的状态(无锁)

代码中开始调用执行synchronized,升级

2)偏向锁

遇到锁冲突,升级

3)轻量级锁

冲突进一步提升,升级

4)重量级锁

首次使用synchronized 对对象进行加锁的时候,不是真的加锁.而只是做一个“标记”(非常轻量非常快,几乎没有开销)

如果没有别的线程尝试对这个对象加锁,就可以保持这个状态,一直到解锁. (解锁也就是修改一下上述标记,也几乎没有开销)

但是如果在偏向锁状态下,有某个线程也尝试来对这个对象加锁,立马把偏向锁,升级成轻量级锁(真的加锁,真的有互斥了),也可以保证锁能够正常生效。

本质上,偏向锁策略就是“懒"字具体体现.能晚加锁,就晚加锁.

上述的升级过程,针对一-个锁对象来说,是不可逆的,- -旦升级到了重量级锁,不会回退到轻量级锁. (当前JVM里面的做法)

2、锁消除

代码里写了加锁操作,编译器&JVM会对你当前的代码做出判定,看这个地方到底是不是真的需要加锁,如果这里不需要加锁,就会自动的把加锁操作,给优化掉,最典型的,就只在-个线程里, 使用synchronized,由于编译器优化,需要保证优化后的逻辑和优化前是要等价的,这里做的是比较保守的.能够起到的作用有限的,这个事情和前面谈到的°偏向锁"互不相干,也不冲突的.

3、锁粗化

加锁的范围内,包含多少代码,包含的代码越多,就认为锁的粒度就越粗

有些逻辑中,需要频繁加锁解锁.编译器就会自动的把多次细粒度的锁,合并成- -次粗粒度的锁.

cas

compare and swap比较和交换:这是一条cpu指令,就可以完成比较和交换这样的一套操作下来.

cas的流程(想象成一个方法):

boolean cas(address, reg1, reg2) {

if (*address == reg1){

把address内存地址的值和reg2寄存器的值进行交换

return true;

}

return false;

}

*address=>获取内存地址

这里说是"交换",实际上更多的是用来"赋值",一般更关心内存中, 交换后的数据,而不关心reg2寄存器里交换后的数据,此处也可以近似的认为,上述操作就是把reg2的值赋值给内存中。

由于CPU提供了.上述指令,因此操作系统内核,也就能够完成上述操作,就会提供出这样的CAS的api

JVM又对于系统的CAS api进-步的封装了,在java代码中也就可以使用CAS操作了.

ABA问题:

使用CAS编写代码比较然后再交换,

在比较时,检查当前内存的值,是否被其他线程修改了,如果被修改了,就要稍后再重试,如果没被修改,接下来就可以直接修改. (不会有线程安全问题的,没有其他线程穿插执行)

值没变!=值没变过

可能另一个线程把这个值从A-> B,又从B-> A,ABA在大部分情况下,没啥事,但是在披端情况下,就可能会产生bug。

ABA问题的典型Bug场景:

balance是当前账户余额,oldBalance和oldBalance-500 都指的是寄存器在取款的过程中,发生bug了,按了一下取款,卡了一下,紧接着又按了一下,此时,产生了两个线程,来执行上述扣款操作.

具体操作如下图:

若t1还没执行到 if 时,有一个人给账户转了500,产生线程t3,具体操作:

上述就是属于ABA问题的典型bug场景,属于非常极端的情况

如何避免ABA问题

核心思路是,引入“版本号”,约定版本号只能加,不能减,每次操作余额,版本号都要+1,通过CAS判定版本号,就可以进一步的来完成上述的操作。

如果版本号没有改变,数据就一定没有变过。

通过上述CAS已经判定了,同一时刻只有一个线程能修改.

其他线程并发执行的时候,无法进入if条件.确保balance不会出现多线程同时修改的情况.

网址:锁策略及优化总结 https://www.yuejiaxmz.com/news/view/548822

相关内容

最优化算法——常见优化算法分类及总结
网络优化总结
性能优化十条策略
小米 14 系列手机获推澎湃 HyperOS 1.0.42.0:新增出行助手、优化锁屏编辑触发策略
数码产品关键词的重要性及优化策略
PyTorch 节省显存的策略总结
优化学习的18个策略
酒店七夕活动策划方案总结(优秀15篇)
Windows服务器最长稳定运行时间,记录及优化策略
小学数学购物策略教学方法总结

随便看看