1.自我介绍
2.Java多线程实现方式以及区别
实现Runnable接口,继承Thread类,实现Callable接口
//继承Thread类
class MyThread extends Thread{
@Override
public void run(){
System.out.println("Thread running:"+Thread.currentThread().getname());
}
}
//使用方式
MyThread thread=new MyThread();
thread.start();
/*
直接继承Thread类,重写run()方法
使用简单直观
每个线程都是独立的Thread对象
*/
//实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("Runnable running:"+Thread.currentThread().getName());
}
}
//使用方式
Thread thread=new Thread(new MyRunnable());
thread.start();
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<String>{
@Override
public String call() throws Exception{
return "Callable result:"+Thread.currentThread().getName();
}
}
// 使用方式
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
String result = futureTask.get(); // 获取返回值
特性 | Thread类 | Runnable接口 | Callable接口 |
---|---|---|---|
返回值 | 无返回值 | 无返回值 | 有返回值(Future) |
异常处理 | 不能抛出受检异常 | 不能抛出受检异常 | 可以抛出异常 |
继承关系 | 单继承限制 | 可多实现接口 | 可多实现接口 |
资源共享 | 每个线程独立对象 | 可共享同一个Runnable实例 | 可共享同一个Callable实例 |
使用场景 | 简单线程任务 | 推荐使用,更灵活 | 需要返回结果的异步任务 |
- 推荐使用Runnable接口:避免单继承限制,更符合面向接口编程原则
- 资源共享优势:多个线程可以共享同一个Runnable实例,节省资源
- Callable的特殊性:需要返回值或异常处理时使用,配合线程池更高效
- 实际开发建议:通常使用线程池+Callable/Runnable,而不是直接new Thread()
最佳实践:优先选择实现Runnable接口,需要返回值时使用Callable接口,尽量避免直接继承Thread类。
面试官问:请说一下实现多线程的三种方法及其区别?
可以这样回答:
"Java中实现多线程主要有三种方式:
第一种是继承Thread类,通过重写run()方法来实现线程逻辑。这种方式简单直接,但由于Java是单继承,会占用继承名额,不够灵活。
第二种是实现Runnable接口,这也是最推荐的方式。它避免了单继承的限制,多个线程可以共享同一个Runnable实例,更符合面向接口编程的原则,资源利用率更高。
第三种是实现Callable接口,它与Runnable的主要区别在于:Callable可以有返回值,通过Future对象获取执行结果;还可以抛出受检异常,更适合需要处理返回值和异常的场景。
在实际开发中,我们通常优先选择实现Runnable接口,需要返回值时使用Callable接口,配合线程池来管理线程,而不是直接new Thread()来创建线程,这样可以更好地控制线程资源和提高性能。"
3.说一下HashMap底层实现
1.8版本之后核心数据结构是数组+链表/红黑树
- 数组:称为哈希桶(Hash Bucket),存储链表或红黑树的头节点
- 链表:解决哈希冲突,当链表长度>=8时转换为红黑树
- 红黑树:提高查询效率,当节点数<6时退化为链表
为什么使用数组:1.快速随机访问 2.内存连续,访问效率高
链表在HashMap中的作用:1.解决哈希冲突 2.链式存储,动态扩容
为什么需要红黑树(自平衡的二叉查找树,能够保证最坏情况下操作时间复杂度为O(log n)):1.链表过长时查询效率低(O(n))2.红黑树保证查询效率(O(log n))
关键参数:
初始容量:默认16,必须是2的幂次
负载因子:默认0.75,控制扩容时机
扩容阈值:容量 x 负载因子,达到时触发扩容
工作原理:
1.哈希计算
// 计算key的哈希值,让高位也参与运算,减少哈希冲突
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2.确定数组下标
// 通过 (n-1) & hash 确定在数组中的位置
int index = (table.length - 1) & hash;
3.插入逻辑
- 如果该位置为空,直接插入新节点
- 如果已有节点,遍历链表/红黑树:
- 找到相同key:更新value
- 未找到相同key:添加到链表末尾或红黑树
4.扩容机制(resize)
- 创建新数组(原容量×2)
- 重新计算所有节点的位置
- 节点在新数组中的位置:要么在原位置,要么在原位置+原容量
线程安全问题
HashMap是非线程安全的,多线程环境下可能产生:
- 死循环(JDK1.7及之前)
- 数据丢失
- 使用ConcurrentHashMap或Collections.synchronizedMap保证线程安全
面试官问:请说一下HashMap的底层实现原理?
可以这样回答:
"HashMap在JDK1.8中采用数组+链表+红黑树的结构。数组称为哈希桶,每个桶可以存储链表或红黑树。
当插入元素时,首先通过key的hashCode计算哈希值,再通过(n-1)&hash确定在数组中的位置。如果该位置为空,直接插入;如果已有元素,则遍历链表比较key,相同则更新value,不同则添加到链表末尾。
当链表长度达到8且数组长度≥64时,链表会转换为红黑树以提高查询效率;当节点数减少到6时,红黑树会退化为链表。
HashMap有负载因子(默认0.75)和扩容机制。当元素数量达到容量×负载因子时,会进行扩容,新容量为原容量的2倍,并重新计算所有元素的位置。
需要注意的是,HashMap是非线程安全的,多线程环境下需要使用ConcurrentHashMap或同步包装类来保证线程安全。"
4.数组与链表的区别
面试官问:请说一下数组和链表的区别?
可以这样回答:
"数组和链表是两种基本的数据结构,主要区别在于:
- 内存存储:数组需要连续的内存空间,而链表的节点可以分散存储
- 访问效率:数组支持随机访问,时间复杂度O(1);链表需要顺序访问,时间复杂度O(n)
- 插入删除:数组插入删除需要移动元素,时间复杂度O(n);链表只需要修改指针,时间复杂度O(1)
- 内存管理:数组大小固定,可能浪费空间;链表动态分配,更灵活
选择建议:
- 如果需要频繁随机访问,选择数组
- 如果需要频繁插入删除,选择链表
- 在实际开发中,ArrayList基于数组,LinkedList基于链表"