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:一组成对的“键值对”对象,允许你是用键来查找值

springboot学习笔记_第1张图片

springboot学习笔记_第2张图片

2.Collection和Collections有什么区别?

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

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

元素有序 允许元素重复
List
Set AbstractSet
HashSet
TreeSet 是(用二叉树排序)
Map AbstractMap key值必须唯一,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 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 {
  
     public static void main(String[] args) {
        CallableThreadTest test = new CallableThreadTest();
        FutureTask 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 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("苹果");
        }
    }

  • 运行结果如下

springboot学习笔记_第3张图片

静态代理的缺点

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

springboot学习笔记_第4张图片

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

springboot学习笔记_第5张图片

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

springboot学习笔记_第6张图片

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

springboot学习笔记_第7张图片

 例4:使用Serializable实现深拷贝

public class CloneUtil {
    private CloneUtil(){
        throw new AssertionError();
    }
    public static  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();
        }
    }
}

springboot学习笔记_第8张图片

六、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三次握手springboot学习笔记_第9张图片

 第一次握手

客户端像服务器发送链接请求报文段。该报文段的头部中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连接的释放

springboot学习笔记_第10张图片

 第一次挥手

若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

利用了

你可能感兴趣的:(java)