java八股文
java基础
1.深拷贝、浅拷贝
浅拷贝
1)拷贝构造方法实现浅拷贝
假如一个Person类里有两个代表性的属性值,一个是引用传递类型,另一个是字串发类型(属于常量),通过拷贝构造方法进行浅拷贝,
P1值传递部分得属性值发生变化,P2不会随之改变
P1引用传递部分属性值发生变化,P2也随之改变
2)通过重写clone()方法浅拷贝
浅拷贝属于Object类中的,Protected Object clone()方法,是受保护的,无法直接使用,需要这个类实现Cloneable接口,否则会抛出异常,重写clone()方法,通过super.clone调用原clone()方法
结果如 1)一样
基本数据类型是值传递,所以修改值后不会影响另一个对象的该属性值;
引用数据类型是地址传递(引用传递),所以修改值后另一个对象的该属性值会同步被修改。
深拷贝
深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。花销更大
1)通过重写clone方法来实现深拷贝
与浅拷贝思路一样
进行了深拷贝之后,无论是什么类型的属性值的修改,都不会影响另一个对象的属性值。
反射机制
Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键
反射机制采用动态加载,通过三种方法获得class
①Object类的getClass()方法
②类的静态属性
③Class类的静态方法
获得class后,可以调用class对象的newInstance()来创建class对象的实例,或者通过类构造器
重写和重载的区别
重载是同一个方法能够根据输入数据的不同,做出不同的处理,同一个方法名传入不同的参数。
重写是当子类继承父类的相同方法,输入数据一样,但要做出有别于父类的相应时,你就要覆盖父类的方法。
java三大特性
封装、继承、多态
封装
将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。
好处:
1.只能通过规定的方法访问数据。
2.隐藏该类的实例细节,方便修改和实现。
继承
继承是类与类的一种关系,是一种”is a”的关系。比如“狗”继承“动物”,这里动物类是狗类的父类或者基类,狗类是动物类的子类或者派生类。
好处:
子类拥有父类的所有属性和方法(除了Private修饰的属性不能拥有)从而实现了代码的复用;
多态
多态是对象的多种形态。
Java中里的多态主要表现在两个方面:
引用多态:
父类的引用指向本类的对象;
父类的引用可以指向子类的对象;
方法多态:
当我们父类的引用指向不同的子对象时,他们调用的方法也是多态的。
创建本类对象时,调用的方法为本类方法;
创建子类对象时,调用的方法为子类重写的方法或者继承的方法;
但是多态使用的时候应该注意:如果子类编写了一个父类不存在的方法,那么就不能通过父类的引用来调用这个方法。
注意:继承是多态的基础
抽象类和接口区别
语法层面上的区别
1.一个类只能继承一个抽象类,而一个类却可以实现多个接口。
2.抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;所以不能实现类中不能重新定义,也不能改变其值;而抽象类成员变量是frendly,其值可以在子类中修改。
3.抽象方法中可以有非抽象方法,接口中则只能有default,static办法(JDK1.8)。
4.接口中可以省略abstract,抽象类是不能省略的。
5.接口中不能含有静态代码块,而抽象类可以有静态代码块;
设计层面上的区别
抽象类是对类的一种抽象,而接口是对行为的抽象。比如飞机和鸟,他们都会飞,我们会把飞机设计为一个类,鸟会设计一个类,但是不会把飞行设计为一个类,只会把飞行设计为一个接口,然后类去实现自己的飞行方式。其他不能飞的东西就不能实现这个接口。
==与equals(重点)
==
它的作用是判断两个对象的地址是否相等。判断是不是同一个对象(基本数据类型的==是比较值,引用数据类型比较的是内存地址)
equals:
它的作用是判断两个对象是否相等,一般都有两种情况:
情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”来比较对象。
情况2:类覆盖了equals()方法。一般,我们都会覆盖equals()方法来比较两个对象的内容是否相等。若他们相等会返回true。
集合类
1. Map
1.数据结构:数据+链表,数组+链表+红黑树
- jdk1.8中,当链表大小超过8时,就会转换为红黑树,当小于6时变回链表,主要是根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候转换,小于等于6转为链表
2.HashMap允许空键空值么
HashMap最多只允许一个键为Null(多条会覆盖),但允许多个值为Null
3. 影响HashMap性能的重要参数
初始容量:创建哈希表(数组)时桶的数量,默认为 16
负载因子:哈希表在其容量自动增加之前可以达到多满的一种尺度,默认为 0.75
4. HashMap的工作原理
HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象
5. 那么为什么默认是16呢?怎么不是4?不是8?
关于这个默认容量的选择,JDK并没有给出官方解释,那么这应该就是个经验值,既然一定要设置一个默认的2^n 作为初始值,那么就需要在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。
太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。
6.HashMap当两个不同的键对象的hashcode相同会发生什么,调用get()会如何键值对中的值
首先在调用put()方法时,会先调用键对象的hashCode方法来计算hashcode,计算出hashcode以后,找到对应的bucket位置
,查看该bucket中已经有值,如果有值说明此时发生了碰撞,那么新添加的键对象会保存在该bucket位置的链表中,当调用get()方法来获取
键对象的值时,会遍历当前链表通过键对象的equals()方法来找到正确的键对象并返回相应的值。
7.HashMap和Hashtable的区别
-
线程安全 HashMap是线程不安全的,而HashTable是线程安全的,每个人方法通过修饰synchronized来控制线程安全。
-
效率 HashMap比HashTable效率高,原因在于HashTable的方法通过synchronized修饰后,并发的效率会降低。
-
允不允许null HashMap运行只有一个key为null,可以有多个null的value。而HashTable不允许key,value为null。
HashMap是非线程安全的,而Hashtable是线程安全的,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;
而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java5提供了ConcurrentHashMap,它是Hashatable的替代,比Hashtable
的扩展性更好
HashMap的遍历方式
-
使用 Iterator 遍历 HashMap EntrySet
-
使用 Iterator 遍历 HashMap KeySet
-
使用 For-each 循环迭代 HashMap
-
使用 Lambda 表达式遍历 HashMap
-
使用 Stream API 遍历 HashMap
HashSet
HashMap和HashSet的区别
ConcurrentHashMap
JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加细粒度的锁
ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。HashEntry 用来封装散列映射表中的键值对。在 HashEntry 类中,key,hash 和 next 域都被声明为 final 型,value 域被声明为 volatile 型。
TreeMap
TreeMap是一个有序的key-value集合,基于红黑树(Red-Black tree)的 NavigableMap实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator进行排序,具体取决于使用的构造方法。
TreeMap的特性:
根节点是黑色
每个节点都只能是红色或者黑色
每个叶节点(NIL节点,空节点)是黑色的。
如果一个节点是红色的,则它两个子节点都是黑色的,也就是说在一条路径上不能出现两个红色的节点。
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
LinkedHashMap
1.LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构。该结构由数组和链表+红黑树 在此基础上LinkedHashMap 增加了一条双向链表,保持遍历顺序和插入顺序一致的问题。
2. 在实现上,LinkedHashMap 很多方法直接继承自 HashMap(比如put remove方法就是直接用的父类的),仅为维护双向链表覆写了部分方法(get()方法是重写的)。
3.LinkedHashMap使用的键值对节点是Entity 他继承了hashMap 的Node,并新增了两个引用,分别是 before 和 after。这两个引用的用途不难理解,也就是用于维护双向链表.
4.链表的建立过程是在插入键值对节点时开始的,初始情况下,让 LinkedHashMap 的 head 和 tail 引用同时指向新节点,链表就算建立起来了。随后不断有新节点插入,通过将新节点接在 tail 引用指向节点的后面,即可实现链表的更新
5.LinkedHashMap 允许使用null值和null键, 线程是不安全的,虽然底层使用了双线链表,但是增删相快了。因为他底层的Entity 保留了hashMap node 的next 属性。
2.List
ArrayList
ArrayList就是有序的动态数组列表,主要⽤来装载数据,只能装载包装类(Integer,String,Double等),它的主要底层实现是数组Object[] elementData
Array和ArrayList的不同点:
Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
①.ArrayList与LinkedList的区别?
- ArrayList的查找和访问元素的速度较快,但新增,删除的速度较慢,LinkedList的查找和访问元素的速度较慢,但是他的新增,删除的速度较快
- ArrayList需要一份连续的内存空间,LinkedList不需要连续的内存空间(特别地,当创建一个ArrayList集合的时候,连续的内存空间必须要大于等于创建的容量)
- 两者都是线程不安全的,如果需要线程安全的就是可以使用Vector
② 1.7和1.8版本初始化的区别
1.7的时候是初始化就创建一个容量为10的数组,1.8后是初始化先创建一个空数组,第一次add时才扩容为10
把某个ArrayList复制到另一个ArrayList中去的几种技术:
- 使用clone()方法,比如ArrayList newArray = oldArray**.clone()**;
- 使用ArrayList构造方法,比如:ArrayList myObject = new ArrayList(myTempObject);
- 使用Collection的copy方法。
LinkedList
LinkedList的底层其实是一个双向链表,每一个对象都是一个Node节点,Node就是一个静态内部类
它是**线程不安全的,**所有的方法都没有加锁或者进行同步
Vector
Vector的底层的实现其实是一个数组
他是线程安全的:由于经常使用的add()方法的源码添加synchronized,所以说他是一个同步方法 ,就连不会对数据结构进行修改的get()方法上也加了synchronized
Set
Set 与List的区别
① List 允许有重复元素
Set 不允许有重复元素(原因:底层是Map实现的,map的key只能出现一次)
② List可以保证每个元素存储顺序
Set无法保证元素的存储顺序哪
哪种集合可以实现自动排序?
TreeSet 集合实现了元素的自动排序
TreeSet集合存储的元素的类型必须实现Comparable接口
Collection
Collection 和 Collections的区别 Collection 是List和Set的父接口
Collections 是针对集合的帮助类,Collections提供了一些方法去操作集合,
例如 Collections.sort()排序
Collections.reverse()反转
Java IO流
java中有几种类型的流?
字符流和字节流。字节流继承inputStream
和OutputStream
,字符流继承自InputSteamReader
和OutputStreamWriter
。
BIO和NIO的区别?
IO的方式通常分为几种,==同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO==。
BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
JVM
类加载器
简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。
类加载器 就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。
- 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
- 其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:
- 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
- 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
双亲委派模型
双亲委派模型工作过程是:
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
为什么需要双亲委派模型?
在这里,先想一下,如果没有双亲委派,那么用户是不是可以自己定义一个java.lang.Object的同名类,java.lang.String的同名类,并把它放到ClassPath中,那么类之间的比较结果及类的唯一性将无法保证,因此,为什么需要双亲委派模型?防止内存中出现多份同样的字节码
怎么打破双亲委派模型?
打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法。
引用
强引用、软引用、弱引用、虚引用的区别?
1)强引用
我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
2)软引用
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
SoftReference<String> softRef=new SoftReference<String>(str); // 软引用
3)弱引用
具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
4)虚引用
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
JVM内存模型
-
程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。
-
Java虚拟栈:存放基本数据类型、对象的引用、方法出口等,线程私有。
-
Native方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有。
-
Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享。
-
方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,各线程共享
堆
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的.
类加载器读取了类文件后,一般会把什么东西放到堆中? 类, 方法 , 常量 , 变量 , ~,保存我们所有引用类型的真实对象;
堆内存中还要细分为三个区域:
- 新生区 (伊甸园区) Young/New
- 养老区 old
- 永久区 Perm jdk8之后叫做 “元空间”
GC垃圾回收, 主要在伊甸园区和养老区~
假设内存满了,OOM内存不够了! java.lang.OutOfMemoryError: java heap space
GC算法
复制算法(好处没有内存碎片,坏处浪费空间to)
标记 -清除算法(优点:不需要额外的空间,缺点:两次扫描,严重浪费时间)、
标记-压缩算法(三部曲:标记,清除.压缩)
多线程
线程与进程的区别
根本区别:进程是操作系统资源分配的基本单元,而线程是处理器任务调度的和执行的基本单位
线程死锁
死锁是指两个或两个以上进程(线程)在执行过程中,由于竞争资源或由于彼此通信造成的一种堵塞的现象,若无外力的作用下,都将无法推进,此时的系统处于死锁状态。
形成死锁的四个必要条件
互斥条件:线程(进程)对所分配的资源具有排它性,即一个资源只能被一个进程占用,直到该进程被释放。
请求与保持条件:一个进程(线程)因请求被占有资源而发生堵塞时,对已获取的资源保持不放。
不剥夺条件:线程(进程)已获取的资源在未使用完之前不能被其他线程强行剥夺,只有等自己使用完才释放资源。
循环等待条件:当发生死锁时,所等待的线程(进程)必定形成一个环路,死循环造成永久堵塞。
创建线程的四种方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
Runnable接口和Callable接口有何区别
相同点:
Runnable和Callable都是接口
都可以编写多线程程序
都采用Thread.start()启动线程
不同点:
Runnable接口run方法无返回值,Callable接口call方法有返回值,是个泛型,和Futrue和FutureTask配合用来获取异步执行结果。
Runable接口run方法只能抛出运行时的异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息。
**注:**Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会堵塞主线程继续往下执行,如果不调用就不会堵塞。
Callable和Future
Callable接口也类似于Runnable接口,但是Runnable不会接收返回值,并且无法抛出返回结果的异常,而Callable功能更强大,被线程执行后,可有返回值,这个返回值可以被Future拿到,也就是说Future可以拿到异步执行任务的返回值。
Future接口表示异步任务,是一个可能没有完成的异步任务结果,所以说Callable用于产生结果,Future用于接收结果。
run()方法和start()方法有和区别
每个线程都是通过某个特定的Thread对象对于的run()方法来完成其操作的,run方法称为线程体,通过调用Thread类的start方法来启动一个线程。
start()方法用于启动线程,run()方法用于执行线程的运行代码,run()可以反复调用,而start()方法只能被调用一次。
start()方法来启动一个线程,真正实现了多线程的运行。调用start()方法无需等待run()方法体代码执行结束,可以直接继续执行其它的代码;调用start()方法线程进入就绪状态,随时等该CPU的调度,然后可以通过Thread调用run()方法来让其进入运行状态,run()方法运行结束,此线程终止,然后CPU再调度其它线程。
为什么调用start()方法会执行run()方法,为什么不能直接调用run()方法
这是一个常问的面试题,new Thread,线程进入了新建的状态,start方法的作用是使线程进入就绪的状态,当分配到时间片后就可以运行了。start方法会执行线程前的相应准备工作,然后在执行run方法运行线程体,这才是真正的多线程工作。
如果直接执行了run方法,run方法会被当作一个main线程下的普通方法执行,并不会在某个线程中去执行它,所以这并不是多线程工作。
小结:
调用start方法启动线程可使线程进入就绪状态,等待运行;run方法只是thread的一个普通方法调用,还是在主线程里执行。
线程声明周期的6种状态
新创建:又称初始化状态,这个时候Thread才刚刚被new出来,还没有被启动。
可运行状态:表示已经调用Thread的start方法启动了,随时等待CPU的调度,此状态又被称为就绪状态。
被终止:死亡状态,表示已经正常执行完线程体run()中的方法了或者因为没有捕获的异常而终止run()方法了。
**计时状态:**调用sleep(参数)或wait(参数)后线程进入计时状态,睡眠时间到了或wait时间到了,再或者其它线程调用notify并获取到锁之后开始进入可运行状态。另一种情况,其它线程调用notify没有获取到锁或者wait时间到没有获取到锁时,进入堵塞状态。
无线等待状态:获取锁对象后,调用wait()方法,释放锁进入无线等待状态
锁堵塞状态:wait(参数)时间到或者其它线程调用notify后没有获取到锁对象都会进入堵塞状态,只要一获取到锁对象就会进入可运行状态。
sleep()和wait()有什么区别
两者都可以使线程进入等待状态
- 类不同:sleep()是Thread下的静态方法,wait()是Object类下的方法
- 是否释放锁:sleep()不释放锁,wait()释放锁
- 用处不同:wait()常用于线程间的通信,sleep()常用于暂停执行。
- 用法不同:wait()用完后,线程不会自动执行,必须调用notify()或notifyAll()方法才能执行,sleep()方法调用后,线程经过过一定时间会自动苏醒,wait(参数)也可以传参数使其苏醒。它们苏醒后还有所区别,因为wait()会释放锁,所以苏醒后没有获取到锁就进入堵塞状态,获取到锁就进入就绪状态,而sleep苏醒后之间进入就绪状态,但是如果cpu不空闲,则进入的是就绪状态的堵塞队列中。
为什么线程通信方法wait(),notify(),notifyAll()要在同步代码块或同步方法中被调用?
wait(),notify(),notifyAll()方法都有一个特点,就是对象去调用它们的时候必须持有锁对象。
如对象调用wait()方法后持有的锁对象就释放出去,等待下一个线程来获取。
如对象调用notifyAll()要唤醒等待中的线程,也要讲自身用于的锁对象释放,让就绪状态中的线程竞争获取锁。
由于这些方法都需要线程持有锁对象,这样只能通过同步来实现,所以它们只能在同步块或同步方法中被调用。
如何停止一个正在运行的线程?
- 使用stop方法终止,但是这个方法已经过期,不被推荐使用。
- 使用interrupt方法终止线程
- run方法执行结束,正常退出
如何在两个线程间共享数据?
两个线程之间共享变量即可实现共享数据。
一般来说,共享变量要求变量本身是线程安全的,然后在线程中对变量使用。
什么是线程安全?Servlet是线程安全吗?
线程安全是指某个方法在多线程的环境下被调用时,能够正确处理多线程之间的共享变量,能程序能够正确完成。
Servlet不是线程安全的,它是单实例多线程的,当多个线程同时访问一个方法时,不能保证共享变量是安全的。
Struts2是多实例多线程的,线程安全,每个请求过来都会new一个新的action分配这个请求,请求完成后销毁。
springMVC的controller和Servlet一样,属性单实例多线程的,不能保证共享变量是安全的。
Struts2好处是不用考虑线程安全问题,springMVC和Servlet需要考虑。
如果想既可以提升性能又可以不能管理多个对象的话建议使用ThreadLocal来处理多线程。
Java中是如何保证多线程安全的?
使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
使用自动锁,synchronized锁
Lock lock = new ReentrantLock(),使用手动锁lock .lock(),lock.unlock()方法
常见线程池
①newSingleThreadExecutor
单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务
②newFixedThreadExecutor(n)
固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行
③newCacheThreadExecutor(推荐使用)
可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。
④newScheduleThreadExecutor
大小无限制的线程池,支持定时和周期性的执行线程
线程池常用参数
- corePoolSize:核心线程数量,会一直存在,除非allowCoreThreadTimeOut设置为true
- maximumPoolSize:线程池允许的最大线程池数量
- keepAliveTime:线程数量超过corePoolSize,空闲线程的最大超时时间
- unit:超时时间的单位
- workQueue:工作队列,保存未执行的Runnable 任务
- threadFactory:创建线程的工厂类
- handler:拒绝策略。
JUC
java.util.concurrent包
集合类不安全
CopyOnWriteArrayList
ArrayList在并发下不安全抛出异常 java.util.ConcurrentModificationException (并发修改异常)
解决方案:
1、使用Vector:
List<String> list = new Vector<>()
2、Collections工具类:
`List<String> list = Collections.synchronizedList(new ArrayList<>())`
3、JUC并发CopyOnWriteArrayList:
List<String> list = new CopyOnWriteArrayList<>()//写入时复制,避免写入时被覆盖
为什么CopyOnWriteArrayList比Vector牛逼?
因为Vector底层使用Synchronized关键字修饰,效率就会降低,jdk8 sync优化不够
而CopyOnWriteArrayList使用的是lock锁
CopyOnWriteArraySet
与ArrayList一样,Set在高并发下也存在安全问题,ConcurrentModificationException异常
解决方法:
1、Collections工具类:
Set<String> set = Collections.synchronizedList(new HashSet<>())`
2、JUC并发CopyOnWriteArrayList:
Set<String> set = new CopyOnWriteArraySet<>()//写入时复制,避免写入时被覆盖
public static void main(String[] args) {
//Set<String> set = Collections.synchronizedList(new HashSet<>())`
Set<String> set = new CopyOnWriteArraySet<>();
//开30个线程给set赋值
for (int i = 0; i < 30; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
CouncurrentHashMap
HashMap在高并发下也不是线程安全的,所以有了一个新的类ConcurrentHashMap
解决方法:
1、HashTable:使用Sync关键字进行同步
2、Collections工具类:与上面两个用法相同
3、JUC并发ConcurrentHashMap:
public static void main(String[] args) {
Map<String,Object> map = new ConcurrentHashMap<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
Callable
创建线程的方式
怎么实现Callable?
public class CallableTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();//创建MyThread
FutureTask<Integer> futureTask = new FutureTask<>(myThread);//使用FutureTask作为适配器 适配Runnable
new Thread(futureTask,"A").start();//启动
new Thread(futureTask,"B").start();//只会有一个输出,结果会被缓存
Integer res = futureTask.get();//获取Callable的返回结果 可能会产生阻塞
System.out.println(res);
}
}
class MyThread implements Callable<Integer>{
//重写call方法
@Override
public Integer call(){
System.out.println("call()");
return 1024;
}
}
常用的辅助类
CountDownLatch
减法计数器
//计数器
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"go out");
countDownLatch.countDown(); // 计数 -1
},String.valueOf(i)).start();
}
countDownLatch.await();//等待计数器归零 再向下执行
System.out.println("Close Door");
}
}
CycliBarrier
加法计数器
public static void main(String[] args) {
/**
* 集齐7颗龙珠召唤神龙
*/
// 召唤龙珠的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("召唤神龙成功!");
});
for (int i = 1; i <= 7; i++) {
final int temp = i;
// lambda能操作到 i 吗
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "收集"+temp+" 个龙珠");
try {
cyclicBarrier.await(); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
Semaphore
计数信号量,可做限流
public static void main(String[] args) {
// 线程数量:停车位! 限流!
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
// acquire() 得到
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到车位");
TimeUnit.SECONDS.sleep(2);//停2s 离开
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // release() 释放
}
},String.valueOf(i)).start();
}
}
原理:
semaphore.acquire()
获得,假设如果已经满了,等待,等待被释放为止!
semaphore.release();
释放,会将当前的信号量释放 + 1,然后唤醒等待的线程! 作用: 多个共享资源互斥的使用!并发限流,控制最大的线程数!
读写锁
/**
* 独占锁(写锁) 一次只能被一个线程占有
* 共享锁(读锁) 多个线程可以同时占有
* ReadWriteLock
* 读-读 可以共存!
* 读-写 不能共存!
* 写-写 不能共存!
*/
public class ReadWriteLockTest {
public static void main(String[] args) {
MyCacheLock myCache = new MyCacheLock();
// 写入
for (int i = 1; i <= 5 ; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
// 读取
for (int i = 1; i <= 5 ; i++) {
final int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
// 加锁的
class MyCacheLock{
private volatile Map<String,Object> map = new HashMap<>();
// 读写锁: 更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//private Lock lock = new ReentrantLock();//对比普通锁
// 存,写入的时候,只希望同时只有一个线程写
public void put(String key,Object value){
readWriteLock.writeLock().lock();//上锁
try {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();//解锁
}
}
// 取,读,所有人都可以读!
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
阻塞队列
Queue(队列)
BlockingQueue 阻塞队列
BlockingQueue 不是新的东西
添加、移除
四组API
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer() | put() | offer(,) |
移除 | remove | poll() | take() | poll(,) |
检测队首元素 | element | peek() | - | - |
SynchronousQueue 同步队列
没有容量, 进去一个元素,必须等待取出来之后,才能再往里面放一个元素!
put、take
线程池
线程池:三大方法、7大参数、4种拒绝策略
池化技术
程序的运行,本质:占用系统的资源! 优化资源的使用!=> 池化技术
线程池、连接池、内存池、对象池///… 创建、销毁。十分浪费资源
池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。
线程池的好处:
1、降低资源的消耗
2、提高响应的速度
3、方便管理。
线程复用、可以控制最大并发数、管理线
线程池:三大方法
阿里巴巴开发手册约定:
// 本质ThreadPoolExecutor()
public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大核心线程池大小
long keepAliveTime, // 超时了没有人调用就会释放
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂:创建线程的,一般不用动
RejectedExecutionHandler handle // 拒绝策略) {}
七大参数的理解(银行办业务)
当超时时间到的时候,多余闲置的线程就会被释放,防止资源占用
手动创建一个线程池
public static void main(String[] args) {
// 自定义线程池!工作 ThreadPoolExecutor
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试去和最早的竞争,也不会抛出异常!
try {
// 最大承载:Deque + max
// 超过 RejectedExecutionException
for (int i = 1; i <= 9; i++) {
// 使用了线程池之后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
4种拒绝策略
- new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异 常
- new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
- new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
- new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常!
提升
池的最大的大小如何去设置!
了解:IO密集型,CPU密集型:(调优)
// 最大线程到底该如何定义
// 1、CPU 密集型,几核,就是几,可以保持CPu的效率最高!Runtime.getRuntime().availableProcessors();//获取CPU核心数
// 2、IO 密集型 > 判断你程序中十分耗IO的线程数量,
// 程序 15个大型任务 io十分占用资源!
四大函数式接口
必需掌握:Consumer、Function、Predicate、Supplier
函数式接口: 只有一个方法的接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
// 泛型、枚举、反射
// lambda表达式、链式编程、函数式接口、Stream流式计算
// 超级多FunctionalInterface
// 简化编程模型,在新版本的框架底层大量应用!
// foreach(消费者类的函数式接口
Function函数式接口:一个参数,一个返回值
public static void main(String[] args) {
//
// Function<String,String> function = new Function<String,String>() {
// @Override
// public String apply(String str) {
// return str;
// }
// };
//lambda表达式简化后
Function<String,String> function = (str)->{return str;};
System.out.println(function.apply("asd"));
}
断定型接口:有一个输入参数,返回值只能是 布尔值!
Consumer 消费型接口:只有输入,没有返回值
Supplier 供给型接口:没有输入,只有返回值
都可以使用lambda表达式简化。
Stream流式计算
什么是流式计算
大数据:存储+计算
集合、Mysql本质就是存东西;计算交给流来做
public static void main(String[] args) {
User u1 = new User(1,"a",21);
User u2 = new User(2,"b",22);
User u3 = new User(3,"c",23);
User u4 = new User(4,"d",24);
User u5 = new User(6,"e",25);
// 集合就是存储
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
// 计算交给Stream流
// lambda表达式、链式编程、函数式接口、Stream流式计算
list.stream()
.filter(u->{return u.getId()%2==0;})//id为偶数
.filter(u->{return u.getAge()>23;})//年龄>23
.map(u->{return u.getName().toUpperCase();})//名字大写
.sorted((uu1,uu2)->{return uu2.compareTo(uu1);})//倒序
.limit(1)//只输出一个
.forEach(System.out::println);//打印
}
ForkJoin
ForkJoin 在 JDK 1.7 , 并行执行任务!提高效率。大数据量!
大数据:Map Reduce (把大任务拆分为小任务)
问题:从1+到10亿
1、for循环
public static void test1(){
Long sum = 0L;
long start = System.currentTimeMillis();
for (Long i = 1L; i <= 10_0000_0000; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("sum="+sum+" 时间:"+(end-start));
}
2、ForkJoin:需要先创建一个ForkJoinPool重写compute方法(采用分治)
public static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);// 提交任务
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("sum="+sum+" 时间:"+(end-start));
}
3、Stream并行流(效率最高,速度最快)
public static void test3(){
long start = System.currentTimeMillis();
// Stream并行流 ()左右开 ;(] 左开右闭
long sum = LongStream.rangeClosed(0L,10_0000_0000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum="+"时间:"+(end-start));
}
JMM
请你谈谈你对 Volatile 的理解
Volatile 是 Java 虚拟机提供轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
什么是JMM
JMM : Java内存模型,不存在的东西,概念!约定
线程:工作内存 、主内存
8种操作
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类 型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便 随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机 遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变 量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中, 以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内 存的变量中
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须 write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量 实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解 锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前, 必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
问题: 程序不知道主内存的值已经被修改过了
Volatile
1、保证可见性
增加volatile关键字 可以保证变量的可见性(线程间的通信)
可解决:线程A在执行一个循环num==0时,在主线程修改num=1(调用B线程修改主存值);A线程会得到num修改的消息。
2、不保证原子性
原子性:不可分割
线程A在执行任务的时候,不能被打扰的,也不能被分割。要么同时成功,要么同时失败。
在多线程下,对变量进行操作尽管加了volatile关键字,也无法保证原子性,是线程不安全的
如果保证原子性
1、加Lock或者synchronized,通过加锁可以使线程安全,每次拿到变量的只有一个线程;
2、使用==原子类==解决原子性,替换对应的变量,比如int 替换为 new AtomicInteger对象(高级方法)
这些类的底层都直接和操作系统挂钩!在内存中修改值!Unsafe类是一个很特殊的存在!
3、禁止指令重排
内存屏障。CPU指令。作用:
1.可以保证操作的执行顺序
2.可以保证某些变量的内存可见性(利用这些特性,就可以实现可见性)
CAS
CAS:比较并交换;比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就 一直循环!
Unsafe 类
unsafe类里有native关键字,说明是调用底层c++的方法
比较并交换的过程
缺点:
1、 循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题
ABA问题
问题描述:当线程A使用CAS进行比较并交换的时候,线程B提前将值进行了改变,并且又变了回来。但是A拿到的值就不是最初的值了。被B做了手脚。
原子引用
解决ABA问题,引入原子引用!对应的思想:乐观锁!
将每个操作带上版本号
public class CASDemo {
//AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
// 正常在业务操作,这里面比较的都是一个个对象
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
//捣乱的线程
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//原子操作比较 期望值为1 改为2 获取版本号 并+1
System.out.println(atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1));
//输出当前版本号
System.out.println("a2=>"+atomicStampedReference.getStamp());
//原子操作比较 期望值为2 改为1 获取版本号 并+1
System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1));
//输出当前版本号
System.out.println("a3=>"+atomicStampedReference.getStamp());
},"a").start();
// 乐观锁的原理相同!第二个线程去拿
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//原子操作比较 期望值为1 改为6 获取版本号 并+1 结果失败了
System.out.println(atomicStampedReference.compareAndSet(1, 6,stamp, stamp + 1));
//输出当前版本号
System.out.println("b2=>"+atomicStampedReference.getStamp());
},"b").start();
}
}
测试遇到问题(Integer坑): Integer 使用了对象缓存机制,默认范围是 -128 ~ 127 ,推荐使用静态工厂方法 valueOf 获取对象实 例,而不是 new,因为 valueOf 使用缓存,而 new 一定会创建新的对象分配新的内存空间;实际泛型为对象类。
阿里云开发手册:
锁
synchronized 与 Lock 区别
(1)synchronized 内置关键字,Lock是一个java类
(2)synchronized 无法获取取锁的状态,Lock 可以判断是否获取到了锁
(3)synchronized 会自动释放锁,Lock 必须要手动释放锁!如果不释放锁,死锁
(4)synchronized 线程1(获得锁,阻塞)、线程2(等待,直到线程1被释放);Lock 锁就不一定会等待下去,会通过lock.tryLock(),尝试获取锁
(5)synchronized 可重入锁,不可中断,非公平;Lock,可重入锁,可以判断,非公平(可以自己设置在ReentrantLock(boolean fair)当fair为true就是公平锁,默认是非公平的)
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
(6)synchronized 适合所少量的代码同步问题,Lock 适合锁大量的同步代码块。
如何避免死锁
我们只需破坏形参死锁的四个必要条件之一即可。
- 破坏互斥条件:无法破坏,我们的锁本身就是来个线程(进程)来产生互斥
- 破坏请求与保持条件:一次申请所有资源
- 破坏不剥夺条件:占有部分资源的线程尝试申请其它资源,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:按序来申请资源。
锁的分类
**1.公平锁(**之多个线程申请,类似打饭,先来后到)
2.非公平锁(不按顺序,在高并发下,优先级反转)
3.可重入锁(递归锁):避免死锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
4.自旋锁:指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
自定义一个自旋锁:
public class SpinlockDemo {
// int 0
// Thread null 原子类
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "==> mylock");
// 自旋锁 CAS 为空则换为thread
while (!atomicReference.compareAndSet(null,thread)){
}
}
// 解锁
// 加锁
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "==> myUnlock");
atomicReference.compareAndSet(thread,null);
}
}
当两个线程都去枷锁和解锁时,当线程A先拿到锁并且需要一段时间才释放锁。此时B就一直在自旋去获得锁。只有当A释放锁的时候,B才能获取锁。
请说明一下synchronized的可重入怎么实现。
每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
请讲一下非公平锁和公平锁在reetrantlock里的实现过程是怎样的。
如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,FIFO。对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁还需要判断当前节点是否有前驱节点,如果有,则表示有线程比当前线程更早请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
synchronized和ReentrantLock的区别
底层实现上来说,①synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法,②ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。
synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁,ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。
锁状态
1.锁粗化:为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
2.锁消除:锁消除是Java虚拟机在JIT编译是,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
3.锁升级:锁的4中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)
8锁现象
有两个同步方法sycn修饰的send和call,创建一个对象和两个线程。
从第2起,send都有4s延迟
1.同一个对象,先拿到锁的方法先执行,send先。
2.设置send延迟睡眠4s,依然是先拿到锁的方法先执行(锁的是同一个对象)。
3.增加一个普通方法,此时同步方法send有4s延迟,同一个对象先执行普通方法(普通方法不需要获取锁)
4.两个对象,分别调用两个同步方法,延迟少的先执行(非同一个对象锁 对比6)
5.在两个sycn同步方法加上static静态修饰,同一个对象send先执行(此时锁的是class)
6.在两个sycn同步方法加上static静态修饰,两个对象分别调用,依然是send先执行(因为锁的是class,两个对象的class类只有一个模板)
7.一个static同步方法,一个普通同步方法,同一个对象调用,call先执行(此时锁的是两个东西,一个锁的对象方法,一个锁的class类模板,所以延迟少的先执行)
8.一个static同步方法,一个普通同步方法,两个对象调用,call先执行(此时锁也不同,对象也不同,延迟少的先执行)
Volatile作用
volatile是让变量在多线程的情况下保持对其他线程的可见。
计算机网络
网络概述
网络七层模型和五层模型
应用层
应用层的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的应用进程间的通信和交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,比如域名系统DNS,万维网的HTTP协议,邮件系统的SMTP协议。我们把应用层交互的数据称为报文。
负载均衡 反向代理模式的优点、缺点
(1)反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。
(2)反向代理负载均衡技术是把将来自internet上的连接请求以反向代理的方式动态地转发给内部网络上的多台服务器进行处理,从而达到负载均衡的目的。
(3)反向代理负载均衡能以软件方式来实现,如apache mod_proxy、netscape proxy等,也可以在高速缓存器、负载均衡器等硬件设备上实现。反向代理负载均衡可以将优化的负载均衡策略和代理服务器的高速缓存技术结合在一起,提升静态网页的访问速度,提供有益的性能;由于网络外部用户不能直接访问真实的服务器,具备额外的安全性(同理,NAT负载均衡技术也有此优点)。
(4)其缺点主要表现在以下两个方面
反向代理是处于OSI参考模型第七层应用的,所以就必须为每一种应用服务专门开发一个反向代理服务器,这样就限制了反向代理负载均衡技术的应用范围,现在一般都用于对web服务器的负载均衡。
针对每一次代理,代理服务器就必须打开两个连接,一个对外,一个对内,因此在并发连接请求数量非常大的时候,代理服务器的负载也就非常大了,在最后代理服务器本身会成为服务的瓶颈。
一般来讲,可以用它来对连接数量不是特别大,但每次连接都需要消耗大量处理资源的站点进行负载均衡,如search等。
dns使用的协议
既使用TCP又使用UDP
Cookies和session区别
Cookies是一种能够让网站服务器把少量数据储存到客户端的硬盘或内存,或是从客户端的硬盘读取数据的一种技术。Cookies是当你浏览某网站时,由Web服务器置于你硬盘上的一个非常小的文本文件,它可以记录你的用户ID、密码、浏览过的网页、停留的时间等信息。
Session是服务器用来跟踪用户的一种手段,每个Session都有一个唯一标识:session ID。当服务器创建了Session时,给客户端发送的响应报文包含了Set-cookie字段,其中有一个名为sid的键值对,这个键值Session ID。客户端收到后就把Cookie保存浏览器,并且之后发送的请求报表都包含SessionID。HTTP就是通过Session和Cookie这两个发送一起合作来实现跟踪用户状态,Session用于服务端,Cookie用于客户端
运输层
TCP与UDP区别
UDP | TCP |
---|---|
无连接 | 面向连接 |
支持一对一、一对多、多对一、多对多交互通信 | 每一条TCP连接只能有两个端点EP,只能是一对一通信 |
对应用层交付的报文直接打包 | 面向字节流 |
不可靠,不使用流量控制和拥塞控制 | 可靠传输、使用流量控制和拥塞控制 |
首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
TCP三次握手
1)主机A向主机B发送TCP连接请求数据包,其中包含主机A的初始序列号seq(A)=x。(其中报文中同步标志位SYN=1,ACK=0,表示这是一个TCP连接请求数据报文
2)主机B收到请求后,会发回连接确认数据包。(其中确认报文段中,标识位SYN=1,ACK=1,表示这是一个TCP连接响应数据报文,
3)第三次,主机A收到主机B的确认报文后,还需作出确认,即发送一个序列号seq(A)=x+1;确认号为ack(A)=y+1的报文;
TCP四次挥手
(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送。
(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。
(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A。
(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。
SYN和ACK
接收端传回发送端所发送的SYN是为了告诉发送端,我接收到的信号信息确实就是你所发送的信号。
双方通信无误必须是两者互相发送信息都无误。传了SYN,证明发送端发到接收方的通道没有问题,但是接收端到发送端的通道还需要ACK信号来进行验证。
为什么tcp为什么要建立连接?
保证可靠传输。
三次握手,超时重传,滑动窗口,拥塞控制。
超时重传:当 TCP 发出一个段后,它启动一个定时器,如果不能及时收到一个确认就重发,3此过后就会发送RST关闭连接
拥塞控制:当网络拥塞时,减少数据的发送。
滑动窗口: TCP中窗口大小是指tcp协议一次传输多少个数据。因为TCP是一个面向连接的可靠的传输协议,既然是可靠的就需要传输的数据进行确认。TCP窗口机制有两种,一种是固定窗口大小,另一种是滑动窗口。数据在传输时,TCP会对所有数据进行编号,发送方在发送过程中始终保持着一个窗口,只有落在发送窗口内的数据帧才允许被发送;同时接收方也始终保持着一个接收窗口,只有落在窗口内的数据才会被接收。这样通过改变发送窗口和接收窗口的大小就可以实现流量控制。
四次挥手重要的是TIME-WAIT状态
要确保服务器是否收到了我们的ACK报文,如果没有收到的话,服务器会重新发FIN报文给客户端,那么客户端再次收到FIN报文之后,就知道之前的ACK报文丢失了,就会再次发送ACK报文。
常见的HTTP相应状态码
- 200:请求被正常处理
- 204:请求被受理但没有资源可以返回
- 206:客户端只是请求资源的一部分,服务器只对请求的部分资源执行GET方法,相应报文中通过Content-Range指定范围的资源。
- 301:永久性重定向
- 302:临时重定向
- 303:与302状态码有相似功能,只是它希望客户端在请求一个URI的时候,能通过GET方法重定向到另一个URI上
- 304:发送附带条件的请求时,条件不满足时返回,与重定向无关
- 307:临时重定向,与302类似,只是强制要求使用POST方法
- 400:请求报文语法有误,服务器无法识别
- 401:请求需要认证
- 403:请求的对应资源禁止被访问
- 404:服务器无法找到对应资源
- 500:服务器内部错误
- 503:服务器正忙
请说明一下http和https的区别
1)https协议要申请证书到CA,需要一定经济成本;
2) http是明文传输,https是加密的安全传输;
3) 连接的端口不一样,http是80,https是443;
4)http连接很简单,没有状态;https是ssl加密的传输,身份认证的网络协议,相对http传输比较安全。
网络层
arp协议和arp攻击
地址解析协议。ARP攻击的第一步就是ARP欺骗。由上述“ARP协议的工作过程”我们知道,ARP协议基本没有对网络的安全性做任何思考,当时人们考虑的重点是如何保证网络通信能够正确和快速的完成——ARP协议工作的前提是默认了其所在的网络是一个善良的网络,每台主机在向网络中发送应答信号时都是使用的真实身份。不过后来,人们发现ARP应答中的IP地址和MAC地址中的信息是可以伪造的,并不一定是自己的真实IP地址和MAC地址,由此,ARP欺骗就产生了。
什么是icmp协议,它的作用是什么
它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用
Socket编程
什么是socket?
在网络编程中,网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
Socket套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。
Socket原理
Socket实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。正如打电话之前,双方必须各自拥有一台电话机一样。
套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。
- 服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
- 客户端请求:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
- 连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
设计模式
设计模式:是一套用来提高代码可复用性,可维护性、可读性、稳健型以及安全性的解决方案
设计模式的本质:是面向对象设计原则的实际运用,是对类的封装、继承、多态以及类的关联关系和组合关系的充分理解。
分类
创建型模式:(描述怎样去创建一个对象,创建和使用分离)
- 单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式
结构型模式:(描述如何将类或对象安装某种类型组成更大的结构)
- 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式
行为型模式:(描述类和对象如何可以相互协作)
- 模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式
单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
饿汉式单例模式:
饿汉式是线程安全的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变;
/**
* 饿汉模式
*/
public class HungrySingle {
private static final HungrySingle sInstance = new HungrySingle();
private HungrySingle() {
}
public static HungrySingle getInstance() {
return sInstance;
}
}
懒汉式单例模式
懒汉式如果在创建实例对象时不加上synchronized则会导致对对象的访问不是线程安全的,推荐使用第一种
/**
* 懒汉模式
*/
public class LazySingle {
//加volatile关键字 避免指令重排
private volatile static LazySingle sInstance = null;
private LazySingle() {
}
//双重检测锁+原子性操作
public static LazySingle getInstance() {
if (sInstance == null) {
synchronized (LazySingle.class) {
if (sInstance == null) {
sInstance = new LazySingle();//这不是一个原子性操作,容易出问题
/**
* 1.分配内存空间。
* 2.执行构造方法,初始化对象
* 3.把这个对象指向 这个空间
* 我们想让它执行顺序为123
* 当多并发情况下,可能发生132的情况。新线程检测到sInstance被创建,直接return这时候对象为空
* 解决办法:加volatile关键字 避免指令重排。
*/
}
}
}
return sInstance;
}
}
懒汉式是延时加载,它是在需要的时候才创建对象;
DCL懒汉式,添加volatile关键字、防止反射
单例模式不安全,反射可以破坏单例模式
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);//反射获取无参构造函数
declaredConstructor.setAccessible(true);//关闭构造函数的私有化
LazyMan instance = declaredConstructor.newInstance(); //创建对象,破坏单例模式
解决办法:可以加一个布尔变量进行判断,防止被破坏。
继续破坏:使用反射得到字段,修改布尔值。
枚举
枚举本身也是一个class类,
通过反射去破解枚举类会报错NoSuchMethodException: com.sz.single.EnumSingle.()
,没有发现这个无参构造函数,我们想得到的报错应该是无法通过反编译破解枚举这个报错。为什么?
1、但是在idea查看枚举的源码,发现有一个无参构造函数(但是报错是不存在无参的,idea骗了我们)
2、利用反汇编查看字节码:javap -p EnumSingle.class,结果依然存在无参构造函数。(字节码依旧欺骗了我们)
3、利用反编译查看源码:果然没有无参构造函数
工厂模式
- 实现了创建者和调用者的分离
- 详细分类:
- 简单工厂模式:用来生产同一等级结构中的任意产品(对于增加新的产品,需要扩展已有代码)
- 工厂方法模式:用来生产同一等级结构中的固定产品(支持增加任意产品)
- 抽象工厂模式:围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。
工厂设计模式的原则(OOP七大原则):
核心本质:
- 实例化对象不使用new,用工厂方法代替 factory
- 将选择实现类,创建对象统一管理和控制。从而将调用者跟我们的实现类解耦
Spring
IoC
(1)IOC就是控制反转,指创建对象的控制权转移给Spring框架进行管理,并由Spring根据配置文件去创建实例和管理各个实例之间的依赖关系,对象与对象之间松散耦合,也利于功能的复用。DI依赖注入,和控制反转是同一个概念的不同角度的描述,即 应用程序在运行时依赖IoC容器来动态注入对象需要的外部依赖。
(2)最直观的表达就是,以前创建对象的主动权和时机都是由自己把控的,IOC让对象的创建不用去new了,可以由spring自动生产,使用java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。
(3)Spring的IOC有三种注入方式 :构造器注入、setter方法注入、根据注解注入。
AOP
AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
动态代理
静态代理
Bean
Bean的生命周期?
简单来说,Spring Bean的生命周期只有四个阶段:实例化 Instantiation --> 属性赋值 Populate --> 初始化 Initialization --> 销毁 Destruction
Spring中bean的作用域:
(1)singleton:默认作用域,单例bean,每个容器中只有一个bean的实例。
(2)prototype:为每一个bean请求创建一个实例。
(3)request:为每一个request请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。
(4)session:与request范围类似,同一个session会话共享一个实例,不同会话使用不同的实例。
(5)global-session:全局作用域,所有会话共享一个实例。如果想要声明让所有会话共享的存储变量的话,那么这全局变量需要存储在global-session中。
Spring基于xml注入bean的几种方式:
- set()方法注入;
- 构造器注入:①通过index设置参数的位置;②通过type设置参数类型;
- 静态工厂注入;
- 实例工厂;
Spring事务
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。Spring只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过binlog或者undo log实现的。Spring会在事务开始时,根据当前环境中设置的隔离级别,调整数据库隔离级别,由此保持一致。
声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
Mybatis
通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)
Mybaits的优缺点:
(1)优点:
① 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
② 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
③ 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
④ 能够与Spring很好的集成;
⑤ 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
(2)缺点:
① SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
② SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
Mybatis在处理{}直接替换成变量的值。而Mybatis在处理#{}时,会对sql语句进行预处理,将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
使用#{}可以有效的防止SQL注入,提高系统安全性。
一级、二级缓存:
1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
(2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;
(3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear 掉并重新更新,如果开启了二级缓存,则只根据配置判断是否刷新。
动态SQL
Mybatis动态sql可以在Xml映射文件内,以标签的形式编写动态sql,执行原理是根据表达式的值 完成逻辑判断 并动态拼接sql的功能。
Mybatis提供了9种动态sql标签:trim | where | set | foreach | if | choose | when | otherwise | bind。
主键生成策略:雪花算法(mybatis-plus)
数据库
四大特性
ACID事务
原子性(atomicity),要么执行,要么不执行 ;要么全部成功,要么全部失败
一致性(consistency),事务前后,数据总额一致
隔离性(isolation),所有操作全部执行完以前其它会话不能看到过程
持久性(durability),一旦事务提交,对数据的改变就是永久的
三大范式
- 第一范式(1NF):指表的列(属性)不可再分,数据库中表的每一列都是不可分割的基本数据项,同一列中不能有多个值;
- 第二范式(2NF):在 1NF 的基础上,还包含两部分的内容:一是表必须有一个主键;二是表中非主键列必须完全依赖于主键,不能只依赖于主键的一部分;
- 第三范式(3NF):在 2NF 的基础上,消除非主键列对主键的传递依赖,非主键列必须直接依赖于主键。
- BC范式(BCNF):在 3NF 的基础上,消除主属性对于码部分的传递依赖
数据库隔离级别
多个事务读可能会遇到以下问题
脏读:事务B读取事务A还没有提交的数据
不可重复读:,一行被检索两次,并且该行中的值在不同的读取之间不同时
幻读:当在事务处理过程中执行两个相同的查询,并且第二个查询返回的行集合与第一个查询不同时
这两个区别在于,不可重复读重点在一行,幻读的重点 ,返回 的集合不一样
InnoDB
常用的存储引擎?InnoDB与MyISAM的区别?
存储引擎是对底层物理数据执行实际操作的组件,为Server服务层提供各种操作数据的API。常用的存储引擎有InnoDB、MyISAM、Memory。这里我们主要介绍InnoDB 与 MyISAM 的区别:
索引
索引 | 区别 |
---|---|
Hash | hash索引,等值查询效率高,不能排序,不能进行范围查询 |
B+ | 数据有序,范围查询 |
索引 | 区别 |
---|---|
聚集索引 | 数据按索引顺序存储,中子结点存储真实的物理数据 |
非聚集索引 | 存储指向真正数据行的指针 |
索引的优缺点
索引最大的好处是提高查询速度,
缺点是更新数据时效率低,因为要同时更新索引
对数据进行频繁查询进建立索引,如果要频繁更改数据不建议使用索引。
索引的底层实现(B+树,为何不采用红黑树,B树)重点
一个m阶的B+树具有如下几个特征:
1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素
索引最左前缀问题
如果对三个字段建立联合索引,如果第二个字段没有使用索引,第三个字段也使用不到索引了
最左匹配原则
最左匹配原则是针对索引的
举例来说:两个字段(name,age)建立联合索引,如果where age=12这样的话,是没有利用到索引的,
这里我们可以简单的理解为先是对name字段的值排序,然后对age的数据排序,如果直接查age的话,这时就没有利用到索引了,
查询条件where name=‘xxx’ and age=xx 这时的话,就利用到索引了,再来思考下where age=xx and name=’xxx‘ 这个sql会利用索引吗,
按照正常的原则来讲是不会利用到的,但是优化器会进行优化,把位置交换下。这个sql也能利用到索引了
索引分类,索引失效条件
分类:
普通索引 最基本的索引,没有任何限制
唯一索引 与"普通索引"类似,不同的就是:索引列的值必须唯一,但允许有空值。
主键索引 它是一种特殊的唯一索引,不允许有空值。
全文索引 针对较大的数据,生成全文索引很耗时好空间。
组合索引 为了更多的提高mysql效率可建立组合索引,遵循”最左前缀“原则
索引失效条件:
条件是or,如果还想让or条件生效,给or每个字段加个索引
like查询,以%开发 ,%在前面会失效,
内部函数
对索引列进行运算操作(字符串不加单引号,底层进行了隐式类型转换)
is null不会用,is not null 会用
范围查询右边的列 会失效
违法最左前缀法则
Sql的优化
对sql语句优化
- 子查询变成left join
- limit 分布优化,先利用ID定位,再分页
- or条件优化,多个or条件可以用union all对结果进行合并(union all结果可能重复)
不必要的排序 - where代替having,having 检索完所有记录,才进行过滤
- 避免嵌套查询
- 对多个字段进行等值查询时,联合索引
SQL的四种链接
左外连接,右外连接,内连接,全连接
外连接:
1)LEFT JOIN或LEFT OUTER JOIN
左向外联接的结果集包括 LEFT OUTER子句中指定的左表的所有行,而不仅仅是联接列所匹配的行。如果左表的某行在右表中没有匹配行,则在相关联的结果集行中右表的所有选择列表列均为空值。
2)RIGHT JOIN 或 RIGHT OUTER JOIN
右向外联接是左向外联接的反向联接。将返回右表的所有行。如果右表的某行在左表中没有匹配行,则将为左表返回空值。
3)FULL JOIN 或 FULL OUTER JOIN
完整外部联接返回左表和右表中的所有行。当某行在另一个表中没有匹配行时,则另一个表的选择列表列包含空值。如果表之间有匹配行,则整个结果集行包含基表的数据值。
Redis
Redis应用篇
1.redis有哪些数据结构?分别有什么应用场景?
字符串String,字典 Hash、列表 List、集合 Set、有序集合 ZSet。
高级: HyperLogLog、Geo、Pub/Sub。
2.Redis Zset相同的score如何排序
当score相同时候,会按照插入的field的字典序排序,即abc比acd靠前,因为第一个字母都是a,但是b比c靠前。
3.Redis是否支持事务(multi)
Redis部分支持事务,不支持的是:强一致性(支持:一次性、顺序性、排他性!执行一些列的命令)
Redis单条命令式保存原子性的,但是事务不保证原子性!
4.Redis中的watch命令是做什么的
WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值)
多线程修改值 , 使用watch 可以当做redis的乐观锁操作!
5.Redis是如何保证高可用的
主从复制,主备切换,哨兵模式
6.如何使用Redis来实现分布式锁?Redlock?
单实例:满足(互斥性,安全性,无死锁,高可用)
**互斥性:**任意时刻只能有一个客户端拥有锁,不能被多个客户端获取
**安全性:**锁只能被持有该锁的客户端删除,不能被其它客户端删除
**死锁:**获取锁的客户端因为某些原因而宕机,而未能释放锁,其它客户端也就无法获取该锁,需要有机制来避免该类问题的发生
**高可用:**当部分节点宕机,客户端仍能获取锁或者释放锁
用set+多个参数的方式实现,(key,value,nx不存在时,px过期时间)
多节点:使用redlock
7.说说redisson
可实现分布式锁。
8.缓存穿透:缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉
解决方法:
- 接口层增加校验,如用户鉴权校验,id做基础校验,id小于等于0的直接拦截;
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力
9.缓存击穿:缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
解决方法:
- 设置热点数据永远不过期。
- 加互斥锁,互斥锁
10.缓存雪崩:缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方法:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
- 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
- 搭建Redis缓存集群
11.哨兵模式
Redis底层篇
1.Redis底层 跳表
在列表上建立索引,使查找对应数据更快,多层索引就形成了跳表
ElasticSearch
常用
分布式、微服务
CAP原则
CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。
CAP原则的精髓就是要么AP,要么CP,要么AC,但是不存在CAP
SpringCloud
Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、智能路由、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署
SpringCloud和dubbo的区别
最大区别:Spring Cloud 抛弃了Dubbo的RPC通信,采用的是基于HTTP的REST方式
严格来说,这两种方式各有优劣。虽然从一定程度上来说,后者牺牲了服务调用的性能,但也避免了上面提到的原生RPC带来的问题。而且REST相比RPC更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖,这个优点在当下强调快速演化的微服务环境下,显得更加合适。
总结:二者解决的问题域不一样:Dubbo的定位是一款RPC框架,而SpringCloud的目标是微服务架构下的一站式解决方案。
SpringCloud和SpringBoot的区别
- SpringBoot专注于快速方便的开发单个个体微服务。
- SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,
- 为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务
- SpringBoot可以离开SpringCloud独立使用开发项目, 但是SpringCloud离不开SpringBoot ,属于依赖的关系
- SpringBoot专注于快速、方便的开发单个微服务个体,SpringCloud关注全局的服务治理框架。
SpringBoot
1.什么是 Spring Boot?
Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。
2.约定大于配置
约定大于配置是一种开发原则,就是减少人为的配置,直接用默认的配置就能获得我们想要的结果。
SpringBoot的约定大于配置,按我的理解是:对比SpringMVC,需要在web.xml里面配置前端控制器,还需要在核心配置文件(*-servlet.xml)中配置视图解析器啥的,更要配置第三方的Tomcat服务器。而SpringBoot就不需要我们配置这些,他内嵌了Tomcat服务器,我们只需要在Maven配置文件(Pom.xml)里面导入SpringMVC所需要的依赖就可以了。
这就是SpringBoot的优势,在传统所需要配置的地方,SpringBoot都进行了约定(配置好了),开发人员能配置得更少,更直接地开发项目,写业务逻辑代码。
3.自动装配原理
- SpringBoot通过main方法启动SpringApplication类的静态方法run()来启动项目。
- 根据注释的意思,run方法从一个使用了默认配置的指定资源启动一个SpringApplication并返回ApplicationContext对象,这个默认配置如何指定呢?
- 这个默认配置来源于@SpringBootApplication注解,这个注解是个复合注解,里面还包含了其他注解。
4.Spring Boot 的核心配置文件有哪几个?它们的区别是什么?
Spring Boot 的核心配置文件是 application 和 bootstrap 配置文件。
application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。
5.Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
- @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
- @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
- @ComponentScan:Spring组件扫描。
6.开启 Spring Boot 特性有哪几种方式?
1)继承spring-boot-starter-parent项目
2)导入spring-boot-dependencies项目依赖
7.运行 Spring Boot 有哪几种方式?
1)打包用命令或者放到容器中运行
2)用 Maven/ Gradle 插件运行
3)直接执行 main 方法运行
Linux
Docker
Docker、K8S
kubernestes
补充
还需深度学习,记得清楚
Redis分布式锁
CMS G1垃圾回收机制
CMS是标记清除的垃圾回收算法,GC是触发垃圾回收
http无状态
数据库MVCC 三大隔离级别
Voliate
进程通信方式
各个集合的扩容机制
为什么重写equals方法后hashcode也要重写
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。
如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。
因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。
AQS
抽象队列同步器框架。是一个用来构建锁和同步器的框架,可以说一下里面的一些实现类,自己用到过哪些!
评论区