- hashMap
hashMap的底层是数组+链表的结构, 使用键值对存储数据, 初始化的容量是
16个, 当数组已用容量超过实际容量超过3/4时, 会进行扩容, 每次扩容要
是2的倍数, 当数组上的链表深度大于8时, 链表会转化为红黑树(提高查询
效率);
具体实现:
hash算法和寻址算法:
put(key, value)时, 对key进行hash((h =
key.hashCode())^(h >>> 16)), key取hash值并右移16位, 和原hash值
取异或(不相同取1, 相同取0), 寻址的公式(n -1)&hash(这里指重新计算
的hash), 之所以使用&运算(同为1时为1, 否则为0), 而不是取模, 因为&
效率更高, 上面之所以使用高16位和低16位异或, 可以有效减少hash碰撞
(有的hash值可能高16位不同, 低16位相同, 这样如果和n-1进行&运算, 相
当于高16位没有参与到运算中);
hash碰撞解决:
主要是通过链表和红黑树的相互转化;
扩容机制:
扩容就会涉及到rehash, 数据量大的时候比较消耗性能
- concurrentHashMap源码解析
hashmap存在线程安全问题, 所以会出现concurrentHashMap来解决;
两种情况,
1. 两个线程同时向map中put元素, 由于链表指向的转化, 导致数据丢失;
2. 一个线程put操作, 一个线程get操作, 可能会get到null值;
hashtable解决方式效率太低: 使用sychronized锁住整个数组;
concurrentHashMap解决的原理: 使用CAS, compare and swap;
- 数组初始化保证线程安全:
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//此处使用cas保证初识化时线程安全
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
put方法的使用
初始化数组完成, 但是没有值时
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//此处使用cas
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
... 省略代码
}
addCount(1L, binCount);
return null;
}
初始化数组完成, 并且有值时, 使用加锁, 这个锁其实只是加在一个链路上;
synchronized (f) {
省略代码...
}
扩容时
第一个线程来扩容的时候, 会将一个头部节点修改为-1, 表示正在扩容, 每次从尾部开始分配给一个线程16个数组节点, 如果后续的线程发现map正在扩容, 则会帮助之前的线程进行rehash, 他会分配上一个线程前面的16个节点进行操作;
线程池
线程池的工作原理
- 线程过来时, 会先判断core线程是否已经满了, 没有的话, 就创建一个core线程执行任务;
- 如果core线程已经满了, 那么他会将任务放入阻塞队列中;
- 当阻塞队列也已经满了以后, 他会判断, 线程池是否还定义了非核心线程数, 如果有非核心线程数, 那么就创建线程, 执行任务;
- 当非核心线程数也已经满了以后, 就会执行我们定义的任务拒绝策略;
线程池的拒绝策略
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
线程池使用无界阻塞队列会发生什么?
一个场景, 当进行远程调用发生阻塞的时候, 导致阻塞队列中的任务
越积越多, 最后出现OOM的问题;
使用有界队列, 当队列和线程都满了以后如何执行后续的任务?
可以自定义拒绝策略, 将任务持久化到磁盘, 当队列中的任务执行完之后, 到磁盘中获取, 在执行;
机器宕机之后, 队列中的任务怎么办?
此时可以采用任务提交之前将他保存到数据库, 并定义状态(未提交),
当提交完成并操作完成之后, 修改状态(已提交);
在程序启动的时候, 定义一个后台进程, 扫描数据库, 将未提交的任
务进行提交;
java内存模型
有序性
程序的操作是按顺序进行的, 这里主要涉及到jvm和cpu的指令重排问题;
原子性
一个线程更新操作共享数据的时候, 不予许其他线程进行更新操作;
可见性
一个线程对一个共享数据的操作, 其他的线程能够看到, 内存层面就是, 一个线程更新完一个数据之后, 会将数据刷新进主内存, 另一个线程读数据的时候, 强制他从主内存中读取;
volatile关键字的原理
主要的原理就是上面的可见性问题, volatile主要是将线程对数据的更新刷回主内存中, 并将其他线程中使用这个数据的线程内存失效, 那么当下次其他线程读取这个数据的时候, 从主内存读取;
TCP/IP网络模型
物理层
简单来说就是无线信号, 网线和海底光缆之类的, 让电脑之间可以在物理上进行连接;
数据链路层
这一层是架构在物理层之上, 就是一台电脑发送给另一台电脑的数据,
但是存在问题, 一台电脑发送的数据是一连串的信号, 另一台电脑并
不知道哪些是发给自己的, 以及发给自己的这些数据要如何解析, 因
此诞生了一些协议, 这里面比如以太网, 互联网:
1. 以太网: 一组电信号就是一个数据包, 就是一帧, 每帧分为两个
部分, head和data, head中是些描述性信息, 数据从哪来到哪去,
以及数据的信息, data就是真实的数据, 这些数据包需要从一台电脑
的网卡发出去, 由另一台电脑的网卡接收, 以太网规定, 每个网卡都
要有一个唯一的标识, 就是mac地址, 而且网卡的数据包发送是以广播
的方式发送给局域网内的所有电脑的, 接收方就是根据head里面的消
息判断这条消息是否是发给自己的;
网络层
数据层解决了数据包发送和解析的问题, 但是, 不能区分网络的分区,
就是那些电脑可以互相联通进行通信, 网络层里面有IP协议, 定义了
电脑的ip地址, 每台电脑的唯一标识, 而判断电脑是不是一个子网的,
还需要一个子网掩码, 将ip地址和子网掩码进行二进制运算, 然后通
过这个二进制数来判断哪些电脑是属于一个子网的;
当两个电脑不在一个局域网中时, 如何通信连接, 这时候会用到路由
器, 路由器上可以注册电脑的IP地址和网卡地址;
传输层
上面解决了电脑和电脑之间的连接, 数据的传输和识别问题, 但是每个电脑上有不同的应用程序, 而一台电脑又只有一个网卡, 所以又引入端口号的概念, 每个应用程序监听不同的端口号, 进行消息接收, 这一层建立了TCP协议(如何建立连接, 如何发送和读取消息, 就是tcp协议规定的);
应用层
拿到数据到底该怎么办, 怎么解析, 就是这个层干的事情, 具体的包括http协议, ftp协议...
浏览器请求一个网络地址时的大概过程
1. 先走DNS解析器, 将地址解析成ip地址;
2. 走应用层的http协议, 这时候会根据http的协议对数据进行打包;
3. 包装传输层, 根据tcp协议, 打包tcp协议的数据包, 包括那台机器
的端口信息;
4. 包装网络层: 根据ip协议包装数据包, 进行网络传输;
5. 包装数据链路层, 根据一台网协议封装数据包, 定义数据包接受者
ip和发送者ip等;
6. 通过路由, 交换机等对数据进行发送;
7. 接受者就根据各层的协议对数据进行层层解包;
TCP三次握手/四次挥手
三次握手
第一次: 客户端给服务端发送请求,SYN=1, ACK=0, seq=x;
第二次: 服务端给客户端响应, ack=x+1, SYN=1, ACK=1, seq=y;
第三次: 客户端给服务端响应, ack=y+1, ACK=1, seq=x+1;
为什么是三次: 主要是为了相互确认, 第一次的请求时客户端要求服
务端建立连接, 第二次的请求是服务端给了客户端响应, 客户端确认
了自己可以接收到服务端响应, 但此时服务端并不能确定客户端是否
要继续连接或者能否接收到客户端的响应, 所以需要第三次来做确认;
四次挥手
第一次挥手:客户端向服务器发送一个FIN报文段,将设置seq为100
和ack为120,;此时,客户端进入 FIN\_WAIT\_1状态,这表示客户端
没有数据要发送服务器了,请求关闭连接;
第二次挥手:服务器收到了客户端发送的FIN报文段,向客户端回一个
ACK报文段,ack设置为101,seq设置为120;服务器进入了
CLOSE\_WAIT状态,客户端收到服务器返回的ACK报文后,进入
FIN\_WAIT\_2状态;
第三次挥手:服务器会观察自己是否还有数据没有发送给客户端,如
果有,先把数据发送给客户端,再发送FIN报文;如果没有,那么服务
器直接发送FIN报文给客户端。请求关闭连接,同时服务器进入
LAST\_ACK状态;
第四次挥手:客户端收到服务器发送的FIN报文段,向服务器发送ACK
报文段,将seq设置为101,将ack设置为121,然后客户端进入
TIME\_WAIT状态;服务器收到客户端的ACK报文段以后,就关闭连接;
此时,客户端等待2MSL后依然没有收到回复,则证明Server端已正常
关闭,客户端也可以关闭连接了;
http工作原理
http请求主要分为请求头和请求体, 请求头中有状态码, 请求方式, 请求地址等信息, 请求体重主要是数据, 主要的过程是上面浏览器请求过程;
Mybatis如何防止sql注入
主要是通过#{}和${}的区别来做到的, #{}会将括号中的内容加上双引号后加在sql语句中, 而${}则是将内容直接加在sql语句中, 前者会对sql语句进行预编译, 后者不会, 因此使用#{}不会存在sql注入问题, ${}则会出现sql注入问题, 如果非要使用, 需要进行手动的参数校验;
CSRF攻击和XSS攻击
CSRF攻击
跨站点请求伪造攻击, 主要是获取客户端的jsessionid, 去模拟客户端请求服务端;
解决方法: 返回cookie的时候, 将设置属性为HttpOnly;
XSS攻击
和上面比较类似, 只是xss攻击使用的是将html+javascript脚本加载到客户端电脑上执行, 通常是使用恶意链接或者在网站评论内容中书写脚本;
解决方法: 返回的cookie, 设置HttpOnly属性, 网站的由客户决定并保存到数据库的内容进行特殊字符处理;
分布式流量攻击
即服务端能够承受的并发量不高, 黑客使用多台或者高性能服务器, 发送大量请求给服务端, 直接让服务瘫痪;
当服务端承受并发量很高时, 也会存在一种情况, 黑客会空气其他人的电脑, 对服务端发起大量攻击;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。