最初计算机是单任务的,然后发展到多任务,接着出现多线程并行,同时计算机也从单cpu进入到多cpu。如下图:
多任务:其实就是利用操作系统时间片轮转使用的原理。操作系统通过将cpu的执行时间分割成多个时间片,为每个任务分配时间片,因为cpu处理速度很快,这样就用户看来好像每个任务都在同时执行,感觉有多个cpu,但本质上一个时间点只有一个任务在运行。
随着多核多线程的出现,我们可以更好的利用资源但是同时也面临着更多的多线程编程挑战。
并行编程的好处:
- 提高资源利用率,提升程序运行时间---cpu的就是利用率
- 提高程序响应速度,比如用户界面的点击按钮,就是使用多线程,服务器收到用户点击请求,将这个请求交于一个新线程(worker)去执行,这样服务器就可以继续等待用户的输入请求,否则服务器在处理上一个请求的时候是无法响应当前用户的请求的。
并行编程的代价和挑战:
- 增加内存的消耗。
- 上下文的切换会消耗额外内存,从一个线程切换到另一个线程,需要记录当前线程的数据变量,指针等,然后执行另一个线程。
- 内存数据的同步,锁,通信等问题。
线程池(ThreadPool):
我想大部分人在听到这个东西的时候会感觉很神奇,但其实ThreadPool特别简单。线程池就是我们通过人工或者手动设置内存当中线程数的数量,使得程序可以最优运行。简单理解就是这样:我们设置一个线程池的大小,比如线程池的数量为10,那么当有线程任务来临的时候我们就使用线程池的线程去执行这个任务,如果线程池的10个线程都在执行任务,就把这个任务加到等待队列,等候其他线程运行结束后再执行。
使用线程池的好处:
- 降低资源消耗,通过重复利用在线程池中已创建好的线程执行任务,减少创建、销毁线程的内存和时间开销。
- 响应速度快,因为直接使用已创建好的线程执行任务,而不是去创建线程,所以响应时间快。
- 可以更好的管理线程,因为线程是稀有资源,避免随意创建线程(一个线程要暂用1M的内存左右)
很多问题我们使用顺序编程便可以解决,但是有些问题如果能够使用多线程并行的执行其中的任务则可以很大程度的提高时间效率,所以多线程还是很有必要的。
我自己总结了JAVA并行的3个发展阶段(菜鸟总结,请体谅)
第一阶段:Thread ,Runable
第二阶段:ExecutorService执行器
第三阶段:ForkJoin并行框架(其实就是ExecutorService的升级应用而已)
并发很大一方面是为了提高程序运行速度,如果想要一个程序运行的更快,那么可以将其分为多个片段,然后在每个单独的处理器上运行多个任务。现在是多核时代我们应该掌握。
但是我们知道并发通常是用在提高单核机器的程序性能上,这个咋一听感觉有点不能理解,学过操作系统的人应该知道,从一个任务切换到另一个任务是会有上下文开销的。但是因为有了“阻塞”,使得这个问题变得有些不同。
“阻塞”通常指的是一个程序的某个任务由于要执行程序控制之外的事,比如请求I/O资源,由于需要等待请求的I/O资源,所以会导致程序的暂停,就是cpu空闲。我们知道cpu是很宝贵的资源,我们应当充分利用它才对,这时候多线程就出来了,想想啊,当某个线程阻塞导致cpu空闲,这时候操作系统就将cpu分配给其他等待的线程使得cpu无时无刻不在运行。单个进程可以有多个并发执行的任务,我们感觉好像每个任务都有自己的cpu一样,其底层机制就是切分cpu时间,通常来说不需要我们管。
从事实上来看,如果程序没有任何阻塞,那么在单处理器上的并发是没有意义的。
(1)传统的并发编程采用Thread类
- 创建Thread类子例并重写run方法
- 编写类的时候实现Runnable接口方法,也是使用run方法。
public class App {
public static class demo extends Thread
{
int x;
public demo(int x)
{
this.x=x;
}
public void run() {
System.out.print("线程"+x);
}
}
public static void main(String[] args) {
demo dem=new demo(1);
dem.start();
}
}
public class CommonRunnable implements Runnable{
public void run() {
System.out.println("MyRunnable running");
}
}
无论是Thread还是Runable其实都只要我们覆盖实现Run方法就好了,但是由于java类只能继承一次而接口可以有无数个所以我们更经常使用Ruanble接口。我们调用新线程都是使用start()方法而不是run()方法。
start方法的本质:从cpu中申请另一个线程空间来执行run方法,这样是并发线程。(其实它也是会自己调用run里面的方法,但是如果我们直接调用run方法的话,那么就是单线程而已)
以上两种虽然可以实现基本的并行结构,但是对于复杂的问题就会很麻烦,因此就有了在jdk5里面引入的Excutor执行器,其实就是实现线程池。
(2)ExecutorService执行器
是指java 5中引入的一系列并发库中与executor相关的一些功能类,其中包括线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。
Executor用来管理Runable对象的执行。用来创建并管理一组Runable对象的线程,这组线程就叫做线程池(Thread pool)
并发编程的一种编程方式是把任务拆分为一些列的小任务,即Runnable,然后在提交给一个Executor执行,Executor.execute(Runnalbe) 。Executor在执行时使用内部的线程池完成操作。
提交或者执行任务:
1. execute(Runnable) 无返回值,无法判断一个线程任务是否已经执行完毕
2. submit(Runnable) 会返回一个Future,通过get()判断是否执行完毕
3. submit(Callable) 会返回一个result(自定义的返回值)
在Executor里面。我们可以使用Callable,Future返回结果,Future<V>代表一个异步执行的操作,通过get()方法可以获得操作的结果,如果异步操作还没有完成,则get()会使当前线程阻塞。FutureTask<V>实现了Future<V>和Runable<V>。Callable代表一个有返回值得操作。
public class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum=0;
int begin=(int) (Math.random()*10); //产生0-10的双精度随机数
for(int i=0;i<begin;i++)
{
sum+=i;
}
return sum;
}
}
public class test {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
ThreadPoolExecutor myExecutor = new ThreadPoolExecutor(3, 10, 200,TimeUnit.SECONDS,new LinkedBlockingDeque<Runnable>());
List<Future<Integer>> results = new ArrayList<Future<Integer>>();
for(int i=0;i<5;i++) {
Task task=new Task();
Future<Integer> result = null;
result=myExecutor.submit(task);
results.add(result);
}
for (Future<Integer> f : results) {
try {
System.out.println(f.get());
} catch (Exception ex) {
// ex.printStackTrace();
f.cancel(true);
}
}
myExecutor.shutdown();
}
}
结束任务:
Shutdown : 中断所有没有正在执行的任务,等待当前正在执行的线程结束然后关闭
ShutdowmNow: 遍历线程池中的所有线程任务,然后中断它们
上面并发执行的挺好的,但是有个问题。不同的线程执行有块有慢,有的任务会提早执行完毕,因此为了利用这些提早执行完毕的线程,使用了一种工作窃取(work-stealing)算法。
(3)ForkJoin并行框架
Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。是不是很像map/reduce。
在一个任务内,首先检查这个任务的大小,如果它比设定的任务阈值要大,就将这个任务分成两份或者多份。然后再使用框架执行。如果要小就直接解决问题。
Fork/Join 模式有自己的适用范围。如果一个应用能被分解成多个子任务,并且组合多个子任务的结果就能够获得最终的答案,那么这个应用就适合用 Fork/Join 模式来解决。ForkJoin是将一个问题递归分解成子问题,再将子问题并行运算后合并结果。
让我们通过一个简单的需求来使用下Fork/Join框架,需求是:计算1+2+3+4的结果。
使用Fork/Join框架首先要考虑到的是如何分割任务,如果我们希望每个子任务最多执行两个数的相加,那么我们设置分割的阈值是2,由于是4个数字相加,所以Fork/Join框架会把这个任务fork成两个子任务,子任务一负责计算1+2,子任务二负责计算3+4,然后再join两个子任务的结果。
因为是有结果的任务,所以必须继承RecursiveTask。
我们只需要关注子任务的划分和中间结果的组合。ForkJoinTask完成子任务的划分,然后将它提交给ForkJoinPool来完成应用。
如果大家有学习集成学习,那么使用Fork/Join来处理对大规模数据的投票是非常好的。比如:
结果查看:可以从下面看到随着分类器个数的增加,使用Fork/Join所提升的时间也是线性增加的。