Java synchronized的实现原理?
大家好,我是锋哥。今天分享关于【Java synchronized的实现原理?】面试题。希望对大家有帮助;
Java synchronized的实现原理?
1000道 互联网大厂Java工程师 精选面试题-Java资源分享网
Java 中的 synchronized
关键字用于实现同步控制,确保在多线程环境中对共享资源的访问是线程安全的。它的实现原理涉及到 对象监视器(monitor),锁(lock) 和 JVM 的内部机制。下面详细解释 synchronized
的实现原理。
1. 对象监视器(Monitor)
在 Java 中,每个对象都关联着一个 监视器(monitor),它用于控制对该对象的访问。每个线程在执行一个同步代码块时,需要获得该对象的监视器。
- 对于实例方法,监视器是当前对象 (
this
)。 - 对于静态方法,监视器是该类的
Class
对象。
监视器是一种互斥锁,确保只有一个线程可以访问同步代码块或方法,其他线程必须等待直到当前线程释放监视器。
2. 获取和释放锁
- 加锁:当一个线程进入同步代码块时,它会尝试获取该对象的监视器。如果没有其他线程持有锁,线程会成功获得锁,进入同步代码块或方法。
- 释放锁:当线程执行完同步代码块后,它会释放锁,允许其他线程获取该锁。
如果有多个线程试图同时访问同一对象的同步方法或代码块,只有一个线程能够获得锁,其他线程会进入 阻塞状态,直到锁被释放。
3. JVM 中的实现细节
Java 中的 synchronized
锁机制是通过 JVM 的内存模型(JMM) 和 对象头(Object Header) 实现的。
-
对象头(Object Header):每个对象在内存中都有一个对象头,其中包括两个重要的信息:锁标志 和 锁对象的引用。这些信息由 JVM 内部管理,用于同步控制。
对象头结构:
- Mark Word:包含锁相关信息,表示锁的状态。
- 指向类的引用:指向该对象所属类的元数据。
-
锁标志:在对象的 Mark Word 中,锁标志标识该对象是否已经被锁定,以及锁的类型和状态。锁的类型通常有:
- 无锁:没有线程持有锁。
- 偏向锁:仅有一个线程持有锁。适用于大部分线程访问的场景,JVM 会通过偏向锁来减少加锁和解锁的性能开销。
- 轻量级锁:在没有其他线程竞争的情况下,多个线程尝试使用锁时,会通过轻量级锁来提高效率。轻量级锁通过自旋来避免引起线程的上下文切换。
- 重量级锁:当有多个线程竞争时,轻量级锁无法解决竞争时,JVM 会将锁升级为重量级锁,进入操作系统的互斥锁机制,性能开销较大。
4. 锁的优化
JVM 在实现 synchronized
时还进行了许多优化,以提高并发性能:
-
偏向锁:偏向锁的目的是优化没有竞争的情况下,线程获得锁的速度。偏向锁会让一个线程在首次获取锁时,偏向于该线程,这样该线程后续执行时就不需要再次竞争锁,从而提高性能。
-
轻量级锁:当锁没有被其他线程占用时,JVM 会使用轻量级锁,它通过原子操作来检查和修改锁的状态,避免了阻塞和上下文切换。
-
自旋锁:当多个线程竞争同一锁时,JVM 会通过自旋锁(即线程在获取不到锁时,等待一段时间再重试,而不是立即进入阻塞状态)来减少线程的上下文切换。
-
重量级锁:当锁竞争激烈时,JVM 会将锁升级为重量级锁,这通常意味着线程会阻塞并进入内核状态。这是最耗性能的状态。
5. synchronized 的内存语义
synchronized
关键字不仅仅是实现线程同步,还具有一定的内存语义。通过 synchronized
,Java 可以确保线程之间的可见性和顺序性:
-
可见性:当一个线程修改了被
synchronized
保护的变量的值时,其他线程能够及时看到该变量的最新值。因为锁的释放和获取涉及到主内存的同步。 -
有序性:通过
synchronized
保证了线程执行顺序。一个线程在执行同步代码时,它会先将当前线程的工作内存刷新到主内存,然后其他线程在获取锁时可以看到这个修改。
6. 代码示例
下面是一个使用 synchronized
的简单示例,展示了 synchronized
如何控制对共享资源的访问:
class Counter {
private int count = 0;
// 使用 synchronized 关键字确保线程安全
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class SynchronizedExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 创建多个线程来增加计数器
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
// 等待线程执行完毕
t1.join();
t2.join();
// 输出最终的计数结果
System.out.println("Count: " + counter.getCount());
}
}
在这个例子中,increment()
和 getCount()
方法是同步的,确保了 count
的访问是线程安全的。
总结:
synchronized
关键字在 Java 中的实现依赖于 JVM 内部的对象监视器和锁机制,通过在对象的头部存储锁标志来控制对对象的并发访问。JVM 还对 synchronized
锁进行了一些优化,例如偏向锁、轻量级锁和重量级锁,以提高并发性能。synchronized
不仅保证了线程之间的互斥访问,还确保了内存的可见性和有序性。