还是银行面试舒服些...
大家好,我是小林。
有同学跟我反馈,看互联网大厂的后端面经都看不懂,之前我发过一次银行面经:冲进银行测开,扛住了!,很多留言说终于看懂了。
我昨天刚发了百度提前批的面经:百度提前批,有点难度!,难度确实挺高的,今天分析一篇招生银行的Java 后端校招面经。
整体难度低很多,重点考察了MySQL、Java基础、Java并发这三个方面,问题的导向主要考察你是否用过 mysql,或者 java 代码写的多不多,偏基础实践的问题。
MySQL
数据库的锁你了解吗?
数据库锁从粒度上来分类的话,主要分为全局锁、表级锁、行级锁这三类。
全局锁
要使用全局锁,则要执行这条命令:
flush tables with read lock
执行后,整个数据库就处于只读状态了,这时其他线程执行以下操作,都会被阻塞:
对数据的增删改操作,比如 insert、delete、update等语句; 对表结构的更改操作,比如 alter table、drop table 等语句。
如果要释放全局锁,则要执行这条命令:
unlock tables
当然,当会话断开了,全局锁会被自动释放。
全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
全局锁的缺点:加上全局锁,意味着整个数据库都是只读状态。那么如果数据库里有很多数据,备份就会花费很多的时间,关键是备份期间,业务只能读数据,而不能更新数据,这样会造成业务停滞。
表级锁
MySQL 里面表级别的锁有这几种:
表锁:可以对表加读写锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。 元数据锁(MDL):对一张表进行 CRUD 操作时,加的是 MDL 读锁,MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。 意向锁:表锁和行锁是满足读读共享、读写互斥、写写互斥的,如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。所以意向锁的目的是为了快速判断表里是否有记录被加锁。
行级锁
行级别锁,主要有记录锁、间隙锁、临键锁、插入意向锁。
普通的 select 语句是不会对记录加锁的,因为它属于快照读。如果要在查询时对记录加行锁,可以使用下面这两个方式,这种查询会加锁的语句称为锁定读。
//对读取的记录加共享锁
select ... lock in share mode;
//对读取的记录加独占锁
select ... for update;
共享锁(S锁)满足读读共享,读写互斥。独占锁(X锁)满足写写互斥、读写互斥。
行级锁的类型主要有四类:
Record Lock,记录锁,也就是仅仅把一条记录锁上; Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身; Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。 插入意向锁:一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。
左连接、右连接和内连接的区别?
左连接(Left Join):左连接返回左表中的所有记录,以及满足连接条件的右表中的匹配记录。如果右表中没有匹配的记录,则返回NULL值。左连接以左表为基准,保留左表的所有记录。
右连接(Right Join):右连接返回右表中的所有记录,以及满足连接条件的左表中的匹配记录。如果左表中没有匹配的记录,则返回NULL值。右连接以右表为基准,保留右表的所有记录。
内连接(Inner Join):内连接返回两个表中满足连接条件的记录。只有在左表和右表中都存在匹配的记录才会被返回,其他不满足连接条件的记录将被忽略。
Join.. on... where语句中on后面的和where后面的有什么区别?
ON
和WHERE
子句之间有以下区别:
ON
子句:ON
子句用于指定连接条件,它在JOIN
语句中使用,用于连接两个或多个表的列。ON
子句是在连接过程中使用的,它决定了如何将表进行连接。它通常用于指定两个表之间的相等条件或其他连接条件。WHERE
子句:WHERE
子句用于在查询过程中对结果进行过滤。它在SELECT
语句中使用,用于筛选满足特定条件的记录。WHERE
子句是在连接之后应用的,它根据指定的条件对连接后的结果集进行筛选,只返回满足条件的记录。
ON
子句用于指定连接条件,它决定了表之间的连接方式,而WHERE
子句用于对连接后的结果集进行筛选,只返回满足条件的记录。
事务的四个特性是什么?分别是怎么实现的?
事务是由 MySQL 的引擎来实现的,我们常见的 InnoDB 引擎它是支持事务的。
不过并不是所有的引擎都能支持事务,比如 MySQL 原生的 MyISAM 引擎就不支持事务,也正是这样,所以大多数 MySQL 的引擎都是用 InnoDB。
事务看起来感觉简单,但是要实现事务必须要遵守 4 个特性,分别如下:
原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样,就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。 一致性(Consistency):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况(该情况,用户 A 和 B 均为 600 元,总共 1200 元)。 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的。 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?
持久性是通过 redo log (重做日志)来保证的; 原子性是通过 undo log(回滚日志) 来保证的; 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的; 一致性则是通过持久性+原子性+隔离性来保证;
事务的隔离级别有哪些?
读未提交,指一个事务还没提交时,它做的变更就能被其他事务看到; 读提交,指一个事务提交之后,它做的变更才能被其他事务看到; 可重复读,指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别; 串行化;会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
按隔离水平高低排序如下:
针对不同的隔离级别,并发事务时可能发生的现象也会不同。
索引的优点是什么?缺点是什么?
索引优点:
可以提高查询效率,减少磁盘i/o操作,因为数据库系统可以直接通过索引定位到需要的数据块,而不需要扫描整个表。
索引缺点:
需要占用物理空间,数量越大,占用空间越大; 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大; 会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。
怎么使用索引查询?
我们查询的时候,sql 语句的查询条件要有索引字段,这样可能才会走索查询。
比如,商品表里,有这些行数据:
主键索引的 B+Tree 如图所示(图中叶子节点之间我画了单向链表,但是实际上是双向链表,原图我找不到了,修改不了,偷个懒我不重画了,大家脑补成双向链表就行):
我们执行了下面这条查询语句:
select * from product where id= 5;
这条语句使用了主键索引查询 id 号为 5 的商品。查询过程是这样的,B+Tree 会自顶向下逐层进行查找:
将 5 与根节点的索引数据 (1,10,20) 比较,5 在 1 和 10 之间,所以根据 B+Tree的搜索逻辑,找到第二层的索引数据 (1,4,7); 在第二层的索引数据 (1,4,7)中进行查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数据(4,5,6); 在叶子节点的索引数据(4,5,6)中进行查找,然后我们找到了索引值为 5 的行数据。
数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。那么上面的整个查询过程一共经历了 3 个节点,也就是进行了 3 次 I/O 操作。
B+Tree 存储千万级的数据只需要 3-4 层高度就可以满足,这意味着从千万级的表查询目标数据最多需要 3-4 次磁盘 I/O,所以B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。
Java基础
java的基本类型有哪些?
数据类型 | 描述 | 大小 |
---|---|---|
boolean | 布尔值 | 1位 |
byte | 字节 | 1字节 |
short | 短整型 | 2字节 |
int | 整型 | 4字节 |
long | 长整型 | 8字节 |
float | 单精度浮点型 | 4字节 |
double | 双精度浮点型 | 8字节 |
char | 字符型 | 2字节 |
long和int可以互转吗 ?
可以的,Java中的long
和int
可以相互转换。由于long
类型的范围比int
类型大,因此将int
转换为long
是安全的,而将long
转换为int
可能会导致数据丢失或溢出。
将int
转换为long
可以通过直接赋值或强制类型转换来实现。例如:
int intValue = 10;
long longValue = intValue; // 自动转换,安全的
将long
转换为int
需要使用强制类型转换,但需要注意潜在的数据丢失或溢出问题。
例如:
long longValue = 100L;
int intValue = (int) longValue; // 强制类型转换,可能会有数据丢失或溢出
在将long
转换为int
时,如果longValue
的值超出了int
类型的范围,转换结果将是截断后的低位部分。因此,在进行转换之前,建议先检查longValue
的值是否在int
类型的范围内,以避免数据丢失或溢出的问题。
数据类型转换方式你知道哪些?
自动类型转换(隐式转换):当目标类型的范围大于源类型时,Java会自动将源类型转换为目标类型,不需要显式的类型转换。例如,将
int
转换为long
、将float
转换为double
等。强制类型转换(显式转换):当目标类型的范围小于源类型时,需要使用强制类型转换将源类型转换为目标类型。这可能导致数据丢失或溢出。例如,将
long
转换为int
、将double
转换为int
等。语法为:目标类型 变量名 = (目标类型) 源类型。字符串转换:Java提供了将字符串表示的数据转换为其他类型数据的方法。例如,将字符串转换为整型
int
,可以使用Integer.parseInt()
方法;将字符串转换为浮点型double
,可以使用Double.parseDouble()
方法等。数值之间的转换:Java提供了一些数值类型之间的转换方法,如将整型转换为字符型、将字符型转换为整型等。这些转换方式可以通过类型的包装类来实现,例如
Character
类、Integer
类等提供了相应的转换方法。
类型互转会出现什么问题吗?
数据丢失:当将一个范围较大的数据类型转换为一个范围较小的数据类型时,可能会发生数据丢失。例如,将一个
long
类型的值转换为int
类型时,如果long
值超出了int
类型的范围,转换结果将是截断后的低位部分,高位部分的数据将丢失。数据溢出:与数据丢失相反,当将一个范围较小的数据类型转换为一个范围较大的数据类型时,可能会发生数据溢出。例如,将一个
int
类型的值转换为long
类型时,转换结果会填充额外的高位空间,但原始数据仍然保持不变。精度损失:在进行浮点数类型的转换时,可能会发生精度损失。由于浮点数的表示方式不同,将一个单精度浮点数(
float
)转换为双精度浮点数(double
)时,精度可能会损失。类型不匹配导致的错误:在进行类型转换时,需要确保源类型和目标类型是兼容的。如果两者不兼容,会导致编译错误或运行时错误。
为什么用bigDecimal 不用double ?
double会出现精度丢失的问题,double执行的是二进制浮点运算,二进制有些情况下不能准确的表示一个小数,就像十进制不能准确的表示1/3(1/3=0.3333...),也就是说二进制表示小数的时候只能够表示能够用1/(2^n)的和的任意组合,但是0.1不能够精确表示,因为它不能够表示成为1/(2^n)的和的形式。
比如:
System.out.println(0.05 + 0.01);
System.out.println(1.0 - 0.42);
System.out.println(4.015 * 100);
System.out.println(123.3 / 100);
输出:
0.060000000000000005
0.5800000000000001
401.49999999999994
1.2329999999999999
可以看到在Java中进行浮点数运算的时候,会出现丢失精度的问题。那么我们如果在进行商品价格计算的时候,就会出现问题。很有可能造成我们手中有0.06元,却无法购买一个0.05元和一个0.01元的商品。因为如上所示,他们两个的总和为0.060000000000000005。这无疑是一个很严重的问题,尤其是当电商网站的并发量上去的时候,出现的问题将是巨大的。可能会导致无法下单,或者对账出现问题。
而 Decimal 是精确计算 , 所以一般牵扯到金钱的计算 , 都使用 Decimal。
import java.math.BigDecimal;
public class BigDecimalExample {
public static void main(String[] args) {
BigDecimal num1 = new BigDecimal("0.1");
BigDecimal num2 = new BigDecimal("0.2");
BigDecimal sum = num1.add(num2);
BigDecimal product = num1.multiply(num2);
System.out.println("Sum: " + sum);
System.out.println("Product: " + product);
}
}
//输出
Sum: 0.3
Product: 0.02
在上述代码中,我们创建了两个BigDecimal
对象num1
和num2
,分别表示0.1和0.2这两个十进制数。然后,我们使用add()
方法计算它们的和,并使用multiply()
方法计算它们的乘积。最后,我们通过System.out.println()
打印结果。
这样的使用BigDecimal
可以确保精确的十进制数值计算,避免了使用double
可能出现的舍入误差。需要注意的是,在创建BigDecimal
对象时,应该使用字符串作为参数,而不是直接使用浮点数值,以避免浮点数精度丢失。
装箱和拆箱是什么?
装箱(Boxing)和拆箱(Unboxing)是将基本数据类型和对应的包装类之间进行转换的过程。
Integer i = 10; //装箱
int n = i; //拆箱
自动装箱主要发生在两种情况,一种是赋值时,另一种是在方法调用的时候。
赋值时
这是最常见的一种情况,在Java 1.5以前我们需要手动地进行转换才行,而现在所有的转换都是由编译器来完成。
//before autoboxing
Integer iObject = Integer.valueOf(3);
Int iPrimitive = iObject.intValue()
//after java5
Integer iObject = 3; //autobxing - primitive to wrapper conversion
int iPrimitive = iObject; //unboxing - object to primitive conversion
方法调用时
当我们在方法调用时,我们可以传入原始数据值或者对象,同样编译器会帮我们进行转换。
public static Integer show(Integer iParam){
System.out.println("autoboxing example - method invocation i: " + iParam);
return iParam;
}
//autoboxing and unboxing in method invocation
show(3); //autoboxing
int result = show(3); //unboxing because return type of method is Integer
show方法接受Integer对象作为参数,当调用show(3)
时,会将int值转换成对应的Integer对象,这就是所谓的自动装箱,show方法返回Integer对象,而int result = show(3);
中result为int类型,所以这时候发生自动拆箱操作,将show方法的返回的Integer对象转换成int值。
自动装箱的弊端
自动装箱有一个问题,那就是在一个循环中进行自动装箱操作的情况,如下面的例子就会创建多余的对象,影响程序的性能。
Integer sum = 0; for(int i=1000; i<5000; i++){ sum+=i; }
上面的代码sum+=i
可以看成sum = sum + i
,但是+
这个操作符不适用于Integer对象,首先sum进行自动拆箱操作,进行数值相加操作,最后发生自动装箱操作转换成Integer对象。其内部变化如下
int result = sum.intValue() + i; Integer sum = new Integer(result);
由于我们这里声明的sum为Integer类型,在上面的循环中会创建将近4000个无用的Integer对象,在这样庞大的循环中,会降低程序的性能并且加重了垃圾回收的工作量。因此在我们编程时,需要注意到这一点,正确地声明变量类型,避免因为自动装箱引起的性能问题。
Java异常处理 有哪些?
异常处理是通过使用try-catch语句块来捕获和处理异常。以下是Java中常用的异常处理方式:
try-catch语句块:用于捕获并处理可能抛出的异常。try块中包含可能抛出异常的代码,catch块用于捕获并处理特定类型的异常。可以有多个catch块来处理不同类型的异常。
try {
// 可能抛出异常的代码
} catch (ExceptionType1 e1) {
// 处理异常类型1的逻辑
} catch (ExceptionType2 e2) {
// 处理异常类型2的逻辑
} catch (ExceptionType3 e3) {
// 处理异常类型3的逻辑
} finally {
// 可选的finally块,用于定义无论是否发生异常都会执行的代码
}
throw语句:用于手动抛出异常。可以根据需要在代码中使用throw语句主动抛出特定类型的异常。
throw new ExceptionType("Exception message");
throws关键字:用于在方法声明中声明可能抛出的异常类型。如果一个方法可能抛出异常,但不想在方法内部进行处理,可以使用throws关键字将异常传递给调用者来处理。
public void methodName() throws ExceptionType {
// 方法体
}
finally块:用于定义无论是否发生异常都会执行的代码块。通常用于释放资源,确保资源的正确关闭。
try {
// 可能抛出异常的代码
} catch (ExceptionType e) {
// 处理异常的逻辑
} finally {
// 无论是否发生异常,都会执行的代码
}
java异常分类有哪些?
两个子类区别:
Error:程序不应该捕捉的错误,应该交由JVM来处理。一般可能指非常重大的错误。这个错误我们一般获取不到,也无法处理!
Exception:程序中应该要捕获的错误。这个异常类及它的子类是我们需要学习获取要处理的。
RuntimeException:运行时异常,也叫未检查异常,是Exception的子类,但不需捕捉的异常超类,但是实际发生异常时,还是会导致程序停止运行的的,只是编译时没有报错而已。比如除数为零,数组空指针等等,这些都是在运行之后才会报错。此类异常,可以处理也可以不处理,并且可以避免。 在Exception的所有子类中 除了RuntimeException类和它的子类,其他类都叫做非运行时异常,或者叫已检查异常,通常被定义为Checked类,是必须要处理可能出现的异常,否则编译就报错了。Checked类主要包含:IO类和SQL类的异常情况,这些在使用时经常要先处理异常(使用throws或try catch捕获)。
try catch中的语句运行情况
try块中的代码将按顺序执行,如果抛出异常,将在catch块中进行匹配和处理,然后程序将继续执行catch块之后的代码。如果没有匹配的catch块,异常将被传递给上一层调用的方法。
Equals和==的区别?
对于字符串变量来说,使用"=="和"equals"比较字符串时,其比较方法不同。"=="比较两个变量本身的值,即两个对象在内存中的首地址,"equals"比较字符串包含内容是否相同。
对于非字符串变量来说,如果没有对equals()进行重写的话,"==" 和 "equals"方法的作用是相同的,都是用来比较对象在堆内存中的首地址,即用来比较两个引用变量是否指向同一个对象。
==:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较; equals():比较的是两个字符串的内容,属于内容比较。
New出的对象什么时候回收?
通过过关键字new
创建的对象,由Java的垃圾回收器(Garbage Collector)负责回收。垃圾回收器的工作是在程序运行过程中自动进行的,它会周期性地检测不再被引用的对象,并将其回收释放内存。
具体来说,Java对象的回收时机是由垃圾回收器根据一些算法来决定的,主要有以下几种情况:
引用计数法:某个对象的引用计数为0时,表示该对象不再被引用,可以被回收。 可达性分析算法:从根对象(如方法区中的类静态属性、方法中的局部变量等)出发,通过对象之间的引用链进行遍历,如果存在一条引用链到达某个对象,则说明该对象是可达的,反之不可达,不可达的对象将被回收。 终结器(Finalizer):如果对象重写了 finalize()
方法,垃圾回收器会在回收该对象之前调用finalize()
方法,对象可以在finalize()
方法中进行一些清理操作。然而,终结器机制的使用不被推荐,因为它的执行时间是不确定的,可能会导致不可预测的性能问题。
Java并发
怎么实现多线程?
在Java中,有多种方式可以实现多线程:
继承Thread类:创建一个继承自Thread类的子类,并重写其run()方法,在run()方法中定义线程要执行的任务。然后通过创建子类的实例,并调用start()方法来启动线程。
class MyThread extends Thread {
public void run() {
// 线程要执行的任务
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
实现Runnable接口:创建一个实现了Runnable接口的类,并实现其run()方法,在run()方法中定义线程要执行的任务。然后通过创建Runnable接口的实例,并将其作为参数传递给Thread类的构造方法,最后调用start()方法来启动线程。
class MyRunnable implements Runnable {
public void run() {
// 线程要执行的任务
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
使用线程池:可以使用java.util.concurrent包中的Executor框架来创建线程池,通过提交任务给线程池来执行。这种方式可以更好地管理线程的生命周期和资源,并提供了更多的灵活性和性能优化。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
executor.execute(new Runnable() {
public void run() {
// 线程要执行的任务
}
});
}
executor.shutdown();
}
}
需要注意的是,在多线程编程中要注意线程安全和资源共享的问题,避免出现竞态条件和数据不一致等问题。
怎么启动线程 ?
启动线程的通过Thread类的**start()**。
//创建两个线程,用start启动线程
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
run方法可以传参吗 ?
在Java中,Thread类的run()方法是不支持传参的。Thread类的run()方法是一个无参数的方法,它定义了线程要执行的任务。当你通过调用start()方法启动线程时,线程会在新的执行上下文中执行run()方法。
如果你需要在线程启动之前传递参数给线程的任务,可以通过以下方式实现:
在Thread类的子类中添加成员变量,并在构造方法中初始化这些成员变量。然后在run()方法中使用这些成员变量作为参数。
class MyThread extends Thread {
private String message;
public MyThread(String message) {
this.message = message;
}
public void run() {
System.out.println(message);
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread("Hello, World!");
thread.start();
}
}
如果使用实现Runnable接口的方式创建线程,可以在实现类中添加成员变量,并在构造方法中初始化这些成员变量。然后在run()方法中使用这些成员变量作为参数。
class MyRunnable implements Runnable {
private String message;
public MyRunnable(String message) {
this.message = message;
}
public void run() {
System.out.println(message);
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable("Hello, World!");
Thread thread = new Thread(runnable);
thread.start();
}
}
两个线程访先后访问Synchronsized关键字修饰的资源,会怎么样 ?
当两个线程同时访问一个由synchronized
关键字修饰的资源时,会发生以下情况:
互斥访问:
synchronized
关键字保证了同一时间只有一个线程可以访问被修饰的资源。当一个线程进入synchronized
代码块或方法时,它会获取到该资源的锁,并且其他线程无法同时进入该代码块或方法,它们会被阻塞,直到锁被释放。顺序执行:当一个线程获取到资源的锁并执行代码块或方法时,其他线程必须等待该线程释放锁后才能获取锁并执行代码块或方法。这样保证了线程对资源的访问是有序的,即先获取锁的线程先执行,后获取锁的线程后执行。
什么情况用线程池?
线程池的主要作用是管理和复用线程,提供了一种有效地管理线程的方式,可以在需要时创建线程,并在完成任务后重复利用这些线程,而不是频繁地创建和销毁线程。以下情况可以考虑使用线程池:
需要异步执行任务:当需要执行一些耗时的任务,但又不希望阻塞主线程时,可以使用线程池来异步执行这些任务,从而提高程序的响应性能。
需要管理并发线程数量:线程池可以限制同一时间执行的线程数量,避免线程过多导致系统资源耗尽,提高系统的稳定性和效率。
需要提高任务执行的效率:线程池通过线程的复用避免了创建和销毁线程的开销,可以在高并发环境下更高效地执行任务。
什么时候销毁线程?
当缓存队列中的任务都执行完了的时候,线程池中的线程数如果大于核心线程数,就销毁多出来的线程,直到线程池中的线程数等于核心线程数。
网络
http请求的流程
URL解析:对 URL
进行解析,解析出域名、方法、资源等,然后生成 http 请求报文。域名解析:对域名进行 dns 解析,首先会看浏览器和操作系统是否有 dns 解析的缓存,如果没有的话,就会通过dns 解析得到 IP。 建立TCP连接:浏览器使用HTTP协议通过TCP/IP建立与百度服务器的连接。它会向百度服务器发送一个SYN(同步)包,然后等待百度服务器的确认响应。 三次握手:百度服务器收到浏览器发送的SYN包后,会发送一个SYN+ACK(同步确认)包给浏览器,表示接受连接请求。浏览器收到百度服务器的响应后,会发送一个ACK(确认)包给服务器,完成三次握手,建立可靠的连接。 发送HTTP请求:浏览器向百度服务器发送一个HTTP请求,请求百度首页的HTML文档。请求中包含了请求方法、请求头和其他相关信息。 服务器处理请求:百度服务器接收到浏览器发送的HTTP请求后,会根据请求的内容进行处理。它可能会读取数据库、执行相关的业务逻辑,并生成响应数据。 发送HTTP响应:百度服务器将生成的响应数据封装成HTTP响应报文,并发送回浏览器。响应报文中包含了响应状态码、响应头和响应体等信息。 接收响应和渲染页面:浏览器接收到百度服务器发送的HTTP响应后,会解析响应报文,提取出HTML文档和其他相关资源。浏览器会根据HTML文档的结构和CSS样式,渲染出页面的可视化效果。