synchronized具有以下特性(只考虑JDK1.8)
- 开始是乐观锁,如果锁冲突频繁,就转换为悲观锁
- 开始是轻量级锁,如果锁被持有时间的较长,就转换成重量级锁
- 实现轻量级锁的时候大概率用到的自旋锁策略
- 是一种不公平锁
- 是一种可重入锁
- 不是读写锁
1. synchronized的加锁过程
JVM 把 synchronized 锁分为 无锁、偏向锁、(自旋锁)轻量级锁、重量级锁。会根据情况,进行升级~
偏向锁
偏向锁,就是线程A针对锁,先进行一个标记,如果在整个代码执行过程中,没有其他线程和线程A竞争,此时就不用真的加锁~
自旋锁
当发生其他线程和线程A竞争的时候,就升级成了自旋锁,自旋锁是很快,但是消耗大量CPU资源
如果有10个线程竞争一把锁,1个竞争上,其他9个都得等待~此时就会消耗大量的CPU资源
重量级锁
面对上述情况,就升级成了重量级锁~在内核里进行阻塞等待(意味着放弃CPU资源,由内核进行后续调度)
对于主流的JVM的实现,只能升级,不能降级!
2. 锁的消除和锁的粗化
2.1 锁消除
非必要不加锁!上述升级是在程序运行时,JVM做出的优化
而锁消除,是编译阶段做的优化:检测当前代码是否是多线程执行/是否有必要加锁。如果没有必要,又把锁写了,就会在编译过程中自动把锁去掉
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
上述代码中,此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加锁解锁操作是没有必要的, 白白浪费了一些资源开销.
2.2 锁粗化
锁的粒度:synchronized 代码块,包含的代码多少(代码越多,粒度越粗,代码越少,粒度越细)
一般写代码的时候,多数情况下,是希望锁的粒度更细一点(串行执行的代码少,并发执行的代码就多)
但是有一个场景,要频繁加锁、解锁,此时编译器就可能把这个操作优化成一个更粗粒度的锁
- 实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁.
- 但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释放锁.