文章目录
- 一、Java基础
- 1、JDK和JRE以及JVM的区别
- 2、String、StringBuffer、StringBuilder
- 3、==和equals方法的区别
- 4、hashCode与equals之间的关系
- 5、ArrayList和LinkedList区别
- 6、CopyOnWriteArrayList的底层原理
- 7、HashMap的扩容机制原理
- 8、避免Java死锁
- 9、Java异常体系
- 10、Java中的类加载器
- 11、ReentrantLock的tryLock()和lock()方法的区别
- 12、ReentrantLock的公平锁和非公平锁的底层实现
- 13、HashMap底层
- 14、this关键字
- 15、创建对象的四步
- 16、static关键字
- 17、静态初始化块
- 18、变量
- 19、抽象类和抽象方法
- 20、接口
- 21、String
- 22、内部类
- 23、采用字节码的好处
- 24、RESTful风格
- 25、普通for循环、增强for循环、forEach
- 二、IO流
- 三、多线程
- 二、Spring
- 三、MySQL
- 四、Redis
- 五、SpringBoot
- 六、MyBatis
- 七、JVM
- 八、业务
- 总结
一、Java基础
1、JDK和JRE以及JVM的区别
- JDK(Java SE Development Kit)是一个Java标准开发包;它提供了Java程序编译、运行时所需要的各种工具和资源,包括Java编译器、Java运行时环境以及常用的Java类库等。
- JRE(Java Runtime Environment),Java运行时环境,用于运行Java的字节码文件。JRE包括了JVM以及JVM工作时所需要的类库。
- JVM(Java Virtual Mechinal),Java虚拟机,是JRE的一部分,它是整个Java实现跨平台的最核心部分,负责运行字节码文件。
即JDK包含JRE,JRE包含JVM;由JDK中的Java编译器javac将java文件编译成.class字节码文件,再由JDK的JRE的JVM来运行字节码文件。
2、String、StringBuffer、StringBuilder
- String 是一个常量,一旦赋值则不可改变;如果尝试去修改,会新生成一个字符串对象。
- StringBuffer是可变的,并且是线程安全的,但是因为线程安全,所以效率会相对低一些。
- StringBuilder也是可变,并且是非线程安全的,所以它的效率会相对高一些。
3、==和equals方法的区别
- ==:如果是基本数据类型(byte、short、int、long、boolean、char、float、double),此时比较的值。
如果是非基本数据类型,也就是引用类型,比较的是引用地址。 - equals:具体看各个类重写equals方法后的比较逻辑,比如String类,虽然是引用类型,但是String类中重写了equals方法,方法内部首先比较的引用地址是否相同,相同则返回true;否则再比较字符串中的各个字符是否全部相等。
4、hashCode与equals之间的关系
- 在Java中,每个对象都可以调用自己的hashCode方法得到自己的哈希值(hashCode),相当于对象的指纹信息。
hashCode是使用hash算法求出的,用来确保对象的唯一性,但又不是那么绝对。
如果两个对象的hashCode不相同,那么这两个对象肯定不相同。
如果两个对象的hashCode相同,不代表这两个对象一定是同一个对象,也可能是两个对象。
如果两个对象相同,那么它们的hashCode就一定相同。 - 在Java的一些集合类的实现中,在比较两个对象是否相等时,会根据上面的原则,会先调用对象hashCode()方法得到HashCode进行比较,如果hashCode不相同,就可以直接认为这两个对象不相同;如果hashCode相同,那么就会进一步调用equals()方法进行比较。而equals()方法,就是用来最终确定两个对象是不是相等的,通常equals方法的实现会比较重,逻辑比较多,而hashCode()主要就是得到一个hashCode值,实际上就是一个数字,相对而言比较轻,所以在比较两个对象时,通常都会先根据hashCode比较一下。
如果重写了equals方法,那么就要注意hashCode()方法,一定要保证能遵循上述规则。即先使用对象的hashCode先判断,再进行equals方法的实现。
5、ArrayList和LinkedList区别
- 首先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的。
- 由于底层数据结构不同,它们所适用的场景也不同,ArrayList更适合随机查找,LinkedList更适合添加和删除。
- 另外ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来用。
6、CopyOnWriteArrayList的底层原理
- 首先,CopyOnWriteArrayList内部也是由数组实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新的数组上进行,读操作在原数组上进行。
- 写操作时会加锁,防止并发写入丢失数据的问题。
- 写操作结束后,会把原数组指向新数组。
- CopyOnWriteArrayList允许在写操作时读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景。
7、HashMap的扩容机制原理
JDK1.8
- 生成新数组
- 遍历老数组中的每个位置上的链表或红黑树。
- 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
- 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素在新数组中的下标位置
a. 统计每个下标位置的元素个数
b. 如果该位置下的元素个数超过8,则生成一个新的红黑树,并将根节点添加到新数组的对应位置
c. 如果该位置下的元素没有超过8,则生成一个新的链表,并将链表的头节点添加到新数组的对应位置 - 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性。
8、避免Java死锁
造成死锁的原因:
- 一个资源每次只能被一个线程使用
- 一个线程在阻塞等待某个资源时,不释放已占有资源
- 一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
- 若干线程形成头尾相接的循环等待资源关系
造成死锁必须要达到这4个条件,如果要避免死锁,只需要不满足其中一个条件即可。而其中前三个条件是作为锁要符合的条件,所以要避免死锁就需要打破第四个条件,即不出现循环等待锁的关系。
在开发中避免死锁:
- 要注意加锁顺序,保证每个线程按同样的顺序进行加锁。
- 要注意加锁时限,可以针对锁设置一个超时时间。
- 要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决。
9、Java异常体系
- Java的所有异常都来自顶级父类Throwable
- Throwable下的两个子类分别是Exception和Error
- Error表示非常严重的错误,如java.lang.StackOverFlowError和java.lang.OutOfMemoryError,也就是栈溢出和内存溢出。这些错误出现时,仅仅靠程序自己是解决不了的,通常不通过代码去捕获这些Error,因为程序可能已经运行不了了
- Exception表示异常,表示程序出现Exception异常时,是可以通过程序自己来解决的。例如空指针和类型转换异常等,是可以通过程序捕获然后做特殊处理的
- Exception的子类通常可以分为RuntimeException和非RuntimeException两类
- RuntimeException异常表示在运行时期抛出的异常,这些异常是非检查异常,程序中可以捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑的角度尽可能避免这类异常的发生,空指针异常和数组越界等运行时异常。
- 非RuntimeException表示非运行时异常,也就是检查时异常,是必须处理的异常,如果不处理,程序就不能通过检查异常。如IOException和SQLException以及用户自定义的异常等。
10、Java中的类加载器
JDK自带有3个类加载器:BootStrapClassLoader、ExtClassLoader、AppClassLoader
- BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%/lib下的jar包和class文件
- ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext下的jar包和class文件
- AppClassLoader是自定义类加载器的父类,负责加载指定classpath下的类文件
11、ReentrantLock的tryLock()和lock()方法的区别
- tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到则返回true,加不到则返回false。
- lock()表示阻塞加锁,线程会阻塞直到加到锁,没有返回值。
12、ReentrantLock的公平锁和非公平锁的底层实现
- 首先不管是公平锁还是非公平锁,它们的底层实现都会使用AQS(AbstractQueuedSynchronizer)来进行排队。
- 它们的区别在于线程使用lock()方法在加锁时,如果是公平锁,会先检查AQS队列中是否有线程正在排队,如果有线程排队,则当前线程也进行排队;如果是非公平锁,则不会去检查是否有线程正在排队,则是直接竞争锁,如果没有抢到锁,再到线程中排队。
- 当锁被释放时,都是唤醒排在最前面的线程;所以非公平锁只是体现在线程加锁阶段,而没有体现在线程被唤醒阶段。
- ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。
可重入锁,即当前线程持有该锁后,可以重复进行加锁。
13、HashMap底层
- HashMap底层实现采用了哈希表,哈希表的本质就是数组+链表。即哈希表结合了数组和链表的优点,也就是查询快,增删也快。即使用数组来存储,然后数组存储的元素是一个链表或者红黑树。
- 当节点个数小于6时,红黑树会转换成链表。当节点个数大于8时,链表会转换成红黑树。
- 当数组的大小超过64时,会将数组中节点个数超过8的链表进行红黑树的转换。
14、this关键字
- 普通方法中,this总是指调用该方法的对象。
- 构造方法中,this总是指向正要初始化的对象。
- this的其他要点
1)this()调用重载的构造方法,避免相同的初始化代码。但是只能在构造方法中使用,并且必须位于构造方法的第一句。
2)this不能用于static方法中。
3)this是作为普通方法的“隐式参数”,由系统传入到方法中。
15、创建对象的四步
- 分配对象内存空间,并将对象成员变量初始化为0或null。
- 执行属性值的显式初始化
- 执行构造方法
- 将对象的内存地址返回到相关变量。
16、static关键字
- 静态变量/静态方法生命周期和类相同,在整个程序执行期间都有效。
- 为该类的公用变量,属于类,被该类的所有实例共享,在类载入时被初始化。
- static成员变量只有一份。
- 一般用“类名.类变量/方法”来调用。
- 在static方法中不可直接访问非static的成员。
17、静态初始化块
- 构造方法用于对象的普通属性初始化。
- 静态初始化块,用于类的初始化操作。
- 在静态初始化块中不能直接访问非静态成员。
18、变量
- 变量:分为局部变量、成员变量、静态变量。
- 局部变量,在方法或语句块内部;从属于方法/语句块;生命周期从声明处开始到方法或语句块结束。
- 成员变量:在类内部,方法外部;从属于对象;生命周期从对象创建,成员变量也跟着创建,对象销毁,成员变量也跟着销毁。
- 静态变量:类内部,static修饰;从属于类;从类被加载开始静态变量就有效,直到类销毁。
19、抽象类和抽象方法
- 抽象方法:
1)使用abstract修饰的方法,没有方法体,只有声明。
2)定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体实现。 - 抽象类:
1)包含抽象方法的类就是抽象类。
2)通过抽象类,就可以做到严格限制子类的设计,使子类之间更加通用。 - 抽象类的使用要点:
1)有抽象方法的类只能定义成抽象类。
2)抽象类不能实例化,即不能用new来实例化抽象类。
3)抽象类可以包含属性、方法、构造方法。但是构造方法不能用来new实例,只能用来被子类调用。
4)抽象类只能用来被继承。
5)抽象方法必须被子类实现。
6)并且abstract不能与static、final、private一起使用。
20、接口
- 接口就是一组规范,所有实现类都要遵守。
接口就是比“抽象类”还要“抽象”的“抽象类”,可以更加规范的对子类进行约束。
全面地专业地实现了规范和具体实现的分离。
接口是两个模块之间通信的标准,通信的规范。
接口和实现类不是父子关系,是实现规则的关系。 - 定义接口
1)访问修饰符:只能是public或默认
2)接口名:和类名采用相同的命名机制
3)extends:接口支持多继承
4)常量:接口中的属性只能是常量,只能是public static final修饰的。
5)方法:接口中的方法一般只能是public abstract修饰的。 - JDK8后,接口中可以包含普通的静态方法、默认方法。即可以不是抽象方法。
1)默认方法,允许给接口添加一个非抽象的方法实现,只需要使用default关键字即可,这个特征又叫默认方法(也称为扩展方法)。例如,default void method(){};
默认方法和抽象方法的区别是抽象方法必须要被实现,默认方法不是。作为替代方法,接口可以提供默认方法的实现,这个接口所有的实现类都可以得到该默认方法。即实现类可以重写该默认方法,类似于继承。
2)静态方法,可以在接口中直接定义静态方法的实现。这个静态方法直接从属于接口(接口也是类,一种特殊的类),可以通过接口名调用。
如果子类中定义了相同名字的静态方法,那就是完全不同的方法了,直接从属于子类,可以通过子类名直接调用。
即接口和实现类的静态方法是无关的。 - 接口的多继承,和类的继承相似,子接口继承父接口,会获得父接口的一切。
21、String
- String类又称为不可变字符序列,因为使用final关键字修饰,一旦赋值就不可变。
- String位于java.lang包中,Java程序默认导入java.lang包下的所有类。
- Java字符串就是Unicode字符序列(Unicode2个字节表示一个编码)。
- String底层实现,在JDK1.8及以前,使用char[],在JDK1.9后,使用byte[],因为使用的字符串值是拉丁字符居多而之前使用的char数组每一个char占用两个字节而拉丁字符只需要一个字节就可以存储,剩下的一个字节就浪费了,造成内存的浪费,gc的更加频繁。因此在jdk9中将String底层的实现改为了byte数组。
- Java中没有内置的字符串类型,而在标准Java类库中提供了一个预定义的类String,每个用双引号括起来的字符串都是String类的一个实例。
- 在不使用构造方法赋值时,字符串会存储在运行时常量池中。如果使用构造方法创建String对象,则会在堆中重新开辟一个内存空间存储字符串。
22、内部类
- 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。
- 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。但外部类不能直接访问内部类的内部属性。
- 内部类又分为非静态内部类、静态内部类、匿名内部类、局部内部类。
- 非静态内部类(外部类里使用非静态内部类和平时使用其他类没有什么不同)
1)非静态内部类对象寄存在一个外部类对象里。因此,如果存在一个非静态内部类对象,那么一定存在一个对应的外部类对象。非静态内部类对象单独属于外部类的某个对象。
2)非静态内部类可以直接访问外部类的成员,但是外部类不能直接访问内部类的成员。
3)非静态内部类不能有静态方法、静态属性、静态初始化块。
4)非静态内部类对成员变量访问的区别。内部类属性:使用this.变量名;外部类属性:外部类名.this.变量名。 - 静态内部类
1)静态内部类可以访问外部类的静态成员,不能访问外部类的普通成员。
2)静态内部类可以看做外部类的一个静态成员。 - 匿名内部类,匿名内部类适合只需要使用一次的类。
- 局部内部类,定义在方法内部,作用域只限于方法内。实际开发中应用很少。
23、采用字节码的好处
- 编译器javac将
*.java
文件编译成*.class
字节码文件,可以做到一次编译到处运行,Windows上编译好的文件可以在Linux上直接运行,通过这种方式实现跨平台。 - Java的跨平台有一个前提条件,就是不同的操作系统上安装的JDK或JRE是不一样的,虽然字节码是通用的,但是需要将字节码解释成各个操作系统的机器码是需要不同的解释器的,所以针对各个操作系统是需要有各自的JDK和JRE的。
24、RESTful风格
REST(Representational State Transfer,简称REST,表现层状态转移)
- RESTful有以下几个特征
1)以资源为基础:资源可以是一个图片、音乐、一个XML格式、HTML格式或者JSON格式等网络上的一个实体,除了一些二进制的资源外,普通的文本资源更多以JSON为载体,面向用户的一组数据(通常从数据库中查询而来)。
2)统一接口:对资源的操作包括获取、创建、修改和删除,这些操作正好对应HTTP协议的GET、POST、PUT、DELETE。
使用RESTful风格的接口时,从接口上你可能只能定位其资源,但无法知晓它具体进行了什么操作,需要具体了解其发生了什么操作动作要从其HTTP请求方法类型上进行判断。
具体的HTTP方法和方法含义如下:
GET(SELECT):从服务器上获取资源(一项或多项)。
POST(CREATE):在服务器上新建一个资源。
PUT(UPDATE):在服务器上更新资源(客户端提供完整的资源数据)。
PATCH(UPDATE):在服务器上更新(客户端提供需要修改的资源数据)。
DELETE(DELETE):从服务器上删除资源。
3)URI指向资源:Universal Resource Identifier(统一资源标识符),用来标识抽象或物理资源的一个紧凑字符串。URI相当于是URL和URN的抽象父类。URL包含协议,URN不包含协议。在这里URI更多时候指的是URL(统一资源定位符)。RESTful是面向资源的,每种资源可以由一个或多个URI对应,但一个URI只能指向一种资源。
4)无状态:服务器不能保存客户端的信息,每次从客户端发送的请求信息中,要包含所有必须的状态信息,会话信息由客户端保存,服务器端根据这些状态信息来处理请求。当客户端可以切换到一个新状态的时候发送请求信息,当一个或多个请求被发送之后,客户端就处于一个状态变迁的过程中。每一个应用的状态描述可以被客户端用来初始化下一次的状态变迁。 - RESTful的6大原则:
1)客户端-服务端(Client-Server):这个更专注客户端和服务端的分离,服务端独立可以更好地服务于前端、安卓、IOS等客户端设备。
2)无状态(Stateless):服务端不保存客户端状态,由客户端保存状态信息,每次请求都要携带状态信息。
3)可缓存性(Cacheability):服务端需要回复是否可以缓存,让客户端甄别是否缓存,从而提高效率。
即服务端让客户端执行是否缓存。
4)统一接口(Uniform Interface):通过一定原则设计接口降低耦合,简化系统架构,这是RESTful设计的基本出发点。
5)分层系统(Layered System):客户端无法知道连接的是终端还是中间设备,分层允许你灵活的部署服务端项目。
6)按需代码(Code-On-Demand):按需代码允许我们灵活的发送一些特殊的代码给客户端。 - RESTful API设计规范
1)URL设计规范
URL为统一资源定位器,接口属于服务器端资源,首先要通过URL定位到资源才能去访问,通常一个URL组成由以下几个部分:
URL=scheme://host:port/path[?query][#fragment]
scheme:指底层用的协议,如http、https、ftp。
host:主机IP或域名。
port:端口号,http默认为80端口。
path:访问资源的路径,就是各种web框架中定义的route路由
query:查询字符串,发送给服务器的参数。
fragment:锚点,定位到页面的资源。
25、普通for循环、增强for循环、forEach
- 普通for循环遍历数组时需要索引。
- 增强for循环不能获取下标,所以遍历数组时最好使用普通for循环。增强for循环使用的Iterator迭代器。
- 普通for循环适用于遍历数组,而增强for循环适用于遍历链表。
- forEach只能用于集合,即有继承Iterable接口的类,如List。不能是数组,但是通过Arrays对象的将数组转换成集合的方法asList可以转换(数组的类型不能是基本数据类型,可以是Long,Integer,String等)。
二、IO流
1. IO流新语法
- JDK1.7及以后版本中可以使用try-with-resource语法更优雅的关闭资源。
- 在java.lang.AutoCloseable接口中包含了一个close方法,该方法用于关闭资源。只要实现了java.lang.AutoCloseable接口的对象,都可以使用try-with-resource关闭资源。即对try()括号中的对象的IO流进行自动关闭,不需要手动在finally中进行IO流的关闭。
2. IO流的分类
- 按流的方向分类:
1)输入流:数据流向是数据源到程序。(以InputStream、Reader结尾的流)
2)输出流:数据流向是程序到目的地。(以OutputStream、Writer结尾的流) - 按处理的数据单元分类
1)字节流:以字节为单位获取数据,命名以Stream结尾的流一般是字节流,如FileInputStream、FileOutputStream。
字节流的话,如果读取中文数据,有可能会出现中文乱码。什么数据都可以使用字节流读取。
2)字符流:以字符为单位获取数据,命名以Reader、Writer结尾的流一般是字符流,如FileReader、FileWriter。适用于读取中文数据。
3. IO流分类
三、多线程
1. synchronized关键字
- syncchronized有两种用法,分别是synchronized方法和synchronized块
1)synchronized方法,通过在方法声明中加入synchronized关键字来声明,这时同一个对象下synchronized方法在多线程中执行时,该方法时同步的。例,public synchronized void add(){},此时的锁资源为该方法的对象。
即一次只能有一个线程进入该方法,其他线程想要进入此方法,只能排队等待,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。
2)synchronized块,可以精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。例,synchronized(锁资源){},此时的锁资源是自己指定的。
synchronized方法的缺陷就是,若将一个大的方法声明为synchronized将会大大影响效率。 - synchronized的偏向锁、轻量级锁、重量级锁
1)偏向锁:在锁对象的对象头中记录一下当前获取该锁的线程ID,该线程下次再来获取该锁时就可以直接获取到了。
2)轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程。
自旋:就是线程循环检查该锁是否被释放,直到获取到该锁为止。
阻塞:就是该锁被占用后,直接将线程进行阻塞,直到被唤醒。可以调用线程的wait()方法让其进行阻塞状态。
3)重量级锁:如果自旋次数过多仍然没有得到锁,则会升级为重量锁,重量锁会导致线程阻塞。
4)自旋锁,自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤是需要操作系统去进行的,比较消耗时间,自旋锁是通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程中线程一直在运行,相对而言没有使用太多的操作系统资源,比较轻量。
2. ReentrantLock对象
- ReentrantLock是一个可重入锁对象。
- ReentrantLock对象可以实现公平锁和非公平锁
1)公平锁:采用“先到先得”的策略,每次获取锁之前都会检查队列是否有排队等待的线程,如果有就将当前线程追加到队列中;如果没有就直接获取锁。
2)非公平锁:采用“有机会插队”的策略,一个线程获取锁之间,要先尝试获取锁,而不是先在队列中等待;如果获取锁失败,那么才将当前线程追加到队列中等待。
即非公平锁比公平锁多了一个线程未入队时的尝试获取锁的操作(插队操作)。
3. ReentrantReadWriteLock对象
- ReentrantReadWriteLock是一个读写锁对象。
- 读锁:读锁是一个共享锁,多个线程同时调用同一个读操作时不会互斥,减少同步耗费的时间。
- 写锁:写锁是一个独享锁,多个线程同时调用同一个写操作时会互斥,需要等待第一个线程的写操作执行完,才能继续执行第二个线程的写操作。
- 读锁和写锁同时使用时:此时一个线程调用读锁的代码,而另一个线程调用写锁的代码,此时读锁和写锁会出现互斥,需要等待线程的读/写操作执行完,才能继续执行写/读的操作。
4. 可重入锁
- 可重入锁,就是一个线程如果抢占到了互斥锁资源,在释放该锁资源之前,再去竞争同一把锁的时候,不需要等待,只需要记录重入次数。
5. Synchronized关键字和ReentrantLock对象的区别
- Synchronized是一个关键字,ReentrantLock是一个类。
- Synchronized会自动加锁和释放锁,而ReentrantLock需要手动加锁和释放锁。
- Synchronized底层是一个JVM层面的锁,ReentrantLock是API层面的锁。
- Synchronized是一个非公平锁,ReentrantLock可以选择公平锁和非公平锁。
- Synchronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态。
- Synchronized底层有一个锁升级的过程,即偏向锁、轻量级锁,重量级锁。
6. 线程池ThreadPoolExecutor
- JVM频繁地创建和销毁线程对象,如果请求的执行时间很短,则有可能花在创建和销毁线程对象的时间大于真正执行任务的时间,导致系统性能会大幅降低。
线程池主要用于支持高并发的访问处理,线程池类ThreadPoolExecutor实现了Executor接口。线程池的核心原理是创建一个“线程池”(ThreadPool),在池中对线程对象进行管理,包括创建和销毁,使用线程池的时候只需要执行具体的任务即可,线程对象的处理都在线程池中被封装了。 - 使用Executors工厂类创建线程池
1)无界线程池
ExecutorService executor = Executors.newCachedThreadPool();
就是池中存放的线程个数是理论上的最大值Integer.MAX_VALUE。
2)有界线程池
ExecutorService executor = Executors.newFixedThreadPool(4);
就是在创建有界线程池时,传入线程池中线程的最大个数,这个值是有界的。
3)单例线程池
ExecutorService executor = Executors.newSingleThreadPool(4);
可以实现以队列的方式来执行任务,采用单例模式的设计。 - ThreadPoolExecutor最常用的构造方法,public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue);
1)corePoolSize:池中至少要保留的线程数,该属性就是定义corePool核心池的大小。
2)maximumPoolSize:池中允许的最大线程数,maximumPoolSize包含了corePoolSize。
3)keepAliveTime:当前线程数大于corePoolSize时,在没有超过指定时间内是不会销毁线程池中多余的空闲线程的;如果超过指定时间,则销毁线程池中多余的空闲线程。
4)unit: keepAliveTime的时间单位。
5)workQueue:执行前用于保存任务的队列。此队列仅保存由executor线程池对象提交的Runnable方法。 - ThreadPoolExecutor的拒绝策略
1)AbortPolicy(默认策略):当任务添加到线程池中被拒绝时,将抛出RejectedExecutionException异常,这是线程池默认使用的拒绝策略。
2)CallerRunsPolicy:当任务添加到线程池中被拒绝时,会让调用者线程去执行被拒绝的任务,此时调用者线程会被阻塞,直到它将被拒绝的任务执行完毕。
3)DiscardOldestPolicy:当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未执行任务,然后将被拒绝的任务添加到等待队列。
4)DiscardPolicy:当添加任务到线程池中被拒绝时,丢弃被拒绝的任务。
二、Spring
1、ApplicationContext和BeanFactory的区别
- BeanFactory是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean。
- 而ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory的所有特点,也是一个Bean工厂,但是ApplicationContext除了继承了BeanFactory之外,还继承了EnvironmentCapable、MessageSource、ApplicationEventPublisher等接口,从而ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的。
2、Dubbo的架构设计
- Dubbo是一个RPC远程调用框架。
- Proxy服务代理层,支持JDK动态代理、javassist等代理机制。
- Registry注册中心层,支持Zookeeper、Redis等作为注册中心。
- Protocol远程调用层,支持Dubbo、Http等调用协议。
- Transport网络传输层,支持netty、mina等网络传输框架。
- Serialize数据序列化层,支持JSON、Hession等序列化机制。
3、Dubbo服务导出
- 首先Dubbo会将@DubboService或@Service注解的类进行解析得到定义的服务参数,包括定义的服务名、服务接口、服务超时时间、服务协议等等,得到一个ServiceBean。
- 然后调用ServiceBean的export方法进行服务导出。
- 然后将服务信息注册到注册中心,如果有多个协议,多个注册中心,那就将服务按单个协议,单个注册中心进行注册。
- 将服务信息注册到注册中心后,还会绑定一些监听器,监听动态配置中心的变更
- 还会根据服务协议启动对应的Web服务器或网络框架,比如Tomcat/Netty等。
4、Dubbo服务引入
- 使用@Reference注解引入一个服务,Dubbo会将注解和服务的信息解析出来,得到当前所引用的服务名、服务接口是什么
- 然后从注册中心进行查询服务信息,得到服务的提供者信息,并存储在消费端的服务目录中
- 并绑定一些监听器用来监听动态配置中心的变更
- 然后根据查询得到的服务提供者信息生成一个服务接口的代理对象,并放入Spring容器中作为Bean
5、Dubbo支持的负载均衡策略
- 随机:从多个服务提供者中随机选择一个来处理本次请求,调用量越大则分布越均匀,并支持按权重设置随机概率。
- 轮询:依次选择服务提供者来处理请求,并支持按权重进行轮询,底层采用的是平滑加权轮询算法。
- 最小活跃调用数:统计服务提供者当前正在处理的请求,下次请求过来则交给活跃数最小的服务器来处理。
- 一致性哈希:相同参数的请求总是发到同一个服务提供者。
8、RocketMQ的事务消息的实现
- 生产者订单系统先发送一条half消息到Broker,half消息对消费者而言是不可见的
- 再创建订单,根据创建的成功与否,向Broker发送commit或rollback
- 并且生产者订单系统还可以提供Broker回调接口,当Broker发现half消息一段时间后还没有收到任何操作命令,则会主动调用此接口来查询订单是否创建成功
- 一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束。
- 如果消费失败,则根据重试策略进行重试,最后还失败则进入死信队列,等待进一步处理。
9、Spring容器启动流程
- 在创建Spring容器,也就是启动Spring时,首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中。
- 然后筛选出非懒加载的单例BeanDefinition进行创建Bean,对于多例Bean则不需要在启动过程中去进行创建,对于多例Bean和懒加载Bean会在每次获取Bean对象时利用BeanDefinition进行创建
- 利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包含了合并BeanDefinition、推断构造方法,实例化、属性填充、初始化前、初始化后等步骤,其中AOP就是发生在初始化后这一步骤中。
- 单例Bean创建完了之后,Spring会发布一个容器启动事件。
- Spring启动结束
12、Spring AOP
用AspectJ需要使用切点表达式配置切点位置,写法如下:
-
标准写法:访问修饰符 返回值 包名.类名.方法名(参数列表)
-
访问修饰符可以省略。
-
返回值使用
*
代表任意类型。 -
包名使用
*
表示任意包,多级包结构要写多个*
,使用*..
表示任意包结构 -
类名和方法名都可以用
*
实现通配。 -
参数列表
- 基本数据类型直接写类型
- 引用类型写
包名.类名
*
表示匹配一个任意类型参数..
表示匹配任意类型任意个数的参数
-
全通配:
* *..*.*(..)
三、MySQL
1、B树和B+树的区别
- B树的特点:
1)节点排序
2)一个节点可以存多个元素,多个元素也排序 - B+树的特点:
1)拥有B树的特点
2)叶子节点之间有指针
3)非叶子节点上的元素在叶子节点上都冗余了,也就是叶子节点中存储了所有元素,并且排好顺序。 - MySQL索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序可以提高查询速度,然后通过一个节点可以存储多个元素,从而可以使得B+树的高度不会太高,在MySQL中一个InnoDB页就是一个B+树节点,一个InnoDB页默认是16kb,所以一般情况下一颗两层的B+树可以存2000万行左右的数据,然后通过利用B+树叶子节点存储了排序的所有数据,并且叶子结点之间有指针,可以很好的支持全表扫描、范围查询等SQL语句。
2、InnoDB实现事务的流程
InnoDB是通过BufferPool、LogBuffer、RedoLog、UndoLog来实现事务。
- InnoDB在收到一个update语句后,会先根据条件找到数据所在的页,并将该页缓存在BufferPool中
- 执行update语句,修改BufferPool中的数据,也就是内存中的数据
- 针对update语句生成一个RedoLog对象,并存入LogBuffer中
- 针对update语句生成undolog日志,用于事务回滚
- 如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将BufferPool中所修改的数据页持久化到磁盘中
- 如果事务回滚,则使用undolog日志进行回滚
3、慢查询优化
- 检查是否走了索引,如果没有则优化SQL利用索引
- 查询利用的索引是否为最优索引
- 检查所查字段是否都是必须的,是否查询过多字段,是否查出多余的数据
- 检查表中数据是否过多,是否应该进行分库分表
- 检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源
四、Redis
1、Redis和MySQL保证数据一致
- 先更新MySQL再更新Redis,可能Redis更新失败,仍然会出现数据不一致。
- 先删除Redis缓存数据,再更新MySQL,再次查询的时候,将数据添加到Redis中,这个方案可以解决方案1的问题,但是在高并发场景下,性能较低,而且也可能会出现数据不一致的问题,例如线程1删除了Redis的数据,正在更新MySQL数据,线程2正在查询数据,则可能会将MySQL的老数据又添加Redis中。
- 延时双删,先删除Redis缓存,再更新MySQL,延迟几百毫秒再删除Redis缓存,这样就算在更新MySQL时,有其他线程查询并将老数据又添加到Redis中,那么也会被删除,从而实现数据一致。
2、Redis数据结构
- 字符串:可以用来做最简单的数据,可以缓存某个简单的字符串,也可以缓存某个JSON格式的字符串。Redis分布式锁的实现就利用这种数据结构,还包括可以实现计数器、Session共享和分布式ID。
- 哈希表:可以用来存储一些key-value对,更适合用来存储对象。
- 列表(链表):Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似于微信公众号、微博等消息流数据。
- 集合:和列表相似,可以存储多个元素,但是不能重复,集合可以进行并集、交集、差集操作,从而可以实现我和某人共同关注的人、朋友圈点赞等功能。
- 有序集合:集合是无序的,有序集合可以设置顺序,可以用来实现排行榜等功能。
3、缓存雪崩、缓存击穿、缓存穿透
- Redis缓存中存放的大多都是热点数据,目的就是让请求可以直接从缓存中获取数据,而不是直接访问MySQL数据库。
- 缓存雪崩:如果缓存中某一时刻大批热点数据同时过期,那么就可能导致大量请求直接访问MySQL,这就是缓存雪崩。
解决方法就是在过期时间上增加一点随机值,还可以搭建一个高可用的Redis集群去防止缓存雪崩。 - 缓存击穿:和缓存雪崩类似,缓存雪崩是大批热点数据失效,而缓存击穿是指某个热点key突然失效,也导致大量请求直接访问MySQL数据,这就是缓存击穿。
解决方法就是考虑这个key不设过期时间。 - 缓存穿透:假如某一时刻访问Redis的大量key都不存在,那么也会给数据库造成压力,这就是缓存穿透。
解决方法是使用布隆过滤器,它的作用是如果它认为某个key不存在,那么这个key就一定不存在;如果它认为这个key存在,那么这个key不一定存在。所以在缓存之前加一层布隆过滤器来拦截不存在的key。
4、Redis单线程快的原因
- 纯内存操作
- 核心是基于非阻塞的IO多路复用机制。
- 单线程反而避免了多线程的频繁上下文切换带来的性能问题。
5、Redis分布式锁底层实现
- 首先利用setnx来保证:如果key不存在,才能获得锁,如果key存在,则获取不到锁
- 然后还要利用lua脚本来保证多个redis操作的原子性。
- 同时还要考虑到锁过期,所以需要额外的一个哨兵定时任务来监听锁是否需要续时。
- 同时还要考虑到Redis节点挂掉后的情况,所以需要采用红锁的方式来同时向N/2+1个节点申请锁,都申请到了才证明获取锁成功,这样就算其中redis节点挂掉了,锁也不能被其他客户端获取到。
五、SpringBoot
1、SpringBoot框架的作用
-
Spring是一个非常优秀的轻量级框架,以IOC(控制反转)和AOP(面向切面)为思想内核,极大的简化了企业级项目的开发。
虽然Spring的组件代码是轻量级的,但是它的配置却是重量级的,使用Spring进行项目开发需要在配置文件中编写很多配置代码,所有这些配置都代表了开发时的损耗。
Spring的配置繁琐,依赖过多,版本复杂。 -
SpringBoot针对Spring进行优化和改善,简化了Spring的开发,即简化了Spring中大量的配置文件和繁琐的依赖引入。所以SpringBoot是一个服务于框架的框架,它不是对Spring功能的增强,而是提供了一种快速使用Spring框架的方式。即Spring的脚手架。
SpringBoot的思想是约定大于配置。 -
SpringBoot的核心功能分别由自动配置和起步依赖,即为了简化Spring的开发。
-
自动配置,SpringBoot自动提供最优配置,同时可以修改默认值满足特定要求。即可以简化Spring的配置。
-
起步依赖,SpringBoot的依赖是基于功能的,而普通项目的依赖是基于JAR包的,SrpingBoot将完成一个功能所需要的所有坐标打包在一起,并完成了版本适配,在使用某功能时,只需要引入一个依赖即可。
坐标,相当于dependency。
2、SpringBoot启动Tomcat
-
首先SpringBoot在启动时,会创建一个Spring容器。
-
在创建Spring容器过程中,会利用@ConditionalOnClass技术来判断当前classpath路径中有没有存在tomcat依赖,如果存在则会生成一个启动Tomcat的Bean。
-
Spring容器创建完之后就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端口等,最后启动Tomcat。
-
SpringBoot是先启动Spring容器,再通过容器去启动Tomcat。
而不用SpringBoot时,是先启动Tomcat,再通过Tomcat去启动Spring容器。
3、SpringBoot配置Tomcat
- SpringBoot对Tomcat默认配置最大连接数为8292,8192为最大连接数,100为最大等待数。
- 可以在yml文件中配置
1)server.tomcat.max-connections:最大连接数
2)server.tomcat.min-pace:最少线程数或核心线程数
3)server.tomcat.max:最多线程数
4)server.tomcat.accept-count:最大等待数
六、MyBatis
1、MyBatis的优缺点
-
优点:
1)基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML文件中,解除SQL与程序的代码耦合,方便统一管理,提供XML标签,支持编写动态SQL语句 ,并可重用。也可以使用注解的形式使用编写SQL语句。
2)跟JDBC相比减少了百分之50以上的代码量,消除了JDBC大量冗余代码,不需要手动开关连接。
3)很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要是JDBC支持的数据库MyBatis都能支持)。
4)能够与Spring很好的集成。
5)提供映射标签,支持对象与数据库的ORM对象关系映射,提供对象关系映射标签,支持对象关系组件维护。 -
缺点:
1)SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底也有一定要求。
2)SQL语句依赖数据库,导致数据库移植性差,不能随意更换数据库。 -
#{}
1)#{}是预编译处理、占位符。
2)MyBatis在处理#{}时,会将SQL中的#{}替换成?,调用PreparedStatement来赋值。
3)使用#{}的话,#{}内的变量不会被解释为SQL代码。即使用#{}可以有效防止SQL注入,提高系统安全性。 -
${}
1)${}
是字符串替换、拼接符。
2)MyBatis在处理${}
时,会将SQL中的${}
替换成变量的值,调用Statement来赋值。
2、MyBatis-Plus
- MyBatis-Plus(MP)是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生。
- 提供快速实现单表的CRUD操作,节省大量时间。
- 只做增强不做改变,不会对现有的工程造成影响。
- 提供代码生成,自动分页,逻辑删除,自动填充等功能。
七、JVM
1、JVM的堆内存模型
- 堆内存模型分为年轻代、年老代、永久代。
- 年轻代中分为Eden(伊甸园)区和Survivor(幸存)区;Eden区存储从未被垃圾回收的新对象;Survivor区又分为Survivor1和Survivor2,存放垃圾回收后仍然存在的对象,在Survivor1和Survivor2中来回存放(即第一次垃圾回收后存放在Survivor1,第二次垃圾回收后从Survivor1存放到Survivor2,反复来回存放)小于等于15次时,存放在Survivor区。
- 年老代,当Survivor区存放超过15次的对象会被存到年老区。
- 永久代,用于存放唯一不变的信息,如Java类、方法等,垃圾回收对永久代没有显著影响。
1)JDK1.7以前就是“方法区”的一种实现。
2)JDK1.8以后已经没有“永久代”了,使用metaspace元数据空间和堆替代。 - Eden区满了就会触发一次Minor GC,用于清理无用对象,将有用对象复制到“survivor1”、“survivor2”区中。
- Major GC,用于清理年老代。
- Full GC,用于清理年轻代、年老代、永久代区域。成本较高,会对系统性能产生影响。
- 可能导致Full GC的原因:
1)年老代(Tenured)被写满。
2)永久代(Perm)被写满。
3)System.gc()被显示调用。
4)上次GC之后Heap的各域分配粗略动态变化。
2、JVM的内存
- JVM内存可以简单分为栈、堆、方法区。
- 栈stack,存储main方法的栈帧、main方法的参数args以及对象引用s1:0x12。即存储一些方法和对象的引用以及局部变量。
- 堆heap,存储对象,即对象成员变量和方法的引用以及成员变量。
- 方法区method area存储普通方法代码,常量池以及static修饰的属性和方法。即存储一些可以共用的代码以及静态变量。
八、业务
1、如何实现接口的幂等性
- 唯一id。每次操作都根据操作和内容生成唯一的id,在执行前先判断id是否存在,如果不存在则执行后续操作,并保存到数据库或Redis等。
- 服务端提供发送token的接口,业务调用接口前先获取token,然后调用业务接口请求时,把token携带过去,服务器判断token是否存在redis中,如果存在表示第一次请求,可以继续执行业务,执行业务完成后,需要把Redis中的token删除。
- 建去重表。将业务中唯一标识的字段保存到去重表,如果表中存在,则表示已经处理过了。
- 版本控制。增加版本号,当版本号符合时,才能更新数据。
- 状态控制。例如订单有状态,已支付、未支付、支付中、支付失败,当处于未支付状态时才允许修改为支付中。
一般需要加锁进行判断和执行操作。