笔记-java中的锁

java对象

java对象=对象头+实例数据+对其填充

java对象头

对象头=MarkWord+class pointer+array length(若对象为数组)

锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。

//普通对象:
|-------------------------------- -------------------------|
|                   Object Header (64 bits)                |
|--------------------------------|-------------------------|
|       Mark Word (32 bits)      |    Klass Word (32 bits) |
|--------------------------------|-------------------------|

//数组对象:   
|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|

对象头的组成

Mark Word

32位JVM的Mark Word默认存储结构如下

class pointer

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。

array length

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度。

总结

长度 内容 说明
32/64bit Mark Word 存储对象的hashCode或锁信息等
32/64bit Class Metadata Address 存储到对象类型数据的指针
32/64bit Array length 数组的长度(如果当前对象是数组)

自旋锁

自旋锁原理:如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,线程不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自选时间阈值:自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!

JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化

  1. 如果平均负载小于CPUs则一直自旋
  2. 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
  3. 如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞
  4. 如果CPU处于节电模式则停止自旋
  5. 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)
  6. 自旋时会适当放弃线程优先级之间的差异

偏向锁

偏向锁:简单来说就是一段同步代码块一直被一个线程所访问,那么该线程就会自动获取锁,降低获取锁的代价。

它会偏向最先获得它的线程,当一个线程访问同步代码块获得锁时,会在对象头和栈帧记录里存储锁偏向的线程ID,当这个线程再次进入同步代码块时,就不需要CAS操作来加锁了,只要测试一下对象头里是否存储着指向当前线程的偏向锁。如果测试成功,则表明该线程已经获得了锁,如果失败,则减产偏向锁的标示是否设为1,也就是当前是否是偏向锁,如果是,则尝试用CAS操作将对象头的偏向锁指向当前线程,如果不是,则用CAS竞争锁。

撤销: 当有其他线程竞争偏向锁的时候,持有偏向锁的线程才会释放锁。当前持有偏向锁的线程如果处于不活动的状态,则直接将对象头设置为无锁状态;如果线程仍然活动,拥有偏向锁的栈会被执行,然后将锁偏向于其他线程,或者恢复到无锁状态。

在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。

轻量级锁

轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。其它线程会通过自选的形式尝试获取锁,不会阻塞,提高性能。

**优点:**轻量级锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!

**缺点:**但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用轻量级锁了,因为轻量级锁在获取锁前一直都是占用cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭轻量级锁;

重量级锁

重量级锁是指当锁为轻量级锁时,另一个线程虽是自旋,自旋一定的次数任然未获得锁就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其它线程阻塞,降低性能。