Java高并发编程详解系列-四
在多线程中最为复杂和最为重要的就是线程安全。多个线程访问同一个对象的时候会导致线程安全问题。通过加锁可以避免这种问题。但是在串行执行的过程中又不用考虑线程安全问题,而使用串行程序效率低没有办法将CPU的利用率提升到最大。所以还要使用多线程并行执行,既然提到了多线程就必须面对线程安全问题。
共享资源
在之前的博客中曾经提到过一个问题,就是JVM的内存模型,在内存模型中我们知道除了堆内存和方法区内存是被所有线程共同使用的,其他的程序计数器、JVM虚拟栈、本地方法栈等等资源都是每个线程独立的资源。那么什么叫做共享资源呢?从这个角度上看看堆内存和方法区内存可以被称为是所有线程共享的资源。也就是说共享资源是能同时被多个线程能共同操作资源。但是一个新的问题就来了,怎么保证多线程访问共享资源的数据一致性问题。解决这个问题就被称为是数据同步或者说是数据共享。
数据同步
数据一致性
在系列博客一中,模拟了一个火车售票系统。设置一共有50张车票。会发现最后在控制台输出的内容其实并不是我们希望看到的结果。
public class TicketRunable implements Runnable {
private int index = 1;
private final static int MAX = 50;
@Override
public void run() {
while (index<=MAX){
System.out.println(Thread.currentThread()+" 的号码是 :"+index++);
}
}
public static void main(String[] args) {
final TicketRunable ticketRunable = new TicketRunable();
Thread thread1 = new Thread(ticketRunable,"一");
Thread thread2 = new Thread(ticketRunable,"二");
Thread thread3 = new Thread(ticketRunable,"三");
Thread thread4 = new Thread(ticketRunable,"四");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
运行上面的这段代码我们会发现,每次运行的结果都是不一样的,但是总结一下主要是由三个问题
- 第一,在程序中某一个编号从来没有出现过
- 第二,在运行过程中有些号码重复出现
- 第三,到程序结束的时候会出现一些号码超过50这个最大值。
数据不一致的原因分析
有些号码没有被显示
在多线程运行过程中如果A线程和B线程同时执行,此时A线程开始获得index=34,如果这个时候线程B已经获取到了index=35,这个时候CPU将线程调度权交给了线程A这个时候线程A获取到的index的值就是35,直接进行加一操作之后直接输出,这样的话就会导致34这个号码被忽略了。
号码重复出现
线程A执行index+1的操作,然后CPU将执行权交给了线程B,而这个时候线程A对于index的赋值操作并没有结束,而线程B拿到的index还是原来的值,然后执行index+1的操作,而在此时线程A获取到了CPU的调度权,这个时候继续执行index+1的操作,就会导致某个值重复出现
超出范围
在线程A快要结束的时候获取到了index+1的操作此时,线程B也到了最后结束的时候,但是两个线程都拿到了满足条件的49,这个时候线程A先获取CPU资源进行index+1操作,而操作完成之后线程B也获得了资源,这个时候index的值已经是50了,但是由于判断的时候进入index+1操作的之前index是49此时A线程将其改为50并不会影响到对于值的判断,所以线程B就会在50的基础上继续执行index+1的操作。就会出现51。
那么怎么解决这个问题呢?在JDK1.5之前Java都是通过Synchronized关键字来解决这些问题的。Synchronized关键字提供了一种排他的机制,也就是说在同一时间内只能有一个线程进行一个基本的操作。
Synchronized关键字
Synchronized关键字实现了防止内存一致性的错误。如果一个对象被多个线程访问,那么这个时候就需要实现对于对象的读写操作都是以同步的方式来实现的。
- synchronized提供一个一种锁定机制,保证共享变量在多个线程访问的时候多个线程是互斥的。防止了数据一致性的问题。
- synchronized 关键字包括monitor enter和monitor exit两个Java虚拟机指令,保证在任何时候线程执行了monitor enter指令成功之前都是从主内存中获取数据,而不是从缓存中获取数据,对于操作系统的内存模型到后面在进行细致说明。当执行了monitor exit命令之后共享变量的值必须被刷到主内存中。
- synchronized关键字严格遵守happens-before原则,也就说上面提到的两个命令必须是同时存在才可以。
怎样使用synchronized关键字
方法加锁操作
对于方法的同步来说,就是在方法上加上synchronized关键字
代码块加锁操作
Java中有时候需要使用代码块进行操作,在代码块之前加上synchronized,就表示代码块加锁
public class TicketWindows implements Runnable {
private int index = 1;
private final static int MAX = 500;
private final static Object MUTEX = new Object();
@Override
public void run() {
synchronized (MUTEX) {
while (index <= MAX) {
System.out.println(Thread.currentThread() + " 的号码是 :" + index++);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
final TicketWindows ticketRunable = new TicketWindows();
Thread thread1 = new Thread(ticketRunable, "一");
Thread thread2 = new Thread(ticketRunable, "二");
Thread thread3 = new Thread(ticketRunable, "三");
Thread thread4 = new Thread(ticketRunable, "四");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
运行上面代码之后会发现每隔一秒钟输出一条数据。但是代码的结果引起了我的注意。并没有出现多个线程争抢的打印的效果。因为synchronized根本没有互斥锁定对应的作用域,线程之间进行lock的争抢只能发生在于monitor关联的同一个引用上,上面代码中每个线程争抢的monitor关联的引用都是彼此独立。所以不可能出现互斥的操作。
问题分析
F:\developersrc\GIT\JavaHighConcurrency\JavaHC\target\classes\com\example\charp04>javap -c TicketWindows.class
Compiled from "TicketWindows.java"
public class com.example.charp04.TicketWindows implements java.lang.Runnable {
public com.example.charp04.TicketWindows();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field index:I
9: return
public void run();
Code:
0: getstatic #3 // Field MUTEX:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: aload_0
7: getfield #2 // Field index:I
10: sipush 500
13: if_icmpgt 77
16: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
19: new #6 // class java/lang/StringBuilder
22: dup
23: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
26: invokestatic #8 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
29: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
32: ldc #10 // String 的号码是 :
34: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
37: aload_0
38: dup
39: getfield #2 // Field index:I
42: dup_x1
43: iconst_1
44: iadd
45: putfield #2 // Field index:I
48: invokevirtual #12 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
51: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
54: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
57: getstatic #15 // Field java/util/concurrent/TimeUnit.SECONDS:Ljava/util/concurrent/TimeUnit;
60: ldc2_w #16 // long 10l
63: invokevirtual #18 // Method java/util/concurrent/TimeUnit.sleep:(J)V
66: goto 6
69: astore_2
70: aload_2
71: invokevirtual #20 // Method java/lang/InterruptedException.printStackTrace:()V
74: goto 6
77: aload_1
78: monitorexit
79: goto 87
82: astore_3
83: aload_1
84: monitorexit
85: aload_3
86: athrow
87: return
Exception table:
from to target type
57 66 69 Class java/lang/InterruptedException
6 79 82 any
82 85 82 any
public static void main(java.lang.String[]);
Code:
0: new #4 // class com/example/charp04/TicketWindows
3: dup
4: invokespecial #21 // Method "<init>":()V
7: astore_1
8: new #22 // class java/lang/Thread
11: dup
12: aload_1
13: ldc #23 // String 一
15: invokespecial #24 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V
18: astore_2
19: new #22 // class java/lang/Thread
22: dup
23: aload_1
24: ldc #25 // String 二
26: invokespecial #24 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V
29: astore_3
30: new #22 // class java/lang/Thread
33: dup
34: aload_1
35: ldc #26 // String 三
37: invokespecial #24 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V
40: astore 4
42: new #22 // class java/lang/Thread
45: dup
46: aload_1
47: ldc #27 // String 四
49: invokespecial #24 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V
52: astore 5
54: aload_2
55: invokevirtual #28 // Method java/lang/Thread.start:()V
58: aload_3
59: invokevirtual #28 // Method java/lang/Thread.start:()V
62: aload 4
64: invokevirtual #28 // Method java/lang/Thread.start:()V
67: aload 5
69: invokevirtual #28 // Method java/lang/Thread.start:()V
72: return
static {};
Code:
0: new #29 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: putstatic #3 // Field MUTEX:Ljava/lang/Object;
10: return
}
"四" #15 prio=5 os_prio=0 tid=0x000000001a701000 nid=0x278c waiting for monitor entry [0x000000001b69f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.charp04.TicketWindows.run(TicketWindows.java:20)
- waiting to lock <0x00000000d7e302f0> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
"三" #14 prio=5 os_prio=0 tid=0x000000001a700800 nid=0x4410 waiting for monitor entry [0x000000001b59f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.charp04.TicketWindows.run(TicketWindows.java:20)
- waiting to lock <0x00000000d7e302f0> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
"二" #13 prio=5 os_prio=0 tid=0x000000001a700000 nid=0xb24 waiting for monitor entry [0x000000001b49f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.charp04.TicketWindows.run(TicketWindows.java:20)
- waiting to lock <0x00000000d7e302f0> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
"一" #12 prio=5 os_prio=0 tid=0x000000001a6fd000 nid=0x1cac waiting on condition [0x000000001b39f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.example.charp04.TicketWindows.run(TicketWindows.java:23)
- locked <0x00000000d7e302f0> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
线程内存分析
synchronized关键字提供了一种互斥锁定机制,也就是说在同一时间内只能有一个线程访问共享资资源
public class Mutex {
private final static Object MUTEX = new Object();
public void doSource(){
synchronized (MUTEX){
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final Mutex mutex = new Mutex();
for (int i = 0; i < 5; i++) {
new Thread(mutex::doSource).start();
}
}
}
运行上面的程序使用Jconsole连接产看线程信息可以看到只有一个线程是处于TIMED_WAITING(Sleep)状态,其他的线程都是在BLOCKED状态。使用jstack -l pid名查看对应的栈信息会发现
D:\jdk1.8\bin>jstack -l 2312
2019-05-02 21:41:17
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.152-b16 mixed mode):
"DestroyJavaVM" #17 prio=5 os_prio=0 tid=0x0000000002b62800 nid=0x23e4 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Thread-4" #16 prio=5 os_prio=0 tid=0x000000001a9d1000 nid=0x40a4 waiting for monitor entry [0x000000001b67f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.charp04.Mutex.doSource(Mutex.java:18)
- waiting to lock <0x00000000d5f1d5c8> (a java.lang.Object)
at com.example.charp04.Mutex$$Lambda$1/990368553.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
"Thread-3" #15 prio=5 os_prio=0 tid=0x000000001a9d0800 nid=0xb3c waiting for monitor entry [0x000000001b57f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.charp04.Mutex.doSource(Mutex.java:18)
- waiting to lock <0x00000000d5f1d5c8> (a java.lang.Object)
at com.example.charp04.Mutex$$Lambda$1/990368553.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
"Thread-2" #14 prio=5 os_prio=0 tid=0x000000001a9cf800 nid=0x4750 waiting for monitor entry [0x000000001b47e000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.charp04.Mutex.doSource(Mutex.java:18)
- waiting to lock <0x00000000d5f1d5c8> (a java.lang.Object)
at com.example.charp04.Mutex$$Lambda$1/990368553.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
"Thread-1" #13 prio=5 os_prio=0 tid=0x000000001a9cf000 nid=0x8bc waiting for monitor entry [0x000000001b37f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.charp04.Mutex.doSource(Mutex.java:18)
- waiting to lock <0x00000000d5f1d5c8> (a java.lang.Object)
at com.example.charp04.Mutex$$Lambda$1/990368553.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
"Thread-0" #12 prio=5 os_prio=0 tid=0x000000001a9ce000 nid=0x4634 waiting on condition [0x000000001b27e000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.example.charp04.Mutex.doSource(Mutex.java:18)
- locked <0x00000000d5f1d5c8> (a java.lang.Object)
at com.example.charp04.Mutex$$Lambda$1/990368553.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
只有Thread-0持有 locked <0x00000000d5f1d5c8> (a java.lang.Object)对象锁处于sleep状态,其他的线程则是处于- waiting to lock <0x00000000d5f1d5c8> (a java.lang.Object)等待获取锁的状态。
JVM基本操作指令
在JDK的bin路径下面有很多的Java提供的命令例如javap 就是对于程序的class编码进行反编译。这样的话就会出现大量的JVM的命令。
F:\developersrc\GIT\JavaHighConcurrency\JavaHC\target\classes\com\example\charp04>javap -c Mutex.class
Compiled from "Mutex.java"
public class com.example.charp04.Mutex {
public com.example.charp04.Mutex();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void doSource();
Code:
0: getstatic #2 获取MUTEX // Field MUTEX:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: getstatic #3 // Field java/util/concurrent/TimeUnit.SECONDS:Ljava/util/concurrent/TimeUnit;
9: ldc2_w #4 // long 100l
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 com/example/charp04/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:(Lcom/example/charp04/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
}
其中有两个关键的地方
monitorenter
每个对象都与一个monitor有关联,一个monitor的lock只能被一个线程在同一时间获取。在线程获取monitor所有权的时候会出现以下的几种情况
- 如果monitor的计数器为0,就意味着还没有被lock,如果有线程获取到对应的monitor的时候则这个计数器就会进行加1的操作。
- 如果一个有monitor的线程重新被加载会导致的后果就是monitor继续累加操作
- 一个monitor已经被线程所有,当其他线程进行获取操作的时候就会进入到Blocking状态直到计数器为0,才会获得monitor的所有权。
monitorexit
这个操作的主要作用就是放弃monitor的所有权,要想放弃所有权首先你要先拥有所有权,也就是说要对计数器进行减一的操作。
注意
- 在设置monitor锁定的时候锁定的关联的对象不能为空,如果为空的话没有对应的关联对象,也就没有monitor
- 不推荐将synchronized的作用域搞的太大,如上面展示了一样。在锁定范围内的数据要保持在一个范围内,如果锁定的返回过大导致monitor之间的东西太多了就会导致效率低下等问题的出现。
- 不同的monitor锁定同样的方法
- 多个锁的交叉会导致死锁
程序出现死锁的原因
1. 程序死锁
- 交叉锁导致程序出现死锁
- 内存不足
- 应答式数据交换
- 文件锁
- 数据库锁
- 死循环导致死锁
2.死锁判断
- 交叉锁引起的死锁,通过jstack工具或者使用jconsole工具进行判断盘查。
public class DeadLock {
private final Object MUTEX_READ = new Object();
private final Object MUTEX_WRITE = new Object();
public void read() {
synchronized (MUTEX_READ) {
System.out.println(Thread.currentThread().getName() + "获取到读锁");
synchronized (MUTEX_WRITE){
System.out.println(Thread.currentThread().getName()+"获取到写锁");
}
System.out.println(Thread.currentThread().getName()+"释放写锁");
}
System.out.println(Thread.currentThread().getName()+"释放读锁");
}
public void write() {
synchronized (MUTEX_WRITE) {
System.out.println(Thread.currentThread().getName() + "获取到写锁");
synchronized (MUTEX_READ){
System.out.println(Thread.currentThread().getName()+"获取到读锁");
}
System.out.println(Thread.currentThread().getName()+"释放读锁");
}
System.out.println(Thread.currentThread().getName()+"释放写锁");
}
public static void main(String[] args) {
final DeadLock deadLock = new DeadLock();
new Thread(()->{
while (true){
deadLock.read();
}
},"读线程").start();
new Thread(()->{
while (true){
deadLock.write();
}
},"写线程").start();
}
}
当然还可以有更多的诊断工具。
总结
通过对Synchronized关键字的深入分析,了解了synchronized的锁定机制。同时也引出了编程中的问题。当我们在编程过程中经常会因为锁的不正确使用导致死锁或者其他问题的发生,这个时候就需要会使用分析工具来进行分析问题。