1、HashMap
- 数组的存储方式, 需要指定下标,那么下标的来源就是将key进行hashcode,数值过大,然后进行取模,如"周瑜"的hashcode为699636,699636%8,8为数组的长度, 就是要获取的下标,而真正使用的是&与操作完成,即hash&(table.length-1)。其中的hash算法进行了大量的异或和右移操作,是由于采用的是table.length-1的与操作,那么基本上算出的都是低位与的结果,而高位没有影响。
- 但是hashcode是随机的,所以如果出现了hashcode值相同,就出现了哈希冲突。解决该问题的方式,一种是再次进行hashcode,即再散列法;另一种方式就是使用链表,而一个新的节点到来时,直接将新节点的next指针指向head节点,然后head指针就重新指向新节点。
- 构造器中的容量参数,会进行2的幂次方比较,如传入的为15,那么容量就是16;这么做的目的是在获取hash值时使用的是与操作,性能要比%好一些。
- 在插入新节点的时候,会进行是否数组扩容的操作,当前存储的元素的个数大于阈值(容量*加载因子),同时当前要插入的节点找到的索引位置不为null的时候,才会进行扩容。扩容的时候,会创建新的2倍数组。循环数组每一个元素,然后针对元素所在的链表进行循环,找到新的hash位置,放置到新的数组上去。
- 重写equals方法的时候一定要重写hashcode方法,因为hashmap在获取元素的时候,判断hash的同时也判断了equals方法。
- 1.8版本的加入了红黑树,是为了解决链表过长,查询效率低的问题。
2、Redis
- 单线程原因:使用单线程 -- 多路复用IO模型来实现高性能的内存服务。
- 缓存穿透,查询一个一定不存在的数据,由于缓存是不命中时需要从数据库中查询,查不到数据则不写入缓存,就导致这个不存在的数据每次都要到数据库中去查询。解决办法,是对空值进行缓存,只是将缓存时间设置的比较短。
- 缓存击穿,在缓存正好失效的情况下,高并发的请求过来,都去查询了数据库,需要使用锁机制来解决。在查询时,缓存中有数据直接返回,没有数据,先上锁,此处加入再次查询缓存的代码,然后去数据库查询数据,放入到缓存中,然后解锁。
1、查询缓存 redis.get
2、缓存有数据,直接返回 cache != null
3、缓存没有数据 cache == null
4、上锁 redis.lock
5、查询缓存 redis.get
6、第一个线程:缓存没有数据
7、第二个线程在第一个线程解锁之后,缓存有数据,直接返回
7、第一个线程去查询数据库,放入缓存,解锁
- 缓存和数据库一致性,第一种情况,先写入数据库,然后删除缓存,如果删除缓存失败,造成数据库数据是新的,而缓存数据是旧的。第二中情况,先删除缓存,然后写入数据库,如果写入数据库失败,顶多用户读取的是旧的数据,数据还是一致的。多线程情况下出现问题:
- 持久化方式,有RDB和AOF两种方式,RDB:当达到一定条件的时候,将内存中的整个数据全部写到磁盘存储,整个过程redis服务器内部需要将缓存的数据进行格式化处理,压缩最后缓存,这是比较耗时的,同时也会占用服务器内部资源,最重要的是快照不是实时操作,中间有时间间隔,这就意味着如果服务器宕机,需要恢复数据是不完整的。为了解决这个问题,可以将用户的操作指令记录并保存,如果需要进行数据恢复,则会通过操作指令一步步进行数据还原,就是AOF。
- 分布式锁,并发编程中,我们一般使用锁来避免由于竞争而造成的数据不一致问题,但是只能保证在同一个JVM进程中执行。
3、Java 内存模型
- 和cpu缓存模型类似,是基于CPU缓存模型建立的。每个线程操作的都是自己的工作内存,也就是操作的是共享变量副本。其他线程是感知不到当前线程共享变量副本的变化的。操作的过程:从主内存中read出来,然后load到工作内存,然后从工作内存中读取变量来进行计算,修改之后,assign赋值写到工作内存中,此时该共享变量在主内存中没有发生变化,加入volatile关键字之后,将工作内存的修改后的值store到主内存,然后write到主内存的共享变量中,将主内存中该共享变量lock加锁,标示为线程独占状态,采用的是总线加锁的方式,但是性能太低,后面采用的是MESI缓存一致性协议来解决。
- MESI缓存一致性协议:多个CPU从主内存读取同一个数据到各自的高速缓存,当某一个cpu修改了缓存的数据,该数据会立马同步到主内存,其他CPU通过总线嗅探机制可以感知到数据的变化,从而将自己缓存里的数据失效。
- volatile是轻量级的同步机制,保证可见性,不保证原子性(num++,可以使用Atomic开头的类),禁止指令重排。
4、AQS原理
- park+自旋,没有竞争到锁时挂起,其实就是将该线程放入到一个等待队列中,一旦锁释放的时候,就去该队列中取出等待的线程,此时那个等待的线程在while循环中,可以去竞争锁,如果拿到了就执行其他逻辑,拿不到继续挂起;
- 使用AQS需要子类去重写tryAcquire和tryRelease方法。
- AQS:同步器,获取锁调用的是acquire方法,该方法中调用的tryAcquire方法需要子类去重写,如果tryAcquire成功了,说明抢到锁了,失败了,通过for循环进行CAS操作,将新结点加入到tail结点(此处有初始化头结点操作);然后获取到前驱结点,如果为head,那么就去执行tryAcquire方法,失败的话去判断前一个结点的状态(此处有初始化头结点状态的操作),为SINGAL就挂起(使用的是LockSupport类),并返回中断状态,如果为CANCEL就一直往前找到不是该状态的前驱节点。
- 状态值:CANCELD 1、初始状态 0、SIGNAL -1、CONDITION -2、PROPAGATE -3,也就是说独占模式下只有一个结点会处于SINGAL状态。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- ReentrantLock:非公平锁的情况下,会先去执行CAS抢锁,失败的话去判断state状态,state=0,尝试CAS抢锁,state!=0,判断持有线程是不是自己,是自己再次加锁;公平锁的情况下,直接去判断state状态,state=0,先去判断队列有没有其他等待的线程,有的话,去排队,没有的话,尝试CAS抢锁,state!=0,判断持有线程是不是自己,是自己再次加锁。
1、非公平锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
2、公平锁
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
5、Redis命令
- 常用的存储方式:string、hash、set、list、zset
- string
1、单值缓存
SET key value
GET key
2、对象缓存
SET user:1 value(json格式数据)
//优点:可以获取单个属性值,比较灵活,修改起来也很容易。
MSET user:1:name admin user:1:balance 100
MGET user:1:name user:1:balance
3、分布式锁
服务器1线程:SETNX product:10001 true //返回1代表获取锁成功
服务器2线程:SETNX product:10001 true //返回0代表获取锁失败
DEL product:10001 //执行完业务逻辑释放锁
SET product:10001 true ex 10 nx //放置程序意外终止导致死锁
4、计数器,可以解决并发问题
INCR article:1000:readcount
GET article:1000:readcount
5、session共享,同一个war包部署在不同的服务器上
spring session + redis实现session共享
- hash
1、对象缓存,key field value
HMSET user 1:name admin 1:balance 100
HMGET user 1:name 1:balance
2、购物车
添加商品:hset cart:1001 10080 1
增加数量:hincrby cart:1001 10080 1
商品总数:hlen cart:1001
删除商品:hdel cart:1001 10080
获取购物车所有商品:hgetall cart:1001
- list
1、命令
LPUSH key value
RPUSH key value
LPOP key
RPOP key
LRANGE key start end
BLPOP key timeout
BRPOP key timeout
2、数据结构
栈:LPUSH + LPOP
队列:LPUSH + RPOP
阻塞队列:LPUSH + BRPOP
3、微博和公众号消息流
A发微博,消息ID为10018
LPUSH msg:1001 10018
B发微博,消息ID为20018
LPUSH msg:1001 20018
查看最新微博消息
LRANGE msg:1001 0 5
- set
1、微信抽奖小程序
点击参与抽奖用户加入集合
SADD activity:1001 111
查看参与抽奖的所有用户
SMEMBERS activity:1001
开始抽奖
SRANDMEMBER activity:1001 2 //选出来的数据不删除
SPOP activity:1001 2 //选出来的数据删除
6、消息中间件
- 重试机制:如果消费者程序业务逻辑部分出现了异常时,会自动实现补偿机制,也就是重试机制,默认一直重试到不出现异常为止。自动签收的功能其实就是rabbitmq在底层使用aop进行拦截,没有异常自动提交事务,有异常的话实现补偿机制。重试机制不会出现并发情况,都是在前一次重试的结果上进行时间间隔的。
- 重试场景:消费者获取到消息后,调用第三方接口的时候,但是该接口暂时无法访问,需要重试机制,可以通过http请求的返回码是不是200来判断,不是直接抛出异常,将由rabbitmq开始重试机制;但是如果抛出异常,不需要进行重试,应该采用日志记录+人工进行补偿。
- 重复消费:使用rabbitmq的全局性ID方式,为每一个消息加一个唯一性ID,然后在消费方根据返回的消息是否有ID来判断是否是重复消费。
- 好处:解耦系统之间调用;将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度;并发量大的时候,可以通过消息队列进行削峰。
- 应用场景:日志记录,将不同级别的日志通过topic机制发送到exchange中。
- 项目中是怎么用消息队列的:
- 1、为什么使用消息队列?使用消息队列有哪些优点和缺点?kafka、activemq、rabbitmq、rocketmq都有什么优点和缺点:
- 结合项目来说明,
- 如何保证消息队列的高可用
- 如何保证消息不被重复消费和幂等性
- 如何保证消息的可靠性传输,丢失了消息怎么办
- 如何保证消息的顺序性
- 如何解决消息队列的延时和过期失效问题,消息队列满了之后怎么办
- 如何让你写一个消息队列,该如何进行架构设计
7、RPC幂等性
- 如果客户 端调用服务端接口超时的话,会采用重试机制,可能会造成服务端出现重复消费。
- 人为的form表单提交也会出现重复消费。
- 解决办法:调用接口前,传递一个全局性的ID,服务器消费前先根据ID判断是否有处理过该请求。将该ID存储在redis中,处理完逻辑之后,将该ID从redis中删除。
public class TokenUtils{
public Boolean getToken(String token){
String redisToken = redisUtils.getString(token);
if(StringUtils.isEmpty(redisToken)){
return Boolean.FALSE;
}
//redis是单线程的
boolean delKey = redisUtils.delKey(redisToken);
if(!delKey){
System.out.println("已经被其他请求删除");
}
return delKey;
}
}
- 但是业务逻辑处理失败的时候,会造成后续的再次提交也被拦截。可以使用aop方式进行异常捕捉,如果业务逻辑出现异常,可以将token重新放置到redis中。
7、推送
- 短轮询:不断地间隔去请求服务器,缺陷是占用了服务器的资源,数据响应不及时。好处是简单,服务端不需要改造。
- 长轮询:基于Http长连接,无须在浏览器安装插件的服务器推送技术,如Servlet3中的异步任务和Spring的DeferedResult。相比短轮询,只是改造了实时性问题。还有一种:Server-Sent-Event(SSE)。
- websocket协议:Html5中的协议,实现客户端与服务端的双向,基于消息的文本或二进制数据通信。适用于对数据实时性要求较高的场景,后端需要单独实现,并不是所有的浏览器都支持。
- websocket建立的时候,是发送的http协议。
8、BIO、NIO
- 阻塞IO:读写过程中会发生阻塞现象,用户线程在发出IO请求之后,会去查看数据是否准备就绪,没有的话就会阻塞,然后让出CPU。典型的是socket的read方法。
- 非阻塞IO:当用户线程发出一个IO请求之后,马上会得到一个结果(可能是准备好,也可能是没有准备好),需要用户线程不断的询问内核数据是否准备就绪,不会让出CPU,缺陷是CPU占用率非常高。
- 多路复用IO:会有一个线程不断地去轮询多个socket的状态,只有当socket真正的有读写事件时,才真正的调用实际的IO操作,如socket的read操作。在Java NIO中是使用selector.select()方法去查询每个通道是否有到达的事件。而轮询每个socket的状态是在内核进行的,效率较高,这样在单线程的情况下可以同时处理多个客户端请求。
- 异步IO:当用户线程发起IO请求之后,立刻可以去做其他的事情,然后当数据准备好时,内核会给用户线程一个信号,告诉它IO操作完成了。
- 服务端建立ServerSocket,监听某一个端口,然后阻塞接受客户端的连接,客户端建立Socket,连接到服务端的端口,发送数据。阻塞的情况会出现在accept和read两处,所以不支持并发操作。
- 为了解决上述问题,为每一个socket开启一个独立的线程,也就是需要借助多线程来支持高并发,缺陷是浪费服务器资源。
- NIO的设计初衷是使用单线程来处理并发,类似redis的单线程处理并发。方式就是将accept和read两处的阻塞都变成非阻塞。
package com.vim;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class App {
public static List<SocketChannel> socketChannelList = new ArrayList<>();
private static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
public static void main( String[] args ) throws Exception {
try{
//解决了accept阻塞的问题
ServerSocketChannel serverSocket = ServerSocketChannel.open();
SocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(socketAddress);
serverSocket.configureBlocking(false);
while (true){
//轮询判断是否有数据
for(SocketChannel channel:socketChannelList){
int read = channel.read(byteBuffer);
if(read > 0){
}else if(read == -1){
socketChannelList.remove(channel);
}
}
SocketChannel accept = serverSocket.accept();
if(accept != null){
//解决了read阻塞的问题
accept.configureBlocking(false);
socketChannelList.add(accept);
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
- 以上新加入的api解决了阻塞的问题,但是瓶颈主要在两个问题:for循环可以交给内核去执行,所以出现了selector选择器。
- tomcat使用的是线程池的方式,每来一个请求都会分配一个单独的线程去处理。所以BIO也可以使用,只是不适合适用长连接的场景,如果大部分都是短连接的话,可以使用BIO+线程池的方式去处理。
- NIO:channel+buffer+selector
package com.vim;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class App {
public static List<SocketChannel> socketChannelList = new ArrayList<>();
private static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
public static void main( String[] args ) throws Exception {
try{
//解决了accept阻塞的问题
ServerSocketChannel serverSocket = ServerSocketChannel.open();
SocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(socketAddress);
serverSocket.configureBlocking(false);
//获取选择器
Selector selector = Selector.open();
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
while (true){
selector.select(1000);
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey result = iterator.next();
iterator.remove();
if(result.isAcceptable()){
SocketChannel socketChannel = serverSocket.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}else if(result.isReadable()){
SocketChannel socketChannel = (SocketChannel) result.channel();
socketChannel.configureBlocking(false);
//取消监听,此处交给线程池去处理
//...线程池代码,在其中代码的finally中要重新将该socketChannel注册到selector上的OP_READ
result.cancel();
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
- 上述代码中while中循环,相当于netty中的bossGroup,线程池,相当于netty中的workGroup。即Reactor中的多线程模型,一个线程接收连接,一个线程处理IO读写事件。
- Netty是一个高性能、异步事件驱动的NIO框架。提供了对TCP、UDP、文件传输的支持。其所有的IO操作都是异步非阻塞的。
- TCP粘包:只有TCP会产生粘包的现象,而UDP不会产生,是因为TCP是基于流的协议,而UDP是基于数据包的协议。
9、数据结构 -- 树
- 每个结点有0个或多个子结点;没有父结点的结点为根结点;每个非根结点有且只有一个父结点
- 结点的度:结点拥有的子树的个数,二叉树的度不大于3
- 叶子结点:度为0的结点
- 兄弟结点:拥有共同父结点的结点
- 树的深度:树中结点的最大层次
- 二叉树:每个结点最多有两个子树的树结构。
package com.vim;
public class DoubleTree {
//根结点
private Node root;
//添加结点
public void add(int value){
Node newNode = new Node(value);
if(root == null){
root = newNode;
}else{
Node temp = root;
while (true){
if(value < temp.getValue()){
//当前结点有没有左孩子
if(temp.getLeft() == null){
temp.setLeft(newNode);
break;
}else{
//向左边移动
temp = temp.getLeft();
}
}else{
//当前结点有没有右孩子
if(temp.getRight() == null){
temp.setRight(newNode);
break;
}else{
//向右边移动
temp = temp.getRight();
}
}
}
}
}
public void showNode(Node node){
//前序遍历
// System.out.println(node.getValue());
if(null != node.getLeft()){
showNode(node.getLeft());
}
//中序遍历
System.out.println(node.getValue());
if(null != node.getRight()){
showNode(node.getRight());
}
//后序遍历
// System.out.println(node.getValue());
}
public static void main(String[] args) {
DoubleTree tree = new DoubleTree();
tree.add(4);
tree.add(1);
tree.add(9);
tree.add(6);
tree.add(0);
tree.add(3);
tree.add(8);
tree.showNode(tree.root);
}
}
10、排序
- 冒泡排序
package com.vim;
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {3, 7, 4, 2, 6, 1};
//排序一趟,会把最大值放到数组的最后面
for(int j=arr.length; j>1; j--){
//比较相邻的两个数字,只要左边的比右边的大,就进行交换
for(int i=0; i<j-1; i++){
if(arr[i] > arr[i+1]){
int temp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = temp;
}
}
}
for(int i=0; i<arr.length; i++){
System.out.println(arr[i]);
}
}
}
- 选择排序
package com.vim;
public class SelectSort {
public static void main(String[] args) {
//在每le一次数组中找到最大的数据,然后和最后的数进行交换
int[] arr = {3, 7, 4, 2, 6, 1};
for(int j=arr.length; j>1; j--){
//找到最大值所在的索引
int max = 0;
for(int i=1; i<j; i++){
if(arr[i] > arr[max]){
max = i;
}
}
//交换
int temp = arr[max];
arr[max] = arr[j-1];
arr[j-1] = temp;
}
for(int i=0; i<arr.length; i++){
System.out.println(arr[i]);
}
}
}
- 插入排序
package com.vim;
public class InsertSort {
public static void main(String[] args) {
//将数字不断地插入到已经排好序的数组中,从最后一个元素开始和数字比较,如果大于数字,就往前差
int[] arr = {3, 7, 4, 2, 6, 1};
for(int j=1; j<arr.length; j++){
//要插入的元素
int insertVal = arr[j];
//要插入的位置
int index = j-1;
while (index >= 0 && insertVal < arr[index]){
//将元素后移
arr[index+1] = arr[index];
//继续往前判断
index--;
}
arr[index+1] = insertVal;
}
for(int i=0; i<arr.length; i++){
System.out.println(arr[i]);
}
}
}
- 堆排序
package com.vim;
public class Tree {
public static void main(String[] args) {
int[] arr = {0,9,4,7,2,1,8,6,3,5};
//1、从下往上,儿子中比出最大的值,然后将这个值与父亲比较,这个值比父亲大,就与父亲交换,称为建立最大堆。
//2、有多少个父结点,每个父结点的索引以及子结点的索引
//父结点的个数 =(数组长度-1)/2
//每个父结点索引 = 0 到 父结点个数-1
//左儿子索引 = 父结点索引*2+1,右儿子索引 = 父结点索引*2+2
//建立最大堆的时候,可以确定从下往上循环的次数
int end = arr.length;
while (end > 1){
//建立最大堆,遍历所有的父结点(从最后一个父结点索引开始遍历)
int parentLength = (end-1)/2;
for(int i=parentLength-1; i>=0 ;i--){
//默认左儿子最大,因为可能出现某个结点没有右儿子的情况
int maxIndex = i*2+1;
if((maxIndex+1 < end) && arr[maxIndex+1] > arr[maxIndex]){
//最大的是右儿子
maxIndex++;
}
//最大的儿子和父结点进行比较交换
if(arr[maxIndex] > arr[i]){
int temp = arr[maxIndex];
arr[maxIndex] = arr[i];
arr[i] = temp;
}
}
//根结点数据与最后一个结点进行交换
int temp = arr[0];
arr[0] = arr[end-1];
arr[end-1] = temp;
//每循环一次,最后一个数的位置向前移动一位
end--;
}
for(int j=0; j<arr.length; j++){
System.out.println(arr[j]);
}
}
}
11、线程池
- 在线程数目到达corePoolSize之前,来的请求会马上创建新的线程去处理; 当线程数目达到corePoolSize后,就会把任务加入到缓存队列中,当缓存队列Queue满了之后,就会创建新的线程去处理任务,一直增加到maxmiumPoolSize,当Queue满了并且线程数目达到了maxmiumPoolSize之后,就会执行拒绝策略。
- keepAliveTime:当线程数目超过corePoolSize之后,线程的空闲时间达到keepAliveTime时,多余的线程会被销毁直到剩下corePoolSize个线程为止。
- 拒绝策略:AbortPolicy默认策略是抛出RejectedExecutionException异常;DiscardPolicy是指直接丢弃任务,不做任何处理也不抛出异常;DiscardOldestPolicy抛弃队列中等待最久的那个任务,然后把新任务放入到队列中;CallerRunsPolicy将请求交给调用者去执行。
- Executors提供的几个方法,存在的问题:FixedThreadPool和SingleThreadPool允许的请求队列长度最大为Integet.MAX_VALUE,可能会堆积大量请求。CachedThreadPool和ScheduledThreadPool允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程。
package com.vim.modules.web.controller;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
for(int i=0; i<9; i++){
executor.submit(()->{
System.out.println(11);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
- 线程池的关闭方式有几种,各自的区别
12、CAS
- 比较并交换,判断内存中的某个位置的值是否为预期值,如果是,说明没有其他线程改过,则更改为新的值,这个过程是原子性的,是一条CPU的原子指令。
- Atomic开头的类,内部使用unsafe类+volatile修饰的数据来解决volatile的非原子性问题,来解决同步问题。
- unsafe里面的所有方法都是native的,基于该类可以像C指针一样的直接操作内存的数据,内部使用的就是compareAndSwap开头的方法来进行while循环判断预期值是否一致。
- 缺点:如果CAS失败,会一直进行尝试,可能会给CPU带来很大的开销。
13、集合 fail-fast 机制(线程不安全)
- ArrayList集合是线程不安全的集合,在高并发下可能会出现ConcurrentModificationException。
- Vector类相比ArrayList,出现要早,而且修改的方法都加了synchronized关键字,底层实现都是使用数组。
- 还可以使用Collections.synchronizedList方法来包装ArrayList。
- 类似的不安全类集合还有:HashSet、HashMap。
14、Java 锁
- 公平锁:多个线程按照申请锁的顺序来获取锁,非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,可能造成优先级翻转或饥饿(每次都没有抢到锁)现象。
- 可重入锁:又名递归锁,指的是同一个线程外层函数获得锁之后,进入内层方法会自动获取该锁。synchronized和ReentrantLock都是可冲入锁。
package com.vim;
public class Test {
public synchronized void method1() throws Exception{
System.out.println("111");
method2();
System.out.println("333");
}
public synchronized void method2() throws Exception{
System.out.println("222");
}
public static void main(String[] args) throws Exception{
Test test = new Test();
new Thread(()->{
try {
test.method1();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
- 自旋锁:尝试获取锁的线程不会阻塞,会采用循环的方式去尝试获取锁,这样的好处是减少了线程上下文切换的消耗,缺点是循环会消耗CPU。
- 读写锁:写锁是独占锁,读锁是共享锁,读写,写写都是互斥锁。
- CountDownLatch:一个线程或多个线程一直等待,直到其他线程执行的操作完成。调用该类await方法的线程会一直处于阻塞状态,直到其他线程调用countDown方法使当前计数器的值变为零。
15、阻塞队列
- 生产消费者模型使用的是synchronized、wait、notify的方式来完成。现在可以使用阻塞队列来完成,好处是不需要关心什么时候需要阻塞,什么时候需要唤醒线程。
- ArrayBlockingQueue:数组结构组成,有界队列。
- LinkedBlockingQueue:链表结构组成,有界(默认值为Integet.MAX_VALUE)队列,需要注意大小。
- PriorityBlockingQueue:优先级排序的无界队列。
- DelayQueue:使用优先级队列实现的延迟无界队列
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列,生产一个,消费一个,不消费,不生产。
- LinkedBlockingDeque:链表结构组成,双向队列。
- 抛出异常:add、remove
- 阻塞方法:put、take
- 返回值:offer、poll
- 检查:element、peek
16、异常机制
- Throwable是所有错误和异常的超类,子类有Error和Exception。Exception分为运行时异常RuntimeException和检查异常CheckException(也称编译时异常)。
- 运行时异常:NullPointerException、ClassCastException、ArrayIndexOutBoundException,此类异常不需要用户强制处理异常。
- 检查异常:IOException,需要用户必须去处理的一类异常, 不处理,程序无法通过编译。
- throw抛出一个具体的异常对象;throws申明异常,将异常的处理交给上一级调用者。在程序中如果手动throw异常对象,需要在方法的后面使用throws申明可能抛出的异常。
17、反射和注解
- 获取想要操作类的Class对象,通过该对象可以获取内部的field、method、constructor。
- 获取Class对象的方式
package com.vim.modules.web.controller;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) throws Exception{
Test test = new Test();
//1.通过实例对象获取
Class testCls1 = test.getClass();
//2.通过类获取
Class testCls2 = Test.class;
//3.通过Class.forName
Class testCls3 = Class.forName(Test.class.getName());
//4.通过Class获取类的属性、方法、构造器等信息
Constructor[] constructors = testCls3.getDeclaredConstructors();
Field[] fields = testCls3.getDeclaredFields();
Method[] methods = testCls3.getDeclaredMethods();
}
}
- 创建对象的方式
package com.vim.modules.web.controller;
import java.lang.reflect.Constructor;
public class Test {
public Test(String i, String j){}
public static void main(String[] args) throws Exception{
//1.new关键字
Test test1 = new Test();
//2.该方式要求Class对象有默认的空构造器
Class testCls = Class.forName(Test.class.getName());
Test test2 = (Test) testCls.newInstance();
//3.利用构造方法区创建对象
Constructor constructor = testCls.getDeclaredConstructor(String.class, String.class);
Test test3 = (Test) constructor.newInstance("1", "2");
}
}
- Annotation注解,是一个接口,程序可以通过反射来获取Annotation对象,通过该对象获取元数据信息。
package com.vim.modules.web.controller;
import java.lang.annotation.*;
@Documented
//修饰的对象范围
@Target(ElementType.FIELD)
//保留的时间,用于描述注解的生命周期:SOURCE、CLASS、RUNTIME
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
String name() default "";
}
- 反射中,Class.forName 和 ClassLoader 区别
18、数据库
- tinyInt 1字节,smallint 2字节,mediumint 3字节,int 4字节,decimal和varchar属于变长型。
19、ThreadLocal
- 为每一个线程提供一个独立的变量副本,从而隔离了线程对数据的访问冲突。相比之下,同步机制采用了“时间换空间”,ThreadLocal采用了“空间换时间”的方式。
- 在Spring中,我们使用模板类(JdbcTemplate、RedisTemplate等)来访问底层数据,虽然模板类通过资源池获取数据连接或会话,但是资源池本身解决的是数据连接和会话的缓存问题,并非数据连接或会话的线程安全问题。比如Spring中的事务控制,为了保证事务内的每一个SQL操作拿到的连接都是一个,就需要使用ThreadLocal。
- 每个Thread内部有一个ThreadLocalMap成员变量,该变量使用Entry数组的方式存储数据,其中Entry的key为threadLoca变量,value就是需要存储的值,这样就可以在一个线程中定义多个ThreadLocal变量。
20、类的实例化顺序
- 比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当 new 的时候, 他们的执行顺序
package com.vim.modules.web.controller;
public class A {
//成员变量
int age = f1();
int f1(){
System.out.println("parent == 成员变量");
return 4;
}
//静态成员变量
static int id=f2();
static int f2(){
System.out.println("parent == 静态成员变量");
return 6;
}
//构造方法
public A() {
System.out.println("parent == 构造方法");
}
//普通块
{
System.out.println("parent == 普通块");
}
//静态块
static {
System.out.println("parent == 静态块");
}
//普通方法
void run1(){
System.out.println("parent成员函数加载");
}
//静态方法
static void walk1(){
System.out.println("parent静态成员函数加载");
}
}
package com.vim.modules.web.controller;
/**
* @作者 Administrator
* @时间 2020-01-15 11:07
* @版本 1.0
* @说明
*/
public class B extends A{
//成员变量
int age = f3();
int f3(){
System.out.println("children == 成员变量");
return 4;
}
//静态成员变量
static int id=f4();
static int f4(){
System.out.println("children == 静态成员变量");
return 6;
}
//构造方法
public B() {
System.out.println("children == 构造方法");
}
//普通块
{
System.out.println("children == 普通块");
}
//静态块
static {
System.out.println("children == 静态块");
}
//普通方法
void run(){
System.out.println("成员函数加载");
}
//静态方法
static void walk(){
System.out.println("静态成员函数加载");
}
}
//执行结果
parent == 静态成员变量
parent == 静态块
children == 静态成员变量
children == 静态块
parent == 成员变量
parent == 普通块
parent == 构造方法
children == 成员变量
children == 普通块
children == 构造方法
21、Map 实现类, 是怎么保证有序的
22、动态代理的几种实现方式
- 使用 JDK 动态代理
package com.vim.modules.web.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyTest {
//接口
interface Person {
void print();
}
//实现类
static class Children implements Person {
@Override
public void print() {
System.out.println("success");
}
}
//代理执行类
static class ProxyPerson implements InvocationHandler{
private Person person;
public ProxyPerson(Person person){
this.person = person;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before");
method.invoke(person, args);
System.out.println("after");
return null;
}
}
public static void main(String[] args) {
Person person = new Children();
//生成代理类
Person proxyPerson = (Person) Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), new ProxyPerson(person));
//执行代理类
proxyPerson.print();
}
}
- 使用CGLIB
package com.vim.modules.web.cglib;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibTest {
//被代理类
static class Children{
public void print(){
System.out.println("success");
}
}
//代理执行类
static class CglibHandler implements MethodInterceptor{
private Object object;
public CglibHandler(Object o){
this.object = o;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("before");
method.invoke(object, objects);
System.out.println("after");
return null;
}
}
public static void main(String[] args) {
Children children = new Children();
//生成代理类
Children childrenProxy = (Children) Enhancer.create(children.getClass(), new CglibHandler(children));
//执行代理类
childrenProxy.print();
}
}
23、单例模式
- 饿汉模式
package com.vim.modules.web.single;
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton(){}
public Singleton getInstance(){
return instance;
}
}
- holder模式
package com.vim.modules.web.single;
public class Singleton {
private static class SingletonHolder{
private static Singleton instance = new Singleton();
}
private Singleton(){}
public Singleton getInstance(){
return SingletonHolder.instance;
}
}
24、深拷贝和浅拷贝
- 浅拷贝:如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。比如clone方法,类实现Cloneable接口,并且覆写Object类的clone方法,调用super.clone即可。
- 深拷贝实现,使用序列化的方式,需要实现 Serializable 接口
package com.vim.modules.web.clone;
import java.io.*;
public class DeepClone {
//浅拷贝实现Cloneable,深拷贝实现Serializable
static class Person implements Cloneable,Serializable{
private String name;
private Book book;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Book getBook() {
return book;
}
public void setBook(Book book) {
this.book = book;
}
//浅拷贝
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
//深拷贝
public Object deepClone() throws IOException, ClassNotFoundException{
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();
}
}
static class Book implements Serializable{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static void main(String[] args) throws Exception {
Book book = new Book();
book.setName("java");
Person person1 = new Person();
person1.setBook(book);
//浅拷贝
// Person person2 = (Person) person1.clone();
//深拷贝
Person person2 = (Person) person1.deepClone();
System.out.println(person1.getBook().getName());
person1.getBook().setName("php");
System.out.println(person2.getBook().getName());
}
}
25、Spring 相关
- aop
- ioc:控制反转:不需要去new对象,只需要将该对象的控制权交给Spring;依赖注入:告诉Spring要使用某个对象。
- 事务
- springmvc运行流程
26、Mybatis 相关
- 一级缓存和二级缓存
- 分页插件原理