Java -- 题目

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 相关

  • 一级缓存和二级缓存
  • 分页插件原理

 

 

 

 

 

 

发布了100 篇原创文章 · 获赞 20 · 访问量 1万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章