总而言之,每个对象都有对象头,对象头里有mark word,存储对象的元数据信息的指针,hashcode,分代年龄,以及同步锁相关信息,假如是偏向锁,那存储的是偏向线程的id,假如是轻量级锁,那么存储的就是指向线程栈中的锁记录指针。假如是重量级锁,指向互斥量的指针。(这里暂时不展开讨论互斥量)
1 java对象头
对象头结构:java对象在Heap里面的结构是这样的:对象头跟对象体,对象体跟C里面的结构体是一样的,对象头由两个域组成:用于存放hashcode、同步、GC的_mask域,和指向方法区该对象Class对象的指针——_klass域,对于64位系统,头部长度理论上讲应该是8+8=16字节。但是从java6u23以后开始,64位的机器会自动开启指针压缩的功能,此时引用指针的长度为4字节。所以,对象头长度应该为8+4=12。
和同步锁相关的只有mark word 对象,每个mark word 都和native 实现的markOopDesc,然后这个对象的monitor()方法,返回一个ObjectMonitor对象。
2 线程如何获取同步锁
首先在synchronized的实现中,线程获取锁的时候,需要经过几个状态转换。要排队竞争。最后只有一个线程拿到锁。
2.1 偏向锁
假如jvm 设置UseBiasedLock=true
那么在只有单个线程
争抢这把锁的时候,线程第二次进入,则不需要获取锁。
*偏向锁这里有一个安全点停顿,stop the word ,会导致性能下降,要查看安全点停顿,可以打开安全点日志,通过设置JVM参数 -XX:+PrintGCApplicationStoppedTime 会打出系统停止的时间,添加-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 这两个参数会打印出详细信息,可以查看到使用偏向锁导致的停顿,时间非常短暂,但是争用严重的情况下,停顿次数也会非常多;
后面再研究,这里先记录一下
然后 假如又过来一个线程,在争抢锁的时候,发生raceCondition.就会升级成轻量锁。
当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:
Contention List:所有请求锁的线程将被首先放置到该竞争队列
Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set
OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Owner:获得锁的线程称为Owner
!Owner:释放锁的线程
如下图所示。
当发生锁争用的时候,
2.2 自旋锁
假如说,一个线程来了之后,直接在ContentionList中等待,从用户态切换到内核态,其实COntentionList,EntryList ,WaitSet,都是阻塞状态。这样的话,一个线程进来等待锁,然后一个线程释放锁,进入waitSet,就需要两次的模式切换,这个对于系统资源的消耗,是很大的。
那假如说,一个线程来了之后,稍微等一下,自旋一下
,可能owner已经释放锁了,就不需要模式切换了,这样子就提高了性能。总而言之,就是尽可能的不要进入阻塞状态。
2.3 轻量锁
在进入轻量锁的时候,会有一个,在对象的mark word中存放线程栈中的Lock Record 记录的指针,然后Lock Record 中的owner 指向mark word .假如这个displaced mark word CAS替换成功的话,更新mark word 锁状态值是‘00’代表是轻量级锁,如果不成功,就锁膨胀成重量级锁。
最后CAS放到后面讲吧,毕竟后面的juc都是基于cas操作,讲的更深入一些。