JDK、JRE、JVM 三者之间的关系?
JDK(Java Development Kit):是Java开发工具包,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。它能够创建和编译程序。
JRE(Java Runtime Environment):是Java的运行环境,它是运行已编译Java程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java核心类库,java命令和其他的一些基础构件。
JVM(Java Virtual Machine):是运行Java字节码的虚拟机,是整个Java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。所有的Java程序会首先被编译为 .class 的类文件,这种类文件可以在虚拟机上执行。
什么是字节码?采用字节码的好处是什么?
JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。
为什么Java代码可以实现一次编写、到处运行?
JVM(Java虚拟机)是Java跨平台的关键。在程序运行前,Java源代码(.java)需要经过编译器编译成字节码(.class)。在程序运行时,JVM负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。同一份Java源代码在不同的平台上运行,它不需要做任何的改变,并且只需要编译一次。而编译好的字节码,是通过JVM这个中间的“桥梁”实现跨平台的,JVM是与平台相关的软件,它能将统一的字节码翻译成该平台的机器码。
注意:(1)编译的结果是生成字节码、不是机器码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行;(2)跨平台的是Java程序、而不是JVM,JVM是用C/C++开发的软件,不同平台下需要安装不同版本的JVM。
为什么说 Java 语言“编译与解释并存”?
高级编程语言按照程序的执行方式分为两种:(1)编译型:编译型语言会通过编译器将源代码一次性翻译成可被该平台执行的机器码。常见的编译性语言有 C、C++、Go等。(2)解释型:解释型语言会通过解释器一句一句的将代码解释为机器代码后再执行。常见的解释性语言有 Python、JavaScript、PHP等等。
Java语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java程序要经过先编译,后解释两个步骤,由Java编写的程序需要先经过编译步骤,生成字节码(.class文件),到执行期时再将字节码直译来解释执行。
一个Java文件里可以有多个类吗(不含内部类)?
一个java文件里可以有多个类,但最多只能有一个被public修饰的类;如果这个java文件中包含public修饰的类,则这个类的名称必须和java文件名一致。
什么是面向对象?面向对象和面向过程的区别?
面向对象的基本思想是使用类、对象、继承、封装、消息等基本概念进行程序设计。它从现实世界中客观存在的事物出发来构造软件系统,强调直接以现实世界中的事物为中心来思考,并根据这些事物的本质特点,把它们抽象地表示为系统中的类,作为系统的基本构成单元,这使得软件系统的组件可以直接映像到客观世界,并保持客观世界中事物及其相互关系的本来面貌。
区别:(1)编程思路不同:面向过程以实现功能的函数开发为主,而面向对象要首先抽象出类、属性及其方法,然后通过实例化类、执行方法来完成功能。
(2)封装性:都具有封装性,但是面向过程封装的是功能,而面向对象封装的是数据和功能。(3)面向对象具有继承性和多态性,而面向过程没有继承性和多态性。
面向对象的三大特性?
(1)封装:将对象的成员变量和实现细节隐藏起来,不允许外部直接访问,然后通过一些公用方法来控制对这些成员变量进行安全的访问和操作。
(2)继承:继承是从已有的类中得到继承信息后创建新类的过程。提供继承信息的类被称为父类,得到继承信息的类被称为子类,当子类继承父类后将直接获得父类的属性和方法。
(3)多态:分为编译时多态(方法重载)和运行时多态(方法重写)。多态指的是子类对象可以直接赋给父类变量,但运行时依然表现出子类的行为特征,这意味着相同类型的变量在执行同一个方法时可能会表现出多种不同的行为特征。要实现多态需要做两件事:一是子类继承父类并重写父类中的方法,二是用父类型引用子类型对象。
封装的目的是什么,为什么要有封装?
封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改。对一个类或对象实现良好的封装,可以隐藏类的实现细节;让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问;可进行数据检查,从而有利于保证对象信息的完整性;便于修改,提高代码的可维护性。
Java的访问权限
Java语言为我们提供了三种访问修饰符,即private、protected、public,在使用这些修饰符修饰目标时,一共可以形成四种访问权限,即private、default、protected、public,注意在不加任何修饰符时为default访问权限。
(1)在修饰类时,该类只有两种访问权限,对应的访问权限的含义如下:
default:该类可以被同一包下其他的类访问;public:该类可以被任意包下,任意的类所访问。
(2)在修饰成员变量/成员方法时,该成员的四种访问权限的含义如下:
private:该成员可以被该类内部成员访问;default:该成员可以被该类内部成员访问,也可以被同一包下其他的类访问;protected:该成员可以被该类内部成员访问,也可以被同一包下其他的类访问,还可以被它的子类访问;public:该成员可以被任意包下,任意类的成员进行访问。
Java的数据类型
Java数据类型包括基本数据类型和引用数据类型两大类。
基本数据类型有8个,分别是整数类型(byte/short/int/long)、浮点类型(float/double)、字符类型(char)、布尔类型(boolean)。在这8个基本类型当中,除了布尔类型之外的其他7个类型,都可以看做是数字类型,它们相互之间可以进行类型转换。
引用类型就是对一个对象的引用,根据引用对象类型的不同,可以将引用类型分为3类,即数组、类、接口类型。引用类型本质上就是通过指针,指向堆中对象所持有的内存空间,只是Java语言不再沿用指针这个说法而已。
byte:1字节(8位),数据范围是 -2^7 ~ 2^7-1。
short:2字节(16位),数据范围是 -2^15 ~ 2^15-1。
int:4字节(32位),数据范围是 -2^31 ~ 2^31-1。
long:8字节(64位),数据范围是 -2^63 ~ 2^63-1。
float:4字节(32位),数据范围大约是 -3.4*10^38 ~ 3.4*10^38。
double:8字节(64位),数据范围大约是 -1.8*10^308 ~ 1.8*10^308。
char:2字节(16位),数据范围是 \u0000 ~ \uffff。
为啥要有包装类?基本类型和包装类型的区别?
Java语言是面向对象的语言,其设计理念是“一切皆对象”。但8种基本数据类型却出现了例外,它们不具备对象的特性。正是为了解决这个问题,Java为每个基本数据类型都定义了一个对应的引用类型,这就是包装类。
区别:(1)成员变量包装类型不赋值就是null,而基本类型有默认值且不是null。(2)包装类型可用于泛型,而基本类型不可以。(3)基本数据类型的局部变量存放在Java虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被static 修饰)存放在 Java 虚拟机的堆中。包装类型属于对象类型,存在于堆中。(4)相比于对象类型,基本数据类型占用的空间非常小。
装箱和拆箱的区别
自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;
自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;
通过自动装箱、自动拆箱功能,可以大大简化基本类型变量和包装类对象之间的转换过程。
如何对Integer和Double类型判断相等?
Integer、Double不能直接进行比较,因为它们是不同的数据类型。整数、浮点类型的包装类,都继承于Number类型,所以可以将Integer、Double先转为转换为相同的基本数据类型(如double),然后使用==进行比较。
Integer和int的区别?
(1)int 是 Java 的八种基本数据类型之一,而Integer是int的包装类;
(2)int型变量的默认值是0,Integer 变量的默认值是null;
(3)Integer变量必须实例化后才可以使用,而int不需要。
注意:①Integer变量和int变量比较时,只要两个变量的值是相等的,则结果为true。因为包装类Integer和基本数据类型int类型进行比较时,Integer会自动拆箱为int类型,然后进行比较,实际上就是两个int型变量在进行比较;②由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的,因为其内存地址是不同的;③非new生成的Integer变量和new Integer()生成的变量进行比较时,结果为false。因为非new生成的Integer变量指向的是Java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同。
重载和重写的区别?
重载:重载发生在同一个类中,若多个方法之间方法名相同、参数列表不同,则它们构成重载的关系。重载与方法的返回值以及访问修饰符无关,即重载的方法不能根据返回类型进行区分。
重写:重写发生在子类与父类之间,若子类方法想要和父类方法构成重写关系,则它的方法名、参数列表必须与父类方法相同。另外,返回值要小于等于父类方法,抛出的异常要小于等于父类方法,访问修饰符则要大于等于父类方法。若父类方法的访问修饰符为private,则子类不能对其重写。
构造方法有哪些特点?构造方法能不能重写?
构造方法特点如下:(1)名字与类名相同;(2)没有返回值;(3)生成类的对象时自动执行,无需调用,主要作用是完成对象的初始化工作。
构造方法不能重写。因为构造方法需要和类保持同名,而重写的要求是子类方法要和父类方法保持同名。如果允许重写构造方法的话,那么子类中将会存在与类名不同的构造方法,这与构造方法的要求是矛盾的。
如果一个类没有声明构造方法,该程序能正确执行吗?
如果一个类没有声明构造方法,也可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java就不会再添加默认的无参数的构造方法了
对象实体与对象引用有何不同?对象的相等和引用相等的区别?
new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
对象的相等一般比较的是内存中存放的内容是否相等;引用相等一般比较的是他们指向的内存地址是否相等。
成员变量和局部变量的区别
成员变量:(1)成员变量是在类的范围里定义的变量;(2)成员变量有默认初始值;(3)未被static修饰的成员变量也叫实例变量,它存储于对象所在的堆内存中,生命周期与对象相同;(4)被static修饰的成员变量也叫类变量,它存储于方法区中,生命周期与当前类相同。
局部变量:(1)局部变量是在方法里定义的变量;(3)局部变量没有默认初始值;(3)局部变量存储于栈内存中,作用的范围结束,变量空间会自动的释放。
静态变量和实例变量的区别?
静态变量:是被static修饰的变量,也称为类变量;当创建了类的多个对象时,多个对象共享同一个静态变量,当修改其中一个对象的静态变量时,其他对象的静态变量也被修改。静态变量随着类的加载而加载,要早于对象的创建,可以通过“类.静态变量”的方式调用。由于类只加载一次,则静态变量在内存中也只会存在一份(在方法区的静态域中)。
实例变量:未被static修饰的成员变量,也叫实例变量,它存储于对象所在的堆内存中,生命周期与对象相同;需要先创建对象,然后通过对象才能访问到它。当创建了类的多个对象时,每个对象独立的拥有一套类中的非静态属性,当修改其中一个对象的非静态属性时,不会导致其他对象中同样的属性被修改。
static关键字
在Java类里只能包含成员变量、方法、构造器、初始化块、内部类(包括接口、枚举)5种成员,static可以修饰类中的所有成员,以static修饰的成员就是类成员,类成员属于整个类,而不属于单个对象。
被static修饰的变量称为静态变量,被static修饰的方法称为静态方法,在静态方法中,只能调用静态的方法或属性,不能访问实例成员。static修饰的成员可以通过类名访问,也可以通过对象名访问。static修饰的类可以被继承。
final关键字
(1)final修饰类:该类不能被继承;(2)final修饰方法:该方法不能被重写
(3)final修饰变量:该变量为常量,final修饰变量时可以赋值的位置有:显示初始化、代码块内初始化、构造方法内初始化。
abstract关键字
abstract修饰类:(1)抽象类不能实例化(不能创造该类的对象);(2)抽象类中可以没有抽象方法;(3)一般都会提供抽象类的子类,通过实例化子类来调用抽象类中的结构
abstarct修饰方法:(1)抽象方法只有方法的声明,没有方法体;(2)包含抽象方法的类一定是抽象类;(3)若子类重写了父类的所有抽象方法,则可以实例化子类,如果子类没有重写父类的所有抽象方法,则需要将子类声明为抽象类。
接口
(1)接口内只能定义全局常量(public static final)、抽象方法(public abstact)、静态方法(public static)、默认方法(public default);
(2)接口内不能定义构造器,因此接口不可以实例化(给接口创建对象);
(3)接口通过类实现(implements)的方式来使用,如果实现类实现了接口中所有的方法,则此实现类可以实例化。
(4)一个类可以实现多个接口(该类需要实现所有接口中的所有方法)
接口和抽象类有什么共同点和区别?
相同:①都不能被实例化;②都可以包含抽象方法;③都可以有默认方法。
不同:①接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则可以包含普通方法;②接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也可以定义静态常量;③接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作;④接口里不能包含初始化块;抽象类可以包含初始化块;⑤一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。
谈谈你对面向接口编程的理解?
接口体现的是规范和实现分离,充分利用接口可以极好地降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。
hashCode()和equals()
hashCode()的作用是获取哈希码(int整数),哈希码的作用是确定该对象在哈希表中的索引位置。equals()是用于比较两个对象是否相等。如果两个对象的hashCode值相等,那这两个对象不一定相等(哈希碰撞),同样的hashCode有多个对象,继续使用 equals()来判断是否真的相同;如果两个对象的hashCode值相等并且equals()方法也返回true,我们才认为这两个对象相等;如果两个对象的hashCode值不相等,我们就可以直接认为这两个对象不相等。两个相等的对象的hashCode值一定相等。
为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的hashCode值一定相等,也就是说如果equals()方法判断两个对象是相等的,那这两个对象的hashCode值也要相等。如果重写equals()时没有重写hashCode()方法的话就可能会导致equals()方法判断是相等的两个对象,hashCode值却不相等。
==和equals()的区别?
==:(1)对于基本数据类型来说,== 比较的是值;(2)对于引用数据类型来说,== 比较的是对象的内存地址,即判断它们是否为同一个对象。
equals()不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。
(1)类没有重写equals()方法:通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,即比较两个对象的内存地址是否相同。(2)类重写了equals()方法:比较两个对象中的内容是否相等(如String)。
String、StringBuffer、StringBuilder的区别?
String:由final修饰,是不可变类,不能被继承,线程安全。
StringBuffer:可变的字符序列、线程安全。
StringBuilder:可变的字符序列、线程不安全、效率高,和StringBuilder有共同的父类AbstractStringBuilder。
String为什么要设计为不可变类?
(1)由于String被广泛使用,会用来存储敏感信息,如果字符串是可变的,容易被篡改,无法保证使用字符串进行操作时,它是安全的。
(2)字符串常量池的需要,字符串常量池是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象;若字符串可变,字符串常量池失去意义。
(3)字符串不变性保证了hash码的唯一性,使得类似HashMap,HashSet等容器才能实现相应的缓存功能。由于String的不可变,避免重复计算hashcode,只要使用缓存的hashcode即可,大大提高了在散列集合中使用String对象的性能。
String字符串修改实现的底层原理?
当用String类型来对字符串进行修改时,其实现方法是首先创建一个 StringBuilder,其次调用StringBuilder的append()方法,最后调用StringBuilder的toString()方法把结果返回。
String str = "abc"和new String("abc")的区别?
String str ="abc":JVM 会首先检查字符串常量池中是否已经存在该字符串对象,如果已经存在,那么就不会再创建了,直接返回该字符串在字符串常量池中的内存地址,将其引用赋值给变量;如果字符串常量池中没有该字符串,那么就会在字符串常量池中创建该字符串对象,然后再返回。
new String("abc"):JVM会首先检查字符串常量池中是否已经存在该字符串,如果已经存在,则不会在字符串常量池中再创建了;如果不存在,则就会在字符串常量池中创建该字符串对象,然后再到堆内存中再创建一份字符串对象,把字符串常量池中的字符串内容拷贝到内存中的字符串对象中,然后返回堆内存中该字符串的内存地址。
如何处理异常?
在Java中,处理异常的语句由try、catch、finally三部分组成。使用try将可能出现异常的代码包裹起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去catch中进行匹配。一旦try中的异常对象匹配到某一个catch时,就进入到catch中进行异常处理。处理完成后,就跳出当前的try-catch结构,继续执行后面的代码。像数据库连接、输入输出流、网络变成Socket等资源,JVM不能自动回收,我们需要手动进行资源释放,此时的资源释放需要声明在finally中。finally中声明的是一定会被执行的代码,即使catch中又出现了异常、try中有return语句或catch中有return语句等情况。
Java也允许程序主动抛出异常。当业务代码中,判断某项错误的条件成立时,可以使用throw关键字向外抛出异常。如果当前方法不知道该如何处理这个异常,指明执行此方法时可能会出现的异常类型,一旦方法体执行时出现异常,会在异常代码处生成一个异常类对象,此对象满足throws后的异常类型时,就会被抛出,后面的代码不会继续执行,则该异常将交给JVM处理。
finally一定会被执行吗?
finally中声明的是一定会被执行的代码,即使catch中又出现了异常、try中有return语句或catch中有return语句等情况。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。
finally可以有return吗?
在通常情况下,不要在finally块中使用return、throw等导致方法终止的语句,一旦在finally块中使用了return、throw语句,将会导致try块、catch块中的return、throw语句失效。
Exception和Error有什么区别?
Error类和Exception类的父类都是Throwable类。主要区别如下:
Exception:程序本身可以处理的异常,可以通过catch来进行捕获。Exception 又可以分为 Checked Exception(受检查异常,必须处理)和 Unchecked Exception (不受检查异常,可以不处理)。
Error:Error属于程序无法处理的错误,例如Java虚拟机运行错误、虚拟机内存不够错误、类定义错误等。这些异常发生时,JVM一般会选择线程终止。
throw和throws的区别?
(1)throw:在方法体内部,表示抛出异常,由方法体内部的语句处理;throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例;
(2)throws:在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理;表示出现异常的可能性,并不一定会发生这种异常。
什么是泛型?有什么作用?
Java的参数化类型被称为泛型,允许程序在创建集合时指定集合元素的类型,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换。
泛型的类型必须是类,不能是基本数据类型,如果要用到基本数据类型,使用包装类替换;
泛型擦除
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,与泛型相关的信息会被擦除掉。比如一个List<String> 类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。
List<? super T>和List<? extends T>有什么区别?
? 是类型通配符,List<?> 可以表示各种泛型List的父类,意思是元素类型未知的List;
List<? super T> 用于设定类型通配符的下限,此处? 代表一个未知的类型,但它必须是T的父类型,即可以接受任何T 的父类构成的 List;
List<? extends T> 用于设定类型通配符的上限,此处 ? 代表一个未知的类型,但它必须是T的子类型,即可以接受任何继承自T的类型的List。
什么是反射?
每个类都有一个Class对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class文件,该文件内容保存着Class对象。类加载相当于 Class对象的加载,类在第一次使用时才动态加载到 JVM 中。反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class不存在也可以加载进来。
具体来说,程序运行时,通过反射机制,我们可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的信息;可以通过反射创建任意一个类的实例,并访问该实例的成员;可以通过反射机制生成一个类的动态代理类或动态代理对象。
Java反射在实际项目中有哪些应用场景?
使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;
多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;
面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。
continue、break和return的区别是什么?
continue:指跳出当前的这一次循环,继续下一次循环。
break:指跳出整个循环体,继续执行循环下面的语句。
return:用于跳出所在方法,结束该方法的运行。
final、finally、finalize 的区别?
final:用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、被其修饰的类不可继承;
finally:异常处理语句结构的一部分,表示总是执行;
finallize:Object类的一个方法,在垃圾回收时会调用被回收对象的finalize
如何实现对象的克隆?
(1)实现Cloneable接口并重写Object类中的clone()方法;
(2)实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆。
深克隆和浅克隆的区别?
(1)浅克隆:拷贝对象和原始对象的引用类型引用同一个对象。浅克隆只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化。
(2)深克隆:拷贝对象和原始对象的引用类型引用不同对象。深拷贝是将对象和值都复制过来,会完全复制整个对象,两个对象修改其中任意的值另一个值不会改变。
Java中有哪些容器(集合)?
Java容器主要包括Collection和Map,Collection主要用于存放单一元素; Map主要用于存放键值对。
Collection接口又有三个主要的子接口:List、Set和Queue。(1)List是元素有序、可重复的集合,主要的实现类有ArrayList、LinkedList、Vector;(2)Set是元素无序、不可重复的集合,主要的实现类有HashSet,LinkedHashSet,TreeSet;(3)Queue是先进先出(FIFO)的队列,存储的元素是有序、可重复的,主要的实现类有ArrayQueue、LinkedList、PriorityQueue。
Map用于存储key - value对,是无序的,其中key不能重复,主要的实现类有HashMap、TreeMap、Hashtable、ConcurrentHashMap。
Java容器(集合)的主要实现类有哪些?
ArrayList:线程不安全,效率高;底层使用Object[ ]存储,查找快,增删较慢,随机访问元素的复杂度是O(1)。
LinkedList:线程不安全;底层使用双向链表存储,查找慢、增删快,随机查找元素的复杂度是O(n);LinkedList的节点除了存储数据,还存储了两个指针,比ArrayList更占内存。
Vector:线程安全,效率低;底层使用Object[ ]存储。
HashSet:线程不安全;基于哈希表实现,不支持有序性操作;可以存储null;
TreeSet:线程不安全;基于红黑树实现,支持自然排序和定制排序;不可以存储null;
LinkedHashSet:线程不安全;是HashSet的子类,基于链表和哈希表实现;对于频繁的遍历操作,其效率大于HashSet;
ArrayDeque:基于可变长度的数组和双指针实现,不可以存储null。
PriorityQueue:线程不安全;基于二叉堆结构实现,底层使用可变长的数组来存储数据;可以用它来实现优先队列。
HashMap:线程不安全、效率高;JDK1.7中的HashMap是基于数组+链表来实现的,它的底层维护一个Entry数组;JDK1.8中的HashMap,是基于数组+链表+红黑树来实现的,它的底层维护一个Node数组;存储的key和value可以是null;
TreeMap:线程不安全;基于红黑树实现,按照key-value对中的key对数据进行排序,实现排序遍历;
Hashtable:线程安全、效率很低;底层采用数组+链表实现,存储的key和value不能是null。
ConcurrentHashMap:线程安全;JDK1.7的ConcurrentHashMap 底层采用分段的数组+链表实现,JDK1.8采用的数组+链表+红黑树。
LinkedHashMap:作为HashMap的子类;底层使用双向链表存储;
ArrayList实现RandomAccess接口有何作用?为何LinkedList没实现这个接口?
RandomAccess 接口只是一个标志接口,标识实现这个接口的类具有随机访问功能。ArrayList底层是数组,而LinkedList底层是链表。数组天然支持随机访问,时间复杂度为O(1)。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。ArrayList实现了RandomAccess接口,就表明了他具有快速随机访问功能。
HashSet的实现原理?
HashSet是基于HashMap实现的,HashSet的构造方法中会初始化一个HashMap对象,所有放入HashSet中的集合元素实际上由HashMap的key来保存,而HashMap的value则存储了一个PRESENT,它是一个静态的Object对象。
Queue与Deque的区别
Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。
Deque 是双端队列,在队列的两端均可以插入或删除元素。
HashMap的put方法的执行过程?
(1)先判断数组是否为空,若数组为空则进行第一次扩容;(2)通过hash算法,计算键值对在数组中的索引;(3)如果当前位置元素为空,则直接插入数据;如果当前位置元素非空,且key已存在,则直接覆盖其value;如果当前位置元素非空,且key不存在,则将数据链到链表末端;(4)若链表长度达到8,则将链表转换成红黑树,并将数据插入树中;(5)如果数组中元素个数(size)超过threshold,则再次进行扩容操作。
HashMap的扩容机制
(1)数组的初始容量为16,而容量是以2的次方扩充的。
(2)数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造器传入。
(3)为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时(7或8),先检测当前数组是否到达一个阈值(64),如果没有到达这个容量,先去扩充数组,否则会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能。
HashMap为什么线程不安全?
HashMap在并发执行put操作时,可能会导致形成循环链表,从而引起死循环。
HashMap中的循环链表是如何产生的?
在多线程的情况下,当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就会产生死循环了。
ConcurrentHashMap的实现原理?
在jdk 1.7中,ConcurrentHashMap是由Segment数据结构和HashEntry数组结构构成,采取分段锁来保证安全性。Segment继承了ReentrantLock,所以它是一种重入锁,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment 数组,一个Segment里包含一个HashEntry数组,Segment的结构和HashMap类似,是一个数组和链表结构。
JDK1.8 的实现已经摒弃了Segment的概念,而是直接用 Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized 和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。
ConcurrentHashMap是怎么分段分组的?
Segment的get操作先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素。get操作的高效之处在于整个get过程都不需要加锁,除非读到空的值才会加锁重读。
当执行put操作时,首先判断是否需要扩容;然后定位到添加元素的位置,将其放入HashEntry数组中。插入过程会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash 操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(尾插法),会通过继承 ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。
HashTable和ConcurrentHashMap的区别?
HashTable 和 ConcurrentHashMap 相比,效率低。
Hashtable之所以效率低主要是使用了synchronized关键字对put等操作进行加锁,而synchronized关键字加锁是对整张Hash表加锁,即每次锁住整张表让线程独占,致使效率低下;
ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,即每个分段则类似于一个Hashtable;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可,因此,ConcurrentHashMap在多线程并发编程中可是实现多线程put操作。
HashMap的get方法的执行过程?能否判断元素是否在map 中?
通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。
不能判断一个key是否包含在map中,因为get返回null有可能是不包含该 key,也有可能该key对应的value为 null。因为HashMap中允许key为null,也允许value为 null。
JDK1.8之后,为什么HashMap头插法改为尾插法?
头插法在并发下时可能形成数据环,get数据时死循环,而在1.8之前因为处理 hash 冲突的方式是用链表存放数据,使用头插法可以提升一定效率。但是在 1.8 之后链表长度达到阈值就要考虑升级红黑树了,所以哪怕进行尾插遍历次数也会很有限,效率影响不大。
Iterator
迭代器可以遍历并选择序列中的对象,并且只能单向移动,而开发人员不需要了解该序列的底层结构。
iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。之后使用next()获得序列中的下一个元素。
Iterator和ListIterator的区别?
Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。
Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
ListIterator 实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引等等。
Java中的IO流
数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件)的过程即输出。
按照数据流向,可以将流分为输入流和输出流,其中输入流只能读取数据、不能写入数据,而输出流只能写入数据、不能读取数据。
按照数据类型,可以将流分为字节流和字符流,其中字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符。字符流通常处理文本文件,字节流用来处理图片、视频、音频等非文本文件。
InputStream:字节输入流;Reader:字符输入流
OutputStream:字节输出流;Writer:字符输出流
怎么用流打开一个大文件?
(1)使用缓冲流。缓冲流内部维护了一个缓冲区,通过与缓冲区的交互,减少与设备的交互次数。使用缓冲输入流时,它每次会读取一批数据将缓冲区填满,每次调用读取方法并不是直接从设备取值,而是从缓冲区取值,当缓冲区为空时,它会再一次读取数据,将缓冲区填满。使用缓冲输出流时,每次调用写入方法并不是直接写入到设备,而是写入缓冲区,当缓冲区填满时它会自动写入设备。
(2)使用NIO。NIO采用内存映射文件的方式来处理输入/输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了,通过这种方式来进行输入/输出比传统的输入/输出要快得多。
Java中3种常见IO模型
(1)BIO:BIO属于同步阻塞IO模型,同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
(2)NIO:NIO主要由Channel,Buffer,Selector组成。它是支持面向缓冲的,基于通道的I/O操作方法。所有的IO在NIO中都从一个Channel开始,数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中。Buffer本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。Selector允许单线程处理多个Channel,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。
(3)AIO:异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
Java的序列化与反序列化
序列化机制可以将对象转换成字节序列,这些字节序列可以保存在磁盘上,也可以在网络中传输,并允许程序将这些字节序列再次恢复成原来的对象。其中,对象的序列化(Serialize)是指将一个Java对象写入IO流中,对象的反序列化(Deserialize),则是指从IO流中恢复该Java对象。
若对象要支持序列化机制,则它的类需要实现Serializable接口,该接口是一个标记接口,它没有提供任何方法,只是标明该类是可以序列化的。
若要实现序列化,则需要使用对象流ObjectInputStream和ObjectOutputStream。其中,在序列化时需要调用ObjectOutputStream对象的writeObject()方法,以输出对象序列。在反序列化时需要调用ObjectInputStream对象的readObject()方法,将对象序列恢复为对象。
Serializable接口为什么需要定义serialVersionUID变量?
serialVersionUID代表序列化的版本,通过定义类的序列化版本,在反序列化时,只要对象中所存的版本和当前类的版本一致,就允许做恢复数据的操作,否则将会抛出序列化版本不一致的错误。
如果不定义序列化版本,在反序列化时可能出现冲突的情况。增加了序列化版本之后,在这种情况下则可以抛出异常,以提示这种矛盾的存在,提高数据的安全性。
除了Java自带的序列化之外,你还了解哪些序列化工具?
Json:简单直观,可读性好,有jackson,gson,fastjson等解析工具
Protobuf:用来序列化结构化数据的技术,可以用来持久化数据或者序列化成网络传输的数据。更加节省空间(以二进制流存储)、速度更快、更加灵活。
Thrift:不仅仅是序列化协议,而是一个RPC框架,能够满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。
Avro:提供两种序列化格式,即JSON格式或者Binary格式。
并行和并发的区别?
(1)并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生;
(2)并行是在不同实体上的多个事件,并发是在同一实体上的多个事件;
线程和进程的区别?
进程:是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
线程:是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
创建线程的几种方式?
1. 继承Thread类:(1)创建一个继承于Thread类的子类;(2)重写Thread类的run():将此线程要执行的操作声明在run()中;(3)创建子类对象;(4)通过此对象调用start():启动当前线程,调用当前线程的run();
2. 实现Runnable接口:(1)创建一个实现了Runnable接口的类;(2)实现类去重写Runnable中的run()方法;(3)创建实现类的对象;(4)创建Thread类的对象,将实现类的对象作为参数传递到Thread类的构造器中;(5)通过Thread类的对象调用start()。
3. 通过实现Callable接口:(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的对象;(2)使用FutureTask类来包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值;(3)使用FutureTask对象作为Thread对象的target创建并启动新线程;(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
run()和start()有什么区别?
run()方法被称为线程执行体,它的方法体代表了线程需要完成的任务,每个线程都是通过某个特定Thread对象所对应的run()方法来完成操作的。
start()方法用来启动线程。调用start()方法启动线程时,系统会把该run()方法当成线程执行体来处理,这时无需等待run()方法体代码执行完毕,可以直接继续执行下面的代码;这时此线程是处于就绪状态,并没有运行。但如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。
阻塞线程的方式有哪些?
(1)线程调用sleep()方法主动放弃所占用的处理器资源;(2)线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;(3)线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有;(4)线程在等待某个通知;(5)程序调用了线程的suspend()方法将该线程挂起。
线程的生命周期
在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。
当线程对象调用了start()方法之后,该线程处于就绪状态,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态;当线程数大于处理器数时,会存在多个线程在同一个CPU上轮换的现象。
当一个线程开始运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。当发生如下情况时,线程将会进入阻塞状态:(1)线程调用sleep()方法主动放弃所占用的处理器资源;(2)线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;(3)线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有;(4)线程在等待某个通知;(5)程序调用了线程的suspend()方法将该线程挂起。
当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:(1)调用sleep()方法的线程经过了指定时间;(2)线程调用的阻塞式IO方法已经返回;(3)线程成功地获得了试图取得的同步监视器;(4)线程正在等待某个通知时,其他线程发出了一个通知;(5)处于挂起状态的线程被调用了resume()恢复方法。
线程会以如下三种方式结束,结束后就处于死亡状态:(1)run()或call()方法执行完成,线程正常结束;(2)线程抛出一个未捕获的Exception或Error;(3)直接调用该线程的stop()方法来结束该线程。
sleep()和wait()的区别?
sleep()方法让正在执行的线程主动让出cpu,然后cpu就可以去执行其他任务,在sleep指定时间后cpu再回到该线程继续往下执行,sleep方法只让出了cpu,而并不会释放同步资源锁,到时间自动恢复。
wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。wait()方法会放弃对象锁,进入等待队列。
sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用。
sleep()不会释放锁,而wait()会释放锁,并需要通过notify()/notifyAll()重新获取锁。
如何实现子线程先执行,主线程再执行?
启动子线程后,立即调用该线程的join()方法,则主线程必须等待子线程执行完成后再执行。
如何实现线程同步?
(1)使用Synchronized关键字:被该关键字修饰的方法或语句块会自动被加上内置锁,从而实现同步。
(2)使用wait和notify:wait()方法可以让当前线程释放对象锁并进入阻塞状态;notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
(3)ReentrantLock:ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
(4)使用特殊域变量volatile:volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新。
(5)使用可重入锁实现线程同步;
(6)使用阻塞队列实现线程同步;
wait、notify和notifyAll()?
wait()、notify()、notifyAll()用来实现线程通信,这三个方法都不是Thread类中所声明的方法,而是Object类中声明的方法,并且被final修饰,无法被重写。原因是每个对象都拥有锁,所以让当前线程等待某个对象的锁,所以应该通过这个对象来操作。
wait()方法可以让当前线程释放对象锁并进入阻塞状态。
notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度。反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。
synchronized关键字
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized关键字最主要的三种使用方式:(1)修饰实例方法:对当前对象实例加锁,进入同步代码前要获得当前对象实例的锁;(2)修饰静态方法:对当前类对象加锁,进入同步代码前要获得当前类对象的锁。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员。(3)修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。和synchronized方法一样,synchronized代码块也是锁定当前对象的。
synchronized可以修饰静态方法,但不能修饰静态代码块。
synchronized与Lock的区别
(1)synchronized是Java关键字,在JVM层面实现加锁和解锁;Lock是一个接口,在代码层面实现加锁和解锁。
(2)synchronized可以用在代码块上、方法上;Lock只能写在代码里。
(3)synchronized在代码执行完或出现异常时自动释放锁;Lock不会自动释放锁,需要在finally中显示释放锁。
(4)synchronized会导致线程拿不到锁一直等待;Lock可以设置获取锁失败的超时时间。
(5)synchronized无法得知是否获取锁成功;Lock则可以通过tryLock得知加锁是否成功。
(6)synchronized锁可重入、不可中断、非公平;Lock锁可重入、可中断、可公平/不公平,并可以细分读写锁以提高效率。
synchronized和ReenTrantLock的区别?
synchronized是关键字,依赖于JVM;ReentrantLock是类,依赖于API;它提供了比synchronized更多更灵活的特性:等待可中断、可实现公平锁、可实现选择性通知(锁可以绑定多个条件)。
volatile关键字
volatile关键字是用来保证有序性和可见性的。当一个变量被定义成volatile之后,它将具备两项特性:(1)保证可见性;(2)禁止指令重排。即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。
虽然volatile能够保证可见性,但它不能保证原子性。volatile变量在各个线程的工作内存中是不存在一致性问题的,但是Java里面的运算操作符并非原子操作,这导致volatile变量的运算在并发下一样是不安全的。
synchronized和volatile的区别?
(1)volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
(2)volatile仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的。
(3)volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
(4)volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
(5)volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
如果不使用synchronized和Lock,如何保证线程安全?
volatile:volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。
乐观锁和悲观锁
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。Java中悲观锁是通过synchronized关键字或Lock接口来实现的。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于多读的应用类型,这样可以提高吞吐量。乐观锁一般会使用版本号机制或者CAS(Compare And Swap)算法实现。
同步器AQS
抽象队列同步器AbstractQueuedSynchronizer(AQS),用来构建锁或者其他同步组件,减少了各功能组件实现的代码量,也解决了在实现同步器时涉及的大量细节问题,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,例如:ReentrantLock可重入锁(支持公平和非公平的方式获取锁);Semaphore计数信号量;ReentrantReadWriteLock读写锁。
AQS的原理是什么?
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
ThreadLocal
ThreadLocal是线程私有的局部变量存储容器,它用来存储线程私有变量,内部真正存取是一个Map。每个线程可以通过set()和get()存取变量,多线程间无法访问各自的局部变量。只要线程处于活动状态,它所对应的ThreadLocal实例就是可访问的,线程被终止后,它的所有实例将被垃圾收集。ThreadLocal存储的变量属于当前线程。
ThreadLocal经典的使用场景是为每个线程分配一个JDBC连接 Connection,这样就可以保证每个线程的都在各自的Connection上进行数据库的操作。另外ThreadLocal还经常用于管理Session会话,将Session保存在ThreadLocal中,使线程处理多次处理会话时始终是同一个Session。
线程池
系统启动一个新线程的成本是比较高的,在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。线程池提供了一种限制和管理资源(包括执行一个任务)的方式。
使用线程池的好处:(1)降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗;(2)提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行;(3)提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
如何创建线程池?
(1)通过ThreadPoolExecutor 的构造方法实现;
(2)通过Executor 框架的工具类 Executors 来实现。
线程池的工作流程
(1)判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。
(2)判断任务队列是否已满,没满则将新提交的任务添加在工作队列。
(3)判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和(拒绝)策略。
线程池都有哪些状态?
RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务。
SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用shutdown()方法会使线程池进入到该状态。
STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于RUNNING或SHUTDOWN状态时,调用shutdownNow()方法会使线程池进入到该状态。
TIDYING:如果所有的任务都已终止了,有效线程数为0,线程池进入该状态后会调用terminated()方法进入TERMINATED 状态。
TERMINATED:在terminated()方法执行完后进入该状态,默认terminated()方法中什么也没有做。
线程池的拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:(1)AbortPolicy:丢弃任务并抛出异常。(2)DiscardPolicy:丢弃任务,但是不抛出异常。(3)DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)。(4)CallerRunsPolicy:由调用线程处理该任务。
线程池的队列大小通常怎么设置?
(1)CPU密集型任务:尽量使用较小的线程池,一般为CPU核心数+1。
(2)IO密集型任务:可以使用稍大的线程池,一般为2*CPU核心数。
(63)混合型任务:可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
线程池有哪些参数,各个参数的作用是什么?
(1)corePoolSize(核心工作线程数):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize。
(2)maximumPoolSize(最大线程数):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
(3)keepAliveTime(多余线程存活时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
(4)TimeUnit(线程活动保持时间的单位)
(5)workQueue(队列):用于传输和保存等待执行任务的阻塞队列。
(6)threadFactory(线程创建工厂):用于创建新线程。
(7)handler(拒绝策略):当线程池和队列都满了,再加入线程会执行此策略。