Java 线程安全
经典面试题:
1.什么是线程安全?
答:如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码,如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的;
2.什么是共享资源?
答:多个线程同时对一份资源进行访问或读写操作,被对个线程访问的资源就是共享资源;
3.描述多线程下数据不同步问题?
答:多线程下某些逻辑被重复或是遗漏执行,导致某变量数据异常(遗漏、重复或越界),数据不是一致的、同步的;
1 数据同步
1.1 数据不一致问题
如下是多线程下营业厅叫号程序:
public class TicketWinRunnable implements Runnable {
private int index = 1;
private final static int MAX = 500;
@Override
public void run() {
while (index <= MAX) {
System.out.println(Thread.currentThread().getName() + "的号码是:" + (index++));
}
}
public static void main(String[] args) {
final TicketWinRunnable ticketWinRunnable = new TicketWinRunnable();
Thread windoWThread1 = new Thread(ticketWinRunnable, "窗口1");
Thread windoWThread2 = new Thread(ticketWinRunnable, "窗口2");
Thread windoWThread3 = new Thread(ticketWinRunnable, "窗口3");
Thread windoWThread4 = new Thread(ticketWinRunnable, "窗口4");
windoWThread1.start();
windoWThread2.start();
windoWThread3.start();
windoWThread4.start();
}
}
执行结果:
...
...
窗口4的号码是:496
窗口4的号码是:497
窗口4的号码是:498
窗口4的号码是:499
窗口4的号码是:500
窗口1的号码是:450
窗口3的号码是:488
窗口2的号码是:478
每次执行都有新发现,该程序数据不一致问题汇总如下:
- 某个号码被掠过没有出现
- 某个号码多次出现
- 号码超过了最大值500
数据不一致原因大致如下:
- 线程执行是CPU时间片轮询调度,线程1执行index++后还没来得及打印,执行权就分配给线程2,导致某个号码被略过了
- 线程1执行index+1,然后执行权限分配给了线程2,由于线程1并未给index赋值,所以线程1和2的计算结果是一样的,导致号码重复
- 当index=499时,线程1和线程2看到条件满足,会进行index++操作并打印,线程2短暂停顿,线程1将index增加到500,线程2恢复运行后又将500加到了501,此时会超过最大值
1.2 解决数据不一致问题
解决数据不一致问题主要思想是让同一时刻只允许一个线程读写变量index;
synchronized关键字就是用来解决这个问题的;
2 synchronized 关键字
synchronized提供一种排他的机制,让同一时刻只能由一个线程执行某些操作;
2.1 synchronized 详解
官方解释:synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程可见,那么对该对象的所有读或写都将通过同步的方式来进行:
- synchronized可以修饰方法或代码块,提供一种锁的机制,让一个变量同一时刻只能由一个线程访问
- synchronized包含monitorenter、monitorexit两个JVM指令,保证任何时候任何线程执行到monitorenter之前都必须从主内存中获取数据,而不是从缓存中,而且在monitorexit运行成功之后,共享变量必须刷入主内存
- synchronized指令严格遵守happen-before原则,一个monitorexit之前必定要有一个moniotorenter指令
2.2 synchronized 用法
修饰方法:
public synchronized void sync() {
//do something...
}
修饰代码块:
//定义与synchronized关联的Object
private final Object MUTEX = new Object();
//将公共的部分加锁
synchronized (MUTEX) {
while (index <= MAX) {
//do something...
}
}
刚才营业厅取票机案例修改:
public class TicketWinSyncRunnable implements Runnable {
private int index = 1;
private final static int MAX = 500;
//定义与synchronized关联的Object
private final Object MUTEX = new Object();
@Override
public void run() {
//将公共的Runnable 逻辑单元部分加锁
synchronized (MUTEX) {
while (index <= MAX) {
System.out.println(Thread.currentThread().getName() + "的号码是:" + (index++));
}
}
}
public static void main(String[] args) {
final TicketWinSyncRunnable ticketWinRunnable = new TicketWinSyncRunnable();
Thread windoWThread1 = new Thread(ticketWinRunnable, "窗口1");
Thread windoWThread2 = new Thread(ticketWinRunnable, "窗口2");
Thread windoWThread3 = new Thread(ticketWinRunnable, "窗口3");
Thread windoWThread4 = new Thread(ticketWinRunnable, "窗口4");
windoWThread1.start();
windoWThread2.start();
windoWThread3.start();
windoWThread4.start();
}
}
这次结果已经正常:
.....
窗口1的号码是:497
窗口1的号码是:498
窗口1的号码是:499
窗口1的号码是:500
注:使用synchronized修饰方法时需要注意:第3部分 ThisMonitor与ClassMonitor
2.3 synchronized 深入理解
很多资料书籍将sunchronized(类A)称为锁,这是不严谨的,准确的说是某线程获取了与类A关联的monitor锁:
2.3.1 线程堆栈分析
通过一案例分析,sunchronized对操作共享资源的线程的影响;
程序是对一个公共部分(进行休眠)加锁,然后启5个线程来执行它,查看此时各个线程的状态:
public class Mutex {
//定义与synchronized关联的Object
private final static Object MUTEX = new Object();
//将公共逻辑单元部分加锁
public void accessResource() {
synchronized (MUTEX) {
try {
TimeUnit.SECONDS.sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final Mutex mutex = new Mutex();
//生产5个线程执行accessResource方法
for (int i = 0; i < 5; i++) {
new Thread(mutex::accessResource).start();
}
}
}
运行状态:
2.3.1.1 Idea Threads中的线程状态
- 第一个线程进入SLEEPING状态
- 其它线程是MONITOR状态
2.3.1.2 Jconsole中线程状态
执行程序后,立马启动C:\Program Files\Java\jdk1.8.0_151\bin路径下的jconsole.exe,选择程序进程的进行连接:
连接后–>线程,对当前进程中线程进行查看,会看到我们的5个线程,发现如下现象:
- 当前获得锁定的线程状态为TIMED_WAITING,也就是在进行休眠,并获取了与synchronized关联Object[MUTEX]的monitor锁
- 竞争Object[MUTEX]monitor锁的其他线程状态为BLOCKED
- Thread0~4执行是无序的,谁获取到CPU时间片执行权,谁就执行
拥有Object[MUTEX]monitor锁的线程:
其他竞争Object[MUTEX]monitor锁的线程:
2.3.1.3 Jstack打印线程堆栈信息
先找到程序进程PID:
然后执行命令:jstack 8448
看到如下现象:
- Thread-4获得了Object[MUTEX]的monitor锁(waiting on condition[0x000000001a46f000]),当前正在进行sleeping,并且对其上了锁(locked <0x00000000d5baf900> (a java.lang.Object))
- 其他竞争Object[MUTEX]monitor锁的线程当前状态为阻塞[BLCODED],并且正在MUTEX monitor锁的wait set中等待(waiting for monitor entry [0x000000001a36f000])
2.3.2 JVM指令分析
使用javap对Mutex.class进行反汇编,得到JVM指令:
到Mutex.class路径下执行命令:javap -c Mutex.class
从反汇编内容中看,肉眼能识别的是:
- accessResource方法中第0个指令[getstatic]是在获取MUTEX对象
- accessResource方法中第5[monitorenter]和25[monitorexit]个指令间的就是synchronized修饰的逻辑单元
- accessResource方法中第6、9、12个指令是在执行休眠
得到指令如下:
Compiled from "Mutex.java"
public class com.wxx.thread.sync.Mutex {
public com.wxx.thread.sync.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.SECONDS:Ljava/util/concurrent/TimeUnit;
9: ldc2_w #4 // long 20l
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/wxx/thread/sync/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/wxx/thread/sync/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存在一个计数器,如果计数器为0,则表示与之关联的对象monitor锁没有被获取;如果被获取了则计数器+1
- 如果已经拥有该monitor锁的对象重入,则计数器累加
- 如果monitor锁被其他线程所拥有,本线程尝试获取该monitor锁时,会进入阻塞状态直到该monitor锁计数器为0
monitorexit:
- 释放对monitor锁的所有权,即计数器-1操作,直到计数器为0;
2.3.3 synchronized 注意事项
- 与monitor关联的对象不能为空
synchronized关键字是获取的与MUTEX关联的monitor锁,MUTEX为null,monitor就无从谈起了;
private final Object MUTEX = new Object();
public void syncMethod() {
synchronized (MUTEX) {
//do someThing...
}
}
- synchronized作用域太大
synchronized具有排他性,所修饰的部分所有线程会串行的执行,所以如果synchronized作用于越大,代表着其效率越低,甚至会丧失并发的优势;
private final Object MUTEX = new Object();
public void syncMethod() {
synchronized (MUTEX) {
//do someThing...
}
}
- 不同的monitor锁相同的区域
synchronized获取的不是同一个对象的monitor锁,此时此作用域的资源在线程间不具有排他性;
public static void main(String[] args) {
//3 获取不同对象的锁
for (int i = 0; i < 5; i++) {
new Thread(() -> {
//定义一个关联对象 该对象不是公共资源
final Object MUTEX = new Object();
//synchronized获取与MUTEX关联的monitor锁
synchronized (MUTEX) {
//do someThing...
}
}).start();
}
}
- 多个锁的交叉导致死锁
相互等待,至同时阻塞;
//4 多个锁交叉导致死锁
private final Object MUTEX_READ = new Object();
private final Object MUTEX_WRITE = new Object();
private void read() {
synchronized (MUTEX_READ) {
synchronized (MUTEX_WRITE) {
//do someThing...
}
}
}
private void write() {
synchronized (MUTEX_WRITE) {
synchronized (MUTEX_READ) {
//do someThing...
}
}
}
3 ThisMonitor & ClassMonitor
当synchronized修饰代码块的时候获取的是与之关联对象的monitor锁;
当synchronized修饰方法时,它获取的是谁的锁呢?
- ThisMonitor 验证修饰普通方法;
- ClassMonitor验证修饰静态方法;
3.1 ThisMonitor
执行如下程序,并找到PID后,通过Jstack[jstack -l PID]打印堆栈信息:
public class ThisMonitor {
public synchronized void method1() {
System.out.println(Thread.currentThread().getName() + "enter to method1");
try {
TimeUnit.SECONDS.sleep(20l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void method2() {
System.out.println(Thread.currentThread().getName() + "enter to method2");
try {
TimeUnit.SECONDS.sleep(20l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method3() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "enter to method3");
try {
TimeUnit.SECONDS.sleep(20l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThisMonitor thisMonitor = new ThisMonitor();
new Thread(thisMonitor::method1).start();
new Thread(thisMonitor::method2).start();
new Thread(thisMonitor::method3).start();
}
}
jstack打印堆栈信息分析:
- 三个线程都在竞争实例thisMonitor <0x00000000d5bb0528>的monitor锁
- Thread-0获取到了thisMonitor 的monitor锁,正在进行sleep
总结:
- synchronized 修饰方法时,获取的是该类事例的monitor锁
jstack打印堆栈信息内容:
3.2 ClassMonitor
执行如下程序,并找到PID后,通过Jstack[jstack -l PID]打印堆栈信息:
public class ClassMonitor {
public static synchronized void method1() {
System.out.println(Thread.currentThread().getName() + "enter to method1");
try {
TimeUnit.SECONDS.sleep(20l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void method2() {
System.out.println(Thread.currentThread().getName() + "enter to method2");
try {
TimeUnit.SECONDS.sleep(20l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void method3() {
synchronized (ClassMonitor.class) {
System.out.println(Thread.currentThread().getName() + "enter to method3");
try {
TimeUnit.SECONDS.sleep(20l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new Thread(ClassMonitor::method1).start();
new Thread(ClassMonitor::method2).start();
new Thread(ClassMonitor::method3).start();
}
}
jstack打印堆栈信息分析:
- 三个线程都在竞争实例ClassMonitor.class实例<0x00000000d5badb38>的monitor锁
- Thread-2获取到了ClassMonitor.class的monitor锁,正在进行sleep
总结:
- synchronized 修饰静态方法时,获取的是ClassMonitor.class实例的monitor锁
jstack打印堆栈信息内容:
4 程序死锁
4.1 程序死锁
死锁的原因大致如下:
- 交叉锁导致程序死锁
- 内存不足
- 一问一答式程序交互
- 数据库锁
- 文件锁
- 死循环引起的死锁
4.2 死锁诊断
死锁案例,在4时生成死锁:
- 1 Thread-Read先启动
- 2 Thread-Read线程启动后不停地获取和释放MUTEX_READ和MUTEX_WRITE的monitor锁
- 3 Thread-Write线程启动后不停地获取和释放MUTEX_READ和MUTEX_WRITE的monitor锁
- 4 某一时刻Thread-Read持有MUTEX_READ的monitor锁,Thread-Write持有MUTEX_WRITE的monitor锁
public class DeadLock {
private final Object MUTEX_READ = new Object();
private final Object MUTEX_WRITE = new Object();
private void read() {
synchronized (MUTEX_READ) {
System.out.println(Thread.currentThread().getName() + "get READ lock");
synchronized (MUTEX_WRITE) {
System.out.println(Thread.currentThread().getName() + "get WRITE lock");
}
System.out.println(Thread.currentThread().getName() + "ralease WRITE lock");
}
System.out.println(Thread.currentThread().getName() + "ralease READ lock");
}
private void write() {
synchronized (MUTEX_WRITE) {
System.out.println(Thread.currentThread().getName() + "get WRITE lock");
synchronized (MUTEX_READ) {
System.out.println(Thread.currentThread().getName() + "get READ lock");
}
System.out.println(Thread.currentThread().getName() + "ralease READ lock");
}
System.out.println(Thread.currentThread().getName() + "ralease WRITE lock");
}
public static void main(String[] args) {
final DeadLock deadLock = new DeadLock();
//Thread-Read 线程
new Thread(() -> {
while (true) {
deadLock.read();
}
}, "Thread-Read").start();
//Thread-Write 线程
new Thread(() -> {
while (true) {
deadLock.write();
}
}, "Thread-Write").start();
}
}
IDE中看到的现象:
Jstack中看到的线程堆栈信息:
在堆栈信息最下边,jstack直接提示发现了死锁:
Jconsole中看到的现象:
参考文献:
[ 1 ] Java高并发编程详解 汪文君著。–北京:机械工业出版社,2018年6月第1版