八股文复习

二分查找复习

在这里插入图片描述

public class BinarySearch {
    public static void main(String[] args) {
    	//二分查找适用于有序查询,无序需要先排序
        int [] array = {1,3,5,14,17,67,99,122};
        int target = 1;
        int res = binarySearch(array, target);
        System.out.println(res);
    }

    public static int binarySearch(int []arr, int target) {
        int l=0, r =arr.length - 1, m;
        //l > r 遍历结束
        while (l <= r) {
            //获取中间索引
            m = (l + r) / 2;
            //判断中间值是否等于目标
            if (arr[m] == target) {
                return m;
            } else if (arr[m] > target) {
                //中间值大于目标值,需要遍历的数据 0 - 中间值
                r = m - 1;
            } else if (arr[m] < target) {
                //中间值小于目标值,需要遍历的数据中间值 - 结束值
                l = m + 1;
            }
        }
        return -1;
    }
}

冒泡排序(稳定排序算法)

集合有序度高的情况下冒泡排序优于选择排序
在这里插入图片描述

import java.util.Arrays;

public class BubbleSort {
    public static void main(String[] args) {
        int [] array = {4,5,1,56,54,100};
//        bubble(array);
//        System.out.println(Arrays.toString(array));
        bubble_v2(array);
    }

    public static void bubble(int [] a) {
        //遍历每一轮
        for (int j = 0; j < a.length - 1; j++) {
            //每一轮冒泡排序找最大值放到最后
            for (int i = 0; i < a.length - 1 - j; i++) {// -j 的作用是优化一下,最大的值找到了就不用再进行比较了
                if (a[i] > a[i+1]) {
                    swap(a, i, i+1);
                }
            }
        }
    }

    public static void swap(int []a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

    public static void bubble_v2(int [] b) {
        int n = b.length - 1;
        while (true) {
            int last = 0;//表示最后一次交换索引位置
            for (int i = 0; i < n; i++) {//可以通过本次计算错音交换次数来判断下一次需不需要进行冒泡
                if (b[i] > b[i + 1]) {
                    swap(b, i, i + 1);
                    last = i;
                }
            }
            n = last;
            System.out.println(Arrays.toString(b));
            if (n == 0) {
                break;
            }
        }
    }
}


选择排序(不稳定排序算法)

在这里插入图片描述

import java.util.Arrays;

public class SelectionSort {
    public static void main(String[] args) {
        int [] array = {4,5,1,56,54,100};
        selection(array);//选择排序就是每一轮找最小的值放入最小或次小的位置,以此类推
    }
    private static void selection(int []a) {
        //寻找前五个元素放入有序的位置,最后一个就不用选择了(一共六个元素)
        for (int i = 0; i < a.length - 1; i++) {
            int s = i;// 每一次最小元素的索引
            for (int j = s + 1; j < a.length; j++) {//遍历找最小值并且交换索引
                if (a[s] > a[j]) {
                    s = j;
                }
            }
            if (s != i) {
                swap(a, s, i);
            }
        }
        System.out.println(Arrays.toString(a));
    }

    public static void swap(int []a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

插入排序(稳定排序算法)

有序度越高的数据效率高
在这里插入图片描述

import java.util.Arrays;

public class InsertSort {
    public static void main(String[] args) {
        int [] a = {1,45,34,22,66,955,11};
        insertSort(a);
        System.out.println(Arrays.toString(a));
    }

    public static void insertSort(int [] a) {
        //i 代表需要插入的索引下标
        for (int i = 1; i < a.length; i++) {
            int temp = a[i];//定义需要插入的数据
            int j = i - 1;//代表已排序区域的最后一个索引
            while (j >= 0 && temp < a[j]) {//判断要插入的数据是否小于有序数据的最后一个数据,并且遍历完成
                a[j+1] = a[j];//当前引用指向后一个地址,后移操作
                System.out.println(Arrays.toString(a));
                /**
                 * [1, 45, 45, 22, 66, 955, 11]
                 * [1, 34, 45, 45, 66, 955, 11]
                 */
                j--;//终止遍历有序数据(要插入数据的前面的数据)
            }
            a[j+1] = temp;//后移最后一个有序数据之后,把要插入的数据补位
        }
    }
}


希尔排序

插入排序基础上改进,插入排序有一个缺点就是当最大的值排在最前,想排序就要给他移动到最后,就要移动array.length次,希尔排序在此基础之上使用了按照一定的索引间距进行排序,也就是例如八个数据第一个、第四个、第八个先插入排序,以此类推进行
在这里插入图片描述

public class ShellSort {
    //核心代码---开始
    public static void sort(Comparable[] arr) {
        int j;
        for (int gap = arr.length / 2; gap >  0; gap /= 2) {
            for (int i = gap; i < arr.length; i++) {
                Comparable tmp = arr[i];
                for (j = i; j >= gap && tmp.compareTo(arr[j - gap]) < 0; j -= gap) {
                    arr[j] = arr[j - gap];
                }
                arr[j] = tmp;
            }
        }
    }
    //核心代码---结束
    public static void main(String[] args) {

        int N = 2000;
        Integer[] arr = {9,324,33,5,1,7,234,2,3,4};
        ShellSort.sort(arr);
        for( int i = 0 ; i < arr.length ; i ++ ){
            System.out.print(arr[i]);
            System.out.print(' ');
        }
    }
}

快速排序(单边循环)

在这里插入图片描述

 选择最右元素作为基准点
 j指针负责找比基准点小的元素,一旦找到则与i交换
 i指针维护小于基准点元素的边界,是每次交换的目标索引
 最后基准点与i交换,i就是分区位置
import java.util.Arrays;

public class QuickSort_v1 {
    public static void main(String[] args) {
        int [] a = {5,3,7,2,9,8,1,4};
        quick(a, 0, a.length - 1);
        System.out.println(Arrays.toString(a));
    }

    private static void quick(int [] a, int l, int h) {
        if (l >= h) {
            return;
        }
        int i = quickSort(a, l, h); //得到第一次分区的基准点,后续分区根据此基准点进行分区
        quick(a, l, i -1);//基准点左侧分区
        quick(a, i + 1 , h);
    }

    private static int quickSort(int [] a, int l, int h) {
        //返回int是为了下一轮分区边界
        int pv = a[h];//基准点值
        int i = l;
        for (int j = l; j < h; j++) {
            if (a[j] < pv) {
                swap(a, i, j);
                i++;
            }
        }
        //代码走到这里 结果为32159874
        swap(a, i, h);
        //基准点交换 结果为32149875
        System.out.println(Arrays.toString(a));
        return i;
    }

    public static void swap(int []a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

快速排序(双边循环)

与单边循环不同 单边是 j去寻找比基准点小的元素,然后再去寻找比基准点大的元素,并且基准点以最右元素作为基准点
双边是以最左元素作为基准点,j指针负责从右向左寻找比基准点小的元素,i负责从左向右寻找比基准点大的元素一旦找到二者交换,最后基准点与i或j交换(i此时 == j)

import java.util.Arrays;

public class QuickSort_v2 {

    public static void main(String[] args) {
        int [] a = {5,3,7,2,9,8,1,4};
        quick(a, 0, a.length - 1);
        System.out.println(Arrays.toString(a));
    }

    private static void quick(int [] a, int l, int h) {
        if (l >= h) {
            return;
        }
        int i = quickSort(a, l, h); //得到第一次分区的基准点,后续分区根据此基准点进行分区
        quick(a, l, i -1);//基准点左侧分区
        quick(a, i + 1 , h);
    }

    private static int quickSort(int [] a, int l, int h) {
        int pv = a[l];
        int i = l;
        int j = h;
        while (i < j) {
            //j从右向左找比基准点小的数据
            while(i < j && a[j] > pv) {//必须先从右向左找
                j--;
            }
            //i从左向右找比基准点大的数据
            while(i < j && a[i] <= pv) {
                i++;
            }
            //交换i与j的数据
            swap(a, i, j);
        }
        swap(a, l, j);
        return i;
    }

    public static void swap(int []a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

归并排序

在这里插入图片描述

ArrayList

扩容机制

1.arraylist初始化的时候无参构造方法初始大小是0

 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

2.当我们add元素的时候或初始化大小为10,添加第11个元素的时候会产生扩容,大小为原来的1.5倍,会把就数组中的十个元素copy到新数组,再把第十一个元素add进新数组,旧的数组没有被引用会被下一次垃圾回收回收掉

3.addAll扩容有些许不同,他是取得下一次扩容与元素个数进行比较,去较大的值作为扩容结果

迭代器Iterator

FailFast遍历过程中如果存在别人修改集合的情况立即抛出异常警告场景发生于arrayList遍历迭代

private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;//expectedModCount迭代一开始记录修改的次数,modCount是list的修改次数

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }
        。。。。。。。
public E next() {
            checkForComodification();//遍历的时候会判断有没有改变
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
       。。。。。。。。。。
final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

FailSafe遍历过程中存在别人修改集合采用牺牲数据一致性来让整个集合遍历完成场景发生于CopyOnWriteArrayList
允许遍历过程中修改元素原因在于CopyOnWriteArrayList.add是读写分离,读是一个数组,写的时候是另一个数组,也是这个原因产生了FailSafe

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();
        }
    }

LinkedList

与arrayList对比来说
Arraylist:

1.基于数组实现,需要连续内存
2.随机访问快,根据下表访问
3.尾插入删除性能可以,其他不问插入删除都会移动数据,因此性能会低
4.可以利用cpu缓存,局部性原理(cpu读的时候会把数组中相邻的元素读到缓存,下次再读就会节省资源开销)

LinkedList:

1.基于双向链表,无需连续内存
2.随机访问慢,(需要依赖头节点依次.next遍历数据)
3.头尾插入删除性能性能高
4.占用内存多(相比arrayList双向链表就不行了,链表之间的元素间距较大,不能很好的配合cpu缓存)

HashMap

底层数据结构,1.7与1.8不同之处:

1.7是数组+链表
1.8是数组+(链表 | 红黑树)

hashMap存储过程:

假如说存储key为a的数据,首先对a进行hash计算,在进行二次hash(二次hash的作用是为了数据散列的更加均匀),计算桶下表(二次hash值%数组长度),取值的时候一样,计算出桶下标直接取

haashMap何时开始扩容

当我们存储的数据大于桶长度的四分之三后会开始扩容比如说现在桶的长度是16,当元素个数超过12时就会扩容至32,并且所有的数据下标都会重新计算,原本是key%16,扩容后会是key%32

为何要用红黑树,为何一开始不是红黑树(红黑树是一种类平衡二叉树,节点左侧都比本身小,右侧都比本身大,红黑树的叶子节点都是黑色),树化的阈值为何是8,何时会发生树化,何时会退化为链表?

1.链表过长的话会影响hashMap的性能
2.链表短的情况下性能是比红黑树高的,所以一开始不必使用红黑树
3.树化的阈值是8原因是在负载因子0.75的情况下,树化的概率是亿分之几的级别,降低了树化的概率,红黑树如果不是特别需要使用还是优先使用链表
4.链表长度超过阈值8时并且数组长度>=64发生树化
5.退化情况,(1)当树元素个数小于等于6的时候就会退化成为链表(2)remove树节时,若root、root.left\root.right\root.left.left有一个为null也会退化为链表(这个检查实在移除动作之前做检查,注意!!)

为何数组容量是2的n次幂

1.计算索引时,如果是2的n次幂可以使用位与运算22%16 == 22 & (16 - 1),位与运算效率更高
2.如果要好的hash分布性,选择一个质数作为容量,如果要好的效率,选择2的n次幂作为容量

hashMap的put流程,1.7与1.8的区别

1.HashMap是懒惰创建数组的,首次使用才创建数组
2.计算索引(桶下标)
3.如果桶下标还没人占用,创建Node占位返回
4.如果桶下标已经有占用
(1)已经是TreeNode走红黑树的添加或更新逻辑
(2)是普通节点,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
5.返回前检查容量是否超过阈值,一旦超过进行扩容
区别:(1)链表插入节点方式不同,7是头插法,8是尾插法
(2)7是大于等于阈值并且插入是没有空位才会扩容,8是大于阈值就扩容
(3)8在扩容计算Node索引时,会优化

加载因子为什么是0.75f

在空间占用与查询时间之间取得较好的权衡
大于这个值,空间节省了,但是链表就会比较长影响性能
小于这个值,冲突减少了,但是扩容就会更频繁占用空间多

多线程下操作HashMap会出现什么问题

public static void main(String[] args) throws InterruptedException {
        HashMap<String, Object> map = new HashMap<>();
        Thread t1 = new Thread(() -> {
            map.put("a", 12);
        }, "t1");

        Thread t2 = new Thread(() -> {
            map.put("1",123);
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(map);
    }

我们使用两个线程来模拟,在put的时候模拟异常场景,让两个线程都走进不该走进的条件中,最后会发现结果被覆盖{a=12},发现hashMap在多线程的情况下会发生并发丢失数据的现象
在这里插入图片描述

HashMap的key是否可以为null,都有什么要求

HashMap的key可以为null,作为key的对象必须实现hashCode和equals方法,并且key的内容不可更改,平时使用最多的对象就是String,Integer,Long等都是final的

单例模式

单例模式的几种实现方式
1.饿汉式饭做好了要吃就吃不吃也做好了,类初始化就会创建好对象

public class Singleton1 {
    //饿汉式单例模式
    private Singleton1(){
		if(INSTANCE  != null) {//可以防止反序列化或者反射破坏饿汉式单例模式
			throw new Exception("已创建实例")
		}
	};

    private static final Singleton1 INSTANCE = new Singleton1();

    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

2.基于枚举实现的饿汉式单例模式,类初始化就会创建好枚举初始化步骤在代码块中
在这里插入图片描述

public enum Singleton2 {
    INSTANCE;
    Singleton2(){};
    public static Singleton2 getInstance() {
        return INSTANCE;
    }
}

3.懒汉式单例模式,吃饭再去做,一吃一做

//懒汉式单例模式
    private Singleton1(){};

	/**
	* 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的。
	* 禁止指令重排序(防止类初始化的时候代码或者指令重排序:比如构造器与对象赋值指令错乱,也就是顺序颠倒)
	/
    private static volatile Singleton1 INSTANCE = null;
    public static Singleton1 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton1.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton1();
                }
            }
        }
        return INSTANCE;
    }

4.内部类实现单例模式

//内部类实现懒汉式模式
    private Singleton1(){};

    private static volatile Singleton1 INSTANCE = null;

    private static class Holder {
        static Singleton1 INSTANCE = new Singleton1();
    }

    public static Singleton1 getInstance() {
        return INSTANCE;
    }

jdk中看到的单例模式

System中和Runtime、Collections中都有大量的单例模式
部分代码

public class Runtime {
    private static java.lang.Runtime currentRuntime = new java.lang.Runtime();

    public static java.lang.Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {
    }

    public void exit(int status) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkExit(status);
        }
        Shutdown.exit(status);
    }

    public void addShutdownHook(Thread hook) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("shutdownHooks"));
        }
        ApplicationShutdownHooks.add(hook);
    }
}

并发

线程有哪些状态:

java线程分为六种状态
new -新建:初始化一个线程
runnable-可运行 表示当前线程可运行状态
blocked-阻塞 获取锁失败的时候该线程会进入到阻塞状态,获取锁成功后会进入可运行状态
waiting-等待 当一个线程正在运行时,可调用wait()方法使该线程进入到等待状态,前提是获得锁后才可以调用wait(),调用wait()后可等待其他线程调用notify()唤醒该线程,该线程重新去抢锁,抢到锁进入到可运行状态
timed_waiting-超时等待 与等待状态不同的是过的超时时间会自动唤醒,抢到锁后会进入到可运行状态具体可以调用wait(long)或者sleep(long)方法
terminated-终止 结束
在这里插入图片描述

操作系统层面分为5种状态:

这里是引用
线程池的几种状态:
这里是引用

线程池的核心参数:

1.corePoolSize核心线程数目:最多保留的线程数
2.maxImumPoolSize最大线程数目:核心线程 + 救急线程
3.keepAliveTime生存时间:针对救急线程的存活时间
4.unit时间单位:针对救急现成的存活时间单位
5.workQueue阻塞队列:核心线程满了后会放入到队列中
6.threadFactory线程工厂:可以为线程取名字
7.handler拒绝策略:见代码
在这里插入图片描述

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger();
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(2);//队列
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,//核心线程数
                3,//最大线程数
                0,//存活时间
                TimeUnit.MICROSECONDS,//存活单位
                queue,//任务队列
                r -> new Thread(r, "myThread" + atomicInteger.getAndIncrement()),//定义线程名称
                new ThreadPoolExecutor.AbortPolicy()//拒绝策略
        );
        //1,2MyTask会作为核心线程,
        threadPoolExecutor.submit(new MyTask("1", 10000L));
        threadPoolExecutor.submit(new MyTask("2", 10000L));
        //1,2线程正在进行,核心线程数量满了,3,4会放入queue队列中
        threadPoolExecutor.submit(new MyTask("3"));
        threadPoolExecutor.submit(new MyTask("4"));
        //线程5开始执行,发现核心线程数量满了,队列也满了,发现最大线程数是3,核心线程数量是2,这是后可以作为救急线程进行执行,执行完毕后发现队列中有两个线程没有执行,会再把队列中的任务执行,
        //整体执行顺序就是1,2,5,3,4
        //threadPoolExecutor.submit(new MyTask("5"));

        //如果线程5同样睡很久,那么3,4线程会依旧在队列中存放,没人去管他俩,这个时候12345线程都在占用线程池,这个时候再放入执行6线程,队列也满了,核心线程也满了,救急线程也满了,这个时候就会出现救急策略
        /**
         * AbortPolicy(异常策略)#
         * 在遇到拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,所以我会叫他为异常策略,因为他会抛出异常。这个异常你可以捕获起来,然后做你想做的事。
         *
         * DiscardPolicy(丢弃策略)#
         * 这个策略就比较悲伤了,当线程满了之后,后面想进来的线程会被直接丢弃掉,丢弃掉的线程后面不会再执行,也不会有通知,更不会有记录,所以这个策略比较危险,他会让线程人间蒸发。这样大家都不知道他有没有发生过。如果发生在业务上,可能会造成业务数据的丢失。
         *
         * DiscardOldestPolicy (牺牲策略)#
         * 这个就比较残忍,虽然不会丢弃掉想要挤进来的线程,但是他会把线程队列中存活时间久的线程给丢弃掉,让想挤进来的线程去替代他。有点像渣男。可以把它叫为渣男策略
         *
         * CallerRunsPolicy(执行策略)#
         * 这个策略就比较负责了,当线程池满了之后,想要进来的线程不会被丢弃,他会直接让提交线程的线程去执行这个线程任务,也是不进入到线程池,你直接就地解决吧。更简单的理解为,当遇到了这个策略,相当于这个线程池新增了一个线程资源。
         * 这样的方式有两点好处
         */
        threadPoolExecutor.submit(new MyTask("5", 10000L));
        threadPoolExecutor.submit(new MyTask("6"));

    }
}

class MyTask implements Runnable {
    private final String name;
    private Long time = 0L;

    public MyTask(String name, Long time) {
        this.name = name;
        this.time = time;
    }

    public MyTask(String name) {
        this.name = name;
    }


    @Override
    public void run() {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

wait与sleep

共同点
wait()、wait(long)、sleep(long)、的效果都是让当前线程蚕食放弃cpu的使用权,进入阻塞状态
不同点
(1)方法归属不同:sleep时Thread的静态方法,wait(),wait(long)都是Object的成员方法,每个对象都有
(2)醒来时机不同:执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来,wait(long)和wait()还可以被notify()唤醒,wait()如果不唤醒就会一直等待下去,他们都可以被打断唤醒
(3)锁特性不同:wait方法的调用必须先获取wait的对象,而sleep则无此限制;wait方法执行后会先释放对象锁,允许其他线程获得该对象锁(我放弃,但你们可以用);sleep如果在synchronized代码中执行,并不会释放锁对象(我放弃,你们也别想执行)

import java.util.concurrent.locks.ReentrantLock;

public class WaitAndSleep {
    public static void main(String[] args) throws InterruptedException {

        Object LOCK = new Object();
//        LOCK.wait();//直接调用会报错
        //必须获取锁才能使用
//        synchronized (LOCK) {
//            LOCK.wait();
//        }

        Thread t1 = new Thread(()->{
            synchronized (LOCK) {
                try {
                    LOCK.wait(100000l);//wait()会释放锁,主线程会一秒钟过后执行
                    Thread.sleep(10000l);//sleep()不会释放锁,主线程会在10秒后执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1");

        t1.start();
        t1.interrupt();//可以打断wait或者sleep
        Thread.sleep(1000l);
        synchronized (LOCK) {
            System.out.println("获取到锁");
        }

    }
}

lock和synchronized

语法层面:
(1)synchronized是关键字,源码在jvm中,用c++实现
(2)Lock是接口,源码有jdk提供,用java语言实现
功能层面:
(1)二者都属于悲观锁、都具备基本的互斥、同步、锁重入功能
(2)Lock提供了需索synchronized不具备的功能,例如获取等待状态、公平锁、可打断、可超市、多条件变量
(3)Lock有适合不同场景的实现,如ReentrantLock、ReentrantReadWriteLock
性能层面:
(1)没有竞争条件下,synchronized做了很多优化,如偏向锁、轻量级锁、性能较好
(2)竞争激烈条件下,Lock的实现通常会提供更好的性能

公平锁和非公平锁:

公平锁:抢锁的时候排队,按照阻塞队列的顺序执行
非公平锁:不排队,谁抢到就是谁的,没有先来后到的说法

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class MyReentrantLock {
    private volatile static boolean stop = true;
    public static void main(String[] args) throws InterruptedException {
        /**
         * false非公平锁:t1获取锁后,未获取到锁的线程放入阻塞队列中,获取到锁的线程执行完后,队列按照先进先出获取锁
         * true公平锁:按照顺序获取锁
         */
        ReentrantLock lock = new ReentrantLock(false);
        new Thread(()->{
            lock.lock();
            System.out.println("t1");
            try {
                Thread.sleep(1000l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.unlock();
        }, "t1").start();
//        Thread.sleep(100l);
        new Thread(()->{
            lock.lock();
            System.out.println("t2");
            lock.unlock();
        }, "t2").start();
//        Thread.sleep(100l);
        new Thread(()->{
            lock.lock();
            System.out.println("t3");
            lock.unlock();
        }, "t3").start();
//        Thread.sleep(100l);
        new Thread(()->{
            lock.lock();
            System.out.println("t4");
            lock.unlock();
        }, "t4").start();
//        Thread.sleep(100l);
        while(stop) {
            new Thread(()->{
                try {
                    boolean b = lock.tryLock(10, TimeUnit.MICROSECONDS);
                    if (b) {
                        System.out.println(Thread.currentThread().getName() + "获取锁成功");
                        stop = false;
                        Thread.sleep(1000l);
                        lock.unlock();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t5").start();
        }
    }
}

Lock锁中的条件变量:Condition的使用

Condition相比于wait可以更好的控制多个线程之间的wait、notify、notifyAll等方法
代码中如果不使用condition也可以使用wait与notifyAll实现,但是相比注释掉的我更觉得condition代码更清晰一些,它指定了唤醒哪个,notify只是会唤醒阻塞状态的线程,condition会唤醒指定的等待队列中的线程

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionTest {
    ReentrantLock reentrantLock = new ReentrantLock();
    Condition condition1 = reentrantLock.newCondition();
    Condition condition2 = reentrantLock.newCondition();
    Condition condition3 = reentrantLock.newCondition();
    private int state = 1;
    public static void main(String[] args) {
        ConditionTest conditionTest = new ConditionTest();
        new Thread(()->{
            while (true) {
                try {
                    conditionTest.t1();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(()->{
            while (true) {
                try {
                    conditionTest.t2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(()->{
            while (true) {
                try {
                    conditionTest.t3();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();



    }
    private void t1() throws InterruptedException {
        reentrantLock.lock();
        try {
            while (state !=1 ) {
                condition1.await();
            }
            System.out.println("t1");
            state ++;
            condition2.signal();
        } finally {
            reentrantLock.unlock();
        }

    }
    private void t2() throws InterruptedException {
        reentrantLock.lock();
        try {
            while (state !=2 ) {
                condition2.await();
            }
            System.out.println("t2");
            state ++;
            condition3.signal();
        } finally {
            reentrantLock.unlock();
        }

    }
    private void t3() throws InterruptedException {
        reentrantLock.lock();
        try {
            while (state !=3 ) {
                condition3.await();
            }
            System.out.println("t3");
            state = 1;
            condition1.signal();
        } finally {
            reentrantLock.unlock();
        }

    }

//    private synchronized void t1() throws InterruptedException {
//            while (state !=1 ) {
//                this.wait();
//            }
//            System.out.println("t1");
//            state ++;
//            this.notify();
//
//    }
//    private synchronized void t2() throws InterruptedException {
//            while (state !=2 ) {
//                this.wait();
//            }
//            System.out.println("t2");
//            state ++;
//            this.notify();
//
//    }
//    private synchronized void t3() throws InterruptedException {
//            while (state !=3 ) {
//                this.wait();
//            }
//            System.out.println("t3");
//            state = 1;
//            this.notify();
//
//
//    }
}

volatile:

1.线程安全要考虑的三个方面:可见性、有序性、原子性
(1)可见性:一个线程对共享变量修改,另一个线程能考到最新的结果
(2)有序性:一个线程内代码按编写顺序执行,使用内存屏障防止指令重排序
(3)原子性:一个线程内多行代码以一个整体运行,期间不能有其他现成的代码插队
2.volatile能够保证共享变量的可见性与有序性,但不能保证原子性

//原子性
public class VolatileTest2 {
    private static volatile int a = 10;
    //原子性
    public static void main(String[] args) {

        new Thread(() -> {
            add();
            try {
                Thread.sleep(1000l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(1000l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sub();
            System.out.println(a);
        }).start();


    }
    public static void add() {
        //违背了原子性原则
        //通过打线程断电模拟多线程下原子性操作,add与sub交替执行,会发现a变量最后得出的结果不是10,结果是5或者15,也就验证了volatile确实不保证原子性
        int temp = a;
        temp += 5;
        a = temp;
    }
    public static void sub() {
        int temp = a;
        temp -= 5;
        a = temp;
    }

}

//可见性
public class VolatileTest {
    private static boolean stop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(1000l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //过一秒后等职test(),但是发现却停不下来,这是因为有一个jit的东东为了节省cpu调度内存的一个优化操作
            stop = true;
            System.out.println("t1.....");
        }, "t1").start();
        test();
    }

    /**
     * jit发现上万次都在重复调用test(),就不会一直去内存中去拿stop的具体值,而是变成了以下代码
     * while(!false),这样就会让test()一直跑,我们可以在Edit Configurations中加一个参数add VM options -Xint即可禁用jit,但是禁用会导致程序性能地下
     * 同样可以在变量上加一个volatile关键字来关闭这个jit保证变脸的可见性
     */
    private static void test() {
        int i =0;
        while (!stop) {
            i++;
        }
        System.out.println(i);
    }
}

悲观锁和乐观锁:

悲观锁:代表Synchronized与Lock锁
(1)核心思想是线程只有占有了锁,才可以去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程要等待,进入阻塞状态
(2)线程从运行道阻塞,再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
(3)线程获取Synchronized和Lock锁时,如果锁已经被占用,都会做几次的重试操作,减少阻塞机会
乐观锁:代表AtomicInteger,使用cas来保证原子性
(1)核心思想就是无需加锁,每次只有一个线程操作共享变量,其失败的线程也会不断重试,直至成功
(2)由于线程一直在运行,不需要阻塞,因此不设计线程上下文切换
(3)他需要多cpu支持,且线程数不应该超过cpu核数

import sun.misc.Unsafe;

import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

public class SyncAndCas {
    public static void main(String[] args) {
    /**
    * 乐观锁使用cas来保证共享数据的,不会读到脏的数据,通过cas可以比较出读到的数据被其他线程修改了,如果被修改了就会		         循环再去取最新的值进行业务处理
    * 悲观锁就是采用加锁的方式来保证共享变量的正确性,抢到锁的先执行,多次抢不到锁会进入阻塞队列中进入阻塞状态,等待通知进行二次抢锁;
    /
        AtomicInteger atomicInteger = new AtomicInteger(10);

        while(true) {
            int a = new Random().nextInt(11);
            System.out.println(a);
            //悲观锁,符合预期的值才回去修改atomicInteger的值否则失败的线程会一直重试,直到成功
            boolean b = atomicInteger.compareAndSet(a, 888);
            if (b) {
                System.out.println(atomicInteger.intValue());
                break;
            }
        }
    }
}

HashTable与ConcurrentHashMap:

HashTable与ConcurrentHashMap都是线程安全的map集合
HashTable并发度低,整个hashTable对应一把锁,同一时刻,只能有一个线程操作
、1.8之前ConcurrentHashMap使用了Segment+数组+链表的结构,每个Segment对应一把锁,如果多个线程访问不同的Segment,则不会冲突
在这里插入图片描述
(1)1.8之前的ConcurrentHashMap的数据结构式一个大数组中每个大数组包着小数组,小数组的容量 = 容量/并发度,小数组的容量最小为2;一共有三个构造器,分别是容量、扩容因子、并发度,有多少个并发度会控制大数组的初始化大小,大数组一旦初始化后就不能扩容了,扩容因子发生在小数组中
(2)1.8之前的ConcurrentHashMap存放数据是把key的值进行二次hash计算,然后转换二进制,取二进制的高几位相加得到大数组索引下标(具体是高几位取决于并发度参数,比如说 == 16那就是2的4次幂,取高四位的和)
(3)1.8之前的ConcurrentHashMap具体放在小数组的哪个位置是取二进制值的最低位
(4)1.8之前的ConcurrentHashMap扩容是指小数组扩容,负载因子也是控制的小数组
(5)1.8之前的ConcurrentHashMap初始化后Segment就会有一个小数组,并且其他小数组初始化会与Segment【0】的小数组大小保持一致
、1.8开始ConcurrentHashMap将数组的每个头节点做为锁,如果多个线程访问的头节点不同,则不会冲突
在这里插入图片描述

(1)1.8开始ConcurrentHashMap就没有Segment这个大数组加小数组了,他和HashMap一样都是数组+链表/红黑树
(2)1.7的是调用构造方法后就会初始化好底层的数据结构,是一种饿汉式的设计思想,8是每次调用put方法时才会初始化,是一种懒汉式的设计思想
(3)1.8的初始化容量是由初始化的容量大小决定的,但不是设置了多大初始化容量就是多大,而是表示我要放进去这么多条数据,比如说我想放进去11个数据,那么取大于11 / 0.75 的最小的2的n次幂的树也就是16,如果想放进去12个数据,12 / 0.75恰巧等于16那么我们应该取32作为初始化的容量大小(不是之前的初始化容量了,而是想放进去这么多元素,最终初始化的容量需要自己在求算)
(4)扩容银子只是第一次计算的时候会用到,假如第一次我们给他初始化为0.5,也只有初始化的时候会用来计算他的容量,后续扩容的时候用的还是0.75
(5)并发问题,ConcurrentHashMap如果操作的不是同一个数组,那么他就可以并发的去执行,否则需要抢到锁后再去执行
(6)扩容中
并发get操作分为两种情况,一种是数据已经迁移走了(根据ForwardingNode判断是否迁移走,迁移走的数组会有ForwardingNode标识),纳闷我们就去扩容后的集合中去查找元素,如果没有迁移走,那么就在老的集合中寻找元素;再有就是在做链表迁移的时候很多node节点不能是同一个对象,也就是迁移的时候要new 一个新的node对象,防止.next()到脏数据(如果迁移后的位置相同,那么就不需要重新创建node对象)
并发put操作分为三种情况,一种是put未迁移的部分,那么就直接put到原位置,如果put到了正在迁移的位置,那就会处于阻塞状态,如果put到已经迁移完成的部分,那么会帮助未签以完成的完成迁移在这里插入图片描述
ThreadLocal:可以通过get()方法获取当前线程的资源,也可以通过set()方法设置资源
ThreadLocal可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免征用引发的线程安全问题;(使用局部对象没问题吗?局部对象不能跨越方法,方法一中只能的局部对象只能在方法一中使用)
THreadLocal同时实现了线程内的资源共享
ThreadLocal原理是每个线程内有一个ThreadLocalMap类型的成员变量,用来存储资源对象
(1)调用set方法就是以ThreadLocal自己作为key,资源对象为value,放入当前线程的ThreadLocalMap集合中
(2)调用get方法就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值
(3)调用remove方法就是以ThreadLocal自己作为key,移除当前线程关联的资源值
为什么ThreadLocalMap中的key(即ThreadLocal)为弱引用
(1)Thread可能需要长时间运行,如果key不再使用,需要在内润不足垃圾回收释放内存
(2)但gc仅是让key的内存释放,后续还要根据key是否为null来进一步释放值的内存,释放的时机有
(a)获取key的值为null key
(b)set key时,会使用启发式扫描(与之邻近的清除),清除邻近的null key,启发次数与元素个数,是否发现null key有关
(c)remove时,因为一般使用ThreadLocal时都把他作为静态变量,因此弱引用也一直被引用,所以gc无法回收

虚拟机

jvm内存结构

这里是引用
方法区:存储类信息,方法信息
堆:存储对象信息
虚拟机栈:存储局部变量和java方法参数例如stu.study就是java方法(引用以及方法的参数)
本地方法栈:存储本地方法,例如上图stu.hahCode();
程序计数器:记录某个线程执行到哪里,以便暂停后恢复运行

jvm内存溢出场景:

只有程序计数器不会出现内存溢出的情况
出现OutOfMemoryError的情况:
(1)堆内存耗尽-对象越来越多,又一直在使用,gc无法回收
(2)方法去内存耗尽-加载的类越来越多,很多框架还会在运行期间动态产生新的累
(3)虚拟机栈累计 - 每个线程最多会占用1M内存,线程个数越来越多,长时间运行不销毁时
出现StackOverflowError的情况:
(1)虚拟机栈内部 - 发发调用次数过多(指的是小3中的1M内存撑爆)

方法区与永久代、元空间:

这里是引用
类加载的时候会把类数据放入元空间中,由创建的对象负责调用
在这里插入图片描述
原空间的内存释放,第一次只有对象c没有被引用,元空间中的Y不会被回收
第二次abc对象都没有被引用,原空间中的x,y也不会被立即回收掉
第三次垃圾回收的时候,如果对象的实例都不在引用了,并且对象的class类也不再被引用才会被集体回收

JVM内存参数: -Xmx20140m -Xms10240m -Xmn5120m -XX:survivorRatio=6

-Xmx20140m :最大内存
-Xms10240m :最小内存
-Xmn5120m :新生代占用内存5120m老年代占用5120m
-XX:survivorRatio=6 新生代一共6份
在这里插入图片描述
survivor = (from + to),新生代一共5个G所以survivor = 2G

JVM垃圾回收算法:

GC
1.GC发生在堆里。
2.GC是分代收集算法:次数上频繁收集Young区 Minor GC,次数较少收集Old区 Full GC,基本不动Perm区
GC四大算法
1.引用计数法:只要对象进行引用了,GC就不进行垃圾回收,现在已经淘汰,因为没有办法处理循环引用。
2.复制算法:年轻代中使用的是复制算法,从一个内存区域复制到另外一个内存区域,从From中找到存活对象,复制到To中,没有标记和清除过程,效率高,没有内存碎片,可以利用bump-the-pointer快速分配内存,但是需要双倍的空间比较浪费空间。
3.标记清除算法:老年代一般由标记清除或者是标记清除与标记整理混合实现,标记Mark:从根集合进行扫描,对存活的对象进行标记。清除Sweep:扫描整个内存空间,回收未被标记的对象,使用free-list记录可以。优点:不需要额外空间。缺点:两次扫描,耗时,标记扫描一次,清除扫描一次并且会产生内存碎片。
4.标记压缩:老年代一般是由标记清除或者是标记清除与标记整理的混合实现,相对于标记清楚产生的缺点进行整理,标记清楚清楚后会产生空余的空间,对这些空间进行压缩,使标记的对象都挨在一起。优点:没有时间碎片,可以利用bump-the-pointer。缺点:需要移动对象的成本。

分代回收:

伊甸园eden,最初对象都会分配到这里,与幸存取合称为新生代区
幸存区survivor,当伊甸园内存不足时,根据跟节点进行标记,然后采用复制算法,把引用的对象复制到to区
在这里插入图片描述
再把伊甸园区所有的对象删除,再把from与to进行位置的交换在这里插入图片描述
第二次如果再有垃圾回收的对象,会把伊甸园区的引用对象与from区的对象都复制到to区,然后把伊甸园的内存与from的内存都清理掉在这里插入图片描述
最后再交换在这里插入图片描述
当新生代区的对象熬过了15次的垃圾回收,就会放到更安全的老年区中

GC规模:

(1)Minor GC发生在新生代的垃圾回收,暂停时间短
(2)Mixed GC新生代 + 老年代部分区域的垃圾回收,G1收集器持有
(3)Full GC新生代+老年代完整垃圾回收,暂停时间久,应尽力避免

三色标记:根据跟节点伴随引用链逐个标记,最后白色的没有被引用违背标记的会被垃圾回收;

黑色-已标记
灰色-标记中
白色-还未标记
在这里插入图片描述
并发问题:用户线程干扰回收线程,用户线程增加关联关系或者删除引用关系会导致漏标为黑色,有以下两种方案
(1)增量更新:只要赋值发生,被复制的对象就会被记录
(2)原始快照:新分配的对象会被记录,被删除引用关系的对象也会被记录

垃圾回收器:

Parallel GC: 注重吞吐量
(1)伊甸园区内存不足时发生Minor GC,标记复制STW(Stop一the一World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿)
(2)old内存不足时,发生Full GC 标记整理STW
ConcurrentMarkSweep GC:注重响应时间
(1)old并发标记,重新标记时需要STW,并发清除
(2)Failback Full GC 保底策略,出现异常的时候会启用Full GC来保证垃圾回收的彻底

内存溢出的场景,解决办法:
一、线程池内存溢出

//-Xmx64m
    public static void main(String[] args) {
        //这种静态方法下面初始化的线程池容易出问题,LinkedBlockingQueue无限大小,这种方法又限制了核心线程数与最大线程数等于2
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        //这里一直向队列中提交,就会导致内存溢出,设置了jvm最大-Xmx64m,这样报错的更快一些
        while(true) {
            executorService.submit(() ->{
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }

二、查询出过多数据时,数据量过大,也会导致内存溢出
三、动态生成的类过多也会导致内存溢出

注意这个sheel是static的,所以不会进行垃圾回收这里是引用
解决办法,缩短shell对象的生命周期,不然对象无法回收,类也会一直在加载,元空间不会对其进行垃圾回收,改为方法中的局部变量即可避免,可以使用jconsole进行监听会发现会进行垃圾回收在这里插入图片描述

类加载过程:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
反编译看一下

{
  private static int a;
    descriptor: I
    flags: ACC_PRIVATE, ACC_STATIC

  private final int b;
    descriptor: I
    flags: ACC_PRIVATE, ACC_FINAL
    ConstantValue: int 2

  private static final int c;
    descriptor: I
    flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: int 3

  int d;
    descriptor: I
    flags:

  public Student();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_2
         6: putfield      #2                  // Field b:I
         9: aload_0
        10: iconst_4
        11: putfield      #3                  // Field d:I
        14: return
      LineNumberTable:
        line 1: 0
        line 7: 4
        line 11: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   LStudent;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: iconst_1
         1: putstatic     #4                  // Field a:I
         4: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #6                  // String student.class init
         9: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: return
      LineNumberTable:
        line 3: 0
        line 5: 4
        line 6: 12
}

双亲委派:

在这里插入图片描述
第一次加载一个类,父加载器中去查找,如果找不到继续父加载器中查找,如果都没有找到再依次向下寻找,之后会在app中找到加载;java中一个类只会加载一次

引用类型:

1.强引用:(1)普通变量赋值就是强引用,如A a = new A(); int b = 15;(2)通过GC Root的引用链,如果强引用引用不到该对象,该对象才能被回收
在这里插入图片描述
2.软引用:(1)SoftReference studentSoftReference = new SoftReference<>(new Student());(2)如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存不足再次垃圾回收的时候才会释放对象;(3)软引用自身需要适配引用队列来释放;(4)典型例子是反射数据
在这里插入图片描述

3.弱引用:(1)WeakReference studentWeakReference = new WeakReference<>(new Student());(2)如果弱引用引用该对象时,只要发生垃圾回收,就会释放对象;(3)弱引用自身需要配合引用队列来释放;(4)典型例子是ThreadLocalMap中的Entry对象
在这里插入图片描述

4.虚引用:(1)PhantomReference studentPhantomReference = new PhantomReference<>(new Student(), new ReferenceQueue();(2)必须配合引用队列一起使用,当虚引用引用的对象被回收时,会将虚引用对象入队,由Reference Handler线程释放其关联的外部资源;(3)典型例子时Cleaner释放DirectByteBuffer占用的直接内存
在这里插入图片描述

Spring

Spring_refresh概述(applicationContext建立起来之后):

共计分为十二个步骤:
在这里插入图片描述
1.prepareRefresh:为后续做好准备工作,准备了Environment环境对象,一开始ApplicationContext是空的,第一步创建了Environment对象并且赋值给ApplicationContext的成员变量,常用的@Value取值就是从这个Environment中自取出来的
在这里插入图片描述> 2.obtainFreshBeanFactory:这一步骤获取或创建BeanFactory,BeanFactory的作用是负责bean的创建、依赖注入和初始化;BeanDefinition作为bean的设计蓝图,规定了bean的特征,如单例多例、依赖关系、初始销毁方法等;BeanDefinition的来源有多种多样,可以是通过xml获取、通过配置类获取、通过组件扫描获得,也可以是编程添加;> 在这里插入图片描述
3.prepareBeanFactory:完善bean工厂,(1)StandardBeanExpressionResolver来解析Spel表达式;(2)ResourceEditorRegistrat会注释类型转换器,并应用ApplicationContext提供的Environment完成#{}解析;(3)特殊bean指beanFactory以及ApplicationContext,通过registerResolvableDependency来注册他们;(4)ApplicationContextAwareProcessor用来解析Aware接口
在这里插入图片描述
4.postProcessBeanFactory:这一步是空实现,留给子类扩展,(1)一般web环境的ApplicatonContext都要利用他注册新的Scope,完善Web的BeanFactory(2)体现的是模板方法设计模式
5.invokeBeanfactoryPostProcessors:通过后处理器的方式对bean工厂的另一种扩展方式
在这里插入图片描述
6.registerBeanPostProcessors:(1)bean后处理器,充当bean的扩展点,可以工作在bean的实例化,依赖注入,初始化阶段(2)AutowiredAnnotationBeanPostProcessor功能有:解析@Autowired,@Value注解;(3)CommomAnnotationBeanPostProcessor功能有解析@Resource,@PostConstruct,@PreDestroy;(4)AnnotationAwareAspectjAutoProxyCreator功能有为符合切点的目标bean自动创建代理
在这里插入图片描述
7.initMessageSource:至此beanFactory初始化完毕,ApplicationContext可以看作是ApplicationCOntext独有功能,具体就是国际化作用
在这里插入图片描述
8.initApplicationEventMulticaster:(1)广播器,用来发布时间给监听器,之前基于spring的监听模式就是基于这个;(2)可以从容其中找名为applicationEventMulticaster的bean作为事件广播器,若没有,也会新建默认的事件广播器;(3)可以调用ApplicationContext.publishEvent(时间对象)来发布事件
在这里插入图片描述
9.onRefresh:(1)这一步也是空实现,留给子类扩展,(2)SpringBoot中的子类可以在这里准备WebServer,及内嵌web容器
10.registerListeners;(1)监听器,用来接收事件;(2)一部分监听器是事先变成添加的、另一部分监听器来自容器中的bean、还有一部分来自于@EventListener的解析;(3)实现ApplicationListener接口,重写其中onApplicationEvent(E e)方法即可
在这里插入图片描述
11.finishBeanFactoryInitialization:这步会把beanFactory初始化完成(2)conversionService也是一套转换机制,作为PropertyEditor的补充;(2)内嵌值解析器用来解析@Value中的${ },借用的是Environment;(3)单例池用来缓存所有单例对象,对象的创建都分为三个阶段(创建、依赖注入、初始化),每一阶段都有不同的bean后处理器参与进来,扩展功能
在这里插入图片描述
12.finishRefresh:(1)用来控制容器内需要生命周期管理的bean;(2)如果容器中有名为lifecycleProcessor的bean就用它,否则创建默认的生命周期管理器;(3)调用context的start,即刻出发所有实现ListCycle接口bean的start
总结
在这里插入图片描述

Spring bean生命周期:

1.首先从doGetBean方法开始,为什么不是create而是get,因为spring获取bean是一种懒惰式的加载,获取的时候创建
阶段1:处理名称、检查缓存
···············先把别名解析为实际名称,再进行处理
···············若要FactoryBean本身,需要使用&名称获取
···············singletonObjects是一级缓存,放单例成品对象
···············singletonFactories是三级缓存,放单例工厂
···············earlySingletonObjects是二级缓存,放单例工厂的产品,可称为提前单例对象
阶段2:检查父工厂
···············如果没有缓存bean会不会直接创建bean呢,答案是不会,还回去父容器中去查bean
···············父子容器中的bean名称可以重复,可以都是名为‘service’的bean
···············有限查找子容器,子容器没有再去查找父容器
阶段3:检查DependsOn
···············如果A实例依赖于B是不是需要B的bean先创建,dependsOn用在非显示以来的bean的创建顺序控制
阶段4:按Scope创建bean
···············创建singleton,单例bean从refresh被创建,到close被销毁,BeanFactory会记录哪些bean要调用销毁方法
···············创建prototype,多例bean从获取实例被创建,其实单例多例都是从调用getBean(bean名称)来获取bean实例,只不过多例bean需要调用销毁方法.destroyBean(bean)
···············创建request,requestbean从首次getBean被创建,到request结束前被销毁,每个WebRequest对象会记录哪些bean要调用销毁方法,request请求结束前会调用这些销毁方法.requestComleted()
阶段5:创建bean
···············创建bean实例:
在这里插入图片描述

···············依赖注入
在这里插入图片描述
···············初始化
在这里插入图片描述
···············登记可销毁bean:(1)如果实现了DisposableBean或AutoCloseAble接口,则为可销毁bean;(2)如果自定义了destroyMethod,则可销毁bean;(3)如果采用@Bean没有指定destroyMethod,则采用自动推断方式获取销毁方法名(colse,shutdown);(4)如果有@PreDestroy标注的方法;(5)单例bean可销毁的bean存储于beanFactory的成员中;(6)自定义scope的可销毁bean会存储于对应的域对象中;(7)prototype scope不会存储,需要自己找到此对象销毁
阶段6:类型转换
···············创建完成bean后,期望的类型通过doGetBean中的requiredType传入进来,如果类型不是想要的,就会走一个尝试类型转换操作
阶段7:销毁bean
···············单例bean的销毁在ApplicationContext.close时,此时会找到所有DisposableBean的名字,逐一销毁
···············自定义bean的销毁在作用域对象生命周期结束时
···············prototype bean的销毁可以通过自己手动调用AutowireCapableBeanFactory.destroyBean方法执行销毁

Spring事务失效的几种场景及原因:

1.spring注解事务抛出异常,抛出检查异常导致事务不能正确回滚,Spring默认只会回滚非检查异常,可通过配置好rollbackFor属性解决;
2.代码可能被try cache捕捉到异常,异常没有抛出去导致注解感知不到,可抛出异常解决,或者手动回滚TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
3.可能被环绕通知的异常捕捉到,导致回滚失败,可以通过设置切面环绕通知的优先级(默认最低可以通过@Order注解给设置优先级,把他设置的更低),但是事务注解也是最低优先级(spring默认就把事务注解排到了最低);
4.@Transactional只能作用在public方法上,否则也会失效或者通过配置修改默认规则也可
在这里插入图片描述
5.Springmvc要注意父子容器问题
6.锁失效场景,多线程情况下,一般我们在业务方法上加锁会是我们普遍的认知,但是在@Transactional后执行完业务代码还会执行事务提交,这里的加锁控制不住,也就是锁的范围小了,没有锁柱事物的commit,所以扩大锁的范围即可;或者加行锁也可行

Springmvc

初始化阶段:

1.在Web容器第一次用到DispatcherServlet的时候,会创建其对象并执行init方法
2.init方法内会创建Spring Web容器,并调用容器refresh方法
3.refresh过程中会创建并初始化SpringMVC中的重要组件,例如MultipartResolver,HandlerMapping,HandlerAdapter,HandlerExceptionResolver,ViewResolver等
4.容器初始化后,会将上一步初始化好的重要组件,赋值给DispatcherServlet的成员变量,留待后使用
在这里插入图片描述

匹配阶段:

1.用户发送的请求统一到达前端控制器DispatcherServlet
2.DispatcherServlet遍历所有HandlerMapping,找到与路径匹配的处理器
(1)HandlerMapping有多个,每个HandlerMapping会返回不同的处理器对象,谁先匹配,返回谁的处理器。其中能识别@RequestMapping的优先级最高
(2)对应@RequestMapping的处理器是HandlerMethod,它包含了控制器对象和控制器方法信息
(3)其中路径与处理器的映射关系在HandlerMapping初始化时就会创立好
在这里插入图片描述
3.将HandlerMethod连同匹配到的拦截器,生成调用链对象HandlerExecutionChain返回
在这里插入图片描述
4.遍历HandlerAdapter处理器适配器,找到能处理HandlerMethod的适配器对象,开始调用
在这里插入图片描述

执行阶段:

1.执行拦截器preHandle返回true继续,返回false被拦截
在这里插入图片描述
2.由HandlerAdapter调用HandlerMethod
(1)调用前处理不同类型的参数
(2)调用后处理不同类型的返回值
在这里插入图片描述
3.第二步骤没有异常
(1)返回ModelAndView
(2)执行拦截器postHandle方法
(3)解析视图,得到View对象,进行视图渲染
(4)最后调用afterCompletion,整个流程结束
在这里插入图片描述
4.第二部有异常,进入HandlerExceptionResolver异常处理流程
在这里插入图片描述
5.最后都会执行拦截器的afterCompletion方法
6.如果控制器方法标注了@ResponseBody注解,则在第二步,就会生成json结果,并标记ModelAndView已处理,这样就不会执行第三步的视图渲染

spring-springmvc-springboot注解

spring注解:

1.@EnableTransactional:开启声明式事务管理,Springboot中不记得用过,但是可以使用@Transactional是因为Springboot自动装配了
@Transactional:声明式事务注解
2.@Order:控制多个bean的顺序,默认最低优先级
3.@Async:声明该方法是一个异步任务,@Async的原理是同各国Spring AOP动态代理的方式来实现的,Spring容器启动初始化bean时,判断类中是否使用了@Asynv注解,如果使用了则为其创建切入点和切入点处理器,根据切入点创建代理;在线程调用@Async注解标注的方法时,会调用代理,执行切入点处理器invoke方法,将方法的执行提交给线程池中的另外一个线程来处理,从而实现了异步
4.@ComponentScan:指定起始包名去扫描,扫描到的加入到容器中管理
5.@Conditional:条件装配,满足条件放入spring容器中
6.@Configuration:标注配置类的,配置类相当于一个工厂,标注@Bean注解的方法相当于工厂方法;默认会为标注的类生成代理,其中的目的时保证@Bean方法相互调用任然保证其单例性
7.@Bean:指定配置类中的哪些方法中的实例需要注入到Spring容器中
8.@Import:导入其他的配置类或者导入其他的selector根据返回的bean名称加入到容器中
9.@Lazy:标注在类上:延迟实例化与初始化,什么是时候用到这个类什么时候初始化与实例化;标注在方法参数与成员变量上:解决循环依赖;
10.@PropertySource:读取外部的properties文件,把他们的键值信息加入进来
11.@Aurowired:依赖注入
12.@Qualifier:依赖注入时,同一个类型有多个bean,使用Qualifier进行区分
13.@Value:值注入

Springmvc注解:

1.@RequestBody:把请求体中的json数据转换为java对象
2.@ResponseBody:把返回的java对象装换成json数据
3.@ResponseStatus:可以控制响应的状态
4.@ControllerAdvice:统一处理,例如实现统一异常的处理,通常与@ExceptionHandler一起配合使用

Spring boot注解:

1.@ConfigurationProperties:把propieties的键值对信心与javabean的属性进行绑定
2.@EnableConfigurationProperties:启用@ConfigurationProperties功能
3.@ConditionalOnXXX:更具XXX条件满足才把bean添加到spring容器中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值