syncronized关键字是JVM内置锁,是对象锁。对某一部分代码使用锁后,这段代码将被视为原子操作执行,保证多个线程对该代码块的访问会按照一定顺序先后执行。
假设我们对以下代码进行加锁:
public class MyTest {
public final static Object o = new Object();
public static int i = 0;
public static void main(String[] args) {
synchronized (o) {
i++;
}
}
}
再使用JDK自带的javap命令查看该文件的字节码文件,则会看到以下信息:
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field o:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter // ~~~~~~~~~~请注意该行~~~~~~~~~~~~
6: getstatic #3 // Field i:I
9: iconst_1
10: iadd
11: putstatic #3 // Field i:I
14: aload_1
15: monitorexit // ~~~~~~~~~~请注意该行~~~~~~~~~~~~
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
这里出现了两条指令,分别是monitorenter
和monitorexit
。
这两条指令分别标识同步块的开始和结束。而这两个指令,是由JVM进行添加的,所以synchronized是一个JVM内置锁。
JVM在识别synchronized关键字后,会通过传入对象的ObjectMonitor
进行锁相关的操作。
这个ObjectMonitor
是每个Ojbect被创建的时候都会自动创建到JVM内部的。
加锁解锁的过程中,肯定需要记录锁的状态和信息,因此需要一个内存空间用来记录。
JVM使用对象中的对象头
来记录对象加锁解锁的状态。
请看下图:
由图可见,一个java对象的内存结构包含了3部分:
紧接着,咱们还可以查看一下JVM的源码,可以从网上下载一份源码,同时找到下面的文件:
/hotspot/src/share/vm/runtime/objectMonitor.hpp
打开看一下,会发现下面的内容:
ObjectMonitor(){
_header = NULL;
_count = 0+1-1 ;
_waiters = 0;
_WaitSet = NULL;
//此处省略一部分...
_owner = NULL;
_EntryList = NULL;
//...
}
当然,中间省略了很多参数。但是从上面几个参数来看,我们很简单就能和上面的图联系起来。
_header: 对象头
_count: 记录加锁的次数,重入时用到
_waiters: 当前有多少thread处于wait状态
_WaitSet: wait中的线程会被加入该集合
_owner: 当前哪个线程持有ObjectMonitor
_EntryList: 当前等待加锁的线程会被加入到该集合
假设现在有两个线程t1和t2。如果t1加锁成功,则_owner = t1
,且_count += 1
。
当t1释放锁的时候,_owner = NULL
,且_count -= 1
。
当锁需要重入时,会对_count
进行累减到0。
-XX:-DoEscapeAnalysis
关闭逃逸分析。或者设置-XX:+DoEscapeAnalysis
开启逃逸分析。逃逸分析的作用:
什么是锁的粗化:
当一个方法中有连续的加锁操作时,多次加锁操作将被JIT优化为一个锁,减少了频繁申请和释放锁的次数,提高了性能。
示例代码如下:
public static void main(String[] args) {
synchronized (LockTest.class){
System.out.println("111");
}
synchronized (LockTest.class){
System.out.println("222");
}
synchronized (LockTest.class){
System.out.println("333");
}
}
来查看一下字节码文件:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: ldc #2 // class com/fujitsu/smartstore/LockTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String 111
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: ldc #2 // class com/fujitsu/smartstore/LockTest
25: dup
26: astore_1
27: monitorenter
28: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
31: ldc #6 // String 222
33: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: aload_1
37: monitorexit
38: goto 46
41: astore_3
42: aload_1
43: monitorexit
44: aload_3
45: athrow
46: ldc #2 // class com/fujitsu/smartstore/LockTest
48: dup
49: astore_1
50: monitorenter
51: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
54: ldc #7 // String 333
56: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
59: aload_1
60: monitorexit
61: goto 71
64: astore 4
66: aload_1
67: monitorexit
68: aload 4
70: athrow
71: return
指令比较多,可以ctrl + F
进行搜索,我们可以找到这两条指令:monitorenter
、monitorexit
。看,前面学习的syncronized原理在这里就用上了。
咱们通过看字节码文件可以发现,这里的锁并没有被粗化成一个锁。
那是因为我们这里查看的是javac
编译后的字节码文件。而逃逸分析的优化是JIT
将字节码文件翻译为机器码文件过程中进行的。
如果想要查看JIT编译后的文件,可以使用JITWatch等工具查看,此处不再赘述。
锁的消除很显而易见,当某个代码块使用的锁对象只能被一个线程获取到,则JIT会消除该同步操作。
如下:
public static void main(String[] args) {
synchronized (new Object()){
System.out.println("111");
}
}
多个线程获取到的锁对象都是不同的Object对象,因此JIT进行逃逸分析时会消除该同步操作。
/hotspot/src/share/vm/oops/markOop.hpp
文件中有对对象头的存储位数进行解释说明。比如,以32位JVM为例,前25位存储hash code,接下来的4位存储age,再往后的1位存储偏向状态,紧接着的2位存储锁的状态。锁升级过程(不可逆):
偏向锁:减少同一个线程去获取锁的成本。在没有竞争线程的情况下,当某个线程短时间内多次获取到锁后,该锁会偏向该线程,Mark Word的结构会转变为偏向锁结构,该线程再次请求该锁时,就不需要重新申请锁了(减少了一些可能会涉及到CAS的操作)。但是当有竞争线程的情况下,不应该使用偏向锁,因为每次获取锁的线程都可能不同。偏向锁失败后就会升级到轻量级锁。
轻量级锁:偏向锁失效后会先升级为轻量级锁(JDK1.6后加入),此时Mark Word转换为轻量级锁的结构。轻量级锁提升性能的依据是:“大部分锁在整个同步周期内都不存在竞争”,但这是经验数据,轻量级锁只适用于线程交替执行同步块的场景。如果同一时间访问同一个锁的话,就会导致轻量级锁升级为重量级锁。
自旋锁:轻量级锁失效后,JVM为了避免线程在操作系统中真正挂起,还会采用自旋锁的形式进行优化。这是基于在大多数情况下,线程持有锁的时间都不会太长而设计的。如果线程直接在操作系统上挂起,就涉及到用户态和内核态之间的切换,这个操作极度消耗时间。由于线程不会持有锁太长时间,因此其他线程自旋中一旦获取到锁,就顺利进入临界区,而不需要进行用户态到内核态的转换了。如果CAS获取锁失败达到一定次数,将升级为重量级锁。
重量级锁:通过操作系统Mutex互斥量将线程挂起,实现同步。
由于本章内容过多,所以将AQS放在下一章来学习~
public void test(){
Object o = new Object();
}
下一章:Java架构师学习之路之并发编程四:Java同步器之synchronized&Lock&AQS(下)
欢迎大家一起讨论,让我们一起向着架构师前进~~~
上篇文章给大家介绍了 Java正则表达式匹配,替换,查找,切割的方法 ,接下来,...
复制代码 代码如下: % URL="http://news.163.com/special/00011K6L/rss_newstop....
本文实例讲述了Laravel框架源码解析之反射的使用。分享给大家供大家参考,具体如...
正则忽略大小写 – RegexOptions.IgnoreCase 例如: 复制代码 代码如下: Str = R...
工具:Eclipse,Oracle,smartupload.jar;语言:jsp,Java;数据存储:Oracle。...
错误描述: 在开发.net项目中,通过microsoft.ACE.oledb读取excel文件信息时,报...
项目中用到的一些特殊字符和图标 html代码 XML/HTML Code 复制内容到剪贴板 div ...
Elasticsearch 是通过 Lucene 的倒排索引技术实现比关系型数据库更快的过滤。特...
4月11日20:30~22:00通过腾讯会议进行了第二次在线学习讨论我把学习笔记整理一下...
DELETEFROMTablesWHEREIDNOTIN(SELECTMin(ID)FROMTablesGROUPBYName) Min的话保...