JVM——synchronized 优化

synchronized 优化

Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存储这个对象的 哈希码 、 分代年龄 。当加锁时,这些信息就根据情况被替换为 标记位 、 线程锁记录指
针 、 重量级锁指针 、 线程ID 等内容

1.轻量级锁

如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。这就好比:
学生(线程 A)用课本占座,上了半节课,出门了(CPU时间到),回来一看,发现课本没变,说明没有竞争,继续上他的课。 如果这期间有其它学生(线程 B)来了,会告知(线程A)有并发访问,线程A 随即升级为重量级锁,进入重量级锁的流程。
而重量级锁就不是那么用课本占座那么简单了,可以想象线程 A 走之前,把座位用一个铁栅栏围起来。
假设有两个方法同步块,利用同一个对象加锁

static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}

每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
JVM——synchronized 优化_第1张图片

2.锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
JVM——synchronized 优化_第2张图片

3. 重量锁

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
如图是自旋成功的例子:
JVM——synchronized 优化_第3张图片
如图是自旋失败的例子:
JVM——synchronized 优化_第4张图片

4 偏向锁

轻量锁在没有竞争的时候(就自己这个线程),每次重入仍需要执行CAS操作。这个时候就引入了偏向锁来做进一步的优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程是自己的就表示没有竞争,不用重新 CAS。

特点
1.撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)。
2.访问对象的 hashCode 也会撤销偏向锁。
3.如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID。
4.撤销偏向和重偏向都是批量进行的,以类为单位。
5.如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的。
可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁。

5. 其它优化

  1. 减少上锁时间
    同步代码块中尽量短。

  2. 减少锁的粒度
    将一个锁拆分为多个锁提高并发度,例如:
    1.ConcurrentHashMap
    2.LongAdder分为base和cells两部分,没有并发争用的时候或者是cells数组正在初始化的时候,会使用 CAS 来累加值到 base,有并发争用,会初始化 cells 数组,数组有多少个 cell,就允许有多少线程并行修改,最后将数组中每个 cell 累加,再加上 base 就是最终的值。
    3.LinkedBlockingQueue 入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高。

  3. 锁粗化
    多次循环进入同步块不如同步块内多次循环 另外 JVM 可能会做如下优化,把多次 append 的加锁操作。粗化为一次(因为都是对同一个对象加锁,没必要重入多次)

new StringBuffer().append("a").append("b").append("c");
  1. 锁消除
    JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候就会被即时编译器忽略掉所有同步操作。
  2. 读写分离
    CopyOnWriteArrayList ConyOnWriteSet

你可能感兴趣的:(JVM,java,jvm,多线程,synchronized)