Java操作Shell指令碼 + Java.lang.Process的原理分析 + 程序與執行緒的分析 + 多執行緒理解
目錄
java程式中要執行linux命令主要依賴2個類:Process和Runtime
程序執行緒的關係
什麼是程序
簡單理解,在多工系統中,每一個獨立執行的程式就是一個程序,也可以理解為當前正在執行的每一個程式都是一個程序。我們當初應用的操縱系統大都是多工系統的,如:Windows、Linux、Mac OS X、Unix等。因為單個CPU在同一時刻只能執行一個程式,這是鐵律。但在系統中單個CPU又怎麼能同時執行多個程式呢?實際情況這是由操縱系統負責對CPU資源停止排程和分配管理的,雖然單個CPU在某一時刻只能做一件事,但是它以非常小的時間間隔切換來執行多個程式,人用弱眼根本無法察覺CPU在往返交替執行多個程式,所以給人以在同一時刻同時執行多個程式的感覺。如果我們同時開啟兩個記事本程式A和B,這就是兩個不同的程序,A編輯的文稿不會影響到B。因為每一個程序都有獨立的程式碼和資料儲存空間,操縱的都是自己空間的資料,所以互不影響。
什麼是執行緒
一個程序中可以包含一個或多個執行緒,一個執行緒就是程式外部的一條執行線索。在單執行緒中,當程式啟動時,就自動發生了一個執行緒,這個執行緒稱為主執行緒。主函式main就是在這個執行緒上執行的,然後主函式按照程式程式碼的呼叫順序依次往下執行。在這類情況下,當主函式呼叫了子函式,主函式必須等待子函式返回以後才能繼承往下執行,不能實現兩段程式碼交替執行的效果。如果要在一個程式中交替執行多段程式碼,就需要發生多個執行緒,並指定每一個執行緒上所要執行的程式程式碼,這就是多執行緒。在Java中建立多執行緒有兩種方法:繼承java.lang.Thread類和實現Runnable介面,並呼叫Thread類的start方法來啟動執行緒。
程序與執行緒場景分析與理解(看圖說話)
- 計算機的核心是CPU,承擔了全部的計算任務。它就好比一座工廠,時刻都在執行。為工廠中的每一個部件提供疏浚與處理的服務。
- 假設這座工廠的電力無限,一次只能供給一個車間應用,也就是說一個車間開工的時候,其它車間都必須停工。當面的意思就是說一個CPU同一時間只能執行一個任務(程序)。
- 程序就好比工廠的車間,任一時刻都只有一個車間在開工出產,其它車間都處於停工狀態。當面的意思就是說,CPU在任一時刻總是隻能執行單個程序,其它程序都處於非活動狀態。
- 一個車間裡可以有很多個工人,它們協同實現一個任務。比如一個手機出產車間,張三負責主機板的安裝與除錯,李四負責顯示屏的測試與加工,王五負責手機零件的組裝等。執行緒就好比這車間裡的工人,一個程序包含了多個執行緒,它們各自負責實現自己的任務。
- 車間裡的空間是工人們共享的,比如車間裡的很多房間(如:加工房、出產房、組裝房等),這些車間裡的每一個房間,工人們都是可以隨意走動、收支的。這就意味者一個程序的記憶體空間是共享的,該程序中的全部執行緒都可以應用這片記憶體空間
- 可是車間裡每間房間的鉅細是不同的。有些房間最多隻能包容1個人,比如茅廁,裡面有人的時候,你就不能再進去了,需要等裡面的人出來了你才能進去。也就是說當一個執行緒在應用某塊共享記憶體的時候,其它執行緒必須等待它應用結束以後,其它執行緒才能應用這塊記憶體。
- 一個訪止他人進入的簡單方法,就是進入茅廁以後,在外面掛一把鎖。先到的人進入茅廁後鎖上門,後到的人看到茅廁上鎖了,就在門口排隊,等鎖打開了再進去。這就是"互斥鎖"(mutex),避免多個執行緒同時讀寫某一塊記憶體區域中的資料。在Java中應用synchronized關鍵字實現多個執行緒之間的互斥。
- 還有些房間,可以同時包容N個人,比如說廚房。如果人數大於N,多出來的人數只能在外面等著,等待其它人出來以後才能進去。這就好比某些共享記憶體區域,只供固定數目的執行緒拜訪
- 這時的處理方式就是在門外掛N把鎖,進去的人就取一把鎖,出來時把鎖掛回原處。後到的人發現鑰匙架空了,就曉得在門外排隊等著了。這類做法叫做“訊號量(Semaphore)”,用來保障多個執行緒不會互相沖突。
更多關於程序如何執行就涉及到作業系統底層了,可參考(TODO):
process類(官方文件)
- 抽象類,封裝了一個程序 ( 呼叫linux的命令或shell指令碼就是為了執行一個在linux下執行的程式, 所以使用這個類 )
- jdk1.5之前啟動和管理程序都必須通過Process類實現,1.5以及之後,通過ProcessBuilder就可以來做了(TODO)
- ProcessBuilder.start() 和 Runtime.exec 方法建立一個本機程序,並返回 Process 子類的一個例項,該例項可用來控制程序並獲取相關資訊
- 建立程序的方法可能無法針對某些本機平臺上的特定程序很好地工作,比如,本機視窗程序,守護程序,Microsoft Windows 上的 Win16/DOS 程序,或者 shell 指令碼。建立的子程序沒有自己的終端或控制檯。它的所有標準 io(即 stdin,stdout,stderr)操作都將通過三個流 (getOutputStream(),getInputStream(),getErrorStream()) 重定向到父程序。父程序使用這些流來提供到子程序的輸入和獲得從子程序的輸出。因為有些本機平臺僅針對標準輸入和輸出流提供有限的緩衝區大小,如果讀寫子程序的輸出流或輸入流迅速出現失敗,則可能導致子程序阻塞,甚至產生死鎖。
- 當沒有 Process 物件的更多引用時,不是刪掉子程序,而是繼續非同步執行子程序。
- 對於帶有 Process 物件的 Java 程序,沒有必要非同步或併發執行由 Process 物件表示的程序
- process類提供了一些方法(直接把原始碼貼上來了) @since JDK1.0 我貼的是Java8
// 建立的子程序沒有自己的終端控制檯,所有標註操作都會通過以下三個流重定向到父程序
//(父程序可通過這些流判斷子程序的執行情況)
// 特別需要注意:如果子程序中的輸入流,輸出流或錯誤流中的內容比較多,最好使用快取(BufferReader)
// 得到程序的輸入流 (輸出流被傳送給由該 Process 物件表示的程序的標準輸入流)
public abstract OutputStream getOutputStream();
// 得到程序的輸出流 (輸入流獲得由該 Process 物件表示的程序的標準輸出流)
// (常用,執行shell後的輸出用它來拿)
public abstract InputStream getInputStream();
// 得到程序的錯誤流 (獲得由該 Process 物件表示的程序的錯誤輸出流傳送的資料)
public abstract InputStream getErrorStream();
// 導致當前執行緒等待,如有必要,一直要等到由該 Process 物件表示的程序已經終止
// 如果已終止該子程序,此方法立即返回。如果沒有終止該子程序,呼叫的執行緒將被阻塞,直到退出子程序
// 0 表示正常終止
public abstract int waitFor() throws InterruptedException;
public boolean waitFor(long timeout, TimeUnit unit)
throws InterruptedException {
long startTime = System.nanoTime();
long rem = unit.toNanos(timeout);
do {
try {
exitValue();
return true;
} catch(IllegalThreadStateException ex) {
if (rem > 0)
Thread.sleep(
Math.min(TimeUnit.NANOSECONDS.toMillis(rem) + 1, 100));
}
rem = unit.toNanos(timeout) - (System.nanoTime() - startTime);
} while (rem > 0);
return false;
}
// 返回子程序的出口值。值0表示正常終止
public abstract int exitValue();
// 殺掉子程序 強制終止此 Process 物件表示的子程序
public abstract void destroy();
public Process destroyForcibly() {
destroy();
return this;
}
public boolean isAlive() {
try {
exitValue();
return false;
} catch(IllegalThreadStateException e) {
return true;
}
}
如何建立Process物件
ProcessBuilder.start() 和 Runtime.exec 方法建立一個本機程序,並返回 Process 子類的一個例項,該例項可用來控制程序並獲得相關資訊。Process 類提供了執行從程序輸入、執行輸出到程序、等待程序完成、檢查程序的退出狀態以及銷燬(殺掉)程序的方法。
1、每個 ProcessBuilder 例項管理一個程序屬性集。start() 方法利用這些屬性建立一個新的 Process 例項。start() 方法可以從同一例項重複呼叫,以利用相同的或相關的屬性建立新的子程序。(ProcessBuilder是1.5的,TODO)
2、Runtime.exec() 方法建立一個本機程序,並返回 Process 子類的一個例項。(我用的)
例子-1-列出所有的程序資訊
//列出所有的程序資訊
public class ListAllProcessTest {
public static void main(String[] args) {
BufferedReader br = null; // 宣告BufferedReader 準備接收輸入流
Process process = null; // 宣告一個Process
try {
process = Runtime.getRuntime().exec("tasklist"); // 呼叫Runtime建立程序
br = new BufferedReader // 獲得輸入流, 用bk緩衝
(new InputStreamReader(process.getInputStream(), "GBK"));
String line = null;
System.out.println("列出所有正在執行的程序資訊:");
while ((line = br.readLine()) != null) { //遍歷列印
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close(); // 關閉流
} catch (Exception e) {
e.printStackTrace();
}
}
if(process!=null){
process.destroy(); //關閉執行緒
}
}
}
}
執行結果
然後是用cmd執行tasklist
例子-2-解決程序無限阻塞
解決程序無限阻塞的方法是在執行命令時,設定一個超時時間,下面提供一個工具類,對Process使用進行包裝,向外提供設定超時的介面。
// ExecuteResult類,對執行命令的結果進行封裝,可以從中獲取退出碼和輸出內容。
public class ExecuteResult {
@Override
public String toString() {
return "ExecuteResult [exitCode=" + exitCode + ", executeOut="
+ executeOut + "]";
}
private int exitCode;
private String executeOut;
public ExecuteResult(int exitCode, String executeOut) {
super();
this.exitCode = exitCode;
this.executeOut = executeOut;
}
public int getExitCode() {
return exitCode;
}
public void setExitCode(int exitCode) {
this.exitCode = exitCode;
}
public String getExecuteOut() {
return executeOut;
}
public void setExecuteOut(String executeOut) {
this.executeOut = executeOut;
}
}
// LocalCommandExecutorService 介面,向外暴露executeCommand()方法
public interface LocalCommandExecutorService {
ExecuteResult executeCommand(String[] command, long timeout);
}
// LocalCommandExecutorServiceImpl 實現類,實現LocalCommandExecutorService 介面的方法
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.*;
public class LocalCommandExecutorServiceImpl implements
LocalCommandExecutorService {
static final Logger logger = LoggerFactory
.getLogger(LocalCommandExecutorServiceImpl.class);
static ExecutorService pool = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
3L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
@Override
public ExecuteResult executeCommand(String[] command, long timeout) {
Process process = null;
InputStream pIn = null;
InputStream pErr = null;
StreamGobbler outputGobbler = null;
StreamGobbler errorGobbler = null;
Future<Integer> executeFuture = null;
try {
process = Runtime.getRuntime().exec(command);
final Process p = process;
//close process's output stream.
p.getOutputStream().close();
pIn = process.getInputStream();
outputGobbler = new StreamGobbler(
pIn, "OUTPUT");
outputGobbler.start();
pErr = process.getErrorStream();
errorGobbler = new StreamGobbler(pErr, "ERROR");
errorGobbler.start();
// create a Callable for the command's Process which can be called
// by an Executor
Callable<Integer> call = new Callable<Integer>() {
public Integer call() throws Exception {
p.waitFor();
return p.exitValue();
}
};
// submit the command's call and get the result from a
executeFuture = pool.submit(call);
int exitCode = executeFuture.get(timeout,
TimeUnit.MILLISECONDS);
return new ExecuteResult(exitCode, outputGobbler.getContent());
} catch (IOException ex) {
String errorMessage = "The command [" + command
+ "] execute failed.";
logger.error(errorMessage, ex);
return new ExecuteResult(-1, null);
} catch (TimeoutException ex) {
String errorMessage = "The command [" + command + "] timed out.";
logger.error(errorMessage, ex);
return new ExecuteResult(-1, null);
} catch (ExecutionException ex) {
String errorMessage = "The command [" + command
+ "] did not complete due to an execution error.";
logger.error(errorMessage, ex);
return new ExecuteResult(-1, null);
} catch (InterruptedException ex) {
String errorMessage = "The command [" + command
+ "] did not complete due to an interrupted error.";
logger.error(errorMessage, ex);
return new ExecuteResult(-1, null);
} finally {
if(executeFuture != null){
try{
executeFuture.cancel(true);
} catch(Exception ignore){}
}
if(pIn != null) {
this.closeQuietly(pIn);
if(outputGobbler != null && !outputGobbler.isInterrupted()){
outputGobbler.interrupt();
}
}
if(pErr != null) {
this.closeQuietly(pErr);
if(errorGobbler != null && !errorGobbler.isInterrupted()){
errorGobbler.interrupt();
}
}
if (process != null) {
process.destroy();
}
}
}
private void closeQuietly(Closeable c) {
try {
if (c != null)
c.close();
} catch (IOException e) {
}
}
}
// StreamGobbler類,用來包裝輸入輸出流
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class StreamGobbler extends Thread {
private static Logger logger = LoggerFactory.getLogger(StreamGobbler.class);
private InputStream inputStream;
private String streamType;
private StringBuilder buf;
private volatile boolean isStopped = false;
/**
* Constructor.
*
* @param inputStream
* the InputStream to be consumed
* @param streamType
* the stream type (should be OUTPUT or ERROR)
* @param displayStreamOutput
* whether or not to display the output of the stream being
* consumed
*/
public StreamGobbler(final InputStream inputStream, final String streamType) {
this.inputStream = inputStream;
this.streamType = streamType;
this.buf = new StringBuilder();
this.isStopped = false;
}
/**
* Consumes the output from the input stream and displays the lines
* consumed if configured to do so.
*/
@Override
public void run() {
try {
//預設編碼為UTF-8,這裡設定編碼為GBK,因為WIN7的編碼為GBK
InputStreamReader inputStreamReader = new InputStreamReader(
inputStream,"GBK");
BufferedReader bufferedReader = new BufferedReader(
inputStreamReader);
String line = null;
while ((line = bufferedReader.readLine()) != null) {
this.buf.append(line + "\n");
}
} catch (IOException ex) {
logger.trace("Failed to successfully consume and display the input stream of type "
+ streamType + ".", ex);
} finally {
this.isStopped = true;
synchronized (this) {
notify();
}
}
}
public String getContent() {
if(!this.isStopped){
synchronized (this) {
try {
wait();
} catch (InterruptedException ignore) {
}
}
}
return this.buf.toString();
}
}
// 測試用例
public class LocalCommandExecutorTest {
public static void main(String[] args) {
LocalCommandExecutorService service = new LocalCommandExecutorServiceImpl();
String[] command = new String[]{"ping","127.0.0.1"};
ExecuteResult result = service.executeCommand(command, 5000);
System.out.println("退出碼:"+result.getExitCode());
System.out.println("輸出內容:"+result.getExecuteOut());
}
}
程式執行結果
cmd中 ping 127.0.0.1
Apache提供了一個開源庫,對Process類進行了封裝,也提供了設定超時的功能,
建議在專案中使用Apache Commons Exec這個開源庫來實現超時功能,除了功能更強大外,穩定性也有保障。(TODO)
Runtime
實際上前面扯了那麼多其實就是建立一個程序, 程序幹啥還是用這個來配置, 當然如果用ProcessBuilder我還沒看, 可能就不用這個了.
RunTime.getRuntime().exec() 方法
Runtime類, 他是一個與JVM執行時環境有關的類,這個類是Singleton的(深入解釋這個類)
- Runtime.getRuntime()可以取得當前JVM的執行時環境,這也是在Java中唯一一個得到執行時環境的方法。
- Runtime上其他大部分的方法都是例項方法,也就是說每次進行執行時呼叫時都要用到getRuntime方法。
- Runtime中的exit方法是退出當前JVM的方法,估計也是唯一的一個吧,因為我看到System類中的exit實際上也是通過呼叫 Runtime.exit()來退出JVM的,這裡說明一下Java對Runtime返回值的一般規則(後邊也提到了),0代表正常退出,非0代表異常中 止,這只是Java的規則,在各個作業系統中總會發生一些小的混淆。
- Runtime.addShutdownHook()方法可以註冊一個hook在JVM執行shutdown的過程中,方法的引數只要是一個初始化過但是沒有執行的Thread例項就可以。(注意,Java中的Thread都是執行過了就不值錢的哦)
- 說到addShutdownHook這個方法就要說一下JVM執行環境是在什麼情況下shutdown或者abort的。文件上是這樣寫的,當最後一個非 精靈程序退出或者收到了一個使用者中斷訊號、使用者登出、系統shutdown、Runtime的exit方法被呼叫時JVM會啟動shutdown的過程, 在這個過程開始後,他會並行啟動所有登記的shutdown hook(注意是並行啟動,這就需要執行緒安全和防止死鎖)。當shutdown過程啟動後,只有通過呼叫halt方法才能中止shutdown的過程並退 出JVM。
Process exec(String command)
在單獨的程序中執行指定的字串命令。
Process exec(String[] cmdarray)
在單獨的程序中執行指定命令和變數。
Process exec(String command, String[] envp)
在指定環境的單獨程序中執行指定的字串命令。
Process exec(String[] cmdarray, String[] envp)
在指定環境的單獨程序中執行指定命令和變數。
Process exec(String command, String[] envp, File dir)
在指定環境和工作目錄的獨立程序中執行指定的字串命令。
Process exec(String[] cmdarray, String[] envp, File dir)
在指定環境和工作目錄的獨立程序中執行指定的命令和變數。
其中,其實cmdarray和command差不多,同時如果引數中如果沒有envp引數或設為null,表示呼叫命令將在當前程式執行的環境中執行;如果沒有dir引數或設為null,表示呼叫命令將在當前程式執行的目錄中執行,因此呼叫到其他目錄中的檔案和指令碼最好使用絕對路徑。各個引數的含義:
- cmdarray: 包含所呼叫命令及其引數的陣列。
- command: 一條指定的系統命令。
- envp: 字串陣列,其中每個元素的環境變數的設定格式為name=value;如果子程序應該繼承當前程序的環境,則該引數為 null。
- dir: 子程序的工作目錄;如果子程序應該繼承當前程序的工作目錄,則該引數為 null。
可以通過呼叫Process類的 waitFor() 檢視是否執行完畢(看上面的Process)
例子-3-簡單的呼叫shell (String command)
public class RunShell {
public static void main(String[] args){
try {
String shpath="/home/hello-java-shell.sh";
Process ps = Runtime.getRuntime().exec(shpath);
ps.waitFor();
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
String result = sb.toString();
System.out.println(result);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
打成可執行jar包扔到linux下執行, 並且該shell指令碼有可執行許可權
其實就是Process類進行呼叫,然後把shell的執行結果輸出到控制檯下
需要注意
- 在呼叫時需要執行waitFor()函式,因為shell程序是JAVA程序的子程序,JAVA作為父程序需要等待子程序執行完畢。
- 控制檯輸出時並不是邊執行邊輸出,而是shell全部執行完畢後輸出,所以如果執行較為複雜的shell指令碼看到沒有輸出時可能會誤以為沒有執行,這個時候看看終端裡面的程序,TOP命令一下就能看到其實shell指令碼已經開始執行了。
例子-2-簡單的呼叫shell (String[] cmdarray)
public class test {
public static void main(String[] args){
InputStream in = null;
try {
Process pro = Runtime.getRuntime().exec(new String[]{"sh",
"/home/test/test.sh","select admin from M_ADMIN",
"/home/test/result.txt"});
pro.waitFor();
in = pro.getInputStream();
BufferedReader read = new BufferedReader(new InputStreamReader(in));
String result = read.readLine();
System.out.println("INFO:"+result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
指令碼如下
#!/bin/sh
#查詢sql
SQL=$1
#查詢結果儲存檔案
RESULT_FILE=$2
#資料庫連線
DB_NAME=scott
DB_PWD=tiger
DB_SERVER=DB_TEST
RESULT=`sqlplus -S ${DB_NAME}/${DB_PWD}@${DB_SERVER}<< !
set heading off
set echo off
set pages 0
set feed off
set linesize 3000
${SQL}
/
commit
/
!`
echo "${RESULT}" >> ${RESULT_FILE}
echo 0;
特別需要注意的是,當需要執行的linux命令帶有管道符時(例如:ps -ef|grep java),用上面的方法是不行的,解決方式是將需要執行的命令作為引數傳給shell
String[] cmds = {"/bin/sh","-c","ps -ef|grep java"};
Process pro = Runtime.getRuntime().exec(cmds);
總結:
比如說你在指令碼或者命令列中開了一個記事本的程式, 但是沒有關閉, 那麼就意味著子執行緒就沒有結束!!!!!
- Runtime.getRuntime().exec()這種呼叫方式在java虛擬機器中是十分消耗資源的,即使命令可以很快的執行完畢,頻繁的呼叫時建立程序消耗十分客觀。
- java虛擬機器執行這個命令的過程是,首先克隆一條和當前虛擬機器擁有一樣環境變數的程序,再用這個新的程序執行外部命令,最後退出這個程序。頻繁的建立對CPU和記憶體的消耗很大
- shell指令碼許可權問題, shell檔案格式問題
- 呼叫runtime去執行指令碼的時候,其實就是JVM開了一個子執行緒去呼叫JVM所在系統的命令,其中開了三個通道: 輸入流、輸出流、錯誤流,其中輸出流就是子執行緒走呼叫的通道。
- waitFor是等待子執行緒執行命令結束後才執行, 但是在runtime中,開啟程式的命令如果不關閉,就不運算元執行緒結束
- process的阻塞 在runtime執行大點的命令中,輸入流和錯誤流會不斷有流進入儲存在JVM的緩衝區中,如果緩衝區的流不被讀取被填滿時,就會造成runtime的阻塞。所以在進行比如:大檔案複製等的操作時,我們還需要不斷的去讀取JVM中的緩衝區的流,來防止Runtime的死鎖阻塞
參考連結
(基本上都寫在上面了)
比較好看(TODO)
全是程式碼(TODO)(裡面有使用ProcessBuilder的程式碼)