目录
一、前言
在Java多线程编程中,线程安全是非常重要的一个概念。为了防止多个线程同时访问共享资源时出现数据不一致或其他竞态条件问题,Java提供了synchronized
关键字和监视器锁(Monitor Lock)机制。这个博客将深入探讨synchronized
的原理、使用场景以及与监视器锁的关系。
二、Synchronized关键字
synchronized
是Java中的一个关键字,用于在多线程环境中控制对共享资源的访问。当一个线程执行synchronized
修饰的方法或代码块时,其他线程将无法访问相同的资源,直到当前线程释放资源锁。
2.1 Synchronized的特性
1. 互斥
Synchronized
会起到互斥效果, 某个线程执行到某个对象的 synchronized
中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。
• 进入 synchronized 修饰的代码块, 相当于 加锁
• 退出 synchronized 修饰的代码块, 相当于 解锁
synchronized (locker){
for (int i = 0; i < 5000; i++) {
count++;
}
}
synchronized用的锁是存在Java对象头里的。
可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于厕所
的 “有人/无人”)。
如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态。
如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队。
理解 “阻塞等待”。
针对每⼀把锁, 操作系统内部都维护了⼀个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试
进行加锁, 就加不上了, 就会阻塞等待, ⼀直等到之前的线程解锁之后, 由操作系统唤醒⼀个新的线程,
再来获取到这个锁.
注意:
• 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁。而是要靠操作系统来 “唤醒”. 这也就
是操作系统线程调度的一部分工作。
• 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C
都在阻塞队列中排队等待.。但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁,
而是和 C 重新竞争, 并不遵守先来后到的规则。
synchronized的底层是使用操作系统的mutex lock实现的。
2. 可重入
synchronized 同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题。
理解 “把自己锁死”
⼀个线程没有释放锁, 然后又尝试再次加锁。
第⼀次加锁, 加锁成功
lock();
第⼆次加锁, 锁已经被占用, 阻塞等待
lock();
按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二
个锁.。但是释放第⼀个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会死锁。
Java 中的 synchronized
是 可重⼊锁, 因此没有上面的问题。
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
synchronized (locker) {
count++;
}
}
}
在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息。
• 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增。
• 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
三、synchronized 的使用示例
synchronized 本质上要修改指定对象的 “对象头”。从使用角度来看,synchronized 也势必要搭配⼀个具体的对象来使用。
3.1 修饰代码块: 明确指定锁哪个对象
1.锁任意对象
public class Synchrinized1 {
private static int count;
public static void main(String[] args) {
Object locker=new Object();
Thread t1=new Thread(()->{
for (int i = 0; i <10000 ; i++) {
synchronized (locker){
count++;//锁任意对象
}
}
});
}
}
2.锁当前对象
public class Synchrinized1 {
private static int count;
public static void main(String[] args) {
Object locker=new Object();
Thread t1=new Thread(()->{
for (int i = 0; i <10000 ; i++) {
synchronized (this){
count++;//锁当前对象
}
}
});
}
}
3.2直接修饰普通方法
public class Synchronized2{
public int count=0;
synchronized public void methond(){
count++;
}
}
3.3 修饰静态方法
public class Synchronized{
public static int count=0;
public static synchronized void methon(){
count++;
}
}
四 、监视器锁(Monitor Lock)
Monitor Lock
(监视器锁)是一种高级同步机制,底层实现了synchronized
关键字的功能。每个对象都关联一个监视器锁,当线程进入synchronized
块时,线程会获取该对象的监视器锁。
4.1 监视器锁的特点
- 互斥性:同一时间只有一个线程可以持有监视器锁。
- 可重入性:同一线程可以多次获取已持有的监视器锁。
- 锁升级:从
未锁定状态
到偏向锁
,再到轻量级锁
,最终升级为重量级锁
。
4.2 Synchronized的底层实现
synchronized
关键字的实现依赖于JVM,而JVM底层使用了监视器锁(Monitor Lock)来实现线程的同步。当线程执行synchronized
代码时,JVM会检查是否已经获得了监视器锁:
- 如果未加锁,当前线程会尝试获取锁。
- 如果已加锁,当前线程将被阻塞,直到锁被释放。
4.3 onitor Lock的状态
一个监视器锁有以下四种状态:
- 未锁定状态:任何线程都可以竞争锁。
- 偏向锁状态:只允许一个线程持有锁,减少竞争。
- 轻量级锁状态:多个线程竞争锁,但未发生旋转或阻塞。
- 重量级锁状态:线程竞争激烈,锁被线程阻塞等待。
4.4 Synchronized与ReentrantLock的比较
特性 | Synchronized | ReentrantLock |
---|---|---|
实现方式 | 关键字,底层监视器锁 | 显式类,灵活性更高 |
锁的获取释放 | 自动获取释放 | 需要手动调用lock() 和unlock() 方法 |
公平性 | 非公平锁 | 可配置为公平锁或非公平锁 |
超时放弃锁 | 不支持 | 支持,通过tryLock(long time, TimeUnit unit) |
多条件变量 | 不支持 | 支持,通过Condition 实现 |
性能 | 在JDK1.5之前较差,1.5后优化后性能接近 | 性能优于synchronized |
4.5 常见问题及解决方案
1. 避免过度同步
过度同步会降低并发性能,应尽量缩小同步范围。
2. 避免死锁
死锁是由于多个线程互相持有对方资源而导致的。可以通过如下方式避免:
- 按顺序加锁:对多个锁的加锁顺序进行统一。
- 避免嵌套锁:减少嵌套使用锁的场景。
3. 数据一致性问题
在单例模式或共享变量的场景中,必须确保所有修改共享资源的操作都在同步块内。
五、总结
通过本文,我们深入探索了Java中的Synchronized
关键字,包括其互斥性和可重入性的特性。文章详细介绍了Synchronized
的三种使用方式:修饰代码块、修饰普通方法和修饰静态方法。同时,我们还解析了监视器锁(Monitor Lock)的底层实现,以及Synchronized
与ReentrantLock
的对比。最后,文章总结了并发编程中常见问题的解决方案,如避免过度同步、防止死锁和保持数据一致性。本文旨在帮助开发者更好地理解和使用Synchronized
,从而编写出高效、安全的并发程序。
希望这个博客对你理解Java多线程编程中的线程安全机制有所帮助!如果你有更多问题,欢迎留言讨论。