1.Docker安装RabbitMQ1.1下载RabbitMQ镜像docker pull rabbitmq1.2创建并启动RabbitMQ容器docker run -id --hostname myrabbit --name rabbitmq -p 15672:15672 -p 5672:5672 rabbitmq1.3进入容器交互页面docker exec -it rabbitmq /bin/bash1.4下载插件rabbitmq-plugins enable rabbitmq_management1.5登录ip+端口号访问账号密码均为：guest1.6拓展# 创建账号和密码 rabbitmqctl add_user 用户名 密码 # 设置用户角色 rabbitmqctl set_user_tags admin administrator # 为用户添加资源权限 # set_permissions [-p <vhostpath>] <user> <conf> <write> <read> # 添加配置、写、读权限 rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"用户级别：administrator：可以登录控制台、查看所有信息、可以对 rabbitmq 进行管理。monitoring：监控者 登录控制台，查看所有信息。policymaker：策略制定者 登录控制台，指定策略。managment：普通管理员 登录控制台。注：有可能会出现下面这个问题访问Docker的rabbitMq容器的Web-UI界面无法查看消息的状态及出现Stats in management UI are disabled on this node解决办法：进入RabbitMQ容器docker exec -it rabbitmq /bin/bash进入容器后，cd到路径/etc/rabbitmq/conf.d/cd /etc/rabbitmq/conf.d/再修改management_agent.disable_metrics_collectorecho management_agent.disable_metrics_collector = false > management_agent.disable_metrics_collector.conf退出容器exit输入指令docker exec -it rabbitmq rabbitmq-plugins enable rabbitmq_management重启RabbitMQdocker restart rabbitmq2.简单使用2.1pom.xml文件添加依赖(本环境是jdk1.8的) com.rabbitmq amqp-client 5.8.0 commons-io commons-io 2.6 2.2新建Producer类为生产者package com.youzi.rabbitmq.one; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @Author youzi * @ClassName Producer * @Date 2022/6/30 15:41 * @Description //TODO 生产者 */ public class Producer { //设置队列名称 public static final String QUEUE_NAME = "hello"; public static void main(String[] args) throws IOException, TimeoutException { //1.创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //2.设置IP factory.setHost("ip地址"); //3.设置用户名密码 factory.setUsername("用户名"); factory.setPassword("密码"); //4.创建连接 Connection connection = factory.newConnection(); //5.获取信道 Channel channel = connection.createChannel(); /** * 参数1.队列名称 * 参数2.队列里面的消息是否持久化，默认是不持久化 * 参数3.该队列是否只供一个消费之进行消费，是否进行消息共享，true表示可以多个消费者消费 * 参数4.是否自动删除，最后一个消费者断开连接以后，该队列是否自动删除，true表示自动删除 * 参数5.其他参数 */ //生成一个队列 channel.queueDeclare(QUEUE_NAME,false,false,false,null); //消息 String message = "hello youzi"; /** * 参数1.交换机名称 * 参数2.路由的key值，队列名称 * 参数3.其他参数 * 参数4.消息体 */ channel.basicPublish("",QUEUE_NAME,null,message.getBytes()); System.out.println("消息发送成功"); channel.close(); connection.close(); } }2.3新建Consumer类为消费者package com.youzi.rabbitmq.one; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @Author youzi * @ClassName Consumer * @Date 2022/6/30 16:44 * @Description //TODO 消费者 */ public class Consumer { //设置队列名称 一定要和生产者的一样才能找到对应的关系 public static final String QUEUE_NAME = "hello"; public static void main(String[] args) throws IOException, TimeoutException { //1.创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //2.设置IP factory.setHost("IP地址"); //3.设置用户名密码 factory.setUsername("用户名"); factory.setPassword("密码"); //4.创建连接 Connection connection = factory.newConnection(); //5.获取信道 Channel channel = connection.createChannel(); //申明回调方法 未成功消费 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println(new String(message.getBody())); }; //申明回调方法 取消消费 CancelCallback cancelCallback = consumerTag -> { System.out.println("消费中断"); }; /** * 消费者接收消息 * 参数1.队列名称 * 参数2.消费成功之后是否要自动应答，true表示自动应答 * 参数3.消费者未成功消费的回调方法 * 参数4.消费者取消消费时的回调方法 */ channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback); } }2.4访问RabbitMQ的后台管理页面
首先 joda-time joda-time 2.10.1 其次import org.joda.time.Interval; import org.joda.time.Period; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * @Author youzi * @ClassName TestDateUtil * startDate 开始时间 * endDate 结束时间 * dateFormat 时间格式（形如 yyyy-MM-dd） * @return 相差的月份数和天数 months 、 days * @throws ParseException * @Date 2022/6/22 14:02 * @Description 计算两个时间差 */ public class TestDateUtil { public static void main(String[] args) throws ParseException { DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); Date dateStart = df.parse("2022-03-04"); Date dateEnd = df.parse("2022-06-03"); if (dateStart.getTime() > dateEnd.getTime()) { Date temp = dateStart; dateStart = dateEnd; dateEnd = temp; } Calendar c = Calendar.getInstance(); c.setTime(dateEnd); c.add(Calendar.DAY_OF_MONTH, 1); //这是明天 dateEnd = c.getTime(); Interval interval = new Interval(dateStart.getTime(), dateEnd.getTime()); Period p = interval.toPeriod(); Integer years = p.getYears(); Integer months = p.getMonths(); Integer weeks = p.getWeeks(); Integer days = p.getDays(); Integer resultMonths = years * 12 + months; Integer resultDays = weeks * 7 + days; Map<String, Integer> result = new HashMap<>(); result.put("months", resultMonths); result.put("days", resultDays); System.out.println(result); } }
重载（Overload）和重写（Override）的区别方法的重载和重写都是实现多态的方式，区别在于前者实现的是编译时的多态性，而后者实现的是运行时的多态性。重载：一个类中有多个同名的方法，但是具有有不同的参数列表（参数类型不同、参数个数不同或者二者都不同）。重写：发生在子类与父类之间，子类对父类的方法进行重写，参数都不能改变，返回值类型可以不相同，但是必须是父类返回值的派生类。即外壳不变，核心重写！重写的好处在于子类可以根据需要，定义特定于自己的行为。注：重载的概念是：方法名称相同，参数个数、次序、类型不同因此重载对返回值没有要求，可以相同，也可以不同，但是如果参数的个数、类型、次序都相同，方法名也相同，仅返回值不同，则无法构成重载如：//这2个方法不能构成重载，会有编译错误。 public int A(int i); public double A(int i); //这2个方法可以形成重载 public int A(int i); public double A(double i);不能根据返回类型来区分重载的原因：如果我们有两个方法如下，当我们调用：test(1) 时，编译器无法确认要调用的是哪个。方法的返回值只是作为方法运行之后的一个“状态”，但是并不是所有调用都关注返回值，所以不能将返回值作为重载的唯一区分条件。// 方法1 int test(int a); // 方法2 long test(int a);抽象类（abstract class）和接口（interface）的区别抽象类只能单继承，接口可以多实现。抽象类可以有构造方法，接口中不能有构造方法。抽象类中可以有成员变量，接口中没有成员变量，只能有常量（默认就是 public static final）。抽象类中可以包含非抽象的方法，在 Java 7 之前接口中的所有方法都是抽象的，在 Java 8 之后，接口支持非抽象方法：default 方法、静态方法等。Java 9 支持私有方法、私有静态方法。抽象类中的方法类型可以是任意修饰符，Java 8 之前接口中的方法只能是 public 类型，Java 9 支持 private 类型。设计思想的区别：接口是自上而下的抽象过程，接口规范了某些行为，是对某一行为的抽象。我需要这个行为，我就去实现某个接口，但是具体这个行为怎么实现，完全由自己决定。抽象类是自下而上的抽象过程，抽象类提供了通用实现，是对某一类事物的抽象。我们在写实现类的时候，发现某些实现类具有几乎相同的实现，因此我们将这些相同的实现抽取出来成为抽象类，然后如果有一些差异点，则可以提供抽象方法来支持自定义实现。打个比方：普通类像亲爹 ，他有啥都是你的。抽象类像叔伯，有一部分会给你，还能指导你做事的方法。接口像干爹，可以给你指引方法，但是做成啥样得你自己努力实现。wait() 和 sleep() 方法的区别来源不同：sleep() 来自 Thread 类，wait() 来自 Object 类。对于同步锁的影响不同：sleep() 不会该表同步锁的行为，如果当前线程持有同步锁，那么 sleep 是不会让线程释放同步锁的。wait() 会释放同步锁，让其他线程进入 synchronized 代码块执行。使用范围不同：sleep() 可以在任何地方使用。wait() 只能在同步控制方法或者同步控制块里面使用，否则会抛 IllegalMonitorStateException。恢复方式不同：两者会暂停当前线程，但是在恢复上不太一样。sleep() 在时间到了之后会重新恢复；wait() 则需要其他线程调用同一对象的 notify()、nofityAll() 才能重新恢复。为什么要使用线程池？为什么不直接new个线程？如果我们在方法中直接new一个线程来处理，当这个方法被调用频繁时就会创建很多线程，不仅会消耗系统资源，还会降低系统的稳定性，一不小心把系统搞崩了，就可以直接去财务那结帐了。如果我们合理的使用线程池，则可以避免把系统搞崩的窘境。总得来说，使用线程池可以带来以下几个好处：降低资源消耗。通过重复利用已创建的线程，降低线程创建和销毁造成的消耗。提高响应速度。当任务到达时，任务可以不需要等到线程创建就能立即执行。增加线程的可管理型。线程是稀缺资源，使用线程池可以进行统一分配，调优和监控。List、Set、MapHashMap 和Hashtable 的区别HashMap 允许 key 和 value 为 null，Hashtable 不允许。HashMap 的默认初始容量为 16，Hashtable 为 11。HashMap 的扩容为原来的 2 倍，Hashtable 的扩容为原来的 2 倍加 1。HashMap 是非线程安全的，Hashtable是线程安全的。HashMap 的 hash 值重新计算过，Hashtable 直接使用 hashCode。HashMap 去掉了 Hashtable 中的 contains 方法。HashMap 继承自 AbstractMap 类，Hashtable 继承自 Dictionary 类。类加载的过程类加载的过程包括：加载、验证、准备、解析、初始化，其中验证、准备、解析统称为连接。加载：通过一个类的全限定名来获取定义此类的二进制字节流，在内存中生成一个代表这个类的java.lang.Class对象。验证：确保Class文件的字节流中包含的信息符合当前虚拟机的要求，并且不会危害虚拟机自身的安全。准备：为静态变量分配内存并设置静态变量初始值，这里所说的初始值“通常情况”下是数据类型的零值。解析：将常量池内的符号引用替换为直接引用。初始化：到了初始化阶段，才真正开始执行类中定义的 Java 初始化程序代码。主要是静态变量赋值动作和静态语句块（static{}）中的语句。饿汉模式、懒汉模式和静态内部类（延迟初始化占位类）饿汉://线程安全、非懒加载、效率高。 public class Singleton { // 1.饿汉 private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }饿汉模式，比较常见的一种写法。在类加载的时候就对实例进行初始化，没有线程安全问题；获取实例的静态方法没有使用同步，调用效率高；但是没有使用懒加载，如果该实例从始至终都没被使用过，则会造成内存浪费。懒汉://线程安全、懒加载、效率低。 public class Singleton { // 2.懒汉 private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }线程安全的懒汉模式，比较常见的一种写法。在第一次使用的时候才进行初始化，达到了懒加载的效果；由于获取实例的静态方法用synchronized修饰，所以也没有线程安全的问题；但是，这种写法每次获取实例都要进行同步（加锁），因此效率较低，并且可能很多同步都是没必要的。注：该模式还有另一种常见写法，就是把getInstance方法上的synchronized去掉，这种方法有线程安全问题，不能使用。静态内部类(延迟初始化占位类)://线程安全、懒加载、效率高。 public class Singleton { // 4.静态内部类 private static class Holder { private static final Singleton INSTANCE = new Singleton(); } private Singleton() {} public static final Singleton getInstance() { return Holder.INSTANCE; } }静态内部类（延迟初始化占位类），比较常见的一种写法。JVM将推迟SingletonHolder的初始化操作，直到开始使用这个类时才初始化，并且由于通过一个静态初始化来初始化Singleton，因此不需要额外的同步。当任何一个线程第一次调用getInstance时，都会使SingletonHolder被加载和被初始化，此时静态初始化器将执行Singleton的初始化操作。排序算法冒泡排序:public static void bubbleSort(int[] array) { if (array == null || array.length == 0) { return; } for (int i = 0; i < array.length - 1; i++) { for (int j = 0; j < array.length - i - 1; j++) { if (array[j] > array[j + 1]) { int temp = array[j]; array[j] = array[j + 1]; array[j + 1] = temp; } } } }时间复杂度:在通常情况下。冒泡排序的比较次数 = (n - 1) + (n - 2) + ... + 2 + 1，即：n * (n - 1) / 2，所以冒泡排序的时间复杂度为：O(n^2)。插入排序public class InsertSort { public static void insertSort(int array[]) { if (array == null || array.length == 0) { return; } for (int i = 1; i < array.length; i++) { // 当array[i]比arra[i - 1]小时才进入处理 if (array[i] < array[i - 1]) { // 临时变量存储array[i]的值 int temp = array[i]; // 从当前位置开始处理 int j = i; // 将比temp大的数往后挪一个位置，为temp腾出一个合适位置 while (j > 0 && temp < array[j - 1]) { array[j] = array[j - 1]; j--; // 填充完后, 继续向前比较 } // 将temp放在属于它的位置上 array[j] = temp; } } } }时间复杂度:在最坏的情况下，即整个数组是倒序的，比较次数 = 1 + 2 + 3 + ... + (n - 2) + (n - 1) = n * (n - 1) / 2，此时的时间复杂度为：O(n^2)。在最好的情况下，即整个数组是正序的，比较次数 = n - 1，此时的时间复杂度为：O(n)。选择排序public class SelectSort { public static void selectSort(int[] array) { if (array == null || array.length == 0) { return; } for (int i = 0; i < array.length - 1; i++) { // 记录当次遍历值最小的数的位置 int minIndex = i; for (int j = i + 1; j < array.length; j++) { // 如果array[j]比array[minIndex]小, 则将minIndex修改为j if (array[j] < array[minIndex]) { minIndex = j; } } if (i != minIndex) { // 将当次遍历的最小值交换到对应位置 int temp = array[i]; array[i] = array[minIndex]; array[minIndex] = temp; } } } }时间复杂度:如果将数组的长度看作n，第一次循环时，j 从1开始直到n - 1，因此比较次数为n - 1；第二次为n - 2，依此类推。最终得到选择排序的比较次数 = (n - 1) + (n - 2) + ... + 2 + 1 = n * (n - 1) / 2，因此时间复杂度为：O(n^2)。快速排序public class QuickSort { private static void quickSort(int[] array, int low, int high) { if (low >= high) { return; } int i = low, j = high, index = array[i]; // 取最左边的数作为基准数 while (i < j) { while (i < j && array[j] >= index) { // 向左寻找第一个小于index的数 j--; } if (i < j) { array[i++] = array[j]; // 将array[j]填入array[i]，并将i向右移动 } while (i < j && array[i] < index) {// 向右寻找第一个大于index的数 i++; } if (i < j) { array[j--] = array[i]; // 将array[i]填入array[j]，并将j向左移动 } } array[i] = index; // 将基准数填入最后的坑 quickSort(array, low, i - 1); // 递归调用，分治 quickSort(array, i + 1, high); // 递归调用，分治 } public static void quickSort(int[] array) { if (array == null || array.length == 0) { return; } quickSort(array, 0, array.length - 1); } }时间复杂度:最好情况的时间复杂度为O(nlogn)。最差情况下每一次取到的数（基准数）都是当前要比较的数中的最大/最小值，在这种情况下，每次都只能得到比上一次少1个数的子序列（即要么全比基准数大，要么全比基准小），此时相当于一个冒泡排序，比较的次数 = (n - 1) + (n - 2) + ... + 2 + 1 = (n - 1) * n / 2，此时的时间复杂度为：O(n^2)。最差情况一般出现在：待排序的数据本身已经是正序或反序排好了。归并排序public class MergeSort { public static void mergeSort(int[] array) { if (array == null || array.length == 0) return; int[] temp = new int[array.length]; mergeSort(array, 0, array.length - 1, temp); } // 归并 private static void mergeSort(int array[], int first, int last, int temp[]) { if (first < last) { int mid = (first + last) / 2; mergeSort(array, first, mid, temp); // 递归归并左边元素 mergeSort(array, mid + 1, last, temp); // 递归归并右边元素 mergeArray(array, first, mid, last, temp); // 再将二个有序数列合并 } } /** * 合并两个有序数列 * array[first]~array[mid]为第一组 * array[mid+1]~array[last]为第二组 * temp[]为存放两组比较结果的临时数组 */ private static void mergeArray(int array[], int first, int mid, int last, int temp[]) { int i = first, j = mid + 1; // i为第一组的起点, j为第二组的起点 int m = mid, n = last; // m为第一组的终点, n为第二组的终点 int k = 0; // k用于指向temp数组当前放到哪个位置 while (i <= m && j <= n) { // 将两个有序序列循环比较, 填入数组temp if (array[i] <= array[j]) temp[k++] = array[i++]; else temp[k++] = array[j++]; } while (i <= m) { // 如果比较完毕, 第一组还有数剩下, 则全部填入temp temp[k++] = array[i++]; } while (j <= n) {// 如果比较完毕, 第二组还有数剩下, 则全部填入temp temp[k++] = array[j++]; } for (i = 0; i < k; i++) {// 将排好序的数填回到array数组的对应位置 array[first + i] = temp[i]; } } }时间复杂度:归并排序的时间复杂度为O(nlogn)。
乐观锁和悲观锁的理解及如何实现，有哪些实现方式？
悲观锁:总是假设最坏的情况，每次去拿数据的时候都认为别人会修改，所以每次在拿数据的时候都会上锁，这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制，比如行锁，表锁等，读锁，写锁等，都是在做操作之前先上锁。 再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。 乐观锁：顾名思义，就是很乐观，每次去拿数据的时候都认为别人不会修改，所以不会上锁，但是在更新的时候会判断一下在此期间别人有没有去更新这个数据，可以使用版本号等机制。乐观锁适用于多读的应用类型，这样可以提高吞吐量，像数据库提供的类似于write_condition 机制，其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。 悲观锁的实现方式：Java 中悲观锁的实现包括 synchronized 关键字和 Lock 相关类等，我们以 Lock 接口为例，例如 Lock 的实现类 ReentrantLock，类中的 lock() 等方法就是执行加锁，而 unlock() 方法是执行解锁。处理资源之前必须要先加锁并拿到锁，等到处理完了之后再解开锁，这就是非常典型的悲观锁思想。乐观锁的实现方式：使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识，不一致时可以采取丢弃和再次尝试的策略。java 中的 Compare and Swap 即 CAS ，当多个线程尝试使用 CAS 同时更新同一 个变量时，只有其中一个线程能更新变量的值，而其它线程都失败，失败的线程并不会被挂起，而是被告知这次竞争中失败，并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置（V）、进行比较的预期原值（A）和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配，那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。
Spring AOP 中的动态代理主要有两种方式，JDK 动态代理和 CGLIB 动态代理：JDK动态代理只提供接口的代理，不支持类的代理。核心 InvocationHandler接口和Proxy类，InvocationHandler 通过 invoke()方法反射来调用目标类中的代码，动态地将横切逻辑和业务编织在一起；接着，Proxy 利用 InvocationHandler 动态创建 一个符合某一接口的的实例, 生成目标类的代理对象。如果代理类没有实现 InvocationHandler 接口，那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。CGLIB（Code Generation Library），是一个代码生成的类库，可以在运行时动态的生成指定类的一个子类对象，并覆盖其中特定方法并添加增强代码，从而实现 AOP。CGLIB 是通过继承的方式做的动态代理，因此如果某个类被标记为 final，那么它是无法使用 CGLIB 做动态代理的。静态代理与动态代理区别在于生成 AOP 代理对象的时机不同，相对来说 AspectJ 的静态代理方式具有更好的性能，但是 AspectJ 需要特定的编译器进行处理，而 Spring AOP 则无 需特定的编译器处理。注：InvocationHandler 的 invoke(Object proxy,Method method,Object[] args)：proxy 是最终生成的代理实例; method 是被代理目标实例的某个具体方法; args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。
AOP实现的关键在于代理模式，AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ AOP；动态代理则以Spring AOP为代表。AspectJ AOP使用的静态代理的增强，所谓静态代理，就是AOP框架会在编译阶段生成AOP代理类，因此也称为编译时增强，他会在编译阶段将AspectJ(切面)织入到Java字节码中， 运行的时候就是增强之后的AOP对象。Spring AOP 使用的是动态代理，所谓的动态代理就是说AOP框架不会去修改字节码，而是每次运行时在内存中临时为方法生成一个AOP对象，这个AOP对象包含了目标对象的全部方法，并且在特定的切点做了增强处理，并回调原对象的方法。
1、事务隔离级别spring 有五大隔离级别，默认值为 ISOLATION_DEFAULT（使用数据库的设置），其他 四个隔离级别和数据库的隔离级别一致：读未提交（Read uncommitted）：在这种隔离级别下，所有事务能够读取其他事务未提交的数据。读取其他事务未提交的数据，会造成脏读。因此在该种隔离级别下，不能解决脏读、不可重复读和幻读。读已提交（Read committed）:在这种隔离级别下，所有事务只能读取其他事务已经提交的内容。能够彻底解决脏读的现象。但在这种隔离级别下，会出现一个事务的前后多次的查询中却返回了不同内容的数据的现象，也就是出现了不可重复读。可重复读（Repeatable read）:在这种隔离级别下，所有事务前后多次的读取到的数据内容是不变的。也就是某个事务在执行的过程中，不允许其他事务进行update操作，但允许其他事务进行add操作，造成某个事务前后多次读取到的数据总量不一致的现象，从而产生幻读。可串行化（Serializable）:在这种隔离级别下，所有的事务顺序执行，所以他们之间不存在冲突，从而能有效地解决脏读、不可重复读和幻读的现象。但是安全和效率不能兼得，这样事务隔离级别，会导致大量的操作超时和锁竞争，从而大大降低数据库的性能，一般不使用这样事务隔离级别。隔离级别脏读不可重复读幻读读未提交（Read uncommitted）×(未解决)××读已提交（Read committed）√(解决)××可重复读（Repeatable read）√√×可串行化（Serializable）√√√2、出现的问题2.1、脏读A事务读取B事务尚未提交的数据，此时如果B事务发生错误并执行回滚操作，那么A事务读取到的数据就是脏数据。就好像原本的数据比较干净、纯粹，此时由于B事务更改了它，这个数据变得不再纯粹。这个时候A事务立即读取了这个脏数据，但事务B良心发现，又用回滚把数据恢复成原来干净、纯粹的样子，而事务A却什么都不知道，最终结果就是事务A读取了此次的脏数据，称为脏读。2.2、幻读事务A在执行读取操作，需要两次统计数据的总量，前一次查询数据总量后，此时事务B执行了新增数据的操作并提交后，这个时候事务A读取的数据总量和之前统计的不一样，就像产生了幻觉一样，平白无故的多了几条数据，成为幻读。2.3、不可重复读事务A在执行读取操作，由整个事务A比较大，前后读取同一条数据需要经历很长的时间 。而在事务A第一次读取数据，比如此时读取了小明的年龄为20岁，事务B执行更改操作，将小明的年龄更改为30岁，此时事务A第二次读取到小明的年龄时，发现其年龄是30岁，和之前的数据不一样了，也就是数据不重复了，系统不可以读取到重复的数据，成为不可重复读。注：幻读和不可重复读的区别在于，幻读是总记录数的变化，不可重复读是一条记录的数据前后不一致。3、Spring的七种事务传播行为3.1、事务传播行为的定义事务传播行为（propagation behavior）指的就是当一个事务方法被另一个事务方法调用时，这个事务方法应该如何运行。例：A方法调用B方法时，B是继续在调用者A的事务中运行呢，还是为自己开启一个新事务运行，这就是由B的事务传播行为决定的。3.2、七种事务传播行为PROPAGATION_REQUIRED：如果当前没有事务，就创建一个新事务，如果当前存在事务，就加入该事务，这是最常见的选择，也是Spring默认的事务传播行为。PROPAGATION_SUPPORTS：支持当前事务，如果当前存在事务，就加入该事务，如果当前不存在事务，就以非事务执行。PROPAGATION_MANDATORY：支持当前事务，如果当前存在事务，就加入该事务，如果当前不存在事务，就抛出异常。PROPAGATION_REQUIRES_NEW：创建新事务，无论当前存不存在事务，都创建新事务。PROPAGATION_NOT_SUPPORTED：以非事务方式执行操作，如果当前存在事务，就把当前事务挂起。PROPAGATION_NEVER：以非事务方式执行，如果当前存在事务，则抛出异常。PROPAGATION_NESTED：如果当前存在事务，则在嵌套事务内执行。如果当前没有事务，则按REQUIRED属性执行。
1、单例模式单例模式是一种常用的软件设计模式，在应用这个模式时，单例对象的类必须保证只有一个实例存在，整个系统只能使用一个对象实例。优点：不会频繁地创建和销毁对象，浪费系统资源。可能这会需要你手写一个单例模式，这就得自己去学了，因为单例模式有很多种写法，懒汉模式，饿汉模式，双重检查模式等。懒汉模式就是用的时候再去创建对象，饿汉模式就是提前就已经加载好的静态static对象，双重检查模式就是两次检查避免多线程造成创建了多个对象。单例模式有很多种的写法，我总结一下：（1）饿汉式单例模式的写法：线程安全（2）懒汉式单例模式的写法：非线程安全（3）双检锁单例模式的写法：线程安全2、代理模式代理模式是给某一个对象提供一个代理，并由代理对象控制对原对象的引用。优点：代理模式能够协调调用者和被调用者，在一定程度上降低了系统的耦合度；可以灵活地隐藏被代理对象的部分功能和服务，也增加额外的功能和服务。缺点：由于使用了代理模式，因此程序的性能没有直接调用性能高；使用代理模式提高了代码的复杂度。例子：黄牛卖火车票：没有流行网络购票的年代是很喜欢找黄牛买火车票的，因为工作忙的原因，没时间去买票，然后就托黄牛给你买张回家过年的火车票。这个过程中黄牛就是代理人，火车票就是被代理的对象。婚姻介绍所：婚姻介绍所的工作人员，搜集单身人士信息，婚介所的工作人员为这个单身人士找对象，这个过程也是代理模式的生活案例。对象就是被代理的对象。注意了，问代理模式的时候，很有可能会问：动态代理。在Spring篇中已经讲述过，如果你把动态代理讲了后，很有可能还会问什么是静态代理？一个动一个是静，大致也能才出来，就是中间代理层使我们手动写的，通常说的代理模式就是静态代理。3、工厂模式简单工厂模式又叫静态工厂方法模式，就是建立一个工厂类，对实现了同一接口的一些类进行实例的创建。比如，一台咖啡机就可以理解为一个工厂模式，你只需要按下想喝的咖啡品类的按钮（摩卡或拿铁），它就会给你生产一杯相应的咖啡，你不需要管它内部的具体实现，只要告诉它你的需求即可。优点：工厂类含有必要的判断逻辑，可以决定在什么时候创建哪一个产品类的实例，客户端可以免除直接创建产品对象的责任，而仅仅“消费”产品；简单工厂模式通过这种做法实现了对责任的分割，它提供了专门的工厂类用于创建对象；客户端无须知道所创建的具体产品类的类名，只需要知道具体产品类所对应的参数即可，对于一些复杂的类名，通过简单工厂模式可以减少使用者的记忆量；通过引入配置文件，可以在不修改任何客户端代码的情况下更换和增加新的具体产品类，在一定程度上提高了系统的灵活性。缺点：不易拓展，一旦添加新的产品类型，就不得不修改工厂的创建逻辑；产品类型较多时，工厂的创建逻辑可能过于复杂，一旦出错可能造成所有产品的创建失败，不利于系统的维护。4、抽象工厂模式抽象工厂模式是在简单工厂的基础上将未来可能需要修改的代码抽象出来，通过继承的方式让子类去做决定。例如，以上面的咖啡工厂为例，某天我的口味突然变了，不想喝咖啡了想喝啤酒，这个时候如果直接修改简单工厂里面的代码，这种做法不但不够优雅，也不符合软件设计的“开闭原则”，因为每次新增品类都要修改原来的代码。这个时候就可以使用抽象工厂类了，抽象工厂里只声明方法，具体的实现交给子类（子工厂）去实现，这个时候再有新增品类的需求，只需要新创建代码即可。5、装饰器模式装饰器模式是指动态地给一个对象增加一些额外的功能，同时又不改变其结构。优点：装饰类和被装饰类可以独立发展，不会相互耦合，装饰模式是继承的一个替代模式，装饰模式可以动态扩展一个实现类的功能。装饰器模式的关键：装饰器中使用了被装饰的对象。例如，创建一个对象“laowang”，给对象添加不同的装饰，穿上夹克、戴上帽子......，这个执行过程就是装饰者模式。一句名言：人靠衣裳马靠鞍。都是装饰器模式的生活案列。6、代理模式和装饰器模式有什么区别？都是结构型模式，代理模式重在访问权限的控制，而装饰器模式重在功能加强。7、模板方法模式模板方法模式是指定义一个算法骨架，将具体内容延迟到子类去实现。优点：提高代码复用性：将相同部分的代码放在抽象的父类中，而将不同的代码放入不同的子类中；实现了反向控制：通过一个父类调用其子类的操作，通过对子类的具体实现扩展不同的行为，实现了反向控制并且符合开闭原则。例子：喝茶：烧水----放入茶叶---喝茶。放入的茶叶每个人自己的喜好不一样，有的是普洱、有的是铁观音等。每日工作：上班打卡----工作---下班打卡。每个人工作的内容不一样，后端开发的、前端开发、测试、产品每个人的工作内容不一样。8、享元模式顾名思义就是被共享的单元。享元模式的意图是复用对象，节省内存，前提是享元对象是不可变对象。具体来讲，当一个系统中存在大量重复对象的时候，如果这些重复的对象是不可变对象，我们就可以利用享元模式将对象设计成享元，在内存中只保留一份实例，供多处代码引用。这样可以减少内存中对象的数量，起到节省内存的目的。典型的使用场景：Integer中cache，就是享元模式很经典的实现。9、享元模式和单例模式的区别？单例模式是创建型模式，重在只能有一个对象。而享元模式是结构型模式，重在节约内存使用，提升程序性能。享元模式：把一个或者多可对象缓存起来，用的时候，直接从缓存里获取。也就是说享元模式不一定只有一个对象。10、策略模式策略模式是指定义一系列算法，将每个算法都封装起来，并且使他们之间可以相互替换。优点：遵循了开闭原则，扩展性良好。缺点：随着策略的增加，对外暴露越来越多。例子：条条大路通罗马，条条大路通北京。我们去北京的交通方式（策略）很多，比如说坐飞机、坐高铁、自己开车等方式。每一种方式就可以理解为每一种策略。11、责任链模式是行为型设计模式之一，其将链中每一个节点看作是一个对象，每个节点处理的请求均不同，且内部自动维护一个下一节点对象。当一个请求从链式的首端发出时，会沿着链的路径依次传递给每一个节点对象，直至有对象处理这个请求为止。优点：解耦了请求与处理；请求处理者（节点对象）只需关注自己感兴趣的请求进行处理即可，对于不感兴趣的请求，直接转发给下一级节点对象；具备链式传递处理请求功能，请求发送者无需知晓链路结构，只需等待请求处理结果；链路结构灵活，可以通过改变链路结构动态地新增或删减责任；易于扩展新的请求处理类（节点），符合开闭原则；缺点：责任链路过长时，可能对请求传递处理效率有影响；如果节点对象存在循环引用时，会造成死循环，导致系统崩溃；生活案列：我们在公司内部发起一个OA审批流程，项目经理审批、部门经理审批、老板审批、人力审批。这就是生活中的责任链模式，每个角色的责任是不同。SpringMVC中的拦截器和Mybatis中的插件机制，都是拦截器经典实现。12、适配器模式适配器模式是将一个类的接口变成客户端所期望的另一种接口，从而使原本因接口不匹配而无法一起工作的两个类能够在一起工作。优点：可以让两个没有关联的类一起运行，起着中间转换的作用；灵活性好，不会破坏原有的系统。缺点：过多地使用适配器，容易使代码结构混乱，如明明看到调用的是 A 接口，内部调用的却是 B接口的实现。生活中的插座，为了适应各种插头，然后上面有两个孔的，三个空的，基本都能适应。还有万能充电器、USB接口等。这些都是生活中的适配器模式。13、观察者模式观察者模式是定义对象间的一种一对多依赖关系，使得每当一个对象状态发生改变时，其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅（Publish/Subscribe）模式、模型-视图（Model/View）模式、源-监听器（Source/Listener）模式或从属者（Dependents）模式。 优点：观察者模式可以实现表示层和数据逻辑层的分离，并定义了稳定的消息更新传递机制，抽象了更新接口，使得可以有各种各样不同的表示层作为具体观察者角色；观察者模式在观察目标和观察者之间建立一个抽象的耦合；观察者模式支持广播通信；观察者模式符合开闭原则（对拓展开放，对修改关闭）的要求。缺点：如果一个观察目标对象有很多直接和间接的观察者的话，将所有的观察者都通知到会花费很多时间；如果在观察者和观察目标之间有循环依赖的话，观察目标会触发它们之间进行循环调用，可能导致系统崩溃；观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的，而仅仅只是知道观察目标发生了变化。在观察者模式中有如下角色：Subject：抽象主题（抽象被观察者），抽象主题角色把所有观察者对象保存在一个集合里，每个主题都可以有任意数量的观察者，抽象主题提供一个接口，可以增加和删除观察者对象；ConcreteSubject：具体主题（具体被观察者），该角色将有关状态存入具体观察者对象，在具体主题的内部状态发生改变时，给所有注册过的观察者发送通知；Observer：抽象观察者，是观察者的抽象类，它定义了一个更新接口，使得在得到主题更改通知时更新自己；ConcrereObserver：具体观察者，实现抽象观察者定义的更新接口，以便在得到主题更改通知时更新自身的状态。在Spring中大量的使用的观察者模式，只要看到是以Event结尾或者Publish开头的基本上都是观察者模式。