ook

基础

1. 重载和重写的区别

  • 重载 overload:方法名相同,返回值和形参,访问修饰符可能不同,发生在一个类中;编译时发生

  • 重写 override:发生在继承中,(覆盖),方法名和形参都相同,修饰符大于等于父类,子类不能重写父类的private方法,

    2. string stringbuffer stringbuilder

  1. string用final修饰,底层用byte[],java9之前用的char[],节省字符串占用的内存
  2. stringbuffer对原对象操作,线程安全,用了sychronized修饰?
  3. stringbuilder线程不安全,单线程使用这个

    3. 接口interface和抽象类abstract class的区别

  4. 抽象类只能继承一个,接口可以有多个实现
  5. 抽象类中可以有普通成员函数(及实现),包括构造方法,接口只能有public的abstract方法
  6. 抽象类中可以有普通成员变量, 接口只能有public static final类型成员
    abstract class:只能继承一个
    interface
  7. 抽象类用于代码复用,比如抽象工厂,接口用于对类的行为进行约束,关注某些操作时用接口

4. hashcode和equals

  • hashcode是获取对象的hash码,作用是确定在哈希表的位置,java任何类都有hashcode()函数,在Object父类里

    5. ArrayList和LinkedList

  • ArrayList基于动态数组,连续内存存储,动态用扩容实现,类似于C++的vector和Java、python的slice
  • LinkedList基于链表实现,存储分散的内存,适合插入删除,不适合查询,根据下标get(i)需要遍历

    6. HashMap和HashTable

  • HashMap并发不安全,HashTable用sychronized修饰,并发安全
  • 数组+链表实现的,

    7. jdk7.concurrentHashMap原理

  1. 数据结构使用ReentrantLock + Segment + HashEntry ,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表
  2. 元素查询:使用二次hash,第一次找到segment,第二次定位到元素所在的头部
  3. 锁使用了Segment分段锁,Segment继承ReentrantLock,其他Segment不受影响,数组扩容不影响其他Segment

    8. jdk8.concurrentHashMap原理

  4. 数据结构使用sychronized + CAS +红黑树,Node的next和val都用volatile修饰,查找替换赋值使用CAS
  5. 元素查询:使用CAS查找【】
  6. 锁:锁了head结点,其他元素的读写不受影响,读操作无锁

    9. IOC容器和AOP是什么

  7. Inversion of Control 控制反转,就是把对象的创建管理的权利反转给外部的环境
  8. Aspect Oriented Programming 面向切片编程,将日志、权限、接口等关注点从核心业务分离出来,通过动态代理等技术。各种注解就是以AOP的思想和机制实现的

    10. Spring的AOP如何实现的【动态代理】

  9. Spring AOP:如果需要代理的对象实现了某个接口,SpringAOP使用JDKProxy创建代理对象,如果存在没有实现接口的对象,使用cglib生成一个被代理对象的子类作为代理
  10. Aspect J:切面多的情况下使用,性能有优势

    11.java程序运行的流程

  11. 源码 .java
  12. 编译器
  13. 字节码 .class
  14. jvm解释器
  15. 机器的二进制码
  16. 运行

    12. equals 和 == 的区别

  17. ==比较基础类型,和引用类型的地址是否相同
  18. equals比较两个对象是否相同
  19. Integer 与int的比较,以右边为基础,使用 Integer == int 发生拆箱;使用int == Integer发生装箱
  20. Integer.equals(int)发生装箱,再比较内容

    13. final finally finalize的区别

  21. final修饰类(不可继承)、方法(不可重写)、变量(不可修改)
  22. finally修饰代码块,常用于释放资源、关闭连接等
  23. finalize用于垃圾回收,已经被废弃

    14. BIO NIO AIO

  24. 阻塞与非阻塞:
    • BIO是阻塞式I/O模型,线程会一直被阻塞等待操作完成。
    • NIO是非阻塞式I/O模型,线程可以去做其他任务,当I/O操作完成时得到通知。
    • AIO也是非阻塞式I/O模型,不需要用户线程关注I/O事件,由操作系统通过回调机制处理。
  25. 缓冲区:
    • BIO使用传统的字节流和字符流,需要为输入输出流分别创建缓冲区。
    • NIO引入了基于通道和缓冲区的I/O方式,使用一个缓冲区完成数据读写操作。
    • AIO则不需要缓冲区,使用异步回调方式进行操作。
  26. 线程模型:
    • BIO采用一个线程处理一个请求方式,面对高并发时线程数量急剧增加,容易导致系统崩溃。
    • NIO采用多路复用器来监听多个客户端请求,使用一个线程处理,减少线程数量,提高系统性能。
    • AIO依靠操作系统完成I/O操作,不需要额外的线程池或多路复用器。

      Java 中 3 种常见 IO 模型

BIO (Blocking I/O)

BIO 属于同步阻塞 IO 模型 。

同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到在内核把数据拷贝到用户空间。

img

在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

NIO (Non-blocking/New I/O)

Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。

Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。

跟着我的思路往下看看,相信你会得到答案!

我们先来看看 同步非阻塞 IO 模型

img
img

同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。

相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。

但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的

这个时候,I/O 多路复用模型 就上场了。

img
img

IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间->用户空间)还是阻塞的。

目前支持 IO 多路复用的系统调用,有 select,epoll 等等。select 系统调用,是目前几乎在所有的操作系统上都有支持

  • select 调用 :内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。
  • epoll 调用 :linux 2.6 内核,属于 select 调用的增强版本,优化了 IO 的执行效率。

IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。

Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。

AIO (Asynchronous I/O)

AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。

最后,来一张图,简单总结一下 Java 中的 BIO、NIO、AIO。

15. 反射用途和实现原理

反射是通过运行时检查类信息完成的

1
2
3
Class<?> cls = ClassName.class;
Class<?> cls = obj.getClass();
Class<?> cls = Class.forName("ClassName");

原理:反射API实现,Class,Method,Field,Constructor

Class.forName和ClassLoader的区别

  • Class.forName是一个静态方法,通过提供类的完全限定名,在运行时加载类。此方法还会执行类的静态初始化块。如果类名不存在或无法访问,将抛出ClassNotFoundException异常。
  • ClassLoader是一个抽象类,类加载器,负责将类文件加载到Java虚拟机中。ClassLoader可以动态加载类,从不同来源加载类文件,如本地文件系统、网络等。

16. 可重入锁是什么?解释ReentrantLock和sychronized的区别

可重入锁是允许重新获取机制的锁。就像拿钥匙开锁一样,你可以反复用同一把钥匙开锁。这种锁在同一线程内是安全的,因为它可以被同一线程多次获取,而不会产生不一致的状态。
举个例子,假设有一个线程A在执行一个方法,同时这个方法内部又调用另一个方法,那么线程A可以重复获取同一个锁,而不会出现死锁的情况。因为同一线程可以多次获取同一个锁,所以这种锁机制避免了死锁的发生。
但是需要注意,在使用可重入锁时,必须保证在释放锁之前已经获取了该锁,否则会导致死锁。同时还需要保证在获取锁的时候没有嵌套地获取其他锁,否则也会导致死锁。另外,还必须保证在获取锁的时候没有阻塞其他线程,否则同样会导致死锁。
总之,可重入锁是一种安全的锁机制,可以避免死锁的发生。但是在使用时需要注意以上几点,以确保程序的正确性和安全性。

  • 实现上:synchronized 是一个关键字,是在JVM层面通过监视器实现的,而 ReentrantLock 是基于AQS(AbstractQueuedSynchronizer)实现的。
  • 用法上:reentrantlock修饰代码块,synchronized修饰方法,静态方法和代码块
  • 显隐式:Synchronized 是隐式锁,进入synchronized代码块之后自动加锁,离开后自动释放锁;ReentrantLock显示定义,然后手动用lockunlock
  • 中断响应:sync不能直接响应终端,reentrantlock可以响应中断,避免死锁

    17. java序列化讲一下

    指将Java对象转换为字节流的过程,可以将这些字节流保存到文件中或通过网络传输。使用implements Serializable接口

    18. notify()和 notifyall()的区别

  • notify方法用于唤醒在当前对象上等待的单个线程,具体是哪个线程被唤醒是不确定的,取决于线程调度器的实现
  • notifyall 用于唤醒在当前对象上等待的所有线程。
    • 如果有多个线程在某个对象上等待,调用notifyAll()方法后,所有等待的线程都会被唤醒并竞争该对象的锁。其中一个线程获得锁后继续执行,其他线程则继续等待。

      19. 静态内部类和非静态内部类

  1. 实例化方式:静态内部类可以直接通过外部类名来实例化,而非静态内部类必须要通过外部类的实例来实例化。
  2. 对外部类的引用:静态内部类不持有对外部类实例的引用,而非静态内部类则会持有对外部类实例的引用。这意味着在静态内部类中不能直接访问外部类的非静态成员(方法或字段),而非静态内部类可以。
  3. 生命周期:静态内部类的生命周期与外部类相互独立,即使外部类实例被销毁,静态内部类仍然存在。非静态内部类的生命周期与外部类实例绑定,只有在外部类实例存在时才能创建非静态内部类的实例。
  4. 访问权限:静态内部类对外部类的访问权限与其他类一样,根据访问修饰符而定。非静态内部类可以访问外部类的所有成员,包括私有成员

    20. 自定义注解的场景和实现

  5. 扩展框架
  6. 运行时检查,单元测试,配和写Log等
  7. 规范约束

    实现方式

  8. 使用@interface关键字定义注解。
  9. 可在注解中定义属性,并指定默认值。
  10. 根据需求,可添加元注解来控制注解的使用方式。
  11. 在代码中使用自定义注解。
  12. 使用反射机制解析注解信息。

    21. java的构造器能否被重写 override【不可以,只能在子类super父类进行增量更改】

22. java实现对象克隆【深拷贝浅拷贝】

  1. 浅拷贝:通过创建一个新对象,并将原对象的非静态字段值复制给新对象实现。新对象和原对象共享引用数据。在Java中,可以使用clone()方法实现浅拷贝。要实现一个类的克隆操作,需要满足以下条件
    • 实现Cloneable接口。
    • 重写Object类的clone()方法,声明为public访问权限。
    • 在clone()方法中调用super.clone(),并处理引用类型字段。
  2. 深拷贝:通过创建一个新对象,并将原对象的所有字段值复制给新对象,包括引用类型数据。新对象和原对象拥有独立的引用数据。实现深拷贝有以下方式:
    • 使用序列化和反序列化实现深拷贝,要求对象及其引用类型字段实现Serializable接口。
    • 自定义拷贝方法,递归拷贝引用类型字段。

      23. java中常见的运行时异常

  3. 空指针异常:当应用程序尝试使用 null 对象时抛出。
  4. 数组越界异常:当应用程序尝试访问数组元素的时候,数组下标超出了数组的范围。
  5. 类转换异常:当应用程序尝试将一个对象强制转换为不是其实例的子类时抛出。
  6. 非法参数异):当应用程序传递了一个无效或不合法的参数时抛出。
  7. 非法状态异常:当应用程序调用了一个不合适的方法或处于不正确的状态时抛出

    24. synchronized的实现原理是什么

    通过互斥锁来控制线程对共享变量的访问。
  8. synchronized的实现基础是对象内部的锁(也称为监视器锁或管程),每个锁关联着一个对象实例。
  9. 当synchronized作用于某个对象时,它就会尝试获取这个对象的锁,如果锁没有被其他线程占用,则当前线程获取到锁,并可以执行同步代码块;如果锁已经被其他线程占用,那么当前线程就会阻塞在同步块之外,直到获取到锁才能进入同步块。
  10. synchronized还支持作用于类上,此时它锁住的是整个类,而不是类的某个实例。在这种情况下,由于只有一个锁存在,所以所有使用该类的线程都需要等待锁的释放。
  11. 在JVM内部,每个Java对象都有头信息,其中包含了对象的一些元信息和状态标志。synchronized通过修改头信息的状态标志来实现锁的获取和释放。
  12. synchronized还支持可重入性,即在同一个线程中可以多次获取同一个锁,这样可以避免死锁问题
  13. Java虚拟机会通过锁升级的方式来提升synchronized的效率,比如偏向锁、轻量级锁和重量级锁等机制,使得在竞争不激烈的情况下,synchronized的性能可以达到与非同步代码相当的水平。

    25. ThreadLocal和场景和原理

  14. 为每个线程创建独立的变量副本,避免竞争状态,代码层面的体验是一定程度上简化了多线程设计
  15. 原理是每个线程都有自己的threadlocalmap,ThreadLocal 对象充当键,线程的变量副本作为对应键的值,set get进行资源设置和获取

    注意避坑【内存泄漏,线程安全性,数据隔离】

    应用场景【线程池,数据库连接管理,传递上下文信息】

    如何防止内存泄漏

  • 内存泄漏是由于 ThreadLocalMap 中的 Entry 没有被及时清理导致的
  1. 使用完 ThreadLocal 后及时调用 remove() 方法
  2. 使用 try-with-resources 或 try-finally 块,在finally释放资源
  3. 使用InheritableThreadLocal

    26. BigDecimal避坑

  4. 使用浮点数初始化时,使用valueOf(),而不是使用new Bigdecimal():valueof()内部先转换string再初始化
  5. 使用equals时,精度(scale)不同也返回false;使用compareTo方法,-1小于,0等于,1大于
  6. divide时,指定一个结果精度,避免无限循环抛出arith异常;使用RoundingMode类选四舍五入等

    27. 阻塞队列BQ,类似于channel

  • 特点1:队列为空,读进程阻塞
  • 特点2:队列为满,写进程阻塞
  1. ArrayBlockingQueue
  2. LinkedBlockingQueue
  3. PriorityBlockingQueue

    应用场景

  4. 生产消费者模型
  5. 线程池任务队列
  6. 线程同步问题:goroutine,多个线程可以共享一个阻塞队列

    28. 守护线程和普通线程的区别

    29. 启动线程使用start,而不是用run

  7. start方法告诉jvm新建了一个线程,并在新线程中执行与run方法相关联的代码块
  8. run方法仅是一个方法调用,没有新线程创建

    30. java的线程如何通信

  9. 共享内存:使用volatile保证共享变量的可见性
  10. 消息传递:消息队列/管道/信号量

    31. 线程调度算法【抢占式算法】

    线程优先级是如何设定的?

    32. 死锁与活锁,饥饿是什么

  11. 死锁是进程间互斥且一直等待对方释放资源,都无法继续执行的情况
  12. 活锁是运行状态下,多个线程不断地改变自己的状态以回应对方,但最终无法取得进展,导致线程不断重试相同的操作,却无法成功
  13. 饥饿,一个比较宽泛的概念,指一个或多个线程或进程由于某种原因无法获得所需的资源或执行机会

33. 什么时候进入waiting状态

  1. 等待获取锁的时候
  2. 等待IO时
  3. 使用Object.wait()方法,等待其他线程调用同对象的notify和notifyall方法唤醒
  4. 使用Thread.join(),使当前线程等待目标线程的结束,目标线程结束后,当前线程被唤醒
  5. 使用LockSupport.park(),使当前线程等待,直到获取LockSupport指定的许可或者线程被中断、调度。

    为什么wait和notify要在同步块中调用

同步块提供了互斥性

  • 希望同一时刻只有一个线程能执行wait/notify,避免并发修改问题,不在互斥时进行wait/notify会导致错误的上下文,还有导致竞争状态

    36. 自动拆箱导致的 空指针 问题

    数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险.
    减少自动拆箱问题

35. 线程安全如何实现【加锁,原子操作,ThreadLocal减少共享资源,线程安全的设计模式】

  • 线程安全的设计模式:使用单例模式中的双重检查锁定
    实现,其实就是线程安全的singleton
    1
    2


34. 三个线程如何顺序执行

  1. 思路是2等1的锁,3等2的锁,1等3的锁。实现用join和LockSupport的park和unpark方法。
  2. CountDownLatch。设置初始计数为 2,分别在 T1 和 T2 的线程内等待计数器减少到 0,然后释放 T3 线程。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    Thread T1 = new Thread(() -> {
    // 线程 T1 的任务
    });
    Thread T2 = new Thread(() -> {
    try {
    T1.join(); // 等待 T1 执行完成
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    // 线程 T2 的任务
    });
    Thread T3 = new Thread(() -> {
    try {
    T2.join(); // 等待 T2 执行完成
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    // 线程 T3 的任务
    });

LockSupport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static Thread t1;
private static Thread t2;
private static Thread t3;
public static void main(String[] args) {

t1 = new Thread(() -> {
System.out.println("T1 is running.");
LockSupport.unpark(t2); // 唤醒线程T2
});

t2 = new Thread(() -> {
LockSupport.park(); // 阻塞线程T2
System.out.println("T2 is running.");
LockSupport.unpark(t3); // 唤醒线程T3
});

t3 = new Thread(() -> {
LockSupport.park(); // 阻塞线程T3
System.out.println("T3 is running.");
});
t1.start();
t2.start();
t3.start();
}

CountDownLatch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);

Thread t1 = new Thread(() -> {
System.out.println("T1 running.");
latch1.countDown(); // T1 执行完后释放 latch1
});

Thread t2 = new Thread(() -> {
try {
latch1.await(); // 等待 latch1 的释放
System.out.println("T2 running.");
latch2.countDown(); // T2 执行完后释放 latch2
} catch (InterruptedException e) {
e.printStackTrace();
}
});

Thread t3 = new Thread(() -> {
try {
latch2.await(); // 等待 latch2 的释放
System.out.println("T3 running.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});

t1.start();
t2.start();
t3.start();