03共享模型之管程(上):synchronized与Monitor
本章进入「共享模型之管程」。多个线程访问同一个共享变量时,指令交错会导致结果不可预测。本文先从 i++ 的线程安全问题讲起,介绍临界区与竞态条件,再讲解 synchronized 的三种用法、变量的线程安全分析方法,最后深入 Java 对象头与 Monitor 的工作原理。
4.1 共享带来的问题
小故事
老王(操作系统)有一个功能强大的算盘(CPU),想把它租出去赚点外快。小南、小女(线程)来使用算盘做计算,并按时间付费。
- 小南不能一天 24 小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 IO 操作),有时还需要一根烟,没烟时思路全无(wait),这些情况统称为阻塞。
- 在这些时候算盘没利用起来(不能收钱了),老王觉得不划算;另外小女也想用算盘,总让小南占着也不公平。
- 于是老王想了个办法:每人用一会,轮流使用算盘。这样当小南阻塞时,算盘可以分给小女,反之亦然。
最近的计算比较复杂,需要存储中间结果,而学生们的脑容量(工作内存)不够,老王申请了一个笔记本(主存),把中间结果先记在本上。
但由于分时系统,事故还是发生了:
- 小南刚读取初始值 0 做了 +1 运算,还没来得及写回结果,时间片就到了,只能念叨着「结果是 1」到一边等待(上下文切换)。
- 小女看到笔记本上还写着 0,做了 -1 运算,把 -1 写入笔记本。
- 小女时间片用完,老王又叫醒小南,小南把脑海中的结果 1 写入笔记本。
小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0。

Java 代码的体现
两个线程对初始值为 0 的静态变量一个做自增、一个做自减,各做 5000 次,结果一定是 0 吗?
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}运行结果可能是正数、负数或零。
问题分析
Java 中对静态变量的自增、自减并不是原子操作,从字节码可以清楚看到。例如 i++(i 为静态变量)实际会产生如下 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i而 i-- 类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量iJava 的内存模型中,完成静态变量的自增、自减需要在主存和线程的工作内存之间进行数据交换:

单线程下这 8 行代码顺序执行(不会交错),没有问题。但多线程下指令可能交错运行:
- 出现负数的情况:线程 2 先读取 0 完成 isub 得到 -1(尚未写回),上下文切换;线程 1 读取 0 完成 iadd 得到 1,先写回 1;切换回线程 2 后写回 -1。
- 出现正数的情况:线程 1 先读取 0 完成 iadd 得到 1(尚未写回),上下文切换;线程 2 读取 0 完成 isub 得到 -1,先写回 -1;切换回线程 1 后写回 1。
临界区 Critical Section
- 一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源。
- 多个线程读共享资源其实也没有问题。
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题。
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
例如下面代码中的临界区:
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
4.2 synchronized 解决方案
应用之互斥
为了避免临界区发生竞态条件,有多种手段:
- 阻塞式的解决方案:synchronized、Lock
- 非阻塞式的解决方案:原子变量
这里使用阻塞式的 synchronized,即俗称的【对象锁】。它采用互斥的方式,让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个对象锁时就会阻塞住。这样就能保证持有锁的线程可以安全地执行临界区内的代码,不用担心线程上下文切换。
注意:虽然 Java 中互斥和同步都可以用 synchronized 完成,但它们有区别:
- 互斥是保证临界区同一时刻只能有一个线程执行;
- 同步是由于线程执行的先后顺序不同,需要一个线程等待其它线程运行到某个点。
synchronized 语法
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}用 synchronized 改造前面的计数器:
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}用房间来类比
synchronized(对象) 中的对象可以想象为一个房间(room),有唯一入口(门),房间一次只能进入一人,线程 t1、t2 是两个人:
- t1 执行到
synchronized(room)时,好比进入房间并锁住门、拿走钥匙,在门内执行counter++。 - 这时 t2 运行到
synchronized(room),发现门被锁住,只能在门外等待,发生上下文切换,阻塞住了。 - 这中间即使 t1 的 CPU 时间片用完被踢出了门外(不是锁住对象就能一直执行下去),门仍然锁着,钥匙还在 t1 手里,t2 仍处于阻塞状态;只有 t1 再次获得时间片时才能开门进入。
- 当 t1 执行完 synchronized 块内的代码,才会出门、解开门锁、唤醒 t2 把钥匙交给它,t2 才能进入房间执行
counter--。
思考
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外不可分割,不会被线程切换所打断。下面几个问题可以加深理解:
- 如果把
synchronized(obj)放在 for 循环的外面,如何理解?—— 整个循环成为一个大的原子操作。 - 如果 t1
synchronized(obj1)而 t2synchronized(obj2)会怎样?—— 两把不同的锁,互不阻塞,临界区仍然有竞态条件。 - 如果 t1
synchronized(obj)而 t2 没有加会怎么样?—— t2 不参与竞争,相当于翻窗户进屋,锁保护不了它。
面向对象改进
把需要保护的共享变量放入一个类中:
class Room {
int value = 0;
public void increment() {
synchronized (this) {
value++;
}
}
public void decrement() {
synchronized (this) {
value--;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}
@Slf4j
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count: {}" , room.get());
}
}4.3 方法上的 synchronized
成员方法上的 synchronized
class Test{
public synchronized void test() {
}
}等价于:
class Test{
public void test() {
synchronized(this) {
}
}
}静态方法上的 synchronized
class Test{
public synchronized static void test() {
}
}等价于:
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}不加 synchronized 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的),锁对它不起作用。
所谓的「线程八锁」
线程八锁其实就是考察 synchronized 锁住的是哪个对象。
情况 1:结果为 12 或 21(两个方法都锁 this,同一对象互斥)
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}情况 2:1s 后 12,或 2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}情况 3:3 1s 12,或 23 1s 1,或 32 1s 1(c 不加锁,不受影响)
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
new Thread(()->{ n1.c(); }).start();
}情况 4:2 1s 后 1(两个线程锁的是不同对象)
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}情况 5:2 1s 后 1(静态方法锁 Class 对象,成员方法锁 this,不是同一把锁)
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}情况 6:1s 后 12,或 2 1s 后 1(都是静态方法,锁同一个 Class 对象)
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}情况 7:2 1s 后 1(a 锁 Class,b 锁 n2 这个实例)
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}情况 8:1s 后 12,或 2 1s 后 1(都是静态方法,即使是两个实例,锁的仍是同一个 Class 对象)
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}4.4 变量的线程安全分析
成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全。
- 如果被共享了,根据状态是否能改变分两种情况:
- 如果只有读操作,则线程安全;
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全。
局部变量是否线程安全?
- 局部变量是线程安全的。
- 但局部变量引用的对象则未必:
- 如果该对象没有逃离方法的作用范围,它是线程安全的;
- 如果该对象逃离了方法的作用范围,需要考虑线程安全。
局部变量线程安全分析
每个线程调用 test1() 方法时,局部变量 i 会在每个线程的栈帧内存中被创建多份,因此不存在共享:
public static void test1() {
int i = 10;
i++;
}对应的字节码也能看出 i 是方法栈帧中的局部变量:
public static void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10
2: istore_0
3: iinc 0, 1
6: return
LocalVariableTable:
Start Length Slot Name Signature
3 4 0 i I
成员变量的例子
下面的代码中 list 是成员变量,存在线程安全问题:
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2();
method3();
// } 临界区
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}其中一种情况是,线程 2 还未 add,线程 1 就 remove,会报错:
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:657)
at java.util.ArrayList.remove(ArrayList.java:496)
...分析:无论哪个线程中的 method2,引用的都是同一个对象中的 list 成员变量;method3 与 method2 相同。

将 list 修改为局部变量
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}这样就不会有上述问题了:
- list 是局部变量,每个线程调用时都会创建不同实例,没有共享;
- method2 的参数从 method1 传递过来,与 method1 引用同一个对象;
- method3 的参数分析与 method2 相同。

访问修饰符带来的思考
如果把 method2 和 method3 修改为 public 会不会有线程安全问题?
- 情况 1:有其它线程直接调用 method2 和 method3;
- 情况 2:在情况 1 的基础上,为 ThreadSafe 添加子类,子类覆盖 method2 或 method3:
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}从这个例子可以体会 private 或 final 提供「安全」的意义所在,这正是开闭原则中的【闭】:方法私有或用 final 禁止覆盖,可以避免子类把局部变量暴露给其它线程。
常见线程安全类
常见的线程安全类有:
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
java.util.concurrent包下的类
这里说它们线程安全,是指多个线程调用它们同一个实例的某个方法时是线程安全的,也可以理解为:它们的每个方法是原子的。但要注意,多个方法的组合不是原子的。
单个方法是安全的:
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();线程安全类方法的组合
分析下面代码是否线程安全?
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}虽然 get 和 put 各自都是原子的,但两个线程可能都先 get 到 null,再分别 put,组合起来仍然存在竞态条件。
不可变类的线程安全性
String、Integer 等都是不可变类,其内部状态不可改变,因此它们的方法都是线程安全的。有同学会疑问,String 有 replace、substring 等方法「可以」改变值啊?实际上这些方法返回的是一个新的对象,并没有修改原来的对象。
public class Immutable{
private int value = 0;
public Immutable(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}如果想增加一个 add 方法,应该这样写:
public class Immutable{
private int value = 0;
public Immutable(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
public Immutable add(int v){
return new Immutable(this.value + v);
}
}实例分析
例 1:成员变量是否安全?
public class MyServlet extends HttpServlet {
// 是否安全?
Map<String,Object> map = new HashMap<>();
// 是否安全?
String S1 = "...";
// 是否安全?
final String S2 = "...";
// 是否安全?
Date D1 = new Date();
// 是否安全?
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}- map 不安全(HashMap 可变且有读写);
- S1、S2 安全(String 不可变);
- D1 不安全(Date 对象可变);
- D2 仍然不安全,
final只保证引用不能再指向别的对象,但 Date 内部的日期状态可以被修改。
例 2:Service 中的计数
public class MyServlet extends HttpServlet {
// 是否安全?
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数
private int count = 0;
public void update() {
// ...
count++;
}
}不安全。Servlet 单例、Service 单例,count 是被多线程读写的成员变量。
例 3:切面中记录开始时间
@Aspect
@Component
public class MyAspect {
// 是否安全?
private long start = 0L;
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}不安全。单例切面中的 start 被多个请求线程共享,before/after 之间可能被其它线程改写。
例 4:DAO 中使用局部 Connection
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}安全。conn 是方法内的局部变量,每个线程各有一份。
例 5:DAO 中 Connection 是成员变量
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}不安全。conn 是成员变量,被多个线程共享。
例 6:每次调用 new 一个 DAO,以及把局部对象交给外星方法
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}UserDaoImpl 每次在方法内创建,是线程安全的。再看下面的抽象类:
public abstract class Test {
public void bar() {
// 是否安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}sdf 本来是局部变量,但 foo 的行为是不确定的。
例 7:外星方法导致对象逃离
public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}foo 的实现把 sdf 传递给了 20 个线程,导致局部对象逃离了方法作用范围。这种行为不确定的方法被称为外星方法,SimpleDateFormat 本身又不是线程安全的。可以对比体会 JDK 中 String 类为什么被设计为 final 且不可变。
例 8:在 Integer 对象上加锁
private static Integer i = 0;
public static void main(String[] args) throws InterruptedException {
List<Thread> list = new ArrayList<>();
for (int j = 0; j < 2; j++) {
Thread thread = new Thread(() -> {
for (int k = 0; k < 5000; k++) {
synchronized (i) {
i++;
}
}
}, "" + j);
list.add(thread);
}
list.stream().forEach(t -> t.start());
list.stream().forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
log.debug("{}", i);
}不安全。Integer 是不可变类,i++ 实际上等价于 i = Integer.valueOf(i.intValue() + 1),会生成新对象,导致 synchronized(i) 每次锁住的都不是同一个对象。
4.5 习题
卖票练习
测试下面代码是否存在线程安全问题,并尝试改正:
public class ExerciseSell {
public static void main(String[] args) {
TicketWindow ticketWindow = new TicketWindow(2000);
List<Thread> list = new ArrayList<>();
// 用来存储买出去多少张票
List<Integer> sellCount = new Vector<>();
for (int i = 0; i < 2000; i++) {
Thread t = new Thread(() -> {
// 分析这里的竞态条件
int count = ticketWindow.sell(randomAmount());
sellCount.add(count);
});
list.add(t);
t.start();
}
list.forEach((t) -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 买出去的票求和
log.debug("selled count:{}",sellCount.stream().mapToInt(c -> c).sum());
// 剩余票数
log.debug("remainder count:{}", ticketWindow.getCount());
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~5
public static int randomAmount() {
return random.nextInt(5) + 1;
}
}
class TicketWindow {
private int count;
public TicketWindow(int count) {
this.count = count;
}
public int getCount() {
return count;
}
public int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}sell 方法中对共享变量 count 的「判断 - 扣减」是临界区,多线程下可能超卖,需要给 sell(以及读取剩余票数的 getCount)加上 synchronized。
另外,把 new Vector<>() 换成普通的 new ArrayList<>() 行不行?不行,sellCount.add 也会被多个线程并发调用,ArrayList 自身线程不安全,仍应使用 Vector 等线程安全集合或加锁。
转账练习
测试下面代码是否存在线程安全问题,并尝试改正:
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看转账2000次后的总金额
log.debug("total:{}",(a.getMoney() + b.getMoney()));
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) +1;
}
}
class Account {
private int money;
public Account(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
public void transfer(Account target, int amount) {
if (this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}这样改正行不行?
public synchronized void transfer(Account target, int amount) {
if (this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}不行。成员方法加 synchronized 锁住的是 this,a 线程锁的是 a 对象,b 线程锁的是 b 对象,两把锁互不阻塞,临界区仍然会交错。要让所有转账线程互斥,需要一把共享的锁,例如锁住 Account 类对象:
public void transfer(Account target, int amount) {
synchronized (Account.class) {
if (this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}4.6 Monitor 概念
Java 对象头
以 32 位虚拟机为例,普通对象的对象头由 Mark Word 和 Klass Word 组成:
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|数组对象的对象头还多一个 array length 字段:
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|其中 Mark Word 的结构为:
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|64 位虚拟机的 Mark Word 如下:
|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|可以看到,Mark Word 在不同状态下会记录哈希码、GC 分代年龄、偏向线程 ID、锁记录指针或重量级 Monitor 指针等信息。
Monitor 的结构
Monitor 被称为监视器或管程。每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级),该对象头的 Mark Word 中就被设置为指向 Monitor 对象的指针。
Monitor 的结构可以用「房间」来类比:

- Owner:同一时刻只有一个线程能成为 Owner,持有对象锁,在房间内执行临界区代码;
- EntryList:竞争锁失败的线程进入 EntryList 阻塞等待;
- WaitSet:曾经获得过锁、但条件不满足而调用 wait 的线程进入 WaitSet 等待(下一篇详解)。
synchronized 的工作原理
- 线程执行 synchronized 代码块时,先尝试让自己成为 Monitor 的 Owner:
- 成功,就持有 Monitor 继续执行临界区代码;
- 失败(Monitor 的 Owner 已经是别的线程),就进入 EntryList 变成 BLOCKED 状态。
- Owner 线程执行完同步代码块后释放锁,唤醒 EntryList 中阻塞的线程重新竞争。
- wait/notify 的等待与唤醒则发生在 WaitSet 与 EntryList 之间。
synchronized 优化初步
早期 synchronized 直接使用 Monitor(重量级锁),成本较高。JDK 6 对 synchronized 做了大量优化:偏向锁、轻量级锁、重量级锁会随着竞争情况逐步升级。仍用小故事来理解:
故事角色:
- 老王 —— JVM
- 小南、小女 —— 线程
- 房间 —— 对象
- 房间门上的防盗锁 —— Monitor(重量级锁)
- 房间门上挂的小南书包 —— 轻量级锁
- 房间门上刻上小南大名 —— 偏向锁
- 批量重刻名 —— 一个类的偏向锁撤销达到 20 次阈值后的批量重偏向
- 不能刻名字 —— 批量撤销该类对象的偏向锁,设置该类不可偏向
故事过程:
- 小南最初用的是防盗锁(重量级锁),上下文切换时锁住门,即使离开了别人也进不来,工作是安全的,但上锁、解锁以及操作系统层面的阻塞切换开销大。
- 很多时候根本没人竞争房间(小南白天用、小女晚上用,时间错开),每次都上锁太麻烦。于是约定谁用房间谁把书包挂在门口(轻量级锁):进门先翻翻书包确认是不是自己的,是自己的就直接进,省去系统级加锁解锁;万一书包不是自己的,就在门外等,并通知对方下次改用锁门的方式(锁膨胀)。
- 后来小女很长时间不用房间,小南挂书包、翻书包还是觉得麻烦,干脆在门上刻上自己的名字(偏向锁):下次来只要名字还在,说明没人打扰,可以直接使用;如果这期间有别人要用房间,就由使用者擦掉名字,升级为挂书包(轻量级锁)的方式。
- 小南在 20 个房间都刻了名字,放假回来的小女要一个个擦掉、升级,成本很高。于是 JVM 提供了批量重偏向:让小女可以直接在门上重刻名字;当刻名/擦名现象越来越频繁,则批量撤销该类所有对象的偏向锁,并设置该类不可偏向,之后只能走轻量级锁流程。
三种锁的升级路径与 Mark Word 的状态一一对应:Biased(偏向锁)→ Lightweight Locked(轻量级锁)→ Heavyweight Locked(重量级锁,指向 Monitor)。锁只能升级不能降级,具体的升级与优化细节在 synchronized 进阶原理中还会深入讲解。