LinkedList和ArrayList的差别主要来自于Array和LinkedList数据结构的不同。ArrayList是基于数组实现的,LinkedList是基于双链表实现的。另外LinkedList类不仅是List接口的实现类,可以根据索引来随机访问集合中的元素,除此之外,LinkedList还实现了Deque接口,Deque接口是Queue接口的子接口,它代表一个双向队列,因此LinkedList可以作为双向对列,栈(可以参见Deque提供的接口方法)和List集合使用,功能强大。
因为Array是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的,可以直接返回数组中index位置的元素,因此在随机访问集合元素上有较好的性能。Array获取数据的时间复杂度是O(1),但是要插入、删除数据却是开销很大的,因为这需要移动数组中插入位置之后的的所有元素。
相对于ArrayList,LinkedList的随机访问集合元素时性能较差,因为需要在双向列表中找到要index的位置,再返回;但在插入,删除操作是更快的。因为LinkedList不像ArrayList一样,不需要改变数组的大小,也不需要在数组装满的时候要将所有的数据重新装入一个新的数组,这是ArrayList最坏的一种情况,时间复杂度是O(n),而LinkedList中插入或删除的时间复杂度仅为O(1)。ArrayList在插入数据时还需要更新索引(除了插入数组的尾部)。
LinkedList需要更多的内存,因为ArrayList的每个索引的位置是实际的数据,而LinkedList中的每个节点中存储的是实际的数据和前后节点的位置。
使用场景:
(1)如果应用程序对数据有较多的随机访问,ArrayList对象要优于LinkedList对象;
( 2 ) 如果应用程序有更多的插入或者删除操作,较少的数据读取,LinkedList对象要优于ArrayList对象;
(3)不过ArrayList的插入,删除操作也不一定比LinkedList慢,如果在List靠近末尾的地方插入,那么ArrayList只需要移动较少的数据,而LinkedList则需要一直查找到列表尾部,反而耗费较多时间,这时ArrayList就比LinkedList要快。
位运算效率非常高
。这里主要说底层原理:
将Java代码
private static volatile Singleton instance = new Singleton();
转变为汇编代码,如下。
0x01a3de1d: movb $0*0,0*1104800(%esi);0x01a3de24: lock addl $0*0,(%esp);
可以发现转变为的汇编代码是有lock前缀的,Lock前缀的指令在多核处理器下会引发两件事情。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会出现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。
volatile的两条实现原则:
给出一个Java内存泄漏的典型例子,
ArrayList list = new ArrayList();
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
内存泄漏的根本原因
内存泄漏的根本原因在于生命周期长的对象持有了生命周期短的对象的引用
1、静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
2、各种连接,如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
3、变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
4、内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
5、改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露
JVM垃圾回收的根对象的范围有以下几种:
(1)虚拟机(JVM)栈中引用对象
(2)方法区中的类静态属性引用对象
(3)方法区中常量引用的对象(final 的常量值)
(4)本地方法栈JNI的引用对象
如果老年代也放不下,会发生full gc。。。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )
没有找到确切的答案,不过从下面看应该是逻辑分区:
根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的。
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。
通常我们常用的就只有泛型类,一个最普通的泛型类:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}
public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}
又或者写链表时,每个node我们不知道他的值是int还是string还是什么,就可以定义为node,然后在使用/调用时传入具体的类型(类型实参)。
什么是泛型?为什么要使用泛型?
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
随着JDK8的到来,JVM不再有PermGen。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。
元空间和永久代最大的区别是元空间并不在虚拟机中,而是使用本地内存。
这样可以提升对元数据的处理,提升gc效率。
答案链接
如果Metaspace的空间占用达到了设定的最大值(可以通过-XX:MetaspaceSize设定),那么就会触发GC来收集死亡对象和类的加载器。
应该都不是,因为元空间既不是新生代也不是老年代。。。(不确定)
java.util.concurrent,JUC是JDK5才引入的并发类库。它的基础就是AbstractQueuedSynchronizer抽象类,Lock,CountDownLatch等的基础就是该类,而该类又用到了CAS操作。常用类如下:
JUC的atomic包下运用了CAS的AtomicBoolean、AtomicInteger、AtomicReference等原子变量类
JUC的locks包下的AbstractQueuedSynchronizer(AQS)以及使用AQS的ReentantLock(显式锁)、ReentrantReadWriteLock
附:运用了AQS的类还有:Semaphore、CountDownLatch、ReentantLock(显式锁)、ReentrantReadWriteLock
JUC下的一些同步工具类:CountDownLatch(闭锁)、Semaphore(信号量)、CyclicBarrier(栅栏)、FutureTask
JUC下的一些并发容器类:ConcurrentHashMap、CopyOnWriteArrayList
JUC下的一些Executor框架的相关类: 线程池的工厂类->Executors 线程池的实现类->ThreadPoolExecutor/ForkJoinPool
JUC下的一些阻塞队列实现类:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue
附:ForkJoinPool:使用work-stealing的工作方式运行
阻塞队列是对普通队列的一种扩展,在普通队列功能上增加了一些额外功能。
普通队列其实主要就是入队、出队操作。阻塞队列接口(BlockingQueue)继承自普通队列。
其实主要在Queue基础上增加了阻塞的入队(put())和出队(take())操作,即当队列已满时调用put()入队时,当前线程会阻塞,直到队列有空间时才会继续入队;当队列为空时,调用take()出队操作时,当前线程会阻塞,直到队列中有元素时才会继续出队。这就是阻塞队列的核心。
Java阻塞队列-BlockingQueue介绍及实现原理
【图解JDK源码】BlockingQueue的基本原理
可以通过反射获取对象的类的getDeclaredField
和getDeclaredMethod
方法访问类的私有属性、方法,还可以重新设置私有属性的值,调用私有方法。如下面的field1 = e.getClass().getDeclaredField("field1");
和Method method1 = e.getClass().getDeclaredMethod("fun1");
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
//Exam.java
class Exam{
private String field1="私有属性";
public String field2="公有属性";
public void fun1(){
System.out.println("fun1:这是一个public访问权限方法");
}
private void fun2(){
System.out.println("fun2:这是一个private访问权限方法");
}
private void fun3(String arg){
System.out.println("fun3:这是一个private访问权限且带参数的方法,参数为:"+arg);
}
}
public class Test02 {
public static void main(String args[]){
Exam e=new Exam();
try {
field1 = e.getClass().getDeclaredField("field1");
field2 = e.getClass().getDeclaredField("field2");
field1.setAccessible(true);
System.out.println("field1: "+field1.get(e));
field1.set(e,"重新设置一个field1值");
System.out.println("field1: "+field1.get(e));
System.out.println("field2: "+field2.get(e));
field2.set(e,"重新设置一个field2值");
System.out.println("field2: "+field2.get(e));
} catch (NoSuchFieldException e1) {
e1.printStackTrace();
}catch (IllegalArgumentException e1) {
e1.printStackTrace();
} catch (IllegalAccessException e1) {
e1.printStackTrace();
}
try {
Method method1 = e.getClass().getDeclaredMethod("fun1");
method1.invoke(e);
Method method2 = e.getClass().getDeclaredMethod("fun2");
method2.setAccessible(true);
method2.invoke(e);
Method method3 = e.getClass().getDeclaredMethod("fun3",String.class);
method3.setAccessible(true);
method3.invoke(e,"fun3的参数");
} catch (NoSuchMethodException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (SecurityException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}catch (IllegalAccessException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (IllegalArgumentException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (InvocationTargetException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
先用 top 命令,找到 java 进程的 pid:
如 pid 为 1000;
再用 top -H -p 1000 命令查看在这个进程中,消耗 cpu 最多 的线程,如 1003;
最后使用 jstack 1000 > dump_file 把这个进程的堆栈信息 dump 到文件中,
打开 dump_file,找到 id 为1003的线程(要转化为16进制),就能发现是哪个方法占用了 cpu,分析自己的代码,
改正 bug!
答案链接
hibernate:
是一个标准的ORM框架(对象关系映射)。入门门槛较高,不需要程序写sql,sql语句自动生成。对sql语句的优化修改比较困难。
应用场景:
适用于需求变化不多的中小型项目,比如后台管理系统,ERP,ORM,OA。
mybatis:
专注sql本身,需要程序员自己编写sql语句,sql语句修改优化比较方便。mybatis是一个不完全的ORM框架,虽然程序员自己写sql,mybatis也可以实现映射(输入映射,输出映射)。
应用场景:
适用于需求变化较多的项目,比如:互联网项目
@Component在类上使用,表明这个类是个组件类,需要Spring为这个类创建Bean。@Bean注解使用在方法上,告诉Spring这个方法将会返回一个Bean对象,需要把返回的对象注册到Spring应用的上下文中。
Spring在初始化过程中要做的事情很多,下面我们就根据ClassPathXmlApplicationContext初始化看看我们的应用走了哪些步骤,我用debug模式下一步步来展现初始化过程。
首先我们看一下ClassPathXmlApplicationContext类的继承关系
ClassPathXmlApplicationContext
AbstractXmlApplicationContext
AbstractRefreshableConfigApplicationContext
AbstractRefreshableApplicationContext
AbstractApplicationContext
DefaultResourceLoader
I ResourceLoad
ApplicationContext context = new ClassPathXmlApplicationContext("conf/applicationContext.xml");
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
throws BeansException {
super(parent);
setConfigLocations(configLocations);
if (refresh) {
refresh();
}
}
初始化应用上下文,使用ClassPathXmlApplicationContext类新建实例,初始化这个类,我们可以看到使用super方法初始化了父类,上面继承树中所有的类都初始化了;然后把传送的配置文件目录设置为配置文件;调用refresh方法,这个方法是AbstractApplicationContext类实现的方法。
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
//准备上下文的刷新,
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
//得到新的Bean工厂,应用上下文加载bean就是在这里面实现的
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
//准备bean工厂用在上下文中
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
//允许子类上下问处理bean工厂
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
//请求工厂处理器作为beans注册在上下文
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
//注册bean处理器拦截bean创建
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
//初始化上下文中消息源
initMessageSource();
// Initialize event multicaster for this context.
//初始化上下文中事件广播
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
//初始化其他具体bean
onRefresh();
// Check for listener beans and register them.
//检查监听bean并注册
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
//实例化未初始化单例
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
//最后一步发布相应事件
finishRefresh();
}
obtainFreshBeanFactory方法把配置文件中的bean加载到容器中。调用AbstractRefreshableApplicationContext的refreshBeanFactory方法,然后调用loadBeanDefinitioans(beanFactory)方法,这个方法的实现类是XmlWebApplicationContext的loadBeanDefinitions方法,方法调用Reader中的loadBeanDefinitions(configLocations)把配置文件传入Reader。
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
//这里定义了Reader来读取Bean,在loadBeanDifinitions方法中传入reader
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
......
// then proceed with actually loading the bean definitions.
initBeanDefinitionReader(beanDefinitionReader);
loadBeanDefinitions(beanDefinitionReader);
}
XmlBeanDefinitionReader中调用AbstractBeanDefinition中的loadBeanDefinitions(location)方法。把配置文件读取成Resource后,调用XmlBeanDefinitionReader中loadBeanDefinitions(Resource)方法,在方法中调用doLoadBeanDefinitions(InputSource inputSource, Resource resource)传入参数是文件流和Resource,方法内调用registerBeanDefinitions(Document doc,Resource resource)方法
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
documentReader.setEnvironment(this.getEnvironment());
int countBefore = getRegistry().getBeanDefinitionCount();
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}
在DocumentReader调用注册bean方法中进行注册,调用DefaultBeanDefinitionDocumentReader的doRegisterBeanDefinitions方法,在parseBeanDefinitions方法中调用parseDefaultElement(ele, delegate);方法,判断如果获取到的是个bean的话执行,processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate)方法最后在DefaultListableBeanFactory类中调用registerBeanDefinition进行注册,注册的容器是this.beanDefinitionMap.put(beanName, beanDefinition);,这个是初始化的CurrentHashMap类。
容器的初始化以及Bean的注册就完成了。
原博链接:https://www.jianshu.com/p/a569aae8b722
https://www.processon.com/special/template/5c9c2cd1e4b08cb4bb00691f
ClassPathXmlApplication 调用 它的父类 AbstractApplicationContext 的 refresh 方法,refresh里调用了obtainFreshBeanFactory方法,obtainFreshBeanFactory调用了 getBeanFactory 方法(这是一个抽象方法,这里用的他的实现类是AbstractRefreshableApplicationContext),getBeanFactory返回了一个beanFactory对象,它是DefaultListableBeanFactory类的实例,使用xmlbeandefinitionreader 将配置文件转换为 document 后,通过 parseBeanDefinition 解析出一个个BeanDefinition 通过 registerBeanDefinition 注册进 beanfactory中(发生在返回beanFactory之前),就是构造好一个beanFactory 在上面的getBeanFactory中返回。
启动流程主要分为三个部分,第一部分进行SpringApplication的初始化模块,配置一些基本的环境变量、资源、构造器、监听器,第二部分实现了应用具体的启动方案,包括启动流程的监听模块、加载配置环境模块、及核心的创建上下文环境模块,第三部分是自动化配置模块,该模块作为springboot自动配置核心,在后面的分析中会详细讨论。在下面的启动程序中我们会串联起结构中的主要功能。
启动:
每个SpringBoot程序都有一个主入口,也就是main方法,main里面调用SpringApplication.run()启动整个spring-boot程序,该方法所在类需要使用**@SpringBootApplication注解**,以及@ImportResource注解(if need),@SpringBootApplication包括三个注解,功能如下:
@EnableAutoConfiguration:SpringBoot根据应用所声明的依赖来对Spring框架进行自动配置
@SpringBootConfiguration(内部为@Configuration):被标注的类等于在spring的XML配置文件中(applicationContext.xml),装配所有bean事务,提供了一个spring的上下文环境
@ComponentScan:组件扫描,可自动发现和装配Bean,默认扫描SpringApplication的run方法里的Booter.class所在的包路径下文件,所以最好将该启动类放到根包路径下
首先进入run方法,run方法中去创建了一个SpringApplication实例,在该构造方法内,我们可以发现其调用了一个初始化的initialize方法,这里主要是为SpringApplication对象赋一些初值。构造函数执行完毕后,我们回到run方法,
该方法中实现了如下几个关键步骤:
创建了应用的监听器SpringApplicationRunListeners并开始监听
加载SpringBoot配置环境(ConfigurableEnvironment),如果是通过web容器发布,会加载StandardEnvironment,其最终也是继承了ConfigurableEnvironment,类图如下
可以看出,*Environment最终都实现了PropertyResolver接口,我们平时通过environment对象获取配置文件中指定Key对应的value方法时,就是调用了propertyResolver接口的getProperty方法
配置环境(Environment)加入到监听器对象中(SpringApplicationRunListeners)
创建run方法的返回对象:ConfigurableApplicationContext(应用配置上下文),我们可以看一下创建方法:
方法会先获取显式设置的应用上下文(applicationContextClass),如果不存在,再加载默认的环境配置(通过是否是web environment判断),默认选择AnnotationConfigApplicationContext注解上下文(通过扫描所有注解类来加载bean),最后通过BeanUtils实例化上下文对象,并返回
回到run方法内,prepareContext方法将listeners、environment、applicationArguments、banner等重要组件与上下文对象关联
接下来的refreshContext(context)方法(初始化方法如下)将是实现spring-boot-starter-*(mybatis、redis等)自动化配置的关键,包括spring.factories的加载,bean的实例化等核心工作。
配置结束后,Springboot做了一些基本的收尾工作,返回了应用环境上下文。回顾整体流程,Springboot的启动,主要创建了配置环境(environment)、事件监听(listeners)、应用上下文(applicationContext),并基于以上条件,在容器中开始实例化我们需要的Bean,至此,通过SpringBoot启动的程序已经构造完成,接下来我们来探讨自动化配置是如何实现。
自动化配置:
之前的启动结构图中,我们注意到无论是应用初始化还是具体的执行过程,都调用了SpringBoot自动配置模块
该配置模块的主要使用到了SpringFactoriesLoader,即Spring工厂加载器,该对象提供了loadFactoryNames方法,入参为factoryClass和classLoader,即需要传入上图中的工厂类名称和对应的类加载器,方法会根据指定的classLoader,加载该类加器搜索路径下的指定文件,即spring.factories文件,传入的工厂类为接口,而文件中对应的类则是接口的实现类,或最终作为实现类,所以文件中一般为如下图这种一对多的类名集合,获取到这些实现类的类名后,loadFactoryNames方法返回类名集合,方法调用方得到这些集合后,再通过反射获取这些类的类对象、构造方法,最终生成实例
下图有助于我们形象理解自动配置流程
mybatis-spring-boot-starter、spring-boot-starter-web等组件的META-INF文件下均含有spring.factories文件,自动配置模块中,SpringFactoriesLoader收集到文件中的类全名并返回一个类全名的数组,返回的类全名通过反射被实例化,就形成了具体的工厂实例,工厂实例来生成组件具体需要的bean。
mybatis-spring-boot-starter、spring-boot-starter-web等组件的META-INF文件下均含有spring.factories文件,自动配置模块中,SpringFactoriesLoader收集到文件中的类全名并返回一个类全名的数组,返回的类全名通过反射被实例化,就形成了具体的工厂实例,工厂实例来生成组件具体需要的bean。
更多看原博吧 太多了会比较晦涩难懂。。。
答案原博:https://www.jianshu.com/p/87f101d8ec41
主要说@SpringBootApplication里面最主要的三个注解及作用
https://zhuanlan.zhihu.com/p/50616871 (写的非常好,吹爆!)
远程机器上有一段代码:HelloImpl.java(实现类)
你本地有一段代码:Hello.java (接口)
然后:
你在本地使用:
@Reference
private Hello hello;
就可以为Hello这个接口注入实现类HelloImpl了。(是不是像spring的Ioc一样?)
至于怎么注入的,以及在本地怎么找到远程我要的实现类的先不管,
再多了解一点下面的RPC知识后就自然而然地知道了。
然后这个“小助手”会与服务端建立网络通讯传递方法调用信息,服务端的“小助手”接收到了客户端的请求,知道他要哪个方法,传递的什么参数,然后服务端的“小助手”就找到对应的方法,并执行,最后将结果通过网络返回给客户端的“小助手”,它再传给发起调用的地方。
客户端的“小助手”要将调用信息发送到服务端就要序列化请求,便于在网络中传输。
服务端的“小助手”需要反序列化后才知道参数到底是啥。
同样数据返回时依然需要序列化与反序列化
UML时序图
影响RPC调用的因素主要是:1.客户端能否与服务端【快速】建立连接、2.序列化与反序列化的速度是否够快。
再结合这篇:https://www.jianshu.com/p/fd0d4bf85c97
RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。
其中的技术细节包括:
rpc和Rest Api的区别
REST是一种设计风格,它的很多思维方式与RPC是不一样的。
RPC的思想是把本地函数映射到API,也就是说一个API对应的是一个function,我本地有一个getAllUsers,远程也能通过某种约定的协议来调用这个getAllUsers。至于这个协议是Socket、是HTTP还是别的什么并不重要;
RPC中的主体都是动作,是个动词,表示我要做什么。
而REST则不然,它的URL主体是资源,是个名词。而且也仅支持HTTP协议,规定了使用HTTP Method表达本次要做的动作,类型一般也不超过那四五种。这些动作表达了对资源仅有的几种转化方式。
ZK使用长连接推送方式,consul使用心跳方式。
可以。
访问修饰符定义了类、属性和方法的访问权限,Java 中包含四种,访问权限从小到大为 private、default、protected 和 public。
简单总结:
AQS(抽象队列同步器,或者简称同步器)其实就是一个可以给我们实现锁的框架
内部实现的关键是:先进先出的队列(CLH队列,双向队列)、state状态(volatile实现可见性,修改时CAS操作保证原子性)
定义了内部类ConditionObject
拥有两种线程模式
在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建
一般我们叫AQS为同步器
获取独占锁的过程就是在acquire定义的,该方法用到了模板设计模式,由子类实现的~
过程:acquire(int)尝试获取资源,如果获取失败,将线程插入等待队列。插入等待队列后,acquire(int)并没有放弃获取资源,而是根据前置节点状态状态判断是否应该继续获取资源,如果前置节点是头结点,继续尝试获取资源,如果前置节点是SIGNAL状态,就中断当前线程,否则继续尝试获取资源。直到当前线程被park()或者获取到资源,acquire(int)结束。
释放独占锁的过程就是在acquire定义的,该方法也用到了模板设计模式,由子类实现的~
首先调用子类的tryRelease()方法释放锁,然后唤醒后继节点,在唤醒的过程中,需要判断后继节点是否满足情况,如果后继节点不为且不是作废状态,则唤醒这个后继节点,否则从tail节点向前寻找合适的节点,如果找到,则唤醒.
AQS实现了一个同步器的基本结构,下面以独占锁与共享锁分开讨论,来说明AQS怎样实现获取、释放同步状态。
独占模式
acquire
: tryAcquire 本身不会阻塞线程,如果返回 true 成功就继续,如果返回 false 那么就阻塞线程并加入阻塞队列。acquireInterruptibly
:支持中断取消tryAcquireNanos
: 带有超时时间,如果经过超时时间则会退出。release
:释放成功会唤醒后续节点共享模式
acquireShared
acquireSharedInterruptibly
tryAcquireSharedNanos
releaseShared
注意以上框架只定义了一个同步器的基本结构框架,的基本方法里依赖的 tryAcquire 、 tryRelease 、tryAcquireShared 、 tryReleaseShared 四个方法在 AQS 里没有实现,这四个方法不会涉及线程阻塞,而是由各自不同的使用场景根据情况来定制。
有别于wait和notiry。这里利用 jdk1.5 开始提供的 LockSupport.park() 和 LockSupport.unpark() 的本地方法实现,实现线程的阻塞和唤醒。
AQS虽然实现了acquire,和release方法,但是里面调用的tryAcquire和tryRelease是由子类来定制的。可以认为同步状态的维护、获取、释放动作是由子类实现的功能,而动作成功与否的后续行为时有AQS框架来实现
https://blog.csdn.net/varyall/article/details/80381626
https://blog.csdn.net/vernonzheng/article/details/8275624
也是上面的博客里讲的。
State机制
提供 volatile 变量 state; 用于同步线程之间的共享状态。通过 CAS 和 volatile 保证其原子性和可见性。
基于AQS构建的Synchronizer包括ReentrantLock,Semaphore,CountDownLatch, ReetrantRead WriteLock,FutureTask等,这些Synchronizer实际上最基本的东西就是原子状态的获取和释放,只是条件不一样而已。
需要记录当前线程获取原子状态的次数,如果次数为零,那么就说明这个线程放弃了锁(也有可能其他线程占据着锁从而需要等待),如果次数大于1,也就是获得了重进入的效果,而其他线程只能被park住,直到这个线程重进入锁次数变成0而释放原子状态。
则是要记录当前还有多少次许可可以使用,到0,就需要等待,也就实现并发量的控制,Semaphore一开始设置许可数为1,实际上就是一把互斥锁。
闭锁则要保持其状态,在这个状态到达终止态之前,所有线程都会被park住,闭锁可以设定初始值,这个值的含义就是这个闭锁需要被countDown()几次,因为每次CountDown是sync.releaseShared(1),而一开始初始值为10的话,那么这个闭锁需要被countDown()十次,才能够将这个初始值减到0,从而释放原子状态,让等待的所有线程通过。
需要记录任务的执行状态,当调用其实例的get方法时,内部类Sync会去调用AQS的acquireSharedInterruptibly()方法,而这个方法会反向调用Sync实现的tryAcquireShared()方法,即让具体实现类决定是否让当前线程继续还是park,而FutureTask的tryAcquireShared方法所做的唯一事情就是检查状态,如果是RUNNING状态那么让当前线程park。而跑任务的线程会在任务结束时调用FutureTask 实例的set方法(与等待线程持相同的实例),设定执行结果,并且通过unpark唤醒正在等待的线程,返回结果。
使用的是ribbon,它默认的策略是轮询策略,也支持随机、权重等策略。
fail-fast ( 快速失败 )机制是集合世界中比较常见的错误检测机制,通常出现在遍历集合元素的过程中。它是一种对集合遍历操作时的错误检测机制,在遍历中途出现意外的修改时,通过 unchecked 异常暴力地反馈出来。这种机制经常出现在多线程环境下,当前线程会维护一个计数比较器, 即 expectedModCount, 记录已经修改的次数。在进入遍历前,会把实时修改次数 modCount 赋值给 expectedModCount,如果这两个数据不相等,则抛出异常。 java.util 下的所有集合类都是 fail-fast,而 concurrent 包中的集合类都是 fail-safe。
fail-fast ( 快速失败 )
ConcurrentModificationException
异常.ArrayList
源代码,在next方法执行的时候,会执行checkForComodification()
方法@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
//...............省略.............
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
原理:
举例
//会抛出ConcurrentModificationException异常
for(Person person : Persons){
if(person.getId()==2)
student.remove(person);
}
注意
这里异常的抛出条件时检测到modCount = expectedmodCount
这个条件.
如果集合发生变化时修改modCount
值, 刚好有设置为了expectedmodCount
值, 则异常不会抛出.(比如删除了数据,再添加一条数据)
所以不能依赖于这个异常是否抛出而进行并发操作的编程, 这个异常只建议检测并发修改的bug.
使用场景 :
java.util包下的集合类都是快速失败机制的, 不能在多线程下发生并发修改(迭代过程中被修改).
fail-safe ( 安全失败 )
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先copy原有集合内容,在拷贝的集合上进行遍历.
原理:
ConcurrentModificationException
缺点:
ConcurrentModificationException
,但同样地, 迭代器并不能访问到修改后的内容 (简单来说就是, 迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的)使用场景:
java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改.
磁盘类已经给出。
public class DiskMemory {
private int totalSize ;
public int getSize(){
return (new Random().nextInt(3)+1)*100;//加一是为了防止获取磁盘大小为0,不符合常理
}
public synchronized void setSize(int size){
totalSize += size;
}
public int getTotalSize(){
return totalSize; // 这里可以返回四个磁盘总大小。
}
}
主要用到的方法是:CountDownLatch,
CountDownLatch类是一个同步倒数计数器,构造时传入int参数,该参数就是计数器的初始值,每调用一次countDown()方法,计数器减1,计数器大于0 时, await()方法会阻塞后面程序执行,直到计数器为0,后面被阻塞的方法才会得以实行。await(long timeout, TimeUnitunit),是等待一定时间,然后执行,不管计数器是否到0了。
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(4);
ExecutorService service = Executors.newFixedThreadPool(6);
DiskMemory diskMemory = new DiskMemory();
for (int i = 0 ; i < 4 ; i ++) {
service.execute(new Runnable() {
@Override
public void run() {
int timer = new Random().nextInt(3) + 1;
try {
Thread.sleep(timer * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int diskSize = diskMemory.getSize();
System.out.printf("完成磁盘的统计任务,耗时%d秒。磁盘大小为%d。\n",timer,diskSize);
diskMemory.setSize(diskSize);
// 任务完成之后,计数器减一
countDownLatch.countDown();
System.out.println("count num = " + countDownLatch.getCount());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
// 主线程一直被阻塞,直到count的计数器被置为0
countDownLatch.await();
System.out.printf("全部磁盘都统计完成,所有磁盘总大小。\n" + ", totalSize = " + diskMemory.getTotalSize());
service.shutdown();
}
输出(因为并行,顺序不定)
完成磁盘的统计任务,耗时1秒。磁盘大小为200。
count num = 3
完成磁盘的统计任务,耗时3秒。磁盘大小为100。
完成磁盘的统计任务,耗时3秒。磁盘大小为300。
count num = 1
count num = 2
完成磁盘的统计任务,耗时3秒。磁盘大小为300。
count num = 0
全部磁盘都统计完成,所有磁盘总大小。
, totalSize = 900
原博链接:https://blog.csdn.net/zhujiangtaotaise/article/details/60570882
基本概念的区分:
Integer、new Integer() 和 int 的比较
Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false
Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false
分析:Integer i = 100 在编译时,会翻译成为 Integer i = Integer.valueOf(100),而 java 对 Integer类型的 valueOf 的定义如下:
java对于-128到127之间的数,会进行缓存。所以 Integer i = 127 时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了。
Integer i = new Integer(100);//自动拆箱为 int i=100; 此时,相当于两个int的比较
int j = 100;
System.out.println(i == j);//true
问题链接:https://mp.weixin.qq.com/s/7PGSaVoss077bGpUrCDg8A
Innodb通过MVCC(Multi-version Concurrency Control多版本并发控制)解决了幻读的问题。
MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC)。
可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,这个事务看到的数据都是一致的。
下面我们通过InnoDB的简化版行为来说明MVCC是如何工作的。
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(systemversionnumber)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。下面看一下在REPEATABLEREAD隔离级别下,MVCC具体是如何操作的。
SELECT InnoDB会根据以下两个条件检查每行记录:InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。只有符合上述两个条件的记录,才能返回作为查询结果。
INSERT InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
DELETE InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
UPDATE InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。MVCC只在REPEATABLEREAD和READCOMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容(4),因为READUNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。
为搜索字段创建索引。
避免使用 select *,列出需要查询的字段。
垂直分割分表。
选择正确的存储引擎。
最左匹配原则
成因
mysql创建复合索引的规则是首先会对复合索引的最左边,也就是索引中的第一个字段进行排序,在第一个字段排序的基础上,在对索引上第二个字段进行排序,其实就像是实现类似order by 字段1,字段2这样的排序规则,那么第一个字段是绝对有序的,而第二个字段就是无序的了,因此一般情况下直接只用第二个字段判断是用不到索引的,这就是为什么mysql要强调联合索引最左匹配原则的原因。
一、开源方面(psql更好)
二、ACID支持方面(psql更好)
PostgreSQL支持事务的强一致性,事务保证性好,完全支持ACID特性。
MySQL只有innodb引擎支持事务,事务一致性保证上可根据实际需求调整,为了最大限度的保护数据,MySQL可配置双一模式,对ACID的支持上比PG稍弱弱。
三、SQL标准的支持方面(psql更好)
PostgreSQL几乎支持所有的SQL标准,支持类型相当丰富。
MySQL只支持部分SQL标准,相比于PG支持类型稍弱。
四、复制
五、并发控制
六、性能
PostgreSQL
① PostgreSQL广泛用于读写速度高和数据一致性高的大型系统。此外,它还支持各种性能优化,当然这些优化仅在商业解决方案中可用,例如地理空间数据支持,没有读锁定的并发性等等。
② PostgreSQL性能最适用于需要执行复杂查询的系统。
③ PostgreSQL在OLTP/ OLAP系统中表现良好,读写速度以及大数据分析方面表现良好,基于PG的GP数据库,在数据仓库领域表现良好。
④ PostgreSQL也适用于商业智能应用程序,但更适合需要快速读/写速度的数据仓库和数据分析应用程序。
MySQL
① MySQL是广泛选择的基于Web的项目,需要数据库只是为了简单的数据事务。 但是,当遇到重负载或尝试完成复杂查询时,MySQL通常会表现不佳。
② MySQL的读取速度,在OLTP系统中表现良好。
③ MySQL + InnoDB为OLTP场景提供了非常好的读/写速度。总体而言,MySQL在高并发场景下表现良好。
④ MySQL是可靠的,并且与商业智能应用程序配合良好,因为商业智能应用程序通常读取很多。
PostgreSQL与MySQL优劣对比
PostgreSQL相对于MySQL的优势
1、在SQL的标准实现上要比MySQL完善,而且功能实现比较严谨;
2、存储过程的功能支持要比MySQL好,具备本地缓存执行计划的能力;
3、对表连接支持较完整,优化器的功能较完整,支持的索引类型很多,复杂查询能力较强;
4、PG主表采用堆表存放,MySQL采用索引组织表,能够支持比MySQL更大的数据量。
5、PG的主备复制属于物理复制,相对于MySQL基于binlog的逻辑复制,数据的一致性更加可靠,复制性能更高,对主机性能的影响也更小。
6、MySQL的存储引擎插件化机制,存在锁机制复杂影响并发的问题,而PG不存在。
7、PG对可以实现外部数据源查询,数据源的支持类型丰富。
8、PG原生的逻辑复制可以实现表级别的订阅发布,可以实现数据通过kafka流转,而不需要其他的组件。
9、PG支持三种表连接方式,嵌套循环,哈希连接,排序合并,而MySQL只支持嵌套循环。
10、 PostgreSQL源代码写的很清晰,易读性比MySQL强太多了。
11、 PostgreSQL通过PostGIS扩展支持地理空间数据。 地理空间数据有专用的类型和功能,可直接在数据库级别使用,使开发人员更容易进行分析和编码。
12、可扩展型系统,有丰富可扩展组件,作为contribute发布。
13、 PostgreSQL支持JSON和其他NoSQL功能,如本机XML支持和使用HSTORE的键值对。 它还支持索引JSON数据以加快访问速度,特别是10版本JSONB更是强大。
14、 PostgreSQL完全免费,而且是BSD协议,如果你把PostgreSQL改一改,然后再拿去卖钱,也没有人管你,这一点很重要,这表明了PostgreSQL数据库不会被其它公司控制。 相反,MySQL现在主要是被Oracle公司控制。
MySQL相对于PG的优势
1、innodb的基于回滚段实现的MVCC机制,相对PG新老数据一起存放的基于XID的MVCC机制,是占优的。新老数据一起存放,需要定时触 发VACUUM,会带来多余的IO和数据库对象加锁开销,引起数据库整体的并发能力下降。而且VACUUM清理不及时,还可能会引发数据膨胀;
2、MySQL采用索引组织表,这种存储方式非常适合基于主键匹配的查询、删改操作,但是对表结构设计存在约束;
3、MySQL的优化器较简单,系统表、运算符、数据类型的实现都很精简,非常适合简单的查询操作;
4、MySQL相对于PG在国内的流行度更高,PG在国内显得就有些落寞了。
5、MySQL的存储引擎插件化机制,使得它的应用场景更加广泛,比如除了innodb适合事务处理场景外,myisam适合静态数据的查询场景。
总结
总体上来说,开源数据库都不是很完善,商业数据库oracle在架构和功能方面都还是完善很多的。从应用场景来说,PG更加适合严格的企业应用场景(比如金融、电信、ERP、CRM),但不仅仅限制于此,PostgreSQL的json,jsonb,hstore等数据格式,特别适用于一些大数据格式的分析;而MySQL更加适合业务逻辑相对简单、数据可靠性要求较低的互联网场景(比如google、facebook、alibaba),当然现在MySQL的在innodb引擎的大力发展,功能表现良好。
https://blog.csdn.net/tongdanping/article/details/79878302
索引是一个排序的列表,在这个列表中存储着索引的值和包含这个值的数据所在行的物理地址,在数据十分庞大的时候,索引可以大大加快查询的速度,这是因为使用索引后可以不用扫描全表来定位某行的数据,而是先通过索引表找到该行数据对应的物理地址然后访问相应的数据
String:
字符串类型的内部编码有3种:
Rdis会根据当前值的类型和长度决定使用哪种内部编码实现。
Hash
哈希类型的内部编码有两种:
list
列表类型的内部编码有两种。
set
集合类型的内部编码有两种:
zset
有序集合类型的内部编码有两种:
有序集合比较典型的使用场景就是排行榜系统。
Redis在已使用内存达到设定的上限时,提供了6种数据淘汰策略(也就是maxmemory-policy可能的值):
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用 的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数 据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据 淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据
何时触发淘汰数据的动作
1、一个客户端执行指令,导致数据的增加时。
2、Redis检测到内存的使用已经达到上限。
3、Redis自身执行指令时,等等
注意:
Redis为了避免反复触发淘汰策略,每次会淘汰掉一批数据。
当Redis指令产生数据比较大时,淘汰掉的数据量也相应也比较大。
为了节省内存,LRU的策略并不是严格执行的,Redis是在整体中随机抽样取出一小部分数据,在这部分数据中严格执行LRU策略,在Redis3.0以后的版本对此算法做了改进,但仍然也是近似的LRU的策略,只是离真正的LRU更近了。
另外用户可以动态的设定随机抽取的样本数,例如:maxmemory-samples 5
LRU(Least recently used,最近最少使用)
CRC16算法产生的hash值有16bit,该算法可以产生2^16-=65536个值。换句话说,值是分布在0~65535之间。那作者在做mod运算的时候,为什么不mod65536,而选择mod16384?
这里是取了一个权衡点:
1)如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。myslots[CLUSTER_SLOTS/8]。65536÷8÷1024=8kb,改为16384就是2kb。所以为了减少心跳时带宽占有率,应该缩减槽点数量。
2) redis的集群主节点数量基本不可能超过1000个。
3) 槽位越小,节点少的情况下,压缩率高。所以为了压缩率,槽位也不能太少(槽位少,则槽位大)。
综上,为了平衡压缩率和心跳时带宽,取值16384。
暂无答案,猜测是使用虚拟槽分区。虚拟槽分区巧妙地使用了哈希分区,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中。
使用虚拟节点
Raft协议是用来解决分布式系统一致性问题的协议,但是redis内并没有用来实现一些分布式锁以及分布式事务,仅仅是用来做master宕机时的选主。
了解了raft协议,我们就看一下redis的一致性解决方案sentinel的架构设计。
sentinel集群内的每一个节点都会监控集群内部的每一个节点的状态,并将定时交换监控的信息。
我们来看下sentinel是怎么用raft协议来实现节点宕机处理的:
至此,redis的故障转移就完成了,redis利用比较易于实现的raft协议实现了节点宕机的自动化处理,保障了集群的高可用性。
答案博客链接
消息队列持久化
穿透分为前后、前:布隆过滤器
缓存并发:加锁阻塞(分布式锁)
缓存雪崩:设置随机过期时间
过期时间和回写局限性太大,回写的话又要连接数据库,又要连接redis。
使用消息队列
使用binlog
本题最好的解法是使用动态规划,首先要想明白一个问题:如何找到以a[n]作为最后一个元素的最长子列。
要找到以a[n]作为最后一个元素的最长子列,我们需要找出数组中所有的a[n]之前并且比a[n]小的元素集合,设为S(a[s]), 设在S(a[s])中以元素a[s]为最后一个元素的子列为Ls(a[s]), 选择Ls(a[s])中最长的一个加上a[n]自身就得到了以a[n]元素为队尾的最长子列。
要得到最终答案只需要再次遍历数组,比较所有以a[k](0 < k <= n)为队末的队列长度并选择最长的一个即可。
leetcode第三百题:
package lc201_400;
/**
* 最长上升子序列
* https://leetcode-cn.com/problems/longest-increasing-subsequence/submissions/
* 给定一个无序的整数数组,找到其中最长上升子序列的长度。
*
* @author binzhang
* @date 2019-08-17
*/
public class LeetCode300 {
public int lengthOfLIS(int[] nums) {
if (nums.length <= 1) {
return nums.length;
}
int[] dp = new int[nums.length];
dp[0] = 1;
for (int i = 1 ; i < nums.length ; i ++) {
int max = 1;
for (int j = 0 ; j < i ; j ++) {
if (nums[i] > nums[j]) {
max = Math.max(max, dp[j] + 1);
}
}
dp[i] = max;
}
int res = 0;
for (int i = 0 ; i < dp.length ; i ++) {
res = Math.max(res, dp[i]);
}
return res;
}
}
leetcode637
通过栈或者队列来解决。
leetcode94
题解链接:https://blog.csdn.net/coder__666/article/details/80349039
中序遍历(LDR)是二叉树遍历的一种,也叫做中根遍历、中序周游。在二叉树中,先左后根再右。巧记:左根右。
中序遍历首先遍历左子树,然后访问根结点,最后遍历右子树
若二叉树为空则结束返回,
否则:
(1)中序遍历左子树
(2)访问根结点
(3)中序遍历右子树
import java.util.Stack;
public class Test
{
public static void main(String[] args)
{
TreeNode[] node = new TreeNode[10];//以数组形式生成一棵完全二叉树
for(int i = 0; i < 10; i++)
{
node[i] = new TreeNode(i);
}
for(int i = 0; i < 10; i++)
{
if(i*2+1 < 10)
node[i].left = node[i*2+1];
if(i*2+2 < 10)
node[i].right = node[i*2+2];
}
midOrderRe(node[0]);
System.out.println();
midOrder(node[0]);
}
public static void midOrderRe(TreeNode biTree)
{//中序遍历递归实现
if(biTree == null)
return;
else
{
midOrderRe(biTree.left);
System.out.println(biTree.value);
midOrderRe(biTree.right);
}
}
public static void midOrder(TreeNode biTree)
{//中序遍历费递归实现
Stack<TreeNode> stack = new Stack<TreeNode>();
while(biTree != null || !stack.isEmpty())
{
while(biTree != null)
{
stack.push(biTree);
biTree = biTree.left;
}
if(!stack.isEmpty())
{
biTree = stack.pop();
System.out.println(biTree.value);
biTree = biTree.right;
}
}
}
}
class TreeNode//节点结构
{
int value;
TreeNode left;
TreeNode right;
TreeNode(int value)
{
this.value = value;
}
}
或者看这个:https://blog.csdn.net/weixin_43852903/article/details/91466111
解题思路
递归和非递归底层实现都是栈。
递归代码实现
void binaryTreeInOrder(TreeNode root) {
if (root == null){
return;
}
binaryTreeInOrder(root.left);
System.out.print(root.value+" ");
binaryTreeInOrder(root.right);
}
非递归代码实现
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
//用栈实现
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
TreeNode top = null;
while (cur != null || !stack.isEmpty()){
//内层循环将当前数据入栈
while (cur != null){
stack.push(cur);
cur = cur.left;
}
//出栈并将该元素放入到链表中
top = stack.pop();
list.add(top.val);
//将cur指向栈顶元素的右孩子
cur = top.right;
}
return list;
}
}
LeetCode136
暴力解法:遍历数组,map中没有,put(arr[i], 1),有的话value+1。
如果其他数字都是偶数次数:直接遍历,如果没出现过放入ArrayList中,有的话从ArrayList中移除。
快速解法:用异或的特性,A^A=0 0^X=X
;以及异或的交换律特性。
class Solution {
public static int singleNumber(int[] nums) {
int result=0;
for(int i=0;i<nums.length;i++){
// 因为其他的都是出现双数次,所以其他的都抵消掉了,最后只剩唯一元素的 A^0=A
result^=nums[i];
}
return result;
}
}
给定两棵二叉树,把它们合并成一个二叉树,合并规则如下:如果两棵树的对应节点不为空,把这两个节点的和作为新树的节点,如果有一个节点为空,则把非空的节点作为新树的节点。
leetcode617
AB两棵树 同步递归 用B更新A并返回A
递归过程:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
if (t1 == null && t2 == null) {
return null;
}
if (t1 != null && t2 == null) {
return t1;
}
if (t1 == null && t2 != null) {
return t2;
}
t1.val += t2.val;
t1.left = mergeTrees(t1.left, t2.left);
t1.right = mergeTrees(t1.right, t2.right);
return t1;
}
}
leetcode142
下面的解法空间复杂度是O(1),还可以直接用Set判断重复元素,时间复杂度是O(n)。
package lc1_200;
/**
* 环形链表 II
* https://leetcode-cn.com/problems/linked-list-cycle-ii/
*
* @author binzhang
* @date 2019-09-08
*/
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
public class LeetCode142 {
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
ListNode p1 = head;
ListNode p2 = head;
// 这里要注意判断p2.next 不然p2.next.next会报错
while (p1 != null && p2 != null && p2.next != null) {
p1 = p1.next;
p2 = p2.next.next;
if (p1 == p2) {
// p1 == p2 已经能证明有环,此时p2走过的路等于p1走过的路+环一圈的路,即从出发点到相遇点的路程等于环一圈的路程,所以相交点到入环点的距离等于出发点到入环点的距离。
p1 = head;
while (p1 != p2) {
p1 = p1.next;
p2 = p2.next;
}
if (p1 == p2) {
return p1;
}
}
}
return null;
}
}
设计模式有六大基本原则:
一、单一职责原则
单一职责原则:不要存在多于一个导致类变更的原因
顾名思义,就是职责单一,只做自己要的职责,其他的东西我不干,比如说一个互联网公司技术开发有android开发,ios开发,前端开发,后端开发 等 ,每一个职位都需要要专业的人来开发,才能开发出优秀的产品。如果公司为了节约成本,做android的又要搞ios开发,又要搞后端开发,所有的事都由一个人来干,这样不但忙不过来,而且不可能每一项技术都是那么专业,这就表明了单一职责的重要性。
二、里氏替换原则
里氏替换原则:子类可以扩展父类的功能,但不能改变父类原有的功能
里氏替换原则本质就是继承和多态的应用
这里面的意思是有
1、子类可以实现父类的抽象方法,也可以扩展添加自己的方法
2、当子类重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松,比如父类方法的形参为String,那么子类的方法形参为String或String父类
3、当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。比如父类的方法的返回值为String,那么子类方法返回值为String或String的子类
三、依赖倒置原则
依赖倒置原则:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象
说白了,就是面向接口
比如我们就常常把访问数据库的代码写成了函数,在访问数据库反复调用,这就叫做高层模块依赖低层模块,但是后来想用其他数据库,问题就来了,我们就不能直接利用高层,解决方法就是面向接口
四、接口隔离原则
接口隔离原则:客户端不应该被强迫地依赖那些根本用不上的方法。
和单一职责原则的区别,其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架
五、迪米特原则
迪米特法则:一个对象应该对其他对象保持最少的了解
这个就是面向对象的一个特征,对象之前尽量减少耦合,在一个类中应该少出现其他类
六、开闭原则
开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
不要改你以前写的代码,你应该加一些代码去扩展原来的功能,来实现新的需求,对于新功能,不要动不动就改别人的代码,而是在之前代码的基础上添加去扩展新功能
依次遍历的话是O(n^2)级别的,肯定不是面试官想要的答案。。。
我们可以先通过遍历算出两个链表的长度,然后得到长度的差值,先让长的走差值的长度,这样因为相交点之后的数据两个链表是一样的,所以在此时两个链表同步向下走,在为null之前有相等的地方就是他们的相交点。
答案链接
这里考虑了两种情况,链表无环和链表有环两种。
leetcode20
解题思路:
解法:
时间复杂度N*logK
解法:
时间复杂度(N+K-1)*logK
可以从两方面着手回答:
秒杀系统的整体架构可以概括为“稳、准、快”。
设计思路:将请求拦截在系统上游,降低下游压力。在一个并发量大,实际需求小的系统中,应当尽量在前端拦截无效流量,降低下游服务器和数据库的压力,不然很可能造成数据库读写锁冲突,甚至导致死锁,最终请求超时。
限流:前端直接限流,允许少部分流量流向后端。
削峰:瞬时大流量峰值容易压垮系统,解决这个问题是重中之重。常用的消峰方法有异步处理、缓存和消息中间件等技术。
异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。
内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。
消息队列:消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。
可拓展:当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了,像淘宝、京东等双十一活动时会临时增加大量机器应对交易高峰。
1. 长连接
HTTP 1.0需要使用keep-alive参数来告知服务器端要建立一个长连接,而HTTP1.1默认支持长连接。
HTTP是基于TCP/IP协议的,创建一个TCP连接是需要经过三次握手的,有一定的开销,如果每次通讯都要重新建立连接的话,对性能有影响。因此最好能维持一个长连接,可以用个长连接来发多个请求。
2. 节约带宽
HTTP 1.1支持只发送header信息(不带任何body信息),如果服务器认为客户端有权限请求服务器,则返回100,否则返回401。客户端如果接受到100,才开始把请求body发送到服务器。
这样当服务器返回401的时候,客户端就可以不用发送请求body了,节约了带宽。
另外HTTP还支持传送内容的一部分。这样当客户端已经有一部分的资源后,只需要跟服务器请求另外的部分资源即可。这是支持文件断点续传
的基础。
3. HOST域
现在可以web server例如tomat,设置虚拟站点是非常常见的,也即是说,web server上的多个虚拟站点可以共享同一个ip和端口。
HTTP1.0是没有host域的,HTTP1.1才支持这个参数。
1. 多路复用
HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。
当然HTTP1.1也可以多建立几个TCP连接,来支持处理更多并发的请求,但是创建TCP连接本身也是有开销的。
TCP连接有一个预热和保护的过程,先检查数据是否传送成功,一旦成功过,则慢慢加大传输速度。因此对应瞬时并发的连接,服务器的响应就会变慢。所以最好能使用一个建立好的连接,并且这个连接可以支持瞬时并发的请求。
关于多路复用,可以参看学习NIO 。
2. 数据压缩
HTTP1.1不支持header数据的压缩,HTTP2.0使用HPACK算法对header的数据进行压缩,这样数据体积小了,在网络上传输就会更快。
3. 服务器推送
意思是说,当我们对支持HTTP2.0的web server请求数据的时候,服务器会顺便把一些客户端需要的资源一起推送到客户端,免得客户端再次创建连接发送请求到服务器端获取。这种方式非常合适加载静态资源。
服务器端推送的这些资源其实存在客户端的某处地方,客户端直接从本地加载这些资源就可以了,不用走网络,速度自然是快很多的。
tail -fn 999 xxx.log
which [-a] command # -a :将所有指令列出,而不是只列第一个
whereis [-bmsu] dirname/filename
locate [-ir] keyword # -r:正则表达式
find / -name xxx
ps aux | grep xxx
pstree -A
top -d 2
netstat -anp | grep port