博主最近重新复习梳理了一下Java后端的技术栈知识,大多都是时下面试很常问的知识点,纯手打整理了好久,现在粘出来供大家一起学习交流,有很多个人可能模棱两可的知识点,也谨慎的去查阅了很多博客,文章中也会出现一些个人选摘的一些很棒的博客,都是关于某个知识点更容易理解的讲法,也希望大家本着学习的态度耐心去读一下。如果还有什么缺陷和不足,欢迎大家评论指正,随时更新修改。废话不多说,上干货:


JAVA中四种修饰符的限制范围:
private:修饰的成员只能在同类中别访问,而在同包、子类和其他包中都不能被访问
public:修饰的成员在同类、同包、子类(继承自本类)、其他包都可以访问
protected:修饰的成员在同类、同包、子类中可以访问,其他包中不能被访问
default:修饰的成员在同类、同包中可以访问,但其他包中不管是不是子类都不能被访问


final修饰:

final去修饰一个类的时候,表示这个类不能被继承, final类中的成员方法都会被隐式的指定为final方法。

final修饰方法,方法不能被重写。 一个类的private方法会隐式的被指定为final方法。如果父类中有final修饰的方法,那么子类不能去重写。

修饰成员变量

a. 必须初始化值。

b. 被fianl修饰的成员变量赋值,有两种方式:1、直接赋值 2、全部在构造方法中赋初值。

c. 如果修饰的成员变量是基本类型,则表示这个变量的值不能改变。

d. 如果修饰的成员变量是一个引用类型,则是说这个引用的地址的值不能修改,但是这个引用所指向的对象里面的内容还是可以改变的。


浅拷贝深拷贝:

数据分为基本数据类型和对象数据类型:

1、基本数据类型的特点:直接存储在栈(stack)中的数据

2、引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里

引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

浅拷贝如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。


Object含有的方法:

Clone:浅拷贝

引申:如何实现深拷贝?:①实现cloneable②先把对象序列化到内存,再反序列化。

class与getClass:

Class:类的Class类实例是通过.class获得的  getClass:是一个对象实例的方法,只有对象实例才有这个方法。

.class功能完全等于.getClass()!只是一个是用类直接获得的,一个是用实例获得的

getName:返回class对象的名称,string字符串

hashcode和equals以及“==”:

两个对象相等,hashcode一定是相等的,两个对象的hashcode相等,这两个对象不一定相等。Hashcode可以重写,如果重写了equals,那么一定要重写hashcode。

基本数据类型,也称原始数据类型byte,short,char,int,long,float,double,boolean。他们之间的比较,应用“==”,比较的是他们的值,引用数据类型用“==”比较的是指向的地址。equals方法,是object中的方法,如果不进行重写的话,比较的也是引用的地址值,实际和“==”一样。 .如果自己所写的类中已经重写了equals方法,那么比较地址指向的内容。

new和newInstance:

newInstance:一般和class.forName搭配使用,class.forName要求jvm去查找加载指定的类,newInstance是给他实例化。

Class.forName("")返回的是类

Class.forName("").newInstance()返回的是object

newInstance( )是一个方法,只能调用无参的构造函数,而且必须保证类已经加载了。

new是一个关键字,生成对象是没有构造函数限制的。

Class.forName和newInstance合起来其实等于new,提供了一种解耦的方式,但是效率低。

例子:

class c = Class.forName("Example");

factory = (ExampleInterface)c.newInstance();

注:java9中已经把newInstance废弃掉了。

wait,notify,notifyAll:

三个方法是Object的本地final方法,无法被重写。

wait()、notify/notifyAll() 必须在synchronized 代码块执行,说明当前线程一定是获取了锁的。

当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。

当 notify/notifyAll() 被执行时候,会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。

也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁。

wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒。


String ,StringBuffer,StringBuilder:

String不可变,因为成员变量是final修饰的,Java中String类其实就是对字符数组的封装。String每次变都是重新生成一个string,丢弃之前的。

单线程操作大量数据可以用stringbuilder,性能高。

多线程要用stringbuffer,其很多方法都加了 synchronized,线程安全。


序列化与反序列化:

序列化就是把对象转换为字节序列的过程,叫对象序列化。

反序列化就是把字节对象恢复为对象的过程。

在代码运行的时候,有些对象的数据信息我们想要持久的保存起来,就把对象变成一连串字节保存文件。

为什么序列化:持久储存数据、网络上传输对象

如何序列化:实现serializable接口,或者Json序列化。


集合

List:

ArrayList和LinkedList:

ArrayList底层是Object数组,LinkedList底层是双向链表。

因为底层的原因,所以ArrayList查找快O(1),LinkedList插入删除快O(1)。

另外,还有链表和线性表的区别,链表不需要连续的地址空间,可以有效利用零散的内存,但是因为要存前后节点指针,所以空间利用率相对不高。

Map:

Jdk1.7:数组(位桶)+链表;jdk8:数组(位桶)+链表+红黑树。

当桶的元素大于8的时候链表转红黑树,当桶的元素小于6的时候,转回链表。因为红黑树的平均查找长度是logn,链表的平均查找长度是n/2,当元素个数为8的时候,链表ASL=4.,这时候红黑树的查找长度更短。之所以选6和8作为转化值,而不是选7,是因为避免频繁增加或者删除一个元素就会导致转化,如果都选7,增加了一个元素转化后,又删除了一个,那么又要转化,频繁的转化也很耗费性能。

底层的逻辑:

图1.1

Hashmap扩容条件:

Jdk1.7:①哈希表长度为0(新建)的时候,扩容为默认长度。②大于阈值(hashmap大小*负载因子,默认是16*0.75=12)&哈希值发生冲突。

1.8:1.7的基础上新增上图逻辑中的扩容条件(见图1.1)。

Hashmap扩容会引发死锁:

1.7会死锁,因为在扩容的时候会把原来的往另一个新的里面拷贝,用的是头插法,原来ABC会变成CBA,如果线程1和线程2同时调用同一个hashmap,线程1先进行完扩容,这时候线程2 原来的A指向B就和现在B指向A形成了环造成死锁,线程不安全。1.8已经优化成尾插法,不会成环导致死锁,但还是线程不安全的,会出现数据覆盖,还是两个线程同时put一个Entry的时候,会把老的数据的最后的next指向新的Entry,线程1执行完了,线程2 又完了,就会把线程1的put覆盖掉。这时候就引出了ConcurrentHashMap,加分段锁,区别于Hashtable锁整个map。

Hashmap,Hashtable,ConcurrentHashMap:

Hashtable线程安全,但是效率低。

Hashmap线程不安全,效率高。

ConcurrentHashMap线程安全,效率高。

put(Entry)流程:

①检查哈希表是否为空,不为空就会扩容

②判断key的数组索引地址,拿到该地址链表或者红黑树的首节点

③如果首节点是空,就把封装成node节点放进去;如果首节点不为空,判断当前key和首节点是否一致,如果一致,替换,不一致就判断当前是红黑是还是链表,如果是红黑树就做树节点插入,如果是链表就遍历判断是否有冲突,如果的个数超过阈值就扩容,如果没有超过阈值就转红黑树(见图1.1)。

注:网上多数只会提到转红黑树的数字是6和8,但是一般都未提到图1.1这个逻辑,还要看哈希表的长度。


I/O:

Tomcat从socket里面取数据两种io模型,BIO和NIO。

Bio:一个socket连接对应一个线程,不是一个http请求对应一个线程,可能是多个请求对应一个线程。

BIO在socket客户端连接的时候和读取数据的时候会阻塞,所以一个客户端对应一个线程,所以BIO的缺点就是线程太多了。

NIO就是为了解决线程多的问题,nio把两个阻塞的地方改为非阻塞,把服务端的socket注册到选择器selector里面,监听socket事件,当有客户端发来请求时,去处理。

Bio是主动监听,nio是被动。


多线程&锁:

线程的五大状态分别为:创建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)、死亡状态(Dead)。

Volatile关键字:

Volatile关键字使得变量在多个线程间可见。各个线程会将共享变量从主内存拷贝到线程工作内存中,执行完之后再放回主内存。Volatile修饰的变量修改会在主内存,立刻被其他线程感知,避免出现脏读。只能保证可见性,不能保证原子性,还是有线程安全问题。

Volatile还可以防止指令重排,在编程中实现我们想要的代码执行顺序。

volatile为什么不能保证原子性:让一个volatile的integer自增(i++),其实要分成3步:1)读取volatile变量值到local; 2)增加变量的值;3)把local的值写回,让其它的线程可见。还有最后一步内存屏障。从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。

进程是如何进行通信的:

1.管道:速度慢,容量有限,只有父子进程能通讯

2.FIFO:任何进程间都能通讯,但速度慢

3.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题

4.信号量:不能传递复杂消息,只能用来同步

5.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

线程是如何进行通信的:

①同步:这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信。本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。

②while轮询的方式:在这种方式下,线程A不断地改变条件,线程ThreadB不停地通过while语句检测这个条件(list.size()==5)是否成立 ,从而实现了线程间的通信。但是这种方式会浪费CPU资源,因为JVM调度器将CPU交给线程B执行时,它没做啥有用的工作,只是在不断地测试某个条件是否成立。

③wait/notify机制:线程A要等待某个条件满足时(list.size()==5),才执行操作。线程B则向list中添加元素,改变list 的size。

wait()和sleep():

这两个方法分别来自Thread和Object,sleep占用cpu,wait不占用。

最主要是sleep方法没有释放锁,而wait方法释放了锁。

死锁条件:互斥条件、不可抢占、请求与保持、循环等待

线程启动:

线程启动方式:

1、继承Thread类

2、实现Runnable接口

3、应用程序可以使用Executor框架来创建线程池。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的。

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现就是悲观锁。

悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

悲观锁在Java中的使用,就是利用各种锁。

乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有。

共享锁是指该锁可被多个线程所持有。

对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。

  读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

  独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

Synchronized是独享锁。

synchronized修饰不加static的方法,锁是加在单个对象上,不同的对象没有竞争关系;

synchronized修饰加了static的方法,锁是加载类上,这个类所有的对象竞争一把锁。

锁升级:https://blog.csdn.net/tongdanping/article/details/79647337

Synchronized和ReentrantLock(synchronized和JDK提供的锁区别):

相似点:

都是重入锁,都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的。

区别:

这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,需要jvm实现,配合wait()和notify()。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁。

线程池(Executors):

线程池类:ThreadPoolExecutor。

为什么要用线程池:

①降低资源消耗。通过重复利用已创建的线程降低线程创建、销毁线程造成的消耗。

②提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

③提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。

创建线程池的方法:Executors工厂方法创建、new ThreadPoolExecutor()自定义创建

线程池拒绝策略:

①默认丢弃任务抛出异常,适用于关键业务场景。

②丢弃任务,但是不抛出异常。建议是一些无关紧要的业务采用此策略。例如博客网站统计阅读量。

③丢弃队列最前面的任务,然后重新提交被拒绝的任务。

④由调用线程处理该任务。

线程池类配置参数:

多线程编程:JUC包下的工具类,包括某些线程池类以及FutureTask等工具类的使用。

原子类:可以保证原子类的操作是原子操作,不会有并发问题,基于cas实现。

CAS(Compare and Swap 比较并交换):CAS是乐观锁的一种实现方式,当多个线程尝试使用CAS同时更新同一个变量时,只有一个线程可以更新变量的值,其他的线程都会失败,失败的线程并不会挂起,而是告知这次竞争中失败了,并可以再次尝试。

CAS操作包括三个操作数:需要读写的内存位置(V)、预期原值(A)、新值(B)。如果内存位置与预期原值的A相匹配,那么将内存位置的值更新为新值B。如果内存位置与预期原值的值不匹配,那么处理器不会做任何操作。

CAS问题:①搭配死循环来不断循环判断值是否是期望值,对cpu有较大开销。②会有ABA的问题,值一开始是1,改成了2然后又改回成1,CAS判断期望值是1,但不知道中间有变化,可以加一个版本号或者日志记录表解决。

AQS:

AQS内部实现了两个双向队列,一个同步队列,一个条件队列。

同步队列的作用是:当线程获取资源失败之后,就进入同步队列的尾部保持自旋等待,不断判断自己是否是链表的头节点,如果是头节点,就不断参试获取资源,获取成功后则退出同步队列。

条件队列是为Lock实现的一个基础同步器,并且一个线程可能会有多个条件队列,只有在使用了Condition才会存在条件队列。

AQS支持独占锁和共享锁两种模式。

独占锁:只能被一个线程获取到(Reentrantlock)

共享锁:可以被多个线程同时获取(CountDownLatch,ReadWriteLock).

无论是独占锁还是共享锁,本质上都是对AQS内部的一个变量state的获取。state是一个原子的int变量,用来表示锁状态、资源数等。

参考:https://www.cnblogs.com/zofun/p/12206759.html


JVM:

Jvm内存模型:

①程序计数器:当前线程执行的字节码的指示器

②虚拟栈:方法本身和操作的参数

③本地方法栈:放native的方法

④堆(问的最多):放属性的类型、对象的本身,但是不放对象的方法。涉及垃圾回收和对象头。

⑤方法区:老的方法区存类的版本、字段、方法、接口信息、常量池

堆展开:

对象头:分三块,真实的对象头、实例数据、对齐补充(往往只需要知道一个对象分三块去放的就可以)例:Integer对象是int的几倍?为什么?答:32位机器,一个Integer对象包括数据部分占4字节,markword占4字节,指针占4字节,还有对齐补充占4个字节,共16字节。如果是64位机器,指针大小变成8字节。

类加载:

加载:.class放在内存当中。

链接:

①验证:验证类的结构等(主要是文件格式的验证,元数据的验证,字节码验证,符号引用验证)

②准备:给静态变量在方法区分配内存,赋初始值,有别于初始化的值。例如代码语句:

int a=1;初始化的时候是赋1,但是这里是系统默认int初始化值0。静态常量在准备阶段赋值就是代码定义的值。

③解析:主要的任务是把常量池中的符号引用替换成直接引用

初始化:为静态变量赋初始值。

注:在java中,一个java类将会编译成一个class文件。在编译时,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。比如org.simple.People类引用org.simple.Student类,在编译时People类并不知道Student类的实际内存地址,因此只能使用符号org.simple.Student(假设)来表示Student类的地址。而在类装载器装载People类时,此时可以通过虚拟机获取Student类 的实际内存地址,因此便可以既将符号org.simple.Student替换为Student类的实际内存地址,及直接引用地址。

参考自:https://www.cnblogs.com/treasury/p/13229029.html

双亲委派:

类加载器、扩展加载器、system加载器,从下到上寻找,从上到下加载。

JVM中提供了三层的ClassLoader:

Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。

ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。

AppClassLoader:主要负责加载应用程序的主函数类

参考自:https://blog.csdn.net/codeyanbao/article/details/82875064

Tomcat破坏掉了双亲委派。(一般不会问太深)

垃圾回收:

GC又分为 Minor GC 和 Full GC。

New出来的对象先放到Eden区,如果Eden放满了(超过阈值),但是程序还在运行,jvm就会进行Minor GC,GCROOT,从栈或者方法区中找到指向堆的内存地址,一层层找引用,知道某个堆内存中的对象不再引用下一个,这一条root上的对象全被设置为非垃圾,把非垃圾对象放到s0区,把没有引用的全部清理掉。接下来继续把s0内的非垃圾对象放到s1,再下一次s1再放到s0,循环放,每次分代年龄加一。s0和s1就是From Space区和To Space区。

Full GC触发条件:

①System.gc()方法的调用②老年代(Tenured Gen)空间不足③通过Minor GC后进入老年代的平均大小大于老年代的可用内存④由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

调优:年轻代的Eden区、s0区、s1区默认8:1:1,Survivor实际上占了很小一部分,这是因为大部分的对象被创建过后很快就会被GC。如果临时的对象特别多,还需要大空间,就可以把Survivor区或者Eden区调大,避免临时大对象进入老年代。

四种引用:

强引⽤(StrongReference):如果⼀个对象具有强引⽤,垃圾回收器绝不会回收它。当内存空间不⾜,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终⽌,也不会回收具有强引⽤的对象。

软引⽤(SoftReference):如果⼀个对象只具有软引⽤,如果内存空间⾜够,垃圾回收器就不会回收它,如果内存空间不⾜了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使⽤。软引用可以用来实现内存敏感的高速缓存。

弱引⽤(WeakReference):如果⼀个对象只具有弱引⽤,和软引用的区别在于:弱引用的对象的生命周期更短,不管当前内存空间够不够,GC时都会将它回收。

虚引⽤(PhantomReference):与其他⼏种引⽤都不同,虚引⽤并不会决定对象的⽣命周期。如果⼀个对象仅持有虚引⽤,那么它就和没有任何引⽤⼀样,在任何时候都可能被垃圾回收。

垃圾回收算法:

判断对象是否死亡:引用计数和可达性分析。

标记-清除算法:如果在被标记后直接对对象进行清除,会使内存碎片化。如果下次有比较大的对象实例需要在堆上分配较大的内存空间时,可能会出现无法找到足够的连续内存而不得不再次触发垃圾回收。

复制算法(Java堆中新生代的垃圾回收算法):此GC算法实际上解决了标记-清除算法带来的“内存碎片化”问题。首先还是先标记处待回收内存和不用回收的内存,下一步将不用回收的内存复制到新的内存区域,这样旧的内存区域就可以全部回收,而新的内存区域则是连续的。它的缺点就是会损失掉部分系统内存,因为你总要腾出一部分内存用于复制。

CMS收集器原理:

①初始标记(CMS initial mark):标记一下GC Roots能直接关联到的对象。需要STW,速度很快。

②并发标记(CMS concurrent mark):进行GC Roots Tracing。不需要STW。会产生浮动垃圾。

③重新标记(CMS remark):找到并发标记期间产生的浮动垃圾。需要STW,停顿时间一般会比初始标记稍长,但远比并发标记短。

④并发清除(CMS concurrent sweep):清除已标记的垃圾。不需要STW。会产生浮动垃圾,只能等下一次GC清理。

CMS有两个STW(Stop-The-World):初始标记和重新标记。

CMS采用的是标记清除算法,算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾回收,另外CMS只能回收老年代。

G1:https://baijiahao.baidu.com/s?id=1668544883896956090&wfr=spider&for=pc

Jvm配置操作参考:https://www.cnblogs.com/likehua/p/3369823.html


MySQL:

InnoDB,是MySQL的数据库引擎之一,现为MySQL的默认存储引擎,InnoDB的最大特色就是支持了ACID兼容的事务(Transaction)功能。

事务的隔离级别,默认值为 Isolation.DEFAULT

可选的值有:

Isolation.DEFAULT

使用底层数据库默认的隔离级别。(MySQl 默认:Repeatable read可重复读)

Isolation.READ_UNCOMMITTED

(读未提交):最低级别,任何情况都无法保证。

Isolation.READ_COMMITTED

(读已提交):可避免脏读的发生。

Isolation.REPEATABLE_READ

(可重复读):可避免脏读、不可重复读的发生

Isolation.SERIALIZABLE

(串行化):可避免脏读、不可重复读、幻读的发生。

索引类型:唯一索引,主键索引,组合索引,全文索引

索引原理:

为什么用B+:既可以精确查询也可以范围查询,还能降低I/O次数。

最左匹配:针对联合索引(A,B,C),AC和BAC会命中索引,但是BC不会命中索引。因为b+搜索树是从左到右搜索的,先看到A才能确定一下个索引方向,AC相当于只命中了A索引。BAC会命中索引是因为MySQL在真正执行之前会自动优化sql语句为最优效率,所以BAC在执行的时候变成了ABC,命中全部索引。BC因为没有A,所以无法命中索引。

聚集索引:聚集索引一定会有,如果有主键,根据主键构造聚集索引。如果没有主键,根据唯一的索引来建聚集索引。如果以上两种都没有,就隐式的生成自增的列来建聚集索引。

辅助索引(非聚集索引):叶子结点不会放数据,放聚集索引的索引位置,相当于书签。不影响聚集索引结构

查找过程:先命中辅助索引找聚集索引的位置,聚集索引查找叶子结点找到真实的数据。

为了防止回表查询,可以索引覆盖。参考:https://www.cnblogs.com/myseries/p/11265849.html

索引失效场景:

1.or连接;

2.复合索引未用最左列字段(违反最左匹配原则);

3.like以%开头;

4.需要类型转换;

5.where中索引列有运算;

6.where中索引列使用了函数;

7.如果mysql觉得全表扫描更快时

SQL优化:减少join,减少排序,尽可能走索引。join的时候小表驱动大表,在join字段上建立索引。谓词下推,join的时候子查询能早过滤掉数据就早过滤掉。

分库分表:

分库:多数据源,springboot、mybatis封装

分表:垂直分表:假如原来是n列,分成n/2列一个表。水平分表:如果数据比较平均,可以按照主键的哈希值分表。

分库分表的问题:分布式事务,跨库的join,全局id。解决:可以用中间件MyCat或分布式存储DRDS、Tidb。

分库分表可参考:https://zhuanlan.zhihu.com/p/137368446

搜索型NoSql(代表----ElasticSearch):

传统关系型数据库主要通过索引来达到快速查询的目的,但是在全文搜索的场景下,索引是无能为力的,like查询一来无法满足所有模糊匹配需求,二来使用限制太大且使用不当容易造成慢查询,搜索型NoSql的诞生正是为了解决关系型数据库全文搜索能力较弱的问题,ElasticSearch是搜索型NoSql的代表产品。

全文搜索的原理是倒排索引,我们看一下什么是倒排索引。要说倒排索引我们先看下什么是正排索引,传统的正排索引是文档-->关键字的映射,例如"Tom is my friend"这句话,会将其切分为"Tom"、"is"、"my"、"friend"四个单词,在搜索的时候对文档进行扫描,符合条件的查出来。这种方式原理非常简单,但是由于其检索效率太低,基本没什么实用价值。

倒排索引则完全相反,它是关键字-->文档的映射,意思是我现在这里有四个短句:

"Tom is Tom"

"Tom is my friend"

"Thank you, Betty"

"Tom is Betty's husband"

搜索引擎会根据一定的切分规则将这句话切成N个关键字,并以关键字的维度维护关键字在每个文本中的出现次数。这样下次搜索"Tom"的时候,由于Tom这个词语在"Tom is Tom"、"Tom is my friend"、"Tom is Betty's husband"三句话中都有出现,因此这三条记录都会被检索出来,且由于"Tom is Tom"这句话中"Tom"出现了2次,因此这条记录对"Tom"这个单词的匹配度最高,最先展示,这就是搜索引擎倒排索引的基本原理。

参考:https://www.cnblogs.com/xrq730/p/11039384.html


Spring:

AOP:思想是代理,如果目标对象实现了接口,默认走jdk的动态代理。

代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。Java 动态代理机制以巧妙的方式近乎完美地实践了代理模式的设计理念。

参考自:https://blog.csdn.net/jiankunking/article/details/52143504

IOC:也叫控制反转或依赖注入。

BeanFactory和FactoryBean:BeanFactory是ioc容器的核心接口,FactoryBean是一个工厂接口,可以自己实现来代理一个对象。

Spring Boot 的核心注解:

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。

@ComponentScan:Spring组件扫描。

@EnableAutoConfiguration:最重要,打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能。加载springfactory配置文件,识别key获取value(类的全路径)读到真实的类,把这个类加载到ioc容器中,这样就实现了自动配置,然后就建立了bean的依赖关系。

Spring Boot不需要独立的容器运行,内置了Tomcat/ Jetty等容器。

事务注解失效场景:

1、事务注解@Transactional只能放在public修饰的方法上,如果放在private,protected方法上,不报错,但是事务不起作用

2、表的存储引擎为MyISAM是不支持事务的,需要使用InnoDB引擎

3、加事务的方法中手动try...catch住了异常,只有将异常抛出来(无论是主动还是被动)事务才能回滚

4、Spring事务默认回滚的是RunTimeException运行时异常,如果是check异常是不会回滚的,可以指定回滚异常

事务传播机制:默认当前无事务就新建事务,有事物就加入。防止同service下方法互相调用,导致spring aop失效


中间件:

Redis:

单线程为什么还那么快:①基于内存,内存操作处理速度快。②只有处理读写是单线程,其他比如接收网络链接等都是多线程。③数据结构简单。

Redis6.0已经支持多线程了,之所以一开始做成单线程是因为单线程性能已经非常高了,而且实现简单,没有并发的一系列问题。

Redis支持的数据类型:

1.string:最基本的数据类型,二进制安全的字符串,最大512M。

2.list:按照添加顺序保持顺序的字符串列表。

3.set:无序的字符串集合,不存在重复的元素。

4.sorted set(zset):有序集合,已排序的字符串集合。

5.hash:key-value对的一种集合。

zset:底层使用hash和跳跃列表。hash保证原数value的唯一性,跳表类似于二分查找,多级跳表降低查询过程。

Redis与Memcached的区别:

1 Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set等数据结构的存储。

2 Redis支持数据的备份,即master-slave模式的数据备份。

3 Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。

Redis持久化:RDB和AOF。

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,全量持久化。

AOF工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录,增量的持久化。

缓存问题:

数据一致性:更新数据库有两种顺序:

①假设先写数据库,再淘汰缓存:第一步写数据库操作成功,第二步淘汰缓存失败,则会出现DB中是新数据,cache中是旧数据,数据不一致。

②假设先淘汰缓存,再写数据库:第一步淘汰缓存成功,第二步写数据库失败。cache中无数据,DB中是旧数据。

所以我们一般都是先淘汰缓存,如果操作完缓存后,在写数据库成功之前,如果有读请求发生,可能导致旧数据入缓存,引发数据不一致,这就是数据不一致的原因。

解决:两阶段提交、分布式事务。

预热、穿透、雪崩:

缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

缓存雪崩是指我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期,所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。

Kafka缺陷:

由于是批量发送,数据并非真正的实时。

不支持物联网传感数据直接接入。

仅支持统一分区内消息有序,无法实现全局消息有序。

监控不完善,需要安装插件。

依赖zookeeper进行元数据管理。

kafka可以关闭自动提交位移改为手动来确保消息不会漏消费:enable.auto.commit=false

MQ(类似kafka):

名词:生产者、消费者、topic会话

消费端重复消费:建立去重表,把消费过的记录下来。

消费端丢失数据:关闭自动提交offset,处理完之后受到移位,enable.auto.commit=false 关闭自动提交位移。

生产端重复发送:消费端消费之前从去重表中判重。

生产端丢失数据:消息发送失败了,重试多次,如果重试多次还失败,把失败的消息放到缓存里面,单独使用独立的topic,消费者再尝试校验消费。ack确认机制设置为 “all” 即所有副本都同步到数据时send方法才返回, 以此来完全判断数据是否发送成功。


分布式:

并发三大问题:

1.缓存导致的可见性问题,如cpu缓存,缓存的时差性导致数据对其他线程不可见

2.线程切换带来的原子性问题,如count += 1是由三个原子指令组成

3.编译优化带来的有序性问题,如双重检查创建单例,可能取到已分配内存地址、未初始化的单例(new指令3步操作,①分内存②初始化③赋值给引用变量,可能会发生①③②的重排序)

接口幂等:

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的,更复杂的操作幂等保证是利用唯一交易号(流水号)实现。其实在我们编程中主要操作就是CURD,其中读取(Retrieve)操作和删除(Delete)操作是天然幂等的,受影响的就是创建(Create)、更新(Update)。

导致这个情况会有几种场景:

前端重复提交:提交订单,用户快速重复点击多次,造成后端生成多个内容重复的订单。

接口超时重试:对于给第三方调用的接口,为了防止网络抖动或其他原因造成请求丢失,这样的接口一般都会设计成超时重试多次。

消息重复消费:MQ消息中间件,消息重复消费。

解决:Token机制、数据库去重表、Redis实现

分布式事务的实现主要有以下 5 种方案:

①XA方案(两阶段提交方案):第一阶段询问,第二阶段执行。有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。

②TCC 方案:一般来说跟钱相关的,跟钱打交道的,支付、交易相关的场景,我们会用 TCC,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。

③本地消息表:严重依赖于数据库的消息表来管理事务。

④可靠消息最终一致性方案:A 系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就直接取消操作别执行了。

如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 mq 发送确认消息,如果失败就告诉 mq 回滚消息。

如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务。

mq 会自动定时轮询所有 prepared 消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了。

⑤最大努力通知方案:略。

分布式锁需要注意的问题:

互斥性:任意时刻只能有一个客户端拥有锁,不能同时多个客户端获取。

安全性:锁只能被持有该锁的用户删除,而不能被其他用户删除。

死锁:获取锁的客户端因为某些原因而宕机,而未能释放锁,其他客户端无法获取此锁,需要有机制来避免该类问题的发生。

容错:当部分节点宕机,客户端仍能获取锁或者释放锁。

Redis分布式锁和Zk分布式锁:

对于redis的分布式锁而言,缺点如下:

获取锁的方式简单粗暴,获取不到锁直接不断尝试获取锁,比较消耗性能。

redis的设计定位决定了它的数据并不是强一致性的。

但是,redis实现分布式锁普及率很高,而且大部分情况下都不会遇到所谓的复杂极端场景。最重要的一点是redis的性能很高,可以支撑高并发的获取、释放锁操作。

对于zk的分布式锁而言:

zookeeper天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。

如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小

如果有较多的客户端频繁的申请加锁、释放锁,对于zk集群的压力会比较大。

推荐高可用大型分布式系统推荐zk分布式锁。

无论是Redis分布式锁还是zk分布式锁,设计思路来源于JUC包里的AQS设计思想,都是在加锁时锁定头结点(最小节点),解锁时删除头结点,WatchDog或者WatchNode机制都是模拟双链表,将下一个最小的节点设置为头结点,再次抢锁。

分布式一致性协议Paxos和Raft的对比:

Paxos算法和Raft算法二者的共同点在于,它们本质上都是单主的一致性算法。


微服务:

限流:令牌、漏斗。

网关:

服务注册发现:dubbo,可以使用nacos或者zookeeper作为服务注册中心,两者服务器存储位置不同,分别采用mysql和zk本身存储。nacos支持两种方式的注册中心,持久化和非持久化存储服务信息,zk是持久化。

负载均衡策略:

随机法:通过系统随机函数。

轮循法:将请求按顺序轮流分配到后台服务器上。

加权轮询法:不同的后台服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不一样。跟配置高、负载低的机器分配更高的权重,使其能处理更多的请求,而配置低、负载高的机器,则给其分配较低的权重,降低其系统负载,加权轮询很好的处理了这一问题,并将请求按照顺序且根据权重分配给后端。

源地址哈希法:采用源地址哈希法进行负载均衡,相同的IP客户端,如果服务器列表不变,将映射到同一个后台服务器进行访问。

最小连接数法:请求次数的均衡并不代表负载的均衡,由于后台服务器的配置不尽相同,对请求的处理有快有慢,它正是根据后端服务器当前的连接情况,动态的选取其中当前积压连接数最少的一台服务器来处理当前请求,尽可能的提高后台服务器利用率。


算法&数据结构:

数组(顺序存储)、链表(链式存储)、栈、队列、二叉树(m叉树,满二叉树,完全二叉树,二叉排序树,二叉搜索树,树的遍历等等),这是最重要的,了解这些数据结构的优缺点,如果有时间,把图也看了,包括DFS,BFS,普里姆算法和克鲁斯卡尔算法寻找图的最小生成树,拓扑排序,迪杰斯特拉算法等等。

排序算法:

插入排序,折半插入排序(利用折半(二分)查找,相对于插入排序比较次数少了,只是用顺序表),冒泡排序,选择排序,快速排序(分治思想),希尔排序(分治思想),堆排序


网络:

http服务器的原理:

1、创建一个ServerSocket,监听并绑定一个端口

2、一系列客户端来请求这个端口

3、服务器使用Accept,获得一个来自客户端的Socket连接对象

4、启动一个新线程处理连接

4.1、读Socket,得到字节流

4.2、解码协议,得到http请求对象

4.3、处理http请求,得到一个结果,封装成一个HttpResponse对象

4.4、编码协议,将结果序列化字节流 写Socket,将字节流发给客户端

5、继续循环步骤3

与http同一层的协议有哪些,与tcp同一层的呢:

http是应用层,同一层有ftp/telnet;与TCP同一层的有udp

http特点:

(1)支持客户端/服务器模式

http工作于客户端服务端的架构之上,浏览器作为客户端通过url向服务器及web服务器发送请求,web服务器根据接收到的请求向客户端发送响应信息。

(2)简单快速

客户端向服务器请求时,只需传送请求方法和路径,请求方法有post、get等,每种方法规定了客户端与服务端

(3)灵活

http允许传输任意类型的数据对象。

(4)无连接

每次连接只能处理一个请求,服务器处理完客户端的请求并收到客户端的应答后即断开。

(5)无状态

无状态是指协议对事务处理没有记忆能力,意味着如果后续处理需要前面的信息,则必须被重传,这可能导致每次连接的数据量增大,另一方面不需要前面信息时,它的应答就较快。

http和tcp的关系:

http协议是建立在TCP协议基础之上的,当浏览器需要从服务器获取网页数据的时候,会发出一次http请求。http会通过TCP建立起一个到服务器的连接通道,当本次请求需要的数据完毕后,http会立即将TCP连接断开,这个过程是很短的。

TCP是底层协议,定义的是数据传输和连接方式的规范。

HTTP是应用层协议,定义的是传输数据的内容的规范。

HTTP协议中的数据是利用TCP协议传输的,所以支持HTTP就一定支持TCP

首先说一下SYN的攻击原理:

在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。

第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。 完成三次握手,客户端与服务器开始传送数据.

如果用户与服务器发起连接请求只进行到第二次握手而不再响应服务器,服务器就会不停地等待用户的确认,如果过多这样的连接就会把服务器端的连接队列占满就会导致正常的用户无法建立连接。所以我们直接从SYN的连接上进行如下改动:

①可缩短 SYN Timeout时间。

②设置SYN Cookie,给每个请求连接的IP地址分配一个Cookie,如果短时间内收到同一个IP的重复SYN报文,则以后从这个IP地址来的包会被丢弃。

如果http流不关闭会怎样:

Http请求造成服务器服务端口大量CLOSE_WAIT,这个IO资源就会被一直占用,这样别人想用就没有办法用了,所以这回造成资源浪费。要关闭资源,最好写在finally中,否则如果出现一个异常,就关不掉了。

HTTP和HTTPS的区别:

一、传输信息安全性不同

1、http协议:是超文本传输协议,信息是明文传输。如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息。

2、https协议:是具有安全性的ssl加密传输协议,为浏览器和服务器之间的通信加密,确保数据传输的安全。

二、连接方式不同

1、http协议:http的连接很简单,是无状态的。

2、https协议:是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议。

三、端口不同

1、http协议:使用的端口是80。

2、https协议:使用的端口是443.

BIO、NIO、Netty:

BIO:一个socket连接对应一个线程,不是一个http请求对应一个线程,可能是多个请求对应一个线程。

BIO在socket客户端连接的时候和读取数据的时候会阻塞,所以一个客户端对应一个线程,所以BIO的缺点就是线程太多了。

NIO就是为了解决线程多的问题,NIO把两个阻塞的地方改为非阻塞,把服务端的socket注册到选择器selector里面,监听socket事件,当有客户端发来请求时,去处理。

BIO是主动监听,NIO是被动。

Netty是建立在NIO基础之上,Netty在NIO之上又提供了更高层次的抽象。

在Netty里面,Accept连接可以使用单独的线程池去处理,读写操作又是另外的线程池来处理。Netty他的api简单、性能高而且社区活跃(dubbo、rocketmq等都使用了它)

tcp udp区别:

TCP是面向连接的,UDP是面向无连接的

TCP是面向字节流的,UDP是基于数据报的

TCP保证数据正确性,UDP可能丢包

TCP保证数据顺序,UDP不保证

基于 UDP 的几个例子:

直播,直播对实时性的要求比较高,宁可丢包,也不要卡顿的,所以很多直播应用都基于 UDP 实现了自己的视频传输协议

实时游戏。游戏的特点也是实时性比较高,在这种情况下,采用自定义的可靠的 UDP 协议,自定义重传策略,能够把产生的延迟降到最低,减少网络问题对游戏造成的影响

session与cookie的区别:

cookie数据保存在客户端,session数据保存在服务器端。

Cookies是属于Session对象的一种。但有不同,Cookies不会占服务器资源,是存在客服端内存或者一个cookie的文本文件中;而“Session”则会占用服务器资源。我们一般认为cookie是不可靠的,session是可靠的。

java后端面试2021知识点大总结(超广范围全包含,随时更新)相关推荐

  1. java后端面试大全,java后端面试宝典

    文章目录 -2 flink -1 linux of view linux查看占用cup最高的10个进程的命令: 〇.分布式锁 & 分布式事务 0-1分布式锁--包含CAP理论模型 概述 分布式 ...

  2. 《Java 后端面试经》Java 基础篇

    <Java 后端面试经>专栏文章索引: <Java 后端面试经>Java 基础篇 <Java 后端面试经>Java EE 篇 <Java 后端面试经>数 ...

  3. Java面试笔试题大汇总一(最全+详细答案)

    Java面试笔试题大汇总二(最全+详细答案)https://www.jianshu.com/p/f5120f1b75be Java面试笔试题大汇总三(最全+详细答案)https://www.jians ...

  4. Java面试笔试题大汇总三(最全+详细答案)

    Java面试笔试题大汇总一(最全+详细答案):https://www.jianshu.com/p/73b6b3d35676 Java面试笔试题大汇总二(最全+详细答案)https://www.jian ...

  5. 三年半Java后端面试经历

    转载自  三年半Java后端面试经历 经过半年的沉淀,加上对MySQL,redis和分布式这块的补齐,终于开始重拾面试信心,再次出征. 鹅厂 面试职位:go后端开发工程师,接受从Java转语言 都知道 ...

  6. JAVA后端面试100 QA之第一篇

    转载自  JAVA后端面试100 Q&A之第一篇 1. synchronized和reentrantlock异同 相同点 都实现了多线程同步和内存可见性语义 都是可重入锁 不同点 实现机制不同 ...

  7. 深圳店小秘Java后端面试

    深圳店小秘Java后端面试 1.笔试 1.JDK.JVM.JRE.JIT是什么? JDK:Java Development Kit.是Java开发工具包,提供Java编程语言内库. JVM:Java ...

  8. 面试题:460道Java后端面试高频题答案版「模块九:Spring」

    Spring 写在前面 相信对于 Spring 这个模块,只要是做过 Java 开发的同学都是不陌生的,或多或少都使用过 Spring 体系的框架.对于平时自己做点小项目会写配置文件可能就行了,但是对 ...

  9. 《Java 后端面试经》微服务篇

    <Java 后端面试经>专栏文章索引: <Java 后端面试经>Java 基础篇 <Java 后端面试经>Java EE 篇 <Java 后端面试经>数 ...

最新文章

  1. 什么是脏读,不可重复读,幻读
  2. python爬携程_python爬虫-携程-eleven参数
  3. UI组件之ImageView及其子类(二)ImageButton ,ZoomButton
  4. python 解压js压缩的图片_20行Python代码,无损压缩千百张图片!Python有多强!就不用说了...
  5. mysql 工时统计_有如下数据,要统计1月3号工时不足8小时的员工姓名及总工时,该sql语句该怎么写(用的mysql哈)...
  6. 探究 Spring 的定时任务配置
  7. 我在 1969 年写代码
  8. vba 字体颜色_Excel填充单元格颜色和改变字体颜色,用VBA是怎样实现的呢?
  9. 记录一次安装gaussian09的经历
  10. 改进的自适应中值滤波算法 去除椒盐噪声 python 代码实现
  11. 职称计算机 将计算机broad_1下的e盘映射为k盘网络驱动器,职称计算机考试网络基础答案(1)...
  12. msfconsole使用手册
  13. php 检查货币类型_php 判断函数是否为费用类型(金额/货币:6.02)有小数点
  14. 石家庄地铁查询系统(双人项目)
  15. win7旗舰版激活提示错误代码0x80072EE2的最简单解决办法
  16. 那些年,磕磕碰碰的BUG
  17. 带你白嫖程序带宽限制命令Trickle
  18. 明日方舟公式计算机,明日方舟公开招募公式汇总
  19. 模板解析原理,脱裤子放屁
  20. Homography 估计单应性矩阵代码实现

热门文章

  1. mysql 日期格式化 yyyymmdd_mysql中格式化日期详解
  2. mysql select 时间格式_Mysql格式化日期
  3. 国内Java面试总是问StringBuffer,StringBuilder区别是啥?
  4. Attribute特性3——自定义特性AttributeUsage
  5. rbac php yii,YII RBAC
  6. HMM模型——隐含马尔科夫模型【详细分析+图】
  7. “小心驶得万年船”@【防滑eva海绵】
  8. hdu5693(区间dp)D Game
  9. python opencv压缩图片大小_使用OpenCV Python调整图像大小的最佳方法
  10. 运维 --- Nginx介绍和在CentOS/Redhat下安装