springboot学习笔记

第一章:第四讲:编写你的第一个Spring程序

1 如何通过spring.io去创建基础的spring项目框架
2 怎么运行一个简单的web程序

Terminal:curl http://localhost:8080/hello
3 spring-boot的maven打包工具的简单使用

Terminal:mvn clean package -Dmaven.test.skip(跳过测试)

Terminal:->target->java -jar helloworld-0.0.1-SNAPSHOT.jar(跳过测试)
4 在pom文件中如果不使用spring-boot自带的parent节点要怎么处理

java面试准备

1.HashMap 底层实现原理是什么

数组特点:

  • 存储区间是连续,且占用内存严重,空间复杂也很大,时间复杂为O(1)
  • 优点:随机读取效率很高,原因是数组是连续(随机访问性强,查找速度快)
  • 缺点:插入和删除数据效率低,因插入数据,这个数据后面的数据在内存中要往后移,且大小固定不易动态扩展

链表特点:

  • 区间离散,占用内存宽松,空间复杂度小,时间复杂度O(N)
  • 优点:插入删除速度快,内存利用率高,没有大小固定,扩展灵活
  • 缺点:不能随机查找,每次都是从第一个开始遍历(查找效率低)

哈希表特点:

  • 数组+(链表/红黑树)组成,既满足了数据的查找方便,同事不占用太多的空间内容,使用也十分方便
  • HashMap的两个重要参数:初始容量大小和加载因子。初始容量大小是创建时给数组分配的容量大小,默认值为16,用数组容量大小乘以加载因子得到的值,一旦数组中存储的元素个数超过该值就会调用rehash方法将数组容量增加到原来的两倍,及扩容。
  • 在扩容时会生成一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容操作非常消耗性能

2.Java 的多线程

3.线程池,以及实现固定大小线程池底层是如何实现的

4.Redis 为什么这么高效,使用的场景是什么

5.分布式服务

6.幂等概念

7.数据库如何实现 rollback 

8.TCP/IP 协议是如何保证数据可靠性的

一、java面试基础

1.JDK 和 JRE 有什么区别

JDK:Java Development Kit(java开发工具包),java语言编写的程序所需要的开发工具包,是提供给程序员使用的。JDK包含了JRE,同时还包含了编译Java源码的编译器Javac,还包含了很多Java程序调试和分析的工具。简单的说JDK是面向开发人员使用的SDK,它提供了Java的开发环境和运行环境,SDK(Software Development Kit,指软件开发包,可以包括函数库、编译程序等)。

JRE:Java Runtime Enviroment(java的运行环境,是面向java程序的使用者),包含了java虚拟机、java基础类库等。

2.== 和 equals 的区别是什么?

==:比较运算符,比较数值类型时,是比较它们的值;比较引用类型时,是比较两个变量指向的内存地址

equals():Object类的方法,默认实现是返回两个对象==的比较结果,但是需要注意的是equals()可以被重写

3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?

不对,在散列表中,hashCode()相等即两个键值对的哈希值相等,而哈希值相等,并不一定能得出键值对相等

hashCode():用于返回对象的哈希码值

4.final 在 java 中有什么作用?

final在Java中是修饰符关键字,final可以修饰变量、方法和类

final修饰的变量,这个变量是不可修改的。如果该变量是值变量,那么该变量的值不能修改;如果该变量是引用变量,那么不允许再给该变量赋值新的对象

final修饰方法,禁止子类继承的时候重写该方法;同时,final修饰后的方法执行速度会更快,因为在调用方法的时候,直接将方法插入到方法调用的位置。

5.java中math的常用方法

(1)取整

Math.floor():向下取整

Math.round():四舍五入取整

Math.ceil():向上取整

(2)java求绝对值

Math.abs()

(3)java随机数

Math.random():随机取0~1的数

(4)java幂函数

Math.pow(a,b):a的b次方

(5)java开根号

Math.sqrt()

6.String类是基本数据类型吗?

String类不是基本的数据类型,是final修饰的java类,java中的基本类型一共有8个

(1)字符类型:char

(2)整数类型:byte, short,int,long

(3)浮点型:float, double

(4)布尔类型:boolean

7.java 中操作字符串都有哪些类?它们之间有什么区别?

String、StringBuffer和StringBuilder:三个类都是以char[]的形式保存的字符串

(1)String:final修饰,字符串是不可变的,对String类型的字符串做修改操作相当于重新创建对象

(2)StringBuilder:可在原有对象的基础上进行操作,非线程安全,但是速度快,单线程环境下推荐使用

(3)StringBuffer:可在原有对象的基础上进行操作,线程安全,多线程环境下推荐使用

效率比较:StringBuilder>StringBuffer>String

8.java如何实现字符串反转

(1)StringBuilder或StringBuffer的reverse():new StringBuilder(str).reverse().toString()

(2)将字符串转换成字符数组,再倒叙拼接

(3)通过charAt(int index)返回char值再进行字符串拼接:reverse = s.charAt(i)+reverse

9.String 类的常用方法都有那些

(1)求字符串长度

String str = new String("abcdefg");

int strLength = str.length();

(2)求字符串某一位置字符

String str = new String("abcdefg");

char ch = str.charAt(1);

(3)提取子串

substring(int beginIndex);substring(int beginIndex, int endIndex);

(4)字符串比较

compareTo(), compareToIgnore() 忽略大小写

equals(),equalsIgnoreCase() 忽略大小写

(5)字符串拼接

concat():"aa".concat("bb");效果等价于"+"

(6)字符串中单个字符查找

indexOf(int ch/String str)

indexOf(int ch/String str, int fromIndex):从fromIndex位置向后查找

lastIndexOf(int ch/String str)

lastIndexOf(int ch/String str, int fromIndex):从fromIndex位置向前查找

(7)字符串中大小写转换

toUpperCase()

toLowerCase()

(8)字符串替换

replace(char oldChar, char newChar):返回新的字符串

replaceFirst(String regex, String replacement):返回新的字符串

replaceAll(String regex, String replacement):返回新的字符串

(9)其他类方法

trim():去除字符串两端的空格

startsWith(String prefix) ,endWith(String prefix):比较起始字符串或者终止字符串是否相同,返回boolean类型

contains(String str):判断参数是否被包含在字符串中,返回boolean类型

10.抽象类是否一定要有抽象方法

不一定。抽象类必须有关键字abstract来修饰。抽象类可以不包含有抽象方法,如果一个类包含抽象方法,则该类必须是抽象类。

11.普通类和抽象类有哪些区别

抽象类不能被实例化

抽象类可以有抽象方法,抽象方法只需申明,无需实现

含有抽象方法的类必须申明为抽象类

抽象类的子类必须实现抽象类中的所有抽象方法,否则这个子类也是抽象类

抽象方法不能被声明为静态,不能用private修饰,不能用final修饰

12.接口和抽象类有什么区别

一个类只能继承一个抽象类,但可以实现多个接口

抽象类中可以包含抽象方法和非抽象方法,而接口中的所有方法都试抽象的

子类继承抽象类必须实现抽象类中的所有抽象方法,否则子类也必须是抽象类;而子类实现接口则必须实现接口中的所有抽象方法

13.java 中 IO 流分为几种

按照流的流向划分,可以分为输入流和输出流

按照操作单元划分,可以分为字节流和字符流

按照流的角色划分,可以分为节点流和处理流

InputStream/Reader:所有的输入流的基类,前者是字节输入流,后者是字符输入流

OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流

14.BIO, NIO, AIO有什么区别

BIO:同步阻塞式IO,实现模型为一个连接就需要一个线程去处理,缺点是资源的浪费

NIO:同步非阻塞IO,有效请求时,才会使用一个线程去处理

AIO:异步非阻塞IO,也被称为NIO2.0,AIO在进行读写操作时,直接调用API的read和write方法即可,这两种均是异步的方法,且完成后会主动回调函数。

15.Files的常用方法都有哪些

Files.exists():检测文件路径是否存在

Files.createFile():创建文件

Files.createDirectory():创建文件夹

Files.delete():删除一个文件或目录

Files.copy():复制文件

Files.move():移动文件

Files.size():查看文件个数

Files.read():读取文件

Files.write():写入文件

二、容器

1.java容器都有哪些?

java容器分为Collection和Map两大类,其下又有很多子类:

Collection:一个独立元素的序列,这些元素都服从一条或多条规则

Map:一组成对的“键值对”对象,允许你是用键来查找值

2.Collection和Collections有什么区别?

Collection是一个接口,它是Set,List等容器的父接口;Collections是一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等

3.List,Set,Map之间的区别?

元素有序允许元素重复
List
SetAbstractSet
HashSet
TreeSet是(用二叉树排序)
MapAbstractMapkey值必须唯一,value可重复
HashMap
TreeMap是(用二叉树排序)

List的实现类(ArrayList,Vector,LinkedList):

ArrayList和Vector内部是线性动态数组结构,所以查询效率上会高很多,Vector是线程安全的,ArrayList是非线程安全的,所以性能会稍慢一些

LinkedList:是双向链表的数据结构存储数据,在做查询时会按照序号索引数据进行向前和向后遍历,查询效率偏低,但插入数据时只需要记录本项的前后项即可,所以插入速度快

4.HashMap和Hashtable的区别

存储:HashMap运行key和value为null,而Hashtable不允许;HashMap无论主键还是值都可以存放null,但是主键要求唯一,所以只能有一个null,Hashtable对null零容忍

线程安全:Hashtable是线程安全的,而HashMap是非线程安全的。推荐在单线程环境下使用HashMap,多线程使用ConcurrentHashMap

5.红黑树

红黑树是一种含有红黑结点并能自平衡的二叉查找树:

(1)每个结点要么是黑色,要么是红色

(2)根结点是黑色

(3)每个叶子结点(NIL)是黑色

(4)每个红色结点的两个子节点一定都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色结点)

(5)任意一结点到每个叶子结点的路径都包含数量相同的黑色结点(即一个结点存在黑子结点,那么该结点肯定有两个子结点)

6..如何决定使用 HashMap 还是 TreeMap

TreeMap的key值是要求实现java.lang,Comparable,所以迭代的时候TreeMap默认是按照Key值升序排序的;TreeMap的实现是基于红黑树结构,适用于按自然顺序或者自定义顺序遍历键。

HashMap的key值实现散列hashCode(),分布是散列的、不均匀的、不支持排序;数据结构主要是桶(数组),链表或红黑树,适用于在Map中插入、删除和定位元素。

7.如何实现数组和 List 之间的转换

List转换为数组:

ArrayList list = new ArrayList();

String[] strs = new String[list.size()];

list.toArray(strs);

数组转为list:

String[] s = {"a","b","c"};

List list = java.util.Arrays.asList(s);

8.在 Queue 中 poll()和 remove()有什么区别?

队列是一个典型的先进先出(FIFO)容器。队列的实现方式Queue: LinkedList PriorityQueue

(1)add()和offer()都是向队列中添加一个元素。但是如果想再一个满的队列中加入一个新元素,调用add()方法会抛出一个unchecked异常,而调用offer()方法会返回false

(2)peek()和element()都将在不移除的情况下返回队头,但是peek()方法在队列为空时返回null,调用element()方法会抛出NoSuchElementException异常

(3)poll()和remove()都将移除并且返回队头,但是poll()在队列为空时返回null,而remove()会抛出NoSuchElementException异常

9.哪些集合类是线程安全的?

java中线程安全的集合类有Stack(栈,继承于Vector)、Vector、Hashtable、ConcurrentHashMap(高效但是线程安全的集合)

java中的Stack类实现了基于后进先出(LIFO)原理的堆栈数据结构。

10.迭代器 Iterator 

Iterator(迭代器)不是一个集合,它是一种用于访问集合的方法,可以迭代ArrayList和HashSet集合

java.lang.Iterable接口被java.util.Collection接口继承,java.util.Collection接口的iterator()方法返回一个Iterator对象

next():获得集合中的下一个元素

hasNext():检查集合中是否还有元素

remove():将删除集合中Iterator指向位置后面的元素

forEachRemaining(Consumer<? super E> action):遍历所有元素

11.Iterator 和 ListIterator 有什么区别?

ListIterator迭代器包含的方法有

hasNext():以正向遍历列表时,如果列表迭代器后面还有元素,则返回true,否则返回false

next():返回列表中ListIterator指向位置后面的元素

hasPrevious():以逆向遍历列表,如果列表迭代器后面还有元素,则返回true,否则返回false

previous():返回列表中ListIterator指向位置前面的元素

nextIndex():返回列表中ListIterator所需位置后面元素的索引

previousIndex():返回列表中ListIterator所需位置前面元素的索引

remove():从列表中删除next()或previous()返回的最后一个元素(删除ListIterator指向位置后面的元素或者前面的元素)

set(E e):从列表中将next()或previous()返回的最后一个元素更改为指定元素e

add(E e):将指定元素插入列表,插入位置为迭代器当前位置之前

Iterator 和 ListIterator相同点:都是迭代器,当需要对集合中元素进行遍历切不需要干涉其遍历过程时,两种迭代器都可以使用

Iterator 和 ListIterator不同点:

(1)使用范围不同:Iterator可以应用于所有的集合,Set,List和Map和这些集合的子类型,而ListIterator只能应用于List及其子类型

(2)ListIterator有add方法,可以向List中添加对象,而Iterator不能

(2)ListIterator可以实现顺序向后遍历,还可以实现逆向(顺序向前)遍历

(3)ListIterator可以定位当前所有的位置

(4)都可以实现删除操作,但是ListIterator可以实现对象的修改,通过set()方法实现;Iterator仅能遍历,不能修改

12.怎么确保一个集合不能被修改?

(1)通过Collections.unmodifiableCollection(Collection c):Collections是工具类,在add、remove等修改方法中会抛出UnsupportedOperationException异常,将原本的集合的值copy了一份,定义为一个新的集合,而且是一个新类型的集合

(2)通过Arrays.asList创建的集合:Arrays.asList创建的ArrayList中没有重写其父类AbstractList的add、remove方法,所以不支持新增和删除,若果强行调用,会抛出UnsupportedOperationException异常

三、多线程

1.并行和并发有什么区别?

并发:一个处理器可以同时处理多个任务。这是逻辑上的同时发生(在单处理器和多处理器系统中都存在)

并行:多个处理器同时处理多个不同的任务。这是物理上的同时发生(在多处理器系统中存在)

2.线程和进程的区别

进程:正在进行的程序,进程是操作系统控制的基本运行单元

线程:进程中独立运行的子任务就是一个线程。如QQ.exe运行时的聊天线程,下载文件线程等

3.什么是守护线程以及作用

定义:当JVM中不存在任何一个正在运行的非守护线程时,则JVM进程即会退出

作用:守护线程拥有自动结束本身生命周期的特性,而非守护线程不具有这个特色,如垃圾回收线程

4.创建线程有哪几种方式

(1)继承Thread类创建线程类

a.定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了该线程要完成的任务

b.创建Thread子类的实例,即创建了线程对象

c.调用线程对象的start()方法来启动该线程

Public class ThreadTest extends Thread {
       int i = 0;
       // 重写run方法
       public void run() {
            for(; i < 100; i++) {
               System.out.println(getName() + " " + i);
            }
       }

       public static void main(String[] args) {
            for(int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " : " + i);
                if (i == 50) {
                    new ThreadTest().start();
                 }
            }
       }
}

(2)通过Runnable接口创建线程类

a.定义runnable接口的实现类,并重写该接口的run()方法

b.创建Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象

c.调用线程对象的start()方法来启动该线程

Public class RunnableThreadTest implements Runnable{
       private int i = 0;
       public void run() {
            for(; i < 100; i++) {
               System.out.println(Thread.currentThread.getName() + " " + i);
            }
       }

       public static void main(String[] args) {
            for(int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
                if (i == 50) {
                    RunnableThreadTest test = new RunnableThreadTest();
                    new Thread(test, "新线程1").start();
                    new Thread(test, "新线程2").start();
                 }
            }
       }
}

(3)通过Callable和Future创建线程

a.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值

b.创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值

c.使用FutureTask对象作为Thread对象的target创建并启动新线程

d.调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

public class CallableThreadTest implements Callable<Interger> {
  
     public static void main(String[] args) {
        CallableThreadTest test = new CallableThreadTest();
        FutureTask<Interger> ft = new FutureTask<>(test);
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " 的循环变量i的值 " + i);
            if (i == 50 ) {
               new Thread(ft, "有返回值的线程").start();    
            }
        }
        try { 
            System.out.println("子线程的返回值: " + ft.get());
        } catch (InterrupetedException e) {            
             e.printStackTrace();
        } catch (ExecutionException e) {     
             e.printStackTrace();
        }
   }

    @Override
    public Interger call() throws Exception {
          int i = 0;
          for(; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
          }
          return i;
    }
}

采用实现Runnable、Callable接口的方式创建多线程:

优势是:线程类只是实现了Runnable接口或Callable接口,还可以继承其它类;

               在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想

劣势:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法

使用继承Thread类的方式创建多线程:

优势:编写简单,如需要访问当前线程,直接使用this即可获得当前线程

劣势:线程类已经继承了Thread类,所以不能再继承其它父类

5.runnable 和 callable的区别

(1)Runnable接口run()方法无返回值;Callable接口call()方法有返回值,支持泛型

(2)Runnable接口run()方法只能抛出运行时异常,且无法捕获处理;Callable接口call()方法允许抛出异常,可以获取异常信息

6.线程有哪些状态

(1)新建状态(New):新创建了一个线程对象

(2)就绪状态(Runnable):线程对象创建后,其它线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待CPU的使用权

(3)运行状态(Running):就绪状态的线程获取了CPU,执行程序代码

(4)阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行,直到线程进入就绪状态,才有机会转到运行状态

a.等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中,进入这个状态后,不能自动唤醒,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒

b.同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中

c.其他阻塞;运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时时、join()等待线程终止或超时,或者I/O处理完毕时,线程重新转入就绪状态

(5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期

7.sleep() 和 wait() 有什么区别

sleep:线程类Thread定义的方法,表示线程休眠,会自动唤醒,但是调用sleep方法不会释放对象锁

wait:Object中定义的方法,需要调用notify()或者notifyAll()方法

8.notify()和 notifyAll()有什么区别

notify()和 notifyAll()都是用来唤醒调用wait()方法进入等待锁资源队列的线程,区别在于:

notify():唤醒正在等待此对象监视器的单个线程。如果有多个线程在等待,则选择其中一个随机唤醒(由调度器决定),唤醒的线程享有公平竞争资源的权利

notifyAll():唤醒正在等待此对象监视器的所有线程,唤醒的所有线程公平竞争资源

9.线程的 run()和 start()有什么区别

系统通过调用线程类的start()方法来启动一个线程,此时该线程处于就绪状态,等待调度,即就是这个线程可以被JVM来调度执行。在调度过程中,JVM底层通过调用线程类的run()方法来完成实际的操作,当run()方法结束后,此线程就会终止。

start()方法能够异步的调用run()方法,但是直接调用run()方法是同步的

10.创建线程池有哪几种方式

(1)newCachedThreadPool():用来处理大量短时间工作任务的线程池

特点:a.缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程

           b.线程闲置时间超过60s,则被终止并移除缓存

           c.长时间闲置时,这种线程池,不会消耗什么资源

           d.其内部使用SynchronousQueue作为工作队列

(2)newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程

特点:a.如果任务数量超过了活动线程数目,将在工作队列中等待空闲线程出现

           b.如果工作线程退出,将会有新的工作线程被创建,以补足指定数目nThreads

(3)newSingleThreadExecutor():单线程化的线程池

特点:所有的任务都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免改变线程数目

(4)newSingleThreadScheduledExector()和newScheduledThreadPool(int corePoolSize):创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程

(5)newWorkStealingPool(int parallelism):内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序

11.线程池都有哪些状态

(1)RUNNING:线程池的初始化状态。接收新的任务,处理等待队列中的任务。线程池一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0

(2)SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN

(3)STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN)-> STOP

(4)TIDYING:所有的任务都销毁了,workCount为0,线程池的状态在转换为TIDYING状态时,会执行钩子方法terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变成TIDYING时进行相应的处理,可以通过重载terminated()函数来实现

当线程池在SHUTDOWN状态下时,阻塞队列为空并且线程池中执行的任务也为空时,就会由SHUTDOWN -> TIDYING。

当线程池状态在STOP状态下时,线程池中执行的任务为空时,就会由STOP -> TIDYING。

(5)TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,就会由TIDYING -> TERMINATED。

12.线程池中 submit()和 execute()方法有什么区别

(1)submit(Callable<T> task)、submit(Runnable task, T result)、submit(Runnable task)归属于ExecutorService接口

(2)execute(Runnable command)归属于Executor接口。ExecutorService继承了Executor

(3)submit()有返回值

(4)execute()没有返回值

13.java程序中怎么保证多线程的运行安全

(1)线程的安全性问题体现在:

  • 原子性:一个或者多个操作在CPU执行的过程中不被中断的特性
  • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到
  • 有序性:程序执行的顺序按照代码的先后顺序执行

(2)导致原因

  • 缓存导致的可见性问题
  • 线程切换带来的原子性问题
  • 编译优化带来的有序性问题

(3)解决办法

  • JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
  • synchronized、volatile、LOCK,可以解决可见性问题
  • Happens-Before规则可以解决有序性问题

Happens-Before规则如下

  • 程序次序规则:在一个现场内,按照程序控制流顺序,书写在前面的操作先行发生于书写在后面的操作
  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生
  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始

14.多线程锁的升级原理是什么

锁的级别从低到高:

无锁--偏向锁--轻量级锁--重量级锁,这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级

* 没有优化以前,sychronized是重量级锁(悲观锁),使用wait和notify、notifyAll来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能

* 所以为了减少获得和缩放锁带来的性能消耗,JVM对sychronized关键字进行了优化,把锁分为无锁、偏向锁、轻量级锁、重量级锁状态

(1)无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功

(2)偏向锁:偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的

(3)轻量级锁:当锁时偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(做一点其他的事情休息一下)的形式尝试获取锁,线程不会阻塞,从而提高性能

(4)重量级锁:当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态

* 所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源,导致性能低下。

* 自旋锁:当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环

自旋锁是非公平的,无法满足等待时间最长的线程优先获取锁

自旋锁是非阻塞的,不会使线程状态发生切换,线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快

15.什么是死锁?产生死锁的原因及必要条件

(1)死锁:指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

(2)产生死锁的原因:

a.竞争资源

  • 可剥夺资源:是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源
  • 不可剥夺资源:当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等
  • 产生死锁中的竞争资源之一指的是竞争不可剥夺资源
  • 产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁

b.进程间推进顺序非法

  • 若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁
  • 例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1被P1占用而阻塞,于是发生进程死锁

(3)死锁产生的4个必要条件

  • 互斥条件:进程要求对所分配的资源进行排它姓控制,即在一段时间内某资源仅为一进程所占用
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放
  • 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链

16.怎么防止死锁

(1)预防死锁:

  • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了(破坏请求条件)
  • 只要有一个资源得不到分配,也不给这个进程分配其他的资源(破坏请保条件)
  • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
  • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

a.以确定的顺序获得锁

如果必须获取多个锁,那么在设计的时候需要充分考虑不同线程之间获得锁的顺序。按照上面的例子,两个线程获得锁的时序图如下:

线程A ------------->  锁住a ------------->  尝试锁住b ------------->  永久等待

线程B ------------->  锁住b ------------->  尝试锁住a ------------->  永久等待

如果此时把获得锁的时序改成如下,那么死锁永远也不会发生

线程A ------------->  锁住a ------------->  尝试锁住b ------------->  永久等待

线程B ------------->  锁住a ------------->  尝试锁住b ------------->  永久等待

b.超时放弃

当使用synchronized关键字提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。

线程A ------------->  锁住a ------------->  尝试锁住b ------------->  超时放弃 -------------> 结束

线程B ------------->  锁住b ------------->  尝试锁住a ------------->   超时放弃 -------------> 结束

(2)避免死锁

  • 预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源,因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。
  • 银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程直到同意该请求后系统状态仍然是安全的。

(3)检测死锁

  • 为每个进程和每个资源指定一个唯一的号码,建立资源分配表和进程等待表

(4)解除死锁

当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:

  • 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态
  • 撤销进程:可以直接撤销死锁进程或撤销代价最小的进程,直至有足够的资源可用,死锁状态消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等

(5)死锁检测

  • Jstack命令

jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前Java虚拟机内每一条线程正在执行的方法堆栈集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。

  • JConsole工具

Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面,而且本身占用的服务器内存很小,甚至可以说几乎不消耗。

17.ThreadLocal 是什么?有哪些使用场景

(1)ThreadLocal 是什么

  • Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离
  • 在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量,而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突
  • ThreadLocal在每个本地线程中创建了一个ThreadLocalMap对象,每个线程可以访问自己内部ThreadLocalMap对象里的value。通过这种方式,实现线程之间的数据隔离

(2)有哪些使用场景

经典场景:为每个线程分配一个JDBC连接的Connection。这样可以保证每个线程都在各自的Connection上进行数据库的操作,不会出现线程A关闭了线程B正在使用的Connection

18.synchronized及synchronized底层实现原理

(1)synchronized:synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

  • 修饰一个代码块:被修饰的代码块成为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
  • 修饰一个方法:被修饰的方法成为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  • 修饰一个静态的方法:其作用的范围是整个静态方法,作用的对象是这个类的所有对象
  • 修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象

synchronized(this)是对象锁,如果有多个对象就有相对应的多个锁(修饰一个代码块)

synchronized(类的名,class)是全局锁,不管有几个对象都共用一把锁(修饰一个类)

(2)synchronized底层实现原理

a.同步代码块底层实现:通过操作系统的两大指令monitorenter(获取锁)和monitorexit(释放锁、解锁)来回去锁的对象的监视器(monitor)

  • 执行同步代码块前要执行monitorenter指令,执行同步代码快后要执行monitorexit指令
  • 使用synchronized实现同步,关键点就是要获取对象的监视器monitor对象,当线程获取到monitor对象后,才可以执行同步代码块,否则只能等待
  • 同一时刻只有一个线程可以获取到该对象的monitor监视器(该特点使得代码块是同步的)
  • 通常一个monitorenter指令会同时对应多个monitorexit指令。(JVM要确保所获取的锁,无论是在正常执行情况或异常执行情况都能正常解锁,即便是有异常,也得先释放锁,再报异常)

b.同步方法底层实现

当使用synchronized标记方法时,字节码会出现访问标记(ACC_SYNCHRONIZED),该标记就表示在进入该方法时,JVM需要进行monitorenter操作。在退出该方法时,无论是否正常返回,JVM均需要进行monitorexit操作

c.总结同步的底层实现

无论是同步代码块还是同步方法,其底层实现都是应用了monitor机制,只不过,同步代码块是直接执行monitorenter与moniterexit,而同步方法是设置了一个ACC_SYNCHRONIZED访问标记,这个标记的底层还是monitor机制

(3)monitor机制:可以将monitor理解为每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针

  • 当JVM执行monitorenter时,如果目标对象monitor的计数器为0,表示此时该对象没有被其他线程所持有,此时JVM会将该锁对象的持有线程设置为当前线程,并将montor计数器+1
  • 当执行monitorexit时,JVM则需将锁对象的计数器-1。当计数器减为0时,那表示该锁已经被释放,并唤醒所有正在等待的线程去竞争该锁
  • monitor机制还具有可重入性

(4)synchronized锁的可重入性

  • 可重入性:在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,JVM可以将计数器再次+1(可重入锁),否则需要等待,知道持有线程释放该锁
  • 可重入性也可以用在父子类继承中,子类完全可以通过“可重入锁”调用父类的同步方法
  • 根据synchronized锁的可重入性可以证明:在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,一定可以得到锁(既然能进入到synchronized内部,肯定就已经拿到了当前对象的锁,当然可以访问该对象的其他同步方法)

(5)synchronized锁的是对象的原因

由于synchronized获取的是当前对象的监视器monitor,类中这么多的同步方法都是属于一个对象的,所以锁的是对象

(6)monitor机制的劣势

对象锁(monitor)机制是JDK1.6之前synchronized底层原理,又称为JDK1.6重量级锁,线程的阻塞以及唤醒均需要操作系统由用户态切换到内核态,开销非常之大,因此效率很低

19.synchronized和volatile的区别

(1)volatile:java提供的一种轻量级的同步机制。java语言包含两种内在的同步机制:同步块/方法和volatile变量,相比于synchronized(重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度,但是volatile变量的同步性较差,而且其使用也更容易出错

(2)作用

  • synchronized表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程
  • volatile表示变量在CPU的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。

(3)区别

  • synchronized可以作用于变量、方法、对象;volatile只能作用于变量
  • synchronized可以保证线程间的有序性、原子性和可见性;volatile只能保证可见性和有序性,无法保证原子性
  • synchronized线程阻塞,volatile线程不阻塞

20.synchronized 和 lock 有什么区别

(1)来源:synchronized是java的一个关键字,是内置的语言实现;lock是一个接口

(2)异常是否释放锁:synchronized在发生异常时会自动释放占有的锁,因此不会发生死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生(所以最后将同步代码用try catch包起来,finally中写入unlock,避免死锁的发生)

(3)是否响应中断:lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断

(4)是否知道获取锁:lock可以通过tryLock来知道有没有获取锁,而synchronized不能

(5)lock可以提高多个线程进行读操作的效率,可以通过ReadWritelock来实现读写分离

(6)性能:当竞争资源不激烈时,两者性能差不多;当竞争资源非常激烈时(即有大量线程同时竞争),此时lock的性能要远远优于synchronized

(7)synchronized使用Object对象本身的wait、notify、notifyAll调度机制,而lock可以使用Condition进行线程之间的调度

21.synchronized 和 ReentrantLock 区别是什么

(1)相同点:

  • 都是用来协调多线程对共享对象、变量的访问
  • 都是可重入锁,同一线程可以多次获得同一个锁
  • 都保证了可见性和互斥性

(2)不同点:ReentrantLock是Lock的实现类,是一个互斥的同步器

  • synchronized竞争锁时会一直等待;ReentrantLock可以尝试获取锁,并且得到获取结果
  • synchronized获取锁无法设置超时;ReentrantLock可以设置获取锁的超时时间
  • synchronized无法实现公平锁;ReentrantLock可以满足公平锁,即先等待先获取到锁
  • synchronized控制等待和唤醒需要结合加锁对象的wait()、notify()、notifyAll();ReentrantLock控制等待唤醒需要结合Condition的await()、signal()和signalAll()
  • synchronized是JVM层面实现的;ReentrantLock是JDK代码层面实现
  • synchronized在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock不会自动释放锁,需要在finally{}代码块显示释放

22.atomic 的原理

Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患。unsafe是java提供的获得对对象内存地址访问的类,它的作用就是在更新操作时提供“比较并替换”。

CAS并发原语现在在java语言中就是sun.misc.Unsafe类的各种方法,调用Unsafe类中的CAS方法,JVM会帮我们实现CAS汇编指令,这是一种完全依赖硬件的功能,通过它实现了原子操作,由于CAS是一种系统原语,原语属于操作系统用于范畴,由于诺干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU原子指令,不会造成所谓的数据不一致问题。

atomic原子性就是通过:自旋+CAS(乐观锁)

CAS相对于其他锁,不会进行内核态操作,有一些性能的提神,但同时引入自旋,当锁竞争较大的时候,自旋次数会增多,CPU资源会消耗很高。CAS+自旋适合使用在低并发并有同步数据的应用场景

四、反射

1.什么是反射

java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性并且能够改变它的属性;这种动态获取信息以及动态调用对象方法的功能成为java语言的反射机制

2.什么是 java 序列化?什么情况下需要序列化

序列化:将java对象转换成字节流的过程

反序列化:将字节流转换成java对象的过程

当java对象需要在网络上传输或者持久化存储到文件中时,就需要对java对象进行序列化处理

序列化的实现:类实现Serializable接口,这个接口没有需要实现的方法。实现Serializable接口是为了告诉JVM这个类的对象可以被序列化

  • 某个类可以被序列化,则其子类也可以被序列化
  • 声明为static何transient的成员变量,不能被序列化。static成员变量是描述类级别的属性,transient表示临时数
  • 反序列化读取序列化对象的顺序要保持住

3.动态代理是什么?有哪些应用?

(1)java中常见的代理有如下几种

  • 静态代理
  • 基于JDK的动态代理
  • 基于CGlib的动态代理
  • Spring的动态代理
  • 其他形式的动态代理

(2)静态代理

静态代理,即一个代理对应一个被代理对象

静态代理的实现模式:首先创建一个接口(JDK代理都是面向接口的),然后创建具体实现类来实现该接口,再创建一个代理类同样实现这个接口,不同之处在于:具体实现类的方法中需要将接口中定义的方法业务逻辑功能实现,而代理类中的方法只要调用具体类中的对应方法即可,这样我们在需要使用接口中的某个方法的功能时直接调用代理类的方法即可,将具体的实现类隐藏在底层

  • 定义总接口
    public interface Iuser {
        void eat(String s);
    }

  • 创建具体实现类
    public class UserImpl implements Iuser {
        @Override
        public void eat(String s) {
            System.out.println("我要吃"+s);
        }
    }

  • 创建代理类
    public class UserProxy implements Iuser {
        private Iuser user = new UserImpl();
        @Override
        public void eat(String s) {
            System.out.println("静态代理前置");
            user.eat(s);
            System.out.println("静态代理前后置");
        }
    }

  • 创建测试类
    public class ProxyTest {
        public static void main(String[] args){
            UserProxy proxy = new UserProxy();
            proxy.eat("苹果");
        }
    }

  • 运行结果如下

静态代理的缺点

a.当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式

  • 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
  • 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类

b.当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护

(2)动态代理

动态代理:在程序运行期间,创建目标对象的代理对象,并对目标对象中的方法进行功能性增强。

代理类在程序运行期间,创建的代理对象称之为动态代理对象。这种情况下,创建的代理对象,并不是实现在Java代码中定义好的,而是在运行期间,根据我们在动态代理对象中的“指示”,动态生成的。动态代理可以在不修改方法源码的情况下,增强被代理对象的方法的功能。

(3)JDK动态代理

JDK动态代理是由Java内部的反射机制来实现的,目标类基于统一的接口(InvocationHandler)

(4)CGLib动态代理

CGLib动态代理底层则是借助asm来实现的,CGLib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势

(5)具体代码实现

public interface UserService {
    public String getName(int id);
    public Integer getAge(int id);
}
public class UserServiceImpl implements UserService {
    @Override
    public String getName(int id) {
        System.out.println("------getName-----");
        return "Marry";
    }

    @Override
    public Integer getAge(int id) {
        System.out.println("------getAge-----");
        return 24;
    }
}
  • jdk动态代理实现如下
public class JDKProxy implements InvocationHandler {
    private Object target;
    /**
     * 绑定委托对象并返回一个代理类
     * @param target
     * @return
     */
    public Object bind(Object target){
        this.target = target;
        // 取得代理对象
        // 要绑定接口,这是一个缺陷,cglib弥补了这个缺陷
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if("getName".equals(method.getName())){
            System.out.println("------before"+method.getName()+"------");
            Object result = method.invoke(target,args);
            System.out.println("------after"+method.getName()+"------");
            return result;
        } else {
            Object result = method.invoke(target,args);
            return result;
        }
    }
}
public class JDKProxyTest {
    public static void main(String[] args){
        JDKProxy proxy = new JDKProxy();
        UserService userService = (UserService) proxy.bind(new UserServiceImpl());
        System.out.println(userService.getName(1));
        System.out.println(userService.getAge(1));
    }
}

  • cglib动态代理实现
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CGLIBProxy implements MethodInterceptor {
    private Object target;

    /**
     * 创建代理对象
     * @param target
     * @return
     * @throws Throwable
     */
    public Object getInstance(Object target){
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        // 回调方法
        enhancer.setCallback(this);
        // 创建代理对象
        return enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("+++++++++before"+methodProxy.getSuperName()+"++++++++");
        System.out.println(method.getName());
        Object result = methodProxy.invokeSuper(o,objects);
        System.out.println("+++++++++after"+methodProxy.getSuperName()+"++++++++");
        return result;
    }
}
public class CGLIBProxyTest {
    public static void main(String[] args){
        CGLIBProxy cglibProxy = new CGLIBProxy();
        UserService userService = (UserService) cglibProxy.getInstance(new UserServiceImpl());
        userService.getName(1);
        userService.getAge(1);
    }
}

(3) 动态代理的应用场景

  • 统计每个api的请求耗时
  • 统一的日志输出
  • 校验被调用的api是否已经登录和权限鉴定

(4)动态代理优点

代理逻辑与业务逻辑是互相独立的,没有耦合

五、对象拷贝

1.为什么要使用克隆

当想对一个对象进行处理,又想保留原有的数据进行接下来的操作,就需要克隆。

  • 浅克隆:指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象,浅拷贝对引用指向的修改会同时更改被拷贝对象
  • 深克隆:不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象,原对象与克隆对象之间100%分离

深克隆的方式有两种:

a.使用Cloneable接口

  • 克隆的类要实现Cloneable接口和重写Object的clone()方法
  • 先调用super.clone()方法克隆出一个新对象,然后手动给克隆出来的属性的对象的非基本数据类型的成员变量赋值

b.使用序列化:序列化深拷贝效率较低

  • 克隆对象实现Serializable接口。先对对象进行序列化,然后反序列化
  • 克隆对象的非基本数据类型成员也需要实现Serializable接口,否则会报错,成员无法被序列化

例1:浅克隆:对象仅包含基本变量

public class StudentSimple implements Cloneable{
    private int number;

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    @Override
    public Object clone(){
        StudentSimple stu = null;
        try {
            stu = (StudentSimple)super.clone();
        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return stu;
    }

}
public class SimpleCloneTest {
    public static void main(String[] args){
        StudentSimple stu1 = new StudentSimple();
        stu1.setNumber(123456);
        StudentSimple stu2 = (StudentSimple)stu1.clone();
        System.out.println("学生1:"+stu1.getNumber());
        System.out.println("学生1:"+stu2.getNumber());

        stu2.setNumber(654321);
        System.out.println("学生1:"+stu1.getNumber());
        System.out.println("学生1:"+stu2.getNumber());

        System.out.println(stu1==stu2);
    }
}

 例2:浅克隆:对象包含引用变量

public class Address {
    private String add;
    
    public String getAdd() {
        return add;
    }

    public void setAdd(String add) {
        this.add = add;
    }
}
public class StudentSimple implements Cloneable{
    private int number;
    private Address address;


    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    @Override
    public Object clone(){
        StudentSimple stu = null;
        try {
            stu = (StudentSimple)super.clone();
        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return stu;
    }

}
public class SimpleCloneTest {
    public static void main(String[] args){
        Address address = new Address();
        address.setAdd("武汉市");
        StudentSimple stu1 = new StudentSimple();
        stu1.setNumber(123456);
        stu1.setAddress(address);
        StudentSimple stu2 = (StudentSimple)stu1.clone();
        System.out.println("学生1:"+stu1.getNumber()+",地址:"+stu1.getAddress().getAdd());
        System.out.println("学生1:"+stu2.getNumber()+",地址:"+stu2.getAddress().getAdd());

        stu2.setNumber(654321);
        address.setAdd("黄石市");
        System.out.println("学生1:"+stu1.getNumber()+",地址:"+stu1.getAddress().getAdd());
        System.out.println("学生1:"+stu2.getNumber()+",地址:"+stu2.getAddress().getAdd());

        System.out.println(stu1==stu2);
    }
}

 例3:深克隆:克隆对象包含引用变量

public class Address implements Cloneable{
    private String add;

    public String getAdd() {
        return add;
    }

    public void setAdd(String add) {
        this.add = add;
    }

    @Override
    public Object clone(){
        Address address = null;
        try {
            address = (Address)super.clone();
        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return address;
    }
}
public class StudentSimple implements Cloneable{
    private int number;
    private Address address;


    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    @Override
    public Object clone(){
        StudentSimple stu = null;
        try {
            // 浅复制
            stu = (StudentSimple)super.clone();
        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        stu.address = (Address) address.clone();
        return stu;
    }

}
public class SimpleCloneTest {
    public static void main(String[] args){
        Address address = new Address();
        address.setAdd("武汉市");
        StudentSimple stu1 = new StudentSimple();
        stu1.setNumber(123456);
        stu1.setAddress(address);
        StudentSimple stu2 = (StudentSimple)stu1.clone();
        System.out.println("学生1:"+stu1.getNumber()+",地址:"+stu1.getAddress().getAdd());
        System.out.println("学生1:"+stu2.getNumber()+",地址:"+stu2.getAddress().getAdd());

        stu2.setNumber(654321);
        address.setAdd("黄石市");
        System.out.println("学生1:"+stu1.getNumber()+",地址:"+stu1.getAddress().getAdd());
        System.out.println("学生1:"+stu2.getNumber()+",地址:"+stu2.getAddress().getAdd());

        System.out.println(stu1==stu2);
    }
}

 例4:使用Serializable实现深拷贝

public class CloneUtil {
    private CloneUtil(){
        throw new AssertionError();
    }
    public static <T> T clone(T obj) throws Exception{
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bout);
        oos.writeObject(obj);

        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bin);
        return (T) ois.readObject();
    }
}
public class Car implements Serializable {
    private static final long serilaVersionUID =  -5713945027627603702L;
    private String brand;
    private int maxSpeed;
    public Car(String brand, int maxSpeed){
        this.brand = brand;
        this.maxSpeed = maxSpeed;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public int getMaxSpeed() {
        return maxSpeed;
    }

    public void setMaxSpeed(int maxSpeed) {
        this.maxSpeed = maxSpeed;
    }

    @Override
    public String toString(){
        return "Car [brand="+brand+", maxSpedd="+maxSpeed+"]";
    }
}
public class Person implements Serializable {
    private static final long serilaVersionUID =  -9102017020286042305L;
    private String name;
    private int age;
    private Car car;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Car getCar() {
        return car;
    }

    public void setCar(Car car) {
        this.car = car;
    }
    public Person(String name, int age, Car car) {
        this.name = name;
        this.age = age;
        this.car = car;
    }

    @Override
    public String toString(){
        return "Person [name="+name+", age"+age+", car="+car+"]";
    }
}
public class SerialCloneTest {
    public static void main(String[] args) {
        try{
            Person p1 = new Person("张三", 30, new Car("Benz", 300));
            // 深度克隆
            Person p2 = CloneUtil.clone(p1);
            p2.getCar().setBrand("BYD");
            System.out.println(p1);
            System.out.println(p2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

六、Java Web

1.session 和 cookie 的区别

  • 存储位置不同,cookie存放在客户端,session存放在服务端,session存储的数据比较安全
  • 存储的数据类型不同,两者都是key-value结构,但是cookie:value只能是字符串类型,session:value是Object类型
  • 存储的数据大小限制不同,cookie受浏览器小智,一般不能超过4kb,session理论上受当前内存的限制
  • 生命周期的控制不同,cookie的生命周期当浏览器关闭时,就消亡了;session一段时间未被访问时被清除

2.session的工作原理

客户端登录完成之后,服务器会创建对应的session,session创建完成之后,会把sessionid发送给客户端,客户端再存储到浏览器中,这样客户端每次访问服务器时,都会带着sessionid,服务器拿到sessionid之后,在内存找到与之对应的session,这样就可以正常工作

3.客户端禁止cookie,session还能用吗

一般默认情况下,在会话中,服务器存储session的sessionid是通过cookie存到浏览器里。如果浏览器禁用了cookie,浏览器请求服务器无法携带sessionid,服务器无法识别请求中的用户身份,session失效。

但是可以通过其他方法在禁用cookie的情况下,可以继续使用session:

  • 通过url重写,把sessionid作为参数追加在员url中,后续的浏览器与服务器交互中携带sessionid参数
  • 服务器的返回数据中包含sessionid,浏览器发送请求时,携带sessionid参数
  • 通过http协议其他header字段,服务器每次返回时设置该header字段信息,浏览器中Js读取该header字段,请求服务器时,js设置携带该header字段

4.如何避免sql注入

  • 采用预编译语句,杜绝sql拼接
  • 使用正则过滤传参

5.什么是XSS攻击?如何避免?

(1)XSS攻击:即跨站脚本攻击,它是Web程序中常见的漏洞。原理是攻击者往Web页面里插入恶意的脚本代码(css代码、Javascript代码等),当用户浏览该页面时,嵌入其中的脚本代码会被执行,从而达到恶意攻击用户的目的,如盗取用户cookie、破坏页面结构、重定向到其他网站等

(2)XSS防御的总体思路是:对输入(和URL参数)进行过滤,对输出进行编码

a.对输入和URL参数进行过滤(白名单和黑名单)

将容易导致XSS攻击的边角字符替换成全角字符;对于每一个输入,在客户端和服务器端还要进行各种验证,验证是否合法字符,长度是否合法,格式是否正确。

b.对输出进行编码

在输出数据之前对潜在的威胁的字符进行编码、转义是防御XSS攻击十分有效的措施

6.什么是CSRF攻击?如何避免?

(1)CSRF攻击:即跨站请求攻击,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品等)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

(2)预防措施

a.检查Referer字段

HTTP头中有一个Referer字段,这个字段用以标明请求来源于哪个地址。通常来说Referer字段应和请求的地址位于同一域名下,服务器可识别恶意的访问。

这种办法简单易行,但是有局限性,因其完全依赖浏览器发送正确的Referer字段,无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段,并且也存在攻击者攻击某些浏览器,篡改其Referer字段的可能

b.添加token校验

token最大特别就是随机性,不可预测

七、异常

1.throw和throws的区别

a.throws:跟在方法声明后面,后面跟的是异常类名

   throw:用在方法体内,后面跟的是异常类对象名

public static void method() throws ArithmeticException { 
 
      int a = 10;
      int b = 0;
      if (b == 0) {
         throw new ArithmeticException();
      } else {
         System.out.println(a/b);
      }
}
      

b.throws:可以跟多个异常类名,用逗号隔开

   throw:只能抛出一个异常对象名

c.throws:表示抛出异常,由该方法的调用者来处理

   throw:表示抛出异常,由该方法体内的语句来处理

d.throws:表示有出现异常的可能性,并不一定出现这些异常

   throw:抛出了异常,执行throw一定出现了某种异常

2.final,finally,finalize的区别

(1)final:用于声明属性、方法和类,分别表示属性不可交变,方法不可覆盖,类不可继承

(2)finally:异常处理语句结果的一部分,finally结构使代码总会被执行,而不管无异常发生。使用finally可以维护对象的内部状态,并可以清理非内存资源。特别是在关闭数据库连接这方面,将数据库连接的close()方法放到finally中,会大大降低程序出错的几率

finally语句块是在程序退出方法之前被执行的(先于return语句执行);finally语句块是在循环被跳过(continue)和中断(break)之前被执行的

(3)finalize:是一个方法,属于java.lang.Object类,finalize()方法是GC(garbagecollector)运行机制的一部分,finalize()方法是在GC清理它所从属的对象时被调用的,如果执行它的过程中抛出了无法捕获的异常(uncaughtexception,GC将终止对该对象的清理,并且该异常会被忽略;直到下一次GC开始清理这个对象时,它的finalize()会被再次调用

3.常见异常类有哪些

  • NullPointerException:当应用程序试图访问空对象时,则抛出异常
  • SQLException:提供关于数据库访问错误或其他错误信息的异常
  • IndexOutOfBoundsException:指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出
  • NumberFormatException:当应用程序试图将字符串转换成一种数值类型,但是该字符串不能转换为适当格式时,抛出该异常
  • FileNotFoundException:当试图打开指定路径名表示的文件失败时,抛出该异常
  • IOException:当发生某种I/O异常时,抛出此异常,此类是失败或者中断I/O操作生成的异常的通用类
  • ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出该异常
  • ArrayStoreException:试图将错误类型的对象存储到一个对象数组时抛出的异常
  • IIIegalArgumentException:抛出的异常表明向方法传递了一个不合法或不正确的参数
  • ArithmeticException:当出现异常的运算条件时,抛出此异常。如一个整数除以0时
  • NegativeArraySizeException:如果应用程序试图创建大小为负的数组时,抛出该异常
  • NoSuchMethodException:无法找到某一特定方法时,抛出该异常
  • SecurityException:由安全管理器抛出异常,指示存在安全侵犯
  • UnsupportedOperationException:当不支持请求的操作时,抛出该异常
  • RuntimeException:可能在Java虚拟机正常运行期间抛出的异常的超类

八、网络

1.http响应码301和302代表的是什么?有什么区别?

  • 301表示被请求url永久转移到新的url;302表示被请求url临时转移到新的url
  • 301搜索引擎会索引新url和新url页面的内容;302搜索引擎可能会索引旧的url和新的url的页面内容

2.forword和redirect的区别

forword和redirect是servlet的两种主要的跳转方式。forword又叫转发,redirect叫做重定向

a.从地址栏显示来说

forword是服务器内部的重定向,服务器直接访问目标地址的url网址,把里面的东西读取出来,但是客户端并不知道,因此用forword的话,客户端浏览器的网址是不会发生变化的

redirect:是服务器根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,所以地址栏显示的是新的地址

b.从数据共享来说

由于在整个定向的过程中用的是同一个request,因此forword会将request信息带到被重定向的jsp或servlet中使用,即可以共享数据

redirect不能共享

c.从运用的地方来说

forword一般用户用户登录的时候,根据角色转发到相应的模块

redirect一般用于用户注销登录时返回主页面或者跳转到其它网站

d.从效率来说

forword效率高,而redirect效率低

e.从本质来说

forword转发是服务器上的行为,而redirect重定向是客户端的行为

f.从请求的次数来说

forword只有一次请求,而redirect有两次请求

3.简述TCP和UDP的区别以及优缺点

TCP是面向连接的通讯协议,通过三次握手建立连接,通讯完成时四次挥手

优点:TCP在数据传输过程中,有保证数据可靠传输的机制,较为可靠

缺点:TCP相对于UDP传输速度慢,要求系统资源较多

UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口号信息

优点:UDP速度快、操作简单、要求系统资源较少,由于通讯不需要连接,可以实现广播发送

缺点:UDP传输数据前不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,没有保证数据可靠传输的机制

4.TCP为什么要三次握手?

(1)TCP三次握手

 第一次握手

客户端像服务器发送链接请求报文段。该报文段的头部中SYN=1,ACK=0,seq=x。请求发送后,客户端便进入SYN-SENT状态

  • SYN=1,ACK=0表示该报文段为连接请求报文
  • x为本次TCP通信的字节流的初始序号。TCP规定:SYN=1的报文段不能有数据部分,但要消耗掉一个序号

第二次握手

服务端收到连接请求报文段后,如果同意连接,则会发送一个应答:SYN=1,ACK=1,seq=y,ack=x+1。该应答发送完成后便进入SYN-RCVD状态

  • SYN=1,ACK=1表示该报文段为连接同意的应答报文
  • seq=y表示服务端作为发送者时,发送字节流的初始序号
  • ack=x+1表示服务端希望下一个数据报发送序号从x+1开始的字节

第三次握手

当客户端收到连接同意的应答后,还要向服务端发送一个确认报文段,表示:服务端发送来的连接同意应答已经成功收到。该报文段的头部为:ACK=1,seq=x+1,ack=y+1.

客户端发送完这个报文段后便进入ESTABLISHED状态。服务端收到这个应答后也进入ESTABLISHED状态,此时连接的建立完成

(2)为什么连接建立需要三次握手,而不是两次握手?

防止失效的连接请求报文段被服务端接收,从而产生错误

(3)TCP四次挥手:TCP连接的释放

 第一次挥手

若A认为数据发送完成,则它需要向B发送连接释放请求。该请求只有报文头,头中携带的主要参数为:FIN=1,seq=u。此时,A将进入FIN-WAIT-1状态

  • FIN=1表示该报文段是一个连接释放请求
  • seq=u,u-1是A向B发送的最后一个字节的序号

第二次挥手

B收到连接释放请求后,会通知相应的应用程序,告诉它A向B这个方向的连接已经释放,此时B进入CLOSE-WAIT状态,并向A发送连接释放的应答,其报文头包含:

ACK=1,seq=v,ack=u+1

  • ACK=1:除TCP连接请求报文段以外,TCP通信过程中所有数据报的ACK都为1,表示应答
  • seq=v,v-1是B向A发送的最后一个字节的序号
  • ack=u+1表示希望收到从第u+1个字节开始的报文段,并且已经成功接收了前u个字节

A收到该应答,进入FIN-WAIT-2状态,等待B发送释放连接请求

第二次挥手完成后,A到B方向的连接已经释放,B不会再接收数据,A也不会再发送数据,但是B到A方向的连接仍然存在,B可以继续向A发送数据

第三次挥手

当B向A发送完所有数据,向A发送连接释放请求,请求头:FIN=1,ACK=1,seq=w,ack=u+1。B便进入LAST-ACK状态

第四次挥手

A收到释放请求后,向B发送确认应答,此时A进入TIME-WAIT状态。该状态会持续2MSL时间,若该时间段内没有B的重发请求的话,就进入CLOSED状态,当B收到确认应答后,也便进入CLOSED状态,撤销TCB

5.get和post请求有哪些区别

  • get在浏览器回退时是无害的,而post会再次提交请求
  • get产生的URL地址可以被Bookmark,而post不可以
  • get请求会被浏览器主动cache,而post不会,除非手动设置
  • get请求只能进行url编码,而post支持多种编码方式
  • get请求参数会被完整保留在浏览器历史记录里,而post中的参数不会被保留
  • get请求在url中传送的参数是有长度限制的,而post没有
  • 对参数的数据类型,get只接受ASCII字符,而post没有限制
  • get没有post安全,因为get请求参数直接暴露在url上,所以不能用来传递敏感信息,post参数放在request body中

6.如何实现跨域

跨域:当浏览器执行脚本时会检查是否同源,只有同源的脚本才会执行,如果不同源即为跨域。

  • 同源指访问的协议、域名、端口都相同

如何实现跨域请求

a.jsonp

利用了<script>标签不受同源策略的限制

缺点:只能get方式,易受到XSS攻击

b.CORS(Cross-Origin Resource Sharing),跨域资源共享

当使用XMLHttpRequest发送请求时,如果浏览器发现违反了同源策略就会自动加上一个请求头origin;后端在接受到请求后确定响应后会在Response Headers中加入一个属性Access-Control-Allow-Origin;浏览器判断响应中的Access-Control-Allow-Origin值是否和当前地址相同,匹配成功后才继续处理,否则报错

缺点:忽略cookie,浏览器版本有一定要求

c.代理跨域请求

前端发送请求,经过代理,请求需要的服务器资源

缺点:需要额外的代理服务器

d.基于Html5 websocket协议

websocket是Html5一种新的协议,基于该协议可以做到浏览器与服务器全双工通信,允许跨域请求

缺点:浏览器有版本要求,服务器需要支持websocket协议

九、设计模式

1.常见的设计模式

(1)创建型模式

工厂模式、抽象工厂模式、单例模式、建造者模式

(2)结构型模式

适配器模式、装饰器模式、桥接模式、代理模式

(3)行为型模式

命令模式、迭代器模式、策略模式、观察者模式

2.六大原则

(1)开闭原则

对扩展开发,对修改关闭

(2)里氏代换原则

任何基类可以出现的地方,子类一定可以出现

(3)依赖倒转原则

依赖于抽象而不依赖于具体

(4)接口隔离原则

使用多个隔离的接口,比使用单个接口要好

(5)迪米特法则/最少知道原则

一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立

(6)合成复用原则

尽量使用合成/聚合的方式,而不是使用继承

十、Spring/Spring MVC

1.什么是AOP

AOP:Aspect Oriented Programming:面向切面编程:面向切面编程是面向对象中的一种方式,在代码执行过程中,动态嵌入其他代码,叫面向切面编程。

(1)AOP的作用及场景

利用AOP对业务逻辑的各个部分进行隔离降低业务逻辑的耦合性,提高程序的可重用型和开发效率。

常见的使用场景:日志、事务、数据库操作

面向切面编程,就是将交叉业务逻辑封装成切面,利用AOP的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的,也主业务逻辑无关的代码。

例:两条业务线:

开始-验证用户-取款-结束

开始-验证用户-查询余额-结束

(2)常见的AOP的实现方式

  • 前置通知:方法执行前调用,对应注解@Before
  • 后置通知:方法执行后调用,对应注解@After
  • 返回通知:方法返回后调用,对应注解@AfterReturning
  • 异常通知:方法出现异常调用,对应注解@AfterThrowing
  • 环绕通知:动态代理、手动推荐方法运行,对应注解@Around

2.什么是IOC

IOC:Inversion of Control:控制反转,它是面向对象编程中的一种设计原则。

采用IOC设计的最大好处就是降低代码之间的耦合,能够大幅减少代码编写量。

IOC不是具体的技术,而是一种思想。在传统的Java程序中是直接通过new关键字来创建对象,是程序主动去创建依赖的对象,而在IOC思想中,它将实现组件间的关系从程序内部提到外部容器,也就是说由容器在运行期间将组件间的某种依赖关系动态注入组件中。

3.Spring有哪些主要模块

Spring框架的七大模块

  • Spring Core:框架的最基础部分,提供IOC容器,对bean进行管理
  • Spring Context:基于bean,提供上下文信息,扩展出JNDI、EJB、电子邮件、国际化、校验和调度等功能
  • Spring DAO:提供了JDBC的抽象层,它可消除冗长的JDBC编码和解析数据库厂商特有的错误代码,还提供了声明性事务管理方法
  • Spring ORM:提供了常用的“对象/关系”映射APIs的集成层,其中包括JPA、JDO、Hibernate、MyBatis等
  • Spring AOP:提供了符合AOP Alliance规范的面向方面的编程实现
  • Spring Web:提供了基础的Web开发的上下文信息,可与其他web进行集成
  • Spring Web MVC:提供了web应用的Model-View-Controller全功能实现

4.Spring常用的注入方式

(1)xml中配置

(2)注解

5.Spring中的bean是线程安全的吗?

spring不保证bean的线程安全。默认spring容器中的bean是单例的,当单例中存在竞态条件,即有线程安全问题。

6.@Autowired注解

@Autowired:可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作,配合@Qualifier可以安装指定名称去装配bean

@Autowired和@Resource的区别

  • @Autowired默认按照byType自动装配,而@Resource默认byName自动装配
  • @Autowired只包含一个参数:required,表示是否开启自动准入,默认是true;而@Resource包含7个参数,最重要的两个参数是:name和type
  • @Autowired如果要使用byName,需要使用@Qualifier一起配合;而@Resoure如果指定了name,则使用byName自动装配,如果指定了type,则用byType自动装配
  • @Autowired能够用在:构造器、方法、参数、成员变量和注解上,而@Resource能用在:类、成员变量和方法上
  • @Autowired是spring定义的注解,而@Resource是JSR-250定义的注解

十一、Spring Boot/Spring Cloud

1.Spring Boot的核心配置文件

Spring Boot有两种类型的配置文件,application和bootstrap文件,文件格式为properties或yml格式

*.properties文件是key = value的形式;*.yml是key:value的形式

*.yml加载的属性是有顺序的,但不支持@PropertySource注解来导入配置,一般推荐用yml文件

bootstrap配置文件是系统级别的,用来加载外部配置,如配置中心的配置信息,也可以用来定义系统不会变化的属性,.bootstrap文件的加载优先于application文件;application配置文件是应用级别的,是当前应用的配置文件

2.JPA和Hibernate有什么区别

(1)概念

JPA(Java Persistence API):是EJB3规范中负责对象持久化的应用程序编程接口(ORM接口),它定义一系列的注释。这些注释大体可以分为:类级别注释、方法级别注释、字段级别注释。给实体类添加适当的注释可以在程序运行时告诉Hibernate如何将一个实体类保存到数据库中以及如何将数据库以对象的形式从数据库中读取出来

Hibernate:是ORM框架,是JPA的一个实现,其功能是JPA的超集

(2)关系

JPA是标准接口,Hibernate是实现

(3)Hibernate实现与JPA的关系

Hibernate主要通过三个组件来实现,hibernate-annotation、hibernate-entitymanager和hibernate-core

  • hibernate-annotation:是Hibernate支持annotation方式配置的基础,它包括了标准的JPA annotation以及Hibernate自身特殊功能的annotation
  • hibernate-core:是Hibernate的核心实现,提供了Hibernate所有的核心功能
  • hibernate-entitymanager:实现了标准的JPA,可以把它看成hibernate-core和JPA之间的适配器,它并不直接提供ORM功能,而是对hibernate-core进行封装,使得Hibernate符合JPA的规范

3.什么是Spring Cloud

(1)SOA:面向服务的架构

SOA是一个组件模型,它将应用程序的不同功能单元(称为服务)进行拆分,并通过这些服务之间定义良好的接口和契约联系起来。

如:将注册功能单独拆分,可在多个系统中进行调用

(2)微服务/微服务架构

微服务是一种架构模式,就是把一个系统中的各个功能点都拆开为一个个的小应用然后单独部署,同时因为这些小应用多,所以需要一些办法来管理这些小应用

微服务架构是SOA架构思想的一种扩展,更加强调服务个体的独立性、拆分粒度更小

(3)Spring Cloud

Spring Cloud是微服务框架的规范(是规范,部署具体的框架),它规定大概要有以下几种功能

  • 服务的注册与发现
  • 负载均衡
  • 服务熔断和限流
  • 智能路由
  • 控制总线
  • 链路监控
  • ...

4.Spring Cloud断路器

(1)产生原因

在微服务架构中,存在着多个微服务,彼此之间可能存在依赖关系,当某个单元出现故障或者网络不通时,就会因为依赖关系形成故障蔓延,最终导致整个系统的瘫痪,相当于传统架构更加不稳定。因此,断路器模式产生。

(2)作用

当某个微服务发生故障时,通过断路器的故障监控,向调用方返回一个错误响应,而不是长时间的等待,这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中蔓延

(3)实现

在Spring Cloud中使用了Hystrix来实现断路器的功能。Hystrix是Netflix的分布式套件之一,该框架目标在于通过控制那些访问远程系统、服务和第三方库的结点,从而对延迟和故障提供更强大的容错能力。Hystrix拥有回退机制和断路器功能的线程和信号隔离,请求缓存和请求打包,以及监控和配置等功能

(4)原理

断路器的三个重要参数

  • 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快找时间窗,默认为最近的10s
  • 请求总数下线:在快照时间窗内,必须满足请求总数下限才有资格熔断。默认为20,意味着在10s内,如果该hystrix命令的调用次数不足20,即使所有的请求都超时或者其它原因失败,断路器都不会打开
  • 错误百分比下限:当请求总数在快照时间窗内超过了下限,比如发生了30次调用,其中16次发生了超时异常,即超过了50错误百分比,在默认设定50%下限情况下,这时候就会将断路器打开

因此,断路器打开的条件是:在快找时间窗口期(默认10s)内,至少发生20次服务调用,并且服务调用错误率超过50%

断路器每隔一段时间进行一次重试,看看原来的主逻辑是否可用,可用就关闭,不可用就继续打开

5.Spring Cloud的核心组件

  • Eureka:各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其它服务在哪里
  • Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台
  • Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求url地址,发起请求
  • Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
  • Zuul:如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务

十二、Hibernate

1.Hibernate有几种查询方式

(1)HQL(Hibernate Query Language)

  • 语法类似sql
  • 把sql语句的表名换成了类名,把字段名换成实体类中的属性
  • 具有跨数据库的优点

(2)QBC(Query By Criteria)

这种方式比较面向对象方式,重点是有三个描述条件的对象:Restrictions, Order, Projections。使用QBC查询,一般需要以下三个步骤(“DetachedCriteria离线查询”不同)

  • 使用Session实例的createCriteria(xxx.class)方法创建Criteria对象
  • 使用工具类Restriction的方法为Criteria对象设置查询条件,Order工具类的方法设置排序方式,Projections工具类的方法进行统计和分组
  • 使用Criteria对象的list()方法进行查询并返回结果

(3)SQL

违背了hibernate的跨平台优点,不易维护,不面向对象。不推荐使用

2.hibernate实体类可以被定义为final吗?

实体类可以定义为final类,但是如此就不能使用hibernate代理模式下的延迟关联提供性能了,所以不建议定义实体类为final

3.在hibernate中使用Integer和int做映射有什么区别

Integer类型为对象,它的值允许为null,而int属于基础数据类型,值不能为null

原始类型:boolean, char, byte, short, int, long, float, double

包装类型:Boolean, Character, Byte, Short, Integer, Long, Float, Double

Integer是int的包装类型,int的初始值为0,Integer的初始值为null

4.get()和load()的区别

(1)返回值

get()返回的是查询出来的实体对象;load()查询出来的是一个目标实体的代理对象

(2)查询时机

get()在调用的时候就立即发出SQL语句查询;load()在访问非id属性的时候才会发出查询语句并且将被代理对象target填充上,但是如果这个动作发生在session被关闭后的话就会抛出LazyInitializationException异常

(3)查询结果为空时

get()抛出NullPointerException异常;load()抛出ObjectNotFoundException异常

5.Hibernate中openSession()与getCurrentSession()的区别与联系

SessionFactory接口获得Session(会话)实例有两种方式,一种是通过openSession(),另一种是通过getCurrentSession()

(1)区别

  • openSession()是获取一个新的session;getCurrentSession()是获取和当前线程绑定的session
  • 通过getCurrentSession()获取的session在commit或rollback时会自动关闭;通过openSession()获取的session则必须手动关闭
  • 通过getCurrentSession()获取session进行查询需要事务提交;通过openSession()进行查询时可以不用事务提交

(2)联系

在SessionFactory启动的时候,Hibernate会根据配置创建相应的CurrentSessionContext,在getCurrentSession()被调用的时候,实际被执行的方法是CurrentSessionContext.currentSession()。在currentSession()执行时,如果当前Session为空,currentSession会调用SessionFactory的openSession。所以getCurrentSession()对于Java EE来说是更好的获取Session的方法

6.Hibernate实体类必须要有无参数构造函数吗?为什么?

hibernate中每个实体类必须提供一个无参构造函数,因为hibernate框架要使用reflection api,通过调用ClassnewInstance()来创建实体类的实例,如果没有无参的构造函数就会抛出异常

十三、Mybatis

1.mybatis中#{}和${}的区别

(1)#{}将传入的数据都当成一个字符串,会自动对传入的数据加一个双引号;${}将传入的数据直接显示生成在sql中

例:where uername = #{username},如果传入的值是111,解析后where uername = "111";

       where uername = ${username},如果传入的值是111,解析后where uername = 111;

(2)#方式能够很大程度防止sql注入,$方式无法防止sql注入

(3)$方式一般用于传入数据库对象,例如传入表名和列名,还有排序时使用order by动态参数时需要使用$,ORDER BY ${username}

2.mybatis有几种分页方式

(1)借助数组进行分页

原理:进行数据库查询操作时,获取到数据库中所有满足条件的记录,保存在应用的临时数组中,再通过List的subList方法,获取到满足条件的记录

缺点:数据库查询并返回所有的数据,而我们需要的只是极少符合要求的数据。当数据量少时,可以接受;当数据量过大时,每次查询对数据库和程序的性能都会产生极大的影响

(2)借助sql语句进行分页

sql语句:select * from table limit index, pageSize;

缺点:不方便统一管理,维护性较差

(3)拦截器分页

原理:拦截StatementHandler接口的prepare方法,然后再拦截器方法中把sql语句改为对应的分页查询sql语句,之后再调用StatementHandler对象的prepare方法,即调用invocation.proceed().

(4)RowBounds实现分页

原理:通过RowBounds实现分页和通过数组方式分页原理差不多,都是一次获取所有符合条件的数据,然后在内存中对大数据进行操作,实现分页效果。

这是一种简单的分页方式,只需在dao层接口中要实现分页的方法中加入RowBounds参数,然后在service层传入参数构建RowBounds对象:new RowBounds(start, limit)

存在问题:一次性从数据库获取的数据可能会很多,对内存的消耗很大,可能导致性能变差,甚至引发内存溢出

适应场景:在数据量很大的情况下,建议还是使用拦截器实现分页效果;RowBounds建议在数据量相对较小的情况下使用

3.mybatis有哪些执行器

  • SimpleExecutor:每执行一次update和select,就开启一个Statement对象,用完立刻关闭Statement对象
  • ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,可重复使用Statement对象
  • BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。

(1)使用场景

Executor这些特点,都严格限制在SqlSession生命周期范围内。Mybatis的默认执行器是SimpleExector,需要配置在创建SqlSeesion对象的时候指定执行器的类型即可。ReuseExectuor和BatchExecutor使用特点场景的sql执行使用,但是必须自己维护Statement对象,执行SqlSession的flushStatements来清除缓存不推荐使用。而CachingExecutor通常来说会配合redis等第三方存储来实现分布式二级缓存

(3)Mybatis如何指定使用哪一种Executor执行器

在mybatis配置文件中,可以指定默认的ExecutorType执行器类型,也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数

十四、MySql

1.数据库的三范式

(1)第一范式(1NF)

指在关系模型中,对于添加的一个规范要求,所有的域都应该是原子性的,即数据库表的每一列都是不可分割的原子数据项,而不能是集合,数组,记录等非原子数据项

在任何一个关系数据库中,第一范式是对关系模式的设计基本要求,一般设计中都必须满足第一范式

(2)第二范式(2NF)

满足第二范式必须先满足第一范式,第二范式要求实体的属性完全依赖于主关键字,即第二范式是在第一范式的基础上属性完全依赖于主键。

例:(职工号,姓名,职称,项目号,项目名称)=》

(职工号,姓名,职称)(项目号,项目名称)

(3)第三范式(3NF)

在第二范式的基础上,任何非主属性不依赖于其它非主属性(在第二范式基础上消除传递依赖),即任何非主属性不得传递依赖于主属性

例:(学号,姓名,年龄,性别,所在院校,院校地址,院校电话)=》

(学号,姓名,年龄,性别,所在院校)--(所在院校,院校地址,院校电话)

2.说一下ACID

Atomicity(原子性):一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即事务不可分割,不可约简

Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束,触发器,级联回滚等

Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。数据隔离分为不同级别,包括读未提交、读提交、可重复读和串行化

Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失

3.char和varchar的区别

(1)char

char(n):固定长度类型,例订阅char(10),当输入“abc”三个字符时,它们占的空间还是10个字节,其他7个是空字节

优点:效率高;缺点:占用空间

适用场景:存储密码的md5值,固定长度的,使用char非常合适

(2)varchar

varchar(n):可变长度,存储的值是每个值占用的字节再加上一个用来记录其长度的字节的长度

空间上:varchar比char合适;效率上:char比varchar合适

4.mysql的内连接、左连接和右连接有什么区别

(1)内连接:inner join

结合两张表的记录,返回相关的查询结果,返回的是两张表的交集部分

(2)左连接/左外连接:left join

左连接查询,左表的信息全部展示出来,右表只会展示符合搜索条件的信息,不足的地方记为NULL

(3)右连接/右外连接:right join

右连接查询,右表的信息全部展示出来,左表只会展示符合搜索条件的信息,不足的地方记为NULL

5.mysql索引如何实现

(1)索引的优缺点

优势:可以快速检索,减少I/O次数,加快检索速度;根据索引分组和排序,可以加快分组和排序

劣势:索引本身也是表,因此会占用存储空间,一般来说,索引表占用的空间是数据表的1.5倍;索引表的维护和创建需要时间成本,这个成本随着数据量增大而增大;构建索引会降低数据表的修改操作(删除、添加、修改)的效率,因为在修改数据表的同时还需要修改索引表

(2)索引的分类

常见的索引类型有:主键索引、唯一索引、普通索引、全文索引、组合索引

a.主键索引:根据主键建立索引,不允许重复,不允许空值

ALTER TABLE 'table_name' ADD PRIMARY KEY ('col');

b.唯一索引:用来建立索引的列的值必须是唯一的,允许空值

ALTER TABLE 'table_name' ADD UNIQUE index_name('col');

c.普通索引:用表中的普通列构建的索引,没有任何限制

ALTER TABLE 'table_name' ADD INDEX index_name('col');

 d.全文索引:用大文本对象的列构建的索引

ALTER TABLE 'table_name' ADD FULLTEXT INDEX ft_index('col');

e.组合索引:用多个列组合构建的索引,这多个列中的值不允许有空值

ALTER TABLE 'table_name' ADD INDEX index_name('col1','col2','col3');

(3)索引的实现原理

MySQL支持诸多存储引擎,而各种存储引擎对索引的支持也各不相同,因此MySQL数据库支持多种索引类型,如BTress索引,B+Tree索引,哈希索引,全文索引等等

a.哈希索引

只有memory(内存)存储引擎支持哈希索引,哈希索引引用索引列的值计算该值的hashCode,然后在hashCode相应的位置存执该值所在行数据的物理位置,因为使用散列算法,因此访问速度非常快,但是一个值只能对应一个hashCode,而且是散列的分布方式,因此哈希索引不支持范围查找和排序功能

b.全文索引

FULLTEXT(全文)索引,仅可用于MyISAM和InnoDB,针对较大的数据,生成全文索引非常的消耗时间和空间,对于文本的大对象,或者较大的CHAR类型的数据,如果使用普通索引,那么匹配前几个字符还是可行的,但是想要匹配文本中间的几个单词,就要使用LIKE%word%来匹配,这样需要很长的时间来处理,响应时间会大大增加,这种情况,就可以使用FULLTEXT索引了,在生成FULLTEXT索引时,会为文本生成一份单词的清单,在索引时即根据这个单词的清单来索引。FULLTEXT可以在创建表的时候创建,也可以在需要的时候用ALTER或者CREATE INDEX来添加

c.BTree索引和B+Tree索引

  • BTree

对于一棵m阶B-tree,每个结点至多可以拥有m个子节点。各结点的关键字和可以拥有的子结点数都有限制,规定m阶B-tree中,根结点至少有2个子节点,除非根结点为叶子结点,相应的,根结点中关键字的个数为1~m-1;非根结点至少有[m/2]([]向上取整)个子结点,相应的,关键字个数为[m/2]-1~m-1

  • B+Tree

对于B+tree,其结构与B-tree相同,不同的是各结点的关键字和可以拥有的子结点数。如m阶B+tree中,每个结点至多可以拥有m个子节点,非根结点至少有[m/2]个子结点,而关键字个数比B_tree多一个,为[m/2]~m

 

  • B+Tree对比BTree的优点

磁盘读写代价更低:一般来说B+tree比Btree更适合实现外存的索引结构,因为存储引擎的设计专家巧妙的利用了外存(磁盘)的存储结构,即磁盘的最小存储单位是扇区(sector),而操作系统的块(block)通常是整数倍的sector,操作系统以页(page)为单位管理内存,一页通常默认为4k,数据库的页通常设置为操作系统页的整数倍,因此索引结构的节点被设计为一个页的大小,然后利用外存的“预读取”原则,每次读取的时候,把整个节点的数据读取到内存中,然后在内存中查找,已知内存的读取速度是外存读取I/O速度的几百倍,那么提升查找速度的关键就在于尽可能的减少磁盘I/O,那么可以知道,每个节点中的Key个数越多,那么树的高度越小,需要I/O的次数越少,因为一般来说B+tree比Btree更快,因为B+tree的非叶子结点中不存储data,就可以存储更多的key

  • d.MyISAM--非聚簇索引

MyISAM存储引擎采用的是非聚簇索引,非聚簇索引的主索引(主键键值建立的索引)和辅助索引(非主索引)几乎是一样的,只是主索引不允许重复,不允许控制,他们的叶子结点的key都存储指向键值对应的数据的物理地址

非聚簇索引的数据表和索引表是分开存储的

非聚簇索引中的数据是根据数据的插入顺序保存,因此非聚簇索引更适合当个数据的查询,插入的顺序不受键值影响

e.InnoDB--聚簇索引

聚簇索引的主索引的叶子结点存储的是键值对应的数据本身,辅助索引的叶子结点存储的是键值对应的数据的主键键值。因此主键的值长度越小越好,类型越简单越好

聚簇索引的数据和主键索引存储在一起

聚簇索引的数据是根据主键的顺序保存。因此适合按主键索引的区间查找,可以有更少的磁盘I/O,加快查询速度。但是也因为这个原因,聚簇索引的插入顺序最后安装主键单调的顺序插入,否则会频繁的引起页分裂,严重影响性能

在InnoDB中,如果只需要查找索引的列,就进了不要加入其它的列,这样会提高查询效率

***使用主索引的时候,更适合使用聚簇索引,因为聚簇索引只需要查找一次,而非聚簇索引在查到数据的地址后,还要进行一次I/O查找数据

***因为聚簇辅助索引存储的是主键的键值,因此可以在数据行移动或者页分裂的时候降低成本,因为这时不用维护辅助索引。但是由于索引存储的是数据本身,因此聚簇索引会占用更多的空间

***聚簇索引在插入新数据时比非聚簇索引慢得多,因为插入新数据时需要检测主键是否重复,这需要遍历主索引的所有叶子节点,而非聚簇索引的叶子节点保存的是数据地址,占用的空间少,因此分布集中,查询的时候I/O更少,但聚簇索引的主索引中存储的是数据本身,数据占用空间大,分布范围更大,可能占用好多的扇区,因此需要多次I/O才能遍历完毕

 (5)索引失效的情况

  • 在组合索引中不能有列的值为NULL,如果有,那么这一列组合索引时是无效的
  • 在一个SELECT语句中,索引只能使用一次,如果在WHERE中使用了,那么在OEDER BY中就不要使用
  • LIKE操作中,‘%aaaa%’索引会失效,但是‘aaa%’可以使用索引
  • 在索引的列上使用表达式或者函数会使索引失效

6.乐观锁与悲观锁

(1)悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完再把资源转让给其它线程)

传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁

(2)乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。

乐观锁适用于多读的应用类型,这样可以提高吞吐量。

a.版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会+1,。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功

b.CAS算法

compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步。

CAS算法涉及到三个操作数

  • 需要读写的内存值V
  • 进行比较的值A
  • 拟写入的新值B

当且仅当V的值等于A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断地重试

c.乐观锁的缺点

ABA问题:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍是A值,但这段时间它的值有可能被改为其它值又改回A值,那么CAS操作会误认为它从来没有被修改过,即CAS操作的“ABA”问题

解决办法:AtomicStampedReference类的compareAndSet方法,首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值

循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销

只能保证一个共享变量的原子操作:CAS只对单个共享变量有效,当操作设计跨多个共享变量时CAS无效。但是AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作,可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作

7.MySQL问题查询有哪些手段

  • 使用show processlist命令查看当前所有连接信息
  • 使用explain命令查询SQL语句执行计划
  • 开启慢查询日志,查看慢查询的SQL

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值