语法基础
JDK
Java运行机制
- 编译期:.java源文件,经过编译,生成.class字节码文件。
- 运行期:JVM加载.class并运行.class。
特点:跨平台、一次编程到处使用。
JVM、JRE、JDK的区别
- JVM
- Java Virtual Machine,Java虚拟机。
- 加载.class并运行.class。
- JRE
- Java Runtime Environment,Java运行环境。
- 除了包含JVM以外还包含了运行Java程序所必须的环境。即JVM+系统类库。
- JDK
- Java Development Kit,Java开发工具包。
- 除了包含JRE以外还包含了开发Java程序所必须的环境,即JRE+编译、运行等工具命令。
跨平台的实现机制
Java将源文件编译成字节码(.class)文件,最终由JVM负责将字节码文件加载并翻译成特定平台下的机器码运行,但对于不同的运行平台,需要安装不同版本的JVM,这就是Java虚拟机机制,该机制屏蔽了不同运行平台底层的差异,实现了跨平台性。
安装JDK
下载安装即可,不用再安装JRE,推荐解压版。
环境配置
- JAVA_HOME:JDK安装目录
- PATH:%JAVA_HOME%\bin
在cmd中使用java、javac、java -version检测是否配置成功。
基本语法
注释
作用:
- 便于代码的阅读。
- 生成文档。
语法:
- 单行注释://
- 多行注释:/* */
- 文档注释:/** */
标识符
用来给包、类、变量进行命名。
规定:
- 只能包含字母、数字、_和$符,且不能以数字开头。
- 严格区分大小写,且长度无限制。
- 不能使用关键字。
- 可以中文命名,但不建议。
规范:
- 包名必须全小写。
- 类名每个单词的首字母大写,帕斯卡命名。
- 方法和变量第一个单词小写,从第二个单词开始首字母大写。
- 常量名全大写,用_分割单词。
变量
变量的本质就是代表一个“可操作的存储空间”,空间位置是确定的,但是里面放置什么不确定,我们可以通过变量名来访问对应的存储空间,从而操作这个存储空间的值。
Java是一种强类型语言,每个变量都必须声明其数据类型,变量的数据类型决定了变量占据存储空间的大小。
分类:
局部变量必须手动初始化,编译器只会给成员变量自动初始化。
数据类型
分类:
基本数据类型
int整型
- 整数直接量默认为int型,但不能超出范围,超范围则编译错误。
- 两个整数相除,结果还是整数,小数位无条件舍弃(不会四舍五入)。
- 整数运算时若超出范围则发生溢出,溢出是需要避免的。
long整型
- 长整型直接量需在数字后加L或l。
- 运算时若有可能溢出,建议在第1个数字加L。
- System.currentTimeMillis()用于获取自1970.1.1零时到此时此刻的毫秒数(时间戳)。
double浮点型
- 浮点数直接量默认为double型,表示float需在数字后加F或f。
- double或float型数据在参与运算时,有可能发生舍入误差。
浮点数
浮点数存在舍入误差,数字不能精确表示,如果需要精确计算,使用“BigDecimal”类。
字符类型
注意字符用单引号,字符串用双引号。
运算符
自增自减
a++,++a在单独使用时并没有区别。在被使用时有区别,a++先运算后加,++a先加再运算。
int a = 5;
int b = 5;
System.out.println(a++); //5
System.out.println(a); //6
System.out.println(++b); //6
System.out.println(b); //6
逻辑运算符
逻辑运算建立在关系运算的基础之上,参与逻辑运算的都是boolean类型。
短路原则:从左往右计算,如果只通过运算符左边的操作数就能够确定该逻辑表达式的值,则不会继续计算运算符右边的操作数,提高效率。
位运算符
对二进制进行计算。
左移(<<):在内存中将该变量的二进制数直接向左移动,超出内存的丢弃,右边补0,所以结果将乘2的n次幂。
带符号右移(>>):在内存中将该变量的二进制数直接向右移动,超出部分丢弃,左边根据最高位(最边左)是什么补什么,所以结果将除2的n次幂。
无符号右移(>>>):与带符号右移的区别是左边始终补0,负数的无符号右移慎用。
类型转换
自动类型转换
实线表示无数据丢失,自动类型转换,而虚线表示在转换时可能有精度类损失的。在算术运算中,数据会自动向上型提升,注意byt类、ehors、tcar会先转为hnt类型再进行运算。
强制类型转换
强制类型转换,又被称为造型,用于显式的转换一个数值的类型。在有可能丢失信息的情况下进行的转换是通过造型来完成的,但可能造成精度降低或溢出。
(type)var
控制语句
控制语句的分类
- 顺序结构
- 分支结构(选择结构)
- 循环结构
任何复杂的程序逻辑都可以通过这三种结构来实现。
分支结构的分类
- if(单选择)
- if … else(双选择)
- if … else if … else(多选择)
- switch(多选择)
if语句
if(布尔表达式){
语句块;
}
布尔表达式为true才执行内部语句块,为false不执行。
if-else语句
if(布尔表达式){
语句块1;
}else{
语句块2;
}
if-else if-else语句
if(布尔表达式1) {
语句块1;
} else if(布尔表达式2) {
语句块2;
}……
else if(布尔表达式n){
语句块n;
} else {
语句块n+1;
}
switch
switch (表达式) {
case 值1:
语句序列1;
break;
case 值2:
语句序列2;
break;
...
default:
默认语句;
}
- 优点:效率高,结构清晰。
- 缺点:只能判断整数,相等,无法逻辑判断。
switch有穿透效果,如果没有break会从入口进入继续运行下一个case,可以利用这个特点将重复条件的语句放在一起。
switch语句中case标签在JDK1.5之前必须是整数(long类型除外)或者枚举,不能是字符串,在JDK1.7之后允许使用字符串(String)
byte、short、int、char、String。
循环结构的分类
- while
- do while
- for
- foreach
while
while (布尔表达式) {
循环体;
}
先判断,当布尔表达式为true时会执行循环体,然后继续判读、执行,直到布尔表达式为false结束。先判度后执行。
三要素:
- 循环变量的初始化。
- 循环的条件。
- 循环变量的改变。
do while
do {
循环体;
} while(布尔表达式) ;
先执行,再判断布尔表达式。先执行后判断。
for
for (初始表达式; 布尔表达式; 迭代因子) {
循环体;
}
foreach
增强型for循环,在遍历数组时推荐使用普通for循环,效率高;但是对于集合等遍历,推荐使用增强型,效率高。
for(数据类型 变量 : 迭代对象){
循环体;
}
没有下标。
跳出循环
break:
跳出循环,不再执行之后的代码。
continue:
跳过循环体中剩余语句而进入下一次循环。
数组
相同数据类型元素的集合。也是一种数据类型,引用类型。
数组的初始化
int[] a = {1,2,3}; // 在声明数组的同时进行初始化
int[] b new int[10]; // 动态初始化
int[] c = new int[]{1,2,3}
- int length可以返回数组的长度。
- 通过一个整型下标可以访问数组中的每一个值。
- 数组下标从0开始。
数组的拷贝
System.arraycopy(src, srcPos, dest, destPos, length);
将src数组中srcPos位置,长度为length的内容拷贝到dest数组的destPos位置。
Arrays.copyOf(original, newLength);
只能从头开始复制,从头开始放置,灵活性差,底层由System.arraycope实现,用于数组的扩容。
匿名数组
new int[] { 17, 19, 23, 29, 31, 37};
这种表示将创建一个新数组并利用括号中提供的值进行初始化,数组的大小就是初始值的个数,使用这种语法形式可以在不创建新变量的情况下重新初始化一个数组。
数组的排序
Arrays.sort(byte[] a);
该方法使用了优化的快速排序算法,快速排序算法对于大多数数据集合来说都是效率比较高的。
数组的查找
int binarySearch(type[]a, int start, int end, type v);
采用二分法搜索算法查找值v,返回位置,否则返回-r。
排序算法
稳定与非稳定:
如果一个排序算法能够保留数组中重复元素的相对位置则可以被称为是稳定的。反之,则是非稳定的。
选择排序
用第一个位置的元素与其后的所有元素依次比较,将比其大/小的元素置换在第一个位置;然后继续第二个位置的元素,直到length-1个位置元素。
int temp;
for (int i = 0; i < array.length-1; i++) {
for (int j = i+1; j < array.length; j++) {
if (array[i] > array[j]) {
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
}
// 外层循环控制比较位置
// 内存循环控制比较次数
冒泡排序
用相邻的两个元素比较,将最小/最大的元素冒泡到最后位置。
int temp;
for (int i = 0; i < array.length-1; i++) {
for (int j = 0; j < array.length-i-1; j++) {
if (array[j] > array[j+1]) {
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
// 外层循环控制起泡次数
// 内存循环控制比较次数
二分法查找
二分法查找也称为折半查找,适用于数据量较大时,但数据需要先排好序。
public static int binarySearch(int[] array, int num) {
int flag = -1;
int star = 0;
int end = array.length-1;
int mid = 0;
while(star <= end){
mid = (star+end)>>2;
if(array[mid] != num){
if (array[mid] > num) {
end = mid - 1;
}else{
star = mid + 1;
}
}else{
flag = mid;
break;
}
}
return flag;
}
方法
方法的重载
overload
同一个类中方法名称相同,参数列表不同构成方法的重载。参数列表的不同可以是形参类型不同,形参个数不同,形参顺序不同。方法的重载与返回值类型无关。
方法的重写
override
子类通过重写父类的方法,可以用自身的行为替换父类的行为。
- 方法名、形参列表相同。
- 返回值类型子类小于等于父类。
- 声明异常类型小于等于父类。
- 访问权限,子类大于等于父类。
可以在重写的方法头上加注解@Override。
方法重写后调用的方法看对象的类型,即new后的类型。
面向对象
面向对象的三大特征:
- 封装。
- 继承。
- 多态。
构造器
构造器也叫构造方法,用于对象的初始化。
- 构造器的方法名必须和类名一致。
- 不能定义返回值类型。构造器是有返回值的,返回值的类型肯定是本类,不能在构造器里使用return返回某个值。
- 如果我们没有定义构造器,则编译器会自动添加无参构造。如果自定义了构造器(无论是否有参),编译器不会再自动添加。
- 构造器支持方法的重载。
- 在构造的时候会初始化对象,给未赋值的成员变量赋默认值。
对象的创建过程
- 分配对象空间,并将对象成员变量初始化为0或空。
- 执行属性值的显示初始化。
- 执行构造方法。
- 返回对象的地址给相关变量
this
this的本质就是创建好的对象的地址,由于在构造方法调用前,对象已经创建,因此,在构造方法中也可以使用this代表当前调用该方法的对象。哪个对象调用方法,this就指代哪个对象。
- this不能用于static方法中。
- this只能用在方法中。
参数传值机制
- 基本数据类型参数的传值:
Java中,方法中所有的参数都是”值传递“,也就是传递的是”值的副本“,副本的改变不会影响原数据。 - 引用类型参数的传值:
引用类型传递的是”对象的地址“,传递的也是值的副本,但副本和原件都指向了同一个地址,改变副本指向对象的值,也意味着原件指向对象的值也发生了改变。
封装
封装的优点:
- 提高代码的安全性。
- 提高代码的复用性。
- “高内聚”:封装细节,便于修改内部代码,提高可维护性。
- “低耦合”:简化外部调用,便于调用者使用,便于扩展和协作。
继承
利用继承可以基于已存在的类构造一个新类,继承已存在的类就是复用这些类的方法和域,在此基础上还可以添加一些新的方法和域,以满足需求。
作用:代码复用。
关键字:extends。
继承的要点
- 父类也称超类、基类。子类也称派生类。
- 超类中放派生类所共有的属性和行为。派生类中放派生类所特有的属性和行为。
- 派生类继承超类后,派生类具有:派生类的+超类的。
- Java中只有单继承,没有多继承,可以多重继承,继承具有传递性,接口有多实现。
- 字类继承父类,得到父类的全部属性和方法,除了构造方法。但不见得可以直接访问,比如父类私有的属性和方法。
- 如果定义一个类没有调用extends,则它的父类是:java.lang.Object。
- 子类的加载会先加载其父类的构造方法。
- 抽取超类的过程叫泛化。
- 被final修饰的类不可被继承,修饰的方法不可被重写。
super
super是直接父类对象的引用,可以通过super来访问父类中被子类覆盖的方法和属性。
继承的缺点
提高了类之间的耦合度,耦合度高就会造成代码之间的联系越紧密,代码独立性差。
子类构造器
如果子类构造器没有显式的调用父类的构造器,则将自动的调用父类默认(没有参数)的构造器。如果父类没有无参构造器,并且在字类的构造器中又没有显示的调用父类的其他构造器,则Java编译器将报错。
instanceof
判断对象是否为某个对象的字类。
向上造型
超类的引用指向派生类的对象。
Persion p = new Student();
能调用什么属性和方法关键看引用的类型。
多态
多态指的是同一个方法调用,由于对象不同可能会有不同的行为。
多态的要点:
- 多态是方法的多态,不是属性的多态(多态与属性无关)。
- 多态的存在要有3个必要条件
- 继承。
- 方法重写。
- 父类引用指向子类对象。
- 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。
final
- 修饰类
- 表示这个类不能被继承。
- final类中的成员方法都会被隐式的指定为final方法。
- 修饰方法
- 表示这个方法不能被重写。
- 一个类的private方法会隐式的被指定为final方法。
- 修饰变量
- 表示该变量不可修改,只能被赋值一次。
- 该变量称为常量。
static
- 静态域
- 静态域从属于类,类被加载,静态域便存在。与对象无关,即便没有对象,静态域也存在。静态域的加载优先于代码块的加载。
- 静态方法
- 静态方法是一种不能向对象实施操作的方法。
- 静态方法无法访问成员变量。因为成员变量从属于对象,想要访问成员变量必须有个“已经实例化好的对象”。
- 静态常量
- 静态变量使用的比较少,但静态常量却用的比较多,例如Math类中的PI,
对象包装器与自动装箱
在Java中只有基本数据类型不是对象,不符合Java的一切皆对象。
包装器(wrapper)
Integer、Long、Float、Double、Short、Byte、Character 、Void和 Boolean
对象包装器类还是final , 因此不能定义它们的子类。
当将一个Integer对象赋给一个int值时, 将会自动地拆箱。
注意:由于包装器类引用可以为 null, 所以自动装箱有可能会抛出一个 NullPointerException 异常。
内部类
- 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
- 内部类可以对同一个包中的其他类隐藏起来。
- 当想要定义一个回调函数且不想编写大量代码时,使用匿名 (anonymous) 内部类比较便捷。
API
时间类
JDK8增加了一套时间类API,在java.time包中,关键类:
- Instant:代表的是时间戳。
- LocalDate:不包含具体时间的日期。
- LocalTime:不含日期的时间。
- LocalDateTime:包含了日期及时间。
时间的比较
在JDK8中,LocalDate类中使用isBefore()、isAfter()、equals()方法来比较两个日期,可直接进行比较。
时间的格式化
在JDK8之前,时间日期的格式化非常麻烦,经常使用SimpleDateFormat来进行格式化,但是SimpleDateFormat并不是线程安全的。在JDK8中,引入了一个全新的线程安全的日期与时间格式器DateTimeFormatter。
String
字符串,在使用+拼接的时候,String有同化作用,一旦遇到Stirng所有类型转String类型。
System.out.println('a'+'A'); //97+65=162
System.out.println(10+5+"50"+30); //155030
集合
数组容器
数组就是一种容器,可以在其中放置对象或基本数据类型。
数组的优势
- 是一种简单的线性序列,可以快速的访问数组元素,效率高。
数组的劣势
- 不灵活,容量需要实现定义好,不能随需求的改变而扩容。
集合的分类
- List:有序、可重复。
- Set:无序、不可重复。
- Map:有映射关系。
- Query:队列集合。
Conllection接口
List
List接口常用的实现类有3个:ArrayList、LinkedList和Vector。
ArrayList
ArrayList底层是用数组实现的存储。
特点:查询效率高,插入效率低,线程不安全,默认长度10,1.5倍扩容。
LinkedList
LinkedList底层用双向链表实现的存储。
特点:查询效率低,增删效率高,线程不安全。
Vector
Vector底层是用数组实现的List,相关的方法都加了同步检查,因此“线程安全,效率低”,2倍扩容。
如何选用ArrayList、LinkedList、Vector?
- 需要线程安全时,用Vector。
- 不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)。
- 不存在线程安全问题时,增加或删除元素较多用LinkedList。
Set
Set接口继承自Collection,Set接口中没有新增方法,方法和Collection保持完全一致。
Set常用的实现类有:HashSet、TreeSet等,我们一般使用HashSet。
HashSet
HashSet是采用哈希算法实现,底层实际是用HashMap实现的(HashSet本质就是一个简化版的HashMap),因此,查询效率和增删效率都比较高。
TreeSet
TreeSet底层实际是用TreeMap实现的,内部维持了一个简化版的TreeMap,通过key来存储Set的元素。 TreeSet内部需要对存储的元素进行排序,因此,我们对应的类需要实现Comparable接口。这样,才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。
使用TreeSet要点:
- 由于是二叉树,需要对元素做内部排序。 如果要放入TreeSet中的类没有实现Comparable接口,则会抛出异常:java.lang.ClassCastException。
- TreeSet中不能放入null元素。
Map
Map就是用来存储“键(key)-值(value) 对”的。 Map类中存储的“键值对”通过键来标识,所以“键对象”不能重复。
Map接口的实现类有HashMap、TreeMap、HashTable。
HashMap
HashMap采用哈希算法实现,是Map接口最常用的实现类。 由于底层采用了哈希表存储数据,我们要求键不能重复,如果发生重复,新的键值对会替换旧的键值对。
HashMap在查找、删除、修改方面都有非常高的效率。
数据结构
在Java7的时候,HashMap的实现是“数组+链表”的形式,在之后的版本中加入了“红黑树”。
在链表的长度大于等于8的时候会转为红黑树。
当HashMap在put()一个键值对进入容器的时候,首先会通过哈希值根据数组的长度来计算一个位置,然后将<key,value>的节点加入进容器。
方法是:index = hash % length
理想状态是将各个元素均匀的分布在数组中,这样的优势是可以充分的利用数组上的空间。但是hashcode是一个随机值,导致计算后的节点位置可能会出现重复,即哈希碰撞。
为了解决哈希碰撞问题,引入了链表结构。
虽然链表的插入元素效率高,但是查询效率低,时间复杂度为O(n),而数组查询时间复杂度为O(1),比链表效率高n个数量级。所以当链表的长度太长时,查询的效率是不可容忍的。
然后就引入了红黑树结构。
红黑树是一种自平衡二叉查找树,每个节点都带有颜色属性,红色或黑色,根节点是黑色,每个红色节点的两个子节点都是黑色。
红黑树的查询时间复杂的为O(logN),效率优于链表。
为何不在链表长度为8前转为红黑树
因为红黑树自身插入的维护代价是比较高的,每次插入元素极有可能会破坏它的平衡,破环后就需要再平衡红黑树,用到左旋、右旋、重新着色。
为什么要在长度大于等于8才转为红黑树
因为底层根据泊松分布概率学,当链表节点到8时,发生哈希碰撞的概率接近0。
为何初始容量要是2的指数次幂
注意在put()方法中,会对初始容量进行设置,Find a power of 2 >= toSize,即比传入容量大的最接近2的指数次幂的数。
第一、效率问题,底层用的取模运算,而在运算的效率上:加法 > 乘法 > 除法 > 取模。甚至在扩容的时候,涉及到数据的迁移,运算更慢。所以改进了运算方法:h & (length-1)。
第二、在计算数组索引的时候,使用的方法是:h & (length-1),h为hashcode,如果length不是2的指数次幂,计算结果和 index = hash % length是不一致的,只有length为2的指数次幂才能保证计算的结果永远在数组的区间范围内。
加载因子为什么为0.75
加载因子越大,空间的利用率越高,但是哈希碰撞的可能性越大;加载因子越小,查询效率越高,哈希碰下的可能性越小,但是空间的利用率越低。在空间利用率和时间复杂度上折中,取到了0.75。
HashTable
HashTable类和HashMap用法几乎一样,底层实现几乎一样,只不过HashTable的方法添加了synchronized关键字确保线程同步检查,效率较低。
HashMap与HashTable的区别
- HashMap: 线程不安全,效率高。允许key或value为null。
- HashTable: 线程安全,效率低。不允许key或value为null。
TreeMap
TreeMap是红黑二叉树的典型实现。
TreeMap和HashMap实现了同样的接口Map,因此,用法对于调用者来说没有区别。HashMap效率高于TreeMap;在需要排序的Map时才选用TreeMap。
多线程
多线程的实现方法
- 继承Thread类。
- 实现Runnable接口。
- 实现Callable接口。
注意少用继承,多用实现,在Java中有单继承的局限性。
继承Thread
//继承Thread
public class ThreadTest extends Thread {
//run()方法中是线程体
@Override
public void run() {
for (int i = 0; i < 30; i++) {
//getName()返回线程名称
System.out.println(this.getName()+":"+i);
}
}
public static void main(String[] args) {
//创建线程对象
ThreadTest threadTest1 = new ThreadTest();
//启动线程
threadTest1.start();
ThreadTest threadTest2 = new ThreadTest();
threadTest2.start();
}
}
实现Runnable
public class RunnableTest implements Runnable {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
public static void main(String[] args) {
//创建对象,把实现类的对象作为参数传入
Thread thread1 = new Thread(new RunnableTest());
thread1.start();
Thread thread2 = new Thread(new RunnableTest());
thread2.start();
}
}
反射
Class类
设计模式
Group of four(GOF23),共有23种设计模式。
设计模式分类
- 创建型模式
- 单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式。
- 结构型模式
- 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
- 行为型模式
- 模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。
单例模式
保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
单例模式的优点
由于单例模式只生成一个实例,减少了系统性能的开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
常见的五种单例模式实现方式
主要:
- 饿汉式(线程安全,调用效率高,不能延时加载)。
- 懒汉式(线程安全,调用效率不高,可以延时加载)。
其他:
- 双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题,不建议使用)。
- 静态内部类式(线程安全,调用效率高,可以延时加载)。
- 枚举单例(线程安全,调用效率高,不能延时加载)。
饿汉式
//饿汉式
public class Singleton1 {
//2、提供静态方法,类初始化时加载这个对象实例
private static Singleton1 instance = new Singleton1();
//1、私有构造器
private Singleton1() {}
//3、对外提供方法
public static Singleton1 getSingleton1() {
return instance;
}
}
这种方法在类加载的时候就会立即初始化对象,天然的线程安全模式。但同时没有延时加载的优势,浪费资源。方法没有同步,调用效率高。
懒汉式
//懒汉式
public class Singleton2 {
//2、提供静态属性,不创建对象,用到再创建
private static Singleton2 instance;
//1、私有化构造器
private Singleton2() {}
//3、对外提供方法,方法同步,调用效率低
public static synchronized Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
资源利用率高了,但是每次调用getInstance()方法都要同步,并发效率低。
静态内部类
//静态内部类
public class Singleton3 {
//1、私有化构造器
private Singleton3() {}
//2、创建内部类
private static class SingletonClass {
private static final Singleton3 instance = new Singleton3();
}
//3、对外提供方法
public static Singleton3 getInstance() {
return Singleton3.SingletonClass.instance;
}
}
线程安全、调用效率高、实现懒加载。
- 外部类没有static属性,则不会像饿汉式那样立即加载对象。
- 只有真正调用getInstance(),才会加载静态内部类,加载类时是线程安全的。instance是static final类型,保证了内存中只有一个实例存在,而且只能被赋值一次,从而保证了线程安全性。