文章目录
1.Arraylist和Linkedlist的区别
1、底层数据结构:
ArrayList:动态数组
LinkedList:双向链表
2、操作数据效率:
1、查找:
根据下标查找:ArrayList查找的时间复杂度是O(1),可以通过寻址公式(数组的首地址+下标*数组元素类型的大小)找到;LinkList不能通过下标查找
根据数据查找:ArrayList和LinkList的时间复杂度都是O(n),都需要整体遍历一遍
2、增加和删除:
ArrayList:向数组尾部插入或删除一个数时间复杂度是O(1)的,其他位置的时间复杂度都是O(n),因为会挪动数组,为其腾出位置。
LinkedList:在链表的头部或者尾部插入或删除一个数的时间复杂度是O(1)的,其他位置的时间复杂度都是O(n),因为首先回去遍历链表找到要插入或删除的位置。
3、内存空间的占用内存空间的占用
ArrayList:底层是数组,内存是连续的
LinkedList:底层是双向链表,需要存储数据,以及两个指针,更占用内存
4、线程安全
ArrayList和LinkedList都不是线程安全的。
如何才能保证他们的线程安全呢?
- 在方法内使用,作为局部变量是线程安全的
- 使用线程安全的ArrayList和LinkedList
-
List<Object> syncArrayList = Collections.sychronizedList(new ArrayList<>);
-
List<Object> syncLinkedList = Collections.sychronizedList(new LinkedList<>);
-
2.哪些集合类是线程安全的
1.常见的线程安全的集合
Vector:其线程安全的原理是为其所有需要保证线程安全的方法都添加了synchronized关键字,锁住了整个对象,确保了每个操作的原子性
HashTable:与Vector类似,都是为每个方法添加了synchronized关键字,来实现的线程安全,锁住了整个对象
2.使用Colletions包装成的线程安全
- Collections.synchronizedList:
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
- Collections.synchronizedSet:
Set<String> synchronizedSet = Collections.synchronizedSet(new HashSet<>());
- Collections.synchronizedMap:
Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
3、CopyOnWrite集合(快照语义):
- CopyOnWriteArrayList(写时复制数组列表): ArrayList的线程安全变体,在每次写操作时,会先上锁,然后创建数组的副本。
- CopyOnWriteArraySet(写时复制数组集合): 类似于CopyOnWriteArrayList,但用于Set。
4、并发集合(java.util.concurrent包):
- ConcurrentHashMap(并发哈希映射): 高度并发和高效的哈希映射实现。
- ConcurrentSkipListMap(并发跳表映射): 排序映射的并发实现。
- ConcurrentSkipListSet(并发跳表集合): 排序集合的并发实现。
3.Hashmap的扩容机制
- 第一次添加数据初始化数组的长度为16,达到了扩容阈值(0.75*数组的长度)就会进行扩容。
- 扩容时会开一个新的数组,新数组的长度为原数组长度*2。
- 然后开始遍历原数组的每个位置,看它是否有链表或者红黑树(也就是看e.next是否等于null),如果没有就直接使用e.hash&(newCap-1)计算新数组的索引位置。
- 如果有就判断它是红黑树还是链表
- 如果是红黑树就添加红黑树。
- 如果是链表,就需要遍历链表,可能需要拆分链表,判断(e.hash&oldCap)是否为0 ,如果是0就让该元素放在新数组的原位置,如果不是0就让该元素移动到(原数组的位置+新数组大小)的位置上
4.红黑树的时间复杂度(Olog(n))
- 查找:在红黑树中查找一个元素的平均时间复杂度为 O(log n),其中 n 是树中节点的数量。
- 添加:先从根节点开始找到元素添加的位置,时间复杂度也是 O(log n),在插入操作中,可能需要进行一些旋转和颜色变换来维护红黑树的性质。
- 删除:先从根节点开始找到元素添加的位置,时间复杂度也是 O(log n),删除完成后也会涉及到复杂度为O(1)的旋转调整操作。
5.创建线程的方式(runnable和callable的区别)
1、继承Thread类:
- 创建一个类,继承自
Thread
类。 - 重写
run
方法,定义线程执行的代码。 - 创建类的实例并调用
start
方法来启动线程。
class MyThread extends Thread {
public void run() {
// 线程执行的代码
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
2、实现Runnable接口:
- 创建一个类,实现
Runnable
接口。 - 实现
run
方法,定义线程执行的代码 - 创建
Thread
类的实例,将实现了Runnable
接口的类的实例作为参数传递给Thread
的构造函数。 - 调用
start
方法来启动线程。
class MyRunnable implements Runnable {
public void run() {
// 线程执行的代码
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
3、实现Callable接口
- 创建一个类实现Callable接口,实现call方法,然后创建该类的实例
- 使用FutureTask类来包装Callable对象(FutureTask对象封装了Callable对象的call()方法的返回值)
- 使用FutureTask对象作为Thread对象的target创建并启动线程
- 调用Future对象的get()方法来获取线程的执行结果
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("MyCallable...call...");
return "OK";
}
public static void main(String[] args) throws
ExecutionException, InterruptedException {
// 创建MyCallable对象
MyCallable mc = new MyCallable() ;
// 创建F
FutureTask<String> ft = new FutureTask<String>(mc) ;
// 创建Thread对象
Thread t1 = new Thread(ft) ;
Thread t2 = new Thread(ft) ;
// 调用start方法启动线程
t1.start();
// 调用ft的get方法获取执行结果
String result = ft.get();
// 输出
System.out.println(result);
}
}
4、线程池创建线程
- 定义一个类实现runnable接口
- 创建线程池
- 将任务提交给线程池
public class MyExecutors implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable...run...");
}
public static void main(String[] args) {
//创建线程池对象
ExecutorService threadPool =Executors.newFixedThreadPool(3);
threadPool.submit(new MyExecutors()) ;
// 关闭线程池
threadPool.shutdown();
}
Runnable和Callable的区别
- Runnable中的run方法没有返回值,Callable中的call方法有返回值,是个泛型,通过FutureTask.get()就可以获取到
- Runnable中的run方法不能向上抛异常,而Callable中的call方法可以
6.JVM的内存模型
1、方法区(线程共享)
存储的是方法的信息(修饰符、方法名、参数、返回值等)、静态变量、常量池、编译后的代码
2、堆(线程共享)
存储的是实例和数组
当堆中没有内存空间可以分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
- 新生代:被分为三部分,Eden区和两个大小严格相同的Survivor区
- 老年代:主要保存一些生命周期长的对象,一般是一些老的对象
3、虚拟机栈(线程隔离)
存储的是8大基本类型+对象引用+实例方法
- 每个线程运行时所需要的内存,称为虚拟机栈,先进后出
- 每个栈由多个栈帧组成,对应每次方法调用时所占用的内存(栈帧过多会导致栈内存移出StackOverFlowError,比如递归调用)
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
4、本地方法栈(线程隔离)
存储的是本地接口库调用的方法,就是java里面native关键字修饰的方法(java作用范围达不到了,会去调用底层C/C++语言的库)。
5、程序计数器
存储的是正在执行的jvm指令,指向下一条将被执行指令的地址。
7.堆中常用的垃圾回收算法
1、标记清除算法
- 根据可达性分析算法得出垃圾进行标记
- 对这些标记为可回收的内容进行垃圾回收
缺点:
①效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序
②通过标记清除算法清理出来的内存,碎片化比较严重
2、复制算法
- 将原有的内存空间一分为二,每次只用其中一块。
- 当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再将该内存空间清空。
优点:
①在垃圾对象多的情况下,效率较高
②清理后,内存无碎片
缺点:
分配的2块内存空间,在同一时刻,只能使用一半,内存使用率较低
3、标记整理算法
标记整理算法是在标记清除算法的基础之上,做了优化改进的算法。
- 标记垃圾
- 将存活的对象向内存另一端移动
- 清除边界以外的垃圾
优缺点对比:
- 同标记清除算法想比:解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响
4、分代收集算法
其基本思想就是根据对象的生命周期将堆内存分为不同的代(新生代,老年代),然后针对每个代采用不同的垃圾回收算法。
8.项目中常用的垃圾回收器有哪些
- 串行垃圾收集器
- Serial串行垃圾收集器:作用于新生代,采用复制算法
- Serial Old串行垃圾收集器:作用于老年代,采用标记-整理算法
- 垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收完成。
- 并行垃圾收集器(JDK8默认使用此垃圾回收器)
- Parallel New:作用于新生代,采用复制算法
- Parallel Old:作用于老年代,采用标记-整理算法
垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
- CMS(并发)垃圾收集器( Concurrent Mark Sweep)
- 是以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好;采用Mark Sweep算法
- G1垃圾收集器
- 应用于新生代和老年代,在JDK9之后默认使用G1
- 划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
- 采用复制算法
- 分成三个阶段:新生代回收、并发标记、混合收集
- 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
9.@autowired和@resource的区别
1、来源不同
- @Autowire是Spring定义的注解
- @Resource是Java定义的注解
2、自动装配方式不同
- @Autowire:是按照类型(byType)进行自动装配的,如果存在多个Bean再根据名称(byName)进行查找。
- @Resource:先根据名称(byName)进行自动装配,如果按名称查找不到,再根据类型(byType)进行查找。
3、支持的注入类型
- @Autowire:支持构造方法、setter方法和属性注入。
-
属性注入
-
@RestController public class UserController { // 属性注入 @Autowired private UserService userService; @RequestMapping("/add") public UserInfo add(String username, String password) { return userService.add(username, password); } }
-
构造方法注入
-
@RestController public class UserController { // 构造方法注入 private UserService userService; @Autowired public UserController(UserService userService) { this.userService = userService; } @RequestMapping("/add") public UserInfo add(String username, String password) { return userService.add(username, password); } }
-
Setter 注入
-
@RestController public class UserController { // Setter 注入 private UserService userService; @Autowired public void setUserService(UserService userService) { this.userService = userService; } @RequestMapping("/add") public UserInfo add(String username, String password) { return userService.add(username, password); } }
-
@Resource:主要用于setter方法和属性注入。不支持构造方法注入。
4、可选属性
@Autowired:有一个可选的‘required’属性,默认为‘true’,表示被注入的Bean必须存在。如果设为‘false’,允许为‘null’
@Resource:没有类似的’required’,有七个参数
10.对IOC的理解
IOC就是控制反转,将手动创建对象的控制权交给spring容器进行管理(比如@component,@service,@repositoryd等等注解下的类交给spring容器管理)
11.MySQL的隔离级别
要解决这个问题,就要先了解一下并发事务带来哪些问题?
- 脏读:一个事务读到了另一个事务未提交的数据。
- 不可重复读:对同一条记录前后两次读取的结果数据不一致。(针对update操作的)。
- 幻读:一个事务读取两次,得到的记录条数不同。(针对insert和delete操作的)。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读(Read uncommitted) | √ | √ | √ |
读已提交(Read committed) | × | √ | √ |
可重复读(Repeatable Read)(Mysql默认) | × | × | √ |
串行化(Serializable) | × | × | × |
12.事务的隔离性是怎么保证的(MVCC和排它锁)
MVCC
Mysql中的多版本并发控制,指维护一个数据的多个版本,使得读写没有冲突
MVCC底层实现的三个部分:
- 隐藏字段
① trx_id(事务id):记录每一次操作的事务id,是自增的
② roll_pointer(回滚指针),指向上一个事务版本记录的地址
- Undo log
① 回滚日志,存储老版本数据
② 版本链:通过roll_pointer指针形成一个链表,多个事务并行操作某一行记录,记录不同事务修改数据的版本
- readView(解决的是一个事务查询选择版本的问题)
①根据readView的匹配原则和当前的一些事务id判断该访问哪个版本的数据
②不同的隔离级别快照读是不一样的,最终访问结果也不一样
RC:每一次执行快照读时都生成ReadView
RR:仅在事务第一次执行快照读时生成ReadView,后续复用
13.MySQL的索引类型
- 唯一索引:确保列中的所有值都是唯一的,但允许存在一些null值。
CREATE TABLE example (
id INT,
email VARCHAR(50) UNIQUE,
name VARCHAR(50)
);
- 主键索引:当我们给一个字段设置主键时,它就会自动创建主键索引,用来确保每一个值都是唯一的。
CREATE TABLE example (
id INT PRIMARY KEY,
name VARCHAR(50)
);
- 组合索引:指包含多个列的索引,用于加速多列的联合查询
CREATE TABLE example (
id INT,
name VARCHAR(50),
age INT,
INDEX idx_name_age (name, age)
);
- 普通索引:是最基本的索引类型,用于加速数据的检索。与唯一索引不同,普通索引允许有重复的值。
CREATE TABLE example (
id INT,
name VARCHAR(50),
INDEX idx_name (name)
);
- 全文索引:用于在需要对文本内容进行关键字搜索的场景
CREATE TABLE example (
id INT,
content TEXT,
FULLTEXT idx_content (content)
);
14、递归调用的实操
题目如下:
public class DepartmentTest {
public static void main(String[] args) {
List<Department> allDepartment = new ArrayList<>();
Department dep1 = new Department(1, 0, "北京总部");
Department dep3 = new Department(3, 1, "研发中心");
Department dep4 = new Department(4, 3, "后端研发组");
Department dep6 = new Department(6, 4, "后端实习生组");
Department dep7 = new Department(7, 3, "前端研发组");
Department dep8 = new Department(8, 1, "产品部");
allDepartment.add(dep6);
allDepartment.add(dep7);
allDepartment.add(dep8);
allDepartment.add(dep1);
allDepartment.add(dep3);
allDepartment.add(dep4);
List<Department> subDepartments = DepartmentTest.getSub(3, allDepartment);
for (Department subDepartment : subDepartments) {
System.out.println(subDepartment);
}
}
/**
* 根据id,获取所有子部门列表(包括隔代子部门,一直到叶子节点)
* 要求:不能新增参数,不能增加static变量
* @param id
* @return
*/
public static List<Department> getSub(int id, List<Department> allDepartment) {
return null;
}
}
class Department {
/** id */
private int id;
/** parent id */
private int pid;
/** 名称 */
private String name;
public Department(int id, int pid, String name) {
this.id = id;
this.pid = pid;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Department{" +
"id=" + id +
", pid=" + pid +
", name='" + name + '\'' +
'}';
}
}
当时是这样写的
public static List<Department> getSub(int id, List<Department> allDepartment) {
List<Department> departments=new ArrayList<Department>();
for(Department department:allDepartment){
if(department.getPid()==id){
departments.add(department);
List<Department> sub = getSub(department.getId(), allDepartment);
departments.addAll(sub);
}
}
return departments;
}
", pid=" + pid +
", name='" + name + '\'' +
'}';
}
}
当时是这样写的
public static List<Department> getSub(int id, List<Department> allDepartment) {
List<Department> departments=new ArrayList<Department>();
for(Department department:allDepartment){
if(department.getPid()==id){
departments.add(department);
List<Department> sub = getSub(department.getId(), allDepartment);
departments.addAll(sub);
}
}
return departments;
}