文章目录
1 线程堆栈分析
synchronized关键字提供了一种互斥机制,也就是说在同一时刻,只能有一个线程访问同步资源,很多资料、书籍将synchronized(mutex: 互斥锁)称为锁,其实这种说法是不严谨的,准确地讲应该是某线程获取了与mutex关联的monitor锁
public class Mutex {
private final static Object MUTEX = new Object();
/**
* 模拟访问共享资源
*/
public void accessResource() {
synchronized (MUTEX) {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final Mutex mutex = new Mutex();
// 创建5个线程
for (int i = 0; i < 5; i++) {
new Thread(mutex::accessResource).start();
}
}
}
上面的代码创建了5个线程,当其中一个线程抢到执行权之后,就会持有锁,进入休眠,不会释放锁,所以剩下的线程都会被block住,等待获取锁。
使用jconsle工具看一下:
选中要建立连接的本地进程,然后点击【连接】按钮进入JConsole控制台,将tab切换至【线程】
随便选中程序中创建的某个线程,会发现只有一个线程在TIMED_WAITING(sleeping)状态,其他线程都进入了BLOCKED状态:
使用jstack命令打印进程的线程堆栈信息,选取其中几处关键的地方对其进行分析:
- Thread0持有monitor<0x000000076baecde8>的锁并且处于休眠状态中,那么其他线程将会无法进入accessResource方法
- Thread-1等剩下4个线程进入BLOCKED状态并且等待着获取monitor<0x000000076baecde8>的锁
2 JVM指令分析
使用JDK命令javap对Mutex class进行反汇编,输出了大量的JVM指令,在这些指令中,你将发现monitor enter和monitor exit是成对出现的(有些时候会出现一个monitor enter多个monitor exit,但是每一个monitor exit之前必有对应的monitor enter,这是肯定的)。
javap -c .\Mutex.class
结果
public class study.wyy.thread.package01.Mutex {
public study.wyy.thread.package01.Mutex();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void accessResource();
Code:
0: getstatic #2 // Field MUTEX:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: getstatic #3 // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
9: ldc2_w #4 // long 10l
12: invokevirtual #6 // Method java/util/concurrent/TimeUnit.sleep:(J)V
15: goto 23
18: astore_2
19: aload_2
20: invokevirtual #8 // Method java/lang/InterruptedException.printStackTrace:()V
23: aload_1
24: monitorexit
25: goto 33
28: astore_3
29: aload_1
30: monitorexit
31: aload_3
32: athrow
33: return
Exception table:
from to target type
6 15 18 Class java/lang/InterruptedException
6 25 28 any
28 31 28 any
public static void main(java.lang.String[]);
Code:
0: new #9 // class study/wyy/thread/package01/Mutex
3: dup
4: invokespecial #10 // Method "<init>":()V
7: astore_1
8: iconst_0
9: istore_2
10: iload_2
11: iconst_5
12: if_icmpge 42
15: new #11 // class java/lang/Thread
18: dup
19: aload_1
20: dup
21: invokevirtual #12 // Method java/lang/Object.getClass:()Ljava/lang/Class;
24: pop
25: invokedynamic #13, 0 // InvokeDynamic #0:run:(Lstudy/wyy/thread/package01/Mutex;)Ljava/lang/Runnable;
30: invokespecial #14 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
33: invokevirtual #15 // Method java/lang/Thread.start:()V
36: iinc 2, 1
39: goto 10
42: return
static {};
Code:
0: new #16 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: putstatic #2 // Field MUTEX:Ljava/lang/Object;
10: return
}
2.1 Monitorenter
每个对象都与一个monitor相关联,一个monitor的lock的锁只能被一个线程在同一时间获得,在一个线程尝试获得与对象关联monitor的所有权时会发生如下的几件事情:
- 如果monitor的计数器为0,则意味着该monitor的lock还没有被获得,某个线程获得之后将立即对该计数器加一,从此该线程就是这个monitor的所有者了
- 如果一个已经拥有该monitor所有权的线程重入,则会导致monitor计数器再次累加。
- 如果monitor已经被其他线程所拥有,则其他线程尝试获取该monitor的所有权时,会被陷入阻塞状态直到monitor计数器变为0,才能再次尝试获取对monitor的所有权。
2.2 Monitorexit
释放对monitor的所有权,想要释放对某个对象关联的monitor的所有权的前提是,你曾经获得了所有权。释放monitor所有权的过程比较简单,就是将monitor的计数器减一,如果计数器的结果为0,那就意味着该线程不再拥有对该monitor的所有权,通俗地讲就是解锁。与此同时被该monitor block的线程将再次尝试获得对该monitor的所有权。
3 使用synchronized需要注意的问题
- 与monitor关联的对象不能为空
private final static Object MUTEX = null;
Mutex为null,很多人还是会犯这么简单的错误,每一个对象和一个monitor关联,对象都为null了,monitor肯定无从谈起。
-
synchronized作用域太大
由于synchronized关键字存在排他性,也就是说所有的线程必须串行地经过synchronized保护的共享区域,如果synchronized作用域越大,则代表着其效率越低,甚至还会丧失并发的优势。synchronized关键字应该尽可能地只作用于共享资源(数据)的读写作用域 -
不同的monitor企图锁相同的方法,就是说如果多个线程想要解决线程安全问题,必须使用的是同一把锁
下面这个就是个错误示例:
public class Mutex extends Thread{
private final static Object MUTEX = new Object();
@Override
public void run() {
accessResource();
}
public void accessResource() {
synchronized (MUTEX) {
// do something
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
// 这里每次都是new,所以线程内部的MUTEX都是不同线程实例的属性
// 也就是说这5个线程是使用的不是同一个monitor
new Mutex().start();
}
}
}
- 多个锁的交叉导致死锁
多个锁的交叉很容易引起线程出现死锁的情况,程序并没有任何错误输出,但就是不工作
4 This Monitor和Class Monitor的详细介绍
前面提过:
同步方法的monitor:this的monitor
同步静态方法的monitor:就是这个方法所在类的class对象的monitor
这里就研究一下下这两个monitor。
4.1 This Monitor
4.1.1 验证同步方法的monitor是this的monitor
public class ThisMonitor {
public synchronized void method1() {
System.out.println(currentThread().getName() + " enter to method1");
// 这里就简单模拟
while (true){
}
}
public void method2() {
synchronized (this) {
System.out.println(currentThread().getName() + " enter to method2");
// 这里就简单模拟
while (true){
}
}
}
public static void main(String[] args) {
ThisMonitor thisMonitor = new ThisMonitor();
new Thread(thisMonitor::method1, "T1").start();
new Thread(thisMonitor::method2, "T2").start();
}
}
创建了两个线程,每个线程内部都加了同步,一个是同步方法,一个是同步代码块显式的指明了使用this。
运行就会发现,始终只会打印T1 enter to method1 或者 T2 enter to method2,因为这两个是同一个monitor
只要一个抢到了,接下来就进入死循环,一直持有锁,所以导致另一个无法抢到锁执行代码。
但是如果改一下:
public class ThisMonitor {
// 改成静态方法,静态方法使用的就是当前类的class对象
public static synchronized void method1() {
System.out.println(currentThread().getName() + " enter to method1");
while (true){
}
}
public void method2() {
synchronized (this) {
System.out.println(currentThread().getName() + " enter to method2");
while (true){}
}
}
public static void main(String[] args) {
ThisMonitor thisMonitor = new ThisMonitor();
new Thread(ThisMonitor::method1, "T1").start();
new Thread(thisMonitor::method2, "T2").start();
}
}
发现输出:
T1 enter to method1
T2 enter to method2
这两个方法都会执行,因为method1我已经改成静态了,此时这两个线程已经不在使用的是同一个monitor了,导致同步代码失效。
足以说明同步方法使用的是this monitor
4.1.2 验证一下同步静态方法的monitor是class monitor
public class ClassMonitor{
public static synchronized void method1()
{
System.out.println(currentThread().getName() + " enter to method1");
while (true){
}
}
public static void method2()
{
synchronized (ClassMonitor.class)
{
System.out.println(currentThread().getName() + " enter to method2");
while (true){
}
}
}
public static void main(String[] args)
{
new Thread(ClassMonitor::method1, "T1").start();
new Thread(ClassMonitor::method2, "T2").start();
}
}
运行就会发现,始终只会打印T1 enter to method1 或者 T2 enter to method2,因为这两个是同一个monitor(当前类的class对象的monitor)
只要一个抢到了,接下来就进入死循环,一直持有锁,所以导致另一个无法抢到锁执行代码。