1.引言
线程安全是并发编程的重中之重,造成并发问题的诱因主要有两点。
1.存在临界资源
2.存在多条线程同时操作共享数据
基于这种情况,我们会考虑一种机制,使得一个线程访问临界资源的时候,其他线程不能对其访问,这种机制呢就称为互斥锁。java中的synchronized就是这样一种机制,其修饰的代码块同时只能有一个线程可以访问,因此保证了安全性,同时synchronized还可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到,也就是说同步代码块中的变量无需使用volatile关键字。
2.synchronized的三种使用方式
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
package com.hqa.design.test;
import java.util.concurrent.CountDownLatch;
public class Test {
/**
* 一把实例锁
*/
private static final Object lock = new Object();
/**
* 静态方法锁,作用于Test.class对象
*/
public static synchronized void classLock() {
try {
System.out.println(Thread.currentThread().getName() + "进入classLock方法,休眠5s");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "退出classLock方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 对象实例锁,作用于当前Test类实例
*/
public synchronized void thisLock() {
try {
System.out.println(Thread.currentThread().getName() + "进入thisLock方法,休眠5s");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "退出thisLock方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 加锁对象为lock
*/
public void objLock() {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + "进入objLock方法,休眠5s");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "退出objLock方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
//声明一个闭锁,这个类的作用现在不讲,大家只需知道此类此处
//是为了让子线程全部执行完毕后,主线程再继续执行的作用
final CountDownLatch cnt = new CountDownLatch(2);
// classLock测试
Thread t1 = new Thread(() -> {
Test.classLock();
cnt.countDown();
});
Thread t2 = new Thread(() -> {
Test.classLock();
cnt.countDown();
});
t1.start();
t2.start();
cnt.await();
System.out.println("===============classLock Test Over==============");
CountDownLatch cnt2 = new CountDownLatch(2);
// thisLock测试
Test t = new Test();
Thread t3 = new Thread(() -> {
t.thisLock();
cnt2.countDown();
});
Thread t4 = new Thread(() -> {
t.thisLock();
cnt2.countDown();
});
t3.start();
t4.start();
cnt2.await();
System.out.println("===============thisLock Test Over==============");
CountDownLatch cnt3 = new CountDownLatch(2);
// objLock测试
Thread t5 = new Thread(() -> {
t.objLock();
cnt3.countDown();
});
Thread t6 = new Thread(() -> {
t.objLock();
cnt3.countDown();
});
t5.start();
t6.start();
cnt3.await();
System.out.println("===============objLock Test Over==============");
}
}
输出:
Thread-0进入classLock方法,休眠5s
Thread-0退出classLock方法
Thread-1进入classLock方法,休眠5s
Thread-1退出classLock方法
===============classLock Test Over==============
Thread-3进入thisLock方法,休眠5s
Thread-3退出thisLock方法
Thread-2进入thisLock方法,休眠5s
Thread-2退出thisLock方法
===============thisLock Test Over==============
Thread-4进入objLock方法,休眠5s
Thread-4退出objLock方法
Thread-5进入objLock方法,休眠5s
Thread-5退出objLock方法
===============objLock Test Over==============
3.synchronized语义
JVM虚拟机中的同步是基于Monitor对象实现,此处无论显示同步还是隐示同步皆如此。我们需要注意同步方法并不是由monitorenter和monitorexit 实现,而是通过指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来实现。
4.Java对象头
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下:
ps:图片引用自:https://blog.csdn.net/javazejian/article/details/72828483
对象头分为两种。
4.1 普通对象头
1,Mark Word
2,指向类的指针
4.2 数组对象头
1,Mark Word
2,指向类的指针
3,数组长度(只有数组对象才有)
4.3和同步锁有关的Mark Word
Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:
锁状态 | 25bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否偏向锁 | 锁标志位 | ||
无锁 | 对象的HashCode | 分代年龄 | 0 | 01 | |
偏向锁 | 线程ID | Epoch | 分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向重量级锁的指针 | 10 | |||
GC标记 | 空 | 11 |
其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。
JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。
JVM一般是这样使用锁和Mark Word的:
1,当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
2,当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
3,当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
4,当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
5,偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
6,轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
7,自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
5.重量级锁
我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象
),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示
monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因
6.synchronized反编译
6.1首先准备一个类
package com.hqa.design.test;
import java.util.concurrent.CountDownLatch;
public class Test {
/**
* 一把实例锁
*/
private static final Object lock = new Object();
/**
* 静态方法锁,作用于Test.class对象
*/
public static synchronized void classLock() {
try {
System.out.println(Thread.currentThread().getName() + "进入classLock方法,休眠5s");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "退出classLock方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 对象实例锁,作用于当前Test类实例
*/
public void thisLock() {
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName() + "进入thisLock方法,休眠5s");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "退出thisLock方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 加锁对象为lock
*/
public void objLock() {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + "进入objLock方法,休眠5s");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "退出objLock方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
6.2对Test.class进行javap查看
javap -c -l -v Test > out.txt
输出:
Classfile /D:/mcWork/workspace/design-share/bin/com/hqa/design/test/Test.class
Last modified 2020-1-20; size 1837 bytes
MD5 checksum eaeadfde841431d8d72d966bc14159ba
Compiled from "Test.java"
public class com.hqa.design.test.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // com/hqa/design/test/Test
#2 = Utf8 com/hqa/design/test/Test
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 lock
#6 = Utf8 Ljava/lang/Object;
#7 = Utf8 <clinit>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Methodref #3.#11 // java/lang/Object."<init>":()V
#11 = NameAndType #12:#8 // "<init>":()V
#12 = Utf8 <init>
#13 = Fieldref #1.#14 // com/hqa/design/test/Test.lock:Ljava/lang/Object;
#14 = NameAndType #5:#6 // lock:Ljava/lang/Object;
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/hqa/design/test/Test;
#19 = Utf8 classLock
#20 = Fieldref #21.#23 // java/lang/System.out:Ljava/io/PrintStream;
#21 = Class #22 // java/lang/System
#22 = Utf8 java/lang/System
#23 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Class #27 // java/lang/StringBuilder
#27 = Utf8 java/lang/StringBuilder
#28 = Methodref #29.#31 // java/lang/Thread.currentThread:()Ljava/lang/Thread;
#29 = Class #30 // java/lang/Thread
#30 = Utf8 java/lang/Thread
#31 = NameAndType #32:#33 // currentThread:()Ljava/lang/Thread;
#32 = Utf8 currentThread
#33 = Utf8 ()Ljava/lang/Thread;
#34 = Methodref #29.#35 // java/lang/Thread.getName:()Ljava/lang/String;
#35 = NameAndType #36:#37 // getName:()Ljava/lang/String;
#36 = Utf8 getName
#37 = Utf8 ()Ljava/lang/String;
#38 = Methodref #39.#41 // java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
#39 = Class #40 // java/lang/String
#40 = Utf8 java/lang/String
#41 = NameAndType #42:#43 // valueOf:(Ljava/lang/Object;)Ljava/lang/String;
#42 = Utf8 valueOf
#43 = Utf8 (Ljava/lang/Object;)Ljava/lang/String;
#44 = Methodref #26.#45 // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
#45 = NameAndType #12:#46 // "<init>":(Ljava/lang/String;)V
#46 = Utf8 (Ljava/lang/String;)V
#47 = String #48 // 进入classLock方法,休眠5s
#48 = Utf8 进入classLock方法,休眠5s
#49 = Methodref #26.#50 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#50 = NameAndType #51:#52 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#51 = Utf8 append
#52 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#53 = Methodref #26.#54 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#54 = NameAndType #55:#37 // toString:()Ljava/lang/String;
#55 = Utf8 toString
#56 = Methodref #57.#59 // java/io/PrintStream.println:(Ljava/lang/String;)V
#57 = Class #58 // java/io/PrintStream
#58 = Utf8 java/io/PrintStream
#59 = NameAndType #60:#46 // println:(Ljava/lang/String;)V
#60 = Utf8 println
#61 = Long 5000l
#63 = Methodref #29.#64 // java/lang/Thread.sleep:(J)V
#64 = NameAndType #65:#66 // sleep:(J)V
#65 = Utf8 sleep
#66 = Utf8 (J)V
#67 = String #68 // 退出classLock方法
#68 = Utf8 退出classLock方法
#69 = Methodref #70.#72 // java/lang/InterruptedException.printStackTrace:()V
#70 = Class #71 // java/lang/InterruptedException
#71 = Utf8 java/lang/InterruptedException
#72 = NameAndType #73:#8 // printStackTrace:()V
#73 = Utf8 printStackTrace
#74 = Utf8 e
#75 = Utf8 Ljava/lang/InterruptedException;
#76 = Utf8 StackMapTable
#77 = Utf8 thisLock
#78 = String #79 // 进入thisLock方法,休眠5s
#79 = Utf8 进入thisLock方法,休眠5s
#80 = String #81 // 退出thisLock方法
#81 = Utf8 退出thisLock方法
#82 = Utf8 objLock
#83 = String #84 // 进入objLock方法,休眠5s
#84 = Utf8 进入objLock方法,休眠5s
#85 = String #86 // 退出objLock方法
#86 = Utf8 退出objLock方法
#87 = Class #88 // java/lang/Throwable
#88 = Utf8 java/lang/Throwable
#89 = Utf8 SourceFile
#90 = Utf8 Test.java
{
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #3 // class java/lang/Object
3: dup
4: invokespecial #10 // Method java/lang/Object."<init>":()V
7: putstatic #13 // Field lock:Ljava/lang/Object;
10: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
public com.hqa.design.test.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #10 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/hqa/design/test/Test;
public static synchronized void classLock();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=4, locals=1, args_size=0
0: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #26 // class java/lang/StringBuilder
6: dup
7: invokestatic #28 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
10: invokevirtual #34 // Method java/lang/Thread.getName:()Ljava/lang/String;
13: invokestatic #38 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #44 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
19: ldc #47 // String 进入classLock方法,休眠5s
21: invokevirtual #49 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #53 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: invokevirtual #56 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: ldc2_w #61 // long 5000l
33: invokestatic #63 // Method java/lang/Thread.sleep:(J)V
36: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
39: new #26 // class java/lang/StringBuilder
42: dup
43: invokestatic #28 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
46: invokevirtual #34 // Method java/lang/Thread.getName:()Ljava/lang/String;
49: invokestatic #38 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
52: invokespecial #44 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
55: ldc #67 // String 退出classLock方法
57: invokevirtual #49 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
60: invokevirtual #53 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
63: invokevirtual #56 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: goto 74
69: astore_0
70: aload_0
71: invokevirtual #69 // Method java/lang/InterruptedException.printStackTrace:()V
74: return
Exception table:
from to target type
0 66 69 Class java/lang/InterruptedException
LineNumberTable:
line 17: 0
line 18: 30
line 19: 36
line 20: 66
line 21: 70
line 23: 74
LocalVariableTable:
Start Length Slot Name Signature
70 4 0 e Ljava/lang/InterruptedException;
StackMapTable: number_of_entries = 2
frame_type = 247 /* same_locals_1_stack_item_frame_extended */
offset_delta = 69
stack = [ class java/lang/InterruptedException ]
frame_type = 4 /* same */
public synchronized void thisLock();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=4, locals=2, args_size=1
0: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #26 // class java/lang/StringBuilder
6: dup
7: invokestatic #28 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
10: invokevirtual #34 // Method java/lang/Thread.getName:()Ljava/lang/String;
13: invokestatic #38 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #44 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
19: ldc #78 // String 进入thisLock方法,休眠5s
21: invokevirtual #49 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #53 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: invokevirtual #56 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: ldc2_w #61 // long 5000l
33: invokestatic #63 // Method java/lang/Thread.sleep:(J)V
36: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
39: new #26 // class java/lang/StringBuilder
42: dup
43: invokestatic #28 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
46: invokevirtual #34 // Method java/lang/Thread.getName:()Ljava/lang/String;
49: invokestatic #38 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
52: invokespecial #44 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
55: ldc #80 // String 退出thisLock方法
57: invokevirtual #49 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
60: invokevirtual #53 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
63: invokevirtual #56 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: goto 74
69: astore_1
70: aload_1
71: invokevirtual #69 // Method java/lang/InterruptedException.printStackTrace:()V
74: return
Exception table:
from to target type
0 66 69 Class java/lang/InterruptedException
LineNumberTable:
line 30: 0
line 31: 30
line 32: 36
line 33: 66
line 34: 70
line 36: 74
LocalVariableTable:
Start Length Slot Name Signature
0 75 0 this Lcom/hqa/design/test/Test;
70 4 1 e Ljava/lang/InterruptedException;
StackMapTable: number_of_entries = 2
frame_type = 247 /* same_locals_1_stack_item_frame_extended */
offset_delta = 69
stack = [ class java/lang/InterruptedException ]
frame_type = 4 /* same */
public void objLock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=4, locals=3, args_size=1
0: getstatic #13 // Field lock:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
9: new #26 // class java/lang/StringBuilder
12: dup
13: invokestatic #28 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
16: invokevirtual #34 // Method java/lang/Thread.getName:()Ljava/lang/String;
19: invokestatic #38 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
22: invokespecial #44 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
25: ldc #83 // String 进入objLock方法,休眠5s
27: invokevirtual #49 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
30: invokevirtual #53 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
33: invokevirtual #56 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: ldc2_w #61 // long 5000l
39: invokestatic #63 // Method java/lang/Thread.sleep:(J)V
42: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
45: new #26 // class java/lang/StringBuilder
48: dup
49: invokestatic #28 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
52: invokevirtual #34 // Method java/lang/Thread.getName:()Ljava/lang/String;
55: invokestatic #38 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
58: invokespecial #44 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
61: ldc #85 // String 退出objLock方法
63: invokevirtual #49 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
66: invokevirtual #53 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
69: invokevirtual #56 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
72: goto 80
75: astore_2
76: aload_2
77: invokevirtual #69 // Method java/lang/InterruptedException.printStackTrace:()V
80: aload_1
81: monitorexit
82: goto 88
85: aload_1
86: monitorexit
87: athrow
88: return
Exception table:
from to target type
6 72 75 Class java/lang/InterruptedException
6 82 85 any
85 87 85 any
LineNumberTable:
line 42: 0
line 44: 6
line 45: 36
line 46: 42
line 47: 72
line 48: 76
line 42: 80
line 51: 88
LocalVariableTable:
Start Length Slot Name Signature
0 89 0 this Lcom/hqa/design/test/Test;
76 4 2 e Ljava/lang/InterruptedException;
StackMapTable: number_of_entries = 4
frame_type = 255 /* full_frame */
offset_delta = 75
locals = [ class com/hqa/design/test/Test, class java/lang/Object ]
stack = [ class java/lang/InterruptedException ]
frame_type = 4 /* same */
frame_type = 68 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 2
}
SourceFile: "Test.java"
参考上述文件,我们可以看到关键部分:
修饰方法:
public static synchronized void classLock();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, `ACC_SYNCHRONIZED`
修饰对象代码块:
public void objLock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=4, locals=3, args_size=1
0: getstatic #13 // Field lock:Ljava/lang/Object;
3: dup
4: astore_1
5: `monitorenter`
6: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
9: new #26 // class java/lang/StringBuilder
.......
80: aload_1
81: `monitorexit`
82: goto 88
85: aload_1
86: `monitorexit`
从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
6.3小结
对于synchronized修饰方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用,monitorexit出现两次是因为即便方法发生异常,也要保证锁的正常释放。
7.关于锁的优化
7.1 锁消除
锁消除指的是在JVM即使编译时,通过运行少下文的扫描,去除不可能存在共享资源竞争的锁。
通过锁消除,可以节省毫无意义的锁请求.
比如在单线程下使用StringBuffer,其中的同步完全没有必要,这时候JVM可以在运行时基于逃逸分析计数,消除不必要的锁。
看以下代码片段:
public static String createStringBuffer(String str1, String str2) {
StringBuffer sBuf = new StringBuffer();
sBuf.append(str1);// append方法是同步操作
sBuf.append(str2);
return sBuf.toString();
这个静态方法内部的sBuf是每次都会new一个的,因此多线程调用此方法也不会有线程安全问题,因此,此时的append操作若是使用同步操作,就是白白浪费的系统资源。
我们可以通过编译器将其优化,将锁消除,前提是java必须运行在server模式(server模式会比client模式作更多的优化),同时必须开启逃逸分析:
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。
7.2 锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
看一个极端情况:
public void doSomethingMethod(){
synchronized(lock){
//do some thing
}
//这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
synchronized(lock){
//do other thing
}
这里面的代码会多次获取锁,其实这个是完全没有必要的,可以合并为:
public void doSomethingMethod(){
//进行锁粗化:整合成一次锁请求、同步、释放
synchronized(lock){
//do some thing
//做其它不需要同步但能很快执行完的工作
//do other thing
}
8.synchronized的可重入性
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
9.关于wait与notify
看一段代码:
package com.hqa.design.test;
public class Test {
/**
* 一把实例锁
*/
private final Object lock = new Object();
public Object getLock(){
return lock;
}
/**
* 加锁对象为lock
*/
public void lock1() {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + "进入lock1方法,休眠3s");
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + ",3s之后释放锁");
lock.wait();
lock.notifyAll();
Thread.sleep(7000);
System.out.println(Thread.currentThread().getName() + "退出lock方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 加锁对象为lock
*/
public void lock2() {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + "进入lock2方法,休眠10s");
Thread.sleep(10000);
lock.notifyAll();
System.out.println(Thread.currentThread().getName() + ",释放锁,退出lock2方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
Thread t1 = new Thread(() -> {
test.lock1();
});
t1.start();
Thread t2 = new Thread(() -> {
test.lock2();
});
t2.start();
}
}
输出:
Thread-0进入lock1方法,休眠3s
Thread-0,3s之后释放锁
Thread-1进入lock2方法,休眠10s
Thread-1,释放锁,退出lock2方法
Thread-0退出lock方法
lock对象为t1和t2所竞争,此处t1线程在执行过程中进行了await,此操作将会释放lock锁,而后通知其他所有线程再次抢锁,此时t2抢到锁,继续执行,完毕后再次通知其他线程执行。关于线程通信我们会独立出一个模块单独讲,篇幅有限,这章节就到此为止了。
参考链接:https://blog.csdn.net/javazejian/article/details/72828483