学习Java8这一篇就够了

x33g5p2x  于2021-09-01 转载在 Java  
字(130.5k)|赞(0)|评价(0)|浏览(644)

第一章 基础语法

1.1、标识符

标识符可以简单理解为:给文件名、类名、接口名、注解名、数组名、方法名、变量名、常量名起名字。

标识符的命名规则:(必须遵守)

  • 标识符由字母(a-z,A-Z)、数字(0-9)、下划线(_)、美元符号($),且第一个字符不能为数字。
  • 标识符是严格区分大小写的。
  • 标识符使用 Unicode 标准字符集,最多可以识别 65535 个字符。
  • 标识符不能是 Java 的关键字和保留字。

标识符的写法规则:(建议遵守)

  • 大驼峰命名法:所有单词的首字母大写,单词间无空格,适用于:文件名、类名、接口名、注解名,例如:StudentName
  • 小驼峰命名法:除首字母小写,其余所有单词的首字母大写,单词间无空格,适用于:数组名、方法名、变量名,例如:studentName
  • 常量的命名法:所有单词大写,单词间用下划线分隔,适用于:常量名,例如:STUDENT_NAME

1.2、关键字和保留字

关键字是 Java 语言中已经被赋予特定意义的一些单词,不可以把这些关键字作为标识符来使用。

关键字的分类展示:(共50个)

  • 用于定义数据类型的关键字:class、interface、byte、short、int、long、float、double、char、boolean、void
  • 用于定义数据类型值的关键字:true、false、null
  • 用于定义流程控制的关键字:if、else、switch、case、default、while、do、for、break、continue、return
  • 用于定义访问权限修饰符的关键字:private、protected、public
  • 用于定义类、方法、变量修饰符的关键字:abstract、final、static、synchronized
  • 用于定义类与类、类与接口、接口与接口之间关系的关键字:extends、implements
  • 用于定义建立实例、引用实例、判断实例的关键字:new、this、super、instanceof
  • 用于定义异常处理的关键字:try、catch、finally、throw、throws
  • 用于定义包、导入包的关键字:package、import
  • 剩余其他的关键字:volatile、transient、strictfp、native、assert

保留字的分类展示:(共2个)

  • 在命名标识符的时候,虽然 const 和 goto 不是 Java 的关键字,但也不可以使用,这两个词可能会在以后的升级版本中被用到。

1.3、基本数据类型

数据类型:

整数类型:

int a = 0b11111111;//支持二进制
int b = 255;//支持十进制
int c = 0377;//支持八进制
int d = 0xFF;//支持十六进制

转义字符:

转义字符字符含义\ddd3位八进制数所代表的任意字符\uxxxx4位十六进制所代表的任意字符\'代表一个单引号字符\"代表一个双引号字符\\代表一个反斜杠字符\t水平制表符\r回车\n换行\b退格\f换页

1.4、变量和常量

  • 变量:在程序运行期间,其值可以发生改变的量,被称为变量。语法:数据类型 标识符 [=初始化值];
  • 常量:在程序运行期间,其值不会发生改变的量,被称为常量。语法:final 数据类型 标识符 [=初始化值];

1.5、类型转换

  • 自动类型转换:把小范围数据类型赋值给大范围数据类型,由于精度并不会丢失,因为这种转换是自动的。

  • 强制类型转换:把大范围数据类型赋值给小范围数据类型,由于精度可能会丢失,因此需要进行强制转换。

  • 语法:目标数据类型 新标识符 = (目标数据类型)(旧标识符 / 表达式);

1.6、运算符

运算符的分类:

  • 算术运算符:+、-、/*、/、%
  • 自增与自减:++、–
  • 位运算符:&(与)、|(或)、~(非)、^(异或)
  • 移位运算符:<<(左移)、>>(有符号右移)、>>>(无符号右移)
  • 逻辑运算符:&&(逻辑与)、||(逻辑或)、!(逻辑非)
  • 关系运算符:==、!=、<、<=、>、>=、instanccof
  • 三目运算符:?:
  • 赋值运算符:=、+=、-=、/*=、/=、%=、&=、|=、^=、<<=、>>=、>>>=

运算符优先级:

位运算的习题:

1.7、输入和输出

// import java.util.Scanner;
Scanner scanner = new Scanner(System.in);

byte b = scanner.nextByte();//输入byte类型数据
short s = scanner.nextShort();//输入short类型数据
int i = scanner.nextInt();//输入int类型数据
long l = scanner.nextLong();//输入long类型数据
float f = scanner.nextFloat();//输入float类型数据
double d = scanner.nextDouble();//输入double类型数据
char c = scanner.next().charAt(0);//输入char类型数据
boolean bool = scanner.nextBoolean();//输入boolean类型数据

System.out.println("b=" + b);//输出byte类型数据
System.out.println("s=" + s);//输出入short类型数据
System.out.println("i=" + i);//输出入int类型数据
System.out.println("l=" + l);//输出long类型数据
System.out.println("f=" + f);//输出float类型数据
System.out.println("d=" + d);//输出double类型数据
System.out.println("c=" + c);//输出char类型数据
System.out.println("bool=" + bool);//输出boolean类型数据

1.8、流程控制

条件语句:

循环语句:

跳转语句:

  • break:结束本次循环。带标签的 break 语句,将跳到与标签匹配的语句块末尾。
  • continue:跳过本次循环。带标签的 continue 语句,将跳到与标签匹配的循环首部。

1.9、数组

  • 一维数组:

  • 格式一:数据类型[] 数组名 = new 数据类型[数组长度];

  • 格式二:数据类型[] 数组名 = new 数据类型[]{元素1,元素2,...};

  • 格式三:数据类型[] 数组名 = {元素1,元素2,...};

  • 二维数组:

  • 格式一:数据类型[][] 数组名 = new 数据类型[行长度][列长度];

  • 格式二:数据类型[][] 数组名 = new 数据类型[][]{{元素1,元素2,...},{元素1,元素2,...},...};

  • 格式三:数据类型[][] 数组名 = {{元素1,元素2,...},{元素1,元素2,...},...};

  • 参数数组:

  • 命令行:public static void main(String[] args)

第二章 面向对象

2.1、类

2.1.1、类的定义

[public] class 类名 {
    // 成员变量(可多个)
    [权限修饰符] {基本数据类型|引用数据类型} 变量名;

    // 成员常量(可多个)
    [权限修饰符] final {基本数据类型|引用数据类型} 变量名;

    // 成员方法(可多个)
    [权限修饰符] 返回值类型 方法名([参数类型 参数名,...]) {
        ...
        [return 返回值;]
    }

    // 构造方法(无参,只一个)
    public 类名() {
        ...
    }

    // 构造方法(有参,可多个)
    public 类名([参数类型 参数名,...]) {
        ...
    }
}

成员变量 和 局部变量的区别?

2.1.2、类的继承

[public] class 父类 { }
[public] class 子类 extends 父类 { }

this 和 super关键字的对比?

继承中变量使用的访问特点?

  1. 子类局部范围找
  2. 子类成员范围找
  3. 父类成员范围找
  4. 如果都没有就报错(不考虑父亲的父亲…)

继承中成员方法的访问特点?

  1. 子类成员范围找
  2. 父类成员范围找
  3. 如果都没有就报错(不考虑父亲的父亲…)

继承中构造方法的访问特点?

  1. 子类中所有的构造方法默认都会访问父类中无参的构造方法,每一个子类构造方法的第一条语句默认都是:super()
  2. 如果父类中没有无参的构造方法,只有带参的构造方法,通过使用 super 关键字去显示的调用父类的带参构造方法。

多态中成员变量的访问特点?

  1. 编译看父类,运行看父类

多态中成员方法的访问特点?

  1. 编译看父类,运行看子类

多态中向上转型和向下转型?

  1. 向上转型:父类引用指向子类对象就是向上转型
  2. 向下转型:子类型 对象名 = (子类型)父类引用;

方法重写 和 方法重载的区别?

  • 方法重写:子类对父类允许访问的方法的实现过程进行重新编写就是方法重写。英文名:Override
  • 方法重载:在一个类里面,方法名字相同而参数列表不同的方法就是方法重载。英文名:Overload

2.1.3、static关键字

由 static 关键字修饰的成员变量、成员常量、成员方法和内部类被称为静态成员变量、静态成员常量、静态成员方法和静态内部类。

  • 修饰成员变量时,将其变为类的静态成员变量,从而实现所有对象对于该变量的共享,可直接使用"类名.变量名"的方式调用。
  • 修饰成员常量时,将其变为类的静态成员常量,从而实现所有对象对于该常量的共享,可直接使用"类名.常量名"的方式调用。
  • 修饰成员方法时,将其变为类的静态成员方法,从而实现所有对象对于该方法的共享,可直接使用"类名.方法名"的方式调用。
  • 修饰内部类时,将其变为类的静态内部类,从而实现所有对象对于该内部类的共享,可直接使用"类名.内部类名"的方式调用。
  • 静态导包用法时,将指定类的方法直接导入到当前类中,从而直接使用"方法名"即可调用指定类的指定方法。
  • 静态代码块用法时,随着类的加载而执行,而且只执行一次,常用于初始化成员变量。

非静态的成员方法 和 静态的成员方法的区别?

2.1.4、final关键字

  • final 关键字修饰基本数据类型时,其数值不能发生改变。
  • final 关键字修饰引用数据类型时,其地址不能发生改变。
  • final 关键字修饰成员方法时,表示该方法不能被重写。
  • final 关键字修饰 class 类时,表示该类不能被继承。

2.1.5、package关键字

概述:包就是文件夹,用来管理类文件的。

语法:package 包名; (多级包名用点分隔)

带包编译:javac –d . 类名.java
例如:javac -d . com.company.demo.HelloWorld.java

带包运行:java 包名+类名
例如:java com.company.demo.HelloWorld

2.1.6、import关键字

概述:使用不同包下的类时,使用的时候要写类的全路径,为了简化操作,Java就提供了导包的功能。

语法:import 包名; (多级包名用点分隔)

2.1.7、权限修饰符

2.2、对象

对象的创建:

通过无参构造创建对象:对象类型 对象名 = new 对象类型();
通过有参构造创建对象:对象类型 对象名 = new 对象类型(参数列表);

对象的使用:

对象名.成员变量;
对象名.成员方法();

对象的比较:

  • ==:用于比较两个对象的地址是否相同。

  • equals():用于比较两个对象的内容是否相同,需要对象重写equals()、hashCode()两个方法。

  • instanceof运算符:用于判断运算符前的对象是否是运算符后的类、抽象类或接口的子类或实现类所创建的对象。

  • 语法:boolean result = 对象 instanceof (类/抽象类/接口);

对象的销毁:

每个对象都有生命周期,当对象生命周期结束时,分配给该对象的内存地址将会被回收,在 Java 中拥有一套完整的垃圾回收机制,用户不必担心废弃的对象占用内存,他会被垃圾回收器自动回收,垃圾回收不保证一定会发生,具体执行时间也不确定,可以手动调用:System.gc();回收。

  • 当对象被置为null时将消亡。
  • 当对象超出作用域时将消亡。

2.3、接口

[public] interface 父接口 { }
[public] class 子实现 implements 父接口 { }
  1. 成员变量:只能是静态常量,默认修饰符:public static final
  2. 成员方法:只能是抽象方法,默认修饰符:public abstract
  3. 构造方法:没有构造方法
  4. 默认方法:public default 返回值类型 方法名(参数列表) { }
  5. 静态方法:public static 返回值类型 方法名(参数列表) { }

如何解决默认方法冲突?

如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样方法,遵循如下规则:

  1. 超类优先。如果超类提供了一个具体方法,同名且有相同参数类型的默认方法会被忽略。
  2. 接口冲突。如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法,必须覆盖这个方法来解决冲突。

2.4、抽象类

[public] abstract 抽象类名 { }
[public] class 实现类 extends 抽象类名 { }
  1. 成员变量:既可以成员变量,也可以成员常量
  2. 成员方法:既可以普通方法,也可以抽象方法
  3. 构造方法:既可以无参构造,也可以有参构造
  4. 注意事项:与abstract不能共存的关键字:final、private、static

2.5、内部类

[public] class Outer {
    [public] class Inner { }
}

内部类的常见分类:

成员内部类:在成员变量位置定义的内部类
	   格式:外部类名.内部类名 对象名 = 外部类对象.内部类对象;
	   注意:Outer.Inner oi = new Outer().new Inner();

局部内部类:在成员方法位置定义的内部类
	   格式:外界是无法直接使用,需要在方法内部创建对象并使用
	   注意:局部内部类可以直接访问外部类的成员,也可以访问方法内的局部变量

匿名内部类:前提是继承一个类或实现接口
	   格式:new 类名 ( ) { 重写方法 } 、 new 接口名 ( ) { 重写方法 }
	   注意:本质上是一个继承了该类或者实现了该接口的子类匿名对象

静态内部类:在成员变量位置定义的内部类
	   格式:使用 static 关键字修饰的成员内部类
	   注意:静态内部类中可以声明静态成员,而成员内部类中不可以声明静态成员,而且静态内部类不可以访问外部类的非静态成员

2.6、枚举类

[public] enum 枚举名 { 枚举项1,枚举项2,枚举项3,... }
  1. 成员变量:既可以成员变量,也可以成员常量
  2. 成员方法:既可以普通方法,也可以抽象方法,但是枚举项必须实现抽象方法
  3. 构造方法:既可以无参构造,也可以有参构造,但是修饰符必须都是 private
  4. 注意事项:枚举类第一行必须是枚举项,多个枚举项之间使用逗号隔开,最后一个枚举项使用分号结束,如果最后一个枚举项后没有其他东西,最后一个分号可以省略,所有枚举类都是Enum的子类,有参构造函数的枚举类的枚举项的用法比较特殊:枚举项("")

枚举类的常用方法:
方法描述示例values()该方法可以将枚举类型成员以数组的形式返回枚举类型名称.values()valueOf(String name)该方法可以实现将普通字符串转换为枚举实例枚举类型名称.valueOf()compareTo()该方法用于比较两个枚举对象在定义时的顺序枚举对象.compareTo()ordinal()该方法用于获取枚举成员的位置索引枚举对象.ordinal()

2.7、注解

//基本注解
public @interface 注解名 {
    成员类型 value() default "默认值; 
}

基本注解可以取啥?

  • @Document注解:表示将此注解包含在 javadoc 中

  • @Inherited注解:表示该注解的继承特性

  • @Retention注解:表示该注解的生命周期

  • RetentionPolicy.SOURCE:源码期间有效

  • RetentionPolicy.CLASS:编译期间有效

  • RetentionPolicy.RUNTIME:运行期间有效

  • @Target注解:表示该注解的标注位置

  • ElementType.TYPE:表示用于类、接口、枚举类以及注解上

  • ElementType.FIELD:表示用于成员变量和枚举变量上

  • ElementType.METHOD:表示用于成员方法上

  • ElementType.PARAMETER:表示用于方法参数上

  • ElementType.CONSTRUCTOR:表示用于构造方法上

  • ElementType.LOCAL_VARIABLE:表示用于局部变量上

  • ElementType.ANNOTATION_TYPE:表示用于注解上

  • ElementType.PACKAGE:表示用于包上

  • ElementType.TYPE_PARAMETER:表示用于类上泛型参数上

  • ElementType.TYPE_USE:表示用于任何类型声明上

  • @Repeatable:表示该注解可重复使用

成员类型可以取啥?

  • 八种基本数据类型、String、Class、枚举类、注解类及以上所列类型的数组

平时开发常用注解?

  • @Override:表示当前方法覆盖了父类的方法
  • @Deprecated:表示当前方法已经过时不建议使用
  • @SuppressWarnings:表示关闭一些警告信息,抑制信息如下:

2.8、泛型

泛型类:

[public] class 类名<T> { }

泛型方法:

权限修饰符 <T> 返回值类型 方法名(T t) {
    return 返回值类型;
}

泛型接口:

[public] interface 类名<T> { }

泛型通配符:

类型通配符:<?>
    List<?>:表示元素类型未知的List,它的元素可以匹配任何的类型
    
类型通配符上限:<? extends 类型>
    List<? extends Number>:它表示的类型是Number或者其子类型
    
类型通配符下限:<? super 类型>
    List<? super Number>:它表示的类型是Number或者其父类型

泛型局限性:

  1. 不能用八种基本类型实例化类型参数,而是用他们所对应的包装类
  2. 无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型( raw type ),原始类型的名字就是删去类型参数后的泛型类型名。擦除( erased ) 类型变量,并替换为限定类型,无限定的变量用Object替换
  3. 运行时类型查询只适用于原始类型,试图查询一个对象是否属于某个泛型类型时,倘若使用instanceof会得到一个编译器错误, 如果使用强制类型转换会得到一个警告,正确的做法是调用getClass()方法进行比较原始类型是否一致
  4. 不能创建参数化类型的数组,例如:Pair<String>[] table = new Pair<String>[10] ;这是错误的,可以声明通配类型的数组, 然后进行类型转换:Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];
  5. 不能实例化类型变量,不能使用像new T(...)newT[...]T.class这样的表达式
  6. 不能在静态域或方法中引用类型变量
  7. 不能抛出或捕获泛型类的实例

2.9、注释

单行注释:// 注释内容
多行注释:/* 注释内容 */
文档注释:/** 注释内容 */

文档注释加在类上的标签:

  • @author:指定类的作者
  • @version:指定类的版本
  • @since:从以下版本开始
  • @see:另请参阅一个类

文档注释加在方法上的标签:

  • @param:说明一个方法的参数
  • @return:说明一个方法的返回值
  • @throws:说明一个方法的异常

第三章 基础类

3.1、Object

描述:该类是所有类的最终根类。
方法描述public final native Class<?> getClass();返回对象的运行时类public boolean equals(Object obj)判断对象是否相等public native int hashCode();返回对象的哈希值protected native Object clone()返回对象的浅拷贝public String toString()返回对象的字符串表示protected void finalize()当对象即将消亡时运行

如何实现对象的深拷贝?

public class Person implements Serializable {
    private static final long serialVersionUID = -7466779555039906117L;
    private String name;
    private Integer age;
    private Person friend;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Person getFriend() {
        return friend;
    }

    public void setFriend(Person friend) {
        this.friend = friend;
    }

    public Person deepClone() {
        ByteArrayOutputStream bos = null;
        ObjectOutputStream oos = null;
        ByteArrayInputStream bis = null;
        ObjectInputStream ois = null;

        try {
            //序列化
            bos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(bos);
            oos.writeObject(this);

            //反序列化
            bis = new ByteArrayInputStream(bos.toByteArray());
            ois = new ObjectInputStream(bis);
            return (Person) ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                ois.close();
                bis.close();
                oos.close();
                bos.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

3.2、Class

描述:在Java中,每个class都有一个相应的Class对象。也就是说,当我们编写一个类,编译完成后,在生成的.class文件中,就会产生一个Class对象,用于表示这个类的类型信息。

如何获取一个classClass实例?有三个方法:

方法一:直接通过一个class的静态变量class获取:

Class cls = String.class;

方法二:如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取:

String s = "Hello";
Class cls = s.getClass();

方法三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取:

Class cls = Class.forName("java.lang.String");

因为Class实例在JVM中是唯一的,所以,上述方法获取的Class实例是同一个实例。

  1. instanceof不但可以匹配指定类型,还可以匹配指定类型的子类
  2. ==判断class实例可以精确地判断数据类型,但不能作子类型比较

由于JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息。这种通过Class实例获取class信息的方法称为反射(Reflection)。

反射创建新的实例:
方法描述public T newInstance()创建新的实例

反射获取资源文件:

方法描述cls.getResourceAsStream("aa.txt");获取编译后Class文件同级目录的aa.txtcls.getClassLoader().getResourceAsStream("bb.txt");获取编译后ClassPath目录下的bb.txt

反射获取构造方法:

方法描述public Constructor<?>[] getConstructors()返回一个包含Constructor对象的数组
不包含私有构造,不包括父类声明的public Constructor<T> getConstructor(Class<?>... parameterTypes)返回一个指定的Constructor对象
不包含私有构造,不包括父类声明的public Constructor<?>[] getDeclaredConstructors()返回一个包含Constructor对象的数组
包含私有构造,不包括父类声明的public Constructor<T> getConstructor(Class<?>... parameterTypes)返回一个指定的Constructor对象
包含私有构造,不包括父类声明的

反射获取成员变量:

方法描述public Field[] getFields()返回一个包含Field对象的数组
不包含私有成员变量,包括父类声明的public Field getField(String name)返回一个指定的Field对象
不包含私有成员变量,包括父类声明的public Field[] getDeclaredFields()返回一个包含Field对象的数组
包含私有成员变量,不包括父类声明的public Field getDeclaredField(String name)返回一个指定的Field对象
包含私有成员变量,不包括父类声明的

反射获取成员方法:

方法描述public Method[] getMethods()返回一个包含Method对象的数组
不包含私有成员方法,包括父类声明的public Method getMethod(String name, Class<?>... parameterTypes)返回一个指定的Method对象
不包含私有成员方法,包括父类声明的public Method[] getDeclaredMethods()返回一个包含Method对象的数组
包含私有成员方法,不包括父类声明的public Method getDeclaredMethod(String name, Class<?>... parameterTypes)返回一个指定的Method对象
包含私有成员方法,不包括父类声明的

反射获取类上注解:

方法描述public Annotation[] getAnnotations()返回一个包含Annotation对象的数组
包括继承的注解,继承需标明@Inheritedpublic <A extends Annotation> A getAnnotation(Class<A> annotationClass)返回一个指定的Annotation对象
包括继承的注解,继承需标明@Inheritedpublic <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass)返回一个指定的Annotation对象数组
包括继承的注解,继承需标明@Inheritedpublic Annotation[] getDeclaredAnnotations()返回一个包含Annotation对象的数组
不包括继承的注解public <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass)返回一个指定的Annotation对象
不包括继承的注解public <A extends Annotation> A[] getDeclaredAnnotationsByType(Class<A> annotationClass)返回一个指定的Annotation对象数组
不包括继承的注解

反射获取类上信息:

方法描述public Package getPackage()获取当前类的包对象public String getName()获取当前类的类全名public Class<?>[] getClasses()获取当前类的所有内部类
不包含私有,包含父类声明的public Class<?>[] getDeclaredClasses()获取当前类的所有内部类
包含私有,不包含父类声明的public native Class<? super T> getSuperclass();返回直接继承的父类,不包含泛型参数public Type getGenericSuperclass()返回直接继承的父类,包含泛型参数public Class<?>[] getInterfaces()返回直接实现的接口,不包含泛型参数public Type[] getGenericInterfaces()返回直接实现的接口,包含泛型参数

反射判断数据类型:

方法描述public native boolean isPrimitive();判断是否是基本类型public native boolean isArray();判断是否是数组类型public native boolean isInterface();判断是否是接口类型public boolean isEnum()判断是否是枚举类型public boolean isAnnotation()判断是否是注解类型public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)判断是否有指定注解public native boolean isAssignableFrom(Class<?> cls);判断是否为某类父类

3.3、System

描述:在 System 类提供的设施中,有标准输入、标准输出和错误输出流;对外部定义的属性和环境变量的访问;加载文件和库的方法;还有快速复制数组的一部分的实用方法。
方法描述public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);进行数组拷贝public static native long currentTimeMillis();获取当前时间(毫秒值)public static void exit(int status)退出当前程序public static void gc()运行垃圾回收public static java.util.Map<String,String> getenv()获取系统环境变量public static String getenv(String name)获取指定环境变量public static Properties getProperties()获取当前系统属性public static String getProperty(String key)获取指定系统属性public static String getProperty(String key, String def)获取指定系统属性(可设默认值)public static String clearProperty(String key)清除指定系统属性public static String lineSeparator()返回系统的分隔符

3.4、Math

描述:Math 类包含执行基本数字运算的方法,如基本指数,对数,平方根和三角函数。

两个常量值:

  • public static final double E = 2.7182818284590452354;
  • public static final double PI = 3.14159265358979323846;
    方法描述public static int max(int a, int b)返回两个数中的最大值public static int min(int a, int b)返回两个数中的最小值public static int abs(int a)返回指定数字的绝对值public static double floor(double a)返回小于或等于参数的最大整数public static double ceil(double a)返回大于或等于参数的最小整数public static long round(double a)返回四舍五入后整数值public static double pow(double a, double b)返回a的b次幂public static double log(double a)返回log以e为底的值public static double sqrt(double a)返回a的正平方根public static double random()返回一个[0.0 , 1.0)之间的随机数
    返回指定范围:(int) (Math.random() /* (max - min + 1) + min)``public static double sin(double a)返回正弦值public static double cos(double a)返回余弦值public static double tan(double a)返回正切值public static double asin(double a)返回反正弦值public static double acos(double a)返回反余弦值public static double atan(double a)返回反正切值

3.5、Number

描述:抽象类 Number 是Byte、Short、Integer、Long、Float、Double、BigInteger、BigDecimal等类的父类。
方法描述public byte byteValue()以 byte 形式返回指定的数值public short shortValue()以 short 形式返回指定的数值public abstract int intValue();以 int 形式返回指定的数值public abstract long longValue();以 long形式返回指定的数值public abstract float floatValue();以 float 形式返回指定的数值public abstract double doubleValue();以 double 形式返回指定的数值

3.6、包装类

基本类型包装类型byteByteshortShortintIntegerlongLongfloatFloatdoubleDoublecharCharacterbooleanBoolean

自动装箱:把基本数据类型转换为对应的包装类型
    例如:Integer total = 99; 
    对应:Integer total = Integer.valueOf(99);

自动拆箱:把包装类型转换为对应的基本数据类型
    例如:int totalprim = total;
    对应:int totalprim = total.intValue();

Integer类:

方法描述public static String toBinaryString(int i)将整数转为二进制字符串public static String toOctalString(int i)将整数转为八进制字符串public static String toHexString(int i)将整数转为十六进制字符串public static int parseInt(String s)将十进制数字字符串解析为整数public static int parseInt(String s, int radix)将指定进制的字符串解析为整数public static Integer valueOf(int i)将十进制数字转换为Integer对象public static Integer valueOf(String s)将十进制数字字符串转换为Integer对象public static Integer valueOf(String s, int radix)将指定进制的字符串解析为Integer对象public static int max(int a, int b)返回两个数中的最大值public static int min(int a, int b)返回两个数中的最小值public static long sum(long a, long b)返回两个数的和

Long类:

方法描述public static String toBinaryString(long i)将整数转为二进制字符串public static String toOctalString(long i)将整数转为八进制字符串public static String toHexString(long i)将整数转为十六进制字符串public static long parseLong(String s)将十进制数字字符串解析为整数public static long parseLong(String s, int radix)将指定进制的字符串解析为整数public static Long valueOf(long l)将十进制数字转换为Long对象public static Long valueOf(String s)将十进制数字字符串转换为Long对象public static Long valueOf(String s, int radix)将指定进制的字符串解析为Long对象public static long max(long a, long b)返回两个数中的最大值public static long min(long a, long b)返回两个数中的最小值public static long sum(long a, long b)返回两个数的和

3.7、大数字

BigInteger类:

BigInteger bi1 = new BigInteger("100");
BigInteger bi2 = new BigInteger("50");

// public BigInteger add(BigInteger val):加
System.out.println("add:" + bi1.add(bi2));

// public BigInteger subtract(BigInteger val):减
System.out.println("subtract:" + bi1.subtract(bi2));

// public BigInteger multiply(BigInteger val):乘
System.out.println("multiply:" + bi1.multiply(bi2));

// public BigInteger divide(BigInteger val):除
System.out.println("divide:" + bi1.divide(bi2));

BigDecimal类:

BigDecimal bd1 = new BigDecimal("0.09");
BigDecimal bd2 = new BigDecimal("0.01");
// public BigDecimal add(BigDecimal val):加
System.out.println("add:" + bd1.add(bd2));

BigDecimal bd3 = new BigDecimal("1.0");
BigDecimal bd4 = new BigDecimal("0.32");
// public BigDecimal subtract(BigDecimal val):减
System.out.println("subtract:" + bd3.subtract(bd4));

BigDecimal bd5 = new BigDecimal("1.015");
BigDecimal bd6 = new BigDecimal("100");
// public BigDecimal multiply(BigDecimal val):乘
System.out.println("multiply:" + bd5.multiply(bd6));

BigDecimal bd7 = new BigDecimal("1.301");
BigDecimal bd8 = new BigDecimal("100");
// public BigDecimal divide(BigDecimal val):除
System.out.println("divide:" + bd7.divide(bd8));

3.8、String

描述:String 类代表字符串。

构造方法:
方法描述public String()初始化构造一个空白字符串public String(char[] value)通过字符数组初始化字符串public String(byte[] bytes)通过使用平台的默认字符集解码指定的字节数组来构造新的Stringpublic String(byte[] bytes, Charset charset)通过使用自定义的字符集解码指定的字节数组来构造新的Stringpublic String(StringBuffer buffer)字符串缓冲区的内容被复制,重新分配一个新的字符串public String(StringBuilder builder)字符串构建器的内容被复制,重新分配一个新的字符串

成员方法:

方法描述public int length()返回此字符串的长度public int indexOf(int ch)返回指定字符第一次出现的字符串内的索引public int lastIndexOf(int ch)返回指定字符的最后一次出现的字符串中的索引public int indexOf(String str)返回指定子字符串第一次出现的字符串内的索引public int lastIndexOf(String str)返回指定子字符串最后一次出现的字符串中的索引public char charAt(int index)返回char指定索引处的值public char[] toCharArray()将此字符串转换为新的字符数组public static String valueOf(int i)返回int参数的字符串int形式public String trim()返回一个删除前后空格的字符串public String toString()返回当前字符串public String concat(String str)将指定的字符串连接到该字符串的末尾public String toLowerCase()返回一个转换为小写的字符串public String toUpperCase()返回一个转换为大写的字符串public String substring(int beginIndex)返回一个以指定索引处的字符开头到该字符串的末尾的子字符串public String substring(int beginIndex, int endIndex)返回一个以指定索引处的字符开头到指定索引处的字符结尾的子字符串public String replace(char oldChar, char newChar)返回一个替换所有旧字符后的新字符串public String replaceAll(String regex, String replacement)返回一个替换所有符合正则表达式字符串后的新字符串public String[] split(String regex)根据正则表达式拆分字符串并返回拆分后的字符串数组public boolean startsWith(String prefix)判断此字符串是否以指定的前缀开头public boolean endsWith(String suffix)判断此字符串是否以指定的后缀结尾public boolean matches(String regex)根据正则表达式判断当前字符串是否符合要求public boolean isEmpty()判断当前字符串是否为空字符串public boolean contains(CharSequence s)判断当前字符串是否包含s字符public boolean equals(Object anObject)判断当前字符串是否和anObject相等

注意事项:

StringBufferStringBuilder内容可以变内容可以变线程安全线程不安全多线程操作字符串单线程操作字符串StringBuffer sb = new StringBuffer();
sb.append(“Hello”);
sb.append(" ");
sb.append(“World”);
System.out.println(sb.toString());StringBuilder sb = new StringBuilder();
sb.append(“Hello”);
sb.append(" ");
sb.append(“World”);
System.out.println(sb.toString());

3.9、Throwable

异常体系:

常见异常:

处理异常:

解决方案一:用try-catch常规的方式处理异常
try {
    可能出现异常的代码;
} catch(异常类名 变量名) {
    异常的处理代码;
} finally {
    释放资源的代码;
}

解决方案二:用try-catch新特性方式处理异常
try (定义流对象;定义流对象....) {
    可能出现异常的代码;
} catch(异常类名 变量名) {
    异常的处理代码;
}

解决方案三:用throws方式抛出异常
public void 方法名() throws 异常类名 {
    
}

解决方案四:用throw方式抛出异常
public void 方法名() {
    //...
    //抛出异常
    throw new 异常类名(异常信息);
    //...
}

注意事项:

3.10、Lambda

Lambda的简介:

Lambda 表达式是 JDK8 的一个新特性,可以取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。虽然使用 Lambda 表达式可以对某些接口进行简单的实现,但并不是所有的接口都可以使用 Lambda 表达式来实现。Lambda 表达式规定接口中只能有一个需要被实现的方法,不是规定接口中只能有一个方法,被 default 修饰的方法会有默认实现,不是必须被实现的方法,所以不影响 Lambda 表达式的使用。

Lambda的标准语法:

标准格式:

(参数类型 参数名称) -> {
    代码体;
}

无参无返回值抽象方法练习:

interface MyInter {
    void show();
}

public class Main {
    public static void main(String[] args) {
        myInterShow(() -> {
            System.out.println("Hello,World");
        });
    }

    public static void myInterShow(MyInter mi) {
        mi.show();
    }
}

有参无返回值抽象方法练习:

interface MyInter {
    void show(String s);
}

public class Main {
    public static void main(String[] args) {
        myInterShow((String s) -> {
            System.out.println(s);
        });
    }

    public static void myInterShow(MyInter mi) {
        mi.show("Hello,World");
    }
}

有参有返回值抽象方法练习:

interface MyInter {
    int show(int x, int y);
}

public class Main {
    public static void main(String[] args) {
        myInterShow((int x, int y) -> {
            return x + y;
        });
    }

    public static void myInterShow(MyInter mi) {
        int show = mi.show(10, 20);
        System.out.println(show);
    }
}

Lambda的简化语法:

  1. 小括号内的参数类型可以省略,但是有多个参数的情况下,不能只省略一个
  2. 如果小括号内有且仅有一个参数,则小括号可以省略
  3. 如果大括号内有且仅有一个语句,则可以同时省略大括号、return关键字及语句分号

Lambda表达式和匿名内部类的区别:

  1. 所需类型不同
  • 匿名内部类:可以是接口,也可以是抽象类,还可以是具体类

  • Lambda表达式:只能是接口

  • 使用限制不同

  • 如果接口中有且仅有一个抽象方法,可以使用Lambda表达式,也可以使用匿名内部类

  • 如果接口中多于一个抽象方法,只能使用匿名内部类,而不能使用Lambda表达式

  • 实现原理不同

  • 匿名内部类:编译之后,产生一个单独的.class字节码文件

  • Lambda表达式:编译之后,没有一个单独的.class字节码文件,对应的字节码会在运行的时候动态生成

第四章 工具类

4.1、函数式接口

4.1.1、内置接口

什么是函数式接口呢?

有且仅有一个抽象方法的接口就是函数式接口,并且可以通过在类上标注@FunctionalInterface注解进行检测。

四大内置函数式接口:

Supplier接口:供给型接口,无参有返回值的接口

public class Main {
    public static void main(String[] args) {
        printMax(() -> Math.max(10, 20));
    }

    public static void printMax(Supplier<Integer> supplier) {
        Integer result = supplier.get();
        System.out.println(result);
    }
}

Consumer接口:消费型接口,有参无返回值的接口

public class Main {
    public static void main(String[] args) {
        printStr(s -> System.out.println(s.toUpperCase()));
    }

    public static void printStr(Consumer<String> consumer) {
        consumer.accept("Hello,World");
    }
}
public class Main {
    public static void main(String[] args) {
        printStr(s -> System.out.println(s.toUpperCase()), s -> System.out.println(s.toLowerCase()));
    }

    public static void printStr(Consumer<String> consumer1, Consumer<String> consumer2) {
        consumer1.andThen(consumer2).accept("Hello,World");
    }
}

Function接口:函数型接口,有参有返回值的接口,第一个参数为输入值,第二个参数为输出值

public class Main {
    public static void main(String[] args) {
        printNum(s -> Integer.parseInt(s));
    }

    public static void printNum(Function<String, Integer> function) {
        Integer result = function.apply("10");
        System.out.println(result);
    }
}
public class Main {
    public static void main(String[] args) {
        Function<Integer, Integer> times2 = i -> i * 2;
        Function<Integer, Integer> squared = i -> i * i;

        //32,先执行apply(4),再4×4,然后16×2
        System.out.println(times2.compose(squared).apply(4));

        //64,先执行apply(4),再4×2,然后8×8
        System.out.println(times2.andThen(squared).apply(4));
    }
}

BiFunction接口:函数型接口,有参有返回值的接口,第一个参数为输入值,第二个参数为输入值,第三个参数为输出值

public class Main {
    public static void main(String[] args) {
        printSum((num1, num2) -> num1 + num2);
    }

    public static void printSum(BiFunction<Integer, Integer, Integer> biFunction) {
        Integer result = biFunction.apply(10, 20);
        System.out.println(result);
    }
}
public class Main {
    public static void main(String[] args) {
        printSumAndMul((num1, num2) -> num1 + num2, num3 -> num3 * num3);
    }

    public static void printSumAndMul(BiFunction<Integer, Integer, Integer> biFunction1, Function<Integer, Integer> function2) {
        Integer result = biFunction1.andThen(function2).apply(10, 20);
        System.out.println(result);
    }
}

Predicate接口:断定式接口,用于条件判断的场景

public class Main {
    public static void main(String[] args) {
        printResult(s -> s.contains("H"));
    }

    public static void printResult(Predicate<String> predicate) {
        boolean result = predicate.test("Hello,World");
        System.out.println(result);
    }
}
public class Main {
    public static void main(String[] args) {
        printAnd(s -> s.contains("H"), s -> s.contains("W"));
        printOr(s -> s.contains("H"), s -> s.contains("W"));
        printNegate(s -> s.contains("H"));
    }

    public static void printAnd(Predicate<String> predicate1, Predicate<String> predicate2) {
        boolean result = predicate1.and(predicate2).test("Hello,World");
        System.out.println(result);
    }

    public static void printOr(Predicate<String> predicate1, Predicate<String> predicate2) {
        boolean result = predicate1.or(predicate2).test("Hello,World");
        System.out.println(result);
    }

    public static void printNegate(Predicate<String> predicate) {
        boolean result = predicate.negate().test("Hello,World");
        System.out.println(result);
    }
}

4.1.2、方法引用

符号表示:::

符号描述:双冒号为方法引用运算符,而他所在的表达式被称为方法引用。

应用场景:如果Lambda表达式所要实现的方案,已经有其他方法存在相同的方案,那么则可以使用方法引用。

常见形式:

对象::方法名

Date date = new Date();
Supplier<Long> supplier = date::getTime;
Long time = supplier.get();
System.out.println(time);

类名::静态方法

Supplier<Long> supplier = System::currentTimeMillis;
Long time = supplier.get();
System.out.println(time);

类名::普通方法

Function<String, Integer> function = String::length;
Integer length = function.apply("Hello");
System.out.println(length);

类名::new 调用对象的构造器

Function<String, String> function = String::new;
String result = function.apply("Hello");
System.out.println(result);

数据类型[]::new 调用数组的构造器

Function<Integer, int[]> function = int[]::new;
int[] result = function.apply(10);
System.out.println(Arrays.toString(result));

4.2、时间工具类

4.2.1、LocalDate

描述:LocalDate类获取日期信息。例如:2021-07-14

LocalDate nowTime = LocalDate.now();
System.out.println(nowTime);//获取日期
System.out.println(nowDate.getYear());//获取当前日期的年份
System.out.println(nowDate.getMonth());//获取当前日期的月份对象
System.out.println(nowDate.getMonthValue());//获取当前日期的月份
System.out.println(nowDate.getDayOfWeek());//表示该对象表示的日期是本周星期几
System.out.println(nowDate.getDayOfMonth());//表示该对象表示的日期是本月第几天
System.out.println(nowDate.getDayOfYear());//表示该对象表示的日期是本年第几天

System.out.println(nowDate.withYear(1997));//修改当前对象的年份
System.out.println(nowDate.withMonth(12));//修改当前对象的月份
System.out.println(nowDate.withDayOfMonth(5));//修改当前对象在本月的日期
System.out.println(nowDate.withDayOfYear(30));//修改当前对象在本年的日期

System.out.println(nowDate.minusYears(1));//一年前
System.out.println(nowDate.minusMonths(1));//一月前
System.out.println(nowDate.minusWeeks(1));//一周前
System.out.println(nowDate.minusDays(1));//一天前

System.out.println(nowDate.plusYears(1));//一年后
System.out.println(nowDate.plusMonths(1));//一月后
System.out.println(nowDate.plusWeeks(1));//一周后
System.out.println(nowDate.plusDays(1));//一天后

LocalDate other = LocalDate.now().minusDays(1);//一天前
System.out.println(nowDate.isBefore(other));//比较当前对象日期是否在other对象日期之前
System.out.println(nowDate.isAfter(other));//比较当前对象日期是否在other对象日期之后
System.out.println(nowDate.isEqual(other));//比较当前对象日期是否与other对象日期相等
System.out.println(nowDate.compareTo(other));//比较当前对象日期与other对象日期在时间上的大小,为正,则nowDate晚

System.out.println(LocalDate.of(1991, 11, 11));//直接传入对应的年月日
System.out.println(LocalDate.of(1991, Month.NOVEMBER, 11));//相对上面只是把月换成了枚举
LocalDate birDay = LocalDate.of(1991, 11, 11);
System.out.println(LocalDate.ofYearDay(1991, birDay.getDayOfYear()));//第一个参数为年,第二个参数为当年的第多少天
System.out.println(LocalDate.ofEpochDay(birDay.toEpochDay()));//参数为距离1970-01-01的天数

System.out.println(LocalDate.parse("1991-11-11"));
System.out.println(LocalDate.parse("19911111", DateTimeFormatter.ofPattern("yyyyMMdd")));

4.2.2、LocalTime

描述:LocalTime类获取时间信息。例如:16:54:14.267

LocalTime nowTime = LocalTime.now();
System.out.println(nowTime);//获取时间
System.out.println(nowTime.getHour());//获取时
System.out.println(nowTime.getMinute());//获取分
System.out.println(nowTime.getSecond());//获取秒
System.out.println(nowTime.getNano());//获取纳秒

System.out.println(nowTime.withHour(19));//修改时
System.out.println(nowTime.withMinute(18));//修改分
System.out.println(nowTime.withSecond(17));//修改秒
System.out.println(nowTime.withNano(16));//修改纳秒

System.out.println(nowTime.minusHours(1));//一小时前
System.out.println(nowTime.minusMinutes(1));//一分钟前
System.out.println(nowTime.minusSeconds(1));//一秒钟前
System.out.println(nowTime.minusNanos(1));//一纳秒前

System.out.println(nowTime.plusHours(1));//一小时后
System.out.println(nowTime.plusMinutes(1));//一分钟后
System.out.println(nowTime.plusSeconds(1));//一秒钟后
System.out.println(nowTime.plusNanos(1));//一纳秒后

LocalTime other = LocalTime.now().minusHours(1);//一小时前
System.out.println(nowTime.isBefore(other));//比较当前时间是否在other对象时间之前
System.out.println(nowTime.isAfter(other));//比较当前对象时间是否在other对象时间之后
System.out.println(nowTime.compareTo(other));//比较当前对象时间与other对象时间在时间上的大小,为正,则nowTime晚

System.out.println(LocalTime.of(8, 20));//时分
System.out.println(LocalTime.of(8, 20, 30));//时分秒
System.out.println(LocalTime.of(8, 20, 30, 150));//时分秒纳秒
LocalTime mTime = LocalTime.of(8, 20, 30, 150);
System.out.println(LocalTime.ofSecondOfDay(mTime.toSecondOfDay()));//参数为距离当天零时的秒数
System.out.println(LocalTime.ofNanoOfDay(mTime.toNanoOfDay()));//参数为距离当天零时的纳秒数

System.out.println(LocalTime.parse("08:20:30"));
System.out.println(LocalTime.parse("082030", DateTimeFormatter.ofPattern("HHmmss")));

4.2.3、LocalDateTime

描述:LocalDateTime类获取日期时间信息。例如:2021-07-14T17:08:23.894

LocalDateTime nowDateTime = LocalDateTime.now();
System.out.println(nowDateTime);
System.out.println(nowDateTime.getYear());//获取年
System.out.println(nowDateTime.getMonthValue());//获取月
System.out.println(nowDateTime.getDayOfMonth());//获取日
System.out.println(nowDateTime.getHour());//获取时
System.out.println(nowDateTime.getMinute());//获取分
System.out.println(nowDateTime.getSecond());//获取秒
System.out.println(nowDateTime.getNano());//获取纳秒

System.out.println(nowDateTime.withYear(1997));//修改年
System.out.println(nowDateTime.withMonth(12));//修改月
System.out.println(nowDateTime.withDayOfMonth(5));//修改日
System.out.println(nowDateTime.withHour(19));//修改时
System.out.println(nowDateTime.withMinute(18));//修改分
System.out.println(nowDateTime.withSecond(17));//修改秒
System.out.println(nowDateTime.withNano(16));//修改纳秒

System.out.println(nowDateTime.minusYears(1));//一天前
System.out.println(nowDateTime.minusMonths(1));//一月前
System.out.println(nowDateTime.minusDays(1));//一天前
System.out.println(nowDateTime.minusHours(1));//一小时前
System.out.println(nowDateTime.minusMinutes(1));//一分钟前
System.out.println(nowDateTime.minusSeconds(1));//一秒钟前
System.out.println(nowDateTime.minusNanos(1));//一纳秒前

System.out.println(nowDateTime.plusYears(1));//一天后
System.out.println(nowDateTime.plusMonths(1));//一月后
System.out.println(nowDateTime.plusDays(1));//一天后
System.out.println(nowDateTime.plusHours(1));//一小时后
System.out.println(nowDateTime.plusMinutes(1));//一分钟后
System.out.println(nowDateTime.plusSeconds(1));//一秒钟后
System.out.println(nowDateTime.plusNanos(1));//一纳秒后

LocalDateTime other = LocalDateTime.now().minusHours(1);//一小时前
System.out.println(nowDateTime.isBefore(other));//比较当前时间是否在other对象时间之前
System.out.println(nowDateTime.isAfter(other));//比较当前对象时间是否在other对象时间之后
System.out.println(nowDateTime.isEqual(other));//比较当前对象时间是否与other对象时间相等
System.out.println(nowDateTime.compareTo(other));//比较当前对象时间与other对象时间在时间上的大小,为正,则nowDateTime晚

LocalDate birDay = LocalDate.of(1991, 11, 11);
LocalTime mTime = LocalTime.of(8, 20, 30, 150);
System.out.println(LocalDateTime.of(birDay, mTime));//参数为LocalDate和LocalTime
System.out.println(LocalDateTime.of(1991, 11, 11, 8, 20));
System.out.println(LocalDateTime.of(1991, Month.NOVEMBER, 11, 8, 20));
System.out.println(LocalDateTime.of(1991, 11, 11, 8, 20, 30));
System.out.println(LocalDateTime.of(1991, Month.NOVEMBER, 11, 8, 20, 30));
System.out.println(LocalDateTime.of(1991, 11, 11, 8, 20, 30, 150));
System.out.println(LocalDateTime.of(1991, Month.NOVEMBER, 11, 8, 20, 30, 150));

System.out.println(LocalDateTime.parse("1991-11-11T08:20:30"));
System.out.println(LocalDateTime.parse("1991-11-11 08:20:30", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

4.2.4、Instant

描述:Instant类由一个静态的工厂方法now()可以返回当前时间戳。

Instant instant = Instant.now();
System.out.println("当前时间戳是:" + instant);
Date date = Date.from(instant);
System.out.println("当前时间戳是:" + date);
instant = date.toInstant();
System.out.println("当前时间戳是:" + instant);

4.2.5、Period

描述:使用Period类计算日期时间差。

LocalDate today = LocalDate.now();
System.out.println(today);
LocalDate birthDate = LocalDate.of(1997, 12, 05);
System.out.println(birthDate);

Period period = Period.between(birthDate, today);//第二个参数减第一个参数
System.out.printf("年龄 : %d 年 %d 月 %d 日", period.getYears(), period.getMonths(), period.getDays());

4.2.6、Duration

描述:使用Duration类计算日期时间差。

LocalDateTime today = LocalDateTime.now();
System.out.println(today);
LocalDateTime birthDate = LocalDateTime.of(1997, 12, 5, 20, 10, 5);
System.out.println(birthDate);

Duration duration = Duration.between(birthDate, today);//第二个参数减第一个参数
System.out.println(duration.toDays());//两个时间差的天数
System.out.println(duration.toHours());//两个时间差的小时数
System.out.println(duration.toMinutes());//两个时间差的分钟数
System.out.println(duration.toMillis());//两个时间差的毫秒数
System.out.println(duration.toNanos());//两个时间差的纳秒数

4.2.7、ChronoUnit

描述:ChronoUnit类可用于在单个时间单位内测量一段时间。

LocalDateTime today = LocalDateTime.now();
System.out.println(today);
LocalDateTime birthDate = LocalDateTime.of(1997, 12, 5, 20, 10, 5);
System.out.println(birthDate);

System.out.println("相差的年数:" + ChronoUnit.YEARS.between(birthDate, today));
System.out.println("相差的月数:" + ChronoUnit.MONTHS.between(birthDate, today));
System.out.println("相差的周数:" + ChronoUnit.WEEKS.between(birthDate, today));
System.out.println("相差的天数:" + ChronoUnit.DAYS.between(birthDate, today));
System.out.println("相差的时数:" + ChronoUnit.HOURS.between(birthDate, today));
System.out.println("相差的分数:" + ChronoUnit.MINUTES.between(birthDate, today));
System.out.println("相差的秒数:" + ChronoUnit.SECONDS.between(birthDate, today));
System.out.println("相差的毫秒数:" + ChronoUnit.MILLIS.between(birthDate, today));
System.out.println("相差的微秒数:" + ChronoUnit.MICROS.between(birthDate, today));
System.out.println("相差的纳秒数:" + ChronoUnit.NANOS.between(birthDate, today));

System.out.println("相差的半天数:" + ChronoUnit.HALF_DAYS.between(birthDate, today));
System.out.println("相差的十年数:" + ChronoUnit.DECADES.between(birthDate, today));
System.out.println("相差的百年数:" + ChronoUnit.CENTURIES.between(birthDate, today));
System.out.println("相差的千年数:" + ChronoUnit.MILLENNIA.between(birthDate, today));
System.out.println("相差的纪元数:" + ChronoUnit.ERAS.between(birthDate, today));

4.2.8、时间格式化

字符串常用占位符:

y	四位数年份
M	月
d	日
H	时(24小时制)
h	时(12小时制)
m	分
s	秒
S	毫秒

线程安全的格式化:

将时间转为字符串:

LocalDateTime ldt = LocalDateTime.now();
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String s = dtf.format(ldt);

将字符串转为时间:

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime ldt = LocalDateTime.parse("2021-07-14 01:02:03", dtf);

线程不安全的格式化:

将时间转为字符串:

Date d = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String s = sdf.format(d);

将字符串转为时间:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d = sdf.parse("2021-07-14 01:02:03");

4.3、原子工具类

4.3.1、原子整数

AtomicBoolean类:

AtomicBoolean atomicBoolean = new AtomicBoolean(false);

// 获取当前 atomicBoolean 的值
System.out.println(atomicBoolean.get());

// 设置当前 atomicBoolean 的值
atomicBoolean.set(true);
System.out.println(atomicBoolean.get());

// 获取并设置 getAndSet 的值
System.out.println("获取并设置结果:" + atomicBoolean.getAndSet(false));
System.out.println(atomicBoolean.get());

// 比较并设置 atomicBoolean 的值,如果期望值不等于传入的第一个参数,则比较失败,返回false
System.out.println("比较并设置结果:" + atomicBoolean.compareAndSet(false, true));
System.out.println(atomicBoolean.get());
false
true
获取并设置结果:true
false
比较并设置结果:true
true

AtomicInteger类AtomicLong类

以 AtomicInteger 为例:

AtomicInteger atomicInteger = new AtomicInteger(0);

// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(atomicInteger.getAndIncrement());

// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(atomicInteger.incrementAndGet());

// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(atomicInteger.decrementAndGet());

// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(atomicInteger.getAndDecrement());

// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(atomicInteger.getAndAdd(5));

// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(atomicInteger.addAndGet(-5));

// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(atomicInteger.getAndUpdate(p -> p - 2));

// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(atomicInteger.updateAndGet(p -> p + 2));

// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(atomicInteger.getAndAccumulate(10, (p, x) -> p + x));

// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// updateAndGet 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// accumulateAndGet 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(atomicInteger.accumulateAndGet(-10, (p, x) -> p + x));

4.3.2、原子引用

有些时候,我们不一定会使用基础类型作为共享变量,也可能会使用对象类型作为共享变量,如何确保在多线程下的线程安全呢?

AtomicReference类:

class BigDecimalAccount {
    private AtomicReference<BigDecimal> balance;

    // 初始余额
    public BigDecimalAccount(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

    // 获取余额
    public BigDecimal getBalance() {
        return balance.get();
    }

    // 增加余额
    public void increase(BigDecimal money) {
        while (true) {
            BigDecimal prev = balance.get();
            BigDecimal next = prev.add(money);
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }

    // 减少余额
    public void decrease(BigDecimal money) {
        while (true) {
            BigDecimal prev = balance.get();
            BigDecimal next = prev.subtract(money);
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        BigDecimalAccount account = new BigDecimalAccount(new BigDecimal(1000.00));
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(() -> { account.decrease(new BigDecimal(100.00)); }));
        }
        for (int i = 0; i < 10; i++) {
            threads.get(i).start();
        }
        for (int i = 0; i < 10; i++) {
            threads.get(i).join();
        }
        System.out.println("money : " + account.getBalance());
    }
}
money : 0

compareAndSet方法,首先,先比较传递过来的参数是否是期望的值,如果是,才会修改,如果不是,则修改失败。

那有没有有一种可能,在 A 线程第二次修改的时候,虽然,他的期望值是 10,但是这个 10,是被 B 线程修改的,他以为别人没有动过,然后执行更改操作,其实中间已经被更改过了,这就是ABA问题。

AtomicInteger atomicInteger = new AtomicInteger(10);

// A 线程修改预期值10为 100
atomicInteger.compareAndSet(10,100);

// B 线程修改预期值100 为 10
atomicInteger.compareAndSet(100,10);

// A 线程修改预期值10 为 0
atomicInteger.compareAndSet(10,0);

为了解决这个问题,我们只需要在每一次的修改操作上加一个版本号,这样即使中间被修改过,也能知道,JDK就提供了一种带版本号的原子引用对象。

AtomicStampedReference类:

AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(10, 0);

// A 线程修改预期值10为 100
Integer prev = asr.getReference();//10
int stamp = asr.getStamp();//0
asr.compareAndSet(prev, 100, stamp, stamp + 1);
System.out.println(asr.getStamp());//1

// B 线程修改预期值100 为 10
prev = asr.getReference();//100
stamp = asr.getStamp();//1
asr.compareAndSet(prev, 10, stamp, stamp + 1);
System.out.println(asr.getStamp());//2

// A 线程修改预期值10 为 0
prev = asr.getReference();//10
stamp = asr.getStamp();//2
asr.compareAndSet(prev, 0, stamp, stamp + 1);
System.out.println(asr.getStamp());//3

AtomicStampedReference可以给原子引用加上版本号,追踪原子引用的变化过程: A -> B -> A,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了
AtomicMarkableReference。

AtomicMarkableReference类:

class GarbageBag {
    String desc;

    public GarbageBag(String desc) {
        this.desc = desc;
    }
}

public class Test {
    public static void main(String[] args) {
        GarbageBag garbageBag = new GarbageBag("垃圾已满");
        AtomicMarkableReference<GarbageBag> amr = new AtomicMarkableReference<>(garbageBag, true);

        // 如果垃圾已满,请及时清理
        GarbageBag prev = amr.getReference();
        System.out.println(amr.compareAndSet(prev, new GarbageBag("垃圾已空"), true, false));
        System.out.println(amr.isMarked());
    }
}
true
false

4.3.3、原子数组

AtomicIntegerArray类AtomicLongArray类AtomicReferenceArray类

以 AtomicIntegerArray 为例:

AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);

for (int i = 0; i < atomicIntegerArray.length(); i++) {
    while (true) {
        int prev = atomicIntegerArray.get(i);
        int next = prev + i;
        if (atomicIntegerArray.compareAndSet(i, prev, next)) {
            break;
        }
    }
}

System.out.println(atomicIntegerArray);
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

4.3.4、字段更新器

AtomicIntegerFieldUpdater类AtomicLongFieldUpdater类AtomicReferenceFieldUpdater类

以 AtomicIntegerFieldUpdater 为例:

class Person {
    volatile String name;
    volatile int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class Test {
    public static void main(String[] args) {
        Person person = new Person("张三", 20);
        System.out.println(person);
        AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(Person.class, "age");

        updater.compareAndSet(person, 20, 18);
        System.out.println(person);
    }
}
Person{name='张三', age=20}
Person{name='张三', age=18}

4.3.5、原子累加器

LongAdder类:

LongAdder longAdder = new LongAdder();

longAdder.add(100L);
longAdder.add(200L);
longAdder.add(300L);
System.out.println(longAdder.sum());

longAdder.increment();
System.out.println(longAdder.sum());

longAdder.decrement();
System.out.println(longAdder.sum());
600
601
600

DoubleAdder类:

DoubleAdder doubleAdder = new DoubleAdder();

doubleAdder.add(100.00D);
doubleAdder.add(200.00D);
doubleAdder.add(300.00D);
System.out.println(doubleAdder.sum());
600.0

4.4、Optional类

Optional类主要解决的问题是臭名昭著的空指针异常(NullPointerException)。

4.4.1、创建对象

方法描述public static<T> Optional<T> empty()返回一个空的Optional实例public static <T> Optional<T> of(T value)返回一个含值Optional实例,value不能为nullpublic static <T> Optional<T> ofNullable(T value)返回一个含值Optional实例,value可以是null

4.4.2、基本使用

基础版:

Optional<String> name = Optional.of("zhangsan");
if (name.isPresent()) {
    String value = name.get();
    System.out.println(value);
}

简化版:

Optional<String> name = Optional.of("zhangsan");
name.ifPresent(v -> System.out.println(v));

4.4.3、高级使用

filter:过滤操作

Optional<String> name = Optional.of("zhangsan");
Optional<String> optional = name.filter(s -> s.startsWith("zhang"));
optional.ifPresent(System.out::println);

map:映射操作

Optional<String> name = Optional.of("zhangsan");
Optional<String> optional = name.map(s -> s.toUpperCase());
optional.ifPresent(System.out::println);

flatMap:展开操作

Optional<String> name = Optional.of("zhangsan");
Optional<String> optional = name.flatMap(s -> Optional.of(s.toUpperCase()));
optional.ifPresent(System.out::println);

4.4.4、其他使用

如果 Optional 中有值则将其返回,否则返回 orElse 方法传入的参数。

String s = Optional.of("zhangsan").orElse("lisi");
System.out.println(s);

如果 Optional 中有值则将其返回,否则返回 orElseGet 方法传入的接口值。

String s = Optional.of("zhangsan").orElseGet(() -> "lisi");
System.out.println(s);

如果 Optional 中有值则将其返回,否则抛出 orElseThrow 方法传入的异常信息。

String s = Optional.of("zhangsan").orElseThrow(() -> new RuntimeException("空值"));
System.out.println(s);

4.5、Arrays类

描述:该类包含用于操作数组的各种方法。
方法描述public static <T> List<T> asList(T... a)描述:返回由指定数组支持的固定大小的列表
举例:List stooges = Arrays.asList(“Larry”, “Moe”, “Curly”);public static String toString(int[] a)描述:返回指定数组的内容的字符串表示形式
举例:
int[] a = { 10, 20, 30, 4, 5, 60, 70, 80 };
String as = Arrays.toString(a);public static void sort(int[] a)描述:按照数字升序顺序排列指定的数组
举例:
int[] a = { 10, 20, 30, 4, 5, 60, 70, 80 };
Arrays.sort(a);public static void sort(int[] a, int fromIndex, int toIndex)描述:按照数字升序顺序对数组的指定范围进行排序
举例:
int[] a = { 10, 20, 30, 4, 5, 60, 70, 80 };
Arrays.sort(a, 2, 7);public static int binarySearch(int[] a, int key)描述:使用二分查找算法在指定的int数组中搜索指定的值
举例:
int[] a = { 10, 20, 30, 4, 5, 60, 70, 80 };
Arrays.sort(a);
int index = Arrays.binarySearch(a, 5);public static int binarySearch(int[] a, int fromIndex, int toIndex, int key)描述:使用二分查找算法在指定的int数组中的指定范围搜索指定的值
举例:
int[] a = { 10, 20, 30, 4, 5, 60, 70, 80 };
Arrays.sort(a);
int index = Arrays.binarySearch(a, 2, 7, 60);

4.6、Objects类

描述:该类包含用于操作对象的各种方法。
方法描述public static boolean isNull(Object obj)判断对象是否为空public static boolean nonNull(Object obj)判断对象是否不空public static <T> T requireNonNull(T obj)检查对象是否不空
如果为空,抛出NullPointerException()public static <T> T requireNonNull(T obj, String message)检查对象是否不空
如果为空:抛出NullPointerException(message)public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier)检查对象是否不空
如果为空:抛出NullPointerException(messageSupplier.get())public static boolean equals(Object a, Object b)比较两个对象是否相等public static boolean deepEquals(Object a, Object b)比较两个对象是否相等public static int hashCode(Object o)获取对象的哈希码public static int hash(Object... values)获取对象的哈希值public static <T> int compare(T a, T b, Comparator<? super T> c)比较两个对象大小public static String toString(Object o)输出对象的字符串形式public static String toString(Object o, String nullDefault)输出对象的字符串形式,可以设置为空时默认值

第五章 集合

5.1、Collection接口

常见方法:
方法描述int size();返回元素数量boolean isEmpty();判断是否为空void clear();清空集合元素boolean add(E e);添加一个元素boolean addAll(Collection<? extends E> c);添加一个集合boolean remove(Object o);删除一个元素boolean removeAll(Collection<?> c);删除一个集合default boolean removeIf(Predicate<? super E> filter)根据条件删除boolean retainAll(Collection<?> c);返回交集部分Object[] toArray();返回一个包含此集合中所有元素的数组。
toArray(new Object[0])的功能与toArray()的相同。<T> T[] toArray(T[] a);返回一个包含此集合中所有元素的数组。
返回的数组的运行时类型是指定数组的运行时类型。

三种遍历:

Collection<String> collection = new ArrayList<>();
collection.add("张三");
collection.add("李四");
collection.add("王五");

//第一种:使用迭代器
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
    String s = iterator.next();
    System.out.println(s);
}

//第二种:使用增强for,其实是迭代器的升级版
for (String s : collection) {
    System.out.println(s);
}

//第三种:使用forEach,其实是增强for的升级版
collection.forEach(s -> {
    System.out.println(s);
});

遍历删除:

Collection<String> collection = new ArrayList<>();
collection.add("aaa");
collection.add("bbb");
collection.add("ccc");
collection.add("ddd");
//使用迭代器删除
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
    String next = iterator.next();
    if (next.equals("aaa")) {
        iterator.remove();
    }
}
collection.forEach(System.out::println);

流式操作:

Stream流是由三部分组成的。数据源,零个或一个或多个中间操作,零个或一个终止操作。

在 Java 8 中,集合接口有两个方法来生成流:

  • stream() − 为集合创建串行流。
  • parallelStream() − 为集合创建并行流。

和以前的Collection操作不同,Stream操作还有两个基础的特征:

  • Pipelining :中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式风格(fluent style)。这样做可以对操作进行优化,比如延迟执行(laziness)和短路( short-circuiting)。
  • 内部迭代 :以前对集合遍历都是通过Iterator或者For-Each的方式,显式的在集合外部进行迭代,这叫做外部迭代。Stream提供了内部迭代的方式,通过访问者模式(Visitor)实现。
    注意:Stream操作并不会存储元素,而是按需计算。

中间操作:

filter:过滤操作

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
collection.stream().filter(s -> s.startsWith("a")).forEach(System.out::println);

distinct:去重操作

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
collection.stream().distinct().forEach(System.out::println);

limit:返回前 n 个元素

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
collection.stream().limit(5).forEach(System.out::println);

skip:跳过前 n 个元素

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
collection.stream().skip(5).forEach(System.out::println);

sorted:对流进行排序

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
collection.stream().sorted().forEach(System.out::println);

map:将每个元素映射为新元素

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
collection.stream().map(s -> s.toUpperCase()).forEach(System.out::println);

flatMap:将每个流转化为一个流

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
collection.stream().flatMap(s -> Stream.of(s.split(""))).forEach(System.out::println);

peek:常用于操作间结果输出

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
collection.stream()
        .map(s -> s.toLowerCase())
        .peek(s -> System.out.println("toLowerCase value: " + s))
        .map(s -> s.toUpperCase())
        .peek(s -> System.out.println("toUpperCase value: " + s))
        .forEach(System.out::println);

max:常用于获取流中最大值

Collection<Integer> collection = Arrays.asList(3, 2, 1, 4, 5, 6);
Optional<Integer> max = collection.stream().max(Comparator.comparingInt(o -> o));
max.ifPresent(System.out::println);

min:常用于获取流中最小值

Collection<Integer> collection = Arrays.asList(3, 2, 1, 4, 5, 6);
Optional<Integer> max = collection.stream().min(Comparator.comparingInt(o -> o));
max.ifPresent(System.out::println);

终止操作:

forEach:用于串行流保持顺序遍历

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
collection.parallelStream().forEach(System.out::println);

forEachOrdered:用于并行流保持顺序遍历

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
collection.parallelStream().forEachOrdered(System.out::println);

anyMatch:检查是否至少匹配一个元素

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
boolean result = collection.parallelStream().anyMatch(s -> s.equals("aaa"));
System.out.println(result);

allMatch:检查是否匹配所有元素

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
boolean result = collection.parallelStream().allMatch(s -> s.equals("aaa"));
System.out.println(result);

noneMatch:检查是否没有匹配所有元素

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
boolean result = collection.parallelStream().noneMatch(s -> s.equals("aaa"));
System.out.println(result);

findAny:将返回当前流中的任意元素

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
Optional<String> any = collection.parallelStream().findAny();
if (any.isPresent()) {
    System.out.println(any.get());
}

findFirst:将返回当前流中的第一元素

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
Optional<String> first = collection.parallelStream().findFirst();
if (first.isPresent()) {
    System.out.println(first.get());
}

count:返回当前流中元素的总数

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
long count = collection.parallelStream().count();
System.out.println(count);

collect:对流中的数据进行收集

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
ArrayList<String> collect1 = collection.parallelStream().collect(Collectors.toCollection(ArrayList<String>::new));
LinkedList<String> collect2 = collection.parallelStream().collect(Collectors.toCollection(LinkedList<String>::new));
List<String> collect3 = collection.parallelStream().collect(Collectors.toList());
Set<String> collect4 = collection.parallelStream().collect(Collectors.toSet());
Map<String, String> collect5 = collection.parallelStream().collect(Collectors.toMap(k -> k, v -> v, (oldVale, newValue) -> newValue));
String string = collection.parallelStream().collect(Collectors.joining());

reduce:对流中的数据进行计算

Collection<String> collection = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
Optional<String> reduce = collection.parallelStream().reduce((acc, item) -> acc + item);
if (reduce.isPresent()) {
    System.out.println(reduce.get());
}
String string = collection.parallelStream().reduce("prefix_", (acc, item) -> acc + item);
System.out.println(string);

5.2、List接口

常见方法:
方法描述void add(int index, E element);在指定索引处添加元素E remove(int index);删除指定位置处的元素E get(int index);获取指定位置处的元素E set(int index, E element);设置指定位置处的元素int indexOf(Object o);从前开始查找,返回该元素第一次出现的索引int lastIndexOf(Object o);从后开始查找,返回该元素第一次出现的索引default void replaceAll(UnaryOperator<E> operator)指定替换方式,替换集合中每个元素为新的值default void sort(Comparator<? super E> c)指定排序方式,更改集合中每个元素排列位置List<E> subList(int fromIndex, int toIndex);根据前闭后开,重新获取集合中的一个子集合

三种遍历:

List<String> list = Arrays.asList("abc", "abd", "abe", "123", "456", "789", "aaa", "bbb", "ccc", "aaa");
ListIterator<String> listIterator = list.listIterator();

//第一种:使用下标遍历
for (int i = 0; i < list.size(); i++) {
    String s = list.get(i);
    System.out.println(i + "=" + s);
}

//第二种:从前向后遍历
while (listIterator.hasNext()) {
    int nextIndex = listIterator.nextIndex();
    String next = listIterator.next();
    System.out.println(nextIndex + "=" + next);
}

//第三种:从后向前遍历
while (listIterator.hasPrevious()) {
    int previousIndex = listIterator.previousIndex();
    String previous = listIterator.previous();
    System.out.println(previousIndex + "=" + previous);
}

遍历删除:

List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
//使用迭代器删除
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
    String next = listIterator.next();
    if (next.equals("bbb")) {
        listIterator.remove();
    }
}
//使用迭代器删除
while (listIterator.hasPrevious()) {
    String previous = listIterator.previous();
    if (previous.equals("ccc")) {
        listIterator.remove();
    }
}
list.forEach(System.out::println);

子类对比:

相同点:元素的存取是有序的,可以重复,可以存取null,元素有下标

不同点:
底层结构线程安全集合特点扩容机制版本Vector数组是查询快、增删慢如果有参构造,满后每次2倍
如果无参构造,默认10,满后每次2倍1.0ArrayList数组否查询快、增删慢如果有参构造,满后每次1.5倍
如果无参构造,默认0,第一次10,满后每次1.5倍1.2LinkedList双向链表否查询慢、增删快无扩容机制1.2

5.3、Set接口

子类对比:

相同点:元素的存取是无序的,不能重复,可以存取null,元素无下标

不同点:
底层结构线程安全集合特点扩容机制版本HashSetHashMap
(数组+单向链表+红黑树)否存取无序同HashMap1.2TreeSetTreeMap
(红黑树)否支持排序无扩容机制1.2LinkedHashSetLinkedHashMap
(数组+双向链表+红黑树)否存取有序同HashMap1.4ConcurrentSkipListSetConcurrentSkipListMap
(跳跃表)是支持排序无扩容机制1.6

5.4、Map接口

常见方法:
方法描述int size();返回元素数量boolean isEmpty();判断是否为空void clear();清空集合元素V put(K key, V value);添加一个键值对void putAll(Map<? extends K, ? extends V> m);添加集合键值对default V putIfAbsent(K key, V value)如果指定键尚未与某个值相关联,
则将其与给定值关联并返回null ,
否则返回当前值V remove(Object key);删除键所指对象default boolean remove(Object key, Object value)删除指定键值对V get(Object key);根据键来获取值default V getOrDefault(Object key, V defaultValue)根据键来获取值,
如果值为空则返回defaultValuedefault V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)根据键来设置值,
如果值为空则置value,否则算新值default V replace(K key, V value)替换指定键值对default boolean replace(K key, V oldValue, V newValue)替换指定键值对default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)函数式替换所有Set<K> keySet();获取所有键集合Collection<V> values();获取所有值集合Set<Map.Entry<K, V>> entrySet();获取键值对集合boolean containsKey(Object key);是否包含指定键boolean containsValue(Object value);是否包含指定值default void forEach(BiConsumer<? super K, ? super V> action)循环遍历键值对default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)如果该键存在,
则使用函数式重新计算新值default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)如果该键存在,且值为null,
则使用函数式重新计算新值default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)如果该键存在,且值不null,
则使用函数式重新计算新值

三种遍历:

Map<Integer, String> map = new HashMap<>();
map.put(1, "张三");
map.put(2, "李四");
map.put(3, "王五");
map.put(4, "赵六");

//第一种遍历:遍历所有键值对
Set<Map.Entry<Integer, String>> entries = map.entrySet();
entries.forEach((Map.Entry<Integer, String> entry) -> {
    System.out.println(entry.getKey() + "=" + entry.getValue());
});

//第二种遍历:遍历所有键集合
Set<Integer> keySets = map.keySet();
keySets.forEach((key) -> {
    System.out.println(key);
});

//第三种遍历:遍历所有值集合
Collection<String> values = map.values();
values.forEach((value) -> {
    System.out.println(value);
});

遍历删除:

Map<Integer, String> map = new HashMap<>();
map.put(1, "张三");
map.put(2, "李四");
map.put(3, "王五");
map.put(4, "赵六");

//第一种删除
Set<Map.Entry<Integer, String>> entries = map.entrySet();
Iterator<Map.Entry<Integer, String>> entryIterator = entries.iterator();
while (entryIterator.hasNext()) {
    Map.Entry<Integer, String> next = entryIterator.next();
    if (next.getKey() == 2) {
        entryIterator.remove();
    }
}

//第二种删除
Set<Integer> keySets = map.keySet();
Iterator<Integer> keySetIterator = keySets.iterator();
while (keySetIterator.hasNext()) {
    Integer next = keySetIterator.next();
    if (next == 3) {
        keySetIterator.remove();
    }
}

子类对比:

相同点:以键值对的形式添加元素,键不能重复,值可以重复

不同点:
底层结构线程安全集合特点扩容机制版本Hashtable数组是键和值不可null
存取无序如果无参构造,加载因子为0.75
默认11,阈值8,大于阈值扩容2倍+11.0HashMap数组+单向链表+红黑树否键和值可以null
存取无序如果无参构造,加载因子为0.75
默认16,阈值12,大于阈值扩容2倍
何时进行树化?链表大小>=8、数组大小>=641.2TreeMap红黑树否键不可null
值可以null
支持排序无扩容机制1.2LinkedHashMapHashMap
(数组+双向链表+红黑树)否键和值可以null
存取有序同HashMap1.4ConcurrentSkipListMap跳跃表是键和值不可null
支持排序无扩容机制1.6

5.5、Collections类

常见方法:
方法描述public static void reverse(List<?> list)反转排列集合元素public static void shuffle(List<?> list)随机排序集合元素public static <T extends Comparable<? super T>> void sort(List<T> list)默认排序集合元素public static void swap(List<?> list, int i, int j)交换指定下标元素public static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)替换集合指定元素的值public static <T> void fill(List<? super T> list, T obj)填充集合元素为指定值public static int frequency(Collection<?> c, Object o)返回指定元素出现次数public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)根据自然顺序,获取集合中的最大值public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll)根据自然顺序,获取集合中的最小值public static <T> List<T> singletonList(T o)创建单例集合列表public static <K,V> Map<K,V> singletonMap(K key, V value)创建单例集合映射

第六章 文件编程

6.1、File类

描述:该类是文件和目录路径名的抽象表示。

构造方法:
方法描述public File(String pathname)通过将给定的路径名字符串转换为抽象路径名来创建新的File实例public File(String parent, String child)从父路径名字符串和子路径名字符串创建新的File实例public File(File parent, String child)从父抽象路径名和子路径名字符串创建新的File实例

创建功能:

方法描述public boolean createNewFile()创建一个由该抽象路径名命名的文件public boolean mkdir()创建由此抽象路径名命名的单级目录public boolean mkdirs()创建由此抽象路径名命名的多级目录

判断功能:

方法描述public boolean isDirectory()测试此抽象路径名表示的File是否为目录public boolean isFile()测试此抽象路径名表示的File是否为文件public boolean exists()测试此抽象路径名表示的File是否存在

获取功能:

方法描述public String getAbsolutePath()返回此抽象路径名的绝对路径名字符串public String getPath()将此抽象路径名转换为路径名字符串public String getName()返回由此抽象路径名表示的文件或目录的名称public String[] list()返回此抽象路径名表示的目录中的文件和目录的名称字符串数组public File[] listFiles()返回此抽象路径名表示的目录中的文件和目录的File对象数组

删除功能:

方法描述public boolean delete()删除由此抽象路径名表示的文件或目录

6.2、Files类

jdk7 引入了 Path 和 Paths 类:

  • Path 用来表示文件路径
  • Paths 是工具类,用来获取 Path 实例
Path source = Paths.get("1.txt"); 					// 相对路径 使用 user.dir 环境变量来定位 1.txt
Path source = Paths.get("d:\\1.txt"); 				// 绝对路径 代表了 d:\1.txt
Path source = Paths.get("d:/1.txt"); 				// 绝对路径 代表了 d:\1.txt
Path projects = Paths.get("d:\\data", "projects"); 	// 绝对路径 代表了 d:\data\projects
  • . 代表了当前路径
  • .. 代表了上一级路径

例如:

d:
	|- data
		|- projects
			|- a
			|- b

代码:

Path path = Paths.get("d:\\data\\projects\\a\\..\\b");
System.out.println(path);				// 原来的路径
System.out.println(path.normalize()); 	// 正常化路径

输出:

d:\data\projects\a\..\b
d:\data\projects\b

检查是否存在:

Path path = Paths.get("data.txt");
System.out.println(Files.exists(path));

创建一级目录:

Path path = Paths.get("helloword");
Files.createDirectory(path);
  • 如果该目录已经存在了,会抛异常 FileAlreadyExistsException
  • 不能一次创建多级目录,否则会抛异常 NoSuchFileException

创建多级目录:

Path path = Paths.get("helloword/hello/world");
Files.createDirectories(path);

拷贝文件方法:

Path source = Paths.get("helloword/source.txt");
Path target = Paths.get("helloword/target.txt");
Files.copy(source, target);
  • 如果文件已存在,会抛异常 FileAlreadyExistsException

如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制

Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

移动文件方法:

Path source = Paths.get("helloword/source.txt");
Path target = Paths.get("helloword/target.txt");
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
  • StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性

删除文件方法:

Path target = Paths.get("helloword/target.txt");
Files.delete(target);
  • 如果文件不存在,会抛异常 NoSuchFileException

删除为空目录:

Path target = Paths.get("helloword/hello/world");
Files.delete(target);
  • 如果目录还有内容,会抛异常 DirectoryNotEmptyException

删除非空目录:

Files.walkFileTree(Paths.get("d:\\a"), new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        Files.delete(file);
        return super.visitFile(file, attrs);
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        Files.delete(dir);
        return super.postVisitDirectory(dir, exc);
    }
});

遍历指定目录:

Files.walkFileTree(Paths.get("d:\\a"), new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        System.out.println(dir);
        return super.preVisitDirectory(dir, attrs);
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        System.out.println("\t" + file);
        return super.visitFile(file, attrs);
    }
});

拷贝多级目录:

String source = "d:\\a";
String target = "d:\\b";
Files.walk(Paths.get(source)).forEach(path -> {
    try {
        String targetName = path.toString().replace(source, target);
        // 目录文件
        if (Files.isDirectory(path)) {
            Files.createDirectory(Paths.get(targetName));
        }
        // 普通文件
        else if (Files.isRegularFile(path)) {
            Files.copy(path, Paths.get(targetName));
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});

6.3、IO流

概述:IO流就是用来处理设备间数据传输问题的。常见的应用:文件复制、文件上传、文件下载、文件的读取、文件的写出等等。

分类:

按照数据流向来分:
	输入流:读数据
	输出流:写数据
	
按照数据类型来分:
	字节流
		字节输入流
		字节输出流
	字符流
		字符输入流
		字符输出流

注意:

  1. 如果操作的是纯文本文件,优先使用字符流
  2. 如果操作的是图片、视频、音频、应用等二进制文件,优先使用字节流
  3. 如果不确定文件类型,优先使用字节流,字节流是万能的流

6.3.1、字节流

体系:

字节流写数据的三种方式:
方法描述public void write(int b)写入一个字节public void write(byte[] b)写入一个字节数组public void write(byte[] b, int off, int len)写入一个字节数组的一部分

字节流读数据的三种方式:

方法描述public abstract int read()读入一个字节public int read(byte[] b)读入一个字节数组public int read(byte[] b, int off, int len)读入一个字节数组的一部分

字节流复制文件的四种方式:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
	public static void main(String[] args) throws IOException {
		method1();
		method2();
		method3();
		method4();
	}

	// 基本字节流一次读写一个字节
	public static void method1() throws IOException {
		FileInputStream fis = new FileInputStream("sFolder\\demo.txt");
		FileOutputStream fos = new FileOutputStream("dFolder\\demo.txt");

		int by;
		while ((by = fis.read()) != -1) {
			fos.write(by);
		}

		fos.close();
		fis.close();
	}

	// 基本字节流一次读写一个字节数组
	public static void method2() throws IOException {
		FileInputStream fis = new FileInputStream("sFolder\\demo.txt");
		FileOutputStream fos = new FileOutputStream("dFolder\\demo.txt");

		byte[] bys = new byte[1024];
		int len;
		while ((len = fis.read(bys)) != -1) {
			fos.write(bys, 0, len);
		}

		fos.close();
		fis.close();
	}

	// 字节缓冲流一次读写一个字节
	public static void method3() throws IOException {
		BufferedInputStream bis = new BufferedInputStream(new FileInputStream("sFolder\\demo.txt"));
		BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("dFolder\\demo.txt"));

		int by;
		while ((by = bis.read()) != -1) {
			bos.write(by);
		}

		bos.close();
		bis.close();
	}

	// 字节缓冲流一次读写一个字节数组
	public static void method4() throws IOException {
		BufferedInputStream bis = new BufferedInputStream(new FileInputStream("sFolder\\demo.txt"));
		BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("dFolder\\demo.txt"));

		byte[] bys = new byte[1024];
		int len;
		while ((len = bis.read(bys)) != -1) {
			bos.write(bys, 0, len);
		}

		bos.close();
		bis.close();
	}
}

字节流复制单级文件夹:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
	public static void main(String[] args) throws IOException {
		File srcFolder = new File("D:\\sFolder");
		File destFolder = new File("D:\\dFolder");
		copyFolder(srcFolder, destFolder);
	}

	/** * 复制单级文件夹 * * @param srcFolder 源文件夹 * @param destFolder 目的文件夹 * @throws IOException */
	private static void copyFolder(File srcFolder, File destFolder) throws IOException {
		// 判断路径是否存在
		if (!destFolder.exists()) {
			destFolder.mkdirs();
		}
		// 获取目的文件列表
		File[] listFiles = srcFolder.listFiles();
		// 遍历目的文件列表
		for (File file : listFiles) {
			copyFile(file, new File(destFolder, file.getName()));
		}
	}

	/** * 复制文件 * * @param srcFile 源文件 * @param destFile 目的文件 * @throws IOException */
	private static void copyFile(File srcFile, File destFile) throws IOException {
		BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
		BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));
		byte[] bys = new byte[1024];
		int len;
		while ((len = bis.read(bys)) != -1) {
			bos.write(bys, 0, len);
		}
		bos.close();
		bis.close();
	}
}

字节流复制多级文件夹:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
	public static void main(String[] args) throws IOException {
		File srcFolder = new File("D:\\sFolder");
		File destFolder = new File("D:\\dFolder");
		copyFolder(srcFolder, destFolder);
	}

	/** * 复制多级文件夹 * * @param srcFolder 源文件夹 * @param destFolder 目的文件夹 * @throws IOException */
	private static void copyFolder(File srcFolder, File destFolder) throws IOException {
		// 判断路径是否存在
		if (!destFolder.exists()) {
			destFolder.mkdirs();
		}
		// 获取目的文件列表
		File[] listFiles = srcFolder.listFiles();
		// 遍历目的文件列表
		for (File file : listFiles) {
			if (file.isDirectory()) {
				copyFolder(file, new File(destFolder, file.getName()));
			} else {
				copyFile(file, new File(destFolder, file.getName()));
			}
		}
	}

	/** * 复制文件 * * @param srcFile 源文件 * @param destFile 目的文件 * @throws IOException */
	private static void copyFile(File srcFile, File destFile) throws IOException {
		BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
		BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));
		byte[] bys = new byte[1024];
		int len;
		while ((len = bis.read(bys)) != -1) {
			bos.write(bys, 0, len);
		}
		bos.close();
		bis.close();
	}
}

6.3.2、字符流

体系:

字符流写数据的五种方式:
方法描述public void write(int c)写入一个字符public void write(char[] cbuf)写入一个字符数组public void write(char[] cbuf, int off, int len)写入一个字符数组的一部分public void write(String str)写入一个字符串public void write(String str, int off, int len)写入一个字符串的一部分

字符流读数据的四种方式:

方法描述public int read()读入一个字符public int read(char[] cbuf)读入一个字符数组public int read(char[] cbuf, int offset, int length)读入一个字符数组的一部分public String readLine()读入一个字符串

字符流复制文本的七种方式:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class Main {
	public static void main(String[] args) throws IOException {
		method1();
		method2();
		method3();
		method4();
		method5();
		method6();
		method7();
	}

	// 基本字符流一次读写一个字符
	public static void method1() throws IOException {
		InputStreamReader isr = new InputStreamReader(new FileInputStream("sFolder\\demo.txt"));
		OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("dFolder\\demo.txt"));

		int ch;
		while ((ch = isr.read()) != -1) {
			osw.write(ch);
		}

		osw.close();
		isr.close();
	}

	// 基本字符流一次读写一个字符数组
	public static void method2() throws IOException {
		InputStreamReader isr = new InputStreamReader(new FileInputStream("sFolder\\demo.txt"));
		OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("dFolder\\demo.txt"));

		char[] chs = new char[1024];
		int len;
		while ((len = isr.read(chs)) != -1) {
			osw.write(chs, 0, len);
		}

		osw.close();
		isr.close();
	}

	// 文件字符流一次读写一个字符
	public static void method3() throws IOException {
		FileReader fr = new FileReader("sFolder\\demo.txt");
		FileWriter fw = new FileWriter("dFolder\\demo.txt");

		int ch;
		while ((ch = fr.read()) != -1) {
			fw.write(ch);
		}

		fw.close();
		fr.close();
	}

	// 文件字符流一次读写一个字符数组
	public static void method4() throws IOException {
		FileReader fr = new FileReader("sFolder\\demo.txt");
		FileWriter fw = new FileWriter("dFolder\\demo.txt");

		char[] chs = new char[1024];
		int len;
		while ((len = fr.read(chs)) != -1) {
			fw.write(chs, 0, len);
		}

		fw.close();
		fr.close();
	}

	// 字符缓冲流一次读写一个字符
	public static void method5() throws IOException {
		BufferedReader br = new BufferedReader(new FileReader("sFolder\\demo.txt"));
		BufferedWriter bw = new BufferedWriter(new FileWriter("dFolder\\demo.txt"));

		int ch;
		while ((ch = br.read()) != -1) {
			bw.write(ch);
		}

		bw.close();
		br.close();
	}

	// 字符缓冲流一次读写一个字符数组
	public static void method6() throws IOException {
		BufferedReader br = new BufferedReader(new FileReader("sFolder\\demo.txt"));
		BufferedWriter bw = new BufferedWriter(new FileWriter("dFolder\\demo.txt"));

		char[] chs = new char[1024];
		int len;
		while ((len = br.read(chs)) != -1) {
			bw.write(chs, 0, len);
		}

		bw.close();
		br.close();
	}

	// 字符缓冲流特有功能复制文本文件
	public static void method7() throws IOException {
		BufferedReader br = new BufferedReader(new FileReader("sFolder\\demo.txt"));
		BufferedWriter bw = new BufferedWriter(new FileWriter("dFolder\\demo.txt"));

		String line;
		while ((line = br.readLine()) != null) {
			bw.write(line);
			bw.newLine();
		}

		bw.close();
		br.close();
	}
}

6.3.3、特殊流

标准输入流:

import java.io.IOException;
import java.io.InputStream;

public class Main {
	public static void main(String[] args) throws IOException {
		InputStream is = System.in;

		int by;
		while ((by = is.read()) != -1) {
			System.out.print((char) by);
		}

		is.close();
	}
}

标准输出流:

import java.io.IOException;
import java.io.PrintStream;

public class Main {
	public static void main(String[] args) throws IOException {
		PrintStream ps = System.out;

		ps.println("Hello,World");
		ps.write("Hello,World".getBytes());

		ps.close();
	}
}

字节打印流:

import java.io.IOException;
import java.io.PrintStream;

public class Main {
	public static void main(String[] args) throws IOException {
		PrintStream ps = new PrintStream("ps.txt");

		ps.println(97);
		ps.write(97);

		ps.close();
	}
}

字符打印流:

import java.io.IOException;
import java.io.PrintWriter;

public class Main {
	public static void main(String[] args) throws IOException {
		PrintWriter pw = new PrintWriter("pw.txt");

		pw.println("hello");
		pw.write("Hello");

		pw.close();
	}
}

对象序列化流:

注意:需要实现Serializable接口,同时需要给出serialVersionUID

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class Student implements Serializable {
	private static final long serialVersionUID = 5923003911550370832L;
	private String name;
	private Integer age;

	public Student() {
		super();
	}

	public Student(String name, Integer age) {
		super();
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	@Override
	public String toString() {
		return "Student [name=" + name + ", age=" + age + "]";
	}
}

public class Main {
	public static void main(String[] args) throws IOException {
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("oos.txt"));

		Student s = new Student("曹晨磊", 30);
		oos.writeObject(s);

		oos.close();
	}
}

对象反序列化流:

注意:成员变量加transient关键字修饰,该关键字标记的成员变量不参与序列化过程

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

class Student implements Serializable {
	private static final long serialVersionUID = 5923003911550370832L;
	private String name;
	private Integer age;

	public Student() {
		super();
	}

	public Student(String name, Integer age) {
		super();
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	@Override
	public String toString() {
		return "Student [name=" + name + ", age=" + age + "]";
	}
}

public class Main {
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("oos.txt"));

		Object obj = ois.readObject();
		Student s = (Student) obj;
		System.out.println(s);

		ois.close();
	}
}

6.4、ByteBuffer

使用步骤:

  1. 向 buffer 写入数据,例如调用 channel.read(buffer)
  2. 调用 flip() 切换至读模式
  3. 从 buffer 读取数据,例如调用 buffer.get()
  4. 调用 clear() 或 compact() 切换至写模式
  5. 重复 1~4 步骤

代码演示:

try (FileChannel channel = new FileInputStream("data.txt").getChannel()) {
    ByteBuffer buffer = ByteBuffer.allocate(10);
    while (true) {
        int len = channel.read(buffer);
        if (len == -1) break;
        buffer.flip();
        while (buffer.hasRemaining()) {
            byte b = buffer.get();
            System.out.print((char) b);
        }
        buffer.clear();
    }
} catch (IOException e) {
    e.printStackTrace();
}

运行效果:

Hello,World

内部结构:

ByteBuffer 有以下重要属性:

  • capacity
  • position
  • limit

一开始:

写模式下,position 是写入位置,limit 等于容量,下图表示写入了 4 个字节后的状态。

flip 动作发生后,position 切换为读取位置,limit 切换为读取限制。

读取 4 个字节后,状态:

clear 动作发生后,状态:

compact 方法,是把未读完的部分向前压缩,然后切换至写模式。

分配空间:

可以使用 allocate 方法为 ByteBuffer 分配空间,其它 buffer 类也有该方法。

Bytebuffer buf = ByteBuffer.allocate(16);
  • ByteBuffer.allocate(16).getClass():class java.nio.HeapByteBuffer,java 堆内存,读写效率低,受到 GC 的影响,分配的效率高
  • ByteBuffer.allocateDirect(16).getClass():class java.nio.DirectByteBuffer,直接内存,读写效率高,不受 GC 影响,分配的效率低

向 buffer 写入数据:

有两种办法:

  • 调用 channel 的 read 方法
int readBytes = channel.read(buf);
  • 调用 buffer 自己的 put 方法
buf.put((byte)127);

从 buffer 读取数据:

有两种办法:

  • 调用 channel 的 write 方法
int writeBytes = channel.write(buf);
  • 调用 buffer 自己的 get 方法
byte b = buf.get();

get 方法会让 position 读指针向后走,如果想重复读取数据

  • 可以调用 rewind 方法将 position 重新置为 0
  • 或者调用 get(int i) 方法获取索引 i 的内容,它不会移动读指针

mark 和 reset方法:

mark 是在读取时,做一个标记,即使 position 改变,只要调用 reset 就能回到 mark 的位置,但是,rewind 和 flip 都会清除 mark 。

字符串与 ByteBuffer 互转:

第一种方式:

ByteBuffer buffer = StandardCharsets.UTF_8.encode("abc123");
String string = StandardCharsets.UTF_8.decode(buffer).toString();
System.out.println(string);

第二种方式:

ByteBuffer buffer = Charset.forName("utf-8").encode("abc123");
String string = Charset.forName("utf-8").decode(buffer).toString();
System.out.println(string);

6.5、FileChannel

Channel 介绍:

Channel 有一点类似于 Stream,它就是读写数据的双向通道,可以从 Channel 将数据读入 buffer,也可以将 buffer 的数据写入 Channel,而之前的 Stream 要么是输入,要么是输出,Channel 比 Stream 相比更为底层。

Channel 分类:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

FileChannel 获取方法:

FileChannel 只能工作在阻塞模式下,不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法。

  • 通过 FileInputStream 获取的 channel 只能读
  • 通过 FileOutputStream 获取的 channel 只能写
  • 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定

FileChannel 读取方法:

会从 channel 读取数据填充 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾。

int readBytes = channel.read(buffer);

FileChannel 写入方法:

写入的正确姿势如下,在 while 中调用 channel.write 是因为 write 方法并不能保证一次将 buffer 中的内容全部写入 channel。

ByteBuffer buffer = ...;
buffer.put(...); // 存入数据
buffer.flip();   // 切换读模式

while(buffer.hasRemaining()) {
    channel.write(buffer);
}

FileChannel 关闭方法:

channel 必须关闭,不过调用了 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 close 方法会间接地调用 channel 的 close 方法,或者使用 try 代码块自动释放。

try (FileChannel channel = new FileInputStream("data.txt").getChannel()) {

} catch (IOException e) {
    e.printStackTrace();
}

FileChannel 位置方法:

获取当前位置

long pos = channel.position();

设置当前位置

long newPos = ...;
channel.position(newPos);

设置当前位置时,如果设置为文件的末尾,这时读取会返回 -1 ,这时写入,会追加内容,但要注意,如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00)。

FileChannel 大小方法:

使用 size 方法获取文件的大小。

FileChannel 强制写入:

操作系统出于性能考虑,会将数据缓存,不是立刻写入磁盘。可以调用 force(true) 方法将文件内容和元数据(文件权限等信息)立刻写入磁盘。

FileChannel 文件拷贝:

try (
    FileChannel from = new FileInputStream("data.txt").getChannel();
    FileChannel to = new FileOutputStream("to.txt").getChannel();
) {
    long size = from.size();
    // remain 变量代表还剩余多少字节
    for (long remain = size; remain > 0; ) {
        remain -= from.transferTo((size - remain), remain, to);
    }
} catch (IOException e) {
    e.printStackTrace();
}

第七章 多线程

7.1、线程基本知识

进程与线程:

进程(Process)是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。

线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行。

在 windows 下查看进程的命令:

  • 查看进程:tasklist
  • 杀死进程:taskkill /pid <PID>

在 linux 下查看进程的命令:

  • 查看进程:ps -fe
  • 查看线程:top -H -p <PID>
  • 杀死进程:kill <PID>

在 java 中查看进程的命令:

  • 查看Java进程:jps
  • 查看Java线程:jstack <PID>
  • 图形化的工具:jconsole

并行与并发:

单核 CPU 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 CPU 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 CPU 在线程间的切换非常快,人类感觉是同时运行的 。总结为一句话就是:微观串行,宏观并行,一般会将这种线程轮流使用 CPU 的做法称为并发(Concurrent)。

多核 CPU下,每个核都可以调度运行线程,这时候线程可以是并行(Parallel)的。

同步与异步:

以调用方角度来讲:

  • 需要等待结果返回后才能继续运行就是同步。
  • 不需要等待结果返回就能继续运行就是异步。

7.2、线程创建方式

  • 方式一:
Thread thread = new Thread(){
    @Override
    public void run() {
        System.out.println("thread run ...");
    }
};

thread.start();

简化后:

Thread thread = new Thread(() -> System.out.println("thread run ..."));

thread.start();
  • 方式二:
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("runnable run ...");
    }
});

thread.start();

简化后:

Thread thread = new Thread(() -> System.out.println("runnable run ..."));

thread.start();
  • 方式三:
Callable<Integer> callable = new Callable() {
    @Override
    public Object call() throws Exception {
        System.out.println("callable run ...");
        return 521;
    }
};
FutureTask futureTask = new FutureTask(callable);
Thread thread = new Thread(futureTask);
thread.start();

简化后:

Thread thread = new Thread(new FutureTask(() -> {
    System.out.println("callable run ...");
    return 521;
}));

thread.start();

7.3、线程基本方法

注意:标黄色的方法代表是 static 方法,可直接类名调用,无需创建对象。
名称描述注意事项start()启动一个新线程,
在新的线程运行 run 方法start 方法只是让线程进入就绪,里面代码不一定立刻
运行(CPU 的时间片还没分给它)。每个线程对象的
start方法只能调用一次,如果调用了多次会出现
IllegalThreadStateExceptionrun()新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则
线程启动后会调用 Runnable 中的 run 方法,否则默
认不执行任何操作。但可以创建 Thread 的子类对象,
来覆盖默认行为join()等待线程运行结束join(long n)等待线程运行结束,
最多等待 n 毫秒getId()获取线程长整型的 idid 唯一getName()获取线程名setName(String name)修改线程名getPriority()获取线程优先级setPriority(int priority)修改线程优先级Java 中规定线程优先级是1~10 的整数,较大的优先级
能提高该线程被 CPU 调度的机率getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为:
NEW, RUNNABLE, BLOCKED, WAITING,
TIMED_WAITING, TERMINATEDinterrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被
打断的线程抛出 InterruptedException,并清除
打断标记;如果打断正在运行的线程,则会设置
打断标记;park 的线程被打断,也会设置打断标记interrupted()判断当前线程是否被打断会清除打断标记isInterrupted()判断当前线程是否被打断不会清除打断标记isAlive()判断当前线程是否存活isDaemon()判断当前线程是否是守护线程setDaemon(boolean on)设置当前线程为守护线程currentThread()获取当前正在执行的线程sleep(long n)让当前执行的线程休眠n毫秒,
休眠时让出 CPU 的时间片
给其它线程yield()提示线程调度器让出当前线程
对 CPU 的使用主要是为了测试和调试,它的具体的实现依赖于
操作系统的任务调度器

7.4、线程安全问题

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

public class Main {
    static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter++;
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter--;
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter);
    }
}

以上的结果可能是正数、负数、零。因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析。

对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 	// 准备常量1
iadd 		// 自增
putstatic i // 将修改后的值存入静态变量i

对于 i-- 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 	// 准备常量1
isub 		// 自减
putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

如果是单线程的情况下,以上8行代码不会交错执行:

如果是多线程的情况下,以上8行代码可能交错执行:

出现负数的情况:

出现正数的情况:

一个程序运行多个线程本身是没有问题的,问题出现在多个线程访问共享资源:多个线程读共享资源其实也没有问题,问题本质在于多个线程对共享资源读写操作时发生指令交错,就会出现问题。

一段代码块内,如果存在对共享资源的多线程读写操作,称这段代码块为临界区(Critical Section)。

例如,下面代码中的临界区:

static int counter = 0;

static void increment()
// 临界区
{
	counter++;
}

static void decrement()
// 临界区
{
	counter--;
}

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件(Race Condition)。

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

我们使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换,以此避免线程安全问题。

虽然 Java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

synchronized的语法:

synchronized(对象)
{
	临界区
}

synchronized改进后:

public class Main {
    static int counter = 0;
    static final Object room = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (room) {
                    counter++;
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (room) {
                    counter--;
                }
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter);
    }
}

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

方法上的 synchronized

class Test{
    public synchronized void test() {
    }
}
等价于
class Test{
    public void test() {
        synchronized(this) {
        }
    }
}
class Test{
    public synchronized static void test() {
    }
}
等价于
class Test{
    public static void test() {
        synchronized(Test.class) {
        }
    }
}

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全

  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况

  • 如果只有读操作,则线程安全

  • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

方法上(内)局部变量是否线程安全?

  • 局部变量是线程安全的

  • 但局部变量引用的对象则未必

  • 如果该对象没有逃离方法的作用访问,它是线程安全的

  • 如果该对象逃离方法的作用范围,需要考虑线程安全

7.5、线程并发工具

7.5.1、ReentrantLock

锁介绍

ReentrantLock 相对于 synchronized,它具备如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
  • 与 synchronized 一样,都支持可重入

基本语法:

// 创建锁
ReentrantLock reentrantLock = new ReentrantLock();
// 获取锁
reentrantLock.lock();
try {
    // 临界区
} finally {
    // 释放锁
    reentrantLock.unlock();
}

可重入

可重入是指同一个线程,如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

public class Main {
    public static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        method1();
    }

    public static void method1() {
        lock.lock();
        try {
            System.out.println("execute method1");
            method2();
        } finally {
            lock.unlock();
        }
    }

    public static void method2() {
        lock.lock();
        try {
            System.out.println("execute method2");
            method3();
        } finally {
            lock.unlock();
        }
    }

    public static void method3() {
        lock.lock();
        try {
            System.out.println("execute method3");
        } finally {
            lock.unlock();
        }
    }
}
execute method1
execute method2
execute method3

可打断

ReentrantLock 支持可打断,如果是不可中断模式,即使使用了 interrupt 也不会让等待中断。

public class Main {
    public static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("t1启动...");
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("t1等锁的过程中被打断");
                return;
            }
            try {
                System.out.println("t1获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");
        lock.lock();
        System.out.println("main获得了锁");
        t1.start();
        try {
            Thread.sleep(1000);
            t1.interrupt();
            System.out.println("main执行打断");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
main获得了锁
t1启动...
main执行打断
t1等锁的过程中被打断
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at com.company.Main.lambda$main$0(Main.java:12)

锁超时

public class Main {
    public static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("t1启动...");
            if (!lock.tryLock()) {
                System.out.println("t1获取失败,返回");
                return;
            }
            try {
                System.out.println("t1获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");
        lock.lock();
        System.out.println("main获得了锁");
        t1.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
main获得了锁
t1启动...
t1获取失败,返回

公平锁

ReentrantLock 默认是不公平的,改为公平锁语法如下,公平锁一般没有必要,会降低并发度。

ReentrantLock lock = new ReentrantLock(true);

多条件

synchronized 中也有条件变量,就是 waitSet 休息室,当条件不满足时进入 waitSet 等待,ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比 synchronized 是那些不满足条件的线程都在一间休息室等消息,而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒。

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)去重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行
public class Main {
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition waitCigaretteQueue = lock.newCondition();
    public static Condition waitbreakfastQueue = lock.newCondition();
    public static volatile boolean hasCigrette = false;
    public static volatile boolean hasBreakfast = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            try {
                lock.lock();
                while (!hasCigrette) {
                    try {
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("小明等到了它的烟");
            } finally {
                lock.unlock();
            }
        },"小明").start();
        new Thread(() -> {
            try {
                lock.lock();
                while (!hasBreakfast) {
                    try {
                        waitbreakfastQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("小红等到了它的早餐");
            } finally {
                lock.unlock();
            }
        },"小红").start();
        Thread.sleep(1000);
        sendBreakfast();
        Thread.sleep(1000);
        sendCigarette();
    }

    private static void sendCigarette() {
        lock.lock();
        try {
            System.out.println("送烟来了");
            hasCigrette = true;
            waitCigaretteQueue.signal();
        } finally {
            lock.unlock();
        }
    }

    private static void sendBreakfast() {
        lock.lock();
        try {
            System.out.println("送早餐来了");
            hasBreakfast = true;
            waitbreakfastQueue.signal();
        } finally {
            lock.unlock();
        }
    }
}
送早餐来了
小红等到了它的早餐
送烟来了
小明等到了它的烟

小测验

线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现?

class AwaitSignal extends ReentrantLock {
    private int loopNumber;// 循环次数

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void start(Condition first) {
        this.lock();
        try {
            first.signal();
        } finally {
            this.unlock();
        }
    }

    public void print(String str, Condition current, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            this.lock();
            try {
                current.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                this.unlock();
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        AwaitSignal as = new AwaitSignal(5);
        Condition aWaitSet = as.newCondition();
        Condition bWaitSet = as.newCondition();
        Condition cWaitSet = as.newCondition();
        new Thread(() -> {
            as.print("a", aWaitSet, bWaitSet);
        }).start();
        new Thread(() -> {
            as.print("b", bWaitSet, cWaitSet);
        }).start();
        new Thread(() -> {
            as.print("c", cWaitSet, aWaitSet);
        }).start();
        as.start(aWaitSet);
    }
}
abcabcabcabcabc

7.5.2、ReentrantReadWriteLock

当读操作远远高于写操作时,这时候使用 读写锁读-读 可以并发,提高性能。

加解读锁

rw.readLock().lock();
rw.readLock().unlock();

加解写锁

rw.writeLock().lock();
rw.writeLock().unlock();

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法。

public class DataContainer {
    private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private int data;

    public DataContainer(int data) {
        this.data = data;
    }

    public int read() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "获取读锁...");
        rw.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "读取");
            Thread.sleep(2000);
            return data;
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁...");
            rw.readLock().unlock();
        }
    }

    public void write(int data) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "获取写锁...");
        rw.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "写入");
            this.data = data;
            Thread.sleep(2000);
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁...");
            rw.writeLock().unlock();
        }
    }
}

测试读-读

public class TestReadRead {
    public static void main(String[] args) {
        DataContainer dataContainer = new DataContainer(123456);
        new Thread(() -> {
            try {
                int data = dataContainer.read();
                System.out.println(data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
        new Thread(() -> {
            try {
                int data = dataContainer.read();
                System.out.println(data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}
t1获取读锁...
t1读取
t2获取读锁...
t2读取
t2释放读锁...
t1释放读锁...
123456
123456

测试读-写

public class TestReadWrite {
    public static void main(String[] args) {
        DataContainer dataContainer = new DataContainer(123456);
        new Thread(() -> {
            try {
                int data = dataContainer.read();
                System.out.println(data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
        new Thread(() -> {
            try {
                dataContainer.write(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}
t1获取读锁...
t2获取写锁...
t1读取
t1释放读锁...
123456
t2写入
t2释放写锁...

测试写-写

public class TestWriteWrite {
    public static void main(String[] args) {
        DataContainer dataContainer = new DataContainer(123456);
        new Thread(() -> {
            try {
                dataContainer.write(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
        new Thread(() -> {
            try {
                dataContainer.write(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}
t1获取写锁...
t1写入
t2获取写锁...
t1释放写锁...
t2写入
t2释放写锁...

7.5.3、StampedLock

该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用。

加解读锁

long stamp = lock.readLock();
lock.unlockRead(stamp);

加解写锁

long stamp = lock.writeLock();
lock.unlockWrite(stamp);

乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 ,如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)) {
    // 锁升级
}

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法。

public class DataContainerStamped {
    private final StampedLock lock = new StampedLock();
    private int data;

    public DataContainerStamped(int data) {
        this.data = data;
    }

    public int read() throws InterruptedException {
        // 乐观锁 - 读锁
        long stamp = lock.tryOptimisticRead();
        System.out.println(Thread.currentThread().getName() + " optimistic read locking..." + stamp);
        Thread.sleep(2000);
        if (lock.validate(stamp)) {
            System.out.println(Thread.currentThread().getName() + " read finish..." + stamp + ", data:" + data);
            return data;
        }
        // 锁升级 - 读锁
        System.out.println(Thread.currentThread().getName() + " updating to read lock..." + stamp);
        try {
            stamp = lock.readLock();
            System.out.println(Thread.currentThread().getName() + " read lock " + stamp);
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + " read finish..." + stamp + ", data:" + data);
            return data;
        } finally {
            System.out.println(Thread.currentThread().getName() + " read unlock " + stamp);
            lock.unlockRead(stamp);
        }
    }

    public void write(int data) throws InterruptedException {
        long stamp = lock.writeLock();
        System.out.println(Thread.currentThread().getName() + " write lock " + stamp);
        try {
            Thread.sleep(2000);
            this.data = data;
        } finally {
            System.out.println(Thread.currentThread().getName() + " write unlock " + stamp);
            lock.unlockWrite(stamp);
        }
    }
}

测试读-读

public class TestReadReadStamped {
    public static void main(String[] args) {
        DataContainerStamped dataContainer = new DataContainerStamped(123456);
        new Thread(() -> {
            try {
                int data = dataContainer.read();
                System.out.println(data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
        new Thread(() -> {
            try {
                int data = dataContainer.read();
                System.out.println(data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}
t1 optimistic read locking...256
t2 optimistic read locking...256
t2 read finish...256, data:123456
t1 read finish...256, data:123456
123456
123456

测试读-写

public class DataContainerStampedTest {
    public static void main(String[] args) {
        DataContainerStamped dataContainer = new DataContainerStamped();
        new Thread(() -> {
            try {
                dataContainer.read();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
        new Thread(() -> {
            try {
                dataContainer.write(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}
t1 optimistic read locking...256
t2 write lock 384
t2 write unlock 384
t1 updating to read lock...256
t1 read lock 513
t1 read finish...513, data:100
t1 read unlock 513
100

测试写-写

public class TestWriteWriteStamped {
    public static void main(String[] args) {
        DataContainerStamped dataContainer = new DataContainerStamped(123456);
        new Thread(() -> {
            try {
                dataContainer.write(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
        new Thread(() -> {
            try {
                dataContainer.write(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}
t1 write lock 384
t1 write unlock 384
t2 write lock 640
t2 write unlock 640

7.5.4、Semaphore

Semaphore信号量,用来限制能同时访问共享资源的线程上限。

场景:我们现在有6个线程,但是我们每次要求只有2个线程运行,这时候可以使用信号量,来限制能同时访问共享资源的线程上限。

public class Main {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println("running...");
                    Thread.sleep(2000);
                    System.out.println("end...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }).start();
        }
    }
}
running...
running...
end...
end...
running...
running...
end...
end...
running...
running...
end...
end...

7.5.5、CountDownLatch

CountdownLatch用来进行线程同步协作,等待所有线程完成倒计时。

其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " begin...");
                Thread.sleep(1000);
                latch.countDown();
                System.out.println(Thread.currentThread().getName() + " end... " + latch.getCount());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " begin...");
                Thread.sleep(2000);
                latch.countDown();
                System.out.println(Thread.currentThread().getName() + " end... " + latch.getCount());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " begin...");
                Thread.sleep(3000);
                latch.countDown();
                System.out.println(Thread.currentThread().getName() + " end... " + latch.getCount());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t3").start();
        System.out.println(Thread.currentThread().getName() + " waiting...");
        latch.await();
        System.out.println(Thread.currentThread().getName() + " wait end...");
    }
}
t1 begin...
t2 begin...
main waiting...
t3 begin...
t1 end... 2
t2 end... 1
t3 end... 0
main wait end...

7.5.6、CyclicBarrier

CyclicBarrier循环栅栏,用来进行线程同步协作,等待线程满足某个计数。

构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行。他可以用于多线程计算数据,最后合并计算结果的场景。CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的。

public class Main {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);
        CyclicBarrier barrier = new CyclicBarrier(2, () -> {
            System.out.println(Thread.currentThread().getName() + " task1, task2 finish...");
        });
        for (int i = 0; i < 4; i++) {
            service.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " task1 begin...");
                try {
                    Thread.sleep(1000);
                    barrier.await(); // 2-1=1
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
            service.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " task2 begin...");
                try {
                    Thread.sleep(2000);
                    barrier.await(); // 1-1=0
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
        }
        service.shutdown();
    }
}
pool-1-thread-1 task1 begin...
pool-1-thread-2 task2 begin...
pool-1-thread-2 task1, task2 finish...
pool-1-thread-1 task1 begin...
pool-1-thread-2 task2 begin...
pool-1-thread-2 task1, task2 finish...
pool-1-thread-2 task1 begin...
pool-1-thread-1 task2 begin...
pool-1-thread-1 task1, task2 finish...
pool-1-thread-2 task1 begin...
pool-1-thread-1 task2 begin...
pool-1-thread-1 task1, task2 finish...

7.5.7、ThreadLocal

ThreadLocal(线程局部变量),在并发编程的时候,成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同一个变量,显然是不行的,并且我们也知道volatile这个关键字也是不能保证线程安全的。那么在有一种情况之下,我们需要满足这样一个条件:变量是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本。这种情况,ThreadLocal就比较好的解决了这个问题。

我们首先来看一个反例:看看线程名中的i与value中的i是否能够一一对应上。

public class ThreadLocalTest {
    static class ResourceClass {
        public static String s1;
        public static String s2;
    }

    static class A {
        public void setOne(String value) {
            ResourceClass.s1 = value;
        }

        public void setTwo(String value) {
            ResourceClass.s2 = value;
        }
    }

    static class B {
        public void display() {
            System.out.println(ResourceClass.s1 + ":" + ResourceClass.s2);
        }
    }

    public static void main(String[] args) {
        final A a = new A();
        final B b = new B();
        for (int i = 0; i < 10; i++) {
            final String resouce1 = "线程-" + i;
            final String resouce2 = " value = (" + i + ")";
            new Thread(() -> {
                a.setOne(resouce1);
                a.setTwo(resouce2);
                b.display();
            }).start();
        }
    }
}
线程-4: value = (4)
线程-5: value = (5)
线程-0: value = (0)
线程-4: value = (4)
线程-6: value = (6)
线程-4: value = (4)
线程-4: value = (5)
线程-7: value = (7)
线程-8: value = (8)
线程-9: value = (9)

我们定义了两个ThreadLocal变量,最终测试的目的就是要看两个值能否相应上,只有这样才能证明ThreadLocal所保存的数据是线程私有的。

public class ThreadLocalTest {
    static class ResourceClass {
        public final static ThreadLocal<String> RESOURCE_1 = new ThreadLocal<>();
        public final static ThreadLocal<String> RESOURCE_2 = new ThreadLocal<>();
    }

    static class A {
        public void setOne(String value) {
            ResourceClass.RESOURCE_1.set(value);
        }

        public void setTwo(String value) {
            ResourceClass.RESOURCE_2.set(value);
        }
    }

    static class B {
        public void display() {
            System.out.println(ResourceClass.RESOURCE_1.get() + ":" + ResourceClass.RESOURCE_2.get());
        }
    }

    public static void main(String[] args) {
        final A a = new A();
        final B b = new B();
        for (int i = 0; i < 10; i++) {
            final String resouce1 = "线程-" + i;
            final String resouce2 = " value = (" + i + ")";
            new Thread(() -> {
                try {
                    a.setOne(resouce1);
                    a.setTwo(resouce2);
                    b.display();
                } finally {
                    ResourceClass.RESOURCE_1.remove();
                    ResourceClass.RESOURCE_2.remove();
                }
            }).start();
        }
    }
}
线程-8: value = (8)
线程-3: value = (3)
线程-2: value = (2)
线程-1: value = (1)
线程-4: value = (4)
线程-6: value = (6)
线程-7: value = (7)
线程-0: value = (0)
线程-5: value = (5)
线程-9: value = (9)

从前面的操作能够发现,ThreadLocal最常见的操作就是set、get、remove三个动作。并且通过上面的分析,我们能够认识到ThreadLocal事实上是与线程绑定的一个变量,如此就会出现一个问题:假设没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。

因此,ThreadLocal的一个非常大的“坑”就是当使用不当时,导致使用者不知道它的作用域范围。大家可能觉得线程结束后ThreadLocal应该就回收了。假设线程真的注销了确实是这种,可是事实有可能并不是如此。比如:在线程池中对线程管理都是采用线程复用的方法(Web容器通常也会采用线程池)。在线程池中线程非常难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致,那么对应的ThreadLocal变量的生命周期也将不可预测。

7.6、线程内存模型

7.6.1、内存模型

JMM,即为Java内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从Java 5开始的JSR-133发布后,已经成熟和完善起来。

7.6.2、内存划分

JMM规定了内存主要划分为主内存和工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。

JVM在设计时候考虑到,如果Java线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。

7.6.3、内存交互

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

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

JMM对这八种指令的使用,制定了如下规则:

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主内存
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

7.6.4、模型特征

  • 原子性: 例如上面八项操作,在操作系统里面是不可分割的单元。被synchronized关键字或其他锁包裹起来的操作也可以认为是原子的。从一个线程观察另外一个线程的时候,看到的都是一个个原子性的操作。
synchronized (this) {
    a=1;
    b=2;
}
  • 可见性: 每个工作线程都有自己的工作内存,所以当某个线程修改完某个变量之后,在其他的线程中,未必能观察到该变量已经被修改。volatile关键字要求被修改之后的变量要求立即更新到主内存,每次使用前从主内存处进行读取。因此volatile可以保证可见性。除了volatile以外,synchronized和final也能实现可见性。synchronized保证unlock之前必须先把变量刷新回主内存。final修饰的字段在构造器中一旦完成初始化,并且构造器没有this逸出,那么其他线程就能看到final字段的值。
public class Main {
    // main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止
    // 解决办法:加上关键字 volatile ,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值
    // 线程操作 volatile 变量都是直接操作主存,这就体现了可见性
    public static volatile boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                if (!run) {
                    break;
                }
            }
        });
        t.start();

        // 预期效果:1s钟以后 t 线程停止
        Thread.sleep(1000);
        run = false;
    }
}
  • 有序性: Java的有序性跟线程相关。如果在线程内部观察,会发现当前线程的一切操作都是有序的。如果在线程的外部来观察的话,会发现线程的所有操作都是无序的。因为JMM的工作内存和主内存之间存在延迟,而且Java会对一些指令进行重新排序。volatile和synchronized可以保证程序的有序性,很多程序员只理解这两个关键字的执行互斥,而没有很好的理解到volatile和synchronized也能保证指令不进行重排序。

7.6.5、happens-before

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见

  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见

  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见

  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)

  • 线程 t1 打断 t2 前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)

  • 对变量(成员变量或静态成员变量)默认值(0,false,null)的写,对其它线程对该变量的读可见

7.7、线程生命周期

7.8、线程状态转换

假设有线程 Thread t

情况 1 NEW --> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况 2 RUNNABLE <–> WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING

  • 调用 obj.notify() ,obj.notifyAll(),t.interrupt() 时

  • 竞争锁成功,t 线程从 WAITING --> RUNNABLE

  • 竞争锁失败,t 线程从 WAITING --> BLOCKED

情况 3 RUNNABLE <–> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING,注意是当前线程在 t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

情况 4 RUNNABLE <–> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了t线程的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

情况 5 RUNNABLE <–> TIMED_WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING

  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

  • 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE

  • 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

情况 6 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING,注意是当前线程在 t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE

情况 7 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

情况 8 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE

情况 9 RUNNABLE <–> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

情况 10 RUNNABLE <–> TERMINATED

  • 当前线程所有代码运行完毕,进入TERMINATED

7.9、线程组的创建

ThreadGroup是Java提供的一种对线程进行分组管理的手段,可以对所有线程以组为单位进行操作,如设置优先级、守护线程等。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int x = 0; x < 5; x++) {
            System.out.println(Thread.currentThread().getThreadGroup() + ":" + Thread.currentThread().getName() + ":" + x);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        // ThreadGroup(String name)
        ThreadGroup tg = new ThreadGroup("线程组");
        // Thread(ThreadGroup group, Runnable target, String name)
        Thread t1 = new Thread(tg, new MyRunnable(), "张三");
        Thread t2 = new Thread(tg, new MyRunnable(), "李四");
        // 获取组名并在控制台输出
        System.out.println("t1的线程组名称:" + t1.getThreadGroup().getName());
        System.out.println("t2的线程组名称:" + t2.getThreadGroup().getName());
        // 表示该组线程是后台线程
        tg.setDaemon(true);
        // 表示该组线程的优先级5
        tg.setMaxPriority(5);
        // 启动线程组中的所有线程
        t1.start();
        t2.start();
        // 打印该组活跃线程的信息
        tg.list();
    }
}
t1的线程组名称:线程组
t2的线程组名称:线程组
java.lang.ThreadGroup[name=线程组,maxpri=5]
    Thread[张三,5,线程组]
    Thread[李四,5,线程组]
java.lang.ThreadGroup[name=线程组,maxpri=5]:张三:0
java.lang.ThreadGroup[name=线程组,maxpri=5]:张三:1
java.lang.ThreadGroup[name=线程组,maxpri=5]:张三:2
java.lang.ThreadGroup[name=线程组,maxpri=5]:张三:3
java.lang.ThreadGroup[name=线程组,maxpri=5]:张三:4
java.lang.ThreadGroup[name=线程组,maxpri=5]:李四:0
java.lang.ThreadGroup[name=线程组,maxpri=5]:李四:1
java.lang.ThreadGroup[name=线程组,maxpri=5]:李四:2
java.lang.ThreadGroup[name=线程组,maxpri=5]:李四:3
java.lang.ThreadGroup[name=线程组,maxpri=5]:李四:4

7.10、线程池的创建

7.10.1、自定义线程池

我们接下来将要自己手动设计一个线程池,这个线程池主要由两部分组成:线程池 + 阻塞队列。

我们首先来设计一下阻塞队列,作用就是,当线程池容量满了以后,如果主程序 main 还继续增加任务,则会被放到阻塞队列中等待。
注意:本章节的内容用到的@Slf4j是Lombok中的注解,建议此章节使用一个全新的Maven项目来演示。

// 阻塞队列
@Slf4j
public class BlockingQueue<T> {
    private Deque<T> deque = new ArrayDeque<>();            // 用来存储阻塞的任务
    private ReentrantLock lock = new ReentrantLock();       // 用来对阻塞队列加锁
    private Condition fullWaitSet = lock.newCondition();    // 生产者条件变量
    private Condition emptyWaitSet = lock.newCondition();   // 消费者条件变量
    private int capcity;                                    // 阻塞队列的容量

    // 无参构造函数
    public BlockingQueue() {
        this.capcity = 4;
    }

    // 有参构造函数
    public BlockingQueue(int capcity) {
        this.capcity = capcity;
    }

    // 获取队列大小
    public int size() {
        lock.lock();
        try {
            return deque.size();
        } finally {
            lock.unlock();
        }
    }
}

向阻塞队列中添加任务:

  • 如果容量已经满了,那么该任务要阻塞
  • 如果容量还有剩余,那么该任务要入队
// 向阻塞队列中添加任务
public boolean put(T task) {
    lock.lock();
    try {
        // 如果容量已经满了,那么该任务要阻塞
        while (deque.size() == capcity) {
            try {
                log.info("等待加入阻塞队列:{}", task);
                fullWaitSet.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果容量还有剩余,那么该任务要入队
        log.info("已经加入阻塞队列:{}", task);
        deque.addLast(task);
        emptyWaitSet.signal();
        return true;
    } finally {
        lock.unlock();
    }
}

向阻塞队列中获取任务:

  • 如果容量已经空了,那么该请求要阻塞
  • 如果容量还有剩余,那么直接返回任务
// 向阻塞队列中获取任务
public T take() {
    lock.lock();
    try {
        // 如果容量已经空了,那么该请求要阻塞
        while (deque.isEmpty()) {
            try {
                emptyWaitSet.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果容量还有剩余,那么直接返回任务
        T task = deque.removeFirst();
        fullWaitSet.signal();
        return task;
    } finally {
        lock.unlock();
    }
}

向阻塞队列中添加任务(带超时时间):

  • 如果容量已经满了,那么该任务要阻塞
  • 如果容量还有剩余,那么该任务要入队
// 向阻塞队列中添加任务(带超时时间)
public boolean offer(T task, Long timeout, TimeUnit timeUnit) {
    lock.lock();
    try {
        long nanos = timeUnit.toNanos(timeout);
        // 如果容量已经满了,那么该任务要阻塞
        while (deque.size() == capcity) {
            try {
                log.info("等待加入阻塞队列:{}", task);
                // awaitNanos的返回值是剩余时间,如果<=0,则表示超时
                if (nanos <= 0) return false;
                nanos = fullWaitSet.awaitNanos(nanos);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果容量还有剩余,那么该任务要入队
        log.info("已经加入阻塞队列:{}", task);
        deque.addLast(task);
        emptyWaitSet.signal();
        return true;
    } finally {
        lock.unlock();
    }
}

向阻塞队列中获取任务(带超时时间):

  • 如果容量已经空了,那么该请求要阻塞
  • 如果容量还有剩余,那么直接返回任务
// 向阻塞队列中获取任务(带超时时间)
public T poll(Long timeout, TimeUnit timeUnit) {
    lock.lock();
    try {
        long nanos = timeUnit.toNanos(timeout);
        // 如果容量已经空了,那么该请求要阻塞
        while (deque.isEmpty()) {
            try {
                // awaitNanos的返回值是剩余时间,如果<=0,则表示超时
                if (nanos <= 0) return null;
                nanos = emptyWaitSet.awaitNanos(nanos);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果容量还有剩余,那么直接返回任务
        T task = deque.removeFirst();
        fullWaitSet.signal();
        return task;
    } finally {
        lock.unlock();
    }
}

阻塞队列已经设计的可以使用了,接下来我们将要设计线程池。

// 自定义线程池
@Slf4j
public class ThreadPool {
    private int coreSize;                                   // 核心线程数
    private Long timeout;                                   // 超时时间值
    private TimeUnit timeUnit;                              // 时间工具类
    private HashSet<Worker> workers = new HashSet<>();      // 线程集合
    private BlockingQueue<Runnable> taskQueue;              // 阻塞队列

    public ThreadPool(int coreSize, Long timeout, TimeUnit timeUnit, int taskQueueSize) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(taskQueueSize);
    }
}

我们需要设计 Worker 工作线程。

// 自定义线程池
@Slf4j
public class ThreadPool {
    private int coreSize;                                   // 核心线程数
    private Long timeout;                                   // 超时时间值
    private TimeUnit timeUnit;                              // 时间工具类
    private HashSet<Worker> workers = new HashSet<>();      // 线程集合
    private BlockingQueue<Runnable> taskQueue;              // 阻塞队列

    public ThreadPool(int coreSize, Long timeout, TimeUnit timeUnit, int taskQueueSize) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(taskQueueSize);
    }

    // 工作线程
    class Worker extends Thread {
        private Runnable task;

        public Worker(Runnable task) {
            this.task = task;
        }

        @Override
        public void run() {
            // 运行任务
            while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
                try {
                    task.run();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    task = null;
                }
            }
            // 释放线程
            synchronized (workers) {
                log.info("移除线程:{}", this);
                workers.remove(this);
            }
        }
    }

    // 执行任务
    public void execute(Runnable task) {
        // 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
        // 当任务数已经超过 coreSize 时,将 task 加入阻塞队列之中
        synchronized (workers) {
            if (workers.size() < coreSize) {
                Worker worker = new Worker(task);
                log.debug("新增 worker 执行 {}", worker, task);
                workers.add(worker);
                worker.start();
            } else {
                taskQueue.put(task);
            }
        }
    }
}

接下来,我们需要对自己设计的自定义线程池进行测试。

@Slf4j
public class ThreadPoolTest {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(5, 1000L, TimeUnit.MILLISECONDS, 10);

        for (int i = 0; i < 10; i++) {
            int info = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("{}", info);
            });
        }
    }
}

@Slf4j
public class ThreadPoolTest {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(5, 1000L, TimeUnit.MILLISECONDS, 1);

        for (int i = 0; i < 10; i++) {
            int info = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("{}", info);
            });
        }
    }
}

当任务数已经超过 coreSize 时,将 task 加入阻塞队列之中,一般来说,我们不能一直都等待,一般有以下几种处理方案:

  • 死等
  • 带超时等待
  • 让调用者放弃任务执行
  • 让调用者抛出异常
  • 让调用者自己执行任务

为了能够支持不同的拒绝策略,我们需要自己设计一个接口:

// 拒绝策略
@FunctionalInterface
interface RejectPolicy<T> {
    void reject(BlockingQueue<T> taskQueue, T task);
}

BlockingQueue 中添加一个尝试加入的方法:

public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
    lock.lock();
    try {
        // 判断队列是否满
        if (deque.size() == capcity) {
            rejectPolicy.reject(this, task);
        } else
            // 如果队列有剩余
        {
            log.info("尝试加入阻塞队列:{}", task);
            deque.addLast(task);
            emptyWaitSet.signal();
        }
    } finally {
        lock.unlock();
    }
}

ThreadPool 中添加一个拒绝策略的属性:

private RejectPolicy<Runnable> rejectPolicy;            // 拒绝策略

ThreadPool 中添加一个有参构造的方法:

public ThreadPool(int coreSize, Long timeout, TimeUnit timeUnit, int taskQueueSize, RejectPolicy<Runnable> rejectPolicy) {
    this.coreSize = coreSize;
    this.timeout = timeout;
    this.timeUnit = timeUnit;
    this.taskQueue = new BlockingQueue<>(taskQueueSize);
    this.rejectPolicy = rejectPolicy;
}

ThreadPool 中修改执行 execute 方法:

// 执行任务
public void execute(Runnable task) {
    // 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
    // 当任务数已经超过 coreSize 时,将 task 加入阻塞队列之中
    synchronized (workers) {
        if (workers.size() < coreSize) {
            Worker worker = new Worker(task);
            log.debug("新增 worker 执行 {}", worker, task);
            workers.add(worker);
            worker.start();
        } else {
            // taskQueue.put(task);
            taskQueue.tryPut(rejectPolicy, task);
        }
    }
}

紧接着,我们需要对自己设计的自定义线程池进行测试。

@Slf4j
public class ThreadPoolTest {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(5, 1000L, TimeUnit.MILLISECONDS, 1, (taskQueue, task) -> {
            // 1. 死等
            taskQueue.put(task);
            // 2) 带超时等待
            // taskQueue.offer(task, 1500L, TimeUnit.MILLISECONDS);
            // 3) 让调用者放弃任务执行
            // log.debug("放弃{}", task);
            // 4) 让调用者抛出异常
            // throw new RuntimeException("任务执行失败 " + task);
            // 5) 让调用者自己执行任务
            // task.run();
        });

        for (int i = 0; i < 10; i++) {
            int info = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("{}", info);
            });
        }
    }
}

7.10.2、ThreadPoolExecutor

7.10.2.1、线程池状态

ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量
状态名高3位接收新任务处理阻塞任务队列说明RUNNING111YY运行状态SHUTDOWN000NY不会接收新任务,但会处理阻塞队列剩余任务STOP001NN会中断正在执行的任务,并抛弃阻塞队列任务TIDYING010--任务全执行完毕,活动线程为 0 即将进入终结TERMINATED011--终结状态

从数字上比较:TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING

这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值。

// c 为旧值, ctlOf 返回结果为新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));

// rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) { return rs | wc; }
7.10.2.2、线程池构造
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler){}
  • corePoolSize:核心线程数目 (最多保留的线程数)
  • maximumPoolSize:最大线程数目
  • keepAliveTime:生存时间 - 针对救急线程
  • unit:时间单位 - 针对救急线程
  • workQueue:阻塞队列
  • threadFactory:线程工厂 - 可以为线程创建时起个好名字
  • handler:拒绝策略

线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入 workQueue 队列排队,直到有空闲的线程。如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现。

  • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
  • CallerRunsPolicy 让调用者运行任务
  • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
  • DiscardPolicy 放弃本次任务
  • Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
  • Netty 的实现,是创建一个新线程来执行任务
  • ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
  • PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略

高峰过去后,超过 corePoolSize 的救急线程,如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime 和 unit 控制。

7.10.2.3、线程池工厂方法

根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池。

(1)newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

特点:

  • 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
  • 阻塞队列是无界的,可以放任意数量的任务
  • 适用于任务量已知,相对耗时的任务

(2)newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

特点:

  • 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着全部都是救急线程(60s 后回收),救急线程可以无限创建
  • 救急线程可以无限创建队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的
  • 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况

(3)newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

特点:

  • 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放
  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池会新建一个线程,保证池的正常工作

注意:

  • Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改,FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
  • Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改,对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
7.10.2.4、线程池提交任务
// 执行任务
public void execute(Runnable command)

// 提交任务,用返回值 Future 获得任务执行结果
public <T> Future<T> submit(Callable<T> task)
public Future<?> submit(Runnable task)
public <T> Future<T> submit(Runnable task, T result)

// 提交 tasks 中所有任务
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
// 提交 tasks 中所有任务,带超时时间
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
7.10.2.5、线程池关闭方法
// 线程池状态变为 SHUTDOWN,不会接收新任务,但已提交任务会执行完,此方法不会阻塞调用线程的执行
public void shutdown()
// 线程池状态变为 STOP,不会接收新任务,会将队列中的任务返回,并用 interrupt 的方式中断正在执行的任务
public List<Runnable> shutdownNow()
// 不在 RUNNING 状态的线程池,此方法就返回 true
public boolean isShutdown()
// 线程池状态是否是 TERMINATED
public boolean isTerminated()
// 调用 SHUTDOWN 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可利用此方法等待
public boolean awaitTermination(long timeout, TimeUnit unit)
7.10.2.6、任务调度线程池
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.schedule(() -> {
    System.out.println("我执行啦1");
}, 1000, TimeUnit.MILLISECONDS);
pool.schedule(() -> {
    System.out.println("我执行啦2");
}, 1000, TimeUnit.MILLISECONDS);
pool.schedule(() -> {
    System.out.println("我执行啦3");
}, 1000, TimeUnit.MILLISECONDS);
pool.schedule(() -> {
    System.out.println("我执行啦4");
}, 1000, TimeUnit.MILLISECONDS);

scheduleAtFixedRate:(延时时间、间隔时间、时间单位),如果执行逻辑时间大于间隔时间,以执行逻辑时间为准。

ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.scheduleAtFixedRate(() -> {
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    log.info("我执行啦");
}, 1000, 1000, TimeUnit.MILLISECONDS);
16:08:44.178 [pool-1-thread-1] INFO com.caochenlei.Test40 - 我执行啦
16:08:46.200 [pool-1-thread-1] INFO com.caochenlei.Test40 - 我执行啦
16:08:48.207 [pool-1-thread-1] INFO com.caochenlei.Test40 - 我执行啦
16:08:50.215 [pool-1-thread-1] INFO com.caochenlei.Test40 - 我执行啦
16:08:52.221 [pool-1-thread-1] INFO com.caochenlei.Test40 - 我执行啦

scheduleWithFixedDelay:(延时时间、间隔时间、时间单位),如果执行逻辑时间大于间隔时间,以执行逻辑时间 + 间隔时间为准。

ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.scheduleWithFixedDelay(() -> {
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    log.info("我执行啦");
}, 1000, 1000, TimeUnit.MILLISECONDS);
16:10:34.122 [pool-1-thread-1] INFO com.caochenlei.Test40 - 我执行啦
16:10:37.157 [pool-1-thread-1] INFO com.caochenlei.Test40 - 我执行啦
16:10:40.167 [pool-1-thread-1] INFO com.caochenlei.Test40 - 我执行啦
16:10:43.174 [pool-1-thread-1] INFO com.caochenlei.Test40 - 我执行啦
16:10:46.192 [pool-1-thread-1] INFO com.caochenlei.Test40 - 我执行啦

7.10.3、Fork & Join

Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算。

所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解。

Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率。

Fork/Join 默认会创建与 cpu 核心数大小相同的线程池。

提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值)。

例如:下面定义了一个对 1~n 之间的整数求和的任务。

@Slf4j
public class MyTask extends RecursiveTask<Integer> {
    int begin;
    int end;

    public MyTask(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        if (begin == end) {
            log.info("join {}", begin);
            return begin;
        }

        int mid = (begin + end) / 2;
        MyTask myTask1 = new MyTask(begin, mid);
        myTask1.fork();
        MyTask myTask2 = new MyTask(mid + 1, end);
        myTask2.fork();

        return myTask1.join() + myTask2.join();
    }
}
public class MyTaskTest {
    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool(4);
        System.out.println(pool.invoke(new MyTask(1, 10)));
    }
}
21:41:05.165 [ForkJoinPool-1-worker-3] INFO com.caochenlei.fj.MyTask - join 6
21:41:05.165 [ForkJoinPool-1-worker-0] INFO com.caochenlei.fj.MyTask - join 3
21:41:05.165 [ForkJoinPool-1-worker-2] INFO com.caochenlei.fj.MyTask - join 1
21:41:05.165 [ForkJoinPool-1-worker-1] INFO com.caochenlei.fj.MyTask - join 4
21:41:05.174 [ForkJoinPool-1-worker-3] INFO com.caochenlei.fj.MyTask - join 7
21:41:05.174 [ForkJoinPool-1-worker-1] INFO com.caochenlei.fj.MyTask - join 5
21:41:05.174 [ForkJoinPool-1-worker-0] INFO com.caochenlei.fj.MyTask - join 2
21:41:05.174 [ForkJoinPool-1-worker-3] INFO com.caochenlei.fj.MyTask - join 8
21:41:05.174 [ForkJoinPool-1-worker-3] INFO com.caochenlei.fj.MyTask - join 9
21:41:05.174 [ForkJoinPool-1-worker-2] INFO com.caochenlei.fj.MyTask - join 10
55

第八章 网络编程

8.1、概述

计算机网络:是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统、网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。

网络编程:在网络通信协议下,实现网络互连的不同计算机上运行的程序间可以进行数据交换。

8.2、三要素

IP地址:是网络中设备的唯一标识,一般分两类:IPV4、IPV6

端口:设备上应用程序的唯一标识,常见的端口:8080、3306

协议:计算机网络中,连接和通信的规则被称为网络通信协议,常见的协议:UDP协议、TCP协议

8.3、UDP编程

UDP协议:用户数据报协议(User Datagram Protocol),无连接通讯协议,但不能保证传输数据完整性,常用于音频、视频、普通数据传输等。

8.3.1、发送数据

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class SendDemo {
	public static void main(String[] args) throws IOException {
		// 1.创建发送端套接字对象
		DatagramSocket ds = new DatagramSocket();
		// 2.创建数据并把数据打包
		byte[] bys = "Hello,World!".getBytes();
		DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("127.0.0.1"), 10086);
		// 3.从此套接字发送数据包
		ds.send(dp);
		// 4.关闭此数据包的套接字
		ds.close();
	}
}

8.3.2、接收数据

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class ReceiveDemo {
	public static void main(String[] args) throws IOException {
		// 1.创建接收端套接字对象
		DatagramSocket ds = new DatagramSocket(10086);
		// 2.创建数据包用于接收数据
		byte[] bys = new byte[1024];
		DatagramPacket dp = new DatagramPacket(bys, bys.length);
		ds.receive(dp);
		// 3.解析数据包并把数据输出
		System.out.println("数据是:" + new String(dp.getData(), 0, dp.getLength()));
		// 4.关闭此数据包的套接字
		ds.close();
	}
}

8.3.3、练习

  • 案例需求:

UDP发送数据:数据来自于键盘录入,直到输入的数据是886,发送数据才结束

UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收

  • 代码实现:
// 发送端代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class SendDemo {
	public static void main(String[] args) throws IOException {
		DatagramSocket ds = new DatagramSocket();
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String line;
		while ((line = br.readLine()) != null) {
			if ("886".equals(line)) {
				break;
			}
			byte[] bys = line.getBytes();
			DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("127.0.0.1"), 10086);
			ds.send(dp);
		}
		ds.close();
	}
}

// 接收端代码:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class ReceiveDemo {
	public static void main(String[] args) throws IOException {
		DatagramSocket ds = new DatagramSocket(10086);
		while (true) {
			byte[] bys = new byte[1024];
			DatagramPacket dp = new DatagramPacket(bys, bys.length);
			ds.receive(dp);
			System.out.println("数据是:" + new String(dp.getData(), 0, dp.getLength()));
		}
	}
}

8.4、TCP编程

TCP协议:传输控制协议 (Transmission Control Protocol),面向连接通讯协议、且可以保证传输数据完整性,常用于文件上传、文件下载、浏览网页等

8.4.1、发送数据

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class ClientDemo {
	public static void main(String[] args) throws IOException {
		// 1.创建客户端的Socket对象
		Socket s = new Socket("127.0.0.1", 10086);
		// 2.获取输出流对象,写数据
		OutputStream os = s.getOutputStream();
		os.write("Hello,World!".getBytes());
		// 3.释放资源
		s.close();
	}
}

8.4.2、接收数据

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo {
	public static void main(String[] args) throws IOException {
		// 1.创建服务端的ServerSocket对象
		ServerSocket ss = new ServerSocket(10086);
		// 2.侦听要连接到此套接字并接受它
		Socket s = ss.accept();
		// 3.获取输入流对象,读数据
		InputStream is = s.getInputStream();
		byte[] bys = new byte[1024];
		int len = is.read(bys);
		String data = new String(bys, 0, len);
		System.out.println("数据是:" + data);
		// 4.释放资源
		s.close();
		ss.close();
	}
}

8.4.3、练习

  • 案例需求

客户端:数据来自于键盘录入,直到输入的数据是886,发送数据才结束

服务端:因为接收端不知道发送端什么时候停止发送,故采用死循环接收

  • 代码实现:
// 客户端
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class ClientDemo {
	public static void main(String[] args) throws IOException {
		Socket s = new Socket("127.0.0.1", 10086);
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
		String line;
		while ((line = br.readLine()) != null) {
			if ("886".equals(line)) {
				break;
			}
			bw.write(line);
			bw.newLine();
			bw.flush();
		}
		s.close();
	}
}

// 服务端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo {
	public static void main(String[] args) throws IOException {
		ServerSocket ss = new ServerSocket(10086);
		Socket s = ss.accept();
		BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
		String line;
		while ((line = br.readLine()) != null) {
			System.out.println(line);
		}
		ss.close();
	}
}

8.5、文件上传

8.5.1、单线程文件上传

客户端代码:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class ClientDemo {
	public static void main(String[] args) throws IOException {
		// 创建客户端Socket对象
		Socket s = new Socket("127.0.0.1", 10086);
		// 封装文本文件的数据
		BufferedReader br = new BufferedReader(new FileReader("src\\ClientDemo.java"));
		// 封装输出流写出数据
		BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
		String line;
		while ((line = br.readLine()) != null) {
			bw.write(line);
			bw.newLine();
			bw.flush();
		}
		// 告诉服务器上传结束
		s.shutdownOutput();
		// 接收服务器端的反馈
		BufferedReader brClient = new BufferedReader(new InputStreamReader(s.getInputStream()));
		String data = brClient.readLine();
		System.out.println("服务器反馈:" + data);
		// 释放资源
		br.close();
		s.close();
	}
}

服务端代码:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;

public class ServerDemo {
	public static void main(String[] args) throws IOException {
		// 创建服务器Socket对象
		ServerSocket ss = new ServerSocket(10086);
		// 监听客户端的连接对象
		Socket s = ss.accept();
		// 获取上传文件随机名称
		String fileName = "src\\" + System.currentTimeMillis() + new Random().nextInt(999999) + ".java";
		// 保存客户端上传的数据
		BufferedInputStream bis = new BufferedInputStream(s.getInputStream());
		BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName));
		byte[] bys = new byte[1024];
		int len;
		while ((len = bis.read(bys)) != -1) {
			bos.write(bys, 0, len);
		}
		// 给客户端发出反馈信息
		BufferedWriter bwServer = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
		bwServer.write("文件上传成功");
		bwServer.newLine();
		bwServer.flush();
		// 释放资源
		bos.close();
		ss.close();
	}
}

8.5.2、多线程文件上传

客户端代码:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

public class ClientDemo {
	public static void main(String[] args) throws IOException {
		// 创建客户端Socket对象
		Socket s = new Socket("127.0.0.1", 10086);
		// 打开本地文件准备上传
		BufferedInputStream bis = new BufferedInputStream(new FileInputStream("src\\ClientDemo.java"));
		BufferedOutputStream bos = new BufferedOutputStream(s.getOutputStream());
		byte[] bys = new byte[1024];
		int len;
		while ((len = bis.read(bys)) != -1) {
			bos.write(bys, 0, len);
			bos.flush();
		}
		// 告诉服务器上传结束
		s.shutdownOutput();
		// 接收服务器端的反馈
		BufferedReader brClient = new BufferedReader(new InputStreamReader(s.getInputStream()));
		String data = brClient.readLine();
		System.out.println("服务器反馈:" + data);
		// 释放资源
		bis.close();
		s.close();
	}
}

服务端代码:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;

class ServerThread implements Runnable {
	private Socket s;

	public ServerThread(Socket s) {
		this.s = s;
	}

	@Override
	public void run() {
		try {
			// 获取上传文件随机名称
			String fileName = "src\\" + System.currentTimeMillis() + new Random().nextInt(999999) + ".java";
			// 保存客户端上传的数据
			BufferedInputStream bis = new BufferedInputStream(s.getInputStream());
			BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName));
			byte[] bys = new byte[1024];
			int len;
			while ((len = bis.read(bys)) != -1) {
				bos.write(bys, 0, len);
			}
			// 给客户端发出反馈信息
			BufferedWriter bwServer = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
			bwServer.write("文件上传成功");
			bwServer.newLine();
			bwServer.flush();
			// 释放资源
			bos.close();
			s.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

public class ServerDemo {
	public static void main(String[] args) throws IOException {
		ServerSocket ss = new ServerSocket(10086);
		while (true) {
			Socket s = ss.accept();
			ServerThread serverThread = new ServerThread(s);
			new Thread(serverThread).start();
		}
	}
}

8.6、Selector

阻塞

阻塞模式下,相关方法都会导致线程暂停。暂停期间不会占用 cpu,但线程相当于闲置。单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持,但多线程下,又有新的问题,体现在以下方面。32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低。可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间 inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接。

  • ServerSocketChannel.accept 会在没有连接建立时让线程暂停。
  • SocketChannel.read 会在没有数据可读时让线程暂停。

非阻塞

非阻塞模式下,相关方法都会不会让线程暂停。但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然在不断运行,白白浪费了 cpu。写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去。

  • ServerSocketChannel.accept 在没有连接建立时,会返回 null,继续运行。
  • SocketChannel.read 在没有数据可读时,会返回 0,但线程不必阻塞,可以去执行其它 SocketChannel 的 read 或是去执行 ServerSocketChannel.accept。

8.6.1、Selector 介绍

单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用。多路复用仅针对网络 IO、普通文件 IO 没法利用多路复用。如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证:

  • 有可连接事件时才去连接
  • 有可读事件才去读取,限于网络传输能力,Channel 未必时时可读,一旦 Channel 可读,会触发 Selector 的可读事件
  • 有可写事件才去写入,限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件

使用 Selector 的好处就是,一个线程配合 Selector 就可以监控多个 Channel 的事件,事件发生线程才去处理。避免非阻塞模式下所做无用功,让这个线程能够被充分利用,节约了线程的数量,减少了线程上下文切换。

8.6.2、Selector 创建

Selector selector = Selector.open();

8.6.3、绑定 Channel 事件

绑定 Channel 事件也称之为注册事件,绑定的事件 Selector 才会关心。

channel.configureBlocking(false);//非阻塞
SelectionKey key = channel.register(selector, 绑定事件);
  • Channel 必须工作在非阻塞模式,FileChannel 没有非阻塞模式,因此不能配合 Selector 一起使用

  • 绑定的事件类型可以有:

  • connect - 客户端连接成功时触发

  • accept - 服务器端成功接受连接时触发

  • read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况

  • write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况

8.6.4、监听 Channel 事件

可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件。

方法1,阻塞直到绑定事件发生

int count = selector.select();

方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)

int count = selector.select(long timeout);

方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件

int count = selector.selectNow();

select 何时不阻塞?

  • 事件发生时

  • 客户端发起连接请求,会触发 accept 事件

  • 客户端发送数据、客户端正常、异常关闭时,都会触发 read 事件,如果发送的数据大于 buffer 缓冲区,会触发多次读取事件

  • channel 可写,会触发 write 事件

  • 在 linux 下 nio bug 发生时

  • 调用 selector.wakeup()

  • 调用 selector.close()

  • selector 所在线程 interrupt

8.7、应用通信

8.7.1、单线程通信

8.7.1.1、服务器
@Slf4j
public class Server {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();//打开一个Selector
        ServerSocketChannel ssc = ServerSocketChannel.open();//打开一个Channel
        ssc.configureBlocking(false);//非阻塞
        ssc.register(selector, SelectionKey.OP_ACCEPT);//注册到Selector并监听ACCEPT事件
        ssc.bind(new InetSocketAddress(8080));//绑定端口
        log.debug("服务器启动成功!");
        while (true) {
            selector.select();//监听 Channel 事件
            Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
            while (selectionKeyIterator.hasNext()) {
                SelectionKey selectionKey = selectionKeyIterator.next();//获取当前SelectionKey
                //区分事件类型
                if (selectionKey.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel acceptChannel = channel.accept();//接收连接
                    acceptChannel.configureBlocking(false);//非阻塞
                    acceptChannel.register(selector, SelectionKey.OP_READ);//注册到Selector并监听READ事件
                    log.debug("客户端连接成功!");
                } else if (selectionKey.isReadable()) {
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(16);
                    int read = channel.read(buffer);
                    if (read == -1) {//正常断开连接,返回值为-1
                        selectionKey.cancel();
                        log.debug("客户端断开连接!");
                    } else {//读取发来数据,向客户端返回一个对应响应
                        buffer.flip();
                        log.debug("客户端发来数据:{}", StandardCharsets.UTF_8.decode(buffer));
                        selectionKey.interestOps(selectionKey.interestOps() + SelectionKey.OP_WRITE);
                        channel.write(StandardCharsets.UTF_8.encode("Hi"));
                        selectionKey.interestOps(selectionKey.interestOps() - SelectionKey.OP_WRITE);
                    }
                }
                selectionKeyIterator.remove();//移除当前SelectionKey
            }
        }
    }
}

注意:

  • 事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发!
  • select 在事件发生后,就会将相关的 selectionKey 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。
  • cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件。
8.7.1.2、客户端
@Slf4j
public class Client {
    public static void main(String[] args) throws IOException {
        SocketChannel channel = SocketChannel.open();
        channel.connect(new InetSocketAddress("localhost", 8080));
        channel.write(StandardCharsets.UTF_8.encode("Hello"));
        ByteBuffer buffer = ByteBuffer.allocate(16);
        StringBuffer sb = new StringBuffer();
        channel.read(buffer);
        buffer.flip();
        while (buffer.hasRemaining()) {
            sb.append((char) buffer.get());
        }
        log.debug("服务器返回数据:{}", sb);
        channel.close();
    }
}
8.7.1.3、控制台

Server

21:35:25 [DEBUG] [main] i.g.c.nio.Server - 服务器启动成功!
21:35:30 [DEBUG] [main] i.g.c.nio.Server - 客户端连接成功!
21:35:30 [DEBUG] [main] i.g.c.nio.Server - 客户端发来数据:Hello
21:35:30 [DEBUG] [main] i.g.c.nio.Server - 客户端断开连接!

Client

21:36:27 [DEBUG] [main] i.g.c.nio.Client - 服务器返回数据:Hi

8.7.2、存在的问题

8.7.2.1、如何处理消息边界

  • 一种思路是固定消息长度,数据包大小一样,服务器按预定长度读取,缺点是浪费带宽

  • 另一种思路是按分隔符拆分,缺点是效率低

  • TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量

  • Http 1.1 是 TLV 格式

  • Http 2.0 是 LTV 格式

服务器:

@Slf4j
public class ExpansionServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();//打开一个Selector
        ServerSocketChannel ssc = ServerSocketChannel.open();//打开一个Channel
        ssc.configureBlocking(false);//非阻塞
        ssc.register(selector, SelectionKey.OP_ACCEPT);//注册到Selector并监听ACCEPT事件
        ssc.bind(new InetSocketAddress(8080));//绑定端口
        log.debug("服务器启动成功!");
        while (true) {
            selector.select();//监听 Channel 事件
            Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
            while (selectionKeyIterator.hasNext()) {
                SelectionKey selectionKey = selectionKeyIterator.next();//获取当前SelectionKey
                //区分事件类型
                if (selectionKey.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel acceptChannel = channel.accept();//接收连接
                    acceptChannel.configureBlocking(false);//非阻塞
                    ByteBuffer buffer = ByteBuffer.allocate(4);
                    acceptChannel.register(selector, SelectionKey.OP_READ, buffer);//主要是关联附件
                    log.debug("客户端连接成功!");
                } else if (selectionKey.isReadable()) {
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    int read = channel.read(buffer);
                    if (read == -1) {//正常断开连接,返回值为-1
                        selectionKey.cancel();
                        log.debug("客户端断开连接!");
                    } else {//读取发来数据,向客户端返回一个对应响应
                        //用分隔符切分数据
                        split(buffer);
                        //看看是否需要扩容
                        if (buffer.position() == buffer.limit()) {
                            ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
                            buffer.flip();
                            newBuffer.put(buffer);
                            selectionKey.attach(newBuffer);
                        }
                        //向客户端返回数据
                        if (!buffer.hasRemaining()) {
                            selectionKey.interestOps(selectionKey.interestOps()+SelectionKey.OP_WRITE);
                            channel.write(StandardCharsets.UTF_8.encode("Hi"));
                            selectionKey.interestOps(selectionKey.interestOps()-SelectionKey.OP_WRITE);
                        }
                    }
                }
                selectionKeyIterator.remove();//移除当前SelectionKey
            }
        }
    }

    private static void split(ByteBuffer source) {
        source.flip();
        for (int i = 0; i < source.limit(); i++) {
            if (source.get(i) == '\n') {
                int length = i + 1 - source.position();
                ByteBuffer target = ByteBuffer.allocate(length);
                for (int j = 0; j < length; j++) {
                    target.put(source.get());
                }
                target.flip();
                log.debug("{}", StandardCharsets.UTF_8.decode(target));
            }
        }
        source.compact();
    }
}

客户端:

@Slf4j
public class ExpansionClient {
    public static void main(String[] args) throws IOException {
        SocketChannel channel = SocketChannel.open();
        channel.connect(new InetSocketAddress("localhost", 8080));
        channel.write(StandardCharsets.UTF_8.encode("Hello,world\nI'm zhangsan\nHo"));
        channel.write(StandardCharsets.UTF_8.encode("w are you?\n"));
        ByteBuffer buffer = ByteBuffer.allocate(16);
        StringBuffer sb = new StringBuffer();
        channel.read(buffer);
        buffer.flip();
        while (buffer.hasRemaining()) {
            sb.append((char) buffer.get());
        }
        log.debug("服务器返回数据:{}", sb);
        channel.close();
    }
}

控制台:

ExpansionServer

22:05:18 [DEBUG] [main] i.g.c.n.ExpansionServer - 服务器启动成功!
22:05:37 [DEBUG] [main] i.g.c.n.ExpansionServer - 客户端连接成功!
22:05:37 [DEBUG] [main] i.g.c.n.ExpansionServer - Hello,world

22:05:37 [DEBUG] [main]

相关文章

微信公众号

最新文章

更多

目录