引子 小艾和小牛在路上相遇,小艾一脸沮丧。 小牛:小艾小艾,发生甚么事了? 小艾:别提了,昨天有个面试官问了我好几个关于 synchronized 关键字的问题,没答上来。 小艾:我后来查了很多资料,有二十多页的概念说明,也有三十来页的源码剖析,看得我头大。 小牛:你那看的是死知识,不好用,你得听我的总结。 小艾:看来是有备而来,那您给讲讲吧。 小牛:那咱们开始! synchronized关键字引入 我们知道,在多线程程序中往往会出现这么一个情况:多个线程同时访问某个线程间的共享变量。来举个例子吧: 假设银行存款业务写了两个方法,一个是存钱 store() 方法 ,一个是查询余额 get() 方法。假设初始客户小明的账户余额为 0 元。(PS:这个例子只是个 toy demo,为了方便大家理解写的,真实的业务场景不会这样。) // account 客户在银行的存款 public void store(int money){ int newAccount=account+money; account=newAccount; } public void get(){ System.out.print("小明的银行账户余额:"); System.out.print(account); } 如果小明为自己存款 1 元,我们期望的线程调用情况如下: 首先会启动一个线程调用 store() 方法,为客户账户余额增加 1; 再启动一个线程调用 get() 方法,输出客户的新余额为 1。 但实际情况可能由于线程执行的先后顺序,出现如图所示的错误: 小明存钱流程 小明:咱家没钱了 小明会惊奇的以为自己的钱没存上。这就是一个典型的由共享数据引发的并发数据冲突问题。 解决方式也很简单,让并发执行会产生问题的代码段不并发行了。 如果 store() 方法 执行完,才能执行 get() 方法,而不是像上图一样并发执行,自然不会出现这个问题。那如何才能做到呢? 答案就是使用 synchronized 关键字。 我们先从直觉上思考一下,如果要实现先执行 store() 方法,再执行 get() 方法的话该怎么设计。 我们可以设置某个锁,锁会有两种状态,分别是上锁和解锁。在 store() 方法执行之前,先观察这个锁的状态,如果是上锁状态,就进入阻塞,代码不运行; 如果这把锁是解锁状态,那就先将这把锁状态变为上锁,之后接着运行自己的代码。运行完成之后再将锁状态设置为解锁。 对于 get() 方法也是如此。 Java 中的 synchronized 关键字就是基于这种思想设计的。在 synchronized 关键字中,锁就是一个对象。 synchronized 一共有三种使用方法: 直接修饰某个实例方法。像上文代码一样,在这种情况下多线程并发访问实例方法时,如果其他线程调用同一个对象的被 synchronized 修饰的方法,就会被阻塞。相当于把锁记录在这个方法对应的对象上。 // account 客户在银行的存款 public synchronized void store(int money){ int newAccount=account+money; account=newAccount; } public synchronized void get(){ System.out.print("小明的银行账户余额:"); System.out.print(account); } 直接修饰某个静态方法。在这种情况下进行多线程并发访问时,如果其他线程也是调用属于同一类的被 synchronized 修饰的静态方法,就会被阻塞。相当于把锁信息记录在这个方法对应的类上。 public synchronized static void get(){ ··· } 修饰代码块。如果此时有别的线程也想访问某个被synchronized(对象0)修饰的同步代码块时,也会被阻塞。 public static void get(){ synchronized(对象0){ ··· } } 小艾问:我看了不少参考书还有网上资料,都说 synchronized 的锁是锁在对象上的。关于这句话,你能深入讲讲吗? 小牛回答道:别急,我先讲讲 Java 对象在内存中的表示。 Java 对象在内存中的表示 讲清 synchronized 关键字的原理前需要理清 Java 对象在内存中的表示方法。 Java 对象在内存中的表示 上图就是一个 Java 对象在内存中的表示。我们可以看到,内存中的对象一般由三部分组成,分别是对象头、对象实际数据和对齐填充。 对象头包含 Mark Word、Class Pointer和 Length 三部分。 Mark Word 记录了对象关于锁的信息,垃圾回收信息等。 Class Pointer 用于指向对象对应的 Class 对象(其对应的元数据对象)的内存地址。 Length只适用于对象是数组时,它保存了该数组的长度信息。 对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。 对齐填充表示最后一部分的填充字节位,这部分不包含有用信息。 我们刚才讲的锁 synchronized 锁使用的就是对象头的 Mark Word 字段中的一部分。 Mark…