当前位置:   article > 正文

个人Java学习笔记_java的int与&0x100

java的int与&0x100

变量的作用域:

  全局变量:

定义方法(函数)和代码块之外的变量

局部变量:

定义在方法或者代码之间的变量

静变量

数据类型:

整型、长整形、短整型 、单精度浮点型、 双、布尔、 字节、 字符型、

数据类型转换

     Boolean类型不参与转换

char和short可以相互转换(互相强转)

小类型转换成大类型(自动转换)

强制类型转换

当大类型数据转换成小类型需要强制转换(类型强转)

 语法:小类型 变量名 =(需要转换的类型)大类型

引用 数据类型不能与基本数据类型相互转换;两种数据类型要彼此兼容,否则不能互相转换。

引用数据类型转换需是继承关系,

任何数据类型与字符串做+运算都会变成字符串。

Java算术运算符

        1、一元运算:

                  -、++、--、(++在前,先自增再赋值)

        2、二元运算符:

                  加、减、乘、除、取余、

        3、逻辑或不论后面是真是假都会往后走。

  Java关系运算符

           1、基本类型的变量、值不能与引用类型的变量、值使用==进行作比较;

            2、

           3、

栈 通常用来存放方法

equals(obj)方法和==的区别:

          equals方法比较的是两个对象的内容是否相等

         ==比较的是两个 对象在内存中存放的地址是否为同一个;

  位移运算符

只有<<(向左位移)          >>(向右位移)   。无<<<或者>>>等符号

next()与nextLine()方法的区别;

1.当遇到空格或者回车的时候,next()停止读取

2.当遇到回车的时候nextLine()方法停止读取,读取整行数据

一、int类型的直接量

在程序中直接给出的整型数值,可分为二进制、十进制、八进制和十六进制4种,其中二进制需要以0B或Ob开头,八进制需要以

0开头,十六进制需要以0x或OX开头。例如123、012(对应十进制的10)、0x12(对应十进制的18)等。

二、long 类型的直接量

在整型数值后添加 | 或 L后就变成了long 类型的直接量。例如3L、0x12L(对应十进制的18L),

三、float 类型的直接量

在一个浮点数后添加f 或 F 就变成了 float 类型的直接量,这个浮点数可以是标准小数形式,也可以是科学计数法形式。例如

5.34F、3.14E5f。

四、double 类型的直接量

直接给出一个标准小数形式或者科学计数法形式的浮点数就是 double 类型的直接量。例如5.34、3.14E5。

五、boolean 类型的直接量

这个类型的直接量只有true 和 false。

char类型的直接量

char 类型的直接量有三种形式,分别是用单引号括起来的字符、转义字符和 Unicode 值表示的字符。例如'a',\n'和\u0061'。

                                     Java课堂笔记

Java常量

常量是指在程序的整个运行过程中值保持不变的量。

常量值

常量值又称为字面常量,它是通过数据直接表示的,因此有很多种数据类型,像整型和字符串型等。

整型常量值

整型常量值主要有如下3种形式。

十进制数形式:如54、-67、 0。

八进制数形式: Java 中的八进制常数的表示以0开头,如0125表示十进制数85, -013 表示十进制数-11。

十六进制数形式: Java中的十六进制常数的表示以0x或0X开头,如0x100表示十进制数256, -0x16 表示十进制数-22。

整型(int) 常量默认在内存中占32位。

实型常量值

Java的实型常量值主要有如下两种形式。

十进制数形式:由数字和小数点组成,且必须有小数点,如12.34、-98.0。

科学记数法形式:如1.75e5或32&E3,中e或E之前必须有数字,且e或E之后的数字必须为整数。

Java实型常量默认在内存中占64位,是具有双精度型(double) 的值。

单精度型数值一般要在该常数后面加 F或f,如69.7f, 表示-个float型实数,它在内存中占32位。

布尔型常量值

Java的布尔型常量只有两个值,即false (假)和true (真)。

Java变量的作用域

变量的作用域规定了变量所能使用的范围,只有在作用域范围内才能被使用。根据变量的声明地点不同,变量的作用域也不同

变量又分为成员变量与局部变量

成员变量分为

  1. 全局变量:无static修饰访问对象名变量名
  2. 静态变量:用static修饰,访问类名变量名

局部变量分为

  1. 方法参数变量:在整个方法内有下效
  2. 方法局部变量:从定义这个变量开始到方法结束这一段时间内有效
  3. 代码块局部变量:从定义这个变量开始到代码结束这一段时间内有效

局部变量在使用前必须被程序员主动初始化值

声明变量

首字符必须是字母、下划线、美元符号、人民币符号

标识符由数字大写字母小写字母下划线美元符号人民币符号以及所有在十六进制Oxco前的ASCLL码组成,不能吧关键字保留字作为标识符

定义常量

常量不同于常量值,它可以在程序中用符号来代替常量值使用,因此在使用前必须先定义。常量与变量类似也需要初始化,即在声明 常量的同时要赋予一个初始值。常量一旦初始化就不可以被修改。

final dataType variab1eName = value

其中,final 是定义常量的关键字,dataType 指明常量的数据类型,variableName 是变量的名称, value 是初始值。final关键字表示最终的,它可以修改很多元素,修饰变量就变成了常量。例如,以下语句使用final关键字声明常量。

Java中的数据类型:

 1、基本数据类型(八种)

byte(字节型)、short(短整型)、int(整形)、long(长整型)、float(单精度浮点型)、double(双精度浮点型)char(字符型)、boolean(布尔型)

字节型:byte by = 127;

 // System.out.println((byte) (by + 1));

 短整型:short sh = 56;

 整形: int num = 78946;

 长整型,注意: 需要早长整型末尾添加L或者l:long number = 56L;

 单精度浮点型, 注意: 需要早长整型末尾添加F或者f;float fl = 3.14F

 System.out.println(fl1);

 双精度浮点型,java中小数默认是double类型;double db = 3.14159;

 字符型, 字符型的值使用单引号引起来,当值为整形时不需要单引号

        char ch1 = 3;

        char ch2 = 'a';

        char ch3 = 97;

        System.out.println(ch1);

        System.out.println(ch3);

 布尔值类型,注意:Java中布尔值类型只有true和false两个值

        boolean bool1 = true;

        boolean bool2 = false;

2、引用数据类型

除了基本数据类型,其他全部是引用数类型,如:String、数组等

Java逻辑运算符

  1. &&   短路与:a && b  (a和b同时为真时为真)

  1. ||     短路或:a && b (a和b有一个为真时为真)

  1. &    逻辑与:同&&
  2. |     逻辑或:同||
  3. !    逻辑非:!a      (a为真时,值为假;a为假时,值为真)

短路与、短路或和逻辑与、逻辑或的区别:

  1. a && b :如果 a fase,则不计算 b(因为不论 b 为何值,结果都为 fase
 
  1. a || b :如果 a true,则不计算 b(因为不论 b 为何值,结果都为 true

  1. a & b   a无论是truefaseb都会进行计算

  1. a | b   a无论是truefaseb都会进行计算

创建扫描器对象

Scanner scan = new Scanner(System.in);

System.out.println("请输入您的名字:");

name = scan.next();

next()与nextLine()方法的区别:

    1、当遇到空格或者回车的时候 next()方法停止读取

2、当遇到回车的时候 nextLine()方法停止读取,读取整行数据

1.二位数组用数据类型 [][] = new 数据类型[][];

例如: int[][] arr = new int[2][3];

2.遍历二位数组;

可以用两个for-each,例如for(int i:arr[])for(int j:arr);遍历二维数组;

3.访问二维数组的语法:arr[x][y] = 1;就是给第x + 1行第y + 1列的值赋成1;

4.二维数组的存储,大概是arr[x][y]中,x存的是二维数组的索引,也就是数组的地址,x存在栈,y存在堆区;static修饰的存在方法区;在类中,static修饰可以直接用类名访问,用对象访问会报警告;      

5.System.arraycopy,src:原数组;srcPos: 原数组起始位置(从这个位置开始复制);dest:目标数组;destPos:目标数组粘贴的起始位置;length: 复制的个数

6.方法的重载是方法名相同,参数列表不同,返回值不做要求;

7.Arrays.copyOf(arr[],7);即arr【】为目标数组,7为新复制的数组长度;

8.数组的扩容与缩容量,使用int aee1[] = Arrays.copyOf(arr,5);

其中arr为原来的数组,5位新数组的大小;增容的操作也类同;这里并没有销毁原来的数组,准确的应该算复制;

9.Arrays.sort能对数组进行排序,返回值为int,返回数据为目标值的下标,二分查找的使用的前提是数组必须排好序;

10,使用equals能比较两个数组是否相等;两个参数分别是一个数组和另一个数组,如果相同返回true,如果不同返回false;

if语句

if双分枝结构:为条件表达语句,当语句为ture时执行语句,当语句为false,执行else语句。

嵌套switch语句

if语句与switch语句都表示条件语句,可以使用效率与实用性两方面加以区分。

从效率上看

对同一个变量的不同值作为条件判断时,当判断条件较多时可以使用switch语句

从实用性上看

当判断条件不多时使用if语句更加方便

while循环与do...while循环的区别:

 1、while是先判断后执行,do...while循环是先执行然后再判断

  2、do...while循环至少执行一次

  while循环与do...while的特点:都是循环执行某一语句,循环次数素不固定

   while循环:

   语法:

  while (循环条件) {

   循环体}

   案例:计算1+到100的和

  1+2+3+4+5+6+7+....+100

        int num = 1;

        int sum = 0;

        while (num <= 100) {

            sum += num;

            num ++;

        }

        System.out.println(sum);

 do.while循环

      语法:

     do{

           循环体

        } while (循环条件);

    案例:计算1+到100的和

       1+2+3+4+5+6+7+....+100

        int num2 = 1;

        int sum2 = 0;

        do {

            sum2 += num2;

            num2 ++;

        } while (num2 <= 100);

        System.out.println(sum2);

  for循环嵌套

如果循环嵌套层数超过了三层,那么一定是逻辑出了问题

 数组定义:

    用来存放相同类型的一组数据

     数组下标从0开始,对数组元素进行操作是通过数组的下标(索引)

     数组一旦被创建,则数组的长度不可被修改

       语法:

          静态创建数组

           数据类型[] 数组名 = {值1,值2....}

            数据类型 数组名[] = {值1,值2....}

 数组元素必须是相同类型的,不允许出现混合类型。
数组的默认初始化、
int a2[] = new int[2];//默认值00
boolean[] b = new boolean[2];//默认值 falsefalse
String[] s = new String[2];//默认值null
随机数:
     1、Random random = new Random();
           生成指定范围内的随机数 random.nextInt(10)
     2、double random = Math.random();
             生成随机数的范围 0.0-1.0,但是永远到不了 

       动态创建数组

    数据类型[] 数组名 = new 数据类型[数组长度]

        数据类型 数组名[] = new 数据类型[数组长度]

     

    // 静态创建数组,特点:创建数组的同时给数组元素赋值

      int arr[] = {1, 2, 4, 8, 9};

   // 动态创建数组,特点创建数组时指定数组的长度

    long[] arr1 = new long[5];

 // 数组元素必须是相同类型的,不允许出现混合类型。

    // dArr[2] = "hello array"; 数组元素类型与数组类型不匹配,语法报错

     二维数组的动态初始化: .

数据类型数组名00 = new数据类型[m][n]数据类型00数组名= new数据类型[m][n]

数据类型0数据名0 = new数据类型[m]In]数组的复制:浅拷贝

System.arrarcoplarr,start,dist,index.length)

Arrays,copofrr,length)

4、Arrays类 具有以下常用的功能

1、给数组赋值:通过调用fill方法

2、对数组排序:通过sort方法

3、比较数组:通过equals方法比较数组中元素的值是否相等

5、数组的扩容于缩容

实质:就是创建一个新的数组,新数组避原来的数组(大,为扩容,小,为缩容)打印数组Arrays. toStingarr)方法的作用是将数组- -字符串的形式输出

给数组赋值:

通常用il方法

Arrays binarySercharr9);该方法的返回值为查找到的元素下标的值

Java面向对象- 0OP

一、什么是面向对象

面向过程与面向对象:

面向对象过程思想:

1、步骤清晰简单

2、面向过程合适处理一些较为简单的问题

面向对象思想的本质就是:以类的方式组织代码,以对象的组织(封装) 数据类是对象的抽象,对象是类的实例。

什么是类:类型/类别,代表-类个体

对象:真实存在的单个个体;

类中包含: 1、所有对象所共有的属性/特征-- -成员变量

2、所有对象所共有的行- -方法

一个类可以创建多个对象

同一类型所创建的对象,结构相同,数据类型不同。

类是对象的模板,对象是类的具体实例

面向对象的三大特征:

封装,继承,多态类的定义:,

修饰符class 类名{成员变量类型变量名 ;

访问对象成员变量和方法,语法:

对象名.成员变量名字/方法名字,能点出什么东西全看类有扫描东西

00A :面向对象分析法

面向对象设计

一个Java文件中只能有一个public修饰的Java类,并且这个被public修饰的Java类名必须与Java文件名字-致。

创建对象的语法:

数据类型

引用类型变量

指向

new关键字对象

Student

new

造成空指针异常的原因:是因为对象为null定义类中的方法:

class类名{

修饰符返回值类型 方法名(参数列表) {

创建并且使用对象:

使用new关键字创建对象:new的运算语法为:

new类名()

# Java面向对象——OOP

# 补充:

包机制的作用:

​为了更好的组织类,Java提供了包机制,用于区分类的命名空间,包语句的语法格式为:

```java

package  包名1.包名2.……

```

一般利用公司的域名倒置作为包名。

```java

import  完整的包名.类名;

```

**注:**

package语句必须位于Java源文件中的第一行,否则编译不通过。

# 一、什么是面向对象

**面向过程与面向对象:**

​**面向过程思想:**

​        1、 步骤清晰简单,第一步要做什么,第二步要做什么……

​        2、 面向过程适合处理一些较为简单的问题

​        **面向对象思想:**

​        1、 物以类聚,分类的思维模式,思考问题首先会解决问题需要哪些分类,然后对这些分类进行单独思考,最后,才对某个分类下的细节进行面向过程的思索。

​        2、 面向对象适合处理复杂的问题,适合处理需要多人协作的问题

**面向过程的结构化程序设计:**

​        结构化程序设计的弊端

​       1、缺乏对数据的封装

​        2、数据与方法(操作数据)的分离

**什么是面向对象:**

​        面向对象的本质就是:以类的方式组织代码,以对象的组织(封装)数据。

**什么是抽象的数据类型:**

​        所谓抽象数据类型可以理解为:将不同类型的数据的集合组成一个整体,用来描述一种新的事务。如:对象类型数据

**什么是?什么是对象?**

​        1、现实世界是由很多很多对象组成的

               基于对象抽出了类

​        2、对象:真实存在的单个的个体

​                 类:类型/类别,代表一类个体

​        3、类中可以包含:

                3.1、所有对象所共有的属性/特征------------成员变量

               3.2、所有对象所共有的行为-----------------方法

​        4、一个类可以创建多个对象

​               同一类型所创建的对象,结构相同,数据不同

​        5、类是对象的模板,对象是类的具体的实例

**面向对象的三大特征:**

​        封装、继承、多态

<b style='color: red'>补充:</b>

​        **OOA**

​        **Object-Oriented Analysis:面向对象分析法**

        指的是在一个系统的开发过程中进行了系统业务调查以后,按照面向对象的思想来分析问题。OOA与结构化分析有较大的区别。OOA所强调的是在系统调查资料的基础上,针对OO方法所需要的素材进行的归类花分析和整理,而不是对管理业务现状和方法的分析。

OOA(面向对象的分析)模型由5个层次(主题层、对象类层、结构层、属性层和服务层)和5个活动(标识对象类、标识结构、定义主题、定义属性和定义服务)组成。在这种方法中定义了两种对象类之间的结构,一种称为分类结构,一种称为组装结构。分类结构就是所谓的一般与特殊的关系。组装结构则反映了对象之间的整体与部分的关系

​        OOA在定义属性的同时,要识别实例连接。实例连接是一个示例与另一个实例的映射关系。

​        OOA在定义服务的同时要识别消息连接。当一个对象需要向另一个对象发送消息时,它们之间就存在消息连接。

​        OOA中的5个层次和5个活动继续贯穿在OOD(面向对象设计)过程中。OOD模型由4各部分组成。它们分别是设计问题域部分、设计人机交互部分、设计任务管理部分、和设计数据管理部分。

**一、OOA的主要原则。**

​        1)**抽象:**从许多食物中舍弃个别的、非本质的特征,抽取共同的、本质性的特征,就叫做抽象。愁乡石形成概念的必须手段。

​               抽象原则有两个方面的意义:第一,尽管问题域中的事物是很复杂的,但是分析员并不需要了解和描述它们的一切,只需要分析      其中与系统目标有关的事物及其本质性特征。第二,通过舍弃个体事物在细节上的差异,抽取其共同特性而得到一批事物的抽象概      念。

​               抽象是面向对象方法中使用最为广泛的原则。抽象原则包括过程抽象和数据抽象两个方面。

​               **过程抽象**是指,任何一个完成确定功能的操作序列,其使用者都可以把它看做一个单一的实体,尽管实际上它可能是由一系列更      低级的操作完成的。

​               **数据抽象**是根据施加于数据之上的操作来定义数据类型,并限定数据的值只能由这些操作来修改和观察。数据抽象是OOA的核      心原则。它强调把数据(属性)和操作(服务)结合为一个不可分的系统单位(即对象),对象的外部只需要知道它做什么,而不必       知道它如何做。

​        2)**封装**就是把对象的属性和服务结合为一个不可分的系统单位,并尽可能隐蔽对象的内部细节。

​        3)**继承:**特殊类的对象拥有的其一般类的全部属性与服务,称作特殊类对一般类的继承。(父子

​        在OOA中运用继承原则,就是在每个由一般类和特殊类形成的一半----特殊结构总,把一般类的对象实例和所有特殊类的对象实        例都共同具有的属性和服务,一次性的在一般类中进行显式的定义。在特殊类中不在重复的定义一般类中已定义的东西,但是在语义        上,特殊类却自动的、隐含地拥有它的一般类(以及所有更上层的一般类)中定义的全部属性和服务。继承原则的好处是:是系统模      型比较简练也比较清晰。

​        4)**分类:**就是把具有相同属性和服务的对象划分为一类,用类作为这些对象的抽象描述。分类原则实际上是抽象原则运用于对象描      述时的一种表现形式。

​        5)**聚合:**又称组装,其原则是:把一个复杂的事物看成若干比较简单的事物组装体,从而简化对复杂事物的描述。

​        6)**关联:**是人类思考问题时经常运用的思想方法:通过一个事物联想到另外的事物。能使人发生联想的原因是事物之间确实存在着某些联系。

​        7)**消息通信:**这一原则要求对象之间只能通过消息进行通信,而不允许在对象之外直接地存取对象内部的属性。通过消息进行通信是由于封装原则而引起的。在OOA中要求消息连接表示出对象之间的动态联系。

​        8)**粒度控制:**一般来讲,人在面对一个复杂的问题域时,不可能在同一时刻既能纵观全局,又能洞察秋毫。因此需要控制自己的视野:考虑全局时,注意其大的组成部分,暂时不详查每一部分的具体的细节:考虑某部分的细节时则暂时撇开其余的部分。着就是粒度控制原则

​        9)**行为分析:**显示世界中事物的行为是复杂的。由大量的事物所构成的问题域中各种行为旺旺相互依赖交织

**二、面向对象分析产生三种模型**

​        1、**对象模型:**对用例模型进行分析,把系统分解成互相协作的分析类,通过类图\对象图描述对象\对象的属性\对象间的关系,是系统的静态模型

​        2、**动态模型:**描述系统的动态行为,通过时序图/协作图/描述对象的交互,以揭示对象间如何协作来完成每个具体的用例。单个对象的状态变化/动态行为可以通过状态图来表示。

​        3、**功能模型**(即用例模型à作为输入)

​​    ​       

​       

​        封装也叫作信息封装,确保组件不会以不可预期的方式改变其它组件内部状态,只有在那些提供了内部状态改变方法的组建中,才可以访问其内部状态。每类组件都提供了一个与其它组件联系的接口,并规定了其它组件进行调用的方法。

​        多态性:组件的引用和类集会涉及到其它许多不同类型的组件,而且引用组件所产生的的结果得依据实际调用的类型。

​        继承性:允许在现存的组件基础上创建子类组件,着统一并强调了多态性和封装性。典型的来说就是用类来对组件进行分组,而且还可以定义新类为现存的类的扩展,这样就可以将类组织成树形或网状结构,这体现了动作的通用性。

# 二、类与对象的创建

**定义一个:**

​        **定义类的成员变量:**

​        1、类的定义包括“成员变量”的定义和“方法”的定义,其中“成员变量”用于描述对象  共同的数据结构,“方法”则是所有对象共同的行为。

​        2、Java语言中,类的成员变量的定义可以使用如下的语法:

```java

修饰符 class  类名 {

      成员变量类型   变量名;

      ………

}

```

​        3、对象创建后,其成员变量可以按照默认的方式初始化,初始化对象成员变量时,其默认值的规则如下表所示:

| **成员变量类型**                                  | **默认初始值** |

| ------------------------------------------------- | -------------- |

| 数值类型(byte、short、int、long、float、double) | 0              |

| boolean类型                                       | false          |

| char类型                                          | /u0000         |

| 引用类型                                          | null           |

**定义类的方法:**

类中除了定义成员变量,还可以定义方法,用于描述对象的行为,封装对象的功能,Java语言中,可以按照如下方式定义类中的方法:

```java

class  类名 {

    修饰符  返回值类型   方法名(参数列表){

         方法体………

    }

    ……………

}

```

**创建并且使用对象:**

​        **使用new关键字创建对象:**

​        类定义完成之后,可以使用new关键字创建对象,创建对象的过程通常称为实例化对象,new的运算语法为:

```java

new   类名();

如:new  JFrame(); //可以创建一个窗体对象

创建对象语法:

数据类型   引用类型变量   指向  new关键字        对象

Student       zs        =    new           Student();

```

**引用类型变量:**

​    为了能够实例化对象进行访问控制,需要使用一个特殊的变量——引用。

​    引用类型变量可以存放该类对象的地址信息,通常称为“指向该类的对象”,当一个引用类型变量指向该类的对象时,就可以通过这个变量对对象实施访问。

​        除了8种基本数据类型之外,用类、接口、数组等声明的变量都称为引用类型变量,简称“引用”。

**访问对象的成员变量,调用方法:**

​        可以通过引用访问对象的成员变量或调用方法。

```java

创建cell类:

public class Cell {

    // 定义属性

    int row;

    int col;

   

    //定义方法

    public void drop() {}

    public void moveLeft(int len) {}

    public String getCellInfo() {

        return "";

    }

}

创建Cell类并访问成员变量及方法

Cell  c  =  new  Cell();

c.row  =  3;

c.col   =  2;

c.drop();

c.moveLeft(2);

String   str   =   c.getCellInfo();

```

**引用类型变量的赋值:**

​        1、引用类型变量存储的是对象的地址信息,相同类型的引用类型变量之间也可以互相赋值。

​        2、引用类型变量之间的赋值不会创建新的对象,但有可能会使两个以上的引用指向同一个对象。例如:

```java

Emp  e1  =  new  Emp();

Emp  e2  =  e1; // 将e1的值(对象地址信息)赋给e2,此时e1和e2指向相同对象

**null和NullPointerException:**

​    1、对于引用类型变量。可以对其赋值为null,null的含义为“空”,表示还没有指向任何对象,例如:

```java

Emp  emp  =  null; //emp为的值null,没有指向任何对象

emp  =  new  Emp(); //emp指向了一个Emp对象

```

​        2、当一个引用变量的值为null的时候,如果通过引用访问对象成员变量或者调用方法不符合逻辑时,会产生NullPointerException,例如:

```java

int[]  arr  =  null;

arr.length;

# 四、Java内存分析

## 1、堆、栈、方法区:

**堆:** new出来的对象(包括实例变量)

**栈:** 局部变量(包括方法的参数)

**方法区:** .class字节码文件(包括方法、静态变量)

## 2、堆,栈,方法区,常量池 的位置分布图:

<img src="06-面向对象(OOP).assets\image-20220913225751369.png" alt="image-20220913225751369" style="zoom:80%;" />

## 3、内存区域类型

​        **1.寄存器:**最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制;

​        **2. 堆:**存放所有new出来的对象;

​        **3. 栈**:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(对象可能在常量池里)(字符串常量对象存放在常量池中。);

​        **4. 静态域:**存放静态成员(static定义的);

​        **5. 常量池:**存放字符串常量和基本类型常量(public static final)。有时,在嵌入式系统中,常量本身会和其他部分分割离开(由于版权等其他原因),所以在这种情况下,可以选择将其放在ROM中 ;

​        **6. 非RAM存储:**硬盘等永久存储空间

1.内存管理:由JVM来管理的

​        **1) 堆:**

​           1.1) 存储new出来的对象(包括实例变量)

​           1.2) 垃圾: 没有任何引用所指向的对象

​            **垃圾回收器(GC)**不定时到内存中清理垃圾,

​            回收过程是透明的(看不到的),不一定一发现垃圾就立刻回收,

​            调用System.gc()可以建议虚拟机尽快调度GC来回收

            1.3) 内存泄漏: 不再使用的内存没有被及时的回收

             建议: 对象不再使用时及时将引用设置为null

        1.4) 实例变量的生命周期:

​        在创建对象时存储在堆中,对象被回收时一并被回收

        **2) 栈:**

               2.1) 存储正在调用的方法中的所有局部变量(包括方法的参数)

                2.2) 调用方法时,会在栈中为该方法分配一块对应的栈帧,

​                          栈帧中存储方法中的局部变量(包括参数),

​                           方法调用结束时,栈帧被清除,局部变量一并被清除

               2.3) 局部变量的生命周期:

​                           方法被调用时存储在栈中,方法结束时与栈帧一并被清除

        **3) 方法区:**

​               3.1) 存储.class字节码文件(包括静态变量、方法)

​               3.2) 方法只有一份,通过this来区分具体的调用对象

# 六、访问控制修饰符

## 1、public和private

​        private修饰的成员变量和方法仅仅只能在本类中访问调用。

​        public修饰的成员变量和方法可以在任何地方调用,public修饰的内容是对外提供可以被调用的功能,需要相对稳定,private修饰的内容是对类实现的封装,如果“公开”会增加维护的成本。

## 2、 protected和默认访问控制

​        1)用protected修饰的成员变量和方法可以被子类及同一个包中的类使用。

​        2)默认的访问控制即不书写任何访问修饰符,默认访问控制的成员变量和方法可以被同一个包中的类调用。

## 3、访问控制修饰类

​        1)对于类的修饰可以使用public和默认的方式,public修饰的类可以被任意一个类使用,默认方法控制的类只能被同包中的类使用。

​        2)protected和private可以用于修饰内部类

# 七、封装

​        封装,简单的说就是该露的露,该藏的藏我们在设计程序是要追求“高内聚,低耦合”,其中,高内聚指的是类的内部数据操作细节由自己完成,不允许外部干涉。低耦合指的是仅暴露少量的方法给外部调用(使用get/set方法)。

​        封装(对数据的隐藏),通常来说,应禁止直接访问应该对象中数据的实际表示,而是应该通过操作接口来访问,这种称为信息隐藏。

**封装的意义:**

​        1、对外提供可调用的,稳定的功能。

​        2、封装容易变化的,具体的细节,外界不可访问

这样封装的意义在于:

​            a. 降低代码出错的可能性,便于维护。

​          b. 当内部的实现细节改变时,只要保证对外的功能定义不变,其他的模块就不会因此而受到牵连。

# 八、继承

## 1、泛化的过程

## 2、extends关键字

​        1、通过extends关键字可以实现类的继承

​        2、子类可以继承父类的成员变量及成员方法,同时也可以定义自己的成员变量和成员方法。

​        3、Java语言不支持多重继承,一个类只能继承一个父类,但是一个父类可以有多个子类。

例如:

<img src="06-面向对象(OOP).assets\image-20220913231357154.png" alt="image-20220913231357154" style="zoom:80%;" />

## 3、继承中的构造方法

​        1、子类的构造方法中必须通过super关键字调用父类的构造方法,这样可以妥善的初始化继承自父类的成员变量。

​        2、如果子类的构造方法中没有调用父类的构造方法,Java编译器会自动的加入对父类的无参构造方法的调用(如果父类没有无参构造方法,则会有编译错误)。

## 4、父类的引用指向子类的对象

​        1、一个子类的对象可以向上造型为父类的类型,即,定义父类型的引用可以指向子类型的对象。

​        2、 父类的引用可以指向子类的对象,但是通过父类的引用只能访问父类所定义的成员,不能访问子类扩展的部分。

例如:

# 九、super关键字

**在java里面,对于super关键字通常有两种用法:**

​        1. 用在子类的构造方法里(初始化用),主要是调用父类的默认构造方法,如果父类有不止一个构造方法,可以通过super指定具体的构造函数,比如 super(paras)。

​        2. 用在子类里调用隐藏或重写的属性或行为,比如 super.onDestroy()等等。

​        对于第1种需要注意,super表示当前类的父类对象,super()调用的是父类默认的构造方法,即这样可以对父类进行初始化。如果没有对父类进行初始化,当子类调用父类的方法时,便会从逻辑上出现错误,因为没对父类初始化,父类的方法和属性便没有内存空间。

 **关于super 与 this 关键字的对比(区别):**

​        1. super(参数):调用基类中的某一个构造函数(应该位于子类构造函数中的第一条语句)。

​        2. this(参数):调用本类中的构造函数(应该位于构造函数中的第一条语句)。

​        3. super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参)。

​        4. this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名)

​        5、调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。

​        6、super()和this()类似,区别是,super()从子类中调用父类的构造方法,this()在同一类内调用其它构造方法。

​        7、super()和this()均需放在构造方法内第一行。

​        8、尽管可以用this调用一个构造器,但却不能调用两个。

​        9、this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。

​        10、 this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。

​        11、 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。

# 十、方法重写

**方法的重写(Override):**

​    1、发生在父子类中,方法名称相同,参数列表相同,方法体不同

​    2、重写方法被调用时,看对象的类型

​    3、遵循"两同两小一大"原则:------------了解

​    3.1、两同:

​    3.1.1、方法名称相同

​    3.1.2、参数列表相同

3.2、两小:

​        3.2.1、派生类方法的返回值类型小于或等于超类方法的

​        a、void时,必须相同

​        b、基本数据类型时,必须相同

​        c、引用数据类型时,小于或等于

​        3.2.2、派生类方法抛出的异常小于或等于超类方法的

​               3.3、一大:

​        3.3.1、派生类方法的访问权限大于或等于超类方法的

**重写与重载的区别:**

​        1、重写(Override):

​        1.1、发生在父子类中,方法名相同,参数列表相同,方法体不同

​        1.2、"运行期绑定",看对象的类型绑定方法

​        2、重载(Overload):

​        2.1、发生在一个类中,方法名相同,参数列表不同,方法体不同

​        2.2、"编译期绑定",看参数/引用的类型绑定方法

<p style="color: red">补充:</p>

​        编译期:.java源文件,经过编译,生成.class字节码文件

​        运行期:JVM加载.class并运行.class

​        编译错误:只是检查语法

# 十一、多态

​        多态指的是同一方法可以根据发送对象的不同而采用多种不同的行为方式。

​        一个对象的实际类型是确定的,但是可以指向对象的引用的类型有很多。

​        多态存在的条件:

​        1、 有继承关系

​        2、 子类重写父类的方法

​        3、 父类引用指向子类对象

**多态的意义:**

​        1、行为的多态(所有抽象方法都是多态的)

​        2、对象的多态(所有对象都是多态的)

**多态的表现形式:**

​        1、重写:根据对象的不同来表现多态

​        2、重载:根据参数的不同来表现多态

注:多态是方法的多态性,属性没有多态性。

多态

  • 多态: 是指同一行为,具有多个不同表现形式。
  • 多态的前提:有继承关系,子类对象是可以赋值给父类类型的变量。

父类类型 变量名 = new 子类/实现类构造器;

变量名.方法名();

引用类型转换

向上转型:多态本身是子类类型向父类类型向上转换(自动转换)的过程,这个过程是默认的。当父类引用指向一个子类对象时,便是向上转型。

使用格式:

父类类型  变量名 = new 子类类型();

向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。

一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。

使用格式:

子类类型 变量名 = (子类类型) 父类变量名;

Final关键字

fina:  不可改变,最终的含义。可以用于修饰类、方法和变量。

    1. 类:被修饰的类,不能被继承。
    2. 方法:被修饰的方法,不能被重写。
    3. 变量:被修饰的变量,有且仅能被赋值一次。

  1. final修饰的类,不能被继承。

格式如下:final class 类名 {

}

  1. final修饰的方法,不能被重写。

格式如下:

                 修饰符 final 返回值类型 方法名(参数列表){

方法体

}

抽象类

父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了(因为子类对象会调用自己重写的方法)。换句话说,父类可能知道子类应该有哪个功能,但是功能具体怎么实现父类是不清楚的(由子类自己决定),父类只需要提供一个没有方法体的定义即可,具体实现交给子类自己去实现。我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类。

抽象方法: 没有方法体的方法。

抽象类:包含抽象方法的类。

abstract使用格式

abstract是抽象的意思,用于修饰方法方法和类,修饰的方法是抽象方法,修饰的类是抽象类。

抽象方法

使用`abstract` 关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。

定义格式:

修饰符 abstract 返回值类型 方法名 (参数列表);

抽象类

如果一个类包含抽象方法,那么该类必须是抽象类。注意:抽象类不一定有抽象方法,但是有抽象方法的类必须定义成抽象类。

定义格式:

                 abstract class 类名字 {    

}

接口

定义格式

//接口的定义格式:

interface 接口名称{

    // 抽象方法

}

// 接口的声明:interface

// 接口名称:首字母大写,满足“驼峰模式”

内部类

按定义的位置来分

1. 成员内部内,类定义在了成员位置 (类中方法外称为成员位置,无static修饰的内部类)

2. 静态内部类,类定义在了成员位置 (类中方法外称为成员位置,有static修饰的内部类)

3. 局部内部类,类定义在方法内

4. 匿名内部类,没有名字的内部类,可以在方法中,也可以在类中方法外。

内部类的使用格式:

外部类.内部类。 // 访问内部类的类型都是用 外部类.内部类

获取成员内部类对象的方式:

外部类.内部类 变量 = new 外部类().new 内部类();

枚举

声明枚举时必须使用 enum 关键字,然后定义枚举的名称、可访问性、基础类型和成员等。

任意两个枚举成员不能具有相同的名称,且它的常数值必须在该枚举的基础类型的范围之内,多个枚举成员之间使用逗号分隔。

例 1

下面代码定义了一个表示性别的枚举类型 SexEnum 和一个表示颜色的枚举类型 Color。

   

public enum SexEnum{

    male,female;

}

public enum Color{

    RED,BLUE,GREEN,BLACK;

}

接口(Interface)

接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过实现(implements)接口的方式,从而来实现接口的抽象方法。

​              接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。

​              除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。

​              接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。

接口与类相似点:

​              1)一个接口可以有多个方法。

​              2)接口文件保存在 .java 结尾的文件中,文件名使用接口名。

​              3)接口的字节码文件保存在 .class 结尾的文件中。

​              4)接口相应的字节码文件必须在与包名称相匹配的目录结构中。

接口与类的区别:

​              1)接口不能用于实例化对象。

​              2)接口没有构造方法。

​              3)接口中所有的方法必须是抽象方法。

​              4)接口不能包含成员变量,除了 static 和 final 变量。

​              5)接口不是被类继承了,而是要被类实现。

​              6)接口支持多继承(接口不能继承类,接口只能继承接口)。

接口特性:

​              1)接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。

​              2)接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。

​              3)接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。

package com.zpark.oop.day07;
public interface  Demo05 {
//    public Demo05(); 接口中不能有构造器
    // 在接口中定义的变量默认为 public final static修饰
    public final static String NAME = "大锤";
    // 接口中的方法必须全部是抽象方法
    public abstract void a();
    int b();
}

抽象类和接口的区别:

​              1. 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。

​              2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。

​              3. 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。

​              4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

内部类

​              java内部类的几种类型:成员内部类,静态内部类,方法内部类,匿名内部类。

成员内部类:成员内部类是类内部的非静态类。成员内部类不能定义静态方法和变量(final修饰的除外)。这是因为成员内部类是非静态的,类初始化的时候先初始化静态成员,如果允许成员内部类定义静态变量,那么成员内部类的静态变量初始化顺序是有歧义的。

成员内部类的使用方法:

​              1、 Inner 类定义在 Outer 类的内部,相当于 Outer 类的一个成员变量的位置,Inner 类可以使用任意访问控制符,如 public 、 protected 、 private 等

​              2、 Inner 类中定义的 test() 方法可以直接访问 Outer 类中的数据,而不受访问控制符的影响,如直接访问 Outer 类中的私有属性a

​              3、 定义了成员内部类后,必须使用外部类对象来创建内部类对象,而不能直接去 new 一个内部类对象,即:内部类 对象名 = 外部类对象.new 内部类( );

静态内部类:

静态内部类是 static 修饰的内部类,这种内部类的特点是:

​1、 静态内部类不能直接访问外部类的非静态成员,但可以通过 new 外部类().成员 的方式访问。

​2、 如果外部类的静态成员与内部类的成员名称相同,可通过“类名.静态成员”访问外部类的静态成员;如果外部类的静态成员与             内部类的成员名称不相同,则可通过“成员名”直接调用外部类的静态成员。

​3、 创建静态内部类的对象时,不需要外部类的对象,可以直接创建 内部类 对象名= new 内部类();

方法内部类(局部内部类):

​              方法内部类就是内部类定义在外部类的方法中,方法内部类只在该方法的内部可见,即只在该方法内可以使用。

需要注意:由于方法内部类不能在外部类的方法以外的地方使用,因此方法内部类不能使用访问控制符和 static 修饰符。

枚举

​              枚举是一个被命名的整型常数的集合,用于声明一组带标识符的常数。类似这种当一个变量有几种固定可能的取值时,就可以将它定义为枚举类型。

声明枚举

​              声明枚举时必须使用 enum 关键字,然后定义枚举的名称、可访问性、基础类型和成员等。

       任意两个枚举成员不能具有相同的名称,且它的常数值必须在该枚举的基础类型的范围之内,多个枚举成员之间使用逗号分隔。

提示:如果没有显式地声明基础类型的枚举,那么意味着它所对应的基础类型是 int。

枚举类

Java 中的每一个枚举都继承自 java.lang.Enum 类。当定义一个枚举类型时,每一个枚举类型成员都可以看作是 Enum 类的实例,这些枚举成员默认都被 final、public, static 修饰,当使用枚举类型成员时,直接使用枚举名称调用成员即可。所有枚举实例都可以调用 Enum 类的方法。

为枚举添加方法

​           Java 为枚举类型提供了一些内置的方法,同时枚举常量也可以有自己的方法。此时要注意必须在枚举实例的最后一个成员后添加分号,而且必须先定义枚举实例。

Java 中的 enum 还可以跟 Class 类一样覆盖基类的方法。

字符串String处理:直接定义和使用String类定义;

将字符数组转为字符串:char[] ch = new char[]{‘H’,’e’,’l’,’l’,’o’}

创建建字符串

        String str = new String(ch);

分配一个新字符串,该字符串包含字符数组参数的子数组中的字符:

      offset参数是子数组第一个字符的索引,count参数指定子数组的长度。

      复制子数组的内容;字符数组的后续修改不会影响新创建的字符串。

   char[] ch1 = new char[]{'H', 'e', 'l', 'l', 'o', ',', 'w', 'o', 'r', 'l', 'd'};

   String str1 = new String(ch1, 2, 8);

   System.out.println(str1);

分配一个新字符串,该字符串包含来自Unicode码点数组参数子数组的字符:

offset参数是子数组第一个编码点的索引,count参数指定子数组的长度。

子数组的内容被转换为字符;int数组的后续修改不会影响新创建的字符串。

        int[] arr = new int[]{65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75};

        String str2 = new String(arr, 0, 5);

        System.out.println(str2);

    }

将字符串转为int类型的值前提是字符内容必须是纯数字,使用包装类进行转换。

String转换为int:

Integer.parselnt(str)

Integer.valueOf(str).intValue();

Int转换为String:

String s = String.valueOf(i);

String s = Integer.toString(i);

String s = “”+i;

将int类型数据转为String类型

        int num1 = 56;

        String str = num1 + "";

        System.out.println(str + 1);

valueOf()方法将数据的内部格式转换为可读的形式。是一种静态方法,对于所有内置的类型,在字符串被重载,以便每一种类型都能被转换成字符串。

parse()

parseXxx(String)这种形式,是指把字符串转换为数值型;

toString()可以把一个引用类型转换为String字符串类型。

concat()方法:String类的concat()方法实现了将一个字符串连接到另外一个字符串后面。语法:字符串1.concat(字符串2);

其他类型数据直接使用“+”拼接;

获取字符串长度:

使用length(),语法:字符串名.length();

1、获取字符串长度

2、将字符串转大小写

3、去除字符串两端的空白

String str = "Hello Java";

// 使用length()函数获取字符串长度

System.out.println(str.length());

// 将字符串转为大写

System.out.println(str.toUpperCase());

// 将字符串转小写

System.out.println(str.toLowerCase());

String str1 = "              hello                     ";

System.out.println(str1.length());

// 取出字符串两端空白

String str2 = str1.trim();

System.out.println(str2.length());

字符串截取(substring()):

         * substring(int start): 从指定位置开始截取,start表示开始位置下标

         * substring(int start, int end):截取指定范围内的字符串,含前不含尾

  • 分割字符串(spilt())​ String 类的 split() 方法可以按指定的分割符对目标字符串进行分割,分割后的内容存放在字符串数组中。

九、字符串的替换 1、replace() 方法​ replace() 方法用于将目标字符串中的指定字符(串)替换成新的字符(串),其语法格式如下:```java字符串.replace(String oldChar, String newChar)```​ 其中,oldChar 表示被替换的字符串;newChar 表示用于替换的字符串。replace() 方法会将字符串中所有 oldChar 替换成 newChar

十、字符串比较​ 字符串比较是常见的操作,包括比较相等、比较大小、比较前缀和后缀串等。在 Java 中,比较字符串的常用方法有 3 个:equals() 方法、equalsIgnoreCase() 方法、 compareTo() 方法。## 1、equals() 方法​ equals() 方法将逐个地比较两个字符串的每个字符是否相同。如果两个字符串具有相同的字符和长度,它返回 true,否则返回 false。

十一、1、equalsIgnoreCase() 方法​ equalsIgnoreCase() 方法的作用和语法与 equals() 方法完全相同,唯一不同的是 equalsIgnoreCase() 比较时不区分大小写。当比较两个字符串时,它会认为 A-Z 和 a-z 是一样的。

2、equals()与==的比较​ 理解 equals() 方法和`==`运算符执行的是两个不同的操作是重要的。如同刚才解释的那样,equals() 方法比较字符串对象中的字符。而`==`运算符比较两个对象引用看它们是否引用相同的实例

  1. 4、compareTo() 方法​ 通常,仅仅知道两个字符串是否相同是不够的。对于排序应用来说,必须知道一个字符串是大于、等于还是小于另一个。一个字符串小于另一个指的是它在字典中先出现。而一个字符串大于另一个指的是它在字典中后出现。字符串(String)的 compareTo() 方法实现了这种功能。​ compareTo() 方法用于按字典顺序比较两个字符串的大小,该比较是基于字符串各个字符的 Unicode 值。
  2. **提示:**如果两个字符串调用 equals() 方法返回 true,那么调用 compareTo() 方法会返回 0。

  • 字符串查找​

 在给定的字符串中查找字符或字符串是比较常见的操作。字符串查找分为两种形式:一种是在字符串中获取匹配字符(串)的索引值,另一种是在字符串中获取指定索引位置的字符。## 1、根据字符查找​ String 类的 indexOf() 方法和 lastlndexOf() 方法用于在字符串中获取匹配字符(串)的索引值。

    1. indexOf() 方法​ indexOf() 方法用于返回字符(串)在指定字符串中首次出现的索引位置,如果能找到,则返回索引值,否则返回 -1。
    2. fromIndex 表示查找时的起始索引,如果不指定 fromIndex,则默认从指定字符串中的开始位置(即 fromIndex 默认为 0)开始查找。
    3. lastlndexOf() 方法​ lastIndexOf() 方法用于返回字符(串)在指定字符串中最后一次出现的索引位置,如果能找到则返回索引值,否则返回 -1。
  • StringBuffer 类是可变字符串类,创建 StringBuffer 类的对象后可以随意修改字符串的内容。每个 StringBuffer 类的对象都能够存储指定容量的字符串,如果字符串的长度超过了 StringBuffer 类对象的容量,则该对象的容量会自动扩大
  • 删除字符串​ StringBuffer 类提供了 deleteCharAt() 和 delete() 两个删除字符串的方法,下面详细介绍。**1、deleteCharAt() 方法**​ deleteCharAt() 方法用于移除序列中指定位置的字符,该方法的语法格式如下:```javaStringBuffer 对象.deleteCharAt(int index);```​ deleteCharAt() 方法的作用是删除指定位置的字符,然后将剩余的内容形成一个新的字符串
  • StringBuilder 和 StringBuffer 功能基本相似,方法也差不多。不同的是,StringBuffer 是线程安全的,而 StringBuilder 则没有实现线程安全功能,所以性能略高。因此在通常情况下,如果需要创建一个内容可变的字符串对象,则应该优先考虑使用 StringBuilder 类。

1、静态常量​ Math 类中包含 E 和 PI 两个静态常量,正如它们名字所暗示的,它们的值分别等于 e(自然对数)和 π(圆周率)。

2、生成随机数(random()和Random类)​ 在 Java 中要生成一个指定范围之内的随机数字有两种方法:一种是调用 Math 类的 random() 方法,一种是使用 Random 类。​ Random 类提供了丰富的随机数生成方法,可以产生 boolean、int、long、float、byte 数组以及 double 类型的随机数,这是它与 random() 方法最大的不同之处。random() 方法只能产生 double 类型的 0~1 的随机数。​ Random 类位于 java.util 包中,该类常用的有如下两个构造方法。​ **Random():**该构造方法使用一个和当前系统时间对应的数字作为种子数,然后使用这个种子数构造 Random 对象。​ **Random(long seed):**使用单个 long 类型的参数创建一个新的随机数生成器。​ Random 类提供的所有方法生成的随机数字都是均匀分布的,也就是说区间内部的数字生成的概率是均等的,在表 1 中列出了 Random 类中常用的方法

3、Math 类的 random() 方法没有参数,它默认会返回大于等于 0.0、小于 1.0 的 double 类型随机数,即 0<=随机数<1.0。对 random() 方法返回的数字稍加处理,即可实现产生任意范围随机数的功能。

4、BigInteger 类型的数字范围较 Integer 类型的数字范围要大得多。BigInteger 支持任意精度的整数,也就是说在运算中 BigInteger 类型可以准确地表示任何大小的整数值。​ 除了基本的加、减、乘、除操作之外,BigInteger 类还封装了很多操作,像求绝对值、相反数、最大公约数以及判断是否为质数等。​ 要使用 BigInteger 类,首先要创建一个 BigInteger 对象。BigInteger 类提供了很多种构造方法,其中最直接的一种是参数以字符串形式代表要处理的数字。

5、igDecimal 类​ BigInteger 和 BigDecimal 都能实现大数字的运算,不同的是 BigDecimal 加入了小数的概念。一般的 float 和 double 类型数据只能用来做科学计算或工程计算,但由于在商业计算中要求数字精度比较高,所以要用到 BigDecimal 类。BigDecimal 类支持任何精度的浮点数,可以用来精确计算货币值。​ BigDecimal 常用的构造方法如下。​ 1、BigDecimal(double val):实例化时将双精度型转换为 BigDecimal 类型。​ 2、BigDecimal(String val):实例化时将字符串形式转换为 BigDecimal 类型。​ BigDecimal 类的方法可以用来做超大浮点数的运算,像加、减、乘和除等。在所有运算中,除法运算是最复杂的,因为在除不尽的情况下,末位小数的处理方式是需要考虑的。

6、Java时间日期的处理​ 在 Java 中获取当前时间,可以使用 java.util.Date 类和 java.util.Calendar 类完成。其中,Date 类主要封装了系统的日期和时间的信息,Calendar 类则会根据系统的日历来解释 Date 对象。下面详细介绍这两个类的具体使用。## 1、Date 类​ Date 类表示系统特定的时间戳,可以精确到毫秒。Date 对象表示时间的默认顺序是星期、月、日、小时、分、秒、年。#### 1.1、构造方法​ Date 类有如下两个构造方法。​ 1、Date():此种形式表示分配 Date 对象并初始化此对象,以表示分配它的时间(精确到毫秒),使用该构造方法创建的对象可 以获取本地的当前时间。​ 2、Date(long date):此种形式表示从 GMT 时间(格林尼治时间)1970 年 1 月 1 日 0 时 0 分 0 秒开始经过参数 date 指定的毫 秒数。

7、Calendar 类​ Calendar 类是一个抽象类,它为特定瞬间与 YEAR、MONTH、DAY_OF—MONTH、HOUR 等日历字段之间的转换提供了一些方法,并为操作日历字段(如获得下星期的日期) 提供了一些方法。​ 创建 Calendar 对象不能使用 new 关键字,因为 Calendar 类是一个抽象类,但是它提供了一个 getInstance() 方法来获得 Calendar类的对象。getInstance() 方法返回一个 Calendar 对象,其日历字段已由当前日期和时间初始化

十七、

  1.  实现 int 和 Integer 的相互转换**​ 可以通过 Integer 类的构造方法将 int 装箱,通过 Integer 类的 intValue 方法将 Integer 拆箱。
    2、 将字符串转换为数值类型**​ 在 Integer 和 Float 类中分别提供了以下两种方法:​ ① Integer 类(String 转 int 型)```javaint parseInt(String s);```​ s 为要转换的字符串。​ ② Float 类(String 转 float 型)```javafloat parseFloat(String s)```​ **注意:**使用以上两种方法时,字符串中的数据必须由数字组成,否则转换时会出现程序错误。


    3、将整数转换为字符串**​ Integer 类有一个静态的 toString() 方法,可以将整数转换为字符串
    4、Object类​ Object 是 Java 类库中的一个特殊类,也是所有类的父类。也就是说,Java 允许把任何类型的对象赋给 Object 类型的变量。当一个类被定义后,如果没有指定继承的父类,那么默认父类就是 Object 类。因此,以下两个类表示的含义是一样的。


    5、toString() 方法​ toString() 方法返回该对象的字符串,当程序输出一个对象或者把某个对象和字符串进行连接运算时,系统会自动调用该对象的 toString() 方法返回该对象的字符串表示。​ Object 类的 toString() 方法返回“运行时类名@十六进制哈希码”格式的字符串,但很多类都重写了 Object 类的 toString() 方法,用于返回可以表述该对象信息的字符串。


6、equals() 方法​ 在前面学习字符串比较时,曾经介绍过两种比较方法,分别是`==`运算符和 equals() 方法,`==`运算符是比较两个引用变量是否指向同一个实例,equals() 方法是比较两个对象的内容是否相等,通常字符串的比较只是关心内容是否相等。


7、getClass() 方法​ getClass() 方法返回对象所属的类,是一个 Class 对象。通过 Class 对象可以获取该类的各种信息,包括类名、父类以及它所实现接口的名字等。

8、Integer类​ Integer 类在对象中包装了一个基本类型 int 的值。Integer 类对象包含一个 int 类型的字段。此外,该类提供了多个方法,能在 int 类型和 String 类型之间互相转换,还提供了处理 int 类型时非常有用的其他一些常量和方法。## 1、Integer 类的构造方法​ Integer 类中的构造方法有以下两个:​ **Integer(int value):**构造一个新分配的 Integer 对象,它表示指定的 int 值。​ **Integer(String s):**构造一个新分配的 Integer 对象,它表示 String 参数所指示的 int 值。

9、Integer 类的常量​ Integer 类包含以下 4 个常量。​ **MAX_VALUE:**值为 231-1 的常量,它表示 int 类型能够表示的最大值。​ **MIN_VALUE:**值为 -231 的常量,它表示 int 类型能够表示的最小值。​ **SIZE:**用来以二进制补码形式表示 int 值的比特位数。​ **TYPE:**表示基本类型 int 的 Class 实例。


10、Float 类的构造方法​ Float 类中的构造方法有以下 3 个。​ **Float(double value):**构造一个新分配的 Float 对象,它表示转换为 float 类型的参数。​ **Float(float value):**构造一个新分配的 Float 对象,它表示基本的 float 参数。​ **Float(String s):**构造一个新分配的 Float 对象,它表示 String 参数所指示的 float 值。


11、Float 类的常用常量​ 在 Float 类中包含了很多常量,其中较为常用的常量如下。​ **MAX_VALUE:**值为 1.4E38 的常量,它表示 float 类型能够表示的最大值。​ **MIN_VALUE:**值为 3.4E-45 的常量,它表示 float 类型能够表示的最小值。​ **MAX_EXPONENT: **有限 float 变量可能具有的最大指数。​ **MIN_EXPONENT:**标准化 float 变量可能具有的最小指数。​ **MIN_NORMAL:**保存 float 类型数值的最小标准值的常量,即 2-126。​ **NaN:**保存 float 类型的非数字值的常量。​ **SIZE:**用来以二进制补码形式表示 float 值的比特位数。​ **TYPE:**表示基本类型 float 的 Class 实例。


12、Double类​ Double 类在对象中包装了一个基本类型 double 的值。Double 类对象包含一个 double 类型的字段。此外,该类还提供了多个方法,可以将 double 类型与 String 类型相互转换,同时 还提供了处理 double 类型时比较常用的常量和方法。## 1、Double 类的构造方法​ Double 类中的构造方法有如下两个。​ **Double(double value):**构造一个新分配的 Double 对象,它表示转换为 double 类型的参数。​ **Double(String s):**构造一个新分配的 Double 对象,它表示 String 参数所指示的 double 值。

13、Number类​ Number 是一个抽象类,也是一个超类(即父类)。Number 类属于 java.lang 包,所有的包装类(如 Double、Float、Byte、Short、Integer 以及 Long)都是抽象类 Number 的子类。

14、在 validateUser() 方法中,使用 for 循环遍历用户输入的用户名、密码和年龄,对其每个字符进行验证,判断其是否符合要求。在验证的过程中,分别使用了 Character 类的 isLetter() 方法、isLetterOrDigit() 方法和 isDigit() 方法。


15、Byte 类的构造方法​ Byte 类提供了两个构造方法来创建 Byte 对象。

File类

1.创建File实例

在 Java 中,File 类是 java.io 包中唯一代表磁盘文件本身的对象,也就是说,如果希望在程序中操作文件和目录,则都可以通过 File 类来完成。File 类定义了一些方法来操作文件,如新建、删除、重命名文件和目录等。

public static void main(String[] args) {

        // 创建File对象

        File f1 = new File("C:\\WorkAndStudy\\java\\a\\c");

        // 删除空的目录

        boolean b1 = f1.delete();

        System.out.println(b1);

        /**

         * 删除目录

         * 任务:

         *      删除java目录下的a目录

         */

        File f2 = new File("C:\\WorkAndStudy\\java\\a");

        // 注意:删除目录时,目录必须为空,否则删除失败

        boolean b2 = f2.delete();

        System.out.println(b2);

    }

 2.File类常用方法

方法名称        说明

boolean canRead()  测试应用程序是否能从指定的文件中进行读取

boolean canWrite() 测试应用程序是否能写当前文件

boolean delete()      删除当前对象指定的文件

boolean exists()       测试当前 File 是否存在

String getAbsolutePath() 返回由该对象表示的文件的绝对路径名

String getName()     返回表示当前对象的文件名或路径名(如果是路径,则返回最后一级子路径名)

String getParent()    返回当前 File 对象所对应目录(最后一级子目录)的父目录名

boolean isAbsolute()       测试当前 File 对象表示的文件是否为一个绝对路径名。该方法消除了不同平台的差异,可以直接判断 file 对象是否为绝对路径。在 UNIX/Linux/BSD 等系统上,如果路径名开头是一条斜线/,则表明该 File 对象对应一个绝对路径;在 Windows 等系统上,如果路径开头是盘符,则说明它是一个绝对路径。

boolean isDirectory()      测试当前 File 对象表示的文件是否为一个路径

boolean isFile()        测试当前 File 对象表示的文件是否为一个“普通”文件

long lastModified()  返回当前 File 对象表示的文件最后修改的时间

long length()    返回当前 File 对象表示的文件长度

String[] list()    返回当前 File 对象指定的路径文件列表

String[] list(FilenameFilter)     返回当前 File 对象指定的目录中满足指定过滤器的文件列表

boolean mkdir()       创建一个目录,它的路径名由当前 File 对象指定

boolean mkdirs()     创建一个目录,它的路径名由当前 File 对象指定

boolean renameTo(File)  将当前 File 对象指定的文件更名为给定参数 File 指定的路径名

字节流

        字节流是由字节组成的,字符流是由字符组成的. Java里字符由两个字节组成.字节流是最基本的,所有的InputStream和OutputStream的子类都是,主要用在处理二进制数据。

1.常用方法:

        InputStream:

                read() 读取一个字节

                read(byte[]) 读取若干(数组长度)字节

                available() 获取可读的字节数

                close() 关闭流, 释放资源

        OutputStream:\nwrite(int) 写出一个字节

                write(byte[]) 写出数组中的所有字节

                write(byte[],start,len);

                close() 关闭流, 释放资源

使用文件字节流对象实现复制功能

public static void main(String[] args) throws Exception {

        // 创建读取文件字节流对象

        FileInputStream fis = new FileInputStream("dir/a.txt");

        // 创建输出文件字节流对象

        FileOutputStream fos = new FileOutputStream("dir/a_back.dat");

        // 定义每次读取的大小

        byte[] data = new byte[1024 * 8];

        int len = -1;

        // 循环读取和写入文件

        while ((len = fis.read(data)) != -1) {

            // 写入数据

            fos.write(data, 0, len);

        }

        // 关闭流

        fis.close();

        fos.close();

    }

FileInputStream: 文件字节输入流

FileInputStream从文件系统中的文件获取输入字节。 什么文件可用取决于主机环境。

FileInputStream用于读取诸如图像数据的原始字节流。

构造方法

FileInputStream(File file)

     通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。

FileInputStream(FileDescriptor fdObj)

     通过使用文件描述符 fdObj创建 FileInputStream ,该文件描述符表示与文件系统中的实际文件的现有连接。

FileInputStream(String name)

     通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。

public static void main(String[] args) throws Exception {

        // 创建fileInputStream对象

        FileInputStream fis = new FileInputStream("dir/a.txt");

        // 常用方法

        for (int i = 0; i < 10; i++) {

            int read = fis.read();

            System.out.println(fis.read(new byte[1024]));

            System.out.println(read);

            // 将输入流的指针返回到设置标记的起始处

            // fis.reset();

        }

        // 关闭文件

        fis.close();

    }

public static void main(String[] args) throws Exception {

        // 创建字节输出流对象

        FileOutputStream fos = new FileOutputStream("dir/fos.txt", true);

        // 写入数据

        String str = "晓看天色,暮看云....";

        fos.write(str.getBytes("utf-8"));

        // 关闭文件

        fos.close();

    }

字符流

BufferedInputStream为另一个输入流添加了功能,即缓冲输入并支持mark和reset方法的功能。创建BufferedInputStream将创建一个内部缓冲区数组。 当从流中读取或跳过字节时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次很多字节。 mark操作会记住输入流中的一个点,并且reset操作会导致从最近的mark操作读取的所有字节在从包含的输入流中取出新字节之前重新读取。

构造方法    Constructor 描述

BufferedInputStream(InputStream in)

     创建一个 BufferedInputStream并保存其参数,输入流 in供以后使用。

BufferedInputStream(InputStream in, int size)

     创建具有指定缓冲区大小的 BufferedInputStream ,并保存其参数,输入流 in供以后使用。

public static void main(String[] args) throws Exception {

        // 创建字节输入流对象

        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("dir/bos.txt"));

        // 创建读缓冲区大小

        byte[] data = new byte[1024 * 8];

        // 将读取数据转出字符串输出

        bis.read(data);

        String result = new String(data, "utf-8");

        System.out.println(result);

        // 关闭流

        bis.close();

    }

public class BufferedOutputStream extends FilterOutputStream该类实现缓冲输出流。 通过设置这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用。构造方法摘要

构造方法    Constructor 描述

BufferedOutputStream(OutputStream out)

     创建一个新的缓冲输出流,以将数据写入指定的底层输出流。

BufferedOutputStream(OutputStream out, int size)

     创建一个新的缓冲输出流,以便以指定的缓冲区大小将数据写入指定的底层输出流。

public static void main(String[] args) throws Exception {

        // 创建流对象

        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("dir/bos.txt"));

        // 创建需要写入的字符串

        String str = "该类实现缓冲输出流。 通过设置这样的输出流,应用程序可以向底层输出流写入字节";

        // 写入数据

        bos.write(str.getBytes("utf-8"));

        // 关闭流

        bos.close();

    }

BufferedReader 类主要用于辅助其他字符输入流,它带有缓冲区,可以先将一批数据读到内存缓冲区。接下来的读操作就可以直接从缓冲区中获取数据,而不需要每次都从数据源读取数据并进行字符编码转换,这样就可以提高数据的读取效率。

public static void main(String[] args) throws Exception {

        // 创建字节流对象

        FileInputStream fis = new FileInputStream("dir/osw.txt");

        // 创建字符流对象

        InputStreamReader isr = new InputStreamReader(fis, "utf-8");

        // 创建缓冲流对象

        BufferedReader br = new BufferedReader(isr);

        // 读

        String s = br.readLine();

        System.out.println(s);

        String s1 = br.readLine();

        System.out.println(s1);

        // 读取到文件末尾结果为null

        String s2 = br.readLine();

        System.out.println(s2);

        // 关闭资源

        fis.close();

        isr.close();

        br.close();

————————————————

多线程

一、多线程基础

              现代操作系统(WindowsmacOSLinux)都可以执行多任务。多任务就是同时运行多个任务,例如:

              CPU执行代码都是一条一条顺序执行的,但是,即使是单核cpu,也可以同时运行多个任务。因为操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行。

              例如,假设我们有语文、数学、英语3门作业要做,每个作业需要30分钟。我们把这3门作业看成是3个任务,可以做1分钟语文作业,再做1分钟数学作业,再做1分钟英语作业:

              这样轮流做下去,在某些人眼里看来,做作业的速度就非常快,看上去就像同时在做3门作业一样

              类似的,操作系统轮流让多个任务交替执行,例如,让浏览器执行0.001秒,让QQ执行0.001秒,再让音乐播放器执行0.001秒,在人看来,CPU就是在同时执行多个任务。

              即使是多核CPU,因为通常任务的数量远远多于CPU的核数,所以任务也是交替执行的。

1、进程

              在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。

              某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。

              进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

              操作系统调度的最小任务单位其实不是进程,而是线程。常用的WindowsLinux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。

              因为同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:

              多进程模式(每个进程只有一个线程):

              多线程模式(一个进程有多个线程):

              多进程+多线程模式(复杂度最高):

2、进程 vs 线程

              进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。具体采用哪种方式,要考虑到进程和线程的特点。

              和多线程相比,多进程的缺点在于:

                            1、创建进程比创建线程开销大,尤其是在Windows系统上;

                            2、进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。

              而多进程的优点在于:

                            多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃              会直接导致整个进程崩溃。

3、多线程

              Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。

              因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。

              和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。

Java多线程编程的特点又在于:

  • 多线程模型是Java程序最基本的并发模型;
  • 网络、数据库、Web开发等都依赖Java多线程模型。

二、创建新线程

              Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。

              要创建一个新线程非常容易,我们需要实例化一个Thread实例,然后调用它的start()方法:

public class Main {

   public static void main(String[] args) {

       Thread t = new Thread();

       t.start(); // 启动新线程

  }

}

              但是这个线程启动后实际上什么也不做就立刻结束了。我们希望新线程能执行指定的代码,有以下几种方法:

1、通过继承Thread来创建线程

Thread派生一个自定义类,然后覆写run()方法:

public class Main {

   public static void main(String[] args) {

       Thread t = new MyThread();

       t.start(); // 启动新线程

  }

}

​

​

class MyThread extends Thread {

   @Override

   public void run() {

       System.out.println("start new thread!");

  }

}

              执行上述代码,注意到start()方法会在内部自动调用实例的run()方法。

2、实现 Runnable 接口创建线程

创建Thread实例时,传入一个Runnable实例:

public class Main {

   public static void main(String[] args) {

       Thread t = new Thread(new MyRunnable());

       t.start(); // 启动新线程

  }

}

​

​

// 创建线程方式二,实现Runnable接口

class MyRunnable implements Runnable {

   @Override

   public void run() {

       System.out.println("start new thread!");

  }

}

              或者用Java8引入的lambda语法进一步简写为:

public class Main {

   public static void main(String[] args) {

       Thread t = new Thread(() -> {

           System.out.println("start new thread!");

      });

       t.start(); // 启动新线程

  }

}

              有同学会问,使用线程执行的打印语句,和直接在main()方法执行有区别吗?

              区别大了去了。我们看以下代码:

public class Main {

   public static void main(String[] args) {

       System.out.println("main start...");

       

       Thread t = new Thread() {

           public void run() {

               System.out.println("thread run...");

               System.out.println("thread end.");

          }

      };

       

       t.start();

       

       System.out.println("main end...");

  }

}

              主线程main线程,main线程执行的代码有4行,首先打印main start,然后创建Thread对象,紧接着调用start()启动新线程。当start()方法被调用时,JVM就创建了一个新线程,我们通过实例变量t来表示这个新线程对象,并开始执行。

              接着,main线程继续执行打印main end语句,而t线程在main线程执行的同时会并发执行,打印thread runthread end语句。

              run()方法结束时,新线程就结束了。而main()方法结束时,主线程也结束了。

              我们再来看线程的执行顺序:

                            1main线程肯定是先打印main start,再打印main end

                            2t线程肯定是先打印thread run,再打印thread end

              但是,除了可以肯定,main start会先打印外,main end打印在thread run之前、thread end之后或者之间,都无法确定。因为从t线程开始运行以后,两个线程就开始同时运行了,并且由操作系统调度,程序本身无法确定线程的调度顺序。

              要模拟并发执行的效果,我们可以在线程中调用Thread.sleep(),强迫当前线程暂停一段时间:

public class Main {

   public static void main(String[] args) {

       System.out.println("main start...");

       Thread t = new Thread() {

           public void run() {

               System.out.println("thread run...");

               try {

                   Thread.sleep(10);

              } catch (InterruptedException e) {}

               System.out.println("thread end.");

          }

      };

       t.start();

       try {

           Thread.sleep(20);

      } catch (InterruptedException e) {}

       System.out.println("main end...");

  }

}

              sleep()传入的参数是毫秒。调整暂停时间的大小,我们可以看到main线程和t线程执行的先后顺序。要特别注意:直接调用Thread实例的run()方法是无效的:

public class Main {

   public static void main(String[] args) {

       Thread t = new MyThread();

       t.run();

  }

}

​

​

class MyThread extends Thread {

   public void run() {

       System.out.println("hello");

  }

}

              直接调用run()方法,相当于调用了一个普通的Java方法,当前线程并没有任何改变,也不会启动新线程。上述代码实际上是在main()方法内部又调用了run()方法,打印hello语句是在main线程中执行的,没有任何新线程被创建。

              必须调用Thread实例的start()方法才能启动新线程,如果我们查看Thread类的源代码,会看到start()方法内部调用了一个private native void start0()方法,native修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。

3、线程的优先级

              可以对线程设定优先级,设定优先级的方法是:

Thread.setPriority(int n) // 1~10, 默认值5

              JVM自动把1(低)~10(高)的优先级映射到操作系统实际优先级上(不同操作系统有不同的优先级数量)。优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。

三、线程的状态

JDK中用Thread.State类定义了线程的几种状态

              要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:                          新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态                        就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源                         运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能                        阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU并临时中止自己的执行,进入阻塞状态                           死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

              Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

用一个状态转移图表示如下:

              当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

线程终止的原因有:

  • 线程正常终止:run()方法执行到return语句返回;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:

public class Main {

   public static void main(String[] args) throws InterruptedException {

       Thread t = new Thread(() -> {

           System.out.println("hello");

      });

       System.out.println("start");

       t.start();

       t.join();

       System.out.println("end");

  }

}

              main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是main线程先打印startt线程再打印hellomain线程最后再打印end

              如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。

小结

              1Java线程对象Thread的状态包括:NewRunnableBlockedWaitingTimed WaitingTerminated

              2、通过对另一个线程对象调用join()方法可以等待其执行结束;

              3、可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;

              4、对已经运行结束的线程调用join()方法会立刻返回。

四、中断线程

              如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。

              我们举个栗子:假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点取消,这时,程序就需要中断下载线程的执行。

              中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。

              我们还是看示例代码:

public class Main {

   public static void main(String[] args) throws InterruptedException {

       Thread t = new MyThread();

       t.start();

       Thread.sleep(1); // 暂停1毫秒

       t.interrupt(); // 中断t线程

       t.join(); // 等待t线程结束

       System.out.println("end");

  }

}

​

​

class MyThread extends Thread {

   public void run() {

       int n = 0;

       while (! isInterrupted()) {

           n ++;

           System.out.println(n + " hello!");

      }

  }

}

              仔细看上述代码,main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了中断请求,至于t线程是否能立刻响应,要看具体代码。而t线程的while循环会检测isInterrupted(),所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法。

              如果线程处于等待状态,例如,t.join()会让main线程进入等待状态,此时,如果对main线程调用interrupt()join()方法会立刻抛出InterruptedException,因此,目标线程只要捕获到join()方法抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。

              我们来看下面的示例代码:

public class Main {

   public static void main(String[] args) throws InterruptedException {

       Thread t = new MyThread();

       t.start();

       Thread.sleep(1000);

       t.interrupt(); // 中断t线程

       t.join(); // 等待t线程结束

       System.out.println("end");

  }

}

​

class MyThread extends Thread {

   public void run() {

       Thread hello = new HelloThread();

       hello.start(); // 启动hello线程

       try {

           hello.join(); // 等待hello线程结束

      } catch (InterruptedException e) {

           System.out.println("interrupted!");

      }

       hello.interrupt();

  }

}

​

class HelloThread extends Thread {

   public void run() {

       int n = 0;

       while (!isInterrupted()) {

           n++;

           System.out.println(n + " hello!");

           try {

               Thread.sleep(100);

          } catch (InterruptedException e) {

               break;

          }

      }

  }

}

              main线程通过调用t.interrupt()从而通知t线程中断,而此时t线程正位于hello.join()的等待中,此方法会立刻结束等待并抛出InterruptedException。由于我们在t线程中捕获了InterruptedException,因此,就可以准备结束该线程。在t线程结束前,对hello线程也进行了interrupt()调用通知其中断。如果去掉这一行代码,可以发现hello线程仍然会继续运行,且JVM不会退出。

              另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束:

public class Main {

   public static void main(String[] args)  throws InterruptedException {

       HelloThread t = new HelloThread();

       t.start();

       Thread.sleep(1);

       t.running = false; // 标志位置为false

  }

}

​

class HelloThread extends Thread {

   public volatile boolean running = true;

   public void run() {

       int n = 0;

       while (running) {

           n ++;

           System.out.println(n + " hello!");

      }

       System.out.println("end!");

  }

}

              注意到HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。

              为什么要对线程间共享的变量用关键字volatile声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!

              这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量a = true,线程1执行a = false时,它在此刻仅仅是把变量a的副本变成了false,主内存的变量a还是true,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true,这就造成了多线程之间共享的变量不一致。

              因此,volatile关键字的目的是告诉虚拟机:

                            1、每次访问变量时,总是获取主内存的最新值;

                            2、每次修改变量后,立刻回写到主内存。

              volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

              如果我们去掉volatile关键字,运行上述程序,发现效果和带volatile差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。

小结

              对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException

              目标线程检测到isInterrupted()true或者捕获了InterruptedException都应该立刻结束自身线程;

              通过标志位判断需要正确使用volatile关键字;

              volatile关键字解决了共享变量在线程间的可见性问题。

六、守护线程

              Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:

class TimerThread extends Thread {

   @Override

   public void run() {

       while (true) {

           System.out.println(LocalTime.now());

           try {

               Thread.sleep(1000);

          } catch (InterruptedException e) {

               break;

          }

      }

  }

}

              如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?答案是使用守护线程(Daemon Thread)。守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。因此,JVM退出时,不必关心守护线程是否已结束。

              如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:

Thread t = new MyThread();

t.setDaemon(true);

t.start();

              在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

七、线程同步

              当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。

              这个时候,有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题。我们来看一个例子:

public class Main {

   public static void main(String[] args) throws Exception {

       var add = new AddThread();

       var dec = new DecThread();

       add.start();

       dec.start();

       add.join();

       dec.join();

       System.out.println(Counter.count);

  }

}

​

​

class Counter {

   public static int count = 0;

}

​

​

class AddThread extends Thread {

   public void run() {

       for (int i=0; i<10000; i++) { Counter.count += 1; }

  }

}

​

​

class DecThread extends Thread {

   public void run() {

       for (int i=0; i<10000; i++) { Counter.count -= 1; }

  }

}

              上面的代码很简单,两个线程同时对一个int变量进行操作,一个加10000次,一个减10000次,最后结果应该是0,但是,每次运行,结果实际上都是不一样的。

              这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。例如,对于语句:

n = n + 1;

              看上去是一行语句,实际上对应了3条指令:

ILOAD

IADD

ISTORE

              我们假设n的值是100,如果两个线程同时执行n = n + 1,得到的结果很可能不是102,而是101,原因在于:

              如果线程1在执行ILOAD后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD后获取的值仍然是100,最终结果被两个线程的ISTORE写入后变成了101,而不是期待的102

              这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:

              通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。

              可见,保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:

synchronized(lock) {

   n = n + 1;

}

              synchronized保证了代码块在任意时刻最多只有一个线程能执行。我们把上面的代码用synchronized改写如下:

public class Main {

   public static void main(String[] args) throws Exception {

       var add = new AddThread();

       var dec = new DecThread();

       add.start();

       dec.start();

       add.join();

       dec.join();

       System.out.println(Counter.count);

  }

}

​

​

class Counter {

   public static final Object lock = new Object();

   public static int count = 0;

}

​

​

class AddThread extends Thread {

   public void run() {

       for (int i=0; i<10000; i++) {

           synchronized(Counter.lock) {

               Counter.count += 1;

          }

      }

  }

}

​

​

class DecThread extends Thread {

   public void run() {

       for (int i=0; i<10000; i++) {

           synchronized(Counter.lock) {

               Counter.count -= 1;

          }

      }

  }

}

              注意到代码:

synchronized(Counter.lock) { // 获取锁

  ...

} // 释放锁

              它表示用Counter.lock实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized语句块结束会自动释放锁。这样一来,对Counter.count变量进行读写就不可能同时进行。上述代码无论运行多少次,最终结果都是0

              使用synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。

我们来概括一下如何使用synchronized

  1. 找出修改共享变量的线程代码块;
  2. 选择一个共享实例作为锁;
  3. 使用synchronized(lockObject) { ... }

在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁:

public void add(int m) {

   synchronized (obj) {

       if (m < 0) {

           throw new RuntimeException();

      }

       this.value += m;

  } // 无论有无异常,都会在此释放锁

}

              我们再来看一个错误使用synchronized的例子:

public class Main {

   public static void main(String[] args) throws Exception {

       var add = new AddThread();

       var dec = new DecThread();

       add.start();

       dec.start();

       add.join();

       dec.join();

       System.out.println(Counter.count);

  }

}

​

​

class Counter {

   public static final Object lock1 = new Object();

   public static final Object lock2 = new Object();

   public static int count = 0;

}

​

​

class AddThread extends Thread {

   public void run() {

       for (int i=0; i<10000; i++) {

           synchronized(Counter.lock1) {

               Counter.count += 1;

          }

      }

  }

}

​

​

class DecThread extends Thread {

   public void run() {

       for (int i=0; i<10000; i++) {

           synchronized(Counter.lock2) {

               Counter.count -= 1;

          }

      }

  }

}

              结果并不是0,这是因为两个线程各自的synchronized锁住的不是同一个对象!这使得两个线程各自都可以同时获得锁:因为JVM只保证同一个锁在任意时刻只能被一个线程获取,但两个不同的锁在同一时刻可以被两个线程分别获取。

              因此,使用synchronized的时候,获取到的是哪个锁非常重要。锁对象如果不对,代码逻辑就不对。我们再看一个例子:

public class Main {

   public static void main(String[] args) throws Exception {

       var ts = new Thread[] { new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread() };

       for (var t : ts) {

           t.start();

      }

       for (var t : ts) {

           t.join();

      }

       System.out.println(Counter.studentCount);

       System.out.println(Counter.teacherCount);

  }

}

​

​

class Counter {

   public static final Object lock = new Object();

   public static int studentCount = 0;

   public static int teacherCount = 0;

}

​

​

class AddStudentThread extends Thread {

   public void run() {

       for (int i=0; i<10000; i++) {

           synchronized(Counter.lock) {

               Counter.studentCount += 1;

          }

      }

  }

}

​

​

class DecStudentThread extends Thread {

   public void run() {

       for (int i=0; i<10000; i++) {

           synchronized(Counter.lock) {

               Counter.studentCount -= 1;

          }

      }

  }

}

​

​

class AddTeacherThread extends Thread {

   public void run() {

       for (int i=0; i<10000; i++) {

           synchronized(Counter.lock) {

               Counter.teacherCount += 1;

          }

      }

  }

}

​

​

class DecTeacherThread extends Thread {

   public void run() {

       for (int i=0; i<10000; i++) {

           synchronized(Counter.lock) {

               Counter.teacherCount -= 1;

          }

      }

  }

}

              上述代码的4个线程对两个共享变量分别进行读写操作,但是使用的锁都是Counter.lock这一个对象,这就造成了原本可以并发执行的Counter.studentCount += 1Counter.teacherCount += 1,现在无法并发执行了,执行效率大大降低。实际上,需要同步的线程可以分成两组:AddStudentThreadDecStudentThreadAddTeacherThreadDecTeacherThread,组之间不存在竞争,因此,应该使用两个不同的锁,即:

              AddStudentThreadDecStudentThread使用lockStudent锁:

synchronized(Counter.lockStudent) {

  ...

}

              AddTeacherThreadDecTeacherThread使用lockTeacher锁:

synchronized(Counter.lockTeacher) {

  ...

}

              这样才能最大化地提高执行效率。

不需要synchronized的操作

JVM规范定义了几种原子操作:

  • 基本类型(longdouble除外)赋值,例如:int n = m
  • 引用类型赋值,例如:List<String> list = anotherList

longdouble64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把longdouble的赋值作为原子操作实现的。

单条原子操作的语句不需要同步。例如:

public void set(int m) {

   synchronized(lock) {

       this.value = m;

  }

}

              就不需要同步。对引用也是类似。例如:

public void set(String s) {

   this.value = s;

}

              上述赋值语句并不需要同步。但是,如果是多行赋值语句,就必须保证是同步操作,例如:

class Pair {

   int first;

   int last;

   public void set(int first, int last) {

       synchronized(this) {

           this.first = first;

           this.last = last;

      }

  }

}

              有些时候,通过一些巧妙的转换,可以把非原子操作变为原子操作。例如,上述代码如果改造成:

class Pair {

   int[] pair;

   public void set(int first, int last) {

       int[] ps = new int[] { first, last };

       this.pair = ps;

  }

}

              就不再需要同步,因为this.pair = ps是引用赋值的原子操作。而语句:

int[] ps = new int[] { first, last };

              这里的ps是方法内部定义的局部变量,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步。

小结

              1、多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;

              2、同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;

              3、注意加锁对象必须是同一个实例;

              4、对JVM定义的单个原子操作不需要同步。

八、同步方法

              我们知道Java程序依靠synchronized对线程进行同步,使用synchronized的时候,锁住的是哪个对象非常重要。

              让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized逻辑封装起来。例如,我们编写一个计数器如下:

public class Counter {

   private int count = 0;

​

   public void add(int n) {

       synchronized(this) {

           count += n;

      }

  }

​

   public void dec(int n) {

       synchronized(this) {

           count -= n;

      }

  }

​

   public int get() {

       return count;

  }

}

              这样一来,线程调用add()dec()方法时,它不必关心同步逻辑,因为synchronized代码块在add()dec()方法内部。并且,我们注意到,synchronized锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行:

var c1 = Counter();

var c2 = Counter();// c1进行操作的线程:

new Thread(() -> {

   c1.add();

}).start();new Thread(() -> {

   c1.dec();

}).start();// c2进行操作的线程:

new Thread(() -> {

   c2.add();

}).start();new Thread(() -> {

   c2.dec();

}).start();

              现在,对于Counter类,多线程可以正确调用。

              如果一个类被设计为允许多线程正确访问,我们就说这个类就是线程安全的(thread-safe),上面的Counter类就是线程安全的。Java标准库的java.lang.StringBuffer也是线程安全的。

              还有一些不变类,例如StringIntegerLocalDate,它们的所有成员变量都是final,多线程同时访问时只能读不能写,这些不变类也是线程安全的。

              最后,类似Math这些只提供静态方法,没有成员变量的类,也是线程安全的。

              除了上述几种少数情况,大部分类,例如ArrayList,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么ArrayList是可以安全地在线程间共享的。

              我们再观察Counter的代码:

public class Counter {

   public void add(int n) {

       synchronized(this) {

           count += n;

      }

  }

  ...

}

              当我们锁住的是this实例时,实际上可以用synchronized修饰这个方法。下面两种写法是等价的:

public void add(int n) {

   synchronized(this) { // 锁住this

       count += n;

  } // 解锁

}
public synchronized void add(int n) { // 锁住this

   count += n;

} // 解锁

              因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。我们再思考一下,如果对一个静态方法添加synchronized修饰符,它锁住的是哪个对象?

public synchronized static void test(int n) {

  ...

}

              对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的Class实例。上述synchronized static方法实际上相当于:

public class Counter {

   public static void test(int n) {

       synchronized(Counter.class) {

          ...

      }

  }

}

              我们再考察Counterget()方法:

public class Counter {

   private int count;

​

   public int get() {

       return count;

  }

  ...

}

              它没有同步,因为读一个int变量不需要同步。然而,如果我们把代码稍微改一下,返回一个包含两个int的对象:

public class Counter {

   private int first;

   private int last;

​

   public Pair get() {

       Pair p = new Pair();

       p.first = first;

       p.last = last;

       return p;

  }

  ...

}

              就必须要同步了。

小结

              1、用synchronized修饰方法可以把整个方法变为同步代码块,synchronized方法加锁对象是this

              2、通过合理的设计和数据封装可以让一个类变为线程安全

              3、一个类没有特殊说明,默认不是thread-safe

              4、多线程能否安全访问某个非线程安全的实例,需要具体问题具体分析。

九、死锁

              Java的线程锁是可重入的锁。什么是可重入的锁?我们还是来看例子:

public class Counter {

   private int count = 0;

​

   public synchronized void add(int n) {

       if (n < 0) {

           dec(-n);

      } else {

           count += n;

      }

  }

​

   public synchronized void dec(int n) {

       count += n;

  }

}

              观察synchronized修饰的add()方法,一旦线程执行到add()方法内部,说明它已经获取了当前实例的this锁。如果传入的n < 0,将在add()方法内部调用dec()方法。由于dec()方法也需要获取this锁,现在问题来了:

              对同一个线程,能否在获取到锁以后继续获取同一个锁?

              答案是肯定的。JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。

              由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。

死锁

              一个线程可以获取一个锁后,再继续获取另一个锁。例如:

public void add(int m) {

   synchronized(lockA) { // 获得lockA的锁

       this.value += m;

       synchronized(lockB) { // 获得lockB的锁

           this.another += m;

      } // 释放lockB的锁

  } // 释放lockA的锁

}

​

public void dec(int m) {

   synchronized(lockB) { // 获得lockB的锁

       this.another -= m;

       synchronized(lockA) { // 获得lockA的锁

           this.value -= m;

      } // 释放lockA的锁

  } // 释放lockB的锁

}

在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。对于上述代码,线程1和线程2如果分别执行add()dec()方法时:

  • 线程1:进入add(),获得lockA
  • 线程2:进入dec(),获得lockB

随后:

  • 线程1:准备获得lockB,失败,等待中;
  • 线程2:准备获得lockA,失败,等待中。

此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。

死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。

因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。

那么我们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致。即严格按照先获取lockA,再获取lockB的顺序,改写dec()方法如下:

public void dec(int m) {

   synchronized(lockA) { // 获得lockA的锁

       this.value -= m;

       synchronized(lockB) { // 获得lockB的锁

           this.another -= m;

      } // 释放lockB的锁

  } // 释放lockA的锁

}

十、waitnotify

              Java程序中,synchronized解决了多线程竞争的问题。例如,对于一个任务管理器,多个线程同时往队列中添加任务,可以用synchronized加锁:

class TaskQueue {

   Queue<String> queue = new LinkedList<>();

​

   public synchronized void addTask(String s) {

       this.queue.add(s);

  }

}

              但是synchronized并没有解决多线程协调的问题。仍然以上面的TaskQueue为例,我们再编写一个getTask()方法取出队列的第一个任务:

class TaskQueue {

   Queue<String> queue = new LinkedList<>();

​

   public synchronized void addTask(String s) {

       this.queue.add(s);

  }

​

   public synchronized String getTask() {

       while (queue.isEmpty()) {

      }

       return queue.remove();

  }

}

              上述代码看上去没有问题:getTask()内部先判断队列是否为空,如果为空,就循环等待,直到另一个线程往队列中放入了一个任务,while()循环退出,就可以返回队列的元素了。

              但实际上while()循环永远不会退出。因为线程在执行while()循环时,已经在getTask()入口获取了this锁,其他线程根本无法调用addTask(),因为addTask()执行条件也是获取this锁。

              因此,执行上述代码,线程会在getTask()中因为死循环而100%占用CPU资源。

如果深入思考一下,我们想要的执行效果是:

  • 线程1可以调用addTask()不断往队列中添加任务;
  • 线程2可以调用getTask()从队列中获取任务。如果队列为空,则getTask()应该等待,直到队列中至少有一个任务时再返回。

因此,多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。

              对于上述TaskQueue,我们先改造getTask()方法,在条件不满足时,线程进入等待状态:

public synchronized String getTask() {

   while (queue.isEmpty()) {

       this.wait();

  }

   return queue.remove();

}

              当一个线程执行到getTask()方法内部的while循环时,它必定已经获取到了this锁,此时,线程执行while条件判断,如果条件成立(队列为空),线程将执行this.wait(),进入等待状态。

              这里的关键是:wait()方法必须在当前获取的锁对象上调用,这里获取的是this锁,因此调用this.wait()

              调用wait()方法后,线程进入等待状态,wait()方法不会返回,直到将来某个时刻,线程从等待状态被其他线程唤醒后,wait()方法才会返回,然后,继续执行下一条语句。

              有些仔细的童鞋会指出:即使线程在getTask()内部等待,其他线程如果拿不到this锁,照样无法执行addTask(),肿么办?

              这个问题的关键就在于wait()方法的执行机制非常复杂。首先,它不是一个普通的Java方法,而是定义在Object类的一个native方法,也就是由JVMC代码实现的。其次,必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。

              因此,只能在锁对象上调用wait()方法。因为在getTask()中,我们获得了this锁,因此,只能在this对象上调用wait()方法:

public synchronized String getTask() {

   while (queue.isEmpty()) {

       // 释放this:

       this.wait();

       // 重新获取this

  }

   return queue.remove();

}

              当一个线程在this.wait()等待时,它就会释放this锁,从而使得其他线程能够在addTask()方法获得this锁。

              现在我们面临第二个问题:如何让等待的线程被重新唤醒,然后从wait()方法返回?答案是在相同的锁对象上调用notify()方法。我们修改addTask()如下:

public synchronized void addTask(String s) {

   this.queue.add(s);

   this.notify(); // 唤醒在this锁等待的线程

}

              注意到在往队列中添加了任务后,线程立刻对this锁对象调用notify()方法,这个方法会唤醒一个正在this锁等待的线程(就是在getTask()中位于this.wait()的线程),从而使得等待线程从this.wait()方法返回。

              我们来看一个完整的例子:

import java.util.*;  public class Main {

   public static void main(String[] args) throws InterruptedException {

       var q = new TaskQueue();

       var ts = new ArrayList<Thread>();

       for (int i=0; i<5; i++) {

           var t = new Thread() {

               public void run() {

                   // 执行task:

                   while (true) {

                       try {

                           String s = q.getTask();

                           System.out.println("execute task: " + s);

                      } catch (InterruptedException e) {

                           return;

                      }

                  }

              }

          };

           t.start();

           ts.add(t);

      }

       var add = new Thread(() -> {

           for (int i=0; i<10; i++) {

               // 放入task:

               String s = "t-" + Math.random();

               System.out.println("add task: " + s);

               q.addTask(s);

               try { Thread.sleep(100); } catch(InterruptedException e) {}

          }

      });

       add.start();

       add.join();

       Thread.sleep(100);

       for (var t : ts) {

           t.interrupt();

      }

  }

}

​

class TaskQueue {

   Queue<String> queue = new LinkedList<>();

​

   public synchronized void addTask(String s) {

       this.queue.add(s);

       this.notifyAll();

  }

​

   public synchronized String getTask() throws InterruptedException {

       while (queue.isEmpty()) {

           this.wait();

      }

       return queue.remove();

  }

}

              这个例子中,我们重点关注addTask()方法,内部调用了this.notifyAll()而不是this.notify(),使用notifyAll()将唤醒所有当前正在this锁等待的线程,而notify()只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)。这是因为可能有多个线程正在getTask()方法内部的wait()中等待,使用notifyAll()将一次性全部唤醒。通常来说,notifyAll()更安全。有些时候,如果我们的代码逻辑考虑不周,用notify()会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。

              但是,注意到wait()方法返回时需要重新获得this锁。假设当前有3个线程被唤醒,唤醒后,首先要等待执行addTask()的线程结束此方法后,才能释放this锁,随后,这3个线程中只能有一个获取到this锁,剩下两个将继续等待。

              再注意到我们在while()循环中调用wait(),而不是if语句:

public synchronized String getTask() throws InterruptedException {

   if (queue.isEmpty()) {

       this.wait();

  }

   return queue.remove();

}

              这种写法实际上是错误的,因为线程被唤醒时,需要再次获取this锁。多个线程被唤醒后,只有一个线程能获取this锁,此刻,该线程执行queue.remove()可以获取到队列的元素,然而,剩下的线程如果获取this锁后执行queue.remove(),此刻队列可能已经没有任何元素了,所以,要始终在while循环中wait(),并且每次被唤醒后拿到this锁就必须再次判断:

while (queue.isEmpty()) {

   this.wait();

}

              所以,正确编写多线程代码是非常困难的,需要仔细考虑的条件非常多,任何一个地方考虑不周,都会导致多线程运行时不正常。

小结

waitnotify用于多线程协调运行:

  • synchronized内部可以调用wait()使线程进入等待状态;
  • 必须在已获得的锁对象上调用wait()方法;
  • synchronized内部可以调用notify()notifyAll()唤醒其他等待线程;
  • 必须在已获得的锁对象上调用notify()notifyAll()方法;
  • 已唤醒的线程还需要重新获得锁后才能继续执行。

十一、ReentrantLock

              Java 5开始,引入了一个高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。

              我们知道Java语言直接提供了synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。

              java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁,我们来看一下传统的synchronized代码:

public class Counter {

   private int count;

​

   public void add(int n) {

       synchronized(this) {

           count += n;

      }

  }

}

              如果用ReentrantLock替代,可以把代码改造为:

public class Counter {

   private final Lock lock = new ReentrantLock();

   private int count;

​

   public void add(int n) {

       lock.lock();

       try {

           count += n;

      } finally {

           lock.unlock();

      }

  }

}

              因为synchronizedJava语言层面提供的语法,所以我们不需要考虑异常,而ReentrantLockJava代码实现的锁,我们就必须先获取锁,然后在finally中正确释放锁。

              顾名思义,ReentrantLock是可重入锁,它和synchronized一样,一个线程可以多次获取同一个锁。和synchronized不同的是,ReentrantLock可以尝试获取锁:

if (lock.tryLock(1, TimeUnit.SECONDS)) {

   try {

      ...

  } finally {

       lock.unlock();

  }

}

              上述代码在尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。

              所以,使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。

小结

              1ReentrantLock可以替代synchronized进行同步;

              2ReentrantLock获取锁更安全;

              3、必须先获取到锁,再进入try {...}代码块,最后使用finally保证释放锁;

              4、可以使用tryLock()尝试获取锁。

十二、Condition

              使用ReentrantLock比直接使用synchronized更安全,可以替代synchronized进行线程同步。

              但是,synchronized可以配合waitnotify实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock我们怎么编写waitnotify的功能呢?

              答案是使用Condition对象来实现waitnotify的功能。

              我们仍然以TaskQueue为例,把前面用synchronized实现的功能通过ReentrantLockCondition来实现:

class TaskQueue {

   private final Lock lock = new ReentrantLock();

   private final Condition condition = lock.newCondition();

   private Queue<String> queue = new LinkedList<>();

​

   public void addTask(String s) {

       lock.lock();

       try {

           queue.add(s);

           condition.signalAll();

      } finally {

           lock.unlock();

      }

  }

​

   public String getTask() {

       lock.lock();

       try {

           while (queue.isEmpty()) {

               condition.await();

          }

           return queue.remove();

      } finally {

           lock.unlock();

      }

  }

}

              可见,使用Condition时,引用的Condition对象必须从Lock实例的newCondition()返回,这样才能获得一个绑定了Lock实例的Condition实例。

              Condition提供的await()signal()signalAll()原理和synchronized锁对象的wait()notify()notifyAll()是一致的,并且其行为也是一样的:

  • await()会释放当前锁,进入等待状态;
  • signal()会唤醒某个等待线程;
  • signalAll()会唤醒所有等待线程;
  • 唤醒线程从await()返回后需要重新获得锁。

此外,和tryLock()类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()signalAll()唤醒,可以自己醒来:

if (condition.await(1, TimeUnit.SECOND)) {

   // 被其他线程唤醒

} else {

   // 指定时间内没有被其他线程唤醒

}

              可见,使用Condition配合Lock,我们可以实现更灵活的线程同步。

小结

              1Condition可以替代waitnotify

              2Condition对象必须从Lock对象获取。

十三、ReadWriteLock

              前面讲到的ReentrantLock保证了只有一个线程可以执行临界区代码:

public class Counter {

   private final Lock lock = new ReentrantLock();

   private int[] counts = new int[10];

​

   public void inc(int index) {

       lock.lock();

       try {

           counts[index] += 1;

      } finally {

           lock.unlock();

      }

  }

​

   public int[] get() {

       lock.lock();

       try {

           return Arrays.copyOf(counts, counts.length);

      } finally {

           lock.unlock();

      }

  }

}

              但是有些时候,这种保护有点过头。因为我们发现,任何时刻,只允许一个线程修改,也就是调用inc()方法是必须获取锁,但是,get()方法只读取数据,不修改数据,它实际上允许多个线程同时调用。

              实际上我们想要的是:允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待:

允许

不允许

不允许

不允许

使用ReadWriteLock可以解决这个问题,它保证:

  • 只允许一个线程写入(其他线程既不能写入也不能读取);
  • 没有写入时,多个线程允许同时读(提高性能)。

ReadWriteLock实现这个功能十分容易。我们需要创建一个ReadWriteLock实例,然后分别获取读锁和写锁:

public class Counter {

   private final ReadWriteLock rwlock = new ReentrantReadWriteLock();

   private final Lock rlock = rwlock.readLock();

   private final Lock wlock = rwlock.writeLock();

   private int[] counts = new int[10];

​

   public void inc(int index) {

       wlock.lock(); // 加写锁

       try {

           counts[index] += 1;

      } finally {

           wlock.unlock(); // 释放写锁

      }

  }

​

   public int[] get() {

       rlock.lock(); // 加读锁

       try {

           return Arrays.copyOf(counts, counts.length);

      } finally {

           rlock.unlock(); // 释放读锁

      }

  }

}

              把读写操作分别用读锁和写锁来加锁,在读取时,多个线程可以同时获得读锁,这样就大大提高了并发读的执行效率。

              使用ReadWriteLock时,适用条件是同一个数据,有大量线程读取,但仅有少数线程修改。

              例如,一个论坛的帖子,回复可以看做写入操作,它是不频繁的,但是,浏览可以看做读取操作,是非常频繁的,这种情况就可以使用ReadWriteLock

小结

使用ReadWriteLock可以提高读取效率:

  • ReadWriteLock只允许一个线程写入;
  • ReadWriteLock允许多个线程在没有写入时同时读取;
  • ReadWriteLock适合读多写少的场景。

十四、StampedLock

              前面介绍的ReadWriteLock可以解决多线程同时读,但只有一个线程能写的问题。

              如果我们深入分析ReadWriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。

              要进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock

              StampedLockReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。

              乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

              我们来看例子:

public class Point {

   private final StampedLock stampedLock = new StampedLock();

​

   private double x;

   private double y;

​

   public void move(double deltaX, double deltaY) {

       long stamp = stampedLock.writeLock(); // 获取写锁

       try {

           x += deltaX;

           y += deltaY;

      } finally {

           stampedLock.unlockWrite(stamp); // 释放写锁

      }

  }

​

   public double distanceFromOrigin() {

       long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁

       // 注意下面两行代码不是原子操作

       // 假设x,y = (100,200)

       double currentX = x;

       // 此处已读取到x=100,但x,y可能被写线程修改为(300,400)

       double currentY = y;

       // 此处已读取到y,如果没有写入,读取是正确的(100,200)

       // 如果有写入,读取是错误的(100,400)

       if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生

           stamp = stampedLock.readLock(); // 获取一个悲观读锁

           try {

               currentX = x;

               currentY = y;

          } finally {

               stampedLock.unlockRead(stamp); // 释放悲观读锁

          }

      }

       return Math.sqrt(currentX * currentX + currentY * currentY);

  }

}

              ReadWriteLock相比,写入的加锁是完全一样的,不同的是读取。注意到首先我们通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。

              可见,StampedLock把读锁细分为乐观读和悲观读,能进一步提升并发效率。但这也是有代价的:一是代码更加复杂,二是StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁。

              StampedLock还提供了更复杂的将悲观读锁升级为写锁的功能,它主要使用在if-then-update的场景:即先读,如果读的数据满足条件,就返回,如果读的数据不满足条件,再尝试写。

小结

              1StampedLock提供了乐观读锁,可取代ReadWriteLock以进一步提升并发性能;

              2StampedLock是不可重入锁。

十五、Semaphore

              前面我们讲了各种锁的实现,本质上锁的目的是保护一种受限资源,保证同一时刻只有一个线程能访问(ReentrantLock),或者只有一个线程能写入(ReadWriteLock)。

              还有一种受限资源,它需要保证同一时刻最多有N个线程能访问,比如同一时刻最多创建100个数据库连接,最多允许10个用户下载等。

              这种限制数量的锁,如果用Lock数组来实现,就太麻烦了。

              这种情况就可以使用Semaphore,例如,最多允许3个线程同时访问:

public class AccessLimitControl {

   // 任意时刻仅允许最多3个线程获取许可:

   final Semaphore semaphore = new Semaphore(3);

​

   public String access() throws Exception {

       // 如果超过了许可数量,其他线程将在此等待:

       semaphore.acquire();

       try {

           // TODO:

           return UUID.randomUUID().toString();

      } finally {

           semaphore.release();

      }

  }

}

              使用Semaphore先调用acquire()获取,然后通过try ... finally保证在finally中释放。

              调用acquire()可能会进入等待,直到满足条件为止。也可以使用tryAcquire()指定等待时间:

if (semaphore.tryAcquire(3, TimeUnit.SECONDS)) {

   // 指定等待时间3秒内获取到许可:

   try {

       // TODO:

  } finally {

       semaphore.release();

  }

}

              Semaphore本质上就是一个信号计数器,用于限制同一时间的最大访问数量。

小结

              如果要对某一受限资源进行限流访问,可以使用Semaphore,保证同一时间最多N个线程访问受限资源。

十六、Concurrent集合

              我们在前面已经通过ReentrantLockCondition实现了一个BlockingQueue

public class TaskQueue {

   private final Lock lock = new ReentrantLock();

   private final Condition condition = lock.newCondition();

   private Queue<String> queue = new LinkedList<>();

​

   public void addTask(String s) {

       lock.lock();

       try {

           queue.add(s);

           condition.signalAll();

      } finally {

           lock.unlock();

      }

  }

​

   public String getTask() {

       lock.lock();

       try {

           while (queue.isEmpty()) {

               condition.await();

          }

           return queue.remove();

      } finally {

           lock.unlock();

      }

  }

}

              BlockingQueue的意思就是说,当一个线程调用这个TaskQueuegetTask()方法时,该方法内部可能会让线程变成等待状态,直到队列条件满足不为空,线程被唤醒后,getTask()方法才会返回。

              因为BlockingQueue非常有用,所以我们不必自己编写,可以直接使用Java标准库的java.util.concurrent包提供的线程安全的集合:ArrayBlockingQueue

              除了BlockingQueue外,针对ListMapSetDeque等,java.util.concurrent包也提供了对应的并发集合类。我们归纳一下:

interface

non-thread-safe

thread-safe

List

ArrayList

CopyOnWriteArrayList

Map

HashMap

ConcurrentHashMap

Set

HashSet / TreeSet

CopyOnWriteArraySet

Queue

ArrayDeque / LinkedList

ArrayBlockingQueue / LinkedBlockingQueue

Deque

ArrayDeque / LinkedList

LinkedBlockingDeque

              使用这些并发集合与使用非线程安全的集合类完全相同。我们以ConcurrentHashMap为例:

Map<String, String> map = new ConcurrentHashMap<>();

// 在不同的线程读写:

map.put("A", "1");

map.put("B", "2");

map.get("A", "1");

              因为所有的同步和加锁的逻辑都在集合内部实现,对外部调用者来说,只需要正常按接口引用,其他代码和原来的非线程安全代码完全一样。即当我们需要多线程访问时,把:

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

改为:

Map<String, String> map = new ConcurrentHashMap<>();

就可以了。

java.util.Collections工具类还提供了一个旧的线程安全集合转换器,可以这么用:

Map unsafeMap = new HashMap();

Map threadSafeMap = Collections.synchronizedMap(unsafeMap);

              但是它实际上是用一个包装类包装了非线程安全的Map,然后对所有读写方法都用synchronized加锁,这样获得的线程安全集合的性能比java.util.concurrent集合要低很多,所以不推荐使用。

小结

              使用java.util.concurrent包提供的线程安全的并发集合可以大大简化多线程编程:

              多线程同时读写并发集合是安全的;

              尽量使用Java标准库提供的并发集合,避免自己编写同步代码。

十七、Atomic

              Javajava.util.concurrent包除了提供底层锁、并发集合外,还提供了一组原子操作的封装类,它们位于java.util.concurrent.atomic包。

我们以AtomicInteger为例,它提供的主要操作有:

  • 增加值并返回新值:int addAndGet(int delta)
  • 1后返回新值:int incrementAndGet()
  • 获取当前值:int get()
  • CAS方式设置:int compareAndSet(int expect, int update)

Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CASCompare and Set

如果我们自己通过CAS编写incrementAndGet(),它大概长这样:

public int incrementAndGet(AtomicInteger var) {

  int prev, next;

  do {

      prev = var.get();

      next = prev + 1;

  } while ( ! var.compareAndSet(prev, next));

  return next;

}

              CAS是指,在这个操作中,如果AtomicInteger的当前值是prev,那么就更新为next,返回true。如果AtomicInteger的当前值不是prev,就什么也不干,返回false。通过CAS操作并配合do ... while循环,即使其他线程修改了AtomicInteger的值,最终的结果也是正确的。

我们利用AtomicLong可以编写一个多线程安全的全局唯一ID生成器:

class IdGenerator {

  AtomicLong var = new AtomicLong(0);

​

  public long getNextId() {

      return var.incrementAndGet();

  }

}

              通常情况下,我们并不需要直接用do ... while循环调用compareAndSet实现复杂的并发操作,而是用incrementAndGet()这样的封装好的方法,因此,使用起来非常简单。

              在高度竞争的情况下,还可以使用Java 8提供的LongAdderLongAccumulator

小结

使用java.util.concurrent.atomic提供的原子操作可以简化多线程编程:

  • 原子操作实现了无锁的线程安全;
  • 适用于计数器,累加器等。

十八、线程池

              Java语言虽然内置了多线程支持,启动一个新线程非常方便,但是,创建线程需要操作系统资源(线程资源,栈空间等),频繁创建和销毁大量线程需要消耗大量时间。

              如果可以复用一组线程:

              那么我们就可以把很多小任务让一组线程来执行,而不是一个任务对应一个新线程。这种能接收大量小任务并进行分发处理的就是线程池。

              简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。

              Java标准库提供了ExecutorService接口表示线程池,它的典型用法如下:

// 创建固定大小的线程池:

ExecutorService executor = Executors.newFixedThreadPool(3);

// 提交任务:

executor.submit(task1);

executor.submit(task2);

executor.submit(task3);

executor.submit(task4);

executor.submit(task5);

因为ExecutorService只是接口,Java标准库提供的几个常用实现类有:

  • FixedThreadPool:线程数固定的线程池;
  • CachedThreadPool:线程数根据任务动态调整的线程池;
  • SingleThreadExecutor:仅单线程执行的线程池。

创建这些线程池的方法都被封装到Executors这个类中。我们以FixedThreadPool为例,看看线程池的执行逻辑:

import java.util.concurrent.*;public class Main {

   public static void main(String[] args) {

       // 创建一个固定大小的线程池:

       ExecutorService es = Executors.newFixedThreadPool(4);

       for (int i = 0; i < 6; i++) {

           es.submit(new Task("" + i));

      }

       // 关闭线程池:

       es.shutdown();

  }

}

​

class Task implements Runnable {

   private final String name;

​

   public Task(String name) {

       this.name = name;

  }

​

   @Override

   public void run() {

       System.out.println("start task " + name);

       try {

           Thread.sleep(1000);

      } catch (InterruptedException e) {

      }

       System.out.println("end task " + name);

  }

}

              我们观察执行结果,一次性放入6个任务,由于线程池只有固定的4个线程,因此,前4个任务会同时执行,等到有线程空闲后,才会执行后面的两个任务。

              线程池在程序结束的时候要关闭。使用shutdown()方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关闭。shutdownNow()会立刻停止正在执行的任务,awaitTermination()则会等待指定的时间让线程池关闭。

              如果我们把线程池改为CachedThreadPool,由于这个线程池的实现会根据任务数量动态调整线程池的大小,所以6个任务可一次性全部同时执行。

              如果我们想把线程池的大小限制在410个之间动态调整怎么办?我们查看Executors.newCachedThreadPool()方法的源码:

public static ExecutorService newCachedThreadPool() {

   return new ThreadPoolExecutor(0, Integer.MAX_VALUE,

                                   60L, TimeUnit.SECONDS,

                                   new SynchronousQueue<Runnable>());

}

              因此,想创建指定动态范围的线程池,可以这么写:

int min = 4;

int max = 10;

ExecutorService es = new ThreadPoolExecutor(min, max,

       60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

ScheduledThreadPool

              还有一种任务,需要定期反复执行,例如,每秒刷新证券价格。这种任务本身固定,需要反复执行的,可以使用ScheduledThreadPool。放入ScheduledThreadPool的任务可以定期反复执行。

              创建一个ScheduledThreadPool仍然是通过Executors类:

ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);

              我们可以提交一次性任务,它会在指定延迟后只执行一次:

// 1秒后执行一次性任务:

ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);

              如果任务以固定的每3秒执行,我们可以这样写:

// 2秒后开始执行定时任务,每3秒执行:

ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);

              如果任务以固定的3秒为间隔执行,我们可以这样写:

// 2秒后开始执行定时任务,以3秒为间隔执行:

ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);

              注意FixedRateFixedDelay的区别。FixedRate是指任务总是以固定时间间隔触发,不管任务执行多长时间:

              FixedDelay是指,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务:

              因此,使用ScheduledThreadPool时,我们要根据需要选择执行一次、FixedRate执行还是FixedDelay执行。

              细心的童鞋还可以思考下面的问题:

                            1、在FixedRate模式下,假设每秒触发,如果某次任务执行时间超过1秒,后续任务会不会并发执行?

                            2、如果任务抛出了异常,后续任务是否继续执行?

              Java标准库还提供了一个java.util.Timer类,这个类也可以定期执行任务,但是,一个Timer会对应一个Thread,所以,一个Timer只能定期执行一个任务,多个定时任务必须启动多个Timer,而一个ScheduledThreadPool就可以调度多个定时任务,所以,我们完全可以用ScheduledThreadPool取代旧的Timer

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/数据流舞者/article/detail/63221
推荐阅读
相关标签
  

闽ICP备14008679号