1)锁自旋

首先明确Java中的线程与OS上的线程是一一对应的,每一次线程的阻塞/就绪状态都会涉及到OS内核态到用户态的切换,这一块的开销主要体现在对象获取锁失败之后进入阻塞状态,而在程序中实际上有很多的锁的持有时间是很短的,这里切换的开销就有点大了。自旋锁解决的就是阻塞到就绪频繁切换的问题,如果获取不到锁不会马上进入阻塞等待,而是再循环多几次获取锁,如果刚好获取到了就可以避免切换的开销了。在JDK1.5之后有了自适应自旋,JVM 会自动判断是否需要自旋、自旋多少次,无需人工干涉。

2)锁消除

除了显示加上的synchronized等锁,程序中还存在一种我们在编码时看不到的锁,例如在进行StringBuffer的append操作时,实际上就会加锁,而如果我们可以确保对象不发生逃逸(不会被外部引用),那么就无需上锁了,因此这里JVM也是会进行精细的逃逸分析,如果判断对象没有被外部引用的危险,只是在线程内被使用,那么就会帮我们把锁消除,减少开销。

3)锁粗化

这里使用上面的append方法加锁来做说明,虽然可能已经发生锁消除了,但不影响描述概念。因为锁是加在append方法上的,因此如果调用十次,就是加了十次的锁,而锁粗化即是把锁的范围拉大,相当于将十次的append方法都包括在一个同步代码块内,这样只需要加一次锁,开销也小了。

4)轻量级锁

我们使用synchronized的时候其实都是假设在同一时刻有多个线程来竞争,这时候就不得不加上monitor锁,也称为重量级锁,因为加锁解锁再加锁这个过程开销是比较大的,而如果我们有几个线程,但他们执行同步代码块的时间是错开的,那么就没有必要频繁加monitor锁了,这时候轻量级锁就上场了。轻量级锁利用到了对象头的Mark Word部分,平常这部分是用来存放hashCode、分代年龄等信息的,锁标志位是01,一旦线程准备进入同步代码块时,就会在线程栈中创建一个Lock Record(锁记录信息),接着获取锁对象Mark Word中的信息,存入Lock Record中,并将自己的线程ID写入Mark Word,将锁标志位设置为00。
如果后续有本线程再次获取锁对象,检查到Mark Word的线程ID一致,那么相应的计数器会加一,后续待到计数器为0才会将锁释放掉。
如果有其他线程获取锁对象,检查了Mark Word发现线程ID不一致,就会获取一个Monitor对象,将Mark Word信息修改为Monitor对象地址,将锁标志位设置为10,自己进入Entry Set中等待获取锁,后续获取锁的对象也需要进入Entry Set中的等待。这也被称为锁膨胀。

5)偏向锁

偏向锁顾名思义就是只偏向一方(一个线程)的锁,在JDK1.6以后引入。如果我们的使用场景下只有一个线程会频繁进入同步代码块(频繁获取锁),那么这时可以直接使用偏向锁,在锁对象的Mark Word中记录下偏向的线程ID,将偏向标志位置为1,这时hashCode的位置就会被偏向进程ID所替代,此后如果获取锁的线程是同一个,那么无需做多余的操作,跟没加锁一样。如果很不幸有其他的线程来竞争,那么会先判断当前是否已被锁定,如果不是则先进行重偏向,将当前线程ID存入Mark Word,如果已被锁定或者后续有多次的重偏向,就会撤销偏向锁,转而加上轻量级锁。
这里需要注意,偏向锁状态下hashCode是没地方存储的,如果对象已经计算好hashCode存放在Mark Word了,那么后续将无法进入偏向锁的状态,如果在偏向锁的状态下对象的hashCode被请求获取了,那么偏向锁将马上膨胀为重量级锁。