锁的策略及synchronized详解

2023-12-13 04:45:47

加锁过程中,处理冲突的过程中,涉及到的一些不同的处理方式。锁的策略决定了线程如何获取和释放锁以及在何种情况下阻塞和唤醒线程。

1. 常见的锁策略?

1.1? 乐观锁和悲观锁

  • 乐观锁:在加锁之前,预估当前出现锁冲突的概率不大,因此在进行加锁的时候不会做太多的工作,于是加锁的速度就可能更快,当更容易引入一些其他的问题,例如肯能会消耗更多的CPU资源。
  • 悲观锁:在加锁之前,预估当前出现锁冲突的概率比较大,因此在加锁的时候就会做更多的工作,所以加锁的速度可能会更慢,但是整个过程中不容易出现其他问题。

1.2?轻量级锁和重量级锁

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

注意:

  • 轻量级锁和重量级锁的定义是加锁后对结果的评价而来
  • 乐观锁和悲观锁的定义是对未发生的事做出预估而来?

?1.3?自旋锁和挂起等待锁

  • 自旋锁:自旋锁就是轻量级锁的一种典型实现,进行加锁的时候搭配一个while循环,如果加锁成功则循环结束,加锁失败不进入阻塞而是再次尝试。这个反复快速执行的过程就称为“自旋”。一旦其他线程释放了锁能第一时间拿到,同时这样的自旋锁也是乐观锁,使用自旋的前提就是预期冲突概率不大,其他线程释放了锁,就能第一时间拿到。如果当前加锁的线程特别多,使用自旋锁则会浪费过多的CPU资源。
  • 挂起等待锁:挂起等待锁就是悲观锁的一种实现,同时也是重量级锁,加锁失败时进入阻塞等待,不会继续消耗CPU资源,挂起等待的时候需要内核调度器介入,这部分要完成的操作很多,所以真正获取到锁的时间会更长,适用于锁冲突激烈的情况。

1.4 普通互斥锁和读写锁?

  • 普通互斥锁:普通互斥锁也被称为排它锁,它确保在任何时刻只有一个线程可以获得锁并访问共享资源。当一个线程持有锁时,其他线程需要等待锁的释放才能继续执行,类似于synchronized 加锁解锁。
  • 读写锁:读写锁把锁分为两种情况,加读锁和加写锁。读锁和读锁之间,不会出现锁冲突,即允许多个线程同时读取共享资源;写锁和写锁之间会出现锁冲突;读锁和写锁之间会出现锁冲突。

?为什么要引入读写锁?

如果两个线程读同一个数据,这个操作本身就是线程安全的,不需要阻塞,如果使用 synchronized 加锁,则会阻塞,对于性能有一定影响。

1.5 公平锁和非公平锁?

  • 公平锁:公平锁按照请求锁的顺序分配锁资源即 “先来后到” ,保证每个线程都有公平的机会获得锁。这种策略避免了线程饥饿现象,但会导致额外的开销,因为线程可能需要等待其他线程释放锁。
  • 非公平锁:不遵守 “先来后到” 的规则,所有线程都有可能获取到锁。

1.6 可重入锁和不可重入锁?

  • ?可重入锁:一个线程使用一把锁连续加锁两次,不会产生死锁,就是可重入锁,如 synchronized。
  • 不可重入锁:一个线程使用一把锁连续加锁两次,会产生死锁,则是不可重入锁。

2. synchronized

2.1?synchronized 的锁策略

synchronized 具有自适应能力

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

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

  • 如果当前锁冲突的激烈程度小,就处于 乐观锁/轻量级锁/自旋锁。
  • 如果当前锁冲突的激烈程度大,就处于 悲观锁/重量级锁/挂起等待锁。

2.2 锁升级

当线程执行到 synchronized 的时候,如果锁还是空闲的,就会经历以下过程:

  1. 偏向锁阶段:每个锁对象中有一个偏向锁标记,当这个锁对象首次被加锁时,会进入偏向锁,锁对象会记录下该线程的id,如果下次加锁,没有锁竞争,那么这个锁仍然是偏向锁。偏向锁并不会真的加上锁,因为没有锁竞争,所以就不用真的加上锁,免去了一定开销。
    注意:如果一个锁此前都是线程1的偏向锁即记录下了线程1的id,在某次线程1再次尝试加锁时,出现了一个线程2 也尝试加锁,此时这个锁会升级为轻量级锁,然后再由这两个线程来竞争。但是本次加锁一定是线程1拿到锁,线程2下次竞争才可能拿到锁。只有发生锁竞争才会 偏向锁才会升级为轻量级锁,例如一个锁此前是线程1的偏向锁,此时线程1没有继续加锁了,现在线程2开始用这个锁对象加锁(此时没有锁竞争),这个锁仍然是偏向锁。
  2. 轻量级锁阶段:当偏向锁出现锁竞争时,这个锁则会升级为轻量级锁。此处是通过自旋锁的方式来实现的。
    ?
  3. 重量级锁阶段:当参与锁竞争的线程达到某个阈值,就会从轻量级锁升级到重量级锁。

注意:锁的升级不可逆。

2.3 锁消除?

锁消除也是 synchronized 中内置的优化策略。

?编译器编译代码的时候,如果发现这个代码不需要加锁,就会取消掉这个锁。但是这个优化是比较有限的,如果代码稍微复杂一些,编译器是判断不了是否需要加锁的。

2.4 锁粗化?

会把多个加锁代码块合成一个代码块,去除了多次加锁解锁的开销。

例如:

synchronized(locker) {
    a++;
}
synchronized(locker) {
    b++;
}

可粗化为:

synchronized(locker) {
    a++;
    b++;
}

文章来源:https://blog.csdn.net/2303_78892316/article/details/134893878
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。