看书的时候,看到这里,觉得有必要记录一下,那就顺手写一下。
先看一下 synchronized 的官方解释的翻译:
synchronized 关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么该对象的所有读或者写都将通过同步的方式来进行。
- synchronized 提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致问题的出现。
- synchronized 包括 monitor enter 和 monitor exit 两个 JVM 指令,它能够保证在任何时候任何线程执行到 monitor enter 成功之前都必须从主内存中获取数据,而不是在缓存中,在 monitor exit 运行成功之后,共享变量被更新后的值必须刷入主存
- synchronized 的指令严格遵守Java happens-before 规则,一个 monitor exit 指令之前必定要有一个 monitor enter
用法的话就不多说了,两种用法,同步方法和同步代码块,可以看别的帖子了解一下。
主要研究一下 synchronized 的原理。
synchronized 提供了一种互斥机制,就是同一时刻,只能有一个线程访问同步资源。就是某个线程获取了与 synchronized(mutex) 中 mutex 关联的 monitor 锁。
先写一个简单的例子,如下
import java.util.concurrent.TimeUnit;
public class Main{
private final static Object MUTEX = new Object();
public static void main(String[] args) {
final Main main = new Main();
for (int i = 0; i < 5; i++) {
new Thread(main::accessResource).start();
}
}
public void accessResource() {
synchronized (MUTEX) {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上边的代码定义了一个 accessResource ,并且使用了同步代码块的方式对其进行了同步,同时定义五个线程调用此方法,前边说了,使用 synchronized 关键字同步了代码块就会有互斥,所以只能有一个线程获取了 mutex monnitor 锁,其他线程只能进入阻塞状态,等待获取到 mutex monitor 锁的线程把它进行释放,先运行这个示例程序,然后打开 JConsole 工具对其进行监控,如下图
选择好本地的进程,然后连接就行,再切换到线程。
看到这里这个 thread-0 在 TIMED_WAITING 状态,而另外那四个线程都是 BLOCKED 状态。
再使用 jstack 命令打印进程的线程堆栈信息。
可以看到 Thread-2 持有 monitor<0x00000000d80302f0> 的锁并且处于休眠状态中,那么其他线程就没办法进入 accessResourse 方法中。其他几个线程进入 BLOCKED 状态并且等待着获取 monitor<0x00000000d80302f0> 的锁。
然后这里使用 JDK 的 javap 对这个类反汇编一下,输出 JVM 指令,指令如下:
public class test.Main {
public test.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class test/Main
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: iconst_0
9: istore_2
10: iload_2
11: iconst_5
12: if_icmpge 42
15: new #4 // class java/lang/Thread
18: dup
19: aload_1
20: dup
21: invokevirtual #5 // Method java/lang/Object.getClass:()Ljava/lang/Class;
24: pop
25: invokedynamic #6, 0 // InvokeDynamic #0:run:(Ltest/Main;)Ljava/lang/Runnable;
30: invokespecial #7 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
33: invokevirtual #8 // Method java/lang/Thread.start:()V
36: iinc 2, 1
39: goto 10
42: return
public void accessResource();
Code:
0: getstatic //这里获取到 MUTEX #9 // Field MUTEX:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter //执行 monitorenter 指令
6: getstatic //获取静态属性 #10 // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
9: ldc2_w #11 // long 10l
12: invokevirtual #13 // Method java/util/concurrent/TimeUnit.sleep:(J)V
15: goto //休眠之后跳转到23行 23
18: astore_2
19: aload_2
20: invokevirtual #15 // Method java/lang/InterruptedException.printStackTrace:()V
23: aload_1 //跳转到这里
24: monitorexit //执行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
static {};
Code:
0: new #16 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: putstatic #9 // Field MUTEX:Ljava/lang/Object;
10: return
}
现在就可以发现 monitor enter 和 monitor exit 指令了,可以看到每一个 monitor exit 之前必有对应的 monitor enter 。
Monitorenter
每个对象都与一个 monitor 相关联,每一个 monitor 的 lock 锁只能被一个线程在同一时间获得,在一个线程尝试获得与对象关联 monitor 的所有权时会发生如下的几个情况。
- 如果 monitor 的计数器为0,则意味着该 monetor 的 lock 还没有被获得,某个线程获得之后立即对该计数器加一,意味着这个线程就是这个 monitor 的持有者了。
- 如果一个已经拥有该 monitor 所有权的线程重入,则会导致 monitor 计数器再次累加。
- 如果 monitor 已经被其他线程所拥有,则其他线程尝试获取该 monitor 的所有权时,就会被陷入到阻塞状态,直到 monitor 计数器变为0,才能再次尝试获取对 monitor 的所有权。
Monitorexit
释放对 monitor 的所有权,想要释放对某个对象关联的 monitor 的所有权的前提是,曾经获得了所有权。释放的过程比较简单,就是将 monitor 的计数器减一,如果计数器的结果为0,那就意味着该线程不再拥有对该 monitor 的所有权,通俗地讲就是解锁。与此同时被该 monitor block 的线程讲再次尝试获得对该 monitor 的所有权。
以上就是 synchronized 的一个简单原理了,接着再看一下 This Monitor 和 Class Monitor 的一个比较详细的解释。
this monitor
下面看个例子
import java.util.concurrent.TimeUnit;
/**
* @author hasaki_w_c
*/
public class ThisMonitor{
public static void main(String[] args) {
ThisMonitor thisMonitor = new ThisMonitor();
new Thread(thisMonitor::method1, "thread1").start();
new Thread(thisMonitor::method2, "thread2").start();
}
public synchronized void method1() {
System.out.println(Thread.currentThread().getName() + "method1");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void method2() {
System.out.println(Thread.currentThread().getName() + "method2");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
比如这个代码中,两个 method 方法都被 synchronized 关键字修饰,启动两个线程分别访问这两个方法,我们看一下这两个方法对应的 monitor 是什么,是不是一致的。
运行一下程序,看一下输出,只输出了一个,另一个没有调用。
去看一下线程的堆栈信息,用 jstack pid 命令看一下子,可以看到 thread1 获取了<0x00000000d618a428> monitor 的 lock 并处于休眠状态,而 thread2 线程想要获取到 <0x00000000d618a428>monitor 的 lock 时,陷入了 BLOCKED 状态。
由此可见,使用 synchronized 关键字同步类的不同示例方法,竞争的是同一个 monitor 的锁,而与之关联的则是 ThisMonitor 的实例引用,我们可以讲上边这个代码改一下,把 method2 的同步方法改成使用 this 的同步代码块。
import java.util.concurrent.TimeUnit;
/**
* @author hasaki_w_c
*/
public class ThisMonitor{
public static void main(String[] args) {
ThisMonitor thisMonitor = new ThisMonitor();
new Thread(thisMonitor::method1, "thread1").start();
new Thread(thisMonitor::method2, "thread2").start();
}
public synchronized void method1() {
System.out.println(Thread.currentThread().getName() + "method1");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method2() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "method2");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
然后运行一下,发现与前边同步方法的结果是一样的,所以便证实了而与之关联的则是 ThisMonitor 的实例引用。下面再来看看 class monitor。
class monitor
和上边方法一样,代码稍微改一下,如下:
import java.util.concurrent.TimeUnit;
/**
* @author hasaki_w_c
*/
public class ClassMonitor {
public static void main(String[] args) {
new Thread(ClassMonitor::method1, "thread1").start();
new Thread(ClassMonitor::method2, "thread2").start();
}
public static synchronized void method1() {
System.out.println(Thread.currentThread().getName() + "method1");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void method2() {
System.out.println(Thread.currentThread().getName() + "method2");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果也与上边一样,同一时刻也是只能有一个线程访问 ClassMonitor 的静态方法,再看看线程堆栈信息。
thread1 线程持有 <0x00000000d6187b88> monitor 的锁,正在休眠,而 thread2线程试图获取<0x00000000d6187b88> monitor锁的时候陷入 BLOCKED状态。因此用 synchronized 同步某个类的不同静态方法争抢的也是同一个 monitor 的 lock,与前边this monitor不同的地方就在与锁后边是a java.lang.Class for xxx ,所以与该 monitor 关联的是 ClassMonitor.class 实例。
与上边同样修改一下代码
import java.util.concurrent.TimeUnit;
/**
* @author hasaki_w_c
*/
public class ClassMonitor {
public static void main(String[] args) {
new Thread(ClassMonitor::method1, "thread1").start();
new Thread(ClassMonitor::method2, "thread2").start();
}
public static synchronized void method1() {
System.out.println(Thread.currentThread().getName() + "method1");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void method2() {
synchronized (ClassMonitor.class) {
System.out.println(Thread.currentThread().getName() + "method2");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这个使用的就是ClassMonitor.class 的实例引用作为 monitor 。
以上就是我最近看书学到的一些关于 synchronized 的东西,在此做一记录。