这是我参与更文挑战的第21天,活动详情查看: 更文挑战
前言
作为学习java时遇到的第一个锁,拼写也没那么容器,但是地位却不低,还是悲观色彩的,就像一个元老级人物一样,开始学习。不过锁升级的过程这里并没有讲全,下次再回来补这块内容。
一、概述
- 三种应用场景:
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是Synchronzied括号里配置的对象
二、Java对象头
- Synchronized用的锁是存在Java对象里的,那么什么是Javd对象头呢?HotSpot虚拟机的对象头主要包括两部分数据:Mark Work(标记字段)和Klass Pointer(类型指针),虚拟机通过这个类型指针判断这个对象是哪个类的实例,Mark word默认存储对象的HashCode等运行时数据。
三、Mark Word
- Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
- 对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化
四、锁升级的对比
- JavaSE1.6引入了偏向锁和轻量级锁的状态。从低到高是 无锁,偏向锁,轻量锁,重量锁。这几个状态会随竞争情况逐渐升级。锁可以升级,但是不可以降级。目的是为了提高获得锁和释放锁的效率。
4.1 偏向锁获取
- “偏向”的意思是假定将来只有第一个申请锁的线程会使用锁(不会有任何线程来申请锁)。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后再进入退出的时候,直接把当前线程ID和Mark word的存储的线程id进行比较。一致就进入。如果Mark word的偏向锁的标识设置为1(当前是偏向锁),还是不死心,则尝试使用CAS去将Mark Word记录的线程id替换为自己的当前id。如果为0的话,表示当前无锁,用CAS去竞争锁。
4.2 偏向锁撤销
- 偏向锁使用了一种等待竞争出现才会释放锁的机制,所以当其他线程尝试竞争偏向锁的时候,持有偏向锁的线程才会释放锁。
- 1、偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)
- 2、暂停持有偏向锁的线程,判断锁对象是否还处于被锁定状态。
- 3、要么恢复到无锁(01)要么恢复到轻量级锁的状态(00)
4.3 轻量级锁加锁
- 线程在执行同步块之前,JVM会先在线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
- 1、判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3);
- 2、JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指正,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3);
- 3、判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;
4.4 轻量级锁解锁
- 会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生,如果失败,表示当前锁存在竞争,锁就会膨胀为重量级锁。