大华集团面经Java开发
JVM 内存模型
JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由新生代和老年代组成,而新生代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;
方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。
方法区和堆是所有线程共享的内存区域;而**java栈、本地方法栈(Native Method Stack)和程序员计数器是运行是线程私有的内存区域**。
百分之99的JVM调优都是在堆中调优,Java栈、本地方法栈、程序计数器是不会有垃圾存在的。
方法区(Method Area),是线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java1.8之前被称为永久代(PermGen),1.8改名为元空间(MetaSpace),后面都以元空间表述,常量池位于方法区中。
Java堆(Java Heap)与方法区一样是是线程共享的内存区域,Java虚拟机所管理的内存最大的一块。在虚拟机启动时创建,此内存区域的唯一目的就是**存放对象实例**,几乎所有的对象实例都在这里分配内存,并不是所有。
程序计数器内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
Java虚拟机栈(Java Virtual Machine Stacks)也是**线程私有的,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息**。
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,也是**线程私有的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务**。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
GC算法和垃圾收集器
在JVM 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的.
堆中垃圾回收过程(GC主要区域)
- 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记 ;
- 判断对象是否有必要执行finalize()方法,(没有覆盖,被调用过,都没有必要执行),放入F-Queue队列;
- 放入F-Queue中,进行第二次标记;
- 被拯救的移除队列,被两次标记的被回收;
垃圾收集算法
- 标记-清除算法:
- 复制算法:将活着的对象复制到另一块,然后把前者清空
- 标记-整理算法:让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
- 分代收集算法:把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法;
- HotSpot算法:在HotSpot的实现中,使用一组称为OopMap的数据结构来存放对象引用 ;
垃圾收集器
新生代收集器:Serial收集器、ParNew收集器、Parallel Scavenge收集器--------复制算法
老年代收集器: Serial Old收集器(标记整理)、Parallel Old 收集器(标记整理)、CMS收集器(标记清除)
G1收集器(重要):收集范围包括新生代和老年代;结合多种垃圾收集算法,空间整合,不产生碎片;高吞吐量;
Java多线程
Java线程的创建方式
线程的创建和启动:Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现。
Thread类的特性
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体;
- 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()。
方式一:实现Runnable接口
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
比较创建线程的两种方式:
开发中:优先选择实现Runnable接口的方式
原因:
- 实现的方式没有类的单继承性的局限性;
- 实现的方式更适合来处理多个线程有共享数据的情况;
联系:public class Thread implements Runnable。
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
方式二·:继承于Thread类
- 创建一个继承于Thread类的子类,也实现了Runnable接口
- 重写Thread类的run() --> 将此线程执行的操作声明在run()中
- 创建Thread类的子类的对象
- 通过此对象调用start()—start()方法作用:①启动当前线程 ② 调用当前线程的run()
方式三:实现Callable接口
- 与使用Runnable相比, Callable功能更强大些;
- 相比run()方法,可以有返回值;
- 方法可以抛出异常;
- 支持泛型的返回值;
- 需要借助FutureTask类,比如获取返回结果;
方式四:使用线程池
思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
wait和sleep的区别
| wait | sleep |
---|
同步 | 只能在同步上下文中调用wait方法,否则或抛出IllegalMonitorStateException异常 | 不需要在同步方法或同步块中调用 | 作用对象 | wait方法定义在Object类中,作用于对象本身 | sleep方法定义在java.lang.Thread中,作用于当前线程 | 释放锁资源 | 是 | 否 | 唤醒条件 | 其他线程调用对象的notify()或者notifyAll()方法 | 超时或者调用interrupt()方法体 | 方法属性 | wait是实例方法 | sleep是静态方法 |
线程的互斥与同步
当多个线程需要访问同一资源时,要求在一个时间段内只能允许一个线程来操作共享资源,操作完毕后别的线程才能读取该资源,这叫线程的互斥。我们需要使用synchronized来给共享区域加锁,确保共享资源安全。
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
方式二:同步方法。
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
使用同步方法解决实现Runnable接口的线程安全问题,使用wait和notify方法,synchronized
notify()或者notifyAll()方法:方法唤醒指定的线程
方式三:使用条件锁ReentrantLock
- 从JDK 5.0开始, Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 在实现线程安全的控制中,比较常用的是ReentrantLock, 可以显式加锁、释放锁。
面试题:synchronized 与 Lock的异同
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
**优先使用顺序:**Lock —> >同步代码块(已经进入了方法体,分配了相应资源)---->> 同步方法(在方法体之外)
可重入锁
可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
synchronized 和 ReentrantLock 都是可重入锁。
可重入锁的意义之一在于防止死锁。
实现原理实现是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。
如果同一个线程再次请求这个锁,计数器将递增;
每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。
关于父类和子类的锁的重入:子类覆写了父类的synchonized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将产生死锁(很好理解吧)。
AQS
AQS原理
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。 AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
**注意:AQS是自旋锁:**在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功;
实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物
对volatile关键字的理解
被volatile修饰的共享变量,就具有了以下两点特性:
1 . 保证了不同线程对该变量操作的内存可见性;
2 . 禁止指令重排序;
**1 . 原子性(Atomicity):**Java中,对基本数据类型的读取和赋值操作是原子性操作,所谓原子性操作就是指这些操作是不可中断的,要做一定做完,要么就没有执行。尽管volatile关键字可以保证内存可见性和有序性,但不能保证原子性。
**2 . 可见性(Visibility):**Java就是利用volatile来提供可见性的。 当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。而普通变量则不能保证这一点。
其实通过synchronized和Lock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是synchronized和Lock的开销都更大。
3.有序性(Ordering)
JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变。
事务的隔离级别(重点)
分类:
- 读未提交(Read uncommitted):脏读/不可重复读/幻读都可能
- 读已提交(Read committed简称RC):脏读不可能,不可重复读/幻读都可能
- 可重复读(Repeatable read简称RR):不可重复读/脏读不可能,幻读可能
- 串行化(Serializable):脏读/不可重复读/幻读都不可能,MySQL中事务隔离级别为串行化(serializable)时会锁表,因此不会出现幻读的情况,这种隔离级别并发性极低,开发中很少会用到。
脏读(Dirty Reads):事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求;
不可重读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做不可重复读;事务A读取到了事务B已经提交的修改数据,不符合隔离性。
幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为"幻读"。
InnoDB存储引擎默认的事务隔离级别是Repeatable read(可重复读),同时通过MVCC + Next-Key Lock可以解决幻读问题。
MySQL引擎,InnoDB对比MyISAM
MyISAM特点
1)不支持行锁(MyISAM只有表锁),读取时对需要读到的所有表加锁,写入时则对表加排他锁;
2)不支持事务
3)不支持外键
4)不支持崩溃后的安全恢复
5)在表有读取查询的同时,支持往表中插入新纪录
6)支持BLOB和TEXT的前500个字符索引,支持全文索引
7)支持延迟更新索引,极大地提升了写入性能
8)对于不会进行修改的表,支持 压缩表 ,极大地减少了磁盘空间的占用
InnoDB特点:
1)支持行锁,采用MVCC来支持高并发,有可能死锁
2)支持事务
3)支持外键
4)支持崩溃后的安全恢复
5)不支持全文索引
主要区别:
1)是否支持事务;
2)是否支持行级锁;
3)崩溃后能否安全恢复;
4)count运算上的区别: 因为MyISAM缓存有表meta-data(行数等),因此在做COUNT( )时对于一个结构很好的查询是不需要消耗多少资源的。而对于InnoDB来说,则没有这种缓存。但如果有where条件,两者都需要执行count()查询。
Mysql的行锁和表锁( 锁是计算机协调多个进程或纯线程并发访问某一资源的机制) 表级锁: 每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低; 行级锁: 每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;
MySQL中索引的使用
索引用于快速找出在某个列中有一特定值的行,不使用索引,MySQL必须从第一条记录开始读完整个表,直到找出相关的行,表越大,查询数据所花费的时间就越多,如果表中查询的列有一个索引,MySQL能够快速到达一个位置去搜索数据文件,而不必查看所有数据,那么将会节省很大一部分时间。
优点:
- 所有的MySql列类型(字段类型)都可以被索引,也就是可以给任意字段设置索引;
- 大大加快数据的查询速度;
缺点:
- 创建索引和维护索引要耗费时间,并且随着数据量的增加所耗费的时间也会增加;
- 索引也需要占空间,我们知道数据表中的数据也会有最大上线设置的,如果我们有大量的索引,索引文件可能会比数据文件更快达到上线值;
- 当对表中的数据进行增加、删除、修改时,索引也需要动态的维护,降低了数据的维护速度;
使用原则:
-
并不是每个字段度设置索引就好,也不是索引越多越好,而是需要自己合理的使用。 -
对经常更新的表就避免对其进行过多的索引,对经常用于查询的字段应该创建索引, -
数据量小的表最好不要使用索引,因为由于数据较少,可能查询全部数据花费的时间比遍历索引的时间还要短,索引就可能不会产生优化效果。 -
在一同值少的列上(字段上)不要建立索引,比如在学生表的"性别"字段上只有男,女两个不同值。相反的,在一个字段上不同值较多可是建立索引。 注意:索引是在存储引擎中实现的,也就是说不同的存储引擎,会使用不同的索引
- MyISAM和InnoDB存储引擎:只支持BTREE索引, 也就是说默认使用BTREE,不能够更换。
- MEMORY/HEAP存储引擎:支持HASH和BTREE索引
索引分为四类:单列索引(普通索引,唯一索引,主键索引)、组合索引、全文索引、空间索引、
- 单列索引:一个索引只包含单个列,但一个表中可以有多个单列索引;
- 普通索引:MySQL中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值,纯粹为了查询数据更快一点;
- 唯一索引:索引列中的值必须是唯一的,但是允许为空值;
- 主键索引:是一种特殊的唯一索引,不允许有空值;
- 组合索引:在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时遵循最左前缀集合;
- 全文索引:只有在MyISAM引擎上才能使用,只能在CHAR,VARCHAR,TEXT类型字段上使用全文索引,介绍了要求,说说什么是全文索引,就是在一堆文字中,通过其中的某个关键字等,就能找到该字段所属的记录行;
- 空间索引:空间索引是对空间数据类型的字段建立的索引,MySQL中的空间数据类型有四种,GEOMETRY、POINT、LINESTRING、POLYGON。在创建空间索引时,使用SPATIAL关键字。要求,引擎为MyISAM,创建空间索引的列,必须将其声明为NOT NULL;
设计模式分类
- 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
- 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
- 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
工厂模式
简单工厂模式优点:
- 工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创建产品对象的职责,很方便的创建出相应的产品。工厂和产品的职责区分明确。
- 客户端无需知道所创建具体产品的类名,只需知道参数即可。
- 也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类
简单工厂模式最大的缺点:
- 是当有新产品要加入到系统中时,必须修改工厂类,需要在其中加入必要的业务逻辑,这违背了“开闭原则”。
- 此外,在简单工厂模式中,所有的产品都由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性.
应用场景:
对于产品种类相对较少的情况,考虑使用简单工厂模式。使用简单工厂模式的客户端只需要传入工厂类的参数,不需要关心如何创建对象的逻辑,可以很方便地创建所需产品。
如何实现增加新产品而不影响已有代码?工厂方法模式应运而生,本文将介绍第二种工厂模式——工厂方法模式。
工厂方法模式(FACTORY METHOD)是一种常用的类创建型设计模式,此模式的核心精神是封装类中变化的部分,提取其中个性化善变的部分为独立类,通过依赖注入以达到解耦、复用和方便后期维护拓展的目的。
抽象工厂模式(Abstract Factory Pattern)隶属于设计模式中的创建型模式,用于产品族的构建。抽象工厂是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂是指当有多个抽象角色时使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体情况下,创建多个产品族中的产品对象
代理模式
代理模式的主要优点:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性
主要缺点:
- 代理模式会造成系统设计中类的数量增加
- 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
- 增加了系统的复杂度;
应用场景:
- 远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。
- 虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
- 安全代理,控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
ArrayList 和 LinkedList 的区别是什么?
是否线程安全: ArrayList 和 LinkedList 都是不保证线程安全的
底层实现: ArrayList 的底层实现是数组, LinkedList 的底层是双向链表。
内存占用: ArrayList 会存在一定的空间浪费,因为每次扩容都是之前的1.5倍,而 LinkedList中的每个元素要存放直接后继和直接前驱以及数据,所以对于每个元素的存储都要比 ArrayList花费更多的空间。
应用场景: ArrayList 的底层数据结构是数组,所以在插入和删除元素时的时间复杂度都会收到位置的影响,平均时间复杂度为o(n),在读取元素的时候可以根据下标直接查找到元素,不受位置的影响,平均时间复杂度为o(1), 所以 ArrayList 更加适用于多读,少增删的场景。 LinkedList的底层数据结构是双向链表,所以插入和删除元素不受位置的影响,平均时间复杂度为o(1),如果是在指定位置插入则是o(n),因为在插入之前需要先找到该位置,读取元素的平均时间复杂度为o(n)。 所以 LinkedList 更加适用于多增删,少读写的场景。
ArrayList 和 Vector 的区别
相同点:
- 都实现了 List 接口;
- 底层数据结构都是数组;
不同点:
- 线程安全: Vector 使用了 Synchronized 来实现线程同步,所以是线程安全的,而ArrayList 是线程不安全的。
- 性能:由于 Vector 使用了 Synchronized 进行加锁,所以性能不如 ArrayList 。
- 扩容: ArrayList 和 Vector 都会根据需要动态的调整容量,但是 ArrayList 每次扩容为旧容量的1.5倍,而 Vector 每次扩容为旧容量的2倍。
Array和ArrayList的区别
ArrayList 内部是由一个array 实现的。ArrayList 是Java集合框架类的一员,可以称它为一个动态数组。array 是静态的,所以一个数据一旦创建就无法更改他的大小。如果需要一个数组可以重新定义他的大小,你应该使用 ArrayList, 这是array 和ArrayList的基本的不同。
不同点:
| Array | ArrayList |
---|
Implementation(实现) | 本地的程序设计组件或者数据结构 | 来自Java集合类的类,一个接口 (Application programming interface) | Performance(性能) | 内存的使用和CPU 耗时较大 | 内存的使用和CPU 耗时相对小 | Type Safety(类型安全) | 不支持泛型,类型不安全 | 支持泛型,类型安全 | Flexibility(灵活性) | 动态数组,灵活的 | 静态数组 | Primitives(基本类型) | 支持对象和基本数据类型 | 基本数据类型 | Generics(泛型) | 不支持泛型 | 支持泛型 | Iteration(迭代) | for、foreach | 支持Iteration | Supported Operations(支持的操作) | 不支持添加删除操作 | 支持添加删除操作 | Size() vs length(大小 vs 长度) | 长度不可变更 | 初始长度为10,向右移一位,扩容1.5倍 | Dimension(维度) | 支持多维数组 | 不支持多维数组 |
相同点:
- Data Structure(数据结构 ):两者都允许存放对象,并且所有的都是基于index的数据结构,提供O(1)的复杂度来获取一个元素,但是如果通过二分查找来查询某个元素依旧需要log(N)的复杂度。
- Order(顺序):array 和ArrayList在添加元素时都维持着元素的顺序。
- Search(查找 ):可以通过index来查找一个元素;
- Null values(空值 ) :array 和 ArrayList都允许存储null值。
- Duplicates(重复 ) :array和Arraylis都允许存储重复的值。
- Zero-based Index(从零开始索引):array和ArrayList都从零开始的索引,即第一个元素从零的开始索引。
Set接口
Set接口:存储无序的(根据数据的哈希值决定的)、不可重复的数据 -->高中讲的“集合”(没有额外定义新的方法,使用的都是Collection中声明过的方法;向Set(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals())
- HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
- LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历 对于频繁的遍历操作,LinkedHashSet效率高于HashSet.
- TreeSet:可以按照添加对象的指定属性,进行排序,要求是相同类的对象——自然排序(实现Comparable接口) 和 定制排序(Comparator)。
HashMap
HashMap在JDK1.7和JDK1.8 的区别
- new HashMap():底层没有创建一个长度为16的数组
- jdk 8底层的数组是:Node[],而非Entry[]
- 首次调用put()方法时,底层创建长度为16的数组
- jdk7底层结构只有:数组+链表。
- jdk8中底层结构:数组+链表+红黑树。(链表长度大于8转为红黑树)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uaaWRsxw-1632538378330)(Photo/image-20210911212612511.png)]
| JDK1.7 | JDK1.8 | JDK1.8的优势 |
---|
底层结构 | 数组+链表 | 数组+链表/红 黑树(链表大于 8) | 避免单条链表过长而影响查询效 率,提高查询效率 | hash值计 算方式 | 9次扰动 = 4次位运算 + 5 次异或运算 | 2次扰动 = 1次 位运算 + 1次异 或运算 | 可以均匀地把之前的冲突的节点 分散到新的桶(具体细节见下面 扩容部分) | 插入数据 方式 | 头插法(先讲原位置的数 据移到后1位,再插入数据 到该位置) | 尾插法(直接 插入到链表尾 部/红黑树) | 解决多线程造成死循环地问题 | 扩容后存 储位置的 计算方式 | 重新进行hash计算 | 原位置或原位 置+旧容量 | 省去了重新计算hash值的时间 |
底层代码:
- 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
- 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
- TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
- MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
- Iterator迭代器接口
HashMap的扩容操作
DEFAULT_INITIAL_CAPACITY 初始值为16,DEFAULT_LOAD_FACTOR 负载因子为0.75,threshold 阈值为负载因子*容量 = 12;- resize() 方法是在 hashmap 中的键值对大于阀值时或者初始化时,就调用 resize() 方法进行扩容。
- 每次扩容,容量都是之前的两倍;
- 扩容时有个判断 e.hash & oldCap 是否为零,也就是相当于hash值对数组长度的取余操作,若等于0,则位置不变,若等于1,位置变为原位置加旧容量。
HashMap默认加载因子为什么选择0.75
主要是考虑空间利用率和查询成本的一个折中。如果加载因子过高,空间利用率提高,但是会使得哈希冲突的概率增加;如果加载因子过低,会频繁扩容,哈希冲突概率降低,但是会使得空间利用率变低。具体为什么是0.75,不是0.74或0.76,这是一个基于数学分析(泊松分布)和行业规定一起得到的一个结论。
为什么要将链表中转红黑树的阈值设为8?为什么不一开始直接使用红黑树?
- 因为红黑树的节点所占的空间是普通链表节点的两倍,但查找的时间复杂度低,所以只有当节点特别多时,红黑树的优点才能体现出来。至于为什么是8,是通过数据分析统计出来的一个结果,链表长度到达8的概率是很低的,综合链表和红黑树的性能优缺点考虑将大于8的链表转化为红黑树。
- 链表转化为红黑树除了链表长度大于8,还要 HashMap 中的数组长度大于64。也就是如果HashMap长度小于64,链表长度大于8是不会转化为红黑树的,而是直接扩容。
为什么HashMap中String、 Integer这样的包装类适合作为Key?
- 这些包装类都是 final 修饰,是不可变性的, 保证了 key 的不可更改性,不会出现放入和获取时
哈希值不同的情况。 - 它们内部已经重写过 hashcode() , equal() 等方法。
如果使用Object作为HashMap的Key,应该怎么办呢? **
- 重写 hashCode() 方法,因为需要计算hash值确定存储位置
- 重写 equals() 方法,因为需要保证 key 的唯一性。
HashMap、 ConcurrentHashMap及Hashtable 的区别
| HashMap(JDK1.8) | ConcurrentHashMap(JDK1.8) | Hashtable |
---|
底层实现 | 数组+链表/红黑树 | 数组+链表/红黑树 | 数组+链表 | 线程安全 | 不安全 | 安全( Synchronized 修饰Node 节点) | 安全 ( Synchronized 修 饰整个表) | 效率 | 高 | 较高 | 低 | 扩容 | 初始16,每次扩容成 2n | 初始16,每次扩容成2n | 初始11 ,每次扩容 2n+1 | 是否支持 Null key和 Null Value | 可以有一个Null key, Null Value多个 | 不支持 | 不支持 |
哪些是线程安全的:
ConcurrentHashMap、Vector、StringBuffer、hashTable、stack、enumration
Spring简介
优点:
- 低侵入式设计,代码的污染极低。
- 独立于各种应用服务器,基于Spring框架的应用,可以真正实现Write Once,Run Anywhere的承诺。
- Spring的IoC容器降低了业务对象替换的复杂性,提高了组件之间的解耦。
- Spring的AOP支持允许将一些通用任务如安全、事务、日志等进行集中式管理,从而提供了更好的复用。
- Spring的ORM和DAO提供了与第三方持久层框架的良好整合,并简化了底层的数据库访问。
- Spring的高度开放性,并不强制应用完全依赖于Spring,开发者可自由选用Spring框架的部分或全部。
Spring的核心机制
管理Bean
程序主要是通过Spring容器来访问容器中的Bean,ApplicationContext是Spring容器最常用的接口,该接口有如下两个实现类:
- ClassPathXmlApplicationContext: 从类加载路径下搜索配置文件,并根据配置文件来创建Spring容器。
- FileSystemXmlApplicationContext: 从文件系统的相对路径或绝对路径下去搜索配置文件,并根据配置文件来创建Spring容器。
Spring容器中的Bean
对于开发者来说,开发者使用Spring框架主要是做两件事:①开发Bean;②配置Bean。对于Spring框架来说,它要做的就是根据配置文件来创建Bean实例,并调用Bean实例的方法完成"依赖注入"——这就是所谓IoC的本质。
依赖注入
控制反转(Inverse of Control,IoC)又叫依赖注入(Dependency Injection)。当某个Java对象(调用者)需要调用另一个Java对象(被依赖对象)的方法时,在传统模式下通常有两种做法:
- 原始做法: 调用者主动创建被依赖对象,然后再调用被依赖对象的方法。
- 简单工厂模式: 调用者先找到被依赖对象的工厂,然后主动通过工厂去获取被依赖对象,最后再调用被依赖对象的方法。
Spring框架的核心功能有两个:
- Spring容器作为超级大工厂,负责创建、管理所有的Java对象,这些Java对象被称为Bean。
- Spring容器管理容器中Bean之间的依赖关系,Spring使用一种被称为"依赖注入"的方式来管理Bean之间的依赖关系。
使用依赖注入,不仅可以为Bean注入普通的属性值,还可以注入其他Bean的引用。依赖注入是一种优秀的解耦方式,其可以让Bean以配置文件组织在一起,而不是以硬编码的方式耦合在一起。
创建Bean的3种方式
使用构造器创建Bean实例
使用构造器来创建Bean实例是最常见的情况,如果不采用构造注入,Spring底层会调用Bean类的无参数构造器来创建实例,因此要求该Bean类提供无参数的构造器。
采用默认的构造器创建Bean实例,Spring对Bean实例的所有属性执行默认初始化,即所有的基本类型的值初始化为0或false;所有的引用类型的值初始化为null。
使用静态工厂方法创建Bean
使用静态工厂方法创建Bean实例时,class属性也必须指定,但此时class属性并不是指定Bean实例的实现类,而是静态工厂类,Spring通过该属性知道由哪个工厂类来创建Bean实例。
除此之外,还需要使用factory-method属性来指定静态工厂方法,Spring将调用静态工厂方法返回一个Bean实例,一旦获得了指定Bean实例,Spring后面的处理步骤与采用普通方法创建Bean实例完全一样。如果静态工厂方法需要参数,则使用<constructor-arg.../> 元素指定静态工厂方法的参数。
调用实例工厂方法创建Bean
实例工厂方法与静态工厂方法只有一个不同:调用静态工厂方法只需使用工厂类即可,而调用实例工厂方法则需要工厂实例。使用实例工厂方法时,配置Bean实例的<bean.../> 元素无须class属性,配置实例工厂方法使用factory-bean 指定工厂实例。 采用实例工厂方法创建Bean的<bean.../> 元素时需要指定如下两个属性:
- factory-bean: 该属性的值为工厂Bean的id。
- factory-method: 该属性指定实例工厂的工厂方法。
若调用实例工厂方法时需要传入参数,则使用<constructor-arg.../> 元素确定参数值。
Spring的AOP
为什么需要AOP
AOP(Aspect Orient Programming)也就是面向切面编程,作为面向对象编程的一种补充,已经成为一种比较成熟的编程方式。其实AOP问世的时间并不太长,AOP和OOP互为补充,面向切面编程将程序运行过程分解成各个切面。
AOP专门用于处理系统中分布于各个模块(不同方法)中的交叉关注点的问题,在JavaEE应用中,常常通过AOP来处理一些具有横切性质的系统级服务,如事务管理、安全检查、缓存、对象池管理等,AOP已经成为一种非常常用的解决方案。
使用AspectJ实现AOP
AspectJ是一个基于Java语言的AOP框架,提供了强大的AOP功能,其他很多AOP框架都借鉴或采纳其中的一些思想。其主要包括两个部分:一个部分定义了如何表达、定义AOP编程中的语法规范,通过这套语法规范,可以方便地用AOP来解决Java语言中存在的交叉关注点的问题;另一个部分是工具部分,包括编译、调试工具等。
AOP实现可分为两类:
- 静态AOP实现: AOP框架在编译阶段对程序进行修改,即实现对目标类的增强,生成静态的AOP代理类,以AspectJ为代表。
- 动态AOP实现: AOP框架在运行阶段动态生成AOP代理,以实现对目标对象的增强,以Spring AOP为代表。
一般来说,静态AOP实现具有较好的性能,但需要使用特殊的编译器。动态AOP实现是纯Java实现,因此无须特殊的编译器,但是通常性能略差。
AOP的基本概念
关于面向切面编程的一些术语:
- 切面(Aspect): 切面用于组织多个Advice,Advice放在切面中定义。
- 连接点(Joinpoint): 程序执行过程中明确的点,如方法的调用,或者异常的抛出。在Spring AOP中,连接点总是方法的调用。
- 增强处理(Advice): AOP框架在特定的切入点执行的增强处理。处理有"around"、"before"和"after"等类型
- 切入点(Pointcut): 可以插入增强处理的连接点。简而言之,当某个连接点满足指定要求时,该连接点将被添加增强处理,该连接点也就变成了切入点。
Spring的AOP支持
Spring中的AOP代理由Spring的IoC容器负责生成、管理,其依赖关系也由IoC容器负责管理。 为了在应用中使用@AspectJ 支持,Spring需要添加三个库:
aspectjweaver.jar aspectjrt.jar aopalliance.jar
|