概念
synchronized是Java中用于实现线程同步的关键字。它提供了一种机制,确保在多线程环境中对共享资源的安全访问。当一个线程获取了对象的锁,其他线程必须等待该线程释放锁才能继续执行,从而保证了对共享资源的互斥访问.
使用方式
synchronized的使用大致为3种:
// 举例
public class SynchronizedUseTest {
private int count = 0;
private static int count2 = 0;
private final Object lock = new Object();
public synchronized void increment1() {
count++;
}
public static synchronized void increment3() {
count2++;
}
public void increment2() {
synchronized (lock) {
count++;
}
}
public static void main(String[] args) {
SynchronizedUseTest obj1 = new SynchronizedUseTest();
// 1.同步普通方法,锁对象是当前实例(this)即obj1
obj1.increment1();
// 2.同步静态方法,锁对象是当前类的 SynchronizedUseTest.Class 对象
SynchronizedUseTest.increment3();
// 3.同步代码块,锁对象是自定义的对象 lock
obj1.increment2();
}
}
// 1.普通方法
对象锁,锁得是当前实例对象
// 2.静态方法
类锁,锁得是当前类的class对象
// 3.同步代码块
锁得是括号里的对象
// 注:
一个类只有一个class,可能有多个实例对象. 若一个类有多个静态方法,可能会造成性能影响。
底层原理
Monitor
每个 Java 对象都有一个对象头(Object Header),其中包含了对象的元数据信息,如对象的哈希码、GC 信息等。对象头中的一部分被用来存储锁状态信息,其中的 Monitor 用来管理与对象锁相关的信息。这个 Monitor 包含了持有锁的线程、等待获取锁的线程队列、重入次数等信息,用于实现 synchronized 关键字的锁机制,确保多线程环境下的安全访问。
// Monitor对象组成
ObjectMonitor() {
_header = NULL; // 指向ObjectMonitor的头部信息
_count = 0; // 记录此ObjectMonitor的引用计数
_waiters = 0, // 表示正在等待获取对象锁的线程数目
_recursions = 0; // 记录当前线程已经获取锁的次数,用于实现锁的重入性
_object = NULL; // 指向当前被监视的对象。
_owner = NULL; // 记录当前持有该对象锁的线程引用。
_WaitSet = NULL; // 存储处于等待(wait)状态的线程,这些线程会被加入到该队列中
_WaitSetLock = 0 ; // 用于保护 _WaitSet 操作的锁
_Responsible = NULL ; // 用于标记负责通知对象锁状态改变的线程。
_succ = NULL ; // 用于将Monitor对象链接成一个链表
_cxq = NULL ; // EntryList 和 WaitSet 上等待获取锁的线程列表。
FreeNext = NULL ; // 空闲Monitor对象的下一个指针。
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ; // 用于实现自旋锁的自旋次数。
_SpinClock = 0 ; // 自旋锁的时钟。
OwnerIsThread = 0 ; // 用于标识当前持有锁的是否是线程,以便区分偏向锁的状态。
}
字节码层面
首先来看字节码层面
// increment1()、increment3()
同步方法,字节码方法上有synchronized修饰
// increment2()
同步代码块被synchronized修饰的部分有两个指令monitorenter 和monitorexit
// -> 解释下这两个指令
monitorenter指令:会尝试获取对象的Monitor锁.
monitorexit指令:会释放对象的Monitor锁.
执行过程
以increment2()为例:
当有多个线程同时调用 increment2() 方法时,每个线程都会尝试获取 lock 对象的 Monitor 锁。假设当前 lock 对象未被任何线程锁定,三个线程 A、B、C 同时进入方法中的 synchronized 代码块。以下是它们获取锁的大致过程:
// 1.竞争获取锁
线程A、B、C 同时进入方法,并同时尝试获取lock对象的Monito锁。
// 2.锁定对象
假设A线程成功获取了lock对象的Monitor锁,可以进入同步代码块执行操作
// 3.其他线程阻塞
同时其他线程(B、C)处于等待状态,进入_EntryList队列
// 4.线程执行与锁释放
线程A在完成synchronized代码块中的操作后,执行monitorexit指令释放lock对象的Monitor锁。
// 5.等待线程唤醒
释放锁后,等待lock对象的Monitor锁的线程(B、C)中的一个会被唤醒,继续尝试获取锁。
// 6.重复过程
被唤醒的线程继续竞争获取锁,其中一个成功获取锁,可以进入同步代码块执行,其他线程继续等待。
这样,通过 Monitor 锁的机制,确保了在同一时间内只有一个线程可以进入同步代码块执行操作,其他线程需要等待锁的释放后才能继续执行。这样保证了对共享资源的安全访问。
注:
Synchronized修饰的方法,在底层,这个同步方法的实现也是基于 monitorenter 和 monitorexit 指令来管理对方法的同步访问。
锁升级
锁信息展示(32位虚拟机),对象头的 Mark Word
synchronized锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
// 根据上图来打印各个锁状态
// 1.无锁状态
Object obj1 = new Object();
System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
// 输出对象头信息 -> 最后三位001,代表无锁
00000000 00000000 00000000 00000001
// 2.偏向锁
// -- jvm启动的时候会延迟启动偏向锁,java本身内部有竞争,为了减少锁的升级,所以延迟了.
Thread.sleep(5000);
Object obj2 = new Object();
synchronized (obj2){
System.out.println(ClassLayout.parseInstance(obj2).toPrintable());
}
// 输出对象头信息 -> 最后三位101,代表偏向锁
00100011 10000000 10010000 00000101
// 3.轻量级锁
Object obj3 = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj3) {
System.out.println("t1:" + ClassLayout.parseInstance(obj3).toPrintable());
}
}
});
t1.start();
t1.join();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj3) {
System.out.println("t2:"+ClassLayout.parseInstance(obj3).toPrintable());
}
}
}).start();
// 输出对象头信息 -> 最后00,代表轻量级锁
00000111 00001011 01111010 01000000
// 4.重量级锁
// 在3的基础上再开一个线程,就可以模拟出重量级锁
// 输出对象头信息 -> 最后三位010,代表重量级锁
00111100 00000010 01010100 00001010
升级过程
当涉及到synchronized锁升级时,通常会涉及偏向锁、轻量级锁和重量级锁。我会给出一个简单的示例来说明锁升级的过程。
public class SynchronizedUpgradeExample {
private static final Object lock = new Object();
private static int count = 0;
public static void main(String[] args) {
// 创建线程A
Thread threadA = new Thread(() -> {
synchronized (lock) { // 获取锁
for (int i = 0; i < 1000000; i++) {
count++;
}
}
});
// 创建线程B
Thread threadB = new Thread(() -> {
synchronized (lock) { // 获取锁
for (int i = 0; i < 1000000; i++) {
count--;
}
}
});
// 启动线程A和线程B
threadA.start();
threadB.start();
try {
// 等待两个线程执行完毕
threadA.join();
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终结果
System.out.println("Final count: " + count);
}
}
// 描述过程
1.无锁状态
lock最开始的状态为无锁状态.
2.偏向锁
- 当线程A进入同步块时,它会尝试偏向于这个锁对象lock,并将对象头中的 Mark Word 设置为偏向锁状态。
- 因为只有一个线程访问这个锁,所以不会发生竞争。线程 A 能够顺利完成对 count 的累加操作。
3.轻量级锁
- 当线程 B 尝试获取锁时,会发现锁对象已经处于偏向锁状态,但它无法获取偏向锁,于是升级为轻量级锁。
- 轻量级锁使用CAS操作来尝试获取锁,这里可能会有一些自旋操作来争夺锁
4.重量级锁
- 如果轻量级锁的自旋操作未能成功,表示竞争较为激烈,锁会升级为重量级锁。
- 重量级锁会使用操作系统的互斥量来确保线程的互斥访问,这可能会导致线程的阻塞和内核态的切换。
结合流程图:
总的来说,synchronized 是 Java 中用于实现线程安全的关键字,提供了一种简单易用的方式来确保多线程环境下共享资源的安全访问。