尽管多线程编程极大地提高了效率,然而也会带来肯定的隐患。比如说两个线程同时往一个数据库表中插入不反复的数据,就可能会导致数据库中插入了雷同的数据。明天咱们就来一起探讨下线程平安问题,以及 Java 中提供了什么机制来解决线程平安问题。
以下是本文的目录纲要:
一. 什么时候会呈现线程平安问题?
二. 如何解决线程平安问题?
三.synchronized 同步办法或者同步块
若有不正之处,请多多体谅并欢送批评指正。
一. 什么时候会呈现线程平安问题?
在单线程中不会呈现线程平安问题,而在多线程编程中,有可能会呈现同时拜访同一个资源的状况,这种资源能够是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等,而当多个线程同时拜访同一个资源的时候,就会存在一个问题:
因为每个线程执行的过程是不可控的,所以很可能导致最终的后果与实际上的欲望相违反或者间接导致程序出错。更多 Java 学习薇老师:hua2021ei
举个简略的例子:
当初有两个线程别离从网络上读取数据,而后插入一张数据库表中,要求不能插入反复的数据。
那么必然在插入数据的过程中存在两个操作:
1)查看数据库中是否存在该条数据;
2)如果存在,则不插入;如果不存在,则插入到数据库中。
如果两个线程别离用 thread- 1 和 thread- 2 示意,某一时刻,thread- 1 和 thread- 2 都读取到了数据 X,那么可能会产生这种状况:
thread- 1 去查看数据库中是否存在数据 X,而后 thread- 2 也接着去查看数据库中是否存在数据 X。
后果两个线程查看的后果都是数据库中不存在数据 X,那么两个线程都别离将数据 X 插入数据库表当中。
这个就是线程平安问题,即多个线程同时拜访一个资源时,会导致程序运行后果并不是想看到的后果。
这外面,这个资源被称为:临界资源(也有称为共享资源)。
也就是说,当多个线程同时拜访临界资源(一个对象,对象中的属性,一个文件,一个数据库等)时,就可能会产生线程平安问题。
不过,当多个线程执行一个办法,办法外部的局部变量并不是临界资源,因为办法是在栈上执行的,而 Java 栈是线程公有的,因而不会产生线程平安问题。
二. 如何解决线程平安问题?
那么一般来说,是如何解决线程平安问题的呢?
基本上所有的并发模式在解决线程平安问题时,都采纳“序列化拜访临界资源”的计划,即在同一时刻,只能有一个线程拜访临界资源,也称作同步互斥拜访。
通常来说,是在拜访临界资源的代码后面加上一个锁,当拜访完临界资源后开释锁,让其余线程持续拜访。
在 Java 中,提供了两种形式来实现同步互斥拜访:synchronized 和 Lock。
本文次要讲述 synchronized 的应用办法,Lock 的应用办法在下一篇博文中讲述。
三.synchronized 同步办法或者同步块
在理解 synchronized 关键字的应用办法之前,咱们先来看一个概念:互斥锁,顾名思义:能到达到互斥拜访目标的锁。
举个简略的例子:如果对临界资源加上互斥锁,当一个线程在拜访该临界资源时,其余线程便只能期待。
在 Java 中,每一个对象都领有一个锁标记(monitor),也称为监视器,多线程同时拜访某个对象时,线程只有获取了该对象的锁能力拜访。
在 Java 中,能够应用 synchronized 关键字来标记一个办法或者代码块,当某个线程调用该对象的 synchronized 办法或者拜访 synchronized 代码块时,这个线程便取得了该对象的锁,其余线程临时无法访问这个办法,只有期待这个办法执行结束或者代码块执行结束,这个线程才会开释该对象的锁,其余线程能力执行这个办法或者代码块。
上面通过几个简略的例子来阐明 synchronized 关键字的应用:
1.synchronized 办法
上面这段代码中两个线程别离调用 insertData 对象插入数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Test {
public static void main(String[] args) {final InsertData insertData = new InsertData();
new Thread() {public void run() {insertData.insert(Thread.currentThread());
};
}.start();
new Thread() {public void run() {insertData.insert(Thread.currentThread());
};
}.start();}
}
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public void insert(Thread thread){for(int i=0;i<5;i++){System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
此时程序的输入后果为:
阐明两个线程在同时执行 insert 办法。
而如果在 insert 办法后面加上关键字 synchronized 的话,运行后果为:
1
2
3
4
5
6
7
8
9
10
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public synchronized void insert(Thread thread){for(int i=0;i<5;i++){System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
从上输入后果阐明,Thread- 1 插入数据是等 Thread- 0 插入完数据之后才进行的。阐明 Thread- 0 和 Thread- 1 是程序执行 insert 办法的。
这就是 synchronized 办法。
不过有几点须要留神:
1)当一个线程正在拜访一个对象的 synchronized 办法,那么其余线程不能拜访该对象的其余 synchronized 办法。这个起因很简略,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其余线程无奈获取该对象的锁,所以无法访问该对象的其余 synchronized 办法。
2)当一个线程正在拜访一个对象的 synchronized 办法,那么其余线程能拜访该对象的非 synchronized 办法。这个起因很简略,拜访非 synchronized 办法不须要取得该对象的锁,如果一个办法没用 synchronized 关键字润饰,阐明它不会应用到临界资源,那么其余线程是能够拜访这个办法的,
3)如果一个线程 A 须要拜访对象 object1 的 synchronized 办法 fun1,另外一个线程 B 须要拜访对象 object2 的 synchronized 办法 fun1,即便 object1 和 object2 是同一类型),也不会产生线程平安问题,因为他们拜访的是不同的对象,所以不存在互斥问题。
2.synchronized 代码块
synchronized 代码块相似于以下这种模式:
1
2
3
synchronized(synObject) {
}
当在某个线程中执行这段代码块,该线程会获取对象 synObject 的锁,从而使得其余线程无奈同时拜访该代码块。
synObject 能够是 this,代表获取以后对象的锁,也能够是类中的一个属性,代表获取该属性的锁。
比方下面的 insert 办法能够改成以下两种模式:
1
2
3
4
5
6
7
8
9
10
11
12
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public void insert(Thread thread){synchronized (this) {for(int i=0;i<100;i++){System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Object object = new Object();
public void insert(Thread thread){synchronized (object) {for(int i=0;i<100;i++){System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
}
从下面能够看出,synchronized 代码块应用起来比 synchronized 办法要灵便得多。因为兴许一个办法中只有一部分代码只须要同步,如果此时对整个办法用 synchronized 进行同步,会影响程序执行效率。而应用 synchronized 代码块就能够防止这个问题,synchronized 代码块能够实现只对须要同步的中央进行同步。
另外,每个类也会有一个锁,它能够用来管制对 static 数据成员的并发拜访。
并且如果一个线程执行一个对象的非 static synchronized 办法,另外一个线程须要执行这个对象所属类的 static synchronized 办法,此时不会产生互斥景象,因为拜访 static synchronized 办法占用的是类锁,而拜访非 static synchronized 办法占用的是对象锁,所以不存在互斥景象。
看上面这段代码就明确了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Test {
public static void main(String[] args) {final InsertData insertData = new InsertData();
new Thread(){
@Override
public void run() {insertData.insert();
}
}.start();
new Thread(){
@Override
public void run() {insertData.insert1();
}
}.start();}
}
class InsertData {
public synchronized void insert(){System.out.println("执行 insert");
try {Thread.sleep(5000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("执行 insert 结束");
}
public synchronized static void insert1() {System.out.println("执行 insert1");
System.out.println("执行 insert1 结束");
}
}
执行后果;
第一个线程外面执行的是 insert 办法,不会导致第二个线程执行 insert1 办法产生阻塞景象。
上面咱们看一下 synchronized 关键字到底做了什么事件,咱们来反编译它的字节码看一下,上面这段代码反编译后的字节码为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class InsertData {
private Object object = new Object();
public void insert(Thread thread){synchronized (object) {}}
public synchronized void insert1(Thread thread){ }
public void insert2(Thread thread){ }
}
从反编译取得的字节码能够看出,synchronized 代码块实际上多了 monitorenter 和 monitorexit 两条指令。monitorenter 指令执行时会让对象的锁计数加 1,而 monitorexit 指令执行时会让对象的锁计数减 1,其实这个与操作系统外面的 PV 操作很像,操作系统外面的 PV 操作就是用来管制多个线程对临界资源的拜访。对于 synchronized 办法,执行中的线程辨认该办法的 method_info 构造是否有 ACC_SYNCHRONIZED 标记设置,而后它主动获取对象的锁,调用办法,最初开释锁。如果有异样产生,线程主动开释锁。
有一点要留神:对于 synchronized 办法或者 synchronized 代码块,当出现异常时,JVM 会主动开释以后线程占用的锁,因而不会因为异样导致呈现死锁景象。