Java基础
- 一. Java运行机制
- 1.1 Java .class 字节码文件
- 1.2 Java JVM 底层原理
- 1.3 Jar包
- 二. Java 语言基础
- 2.1 Java 程序基本规则
- 2.2 Java 语法基础
- 2.3 Java 对象
- 2.3 Java 集合
- 2.4 Java 注解 Annotation
- 2.5 Java 线程
- 2.6 Java 线程池
- 2.6 类的加载与反射
- 参考文档
一. Java运行机制
“字节码”,针对不同平台编写虚拟机(JVM),不同平台的虚拟机负责加载字节码并执行,这样就实现了“一次编写,到处运行”。
1.1 Java .class 字节码文件
1.2 Java JVM 底层原理
1.3 Jar包
Jar是Java的档案文件,是一种压缩文件,Jar文件与ZIP压缩文件兼容,区别是Jar文件中默认包含了一个的清单文件。当一个应用开发完成后,会将包含的文件打包成一个Jar文件提供给其他人使用,只需要将Jar包路径添加到环境变量中,JVM会把这个Jar文件当成一个路径来处理,并在内存中解压Jar包。
Q. 使用Jar文件的好处?
① 可以对Jar文件进行数字签名,保证Jar文件的安全。
② 加快下载速度,当多个文件进行传输时,需要每个文件单独建立一个HTTP连接。如果压缩成Jar文件,只需要一次连接即可。
③ 使文件变小,减少空间的占用
④ 可移植性,Jar包时Java平台内部处理的标准,能够在各种平台上直接使用。
二. Java 语言基础
2.1 Java 程序基本规则
与C++相比,Java是完全的面向对象的语言,因此Java与C++在程序规则上有所区别:
① Java程序必须以的形式存在, 是java的最小程序单位。在Java程序中,不允许可执行语句、方法独立存在。
② 如果一个Java程序文件中定义了一个类,则该程序文件的文件名必须和该类名相同。因此,一个Java程序文件最多只能有一个类。
③ Java中编译器通过Main()方法作为程序的入口,且Main()的修饰符必须为:
2.2 Java 语法基础
▍ 2.2.1 Java关键字
Java中一共有48种关键字,如下表所示。关键字可以分为程序逻辑控制关键字,系统控制(线程同步)关键字,数据类型关键字,类/对象关键字,包/方法管理关键字。红色的为常用的关键字。由于Java与C++的关键字有很多相同之处,未介绍的关键字可以查看「C++基础」
▍ 2.2.2 Package 与 import 机制
Java引入了包机制,用于解决类名冲突与类文件管理等问题(类似于C++中的)。Java允许将一组功能相关的类放在同一个下,构成类库单元。当Java程序文件中使用了语句时,则该程序文件中定义的所有类都在这个下。同时,在父包下可以创建子包,虽然父包和子包存在某种联系,但在用法上没有任何关系,如在父包类中需要使用子包中的类时,必须使用子包的全名,而不能省略父包部分。
同时Java引入了机制,可以向某个Java文件中指定包层次下的某个类或全部类。
2.3 Java 对象
▍ 2.3.1 Java Class类 / 对象
在Java中,一切皆对象。Java分为两种对象:Java实例对象 和 Java Class对象。每个类的运行时的类型信息就是用Class对象表示的。它包含了与类有关的信息。每一个类有且只有一个Class对象,Class对象对应类,是对类的抽象和集合。Class类的特点如下:
Q. Class类的作用 ?
在C++中有个重要的概念:运行时类型识别(),其作用是在运行时识别一个对象的类型和类的信息。Java中同样存在,Java中的实现有两种方式:
● 在编译期已确定其所有对象的类型,这种方式需要在写程序的时候将对象通过new创建出来。
● 通过反射机制,在运行时发现和使用类型的信息。在Java中用来表示运行时类型信息的对应类就是Class类。
▍ 2.3.2 Java 类加载
1. 类的生命周期
类的生命周期是从虚拟机将文件加载到内存开始,直到卸载出内存为止。类的生命周期分为7个阶段,如下图所示:
● 加载阶段(类加载器): JVM通过类的全限定名获取定义此类文件中的二进制字节流,并将字节流中的静态存储结构转化为方法区的运行时数据结构,最终在内存中生成一个代表这个类的对象,作为方法区该类的各种数据的访问入口。
● 验证阶段: 确保文件的字节流包含的信息符合虚拟机的要求,不会危害虚拟机自身的安全。验证阶段分为4个过程:
① 文件格式验证:验证字节流是否符合文件格式的规范,并且能被当前版本的虚拟机处理,通过该阶段后,字节流会进入内存的方法区中进行储存。
② 元数据验证(.class语言分析):对字节码描述的信息进行语言分析,对类的元数据信息进行语义校验,确保其描述的信息符合Java语言规范要求。
③ 字节码验证(.class语义分析):通过数据流和控制流分析,确定程序语义是符合逻辑的。这个阶段对类的方法进行校验分析,保证类的方法在运行时不会做出危害虚拟机安全的事件。
④ 符号引用验证:对类自身以外的信息(常量池中的符号引用)的信息进行校检,确保后续的解析动作能够正常执行。
● 准备阶段(分配空间): 为类变量(仅为修饰的变量)分配内存空间并设置数据类型零值。实例对象会在对象实例化时分配在Java堆中。
● 解析阶段: 将常量池的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、和调用限定符7类符号引用。
Q. 什么是符号引用、直接引用 ?
⊕ 符号引用是以一组符号来描述所引用的目标,符号可以是任何字面量,只要使用时无歧义定位到目标即可。符号引用与虚拟机的内存布局无关。在编译的时候每个Java类都会被编译成一个文件,但在编译的时候虚拟机并不知道所引用类的地址,就用符号引用来代替,而在解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
⊕ 直接引用是可以直接定位到目标的指针、相对偏移量或是一个能间接定位目标的句柄。直接引用是与虚拟机的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用不会相同。
● 类初始化: 执行类中定义的Java代码 ,分为类的主动引用、被动引用:
⊕ 主动引用:是指在初始化阶段,一定会对类进行初始化。有4种场景会发生类的主动引用:
「1」使用new关键字实例化对象的时候;读取或设置一个类的静态字段【、、操作静态字段的指令】(被修饰、已经在编译期把结果放入常量池的静态字段除外),以及调用一个类的静态方法的时候。
「2」使用包的方法对类进行反射调用的时候。
「3」当初始化一个类的时候,如果发现其父类还没进行初始化,则必须对父类进行初始化。
「4」当虚拟机启动时,用户指定的要执行的主类(包含main方法的类)
⊕ 被动引用:除了主动引用,其余引用类的方式都不会触发初始化,因此称为被动引用。
「1」对于静态字段,只有直接定义这个字段的类才会被初始化,通过其子类来引用父类中定义的静态字段,只会触发其父类的初始化而不会触发子类的初始化。
通过数组定义来引用类,不会触发此类的初始化。
常量在编译阶段会存入调用类的常量池中,并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
Q. static,final,static final 对修饰字段赋值时的区别 ?
● 在准备阶段时被初始化为0或null,在初始化阶段时被赋予代码中设定的值,如果没有设定值,则仍为默认值。
● 在通过Javac编译时生成常量值属性,在准备阶段时根据设定值为该字段进行赋值。该字段没有默认值,必须显式的赋值。
● 在运行时被初始化,一旦初始化便不可更改。
2. 类加载器
在类生命周期的加载阶段,是由类加载器来完成的。类加载器根据一个类的全限定名读取类的二进制字节流到JVM中,然后生成对应的实例。JVM中包含四种类型的类加载器:
● :类加载器是由本地代码实现,它负责将下面的核心类库或选项指定的jar包加载到内存中。类加载器由JVM本地实现,因此开发者无法直接获取到类加载器的引用。
●:类加载器由 实现,负责将或者由系统变量指定位置中的类库加载到内存中,开发者可以直接使用标准扩展类加载器。
●:Syetem类加载器由 实现,负责将当前类所在路径及其引用的第三方类库的路径下的类库加载到内存中。
●:类加载器是根据自身需要自定义的。
3. 类的加载机制 - 双亲委派机制
Q1. 双亲委派机制的作用及优点 ?
① 为了保证Java系统核心类的安全,对于Java 核心库的类的加载工作由引导类加载器来统一完成,保证Java 应用所使用的都是同一个版本的 Java 核心库的类,避免用户对核心类重写,使Java核心类出现混乱。如用户重写了类,在类加载时,双亲委派机制会从类加载器开始加载类,从而不会加载用户重写的核心类。避免Java核心API中的类被随意替换。
② 双亲委派机制避免类的重复加载,当父类加载器已经加载了该类时,子类加载器就不用再加载。
▍ 2.3.3 Java 类引用
1. 类的引用
所谓引用,是指创建一个”对象标识符“,通过操纵”标识符“使其指向一个对象。这个”标识符“称为引用变量(通俗来说,引用 = 引用变量 = 常说的变量)。一个引用变量可以指向多个对象,但仅保留最后一次引用。一个对象也可以被多个引用变量所指。
从高到低依次为:强引用,软引用,弱引用,虚引用。
● 强引用:强引用是指创建一个对象并把这个对象赋给一个引用变量,强引用是使用最普遍的引用。如果一个对象具有强引用,当内存空间不足时,JVM宁愿抛出错误,使程序异常终止,也不会回收具有强引用的对象来解决内存不足的问题。因此,当强引用对象不使用时,需要将其指向,使其可以被GC回收。
● 软引用 (SoftReference):如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,JVM会将软引用中的对象引用置为,然后通知垃圾回收器GC进行内存回收。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存,如图片缓存,浏览器后端页面缓存。
● 弱引用 (WeakReference):只具有弱引用的对象拥有更短暂的生命周期,当垃圾回收器GC扫描到只具有弱引用的对象,不管当前内存空间是否足够,都会回收内存。
● 虚引用 (PhantomReference):虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。使用虚引用的目的就是为了得知对象被GC的时机,可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。
Q1. 虚引用 与 软引用、弱引用的区别 ?
虚引用必须和引用队列 () 联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
2. Java 类加载过程中的引用关系
在类的全生命周期中,从类的加载器,生成类的Class对象,到类的实例对象的使用有着密不可分的引用关系:
● 类加载器与Class对象:类的Class和加载它的加载器之间是双向关联关系。即一个Class对象总是会引用他的类加载器,调用Class对象的方法就可以获得它的类加载器。
● 类,类的Class对象,类的实例对象:一个类的实例对象总是引用该类的Class对象,在Object类中定义该类方法,会返回对象所属类的Class对象的引用。
▍ 2.3.4 Java 类反射 - Class对象的主要应用
Java程序在运行之前需要先编译。程序中的对象初始化,对象的调用在编译时期就已经确定并加载到JVM中。当程序在运行时需要 “动态加载” 某些类时,由于这些类并没有加载到JVM当中,无法直接获取。但是通过Java的反射机制,可以在运行时动态地创建对象并调用其属性和方法,不需要提前在程序中(编译时期)进行对象的初始化和调用。Java反射机制的本质是JVM得到Class对象之后,再通过Class对象进行反编译,从而获取对象的各种信息。
Q1. 反射有什么用途 ?
① 通过Java反射机制,访问Java未初始化对象的属性和方法。
② 反射最重要的用途就是开发各种通用框架。如: 都是通过文件配置化的,为了保证框架的通用性,大多数框架需要根据配置文件加载不同的类或者对象,调用不同的方法,这时框架就必须使用反射在程序运行时动态加载需要使用的对象。
③ 当使用IDE编程时,IDE会自动列举某一对象所包含的方法和属性,这是通过反射实现的。
▍ 2.3.5 Java继承(extends)与接口(implement/interface)
1. 继承 extends
继承是Java面向对象的特征之一,Java的继承通过关键字来实现,与C++相比,Java摒弃了多继承,只保留了单继承,即每一个类最多只有一个直接父类。在子类创建某个类对象时,系统会隐式的创建该类的父类对象,且可以通过来该子类的父类对象。如果一个Java类没有显式指定直接父类,则默认其直接父类为。
由于继承会破坏父类的封装性,使子类与父类之间的耦合,因此子类与父类之间应当遵循如下规则:
① 将父类的所有属性设置为,不让子类直接访问父类属性。
② 不要让子类随意修改、访问父类的方法。父类中的工具方法要设置为; 父类中需要被外部调用但不希望子类重写该方法的要设置为; 如果父类的方法能够被子类重写,但不希望被其他类访问,要设置为。
③ 不要在父类构造器中调用被子类重写的方法。
2. 抽象类与接口
与C++相比,Java的抽象与接口定义更加明确。在Java中,通过定义抽象类,与C++相同,Java中的抽象类也无法进行实例化,仅能通过子类的继承对抽象方法进行实现。除此之外,Java还引入了一种更加纯粹的抽象类 - 接口(),在中,所有的方法都是抽象方法,同时引入了来实现接口。
2.3 Java 集合
2.4 Java 注解 Annotation
注解是Java提供的一种途径和方法,可以使源程序中的元素关联到代码中的。 注解是附加在代码中的一些源信息,用于在一些工具在编译、运行时进行解释和使用,起到说明,配置的功能。注解为一种修饰符,应用于类、方法、参数、变量的声明语句中。注解不会也不能影响代码本身的业务逻辑,仅仅只能起到辅助性的作用。使用注解时要在其前面增加符号。
Q1. 什么是元数据 (Metadata)?
要真正了解注解的工作方式和原理,就需要先了解什么是元数据。
元数据是一种很抽象的定义, 元数据是一系列关于数据的数据,是一系列用来描述数据的数据。元数据可以为数据说明其元素或属性(如:名称、大小、数据类型等),或其结构 (如:长度、字段等),或其相关数据(如:位于何处、数据拥有者等)
Q2. 注解的本质 ?
(注解)是一个接口,程序可以通过反射来获取指定程序元素的
对象,然后通过
对象来获取注解里面的元数据。注解不支持继承,因此不能使用关键字
来继承某个
,但注解在编译后,编译器会自动继承
接口。因此,注解的本质是一个继承了
接口的接口。注解作为一个特殊的接口,其实现类是在代码运行时生成的动态代理类,而之后底层代码通过
反射
的方式获取到注解。
下图以
为例,说明注解的本质:
▍ 2.4.1 基本Annotation
在Java中存在三个基本的Annotation:
● @Override - 限定重写父类方法:强制一个子类覆盖父类的方法。
● @Deprecated - 标示已过时:表示某个程序元素(类,方法,成员变量)已过时,当使用过时方法时,编译器会发出警告。
● @SuppressWarnings - 抑制编译器警告:通常情况下,程序中使用没有泛型限制的集合会引起编译器警告,为了避免警告,可以通过抑制编译器警告。
▍ 2.4.2 元注解
元注解是 Java 提供的用于修饰注解的注解,用于自定义注解。提供了四种元注解,分别是:
● @Target: 注解所作用的目标,指明这个注解最终是用来修饰方法,还是修饰类,还是修饰属性;
● @Retention:用于指明当前注解的生命周期,生命周期包含三个阶段:在编译阶段丢弃、在类加载的时候丢弃、始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。
● @Documented:注解修饰的注解,当执行 JavaDoc 文档打包时会被保存进 doc 文档,反之将在打包时丢弃。
● @Inherited:注解修饰的注解是具有可继承性的,也就说我们的注解修饰了一个类,而该类的子类将自动继承父类的该注解。
▍ 2.4.3 自定义Annotation
定义新的Annotation类型需要使用 关键字,自定义Annotation的规则如下:
② 注解的参数成员(变量、方法)只能用 或默认()这两个访问权修饰。
④ 要获取类方法和字段的注解信息,必须通过Java的反射技术来获取 对象。
2.5 Java 线程
线程,一个执行实体,正在执行的程序,担当分配系统资源(CPU、内存)的实体。线程的底层实现原理在操作系统中具体介绍。这里介绍Java线程的实现方式以及Java线程如何使用。
▍ 2.5.1 Java 线程的实现
Java的线程是通过类来实现的,在Java中有三种方法来实现线程:
① 可以通过创建继承类的实例来创建新的线程。每个线程都是通过对应的方法来描述该线程需要执行的操作。通过调用类的方法来启动一个线程。
② Java中只支持单继承,如果一个类继承了某个父类,就无法再继承类。因此类提供了一个接口,可以通过重写接口中的方法,也可以实现线程的启动,因此一个线程操作的方法体,是用户定义的需要完成的具体任务,并通过 Thread.start() 去启动线程。
③ 对于某些场景,需要在线程执行完成后将任务执行的结果返回,或当线程在执行时抛出异常。因此可以通过实现接口中的 方法来完成结果的返回。接口通常与一起使用,通过来异步的执行线程,并保存线程结果。注:通过创建线程时,只能通过,不能通过。
Notice:当一个线程运行结束后,无法通过方法再次启动。即每个线程只能被启动一次。
Q1. run() 和 start() 的区别 ?
● 方法是一个普通的成员方法,当线程调用了方法后,该线程会去调用这个方法,运行该线程需要执行的操作。因此,如果直接调用run() 方法,只会在原有线程上运行,不会创建一个新的线程。
● 方法用来启动线程。当线程创建成功时,线程处于状态,调用后,线程会变为状态,在等待CPU调度后,线程才可以运行,进入状态。
▍ 2.5.2 Java 线程状态切换
Q1. sleep()、wait() 和 notify() 的区别 ?
Java中的和函数都可以挂起当前线程,使线程休眠,但实现方式和用法不同:是 Thread类的方法静态方法,需要通过Thread类调用 。而 和 是 Object类中的实例方法,因为Java所有类都继承于 Object类,所有类中都可以使用。
● 、必须用在 synchronized 代码块中调用。当调用 方法后,当前获得 synchronized 同步块对象锁的线程进入”等待阻塞“状态,同时释放当前线程的对象锁。此时其他线程可以获得该 synchronized 同步块的对象锁。被阻塞的线程需要通过 方法来唤醒。
● 当在 synchronized 同步块中使用 ,该线程会被挂起,但不会释放对象锁,所以如果有其他线程等待执行该 synchronized 代码块,一直会被阻塞,等待该线程被 唤醒释放对象锁。
▍ 2.5.3 Java 线程安全与数据安全
① 线程安全: 当多线程在访问共享数据 (Runnable run()中的局部变量) 时,如果一个线程正在执行而没有执行完,它的执行权被其他线程抢走了就有可能出现安全问题。为了解决这个问题,Java提供了同步器来保证线程运行时顺序和同步。
② 数据安全: 由于局部变量var是所有线程共同拥有的,因此Thread-1对var的修改会影响Thread-2对var的使用。为了解决这个问题,Java提供了 ThreadLocal 保证线程数据的安全性。
1. Java 线程安全2. Java 数据安全
● ThreadLocal - 任务实体中的变量:
为了保证线程的数据安全,在Java中可以使用 ThreadLocal 维护变量,ThreadLocal为每个使用该变量的线程提供独立的局部变量副本,每一个线程都可以独立地改变自己的副本,通过 set() 和 get() 来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。
Q1.ThreadLocal 是如何实现线程 (数据) 隔离的 ?
Thread类中有两个变量和。在每个Thread线程对象中,都维护了一个ThreadLocalMap。即一个Thread线程对象,最多只有一个ThreadLocalMap,而ThreadLocalMap底层是一个Entry数组,但是一个Thread可以有多个ThreadLocal,一个ThreadLocal对应一个变量数据,变量数据将ThreadLocal作为Key,Object作为value,封装成Entry存到ThreadLocalMap中Entry[]数组中。Thread与ThreadLocal之间的关系如下图所示:
Q2.ThreadLocalMap 是如何解决Hash冲突的 ?
每个ThreadLocal都有一个对应的
,通过
可以算出ThreadLocal变量对应的Entry[]数组的下标(即Key)。当Key发生Hash冲突时,ThreadLocalMap采用
线性探测方法
,循环查找下一位(索引)是否冲突,直到找到不存在冲突的索引(Entry[]数组下标)。
Q3.ThreadLocalMap的Entry中,对 ThreadLocal 的引用为什么要设置成弱引用 ?
当代码中将ThreadLocal的强引用置为null后,这时候Entry中的ThreadLocal应该被回收了,但是如果Entry的key被设置成强引用则该ThreadLocal就不能被回收,从而导致内存泄露。
Q4. ThreadLocal的内存泄露问题?
● 虽然 Entry对象中的ThreadLocal引用为弱引用,但这个弱引用只是针对key的。当把 Threadlocal 实例置为null以后,没有任何强引用指向 Threadlocal 实例,此时Threadlocal将会被gc回收。虽然ThreadLocal被回收了,但是Entry对象中的value却不能回收,因为存在一条从
连接过来的强引用。只有当前Thread结束以后,
就不会存在栈中,连接value的强引用断开。此时Current Thread, ThreadLocalMap, Entry-value将全部被GC回收。
● 根据上述ThreadLocal内存回收的过程可以看出,只要当前的线程对象被GC回收,ThreadLocal就不会出现内存泄露的情况。但如果是在使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。因此,当使用完ThreadLocal之后,调用
的
方法把当前
从当前线程的
中移除。
● InheritableThreadLocal (ITL):
虽然ThreadLocal为每个使用该变量的线程提供独立的局部变量副本,使当前线程变量不会和其他线程的局部变量进行冲突。但是在父线程中创建的本地变量是无法传递给子线程的,因此Java提供了 来解决线程在继承过程中变量的传递问题。
Q1. InheritableThreadLocal 是如何实现线程本地变量继承的 ?
是ThreadLocal的子类。在线程在创建并初始化时,会检查其父线程是否存在inheritableThreadLocals,如果存在则会在父线程的inheritableThreadLocals的基础上创建子线程。
● TransmittableThreadLocal (TTL):
TL 解决了不同线程之间本地变量冲突的问题,ITL解决了在线程继承中,本地变量的继承问题。但在上下文传递 (所谓上下文,就是当前节点的环境,如一个方法的入参,结果,以及执行过程中产生的临时数据) 的过程中
2.6 Java 线程池
▍ 2.6.1 Java 线程池描述
Q1. 为什么要用线程池 及 线程池的作用 ?
● 当计算机中的线程过多时,会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。通过线程池来维护多个线程,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
● 线程池的作用有以下几个方面:
① 降低系统资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的性能损失。
② 提高系统的响应时间:当任务到来时,无需创建线程就可以并发的执行任务。
③ 加强对线程的管理:通过线程池,可以对线程进行统一的分配、调优和监控。若线程是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度上下文切换。
▍ 2.6.2 Java 线程池原理
Java线程池的核心实现类是,线程池的状态维护,任务管理都在类中实现,其继承关系如下图所示:
Q1. Executor接口、ExecutorService接口 和 Exexutors的区别 ?
● Executor:Executor 是一个抽象层面的核心接口,主要是将任务 Task本身和执行任务分离,解耦合。
● ExecutorService:ExecutorService 接口继承了 Executor 接口,对 Executor 接口进行了扩展,并返回 ,为线程池终止,关闭线程池等提供操作方法。提供了异步执行,因此只要提交需要执行的任务,无需等待结果,在需要时通过检查 Future 是否执行完成。如果执行完成,就可以通过 获得执行结果。注意: 是一个阻塞式的方法,如果调用时任务还没有完成,会阻塞等待,直到任务执行结束。因此在获取结果前,先通过检查任务是否执行完成。
● Executors 类提供工厂方法用来创建不同类型的线程池。如: 创建一个只有一个线程的线程池,来创建固定线程数的线程池,可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程。
1. Java 线程池状态 - 生命周期管理
Java的线程池共有5种运行状态,其运行状态的流转如图所示:
线程池运行的状态,由内部来维护。线程池内部使用一个变量维护两个值:和。其存储结构如下图所示:
2. Java 线程池 - 任务执行过程
线程池的任务是处理问题的基本”单元“,就像需要搬运的货物,只有合理使用对任务进行管理,才能保证线程池的高效运行。线程池任务的执行过程主要分为4个部分:任务分配(调度),任务缓存,任务获取,任务拒绝。 ● 任务分配: 任务分配是线程池的”入口“,当用户提交了一个任务,线程池会根据当前状态对任务进行分配,缓存或者拒绝。线程池任务的分配(调度)都是由 方法完成的。任务调度的执行过程如下:
① 检测线程池运行状态,如果不是,则直接拒绝任务;
② 判断,则创建并启动一个线程来执行新提交的任务;
③ 如果,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中;
④ 如果,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务;
⑤ 如果,并且线程池内的阻塞队列已满, 则抛出异常,拒绝该任务。
● 任务缓存: 在任务分配过程中,当,且线程池的阻塞队列未满时,则将任务添加到该阻塞队列中。阻塞队列在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。利用阻塞队列的特性实现生产者消费者模型,保证了任务和工作线程两者解耦。
Q1. 线程池为什么要使用阻塞队列作为缓存,而不使用普通队列 ?
● 线程池创建线程需要获取全局锁,影响并发效率,如果采用普通队列,可能会导致没有及时入队,而使得任务丢失。而阻塞队列可以很好的缓冲。
● 阻塞队列主要是用于生产者-消费者模型。如果使用非阻塞队列,它不会对当前线程产生阻塞,就必须额外地实现同步策略以及线程间唤醒策略。
Q2. 在线程池自定义队列中,为什么不能使用无界队列 ?
如果采用无界队列,当 时,会不断的将任务加入到队列中。同时由于队列无界,就无法使队列满队,从而无法创建新的Worker,会直接导致最大线程数的配置失效。 实际使用的线程数的最大值始终是 ,即便设置了 也没有生效。 要用上 ,允许在核心线程满负荷下,继续创建新线程来工作 ,就需要选用有界任务队列。
● 任务获取: 线程获取任务有两种可能:
① 当线程是新创建的工作线程时,则获取的任务为 firstTask。
② 当线程执行完 firstTask 任务后,再次获取任务时,需要从任务缓存中获取任务并执行,从缓存中获取任务是由方法完成的,其方法的流程如下图所示:
● 任务拒绝: 任务拒绝是为了保护线程池,当线程池的任务缓存队列已满,并且线程池中的工作线程数目 时,就需要采取任务拒绝策略,拒绝掉该任务,保护线程池。Java中设置了4种拒绝策略,如下图所示,同时用户可以通过实现 接口去定制拒绝策略。
3. Java 线程池 - Worker线程
如果将线程池任务比作货物,则线程就是运送货物的车辆。线程是完成任务的“工具”和基础。
● Worker线程管理:
线程池对Worker线程的管理使用一张表去持有线程的引用,可以通过添加引用、移除引用来控制线程的生命周期。
对单个Worker线程进行具体分析: Worker线程实现了Runnable接口,并包含一个线程Thread、一个初始化任务 firstTask。Thread是在调用构造方法时通过来创建的线程;用它来保存传入的第一个任务。如果,那么该线程会在启动时立即执行firstTask任务,对应的Worker线程为核心线程;如果,那么该线程会通过方法获取并执行任务列表(workQueue)中的任务,对应的Worker线程为非核心线程。
★ 线程池在执行方法或方法时会调用方法来中断空闲的线程,方法会使用方法来判断线程池中的线程是否是空闲状态,如果线程是空闲状态则可以安全回收。 ● Worker线程申请 (新增):
Worker线程的新增是通过方法。方法有两个参数,firstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空;core参数为true时表示在新增线程时会判断,false表示新增线程前需要判断。
● Worker线程执行任务:
在Worker类中的方法调用了方法来执行任务。在中 Worker线程会在while循环中不断的通过getTask()方法从阻塞队列中获取任务。如果 时,则会跳出循环,执行 进行线程回收和销毁。如果此时线程池处于正在停止状态,则当前Worker线程须处于中断状态,否则要保证当前Worker线程不是中断状态。
● Worker线程回收 与 线程池终止状态:
由于引起线程销毁的可能性有很多,线程池还要判断是什么引发了这次销毁,是否要改变线程池的现阶段状态,是否要根据新状态,重新分配线程。具体分为不调用 、调用 和 调用 三种情况:
① 不调用:
该情况下,线程池处于状态,此时线程池的主要工作是维护一定数量的核心线程引用,防止这部分核心线程被JVM回收,同时将大于的进行回收。
方法会维护一个while循环,不断获取任务,若不为空,执行任务;若取不到任务,执行; 把工作线程移除掉。如果如果移除工作线程后 ,则创建一个新的工作线程来代替移除的工作线程,以维持。
② 调用:
调用关闭线程池后,线程池状态被设置为。此时无论是核心线程还是非核心线程,所有工作线程都会被销毁。调用shutdown()之后,会向所有的空闲工作线程发送中断信号。对于正在处理任务的Worker,当阻塞队列中的任务执行完成后,原本的Worker会阻塞。由于此时线程池状态为,且,此时每当唤醒一个阻塞的工作线程,中断并回收该线程,同时会遍历剩余的所有工作线程,并随机中断一个空闲的工作线程去传播中断信号,直到所有的线程被中断回收。
③ 调用:
方法将会把线程池状态设置为STOP,然后中断所有线程(包括工作线程),最后取出工作队列中所有未完成的任务返回给调用者。
▍ 2.6.2 Java 线程池应用与配置
2.6 类的加载与反射
参考文档
Java类加载:
1。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.bianchenghao6.com/h6javajc/25228.html