学而不思则罔,思而不学则殆
【Java】线程安全与数据同步
什么是共享资源?共享资源指的是多个线程同时对一份资源进行访问(读写操作),被多个线程访问的资源就称为共享资源。如何保证多个线程访问到的数据是一致的,则被称为数据同步或者资源同步。
初识synchronized关键字
什么是synchronized
synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程可见的,那么对该对象的所有读或者所有读都将通过同步的方式来进行,具体表现如下:
- synchronized关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致问题的出现。
- synchronized关键字包括monitor enter和monitor exit两个JVM指令,它能够保证在任何时候线程执行到monitor enter成功之前都必须要从主内存中获取数据,二不是从缓存中;在monitor exit运行成功之后,共享变量被更新后的值必须刷入主存。
- synchronized的指令严格遵守java happens-before规则,一个monitor exit指令之前必定要有一个monitor enter
synchronized关键字的用法
synchronized修饰方法
public synchronized void show5() {
show("普通方法锁");
}
public static synchronized void show6() {
show("静态方法锁");
}
synchronized修饰代码块
private static final Object mStaticLock = new Object();
private final Object mLock = new Object();
public void show2() {
synchronized (mStaticLock) {
show("静态变量锁 代码块");
}
}
public void show3() {
synchronized (mLock) {
show("普通对象锁 代码块");
}
}
深入synchronized关键字
线程堆栈分析
一个简单的例子:
public class SynchronizedMainTest {
private final static Object MUTEX = new Object();
public void accessResource() {
System.out.println("accessResource:" + Thread.currentThread() + " synchronized");
synchronized (MUTEX) {
try {
System.out.println("accessResource:" + Thread.currentThread() + " sleep");
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final SynchronizedMainTest synchronizedMainTest = new SynchronizedMainTest();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
synchronizedMainTest.accessResource();
}
}).start();
}
}
}
展示如下:
accessResource:Thread[Thread-0,5,main] synchronized
accessResource:Thread[Thread-0,5,main] sleep
accessResource:Thread[Thread-1,5,main] synchronized
accessResource:Thread[Thread-2,5,main] synchronized
accessResource:Thread[Thread-3,5,main] synchronized
accessResource:Thread[Thread-4,5,main] synchronized
除了线程【Thread-0】进入了休眠,其他线程都没有执行休眠方法,说明没有拿到锁。
通过Jconsole查看线程
情况如下:
除了线程【Thread-0】- 处于 【TIMED_WAITING】状态,其他四个线程都处于【BLOCKED】,还告诉我们对象当前的拥有者是Thread-0.
通过Jstack查看线程
情况如下:
Thread-0
其他四个线程:
线程【Thread-0】持有了【0x000000074069fa10】对象,进入了休眠,没有释放锁,状态为【TIMED_WAITING】
另外四个线程都在等待锁【0x000000074069fa10】的释放,进入了堵塞状态【BLOCKED】。
JVM指令分析
命令javap:
javap -c SynchronizedMainTest.class
之前打印log的代码去掉,如下:
Compiled from "SynchronizedMainTest.java"
public class com.thread.SynchronizedMainTest {
public com.thread.SynchronizedMainTest();
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 com/thread/SynchronizedMainTest
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 39
15: new #11 // class java/lang/Thread
18: dup
19: new #12 // class com/thread/SynchronizedMainTest$1
22: dup
23: aload_1
24: invokespecial #13 // Method com/thread/SynchronizedMainTest$1."<init>":(Lcom/thread/SynchronizedMainTest;)V
27: invokespecial #14 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
30: invokevirtual #15 // Method java/lang/Thread.start:()V
33: iinc 2, 1
36: goto 10
39: 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 //4
24: monitorexit //5.执行monitor exit JVM指令
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
选取其中重要的片段:
- 获取MUTEX
- 执行monitorenter JVM指令
- 休眠结束后goto到23行
- 执行monitor exit JVM指令
Monitorenter
每个对象都与一个monitor相关联,一个monitor的lock锁只能被一个线程在同一时间获得,在一个线程尝试获取与对象关联的monitor的所有权时,会发生如下几件事:
- 如果monitor的计算器为0,则意味着该monitor的lock该没有被获得,某个线程获得之后立刻对该计数器加一,从此该线程就是这个monitor的所有者了
- 如果一个已经拥有该monitor所有权的线程重入,则会导致monitor计数器再次累加。
- 如果monitor已经被其他线程所拥有,则其他线程尝试获取该monitor的所有权时,会被陷入阻塞状态,直到monitor计数器变为0,才能再次尝试获取对monitor的所有权。
Monitorexit
释放对monitor的所有权,想要释放对某个对象关联的monitor的所有权的前提是你曾经获得了所有权。
释放monitor所有权的过程比较简单,就是讲monitor的计数器减一,如果计数器为0,那就意味着该线程不在拥有对该monitor的所有权,通俗的将就是解锁
与此同时被该monitor block的线程将会再次尝试获得该monitor的所有权。
使用synchronized 需要注意的问题
- 与monitor关联的对象不能为空
- synchronized 作用域太大
- 不同的monitor企图锁相同的方法
- 多个锁的交叉导致死锁
private final static Object MUTEX = null;
public void accessResource() {
synchronized (MUTEX) {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
报错异常:
Exception in thread "Thread-0" java.lang.NullPointerException
at com.thread.SynchronizedMainTest.accessResource(SynchronizedMainTest.java:9)
at com.thread.SynchronizedMainTest$1.run(SynchronizedMainTest.java:24)
at java.lang.Thread.run(Thread.java:748)
This Monitor和Class Monrtor的详细介绍
this monitor - 对象锁
class monitor - 类锁
程序死锁的原因以及如何诊断
程序死锁
- 交叉锁可导致程序出现死锁
线程A持有R1锁,等待获取R2锁,线程B持有R2锁等待获取R1锁,这种情况下爱最容易发生程序死锁
- 内存不足
- 一问一答式的数据交换
- 数据库锁
- 文件锁
- 死循环引起的死锁
程序死锁举例
public class SynchronizedMainTest {
private final static Object READ = new Object();
private final static Object WRITE = new Object();
public void read() {
synchronized (READ) {
System.out.println(Thread.currentThread().getName() + " get READ lock");
synchronized (WRITE) {
System.out.println(Thread.currentThread().getName() + " get WRITE lock");
}
System.out.println(Thread.currentThread().getName() + " release WRITE lock");
}
System.out.println(Thread.currentThread().getName() + " release READ lock");
}
public void write() {
synchronized (WRITE) {
System.out.println(Thread.currentThread().getName() + " get WRITE lock");
synchronized (READ) {
System.out.println(Thread.currentThread().getName() + " get READ lock");
}
System.out.println(Thread.currentThread().getName() + " release READ lock");
}
System.out.println(Thread.currentThread().getName() + " release WRITE lock");
}
public static void main(String[] args) {
final SynchronizedMainTest synchronizedMainTest = new SynchronizedMainTest();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronizedMainTest.read();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronizedMainTest.write();
}
}
}).start();
}
}
这段代码执行后很快就会发生死锁:
Thread-0 get READ lock
Thread-1 get WRITE lock
死锁诊断
通过命令:
jstack.exe -l 11708
查看死锁检测:
【Thread-1】等待的锁被线程【Thread-0】held
【Thread-0】等待的锁被线程【Thread-1】held
一般交叉死锁引起的思考所很容易进入BLOCKED状态,cpu占用不高们很容易借助工具来发现。
总结
了解synchroized关键字的内在原理。