http://blog.csdn.net/abc_key/article/details/38419469
本章介绍
- 线程模型(thread-model)
- 事件循环(EventLoop)
- 并发(Concurrency)
- 任务运行(task execution)
- 任务调度(task scheduling)
线程模型定义了应用程序或框架怎样运行你的代码。选择应用程序/框架的正确的线程模型是非常重要的。Netty提供了一个简单强大的线程模型来帮助我们简化代码,Netty对全部的核心代码都进行了同步。全部ChannelHandler,包含业务逻辑。都保证由一个线程同一时候运行特定的通道。
这并不意味着Netty不能使用多线程,仅仅是Netty限制每一个连接都由一个线程处理。这样的设计适用于非堵塞程序。我们没有必要去考虑多线程中的不论什么问题,也不用操心会抛ConcurrentModificationException或其它一些问题,如数据冗余、加锁等,这些问题在使用其它框架进行开发时是常常会发生的。
读完本章就会深刻理解Netty的线程模型以及Netty团队为什么会选择这种线程模型。这些信息能够让我们在使用Netty时让程序由最好的性能。
此外,Netty提供的线程模型还能够让我们编写整洁简单的代码,以保持代码的整洁性;我们还会学习Netty团队的经验,过去使用其它的线程模型,如今我们将使用Netty提供的更easy更强大的线程模型来开发。
虽然本章讲述的是Netty的线程模型,可是我们仍然能够使用其它的线程模型。至于怎样选择一个完美的线程模型应该依据应用程序的实际需求来推断。
本章如果例如以下:
- 你明确线程是什么以及怎样使用,并有使用线程的工作经验;若不是这样,就请花些时间来了解清楚这些知识。
推荐一本书:Java并发编程实战。
- 你了解多线程应用程序及其设计,也包含怎样保证线程安全和获取最佳性能。
- 你了解java.util.concurrent以及ExecutorService和ScheduledExecutorService。
15.1 线程模型概述
比如,你有一个餐厅。向你的客户提供食品,食物须要在厨房煮熟后才干给客户;某个客户下了订单后,你须要将煮熟事物这个任务发送到厨房,而厨房能够以不同的方式来处理,这就像一个线程模型,定义了怎样运行任务。
- 仅仅有一个厨师:
- 这样的方法是单线程的。一次仅仅运行一个任务。完毕当前订单后再处理下一个。
- 你有多个厨师,每一个厨师都能够做,空暇的厨师准备着接单做饭:
- 这样的方式是多线程的,任务由多个线程(厨师)运行。能够并行同一时候运行。
- 你有多个厨师并分成组,一组做晚餐,一个做其它:
- 这样的情况也是多线程,可是带有额外的限制;同一时候运行多个任务是由实际运行的任务类型(晚餐或其它)决定。
从上面的样例看出。日常活动适合在一个线程模型。可是Netty在这里适用吗?不幸的是,它没有那么简单。Netty的核心是多线程,但隐藏了来自用户的大部分。
Netty使用多个线程来完毕全部的工作。仅仅有一个线程模型线型暴露给用户。大多数现代应用程序使用多个线程调度工作。让应用程序充分使用系统的资源来有效工作。在早期的Java中,这样做是通过按需创建新线程并行工作。
但非常快发现者不是完美的方案,由于创建和回收线程须要较大的开销。
在Java5中增加了线程池,创建线程和重用线程交给一个任务运行,这样使创建和回收线程的开销降到最低。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYWJjX2tleQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="">
使用多线程在刚開始可能没有什么问题。但随着系统的负载添加。可能在某个点就会让系统崩溃。
让我们来看看Netty是怎样解决问题的。
15.2 事件循环
15.2.1 使用事件循环
Channel ch = ...;
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
System.out.println("run in the eventloop");
}
});
Channel ch = ...;
Future<?> future = ch.eventLoop().submit(new Runnable() {
@Override
public void run() { }
});
if(future.isDone()){
System.out.println("task complete");
}else {
System.out.println("task not complete");
}
检查运行任务是否在事件循环中:
Channel ch = ...;
if(ch.eventLoop().inEventLoop()){
System.out.println("in the EventLoop");
}else {
System.out.println("outside the EventLoop");
}
仅仅有确认没有其它EventLoop使用线程池了才干关闭线程池,否则可能会产生没有定义的副作用。
15.2.2 Netty4中的I/O操作
这些读和写操作是网络API的一部分,通过java和底层操作系统提供。
下图显示在EventLoop上下文中运行入站和出站操作。假设运行线程绑定到EventLoop。操作会直接运行;假设不是,该线程将排队运行:
我们在了解Netty3后会更easy理解为什么新的线程模型是可取的。
15.2.3 Netty3中的I/O操作
问题是。其实,你如今的情况是在调用线程上运行,但捕获到异常事件必须交给工作线程来运行。这是可行的,但如果你忘了传递过去。它会导致线程模型失效;如果入站事件仅仅有一个线程不是真,这可能会给你各种各样的竞争条件。
- 字节写入到远程对等通道有多快
- I/O线程是否繁忙
- 上下文切换
- 锁定
你能够看到非常多细节影响总体延迟。
15.2.4 Netty线程模型内部
假设线程是同样的EventLoop中的一个。讨论的代码块被运行;假设线程不同,它安排一个任务并在一个内部队列后运行。
一般是通过EventLoop的Channel仅仅运行一次下一个事件,这同意直接从不论什么线程与通道交互,同一时候还确保全部的ChannelHandler是线程安全。不须要操心并发訪问问题。
这多少会影响整个系统依赖于EventLoop实现用于特殊传输的实现。
传输之间的切换在你的代码库中可能没有不论什么改变,重要的是:切勿堵塞I/O线程。假设你必须做堵塞调用(或运行须要长时间才干完毕的任务),使用EventExecutor。
15.3 调度任务运行
15.3.1 使用普通的Java API调度任务
- newScheduledThreadPool(int)
- newScheduledThreadPool(int, ThreadFactory)
- newSingleThreadScheduledExecutor()
- newSingleThreadScheduledExecutor(ThreadFactory)
看以下代码:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(10);
ScheduledFuture<?> future = executor.schedule(new Runnable() {
@Override
public void run() {
System.out.println("now it is 60 seconds later");
}
}, 60, TimeUnit.SECONDS);
if(future.isDone()){
System.out.println("scheduled completed");
}
//.....
executor.shutdown();
15.3.2 使用EventLoop调度任务
使用ScheduledExecutorService工作的非常好,可是有局限性,比方在一个额外的线程中运行任务。假设须要运行非常多任务。资源使用就会非常严重;对于像Netty这种高性能的网络框架来说,严重的资源使用是不能接受的。Netty对这个问题提供了非常好的方法。
Channel ch = ...;
ch.eventLoop().schedule(new Runnable() {
@Override
public void run() {
System.out.println("now it is 60 seconds later");
}
}, 60, TimeUnit.SECONDS);
Channel ch = ...;
ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("after run 60 seconds,and run every 60 seconds");
}
}, 60, 60, TimeUnit.SECONDS);
// cancel the task
future.cancel(false);
15.3.3 调度的内部实现
Netty内部实现事实上是基于George Varghese提出的“Hashed and hierarchical timing wheels: Data structures to efficiently implement timer facility(散列和分层定时轮:数据结构有效实现定时器)”。这样的实现仅仅保证一个近似运行,也就是说任务的运行可能不是100%准确;在实践中。这已经被证明是一个可容忍的限制,不影响多数应用程序。所以。定时运行任务不可能100%准确的按时运行。
- 在指定的延迟时间后调度任务。
- 任务被插入到EventLoop的Schedule-Task-Queue(调度任务队列);
- 假设任务须要立即执行。EventLoop检查每一个执行;
- 假设有一个任务要运行,EventLoop将立马运行它,并从队列中删除。
- EventLoop等待下一次执行。从第4步開始一遍又一遍的反复。
由于这种实现计划运行不可能100%正确,对于多数用例不可能100%准备的运行计划任务;在Netty中,这种工作差点儿没有资源开销。可是假设须要更准确的运行呢?非常easy,你须要使用ScheduledExecutorService的还有一个实现,这不是Netty的内容。
记住。假设不遵循Netty的线程模型协议,你将须要自己同步并发訪问。
15.4 I/O线程分配细节
这三个线程会分配给每一个新创建的已连接通道,这是通过EventLoopGroup实现的。使用线程池来管理资源;实际会平均分配通道到全部的线程上,这样的分布以循环的方式完毕。因此它可能不会100%准确,但大部分时间是准确的。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYWJjX2tleQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="">
我们能够使用java.io.*包里的类来开发基于堵塞I/O的应用程序,即使语义改变了。但有一件事仍然保持不变,每一个通道的I/O在同一时候仅仅能被一个线程处理;这个线程是由Channel的EventLoop提供,我们能够依靠这个硬性的规则,这也是Netty框架比其它网络框架更easy编写的原因。