回答思路
得分点 作用于三个位置、对象头、锁升级 标准回答 用法 synchronized可以作用在三个不同的位置,对应三种不同的使用方式,这三种方式的区别是锁对象不同。不同的锁对象,意味着不同的锁粒度,所以我们不应该无脑地将它加在方法前了事,尽管通常这可以解决问题。而是应该根据要锁定的范围,准确的选择锁对象,从而准确地确定锁的粒度,降低锁带来的性能开销。
1. 作用在静态方法上,则锁是当前类的Class对象。
2. 作用在普通方法上,则锁是当前的实例(this)。
3. 作用在代码块上,则需要在关键字后面的小括号里,显式指定一个对象作为锁对象。 原理 synchronized的底层是采用Java对象头来存储锁信息的,并且还支持锁升级。 Java对象头包含三部分,分别是Mark Word、Class Metadata Address、Array length。其中,Mark Word用来存储对象的hashCode及锁信息,Class Metadata Address用来存储对象类型的指针,而Array length则用来存储数组对象的长度。如果对象不是数组类型,则没有Array length信息。synchronized的锁信息包括锁的标志和锁的状态,这些信息都存放在对象头的Mark Word这一部分。 Java 6为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。所以,在Java 6中,锁一共被分为4种状态,级别由低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。随着线程竞争情况的升级,锁的状态会从无锁状态逐步升级到重量级锁状态。锁可以升级却不能降级,这种只能升不能降的策略,是为了提高效率。 synchronized的早期设计并不包含锁升级机制,所以性能较差,那个时候只有无锁和有锁之分。是为了提升性能才引入了偏向锁和轻量级锁,所以需要重点关注这两种状态的原理,以及它们的区别。 偏向锁,顾名思义就是锁偏向于某一个线程。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程再进入和退出同步块时就不需要做加锁和解锁操作了,只需要简单地测试一下Mark Word里是否存储着自己的线程ID即可。 轻量级锁,就是加锁时JVM先在当前线程栈帧中创建用于存储锁记录的空间,并将Mark Word复制到锁记录中,官方称之为Displaced Mark Word。然后线程尝试以CAS方式将Mark Word替换为指向锁记录的指针,如果成功则当前线程获得锁,如果失败则表示其他线程竞争锁,此时当前线程就会通过自旋来尝试获取锁。 加分回答 下面,我们再从实际场景出发,来具体说说锁升级的过程:
1. 开始,没有任何线程访问同步块,此时同步块处于无锁状态。
2. 然后,线程1首先访问同步块,它以CAS的方式修改Mark Word,尝试加偏向锁。由于此时没有竞争,所以偏向锁加锁成功,此时Mark Word里存储的是线程1的ID。
3. 然后,线程2开始访问同步块,它以CAS的方式修改Mark Word,尝试加偏向锁。由于此时存在竞争,所以偏向锁加锁失败,于是线程2会发起撤销偏向锁的流程(清空线程1的ID),于是同步块从偏向线程1的状态恢复到了可以公平竞争的状态。
4. 然后,线程1和线程2共同竞争,它们同时以CAS方式修改Mark Word,尝试加轻量级锁。由于存在竞争,只有一个线程会成功,假设线程1成功了。但线程2不会轻易放弃,它认为线程1很快就能执行完毕,执行权很快会落到自己头上,于是线程2继续自旋加锁。
5. 最后,如果线程1很快执行完,则线程2就会加轻量级锁成功,锁不会晋升到重量级状态。也可能是线程1执行时间较长,那么线程2自旋一定次数后就放弃自旋,并发起锁膨胀的流程。届时,锁被线程2修改为重量级锁,之后线程2进入阻塞状态。而线程1重复加锁或者解锁时,CAS操作都会失败,此时它就会释放锁并唤醒等待的线程。 总之,在锁升级的机制下,锁不会一步到位变为重量级锁,而是根据竞争的情况逐步升级的。当竞争小的时候,只需以较小的代价加锁,直到竞争加剧,才使用重量级锁,从而减小了加锁带来的开销。