前言
提示:本博客仅为本人学习记录笔记使用,有错误可以进行指正。
Java中存在一个synchronized关键字, 它是用来保证线程安全的一个机制 , 但本人一直对其原理没有深入研究过, 故而写这个synchronized原理系列来简单介绍一下其原理,供以后回头复习并发知识用。
前置知识
在真正介绍synchronized的概念之前,首先需要了解几个知识,Java对象头和Monitor监视器锁的知识。本篇就先来简单介绍一下这两个概念。
一、Java对象头
什么是Java对象头呢,可能大家都知道在Java中生成出来的对象会存放在JVM的堆区中,它们是线程共享的。这些对象可能由很多结构组成。而对象头就是其中的一部分结构, 是对象的头部结构,所以叫Java对象头(Object Header)。下图简单展示了Java对象的一个结构. 可以看到对象结构由三部分组成。对象头,实例数据以及对齐填充。
那么对象头的详细结构又是怎么样的呢? 下面阐述的结构32位JVM为准。普通对象的对象头由两部分组成:
一、Mark Word (32bits)
二、Klass Word (32bits)
而数组对象的对象头由三部分组成:
一、Mark Word (32bits)
二、Klass Word (32bits)
三、Array Length (32bits)
下面来简单介绍一下这三部分的概念。
1.Mark Word
Mark Word 在对象头中是一个很重要的结构。它标识了一个对象在不同加锁状态下的一个结构。那么Mark Word在32位虚拟机下的结构有哪几种情况呢。下图展示了Mark Word对应的结构。
可以看到在不同的加锁状态下,对应的MarkWord的结构是不一致的,具体每个锁状态对应的结构会在后面的系列详细阐述,此处先有个印象即可。
2.Klass Word
Klass Word可以简单理解成为指向该对象对应的方法区类对象的一个指针。通过这个指针就能够标识该类是哪个类型的数据,这里就不再过多介绍了。
3.Array Length
Array Length这个结构翻译过来就是数组长度。所以此部分由数组对象特有,是用来标识数组的长度的结构。
二、Monitor(监视器锁)
1.Monitor简介
Monitor可以被翻译成监视器或管程,每一个Java对象都会关联一个Monitor对象 (重要)。我们平时在使用synchronized关键字对一个对象加锁(重量级)时,其流程是与这个Monitor对象息息相关的。所以了解Monitor对象,可以帮助我们很好的理解synchronized的原理。话不多说,直接开整。
首先来看下Monitor对象的结构,如下图所示:
可以看到绿色部分对应的Monitor对象的核心组成部分有如下几个:
一、Owner
二、EntryList
三、WaitSet
Owner: 它标识了当前的持有Monitor对象的线程信息。
EntryList: 可以把它理解成一个阻塞队列,因为同一时间只有一个线程能争抢到锁,那么没有争抢到锁的线程就会被阻塞(状态变成Blocked),也就是加入到Monitor对象的EntryList中,等待Owner中的线程释放锁后,再去重新争抢锁。
WaitSet: 翻译过来叫等待集合,当持有锁的线程调用了锁对象的wait方法时, 因为wait方法会去释放当前锁,所以当前线程就会从Owner退出来,加入到WaitSet中,并且状态变成Waiting状态,等待其他的线程重新唤醒。
2.样例分析
上面介绍了一下Monitor的大致结构,现在让我们再来看一段代码,然后借助这个例子来阐述一下大致的流程,代码如下:
public class Main {
// 一个锁对象
private Lock lock = new Lock();
// 共享变量
private int num = 0;
public static void main(String[] args) {
Main main = new Main();
// 创建两个线程调用main对象的testLock方法
Thread t1 = new Thread(main::testLock, "t1");
Thread t2 = new Thread(main::testLock, "t2");
// 启动t1,t2两个线程
t1.start();
t2.start();
try {
// 此处调用t1,t2的join方法,保证两个线程结束后,在执行下面的代码
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出num, 若线程安全, 则num为10000. 若线程不安全, 则num不为10000
System.out.println("num最终为:" + main.num);
}
// 测试synchronized的一个方法
public void testLock() {
// 当前线程对lock对象加锁
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "对lock上了锁");
// 临界区代码, 对num加1, 5000次
for(int i = 0; i < 5000;i++) {
this.num++;
}
}
System.out.println(Thread.currentThread().getName() + "对lock释放了锁");
}
}
class Lock {
public Lock() {
}
}
结果如下: 可以看到t1, t2线程只能同时有1个线程先执行完临界区的代码
t1对lock上了锁
t1对lock释放了锁
t2对lock上了锁
t2对lock释放了锁
num最终为:10000
再来对比一下如果不加synchronized关键字的结果: t1, t2线程会同时执行临界区的代码。就会导致线程不安全问题
t2对lock上了锁
t1对lock上了锁
t2对lock释放了锁
t1对lock释放了锁
num最终为:9358
流程分析: 可以看到代码中有两个线程t1, t2。它们在testLock()方法中使用synchronized尝试去给lock对象加锁,并执行num自增5000次的操作。假设线程t1先执行到了synchronized(lock) 这行代码,那么t1首先会去检查lock对象对应的Monitor对象的Owner是否已经被其他线程所占用,如果没有被占用,则t1线程就变成Owner, 在t1线程没释放锁之前,任何线程都不能再成为该lock对象对应的Monitor的Owner。接着t2线程执行testLock()方法,然后再继续走上面提到的流程,发现Monitor对象的Owner已经存在别的线程了,那么t2线程只能先加入到EntryList中,并且状态为Blocked(阻塞),直到t1线程释放锁后,再去争抢锁。若争抢到了,则t2就成为了Owner,在去继续执行相应的临界区代码, 否则还是在EntryList中阻塞,下图简单展示了一个大概的流程,读者可以结合下图理解这段话的含义。当然我上面的例子只有两个线程, 读者可以去思考有N个线程的场景,举一反三。
这里在简单的提一下,如果某个线程成为了Monitor对象的Owner。那么这个线程持有的锁对象的Mark Word就会变成上述的重量级锁状态对应的结构。也就是下图的结构。 其中前30位字节代表的是指向monitor对象的一个指针, 后两位10代表的是重量级锁的状态。而原本的hashcode等信息会被存入到Monitor对象中。等到线程释放锁的时候再被重置回来。
好了,如果看到这里的话相信大家已经对java对象头和Monitor对象已经有一个大致的了解了。有些博文可能会以字节码的角度阐述加锁的流程。字节码指令中的monitorenter就是去关联monitor对象的流程。monitorexit就是线程释放锁并且重置MarkWord的流程. 如果synchornized中出现异常,也会出现monitorexit指令去释放锁,下图是testLock方法的字节码指令,可以看到6是monitorenter指令。而63,69是monitorexit指令,分别代表关联monitor对象,正常退出释放锁和出现异常释放锁的情况。唔,这里笔者技术不够,就不再继续班门弄斧了。
public void testLock();
Code:
0: aload_0
1: getfield #4 // Field lock:LLock;
4: dup
5: astore_1
6: monitorenter
7: getstatic #18 // Field java/lang/System.out:Ljava/io/PrintStream;
10: new #19 // class java/lang/StringBuilder
13: dup
14: invokespecial #20 // Method java/lang/StringBuilder."<init>":()V
17: invokestatic #26 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
20: invokevirtual #27 // Method java/lang/Thread.getName:()Ljava/lang/String;
23: invokevirtual #22 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
26: ldc #28 // String 对lock上了锁
28: invokevirtual #22 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #24 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: invokevirtual #25 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: iconst_0
38: istore_2
39: iload_2
40: sipush 5000
43: if_icmpge 62
46: aload_0
47: dup
48: getfield #5 // Field num:I
51: iconst_1
52: iadd
53: putfield #5 // Field num:I
56: iinc 2, 1
59: goto 39
62: aload_1
63: monitorexit
64: goto 72
67: astore_3
68: aload_1
69: monitorexit
70: aload_3
71: athrow
72: getstatic #18 // Field java/lang/System.out:Ljava/io/PrintStream;
75: new #19 // class java/lang/StringBuilder
78: dup
79: invokespecial #20 // Method java/lang/StringBuilder."<init>":()V
82: invokestatic #26 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
85: invokevirtual #27 // Method java/lang/Thread.getName:()Ljava/lang/String;
88: invokevirtual #22 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
91: ldc #29 // String 对lock释放了锁
93: invokevirtual #22 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
96: invokevirtual #24 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
99: invokevirtual #25 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
102: return
总结
提示:重申一遍,本博客仅为本人学习记录笔记使用,有错误可以进行指正。
本篇作为笔者的第一篇博客,也是力求以能让大家理解的话术去编写了这篇文章。相信大家仔细阅读后,一定能够充分了解一波Java对象头和Monitor对象的知识。笔者也只是一个在奋力挣扎的菜鸡,希望大家互相共勉吧。