并行
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是时间单位。