并行


1.进程和线程

我们都知道,程序指令的执行依赖于CPU,假设计算机中只有一个CPU,它是如何运行多个程序的呢?

如果计算机上没有安装任何操作系统,那么CPU实际上只能运行一个程序,这个程序独占整个CPU的运行时间。我们现在用的Window,被称之为多任务的操作系统,它内部有一个模块称之为调度器(scheduler),由它来规划CPU运行每个任务的时间,让CPU时间轮流分配给不同的程序去使用。

宏观上,因为分配切换的速度很快,就造成了并行的假象。

下图表示三个进程轮流使用CPU的情况,-表示进程内的一段代码

进程1 --        --
进程2   --  --    --  --
进程3     --  --    --

进程

因为物理内存只有一份,每个程序要使用自己独立的内存空间,给程序编写带来复杂性,因此从硬件和操作系统层面都增加了抽象,让应用程序使用虚拟地址,虚拟地址再与物理地址进行映射。

当一个程序运行起来,操作系统为其分配一个独立的虚拟地址空间,这时就启动了一个进程,Window 系统下可以使用任务管理器来查看当前运行的所有进程,Linux 系统下可以采用ps命令来查看当前运行的所有进程。

线程

现代的操作系统,由于安全性方面的考虑,是不允许进程之间直接通信的。那么一个进程内如果想实现并行的效果,轮流使用CPU时间,就需要使用线程。

线程与进程类似,线程内的代码同样可以由调度器分配CPU时间片,达到宏观并行的效果。

进程 vs 线程

  • 它们都可以被CPU调度,让代码并行运行
  • 一个进程可以包含一到多个线程
  • 进程有着自己独立的地址空间,而线程共享进程内的地址空间
  • 进程间的相互访问比较麻烦,两个java程序进程之间传递数据,调用代码时必须用到socket或rpc;而java进程与其他进程之间访问,通过http或web service才能完成
  • 而线程间轻易访问到进程内共享的数据(java中堆就属于共享空间)

2.Java中的线程

Java中将线程进行了抽象为java.lang.Thread类,而对线程中要运行的任务则抽象为java.lang.Runnable接口

创建线程

方法1:

Thread t = new Thread(new Runnable(){
   public void run() {
      // 要并行执行的代码
   }
});

方法2:因为Thread类自己也实现了Runnable接口,因此生成一个Thread的子类,重写run方法也是可以的:

Thread t = new Thread(){
   public void run() {
      // 要并行执行的代码
   }
};

方法3:如果使用JDK8,可以用lambda表达式简化Runnable的写法:

Thread t = new Thread(()-> { // 要并行执行的代码 });

启动线程

一个线程对象被创建后,并没有立刻运行Runnable中的代码,需要执行线程对象的start()方法:

t.start();

start() 方法的作用是让线程进入就绪状态,接下来才会被调度器交给CPU调度执行。

线程常用方法

静态方法:

Thread.currentThread() // 获取当前线程
Thread.sleep(long n)  // 让当前线程休眠n毫秒

实例方法:

t.getName() // 获取线程名
t.join() // 让当前线程等待t线程运行结束
t.join(long n) // 让当前线程等待t线程运行结束,最多等待n毫秒
t.interrupt() // 打断正在wait(), sleep(), 或join() 的t线程

线程池

在开发Web应用时,Web服务器需要同时处理大量请求,显然这多个请求希望达到被并行处理的效果。一种想当然的做法是来一个请求,就为其创建一个线程来处理这个请求。Web请求的特点是时间短,请求量密集,但实际线程的创建和销毁的时间也是相当昂贵的,频繁创建线程会影响整个系统的效率。

解决方法是,采用线程池的思想,即预先创建好一批线程,轮流执行任务,每个线程执行完一个任务后,并没有结束,而是进入空闲状态,可以由线程池继续分派任务给它执行。

线程池的好处是:实现了线程对象的复用

Java中使用线程池的例子:

ExecutorService service = new ThreadPoolExecutor(coreSize,maxSize,keepAliveTime,TimeUnit,BlockingQueue);

serivce.execute(new Runnable(){ 
  public void run(){
  }
});

其中,coreSize是线程池的基本线程数,也可以理解为最小保留的线程数;

如果高峰期任务量比较大,超过了coreSize,那么超过的任务会被放入阻塞队列BlockingQueue缓冲起来,排队等待执行;

maxSize是线程池的最大线程数,当任务非常密集,不仅超过了基本线程数,并且连Queue都放满了,那么线程池会新建一些线程来救急,新建的线程数+基本线程数不会超过maxSize;

这些新建的线程在任务高峰期过去后,一般没必要保留,让它们结束就好,因此会有keepAliveTime来决定它们在任务结束后会保留多长时间,TimeUnit是时间单位。

results matching ""

    No results matching ""