java-并发-高级并发对象1
以往说到的线程对象都是java平台中非常初级的API,用于处理一些基本的任务,对于一些复杂高级的工作,就需要一些高级的并发对象,尤其是针对于大规模并发应用程序,要充分利用现在的多核多处理器系统的性能。
以下内容包括一些从java5开始java平台具有的一些高并发特性。这些特征多数在包java.util.concurrent中实现,java集合框架中也有新的并发数据结构。
锁对象
同步代码依赖于一种简单的再进入锁,这种锁比较易用,但有很多局限性,java.util.concurrent.locks包支持许多高级的锁用法,在这里集中讲其中最基本的接口Lock。
Lock对象的作用方式很像同步代码使用的隐式锁,相似之处在于一个锁对象一次只能被一个线程拥有,Lock对象也支持wait/notify机制,通过相关的Condition对象实现。
Lock对象相对于隐式锁最大的优势在于其能够收回锁的请求,调用tryLock方法请求锁时,如果锁无立即法获取或者在超时之前(如果指定)无法获取就会收回请求;调用lockInterruptibly请求锁
,如果在获得锁之前另一个线程发送中断信号,请求将被收回。
正确使用锁对象可以避免死锁的产生,即如果一个线程需要对两个对象进行同步操作,则确保同时获得两个对象的锁就可以了,tryLock的典型用法为:
Lock lock = ...;//此处省略锁的创建表达式 if (lock.tryLock()) { try { //如果成功获得锁情况下的操作 } finally {//操作之后释放锁 lock.unlock(); } } else { // 如果未能获得锁的操作 }
执行者
之前所有举的例子中,都有和一个新的线程所作的工作与线程本身的紧密联系,然而在大型的程序中,一般会将线程的管理和创建与程序的其他部分分开,而封装了这些功能的对象叫做执行者,以下将执行者分三部分:
- 执行者接口--定义三个执行者对象接口类型
- 线程池--最常用的一类执行者的实现
- Fork/Join 充分利用多处理器性能的框架(JDK7引入)
Executer接口
java.util.concurrent包中定义了三个执行者接口:
- Executor--一个支持启动新任务的简单接口
- ExecutorService--Executor的子接口,在其基础上添加了帮助管理生命周期的特性,包括个体任务和执行者本身的生命周期
- ScheduledExecutorService--ExecutorService的子接口,支持 and/or 周期性地执行任务
一般情况下,引用执行者对象的变量的类型都被声明为以上三种接口类型,而不是实现接口的类的类型。
Executor接口提供了单一的方法,execute,其被设计为一个通常的线程对象创建的替代方法,如:
//将以下线程创建方法 (new Thread(r)).start(); //替换为 e.execute(r); //注意e是已经创建号的Executor对象
方法execute的定义不是很明确,低级的方法创建一个新线程对象后就立马开始执行,而Executor对象根据具体的实现,可能也是立即执行,但更多的时候是利用已经存在的工人线程去运行示例中的r,或者将r放在队列中等待运行(下面的线程池中会讲到)。
java.util.concurrent包中的执行者实现类是为了充分更高级的ExecutorService 和ScheduledExecutorService接口而设计的,当然也适用于基础的Executor接口。
ExecutorService接口对于execute方法补充了一个类似的,但是功能更强的submit方法,就像execute方法一样,submit方法接收Runnable对象为参数,也可以接收允许任务返回一个值的Callable对象,submit方法返回一个Future对象,该Future对象可以检索Callable对象返回的值,并可以管理Callable和Runnable任务的状态。
ExecutorService接口还提供了支持提交大量Callable对象集合的方法,最后还有许多管理执行者终结的方法,如果需要立马终结,则需要正确处理中断。
ScheduledExecutorService接口除了继承ExecutorService的特征外,还补充了schedule方法,该方法可以延迟执行Runnable和Callable对象,此外scheduleAtFixedRate和scheduleWithFixedDelay方法分别指定执行任务的重复和执行间隔。
线程池
大部分java.util.concurrent包中的执行者接口的实现类都使用线程池,线程池由多个工人线程组成,这种线程于其所执行的Runnable和Callable任务是分开的,经常被来执行多任务。
线程池的使用使线程创建的开销最小化,线程对象需要使用大量的内存,而且在大型的程序中,线程对象的分配和解除分配会带来大量的内存管理开销。
有一种常用的线程池叫固定线程池,这种线程池总是有指定数量的线程在运行,如果一个线程由于某种原因终止而线程池还在运行的时候,线程池就会自动替换新的线程。当活动的任务多余线程数量的时候,任务就会通过一个内部的队列提交到线程池。
固定线程池的重要优势就是应用程序可以利用其巧妙地降低负荷,比如一个web服务器应用程序中,每一个HTTP请求由一个独立线程处理,如果该程序对于一个新的HTTP请求只是简单地创建新的线程,随着请求量增加,系统很快就没有资源再创建更多的线程,程序因此可能会无法运行下去。当对线程创建的数量进行限制,并按照队列依次处理,程序才可能有效运行。
简单的创建一个使用固定线程池的执行者的方法就是调用java.util.concurrent.Executors的工厂方法newFixedThreadPool,这个类中也提供了以下工厂方法:
- newCatchedThreadPool方法创建一个含有可扩展线程池的执行者,该执行者适用于需要执行短时间任务的程序。
- newSingleThreadExecutor方法创建一个一次执行单个任务的执行者
当以上工厂方法不能满足需求,可以创建java.util.concurrent.ThreadPoolExecutor或者
java.util.concurrent.ScheduledThreadPoolExecutor实例以提供更丰富的功能。
Fork/Join
Fork/Join框架是一个ExecutorService
接口的实现,帮助你充分利用多处理器的性能,它是为可以递归细分的任务而设计的,目的是使用所有的可使用的处理资源来增强程序的性能。
就像其他ExecutorService接口的实现一样,fork/join框架将任务分配给线程池中的工作线程,fork/join框架与众不同的地方在于其“工作窃取“算法,已经处理完成任务的线程可以从其他正在繁忙的线程中窃取工作任务。
fork/join框架的核心是ForkJoinPool类,它是AbstractExecutorService类的扩展,ForkJoinPool类实现最核心的工作窃取算法,并且可以执行ForkJoinTask过程。
fork/join基本的用法就是先定义一个继承ForkJoinTask类或者ForkJoinTask子类的类,用于处理部分的工作,定义好类以后,实例化该类并作为参数传给ForkJoinPool
实例的invoke方法。
java se中有一些通用的特征已经用fork/join框架实现,如java8的java.util.Arrays类的parallelSort方法,这个方法与sort方法类似,但是利用了fork/join框架,在多处理器系统上,对于大型数组的并行排序比串行排序快。另外一个利用了fork/join框架的实现是java.util.streams包中的一些方法。