锁状态
锁信息存储在java对象的markword内容中
markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,如下表所示:
状态 标志位 存储内容
未锁定 01 对象哈希码、对象分代年龄
轻量级锁定 00 指向锁记录的指针
膨胀(重量级锁定) 10 执行重量级锁定的指针
GC标记 11 空(不需要记录信息)
可偏向 01 偏向线程ID、偏向时间戳、对象分代年龄
markdown-lock.png
偏向锁
偏向锁是jdk底层的优化,当只有一个线程会访问同步代码的时候,启用偏向锁
若遇到竞争(另一个线程去访问同步代码),会先通过CAS方式尝试获取偏向锁,若偏向锁未获取到存在竞争则升级成轻量级锁
升级轻量级锁前会释放偏向锁,释放偏向锁时会先进入安全点,发生stop the world的事情
轻量级锁
一开始线程进入同步代码时,拷贝对象的markdown信息,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”)
线程会在当前栈帧中建立锁记录Lock Record,并通过CAS更新对象对象头信息,将锁设置为00轻量锁
若设置轻量锁失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
阻塞
线程阻塞状态需要操作系统的介入,在JVM和操作系统中切换线程需要耗费大量的系统资源(用户线程和内核的切换的消耗),因此在进入阻塞状态前线程会尝试通过自旋获取锁
自旋锁
如果持有锁对象的线程能在短时间内释放锁,则线程通过自选的方式避免用户线程与内核线程之间的切换。
自旋锁会消耗cpu资源,在自旋期间线程会一直占用CPU资源,所以需要设置自旋最大等待时间。
注意:自旋锁是非公平竞争
线程优化
1.根据线程竞争激烈程度,适当的开启或者关闭自旋锁、偏向锁、轻量级锁
2.减少锁的时间:不必要的方法不要放在同步代码中执行
3.减少锁的粒度:将一个锁拆成多个逻辑上的锁,增加并行度,从而降低锁的竞争
4.使用ConcurrentHashMap:通过多个Segment细分锁的粒度,put时先获取需要的Segment
5.LongAdder:LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能
6.LinkedBlockingQueue:对头和队尾分别加锁
拆锁的粒度不能无限拆,最多可以将一个锁拆为当前cup数量个锁即可;
7.使用读写锁:读写分离
8.使用cas(乐观锁)
9.消除缓存行的伪共享
Synchonized同步锁
- 普通同步方法--锁:当前实例对象
- 静态同步方法--锁:当前类的class对象
- 同步方法快--锁:Synchonized括号里的对象
网友评论