进程是一个动态的过程,是一个活动的实体。简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执行者。可以说,进程中包含了多个可以同时运行的线程。
线程,程序执行流的最小执行单位,是进程中的实际运作单位。
线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗,那为什么我们要使用线程池,主要解决如下几个问题:
创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率
线程并发数量过多,抢占系统资源,从而导致系统阻塞
能够容易的管理线程,比如:线程延迟执行、执行策略等
线程的生命周期,线程的生命周期可以利用以下的图解来更好的理解:
首先使用new Thread()的方法新建一个线程,在线程创建完成之后,线程就进入了就绪(Runnable)状态,此时创建出来的线程进入抢占CPU资源的状态,当线程抢到了CPU的执行权之后,线程就进入了运行状态(Running),当该线程的任务执行完成之后或者是非常态的调用的stop()方法之后,线程就进入了死亡状态。而我们在图解中可以看出,线程还具有一个则色的过程,这是怎么回事呢?当面对以下几种情况的时候,容易造成线程阻塞,第一种,当线程主动调用了sleep()方法时,线程会进入则阻塞状态,除此之外,当线程中主动调用了阻塞时的IO方法时,这个方法有一个返回参数,当参数返回之前,线程也会进入阻塞状态,还有一种情况,当线程进入正在等待某个通知时,会进入阻塞状态。那么,为什么会有阻塞状态出现呢?我们都知道,CPU的资源是十分宝贵的,所以,当线程正在进行某种不确定时长的任务时,Java就会收回CPU的执行权,从而合理应用CPU的资源。我们根据图可以看出,线程在阻塞过程结束之后,会重新进入就绪状态,重新抢夺CPU资源。这时候,我们可能会产生一个疑问,如何跳出阻塞过程呢?又以上几种可能造成线程阻塞的情况来看,都是存在一个时间限制的,当sleep()方法的睡眠时长过去后,线程就自动跳出了阻塞状态,第二种则是在返回了一个参数之后,在获取到了等待的通知时,就自动跳出了线程的阻塞过程。
单线程,顾名思义即是只有一个线程在执行任务,这种情况在我们日常的工作学习中很少遇到,所以我们只是简单做一下了解
多线程,创建多个线程同时执行任务,这种方式在我们的日常生活中比较常见。但是,在多线程的使用过程中,还有许多需要我们了解的概念。比如,在理解上并行和并发的区别,以及在实际应用的过程中多线程的安全问题,对此,我们需要进行详细的了解。
并行和并发:在我们看来,都是可以同时执行多种任务,那么,到底他们二者有什么区别呢?
并发:从宏观方面来说,并发就是同时进行多种时间,实际上,这几种时间,并不是同时进行的,而是交替进行的,而由于CPU的运算速度非常的快,会造成我们的一种错觉,就是在同一时间内进行了多种事情
并行:则是真正意义上的同时进行多种事情。这种只可以在多核CPU的基础上完成。
还有就是多线程的安全问题?为什么会造成多线程的安全问题呢?我们可以想象一下,如果多个线程同时执行一个任务,意味着他们共享同一种资源,由于线程CPU的资源不一定可以被谁抢占到,这是,第一条线程先抢占到CPU资源,他刚刚进行了第一次操作,而此时第二条线程抢占到了CPU的资源,共享资源还来不及发生变化,就同时有两个线程使用了同一条资源,会造成数据不一致性,导致线程执行错误发生。
有造成问题的原因我们可以看出,这个问题主要的矛盾在于,CPU的使用权抢占和资源的共享发生了冲突,解决时,我们只需要让一条线程占用了CPU的资源时,阻止第二条线程同时抢占CPU的执行权,在代码中,我们只需要在方法中使用同步代码块即可。
在Java中,线程池的概念是Executor这个接口,具体实现为ThreadPoolExecutor类,学习Java中的线程池,就可以直接学习它。对线程池的配置,就是对ThreadPoolExecutor构造函数的参数的配置,既然这些参数这么重要,就来看看构造函数的各个参数吧
ThreadPoolExecutor提供了四个构造函数
//五个参数的构造函数public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) //六个参数的构造函数-1public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory) //六个参数的构造函数-2public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler) //七个参数的构造函数public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
其它这四个构造函数,一共牵涉到7个参数类型,下面主要讲解七个参数。
核心线程:
线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程
核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。
如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉
很好理解吧,正常情况下你不干活我也养你,因为我总有用到你的时候,但有时候特殊情况(比如我自己都养不起了),那你不干活我就要把你干掉了
int maximumPoolSize
该线程池中线程总数最大值
线程总数 = 核心线程数 + 非核心线程数。核心线程在上面解释过了,这里说下非核心线程:
不是核心线程的线程(别激动,把刀放下…),其实在上面解释过了
long keepAliveTime
该线程池中非核心线程闲置超时时长
一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉
如果设置allowCoreThreadTimeOut = true,则会作用于核心线程
TimeUnit unit
keepAliveTime的单位,TimeUnit是一个枚举类型,其包括:
NANOSECONDS : 1微毫秒 = 1微秒 / 1000
MICROSECONDS : 1微秒 = 1毫秒 / 1000
MILLISECONDS : 1毫秒 = 1秒 /1000
SECONDS : 秒
MINUTES : 分
HOURS : 小时
DAYS : 天
BlockingQueue workQueue
该线程池中的任务队列:维护着等待执行的Runnable对象
当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务
常用的workQueue类型:
SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大
LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize
ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误
DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务
ThreadFactory threadFactory
创建线程的方式,这是一个接口,你new他的时候需要实现他的Thread newThread(Runnable r)方法,一般用不上。
小伙伴应该知道AsyncTask是对线程池的封装吧?那就直接放一个AsyncTask新建线程池的threadFactory参数源码吧:
new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread new Thread(Runnable r) { return new Thread(r,"AsyncTask #" + mCount.getAndIncrement()); }}
RejectedExecutionHandler handler
这玩意儿就是抛出异常专用的,比如上面提到的两个错误发生了,就会由这个handler抛出异常,你不指定他也有个默认的抛异常能抛出什么花样来?一般情况下根本用不上。
新建一个线程池的时候,一般只用5个参数的构造函数。
向ThreadPoolExecutor添加任务
那说了这么多,你可能有疑惑,我知道new一个ThreadPoolExecutor,大概知道各个参数是干嘛的,可是我new完了,怎么向线程池提交一个要执行的任务啊?
通过ThreadPoolExecutor.execute(Runnable command)方法即可向线程池内添加一个任务
ThreadPoolExecutor的策略
上面介绍参数的时候其实已经说到了ThreadPoolExecutor执行的策略,这里给总结一下,当一个任务被添加进线程池时:
线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
线程数量达到了corePools,则将任务移入队列等待
队列已满,新建线程(非核心线程)执行任务
队列已满,总线程数又达到了maximumPoolSize,就会由上面那位星期天(RejectedExecutionHandler)抛出异常
如果你不想自己写一个线程池,那么你可以从下面看看有没有符合你要求的(一般都够用了),如果有,那么很好你直接用就行了,如果没有,那你就老老实实自己去写一个吧
Java通过Executors提供了四种线程池,这四种线程池都是直接或间接配置ThreadPoolExecutor的参数实现的,下面我都会贴出这四种线程池构造函数的源码,各位大佬们一看便知!
来,走起:
CachedThreadPool()
可缓存线程池:
线程数无限制(没有核心线程,全部是非核心线程)
有空闲线程则复用空闲线程,若无空闲线程则新建线程
一定程序减少频繁创建/销毁线程,减少系统开销
适用场景:适用于耗时少,任务量大的情况
创建方法:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
源码:
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue());}
定长线程池:
创建方法:
//nThreads => 最大线程数即maximumPoolSizeExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads); //threadFactory => 创建线程的方法!ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory);
源码:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());}
2个参数的构造方法源码,不用我贴你也知道他把星期六放在了哪个位置!所以我就不贴了,省下篇幅给我扯皮
ScheduledThreadPool()
定长线程池:
支持定时及周期性任务执行。
有核心线程,也有非核心线程
非核心线程数量为无限大
适用场景:适用于执行周期性任务
创建方法:
//nThreads => 最大线程数即maximumPoolSizeExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);
源码:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize);} //ScheduledThreadPoolExecutor():public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue());}
SingleThreadExecutor()
单线程化的线程池:
有且仅有一个工作线程执行任务
所有任务按照指定顺序执行,即遵循队列的入队出队规则
适用场景:适用于有顺序的任务应用场景
创建方法:
ExecutorService singleThreadPool = Executors.newSingleThreadPool();
源码:
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));}
还有一个Executors.newSingleThreadScheduledExecutor()结合了3和4,就不介绍了,基本不用。
问题:协程存在的原因?协程能够解决哪些问题?
在我们现在CS,BS开发模式下,服务器的吞吐量是一个很重要的参数。其实吞吐量是IO处理时间加上业务处理。为了简单起见,比如,客户端与服务器之间是长连接的,客户端定期给服务器发送心跳包数据。客户端发送一次心跳包到服务器,服务器更新该新客户端状态的。心跳包发送的过程,业务处理时长等于IO读取(RECV系统调用)加上业务处理(更新客户状态)。吞吐量等于1s业务处理次数。
业务处理(更新客户端状态)时间,业务不一样的,处理时间不一样,我们就不做讨论。
那如何提升recv的性能。若只有一个客户端,recv的性能也没有必要提升,也不能提升。若在有百万计的客户端长连接的情况,我们该如何提升。以Linux为例,在这里需要介绍一个“网红”就是epoll。服务器使用epoll管理百万计的客户端长连接,代码框架如下:
hile (1) { int nready = epoll_wait(epfd, events, EVENT_SIZE, -1); for (i = 0;i < nready;i ++) { int sockfd = events[i].data.fd; if (sockfd == listenfd) { int connfd = accept(listenfd, xxx, xxxx); setnonblock(connfd); ev.events = EPOLLIN | EPOLLET; ev.data.fd = connfd; epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); } else { handle(sockfd); } }}
对于响应式服务器,所有的客户端的操作驱动都是来源于这个大循环。来源于epoll_wait的反馈结果。
对于服务器处理百万计的IO。Handle(sockfd)实现方式有两种。
第一种,handle(sockfd)函数内部对sockfd进行读写动作。代码如下
int handle(int sockfd) { recv(sockfd, rbuffer, length, 0); parser_proto(rbuffer, length); send(sockfd, sbuffer, length, 0); }
handle的io操作(send,recv)与epoll_wait是在同一个处理流程里面的。这就是IO同步操作。
优点:
1. sockfd管理方便。
2. 操作逻辑清晰。
缺点:
1. 服务器程序依赖epoll_wait的循环响应速度慢。
2. 程序性能差
第二种,handle(sockfd)函数内部将sockfd的操作,push到线程池中,代码如下:
int thread_cb(int sockfd) { // 此函数是在线程池创建的线程中运行。 // 与handle不在一个线程上下文中运行 recv(sockfd, rbuffer, length, 0); parser_proto(rbuffer, length); send(sockfd, sbuffer, length, 0);}int handle(int sockfd) { //此函数在主线程 main_thread 中运行 //在此处之前,确保线程池已经启动。 push_thread(sockfd, thread_cb); //将sockfd放到其他线程中运行。}
Handle函数是将sockfd处理方式放到另一个已经其他的线程中运行,如此做法,将io操作(recv,send)与epoll_wait 不在一个处理流程里面,使得io操作(recv,send)与epoll_wait实现解耦。这就叫做IO异步操作。
优点:
1. 子模块好规划。
2. 程序性能高。
缺点:
正因为子模块好规划,使得模块之间的sockfd的管理异常麻烦。每一个子线程都需要管理好sockfd,避免在IO操作的时候,sockfd出现关闭或其他异常。
上文有提到IO同步操作,程序响应慢,IO异步操作,程序响应快。
下面来对比一下IO同步操作与IO异步操作。
代码如下:
https://github.com/wangbojing/c1000k_test/blob/master/server_mulport_epoll.c
在这份代码的486行,#if 1, 打开的时候,为IO异步操作。关闭的时候,为IO同步操作。
接下来把我测试接入量的结果粘贴出来。
IO异步操作,每1000个连接接入的服务器响应时间(900ms左右)。
IO同步操作,每1000个连接接入的服务器响应时间(6500ms左右)。
IO异步操作与IO同步操作
有没有一种方式,有异步性能,同步的代码逻辑。来方便编程人员对IO操作的组件呢? 有,采用一种轻量级的协程来实现。在每次send或者recv之前进行切换,再由调度器来处理epoll_wait的流程。
就是采用了基于这样的思考,写了NtyCo,实现了一个IO异步操作与协程结合的组件。https://github.com/wangbojing/NtyCo,
协程框架实现,调度器模式实现,底层原理,多核模式,性能分析,ntyco作者亲讲
协程起源 — 存在的原因?协程能够解决哪些问题?
协程案例 — 如何使用?与线程使用有何区别?
协程实现之工作流程 — 内部是如何工作的?
协程实现之原语操作 — 原语操作有哪些?分别如何实现?
协程实现之切换 — 上下文如何切换?代码如何实现?
协程实现之定义 — 运行体如何定义?调度器如何定义?
协程实现之调度器 — 协程如何被调度?
协程多核模式 — 多核实现
协程性能测试 — 实战性能测试