JavaSE核心要点总结

文章结构:

序号总体范畴章节细分内容
1基础1/2/3/4JDK、语言基础、流程控制
2面向对象6/7/8封装、继承、多态、抽象、接口
3容器5/12泛型、容器
4异常9
5IO流&文件10/18IO、文件、网络编程
6多线程11
7Java 8新特性13注解、反射、内部类、Lambda表达式

一、基础

二、面向对象

(一)封装

1、什么是类?

(1)“类”是对某一类事物的描述,是一个抽象的、概念上的定义;

(2)在Java语言中,类是将数据和方法封装在一起的一种数据结构,其中数据表示类的属性,方法表示类的行为;

(3)定义一个类实际上是定义类的属性和方法,在使用类之前,必须先定义它,然后才可以利用所定义的类来声明相应的变量,并创建对象,这与声明一个基本类型变量(如 int x)实质上是一个概念,只是基本数据类型是系统定义好的。

2、什么是对象?

(1)“对象”是相对于“类”而言的,类是某一同类事物的描述,而对象则是实际存在的、属于该类事物的具体个体,因为对象也称为类的实例(instance);

  • 举例:水果是一个类,苹果、橘子、西瓜是对象;水果是抽象的,苹果、橘子、西瓜是客观存在的实体。
3、什么是封装?

封装是指把变量和方法包装在用一个类内,以限定外界对该类成员的访问,从而达到保护数据的一种技术。

4、对象的创建过程?

通过 new 关键字创建一个普通的对象过程:

(1)检测类是否被加载:当虚拟机执行到new时,会先去常量池中查找这个类的符号引用。如果能找到符号引用,说明此类已经被加载到方法区(方法区存储虚拟机已经加载的类的信息),可以继续执行;如果找不到符号引用,就会使用类加载器执行类的加载过程,类加载完成后继续执行。

(2)为对象分配内存:类加载完成以后,虚拟机就开始为对象分配内存,具体的分配内存有两种情况:

  • 第一种情况:内存空间绝对规整,虚拟机只需要在被占用的内存和可用空间之间移动指针即可,这种方式被称为指针碰撞。
  • 第二种情况:内存空间是不连续的,虚拟机需要维护一个列表,来记录哪些内存是可用的。分配内存的时候需要找到一个可用的内存空间,然后在列表上记录下已被分配,这种方式成为空闲列表。

(3)为分配的内存空间初始化零值:对象的内存分配完成后,还需要将对象的内存空间都初始化为零值,这样能保证对象即使没有赋初值,也可以直接使用。

(4)对对象进行其他设置:分配完内存空间,初始化零值之后,虚拟机还需要对对象进行其他必要的设置,设置的地方都在对象头中,包括这个对象所属的类,类的元数据信息,对象的hashcode,GC分代年龄等信息。

(5)执行 init 方法:执行完上面的步骤之后,在虚拟机里这个对象就算创建成功了,但是对于Java程序来说还需要执行init方法才算真正的创建完成,因为这个时候对象只是被初始化零值了,还没有真正的去根据程序中的代码分配初始值,调用了init方法之后,这个对象才真正能使用。

5、什么是匿名对象?

(1)当一个对象被创建之后,在调用该对象的方法时,也可以不定义对象的引用变量,而直接调用这个对象的方法,这样的对象成为匿名对象。例如:

定义对象的引用变量 stu :

Student stu = new Student();
stu.setName("张三");

改写为匿名对象的形式:

new Student().setName("张三");
  • 注意:这个方法执行完之后,这个对象也就成了垃圾

(2)使用匿名对象的两种场景:

  • 场景一:如果对一个对象只需要进行过一次方法调用,那么就可以使用匿名对象;
  • 场景二:将匿名对象作为实参传递给一个方法调用。
6、什么是方法的重载?

在面向对象的程序设计语言中,有一些方法的含义相同,但带有不同的参数,这些方法使用相同名字,这就叫方法的重载(Overload)。

  • 换句话说,重载是指在同一个类内具有相同名称的多个方法,这些同名方法如果参数个数不同,或者参数个数相同但类型不同,则这些同名的方法就具有不同的功能,这就是方法的重载。

  • 方法的重载中参数的类型是关键,仅仅参数的变量名不同是不行的,也不允许参数个数或参数类型完全相同而只有返回值类型不同的重载。

    方法重载的条件:

    条件一:在同一个类中

    条件二:方法名相同

    条件三:形参列表不同(个数,类型)

  • 什么是方法的重写?子类继承父类以后,继承过来的方法不能满足子类当前的业务需求,子类有权利对这个方法进行重新编写,这就是方法的重写(又称为方法的覆盖)。

    方法重写的条件:

    条件一:两个类要有继承关系
    条件二:重写后的方法和之前的方法具有:相同的返回值类型、相同的方法名、相同的形参列表
    条件三:重写的方法的访问权限不能更低
    条件四:重写之后的方法抛出的异常类型不能大于父类抛出的异常类型

7、什么是构造方法?

(1)构造方法是一种特殊的、与类名相同的方法,分为无参构造方法和有参构造方法,作用是专门用于创建对象时,完成初始化工作;

(2)若一个类没有声明构造方法,依然可以创建新的对象,并能正确执行程序,这是因为如果省略构造方法,Java编译器会自动为该类生成一个默认的构造方法,程序在创建对象时会自动调用默认的构造方法。默认的构造方法没有参数,在其方法体中也没有任何代码,即什么都不做。

(3)构造方法的特殊性主要包括如下几方面:

  • 构造方法的方法名与类型相同;
  • 构造方法没有返回值,但不能些void;
  • 构造方法的主要作用是完成对类对象的初始化工作;
  • 构造方法一般不能由编程人员显式地直接调用,而是用new来调用;
  • 在创建一个类的对象的同时,系统对自动调用该类的构造方法,完成新对象的初始化。
8、static 关键字有什么用?

static 称为静态修饰符,它可以修饰类中的成员。没被 static 修饰的成员称为实例成员,实例成员属于个别对象所有,彼此之间不能共享。而被 static 修饰的成员称为静态成员,也称为类成员。

(1)静态变量:用 static 修饰的成员变量称为静态变量

  • 静态变量是隶属于类的变量,而不是属于任何一个类的具体对象;

  • 静态对象不需要实例化,就可以直接使用;当然也可以通过实例对象来访问静态变量,例如:

    类名.静态变量名;
    对象名.静态变量名;
    
  • 静态变量是一个公共的储存单元,不是保存在某个对象实例的内存空间中,而是保存在类的内存空间的公共存储单元中。

  • 对于类的任何一个具体对象而言,静态变量是一个公共的存储单元,类的任何一个对象访问它时,取到的都是一个相同的数值。

  • 由于静态变量是所有对象的公共存储空间,所以使用静态变量的另一个优点是可以节省大量的内存空间,尤其是大量创建对象的时候。

(2)静态方法:用 static 修饰的方法属于类的静态方法,又称为类方法

  • static 是属于类的,他在内存中的代码段将被所有对象所共用

  • static 方法只能访问 static 成员变量或调用 static 成员方法,不能直接访问非静态的成员,但可以通过创建对象的方法间接地访问非静态成员。

  • 调用静态方法,可以直接使用类名调用,也可以用某一个具体的对象名来调用,例如:

    类名.静态方法名();
    对象名.静态方法名();
    

(3)静态初始化器(“静态代码块”):由 static 修饰的一对花括号 “{ }” 的语句组

  • 作用:和构造方法类似,都是用来完成初始化工作的
  • 和构造方法的区别:
    • 构造方法是对每个新创建的对象进行初始化,而静态初始化器是对类自身进行初始化。
    • 构造方法是用 new 关键字创建新对象时由系统执行执行,而静态初始化器一般不能由程序调用,它是在所属类被加载入内训时由系统自动执行的。
    • 用 new 关键字创建多少个对象,就会调用多少次构造方法吗;但静态初始化器则在类被加载入内存时只执行一次,与创建多少个对象无关。
9、Java 的垃圾回收机制

定义:Java 运行环境提供了一个系统的垃圾回收器线程,负责自动回收那些没有被引用的对象所占用的内存。垃圾回收的好处:

  • 它把程序员从复杂的内存追踪、监测、释放等工作中解放出来;
  • 它防止系统内存被非法释放,从而使系统更加稳定;
  • 当垃圾回收器将要释放无用对象占用的内存时,先调用该对象的 finalize() 方法。

(二)继承

1、什么是继承?

(1)继承的定义:类的继承是使用已有的类为基础,派生出新的类。被继承的类称为父类或超类,由继承而得到的类称为子类。

(2)继承关键字:extends,例如:

class Subclass extends Superclass {
	...
}

(3)继承的实质:通过类继承的方式,便能开发出新的类,而不需要编写相同的程序代码,实质上是实现代码的复用,提高开发效率。

(4)继承的特点:

  • 一个父类可以同时拥有多个子类,但一个类只能有一个“直接父类”,即:“一个父类可以有多个儿子,而这个儿子只能有一个亲生父亲”。
  • 父类是子类的公共成员的集合,而每一个子类则是父类的特殊化,是对公共成员变量和方法在功能、内涵方面的扩展和延伸。
  • 子类继承父类之后,可以直接访问父类的非私有成员。
  • 采用继承机制来组织、设计系统中的类,可以提高程序的抽象程度,使之更能接近于人类的思维方式。
2、子类有哪些特性?

(1)新定义的类,它可以从父类那里继承所有“非私有成员”作为自己的成员。

(2)子类的每个对象也是其父类的对象,这是继承性的“即是”性质;反之,父类的对象不一定是它子类的对象。

(3)Java 程序在执行子类的构造方法之前,会先调用父类中的无参构造方法,其目的是为了要帮助继承自父类的成员做初始化操作。

(4)在子类的构造方法中,通过 super() 语句来调用父类特定的构造方法,但 super() 语句必须写在子类构造方法的第一行,否则编译时将出现错误信息。

3、protected 关键字的好处?

(1)用 protected 修饰的成员可以被该类自身、与它在同一个包中的其他类、在其他包中该类的子类三种类所引用。

(2)将成员声明为 protected 的最大好处是可以同时兼顾成员的安全性与便利性,因为它只能供父类与子类或同一包中的类来访问,而其他类则无法访问它。

4、什么是方法的覆盖?

(1)定义:

  • 方法的重载:重载是指在同一个类内定义多个名称相同但参数个数或类型不同的方法,Java。

  • 方法的覆盖:覆盖是指在子类中定义名称、参数个数与类型均与父类中完全相同的方法,用以重写父类中同名方法的功能。

    • 也就是说,父类与子类的方法名称、参数个数与类型必须完全相同,否则就不是覆盖。

(2)子类中不能覆盖父类中声明为 final 或 static 的方法。

5、什么是“向上转型”和“向下转型”?

(1)向上转型:创建父类类型的变量指向子类对象,即将子类对象赋值给父类类型的变量,这种技术称为“向上转型”。

  • 注:向上转型是将子类对象看作是父类对象,是从一个较具体的类到一个较抽象的类之间的转换,所以它是安全的。

(2)向下转型:将父类对象通过强制转换为子类型,再赋值给子类对象的技术。向下转型就是将较抽象的类转换为较具体的类。

  • 当在程序中使用向下转型技术时,必须使用显示类型转换。
6、final 关键字有什么用?

(1)修饰成员变量:如果用 final 来修饰成员变量,则说明该变量是最终变量,不可再改变,即“常量”,程序的其他部分都可以访问,但不能修改。

  • 若一个成员变量被 static final 两个修饰符所限定,它实际的含义就是常量,所以在程序中通常用 static 和 final 一起来指定一个常量,且这样的常量只能在定义时被赋值;
  • 若一个成员变量只被 final 修饰而不用 static 修饰,则必须且只能赋值一次,不能默认,这种成员变量的赋值方式有两种:一种是在定义变量是附初值,另一种是在某一个构造方法中进行赋值。

(2)修饰成员方法:如果用 final 来修饰成员方法,则该成员方法不能再被子类所覆盖,即该方法为最终方法

  • 注:对于一些比较重要且不希望被子类重写的方法,可以使用 final 修饰符对成员方法进行修饰,这样可以增加代码的安全性。

(3)修饰类:如果一个类被 final 修饰符所修饰,则说明这个类不能再被其他类所继承,即该类不可能有子类,这种类被称为最终类。例如:class String

7、Object 类有哪些特性?

(1)Object 类是所有类的来源,如果某个类没有使用 extends 关键字,则该类默认为 java.lang.Object 类的子类。

(2)Object 类常用的方法:

  • boolean equals(Object obj) :判断两个对象变量所指向的是否为同一个对象
  • String toString(): :将调用 toString() 方法的对象转换成字符串
  • final Class getClass() :返回运行时对象所属的类
  • Object clone() :返回调用该方法的对象的一个副本
8、== 和 .equals() 的区别?

(1)对于字符串变量来说:

注意:对于字符串的操作,Java 程序在执行时会维护一个字符串池,对于一些可共享的字符串对象,会现在字符串池中查找是否有相同的字符串内容(字符相同),如果有就直接返回,而不是直接创建一个新的字符串对象,以减少内存的占用。当在程序中直接使用 “ ” 括起来的一个字符串时,该字符串就会在字符串池中。

  • == 用于比较两个字符串对象在堆内存中的首地址;
  • .equals() 用于比较两个字符串所包含的内容(字符)是否相同;

(2)对于非字符串变量来说:== 和 .equals() 方法都用来比较其所指对象在堆内存中的首地址,换句话来说,== 和 .equals() 方法都是用来比较两个类类型的变量是否指向同一个对象。

(三)多态

1、什么是多态?

多态的定义:“多态”即“对象的多种形态”,指同一个实体同时具有多种形式,即同一个对象,在不同时刻,代表的对象不一样。例如:

Map<Integer, String> map = new HashMap()<>; 

注:多态的规则:

  • 一个对象的编译类型与运行类型可以不一致;
  • 编译类型在定义对象时就确定了,不能改变,而运行类型是可以变化的;
  • 编译类型看定义对象时 = 号的左边(看自身),运行类型看 = 号的右边(看子类,或实现类的实例对象);
2、实现多态的前提?
  • 前提一:要有继承;
  • 前提二:要有方法的重写;
  • 要有父类引用指向子类对象,例如:Animal a = new Cat();
3、实现多态的机制?

Java实现多态,靠的是父类(或接口定义)的引用变量,可以指向子类(或具体实现类的实例对象),而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。

4、多态的优势?

(1)可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。

(2)可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。

(3)接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。

(4)灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。

(5)简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

(四)抽象

1、什么是抽象?

在小吴看来,Java中的“抽象”是一种设计理念,旨在将类或者方法的共性抽出来,做一个模版,供后续使用。例如:

  • 抽象类就是专门用来当作父类的类,换句话说:“生来就是当爹的料”。
  • 抽象方法制定了必须要做的工作,但不指定如何做这项工作。
2、Java语言使用抽象理念的地方?

Java语言包含抽象类和抽象方法:

(1)抽象类:以 abstract 修饰符所修饰的类,称为抽象类;

abstract class ClassName {
    ...
}

(2)抽象方法:以 abstract 修饰符所修饰的方法,称为抽象方法;

public abstract void run()
3、抽象类有哪些特征?

抽象代码示例:

public abstract class Animal {
    public abstract void eat();
}
public class Cat extends Animal {
    public void eat() {
        System.out.println("小猫爱吃鱼");
    }
}
public class Dog extends Animal {
    public void eat() {
        System.out.println("小狗爱吃骨头");
    }
}

(1)抽象类的作用优点类似“模版”,它可以作为父类被它所有的子类所共享,其目的是根据它的格式来创建和修改新的子类。

(2)抽象类不能直接创建对象,只能通过抽象类派生出新的子类,再由子类来创建对象。

(3)抽象类的子类必须实现父类中所有的抽象方法,或者将自己也声明为抽象的。

  • 当一种方法声明为抽象方法时,意味着这种方法必须被子类的方法所覆盖,否则子类仍然是抽象的。

(4)由于抽象类是需要被继承的,所以抽象类不能用 final 来修饰。

(5)一个类不能即是最终类,又是抽象类,即关键字 abstract 与 final 不能同时使用。

4、抽象方法有那些特征?

(1)抽象方法主需要声明,不需要实现。即用 “;” 结尾,而不是用 “{ }” 结尾。

(2)抽象方法声明中,修饰符 static 和 abstract 不能同时使用。

(3)抽象类不一定包含抽象方法,但是包含抽象方法的类,一定要声明为抽象类。

(五)接口

1、什么是接口?
public interface InterfaceName {
    ...
}

(1)官方解释:Java 接口是一系列方法的声明,是一些方法特征的集合。接口中只有方法的特征,而没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的功能。

(2)我的理解:

  • 接口是一种特殊的类,接口也具有属性和方法,只不过接口中的方法没有方法体。
  • Java 设计接口是为了解决无法“多继承”的限制。
  • 可以直接把接口理解为100%的抽象类,既接口中的方法必须全部是抽象方法。
2、接口有哪些特性?
public interface List<E> extends Collection<E> {
    
    int size();
    boolean isEmpty();
    Iterator<E> iterator();

    ...
}

(1)接口中的“抽象方法”只需做声明,不用定义其处理数据的方法体;

(2)数据成员都是静态的,且必须赋初值,即数据成员必须是静态常量;

(3)接口中的成员属性和成员方法,都是公开的,所以在定义接口时若省略了 public 修饰符,在实现抽象方法时则不能省略该修饰符;

(4)由接口的定义可以看出,接口实际上就是一种特殊的抽象类。

3、为什么使用接口?

(1)接口被用来描述一种抽象;

(2)接口被用来实现解耦;

(3)解决 Java 语言无法多继承的问题。

  • 什么是多重继承?是指一个子类可以有一个以上的直接父类,该子类可以继承它所有直接父类的非私有成员。
  • Java 语言可以利用接口间接地接近多重继承的问题,实现更强大的功能。这得益于接口的继承。
4、什么是接口的继承?

定义一个接口时,可通过 extends 关键字声明该接口是某个已存在接口的子接口,它将继承父接口的常量、抽象方法和默认方法。

接口继承的特点:

(1)一个接口可以有一个以上的父接口,他们之间用 “,” 间隔,形成父接口列表;

(2)新接口将继承所有父类口中的常量、抽象方法和默认方法,但不能继承父接口中的静态方法,也不能被实现类所继承;

(3)如果类实现的接口继承自另外一个接口,那么该类必须实现在接口继承链中定义的所有方法。

三、容器

(1)容器类:

容器类是 Java 以类库的形式供用户开发程序时可直接使用的各种数据结构。

(2)容器框架:

Java 容器框架中,有两个名称分别为 Collection 和 Set 的接口。从 JDK 5 开始,容器框架全部采用泛型实现。

Java 容器框架结构由两棵接口树构成:

  • 第一棵树根节点为 Collection 接口,它定义了所有容器的基本操作,如添加、删除、遍历等。它的子接口 Set、 List 等则提供了更加特殊的功能,其中 Set 的对象用于存储一组不重复元素的集合,而 List 的对象用于存储一个由元素构成的线性表。
  • 第二棵树根节点为 Map 接口,它保持了“键”到“值”的映射,可以通过键来实现对值的快速访问。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7SHKqiU8-1688804917583)(C:\Users\吴启仁\AppData\Roaming\Typora\typora-user-images\image-20230309235849791.png)]

(一)Collection

1、Collection 接口概述?

(1)Collection 是一个接口,是高度抽象出来的集合,它包含了集合的基本操作,比如添加、删除、清空、遍历(读取)、是否为空、获取大小、是否保护某元素。

(2)Collection 接口的定义如下:

public interface Collection<E> extends Iterable<E> {
    ...
}

(二)List

1、List 集合概述?

(1)列表接口 List 是 Collection 的子接口;

(2)List 是一种包含有序元素的线性表,其中元素必须按顺序存放,且可重复,也可以是空值 null,List 接口使用下标来访问元素;

(3)元素之间的顺序关系可以由添加到列表的先后来决定,也可以由元素值的大小来决定。

(4)实现 List 接口的类主要由两个:链表类 LinkedList 和数组列表类 ArrayList,他们都是线性表。

(5)List 集合的接口定义:

public interface List<E> extends Collection<E> {
    int size();
    boolean isEmpty();
	boolean contains(Object o);
    Iterator<E> iterator();
    
	...
}
2、ArrayList 集合概述?

(1)ArrayList 集合是 List 接口的实现类之一;

(2)ArrayList 非线程安全,底层是一个 Object[] 动态数组,默认容量为 10,每次扩容 1.5 倍;

(3)ArrayList 集合中的元素通过索引访问,元素可以为 null,元素可以相同;

(4)ArrayList 集合源码分析:

  • 集合定义:

    public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
        ...
    }
    
  • 主要成员变量:

    private static final int DEFAULT_CAPACITY = 10;
    // ArrayList的默认长度是多少
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // ArrayList的默认空元素链表
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // ArrayList存放的数据
    transient Object[] elementData; // non-private to simplify nested class access
    // ArrayList的长度
    private int size;
    
  • 构造函数

    // 构造一个初始化容量为10的空列表
    public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
    // 初始化一个指定大小容量的列表
    public ArrayList(int initialCapacity) {
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {
                this.elementData = EMPTY_ELEMENTDATA;
            } else {
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            }
        }
    // 构造一个包含指定集合的元素列表, 按照它们由集合迭代器返回的顺序
    public ArrayList(Collection<? extends E> c) {
            elementData = c.toArray();
            if ((size = elementData.length) != 0) {
                // c.toArray might (incorrectly) not return Object[] (see 6260652)
                if (elementData.getClass() != Object[].class)
                    elementData = Arrays.copyOf(elementData, size, Object[].class);
            } else {
                // replace with empty array.
                this.elementData = EMPTY_ELEMENTDATA;
            }
        }
    
  • 扩容机制

    // 增加元素的方法
    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    
     //判断当前数组是否是默认构造方法生成的空数组,如果是的话minCapacity=10反之则根据原来的值传入下一个方法去完成下一步的扩容判断
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            }
            return minCapacity;
            }
    
    //minCapacitt表示修改后的数组容量,minCapacity = size + 1
     private void ensureCapacityInternal(int minCapacity) {
            //判断看看是否需要扩容
            ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
        }
    
3、LinkedList 集合概述?

(1)LinkedList 集合是 List 接口的实现类之一;

(2)LinkedList非线程安全,底层是链表,双向循环链表;

  • 在添加和删除元素时,LinkedList 具有比 ArrayList 更好的性能,但在 get 与 set 方面弱于 ArrayList

(3)LinkedList 允许元素为 null,且允许元素重复;

4、ArrayList 和 LinkedList 的异同?

(1)ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。

(2)对于随机访问 get 和 set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。

(3)对于新增和删除操作 add 和 remove,LinedList比较占优势,因为ArrayList要移动数据。

(三)Set

1、Set 集合概述?

(1)Set 接口是 Collection 接口的子接口;

(2)Set 集合无序、不可包含重复元素;

(3)HashSet 是基于 HashMap 实现的,TreeSet 是基于 TreeMap 实现的。

2、HashSet 集合概述?

(1)HashSet 集合是 Set 接口的实现类之一;

(2)HashSet 是底层是基于哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null;

(3)HashSet 要求放入的对象必须实现 HashCode() 方法,放入的对象,是以 hashcode 码作为标识的,而具有相同内容的 String 对象,hashcode 是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例 。

(4)HashSet 集合源码:

  • 集合定义:

    public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
        ...
    }
    
  • 构造函数:

    private transient HashMap<E,Object> map;
    //默认构造器
    public HashSet() {
        map = new HashMap<>();
    }
    //将传入的集合添加到HashSet的构造器
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
    //明确初始容量和装载因子的构造器
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    //仅明确初始容量的构造器(装载因子默认0.75)
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }
    

    注:通过上面的源码,我们发现了HashSet就是一个皮包公司,它就对外接活儿,活儿接到了就直接扔给HashMap处理了。因为底层是通过HashMap实现的,这里简单提一下:HashMap的数据存储是通过数组+链表/红黑树实现的,存储大概流程是通过hash函数计算在数组中存储的位置,如果该位置已经有值了,判断key是否相同,相同则覆盖,不相同则放到元素对应的链表中,如果链表长度大于8,就转化为红黑树,如果容量不够,则需扩容。

3、TreeSet 集合概述?

(1)TreeSet 集合是 Set 接口的实现类之一;

(2)TreeSet 是给予二叉树实现的,Treeset中的数据是自动排好序的,不允许放入null值;

(3)TreeSet可以确保集合元素处于排序状态,TreeSet支持两种排序方式:自然排序和定制排序。

  • 自然排序:使用要排序元素的 CompareTo (Object obj) 方法来比较元素之间大小关系,然后将元素按照升序排列;
  • 定制排序:使用Comparator接口,实现 int compare(T o1,T o2) 方法。

(四)Map

1、Map 接口概述?

(1)Map 提供了一种映射关系,其中的元素是以键值对(key-value)的形式存储的,能够实现根据 key 快速查找 value;

(2)Map 中的键值对以 Entry 类型的对象实例形式存在;

(3)键(key)不可重复,值(value) 可以重复,一个value值可以和很多 key 值形成对应关系,每个键最多只能映射到一个值。

(4)Map支持泛型,形式如:Map<K,V>

(5)Map中使用put(K key,V value)方法添加

2、什么是 Entry<K,V> ?

(1)接口是 Map 接口中的一个内部接口,定义 Map 集合的键值对,其定义如下:

interface Entry<K,V> {
    
    K getKey();
    
    V getValue();
    
    ...
    
}

(2)为什么要设计这个内部接口?

Map 中存放的元素均为键值对,所以每一个键值对必然存在一个映射关系,Map 中采用 Entry 内部类来表示一个映射项,映射项包含 Key 和 Value

  • 我们常说键值对键值对, 每一个键值对也就是一个Entry
  • Map.Entry 里面包含 getKey() 和 getValue() 方法
3、什么是 keySet() ?

(1)Set<K> keySet(); 是 Map 集合的一个内部接口,返回值是个只存放key值的Set集合;

  • 注:Map 集合中没有迭代器,Map 集合取出键值的原理就是将 Map 集合转成 Set 集合,再通过迭代器取出。

(2)HashMap 集合中 keySet() 方法的定义:

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

(3)keySet() 方法的使用举例:

public class Demo {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "第一");
        map.put(2, "第二");
        map.put(3, "第三");
        map.put(4, "第四");
        map.put(5, "第五");
        Set<Integer> set = map.keySet();
        System.out.println(set);    // 结果:[1, 2, 3, 4, 5]
    }
}
4、什么是 entrySet() ?

(1)是 Map 接口中的一个抽象方法,返回 Map 接口的“键值对” Set 集合,Set 集合里面的类型是Map.Entry;

(2)该方法由 Map 接口的实现类去重写,entrySet() 方法的定义如下:

Set<Map.Entry<K, V>> entrySet();

(3)entrySet() 在 HashMap 类中的重写:

public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

(4)entrySet() 方法的使用举例:

public class Demo {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(2, "第二");
        map.put(4, "第四");
        map.put(1, "第一");
        map.put(5, "第五");
        map.put(3, "第三");

        // 将Map中的元素,以“键-值”的形式存储到一个Set集合中,无序存放
        Set<Map.Entry<Integer, String>> entries = map.entrySet();
        for (Map.Entry<Integer, String> entry : entries) {
            System.out.print(entry + " "); // 结果:1=第一 2=第二 3=第三 4=第四 5=第五
        }

        Iterator<Map.Entry<Integer, String>> iterator = entries.iterator();
        List<Integer> list = new ArrayList<>();
        while (iterator.hasNext()) {
            int key = iterator.next().getKey();
            list.add(key);
        }
        // 将Map的key放在一个集合中
        System.out.println(Arrays.toString(list.toArray()));    // 结果:[1, 2, 3, 4, 5]

        // 将key排序之后,查找对应的value
        list.sort((o1, o2) -> o2 - o1);
        for (int i = 0; i < map.size(); i++) {
            String value =  map.get(list.get(i));
            System.out.print(value + " ");  // 结果:第五 第四 第三 第二 第一 
        }
    }
}

(五)HashMap

1、HashMap 集合概述?

(1)HashMap 集合类是 Map 接口的实现类之一,也是最常用的 Map 类,底层基于哈希表实现;

(2)HashMap 集合类的 key 值和 value 值都可以为null,但是一个 HashMap 只能有一个 key 值为 null 的映射,因为key值不可重复。

2、HashMap 的数据结构?

(1)HashMap 底层是基于数组和链表来实现的,即 HashMap 底层实现还是数组,只是数组的每一项都是一条链表。

  • 它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。
  • HashMap 中主要是通过 key 的 hashCode 来计算 hash 值的,只要 hashCode 相同,计算出来的 hash 值就一样。
  • 如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的 hash 冲突。
  • 学过数据结构的同学都知道,解决 hash 冲突的方法有很多,HashMap 底层是通过链表来解决 hash 冲突的。

(2)JDK 1.8前后 HashMap 的变化:

  • 在jdk7中,HashMap处理“碰撞”的时候,都是采用链表来存储,当碰撞的结点很多时,查询时间是O(n)。
  • 在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d2BOp7pl-1688804917584)(C:\Users\吴启仁\AppData\Roaming\Typora\typora-user-images\image-20230314225712373.png)]

3、HashMap 源码分析?
  • 定义:

    public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
        ...
    }
    
  • 构造函数

    // 构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    
    // 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    //构造一个带指定初始容量和加载因子的空 HashMap
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    

    注:

    • 在这里提到了两个参数:初始容量,加载因子,这两个参数是影响HashMap性能的重要参数;

    • ”初始容量“是创建哈希表时的容量,容量表示哈希表中桶的数量,

    • ”加载因子“是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。系统默认负载因子为0.75,一般情况下我们是无需修改的。

4、HashMap 和 TreeMap 的区别?

HashMap 和 TreeMap 都是 Java 中常用的 Map 接口实现类。

(1)HashMap 是基于哈希表实现的,它是非线程安全的,在多线程环境下需要使用 Collections.synchronizedMap(new HashMap()) 来进行包装。它的键值对是无序的。

(2)TreeMap 是基于红黑树实现的,它是线程安全的,键值对是有序的,它按照键的自然顺序或者比较器顺序来排序。

(3)总的来说 HashMap 更快,而 TreeMap 更适用于需要排序和有序的情况。

5、HashMap 和 LinkedHashMap 的区别?

LinkedHashMap 和 HashMap 都是 Java 中用于存储键值对的映射表,LinkedHashMap 是 HashMap 的子类,继承了父类的主要方法。主要区别在于:

(1)顺序:LinkedHashMap 会按照元素插入的顺序遍历,而 HashMap 则没有特定的顺序。

(2)实现:LinkedHashMap 内部使用链表来维护元素的顺序,而 HashMap 使用数组+链表的结构。

(3)性能:在需要保证顺序的场景下使用LinkedHashMap会有较差的性能,因为需要维护链表。而HashMap没有这样的限制,性能会更高。

(4)常用用途:HashMap 通常用于简单的键值映射,而LinkedHashMap 则多用于缓存和 LRU 算法中。

四、异常

五、I/O 流与文件

六、多线程

(一)多线程基础

1、什么是程序?

(1)程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说,程序是静态的代码;

(2)当程序在执行时,将会被操作系统载入内存中,并启动它的工作,然后就变成了所谓的进程。

2、什么是进程?

(1)进程是程序的一次执行过程,是系统运行程序的基本单位,进程是动态的;

(2)进程是操作系统资源分配和处理器调度的基本单位,拥有独立的代码、内部数据和运行状态;

(3)频繁的切换进程状态,会消耗大量的系统资源;

(4)系统运行一个程序即是一个进程从创建、运行到消亡的过程;

(5)每个进程之间是相互独立的,一起执行他们和先后执行他们是没有什么差别的。

3、什么是多任务?

(1)多任务是指在一个系统中可以同时运行多个进程,即有多个独立运行的任务,每一个任务对应一个进程;

(2)多任务是由操作系统将系统资源分配给各个进程,每个进程在CPU上交替运行着。

4、什么是线程?

(1)线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位;

(2)一个进程在其执行过程中可以产生多个线程,形成多条执行路径;

(3)线程不能独立存在,必须存在于线程之中;

(4)一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qpFCo7yJ-1688804917585)(C:\Users\吴启仁\AppData\Roaming\Typora\typora-user-images\image-20230202222234402.png)]

5、什么是多线程?

(1)多线程(multi-thread)是指同一个进程中同时存在几个执行体,按几条不同的执行路径同时工作的情况;

(2)执行一个线程不必等待另一个线程执行完成后才进行,所有线程都可以发生在同一时刻;

  • 多线程编程的含义就是可将一个程序任务分成几个可以同时并发执行的子任务;
  • 在网络编程中,许多功能都是可以并发执行的。
6、为什么要实现多线程?

使用多线程,本质上就是提升程序性能。度量性能的指标有很多,但是有两个指标是最核心的,它们就是延迟和吞吐量。

(1)延迟:发出请求到收到响应这个过程的时间;延迟越短,意味着程序执行得越快,性能也就越好。

(2)吞吐量:在单位时间内能处理请求的数量;吞吐量越大,意味着程序能处理的请求越多,性能也就越好。

这两个指标内部有一定的联系(同等条件下,延迟越短,吞吐量越大),但是由于它们隶属不同的维度(一个是时间维度,一个是空间维度),并不能互相转换。

所谓提升性能,从度量的角度,主要是降低延迟,提高吞吐量。这也是使用多线程的主要目的。

7、线程的生命周期

每个Java程序都有一个默认的主线程,对于应用程序来说其主线程是 main() 方法执行的线程。要想实现多线程,必须在主线程中创建新的线程对象。

新建的线程在它的一个完整生命周期内通常要经历五种状态:

(1)新建(newborn):当一个 Thread 类或其子类的对象被声明并创建,但还未被执行的这段时间里,处于一种特殊的新建状态中。特征:

  • 此时线程对象已经被分配了内存空间和其他资源,并已被初始化,但是该线程尚未被调度
  • 此时的线程还可以被调度,编程就绪状态

(2)就绪(runnable):处于新建状态的线程被启动后,将进入线程队列排队等待CPU资源,此时它已具备了运行的条件,也就是出于就绪状态。特征:

  • 一旦轮到它来享用CPU资源时,就可以脱离创建它的主线程、独立开始自己的生命周期了
  • 原来处于阻塞状态的线程被解除阻塞后,也将进入就绪状态

(3)执行(running):当就绪状态的线程被调度并获取CPU资源的时候,便进入执行状态。特征:

  • 该状态表示线程正在执行,该线程已经拥有了对CPU的控制权。
  • 每一个 Thread 类及其子类的对象都一个重要的 run() 方法,该方法定义了这一类线程操作和功能。
  • 当线程对象被调度执行时,它将自动调用本对象的 run() 方法,从该方法的第一条语句开始执行,一直执行完毕,除非线程主动让出CPU的控制权或者被优先级更高的线程抢占。
  • 处于执行状态的线程在下列情况下将让出CPU的控制权:
    • 线程执行完毕;
    • 有比当前线程优先级更高的线程处于就绪状态;
    • 线程主动睡眠一段时间;
    • 线程在等待某一资源。

(4)阻塞(blocked):一个正在执行的线程如果在某些特殊情况下,将让出CPU并暂时中止自己的执行,线程处于这种不可执行的状态,就被称为阻塞状态。下面几种情况可使得一个线程进入阻塞状态:

  • 一是调用 sleep() 或 yield() 方法;
  • 二是为等待一个条件变量,线程调用 wait() 方法;
  • 三是该线程与另一线程 join() 在一起。

(5)消亡(dead):导致线程消亡的原因有两个:

  • 一是正常运行的线程完成了它的全部工作,即执行完了 run() 方法的最有一条语句并退出;
  • 二是当进程因故停止运行时,该进程中的所有线程将被强行终止。

注:当线程处于消亡状态、并且没有该线程对象的引用时,垃圾回收期会从内存中删除该线程对象。

8、线程的优先级

(1)在多线性的系统中,每个线程都被赋予了一个执行优先级,优先级决定了线程被CPU执行的优先顺序。优先级高的线程可以在一段时间内获得比优先级低的线程更多的执行时间。

(2)Java 语言的线程优先级从低到高以整数 1 ~ 10 表示,共分为 10 级。Thread 类有三个关于线程优先级的静态常量:

  • MIN_PRIORITY:表示最小优先级,通常为 1;

  • MAX_PRIORITY:表示最高优先级,通常为 10;

  • NORM_PRIORITY:表示普通优先级,默认为5。

注:如果想要改变线程的优先级,可以通过调用线程对象的 setPriority() 方法来进行设置。

(二)多线程实现法

1、继承 Thread 类

具体操作:继承 Thread 类,并重写父类的 run() 方法

public class MyThread extends Thread {
    // 演唱会,需要卖票100000张
    private int ticket = 100000;
    @Override
    public void run() {
        while (true) {
            synchronized (MyThread.class) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + " 卖票: No." + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}
public class MyThreadTest {
    public static void main(String[] args) {
        // 启动3个线程th1,th2,th3;共同卖100000张票
        MyThread window = new MyThread();
        Thread th1 = new Thread(window, "窗口1");
        Thread th2 = new Thread(window, "窗口2");
        Thread th3 = new Thread(window, "窗口3");
        th1.start();
        th2.start();
        th3.start();
    }
}
2、实现 Runnable 接口

具体操作:实现 Runnable 接口,并重写接口的 run() 方法

public class MyRunnable implements Runnable {
    // 演唱会,需要卖票100000张
    private int ticket = 100000;
    @Override
    public void run() {
        while (true) {
            synchronized (MyThread.class) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + " 卖票: No." + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}
public class MyRunnableTest {
    public static void main(String[] args) {
        // 启动3个线程th1,th2,th3;共同卖100000张票
        MyRunnable window = new MyRunnable();
        Thread th1 = new Thread(window, "窗口1");
        Thread th2 = new Thread(window, "窗口2");
        Thread th3 = new Thread(window, "窗口3");
        th1.start();
        th2.start();
        th3.start();
    }
}
3、实现 Callable 接口

具体操作:实现 Callable 接口,并重写接口的 call() 方法

public class MyCallable implements Callable<String> {
    // 演唱会,需要卖票100000张
    private int ticket = 100000;
    @Override
    public String call() throws Exception {
        while (true) {
            synchronized (MyThread.class) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + " 卖票: No." + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
        return "票卖完了...";
    }
}
public class MyCallableTest {
    public static void main(String[] args) {
        Callable<String> callable = new MyCallable();
        MyCallable window = new MyCallable();
        FutureTask<String> futureTask1 = new FutureTask<>(window);
        FutureTask<String> futureTask2 = new FutureTask<>(window);
        FutureTask<String> futureTask3 = new FutureTask<>(window);
        new Thread(futureTask1).start();
        new Thread(futureTask2).start();
        new Thread(futureTask3).start();
        try {
            System.out.println("窗口1返回结果:" + futureTask1.get());
            System.out.println("窗口2返回结果:" + futureTask2.get());
            System.out.println("窗口3返回结果:" + futureTask3.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}
4、线程池方式

具体操作:系统启动时创建线程池,使用时直接调用。是实际中用的最多的方法。

public class MyThreadPool implements Runnable{
    // 演唱会,需要卖票100000张
    private int ticket = 100000;
    @Override
    public void run() {
        while (true) {
            synchronized (test12_thread.Thread.MyThread.class) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + " 卖票: No." + ticket);
                    ticket--;
                } else {
                    System.out.println("票卖完了...");
                    break;
                }
            }
        }
    }
}
public class MyThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        // 提交5个任务,让线程池去执行
        pool.submit(new MyThreadPool());
        pool.submit(new MyThreadPool());
        pool.submit(new MyThreadPool());
        pool.submit(new MyThreadPool());
        pool.submit(new MyThreadPool());
        // 销毁线程池
        pool.shutdown();
    }
}

(三)线程池

1、线程池的本质
  • 线程池是一种多线程处理形式;
  • 创建若干个可执行的线程放入一个池子(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。
2、线程池实现多线程的优势

(1)降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

(2)提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。

(3)提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

3、ThreadPoolExecutor 类

(1)ThreadPoolExecutor 类的源码

线程池的真正实现类是 ThreadPoolExecutor

public class ThreadPoolExecutor extends AbstractExecutorService {
    ...
}

(2)ThreadPoolExecutor 类的构造方法

  • 构造方法1:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler);
    }
    
  • 构造方法2:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);
    }
    
  • 构造方法3:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler);
    }
    
  • 构造方法4:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
            null :
        AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
    
  • 构造方法参数解读

    • corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
    • maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
    • keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
    • unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
    • workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
    • threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
    • handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
4、线程池的参数

(1)任务队列(workQueue)

任务队列是基于阻塞队列实现的,即采用 生产者 - 消费者 模式,在 Java 中需要实现 BlockingQueue 接口。但 Java 已经为我们提供了 7 种阻塞队列的实现:

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
  • LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
  • PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
  • DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
  • SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
  • LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)。
  • LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列。

注意:有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置 maximumPoolSize 没有任何意义。

(2)线程工厂(threadFactory)

线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂:

// The default thread factory
static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
        Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
            poolNumber.getAndIncrement() +
            "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

(3)拒绝策略(handler)

当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过 Executors 框架已经为我们实现了 4 种拒绝策略:

  • AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
  • CallerRunsPolicy:由调用线程处理该任务。
  • DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
  • DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
5、线程池实现一:自定义线程池
// (1)创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                             MAXIMUM_POOL_SIZE,
                                             KEEP_ALIVE,
                                             TimeUnit.SECONDS,
                                             sPoolWorkQueue,
                                             sThreadFactory);

// (2)向线程池提交任务
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        ... // 线程执行的任务
    }
});

// (3)关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
6、线程池实现二:功能线程池

Executors 已经为我们封装好了 4 种常见的功能线程池,如下:

  • 定长线程池(FixedThreadPool)
  • 定时线程池(ScheduledThreadPool)
  • 可缓存线程池(CachedThreadPool)
  • 单线程化线程池(SingleThreadExecutor)

(1)定长线程池(FixedThreadPool)

  • 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。

  • 应用场景:控制线程最大并发数。

  • 创建方法源码:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
    }
    
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), 		
                                      threadFactory);
    }
    
  • 使用示例:

    // 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
    
    // 2. 创建好Runnable类线程对象 & 需执行的任务
    Runnable task = new Runnable(){
      public void run() {
         System.out.println("执行任务啦");
      }
    };
    
    // 3. 向线程池提交任务
    fixedThreadPool.execute(task);
    

(2)定时线程池(ScheduledThreadPool )

  • 特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。

  • 应用场景:执行定时或周期性的任务。

  • 创建方法源码:

    private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
     
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue());
    }
     
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }
    
    public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) {
        super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue(), threadFactory);
    }
    
  • 使用示例:

    // 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
    
    // 2. 创建好Runnable类线程对象 & 需执行的任务
    Runnable task = new Runnable(){
      public void run() {
         System.out.println("执行任务啦");
      }
    };
    
    // 3. 向线程池提交任务
    scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
    scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
    

(3)可缓存线程池(CachedThreadPool)

  • 特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。

  • 应用场景:执行大量、耗时少的任务。

  • 创建方法的源码:

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }
    
  • 使用示例:

    // 1. 创建可缓存线程池对象
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    
    // 2. 创建好Runnable类线程对象 & 需执行的任务
    Runnable task = new Runnable(){
      public void run() {
         System.out.println("执行任务啦");
      }
    };
    
    // 3. 向线程池提交任务
    cachedThreadPool.execute(task);
    

(4)单线程化线程池(SingleThreadExecutor)

  • 特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。

  • 应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。

  • 创建方法的源码:

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }
    
  • 使用示例:

    // 1. 创建单线程化线程池
    ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    
    // 2. 创建好Runnable类线程对象 & 需执行的任务
    Runnable task =new Runnable(){
      public void run() {
         System.out.println("执行任务啦");
      }
    };
    
    // 3. 向线程池提交任务
    singleThreadExecutor.execute(task);
    

(5)功能线程池的比较

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JeoJ0ipN-1688804917586)(C:\Users\吴启仁\AppData\Roaming\Typora\typora-user-images\image-20230330234436576.png)]

7、线程池总结

(1)Executors 的 4 个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

例如,在实例项目中,创建线程池:

/**
 * 多数据中心用于像各个边缘站点发送数据
 * 
 * @authors00422198
 * @since 2022-10-12
 */
public class MultiDCRestDispatcher {
    private static final Logger LOGGER = LoggerFactory.getLogger(MultiDCRestDispatcher.class);
    
    private static Supplier<MultiDCRestDispatcher> INSTANCE = Suppliers.memoize(MultiDCRestDispatcher::new);
    
    private static final int POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2 + 2;
    
    private static final int MAX_POOL_SIZE = 100;
    
    private static final ExecutorService CALL_EDGE_SITES_EXECUTOR = new ThreadPoolExecutor(POOL_SIZE, MAX_POOL_SIZE,
            10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new NamedThreadFactory("multi-dc-rest-dispatch"));
    
    public static MultiDCRestDispatcher getInstance() {
        return INSTANCE.get();
    }
}

(2)其实 Executors 的 4 个功能线程有如下弊端:

  • FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。
  • CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
8、线程池内的线程数量越多越好吗?

(1)“降低延迟,提高吞吐量”的方法?

基本上有两个方向:

  • 优化算法
  • 将硬件的性能发挥到极致

注:前者属于算法范畴,后者则是和并发编程息息相关了。

计算机主要硬件类型:一个是 I/O,一个是 CPU。在并发编程领域,提升性能本质上就是提升硬件的利用率,就是提升 I/O 的利用率和 CPU 的利用率。

(2)创建多少线程合适?

答:创建多少线程合适,要看多线程具体的应用场景。

  • I/O 密集型场景:程序一般都是 CPU 计算和 I/O 操作交叉执行的,由于 I/O 设备的速度相对于 CPU 来说都很慢,所以大部分情况下,I/O 操作执行的时间相对于 CPU 计算来说都非常长,这种场景一般都称为 I/O 密集型计算;
  • CPU 密集型场景:大部分场景下都是纯 CPU 计算。
  • 通用多线程创建数量:最佳线程数 = CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]

(四)多线程同步控制

1、为什么需要考虑线程同步情况?

(1)简单场景:因为线程功能简单,每个线程都包含了运行时所需要的数据和方法,这样的线程在运行时不需要外部的数据和方法,就不必关心其他线程的状态或行为,称这样的线程为独立的、不同步的或是异步执行的。

(2)复杂场景:当应用问题的功能增强、关系复杂,存在多个线程之间共享数据时,若线程仍以异步方式访问共享数据时,有时是不安全或不符合逻辑的。

2、什么是线程的同步?

当一个线程对共享的数据进行操作时,应使之成为一个“原子操作”,即在没有完成相关操作之前,不允许其他线程打断它,否则就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。

3、什么是线程间互斥?

被多个线程共享的数据,在同一时刻只允许一个线程处于操作之中,这就是同步控制中的线程间互斥问题。

4、Java 实现线程间互斥的方法?

为了使临界代码对临界资源的访问成为一个不可被中断的原子操作,Java 技术利用对象的 “互斥锁” 机制来实现线程间的互斥操作。

  • 在 Java 语言中每个对象都有一个互斥锁与之相连。

  • 一个对象的互斥锁只有一个,利用对一个对象互斥锁的争夺,可以实现不同线程的互斥效果。

  • 当一个线程获得互斥锁后,则需要该互斥锁的其他线程只能处于等待状态。

  • 例如:当线程 A 获得了一个对象的互斥锁后,线程 B 若也想获得该对象的互斥锁,就必须等待线程A完成规定的操作并释放出互斥锁后,才能获得该对象的互斥锁,并执行线程 B 中的操作。

  • Java 语言使用 synchronized 关键字来表示同步资源,这里的资源可以说一种类型的数据,也就是对象,也可以是一个方法,还可以是一段代码。

    • 注意:synchronized 直译为“同步”,但实际指的是“互斥”。
5、Synchronized 的功能?

首先判断对象或方法的互斥锁是否存在:

(1)互斥锁若在,就获得互斥锁,然后就可以执行紧随其后的临界代码段或方法体;

(2)互斥锁若不在(已被其他线程拿走),就进入等待状态,知道获得互斥锁。

注:当被 Synchronized 限定的代码段执行完,就会自动释放对象或方法的互斥锁。

6、Synchronized 关键字的使用方法?

(1)格式一:同步语句

Synchronized(对象) {
    // 临界代码段
}

(2)格式二:同步方法

public synchronized 返回值类型 方法名() {
    // 方法体
}
7、Synchronized 关键字注意事项

(1)synchronized 锁定的通常是临界代码,这些代码之间是串行执行的,不再是相互交替穿插并发执行,因而保证了 synchronized 代码块操作的原子性。

(2)synchronized 代码块中的代码数量越少越好,包含的范围越小越好,否则就失去多线程并发执行的很多优势。

(3)任何时刻,一个对象的互斥锁只能被一个线程拥有。

(4)临界代码中的共享变量应定义为 private 型,否则其他类的方法可能直接访问和操作该共享变量,这样 synchronized 的保护就失去了意义。

(5)如果 synchronized 用在类声明中,则表示该类中的所有方法都是 synchronized 的。

8、synchronized 和 lock 的区别?

synchronizedjava.util.concurrent.locks.Lock 是 Java 中两种用来实现线程同步的方式。

(1)synchronized 是 Java 的关键字,它可以修饰方法和代码块。当一个线程访问一个对象的同步方法或同步代码块时,其他线程不能访问这个对象的其他同步方法或同步代码块。

(2) Lockjava.util.concurrent.locks 包中的一个接口,它提供了更多的灵活性。比如可以尝试获取锁而不会阻塞线程、可以重试获取锁的次数以及可以提供公平锁和非公平锁。

总的来说,如果你需要基本的线程同步,可以使用 synchronized 关键字,如果需要更高级的线程同步,可以使用 Lock 接口。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uGRx6ZcY-1688804917586)(C:\Users\吴启仁\AppData\Roaming\Typora\typora-user-images\image-20230401155228226.png)]

(五)线程之间的通信

1、什么是线程之间的通信?

多线程的执行往往需要相互之间的配合,为了更有效地协调不同的线程的工作,需要在线程间建立沟通渠道,通过线程间的“对话”类解决线程间的同步问题,而不是紧紧依靠互斥机制。

2、Object 类中用于线程通信的常用方法?

(1)public final void wait() :如果一个正在执行同步代码(synchronized)的线程A执行了 wait() 调用在对象 x 上,该线程暂停执行而进入对象 x 的等待队列,并释放已获得的对象 x 的互斥锁。线程A到一直等到其他线程在对象 x 上调用 notify()notifyAll() 方法,才能够在重新获得对象 x 的互斥锁后继续执行(从 wait() 语句后继续执行)。

(2)public void notify() :唤醒正在等待该对象互斥锁的第一个线程;

(3)public void notifyAll() :唤醒正在等待该对象互斥锁的所有线程,具有最高优先级的线程首先被唤醒并执行。

注意:

  • 对于一个线程,若基于对象 x 调用wait()notify()notifyAll() 方法,该线程必须已经获得对象 x 的互斥锁。
  • wait()notify()notifyAll() 只能在同步代码块里调用。
3、wait() 和 sleep() 方法的区别?

(1)相同点:

  • 都可以让线程进入休眠状态。

  • 都可以相应interrupt中断请求。

(2)不同点:

  • wait() 必须配合 synchronized 使用,而 sleep() 不用;

  • wait() 属于 Object 对象的方法,而 sleep() 是属于 Thread 线程的方法

  • sleep() 不释放锁,而 wait() 要释放锁;

  • sleep() 必须要传递一个数值型的参数,而 wait() 可以不传参;

  • sleep() 让线程进入到 TIMED_WAITING 状态,而无参的 wait() 方法让线程进入了 WAITING 状态;

  • 一般情况下,sleep() 只能等待超过时间之后才能恢复执行,而 wait() 可以接收 notify() 、notifyAll() 之后就可以执行。

(六)ThreadLocal

待定。

七、Java 8新特性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值