面试复习计划

复习计划

  1. Java多线程实现原理,准备一份讲演计划;
  2. Spring实现原理,准备一份讲演计划;
  3. 数据库索引创建与优化,实现原理与常用的优化方式,准备一份演讲稿;
  4. Java虚拟机中垃圾回收机制,和类加载机制中的双亲委派模型;

 

一、多线程

       对于Java多线程,首先需要理解的是其内存模型,在Java虚拟机规范中规定,所有的数据都存储在主内存中,而每个线程则有一个工作工作内存,线程需要进行数据处理时,都是先从主内存中读取数据到自己的工作内存,然后进行相应的操作,最后将操作结果赋值到主内存中。但是这里存在的问题在于,如果工作内存中的数据没有及时更新到主内存中,那么其他的线程从主内存中读取数据时,读取到的数据就是更新之前的数据,这样在多线程环境中就会造成数据的不一致问题。

       为了解决这两个问题,Java定义了两种类型的锁操作,一种是使用synchronized修饰的重量级锁,一种是使用volatile修饰的轻量级锁,严格的说volatile并不能算是锁,因为其只保证了对数据操作的可见性,而没有保证原子性,只不过Java通过使用CAS算法来将其作为一个锁来使用了。Java虚拟机规范关于锁规定了两个happens-before规则:①对于一段代码,其解锁操作一定发生在随后对其进行加锁的行为之后;②对于volatile修饰的变量,对其进行的写入操作一定发生在随后对其进行的读取操作之前。这两个happens-before规则其实根本意思就是表达了如果一个线程A在一段加锁的代码中进行了某些操作,然后释放了锁,此时另一个线程B获取到了锁,然后进行该线程的操作,那么Java虚拟机就能够保证A线程进行的操作一定都是发生在B线程之前,这样就能保证A对共享数据的操作结果是能够为B所见的,这也就是synchronized和volatile能够保证对其修饰的共享数据的可见性的原因。

        对于synchronized实现的锁,其是重量级的,对于被锁定的代码,每次只有一个线程能够访问,并且Java虚拟机能够保证执行结束的结果都已经刷新到了主内存中,然后另一个线程从主内存中读取数据时就是最新的了。对于volatile而言,其主要有两个特性,一个是可见性,一个是禁止了指令重排序。这里的禁止指令重排序指的是对于volatile变量的读取或写入操作之前的指令一定不会被重排序到读取或写入操作之后。需要注意的是volatile变量是不能保证原子性的,这里比较典型的就是对基本数据类型int,long和double数据的读取。现代的处理器主要有32位和64位两种,但即使是64位处理器,其也是被拆分为两个32位的操作来进行的,也就是说处理器处理数据的基本单元是32位,对应的也就是int类型的长度,而long和double是64位的,因而会被拆分为两个32位的单元来进行处理。总结来说,就是由于处理器的支持,对于一个int类型的变量,其读取和写入操作一定是原子的,而volatile则能够保证一个变量的可见性,那么如果一个int类型变量使用volatile修饰之后,其就同时具有了原子性,可见性和禁止指令重排序的特性。

       实际上,在java.concurrent包中的多线程工具类的核心实现原理就是对一个volatile修饰的int类型变量state的操作。其核心实现算法是CAS算法,也就是compare and set,该算法需要传入三个参数,一个需要操作的变量地址,一个当前线程认为的值,一个是当前线程想要将其更新为的值。比如对于Java里的ReentrantLock,每个线程在尝试获取锁时都是将当前线程认为的值设置为0,然后其要修改的目标值是1,这样主要一个线程获取到了锁,那么state就不会为0,对于其他的线程而言,其compare and set就始终不会成功,而对于获取了锁的线程,其在执行完锁定代码之后会释放锁,这个释放锁的过程就是将state的值重新设置成0,这样,其他的线程进行compare and set操作的时候就会有一个是成功的。实际上,ReentrantLock在进行锁竞争的时候并不是像上面说的这样一直都在无限for循环中一直进行CAS操作,而是在无限for循环中进行一次尝试,如果没获取到锁,其就会进入一个等待队列中,并且Java虚拟机就会将其搁置起来,以释放CPU资源,当正在使用锁的线程使用完锁之后,其会唤醒队列中第一个等待的线程,其还是会在无限for循环中再次使用CAS算法获取锁,而这一次是可以获取到的。这里关于ReentrantLock主要有两个特性:可重入性和公平性的设置。可重入性指的是同一个线程能否多次获取同一个锁,而公平性指的是线程在获取锁的时候与其他的线程是直接竞争还是按照尝试获取锁的时间顺序来依次获取锁。对于可重入性,其实现比较简单,就是判断当前线程如果是尝试再次获取锁,那么就将state值加1,也就是说记录锁的标志state的值记录了当前线程重复获取当前锁的次数,而最后当前线程获取了多少次锁,其就会释放多少次。对于公平性和非公平性,其实就是当一个线程尝试获取锁的时候是直接进入等待队列还是与当前正在尝试获取锁的线程进行竞争之后再进入等待队列。

       java.concurrent包中提供的主要类有ReentrantLock,CountDownLatch,CyclicBarrier,ThreadPoolExecutor,ForkJoinPool,ThreadLocal,FutureTask以及一些其他的一些队列。这里的ReentrantLock,CountDownLatch和CyclicBarrier的作用和实现比较简单,ReentrantLock的原理上面已经讲到了,而CountDownLatch就是设置了一个倒数的机制,其构造函数需要传入需要倒数的次数,首先主线程会执行await()方法,而其他的用于执行某些任务的线程在每次执行完任务之后就会调用CountDownLatch.countDown()方法,那么其就会倒数一次,直到最后所有的任务执行完成之后,倒数计数器的值为0,这个时候主线程就会被唤醒,然后继续执行后续操作,CountDownLatch主要的作用在于可以将多个任务进行并行操作,而任务执行完成之后再对任务的结果进行汇总。根据这里的讲解,其实CountDownLatch的实现原理也比较简单,就是在构造对象时设置state值为传入的值,然后每次执行countDown()方法时就使用CAS算法将state减一,直到最后一个线程将其更新为0之后由该线程唤醒主线程继续往下执行。对于CyclicBarrier,在构造函数中也会为其传入一个初始值,这个值也是直接设置到state中,其只提供了一个await()方法,每当一个线程执行了await()方法之后,如果state值还没有倒数到0,那么该线程就会进入等待状态,直到所有的线程都执行完各自的任务之后,最后一个线程就会唤醒所有等待的线程,这样就可以保证这些线程在同一起跑线上开始执行接下来的任务。关于CyclicBarrier很典型的一个用法是在进行游戏加载的时候,比如五个人协作的游戏,在进行游戏装载阶段,必须要等待五个人都加载完成之后才能开始进行游戏。

       对于ThreadPoolExecutor,ForkJoinPool,ThreadLocal和FutureTask,它们的实现相对复杂一些。ThreadPoolExecutor,本质上是一个线程池,其内部维护了一定数量的线程,这里可以主要讲解一下它的调度策略。在初始状态时,每当push一个任务进来时,线程池中是没有线程的,此时就会为其创建一个线程执行任务,当线程数达到核心线程数之后,再有任务进来时,如果没有可用的核心线程,那么就会将当前任务添加到任务队列中,当任务比较多,任务队列也填满了之后,如果设置的最大线程数大于核心线程数,那么就会继续创建线程执行任务,直到任务队列中存满了任务,并且所有的线程都在执行任务,这个时候就会有一个拒绝策略的问题,这里主要有四种策略:AbortPolicy,CallerRunsPolicy,DiscardPolicy和DiscardOldestPolicy。这四种策略的意思是拒绝当前任务,由调用方自己执行该任务,忽略队列中最后添加的任务而添加当前任务,忽略队列中最早添加的任务而添加当前任务。这里的调度策略中还需要说明的一点就是,对于核心线程与最大线程数的区别,对于核心线程,只要其创建了,那么线程池中始终是会维护这么多线程的,而对于最大线程数,只要多出来的线程没有任务可以执行,那么在一定时间之后其就会被销毁掉。对于ForkJoinPool,这个线程池的执行效率非常之高,因为其使用了工作窃取算法,其内部也是有一个线程池,但是其每个线程都维护了一个队列,不像ThreadPoolExecutor,其只维护了一个队列由多个线程进行竞争。对于ForkJoinPool,当前线程如果执行完了当前队列中的任务,那么其就会去其他的线程维护的队列中查看是否有未执行完的任务,如果有的话就会帮助该线程执行任务,而且当前线程偷取的任务并不是与目标线程进行竞争,而是目标线程在队列头获取任务,而窃取任务的线程只在队列尾部获取任务,只有在最后只剩下一个任务的时候两个线程才会出现竞争。ForkJoinPool非常适合那种有大量的任务,并且任务执行时间相对不长的场景使用。

       对于ThreadLocal对象,其主要的设计理念就是对于某些数据,其不需要进行共享,而只需要和当前线程绑定即可,对于这样的数据,也就不会存在多线程环境竞争的问题。在每个线程中都有一个ThreadLocalMap变量,当前线程使用ThreadLocal保存的数据都存储在该Map中,由于该Map是和当前线程绑定的,因而也就达到了将变量与当前线程进行绑定的目的。

       对于FutureTask,其使用比较简单,但是其设计理念是比较有趣的。其作用在于对于一些比较耗时的任务,在一开始时就将任务封装到FutureTask中,让其执行,然后主线程进行其他的动作,当主线程执行到必须要FutureTask的执行结果的位置时,再调用其get()方法获取结果,此时,如果FutureTask任务还未执行完,get()方法就会阻塞主线程,直到其执行完,如果FutureTask任务执行完成,那么get()方法就可以直接得到结果。从这里可以看出,FutureTask主要是用于将比较耗时的操作在前期就进行封装,从而达到一种并行执行的目的。

 

乐观锁:认为对数据的操作大部分都是读操作,在数据库中,每条数据都有一个版本号,每次对该数据的修改都会将版本号增一,如果一个线程尝试更新一个数据,那么它首先会读取这条数据,包括版本号,然后乐观的认为数据库在此期间没有修改过数据,从而拿着此版本号与数据库的版本号进行比较,如果版本号一致,则更新这条数据,如果不一致,则尝试再次读取并更新。乐观锁适用于读比较多,而写操作比较少的场景。

悲观锁:在每次进行数据读取并更新的过程中,都认为会有其他的线程会来修改该数据,因而在数据还未处理完成之前会一直对该数据添加独占锁。

 

 

 

二、Spring实现原理

       对于Spring,从整体上来讲,其主要分为两个部分:Spring初始化和请求处理。我这里以基于Tomcat的初始化过程来进行讲解,首先Tomcat是基于servlet规范来实现的,其有一个web.xml文件,在该文件中,我们一般会指定两个类:ContextLoaderListener和DispatcherServlet。这两个类分别实现了ServletContextListener接口和HttpServlet接口。这两个接口都是servlet规范中所提供的接口,它们的作用主要在于,ServletContextListener接口提供了contextInitialized()方法和contextDestroyed()方法,用于进行servlet上下文环境的初始化和销毁的,这里的contextInitialized()方法会在Tomcat容器启动的时候调用,而contextDestroyed()方法则是在其销毁的时候进行调用。Spring容器的启动就是在contextInitialized()方法中进行的。对于HttpServlet,其主要是用于处理请求的,其提供了doGet(),doPost()等一系列方法用于处理GET,POST等等请求。这里我们首先可以讲解一下Spring容器是如何启动的,在ContextLoaderListener的contextInitialized()方法开始调用时候,其首先会根据配置的contextConfigLocation参数指定的配置文件路径读取该配置文件,并且初始化一个XmlWebApplicationContext的类,很明显,这个类是实现了ApplicationContext接口的,这也是在Tomcat容器中Spring所启动所使用容器。在实例化了XmlWebApplicationContext之后,就会调用该类的refresh()方法,该方法就是用于初始化Spring容器的。在该方法中,Spring首先会读取配置文件中配置的bean,并将bean的配置信息使用一个BeanDefinition对象进行存储,然后注册到BeanDefinitionRegistry中。这里关于配置文件的读取,Spring是分了两种情况的,一种是基本标签的读取,在Spring中,基本标签只有四种:beans,bean,import和alias标签。对于使用这四种标签声明的bean,Spring会直接将其转换为一个BeanDefinition对象。而其他的标签,比如我们常用的mvc:annotation-driven,tx:annotation-driven等等,全部都属于自定义标签。Spring实现自定义标签的方式也比较简单,就是在提供的jar包的META-INF目录下有一个spring.handlers和spring.schemas文件,这个spring.handlers文件中存储了当前命名空间所对应的处理类的全路径名,这些类都实现了NamespaceHandler接口,而spring.schemas文件中则存储了当前命名空间所能使用的xml标签节点,最终Spring解析到这些自定义标签之后,首先会根据其命名空间找到其所对应的处理类,然后通过调用处理类的init()方法为每个命名空间的子标签注册一个实现了BeanDefinitionParser接口对象,最终通过调用该对象的parse()方法将这些自定义标签中的属性转换为一个BeanDefinition对象。

       在Spring解析完所有的标签之后,这里提供了一个切入点,即BeanFactoryPostProcessor,该接口中有一个postProcessBeanFactory()接口,Spring会获取当前容器中所有实现了BeanFactoryPostProcessor接口的对象,并且逐个调用该对象的postProcessBeanFactory()方法,从而实现对已经注册的BeanDefinition进行自定义的目的。

       在此之后,Spring就会遍历所有的BeanDefinition逐个进行实例化,如果存在bean的相互依赖问题,就会递归的进行初始化,实例化的方式就是通过反射进行。在进行bean实例化的过程中,Spring提供了一个非常重要的切入点,即BeanPostPorcessor,在接口有有两个方法:postProcessBeforeInitialization()和postProcessAfterInitialization()。这两个方法的主要目的分别在bean初始化之前和之后执行,以提供给用户在bean实例化之前和之后提供两个切入点,从而实现对bean的自定义。这里关于这两个方法比较典型的用法就是Spring实现aop和transaction的原理。

      在实例化完bean之后,剩下的主要是初始化MessageSource,ApplicationListener等bean的处理,至此Spring的初始化才算完成。

      对于DispatcherServlet,其主要是用于处理请求的,在DispatcherServlet中,其有一个init()方法,在该方法中,Spring会初始化用于请求处理的九大组件,其中相对比较重要的是HandlerMapping,HandlerAdapter,ViewResolver和HandlerExceptionResolver。这几个类而言,首先RequestMappingHandlerMapping主要是用于进行请求映射的,其会扫描当前容器中所有controller中注册的mapping,将其配置的数据,比如请求的url,method等信息封装到一个RequestMappingInfo对象中,当一个请求过来时,RequestMappingHandlerMapping会根据请求的url和method等信息遍历所有的RequestMappingInfo对象,并且找到最匹配的一个。在通过RequestMappingHandlerMapping找到最匹配的handler之后,就会通过HandlerAdapter就会对当前的handler进行适配,所谓适配的意思就是将请求url中的比如request param,path variable和request body等等数据适配成当前handler方法的参数类型。这个适配过程中,主要涉及到的一个接口是HandlerMethodArgumentResolver,在handler方法的请求参数上,一般每个参数都有一个标志位,比如@RequestParam,@PathVariable,@ReqeustBody,@ModelAttribute等等注解,每一个标志位的解析都对应有一个HandlerMethodArgumentResolver对象以实现将url参数转换为该标志位所描述的参数的数据。在所有的参数都进行处理之后,RequestMappingHandlerAdapter就会通过反射调用目标handler的方法。调用方法完成之后所有的数据都会通过ReturnValueArgumentResolver对象进行处理,对于返回值的处理,Spring也是和参数处理相似,比如对于视图类型就是直接返回ModelAndView对象即可,对于ajax请求,对应的handler就会使用@ResponseBody注解进行标注。通过这种标志位的方式Spring就可以判断具体是使用哪一个ReturnValueArgumentResolver处理当前的返回值。在处理完成之后,最终Spring会将所有视图类型的返回值封装为一个ModelAndView对象,如此RequestMappingHandlerAdapter就结束了其调用,然后将返回值交给ViewResolver处理,ViewResolver会通过一定的策略解析当前的viewName,并且得到具体需要渲染的视图的位置。在解析完成之后ViewResolver就会将需要渲染的视图和渲染视图所需要的Model数据传递给View对象用于进行视图的渲染,从而展示在前端页面。

       对于Spring AOP的实现,首先AOP的入口是一个自定义标签:aop:aspectj-autoproxy,Spring在解析标签的时候首先会找到META-INF目录下spring.handlers文件中对该命名空间进行的定义,然后获取到解析该标签的类,对AOP进行处理的是AopNamespaceHandler,其会将aspectj-autoproxy这个名称对应的标签处理交付给一个实现了BeanDefinitionParser接口的类处理,解析的最终结果就是注册了一个AnnotationAwareAspectjAutoProxyCreator的BeanDefinition。这里的AnnotationAwareAspectjAutoProxyCreator实现了BeanPostProcessor接口,实现该接口的类是具有能够在一个bean创建之前和创建之后对目标bean进行一定的处理的。这里AOP主要就是在BeanPostProcessor.postProcessAfterInitialization()方法中进行处理的,也就是说对目标bean的代码实际就是在目标bean生成之后将其使用AOP进行封装,然后在Spring进行bean注入的时候使用封装后的bean。这里AOP在处理目标bean的时候,首先Spring会使用两种方式获取当前容器中的切面,第一种是获取当前容器中实现了Advisor接口的类,第二种是扫描容器中所有的bean,判断其是否使用@Aspect注解进行了标注,如果是,则说明其是一个切面。对于第一种,只需要直接使用即可,因为最终的切面信息都会被封装为一个Advisor对象。对于第二种,也就是使用@Aspectj标注的类,Spring首先会对其进行解析,解析主要分为两个方面:一方面是对@Aspectj注解中的参数进行解析,另一方面是对该注解标注的类中的方法上的切面注解,比如@Before,@After,@Around等注解进行解析。这里的解析的过程是使用递归进行的,因为在这些切面注解中可以使用&&,||,!和()等逻辑运算符。最终解析的结果就是Spring会生成一个Advisor对象,该对象中有两个属性,一个Advice和一个Pointcut对象,这里的Advice对象封装了当前需要织入的切面逻辑,而Pointcut对象则封装了当前切面的过滤逻辑,也就是当前切面需要对哪些bean进行环绕。这里Pointcut对象中主要封装了两个属性:ClassFilter和MethodMatcher,它们分别对应了当前切面中类的@Aspectj注解后的参数和方法上使用@Before,@After,@Around等注解标注的参数。解析完成之后,Spring首先会遍历解析得到的所有切面Advisor对象,通过Advisor对象的Pointcut属性来判断当前bean是否为需要进行代理的bean,如果不需要,则不织入当前切面逻辑,最终会得到一个需要织入的Advisor的列表。进行切面逻辑的织入方式比较简单,主要的织入方式有两种:Cglib代理和Jdk代理。这里Cglib代理是通过生成目标类的子类字节码的方式来实现代理的,而Jdk代理则是通过生成一个与目标类实现同一个接口的类的方式来进行代理。Spring默认使用的是Jdk代理,这里两个代理的方式的用法主要是Cglib代理是使用一个Enhancer类来创建代理类,而需要织入的切面逻辑需要封装到一个实现了Callback接口的类中。Jdk代理则是需要将要织入的代理逻辑封装到一个实现了InvocationHandler接口的类中,然后通过Proxy这个类生成代理类的字节码,这里代理了类的字节码继承了Proxy类,并且实现了所有需要代理的接口,而所有的接口方法则统一委托给出入的InvocationHandler对象来处理。这也就是为什么我们在InvocationHandler接口中进行逻辑织入时需要使用方法名进行判断当前是调用的哪个方法的原因。

       对于Spring事务的实现,其底层还是使用AOP来实现的,整个事务织入过程可以分为两步,一是对事织入的入口,二是事务织入的逻辑。首先我们在声明一个事务时使用的是tx:annotation-driven,Spring会到META-INF目录下的spring.handlers文件中查找该命名空间对应NamespaceHandler,然后找到annotation-driven这个子名称对应的处理类,最终对于事务标签各个属性的解析之后,Spring注册了三个BeanDefinition到Spring容器中,这个三个分别是BeanFactoryTransactionAttributeSourceAdvisor,TransactionAttributeSourcePointcut和TransactionInterceptor。它们的从属关系是,TransactionAttributeSourcePointcut和TransactionInterceptor是BeanFactoryTransactionAttributeSourceAdvisor的两个属性。在Spring AOP中,最终所有的切面都会被封装为一个Advisor对象,Advisor对象中则封装了一个Advice和一个Pointcut对象,这里的Advice中保存有当前需要织入的切面逻辑,而Pointcut对象中则封装了对于目标bean进行过滤的ClassFilter和MethodMatcher对象。对应的这里的事物注册的三个类也是和这里的AOP所需的三个角色完全对应的,BeanFactoryTransactionAttributeSourceAdvisor实现了Advisor接口,而TransactionInterceptor实现了Advice接口,TransactionAttributeSourcePointcut接口则实现了Pointcut接口。之前我们讲到,在Spring进行切面逻辑选择时,首先会查找当前容器中所有实现了Advisor接口的类,将其作为一个切面进行织入,而这里的BeanFactoryTransactionAttributeSourceAdvisor就是这样一个实现了Advisor接口的类,因而其是进行事务织入的入口。对于事务的逻辑的织入,Spring主要还是借助于数据库事务的支持,其本身是没有实现事务的。这里关于事务主要需要说明的两个点是事务的隔离级别和传播性。事务的隔离级别主要有四种:READ UNCOMMITTED,READ COMMITTED,REPEATABLE READ和SERIALIZABLE。这四个隔离级别的区别是READ UNCOMMITTED会读取到其他事务没有提交的内容,这就是脏读;READ COMMITTED只会读取到其他事务提交的内容,其解决了脏读的问题,但是却存在不可重复读的问题,也就是比如在同一个事务中,首先读取了一条记录,但是后来再次读取该记录时候却该记录已经发生了改变,因为其他的事务修改了该记录;REAPEATALBE READ也即可重复读,其解决了脏读和不可重复读的问题,但是理论上其没有解决幻读的问题,所谓的幻读就是在一个事务中查询某条记录,发现其不存在,然后进行插入,此时却报错提示已经存在了该记录。这就是幻读。实际上,Mysql在数据库层面对REPEATABLE READ使用了GAP锁来解决这个问题,也就是在一个事务中读取某条记录时,会同步的将这条记录相邻的间隙给锁定,比如查询条件为 id > 3,查询结果为空,那么在这个事务还未结束之前,MySQL会将id > 3这个间隙给锁定,从而防止其他的事务在此期间往此间隙中插入数据。也就是说如果数据库使用的是Mysql,那么其也已经解决了幻读的问题,从而完全实现了数据库的ACID特性。对于数据库的传播性,其主要有REQUIRED,SUPPORTS,MANDATORY,REQUIRES_NEW,NOT_SUPPORTED,NEVER,NESTED。顾名思义,这些传播特性主要的区别在于REQUIRED:当前方法需要一个事务,如果已经存在一个事务,则复用该事务,如果不存在,则重新创建一个;SUPPORTS:当前存在事务则复用该事务,当前不存在事务,则不使用事务;MANDATORY:强制当前必须存在一个事务,并且自己不会创建事务,如果不存在则抛出异常;REQUIRES_NEW:无论当前是否存在事务,都重新创建一个事务;NOT_SUPPORTED:要求当前不能存在事务,如果存在,则挂起该事务,然后执行当前方法,执行完成之后再开始该事务;NEVER:强制当前不能存在事务,如果存在,则抛出异常;NESTED:使用保存点的方式支持嵌套事务,即父子事务,父子事务有一个特点是,如果子事务抛出异常,父子事务都会回滚,如果是父事务抛出异常,而子事务已经完成,则只回滚父事务。

 

 

三、数据库索引创建与优化

       关于数据库索引创建与优化,首先需要明确的一点是,Mysql是一种插件式的数据存储服务,MySQL只提供了一个server,而其实际存储数据的工作是各个插件来实现的,这种形式的数据存储方式会导致一个问题就是MySQL如果要对数据进行过滤,那么就得把可能符合条件的数据从存储引擎中拉到MySQL server中,然后遍历这些数据以获取最终的数据,这会产生一个很大的问题就是,如果目标数据量非常大,那么从存储引擎中拉取的数据量将会非常大。数据库索引的设计其实最终目的就是为了在MySQL从存储引擎拉取数据之前先通过索引进行一部分的过滤,减少需要扫描的结果集大小,从而提升查询的效率。

       对于索引,从存储数据的形式上来讲,主要有两种,一种是聚簇索引,另一种是二级索引,这两种索引都是通过B+树来组织的。对于聚簇索引,这里可以这么理解,MySQL在将数据存储到磁盘时,数据在磁盘是有一个顺序的,那么这里的顺序就是使用聚簇索引来组织的,MySQL一般使用主键id作为聚簇索引。也就是说磁盘上的数据存储的数据就是主键id的顺序,而索引B+树的叶节点上存储了当前数据在磁盘的地址值。对于二级索引,数据库中所有除了聚簇索引以外的索引都是二级索引,二级索引与聚簇索引最大的区别就是二级索引的叶节点存储的是聚簇索引的主键id值。在使用二级索引进行查询的时候,首先通过查询字段定位到二级索引的一部分叶节点,获取对应的主键id,然后通过id在聚簇索引中查询对应的数据。

      常用的二级索引有单列索引,联合索引,字符串前缀索引和hash索引。

      这里单列索引就是简单的为某个列建一个索引,单列索引主要需要注意的问题是字段的选择性,如果一个字段的选择性比较差,那么以该字段建立索引其实意义不大,因为根据索引过滤之后,MySQL还是需要在磁盘上拉取大量的数据。不过单列索引有一个好处就是可以进行索引合并扫描,比如查询条件是A or B,这个时候为A和B两列分别建立一个单列索引,MySQL就会用到这两个索引,然后将过滤后的结果进行合并,再到磁盘上拉取数据。

       对于联合索引,这是使用最多的一个索引,其优点主要有四个:①如果查询条件是等值条件,那么只要这些等值条件与联合索引的前缀相匹配,那么就可以使用到索引进行过滤;②如果查询条件既有等值条件,也有非等值条件,那么如果联合索引有覆盖非等值条件的字段,MySQL就可以使用到索引覆盖扫描,也就是对于非等值条件,虽然不能使用B+树排序的特性,但是也可以直接从索引中进行数据扫描,而不用回到磁盘进行数据拉取;③如果查询条件中有group by,order by,或者distinct等条件,这个时候如果group by,order by或distinct后的字段是联合索引的最后一个字段,那么MySQL就可以不用再对数据进行这之类的处理了,因为在索引中数据就天然的已经聚合,并且排序了;④如果查询字段不多,那么建立一个联合索引,将查询条件和字段都放到索引里,这样所需要的数据就都在索引里了,从而可以省略回磁盘读取数据的过程了。在使用联合索引的时候,有一个问题需要注意就是,联合索引是有一个最左前缀匹配原则的,比如条件的字段是A和B,而联合索引是(A,B,C),由于索引的组织顺序是按照先由A排序,当A相等时再按照B排序,B相等时再按照C排序,这样我们如果只使用A和B两个字段进行查询,也是能够使用到该联合索引的,这里如果使用A和C字段进行查询也能使用该索引,但只能首先根据A字段进行过滤,然后在过滤后的索引中全量扫描C字段,这样虽然效率没有使用前缀索引高,但是能够使用索引覆盖扫描,而不用回磁盘读取数据。

       对于字符串前缀索引,就是对于某些字符串字段比较长,并且前缀选择性比较高的情况来创建的一个索引,MySQL是内在支持这种索引的,原理就是取该字段的某一个前缀来组成一个索引。这种索引存在两个问题:①前缀索引只能进行等值条件查询,无法像联合索引一样对group by,distinct和order by等进行支持;②前缀长度的选择需要通过计算字段的选择性来决定,也就是分别取前缀为2,3,4,5等长度,看其选择性与全字段建索引的选择性是否一致。

       对于hash索引,MySQL内部是不支持的,不过我们可以通过冗余字段的方式来创建hash索引,比如对于一个很长的字符串字段,我们经常需要对该字段进行单点查询,那么这个时候直接为该字段建立前缀索引可能会导致索引片厚度太大,我们就可以新建一个列,用于存储这个字符串字段值得hash值,因为都是整型值,我们这个时候就可以为这个新建的字段创建索引了。那么查询的时候我们首先根据查询条件按照同一种hash方式将查询的数据映射为一个hash值,然后通过新建的hash字段定位到这个hash值所对应的数据记录,然后逐条比较其与我们需要的结果是否一致。

 

四、垃圾回收机制

       垃圾回收算法:

①引用计数算法:每一块对象内存都保存了一个计数器,用来记录当前对象被引用的次数,引用计数算法就是当计数器的值为0的时候就说明当前内存是没有被任何对象所引用的,那么就可以被回收了,但是引用计数算法存在的最大问题是无法解决循环依赖的问题,比如A引用了B,而B也引用了A,那么它们的计数器始终都不会为0,但它们整体上是没有被外部引用的,因而应该是需要被回收的;

②复制算法:复制算法将内存区域分为两部分,每次只使用其中一部分,当被使用的一部分占满了之后就对其进行回收,将其中还存活的对象转移到另一部分中,并且将原来那一部分完全清除即可,复制算法的优点在于如果每次复制时存活的对象比较少,那么其效率较高,如果存活的对象比较多,那么效率就比较低;

③标记清除算法:标记清除算法主要分为两阶段,标记阶段和清除阶段,在标记阶段,Java虚拟机会将当前内存区域中所有还存活的对象进行标记,清除阶段则是将未被标记的对象进行清除,标记清除算法的缺点是清除对象之后还存活的对象在内存中各处都有分布,这会导致产生很多内存碎片,而不利于后续大对象的分配;

④标记压缩算法:标记压缩算法基本步骤与标记清除算法类似,只是其多了一个压缩阶段,就是在清除之后,将还存活的对象统一移动到内存区域的起始位置,移动完成之后剩下的位置就都是可用的;

⑤分代算法:分代算法的基本思想是根据对象的特点将内存区域分为新生代和老年代两个区域,新生代的对象存活时间较短,老年代的对象存活时间较长,这样就可以根据其特点在新生代和老年代分别使用不同的垃圾回收算法,比如Java虚拟机在新生代使用复制算法,老年代使用的标记压缩算法;

⑥分区算法:分区算法的基本思想是将内存区域分为多个区,每次进行垃圾回收时可以只回收其中几个区域,而另外的区域则可正常使用,这种算法的优点在于在进行垃圾回收的时候能够大幅度的减少虚拟机的停顿时间。

 

       垃圾回收器:①Serial垃圾回收器:串行回收器主要是指由一个单线程使用复制算法进行垃圾回收;

②ParNew回收器:ParNew GC主要是将串行的垃圾回收修改为了并行的;

③ParallelGC:Parallel GC是一个关注吞吐量的垃圾回收器,它有两个参数,主要作用是设置每次垃圾回收的最大停顿时间和设置垃圾回收时间所占用的总运行时间的比例;

④ParallelOldGC:ParallelOldGC与ParallelGC一样,是一个关注吞吐量的垃圾回收器,不过它主要用于老年代的GC;

⑤CMS:CMS垃圾回收器是一个老年代的并行回收器,它主要使用的是标记清除算法来进行老年代的垃圾回收;

⑥G1GC:G1GC也是一个并行垃圾回收器,它主要使用的是分区算法进行垃圾回收。

 

 

       双亲委派模型:启动类加载器主要加载系统的核心类,扩展类加载器主要加载jre的ext目录下的类,应用程序类加载器则是加载运行的应用程序中的类。双亲委派模型就是这三个加载器以组合的方式进行自顶而下的类加载,比如在加载一个类的时候,首先是通过启动类加载器加载,如果启动类加载器中没有,则通过扩展类加载器进行加载,如果扩展类加载器中没有,则通过应用程序类加载器加载。

 

五、Redis

①Redis有六种数据结构:sds,dict,list,zskiplist,intset,ziplist。对应的操作命令是:set,hset,lpush,zadd,sadd。

②Redis控制数据过期时间的命令是:expire,单位为秒;pexpire单位为毫秒。

③Redis的lru算法有两种策略:a. 在获取一个键时,如果发现其已经过期,那么直接将其删除,并返回空;b. 每秒进行10次抽样检查,每次取20个键,删除其中的过期键,如果抽样的结果中有超过25%的键过期,那么就继续抽样删除。

④Redis数据持久化的方式有两种:rdb和aop。rdb是定期进行存储的,而aop则是每次执行命令时将执行的命令存储到该文件中。

⑤Redis的zadd命令的时间复杂度是log N。

 

 

六、HTTP协议

①HTTP状态码:

    100~199信息类状态码,100表示服务器收到了客户端的请求,请客户端继续;

     200~299成功类状态码,200表示服务器已经正常接收到了请求,并处理完成;201表示服务器已正常接收请求,并且正在处理,至于处理完成没有暂时不知;

     300~399重定向类状态码,300表示客户端请求的URL包含有多个资源,返回这个状态码时会带有一个选项列表,客户端可以根据选项列表来选择具体使用哪个资源;301表示当前资源已经转移,响应中的Location首部中包含有资源新的地址;302表示当前资源已经转移,不过只是临时转移,将来还是可以使用老的资源,新的临时资源地址在Location首部中有保存;

     400~499客户端错误状态码,400表示客户端发送了一个错误的请求;401表示客户端当前未被授权;403表示当前请求被服务器拒绝了,拒绝原因服务器可在响应的主体部分中选择性的告知;404表示请求的URL不存在;405表示请求的方法不支持,在响应的Allow首部中将包含当前url所支持的方法;

     500~599服务器错误,500表示当前服务器发生了未知错误。

转载于:https://my.oschina.net/zhangxufeng/blog/2392968

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值