参考资料:《Java编程思想》
一、什么是并发?
生活中大多的问题可以通过顺序执行来完成,但是某些时候,并发是必须的,且可提高效率,例如:火车站买票的问题,可以在一个窗口完成售票,大家都选择排队,这个没有什么问题。但是效率会令人崩溃,如果开设多个售票窗口,同时进行售票操作,大家并行购票,极大的提高了效率,这个就是并发的优势之一。
二、并发的多面性
1、正如上面举的例子,并发可以提高效率,在程序中使用并发,可以让程序更快的执行。【单处理器上运行并发程序的开销确实要比该程序的所有部分都顺序执行的开销要大,因为增加了上下文的切换】,但是另一个问题是阻塞,如果程序中的某个过程因为某些条件(通常是IO)而不能继续执行的话,当前任务就会阻塞,如果没有并发,那么整个程序都会停止下来,直至外部条件发生变化。但是如果我们的程序是支持并发的,那么除了阻塞任务,其他的部分依然可以继续执行。
事实上,从性能的角度来看,如果没有任务会阻塞,那么在单处理器机器上使用并发就没有任何意义
2、代码设计的改进,在单CPU机器上使用多任务的程序在任意时刻,仍旧只是在执行一项工作。在生活中,我们都见过仿真的场景,例如计算机游戏或者电影中计算机生成的动画效果。仿真涉及到许多交互元素,每一个都有其自己的想法。从编程的角度,模拟每个仿真元素的都有其自己的处理器并且都是独立的任务,这种方式要容易的多
三、基本的线程机制
1、概要:并发编程可以让我们将程序划分为多个分离的,独立运行的任务,通过使用多线程机制,这些独立任务(也可以称为子任务)中的每一个都将由执行线程来驱动,一个线程就是在晋城中的一个单一的顺序控制流。所以呢,单个进程可以拥有多个并发执行的任务,但是你的程序使得每个任务都好像拥有自己的CPU 一样,其底层机制是切分CUP时间,但是我们通常不用考虑
2、定义任务:线程可以驱动任务,因此我们需要定义一种描述任务的方式,这个可以通过Runnable接口来提供,要想定义一个任务,只需要实现Runnable接口并编写run()方法,是的该任务可以执行你的命令。例如
/**
* 定义一个任务
*/
public class LiftOff implements Runnable{
protected int countDown = 10;
// 标识有多少个任务
private static int taskCount = 0;
// 标识符id 可以区分任务的多个实例,因为是final的,一旦创建之后,无法更改
private final int id = taskCount ++;
public LiftOff(){};
public LiftOff(int countDown){
this.countDown = countDown;
}
public String status(){
return "#" + id + "(" + (countDown >0 ? countDown : "LiftOff!") + "),";
}
@Override
public void run() {
while (countDown -- >0){
System.out.println(status());
Thread.yield();
}
}
}
3、线程:上述代码片段实现了Runnable接口,复写了run方法,这个run方法就是一个普通的方法。没有任何内在的线程能力。而将Runnable类转换成工作任务的传统方式就是把他提交给一个Thread构造器,调用Thread对象的start 方法,为该线程的执行必需的初始化操作。
4、Executor:线程的创建具有一定的代价,频繁创建线程,执行销毁,对于程序来说,代价很高。所以我们一般不不会自己显示地创建线程,而是通过创建线程池,然后从线程池中获取线程。java.util.concurrent包中的执行器(Executor)可以为你管理Thread对象,从而简化了并发编程。Executor为客户端和任务执行之间提供了一个间接层,Executor代替客户端执行任务,并允许你管理一部任务的执行,而无须显示地管理线程的生命周期
通常情况下,我们使用单个的Executor来创建和管理系统中所有的任务。
ExecutorService executorService = Executors.newCachedThreadPool();
// ExecutorService executorService = Executors.newFixedThreadPool(10);
FixedThreadPool 使用了有限的线程集来执行所提交的任务,通过FixedThreadPool,可以一次性预先执行代价高昂的线程分配,不用为每个任务都固定付出创建线程的开销。上面通过 Executors.newCachedThreadPool() 和 Executors.newFixedThreadPool(10) 来创建Executor对象,其中CachedThreadPool在程序执行过程中会创建与所需数量相同的线程。
5、Callable接口,上面讲述任务的时候,提到了Runnable接口,Runnable接口是执行工作的独立任务,但是不反悔任何值。如果你希望在任务执行完成时能返回一个值,那么可以实现Callable接口。他的返回参数类型是从call方法返回的值,而不是run方法,而且必须使用ExecutorService.submit方法来调用,submit方法会返回Future对象
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Random;
import java.util.concurrent.Callable;
/**
* <p>Description: 定义任务-有返回值</p>
* <p>Copyright: @2018</p>
* <p>Company: YeePay</p>
*
* @author 小蚱蜢
* @version V1.0 2018/6/10
*/
public class MyCallable implements Callable<String> {
private static final Logger logger = LoggerFactory.getLogger(MyCallable.class);
private int id;
public MyCallable(int id){
this.id = id;
}
// 定义线程执行体
@Override
public String call() throws Exception {
// 这里模拟调用远程接口,做一些操作,返回时间大概在1-3s之间
int sleepTime = getThreadSleepTime();
Thread.sleep(sleepTime);
logger.info("模拟调用接口,耗时时间:{}, 模拟调用接口返回结果#{}",sleepTime,id);
return "模拟调用接口返回结果";
}
/**
* 模拟任务执行耗费时间
* @return
*/
private int getThreadSleepTime(){
Random random = new Random();
int nextInt = random.nextInt(3);
int sleepTime = 1000 * nextInt;
return sleepTime;
}
}
@Test
public void test_MyCallable(){
int taskCount = 1000000;
// 假设每个任务耗时1秒钟,执行完10个任务,需要耗时10秒
// 使用多线程调用
long timeStart = System.currentTimeMillis();
ExecutorService executorService = Executors.newCachedThreadPool();
// ExecutorService executorService = Executors.newFixedThreadPool(10);
List<Future<String>> futureResult = new ArrayList<Future<String>>();
for (int i =0; i<taskCount ; i++){
logger.info("开始执行任务:{}",i);
Future<String> submitResult = executorService.submit(new MyCallable(i));
futureResult.add(submitResult);
}
for (Future<String> sigleFuture : futureResult){
logger.info("sigleFuture.isDone() :{}", sigleFuture.isDone());
// if (sigleFuture.isDone()){
// try {
// logger.info("Future has finished task:{}", sigleFuture.get());
// } catch (InterruptedException e) {
// e.printStackTrace();
// } catch (ExecutionException e) {
// e.printStackTrace();
// }
// }
try {
logger.info("task result :{}", sigleFuture.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
logger.info("使用多线程执行{}个任务,每个任务大概耗时1秒,最终耗费时间:{}",taskCount, ((endTime-timeStart)/ 1000.00));
}
6、休眠:影响任务行为的一种简单的方法是调用sleep,这将是的任务中止给定的时间。
7、优先级:线程的优先级是将该线程的重要性传递给调度器,但是并不意味着优先级低的线程得不到执行。在绝大多数时间里,所有的线程都应该默认优先级执行。
8、让步:调用yield方法,给调度器机制一个暗示。但是并不能保证调度器一定采纳
四、名词疑惑:java 中,如何选择使用多线程,也即并发编程通常令人困惑,这些困惑的地方在于以下几点
1、任务并非线程,任务需要通过线程来驱动
2、任务类Runnable接口的命名
Java编程思想这本书中,有这么一段话,或许能帮助答疑这种困惑 |