Synchronized 底层实现原理

一、Monitor 锁简介

        通常我们创建的 java 对象,它在内存中都是由两部分组成,一部分是由对象头,另外一部分是对象中的那些成员变量。

1.1 对象头

1.1.1 普通对象

        以 32 位虚拟机为例,普通对象的对象头在 32 位的虚拟机里占用 64 位,即 个字节大小,其中 4 个字节是 Mark Word,它里面存储了很多的信息。另外 4 个字节存储 Klass Word,存储的是对象的类型,本质上是个指针,指向了这个对象所从属的 class

        32 位虚拟机的 Mark Word 结构如下

        64 位虚拟机的 Mark Word 结构如下

        不同的 State 表示 Mark Word 所存储的数据结构是不一样的。 我们先只考虑 Normal 状态下存储的数据结构信息。

        1、hashCode:25:表示存储的 hashCode,占用 25 位。

        2、age:4:表示垃圾回收时的分代年龄,占用 4 位。

        3、biased_lock:0:表示是否为偏向锁。

        4、01:表示是否为加锁状态。 

1.1.2 数组对象

        以 32 位虚拟机为例,数组对象的对象头在 32 位的虚拟机里占用 96 位,即 12 个字节大小,除了 Mark Word  Klass Word 以外还有一个 array length 用于存储数组的长度,占用 4 个字节。

1.1.3 举例说明

        以 int 类型为例,它占用 4 个字节。但是一个 Integer 对象,在 32 位虚拟机里面,一个对象头占用 8 个字节,还需要一个 value 值来存储 int 的整型,所以还需要 4 个字节,所以加起来就是 12 个字节。

        所以在一些内存很敏感的场景下,建议用基本类型而不是用包装类型的原因,因为包装类型占用的空间比较大。

1.2 Monitor

1.2.1 工作原理

        Monitor 被翻译为监视器或管程。每个 java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该 java 对象的 Mark Word 中就被设置指向 Monitor 对象的指针。

// obj 对象的 Mark Word 指向了自己的 Monitor 对象
// 每一个对象都有自己的 Monitor 对象
static Object obj = new Object();
public static void main(String[] args)  {
	Thread t1 = new Thread(new Runnable() {
		@Override
		public void run() {
			synchronized (obj){
				// todo
			}
		}
	});
	Thread t2 = new Thread(new Runnable() {
		@Override
		public void run() {
			synchronized (obj){
				// todo
			}
		}
	});
	t1.start();
	t2.start();
}

        Monitor 结构如下:

        1、刚开始 Monitor Owner null。

        2、 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2Monitor 中只能有一个 Owner

        3、Thread-2 上锁的过程中,如果 Thread-3Thread-4Thread-5 也来执行 synchronized(obj),就会进入 EntryList 中,并处于 BLOCKED 状态。

        4、Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,不一定是先进来的会成为 Owner

        5、图中 WaitSet 中的 Thread-0Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程。

1.2.2 注意

        1、synchronized 必须是进入同一个对象的 monitor 才有上述的效果

         2、不加 synchronized 的对象不会关联监视器,不遵从以上规则。

二、Synchronized 原理进阶

2.1 小故事

2.1.1 故事角色

        1、老王 - JVM

        2、小南 - 线程

        3、小女 - 线程

        4、房间 - 对象

        5、房间门上 - 防盗锁 - Monitor

        6、房间门上 - 小南书包 - 轻量级锁

        7、房间门上 - 刻上小南大名 - 偏向锁

        8、批量重刻名 - 一个类的偏向锁撤销到达 20 阈值

        9、不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向

2.1.2 故事情节

        小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。

        但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?

        小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。

        后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。

        于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。

        同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字。

        后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包。

2.2 轻量级锁

2.2.1 使用场景

        如果一个对象处于多线程的环境下并且需要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

        轻量级锁对使用者是透明的,即语法仍然是 synchronized,即当我们使用 synchronized 进行加锁时,优先使用的是轻量级锁进行加锁,如果轻量级锁失败了,才会去用重量级锁去加锁。

2.2.2 场景分析

        假设有两个方法同步块,利用同一个对象加锁,代码如下:

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

        先来回顾下,obj 对象由两部分组成,对象头和对象体,对象头又由两部分组成,一部分是 Mark Word,另一部分是 Klass Word。对象体里面存储的是成员变量。

        1、首先在 Thread-0 的栈帧中创建锁记录(Lock Record)对象,一部分存储对象指针,用于存储锁住对象的内存地址;另一部分存储加锁对象的 Mark Word。如下图

        2、让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object Mark Word,将 Mark Word 的值存入锁记录。此时 Mark Word 的状态位为 01,表示未加锁状态;而锁记录里面的状态位为 00,表示的是轻量级锁的状态。 

        3、如果 cas 替换成功,对象头中存储了锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

        如果 cas 失败,有两种情况:

        1、如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程。

        2、如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数。

        4、当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一 。

        5、当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas Mark Word 的值恢复给对象头。

        如果成功,则解锁成功。

        如果失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

2.3 锁膨胀

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

        假设此时又有一个线程需要对 obj 进行加锁,代码如下

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

        1、Thread-1 进行轻量级加锁时,发现 Thread-0 已经对该对象加了轻量级锁,如下图

        2、这时 Thread-1 加轻量级锁失败,进入锁膨胀流程。即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址,然后自己进入 Monitor EntryList 并处于 BLOCKED 状态,如下图

        3、 Thread-0 退出同步块解锁时,使用 cas Mark Word 的值恢复给对象头,会发现恢复失败了。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner null,唤醒 EntryList BLOCKED 的线程。

2.4 自旋优化

        重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

2.4.1 自旋成功

        自旋重试成功的情况,如下

2.4.2 自旋失败

        自旋重试失败的情况,如下

2.4.3 注意

        1、自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

        2、Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

        3、Java 7 之后不能控制是否开启自旋功能。

2.5 偏向锁

2.5.1 偏向锁背景

        轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

        为了解决这一问题,Java 6 中引入了偏向锁来做进一步优化,在第一次 CAS 时,将线程 ID 设置到对象的 Mark Word 头,之后如果发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。

        以下面的代码为例,模拟展示下轻量级锁和偏向锁的调用过程

static final Object obj = new Object();
public static void m1() {
	synchronized( obj ) {
		// 同步块 A
		m2();
	}
}
public static void m2() {
	synchronized( obj ) {
		// 同步块 B
		m3();
	}
}
public static void m3() {
	synchronized (obj) {
		// 同步块 C
	}
}

2.5.2 轻量级锁调用过程

        第一次 synchronized 时给对象加上锁了,锁记录替换了对象的 mark word,当发生锁重入的时候,就会又产生一条锁记录,这条锁记录又要去尝试用锁记录替换了对象的 mark word,当然了第二次就失败了。

        此时它知道这个锁是自己家的了,这条锁记录还是会被保留作为锁重入时的一个计数,虽然是自己家的了,但还是做了一次 CAS 。还是有一定的性能损耗的。

2.5.3 偏向锁调用过程

        第一次 CAS 时,用 ThreadID 替换 Mark Word,后续发生锁重入时,就不会使用 CAS 去检查,直接看对象头的 mark word ThreadID 就可以了。

2.5.4 偏向状态

         先回忆一下对象头格式,如下图

        1、一个对象创建时,如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 threadepochage 都为 0。

        2、偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。

        3、如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcodeage 都为 0,第一次用到 hashcode 时才会赋值。

2.5.5 延迟性测试

        首先创建一个 maven 工程,并引入相关的依赖,如下所示:

<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-api</artifactId>
	<version>1.7.25</version>
</dependency>
<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.16</version>
</dependency>

        测试代码如下所示:

@Slf4j
public class TestA {
    public static void main(String[] args) throws InterruptedException {
        Dog d = new Dog();
        // 使用 ClassLayout 可以打印对象的头,true 表示只打印 2 进制的 Mark Word 头
        log.debug( ClassLayout.parseInstance(d).toPrintable());

        Thread.sleep(4000);
        log.debug( ClassLayout.parseInstance(new Dog()).toPrintable());
    }
}

class Dog{

}

        打印的结果如下所示,

# 第一次打印
# 十六进制的 mark word
0x0000000000000001
# 转化为二进制为
00000000 00000000  00000000  00000000  00000000  00000000  00000000  00000001

# 第二次打印
# 十六进制的 mark work
0x0000000000000005
# 转换为二进制为
00000000 00000000  00000000  00000000  00000000  00000000  00000000  00000101

         第一次打印偏向锁还没有生效,所以最后三位是 001,而 4 秒以后,偏向锁就会生效,所以最后三位是 101

        偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。如下图

2.5.6 偏向锁测试

         上面的测试只是证明这个 d 对象可以支持偏向锁,但是还没有加锁,接下来加锁来测试下偏向锁,代码如下:

@Slf4j
public class TestA {
    public static void main(String[] args) throws InterruptedException {
        Dog d = new Dog();
        // 此时我们是配置了偏向锁立即生效的命令了
        // 加锁前打印对象的头
        log.debug( ClassLayout.parseInstance(d).toPrintable());
        synchronized (d){
            // 加锁后打印对象头
            log.debug( ClassLayout.parseInstance(d).toPrintable());
        }
        // 解锁后打印对象头
        log.debug( ClassLayout.parseInstance(d).toPrintable());
    }
}

class Dog{

}

        打印结果如下 

# 第一次打印
# 十六进制的 mark word
0x0000000000000005
# 转化为二进制为
00000000 00000000  00000000  00000000  00000000  00000000  00000000  00000101

# 第二次打印
# 十六进制的 mark work
0x0000000003373805
# 转换为二进制为
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101

# 第三次打印
# 十六进制的 mark work
0x0000000003373805
# 转换为二进制为
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101

         第一次打印的最后三位是 101 ,证明启用了偏向锁。

        第二次打印除了最后三位是 101,前面还多了很多的东西,这些东西是线程 ID

        第三次打印是释放完锁之后的打印,最后三位仍是 101,前面的线程 ID 也没有发生变化,这就证明发生了偏向,以后这个 d 对象就给你这个主线程用了,即 d 对象从属于主线程了。

2.5.7 偏向锁禁用

        偏向锁的使用场景是冲突很少的情况,比如只有一个线程使用这些对象给对象加锁。但是如果应用程序的使用场景是多线程竞争使用锁对象,这个时候偏向锁就不合适了,可以通过 -XX:-UseBiasedLocking 参数来禁用偏向锁,代码如下:

@Slf4j
public class TestA {
    public static void main(String[] args) throws InterruptedException {
        Dog d = new Dog();
        // 此时我们是配置了禁用偏向锁的命令了
        // 加锁前打印对象的头
        log.debug( ClassLayout.parseInstance(d).toPrintable());
        synchronized (d){
            // 加锁后打印对象头
            log.debug( ClassLayout.parseInstance(d).toPrintable());
        }
        // 解锁后打印对象头
        log.debug( ClassLayout.parseInstance(d).toPrintable());
    }
}

class Dog{

}

# 第一次打印
# 十六进制的 mark word
0x0000000000000001
# 转化为二进制为
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

# 第二次打印
# 十六进制的 mark work
0x0000000002b8f288
# 转换为二进制为
00000000 00000000 00000000 00000000 00000001 01011100 01111001 10001000

# 第三次打印
# 十六进制的 mark work
0x0000000000000001
# 转换为二进制为
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

        第一次打印最后三位是 001,此时处于正常状态。 

        第二次打印最后两位是 00,表示加的是轻量级锁。

        第三次打印最后三位是 001,此时处于正常状态。

2.5.8 偏向锁撤销

2.5.8.1  hashCode 撤销

        首先去掉配置的 JVM 参数,然后把延时为 0 的参数加上去。测试代码如下:

@Slf4j
public class TestA {
    public static void main(String[] args) throws InterruptedException {
        Dog d = new Dog();
        d.hashCode();// 会禁用掉这个对象的偏向锁
        // 加锁前打印对象的头
        log.debug( ClassLayout.parseInstance(d).toPrintable());
        synchronized (d){
            // 加锁后打印对象头
            log.debug( ClassLayout.parseInstance(d).toPrintable());
        }
        // 解锁后打印对象头
        log.debug( ClassLayout.parseInstance(d).toPrintable());
    }
}

class Dog{

}

        测试结果如下所示

# 第一次打印
# 十六进制的 mark word
0x0000006438a39601
# 转化为二进制为
00000000 00000000 00000000 01100100 00111000 10100011 10010110 00000001

# 第二次打印
# 十六进制的 mark work
0x0000000002a1f688
# 转换为二进制为
00000000 00000000 00000000 00000000 00000010 10100001 11110110 10001000

# 第三次打印
# 十六进制的 mark work
0x0000006438a39601
# 转换为二进制为
00000000 00000000 00000000 01100100 00111000 10100011 10010110 00000001

        第一次打印最后三位是 001,此时处于正常状态。 

        第二次打印最后两位是 00,表示加的是轻量级锁。

        第三次打印最后三位是 001,此时处于正常状态。

        为什么调用一次 hashCode 方法就会禁用掉偏向锁呢?是因为 mark word 里面没有足够的空间去存储 hashCode 了。即当一个可偏向的对象调用了自己的 hashCode 方法后,就会撤销这个对象的偏向状态。

2.5.8.2 其他线程使用撤销

        当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁,测试代码如下:

@Slf4j
public class TestB {

    private static void test2() throws InterruptedException {
        Dog d = new Dog();
        Thread t1 = new Thread(() -> {
            synchronized (d) {
                log.debug( ClassLayout.parseInstance(d).toPrintable());
            }
            synchronized (TestB.class) {
                TestB.class.notify();
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(() -> {
            synchronized (TestB.class) {
                try {
                    TestB.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug( ClassLayout.parseInstance(d).toPrintable());
            synchronized (d) {
                log.debug( ClassLayout.parseInstance(d).toPrintable());
            }
            log.debug( ClassLayout.parseInstance(d).toPrintable());
        }, "t2");
        t2.start();
    }
}

class Dog{

}
2.5.8.3 调用 wait/notify 撤销

        对象调用 wait/notify 方法时会将偏向锁升级为轻量级锁,测试代码如下:

@Slf4j
public class TestB {

    public static void main(String[] args) throws InterruptedException {
        Dog d = new Dog();
        Thread t1 = new Thread(() -> {
            log.debug( ClassLayout.parseInstance(d).toPrintable());
            synchronized (d) {
                log.debug( ClassLayout.parseInstance(d).toPrintable());
                try {
                    d.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug( ClassLayout.parseInstance(d).toPrintable());
            }
        }, "t1");
        t1.start();
        new Thread(() -> {
            try {
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (d) {
                log.debug("notify");
                d.notify();
            }
        }, "t2").start();
    }
}

class Dog{

}

2.5.9 批量重偏向

        如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID。

        当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。

        测试代码如下:

@Slf4j
public class TestB {

    public static void main(String[] args) throws InterruptedException {
            Vector<Dog> list = new Vector<>();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 30; i++) {
                    Dog d = new Dog();
                    list.add(d);
                    synchronized (d) {
                        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                    }
                }
                synchronized (list) {
                    list.notify();
                }
            }, "t1");
            t1.start();

            Thread t2 = new Thread(() -> {
                synchronized (list) {
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("===============> ");
                for (int i = 0; i < 30; i++) {
                    Dog d = list.get(i);
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                    synchronized (d) {
                        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                    }
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
            }, "t2");
            t2.start();
        }
}

class Dog{

}

2.5.10 批量撤销

        当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

        测试代码如下:

@Slf4j
public class TestB {
    static Thread t1,t2,t3;
    public static void main(String[] args) throws InterruptedException {

        Vector<Dog> list = new Vector<>();
        int loopNumber = 39;
        t1 = new Thread(() -> {
            for (int i = 0; i < loopNumber; i++) {
                Dog d = new Dog();
                list.add(d);
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
            }
            LockSupport.unpark(t2);
        }, "t1");
        t1.start();
        t2 = new Thread(() -> {
            LockSupport.park();
            log.debug("===============> ");
            for (int i = 0; i < loopNumber; i++) {
                Dog d = list.get(i);
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
            }
            LockSupport.unpark(t3);
        }, "t2");
        t2.start();
        t3 = new Thread(() -> {
            LockSupport.park();
            log.debug("===============> ");
            for (int i = 0; i < loopNumber; i++) {
                Dog d = list.get(i);
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
            }
        }, "t3");
        t3.start();
        t3.join();
        log.debug(ClassLayout.parseInstance(new Dog()).toPrintable());
    }
}

class Dog{

}

2.6 锁消除

        测试代码如下,主要讲的是里面有两个方法都对静态变量进行 ++ 操作,不同的是 b 方法先创建了一个局部变量并进行了加锁,我们来分析下不加锁和加锁性能到底差了多少

@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations=3)
@Measurement(iterations=5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
    static int x = 0;
    @Benchmark
    public void a() throws Exception {
        x++;
    }
    @Benchmark
    public void b() throws Exception {
        Object o = new Object();
        synchronized (o) {
            x++;
        }
    }
}

        运行结果如下: 

        我们发现这两个方法得分几乎一致,明明一个加锁了,肯定是有性能损耗的。

        因为 Java 运行时有一个 JIT 即时编译器,他会对我们的字节码进行进一步优化,他发现这个 o 对象不会逃离方法的作用域,所以它会把 synchronized 关键字给优化掉,真正执行时是没有 synchronized 的,只是执行了里面的 ++ 操作。所以性能是非常相近的。

        这种操作是有一个开关可以控制的,锁消除开关默认打开,命令为:-XX:-EliminateLocks,接下来我们关闭这个开关来测试下:

        可以发现性能相差的还是蛮多的。

2.7 总结

        synchronized 的加锁过程为:无锁 >>>> 偏向锁 >>>> 轻量级锁 >>>> 重量级锁。

        synchronized 的锁分为以上四个阶段,会根据实际情况,对锁进行升级。应该注意的是,目前所只能升级,不能降级。

2.7.1 偏向锁

        当锁第一次被一个线程获取时,优先进入偏向锁状态。偏向锁并非真的“加锁”,而是在对象头中做一个“偏向锁标记”,记录该锁属于哪个线程。如果后续没有其他线程竞争该锁,那么该锁不会有后续升级操作,减少了加锁带来的系统开销。

        如果后续有其他线程竞争该锁,那么锁会真正的加锁,升级为轻量级锁。由于之前已经记录了该锁属于哪个线程,所以此时锁也是被记录的线程获取的。这种升级操作实际上属于延迟加锁,不必要不加锁,减少了加锁的系统开销,提高了运行效率。

2.7.2 轻量级锁

        随着锁竞争的开始,锁将进入轻量级锁的状态,在初始状态是通过自旋锁实现的。自旋锁是循环不断地让线程尝试获取锁。优点在于当锁被释放,其他线程可以第一时间获取到锁。而自旋锁的缺点也在于会一直占用 CPU 资源。synchronized 对此也进行了优化,当自旋达到一定的时间或次数时,就不再自旋了,将转换为挂起等待。

        同时,synchronized 内部也会统计当前锁对象有多少线程在竞争,如果锁竞争更加激烈,synchronized 就会从轻量级锁升级为重量级锁。

2.7.3 重量级锁

        重量级锁是指使用内核提供的 mutex 锁。mutex 锁执行加锁操作时,会先进入内核态,在内核态判定当前锁是否已经被占用。如果该锁没有被占用,则加锁成功,并切换回用户态。如果该锁已经被占用,则加锁失败,线程阻塞等待,直到下一次唤醒。

2.7.4 锁消除

        锁消除是 synchronized 锁的一种较保守的优化策略,通过编译器和 JVM 判断锁是否可以消除。这里的锁消除只会处理一些直接可以判断,完全不涉及线程安全问题的锁,比如在单线程环境下使用 StringBuffer 类中的方法。

2.7.5 锁粗化

        这里有一个锁的粒度的概念,可以这么认为:在锁对象代码块中的代码越少则认为锁的粒度越细,反之则是越粗。实际开发中,使用细粒度的锁,往往是为了锁可以被其他线程及时获取。但有时,可能很长一段时间都没用其他线程来竞争这个锁。

        因此,如果一段逻辑中出现多次加锁解锁,根据编译器和 JVM 的判断会自动对锁进行粗化。锁粗化是指将多个细粒度的锁合并为一个粗粒度的锁,可以在特定场景下提高程序的执行效率,减小系统开销。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐的小三菊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值