Java基础
1. 面向对象有哪些特性?
面向对象四大特性:封装,继承,多态,抽象
- 封装就是将类的信息隐藏在类内部,不允许外部程序直接访问,而是通过该类的方法实现对隐藏信 息的操作和访问。 良好的封装能够减少耦合。
- 继承是从已有的类中派生出新的类,新的类继承父类的属性和行为,并能扩展新的能力,大大增加 程序的重用性和易维护性。在Java中是单继承的,也就是说一个子类只有一个父类。 (Java是单继承,通过多层继承、内部类和接口 可以实现多继承)
- 多态是同一个行为具有多个不同表现形式的能力。在不修改程序代码的情况下改变程序运行时绑定 的代码。实现多态的三要素:继承、重写、父类引用指向子类对象。 静态多态性:通过重载实现, 相同的方法有不同的參数列表,可以根据参数的不同,做出不同的处理。 动态多态性:在子类中重 写父类的方法。运行期间判断所引用对象的实际类型,根据其实际类型调用相应的方法。
- 抽象。把客观事物用代码抽象出来。
2. 自动装箱和拆箱
public void testAutoBox() {
int a = 100;
Integer b = 100;
System.out.println(a == b); // true
Integer c = 100;
Integer d = 100;
System.out.println(c == d); // true
Integer e = 200;
Integer f = 200;
System.out.println(e == f); //false
}为什么第三个输出是false?Integer e = 200; 会调用 调⽤ Integer.valueOf(200) 。而从Integer的valueOf()源码可以看到,这里的实现并不是简单的new Integer,而是用IntegerCache做一个cache。默认Integer cache 的下限是-128,上限默认127。当赋值200给Integer时,不在cache 的范围内,所以会new Integer并返回,当然==比较的结果是不相等的。
3. String不可变,String, StringBuffer 和 StringBuilder区别
对于String来说,是把数据存放在了常量池中,因为所有的String,默认都是以常量形式保存,且由final修饰,因此在线程池中它是线程安全的。

4.两个对象的hashCode()相同,则 equals()是否也一定 为 true?
equals()比较之前会先比较hashCode()是否相等,只有hashCode()相等之后才会比较equals。这是由于hash算法是二进制算法,计算机本质是二进制,所有hash算法速度很快。
**以下是关于hashcode的一些规定:
- 两个对象相等,hashcode一定相等
- 两个对象不等,hashcode不一定不等
- hashcode相等,两个对象不一定相等
- hashcode不等,两个对象一定不等**
5. Java创建对象的方式
- 用new语句创建对象。
- 使用反射,使用Class.newInstance()创建对象。
- 调用对象的clone()方法。
- 运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。
6. final, finally, finalize 的区别
- final 用于修饰属性,方法和类, 分别表示属性不能被重新赋值, 方法不可被覆盖, 类不可被继承。
- finally 是异常处理语句结构的一部分,一般以ty-catch-finally出现,finally代码块表示总是被执行。
- finalize 是Object类的一个方法,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize()方法,回收垃圾,JVM并不保证此方法总被调用。
final关键字的作用
- final 修饰的类不能被继承。
- final 修饰的方法不能被重写。
- final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
7.常见的Exception有哪些?
RuntimeException
- ClassCastException //类型转换异常
- IndexOutOfBoundsException //数组越界异常
- NullPointerException //空指针
- ArrayStoreException //数组存储异常
- NumberFormatException //数字格式化异常
- ArithmeticException //数学运算异常
unchecked Exception
- NoSuchFieldException //反射异常,没有对应的字段
- ClassNotFoundException //类没有找到异常
- IllegalAccessException //安全权限异常,可能是反射时调用了private方法
8. BIO/NIO/AIO区别的区别?
- BIO同步阻塞IO : 用户进程发起一个IO操作以后,必须等待IO操作的真正完成后,才能继续运行。
- NIO同步非阻塞IO: 客户端与服务器通过Channel连接,采用多路复用器轮询注册的Channel。提高吞吐量和 可靠性。用户进程发起一个IO操作以后,可做其它事情,但用户进程需要轮询IO操作是否完成,这样造 成不必要的CPU资源浪费。
- AIO异步非阻塞IO: 非阻塞异步通信模式,NIO的升级版,采用异步通道实现异步通信,其read和write方法 均是异步方法。用户进程发起一个IO操作,然后立即返回,等IO操作真正的完成以后,应用程序会得到 IO操作完成的通知。类似Future模式。
9. java集合
List
- ArrayList和LinkedList 继承了 AbstractList,并实现了 List接口
- ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。 默认情况下,新的容量会是原容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
- 遍历ArrayList时移除元素,使用foreach会导致快速失败(
fail fast:当多个线程对同一个集合进行操作时,就有可能会产生fast-fail事件)问题,可以通过使用迭代器remove()方法Iterator itr = list.iterator(); while(itr.hasNext()) { if(itr.next().equals("jay") { itr.remove(); } } Arraylist 和 Vector 的区别:
- ArrayList在内存不够时默认是扩展50% + 1个,Vector是默认扩展1倍;
- Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为操作Vector效率比较低
Arraylist 与 LinkedList 区别:
- ArrayList基于动态数组实现;LinkedList基于链表实现。
- 对于随机index访问的get和set方法,ArrayList的速度要优于LinkedList。
- 新增和删除元素,LinkedList的速度要优于ArrayList
Map
- HashMap 使用数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的, 链表长度大于8 (TREEIFY_THRESHOLD)时,会把链表转换为红黑树,红黑树节点个数小于6 (UNTREEIFY_THRESHOLD)时才转化为链表,防止频繁的转化。
- 1.8扩容机制:当元素个数大于threshold(原容量*扩容因子)时,会进行扩容,使用2倍容量的数组代替原有数组。采用尾插入的方式将原数组元素拷贝到新数组。原数组的元素在重新计算hash之后,因为数组容量n变为2倍,那么n-1的mask范围在高位多1bit。在元素拷贝过程不需要重新计算元素在数组中的位置,只需要看看原来的hash值新增的那个bit是1还是0, 是0的话索引没变,是1的话索引变成“原索引+oldCap”(根据
e.hash & (oldCap - 1) == 0判断)。这样可以省去重新计算hash值的时间,而且由于新增的1bit是0还是1可以认为是随机的,因此resize的 过程会均匀的把之前的冲突的节点分散到新的bucket。 - 为什么使用红黑树而不使用AVL树?ConcurrentHashMap 在put的时候会加锁,使用红黑树插入速度更快,可以减少等待锁释放的时间。红黑树是对AVL树的优化,只要求部分平衡,用非严格的平衡来换取增删节点时候旋转次数的降低,提高了插入和删除的性能。
- 在解决 hash 冲突的时候,为什么选择先用链表,再转红黑树?因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,链表结构可以保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表 是 O(n),此时需要红黑树来加快查询速度,但是插入和删除节点的效率变慢了。如果一开始就用红黑树结构,元素太少,插入和删除节点的效率又比较慢,浪费性能。
- HashMap为什么线程不安全?多线程下扩容死循环。JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时 候有可能导致环形链表的出现,形成死循环。 在JDK1.8中,在多线程环境下,会发生数据覆盖的情况。
让HashMap线程安全的方法:
- 使用Collections的synchronizedMap方法将HashMap转换为线程安全的Map。
CodeMap synchronizedMap = Collections.synchronizedMap(new HashMap<>()); - 使用ConcurrentHashMap,它是Java提供的线程安全的散列表实现。
CodeMap concurrentHashMap = new ConcurrentHashMap<>(); - 使用读写锁(ReentrantReadWriteLock)来保护HashMap的读写操作。
CodeReadWriteLock lock = new ReentrantReadWriteLock(); Map map = new HashMap<>(); // 写操作加写锁 lock.writeLock().lock(); try { // 执行写操作 map.put(key, value); } finally { lock.writeLock().unlock(); }
- 使用Collections的synchronizedMap方法将HashMap转换为线程安全的Map。
HashMap和HashTable的区别?
- HashMap可以接受为null的key和value,key为null的键值对放在下标为0的头结点的链表中,而 Hashtable则不行。
- HashMap是非线程安全的,HashTable是线程安全的。Jdk1.5提供了ConcurrentHashMap,它是 HashTable的替代。
- Hashtable很多方法是同步方法,在单线程环境下它比HashMap要慢。
- 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
- LinkedHashMap底层原理?LinkedHashMap继承于HashMap,是HashMap和LinkedList的融合体,具备两者的特性。每次put操作都会将entry插入到双向链表的尾部。
- TreeMap: TreeMap是有序的key-value集合,通过红黑树实现。根据键的自然顺序进行排序或根据提供的 Comparator进行排序。 TreeMap继承了AbstractMap,实现了NavigableMap接口,支持一系列的导航方法,给定具体搜索目标,可以返回最接近的匹配项。如floorEntry()、ceilingEntry()分别返回小于等于、大于等于给 定键关联的Map.Entry()对象,不存在则返回null。lowerKey()、floorKey、ceilingKey、 higherKey()只返回关联的key。
ConcurrentHashMap加锁细节:
- 分段锁:ConcurrentHashMap内部将数据结构分为多个Segment(分段锁),每个Segment都有自己的锁。不同的线程可以同时操作不同的Segment,从而提高并发性能。
- 锁粒度:相比整个HashMap上锁,分段锁的粒度更细。只有在修改或查找某个Segment中的元素时,才需要锁定该Segment,而不会影响其他Segment的访问。
- 读操作的无锁:对于读操作,ConcurrentHashMap允许多个线程同时读取,不需要加锁,这提高了读取的并发性能。
- 写操作的加锁:对于写操作,需要先获取相应Segment的写锁,保证写操作的原子性和线程安全性。在获取写锁期间,其他线程的读/写操作会被阻塞。
ConcurrentHashMap 和 Hashtable 的区别?
- Hashtable通过使用synchronized修饰方法的方式来实现多线程同步,因此,Hashtable的同步会锁住整个数组。在高并发的情况下,性能会非常差。ConcurrentHashMap采用了更细粒度的锁来 提高在并发情况下的效率。注:Synchronized容器(同步容器)也是通过synchronized关键字来 实现线程安全,在使用的时候会对所有的数据加锁。
- Hashtable默认的大小为11,当达到阈值后,每次按照下面的公式对容量进行扩充:newCapacity = oldCapacity * 2 + 1。ConcurrentHashMap默认大小是16,扩容时容量扩大为原来的2倍。
Set
HashSet、LinkedHashSet 和 TreeSet 的区别?
- HashSet 是 Set 接口的主要实现类,HashSet 的底层是 HashMap线程不安全的,可以存储 null 值;
- LinkedHashSet 是 HashSet的子类,能够按照添加的顺序遍历;
- TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式可以自定义(有序)。
10. java并发
线程池
线程池优势:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。统一管理线程,避免系统创建大量同类线程而导致消耗完内存。
主要参数
- 核心线程数(Core Pool Size):线程池中始终保持的最小线程数。即使线程处于空闲状态,也 不会被销毁,除非线程池被关闭。新任务提交时,如果线程池中的线程数还没有达到核心线程数, 会创建一个新的核心线程来执行任务。
- 最大线程数(Maximum Pool Size):线程池允许存在的最大线程数。当等待队列已满且当前运行的线程数小于最大线程数时,线程池会创建新的线程来执行任务,直到达到最大线程数。
- 空闲线程存活时间(Keep Alive Time):当线程池中的线程数超过核心线程数,并且没有新的任务需要执行时,空闲线程的存活时间。超过这个时间,多余的线程将会被销毁,以回收资源。
- 阻塞队列(Blocking Queue):用于存放等待执行的任务的队列。当线程池中的线程数达到核心线程数时,新的任务会被放入阻塞队列中等待执行。 5
- 拒绝策略(Rejected Execution Handler):当线程池已经达到最大线程数,且阻塞队列也已满,无法接受新任务时,采取的策略。常见的策略有抛出异常、丢弃任务、丢弃最旧的任务等
新线程加入线程池流程
- 如果线程池中的线程数小于核心线程数,则创建一个新的核心线程来执行任务。
- 如果线程池中的线程数已达到核心线程数,将新任务放入阻塞队列中等待执行。
- 如果阻塞队列已满且线程池中的线程数还未达到最大线程数,则创建一个新的非核心线程来执行任务。
- 如果线程池中的线程数已达到最大线程数,且阻塞队列已满,根据拒绝策略来处理新任务的提交,例如抛出异常或丢弃任务。
线程池种类与使用场景
FixedThreadPool(固定线程池):
- 场景:当需要控制线程数量并希望固定线程数时,适用于长时间运行的任务。
- 参数配置:通过 Executors.newFixedThreadPool(int n) 方法创建,可设置固定的线程数量。
CachedThreadPool(缓存线程池):
- 场景:适用于执行大量的短期异步任务,自动根据需求创建线程,可重复利用空闲线程。
- 参数配置:通过 Executors.newCachedThreadPool() 方法创建,不需要手动设置线程数量。
SingleThreadExecutor(单线程池):
- 场景:适用于需要顺序执行任务的场景,保证任务按顺序执行,无需担心并发问题。
- 参数配置:通过 Executors.newSingleThreadExecutor() 方法创建,只有一个工作线程。
ScheduledThreadPool(定时任务线程池):
- 场景:适用于需要定期执行任务的场景,如定时任务、定期数据备份等。
- 参数配置:通过 Executors.newScheduledThreadPool(int n) 方法创建,可以设置核心线程数量。
WorkStealingPool(工作窃取线程池):
- 场景:适用于执行大量相互独立的任务,多个线程可以从任务队列中窃取任务,提高效率。
- 参数配置:通过 Executors.newWorkStealingPool(int parallelism) 方法创建,设置并行度。
自定义线程池:
- 场景:适用于特殊需求的场景,可以根据具体业务需求自定义线程池参数配置。
- 参数配置:通过 ThreadPoolExecutor 类进行手动配置线程池参数,如核心线程数、最大线程数、任务队列大小等。
进程线程
进程(Process):
- 进程是操作系统中资源分配的基本单位,是一个正在执行中的程序。
- 每个进程都有独立的内存空间,包括代码、数据、堆栈等。
- 进程之间相互独立,通信需要使用进程间通信(IPC)的机制,如管道、消息队列等。
- 进程的切换开销较大,需要保存和恢复整个进程的上下文信息。
线程(Thread):
- 线程是进程中的一个执行单元
- 同一进程中的多个线程共享相同的内存空间,包括代码、数据、堆栈等。
- 线程可以看作轻量级的进程,多个线程之间可以直接进行通信,共享进程的资源。
- 线程的切换开销较小,只需要保存和恢复线程的上下文信息。
协程(Coroutine):
- 协程是一种用户态的轻量级线程,由程序员控制调度。
- 协程可以在单个线程内实现多个执行流,并发执行,但并非真正的并行。
- 协程之间通过协程库提供的特定方法进行切换,切换开销很小。
- 协程能够有效地解决高并发和高性能的问题,适用于 I/O 密集型任务。
线程安全:线程安全是指当多个线程同时访问一个共享的资源时,保证该资源在并发情况下仍然能够正确地被访问和操作,而不会出现数据不一致或不可预期的错误。线程安全的实现需要保证以下几点:
- 原子性:单个操作是不可分割的,要么全部执行成功,要么全部不执行,没有中间状态。
- 可见性:对共享数据的修改对其他线程是可见的,即一个线程对共享数据的修改能够被其他线程及 时获取到。
- 有序性:线程间的操作按照一定的规则进行调度和执行,不会乱序执行。
线程的生命周期
- 初始(NEW):线程被构建,还没有调用 start()。
- 就绪(Runnable):当线程被创建后,调用了start()方法进行启动,进入就绪状态。处于就绪状态的线程已经具备了运行的条件,等待CPU的调度。
- 运行(Running):当就绪状态的线程被CPU调度执行时,进入运行状态。此时线程正在执行任务代码。
- 阻塞(BLOCKED):一般是被动的,在抢占资源中得不到资源,被动的挂起在内存,等待资源释放将其唤醒。线程被阻塞会释放CPU,不释放内存。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
创建线程的方式
- 通过扩展Thread类来创建多线程
MyThread mThread1 = new MyThread(); mThread1.start(); 通过实现Runnable接口来创建多线程,可实现线程间的资源共享
Runnable1 r = new Runnable1(); Thread thread = new Thread(r); thread.start();- 实现Runnable接口比继承Thread类所具有的优势:
- 资源共享,适合多个相同的程序代码的线程去处理同一个资源
- 可以避免java中的单继承的限制
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类
- 实现Callable接口,通过FutureTask接口创建线程。
Callable1 c = new Callable1(); FutureTask result = new FutureTask<>(c); new Thread(result).start(); - 使用Executor框架来创建线程池。
//获取ExecutorService实例,生产禁用,需要手动创建线程池 ExecutorService executorService = Executors.newCachedThreadPool(); //提交任务 executorService.submit(new RunnableDemo());
- 通过扩展Thread类来创建多线程
Runnable和Callable的区别
- Callable接口方法是call(),Runnable的方法是run();
- Callable接口call方法有返回值,支持泛型,Runnable接口run方法无返回值。
- Callable接口call()方法允许抛出异常;而Runnable接口run()方法不能继续上抛异常;
死锁产生的必要条件
- 互斥:一个资源每次只能被一个进程使用(资源独立)
- 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放(不释放锁)
- 不剥夺:进程已获得的资源,在未使用之前,不能强行剥夺(抢夺资源)
- 循环等待:若干进程之间形成一种头尾相接的循环等待的资源关闭(死循环)
避免死锁的方法
- 第一个条件 "互斥" 是不能破坏的,因为加锁就是为了保证互斥
- 一次性申请所有的资源,破坏 "占有且等待" 条件
- 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源,破坏 "不可抢占" 条件
- 按序申请资源,破坏 "循环等待" 条件
常见线程的方法
- start():启动线程,使其进入可运行状态。
- run():定义线程的执行逻辑,线程启动后将自动调用该方法。
- join():等待线程执行完成,主线程会阻塞直到该线程执行完毕。(放弃CPU控制权)
- sleep(long millis) / sleep(long millis, int nanos):使线程进入休眠状态,暂停指定的时间。(让出cpu资源,但不释放对象锁)
- isAlive():判断线程是否存活,返回布尔值。
- getName() / setName(String name):获取或设置线程的名称。
- setPriority(int priority):设置线程的优先级,范围为1到10。
- getPriority():获取线程的优先级。
- interrupt():中断线程,给线程发送中断信号。
- isInterrupted():判断线程是否被中断,返回布尔值。
- currentThread():获取当前正在执行的线程对象。
- yield():让出当前线程的执行权,让其他线程有机会执行。当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。
- isDaemon() / setDaemon(boolean on):判断线程是否为守护线程,或设置线程是否为守护线程。
- wait() / notify() / notifyAll():用于线程间的等待和唤醒机制,配合synchronized关键字使用。
- run()和start()区别:调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run();直接调用 run() 方法,无法达到启动多线程的目的,相当于主线程线性执行 Thread 对象的 run() 方法。 一个线程的 start() 方法只能调用一次,多次调用会抛出
java.lang.IllegalThreadStateException异常;run() 方法没有限制。 sleep()和wait()区别:
- 来自不同类:sleep() 是 Thread 类的静态方法,而 wait() 是 Object 类的实例方法。
- 使用方式不同:sleep() 方法用于暂停当前正在执行的线程的执行,可以通过指定时间来控制暂停的时长。wait() 方法用于使当前线程等待,直到其他线程调用该对象的 notify() 或 notifyAll() 方法来唤醒它。
- 锁的释放情况不同:sleep() 方法在执行期间不会释放锁资源,而 wait() 方法在调用后会释放对象的监视器锁,以便其他线程能够访问。
- 触发条件不同:sleep() 方法不需要依赖外部条件来触发,它会在指定的时间过去后自动恢复执行。wait() 方法则需要依赖其他线程改变共享对象的状态并调用 notify() 或 notifyAll() 方法来唤醒等待的线程。
- 异常抛出情况不同:sleep() 方法在执行期间可能会抛出 InterruptedException 异常,并且 需要在方法体内捕获或声明抛出。wait() 方法必须在 synchronized 块中调用,并且在没有持有相关对象的锁时调用会抛出 IllegalMonitorStateException 异常。
锁
常见锁
synchronized关键字:
- 用法:synchronized关键字可以修饰代码块或方法。通过获取对象的内部锁(也称为监视器锁)来实现线程同步和互斥。
- 底层原理:synchronized使用的是JVM中的各种Monitor(监视器)操作指令来实现锁的获取和释放,保证了同一时间只有一个线程执行被锁定的代码。
- 作用:确保同一时刻只有一个线程能够执行被锁定的代码,保证了数据的安全性。
- 特点:可重入(一个线程已经获得了某个对象的锁,再次进入该对象的 synchronized 代码块时,线 程可以重复地获取该对象的锁,而不会被自己所持有的锁所阻塞。这样可以避免死锁的发生。)、独占、互斥。
ReentrantLock类:
- 用法:ReentrantLock是显式锁(需要手动获取和释放),通过调用lock()方法获取锁,unlock()方法释放锁。
- 底层原理:ReentrantLock底层使用了AbstractQueuedSynchronizer (AQS)框架,基于CAS(Compare and Swap)操作实现锁的获取和释放。
- 作用:提供了更灵活的线程同步机制,可以实现可重入性、公平性等特性,并且支持条件变量。
- 特点:可重入、独占、互斥,支持公平性设置。
ReadWriteLock接口:
- 用法:ReadWriteLock接口定义了读写锁,包括读锁和写锁。通过调用readLock()获取读锁,writeLock()获取写锁。
- 底层原理:ReadWriteLock维护了一个计数器来跟踪获取读锁的线程数量,只有当没有线程持有写锁时,才能获取读锁或者有限数量的读锁。
- 作用:在读多写少的场景中提供更高的并发性,允许多个线程同时读取共享资源,但只有一个线程可以进行写操作。
- 特点:支持多个读操作并发进行,写操作互斥。
Condition接口:
- 用法:Condition接口通常和显式锁(如ReentrantLock)一起使用。通过调用await()方法使线程等待,signal()方法或signalAll()方法唤醒等待的线程。
- 底层原理:Condition使用了底层锁(如ReentrantLock)的同步队列来管理等待和被唤醒的线程。
- 作用:提供了更灵活的线程协作机制,可以实现复杂的等待和通知逻辑。
- 特点:提供更灵活的线程协作机制,可以实现复杂的等待和通知逻辑。
CountDownLatch类:
- 用法:CountDownLatch是一个同步辅助类,通过指定计数器的初始值,调用countDown()方法对计数器进行减1操作,await()方法等待计数器变为0。
- 底层原理:CountDownLatch使用了底层的Unsafe类实现计数器的操作,并且使用了内部的AQS框架来实现线程等待和唤醒。
- 作用:控制一个或多个线程等待其他线程执行完毕后再继续执行,实现线程间的协调和同步。
- 特点:等待其他线程执行完毕后再继续执行。
CyclicBarrier类:
- 用法:CyclicBarrier是一个同步辅助类,通过指定参与线程的数量,当所有参与线程都达到屏障点时,调用await()方法等待,屏障开放后继续执行。
- 底层原理:CyclicBarrier使用了底层的ReentrantLock和Condition来管理线程的等待和唤醒。
- 作用:多个线程相互等待,到达共同状态后再继续执行,常用于分阶段任务或分组计算场景。
- 特点:多个线程相互等待,达到共同状态后再继续执行。
相关概念与对比
volatitle底层原理
volatile 是 Java 中的一个关键字,用于修饰变量。它的主要作用是确保多个线程对该变量的可见性和有序性,即保证线程间的内存可见性和禁止指令重排序。
当一个变量被声明为 volatile 后,对该变量的读操作和写操作都会直接从主内存中进行,而不会从线程的工作内存中读取或写入。这样可以确保不同线程之间对该变量的读写操作是一致的。
volatile 的原理可概括如下:
- 可见性:使用 volatile 修饰的变量在被写操作修改后,会立即写回主内存,并使其他线程能够 立即看到这个修改。这样就避免了线程之间对变量读取脏数据的情况。
- 禁止指令重排序:volatile 关键字会禁止指令的重排序优化,保证指令按照程序的顺序执行。这样可以避免由于指令重排序导致的执行结果与预期不一致的问题。 需要注意的是,虽然 volatile 能够保证可见性和禁止指令重排序,但它并不能保证原子性。对于复 合操作(如 i++)等非原子性操作,仍然需要通过加锁等其他方式来保证线程安全。
总结起来,volatile 关键字通过保证内存可见性和禁止指令重排序来确保多线程环境下对变量的读写 操作的一致性。使用 volatile 可以在一些特定场景下提供简单且高效的线程安全保障,但在复杂的 并发操作中,可能需要结合其他机制来实现更强大的线程安全性。
volatile和synchronized的区别是什么?
- volatile只能使用在变量上;而synchronized可以在类,变量,方法和代码块上。
- volatile保证可见性;synchronized保证原子性与可见性。
- volatile禁用指令重排序;synchronized不会。
- volatile不会造成阻塞;synchronized会。
AQS与CAS
AQS(AbstractQueuedSynchronizer): AQS是一个抽象类,是JUC(Java Util Concurrent)包中提供的用于构建锁和其他同步器的框架。它使用了一个双向队列(等待队列)来管理线程的等待和唤醒,通过继承和实现AQS类的方式来实现不同类型的同步器(如ReentrantLock、CountDownLatch等)。 AQS的关键方法包括:
- acquire():尝试获取同步状态(state),如果获取失败则将当前线程加入等待队列并阻塞。
- release():释放同步状态,并唤醒等待队列中的线程。 AQS的底层使用了CAS操作来实现对同步状态的原子更新,保证线程之间的互斥和同步。
CAS(Compare and Swap): CAS是一种非阻塞算法,用于实现并发环境下的原子操作。它基于硬件的原子指令,通过比较内存中的值与期望值是否相等,如果相等则将新值写入内存,否则重新尝试。CAS操作包含三个操作数:内存地址(或称为变量的偏移量),期望值和新值。 CAS的关键方法包括:
- compareAndSet():比较内存中的值与期望值,如果相等则将新值写入内存,返回更新是否成功。
- getAndAdd():从内存中取出当前值,并增加指定的增量,返回旧值。 CAS操作具有原子性,不需要加锁,可以避免使用锁时可能带来的性能损失和线程阻塞。
CAS存在的问题
- ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但 是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变 化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时 候都把版本号加一,这样变化过程就从 A-B-A 变成了 1A-2B-3A 。 JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,原子更新带有版本号的引用类 型。
- 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
- 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对 多个共享变量操作时,CAS是无法保证操作的原子性的。 Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在 一个对象里来进行CAS操作。
ReentrantLock和synchronized区别
- 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁,而ReentrantLock需要手动释放锁。
- synchronized是非公平锁,ReentrantLock可以设置为公平锁。
- ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。
- ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。
- ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。
ReentrantLock底层原理
- 原子操作CAS: ReentrantLock使用了CAS操作来实现对同步状态的原子更新。在ReentrantLock中,同步状态是由一个int类型的成员变量state表示,在多线程访问时需要进行原子性操作。
- AQS框架: ReentrantLock继承了AQS抽象类,使用AQS的等待队列来管理线程的等待和唤醒。在AQS框架中,同步状态的改变被分为两个部分:获取同步状态和释放同步状态。
- 获取同步状态: 当一个线程尝试获取锁时,会调用ReentrantLock的lock()方法。在该方法中,首先会尝试使用CAS操作将同步状态从0修改为1,如果修改成功则表示获取锁成功,否则会将当前线程加入等待队列中。如果等待的时候被中断了,会通过LockSupport.park()方法挂起线程,等待获取锁的信号。当其他线程释放了锁的时候,等待队列中的线程会被重新唤醒,从而继续尝试获取锁。
- 释放同步状态: 当一个线程释放锁时,会调用ReentrantLock的unlock()方法。在该方法中,首先会使用CAS操作将同步状态从1修改为0,表示释放了锁。然后会将等待队列中的第一个节点(即等待时间最长的节点)唤醒,使其可以继续尝试获取锁。如果等待队列中还有其他线程在等待,则这些线程会按照FIFO的顺序依次被唤醒。
- 可重入特性: ReentrantLock支持同一个线程多次获取锁,即可重入特性。在ReentrantLock内部,每个线程都有一个记录锁持有次数的计数器,当线程第一次获取锁时,计数器加1,每次重入锁时,计数器再加1,当计数器为0时,表示锁已完全释放。
上述lock()的具体实现逻辑
- 当一个线程调用Lock.lock()时,它会尝试去获取锁。如果锁当前没有被其他线程持有,那么该线程立即获取到锁,并将锁的持有计数设置为1,然后继续执行临界区代码。
- 如果锁已经被其他线程持有,那么调用Lock.lock()的线程会进入阻塞状态,直到锁可用。当锁可用时,该线程再次尝试获取锁。
- 如果多个线程同时调用Lock.lock(),但只有一个线程能够成功获取到锁,其他线程将会进入阻塞状态。
- 当一个线程成功获取到锁后,在执行完临界区代码后,需要调用Lock.unlock()释放锁。这样可以让其他等待获取锁的线程获得执行的机会。
- 在使用Lock.lock()获取锁时,可以选择超时时间或者中断响应的机制,以便更好地控制和管理锁的获取。
ConcurrentHashMap底层原理
- 分段锁(Segment): ConcurrentHashMap内部采用了分段锁的机制来实现并发操作。整个哈希表被分成多个段(Segment),每个段维护着一部分键值对。每个段都拥有自己的锁,不同的段可以被不同的线程同时访问,从而实现了并发的读写操作。
- 哈希桶数组: ConcurrentHashMap内部使用一个哈希桶数组来存储键值对。这个数组的每个元素都是一个链表或红黑树,用于解决哈希冲突。每个段(Segment)维护着一部分哈希桶,通过哈希算法将键值对分配到相应的段中。
- 锁粒度: ConcurrentHashMap的锁粒度是基于段(Segment)的,即对于每个段都有一个独立的锁。这样不同的线程可以同时访问不同的段,提高了并发性能。只有当多个线程同时访问同一个段时,才需要竞争段级别的锁。
- 并发读操作: ConcurrentHashMap的并发读操作是无锁的,多个线程可以同时进行读取操作而不会发生冲突。这是因为每个段(Segment)内部的链表或红黑树是线程安全的,只需要保证在读取过程中不修改数据即可。
- 并发写操作: ConcurrentHashMap的并发写操作是基于段级别的锁来实现的。当一个线程进行写操作时,只需要加锁对应的段,其他线程仍然可以并发进行读操作和写操作(只要操作的段不冲突)。这样可以提高并发性能,减少锁竞争的范围。
使用volatitle关键字双重检查锁的懒汉单例
public class LazyDoubleCheck{
private volatitle state LazyDoubleCheck instance;
private LazyDoubleCheck(){
}
public static LazyDoubleCheck getInstance(){
if(instance == null){
synchronized (LazyDoubleCheck.class){
if(instance == null){
instance = new LazyDoubleCheck();
}
}
}
return instace;
}
}11. JVM
基础知识
JVM内存结构
JVM内存结构 = 类加载器 + 执行引擎 + 运行时数据区域

程序计数器
- 当前线程所执行的字节码的行号指示器,通过改变它实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
- 程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
虚拟机栈:
- Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用结束后,都会有一个栈帧被弹出。
- Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。Java 虚拟机栈会出现两种错误: StackOverFlowError 和 OutOfMemoryError 。
- 本地方法栈:虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。Native 方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
- 堆:此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆。Java 堆可以细分为:新生代(Eden 空间、From Survivor、To Survivor 空间)和老年代。进一步划分的目的是更好地回收内存,或者更快地分配内存。
- 方法区:方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区逻辑上属于堆的一部分。
- 运行时常量池:运行时常量池是方法区的一部分,在类加载之后,会将编译器生成的各种字面量和符号引号放到运行时常量池。在运行期间动态生成的常量,如 String 类的 intern()方法,也会被放入运行时常量池。
- 直接内存:直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。直接内存的读写操作比堆内存快,可以提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到直接内存。
堆和栈的区别
存储内容:
- 堆(Heap):堆用于存储对象实例和数组等动态分配的数据。在Java程序中,所有通过关键字
new创建的对象都会在堆上分配内存。堆是线程共享的,多个线程可以同时访问和修改堆中的对象。 - 栈(Stack):栈用于存储线程执行方法的调用帧(包括局部变量、方法参数、返回地址等)。每个线程都会有自己的栈,栈是线程私有的。栈中的数据随着方法的调用和返回而动态地入栈和出栈。
- 堆(Heap):堆用于存储对象实例和数组等动态分配的数据。在Java程序中,所有通过关键字
内存管理:
- 堆:堆的内存空间由JVM自动进行管理,包括自动分配和回收。在对象不再被引用时,垃圾回收器会自动回收堆中的内存,以便下次的对象分配使用。
- 栈:栈的内存空间由编译器自动进行管理,包括方法的入栈和出栈。栈上的数据是临时的,方法执行完毕后会立即释放,不需要垃圾回收器来回收。
内存分配:
- 堆:堆上的内存分配由垃圾回收器负责,采用动态分配的方式。当对象被创建时,垃圾回收器会在堆上找到足够大的连续内存空间来存储对象,并返回对象的引用。
- 栈:栈上的内存分配是通过编译器进行静态分配的。编译器在编译阶段确定每个方法的栈帧大小,根据方法的调用关系预先分配好栈帧所需的内存空间。
大小限制:
- 堆:堆的大小是通过JVM参数来配置的,例如
-Xmx和-Xms参数可以分别设置堆的最大和初始大小。堆的大小一般比较大,取决于可用的物理内存。 - 栈:栈的大小是固定的,取决于JVM规范或操作系统的限制。栈的大小主要受限于操作系统的栈大小限制。
- 堆:堆的大小是通过JVM参数来配置的,例如
栈内存溢出的情况
当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常。通过调整参数 -xss 可以调整JVM栈的大小。
类加载与加载过程
类的加载指的是将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个对象,这个对象封装了类在方法区内的数据结构,并且提供了访问方法区内的类信息的接口。
- 加载:在Java虚拟机(JVM)中寻找并加载需要运行的类。类加载器会根据类的全限定名查找类的字节码文件,并将其加载到JVM中。
- 验证:验证所加载的类是否符合Java虚拟机规范,例如是否有正确的Class文件格式、是否有不被允许的操作等。
- 准备:为类的静态变量分配内存空间,并为其设置默认值。
- 解析:将类的二进制数据中的符号引用转化为直接引用的过程。其中符号引用是一种用来描述所引用的目标的名称。直接引用则是内存地址。
- 初始化:执行类的初始化代码,包括静态变量赋值和static块中的代码执行等。
- 使用:使用加载后的类,进行对象的实例化、方法的调用等操作。
- 卸载:当类不再被使用时,从JVM中卸载该类。
双亲委派模型与作用
一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
作用:可以防止内存中出现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证。
类加载器和分类
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
- 启动类加载器:用来加载 Java 核心类库,无法被 Java 程序直接引用。
- 扩展类加载器:它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器:它根据应用的类路径来加载 Java 类。可通过 ClassLoader.getSystemClassLoader()来获取它。
- 用户自定义类加载器:通过继承 java.lang.ClassLoader类的方式实现。
类的实例化顺序
- 父类静态成员初始化和静态代码块:如果当前类有父类,那么会先执行父类的静态成员初始化和静态代码块。
- 子类静态成员初始化和静态代码块:接着执行子类的静态成员初始化和静态代码块。
- 父类成员变量的初始化和普通代码块:父类的成员变量按照定义的顺序进行初始化,同时会执行普通代码块。
- 父类构造方法:执行父类的构造方法,完成父类的实例化。
- 子类成员变量的初始化和普通代码块:子类的成员变量按照定义的顺序进行初始化,同时会执行普通代码块。
- 子类构造方法:执行子类的构造方法,完成子类的实例化。
类被卸载的情况
需要同时满足下面 3 个条件才算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
对象创建过程
- 类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
- 初始化零值。分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
- 设置对象头。Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据 (哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。
- 执行init方法。按照Java代码进行初始化。
强引用、软引用、弱引用、虚引用是什么,有什么区别?
- 强引用:垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
- 软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
- 弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
- 虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。
如何判断一个对象是否存活
- 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。这种方法很难解决对象之间相互循环引用的问题。
- 可达性分析:通过GC Root对象为起点,从这些节点向下搜索,搜索所走过的路径叫引用链,当一个对象到GC Root没有任何的引用链相连时,说明这个对象是不可用的。
可以作为GC Roots的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁(synchronized关键字)持有的对象。
Minor GC 和 Full GC
- Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。 (当 新生代Eden 空间满时,就将触发一次 Minor GC)
- Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
Full GC 的触发条件:
- 调用 System.gc():只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。
- 老年代空间不足:老年代空间不足的常见场景为大对象直接进入老年代、长期存活的对象进入老年代等
- 空间分配担保失败:使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
内存分配策略
- 对象优先在 Eden 分配:大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
- 大对象直接进入老年代:大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。可以设置JVM参数
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。 - 长期存活的对象进入老年代:通过参数
-XX:MaxTenuringThreshold可以设置对象进入老年代的年龄阈值。对象在 Survivor 中每经过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度,就会被晋升到老年代中。 - 动态对象年龄判定:虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
- 空间分配担保:在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败。如果允许,那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
垃圾回收
垃圾回收算法
- 标记清除算法:标记清除算法就是分为“标记”和“清除”两个阶段。标记出所有需要回收的对象,标记结束后统一回收所有被标记的对象。这种垃圾回收算法效率较低,并且会产生大量不连续的空间碎片。
- 复制清除算法:半区复制,用于新生代垃圾回收。将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。实现简单,运行高效,但可用内存缩小为了原来的一半,浪费空间。
- 标记整理算法:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。提高了内存分配的效率,但暂停的时间仍然较长
- 分代收集算法:新生代使用复制算法进行垃圾回收,因为新生代中的对象往往具有较短的生命周期。而老年代使用标记-整理算法或标记-清除算法进行垃圾回收,因为老年代中的对象存活时间更长,且较稳定。通过分代收集,可以针对不同对象特性采用合适的垃圾收集算法,提高垃圾回收的效率。
垃圾回收器

Serial收集器
单线程收集器,使用一条垃圾收集线程去完成垃圾收集工作,在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。
特点:简单高效;内存消耗最小;没有线程交互的开销,单线程收集效率高;需暂停所有的工作线程,用户体验不好。

ParNew收集器
Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收 策略等等)和 Serial 收集器完全一样

Parallel Scavenge收集器
新生代收集器,基于复制清除算法实现的收集器。吞吐量优先收集器,也是能够并行收集的多线程收集器,允许多个垃圾回收线程同时运行,降低垃圾收集时间,提高吞吐量。
相比ParNew收集器,Parallel Scavenge 的优点:
- 精确控制吞吐量;
- 垃圾收集的自适应的调节策略。通过参数-XX:+UseAdaptiveSizePolicy 打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或者最大的吞吐量。
Serial Old收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。多线程垃圾收集,使用标记-整理算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS收集器
Concurrent Mark Sweep 并发标记清除,目的是获取最短应用停顿时间。第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程基本上同时工作。在并发标记和并发清除阶段,虽然用户线程没有被暂停,但是由于垃圾收集器线程占用了一部分系统资源,应用程序的吞吐量会降低。

执行过程
- 初始标记: stw(Stop-The-World)暂停所有的其他线程,记录直接与 gc root 直接相连的对象,速度很快。
- 并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长,但是不需要停顿用户线程。
- 重新标记: 在并发标记期间对象的引用关系可能会变化,需要重新进行标记。此阶段也会stw,停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
- 并发清除:清除死亡对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
- 优点:并发收集,低停顿。
缺点:
- 标记清除算法导致收集结束有大量空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
- 会产生浮动垃圾,由于CMS并发清理阶段用户线程还在运行着,会不断有新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好等到下一次GC去处理;
- 对处理器资源非常敏感。在并发阶段,收集器占用了一部分线程资源,导致应用程序变慢,降低总吞吐量。
CMS垃圾回收特点
- CMS只会回收老年代和永久代(1.8开始为元数据区,需要设置CMSClassUnloadingEnabled),不会收集年轻代;
- CMS垃圾回收器开始执行回收操作,有一个触发阈值,默认是老年代或永久带达到92%,不能等到 old内存用尽时回收,否则会导致并发回收失败。因为需要预留空间给用户线程运行。
G1收集器
G1垃圾收集器的目标是用在多核、大内存的机器上,在不同应用场景中追求高吞吐量和低停顿之间的最佳平衡。
G1可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式
G1将整个堆分成相同大小的分区(Region),有四种不同类型的分区:Eden、Survivor、Old和Humongous(大对象)。分区的大小取值范围为1M到32M,都是2的幂次方。Region大小可以通过 -XX:G1HeapRegionSize 参数指定。Humongous区域用于存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。

G1 收集器对各个Region回收所获得的空间大小和回收所需时间的经验值进行排序,得到一个优先级列表,每次根据用户设置的最大的回收停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200 毫秒),优先处理回收价值最大的 Region。
Java堆分成多个独立Region,Region里面会存在跨Region引用对象,在垃圾回收寻找GC Roots需要扫描整个堆。G1采用了Rset(Remembered Set)来避免扫描整个堆。每个Region会有一个RSet,记录了哪些Region引用本Region中对象,即谁引用了我的对象,这样的话,在做可达性分析的时候就可以避免全堆扫描。
- 回收特点:可以由用户指定期望的垃圾收集停顿时间。
回收过程
- 初始标记:stw暂停所有的其他线程,记录直接与 gc root 直接相连的对象,速度很快。
- 并发标记。从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
- 最终标记。对用户线程做另一个短暂的暂停,用于处理并发阶段对象引用出现变动的区域。
- 筛选回收。对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
优点:
- 低延迟:G1回收器的主要目标是降低垃圾回收期间的停顿时间。它将堆内存划分为多个区域(Region),并采用并发标记、整理和回收的策略,以减小STW暂停的时间,实现更平均的GC停顿。
- 内存管理灵活:G1可以根据应用程序的内存使用情况,智能地动态调整每个区域的大小,避免了传统的固定大小的年轻代和老年代。这使得G1对于大内存应用程序具有较好的适应性。
- 高吞吐量:尽管G1的主要目标是降低暂停时间,但它仍然能够提供相对较高的吞吐量。G1通过并行和并发执行,以及利用多个处理器核心来执行垃圾回收操作,从而提高整体的吞吐量。
缺点:
- 对CPU资源要求较高:G1回收器需要更多的CPU资源来执行垃圾回收操作。因为它需要同时进行并发标记、整理和回收操作,可能会占用较多的CPU时间片。
- 垃圾回收算法复杂:G1回收器的垃圾回收算法相对较为复杂。它需要维护大量的内部数据结构,并进行动态的区域划分和优先级排序等操作,这可能导致一定的系统开销。
- 可能产生全局停顿:在某些情况下,G1回收器可能出现全局停顿(Full GC)。当老年代中的垃圾比例较高时,G1可能无法找到足够的连续空间来分配新对象,从而触发全局停顿并进行整理操作。
ZGC(The Z Garbage Collector)收集器
- 启用参数:
-XX:+UseZGC - 目标:一款可扩展的低延迟回收器,停顿时间不超过10毫秒,且停顿时间不会随堆大小增长而显著增加(TB级堆内存下依然保持低停顿)。
核心技术:
- 着色指针:不再将GC元数据(如标记状态、重映射状态)保存在对象头里,而是直接编码到对象的引用指针中,这使得ZGC在无需访问对象本身的情况下,仅通过指针就能快速做出GC相关决策,效率极高。
- 读屏障:这是一个由JIT编译器插入到应用程序代码中的一小段逻辑,当线程从堆中加载对象引用时便会触发。它的关键作用在于,在并发转移阶段,如果应用线程试图访问一个已被移动的对象,读屏障能拦截这个请求,并根据转发表将指针"自愈"到新地址,确保应用总是拿到有效的引用
- 工作方式:ZGC的回收过程可概括为四个主要的并发阶段:并发标记 -> 并发预备重分配 -> 并发重分配 -> 并发重映射。整个过程仅在开始时(扫描GC Roots)等极少数环节需要短暂的停顿,且停顿时间仅与GC Roots数量有关,与堆大小无关
- 优点:亚毫秒到十毫秒级别的极低停顿,处理超大堆能力强劲。
- 缺点:在JDK 11早期是实验版,吞吐量略低于G1。每次回收都要扫描全堆,这在对象生命周期差异明显的场景下效率不高
- 发展:在JDK 15中转为正式功能,性能持续优化。
Generational ZGC(分代式ZGC)
- 启用参数:
-XX:+UseZGC -XX:+ZGenerational - 设计思想:将ZGC与分代理论相结合。它维护了独立的年轻代和老年代。核心优势基于弱分代假说:即绝大多数对象都是"朝生夕死"的
核心技术:
- 无色指针与记录集:由于需要区分的状态更多,传统的染色指针和多重映射技术不再适用。Generational ZGC引入了无色指针,并通过读屏障和写屏障来维护两个记录集,用于高效追踪从老年代指向新生代的引用,从而在Minor GC时避免扫描整个老年代。
- Dense Heap Regions机制:在回收新生代时,Generational ZGC会分析页面内对象的存活情况和转移成本。对于存活对象分布稀疏、转移性价比不高的页面,会采用整页晋升的策略,直接将整个页面划入老年代,从而减少不必要的复制开销
优势:
- 显著降低分配停顿:年轻代的回收非常快速。
- 减少内存开销:分代后,ZGC所需的内存屏障开销和内存映射压力都大大降低。
- 提高吞吐量:避免了不必要的老年对象扫描。
- 意义:这是ZGC的下一代形态,结合了分代收集的效率和并发收集的低延迟优势,旨在未来成为统一的、高性能的默认回收器。在JDK 21中,它已结束实验阶段,成为正式功能。

计算机网络
1.TCP/IP 五层模型
- 应用层:为应用程序提供交互服务。在互联网中的应用层协议很多,如域名系统DNS、HTTP协议、SMTP协议等。
- 传输层:负责向两台主机进程之间的通信提供数据传输服务。传输层的协议主要有传输控制协议TCP和用户数据协议UDP。
- 网络层:选择合适的路由和交换结点,确保数据及时传送。主要包括IP协议。
- 数据链路层:在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧
- 物理层:实现相邻节点间比特流的透明传输,尽可能屏蔽传输介质和物理设备的差异。
2.TCP两次握手、三次握手、四次挥手详细流程
两次握手(Two-way Handshake):
- 第一步:客户端向服务器发送一个 SYN(同步)包,将自己的初始序列号设置为 x。
- 第二步:服务器收到 SYN 包后,回复一个 ACK(确认)包,将自己的初始序列号设置为 y,并将确认序号设置为 x+1。
三次握手(Three-way Handshake):(主要为了防止已失效的连接请求报文段突然又传输到了服务端,导致产生问题。)
- 第一步:客户端向服务器发送一个 SYN(同步)包,将自己的初始序列号设置为 x。
- 第二步:服务器收到 SYN 包后,回复一个 SYN+ACK 包,将自己的初始序列号设置为 y,并 将确认序号设置为 x+1,同时将自己的 SYN 标志位置位。
- 第三步:客户端收到 SYN+ACK 包后,回复一个 ACK 包,将确认序号设置为 y+1,同时将自 己的 SYN 标志位置位。
四次挥手(Four-way Handshake):
- 第一步:客户端发送一个 FIN(结束)包,表示自己已经完成数据发送。
- 第二步:服务器收到 FIN 包后,回复一个 ACK 包,确认收到客户端的 FIN 包。
- 第三步:服务器发送一个 FIN 包,表示自己已经完成数据发送。
- 第四步:客户端收到服务器的 FIN 包后,回复一个 ACK 包,确认收到服务器的 FIN 包。
整个过程可以简化为:
客户端 --> (SYN, x) --> 服务器
客户端 <-- (SYN, y) (ACK, x+1) <-- 服务器
客户端 --> (ACK, y+1) --> 服务器
3. 第四次挥手为什么要等待2MSL?
保证A发送的最后一个ACK报文段能够到达B。这个 ACK 报文段有可能丢失,B收不到这个确认报文,就会超时重传连接释放报文段,然后A可以在 2MSL 时间内收到这个重传的连接释放报文段,接 着A重传一次确认,重新启动2MSL计时器,最后A和B都进入到 CLOSED 状态,若A在 TIME-WAIT 状 态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到B重传的连接释放报文 段,所以不会再发送一次确认报文段,B就无法正常进入到 CLOSED 状态。
防止已失效的连接请求报文段出现在本连接中。A在发送完最后一个 ACK 报文段后,再经过2MSL, 就可以使这个连接所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现旧的连接请 求报文段。
4.TCP和UDP的区别
- TCP面向连接;UDP是无连接的,即发送数据之前不需要建立连接。
- TCP提供可靠的服务;UDP不保证可靠交付。
- TCP面向字节流,把数据看成一连串无结构的字节流;UDP是面向报文的。
- TCP有拥塞控制;UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如实时视频会议等)。
- 每一条TCP连接只能是点到点的;UDP支持一对一、一对多、多对一和多对多的通信方式。
- TCP首部开销20字节;UDP的首部开销小,只有8个字节。
5.HTTPS与HTTP的区别?
- HTTP是超文本传输协议,信息是明文传输;HTTPS则是具有安全性的ssl加密传输协议。
- HTTP和HTTPS用的端口不一样,HTTP端口是80,HTTPS是443。
- HTTPS协议需要到CA机构申请证书,一般需要一定的费用。
- HTTP运行在TCP协议之上;HTTPS运行在SSL协议之上,SSL运行在TCP协议之上。
6.HTTP2.0的新特性
- 新的二进制格式:HTTP1.1 基于文本格式传输数据;HTTP2.0采用二进制格式传输数据,解析更高效。
- 多路复用:在一个连接里,允许同时发送多个请求或响应,并且这些请求或响应能够并行的传输而不被阻塞,避免 HTTP1.1 出现的”队头堵塞”问题。
- 头部压缩,HTTP1.1的header带有大量信息,而且每次都要重复发送;HTTP2.0 把header从数据中分离,并封装成头帧和数据帧,使用特定算法压缩头帧,有效减少头信息大小。并且 HTTP2.0在客户端和服务器端记录了之前发送的键值对,对于相同的数据,不会重复发送。比如请求a发送了所有的头信息字段,请求b则只需要发送差异数据,这样可以减少冗余数据,降低开销。
- 服务端推送:HTTP2.0允许服务器向客户端推送资源,无需客户端发送请求到服务器获取。
7.TCP可靠传输保证机制
- 序列号和确认:TCP 使用序列号来对每个传输的数据进行编号,并且使用确认机制确保对方接收到数据。接收方会发送确认信息来告知发送方已经成功接收到数据,如果发送方在一定时间内没有收到确认信息,则会重新发送数据。
- 超时重传:TCP 使用超时重传机制来应对可能发生的数据丢失。当发送方发送数据后,会启动一个定时器,如果在规定的时间内没有收到确认信息,就会认为数据丢失并重新发送数据。这样可以确保数据的可靠传输。
- 滑动窗口:TCP 使用滑动窗口机制来实现流量控制和拥塞控制。滑动窗口定义了发送方可以发送多少个字节的数据,接收方通过确认信息告知发送方窗口大小,从而实现发送和接收之间的平衡,避免数据拥塞和丢失。
- 确认丢失的重传:如果发送方发送的确认信息没有及时到达接收方,发送方会认为确认信息丢失,并进行重传。这样可以确保对方正确接收到确认信息,进而保证数据的可靠传输。
- 拥塞控制:TCP 使用拥塞控制机制来避免网络拥塞。通过动态调整发送窗口大小、拥塞窗口大小等参数,TCP 可以根据网络状况来控制发送的数据量,防止网络拥塞和数据丢失。
MySql
1.事务的四大特性
- 原子性(Atomicity):事务包含的所有操作要么全部成功,要么全部失败回滚。
- 一致性(Consistency):一个事务执行之前和执行之后都必须处于一致性状态。比如a与b账户共有1000块,两人之间转账之后无论成功还是失败,它们的账户总和还是1000。
- 隔离性(Isolation):跟隔离级别相关,如read committed,一个事务只能读到已经提交的修改。
- 持久性(Durability):一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库 系统遇到故障的情况下也不会丢失提交事务的操作。
2.隔离级别以及或出现的问题
- 读未提交(Read Uncommitted): 最低的隔离级别,允许一个事务读取另一个事务未提交的数据。 可能会导致脏读、不可重复读和幻读的问题。
- 读已提交(Read Committed): 允许一个事务只能读取另一个事务已提交的数据。 避免了脏读的问题,但可能仍会出现不可重复读和幻读的问题。
- 可重复读(Repeatable Read): 默认隔离级别,保证在同一事务中多次读取同一数据时,结果始终相同。 避免了脏读和不可重复读的问题,但可能仍会出现幻读的问题。
- 串行化(Serializable): 最高的隔离级别,通过锁定读取的数据来避免任何并发问题。 可以解决脏读、不可重复读和幻读的问题,但同时也导致了较低的并发性能。
不同隔离级别会带来不同的问题:
- 脏读(Dirty Read): 一个事务读取了另一个未提交的事务中的数据,如果未提交的事务回滚,则读取到的数据是无效的。
- 不可重复读(Non-repeatable Read): 一个事务内多次读取同一数据,但在其间可能有其他事务对该数据进行了修改,导致不一致的读取结果。
- 幻读(Phantom Read): 一个事务内多次查询同一个范围的数据,但在其间有其他事务插入了新的符合条件的数据,导致后续的查询结果不一致。
3.索引
索引的优缺点
优点
- 加快数据查找的速度
- 为用来排序或者是分组的字段添加索引,可以加快分组和排序的速度
- 加速表与表之间的连接
缺点
- 建立索引需要占用物理空间
- 会降低表的增删改的效率,因为每次对表记录进行增删改,需要进行动态维护索引,导致增删改时间变长
需要建索引以及不需要建索引的情况
需要建索引
- 经常用于查询的字段
- 经常用于连接的字段(如外键)建立索引,可以加快连接的速度
- 经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度
不需要建索引
- where条件中用不到的字段不适合建立索引
- 表记录较少
- 需要经常增删改
- 参与列计算的列不适合建索引
- 区分度不高的字段不适合建立索引,性别等
索引数据结构
- B树索引(B-tree Index): B树索引是MySQL中最常用的索引类型。 B树索引采用平衡树的结构,适用于范围查询和精确匹配。 每个节点可以包含多个键值对,子节点按键值排序存储,实现高效的查找和插入。
- B+树索引(B+ Tree Index):B+树索引也是常见的索引类型,类似于B树索引。 B+树索引在B树索引基础上做了优化,将索引中的键值只存储在叶子节点上,非叶子节点只存储键值的引用(指针),提高了查询效率。 B+树索引适合大部分数据库的索引需求,并且在范围查询和顺序访问时表现更好。
- 哈希索引(Hash Index): 哈希索引适用于等值查询,通过哈希函数计算存储位置,无需遍历整个索引树。 哈希索引在内存中进行查找,速度非常快,但不支持范围查询和排序操作。 MySQL中的哈希索引适用于Memory存储引擎和Memory表类型。
- 全文索引(Full-text Index): 全文索引用于全文搜索,适用于对文本内容进行模糊匹配的操作。 全文索引不是基于B树或哈希的结构,而是采用倒排索引(Inverted Index)技术。 MySQL中的全文索引适用于InnoDB和MyISAM存储引擎,并提供了多种全文搜索函数和查询语法。
哈希索引与B+数索引的区别
- 哈希索引不支持排序,因为哈希表是无序的。
- 哈希索引不支持范围查找。
- 哈希索引不支持模糊查询及多列索引的最左前缀匹配。
- 因为哈希表中会存在哈希冲突,所以哈希索引的性能是不稳定的,而B+树索引的性能是相对稳定的,每次查询都是从根节点到叶子节点。
B+树比B树更适合实现数据库索引的原因
- 范围查询效率更高:B+树索引在叶子节点上存储了所有的键值对,而非叶子节点只存储键值的引用(指针)。这种结构使得在B+树上进行范围查询非常高效,因为可以通过顺序遍历叶子节点来获取连续的值。相比之下,B树索引需要遍历整个树来找到满足范围条件的数据。
- 支持更大的索引:由于B+树索引将所有的键值对存储在叶子节点上,非叶子节点只存储引用,这样就能够存储更多的叶子节点。相比之下,B树索引需要在每个节点上存储键值对,因此在相同的存储空间下,B+树索引可以容纳更多的数据。
- 适合顺序访问:B+树索引的叶子节点通过链表顺序连接在一起,这样可以快速进行顺序遍历,例如范围查询、分页查询等。而B树索引则需要通过指针进行跳转,访问顺序不连续,效率较低。
- 更好的磁盘IO利用率:B+树索引在磁盘上的数据分布更加紧凑,相比之下B树索引可能会导致频繁 的磁盘IO操作。由于B+树索引叶子节点的数据是顺序存储的,可以减少随机IO读取的次数,提高磁 盘IO的效率。
索引分类
- 主键索引:每个表只能有一个主键索引,用于唯一标识表中的每一行数据,不允许有空值。
- 唯一索引:确保每一列的值都是唯一的,但是可以有空值。一个表可以有多个唯一索引。
- 普通索引:最基本的索引类型,没有特殊限制。
- 全文索引:用于在文本数据中搜索关键字,支持自然语言查询,但是只能针对MyISAM引擎的表使用。
- 组合索引:将多个列作为索引的组合,可加快多列条件查询的速度,但是顺序很重要。
- 空间索引:用于处理地理空间数据,支持点、线、面等类型的查询,但是只能针对MyISAM引擎的表使用。
聚簇索引和非聚簇索引
- 聚簇(cu)索引(Clustered Index): 聚簇索引是一种特殊的索引类型,其叶子节点的顺序与数据行的物理存储顺序相同。 对于使用聚簇索引的表,表中的数据行按照聚簇索引的键值进行排序,并直接存储在索引的叶子节点中。 表只能有一个聚簇索引,它可以加速范围查询、顺序访问和覆盖查询。同时,聚簇索引也对表的插入、更新和删除操作产生影响,因为数据的物理存储顺序可能需要变动。
- 非聚簇索引(Non-clustered Index): 非聚簇索引将索引的键值与对应的数据行的物理存储位置进行映射,而数据行的物理存储顺序与索引的顺序无关。 对于非聚簇索引的表,表中的数据行按照其在表中的物理存储顺序排列,而索引则指向这些数据行的位置。 表可以有多个非聚簇索引,它们可以加速等值查询和覆盖查询,但对范围查询和顺序访问的性能影响较小。非聚簇索引的叶子节点包含了指向对应数据行的引用,通过这些引用可以进一步获取需要的数据。
在选择使用聚簇索引还是非聚簇索引时,需考虑以下因素:
- 聚簇索引适合对范围查询和顺序访问的性能要求较高的表,但会影响插入、更新和删除操作的效率。
- 非聚簇索引适合对等值查询进行优化,而且可以在同一个表上创建多个非聚簇索引。
索引的设计原则
- 索引列的区分度越高,索引的效果越好。比如使用性别这种区分度很低的列作为索引,效果就会很差。
- 尽量使用短索引,对于较长的字符串进行索引时应该指定一个较短的前缀长度,因为较小的索引涉及到的磁盘I/O较少,并且索引高速缓存中的块可以容纳更多的键值,会使得查询速度更快。
- 索引不是越多越好,每个索引都需要额外的物理空间,维护也需要花费时间。
- 利用最左前缀原则。
索引失效的情况
- 对于组合索引,不是使用组合索引最左边的字段,则不会使用索引
- 以%开头的like查询如 %abc ,无法使用索引;非%开头的like查询如 abc% ,相当于范围查询,会使用索引
- 查询条件中列类型是字符串,没有使用引号,可能会因为类型不同发生隐式转换,使索引失效
- 判断索引列是否不等于某个值时
- 对索引列进行运算
- 查询条件使用or连接,也会导致索引失效
4.存储引擎
常见存储引擎
InnoDB:InnoDB是MySQL默认的事务型存储引擎,使用最广泛,基于聚簇索引建立的。InnoDB内部做了很多优化,如能够自动在内存中创建自适应hash索引,以加速读操作。
- 优点:支持事务、行级锁和外键,具备高并发读写能力和数据完整性,可靠性较高。
- 缺点:相比其他存储引擎,占用的磁盘空间稍大,读写效率略低。
- 适用场景:适合于对事务支持要求高、并发读写频繁、数据完整性重要的应用,如电子商务、社交网站等。
MyISAM:数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,可以使用MyISAM引擎。 MyISAM会将表存储在两个文件中,数据文件.MYD和索引文件.MYI。
- 优点:读取速度快,索引效率高,适用于读密集型应用,对于静态或者不需要频繁修改的数据表来说,性能较好。
- 缺点:不支持事务和行级锁,容易出现数据不一致的情况,对并发写入不友好。
- 适用场景:适合于以查询为主、并发写入较少的应用场景,如报表生成、日志记录等。
MEMORY:MEMORY引擎将数据全部放在内存中,访问速度较快,但是一旦系统奔溃的话,数据都会丢失。MEMORY引擎默认使用哈希索引,将键的哈希值和指向数据行的指针保存在哈希索引中。
- 优点:读写速度非常快,适用于临时数据、缓存数据等存储需求。
- 缺点:数据存储在内存中,断电或重启会导致数据丢失,数据容量受限。
- 适用场景:适合于临时表、缓存数据、计数器等不需要持久化的数据存储。
Archive:该存储引擎非常适合存储大量独立的、作为历史记录的数据。ARCHIVE提供了压缩功能,拥有高效的插入速度,但是这种引擎不支持索引,所以查询性能较差。
- 优点:高压缩比,节省磁盘空间,适用于大规模历史数据的归档和查询。
- 缺点:不支持索引和更新操作,查询速度相对较慢。
- 适用场景:适合于只做查询的历史数据存储,如日志分析、存档数据。
MyISAM和InnoDB的区别
- 是否支持行级锁 : MyISAM 只有表级锁,而 InnoDB 支持行级锁和表级锁,默认为行级锁。
- 是否支持事务和崩溃后的安全恢复: MyISAM 注重性能,每次查询具有原子性,其执行速度比 InnoDB 类型更快,但是不提供事务支持。而 InnoDB 提供事务支持,具有事务、回滚和崩溃修复能力。
- 是否支持外键: MyISAM 不支持,而 InnoDB 支持。
- 是否支持MVCC : MyISAM 不支持, InnoDB 支持。应对高并发事务,MVCC比单纯的加锁更高效。
- MyISAM 不支持聚集索引, InnoDB 支持聚集索引。 MyISAM 引擎主键索引和其他索引区别不大,叶子节点都包含索引值和行指针。 innoDB 引擎二级索引叶子存储的是索引值和主键值(不是行指针),这样可以减少行移动和数据页分裂时二级索引的维护工作。
5.MVCC实现原理
MVCC( Multiversion concurrency control ) 就是同一份数据保留多版本的一种方式,进而实现并发控制。在查询的时候,通过read view和版本链找到对应版本的数据。
作用:提升并发性能。对于高并发场景,MVCC比行级锁更有效、开销更小。
实现原理:MVCC 的实现依赖于版本链,版本链是通过表的三个隐藏字段实现。
- DB_TRX_ID :当前事务id,通过事务id的大小判断事务的时间顺序。
- DB_ROLL_PRT :回滚指针,指向当前行记录的上一个版本,通过这个指针将数据的多个版本连接在一起构成undo log版本链。
- DB_ROLL_ID :主键,如果数据表没有主键,InnoDB会自动生成主键。
使用事务更新行记录的时候,就会生成版本链,执行过程如下:
- 用排他锁锁住该行;
- 将该行原本的值拷贝到 undo log,作为旧版本用于回滚;
- 修改当前行的值,生成一个新版本,更新事务id,使回滚指针指向旧版本的记录,这样就形成一条版本链。
可见性(read view)是指在数据库中,事务执行过程中所能够看到的数据版本。在MVCC机制下,通过事务开始时间和数据版本的时间戳来判断数据是否对事务可见。具体规则如下:
对于读操作:
- 如果数据版本的提交时间戳早于等于当前事务开始时间,则该版本对当前事务可见。
- 如果数据版本的提交时间戳晚于当前事务开始时间,则该版本对当前事务不可见。
对于写操作:
- 创建一个新的数据版本,并将该版本的时间戳设置为当前事务的提交时间戳。
- 新版本的数据对于后续开始的事务可见,但对于当前事务不可见(即读取的仍然是之前的版本)。
6.共享锁和排他锁
select * from table where id<6 lock in share mode;--共享锁
select * from table where id<6 for update;--排他锁这两种方式主要的不同在于 LOCK IN SHARE MODE 多个事务同时更新同一个表单时很容易造成死锁。
申请排他锁的前提是,没有线程对该结果集的任何行数据使用排它锁或者共享锁,否则申请会受到阻塞。在进行事务操作时,MySQL会对查询结果集的每行数据添加排它锁,其他线程对这些数据的更改或删除操作会被阻塞(只能读操作),直到该语句的事务被commit语句或rollback语句结束为止。
SELECT... FOR UPDATE 使用注意事项:
- for update 仅适用于Innodb,且必须在事务范围内才能生效。
- 根据主键进行查询,查询条件为 like或者不等于,主键字段产生表锁。
- 根据非索引字段进行查询,name字段产生表锁。
7.乐观锁和悲观锁
- 悲观锁:假定会发生并发冲突,在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制。
- 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否数据是否被修改过。给表增加version字段,在修改提交之前检查version与原来取到的version值是否相等,若相等,表示数据没有被修改,可以更新,否则,数据为脏数据,不能更新。实现方式:乐观锁一般使用版本号机制或CAS算法实现。
8.bin log/redo log/undo log
二进制日志(bin log)是MySQL数据库级别的文件,记录对MySQL数据库执行修改的所有操作,不会记录select和show语句,主要用于恢复数据库和同步数据库。
重做日志(redo log)是Innodb引擎级别,用来记录Innodb存储引擎的事务日志,不管事务是否提交都会记录下来,用于数据恢复。当数据库发生故障,InnoDB存储引擎会使用redo log恢复到发生故障前的时刻,以此来保证数据的完整性。将参数 innodb_flush_log_at_tx_commit 设置为1,那么在执行 commit时会将redo log同步写到磁盘。
回滚日志(undo log)除了记录redo log外,当进行数据修改时还会记录undo log,undo log用于数据的撤回操作,它保留了记录修改前的内容。通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的数据,实现MVCC。
9.MySql的架构
Service层:所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。
- 连接器: 当客户端连接 MySQL 时,server层会对其进行身份认证和权限校验。
- 查询缓存: 执行查询语句的时候,会先查询缓存,先校验这个 sql 是否执行过,如果有缓存这个sql,就会直接返回给客户端,如果没有命中,就会执行后续的操作。
- 分析器: 没有命中缓存的话,SQL 语句就会经过分析器,主要分为两步,词法分析和语法分析,先看 SQL 语句要做什么,再检查 SQL 语句语法是否正确。
- 优化器: 优化器对查询进行优化,包括重写查询、决定表的读写顺序以及选择合适的索引等,生成执行计划。
- 执行器: 首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限, 就会根据执行计划去调用引擎的接口,返回结果。
- 存储引擎:主要负责数据的存储和读取。server 层通过api与存储引擎进行通信。
10.查询语句的执行流程
- 权限校验:首先检查权限,没有权限则返回错误;
- 查询缓存:MySQL以前会查询缓存,缓存命中则直接返回,没有则执行下一步;
- 分析器:词法分析和语法分析。提取表名、查询条件,检查语法是否有错误;
- 优化器:优化器根据自己的优化算法选择执行效率最 好的方案;
- 权限校验:校验权限,有权限就调用数据库引擎接口,返回引擎的执行结果。
- 执行器
- 引擎
11.更新语句执行流程
- 分析器:先查询到 id 为1的记录,有缓存会使用缓存。
- 权限校验:拿到查询结果,将 name 更新为 大彬,然后调用引擎接口,写入更新数据,innodb 引擎将数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态。
- 执行器
- 引擎
- redo log(prepare 状态)
- binlog
- redo log(commit状态):执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为提交状态。
12.truncate、delete与drop区别?
相同点:
- truncate和不带where子句的delete、以及drop都会删除表内的数据。
- drop、truncate都是DDL语句(数据定义语言),执行后会自动提交。
不同点:
- truncate 和 delete 只删除数据不删除表的结构;drop 语句将删除表的结构被依赖的约束、触发器、索引;
- 一般来说,执行速度: drop > truncate > delete。
13.三范式
| 范式名称 | 核心要求 | 要解决的主要问题 |
|---|---|---|
| 第一范式 (1NF) | 字段是原子性的,不可再分。 | 消除重复组和多值字段。 |
| 第二范式 (2NF) | 满足1NF,且非主属性必须完全依赖于整个主键(对于组合主键的情况)。 | 消除非主属性对主键的部分依赖。 |
| 第三范式 (3NF) | 满足2NF,且非主属性之间不能存在依赖,即直接依赖于主键。 | 消除非主属性对主键的传递依赖。 |
第三范式建立在前两个范式的基础之上。它的核心思想是:一张数据库表中的每个非主属性,都必须直接依赖于主键,而不能依赖于其他非主属性。换句话说,就是不允许存在“传递依赖”
- 传递依赖是什么?它指的是这样一种关系:A → B → C(A 决定 B,B 决定 C)。在数据库表中,如果主键是 A,那么非主属性 C 实际上是通过 B 间接依赖于主键 A 的,这就构成了传递依赖。
- 一个经典的例子:
假设我们有一张 员工表,包含以下字段:
员工编号(主键), 员工姓名, 部门编号, 部门名称, 部门地点。
在这个表中,存在以下依赖关系:
员工编号→员工姓名,部门编号,部门名称,部门地点(这是正确的,所有属性都依赖于主键)部门编号→部门名称,部门地点(这是正确的,部门信息依赖于部门编号)
问题在于:部门名称和 部门地点这两个非主属性,并不是直接依赖于主键 员工编号,而是先依赖于另一个非主属性 部门编号,再由 部门编号依赖于 员工编号。即:员工编号→ 部门编号→ 部门名称。这就违反了第三范式。
评论