对象头:synchronized用的锁是存在Java对象头里的。Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位,主要用来表示对象的线程锁状态。
栈帧:也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。(我只能浅显的理解为是记录信息的,看了很多文章还是说不清楚)
锁会随着线程的竞争情况逐渐升级,偏向锁 => 轻量级锁 => 重量级锁 。锁可以升级但是不能降级。升级的目的是为了提高获得锁和释放锁的效率。
偏向锁
当一个线程访问同步块获取锁时,会在对象头和栈帧的锁记录里存储锁偏向的线程ID,以后该线程再进入和推出同步块时不需要进行CAS(比较和交换,下次详细记录一下)操作来加锁和解锁。
通过判断对象头的Mark Word里面是否存有指向当前线程的偏向锁来决定是否需要使用CAS竞争锁。
适用场景:偏向锁只适合大部分锁没有被竞争的系统中(也可以说是只有一个线程访问同步块的场景),如果系统中存在大量被争用的锁时,会导致持有锁的线程不断切换,这时可以考虑关闭偏向锁。
可以通过JVM参数关闭偏向锁:
-XX:- UseBiasedLocking=false
,程序默认会进入轻量级锁状态。
轻量级锁
阻塞线程需要cpu从用户态转到内核态,代价比较大。而且可能会出现刚阻塞不久,锁就被释放的情况。为了缩小性能消耗,引入轻量级锁。
加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
适用场景:少量线程交替获取锁,同步块执行速度非常快的场景。追求响应时间。
优缺点对比
偏向锁
优:加锁/解锁不需要额外的消耗
缺:有竞争时,会有额外的撤销锁或升级锁的消耗
轻量级锁
优:竞争的线程不会被阻塞(采用自旋),提高了程序的响应速度
缺:始终得不到锁的线程一直自旋会消耗cpu,造成cpu浪费(自旋好像就是无实际意义的循环,可以设定一个自旋等待的最大时间)
重量级锁
优:线程竞争不使用自旋,线程竞争锁失败后会阻塞,cpu的消耗会减少,增大了数据的吞吐量
缺:线程阻塞,响应速度慢
适用场景:大量线程同时竞争锁,追求吞吐量。