面试八股文

// 创建线程的4种方式
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 方式一:extents Thread
        MyThread myThread = new MyThread();
        myThread.start();

        // 方式二:implements Runnable接口
        Thread thread = new Thread(new MyRunnable());
        thread.start();

        // 方式二:匿名内部类形式
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类方式:线程执行了");
            }
        });
        thread1.start();

        // 方式二:函数式接口用lambda形式表示
        Thread thread2 = new Thread(()->{
            System.out.println("lambda形式: 线程执行了");
        });
        thread2.start();

        // 方式三:implements Callable接口
        FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
        Thread thread3 = new Thread(futureTask);
        thread3.start();
        // implements Callable方式可以诸塞式地获取返回值
        String result = futureTask.get();
        System.out.println(result);

        // 方式四:线程池 工作中不建议使用Executors来创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService.execute(new MyRunnable());

    }
}
// Thread
class Thread implements Runnable

// Callable
class FutureTask<V> implements RunnableFuture
interface RunnableFuture<V> extends Runnable, Future<V>

总结:以上几种方式底层都是基于Runable

start()run()的区别:
start方法启动线程后自动调用run方法
如果我们没有启动线程(没有调用线程的start()方法)而是在应用代码中直接调用run()方法,
那么这个线程的run()方法其实运行在当前线程之中,而不是运行在其自身的线程中

// 为啥不建议使用Executors来创建线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

LinkedBlockingQueue:容量可以是无界的,可能导致OOM
/** The capacity bound, or Integer.MAX_VALUE if none */
private final int capacity; 

public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
    
public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }
 
// 使用Executors来创建线程池除了可能造成OOM之外,使用Executors创建线程池也不能自定义线程的名字,不利于排查问题
// 工作中直接使用ThreadPoolExecutor来创建线程

// Executors类提供了4种不同的线程池
newCachedThreadPool:corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;keepAliveTime为60S,意味着线程空闲时间超过60S就会被杀死;采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
newFixedThreadPool:创建一个固定大小的线程池,采用无界的阻塞队列
newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
newScheduledThreadPool:适用于执行延时或者周期性任务

// 线程池都有哪几种工作队列
1ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2LinkedBlockingQueue
一个基于链表结构的无界阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue
Executors.newFixedThreadPool()使用了这个队列
3SynchronousQueue
一个不存储元素的阻塞队列。Executors.newCachedThreadPool()使用了这个队列。
4PriorityBlockingQueue
一个具有优先级的无限阻塞队列
 ThreadPoolExecutor创建线程: 
 ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

corePoolSize:指定了线程池中核心线程数。
maximumPoolSize:指定了线程池中的最大线程数量。
keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。
unit:keepAliveTime 的单位。
workQueue:任务队列,存储被提交但尚未被执行的任务。
threadFactory:线程工厂,用于创建线程,一般用默认的即可。
RejectedExecutionHandler :拒绝策略,当任务太多来不及处理,如何拒绝任务
// 线程池状态
RUNNING: 线程池正常运行,线程池进入RUNNING状态,允许接受新任务
SHUTDOWN: 调用线程池shutdown()方法时,线程池进入SHUTDOWN状态,队列中的任务会继续被处理,但是线程池不再接受新任务
STOP: 调用线程池showdownnow()方法时,线程池进入STOP状态,正在执行的线程会被中断并打上中断标志位,队列中的任务不会被处理,不再接受新任务
TIDYING(整理): 线程池中无线程在运行时,线程池进入TIDYING状态,并且会调用terminated(),该方法为空方法,留给程序员进行扩展
TERMINATE: terminated()方法执行完后,线程池进入TERMINATE状态

线程池执行过程

// synchronized(内置锁)和 ReentrantLock(可重入锁)有哪些不同点
// ReentrantLock 除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法
1.用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。
// 加锁方法
public synchronized void method() {
   // ...
}
// 加锁代码块
public void method() {
    synchronized (this) {
        // ...
    }
}

public class LockExample {
    // 创建锁对象
    private final ReentrantLock lock = new ReentrantLock();
    public void method() {
        // 加锁操作
        lock.lock();
        try {
            // ...
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

2.获取锁和释放锁的机制不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。

3.锁类型不同:synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁。
synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁

// 默认情况下 ReentrantLock 为非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

4.响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
ReentrantLock 可以使用 lockInterruptibly 获取锁并响应中断指令, ReentrantLock 可以响应中断并释放锁,从而解决死锁的问题
synchronized 不能响应中断,如果死锁就会一直等待下去

5.底层实现不同:synchronizedJVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。
monitorenter 表示进入监视器,相当于加锁操作,而 monitorexit 表示退出监视器,相当于释放锁的操作

abstract static class Sync extends AbstractQueuedSynchronizer {
AQS ( Abstract Queued Synchronizer )是一个抽象的队列同步器,通过维护一个共享资源状态( Volatile Int State )
和一个先进先出( FIFO )的线程等待队列来实现一个多线程访问共享资源的同步框架
AQS = 双向链表队列 + State信号量

/**
* The synchronization state.
*/
private volatile int state;
getState()setState()compareAndSetState(),均是原子操作

AQS维护的共享资源状态

// new FairSync() : new NonfairSync() 区别
// 非公平锁性能比公平锁高 5~10 倍
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;
}

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;
}
// Sychronized的锁升级过程是怎么样的?

// 偏向锁: Sychronized在锁对象头中记录当前获取到该锁的线程ID,如果该线程再次来获取该锁就可以直接获取到,也就是支持锁重入
// 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,如果另外线程来竞争锁,偏向锁升级为轻量级锁,轻量级锁底层通过自旋实现,不会阻塞线程、唤醒线程操作(如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源)
// 自旋锁:线程通过CAS(Compare And Swap)循环获取锁,如果持有锁的线程需要长时间占用锁执行同步块,会导致获取锁的时间很长,自旋锁在获取锁过程占用cpu做无用功,自旋次数过多会升级为重量级锁
// 重量级锁:JDK1.6之前Sychronized为重量级锁,重量级锁会阻塞线程、唤醒线程,JDK1.6后对锁做了大量优化如偏向锁,轻量锁,自旋锁,自适应锁等等
// 重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁
// Sychronized 和 ReetrantLock的区别
1. sychronized
// finally块执行问题
public static void main(String[] args) {
    System.out.println(Foo(50));
    System.out.println("-----------------");
    System.out.println(Foo(200));
}

static int Foo(Integer a)
{
    try {
        if (a > 100)
            throw new Exception();
        return a;
    } catch (Exception e) {
        System.out.println("异常执行~");
        return 0;
    } finally {
        System.out.println("finally执行~");
    }
}

// 执行结果如下:finally无论如何都会执行(异常时会执行,return前会执行,除非System.exit(status))
finally执行~
50
-----------------
异常执行~
finally执行~
0
// 分布式如何控制事务
// ThreadLocal 有哪些应用场景?
1.解决过度传参问题
public class ThreadLocalTest {
    private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
    void work(User user) {
        try {
            userThreadLocal.set(user);
            getInfo();
            checkInfo();
        } finally {
            userThreadLocal.remove();
        }
    }
 
    void setInfo() {
        User u = userThreadLocal.get();
        //.....
    }
 
    void checkInfo() {
        User u = userThreadLocal.get();
        //....
    }
}

2.优雅解决SimpleDateFormat线程不安全问题
public class DataUtils {

    private DataUtils() {
    }

    private static Map<String, ThreadLocal<SimpleDateFormat>> map = new HashMap<>();

    private static ThreadLocal<SimpleDateFormat> getThreadLocal(String pattern) {
        // 如果sdfMap有pattern的value就直接返回,如果没有就创建一个,然后返回
        ThreadLocal<SimpleDateFormat> threadLocal = map.computeIfAbsent(pattern, p ->
                ThreadLocal.withInitial(() -> new SimpleDateFormat(pattern))
        );
        return threadLocal;
    }

    public static String getDataStr(Date date, String pattern) {
        SimpleDateFormat simpleDateFormat = getThreadLocal(pattern).get();
        return simpleDateFormat.format(date);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        executorService.execute(() -> System.out.println(getDataStr(new Date("2021/01/17"), "yyyy-MM-dd")));
        executorService.execute(() -> System.out.println(getDataStr(new Date("2021/02/17"), "yyyy-MM-dd")));
        executorService.execute(() -> System.out.println(getDataStr(new Date("2021/03/17"), "yyyy-MM-dd")));
        executorService.execute(() -> System.out.println(getDataStr(new Date("2021/04/17"), "yyyy-MM-dd")));
        executorService.execute(() -> System.out.println(getDataStr(new Date("2021/05/17"), "yyyy-MM-dd")));
        executorService.shutdown();
        
    }
}

// 底层如何实现?
Threadlocal是线程的本地存储机制,底层使用ThreadLocalMap,map中key为ThreadLocal对象,value为需要缓存的值
public class Test {
    public ThreadLocal<String > nameLocal = new ThreadLocal<>();
    public ThreadLocal<String > ageLocal = new ThreadLocal<>();
    public static void main(String[] args) {
        Test test = new Test();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                test.nameLocal.set(Thread.currentThread().getName() + "的nameLocal数据");
                test.ageLocal.set(Thread.currentThread().getName() + "的ageLocal数据");
                System.out.println(test.nameLocal.get());
                System.out.println(test.ageLocal.get());
            }).start();
        }
    }
}

// set源码:获取当前线程的ThreadLocalMap
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocal.ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

// 用完ThreadLocal对象需要手动调用remove方法,否则会出现内存泄漏风险
强引用:最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象
弱引用:只具有弱引用的对象,不管当前内存空间足够与否,垃圾回收器都会回收它的内存

当key只有弱引用被回收后,value一直有强引用不会被回收,导致value内存溢出
业务代码中使用完ThreadLocal,threadLocal Ref被回收了(现在没有任何强引用指向threadlocal实例)
由于ThreadLocalMap只持有ThreadLocal的弱引用,所以threadlocal就可以顺利被gc回收,此时Entry中的key=null
但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry-> value,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏

// 为啥key不设置为强引用
业务代码中使用完ThreadLocal,threadLocal Ref被回收了
因为threadLocalMap的Entry的key强引用了threadLocal,造成threadLocal无法被回收
在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏

内存泄漏与强弱引用关系

// Tomcat中为什么要使用自定义类加载器

// Tomcat中可以部署多个应用,比如一个订单系统中可能存在一个用户类,一个库存中也可能存在一个用户类,如果tomcat中只存在一个类加载器,这样就存在冲突问题,所以Tomcat会为部署的每个应用都生成一个类加载器实例webAppClassLoader,Tomcat使用自定义加载器实现热加载功能。
// 热加载:tomcat监听到类有修改,会去重新建一个webAppClassLoader,当有新的请求过来的时候,会用这个新的webAppClassLoader去加载这个类
// JDK、JRE、JVM区别
JDK ( Java SE Development Kit ), Java 标准开发包,它提供了编译、运行 Java 程序所需的各种工具和资源,包括 Java 编译器、Java 运行时环境,以及常用的 Java 类库等.
JRE ( Java Runtime Environment ), Java 运行环境,用于运行 Java 的字节码文件。 JRE 中包括了 JVM 以及 JVM 工作所需要的类库
JVM ( Java Virtual Mechinal ), Java 虚拟机,是 JRE 的一部分,它是整个 java 实现跨平台的最核心的部分,负责运行字节码文件

开发 Java 程序,那就需要 JDK ,因为要编译 Java 源文件
只想运行已经编译好的 Java 字节码文件,也就是.class 文件,那么就只需要 JRE
JDK中包含了JRE, JRE中包含了JVM
JVM 在执行 Java 字节码时,需要把字节码解释为机器指令,而不同操作系统的机器指令是有可能不一样的,所以不同的操作系统JDK不同
// HashCode 与 equals
// equals源码,Object的equals()方法等价于==,也就是判断两个引用的对象是否是同一对象,所谓同一对象就是指内存中同一块存储单元
public boolean equals(Object obj) {  
  return (this == obj);  
}

User user1 = new User("txl", 18);
User user2 = new User("txl", 19);
System.out.println(user1 == user2); // false
System.out.println(user1.equals(user2)); // false

// 要判断两个对象逻辑相等就要覆盖equals()方法,当覆盖equals()方法时建议覆盖hashCode()方法
@Override
public boolean equals(Object o) {
    User user = (User) o;
    return user.getName().equals(this.name);
}

@Override
public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    return result;
}

User user1 = new User("txl", 18);
User user2 = new User("txl", 19);
System.out.println(user1 == user2); // false
System.out.println(user1.equals(user2)); // true

// 在一些散列存储结构的集合中(Hashset,HashMap...)判断两个对象是否相等是先判断hashCode是否相等,再判断两个对象equals()是否相等
// 如果只重写了equals(),不重写hashcode(), 那么hashcode不相等,则从HashMap中取不出来数据
HashMap<User, String> map = new HashMap<>();
User user = new User("txl", 18);
map.put(user, "勇往无前");
System.out.println(map.get(new User("txl")));

HashMap.get源码分析

// String、StringBuffer、StringBuilder
String是不可变的,如果去修改,会新生成一个字符串对象
Strings are constant; their values cannot be changed after they are created.
String s = "abc";
s = "def";

StringBufferStringBuilder是可变的
StringBuffer是线程安全的
like a String, but can be modified, A thread-safe, The methods are synchronized
@Override
public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String.valueOf(obj));
    return this;
}

StringBuilder是线程不安全的,单线程环境下效率更高
This class provides an API compatible with StringBuffer,but with no guarantee of synchronization.
@Override
public StringBuilder append(Object obj) {
    return append(String.valueOf(obj));
}
<? extends T> 表示包含T在内的任何T的子类
<? super T> 表示包含T在内的任何T的父类

泛型:将类型由原来的具体的类型参数化
public class NumList<E extends Number> {
    private ArrayList<Integer> integerList = new ArrayList<>();
    private ArrayList<BigDecimal> bigDecimalList = new ArrayList<>();

    public void add(E e){
        if (e instanceof Integer){
            integerList.add((Integer) e);
        }
        if (e instanceof BigDecimal){
            bigDecimalList.add((BigDecimal) e);
        }
    }

    @Override
    public String toString() {
        return "NumList{" +
                "integerList=" + integerList +
                ", bigDecimalList=" + bigDecimalList +
                '}';
    }
}
// == 和 equals的区别
==:比较 "地址"
equals:父类Object类比较地址,Integer类、String类等对equals重写后比较 "值"
public boolean equals(Object obj) {
    return (this == obj);
}
// 重载和重写的区别
重载:同一个类中,方法名相同,参数类型、个数、顺序不同,如果只是返回值、修饰符不同,会编译报错
重写:发生在父子类中,方法名、参数列表相同
返回值范围<=父类
抛出的异常范围<=父类
访问修饰符范围>=父类,如果父类方法访问修饰符为private,则子类不能对其重写
// List 和 Set的区别
List :有序,按对象插入的顺序保存对象,可重复,允许插入多个 null 元素对象,可以使用 iterator 取出所有元素,再逐一遍历
还可以使用 get(int index)获取指定下标的元素.
Set :无序,不可重复,最多允许有一个 Null 元素对象,取元素时只能用 iterator 接口取得所有元素,再逐一遍历各个元素
// ArrayList 和 LinkedList区别
ArrayList基于数组、LinkedList基于链表
随机访问get、set,ArrayList优于LinkedList
添加删除add、remove,LinkedList优于ArrayList
都实现了list接口,LinkedList额外实现了Deque接口,所以LinkedList可以当做队列来使用
// concurrentHashMap的扩容机制
ConcurrentHashMapJava 中一种线程安全的 HashMap 实现
大家选择ConcurrentHashMap而不是hashTable的原因:
hashtable采用在所有的方法上加上synchronized来保证线程安全,而synchronized是全局锁
ConcurrentHashMap 采用分段锁的机制,锁粒度更小,因此性能更高

1.7版本:
Segment+ReentrantLock,一个Segment是一个1.7版本HashMap(无红黑树)
ConcurrentHashMap并发度为segment个数
Segment继承了ReentrantLock,通过ReentrantLock进行分段加锁
Segment内部会进行扩容,不影响其他Segment对象

1.8版本:
HashMap+(synchronized+CAS)

nextTable: 扩容期间,元素迁移的新Map, nextTable是共享变量。
sizeCtl: 多线程之间,通过CAS设置sizeCtl属性,通过sizeCtl来判断ConcurrentHashMap当前所处的状态。
transferIndex: 扩容索引,表示已经完成数据分配的table数组索引位置,其他线程根据这个值帮助扩容从这个索引位置继续转移数据
ForwardingNode节点: 标记作用,表示此节点已经扩容完毕

// 为什么要使用CAS+Synchronized取代Segment+ReentrantLock
Synchronized因为自旋锁,偏向锁,轻量级锁的原因,不用将等待线程挂起
CAS的全称是Compare And Swap 即比较交换,其算法核心思想如下
执行函数:CAS(V,E,N),其包含3个参数:V表示要更新的变量;E表示预期值;N表示新值
若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做

ReentrantLock中会调用LockSupport.park()方法,会导致线程挂起,线程上下文切换消耗系统资源

1.7版本的concurrentHashMap

// JDK1.7到JDK1.8 HashMap发生了哪些变化?
1.7中底层是数组+链表,使用头插法
1.8中底层是数组+链表+红黑树,使用尾插法,加红黑树的目的是提高HashMap插入和查询整体效率
(HashMap 查找的时间复杂度 O(1) + O(n) (before JDK 1.7) | O(1) + O(logN) (after JDK 1.8))
JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题 (在并发环境中进行扩容时)。
但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
1.8简化了哈希算法,复杂的hash算法是为了提高散列性提高hashmap的整体效率,1.8新增了红黑树,可适当简化hash算法节省CPU资源

注意:下图中链表转换红黑树的条件少了一个节点数大于64,即完整的转换条件为链表长度大于8,
且数组长度大于等于64,如果数组长度<64那么先进行数组扩容,当链表长度小于6,则会将红黑树转回链表)
在这里插入图片描述

// HashMap的put方法
1.根据Key通过hash运算得出数组下标
2.如果数组下标位置元素为空,则将key和value封装为一个Entry对象(JDK 1.7中是一个Entry JDK 1.8中是一个Node对象)并放入该位置
3.如果数组下标元素不为空(1.7先判断是否扩容再插入,1.8先插入后判断是否扩容)1)如果是JDK 1.7,则先判断是否需要扩容,如果需要扩容就进行扩容,如果不需要扩容就生成Entry对象,并且使用头插法添加到当前位置的链表中
(2)如果是JDK 1.8,则会先判断当前位置的Node的类型,看是红黑树Node,还是链表Node
1)如果是红黑树Node,则将key和value封装为一个红黑树节点并添加到红黑树中,在这个过程中会先判断红黑树中是否存在key,如果存在则更新value
2) 如果是链表,则将key和value封装为一个Node并通过尾插法插入到链表的最后,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新,不存在,插入到链表后,会看当前链表的节点个数,如果链表长度>8且数组>=64,那么将会将链表转化为红黑树。
3)将key和value封装为Node插入到链表或红黑树后,再判断是否需要扩容,如果需要就扩容,如果不需要就结束Put方法。
// 深拷贝与浅拷贝
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
// HashMap扩容机制原理
1.7版本
生成新数组(一般是原来数组的两倍)
遍历数组每个位置上链表的每个元素,取每个元素的key,计算出元素在新数组中的下标,并添加到新数组中去
所以元素转移完后,将新数组赋值给HashMap对象的table属性

1.8版本
生成新数组(一般是原来数组的两倍)
遍历数组每个位置上链表或红黑树的每个元素
如果是链表:遍历数组每个位置上链表的每个元素,取每个元素的key,计算出元素在新数组中的下标,并添加到新数组中去
如果是红黑树:遍历红黑树,计算元素在新数组的下标位置,统计每个下标位置的元素个数
	a.如果没超过8个,生成一个链表
	b.如果超过8个,生成一个红黑树
所以元素转移完后,将新数组赋值给HashMap对象的table属性
// CopyOnWriteArrayList的底层原理
1.内部也是数组实现,添加元素的时候,会复制一个新数组,写操作在新数组上进行,读操作在原数组上进行
2.写操作会加ReentrantLock确保线程安全,写结束后会指向新数组
CopyOnWriteArrayList允许在写的时候读取数据
	优点:线程安全的,适合读多写少的场景
	缺点:占用内存(每次写都要copy一个新数组)、读到的数据可能不是最新数据,不适合实时性要求高的场景
	
// add源码
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
final void setArray(Object[] a) {
    array = a;
}	
// java中的异常
1Error类是程序无法处理的错误,由jvm产生和抛出,遇到错误,jvm一般会选择终止线程
2Exception类分为RuntimeException(运行时异常)和 非运行时异常(受检异常,需要处理这类异常,否则无法通过编译)

异常体系

// 什么时候应该抛出异常,什么时候捕获异常
本方法能否合理处理该异常,如果处理不了就继续向上抛异常
// java中有哪些类加载器
JVM中存在三个默认的类加载器
1.BootstrapClassLoader(引导类加载器):加载%JAVA_HOME%/lib下的jar包和class文件
2.ExtClassLoader(扩展类加载器):加载%JAVA_HOME%/lib/ext下的jar包和class文件
3.AppClassLoader(系统类加载器):加载classpath下的类

自定义加载器:通过继承java.lang.ClassLoader类的方式实现自己的类加载器

BootstrapClassLoader由原生代码(如C语言)编写,不继承自java.lang.ClassLoader
BootstrapClassLoaderExtClassLoader的父加载器
ExtClassLoaderAppClassLoader的父加载器
AppClassLoader完成Java应用的类的加载

// 类加载器双亲委派模型
JVM在加载一个类时,会调用ApplicationClassLoader 的loadClass方法来加载这个类。
不过在这个方法中,会先使用ExtensionClassLoader的loadClass方法来加载类。
同样ExtensionClassLoader的loadclass会使用BootstrapClassLoader来加载类。如果BootstrapClassLoader加载到了就直接成功。
如果BootstrapClassLoader没有加载到,那么ExtensionClassLoader就会自己尝试加载该类。如果没有加载到,那么则会ApplicationClassLoader 来加载这个类。
所以,双亲委派指的是jvm在加载类时,会委派给ExtensionClassLoaderBootstrapClassLoader进行加载,如果没加载到才有自己进行加载。
// JVM中哪些是线程共享区
堆区和⽅法区是所有线程共享的,栈、本地⽅法栈、程序计数器是每个线程独有的
	堆: 不用多说了,放对象的地方
	方法区: 类定义的成员变量丶常量丶静态变量丶方法都在这里
	栈: 程序运行才有的,会把运行时的方法压入栈,里面有局部变量等东西
	本地方法栈: 操作系统方法
	程序计数器: 标记代码走到哪里了

在这里插入图片描述

// 如何排查内存溢出问题
-XX:+HeapDumpOnOutOfMemoryError 当发送OOM时会自动dump下.hprof文件,然后用Jprofile工具查看分析(Memory Analyzer (MAT))
根据dump文件找到占用内存异常的实例对象,定位到具体的代码
// 项目如何排查JVM问题(JVM调优)
jmap:得到运行java程序的内存分配的详细情况 
	例子:jmap -histo 55126 (-histo打印每个class的实例数目,内存占用,类全名信息)

jstack:查看线程运行情况,线程阻塞,线程死锁
	死锁,Deadlock(重点关注)
	等待资源,Waiting on condition(重点关注)
	等待获取监视器,Waiting on monitor entry(重点关注)
	阻塞,Blocked(重点关注)
	
jstat:查看垃圾回收情况,特别是fullgc
	例子:jstat -gc pid (-gc:统计jdk gc时heap信息)
// 一个对象从加载到JVM,再到被GC清除,都经历了什么过程?
1.首先将字节码文件内容加载到方法区
2.根据类信息在堆中创建对象
3.对象首先会分配在堆中年轻代的Eden区,经过一次Minor GC后,如果对象存活则会进入Survivor区,在后续的每次Minor GC中,如果对象一直存活就会在Survivor区来回拷贝,每次移动年龄+1
4.当年龄超过15后,对象依然存活,对象就会进入老年代
5.老年代满后会触发Full GC对老年代和新生代进行回收

// Minor GC 与 (Major GC / Full GC)
Minor GC: 指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快,当Eden区满触发回收
Major GC / Full GC: 指发生在老年代的 GC,Major GC 的速度一般会比 Minor GC 的慢 10 倍以上
(1)调用System.gc时(一般情况下,垃圾回收应该是自动进行的,无须手动触发)2)老年代空间不足

在这里插入图片描述

// 如何确定一个对象是否是垃圾
1.引用计数算法:在早期JDK中使用,对象的引用个数为0认为是垃圾,无法解决循环引用的问题
2.可达性算法:从根对象一直向下找,找不到的对象就是垃圾
// JVM中有哪些垃圾回收算法
1.标记-清除算法:会产生大量内存碎片(缺点)
2.标记-复制算法:将内存分成大小相等的两半,每次使用其中一半,垃圾回收时将这一块的存活对象拷贝到另一半内存中,然后当前这一半的内存可以直接清除,浪费空间(缺点)
3.标记-整理算法:移动存活的对象到一端,然后清除边界以外的内存,对于老年代中GC之后大部分都为存活对象,将这些对象移动且更新引用很耗时(缺点)

更新引用需要暂停用户线程来保证用户线程访问对象不会出错,简称STW,Stop the WordGC各种算法优化的重点是减少STW,同时也是JVM调优的重点
// 常用JVM启动参数有哪些
1.设置堆内存(专用服务器上需要保持-Xms-Xmx一致,否则应用刚启动可能就有好几个Full GC。 当两者配置不一致时,堆内存扩容可能会导致性能抖动)
-Xmx,指定最大堆内存。 如 -Xmx4g
-Xms,指定堆内存空间的初始大小。 如 -Xms4g
2.设置Metaspace大小
-XX:MetaspaceSize=size //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=size //设置 Metaspace 的最大大小,Java8默认不限制Meta空间,一般不允许设置该选项。
3.GC相关
-XX:+UseG1GC:使用 G1 垃圾回收器
-XX:+PrintGCDetails 打印GC日志详情
-XX:+PrintGCDateStamps 打印GC事件发生的日期时间
4.内存溢出
-XX:+HeapDumpOnOutOfMemoryError 内存溢出时,自动Dump堆内存
-XX:HeapDumpPath=/usr/local/ 自动Dump的hprof文件会存储到/usr/local/目录下

在这里插入图片描述

// 说说对线程安全的理解
一段代码在多线程同时执行的情况下,能否得到正确的结果
// 对守护线程的理解
线程分用户线程、守护线程,用户线程是普通线程,守护线程是JVM的后台线程,比如垃圾回收线程就是一个守护线程,守护线程在其他普通线程都停止之后自动关闭,我们可以通过thread.setDaemon(true)来把一个线程设置为守护线程
// 串行、并行、并发的区别
并行:同一时刻同时执行
并发:可以通过时间分片轮流执行,视觉上是同时执行
// 死锁的原因
1、互斥
一个资源同时只能被一个线程使用
2、请求并保持
线程在请求资源阻塞的时候,并不会释放其已经拥有的资源。
3、不可剥夺
对于线程已经获得的资源,只能线程自己释放,其他线程无法强制剥夺。
4、循环等待
两个或者两个以上线程出现等待资源出现循环依赖

// 死锁的解决办法
0.死锁检测(预防死锁)
1、互斥条件
这个条件一般是无法打破,因为锁大部分的场景就是同时只能允许一个线程获得。

2、请求并保持
防止线程在持有资源后并申请其他资源的情况。线程一次性申请所有需要的资源。申请成功就运行,不成功就等待。
缺点:线程会提前申请后期运行需要的资源,会导致其他线程无法获取对应的资源。会造成线程饥饿,资源的利用率太低。

3、不可剥夺条件
当想要申请新资源并且失败的时候,将当前拥有的资源释放。当新资源可用时,会重新申请之前放弃的资源。
缺点:反复的放弃和申请资源会造成资源的浪费,降低系统的吞吐量。

4、循环等待条件
将所有的资源从小到大编号,线程申请资源按照编号从小到大申请
// 线程池底层工作原理
1. 线程池中线程数量<corePoolSize,创建新的线程
2. 线程池中线程数量=corePoolSize,缓冲队列workQueue未满,任务进入缓冲队列
3. corePoolSize<=线程池中线程数量<maximumPoolSize,缓冲队列workQueue满,创建新的线程来处理被添加的任务
4. 线程池中线程数量=maximumPoolSize,通过handler拒绝策略来处理任务
5. 当线程池中线程数量>corePoolSize,如果某线程空闲时间超过KeepAliveTime,线程将被终止,线程池通过这样动态调整线程数
// reentrantLock中tryLock()和lock()的区别
tryLock()是尝试加锁,加到锁返回TRUE,没加到返回FALSE,不会阻塞线程
lock()会阻塞线程直到加到锁为止,方法没有返回值
// CountDownLatch 和 Semaphore的区别
CountDownLatch表示计算器,可以设置一个数
调用CountDownLatchawait()将会阻塞,并放入AQS队列中等待唤醒
调用CountDownLatchcountDown()方法对CountDownLatch数字减一,当数字变为0时,唤醒AQS队列中线程

Semaphore表示信号量,可以设置一个数
调用acquire()获取,如果没有获取到线程将被阻塞并放入AQS队列中排队
调用release()释放,并唤醒AQS队列中等待资源的线程

CountDownLatch countDownLatch = new CountDownLatch(3);
countDownLatch.await();
countDownLatch.countDown();

Semaphore semaphore = new Semaphore(3);
semaphore.acquire(2);
semaphore.release(3);
// 谈谈对IOC的理解
IOC(Inversion Of Control)控制反转:
控制什么:
	控制对象的创建
	控制对象内属性的赋值
反转:将对象的控制权限交给spring。之前我们创建对象时用new,现在直接从spring容器中取

我们可以通过注解或者xml配置文件的方式把类给spring,spring把类转换成Bean Definition即bean定义对象,然后由bean工厂负责根据Bean定义创建出对象,其生命周期由spring容器管理。 用户在使用时,只需要通过注解@Autowired@Resource 注入使用
// 单例Bean和单例模式
单例模式:JVM中某个类的对象只会存在一个
单例Bean:某个name的Bean是唯一的,不同name的Bean可以是同一个类不同对象
// 单个事务在Spring中的处理流程
Spring事务使用AOP的机制实现,会在@Transactional注解修饰的方法前后分别织入开启事务的逻辑,以及提交或回滚的逻辑。
@Transactional可以修饰在方法或者类上,区别就在于修饰于类上的,会对该类下符合条件的方法(例如private修饰的方法就不符合条件)前后都织入事务的逻辑
默认情况下只有RunTimeException以及Error是会进行回滚的,如果自定义了抛出异常需要@Transactional显式声明了rollbackFor,否则事务失效

在这里插入图片描述

// Spring事务传播机制
事务传播是如果a方法中调用了b方法,那么b方法是用a的事务呢还是单独开启另外的事务呢?
嵌套事务: 需要考虑嵌套事务下的提交顺序,以及回滚顺序, 对此,Spring提供了多种的传播机制,每种传播机制的效果都不尽相同

REQUIRED:默认值,支持当前事务,如果没有事务会创建一个新的事务(默认)
SUPPORTS:支持当前事务,如果没有事务的话以非事务方式执行
MANDATORY:支持当前事务,如果没有事务抛出异常
REQUIRES_NEW:创建一个新的事务并挂起当前事务
NOT_SUPPORTED:以非事务方式执行,如果当前存在事务则将当前事务挂起
NEVER:以非事务方式进行,如果存在事务则抛出异常
NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与REQUIRED类似的操作

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

// Spring事务失效场景

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

// Spring中的Bean是线程安全的吗?
不是线程安全的,spring bean默认来说,singleton,都是线程不安全的,java web系统,一般来说很少在spring bean里放一些实例变量,一般来说他们都是多个组件互相调用,最终去访问数据库的,但是如果其中变量是线程安全的,一堆方法的调用也是线程安全的,就看这个变量是不是被全局共享使用,如果非要定义变量需要自己解决线程安全问题

最后总结Spring容器中的Bean是否线程安全其实和Sping容器是没有什么关系的。Spring容器只负责创建和管理Bean,并未提供Bean的线程安全策略。因此,Spring容器中的Bean并不具备线程安全的特性。Spring中的Bean又可分为多例bean和单例bean,单例bean又可分为无状态的bean和有状态的bean。多例bean和无状态的bean是不需要考虑线程安全的。只有有状态的单例bean是需要考虑线程安全的,可使用ThreadLocal来管理成员变量,使之线程隔离,以达到线程安全的目的。
// Spring中Bean创建的生命周期有哪些步骤
1.通过XMLJava annotation(注解)以及Java Configuration(配置类)等方式加载Spring Bean
2.Bean解析成Spring内部的BeanDefinition结构,将bean(例如<bean>)的定义信息存储到这个对应BeanDefinition相应的属性中
3.BeanFactoryPostProcessor是对BeanDefinition属性填充、修改等操作
4.BeanFactory实例化生成我们的bean,根据class属性反射机制实例化对象,反射赋值设置属性
5.调用Aware感知接口:例如BeanNameAwareApplicationContextAware6.如果检测到Bean对象实现了BeanPostProcessor后置处理器,
7.初始化BeanInitializingBean和init-method)
8.销毁

在这里插入图片描述

// ApplicationContext和BeanFactory有什么区别
BeanFactorySpring 中非常核心的组件,表示Bean工厂,可以生成 Bean ,维护 Bean
ApplicationContext 继承了 BeanFactory ,所以 ApplicationContext 拥有 BeanFactory 所有的特点,也是一个Bean工厂,但是 ApplicationContext 除开继承了 BeanFactory 之外,还继承了诸如 EnvironmentCapableMessageSourceApplicationEventPublisher 等接口,从而ApplicationContext 还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory 所不具备的
// Spring中的事务是如何实现的
1.Spring事务底层是基于数据库事务和 AOP 机制的
2.首先对于使用了@ Transactional 注解的 Bean , Spring 会创建一个代理对象作为 Bean
3.当调用代理对象的方法时,会先判断该方法上是否加了@ Transactional 汪解
4.如果加了,那么则利用事务管理器创建一个数据库连接
5.并旦修改数据库连接的 autocommit 属性为 false ,禁止此连接的自动提交,这是实现 Spring 事务非常重要的一步
6.然后执行当前方法,方法中会执行 sql
7.执行完当前方法后,如果没有出现异常就直接提交事务
8.如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
9.Spring 事务的隔高级别对应的就呈数据库的隔高级别
10.Spring 事务的传播机制是 Spring 事务自己实现的,也是 Spring 事务中最复杂的
11.Spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就呈先建立一个数据库连接,在此新数据库连接上执行 sql
// Spring容器的启动流程
1.首先会进行扫描,扫描得到所有的 BeanDefinition对象,并存在一个 Map2.非懒加载的单例BeanDefinition进行创建Bean,对于多例 Bean 不需要在启动过程中去进行创建,对于多例 Bean 会在每次获取 Bean 时利用BeanDefinition去创建Bean
3.BeanDefinition创建Bean:就是 Bean 的创建生命周期,这期间包括了合并 BeanDefinition 、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤
4.单例 Bean 创建完了之后, Spring 会发布一个容器启动事件
5.Spring启动结束
// Spring中用到哪些设计模式
☆(1)工厂模式:Spring使用工厂模式,通过BeanFactoryApplicationContext来创建对象
☆(2)单例模式:Bean默认为单例模式
☆(3)策略模式:例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略
☆(4)代理模式:SpringAOP功能用到了JDK的动态代理和CGLIB字节码生成技术
☆(5)模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate, JmsTemplate, JpaTemplate6)适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller7)观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。
(8)桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,客户在每次访问中根据需要会去访问不同的数据库
1. @SpringBootApplication 注解:这个注解标识了一个 SpringBoot 工程,它实际上是另外三个注解的组合,这三个注解是:
	a. @SpringBootConfiguration :这个注解实际就是一个@Configuration ,表示启动类也是一个配置类
	b. @EnableAutoConfiguration :Spring 容器中导入了一个 Selector ,用来加载 ClassPathSpringFactories 中所定义的自动配置类,将这些自动加载为配置 Bean
	c. @ComponentScan :标识扫描路径,因为默认是没有配置实际扫描路径,所以 SpringBoot 扫描的路径是启动类所在的当前目录
2. @Bean 注解:用来定义BeanSpring 在启动时,会对加了@Bean 注解的方法进行解析,将方法的名字做为 beanName,并通过执行方法得到bean对象
3. @Controller@Service@ResponseBody@Autowired 都可以说

@EnableAutoConfiguration使用@Import添加了一个AutoConfigurationImportSelector类,Spring自动注入配置的核心功能就依赖于这个对象
在这个类中,提供了一个getCandidateConfigurations()方法用来加载配置文件。借助Spring提供的工具类SpringFactoriesloadFactoryNames()方法加载配置文件。扫描的默认路径位于META-INF/spring.factories中

@Autowired@Resource区别:
相同点:都是用来自动装配bean,一个接口只有一个实现类时基本相同
不同点:
	@resource是java提供的,@AutowireSpring提供的
	@Resource有name和type两个属性,都不配置时默认使用name,配置哪个用哪个,都配置用name
	@Autowired只能用type来装载bean,如果区分不开,需要协助使用@Qualifier("xxx")@Primary
	如果这个Bean有多个候选者,假如其中一个候选者具有@Primary注解修饰,该候选者会被选中,作为自动装配的bean

在这里插入图片描述
在这里插入图片描述

// SpringBoot 是如何启动 Tomcat 的
1.首先,SpringBoot 在启动时会先创建一个 Spring 容器
2.在创建 Spring 容器过程中,会利用@ConditionalOnClass 技术来判断当前 classpath 中是否存在 Tomcat 依赖,如果存在则会生成一个启动 TomcatBean
3.Spring容器创建完之后,就会获取启动TomcatBean ,并创建 Tomcat 对象,并绑定端口等,然后启动 Tomcat
// Mybatis的优缺点
优点:
1.基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响, SQL 单独写,解除 sql 与程序代码的耦合,便于统一管理。
2.与 JDBC 相比,减少了50%以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接;
3.很好的与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,所以只要 JDBC 支持的数据库 MyBatis 都支持).
4.能够与 Spring 很好的集成;
5.提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

缺点:
1.SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写 SQL 语句的功底有一定要求。
2.SQL 语句依赖于数据库,导致数据库移拍性差,不能随意更换数据库
// Mybatis 中#{}和${}的区别是什么?
1.#{}是预编译处理、是占位符,${}是字符串替换、是拼接符
2. Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用 PreparedStatement 来赋值
3. Mybatis 在处理${}时,就是把${}替换成变量的值,调用 Statement 来赋值
4.使用#{}可以有效的防止 SQL 注入,提高系统安全性。
// 不适合创建索引的字段
1.更新频繁的字段
2.区分度很低,男/3.查询很少涉及的列
4.text、image、bit数据类型不要建立索引 
// 事务的基本特性和隔离级别
ACID:
A(atomicity):原子性
C(consistency):一致性
I(isolation):隔离性
D(durability):持久性

隔离级别:
读未提交:存在脏读
读已提交:存在不可重复读
可重复读:存在幻读,MySQL默认隔离级别,在此隔离级别下,解决幻读除了需要锁定所有扫描到的记录行外,还需要锁定行之间的间隙,也就是通过间隙锁来解决幻读的问题
串行:

脏读:读到其他事务未提交的数据
不可重复读:同一事务两次读取到的数据不一致
幻读:查到了其他事务insert进来的数据
// MyISAM和InnoDB区别
1. InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
2. InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;  
3. InnoDB 是聚集索引,MyISAM 是非聚集索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
4. InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
5. InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
// 索引覆盖
聚簇索引(聚集索引、主键索引)和辅助索引(二级索引)

回表:我们可以通过二级索引找到B+树中的叶子结点,但是二级索引的叶子节点的内容并不全,只有索引列的值和主键值。我们需要拿着主键值再去聚簇索引(主键索引)的叶子节点中去拿到完整的用户记录

索引覆盖:我们把索引中已经包含了所有需要读取的列数据的查询方式称为索引覆盖
如:SELECT id, name,  phone FROM user_innodb WHERE name = "蝉沐风";
如果我为name和phone字段创建了一个联合索引,就不需要回表查询

索引下推:
以name和phone的联合索引为例,我们要查询所有name为「蝉沐风」,并且手机尾号为6606的记录
SELECT * FROM user_innodb WHERE name = "蝉沐风" AND phone LIKE "%6606";
流程:
1.InnoDB使用联合索引查出所有name为蝉沐风的二级索引数据,得到10000个主键值
2.拿到主键索引进行回表查询完整的用户记录
3.10000条用户记录返回给MySQLServer层,在Server层过滤出尾号为6606的用户

索引下推怎么处理:索引条件下推(Index Condition PushdownICP)
第一步已经通过name = "蝉沐风"在联合索引的叶子节点中找到了符合条件的10000条记录,而且phone字段也恰好在联合索引的叶子节点的记录中。
这个时候可以直接在联合索引的叶子节点中进行遍历,筛选出尾号为6606的记录
使用ICP的方式能有效减少回表的次数
ICP是默认开启的,对于二级索引,只要能把条件甩给下面的存储引擎,存储引擎就会进行过滤,不需要我们干预

如何避免索引失效:
1.最左前缀原则
2.like以通配符开头、OR连接
3.对列进行操作(函数、表达式、类型转换)

最左前缀原则:
当创建(a,b,c)联合索引时,索引只能使用 a和ab、ac和abc四种
mysql>SELECT `a`,`b`,`c` FROM A WHERE `a`='a1' ; //索引生效
mysql>SELECT `a`,`b`,`c` FROM A WHERE `b`='b2' AND `c`='c2'; //索引失效
mysql>SELECT `a`,`b`,`c` FROM A WHERE `a`='a3' AND `c`='c3'; //索引生效,但实际上只使用了索引a

在这里插入图片描述
在这里插入图片描述

// Innodb是如何实现事务的
innodb 通过buffer pool, logBuffer, Redo Log, Undo Log 来实现事务的, 以update语句举例:
innodb 在收到一个update语句后, 会先根据条件找到数据所在的页, 并将该页缓存在buffer pool中
执行update语句, 修改buffer pool中的数据, 即内存中的数据

针对update语句生成一个redo Log对象, 并存入LogBuffer中
针对update语句生成undo Log日志, 用于事务回滚

如果事务提交, 那么则把redo Log对象进行持久化, 后续还有其他机制将buffer pool中修改的数据页持久化到磁盘中
如果事务回滚, 则利用undo log 日志进行回滚
// B树和B+树区别,为什么MySQL使用B+树
B 树的特点:多路平衡查找树
1.节点排序
2.一个节点(默认16kb)可以存多个元素,多个元素也排序了

B+树的特点:
1.拥有B树的特点
2.叶子节点之间有指针(利于范围查找)
3.非叶子节点上的元素在叶子节点上都冗余了,也就是叶子节点中存储了所有的元素,并且排好顺序

Mysql 索引使用的是 B+树,因为索引是用来加快查询的,而 B+树通过对数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得 B+树的高度不会太高,在 Mysql 中一个 Innodb 页就是一个 B+树节点,一个Innodb 页默认16kb,所以一般情况下一颗两层的B+树可以存2000万行左右的数据,然后通过利用 B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL语句

B树:
B树
B+树:
在这里插入图片描述

// Mysql 锁有哪些,如何理解
按锁粒度分类:
1.行锁:锁某行数据,锁粒度最小,并发度高
2.表锁:锁整张表,锁粒度最大,并发度低
3.间隙锁:锁的是一个区间

还可以分为:
1.共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写
2.排它锁:也就是写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写

还可以分为:
1.乐观锁:并不会真正的去锁某行记录,而是通过一个版本号来实现的
2.悲观锁:上面说的行锁、表锁等都是悲观锁在事务的隔离级别实现中,就需要利用所来解决幻读
// 什么是RDB和AOF
RDBRedis DataBase)持久化是把当前Redis中全部数据生成快照保存在硬盘上。RDB持久化可以手动触发,也可以自动触发
fork一个子进程将数据集写入临时文件,待操作成功后才会用这个临时文件替换掉上一次的备份
手动执行SAVEBGSAVE命令生成快照
生产环境周期生成快照:
# 默认的设置为:
save 900 1 // 如果900秒内有1条Key信息发生变化,则进行快照
save 300 10 // 如果300秒内有10条Key信息发生变化,则进行快照
save 60 10000 // 如果60秒内有10000条Key信息发生变化,则进行快照

缺点:
1.数据安全性低,隔断时间进行快照,无法做到实时持久化,或者秒级持久化

AOF(Append Only File),以日志的形式记录每一个写、删除操作,查询操作不会记录
缺点:
1.AOF文件比RDB文件大,且恢复速度慢

AOFRDB更安全,优先使用AOF恢复数据
// Redis过期键的删除策略
惰性删除:访问时候判断是否已过期,过期则删除,优点节省CPU资源,缺点占用内存
定期删除:每隔一段时间扫描一定数量的key,并清除已过期的key
// Redis事务
Redis事务是指将多条命令加入队列,一次批量执行多条命令,每条命令会按顺序执行
第一阶段:开始事务
第二阶段:命令入队
第三阶段:执行事务

WATCH:监视Key改变,用于实现乐观锁。如果监视的Key的值改变,事务最终会执行失败。
UNWATCH:放弃监视

MULTI:标识一个事务的开启,即开启事务;
EXEC:执行事务中的所有命令,即提交;
DISCARD:放弃事务;和回滚不一样,Redis事务不支持回滚。


取消监视:
事务执行之后,不管是否执行成功还好是失败,都会取消对应的监视;
当监视的客户端断开连接时,也会取消监视;
可以手动UNWATCH取消所有Key的监视;

在这里插入图片描述

// redis主从复制的核心原理
通过slaveof配置服务器去复制另一个服务器的数据,从数据库一般只读,并同步主库的数据
当主从服务器刚建立连接的时候,进行全量同步;全量复制结束后,进行增量复制。当然,如果有需要,slave在任何时候都可以发起全量同步

主从复制的好处:
	1.可以轻易地实现横向扩展,实现读写分离,一个 master 用于写,多个 slave 用于分摊读的压力,从而实现高并发
	2.实现了数据的热备份,是持久化之外的一种数据冗余方式
	3.如果master宕掉了,使用哨兵模式,可以提升一个 slave 作为新的 master,进而实现故障转移,实现高可用

主从复制步骤:先全量复制->部分复制
1.slave服务器连接到master服务器,发送psync命令请求同步数据
2.master服务器收到psync命令之后,开始执行bgsave命令生成RDB快照文件并使用缓存区记录此后执行的所有写命令
3.master服务器bgsave命令执行完后向所有Slava服务器发送快照文件,并在发送期间继续在缓冲区内记录被执行的写命令
4.master服务器发送完RDB快照文件,接着发缓冲区的命令,slave完成对快照的载入,接受命令请求,并执行来自主服务器缓冲区的写命令

部分复制:主从节点分别会维护一个复制偏移量offset
部分复制过程中,如果master挂了,一个slave会变成master,runid会变化,只能换成全量同步

在这里插入图片描述

// redis数据结构和应用场景
1.String
2.Hash
3.list
4.set
5.Zset
参考:https://baijiahao.baidu.com/s?id=1730716661153081344&wfr=spider&for=pc
Redis实现分布式锁:
实际项目开发中,Redisson,它既提供了Redis的基本命令的封装,也提供了Redis分布式锁的封装

基础版:分布式锁实现 setnx key value
存在问题:没有给key设置过期时间,万一程序在发送delete命令释放锁之前宕机了,那么这个key就会永久的存储在Redis中了,其他客户端也永远获取不到这把锁了
在这里插入图片描述
升级版本:设置key的过期时间 set key value ex seconds nx
存在问题:存在锁误删的情况
在这里插入图片描述
在这里插入图片描述
升级版本2:value使用唯一值,删除锁时判断value是否当前线程
把value设置成一个唯一值,每个线程的value都不一样,在删除key之前,先通过get key命令得到value,防止误删(还是存在误删情况)
缺点:还是存在误删情况
在这里插入图片描述
在这里插入图片描述
终极版:Lua脚本
Lua脚本原子性执行(get key value、判断value是否属于当前线程、删除锁)
在这里插入图片描述

// redis集群策略
1.主从模式
主库进行读写,并且会和从库进行数据同步,客户端可以连接主库或从库,但是主库或从库宕机后,客户端需要手动修改IP
缺点:
不高可用:宕机后需要手动修改IP、
容量受限:难扩容、存储容量受限于某台机器的内存容量不支持特大数据量
2.哨兵模式
在主从的基础上增加哨兵节点,
哨兵作用:可以发现主库是否宕机,如果宕机可以在从库中选择一个库作为主库,另外哨兵可以做成集群,解决了高可用问题,但是容量受限问题依旧存在
3.Cluster模式
用得比较多的一种模式,与MySQL分库分表类似

总结:Redis数据量不大可以选择哨兵模式,Redis存的数据量大,选择cluster模式

在这里插入图片描述

// 缓存雪崩、缓存击穿、缓存穿透
缓存雪崩:大批热点数据同时过期,导致大量请求直接访问MySQL,解决办法:过期时间上增加一点随机值,确保不在同一时间大量数据同时过期
缓存击穿:和缓存雪崩类似,缓存击穿指某一个热点key突然失效导致大量请求访问MySQL,解决办法:考虑这个热点key不设置过期时间
缓存穿透:某些key在Redis中不存在,如黑客大量伪造这种key进行访问,就会直接访问到MySQL,解决办法:布隆过滤器

布隆过滤器:
参考:https://blog.csdn.net/qq_55624813/article/details/121316520

在这里插入图片描述
在这里插入图片描述

// redis和MySQL如何保证数据一致性
方案一:先删除Redis的数据,然后更新MySQL,在高并发下任然存在数据不一致的情况,比如:线程1删除Redis数据后,正要更新MySQL,此时线程2进行查询然后又把未更新的MySQL老数据加载到Redis了
方案二:延时双删,先删除Redis缓存数据,再更新MySQL,延迟几百毫秒再删除Redis缓存数据,这样就算在更新MySQL时,有其他线程把MySQL的老数据读到了Redis中,那么也会被删除掉,从而保证数据一致
// redis单线程为啥这么快
为啥单线程:Redis只在内存中操作,如果是多线程,会有上下文切换,更耗时
java中为啥要用多线程:因为还涉及IO操作,某线程进行IO处理的时候可以将CPU让给其他线程

1.内存操作:CPU读取内存的速度要比读取磁盘的速度快得多

2.高效的底层数据结构:

3.多路复用IO模型
阻塞IO
非阻塞IO
非阻塞多路IO复用

4.单线程避免多线程切换开销

高效的底层数据结构

// CAP理论 BASE理论
CAP理论:分布式系统的三个指标,一个分布式系统,不可能同时做到这三点
Consistency(一致性)
Availability(可用性)
Partition tolerance(分区容忍性)

BASE理论:Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)
// 分布式ID,有哪些解决方案
在单体架构中,用数据库自增ID或在内存中维护一个自增ID都可以
分布式架构:
	1.UUID,复杂度最低,但是会影响存储空间和性能
	2.利用单机数据库自增主键作为分布式ID生成器,复杂度适中,较UUID更短,但并发大时不是最优方案
	3.使用 Redis 的自增原子性来生成唯一 id,相较用MySQL生成ID性能有所提高
	4.雪花算法(SnowFlake 算法)	
		优点:
		高并发分布式环境下生成不重复 id,每秒可生成百万个不重复 id。
		基于时间戳,以及同一时间戳下序列号自增,基本保证 id 有序递增。
		不依赖第三方库或者中间件。
		算法简单,在内存中进行,效率高

在这里插入图片描述

雪花算法

// 分布式锁的使用场景,有哪些实现方案
在单体架构中,多个线程都是属于同一个进程的,所以在线程并发执行时,遇到资源竞争时,可以利用 ReentrantLocksynchronized 等技术来作为锁,来控制共享资源的使用。而在分布式架构中,多个线程是可能处于不同进程中的,而这些线程并发执行遇到资源竞争时,利用 ReentrantLocksynchronized 等技术是没办法来控制多个进程中的线程的,所以需要分布式锁,意思就是,需要一个分布式锁生成器,分布式系统中的应用程序都可以来使用这个生成器所提供的锁,从而达到多个进程中的线程使用同一把锁。目前主流的分布式锁的实现方案有两种:

zookeeper :利用的是 zookeeper 的临时节点、顺序书点、 watch 机制来实现的, zookeeper 分布式锁的特点是高一致性,因为 zookeeper 保证的是 CP ,所以由它实现的分布式锁更可靠,不会出现混乱

redis :利用 redis 的 setnx 、 lua 脚本、消费订阅等机制来实现的, redis 分布式锁的特点是高可用,因为 redis 保证的是 AP ,所以由它实现的分布式锁可能不可靠,不稳定(一旦 redis 中的数据出现了不一致),可能会出现多个客户端同时加到锁的情况
// Dubbo支持哪些负载均衡策略
1.随机:从多个服务提供者随机选择一个来处理本次请求,调用量越大则分布越均匀,并支持按权重设置随机概率
2.轮询:依次选择服务提供者来处理请求,并支持按权重进行轮询,底层采用的是平滑加权轮询算法
3.最小活跃调用数:统计服务提供者当前正在处理的请求,下次请求过来则交给活跃数最小的服务器来处理
4.一致性哈希:相同参数的请求总是发到同一个服务提供者
// Dubbo 是如何完成服务导出的?
1.首先 Dubbo 会将程序员所使用的@DubboService 注解 或@Service 注解进行解析得到程序员所定义的服务参数,包括定义的服务名、服务接口、服务超时时间、服务协议等等,得到一个 ServiceBean2.然后调用 ServiceBean 的 export 方法进行服务导出
3.然后将服务信息注册到注册中心,如果有多个协议,多个注册中心,那就将服务按单个协议,单个注册中心进行注册
4.将服务信息注册到注册中心后,还会绑定一些监听器,监听动态配置中心的变更
5.还会根据服务协议启动对应的 Web 服务器或网络框架,比如 TomcatNetty
// 分布式架构下,Session共享有什么方案

cookie的内容主要包括:名字,值,过期时间,路径和域。路径与域合在一起就构成了cookie的作用范围。
如果不设置过期时间,则表示这个cookie的生命期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了,这种生命期为浏览器会话期的 cookie被称为会话cookie。会话cookie一般不存储在硬盘上而是保存在内存里。如果设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直到超过设定的过期时间

我们可以从很多网站的登录界面中看到“请记住我”这样的选项,如果你勾选了它之后再登录,那么在下一次访问该网站的时候就不需要进行重复而繁琐的登录动作了,而这个功能就是通过Cookie实现的

当客户端第一次请求服务端,当server端程序调用 HttpServletRequest.getSession(true)这样的语句时的时候,服务器会为客户端创建一个session,并将通过特殊算法算出一个session的ID,用来标识该session对象
客户端只保存sessionid到cookie中,而不会保存session

浏览器的关闭并不会导致Session的删除,只有当超时、程序调用HttpSession.invalidate()以及服务端程序关闭才会删除。
CookieSession的关系:
cookie和session的方案虽然分别属于客户端和服务端,但是服务端的session的实现对客户端的cookie有依赖关系的,服务端执行session机制时候会生成session的id值,这个id值会发送给客户端,客户端每次请求都会把这个id值放到http请求的头部发送给服务端,而这个id值在客户端会保存下来,保存的容器就是cookie,因此当我们完全禁掉浏览器的cookie的时候,服务端的session也会不能正常使用

分布式下Session共享方案:
把 Redis存储在Redis中,虽然架构复杂,好处:
1.实现Session共享
2.服务器重启Session不丢失
3.不仅可以跨服务器session共享,也可以跨平台session共享(网页、APP共享)
// 如何实现接口的幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用
在增删改查 4 个操作中,尤为注意就是insert和update
select 是天然的幂等操作
delete 删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回 0,删除的数据多条,返回结果多个,在不考虑返回结果的情况下,删除操作也是具有幂等性的)

update 修改在大多场景下结果一样,但是如果是增量修改是需要保证幂等性的,如下例子:
把表中 id 为 XXX 的记录的 A 字段值设置为 1,这种操作不管执行多少次都是幂等的。
把表中 id 为 XXX 的记录的 A 字段值增加 1,这种操作就不是幂等的

insert 

解决办法:避免重复提交

前端拦截:比如在用户点击完“提交”按钮后,我们可以把按钮设置为不可用或者隐藏状态,避免用户重复点击

唯一ID:新增时,比如查一下iPhone是否已经注册过了

使用乐观锁,version版本是否一致,一致这次操作才有效

token:一次申请,一次性使用
1.服务端提供了获取token的接口。如果业务是存在幂等问题的,就在执行业务前,先去获取token,服务器会把token保存到Redis2.然后调用业务接口请求时,把token携带上
3.服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务
4.如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给客户端, 这样就保证了业务代码不被重复执行
// 存储拆分后如何解决主键唯一: 分布式下如何确保主键唯一
1.UUID:简单,性能好,没有顺序,存在泄漏Mac地址的风险
2.单数据库主键:存在性能瓶颈
3.Redis,zk
4.雪花算法
// 缓存淘汰算法
1.FIFO(First In First Out, 先进先出),根据缓存被存储的时间,离当前时间最久的数据被淘汰
2.LRU(Least Recently Used, 最近最少使用),离当前时间最久的数据被淘汰
3.LFU(Least Frequently Used, 最近频次最少),根据数据的历史访问频率来淘汰数据
// 分布式寻址问题
hash算法(key)%节点个数=在哪个节点上
缺点:扩展节点或减少分片,所有数据需要重新计算分片存储位置

// redis Cluster hash槽原理
hash(key) -> hash slot(hash槽) -> 服务器节点
新增服务器节点,把部分hash槽让给新节点进行管理,只需迁移hash slot槽的对应数据到新增节点
// 服务雪崩,服务限流
服务雪崩:当服务 A 调用服务 B ,服务 B 调用 C ,此时大量请求突然请求服务 A ,假如服务 A 本身能抗住这些请求,但是如果服务 C 抗不住,导致服务 C 请求堆积,从而服务 B 请求堆积,从而服务 A 不可用,这就是服务雪崩,解决方式就是服务降级和服务熔断

服务限流:服务限流是指在高并发请求下,为了保护系统,可以对访问服务的请求进行数量上的限制,从而防止系统不被大量请求压垮,在秒杀中,限流是非常重要的

// 服务降级、服务熔断
服务熔断:是指当服务 A 调用的某个服务B不可用时,上游服务 A 为了保证自己不受影响,从而不再调用服务 B,直接返回一个结果,减轻服务 A 和服务 B 的压力,直到服务 B 恢复

服务降级:是指当发现系统压力过载时,可以通过关闭某个服务,或限流某个服务来减轻系统压力,这就是服务降级
// 什么是中台
将各个业务线中可以复用的一些功能抽取出来,剥离个性,提取共性,形成可复用的组件
业务中台、数据中台、技术中台
// 消息队列如何保证消息可靠性
confirm机制确保生产者发送给broker
关闭自动ack,消费者确保消费完消息后给broker发送一个ack,broker接受到ack后删除消息
// 死信队列、延时队列
1.死信队列也是一个消息队列,它是用来存放那些没有成功消费的消息的,通常可以用来作为消息重试
2.延时队列就是用来存放需要在指定时间被处理的元素的队列,通常可以用来处理一些具有过期性操作的业务,比如十分钟内未支付则取消订单
// TCP三次握手四次挥手
TCP 协议是7层网络协议中的传输层协议,负责数据的可靠传输。在建立 TCP 连接时,需要通过三次握手来建立,过程是:
1.客户端向服务端发送一个 SYN
2.服务端接收到 SYN 后,给客户端发送一个 SYN _ ACK
3.客户端接收到 SYN _ ACK 后,再给服务端发送一个 ACK 在断开 TCP 连接时,需要通过

四次挥手来断开,过程是:
1.客户端向服务端发送 FIN
2.服务端接收 FIN 后,向客户端发送 ACK ,表示我接收到了断开连接的请求,客户端你可以不发数据了,不过服务端这边可能还有数据正在处理
3.服务端处理完所有数据后,向客户端发送 FIN ,表示服务端现在可以断开连接
4.客户端收到服务端的 FIN ,向服务端发送 ACK ,表示客户端也会断开连接了

三次握手
四次挥手

// 浏览器发出一个请求到收到相应经历了哪些?
1.浏览器解析用户输入的 URL ,生成一个 HTTP 格式的请求
2.先根据 URL 域名从本地 hosts 文件查找是否有映射 IP ,如果没有就将域名发送给电脑所配置的 DNS 进行域名解析,得到 IP 地址
3.浏览器通过操作系统将请求通过四层网络协议发送出去
4.途中可能会经过各种路由器、交换机,最终到达服务器
5.服务器搜到请求后,根据请求所指定的端口,将请求传递给绑定了该端口的应用程序,比如8080被 tomcat 占用了
6. tomcat 接收到请求数据后,按照 http 协议的格式进行解析,解析得到所要访问的 servlet 
7.然后 servlet 来处理这个请求,如果是 SpringMVC 中的 DispatchServlet ,那么则会找到对应的 Controller 中的方法,并执行该方法得到结果
8. Tomcat 得到响应结果后封装成 HTTP 响应的格式,并再次通过网络发送给浏览器所在的服务器
9.浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负责解析并渲染
// 跨域请求,有什么问题,怎么解决
跨域是指浏览器在发起请求时,会检查该请求所对应的协议,域名,端口和当前网页是否一致,如果不一致,则浏览器会进行限制,比如在百度的某个网页中,如果使用ajax去访问京东是不行的,但是如果是img,iframe,script等标签的src属性去访问则是可以的,之所以浏览器要做这层限制,是为了用户信息安全,但是如果开发者要想绕过这层限制也是可以的。

1.response添加header,比如resp.setHeader(“Access-Control-Allow-Origin”,“*”);表示可以访问所有网站,不受是否同源的限制。
2.jsonp的方式,该技术底层就是急于script标签来实现的。因为script标签是可以跨域的。
3.后台自己控制,先访问同域名下的接口,然后在接口中再去使用httpClient等工具去调用目标接口。
4.网关,和第三种方式类似,都是交给后台服务来进行跨域访问。
// 零拷贝

// volatile
volatile的主要作用是保证可见性以及有序性
可见性:假如一个线程A修改一个共享变量flag之后,则线程B去读取,一定能读取到最新修改的flag

有序性:volatile的禁止指令重排 指令重排
CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题

内存屏障
// 压测工具
jcstress
// JVM
类加载器:
双亲委派指的是jvm在加载类时,会委派给ExtensionClassLoaderBootstrapClassLoader进行加载,如果没加载到才用ApplicationClassLoader 进行加载
堆、方法区、栈、程序计算器
javap -c xxx.class 对其进行反编译

// 堆、栈、方法区主要存储类的哪些元素?他们之间的联系是什么?本地方法栈?
堆区:
存储的全部是对象实例,每个对象都包含一个与之对应的class的信息(class信息存放在方法区)。
jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身,几乎所有的对象实例和数组都在堆中分配。

方法区:
又叫静态区,跟堆一样,被所有的线程共享。它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

栈区:
每个线程包含一个栈区,栈中只保存方法中(不包括对象的成员变量)的基础数据类型和自定义对象的引用(不是对象),对象都存放在堆区中
每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

本地方法栈:给native修饰的方法分配空间

JVM优化主要是对堆进行优化:调优工具arthas

排查问题:
dashboard
thread xxx 可直接定位到问题代码位置
thread-b 可排查死锁的问题

调优的目的是减少STW次数(Stop The World),主要是减少full gc次数
-Xms:初始堆大小
-Xmx:最大堆大小
-Xmn:新生代大小
-XX:NewSize : 设置Yong Generation的初始值大小
-XX:MaxNewSize:设置Yong Generation的最大值大小
-XX:NewRatio: 默认值为2,表示Old GenerationYong Generation2倍,即Yong Generation占据内存的1/3
-XX:SurviorRatio : 默认值为8,即EdenTo(S2)的比例是8,(FromTo是一样大的各占1),此时Eden占据Yong Generation8/10
-XX:InitialTenuringThreshol : 设置晋升到老年代的对象年龄的最小值,默认为7
-XX:MaxTenuringThreshold : 设置晋升到老年代的对象年龄的最大值
-XX:MetaspaceSize-XX:MaxMetaspaceSize:分别设置元空间最小大小与最大大小

在这里插入图片描述
在这里插入图片描述

// Redis
并发超卖问题:Redisson框架实战

重复提交问题:
前端限制重复提交
token + Redis锁,存在两个页面还是可以重复提交
订单号+Redis

支付订单、取消订单在高并发情况下问题:

Redis 缓存架构:
先查Redis,没有去数据库查,查出来再放Redis中,同时设置一个过期时间

缓存雪崩:批量过期,设置过期时间加个随机值
缓存击穿:一个热点过期,
	
缓存穿透:黑客伪造Redis不存在的key,MySQL也不存在
	如果数据库返回空值,也把空值缓存在Redis中,并设置一个过期时间
	布隆过滤器现在某些值的查询
java三大特性:封装、继承、多态
// 设计模式及其应用场景
单例模式:
工厂模式:
策略模式:
模板模式:
// 饿汉式实现单例模式
饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题
public class HungrySingleton {
	private static final HungrySingleton hungrySingleton = new HungrySingleton();
	/**
	 * 私有构造
	 */
	private HungrySingleton() {}
	
	//取实例方法
	public static HungrySingleton getInstance() {
		return hungrySingleton;
	}
}

// 工厂方法模式:缺点:没增加一个产品就要增加一个产品类和一个对应的具体工厂类,增加了系统的复杂度
参考:https://www.runoob.com/design-pattern/factory-pattern.html
// Spring中使用了哪些设计模式
单例:Spring依赖注入Bean实例默认是单例模式
工厂模式:beanfactory
代理模式:AOP底层使用动态代理模式实现
... 
// Elasticsearch是什么
非关系型文档数据库,基于Lucene的一款开源的搜索、聚合分析、存储引擎

// es中拼写纠错是如何实现的:
fuzziness: (0,1,2) 并不是越大越好,召回率高但是结果不准确
距离算法:Damerau-Levenshtein

// 分片
实现高可用
提高吞能力

线程的状态
在这里插入图片描述

如何停止线程
interrupt

java中sleep和wait的区别:
sleep属于Thread类中的static方法,wait属于Object方法
sleep属于TIMED_WATING,自动被唤醒,wait属于WATING,需要手动唤醒
sleep在持有锁执行,不会释放锁资源,wait会释放锁资源
wait方法必须在只有锁时才能执行:将owner扔到WaitSet集合中,这个操作是修改ObjectMonitor对象

// 分布式事务

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值