1.String、StringBuffer和StringBuilder的区别
都是final类,都不允许被继承;
String类长度是不可变的,StringBuffer和StringBuilder类长度是可以改变的;
String 和 StringBuffer:
1、String类型和StringBuffer类型的主要性能区别:String是不可变的对象,因此每次在对String类进行改变的时候都会生成一个新的string对象,然后将指针指向新的string对象,所以经常要改变字符串长度的话不要使用string,因为每次生成对象都会对系统性能产生影响,特别是当内存中引用的对象多了以后,JVM的GC就会开始工作,性能就会降低;
StringBuffer 和 StringBuilder 的 有3 个区别
区别1:线程安全
StringBuffer:线程安全,StringBuilder:线程不安全。因为 StringBuffer 的所有公开方法都是 synchronized 修饰的,而 StringBuilder 并没有 synchronized 修饰。
StringBuffer 代码片段:
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
区别2:缓冲区
StringBuffer 代码片段:
private transient char[] toStringCache;
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
StringBuilder 代码片段:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
可以看出,StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。
而 StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。
所以,缓存冲这也是对 StringBuffer 的一个优化吧,不过 StringBuffer 的这个toString 方法仍然是同步的。
区别3:性能
既然 StringBuffer 是线程安全的,它的所有公开方法都是同步的,StringBuilder 是没有对方法加锁同步的,所以毫无疑问,StringBuilder 的性能要远大于 StringBuffer。
总结
所以,StringBuffer 适用于用在多线程操作同一个 StringBuffer 的场景,如果是单线程场合 StringBuilder 更适合。
2.MySQL的索引有几种类型?
按照逻辑分类,索引可分为:
主键索引:一张表只能有一个主键索引,不允许重复,不允许为null;
唯一索引:数据列不允许重复,允许为NULL值,一张表可有多个唯一索引,但是一个唯一索引只能包含
一列,比如身份证号码,卡号都可以作为唯一索引;
普通索引:一张表可以创建多个普通索引,一个普通索引可以包含多个字段,允许数据重复,允许NULL值插入;
全文索引:让搜索关键词更高效的一种索引;
按照物理分类,索引可分为:
聚集索引:一般是表中的主键索引,如果表中没有显示指定主键,则会选择表中的第一个不允许为NULL的唯一索引,
如果还是没有的画,就采用Innodb存储引擎为每行数据内置的6字节rowid作为聚集索引。每张表只有一个聚集索引,
因为聚集索引的兼职的逻辑顺序决定了表中相应行的物理顺序。聚集索引在精确查找和范围查找方面有良好的性能表
现(相对于普通索引和全表扫描),聚集索引就显得弥足珍贵,聚集索引选择还是要慎重(一般不会让没有语义的自增
id充当聚集索引);
* 如果表设置了主键,则主键就是聚簇索引
* 如果表没有主键,则会默认第一个NOT NULL,且唯一(UNIQUE)的列作为聚簇索引
* 以上都没有,则会默认创建一个隐藏的row_id作为聚簇索引
非聚集索引:它存储的是 索引键值与对应数据行的指针(Row Locator),而不是数据本身。因此,非聚簇索引的顺序与表中数据的物理存储顺序无关。一个表中可以拥有多个非聚集
索引;
创建主键索引
alter table t add primary key add(`id`)
创建唯一索引
alter table t add unique(`username`)
创建普通索引
alter table t add index index_name(`username`)
创建全文索引
alter table t add fulltext (`username`)
聚簇索引和非聚簇索引(也叫二级索引)的区别
创建聚集索引
1.创建表的时候指定主键(注意:SQL Sever默认主键为聚集索引,也可以指定为非聚集索引,而MySQL里主键就是聚集索引)
create table t1(
id int primary key,
name nvarchar(255)
)
非聚集(unclustered)索引。
定义:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。
非聚集索引,分成普通索引,唯一索引,全文索引。
使用非聚簇索引,可以直接从索引的节点里获取到查询列的数据。没有建索引的话,则需要二次查询去获取原始数据行的score.
聚集索引与非聚集索引的区别是:叶节点是否存放一整行记录
InnoDB 主键使用的是聚簇索引,MyISAM 不管是主键索引,还是二级索引使用的都是非聚簇索引。
下图形象说明了聚簇索引表(InnoDB)和非聚簇索引(MyISAM)的区别:

聚簇索引与非聚簇索引
1.对于非聚簇索引表来说(右图),表数据和索引是分成两部分存储的,主键索引和二级索引存储上没有任何区别。使用的是B+树作为索引的存储结构,所有的节点都是索引,叶子节点存储的是索引+索引对应的记录的数据。
2.对于聚簇索引表来说(左图),表数据是和主键一起存储的,主键索引的叶结点存储行数据(包含了主键值),二级索引的叶结点存储行的主键值。使用的是B+树作为索引的存储结构,非叶子节点都是索引关键字,但非叶子节点中的关键字中不存储对应记录的具体内容或内容地址。叶子节点上的数据是主键与具体记录(数据内容)。
二级索引:叶子节点中存储主键值,每次查找数据时,根据索引找到叶子节点中的主键值,根据主键值再到聚簇索引中得到完整的一行记录。
聚簇索引的优点
1.当你需要取出一定范围内的数据时,用聚簇索引也比用非聚簇索引好。
2.当通过聚簇索引查找目标数据时理论上比非聚簇索引要快,因为非聚簇索引定位到对应主键时还要多一次目标记录寻址,即多一次I/O。
3.使用覆盖索引扫描的查询可以直接使用页节点中的主键值。
聚簇索引的缺点
1.插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键。
2.更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新。
3.二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。
二级索引的叶节点存储的是主键值,而不是行指针(非聚簇索引存储的是指针或者说是地址),这是为了减少当出现行移动或数据页分裂时二级索引的维护工作,但会让二级索引占用更多的空间。
4.采用聚簇索引插入新值比采用非聚簇索引插入新值的速度要慢很多,因为插入要保证主键不能重复,判断主键不能重复,采用的方式在不同的索引下面会有很大的性能差距,聚簇索引遍历所有的叶子节点,非聚簇索引也判断所有的叶子节点,但是聚簇索引的叶子节点除了带有主键还有记录值,记录的大小往往比主键要大的多。这样就会导致聚簇索引在判定新记录携带的主键是否重复时进行昂贵的I/O代价。
btree树为什么查询速度快
B树(B-Tree)是一种平衡的多叉树,广泛应用于数据库和文件系统中,特别适合在磁盘存储中实现。B树的查询速度快,主要得益于以下几个关键原因:
1. 降低树的高度
- B树的每个节点可以包含多个子节点,因此B树的高度通常比二叉树要低得多。
- 在一个平衡的二叉搜索树(如 AVL 树或红黑树)中,每个节点只有两个子节点,而在B树中,每个节点可以有多个子节点(取决于阶数 m)。这意味着在存储相同数量的元素时,B树的高度会更低。
- 较低的高度意味着查询过程中需要的节点访问次数更少,进而减少了磁盘I/O操作的次数(在磁盘存储的情况下),提升了查询效率。
2. 减少磁盘I/O操作
- B树通常用于处理大量数据,并存储在磁盘中。由于磁盘的随机访问速度较慢,因此减少磁盘I/O次数是提升查询速度的关键。
- B树的每个节点可以容纳多个键和指针(在高阶 B 树中,这个数值可以很大),所以每次读取一个节点时,就可以同时加载多个键。
- 在查询过程中,每次加载一个节点后,可以根据该节点中的键直接判断进入哪个子节点,这样就减少了访问磁盘的次数,从而加快了查询速度。
3. 良好的局部性
- B树在磁盘存储中通常会将一个节点的所有数据存储在一个连续的磁盘块中,这种结构利用了磁盘的局部性优势。
- 当一个节点被加载到内存时,其包含的所有键值和指针也会一起加载到内存中。这就意味着每次访问内存时可以获得大量相关数据,提升了缓存命中率。
- 由于每个节点包含的键值较多,整个 B 树的节点访问次数减少,进一步提升了查询效率。
4. 自平衡结构
- B树是自平衡的,每次插入和删除操作都保持树的高度平衡。
- 在查询时,不会出现二叉树中“倾斜”或“不平衡”导致的最坏情况(如链表一样的结构),始终可以在接近对数时间内完成查询操作。
- 这种平衡性使得 B 树在最坏情况下的查询效率也较高,通常保持在 O(logN)O(\log N)O(logN) 的时间复杂度。
5. 适用于批量处理数据
- B树的结构适合批量处理数据。在数据库或文件系统中,一次查询或操作通常涉及到批量的数据访问。B树的节点中可以存放多个数据块,能够有效支持批量查询操作。
- 比如在一个B树节点中可以存放多个索引值和对应的指针,查询时可以快速筛选出相关数据,提高查询效率。
总结
B树的查询速度快,主要归功于它的低树高(减少访问层数)、减少磁盘I/O操作(一次性读取多个键值和指针)、良好的局部性(充分利用缓存和磁盘局部性)以及自平衡结构(避免性能恶化)。这些特性使得 B 树特别适合用于磁盘存储的场景,在数据库索引和文件系统中得到了广泛应用。
覆盖索引
覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。
如果一个索引包含了(或覆盖了)满足查询语句中字段与条件的数据就叫做覆盖索引。
如何实现覆盖索引
常见的方法是:将被查询的字段,建立到联合索引里去。1、如实现:select id,age from user where age = 10;
explain分析:因为age是普通索引,使用到了age索引,通过一次扫描B+树即可查询到相应的结果,这样就实现了覆盖索引
2、实现:select id,age,name from user where age = 10;explain分析:age是普通索引,但name列不在索引树上,所以通过age索引在查询到id和age的值后,需要进行回表再查询name的值。此时的Extra列的NULL表示进行了回表查询

为了实现索引覆盖,需要建组合索引idx_age_name(age,name)
drop index idx_age on user;
create index idx_age_name on user(`age`,`name`);
explain分析:此时字段age和name是组合索引idx_age_name,查询的字段id、age、name的值刚刚都在索引树上,只需扫描一次组合索引B+树即可,这就是实现了索引覆盖,此时的Extra字段为Using index表示使用了索引覆盖。

分页查询
例如:select id,age,name from user order by age limit 100,2;因为name字段不是索引,所以在分页查询需要进行回表查询,此时Extra为Using filesort文件排序,查询性能低下。

使用索引覆盖:建组合索引idx_age_name(age,name)

表结构

150多万的数据,这么一个简单的语句:

慢查询日志里居然很多用了1秒的,Explain的结果是:

从Explain的结果可以看出,查询已经使用了索引,但为什么还这么慢?
分析:首先,该语句ORDER BY 使用了Using filesort文件排序,查询效率低;其次,查询字段不在索引上,没有使用覆盖索引,需要通过索引回表查询;也有数据分布的原因。
知道了原因,那么问题就好解决了。
解决方案:由于只需查询uid字段,添加一个联合索引便可以避免回表和文件排序,利用覆盖索引提升查询速度,同时利用索引完成排序。

覆盖索引:SQL只需要通过索引就可以返回查询所需要的数据,而不必通过二级索引查到主键之后再去查询数据。
我们再Explain看一次:

Extra信息已经有'Using Index',表示已经使用了覆盖索引。经过索引优化之后,线上的查询基本不超过0.001秒。
覆盖索引和联合索引的区别在哪里?
没有区别,覆盖索引只是特定于具体select语录而言的联合索引。也就是说一个联合索引对于某个select语句,通过索引可以直接获取查询结果,而不再需要回表查询啦,就称该联合索引覆盖了这条select语句。
最左前缀匹配原则:在MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。
比如索引abc_index:(a,b,c)是a,b,c三个字段的联合索引,下列sql执行时都无法命中索引abc_index;
select * from table where c = '1';
select * from table where b ='1' and c ='2';
以下三种情况却会走索引:
select * from table where a = '1';
select * from table where a = '1' and b = '2';
select * from table where a = '1' and b = '2' and c='3';
是的,索引abc_index:(a,b,c),只会在where条件中带有(a)、(a,b)、(a,b,c)的三种类型的查询中使用。其实这里说的有一点歧义,其实当where条件只有(a,c),(c,a)时也会走,但是只走a字段索引,不会走c字段。
3.什么字段适合建立索引,什么字段不适合建立索引
一、哪些情况下适合建索引
- 主键自动建立唯一索引
- 频繁作为查询条件的字段应该创建索引。
- 经常与其他表进行连接的表,在连接字段上应该建立索引;
- 经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
- 索引应该建在选择性高(字段范围比较大)的字段上;
- 索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
- 复合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替:
A、正确选择复合索引中的主列字段,一般是选择性较好的字段;
B、复合索引的几个字段是否经常同时以AND方式出现在Where子句中?单字段查询是否极少甚至没有?如果是,则可以建立复合索引;否则考虑单字段索引;
C、如果复合索引中包含的字段经常单独出现在Where子句中,则分解为多个单字段索引;
E、如果既有单字段索引,又有这几个字段上的复合索引,一般可以删除复合索引;
字段选择性
选择性较低索引 可能带来的性能问题
索引选择性=索引列唯一值/表记录数;
选择性越高索引检索价值越高,消耗系统资源越少;选择性越低索引检索价值越低,消耗系统资源越多;
查询条件含有多个字段时,不要在选择性很低字段上创建索引
可通过创建组合索引来增强低字段选择性和避免选择性很低字段创建索引带来副作用;
尽量减少possible_keys,正确索引会提高sql查询速度,过多索引会增加优化器选择索引的代价,不要滥用索引;
二、哪些情况下不适合建索引
- 频繁进行数据操作的表,不要建立太多的索引;
- 频繁更新的字段不适合建立索引
- 表数据可以确定比较少的不需要建索引
- 数据重复且发布比较均匀的的字段不适合建索引(唯一性太差的字段不适合建立索引),例如性别,真假值
- 参与列计算的列不适合建索引
- 删除无用的索引,避免对执行计划造成负面影响;
注意事项:
过多的复合索引,在有单字段索引的情况下,一般都是没有存在价值的;相反,还会降低数据增加删除时的性能,特别是对频繁更新的表来说,负面影响更大。
在对建立索引的时候要对表进行加锁,因此应当注意操作在业务空闲的时候进行。
总的来说,小型表肯定不建索引,
或者数据库记录在亿条数据级以上,还是建议使用非关系型数据库。
还有些特殊字段的数据库,比如BLOB,CLOB字段肯定也不适合建索引。
4.怎么创建多线程,创建多线程有哪几种方法?
1.继承Thread类,重写run方法
2.实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
3.通过Callable和FutureTask创建线程
4.通过线程池创建线程
方法1:自己的类extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
在合适的地方启动线程:new MyThread().start();
方法2:实现一个Runnable接口:
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:
MyThread myt = new MyThread();
Thread t = new Thread(myt);
t.start();
事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:
public void run() {
if (target != null) {
target.run();
}
}
方法3:通过Callable和FutureTask创建线程:
注:1. Runnable和Callable有什么不同?
Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在 JDK1.5增加的。它们的主要区别是Callable的 call() 方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象
Runnable和Callable的区别
(1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
(3) call方法可以抛出异常,run方法不可以。
(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
方法4:通过线程池创建线程
6.多线程有哪几种状态?不同状态之间是怎么进行转换的?
1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程阻塞于锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。
1. 初始状态
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。
2.1. 就绪状态
- 就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
- 调用线程的start()方法,此线程进入就绪状态。
- 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
- 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
- 锁池里的线程拿到对象锁后,进入就绪状态。
2.2. 运行中状态
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
3. 阻塞状态
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
4. 等待
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
5. 超时等待
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
6. 终止状态
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
7. 为什么要用线程池,怎么创建线程池?四种线程池的用法分析
那先要明白什么是线程池
线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。
使用线程池的好处
线程池加快了线程的创建时间,由于线程池中的线程已经准备好,应用程序可以直接拿来使用,
1、new Thread的弊端
执行一个异步任务你还只是如下new Thread吗?
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
}
).start();
那你就out太多了,new Thread的弊端如下:
a. 每次new Thread新建对象性能差。
b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
c. 缺乏更多功能,如定时执行、定期执行、线程中断。
相比new Thread,Java提供的四种线程池的好处在于:
a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。
2、Java 线程池
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
(1)newCachedThreadPool:
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。示例代码如下:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(index * 1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(index);
}
});
}
线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
(2)newFixedThreadPool:
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。示例代码如下:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
(3)newScheduledThreadPool:
创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);
表示延迟3秒执行。
定期执行示例代码如下:
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("delay 1 seconds, and excute every 3 seconds");
}
}, 1, 3, TimeUnit.SECONDS);
表示延迟1秒后每3秒执行一次。
ScheduledExecutorService比Timer更安全,功能更强大
(4)newSingleThreadExecutor:
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
结果依次输出,相当于顺序执行各个任务。
现行大多数GUI程序都是单线程的。Android中单线程可用于数据库操作,文件操作,应用批量安装,应用批量删除等不适合并发但可能IO阻塞性及影响UI线程响应的操作。


本文详细对比了多种核心技术,包括Java集合框架中ArrayList与LinkedList的区别,HashMap、Hashtable与ConcurrentHashMap的特性与适用场景,以及多线程中ThreadLocal的使用与原理。深入分析了数据库事务隔离级别的概念,MySQL的索引类型及其优化策略,Redis的基本数据结构,Java异常处理机制,乐观锁与悲观锁的区别,线程池的创建与使用,动态代理的实现方式,以及数据库查询语句的优化技巧。



1076

被折叠的 条评论
为什么被折叠?



