JAVA相关
HashMap:
继承自AbstractMap,实现Map接口,以键值对形式,可null值null键,线程不安全。默认存储容量为16,负载因子0.75,当存储数量大于12(16 * 0.75)时自动扩容一倍,扩容为2的幂。数据结构为数组➕链表(JDK1.8后,当链表中数据超过8时转为红黑树)。put时,根据key.hashcode找到对应位置,无值则直接存储,有值则key.equal判断值是否相同,相同则替换,否则插入链表中。get时key.equal直取。
HashTable:
继承自Dictionery,实现Map接口,默认容量11,扩容2*size + 1,线程安全。
ConcurrentHashMap:
线程安全
JDK1.7 segment数组+多个HashEntry,适用分段锁。
JDK1.8 Node数组+链表+红黑树(synchronized + CAS + HashEntry + 红黑树),数据结构接近HashMap。
问:HashMap为什么默认容量是16?扩容为什么是2的幂?如何扩容?
答:为了减少hash碰撞,需要一个尽量均匀分布的hash函数,key.hashcode。index = key.hash & (length - 1),index的最终结果取决于hashcode值的最后几位,如果是16或者2的幂(length - 1的值二进制为1111,做与运算时index的最终结果就完全取决于hashcode后几位的值);扩容是在put方法中自动扩容,创建一个新的Entry数组为原来的两倍,重新计算元素在新数组中的位置,然后进行数据转移。
hash算法使用位运算的原因(h = key.hashcode)^(h>>>16)高16位二进制:为了使得到的hash值更加散列,减少hash冲突,提升性能。
JVM垃圾回收机制:
引用计数法和可达性分析法;
可达性分析法GCRoot对象:虚拟机栈中引用的对象(方法中的局部变量)、方法区中类静态变量引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象;
JVM内存模型:
方法区:存储每个类的信息,静态变量,常量以及编译器编译后的代码;
堆:存储对象的实体,是垃圾回收主要管理区域;
虚拟机栈:java方法执行的内存模型,java栈中存放的是一个个栈帧,每个栈帧对应被调用的方法,栈帧中包含局部变量表、操作数栈、指向当前方法所属的类的运行时常量池的引用、方法返回地址和一些额外的附加信息;
本地方法栈:Native方法;
程序计数器:是一块较小的内存空间,是一个计数器,改变这个计数器的值来执行字节码指令,此区域是唯一一个没有oomError的区域;
方法区和堆是线程共享的数据区。虚拟机栈、本地方法栈、程序计数器是线程隔离的数据区。
引用计数器:引用对象计数器+1,不被引用计数器-1,为0时回收,但无法解决对象互相引用的问题。
可达性分析法俩次标记过程:
1.没有GCRoots相连的引用链,标记一次筛选,若对象没有重写finalize方法或者重写的finalize方法被虚拟机执行过,即对象将会被回收;若对象重写了finalize方法并且未被执行,则该对象会被放置在F-Queue的队列中,后续由Finalizer线程执行处理。
2.对F-Queue中对象进行二次标记,如连上引用链,则移除,否则回收。
垃圾回收算法:
标记-清除:标记需要回收的对象,回收所有被标记的对象。缺点:效率不高,空间上标记清除后会产生大量不连续的内存碎片,后续分配内存时无法找到足够的连续内存。
标记-整理:标记需要回收的对象,整理后清除被标记的对象。缺点:效率低。
复制算法:内存分为两块,存活的对象复制到另一块内存上。缺点:内存缩小为原来的一半,可用内存降低。大部分商用虚拟机采用该算法回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor1和Survivor2,当回收时,将Eden、S1复制到S2,清理Eden和S1,默认比例8:1:1。
分代收集法:将java堆分为新生代和老年代,不同的区域采用不同的算法。新生代一般采用复制算法,老年代使用标记-清除或标记-整理算法。
年轻代垃圾回收:大多数情况下,Eden区没有足够空间触发MinorGC。
老年代垃圾回收:1.老年代可用的连续内存空间” < “新生代历次Young GC后升入老年代的对象总和的平均大小,此时先old gc 后MinorGC;2.执行 Young GC之后有一批对象要放入老年代,此时老年代没有足够的内存空间存放对象,进行old gc;3.老年代空间不足进行old gc。
FullGC:年轻代、老年代、永久代回收。
通常情况下,每经过一次Minor Gc,年龄 + 1,当年龄大于等于阈值放入老年代,为了保证Survivor区能够得到充分利用,每进行一次Minor gc,虚拟机会将from区和to区互换位置。
年轻代进入老年代的几种情况:
1.年龄超过阈值;
2.大对象直接进入老年代;
3.长期存活的对象进入老年代。
垃圾收集器:
1.Serival收集器:单线程,新生代标记-清除算法;
2.ParNew收集器:多线程,新生代复制算法;
3.Paravel Scavenge收集器:多cpu核心,复制算法;
4.CMS收集器:标记-清除算法;
5.G1收集器:将堆分成很多单元格,不局限年轻代、老年代,标记-整理算法。
JAVA线程同步机制:
事物:
原子性:事物被视为一个不可分割的操作单元,要么全部执行成功,要么全部失败回滚;
一致性:事物执行前后,数据库必须处于一致状态满足所有数据约束;
隔离性:并发执行的事物之间应相互隔离互不干扰,以防止数据损坏或读取到不一致的数据;
持久性:事物提交后,其对数据库的修改应永久保存,即使在系统故障或重启后也不应丢失。
乐观锁:每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。
悲观锁:线程得到锁后其他线程挂起。
CAS:compare and swap 比较并替换
三个基本操作数:内存位置V、预期原值A、新值B
更新一个变量的时候,只有当变量的预期原值A和内存位置V的值相同时,才会将内存位置V对应的值修改为新值B。
CAS存在的问题:
1.ABA问题:加版本号,java1.5后提供AtomicStampedRefrence;
2.循环时间长开销大:java8中的LongAdder。分段CAS和自动分段迁移;
3.一个变量的原子操作:java1.5后使用AtomticRefrence,把多个变量放在一个对象进行CAS操作。
volatile关键字:
可读性:遵循缓存一致性原则(处理器通过检查自己缓存的数据同主内存中值是否一致,不一致则设为无效状态,并强制从主内存(注:线程主体有工作内存和主内存,工作内存进行变量操作,主内存线程共享)中取最新值);
有序性:防止指令重排,通过设置内存屏障,保证读取都立刻从主内存中。保证变量1.分配内存空间2.初始化3.指向分配的内存地址的顺序不变;
适用volatile达到线程间变量共享的问题,但无法保证原子性。可以通过synchronized/lock或Atomic(java提供的CAS)来保证原子性。
synchronized关键字:
对象由对象头、实例数据、对齐填充组成。
对象头:
Mark Word(hashcode、Gc标志或分代年龄或锁信息等);
Klass Word(对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例)。
Monitor:
监视器或管程,每一个java对象都会关联一个Monitor对象。
1.Owner:它标识了当前持有Monitor对象的线程信息;
2.EntryList:阻塞队列,没有争取到锁的线程会放到EntryList中;
3.WaitSet:当持有锁的对象调用了锁对象的wait方法时,会释放锁,当前线程就会从Owner退出来,加入到WaitSet中,并且变成waiting状态,等待重新唤醒。
同步方法(synchronized void method())底层原理:
方法级的同步是隐式,JVM可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否是同步方法,方法调用时先检查标志是否被设置,如设置则线程持有Monitor,方法完成释放Monitor。
同步代码块(synchronized(xx.class))底层原理:
当同步代码块开始位置(monitorenter),当前线程尝试获取monitor,如monitor的计数器为0,则可以获取monitor,并将计数器+1,如果其他线程已经拥有monitor,当前线程阻塞,等待其他线程执行monitorexit释放monitor并设置计数器为0。
一句话总结:
自旋拿锁,拿到+1,拿不到等待(没拿到,尝试拿一次,再自旋去拿,实在拿不到就去竞争队列等待唤醒),释放后进入减减操作,直到为0然后唤醒队列。当拥有锁的线程执行了wait方法后,线程释放锁,Owner变成null,同时将该线程放入到waitSet中,调用notify方法随机唤醒waitSet队列中某一个线程,释放锁后,会唤醒EntryList中所有线程获取锁。
Lock原理:
存储结构:一个int类型状态值,一个双向链表(存储等待中的线程)
获取锁的过程:本质上通过CAS来获取状态值修改,如果没获取到则将该线程放到等待链表中;
释放锁的过程:修改状态值,调整等待链表。
ArrayList和LinkedList
1.ArrayList动态数组结构,LinkedList双向链表结构;
2.ArrayList存取快,时间复杂度为O(1)(插入时不扩容),LinkedList存储时间复杂度为O(N);
3.LinkdedList更占内存,每个节点存储了两个引用;
4.多数据时ArrayList增删慢;
5.都是线程不安全。
String和StringBuffer和StringBuidler
1.String是字符串常量,内容不可改变(重新赋值时会创建新的对象并指向常量池中新的常量);
2.StringBuffer线程安全,速度慢;StringBuilder线程不安全,速度快;
Serializable和Parcelable
1.Parcelable性能好,内存开销小,内存间数据传输推荐使用;
2.Serializable可将数据持久化保存,保存或网络数据时推荐使用;
3.Parcelable数据仅在内存中存在,是通过Binder通信;
4.Serializable实现简单,Parcelable需要实现writeToParcel、describeContents以及静态的Creator变量。
注解:
一种元数据标记
标准注解:java自带的,包括@Override、@Deprecated、@SuppressWarnnings等;
元注解:用于定义注解的注释,包括@Retention、@Target等;
自定义注解:可根据自己的需求自定义;
适用场景:编译时检查、运行时处理、文档生成、测试框架等。
AOP
面向切面编程,通过预编译方式和运行时动态代理实现程序功能的统一维护的一种技术,底层原理就是动态代理实现的。
当目标对象需实现接口,用JDK代理;
当目标对象不需要实现接口,用cglib代理;cglib代理也叫子类代理(当目标对象只是一个单独的对象,未实现任何的接口),它是在内存中构建一个子类对象,从而实现对目标对象功能的扩展。
java常用设计模式:
1.观察者模式 常见EventBus、Rxjava、广播
如果观察者和被观察者中间有循环依赖,触发循环调用可能导致系统崩溃。
2.工厂模式
简单工厂模式:通过一个工厂类来封装对象的创建逻辑,根据客户传入的参数返回相应的产品对象;
工厂方法模式:每个具体子类负责创建一个特定产品对象,客户端通过调用工厂方法来获取所需的产品对象;
抽象工厂模式:创建一系列相关或相互依赖的对象接口,生产一系列相关的产品。
3.单例模式
将类的构造方法私有化,防止外部直接构造函数创建实例,类内部提供一个静态方法或变量来获取该类的唯一实例。
饿汉式:线程安全,类加载时就实例化,多个饿汉的单例占用内存,浪费资源,可以通过反射创建不同的对象破坏单例;
懒汉式:第一次调用时实例化,性能有延迟;
懒汉式双重校验锁原理(volatile + synchronized + 双重null判断):使用volatile防止指令重排,避免获取到的对象是未初始化完成的对象。使用synchronized保证线程安全。第一个判空,假设AB俩个线程,A进入判断并创建了实例,此时B不需要进入代码块就直接返回实例,可以提高效率,避免instance不为空仍然去竞争锁;第二个判空避免多个线程重复创建对象。
其他方式:静态内部类、枚举(可以防止反射破坏)
4.构造器模式 常见okhttp、dialog
抽象建造者Builder、具体建造者ConcreteBuilder、产品角色Product、指挥者Director
5.适配器模式 adapter
把一个类的接口变换成客户端期待的另一种接口,从而使原本接口不匹配而无法一起工作的两个类一起工作。
6.代理模式
通过代理对象来访问目标对象
静态代理:代理类和被代理类实现相同的接口,在编译期间就已经确定;
动态代理:在运行时创建代理对象,使用反射来动态生成代理类和代理对象,动态代理接口运行代理通过proxy(newProxyInstance)类和InvocationHandler(invoke方法)接口实现。