线程安全与数据同步
synchronized关键字
多个线程对index变量(共享变量/资源)同时操作引起的,再JDK1.5以前,要解决这个问题需要使用synchronized关键字,synchronized提供了一种排他的机制,同一个时间只能有一个线程执行某些操作。
synchronized关键字的解释
synchronized关键字可以实现一个简单的策略来放置线程干扰和内存一致行错误,如果一个对象多个线程是可见的,那么该对象所有读或者些都将通过同步的方式来进行。
-
synchronized关键字提供了一种锁机制,能够确认保存共享变量的互斥访问,从而放置数据不一致问题的出现。
-
synchronized关键字包括monitor enter和monitor exit 两个JVM指令,它能够保证再任何时候任何线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中,再monitor exit和运行成功之后,共享变量被更新后的值必须刷入主内存。
-
synchronized指令严格遵守java happens-before规则,一个monitor exit指令之前必须要由一个monitor enter。
synchronized关键字的理解
线程堆栈分析
syncronized关键字提供了一种互斥机制,也就是说在统一时刻,只能有一个线程访问同步资源,synchronized是某线程取了与mutex关联的monitor锁. 例子:
public class Demo {
private final static Object MUTEX = new Object();
public void accessResource() {
synchronized (MUTEX) {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final Demo mutex = new Demo();
for (int i = 0; i < 5; i++) {
new Thread(mutex::accessResource).start();
}
}
}
线程状态查看: 方法一:
方法二:
JVM指令分析
使用JDK自带javap工具堆Demo class进行反编译,输出大量的JVM指令,在这些指令中,monitor enter和monitor exit是成对出现,有可能出现一个monitor,多个monitor exit,但是每个monitor exit之前必须有对应的monitor enter.
PS E:\book\allCode\JavaConcurrency\target\classes\chapter_4\com\flexible> javap -c Demo
警告: 二进制文件Demo包含chapter_4.com.flexible.Demo
Compiled from "Demo.java"
public class chapter_4.com.flexible.Demo {
public chapter_4.com.flexible.Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void accessResource();
Code://获取MUTEX
0: getstatic #2 // Field MUTEX:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter//执行monitor enter 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//跳转到23
18: astore_2
19: aload_2
20: invokevirtual #8 // Method java/lang/InterruptedException.printStackTrace:()V
23: aload_1
24: monitorexit //执行monitor exitjvm指令
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 chapter_4/com/flexible/Demo
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:(Lchapter_4/com/flexible/Demo;)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
}
1.Monitorenter 每个对象都与一个monitor相关联,一个monitor的lock的锁只能被一个线程同时获得,在一个线程尝试获得与对象关联的monitor的所有权时会发生如下的事情。
-
如果monitor的计数器为0,则以为着该monitor的lock还没有被获得,某个线程获得之后将立即计数器加1.从此该线程就是这个monitor的所有者。
-
如果一个已经永远了该monitor所有权的线程重入,则会导致monitor计数器再次加1
-
如果monitor已经被其他线程所拥有,则其他的线程尝试获取该monitor的所有权时,会被现如阻塞状态知道monitor计数器变为0,才能再次尝试获取堆monitor的所有权.
2.Monitorexit
释放对monitor的所有权,想要释放对某个对象关联的monitor的所有权的前提时,你增加获得了所有权。释放monitor的所有权的过程比较简单,就是将monitor的计数器减1,如果计数器的结果为0,那么就以为这个该线程不再拥有对该monitor的所有权,通俗的讲就是解锁。
使用synchronized需要注意的问题
1.与monitor关联的对象不能为空(null)
2.synchronized作用域不要太大(比如直接将synchronized方法放在方法)
3.不同的monitor企图锁相同的方法 public class MyTask implements Runnable { private static final Object MUTEX = new Object(); @Override public void run() { synchronized (MUTEX){ //... System.out.println(Thread.currentThread().getName()); try { TimeUnit.MILLISECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }
}
public static void main(String[] args) {
for (int i=0;i<5;i++){
new Thread(MyTask::new).start();
}
}
}
4.多锁交叉导致死锁
This Monitor和Class Monitor的详细介绍
This monitor
public class ThisMonitorDemo {
public synchronized void method_1() {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void method_2() {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method_3() {
synchronized (this) {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThisMonitorDemo demo = new ThisMonitorDemo();
Thread t1 = new Thread(demo::method_1, "T1");
Thread t2 = new Thread(demo::method_2, "T2");
Thread t3 = new Thread(demo::method_2, "T3");
t1.start();
t2.start();
t3.start();
}
}
执行的结果:
由上可知三个线程都是同一个实例锁
class monitor
在修饰类方法的时候使用的是类监视器而不是实例监视器,通过下面的例子就可以看出来。
public class ClssMonitorDemo {
public static synchronized void method_1() {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void method_2() {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void method_3() {
synchronized (ClssMonitorDemo.class) {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThisMonitorDemo demo = new ThisMonitorDemo();
Thread t1 = new Thread(ClssMonitorDemo::method_1, "T1");
Thread t2 = new Thread(ClssMonitorDemo::method_2, "T2");
Thread t3 = new Thread(ClssMonitorDemo::method_2, "T3");
t1.start();
t2.start();
t3.start();
}
}
根据下图的画红线的地方有java.lang.Class就可以看出来了。
程序思索的原因以及如何诊断
程序死锁
1.交叉锁可导致程序出现死锁 例如线程1获取道L1锁,等待获取L2锁,线程2获得了L2锁,等待会的L1锁,这种"哲学家们吃面"的场景就会导致死锁。
2.内存不足 在并发请求的系统可能内存时,如果内存时。可能会出现死锁的情况,线程1和线程2,如果线程一执行都各需要10M,而内存只有10M,那么线程1和线程2都在等待对方释放内存资源而导致死锁。
3.一问一答式数据交换 服务段开启某个端口,等待客户访问,客户端发送请求立即等待接收,由于某种原因服务段错过了客户端的请求,仍然在等待一问以打的式的数据交换,此时,服务端和客户端都在等待双方发送数据。
4.数据库锁 无论数据库表级锁,还是行级锁,比如某个线程执行for update语句推出了事务,其他线程访问该数据库时都会现如死锁。
5.文件锁 某线程获得文件锁意外退出,其他读取该文件的线程也将会进入死锁,知道系统释放文件句柄资源。
6.死循环引起的死锁。 程序由于代码原因或者对某个一次处理不当,进入了死循环,虽然看线程堆栈信息不会发现任何死锁的集乡,但是程序不工作了,CPU的使用率居高不下,这种死锁现象一般称为系统假死,这种情况一般排查比较困难。
死锁诊断
1.交叉锁引起的死锁 一般使用jstack -l PID 或者使用jconsole图形界面上直接查看.
2.死循环引起的死锁(假死) 一般使用jstack,jconsole,jvisualvm工具或者jProfiler(收费)进行检查,四实查看无法得到很明显的结果,因为线程没有死锁,只是一致runnbale而且CPU的使用率居高不下.