Java面试笔试题
JavaSE
为什么重写equals()方法后,要重写hashCode()方法?
如果不重写equals()方法的话,比较的是对象的内存地址
如果不重写hashCode()方法的话,是调用本地方法,返回的是内存地址
重写equals()方法主要是为了方便比较两个对象的内容是否相等。如果重写了equals()方法,通常也要重写hashCode()方法,目的是为了维护hashCode()方法的规定:相等的对象必须有相同的散列码。在HashMap中,比较key是否相等时,先调用hashCode()方法,后调用equals()方法,只有两个都表明是同一个对象时才说明是同一个key。
==比较的是什么?
比较两个对象时,是基于内存地址的比较;比较两个基本类型时,是比较两个值是否相等。
基本类型有哪些?什么是自动装箱和自动拆箱?
基本类型 | 包装类型 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
boolean | Boolean |
char | Character |
自动装箱是Java编译器将基本数据类型转化成与之对应的对象包装类型
自动拆箱是Java编译器将对象包装类型转化为与之对应的基本数据类型
基本数据类型转化
当使用+、-、*、/、%运算符时,遵循以下的规则:
- 如果操作数中有double,那么会将另外一个操作数的类型转化为double,并且结果也为double类型
- 如果操作数中有float,那么会将另外一个操作数的类型转化为float,并且结果为float类型
- 如果操作数中long,那么会将另外一个操作数的类型转化为long,并且结果为long类型
- 除此之外,byte、short、int、char之间的操作会将操作数都转化为int的类型(前面三条优先级依次降低)
值传递和引用传递?
值传递是对基本数据类型而言的,传递的是该变量的一个副本,改变该变量的副本并不会对原数据产生影响
引用传递是对对象类型变量而言的,传递的是该对象地址的副本,并不是原对象本身,对引用传递进行修改会影响到原对象
在Java中:
基本类型的变量保存原始值,即它代表的值就是数值本身;
而引用类型的变量保存的值是引用值,"引用值"指向内存空间的地址,代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。
Java中只有值传递。当一个对象实例作为一个参数传递到方法中时,参数的值就是该对象的引用值。对象的内容可以在被调用的方法中改变,但对象的引用是不会改变的。
如何跳出多重循环?
可以在外层循环的前面做个标记A,然后可以用break A跳出多重循环(Java中支持带标签的break和continue语句)
为什么会出现4.0 - 3.6 =0.40000001这种情况?
计算机在计算10进制小数的时候会先转化为2进制计算,在这个过程中出现了误差
String、StringBuffer、StringBuilder有什么异同?
可变性:
String在底层上是用final修饰的数组实现的: private final char value[],所以String是不可变的
StringBuilder和StringBuffer都继承自 AbstractStringBuilder,它是用 char[] value实现的,所以是可变的
线程安全性:
String对象是不可变的,所以是线程安全的
StringBuffer中的方法都加了synchronized关键字,所以是线程安全的
StringBuilder中的方法并没有添加同步锁,所以是线程不安全的
性能:
每次对String类型的对象进行修改,都会生成一个新的String对象,然后指针指向新的对象
StringBuffer和StringBuilder每次都是对自身进行操作,不会生成新的对象,但StringBuffer是线程安全的,性能差了一点
使用总结:
操作少量数据使用String
单线程操作大量数据使用StringBuilder
多线程操作大量数据使用StringBuffer
重载和重写
重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和修饰符可以不同
重写:发生在父子类中,方法名和参数列表必须相同,返回值范围小于等于父类、异常范围小于等于父类、访问修饰符大于等于父类,如果父类方法的访问修饰符是private,那么子类不能重写该方法
&和&&运算符的区别
&&是短路与运算,也就是说前面的不成立不会执行后面的,具有短路性质
&则需要两边都执行,而且&还是按位与运算。
同理的还有 | |和 |
异常语句块try、catch、finally
try:用于捕获异常。后面可以接多个catch块,如果没有catch块,则必须接一个finally块
catch:用于处理try捕获到的异常
finally:无论是否捕获或处理异常,finally块里的语句都会执行。当try和catch中有return时,finally会在方法返回之前执行
以下四种情况finally不会被执行:
- 在finally语句块中发生了异常
- 在前面的代码中用System.exit()方法退出程序
- 程序所在的线程死亡
- 关闭CPU
final 关键字
当使用final修饰一个类的时候,表明这个类不能被继承。final类中的成员变量可以根据需要设为final,但是final类中的方法都会被隐式地指定为final方法
对于一个final变量,如果是基本数据类型,则其数值在初始化后便不再会被修改;如果是引用类型地变量,则对其初始化后便不会再让其指向另一个对象
static关键字
static表明一个成员变量或者成员方法可以没有所属的类的实例变量的情况下被访问。Java中的static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。
static变量在Java中是属于类的,它在所有的实例中的值是一样的。当类被Java虚拟机载入的时候,会对static变量进行初始化。
静态内部类和普通的内部类
静态内部类(static nested class)不依赖于外部实例被实例化。而普通的内部类(Inner class)需要在外部类实例化后才可以被实例化。
静态内部类的成员既可以定义成为静态的,也可以被定义成动态的。静态内部类的静态成员只能对外部类的静态成员进行操作,而不能操作外部类的动态成员。静态内部类的动态成员可以操作外部类的所有成员。
有一个很普通的原则:因为静态方法总是跟类相关联的,而动态方法总是跟实例对象相关联的,所以静态方法永远不可以访问跟对象相关联的动态成员。一个类的动态成员可以访问这个类的任何成员,包括静态成员。
Map
Map主要用来存储键值对,根据键取得值,因此不允许有key重复(重复了就覆盖),但允许value重复
Map接口有四个主要实现类:HashMap、HashTable、LinkedHashMap、TreeMap
HashMap
在JDK1.8之前,HashMap的底层是用数组+链表结合在一起的链表散列实现的。HashMap通过key的hashcode经过扰动函数处理过后得到hash值,然后通过 (n - 1) & hash 判断当前元素存放的位置(n 指数组的长度,一般是偶数),如果当前位置存在元素的话,就判断该元素与要存放的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突
所谓的扰动函数就是HashMap的hash()方法,使用hash()方法可以减少碰撞
所谓的拉链法:将数组和链表相结合。也就是创建一个链表数组,数组中的每一格就是一个链表。若遇到冲突,则将冲突的值添加到链表中即可。
在JDK1.8之后,解决冲突有所改变,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
HashTable
HashTable与HashMap类似,但是它是线程安全的(同一把synchronized锁),而且不允许key和value为null,它也没有HashMap转化红黑树这样的机制
LinkedHashMap
LinkedHashMap是HashMap的一个子类,保存了插入时的顺序,用Iterator遍历时,先得到的肯定是先插入的
TreeMap
TreeMap的实现就是红黑树数据结构,也就是一棵自平衡的排序二叉树,这样可以保证当前需要快速检索指定节点。红黑树的插入、删除、遍历时间复杂度都是O(lgN),所以性能上低于哈希表。但是哈希表无法提供键值对的有序输出,红黑树因为是排序插入的,可以按照键值的大小有序输出。
红黑树的性质:
- 每个节点要么是红色,要么是黑色
- 根节点永远是黑色的
- 所有的叶节点都是空节点,并且是黑色的
- 每个红色节点的两个子节点都是黑色
- 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点
ConcurrentHashMap
JDK1.7:首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment实现了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色。HashEntry用于存储键值对数据
JDK1.8:ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构和HashMap类似。
synchronized只锁定当前链表或红黑树的首节点,这样只要hash不冲突,就不会产生并发。
List
ArrayList
ArrayList的底层使用的是Object数组,所以插入和删除元素的时间复杂度受元素位置的影响,支持随机快速访问,并发操作下可能会出现数组下标越界
LinkedList
LinkedList底层使用的是双向链表数据结构,所以插入和删除元素的时间复杂度不受元素位置影响,不支持随机快速访问
Vector
Vector的所有方法都是同步的,是线程安全的
Set
HashSet
HashSet在底层是用HashMap实现的,之所以set只添加一个,而map添加两个的键值对,那是因为set的add()方法只在map中添加建,而值是添加的一个Object的常量
集合框架底层数据结构总结
Collection
- List
- ArrayList:Object数组
- Vector:Object数组
- LinkedList:双向链表
- Set
- HashSet:基于HashMap实现
- LinkedHashSet:继承自HashSet,并且内部是通过LinkedHashMap实现的
- TreeSet:红黑树
Map
- HashMap:数组+链表/红黑树
- LinkedHashMap: 继承自HashMap,另外加了一条双向链表
- HashTable:数组+链表
- TreeMap:红黑树
线程的几种状态
- 新建(new):新创建了一个线程对象
- 可运行(runnable):线程对象创建后,调用了start()方法,等待运行
- 运行(running):runnable的线程获取CPU时间片,开始运行
- 阻塞(block):阻塞状态是线程由于某种原因放弃了CPU使用权,暂时停止运行
- 死亡(dead):线程执行完毕,或者退出,线程的生命周期结束
创建线程的方式
继承Thread类
重写Thread类的run()方法,直接实例化该类,调用start()方法即可启动线程
实现Runnable接口
实现Runnable接口,重写run()方法,实例化该类,并将该类传入一个Thread类,调用start()方法启动线程
实现Callable接口,FutureTask调用
实现Callable接口,重写call()方法,实例化该类,将实例对象传入FutureTask类,再将FutureTask的实例对象传入Thread类,调用Thread类的start()方法即可启动线程。相较于runnable接口,这个接口可以获取返回值。
利用线程池创建线程
实例化一个线程池,直接调用线程池的execute()方法,传入要执行的线程
sleep和wait方法
sleep是Thread类的方法,该方法会暂停线程一段指定时间,把执行机会给其他线程,但监控状态依旧保持,到时间后自动恢复,而且sleep方法是不释放锁的。
wait是Object类的方法,对一个对象调用该方法会让线程放弃锁,只有对该对象调用notify方法后,才会重新获取锁执行线程。
JMM(Java内存模型)
JMM规定线程之间的共享变量存放在主内存,每个线程都有一个私有的工作内存,各线程的私有变量都存储在各自的工作内存。
线程对变量的读写操作必须在自己的工作内存中完成,不能直接操作内存中变量,各个线程的工作内存空间存储着主内存中变量的拷贝副本,不同的线程无法访问对方的工作内存,线程间的传值(通信)必须通过主内存来完成
JMM线程安全的保证:
- 可见性
- 原子性
- 有序性
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷回主内存
- 线程加锁前,必须读取主内存中的最新值到自己工作内存
- 加锁解锁是同一把锁
ThreadLocal
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期要长。任何线程局部变量一旦在工作完成后没有释放,Java应用就存在内存泄露的风险。
synchronized
synchronized解决的是多个线程之间访问资源的同步性问题,synchronized关键字可以保证被它修饰的方法或者代码块任意时刻只能有一个线程执行。
synchronized的主要三种使用方式:
- 修饰实例方法,作用于当前实例对象加锁,进入同步代码块前要获取当前实例对象的锁。
- 修饰静态方法,给当前类加锁,会作用于当前类的所有实例对象。所以如果一个线程A调用了一个实例对象的非静态synchronized方法,而线程B调用了这个实例对象所属类的静态synchronized方法,是允许的,因为前者是对象锁,后者是类锁,不是同一把锁。
- 修饰代码块,指定加锁对象。
双重检验锁实现的单例模式就用到了synchronized:
public class Singleton{
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
在这一段代码中单利对象用volatile修饰可以禁止指令重排序,因为new 一个对象的步骤不是原子性的,分为了三个部分:
- 为instance分配内存空间
- 初始化instance
- 将instance指向分配好的内存地址
synchronized的底层原理实现:
修饰代码块:JVM用monitorenter湖人monitorexit两个指令来实现同步,其中monitorenter指向同步代码开始的地方,monitorexit是同步代码退出的地方。
修饰方法:使用ACC_SYNCHRONIZED标识,该标识指明该方法是一个同步方法,JVM通过该标识来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
volatile
volatile关键字是一种轻量级的同步机制。它有以下几个特征:
- 保证可见性
- 禁止指令重排序
- 没能保证原子性
上面的双重锁检验实现的单例模式中应用到了volatile关键字
JUC中的Atomic原子类
原子操作是不可在进行分割的,原子类也就是不可分割的类
基本类型:
- AtomicInteger:整形原子类
- AtomicLong:长整形原子类
- AtomicBoolean:布尔型原子类
数组类型:
- AtomicIntegerArray:整数型数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray:引用类型数组原子类
引用类型:
- AtomicReference:引用类型原子类
- AtomicStampedReference:原子更新带有版本号的引用类型
- AtomicMarkedReference:原子更新带有标记位的的引用类型
更新器:
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
- AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器
在多线程中i++操作不是原子的,可能导致数据不一致性,所以我们可以用AtomicInteger类的getAndIncrement()方法来实现++操作。
AtomicInteger的实现原理:
getAndIncrement()方法调用unsafe类的getAndAddInt()方法,该方法利用本地方法完成CAS操作保证原子性。
https://blog.csdn.net/qq_41282026/article/details/98089231
线程池
线程池做的工作主要是控制线程运行的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕后,再从队列中取出任务来执行。
优势:
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就可以立即执行
- 降低资源消耗:通过复用已经创建的线程来降低线程创建和销毁所带来的消耗
- 提高线程的可管理性:使用线程池可以进行线程的统一的分配、调优和监控
线程池可以通过Executors工具类来创建:
- newSingleThreadExecutor():
创建一个单线程化的线程池,即该线程池只有一个线程可以进行工作,顺序执行各任务 - newFixedThreadPool():
创建一个指定数量的线程池 - newCachedThreadPool():
创建一个可以缓存的线程池,数量没有限制 - newScheduledThreadPool:
创建一个定长的线程池,支持定时的以及周期性的任务执行
总结:
前两种允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,导致OOM
后两种允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
所以,我们一般不采用这种方式创建线程池,而是直接用构造方法创建:ExecutorService executor = new ThreadPoolExecutor(…),参数有7种,可以自己指定传入的参数决定该线程池的特征。
七大参数
- corePoolSize:线程池中的常驻核心线程数
- maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于1
- keepAliveTime:多余的空闲线程存活时间
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般默认的即可
- handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数时,如何拒绝请求执行的Runnable的策略
线程池的工作流程
- 线程池加入一个任务时,先判断核心线程数是否满了
- 如果没有满,就创建线程执行该任务
- 如果满了,就需要判断任务队列是否满了
- 如果任务队列没有满,就将该任务放入任务队列
- 如果任务队列也满了,就判断最大线程数是否满了
- 如果最大线程数没有满,就创建非核心线程执行任务
- 如果最大线程数也满了,就执行拒绝策略,根据策略进行拒绝无法执行的任务线程
AQS
AQS的是AbstractQueuedSynchronizer(java.util.concurrent.locks)类的缩写,AQS是用来构建锁和Synchronizer的框架。
什么是Java虚拟机
Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能够被Java虚拟机执行的字节码文件。
Java虚拟机保证了Java是可以跨平台的,因为它知道底层硬件平台的指令长度和其他特征。
JVM运行时数据区
-
方法区
方法区存储的是:静态变量+常量+类信息+运行时常量池
-
堆
存放对象实例
-
虚拟机栈
堆主管Java的运行,是在线程创建时创建,它的生命周期是跟随现成的生命周期,线程结束栈内存也就释放了,对于栈来说,不存在垃圾回收问题,只要线程结束栈就结束。基本类型的变量和对象的引用变量都是在函数的栈内存中分配
存放的是:本地变量+栈帧数据+栈操作
-
本地方法栈
存放本地方法
-
程序计数器
每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码,由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记
垃圾判断算法
引用计数法
- 给每一个对象配引用一个计数器
- 每当引用这个对象的时候,计数器就加一
- 每当引用失效时,计数器就减一
- 当计数器的值为0的时候,就表示这个对象需要进行回收
缺点:无法解决对象之间的相互循环引用
可达性分析
- 有一组GCRoots对象作为起始点,从起始点开始向下搜索,走过的路径称为引用链
- 如果一个对象和GCRoots之间有引用链相连接,说明对象是可达的,即对象可用
- 如果一个对象和GCRoots之间没有引用链相连接,说明对象是不可达的,即对象不可用
GCRoots必须是一组活跃的引用:
- 虚拟机栈中的引用对象
- 方法区中类静态属性引用的变量
- 方法区中常量引用的对象
- 本地方法栈中引用的对象
GC算法
-
复制算法
将内存分为两块,每次只是用其中的一块,在垃圾回收时,将正在使用的内存中的存活的对象复制到另外一块内存中,之后,清除正在使用的内存中的所有对象,交换两个区域的角色。
优势:不会产生内存碎片,内存是连续的
缺点:浪费了一半的内存空间 -
标记清除
分为两个阶段:1.对存活的对象进行标记。2. 回收所有没被标记的对象
优点:简单易于实现,不需要额外的空间
缺点:会产生内存碎片,内存不是连续的 -
标记整理
标记阶段:对存活的对象进行标记
整理阶段:将存活的对象整理到一端
清除阶段:清理边界外所有的内存优点:不会产生内存碎片
缺点:耗时严重
垃圾收集器
1. Serial
Serial收集器是单线程收集器,在进行收集时,必须停止用户的所有进程,直到垃圾收集完成,使用复制算法
XX:+UseSerialGC
2. Serial Old
Serial Old收集器是Serial的老年代版本,使用标记整理算法
3. ParNew
ParNew收集器是Serial的多线程版本,在进行回收时,依旧必须停止所有的用户进程,直到收集完成,使用复制算法
XX:+UseParNewGC
4. Parallel Scavenge
Parallel Scavenge是一个多线程的垃圾收集器,和ParNew相似。但是它关注的是:可控制的吞吐量和自适应调节策略。
-XX:+UseParallelGC
5. Parallel Old
Parallel Scavenge收集器的老年代版本。
6. CMS
CMS(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的老年代收集器,使用的标记清除算法
分为四个步骤:
- 初始标记:只是标记GCRoots能够直接关联到的对象,需要暂停其他工作线程
- 并发标记:进行GCRoots的跟踪标记,和用户线程一起工作,不需要暂停工作线程
- 重新标记:修正并发标记期间用户线程工作而出现改动的对象,需要 暂停其他工作线程
- 并发清除:清除GCRoots不可达的对象,和用户线程一起工作
-XX:+UseConcMarkSweepGC
7. G1
G1将整个堆内存划分为多个大小相等的独立区域(Region)(1M~32M),在JVM启动时会自动设置这些子区域的大小,G1不要求对象的存储一定是物理上连续的,只要是逻辑上连续的即可,每个分区也不会固定为某个代服务,可以按需求在年轻代和老年代之间切换。
特点:
- 并行与并发:G1能够充分的利用CPU,多核环境下的硬件优势来缩短StopTheWorld的停顿时间
- 分代收集:分代的概念依旧存在,不过G1独自就可以管理整个堆空间
- 空间整合:有利于长时间运行,分配大对象不会因为没有连续空间而导致FullGC
- 可预测的停顿:使用者可以明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾回收上的时间不超过N毫秒
4个步骤:
- 初始标记:只标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing的过程
- 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
- 筛选回收:根据时间来进行价值最大化的回收
类的加载过程
加载
通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。
验证
验证阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的规范,并且不会危害虚拟机自身的安全
准备
准备阶段是正式为类变量分配内存并设置类扮靓初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个时候进行内存分配的仅包括类变量(static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配到Java堆中。同时,初始值一般都是设为0。
解析
解析阶段是虚拟机将常量池内的符号引用替换成为直接引用的过程。
符号引用:符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义带Java虚拟机规范的Class文件格式中。
直接引用:直接引用是指向目标的指针,相对偏移量,或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标必定已经存在内存中。
初始化
初始化阶段是执行了类构造器 <clinit>()方法的过程.。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值操作和静态语句块中的语句合并产生的。虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不生成<clinit>()方法。
类加载器
启动类加载器
Bootstrap ClassLoader负责加载JAVA_HOME\lib目录中的,或通过-Xbootclasspath参数所指定的路径中的,并且被虚拟机认可的类
拓展类加载器
Extension ClassLoader负责加载JAVA_HOME\lib\ext目录中的,或通过java.ext.dirs系统变量所指定的路径中的所有类库
应用程序类加载器
Application ClassLoader负责加载用户类路径(classpath)上的类库
双亲委派模型
如果一个类加载器收到一个类加载的请求,会先交给父类加载器去完成,因此所有的加载请求最终会传递到顶层的启动类加载器。只有当父类加载器无法完成加载任务的时候,子类加载器才会尝试自己加载。
这样做的好处是Java类随着它的类加载器一起具备一种带有优先级的层次关系。
比较一个类是否相等
任意一个类,都需要由加载它的加载器和这个类本身一同确定其在Java虚拟机中的唯一性,每一个类加载器,都有拥有一个独立的类名称空间。
换句话说:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。
JavaEE
什么是spring
spring是Java是一个轻量级的企业级应用开源框架,旨在提高开发人员的开发效率和系统的可维护性。
IoC
控制反转:一种设计思想,将原本在程序中手动创建对象的控制权交由spring框架来管理。IoC容器是spring用来实现IoC的载体,IoC容器实际上就是一个Map,Map中存放的是各种对象。将对象之间的相互依赖关系交给IoC容器来管理,并由IoC容器完成对象的注入。
DI(依赖注入)是控制反转的一个实现。
依赖注入的实现方式:
- 构造注入
- 属性注入
- 接口注入
具体做法:
- 将bean之间的依赖关系转化为关联关系
- 将对具体类的关联尽可能地转化为对Java Interface的关联
- bean实例具体关联相关Java Interface的哪个实现类的实例,在配置信息的元数据中描述
- 由IoC组件(容器)根据配置信息,实例化具体的bean类,将bean之间的依赖关系注入进来
目的:
- 脱开、降低类之间的耦合
- 倡导面向接口编程、实施依赖倒置原则
- 提高系统的可修改、可测试、可插入特性
AOP
面向切面编程,用于处理系统中分布于各个模块的横切关注点,比如日志处理、事务管理、权限。AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理分为静态代理和动态代理。
静态代理:AspectJ是静态代理,AOP框架会在编译阶段生成AOP代理类,属于编译时增强
动态代理:属于运行时增强
- JDK动态代理:通过反射来接收被代理的对象,并且被代理的类必须实现一个接口
- Cglib动态代理:通过继承实现动态代理,不需要实现接口,但是如果是final,那么无法使用Cglib动态代理。
AOP的关键术语
- 连接点(Joinpoint):连接点就是在程序执行过程中某个特定的点,可以被拦截的点(可以增强/通知的点)
- 切入点(Pointcut):切入点指一个或多个连接点,真正被拦截的点
- 通知(Advice):在不修改原有代码的前提下,为某一个对象增加新的功能
- 切面(Aspect):多个通知和多个切入点的组合
- 目标对象(Target):被一个或者多个切面(aspect)所通知(advise)的对象。即被通知(advised)对象
- 代理对象(Proxy):被增强后所产生的对象
- 织入(Weaving):把通知应用到目标的过程
Spring的Bean的作用域
- singleton:单例模式,在整个SpringIoC容器中,使用singleton定义的Bean将只有一个实例
- prototype:原型模式,每次通过容器的getBean方法获取prototype定义的Bean时,都会产生一个新的Bean实例
- request:对于每次Http请求,使用request定义的Bean都将会产生一个新的实例,即每次HTTP请求将会产生不同的Bean实例。只有在Web应用中使用Spring时,该作用域才有用
- session:对于每此Http Session,使用session定义的Bean都将会产生一个新实例。同样只有 在Web应用中使用Spring才有作用
- globalsession:全局Session,仅仅在基于portlet的web应用中才有意义,spring5中已经没有了该组件
Spring支持的事务管理
编程式事务管理
编程式事务使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。
声明式事务管理
声明式事务是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。
spring中用到的设计模式
- 单例模式:spring中的bean都是默认为单例的
- 工厂模式:spring中的BeanFactory和ApplicationContext都是运用到了工厂模式来创建bean对象
- 代理模式:spring中的AOP功能用到了代理模式
- 模板模式:spring中的JdbcTemplate等以Template结尾的类用到了模板模式
- 适配器模式:spring中的MVC用到了适配器模式来适配controller
Spring自动装配的方式
- no:不进行自动装配,手动设置bean的依赖关系
- byName:通过bean的名字进行自动装配
- byType:根据bean的类型进行自动装配
- constructor:根据构造方法参数进行自动装配
- autodetect:如果有默认的构造器就用constructor方式,如果没有就用byType方式
@Component和@Bean
@Component作用于类,@Bean作用于方法
@Autowired和@Resource
共同点:
这两个注解都可以运用到字段和setter方法上,两者如果用到字段上,就不需要写setter方法
不同点:
- @Autowired是按照类型装配依赖对象,默认情况下要求依赖的对象必须存在,如果允许null值,可以设置它的require值为false。如果我们需要按照名称进行注入,需要配合@Qualifier一起使用
- @Resource默认按照名称装配依赖对象,它有两个属性name和type,有name属性就是按照名称注入,有type就是按照类型进行注入。
@Controller和@RestController
@RestController相当于@Controller和@ResponseBody一起使用的效果。
BeanFactory和ApplicationContext
-
BeanFactory:老版本的工厂类
调用getBean()的时候,才会生成类的实例
-
ApplicationContext:新版本的工厂类
加载配置文件的时候,就会将spring管理的类都实例化
- 有两个实现类 :
- ClassPathXmlApplicationContext:加载类路径下的配置文件(src)
- FileSystemXmlApplicationContext:加载文件系统路径下的配置文件(本地磁盘上的绝对路径)
SpringMVC的运行机制
DispatcherServlet
前端控制器:接收请求,响应结果,返回可以是json,String等数据类型,也可以是页面(Model)。
HandlerMapping
处理映射器:根据DispatcherServlet发送的url请求路径查找Handler
HandlerAdapter
处理器适配器:按照特定规则(HandlerAdapter要求的规则)去执行Handler。
Handler
处理器:就是controller,编写Handler时按照HandlerAdapter的要求去做,这样适配器才可以去正确执行Handler。
ViewResolver
视图解析器:进行视图解析,根据逻辑视图名解析成真正的视图。
处理流程
- 用户发送请求,DispatcherServlet接收请求
- DispatcherServlet调用HandlerMapping查询Handler
- HandlerMapping找到具体的Handler,返回HandlerExcutorChain(包含了Handler以及拦截器集合)给DispatcherServlet。
- DispatcherServlet接收到HandlerMapping返回的HandlerExcutorChain后,调用HandlerAdapter请求执行具体的Handler。
- HandlerAdapter经过适配调用具体的Handler(Controller即后端控制器)。
- Handler执行完成返回ModelAndView(其中包含逻辑视图和数据)给HandlerAdaptor。
- HandlerAdaptor再将ModelAndView返回给DispatcherServlet。
- DispatcherServlet请求视图解析器ViewReslover解析ModelAndView。
- ViewReslover解析后返回具体View(物理视图)到DispatcherServlet。
- DispatcherServlet使用Model中的数据对View进行渲染。
- DispatcherServlet将响应结果返回给用户
SpringMVC中的控制器是不是单例模式
控制器是单例模式,在多线程访问的时候有线程安全性问题。
Mybatis的Mapper接口调用
- Mapper.xml文件的namespace的值就是接口的全限定名
- Mapper接口方法名和Mapper.xml中定义的每个sql的id相同
- Mapper接口方法的输入参数和mapper.xml中定义的每个parameterType类型相同
- Mapper接口方法的输出参数和mapper.xml中定义的resultType类型相同
Mybatis的分页查询
Mybatis本身具有分页查询,但是不是真正的分页查询,它是把数据查出来放在内存中,你想要什么就给你什么。而我们使用Mybatis实现分页查询的时候,是要实现真分页,使用sql语句来来实现分页查询。
resultType和resultMap
两个属性都是用来做映射输出结果,当Sql查询出来的列与pojo实体一样的时候,使用resultType;当Sql查询出来的列于pojo实体不一样的时候,使用resultMap进行别名转换映射
#{}和${}
#{}表示占位符(jdbc中的?),可以有效防止sql注入。用PreparedStatement语句进行预编译,mybatis会进行必要的安全检查和转义
${}表示字符串拼接
Statement和PreparedStatement
执行sql的一般步骤:转换sql,编译sql,优化查询数据路径,返回数据
PreparedStatement会先初始化SQL,把SQL提交到数据库中进行预处理,多次使用可以提高效率,安全性好,有效防止sql注入
Statement不会初始化,没有预处理,每次都是重新开始执行sql
Mybatis中namespace的作用
为每一个映射文件起一个唯一的命名空间,这样定义在这个映射文件中的每个sql语句就成了定义在这个命名空间中的一个ID,只要我们能够保证每个命名空间中这个ID是唯一的,即使在不同映射文件中的语句ID相同,也不会产生冲突。
Mybatis中的动态sql
根据用户指定的条件动态生成sql语句。主要有:
- if
- choose / when / otherwise
- trim
- where
- set
- foreach
JSP内置对象
pageContext:作用域在当前页
request:作用域仅限于一次请求
session:作用域仅限于一次会话
application:作用域为整个项目
response:普通的response
out:JspWriter
config:ServletConfig
page:JSP翻译成的实例对象
exception:只有错误页面中才有
JSP指令
include:静态包含,把另外一个页面的所有内容拿到这个页面后再一起来执行
page:该页面的设置
taglib:导入标签
JSP动作标签
include:动态包含,先把另外一个页面执行,再把结果放到这个页面中
forward:请求转发,等同于 request.getRequestDispatcher(“xxx.jsp”).forward(request,response)
param:在跳转到另一个页面的时候,加入参数时用到
JSP和Serlvet
JSP是Servlet技术的拓展,本质上是Servlet,更加强调应用的外表表达,JSP编译后是servlet。
Servlet和JSP最主要的不同点在于,Servlet的应用逻辑在Java文件中,并且完全从表示层中HTML里分离出来。而JSP是Java和HTML的组合,一个.jsp拓展名的文件。JSP侧重于视图,Servlet侧重于控制逻辑。
Servlet
Servlet的生命周期可被定义成从创建到毁灭的整个过程。
Servlet调用init()方法进行初始化
Servlet调用service()方法来处理客户端的请求,多线程执行
Servlet调用destroy()方法终止
最后,servlet由JVM的垃圾收集器进行垃圾回收
Servlet是单例的,减少了Servlet的开销
Filter
过滤器是一个驻留在服务端的web组件,它可以截取客户端和服务端之间的请求与响应信息,并对这些信息进行过滤。当web容器接收到一个对资源的请求时,它将判断是否有过滤器与这个资源相关联。如果有,那么就把请求交给过滤器进行处理。当目标资源对请求做出响应的时候,web容器同样会将响应先转发给过滤器,然后再从过滤器交给客户端。
常见的过滤器用途:对用户的请求进行统一的认证、对用户的访问请求进行记录和审核、对用户发送的数据进行过滤或替换、对响应的内容进行压缩以减少传输量、对请求或响应进行加解密处理
Listener
监听器局势application、session、request三个对象创建、销毁或者修改删除属性时自动执行代码的功能组件
- SerlvetContextListener:对Servlet上下文的创建和销毁进行监听
- ServletContextAttributeListener:对Servlet上下文的修改、删除、添加属性进行监听
- HttpSessionListener:对Session的创建和销毁进行监听
- HttpSessionAttributeListener:对Session的修改、删除、添加属性进行监听
- HttpRequestListener:对Request的创建和销毁进行监听
- HttpRequestAttributeListener:对Request的修改、删除、添加属性进行监听
web.xml
web.xml用于配置Web应用的相关信息,如:监听器、过滤器、Servlet、相关参数、会话超时时间、安全验证方式、错误页面。
forward和redirect
forward:请求转发,地址栏不变,共享request中的数据,效率高
redirect:重定向,地址栏改变,不能共享数据,效率低
状态码
- 1XX : 通知,表示请求已接收,继续处理。
- 2XX : 成功,表示请求已被成功接收、理解、接受。
- 3XX : 重定向,表示要完成请求必须进行更进一步的操作
- 300 : multiple choices(可选重定向): 被请求的资源有一系列可供选择的反馈信息,由浏览器/用户自行选择其中一个。
- 301 : moved permanently(永久重定向): 该资源已被永久移动到新位置,将来任何对该资源的访问都要使用本响应返回的若干个URI之一。
- 302 : move temporarily (临时重定向) : 请求的资源现在临时从不同的URI中获得
- 303 : See Other : 对应当前请求的响应可以在另一个 URL 上被找到,而且客户端应当采用 GET 的方式访问那个资源
- 304 : not modified : 如果客户端发送一个待条件的GET请求并且该请求以经被允许,而文档内容未被改变,则返回304,该响应不包含包体(即可直接使用缓存)。
- 4XX : 客户端错误,表示请求有语法错误或请求无法实现。
- 5XX : 服务端错误,表示服务器未能实现合法的请求。
get和post
- 在客户端,Get方式通过url提交数据,数据url中可以看到;Post方式将表单中的数据放在头信息中提交
- Get方式提交的数据最多只有 1024字节;Post没有此限制
- Get用来向服务器获取资源;Post用来向服务器提交数据
BIO、NIO、AIO
BIO:Block IO,同步阻塞式IO,就是我们传统上的IO,它的特点是模式简单使用方便,并发处理能力低
NIO:New IO,同步非阻塞式IO,是传统IO的升级,客户端和服务端通过Channel(管道)通讯,实现了多路复用
AIO:Asynchronous IO,异步非阻塞IO,是NIO的升级,异步IO的操作基于事件和回调机制
Mysql
数据库存储引擎
InnoDB
聚集索引,支持事务,支持外键,支持行级锁
MyISAM
非聚集索引,不支持事务,只支持表级锁。
总结:MyISAM适合读密集的表,而InnoDB适合写密集的表。在数据库做主从分离的情况下,经常选择MyISAM作主库的存储引擎。
为什么使用索引
- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性
- 可以大大加快数据的检索速度
- 帮助服务器避免排序和临时表
- 将随机IO变为顺序IO
- 可以加快表与表之间的连接
为什么不为表中每一列创建索引
- 当对表中的数据进行增加、修改、删除的时候,也会动态维护索引,这样会降低维护速度
- 索引要占用物理空间
- 创建索引和维护索引消耗时间,这种消耗会随着数据的增加而增加
索引是如何提高查迅速的
将无序的数据变为有序的数据(如同书的目录一样)
底层结构用的是B+树
一些关键字会导致索引失效:or、!=、not in、is null、is not null
数据库的隔离级别
未提交读
一个事物读到了另外一个事务还未提交的数据(引发问题:【脏读】)
已提交读
一个事务读到了另外一个事务已提交的数据,造成前后两次读到的数据不一致,也就是不能执行多次读取,否则会出现结果不一致(解决问题:【脏读】,引发问题:【不可重复读】)
可重复读
可以让事务在自己的会话中重复读取数据,并且不会发生结果不一致的问题,即使数据已经提交了,也依然还是显示以前的数据(确保数据不受其他事务的影响)(解决问题:【脏读、不可重复读】)
可串行化
最高的隔离级别(解决【脏读、不可重复读、幻读】问题),把事务串在一起先后执行,如果一个连接的隔离级别设置了串行化,那么谁先打开事务,谁就有先执行的权力,,后打开事务的,就只能等前面的事务提交或回滚后才能执行。但是容易造成性能问题,效率低下。
MySQL单表数据过大时的优化
- 限定数据的范围。禁止不带任何限制数据范围条件的查询语句。
- 读/写分离。主库负责写,从库负责读
- 垂直分区。将单个表按照不同的模块(相关性)进行拆分。比如,用户表中有登陆信息和基本信息,那么可以拆分为两个单独的表。简单来说,垂直拆分就是将数据表按列进行拆分。优点:使得行数据变小;简化表结构。缺点:主键出现冗余;事务更加复杂。
- 水平分区:保持数据库表的结构不变,通过某种策略进行分片。
数据库的乐观锁和悲观锁
悲观锁
每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
Java的Synchronized就属于悲观锁,每次线程要修改数据时都要先获得锁,保证同一时刻只有一个线程能够操作数据,其他线程会被阻塞。
乐观锁
每次拿数据的时候认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有更新这个数据。
假设不会发生并发冲突,只在提交操作的时候检查是否违反数据完整性
乐观锁的实现方式:
-
使用数据版本记录机制实现。为数据库表增加一个数字类型的version字段,当读取数据时,将version字段的值一同读取,数据每更新一次,对此version字段加一。当我们提交更新的时候,判断数据库表对应记录中的当前版本信息和第一次取出来的version值进行比较,如果相等,就更新,如果不相等,就认为是过期数据
-
使用时间戳。在表中增加一个时间戳的字段,和version差不多,也是在更新的时候检查当前数据库中的时间戳和自己更新取到的时间戳进行对比,如果一致就更新,不一致就不更新
数据库ACID
原子性:事务是一个不可分割的工作单位
一致性:事务前后的数据完整性保持一致
隔离性:多个用户并发访问数据库时,一个用户的事务不能被其他事务干扰,多个事务之间要相互隔离
持久性:一个事务一旦提交,它对数据库中数据的改变就是永久的,即使发生故障也不会对其有任何影响
MySql的主从复制
用途:
- 作为数据库备份
- 读写分离。主数据库进行实时的数据库操作,从数据库进行读取操作
- 增加多个数据存储节点,将负载分布在多个从节点上,降低单机磁盘I/O访问的频率,提高单个机器的I/O性能。
步骤:
- slave的IO线程连接上master,并请求指定日志文件的指定位置之后的日志内容。
- master接收到来自slave的IO线程的请求后,负责复制的IO线程读取指定日志文件的指定位置之后的日志内容返回给slave的IO线程。返回信息除了日志文件所包含的信息之外,还包括返回本次的信息的bin-log文件名称和文件位置。
- slave的IO线程接收到来自信息后,将接收到的日志内容一次添加到slave的relay-log文件的最末端,并将读取到的master的bin-log的文件名和bin-log文件位置添加到master-info文件中,以便下次使用。
- slave的sql线程检测到relay-log中新增了内容之后,会马上解析relay-log的内容,成为真实执行时候的可执行任务,并在自身执行。
left join和right join
left join:左联接,返回左表中的完整信息,右表中有关联的数据,没有就显示null
right join:右联接,返回右表中完整信息,左表中有关联的数据,没有就显示null
Statement和PreparedStatement
- PreparedStatement接口代表预编译的语句,它主要的优势在于可以减少SQL的编译错误并增加SQL的安全性
- PreparedStatement中的SQL语句是可以带参数的,避免了用字符串拼接SQL语句麻烦和不安全
- 当批量处理SQL或频繁执行相同的查询时,PreparedStatement有明显的性能上的优势,由于数据库可以将编译好的SQL语句缓存起来,下次执行相同结构的语句时就会很快
XML文档定义有几种形式?解析XML文档有几种形式?
文档定义的两种形式:
- dtd:数据类型定义,用以描述XML文档的文档结构。
- scheme:其本身是基于scheme编写的,在类型和语法上的限定能力比dtd强,处理也比较方便。本质上是XML,可以被XML解析器解析
解析XML文档的方法:
- DOM:处理大型文件时其性能下降的非常厉害。这个问题是由DOM的树型结构造成的,这种结构占用的内存较多,而且DOM必须在解析文件之前把整个文档装入内存,适合对XML的随机访问。
- SAX:SAX是事件驱动型的XML解析方式。它顺序读取XML文件,不需要一次全部装载整个文件。适合对XML的顺序访问。
- JDOM:JDOM采用了Java中的Collection架构来封装集合
- DOM4J:xml解析器一次性把整个xml文档加载进内存,然后在内存中构建一颗Document的对象树,通过Document对象,得到树上的节点对象,通过节点对象访问(操作)到xml文档的内容
Redis
RDB和AOF
RDB:在指定的时间间隔内将内存中的数据集快照写入磁盘,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
AOF:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。
Redis的主从复制
第一阶段:slave与master建立连接
第二阶段:slave向master发起同步请求
第三阶段:接收master发来的RDB数据
第四阶段:载入RDB文件
redis读写速度快
Redis是纯内存数据库,相对于读写磁盘速度快了很多。
多路复用I/O:多路指多个网络连接,复用指复用同一个线程。采用多路复用IO技术可以让单个线程高效的处理多个连接请求。可以直接理解为:单线程的原子操作,避免上下文切换的时间和性能消耗;加上对内存中数据的处理速度,redis自然就快了
缓存的优点
减少对数据库的操作,数据库的压力降低
提高了响应速度
消息队列
什么是消息队列
消息队列是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。
为什么使用消息队列
- 通过异步处理的方式提高系统性能
- 降低系统耦合性
使用消息队列带来的一些问题
- 系统可用性降低:需要考虑消息的丢失等问题
- 系统复杂性提高:需要保证消息没被重复消费、处理消息丢失的情况、保证消息传递的顺序性等问题
- 一致性问题:消息的真正消费者没有正确消费消息,导致数据不一致
JMS
JMS是Java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。JMS API是一个消息服务的标准或规范,允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。ActiveMQ是基于JMS规范实现的。
JMS的两种消息模型:
- 点到点(P2P)模型:一条消息只能被一个消费者使用
- 发布/订阅(Pub/Sub)模型:发布者发布一条消息,该消息通过主题传递给所有的订阅者,发布消息后才订阅是收不到之前的消息的
AMQP
提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计,兼容JMS。RabbitMQ是基于AMQP协议实现的。
AMQP的五种模型:
- direct exchange
- fanout exchange
- topic exchange
- headers exchange
- system exchange
操作系统
Linux的文件系统
在Linux中,所有的被操作系统管理的资源都是文件,包括硬件资源。一切皆文件
Linux支持的5种文件类型:
- 普通文件:用来辅助存储设备上存储信息和数据
- 目录文件:系统中的各级目录
- 链接文件:用于不同目录下的文件共享
- 设备文件:用来访问硬件设备
- 命名管道:进程之间的通信通过该文件完成
常见目录说明:
- /bin:存放二进制可执行文件(命令)
- /etc:存放系统管理和配置文件
- /home:存放所有用户文件的根目录
- /usr:用于存放系统应用程序
- /opt:额外安装的可选应用程序
- /proc:虚拟文件系统目录,是系统内存的映射。可直接访问这个目录获取系统信息
- /root:超级用户的根目录
- /sbin:存放二进制可执行文件,系统管理级别的命令
- /dev:用于存放设备文件
- /mnt:用户临时挂载其他的文件系统的目录
- /boot:存放用于系统引导的文件
- /lib:存放用于系统运行的库文件
- /tmp:用于存放临时文件
- /var:用于存放运行时需要改变数据的文件(日志文件)
- /lost+found:系统非正常关机而留下的文件
Linux常见命令
- 目录切换命令:cd
- 目录的操作命令:
- mkdir 目录名称:增加目录
- ls或ll:查看目录信息
- find 目录 参数:查找目录
- mv 目录名称 新目录名称:修改目录名称
- mv 目录名称 目录的新位置:移动目录的位置
- cp -r 目录名称 目标拷贝的目标位置:拷贝目录 -r表示递归拷贝
- rm -rf 目录:删除目录
- 文件操作命令:
- touch 文件名称:文件的创建
- cat 文件名称:查看最后一屏的内容
- more 文件名称:可以显示百分比,回车下一行,空格下一页,q退出查看
- less 文件名称:可以向上和向下翻页,q结束查看
- tail -n 文件名称:查看文件的最后n行,Ctrl+C退出查看
- vim 文件名称:修改文件中的内容
- 压缩文件的操作命令:
- tar -zcvf 打包压缩后的文件名 要打包压缩的文件:打包压缩
- z:调用gzip压缩命令进行压缩
- c:打包文件
- v:显示运行过程
- f:指定文件名
- tar -xvf 压缩文件:解压压缩包
- x:代表解压
- tar -zcvf 打包压缩后的文件名 要打包压缩的文件:打包压缩
- 其他常用命令:
- pwd:显示当前所在的位置
- grep 要搜索的字符串 要搜索的文件 --color:搜索命令,–color表示高亮显示
- ps -ef / ps aux:查看当前正在运行的进程
- kill pid:杀死进程
- ifconfig:查看当前系统的网卡信息
- ping:查看与某台主机的连接情况
- netstat -an:查看当前系统的端口使用
- shutdown:关机
- reboot:重启
死锁的发生条件
- 互斥条件:一个资源一次只能被一个进程访问
- 请求与保持条件:一个进程因请求资源而被阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已经获得的资源,在未使用完之前不能强行剥夺
- 循环等待条件:若干个资源形成一种头尾相连的循环等待资源关系
解决算法:银行家算法
系统如何提高并发性
-
提高CPU并发计算能力
- 多线程
- 减少进程切换,使用线程
- 较少使用不必要的锁
- 考虑进程优先级
- 关注系统负载
-
改进IO模型
- DMA技术
- 异步IO
- 改进多路IO
- Spendfile
- 内存映射
- 直接IO
计算机网络
网络层
ARP
地址解析协议。解决同一个局域网上的主机或路由器的IP地址和硬件地址的映射问题。
ICMP
网际控制报文协议。允许主机或路由器报告差错情况和提供有关异常情况的报告。
因特网的路由选择协议
运输层
TCP和UDP
TCP提供面向连接的服务。在传数据之前必须要先建立连接,数据传输完后要释放连接。TCP不提供广播和多播服务。传输的是字节流。由于面向连接,所以不可避免地增加了许多开销。TCP一般用于传输文件、发送和接收邮件、远程登陆。
UDP在传输数据之前不需要进行连接,远地主机接收到UDP报文后不需要给出任何确认。传输的是数据段报文。UDP是一种不可靠的传输方式。一般用于视频对话、直播。
TCP的三次握手
- 客户端向服务端发送带有SYN标志的数据包(SYN=1,seq=x)第一次握手
- 服务端向客户端发送带有SYN和ACK标志的数据包(SYN=1,ACK=1,seq=y,ack=x+1)第二次握手
- 客户端向服务端发送带有ACK标志ide数据包(ACK=1,seq=x+1,ack=y+1)第三次握手
为什么三次握手
三次握手的目的是为了建立可靠的传输,也就是数据的发送和接收,而三次握手的目的最主要就是为了确认双方的接收和发送能力是否正常
第一次握手,Client什么都不能确认;Server确认对方发送正常,自己接收正常
第二次握手,Client确认自己发送、接收正常,对方发送、接收正常;Server确认自己接收正常,对方发送正常
第三次握手,Client确认自己发送、接收正常,对方发送、接收正常;Server确认自己发送、接收正常,对方发送、接收正常
TCP的四次挥手
TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN只是意味着这一方向上没有数据流动,一个TCP收到FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,另一方执行被动关闭。
- 客户端向服务端发送带有FIN标志的数据包(FIN=1,seq=u)用来关闭客户端到服务端的数据传送
- 服务端接收到这个数据包后,发回一个ACK标志的数据包(ACK=1,seq=v,ack=u+1)
- 服务端向客户端发送带有FIN标识的数据包(FIN=1,ACK=1,seq=w,ack=u+1)用来关闭服务端到客户端的数据传送
- 客户端返回一个ACK报文(ACK=1,seq=u+1,ack=w+1)确认
TCP为什么是可靠的
三次握手、超时重传、滑动窗口、拥塞控制
应用层
DNS
当DNS客户机需要在程序中使用名称时,它会查询DNS服务器来解析该名称。客户机发送的每条查询信息包括三条信息:指定的DNS域名,指定的查询类型,DNS域名的指定类别。基于UDP服务,端口53。该应用一般不直接为用户使用,而是为其他应用服务,如HTTP,SMTP等在其中需要完成主机名到IP地址的转换。
HTTP
超文本传输协议,是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。
STMP
FTP
在浏览器输入url地址到显示主页的全过程
- DNS解析
- TCP连接
- 发送HTTP请求
- 服务器处理请求并返回HTTP报文
- 浏览器解析渲染页面
- 连接结束
常见排序
选择排序
public class SelectionSort {
private void swap(int i,int j,int[] arr){
int tem = arr[j];
arr[j] = arr[i];
arr[i] = tem;
}
public void selectionSort(int[] arr){
if(arr == null||arr.length < 2 ){
return;
}
int length = arr.length;
for(int i = 0;i < length - 1;i++){
int minIndex = i;
for(int j = i+1;j < length;j++){
//每一轮中找出最小值的下标
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(minIndex,i,arr);
}
}
}
冒泡排序
public class BubbleSort {
private void swap(int i,int j,int[] arr){
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
public void bubbleSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for(int i = arr.length - 1;i > 0;i--){
for(int j = 0;j < i;j++){
if (arr[j] > arr[j+1]){
swap(j,j+1,arr);
}
}
}
}
}
插入排序
public class InsertionSort {
private void swap(int i,int j,int[] arr){
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
public void insertionSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for(int i = 1;i < arr.length;i++){
//保证前面i个数是有序的
for(int j = i - 1;j >= 0 && arr[j] > arr[j + 1];j--){
swap(j+1,j,arr);
}
}
}
}
快速排序
public class QuickSort {
private void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public void quickSort(int[] arr){
if (arr == null || arr.length < 2){
return;
}
quickSort(arr,0,arr.length-1);
}
private void quickSort(int[] arr,int L,int R){
if(L < R){
int[] p = partition(arr,L,R);
quickSort(arr,L,p[0] - 1);
quickSort(arr,p[1] + 1,R);
}
}
private int[] partition(int[] arr,int L,int R){
int less = L - 1;
int more = R + 1;
int cur = L;
int tem = arr[R];
while(cur < more){
if(arr[cur] < tem){
swap(arr,++less,cur++);
}else if(arr[cur] > tem){
swap(arr,--more,cur);
}else{
cur++;
}
}
return new int[] {less + 1,more - 1};
}
}
归并排序
public class MergeSort {
public void mergeSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
mergeSort(arr,0,arr.length - 1);
}
private void mergeSort(int[] arr,int left,int right){
if(left == right){
return;
}
int mid = left + (right - left)/2;
mergeSort(arr,left,mid);
mergeSort(arr,mid+1,right);
merge(arr,left,mid,right);
}
private void merge(int[] arr,int left,int mid,int right){
int[] help = new int[right - left + 1];
int i = 0;
int p1 = left;
int p2 = mid + 1;
while(p1 <= mid && p2 <= right){
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1 <= mid){
help[i++] = arr[p1++];
}
while(p2 <= right){
help[i++] = arr[p2++];
}
for (i = 0; i < help.length;i++) {
arr[left + i] = help[i];
}
}
}
堆排序
public class HeapSort {
public void heapSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
//构建大根堆
for(int i = 0;i < arr.length;i++){
heapInsert(arr,i);
}
//将根堆的和最后一个数交换,并对前面的大根堆重新进行构建
int size = arr.length;
swap(arr,0,--size);
while(size > 0){
heapify(arr,0,size);
swap(arr,0,--size);
}
}
private void heapInsert(int[] arr,int index){
while(arr[index] > arr[(index - 1) / 2]){
swap(arr,index,(index - 1) / 2);
index = (index - 1) / 2;
}
}
private void heapify(int[] arr,int index,int size){
int left = index * 2 + 1;
while(left < size){
int largest = left + 1 < size && arr[left] < arr[left + 1] ? left + 1 : left;
largest = arr[index] < arr[largest] ? largest : index;
if(largest == index){
break;
}
swap(arr,index,largest);
index = largest;
left = index * 2 + 1;
}
}
private void swap(int[] arr,int i,int j){
int tem = arr[i];
arr[i] = arr[j];
arr[j] = tem;
}
}
System.out.println(3|9)输出什么?
| 和 ||:两者都可做逻辑运算符。他们都表示只要有任意一边为true,结果就为true
| 也是为运算符。| 表示两边都会算,然后再判断结果;|| 表示先运算左边,然后判断是否为true,是true就停下来,是false就继续算右边的。
3 | 9 = 0011 | 1001 = 1011 = 11
说以下转发(forward)和重定向(redirect)
转发是服务器行为,重定向是客户端行为。
转发通过RequestDispatcher对象的forward方法实现的,RequestDispatcher可以通过HttpServletRequest的getRequestDispatcher方法获得
下面这个例子就是一个转发
request.getRequestDispatcher("test.jsp").forward(request,response);
重定向是利用服务器返回的状态码实现的。客户端浏览器请求服务器的时候,服务器返回一个状态码。服务器通过HttpServletResponse的setStatus方法设置状态码。如果服务器返回301或302,则浏览器就会返回到新的网址重新请求资源
- 从地址栏显示来说:forward是服务器请求资源,服务器直接返回访问目标的URL,把内容读取出来,然后把这些内容再发给浏览器。浏览器根本不知道服务器发送的内容从哪里来,所以它的地址栏还是原来的地址。redirect是服务端根据逻辑发送一个状态码,告诉浏览器重新去请求哪个地址,所以地址栏显示新的地址
- 从数据共享来说:forward转发页面和转发到的页面可以共享request里的数据。redirect不能共享数据
- 从运用地方来说:forward一般用于用户登陆的时候,根据角色转发到相应的模块。redirect一般用于用户注销的时候,返回到主页面或其他页面
- 从效率来说:forward的效率高。redirect的效率低
从浏览器中输入url到显示主页的过程会使用哪些协议
- 浏览器查找域名的IP地址:DNS
- TCP连接
- 发送HTTP请求
- 服务器处理请求并返回HTTP报文
- 浏览器渲染解析页面
- 连接结束
DNS、TCP、IP(网络层)、OPSF(路由器)、ARP(将IP转化为MAC地址)、HTTP
TCP三次握手和四次挥手
三次握手
- 客户端向服务端发送带有SYN标志的数据包(SYN=1,seq=x)
- 服务端向客户端发送带有SYN/ACK标志的数据包(SYN=1,ACK=1,seq=y,ack=x+1)
- 客户端向服务端发送带有ACK标志的数据包(ACK=1,seq=x+1,ack=y+1)
为什么要进行三次握手
进行三次握手是为了确认双方的接收和发送功能是否完好。
第一次握手,客户端无法确定任何事,服务端可以确认对方发送、自己接收正常
第二次握手,客户端可以确认自己发送、接收正常,对方发送、接收正常;服务端可以确认对方发送、自己接收正常
第三次握手,客户端可以确认自己发送、接收正常,对方发送、接收正常;服务端可以确认对方发送、接收正常,自己发送、接收正常
四次挥手
TCP是全双工的,因此每个方向都需要进行单独的关闭
- 客户端向服务端发送带有FIN标志的数据包
- 服务端向服务端发送带有ACK标志的数据包
- 服务端向客户端发送带有FIN标志的数据包
- 客户端向服务端发送带有ACK标志的数据包
IP地址和MAC地址的区别
IP地址是互联网协议地址。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址用来屏蔽物理地址的差异
MAC地址又被称为物理地址、硬件地址,用来定义网络设备的位置。网卡的物理地址通常是网卡生产厂家写入网卡的,具有全球唯一性。MAC地址用于网络中唯一标识一个网卡,一台电脑会有一或多个网卡,每个网卡都需要一个唯一的MAC地址。
HTTP请求、响应报文格式
HTTP请求报文主要由请求行、请求头部、请求正文3部分组成
HTTP响应报文主要有状态行、响应头部、相应正文3部分组成
为什么要使用索引?为什么不对表中的每一个列创建一个索引?索引是如何提高查询速度的?索引有哪些注意事项?Mysql索引主要使用的两种数据结构?什么是覆盖索引?
为什么使用索引?
- 通过创建唯一性索引,可以保证表中的每一行数据都具有唯一性
- 可以加快数据的检索速度
- 帮助服务器避免排序和临时表
- 将随机IO变为顺序IO
- 可以加快表和表之间的连接
为什么不对表中的每一列创建索引
- 当对表中的数据进行增加、删除、修改的时候,索引也要动态的维护,这样就降低了数据的维护速度
- 索引需要占用物理空间
- 创建和维护索引要消耗时间,这种时间消耗随着数据量的增加而增加
索引是如何提高查询速度的
将无序的数据变为相对有序的数据(就像查目录一样)
使用索引的注意事项
- 避免where子句对字段施加函数,这样会造成无法命中索引
- 在使用InnoDB时使用与业务无关的自增主键作为主键,即逻辑主键,而不要使用业务主键
- 将打算加索引的列设为NOT NULL,否则将导致引擎放弃使用索引而改用全表扫描
- 删除长期未使用的索引
- 在使用limit offset查询速度慢时,可以借助索引来提高性能
Mysql索引主要使用的两种数据结构
哈希索引:底层的数据结构就是哈希表
BTree索引:底层数据结构是B+Tree
覆盖索引
如果一个索引包含所有需要查询的字段的值,我们称之为“覆盖索引”。我们知道在InnoDB存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是通过主键再查找一次,这样就会比较慢。覆盖索引就是把要查询出的列和索引对应的,不做回表操作。
进程和线程的区别是什么?进程间的通信方式?线程间的通信方式?
进程和线程的区别
线程和进程相似,但线程是比进程更小的执行单位。一个进程再执行过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统产生一个线程或在各个线程间切换工作时,负担要比进程小。也正因为是共享资源,所以线程执行时一般都要进行同步和互斥。
进程的通信方式
- 管道(pipe):管道时一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。进程的血缘关系指父子进程关系。管道分为pipe(无名管道)和fifo(命名管道)两种。命名管道也是半双工的通信方式,但是它允许无亲缘关系进程通信
- 信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止进程在访问某个资源的时候,其他进程也访问该共享资源。
- 消息队列(message queue):消息队列是由信息组成的链表,存放在内核中,并由消息队列标识符标记。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
- 信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生。
- 共享内存(shared memory):共享内存就是映射一段能被其他进程访问的内存,这段共享内存由一个进程创建,但多个进程可以访问,共享内存是最快的IPC方式,它是针对其他进程间的通信方式进行效率低而专门设计的。它往往与其他通信机制配合使用,来实现进程间的同步和通信
- 套接字(socket):客户/服务器系统的开发工作既可以在本机上进行,也可以跨网络进行。
线程间的通信方式
- 锁机制
- 互斥锁:提供了以排它方式阻止数据结构被并发修改的方法
- 读写锁:允许多个线程同时读共享数据,而对写操作互斥
- 条件变量:可以以原子的方式阻塞线程,知道某个条件为真为止
- 信号量机制:无名线程信号量和有名线程信号量
- 信号机制:类似于进程间的信号处理
为什么要使用单例模式
对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,减少了一笔很不错的系统开销
由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC压力,缩短GC停顿时间
双重检查锁
public class Singleton{
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
静态内部类
public class Singleton{
private static class SingletonHolder{
private static final Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton getIntance(){
return SingletonHolder.instance;
}
}
简单介绍以下什么是bean,Spring的bean的作用域和生命周期
在Spring中,那些组成应用程序的主体及由IoC容器管理的对象,被称为bean。简单来说,bean就是由IOC容器初始化、装配、及管理的对象,除此之外,bean就与应用程序中的其他对象没有什么区别了。而bean的定义和依赖关系将通过配置元数据来描述。Spring中的bean默认是单例的。
- singleton:单例
- prototype:多例
- request:不同的request不同的bean
- session:不同session不同的bean
- globalSession
Spring中的事务传播行为,TransactionDefinition接口中5个隔离级别常量
事务传播行为
事务传播行为:当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。
支持当前事务的情况:
- PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果没有,就创建一个新的事务
- PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果没有,就以非事务的方式继续运行
- PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果没有,就抛出异常
不支持当前事务的情况:
- PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,就把当前的事务挂起
- PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,就把当前的事务挂起
- PROPAGATION_NEVER:以非事物方式运行,如果当前存在事务,则抛出异常
其他情况:
- PROPAGATION_NESTED:如果当前存在事务,则创建一个新的事务作为当前事物的嵌套事务来执行;如果当前没有事务,则创建一个新的事务来执行
隔离级别
- ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认READ,Oracle默认COMMITED
- ISOLATION_READ_UNCOMMITED:允许读取尚未提交的数据
- ISOLATION_READ_COMMITED:允许读取已经提交的数据
- ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果是一样的
- ISOLATION_SERIALIZABLE:串行化,最高的隔离级别,所有的事务依次执行
SpringMVC原理
- 用户发送请求,DispatcherServlet接收请求
- DispatcherServlet调用HandlerMapping查询Handler
- HandlerMapping找到具体的Handler,返回HandlerExcutorChain(包含了Handler以及拦截器集合)给DispatcherServlet。
- DispatcherServlet接收到HandlerMapping返回的HandlerExcutorChain后,调用HandlerAdapter请求执行具体的Handler。
- HandlerAdapter经过适配调用具体的Handler(Controller即后端控制器)。
- Handler执行完成返回ModelAndView(其中包含逻辑视图和数据)给HandlerAdaptor。
- HandlerAdaptor再将ModelAndView返回给DispatcherServlet。
- DispatcherServlet请求视图解析器ViewReslover解析ModelAndView。
- ViewReslover解析后返回具体View(物理视图)到DispatcherServlet。
- DispatcherServlet使用Model中的数据对View进行渲染。
- DispatcherServlet将响应结果返回给用户
IOC和AOP实现原理
控制反转(IOC):在系统运行时,动态的向某个对象提供它所需要的其他对象。对组件对象控制权的转移就是控制反转,从程序代码本身转移到了外部容器,由容器来创建对象并管理对象之间的依赖关系。利用反射机制
具体做法:
- 将bean之间的依赖关系转化为关联关系
- 将对具体类的关联尽可能地转化为对Java Interface的关联
- bean实例具体关联相关Java Interface的哪个实现类的实例,在配置信息的元数据中描述
- 由IoC组件(容器)根据配置信息,实例化具体的bean类,将bean之间的依赖关系注入进来
面向切面编程(AOP),用于处理系统中分布于各个模块的横切关注点,比如日志处理、事务管理、权限。AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理分为静态代理和动态代理。利用代理模式
静态代理:AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,代表为AspectJ
动态代理:
- JDK动态代理:通过反射来接收被代理的对象,并且被代理的类必须实现一个接口
- Cglib动态代理:通过继承实现动态代理,不需要实现接口,但是如果是final,那么无法使用Cglib动态代理。