Java常见的锁策略

发布于:2024-11-29 ⋅ 阅读:(15) ⋅ 点赞:(0)

Java常见的锁策略

悲观锁和乐观锁

这是两种不同的锁的实现方式

  • 乐观锁,在加锁之前,预估当前出现锁冲突的概率不大,因此在进行加锁的时候就不会做太多的工作。

加锁过程做的事情比较少,加锁的速度可能就更快,但是是更容易引入一些其他的问题(但是可能会消耗更多的CPU资源)

  • 悲观锁,在加锁之前,预估当前锁冲突出现的概率比较大,因此加锁的时候,就会做更多的工作,

做的事情更多,加锁的速度可能更慢,但是整个过程中不容易出现其他问题。

轻量级锁和重量级锁

  • 轻量级锁,加锁的开销小,加锁的速度更快—>轻量级锁,一般就是乐观锁
  • 重量级锁,加锁的开销更大,加锁速度更慢---->重量级锁,一般也就是悲观锁

轻量重量,加锁之后,对结果的评价。

悲观乐观,是加锁之前,对未发生的事情进行的预估

整体来说,这两种角度,描述的是同一个事情。

自旋锁和挂起等待锁

  • 自旋锁就是轻量级锁的一种典型实现

进行加锁的时候,搭配一个while循环,如果加锁成功,自然循环结束。如果加锁不成功,不是阻塞放弃CPU,而是进行下一次循环,再次尝试获取到锁。

这个反复快速执行的过程,就称为“自旋”,一旦其他线程释放了锁,就能第一时间拿到锁。

同时,这样的自旋锁,也是乐观锁。使用自旋的前提,就是预期锁冲突概率不大,其他线程释放了锁,就能第一时间拿到。

万一当前加锁的线程特别多,自旋意义就不大了,白白浪费CPU了。

  • 挂起等待锁,就是重量级锁的一种典型实现,同时也是一种悲观锁。

进行挂起等待的时候,就需要内核调度器介入了,这一块要完成的操作就多了,真正获取到锁要花的时间就更多一些了。

这个锁可以适用于锁冲突激烈的情况

普通互斥锁和读写锁

  • 普通互斥锁:类似于synchronized(操作涉及到 加锁 和 解锁)
  • 读写锁:这里的读写锁,把加锁分成两种情况:

1)加读锁

2)加写锁

读锁和读锁之间,不会出现锁冲突(不会阻塞)

写锁和写锁之间,会出现锁冲突(会阻塞)

读锁和写锁之间,会出现锁冲突(会阻塞)

一个线程加读锁的时候,另一个线程,只能读,不能写

一个线程加写锁的时候,另一个线程,不能写,也不能读

为啥要引入读写锁???

如果两个线程读,本身就是线程安全的!不需要进行互斥!

如果使用synchronized这种方式加锁,两个线程读,也会产生互斥,产生阻塞(对于性能有一定的损失)

完全给读操作不加锁,也不行,就怕一个线程读一个线程写,可能会读到写了一半的数据

读写锁,就可以很好的解决上述问题。

实际开发中,读操作本身就是非常频繁的,非常常见的

读写锁就能把这些并发读之间的锁冲突的开销给省下了,就对于性能提升很明显了

公平锁和非公平锁

注意,此处定义的 ”公平“,遵循先来后到,才叫公平!

Java中的synchronized就是非公平的(也就是没有按先后顺序)

要想实现公平锁,就需要引入额外的数据结构(引入队列,记录每个线程先后顺序)才能实现公平锁。(能记录先后顺序的)

使用公平锁,天然就可以避免线程饿死的问题

非公平锁:就是每个线程等概率竞争,不遵循先来后到

可重入锁和不可重入锁

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

synchronized是可重入锁。

系统自带的锁,是不可重入的锁。

可重入锁需要记录持有锁的线程是谁,加锁的次数的计数

Java中的synchronized算哪种情况?

synchronized具有自适应能力!!

synchronized在某些情况下是 乐观锁/轻量级锁/自旋锁,某些情况下是 悲观锁/重量级锁/挂起等待锁

内部会自动的评估当前锁冲突的激烈程度。

如果当前锁冲突的激烈程度不大,就处于 乐观锁/轻量级锁/自旋锁

如果当前锁冲突的激烈程度很大,就处于 悲观锁/重量级锁/挂起等待锁

不是读写锁

非公平锁

可重入锁

synchronized内部优化地非常好,大部分情况下使用synchronized都是不会有啥问题的(无脑用

系统原生的锁算哪种情况?

对于系统原生的锁(Linux提供的mutex这个锁)

1.悲观锁

2.重量级锁

3.挂起等待锁

4.不是读写锁

5.非公平锁

6.不可重入锁

synchronized的加锁过程,尤其是“自适应”是咋回事?

当线程执行到synchronized的时候,如果这个对象当前处于未加锁的状态,就会经历以下过程:

1.偏向锁阶段

核心思想,“懒汉模式”,能不加锁,就不加锁,能晚加锁,就晚加锁

所谓的偏向锁,并非真的加锁了,而只是做了一个非常轻量的标记

搞暧昧,就是偏向锁,只是做了一个标记。没有真加锁(也不会有互斥)

一旦有其他线程,来和我竞争这个锁,就在另一个线程之前,先把锁获取到

从偏向锁就会升级到轻量级锁(真加锁了,就有互斥了)

如果我搞暧昧的过程中,要是没人来竞争,整个过程就把加锁这样的操作就完全省略了

非必要不加锁。

在遇到竞争的情况下,偏向锁没有提高效率

但是如果在没有竞争的情况下,偏向锁就大幅度的提高了效率。

总的来说,偏向锁意义还是很大的。

2.轻量级锁阶段

(假设有竞争,但是不多)

此处就是通过自旋锁的方式来实现的

优势:另外的线程把锁释放了,就会第一时间拿到锁

劣势:比较消耗CPU

于此同时,synchronized内部也会统计,当前这个锁对象上,有多少个线程在参与竞争

这里当发现参与竞争的线程比较多了

就会进一步升级到重量级锁

对于自旋锁来说,如果同一个锁竞争很多

大量的线程都在自旋,整体CPU的消耗就很大了

3.重量级锁阶段

此时拿不出锁的线程就不会继续自旋了,而是进入“阻塞等待”

就会让出CPU了(不会使CPU占用率太高)

当当前线程释放锁的时候,就由系统随机唤醒一个线程来获取锁了

此处锁 只能 升级,不能降级,自适应这个词,严格的说不算很严谨(但是,保不齐未来某个版本就能降级了)

synchronized中内置的优化策略

锁消除

编译器编译代码的时候,如果发现这个代码,不需要加锁,就会自动把锁给干掉

这里的优化是比较保守的,针对一眼看上去就完全并不涉及线程安全问题的代码,能够把锁消除掉

锁粗化

会把多个细粒度的锁,合并一个粗粒度的锁

synchronized{}大括号里面包含的代码越少,就认为锁的粒度越细

包含的代码越多,就认为锁的粒度越粗

在这里插入图片描述

在这里插入图片描述

小结

synchronized背后涉及到了很多的“优化手段”

1.锁升级。偏向锁–>轻量级锁–>重量级锁
2.锁消除。自动干掉不必要的锁
3.锁粗化。把多个细粒度的锁合并成一个粗粒度的锁,减少锁竞争的开销

这些机制都是在内部,在看不到的地方默默发挥作用的