Day1
接口和抽象类的区别
接口 | 抽象类 | |
---|---|---|
方法 | 抽象方法 | 既可以有抽象方法,也可以有普通方法 |
关键字修饰 | interface | abstract class |
定义常量变量 | 只能定义静态常量 | 成员变量 |
子类方法 | 所有方法必须实现 | 实现所有的抽象方法 |
子类继承 | 多继承 | 单继承 |
构造方法 | 不能有构造方法 | 可以有构造方法 |
接口实现 | 只能继承接口,不能实现接口 | 可以实现接口,并且不实现接口中的方法 |
- 静态常量:
- 静态常量const定义期间必须初始化,并且之后不能进行数据修改,即静态常量不能修改
- 在编译期间进行初始化
- 静态常量不能通过构造函数进行修改,只能通过初始化器
- 成员变量:
- 成员变量定义在类中,在整个类中都可以被访问
- 成员变量随着对象的建立而建立,随着对象的消失而消失,存在于对象所在的堆内存中
- 成员变量有默认初始化值
- 构造方法:
- 对象创建时要执行的方法
Java 中的继承和 C++ 有什么不同
C++ 中可以实现多继承,而 Java 只能实现单继承。
Java 中有哪些数据结构?用过 HashMap 吗,说一下 HashMap 底层实现。
数据结构有数组、链表、栈、队列、堆、树、集合、映射等。其中HashMap 是一种常用的映射数据结构。
HashMap在JDK1.7之前采用数组加链表的形式实现,在1.8之后使用了红黑树:通过底层数组存储对象节点,采用链地址法,把新增对象节点连接在当前地址的节点下面,且规定当链表长度大于8时,将链表转换成红黑树结构。
HashMap 是基于哈希表实现的,它使用了数组和链表(或红黑树)的结合来实现键值对的存储。HashMap 的底层数据结构主要包括一个数组和若干个链表(或红黑树)。在 HashMap 中,每个键值对会被映射到数组中的一个位置,这个位置就是通过哈希函数计算得到的索引。如果发生哈希冲突(即不同的键映射到了同一个位置),则会使用链表(或红黑树)来存储冲突的键值对。
当往 HashMap 中放入键值对时,首先会根据键的哈希值找到对应的数组位置,然后将键值对插入到链表(或红黑树)的头部。如果链表长度超过阈值(8),(先判断table的长度是否大于64,如果小于64,就通过扩容的方式来解决,避免红黑树结构化)链表会转换为红黑树,以提高查询效率。在进行查找操作时,同样会根据键的哈希值找到对应的数组位置,然后在链表(或红黑树)中顺序查找或通过红黑树的方式查找对应的值。
Java 中用的是值传递还是引用传递?
在 Java 中,方法调用时传递的是变量的值,而不是变量本身。这意味着 Java 使用的是值传递。在 Java 中,对于基本数据类型,传递的是值本身;对于对象类型,传递的是对象的引用值。因此,有时候会有人说 Java 使用的是"引用传递",但更准确的说法是 Java 使用的是"值传递",只不过对于对象类型的值,传递的是引用值。
面向过程和面向对象有什么区别?
面向过程是一种以过程为中心的编程思想,在处理某件事的时候,以正在进行什么为主要目标,分步骤完成目标。而面向对象的思想是将事物抽象为对象,赋予其属性和方法,通过每个对象执行自己的方法来完成目标。面向过程效率更高,而面向对象耦合低(易复用),扩展性强,易维护。
final、finally、finalize 的区别?
- final 用于修饰属性、类、方法,修饰的变量一旦赋值后,不能被重新赋值。被 final 修饰的实例变量必须显式指定初始值。被final修饰的类不能被继承,方法不能被重写,变量不能改变。
- finally 用于异常处理,只能⽤在 try/catch 中,finally 后的代码总会被执行。
- finalize 是 java.lang.Object 类中的⽅法,每⼀个对象都会继承这个方法,在垃圾回收机制执行的时候会被调用,允许回收未被使用的内存垃圾。
什么是序列化?什么是反序列化?
序列化其实就是将对象转化成字节序列格式,使其可储存可传输。
反序列化就是将字节序列格式转换成对象,是序列化的补集。
什么是不可变类
不可变类是指实例的属性不能被修改的类。一个不可变类的实例对象从被创建出来,它的成员变量就不能被修改。Java 平台的类库中包含许多的不可变类,比如 String、基本类型的包装类等。不可变类比一般的更加安全。
为什么 Java 中 String 是不可变类?
- String 类中包含 char 数组 value、整形的 offset 和 count 三个属性,这三个属性都是 private 的,且没有提供方法修改数值,因此在初始化后无法从外部改变。
- String 类中的这三个属性都是被 final 修饰的,无法从内部进行改变;
- 方法区有一块特殊存储区域 String Pool,当创建 String 时,如果在 String Pool 中找到相同的字符串值,则会返回一个已存在 String 的引用而不会新建一个对象。假设 String 是可变的,则会导致其他引用这个字符串值的 String 的值发生变化。
API 和 SPI 的区别
API 和 SPI 都是制定接口传输数据。API 是由实现方负责定义和实现,调用方只负责调用的 API。而 SPI 是指由调用方制定的接口,这个接口由实现方针对接口进行不同的实现,再由调用方选择实现方。例如在 JDBC 连接数据库时,针对不同的数据库需要不同的驱动实现,JDBC 提供了驱动接口,由不同的实现方进行实现了这些不同的驱动,然后我们就可以在 JDK 中引入这些实现了的驱动包进行使用。
API(Application Programming Interface)
大多数情况下,都是实现方来制定接口并完成对接口的不同实现,调用方仅仅依赖并直接进行使用却无权选择不同实现。
SPI(Service Provider Interface)
SPI 是JDK 提供的一种服务提供发现机制, 调用方来制定接口,实现方来针对接口来进行不同的实现。调用方来选择自己需要的实现方。
Day2
线程和进程的区别
进程 | 线程 | |
---|---|---|
定义 | 操作系统进行资源分配和调度的基本单位 | 操作系统能够进行运算调度的最小单位 |
从属关系 | 运行程序的实例 | 进程的一个执行流 |
资源共享 | 进程间不能进行资源共享 | 线程间可以共享资源 |
上下文切换 | 切换速度慢 | 切换速度快 |
操纵方 | 操作系统 | 编程人员 |
线程和协程的区别?什么场景下用到协程?
线程 | 协程 | |
---|---|---|
定义 | 操作系统最小的执行单元 | 操作系统最小的资源管理单元 |
从属关系 | 一个进程可以有多个线程 | 一个线程可以有多个协程 |
同步异步 | 同步机制 | 异步机制 |
资源消耗 | MB级,更大 | KB级,更小 |
- 高并发服务,如秒杀系统、RPC 服务器等。
- 爬虫开发。
- 即时通信服务,如聊天室、游戏服务器等等。
线程和协程是并发编程中两种不同的机制,它们有一些区别。
- 调度方式:线程由操作系统内核进行调度,它们是操作系统的原生概念,执行的调度和切换由操作系统决定。而协程是在用户空间进行调度,程序员可以显式地控制协程的切换,而不需要依赖操作系统的调度。
- 并发性:线程是并发执行的,一个程序可以同时有多个线程在运行,每个线程都有自己的执行流。协程通常是单线程的,一个协程执行时可以暂停,切换到另一个协程执行,这样实现了并发。
- 资源消耗:线程需要较多的资源,如栈空间和线程上下文切换的开销较大。而协程通常只需要较小的栈空间,并且切换开销较低,因为协程的切换是由程序员控制的,不需要切换到操作系统内核态。
- 阻塞和同步:在传统的线程模型中,当一个线程遇到阻塞操作(如IO操作)时,会被操作系统调度到等待状态,从而允许其他线程运行。而协程可以通过显式的挂起和恢复操作来实现非阻塞的同步,避免了线程切换和阻塞等待。
在一些特定的场景下,协程可以提供更高效的并发编程解决方案:
- 高并发IO密集型应用:当应用程序需要同时处理大量IO操作(如网络请求、数据库查询等)时,使用协程可以避免线程切换的开销,提高并发处理能力。
- 异步编程:协程可以使异步代码的编写更加简洁和可读。通过使用协程库或语言提供的异步/await等关键字,可以编写顺序化、可读性高的异步代码,而无需显式地处理回调函数和线程切换。
- 有限状态机:协程可以用于实现有限状态机(FSM)的编程模型。通过使用协程来表示状态和状态转换,可以简化复杂的状态逻辑和事件处理。
需要注意的是,协程并非在所有情况下都是最佳选择。在某些情况下,如CPU密集型任务或需要与底层系统紧密交互的情况下,线程模型可能更为适合。在选择使用协程时,需要根据具体的应用场景和需求进行评估和权衡。
怎么理解容器的线程安全与线程不安全?里面具体做了什么样的实现?
- 线程安全
在拥有共享数据的多条线程并行执行的程序中,代码可以通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据错误等意外情况,称为线程安全。 - 线程不安全
不提供机制保护,出现多个线程先后更改数据造成得到脏数据的可能。 - 线程安全里面具体做了什么样的实现
- 互斥同步
通过 synchronized 关键字编译后,在同步块的前后生产 monitorenter 和 monitorexit 两个字节码指令,需要明确的 reference 对象进行解锁。获取 reference 对象后当前线程可以操作代码块,当锁的持有数归 0 锁释放后,可以在被其他线程获取。 - 非阻塞同步
对互斥同步的优化,对于获取锁失败的线程,将不再让其挂起,而是自旋等待一段时间,若还是无法获取锁才将其挂起。- 无同步方案
- 可重入代码
不允许任何进程进行修改,在运行的任何时刻中断去执行其他代码,在合理范围内(多次重入等)继续执行。 - 线程本地储存
每个线程都有一个副本,对线程所做的修改都是对副本修改,不会对其他副本造成影响。
- 可重入代码
- 无同步方案
- 互斥同步
Java 如何实现线程安全
- 使用 synchronized 关键字:
在关键代码段使用 synchronized 关键字可以确保同一时间只有一个线程可以访问该代码块,从而避免多个线程同时访问造成的数据竞争问题。
public synchronized void synchronizedMethod() {
// 线程安全的代码块
}
- 使用 AtomicInteger 和 AtomicReference 等原子类:
Java 中提供了一系列原子类,可以实现针对基本数据类型和对象引用的原子操作,避免了线程安全问题。
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
- 使用 ThreadLocal:
ThreadLocal 可以实现每个线程都拥有自己的变量副本,从而避免多个线程之间的数据共享问题。
private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void increment() {
int value = threadLocal.get();
threadLocal.set(value + 1);
}
并发类库提供的线程池实现有哪些?
- newCachedThreadPool()
- newFixedThreadPool(int nThreads)
- newSingleThreadExecutor()
- newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize)
- newWorkStealingPool(int parallelism)