synchronized

⭐ 作者:小胡_不糊涂
🌱 作者主页:小胡_不糊涂的个人主页
📀 收录专栏:JavaEE
💖 持续更文,关注博主少走弯路,谢谢大家支持 💖

1. 特性

1.1 互斥

synchronized 会起到互斥效果,某个线程执⾏到某个对象的 synchronized 中时,其他线程如果也执⾏到同⼀个对象synchronized 就会阻塞等待。
在这里插入图片描述

synchronized⽤的锁是存在Java对象头⾥的。可以粗略理解成,每个对象在内存中存储的时候,都存有⼀块内存表⽰当前的 “锁定” 状态(类似于厕所的 “有⼈/⽆⼈”)。
如果当前是 “⽆⼈” 状态,那么就可以使⽤,使⽤时需要设为 “有⼈” 状态;如果当前是"有⼈"状态,那么其他⼈⽆法使⽤,只能排队。

在这里插入图片描述

针对每⼀把锁,操作系统内部都维护了⼀个等待队列。当这个锁被某个线程占有的时候,其他线程尝试进⾏加锁,就加不上了,就会阻塞等待,⼀直等到之前的线程解锁之后,由操作系统唤醒⼀个新的线程,再来获取到这个锁。

注意:

  • 当上⼀个线程解锁之后,下⼀个线程并不是⽴即就能获取到锁,⽽是要靠操作系统来 “唤醒”,这也就是操作系统线程调度的⼀部分⼯作。
  • 假设有 A B C 三个线程,线程 A 先获取到锁,然后 B 尝试获取锁,然后 C 再尝试获取锁,此时 B 和 C都在阻塞队列中排队等待。但是当 A 释放锁之后,虽然 B ⽐ C 先来的,但是 B 不⼀定就能获取到锁,⽽是和 C 重新竞争,并不遵守先来后到的规则。

1.2 可重入

synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题。

什么情况下,会把自己锁死?
⼀个线程没有释放锁,然后⼜尝试再次加锁。
// 第⼀次加锁,加锁成功
// 第⼆次加锁,锁已经被占⽤,阻塞等待。
按照之前对于锁的设定,第⼆次加锁的时候,就会阻塞等待。直到第⼀次的锁被释放,才能获取到第⼆个锁。
但是释放第⼀个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想⼲了,也就⽆法进⾏解锁操作。这时候就会 死锁,这样的锁也就称为 不可重入锁

在可重⼊锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息。

  • 如果某个线程加锁的时候,发现锁已经被⼈占⽤,但是恰好占⽤的正是⾃⼰,那么仍然可以继续获取到锁,并让计数器自增。
  • 解锁的时候计数器递减为 0 的时候,才真正释放锁。(才能被别的线程获取到)

2. 使用

synchronized 本质上要修改指定对象的 “对象头”。从使⽤⻆度来看,synchronized 也势必要搭配⼀个具体的对象来使⽤。

2.1 修饰代码块

明确指定锁哪个对象

//锁任意对象
public class SynchronizedDemo {
    private Object locker = new Object();
     public void method() {
         synchronized (locker) {
         }
     }
}
//锁当前对象
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
        }
    }
}

2.2 修饰普通方法

锁的 SynchronizedDemo 对象

public class SynchronizedDemo {
    public synchronized void methond() {
    }
}

2.3 修饰静态方法

锁的 SynchronizedDemo 类的对象

public class SynchronizedDemo {
    public synchronized static void method() {
    }
}

两个线程竞争同⼀把锁,才会产⽣阻塞等待。
两个线程分别尝试获取两把不同的锁,不会产⽣竞争。

3. 锁机制

3.1 锁升级

JVM 将 synchronized 锁分为 ⽆锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进⾏依次升级。
在这里插入图片描述

  1. 偏向锁
    第⼀个尝试加锁的线程,优先进⼊偏向锁状态。
    偏向锁不是真的加锁,⽽只是在锁的对象头中记录⼀个标记(记录该锁所属的线程)。如果没有其他线程参与竞争锁,那么就不会真正执⾏加锁操作,从⽽降低程序开销。⼀旦真的涉及到其他的线程竞争,再取消偏向锁状态,进⼊轻量级锁状态。
    偏向锁本质上相当于 “延迟加锁” 。能不加锁就不加锁,尽量来避免不必要的加锁开销。
  2. 轻量级锁
    随着其他线程进⼊竞争,偏向锁状态被消除,进⼊轻量级锁状态(⾃适应的⾃旋锁)。
    此处的轻量级锁就是通过 CAS 来实现。

通过 CAS 检查并更新⼀块内存 (⽐如 null => 该线程引⽤)
如果更新成功,则认为加锁成功;
如果更新失败,则认为锁被占⽤,继续⾃旋式的等待(并不放弃 CPU)。
此处的⾃旋不会⼀直持续进⾏,⽽是达到⼀定的时间/重试次数,就不再⾃旋了,也就是所谓的 “⾃适应”。

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

3.2 锁消除

锁消除是一种编译器优化的手段。
编译器+JVM 判断锁是否可消除。如果可以,就直接消除。
有些应⽤程序的代码中,⽤到了 synchronized。例如 StringBuffer:

StringBuffer str=new StringBuffer();
str.append("a");
str.append("b");

这里的每个 append 的调⽤都会涉及加锁和解锁,但如果只是在单线程中执⾏这个代码,编译器就会自动把synchronized优化掉。

3.3 锁粗化

⼀段逻辑中如果出现多次加锁解锁,编译器 + JVM 会⾃动进⾏锁的粗化。
在这里插入图片描述

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值