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有哪些？RuntimeExceptionClassCastException //类型转换异常IndexOutOfBoundsException //数组越界异常NullPointerException //空指针ArrayStoreException //数组存储异常NumberFormatException //数字格式化异常ArithmeticException //数学运算异常unchecked ExceptionNoSuchFieldException //反射异常，没有对应的字段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集合ListArrayList和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的速度要优于ArrayListMapHashMap 使用数组+链表+红黑树（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(); }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倍。SetHashSet、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())；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与CASAQS（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）：栈用于存储线程执行方法的调用帧（包括局部变量、方法参数、返回地址等）。每个线程都会有自己的栈，栈是线程私有的。栈中的数据随着方法的调用和返回而动态地入栈和出栈。内存管理：堆：堆的内存空间由JVM自动进行管理，包括自动分配和回收。在对象不再被引用时，垃圾回收器会自动回收堆中的内存，以便下次的对象分配使用。栈：栈的内存空间由编译器自动进行管理，包括方法的入栈和出栈。栈上的数据是临时的，方法执行完毕后会立即释放，不需要垃圾回收器来回收。内存分配：堆：堆上的内存分配由垃圾回收器负责，采用动态分配的方式。当对象被创建时，垃圾回收器会在堆上找到足够大的连续内存空间来存储对象，并返回对象的引用。栈：栈上的内存分配是通过编译器进行静态分配的。编译器在编译阶段确定每个方法的栈帧大小，根据方法的调用关系预先分配好栈帧所需的内存空间。大小限制：堆：堆的大小是通过JVM参数来配置的，例如-Xmx和-Xms参数可以分别设置堆的最大和初始大小。堆的大小一般比较大，取决于可用的物理内存。栈：栈的大小是固定的，取决于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 GCMinor 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 可以根据网络状况来控制发送的数据量，防止网络拥塞和数据丢失。MySql1.事务的四大特性原子性（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 状态)binlogredo 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 的，这就构成了传递依赖。一个经典的例子：假设我们有一张 员工表，包含以下字段：员工编号（主键）, 员工姓名, 部门编号, 部门名称, 部门地点。在这个表中，存在以下依赖关系：员工编号→ 员工姓名, 部门编号, 部门名称, 部门地点（这是正确的，所有属性都依赖于主键）部门编号→ 部门名称, 部门地点（这是正确的，部门信息依赖于部门编号）问题在于：部门名称和 部门地点这两个非主属性，并不是直接依赖于主键 员工编号，而是先依赖于另一个非主属性 部门编号，再由 部门编号依赖于 员工编号。即：员工编号→ 部门编号→ 部门名称。这就违反了第三范式。
集成Spring AMQP1.可通过如下 Docker 命令 安装 RabbiMQ: docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.10-management2.登录 RabbiMQ 的 web 管理界面，创建虚拟主机novel: 3.项目中加入如下的 maven 依赖：<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>4.在 application.yml 配置文件中加入 RabbitMQ 的连接配置:spring: rabbitmq: addresses: "amqp://guest:guest@47.106.243.172" virtual-host: novel template: retry: # 开启重试 enabled: true # 最大重试次数 max-attempts: 3 # 第一次和第二次重试之间的持续时间 initial-interval: "3s"相关配置1.建立相关常量:/** * @author canace * @version 1.0 * @description AMQP 相关常量 * @date 2024/2/23 10:15 */ public class AmqpConsts { /** * 小说信息改变MQ */ public static class BookChangeMQ { /** * 小说信息改变交换机 */ public static final String EXCHANGE_NAME = "EXCHANGE-BOOK-CHANGE"; /** * Elasticsearch book 索引更新的队列 */ public static final String QUEUE_ES_UPDATE = "QUEUE-ES-BOOK-UPDATE"; /** * Redis book 缓存更新的队列 */ public static final String QUEUE_REDIS_UPDATE = "QUEUE-REDIS-BOOK-UPDATE"; } }2.创建 AMQP 配置类，配置各个交换机、队列以及绑定关系：/** * @author canace * @version 1.0 * @description AMPQ 配置类 * @date 2024/2/23 10:25 */ @Configuration public class AmqpConfig { /** * 小说信息改变交换机(广播模式) */ @Bean public FanoutExchange bookChangeExchange() { return new FanoutExchange(AmqpConsts.BookChangeMQ.EXCHANGE_NAME); } /** * Elasticsearch book 索引更新队列 */ @Bean public Queue esBookUpdateQueue() { return new Queue(AmqpConsts.BookChangeMQ.QUEUE_ES_UPDATE); } /** * Elasticsearch book 索引更新队列绑定到小说信息改变交换机 */ @Bean public Binding esBookUpdateQueueBinding() { return BindingBuilder.bind(esBookUpdateQueue()).to(bookChangeExchange()); } }3.配置 AMQP 消息管理类：/** * @author canace * @version 1.0 * @description AMQP 消息管理器 * @date 2024/2/23 10:32 */ @Component @RequiredArgsConstructor public class AmqpMsgManager { private final AmqpTemplate amqpTemplate; // 判断配置是否启动 RabbitMq @Value("${spring.amqp.enabled:false}") private boolean amqpEnabled; // 在小说进行信息更新或者新建时，只需调用这个接口即可实现 // 这里没有写更新书籍信息的接口，不做演示 // amqpMsgManager.sendBookChangeMsg(dto.getBookId()); public void sendBookChangeMsg(Long bookId){ if (amqpEnabled) { sendAmqpMessage(amqpTemplate, bookId); } } private void sendAmqpMessage(AmqpTemplate amqpTemplate, Object message) { // 如果在事务中则在事务执行完成后再发送，否则可以直接发送 if (TransactionSynchronizationManager.isActualTransactionActive()) { TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { @Override public void afterCommit() { amqpTemplate.convertAndSend(AmqpConsts.BookChangeMQ.EXCHANGE_NAME, null, message); } }); return; } // 将Java对象转为Amqp,并发送消息 amqpTemplate.convertAndSend(AmqpConsts.BookChangeMQ.EXCHANGE_NAME, null, message); } }4.配置队列监听器/** * @author canace * @version 1.0 * @description Rabbit 队列监听器 * @date 2024/2/23 10:58 */ @Component @RequiredArgsConstructor @ConditionalOnProperty(prefix = "spring", name = {"elasticsearch.enabled", "amqp.enabled"}, havingValue = "true") @Slf4j public class RabbitQueueListener { private final BookInfoMapper bookInfoMapper; private final ElasticsearchClient esClient; /** * 监听小说信息改变的 ES 更新队列，更新最新小说信息到 ES * 即如果调用了amqpMsgManager.sendBookChangeMsg(dto.getBookId());就在这里可以监听到，随后进行相关操作，这里是更新书籍信息到ES */ @RabbitListener(queues = AmqpConsts.BookChangeMQ.QUEUE_ES_UPDATE) @SneakyThrows public void updateEsBook(Long bookId) { log.info("监听到书籍信息更新"); BookInfo bookInfo = bookInfoMapper.selectById(bookId); IndexResponse response = esClient.index(i -> i .index(EsConsts.BookIndex.INDEX_NAME) .id(bookInfo.getId().toString()) .document(EsBookDto.build(bookInfo)) ); log.info("Indexed with version " + response.version()); } }注：当服务集群部署时，由于多个消费者绑定同一个队列是无法同时消费的，一个消息只能被一个消费者消费，所以刷新本地缓存的 MQ 队列命名应该使用固定名 + 唯一随机值这种动态形式。这样每次启动会生成一个新的队列，我们需要设置该队列的 autoDelete = true，让所有消费客户端连接断开时自动删除该队列
Canal 工作原理Canal 模拟 MySQL slave 的交互协议，伪装自己为 MySQL slave ，向 MySQL master 发送dump 协议MySQL master 收到 dump 请求，开始推送 binary log 给 slave (即 canal )Canal 解析 binary log 对象(原始为 byte 流)环境准备本项目所有的环境都是在 Docker 环境下进行了，需要自行在安装 DockerMysql安装# 拉取镜像 docker pull mysql # 创建文件并写入内容 vim /usr/local/mysql/conf/my.cnf [client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4 [mysqld] # 设置东八区时区 default-time_zone = '+8:00' # 设置密码验证规则，default_authentication_plugin参数已被废弃 # 改为authentication_policy #default_authentication_plugin=mysql_native_password authentication_policy=mysql_native_password # 限制导入和导出的数据目录 # 为空，不限制导入到处的数据目录； # 指定目录，必须从该目录导入到处，且MySQL不会自动创建该目录； # 为NULL，禁止导入与导出功能 #secure_file_priv=/var/lib/mysql secure_file_priv= init_connect='SET collation_connection = utf8mb4_0900_ai_ci' init_connect='SET NAMES utf8mb4' character-set-server=utf8mb4 collation-server=utf8mb4_0900_ai_ci skip-character-set-client-handshake skip-name-resolve [mysqld] log-bin=mysql-bin # 开启binlog binlog-format=ROW # 选择ROW模式 server_id=1 # 配置MySQL replaction需要定义，不和Canal的slaveId重复即可 # 运行mysql容器 docker run -p 3306:3306 --name mysql --restart=always --privileged=true \ -v /usr/local/mysql/log:/var/log/mysql \ -v /usr/local/mysql/data:/var/lib/mysql \ -v /usr/local/mysql/conf:/etc/mysql \ -v /etc/localtime:/etc/localtime:ro \ -e MYSQL_ROOT_PASSWORD=123456 -d mysql:latestCanal安装这里建议使用v1.1.6版本，v1.1.7版本使用时报错# mysql创建授权账号 CREATE USER canal IDENTIFIED BY 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; -- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ; FLUSH PRIVILEGES; # 先创建一个临时容器 docker run --name canal -p 11111:11111 \ -d canal/canal-server:v1.1.6 # 复制配置文件 docker cp canal:/home/admin/canal-server/conf/canal.properties /mydata/canal/conf docker cp canal:/home/admin/canal-server/conf/example/instance.properties /mydata/canal/conf # 删除该临时容器 docker rm -f canal # 新建canal容器 docker run --name canal -p 11111:11111 \ -v /mydata/canal/conf/instance.properties:/home/admin/canal-server/conf/example/instance.properties \ -v /mydata/canal/conf/canal.properties:/home/admin/canal-server/conf/canal.properties \ --privileged=true \ --restart=always \ -d canal/canal-server:v1.1.6 # 修改/mydata/canal/conf/canal.properties canal.serverMode = rabbitMQ canal.destinations = canal.shortlink # 可自定义 rabbitmq.host = 192.168.10.107 # rabbitmq地址 rabbitmq.virtual.host = / rabbitmq.exchange = canal.exchange # 可自定义 rabbitmq.username = guest rabbitmq.password = guest rabbitmq.deliveryMode = # 修改/mydata/canal/conf/instance.properties canal.instance.mysql.slaveId=111 # 和mysql的server_id不一样即可 canal.instance.master.address= 192.168.10.107:3306 # mysql地址 canal.instance.dbUsername= canal # 数据库用户名 canal.instance.dbPassword= canal # 数据库密码 canal.instance.filter.regex=link\\..* # 白名单，这里link是表示link的数据库，其余内容可至canal官网查看配置 canal.instance.filter.black.regex=mysql\\.slave_.*,link\\.BASE.* # link\\.BASE.*得添加，否则会报错 canal.mq.topic= canal.shortlink # 和canal.destinations一致Springboot引入依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>新建RabbitMQ配置类package com.study.config; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author canace * @version 1.0 * @description rabbitmq配置类 * @date 2024/1/30 14:21 */ @Configuration @Slf4j public class RabbitConfig { @Bean public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate template = new RabbitTemplate(); template.setConnectionFactory(connectionFactory); template.setMessageConverter(new Jackson2JsonMessageConverter()); return template; } /** * template.setMessageConverter(new Jackson2JsonMessageConverter()); * 这段和上面这行代码解决RabbitListener循环报错的问题 */ @Bean public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setMessageConverter(new Jackson2JsonMessageConverter()); return factory; } }新建消息实体类package com.study.config; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; /** * @author canace * @version 1.0 * @description canal消息实体类 * @date 2024/1/30 14:06 */ @NoArgsConstructor @Data public class CanalMessage<T> { private String type; private String table; private List<T> data; private String database; private Long es; private Integer id; private Boolean isDdl; private List<T> old; private List<String> pkNames; private String sql; private Long ts; }新建监听类package com.study.config; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.study.cacheManager.GroupCacheManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.Exchange; import org.springframework.amqp.rabbit.annotation.Queue; import org.springframework.amqp.rabbit.annotation.QueueBinding; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.stereotype.Component; /** * @author canace * @version 1.0 * @description Canal + RabbitMQ 监听数据库变化 * @date 2024/1/30 13:59 */ @Component @Slf4j @RequiredArgsConstructor public class CanalListener { private final GroupCacheManager groupCacheManager; @RabbitListener(bindings = { @QueueBinding( value = @Queue(value = "canal.queue", durable = "true"), exchange = @Exchange(value = "canal.exchange"), key = "canal.shortlink" ) }) public void handleDataChange(@Payload CanalMessage<?> message) { String tableName = message.getTable(); if ("t_group".equals(tableName)) { if ("INSERT".equals(message.getType())) { // 新增只需要清空用户名的分组缓存即可 String username = JSONUtil.parseObj(message.getData().get(0)).getStr("username"); log.info("新增分组，清空用户名为{}的分组缓存", username); groupCacheManager.delCacheGroupListByUsername(username); } else { // 其他情况需要清空当前gid的缓存和用户名的缓存 String username = JSONUtil.parseObj(message.getData().get(0)).getStr("username"); String gid = JSONUtil.parseObj(message.getData().get(0)).getStr("gid"); log.info("修改或删除分组，清空用户名为{}的分组缓存和gid为{}的分组缓存", username, gid); groupCacheManager.delCacheGroupByGid(gid); groupCacheManager.delCacheGroupListByUsername(username); } } } }综上即可完成canal监听后由rabbitmq通知后，再对缓存进行处理
如何查询树形结构的数据在遇到数据库保存的数据为树形结构，如多级分类列表或者评论等，此处以分类列表举例我们需要向前端返回如下部分数据[ { "id": "1-1", "name": "前端开发", "label": "前端开发", "parentid": "1", "isShow": 1, "orderby": 1, "isLeaf": 0, "childrenTreeNodes": [ { "id": "1-1-1", "name": "HTML/CSS", "label": "HTML/CSS", "parentid": "1-1", "isShow": 1, "orderby": 1, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-10", "name": "其它", "label": "其它", "parentid": "1-1", "isShow": 1, "orderby": 10, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-2", "name": "JavaScript", "label": "JavaScript", "parentid": "1-1", "isShow": 1, "orderby": 2, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-3", "name": "jQuery", "label": "jQuery", "parentid": "1-1", "isShow": 1, "orderby": 3, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-4", "name": "ExtJS", "label": "ExtJS", "parentid": "1-1", "isShow": 1, "orderby": 4, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-5", "name": "AngularJS", "label": "AngularJS", "parentid": "1-1", "isShow": 1, "orderby": 5, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-6", "name": "ReactJS", "label": "ReactJS", "parentid": "1-1", "isShow": 1, "orderby": 6, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-7", "name": "Bootstrap", "label": "Bootstrap", "parentid": "1-1", "isShow": 1, "orderby": 7, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-8", "name": "Node.js", "label": "Node.js", "parentid": "1-1", "isShow": 1, "orderby": 8, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-9", "name": "Vue", "label": "Vue", "parentid": "1-1", "isShow": 1, "orderby": 9, "isLeaf": 1, "childrenTreeNodes": null } ] }, { "id": "1-10", "name": "研发管理", "label": "研发管理", "parentid": "1", "isShow": 1, "orderby": 10, "isLeaf": 0, "childrenTreeNodes": [ { "id": "1-10-1", "name": "敏捷开发", "label": "敏捷开发", "parentid": "1-10", "isShow": 1, "orderby": 1, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-10-2", "name": "软件设计", "label": "软件设计", "parentid": "1-10", "isShow": 1, "orderby": 2, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-10-3", "name": "软件测试", "label": "软件测试", "parentid": "1-10", "isShow": 1, "orderby": 3, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-10-4", "name": "研发管理", "label": "研发管理", "parentid": "1-10", "isShow": 1, "orderby": 4, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-10-5", "name": "其它", "label": "其它", "parentid": "1-10", "isShow": 1, "orderby": 5, "isLeaf": 1, "childrenTreeNodes": null } ] }, { "id": "1-11", "name": "系统运维", "label": "系统运维", "parentid": "1", "isShow": 1, "orderby": 11, "isLeaf": 0, "childrenTreeNodes": [ { "id": "1-11-1", "name": "Linux", "label": "Linux", "parentid": "1-11", "isShow": 1, "orderby": 1, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-11-10", "name": "其它", "label": "其它", "parentid": "1-11", "isShow": 1, "orderby": 10, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-11-2", "name": "Windows", "label": "Windows", "parentid": "1-11", "isShow": 1, "orderby": 2, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-11-3", "name": "UNIX", "label": "UNIX", "parentid": "1-11", "isShow": 1, "orderby": 3, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-11-4", "name": "Mac OS", "label": "Mac OS", "parentid": "1-11", "isShow": 1, "orderby": 4, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-11-5", "name": "网络技术", "label": "网络技术", "parentid": "1-11", "isShow": 1, "orderby": 5, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-11-6", "name": "路由协议", "label": "路由协议", "parentid": "1-11", "isShow": 1, "orderby": 6, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-11-7", "name": "无线网络", "label": "无线网络", "parentid": "1-11", "isShow": 1, "orderby": 7, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-11-8", "name": "Ngnix", "label": "Ngnix", "parentid": "1-11", "isShow": 1, "orderby": 8, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-11-9", "name": "邮件服务器", "label": "邮件服务器", "parentid": "1-11", "isShow": 1, "orderby": 9, "isLeaf": 1, "childrenTreeNodes": null } ] }, { "id": "1-2", "name": "移动开发", "label": "移动开发", "parentid": "1", "isShow": 1, "orderby": 2, "isLeaf": 0, "childrenTreeNodes": [ { "id": "1-2-1", "name": "微信开发", "label": "微信开发", "parentid": "1-2", "isShow": 1, "orderby": 1, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-2-2", "name": "iOS", "label": "iOS", "parentid": "1-2", "isShow": 1, "orderby": 2, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-2-3", "name": "手游开发", "label": "手游开发", "parentid": "1-2", "isShow": 1, "orderby": 3, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-2-4", "name": "Swift", "label": "Swift", "parentid": "1-2", "isShow": 1, "orderby": 4, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-2-5", "name": "Android", "label": "Android", "parentid": "1-2", "isShow": 1, "orderby": 5, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-2-6", "name": "ReactNative", "label": "ReactNative", "parentid": "1-2", "isShow": 1, "orderby": 6, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-2-7", "name": "Cordova", "label": "Cordova", "parentid": "1-2", "isShow": 1, "orderby": 7, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-2-8", "name": "其它", "label": "其它", "parentid": "1-2", "isShow": 1, "orderby": 8, "isLeaf": 1, "childrenTreeNodes": null } ] }, { "id": "1-3", "name": "编程开发", "label": "编程开发", "parentid": "1", "isShow": 1, "orderby": 3, "isLeaf": 0, "childrenTreeNodes": [ { "id": "1-3-1", "name": "C/C++", "label": "C/C++", "parentid": "1-3", "isShow": 1, "orderby": 1, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-3-2", "name": "Java", "label": "Java", "parentid": "1-3", "isShow": 1, "orderby": 2, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-3-3", "name": ".NET", "label": ".NET", "parentid": "1-3", "isShow": 1, "orderby": 3, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-3-4", "name": "Objective-C", "label": "Objective-C", "parentid": "1-3", "isShow": 1, "orderby": 4, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-3-5", "name": "Go语言", "label": "Go语言", "parentid": "1-3", "isShow": 1, "orderby": 5, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-3-6", "name": "Python", "label": "Python", "parentid": "1-3", "isShow": 1, "orderby": 6, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-3-7", "name": "Ruby/Rails", "label": "Ruby/Rails", "parentid": "1-3", "isShow": 1, "orderby": 7, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-3-8", "name": "其它", "label": "其它", "parentid": "1-3", "isShow": 1, "orderby": 8, "isLeaf": 1, "childrenTreeNodes": null } ] }, { "id": "1-5", "name": "人工智能", "label": "人工智能", "parentid": "1", "isShow": 1, "orderby": 5, "isLeaf": 0, "childrenTreeNodes": [ { "id": "1-5-1", "name": "机器学习", "label": "机器学习", "parentid": "1-5", "isShow": 1, "orderby": 1, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-5-2", "name": "深度学习", "label": "深度学习", "parentid": "1-5", "isShow": 1, "orderby": 2, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-5-3", "name": "语音识别", "label": "语音识别", "parentid": "1-5", "isShow": 1, "orderby": 3, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-5-4", "name": "计算机视觉", "label": "计算机视觉", "parentid": "1-5", "isShow": 1, "orderby": 4, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-5-5", "name": "NLP", "label": "NLP", "parentid": "1-5", "isShow": 1, "orderby": 5, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-5-6", "name": "强化学习", "label": "强化学习", "parentid": "1-5", "isShow": 1, "orderby": 6, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-5-7", "name": "其它", "label": "其它", "parentid": "1-5", "isShow": 1, "orderby": 7, "isLeaf": 1, "childrenTreeNodes": null } ] }, { "id": "1-9", "name": "智能硬件/物联网", "label": "智能硬件/物联网", "parentid": "1", "isShow": 1, "orderby": 9, "isLeaf": 0, "childrenTreeNodes": [ { "id": "1-9-1", "name": "无线通信", "label": "无线通信", "parentid": "1-9", "isShow": 1, "orderby": 1, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-9-10", "name": "物联网技术", "label": "物联网技术", "parentid": "1-9", "isShow": 1, "orderby": 10, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-9-11", "name": "其它", "label": "其它", "parentid": "1-9", "isShow": 1, "orderby": 11, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-9-2", "name": "电子工程", "label": "电子工程", "parentid": "1-9", "isShow": 1, "orderby": 2, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-9-3", "name": "Arduino", "label": "Arduino", "parentid": "1-9", "isShow": 1, "orderby": 3, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-9-4", "name": "体感技术", "label": "体感技术", "parentid": "1-9", "isShow": 1, "orderby": 4, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-9-5", "name": "智能硬件", "label": "智能硬件", "parentid": "1-9", "isShow": 1, "orderby": 5, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-9-6", "name": "驱动/内核开发", "label": "驱动/内核开发", "parentid": "1-9", "isShow": 1, "orderby": 6, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-9-7", "name": "单片机/工控", "label": "单片机/工控", "parentid": "1-9", "isShow": 1, "orderby": 7, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-9-8", "name": "WinCE", "label": "WinCE", "parentid": "1-9", "isShow": 1, "orderby": 8, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-9-9", "name": "嵌入式", "label": "嵌入式", "parentid": "1-9", "isShow": 1, "orderby": 9, "isLeaf": 0, "childrenTreeNodes": [ { "id": "1-9-9-1", "name": "嵌入式基础", "label": "嵌入式基础", "parentid": "1-9-9", "isShow": 1, "orderby": 1, "isLeaf": 1, "childrenTreeNodes": null } ] } ] } ]由于返回的是一个List集合，因此需要定义一个DTO类表示分类信息的模型类@Data public class CourseCategoryTreeDto extends CourseCategory { //子节点 List<CourseCategoryTreeDto> childrenTreeNodes; }定义Controller类@Resource private CourseCategoryService categoryService; @GetMapping("/course-category/tree-nodes") public List<CourseCategoryTreeDto> queryCatTreeNodes(){ return categoryService.queryCatTreeNodes("1"); //此处1为根节点 }定义Mapperpublic interface CourseCategoryMapper extends BaseMapper<CourseCategory> { public List<CourseCategoryTreeDto> selectTreeNodes(String id); }写递归SQL（用于不确定有几级层级） WITH recursive t1 as( SELECT * from course_category WHERE id = #{id} UNION ALL SELECT c2.* from course_category c2 INNER JOIN t1 on t1.id = c2.parentid ) SELECT * from t1 order by t1.id, t1.orderby 内连接查询（确定有多少级层级） select one.id one_id, one.name one_name, one.parentid one_parentid, one.orderby one_orderby, one.label one_label, two.id two_id, two.name two_name, two.parentid two_parentid, two.orderby two_orderby, two.label two_label from course_category one inner join course_category two on one.id = two.parentid where one.parentid = 1 and one.is_show = 1 and two.is_show = 1 order by one.orderby, two.orderbyService和其实现类//Service类 public interface CourseCategoryService extends IService<CourseCategory> { List<CourseCategoryTreeDto> queryCatTreeNodes(String id); } //实现类 @Service public class CourseCategoryServiceImpl extends ServiceImpl<CourseCategoryMapper, CourseCategory> implements CourseCategoryService { @Resource CourseCategoryMapper courseCategoryMapper; /** * 根据id获取课程分类列表 * @param id 根节点id * @return 课程分类列表 */ @Override public List<CourseCategoryTreeDto> queryCatTreeNodes(String id) { //执行SQL，获取查询数据 List<CourseCategoryTreeDto> treeDtos = courseCategoryMapper.selectTreeNodes(id); //将结果转为map，便于后续找根节点，同时可以过滤当前id节点 //.filter(item -> !id.equals(item.getId()))不显示根节点 Map<String, CourseCategoryTreeDto> map = treeDtos.stream().filter(item -> !id.equals(item.getId())).collect(Collectors.toMap(CourseCategory::getId, value -> value, (key1, key2) -> key2)); List<CourseCategoryTreeDto> list = new ArrayList<>(); //遍历返回最终结果,排除根节点 treeDtos.stream().filter(item -> !id.equals(item.getId())).forEach(item ->{ //先将根节点的一级子节点放入数组 if (item.getParentid().equals(id)){ list.add(item); } //如果不为一级节点，就找到它的父节点，通过之前保存的map CourseCategoryTreeDto treeParent = map.get(item.getParentid()); //如果存在父节点就将其放入它的父节点的list中 if (treeParent != null){ //如果它的父节点的子节点列表为空，就新建一个对象存储 if (treeParent.getChildrenTreeNodes() == null){ treeParent.setChildrenTreeNodes(new ArrayList<>()); } //不为空就放入子节点中 //注意这里是需要获取子节点列表然后插入进入 treeParent.getChildrenTreeNodes().add(item); } }); return list; } } 前端显示示例
完美解决方案-雪花算法ID到前端之后精度丢失问题
一、现象是这样的下面我把异常的现象给大家描述一下，小伙伴建了一张表，表的主键是id BigINT，用来存储雪花算法生成的ID，嗯，这个没有问题！CREATE TABLE user ( id BIGINT(20) NOT NULL COMMENT '主键ID', #其他字段省略 );使用Long 类型对应数据库ID数据。嗯，也没有问题，雪花算法生成的就是一串数字，Long类型属于标准答案！@Data public class User { private Long id; //其他成员变量省略在后端下断点。看到数据响应以JSON响应给前端，正常{ id：1297873308628307970, //其他属性省略 }最后，这条数据返回给前端，前端接收到之后，修改这条数据，后端再次接收回来。奇怪的问题出现了：后端重新接收回来的id变成了： 12978733086283000000 ，不再是 1297873308628307970 二、分析问题我的第一感觉是，开发小伙伴把数据给搞混了，张冠李戴了，把XXX的对象ID放到了YYY对象的ID上。所以，就按照代码从前端到后端、从后端到前端调试跟踪了一遍。从代码的逻辑角度上没有任何问题。这时，我有点烦躁了，真的是耽误我下班了！但开工没有回头箭，既然坐下来了就得帮他解决，不然以后这队伍怎么带？想到这我又静下心来，开始思考。1297873308628300000 ---> 1297873308628307970 这两个数长得还挺像的，似乎是被四舍五入了。此时脑袋里面冒出一个想法，是精度丢失了么？哪里能导致精度丢失？服务端都是Long类型的id，不可能丢失前端是什么类型，JSON字符串转js对象，接收Long类型的是number上网查了一下Number精度是16位（雪花ID是19位的），So：JS的Number数据类型导致的精度丢失。问题是找到了！小伙伴投来敬佩的眼光，5分钟就把这问题发现了。可是发现了有什么用？得解决问题啊！三、解决问题开发小伙伴说：那我把所有的数据库表设计，id字段由Long类型改成String类型吧。我问他你有多少张表？他说100多张吧。100多张表还有100多个实体类需要改还有各种使用到实体类的Service层要改Service等改完Controller层要改关键的是String和Long都是常用类型，他还不敢批量替换小伙伴拿起电话打算订餐，说今晚的加班是无法避免了。我想了想说：你最好别改，String做ID查询性能会下降，我再想想！后端A到前端B出现精度丢失，要么改前端，要么改后端，要么…… 。“哎哎，你等等先别订餐，后端A到前端B你用的什么做的序列化？” 小伙伴告诉我说使用的是Jackson，这就好办了，Jackson我熟悉啊！解决思路：后端的ID(Long) ==> Jackson(Long转String) ==> 前端使用String类型的ID,前端使用js string精度就不会丢失了。 那前端再把String类型的19位数字传回服务端的时候，可以用Long接收么？当然可以，这是Spring反序列化参数接收默认支持的行为。最终方案就是：前端用String类型的雪花ID保持精度，后端及数据库继续使用Long(BigINT)类型不影响数据库查询执行效率。剩下的问题就是：在Spring Boot应用中，使用Jackson进行JSON序列化的时候怎么将Long类型ID转成String响应给前端。方案如下： import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; /** * 解决后端传给前端Long类型，前端处理后有可能失精问题 * 雪花算法19位，前端Number 16位 */ @Configuration public class JacksonConfig { @Bean @Primary @ConditionalOnMissingBean(ObjectMapper.class) public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder){ ObjectMapper objectMapper = builder.createXmlMapper(false).build(); // 全局配置序列化返回 JSON 处理 SimpleModule simpleModule = new SimpleModule(); //JSON Long ==> String simpleModule.addSerializer(Long.class, ToStringSerializer.instance); objectMapper.registerModule(simpleModule); return objectMapper; } }
ShardingSphere-JDBC采用Docker实现主从复制实现新增主节点配置文件mkdir -p /wolfcode/mysql/write_db/conf vi /wolfcode/mysql/write_db/conf/my.cnf #新增内容如下 [mysqld] #服务器唯一id，注意在集群中不要出现一样的 server-id=1 binlog_format=STATEMENT log-bin=master-bin log-bin-index=master-bin.index启动MySQL主节点docker run -d --name write_db -p 3306:3306 \ -v /wolfcode/mysql/write_db/conf:/etc/mysql/conf.d \ -v /wolfcode/mysql/write_db/data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=root \ mysql进行master容器，登录mysqldocker exec -it write_db /bin/bash mysql -uroot -proot #查看主节点状态 show master status;在mysql内部创建slave同步账号-- 创建slave用户 create user 'slave_user'@'%'; -- 设置密码 alter user 'slave_user'@'%' identified with mysql_native_password by 'root'; -- 授予复制权限 grant replication slave on *.* to 'slave_user'@'%'; -- 刷新权限 flush privileges;从节点1配置信息#退出mysql和docker exit; exit; mkdir -p /wolfcode/mysql/read_db0/conf vi /wolfcode/mysql/read_db0/conf/my.cnf #新增内容如下 [mysqld] #服务器唯一id，注意在集群中不要出现一样的 server-id=2 relay-log-index=slave-relay-bin.index relay-log=slave-replay-bin #启动从节点1 docker run -d --name read_db0 -p 3307:3306 \ -v /wolfcode/mysql/read_db0/conf:/etc/mysql/conf.d \ -v /wolfcode/mysql/read_db0/data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=root \ mysql #进入从节点容器，并登录mysql docker exec -it read_db0 /bin/bash mysql -uroot -proot #进入mysql执行命令 #docker inspect -f '{{.Name}} => {{.NetworkSettings.IPAddress }}' $(docker ps -aq) 查看ip change master to master_host='172.17.0.2', master_user='slave_user', master_password='root', master_port=3306, master_log_file='master-bin.000004', master_log_pos=156; #启动从节点1并查看状态 start slave; show slave status \G # Slave_IO_Running: Yes Slave_SQL_Running: Yes从节点2配置信息#退出mysql和docker exit; exit; mkdir -p /wolfcode/mysql/read_db1/conf vi /wolfcode/mysql/read_db1/conf/my.cnf #新增内容如下 [mysqld] #服务器唯一id，注意在集群中不要出现一样的 server-id=3 relay-log-index=slave-relay-bin.index relay-log=slave-replay-bin #启动从节点2 docker run -d --name read_db1 -p 3308:3306 \ -v /wolfcode/mysql/read_db1/conf:/etc/mysql/conf.d \ -v /wolfcode/mysql/read_db1/data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=root \ mysql #进入从节点容器，并登录mysql docker exec -it read_db1 /bin/bash mysql -uroot -proot #进入mysql执行命令 #docker inspect -f '{{.Name}} => {{.NetworkSettings.IPAddress }}' $(docker ps -aq) 查看ip #show master logs; 查看主节点log日志名 change master to master_host='172.17.0.2', master_user='slave_user', master_password='root', master_port=3306, master_log_file='master-bin.000004', master_log_pos=156; #启动从节点2并查看状态 start slave; show slave status \G # Slave_IO_Running: Yes Slave_SQL_Running: Yes创建完成后连接三个数据库，主表的变化，从表都会进行更新 --> Ip地址相同，端口不同3306，3307，3308使用ShardingSphere-JDBC（springboot框架）引入maven依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core</artifactId> <version>5.3.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.27</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>1.33</version> </dependency>配置#在resources目录下新建sharding.yaml #数据源配置 dataSources: write_db: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.jdbc.Driver jdbcUrl: jdbc:mysql://192.168.31.128:3306/db_user?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: root read_db0: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.jdbc.Driver jdbcUrl: jdbc:mysql://192.168.31.128:3307/db_user?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: root read_db1: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.jdbc.Driver jdbcUrl: jdbc:mysql://192.168.31.128:3308/db_user?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: root rules: # 读写分离 - !READWRITE_SPLITTING dataSources: readwrite_ds: staticStrategy: writeDataSourceName: write_db readDataSourceNames: - read_db0 - read_db1 loadBalancerName: random #负载均衡 loadBalancers: random: type: RANDOM #显示sql日志 props: sql-show: true #application.yml spring: datasource: driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver url: jdbc:shardingsphere:classpath:sharding.yamltest下新建一个Generator.java,用于mybatis-plus 代码生成public class Generator { @Test void generatorDomain() { FastAutoGenerator.create("jdbc:mysql://192.168.31.128:3306/db_user?serverTimezone=UTC&useSSL=false", "root", "root") .globalConfig(builder -> { builder.author("canace") // 设置作者 // .enableSwagger(f) // 开启 swagger 模式 .fileOverride() // 覆盖已生成文件 .outputDir("F:\\JavaFile\\ShardingSphereJDBC\\src\\main\\java"); // 指定输出目录 }) .packageConfig(builder -> { builder.parent("com.study.shardingspherejdbc") // 设置父包名 .moduleName(null) // 设置父包模块名 .pathInfo(Collections.singletonMap(OutputFile.xml, "F:\\JavaFile\\ShardingSphereJDBC\\src\\main\\resources")); // 设置mapperXml生成路径 }) .strategyConfig(builder -> { builder.addInclude("sys_user") // 设置需要生成的表名 .addTablePrefix("t_", "c_"); // 设置过滤表前缀 }) .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板，默认的是Velocity引擎模板 .execute(); } }springboot启动类上加mybatis-plus注解@SpringBootApplication @MapperScan("com.study.shardingspherejdbc.mapper") public class ShardingSphereJdbcApplication { public static void main(String[] args) { SpringApplication.run(ShardingSphereJdbcApplication.class, args); } }test文件内测试 @Autowired private ISysUserService iSysUserService; /** * 插入数据测试，三个数据库都保存了该数据 */ @Test void testInsert() { SysUser sysUser = new SysUser(); sysUser.setLoginName("canace1"); sysUser.setUserName("admin"); sysUser.setPhoneName("12345678999"); sysUser.setPassword("123456"); sysUser.setSalt("1234"); iSysUserService.save(sysUser); } /** * 读取测试，数据从两个读数据库中随机读取 */ @Test void testRead(){ for (int i = 0; i < 5; i++) { List<SysUser> sysUsers = iSysUserService.list(); sysUsers.forEach(System.out::println); System.out.println("=========================="); } }事务情况测试 /** * 没有使用事务测试，存入写数据库write_db，从读数据中读取 */ @Test void testNoTransactional() { SysUser sysUser = new SysUser(); sysUser.setLoginName("canace2"); sysUser.setUserName("admin2"); sysUser.setPhoneName("12345678999"); sysUser.setPassword("123456"); sysUser.setSalt("1234"); iSysUserService.save(sysUser); System.out.println("=========================="); List<SysUser> sysUsers = iSysUserService.list(); sysUsers.forEach(System.out::println); } /** * 使用事务测试，该数据由于事务没有提交，没有存入到任何数据库 * 但是在读取时会从读数据库中读，并且读到该数据 * Actual SQL: write_db ::: SELECT id,login_name,user_name,phone_name,password,salt FROM sys_user */ @Test @Transactional void testTransactional() { SysUser sysUser = new SysUser(); sysUser.setLoginName("canace3"); sysUser.setUserName("admin3"); sysUser.setPhoneName("12345678999"); sysUser.setPassword("123456"); sysUser.setSalt("1234"); iSysUserService.save(sysUser); System.out.println("=========================="); List<SysUser> sysUsers = iSysUserService.list(); sysUsers.forEach(System.out::println); }负载均衡策略配置rules: # 读写分离 - !READWRITE_SPLITTING dataSources: readwrite_ds: staticStrategy: writeDataSourceName: write_db readDataSourceNames: - read_db0 - read_db1 #负载均衡算法名称 loadBalancerName: weight # loadBalancerName: random # loadBalancerName: round_robin # 负载均衡算法配置 loadBalancers: weight: type: WEIGHT #权重 props: read_db0: 1 #两个读数据库进行1:2的权重进行读取 read_db1: 2 # round_robin: # type: ROUND_ROBIN #轮转 # props: # transaction-read-query-strategy: FIXED_PRIMARY #transaction-read-query-strategy说明：所以策略都有这个，事务内读请求路由策略，可选值：FIXED_PRIMARY(路由到 primary)、FIXED_REPLICA(根据随机策略选择一个固定的 replica)、DYNAMIC_REPLICA(根据随机策略路由到不同的 replica)，默认值：FIXED_PRIMARY。 # random: # type: RANDOM #随机轮转负载均衡算法(ROUND_ROBIN)随即负载均衡算法(RANDOM)权重负载均衡算法(WEIGHT)分库分表实现创建新容器docker run -d \ -p 3309:3306 \ -v /wolfcode/mysql/customer_data0/conf:/etc/mysql/conf.d \ -v /wolfcode/mysql/customer_data0/data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=root \ --name customer_data0 \ mysql #进入从节点容器，并登录mysql docker exec -it customer_data0 /bin/bash mysql -uroot -proot创建数据库create database customer_data; use customer_data; CREATE TABLE `bus_statement` ( `statement_no` bigint(0) NOT NULL AUTO_INCREMENT, `customer_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `customer_phone` bigint(0) NULL DEFAULT NULL, `actual_arrival_time` datetime(0) NULL DEFAULT NULL, `service_type` bigint(0) NULL DEFAULT NULL, `status` int(255) UNSIGNED ZEROFILL NULL DEFAULT NULL, `total_amount` decimal(10, 0) NULL DEFAULT NULL, `create_time` datetime(0) NULL DEFAULT NULL, `info` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, PRIMARY KEY (`statement_no`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;生成bus_satatement相关代码 @Test void generatorDomain() { FastAutoGenerator.create("jdbc:mysql://192.168.31.128:3309/customer_data?serverTimezone=UTC&useSSL=false", "root", "root") .globalConfig(builder -> { builder.author("canace") // 设置作者 // .enableSwagger(f) // 开启 swagger 模式 .fileOverride() // 覆盖已生成文件 .outputDir("F:\\JavaFile\\ShardingSphereJDBC\\src\\main\\java"); // 指定输出目录 }) .packageConfig(builder -> { builder.parent("com.study.shardingspherejdbc") // 设置父包名 .moduleName(null) // 设置父包模块名 .pathInfo(Collections.singletonMap(OutputFile.xml, "F:\\JavaFile\\ShardingSphereJDBC\\src\\main\\resources\\mapper")); // 设置mapperXml生成路径 }) .strategyConfig(builder -> { builder.addInclude("bus_statement") // 设置需要生成的表名 .addTablePrefix("t_", "c_"); // 设置过滤表前缀 }) .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板，默认的是Velocity引擎模板 .execute(); }新增yaml信息 customer_data0: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.jdbc.Driver jdbcUrl: jdbc:mysql://192.168.31.128:3309/customer_data?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: root #进行分片 - !SHARDING tables: bus_statement: #表名 actualDataNodes: customer_data0.bus_statement # 由数据源名 + 表名组成（参考 Inline 语法规则）垂直分表 /** * 测试垂直分表，两个不同的表正确插入自己的表中 */ @Test void testVerticalShardingWrite(){ SysUser sysUser = new SysUser(); sysUser.setLoginName("canace4"); sysUser.setUserName("admin4"); sysUser.setPhoneName("12345678999"); sysUser.setPassword("123456"); sysUser.setSalt("1234"); iSysUserService.save(sysUser); BusStatement busStatement = new BusStatement(); busStatement.setCustomerName("canace"); busStatement.setCustomerPhone(12345678888L); busStatement.setActualArrivalTime(new Date()); busStatement.setCreateTime(new Date()); busStatement.setServiceType(0L); busStatement.setTotalAmount(new BigDecimal(588)); busStatement.setInfo("无"); iBusStatementService.save(busStatement); }分布式序列ID现在的服务基本是分布式、微服务形式的，而且大数据量也导致分库分表的产生，对于水平分表就需要保证表中 id 的全局唯一性。对于 MySQL 而言，一个表中的主键 id 一般使用自增的方式，但是如果进行水平分表之后，多个表中会生成重复的 id 值。那么如何保证水平分表后的多张表中的 id 是全局唯一性的呢？雪花算法是其中一个用于解决分布式 id 的高效方案，也是许多互联网公司在推荐使用的。#进行分片 - !SHARDING tables: bus_statement: #表名 actualDataNodes: customer_data0.bus_statement # 由数据源名 + 表名组成（参考 Inline 语法规则） keyGenerateStrategy: #分布式序列策略 column: statement_no # 自增列名称，缺省表示不使用自增主键生成器 keyGeneratorName: snowflake # 分布式序列算法名称 雪花算法 keyGenerators: snowflake: type: SNOWFLAKE /** * 测试雪花算法 */ @Test void testSnowFlake(){ BusStatement busStatement = new BusStatement(); busStatement.setCustomerName("canace2"); busStatement.setCustomerPhone(12345678888L); busStatement.setActualArrivalTime(new Date()); busStatement.setCreateTime(new Date()); busStatement.setServiceType(0L); busStatement.setTotalAmount(new BigDecimal(588)); busStatement.setInfo("无"); iBusStatementService.save(busStatement); }水平分表复制表bus_statement为bus_statement1，之前表改为bus_statement0#进行分片 - !SHARDING tables: bus_statement: #表名 # actualDataNodes: customer_data0.bus_statement # 由数据源名 + 表名组成（参考 Inline 语法规则） actualDataNodes: customer_data0.bus_statement$->{0..1} #支持多表，即bus_statement0和bus_statement1 tableStrategy: # 分表策略，同分库策略 standard: # 用于单分片键的标准分片场景 shardingColumn: statement_no # 分表列名称 shardingAlgorithmName: t_mod # 分表算法名称 keyGenerateStrategy: #分布式序列策略 column: statement_no # 自增列名称，缺省表示不使用自增主键生成器 keyGeneratorName: snowflake # 分布式序列算法名称 雪花算法 # 分片算法配置 shardingAlgorithms: t_mod: type: MOD props: sharding-count: 2 keyGenerators: snowflake: type: SNOWFLAKE /** * 水平分表测试 */ @Test void testTableShardingInsert(){ for (int i = 0; i < 15; i++) { BusStatement busStatement = new BusStatement(); busStatement.setCustomerName("canace2"+ i); busStatement.setCustomerPhone(12345678888L); busStatement.setActualArrivalTime(new Date()); busStatement.setCreateTime(new Date()); busStatement.setServiceType(0L); busStatement.setTotalAmount(new BigDecimal(588)); busStatement.setInfo("无"); iBusStatementService.save(busStatement); } } /** * 水平分表测试数据是否都查询出来了 */ @Test void testTableShardingRead(){ List<BusStatement> list = iBusStatementService.list(); list.forEach(System.out::println); }水平分库创建新容器docker run -d \ -p 3310:3306 \ -v /wolfcode/mysql/customer_data1/conf:/etc/mysql/conf.d \ -v /wolfcode/mysql/customer_data1/data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=root \ --name customer_data1 \ mysql创建表create database customer_data; use customer_data; CREATE TABLE `bus_statement0` ( `statement_no` bigint(0) NOT NULL AUTO_INCREMENT, `customer_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `customer_phone` bigint(0) NULL DEFAULT NULL, `actual_arrival_time` datetime(0) NULL DEFAULT NULL, `service_type` bigint(0) NULL DEFAULT NULL, `status` int(255) UNSIGNED ZEROFILL NULL DEFAULT NULL, `total_amount` decimal(10, 0) NULL DEFAULT NULL, `create_time` datetime(0) NULL DEFAULT NULL, `info` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, PRIMARY KEY (`statement_no`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; CREATE TABLE `bus_statement1` ( `statement_no` bigint(0) NOT NULL AUTO_INCREMENT, `customer_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `customer_phone` bigint(0) NULL DEFAULT NULL, `actual_arrival_time` datetime(0) NULL DEFAULT NULL, `service_type` bigint(0) NULL DEFAULT NULL, `status` int(255) UNSIGNED ZEROFILL NULL DEFAULT NULL, `total_amount` decimal(10, 0) NULL DEFAULT NULL, `create_time` datetime(0) NULL DEFAULT NULL, `info` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, PRIMARY KEY (`statement_no`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;配置 customer_data1: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.jdbc.Driver jdbcUrl: jdbc:mysql://192.168.31.128:3310/customer_data?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: root #进行分片 - !SHARDING tables: bus_statement: #表名 # actualDataNodes: customer_data0.bus_statement # 由数据源名 + 表名组成（参考 Inline 语法规则） actualDataNodes: customer_data$->{0..1}.bus_statement$->{0..1} #支持多表，即bus_statement0和bus_statement1 tableStrategy: # 分表策略 standard: # 用于单分表键的标准分表场景 shardingColumn: statement_no # 分表列名称 shardingAlgorithmName: t_mod # 分表算法名称 databaseStrategy: #分库策略 standard: # 用于单分片键的标准分片场景 shardingColumn: customer_phone # 分片列名称 shardingAlgorithmName: t_inline # 分片算法名称 keyGenerateStrategy: #分布式序列策略 column: statement_no # 自增列名称，缺省表示不使用自增主键生成器 keyGeneratorName: snowflake # 分布式序列算法名称 雪花算法 # 分片算法配置 shardingAlgorithms: t_mod: type: MOD props: sharding-count: 2 t_inline: type: INLINE props: algorithm-expression: customer_data$->{customer_phone % 2} keyGenerators: snowflake: type: SNOWFLAKE /** * 水平分片测试，查看是否按照手机号码将数据分片到两个数据库中 */ @Test void testDatabaseShardingInsert(){ for (int i = 0; i < 5; i++) { BusStatement busStatement = new BusStatement(); busStatement.setCustomerName("canace"+ i); busStatement.setCustomerPhone(12345678888L); busStatement.setActualArrivalTime(new Date()); busStatement.setCreateTime(new Date()); busStatement.setServiceType(0L); busStatement.setTotalAmount(new BigDecimal(588)); busStatement.setInfo("无"); iBusStatementService.save(busStatement); } for (int i = 0; i < 10; i++) { BusStatement busStatement = new BusStatement(); busStatement.setCustomerName("dddd"+ i); busStatement.setCustomerPhone(99999999999L); busStatement.setActualArrivalTime(new Date()); busStatement.setCreateTime(new Date()); busStatement.setServiceType(0L); busStatement.setTotalAmount(new BigDecimal(688)); busStatement.setInfo("无"); iBusStatementService.save(busStatement); } } /** * 查看两个库两张表数据都查出来了 */ @Test void testDatabaseShardingRead(){ List<BusStatement> list = iBusStatementService.list(); list.forEach(System.out::println); }分片算法自动分片算法取模分片算法 MOD哈希取模分片算法 HASH_MOD基于分片容量的范围分片算法 VOLUME_RANGE基于分片边界的范围分片算法 BOUNDARY_RANGE自动时间段分片算法 AUTO_INTERVAL标准分片算法行表达式分片算法 INLINE时间范围分片算法 INTERVAL基于Cosld的固定时间范围的分片算法 COSID_INTERVAL基于Cosld的雪花ID固定时间范围的分片算法 COSID_INTERVAL_SNOWFLAKE基于Cosld的取模分片算法 COSID_MOD复合分片算法 COMPLEX_INLINEHint分片算法 HINT_INLINE自定义类分片算法详见：https://shardingsphere.apache.org/document/current/cn/user-manual/common-config/builtin-algorithm/sharding
Java快速入门Java基础语法Java基础语法类型转换运算符键盘录入技术程序流程控制程序流程控制是控制代码怎么去执行的顺序结构代码按照顺便正常执行分支结构循环结构跳转关键字（break,continue）随机数Random类数组作用一个容器，用于在程序中存储一批同种类型的数据定义遍历常见问题方法作用封装一段代码的语法结构，可以被重复调用，以此提高代码的复用性，提高开发效率，让程序逻辑更清晰定义格式修饰符 返回值类型 方法名（形参列表）{ 方法体代码（需要执行的功能代码） return 返回值; } //如果方法没有返回结果，返回值类型申明为:void //方法需要调用才能执行注意事项方法重载return跳出并立即结束当前方法的执行面向对象编程（oop）定义（思想）java 万物皆对象创建对象构造器this关键字封装常用API（String、ArrayList)StringArrayListstatic饿汉单例代码示例：/** 使用饿汉单例实现单例类 */ public class SingleInstance { /** 2、饿汉单例是在获取对象前，对象已经提前准备好了一个。 这个对象只能是一个，所以定义静态成员变量记住。 */ public static SingleInstance instance = new SingleInstance(); /** 1、必须把构造器私有化。 */ private SingleInstance(){ } }懒汉单例代码示例：/** 懒汉单例 */ public class SingleInstance2 { /** 2、定义一个静态的成员变量负责存储一个对象。 只加载一次，只有一份。 注意：最好私有化，这样可以避免给别人挖坑！ */ private static SingleInstance2 instance; /** 3、提供一个方法，对外返回单例对象。 */ public static SingleInstance2 getInstance() { if(instance == null){ // 第一次来拿对象 ：此时需要创建对象。 instance = new SingleInstance2(); } return instance; } /** 1、私有化构造器 */ private SingleInstance2(){ } } 继承权限修饰符final常量枚举抽象类接口多态定义多态是同一个行为具有不同的表现形式或形态的能力同一方法可以根据发送对象的不同而采用不同的行为方式多态就是事物的多种形态，一个对象在不同条件下所表现的不同形式存在条件继承或实现：在多态中必须存在有继承或实现关系的子类和父类方法的重写：子类对父类中的某些方法进行重新定义（重写，使用@Override注解进行重写）基类引用指向派生类对象，即父类引用指向子类对象，父类类型：指子类对象继承的父类类型，或实现的父接口类型格式父类类型 变量名 = new 子类类型（）； 然后通过 变量名.方法名（）调用在子类中重写的方法 多态体现为父类引用变量可以指向子类对象：定义了一个父类类型的引用，指向新建的子类类型的对象，由于子类是继承他的父类的，所以父类类型的引用是可以指向子类类型的对象的特点多态情况下，子类和父类存在同名的成员变量时，访问的时父类的成员变量多态情况下，子父类存在同名的非静态成员方法时，访问的是子类中重写的方法多态情况下，子父类存在同名的静态成员变量成员方法时，访问的是父类的成员函数多态情况下，不能访问子类独由的方法instanceof关键字格式变量名 instanceof 数据类型 //如果变量属于该数据类型或者其子类型，返回true //如果变量不属于该数据类或者其子类型，返回false代码示例 public class DemoApplication { public static void main(String[] args) { //向上转型 //父类类型 对象 = new 子类类型（） Animal animal = new Cat(); //向下转型 //子类类型 子类变量名 = （子类类型） 父类变量名 if ( animal instanceof Cat){ Cat cat = (Cat) animal; cat.sleep(); }else if(animal instanceof Dog){ Dog dog = (Dog) animal; dog.walk(); } } }集合数组的长度是固定的。集合的长度是可变的。数组中存储的是同一类型的元素，可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候，使用集合进行存储。ColloctionColloction常用APIpublic boolean add(E e)： 把给定的对象添加到当前集合中 。 public void clear():清空集合中所有的元素。 public boolean remove(E e): 把给定的对象在当前集合中删除。 public boolean contains(Object obj): 判断当前集合中是否包含给定的对象。 public boolean isEmpty(): 判断当前集合是否为空。 public int size(): 返回集合中元素的个数。 public Object[] toArray(): 把集合中的元素，存储到数组中Iterator 迭代器迭代：即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素，如果有，就把这个元素取出来，继续在判断，如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。常用方法public E next():返回迭代的下一个元素。 public boolean hasNext():如果仍有元素可以迭代，则返回 true。代码示例public class IteratorDemo { public static void main(String[] args) { // 使用多态方式 创建对象 Collection<String> coll = new ArrayList<String>(); // 添加元素到集合 coll.add("串串星人"); coll.add("吐槽星人"); coll.add("汪星人"); //遍历 //使用迭代器 遍历 每个集合对象都有自己的迭代器 Iterator<String> it = coll.iterator(); // 泛型指的是 迭代出 元素的数据类型 while(it.hasNext()){ //判断是否有迭代元素 String s = it.next();//获取迭代出的元素 System.out.println(s); } } } //注意，如果集合没有元素使用迭代器的next的方法会java.util.NoSuchElementException没有集合元素的错误。删除元素常用在集合Collection的remove方法无法根据条件删除代码示例： @Test public void test02(){ Collection<String> coll = new ArrayList<>(); coll.add("陈琦"); coll.add("李晨"); coll.add("邓超"); coll.add("黄晓明"); //删除名字有三个字的 // coll.remove(o)//无法编写 Iterator<String> iterator = coll.iterator(); while(iterator.hasNext()){ String element = iterator.next(); if(element.length()==3){ // coll.remove(element);//错误的 iterator.remove(); } } System.out.println(coll); } 增强for示例：for(元素的数据类型 变量 : Collection集合or数组){ //写操作代码 }List特点List集合所有的元素是以一种线性方式进行存储的，例如，存元素的顺序是11、22、33。那么集合中，元素的存储就是按照11、22、33的顺序完成的）它是一个元素存取有序的集合。即元素的存入顺序和取出顺序一致。它是一个带有索引的集合，通过索引就可以精确的操作集合中的元素（与数组的索引是一个道理）。集合中可以有重复的元素，通过元素的equals方法，来比较是否为重复的元素。常用方法1.添加元素 void add(int index, E ele) boolean addAll(int index, Collection<? extends E> eles) 2、获取元素 E get(int index) List subList(int fromIndex, int toIndex) 3、获取元素索引 int indexOf(Object obj) int lastIndexOf(Object obj) 4、删除和替换元素 E remove(int index) E set(int index, E ele)实现类ArrayList集合java.util.ArrayList集合数据存储的结构是数组结构。元素增删慢，查找快，由于日常开发中使用最多的功能为查询数据、遍历数据，所以ArrayList是最常用的集合。代码示例class Example2{ public static void main(String[] args) { ArrayList arrayList = new ArrayList(); arrayList.add("a"); arrayList.add("b"); arrayList.add("c"); System.out.println("arrayList的元素为："+arrayList); arrayList.set(0,"c"); //将索引为０的位置对象a修改为对象c System.out.println("arrayList的元素为"+arrayList); arrayList.add(1,"e"); //将对象e添加到索引为１的位置 System.out.print("arrayList的元素为:"); for (int i=0;i<arrayList.size();i++){ System.out.print(arrayList.get(i));//for循环迭代arrayList集合元素 } System.out.println(""); System.out.println("arrayList指定元素c位置的索引为"+arrayList.indexOf("c")); //返回列表中指定元素c位置的索引 System.out.println("arrayList指定元素c最后位置的索引为"+arrayList.lastIndexOf("c")); //返回列表中指定元素c最后位置的索引 System.out.println("arrayList的指定区域为"+arrayList.subList(1,2)); //返回列表中指返回一个指定区域的List集合对象[1,2) } }LinkedList集合java.util.LinkedList集合数据存储的结构是链表结构(双链表)。方便元素添加、删除的集合。代码示例public class Example3 { public static void main(String[] args) { LinkedList linkedList = new LinkedList(); linkedList.add("a"); linkedList.add("b"); linkedList.add("c"); linkedList.add("d"); //获得并输入列表开头的对象 System.out.println("列表开头元素为："+linkedList.getFirst()+"列表结尾元素为："+linkedList.getLast()); linkedList.addFirst("rr"); //向列表开头添加一个对象 System.out.println("列表中所有元素："+linkedList); linkedList.removeLast();//移除列表结尾元素 System.out.println("列表结尾元素为："+linkedList.getLast()); //获取并输出列表结尾的对象 } }ListlteratorList 集合额外提供了一个 listIterator() 方法，该方法返回一个 ListIterator 对象， ListIterator 接口继承了 Iterator 接口，提供了专门操作 List 的方法：代码示例 public static void main(String[] args) { List<Student> c = new ArrayList<>(); c.add(new Student(1,"张三")); c.add(new Student(2,"李四")); c.add(new Student(3,"王五")); c.add(new Student(4,"赵六")); c.add(new Student(5,"钱七")); //从指定位置往前遍历 ListIterator<Student> listIterator = c.listIterator(c.size()); while(listIterator.hasPrevious()){ Student previous = listIterator.previous(); System.out.println(previous); } }Set特点特点：无序，不重复遍历：foreach，迭代器扩容： 初始容量16，负载因子0.75，扩容增量1倍实现类HsshSet特点它存储唯一元素并允许空值，依据对象的hashcode来确定该元素是否存在由HashMap支持不保持插入顺序（无序）非线程安全性能参数：初始容量，负载因子，默认值： 初始容量16，负载因子0.75，示例：new HashSet<>(20, 0.5f);注意HashSet 按 Hash 算法来存储集合中的元素，因此具有很好的存取和查找性能。HashSet 集合判断两个元素相等的标准：两个对象通过 hashCode() 方法比较相等，并且两个对象的 equals() 方法返回值也相等。因此，存储到HashSet的元素要重写hashCode和equals方法。LinkedHashSetLinkedHashSet是HashSet的子类，它在HashSet的基础上，在结点中增加两个属性before和after维护了结点的前后添加顺序。java.util.LinkedHashSet，它是链表和哈希表组合的一个数据存储结构。LinkedHashSet插入性能略低于 HashSet，但在迭代访问 Set 里的全部元素时有很好的性能。(保持插入顺序)TreeSet特点是一个包含有序的且没有重复元素的集合作用是提供有序的Set集合，自然排序或者根据提供的Comparator进行排序TreeSet是基于TreeMap实现的代码示例//演示两种自定义排序方法 //1. 较器通过构造函数传入比 TreeSet<Integer> tset = new TreeSet<Integer>(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { // TODO Auto-generated method stub return o2 - o1; } }); //2.对象类中定义 @Override public int compareTo(Student o) { if (this.getAge() - o.getAge() == 0) { return this.getSid() - o.getSid(); } return this.getAge() - o.getAge(); }Map概述现实生活中，我们常会看到这样的一种集合：IP地址与主机名，身份证号与个人，系统用户名与系统用户对象等，这种一一对应的关系，就叫做映射。Java提供了专门的集合类用来存放这种对象关系的对象，即java.util.Map<K,V>接口。我们通过查看Map接口描述，发现Map<K,V>接口下的集合与Collection<E>接口下的集合，它们存储数据的形式不同。Collection中的集合，元素是孤立存在的（理解为单身），向集合中存储元素采用一个个元素的方式存储。Map中的集合，元素是成对存在的(理解为夫妻)。每个元素由键与值两部分组成，通过键可以找对所对应的值。Collection中的集合称为单列集合，Map中的集合称为双列集合。需要注意的是，Map中的集合不能包含重复的键，值可以重复；每个键只能对应一个值（这个值可以是单个值，也可以是个数组或集合值）。常用APIpublic class MapDemo { public static void main(String[] args) { //创建 map对象 HashMap<String, String> map = new HashMap<String, String>(); //添加元素到集合 map.put("黄晓明", "杨颖"); map.put("文章", "马伊琍"); map.put("邓超", "孙俪"); System.out.println(map); //String remove(String key) System.out.println(map.remove("邓超")); System.out.println(map); // 想要查看 黄晓明的媳妇 是谁 System.out.println(map.get("黄晓明")); System.out.println(map.get("邓超")); } } //使用put方法时，若指定的键(key)在集合中没有，则没有这个键对应的值，返回null，并把指定的键值添加到集合中； //若指定的键(key)在集合中存在，则返回值为集合中键对应的值（该值为替换前的值），并把指定键所对应的值，替换成指定的新值。遍历public class TestMap { public static void main(String[] args) { HashMap<String,String> map = new HashMap<>(); map.put("许仙", "白娘子"); map.put("董永", "七仙女"); map.put("牛郎", "织女"); map.put("许仙", "小青"); //单独遍历key和值 System.out.println("所有的key:"); Set<String> keySet = map.keySet(); for (String key : keySet) { System.out.println(key); } System.out.println("所有的value："); Collection<String> values = map.values(); for (String value : values) { System.out.println(value); } //同时遍历key-value entrySet() System.out.println("所有的映射关系"); Set<Map.Entry<String,String>> entrySet = map.entrySet(); for (Map.Entry<String,String> entry : entrySet) { System.out.println(entry.getKey()+"->"+entry.getValue()); } } } 实现类HashMapHashMap线程不安全，允许使用null键和null值；Hashtable是线程安全的，任何非 null 对象都可以用作键或值。LinkedHashMap维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序，该迭代顺序通常就是将键插入到映射中的顺序（插入顺序）。TreeMap基于红黑树（Red-Black tree）的 NavigableMap 实现。该映射根据其键的自然顺序进行排序，或者根据创建映射时提供的 Comparator 进行排序，具体取决于使用的构造方法。package com.atguigu.map; import java.util.Comparator; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import org.junit.Test; public class TestTreeMap { @Test public void test1() { TreeMap<String,Integer> map = new TreeMap<>(); map.put("Jack", 11000); map.put("Alice", 12000); map.put("zhangsan", 13000); map.put("baitao", 14000); map.put("Lucy", 15000); //String实现了Comparable接口，默认按照Unicode编码值排序 Set<Entry<String, Integer>> entrySet = map.entrySet(); for (Entry<String, Integer> entry : entrySet) { System.out.println(entry); } } @Test public void test2() { //指定定制比较器Comparator，按照Unicode编码值排序，但是忽略大小写 TreeMap<String,Integer> map = new TreeMap<>(new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.compareToIgnoreCase(o2); } }); map.put("Jack", 11000); map.put("Alice", 12000); map.put("zhangsan", 13000); map.put("baitao", 14000); map.put("Lucy", 15000); Set<Entry<String, Integer>> entrySet = map.entrySet(); for (Entry<String, Integer> entry : entrySet) { System.out.println(entry); } } }Collections工具类Collections 是一个操作 Set、List 和 Map 等集合的工具类。Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作，还提供了对集合对象设置不可变、对集合对象实现同步控制等方法：public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)在coll集合中找出最大的元素，集合中的对象必须是T或T的子类对象，而且支持自然排序 public static T max(Collection<? extends T> coll,Comparator<? super T> comp)在coll集合中找出最大的元素，集合中的对象必须是T或T的子类对象，按照比较器comp找出最大者 public static void reverse(List<?> list)反转指定列表List中元素的顺序。 public static void shuffle(List<?> list) List 集合元素进行随机排序，类似洗牌 public static <T extends Comparable<? super T>> void sort(List list)根据元素的自然顺序对指定 List 集合元素按升序排序 public static void sort(List list,Comparator<? super T> c)根据指定的 Comparator 产生的顺序对 List 集合元素进行排序 public static void swap(List<?> list,int i,int j)将指定 list 集合中的 i 处元素和 j 处元素进行交换 public static int frequency(Collection<?> c,Object o)返回指定集合中指定元素的出现次数 public static void copy(List<? super T> dest,List<? extends T> src)将src中的内容复制到dest中 public static boolean replaceAll(List list，T oldVal，T newVal)：使用新值替换 List 对象的所有旧值StreamStream流是对集合（Collection）对象功能的增强，与Lambda表达式结合，可以提高编程效率、间接性和程序可读性。特点1、代码简洁：函数式编程写出的代码简洁且意图明确，使用stream接口让你从此告别for循环2、多核友好：Java函数式编程使得编写并行程序如此简单，就是调用一下方法流程创建流Stream创建Stream<Integer> stream1 = Stream.of(1,2,3,4,5);Collection集合创建（应用中最常用的一种）List<Integer> integerList = new ArrayList<>(); integerList.add(1); integerList.add(2); integerList.add(3); integerList.add(4); integerList.add(5); Stream<Integer> listStream = integerList.stream();Array数组创建int[] intArr = {1, 2, 3, 4, 5}; IntStream arrayStream = Arrays.stream(intArr);通过Arrays.stream方法生成流，并且该方法生成的流是数值流【即IntStream】而不是 Stream注：使用数值流可以避免计算过程中拆箱装箱，提高性能。Stream API提供了mapToInt、mapToDouble、mapToLong三种方式将对象流【即Stream 】转换成对应的数值流，同时提供了boxed方法将数值流转换为对象流文件创建try { Stream<String> fileStream = Files.lines(Paths.get("data.txt"), Charset.defaultCharset()); } catch (IOException e) { e.printStackTrace(); } //把流创建在try(){}的()里可以在结束时自动关闭该流通过Files.line方法得到一个流，并且得到的每个流是给定文件中的一行函数创建iteratorStream<Integer> iterateStream = Stream.iterate(0, n -> n + 2).limit(5);iterate方法接受两个参数，第一个为初始化值，第二个为进行的函数操作，因为iterator生成的流为无限流，通过limit方法对流进行了截断，只生成5个偶数 generatorStream<Double> generateStream = Stream.generate(Math::random).limit(5);generate方法接受一个参数，方法参数类型为Supplier ，由它为流提供值。generate生成的流也是无限流，因此通过limit对流进行了截断操作流中间操作符//1、filter：输出ID大于6的user对象(过滤) List<User> filetrUserList = userList.stream().filter(user -> user.getId() > 6).collect(Collectors.toList()); filetrUserList.forEach(System.out::println); //2、map List<String> mapUserList = userList.stream().map(user -> user.getName() + "用户").collect(Collectors.toList()); mapUserList.forEach(System.out::println); //3、distinct：去重 //User::getCity() 完全写法为：user -> user.getCity() List<String> distinctUsers = userList.stream().map(User::getCity).distinct().collect(Collectors.toList()); distinctUsers.forEach(System.out::println); //4、sorted：排序，根据名字倒序 userList.stream().sorted(Comparator.comparing(User::getName).reversed()).collect(Collectors.toList()).forEach(System.out::println); //5、limit：取前5条数据 userList.stream().limit(5).collect(Collectors.toList()).forEach(System.out::println); //6、skip：跳过第几条取后几条 userList.stream().skip(7).collect(Collectors.toList()).forEach(System.out::println); //7、flatMap：数据拆分一对多映射 userList.stream().flatMap(user -> Arrays.stream(user.getCity().split(","))).forEach(System.out::println); //8、peek：对元素进行遍历处理，每个用户ID加1输出 userList.stream().peek(user -> user.setId(user.getId()+1)).forEach(System.out::println);终端操作符//1、collect：收集器，将流转换为其他形式 Set set = userList.stream().collect(Collectors.toSet()); set.forEach(System.out::println); System.out.println("--------------------------"); List list = userList.stream().collect(Collectors.toList()); list.forEach(System.out::println); //2、forEach：遍历流 userList.stream().forEach(user -> System.out.println(user)); userList.stream().filter(user -> "上海".equals(user.getCity())).forEach(System.out::println); //3、findFirst：返回第一个元素 User firstUser = userList.stream().findFirst().get(); User firstUser1 = userList.stream().filter(user -> "上海".equals(user.getCity())).findFirst().get(); //4、findAny：将返回当前流中的任意元素 User findUser = userList.stream().findAny().get(); User findUser1 = userList.stream().filter(user -> "上海".equals(user.getCity())).findAny().get(); //5、count：返回流中元素总数 long count = userList.stream().filter(user -> user.getAge() > 20).count(); System.out.println(count); //6、sum：求和 int sum = userList.stream().mapToInt(User::getId).sum(); //7、max：最大值 int max = userList.stream().max(Comparator.comparingInt(User::getId)).get().getId(); //8、min：最小值 int min = userList.stream().min(Comparator.comparingInt(User::getId)).get().getId(); //9、anyMatch：检查是否至少匹配一个元素 boolean matchAny = userList.stream().anyMatch(user -> "北京".equals(user.getCity())); //10、allMatch：检查是否匹配所有元素 boolean matchAll = userList.stream().allMatch(user -> "北京".equals(user.getCity())); //11、noneMatch：检查是否没有匹配所有元素，返回boolean boolean nonaMatch = userList.stream().allMatch(user -> "云南".equals(user.getCity())); //12、reduce：将流中元素反复结合起来，得到一个值 Optional reduce = userList.stream().reduce((user, user2) -> { return user; }); if(reduce.isPresent()) System.out.println(reduce.get());Collect收集//1.toList() 将用户ID存放到List集合中 List<Integer> idList = userList.stream().map(User::getId).collect(Collectors.toList()) ; //2.toMap() 将用户ID和Name以Key-Value形式存放到Map集合中 Map<Integer,String> userMap = userList.stream().collect(Collectors.toMap(User::getId,User::getName)); //3.toSet() 将用户所在城市存放到Set集合中 Set<String> citySet = userList.stream().map(User::getCity).collect(Collectors.toSet()); //4.counting() 符合条件的用户总数 long count = userList.stream().filter(user -> user.getId()>1).collect(Collectors.counting()); //5.summingInt() 对结果元素即用户ID求和 Integer sumInt = userList.stream().filter(user -> user.getId()>2).collect(Collectors.summingInt(User::getId)) ; //6.minBy() 筛选元素中ID最小的用户 User maxId = userList.stream().collect(Collectors.minBy(Comparator.comparingInt(User::getId))).get() ; //7.joining() 将用户所在城市，以指定分隔符链接成字符串； String joinCity = userList.stream().map(User::getCity).collect(Collectors.joining("||")); //8.groupingBy() 按条件分组，以城市对用户进行分组； Map<String,List<User>> groupCity = userList.stream().collect(Collectors.groupingBy(User::getCity));异常try( //此处使用如stream流之类的需要使用后关闭的 ){ //此处使用正常的主体代码 }catch(Exception e){ //打印异常或写入日志 }finally{ //不管程序是否异常，这里都会执行 }File和IO流File对象创建File对象 public static void main(String[] args) { //创建一个File对象 并指定文件得路径 \转移字符串 //第一种Windows通用两种分隔符 File file01 = new File("D:\\AAA\\aaa.txt"); File file02 = new File("D:/AAA/bbb.txt"); // 第二种 File.separator根据当前代码所在得系统自动获取相应得分割符 System.out.println(File.separator); // \因为我们当前在window下 File file03 = new File("D:"+File.separator+"AAA"+File.separator+"ccc.txt");添加文件 //创建文件对象 File file = new File("F:/AAA/a.txt"); //添加文件操作 file.createNewFile(); File file2 = new File("F:/AAA/bbb"); file2.mkdir();//创建单层文件夹 File file3 = new File("F:/AAA/bbb/ccc"); file3.mkdirs();//创建多层文件夹删除操作 //创建文件对象 File file = new File("F:/AAA/a.txt"); //删除操作 file.delete();//删除文件，同时可以删除空的文件夹 file3.deleteOnExit();//程序退出时删除文件修改文件 //创建文件对象 File file = new File("F:/AAA/a.txt"); //修改操作 file.setReadable(false);//修改能否打开文件的权限 file.setWritable(false);//修改能否修改文件中的内容的权限 file.setReadOnly();//修改文件为只读 file.renameTo(new File("D:/AAA/aaa.txt"));//给文件重命名查询文件 //创建文件对象 File file = new File("F:/AAA/a.txt"); //查询操作 String name = file.getName();//获取文件名称 String parent = file.getParent();//获取父级的名称 String path = file.getPath();//获取路径 System.out.println(name + " "+ parent+" "+path); boolean f1 = file.isFile();//是否为文件类型 boolean f2 = file.isDirectory();//是否为文件夹类型 System.out.println(f1 + " "+f2);遍历public class test { public static void main(String[] args) { String[] s=new String[100]; File f=new File("D:/个人文件"); s=f.list(); //获取当前目录下所有的一级文件名称到一个字符串数组中返回 for(String ss:s){ System.out.println(ss); } File[] s1=new File[100]; File f1=new File("D:/个人文件"); s1=f1.listFiles();//获取当前目录下所有的一级文件对象到一个对象数组中返回（重点） for(File ss1:s1){ System.out.println(ss1); } } } IO流IO流即输入输出流，用于处理数据的传输字符集编码、解码操作字节流字节流基类字节输入流(InputStream) 常用方法： // 从输入流中读取数据的下一个字节 abstract int read() // 从输入流中读取一定数量的字节，并将其存储在缓冲区数组 b中 int read(byte[] b) // 将输入流中最多 len 个数据字节读入 byte 数组 int read(byte[] b, int off, int len) // 跳过和丢弃此输入流中数据的 n个字节 long skip(long n) // 关闭此输入流并释放与该流关联的所有系统资源 void close()字节输出流(OutputStream) 常用方法： // 将 b.length 个字节从指定的 byte 数组写入此输出流 void write(byte[] b) // 将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流 void write(byte[] b, int off, int len) // 将指定的字节写入此输出流 abstract void write(int b) // 关闭此输出流并释放与此流有关的所有系统资源 void close() // 刷新此输出流并强制写出所有缓冲的输出字节 void flush()字节文件操作流字节文件输入流(FileInputStream) 构造方法： // 通过打开一个到实际文件的连接来创建一个FileInputStream，该文件通过文件系统中的File对象file指定 FileInputStream(File file) // 通过打开一个到实际文件的连接来创建一个FileInputStream，该文件通过文件系统中的路径name指定 FileInputStream(String name) //示例（使用字节数组） //构造方法 InputStream inputStream2 = new FileInputStream("f://hello/test.txt"); // 字节数组 byte[] b = new byte[2]; int i2 = 0; // 一次读取一个字节数组 while ((i2 = inputStream2.read(b)) != -1) { System.out.print(new String(b, 0, i2) + " ");// AB CD } //关闭IO流 inputStream2.close();字节文件输出流(FileOutputStream) 构造方法： // 创建一个向指定File对象表示的文件中写入数据的文件输出流 FileOutputStream(File file) // 创建一个向指定File对象表示的文件中写入数据的文件输出流 FileOutputStream(File file, boolean append) // 创建一个向具有指定名称的文件中写入数据的输出文件流 FileOutputStream(String name) // 创建一个向具有指定name的文件中写入数据的输出文件流 FileOutputStream(String name, boolean append) OutputStream outputStream = new FileOutputStream(new File("test.txt")); // 写出数据 outputStream.write("ABCD".getBytes()); // 关闭IO流 outputStream.close(); // 内容追加写入 OutputStream outputStream2 = new FileOutputStream("test.txt", true); // 输出换行符 outputStream2.write("\r\n".getBytes()); // 输出追加内容 outputStream2.write("hello".getBytes()); // 关闭IO流 outputStream2.close();字节缓冲流(高级流)字节缓存输入流(BufferedInputStream) 构造方法： // 创建一个 BufferedInputStream并保存其参数，即输入流in，以便将来使用。 BufferedInputStream(InputStream in) // 创建具有指定缓冲区大小的 BufferedInputStream并保存其参数，即输入流in以便将来使用 BufferedInputStream(InputStream in, int size) //示例 InputStream in = new FileInputStream("test.txt"); // 字节缓存流 BufferedInputStream bis = new BufferedInputStream(in); byte[] bs = new byte[20]; int len = 0; while ((len = bis.read(bs)) != -1) { System.out.print(new String(bs, 0, len)); } // 关闭流 bis.close();字节缓存输出流(BufferedOutputStream) 构造方法： // 创建一个新的缓冲输出流，以将数据写入指定的底层输出流 BufferedOutputStream(OutputStream out) // 创建一个新的缓冲输出流，以将具有指定缓冲区大小的数据写入指定的底层输出流 BufferedOutputStream(OutputStream out, int size) 常用方法： // 将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此缓冲的输出流 void write(byte[] b, int off, int len) // 将指定的字节写入此缓冲的输出流 void write(int b) // 刷新此缓冲的输出流 void flush() //示例 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("test.txt", true)); // 输出换行符 bos.write("\r\n".getBytes()); // 输出内容 bos.write("Hello Android".getBytes()); // 刷新此缓冲的输出流 bos.flush(); // 关闭流 bos.close();字符流字符流基类字符输入流(Reader) 常用方法： // 读取单个字符 int read() // 将字符读入数组 int read(char[] cbuf) // 将字符读入数组的某一部分 abstract int read(char[] cbuf, int off, int len) // 跳过字符 long skip(long n) // 关闭该流并释放与之关联的所有资源 abstract void close()字符输出流(Write) 常用方法： // 写入字符数组 void write(char[] cbuf) // 写入字符数组的某一部分 abstract void write(char[] cbuf, int off, int len) // 写入单个字符 void write(int c) // 写入字符串 void write(String str) // 写入字符串的某一部分 void write(String str, int off, int len) // 将指定字符添加到此 writer Writer append(char c) // 将指定字符序列添加到此 writer Writer append(CharSequence csq) // 将指定字符序列的子序列添加到此 writer.Appendable Writer append(CharSequence csq, int start, int end) // 关闭此流，但要先刷新它 abstract void close() // 刷新该流的缓冲 abstract void flush()字符转换流InputStreamReader(字节流转字符流) 构造方法： // 创建一个使用默认字符集的 InputStreamReader InputStreamReader(InputStream in) // 创建使用给定字符集的 InputStreamReader InputStreamReader(InputStream in, Charset cs) // 创建使用给定字符集解码器的 InputStreamReader InputStreamReader(InputStream in, CharsetDecoder dec) // 创建使用指定字符集的 InputStreamReader InputStreamReader(InputStream in, String charsetName) 特有方法： //返回此流使用的字符编码的名称 String getEncoding() //代码示例 //使用默认编码 InputStreamReader reader = new InputStreamReader(new FileInputStream("test.txt")); int len; while ((len = reader.read()) != -1) { System.out.print((char) len);//爱生活，爱Android } reader.close(); //指定编码 InputStreamReader reader = new InputStreamReader(new FileInputStream("test.txt"),"utf-8"); int len; while ((len = reader.read()) != -1) { System.out.print((char) len);//????????Android } reader.close();OutputStreamWriter(字节流转字符流) 构造方法： // 创建使用默认字符编码的 OutputStreamWriter OutputStreamWriter(OutputStream out) // 创建使用给定字符集的 OutputStreamWriter OutputStreamWriter(OutputStream out, Charset cs) // 创建使用给定字符集编码器的 OutputStreamWriter OutputStreamWriter(OutputStream out, CharsetEncoder enc) // 创建使用指定字符集的 OutputStreamWriter OutputStreamWriter(OutputStream out, String charsetName) 特有方法： //返回此流使用的字符编码的名称 String getEncoding() 字符缓冲流BufferedReader 构造方法： // 创建一个使用默认大小输入缓冲区的缓冲字符输入流 BufferedReader(Reader in) // 创建一个使用指定大小输入缓冲区的缓冲字符输入流 BufferedReader(Reader in, int sz) 特有方法： // 读取一个文本行 String readLine() //代码示例 //生成字符缓冲流对象 BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("test.txt"))); String str; //一次性读取一行 while ((str = reader.readLine()) != null) { System.out.println(str);// 爱生活，爱Android } //关闭流 reader.close();BufferedWriter 构造方法： // 创建一个使用默认大小输出缓冲区的缓冲字符输出流 BufferedWriter(Writer out) // 创建一个使用给定大小输出缓冲区的新缓冲字符输出流 BufferedWriter(Writer out, int sz) 特有方法： // 写入一个行分隔符 void newLine() 线程创建线程：方式一方式二匿名写法：方式三三种方式对比总结线程Thread常用方法、构造器线程安全问题原因：多个线程同时访问同一个共享资源且存在修改该资源解决方案：加锁，把共享资源进行上锁，每次只能一个线程进入访问完毕以后解锁，然后其他线程才能进来同步代码块：synchronized(同步锁对象){ 操作共享资源的代码（核心代码） }锁对象规范要求：同步方法：同步代码块与同步方法：同步代码块锁的范围更小，同步方法锁的范围更大。Lock锁：线程通信线程池（重点）定时器Timer定时器：本身是一个单线程ScheduledExecutorService定时器：多线程，基于线程池并发、并行并发：CPU分时轮询的执行线程并行：同一时刻同时在执行线程的生命周期网络编程（通信）实现网络编程关键的三要素IP端口号协议网络通信协议：连接和通信数据的规则UDP协议代码示例（默认支持多个客户端）//客户端 import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.util.Scanner; public class ClientDemo1 { public static void main(String[] args) throws Exception { //1.创建发送端对象：发送端自带默认的端口号（人） DatagramSocket socket = new DatagramSocket(); Scanner sc = new Scanner(System.in); while (true) { System.out.println("请输入发送的消息："); String msg = sc.nextLine(); if ("exit".equals(msg)) { System.out.println("离线成功！"); socket.close(); break; } //2.创建一个数据包对象封装数据 byte[] buffer = msg.getBytes(); //封装发送的数据（字节）、发送数据的大小、服务端IP地址（本次使用的是本机）、服务端端口 DatagramPacket packet = new DatagramPacket(buffer, buffer.length, InetAddress.getLoopbackAddress(), 8888); //3.发送数据 socket.send(packet); } } } //服务端 import java.net.DatagramPacket; import java.net.DatagramSocket; public class SercerDemo1 { public static void main(String[] args) throws Exception { //1.创建接收端对象：注册端口 DatagramSocket socket = new DatagramSocket(8888); //2.创建一个数据包对象接收数据 byte[] buffer = new byte[1024 * 64]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); while (true) { //3.等待接收数据 socket.receive(packet); //4.取出数据 //读取多少取多少 int len = packet.getLength(); String rs = new String(buffer, 0, len); System.out.println("收到了来自：" + packet.getAddress() + ",对方端口是：" + packet.getPort() + "的消息：" + rs); } } }广播、组播TCP协议代码示例（默认不支持多个客户端）-->需要使用多线程去完成处理多个客户端消息(只需服务端进行多线程修改)//客户端 import java.io.OutputStream; import java.io.PrintStream; import java.net.Socket; import java.util.Scanner; public class ClientDemo1 { public static void main(String[] args) { try { //1.创建Socket通信管道请求服务端连接 //服务端IP地址、服务端端口 Socket socket = new Socket("127.0.0.1", 8888); //2.从socket通讯管道中得到一个字节输出流，负责发送 OutputStream os = socket.getOutputStream(); //3.把低级的字节流包装成打印流 PrintStream ps = new PrintStream(os); Scanner sc = new Scanner(System.in); while (true) { System.out.println("请输入你想要发送的消息："); String msg = sc.nextLine(); //4.发送消息并刷新 ps.println(msg); //注意这里必须至少要一行消息，所有需要使用println ps.flush(); } } catch (Exception e) { throw new RuntimeException(e); } } } //服务端 import java.net.ServerSocket; import java.net.Socket; public class ServerDemo1 { public static void main(String[] args) { //1.注册端口 try { ServerSocket serverSocket = new ServerSocket(7777); //a.定义一个死循环由主线程负责不断地接收客户端的Socket管道连接 while (true) { //2.必须调用accept方法：等待接受客户端的Socket连接请求，建立Socket通信管道 Socket socket = serverSocket.accept(); //用于客户端上线提醒 System.out.println(socket.getRemoteSocketAddress() + "上线了！"); //下面注释代码为不使用线程只能一个客户端使用的情况 // //3.从socket通信管道中得到一个字节输入流 // InputStream is = socket.getInputStream(); // //4.把字节输入流1包装成缓冲字符输入流进行消息接收 // BufferedReader br = new BufferedReader(new InputStreamReader(is)); // //5.按照行读取消息 // String msg; // while ((msg = br.readLine()) != null) { // System.out.println(socket.getRemoteSocketAddress() + "说了：" + msg); // } //3.开始创建独立线程处理Socket new ServerReaderThread(socket).start(); } } catch (Exception e) { throw new RuntimeException(e); } } } //线程类（ServerReaderThread.java） import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.net.Socket; public class ServerReaderThread extends Thread { private Socket socket; public ServerReaderThread(Socket socket) { this.socket = socket; } @Override public void run() { try { //3.从socket通信管道中得到一个字节输入流 InputStream is = socket.getInputStream(); //4.把字节输入流1包装成缓冲字符输入流进行消息接收 BufferedReader br = new BufferedReader(new InputStreamReader(is)); //5.按照行读取消息 String msg; while ((msg = br.readLine()) != null) { System.out.println(socket.getRemoteSocketAddress() + "说了：" + msg); } } catch (Exception e) { //可以用于客户端下线了提醒 System.out.println(socket.getRemoteSocketAddress() + "下线了！"); } } } 使用线程池代码示例//客户端（和之前一样） import java.io.OutputStream; import java.io.PrintStream; import java.net.Socket; import java.util.Scanner; public class ClientDemo1 { public static void main(String[] args) { try { //1.创建Socket通信管道请求服务端连接 //服务端IP地址、服务端端口 Socket socket = new Socket("127.0.0.1", 7777); //2.从socket通讯管道中得到一个字节输出流，负责发送 OutputStream os = socket.getOutputStream(); //3.把低级的字节流包装成打印流 PrintStream ps = new PrintStream(os); Scanner sc = new Scanner(System.in); while (true) { System.out.println("请输入你想要发送的消息："); String msg = sc.nextLine(); //4.发送消息并刷新 ps.println(msg); //注意这里必须至少要一行消息，所有需要使用println ps.flush(); } } catch (Exception e) { throw new RuntimeException(e); } } } //服务端（使用线程池，完成服务端可以同时处理多个客户端消息） import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.*; public class ServerDemo1 { //使用静态变量记住一个线程池对象 private static ExecutorService pool = new ThreadPoolExecutor(3, 5, 6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); public static void main(String[] args) { try { //1.注册端口 ServerSocket serverSocket = new ServerSocket(7777); while (true) { //2.每接收到一个客户端的Socket管道 Socket socket = serverSocket.accept(); System.out.println(socket.getRemoteSocketAddress() + "上线了！"); Runnable target = new ServerReaderRunnable(socket); pool.execute(target); } } catch (Exception e) { throw new RuntimeException(e); } } } //线程池类（ServerReaderRunnable.java） import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.net.Socket; public class ServerReaderRunnable implements Runnable { private Socket socket; public ServerReaderRunnable(Socket socket) { this.socket = socket; } @Override public void run() { try { //3.从socket通信管道中得到一个字节输入流 InputStream is = socket.getInputStream(); //4.把字节输入流1包装成缓冲字符输入流进行消息接收 BufferedReader br = new BufferedReader(new InputStreamReader(is)); //5.按照行读取消息 String msg; while ((msg = br.readLine()) != null) { System.out.println(socket.getRemoteSocketAddress() + "说了：" + msg); } } catch (Exception e) { //可以用于客户端下线了提醒 System.out.println(socket.getRemoteSocketAddress() + "下线了！"); } } } 使用线程池优点：实战案例-即时通信//客户端 import java.io.OutputStream; import java.io.PrintStream; import java.net.Socket; import java.util.Scanner; public class ClientDemo1 { public static void main(String[] args) { try { //1.创建Socket通信管道请求服务端连接 //服务端IP地址、服务端端口 Socket socket = new Socket("127.0.0.1", 7777); //创建一个独立的线程专门负责这个客户端的读消息(服务端随时可发消息过来) new ClientReaderThread(socket).start(); //2.从socket通讯管道中得到一个字节输出流，负责发送 OutputStream os = socket.getOutputStream(); //3.把低级的字节流包装成打印流 PrintStream ps = new PrintStream(os); Scanner sc = new Scanner(System.in); while (true) { System.out.println("请输入你想要发送的消息："); String msg = sc.nextLine(); //4.发送消息并刷新 ps.println(msg); //注意这里必须至少要一行消息，所有需要使用println ps.flush(); } } catch (Exception e) { throw new RuntimeException(e); } } } //客户端线程类（ClientReaderThread.java） import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.net.Socket; public class ClientReaderThread extends Thread { private Socket socket; public ClientReaderThread(Socket socket) { this.socket = socket; } @Override public void run() { try { //3.从socket通信管道中得到一个字节输入流 InputStream is = socket.getInputStream(); //4.把字节输入流1包装成缓冲字符输入流进行消息接收 BufferedReader br = new BufferedReader(new InputStreamReader(is)); //5.按照行读取消息 String msg; while ((msg = br.readLine()) != null) { System.out.println("收到消息：" + msg); } } catch (Exception e) { //可以用于客户端下线了提醒 System.out.println("服务端把你踢出去了"); } } } //服务端 import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.List; public class ServerDemo1 { //定义一个静态的List集合存储当前全部在线的socket管道 public static List<Socket> allOnlineSockets = new ArrayList<>(); public static void main(String[] args) { //1.注册端口 try { ServerSocket serverSocket = new ServerSocket(7777); //a.定义一个死循环由主线程负责不断地接收客户端的Socket管道连接 while (true) { //2.必须调用accept方法：等待接受客户端的Socket连接请求，建立Socket通信管道 //注意，在这里等待客户端的socket管道连接 Socket socket = serverSocket.accept(); //用于客户端上线提醒 System.out.println(socket.getRemoteSocketAddress() + "上线了！"); allOnlineSockets.add(socket); //3.开始创建独立线程处理Socket new ServerReaderThread(socket).start(); } } catch (Exception e) { throw new RuntimeException(e); } } } //服务端线程类（ServerReaderThread.java） import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.Socket; public class ServerReaderThread extends Thread { private Socket socket; public ServerReaderThread(Socket socket) { this.socket = socket; } @Override public void run() { try { //3.从socket通信管道中得到一个字节输入流 InputStream is = socket.getInputStream(); //4.把字节输入流包装成缓冲字符输入流进行消息接收 BufferedReader br = new BufferedReader(new InputStreamReader(is)); //5.按照行读取消息 String msg; while ((msg = br.readLine()) != null) { System.out.println(socket.getRemoteSocketAddress() + "说了：" + msg); //把消息进行端口转发给全部客户端socket管道 sendMsgToAll(msg); } } catch (Exception e) { //可以用于客户端下线了提醒 System.out.println(socket.getRemoteSocketAddress() + "下线了！"); ServerDemo1.allOnlineSockets.remove(socket); } } private void sendMsgToAll(String msg) throws Exception { for (Socket socket : ServerDemo1.allOnlineSockets) { PrintStream ps = new PrintStream(socket.getOutputStream()); ps.println(msg); ps.flush(); } } } 大杂烩单元测试*代码示例：常用注解反射反射的作用1.绕过编译阶段为集合添加数据2.通用框架的底层原理注解自定义注解元注解用在自定义注解上面的注解，用于限制自定义注解注解的解析获取注解里的内容动态代理代理：某些场景下对象会找一个代理对象，来辅助自己完成一些工作案例：通过代理实现方法性能统计优点XML定义：可扩展标记语言，它是一种数据表示格式，可以描述非常复杂的数据结构，常用于传输和存储数据XML解析使用Dom4j解析XMLXpath检索出XML文件
HashMap中k的值没有顺序，常用来做统计。2.LinkedHashMap吧。它内部有一个链表，保持Key插入的顺序。迭代的时候，也是按照插入顺序迭代，而且迭代比HashMap快。TreeMap的顺序是Key的自然顺序（如整数从小到大），也可以指定比较函数。但不是插入的顺序。4.Hashtable与 HashMap类似,它继承自Dictionary类、不同的是:它不允许记录的键或者值为空;它支持线程的同步、即任一时刻只有一个线程能写Hashtable,因此也导致了 Hashtable在写入时会比较慢。 HashMap HashMap 的底层数据结构是 “数组 + 链表”LinkedHashMapLinkedHashMap 拥有 HashMap 的所有特性，它比 HashMap 多维护了一个双向链表，因此可以按照插入的顺序从头部或者从尾部迭代，是有序的，不过因为比 HashMap 多维护了一个双向链表，它的内存相比而言要比 HashMap 大，并且性能会差一些，但是如果需要考虑到元素插入的顺序的话， LinkedHashMap 不失为一种好的选择 TreeMap与 HashMap 不同， TreeMap 的底层就是一颗红黑树，它的 containsKey , get , put and remove 方法的时间复杂度是 log(n) ，并且它是按照 key 的自然顺序（或者指定排序）排列，与 LinkedHashMap 不同， LinkedHashMap 保证了元素是按照插入的顺序排列。 典型回答：Hashtable、HashMap、TreeMap都实现了Map接口，使用键值对的形式存储数据和操作数据。Hashtable是java早期提供的，方法是同步的（加了synchronized）。key和value都不能是null值。HashMap的方法不是同步的，支持key和value为null的情况。行为上基本和Hashtable一致。由于Hashtable是同步的，性能开销比较大，一般不推荐使用Hashtable。通常会选择使用HashMap。HashMap进行put和get操作，基本上可以达到常数时间的性能TreeMap是基于红黑树的一种提供顺序访问的Map，和HashMap不同，它的get或put操作的时间复杂度是O(log(n))。具体的顺序由指定的Comparator来决定，或者根据键key的具体顺序来决定。