synchronized是什么
synchronized是java同步锁,同一时刻多个线程对同一资源进行修改时,能够保证同一时刻只有一个线程获取到资源并对其进行修改,因此保证了线程安全性。
synchronized可以修饰方法和代码块,底层实现的逻辑略有不同。
Object obj=new Object(); synchronized(obj){ //do soming }
编译后的代码为:
... 10 astore_2 11 monitorenter 12 aload_2 13 monitorexit 14 goto 22 (+8) 17 astore_3 18 aload_2 19 monitorexit 20 aload_3 21 athrow 22 return
当代码执行到synchronize(obj)
时,对应的字节码为monitorenter
进行加锁操作,代码执行完后就是monitorexit
进行锁的释放。两个 monitorexit
是正常退出和异常退出两种情况下锁的释放。
public synchronized void test1(){ //do somthing }
当修饰方法时是在编译后的字节码上加上了synchronized
的访问标识
Monitor机制
Monitor是一种同步机制,它的作用是保证同一时刻只有一个线程能访问到受保护的资源,JVM中的同步是基于进入和退出监视对象来实现的,是synchronized
的底层实现,每个对象实例都是一个Montor对象,Monitor对应的是底层的MonitorObject,是基于操作系统的互斥mutex
实现的。
ObjectMonitor中有几个关键属性
属性 | 描述 |
---|---|
_owner | 指向持有ObjectMonitor对象的线程 |
_WaitSet | 存放处于wait状态的线程队列 |
_EntryList | 存放处于等待锁block状态的线程队列 |
_recursions | 锁的重入次数 |
_count | 用来记录该线程获取锁的次数 |
- 进入monitor,被分配到
Entry List
中,等待持有锁的线程释放锁, - 当线程获取到锁后,是锁的持有者,
owner
指向当前线程 - 当线程进行
wait
时进入Wait Set
,等待锁的持有者进行唤醒。
synchronized锁的实现原理
- 当代码执行到被
synchronized
修饰的代码块或方法时,首先通过monitor
去获取对象实例的锁 - 当获取到锁时,会在对象实例的
对象头
上添加锁标识位 - 没有获取到锁的线程,会进行到对对象实例的
entry list
中进行等待 - 持有锁的线程的业务处理完后通过修改
对象头
上锁标识位来进行释放锁 - 当线程进行
wait
操作时,当前也会释放锁,然后进行wait set
区等待被唤醒 - 在
entry list
中处理等待的线程再次进行锁的竞争
Mark Word
一个对象的创建要经过这几步:
- 加载:如果对象的Class还没加载
- 链接:由符号引用转换为地址引用
- 初始化:执行Class的
方法 - 开辟一个地址空间(可以使用TLAB技术进行优化,避免通过CAS产生的资源竞争)
- 初始化对象头信息
- 执行代码的
方法
7.返回对象地址
一个对象有:对象头
、实例数据
和对齐填充
三部分组成
对象头有:对象标记(Mark Word)
和类型指针
组成,如果对象是数组,对象头中还有数组的长度
在64位系统中,对象标记占8个字节,类型指针占8个字节,对象头共点16个字节
对象标记中有hashcode码
、GC年龄
、锁标记
组成
每个字节占8位,8个字节的MarkWord共占64位
在无锁
的状态下,前25位没有使用,紧接着的32位保存了对象的hashcode
,在1位未使用,后面的4位对象的GC年龄
,后面的3位是锁标记位。
为什么GC年龄不能超过16
在MarkWord中可以看出GC年龄标记只有4位,二进制表示就是:1111
,对应的十进制就是15。
下面通过jol
进行查看MarkWord的信息,
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
无锁时
import org.openjdk.jol.info.ClassLayout; public class MarkWordTest { public static void main(String[] args) { Hummy hummy=new Hummy(); int hashCode = hummy.hashCode(); System.out.println(hashCode); System.out.println("二进制:"+Integer.toBinaryString(hashCode)); System.out.println("十六进制: "+Integer.toHexString(hashCode)); System.out.println(ClassLayout.parseInstance(hummy).toPrintable()); } } class Hummy{}
打印出的结果如下:
可以看到对象的hashcode是:6f496d9f
,可以在左边的Value的找到hashcode值,只不过是反过来的。
最后1字节的00000001
包含了gc年龄和锁标记位。
加锁时
import org.openjdk.jol.info.ClassLayout; public class MarkWordTest { public static void main(String[] args) { //java -XX:BiasedLockingStartupDelay=0 Hummy hummy=new Hummy(); synchronized (hummy){ System.out.println(ClassLayout.parseInstance(hummy).toPrintable()); } } } class Hummy{}
最后一个00000101
的最后3位101
表示偏向锁
synchronized的优化
jdk1.6之前只有重量级锁,面在java1.6之后对synchronized的锁进行了优化,有偏向锁、轻量级锁、重量级锁,主要是因为重量级锁需要用到操作系统mutex
,操作系统实现线程之间的切换需要从用户态到内核态的,成本非常高。
锁 | 锁标识 | 场景 |
---|---|---|
无锁 | 001 | 不受保护时 |
偏向锁 | 101 | 只有一个线竞争时 |
轻量级锁 | 00 | 竞争不激烈时 |
重量级锁 | 10 | 竞争非常激烈 |
锁升级的过程:
- 当访问同步代码时,首先判断markword是否是
无锁状态(001)
或者在偏向锁状态下markword中的线程id与当前线程id是否一样,如果是则把当前线程id通过CAS的方式设置到markword中 - 设置成功后则锁标记修改为(101),升级为偏向当前线程的
编向锁(101)
,执行同步内的方法 - 如果失败,则由jvm进行偏向锁的撤消
- 当持有锁的线程运行到
安全点
时,检查偏向锁的状态 - 当持有锁的线程
已退出同步方法
时,释放原线程持有的锁,变成无锁状态,到1处执行 - 当持有锁的线程
还在同步代码
中,则升级锁为轻量级锁(00)
,当前线程持有,另个线程通过CAS的方法进行获取锁,当自旋到一定次数(20)时,则升级为重量级锁(10)
,进入堵塞状态。