Synchronized关键字解析

参考文章为:

https://www.cnblogs.com/pureEve/p/6421273.html

https://www.jianshu.com/p/9932047a89be  (jvm锁降级)

http://www.cnblogs.com/charlesblc/p/5994162.html   (详解)

monitorenter、montitorexit

 

线程获取锁的状态转换:

Synchronized关键字解析_第1张图片

1. ContentionList:请求锁的线程竞争队列(阻塞队列)

2. EntryList:满足要求的队列(阻塞队列)

3. OnDeck:任一时刻仅能有一个线程去竞争锁

4. 锁拥有者线程

5. 调用wait的阻塞队列(阻塞队列)

 

描述:

方法或代码块在运行时,保证同一时刻只有一个线程进入该代码。

 

下面反编译一个代码看看他的机制:

class Sync {

    public synchronized void test(){

        System.out.println("sync test func");

    }



    public void test2(){

        synchronized (this){

            System.out.println("sync test block");

        }

    }

}



public class Test {

    public static void main(String[] args) {

        Sync sync = new Sync();

        sync.test();

        sync.test2();

    }

}

然后执行如下指令反编译并查看结果:

$ javac Test.java

$ javap -verbose Sync

Synchronized关键字解析_第2张图片

从上面可看出  同步方法使用方法修饰符ACC_SYNCHRONIZED标识,

                      同步代码块使用monitorenter、monitorexit来获取释放锁。monitorenter插入到同步代码块开始处,monitorexit插入到同步代码块结束处。

 


前提:java对象头和monitor是synchronized基础,以下讲解

Java对象头:

Java对象头一般占两个机器码(32位虚拟机中,1个机器码=4字节=32bit,如对象是数组则需多一机器码记录长度),以下是32位对象头存储结构:

Synchronized关键字解析_第3张图片

上图为Mark Word的结果图。

synchronized用的锁存在Java对象头中,包含"标记字段Mark Word"、"类型指针Klass Pointer"分别如上图区域部分。其中标记字段存储对象自身运行时数据,实现轻量级锁和偏向锁的关键。类型指针确定是哪个类的实例。MarkWord被设置为对象的HashCode,其低三位表示MarkWord状态,初始无锁状态下为001。

 

Monitor:

每个对象均会带有一个Monitor或对象锁,Monitor是线程私有的数据结构。每个线程包含(可用Monitor Record列表、全局可用列表),每个被锁住的对象都会和一个monitor关联。

Synchronized关键字解析_第4张图片

注:Owner标识该锁被对应标识的线程占用。


Java对象头有四种状态的锁(synchronized用的锁存在Java对象头中):

前提:1. Java SE1.6锁共四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状。

          2. 锁只能升级(锁膨胀),不能降级(频繁锁升降级会影响性能)。

什么是自旋锁:

在线程进入阻塞之前,可采取自旋的策略,占着cpu不放,这是为了避免线程被阻塞后进入内核调度,导致内核与用户态频繁而切换印象锁性能。最理想的自旋周期是一个CPU上下文切换时间。

 

什么是偏向锁:

    HotSpot作者大多数情况不存在多线程竞争,导致锁由同一个线程多次获得,耗费资源。为了降低锁获取代价,引入偏向锁。偏向锁会在线程访问时,在Java对象头锁记录中存储锁偏向的线程ID。在当前线程加锁或解锁情况下,测试对象头Mark Word是否含指向当前线程的偏向锁。测试成功表当前线程已获得锁,测试失败则首先确认Mark Word中偏向锁标识是否设置为1,是则尝试用CAS将对象头偏向锁指向当前线程。否则用CAS竞争。

 

    偏向锁撤销:有竞争时,当前偏向锁就释放掉。但需要等待全局安全点(当前时间点无字节码在执行)。撤销过程首先暂停所有拥有偏向锁线程,尝试直接切换。失败则继续运行,标记对象不适合偏向锁,锁膨胀(轻量级锁)。

    

    偏向锁在java6\7默认启用,并在应用程序启动延迟几秒后激活,可用-XX:BiasedLockingStartupDelay = 0关闭延迟。用-XX:-UseBiasedLocking=false 关闭偏向锁。

 

什么是轻量级锁:

    轻量级加锁:线程在执行同步之前,jvm在当前线程创建用于存储锁记录空间,并将Mark Word复制到所记录中,称"Displaced Mark Word"。然后线程尝试用CAS将Mark Word替换为指向锁记录指针。成功表示无竞争,失败则存在竞争,当前线程尝试使用自旋来获取锁,如果自旋获取失败则锁膨胀升级为重量级锁。    

    轻量级解锁:原子性CAS操作将"Displaced Mark Word"替换回对象头,成功表没竞争,失败表存在竞争,锁膨胀为重量锁。

 

锁升级的总过程:

第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁”

第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,

之前线程将Markword的内容置为空。

第三步,两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作

把共享对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord,

第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋

第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败

第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。

 

总结:偏向锁适合于单线程请求环境,如果线程一般就一个可采用偏向锁,并且取消其延时提高效率。如果一般在线程竞争环境下即可直接取消偏向锁,提高性能。锁只会升级不会降低,避免锁切换带来的额外性能消耗。

你可能感兴趣的:(JAVA)