锁策略和Synchronized优化机制

文章详细介绍了各种锁机制,包括乐观锁与悲观锁的概念及其应用场景,自旋锁与挂起等待锁的区别,以及轻量级锁和重量级锁的权衡。此外,还讨论了读写锁在多线程环境中的重要性,以及synchronized的优化机制,如偏向锁、轻量级锁和重量级锁的转换。
摘要由CSDN通过智能技术生成

 1. 乐观锁vs悲观锁

乐观锁:乐观锁一般不会产生并发冲突,在数据提交进行更新的时候(即将出结果),才会正式对数据是否产生并发冲突进行检测,如果发现了并发冲突就返回错误信息,让用户自己决定下一步该怎么办。

悲观锁:总是假设每次取数据的时候别人都会修改,所以在每次准备拿数据的时候都会上锁,直到自己释放锁。

例子:

        假设有两个同学需要找理发师剪头发,A同学是很乐观的,他每次去找的时候不会提前预约理发师,去了理发店如果理发师没有在忙,自己就去把头发剪了,如果理发师在忙,自己就出去溜达一圈,回来再看理发师是否在忙。

B同学很悲观,他认为自己不约的话,理发师永远不会闲下来给自己剪头发,所以每次去剪头发之前都跟理发师在三约定,确保自己不白跑一趟。

这两种策略没有谁好谁坏,只是在不同场景下是否合适,倘若理发师确实很忙,那悲观锁会减少白跑的概率,如果理发师不忙,乐观锁可以减少预约的浪费。

2. 自旋锁vs挂起等待锁

自旋锁理解起来是很像乐观锁的,如果在获取锁失败之后,立即在尝试获取锁,无限循环,知道获取锁为止,好处是锁一旦被其他线程释放,自旋锁可以第一时间获取到锁。

挂起等待锁: 在一次尝试获取锁失败之后,不继续尝试获取锁,而是将获取锁的行为挂起,等待其他线程通知。

用一个例子理解:

    假设我现在要追一个女生,她现在有对象,我去向她表白但是被发好人卡。接下来(假设不会被讨厌):自旋锁:隔一天又表白,重复不断,直到我追到她。挂起等待锁:我不管了不追了爱咋咋,等到很久之后这个女孩可能发来消息:“试试?”。。。

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

优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.

缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是不消耗 CPU 的).

synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的

3. 轻量级锁vs重量级锁

    先举一个例子:我在学校试图点外卖,有两种取法:一种是自己去拿,方便快捷但是需要自己动,一种是等工作人员给你送上门,但是你并不知道他送的时候是不是只给你一个人送,也不知道他除了送饭还有什么事情要去做,完全不知道什么时候会来,这个过程里你会不会饿死。

    在锁的核心特性“原子性”里CPU提供“原子操作指令”,操作系统基于原子性指令实现“mutex”互斥锁,jvm根据操作系统提供的互斥锁实现了synchronized等关键字和类。

此时轻量级锁就相当于自己去拿外卖一样,尽量不依赖工作人员,重量级锁就相当于需要重度依赖工作人员(mutex)实现。

顾名思义:轻量级锁加锁尽可能在用户态完成,不依赖系统自行调度,重量级锁需要系统调度协助,消耗较大。

4. 读写锁(readers-writer lock)

        多线程之间,数据的读取不存在线程安全问题,但是数据写的时候以及写和读的时候都需要进行互斥。但是如果两种情况下都用同一个锁,就会产生性能的损耗。读写锁因此需要产生。

读写锁区分读和写操作,Java提供ReentrantReadwritelock类,实现读写锁。

ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.

ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁.

其中读之间不互斥,写锁之间互斥,读和写锁之间也互斥。

读写锁特别适合“频繁读,不频繁写”的场景中,比如某教务系统,每天浏览量巨大,但是写入数据的次数不多。

Synchronied 不是读写锁

5. 公平锁vs非公平锁

    假设ABC三个线程同时尝试获取锁,A成功,BC获取失败阻塞等待。

此时A释放锁后,两种锁策略对应着公平锁和非公平锁。

公平锁:遵守“先来后到”。B比C先来,所以当A释放锁之后,B先于C获取锁。

非公平锁:不遵守“先来后到”,A释放后,BC都有可能获取到锁。

所以此处的公平与否是参考是否遵守“先来后到”的规则的。

操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要 想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.

公平锁和非公平锁没有好坏之分, 关键还是看适用场景.

synchronized 是非公平锁.

6.可重入锁vs不可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。

比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入 锁(因为这个原因可重入锁也叫做递归锁)。

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括 synchronized关键字锁都是可重入的。 而 Linux 系统提供的 mutex 是不可重入锁.(此处在死锁章节有提到。)

7. Synchronized优化机制

总结以上策略,synchronized具有以下特性:

开始是乐观锁,如果锁冲突频繁,就转化为悲观锁

开始是轻量级锁,如果锁被出游的时间较长就转换成重量级锁

实现轻量级锁的时候大概率用到自旋锁策略

是一种不公平锁

是一种可重入锁

不是读写锁

JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁的状态。会根据情况,进行依次升级。

1偏向锁

第一个尝试加锁的线程,优先进入偏向锁状态,针对对象头中做一个标记,记录这个锁属于哪个线程,如果不存在其他锁竞争的情况下,不进行加锁操作,同时线程是安全的。如果有其他线程来进行锁竞争,就会取消偏向锁状态,转为轻量级锁。

2 轻量级锁

随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).

此处的轻量级锁就是通过 CAS 来实现. 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用) 如果更新成功, 则认为加锁成功,如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU)。

(自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源. 因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了. 也就是所谓的 "自适应)。

3重量级锁

如果竞争进一步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁。此处的重量级锁就是指用到内核提供的mutex .执行加锁操作, 先进入内核态。在内核态判定当前锁是否已经被占用,如果该锁没有占用,则加锁成功,并切换回用户态. 如果该锁被占用,则加锁失败.此时线程进入锁的等待队列,挂起.等待被操作系统唤醒。经历了一系列的系统调度之后, 这个锁被其他线程释放了,操作系统也想起了这个挂起的线程,于是唤醒这个线程,尝试重新获取锁.

8.其他优化策略

锁消除

jvm+编译器会判断锁是否可以消除,如果可以直接消除锁。比如在单线程模式下,使用到synchronized关键字,其实没必要加锁,此时就会直接消除锁。

锁粗化

一段逻辑中如果出现多次的加锁解锁,JVM就会自动进行锁的粗化。

比如(大写字母表示加锁,小写表示解锁)下面的例子中

AaBbCcDd A和d中间有很多加锁解锁的过程,但是实际上运行的时候没有那么多线程来抢锁,此时JVM+编译器就会将锁的粒度粗化为Ad,避免频繁申请和释放锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值