首页 / JAVA  

java深入理解线程和线程池

什么是线程

线程,程序执行流的最小执行单位,是行程中的实际运作单位,经常容易和进程这个概念混淆。那么,线程和进程究竟有什么区别呢?首先,进程是一个动态的过程,是一个活动的实体。简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执行者。可以说,进程中至少包含了一个运行的线程。

线程的生命周期



第一步:是用new Thread()的方法新建一个线程,在线程创建完成之后,线程不会立即执行,而是进入了就绪(Runnable)状态,此时创建出来的线程进入抢占CPU执行权的状态,

第二步:当线程抢到了CPU的执行权之后,线程就进入了运行状态(Running),当该线程的任务执行完成之后或者是非常态的调用的stop()方法之后,线程就进入了死亡状态。

其它状态:

当我们的线程在执行开始到结束中间这个过程,会出现以下几种造成当前线程失去CPU执行权

第一种,当线程主动调用了sleep()方法时,线程会进入则阻塞状态,除此之外,当线程中主动调用了阻塞时的IO方法时,这个方法有一个返回参数,当参数返回之前,线程也会进入阻塞状态,失去CPU执行权。

第二种,当线程进入正在等待wait()某个通知时,会进入阻塞状态,失去CPU执行权。

第三种,我们都知道,CPU的资源是十分宝贵的,所以,当线程正在进行某种不确定时长的任务时,Java就会收回CPU的执行权,当前线程则又进入就绪状态,从而合理应用CPU的资源。

第四种,当遇到锁的时候,当前线程会进入同步阻塞状态,失去CPU执行权;阻塞结束后进入就绪状态

我们根据图可以看出,不管是线程在阻塞过程结束之后还是正在运行的线程CPU执行权被回收,它们都会重新进入就绪状态,重新抢夺CPU资源。

如何跳出阻塞过程呢?

从以上几种可能造成线程阻塞的情况来看,都是存在一个时间限制的,

第一种:当sleep()方法的睡眠时长过去后,线程就自动跳出了阻塞状态,

第二种:当调用wait()方法时,在获取到了等待的notify()通知时,就自动跳出了线程的阻塞过程

什么是单线程和多线程?

单线程,顾名思义即是只有一条线程在执行任务

多线程,创建多条线程"同时"执行任务;但是,在多线程的使用过程中,还有许多需要我们了解的概念。比如,在理解上并行和并发的区别,以及在实际应用的过程中多线程的安全问题,对此,我们需要进行详细的了解。

并行和并发:在我们看来,都是可以同时执行多种任务,那么,到底他们二者有什么区别呢?

并发,从宏观方面来说,并发就是同时进行多种时间,实际上,并不是同时进行的,而是交替进行的,只有获取到CPU的执行权的线程,才有资格执行任务;由于CPU的运算速度非常的快,会造成我们的一种错觉,就是在同一时间内进行了多种事情

什么时候会产生并发:

比如同一时刻有多个客户端(多个线程)同时请求服务端并访问同一资源会产生并发问题。
 银行两个工作人员同时操作同一账户就是典型的例子。比如A、B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户减去50元,A先提交,B后提交。 最后实际账户余额为1000-50=950元,但本该为 1000+100-50=1050。这就是典型的并发问题。
 
 并发产生原因:

 由于CPU的时间是按时间片分的,而不是一个时间点,并发问题是由于CPU线程切换导致的(只有获取到CPU执行权的线程才能执行操作任务)。

假设有一段代码

//增加现金
increaseCash(){
A工作人员读取账户的余额为1000元 // -------01
执行增加操作,为账户增加100元   // -------02

// 扣除现金
deductionCash() {
   B工作人员读取账户的余额为1000 // --------01
   执行扣除操作,为账户减少50元   // -------02
}

当A,B俩个工作人员(A,B线程)同时请求服务器执行操作时,假设A线程先被CPU调度,然而A线程在increaseCash()操作中,程序只执行到了01这一步
,时间片到期了(即失去了CPU的执行权),此时A线程的代码并没有执行完,但是CPU此时会调度B线程,并不会管A线程是不是执行完了increaseCash()里所有的操作。

  再接着当B线程在执行deductionCash()操作时,程序也只执行到了01这一步,此时CPU 现在又会调度A线程(B线程此时失去了CPU执行权),并且A线程从increaseCash()操作的01处继续执行(注意不是重新执行,CPU切换的时候保存了线程的上下文)

所以在整个操作过程中,就会账户余额与实际的不符的情况,也就是所谓的并发,造成了多线程下的线程安全问题;

并行,则是真正意义上的同时进行多种事情。这种只可以在多核CPU的基础下完成。


为什么会造成多线程的安全问题呢?我们可以想象一下,如果多个线程同时执行一个任务,name意味着他们共享同一种资源,由于线程CPU的资源不一定可以被谁抢占到,这是,第一条线程先抢占到CPU资源,他刚刚进行了第一次操作,而此时第二条线程抢占到了CPU的资源,name,共享资源还来不及发生变化,就同时有两条数据使用了同一条资源,具体请参考多线程买票问题。这个问题我们应该如何解决那?

由造成问题的原因我们可以看出,这个问题主要的矛盾在于,CPU的使用权抢占和资源的共享发生了冲突,解决时,我们只需要让一条线程战歌了CPU的资源时,阻止第二条线程同时抢占CPU的执行权,在代码中,我们只需要在方法中使用同步代码块即可(可以使用锁)。


线程池

在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。

线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。

那么,我们应该如何创建一个线程池那?Java中已经提供了创建线程池的一个类:Executor

而我们创建时,一般使用它的子类:ThreadPoolExecutor.

public ThreadPoolExecutor(int corePoolSize,  
                              int maximumPoolSize,  
                              long keepAliveTime,  
                              TimeUnit unit,  
                              BlockingQueue<Runnable> workQueue,  
                              ThreadFactory threadFactory,  
                              RejectedExecutionHandler handler)

这是其中最重要的一个构造方法,这个方法决定了创建出来的线程池的各种属性,下面依靠一张图来更好的理解线程池和这几个参数:

corePoolSize:就是线程池中的核心线程数量,这几个核心线程,即使在没有用即空闲的时候,也不会被回收

maximumPoolSize:就是线程池中可以容纳的最大线程的数量

keepAliveTime:就是线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间

util:就是计算这个时间的一个单位

workQueue:就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)

threadFactory:就是创建线程的线程工厂

handler:是一种拒绝策略,我们可以在任务满了之后,拒绝执行某些任务




由图我们可以看出线程池执行步骤:

1.任务进来时,首先判断核心线程数是否已满,如果没有满,就创建核心线程执行任务;

2.如果核心线程已满,则判断任务队列是否已满, 如果没有满,就将任务放入任务队列中,等待执行(有可能是核心线程执行也有可能是非核心线程来执行),

3.如果任务队列满了,则判断最大可容纳的线程数,如果没有超出这个数量,就创建非核心线程执行任务,

4.如果超出最大可容纳的线程数了,就调用handler实现拒绝策略。


handler的拒绝策略:

有四种:

第一种AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满

第二种DisCardPolicy:不执行新任务,也不抛出异常

第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行

 第四种CallerRunsPolicy:直接调用execute来执行当前任务

(上图参考线程池执行4个步骤有助理解)

四种常见的线程池:

CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。

SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。

SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。

FixedThreadPool:定长的线程池,只有核心线程,核心线程数即为最大的线程数量,没有非核心线程



2019-10-24