2021秋-面试整理
面试
java基础
String
String为什么要设置为final的
-
便于实现字符串常量池
java中由于会大量的使用字符串常量,如果每次使用字符串对象都去堆空间中创建对象的话,会造成空间资源的浪费,所以在堆中开辟了一块字符串常量池空间,当初始化一个字符串对象时,如果常量池中有该字符串,则直接返回常量池中的字符串对象,不再创建新的对象,但如果字符串是可以改变的,字符串改变后,其指向也会发生改变,就无法实现字符串的常量池
-
支持多线程安全
在多线程中,只有不变的对象和值才是安全的,可以在多线程中共享数据,由于String的不可变,当一个线程修改了字符串的值,只会产生一个新的字符串对象,不会对其他线程的访问产生问题。
-
可以缓存 hash 值
String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
String加法
- JDK5之前:String加法转换为StringBuffer对象的连续append()操作
- JDK5及以后:String加法转换为StringBuilder对象的连续append()操作
haspMap
负载因子
- 假如负载因子定为1(最大值),那么只有当元素填满组长度的时候才会选择去扩容,虽然负载因子定为1可以最大程度的提高空间的利用率,但是会增加hash碰撞,以此可能会增加链表长度,因此查询效率会变得低下(因为链表查询比较慢)。hash表默认数组长度为16,好的情况下就是16个空间刚好一个坑一个,但是大多情况下是没有这么好的情况。
- 加入负载因子定为0.5(一个比较小的值),也就是说,直到到达数组空间的一半的时候就会去扩容。虽然说负载因子比较小可以最大可能的降低hash冲突,链表的长度也会越少,但是空间浪费会比较大。
2的次方的原因
- 扩容也是以2的次方进行扩容,是因为2的次方的数的二进制是10..0,在二的次方数进行减1操作之后,二进制都是11...1,那么和hashcode进行与操作时,数组中的每一个空间都可能被使用到。
- 如果不是2的次方,比如数组长度为17,那么17的二进制是10001,在indexFor方法中,进行减1操作为16,16的二进制是10000,随着进行与操作,很明显,地址二进制数末尾为1的空间,不会得到使用,比如地址为10001,10011,11011这些地址空间永远不会得到使用。因此就会造成大量的空间浪费。
- 负载因子是0.75即3/4,又因为数组长度为2的次方,那么相乘得到的扩容临界值必定是整数,这样更加方便获得一个方便操作的扩容临界值。
到8个元素时构建红黑树
-
利用泊松分布计算出当链表长度大于等于8时,几率很小很小
当put进来一个元素,通过hash算法,然后最后定位到同一个桶(链表)的概率会随着链表的长度的增加而减少,当这个链表长度为8的时候,这个概率几乎接近于0,所以我们才会将链表转红黑树的临界值定为8。
-
为什么不上来就用红黑树
- 因为经过泊松定律知道,一个在负载因子为0.75时,出现的hash冲突,在一个桶中的链表长度大于8的几率是很少很少几乎为0,如果一来就使用红黑树,由于增删频繁,从而会调整树的结构,反而增加了负担,浪费时间,而直接使用链表增删反而比红黑树快很多,因此为了增加效率,而只是在长度大于8时使用红黑树。
-
为什么选择红黑树不是AVL树
- 红黑树,在时间复杂度上与AVL树相持平,但是在调整树上没有AVL树严苛,它允许局部很少的不完全平衡,但最长路径不会超过最短路径的2倍,这样以来,最多只需要旋转3次就可以使其达到平衡,调整时间花费较少。
- 在JUC中有一个
CurrentHashMap
类,该类为线程同步的hashmap类,当高并发时,需要在意的是时间,由于AVL树在调整树上花费的时间相对较多,因此在调整树的过程中,其他线程需要等待的时间就会增长,这样导致效率降低,所以会选择红黑树。
扩容
- HashMap在扩容到过程中不仅要对其容量进行扩充,还需要进行rehash!所以,这个过程其实是很耗时的,并且Map中元素越多越耗时
- rehash的过程相当于对其中所有的元素重新做一遍hash,重新计算要分配到那个桶中。
- 扩容临界值=负载因子
*
容量
哈希
- 位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。之所以可以做等价代替,前提是要求HashMap的容量一定要是2^n 。
- return h & (length-1)
初始容量
- 默认情况下HashMap的容量是16
- 如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。
- initialCapacity = (需要存储的元素个数 / 负载因子)+ 1
put的流程
- put:往map插入元素的时候首先通过对key hash然后与数组长度-1进行与运算((n-1)&hash),都是2的次幂所以等同于取模,但是位运算的效率更高。找到数组中的位置之后,如果数组中没有元素直接存入,反之则判断key是否相同,key相同就覆盖,否则就会插入到链表的尾部,如果链表的长度超过8,则会转换成红黑树,最后判断数组长度是否超过默认的长度*负载因子也就是12,超过则进行扩容。
头插和尾插
- 头插
- 在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。
- 头插原因:局部性原理,被访问到的东西,有很大概率在不久时间内会被再次访问到。
- 尾插
- 无法保证上一秒put的值下一秒get的时候还是原值,所以线程安全还是无法保证。
TreeMap和LinkedHashMap
-
LinkedHashMap
- 事实上LinkedHashMap是HashMap的直接子类,二者唯一的区别是LinkedHashMap在HashMap的基础上,采用双向链表的形式将所有
entry
连接起来,这样是为保证元素的迭代顺序跟插入顺序相同。 - 迭代LinkedHashMap时不需要像HashMap那样遍历整个
table
,而只需要直接遍历header
指向的双向链表即可,也就是说LinkedHashMap的迭代时间就只跟entry
的个数相关,而跟table
的大小无关。 - LRU机制:LinkedHashMap有一个子类方法
protected boolean removeEldestEntry(Map.Entry<K,V> eldest)
,该方法的作用是告诉Map是否要删除“最老”的Entry,所谓最老就是当前Map中最早插入的Entry,如果该方法返回true
,最老的那个元素就会被删除。
- 事实上LinkedHashMap是HashMap的直接子类,二者唯一的区别是LinkedHashMap在HashMap的基础上,采用双向链表的形式将所有
-
TreeMap
- TreeMap实现了SortedMap接口,会按照
key
的大小顺序对Map中的元素进行排序 - TreeMap底层通过红黑树(Red-Black tree)实现
- TreeMap实现了SortedMap接口,会按照
四种访问修饰符的区别
- protected不能修饰类。
- priavte可以修饰类,但是该类必须是一个内部类。
- 接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。
抽象类和接口的区别
- 抽象类是单继承,接口可以多实现
- 抽象类可以有构造方法,接口没有构造方法
- 抽象类可以有普通成员变量,接口没有普通成员变量
- 抽象类和接口都可有静态成员变量,抽象类中静态成员变量访问类型任意,接口只能public static final(默认)
- 抽象类可以没有抽象方法,抽象类可以有普通方法,接口中都是抽象方法
- 抽象类可以有静态方法,接口不能有静态方法
- 抽象类中的方法可以是public、protected;接口方法只有public abstract
hashCode和equals的区别
- hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。
- 等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。
- 重写equals必须重写hashCode
异常
- 异常主要分为Error和Exception,语法错误不属于异常
- Error:java虚拟机无法解决的严重问题
- Exception:可以使用try-catch解决
- 编译时异常、受检异常(checked)
- IOException
- ClassNotFoundException
- 运行时异常(unchecked)
- NullPointException
- ArrayIndexOutOfBoundsException
- 编译时异常、受检异常(checked)
泛型
- 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
- 泛型只在程序源代码中存在,在编译后的字节码中,泛型都被替换为了原来的裸类型,并在访问和修改时,自动插入强制转换代码
- 裸类型应被视为所有该类型泛型化实例的共同父类型
- 使用泛型的好处
- 它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。
- 擦除式泛型几乎只需要在Javac编译器上做出改进即可,不需要改动字节码,不需要改动Java虚拟机,保证了以前没有使用泛型的库可以直接运行在Java 5.0 之上
- 存在的问题
- 不支持原始类型作为泛型,需要使用包装类,会有自动拆箱装箱的开销
- 对于形参列表使用不同泛型区分的函数,不能构成重载,不能通过编译,因为类型擦除导致两个方法的特征签名一模一样
- 对于形参列表使用不同泛型区分的,带有返回值的函数,能够通过编译,因为两个方法具有相同的名称和特征签名,但是返回值不同,他们可以合法的共存于一个Class文件中
反射
-
概念
- 反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性
-
功能
- 在运行时判断任意一个对象所属的类;
- 在运行时构造任意一个类的对象;
- 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
- 在运行时调用任意一个对象的方法
-
java 的 Class.forName 和 ClassLoader.loadClass 方法的区别
-
一个 Java 类加载到 JVM 中会经过三个步骤,装载(查找和导入类或接口的二进制数据)、链接(校验:检查导入类或接口的二进制数据的正确性;准备:给类的静态变量分配并初始化存储空间;解析:将符号引用转成直接引用;)、初始化(激活类的静态变量的初始化 Java 代码和静态 Java 代码块)。
-
对于Class.forName 方法来说:
//Class.forName(className) 方法内部实际调用的方法是 Class.forName(className, true, classloader); public static Class<?> forName(String name, boolean initialize, ClassLoader loader) /** 三个参数的含义分别为: name:要加载 Class 的名字 initialize:是否要初始化 loader:指定的 classLoader */
-
对于ClassLoader.loadClass 方法来说:
//ClassLoader.loadClass(className) 方法内部实际调用的方法是 ClassLoader.loadClass(className, false); protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException /** 两个参数的含义分别为: name:class 的名字 resolve:是否要进行链接 */
-
所以通过传入的参数可以知道 Class.forName 方法执行之后已经对被加载类的静态变量分配完了存储空间,而 ClassLoader.loadClass 方法并没有一定执行完链接这一步;当想动态加载一个类且这个类又存在静态代码块或者静态变量而你在加载时就想同时初始化这些静态代码块则应偏向于使用 Class.forName 方法。
-
Integer和int区别
-
Integer是引用数据类型,int是基本数据类型
-
Integer和int存在自动拆箱与装箱机制
-
Interger存在cache,保存-128到127的值,当值在cache范围内时,用=比较时,比较的是大小,否则比较的是地址
-
java 1.5 开始的自动装箱拆箱机制其实是编译时自动完成替换的
-
装箱阶段自动替换为了 valueOf 方法,拆箱阶段自动替换为了 xxxValue 方法。
-
Serializable接口中serialVersionUID
- 在定义了同一个serialVersionUID值之后,类做了扩展的情况下,依然保持了兼容性,能将老版本时生成的序列化文件反序列化为新版的类,只是其中新增的的变量为默认值。
- 如果类结构发生了非常规性改变,比如修改了类名,类型等,这个时候尽管serialVersionUID 验证通过了,但是反序列化过程还是会失败
线程池
- 核心参数
- 最大线程数maximumPoolSize
- 核心线程数corePoolSize
- 活跃时间keepAliveTime
- 阻塞队列workQueue
- 拒绝策略RejectedExecutionHandler
- AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
- CallerRunsPolicy:只用调用者所在的线程来处理任务
- DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务
- DiscardPolicy:直接丢弃任务,也不抛出异常
- 执行流程
- 当我们提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任务
- 当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队
- 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁
- 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
阻塞队列
-
ArrayBlockingQueue:由数组结构组成的有界阻塞队列
-
应用场景:在线程池中有比较多的应用,生产者消费者场景
工作原理:基于ReentrantLock保证线程安全,根据Condition实现队列满时的阻塞
-
-
LinkedBlockingQueue:由链表结构组成的有界阻塞(默认大小
Integer.MAX_VALUE()
)队列- 使用无限 BlockingQueue 设计生产者 - 消费者模型时最重要的是 消费者应该能够像生产者向队列添加消息一样快地消费消息 。否则,内存可能会填满,然后就会得到一个 OutOfMemory 异常。
-
PriorityBlockingQueue:支持优先级排序的无界阻塞队列
-
DelayQueue:使用优先队列实现延迟无界阻塞队列
-
应用场景:电影票
工作原理:队列内部会根据时间优先级进行排序。延迟类线程池周期执行。
-
-
SynchronizedQueue:不存储元素的阻塞队列,也即单个元素的队列
- SynchronizedQueue是一个不存储元素的BlockingQueue,每一个put操作必须等待一个take操作,否则就不能继续添加元素,反之亦然。总之,SynchronizedQueue,生产一个,消费一个
IO和NIO和AIO(需完善)
- IO的分类
- 字节流:字节流读取单个字节,字节流用来处理二进制文件
- 字符流:字符流读取单个字符,字符流用来处理文本文件
- 简而言之,字节是个计算机看的,字符才是给人看的。
JUC的工具包
Semaphore
- Semaphore用来控制同时访问特定资源的线程数量
- Semaphore可以用作流量控制,特别是共用资源有限的应用场景,比如数据库连接
CountDownLatch
- 构造函数可以传入一个int类型的参数N作为计数器
- 调用CountDownLatch的countDown方法,N就会减一,CountDownLatch的await会阻塞当前线程,直到N变成零
- countDown方法可以用在任何地方,可以是一个线程中的N个执行步骤,也可以是N个线程
- 计数器必须大于等于0,只有等于0的时候,调用await方法不会阻塞当前线程
- 一个线程的countDown方法happen-before另一个线程调用await方法
CyclicBarrier
- 构造方法CyclicBarrier(int parties),其参数表示屏障拦截的线程数量
- CyclicBarrier的计数器可以用reset()方法重置
Java基础问题
-
a = a + b 与 a += b 的区别
-
+= 隐式的将加操作的结果类型强制转换为持有结果的类型。如果两这个整型相加,如 byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。
-
byte a = 127; byte b = 127; b = a + b; // error : cannot convert from int to byte //因为 a+b 操作会将 a、b 提升为 int 类型,所以将 int 类型赋值给 byte 就会编译出错 b += a; // ok
-
设计模式
单例模式
-
保证一个类仅有一个实例,并提供一个访问它的全局访问点。Spring中,单例模式只实现了后半句,提供了一个全局访问点
-
饿汉式
-
懒汉式:静态内部类,双重检验
public class Singleton { public static volatile Singleton singleton; /** * 构造函数私有,禁止外部实例化 */ private Singleton() {}; public static Singleton getInstance() { if (singleton == null) { synchronized (singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
-
volatile的原因
- 对象的分配过程
- 分配内存空间。
- 初始化对象。
- 将内存空间的地址赋值给对应的引用。
- 分配过程可能出现指令重排序,使得出现下述顺序
- 分配内存空间。
- 将内存空间的地址赋值给对应的引用。
- 初始化对象
- 如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量
- 对象的分配过程
-
序列化可以破坏单例
-
在
Singleton
类中定义readResolve
就可以解决该问题 -
public class Singleton implements Serializable{ private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } private Object readResolve() { return singleton; } }
-
-
原型模式
-
使用原型实例指定要创建对象的类型,通过复制这个原型来创建新对象。
-
原型模式的核心是一个clone方法,通过该方法进行对象的拷贝,Java 提供了一个Cloneable接口来标示这个对象是可拷贝的
-
clone()方法是覆写了Object类中的clone方法
-
通用代码
public class PrototypeClass implements Cloneable{ @Override protected PrototypeClass clone() throws CloneNotSupportedException { PrototypeClass prototypeClass = null; try { prototypeClass = (PrototypeClass) super.clone(); }catch (CloneNotSupportedException e){ e.printStackTrace(); } return prototypeClass; } }
-
优点
- 原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多
- 不会执行构造函数,减少了约束
-
使用场景
-
资源优化场景 :类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
-
性能和安全要求的场景 :通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
-
一个对象多个修改者的场景 :一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑
使用原型模式拷贝多个对象供调用者使用。
-
浅拷贝
-
概念:其对象内部的数组、引用对象等都不拷贝,还是指向原生对象 的内部元素地址
-
引用的成员变量必须满足两个条件才不会被拷贝:
- 是类的成员变量,而不是方法内变量
- 是必须是一个可变的引用对象,而不是一个原始类型或不可变对象
-
实现:
-
public class PrototypeClass implements Cloneable{ @Override protected PrototypeClass clone() throws CloneNotSupportedException { PrototypeClass prototypeClass = null; try { prototypeClass = (PrototypeClass) super.clone(); }catch (CloneNotSupportedException e){ e.printStackTrace(); } return prototypeClass; } }
-
深拷贝
-
概念:其对象内部的数组、引用对象等也会拷贝
-
注意:深拷贝时,成员变量不能定义为final类型
-
实现:
-
public class DeepCopy implements Cloneable{ private ArrayList<String> arrayList = new ArrayList<>(); @Override protected DeepCopy clone() throws CloneNotSupportedException { DeepCopy deepCopy = null; try { deepCopy = (DeepCopy) super.clone(); //对私有变量进行独立拷贝 deepCopy.arrayList = (ArrayList<String>) this.arrayList.clone(); }catch (CloneNotSupportedException e){ e.printStackTrace(); } return deepCopy; } }
-
//二进制流实现 public class DeepClone implements Serializable { private static final long serialVersionUID = -6849794470757327710L; //实现深拷贝 public Object myClone() throws Exception{ // 序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); // 反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); } }
-
简单工厂模式
-
缺点
- 是工厂类的扩展比较困难,不符合开闭原则
-
通用代码
-
工厂类
public class HumanFactory { public static <T extends Human> T createHuman(Class<T> c) { Human human = null; try { human = (T) Class.forName(c.getName()).newInstance(); } catch (InstantiationException | ClassNotFoundException | IllegalAccessException e) { e.printStackTrace(); } return (T) human; } }
-
main
public class Main { public static void main(String[] args) { Human human = HumanFactory.createHuman(WhiteHuman.class); //业务方法 } }
-
抽象工厂模式
-
为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。
-
缺点
- 产品族扩展非常困难
-
使用场景
- 一个对象族(或是一组没有任何关系的对象) 都有相同的约束,则可以使用抽象工厂模式。
-
通用代码
-
main
public class Main { public static void main(String[] args) { HumanFactory femaleHumanFactory = new FemaleHumanFactory(); HumanFactory maleHumanFactory = new MaleHumanFactory(); Human femaleBlackHuman = femaleHumanFactory.createBlackHuman(); //业务 } }
-
工厂方法模式
-
优点
- 良好的封装性,代码结构清晰。
- 扩展性非常优秀,在增加产品类的情况下,只要适当地修改具体 的工厂类或扩展一个工厂类,就可以完成“拥抱变化”。
-
使用场景
- 需要灵活的、可扩展的框架时,可以考虑采用工厂方法模式。
- 有需要生成对象的地方都可以 使用,但是需要慎重地考虑是否要增加一个工厂类进行管理
-
通用代码:
-
抽象产品类
public interface Human { void getColor(); void talk(); }
-
具体产品类
public class WhiteHuman implements Human{ @Override public void getColor() { System.out.println("白色"); } @Override public void talk() { System.out.println("白人英语"); } } public class YellowHuman implements Human{ @Override public void getColor() { System.out.println("黄色"); } @Override public void talk() { System.out.println("汉语"); } }
-
抽象工厂类
public abstract class AbstractHumanFactory { abstract <T extends Human> T createHuman(Class<T> c); }
-
具体工厂类
public class HumanFactory extends AbstractHumanFactory{ @Override <T extends Human> T createHuman(Class<T> c) { Human human = null; try { human = (T) Class.forName(c.getName()).newInstance(); } catch (InstantiationException | ClassNotFoundException | IllegalAccessException e) { e.printStackTrace(); } return (T) human; } }
-
main
public class Main { public static void main(String[] args) { AbstractHumanFactory humanFactory = new HumanFactory(); Human whiteHuman = humanFactory.createHuman(WhiteHuman.class); //业务 } }
-
代理模式
-
为其他对象提供一种代理以控制对这个对象的访问。
-
优点
- 真实的角色就是实现实际的业务逻辑,不用关心其他非本职责的事务,通过后期的代理 完成一件事务,附带的结果就是编程简洁清晰。
- 具体主题角色是随时都会发生变化的,只要它实现了接口,代理类完全就可以在不做任何修改的情况下使用
- 智能化
-
CGLIB动态代理
- 其实就是利用了CGLIB,在运行时期生成被代理对象的子类,来作为代理对象。子类重写要代理的类的所有不是final的方法。
- CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM(Java字节码操控框架),来转换字节码并生成新的类。
- 缺点:对于final方法,无法进行代理。
-
通用代码
-
抽象主题
public interface IGamePlayer { void login(String userName, String password); void killBoss(); }
-
真实主题
public class GamePlayer implements IGamePlayer { private String name; public GamePlayer(String name){ this.name = name; } @Override public void login(String userName, String password) { System.out.println(userName + "登录成功"); } @Override public void killBoss() { System.out.println("打怪"); } }
-
动态代理的Handler
public class GamePlayerIH implements InvocationHandler { Class cl = null; Object obj = null; public GamePlayerIH(Object obj){ this.obj = obj; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("login")){ System.out.println("有人异地登录"); } return method.invoke(this.obj,args); } }
-
main
public class Client { public static void main(String[] args) { IGamePlayer player = new GamePlayer("clcoding"); InvocationHandler handler = new GamePlayerIH(player); ClassLoader cl = player.getClass().getClassLoader(); IGamePlayer proxy = (IGamePlayer) Proxy.newProxyInstance(cl,new Class[]{IGamePlayer.class},handler); proxy.login("clcoding","password"); proxy.killBoss(); } }
-
观察者模式
-
定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。
-
优点
- 观察者和被观察者之间是抽象耦合
-
缺点
- 观察者模式需要考虑一下开发效率和运行效率问题,一个被观察者,多个观察者,开发和调试就会比较复杂,而且在Java中消息的通知默认是顺序执行,一个观察者卡壳,会影响整体的执行效率。
-
通用代码
-
被监听者逻辑方法接口
public interface ISubject { void doSomething(); }
-
具体被监听者
public class Subject extends Observable implements ISubject { @Override public void doSomething() { System.out.println("被监听者发生修改"); super.setChanged(); super.notifyObservers("发生了改动"); } }
-
监听者
public class MyObserver implements Observer { @Override public void update(Observable o, Object arg) { System.out.println("监听者发现改动" + " " + arg.toString()); } }
-
main
public class Main { public static void main(String[] args) { //被监听者 Subject subject = new Subject(); //监听者 MyObserver Observer = new MyObserver(); //添加监听者 subject.addObserver(Observer); //被监听者执行 subject.doSomething(); } }
-
策略模式
-
定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。
-
为什么不直接调用
- 为了更加体现面向对象,是对象去执行策略,而不是策略执行策略
-
在实际项目中,我们一般通过工厂方法模式来实现策略类的声明
-
优点
- 算法可以自由切换
- 避免使用多重条件判断 :使用策略模式后,可以由其他模块决定采用何种策略,策略 家族对外提供的访问接口就是封装类,简化了操作,同时避免了条件语句判断。
- 扩展性良好
-
缺点
- 每个策略都是一个类,类数目比较多
- 所有策略类都必须向外暴露
-
通用代码
-
抽象策略角色
public interface Strategy { void doSomething(); }
-
具体策略角色
public class ConcreteStrategy1 implements Strategy{ @Override public void doSomething() { System.out.println("策略1"); } } public class ConcreteStrategy2 implements Strategy{ @Override public void doSomething() { System.out.println("策略2"); } }
-
封装角色
public class Context { private Strategy strategy = null; public Context(Strategy strategy){ this.strategy = strategy; } public void doAnything(){ this.strategy.doSomething(); } }
-
main
public class Main { public static void main(String[] args) { Strategy strategy = new ConcreteStrategy1(); Context context = new Context(strategy); context.doAnything(); } }
-
适配器模式
- 将一个类的接口变换成客户 端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
- 优点
- 适配器模式可以让两个没有任何关系的类在一起运行,只要适配器这个角色能够搞定他们就成
- 增加了类的透明性
- 提高了类的复用度
- 灵活性非常好:不想要适配器,删除掉这个适配器就可以了,其他的代码都不用修改
责任链模式
-
使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
-
优点
- 将请求和处理分开。请求者可以不用知道是谁处理的,处理者可以不用知道请求的全貌
-
缺点
- 性能问题:每个请求都是从链头遍历到链尾,特别是在链比较长的时候,性能是一个非常大的问题
- 调试不方便:链条比较长,环节比较多的时候,由于采用了类似递归的方式,调试的时候逻辑可能比较复杂
-
通用代码
-
抽象处理者
public abstract class Handler { private Handler nextHandler; public final Response handlerMessage(Request request){ Response response = null; if (this.getHandlerLevel().equals(request.getRequestLevel())){ response = this.echo(request); }else { if (this.nextHandler != null){ response = this.nextHandler.handlerMessage(request); }else { System.out.println("没有合适的处理者"); } } return response; } public void setNextHandler(Handler nextHandler){ this.nextHandler = nextHandler; } protected abstract Level getHandlerLevel(); protected abstract Response echo(Request request); }
-
具体处理者
public class ConcreteHandler1 extends Handler{ @Override protected Level getHandlerLevel() { //设置自己的处理级别 return null; } @Override protected Response echo(Request request) { //完成处理逻辑 return null; } }
-
main
public class Client { public static void main(String[] args) { Handler handler1 = new ConcreteHandler1(); Handler handler2 = new ConcreteHandler2(); handler1.setNextHandler(handler2); Response response = handler1.handlerMessage(new Request()); } }
-
Spring
什么是IoC和DI
- 控制反转是把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。由容器来创建对象并管理对象之间的依赖关系。
- 依赖注入的基本原则是应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由容器负责,查找资源的逻辑应该从应用组件的代码中抽取出来,交给容器来完成。DI是对IoC更准确的描述,即组件之间的依赖关系由容器在运行期决定,形象的来说,即由容器动态的将某种依赖关系注入到组件之中。
- 依赖注入可以通过setter方法注入(设值注入)、构造器注入和接口注入三种方式来实现,Spring支持setter注入和构造器注入,通常使用构造器注入来注入必须的依赖关系,对于可选的依赖关系,则setter注入是更好的选择,setter注入需要类提供无参构造器或者无参的静态工厂方法来创建对象。
SpringIOC原理
-
IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象
-
DI的实现:Java 1.3之后一个重要特征是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。
-
IOC实现:
-
定义用来描述bean的配置的Java类
解析bean的配置,將bean的配置信息转换为上面的BeanDefinition对象保存在内存中,spring中采用HashMap进行对象存储,其中会用到一些xml解析技术
遍历存放BeanDefinition的HashMap对象,逐条取出BeanDefinition对象,获取bean的配置信息,利用Java的反射机制实例化对象,將实例化后的对象保存在另外一个Map中即可。
-
AOP
- AOP(Aspect Oriented Programming),即面向切面编程,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
- 横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
- Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。
- 如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
Bean的生命周期
Spring中Bean的作用域有哪些
- 在Spring的早期版本中,仅有两个作用域:singleton和prototype,前者表示Bean以单例的方式存在;后者表示每次从容器中调用Bean时,都会返回一个新的实例,prototype通常翻译为原型。
- Spring 2.x中针对WebApplicationContext新增了3个作用域,分别是:request(每次HTTP请求都会创建一个新的Bean)、session(同一个HttpSession共享同一个Bean,不同的HttpSession使用不同的Bean)和globalSession(同一个全局Session共享一个Bean)。
autowired 和resource区别
- Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false。如果我们想使用按照名称(byName)来装配,可以结合@Qualifier注解一起使用。
- @Resource默认按照ByName自动注入,由J2EE提供
循环依赖(需完善)
Spring中BeanFactory和ApplicationContext的区别
- ApplicationContext是BeanFactory的子类,因为古老的BeanFactory无法满足不断更新的spring的需求,于是ApplicationContext就基本上代替了BeanFactory的工作,以一种更面向框架的工作方式以及对上下文进行分层和实现继承,
- 如果使用ApplicationContext,如果配置的bean是singleton,那么不管你有没有或想不想用它,它都会被实例化。好处是可以预先加载,坏处是浪费内存。
- BeanFactory,当使用BeanFactory实例化对象时,配置的bean不会马上被实例化,而是等到你使用该bean的时候(getBean)才会被实例化。好处是节约内存,坏处是速度比较慢。多用于移动设备的开发。
Spring MVC的工作原理
- ①客户端的所有请求都交给前端控制器DispatcherServlet来处理,它会负责调用系统的其他模块来真正处理用户的请求。
② DispatcherServlet收到请求后,将根据请求的信息(包括URL、HTTP协议方法、请求头、请求参数、Cookie等)以及HandlerMapping的配置找到处理该请求的Handler(任何一个对象都可以作为请求的Handler)。
③在这个地方Spring会通过HandlerAdapter对该处理器进行封装。
④ HandlerAdapter是一个适配器,它用统一的接口对各种Handler中的方法进行调用。
⑤ Handler完成对用户请求的处理后,会返回一个ModelAndView对象给DispatcherServlet,ModelAndView顾名思义,包含了数据模型以及相应的视图的信息。
⑥ ModelAndView的视图是逻辑视图,DispatcherServlet还要借助ViewResolver完成从逻辑视图到真实视图对象的解析工作。
⑦ 当得到真正的视图对象后,DispatcherServlet会利用视图对象对模型数据进行渲染。
⑧ 客户端得到响应,可能是一个普通的HTML页面,也可以是XML或JSON字符串,还可以是一张图片或者一个PDF文件。
Spring MVC的执行流程
get和post请求,它们之间的区别?
GET
请求的请求参数是添加到head
中,可以在url
中可以看到;POST
请求的请求参数是添加到BODY
中,在url
中不可见。- 请求的
url
有长度限制,这个限制由浏览器和web
服务器决定和设置的。例如IE浏览器对URL
的最大限制为2083个字符,如果超过这个数字,提交按钮没有任何反应。因为GET
请求的参数是添加到URL
中,所以GET
请求的URL
的长度限制需要将请求参数长度也考虑进去。而POST
请求不用考虑请求参数的长度。 GET
请求产生一个数据包;POST
请求产生2个数据包,在火狐浏览器中,产生一个数据包。这个区别点在于浏览器的请求机制,先发送请求头,再发送请求体。因为GET
没有请求体,所以就发送一个数据包,而POST
包含请求体,所以发送两次数据包,但是由于火狐机制不同,所以发送一个数据包。GET
请求会被浏览器主动缓存下来,留下历史记录,而 POST 默认不会。GET
是幂等的,而POST
不是。(幂等表示执行相同的操作,结果也是相同的)
cookie 和 session 的区别?
- cookie的数据信息存放在客户端浏览器上,session的数据信息存放在服务器上。
- 单个cookie保存的数据<=4KB,一个站点最多保存20个Cookie。对于session来说并没有上限,但出于对服务器端的性能考虑,session内不要存放过多的东西,并且设置session删除机制。
- cookie中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据。session中能够存储任何类型的数据,包括且不限于string,integer,list,map等。
- cookie对客户端是可见的,别有用心的人可以分析存放在本地的cookie并进行cookie欺骗,所以它是不安全的。session存储在服务器上,对客户端是透明对,不存在敏感信息泄漏的风险。
- 开发可以通过设置cookie的属性,达到使cookie长期有效的效果。session依赖于名为JSESSIONID的cookie,而cookie JSESSIONID的过期时间默认为-1,只需关闭窗口该session就会失效,因而session不能达到长期有效的效果。
- cookie保管在客户端,不占用服务器资源。对于并发用户十分多的网站,cookie是很好的选择。session是保管在服务器端的,每个用户都会产生一个session。假如并发访问的用户十分多,会产生十分多的session,耗费大量的内存。
Spring Boot
自动配置(需完善)
核心注解
@SpringBootApplication
-
@SpringBootApplication是@EnableAutoConfiguration、@ComponentScan、@Configuartion三种注解的组合
-
@EnableAutoConfiguration:开启Spring Boot自动装配机制
-
@ComponentScan:激活@Component的扫描
-
@ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, //用于查找BeanFactory中已注册的TypeExcludeFilter Bean,作为代理执行对象 classes = {TypeExcludeFilter.class} ), @Filter( type = FilterType.CUSTOM, //用于排除其他同时标注@Configuration 和 @EnableAutoConfiguration的类 classes = {AutoConfigurationExcludeFilter.class} )} )
-
-
@Configuartion:声明被标注为配置类
-
Spring Boot 1.4以后,更改为@SpringBootConfiguration
-
@Configuartion和@SpringBootConfiguration都是@Component派生出来的,所以它们能被@ComponentScan进行扫描
-
-
@SpringBootConfiguration
- 、
@EnableAutoConfiguration
@ComponentScan
MySQL
数据库的三范式
- 第一范式:所谓第一范式(1NF)是指在关系模型中,对于添加的一个规范要求,所有的域都应该是原子性的,即数据库表的每一列都是不可分割的原子数据项,而不能是集合,数组,记录等非原子数据项。
- 第二范式:在1NF的基础上,非码属性必须完全依赖于候选码(在1NF基础上消除非主属性对主码的部分函数依赖)。
- 第三范式: 在2NF基础上,任何非主属性不依赖于其它非主属性(在2NF基础上消除传递依赖)
innoDB和MyISAM的区别
-
InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
-
InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;
-
InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
-
InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
-
Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高;
数据库的隔离级别
未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据。
提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)。
可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读。
串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。
MVCC机制
MVCC
的实现依赖于:隐藏字段、Read View、undo log。- 在内部实现中,
InnoDB
通过数据行的DB_TRX_ID
和Read View
来判断数据的可见性,如不可见,则通过数据行的DB_ROLL_PTR
找到undo log
中的历史版本。 - 每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建
Read View
之前已经提交的修改和该事务本身做的修改
数据库事务的ACID
- 原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;
- 利用Innodb的undo log实现
- 一致性(Consistent):事务结束后系统状态是一致的;
- 隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;
- 利用的是锁和MVCC机制
- 持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步备份可以在故障发生后重建数据。
- 利用Innodb的redo log实现
数据库的锁
- 共享锁
- 共享锁也叫读锁或 S 锁,共享锁锁定的资源可以被其他用户读取,但不能修改。在进行SELECT的时候,会将对象进行共享锁锁定,当数据读取完毕之后,就会释放共享锁,这样就可以保证数据在读取时不被修改。
- 排它锁
- 排它锁也叫独占锁、写锁或 X 锁。排它锁锁定的数据只允许进行锁定操作的事务使用,其他事务无法对已锁定的数据进行查询或修改。
- 当我们对数据进行更新的时候,也就是INSERT、DELETE或者UPDATE的时候,数据库也会自动使用排它锁,防止其他事务对该数据行进行操作。
- 乐观锁
- 认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者时间戳机制实现。
- 乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
- 版本号机制:在表中设计一个版本字段 version,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行UPDATE ... SET version=version+1 WHERE version=version。此时如果已经有事务对这条数据进行了更改,修改就不会成功。
- 时间戳机制:时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突。
- 悲观锁
- 对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。
- 悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止读 - 写和写 - 写的冲突。
mysql的日志文件
bin log
-
用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中
可以简单理解为记录的就是sql语句
binlog 是 mysql 的逻辑日志,并且由
Server
层进行记录,使用任何存储引擎的 mysql 数据库都会记录 binlog 日志使用场景
在实际应用中, binlog 的主要使用场景有两个,分别是 主从复制 和 数据恢复
刷盘时机
mysql 通过
sync_binlog
参数控制 biglog 的刷盘时机,取值范围是 0-N:- 0:不去强制要求,不是直接进入磁盘文件,而是进入os cache内存缓存,由系统自行判断何时写入磁盘;
- 1:每次 commit 的时候都要将 binlog 写入磁盘;
- N:每N个事务,才会将 binlog 写入磁盘。
可以看出,
sync_binlog
最安全的是设置是 1日志格式
binlog 日志有三种格式,分别为 STATMENT 、 ROW 和 MIXED
在 MySQL 5.7.7 之前,默认的格式是 STATEMENT , MySQL 5.7.7 之后,默认值是 ROW
日志格式通过
binlog-format
指定。- STATMENT : 基于 SQL 语句的复制,每一条会修改数据的sql语句会记录到 binlog 中
- ROW : 基于行的复制
- MIXED : 基于 STATMENT 和 ROW 两种模式的混合复制,比如一般的数据操作使用 row 格式保存,有些表结构的变更语句,使用 statement 来记录
redo log
-
物理日志,具体来说就是只记录事务对数据页做了哪些修改
这种日志大致的格式如下:对表空间XX中的数据页XX中的偏移量为XXXX的地方更新了数据XXX
redo log写日志,是顺序写入磁盘文件,每次都是追加到磁盘文件末尾去,速度也是很快的
包括两部分:一个是内存中的日志缓冲( redo log buffer ),另一个是磁盘上的日志文件
mysql 每执行一条 DML 语句,先将记录写入
redo log buffer
,后续某个时间点再一次性将多个操作记录写到 redo log file这种 先写日志,再写磁盘 的技术就是 MySQL里经常说到的 WAL 技术
undo log
-
原子性 底层就是通过 undo log 实现的
undo log 主要记录了数据的逻辑变化,比如一条 INSERT语句,对应一条 DELETE 的
undo log
,对于每个 UPDATE 语句,对应一条相反的 UPDATE 的 undo log ,这样在发生错误时,就能回滚到事务之前的数据状态。同时,undo log
也是 MVCC(多版本并发控制)实现的关键
-
索引
- 索引的本质是一种排好序的数据结构。
- 主键需要自增
- 如果主键不是自增的那么每次都需要从头开始比较,然后找到合适的位置,再将记录插入进去,这样严重影响效率
索引底层数据结构
- B+树
- B+树非叶子节点上是不存储数据的,仅存储键值
- 数据库中页的大小是固定的,innodb中页的默认大小是16KB。如果不存储数据,那么就会存储更多的键值,相应的树的阶数会更大,树就会矮。
- B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。使得范围查找,排序查找,分组查找以及去重查找变得异常简单
- B+树中各个页之间是通过双向链表连接的,叶子节点中的数据是通过单向链表连接的。
- 在MyISAM中,B+树索引的叶子节点并不存储数据,而是存储数据的文件地址。
- B+树非叶子节点上是不存储数据的,仅存储键值
- 二叉查找树
- 极端情况下,会退化成链表,查询相当于全表扫描
- 平衡二叉树
- 每个节点只存储一个键值和数据的,存储大量数据,树会很高
- 应当尽量减少从磁盘中读取数据的次数。 另外,从磁盘中读取数据时,都是按照磁盘块来读取的,并不是一条一条的读
- 每个节点只存储一个键值和数据的,存储大量数据,树会很高
- B树
- 每个节点存储的键值(key)和数据(data)
- B树因为数据分散在各个节点,范围查找,排序查找不方便
- Hash
- 仅适合精确查找
非聚簇索引和聚簇索引
- 聚簇索引
- 以主键作为B+树索引的键值而构建的B+树索引,我们称之为聚集索引。
- 以innodb作为存储引擎的表,表中的数据都会有一个主键,即使你不创建主键,系统也会帮你创建一个隐式的主键。
- innodb是把数据存放在B+树中的,而B+树的键值就是主键,在B+树的叶子节点中,存储了表中所有的数据
- 非聚簇索引
- 以主键以外的列值作为键值构建的B+树索引,我们称之为非聚集索引。
- 非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键
- 查找数据还需要根据主键再去聚集索引中进行查找
- 在MyISAM中,聚集索引和非聚集索引的叶子节点都会存储数据的文件地址。
覆盖索引
- 一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。
- 当一条查询语句符合覆盖索引条件时,MySQL只需要通过索引就可以返回查询所需要的数据,这样避免了查到索引后再返回表操作,减少I/O提高效率。
最左匹配
- 在联合索引中,MySQL索引查询会遵循最左前缀匹配的原则,即最左优先,在检索数据时从联合索引的最左边开始匹配
- 如(key1,key2,key3),相当于创建了(key1)、(key1,key2)和(key1,key2,key3)三个索引
b+树的插入删除
-
插入
-
若被插入关键字所在的结点,其含有关键字数目小于阶数 M,则直接插入;
-
若被插入关键字所在的结点,其含有关键字数目等于阶数 M,则需要将该结点分裂为两个结点,一个结点包含
⌊M/2⌋
,另一个结点包含⌈M/2⌉
。同时,将⌈M/2⌉
的关键字上移至其双亲结点。假设其双亲结点中包含的关键字个数小于 M,则插入操作完成。 -
如果上移操作导致其双亲结点中关键字个数大于 M,则应继续分裂其双亲结点。
-
若插入的关键字比当前结点中的最大值还大,破坏了B+树中从根结点到当前结点的所有索引值,此时需要及时修正后,再做其他操作。
-
-
删除
-
找到存储有该关键字所在的结点时,由于该结点中关键字个数大于
⌈M/2⌉
,做删除操作不会破坏 B+树,则可以直接删除 -
当删除某结点中最大或者最小的关键字,就会涉及到更改其双亲结点一直到根结点中所有索引值的更改。
-
当删除该关键字,导致当前结点中关键字个数小于
⌈M/2⌉
,若其兄弟结点中含有多余的关键字,可以从兄弟结点中借关键字完成删除操作。 -
如果其兄弟结点没有多余的关键字,则需要同其兄弟结点进行合并。
-
什么时候适合建立索引
- 为频繁查询的字段建立索引
- 尽量为ORDER BY 和 GROUP BY 后面的字段建立索引
- 选择区分度大的列作为索引
索引失效原因
- 没有遵循最左匹配原则。
- 一些关键字会导致索引失效,例如 or, != , not in,is null ,is not unll
- like查询是以%开头
- 隐式转换会导致索引失效。
- 对索引应用内部函数,索引字段进行了运算。
MySQL的sql执行顺序
Redis
底层数据结构(需完善)
SDS
-
定义:
struct sdshdr { //记录buf数组中已经使用字节的数量 //等于SDS所保存字符串的长度 int len; //记录buf数组中未使用字节的数量 int free; //字节数组,用于保存字符串 char buf[]; }
-
特点:
- 常数复杂度获取字符串的长度
- C语言需要遍历整个字符串获取字符串长度
- SDS使用len标记字符串长度
- 杜绝缓冲区溢出
- C语言在strcat之前,若没有分配足够的空间,s1的数据将溢出到s2的空间中,导致s2的数据被修改
- SDS在修改字符串时,会检查空间是否满足,若不满足则会自动扩展至修改需要的大小
- 减少修改字符串带来的内存重分配
- 空间预分配
- 如果对SDS修改后,其长度小于1MB,那么程序分配和len属性同样大小的未使用空间,buf数组长度为
2*len + 1
- 如果对SDS修改后,其长度大于等于1MB,程序分配1MB的未使用空间,buf数组长度为
len + 1MB + 1
- 如果对SDS修改后,其长度小于1MB,那么程序分配和len属性同样大小的未使用空间,buf数组长度为
- 惰性空间释放
- 当SDS缩短后,将多出来的字节通过free记录下来,以等待将来使用
- SDS也提供了API,可以在有需要时,真正释放SDS的未使用空间,不会造成内存浪费
- 空间预分配
- 二进制安全
- 所以的SDS API都以处理二进制的方式处理SDS存放在buf数组中的数据
- SDS使用len属性的值而不是空字符来判断字符串是否结束
- 因此,Redis可以用来存储图片,音频,视频,压缩文件等二进制数据
- 部分兼容C字符串函数
- SDS遵循C字符串以空字符结尾的惯例
- 常数复杂度获取字符串的长度
链表
-
定义
typedef struct listNode{ //前置节点 struct listNode *prve; //后置节点 struct listNode *next; //节点的值 void *value; }listNode; typedef struct list{ //表头节点 listNode *head; //表尾节点 listNode *tail; //链表所包含的节点数量 unsigned long len; //节点值复制函数 void *(*dup)(void *ptr); //节点值释放函数 void (*free)(void *ptr); //节点值对比函数 int (*match)(void *ptr,void *key); }list;
-
特性
- 双端:带有prev和next指针,获取前置节点和后置节点时间复杂度都是O(1)
- 无环:表头节点的prev和表尾节点的next指针都指向NULL
- 带表头指针和表尾指针:list结构记录了表头和表尾指针
- 带链表长度计数器:list结构记录了链表长度,获取长度复杂度为O(1)
- 多态:使用void*指针保存节点值,所以链表可以保存不同类型的值
字典
-
定义
typedef struct dict{ //类型特定函数 dictType *type; //私有数据 void *privdata; //哈希表 dictht ht[2]; //rehash索引 //当rehash不在进行时,值 int rehashidx; } typedef struct dictht { //哈希表数组 dictEntry **table; //哈希表大小 unsigned long size; //哈希表大小掩码,用于计算索引值 //总是等于size-1 unsigned long sizemask; //该哈希表已有节点的数量 unsigned long used; }dictht; typedef struct dictEntry { //键 void *key; //值 union{ void *val; uint64_tu64; int64_ts64; } v; //指向下一个哈希表节点,形成链表 struct dictEntry *next; }dictEntry;
跳跃表(需完善)
redis单线程的原因
- Redis并没有在网络请求模块和数据操作模块中使用多线程模型的原因
- Redis 操作基于内存,绝大多数操作的性能瓶颈不在 CPU
- 使用单线程模型,可维护性更高,开发,调试和维护的成本更低
- 单线程模型,避免了线程间切换带来的性能开销
- 在单线程中使用多路复用 I/O技术也能提升Redis的I/O利用率
为什么 redis 读写速率快、性能好?
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
- 数据结构简单,对数据操作也简单,如哈希表、跳表都有很高的性能。
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU
- 使用多路I/O复用模型
缓存问题
缓存击穿
- 缓存击穿指的是某个 key 一直在扛着高并发,大量的请求都是获取这个 key 对应的值,而这个 key 在某个时间突然失效,大量的请求就无法在缓存中获取数据,而是去请求数据库了,这样很有可能导致数据库被击垮。
- 解决方案
- 不设置过期时间,如果该key的数据更新了,那么就通过互斥锁的方式将其更新。
缓存穿透
- 缓存穿透是某个不存在的key一直被访问,结果发现数据库中也没有这样的数据,最终导致访问该key的所有请求都直接请求到数据库了。
- 解决方案
- 缓存空数据,某个key数据并不存在,那么就存一个 NULL 就好了,并且设置一个过期时间
- 布隆过滤器,通过布隆过滤器来快速的判断出一个key是否存在数据库,如果可能存在再去数据库查询,如果布隆过滤器中不存在那么就需要再去数据库查询了。
- 前端检查,将所访问的请求进行合法的检测,把恶意的请求直接过滤,不让请求到达缓存和数据库层。
缓存雪崩
- 缓存雪崩是由于某个时间节点大量的 key 失效而导致的问题
- 解决方案
- 过期时间加随机值,在设置key的过期时间的时候再加一个随机值,避免同一时间大量key失效
- 加锁
- 多个请求同时到达业务系统时候,只能有一个线程能获取到锁,然后才能继续去缓存或者是数据库中查询数据
- 优化:将加锁延后至访问数据库
缓存预热
- 将一些可能经常使用数据在系统启动的时候预先设置到缓存中,这样可以避免在使用到的时候先去数据库中查询。
双写一致性
-
延时双删
- 步骤
- 先删除缓存
- 再更新数据库
- 休眠一会(比如1秒),再次删除缓存。
- 休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。 为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。
- 步骤
-
监听MySQL的binlog进行缓存更新【canal】
- 步骤
- 读Redis:热数据基本都在Redis
- 写MySQL:增删改都是操作MySQL
- 更新Redis数据:订阅MySQ的数据操作记录binlog,来更新到Redis
- 步骤
-
先删缓存,再修改数据库
-
问题
-
线程A删除缓存
-
线程B查询数据,发现缓存数据不存在
-
线程B查询数据库,得到旧值,写入缓存
-
线程A将新值更新到数据库
- 缓存中的数据仍然是旧值
-
-
-
先更改数据库,再删缓存
- 问题
- 缓存失效了
- 线程B从数据库读取旧值
- 线程A从数据库读取旧值
- 线程B将新值更新到数据库
- 线程B删除缓存
- 线程A将旧值写入缓存
- 问题
持久化机制(需完善)
AOF
RDB
sentinel(需完善)
主从复制(需完善)
IO多路复用(需完善)
淘汰机制(需完善)
计算机网络
ISO7层模型
TCP协议的3次握手、4次挥手
-
三次握手的过程
1)主机A向主机B发送TCP连接请求数据包,其中包含主机A的初始序列号seq(A)=x。(其中报文中同步标志位SYN=1,ACK=0,表示这是一个TCP连接请求数据报文;序号seq=x,表明传输数据时的第一个数据字节的序号是x);
2)主机B收到请求后,会发回连接确认数据包。(其中确认报文段中,标识位SYN=1,ACK=1,表示这是一个TCP连接响应数据报文,并含主机B的初始序列号seq(B)=y,以及主机B对主机A初始序列号的确认号ack(B)=seq(A)+1=x+1)
3)第三次,主机A收到主机B的确认报文后,还需作出确认,即发送一个序列号seq(A)=x+1;确认号为ack(A)=y+1的报文;
四次挥手过程
假设主机A为客户端,主机B为服务器,其释放TCP连接的过程如下:
1) 关闭客户端到服务器的连接:首先客户端A发送一个FIN,用来关闭客户到服务器的数据传送,然后等待服务器的确认。其中终止标志位FIN=1,序列号seq=u。
2) 服务器收到这个FIN,它发回一个ACK,确认号ack为收到的序号加1。
3) 关闭服务器到客户端的连接:也是发送一个FIN给客户端。4) 客户段收到FIN后,并发回一个ACK报文确认,并将确认序号seq设置为收到序号加1。 首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
为什么要等待2MSL?
MSL:报文段最大生存时间,它是任何报文段被丢弃前在网络内的最长时间。
- 第一点:保证TCP协议的全双工连接能够可靠关闭:由于IP协议的不可靠性或者是其它网络原因,导致了Server端没有收到Client端的ACK报文,那么Server端就会在超时之后重新发送FIN,如果此时Client端的连接已经关闭处于
CLOESD
状态,那么重发的FIN就找不到对应的连接了,从而导致连接错乱,所以,Client端发送完最后的ACK不能直接进入CLOSED
状态,而要保持TIME_WAIT
,当再次收到FIN的收,能够保证对方收到ACK,最后正确关闭连接。 - 第二点:保证这次连接的重复数据段从网络中消失如果Client端发送最后的ACK直接进入
CLOSED
状态,然后又再向Server端发起一个新连接,这时不能保证新连接的与刚关闭的连接的端口号是不同的,也就是新连接和老连接的端口号可能一样了,那么就可能出现问题:如果前一次的连接某些数据滞留在网络中,这些延迟数据在建立新连接后到达Client端,由于新老连接的端口号和IP都一样,TCP协议就认为延迟数据是属于新连接的,新连接就会接收到脏数据,这样就会导致数据包混乱。所以TCP连接需要在TIME_WAIT
状态等待2倍MSL,才能保证本次连接的所有数据在网络中消失。
浏览器从接收到一个URL,到最后展示出页面,经历了哪些过程
-
1.DNS解析:浏览器会依据URL逐层查询DNS服务器缓存,解析URL中的域名对应的IP地址,DNS缓存从近到远依次是浏览器缓存、系统缓存、路由器缓存、IPS服务器缓存、域名服务器缓存、顶级域名服务器缓存。
从哪个缓存找到对应的IP直接返回,不再查询后面的缓存。
2.TCP连接:结合三次握手
3.发送HTTP请求:浏览器发出读取文件的HTTP请求,该请求发送给服务器
4.服务器处理请求并返回HTTP报文:服务器对浏览器请求做出响应,把对应的带有HTML文本的HTTP响应报文发送给浏览器
5.浏览器解析渲染页面
6.连接结束:浏览器释放TCP连接,该步骤即四次挥手。
第5步和第6步可以认为是同时发生的,哪一步在前没有特别的要求
dns解析过程
TCP协议
主要特点
- TCP是面向连接的运输层协议;所谓面向连接就是双方传输数据之前,必须先建立一条通道,例如三次握手就是建议通道的一个过程,而四次挥手则是结束销毁通道的一个其中过程。
- 每一条TCP连接只能有两个端点(即两个套接字),只能是点对点的;
- TCP提供可靠的传输服务。传送的数据无差错、不丢失、不重复、按序到达;
- TCP提供全双工通信。允许通信双方的应用进程在任何时候都可以发送数据,因为两端都设有发送缓存和接受缓存;
- 面向字节流。虽然应用程序与TCP交互是一次一个大小不等的数据块,但TCP把这些数据看成一连串无结构的字节流,它不保证接收方收到的数据块和发送方发送的数据块具有对应大小关系,例如,发送方应用程序交给发送方的TCP10个数据块,但就受访的TCP可能只用了4个数据块久保收到的字节流交付给上层的应用程序
可靠性原理
- 传输信道无差错,保证传输数据正确;
- 不管发送方以多快的速度发送数据,接收方总是来得及处理收到的数据;
- 首先,采用三次握手来建立TCP连接,四次握手来释放TCP连接,从而保证建立的传输信道是可靠的。
- 其次,TCP采用了连续ARQ协议(回退N(Go-back-N);超时自动重传)来保证数据传输的正确性,使用滑动窗口协议来保证接方能够及时处理所接收到的数据,进行流量控制。
- TCP使用慢开始、拥塞避免、快重传和快恢复来进行拥塞控制,避免网络拥塞。
UDP协议
主要特点
- UDP是无连接的传输层协议;
- UDP使用尽最大努力交付,不保证可靠交付;
- UDP是面向报文的,对应用层交下来的报文,不合并,不拆分,保留原报文的边界;
- UDP没有拥塞控制,因此即使网络出现拥塞也不会降低发送速率;
- UDP支持一对一 一对多 多对多的交互通信;
- UDP的首部开销小,只有8字节
TCP和UDP的区别
- TCP是可靠传输,UDP是不可靠传输;
- TCP面向连接,UDP无连接;
- TCP传输数据有序,UDP不保证数据的有序性;
- TCP不保存数据边界,UDP保留数据边界;
- TCP传输速度相对UDP较慢;
- TCP有流量控制和拥塞控制,UDP没有;
- TCP是重量级协议,UDP是轻量级协议;
- TCP首部较长20字节,UDP首部较短8字节;
Http响应码
状态码 | 含义 |
---|---|
200 | 成功响应 |
301 | 请求对象已经被永久转移,新的URL定义在响应报文的首部行,客户端将自动获取 |
302 | 资源临时被移动,客户端继续使用原有URL |
400 | 通用的差错代码,请求不能被服务器理解 |
401 | 未认证,缺乏相关权限 |
403 | 服务器理解客户端的请求,但是拒绝执行 |
404 | 被请求的文档不在服务器,资源找不到 |
405 | 客户端请求的方法被禁止,例如需要POST方式但是使用了GET |
500 | 服务器内部错误,无法完成请求 |
501 | 服务器不支持请求的功能 |
502 | 网关或代理工作的服务器尝试执行请求时,从远程服务器收到了一个无效响应 |
503 | 由于超载或系统维护,服务器暂时无法处理客户端请求 |
504 | 充当网关或代理的服务器,未及时从远端服务器获取请求 |
Https
- 在 HTTP 传输上增加了 SSL 安全套接字层,通过机密性、数据完整性、身份鉴别为 HTTP 事务提供安全保证。SSL 会对数据进行加密并把加密数据送往 TCP 套接字,在接收方,SSL 读取 TCP 套接字的数据并解密,把数据交给应用层。HTTPS 采用混合加密机制,使用非对称加密传输对称密钥保证传输安全,使用对称加密保证通信效率。
- HTTPS流程
- ① 客户发送它支持的算法列表以及一个不重数。不重数就是在协议的生存期只使用一次的数,用于防止重放攻击,每个 TCP 会话使用不同的不重数,可以使加密密钥不同,重放记录无法通过完整性检查。
- ② 服务器从该列表中选择一种对称加密算法(例如 AES),一种公钥加密算法(例如 RSA)和一种报文鉴别码算法,然后把它的选择、证书,一个不重数返回给客户。
- ③ 客户通过 CA 提供的公钥验证证书,成功后提取服务器的公钥,生成一个前主密钥 PMS 并发送给服务器。
- ④ 客户和服务器独立地从 PMS 和不重数中计算出仅用于当前会话的主密钥 MS,然后通过 MS 生成密码和报文鉴别码密钥。此后客户和服务器间发送的所有报文均被加密和鉴别。
操作系统
进程
-
进程是程序的一次执行,是系统进行资源分配和调度的独立单位,他的作用是是程序能够并发执行提高资源利用率和吞吐率。
由于进程是资源分配和调度的基本单位,因为进程的创建、销毁、切换产生大量的时间和空间的开销,进程的数量不能太多,而线程是比进程更小的能独立运行的基本单位,他是进程的一个实体,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。
进程调度算法
- 先到先服务(FCFS)调度算法 : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
- 短作业优先(SJF)的调度算法 : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
- 时间片轮转调度算法 : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin)调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
- 多级反馈队列调度算法 :前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。
- 优先级调度 : 为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。
进程通信方式
- 管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
- 有名管道(Names Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
- 信号(Signal) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
- 消息队列(Message Queuing) :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
- 信号量(Semaphores) :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
- 共享内存(Shared memory) :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
- 套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
线程
- 线程基本不拥有系统资源,只有一些运行时必不可少的资源,比如程序计数器、寄存器和栈,进程则占有堆、栈。
- 线程的引入,可以把一个进程的资源分配和调度分开,各个线程既可以共享进程资源,又可以独立调度
线程的实现方式
- 内核线程实现
- 内核线程实现也被称为1:1实现
- 程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口------轻量级进程,每个轻量级进程由一个内核线程支持,只有先支持内核线程,才能有轻量级进程
- 缺点
- 基于内核线程实现,各种线程操作都需要进行系统调用,需要在用户态和内核态来回切换
- 轻量级进程要消耗一定的内核资源(如内核线程的栈空间)
- 用户线程实现
- 用户线程实现被称为1:N实现
- 狭义上用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在以及如何实现
- 缺点
- 由于没有系统内核的支持,对“阻塞如何处理”,“多处理器系统如何将线程映射到其他处理器上”解决起来异常困难,甚至无法实现
- 混合实现
- 混合实现方式称为N:M实现
- 即存在用户线程,也存在轻量级进程
- 轻量级进程作为用户线程和内核线程之间的桥梁,可以使用内核提供的调度功能及处理器映射,用户线程的系统调用通过轻量级进程实现
Java的线程实现
- 主流商用虚拟机线程模型普遍使用的基于操作系统原生线程模型来实现
- HotSpot每个Java线程都是直接映射到一个操作系统原生线程实现,中间没有额外的间接结构,线程调度由操作系统决定
Java的线程调度
- Java的线程调度采用的抢占式调度方式
- 线程调度主要有协同式线程调度和抢占式线程调度
- 协同式线程调度
- 线程的执行时间由线程本身控制,线程把自己工作执行完了之后,需要主动通知系统切换到另一个线程上去
- 由于线程把自己的事情执行完之后才会进行线程切换,切换操作对线程可知,一般不会有线程同步问题
- 一个线程坚持不让出处理器执行时间,可能会导致整个系统奔溃
- 抢占式调度
- 由操作系统来分配执行时间
- 可以使用优先级来给操作系统建议,给线程分配执行时间的长短
状态转换
- Java语言定义了6中状态,任意时间一个线程只能有且只有一个状态
- 新建:创建后尚未启动的线程处于这种状态
- 运行:包括操作系统线程状态中的Running和Ready,处于此状态的线程有可能正在执行,也有可能正在等待操作系统分配执行时间
- 无限期等待:处于这种状态的线程不会被分配处理器执行时间,要等待其他线程显式唤醒
- 没有设置Timeout参数的Object::wait()方法
- 没有设置Timeout参数的Thread::join()方法
- LockSupport::park()方法
- 限期等待:处于这种装填的线程不会被分配处理器执行时间,但是一定时间后会由系统自动唤醒
- Thread::sleep()方法
- 设置了Timeout参数的Object::wait()方法
- 设置了Timeout参数的Thread::join()方法
- LockSupport::parkNanos()方法
- LockSupport::parkUntil()方法
- 阻塞:阻塞状态等待一个排它锁,程序进入同步区域的时候,线程将进入这种状态
- 结束:线程已经结束执行
协程
- 应用自己模拟多线程的做法,多数用户线程是被设计成协同式调度的,所以被叫做协程
- 有栈协程:协程能完整的做调用栈的保护、恢复工作
- 无栈协程:本质是一种有限状态机,状态保存在闭包里,功能有限,比有栈协程调用栈轻量的多
- 优势
- 轻量,HotSpot的线程栈容量为1MB,内核数据结构16KB内存;协程的栈通常在几百字节到几KB之间,Java虚拟机里线程池容量达到200已经很高了,但是很多支持协程的应用中,同时并存的协程数量可数以十万计。
- 局限
- 需要在应用层面实现的内容(调用栈,调度器等)特别多
动态链接和静态链接
- 动态链接
- 把链接这个过程推迟到了运行时再进行,在可执行文件装载时或运行时,由操作系统的装载程序加载库。
- 这里的库指的是动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。
- 优点
- 生成的可执行文件较静态链接生成的可执行文件小;
- 适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试;
- 不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;
- DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性;
- 缺点
- 使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息;
- 速度比静态链接慢;
- 静态链接
- 静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。
- 链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一块生成可执行程序
- 这里的库指的是静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。
- 优点
- 代码装载速度快,执行速度略比动态链接库快;
- 只需保证在开发者的计算机中有正确的.lib文件,在以二进制形式发布程序时不需考虑在用户的计算机上.lib文件是否存在及版本问题。
- 缺点
- 使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费。
死锁
-
四个必要条件
-
互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待
-
请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求时,该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放
-
不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)
-
循环等待条件: 若干进程间形成首尾相接循环等待资源的关系
-
-
银行家算法
-
在避免死锁的方法中,允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,令进程等待
因此,避免死锁的实质在于:系统在进行资源分配时,如何使系统不进入不安全状态
-
内存管理
- 块式管理 : 远古时代的计算机操系统的内存管理方式。将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为碎片。
- 页式管理 :把主存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。
- 段式管理 : 页式管理虽然提高了内存利用率,但是页式管理其中的页实际并无任何实际意义。 段式管理把主存分为一段段的,每一段的空间又要比一页的空间小很多 。但是,最重要的是段是有实际意义的,每个段定义了一组逻辑信息,例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。
- 段页式管理机制 :段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说 段页式管理机制 中段与段之间以及段的内部的都是离散的。
快表
为了解决虚拟地址到物理地址的转换速度,操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地址到物理地址的转换。我们可以把快表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。
使用快表之后的地址转换流程是这样的:
- 根据虚拟地址中的页号查快表;
- 如果该页在快表中,直接从快表中读取相应的物理地址;
- 如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中;
- 当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。
内存管理单元(MMU)
- 现代处理器使用的是一种称为 虚拟寻址(Virtual Addressing) 的寻址方式。使用虚拟寻址,CPU 需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。 实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为 内存管理单元(Memory Management Unit, MMU) 的硬件
虚拟内存
- 通过 虚拟内存 可以让程序可以拥有超过系统物理内存大小的可用内存空间。
- 虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间)。这样会更加有效地管理内存并减少出错。
- 虚拟内存的重要意义是它定义了一个连续的虚拟地址空间,并且 把内存扩展到硬盘空间,实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换
实现方式
- 请求分页存储管理 :建立在分页管理之上,为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能。请求分页是目前最常用的一种实现虚拟存储器的方法。请求分页存储管理系统中,在作业开始运行之前,仅装入当前要执行的部分段即可运行。假如在作业运行的过程中发现要访问的页面不在内存,则由处理器通知操作系统按照对应的页面置换算法将相应的页面调入到主存,同时操作系统也可以将暂时不用的页面置换到外存中。
- 请求分段存储管理 :建立在分段存储管理之上,增加了请求调段功能、分段置换功能。请求分段储存管理方式就如同请求分页储存管理方式一样,在作业开始运行之前,仅装入当前要执行的部分段即可运行;在执行过程中,可使用请求调入中断动态装入要访问但又不在内存的程序段;当内存空间已满,而又需要装入新的段时,根据置换功能适当调出某个段,以便腾出空间而装入新的段。
- 请求段页式存储管理
页面置换算法
- OPT 页面置换算法(最佳页面置换算法) :最佳(Optimal, OPT)置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。一般作为衡量其他置换算法的方法。
- FIFO(First In First Out) 页面置换算法(先进先出页面置换算法) : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。
- LRU (Least Recently Used)页面置换算法(最近最久未使用页面置换算法) :LRU算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。
- LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法) : 该置换算法选择在之前时期使用最少的页面作为淘汰页。
JVM
JVM内存划分(需完善)
堆的回收算法
标记清除算法
- 过程
- 标记出所有需要回收的对象
- 统一回收所有未被标记的对象
- 缺点
- 执行效率不稳定,如果堆中包含大量对象,且大部分需要被回收,必须进行大量标记和清除动作,两个过程的执行效率随着对象数量的增加而降低
- 内存空间的碎片化问题,标记清除后会产生大量不连续的内存碎片
标记复制算法
-
过程
- 内存容量划分为大小相等的两块,每次只是用其中一块
- 每次垃圾回收,将存活着的对象复制到另外一块上面,把已使用过得内存空间一次清理掉
-
缺点
- 可用内存缩小为原来的一半,浪费内存空间
- 对象存活率较高时,需要进行较多的复制操作,效率会降低
-
大多虚拟机使用标记复制算法回收新生代
- 新生代中的对象,98%熬不过第一轮收集
- 新生代分为Eden区和2块Survivor区,每次只使用Eden区和一块Survivor区,垃圾收集时,将存活的对象一次性复制到另一块Survivor区
- Eden区和Survivor区的大小比例是8:1
- Survivor空间不足以容纳一次Minor GC之后的对象时,进行分配担保,这些对象直接进去老年代
标记整理算法
- 过程
- 标记出所有需要回收的对象
- 让所有存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存
- 缺点
- 在老年代每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方,是极为负重的操作,并且移动操作必须全程暂停用户应用程序才能进行,即STW
Full GC触发的条件
-
Minor GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内 存的大小。此时老年代都放不下这些存活对象了,就会发生“Handle Promotion Failure”的情况,这个时候就会触 发一次“Full GC”如果要是Full GC过后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致所谓的 “OOM”内存溢出了
-
如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发Full GC。为了避免这种情况,最好就是不要创建太大的对象。
-
如果有持久代空间的话,系统当中需要加载的类,调用的方法很多,同时持久代当中没有足够的空间,就出触发一次Full GC
-
老年代可用内存小于新生代全部对象的大小,如果没开启空间担保参数,会直接触发Full GC,所以一般空间担保参数都会打 开;开启空间担保参数,老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC;
-
CMS收集器老年代已经使用的 内存空间超过了这个参数“-XX:CMSInitiatingOccupancyFaction”指定的比例,也会自动触发Full GC
-
显示调用System.gc
可以作为GC Root的对象
- 虚拟机栈中引用的对象(栈帧中的本地变量表中引用的对象)
- 方法区中的常量引用的对象
- 方法区中的类静态属性引用的对象
- 本地方法栈中JNI(Native方法)的引用对象
- 活跃线程的引用对象
内存分配策略
-
对象在Eden区分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC
-
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)
JVM参数
-XX:PretenureSizeThreshold
可以设置大 对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代 -
长期存活的对象将进入老年代
虚拟机给每个对象一个对象年龄计数器
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度 (默认为15岁,CMS收集器默认6岁),就会被晋升到老年代中。对象晋升到老年代 的年龄阈值,可以通过参数
-XX:MaxTenuringThreshold
来设置 -
对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的 50%(
-XX:TargetSurvivorRatio
可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会 把年龄n(含)以上的对象都放入老年代
这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的
-
老年代空间分配担保机制
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间
如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象) 就会看一个
“-XX:-HandlePromotionFailure”
(jdk1.8默认就设置了)的参数是否设置了 如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾, 如果回收完还是没有足够空间存放新的对象就会发生OOM,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生OOM
垃圾收集器(需完善)
CMS
G1
内存泄漏
-
定义
- 对象不再被应用程序使用,但是垃圾回收器却不能移除它们,因为它们正在被引用。
-
示例:
- 对象A引用了对象B。A的生命周期(t1—t4)要比B的生命周期(t2—t3)长很多。当B不再用于应用中时,A仍然持有对它的引用。在这种方式下,垃圾回收器就不能将B从内存中移除。这将可能导致出现内存不足的问题,因为如果A对更多的对象做同样的事情,那么内存中将会有很多无法被回收的对象,这将极度耗费内存空间。
- 也有可能B持有大量对其他对象的引用,这些被B引用的对象也不能够被回收。所有这些未被使用的对象将会耗费宝贵的内存空间。
-
阻止内存泄漏
- 集合类,如HashMap,ArrayList是内存泄漏经常发生的地方,当它们被声明为静态时,它们的生命周期和应用程序的生命周期一样长。
- 注意事件监听器和回调,如果一个监听器已经注册,但是当这个类不再被使用时却未被注销,就会发生内存泄漏。
内存溢出
- 解决办法
- 设置jvm值的方法是通过-Xms(堆的最小值),-Xmx(堆的最大值)
- 设置栈大小的方法是设置-Xss参数
JMM内存模型
-
本身随着CPU和内存的发展速度差异的问题,导致CPU的速度远快于内存,所以现在的CPU加入了高速缓存,高速缓存一般可以分为L1、L2、L3三级缓存。基于上面的例子我们知道了这导致了缓存一致性的问题,所以加入了缓存一致性协议,同时导致了内存可见性的问题,而编译器和CPU的重排序导致了原子性和有序性的问题,JMM内存模型正是对多线程操作下的一系列规范约束,因为不可能让程序员的代码去兼容所有的CPU,通过JMM我们才屏蔽了不同硬件和操作系统内存的访问差异,这样保证了Java程序在不同的平台下达到一致的内存访问效果,同时也是保证在高效并发的时候程序能够正确执行。
-
-
原子性:Java内存模型通过read、load、assign、use、store、write来保证原子性操作,此外还有lock和unlock,直接对应着synchronized关键字的monitorenter和monitorexit字节码指令。
可见性:可见性的问题在上面的回答已经说过,Java保证可见性可以认为通过volatile、synchronized、final来实现。
有序性:由于处理器和编译器的重排序导致的有序性问题,Java通过volatile、synchronized来保证。
happen-before规则
虽然指令重排提高了并发的性能,但是Java虚拟机会对指令重排做出一些规则限制,并不能让所有的指令都随意的改变执行位置,主要有以下几点:
- 单线程每个操作,happen-before于该线程中任意后续操作
- volatile写happen-before与后续对这个变量的读
- synchronized解锁happen-before后续对这个锁的加锁
- final变量的写happen-before于final域对象的读,happen-before后续对final变量的读
- 传递性规则,A先于B,B先于C,那么A一定先于C发生
volatile
-
lock 前缀的指令在多核处理器下会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存。
- 写回内存的操作会使在其他 CPU 里缓存了该内存地址的额数据无效。
-
缓存一致性协议
- 缓存是分段(line)的,一个段对应一块存储空间,称之为缓存行,它是 CPU 缓存中可分配的最小存储单元,大小 32 字节、64 字节、128 字节不等,这与 CPU 架构有关,通常来说是 64 字节。 LOCK# 因为锁总线效率太低,因此使用了多组缓存。 为了使其行为看起来如同一组缓存那样。因而设计了 缓存一致性协议。 缓存一致性协议有多种,但是日常处理的大多数计算机设备都属于 " 嗅探(snooping)" 协议。 所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线。 缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个 CPU 缓存可以读写内存)。 CPU 缓存不仅仅在做内存传输的时候才与总线打交道,而是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。 当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。 只要某个处理器写内存,其它处理器马上知道这块内存在它们的缓存段中已经失效。
-
相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,他没有上下文切换的额外开销成本。使用volatile声明的变量,可以确保值被更新的时候对其他线程立刻可见。volatile使用内存屏障来保证不会发生指令重排,解决了内存可见性的问题。
-
volatile不能保证完全的原子性,只能保证单次的读/写操作具有原子性。
-
volatile修饰之后会加入不同的内存屏障来保证可见性的问题能正确执行。这里写的屏障基于书中提供的内容,但是实际上由于CPU架构不同,重排序的策略不同,提供的内存屏障也不一样,比如x86平台上,只有StoreLoad一种内存屏障。
- StoreStore屏障,保证上面的普通写不和volatile写发生重排序
- StoreLoad屏障,保证volatile写与后面可能的volatile读写不发生重排序
- LoadLoad屏障,禁止volatile读与后面的普通读重排序
- LoadStore屏障,禁止volatile读和后面的普通写重排序
四大引用
- 强引用
- 当JVM进行GC时,对于强引用对象,就算出现了OOM也不会对该对象进行回收。强引用是造成Java内存泄露的主要原因之一
- 软引用
- 当系统内存充足时,软引用不会回收;
- 当系统内存不足时,软引用会被回收,回收后内存仍然不足,就抛出异常;
- 弱引用
- 弱引用通过
java.lang.ref.WeakReference
来完成,当JVM进行GC时,无论内存是否充足,被弱引用关联的对象都会被回收,即弱引用关联的对象活不到下一次GC时刻
- 弱引用通过
- 虚引用
- 虚引用需要
java.lang.ref.PhantomReference
类来实现,如果一个对象仅持有虚引用,那么它和没有任何引用一样,调用get()方法总返回null,在任何时候都可能被垃圾回收器回收,虚引用必须和引用队列(ReferenceQueue)联合使用: - 虚引用的唯一目的就是:当该对象被垃圾收集器回收的时候收到一个系统通知或者后续添加进一步处理,它的作用在于跟踪垃圾回收过程
- 虚引用需要
锁的优化机制
锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。
自旋锁:
- 由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。
- 自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。
自适应锁:
- 自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。
- 对于某一个锁,自旋很少成功获得过锁,那在以后要获取这个锁时,可能会直接省略掉自旋过程,以避免浪费处理器资源
锁消除:
- 锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。
- 通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配
锁粗化:
- 锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。
偏向锁:
- 当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。
- 对象进入偏向状态后,Mark Word大部分的空间都用来存储持有锁的线程ID了,占用了原来存储对象哈希码的位置
- 当一个对象已经计算过一次一致性哈希码之后,它就再也无法进入偏向锁状态了
- 当一个对象正处于偏向锁状态,收到计算其一致性哈希码请求时,偏向状态会被立即撤销,并且膨胀为重量级锁
轻量级锁:
- JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。
- 在代码即将进入同步块的时候,如果此同步对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象当前的Mark Word的拷贝
- 虚拟机使用CAS尝试把对象的Mark Word更新为指向Lock Record的指针,更新成功,该线程拥有这个对象的锁,并将Mark Word的锁标志位转变为00
- 出现两条以上的线程争夺同一个锁时,膨胀为重量级锁,锁标志的状态值变为10
- 解锁过程中,如果对象的Mark Word仍然指向线程的锁记录,使用CAS把对象的Mark Word和线程中复制的Displace Mark Word替换回来
Synchronized和lock
-
Synchronized
-
synchronized是java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁,使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。
执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。
执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。
synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。
从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,而释放锁的过程则是将工作内存中的共享变量写回主内存。
如果再深入到源码来说,synchronized实际上有两个队列waitSet和entryList。
- 当多个线程进入同步代码块时,首先进入entryList
- 有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
- 如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁
- 如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null
-
-
区别
- synchronized属于JVM层面,底层通过
monitorenter
和 monitorexit 两个指令实现;lock是JUC提供的具体类,是API层面的东西; - synchronized不需要用户手动释放锁,当synchronized代码执行完毕之后会自动让线程释放持有的锁;lock需要一般使用try-finally模式去手动释放锁,并且加锁-解锁数量需要一直,否则容易出现死锁或者程序不终止现象;
- synchronized是不可中断的,除非抛出异常或者程序正常退出;lock可中断:设置超时方法
tryLock(time, unit);
使用lockInterruptibly
,调用interrupt方法可中断; - synchronized是非公平锁;lock默认是非公平锁,但是可以通过构造函数传入boolean类型值更改是否为公平锁;
- 锁是否能绑定多个条件:synchronized没有condition的说法,要么唤醒所有线程,要么随机唤醒一个线程;lock可以使用condition实现分组唤醒需要唤醒的线程,实现精准唤醒;
- 加解锁顺序不同,对于 Lock 而言如果有多把 Lock 锁,Lock 可以不完全按照加锁的反序解锁,比如我们可以先获取 Lock1 锁,再获取 Lock2 锁,解锁时则先解锁 Lock1,再解锁 Lock2,加解锁有一定的灵活度
- synchronized 锁只能同时被一个线程拥有,但是 Lock 锁没有这个限制,例如在读写锁中的读锁,是可以同时被多个线程持有的,可是
synchronized
做不到 - 性能区别:在 Java 5 以及之前,
synchronized
的性能比较低,但是到了 Java 6 以后,发生了变化,因为 JDK 对 synchronized 进行了很多优化,比如自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等,所以后期的 Java 版本里的 synchronized 的性能并不比 Lock 差。
- synchronized属于JVM层面,底层通过
锁
ReentrantLock
相比于synchronized,ReentrantLock需要显式的获取锁和释放锁,相对现在基本都是用JDK7和JDK8的版本,ReentrantLock的效率和synchronized区别基本可以持平了。他们的主要区别有以下几点:
- 等待可中断,当持有锁的线程长时间不释放锁的时候,等待中的线程可以选择放弃等待,转而处理其他的任务。
- 公平锁:synchronized和ReentrantLock默认都是非公平锁,但是ReentrantLock可以通过构造函数传参改变。只不过使用公平锁的话会导致性能急剧下降。
- 绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象。
ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现。
AQS内部维护一个state状态位,尝试加锁的时候通过CAS(CompareAndSwap)修改值,如果成功设置为1,并且把当前线程ID赋值,则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空。
读写锁(需完善)
乐观锁
悲观锁
对象头
在我们常用的Hotspot虚拟机中,对象在内存中布局实际包含3个部分:
- 对象头
- 实例数据
- 对齐填充
而对象头包含两部分内容,Mark Word中的内容会随着锁标志位而发生变化,所以只说存储结构就好了。
- 对象自身运行时所需的数据,也被称为Mark Word,也就是用于轻量级锁和偏向锁的关键点。具体的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程ID、偏向锁时间戳。
- 存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。
如果是数组的话,则还包含了数组的长度
CAS
CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:
- 变量内存地址,V表示
- 旧的预期值,A表示
- 准备设置的新值,B表示
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。
存在的问题:
-
ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。
Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
-
循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。
-
只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。
类加载机制
- 类加载的整个生命周期:加载,验证,准备,解析,初始化,使用,卸载
- 这些阶段通常都是相互交叉的混合进行的,会在一个阶段执行的过程中调用,激活另一个阶段
类加载时机
-
遇到new、getstatic、putstatic或invokestatic四条字节码指令的时候,如果类型没有进行过初始化,则需要先触发其初始化过程。
- 使用new关键字实例化对象的时候
- 读取或设置一个类型的静态字段
- 调用一个类型的静态方法
-
使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化过程。
-
当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
-
当虚拟机启动时,先初始化包含main的主类
-
接口中定义了默认方法,接口实现类发生了初始化,接口要在其之前初始化
注:
- 通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化
- 常量在编译阶段会存入调用类的常量池,因此不会触发定义常量的类的初始化
类加载过程
- 加载
- 主要完成三件事情
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 数组类本身不通过类加载器创建,由虚拟机直接在内存中动态构造出来
- 主要完成三件事情
- 验证
- 目的:确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全
- 验证阶段的工作量在类加载过程中占了相当大的比重,如果代码以及被反复使用验证过,可以考虑关闭大部分的类验证措施,缩短虚拟机类加载的时间
- 四个阶段
- 文件格式验证
- 该阶段验证基于二进制字节流进行,只有通过这个阶段的验证之后,这段字节流才被允许进入Java虚拟机内存的方法区进行存储
- 验证点:
- 是否以魔数0xCAFEBABE开头
- 主次版本号是否在当前Java虚拟机接受范围之内
- 常量池的常量 中是否有不被支持的常量类型
- 元数据验证
- 主要目的是对类的元数据信息进行语义校验
- 验证点:
- 这个类是否有父类
- 这个类的父类是否继承了不允许被继承的类
- 字节码验证
- 主要目的是通过数据流分析和控制流分析,确定语义是合法的,符合逻辑的
- 验证点
- 保证任何跳转指令不会跳转到方法体以外的字节码指令上
- JDK6之后,把尽可能多的校验辅助措施挪到javac编译器里进行,给方法体Code属性的属性表新增了一项“StackMapTable”的新属性,字节码验证期间,Jaba虚拟机就不需要根据程序推导这些状态的合法性,只需要检查StackMapTable属性中记录
- 符号引用验证
- 文件格式验证
- 准备
- 解析
- 初始化
双亲委派机制(需完善)
ThreadLocal原理
- 通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。
ThreadLocal
类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal
类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。 - 如果你创建了一个
ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal
变量名的由来。他们可以使用get()
和set()
方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题
ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。
ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。
弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。
但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。
但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的。
分布式
分布式事务
TCC
分布式锁(需完善)
RedLock
Linux
常用命令(需完善)
top
ps
grep
pwd
mkdir
算法
数据结构
布隆过滤器
-
概念
- 长度为m的bit类型数组,每个输入对象经过k个哈希函数运算过后,对算出来的结果都对m取余,在bit数组的相应位置设置为1
- 检查:如果一个对象经过k个哈希函数运算后,得到的结果,有一个不为1,则它不在集合中。否则判定它在集合中
- 误判:对发现的误报样本可以通过建立白名单来防止误报
-
布隆过滤器的大小
-
n是输入对象的个数,p是容错率
-
$$
m = -\frac{n * \ln p} {(\ln 2)^2}
$$
-
-
哈希函数的个数
-
m是布隆过滤器的大小,n是输入对象的个数
-
$$
k = \ln 2 * \frac {m}{n} = 0.7 * \frac {m}{n}
$$
-
基础算法
排序算法比较
排序算法 | 平均时间复杂度 | 最好 | 最坏 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
插入排序 | O(n2) | O(n2) | O(n2) | O(1) | 稳定 |
希尔排序 | O(n log n) | O(n log2 n) | O(n log 2n) | O(1) | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
快速排序 | O(n log n) | O(n log n) | O(n2) | O(log n) | 不稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
快速排序
-
private static void quickSort(int[] arr, int left, int right) { int l = left; int r = right; int base = arr[(left + right) / 2]; while (l < r){ while (arr[l] < base){ l++; } while (arr[r] > base){ r--; } if (l >= r){ break; } int temp = arr[l]; arr[l] = arr[r]; arr[r] = temp; if (arr[l] == base){ r--; } if (arr[r] == base){ l++; } } if (l == r) { l += 1; r -= 1; } if (left < r) { quickSort(arr, left, r); } if (right > l) { quickSort(arr, l, right); } }
希尔排序
-
算法先将要排序的一组数按某个增量d(n/2,n为要排序数的个数)分成若干组,每组中记录的下标相差d,对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。当增量减到1时,进行直接插入排序后,排序完成
-
代码实现
-
public static int[] ShellSort(int[] array) { int len = array.length; int temp, gap = len / 2; while (gap > 0) { for (int i = gap; i < len; i++) { temp = array[i]; int preIndex = i - gap; while (preIndex >= 0 && array[preIndex] > temp) { array[preIndex + gap] = array[preIndex]; preIndex -= gap; } array[preIndex + gap] = temp; } gap /= 2; } return array; }
-
插入排序
-
插入排序算法的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入,因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间
-
代码实现
-
public static void insertSort(int[] arr){ int n = arr.length; if (n <= 1){ return; } int temp,j; for (int i = 0; i < n; i++) { temp = arr[i]; for (j = i; j > 0; j--) { if (arr[j - 1] > temp){ arr[j] = arr[j - 1]; }else { break; } } arr[j] = temp; } }
-
归并排序
-
代码实现
-
public class MergeSort { public static void main(String[] args) { int data[] = { 9, 5, 6, 8, 0, 3, 7, 1 }; mergeSort(data, 0, data.length - 1); System.out.println(Arrays.toString(data)); } public static void mergeSort(int data[], int left, int right) { // 数组的两端 if (left < right) { // 相等了就表示只有一个数了 不用再拆了 int mid = (left + right) / 2; mergeSort(data, left, mid); mergeSort(data, mid + 1, right); // 分完了 接下来就要进行合并,也就是我们递归里面归的过程 merge(data, left, mid, right); } } public static void merge(int data[], int left, int mid, int right) { int temp[] = new int[data.length]; //借助一个临时数组用来保存合并的数据 int point1 = left; //表示的是左边的第一个数的位置 int point2 = mid + 1; //表示的是右边的第一个数的位置 int loc = left; //表示的是我们当前已经到了哪个位置了 while(point1 <= mid && point2 <= right){ if(data[point1] < data[point2]){ temp[loc] = data[point1]; point1 ++ ; loc ++ ; }else{ temp[loc] = data[point2]; point2 ++; loc ++ ; } } while(point1 <= mid){ temp[loc ++] = data[point1 ++]; } while(point2 <= right){ temp[loc ++] = data[point2 ++]; } for(int i = left ; i <= right ; i++){ data[i] = temp[i]; } } }
-
堆排序
-
对于每个节点的值都大于等于子树中每个节点值的堆,我们叫做“大顶堆”。
-
对于每个节点的值都小于等于子树中每个节点值的堆,我们叫做“小顶堆”。
-
一般升序用大根堆,降序就用小根堆
-
代码实现
-
public static void maxHeap(int data[], int start, int end) { int parent = start; int son = parent * 2 + 1; // 下标是从0开始的就要加1,从1就不用 while (son < end) { int temp = son; // 比较左右节点和父节点的大小 if (son + 1 < end && data[son] < data[son + 1]) { // 表示右节点比左节点大 temp = son + 1; // 就要换右节点跟父节点 } // temp表示的是 我们左右节点大的那一个 if (data[parent] > data[temp]) return; // 不用交换 else { // 交换 int t = data[parent]; data[parent] = data[temp]; data[temp] = t; parent = temp; // 继续堆化 son = parent * 2 + 1; } } return; } public static void heapSort(int data[]) { int len = data.length; for (int i = len / 2 - 1; i >= 0; i--) { maxHeap(data, i, len); } for (int i = len - 1; i > 0; i--) { int temp = data[0]; data[0] = data[i]; data[i] = temp; maxHeap(data, 0, i); } }
-
斐波那契数列
-
//递归 class Solution { public int fib(int n) { if(n <= 1){ return n; } return fib(n-1) + fib(n-2); } }
-
//动态规划 class Solution { public int fib(int n) { int a = 0, b = 1, sum; for(int i = 0; i < n; i++){ sum = (a + b); a = b; b = sum; } return a; } }
-
//通项公式 class Solution { public int fib(int n) { double sqrt5 = Math.sqrt(5); double fibN = Math.pow((1 + sqrt5) / 2, n) - Math.pow((1 - sqrt5) / 2, n); return (int) Math.round(fibN / sqrt5); } }
背包问题
01背包
-
问题描述:一共有 N 件物品,第 i(i 从 1 开始)件物品的重量为 w[i],价值为 v[i]。在总重量不超过背包承载上限 W 的情况下,能够装入背包的最大价值是多少?
-
解法:如果是 01 背包,即数组中的元素不可重复使用,外循环遍历 arrs,内循环遍历 target,且内循环倒序:
完全背包
- 问题描述:完全背包与 01 背包不同就是每种物品可以有无限多个:一共有 N 种物品,每种物品有无限多个,第 i(i 从 1 开始)种物品的重量为 w[i],价值为 v[i]。在总重量不超过背包承载上限 W 的情况下,能够装入背包的最大价值是多少?
- 解法:
- 如果是完全背包,即数组中的元素可重复使用并且不考虑元素之间顺序,arrs 放在外循环(保证 arrs 按顺序),target在内循环。且内循环正序。
- 如果组合问题需考虑元素之间的顺序,需将 target 放在外循环,将 arrs 放在内循环,且内循环正序。
LeetCode
无重复字符的最长子串
-
解题思路:滑动窗口
- 其实就是一个队列,比如例题中的
abcabcbb
,进入这个队列(窗口)为abc
满足题目要求,当再进入a
,队列变成了abca
,这时候不满足要求。所以,我们要移动这个队列! - 我们只要把队列的左边的元素移出就行了,直到满足题目要求!
- 其实就是一个队列,比如例题中的
-
class Solution { public int lengthOfLongestSubstring(String s) { if (s.length()==0) return 0; HashMap<Character, Integer> map = new HashMap<Character, Integer>(); int max = 0; int left = 0; for(int i = 0; i < s.length(); i ++){ if(map.containsKey(s.charAt(i))){ left = Math.max(left,map.get(s.charAt(i)) + 1); } map.put(s.charAt(i),i); max = Math.max(max,i-left+1); } return max; } }
寻找两个正序数组的中位数
-
解题思路:
- 归并排序
- 二分查找
- 题目是求中位数,其实就是求第
k
小数的一种特殊情况,而求第k
小数有一种算法。 - k 为 (m+n)/2(m+n)/2 或 (m+n)/2+1(m+n)/2+1。
- 题目是求中位数,其实就是求第
-
class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int length1 = nums1.length, length2 = nums2.length; int totalLength = length1 + length2; if (totalLength % 2 == 1) { int midIndex = totalLength / 2; double median = getKthElement(nums1, nums2, midIndex + 1); return median; } else { int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2; double median = (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0; return median; } } public int getKthElement(int[] nums1, int[] nums2, int k) { /* 主要思路:要找到第 k (k>1) 小的元素,那么就取 pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较 * 这里的 "/" 表示整除 * nums1 中小于等于 pivot1 的元素有 nums1[0 .. k/2-2] 共计 k/2-1 个 * nums2 中小于等于 pivot2 的元素有 nums2[0 .. k/2-2] 共计 k/2-1 个 * 取 pivot = min(pivot1, pivot2),两个数组中小于等于 pivot 的元素共计不会超过 (k/2-1) + (k/2-1) <= k-2 个 * 这样 pivot 本身最大也只能是第 k-1 小的元素 * 如果 pivot = pivot1,那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums1 数组 * 如果 pivot = pivot2,那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums2 数组 * 由于我们 "删除" 了一些元素(这些元素都比第 k 小的元素要小),因此需要修改 k 的值,减去删除的数的个数 */ int length1 = nums1.length, length2 = nums2.length; int index1 = 0, index2 = 0; int kthElement = 0; while (true) { // 边界情况 if (index1 == length1) { return nums2[index2 + k - 1]; } if (index2 == length2) { return nums1[index1 + k - 1]; } if (k == 1) { return Math.min(nums1[index1], nums2[index2]); } // 正常情况 int half = k / 2; int newIndex1 = Math.min(index1 + half, length1) - 1; int newIndex2 = Math.min(index2 + half, length2) - 1; int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2]; if (pivot1 <= pivot2) { k -= (newIndex1 - index1 + 1); index1 = newIndex1 + 1; } else { k -= (newIndex2 - index2 + 1); index2 = newIndex2 + 1; } } } }
最长回文子串
-
解题思路:
- 动态规划
- 对于一个子串而言,如果它是回文串,并且长度大于 2,那么将它首尾的两个字母去除之后,它仍然是个回文串。
- 中心扩展
- 动态规划
-
//动态规划 public class Solution { public String longestPalindrome(String s) { int len = s.length(); if (len < 2) { return s; } int maxLen = 1; int begin = 0; // dp[i][j] 表示 s[i..j] 是否是回文串 boolean[][] dp = new boolean[len][len]; // 初始化:所有长度为 1 的子串都是回文串 for (int i = 0; i < len; i++) { dp[i][i] = true; } char[] charArray = s.toCharArray(); // 递推开始 // 先枚举子串长度 for (int L = 2; L <= len; L++) { // 枚举左边界,左边界的上限设置可以宽松一些 for (int i = 0; i < len; i++) { // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得 int j = L + i - 1; // 如果右边界越界,就可以退出当前循环 if (j >= len) { break; } if (charArray[i] != charArray[j]) { dp[i][j] = false; } else { if (j - i < 3) { dp[i][j] = true; } else { dp[i][j] = dp[i + 1][j - 1]; } } // 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置 if (dp[i][j] && j - i + 1 > maxLen) { maxLen = j - i + 1; begin = i; } } } return s.substring(begin, begin + maxLen); } }
-
//中心扩散 class Solution { public String longestPalindrome(String s) { if (s == null || s.length() < 1) { return ""; } int start = 0, end = 0; for (int i = 0; i < s.length(); i++) { int len1 = expandAroundCenter(s, i, i); int len2 = expandAroundCenter(s, i, i + 1); int len = Math.max(len1, len2); if (len > end - start) { start = i - (len - 1) / 2; end = i + len / 2; } } return s.substring(start, end + 1); } public int expandAroundCenter(String s, int left, int right) { while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) { --left; ++right; } return right - left - 1; } }
Z 字形变换
-
public class Solution { public String convert(String s, int numRows) { if (numRows < 2) return s; List<StringBuilder> rows = new ArrayList<StringBuilder>(); for (int i = 0; i < numRows; i++) rows.add(new StringBuilder()); int i = 0, flag = -1; for (char c : s.toCharArray()) { rows.get(i).append(c); if (i == 0 || i == numRows - 1) flag = -flag; i += flag; } StringBuilder res = new StringBuilder(); for (StringBuilder row : rows) res.append(row); return res.toString(); } }
两两交换链表中的节点
-
解题思路:
- 递归
- 递归
-
class Solution { public ListNode swapPairs(ListNode head) { if (head == null || head.next == null) { return head; } ListNode newHead = head.next; head.next = swapPairs(newHead.next); newHead.next = head; return newHead; } }
括号生成
-
解题思路:
- 回溯算法
-
public class Solution { public List<String> generateParenthesis(int n) { List<String> res = new ArrayList<>(); // 特判 if (n == 0) { return res; } dfs("", 0, 0, n, res); return res; } /** * @param curStr 当前递归得到的结果 * @param left 左括号已经用了几个 * @param right 右括号已经用了几个 * @param n 左括号、右括号一共得用几个 * @param res 结果集 */ private void dfs(String curStr, int left, int right, int n, List<String> res) { if (left == n && right == n) { res.add(curStr); return; } // 剪枝 if (left < right) { return; } if (left < n) { dfs(curStr + "(", left + 1, right, n, res); } if (right < n) { dfs(curStr + ")", left, right + 1, n, res); } } }
删除有序数组中的重复项
-
public int removeDuplicates(int[] nums) { int index = 0; for(int i = 1; i < nums.length; ++i){ if(nums[i] != nums[index]){ nums[++index] = nums[i]; } } return index + 1; }
搜索旋转排序数组
-
解题思路:
- 二分查找
-
class Solution { public int search(int[] nums, int target) { int n = nums.length; if (n == 0) { return -1; } if (n == 1) { return nums[0] == target ? 0 : -1; } int l = 0, r = n - 1; while (l <= r) { int mid = (l + r) / 2; if (nums[mid] == target) { return mid; } if (nums[l] <= nums[mid]) { if (nums[l] <= target && target < nums[mid]) { r = mid - 1; } else { l = mid + 1; } } else { if (nums[mid] < target && target <= nums[n - 1]) { l = mid + 1; } else { r = mid - 1; } } } return -1; } }
寻找旋转排序数组中的最小值 II
-
实现
class Solution { public int findMin(int[] nums) { int low = 0; int high = nums.length - 1; while (low < high) { int pivot = low + (high - low) / 2; if (nums[pivot] < nums[high]) { high = pivot; } else if (nums[pivot] > nums[high]) { low = pivot + 1; } else { high -= 1; } } return nums[low]; } }
旋转数组
-
数组翻转
-
代码实现
class Solution { public void rotate(int[] nums, int k) { k %= nums.length; reverse(nums, 0, nums.length - 1); reverse(nums, 0, k - 1); reverse(nums, k, nums.length - 1); } public void reverse(int[] nums, int start, int end) { while (start < end) { int temp = nums[start]; nums[start] = nums[end]; nums[end] = temp; start += 1; end -= 1; } } }
剑指offer
重建二叉树
-
代码实现
import java.util.HashMap; class Solution { int[] preorder; HashMap<Integer, Integer> dic = new HashMap<>(); public TreeNode buildTree(int[] preorder, int[] inorder) { this.preorder = preorder; for(int i = 0; i < inorder.length; i++) dic.put(inorder[i], i); return recur(0, 0, inorder.length - 1); } TreeNode recur(int root, int left, int right) { if(left > right) return null; // 递归终止 TreeNode node = new TreeNode(preorder[root]); // 建立根节点 int i = dic.get(preorder[root]); // 划分根节点、左子树、右子树 node.left = recur(root + 1, left, i - 1); // 开启左子树递归 node.right = recur(root + i - left + 1, i + 1, right); // 开启右子树递归 return node; // 回溯返回根节点 } }
旋转数组的最小数字
-
代码实现
class Solution { public int minArray(int[] numbers) { int low = 0; int high = numbers.length - 1; while (low < high) { int pivot = low + (high - low) / 2; if (numbers[pivot] < numbers[high]) { high = pivot; } else if (numbers[pivot] > numbers[high]) { low = pivot + 1; } else { high -= 1; } } return numbers[low]; } }
阶乘后的零
-
思路:找5的个数
-
代码实现
class Solution { public int trailingZeroes(int n) { int count = 0; while (n > 0) { count += n / 5; n = n / 5; } return count; }
快速幂
-
实现
class Solution { public double myPow(double x, int n) { if(x == 0) return 0; long b = n; double res = 1.0; if(b < 0) { x = 1 / x; b = -b; } while(b > 0) { if((b & 1) == 1) res *= x; x *= x; b >>= 1; } return res; } }
快速乘
-
代码实现
class Solution { int quickMulti(int A, int B) { int ans = 0; for ( ; B != 0; B >>= 1) { if ((B & 1) != 0) { ans += A; } A <<= 1; } return ans; } }
二叉搜索树的最近公共祖先
-
实现
class Solution { public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { while(root != null) { if(root.val < p.val && root.val < q.val) // p,q 都在 root 的右子树中 root = root.right; // 遍历至右子节点 else if(root.val > p.val && root.val > q.val) // p,q 都在 root 的左子树中 root = root.left; // 遍历至左子节点 else break; } return root; } }
-
思想
-
代码实现
class Solution { public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if(root == null || root == p || root == q) return root; TreeNode left = lowestCommonAncestor(root.left, p, q); TreeNode right = lowestCommonAncestor(root.right, p, q); if(left == null) return right; if(right == null) return left; return root; } }
面试
判断是否为平衡二叉树
class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null) {
return true;
} else {
return Math.abs(height(root.left) - height(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
}
public int height(TreeNode root) {
if (root == null) {
return 0;
} else {
return Math.max(height(root.left), height(root.right)) + 1;
}
}
}
二叉树中的最大路径和
class Solution {
int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGain(root);
return maxSum;
}
public int maxGain(TreeNode node) {
if (node == null) {
return 0;
}
// 递归计算左右子节点的最大贡献值
// 只有在最大贡献值大于 0 时,才会选取对应子节点
int leftGain = Math.max(maxGain(node.left), 0);
int rightGain = Math.max(maxGain(node.right), 0);
// 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
int priceNewpath = node.val + leftGain + rightGain;
// 更新答案
maxSum = Math.max(maxSum, priceNewpath);
// 返回节点的最大贡献值
return node.val + Math.max(leftGain, rightGain);
}
}
二叉树的最近公共祖先
class Solution {
private TreeNode ans;
public Solution() {
this.ans = null;
}
private boolean dfs(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) return false;
boolean lson = dfs(root.left, p, q);
boolean rson = dfs(root.right, p, q);
if ((lson && rson) || ((root.val == p.val || root.val == q.val) && (lson || rson))) {
ans = root;
}
return lson || rson || (root.val == p.val || root.val == q.val);
}
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
this.dfs(root, p, q);
return this.ans;
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析