正文
今天一起讨论一下java对象里面锁的问题。本文按照锁消除、锁粗化、锁升级依次展开讨论。
锁消除问题
锁消除,顾名思义,但是它是如何消除的呢?有什么约束条件嘛?才可以达到一个对象可以抛开自己的锁,在多线程并发的情况下,可以正确的得到想要的结果。正如下面的代码所示:
上面的代码实现了一个简单的功能,实现从0到99返回一个简单的字符串。观察仔细的人,可以发现,这里采用的是StringBuffer进行操作字符串的。众所周知,这里面的方法都是采用synchronized修饰的同步方法。换一句话说,每条用一次方法,都会对monitor进行加一和减一,是不是很麻烦?为了消除麻烦,这里JVM进行了优化,如果局部变量的对象,加锁和去掉锁执行的结果是一样的,在JVM执行的时候,就可以对局部变量对象不需要加锁就可以执行。这样的一个过程就是JVM锁消除的过程。
锁租化问题
锁租化,按照名字来讲,也就是扩大了一个对象的锁的范围。但是如何地扩大呢?有什么要求呢?JVM是怎么处理的呢?上代码如下:
锁粗化与锁消除类似,但是有一点不同的是,这里面锁的对象从局部变量变化成了全局变量,所有的线程都可以共享、一起使用。换一句话说,不加锁肯定是不行的,但是JVM还是有优化的办法的。在上面的方法中,调用了多次的append方法,但是每一次的调用都需要加锁和释放锁,在多线程并发的场景,线程的操作会频繁的从用户态到内核态的调用,很影响JVM的运行效率。JVM为了提升代码的运行性能,针对这样的场景进行了优化,也就是将红色框内的代码,进行统一的加锁和释放锁,而且是一次。这样就避免了频繁用户态到内核态的状态转换。似乎,锁的粒度变粗了,所以这也就是锁粗化的意思啦!
锁升级问题
锁升级的过程也是针对JVM运行用户的代码进行的优化。synchronized是一个重量级锁,他会操作用户态到内核态。但是这种操作有一些场景往往是没有必要的,也就是说,想要操作用户态到内核态,也是有条件的,逐渐变化的。在昨天的文章中,Mark Word区域记录了某一个对象的锁的信息,比较关键的是偏向锁、锁信息。一个对象的锁升级的过程如下:
当某一个用户线程,获取到一个加锁的对象,并且没有其他的用户线程竞争的时候,完全可以不需要加锁进行执行。但是这个时候,第二个用户线程过来了,请求相同的锁对象,这个时候所信息会设置偏向锁,指向第一个获取用户线程。如果,用户线程超过了3个,并且竞争很激烈,这时候,会启用lock表示位,变化为轻量级锁。无论是偏向锁、轻量级锁都是在用户态完成的。如果竞争了很多次,而且用户线程越来越多,这时候会严重影响用户线程的有效执行、JVM的运行效率,此时就会变成重量级锁,从用户态到内核态。结果就是获取锁的线程继续执行,其他的线程被挂起,等待可以执行的资源。为什么会这个样子呢,因为轻量级锁和偏向锁都是在用户态完成的,势必会进行循环执行,检测这个锁是否可以获取,线程的数目比较少还好,如果比较多,就会增加用户态的压力,这时候只能将这些竞争的线程挂起,释放用户态执行的压力。