“锁膨胀” 的过程~
场景:多个线程尝试i++,假设这些线程不是同时运行的。
1)第一个线程开始i++,就需要先加锁~
但是第一个线程首次加锁,不是进行真加锁,而只是在对象头里通过一个特殊标志位,标记一下(某个线程想要获取到锁),这里由于是一个线程进行操作,所以还不涉及到线程问题,所以只是标记一下。这个线程赌后面没有其他线程来竞争这个锁!!这也是一个乐观锁。
同时这不是一个真的加锁,而只是标记了一下,所以我们也叫作偏向锁。
2)当第一个线程以偏向锁的状态进行i++的过程中,第二个线程也来尝试竞争锁~~
这个时候第二个线程也来竞争锁,第一个线程立马不乐意了,锁竞争真的发生了,于是第一个线程就会立即获取到锁状态~~ 第二个线程也会真加锁,就真正涉及到了锁竞争~~
举个例子:
就比如张三,在追妹子的时候,已经和妹子关系很好了,但是只是搞暧昧,并没有明确关系,突然李四来了,也向妹子发起追求,此时张三就坐不住了,立马和妹子表态,确认男女朋友关系,然后李四就竞争失败,变成备胎,继续等~~
上面说的立刻获取到锁状态,是使用轻量级锁来加锁的~,第二个线程也是通过轻量级锁来加锁 ~
就从 偏向锁 到 轻量级锁 就有一个从轻到重的过程,就叫做“锁膨胀”,锁的量级从轻到重~
3)接下来,其他线程接二连三的过来竞争,i++操作的锁竞争越来越激烈,轻量级锁虽然快,也不是那么容易获取到锁了,吃的CPU也变多了;
此时这些持有轻量级锁的线程,就要在进一步膨胀成‘重量级锁’ mutex,然后获取到锁的线程继续工作,没有获取到锁得线程就进入内核态,阻塞等待…就变成了 重量级锁!!
这整个锁策略自动切换的过程,称为“锁膨胀” / “锁升级” ,都是由synchronized内部完成的(也就是JVM内部来完成的)
目的就是为了,在不同场景下,使用不同的锁~~ 提高效率!!
偏向锁 -》 轻量级锁 -》重量级锁,这样一个过程是synchronized 的 自适应 完成的~,根据锁竞争的激烈程度 ~
synchronized 的一些其他优化手段
优化是编译器 + JVM共同配合完成的工作~ 设计编译器和JVM的大佬们,就希望达到,代码你随便写…我给你优化!!
1. 锁消除
如果某个代码 编译器/ JVM 判定不涉及线程安全问题,就会自动把程序猿加的锁去掉~~
举个例子: StringBuilder 和 StringBuffer
但是如果是单线程下,直接使用StringBuffer,其实就会涉及到一些无意义的加锁操作,虽然有锁的自适应,但是自适应并不是等于 0,也还是有开销的,所以我们还是希望,让他能不做,就尽量不做~
这个时候,锁消除就起到效果了。编译器就会判断,针对StringBuffer的操作是否都是在一个线程内完成的,如果是,就不再真的加锁。而是在编译生成字节码里就把加锁操作给去掉了~
2.锁的粗化
就好比StringBuffer 添加元素的时候~
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("bbb");
stringBuffer.append("bbb");
stringBuffer.append("bbb");
stringBuffer.append("bbb");
上述代码中,每个append 都会加锁,解锁~
假设这里确实是多线程操作StringBuffer,不能消除锁了~
这里就涉及到一个问题 锁的‘粒度’,指的就是synchronized影响到代码的范围~
所谓的对锁进行粗化,就是把这种连续的加锁解锁操作,合并成一次加锁解锁(锁的粒度变大了)