Executors,Timer,DelayQueue,Interruption

本文深入探讨Java并发编程中的线程池与Executor框架,阐述其相较于直接创建线程的优势,包括减少创建销毁线程的开销、提高响应性和管理生命周期的能力。通过实例展示了如何使用Executors的静态工厂方法创建不同类型的线程池,以及如何正确关闭Executor以避免JVM无法结束的问题。此外,对比了Timer与线程池在任务调度上的差异,强调了使用线程池替代Timer类的必要性。最后,介绍了DelayQueue的功能及其在处理一组任务超时后的应用场景,特别针对服务器关闭空闲连接、缓存对象过期、网络协议中的超时处理等场景进行了模拟演示。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在Java类库中执行任务的主要抽象不是Thread,而是Executor。

Executor

Web服务器上为每个请求创建一个线程存在诸多弊端:
  1. 如果请求到达的频率很高,而请求的处理又是轻量级的,那创建和销毁线程的开销相对就很高。
  2. 线程数多于可用处理器数时,大量线程会闲置,空闲的线程会战胜大量的内存,而且大量线程在竞争CPU资源时会产生其他的性能开销。
  3. 可创建线程的数量有一个上限。这跟平台相关,与JVM启动参数、Thread构造函数中请求栈的大小等都有关系。
在一定范围内增加线程可以提高系统的吞吐率;但超出了这个范围只会降低执行速度。
相比之下,线程池就有诸多优势,因为线程池中的线程是可以重用的,减少了创建销毁线程的开销,而且当任务到来时由于不需要等待创建新线程因而提高了响应性。
线程池是Executor框架的一部分,但不是全部。Executor还实现了对生命周期的支持、统计信息收集、应用程序管理机制和性能监视等机制。
可以调用Executors的静态工厂方法来创建线程池:
  1. newFixedThreadPool。创建固定长度的线程池,如果某个线程发生了未预期的Exception而结束,那么会线程池会补充一个新线程。
  2. newCachedThreadPool。如果线程池规模达到了处理需求,会回收空闲线程;如果需求增加会增添新线程。线程池规模没有任何限制。
  3. newSingleThreadPool。单线程,确保任务串行执行(如FIFO,LIFO,优先级).
  4. newScheduledThreadPool。固定长度的线程池,而且以延迟或定时的方式来执行任务。用来替代Timer类。
JVM只有在所有(非守护)线程全部终止后才会退出,因此如果无法正确地关闭Executor,那么JVM将无法结束。
Executor采用异步方式来执行任务,因此任一时刻,之前提交的任务状态不是立即可见的。
Executor扩展了ExecutorService接口。
ExecutorService生命周期有3种状态:运行、关闭、终止。shutdown()以平缓的方式关闭:不再接收新任务,同时等待已提交的任务完成--包括那些还未开始执行的任务。shutdownNow()以粗暴的方式关闭:取消所有正在运行的任务,并不再启动已提交在等待队列中的任务。awaitTermonation来等待ExecutorService到达终止状态,isTermination轮询ExecutorService是否已终止。

Timer

Timer类负责管理延迟任务(比如100ms后执行)或周期任务(比如每隔10ms执行一次该任务)。
public class OutOfTime {
	public static void main(String[] args) {
		Timer timer = new Timer();
		timer.schedule(new ThrowTask1(), 5);		//任务1延迟5毫秒之后再执行
		timer.schedule(new ThrowTask2(), 1);		//任务2延迟1毫秒之后再执行
		try {
			Thread.sleep(10);			//主线程休息10毫秒之后,取水Timer中的所有任务
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		timer.cancel();			//如果不调用cancle,则Timer线程不会退出
	}

	static class ThrowTask1 extends TimerTask {
		public void run() {
			System.out.println(this.getClass().getName());
		}
	}
	static class ThrowTask2 extends TimerTask {
		public void run() {
			System.out.println(this.getClass().getName());
			
		}
	}
}
输出:
conurCollection.OutOfTime$ThrowTask2
conurCollection.OutOfTime$ThrowTask1
任务2先于任务1执行。
如果我们在ThrowTask2中故意抛出一个RuntimeException,则ThrowTask1就不会执行。原因:Timer线程并不捕获未受检查的异常,当它抛出未受检查的异常时会终止整个线程。而ScheduledThreadPoolExecutor则能够正确处理这些表现出错误行为的任务,所以在JDK5.0中已很少使用Timer,而使用ScheduledThreadPoolExecutor取代之。

DelayQueue

DelayQueue功能上跟Timer很相似,它管理着一组Delayed对象,每个Delayed对象都有一个相应的延迟时间:在DelayQueue中只有某个元素逾期后,才能从DelayQueue中执行take操作。从DelayQueue中返回的对象将根据它们的延迟时间进行排序。
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {

    private transient final ReentrantLock lock = new ReentrantLock();
    private final Condition available = lock.newCondition();

    private final PriorityQueue<E> q = new PriorityQueue<E>();
    //……
}
从DelayQueue的定义中可以看到它是一种BlockingQueue,它使用PriorityQueue来存放元素,元素的类型必须是Delayed的子类型。
Delayed是一个无界的阻塞队列,不允许将NULL放入其中。Delayed中有一个抽象方法getDelay(),当元素的getDelay()方法返回0或者小于0的时候才能将其出队。Delayed接口继承了Comparable接口,比较的基准为延时的时间值, compareTo 方法必须提供与 getDelay 方法一致的排序。Java中的PriorityQueue是小根堆,最小的最在队首,也最先出队。
DelayQueue的使用场景:
a) 关闭空闲连接。服务器中,有很多客户端的连接,空闲一段时间之后需要关闭之。
b) 缓存。缓存中的对象,超过了空闲时间,需要从缓存中移出。
c) 任务超时处理。在网络协议滑动窗口请求应答式交互时,处理超时未响应的请求。
这些场景有一个共同点:一组任务经过一段随机的时间后可能会到达一种状态,当它到达这个状态时我们要采取一定的行动。
一种笨笨的办法就是,使用一个后台线程,遍历所有对象,挨个检查。这种笨笨的办法简单好用,但是对象数量过多时,可能存在性能问题,检查间隔时间不好设置,间隔时间过大,影响精确度,过小则存在效率问题。而且做不到按超时的时间顺序处理。 
这场景,使用DelayQueue最适合了。
正面的代码就模拟一个服务器上有若干连接,连接空闲一段时间后就要将其关闭:
package conurCollection;

import java.util.Random;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class Connection implements Runnable,Delayed{
	private long time;		//用于记录状态变为空闲的时刻,单位是纳秒
	private final String name;
	public volatile boolean stop;
	
	public Connection(String name){
		this.name=name;
		time=Long.MAX_VALUE;
		stop=false;
	}

	@Override
	public int compareTo(Delayed o) {
		Connection that=(Connection)o;
		long rect=time-that.getTime();
		if(rect>0)
			return 1;
		else if(rect<0)
			return -1;
		else
			return 0;
	}

	@Override
	public long getDelay(TimeUnit unit) {
		return unit.convert(time-System.nanoTime()+TimeUnit.NANOSECONDS.convert(WebServer.DELAYTIME, TimeUnit.MILLISECONDS),TimeUnit.NANOSECONDS);
	}

	@Override
	public void run() {
		//连接要循环地“处理请求--休息”
		while(!stop){
			time=Long.MAX_VALUE;
			System.out.println(name+"处理了一个客户端请求.");
			//工作完了,连接的状态改为空闲,记录下当前的时间
			time=System.nanoTime();
			//休息
			try{
				Thread.sleep(new Random().nextInt(5000));		//随机休息一段时间,但都不会超过5秒
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
		System.out.println(name+"终止.");
	}

	public long getTime() {
		return time;
	}
	
	public String getName() {
		return name;
	}	
	
}

package conurCollection;

import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class WebServer {
	public static long DELAYTIME=1000;			//单位是毫秒,连接空闲超过这个时间后将被关闭
	
	public static void main(String[] args){
		DelayQueue<Connection> queue=new DelayQueue<Connection>();
		ExecutorService exec=Executors.newCachedThreadPool();
		final int count=5;		//创建连接的数量
		for(int i=0;i<count;i++){
			Connection conn=new Connection((i+1)+"号连接");
			queue.put(conn);
			exec.execute(conn);
		}
		for(int i=0;i<count;i++){
			Connection conn=null;
			try {
				conn=queue.take();		//队首元素出列,如果队首元素getDelay()返回的是正值,则take()操作阻塞
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			if(conn!=null){
				System.out.println(conn.getName()+"已空闲超时,将被关闭");
				conn.stop=true;
			}
		}	
		exec.shutdown();
	}
}
输出:
1号连接处理了一个客户端请求.
3号连接处理了一个客户端请求.
5号连接处理了一个客户端请求.
2号连接处理了一个客户端请求.
4号连接处理了一个客户端请求.
4号连接处理了一个客户端请求.
1号连接已空闲超时,将被关闭
3号连接已空闲超时,将被关闭
5号连接已空闲超时,将被关闭
2号连接已空闲超时,将被关闭
1号连接终止.
4号连接处理了一个客户端请求.
4号连接已空闲超时,将被关闭
4号连接终止.
3号连接终止.
5号连接终止.
2号连接终止.

使用中断来取消线程

Java没有提供任何机制来安全地终止线程--避免使用Thread.stop和Thread.suspend,这些机制存在严重缺陷。
当然使用上面Connection类中终止线程的方法也可以:设一个stop标记变量,然后从外部改变stop的值。
public void run() {
		//连接要循环地“处理请求--休息”
		while(!stop){
			time=Long.MAX_VALUE;
			System.out.println(name+"处理了一个客户端请求.");
			//工作完了,连接的状态改为空闲,记录下当前的时间
			time=System.nanoTime();
			//休息
			try{
				Thread.sleep(new Random().nextInt(5000));		//随机休息一段时间,但都不会超过5秒
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
		System.out.println(name+"终止.");
	}
通常,Interruption是实现协作机制的最好方法,Interruption是一种协作机制,能够使一个线程终止另一个线程的当前工作。
每个Thread都有一个boolean类型的中断状态。
public class Thread{
    //中断目标线程
    public void interrupt(){...}
    //返回目标线程的中断状态
    public boolean isInterrupted(){...}
    //清除当前线程的中断状态,并返回它之前的值
    public static boolean interrupted(){...}
    ...
}
阻塞库方法,例如Thread.sleep和Thread.wait都会检查线程何时中断,并在发现中断时提前返回。严格地讲,Interruption并不一定能真正地中断线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。
public void run(){
    while(!Thread.currentThread().isInterrupted()){
        ...
    }
    System.out.println("线程终止");
}



资源下载链接为: https://pan.quark.cn/s/f989b9092fc5 今天给大家分享一个关于C#自定义字符串替换方法的实例,希望能对大家有所帮助。具体介绍如下: 之前我遇到了一个算法题,题目要求将一个字符串中的某些片段替换为指定的新字符串片段。例如,对于源字符串“abcdeabcdfbcdefg”,需要将其中的“cde”替换为“12345”,最终得到的结果字符串是“ab12345abcdfb12345fg”,即从“abcdeabcdfbcdefg”变为“ab12345abcdfb12345fg”。 经过分析,我发现不能直接使用C#自带的string.Replace方法来实现这个功能。于是,我决定自定义一个方法来完成这个任务。这个方法的参数包括:原始字符串originalString、需要被替换的字符串片段strToBeReplaced以及用于替换的新字符串片段newString。 在实现过程中,我首先遍历原始字符串,查找需要被替换的字符串片段strToBeReplaced出现的位置。找到后,就将其替换为新字符串片段newString。需要注意的是,在替换过程中,要确保替换操作不会影响后续的查找替换,避免遗漏或重复替换的情况发生。 以下是实现代码的大概逻辑: 初始化一个空的字符串result,用于存储最终替换后的结果。 使用IndexOf方法在原始字符串中查找strToBeReplaced的位置。 如果找到了,就将originalString中从开头到strToBeReplaced出现位置之前的部分,以及newString拼接到result中,然后将originalString的查找范围更新为strToBeReplaced之后的部分。 如果没有找到,就直接将剩余的originalString拼接到result中。 重复上述步骤,直到originalStr
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值