1、【java线程及线程池系列】java线程及线程池概念详解

本文详细阐述了Java线程的概念,包括线程的生命周期、状态转换、线程间通信、调度策略、中断机制以及守护线程。同时,讨论了线程池的原理,如线程池状态、任务执行、线程初始化和关闭,以及线程池的配置和拒绝策略。此外,还提到了Runnable和Callable接口的区别,以及ExecutorService的使用。文章强调了线程池的合理配置和使用对并发性能的影响,以及如何通过设置线程池参数来优化并发程序。
摘要由CSDN通过智能技术生成

java线程及线程池 系列文章

1、【java线程及线程池系列】java线程及线程池概念详解
2、【java线程及线程池系列】synchronized、ReentrantLock和ReentrantReadWriteLock介绍及示例
3、【java线程及线程池系列】线程池ThreadPoolExecutor的类结构、使用方式示例、线程池数量配置原则和线程池使用注意事项



本文介绍了线程与线程池的概念介绍以及线程的创建,是该系列的起步篇。
阅读本文需要对java有比较深入的了解。
本文部分图片来源于互联网。

一、线程原理和概念

Java 给多线程编程提供了内置的支持。一个多线程程序包含两个或多个能并发运行的部分。程序的每一部分都称作一个线程,并且每个线程定义了一个独立的执行路径。多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守候线程都结束运行后才能结束。多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。

1、进程与线程

  • 进程是操作系统分配资源的单位,这里的资源包括CPU、内存、IO、磁盘等等设备,进程之间切换时,操作系统需要分配和回收这些资源,所以其开销相对较大(远大于线程切换);
  • 线程是CPU分配时间的单位,理论上,每个进程至少包含一个线程,每个线程都寄托在一个进程中。一个线程相当于是一个进程在内存中的某个代码段,多个线程在切换时,CPU会根据其优先级和相互关系分配时间片。除时间切换之外,线程切换时一般没有其它资源(或只有很少的内存资源)需要切换,所以切换速度远远高于进程切换。

进程的调度以时间片为单位进行,如果两个进程都分得一个同等长度的时间片,其中进程A只有一个线程,另一个进程B有10个线程,那么两个进程执行的时间片相同,但A的线程执行的时间是B的线程的10倍。

一般来说,线程可以分为内核级线程和用户级线程。

  • 内核级线程是需要内核支持的线程,其创建、撤销、切换,需要内核系统的支持,在操作系统内核中为每个内核线程分配有一个内核控制模块,用以感知和控制线程。事实上,可以将内核系统程序当成一个大的内核进程,内核级线程就是这个内核进程的一个线程。
  • 用户级线程只存在于用户空间中,其创建、撤销、切换,都不需要内核系统支持,用户级线程的切换,发生在用户进程内部,切换很快捷。

在java中,一般只关注用户级线程,所有java的线程,就是在java的JVM主进程下启动的各个线程。
java的各个线程并发执行,其实往往只是一种错觉,对于单核cpu而言,java各线程只是按调度策略执行一个个的时间段,所以在一个cpu中,一个时间点上只有一个线程在执行的,但可能还没执行完,就轮到下个线程执行了。对于多核cpu而言可能存在绝对意义上的线程并发(即两个线程在两个cpu中同时执行)。

在Java中,还常常遇到用户线程和守护(后台)线程的概念。

2、线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
在Java中,线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。
下图显示了一个线程完整的生命周期。
在这里插入图片描述

1)、创建状态

在生成线程对象,并且还没有调用该对象的start方法时,这是线程处于创建状态,在这种状态下,用getState()方法可以获取当前线程的状态,状态值为State.NEW。

2)、就绪状态

当调用了线程对象的start方法之后,并且该线程没有被BLOCK,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。如果此时调用getState()方法,会得到State.RUNABLE;

3)、运行状态

线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。运行状态仅仅发生在处于就绪状态的线程获得了JVM的调度的情况下,所以处于运行状态的线程没有专门定义RUNNING状态,对于处于运行状态的线程,调用的getState()获得的仍然是State.RUNABLE。

4)、阻塞状态

线程正在运行的时候,被暂停,通常是为了等待某个事件的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait,join等方法都可以导致线程阻塞。
在阻塞状态下,调用getState()可能获得3种线程状态:

  • BLOCKED:这种状况仅仅会发生在线程期望进入同步代码块或同步方法(synchronized or Lock),并且尚未获得锁的情况下,这有两种可能,一种是线程直接从运行(RUNNING)状态抢锁进入同步代码;另一种是线程在执行了wait方法后,被notify/notifyAll唤醒(或wait(long)时间到期),然后希望重新抢锁的情况下,可以直接进入BLOCKED状态。处于BLOCKED状态的线程,只有在获得了锁之后,才会脱离阻塞状态
  • TIMED_WAITING:如果线程执行了sleep(long)/join(long),wait(long),会触发线程进入TIMED_WAITING状态,在这种状态下,与普通的WAITING状态相似,但是当设定的时间到了,就会脱离阻塞状态
  • WAITING:如果调用join()或wait()方法,就进入了WAITING状态,在该状态下,如果是因为调用了join()方法进入WAITING,则当join的目标线程执行完毕,该线程就会进入到RUNNABLE状态,如果是因为调用了wait()进入的WAITING,则需要等锁对象执行了notify()或notifyAll()之后才能脱离阻塞。

无论上面3种哪种阻塞状态,都只能是从运行(RUNNING,而不是RUNABLE)状态而非其它任何状态转换得来。

5)、死亡状态

如果一个线程的run方法执行结束或抛出异常或者调用stop方法并完成对线程的中止之后,该线程就会死亡。此时,再调用getState()方法,得到的是State.TERMINATED。对于已经死亡的线程,无法再使用start方法令其进入就绪。
对于线程的几种状态,下图说明了状态之间的转换关系。
在这里插入图片描述

3、线程的局部变量表

一个Thread类实例只是一个对象,像Java中的任何其他对象一样,具有变量和方法,生死于堆上。
Java中,每个线程都有一个调用栈,即使你不在Java程序中创建任何新的线程,线程也在后台运行着(如:main线程)。一个Java应用总是从main()方法开始运行,mian()方法运行在一个线程内,它被称为主线程。一旦创建一个新的线程,就产生一个新的调用栈。

Java的虚拟机内存分为几个部分,其中线程共享的是Heap和Method area,线程私有的是jvm stack,native method stack和program counter register(程序计数器)

heap,由各个线程共享,存储的是对象的实例;
jvm stack,指的是虚拟机栈(在HotSpot虚拟机中,本地方法栈是与虚拟机栈放在一起实现的,所以这里不再专门区分),存储的是局部变量表、动态链接、方法出口。

局部变量表是什么呢?它是每个线程私有一份的,内存空间在编译时期就已经确定了,在执行时,不会改变局部变量表的大小。
其中存储的主要是下面的数据,

  • 1、基本数据类型(boolean、byte、char、short、int、float),每个变量占有一个内存单元。这些变量在堆内存中也有一份,在每个线程的局部变量表中,存储的不过是堆内存中这份数据的一份拷贝。
  • 2、基本数据类型(long、double),与其它基本数据类型相同,他们也是局部变量表中的一份拷贝,不过区别在于这两个类型的数据占有2个内存单元(64位机器除外);
  • 3、String类型,String是一个很特殊的类型:String与基本数据类型一样,是覆盖型修改的,也就是说,String是不可变字符序列,String一旦创建,就不能对其value进行修改,如果要修改,只能先创建一个新的对象,并将引用指向新对象。

String有两种声明方式:
String aaa=“abcd”;
String bbb=new String(“abcd”);
第一种声明方式是直接在常量池中找一下有没有现成的"abcd"串的存在,如果有,将aaa的引用指向该串,如果没有,在常量池中新产生一个"abcd",并将aaa的引用指向该串;
第二种声明方式是现在堆中new一个String对象(只要用到new,就一定是先在堆中创建对象"abcd"),然后bbb的应用指向该对象。这个对象与常量池中的"abcd"没有关系,只有在调用bbb.intern()方法时,才能查到常量池中的"abcd"串。
无论上面哪种声明方式,String都具有不可变性,所以,虽然局部变量表中保持的是String的一份引用,但是这份引用是堆中引用的一个副本,可能出现主内存和线程local内存不同步更新的情况,因此类似于基本数据类型。

  • 4、普通对象引用,注意,线程中存储的没有对象,只有对象引用,可能通过句柄方式或直接引用方式来查找到堆上的对象
  • 5、返回地址(returnAddress),指向了一条字节码指令的地址,不属于变量的一部分

在上面的描述中,把那些非常量的 基本数据类型、String、普通对象引用 统称为线程中的“变量”。

4,Java内存模型

把上面所说的堆内存中的对象和基本数据类型的备份,称为主内存(main memory),把上面所说的栈内存中用于存储变量的部分内存,称为本地内存(local memory)(或叫工作内存),这就组成了Java内存模型(JMM)。

  • Java线程对于变量的所有操作(读取、赋值),都是在自己的工作内存中进行的,线程不直接读写主内存中的变量。
  • 不同线程无法直接访问对方工作内存中的变量;
  • 线程间变量值的传递,需要主内存来完成。
    如下图所示

在这里插入图片描述
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成。

1)、Java内存模型八种操作

  • lock(锁定),作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁),作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取),作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入),作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用),作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
  • assign(赋值),作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
  • store(存储),作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
  • write(写入),作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中

如果要把一个变量从主内存中复制到工作内存,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。

Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

2)、Java内存模型八种操作规则

  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,不许先执行过了assign和load操作
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量
  • 对一个变量执行unlock操作之前,必须先把次变量同步到主内存中(执行store和write操作)。

5、线程间通信

Java线程间通信,使用的是共享内存模型,而非消息传递模型,即线程间是通过write-read内存中的公共状态来进行隐式通信的,多个线程之间不能直接传递数据交互,它们之间的交互只能通过共享变量来实现。

1)、线程间通信的步骤

  • 线程A把本地内存A中更新过的共享变量刷新到主内存中去
  • 线程B到主内存中去读取线程A之前已更新过的共享变量
    在这里插入图片描述

6、重排序和一致性规则

Java的内存模型与硬件系统内存模型是相对应的,主内存相当于硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中,而每个线程的本地内存就相当于是寄存器和高速缓存。

基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主存,多个处理器运算任务都涉及同一块主存,需要一种协议可以保障数据的一致性,这类协议有MSI、MESI、MOSI及Dragon Protocol等。

为了使得处理器内部的运算单元能尽可能被充分利用,处理器可能会对输入代码进行乱起执行(Out-Of-Order Execution)优化,处理器会在计算之后将对乱序执行的代码进行结果重组,保证结果准确性。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Recorder)优化。

重排序分成三种类型

  • 编译器优化的重排序,编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序,现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序,由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
    在这里插入图片描述

7、线程调度和常见内部方法

Java线程的调度是多线程执行的核心,良好的调度策略,可以充分发挥系统的性能,并提高程序的执行效率。Java线程的执行具有一定的控制粒度,即,你编写的线程调度策略只能最大限度的控制和影响线程执行次序,而无法做到精准控制。

1)、线程交互和协作方法

  • wait():调用一个对象的wait方法,会导致当前持有该对象锁的线程等待,直到该对象的另一个持锁线程调用notify/notifyAll唤醒
  • wait(long timeout):与wait相似,不过除了被notify/notifyAll唤醒以外,超过long定义的超时时间,也会自动唤醒
  • wait(long timeout, int nanos):与wait(long)相同,不过nanos可以提供纳秒(毫微秒)级别的更精确的超时控制
  • notify():调用一个对象的notify()方法,会导致当前持有该对象锁的所有线程中的随机某一个线程被唤醒
  • notifyAll():调用一个对象的notifyAll(),会导致当前持有该对象锁的所有线程同时被唤醒。

1、对于wait/notify/notifyAll的调用,必须在该对象的同步方法或同步代码块中
2、wait方法的调用会释放锁,而sleep或yield不会
3、当wait被唤醒或超时时,并不是直接进入运行态或就绪态,而是先进入Blocked态,抢锁成功,才能进入运行态
4、notify和notifyAll的区别在于:
notify唤醒的是对象多个锁线程中的一个线程,这个线程进入Blocked状态,开始抢锁,当这个线程执行完释放锁的时候,即使现在没有其它线程占用锁,其它处于wait状态的线程也会继续等待notify而不是主动去抢锁
notifyAll,一旦notifyAll消息发出,所有wait在这个对象上的线程都会去抢锁,抢到锁的执行,其它线程Blocked在这个锁上,当抢到锁的线程执行完成释放锁之后,其它线程自动抢锁
线程wait后的唤醒过程必须是:wait-notify-抢锁-执行-释放锁
5、notify和wait必须加循环进行保证,没有循环条件保证的话,如果有多个wait线程在等待notify,当notifyAll发出时,两个wait线程同时被唤醒,进入RUNABLE状态,如果此时他们竞争一个非锁资源,则只有一个能抢到,另一个虽然抢不到,但因为是非锁资源,所以会继续执行,就容易造成问题。
在java中,还有另一对方法suspend()和resume(),他们的作用类似于wait()/notify(),区别在于,suspend()和resume()不会释放锁,所以这两个方法容易造成死锁问题。现在这两个方法已经不再使用了

2)、休眠

sleep方法如其名,是让线程休眠的,而且是哪个线程调用,就是哪个线程休眠。可以是调用TimeUtil.sleep(long)、Thread.sleep(long),或当前线程对象t上的t.sleep(long),其结果都是当前线程休眠。

sleep方法有两个:sleep(long timeout), sleep(long timeout,int nanos),两个方法功能相似,后一种方法能够提供纳秒级别的控制。

sleep是Thread类的方法,不是对象的,也无法通过notify来唤醒,当sleep的时间到了,自然会唤醒。

在sleep休眠期间,线程会释放出CPU资源给其它线程,但仍占有锁,而不会释放锁。

sleep()的调用使得其它低优先级、同等优先级、高优先级的线程有了执行的机会。

3)、优先级

java线程的优先级并不绝对,它所控制的是执行的机会,也就是说,优先级高的线程执行的概率比较大,而优先级低的线程只是执行的概率相对低一些。Java线程一共有10个优先级,分别为1-10,数值越大,表明优先级越高,一个普通的线程,其优先级为5;

线程的优先级具有继承性,如果一个线程B是在另一个线程A中创建的,则B叫做A的子线程,B的初始优先级与A保持一致。

java中使用t.setPriority(n)来设置优先级,n必须为1-10之间的整数,否则会抛异常。

Java各优先级线程间具有不确定性,由于操作系统的不同,不同优先级的线程会有很大的表现上的不同,所以很难比较或统计。不过,需要注意的是编码过程中,最好不要有代码逻辑是依赖于线程优先级的,不然可能造成问题,因为在Java中,高优先级不一定比低优先级先执行,也不一定比他低优先级线程被调度到的几率大。

对于线程组ThreadGroup的优先级,具有特殊性,简单的说,就是线程组内的线程,优先级不能超过线程组的整体设置。

4)、让步

Java的让步使用的是Thread.yield()静态方法,功能是暂停当前线程的执行,并让步于其它同优先级线程,让其它线程先执行。

yield()仅仅是让出CPU资源,但是让给谁,是由系统决定的,是不确定的,当前线程使用yield()让出资源后,线程不会释放锁,而是回到就绪状态,等待调度执行。

yield()只是使当前线程重新回到可执行状态,所有执行yield()的线程有可能在进入到可执行状态后马上又被执行,所以yield()方法只能使同优先级的线程有执行的机会,如果调用yield()后发现没有同等级别的其它线程,则当前线程会立即重新进入运行态。

yield()从某种程度上与sleep()相似,但yield不能指定让步的时间。而且,sleep()让出的机会并不限制其它线程的优先级,而yield仅限于其它同优先级线程。

实际上,yield()方法对应了如下操作;先检测当前是否有相同优先级的线程处于可运行状态,如有,则把CPU的占有权交给次线程,否则继续运行原来的线程,所以yield()方法称为“退让”,它把运行机会让给了同等级的其他线程。

5)、合并

Java线程见合并,使用的是join方法。join()方法做的事情是将并行执行的线程合并为串行执行的,例如,如果在线程ta中调用tb.join(),则ta会停止当前执行,并让tb先执行,直到tb执行完毕,ta才会继续执行。

join方法有3个重载方法。

  • t.join()是允许t插队到自己前面,等t执行完成再执行自己;
  • t.join(long timeout)是允许t插队到自己前面,等待t执行,且最长只等待timeout毫秒(不管t有没有被调度执行,当前调用t.join的线程都只等timeout的时间);
  • t.join(long timeout, int nanos)与t.join(long)一样,只不过可以提供纳秒级的精度;

如果当前线程ta调用tb.join(),tb开始执行,ta进入WAITING或TIMED_WAITING状态。

如果ta调用tb.join(),则ta会释放当前持有的锁。事实上,join是通过wait/notify来实现的,当ta调用tb.join(),ta就wait在tb对象上,同时释放锁,tb对象抢锁执行,当执行完成后,tb自己发出notify通知。触发ta继续执行,注意,当tb用notify通知ta后,ta还要重新抢锁。

Java7中,出现了一个新的模式:fork()/join()模式,采用的是分而治之的思想来实现并发编程,fork用于将现场拆分成多个小块并行执行,join用于合并结果。不过这个模式容易出问题,要慎用。

6)、守护线程

如果对一个线程t,调用t. setDaemon(true),则可以将该线程设置为守护线程,JVM判断程序运行结束的标准是所有用户线程执行完成,当用户线程全部结束,即使守护线程仍在运行,或尚未开始,JVM都会结束。

不能将正在运行的线程设置为守护线程,因此t.setDaemon(true)方法必须在t.start()之前调用;如果在一个守护线程中new出来一个新线程,即使不执行setDaemon(true),新的线程也是守护线程;守护线程一般用来做GC、后台监控、内存管理等后台型任务,且这些任务即使随时被结束,也不影响整体程序的运行。

7)、中断

在线程中,中断是一个重要的功能。在Java线程中,中断有且只有一个含义,就是让线程退出阻塞状态,t.interrupt()只是向线程t发出一个中断信号,让该线程退出阻塞状态。

  • 1、如果线程t当前是可中断的阻塞状态(如调用了sleep、join等方法导致线程进入WATING / TIMED_WAITING状态),在任意其它线程中调用t.interrupt(),那么线程会立即抛出一个InterruptedException,退出阻塞状态
  • 2、如果是调用wait进入的WAITING / TIMED_WAITING状态,调用了t.interrupt()后,需要先等线程抢到锁,脱离BLOCKED状态,才会抛出InterruptException
  • 3、如果线程t当前是不可中断的阻塞状态(如不能中断的IO操作、尚未获取锁的BLOCKED状态),调用了t.interrupt()后,则需要等到脱离了阻塞状态之后,才立即抛出InterruptedException
  • 4、如果线程t当前处在运行状态,则调用了t.interrupt(),线程会继续运行,直到发生了sleep、join、wait等方法的调用,才会在进入阻塞之后,随后立即抛出InterruptedException,跳出阻塞状态

在sleep、wait、join方法中,会不断的检查中断状态的值,如果发现中断状态为true,则立即抛出InterruptedException,并尝试跳出阻塞(用尝试的原因是wait方法阻塞的线程可能需要先抢锁)

示例下面3个方法的实际效果如下

  • 1、sleep() & interrupt()
    线程A正在使用sleep()暂停着: Thread.sleep(1000);
    如果要取消他的等待状态,可以在正在执行的线程里(比如这里是B)调用
    a.interrupt();
    令线程A放弃睡眠操作,这里a是线程A对应到的Thread实例
    当在sleep中时线程被调用interrupt()时,就马上会放弃暂停的状态,并抛出InterruptedException。丢出异常的是A线程。
  • 2、 wait() & interrupt()
    线程A调用了wait()进入了等待状态,也可以用interrupt()取消。
    不过要小心锁定的问题。
    线程在进入等待区,会把锁定解除,当对等待中的线程调用interrupt()时,会先重新获取锁定,再抛出异常。在获取锁定之前,是无法抛出异常的。
  • 3、join() & interrupt()
    当线程以join()等待其他线程结束时,当它被调用interrupt(),它与sleep()时一样,会马上跳到catch块里.。
    对谁调用interrupt()方法,一定是调用被阻塞线程的interrupt方法。如在线程a中调用来线程t.join()
    则a会等t执行完后在执行t.join后的代码,当在线程b中调用来 a.interrupt()方法则会抛出InterruptedException,当然join()也就被取消了。
    Thread.interrupted()方法可以用来判断当前线程的中断状态,不过,需要注意的是该方法同时具有清除中断位的作用,也就是说,如果一个线程t被中断了,当该方法第一次被调用时,返回结果为true,同时中断状态被清除,第二次被调用时,返回结果为false;
    一般的,在线程的run方法中,可以采用try catch捕获异常+ 中断状态判断相结合的方式来判断:

try {
//检查程序是否发生中断 
while (!Thread. interrupted()) {
	System.out.println( "I am running!");
	//point1 before sleep 
	Thread.sleep( 20);
	//point2 after sleep 
	System.out.println( "Calculating");
	}
} catch (InterruptedException e) {
	System.out.println( "Exiting by Exception");
}
System.out.println( "ATask.run() interrupted!" );

如果一个线程无法响应中断(比如线程的run中是一个不sleep的死循环),则可能永远无法用interrupt()方法来结束它的运行。这一点在Java线程池(ThreadPoolExecutor)中用到,因为线程池有可能调用shutdown()或shutdownNow()来结束池中线程,这两个方法都是通过interrupt来执行的,如果其中一个线程无法中断,那线程池的这个方法就可能达不到预期效果。

8、线程接口创建

1)、Runable与Callable

创建Java线程除了使用Thread类之外,还可以有Runable和Callable两种,这两者都是可以实现并发线程的接口

  • 区别是

1,Runnable是JDK1.1中就出现的,属于包java.lang,而Callable是在JDK1.5才提供的,属于java.util.concurrent;
2,Runnable中要实现的是void run()方法,没有返回值,而Callable要实现的是V call()方法,返回一个泛型V的返回值(通过Future.get()方法获取);
3,Runnable中抛出的异常,在线程外是无法捕获的,而Callable是可以抛出Exception;
4,Runnable和Callable都可以用于ExecutorService,而Thread类只支持Runnable,当然,可以用FutureTask对Callable进行封装,并用Thread类才能运行;
5,运行Callable可以得到一个Future对象,用于表示异步计算的结果,类似于CallBack,而Runable不行;

运行Runnable和Callable的代码示例如下

  • Runnable
class SomeRunnable implements Runnable
{
	public void run()
	{
		//do something here
	}
}

Runnable oneRunnable = new SomeRunnable ();
Thread oneThread = new Thread(oneRunnable);
oneThread.start();
  • Callable
class SomeCallable implements Callable <String>
{
	public String call() throws Exception {
		// do something
		return "" ;
	}
}

Callable<String> oneCallable = new SomeCallable();
FutureTask<String> oneTask = new FutureTask<String>(oneCallable);
Thread twoThread = new Thread (oneTask);
twoThread.start();

2)、Future和FutureTask

Future是对Runnable或Callable的任务结果进行查询、获取结果、取消等操作的异步管理类(Future对Runnable的管理是通过FutureTask实现的)
Future与Callable一样位于java.util.concurrent包下,是一个泛型接口。
提供如下方法:

//该方法用于取消任务,如果取消成功,返回true,如果取消失败,返回false。mayInterruptIfRunning参数表示的是是否允许取消正在执行且没有完毕的任务,true表示可以取消正在执行的任务;如果任务已经执行完成,无论参数是什么,该方法都返回false;如果任务正在执行:若参数为true,则取消成功的话返回true;若参数为false,则直接返回false;若任务尚未执行,则无论参数是什么,都在取消成功后返回true;
boolean cancel(boolean mayInterruptIfRunning);
//该方法判断任务是否被取消成功,如果在任务正常完成前被取消成功,则返回true;
boolean isCancelled();
//该方法判断任务是否正常完成,如果是,返回true;
boolean isDone(); 
// 该方法用于获取执行结果,调用该方法后,会产生阻塞,调用者会一直阻塞直到任务完毕返回结果才继续执行;
V get() throws InterruptedException, ExecutionException;
//该方法与get相似,只不过,有超时限制,如果到了指定时间还没有得到结果,则返回null;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;

Future是一个接口,无法用于直接创建对象,而且Runnable也无法直接用Future,所以就有了FutureTask,FutureTask也位于java.util.concurrent包,
FuntureTask的实现如下:

public class FutureTask<V> implements RunnableFuture<V>

就是说,FutureTask实现了RunnableFuture接口,而RunnableFuture接口是怎么回事呢?

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

可见,FutureTask实际上同时实现了Future接口和Runnable接口,所以它既可以作为Runnable被Thread线程执行,也可以作为Future得到Callable的返回值;
FutureTask提供了两个构造器:

public FutureTask(Callable<V> callable) {}
public FutureTask(Runnable runnable, V result) {}

用这两个构造器,FutureTask可以对callable和runnable的做出实现,并且由于FutureTask实现了Future接口,所以可以实现对于callable和runnable的管理。

9、线程池

线程池存在的目的在于提前创建好需要的线程,减少临时创建线程带来的资源消耗。而且每个ThreadPoolExecutor线程池还维护者一些统计数据,如完成的任务数,可以方便的进行统计,同时该类还提供了很多可调整的参数和扩展的钩子(hook)。java.util.concurrent中,关于线程池提供了很多接口和类,这些接口和类的关系如下:
在这里插入图片描述

  • Executor是顶层接口,其中只有一个execute(Runnable)的声明,返回值是void;
  • ExecutorService接口集成了Executor接口,同时提供了submit、invokeAll、invokeAny、shutDown等方法;
  • AbstractExecutorService实现了ExecutorService接口,并基本实现其所有方法;
  • ThreadPoolExecutor继承了类AbstractExecutorService;并提供了execute()/submit()/shutdown()/shudownNow()等方法的具体实现(execute是提供了具体实现,其它方法用了超类的实现);

execute和submit的区别在于:
execute是定义在Executor中,并在ThreadPoolExecutor中具体实现,没有返回值,其作用就是向线程池提交一个任务并执行;
submit是定义在ExecutorService中,并在AbstractExecutorService中具体实现,且在ThreadPoolExecutor中没有对其进行重写,submit能够返回结果,其内部实现,其实还是在调用execute(),不过,它利用Future&FutureTask来获取任务结果
ExecutorService提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。可以关闭ExecutorService,这将导致其拒绝新任务。

  • 提供两个方法来关闭 ExecutorService:
    shutdown()方法在终止前允许执行以前提交的任务;
    shutdownNow()方法阻止等待任务的启动并试图停止当前正在执行的任务。在终止后,执行程序没有任务在执行,也没有任务在等待执行,并且无法提交新任务。应该关闭未使用的 ExecutorService以允许回收其资源;
  • 通过创建并返回一个可用于取消执行和/或等待完成的 Future,方法submit扩展了基本方法Executor.execute(java.lang.Runnable)。
  • 方法 invokeAny和invokeAll是批量执行的最常用形式,它们执行任务 collection,然后等待至少一个,或全部任务完成(可使用 ExecutorCompletionService类来编写这些方法的自定义变体)。

1)、线程池的状态

在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态。

  • 状态
volatile int runState;
static final int RUNNING    = 0;
static final int SHUTDOWN   = 1;
static final int STOP       = 2;
static final int TERMINATED = 3;
  • volatile
    针对多线程使用的变量如果不是volatile或者final修饰的,很有可能产生不可预知的结果(另一个线程修改了这个值,但是之后在某线程看到的是修改之前的值)。其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的,本质上,volatile就是不去缓存,直接取值。在线程安全的情况下加volatile会牺牲性能。
    runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;

下面的几个static final变量表示runState可能的几个取值。
当创建线程池后,初始时,线程池处于RUNNING状态;
如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

2)、任务的执行

在任务提交到线程池并且执行完毕之前,需要先了解ThreadPoolExecutor中的几个重要成员变量:

private final BlockingQueue<Runnable> workQueue;
//任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock= new ReentrantLock();
//线程池的主要状态锁,对线程池状态(比如线程池大小、runState等)的改变都要使用这个锁
private final HashSet<Worker> workers= new HashSet<Worker>();
//用来存放工作集
private volatile long  keepAliveTime;
//线程存活时间   
private volatile boolean allowCoreThreadTimeOut;
//是否允许为核心线程设置存活时间
private volatile int   corePoolSize;
//核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int   maximumPoolSize;
//线程池最大能容忍的线程数
private volatile int   poolSize;
//线程池中当前的线程数
private volatile RejectedExecutionHandler handler;
//任务拒绝策略
private volatile ThreadFactory threadFactory;
//线程工厂,用来创建线程
private int largestPoolSize;
//用来记录线程池中曾经出现过的最大线程数
private long completedTaskCount;
//用来记录已经执行完毕的任务个数

其中,corePoolSize指的是核心池大小,如果线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列,如果缓存队列也放不下了,则会考虑扩展线程池中的线程数,一直扩展到maximumPoolSize; maximumPoolSize是线程池最大能容忍多少线程数;

上面两个参数遵循下面的规则:
如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理; 如果线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

通过 setCorePoolSize()和setMaximumPoolSize()可以动态设置线程池容量的大小。

3)、线程池中的线程初始化

默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:
prestartCoreThread():初始化一个核心线程;
prestartAllCoreThreads():初始化所有核心线程

4)、任务缓存队列及排队策略

在前面多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。
workQueue的类型为BlockingQueue,通常可以取下面三种类型:
1)ArrayBlockingQueue,基于数组的先进先出队列,此队列创建时必须指定大小;
2)LinkedBlockingQueue,基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
3)synchronousQueue,这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

5)、任务拒绝策略

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:

  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

6)、线程池的关闭

ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:

  • shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
  • shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

7)、工厂方法

Executors类为创建ExecutorServ]Cice提供了便捷的工厂方法。
推荐使用Executors 工厂方法:

  • Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收);
  • Executors.newFixedThreadPool(int)(固定大小线程池);
  • Executors.newSingleThreadExecutor()(单个后台线程);

这些工厂方法为大多数场景预定义了参数设置。如果不用这些配置,需要自己设置比较麻烦;

8)、特殊线程类

在Java线程相关的类中,有一些特殊的类并不经常用,但在特定场景下,会非常重要,比如:
CycliBarrier和CountDownLatch
这些类都有其特定的应用场景,在Java编程思想中也有相关介绍,这里就不再赘述

9)、特殊线程分析工具

主要的线程分析工具有Jstatck,这个在网上已经有很多教程

二、线程的创建示例

简单的创建一个线程有三种方式,即继承Thread、实现Runnable和Callable接口。其中Thread和Runnable可以直接集成或者实现其接口,然后通过start方法启动即可。但Callable接口不但需要实现还需要在线程池中使用,且需要返回值时只能通过线程池的submit方法进行调用,使用Future进行接收。
Thread和Runnable的实现run接口,但是没有返回值,也不能捕获异常处理。Callable实现其call接口,可以返回值。

1、Thread和Runnable的使用

以下是线程的四种实现方式,其实就是两种的不同变种,即继承Thread和实现Runnable接口,线程的实现还有一种就是实现Callable接口

		//第一种
		MyThread myThread = new MyThread();//继承了Thread的类
		myThread.start();
		//第二种
		new Thread(){
			@Override
			public void run() {
				System.out.println("MyThread2 running");
			}
		}.start();
		
		//第三种
		Thread thread = new Thread(new MyRunnable());//实现了Runnable的接口
		thread.start();
		
		//第四种
		Runnable runnable = new Runnable(){
			@Override
			public void run() {
				System.out.println("MyRunnable2 running");
				
			}
			
		};
		Thread thread2= new Thread(runnable);
		thread2.start();

2、Callable的使用

Runnable是执行工作的独立任务,但是它不返回任何值。在Java SE5中引入的Callable是一种具有类型参数的泛型,它的类型参数表的是从方法call()中返回的值,并且必须使用ExecutorServices.submit()方法调用它,下面是一个简单示例。

public class CallableTest {  
    public static void main(String[] args) {  
        ExecutorService exec=Executors.newCachedThreadPool();  
        List<Future<String>> results=new ArrayList<Future<String>>();  
          
        for(int i=0;i<5;i++) {  
            results.add(exec.submit(new TaskWithResult(i)));  
        }  
          
        for(Future<String> fs :results) {  
            try {  
                System.out.println(fs.get());  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } catch (ExecutionException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  
  
class TaskWithResult implements Callable<String> {  
    private int id;  
    public TaskWithResult(int id) {  
        this.id=id;  
    }  
      
    @Override  
    public String call() throws Exception {  
        return "result of TaskWithResult "+id;  
    }  
}

1)、线程池数量设置

在创建线程池时需要能明确的知道创建的线程池的corePoolSize、maximumPoolSize以及缓冲队列的长度。一般而言,线程池的数量是cpu的2倍(针对是计算任务)或者5-10倍(针对时IO任务,线程数量cpus/1-0.8或者cpus/1-0.9),缓冲队列数量设置为maximumPoolSize的5倍。

  • 计算型的任务,尽可能的减少在运算过程中进行cpu的切换,所以有的推荐线程数是cpu数量+1;
    IO型的任务,由于IO任务在运行过程中总是独占的,所以设置多一些线程处理其他的业务,一般推荐是cpu数量的5-10倍,也有推荐是cpu数量的2倍的。

  • 缓冲队列长度的设置,按照一般的理解应该设置成maximumPoolSize的5倍,因为单个线程在不切换cpu的情况下是单个cpu运算一个线程,corePoolSize设置成和cpu一样的数量,maximumPoolSize设置成cpu的2倍,避免cpu处于空闲状态则直接加入一旦cpu空闲理解执行的线程数量,同时由于cpu的速度较快以及有请求执行的线程加入缓冲队列中减少cpu等待时间,故将缓冲队列设置大一些。但缓冲队列也是较为消耗资源的。

2)、缓冲队列三种类型

  • SynchronousQueue 不排队,直接提交
    将任务直接交给线程处理而不保持它们,可使用SynchronousQueue
    如果不存在可用于立即运行任务的线程(即线程池中的线程都在工作),则试图把任务加入缓冲队列将会失败,因此会构造一个新的线程来处理新添加的任务,并将其加入到线程池中(corePoolSize–>maximumPoolSize扩容)
    Executors.newCachedThreadPool()采用的便是这种策略
  • 无界队列,一般推荐使用此种方式
    可以使用LinkedBlockingQueue(基于链表的有界队列,FIFO),理论上是该队列可以对无限多的任务排队
    将导致在所有corePoolSize线程都工作的情况下将新任务加入到队列中。这样,创建的线程就不会超过corePoolSize,也因此,maximumPoolSize的值也就无效了
  • 有界队列
    可以使用ArrayBlockingQueue(基于数组结构的有界队列,FIFO),并指定队列的最大长度
    使用有界队列可以防止资源耗尽,但也会造成超过队列大小和maximumPoolSize后,提交的任务被拒绝的问题,比较难调整和控制。

注意:
如果使用LinkedBlockingQueue缓冲队列,且设置了大小了,同样超出了长度会拒绝接收新的任务,和使用ArrayBlockingQueue就没有区别了,但ArrayBlockingQueue会比的读取更快,同样频繁的移除LinkedBlockingQueue效率更高,至于两者如何选择视具体情况而定。即如果是短时的任务,使用LinkedBlockingQueue更适合,如果是长时任务ArrayBlockingQueue更合适,当然如果存在长时任务且进行任务切换的ArrayBlockingQueue也是更合适的。一般情况下,线程任务都是短时且频繁的移除,所以推荐使用LinkedBlockingQueue。

以上关于数字设置需要经过实际的测试而定。

public class CallableTest {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		Callable<Integer> task = () -> {
			try {
				TimeUnit.SECONDS.sleep(1);
				return 123;
			} catch (InterruptedException e) {
				throw new IllegalStateException("task interrupted", e);
			}
		};
		ExecutorService executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS,
				new LinkedBlockingQueue<Runnable>(1024),
				new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());

		// ExecutorService executor = Executors.newFixedThreadPool(1);
		Future<Integer> future = executor.submit(task);

		System.out.println("future done? " + future.isDone());

		Integer result = future.get();

		System.out.println("future done? " + future.isDone());
		System.out.print("result: " + result);
		
	}

}

以上,完成了线程与线程池的概念介绍以及线程的创建,是该系列的起步篇。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一瓢一瓢的饮 alanchanchn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值