Java操作Shell脚本 + Java.lang.Process的原理分析 + 进程与线程的分析 + 多线程理解

目录

java程序中要执行linux命令主要依赖2个类:Process和Runtime

进程线程的关系

什么是进程

什么是线程

进程与线程场景分析与理解(看图说话)

process类(官方文档)

 如何创建Process对象

例子-1-列出所有的进程信息

例子-2-解决进程无限阻塞

Runtime

例子-3-简单的调用shell  (String command)

例子-2-简单的调用shell   (String[] cmdarray) 

总结:

参考链接


 

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):  

编程思想之多线程与多进程系列(上)---importnew

Java 中的进程与线程---IBM


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的(深入解释这个类)

  1. Runtime.getRuntime()可以取得当前JVM的运行时环境,这也是在Java中唯一一个得到运行时环境的方法。
  2. Runtime上其他大部分的方法都是实例方法,也就是说每次进行运行时调用时都要用到getRuntime方法。
  3. Runtime中的exit方法是退出当前JVM的方法,估计也是唯一的一个吧,因为我看到System类中的exit实际上也是通过调用 Runtime.exit()来退出JVM的,这里说明一下Java对Runtime返回值的一般规则(后边也提到了),0代表正常退出,非0代表异常中 止,这只是Java的规则,在各个操作系统中总会发生一些小的混淆。
  4. Runtime.addShutdownHook()方法可以注册一个hook在JVM执行shutdown的过程中,方法的参数只要是一个初始化过但是没有执行的Thread实例就可以。(注意,Java中的Thread都是执行过了就不值钱的哦)
  5. 说到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,表示调用命令将在当前程序执行的目录中执行,因此调用到其他目录中的文件和脚本最好使用绝对路径。各个参数的含义:

  1. cmdarray: 包含所调用命令及其参数的数组。 
  2. command: 一条指定的系统命令。
  3. envp: 字符串数组,其中每个元素的环境变量的设置格式为name=value;如果子进程应该继承当前进程的环境,则该参数为 null。
  4. 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);  

 

 

例子-3-鸡肋

 


总结:

       比如说你在脚本或者命令行中开了一个记事本的程序, 但是没有关闭, 那么就意味着子线程就没有结束!!!!!

  • Runtime.getRuntime().exec()这种调用方式在java虚拟机中是十分消耗资源的,即使命令可以很快的执行完毕,频繁的调用时创建进程消耗十分客观。
  • java虚拟机执行这个命令的过程是,首先克隆一条和当前虚拟机拥有一样环境变量的进程,再用这个新的进程执行外部命令,最后退出这个进程。频繁的创建对CPU和内存的消耗很大
  • shell脚本权限问题, shell文件格式问题
  • 调用runtime去执行脚本的时候,其实就是JVM开了一个子线程去调用JVM所在系统的命令,其中开了三个通道: 输入流、输出流、错误流,其中输出流就是子线程走调用的通道。
  • waitFor是等待子线程执行命令结束后才执行, 但是在runtime中,打开程序的命令如果不关闭,就不算子线程结束
  • process的阻塞     在runtime执行大点的命令中,输入流和错误流会不断有流进入存储在JVM的缓冲区中,如果缓冲区的流不被读取被填满时,就会造成runtime的阻塞。所以在进行比如:大文件复制等的操作时,我们还需要不断的去读取JVM中的缓冲区的流,来防止Runtime的死锁阻塞

 

参考链接

(基本上都写在上面了)

进程线程关系的图参考地址

process类中的代码参考地址

一个简单的java执行shell例子

java调用shell并传参(TODO)

比较好看(TODO)

秦老师的理解

全是代码(TODO)(里面有使用ProcessBuilder的代码)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值