线程池概述
系统启用一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情形下,使用线程池可以很好的提高性能。线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池会启动一个线程来执行它们的run()或call方法,当方法执行结束后,线程并不会死亡,而是再次返回到线程池成为空闲状态,等待执行下一个Runnable对象的方法。除此之外,线程池可以有效地控制系统中并发线程的数量。
Java8改进的线程池
在Java5之前,开发者需要手动实现自己的线程池,从Java5开始,Java内建支持线程池。提供了一个Executors工厂类来产生线程池,该工厂类包含如下几个静态工厂方法来创建线程池。
newCachedThreadPool()
:创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。newFixedThreadExecutor(int nThreads)
:创建一个可重用的、具有固定线程数的线程池。newSingleThreadExecutor()
:创建一个只有单线程的线程池;newScheduledThreadPool(int corePoolSize)
:创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。newSingleThreadScheduledPool()
:创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。ExcutorService newWorkStealingPool(int parallelism)
:创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争。ExcutorService newWorkStealingPool()
:前一个版本的简化版,如果当前机器有4个CPU,则目标并行级别被设置为4.
前三个方法返回ExecutorService对象,中间两个方法返回它的子类;ScheduledExecutorService线程池,后来两个方法充分利用多CPU并行的能力;
ExecutorService代表尽快执行线程的线程池,它提供了如下三个方法:
Future<?> submit(Runnable task)
:将一个Runnable对象提交给指定的线程池,线程池将在有空闲线程时执行任务。其中Future代表Runnable任务的返回值,但run方法没有返回值,所以返回null,但是可以调用Future的isDone()、isCancelled()方法来获得Runnable对象的执行状态。<T>Future<T>submit(Runnable task,T result)
:将一个Runnable对象提交给指定的线程池,线程池将在有空闲线程时执行任务。其中result显式指定线程执行结束后的返回值,所以Future对象将在方法执行完之后返回null。<T>Future<T> submit(Callable<T> task)
:将一个Callable对象提交给指定的线程池,线程池将在有空闲线程时执行任务。其中Future代表Callable任务的返回值.
ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池。
ScheduledFuture<V>schedule(Callable<V>callable,long delay,TimeUnit unit)
:指定callable任务将在delay延迟后执行。ScheduledFuture<?>schedule(Runnable command,long delay,TimeUnit unit)
:指定command任务将在delay延迟后执行。ScheduledFuture<?>scheduleAtFixedRate(Runnable command,,long initialDelay,long period,TimeUnit unit)
:指定command任务将在delay延迟后执行,并且以设定频率重复执行;ScheduledFuture<?>scheduleWithFixedRate(Runnable command,,long initialDelay,long delay,TimeUnit unit)
:创建并执行一个在给定初始延迟后首次启用的定期操作,随后在每一个执行终止和下一次执行开始之间都存在给定延迟,如果任务在任一次执行时遇到异常,就会取消后续执行,否则,只能通过程序来显式取消或终止该任务。
用完一个线程池之后,应该调用该线程池的shutdown方法,该方法将启动线程池的关闭序列,调用后的线程池将不再接受新任务,但会将以前所有已经提交的任务完成。当所有线程都完成的时候,线程池都的所有线程都会死亡;除此之外,也可以调用线程池的shutdownNow()方法来关闭线程池,该方法试图通知所有正在执行的活动任务,暂停正在等待的任务,并返回等待的任务列表。
使用线程池来执行线程任务的步骤:
- 调用Executors类的静态工厂方法创建一个Executor Service对象,该对象代表一个线程池;
- 创建Runnable实现类或Callable实现类的实例,作为线程执行任务;
- 调用Executor Service对象的submit()方法来提交Runnable实例或者Callable实例;
- 使用shutdown来关闭线程池;
package org.westos.demo8;
import java.util.TreeMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
//创建一个具有固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(6);
//使用Lambda表达式创建Runnable对象
Runnable target=()->{
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"的i值为:"+i);
}
};
//向线程池中提交两个线程
pool.submit(target);
pool.submit(target);
//关闭线程池
pool.shutdown();
}
}
Java8增强的ForkJoinPool
为了充分利用国多CPU、多核CPU的优势,Java7提供了ForkJoinPool来支持讲一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。
ForkJoinPool是ExecutorService的实现类。
常用构造器:
ForkJoinPool(int parallelism)
:创建一个包含parallelism个并行线程的ForkJoinPool。ForkJoinPool()
:以Runtime.availableProcessors()方法的返回值作为parallelism参数创建ForkJoinPool。
Java8开始ForkJoinPool中增加了通用池功能:
ForkJoinPool commonPool()
:该方法返回一个通用池,通用池的运行状态不会受shutdown或shutdownNow方法的影响。除非使用System.exit(0)来终止虚拟机;int getCommonPoolParallelism()
:该方法返回通用池的并行级别;
创建了ForkJoinPool实例后,就可以调用ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法来执行指定任务。其中ForkJoinTask代表一个可以并行、合并的任务。
案例:以执行没有返回值的“大任务”(打印0~300的数值)为例,程序将一个“大任务”差分成多个“小任务”,并将任务交给ForkJoinPool来执行。
package org.westos.demo8;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;
class PrintTask extends RecursiveAction{
//每个“小任务”最多只能打印50个数
private static final int THRESROLD=50;
private int start;
private int end;
public PrintTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected void compute() {
//当end和start之间的数小于THRESROLD时开始打印
if(end-start<THRESROLD){
for(int i=start;i<end;i++){
System.out.println(Thread.currentThread().getName()
+"的i值:"+i);
}
}else {
//当任务大于50时,分解大任务
int middle=(end+start)/2;
PrintTask left = new PrintTask(start, middle);
PrintTask right = new PrintTask(middle, end);
//并行执行两个小任务
left.fork();
right.fork();
}
}
}
public class ForkJoinPoolTest {
public static void main(String[] args) throws InterruptedException {
ForkJoinPool pool = new ForkJoinPool();
pool.submit(new PrintTask(0,300));
pool.awaitTermination(2,TimeUnit.SECONDS);
//关闭县城池
pool.shutdown();
}
}
从运行结果可以看到,线程池启动了四个线程来打印,这是因为运行计算机是四核的。还可以看到打印并不是按顺序打印,这是因为四个线程是并行的。
上面案例是一个没有返回值的打印任务,如果是有返回值的打印任务,则可以让任务继承RecuriveTask,其中泛型参数T代表的就是返回值的类型。
案例2:对一个长度为100的数组值进行叠加:
在这里插入代码片
线程相关类
Java还为线程安全提供了一些工具类,如ThreadLocal类,它代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从而避免并发访问的线程安全问题。
ThreadLocal
Thread的功能是为每一个使用该变量的线程都提供变量值副本,使每个线程可以独立地改变自己的副本,而不会和其他线程副本冲突。
ThreadLocal类的用法很简单,它只提供了如下三个方法:
T get()
:返回此线程局部变量中当前线程副本的值;void remove()
:删除此线程局部变量中当前线程的值;void set(T value)
:设置次线程局部变量中当前线程副本的值;
package org.westos.demo4;
class Account{
//定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量,每个线程都会保留该变量的一个副本
private ThreadLocal<String> name=new ThreadLocal<>();
//定义一个初始化name成员变量的构造器
public Account(String str){
this.name.set(str);
//用于访问当前线程的name副本的值
System.out.println("---"+this.name.get());
}
public String getName() {
return this.name.get();
}
public void setName(String name) {
this.name.set(name);
}
}
class MyTest extends Thread {
private Account account;
public MyTest(Account account, String name) {
super(name);
this.account = account;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i == 6) {
//当i=6时,将账户名改为当前线程名
account.setName(getName());
}
System.out.println(account.getName() + "账户的值:" + "i");
}
}
}
public class ThreadLocalTest {
public static void main(String[] args) {
Account at = new Account("初始名");
new MyTest(at, "线程甲").start();
new MyTest(at, "线程乙").start();
}
}
可以看到两个线程都会在i=6时将账户名改为与线程名相同,所以两个线程拥有两个账户名的情形。
实际上账户有三个副本,主线程一个,另外两个启动线程各一个,它们的值互不干扰。
Thread和其他所有同步机制一样,都是为了解决多线程中对同一变量的访问冲突。ThreadLocal并不能代替同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是为了隔离多个线程的数据共享,从而避免多个线程之间对共享资源的竞争,也就不需要对多个线程进行同步了。