- 考查对Java设计的掌握程度
- Java的private修饰符并不是为了绝对安全性设计的,更多的是对用户常规使用Java的一种约束
- 从外部对对象进行常规调用时,可以清晰了解类结构
- Java类的初始化顺序:
- 基类静态代码块,基类静态成员变量(并列优先级,按照代码中出现的先后顺序执行,并且只有第一次加载时执行)
- 派生类静态代码块,派生类静态成员变量(并列优先级,按照代码中出现的先后顺序,并且只有第一次加载时执行)
- 基类普通代码块,基类普通成员变量(并列优先级,按照代码中出现的先后顺序执行)
- 基类构造函数
- 派生类普通代码块,派生类普通成员变量(并列优先级,按照代码中出现的先后顺序执行)
- 派生类构造函数
- 成员变量:
- 可以不经初始化,在类的加载过程中的准备阶段可以赋予默认值
- 赋值和取值访问的先后顺序具有不确定性
- 成员变量可以在一个方法调用前赋值,也可以在方法调用后进行赋值. 这是在运行时发生的,编译器确定不了,所有交给JVM来赋值
- 局部变量:
- 在使用之前需要显式赋予初始值
- 局部变量的赋值和访问顺序是确定的
- 这样设计是一种约束,尽最大可能减少使用者犯错:
- 假使局部变量可以使用默认值,可能总会无意间忘记赋值,进而导致不可预期的情况发生
- 从结构实现上来讲:
- HashMap实现是数组+链表+红黑树(红黑树部分是JDK 1.8之后增加的)
- HashMap最多允许一条记录的键为null,允许多条记录的值为null
- HashMap是非线程安全的
- 可变与不可变:
- String不可变,每一次执行 "+" 都会新生成一个新对象,所以频繁改变字符串的情况下不用String,以节省内存
- 是否多线程安全:
- StringBuilder并没有对方法进行加同步锁,所以是非线程安全的.StringBuffer和String都是线程安全的
- ArrayList在内存不够时默认扩展是50%+1个,Vector默认是扩展1倍
- Vector是属于线程安全级别的,但是大多数情况下不使用Vector,因为线程安全需要更大的系统开销
- HashTable继承Dictionary类,HashMap继承AbstrctMap类
- HashTable不允许空键值对,而HashMap允许空键值对,但最多只有一个空对象
- HashTable同步,而HashMap不同步,效率上比HashTable要高
- ConcurrentHashMap融合了HashTable和HashMap二者的优势:
- HashTable是做了同步的,是线程安全的,而HashMap未考虑同步,所以HashMap在单线程情况下效率比较高
- HashTable在多线程的情况下,同步操作能保证程序执行的正确性
- 但是HashTable是阻塞的,每次同步执行的时候都要锁住整个结构
- ConcurrentHashMap正好解决了效率和阻塞问题:
- ConcurrentHashMap允许多个修改操作并发进行,技术的关键是使用了锁分离,即一个Array保存多个Object,使用这些对象的锁作为分离锁,get或者put的时候随机使用任意一个
- ConcurrentHashMap使用了多个锁来控制对Hash表的不同部分进行的修改
- 从JDK 1.6开始,在HashEntry结构中,每次插入将新添加节点作为链的头节点,这与HashMap实现相同.
- 每次删除一个节点时,会将删除节点之前的所有节点拷贝一份组成一个新的链,而将当前节点的上一个节点的next指向当前节点的下一个节点.从而在删除以后会有两条链存在
- 因此可以保证即使在同一条链中,有一个线程在删除,而另一个线程在遍历,都能工作良好.因为遍历的线程能继续使用原有的链
- 在Java 8中,使用volatile HashEntry保存数据,table元素作为锁.从Table数组+单向链表又加上了红黑树
- 红黑树是一种特别的二叉查找树,红黑树的特性:
- 节点为红或黑
- 根节点为黑
- 叶节点为黑
- 一节点为红,则一节点为黑
- 一节点到其子孙节点所有路径上的黑节点数目相同
- ArrayList底层的数据结构是数组,支持随机访问.LinkedList的底层数据结构是链表,不支持随机访问
- 使用下表访问一个元素:
- ArrayList的时间复杂度是O(1)
- LinkedList的时间复杂度是O(n). LinkedList是双向链表
- Comparable接口用于定义对象的自然顺序,是排序接口
- Comparator通常用于定义用户定制的顺序,是比较接口
- 如果需要控制某个类的次序,而该类本身不支持排序,即没有实现Comparable接口,就可以建立一个"该类的比较器"来进行排序
- Comparable总是只有一个,但是可以有多个Comparator来定义对象的顺序
- 抽象类是不允许被实例化的类,一个类只能使用一次继承关系,但是一个类可以实现多个接口
- 抽象类和接口所反映出的设计理念不同:
- 抽象类表示的是 "is - a"
- 接口表示的是 "like - a"
- 实现抽象类和接口的类必须实现其中的所有方法.抽象类可以有非抽象方法,接口中则不能有实现方法,但是在Java 8中允许接口中有静态默认方法
- 接口中定义的变量默认是public static final型,且必须给出初值,所以实现类中不能重新定义,也不能改变这个值
- 抽象类中定义的变量默认是friendly型,这个变量的值可以在子类中重新定义,也可以重新赋值
- 子类中实现父类中的抽象方法时.可见性可以大于等于父类中的
- 接口实现类类中的接口方法的可见性只能与接口中的相同,即为public
- 使用抽象类是为了重用,减少编码量,降低耦合性
- 重载和重写都是使用相同的名称实现不同的功能,但是重载是编译时活动,重写是运行时活动
- 可以在同一个类中重载方法,但只能在子类中重写方法,重写必须要有继承
- 重载:
- 重载的时候,方法名要一样,但是参数类型和参数个数不一样,返回值类型可以相同也可以不同
- 无法以返回型别作为重载函数的区分标准
- 重写:
- 在子类中可以根据需要对从基类中继承的方法进行重写
- 重写的方法和被重写的方法必须具有相同的方法名称,参数列表和返回类型
- 重写方法不能使用比被重写方法更严格的访问权限
- Collection< E >是Java集合框架中的基本接口
- Collections是Java集合框架提供的一个工具类,其中包含了大量用于操作和返回集合的静态方法
- 多态指的是父类引用指向子类的对象,调用方法时会调用子类的实现而不是父类的实现
- 多态的实现关键在于动态绑定
- clone()
- equals()
- hashCode()
- toString()
- notify()
- notifyAll()
- wait()
- finalize()
- getClass()
- 泛型即参数化类型,在创建集合时,指定集合元素的类型,此集合只能传入该类型的参数
- 类型擦除:Java编译器生成的字节码不包括泛型信息,所以在编译时擦除
- 泛型用最顶级的父类替换
- 移除
- Lambda表达式
- 允许像对象一样传递匿名函数Stream API,充分利用现代多核CPU,可以写出很简洁的代码
- Date与Time API,有一个稳定简单的日期和时间库可供使用
- 接口中可以有静态,默认方法
- 重复注解,可以将相同的注解在同一类型上使用多次
- protected可在包内及包外子类访问
- default只能在同一包内访问
- private只能在同一个类中访问
- 集合
- 线性结构
- 数组
- 队列
- 链表
- 栈
- 树形结构
- 图状结构
Java中的TreeMap是使用红黑树实现的
- 匿名内部类就是没有名字的内部类,匿名内部类只能使用一次,通常用来简化代码编写
- 匿名内部类只能访问外部类的final变量
- 在Java 8中,如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了final修饰
- 通过枚举
- 通过静态内部类
- 也可以通过双重检查创建单例模式,但是这种单例模式是线程不安全的
- poll()和remove都是从队列中取出一个元素
- poll()在获取元素失败时会返回空
- remove()在获取元素失败时会抛出异常
- 使用迭代器
Iterator it = list.iterator(); while (it.hasNext()) { if (...) { it.remove(); } }
- GPL: GNU General Public License,GNU通用公共许可协议
- LGPL: GNU Lesser General Public License, GNU宽通用公共许可协议
- BSD: Berkeley Software Distribution, 伯克利软件分发许可协议
- MIT: Massachusetts Institute of Technology
- Apache: Apache Licence, Apache许可协议
- MPL: Mozilla Public Licence, Mozilla公共许可协议
- 线程同步与否和阻塞非阻塞没有关系
- 同步是一个过程,阻塞是线程的一种状态
- 多个线程操作共享变量时会出现竞争
- 需要使用同步来防止两个以上的线程同时进入临界区内,在这个过程中,后进入临界区的线程将阻塞,等待先进入的线程走出临界区
- 同步和异步最大的区别是: 一个需要等待,一个不需要等待
- 同步可以避免出现死锁,读脏数据的发生,一般共享某一资源的时候使用
- 如果每个人都有修改权限,同时修改一个文件,有可能使一个人读取另一个人已经删除的内容,就会出错
- 同步就会按照顺序来修改
- 线程池的作用是根据系统自身的情况,有效的限制执行线程的数量,使得运行效果达到最佳
- 线程池主要执行的是:
- 控制执行线程的数量
- 超出数量的线程排队等候
- 等待有任务执行完毕
- 再从队列中最前面取出任务执行
- wait()方法应该在循环中调用:
- 因为当线程获取到CPU开始执行的时候,其他条件可能还没有满足
- 所以在处理前,循环检测条件是否满足更好
- wait(),notify()和notifyAll()方法是java.lang.Object类为线程提供的用于实现线程间通信的同步控制的等待和唤醒方法
- 实现线程的方法:
- 继承Thread类,重写run函数
- 实现Runnable接口,重写run函数
- 实现Callable接口,重写call函数
- 伪共享是多线程系统(这个系统的每隔处理器都有自己的局部缓存)中一个普遍存在的性能问题
- 缓存系统中是以缓存行(cache line)为单位存储的
- 缓存行是2的整数幂个连续字节,一般为32 - 256字节
- 最常见的缓存行是64个字节
- 当多线程修改相互独立的变量时,如果这些变量共享同一个缓存行,就会影响彼此的性能,这就是伪共享
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.lock
- ReadWriteRock读写锁的使用场景:
- 读 - 读
- 读 - 写
- 写 - 写
- 除了读 - 读之间是共享的,其余都是互斥的
- 考查对AQS, CAS的掌握程度
- Semaphore
- CountDownLatch
- CyclicBarrier
- Exchanger
- ReadWriteLock读写锁的使用场景:
- 读,读
- 读,写
- 写,写
- 除了读和读之间是共享的,其他都是互斥的
这样之后会讨论怎样实现互斥锁和同步锁的,了解对AQS,CAS的掌握程度,技术学习深度
- Semaphore拿到执行权的线程之间是互斥的
- Semaphore, CountDownLatch, CyclicBarrier, Exchanger是Java并发编程中的4个辅助类,了解CountDownLatch和CyclicBarrier之间的区别
- Semaphore可能有多把锁,可以允许多个线程同时拥有执行权,这些有执行权的线程如果并发访问同一对象,会产生线程安全问题
- Semaphore:
- 可以有多把锁,允许多个线程同时拥有执行权
- 这些有执行权的线程如果并发访问同一对象,会产生线程安全问题
- 单例模式是最常遇到的设计模式之一,考查对经常碰到的问题的理解的深度
- 单例一共有5种实现方式:
- 饿汉
- 懒汉
- 静态内部类
- 双检锁
- 枚举
- 要是写了简单的懒汉式可能会问: 要是多线程情况下怎样保证线程安全呢?
- 使用双检锁可以保证线程安全.
- 为什么要两次校验?光是双检锁还会有什么问题?
- 对象在定义的时候加上volatile关键字
- 引申讨论原子性和可见性,Java内存模型,类的加载过程
- 枚举方式,静态内部类,双检锁都可以实现单例模式. 双检锁的单例模式:
public Class Singleton { private Singleton() { } private volatile static Singleton instance; public static Singleton getInstance() { if (null == instance) { synchronized (Singleton.class) { if (null == instance) { instance = new Singleton(); } } } return instance; } }
- 死锁的四个条件:
- 示例: 定义两个ArrayList,都加上锁A,B.线程1,2. 线程1获取到锁A,请求锁B. 线程2获取到锁B,请求锁A. 在等待对方释放锁的过程中都不会让出已获得的锁
public class DeadLock { public static void main(String[] args) { final List<Integer> list1 = Arrays.asList(1, 2, 3); final List<Integer> list2 = Arrays.asList(4, 5 ,6); new Thread(new Runnable() { @Override public void run() { synchronized (list1) { for (Integer i : list1) { System.out.println(i); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (list2) { for (Integer i : list2) { System.out.println(i); } } } } }).start(); new Thread(new Runnable() { @Override public void run() { synchronized (list2) { for (Integer i : list2) { System.out.println(i); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (list1) { for (Integer i : list1) { System.out.println(i); } } } } }).start(); } }
- 相等
- new一个对象赋给变量
- 这行表达式创建了几个对象
- 是
- 不可以使用for循环直接删除ArrayList中的特定元素:
- 不同的for循环会发生不同的异常
- 泛型for会抛出ConcurrentModificationException
- 普通的for想要删除集合中重复且连续的元素,只能删除第一个
- 原因:
- JDK中的ArrayList源码
- ArrayList中的remove有两个同名方法,只是入参不同:
- 入参为Object的实现:
- 一般情况下程序的执行路径走到else路径下最终调用faseRemove() 方法,会执行System.arraycopy() 方法,导致删除元素时涉及到数组元素的移动
- 普通for循环,在 遍历第一个符合删除条件的字符串时将该元素从数组中删除,并且将后一个元素即第二个元素移动到当前位置,导致下一次遍历时后一个字符串并没有遍历成功,所以无法删除. 这种可以使用倒序删除的方式来避免
- 解决方法: 使用迭代器Iterator
List<String> list = new ArrayList(Arrays.asList("a", "b", "b", "c", "d")); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); if ("b".equals(element)) { iterator.remove(); } }
- 第一步: 线程池判断核心线程池里的线程是否都在执行任务. 如果不是,则创建一个新的工作线程来执行任务. 如果核心线程池里的线程都在执行任务,则执行第二步
- 第二步: 线程池判断工作队列是否已经满了. 如果工作队列没有满,则将新提交的任务存储在这个工作队列中等待. 如果工作队列满了,则执行第三步
- 第三步: 线程池判断线程池的线程是否都处于工作状态. 如果没有,则创建一个新的工作线程来执行任务. 如果已经满了,则交给饱和策略来处理这个任务
- 抽象队列同步器AQS: AbstractQueuedSychronizer
- 如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心
- ReentrantLock, CountDownLatch, Semaphore都用到了AQS
- AQS实际上以双向队列的形式连接所有的Entry:
- ReentrantLock: 所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行
- AQS定义了对双向队列所有的操作,并且只开放了tryLock和tryRelease方法给开发者使用.开发者可以根据自己的实现重写tryLock和tryRelease方法来实现自己的并发功能
- 比较并替换CAS: Compare and Swap
- 假设有三个操作数:
- 内存之V
- 旧的预期值A
- 要修改的值B
- 当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true. 否则什么都不做并返回false.
- 整个比较并替换的操作是一个原子操作
- CAS必须要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的响应值. 否则旧的预期值A对某条线程来说,永远是一个不会变的值A. 只要某次CAS操作失败,则CAS操作永远不会成功
- CAS高效地解决了原子操作的问题,但仍然存在三大问题:
- 循环时间长开销很大
- 只能保证一个共享变量的原子操作
- ABA问题
- synchronized(this)原理:
- 两条指令: monitorenter和monitorexit
- 同步方法: 从同步方法的反编译的结果中可以看出 - 方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,在常量池中多了ACC_SYNCHRONIZED标识符
- JVM就是根据ACC_SYNCHRONIZED标识符来实现方法同步的:
- 当方法被调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置
- 如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完之后再释放monitor
- 在方法执行期间,其余任何线程都无法再获得同一个monitor对象
- 理解volatile关键字的作用的前提是要理解Java的内存模型
- volatile关键字的作用主要有两点:
- 多线程主要围绕可见性和原子性两个特性展开.使用volatile关键字修饰的变量,保证了在多线程之间的可见性.即每次读取到volatile变量,一定是最新的数据
- 底层代码的执行: Java代码 -> 字节码 -> 根据字节码执行对应的C/C++代码 -> C/C++代码被编译成汇编语言 -> 和硬件电路交互.现实中,为了获取更好的性能,JVM可能会对指令进行重排序,多线程下可能会出现意想不到的问题.使用volatile则会禁止对语义重排序,不过也会一定程度上降低代码的执行效率
- 从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性. 比如AtomicInteger
- B树和B+树,既考查MySQL索引的实现原理,也考查数据结构基础
- 首先从二叉树说起:
- 因为会产生退化现象,提出平衡二叉树
- 再提出怎么样让每一层放的节点多一些来减少遍历高度,引申出m叉树
- m叉搜索树同样会有退化现象,引出m叉平衡树,即B树
- 这个时候每个节点既放了key又放了value.怎样使每个节点放尽可能多的key值,以减少遍历高度也就是访问磁盘的次数
- 可以将每个节点只放key值,将value值放在叶子节点,在叶子节点的value值增加指向相邻节点的指针,这就是优化后的B+树
- 然后谈谈数据库索引失效的情况:
- 为什么给离散度低的字段,比如性别建立索引是不可取的?查询数据反而更慢
- 如果将离散度高的字段和离散度低的字段,比如性别建立联合索引会怎样,有什么需要注意的?
- AOP和IOC是Spring的精华部分
- AOP:
- AOP可以看作是对OOP的补充,对代码进行横向扩展
- 通过代理模式实现.代理模式有静态代理和动态代理.
- Spring利用的是动态代理,在程序运行过程中将增强代码织入源代码中
- IOC: 控制反转
- 将对象的控制权交给Spring框架,用户使用对象无需创建,直接使用即可
- AOP和IOC重点要了解设计思想
- Spring的循环依赖问题:
- 什么是循环依赖?
- 怎样检测出循环依赖?
- Spring循环依赖有几种方式,使用基于setter属性的循环依赖为什么不会出现问题?
- 1. 用户发送请求 -> DispatcherServlet: 前端控制器收到请求后自己不进行处理,而是委托给其余解析器进行处理,作为统一的访问点,进行全局的流程控制
- 2. DispatcherServlet -> HandlerMapping: HandlerMapping将会把请求映射为HandlerExecutionChain对象.HandlerExecutionChain包含一个Hander处理器,多个HandlerInterceptor拦截器
- 3. DispatcherServlet -> HandlerAdapter: HandlerAdapter将会将处理器包装为适配器,从而支持多种类型的处理器
- 4. HandlerAdapter -> 处理器功能方法的调用: HandlerAdapter将会根据适配的结果调用真正的处理器的功能处理方法,完成功能处理,并返回一个ModelAndView对象. ModelAndView对象包含模型数据.逻辑视图名
- 5. ModelAndView的逻辑视图名 -> ViewResolver: ViewResolver将逻辑的视图名解析为具体的View
- 6. View -> 渲染: View会根据传进来的Model模型数据进行渲染,这里的Model是一个Map数据结构
- 7. 返回控制权给DispatcherServlet,由DispatcherServlet返回响应给用户
- 第一范式: 数据库中的表的所有字段值都是不可分割的原子数据项
- 第二范式: 数据库表中的每一列都和主键相关,而不能只和主键的某一部分相关
- 第三范式: 数据库表中每一列数据都和主键直接相关,不能间接相关
- 范式是为了减少数据冗余
- 重复性强的字段,不适合添加索引
- MySQL给离散度低的字段,比如性别设置索引,再以性别作为条件查询反而会更慢
- 一个表可能会涉及两个数据结构:
- 数据表: 存放表中的数据
- 索引
- 索引:
- 将一个或几个字段(组合索引)按规律排列起来,再附加上该字段所在行数据的物理地址(位于表中)
- 比如有个字段是年龄,如果需要选取某个年龄段的所有行,那么一般情况下可能需要进行一次全表扫描
- 但是如果以这个年龄段建立一个索引,那么索引会按照年龄值根据特定的数据结构建一个排列,这样在索引中就能迅速定位,不需要进行全表扫描
- 为什么性别不适合建立索引呢?
- 因为访问索引需要有额外的IO开销,从索引中拿到的只是地址,要想真正访问到数据还是要对表进行一次IO
- 如果要从表中的100万行数据中取几个数据,那么利用索引迅速定位,访问索引的IO开销就可以忽略不计
- 如果要从标中的100万行数据取50万行数据,再访问50万次表,加起来的开销并不会比对表进行一次完整的扫描小
- 如果将性别字段设为聚焦索引,那么肯定能加快大约一半该字段的查询速度
- 聚焦索引:
- 指的是表本身数据按照哪个字段的值来进行排序
- 聚焦索引不会付出额外IO开销
- 聚焦索引只能有一个
-因此聚焦索引要用到搜索最频繁的字段上- 可以根据业务场景需要,将性别和其余的字段建立联合索引. 比如时间戳,要将时间戳字段放在性别前面
- undo log:
- redo log:
- binlog:
- 数据库中的索引的结构是一种排序的数据结构,数据库的索引是通过B树和变形的B+树实现的
- 什么情况下不适合建立索引:
- 对于在查询过程中很少使用或者参考的列
- 对于只有很少数据值的列
- 对于定义为image,text和bit数据类型的列
- 当修改性能远远大于检索性能时
- 根据系统自身的环境情况,有效限制线程数量,使得运行效果达到最佳
- 线程主要是通过控制执行线程的数量,超出数量的线程排队等候,等待有任务执行完毕,再从队列最前面取出任务执行
- SQL优化
- 表结构优化
- 索引优化
- 缓存参数优化
- 生产者消费者模式:
- synchronized锁住一个LinkedList:
- 生产者: 只要队列不满,生产后往里存
- 消费者: 只要队列不空,消费后往外取
- 两者通过wait() 和notify() 进行协调
- 要考虑怎么样提高效率
- 熟悉消息队列设计精要思想及使用
- 异步处理: 相对于传统的串行,并行方式,提高了系统的吞吐量
- 应用解耦: 系统间通过消息通信,不用关心其他系统的处理
- 流量削峰: 可以通过消息队列长度控制请求量,可以缓解短时间内高并发请求
- 日志处理: 解决大量日志传输
- 消息通讯: 消息队列一般都内置了高效的通信机制,因此可以用在纯的消息通讯. 比如实现点对点消息队列,聊天室等
- 将所有Broker和待分配的Partition排序
- 将第i个Partion分配到第 (i mod n) 个Broker上
- 将第i个Partion的第j个Replica分配到第 ((i+j) mod n) 个Broker上
- 消息队列的顺序问题
- 消息有序指的是可以按照消息的发送顺序来消费
- 假定生产者产生了2条消息:M1,M2.假定M1发送到S1,M2发送到S2.如果要保证M1优先于M2被消费,如何保证:
- 解决方案:
- 保证生产者 - MQSever - 消费者是一对一对一的关系
- 缺陷:
- 并行度会成为系统的瓶颈,吞吐量不够
- 会出现更多的异常处理问题: 只要消费者出现问题,就会导致整个流程堵塞,不得不解决阻塞的问题
- 可以通过合理的设计或者将问题分解来规避:
- 不关注乱序的应用实际大量存在
- 队列无序并不意味着消息无序
- 消息的重复问题:
- 造成消息重复的根本原因: 网络不可达
- 所以解决这个问题的方法就是绕过这个问题.也就是: 如果消费端收到两条一样的消息,应该怎样处理?
- 解决方案:
- 消费端处理消息的业务逻辑保持幂等性
- 只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样
- 保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现
- 利用一张日志表来记录已经处理成功的消息的ID,如果新到的消息ID已经在日志表中,那么就不再处理这条消息
- 服务容器负责启动,加载,运行服务提供者
- 服务提供者在启动时,向注册中心注册自己提供的服务
- 服务消费者在启动时,向注册中心订阅自己所需的服务
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选择一台提供者进行调用.如果调用失败,再选择另一台进行调用
- 服务消费者和服务提供者,在内存中累计调用次数和调用时间,定时每分钟发送统计数据到监控中心
- Random:
- 随机负载均衡策略,按权重设置随机概率
- 在一个截面上的碰撞概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重
- RoundRobin:
- 轮循负载均衡策略,按公约后的权重设置轮循比率
- 存在慢的提供者累积请求的问题
- 比如: 第二台机器很慢,但没有宕机,当请求到第二台机器就会卡住,久而久之,所有的请求都会卡在 调到第二台机器的时候
- LeastActive:
- 最少活跃调用数负载均衡策略,相同活跃数的随机调用.活跃数指的是调用前后计数差
- 使慢的提供者收到更少的请求,因为越慢的提供者的调用前后计数差会越大
- ConsistentHash:
- 一致性Hash负载均衡策略,相同的参数请求总是发到同一提供者
- 当某台提供者宕机时,原本发往该提供者的请求,基于虚拟节点,平摊到其他提供者,不会引起剧烈变动
- 缺省只对第一个参数Hash,如果要修改,需要修改 < dubbo:parameter key="hash.arguments" value="0,1" />
- 缺省使用160份虚拟节点,如果要修改,需要修改< dubbo:parameter key="hash.nodes" value="320" >
- Failover: 失败自动切换,当出现失败,重试其他服务器. 通常用于读操作,但重试会带来更长延迟. 可以通过设置retries="2" 来设置重试次数,不包含第一次
- Failfast: 快速失败,只发起一次调用,失败立即报错. 通常用于非幂等性的写操作,比如新增记录
- Failsafe: 失败安全,出现异常时,直接忽略. 通常用于写入审计日志等操作
- Failback: 失败自动恢复,后台记录失败请求,定时重发. 通常用于消息通知操作
- Forking: 并行调用多个服务器,只要一个成功即返回. 通常用于实时性要求比较高的读操作,但需要浪费更多服务资源,可以通过设置 forks="2"来设置最大并行数
- Broadcast: 广播调用所有提供者,逐个调用,任意一台报错即报错. 通常用于通知所有提供者更新缓存或日志等本地资源信息
- Dubbo作为RPC框架,首先要完成的就是跨系统,跨网络的服务调用
- 消费方和提供方遵循统一的接口定义
- 消费方调用接口时,Dubbo将其转换为统一格式的数据结构
- 通过网络传输,提供方根据规则找到接口实现,通过反射完成调用
- 消费方获取的是对远程服务的一个代理 Proxy, 提供方因为要支持不同的接口实现,需要一个包装层Wrapper
- 调用过程:
- 消费方的Proxy和提供方的Wrapper得以让Dubbo构建出复杂,统一的体系
- 这种动态代理与包装是通过SPI的插件方式实现的,接口就是ProxyFactory:
@SPI("javassist") public interface ProxyFactory { @Adaptive({Constants.PROXY_KEY}) <T> T getProxy(Invoker<T> invoker) throws RpcException; @Adaptive({Constants.PROXY_KEY}) <T> invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException; }
- ProxyFactor有两种实现方式:
- 基于JDK的代理实现
- 基于javassist的实现
- ProxyFactory接口上定义了 @SPI("javassist"), 默认为javassist的实现
- Dubbo序列化: 阿里基于Java的序列化实现
- Hessian2序列化: Hessian是一种跨语言的高效二进制的序列化方式. 这里实际不是原生的Hessian2序列化,而是阿里修改过的Hessian Lite,是Dubbo默认启用的序列化方式
- Json序列化: 目前有两种实现:
- 采用阿里的fastjson库
- 采用Dubbo自身实现的简单Json库
- 一般情况下,json这种文本序列化性能不如二进制序列化
- Kryo和FST: Kryo和FST的性能普遍优于Hessian和Dubbo序列化
- Hessian是一个轻量级的remoting on http工具,采用Binary RPC协议,很适合发送二进制数据,同时又具有防火墙穿透能力
- Hessian支持跨语言串行
- Hessian序列化比Java默认的序列化具有更好的性能和易用性
- Hessian序列化支持的语言比较多
- Protoco Buffer是谷歌出品的一种轻量并且高效的结构化数据存储格式,性能比Json,XML强大得多
- Protoco的序列化和反序列化简单并且速度快. 原因在于:
- 编码和解码方式简单,只需要简单的数学运算=位移等等
- 采用Protoco Buffer自身的框架代码和编译器共同完成
- Protoco Buffer的数据压缩效果好,即序列化后数据量的体积小. 原因在于:
- 采用独特的编码方式,比如Varint,Zigzag编码方式等等
- 采用 T - L - V 数据存储方式,减少了分隔符的使用并且数据存储得紧凑
- 可以
- Dubbo消费者在应用启动时会从注册中心拉取已注册的生产者的地址接口,并缓存在本地. 每次调用时,按照本地存储的地址进行调用
- ZooKeeper是一个分布式应用协调系统,已经应用到了许多分布式项目中,用来完成
- 统一命名服务
- 状态同步服务
- 集群管理
- 分布式应用配置项的管理
- 每个Server在内存中存储了一份数据
- ZooKeeper启动时,将从实例中选举一个leader(Paxo协议)
- Leader负责处理数据更新等操作(Zab协议)
- 当且仅当大多数Server在内存中成功修改数据时,一个更新操作成功
- Netty是一个网络通信框架
- Netty进行事件处理的流程:
- Channel是连接的通道,是ChannelEvent的产生者
- ChannelPipeline可以理解为ChannelHandler的集合
- IO的方式通常分为:
- 同步阻塞的BIO
- 同步非阻塞的NIO
- 异步非阻塞的AIO
- 在使用同步阻塞的BIO的网络应用:
- 如果要同时处理多个客户端请求,或者是在客户端要同时和多个服务器进行通讯,就必须使用多线程来处理
- 同步非阻塞的NIO基于Reactor:
- 当socket有流可读或者可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或者写入操作系统
- 这个时候,不是一个连接就要对应一个处理线程了.而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的
- 异步非阻塞的AIO与NIO不同:
- 当进行读写操作时,只需要直接调用API的read或者write方法即可
- 这两种方法均为异步的:
- 对于读操作而言, 当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序
- 对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序
- read或者write方法都是异步的,完成后会主动调用回调函数
- 系统拆分的分类:
- 从资源角度:
- 应用拆分
- 数据库拆分
- 从采用的先后顺序:
- 水平扩展
- 垂直拆分
- 业务拆分
- 水平拆分
- 是否使用Dubbo依据实际业务场景来决定:
- 当垂直应用越来越多,应用之间的交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求. 此时,用于提高业务复用以及整合的分布式框架RPC是关键
- 当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需要增加一个调度中心基于访问压力实时管理集群容量,提高集群的利用率. 此时,用于提高机器利用率的资源调度和治理中心SOA是关键
- Dubbo支持服务治理,而Thrift不支持
- Thrift是跨语言RPC框架
setNX key value
value保证唯一性,避免线程A释放线程B拿到的锁
设置过期时间
set命令提供了相应的原子操作命令来保证set key value和设置过期时间的原子操作
Redis集群使用的是多主多从,当一半以上的主节点set成功,才算成功
先Delete缓存,再更新DB,延时一段时间再Delete缓存
或者先更新DB,延时一段时间再Delete缓存
因为如果线程A先Delete缓存,此时线程B发现缓存中没有数据,则从DB中读出老数据并reload到缓存,线程A更新数据库之后,则缓存与数据库数据库中的数据不一致,因此需要延时一段时间执行删除
重试机制
- 并发编程中的问题:
- 原子性问题
- 可见性问题
- 有序性问题
- volatile:
- volatile关键字能保证可见性,只能禁止指令重排序,不能保证原子性
- 可见性只能保证每次读取的是最新的值,但是volatile无法保证对变量的操作的原子性
- 在生成的会变语句中加入Lock关键字和内存屏障
- Lock:
- Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作,能够使用更优雅的方式解决线程同步问题
- 用synchronized修饰的方法或者语句块在代码执行完之后锁自动释放,然而使用Lock修饰的方法或者语句需要手动释放锁
热部署
- 管道: Pipe
- 命名管道: Named Pipe
- 信号: Signal
- 消息队列: Message
- 共享内存
- 内存映射: Mapped Memory
- 信号量: Semaphore
- 套接口: Socket
Synchronized修饰静态方法,锁定本身不是实例.非静态方法锁定实例
- 死锁: 指多个进程在运行过程中因争夺资源而造成的一种僵局
- 产生原因: 竞争资源
- 当系统中多个进程使用共享资源,并且资源不足以满足需要,会引起进程对资源的竞争而产生死锁
- 进程间推进的顺序不当
- 请求和释放资源的顺序不当,同样也会产生进程死锁
- 互斥条件: 进程独占资源
- 请求与保持: 进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件: 进程已经获得资源,在未使用完之前,不能强行剥夺
- 循环等待: 若干进程之间形成头尾相接的循环等待资源关系
线上服务器是分布式多台部署的,经常会面临解决分布式场景下数据一致性问题,这是就要利用分布式锁来解决这些问题
- Java类的初始化顺序:
- 基类静态代码块,基类静态成员变量. 并列优先级,按照代码中出现的先后顺序执行,并且只有第一次加载时执行
- 派生类静态代码块,派生类静态成员变量. 并列优先级,按照代码中出现的先后顺序执行,并且只有第一次加载时执行
- 基类普通代码块,基类普通成员变量. 并列优先级,按照代码块中出现的先后顺序执行
- 基类构造函数.
- 派生类普通代码块,派生类普通成员变量. 并列优先级,按照代码块中出现的先后顺序执行
- 派生类构造函数.
- 方法区是JVM规范中要求的 ,永久区是Hotspot虚拟机对方法区的具体实现
- 方法区是规范,永久区是实现方式(JDK 1.8以后做了改变)
- 文件中有几个类,编译后就有几个class文件
- 成员变量是可以不经初始化的,在类加载过程的准备阶段即可以给成员变量赋予默认值.
- 局部变量在使用之前需要显式赋予初始值
- javac不是推断不出不可以这样做,对于成员变量而言,其赋值和取值访问的先后顺序具有不确定性,对于一个成员变量可以在一个方法调用前赋值,也可以在方法调用后进行赋值,这是运行时发生的,编译器确定不了,交给JVM做比较合适
- 对于局部变量而言,局部变量的赋值和访问顺序是确定的,这样设计是一种约束,尽最大程度减少使用者犯错的可能性:
- 假使局部变量可以使用默认值,可能总会无意间忘记赋值,进而导致不可预期的情况出现
- 类加载过程:
- 加载
- 验证: 验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成伤害
- 准备: 准备阶段为变量分配内存并设置类变量的初始化
- 解析: 解析过程是将常量池内的符号引用替换成直接引用
- 初始化
- 双亲委派模型中的方法: 双亲委派是指如果一个类收到类加载请求,不会自己先尝试加载,先找父类加载器完成.当顶层启动类加载器表示无法加载这个类的时候,子类才会自己去加载.当回到最开始的发起者加载器还无法加载时,并不会向下找,而是抛出ClassNotFound异常
- 启动(Bootstrap)类加载器
- 标准扩展(Extension)类加载器
- 应用程序(Application)类加载器
- 上下文(Custom)类加载器
- 意义是防止内存中出现多份同样的字节码
- JVM如何判断一个对象已经变成可回收的垃圾:
- 引用计数器法: 引用计数器无法解决循环引用的问题
- 根搜索算法: 从一系列的GC Roots对象开始向下搜索,搜索的路径称为引用链.当一个对象到GC Roots之间没有引用链时称为引用不可达.引用不可达的对象被认为是可回收对象
- 几种垃圾回收器:
- Serial New或者Serial Old: 串行
- Parrallel New: 并行
- Parrallel Scavenge
- Parrallel Old
- G1: 一款并行与并发收集器,并且可建立可预测的停顿时间模型,整体上是基于标记清理,局部采用复制
- CMS
- CMS收集器是一个以获得最短回收停顿时间为目标的收集器,是一种并发收集器,采用的是Mark - Sweep算法
- 方法区(Method): 被所有线程共享,方法区包含所有的类信息和静态变量
- 堆(Heap): 被所有的线程共享,存放对象实例以及数组,Java堆是GC的主要区域
- 栈(Stack): 每一个线程包含一栈区,栈中保存一些局部变量
- 程序计数器: 当前线程执行的字节码行指示器
- 新生代存放所有新生成的对象
- 老年代存放的都是一些生命周期较长的对象
- 持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大
- 内存溢出: out of memory,程序申请内存时,没有足够的内存
- 内存泄露: 垃圾对象无法回收,可是使用memory analyzer工具查看泄露
- 进程: 运行中的程序,具有独立性,动态性,并发性
- 线程: 指进程中的顺序执行流
- 进程与线程的区别:
- 进程间不共享内存
- 创建进程进行资源分配的代价要大得多,所以多线程在高并发的环境中效率高
- 序列化: 将Java对象转化为字节序列
- 反序列化: 将字节序列转化为Java对象
- 序列化和反序列化主要是为了Java线程间的通讯,实现对象传递.只有实现了Serializable或者Externalizable接口类对象才可被序列化
在JVM中,int类型的变量的长度是一个固定值,与平台无关,4个字节,长度为32位
- Java中一共有四种类型的引用:
- StrongReference
- SoftReference
- WeakReference
- PhantomReference
- StrongReference是Java的默认引用实现,会尽可能长时间的存活于JVM内,当没有任何对象指向时将会被GC回收
- SoftReference会尽可能长的保留引用直到JVM内存不足时才会被回收,通过虚拟机保证.这一特性使得SofeReference非常适合缓存应用
- WeakReference是一个弱引用,当所引用的对象在JVM内不再有强引用时,将被GC回收
- WeakReference和SoftReference的区别:
- WeakReference与SoftReference都有利于提高GC和内存的效率
- WeakReference一旦失去最后一个强引用,就会被GC回收
- SoftReference会尽可能长的保留引用直到JVM内存不足时才会被回收,通过虚拟机保证
- Java中的堆和栈属于不同的内存区域,使用目的也不同
- 栈通常用于保存方法帧和局部变量.而对象总是在堆上分配
- 栈通常比堆小,也不会在多个线程之间共享,而堆是被整个JVM所有线程共享
- Java堆空间:
- 当通过Java命令启动Java进程的时候,会分配内存,内存的一部分用于创建堆空间
- 当程序中创建对象的时候,就从堆空间中分配内存
- GC:
- GC是JVM内部的一个进程,回收无效对象的内存用于将来的分配
- 在TCP连接中,数据流必须以正确的顺序送达对方
-TCP可靠性:
- 通过顺序编码和确认(ACK) 来实现的
- TCP连接是通过三次握手进行初始化的,三次握手的目的是同步连接双方序列号和确认号并交换TCP窗口大小信息:
- 第一次: 客户端发起连接
- 第二次: 表示服务器收到了客户端请求
- 第三次: 表示客户端收到了服务器反馈
- 在cookie中存储的session id
- cd: 用来改变所在目录. cd / - 转到根目录, cd ~ - 转到用户目录
- ls: 用来查看目录的内容
- cp: 用来拷贝文件. cp sourceFileName targetFileName
- mv: 移动文件. mv t.txt Document
- 加法Hash: 所谓的加法Hash就是把输入元素一个一个加起来构成最后的结果
- 位运算Hash: 这种类型的Hash函数通过利用各种位运算,比如移位或者异或来充分的混合输入元素
- 乘法Hash: 33*hash + key.charAt(i)
- 一致性Hash的设计目标是为了解决因特网中的热点(Hot spot)问题,一致性Hash算法提出了在动态变化的Cache环境中,判定Hash算法好坏的四个定义:
- 平衡性 :Balance
- 单调性 :Monotonicity
- 分散性 :Spread
- 负载 :Load
- get是从服务器获取信息, post是向服务器传信息
- get传送的数据量比较小, post传递的数据量可以比较大
- get的安全性比post低
- TCP: Tranfer Control Protocol, 是一种面向连接的保证传输协议,在传输数据流之前,双方会建立一条虚拟的通信道,可以极少差错传输数据
- UDP: User DataGram Protocol,是一种无连接的协议,使用UDP时,每一个数据段都是一个独立的信息,包括完整的源地址和目的地,在网络上以任何可能的路径到达目的地.因此,能否到达目的地,以及到达目的地的时间和内容的完整性都不能保证
- TCP比UDP多了建立连接的时间.相对UDP而言,TCP具有更高的安全性和可靠性
- TCP协议传输的大小不限制,一旦连接被建立,双方可以按照吧一定的格式传输大量的数据,而UDP是一个不可靠协议,大小有限制,每次不能超过64K