概述
Synchronized是Java中保证线程安全的使用较多的一个关键字
使用场景
- 修饰实例方法,对当前实例对象this加锁
public class Synchronized {
public synchronized void aaa(){
}
}
- 修饰静态方法,对当前类的Class对象加锁
public class Synchronized {
public synchronized static aaa(){
}
}
- 修饰代码块,指定一个加锁的对象,给对象加锁
public class Synchronized {
public void aaa(){
synchronized(new test()){
}
}
}
其实就是锁方法、锁代码块和锁对象
Java 对象在内存中的结构
- 对象头
- Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
- Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
- 实例数据
- 对齐填充
- 由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
- 一个空对象占多少个字节
- 就是8个字节,是因为对齐填充的关系,不到8个字节对齐填充会帮我们自动补齐
synchronized 锁使用的就是对象头的 Mark Word 字段中的一部分 Mark Word 中的某些字段发生变化,就可以代表锁不同的状态 如果当前对象是无锁状态,对象的 Mark Word 如图所示: 该对象头的 Mark Word 字段分为四个部分:
- 对象的 hashCode ;
- 对象的分代年龄,这部分用于对对象的垃圾回收;
- 是否为偏向锁位,1代表是,0代表不是;
- 锁标志位,这里是 01。
synchronized关键字的实现原理
这里看实现很简单,我写了一个简单的类,分别有锁方法和锁代码块,我们反编译一下字节码文件,就可以了。
public class Synchronized {
public synchronized void husband(){
synchronized(new Volatile()){
}
}
}
编译完成,我们去对应目录执行 javap -c xxx.class 命令查看反编译的文件
public class juc.Synchronized
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#14
#2 = Class #15
#3 = Class #16
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Ljuc/Synchronized;
#11 = Utf8 husband
#12 = Utf8 SourceFile
#13 = Utf8 Synchronized.java
#14 = NameAndType #4:#5
#15 = Utf8 juc/Synchronized
#16 = Utf8 java/lang/Object
{
public juc.Synchronized();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljuc/Synchronized;
public synchronized void husband();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=3, args_size=1
0: ldc #2
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: return
Exception table:
from to target type
5 7 10 any
10 13 10 any
LineNumberTable:
line 10: 0
line 12: 5
line 13: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Ljuc/Synchronized;
}
同步方法 编译器会为其自动生成了一个 ACC_SYNCHRONIZED 关键字用来标识。同步方法的时候,一旦执行到这个方法,就会先判断是否有标志位,然后,ACC_SYNCHRONIZED会去隐式调用刚才的两个指令:monitorenter和monitorexit。所以归根究底,还是monitor对象的争夺。
同步代码块 当我们进入一个方法的时候,执行monitorenter,就会获取当前对象的一个所有权,这个时候monitor进入数为1,当前的这个线程就是这个monitor的owner 如果你已经是这个monitor的owner了,你再次进入,就会把进入数+1. 同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。
为什么会有两个 monitorexit 指令呢? 正常退出,得用一个 monitorexit 吧,如果中间出现异常,锁会一直无法释放。所以编译器会为同步代码块添加了一个隐式的 try-finally 异常处理,在 finally 中会调用 monitorexit 命令最终释放锁。
优化锁升级
JDK1.6之前Synchronized是重量级锁, 是ObjectMonitor调用的过程 ObjectMonitor源码中Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,对应的线程就是park()和upark(),这个操作涉及用户态和内核态的转换了,这种切换是很耗资源的,所以效率才低
JDK1.6之后对Synchronized做了优化 升级方向:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 注意:这个升级过程是不可逆的
偏向锁 偏向锁的申请流程
- 首先需要判断对象的 Mark Word 是否属于偏向模式,如果不属于,那就进入轻量级锁判断逻辑。否则继续下一步判断;
- 判断目前请求锁的线程 ID 是否和偏向锁本身记录的线程 ID 一致。如果一致,继续下一步的判断,如果不一致,跳转到步骤4;
- 判断是否需要重偏向,重偏向逻辑在后面一节批量重偏向和批量撤销会说明。如果不用的话,直接获得偏向锁;
- 利用 CAS 算法将对象的 Mark Word 进行更改,使线程 ID 部分换成本线程 ID。如果更换成功,则重偏向完成,获得偏向锁。如果失败,则说明有多线程竞争,升级为轻量级锁。
轻量级锁 该对象头Mark Word分为两个部分。第一部分是指向栈中的锁记录的指针,第二部分是锁标记位,针对轻量级锁该标记位为 00。
轻量级锁的上锁步骤
- 如果当前这个对象的锁标志位为 01(即无锁状态或者轻量级锁状态),线程在执行同步块之前,JVM 会先在当前的线程的栈帧中创建一个 Lock Record,包括一个用于存储对象头中的 Mark Word 以及一个指向对象的指针。
- JVM接下来会利用CAS尝试把对象原本的Mark Word 更新为Lock Record的指针,成功就说明加锁成功,改变锁标志位,执行相关同步操作。
- 如果失败了,就会判断当前对象的Mark Word是否指向了当前线程的栈帧,是则表示当前的线程已经持有了这个对象的锁,否则说明被其他线程持有了,继续锁升级,修改锁的状态,之后等待的线程也阻塞。
补充
用户态和内核态 Linux系统的体系结构分为用户空间(应用程序的活动空间)和内核。 我们所有的程序都在用户空间运行,进入用户运行状态也就是(用户态),但是很多操作可能涉及内核运行,比我I/O,我们就会进入内核运行状态(内核态)。
- 用户态把一些数据放到寄存器,或者创建对应的堆栈,表明需要操作系统提供服务。
- 用户态执行系统调用(系统调用是操作系统的最小功能单位)。
- CPU切换到内核态,跳到对应的内存指定的位置执行指令。
- 系统调用处理器去读取我们先前放到内存的数据参数,执行程序的请求
- 调用完成,操作系统重置CPU为用户态返回结果,并执行下个指令
synchronized和Lock
- synchronized是关键字,是JVM层面的底层啥都帮我们做了,而Lock是一个接口,是JDK层面的有丰富的API
- synchronized会自动释放锁,而Lock必须手动释放锁
- synchronized是不可中断的,Lock可以中断也可以不中断
- 通过Lock可以知道线程有没有拿到锁,而synchronized不能
- synchronized能锁住方法和代码块,而Lock只能锁住代码块
- synchronized是非公平锁,ReentrantLock可以控制是否是公平锁
|