线程池概述
为什么要使用线程池
1.服务器创建和销毁工作线程的开销很大
2.如果频繁的创建和销毁线程会导致频繁的切换线程,因为一个线程被销毁后,必然要把CPU转让给另一个已经就绪的线程
3.除了创建和销毁线程的开销之外,活动线程也销毁系统资源。每个线程本身会占用一定的内存(每个线程需要大约1MB内存),如果创建大量工作线程,它们消耗大量内存,可能会导致系统的内存空间不足。
线程池概念
线程池为线程生命周期开销问题和系统资源不足问题提供了解决方案。线程池中预先创建了一些工作线程,它们不断的从工作队列中取出任务,然后执行该任务。当工作线程执行完一个任务时,就会继续执行工作队列中的下一个任务。
线程池的特点:
1.减少了创建和销毁线程的次数,每个工作线程都可以一直被重用,能执行多个任务
2.可以根据系统的承载能力,方便的调整线程池中的线程数目,防止因为消耗过量的系统资源而导致系统崩溃
JDK类库提供的线程池
Executor接口表示线程池,它的execute(Runnable task)方法用来执行Runnable类型的任务。
Executor的子接口ExecutorService中声明了管理线程池的一些方法,比如关闭线程池的shutdown()方法等。
Excutors类中包含一些静态方法,他们负责生成各种类型的线程池ExecutorService实例
public class Machine implements Runnable{ private int id; public Machine(int id) { super(); this.id = id; } @Override public void run() { for(int a=0;a<10;a++){ System.out.println("当前线程:"+Thread.currentThread().getName()+" 当前Machine"+id+" :a="+a); Thread.yield(); } } public static void main(String[] args) throws Exception { ExecutorService service = Executors.newFixedThreadPool(2); for(int i=0;i<5;i++){ service.submit(new Machine(i));//像线程池提交五个Machine任务 } System.out.println("任务提交结束"); service.shutdown(); System.out.println("等任务提交任务完成后,服务会关闭"); } }
也可以通过线程池的构造方法,用给定的初始参数和默认的线程工厂创建线程池 推荐(比上面的创建方式更明确的线程池的运行规则,避免资源耗尽的风险)。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
- 参数:
corePoolSize
-- 表示常驻核心线程数 ,如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的,如果线程池的线程数小于核心线程数 即使执行完毕也不会销毁。
maximumPoolSize
-- 池中允许的最大线程数。如果执行的线程数大于核心线程数时,需要借助第五个参数的帮助,缓存在队列中
- 如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程。
- 如果设置的 corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池。
keepAliveTime
- 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。(呆多长时间之后 没有任务传给它,它就会消失)unit
- keepAliveTime 参数的时间单位。- 如果池中当前有多于 corePoolSize 的线程,则这些多出的线程在空闲时间超过 keepAliveTime 时将会终止(参见
getKeepAliveTime(java.util.concurrent.TimeUnit)
) workQueue
- 表示缓存队列(装任务的容器)-
如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。 handler
- 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序(所有线程都忙着 即线程池忙 并且任务队列满了)。
- 思考:线程池达到最大线程数量,缓冲队列满时,多余的线程会被丢弃,这是不想看到的,可不可以将缓冲队列设置无限大?如果这么做对线程池有何影响?
- 任务队列+最大线程数=线程池总共能处理的任务
自己模拟一个线程池
package javaee.net.cn.thread; import java.util.LinkedList; /** * @Modified By:自定义线程池的实现 */ public class ThreadPool extends ThreadGroup { private boolean isClosed = false;//线程池是否关闭 private LinkedList<Runnable> workQuee;//表示工作队列 private static int threadPoolID;//表示线程池ID private int workThreadID;//表示工作线程池ID public ThreadPool(int poolSize) { //poolSize表示线程池中工作线程的数目 //每一次的初始化,线程池ID都会自动增加 super("ThreadPool-" + (threadPoolID++)); //设置为守护线程 setDaemon(true); workQuee = new LinkedList<>();//创建工作队列 for (int i = 0; i < poolSize; i++) { new WorkThread().start(); } } /** * 向工作队列中添加一个新的任务,让工作线程去执行 * * @param task */ public synchronized void execute(Runnable task) { if (isClosed) { //线程池关闭,则抛出如下的异常 throw new IllegalStateException(); } if (task != null) { workQuee.add(task); notify();//唤醒正在等待获取任务的工作线程getTask(); } } /** * 从工作队列中取出一个线程,让线程执行任务 * * @return * @throws InterruptedException */ public synchronized Runnable getTask() throws InterruptedException { while (workQuee.size() == 0) { //当工作队列中的线程为0时,如果线程池关闭,则返回null.负责,等待任务 if (isClosed) { return null; } else { wait(); } } return workQuee.removeFirst();//从工作队列中弹出第一个元素 } /** * 关闭线程池 */ public synchronized void closed() { //线程池没有关闭 if (!isClosed) { isClosed = true; workQuee.clear();//清除工作队列 interrupt();//中断所有的线程 } } /** * 等在工作线程将若有的任务执行完毕 */ public void join() { synchronized (this) { isClosed = true; notifyAll();//唤醒所有的等待任务的工作线程 } //activeCount() 返回此线程组中活动线程的估计数。 来自ThreadGroup Thread[] threads = new Thread[activeCount()]; // enumerate[list] 把此线程组及其子组中的所有活动线程复制到指定数组中。 int count = enumerate(threads);//返回所有的活着的线程数量 for (int i = 0; i < count; i++) { try { threads[i].join();//等待所有的活动的工作宣称的结束 } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 内部类,创建的工作线程对象 */ private class WorkThread extends Thread { public WorkThread() { super(ThreadPool.this, "WorkThread-" + workThreadID++); } @Override public void run() { //线程没有中断 while (!isInterrupted()) { Runnable task = null; try { task = getTask();//获取任务 } catch (InterruptedException e) { e.printStackTrace(); } if (task == null) { return; } try { task.run();//运行任务 } catch (Throwable t) { t.printStackTrace(); } } } } }
在ThreadPool类中定义一个LinkedList类型的workQueue成员变量,它表示工作队列,用来存放线程池要执行的任务,每个任务都是Runnable实例。
由ThreadPool来执行任务的程序 只要调用ThreadPool类的excute(Runnable task)方法,就能像线程池提交任务
在线程池的excute()方法中,先判断线程池是否已经关闭。如果线程池已经关闭就不在接收任务,否则就把任务加入到工作队列中,并且唤醒正在等待任务的工作线程(notify()方法)。
在ThreadPool类的构造方法中,会创建并启动若干个工作线程,工作线程的数目由构造方法的参数poolSize决定。WorkThread表示工作线程,它是ThreadPool类的内部类。
工作线程从工作队列中取出一个任务,接着执行该任务,然后从工作队列中取出下一个任务并执行它,如此反复。
工作线程从工作队列中取任务的操作是由ThreadPool类的getTask()方法实现的,它的处理逻辑如下
- 如果队列的线程池已经关闭,就返回null,表示已经没有任务可以执行了。
- 如果队列为空,并且线程池没有关闭,那就在此等待,直到其他线程将其唤醒或者中断。
- 如果队列中有任务,就取出第一个任务并将其返回
线程池的join()和close()方法都可用来关闭线程池,join()方法确保关闭线程池之前,工作线程把队列中的所有任务都执行完。而close()方法时立即清空队列,并且中断所有的工作线程
ThreadPool类是ThreadGroup类的子类。ThreadGroup表示线程组,它提供了管理线程组中线程的方法,列如interrupt()方法相当于调用线程组中所有活着的线程的interrupt()方法。线程池中所有工作线程都加入到当前ThreadPool对象表示的线程组中。
ThreadPool类在close()方法中调用了iterrupt()方法。
/** * 关闭线程池 */ public synchronized void closed() { //线程池没有关闭 if (!isClosed) { isClosed = true; workQuee.clear();//清除工作队列 interrupt();//中断所有的线程 } }
以上interrupt()方法用于中断所有的工作线程。iterrupt()方法会对工作线程造成以下影响:
- 如果一个工作线程正在ThreadPool的getTask()方法中因为执行wait()方法而阻塞,则会抛出InterruptedException
- 如果此时一个工作线程正在执行一个任务,并且这个任务不会被阻塞,那么这个工作线程会正常执行完任务,但是在执行下一轮while(!isInterrupted()){...}循环时,由于isInterrupted()会返还true 因此会退出while循环
运行测试线程池
package javaee.net.cn.thread; /** * @Modified By:线程池测试 */ public class ThreadPoolTester { public static void main(String[] args) { if (args.length != 2) { System.out.println( "用法: java ThreadPoolTest numTasks poolSize"); System.out.println( " numTasks - integer: 任务的数目"); System.out.println( " numThreads - integer: 线程池中的线程数目"); return; } int numTasks = Integer.parseInt(args[0]); int poolSize = Integer.parseInt(args[1]); ThreadPool threadPool = new ThreadPool(poolSize); //创建线程池 // 运行任务 for (int i = 0; i < numTasks; i++) threadPool.execute(createTask(i)); threadPool.join(); //等待工作线程完成所有的任务 // threadPool.close(); //关闭线程池 }//#main() /** * 定义了一个简单的任务(打印ID) */ private static Runnable createTask(final int taskID) { return new Runnable() { public void run() { System.out.println("Task " + taskID + ": start"); try { Thread.sleep(500); //增加执行一个任务的时间 } catch (InterruptedException ex) { } System.out.println("Task " + taskID + ": end"); } }; } }
ThreadPool threadPool = new ThreadPool(poolSize); //创建线程池
创建线程池的构造方法中,初始化所有工作线程,并通过.start()方法启动所有线程。
此处注意 while 如果线程没有被终止 就会一直执行如下代码(获取任务)。这也是和数据库连接池不同的地方
线程池中的线程使用完成之后不需要做任何处理(不需要像连接池一样放回池子里)
通过下面的getTask()方法 如果workQueue为空,就阻塞等待到workQueue有任务。
//线程没有中断 while (!isInterrupted()) { Runnable task = null; try { task = getTask();//获取任务 } catch (InterruptedException e) { e.printStackTrace(); } if (task == null) { return; } try { task.run();//运行任务 } catch (Throwable t) { t.printStackTrace(); } }
通过下面的getTask()方法得知,初始化所有工作线程的时候,工作队列是空的,然后所有的线程都wait()住了,不会往下执行removeFirst()方法
public synchronized Runnable getTask() throws InterruptedException { while (workQuee.size() == 0) { //当工作队列中的线程为0时,如果线程池关闭,则返回null.否则,等待任务 if (isClosed) { return null; } else { wait(); } } return workQuee.removeFirst();//从工作队列中弹出第一个元素 }
一直到exectue方法,往工作队列里面添加任务,并且手动notify()
public synchronized void execute(Runnable task) { if (isClosed) { //线程池关闭,则抛出如下的异常 throw new IllegalStateException(); } if (task != null) { workQuee.add(task); notify();//唤醒正在等待获取任务的工作线程getTask(); } }
注意:execute()方法中的 workQuee.add(task)代码 和 getTask()方法中的workQuee.removeFirst() 对应.
使用线程池的注意事项
1.死锁
任何多线程程序都有死锁的风险。造成死锁最简单的情形是,线程A持有对象X的锁,并且在等待对象Y的锁,而线程B持有对象Y的锁,并且在等待对象X的锁。
线程A和线程B都不能释放自己持有的锁,并且等待对方的锁,这样就导致两个线程永远等待下去,死锁就这样产生了。
虽然任何多线程都有死锁的风险,但是线程池还会导致另外一种死锁。
在这种情形下,假设线程池中的所有工作线程,都在执行各自任务时被阻塞,它们都在等待某个任务A的执行结果,而任务A仍在工作队列中,由于没有空闲线程,使得任务A一直不能执行。
这使得线程池中的所有工作线程都永远阻塞下去,死锁就这样产生了。
2.并发错误
线程池的工作队列依靠wait()和notify()方法来使得工作线程及时取得任务,但这两个方法都难于使用。如果编码不正确,可能会丢失通知,导致工作线程一直保持空闲状态,无视工作队列中需要处理的任务。
3.线程泄漏
工作线程在执行一个任务时被阻塞,如果等待用户的输入数据,但是由于用户一直不输入数据(外部原因),导致这个工作线程一直被阻塞。这样的工作线程名存实亡,它实际上不执行任何任务了。
加入线程池中的所有工作线程都处于这样的阻塞状态。那么线程池就无法处理新加入的任务了。
4.和ThreadLocal使用产生脏数据
由于线程池会复用Thread对象,那么与Thread绑定的类的静态属性ThreadLocal变量也会被重用。解决方法就是:在每次用完ThreadLocal时,必须要及时调用remove()方法进行清理。
使用线程池的原则
上面提到了线程池所带来的风险,所以使用线程池时,需要遵循以下原则。
1.如果任务A在执行过程中需要同步等待任务B的执行结果,那么任务A不适合加到线程池的工作队列中。
如果把像任务A一样的需要等待其他任务执行结果的任务加入到工作队列中,可能会导致线程池的死锁
2.如果执行某个任务可能会阻塞,并且是长时间阻塞,则应该设置超时时间,避免工作线程永久阻塞下去导致线程池的泄漏。
Tip: Eclipse调试多线程的时候可能会有问题(debug的顺序未必是真正的执行顺序,多线程的问题建议打印调试)