十问synchronized
1. 为什么Java中需要使用synchronized关键字?
答:为了保证多线程环境下数据的同步性和一致性,防止多个线程同时访问共享资源导致的数据不一致或竞态条件问题。
2. 为什么synchronized能提供线程间的同步?
答:synchronized关键字通过锁定机制确保同一时间只有一个线程可以访问被保护的代码块或方法,从而避免了并发访问带来的冲突。
3. 为什么synchronized可以锁定对象或代码块?
答:锁定对象可以确保对对象状态的独占访问,锁定代码块则可以更细粒度地控制同步范围,减少不必要的阻塞,提高并发性能。
4. 为什么synchronized有对象锁和类锁之分?
答:对象锁用于控制同一对象实例上的并发访问,而类锁用于控制所有实例对类的静态成员的并发访问,这样可以分别解决不同层面的并发控制需求。
5. 为什么synchronized在使用时会阻塞线程?
答:当一个线程获取了锁,其他尝试获取同一锁的线程会被阻塞,直至锁被释放,这是为了确保同一时刻只有一个线程可以执行临界区代码,防止数据竞争。
6. 为什么synchronized有重量级和轻量级之说?
答:早期的JVM中,synchronized采用操作系统级别的互斥锁,开销较大,称为重量级锁;随着JVM优化,引入了偏向锁、轻量级锁等,减少了锁的开销,提升了性能。
7. 为什么Java后来引入了偏向锁和轻量级锁?
答:为了减少线程间切换和调度的开销,提高并发性能。偏向锁假设大多数情况下锁不会被多个线程竞争,从而避免了不必要的锁操作;轻量级锁则在竞争不激烈时,尽量减少操作系统介入,使用自旋等待减少阻塞。
8. 为什么synchronized的可重入特性很重要?
答:可重入性意味着同一个线程可以多次获取同一个锁,不会造成死锁,这对于递归调用或含有循环锁依赖的代码非常重要,保证了线程的正常执行流程。
9. 为什么使用synchronized时要谨慎,避免死锁?
答:尽管synchronized提供同步机制,但如果多个线程以不恰当的顺序请求多个锁,可能会形成循环等待,导致死锁。谨慎设计同步逻辑,遵循锁的获取顺序,可以有效避免死锁。
10. 为什么在某些情况下推荐使用更高级的并发工具(如java.util.concurrent包下的工具类)而不是直接使用synchronized ?
答:高级并发工具提供了更丰富的同步原语,如Semaphore、ReentrantLock、CountDownLatch等,它们在特定场景下提供了比synchronized更细粒度的控制、更高的灵活性和更好的性能,能够更好地解决复杂的并发控制问题。同时,这些工具类往往内置了更好的性能优化和更友好的API设计,便于开发者使用。
了解synchronized
synchronized关键字原理
synchronized是Java中的一个关键字,用于实现线程同步,确保线程安全。它主要通过两种方式工作:同步方法和同步代码块。
- 同步方法:当一个方法被
synchronized修饰时,一次只能有一个线程执行该方法。方法的锁是针对当前实例对象(如果是非静态方法)或类对象(如果是静态方法)的。 - 同步代码块:允许更细粒度的锁定,仅锁定特定代码块。你可以指定一个对象作为锁,只有获得了这个对象的锁,线程才能执行这块代码。
原理深入
synchronized底层依赖于Java对象监视器(Monitor),这是一个操作系统级别的机制,用来协调多个线程对共享资源的访问。当线程试图进入synchronized区域时:
- 获取锁:线程尝试获取锁,如果锁未被其他线程持有,则获取成功并进入临界区执行。
- 等待/阻塞:如果锁已被其他线程持有,该线程将进入等待队列,直到锁被释放。
- 释放锁:执行完同步代码或抛出异常后,线程会自动释放锁,允许等待队列中的下一个线程获取锁。
生活中的类比
想象一个公共图书馆,馆内有一台打印机(共享资源)非常受欢迎。
- 无同步:如果没有任何管理措施,所有人都可以直接去使用打印机,就会出现几个人同时抢着打印的情况,导致混乱和打印错误(类似于并发访问导致的数据不一致)。
- 使用
synchronized:现在图书馆引入了一个规则,即在使用打印机前,必须先到管理员那里领取一个“使用令牌”。这个令牌就像是synchronized锁,同一时间只有一个读者能拿到。如果打印机正在被使用,其他想打印的读者就需要排队等待,直到当前使用者完成并归还“令牌”(释放锁),下一个人才能继续使用。这样一来,虽然大家可能需要等待,但确保了每次只有一个读者能够使用打印机,避免了混乱和错误(保证了线程安全)。
通过这个类比,我们可以看到synchronized是如何通过限制访问权限,确保了共享资源在同一时刻只被一个线程访问,从而避免了并发问题,保证了数据的一致性和完整性。
synchronized底层实现原理
在Java虚拟机(JVM)中,synchronized关键字的底层执行涉及到多个步骤,确实与线程堆栈紧密相关。下面是其大致的执行流程,以及与堆栈的关联:
1. 监测器锁(Monitor Lock)的获取:
- 当线程遇到`synchronized`代码块或方法时,首先尝试获取与之关联的监视器锁。对于非静态方法,锁是对象实例;对于静态方法,锁是类的Class对象。
- 这一步骤涉及到了对象头的修改,特别是其中的Mark Word部分,用于记录锁的状态。如果锁是自由的,JVM会将当前线程ID记录到Mark Word中,表示锁被占有。
--->对于这两点的解释:
当一个Java线程遇到`synchronized`代码块或方法时,JVM会执行一系列复杂的步骤来确保线程安全。以下我详细讲一下这个过程,特别是涉及到的对象头修改和Mark Word的作用。
###### 对象头结构
在JVM中,每个对象在内存中都包含一个对象头(Object Header),这个头部包含了对象自身的元数据信息,其中最为关键的一部分就是**Mark Word**。Mark Word是一个固定大小的字段(通常是几个字节),存储了对象的各种运行时状态信息,如哈希码、GC分代年龄、锁状态标志等。Mark Word的具体内容会根据对象的状态变化而动态调整。
###### 锁状态与Mark Word
- **无锁状态**:在没有线程竞争的情况下,Mark Word中存储的是对象的哈希码、分代年龄等普通信息。
- **偏向锁状态**:如果JVM检测到对象经常被单一线程访问,它可能会将对象的锁状态升级为偏向锁,并在Mark Word中记录该线程的ID,以加速后续的访问。
- **轻量级锁状态**:当有其他线程尝试获取锁时,如果之前是偏向锁状态,会先撤销偏向锁,然后尝试获取轻量级锁。此时,Mark Word中会存储指向当前持有锁的线程栈帧中锁记录的指针(Displaced Mark Word)。
- **重量级锁状态**:如果轻量级锁竞争失败(例如,自旋达到一定次数),会进一步升级为重量级锁,此时,Mark Word中存储的是指向操作系统互斥量(monitor)的指针。
(
- 升级为重量级锁主要带来以下影响:
- 性能下降:因涉及操作系统级的线程挂起与唤醒,导致更高的执行开销,响应变慢。
- 阻塞线程:未获取锁的线程被阻塞,直至锁释放,降低了并发执行效率。
- 资源消耗增加:更多系统资源被占用,包括CPU和内存,特别是在高竞争场景下。
- 影响并发能力:减少了有效并发,因为线程间的自旋等待被阻塞取代。
- 潜在的死锁风险:复杂的锁依赖和线程管理不当可能引发死锁。
简而言之,重量级锁虽确保了线程安全,但以牺牲性能和并发性为代价。
)
###### 获取监视器锁的过程
1. **检查锁状态**:线程首先检查对象头的Mark Word,确定锁的状态。
1. **尝试获取锁**:
- 对于**非静态方法**,锁是对象实例本身,线程尝试修改Mark Word,将其设置为指向当前线程的指针,如果之前是无锁状态或当前线程已经持有锁(偏向锁或轻量级锁),则操作成功。
- 对于**静态方法**,锁是类的Class对象,操作原理类似,但锁是针对整个类而不是类的实例。
3. **锁升级**:如果遇到竞争(即有其他线程也在尝试获取同一锁),根据竞争情况可能升级到轻量级锁或重量级锁,涉及到Mark Word的进一步修改和操作系统层面的互斥量操作。
3. **自旋等待**:在轻量级锁阶段,如果持有锁的线程很快会释放锁,当前线程可能会执行自旋操作,避免立即阻塞,节省上下文切换开销。
(
什么是自旋操作 ?
具体来说,当一个线程尝试获取一个锁,如果发现锁已被占用,并不是,而不是直接挂起自己 , 立即放弃处理器的执行权限进入休眠状态, 而是可以选择自旋,即在一个循环中不断地检查锁是否已经释放
抽象代码示例底层实现 :
-----
while (!try_to_acquire_lock()) {
// 使用CPU指令集中的原子操作尝试获取锁
// 如果失败,则继续自旋(可能伴有轻微延迟)
}
// 锁成功获取,执行临界区代码
critical_section();
// 释放锁
release_lock();
)
------
### 标记锁占有
如果锁是自由的(无锁状态或当前线程已持有),JVM会修改Mark Word的内容,记录下当前线程的ID或其他表示锁被持有的标记,从而表明该锁已经被当前线程占有。这样,其他线程在尝试获取相同锁时,就能通过检查Mark Word得知锁已被占用,进而采取相应的等待或竞争策略。
通过这样的机制,JVM确保了`synchronized`代码块或方法的线程安全性,有效地管理了并发访问。
2. 线程状态的转换:
- 如果锁已被其他线程持有,当前线程会经历状态转换,从可运行状态(Runnable)变为阻塞状态(Blocked),并被加入到该锁的等待集(Wait Set)中。这个过程涉及到了操作系统的线程调度,以及JVM内部的线程状态管理。
3. 线程堆栈的作用:
- 在JVM中,每个线程都有自己的线程堆栈。当线程尝试进入
synchronized区域时,若需要阻塞,JVM会通过操作线程堆栈来保存当前执行的上下文,包括局部变量、操作数栈等,然后将线程挂起。 - 当锁被释放,等待线程被唤醒时,JVM会根据线程堆栈中保存的信息恢复线程的执行上下文,使得线程能够从上次停止的地方继续执行。
4. 自旋锁与锁升级:
- 在轻量级锁的场景下,线程可能不会立即阻塞,而是执行自旋操作,即在原地快速循环尝试获取锁,避免了昂贵的上下文切换。这期间,线程堆栈也会被持续监控,以便在自旋结束后立即恢复执行或采取进一步的阻塞措施。 - 锁升级机制(从偏向锁到轻量级锁再到重量级锁)也是基于线程竞争情况动态调整的,涉及线程堆栈中状态的跟踪与调整。
5. 锁的释放与线程唤醒:
- 当持有锁的线程执行完同步代码块或方法后,会释放锁,并修改对象头的Mark Word。如果等待集中有线程,JVM会选择其中一个线程(公平锁按顺序,非公平锁随机或直接尝试),将其从阻塞状态转换回可运行状态,并通过线程堆栈恢复其执行上下文。
synchronized的执行过程与线程堆栈密切相关,从锁的尝试获取、线程状态转换、自旋等待、到锁的释放和线程唤醒,每一步都离不开对线程堆栈的管理与操作,以确保线程安全和高效的并发控制。
Java代码实现synchronized相同的功能
在Java中,synchronized关键字提供的功能主要是保证代码块或方法的线程同步,确保同一时间只有一个线程可以访问特定资源。要实现类似synchronized的功能,可以使用java.util.concurrent.locks包下的ReentrantLock类。下面是一个简单的示例,展示如何使用ReentrantLock模拟synchronized关键字的功能:
import java.util.concurrent.locks.ReentrantLock;
public class SynchronizedExample {
// 创建一个可重入锁对象
private final ReentrantLock lock = new ReentrantLock();
public void synchronizedMethod() {
lock.lock(); // 加锁
try {
// 临界区代码,模拟synchronized方法或代码块
System.out.println(Thread.currentThread().getName() + " 开始执行");
Thread.sleep(1000); // 模拟耗时操作
System.out.println(Thread.currentThread().getName() + " 执行结束");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Thread interrupted.");
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
// 创建两个线程执行同步方法
Thread t1 = new Thread(() -> example.synchronizedMethod(), "Thread-1");
Thread t2 = new Thread(() -> example.synchronizedMethod(), "Thread-2");
t1.start();
t2.start();
}
}
代码输出预测 :
由于线程的执行顺序受到操作系统调度策略的影响,所以确切的输出顺序可能会有所不同。
- 线程开始执行顺序:
Thread-1和Thread-2的启动顺序不确定,取决于操作系统的线程调度策略。哪个线程先获得CPU时间片,哪个线程就先尝试获取锁。 - 锁的获取与执行:无论哪个线程先尝试获取锁,一旦成功获取,它会打印出“开始执行”的消息,然后执行耗时操作(模拟的耗时操作由
Thread.sleep(1000)实现),之后打印“执行结束”。由于ReentrantLock保证了互斥性,另一个线程在此期间会处于等待状态,直到锁被释放。 - 输出示例:
- 如果
Thread-1先获得锁,可能的输出是:
Thread-1 开始执行
(等待约1秒)
Thread-1 执行结束
(几乎紧接着)
Thread-2 开始执行
(等待约1秒)
Thread-2 执行结束
- 反之,如果
Thread-2先获得锁,输出顺序则相反,但模式相同。
- 确保的执行特点:无论线程执行的先后顺序如何,可以确保的是,两个线程不会同时执行
method方法内的代码,即打印“开始执行”到“执行结束”的过程对每个线程来说是连续且互斥的。
因此,执行结果会体现出线程的交替执行,且每个线程的“开始执行”到“执行结束”之间不会有其他线程的输出穿插。实际输出中,哪个线程先执行(即执行顺序)是不确定的,但锁的互斥性保证了执行的正确性。
在这个例子中,ReentrantLock类的实例lock用来控制对method方法的访问。当一个线程进入lock.lock()后,它就获得了锁,其他尝试进入该方法的线程将被阻塞,直到当前线程执行完临界区的代码并调用lock.unlock()释放锁。这种方式与synchronized关键字非常相似,但提供了更高级的控制选项,如尝试获取锁、定时锁等,增加了灵活性。