1 線程與線程間通信
1.1 基本概念以及線程與進程之間的區別聯系
- 關於進程和線程,首先從定義上理解就有所不同:
- 進程是具有一定獨立功能的程序、它是系統進行資源分配和調度的一個獨立單位,重點在系統調度和單獨的單位,也就是說進程是可以獨 立運行的一段程序。
- 線程是進程的一個實體,是CPU調度和分派的基本單位,他是比進程更小的能獨立運行的基本單位,線程自己基本上不擁有系統資源。在運行時,只是暫用一些計數器、寄存器和棧 。
-
他們之間的關系
- 一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程(通常說的主線程)。
- 資源分配給進程,同一進程的所有線程共享該進程的所有資源。
- 線程在執行過程中,需要協作同步。不同進程的線程間要利用消息通信的辦法實現同步。
- 處理機分給線程,即真正在處理機上運行的是線程。
- 線程是指進程內的一個執行單元,也是進程內的可調度實體。
-
從三個角度來剖析二者之間的區別
- 調度:線程作為調度和分配的基本單位,進程作為擁有資源的基本單位。
- 並發性:不僅進程之間可以並發執行,同一個進程的多個線程之間也可以並發執行。
- 擁有資源:進程是擁有資源的一個獨立單位,線程不擁有系統資源,但可以訪問隸屬於進程的資源。
1.2 多線程間通信方式
- 共享變量
- wait/notify機制
- Lock/Condition機制
- 管道
1.2.1 共享變量
- 線程間發送信號的一個簡單方式是在共享對象的變量里設置信號值。線程A在一個同步塊里設置boolean型成員變量hasDataToProcess為true,線程B也在同步塊里讀取hasDataToProcess這個成員變量。這個簡單的例子使用了一個持有信號的對象,並提供了set和check方法:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class MySignal{
protected boolean hasDataToProcess = false;
public synchronized boolean hasDataToProcess(){
return this.hasDataToProcess;
}
public synchronized void setHasDataToProcess(boolean hasData){
this.hasDataToProcess = hasData;
}
}
|
- 線程A和B必須獲得指向一個MySignal共享實例的引用,以便進行通信。如果它們持有的引用指向不同的MySingal實例,那么彼此將不能檢測到對方的信號。需要處理的數據可以存放在一個共享緩存區里,它和MySignal實例是分開存放的。
1.2.2 wait()/notify機制
- 為了實現線程通信,我們可以使用Object類提供的wait()、notify()、notifyAll()三個方法。調用wait()方法會釋放對該同步監視器的鎖定。這三個方法必須由同步監視器對象來調用,這可分成兩種情況:
- 對於使用synchronized修飾的同步方法,因為該類的默認實例是(this)就是同步監視器,所以可以直接調用這三使用個方法。
- 對於synchronized修飾的同步代碼塊,同步監視器是synchronized括號里的對象,所以必須使用該對象調用這三個方法。
-
假設系統中有兩條線程,這兩條線程分別代表取錢者和存錢者。現在系統有一種特殊的要求,系統要求存款者和取錢者不斷的實現存款和取錢動作,而且要求每當存款者將錢存入指定賬戶后,取錢者立即將錢取走.不允許存款者兩次存錢,也不允許取錢者兩次取錢。
我們通過設置一個旗標來標識賬戶中是否已有存款,有就為true,沒有就標為false。具體代碼如下: -
首先我們定義一個Account類,這個類中有取錢和存錢的兩個方法,由於這兩個方法可能需要並發的執行取錢、存錢操作,所有將這兩個方法都修改為同步方法.(使用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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
public class Account {
private String accountNo;
private double balance;
//標識賬戶中是否有存款的旗標
private boolean flag=false;
public Account() {
super();
}
public Account(String accountNo, double balance) {
super();
this.accountNo = accountNo;
this.balance = balance;
}
public synchronized void draw (double drawAmount){
try {
if(!flag){
this.wait();
}
else {
//取錢
System.out.println(Thread.currentThread().getName()+
" 取錢:"+drawAmount);
balance=balance-drawAmount;
System.out.println(
"余額 : "+balance);
//將標識賬戶是否已有存款的標志設為false
flag=
false;
//喚醒其它線程
this.notifyAll();
}
}
catch (Exception e) {
e.printStackTrace();
}
}
public synchronized void deposit(double depositAmount){
try {
if(flag){
this.wait();
}
else{
System.out.println(Thread.currentThread().getName()+
"存錢"+depositAmount);
balance=balance+depositAmount;
System.out.println(
"賬戶余額為:"+balance);
flag=
true;
//喚醒其它線程
this.notifyAll();
}
}
catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
|
- 接下來創建兩個線程類,分別為取錢和存錢線程!
- 取錢線程類:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class DrawThread implements Runnable {
private Account account;
private double drawAmount;
public DrawThread(Account account, double drawAmount) {
super();
this.account = account;
this.drawAmount = drawAmount;
}
public void run() {
for(int i=0;i<100;i++){
account.draw(drawAmount);
}
}
}
|
- 存錢線程類:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class depositThread implements Runnable{
private Account account;
private double depositAmount;
public depositThread(Account account, double depositAmount) {
super();
this.account = account;
this.depositAmount = depositAmount;
}
public void run() {
for(int i=0;i<100;i++){
account.deposit(depositAmount);
}
}
}
|
- 最后我們測試一下這個取錢和存錢的操作
|
1
2
3
4
5
6
7
8
9
10
|
public class TestDraw {
public static void main(String[] args) {
//創建一個賬戶
Account account=
new Account();
new Thread(new DrawThread(account, 800),"取錢者").start();
new Thread(new depositThread(account, 800),"存款者甲").start();
new Thread(new depositThread(account, 800),"存款者乙").start();
new Thread(new depositThread(account, 800),"存款者丙").start();
}
}
|
- 大致的輸出結果如下
|
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
36
|
存款者甲存錢
800.0
賬戶余額為:
800.0
取錢者 取錢:
800.0
余額 :
0.0
存款者丙存錢
800.0
賬戶余額為:
800.0
取錢者 取錢:
800.0
余額 :
0.0
存款者甲存錢
800.0
賬戶余額為:
800.0
取錢者 取錢:
800.0
余額 :
0.0
存款者丙存錢
800.0
賬戶余額為:
800.0
取錢者 取錢:
800.0
余額 :
0.0
存款者甲存錢
800.0
賬戶余額為:
800.0
取錢者 取錢:
800.0
余額 :
0.0
存款者丙存錢
800.0
賬戶余額為:
800.0
取錢者 取錢:
800.0
余額 :
0.0
存款者甲存錢
800.0
賬戶余額為:
800.0
取錢者 取錢:
800.0
余額 :
0.0
存款者丙存錢
800.0
賬戶余額為:
800.0
取錢者 取錢:
800.0
余額 :
0.0
存款者甲存錢
800.0
賬戶余額為:
800.0
取錢者 取錢:
800.0
余額 :
0.0
|
1.2.3 Lock/Condition機制
- 如何程序不使用synchronized關鍵字來保持同步,而是直接適用Lock對像來保持同步,則系統中不存在隱式的同步監視器對象,也就不能使用wait()、notify()、notifyAll()來協調線程的運行.
- 當使用LOCK對象保持同步時,Java為我們提供了Condition類來協調線程的運行。關於Condition類,JDK文檔里進行了詳細的解釋.,再次就不啰嗦了。
- 我們就拿Account類進行稍微的修改 一下吧!
|
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
//顯示定義Lock對象
private final Lock lock=new ReentrantLock();
//獲得指定Lock對象對應的條件變量
private final Condition con=lock.newCondition();
private String accountNo;
private double balance;
//標識賬戶中是否有存款的旗標
private boolean flag=false;
public Account() {
super();
}
public Account(String accountNo, double balance) {
super();
this.accountNo = accountNo;
this.balance = balance;
}
public void draw (double drawAmount){
//加鎖
lock.lock();
try {
if(!flag){
// this.wait();
con.await();
}
else {
//取錢
System.out.println(Thread.currentThread().getName()+
" 取錢:"+drawAmount);
balance=balance-drawAmount;
System.out.println(
"余額 : "+balance);
//將標識賬戶是否已有存款的標志設為false
flag=
false;
//喚醒其它線程
// this.notifyAll();
con.signalAll();
}
}
catch (Exception e) {
e.printStackTrace();
}
finally{
lock.unlock();
}
}
public void deposit(double depositAmount){
//加鎖
lock.lock();
try {
if(flag){
// this.wait();
con.await();
}
else{
System.out.println(Thread.currentThread().getName()+
"存錢"+depositAmount);
balance=balance+depositAmount;
System.out.println(
"賬戶余額為:"+balance);
flag=
true;
//喚醒其它線程
// this.notifyAll();
con.signalAll();
}
}
catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
finally{
lock.unlock();
}
}
}
|
- 輸出結果和上面是一樣的! 只不過這里 顯示的使用Lock對像來充當同步監視器,使用Condition對象來暫停指定線程,喚醒指定線程!
1.2.4 管道
-
管道流是JAVA中線程通訊的常用方式之一,基本流程如下:
-
創建管道輸出流PipedOutputStream pos和管道輸入流PipedInputStream pis
-
將pos和pis匹配,pos.connect(pis);
-
將pos賦給信息輸入線程,pis賦給信息獲取線程,就可以實現線程間的通訊了
-
|
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
public class testPipeConnection {
public static void main(String[] args) {
/**
* 創建管道輸出流
*/
PipedOutputStream pos =
new PipedOutputStream();
/**
* 創建管道輸入流
*/
PipedInputStream pis =
new PipedInputStream();
try {
/**
* 將管道輸入流與輸出流連接 此過程也可通過重載的構造函數來實現
*/
pos.connect(pis);
}
catch (IOException e) {
e.printStackTrace();
}
/**
* 創建生產者線程
*/
Producer p =
new Producer(pos);
/**
* 創建消費者線程
*/
Consumer1 c1 =
new Consumer1(pis);
/**
* 啟動線程
*/
p.start();
c1.start();
}
}
/**
* 生產者線程(與一個管道輸入流相關聯)
*
*/
class Producer extends Thread {
private PipedOutputStream pos;
public Producer(PipedOutputStream pos) {
this.pos = pos;
}
public void run() {
int i = 0;
try {
while(true)
{
this.sleep(3000);
pos.write(i);
i++;
}
}
catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 消費者線程(與一個管道輸入流相關聯)
*
*/
class Consumer1 extends Thread {
private PipedInputStream pis;
public Consumer1(PipedInputStream pis) {
this.pis = pis;
}
public void run() {
try {
while(true)
{
System.out.println(
"consumer1:"+pis.read());
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}
|
- 程序啟動后,就可以看到producer線程往consumer1線程發送數據
consumer1:0 consumer1:1 consumer1:2 consumer1:3 ......
-
管道流雖然使用起來方便,但是也有一些缺點
-
管道流只能在兩個線程之間傳遞數據 。線程consumer1和consumer2同時從pis中read數據,當線程producer往管道流中寫入一段數據后,每一個時刻只有一個線程能獲取到數據,並不是兩個線程都能獲取到producer發送來的數據,因此一個管道流只能用於兩個線程間的通訊。不僅僅是管道流,其他IO方式都是一對一傳輸。
-
管道流只能實現單向發送,如果要兩個線程之間互通訊,則需要兩個管道流 。可以看到上面的例子中,線程producer通過管道流向線程consumer發送數據,如果線程consumer想給線程producer發送數據,則需要新建另一個管道流pos1和pis1,將pos1賦給consumer1,將pis1賦給producer,具體例子本文不再多說。
-
2 進程與進程間通信
2.1 進程間通信方式
- 管道(Pipe) :管道可用於具有親緣關系進程間的通信,允許一個進程和另一個與它有共同祖先的進程之間進行通信。
- 命名管道(named pipe) :命名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關 系 進程間的通信。命名管道在文件系統中有對應的文件名。命名管道通過命令mkfifo或系統調用mkfifo來創建。
- 信號(Signal) :信號是比較復雜的通信方式,用於通知接受進程有某種事件發生,除了用於進程間通信外,進程還可以發送 信號給進程本身;Linux除了支持Unix早期信號語義函數sigal外,還支持語義符合Posix.1標准的信號函數sigaction(實際上,該函數是基於BSD的,BSD為了實現可靠信號機制,又能夠統一對外接口,用sigaction函數重新實現了signal函數)。
- 消息(Message)隊列 :消息隊列是消息的鏈接表,包括Posix消息隊列system V消息隊列。有足夠權限的進程可以向隊列中添加消息,被賦予讀權限的進程則可以讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式字節流以及緩沖區大小受限等缺
- 共享內存 :使得多個進程可以訪問同一塊內存空間,是最快的可用IPC形式。是針對其他通信機制運行效率較低而設計的。往往與其它通信機制,如信號量結合使用,來達到進程間的同步及互斥。
- 內存映射(mapped memory) :內存映射允許任何多個進程間通信,每一個使用該機制的進程通過把一個共享的文件映射到自己的進程地址空間來實現它。
- 信號量(semaphore) :主要作為進程間以及同一進程不同線程之間的同步手段。
- 套接口(Socket) :更為一般的進程間通信機制,可用於不同機器之間的進程間通信。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到其它類Unix系統上:linux和System V的變種都支持套接字。
https://www.tuhd.top/2017/08/04/2017-08-04-threadandprocess/
