美文网首页
4、Java基本功-并发编程

4、Java基本功-并发编程

作者: 春涛的随笔 | 来源:发表于2019-06-06 10:06 被阅读0次

    、基本概念

    1、进程与线程

            进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

    2、并发和并行

        当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其他线程处于挂起状态。这种方式我们称之为并发(Concurrent)。

        当系统有一个以上的CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

        区别:并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。

    3、上下文切换

           CPU是通过时间片分配算法循环执行任务,任务从保存到下次加载就是一次上下文切换,通过vmstat的cs参数,可以获取到每秒上下文切换次数,大约1000次。减少上下文的切换在并发编程中要考虑的,通常采用三种方法,1)尽量避免使用锁的方式,采用HASH算法将数据分片,不同任务处理不同的数据;2)尽量采用CAS算法的Atomic包;3)增加协程,利用协程对多任务进行调度,完成任务切换。

    4、锁

            锁(lock)作为用于保护临界区(critical sec-tion)的一种机制,被广泛应用在多线程程序中,比如Java(synchronized,ReentrantLock…)。

            重入锁:就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

            读写锁:在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

    二、Java并发机制原理

    1、volatile

            关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

    2、synchronized

            关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。

            先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式。 1)对于普通同步方法,锁是当前实例对象。 2)对于静态同步方法,锁是当前类的Class对象。 3)对于同步方法块,锁是Synchonized括号里配置的对象。当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁到底存在哪里呢?锁里面会存储什么信息呢?

            Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率,

            使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。例如,针对一个场景,手把手进行锁获取和释放,先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下,syn-chronized关键字就不那么容易实现了,而使用Lock却容易许多。

    3、Lock接口

            锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而JavaSE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的。

            队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

    三、JDK并发类库

    1、ThreadLocal

            ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

            ThreadLocal类在维护变量时,实际使用了当前线程(Thread)中的一个叫做ThreadLocalMap的独立副本,每个线程可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程,避免了线程访问实例变量发生冲突的问题。ThreadLocal本身并不是一个线程,而是通过操作当前线程中的一个内部变量来达到与其他线程隔离的目的。之所以取名为ThreadLocal,所期望表达的含义是其操作的对象是线程的一个本地变量。

            ThreadLocal模式至少从两个方面完成了数据访问隔离,即横向隔离和纵向隔离。有了横向和纵向两种不同的隔离方式,ThreadLocal模式就能真正地做到线程安全;1)纵向隔离——线程与线程之间的数据访问隔离。这一点由线程的数据结构保证。因为每个线程在进行对象访问时,访问的都是各个线程自己的ThreadLocalMap。 2)横向隔离——同一个线程中,不同的ThreadLocal实例操作的对象之间相互隔离。这一点由ThreadLocalMap在存储时采用当前ThreadLocal的实例作为key来保证。

            ThreadLocal具有以下特点: 1)ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量。  2)线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的。 3)在创建ThreadLocalMap之前,会首先检查当前线程中的ThreadLocalMap变量是否已经存在,如果不存在则创建一个;如果已经存在,则使用当前线程已创建的ThreadLo-calMap。 4)使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key进行存储。

    2、ConcurrentHashMap

            ConcurrentHashMap采用了分拆锁的思想,实现使用了一个包含16个锁的数组,每一个锁都守护HashMap的1/16。假设Hash值均匀分布,这将会把对于锁的请求减少到约为原来的1/16。这项技术使得ConcurrentHashMap能够支持16个并发Writer。当多处理器系统的大负荷访问需要更好的并发性时,锁的数量还可以增加。

            ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。

            ConcurrentHashMap常用有3种操作——get操作、put操作和size操作。Segment的get操作实现非常简单和高效。get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空才会加锁重读。put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。

    3、BlockingQueue

            阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。

            阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。

            JDK 7提供了7个阻塞队列,如下。 

            1)ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。 此队列按照先进先出(FIFO)的原则对元素进行排序。

            2)LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。 此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。

            3)PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。 默认情况下元素采取自然顺序升序排列。

            4)DelayQueue:一个使用优先级队列实现的无界阻塞队列。 队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。

            5)SynchronousQueue:一个不存储元素的阻塞队列。 每一个put操作必须等待一个take操作,否则不能继续添加元素。

            6)LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。 相对于其他阻塞队列,LinkedTransfer-Queue多了tryTransfer和transfer方法。

            7)LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。所谓双向队列指的是可以从队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。

    4、CountDownLatch

            CountDownLatch允许一个或多个线程等待其他线程完成操作。CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,只需要把这个CountDownLatch的引用传递到线程里即可。

           CyclicBarrier和CountDownLatch不同,CyclicBarrier是当await的数量到达了设定的数量后,才继续往下执行。CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。 CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数器,并让线程重新执行一次。

    5、FutureTask

            FutureTask可用于要异步获取执行结果或取消执行任务的场景,通过传入Runnable或Callable的任务给FutureTask,直接调用其run方法或放入线程池执行,之后可在外部通过FutureTask的get异步获取执行结果。FutureTask可以确保即使调用了多次run方法,它都只会执行一次Runnable或Callable任务,或者通过cancel取消FutureTask的执行等。可以把FutureTask交给Executor执行;也可以通过ExecutorService.submit方法返回一个FutureTask,然后执行FutureTask.get()方法或FutureTask.cancel方法。除此以外,还可以单独使用FutureTask。FutureTask的实现基于AbstractQueuedSynchronizer(简称为AQS)。

            Future接口可以构建异步应用,但依然有其局限性。它很难直接表述多个Future 结果之间的依赖性。实际开发中,我们经常需要达成以下目的:1)将两个异步计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第一个的结果。2)等待 Future 集合中的所有任务都完成。3)仅等待 Future集合中最快结束的任务完成(有可能因为它们试图通过不同的方式计算同一个值),并返回它的结果。4)通过编程方式完成一个Future任务的执行(即以手工设定异步操作结果的方式)。5)应对 Future 的完成事件(即当 Future 的完成事件发生时会收到通知,并能使用 Future 计算的结果进行下一步的操作,不只是简单地阻塞等待操作的结果)

            JDK1.8才新加入的的CompletableFuture类将使得这些成为可能。实现了Future<T>, CompletionStage<T>两个接口。当一个Future可能需要显示地完成时,使用CompletionStage接口去支持完成时触发的函数和操作。CompletableFuture配合流式编程,速度较快,避免了Future引起的CPU高速轮询、耗资源的问题,推荐使用。

    6、ThreadPoolExecutor

            从JDK 5开始,把工作单元与执行机制分离开来。工作单元包括Runnable和Callable,而执行机制由Executor框架提供。Executor框架主要由3大部分组成如下。 1)任务。包括被执行任务需要实现的接口:Runnable接口或Callable接口。2)任务的执行。包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。3)异步计算的结果。包括接口Future和实现Future接口的FutureTask类。

            ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。可以通过new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,milliseconds,runnableTaskQueue, handler)创建1个线程池;传入参数含义

            1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。

            2)maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。

            3)runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThread-Pool使用了这个队列。PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

            4)ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。

            5)RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。

            6)keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。

            7)TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。

            Executors提供了一些方便创建ThreadPoolExecutor的方法,主要有以下几个;

            1)newFixedThreadPool(int),创建固定大小的线程池,线程keepAliveTime为0,默认情况下,ThreadPoolExecutor中启动的corePoolSize数量的线程启动后就一直运行,并不会由于keepAliveTime时间到达后仍没有任务需要执行就退出。缓冲任务的队列为LinkedBlock-ingQueue,大小为整型的最大数。当使用此线程池时,在同时执行的task数量超过传入的线程池大小值后,将会放入Linked-BlockingQueue,在LinkedBlockingQueue中的task需要等待线程空闲后来执行,当放入LinkedBlockingQueue中的task超过整型最大数时,抛出RejectedExecutionException。

            2)newSingleThreadExecutor(),相当于创建大小为1单位的固定线程池,当使用此线程池时,同时执行的task只有1个,其他task都在LinkedBlockingQueue中。

            3)newCachedThreadPool(),创建corePoolSize为0,最大线程数为整型的最大数,线程keepAliveTime为1分钟,缓存任务的队列为SynchronousQueue的线程池。在使用时,放入线程池的task都会复用线程或启动新线程来执行,直到启动的线程数达到整型最大数后抛出RejectedExecutionException,启动后的线程存活时间为1分钟。

            4)newScheduledThreadPool(int)创建corePoolSize为传入参数,最大线程数为整型的最大数,线程keepAliveTime为0,缓存任务的队列为DelayedWorkQueue的线程池。在实际业务中,通常会有一些需要定时或延迟执行的任务,而对于分布式Java应用而言,更为典型的则是在异步操作时需要超时回调的场景。这种情况下Sched-uledThreadPoolExecutor是不错的选择。

            可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

            可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

    四、JAVA开源框架Netty

    1、概述

            Netty是由JBOSS提供的一个java开源框架。提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

            Netty 对 JDK 自带的 NIO 的 API 进行封装,简化了NIO 的类库和API,提高了可靠性,简化了工作量,解决了客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,避免了原生态NIO中Epoll Bug导致 Selector 空轮询的问题。主要特点有:

        1)设计优雅,适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型 - 单线程,一个或多个线程池;真正的无连接数据报套接字支持(自 3.1 起)。

        2)使用方便,详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK 5(Netty 3.x)或 6(Netty 4.x)就足够了。

        3)高性能,吞吐量更高,延迟更低;减少资源消耗;最小化不必要的内存复制。

        4)安全,完整的 SSL/TLS 和 StartTLS 支持。

        5)社区活跃,不断更新,社区活跃,版本迭代周期短,发现的 Bug 可以被及时修复,同时,更多的新功能会被加入。

            Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。 非常方便定制和开发私有协议栈。典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通信,它的 Netty Service 基于 Netty 框架二次封装实现。

            Netty 作为异步事件驱动的网络,高性能之处主要来自于其 I/O 模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据。

    2、体系结构

            Netty 功能特性如下:

            1)传输服务,支持 BIO 和 NIO。

            2)容器集成,支持 OSGI、JBossMC、Spring、Guice 容器。

            3)协议支持,HTTP、Protobuf、二进制、文本、WebSocket 等一系列常见协议都支持。还支持通过实行编码解码逻辑来实现自定义协议。

            4)Core 核心,可扩展事件模型、通用通信 API、支持零拷贝的 ByteBuf 缓冲对象。

    模块组件

            1)Bootstrap、ServerBootstrap:Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。

            2)Future、ChannelFuture:在 Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

            3)Channel:Netty 网络通信的组件,能够用于执行网络 I/O 操作。Channel 为用户提供:当前网络连接的通道的状态,网络连接的配置参数,提供异步的网络 I/O 操作。不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应。

            4)Selector:Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询(Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。

            5)NioEventLoop:NioEventLoop 中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务。

            6)NioEventLoopGroup:NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。

            7)ChannelHandler: 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。

            8)ChannelHandlerContext:保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象。

            9)ChannelPipline:保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作。ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互。

    3、IO模型

            用什么样的通道将数据发送给对方,BIO、NIO 或者 AIO,I/O 模型在很大程度上决定了框架的性能。

            阻塞 I/O:每个请求都需要独立的线程完成数据 Read,业务处理,数据 Write 的完整操作问题。当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。 I/O 是面向字节流或字符流的,以流式的方式顺序地从一个 Stream 中读取一个或多个字节, 因此也就不能随意改变读取指针的位置。

            I/O 复用模型:在 I/O 复用模型中,会用到 Select,这个函数也会使进程阻塞,但是和阻塞 I/O 所不同的是这两个函数可以同时阻塞多个 I/O 操作。而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。Netty 的非阻塞 I/O 的实现关键是基于 I/O 复用模型。由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。在 NIO 中,引入了 Channel 和 Buffer 的概念,只能从 Channel 中读取数据到 Buffer 中或将数据从 Buffer 中写入到 Channel。基于 Buffer 操作不像传统 IO 的顺序操作,NIO 中可以随意地读取任意位置的数据。

    4、线程处理模型

            数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发,线程模型的不同,对性能的影响也非常大。

            事件驱动模型:发生事件,主线程把事件放入事件队列,在另外线程不断循环消费事件列表中的事件,调用事件对应的处理逻辑处理事件。事件驱动方式也被称为消息通知方式,其实是设计模式中观察者模式的思路。主要包括 4 个基本组件:1)事件队列(event queue):接收事件的入口,存储待处理事件。2)分发器(event mediator):将不同的事件分发到不同的业务逻辑单元。3)事件通道(event channel):分发器与处理器之间的联系渠道。4)事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作。

            Reactor 线程模型:Reactor 是反应堆的意思,Reactor 模型是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多了复用统一监听事件,收到事件后分发(Dispatch 给某进程),是编写高性能网络服务器的必备技术之一。Reactor 模型中有 2 个关键组成:1)Reactor,Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人。2)Handlers,处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。

    Reactor 模型

            Netty 主要基于主从 Reactors 多线程模型(如下图)做了一定的修改,其中主从 Reactor 多线程模型有多个 Reactor:MainReactor 负责客户端的连接请求,并将请求转交给 SubReactor。SubReactor 负责相应通道的 IO 读写请求。非 IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理。

            这里引用 Doug Lee 大神的 Reactor 介绍:Scalable IO in Java 里面关于主从 Reactor 多线程模型的图:

    主从 Rreactor 多线程模型

    5、与Mina的对比

            Netty和Mina都是Trustin Lee(韩国人)的作品,Netty更晚。GitHub主页地址 :https://github.com/trustin。尽管创作者现在已经不专注与开发了。但是框架的后续开发和继承,可以说都是符合最开始的设定的。两个框架的架构设计思路基本一致。Netty从某种程度上讲是Mina的延伸和扩展。解决了一些Mina上的设计缺陷,也优化了一下Mina上面的设计理念。

            Mina将内核和一些特性的联系过于紧密,使得用户在不需要这些特性的时候无法脱离,相比下性能会有所下降,Netty解决了这个设计问题;Netty的文档更清晰;Netty更新周期更短,新版本的发布比较快;架构差别不大,Mina靠apache生存,而Netty靠jboss,和jboss的结合度非常高。

    相关文章

      网友评论

          本文标题:4、Java基本功-并发编程

          本文链接:https://www.haomeiwen.com/subject/khtuiqtx.html