java定时器与ThreadLocal编程陷阱

定时器

1 问题描述

1) 与每个 Timer 对象相对应的是单个后台线程,用于顺序地执行所有计时器任务。计时器任务应该迅速完成。如果完成某个计时器任务的时间太长,那么它会“独占”计时器的任务执行线程。因此,这就可能延迟后续任务的执行,而这些任务就可能“堆在一起”。    --摘自jdk1.6文档 

2) 如果某个任务抛出了异常,整个Timer会停止,后继任务都不能被执行

下面笔者通过编程来重现以上两个问题。

/** 
 * Timer执行周期任务,任务间隔5秒
*/
public class Test {

	public static void main(String[] args) {
		Timer timer = new Timer();
		//周期任务,任务间隔5s
		timer.schedule(task, 0, 5*1000);

	}
	static TimerTask task = new TimerTask() {

		@Override
		public void run() {
			System.out.println(System.currentTimeMillis()/1000L);
			//注释1. 暂停10s,延长任务执行时间
//			try {
//				Thread.sleep(10 * 1000);
//			} catch (Exception e) {
//			}
			
//			throw new RuntimeException("execption test");//注释2. 异常测试。
		}
	}
}

执行结果如下:

说明 结果 分析
直接执行

任务执行时间戳间隔为5秒,与预期相符
打开注释1,延长任务执行时间

任务执行间隔为10秒,与预期不符。印证了问题1).
关闭注释1,打开注释2.

 只打印了一个时间就抛出了异常

执行任务抛出异常,程序退出,但不要因此而小看这个问题,如果把它放在servlet容器(如tomcat)或在某个框架中运行,虽然抛出了异常但程序很可能会继续执行的,如果异常没有记录到日志中,那你就头疼去吧。

2.问题分析

通过查看Timer源码我们可以发现Timer本质上是将所有要进行调度的TimerTask放进一个根据下次执行时间nextExecutionTime排序的队列中(TaskQueue实例,它最大能存储128个TimerTask),然后通过一个线程(TimerThread实例)来循环执行此队列中的任务。因此Timer调度的任务是在一个线程中执行的。而如果其中某一任务运行时间大于任务间执行间隔,会阻塞后续任务,所以Timer的定时在时间上是不严格的。而且对于任务执行时出现的异常也没有处理,所以它也是不可靠的。这两个问题也存在于ScheduledExecutorService的scheduleAtFixedRate方法中。

如下是TimerThread的主循环方法:

   /**
     * The main timer loop.  (See class comment.)
     */
    private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
                    // Wait for queue to become non-empty
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; // Queue is empty and will forever remain; die

                    // Queue nonempty; look at first evt and do the right thing
                    long currentTime, executionTime;
                    task = queue.getMin();
                    synchronized(task.lock) {
                        if (task.state == TimerTask.CANCELLED) {
                            queue.removeMin();
                            continue;  // No action required, poll queue again
                        }
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        if (taskFired = (executionTime<=currentTime)) {
                            if (task.period == 0) { // Non-repeating, remove
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { // Repeating task, reschedule
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    if (!taskFired) // Task hasn't yet fired; wait
                        queue.wait(executionTime - currentTime);
                }
                if (taskFired)  // Task fired; run it, holding no locks
                    task.run(); //此处执行被调度的任务,没有对运行时异常作处理
            } catch(InterruptedException e) {
            }
        }
    }


ThreadLocal

1.问题描述

你觉得下面这段代码有没有问题?

public class Test {

	public static void main(String[] args) {
		//取得一个10个线程的线程池
		ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
		for(int i=0; i<100; i++){
			//每隔200ms执行一次
			executor.schedule(new ProblematicCommand(), 200, TimeUnit.MILLISECONDS);
		}
		//加入调度的线程任务全部执行后关闭线程池,程序退出。
		executor.shutdown();
	}
	
	static class ProblematicCommand implements Runnable{
		static ThreadLocal<SomeResource> resources = new ThreadLocal<SomeResource>(){
			@Override
			protected SomeResource initialValue() {
				System.out.printf("[Thread-%2d] init resource\r\n", Thread.currentThread().getId());
				return new SomeResource();
			}
		};
		
		public void releaseResource(){
			resources.get().release();
		}

		public void run() {
			resources.get().use();
			releaseResource(); //label 1: 释放资源
		}
	}
	static class SomeResource{
		private boolean released=false;
		//使用资源,如果资源已释放会抛出运行时异常
		public void use() {
			if(!released)
				System.out.printf("[Thread-%2d] Functional Tool is used\r\n", Thread.currentThread().getId());
			else
				throw new RuntimeException("resource has already been closed.");
		}
		//释放资源
		public void release(){
			this.released=true;
			System.out.printf("[Thread-%2d] resource released.\r\n", Thread.currentThread().getId());
		}
	}
}

我们将上面的代码运行起来后会发现它没有执行预期的次数(100次)就退出了,在我的电脑上运行结果为:初始化资源5次,使用5次,释放资源5次。

而如果把label 1代码注释掉,执行后发现代码只初始化了5次资源,但使用了100次!

2.问题分析

因为使用了线程池,所以上面的那些任务是在特定的几个线程中执行的(即5个线程共执行了100次任务),这就导致了ThreadLocal变量中只储存了与线程池中的线程ID相关的线程变量(ThreadLocal可以简单理解为Map<线程ID, SomeResource>)。也就是说某一个线程中执行了多次任务,因此在这个线程中执行的多个任务使用的却是同一个线程ID, 这就是资源只初始化5次的原因。而如果在任务执行完成后释放了资源,那么在释放资源的线程中再次执行任务时,由于与该线程ID绑定的资源已经被释放,在使用资源时抛出了异常,而线程池在发现异常后会导致整个线程池,所以程序直接退出了。

结论

有某任务需要循环执行,设其单次执行时间为t, 执行间隔为g,当t<g时定时器可以按时执行,但当t>=g时,任务被执行的间隔就成为了t.

另外在调度任务时要注意可能出现的异常。Timer在出现异常时会打印出异常栈,而ScheduledExecutorService却没有任何提示,但它们都会停止对任务的执行。

而在线程池中使用ThreadLocal,或通过定时器循环调度某一任务时(Timer可以看成是拥有一个线程的线程池)要注意ThreadLocal所持有的资源的状态。



你可能感兴趣的:(线程池,Timer陷阱,ThreadLocal陷阱)