一、学习内容
- synchronized关键字介绍
- synchronized和Intrinsic Locks 关系
- synchronized用法和注意的问题,反编译观察JVM指令
- 程序死锁
- 总结
二、具体内容
在串行化的任务执行中,由于不存在资源的共享,线程安全问题当然不用考虑,但是串行化的任务执行效率低下,让人忍无可忍。多线程的执行任务应用而生,同时多线程的引入也带来了共享资源的安全问题。
- 什么是共享资源?望文生义一下,
共享资源:指的是多个线程对同一份资源进行访问(读写)被多个线程访问的资源被称为共享资源,如何保证多个线程访问到的数据是一致的(最新的),则被数据同步或资源同步。
2.1 synchronized关键字
- 通过查看JDK官网现在说一下Intrinsic Locks and Synchronization 关系?JDK官网描述
同步是围绕一个内部实体建立的,称为内部锁或监视器锁。(api规范经常将这个实体简单地称为“监视器”)内在锁在同步的两个方面都扮演着角色:强制对对象状态的独占访问和在对可见性至关重要的关系之前建立事件。
每个对象都有一个与之相关的内在锁。按照惯例,需要对对象的字段进行独占和一致访问的线程必须在访问对象之前获取该对象的内锁,然后在完成后释放内锁。一个线程被称为拥有从获得锁到释放锁之间的内在锁。只要线程拥有固有锁,就没有其他线程能够获得相同的锁。另一个线程在试图获取锁时会堵塞。
当线程释放一个内在锁时,在该操作与随后获得同一锁之间建立关系之前发生了一次事件。
synchronized关键字特点具体表现如下:
- synchronized关键字提供了一种锁机制,能够确保共享变量的互斥访问,从而防止数据不一致问题
- synchronized关键字包括monitor enter 和monitor exit 两个JVM指令,它能够保证在任何时候任何线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功之后,共享变量会被刷新到主内存
- synchronized的指令严格遵守Java happens-before原则,一个monitor exit指令之前必须要有一个monitor enter指令。
一个机制、一个原则、两个指令,112。
2.1.1 synchronized关键字的用法
同步方法定义
public synchronized void sync(){ }
public synchronized static void staticSync(){ }
同步代码块定义
private final Object MUTEX = new Object();
public void sync(){
synchronized(MUTEX){
}
}
实现一下之前的叫号程序:
public class TicketWindow implements Runnable {
//最多受理业务数
public static final int MAX = 50;
private int index = 1;
private final static Object MUTEX = new Object();
@Override
public void run(){
synchronized(MUTEX){
while(index <= MAX){
System.out.println(Thread.currentThread()+" 当前处理的号码是: " + (this.index++));
}
}
}
public static void main(String args[]){
final TicketWindow task = new TicketWindow();
Thread wt1 = new Thread(task , "窗口一");
Thread wt2 = new Thread(task , "窗口二");
Thread wt3 = new Thread(task , "窗口三");
Thread wt4 = new Thread(task , "窗口四");
wt1.start();
wt2.start();
wt3.start();
wt4.start();
}
}
2.1.2 使用synchronized要注意的问题
1.与monitor关联的对象不能为空
public final Object mutex = null;
public void syncMethod(){
synchronized(mutex){
}
}
2.synchronized作用域太大
synchronized关键字存在排他性,也就是说所有的线程必须串行地经过synchronized保护的共享区域(我的地盘我做主),如果synchronized作用域过大,则执行效率越低,并发优势也就丧失了。synchronized关键字应该尽可能的作用于共享资源(数据)的读写作用域。
public static class Task implements Runnable{
public synchronized void run(){
}
}
3.不同的monitor企图锁不同的方法
public static class Task implements Runnable{
private final Object MUTEX = new Object();
@Override
public void run(){
//...
synchronized(MUTEX){
//...
}
//...
}
}
public static void main(String args[]){
for(int i = 1; i < 3; i++){
new Thread(Task :: new).start();
}
}
- 构建三个线程,同时构造了3个Runnable实例,Runnable作为线程逻辑执行单元传递给Thread,然而我们将发现synchronized互斥不了与之对应的作用域,线程之间进行monitor lock的争抢只能发生在于monitor关联的同一个引用上,上面的代码每一个线程争抢的monitor关联引用都是独立的,因此不可能起到互斥的作用。
简单的线程堆栈运行信息观察
public class TestThread01 {
private final static Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
synchronized (LOCK) {
try {
Thread.sleep(300_000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable);
t1.start();
t2.start();
t3.start();
}
}
打开JDK自带的JConsole
进来之后是这样的
锁的抢占可以看到,队长额跟踪信息也有。我们可以到dos窗口中打印堆栈信息。
"Thread-2" #13 prio=5 os_prio=0 tid=0x000000001b3c7800 nid=0x2ea8 waiting for monitor entry [0x000000001bf0f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.thread.basicmethod.TestThread01.lambda$main$0(TestThread01.java:23)
- waiting to lock <0x00000000d7e8ad10> (a java.lang.Object)
at com.thread.basicmethod.TestThread01$$Lambda$1/558638686.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001b3c6800 nid=0x53c waiting for monitor entry [0x000000001be0f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.thread.basicmethod.TestThread01.lambda$main$0(TestThread01.java:23)
- waiting to lock <0x00000000d7e8ad10> (a java.lang.Object)
at com.thread.basicmethod.TestThread01$$Lambda$1/558638686.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"Thread-0" #11 prio=5 os_prio=0 tid=0x000000001b3c5000 nid=0x1be0 waiting on condition [0x000000001bd0e000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.thread.basicmethod.TestThread01.lambda$main$0(TestThread01.java:23)
- locked <0x00000000d7e8ad10> (a java.lang.Object)
at com.thread.basicmethod.TestThread01$$Lambda$1/558638686.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Thread-0持有monitor<0x00000000d7e8ad10>的锁并且处于休眠状态中,那么其他线程无法进入acessResource方法。
接下来使用JDK命令 javap 对 LOCK class 进行反 编译,输出 JVM指令,
注意要打开物理文件的绝对路径:我的class路径
E:\ideafile\ThreadProject\out\production\ThreadProject\com\thread\basicmethod
使用命令:javap -c TestThread01.class
我们就可以看到命令窗口出现的一大堆东西,而且monitor enter 和monitor exit 是成对出现的。
信息打印如下
Compiled from "TestThread01.java"
public class com.thread.basicmethod.TestThread01 {
public com.thread.basicmethod.TestThread01();
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[]) throws java.lang.InterruptedException;
Code:
0: new #9 // class com/thread/basicmethod/TestThread01
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:(Lcom/thread/basicmethod/TestThread01;)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
}
特别说明
public void accessResource();
Code:
0: getstatic #2 // Field MUTEX:Ljava/lang/Object; 1、获取MUTEX
3: dup
4: astore_1
5: monitorenter // 2、执行 monitorenter JVM指令
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 // 3、跳转到 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 // 4.
30: monitorexit // 5. 执行 monitorexit JVM指令
31: aload_3
32: athrow
33: return
对象监视器
(1)Monitorenter
每个对象都与一个monitor相关联,一个monitor的lock的锁只能被一个线程同一时间获得,在一个线程尝试获得与对象关联monitor的所有权时会发生如下:
- 如果monitor的计数器为0,则意味着monitor的lock还没有被获得,某个线程获得之后立即将计数器加1,从而该线程就是这个monitor的所有者了
- 如果一个已经拥有monitor所有权的线程重入,会导致计数器再次累加
- 如果monitor已经被其它线程所有,则其它线程尝试获取monitor锁时会陷入阻塞状态直到monitor计数器为0,才会再次尝试获取对monitor的所有权(不管你摔的多惨我都接着你)。
(2)Monitorexit
释放对monitor的所有权,想要释放就要曾经拥有(低调的前提是要能高调起来)。释放monitor所有权就是将计数器减1,如果计数器结果为0,那就意味着不再拥有对该monitor的所有权。
对象、对象的监视器、同步队列和执行线程之间的关系如图:
从图中我们可以看出,任意线程对Object(Object由synchronized保护)的访问,首先获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为Blocked。当访问Object的前驱(获得了锁的线程,,后继)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程。
2.2 程序死锁
- 交叉锁可导致程序出现死锁
线程A持有R1的锁等待获取R2的锁,线程B持有R2的锁等待获取R1的锁(操作系统学过肯定知道哲学家吃面问题),这种情况最容易导致程序发生死锁的问题。
- 内存不足
当并发请求系统可用内存时,如果此时系统内存不足,则可能会出现死锁的情况,
- 一问一答式的数据交换
服务端开启某个端口,等待客户端访问,客户端发送请求立即等待接收,由于某种原因服务端错过了客户端的请求(没有收到),而仍然在等待疑问一答式的数据交换,此时服务器和客户端都在等待对方发送数据
- 数据库锁
无论是数据库表级别的锁 ,还是行级别的锁,比如某个线程执行for update语句退出了事务,其它线程访问该数据库时陷入了死锁。
- 文件锁
某线程获得了文件锁意外退出,其它读取文件的线程也将会进入死锁知道系统释放文件句柄资源。
- 死循环引起的死锁
程序由于代码原因或者对异常处理不当,进入了死循环,虽然查看线程堆栈信息不会发现任何死锁的迹象,但是程序不工作,CPU占有率很高,这种死锁
称为系统假死,是一种最为致命也是最难排查的死锁现象。
代码示例
定义DeadLock类
package com.thread.basicmethod.chapter05;
/********************************
* @Author: kangna
* @Date: 2019/8/21 22:53
* @Version: 1.0
* @Desc: 线程死锁
********************************/
public class DeadLock {
private OtherService otherService;
public DeadLock(OtherService otherService) {
this.otherService = otherService;
}
private final Object lock = new Object();
public void m1() {
synchronized (lock) { // 我自己有一个锁
System.out.println("DeadLock---m1---");
otherService.s1(); // s1 方法中又有一个锁
}
}
public void m2() {
synchronized (lock) {
System.out.println("DeadLock---m2---");
}
}
}
定义OtherService
package com.thread.basicmethod.chapter05;
/********************************
* @Author: kangna
* @Date: 2019/8/21 22:55
* @Version: 1.0
* @Desc: 提供服务
********************************/
public class OtherService {
private final Object lock = new Object();
private DeadLock deadLock;
public void s1() {
synchronized (lock) {
System.out.println("OtherService---s1---");
}
}
public void s2() {
synchronized (lock) {
System.out.println("OtherService---s2---");
deadLock.m2();
}
}
public void setDeadLock(DeadLock deadLock) {
this.deadLock = deadLock;
}
}
测试类
public class DeadLockTest {
public static void main(String[] args) {
OtherService otherService = new OtherService();
DeadLock deadLock = new DeadLock(otherService);
otherService.setDeadLock(deadLock);
new Thread() {
@Override
public void run() {
while (true) {
deadLock.m1();
}
}
}.start();
new Thread() {
@Override
public void run() {
while (true) {
otherService.s2();
}
}
}.start();
}
}
然而运行是这样的
使用JVM命令,查看当前 程序的进程 号dos窗口进行
打印信息
你锁住的我想要,我在等;我锁住的你想要,你在等。(我的心在等待,永远在等待)
打印的信息如下:
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.181-b13 mixed mode):
"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x00000000027fe000 nid=0x1934 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001ab3e800 nid=0x58e4 waiting for monitor entry [0x000000001b7bf000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.thread.basicmethod.chapter05.DeadLock.m2(DeadLock.java:27)
- waiting to lock <0x00000000d5f8ab18> (a java.lang.Object)
at com.thread.basicmethod.chapter05.OtherService.s2(OtherService.java:24)
- locked <0x00000000d5f87990> (a java.lang.Object)
at com.thread.basicmethod.chapter05.DeadLockTest$2.run(DeadLockTest.java:26)
"Thread-0" #11 prio=5 os_prio=0 tid=0x000000001aaf5800 nid=0x43d4 waiting for monitor entry [0x000000001b6bf000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.thread.basicmethod.chapter05.OtherService.s1(OtherService.java:17)
- waiting to lock <0x00000000d5f87990> (a java.lang.Object)
at com.thread.basicmethod.chapter05.DeadLock.m1(DeadLock.java:21)
- locked <0x00000000d5f8ab18> (a java.lang.Object)
at com.thread.basicmethod.chapter05.DeadLockTest$1.run(DeadLockTest.java:18)
"Service Thread" #10 daemon prio=9 os_prio=0 tid=0x000000001aaa6800 nid=0x35f8 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000001aa9e800 nid=0x5be4 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000000001aa36000 nid=0x5cd8 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000000001aa33000 nid=0x5a80 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001aa06000 nid=0x54e4 runnable [0x000000001b0bf000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x00000000d6035168> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x00000000d6035168> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001961f000 nid=0x5824 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001a9d0800 nid=0x4290 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000000004a9a800 nid=0xfcc in Object.wait() [0x000000001a95f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d5e08ed0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x00000000d5e08ed0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000004a92000 nid=0x55f0 in Object.wait() [0x000000001a85f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d5e06bf8> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x00000000d5e06bf8> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=2 tid=0x00000000195d7800 nid=0x2ce4 runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00000000049b8000 nid=0x13f8 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00000000049b9800 nid=0x49f4 runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00000000049bb000 nid=0x4b30 runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00000000049bc800 nid=0x42c0 runnable
"VM Periodic Task Thread" os_prio=2 tid=0x000000001aaf3000 nid=0x5c1c waiting on condition
JNI global references: 12
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000004a97c58 (object 0x00000000d5f8ab18, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000000004a9a6f8 (object 0x00000000d5f87990, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.thread.basicmethod.chapter05.DeadLock.m2(DeadLock.java:27)
- waiting to lock <0x00000000d5f8ab18> (a java.lang.Object)
at com.thread.basicmethod.chapter05.OtherService.s2(OtherService.java:24)
- locked <0x00000000d5f87990> (a java.lang.Object)
at com.thread.basicmethod.chapter05.DeadLockTest$2.run(DeadLockTest.java:26)
"Thread-0":
at com.thread.basicmethod.chapter05.OtherService.s1(OtherService.java:17)
- waiting to lock <0x00000000d5f87990> (a java.lang.Object)
at com.thread.basicmethod.chapter05.DeadLock.m1(DeadLock.java:21)
- locked <0x00000000d5f8ab18> (a java.lang.Object)
at com.thread.basicmethod.chapter05.DeadLockTest$1.run(DeadLockTest.java:18)
Found 1 deadlock.
3 总结
- synchronized 一个机制,一个原则,二个指令,源码分析
- 多线程任务执行,堆栈信息分析工具JConsole
- 程序死锁介绍,死锁使用 (jstack 进程号)