java知识

JVM

基本概念

​ JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、 一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接 的交互。
在这里插入图片描述

运行过程

​ Java 源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件, 而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。

  • Java 源文件—->编译器—->字节码文件

  • 字节码文件—->JVM—->机器码

每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。

JAVA集合

接口继承关系和实现

​ 集合类存放于Java.util包中,主要有三种:set(集)、list(列表包含Queue)和map(映射)

  • Collection:Collection是集合List、Set、Queue的最基本接口。
  • Iterator:迭代器,可以通过迭代器遍历集合中的数据
  • Map:映射的基础接口。

在这里插入图片描述

在这里插入图片描述

List

​ List 是常用的数据类型。List 是有序的Collection。List 一共三个实现类: 分别是 ArrayList、Vector 和 LinkedList。
在这里插入图片描述

ArrayList(数组)

​ ArrayList是最常用的List实现类,内部通过数组实现,允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

Vector(数组实现、线程同步)

​ Vector 与ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此, 访问它比访问ArrayList 慢。

LinkList(链表)

​ LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

Set

​ Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。

在这里插入图片描述

HashSet(Hash 表)

哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较

equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。

哈希值相同 equals 为 false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希一样的存一列。如图 1 表示 hashCode 值不相同的情况;图 2 表示 hashCode 值相同,但 equals 不相同的情况。

TreeSet(二叉树)

​ TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。

​ Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的,自己定义的类必须实现Comparable 接口,并且覆写相应的 compareTo()函数,才可以正常使用。

​ 在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序

​ 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。

LinkHashSet(HashSet+LinkedHashMap)

​ 对于 LinkedHashSet 而言, 它继承与 HashSet 、又基于 LinkedHashMap 来实现的。LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有的方法操作上又与HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类HashSet 的操作相同,直接调用父类 HashSet 的方法即可。

Map

在这里插入图片描述

HashMap(数组**+链表+**红黑树)

​ HashMap 根据键的hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。我们用下面这张图来介绍HashMap 的结构。

JAVA7实现

HashMap里面是一个数组,数组中的每一个元素是一个单向链表,每个绿色的实体是嵌套类Entry的实例,Entry包含四个属性:keyvaluehash值用于单向链表的next

在这里插入图片描述

  • capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。

  • loadFactor:负载因子,默认为 0.75。

  • threshold:扩容的阈值,等于 capacity * loadFactor

JAVA8实现

JAVA8对HashMap进行了一些修改,利用了红黑树,由数组+链表+红黑树组成

在这里插入图片描述

ConcurrentHashMap
Segment段

​ ConcurrentHashMap和HashMap的思路差不多,但ConcurrentHashMap支持并发操作,更复杂,整个ConcurrentHashMap由一个个Segment组成,Segment代表"部分"或者"一段"的意思,所以很多地方都会将其描述为分段锁

线程安全(Segment 继承 ReentrantLock 加锁)

​ ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。

在这里插入图片描述

并行度(默认 16

​ concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。

Java8 实现 (引入了红黑树)

在这里插入图片描述

HashTable(线程安全)

​ Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类, 并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap, 因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用HashMap 替换,需要线程安全的场合可以用ConcurrentHashMap 替换。

TreeMap(可排序)

​ TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序, 也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。

如果使用排序的映射,建议使用 TreeMap。

​ 在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常。

参考:https://www.ibm.com/developerworks/cn/java/j-lo-tree/index.html

LinkHashMap(记录插入顺序)

​ LinkedHashMap 是 HashMap 的一个子类, 保存了记录的插入顺序, 在用 Iterator 遍历LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

参考 1:http://www.importnew.com/28263.html

参考 2:http://www.importnew.com/20386.html#comment-648123

JAVA多线程并发

JAVA并发知识库

在这里插入图片描述

JAVA 线程实现及创建方式

继承Thread类

​ Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread 类的start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行run()方法。

public class MyThread extends Thread { 
    public void run(){
        System.out.println("MyThread.run()");
    }
}
MyThread myThread1 = new MyThread(); 
myThread1.start();   
实现Runnable接口

如果当前类已经继承了一个其他类,就无法再去继承Thread类,此时,可以实现一个Runnable接口

public class MyThread extends OtherClass implements Runnable { 
    public void run() {
        System.out.println("MyThread.run()");
    }
}

//启动MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例: 
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread); 
thread.start();
//事实上,当传入一个Runnable target 参数给 Thread 后,Thread 的run()方法就会调用target.run()
public void run() {
    if (target != null) { 
        target.run();
    }
}
ExecutorService、Callable、Future 有返回值线程

​ 有返回值的任务必须实现Callable 接口,无返回值的任务必须Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程

//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>(); 
for (int i = 0; i < taskSize; i++) {
    Callable c = new MyCallable(i + " ");
	// 执行任务并获取Future 对象
    Future f = pool.submit(c); 
    list.add(f);
}
// 关闭线程池pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
	// 从Future 对象上获取任务的返回值,并输出到控制台
    System.out.println("res:" + f.get().toString());
}

基于线程池的方式

​ 线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10); 
while(true) {
    threadPool.execute(new Runnable() { 
        // 提交多个线程任务,并执行
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " is running .."); 
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) { 
                e.printStackTrace();
            }
        }
    });
}

4种线程池

​ Java里面线程池的顶级接口是Executor,但严格意义上Executor并不是一个线程池,只是一个执行线程的工具,而真正的线程接口是ExecutorService

在这里插入图片描述

newCachedThreadPool

​ 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。==调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。==因此,长时间保持空闲的线程池不会使用任何资源。

newFixedThreadPool

​ ==创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。==在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务, 则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

newScheduledThreadPool

创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); scheduledThreadPool.schedule(newRunnable(){
    @Override
    public void run() { 
        System.out.println("延迟三秒");
    }
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){ 
    @Override
    public void run() {
        System.out.println("延迟 1 秒后每三秒执行一次");
    }
},1,3,TimeUnit.SECONDS);
newSingleThreadExecutor

​ Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程 池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!

线程生命周期(状态)

​ 当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换

新建状态(NEW)

​ 当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值

就绪状态(RUNNABLE):

​ 当线程对象调用了start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

运行状态(RUNNING):

​ 如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。

阻塞状态(BLOCKED):

​ 阻塞状态是指线程因为某种原因放弃了 CPU 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

  • 等待阻塞(***o.wait->***等待对列):

    运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。

  • 同步阻塞***(lock->*锁池*)***

    运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。

  • 其他阻塞***(sleep/join)***

    运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入可运行(runnable)状态。

线程死亡(DEAD

线程会以下面三种方式结束,结束后就是死亡状态。

  • 正常结束

    run()或 call()方法执行完成,线程正常结束。

  • 异常结束

    线程抛出一个未捕获的 Exception 或 Error。

  • 调用 stop

    直接调用该线程的stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。

在这里插入图片描述

终止线程 4 种方式

正常运行结束

程序运行结束,线程自动结束。

使用退出标志退出线程

​ 一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如: 最直接的方法就是设一个boolean 类型的标志,并通过设置这个标志为true 或false 来控制while 循环是否退出,代码示例:

public class ThreadSafe extends Thread { 
    public volatile boolean exit = false;
    public void run() { 
        while (!exit){
            //do something
        }
    }
}

​ 定义了一个退出标志exit,当 exit 为 true 时,while 循环退出,exit 的默认值为false.在定义 exit 时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改exit 的值。

Interrupt 方法结束线程

使用 interrupt()方法来中断线程有两种情况:

  • 线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时, 会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获InterruptedException 异常之后通过break 来跳出循环,才能正常结束 run 方法。

  • 线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。

public class ThreadSafe extends Thread { 
    public void run() {
        while (!isInterrupted()){ 
            //非阻塞过程中通过判断中断标志来退出
            try{
                Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
            }catch(InterruptedException e){ 
                e.printStackTrace();
                break;//捕获到异常之后,执行 break 跳出循环
            }
        }
    }
}

stop 方法终止线程(线程不安全)

​ 程序中可以直接使用thread.stop()来强行终止线程,但是stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是: thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop 方法来终止线程。

sleep与 wait 区别

  • 对于sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。

  • sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

  • 在调用 sleep()方法的过程中,线程不会释放对象锁。

  • 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

start 与 run 区别

  • start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕, 可以直接继续执行下面的代码。

  • 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。

  • 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后CPU 再调度其它线程。

JAVA 后台线程

定义:守护线程–也称“服务线程”,他是后台线程,它有一个特性,即为用户线程 提供 公共服务,在没有用户线程可服务时会自动离开。

​ ==优先级:==守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。

​ ==设置:==通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的setDaemon 方法。

在 Daemon 线程中产生的新线程也是 Daemon 的。

​ 线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的。

example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread, 程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

​ ==生命周期:==守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出。

JAVA 锁

乐观锁

​ 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为 别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数 据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新), 如果失败则要重复读-比较-写的操作。 java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入 值是否一样,一样则更新,否则失败

悲观锁

​ 悲观锁就是一种悲观思想,认为写多,遇到并发写的可能性多,每次去拿数据的时候都认为别人会修改,所以每次读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。

​ Java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

自旋锁

​ 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁 的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋), 等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

​ 线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程 也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。

​ 如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁 的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自旋锁的优缺点

​ 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来 说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会 导致线程发生两次上下文切换!

​ 但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合 使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量 线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗, 其它需要 cup 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁;

自旋锁时间阈值(1.6 引入了适应性自旋锁)

​ 自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择 自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而 会影响整体系统的性能。因此自旋的周期选的额外重要!

​ JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的,在 1.6 引入了适应性自旋锁,适应 性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥 有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当 前 CPU 的负荷情况做了较多的优化,如果平均负载小于 CPUs 则一直自旋,如果有超过(CPUs/2) 个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现 Owner 发生了变化则延迟自旋 时间(自旋计数)或进入阻塞,如果 CPU 处于节电模式则停止自旋,自旋时间的最坏情况是 CPU 的存储延迟(CPU A 存储了一个数据,到 CPU B 得知这个数据直接的时间差),自旋时会适当放 弃线程优先级之间的差异

自旋锁的开启
  • JDK1.6 中-XX:+UseSpinning 开启
  • -XX:PreBlockSpin=10 为自旋次数
  • JDK1.7 后,去掉此参数,由 jvm 控制
Synchronized同步锁

Synchronized可以把任意一个非NULL的对象当作锁。它属于独占式的悲观锁,同时属于可重入锁

Synchrzonized作用范围
  • 作用于方法时,锁住的是对象的实例(this)

  • 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于一个全局锁,会锁所有调用该方法的线程。

  • synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列, 当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

Synchronized 核心组件
  • Wait Set哪些调用 wait 方法被阻塞的线程被放置在这里
  • Contention List竞争队列,所有请求锁的线程首先被放在这个竞争队列中
  • Entry ListContention List 中那些有资格成为候选资源的线程被移动到 Entry List 中
  • OnDeck任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck
  • Owner当前已经获取到所资源的线程被称为 Owner
  • !Owner当前释放锁的线程。
Synchronized实现

在这里插入图片描述

JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到EntryList 中作为候选竞争线程。

Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为OnDeck 线程(一般是最先进去的那个线程)。

Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck, OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。

OnDeck 线程获取到锁资源后会变为Owner 线程,而没有得到锁资源的仍然停留在EntryList 中。如果Owner 线程被wait 方法阻塞,则转移到WaitSet 队列中,直到某个时刻通过notify 或者 notifyAll 唤醒,会重新进去 EntryList 中。

处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。

Synchronized 是非公平锁。 Synchronized 在线程进入ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。

参考:https://blog.csdn.net/zqz_zqz/article/details/70233767

每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的

synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。

Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。

锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;

JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。

ReentantLock

​ ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

Lock 接口的主要方法
  • void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.

  • boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.

  • void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.

  • Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定, 当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。

  • getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行lock 方法的次数。

  • getQueueLength():返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个线程获得锁,此时返回的是 9

  • getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10

  • hasWaiters(Condition condition) : 查询是否有线程等待与此锁有关的给定条件(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法

  • hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁

  • hasQueuedThreads():是否有线程等待此锁

  • isFair():该锁是否公平锁

  • isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是false 和 true

  • isLock():此锁是否有任意线程占用

  • lockInterruptibly():如果当前线程未被中断,获取锁

  • tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁

  • tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持, 则获取该锁。

Spring原理

Spring IOC 原理

概念:

Spring 通过一个配置文件描述 Bean 及 Bean 之间的依赖关系,利用 Java 语言的反射功能实例化Bean 并建立 Bean 之间的依赖关系。 Spring 的 IoC 容器在完成这些底层工作的基础上,还提供了 Bean 实例缓存、生命周期管理、 Bean 实例代理、事件发布、资源装载等高级服务。

Spring 容器高层视图

Spring 启动时读取应用程序提供的 Bean 配置信息,并在 Spring 容器中生成一份相应的 Bean 配置注册表,然后根据这张注册表实例化 Bean,装配好 Bean 之间的依赖关系,为上层应用提供准备就绪的运行环境。其中 Bean 缓存池为 HashMap 实现。

在这里插入图片描述

IOC容器实现
BeanFactory框架基础设施

BeanFactory 是 Spring 框架的基础设施,面向 Spring 本身;ApplicationContext 面向使用Spring 框架的开发者,几乎所有的应用场合我们都直接使用 ApplicationContext 而非底层的 BeanFactory。

在这里插入图片描述

BeanDefinitionRegistry 注册表

Spring 配置文件中每一个节点元素在 Spring 容器里都通过一个 BeanDefinition 对象表示, 它描述了 Bean 的配置信息。而 BeanDefinitionRegistry 接口提供了向容器手工注册BeanDefinition 对象的方法。

BeanFactory 顶层接口

位于类结构树的顶端 ,它最主要的方法就是 getBean(String beanName),该方法从容器中返回特定名称的 Bean,BeanFactory 的功能通过其他的接口得到不断扩展:

ListableBeanFactory

该接口定义了访问容器中 Bean 基本信息的若干方法,如查看 Bean 的个数、获取某一类型Bean 的配置名、查看容器中是否包括某一 Bean 等方法;

HierarchicalBeanFactory 父子级联

父子级联 IoC 容器的接口,子容器可以通过接口方法访问父容器; 通过HierarchicalBeanFactory 接口, Spring 的 IoC 容器可以建立父子层级关联的容器体系,子容器可以访问父容器中的 Bean,但父容器不能访问子容器的 Bean。Spring 使用父子容器实现了很多功能,比如在 Spring MVC 中,展现层 Bean 位于一个子容器中,而业务层和持久层的 Bean 位于父容器中。这样,展现层 Bean 就可以引用业务层和持久层的 Bean,而业务层和持久层的 Bean 则看不到展现层的 Bean。

ConfigurableBeanFactory

是一个重要的接口,增强了 IoC 容器的可定制性,它定义了设置类装载器、属性编辑器、容器初始化后置处理器等方法;

AutowireCapableBeanFactory 自动装配

定义了将容器中的 Bean 按某种规则(如按名字匹配、按类型匹配等)进行自动装配的方法;

SingletonBeanRegistry 运行期间注册单例 Bean

定义了允许在运行期间向容器注册单实例 Bean 的方法;对于单实例( singleton)的 Bean 来说,BeanFactory 会缓存 Bean 实例,所以第二次使用 getBean() 获取 Bean 时将直接从IoC 容器的缓存中获取 Bean 实例。Spring 在 DefaultSingletonBeanRegistry 类中提供了一个用于缓存单实例 Bean 的缓存器,它是一个用HashMap 实现的缓存器,单实例的 Bean 以beanName 为键保存在这个 HashMap 中。

依赖日志框框

在初始化 BeanFactory 时,必须为其提供一种日志框架,比如使用 Log4J, 即在类路径下提供 Log4J 配置文件,这样启动 Spring 容器才不会报错。

ApplicationContext 面向开发应用

​ ApplicationContext 由 BeanFactory 派生而来, 提供了更多面向实际应用的功能。ApplicationContext 继承了 HierarchicalBeanFactory 和 ListableBeanFactory 接口,在此基础上,还通过多个其他的接口扩展了 BeanFactory 的功能:

在这里插入图片描述

  • ClassPathXmlApplicationContext:默认从类路径加载配置文件

  • FileSystemXmlApplicationContext:默认从文件系统中装载配置文件

  • ApplicationEventPublisher:让容器拥有发布应用上下文事件的功能,包括容器启动事 件、关闭事件等。

  • MessageSource:为应用提供 i18n 国际化消息访问的功能;

  • ResourcePatternResolver : 所 有 ApplicationContext 实现类都实现了类似于PathMatchingResourcePatternResolver 的功能,可以通过带前缀的Ant 风格的资源文件路径装载 Spring 的配置文件。

  • LifeCycle:该接口是 Spring 2.0 加入的,该接口提供了 start()和 stop()两个方法,主要用于控制异步处理过程。在具体使用时,该接口同时被 ApplicationContext 实现及具体Bean 实现, ApplicationContext 会将 start/stop 的信息传递给容器中所有实现了该接口的 Bean,以达到管理和控制 JMX、任务调度等目的。

  • ConfigurableApplicationContext 扩展于 ApplicationContext,它新增加了两个主要的方法: refresh()和 close(),让 ApplicationContext 具有启动、刷新和关闭应用上下文的能力。在应用上下文关闭的情况下调用 refresh()即可启动应用上下文,在已经启动的状态下,调用 refresh()则清除缓存并重新装载配置信息,而调用 close()则可关闭应用上下文。

WebApplication体系架构
	WebApplicationContext 是专门为 Web 应用准备的,它允许从相对于 Web 根目录的路径中装载配置文件完成初始化工作。从 WebApplicationContext 中可以获得ServletContext 的引用,整个 Web 应用上下文对象将作为属性放置到 ServletContext 中,以便 Web 应用环境可以访问 Spring 应用上下文。

在这里插入图片描述

Spring Bean 作用域

​ Spring 3 中为 Bean 定义了 5 中作用域,分别为 singleton(单例)、prototype(原型)、request、session 和 global session,5 种作用域说明如下:

singleton:单例模式(多线程下不安全)

singleton:单例模式,Spring IoC 容器中只会存在一个共享的 Bean 实例,无论有多少个Bean 引用它,始终指向同一对象。该模式在多线程下是不安全的。Singleton 作用域是Spring 中的缺省作用域,也可以显示的将 Bean 定义为singleton 模式,配置为:

<bean id="userDao" class="com.ioc.UserDaoImpl" scope="singleton"/>
prototype:原型模式每次使用时创建

prototype:原型模式,每次通过 Spring 容器获取 prototype 定义的 bean 时,容器都将创建一个新的 Bean 实例,每个 Bean 实例都有自己的属性和状态,而 singleton 全局只有一个对象。根据经验,对有状态的bean 使用prototype 作用域,而对无状态的bean 使用singleton 作用域。

Request:一次 request 一个实例

request:在一次 Http 请求中,容器会返回该 Bean 的同一实例。而对不同的 Http 请求则会产生新的 Bean,而且该 bean 仅在当前 Http Request 内有效,当前 Http 请求结束,该 bean 实例也将会被销毁。

<bean id="loginAction" class="com.cnblogs.Login" scope="request"/>
session

session:在一次 Http Session 中,容器会返回该Bean 的同一实例。而对不同的 Session 请求则会创建新的实例,该 bean 实例仅在当前 Session 内有效。同 Http 请求相同,每一次session 请求创建新的实例,而不同的实例之间不共享属性,且实例仅在自己的 session 请求内有效,请求结束,则实例将被销毁。

<bean id="userPreference" class="com.ioc.UserPreference" scope="session"/>
global Session

global Session:在一个全局的 Http Session 中,容器会返回该 Bean 的同一个实例,仅在使用 portlet context 时有效。

Spring Bean生命周期
  • 实例化实例化一个 Bean,也就是我们常说的 new。

  • IOC 依赖注入按照 Spring 上下文对实例化的 Bean 进行配置,也就是 IOC 注入。

  • setBeanName实现如果这个 Bean 已经实现了 BeanNameAware 接口,会调用它实现的 setBeanName(String) 方法,此处传递的就是 Spring 配置文件中 Bean 的 id 值

  • BeanFactoryAware实现如果这个 Bean 已经实现了 BeanFactoryAware 接口,会调用它实现的 setBeanFactory, setBeanFactory(BeanFactory)传递的是 Spring 工厂自身(可以用这个方式来获取其它Bean, 只需在 Spring 配置文件中配置一个普通的 Bean 就可以)。

  • ApplicationContextAware实现如果这个 Bean 已经实现了 ApplicationContextAware 接口,会调用setApplicationContext(ApplicationContext)方法,传入 Spring 上下文(同样这个方式也可以实现步骤 4 的内容,但比 4 更好,因为ApplicationContext 是 BeanFactory 的子接口,有更多的实现方法)

  • postProcessBeforeInitialization 接口实现-*初始化预处理*:如果这个 Bean 关联了 BeanPostProcessor 接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor 经常被用作是 Bean 内容的更改,并且由于这个是在 Bean 初始化结束时调用那个的方法,也可以被应用于内存或缓存技术。

  • init-method如果 Bean 在 Spring 配置文件中配置了 init-method 属性会自动调用其配置的初始化方法。

  • postProcessAfterInitialization:如果这个 Bean 关联了 BeanPostProcessor 接口,将会调用postProcessAfterInitialization(Object obj, String s)方法。

    注:以上工作完成以后就可以应用这个 Bean 了,那这个 Bean 是一个 Singleton 的,所以一般情况下我们调用同一个 id 的 Bean 会是在内容地址相同的实例,当然在 Spring 配置文件中也可以配置非 Singleton。

  • Destroy过期自动清理阶段当 Bean 不再需要时,会经过清理阶段,如果 Bean 实现了 DisposableBean 这个接口,会调用那个其实现的destroy()方法;

  • destroy-method配置清理如果这个 Bean 的 Spring 配置中配置了 destroy-method 属性,会自动调用其配置的销毁方法。

  • bean 标签有两个重要的属性(init-method 和 destroy-method)。用它们你可以自己定制初始化和注销方法。它们也有相应的注解(@PostConstruct 和@PreDestroy)。

    <bean id="" class="" init-method="初始化方法" destroy-method="销毁方法">
    

在这里插入图片描述

Spring 依赖注入的四种方式
  • 构造器注入

    /*带参数,方便利用构造器进行注入*/ public CatDaoImpl(String message){    this. message = message;}
    
    <bean id="CatDaoImpl" class="com.CatDaoImpl">	<constructor-arg value=" message "></constructor-arg></bean>
    
  • setter方法注入

    public class Id {     private int id;	public int getId() {	        return id;	    }	public void setId(int id) {	        this.id = id;     }}
    
    <bean id="id" class="com.id ">     <property name="id" value="123"></property> </bean>
    
  • 静态工厂注入:静态工厂顾名思义,就是通过调用静态工厂的方法来获取自己需要的对象,为了让 spring 管理所有对象,我们不能直接通过"工程类.静态方法()"来获取对象,而是依然通过 spring 注入的形式获取:

    public class DaoFactory { //静态工厂	public static final FactoryDao getStaticFactoryDaoImpl(){         return new StaticFacotryDaoImpl();	}}public class SpringAction {	private FactoryDao staticFactoryDao; //注入对象	//注入对象的 set 方法	public void setStaticFactoryDao(FactoryDao staticFactoryDao) {        this.staticFactoryDao = staticFactoryDao;	}}
    
    //factory-method="getStaticFactoryDaoImpl"指定调用哪个工厂方法<bean name="springAction" class=" SpringAction" >	<!--使用静态工厂的方法注入对象,对应下面的配置文件-->	<property name="staticFactoryDao" ref="staticFactoryDao"></property></bean><!--此处获取对象的方式是从工厂类中获取静态方法--><bean name="staticFactoryDao" class="DaoFactory" factory-method="getStaticFactoryDaoImpl"></bean>
    
  • 实例工厂:实例工厂的意思是获取对象实例的方法不是静态的,所以你需要首先 new 工厂类,再调用普通的实例方法:

    public class DaoFactory {     //实例工厂public FactoryDao getFactoryDaoImpl(){	return new FactoryDaoImpl();	}}public class SpringAction {	private FactoryDao factoryDao;	//注入对象	public void setFactoryDao(FactoryDao factoryDao) {         this.factoryDao = factoryDao;	}}
    
    <bean name="springAction" class="SpringAction">	<!--使用实例工厂的方法注入对象,对应下面的配置文件-->	<property name="factoryDao" ref="factoryDao"></property></bean><!--此处获取对象的方式是从工厂类中获取实例方法--><bean name="daoFactory" class="com.DaoFactory"></bean><bean name="factoryDao" factory-bean="daoFactory" factory-method="getFactoryDaoImpl"></bean>
    
5种不同方式的自动装配

Spring 装配包括手动装配和自动装配,手动装配是有基于 xml 装配、构造方法、setter 方法等自动装配有五种自动装配的方式,可以用来指导 Spring 容器用自动装配方式来进行依赖注入。

  • no:默认的方式是不进行自动装配,通过显式设置 ref 属性来进行装配。

  • byName:通过参数名自动装配,Spring 容器在配置文件中发现bean 的autowire 属性被设置成 byname,之后容器试图匹配、装配和该 bean 的属性具有相同名字的bean。

  • byType:通过参数类型自动装配,Spring 容器在配置文件中发现 bean 的 autowire 属性被设置成byType,之后容器试图匹配、装配和该bean 的属性具有相同类型的bean。如果有多个 bean 符合条件,则抛出错误。

  • constructor:这个方式类似于 byType, 但是要提供给构造器参数,如果没有确定的带参数的构造器参数类型,将会抛出异常。

  • autodetect:首先尝试使用 constructor 来自动装配,如果无法工作,则使用byType 方式。

Spring AOP 原理

概念

​ “横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块, 并将其命名为"Aspect”,即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共 同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未 来的可操作性和可维护性。

​ 使用"横切"技术,AOP 把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

AOP 主要应用场景
  • Authentication 权限

  • Caching 缓存

  • Context passing 内容传递

  • Error handling 错误处理

  • Lazy loading 懒加载

  • Debugging 调试

  • logging, tracing, profiling and monitoring 记录跟踪 优化 校准

  • Performance optimization 性能优化

  • Persistence 持久化

  • Resource pooling 资源池

  • Synchronization 同步

  • Transactions 事务

AOP 核心概念
  • 切面(aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象

  • 横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点。

  • 连接点(joinpoint):被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring 中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。

  • 切入点(pointcut):对连接点进行拦截的定义

  • 通知(advice):所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类。

  • 目标对象:代理的目标对象

  • 织入(weave):将切面应用到目标对象并导致代理对象创建的过程

  • 引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。

    参考:https://segmentfault.com/a/1190000007469968
    在这里插入图片描述

AOP两种代理方式

​ Spring 提供了两种方式来生成代理对象: JDKProxy 和 Cglib, 具体使用哪种方式生成由AopProxyFactory 根据 AdvisedSupport 对象的配置来决定。默认的策略是如果目标类是接口, 则使用 JDK 动态代理技术,否则使用 Cglib 来生成代理。

JDK动态接口代理

JDK 动态代理主要涉及到 java.lang.reflect 包中的两个类:Proxy 和 InvocationHandler。InvocationHandler 是一个接口,通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编制在一起。Proxy 利用 InvocationHandler 动态创建一个符合某一接口的实例,生成目标类的代理对象。

CGLib动态代理

CGLib 全称为 Code Generation Library,是一个强大的高性能,高质量的代码生成类库, 可以在运行期扩展 Java 类与实现 Java 接口,CGLib 封装了 asm,可以再运行期动态生成新的 class。和 JDK 动态代理相比较:JDK 创建代理有一个限制,就是只能为接口创建代理实例, 而对于没有通过接口定义业务方法的类,则可以通过 CGLib 创建动态代理。

实现原理
@Aspectpublic class TransactionDemo {        @Pointcut(value="execution(* com.yangxin.core.service.*.*.*(..))")     public void point(){}    @Before(value="point()")     public void before(){        System.out.println("transaction begin");    }    @AfterReturning(value = "point()")     public void after(){        System.out.println("transaction commit");    }    @Around("point()")	public void around(ProceedingJoinPoint joinPoint) throws Throwable{         System.out.println("transaction begin");		joinPoint.proceed();         System.out.println("transaction commit");	}} 

在这里插入图片描述

Spring MVC 原理

​ Spring 的模型-视图-控制器(MVC)框架是围绕一个 DispatcherServlet 来设计的,这个 Servlet 会把请求分发给各个处理器,并支持可配置的处理器映射、视图渲染、本地化、时区与主题渲染等,甚至还能支持文件上传。

在这里插入图片描述

  • 用户发送一个请求到前端控制器(DispatcherServlet)

  • 前端控制器收到请求调用处理映射器HandlerMappering,解析请求对应的handler

  • 找到以后处理器映射器(HandlerMappering)找到相应的handler,生成处理器对象及处理拦截器一并返回给前端控制器(DispatcherServlet)

  • 前端控制器(DispatcherServlet)调用处理器适配器(HandlerAdapter)

  • 处理器适配器经过处理适配找到对应的controller

  • Controller执行完成返回模型视图(ModelAndView)

  • 处理器适配器将Controller的执行结果视图(ModelAndView)返回给前端安控制器(DispatcherServlet)

  • 前端控制器(DispatcherServlet)将视图(ModelAndView)传给视图解析器(ViewReslover)

  • 视图解析器(ViewReslover)解析后返回具体的视图(View)

  • 前端控制器(DispatcherServlet)根据视图进行渲染

  • 前端控制器(DispatcherServlet)响应客户

MVC 常用注解

在这里插入图片描述

SpringBoot 原理

​ Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot 致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。其特点如下:

  • 创建独立的 Spring 应用程序

  • 嵌入的 Tomcat,无需部署 WAR 文件

  • 简化 Maven 配置

  • 自动配置 Spring

  • 提供生产就绪型功能,如指标,健康检查和外部配置

  • 绝对没有代码生成和对 XML 没有要求配置 [1]

事务

​ 事务是计算机应用中不可或缺的组件模型,它保证了用户操作的原子性 ( Atomicity )一致性( Consistency )隔离性 ( Isolation )持久性 ( Durabilily )

本地事务

​ 紧密依赖于底层资源管理器(例如数据库连接 ),事务处理局限在当前事务资源内。此种事务处理方式不存在对应用服务器的依赖,因而部署灵活却无法支持多数据源的分布式事务。在数据库连接中使用本地事务示例如下:

public void transferAccount() {    Connection conn = null;     Statement stmt = null;     try{        conn = getDataSource().getConnection();        // 将自动提交设置为 false,若设置为 true 则数据库将会把每一次数据更新认定为一个事务并自动提交        conn.setAutoCommit(false);         stmt = conn.createStatement();        // 将 A 账户中的金额减少 500        stmt.execute("update t_account set amount = amount - 500 where account_id = 'A'");        // 将 B 账户中的金额增加 500        stmt.execute("update t_account set amount = amount + 500 where account_id = 'B'");        // 提交事务        conn.commit();        // 事务提交:转账的两步操作同时成功    } catch(SQLException sqle){        // 发生异常,回滚在本事务中的操做        conn.rollback();        // 事务回滚:转账的两步操作完全撤销        stmt.close();        conn.close();    }}
分布式事务

​ Java 事务编程接口(JTA:Java Transaction API)和 Java 事务服务 (JTS;Java Transaction Service) 为 J2EE 平台提供了分布式事务服务。分布式事务(Distributed Transaction)包括事务管理器( Transaction Manager ) 和一个或多个支持 XA 协议的资源管理器 ( Resource Manager )。我们可以将资源管理器看做任意类型的持久化数据存储;事务管理器承担着所有事务参与单元的协调与控制。

public void transferAccount() {    UserTransaction userTx = null;    Connection connA = null;     Statement stmtA = null;     Connection connB = null;     Statement stmtB = null;    try{        // 获得 Transaction 管理对象        userTx = (UserTransaction)getContext().lookup("java:comp/UserTransaction");         connA = getDataSourceA().getConnection();        // 从数据库 A 中取得数据库连接        connB = getDataSourceB().getConnection();        // 从数据库 B 中取得数据库连接        userTx.begin();         // 启动事务        stmtA = connA.createStatement();        // 将 A 账 户 中 的 金 额 减 少 500         stmtA.execute("update t_account set amount = amount - 500 where account_id = 'A'");        // 将 B 账户中的金额增加 500        stmtB = connB.createStatement();        stmtB.execute("update t_account set amount = amount + 500 where account_id = 'B'");         serTx.commit();// 提交事务        // 事务提交:转账的两步操作同时成功(数据库 A 和数据库 B 中的数据被同时更新)    } catch(SQLException sqle){        // 发生异常,回滚在本事务中的操纵        userTx.rollback();// 事务回滚:数据库 A 和数据库 B 中的数据更新被同时撤销    } catch(Exception ne){ }}
两阶段提交

​ 两阶段提交主要保证了分布式事务的原子性:即所有结点要么全做要么全不做,所谓的两个阶段是指:第一阶段:准备阶段;第二阶段:提交阶段
在这里插入图片描述

  • 准备阶段

    ​ 事务协调者(事务管理器)给每个参与者(资源管理器)发送 Prepare 消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的 redo 和 undo 日志,但不提交,到达一种“万事俱备,只欠东风”的状态。

  • 提交阶段

    ​ 如果协调者收到了参与者的失败消息或者超时,直接给每个参与者==发送回滚(Rollback)==消息;否则, 发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过 程中使用的锁资源。(注意:必须在最后阶段释放锁资源)

​ 将提交分成两阶段进行的目的很明确,就是尽可能晚地提交事务,让事务在提交前尽可能地完成所有能完成的工作。

Mybatis 缓存

​ Mybatis 中有一级缓存二级缓存默认情况下一级缓存是开启的,而且是不能关闭的。一级缓存是指 SqlSession 级别的缓存,当在同一个 SqlSession 中进行相同的 SQL 语句查询时,第二次以后的查询不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存 1024 条 SQL。二级缓存是指可以跨 SqlSession 的缓存。是 mapper 级别的缓存,对于 mapper 级别的缓存不同的sqlsession 是可以共享的。
在这里插入图片描述

Mybatis 的一级缓存原理(sqlsession 级别)

第一次发出一个查询 sql,sql 查询结果写入 sqlsession 的一级缓存中,缓存使用的数据结构是一个 map。

  • key:MapperID+offset+limit+Sql+所有的入参

  • value:用户信息

同一个 sqlsession 再次发出相同的 sql,就从缓存中取出数据。如果两次中间出现 commit 操作(修改、添加、删除),本 sqlsession 中的一级缓存区域全部清空,下次再去缓存中查询不到所以要从数据库查询,从数据库查询到再写入缓存。

二级缓存原理(mapper 基本)

​ 二级缓存的范围是mapper 级别(mapper 同一个命名空间),mapper 以命名空间为单位创建缓存数据结构,结构是map。mybatis 的二级缓存是通过 CacheExecutor 实现的。CacheExecutor其实是 Executor 的代理对象。所有的查询操作,在 CacheExecutor 中都会先匹配缓存中是否存在,不存在则查询数据库。

  • key:MapperID+offset+limit+Sql+所有的入参

具体使用需要配置:

  • Mybatis 全局配置中启用二级缓存配置

  • 在对应的 Mapper.xml 中配置 cache 节点

  • 在对应的select 查询节点中添加 useCache=true

微服务

服务注册发现

​ 服务器注册就是维护一个登记簿,它管理系统内系统内所有的服务地址。当新的服务启动后,它会向登记簿交待自己的地址信息,服务的依赖方直接向登记簿要Service Provider地址就行了。当下用于服务注册的工具非常多ZookeeperConsulEtcd还有Netflix的eureka等。服务注册有两种方式:客户端注册第三方注册

客户端注册(Zookeeper)

​ 客户端注册是服务自身要负责注册与注销的工作。当服务启动后向注册中心注册自身,当服务下线时注销自己。期间还需要和注册中心保持心跳。心跳不一定要客户端来做,也可以由注册中心负责(这个过程叫探活)。这种方式的缺点是注册工作与服务耦合在一起,不同语言都要实现一套注册逻辑。

在这里插入图片描述

第三方注册(独立的服务Registrar)

​ 第三方注册由一个独立的服务Registrar 负责注册与注销。当服务启动后以某种方式Registrar, 然后 Registrar 负责向注册中心发起注册工作。同时注册中心要维护与服务之间的心跳,当服务不可用时,向注册中心注销服务。这种方式的缺点是 Registrar 必须是一个高可用的系统,否则注册工作没法进展。
在这里插入图片描述

RabbitMQ

概念

​ RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现

​ AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。

RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。具体特点包括:

  • 可靠性(Reliability):RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。

  • 灵活的路由(Flexible Routing):在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。

  • 消息集群(Clustering):多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。

  • 高可用(Highly Available Queues):队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。

  • 多种协议(Multi-protocol):RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。

  • 多语言客户端(Many Clients):RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。

  • 管理界面(Management UI):RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。

  • 跟踪机制(Tracing):如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。

  • 插件机制(Plugin System):RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。

RabbitMQ 架构

在这里插入图片描述

Message

​ 消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。

Publisher
	`消息的生产者,也是一个向交换器发布消息的客户端应用程序。`
Exchange(将消息路由给队列 )

交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。

Binding(消息队列和交换器之间的关联)

绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。

Queue

消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。

Connection

网络连接,比如一个TCP 连接。

Channel

信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的 TCP 连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。

Consumer

消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。

Virtual Host

虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。

Broker

表示消息队列服务器实体。

Exchange类型

​ Exchange 分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键,此外 headers 交换器和direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接看另外三种类型:

Direct 键(routing key)分布:

Direct:消息中的路由键(routing key)如果和 Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中。它是完全匹配、单播的模式。

在这里插入图片描述

Fanout(广播分发)

Fanout:每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。

在这里插入图片描述

topic 交换器(模式匹配)

topic 交换器topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“”。#匹配 0 个或多个单词,匹配不多不少一个单词。

在这里插入图片描述

JAVA算法

二分查找

​ 又叫折半查找,要求待查找的序列有序。每次取中间位置的值与待查关键字比较,如果中间位置的值比待查关键字大,则在前半部分循环这个查找的过程,如果中间位置的值比待查关键字小, 则在后半部分循环这个查找的过程。直到查找到了为止,否则序列中没有待查的关键字

public static int biSearch(int []array,int a){     
	int left=0;    
	int right=array.length-1;     
	int mid;     
	while(left <= right){        
		mid = (left + right)/2;//中间位置        
		if(array[mid] == a){            
			return mid+1;        
		}else if(array[mid] < a){             
			//向右查找            
			left = mid + 1;        
		}else{ 
			//向左查找            
			right = mid - 1;        
		}   
	}    
	return -1;
}

冒泡排序算法

  • 比较前后相邻的二个数据,如果前面数据大于后面的数据,就将这二个数据交换。

  • 这样对数组的第 0 个数据到N-1 个数据进行一次遍历后,最大的一个数据就“沉”到数组第N-1 个位置。

  • N=N-1,如果N 不为 0 就重复前面二步,否则排序完成。

public static void bubbleSort1(int [] a, int n){     
	for(int i = 0; i < n; i++){        
		//表示 n 次排序过程。        
		for(int j = 1; j < n-i; j++){            
			if(a[j-1] > a[j]){                
				int temp;                
				temp = a[j-1];                 
				a[j-1] = a[j];                 
				a[j] = temp;            
			}        
		}    
	}
}

插入排序算法

public void sort(int arr[]) {    
	for(int i = 1; i<arr.length;i++) {        
	//插入的数        
	int insertVal = arr[i];       
	//被插入的位置(准备和前一个数比较)         
	int index = i-1;        
	//如果插入的数比被插入的数小        
	while(index >= 0 && insertVal < arr[index]) {            
		//将把arr[index] 向后移动            
		arr[index+1] = arr[index];            
		//让 index 向前移动            
		index--;        
	}        
	//把插入的数放入合适位置        
	arr[index+1] = insertVal;    
}}

快速排序算法

​ 快速排序的原理:选择一个关键值作为基准值。比基准值小的都在左边序列(一般是无序的), 比基准值大的都在右边(一般是无序的)。一般选择序列的第一个元素。

一次循环:从后往前比较,用基准值和最后一个值比较,如果比基准值小的交换位置,如果没有继续比较下一个,直到找到第一个比基准值小的值才交换。找到这个值之后,又从前往后开始比较,如果有比基准值大的,交换位置,如果没有继续比较下一个,直到找到第一个比基准值大的值才交换。直到从前往后的比较索引>从后往前比较的索引,结束第一次循环,此时,对于基准值来说,左右两边就是有序的了。

public void sort(int[] a,int low,int high) {    int start = low;    int end = high;    int key = a[low];     while(end>start) {        //从后往前比较        while(end>start&&a[end]>=key);        //如果没有比关键值小的,比较下一个,直到有比关键值小的交换位置,然后又从前往后比较        end--;        if(a[end]<=key){            int temp = a[end];            a[end] = a[start];            a[start] = temp;        }        //从前往后比较        while(end>start&&a[start]<=key)            //如果没有比关键值大的,比较下一个,直到有比关键值大的交换位置            start++;        if(a[start] >= key){            int temp = a[start];            a[start] = a[end];            a[end] = temp;\        }        //此时第一次循环比较结束,关键值的位置已经确定了。左边的值都比关键值小,右边的值都比关键值大,但是两边的顺序还有可能是不一样的,进行下面的递归调用    }    //递归    if(start>low)        sort(a,low,start-1);//左边序列。第一个索引位置到关键值索引-1     if(end<high)        sort(a,end+1,high);//右边序列。从关键值索引+1 到最后一个}                                         

在这里插入图片描述

希尔排序算法

基本思想:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

  • 操作方法:选择一个增量序列 t1,t2,…,tk,其中 ti>tj,tk=1;

  • 按增量序列个数k,对序列进行 k 趟排序;

  • 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度

在这里插入图片描述

public void shellSort(int[] a) {    int dk = a.length/2;     while( dk >= 1 ) {         ShellInsertSort(a, dk);        dk = dk/2;    }}private void ShellInsertSort(int[] a, int dk) {    //类似插入排序,只是插入排序增量是 1,这里增量是 dk,把 1 换成 dk 就可以了    for(int i=dk;i<a.length;i++){        if(a[i]<a[i-dk]){            int j;            int x=a[i];//x 为待插入元素            a[i]=a[i-dk];            for(j=i-dk; j>=0 && x<a[j];j=j-dk){                //通过循环,逐个后移一位找到要插入的位置。                a[j+dk]=a[j];            }            a[j+dk]=x;//插入        }    }}

数据库

存储引擎

概念

​ 数据库存储引擎是数据库底层软件组织,数据库管理系统(DBMS)使用数据库引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、所以技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。大多数数据库管理系统都支持多种不同的数据引擎。

​ 存储引擎主要有:Mylsam、InnoDB、Memory、Archive、Federated

InnoDB(B+树)

InnoDB 底层存储结构为B+树, B树的每个节点对应innodb 的一个page,page大小是固定的, 一般设为 16k。其中非叶子节点只有键值,叶子节点包含完成数据。
在这里插入图片描述

适用场景:

  • 经常更新的表,适合处理多重并发的更新请求。

  • 支持事务。

  • 可以从灾难中恢复(通过 bin-log 日志等)。

  • 外键约束。只有他支持外键。

  • 支持自动增加列属性auto_increment。

TokuDB(Fractal Tree-节点带数据)

​ TokuDB 底层存储结构为Fractal Tree,Fractal Tree 的结构与 B+树有些类似, 在 Fractal Tree 中,每一个 child 指针除了需要指向一个 child 节点外,还会带有一个 Message Buffer ,这个Message Buffer 是一个 FIFO 的队列,用来缓存更新操作。

例如,一次插入操作只需要落在某节点的 Message Buffer 就可以马上返回了,并不需要搜索到叶子节点。这些缓存的更新会在查询时或后台异步合并应用到对应的节点中

在这里插入图片描述

​ TokuDB 在线添加索引,不影响读写操作, 非常快的写入性能, Fractal-tree 在事务实现上有优势。 他主要适用于访问频率不高的数据或历史数据归档。

MyIASM

​ MyIASM 是MySQL 默认的引擎,但是它没有提供对数据库事务的支持,也不支持行级锁和外键, 因此当 INSERT(插入)或 UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。

ISAM 执行读取操作的速度很快,而且不占用大量的内存和存储资源。在设计之初就预想数据组织成有固定长度的记录,按顺序存储的。—ISAM 是一种静态索引结构。

缺点是它不 支持事务处理。

Memory

​ Memory(也叫 HEAP)堆内存:使用存在内存中的内容来创建表。每个 MEMORY 表只实际对应一个磁盘文件。MEMORY 类型的表访问非常得快,因为它的数据是放在内存中的,并且默认使用HASH 索引。但是一旦服务关闭,表中的数据就会丢失掉。 Memory 同时支持散列索引和 B 树索引,B 树索引可以使用部分查询和通配查询,也可以使用<,>和>=等操作符方便数据挖掘,散列索引相等的比较快但是对于范围的比较慢很多。

索引

​ 索引(Index)是帮助 MySQL 高效获取数据的数据结构。常见的查询算法,顺序查找,二分查找,二叉排序树查找,哈希散列法,分块查找,平衡多路搜索树B 树(B-tree)

常见的索引原则
  • 唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录。

  • 为经常需要排序、分组和联合操作的字段建立索引:

  • 为常作为查询条件的字段建立索引。

  • 限制索引的数目:

    越多的索引,会使更新表变得很浪费时间。

    尽量使用数据量少的索引

  • 如果索引的值很长,那么查询的速度会受到影响。

    尽量使用前缀来索引

  • 如果索引字段的值很长,最好使用值的前缀来索引。

  • 删除不再使用或者很少使用的索引

  • 最左前缀匹配原则,非常重要的原则。

  • 尽量选择区分度高的列作为索引

    区分度的公式是表示字段不重复的比例

  • 索引列不能参与计算,保持列“干净”:带函数的查询不参与索引。

  • 尽量的扩展索引,不要新建索引。

数据库三范式
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨_客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值