synchronized
约 2881 字大约 10 分钟
2025-01-22
1.synchronized特点
synchronized的特点:原子性、可见性、可重入性
原子性:一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
可见性:多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的
可重入性:单个线程可以重复拿到某个锁,锁的粒度是线程而不是调用
2.synchronized同步方式
synchronized可以修饰的地方如图所示,synchronized 上锁的资源只有两类:对象、类
- 普通的成员函数方法,属于类的实例对象
- 被static修饰的静态方法、静态属性,属于该类

举例:
public class Testl {
private int i=0;
private static int j=0;
private final Testl instance = new Test1();
//对成员函数加锁,必须获得该类的实例对象的锁才能进入同步块
public synchronized void add1(){
i++;
}
//对静态方法加锁,必须获得类的锁才能进入同步块
public static synchronized void add2(){
i++;
}
public void method(){
// 同步块,执行前必须获得Test1类的锁
synchronized(Testl.class){
}
//同步块,执行前必须先获得实例对象的锁
synchronized(instance){
}
}
}
2.1 同步代码块
结论:依赖于monitorenter和monitorexit指令
public class Test3
private static int i=0;
public void method(){
synchronized (Test3.class){
i++;
}
}
}
反编译,可得如下图:

- 由图可得,添加了synchronized关键字的代码块,多了两个指令monitorenter、monitorexit。即JVM使用monitorenter和monitorexit两个指令实现同步。
- 同步块是由monitorenter指令进入,然后monitorexit释放锁
- 在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1。当获取锁失败时会被阻塞,一直等待锁被释放。
- 第二个monitorexit是来处理异常的,仔细看反编译的字节码,正常情况下第一个monitorexit之后会执行
goto
指令,而该指令转向的就是23行的return
,也就是说正常情况下只会执行第一个monitorexit释放锁,然后返回。而如果在执行中发生了异常,第二个monitorexit就起作用了,它是由编译器自动生成的,在发生异常时处理异常然后释放掉锁。
2.2 同步方法
- 同步方法:通过方法flags标志
public synchronized void dosth(){
System.out.println("test Synchronized method");
}
反编译,可得如下图:
由图可得,添加了synchronized关键字的方法,多了ACC_SYNCHRONIZED标记。即JVM通过在方法访问标识符(flags)中加入ACC_SYNCHRONIZED来实现同步功能。
3.synchronized底层
synchronized的底层实现是完全依赖JVM虚拟机的,谈数据在JVM内存的存储:Java对象头、Monitor对象监视器。
3.1 Java对象
3.1.1 对象结构
在JVM中,对象是分成三部分:对象头、实例数据、对其填充,如下图
- 实例数据:对象真正存储的有效信息,存放类的属性数据信息,包括父类的属性信息;
- 填充数据:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
- 对象头:是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。
- 对象头主要结构是由
Mark Word
和Class Metadata Address
组成 - 其中
Mark Word
存储对象的hashCode、锁信息或分代年龄或GC标志等信息**,** Class Metadata Address
是类型指针指向对象的类元数据 ,JVM通过该指针确定该对象是哪个类的实例
- 对象头主要结构是由
下面讲讲对象头结构中的 Mark Word标记字段:
- Mark Word 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等
- 锁标志位(2bit):
- 01:该对象为无锁状态
- 00:该对象为轻量级锁,指向栈中锁记录的指针
- 10:重量级锁,指向互斥量的指针
3.1.2 加锁过程
JVM一般是这样使用锁和Mark Word的(64位JVM对象):
- 一个对象没有被当成锁时,是一个普通的对象,Mark Word标记字段记录对象的HashCode,锁标志位为01,是否偏向锁位为0
- 当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前54bit记录抢到锁的线程id,表示进入偏向锁状态
- 当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码
- 当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5
- 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
- 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
- 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞
对象怎么和monitor实现关联?
- 对象里有对象头
- 对象头里面有Mark Word 标记字段
- Mark Word指针指向了ObjectMonitor
- 每个对象都与一个monitor相关联,线程可以占有或者释放monitor。
3.2 ObjectMonitor
在Java虚拟机(HotSpot)中,Monitor(管程)是由ObjectMonitor实现的,其主要数据结构如下:
ObjectMonitor中几个关键字段:
- _count:记录owner线程获取锁的次数
- _owner:指向持有ObjectMonitor对象的线程
- _WaitSet:存放处于wait状态的线程队列
- _EntryList:存放处于等待锁block状态的线程队列
- _recursions:锁的重入次数
多个线程同时访问一段同步代码执行过程:
- 首先,要获取ObjectMonitor的线程会进入 _EntryList 集合
- 当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1
- 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet 集合中等待被唤醒
- 若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)
4.synchronized优化
JDK6 的时候,新增了两个锁状态:偏向锁、轻量级锁,并通过锁消除、锁粗化等方法使用各种场景,给synchronized性能带来了很大的提升。
无锁:
偏向锁:
- 核心思想:让同一个线程一直拥有同一个锁,直到出现竞争,才去释放锁
- 举例:如果一个线程获得了锁,那么锁就进入偏向模式,此时
Mark Word
的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word
的锁标记位为偏向锁以及当前线程ID等于Mark Word
的ThreadID即可,这样就省去了大量有关锁申请的操作。
轻量级锁(自旋锁):
- 核心思想:一个线程去申请一个已经被另一个线程占有的锁时,当前线程自旋申请持有锁,而不是阻塞
- 举例:当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁
重量级锁:
- 核心思想:当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,竞争不到锁的线程进入阻塞等待
- 重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
5.锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。 比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有变量,不存在所得竞争关系。

6.锁粗化
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。比如下面method3经过锁粗化优化之后就和method4执行效率一样了。

7.小结
7.1 synchronized和Lock区别
synchronized | Lock | |
---|---|---|
形态不同 | java关键字、jvm层次 | 接口 |
锁的释放不同 | 1.执行完同步代码,自动释放锁 2.发生异常,jvm释放锁 | 1.手动释放锁 unlock() 2.在finally里必须释放,不然会死锁 |
锁类型不同 | 可重入、非公平 | 可重入、可公平(非公平) |
是否可以尝试获取锁 | 不可以 | 可以,tryLock() |
粒度 | 粗 | 细 |
7.2 synchronized和ReentrantLock区别
synchronized | ReentrantLock | |
---|---|---|
锁类型不同 | 非公平锁 | 非公平锁、公平锁 |
锁的释放不同 | 1.执行完同步代码,自动释放锁 2.发生异常,jvm释放锁 | 手动释放锁 |
是否可以尝试获取锁 | 不可以 | 可以,tryLock() |
是否可以超时获取锁 | 不支持 | 可以,tryLock(time) |
是否可响应中断 | 不支持,不可响应线程的interrupt信号 | 支持,lockInterruptibly() |
性能 | 较差 | 比Synchronized优20% |