java基础
java是值传递还是引用传递?
java的参数传递机制是按值传递
对于基本数据类型:方法接收到的是变量的值的副本,因此方法内对参数的修改不会影响原始变量。
对于对象引用:传递的是引用的副本,而不是对象本身,因此方法内对于引用的操作,不会影响原始引用,但会影响原始引用指向的对象。
反射的作用和原理?
什么是反射?
反射是java的一种机制,可以在运行态获取任意一个类的所有属性和方法,可以用来创建对象、调用方法、对属性进行赋值。
Class clz = Class.forName("xxx.User");
Object object = clz.newInstance();
反射有什么优缺点?
优点:能够在运行时动态获取类的实例,提高了程序的灵活性
缺点:反射机制中包括了一些动态类型,JVM无法对反射代码进行优化,因此性能较差,对性能要求高的程序尽量少用反射。
java有哪些类型的异常?
1. java异常主要有 Error 和 Exception 2类
他们都继承Throwable类,在java中只有Throwable类才可以抛出或捕获异常。
Error一般与JVM相关,如系统崩溃、内存不足等,如OutOfMemoryError,这类错误仅靠程序本身无法恢复;Exception表示程序可以处理的异常,如FileNotFoundException。
2. 运行时异常和非运行时异常
- Exception可以分为checked exceptions和unchecked exceptions
- Checked Exception :指编译阶段会进行检查的异常,即非运行时异常,如:IOException、SQLException
- Unchecked Exception :运行时异常,是RuntimeException类及其子类,如:NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)、ClassCastException(类转换异常)、ArrayStoreException(数据存储异常,操作数组时类型不一致)
BIO、NIO、AIO 的区别?
- BIO:Blocking I/O,同步阻塞 I/O 模式,调用方阻塞等待数据读取/写入的完成。
- NIO:New I/O,或 Non-blocking I/O,同步非阻塞 I/O 模型。
- AIO:Asynchronous I/O,异步非阻塞的 IO 模型,基于事件和回调机制实现。调用方调用I/O后,被调用方会直接返回,不会堵塞在那里;当后台处理完成,操作系统会通知相应的线程进行后续的操作。
重载和重写的区别?
重载:
- 发生在同一个类中
- 方法名相同,参数列表不同
重写:
- 发生在父子类中
- 方法名、参数列表必须相同
- 返回值范围小于等于父类,抛出的异常范围小于等于父类
- 访问修饰符范围大于等于父类
String、StringBuffer、StringBuilder的区别?
1. 可变性
String使用final字符数组保存字符串,所以String对象不可变,而StringBuilder与StringBuffer可变。
2. 线程安全
- String对象不可变,可理解为常量,线程安全。
- StringBuffer对方法加了同步锁,线程安全的。
- StringBuilder没有对方法加同步锁,非线程安全。
3. 性能
- 每次对String改变时,会生成一个新的String对象,然后将指针指向新的String对象。
- StringBuffer、StirngBuilder每次只会对本身进行操作,不生成新对象,性能更好。
- 相同情况下,StirngBuilder比StringBuffr仅获得15%左右的性能提升,但却要冒线程不安全的风险。
总结:
- 操作少量的数据使用String
- 单线程操作大量数据使用StringBuilder
- 多线程操使用StringBuffer
jdk代理和CGLIB代理的区别?
JDK动态代理只能对实现了接口的类生成代理,而不能针对类;CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法(继承)。
集合
HashMap的底层实现原理?
1. 数据结构
- JDK7:数组 + 链表
- JDK8+:链表长度 > 8时会转化为红黑树,红黑树元素个数 ≤ 6 时会转化为链表。
2. put元素的原理
- 计算K的hash值:hash = (key==null) ? 0 : (h=key.hashCode()) ^ (h>>>16)
- 计算K的数组位置:index = hash & (length - 1)
- 如果有相同K,则覆盖V;如果没有相同K,JDK7采用头插法(刚添加的元素被访问的概率大),但会引入循环引用问题,导致CPU高,JDK8采用尾插法,避免了这个问题。
- 如果元素个数超过阈值,进行扩容或数据结构变更(链表 → 红黑树)的操作。
3. get元素的原理
- 计算K的hash值
- 计算K的数组index值
- 遍历寻找元素
4. 扩容机制
什么时候扩容?
- 当元素数量超过阈值时扩容
- 阈值 = 数组容量 * 加载因子
- 数组容量默认16,加载因子默认0.75,所以默认阈值12。
扩容原理?
- 创建新数组,容量翻倍
- 重新计算旧数组元素在新数组的位置,然后逐个迁移
ConcurrentHashMap的数据结构?
1. JDK7
ConcurrentHashMap由一个Segment数组构成(默认长度16),Segment继承自ReentrantLock,所以加锁时Segment数组元素互不影响,可实现分段加锁,性能高。
Segment本身是一个HashEntry链表数组,所以每个Segment相当于是一个HashMap。
2. JDK8+
为提升存取效率,摒弃Segment,使用Node数组+链表/红黑树的数据结构。
Node和HashEntry的作用相同,但把值和next采用了volatile修饰,保证了可见性;同时,引入了红黑树,元素多时,存取效率高。
并发控制使用 synchronized + CAS 实现,整体看起来像是线程安全的JDK8 HashMap。
ArrayList 与 LinkedList 的区别?
1. 底层数据结构:都是List接口的实现类,但一个底层数据结构是Array(动态数组),一个是Link(链表)。
2. 随机访问(get和set操作):ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
3. 增加和删除(add和remove操作):对数据进行增加和删除的操作时,LinkedList比ArrayList的效率更高,因为对数组增删后,对所有数据的下标索引造成影响,需要进行数据移动。
fail-fast 和 fail-safe 的区别?
1. fail-fast
fail-fast 是一种快速失败机制,用来检测错误,但不会对错误进行恢复,抛出ConcurrentModificationException 异常
java.util 包下所有的集合都是快速失败
2. fail-safe
fail-safe 是一种安全失败机制,在遍历集合时,不直接在原集合上进行访问,而是先复制原有集合内容,在拷贝的集合上进行遍历。
在遍历过程中,对原集合修改不会触发ConcurrentModificationException。
java.util.concurrent 包下的容器都是安全失败的,可在多线程条件下使用。
多线程
介绍下java的内存模型?
Java内存模型(Java Memory Model,JMM)规定:所有变量存储在主内存
每条线程都有自己的工作内存,保存被该线程使用的变量的主内存副本。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。
不同线程之间无法之间访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
线程池的工作原理?
- 向线程池提交任务
- 如果核心线程池没满,则创建核心线程执行任务
- 如果核心线程池已满,但等待队列没满,则加入等待队列
- 如果核心线程池已满,等待队列已满,但没达到最大线程数,则创建非核心线程执行任务
- 如果核心线程池已满,等待队列已满,达到最大线程数,执行抛弃/拒绝策略
线程池如何创建?如何管理?
1. 使用jdk提供的线程池
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可以控制并发的线程数,超出的线程会在队列中等待
- Executors.newCachedThreadPool:创建一个可缓存的线程池,优先使用空闲线程,若线程超过处理所需,缓存一段时间后会回收
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池
- Executors.newSingleThreadExecutor:创建单个线程的线程池,保证先进先出的执行顺序
2. Executors创建线程存在的问题
- 问题1:默认是无界的阻塞队列,建议指定长度的阻塞队列。
- 问题2.:newCachedThreadPool 的最大线程数是Integer.MAX_VALUE,注意控制下最大线程数。
- 问题3.:默认拒绝策略是AbortPolicy,会丢弃任务,抛出RejectedExecutionException异常。可选择DiscardPolicy(丢弃任务,但是不抛出异常)或 DiscardOldestPolicy(丢弃队列最前面的任务)。
3. 建议使用ThreadPoolExecutor手动创建线程池
参数说明:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数(包含核心线程数)
- keepAliveTime:空闲线程的存活时间
- TimeUnit:时间单位
- BlockingQueue:任务队列,用于存储线程池的待执行任务的
- ThreadFactory:线程工厂,用于生成线程
- handler:拒绝策略
synchronized和ReentrantLock的区别?如何选择?
java实现加锁,主要有2种方式:synchronized 和 ReentrantLock
1. 实现原理不同
(1) synchronized原理
synchronized是java的1个关键字,底层由JVM实现加锁。
synchronized 关键字编译后,会在同步块前后生成 monitorenter 和 monitorexit 两个字节码指令,作用是获取和释放对象的锁。
那对象的锁在哪里?1个对象由三个部分组成:对象头、实例数据、对齐填充,对象的锁状态就存储在对象头的markword中,有无锁、偏向锁、轻量级锁、重量级锁,4种锁状态。
修饰方法,锁则是对象;修饰静态方法,锁的是当前类的Class实例;修饰代码块,锁的是传入synchronized的对象。
public synchronized void fun() {}
public static synchronized void fun() {}
synchronized(obj) {}
(2) ReentrantLock的原理
ReentrantLock底层使用 CAS + AQS 队列来实现加锁,使用 lock()方法加锁,unlock()解锁。
当线程调用lock()方法,如果锁没被任何线程占用,则当前线程获取到锁,然后设置锁的拥有者为当前线程,并设置AQS的状态值为1;如果当前线程之前已获得该锁,则只把AQS的状态值加1;如果锁被其他线程持有,则线程会被放入AQS队列后阻塞挂起。
volatile ReentrantLock lock =
new ReentrantLock(); lock.lock();
try {
//xxx
} finally {
lock.unlock();
}
2. 是否公平锁
公平锁:先来先得,按照申请锁的顺序去获得锁
synchronized为非公平锁;ReentrantLock可以选择公平非公平,通过构造方法传入boolean值进行选择,默认false非公平,true为公平。
3. 是否可主动释放锁
synchronized 不需要手动释放锁,优点是不会忘记释放锁,缺点是无法干预锁,只能等JVM释放;ReentrantLock需要手动释放锁,优点是灵活,不需要一直阻塞等待,缺点是可能忘记释放锁,导致死锁。
4. 锁是否可中断
synchronized不可中断;ReentrantLock可调用interrupt()方法进行中断,更加灵活。
如何选择?
如果对公平、中断、可释放等有诉求,可以选ReentrantLock。否则都可以使用synchronized,JVM一直在对synchronized进行优化,性能不差。
ThreadLocal的作用和原理?
背景:多线程访问共享变量时,一般是通过对共享变量加锁来实现并发控制,但是加锁会带来性能下降。
方案:ThreadLocal使用空间换时间思想,在Thread类里有一个成员变量ThreadLocalMap,专门存储当前线程的共享变量副本,每个线程只对自己的变量副本来做更新操作。这样既解决了线程安全问题,又避免了加锁的开销。
volatile的作用和原理?
1. 可见性
当变量被 volatile 修饰,变量发生修改时,值会立即被更新到主存,其他线程可以及时在内存中读取到最新值。
上图为Java内存模型JMM,它规定所有变量都存储在主内存,每个线程有自己的工作内存,其中保存了主内存变量的副本。
可见性原理:volatile变量转译为汇编代码后,会多出一条Lock前缀的指令,它会触发CPU的缓存一致性协议,可以保证线程内的修改会及时同步到主内存。
2. 有序性
编译器和处理器为了优化程序性能而对指令序列进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
被volatile修饰的变量,会禁止指令重排序,从而保证有序性。
原理:通过对volatile修饰的变量的读写操作前后加上各种特定的内存屏障,来禁止指令重排序来保障有序性的。
3. 不保证原子性
原子性操作就是一个操作或多个操作要么执行都成功,要么执行都失败,不可中断,不存在中间状态。
volatile不能保证原子性,所以多线程下存在线程安全问题。
synchronized虽然能实现原子性,但锁的性能较差,所以我们可利用 volatile的可见性 + CAS 形成一种高性能的无锁,也能保证线程安全。
多个线程同时读写,读线程的数量远远⼤于写线程,你会选择加什么样的锁?
JAVA的并发包提供了读写锁ReentrantReadWriteLock,读写锁内部维护了一对锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁。
线程获得读锁的条件:没有其他线程的写锁,所以可以并发读。
线程获得写锁的条件:没有其他线程的读锁 + 没有其他线程的写锁。当有线程想写,就不允许读和写了。
对于读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。
什么是CAS?
在 Java 开发中,CAS 是“Compare and Swap(比较并交换)”的缩写,CAS 操作包括三个操作数:内存位置 V、旧的预期数 A 和新的值 B。CAS 会比较内存位置 V 的值与预期值 A,如果相等,则将内存位置 V 的值更新为新值 B。整个操作是原子的,即在执行过程中不会被中断,因此可以保证并发情况下的数据一致性。
java.util.concurrent.atomic 包下提供了一系列基于 CAS 的原子类,比如 AtomicInteger,这些类提供了一种线程安全的方式来进行原子操作,避免了使用锁的开销,性能高。
谈谈对AQS的理解?
AQS的核心思想是对于共享资源,维护一个双端队列来管理线程,队列中的线程依次获取资源,获取不到的线程进入队列等待,直到资源释放,队列中的线程依次获取资源。
AQS的基本框架如图所示:
AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock。
介绍下wait()、notify()、notifyAll(),这3个方法的作用?
wait()、notify()、notifyAll()都是Object类的方法。
Object.wait():释放当前对象锁,并进入阻塞队列;Object.notify():唤醒当前对象阻塞队列里的任一线程(并不保证唤醒哪一个);Object.notifyAll():唤醒当前对象阻塞队列里的所有线程。
因wait()而导致阻塞的线程是放在阻塞队列中的,因竞争失败导致的阻塞是放在同步队列中的,notify()/notifyAll()实质上是把阻塞队列中的线程放到同步队列中去
wait()、notify()、notifyAll()方法需要包含在synchronized块中吗?为什么?
调用wait()是要释放锁,释放锁的前提是必须先获得锁,所以需要在synchronized中执行。
notify()、notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;(本质是让处于入口队列的线程竞争锁)
CopyOnWrite的应用场景和缺点?
CopyOnWrite是先对集合进行copy,再write,修改完之后,原容器引用指向修改后的副本。
应用场景:用在读多写少的场景
缺点:
- 内存占用问题:写操作会进行数据拷贝,且旧数据引用也可能被其他线程占有一段时间,可能会占用相当大的内存,GC时间也可能相应的增加。
- 数据一致性问题:由于读操作没有并发控制,可能某个线程读到的数据不是实时数据。
什么是死锁?如何防止死锁?
死锁:多个线程互相等待对方资源。产生死锁的四大必要条件:
- 资源互斥:资源同一时刻只能被一个进程或线程使用。
- 请求和保持:线程获得资源后,又对其他资源发出请求,但是该资源被其他进程占有,此时请求阻塞,但又对自己已有的资源保持不放。
- 资源不可剥夺:资源需要等资源占有者主动释放,不能被强制剥夺。
- 环路等待:组成一条等待环路
防止死锁产生的方法:破坏4大条件
JVM
JVM的组成部分?
JVM包含4部分,运行时数据区、类加载器、执行引擎、本地库接口。运行时数据区最关键,由5部分组成:
- 堆:线程共享,大部分对象在这里分配内存
- 方法区:线程共享,存储已被虚拟机加载的类信息,比如常量、静态变量
- 虚拟机栈:存储java方法的局部变量、方法出口等信息。
- 本地方法栈:与虚拟机栈作用一样
- 程序计数器:线程当前执行的字节码行号
垃圾回收算法有哪些?
1. 引用计数法
给每个对象一个引用计数器,有地方引用它时,计数器加1;引用失效时,计数器减1,计数器的值为0时,可以GC。
缺点:循环引用无法回收,A和B都置为null,但内部存在互相引用,都不能被回收。
2. 标记清除法
- 标记:从根节点开始标记引用的对象。
- 清除:未被标记引用的对象就是垃圾对象,可以被清理。
- 缺点:效率较低,标记和清除两个动作都需要遍历所有的对象;内存碎片化严重。
3. 标记压缩算法
标记压缩算法是在标记清除算法的基础之上,将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。
缺点:移动内存的步骤,对效率一定影响。
4. 复制算法
将内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空。
当垃圾对象较多时,需要复制的对象就会少,效率比较高,反之则不适合,会浪费大量内存空间。
5. 分代回收算法
(1) 年轻代使用复制算法
GC开始前,对象只存在于Eden区和名为From的Survivor区,Survivor区To是空的。
紧接着GC,Eden区中所有存活对象,年龄达到一定值的对象会被移动到年老代,没有达到阀值的对象会被复制到To区域。
GC会一直重复这样的过程,直到To区被填满,To区被填满之后,会将所有对象移动到年老代。
(2) 老年代使用标记整理算法
老年代对象的存活率比较高,一直复制过来,复制过去,没啥意义,浪费时间。所以针对老年代提出了“标记整理”算法。
老年代的垃圾回收算法的速度至少比新生代的垃圾回收算法的速度慢10倍。如果系统频繁出现老年代的Full GC垃圾回收,会导致系统性能被严重影响,出现频繁卡顿的情况。
类的加载过程?
- 加载:通过全限定名,获取此类的二进制字节流
- 验证:验证字节流中的文件格式、元数据、符号引用是否符合JVM规范
- 准备:为类的静态变量分配空间,并且初始化这些字段
- 解析:常量池内的符号引用替换成直接引用
- 初始化:在准备阶段,变量被赋予了初始值,初始化阶段会按照用户编写的代码重新初始化
什么是双亲委派模型?有没有办法打破?
当需要加载一个class文件的时候,首先会把这个class的查询和加载 委派给父加载器 去执行,如果父加载器都无法加载,再尝试自己来加载这个class。
双亲委派并不是一个强制性的约束模型,可以通过一些方式去打破:
1. 继承ClassLoader抽象类,重写loadClass方法,在这个方法可以自定义要加载的类使用的类加载器。
2. 使用线程上下文加载器,通过Thread类的setContextClassLoader()方法来设置当前类使用的类加载器类型。
JVM调优的方法有哪些?
1. 内存调优:调整-Xms 和 -Xmx 参数,即初始堆和最大堆大小;调整-Xmn或-XX:NewRatio参数,即新生代与老年代的比例;调整-XX:SurvivorRatio参数,即Eden区和Survivor区的比例。
2. 垃圾回收调优:使用-Xloggc:<file-path>参数;分析GC日志,了解GC事件的频率和持续时间,以及内存分配和回收的模式。
3. 性能监控和分析:使用性能监控工具,如 JConsole,对 JVM 运行时进行监控和分析,发现性能瓶颈和问题点。
4. 调整线程栈大小:使用-Xss参数设置每个线程的栈大小,以减少内存使用或避免栈溢出错误。
5. 代码层面的优化:及时释放不需要的资源;避免创建大对象。
Spring
IOC 的原理?
Spring IOC(控制反转)是Spring框架的核心特性之一,原理就是通过容器管理Bean的生命周期和依赖关系,实现了应用程序组件之间的解耦,简化了应用程序的开发和维护。
1. Bean定义:在Spring IOC容器中,所有的组件都被称为Bean。每个Bean都有一个对应的Bean定义,它包含了Bean的配置元数据,比如类的全限定名、依赖关系、初始化方法、销毁方法等信息。
2. 容器管理:Spring IOC容器负责管理Bean的生命周期和依赖关系。容器在启动时读取Bean定义,然后根据定义创建Bean的实例,并在需要时注入依赖关系。
3. 依赖注入:依赖注入是Spring IOC的核心机制。通过依赖注入,容器在创建Bean的实例时,自动将其依赖的其他Bean注入进来。这样,Bean之间的依赖关系由容器负责管理,而不是由Bean自己去查找或创建依赖的对象。
4. Bean的生命周期管理:Spring IOC容器负责管理Bean的生命周期,包括Bean的创建、初始化、使用和销毁。通过配置初始化方法和销毁方法,可以在Bean的生命周期的不同阶段执行相应的操作。
5. 解耦应用程序:通过IOC容器管理Bean的依赖关系,应用程序的各个组件之间的耦合度大大降低。这样,可以更容易地进行单元测试、模块替换和系统升级。
AOP 的原理?
Spring AOP(面向切面编程)通过在运行时动态地将横切逻辑(如日志记录、性能统计、安全控制等)插入到应用程序的特定位置来实现特定的功能。通过AOP,可以将横切逻辑与业务逻辑分离,提高了代码的可维护性和可重用性。底层原理:
1. 代理模式:当一个Bean被AOP代理后,调用该Bean的方法时,实际上是调用了代理对象的方法。代理对象在方法执行前后可以执行额外的横切逻辑,比如记录日志、检查权限等。
2. 切点(Pointcut):定义了在哪些连接点(方法调用、字段访问等)上可以应用横切逻辑
3. 通知(Advice):定义了在连接点上执行的具体操作,比如在方法执行前后执行的操作。Spring AOP提供了几种不同类型的通知,包括前置通知、后置通知、环绕通知等。
4. 织入(Weaving):将横切逻辑应用到目标对象中,并创建最终的代理对象的过程。
Spring Bean的作用域有哪些?
Spring框架中,Bean可以在不同的情况下有不同的作用域,主要包括:
1、Singleton: 默认作用域,每个Spring容器中只有一个Bean实例。
2、Prototype: 每次请求都会创建一个新的Bean实例。
3、Request: 在一次HTTP请求中,每个Bean都是一个新的实例。
4、Session: 在一个HTTP会话中,每个Bean都是一个新的实例。
5、GlobalSession: 在全局HTTP会话中,每个Bean都是一个新的实例,主要用于Portlet应用。
6、Application: 在ServletContext生命周期内,Bean为单例。
7、WebSocket: 在WebSocket生命周期内,Bean为单例。
spring bean默认单例,如何避免线程安全问题?
Spring不会对单例Bean的线程安全做特殊处理,取决于Bean本身的实现。
1、 无状态Bean: 最简单的方法是让Bean保持无状态。这意味着Bean不保留任何数据(状态),可以被多个线程安全地共享。
2、 线程局部变量: 如果必须保留状态,可以使用ThreadLocal变量确保每个线程有自己的状态副本。
3. 同步访问控制: 另一个选择是使用synchronized控制对状态的访问。
SpringMVC的原理?
- SpringMVC是基于MVC模式的Web框架
- 核心组件是DispatcherServlet,它负责接收请求,并将请求根据HandlerMapping分发到不同的Handler。
- 控制器Handler处理请求,并返回ModelAndView对象。
- DispatcherServlet根据ModelAndView对象中的视图名称,将请求转发到相应的视图,最后将视图渲染给用户。