共计 4969 个字符,预计需要花费 13 分钟才能阅读完成。
- 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 属性, 网站的由客户决定并保存到数据库的内容进行特殊字符处理;
分布式流量攻击
即服务端能够承受的并发量不高, 黑客使用多台或者高性能服务器, 发送大量请求给服务端, 直接让服务瘫痪;
当服务端承受并发量很高时, 也会存在一种情况, 黑客会空气其他人的电脑, 对服务端发起大量攻击;
正文完