Java面试笔记
Java面试笔记
第一章:Java基础知识
1.1 Java程序初始化顺序
Java程序初始化一般遵循以下三个原则(优先级依次递减)
- 静态对象(变量)优先于非静态对象初始化
- 静态对象初始化一次
- 非静态对象可能初始化多次
- 父类优先于子类初始化
- 按照成员变量定义顺序进行初始化
- 即使变量定义散布于方法定义之中,它们依然在任意方法(包括构造方法)被调用之前进行初始化
1.2 构造方法
构造方法具有如下特点:
- 构造方法名必须与类名相同,并且不能有返回值;
- 每个类可以有多个构造方法;
- 如果开发者不指定构造方法,那么构造方法将由编译器自动生成;
- 如果开发者指定了构造方法,那么编译器将不会再创建构造方法;
- 构造方法可以有0,1,或者1个以上的参数;
- 构造方法只能伴随着new关键字一起被调用,不能由开发者单独调用;
- 构造方法的主要功能是完成对象的初始化工作;
- 构造方法不能被继承,因此也不能被重写,但是可以被重载;
- 重写:函数名与形参列表、返回值类型都必须与原函数相同,发生在子类继承父类时;
- 重载:函数名相同,但是函数的形参个数或者类型不相同,发生在本类内部;
- 子类可以使用super关键字来显式地调用父类的构造方法;
- 如果父类中提供了无参构造方法,此时子类就不可以显式地调用父类的构造方法,此时编译器默认调用父类的无参构造方法;
- 当有父类存在时,实例化子类对象时会先执行父类的构造方法,然后才会执行子类的构造方法;
- 当父类和子类都没有构造方法的时候,编译器会给二者均生成默认无参构造方法,该方法只与类的修饰符保持一致(
public
、private
、protect
) - 普通方法也可以与构造方法同名,互不影响,但是普通方法在定义时需要返回值类型。
1.3 Java中的clone方法
- Java中没有指针;
- 每一个new语句返回的都是一个指针的引用;
- Java中的值传递与引用传递:
- 在处理基本数据类型时采用值传递;
- 在处理所有非基本数据类型时采用引用传递;
- 当需要对象拷贝这一功能时就需要使用
clone()
函数; - 对于
clone()
函数的一般约定: - ★
x.clone() != x;
即克隆对象与原对象不是同一个对象; - ★
x.clone().getClass() <span style="font-weight: bold;" class="mark"> x.getClass();
即克隆的是同一类型的对象; x.clone().equals(x) </span> true;
如果x.equals()方法定义恰当的话;- (可选)
clone()
函数的基本运行流程 - 继承Cloneable接口,该接口没有任何方法;
- 在类中重写
Object
的clone()
方法; - 在
clone()
方法中调用super.clone()
方法; - 把浅拷贝的引用指向原型对象的新克隆体;
- 浅拷贝与深拷贝
- 浅拷贝:被clone的对象拥有与原来对象中相同的值,但是对象的引用仍然指向原来的对象;
- 深拷贝:引用域所指向的对象也克隆一遍;
1.4 反射
- 定义:反射就是把java类中的各种成分映射成一个个的Java对象。
- 功能:反射能直接操作类私有属性。反射可以在运行时获取一个类的所有信息,(包括成员变量,成员方法,构造器等),并且可以操纵类的字段、方法、构造器等部分。
- 注意:反射可以直接操纵
- 常用的反射方法:
- 获取类的构造方法
getConstructor(参数类型列表)
//获取公开的构造方法getConstructors()
//获取所有的公开的构造方法getDeclaredConstructors()
//获取所有的构造方法,包括私有getDeclaredConstructor(int.class,String.class)
- 获取类的成员变量的方法
getFields()
//获取所有公开的成员变量,包括继承变量getDeclaredFields()
//获取本类定义的成员变量,包括私有,但不包括继承的变量getField(变量名)
getDeclaredField(变量名)
- 获取类的方法
getMethods()
//获取所有可见的方法,包括继承的方法getMethod(方法名,参数类型列表)
getDeclaredMethods()
//获取本类定义的的方法,包括私有,不包括继承的方法getDeclaredMethod(方法名,int.class,String.class)
1.5 Lambda表达式
- 简介:在Java8以前,Lambda表达式本质是一个匿名函数;现在是一种简化匿名内部类的代码写法;
- 基本格式(Java8以后):
(参数列表)->表达式语句
(参数列表)->{多个表达式语句;}
- 几个简单的例子
// 1. 不需要参数,返回值为 5
() -> 5
// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 3. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y
// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)
- 注意事项(即Lambda表达式不具有大括号分割变量作用域的功能)
- 可以直接在 lambda 表达式中访问外层的局部变量;
- 隐性的具有
final
的语义:lambda 表达式的局部变量可以不用声明为**final**
,但是必须不可被后面的代码修改 - 在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。
1.6 Java多态的实现机制
- 多态的两种表现方式:重载(Overload)和重写(Override)
- 重载
- 发生在类内部或者子类中;
- 方法名必须相同;
- 方法的参数列表一定不同;
- 访问修饰符和返回值类型可以相同也可以不同;
- 重写
- 一般发生在父类和子类之间;
- 方法名、返回值类型、参数列表必须相同;
- 访问权限不能比父类中被重写的方法的访问权限更低;
- 同一个包中,子类可以重写父类所有方法,除了父类中的
private
和final
方法; - 构造方法不能被重写;
1.7 重载(Overload)和重写(Override)的区别与联系
- 重载
- 发生在类内部或者子类中;
- 方法名必须相同;
- 方法的参数列表一定不同;
- 访问修饰符和返回值类型可以相同也可以不同;
- 重写
- 一般发生在父类和子类之间;
- 方法名、返回值类型、参数列表必须相同;
- 访问权限不能比父类中被重写的方法的访问权限更低;
- 同一个包中,子类可以重写父类所有方法,除了父类中的
private
和final
方法; - 构造方法不能被重写;
1.8 抽象类(Abstract)和接口(Interface)的区别与联系
1.8.1 抽象类
- 定义:抽象类是类和类之间的共同特征,将这些共同特征进一步形成抽象类;
- 要求(了解):
- 被
abstract
关键字定义的类就是抽象类,采用abstract
关键字定义的方法就
是抽象方法 - 抽象类无法创建对象,但抽象类有构造函数可以供子类使用;
- 抽象方法不能被
final
修饰,因为抽象方法就是被子类实现的; - 抽象类中不一定有抽象方法,抽象方法必须出现在抽象类中;
final
和abstract
不能同时同时使用,这两个关键字是对立的;- 抽象类的子类可以是抽象类。也可以是非抽象类;
- 抽象方法表示没有实现的方法,没有方法体的方法;
1.8.2 接口
- 定义:接口是特殊的抽象类,类与类是继承extends,类与接口是实现implements,其实都是继承;
- 要求(了解):
- 支持多继承,且一个接口可以继承多个接口;
- 接口中的方法是抽象,所以不能有方法体;
- 定义抽象方法的时候可以省略修饰符
public abstract
1.8.3 总结(重点)
比较内容 | 抽象类 | 接口 |
---|---|---|
构造方法 | 可以有 | 不可以有 |
方法 | 可以有抽象方法(抽象方法只能被abstract |
|
修饰,不可被private 、static 、synchronized 和native 修饰)和普通方法 |
只能有抽象方法,但1.8版本之后可以有默认方法。接口只有定义,不可有方法实现 | |
实现 | extend |
implments |
类修饰符 | public 、default 、protected |
默认public |
变量 | 可以有常量也可以有变量 | 只能是静态常量默认有public |
、static 、final 修饰,必须赋上初始值,并且不能被修改 |
||
多继承 | 只允许单继承 | 允许实现多个接口 |
静态方法 | 可以有 | 不可以 |
1.9 break
、continue
和return
的区别与联系(包括goto
)
break
:立刻终止break
语句所在的循环层的循环,外层循环不受影响;
continue
:立刻结束该轮次的循环,进入循环的下一轮次。循环中本轮次,在continue
后的语句本轮次不再执行;
return
:return
是一个跳转语句,用来表示从一个方法返回的数据结构,可以试用程序控制返回到调用它方法的地方。当main
方法被执行时,main
方法中的reuturn
语句可以使程序执行返回到Java运行系统。
goto
:goto
是Java的保留字,但是Java并没有实现goto
作为对应C++的功能,但是可以通过标识符:
的形式定义标签,以便于Java使用break
和continue
对于多重循环的控制。
1.10 switch
使用时有什么需要注意的
switch
的在Java中属于分支语句的一类,同类的分支语句包括if
、else
等;- 语法格式
switch (表达式) {
case 常量表达式或枚举常量:
语句;
break;
case 常量表达式或枚举常量:
语句;
break;
......
default: 语句;
break;
}
- 表达式所返回的类型有包含如下几种:
- 整型:
byte
、short
、int
- 字符:
char
- 字符串:
string
- 枚举类型
- 注意事项
case
后面跟的是要和表达式进行比较的值(被匹配的值);break
表示中断,结束的意思,用来结束switch
语句;default
表示所有情况都不匹配的时候,就执行该处的内容;
1.11 volatile
在Java中有什么作用
- 目的:为了线程安全;
- 线程安全
- 定义:在多线程情况下,对共享内存的使用不会因为不同线程的访问修改而发生不期望的情况;
- 三要素
- ✅可见性;
- ✅有序性;
- ❎原子性;
volatile
三个作用- 解决多核CPU高速缓存导致的变量不同步(解决可见性问题);
- CPU与内存之间存在多级缓存
- 不同处理器对于数据的修改逻辑不同从而发生变量不同步现象
如上述两图所示,X的变量同时被不同的处理器修改成各自的Y和Z
2.volatile
基于内存屏障解决变量不同步 - 当GPU写入数据时,若发现一个变量在其他变量中存有副本;
- 此时会发出信号通知其它CPU将该副本对应的缓存行置为无效状态;
- 当CPU读取变量副本,发现该缓存行无效时,它会重新从主内存中重新读取变量。
- 解决指令重排序问题(解决有序性问题);
- 一般情况下,指令会按照顺序从上到下、从左往右执行;
- 对于存在依赖关系的多个指令,譬如
int i=0;
和i++;
这两个指令,一定是先有变量i的定义,后对i进行操作; - 对于不存在依赖关系的多个指令,CPU在运行期间会对指令进行优化,即CPU会基于最优化的方式对不存在依赖关系的指令进行重排;
- 单线程下指令重排不会引发任何问题;
- 多线程下指令重排会引发很多错误,这会让线程“不安全”,因为这违反了线程安全规则之一的“有序性”;
- 被
volatile
修饰过的变量将不再运行时被CPU进行指令优化重排,保证了有序性。
- 不保证操作的原子性;
- 原子性:一个或一组操作,要么连续执行不会被打断,要么都不执行,满足这样特征的一个或一组操作就体现出了原子性;
volatile
不保证操作的原子性!
1.12 Java基本数据类型
- Java提供了8中基本数据类型
- 注意要点
- Java中不存在无符号的数;
- Java中的数据的范围是固定的,不会随着硬件和操作系统的改变而发生改变;
- Java中默认的浮点数类型是
double
,因此,如果需要使用float
关键字初始化浮点型变量时,需要使用类型转换符号,例如float f = 1.0f
或者float f =(float)1.0
null
null
不是一个合法的Object实例,编译器并没有为其分配内存;null
仅仅用于表明该引用目前目前没有指向任何对象;null
将会对引用变量的值全部置为0;- 如果定义
String x = null
,它表示定义了一个变量x,x中存放的是String
的引用,此处为null
1.13 不可变类
- 定义
不可变类(Immutable Class)在创建实例后,在整个程序的运行期间内,都不允许修改其成员变量的值,但是刻印读取,类似于常量; - 要点
- 所有的基本数据类型的封装类都是不可变类;
- 不可变类的值不可以被改变,但是可以修改其指向;
- 创建一个不可变类
- 所有成员变量均必须被private修饰;
- 类中不能定义能够休干成员变量的方法;
- 必须确保类中的所有方法均不能为子类所覆盖;
- 若类成员不是不可变量,那么在成员初始化或者使用get方法获取其成员变量时,需要使用clone方法来确保类的不可变性;
-
1.14 值传递和引用传递的区别
- 值传递:就是在方法调用的时候,实参是将自己的值拷贝一份赋给形参,在方法内,对该参数值的修改不影响原来的实参。
- 引用传递:是在方法调用的时候,实参将自己的地址传递给形参,此时方法内对该参数值的改变,就是对该实参的实际操作。
1.15 ++i与i++
- i++先赋值,再自增;
- ++i先自增,再赋值;
1.16 String字符串的创建与存储机制
String类型的要点
- String表示字符串类型,属于引用数据类型,不属于基本数据类型;
- Java中使用双引号括起来的都是String对象。例如:“abc”、“lei”、“hello world!”,这就是三个对象;
- Java中规定,双引号括起来的字符串是不可变的,也就是说“abc”从出生到消亡双引号里的内容是不会变的,不可能变成“abcd”,也不可能变成“ab”;
- 字符串都是直接存储在“方法区”的字符串常量池当中的;
- 为了提升开发效率,所以把字符串存储在“字符串常量池”当中。
String对象创建的两种方式
- 通过字符串常量赋值的方式创建
//方式一:通过常量的方式创建string对象
//以下两行代码表示底层创建了3个字符串对象,都在字符串常量池当中。
String s1 = "abcdef";
String s2 = "abcdef" + "xy";
s1 = "xyz"
此时字符串常量池中存在了4个字符串:abcdef、xy、abcdefxy、xyz(只要创建了,即使后面用不到,也会一直存在于常量池之中,直到程序结束为止)
- 通过new的方式创建
//通过new的方式创建String对象。
//分析:结合以上内容可以先分析s3的“xy”从哪来。
//提示:凡是“”括起来的都在字符串常量池中。
//注意:new对象的时候一定在堆内存中开辟空间。
String s3 = new String("xy");
new对象的时候一定在堆内存中开辟空间。
常见String面试题
- Q:下面代码创建了几个对象?
String s3 = new String("xy");
A:1个或2个;若字符串常量池中没有存储"xy",那么就是2个;若字符串常量池中存储有"xy",那么就是1个。
- Java中的substring方法是否会引起内存泄漏?
A:1.首先解释内存泄露:内存泄露通常指是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,而Java中,程序员无需手动释放堆内存,这些操作均由jvm提供的gc来完成,所以,在Java中的内存泄漏,指的是程序员认为一个对象会被gc收集,但是因为某种原因,gc无法回收该对象;
substring引发内存泄漏当且仅当在Java1.7之前的版本才会出现,Java1.7以及以后的版本重写了关于substring相关的代码,并不会出现内存泄漏现象
1.17 、equals()和hashCode()的区别
- 1. 可以比较两个基本类型的数据以及引用类型的数据是否相等; 2. 对于对象之间,运算符只能比较两个对象是否指向同一块存储空间; 3. 对于两个对象之间的内容(属性)的比较,运算符就无法实现;
- equals()
- equals()方法是Object类提供的方法之一;
- equals()可以比较对象之间的属性值(内容)是否相等;
- hashCode()
- hashCode()方法继承自Object类,它也用来鉴定两个对象是否相等;
- hashCode()方法用户一般不会调用;
- hashCode()方法相当于是对象的编码,返回值能是int类型;
1.18 String家族之间的区别与联系
- String
- String是不可变类,其创建的对象会被置入字符串常量池中;
- 所有String创建的对象均无法被修改,如需修改,需要在字符串常量池中创建新的字符串对象;
- 可以使用直接赋值与new关键字创建字符串对象;
- 性能最弱;
- StringBuffer
- StringBuffer是可变类,线程安全;
- StringBuffer在修改字符串变量时无需在字符串常量池中创建新的对象,步骤也比String少,也无需频繁gc,性能强于String;
- 只能使用new关键字创建新的字符串对象;
- 性能其次;
- StringBuilder
- StringBuilder是可变类,与StringBuffer类似;
- StringBuilder线程不安全,多线程时,建议使用StringBuffer;
- 性能最强。
1.19 Finally代码块何时执行
- return和finally的执行顺序
- finally语句在return语句执行之后、return返回之前执行的(return执行后、返回前);
- finally块中的return语句,会覆盖try块中的return返回;
- 如果finally语句中没有return语句覆盖返回值,那么原来的返回值可能因为finally里的修改而改变也可能不变;
- try块里的return语句在异常的情况下不会被执行,这样具体返回哪个看情况;
- 当发生异常后,catch中的return执行情况,与未发生异常时try中return的执行情况完全一样;
- finally不会执行的情况
- try语句没有被执行到,如在try语句之前就返回了,这样finally语句就不会执行,这也说明了finally语句被执行的必要而非充分条件是:相应的try语句一定被执行到(try和finally一荣俱荣,一损俱损);
- 在try块中有System.exit(0);这样的语句,System.exit(0);是终止Java虚拟机JVM的,连JVM都停止了,所有都结束了,当然finally语句也不会被执行。
1.20 Java异常处理
1.20.1 非检查性异常
异常 | 描述 |
---|---|
ArithmeticException | 当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。 |
ArrayIndexOutOfBoundsException | 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。 |
ArrayStoreException | 试图将错误类型的对象存储到一个对象数组时抛出的异常。 |
ClassCastException | 当试图将对象强制转换为不是实例的子类时,抛出该异常。 |
IllegalArgumentException | 抛出的异常表明向方法传递了一个不合法或不正确的参数。 |
IllegalMonitorStateException | 抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。 |
IllegalStateException | 在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。 |
IllegalThreadStateException | 线程没有处于请求操作所要求的适当状态时抛出的异常。 |
IndexOutOfBoundsException | 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。 |
NegativeArraySizeException | 如果应用程序试图创建大小为负的数组,则抛出该异常。 |
NullPointerException | 当应用程序试图在需要对象的地方使用 null 时,抛出该异常 |
NumberFormatException | 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。 |
SecurityException | 由安全管理器抛出的异常,指示存在安全侵犯。 |
StringIndexOutOfBoundsException | 此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。 |
UnsupportedOperationException | 当不支持请求的操作时,抛出该异常。 |
1.20.2 检查性异常
ClassNotFoundException | 应用程序试图加载类时,找不到相应的类,抛出该异常。 |
---|---|
CloneNotSupportedException | 当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。 |
IllegalAccessException | 拒绝访问一个类的时候,抛出该异常。 |
InstantiationException | 当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常。 |
InterruptedException | 一个线程被另一个线程中断,抛出该异常。 |
NoSuchFieldException | 请求的变量不存在 |
NoSuchMethodException | 请求的方法不存在 |
第二章:流
2.1 I/O流
2.1.1 Java I/O流的实现机制
- 流的分类(重点)
- 按照Java版本分类
- BIO(同步阻塞I/O模式);
- NIO(同步非阻塞I/O模式);
- AIO(异步非阻塞I/O模式);
- 按照数据流向分类
- 输入流;
- 输出流;
- 按照传输单位分类
- 字节流,继承自InputStream类与OutputStream类;
- 字符流,继承自Reader与Writer;
- 按照Java版本分类
- I/O流的层次架构图
2.1.2 管理文件和目录的类
Java提供了File类实现对文件和目录的操作,以下为File类的一些基本方法
方法 | 说明 |
---|---|
File(String PathName) | 根据指定路径创建一个File对象 |
createNewFile() | 如果目录或者文件存在,则返回False,否则创建文件或者文件夹 |
delete() | 删除文件或者文件夹 |
isFile() | 判断这个对象表示的是否是文件 |
isDirectory() | 判断这个对象表示的是否是文件夹 |
listFiles() | 如果对象代表目录,则返回目录中所有的文件File对象 |
mkdir() | 根据当前对象指定的路径创建目录 |
exists() | 判断对象对应的文件是否存在 |
2.1.3 Java Socket
- S0cket又被称之为套接字,其分为两种类型:
- 面向连接的TCP;
- 面向无连接的UDP;
- 任何一个Socket都是由端口+IP唯一确定的;
- 示意图
- Java Socket实现细节(单线程下简易Scoket)
- 服务端
package socket.socket1.socket;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args) {
try {
// 初始化服务端socket并且绑定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
//等待客户端的连接
Socket socket = serverSocket.accept();
//获取输入流,并且指定统一的编码格式
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
socket.getInputStream(), "UTF-8"));
//读取一行数据
String str;
//通过while循环不断读取信息,
while ((str = bufferedReader.readLine()) != null) {
//输出打印
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 客户端
package socket.socket1.socket;
import java.io.*;
import java.net.Socket;
public class ClientSocket {
public static void main(String[] args) {
try {
//初始化一个socket,ip+端口
Socket socket = new Socket("127.0.0.1", 9999);
//通过socket获取字符流
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(
socket.getOutputStream()));
//通过标准输入流获取字符流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
System.in, "UTF-8"));
while (true) {
/*等待接受键盘的输入*/
String str = bufferedReader.readLine();
/*将数据写入流*/
bufferedWriter.write(str);
bufferedWriter.write("\n");
/*刷新入流*/
bufferedWriter.flush();
/*关闭socket输出流*/
//socket.shutdownOutput();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.1.4 Java序列化
序列化是一种对象持久化的方式,Java提供了两种序列化方法,分别为序列化和外部序列化。
- 序列化
- 定义:序列化是一种将对象以一串字节描述的过程
- 特点
- 如果一个类能被序列化,那么它的子类也能够被序列化;
- static、transient等代表对象临时数据的成员不能够被序列化;
- Java提供了多个序列化接口,包括ObjectOutput、ObjectInput、ObjectOutputStream、ObjectInputStream
- 局限性
- 频繁使用序列化会影响程序运行时性能;
- 两种使用序列化的场景
- 通过网络来发送对象,或者对象的状态需要被持久化到数据库或者文件中;
- 序列化能够实现深拷贝,即可以拷贝引用的对象;
- serialVersionID
- 定义与用途:定义为代表类定义的版本,在反序列化时,jvm会将字节流状态的类中的serialVersionUID与本地类中的serialVersionUID进行比较,如果相同,则进行序列化,不相同就抛InvalidClassException异常;
- 自定义(显式声明)serialVersionID
- 减少序列化与反序列化的计算开销,提升程序性能;
- 提高程序在不同平台上(不同平台计算serialVersionID的方式不同会导致serialVersionID不同从而发生错误)的兼容性;
- 增强程序各个版本的健壮性(后期开发、维护代码时,会增加/减少新的属性,此时serialVersionID会发生改变从而导致序列化与反序列化出现错误)
- 外部序列化
- 序列化是内置的API,只需要实现Serializable接口,开发 人员不需要编写任何代码就可以实现对象的序列化;
- 使用外部序列化时,Extemalizable接口中的读写方法必须由开发人员来实现;
- ps:在用接口Serializab‖e实现序列化的时候,这个类中的所有属性都会被序列化。怎样才能实现只序列化部分属性呢?
- 方法一:实现Extemaljzable接口,根据实际需求来实现readExtαnal与wIiteExtemal方法,从而控制序列化与反序列化所使用的属性;
- 方法二:使用关键字transient来控制序列化的属性,使用该属性修饰的属性被编译器认为是临时属性,序列化时将会被忽略;
2.1.5 同步与异步、阻塞与非阻塞
2.1.5.1 不同语境下同步与异步、阻塞与非阻塞的概念
- 多线程语境下
- 同步:指代码的同步执行(SynchmnousInvoke),—个执行块同一时间只有一个线程可以访问;
- 异步:指代码的异步执行(AsynchronousInvoke),多个执行块可以同时被多个线程访问;
- 阻塞:线程阻塞状态(ThreadBlock),表示线程挂起;
- 非阻塞:线程不处于阻塞状态,表示线程没有挂起;
- I/O流语境下
- 同步:是指发起—个IO操作时,在没有得到结果之前,该操作不返回结果,只有调用结束后,才能获取返回值并继续执行后续的操作(一个操作对应一个返回结果,结果不返回,不执行任何行动);
- 异步:是指发起—个IO操作后,不会得到返回,结果由发起者自己轮询,或者IO操作的执行者发起回调(操作发起后不会主动返回结果,除非操作调用或者轮询时才会返回);
- 阻塞:是指发起者在发起IO操作后,不能再处理其他业务,只能等待IO操作结束;
- 非阻塞:是指发起者不会等待IO操作完成;
2.1.5.2 并发与并行
- 并发
- 微观上:同一时刻只能有一条指令执行;
- 宏观上:一段时间被分为很多时刻,每个时刻按顺序执行指令,宏观上认为这段时间内这些指令是同时进行的;
- 并行
- 同一时刻,多条指令在多个处理器上同时执行,无论围观或者宏观,二者都是同时执行的。
2.1.6 BIO(同步阻塞I/O模式)
BIO是最传统的同步阻塞IO模型,服务器端的实现是一个连接只有一个线程处理,线程在发
起请求后,会等待连接返回。
如果多个请求同时发送,则服务端需要同时开辟同样数量的线程来接受处理,而系统资源是有限的,允许创建的线程数一般来说要远远小于请求数。
2.1.7 NIO(同步非阻塞I/O模式)
NIO是在Javal.4中被纳入JDK中,提供了基于Selector的异步网络I/O使得一个线程可以管理多个连接;
NIO通过Selector(选择器)、Channels(管道)和Buffers(缓冲区)来实现非阻塞的IO。
- Channel(管道):一个双向的通道,不仅能读取数据,而且还能写入数据,只能通过Buffer来完成;
- Buffer(缓冲区):NIO中,数据只能被写入缓冲区,也只能从缓冲区中读取数据;
- Selector(选择器):轮询所有被注册的Channel,一旦发现Chamel上被注册的事件发生,就可以对这个事件进行处理。
2.1.8 AIO(异步非阻塞I/O模式)
AIO是对NIO的改进,是基于Proactor模型实现的。
第三章:容器
3.1 Collection框架
包含两个大部分:Collection和Map。
- Collection
- List:按照顺序保存插入的对象,没有特殊限制,相当于是一个长度可以动态变化的数组;
- Set:与List的定义保持一致,但是加入了一个特殊限制:Set中的元素不可以重复
- Queue和Stack:该两个定义与List保持一致,但是在元素插入和取出时,各自有各自特殊的规则:
- Queue服从先进先出原则;
- Stack服从先进后出原则。
- Map
- Map通过<键:值>的键值对方式保存数据;
- 其中,键不可以重复,值可以重复;
- 可以通过键来取值,也可以通过值来筛选符合条件的键。
3.2 ArrayList、Vector、LinkList的区别
- 联系:
- ArrayList、Vector和LinkedList都是实现了List接口的类;
- 区别:
- ArrayList和Vector使用动态数组作为内部数据结构,而LinkedList使用双向链表作为内部数据结构;
- ArrayList和Vector在添加或删除元素时需要调整数组大小,可能影响性能,而LinkedList在添加或删除元素时只需要修改指针,性能更好。
- ArrayList和Vector在随机访问元素时性能更好,因为可以直接通过索引访问数组元素,而LinkedList在随机访问元素时性能较差,因为需要遍历链表。
- Vector是线程安全的,因为它的所有方法都是同步的,而ArrayList和LinkedList是非线程安全的。
- Vector是一个可增长的对象数组,它的所有方法都是同步的。这意味着在多线程环境下,任何访问或修改Vector内容的方法都是线程安全的。但是,这也会影响性能和内存消耗,因为每次调用方法时都需要获取锁和释放锁。
3.3 各种Map
主要常用的有三种Map:HashMap TreeMap LinkedHashMap
- HashMap
- HashMap是基于哈希表实现的,它可以实现O(1)的查找和插入,但遍历时键的顺序是随机的;
- HashMap最多只允许一个键为null,多个值为null;
- HashMap不支持线程同步,如果需要同步,可以使用Collections.synchronizedMap或ConcurrentHashMap;
- LinkedHashMap
- LinkedHashMap是基于双向链表和哈希表实现的,它保留了插入或访问的顺序。
- LinkedHashMap在其他方面与HashMap相似。
- TreeMap
- TreeMap是基于红黑树实现的,它可以实现O(log n)的查找和插入,而且遍历时键的顺序是有序的。
- TreeMap不允许键为null,但允许值为null。
- TreeMap还实现了NavigableMap和SortedMap接口,提供了更多排序和导航相关的方法。
- WeakHashMap
- WeakHashMap也是一种哈希表实现类,但与其他Map不同之处在于其键为弱引用类型。当某个键不再被引用时会被自动从WeakHashMap中删除以释放内存空间。
3.4 HashMap TreeMap HashTable WeakHashMap的区别与联系
- 实现方式不同
- HashMap基于哈希表实现;
- TreeMap基于红黑树实现;
- HashTable也是基于哈希表实现的,但已经过时了,在多线程环境下需要使用ConcurrentHashMap代替;
- WeakHashMap也是一种哈希表实现类,但其键为弱引用类型;
- 排序方式不同
- HashMap没有排序功能;
- TreeMap按照自然排序或指定比较器进行排序,并保证每个节点都满足左子树小于右子树;
- LinkedHashMap可以按照插入顺序或访问顺序输出元素。
- 线程安全性不同
- HashMap非线程安全,多线程环境下需要使用ConcurrentHashMap代替;
- ConcurrentHashMap采用分段锁机制提高并发性能,在多线程环境下效率更高;
- HashTable与HashMap类似,查询速度慢,线程安全。
- 键值类型限制不同
- HashMap最多只允许一个键为null,多个值为null;
- LinkedHashMap在其他方面与HashMap相似;
- TreeMap不允许键为null,但允许值为null。
- HashTable不允许空键和空值。
3.5 使用自定义类型作为HashMap或者HashTable的key时需要注意哪些问题
- 重写hashCode()方法
当我们使用自定义类型作为HashMap或者HashTable的key时,需要确保该类已经正确地实现了hashCode()方法。因为哈希表是根据对象的hashCode值来确定其在数组中存储位置的,如果没有正确实现hashCode()方法,则可能会导致元素无法被正确插入到哈希表中。 - 重写equals()方法
除了要重写hashCode()方法之外,还需要确保该类已经正确地实现了equals()方法。这样才能够比较两个对象是否相等,在查找元素时才能够正常工作。 - 不可变性
由于哈希表是根据对象的hashcode值来进行查找和定位操作的,所以如果一个键对应的hashcode值发生改变,则它将无法再被正常查找到。因此,在设计自定义类型作为HashMap或者HashTable的key时,应该尽量使其成员变量不可变(即声明为final),避免出现意外情况。
3.6 ConcurrentHashMap
Java中的ConcurrentHashMap是一个线程安全的哈希表,它提供了高效的并发访问和修改。与普通的HashMap不同,ConcurrentHashMap允许多个线程同时读取和写入数据,而且在进行操作时不需要加锁。它的特点如下:
- 线程安全,支持高并发。
- 采用了锁分段技术,把整个map划分为多个segment,每个segment相当于一个小的hashmap。
- 在JDK1.8中,抛弃了segment分段锁,而采用了CAS+synchronized来保证并发安全性。
- 可以用作可扩展的频率映射或多重集合。
3.7 各种Set
简略版:
- HashSet:使用哈希表结构,利用元素的hashCode和equals方法来判断重复和存储位置。
- TreeSet:使用红黑树结构,对元素进行排序,可以实现自然排序或者定制排序。
- LinkedHashSet:使用链表和哈希表结构,保留了插入顺序。
- EnumSet:使用位向量结构,专门用于枚举类型的集合。
- CopyOnWriteArraySet:使用数组结构,线程安全,适合读多写少的场景。
详细版:
- HashSet
HashSet是最常见、最基本的Set实现类之一。它使用哈希表来存储元素,并且可以快速地进行插入、删除和查找操作。但由于哈希表需要占用较大内存空间,因此当数据量很大时可能会导致性能下降。 - TreeSet
TreeSet是一个基于红黑树(自平衡二叉搜索树)实现的有序集合。它可以保证元素按照自然顺序或者指定比较器排序后排列,并且支持高效地范围查询操作(如获取某个区间内所有元素)。但由于维护红黑树需要消耗额外时间和空间开销,在插入、删除等操作上相对HashSet略慢。 - LinkedHashSet
LinkedHashSet是一个具有可预测迭代顺序的哈希表实现。与普通HashSet不同,LinkedHashSet还维护了一个双向链表来记录插入顺序或访问顺序(LRU缓存淘汰策略就可以利用这个特性)。虽然在添加/删除单个元素时稍微慢一些,但遍历整个集合时则比普通HashMap更快。 - EnumSet
EnumSet专门针对枚举类型设计而成,底层采用位向量(bit vector)表示其中包含哪些枚举值。因为位运算非常高效,在处理枚举值方面EnumSet要比其他任何集合类型都快得多,并且只需占据非常小的内存空间。 - CopyOnWriteArraySet
CopyOnWriteArraySet是线程安全版本的set容器, 内部通过Copy-On-Write技术来保证并发读写安全性, 也就意味着读取数据无锁化, 而写入数据则会加锁.
3.8 各种BlockingQueue
BlockingQueue是Java中的一个并发容器,它是Queue接口的一个子接口,表示一个支持阻塞操作的队列。它有以下几种特点:
- 线程安全,基于ReentrantLock实现。
- 当队列为空时,取数据的线程会阻塞等待直到队列非空。
- 当队列满时,插入数据的线程会阻塞等待直到队列非满。
- 适合用于生产者/消费者模式,无需额外的同步和唤醒机制。
Java标准库中提供了多种实现BlockingQueue接口的类,每个类都有其特定的优缺点和适用场景。
简略版:
- ArrayBlockingQueue:使用数组结构,按FIFO顺序存储元素,需要指定容量大小。
- LinkedBlockingQueue:使用链表结构,按FIFO顺序存储元素,可以指定容量大小或默认为Integer.MAX_VALUE。
- PriorityBlockingQueue:使用堆结构,按元素优先级存储元素,无界队列。
- DelayQueue:使用堆结构,按元素延迟时间存储元素,无界队列。
- SynchronousQueue:不存储元素,每个插入操作必须等待一个相应的删除操作才能完成。
详细版:
- ArrayBlockingQueue
ArrayBlockingQueue是一个基于数组实现的有界阻塞队列。它按照FIFO(先进先出)原则对元素进行排序,并且支持公平锁和非公平锁两种模式。由于底层数组固定大小,在创建时需要指定容量大小,并且不能动态扩展。 - LinkedBlockingQueue
LinkedBlockingQueue是一个基于链表实现的无界阻塞队列。它同样按照FIFO原则对元素进行排序,并且支持公平锁和非公平锁两种模式。由于没有固定容量限制,因此可以不断添加新元素而不会抛出异常。 - PriorityBlockingQueue
PriorityBlockingQueue是一个具有优先级顺序功能的无界阻塞队列。它使用堆数据结构来维护元素之间的优先级关系,默认情况下采用自然顺序升序排列,也可以通过传入比较器对象来改变排序方式。 - SynchronousQueue
SynchronousQueue是一种特殊类型的Transfer Queue(即直接交换),其中每个插入操作必须等待另一个线程执行相应删除操作(反之亦然)。这里没有任何内部存储空间可用于保留插入项;因此只能在消费者准备好处理要消耗掉该项时立即将其移交给消费者线程。 - DelayedWorkDequeuue
DelayedWorkDequeuue 是 JDK 1.5 引入 Delayed 接口后新增加到 Java 并发包中 Blocking 队列, 它典型地应用场景为任务调度系统, 具体情况如下:- 每个任务都需要设置延迟时间.
- 系统根据任务设定延迟时间从小到大依次执行.
- 周期性地执行某些重复性质计算.
3.9 Java中Collection和Collections的区别是什么?
Java中的Collection和Collections是两个不同的概念。
- Collection
Collection是一个接口,它代表一组对象(元素)的集合。它提供了基本操作方法,如添加、删除、遍历等,并且有多种实现类可以选择使用,例如List、Set和Queue等。Collection接口继承自Iterable接口,因此所有实现了Collection接口的类都可以使用for-each循环进行遍历。 - Collections
Collections是一个工具类,它包含了许多静态方法来对集合进行操作。这些方法包括排序、查找最大/最小值、反转顺序等常见操作,并且还提供了一些特殊用途的算法和数据结构支持。Collections中定义的所有方法都是静态方法,并且通常以传入一个集合对象作为参数来执行相应操作。
总之,在Java中:- Collection表示一组元素或者说容器(数据类型的实体+基本操作);
- Collections则是针对容器类型编写出来方便开发人员处理各种容器类型数据而设计出来的工具类库(更加拓展化的高级操作)。
需要注意区分二者概念上面存在差异。
3.10 迭代器
Java中的迭代器是一种用于遍历集合类(如List、Set、Map)元素的接口。它提供了一种统一的访问方式,使得我们可以在不知道具体集合实现细节的情况下对其进行遍历操作。
Java中常见的迭代器有两种:Iterator和ListIterator:
- Iterator适用于所有实现了Iterable接口(包括List、Set等)的集合类;
- ListIterator则仅适用于实现了List接口(如ArrayList、LinkedList等)的列表类。
使用迭代器需要先通过调用集合对象上相应方法获取到一个迭代器对象,然后就可以使用该对象上提供的方法依次访问集合中每个元素。迭代器有如下的特点:
- 迭代器可以在遍历过程中删除集合中的元素。
- 迭代器的方法名更加清晰,如hasNext()和next()。
- 迭代器不保证遍历的顺序。
3.11 并行数组
并行数组是一种在Java 8中引入的新特性,它可以利用多线程的概念来提高数组排序的效率。
并行数组的方法是parallelSort(),它可以对所有基本数据类型和可比较对象的数组进行排序。
第四章:多线程
4.1 进程与线程
进程和线程是操作系统中的两个重要概念,Java作为一种高级编程语言,在多线程方面提供了很好的支持。在Java中,进程和线程也有着自己独特的含义。
- 进程
进程是指正在运行中的程序实例。每个进程都有自己独立的内存空间、代码段、数据段以及堆栈等资源。不同进程之间相互隔离,彼此之间不能直接访问对方所拥有的资源。
在Java中启动一个新进程可以使用Runtime类或ProcessBuilder类来完成。 - 线程
线程是指执行程序时单独运行的代码片段,它可以看做是轻量级的“子任务”。一个应用程序通常包含多个线程,并且这些线性共享同一个内存空间和其他资源。
在Java中创建新线性可以通过继承Thread类或实现Runnable接口来完成。 - 区别与联系
(1)区别:- 进程属于操作系统范畴,而线性则属于应用程序范畴。
- 每个进城都拥有自己独立的地址空间和系统资源,而所有县城共享同一份地址空间。
- 进城之前需要先分配足够大的内存空间并加载相关文件到该内存区域里;而县城则只需占用少量内存即可。
- 由于各县城共享同一份地址空间,在处理数据时需要考虑到并发问题;而不同进城之前不存在这样问
(2)联系:
- 在操作系统层面上,每个县城其实就是某个特定类型下面某个特定状态下面某条执行路径;
- 应用程序开发者无法直接控制CPU时间片分配等底层机制;
- 应用程序开发者只能通过API调度器来管理各县城之前交替执行,并保证正确地处理并发问题;
4.2 同步和异步
Java中的同步和异步是指程序执行过程中,不同任务之间的协调方式。具体来说,同步是指多个任务按照一定顺序依次执行,而异步则是指多个任务可以同时进行。
- 同步
在Java中,同步通常使用synchronized关键字或Lock接口来实现。当一个线程获取到锁时,其他线程就必须等待该线程释放锁后才能继续执行。这样可以保证共享资源被正确地访问,并避免出现数据竞争、死锁等问题。 - 异步
在Java中,异步通常使用回调函数、Future/Promise模式或CompletableFuture类来实现。当一个任务需要耗费大量时间时,在传统的同步方式下会阻塞整个程序运行;而采用异步方式,则可以将该任务交给另外一个线程去处理,并立即返回结果或注册回调函数以便在处理完成后得到通知。 - 区别与联系
- 同步和异步都是为了解决多个任务之间的协作问题;
- 同步强制要求各个任务按照特定顺序依次执行,并且只有前面的任务完成后才能开始下一个;而异步则允许各个任务并发地执行;
- 在某些情况下,采用同/异 步方式都可以达到相应效果;但对于那些需要长时间计算或IO操作的场景,则必须采用异/同 步方式以避免程序阻塞;
- Java提供了很多工具类和框架支持开发者灵活选择适合自己业务需求的协作模式。
4.3 如何实现Java多线程?
Java多线程的实现有以下几种方式:
- 继承Thread类,重写run方法,然后创建Thread对象并调用start方法来启动线程。
- 实现Runnable接口,重写run方法,然后创建Thread对象并传入Runnable实现类的对象作为参数,再调用start方法来启动线程。
- 实现Callable接口,重写call方法,然后创建FutureTask对象并传入Callable实现类的对象作为参数,再创建Thread对象并传入FutureTask对象作为参数,最后调用start方法来启动线程。
- 使用ExecutorService接口和Executors工具类来创建线程池,并通过submit或execute方法来提交Runnable或Callable任务到线程池中执行。
或者使用如下回答:
Java多线程可以通过以下几种方式实现:
- 继承Thread类,重写run()方法,并调用start()方法启动线程。
- 实现Runnable接口,重写run()方法,并将其作为参数传递给Thread对象的构造函数中创建新线程。
- 实现Callable接口并使用FutureTask来包装Callable对象。然后将FutureTask对象传递给Thread对象的构造函数中创建新线程。
- 使用Executor框架提供的ThreadPoolExecutor等工具类来管理和执行多个任务。
无论哪种方式,都需要注意同步问题以及避免死锁等常见问题。
4.4 run方法和start方法有何异同?
在Java多线程中,run()方法和start()方法是两个不同的方法,但它们之间有联系。
- 区别:
- run()方法是线程执行体,定义了线程要执行的任务内容。
- start()方法是启动一个新线程,并让该线程开始执行run()方法中定义的任务。
- 联系:
- 在继承Thread类创建新线程时,需要重写run()方法来定义该线程要执行的任务。
- 调用start()方法后会自动调用run() 方法并启动新的子进程。如果直接调用run(),则只会在当前主进程中串行地运行代码,并没有真正意义上开启一个新的子进程序列。
因此,在使用Java多线程时应该注意:必须通过start() 方法来启动一个新的子进程序列;而不是直接调用 run () 方法作为普通函数去使用。
4.5 多线程同步
Java中可以通过以下几种方式实现多线程同步:
- synchronized关键字:使用synchronized关键字来保证同一时刻只有一个线程访问共享资源,其他线程需要等待当前线程释放锁才能继续执行。synchronized可以用在方法上或代码块中。
- Lock接口:Lock接口提供了比synchronized更灵活的锁机制,它允许多个条件变量和公平性选择,并且支持可重入、超时获取锁和非阻塞式获取锁等特性。
- volatile关键字:volatile关键字可以确保变量的可见性和禁止指令重排,但不能保证原子性。因此,在对变量进行读写操作时需要加上synchronized或者Lock来保证原子性。
- wait()、notify()、notifyAll()方法:这三个方法是Object类中定义的用于线程间通信的方法。wait()会使当前线程进入等待状态并释放对象监视器(即锁),直到其他线程调用notify()/notifyAll()唤醒该线程;而notify()/notifyAll()则会随机唤醒一个或所有正在等待该对象监视器的线程。
无论采用哪种方式,都需要注意避免死锁问题以及尽可能减少同步范围以提高程序效率。
4.6 各种Lock
Java中的Lock是一种用于多线程同步的机制,它可以控制对共享资源的访问。与传统的synchronized关键字相比,Lock提供了更加灵活和可扩展的锁定方式。
Java中常见的Lock包括以下几种:
- ReentrantLock
ReentrantLock是一个可重入锁,它允许线程在持有锁时再次获取该锁而不会造成死锁。ReentrantLock还支持公平性策略,在等待队列中按照先进先出(FIFO)顺序分配锁。 - ReadWriteLock
ReadWriteLock是一个读写锁,它充分利用了共享数据通常被读取而不是修改这个特点。ReadWriteLock维护了两个相关联的锁:读取锁和写入锁。多个线程可以同时获得读取器上的读取操作权限,但只能有一个线程获得写入器上的写入操作权限。 - StampedLock
Stampedlock也是一种读写锁,并且比ReadWriteLock更加高效。Stampedlock使用乐观并发策略来实现非阻塞式访问,并且支持三种模式:悲观、乐观和尝试性悲观。 - Condition
Condition接口提供了类似于Object.wait()和Object.notify()方法功能的方法await()和signal()以及signalAll()方法。Condition对象必须由某个具体类型(如ReentrantLcok)创建,并且只能通过其关联到该类型对象进行使用。
总之,在Java多线程编程中需要考虑到各种情况下对共享资源进行安全地访问控制,选择合适类型并正确地使用各类 Lock 是至关重要 的 。
4.7 synchronized与Lock存在哪些异同?
synchronized和Lock都是Java中用于实现多线程同步的机制,它们之间有以下异同:
- 实现方式
synchronized是Java语言内置的关键字,通过在方法或代码块前加上synchronized来实现锁定。而Lock则是一个接口,在使用时需要先创建一个具体的实例对象。 - 可重入性
synchronized可以自动进行嵌套调用,即可重入锁。而ReentrantLock也支持可重入性。 - 锁类型
synchronized只支持一种锁类型:独占锁(排他锁)。而Lock提供了两种常见的锁类型:独占锁和共享锁。 - 粒度控制
synchronized粒度较大,只能对整个方法或代码块进行加锁。而Lock可以根据需求灵活地控制粒度大小,并且提供了更细粒度、更灵活的线程交互方式。 - 性能表现
在低并发情况下,两者性能差别不大;但在高并发情况下,由于JVM会为每个被 synchronized 修饰过的方法或代码块分配一个监视器(monitor),因此当竞争激烈时会导致频繁地上下文切换和阻塞等问题。相比之下,基于 Lock 的机制则可以更好地应对高并发场景,并且通常具有更好的扩展性和吞吐量表现。
总之,在选择使用 synchronized 还是 Lock 时需要考虑到具体业务场景及其特点、系统负载水平以及所需保证安全与效率等方面因素,并做出合理权衡取舍。
4.8 sleep与wait的区别是什么?
Java中的sleep()和wait()都是用于线程间同步的方法,它们之间有以下区别:
- 调用方式
sleep()是Thread类中静态方法,可以直接调用;而wait()则是Object类中非静态方法,只能在已经获得对象锁的情况下才能调用。 - 锁释放
当一个线程执行sleep()时,并不会释放其持有的任何锁。而当一个线程执行wait()时,则会立即释放其持有的对象锁,并进入等待状态。 - 唤醒方式
通过调用notify()/notifyAll()方法可以唤醒正在等待某个对象上的所有线程。而对于使用 sleep 方法休眠的线程来说,如果没有其他线程将它唤醒或者到达了指定时间后仍未被唤醒,则该线程会自动苏醒并继续执行。 - 使用场景
通常情况下,我们使用 sleep 方法来暂停当前正在运行的程序一段时间(例如模拟网络延迟、CPU占用率过高等),以便让其他任务有机会运行。而 wait 方法则通常与 notify/notifyAll 配合使用,在多个并发执行任务之间进行同步控制。
总之,在实际编码过程中需要根据具体业务需求选择适合场景和目标效果最佳 的 线程同步方案。
简略版:
- sleep是Thread类中的方法,wait是Object类中的方法。
- sleep方法不会释放锁,wait方法会释放锁,并加入到等待队列中。
- sleep方法不依赖于同步器synchronized,wait方法需要在同步代码块或同步方法中使用。
- sleep方法不需要被唤醒,会在指定时间后自动恢复,wait方法需要被notify或notifyAll唤醒。
- sleep方法可以在任何地方使用,wait方法只能在同步代码块或同步方法中使用。
4.9 终止线程的方法
Java中终止线程的方法有以下几种:
- 使用标志位:在run()方法中使用一个boolean类型的标志位来控制线程是否运行。当需要停止线程时,将该标志位置为false即可。
- 调用interrupt()方法:调用线程对象的interrupt()方法可以使得处于阻塞状态下的线程抛出InterruptedException异常,从而退出阻塞状态。
- 调用stop()方法(已过时):调用stop()方法会直接终止正在运行的线程,但这种方式容易导致数据不一致和死锁等问题,因此已经被废弃。
- 使用ThreadGroup来批量管理多个线程,并通过ThreadGroup.interrupt()或者ThreadGroup.stop()来统一终止所有子线程。
这些方式之间主要区别在于实现机制和适用场景。使用标志位是比较常见且安全可靠的做法;interrupt()适合处理I/O操作等可能会发生阻塞情况下需要及时响应中断信号的场景;stop()虽然简单但存在很大风险性,在实际开发中应避免使用;ThreadGroup则可以方便地管理多个相关联的子线程并进行统一控制。
4.10 死锁
Java中的死锁是指两个或多个线程在互相等待对方释放资源而无法继续执行的状态。具体来说,当一个线程持有某个对象的锁并试图获取另一个对象的锁时,如果另一个线程已经持有了该对象的锁并且正在尝试获取第一个对象的锁,则会发生死锁。
解决死锁问题可以采取以下几种方法:
- 预防性措施:避免出现多个线程竞争同一组资源,并确保所有线程按照相同顺序请求共享资源。
- 超时机制:设置超时时间,在规定时间内未能成功获取到所需资源则自动退出。
- 死锁检测与恢复:通过算法识别潜在死锁情况,并进行恰当处理以解除死循环。
- 防止嵌套加锁:尽量避免使用嵌套加锁方式,如synchronized块中再次调用synchronized方法等。
总之,在编写程序时应注意避免出现死循环、重复加/解同一把琐、不合理地长期占用共享资源等情况导致产生死锁。
4.11 守护线程
在Java中,守护线程(Daemon Thread)是一种特殊类型的线程,它被用来为其他非守护线程提供服务。当所有非守护线程结束时,JVM会自动关闭所有正在运行的守护线程并退出程序。
与普通线程不同的是,当一个Java应用程序只剩下守护进程时,JVM就会自动退出。因此,在创建和使用守护进程时需要注意以下几点:
- 它们不能访问任何用户界面或I/O流。
- 它们必须在启动前设置为“daemon”模式。
- 它们不能持有任何锁或资源,并且应该尽可能地简单。
要将一个线程设置为守护进程模式,请调用Thread类的setDaemon()方法,并将其参数设置为true。
4.12 join方法有什么作用?
- join方法是Thread类中的一个方法,它可以让一个线程等待另一个线程完成其执行。
- 例如,如果在A线程中调用了B线程的join方法,那么A线程会被阻塞,直到B线程执行完毕或者达到指定的等待时间,才会继续执行。
- join方法的原理是基于wait和notifyAll方法实现的。
- join方法可以实现线程之间的同步和顺序执行。
4.13 如何捕获一个线程抛出的异常?
在Java中,捕获一个线程抛出的异常有几种方式。
- 一种是在线程的run方法中,使用try-catch-finally代码块来处理受检异常和运行时异常。
- 另一种是使用Thread类提供的setUncaughtExceptionHandler方法,来设置一个线程的异常处理器,当线程发生未捕获的异常时,由JVM回调执行该处理器。
- 还有一种是使用Thread类提供的setDefaultUncaughtExceptionHandler方法,来设置一个全局的默认异常处理器,当线程没有设置自己的异常处理器时,就会使用该默认处理器。
4.14 线程池
线程池是一种管理线程资源的对象池,它可以避免频繁地创建和销毁线程,提高任务执行的效率和响应速度,降低资源的消耗和线程切换的开销,以及提高线程的可管理性。
Java中的核心线程池类是ThreadPoolExecutor,它有一个构造方法和五个参数来创建一个线程池。2这五个参数分别是:
- corePoolSize:核心线程数,即线程池中常驻的最小线程数。
- maximumPoolSize:最大线程数,即线程池中能容纳的最大线程数。
- keepAliveTime:空闲线程存活时间,即当线程池中的线程数量超过corePoolSize时,多余的空闲线程在被回收之前能存活的时间。
- unit:空闲线程存活时间的单位,如秒、毫秒等。
- workQueue:任务队列,用于存放等待执行的任务。
4.15 Executor接口
Java中的Executor接口是一个用于执行异步任务的框架。它提供了一种将任务提交给线程池进行处理的方式,从而使得开发人员可以更加方便地管理和控制多线程程序。
Executor接口定义了一个execute()方法,该方法接受一个Runnable对象作为参数,并将其提交到线程池中进行处理。除此之外,Executor还提供了一些其他有用的方法,例如shutdown()、awaitTermination()等等。
在Java中,常见的实现Executor接口的类包括ThreadPoolExecutor、ScheduledThreadPoolExecutor等。这些类都提供了不同类型和大小的线程池来满足不同场景下对并发性能和资源利用率要求不同的需求。
总之,通过使用Java中的Executor接口及其相关实现类,在编写多线程程序时可以更加高效地管理和控制异步任务,并且避免出现由于过度创建或销毁线程导致系统负载过重或者资源浪费等问题。
4.16 ExecutorService
Java中的ExecutorService是一个线程池框架,它提供了一种管理和执行多个异步任务的方式。通过使用ExecutorService,可以将任务提交到线程池中,并由线程池自动分配和调度线程来执行这些任务。
ExecutorService接口继承自Executor接口,它定义了更加丰富的方法来管理和控制线程池。其中包括:
- submit(Runnable task):向线程池提交一个Runnable类型的任务,并返回一个Future对象,用于获取该任务执行结果或取消该任务。
- submit(Callable task):向线程池提交一个Callable类型的任务,并返回一个Future对象,用于获取该任务执行结果或取消该任务。
- shutdown():关闭当前正在运行的所有线程并停止接受新的请求。
- shutdownNow():立即关闭所有正在运行的线程并停止接受新请求,并尝试中断所有未完成但已经开始执行的操作。
- awaitTermination(long timeout, TimeUnit unit):等待指定时间以使先前提交给executor service 的task完成其工作后终止executor service 。
- execute(Runnable command) :在将来某个时间点上执行给定命令
- invokeAll(Collection<? extends Callable> tasks): 执行给定集合中所有callable实例表示为future ,当全部成功时返回future列表
8.invokeAny(Collection<? extends Callable> tasks): 执行给定集合中任意callable实例表示为future ,如果有至少一项成功,则返回此项结果
4.17 ThredPoolExecutor
ThreadPoolExecutor是Java中的一个线程池实现类,它可以管理和复用多个线程来执行任务。使用线程池能够提高程序的性能和可伸缩性。
ThreadPoolExecutor有以下几个主要参数:
- corePoolSize:核心线程数,即在池中保持活动状态的最小线程数。
- maximumPoolSize:最大线程数,即允许创建的最大线程数。
- keepAliveTime:非核心空闲线程存活时间,在达到corePoolSize后,如果新任务继续提交,则会创建新的非核心空闲线程来处理请求。这些非核心空闲线程在超过keepAliveTime时间后将被回收。
- workQueue:工作队列,用于保存等待执行的任务。常见类型包括SynchronousQueue、LinkedBlockingQueue、ArrayBlockingQueue等。
- threadFactory:用于创建新的Thread对象,默认为Executors.defaultThreadFactory()方法返回值。
除了以上参数外,还有一些其他设置可以对ThreadPoolExecutor进行更精细化地配置。
使用ThreadPoolExecutor时需要注意以下几点:
- 线程池大小应该根据系统负载情况进行调整;
- 如果工作队列为空,并且所有核心和非核心空闲时间都已经超过keepAliveTime,则额外的资源将被释放;
- ThreadPoolExecutor不适合短期异步操作或I/O密集型操作;
总之,在Java中使用ThreadPoolExecutor可以有效地管理多个并发任务,并提高程序效率和可扩展性。
4.18 常见的4种线程池应用
Java中,Executor提供了四种常见的线程池应用,它们分别是:
- newCachedThreadPool:创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool:创建一个固定大小的线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool:创建一个定时的线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序 (FIFO, LIFO, 优先级)执行。
4.19 线程池的使用方法
- Java中的ThreadLocal是一个类,它提供了一种线程局部变量的机制。
- 线程局部变量是一种特殊的变量,它为每个访问它的线程都创建了一个独立的副本。2这样,每个线程都可以在自己的副本上进行操作,而不会影响其他线程的副本。
- ThreadLocal类提供了get和set方法,用来获取和设置当前线程的副本值。
- ThreadLocal类还提供了一个initialValue方法,用来初始化每个线程的副本值。
- ThreadLocal类通常用来实现线程安全,避免多线程之间共享变量导致的数据不一致问题。
但是,ThreadLocal类也有一些注意事项,比如:
- ThreadLocal类不解决多个线程之间需要协作的问题,只适用于每个线程独立处理自己的数据的场景。
- ThreadLocal类可能会导致内存泄漏,因为它持有对每个线程副本值的强引用。如果这些副本值是可回收对象,而且没有及时清理,就会占用内存空间。
- ThreadLocal类和线程池一起使用时要特别小心,因为线程池中的线程可能被复用,而且不一定会及时清理ThreadLocal中的副本值。这可能会导致数据混乱或内存泄漏等问题。
4.20 Latch
在Java中,Latch是一种同步工具,它可以使一个或多个线程等待其他线程完成操作后再继续执行。Latch通常用于协调多个线程的执行顺序。
Java中有两种类型的Latch:CountDownLatch和CyclicBarrier。
- CountDownLatch: 它允许一个或多个线程等待其他线程完成某些操作后再继续执行。当计数器为0时,所有等待的线程都会被释放。
- CyclicBarrier: 它允许一组线程互相等待,直到达到某个公共屏障点(barrier point)。当所有参与者都到达这个屏障点时,才能同时开始下一阶段任务。
4.21 Barrier
在Java中,Barrier是一种同步工具,它可以让多个线程等待彼此达到某个状态后再继续执行。当一个线程到达Barrier时,它会被阻塞直到所有其他线程也都到达了该Barrier。
Java中的Barrier有两种类型:CyclicBarrier和CountDownLatch。
- CyclicBarrier
CyclicBarrier允许一组线程互相等待,直到所有的参与者都已经满足条件之后才能继续执行。每次调用await()方法时,当前线程将被阻塞,并且计数器将减少1。当计数器为0时,则表示所有参与者已经就位,并且可以开始执行下一步操作。 - CountDownLatch
CountDownLatch是另外一种实现同样目标的类似于 Barrier 的 Java 类。但是不像 CyclicBarriers ,CountDownLatches 不能重置或重新使用;他们只能由 await 方法触发的倒计数值降至零以释放其等待方面所需的所有线程。
总体来说,在并发编程中使用 Barrier 可以使得程序更加灵活、高效地处理多个任务间协作问题。
4.22 Fork/Join框架
Java中的Fork/Join框架是一种用于并行处理任务的机制,它可以将一个大型任务拆分成多个小任务,并在多个处理器上同时执行这些小任务。
该框架最初是在JDK 7中引入的。 Fork/Join框架主要由两部分组成:fork()和join()方法。
其中,fork()方法用于将一个大型任务拆分成若干个子任务,并提交给线程池进行并发执行;
而join()方法则用于等待所有子任务完成,并将结果合并返回。
使用Fork/Join框架需要继承RecursiveTask或RecursiveAction类,前者表示有返回值的子任务,后者表示没有返回值的子任务。然后重写compute()方法,在该方法内部实现具体的计算逻辑。
4.23 CAS
Java中的CAS(Compare and Swap)是一种乐观锁机制,用于实现多线程环境下的同步操作。
它通过比较内存中某个位置的值与预期值是否相等来决定是否更新该位置的值,从而避免了传统锁机制所带来的性能损失和死锁问题。
在Java中,CAS通常使用Atomic类或Unsafe类提供支持。
其中Atomic类提供了一组原子操作方法,如getAndAdd()、compareAndSet()等;
Unsafe类则提供了更底层、更灵活但也更危险的API。
Java本身没有直接实现CAS,而是通过JNI调用C++内联汇编来实现。
4.24 线程调度优先级
Java中的线程调度优先级是由操作系统决定的,不同操作系统可能有不同的实现方式。一般来说,Java中线程调度优先级分为1~10共10个等级,数值越大表示优先级越高。但是并不能保证高优先级的线程一定会比低优先级的线程更快地执行完毕。
在Java中可以通过Thread类提供的setPriority()和getPriority()方法设置和获取线程调度优先级,默认情况下所有新创建的线程都具有普通(NORM_PRIORITY)优先级。
需要注意的是,在使用多线程时应该避免过于依赖于线程调度器对各个线程之间进行公平、合理地分配CPU时间片。因为这种行为很容易导致程序出现死锁、饥饿等问题,并且也无法保证在不同操作系统上表现相同。
在Java中,线程一般有以下五个状态:
- 新建(New):当创建一个Thread对象时,该线程处于新建状态。此时它还没有开始执行。
- 运行(Runnable):当调用start()方法启动线程后,该线程进入就绪队列等待获取CPU时间片并执行。注意,在这个状态下的线程可能会被操作系统随机切换出去而暂停运行。
- 阻塞(Blocked):当一个正在运行的线程因为某些原因无法继续执行时,如等待I/O、请求锁资源等,则进入阻塞状态。在这种情况下,该线程不会消耗CPU时间,并且也不会参与竞争CPU资源。
- 等待(Waiting):当一个正在运行的线程调用了wait()、join()或LockSupport.park()方法后,则进入等待状态。在这种情况下,该线程也不会消耗CPU时间,并且需要其他事件触发才能够重新回到就绪队列中。
- 终止(Terminated):当run()方法正常退出或者抛出异常导致程序异常结束时,则表示该线程已经完成任务并处于终止状态。
第五章:内存分配
5.1 JVM内存划分
JVM内存划分主要包括以下几个部分:
- 程序计数器(Program Counter Register)
程序计数器是一块较小的内存区域,它可以看作是当前线程所执行的字节码行号指示器。每个线程都有一个独立的程序计数器,用于记录下一条需要执行的指令地址。当线程被挂起或者等待I/O时,该值会保持不变。 - Java虚拟机栈(Java Virtual Machine Stacks)
Java虚拟机栈也叫做Java方法栈,用于保存方法调用过程中产生的局部变量、操作数栈、方法出口等信息。每个线程都有自己独立的虚拟机栈空间,并且在创建新线程时会为其分配一个固定大小的初始空间。 - 本地方法栈(Native Method Stack)
本地方法栈与Java虚拟机栈类似,但它是为Native代码服务的。即使用JNI技术将C/C++代码嵌入到Java应用程序中时所需使用到的堆外内存区域。 - Java堆(Heap)
Java堆是所有对象实例和数组对象所占据空间之总称,在JVM启动后就已经存在并由GC进行管理清理工作。其中还可细分为新生代和老年代两部分以及永久代/元数据区域等特殊目录。 - 方法区/元数据区
方法区也叫非堆内存或永久带(Permanent Generation),主要用来保存已加载类信息、常量池、静态变量、即使编译生成代码缓存等数据结构。 - 执行引擎
- 垃圾回收器
5.2 运行时内存划分
当Java程序启动时,JVM会为其分配一块内存作为运行时数据区域。这个运行时数据区域被划分成了几个不同的部分,每个部分都有自己的用途和特点。
- 程序计数器(Program Counter Register):是一块较小的内存空间,它可以看做是当前线程所执行的字节码指令的地址指示器。在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个核心)只能执行一条线程中的指令,在此情况下,为了线程切换后能恢复到正确位置上继续执行原来进度, 每个线程都需要有独立、连续 的程序计数器。
- Java虚拟机栈(Java Virtual Machine Stacks):每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每次方法调用结束后该栈帧也随之销毁。因此,虚拟机栈中保存着正在执行Java方法所需的所有数据和信息。
- 本地方法栈(Native Method Stack):与虚拟机栈类似,本地方法栈则为虚拟机使用到得Native 方法服务。
- Java堆(Java Heap):Java堆是JVM管理最大的一块内存空间,在虚拟机启动时创建,并且唯一目标就是存放对象实例。几乎所有对象实例以及数组都要在堆上进行分配。
- 方法区(Method Area) :另外一种称呼叫非堆(Non-Heap),主要用于存储已被加载类信息、常量池、静态变量等数据
- 运行时常量池(Runtime Constant Pool): 是Method Area 的重要组成部分, Class 文件除了有类版本号, 字段, 方法代码等描述信息外还包含了很多其他附加信息 , 其中便包含了符号引用(Symbolic Reference), 符号引用直接或者间接指向字符串常量或者类型描述符.
- 直接内存(Direct Memory) : 不属于JVM规范定义好得运行期内存区域 ,但NIO库可以通过DirectByteBuffer 使用直接内存在进行I/O操作从而提高性能 。
5.3 垃圾回收
Java中的垃圾回收是一种自动内存管理机制,它可以在程序运行时自动识别和释放不再使用的对象所占用的内存空间。这个过程由JVM负责执行。
Java中的垃圾回收器通常会按照以下步骤进行工作:
- 标记:首先,垃圾回收器会标记出所有仍然被引用着的对象。这些对象将被视为“活跃”的,并且不会被清理掉。
- 清除:接下来,垃圾回收器会扫描堆中所有未标记(即未被引用)的对象,并将其从堆中删除。这样就释放了一些内存空间。
- 压缩:最后,在清除完毕之后,如果有必要,垃圾回收器还可以对剩余的内存空间进行压缩整理操作。这个过程可以让已经分散到各处的碎片化内存块重新组合成更大、更连续、更易于分配使用的块。
需要注意几点:- Java虚拟机并没有规定具体实现方式, 不同厂商或者版本可能采取不同策略
- 以上三步骤也可能同时发生。
- 在某些情况下(如System.gc()调用) JVM 可能只执行部分GC 或者根本没做任何事情
另外值得提及以下几类GC算法:
- 标记 - 清除 (Mark and Sweep): 最基础简单 GC 算法, 先标记可达区域, 再清除非可达区域.
- 复制(Copying): 将原始堆划分为两个相等大小区域 , 活跃数据复制到其中一个上面去 ,当该区满时把还活着数据复制到另外一个上面去 。每次都是对其中一个半区进行处理 ,而另一个半区则保持为空 。优点: 避免了"碎片化", 缺点: 浪费50% 的空间。
- 标记 - 整理(Mark and Compact): 类似 Mark-Sweep , 区别在于完成标志后直接移动活跃数据至一端 , 然后直接清理边界以外全部内容.
- 分代(Generational Collection) : 把 Java 堆划分成新生代和老年代两个部分, 新生代里面很多对象朝生夕死(80%左右), 而老年代里面则有很多长期存在并且随时间增长而越来越稳定地存在下来 的对象 . 因此针对新生成和老年代采取不同策略.
总之选择哪种 GC 算法主要依据应用场景与性能需求等因素考虑
5.4 为什么说Java是平台独立性语言?
ava被称为平台独立性语言,主要是因为它的编译方式和执行环境都具有跨平台特性。这种跨平台能力源于以下两个方面:
- Java编译器将Java代码编译成字节码(Bytecode),而不是直接生成机器码。字节码可以在任何支持Java虚拟机(JVM)的操作系统上运行,包括Windows、Linux、MacOS等。
- JVM提供了一个标准化的运行时环境,它负责解释并执行字节码,并且对底层硬件进行抽象化处理。这样一来,在不同的操作系统上使用相同版本的JVM就可以保证程序在各个平台上都能够正确地运行。
因此,只要安装了适当版本的JVM,无论是哪种操作系统或者硬件架构下都可以运行相同版本的Java程序。这也使得开发人员无需关注底层硬件和操作系统差异问题, 更加专注于业务逻辑实现.
5.5 Java平台与其他语言平台有什么区别?
Java平台与其他语言平台有以下几个区别:
- 跨平台性:Java是一种跨平台的编程语言,它可以在不同的操作系统上运行。这是因为Java程序被编译成字节码,并且可以在任何支持Java虚拟机(JVM)的计算机上运行。
- 内存管理:与C++等语言相比,Java具有自动内存管理功能。这意味着开发人员无需手动分配和释放内存,而是由垃圾回收器来处理。
- 安全性:由于其安全特性,许多企业使用Java作为其首选开发语言。 Java提供了一个安全模型,在其中应用程序只能访问受控制的资源。
- 多线程支持: Java提供了强大的多线程支持,使得开发人员能够轻松地创建并发应用程序。
- 面向对象编程范式: Java 是一种面向对象编程 (OOP) 语言, 提供类、继承、封装和抽象等 OOP 特征, 这些特征使得代码更加可重用、易于维护和扩展.
总之, Java 平台以其跨平台性、自动内存管理、安全性、多线程支持及面向对象编程范式等优势脱颖而出,并广泛应用于Web 应用程序开发中。
5.6 JVM加载class文件的原理和机制
JVM(Java虚拟机)加载class文件的原理和机制如下:
- 类加载器:当一个类被使用时,JVM会通过类加载器将该类的.class文件从磁盘读取到内存中。在Java中有三种不同类型的类加载器:启动类加载器、扩展类加载器和应用程序或系统类加载器。
- 验证:一旦.class文件被读入内存,JVM会对其进行验证以确保它符合Java语言规范并且没有安全漏洞。这个过程包括检查字节码是否正确、是否存在不允许出现的指令序列等。
- 准备阶段:在准备阶段,JVM为静态变量分配内存,并设置默认值。例如,int类型的静态变量默认值为0。
- 解析阶段:解析是将符号引用转换为直接引用的过程。在此阶段,常量池中所有符号引用都将被替换成直接引用。
- 初始化阶段: 在初始化阶段, JVM 会执行 static 块代码, 并赋予静态变量初始值.
- 使用和卸载: 当一个对象不再被使用时,它就可以被垃圾回收机制回收并释放相应资源;而当一个Classloader实例不再需要时,则可以通过垃圾回收机制自动卸载掉相关Class信息.
总之,JVM 加载 class 文件主要经历了以下步骤: 类装载、链接(验证、准备与解析) 和初始化, 进而使得 Java 程序能够正常运行起来。
5.7 Java是否存在内存泄露问题?
是的,Java也存在内存泄漏问题。虽然Java具有自动内存管理功能,但仍然可能发生内存泄漏。当一个对象不再被使用时,垃圾回收器会将其从堆中删除并释放相应的内存空间。但如果该对象仍然被其他对象引用,则它将无法被垃圾回收机制清除,并且在程序运行期间一直占用着系统资源。
以下是几种常见的导致Java内存泄漏的情况:
- 静态集合类:静态集合类(如HashMap、ArrayList等)容易导致内存泄露。如果这些集合类持有对某个对象的引用,并且该对象已经不再需要了,那么这个对象就不能被垃圾回收机制清除。
- 内部类和匿名类:当一个外部类实例化了一个非静态的内部或匿名类时,它们会隐式地保留对外部实例变量的引用,在外部实例没有显式地解除引用之前,这些子级都不能被垃圾回收。
- 单例模式:单例模式通常使用静态成员变量来保存唯一实例,并通过私有构造函数防止创建多个实例。如果单例持续存在于应用程序中并且未正确处理,则可能导致永久性内存泄漏。
- 对象池: 如果缓冲池或连接池过大, 也可能会导致 Java 程序出现 OOM (Out Of Memory) 错误.
为避免Java中出现内存泄露问题, 开发人员可以采取以下措施:
- 及时解除不必要的引用
- 使用弱引用或软引用代替强引用
- 尽量避免使用static关键字定义变量
- 使用try-with-resources语句块关闭I/O流等资源.
5.8 Java中的堆和栈的区别是什么?
在Java中,堆和栈是两个不同的内存区域,用于存储程序运行时所需的数据。它们之间的主要区别如下:
- 分配方式:堆是由JVM自动分配和管理的一块内存区域,而栈则是按照方法调用顺序自动分配和释放。
- 存储内容:堆主要用于存储对象实例以及数组等复杂数据类型,而栈主要用于存储基本数据类型(例如int、float、double等)以及对象引用。
- 内存大小:堆通常比栈大得多,并且可以动态增长或缩小。但是,在某些情况下,如果使用了过多的对象实例,则可能会导致OutOfMemoryError异常。
- 访问速度:由于JVM需要对堆进行更复杂的管理操作(例如垃圾回收),因此访问速度较慢。相反,由于栈具有固定大小并且只包含简单数据类型,因此访问速度非常快。
总之,在编写Java代码时,请注意将适当数量的变量保存在栈上,并尽可能减少创建新对象实例来避免影响性能。
5.9 JVM的常见参数
JVM是Java虚拟机的缩写,它是Java程序运行的核心组件。在启动JVM时,可以使用一些参数来配置其行为和性能。以下是常见的JVM参数:
- -Xmx:指定最大堆内存大小。
- -Xms:指定初始堆内存大小。
- -XX:MaxPermSize:指定最大永久代(Permanent Generation)空间大小。
- -XX:+UseConcMarkSweepGC:开启CMS垃圾回收器。
- -XX:+UseParallelGC:开启并行垃圾回收器。
- -verbose:gc:输出详细的GC日志信息。
- -Djava.security.manager=XXX:设置安全管理器类名。
除了上述参数外,还有许多其他可用于调整性能和优化应用程序的JVM参数。但需要注意,在使用这些参数之前,请确保您已经理解了它们所做的事情,并且对系统进行充分测试以确保不会出现意外问题。
第六章:设计模式
6.1 设计模式有哪些原则
- 单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因。
- 开放封闭原则(Open-Closed Principle,OCP):软件实体应该对扩展开放,对修改关闭。
- 里氏替换原则(Liskov Substitution Principle,LSP):子类型必须能够替换掉它们的父类型。
- 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖于它不需要使用的接口。
- 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块。两者都应该依赖于抽象接口。抽象接口不应该依赖于具体实现细节。具体实现细节应该依赖于抽象接口。
- 迪米特法则或最少知识原则(Law of Demeter or Least Knowledge, LoD/LKP) :一个对象应当对其他对象保持最少的了解
- 合成/聚合复用原则 (Composite/Aggregate Reuse principle,CARP) : 尽量使用合成/聚合关系来代替继承关系
6.2 单例模式
单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供了全局访问点。
在单例模式中,通过将构造函数私有化来防止外部直接创建对象。然后,在该类内部定义一个静态方法或变量来返回唯一的实例。这个静态方法或变量可以被其他代码调用以获取该类的唯一实例。
单例模式通常使用懒加载方式进行初始化,即只有在第一次请求时才会创建实例。此外,为了确保线程安全和避免多次初始化,需要对其进行同步处理。
应用场景:当我们需要确保系统中某个类只存在一个实例时就可以考虑使用单例模式。例如数据库连接池、日志记录器等情况下都适合使用单例模式来管理资源并减少不必要的开销。
6.3 工厂模式
工厂模式是一种创建型设计模式,它提供了一个通用的接口来创建对象,但允许子类决定要实例化的类。这样可以将对象的创建与使用分离开来,并且可以降低代码之间的耦合度。
在工厂模式中,我们定义一个抽象工厂接口和多个具体工厂实现该接口。每个具体工厂都负责创建特定类型的对象。客户端通过调用抽象工厂方法并传递相应参数来获取所需类型的对象。
优点: 工厂模式可以帮助我们隐藏复杂性、降低耦合度、增加可扩展性以及保持代码清晰易懂等方面有很大好处
应用场景:当需要根据不同条件生成不同实例时就可以考虑使用工厂模式。例如,在游戏中需要根据用户选择生成不同角色或武器;在电商网站上需要根据用户购买记录生成推荐商品列表等情况下都适合使用工厂模式进行处理。
6.4 适配器模式
适配器模式是一种结构型设计模式,它允许将不兼容的对象包装在适配器中以便其能够与其他对象进行交互。适配器模式可以帮助我们复用现有类而无需修改其源代码。
在适配器模式中,我们定义一个适配器类来包装需要使用的类,并实现所需接口或抽象方法。这个适配器类就像一个桥梁,连接了两个不同的接口。当客户端调用目标接口时,实际上是通过调用适配器来间接调用被包装的原始对象。
优点: 通过使用适配器模式可以使得原本不兼容的组件之间协同工作成为可能;同时也提高了代码重用性和灵活性
应用场景: 当需要将已有系统与新系统集成时、或者需要复用旧组件并且又不能对其进行修改时就可以考虑使用该设计模式。例如,在开发过程中经常会遇到第三方库更新导致API变化等情况下都可采取此方式解决问题
6.5 观察者模式
观察者模式是一种行为型设计模式,它定义了对象之间的一对多依赖关系,使得当一个对象状态发生改变时,所有依赖于它的对象都会自动收到通知并进行更新。
在观察者模式中,我们有两个主要角色:Subject(被观察者)和Observer(观察者)。Subject维护着一个列表来存储所有注册过的Observer,并提供了添加、删除和通知等方法。而Observer则通过订阅Subject来接收其状态变化的通知,并根据需要进行相应处理。
优点: 观察者模式可以帮助我们实现松耦合、可扩展性强以及易于维护等方面有很大好处
应用场景: 当需要将系统分离成不同部分且这些部分之间存在着复杂交互时就可以考虑使用该设计模式。例如,在MVC框架中Model与View之间采用此方式解耦;在GUI编程中也经常使用该设计模式实现用户界面组件与数据源之间的交互
第七章:Struts
略
第八章:MyBatis
8.1 MyBatis缓存的基本概念
MyBatis 是一个优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。它通过封装JDBC的操作来实现与数据库的交互,从而把开发者从编写大量的JDBC代码中解脱出来。使用MyBatis,开发者只需要关注SQL 语句开发,而不需要再去编写加载驱动、创建连接、创建statement,释放连接等繁杂的操作。
MyBatis 缓存是 MyBatis 框架中的一个重要特性,它包括一级缓存和二级缓存。
- 一级缓存:也称为 SqlSession 级别的缓存。当我们执行查询时,MyBatis 会将查询结果缓存到一级缓存中。一级缓存的生命周期与 SqlSession 保持一致,当 SqlSession 被关闭或者清空,那么一级缓存就会消失。
- 二级缓存:也称为 Mapper 级别的缓存。当我们执行查询时,MyBatis 会将查询结果缓存到二级缓存中。二级缓存的生命周期与 Mapper 保持一致,也就是说,只要 Mapper 不被卸载,那么二级缓存就会一直存在。
MyBatis 缓存的主要目的是提高应用程序的性能,通过缓存数据减少对数据库的访问次数,从而提高应用程序的响应速度。但需要注意的是,由于 MyBatis 的缓存是存在内存中的,如果数据库中的数据发生了改变,而 MyBatis 的缓存中的数据没有及时更新,那么就会出现数据不一致的问题。因此,在使用 MyBatis 缓存时,需要考虑到数据一致性的问题。
8.2 MyBatis分页
MyBatis分页是将所有数据分段展示给用户的技术。
MyBatis分页有逻辑分页和物理分页两种方式。逻辑分页是我们的程序在显示每页的数据时,首先查询得到表中的所有数据,然后根据当前页的“页码”选出其中的一部分数据来显示。物理分页是程序先判断出该选出所有数据的哪一部分,然后数据库根据程序给出的信息查询出程序需要的部分数据返回给我们的程序。
8.3 MyBatis的查询类型
MyBatis的查询类型有三种,分别是:
- SelectOne:适用于返回结果只有一条数据的情况。
- SelectList:适用于返回结果有多条数据的情况1。
- SelectMap:适用于需要在查询结果中,通过某列的值取到这行数据的情况。
8.4 MyBatis的延时加载
MyBatis的延时加载也称为懒加载,是指在进行关联查询时,按照设置的延迟规则推迟对关联对象的select查询。
MyBatis的延时加载可以有效的减少数据库压力。
MyBatis根据对关联对象查询的select语句的执行时机,分为三类:直接加载、侵入式延迟加载、深度延迟加载:
- 直接加载:即不延迟加载,执行完主加载对象的select语句,马上执行对关联对象的select查询。
- 侵入式延迟加载:执行完主加载对象的select语句,不会执行对关联对象的select查询。但当要访问主加载对象的某个属性(该属性不是关联对象的属性)时,就会马上执行关联对象的select查询。
- 深度延迟加载:执行完主加载对象的select语句,不会执行对关联对象的select查询。
第九章:Redis
9.1 Redis的基本概念与优缺点
Redis是一个开源的、内存数据结构存储系统,它支持多种数据结构(如Strings、Lists、Sets、Sorted Sets、Hashes等)和多种操作(如读取、写入、删除等)。
Redis的优点:
- 性能高:Redis单线程非常适合读多写少的场景,可以减轻数据库压力,数据库并不可以随意横向拓展,并且大多数场景,性能瓶颈都在数据库。
- 集群分布式存储:可以横向拓展,可以做到高可用。
- 数据结构类型丰富:String、List、Set、Sortrd Set、Hash等。
- 支持数据持久化:Redis支持将数据写入磁盘,保证数据的可靠性。
- 支持事务:虽然一般用lua脚本来代替事务功能。
Redis的缺点:
- 数据可靠性低:Redis直接将数据存储到内存中,数据有可能在宕机情况会丢失少部分数据。
- 存储成本高:因为是内存存储。
标签:Java,对象,笔记,面试,线程,使用,执行,方法 From: https://www.cnblogs.com/emanuel/p/javamian-shi-bi-ji-2hjdqp.html