一. 進程間的通信
進程間的通信(IPC)是指在不同進程之間傳播或交換信息。
IPC的方式通常有管道、消息隊列、信號量、共享存儲、Socket和Streams等。其中Socket和Stream支持不同主機上的兩個進程IPC。
- 管道:
匿名管道概念:在內核中申請一塊固定大小的緩衝區,程序擁有寫入和讀取的權利,一般使用fork函數實現父子進程的通信。(它可以看成一個特殊的文件。對於它的讀寫也可以使用普通的read,write等函數。但是它不是普通的文件,並不屬於其他任何文件系統,並且只存在於內存之中。)
當一個管道建立時,它會創建兩個文件描述,fd[0]爲讀打開,fd[1]爲寫打開。
單個進程的管道幾乎沒有任何用處。所以,通常調用pipe的進程接着調用fork,這樣就創建了父進程與子進程之間的IPC通道。
命名管道(FIFO)概念:在內核中申請一塊固定大小的緩衝區,程序擁有寫入和讀取的權利,沒有血緣關係的進程也可以進程間通信。(有路徑名與之相關聯,它以一種特殊設備文件形式存在於文件系統中)
FIFO的通信方式類似於進程中使用文件來傳輸數據,只不過FIFO類型文件同時具有管道的特性。在數據讀出時,FIFO管道同時清除數據,並且“先進先出”。
特點:
- 面向字節流
- 生命週期隨內核
- 自帶同步互斥機制
- 半雙工(即數據只能在一個方向上流動),具有固定的讀端和寫端。單向通信,兩個管道實現雙向通信。
2. 消息隊列:
消息隊列,是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標識。
特點: - 消息隊列是面向記錄的,其中消息具有特定的格式以及特定的優先級。
- 消息隊列獨立於發送與接收進程。進程終止時,消息隊列及其內容並不會被刪除。
- 消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。
3. 信號量:
信號量是一個計數器,信號量用於實現進程間的互斥與同步,而不是用於存儲進程間通信數據。最簡單的信號量只能取0和1變量,這也是信號量最常見的一種形式,叫做二值信號量。而可以取正整數的信號量被稱爲通用信號量。
特點:
- 信號量用於進程間同步,若要在進程間傳遞數據需要結合共享內存。
- 信號量基於操作系統的PV操作(P操作意味着進程請求一個資源,V操作意味着進程釋放一個資源),程序對信號量的操作都是原子操作。
- 每次對信號量的PV操作不僅限於對信號量加1或減1,而且可以加減任意正整數。 支持信號量組。
4. 共享內存:
共享內存,指兩個或多個進程共享一個給定的存儲區。
特點:
- 共享內存是最快的一種IPC,因爲進程是直接對內存進行存取。 因爲多個進程可以同時操作,所以需要進行同步。
- 信號量+共享內存通常結合在一起使用,信號量用來同步對共享內存的訪問。
五種通信方式總結:
1.管道:速度慢,容量有限,只有父子進程能通訊
2.FIFO:任何進程間都能通訊,但速度慢
3.消息隊列:容量受到系統限制,且要注意第一次讀的時候,要考慮上一次沒有讀完數據的問題
4.信號量:不能傳遞複雜消息,只能用來同步
5.共享內存區:能夠很容易控制容量,速度快,但要保持同步,比如一個進程在寫的時候,另一個進程要注意讀寫的問題,相當於線程中的線程安全,當然,共享內存區同樣可以用作線程間通訊,不過沒這個必要,線程間本來就已經共享了同一進程內的一塊內存
二.線程如何同步?
Java允許多線程併發控制,當多個線程同時操作一個可共享資源變量時,將會導致數據不準確,相互之間產生衝突。因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程調用。從而保證了該變量的唯一性和準確性。
1.同步方法
即有synchronized關鍵字修飾的方法。
由於java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時,內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處於阻塞狀態。
【代碼示例】
//同步方法
package Thread;
public class TicketRunnableImpl implements Runnable{
private int ticketNum=1000;
@Override
public void run() {
while(ticketNum>0){
sellTickets();
}
}
public synchronized void sellTickets() {
//判斷
if(ticketNum>0){
ticketNum--;
System.out.println(Thread.currentThread().getName()+"售出一張票,剩餘:"
+ticketNum);
try{
Thread.sleep(20);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
public static void main(String[] args){
TicketRunnableImpl t=new TicketRunnableImpl();
t.run();
}
}
2.同步代碼塊
即有synchronized關鍵字修飾的語句塊
被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步。
【代碼示例】
//同步代碼塊
package Thread;
public class TicketRunnableImpl2 implements Runnable{
private int ticketNum=1000;
@Override
public void run() {
while(ticketNum>0){
//同步代碼塊
synchronized (this) {
if(ticketNum>0){
ticketNum--;
System.out.println(Thread.currentThread().getName()+"售出一張票,剩餘:"
+ticketNum);
try{
Thread.sleep(20);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args){
TicketRunnableImpl2 t=new TicketRunnableImpl2();
t.run();
}
}
3.使用重用鎖(對象鎖)實現線程同步
在JavaSE5.0中新增了一個java.util.concurrent包來支持同步。
ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,
它與使用synchronized方法和快具有相同的基本行爲和語義,並且擴展了其能力
ReenreantLock類的常用方法有:
- ReentrantLock() : 創建一個ReentrantLock實例
- lock() : 獲得鎖
- unlock() : 釋放鎖
注:ReentrantLock()還有一個可以創建公平鎖的構造方法,但由於能大幅度降低程序運行效率,不推薦使用 ,使用該類注意及時釋放鎖。
【代碼示例】
//重用鎖
package Thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketRunnableImpl3 implements Runnable{
private int ticketNum=1000;
//創建鎖對象
Lock lock=new ReentrantLock();
@Override
public void run() {
while(ticketNum>0){
//上鎖
lock.lock();
//判斷
if(ticketNum>0){
ticketNum--;
System.out.println(Thread.currentThread().getName()+"售出一張票,剩餘:"
+ticketNum);
try{
Thread.sleep(20);
}catch(InterruptedException e){
e.printStackTrace();
}
}
//解鎖
lock.unlock();
}
}
public static void main(String[] args){
//創建接口實現類實例化對象
Runnable t=new TicketRunnableImpl3();
//創建線程
Thread t1=new Thread(t,"窗口一");
Thread t2=new Thread(t,"窗口二");
Thread t3=new Thread(t,"窗口三");
t1.start();
t2.start();
t3.start();
}
}
4.使用局部變量實現線程同步
如果使用ThreadLocal管理變量,則每一個使用該變量的線程都獲得該變量的副本,副本之間相互獨立,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產生影響。
- ThreadLocal() : 創建一個線程本地變量
- get() : 返回此線程局部變量的當前線程副本中的值
- initialValue() : 返回此線程局部變量的當前線程的"初始值"
- set(T value) : 將此線程局部變量的當前線程副本中的值設置爲value
【代碼示例】
//局部變量
package Thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketRunnableImpl4 implements Runnable{
private volatile int ticketNum=1000;
@Override
public void run() {
while(ticketNum>0){
//判斷
if(ticketNum>0){
ticketNum--;
System.out.println(Thread.currentThread().getName()+"售出一張票,剩餘:"
+ticketNum);
try{
Thread.sleep(20);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
public static void main(String[] args){
//創建接口實現類實例化對象
Runnable t=new TicketRunnableImpl4();
//創建線程
Thread t1=new Thread(t,"窗口一");
Thread t2=new Thread(t,"窗口二");
Thread t3=new Thread(t,"窗口三");
t1.start();
t2.start();
t3.start();
}
}
5.使用特殊域變量(volatile)實現線程同步
- a.volatile關鍵字爲域變量的訪問提供了一種免鎖機制。
- b.使用volatile修飾域相當於告訴虛擬機該域可能會被其他線程更新。因此每次使用該域就要重新計算,而不是使用寄存器中的值
- c.volatile不會提供任何原子操作,它也不能用來修飾final類型的變量
【代碼示例】
package Thread;
public class TicketRunnableImpl5 implements Runnable{
private static ThreadLocal<Integer> ticketNum=new ThreadLocal<Integer>(){
@Override
protected Integer initialValue(){
return 1000;
}
};
@Override
public void run() {
while(ticketNum.get()>0){
//判斷
if(ticketNum.get()>0){
ticketNum.set(ticketNum.get()-1);
System.out.println(Thread.currentThread().getName()+"售出一張票,剩餘:"
+ticketNum.get());
try{
Thread.sleep(20);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
public static void main(String[] args){
//創建接口實現類實例化對象
Runnable t=new TicketRunnableImpl5();
//創建線程
Thread t1=new Thread(t,"窗口一");
Thread t2=new Thread(t,"窗口二");
Thread t3=new Thread(t,"窗口三");
t1.start();
t2.start();
t3.start();
}
}
三、線程如何終止?
終止一個線程通常意味着在線程處理任務完成之前停掉正在做的操作,也就是放棄當前操作。
在Java中有以下三種方法可以終止正在運行的線程:
1.使用退出標誌,使線程正常退出,也就是當run()方法完成後線程終止。
2.使用stop()方法強行終止線程,但是不推薦該方法,該方法已經被棄用。
3.使用interrupt方法終止線程。
1.使用標誌位終止線程
在 run() 方法執行完畢後,該線程就終止了。但是在某些特殊的情況下,run() 方法會被一直執行;比如在服務端程序中可能會使用 while(true) { … } 這樣的循環結構來不斷的接收來自客戶端的請求。此時就可以用修改標誌位的方式來結束 run() 方法。
【代碼示例】
package Thread;
public class ThreadFlag extends Thread{
public volatile boolean exit=false;
@Override
public void run(){
while(!exit){
System.out.println("aaa");
}
}
public static void main(String[] args) throws Exception{
ThreadFlag thread =new ThreadFlag();
thread.start();
sleep(5000);
thread.exit=true;
//join()方法的作用是調用線程等待該線程完成後,才能繼續向下運行
thread.join();
System.out.println("線程退出!");
}
}
2.使用stop方法強制終止線程(已棄用)
使用stop方法可以強行終止正在運行或掛起的線程。我們可以使用如下的代碼來終止線程:
thread.stop();
爲什麼棄用stop:
1.調用 stop() 方法會立刻停止 run() 方法中剩餘的全部工作,包括在 catch 或 finally 語句中的,並拋出ThreadDeath異常(通常情況下此異常不需要顯示的捕獲),因此可能會導致一些清理性的工作的得不到完成,如文件,數據庫等的關閉。
2.調用 stop() 方法會立即釋放該線程所持有的所有的鎖,導致數據得不到同步,出現數據不一致的問題。
3.使用Interrupt中斷線程
使用interrupt方法來終端線程可分爲兩種情況:
(1)線程處於阻塞狀態,如使用了sleep方法。
【代碼示例】
package Thread;
public class ThreadInterrupt extends Thread{
@Override
public void run(){
try{
sleep(5000);
}
catch(InterruptedException e){
System.out.println(e.getMessage());
}
}
public static void main(String[] args) throws Exception{
Thread thread=new ThreadInterrupt();
thread.start();
System.out.println("在50秒內按任意鍵中斷線程");
System.in.read();
thread.interrupt();
thread.join();
System.out.println("線程已退出");
}
}
(2)使用while(!isInterrupted()){……}來判斷線程是否被中斷。
【代碼示例】
package Thread;
public class ThreadInterrupt2 extends Thread{
@Override
public void run(){
for(int i=0;i<100000;i++){
while(!isInterrupted()){
break;}
System.out.println("i="+i);
}
}
public static void main(String[] args) throws Exception{
Thread thread=new ThreadInterrupt2();
thread.start();
thread.sleep(5000);
thread.interrupt();
thread.join();
System.out.println("線程已退出");
}
}
在第一種情況下使用interrupt方法,sleep方法將拋出一個InterruptedException例外,而在第二種情況下線程將直接退出。
注意:在Thread類中有兩個方法可以判斷線程是否通過interrupt方法被終止。一個是靜態的方法interrupted(),一個是非靜態的方法isInterrupted(),這兩個方法的區別是interrupted用來判斷當前線是否被中斷,而isInterrupted可以用來判斷其他線程是否被中斷。因此,while (!isInterrupted())也可以換成while (!Thread.interrupted())。