Featured image of post Java 中的对象 -- 对象与锁

Java 中的对象 -- 对象与锁

对象和锁的关系,hashcode 对偏向锁的影响

Java 中的对象 – 对象基本信息中我们主要讲述了对象的基本信息,围绕对象的组成,我们针对对象的创建流程、大小以及对象头的信息有了详细的介绍,最后还延伸了对象头和锁的关系,那么接下来我们就针对对象头和锁的关系聊一聊剩下的问题。

旧博客翻新,原博客写于 Mar 19,2020

问题

问题五:对象头和锁的关系

Java中的加锁方式简要来分可以分为使用synchronized和使用Lock两种方式,其中synchronized是使用的java对象的内置锁来实现的,而Lock的锁方式,是基于AQS来实现的,这里主要讲的是synchronized对应的对象内置锁,因为这个锁才和Java对象有着千丝万缕的联系。

Java 1.6synchronized进行了大幅度的优化,其性能也有了大幅度的提升。Java 1.6引入了偏向锁轻量级锁的概念,减少了获得锁和释放锁的消耗。在Java 1.6之后,加上原有的重量级锁,锁一共有4种状态,分别是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。目前由于**Java虚拟机规范**中并没有明确虚拟机是否支持锁升级和降级,因此不同的虚拟机,可能虚拟机支持锁策略也不同,部分虚拟机可能仅支持锁的升级,不支持锁降级;但是这里必须要提一下,HotSpot VM是支持锁的升降级的,但是由于降级效率较低,基本可以认为不支持降级。

接下来,先来看看对象中的几种锁以及这些锁是如何进行升级的。

无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功退出,否则就继续循环尝试,如果有多个线程修改同一个值,必定会有一个线程修改成功,而其他修改失败的线程会不断重试直到修改成功。无锁基于CAS实现,在某些条件下,无锁的效率还是较高的,但是无锁无法完全替代有锁。

偏向锁

偏向锁是指同一段代码一直被一个线程访问,那么该线程会自动获取锁,降低获取锁的代价。

在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争关系,所以出现了偏向锁,其目标就是当只有一个线程执行的时候,同步代码块能够提高性能。

当一个线程访问同步代码块并获取锁的时候,会先在对象的Mark Word中记录锁偏向的线程ID,在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word中记录的线程ID是否是当前线程ID;引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行逻辑,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换Thread ID的时候依赖一次CAS操作即可。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁,偏向锁的撤销需要等待全局安全点(SafePoint)时进行,在安全点时,会先暂停持有偏向锁的线程,判断锁对象是否处于被锁定的状态,如果被锁定,那么将锁升级为轻量级锁,其他线程自旋获取轻量级锁,否则先将该对象锁恢复为无锁,之后将锁偏向另外的线程。

轻量级锁

当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取,不会阻塞。

当线程进入同步块的时候,此时同步对象状态有两种,一种是无锁,另外一种是持有轻量级锁。那么便有以下两种情况:

  • 无锁时(标志位位01,是否偏向锁为0)虚拟机会首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象当前的Mark Word的拷贝(即Displaced Mark Word),然后将对象头中的Mark Word复制到锁记录中;拷贝成功之后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock RecordDisplaced Mark Word地址。此时CAS操作如果成功,那么就表示当前线程拥有了该对象的锁,并且对象Mark Word的锁标志被设置为00,表示此对象处于轻量级锁定状态。如果CAS操作失败,那么说明在当前线程尝试更新Mark Word的时候,有一个线程已经更新完了Mark Word中的信息,当前线程只能进入阻塞状态。
  • 线程进入同步块,发现已经存在线程持有了该对象锁时,那么当前线程就会先判断对象的Mark Word是否是当前线程的栈帧,如果是,说明存在一个递归调用关系在里面,那么直接进入同步块内部执行,否则说明存在多个线程竞争锁,如果当前没有线程在等待,那么该线程就会自旋进行等待;但是当自旋超过一定的次数,或者超过一个线程在等待,那么轻量级锁就会升级为重量级锁。

重量级锁

升级为重量级锁是,锁标志的状态值变为10,此时Mark Word中存储的是指向重量级锁的指针,这时等待锁的线程都会进入阻塞状态。

综述

偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作,而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能,重量级锁是将除了拥有锁的线程意外的线程都阻塞。

问题六:hashCode是什么?hashCode和对象的关系?

从上面几个问题的解答,我们能够发现,在对象处于无锁状态时,Mark Word中存储的是对象的hashcode,这个我们很熟悉,在每一个类中都会存在一个native方法,那就是hashcode(),这个方法用于计算对象的哈希值,可以和equals()方法配合来比较两个对象是否相等。这里插一句,不同的对象可能会生成相同的hashcode值,所以不能只根据hashcode值判断两个对象是否是同一对象,但是如果两个对象的hashcode值不等,则必定是两个不同的对象,也就是说在比较对象是否为同一对象的时候,先判断两个对象的hashcode是否真正相等,如果不相等,那么肯定不是同一对象,如果相等,然后使用equals方法,如果相等就是同一对象。

一般来说,我们都是认为hashcode对应的是对象的内存地址,其实这样的说法不太准确。举个例子:我们都知道垃圾整理算法会将存活的对象移动到内存的连续区域,那么移动前后这个对象对应的地址肯定发生了变化,那这个对象对应的hashcode难道变化了?肯定不会变化!

某些情况下,我们会重写equals()方法,这样会导致hashCode()方法也会被重写,进而我们我们通过object.hashCodde()获取到的hashcode值肯定不会是对象头中存储的hashcode值,那么此时如何获取到对象头中的hashcode值呢?其实Java中还存在一个对象hashcode值的方式,那就是通过使用System.identityHashCode(Object)来获取对象的hashcode值,此时获取到的永远都是Mark Word中的值。

那么,hashcode值究竟是如何生成的呢?我想还是需要去看看源码

synchroizer.cpp中的get_next_hash()方法中,介绍了几种计算hashcode的算法,如下所示(有删减):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// hashCode() generation
static inline intptr_t get_next_hash(Thread * Self, oop obj) {
  intptr_t value = 0 ;
  if (hashCode == 0) {
     // 系统生成的随机数
     value = os::random() ;
  } else if (hashCode == 1) {
     // 对对象的内存地址进行二次计算
     // This can be useful in some of the 1-0 synchronization schemes.
     intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
     value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
  } else if (hashCode == 2) {
     value = 1 ;                            // 用于敏感性测试
  } else if (hashCode == 3) {
     value = ++GVars.hcSequence ;           // 自增的序列
  } else if (hashCode == 4) {
     value = cast_from_oop<intptr_t>(obj) ; // 对象的内存地址,转为int
  } else {
     // hashCode == 5
     // Marsaglia's xor-shift scheme with thread-specific state
     unsigned t = Self->_hashStateX ;
     t ^= (t << 11) ;
     Self->_hashStateX = Self->_hashStateY ;
     Self->_hashStateY = Self->_hashStateZ ;
     Self->_hashStateZ = Self->_hashStateW ;
     unsigned v = Self->_hashStateW ;
     v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
     Self->_hashStateW = v ;
     value = v ;
  }

  value &= markOopDesc::hash_mask;
  if (value == 0) value = 0xBAD ;
  assert (value != markOopDesc::no_hash, "invariant") ;
  TEVENT (hashCode: GENERATE) ;
  return value;
}

根据上面的代码,可以知道,有如下几种算法:

  • hashCode == 0:系统生成的随机数
  • hashCode == 1:对对象的内存地址进行二次计算
  • hashCode == 2:硬编码1(用于敏感性测试)
  • hashCode == 3:一个字增的序列
  • hashCode == 4:对象的内存地址,转为int
  • hashCode == 5Marsaglia’s xor-shift scheme with thread-specific state

global.hpp中,我们可以看出,JDK 1.8使用的计算hashcode的算法为5, 也就是Marsaglia’s xor-shift scheme with thread-specific state ,所以hashcode的值与对象的内存地址,没有什么关系。

1
  product(intx, hashCode, 5, "(Unstable) select hashCode generation algorithm")

:可以使用参数-XX:hashCode=[0-5]来改变默认的算法。

问题七:如何验证hashCode对偏向锁的影响

Java 中的对象 – 对象基本信息文章中我们知道,对象Mark Word中有存有对象的hashCode,但是锁在升级的时候,会发现对象Mark Word中没有保存hashCode,那么如果我们此时使用对象hashCode的话该怎么办。

如图所示,在无锁时,Mark Word中有25bit用来存储对象hashCode,但是锁升级之后,Mark Word中并没有保存对象hashCode

引用RednaxelaFX的话来解释一下:

  • 当一个对象已经计算过 identity hash code,它就无法进入偏向锁状态;
  • 当一个对象当前正处于偏向锁状态,并且需要计算其 identity hash code 的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;
  • 重量锁的实现中,ObjectMonitor 类里有字段可以记录非加锁状态下的 mark word,其中可以存储 identity hash code 的值。或者简单说就是重量锁可以存下 identity hash code。
  • 因为 mark word 里没地方同时放 bias 信息和 identity hash code。HotSpot VM 是假定“实际上只有很少对象会计算 identity hash code ”来做优化的;换句话说如果实际上有很多对象都计算了 identity hash code 的话,HotSpot VM 会被迫使用比较不优化的模式。
  • 请一定要注意,这里讨论的 hash code 都只针对 identity hash code。用户自定义的 hashCode() 方法所返回的值跟这里讨论的不是一回事。Identity hash code 是未被覆写的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值。

为了验证hashCode对偏向锁的影响,我们可以使用代码来进行测试:

  • 首先,引入JMH框架
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!-- JMH-->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.23</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.23</version>
    <scope>provided</scope>
</dependency>
  • 然后,编写测试代码,我们这里使用别人的代码,代码如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package benchmark;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;

/**
 * @author yinan
 * @date 2020/3/19
 */

@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 4)
@Fork(value = 5, jvmArgsAppend = {"-XX:BiasedLockingStartupDelay=0"})
public class BiasedLockingBenchmark {

    int unsafeCounter = 0;
    Object withIdHash;
    Object withoutIdHash;

    @Setup
    public void setup() {
        withIdHash = new Object();
        withoutIdHash = new Object() {
            @Override
            public int hashCode() {
                return 1;
            }
        };
        withIdHash.hashCode();
        withoutIdHash.hashCode();
    }

    @Benchmark
    public void withIdHash(Blackhole bh) {
        synchronized(withIdHash) {
            bh.consume(unsafeCounter++);
        }
    }

    @Benchmark
    public void withoutIdHash(Blackhole bh) {
        synchronized(withoutIdHash) {
            bh.consume(unsafeCounter++);
        }
    }

    @Benchmark
    @Threads(2)
    public void withoutIdHashContended(Blackhole bh) {
        synchronized(withIdHash) {
            bh.consume(unsafeCounter++);
        }
    }

    @Benchmark
    @Threads(2)
    public void withIdHashContended(Blackhole bh) {
        synchronized(withoutIdHash) {
            bh.consume(unsafeCounter++);
        }
    }

    public static void main(String[] args) throws RunnerException {
        Options options = new OptionsBuilder()
                .include(BiasedLockingBenchmark.class.getSimpleName())
                .build();
        new Runner(options).run();
    }

}

为了避免JVM花费4秒触发优化,我们在这里启动了JVM参数-XX:BiasedLockingStartupDelay=0,最后运行结果如下:

1
2
3
4
5
Benchmark                                       Mode  Cnt       Score      Error   Units
BiasedLockingBenchmark.withIdHash              thrpt    5   54849.483 ± 1173.958  ops/ms
BiasedLockingBenchmark.withIdHashContended     thrpt    5   19615.247 ±  191.165  ops/ms
BiasedLockingBenchmark.withoutIdHash           thrpt    5  263447.816 ± 2964.859  ops/ms
BiasedLockingBenchmark.withoutIdHashContended  thrpt    5   19255.936 ± 1444.000  ops/ms

从结果中我们可以看到使用自定义hashCodelock/unlock 速度比使用原始哈希码的速度快了4倍到5倍,而当两个线程同时去争抢锁时,lock/unlock 速度几乎是相同的,说明偏向锁被禁止了。

为了说明偏向锁被禁用之后两者之间没有差异,我们再添加-XX:-UseBiasedLocking参数来进行测试:

1
2
3
4
5
Benchmark                                       Mode  Cnt      Score      Error   Units
BiasedLockingBenchmark.withIdHash              thrpt    5  55352.726 ±  216.518  ops/ms
BiasedLockingBenchmark.withIdHashContended     thrpt    5  24196.326 ± 1972.279  ops/ms
BiasedLockingBenchmark.withoutIdHash           thrpt    5  55279.982 ±  247.573  ops/ms
BiasedLockingBenchmark.withoutIdHashContended  thrpt    5  22754.403 ±  549.393  ops/ms

可以看到禁用偏向锁之后两者之间没有了差异,说明hashCode对偏向锁是存在影响的……

总结

通过两篇文章,解答七个问题之后,关于对象和锁的关系也暂时告一段落了,从开始仅仅是想知道一个对象多大,到后面不断的深挖,看到这么多背后的东西,感觉还是挺奇妙的,也说明了学习是一个不断深挖的过程,同时也是一个不断总结过程,之后可能会再考虑深入去复习一下AQS相关知识。

参考

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus