背景
在多线程环境下同时访问共享资源会出现一些数据问题,此关键字就是用来保证线程安全的解决这一问题。
内存可见的问题
在了解synchronized
之前先了解一下java内存模型,如下图:
- 线程1去主内存获取x的值读入本地内存此时x的值为1,进行运算x+1此时线程1的x值为2,然后写入主内存;
- 此时在线程1先入主内存之前,此时线程2去主内存读取了x的值,它读取到的值是1;
- 最后x的值在主内存里的值是2,线程2读取到的是1,出现了内存不可见的问题。
synchronized关键字的使用方式
修饰方法
public class SynchronizedMethodExample {
private int counter = 0;
// synchronized 修饰的方法
public synchronized void increment() {
// 这里的操作是原子的,同一时刻只有一个线程能够执行
counter++;
System.out.println("Incremented counter to: " + counter);
}
public static void main(String[] args) {
SynchronizedMethodExample example = new SynchronizedMethodExample();
// 创建多个线程,同时调用 increment 方法
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.increment();
}
});
// 启动线程
thread1.start();
thread2.start();
try {
// 等待两个线程执行完毕
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终的计数器值
System.out.println("Final counter value: " + example.counter);
}
}
由于 synchronized 修饰了 increment 方法,保证了同一时刻只有一个线程能够执行该方法。因此,两个线程在执行 increment 方法时会互斥,不会同时对 counter 进行操作。
执行结果:
Incremented counter to: 1
Incremented counter to: 2
Incremented counter to: 3
Incremented counter to: 4
Incremented counter to: 5
Incremented counter to: 6
Incremented counter to: 7
Incremented counter to: 8
Incremented counter to: 9
Incremented counter to: 10
Final counter value: 10
修饰同步代码块
public class SynchronizedBlockExample {
private int counter = 0;
private final Object lockObject = new Object(); // 用于同步的对象
public void increment() {
// 一些非同步的代码
synchronized (lockObject) {
// 需要同步的代码块
counter++;
System.out.println("Incremented counter to: " + counter);
}
// 继续执行非同步的代码
}
public static void main(String[] args) {
SynchronizedBlockExample example = new SynchronizedBlockExample();
// 创建多个线程,同时调用 increment 方法
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.increment();
}
});
// 启动线程
thread1.start();
thread2.start();
try {
// 等待两个线程执行完毕
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终的计数器值
System.out.println("Final counter value: " + example.counter);
}
}
在这个例子中,increment 方法包含了一个同步的代码块,使用 synchronized (lockObject) 对 counter 进行递增操作。由于使用了 lockObject 作为同步对象,保证了两个线程在执行同步代码块时是互斥的,不会同时对 counter 进行操作。
执行结果:
Incremented counter to: 1
Incremented counter to: 2
Incremented counter to: 3
Incremented counter to: 4
Incremented counter to: 5
Incremented counter to: 6
Incremented counter to: 7
Incremented counter to: 8
Incremented counter to: 9
Incremented counter to: 10
Final counter value: 10
修饰静态方法
public class SynchronizedStaticMethodExample {
private static int counter = 0;
// synchronized 修饰的静态方法
public static synchronized void increment() {
// 这里的操作是原子的,同一时刻只有一个线程能够执行
counter++;
System.out.println("Incremented counter to: " + counter);
}
public static void main(String[] args) {
// 创建多个线程,同时调用静态方法 increment
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
SynchronizedStaticMethodExample.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
SynchronizedStaticMethodExample.increment();
}
});
// 启动线程
thread1.start();
thread2.start();
try {
// 等待两个线程执行完毕
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终的计数器值
System.out.println("Final counter value: " + SynchronizedStaticMethodExample.counter);
}
}
在这个例子中,increment 方法是一个静态方法,使用 synchronized 修饰。由于是静态方法,它锁定的是整个类的 Class 对象。两个线程无法同时调用 increment 方法,确保了对 counter 的递增操作是线程安全的。
Incremented counter to: 1
Incremented counter to: 2
Incremented counter to: 3
Incremented counter to: 4
Incremented counter to: 5
Incremented counter to: 6
Incremented counter to: 7
Incremented counter to: 8
Incremented counter to: 9
Incremented counter to: 10
Final counter value: 10
synchronized原理
java对象的组成
在讲原理之前我们先了解一下java对象的组成:
实例数据:
- 存放类的属性数据信息,包括父类的属性信息。
对齐填充:
- 由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
对象头:
- 标志位(Flags):
- Mark Word 的一些位用于存储对象的状态标志,例如是否被锁定、是否是偏向锁、是否是轻量级锁、是否是GC标记等。这些标志位的组合形成对象的状态信息。
- 锁信息:
- 存储锁相关的信息,用于实现对象的同步机制。
- 对象可以处于无锁状态、偏向锁状态、轻量级锁状态或重量级锁状态。
- 哈希码:
- 用于支持对象的哈希操作,例如在哈希表中查找对象。
- 哈希码是对象的标识,有助于提高哈希表的性能。
- 对象分代年龄:
- 用于支持分代垃圾回收算法。
- 标识对象被创建后经历的垃圾回收次数。
- 其他:
- 可能还包含其他与垃圾回收、锁定等相关的信息。
ObjectMonitor
HotSpot虚拟机源码中ObjectMonitor.hpp。
ObjectMonitor::ObjectMonitor() {
_header = NULL; // 监视器头部,用于保存状态信息
_count = 0; // 计数器,用于记录监视器的使用次数
_waiters = 0; // 等待线程数
_recursions = 0; // 当前线程对该锁的递归次数
_object = NULL; // 监视的对象
_owner = NULL; // 拥有锁的线程
_WaitSet = NULL; // 等待队列,存储等待该锁的线程
_WaitSetLock = 0 ; // 用于保护等待队列的锁
_Responsible = NULL ; // 释放锁的线程
_succ = NULL ; // 后继监视器
_cxq = NULL ; // 入口等待队列
FreeNext = NULL ; // 空闲监视器链表的下一个
_EntryList = NULL ; // 入口列表
_SpinFreq = 0 ; // 自旋频率
_SpinClock = 0 ; // 自旋时钟
OwnerIsThread = 0 ; // 拥有者是否为线程
}
ObjectMonitor.hpp
是 HotSpot 虚拟机(OpenJDK 的默认虚拟机实现)中用于实现对象监视器的头文件。对象监视器在 Java 中由 synchronized
关键字提供支持,用于实现多线程之间的同步。以下是对 ObjectMonitor.hpp
的一些关键部分的简要解释:
- ObjectMonitor 结构体:
ObjectMonitor
是一个结构体,表示对象监视器。它包含了维护监视器状态和控制线程访问的各种信息。主要字段包括:
header
:用于保存监视器的状态信息,如锁的状态、等待队列等。owner
:指向当前拥有锁的线程。wait_set
:等待队列,用于存储等待该锁的线程。- 等等。
- ObjectMonitor 头部(header):
ObjectMonitor
的头部包含了一系列标志位,用于表示锁的状态、等待队列的状态等。这些标志位在字节层面上被设置和检查,以进行对锁的操作。一些常见的标志位有:
INFLATED
:表示锁已经被膨胀,即从轻量级锁升级为重量级锁。CONTENTION
:表示锁有竞争。HELD_EXCLUSIVELY
:表示锁被当前线程独占。
- 等待队列(WaitSet):
ObjectMonitor
中包含一个等待队列,用于存储等待该锁的线程。线程在等待队列中等待时,它会进入等待状态,直到被唤醒。等待队列的管理涉及到线程的入队和出队等操作。 - 线程入队和出队:
ObjectMonitor
定义了一些方法,用于线程的入队和出队操作。例如:
void enter(Handle h)
:线程尝试进入临界区。void exit()
:线程退出临界区。void wait(bool, jlong, jlong)
:线程进入等待状态。void notify()
和void notifyAll()
:唤醒一个或所有等待线程。
- 锁的状态转换:
ObjectMonitor
定义了一些方法来实现锁状态的转换,例如从无锁到轻量级锁、从轻量级锁到重量级锁等。这些状态的转换涉及到了底层的原子操作和 CAS(Compare and Swap)等机制。 - 适应性自旋锁:
HotSpot 虚拟机中的ObjectMonitor
还包括适应性自旋锁的机制,该机制用于在获取锁时进行自旋,以避免线程进入阻塞状态。适应性自旋锁的目标是根据程序运行时的历史信息来动态调整自旋次数,以提高性能。
-
当多个线程同时访问同步代码块时,首先会进入到
EntryList
中,然后通过CAS的方式尝试将Monitor
中的owner
字段设置为当前线程,同时count
加1,若发现之前的owner
的值就是指向当前线程的,recursions
也需要加1。如果CAS尝试获取锁失败,则进入到EntryList中; -
当获取锁的线程调用wait()方法,则会将owner设置为null,同时
count
减1,recursions
减1,当前线程加入到WaitSet中,等待被唤醒; -
当前线程执行完同步代码块时,则会释放锁,
count
减1,recursions
减1。当recursions的值为0时,说明线程已经释放了锁.
synchronized作用于同步代码块的字节码指令
在Java中,synchronized
作用于同步代码块的字节码指令主要涉及到 monitorenter
和 monitorexit
指令。这两个指令用于实现监视器(monitor)的进入和退出,即获取和释放锁。
以下是一个简单的Java同步代码块的例子:
public class SynchronizedExample {
private static final Object lock = new Object();
public void synchronizedMethod() {
synchronized (lock) {
// 同步代码块
// ...
}
}
}
对应的字节码大致如下:
public synchronizedMethod()V
L0
LINENUMBER 7 L0
GETSTATIC SynchronizedExample.lock : Ljava/lang/Object;
DUP
ASTORE 1
MONITORENTER // monitorenter 指令,获取锁
// 同步代码块的字节码指令
// ...
L1
LINENUMBER 9 L1
ALOAD 1
MONITOREXIT // monitorexit 指令,释放锁
ATHROW
解释一下上述字节码:
GETSTATIC SynchronizedExample.lock : Ljava/lang/Object;
: 获取lock
字段的值,即获取锁对象。DUP
: 复制栈顶数值,用于后续的ASTORE 1
操作。ASTORE 1
: 将锁对象的引用存储到局部变量1。MONITORENTER
: 进入监视器,即获取锁。如果锁已经被其他线程占用,当前线程将阻塞等待。- 同步代码块的具体实现。
ALOAD 1
: 将之前存储的锁对象引用加载到栈顶。MONITOREXIT
: 退出监视器,即释放锁。ATHROW
: 抛出异常,确保在任何情况下都会释放锁。
synchronized作用于方法字节码指令
以下是一个简单的例子,演示了 synchronized
修饰方法的字节码指令:
public class SynchronizedMethodExample {
private int counter = 0;
public synchronized void synchronizedMethod() {
counter++;
}
}
对应的字节码可能类似于:
public class SynchronizedMethodExample {
private int counter;
public SynchronizedMethodExample() {
counter = 0;
}
public synchronized void synchronizedMethod() {
// 获取锁
monitorenter
try {
// 同步代码块
counter++;
} finally {
// 释放锁
monitorexit
}
}
}
在这个例子中:
monitorenter
指令:在进入同步代码块之前获取锁。monitorexit
指令:在同步代码块执行完毕后释放锁。
synchronized锁的优化
JDK1.5之前,synchronized
是属于重量级锁,重量级需要依赖于底层操作系统的Mutex Lock
实现,然后操作系统需要切换用户态和内核态,这种切换的消耗非常大,所以性能相对来说并不好。
- 偏向锁(Biased Locking):
- 在程序刚启动时,对象的锁大多数情况下只被一个线程所持有。为了提高性能,引入了偏向锁机制。
- 当一个线程获取了对象的锁后,会在对象头的 Mark Word 中记录这个线程的 ID,表示这个锁被偏向于该线程。之后,该线程再次进入同步块时,无需再竞争锁,直接获得。
- 偏向锁的目标是降低无竞争情况下的锁操作的开销。
- 轻量级锁(Lightweight Locking):
- 当多个线程争夺同一个锁时,偏向锁会升级为轻量级锁。
- 轻量级锁使用 CAS 操作来避免传统的互斥量(Mutex)的开销。如果有多个线程竞争同一个锁,会使用 CAS 操作来尝试获取锁,而不是阻塞线程。
- 自旋锁和自适应自旋锁:
- 在无法获取锁时,线程可能会进行一定次数的自旋等待。自旋是一种忙等待的策略,避免了线程的阻塞和唤醒带来的开销。
- 自适应自旋锁会根据锁的持有时间和竞争情况来动态调整自旋的次数,以在不同的场景下取得更好的性能。
- 锁消除和锁粗化:
- 锁消除是指在编译期间,对一些明显不会发生竞争的锁进行消除,从而减少锁的使用。
- 锁粗化是指将多个连续的操作都加锁,从而减少加锁和解锁的次数。
总结
synchronized
是 Java 中用于实现线程同步的关键字,它提供了对代码块、方法以及静态方法的同步支持。以下是关于 synchronized
锁的总结:
- 对象锁:
synchronized
可以用于实现对对象的同步,确保同一时刻只有一个线程能够访问被同步的代码块或方法。- 对象锁的粒度可以是对象实例(实例方法)或类(静态方法)。
- 方法锁:
synchronized
可以直接修饰方法,使整个方法具有同步性。此时,锁对象是方法所属的对象实例。- 修饰实例方法时,锁对象是方法调用的实例;修饰静态方法时,锁对象是类的 Class 对象。
- 代码块锁:
synchronized
还可以用于修饰代码块,指定锁的粒度更加灵活。- 在代码块中,需要指定一个对象作为锁,多个线程只有在获取了相同的锁时才会争夺执行权。
- 锁的释放:
- 当一个线程获得了对象锁后,其他线程必须等待该线程释放锁才能进入同步代码块或方法。
- 如果线程执行的同步代码块出现异常,锁会被自动释放。
- 偏向锁、轻量级锁和重量级锁:
- 为了减小锁的操作开销,Java 中引入了偏向锁、轻量级锁和重量级锁的概念。
- 在无竞争的情况下,偏向锁能够提高性能;在低竞争的情况下,轻量级锁可以减小锁的争用开销。
- 可重入性:
synchronized
具有可重入性,即同一个线程可以多次获得同一把锁而不会出现死锁。
- 性能优化:
- Java 虚拟机和编译器对
synchronized
进行了一系列优化,包括偏向锁、轻量级锁、自适应自旋等,以提高多线程程序的性能。
总体而言,synchronized
是一种简单而有效的同步机制,用于确保多线程程序中对共享资源的安全访问。然而,在一些高并发场景下,可能需要考虑使用更灵活的锁机制,如 java.util.concurrent
包中提供的锁。
参考
https://zhuanlan.zhihu.com/p/377423211
https://juejin.cn/post/6973571891915128846