简单概念

《Java编程思想》对并发概念的重要性阐述:

Java是一种多线程语言,并且提出了并发问题,不管你是否意识到了。因此,有很多使用中的Java程序,要么只是偶尔工作,要么是在大多数时间里工作,并且会由于未发现的并发缺陷而时不时地神秘崩溃。有事这种崩溃是温和的,但有时却意味着重要数据的丢失,并且如果没有意识到并发问题,你能最终会认为问题出现在其他什么地方,而不是你的软件中。如果程序被迁移到多处理器系统中,这些种类的问题还会被暴露或放大。基本上,了解并发可以使你意识到明显正确的程序可能展示出不正确的行为。

所以在你编写任何复杂程序之前,应该学习一下专门讨论并发主题的数据。

使用并发解决的问题可以分为两种

  • 更快的速度
    更快的速度是针对阻塞(通常是I/O)来讲的,其实如果没有阻塞使用并发是没有任何意义,反而比顺序执行还增加了“上下文切换(从一个任务切换到另一个任务)”的开销。因为有了阻塞,才把程序断开为多个片段,然后在单处理器上运行每个片段。
  • 改进代码的设计
    线程通常使你能够创建更加松散耦合的设计如用户交互,否则,你的代码中各个部分都必须显示地关注那些通常可以由线程来处理的任务。

进程与线程

  • 进程:
    进程是运行在它自己的地址空间内的自包容的程序,进程是自愿分配的基本单位,它也是抢占处理器的调度单。多任务操作系统可以通过周期性地将CPU从一个进程切换到另一个进程,来实现同时运行多个进程(程序)。
  • 线程:
    台湾称为"执行者",我认为更确切些,它是进程中某个单一顺序的控制流,是程序(进程)执行流的最小单位,也是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属于一个进程的其他线程共享进程所拥有的全部资源。

发生进程切换与发生线程切换时相比较,进程切换时涉及到有关资源指针的保存以及地址空间的变化等问题;线程切换时,由于同进程内的线程共享资源和地址 空间,将不涉及资源信息的保存和地址变化问题,从而减少了操作系统的开销时间。而且,进程的调度与切换都是由操作系统内核完成,而线程则既可由操作系统内 核完成,也可由用户程序进行。

任务描述

线程可以驱动,因此可以说线程是任务的执行载体,而任务是真正的业务逻辑。而描述任务可以使用继承Thread类或使用Runnablecallable两个接口。

实现Runnable接口即可定义为任务,然后使用线程驱动,Callable 接口也类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常,而Callable可以。

使用Runable定义任务和执行简单示例:

public class App {

	public static void main(String[] args) {
for(int i=0;i<10;i++){//开10个线程
new Thread(new Task()).start();
}
}
}
/**
* 定义任务
* @author Administrator
*/
class Task implements Runnable{
@Override
public void run() {
System.out.println("线程ID:"+Thread.currentThread().getId());
}
}

输出:

线程ID:8
线程ID:10
线程ID:12
线程ID:14
线程ID:16
线程ID:9
线程ID:11
线程ID:13
线程ID:15
线程ID:17

使用继承Thread类的方式,与此类似就不做举例了。

Executor框架

Runnable 的任务可以直接使用Thread类来执行,但Callable却不能,需要使用Executor框架来执行,其实最好不要直接使用Thread来执行Runnable任务,而是使用Executor框架。

Executor是JAVA SE5的java.util.concurrent包中的执行器,它为你管理Thread对象。Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。Executor存在的目的是提供一种将"任务提交"与"任务如何运行"分离开来的机制。架构如下图:

Executor接口定义如下:

public interface Executor {
void execute(Runnable command);
}

虽然只有一个方法,但却为灵活强大的异步任务执行框架提供了基础。它将任务的提交过程与执行过程解耦:用Runnable/Callable来表示任务,执行的任务放入run/call方法中即可,将Runnable/Callable接口的实现类交给线程池的execute方法来执行。实际上我们并不是直接使用Execuotr接口的,而是使用更为方便的ExecutorService接口,因为它对任务的生命周期做了管理:

public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
......
}

对于上述框架图:

ExecutorService: 真正的线程池接口。
ScheduledExecutorService接口: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
ThreadPoolExecutor: ExecutorService的默认实现。
ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

线程池

那么线程池从而而来呢?这时Executors就出场了,它为Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable类提供了一些工具方法,类似于集合中的Collections类的功能。

Executors可以很方便的创建线程池:

static ExecutorService newSingleThreadExecutor();
static ExecutorService newFixedThreadPool(int nThreads);
static ExecutorService newCachedThreadPool();
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
  • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  • newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

其实还有很多与线程池相关的方法,具体看参考JDK API文档,其实上面列出的方法都是使用ThreadPoolExecutor类来实现的,因为ThreadPoolExecutor是ExecutorService的默认实现。我们来看看ThreadPoolExecutor的几个构造函数:

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

各参数含义如下:

  • corePoolSize(基本线程池的大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
  • maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界(不限制线程数)的任务队列这个参数就没什么效果。
  • keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
  • TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
  • workQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字或后台线程等等。
  • RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:只用调用者所在线程来运行任务。
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    • DiscardPolicy:不处理,丢弃掉。
    • 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

如Executors的“newCachedThreadPool()”方法的实现使用的ThreadPoolExecutor的构造函数如:

public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

由于ThreadPoolExecutor 将根据 corePoolSize和 maximumPoolSize设置的边界自动调整池大小,当新任务在方法 execute(java.lang.Runnable) 中提交时:

  1. 如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的;
  2. 如果设置的corePoolSize 和 maximumPoolSize相同,则创建的线程池是大小固定的,如果运行的线程与corePoolSize相同,当有新请求过来时,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理
  3. 如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程才创建新的线程去处理请求;
  4. 如果运行的线程多于corePoolSize 并且等于maximumPoolSize,若队列已经满了,则通过handler所指定的策略来处理新请求;
  5. 如果将 maximumPoolSize 设置为基本的无界值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务

也就是说,处理任务的优先级为:

  1. 核心线程corePoolSize > 任务队列workQueue > 最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
  2. 当池中的线程数大于corePoolSize的时候,多余的线程会等待keepAliveTime长的时间,如果无请求可处理就自行销毁。

使用Executors来提交和执行任务

  1. 使用Runnable来表示的无返回值的任务:

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors; public class App { public static void main(String[] args) {
    ExecutorService exec=Executors.newFixedThreadPool(10);
    for(int i=0;i<10;i++){
    exec.execute(new Task());
    }
    exec.shutdown();
    }
    }
    /**
    * 定义任务
    * @author Administrator
    */
    class Task implements Runnable{ @Override
    public void run() {
    System.out.println(Thread.currentThread().getId());
    }
    }
  2. 使用Callable来表示的有返回值的任务:
    import java.util.ArrayList;
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future; public class App { public static void main(String[] args) throws Exception {
    ExecutorService exec=Executors.newFixedThreadPool(10);
    ArrayList<Future<Long>> results=new ArrayList<Future<Long>>();
    for(int i=0;i<10;i++){
    results.add(exec.submit(new Task()));
    } for(Future<Long> fs :results){
    System.out.println(fs.get());
    }
    exec.shutdown();
    }
    }
    /**
    * 定义任务
    * @author Administrator
    */
    class Task implements Callable<Long>{ @Override
    public Long call() throws Exception {
    return Thread.currentThread().getId();
    }
    }

    Callable接口只能使用submit来提交任务,submit会产生Future对象结果,可以用阻塞的get()来获取这个结果,但你使用isDone方法来查询是否已经产生了Future结果,然后再获取。

关闭线程池:

  1. shutdown():平缓的关闭线程池。线程池停止接受新的任务,同时等待已经提交的任务执行完毕,包括那些进入队列还没有开始的任务;
  2. shutdownNow():立即关闭线程池。线程池停止接受新的任务,同时线程池取消所有执行的任务和已经进入队列但是还没有执行的任务;

向任务传值

Runnable的run和Callable的call都是无参接口,那么在运行状态如何传递值呢?网上说又三种方式,其实本质是一种:向Runnable/Callable的实现类传值,所以你有多种方式比如构造函数,属性等等:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; public class App { public static void main(String[] args) {
ExecutorService exec=Executors.newFixedThreadPool(10);
for(int i=0;i<10;i++){
exec.execute(new Task("线程ID"));
}
exec.shutdown();
}
}
/**
* 定义任务
* @author Administrator
*/
class Task implements Runnable{
private String outVal; public Task(String outVal){
this.outVal=outVal;
}
@Override
public void run() {
System.out.println(outVal+":"+Thread.currentThread().getId());
}
}

输出:

线程ID:8
线程ID:10
线程ID:12
线程ID:14
线程ID:16
线程ID:9
线程ID:11
线程ID:13
线程ID:15
线程ID:17

Java并发之任务的描述和执行的更多相关文章

  1. java并发之固定对象与实例

    java并发之固定对象与实例 Immutable Objects An object is considered immutable if its state cannot change after ...

  2. Java并发之Semaphore的使用

    Java并发之Semaphore的使用 一.简介 今天突然发现,看着自己喜欢的球队发挥如此的棒,然后写着博客,这种感觉很爽.现在是半场时间,就趁着这个时间的空隙,说说Java并发包中另外一个重量级的类 ...

  3. Java并发之BlockingQueue的使用

    Java并发之BlockingQueue的使用 一.简介 前段时间看到有些朋友在网上发了一道面试题,题目的大意就是:有两个线程A,B,  A线程每200ms就生成一个[0,100]之间的随机数, B线 ...

  4. java并发基础(三)--- 任务执行

    第6章开始是第二部分,讲解结构化并发应用程序,大多数并发应用程序都是围绕“任务执行”构造的,任务通常是一些抽象的且离散的工作单元. 一.线程池 大多数服务器应用程序都提供了一种自然的任务边界:以独立的 ...

  5. 深入理解Java并发之synchronized实现原理

    深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入 ...

  6. 【深入理解JAVA虚拟机】第三部分.虚拟机执行子系统.3.函数调用与执行

    这章原名叫“虚拟机字节码执行引擎”,实际就是讲的函数如何调用和执行的. 1.概述 “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力, 其区别是物理机的执行引擎是直接建立在处理器. 硬 ...

  7. 【深入理解JAVA虚拟机】第三部分.虚拟机执行子系统.1.类文件结构

    无关性 无关性的体现有两个方面: 1.平台无关性:可在不同的操作系统和机器指令集上执行,可在不同厂商的虚拟机平台上执行. 2.语言无关性:用不同编程语言写出的代码编译生成的文件都可以运行. 实现思想: ...

  8. Java并发之CyclicBarria的使用

    Java并发之CyclicBarria的使用 一.简介 笔者在写CountDownLatch这个类的时候,看到了博客园上的<浅析Java中CountDownLatch用法>这篇博文,为博主 ...

  9. Java并发之CountDownLatch的使用

    Java并发之CountDownLatch的使用 一. 简介 Java的并发包早在JDK5这个版本中就已经推出,而且Java的并发编程是几乎每个Java程序员都无法绕开的屏障.笔者今晚在家闲来无事,翻 ...

随机推荐

  1. 2015年15+最佳的响应式HTML5网站模板

    015年最好的免费响应式HTML5模板通常用于创建新潮的网站. HTML5是HTML用于创建现代化网站的最新版本.随着这一现代标记语言的出现,网上冲浪的趋势变得越来越智能化越来越酷.几乎每个web开发 ...

  2. delphi 字符串查找替换函数 转

    1.       提取字符串中指定子字符串前的字符串 Function Before( Src:string ; S:string ): string ; Var   F: Word ; begin ...

  3. 关于firefox启动就崩溃的问题

    前些天在公司内网机器安装了Firefox Developer,每次启动直接就崩溃.最后发现问题出在Firefox的硬件加速上.解决办法: 1.右击firefox快捷方式,选择属性,在“目标”后面,即f ...

  4. mysql 无法启动的原因Can't start server: can't create PID file: No space left on device

    一大早来到公司,看到了一个噩梦,后台总是登录不上,登录就出错,还以为被黑客入侵了.经过1个小时的排错原因如下: 我的服务器是linux的,mysql的报错日志路径是/var/log/,经过查看日志发现 ...

  5. request.setAttribute和request.getAttribute还有session.setAttribute和session.getAttribute还有request.getParameter和request.getAttribute区别和联系

    1.session.setAttribute()和session.getAttribute()配对使用,作用域是整个会话期间,在所有的页面都使用这些数据的时候使用. 2.request.setAttr ...

  6. 王爽汇编语言(第三版)环境搭建(附PDF及工具下载)

    一.前言 最近在学习汇编语言,使用的是读者评价非常高的王爽老师写的<汇编语言>(第三版),为了适应现在各个版本的windows操作系统,所以采用VMWare虚拟机来搭建纯DOS环境. 二. ...

  7. Bezier(贝塞尔)曲线简介

    在计算机图形学中,Bezier曲线被广泛用于对平滑的曲线进行建模,对其有适当的了解是必要的.一条Bezier曲线由一系列控制点定义,称为曲线的阶数,由此可知,使用两个控制点()可以定义一条一阶Bezi ...

  8. 无分类编址 CIDR (构成超网)

    划分子网在一定程度上缓解了因特网在发展中遇 到的困难.然而在 1992 年因特网仍然面临三个必 须尽早解决的问题,这就是: B 类地址在 1992 年已分配了近一半,眼看就要在 1994 年 3 月全 ...

  9. HttpClient请求详解

    HttpClient 是 Apache Jakarta Common 下的子项目,可以用来提供高效的.最新的.功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建 ...

  10. HTTP笔记(一)

    最近在看<图解HTTP>.全书以图解的形式生动形象的讲解了HTTP协议.本文是根据该书整理的笔记,方便以后回顾. HTTP的诞生 HTTP又称超文本传输协议(HTTP,HyperText ...