可能部分本地图片上传出现问题,需要完整文件私信我发你压缩文件哈~
Java源代码(.java)需要经过编译器编译成字节码(.class)。在程序运行时,JVM负责将字节码翻译成特定平台下的机器码并运行。只要在不同的平台上安装对应的JVM,就可以运行字节码文件。
1)Python的执行速度不够快:Python的缺点主要是执行速度还不够快。当然,这并不是一个很严重的问题,一般情况下,我们不会拿Python语言与C/C++这样的语言进行直接比较。在Python语言的执行速度上,一方面,网络或磁盘的延迟会抵消部分Python本身消耗的时间;另一方面,因为Python特别容易和C结合使用,所以我们可以通过分离一部分需要优化速度的应用,将其转换为编译好的扩展,并在整个系统中使用Python脚本将这部分应用连接起来,以提高程序的整体效率。
2)无法有效利用多线程:因为python内部有一个锁,导致多线程的cpu效率提升不大。Python的GIL锁限制并发:Python的另一个大问题是,对多处理器支持不好。如果你接触Python的时间比较长,那就一定听说过GIL。GIL是指Python全局解释器锁(Global Interpreter Lock),当Python的默认解释器要执行字节码时,都需要先申请这个锁。这意味着,如果试图通过多线程扩展应用程序,将总是被这个全局解释器锁限制。当然,我们可以使用多进程的架构来提高程序的并发,也可以选择不同的Python实现来运行我们的程序。
3)Python 2与Python 3不兼容:如果一个普通的软件或者库不能够做到向后兼容,它一定会被用户无情地抛弃。在Python中,一个大的槽点就是Python 2与Python 3不兼容。这给所有Python工程师带来了无数烦恼。
4)Python在编译时不检查变量类型
Python将类型与对象关联,而不是与变量关联,这就意味着Python 解释器无法识别出变量类型不符的错误。假设变量count本来是用来保 存整数的,但如果将字符串"two"赋给它,在Python里也完全没问题。 传统的程序员将这种处理方式算作一个缺点,因为对代码失去了额外的 免费检查。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HqrV5gUb-1666488157629)(image-20220906210849738.png)]
Java语言是面向对象的语言。但8种基本数据类型却出现了例外,它们不具备对象的特性。正是为了解决这个问题,Java为每个基本数据类型都定义了一个对应的引用类型,这就是包装类。
两者的主要区别在于解决问题的方式不同:
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向 0 个或 1 个对象;一个对象可以有 n 个引用指向它。
一个java文件里可以有多个类,但最多只能有一个被public修饰的类;如果这个java文件中包含public修饰的类,则这个类的名称必须和java文件名一致。
Java中的变量分为成员变量和局部变量,它们的区别如下:
成员变量:成员变量是在类的范围里定义的变量;成员变量有默认初始值;
实例变量:未被static修饰的成员变量也叫实例变量,它存储于对象所在的堆内存中,生命周期与对象相同;
类变量:被static修饰的成员变量也叫类变量,它存储于方法区中,生命周期与当前类相同。
局部变量:局部变量是在方法里定义的变量;局部变量没有默认初始值;局部变量存储于栈内存中,作用的范围结束,变量空间会自动的释放。
面向对象的程序设计方法具有三个基本特征:封装、继承、多态。
封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改。对一个类或对象实现良好的封装,可以实现以下目的:
因为子类其实是一种特殊的父类,因此Java允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,或者被称为向上转型,向上转型由系统自动完成。当把一个子类对象直接赋给父类引用变量时,例如 BaseClass obj = new SubClass();,这个obj引用变量的编译时类型是BaseClass,而运行时类型是SubClass,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态。
多态的实现离不开继承,在设计程序时,我们可以将参数的类型定义为父类型。在调用程序时,则可以根据实际情况,传入该父类型的某个子类型的实例,这样就实现了多态。对于父类型,可以有三种形式,即普通的类、抽象类、接口。对于子类型,则要根据它自身的特征,重写父类的某些方法,或实现抽象类/接口的某些抽象方法。
构造方法不能重写。因为构造方法需要和类保持同名,而重写的要求是子类方法要和父类方法保持同名。如果允许重写构造方法的话,那么子类中将会存在与类名不同的构造方法,这与构造方法的要求是矛盾的。
Object类提供了如下几个常用方法:
hashCode()用于获取哈希码(散列码),eauqls()用于比较两个对象是否相等,它们应遵守如下规定:
==运算符:
equals()方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EMkHrmWO-1666488157630)(%E6%88%AA%E5%B1%8F2022-09-13%2019.19.53-16630716078925.png)]
第一步、判断 从上边的equals传过来的参数name2对应的也就是o是否为空,如果为空那么没有比较的意义了返回false
第二步、判断 o是不是一个Object 因为我们要比较的两个肯定是相同类型的 不是相同类型的 那就不是相同的了 返回false
第三步判断 他们的内存地址是否相同 this表示当前的调用的 通俗点就是谁调用我谁就是this 由上面我们可以看出来 name1是this 然后o是name2 如果两个住在一个家 那就为一个人了 返回true
如果o不是null 又是Object 同时 和this(也就是name1)的内存地址不同 没有返回值返回
那么判断第四步 :
首先由于o是Obejct 因此进行向下类型转换 把它转换为Name
然后再由this.name也就是 调用者(name1)的name和强转之后的o(也就是n)的name
进行比较 判断两个成员变量的值是否相同 也就是比较名字 名字相同那么这两个值就相同 返回true
char charAt(int index)//返回指定索引处的字符;
String substring(int beginIndex, int endIndex)//从此字符串中截取出一部分子字符串;
String[] split(String regex)//以指定的规则将此字符串分割成数组;
String trim()//删除字符串前导和后置的空格;
int indexOf(String str)//返回子串在此字符串首次出现的索引;
int lastIndexOf(String str)//返回子串在此字符串最后出现的索引;
boolean startsWith(String prefix)//判断此字符串是否以指定的前缀开头;
boolean endsWith(String suffix)//判断此字符串是否以指定的后缀结尾;
String toUpperCase()//将此字符串中所有的字符大写;
String toLowerCase()//将此字符串中所有的字符小写;
String replaceFirst(String regex, String replacement)//用指定字符串替换第一个匹配的子串;
String replaceAll(String regex, String replacement)//用指定字符串替换所有的匹配的子串。
String类由final修饰,所以不能被继承。
String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。
StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。
StringBuffer、StringBuilder都代表可变的字符串对象,它们有共同的父类 AbstractStringBuilder,并且两个类的构造方法和成员方法也基本相同。不同的是,StringBuffer是线程安全的,而StringBuilder是非线程安全的,所以StringBuilder性能略高,速度快。一般情况下,要创建一个内容可变的字符串,建议优先考虑StringBuilder类。
通过查看源码,可以知道:StringBuffer和StringBuilder来源出处是一致的,继承相同的类,实现相同的接口
而StringBuffer从JDK1.0时就有了,StringBuilder从JDK1.5才出现;所以我们可以清楚,StringBuilder就是为了提升StringBuffer效率而出现的
通过查看二者的源码,可以发现:
StringBuffer重写了length()和capacity()、append等方法,在他们的方法上面都有synchronized 关键字实现线程同步
拼接字符串有很多种方式,其中最常用的有4种,下面列举了这4种方式各自适合的场景。
由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。
抽象类可以有构造方法,只是不能直接创建抽象类的实例对象而已。在继承了抽象类的子类中通过super()或super(参数列表)调用抽象类中的构造方法。
从设计目的上来说,二者有如下的区别:
从使用方式上来说,二者有如下的区别:
(共同点)接口和抽象类很像,它们都具有如下共同的特征:
在Java中,可以按照如下三个步骤处理异常:
Throwable是异常的顶层父类,代表所有的非正常情况。它有两个直接子类,分别是Error、Exception。
RuntimeException
类及其子类异常,如NullPointerException
(空指针异常)、IndexOutOfBoundsException
(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。try-catch
语句捕获它,也没有用throws
子句声明抛出它,也会编译通过。RuntimeException
以外的异常,类型上都属于Exception
类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException
、SQLException
等以及用户自定义的Exception异常,一般情况下不自定义检查异常。String getMessage()// 返回异常发生时的简要描述
String toString()// 返回异常发生时的详细信息
String getLocalizedMessage()// 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
void printStackTrace()// 在控制台上打印 Throwable 对象封装的异常信息
不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。
正常情况下,先执行try里面的代码,捕获到异常后执行catch中的代码,最后执行finally中代码,但当在try catch中执行到return时,要判断finally中的代码是否执行,如果没有,应先执行finally中代码再返回。
通常来说,用new创建类的对象时,数据存储空间才被分配,方法才供外界调用。但**有时我们只想为特定域分配单一存储空间,不考虑要创建多少对象或者说根本就不创建任何对象,再就是我们想在没有创建对象的情况下也想调用方法。**在这两种情况下,static关键字,满足了我们的需求。
在Java类里只能包含成员变量、方法、构造器、初始化块、内部类(包括接口、枚举)5种成员,而static可以修饰成员变量、方法、初始化块、内部类(包括接口、枚举),以static修饰的成员就是类成员。类成员属于整个类,而不属于单个对象。
因为抽象类是不能被实例化的,即不能分配内存。而static修饰的方法,在实例化之前就已经分配了内存,所以抽象类中不能有静态的抽象方法。
如果要在启动系统时对静态资源进行初始化,则建议使用静态代码块完成数据的初始化操作。
"static"关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。Java中static方法不能被覆盖,因为**方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。**static方法跟类的任何实例都不相关,所以概念上不适用。
static修饰的类可以被继承。
如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此使用static修饰的内部类被称为类内部类,有的地方也称为静态内部类。
外部类的上一级程序单元是包,所以不可使用static修饰;而内部类的上一级程序单元是外部类,使用static修饰可以将内部类变成外部类相关,而不是外部类实例相关。因此static关键字不可修饰外部类,但可修饰内部类。
静态内部类需满足如下规则:
外部类.内部类 变量名 = new 外部类.内部类构造方法();
static关键字可以修饰成员变量、成员方法、初始化块、内部类,被static修饰的成员是类的成员,它属于类、不属于单个对象。以下是static修饰这4种成员时表现出的特征:
final关键字可以修饰类、方法、变量,以下是final修饰这3种目标时表现出的特征:
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList
这行代码就指明了该 ArrayList 对象只能传入 Persion 对象,如果传入其他类型的对象就会报错。并且,原生 List 返回类型是 Object ,需要手动转换类型才能使用,使用泛型后编译器自动转换。
反射的概念:
编译时根本无法预知该对象和类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。获取 Class 对象的 3 种方法 / 获取反射的3种方法
Person p = new Person();
Class clazz = p.getClass();
Class clazz = Person.class();
Class clazz = Class.forName("类的全路径");
Java中的集合类主要由Collection和Map这两个接口派生而出.
其中Collection接口又派生出三个子接口,分别是Set、List。
List:代表有序的,元素可以重复的集合
Set:代表无序的,元素不可重复的集合
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mWQPDI42-1666488157631)(image-20220911150444891.png)]
Map接口有很多实现类,其中比较常用的有HashMap、LinkedHashMap、HashTable、TreeMap、ConcurrentHashMap。
Collection是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的的子类,比如List,Set等
Collections是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法,Collections.sort(list)
使用Iterator最快(it.hasNext()),转化为数组进行迭代(toArray)次之,forEach最慢。
综上来说,在需要频繁读取集合中的数据时,更推荐使用ArrayList,而在插入和删除操作较多时,更推荐LinkedList
HashSet 是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。
HashMap是线程不安全的实现;HashMap可以使用null作为key或value。
首次扩容:
计算索引:
插入数据:
再次扩容:
B/B+树多用于外存上时,B/B+也被成为一个磁盘友好的数据结构。
HashMap本来是数组+链表的形式,链表由于其查找慢的特点,所以需要被查找效率更高的树结构来替换。如果用B/B+树的话,在数据量不是很多的情况下,数据都会“挤在”一个结点里面,这个时候遍历效率就退化成了链表。
HashMap在并发执行put操作时,可能会导致形成循环链表,从而引起死循环。
为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时,会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时,又会将红黑树转换回单向链表提高性能。
HashMap是非线程安全的,这意味着不应该在多线程中对这些Map进行修改操作,否则会产生数据不一致的问题,甚至还会因为并发插入元素而导致链表成环,这样在查找时就会发生死循环,影响到整个应用程序。
Collections工具类可以将一个Map转换成线程安全的实现,其实也就是通过一个包装类,然后把所有功能都委托给传入的Map,而包装类是基于synchronized关键字来保证线程安全的(Hashtable也是基于synchronized关键字),底层使用的是互斥锁,性能与吞吐量比较低。
ConcurrentHashMap的实现细节远没有这么简单,因此性能也要高上许多。它没有使用一个全局锁来锁住自己,而是采用了减少锁粒度的方法,尽量减少因为竞争锁而导致的阻塞与冲突,而且ConcurrentHashMap的检索操作是不需要锁的。
LinkedHashMap使用双向链表来维护key-value对的顺序(其实只需要考虑key的顺序),该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致。
LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。
LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能。但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。
**TreeMap基于红黑树(Red-Black tree)实现。**映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。TreeMap的基本操作containsKey、get、put、remove方法,它的时间复杂度是log(N)。
TreeMap包含几个重要的成员变量:root、size、comparator。其中root是红黑树的根节点。它是Entry类型,Entry是红黑树的节点,它包含了红黑树的6个基本组成:key、value、left、right、parent和color。Entry节点根据根据Key排序,包含的内容是value。Entry中key比较大小是根据比较器comparator来进行判断的。size是红黑树的节点个数。
HashSet是基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。它封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。
java8新特性有:1、Lambda表达式;2、方法引用;3、默认方法;4、新编译工具;5、Stream API;6、Date Time API;7、Option;8、Nashorn javascript引擎。
原文链接:https://www.php.cn/java/guide/461886.html
位置不同。throws用在函数上,后边跟的是异常类,可以跟多个异常类。throw用在函数内,后面跟的是异常对象。
功能不同。①throws用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先得处理方式。throw抛出具体的问题对象,执行到throw。功能就已经结束了跳转到调用者,并将具体的问题对象抛给调用者,也就是说throw语句独立存在时,下面不要定义其他语句,因为执行不到。②throws表示出现异常的一种可能性,并不一定会发生这些异常,throw则是抛出了异常,执行throw则一定抛出了某种异常对象。
Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?
系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
加载,主要完成下面 3 件事情:
通过全类名获取定义此类的二进制字节流
将字节流所代表的静态存储结构转换为方法区的运行时数据结构
在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
验证:确保加载的类符合 JVM 规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的事件。
准备:为static变量在方法区中分配内存空间,设置变量的初始值。准备阶段只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量是对象初始化时赋值的。
解析:虚拟机将常量池内的符号引用替换为直接引用的过程(符号引用比如我现在import java.util.ArrayList这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行)。
初始化:初始化阶段是执行初始化方法 < c l i n i t > ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
卸载:GC将无用对象从内存中卸载。
JVM 主要由四大部分组成:ClassLoader(类加载器),Runtime Data Area(运行时数据区,内存分区),Execution Engine(执行引擎),Native Interface(本地库接口),下图可以大致描述 JVM 的结构。
jvm将虚拟机分为5大区域,程序计数器、虚拟机栈、本地方法栈、java堆、方法区;
**线程私有的:**程序计数器,虚拟机栈,本地方法栈
**线程共享的:**堆,方法区,直接内存 (非运行时数据区的一部分)
双亲委派机制:是为了保证安全
类加载顺序:自定义类加载器 ---->APP(应用程序加载器)----> EXC(扩展类加载器)----->BOOT(启动类(根)加载器)(最终执行)
双亲委派机制流程:
由于双亲委派模式优势:
沙箱安全机制:自己写的String.class类不会被加载,这样便可以防止核心API库被随意篡改
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次
Java安全模型的核心就是Java沙箱(sandbox),**什么是沙箱?沙箱是一个限制程序运行的环境
沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。**沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
①OutOfMemoryError:
除了程序计数器,其他内存区域都有OOM的风险。
②StackOverFlowError栈溢出:
内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象已经不再需要,但由于长生命周期对象持有它的引用而导致不能被回收。以发生的方式来分类,内存泄漏可以分为4类:
避免内存泄漏的几点建议:
内存溢出(out of memory):简单地说**内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,**于是就发生了内存溢出。引起内存溢出的原因有很多种,常见的有以下几种:
内存溢出的解决方案:
专业工具:内存快照分析工具:Jpro
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
//-Xms 设置初始化内存分配大小,默认为电脑内存1/64
//-Xmx 设置最大分配内存,默认为电脑内存1/4
//-XX:PringGCDetails 打印GC垃圾回收信息
//-XX:+HeapDumpOnOutOfMemoryError 打印OOM信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VFfkgwHY-1666488157632)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
所有的类都由类加载器加载,加载的作用就是将 .class文件加载到内存。
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader:
定义一个类,继承ClassLoader
重写loadClass方法
实例化class对象
堆里面分为新生代和老生代(java8取消了永久代,采用了Metaspace**(元空间)**),新生代包Eden+Survivor区,survivor区里面分为from和to区,内存回收时,如果用的是复制算法,从from复制到to,当经过一次或者多次GC之后,存活下来的对象会被移动到老年区,当JVM内存不够用的时候,会触发Full GC,清理JVM老年区。当新生区满了之后会触发YGC,先把存活的对象放到其中一个Survice区,然后进行垃圾清理。因为如果仅仅清理需要删除的对象,这样会导致内存碎片,因此一般会把Eden 进行完全的清理,然后整理内存。那么下次GC 的时候,就会使用下一个Survive,这样循环使用。如果有特别大的对象,新生代放不下,就会使用老年代的担保,直接放到老年代里面。因为JVM 认为,一般大对象的存活时间一般比较久远。
引用计数算法:
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
可达性分析算法:
当前主流的商用程序语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
标记清除,标记整理(压缩),复制算法,引用计数
给每个对象分配一个计数器,用一次加一次,清理计数为 0 的对象,下图中 C 就会被干掉~
扫描整个对象空间并对存活的对象标记,然后清除没有标记的对象。
是标记清除的再优化!
压缩:通过再次扫描,减少内存碎片,将空的坑位平移。
最后两种算法可以合并为一种算法——标记-清除-压缩算法
但是扫描时间长的缺点还是没有克服!
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
因为老年代保留的对象都是难以消亡的,而标记复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低,所以在老年代一般不能直接选用这种算法。
新生成的对象首先放到年轻代Eden区,当 Eden空间满了,触发轻 GC,存活下来的对象移动到幸存0区,幸存0区满后触发执行轻 GC,幸存0区存活对象移动到幸存1区,这样保证了一段时间内总有一个幸存区为空。经过多次轻 GC仍然存活的对象移动到老年代。老年代存储长期存活的对象,占满时会触发重 GC,GC期间会停止所有线程等待GC完成,所以对相应要求高的应用尽量减少发生Major GC,避免响应超时。
当 Eden 区的空间耗尽时 Java 虚拟机便会触发一次 新生代 GC 来收集新生代的垃圾,存活下来的对象,则会被送到 Survivor 区,简单说就是当新生代的Eden区满的时候触发 新生代 GC 。
serial GC 串行垃圾收集器中,老年代内存剩余已经小于之前年轻代晋升老年代的平均大小,则进行 Full GC。而在 CMS 等并发收集器中则是每隔一段时间检查一下老年代内存的使用量,超过一定比例时进行 Full GC 回收。
可以采用以下措施来减少Full GC的次数:
增加方法区的空间;
增加老年代的空间;
减少新生代的空间;
禁止使用System.gc()方法;
使用标记-整理算法,尽量保持较大的连续内存空间;
排查代码中无用的大对象。
虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SxdENGzP-1666488157632)(image-20220912111942426.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4hvmj9eJ-1666488157633)(image-20220912112459278.png)]
方法区(Method Area)。与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
Java虚拟机栈(Java Virtual Machine Stack)。**线程私有的,**它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。
HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。
定义一个native方法,不提供方法体(类似于抽象方法)。因为其方法体是由非java语言在外面实现的。
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序,JMM内部会有指令重排,并且会有af-if-serial和happen-before的理念来保证指令的正确性。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YnrxTFkQ-1666488157633)(image-20220916211426069.png)]
JMM
Java Memory Model Java内存模型
作用:缓存一致性协议,用于定义数据读写的规则
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory java就一个)中,每个线程都有一个私有的本地内存(Local Memory)(每个线程都有自己的工作区域,是从主内存拷贝的)
volilate关键字:解决共享对象可见性问题
线程每次修改后立刻同步到主内存中,保证其它线程可以看到变量的修改
线程每次使用变量前,先从主内存中刷新最新到工作内存(本地内存),保证能看见其它线程对变量的修改的同步
JVM
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AhdZe4bJ-1666488157633)(v2-ef8dec436662045c7c15ec7fbb3cfe1b_1440w.jpg)]
-- 创建数据库exercise
create database exercise;
-- 使用数据库exercise
use exercise;
-- 创建学生表student
create table student
(Sno varchar(10) not null,
Sname varchar(10) ,
Sage date ,
Ssex varchar(10) ,
primary key (Sno));
start transaction;
insert into student values ('01', '赵雷', '1990-01-01', '男');
insert into student values ('02', '钱电', '1990-12-21', '男');
insert into student values ('03', '孙风', '1990-05-20', '男');
insert into student values ('04', '李云', '1990-08-06', '男');
insert into student values ('05', '周梅', '1991-12-01', '女');
insert into student values ('06', '吴兰', '1992-03-01', '女');
insert into student values ('07', '郑竹', '1989-07-01', '女');
insert into student values ('08', '王菊', '1990-01-20', '女');
commit;
-- 创建科目表course
create table course
(Cno varchar(10) not null,
Cname varchar(10) ,
Tno varchar(10) ,
primary key (Cno));
start transaction;
insert into course values ('01', '语文', '02');
insert into course values ('02', '数学', '01');
insert into course values ('03', '英语', '03');
commit;
-- 创建教师表teacher
create table teacher
(Tno varchar(10) not null,
Tname varchar(10) ,
primary key (Tno));
strat transaction;
insert into teacher values ('01', '张三');
insert into teacher values ('02', '李四');
insert into teacher values ('03', '王五');
commit;
-- 创建成绩表 sc
create table sc
(Sno varchar (10) ,
Cno varchar (10) ,
score decimal(18,1),
primary key (Sno, Cno));
start transaction;
insert into SC values('01' , '01' , 80);
insert into SC values('01' , '02' , 90);
insert into SC values('01' , '03' , 99);
insert into SC values('02' , '01' , 70);
insert into SC values('02' , '02' , 60);
insert into SC values('02' , '03' , 80);
insert into SC values('03' , '01' , 80);
insert into SC values('03' , '02' , 80);
insert into SC values('03' , '03' , 80);
insert into SC values('04' , '01' , 50);
insert into SC values('04' , '02' , 30);
insert into SC values('04' , '03' , 20);
insert into SC values('05' , '01' , 76);
insert into SC values('05' , '02' , 87);
insert into SC values('06' , '01' , 31);
insert into SC values('06' , '03' , 34);
insert into SC values('07' , '02' , 89);
insert into SC values('07' , '03' , 98);
commit;
select count(*)
from teacher
where tname like '李%';
+----------+
| count(*) |
+----------+
| 1 |
+----------+
select Sno,Sname,Sage,Ssex --也可以用select * 替换
from student
where Sname like '%风%';
+-----+-------+------------+------+
| Sno | Sname | Sage | Ssex |
+-----+-------+------------+------+
| 03 | 孙风 | 1990-05-20 | 男 |
+-----+-------+------------+------+
select Ssex 性别,count(*) 人数
from student
group by Ssex;
+------+------+
| 性别 | 人数 |
+------+------+
| 男 | 4 |
| 女 | 4 |
+------+------+
select Cno 课程号,sum(Score) 总成绩
from sc
where Cno = '02';
+--------+--------+
| 课程号 | 总成绩 |
+--------+--------+
| 02 | 436.0 |
+--------+--------+
select Cno 课程号,avg(Score) as 平均成绩
from sc
group by Cno
order by 平均成绩 desc,课程号 asc;
+--------+----------+
| 课程号 | 平均成绩 |
+--------+----------+
| 02 | 72.66667 |
| 03 | 68.50000 |
| 01 | 64.50000 |
+--------+----------+
select Cno as 课程号, count(Sno) as 学生人数
from sc
group by 课程号;
+--------+----------+
| 课程号 | 学生人数 |
+--------+----------+
| 01 | 6 |
| 02 | 6 |
| 03 | 6 |
+--------+----------+
select Cno as 课程号, count(Sno) as 学生人数
from sc
group by 课程号
having count(Sno)>5;
+--------+----------+
| 课程号 | 学生人数 |
+--------+----------+
| 01 | 6 |
| 02 | 6 |
| 03 | 6 |
+--------+----------+
select sno 学生学号
from sc
group by sno
having count(sno)>= 2;
+----------+
| 学生学号 |
+----------+
| 01 |
| 02 |
| 03 |
| 04 |
| 05 |
| 06 |
| 07 |
+----------+
select *
from student
where sno in (select sno from sc);
+-----+-------+------------+------+
| Sno | Sname | Sage | Ssex |
+-----+-------+------------+------+
| 01 | 赵雷 | 1990-01-01 | 男 |
| 02 | 钱电 | 1990-12-21 | 男 |
| 03 | 孙风 | 1990-05-20 | 男 |
| 04 | 李云 | 1990-08-06 | 男 |
| 05 | 周梅 | 1991-12-01 | 女 |
| 06 | 吴兰 | 1992-03-01 | 女 |
| 07 | 郑竹 | 1989-07-01 | 女 |
+-----+-------+------------+------+
select * from student
where sno not in (select sno from sc where cno =01)
and sno in (select sno from sc where cno=02);
+-----+-------+------------+------+
| Sno | Sname | Sage | Ssex |
+-----+-------+------------+------+
| 07 | 郑竹 | 1989-07-01 | 女 |
+-----+-------+------------+------+
select * from student
where sno in(select sno from sc where cno='01')
and sno in (select sno from sc where cno='02');
+-----+-------+------------+------+
| Sno | Sname | Sage | Ssex |
+-----+-------+------------+------+
| 01 | 赵雷 | 1990-01-01 | 男 |
| 02 | 钱电 | 1990-12-21 | 男 |
| 03 | 孙风 | 1990-05-20 | 男 |
| 04 | 李云 | 1990-08-06 | 男 |
| 05 | 周梅 | 1991-12-01 | 女 |
+-----+-------+------------+------+
select Sno,Sname
from student
where sno in (select sno from sc group by sno having count(cno)=2);
+-----+-------+
| Sno | Sname |
+-----+-------+
| 05 | 周梅 |
| 06 | 吴兰 |
| 07 | 郑竹 |
+-----+-------+
select *
from student
where sno not in (select sno from sc group by sno having count(cno)=(select count(*) from course));
+-----+-------+------------+------+
| Sno | Sname | Sage | Ssex |
+-----+-------+------------+------+
| 05 | 周梅 | 1991-12-01 | 女 |
| 06 | 吴兰 | 1992-03-01 | 女 |
| 07 | 郑竹 | 1989-07-01 | 女 |
| 08 | 王菊 | 1990-01-20 | 女 |
+-----+-------+------------+------+
select *
from student
where sno in (select sno from sc group by sno having count(cno)= (select count(*) from course));
+-----+-------+------------+------+
| Sno | Sname | Sage | Ssex |
+-----+-------+------------+------+
| 01 | 赵雷 | 1990-01-01 | 男 |
| 02 | 钱电 | 1990-12-21 | 男 |
| 03 | 孙风 | 1990-05-20 | 男 |
| 04 | 李云 | 1990-08-06 | 男 |
+-----+-------+------------+------+
找的是所有课程都小于60的人,所以把大于60的人都给排除了,然后发现有的学生每月选课,所以还需要确保他的学号出现在sc表中。
select sno,sname
from student
where sno not in (select distinct sno from sc
where score >= 60) and sno in(select distinct sno from sc);
+-----+-------+
| sno | sname |
+-----+-------+
| 04 | 李云 |
| 06 | 吴兰 |
+-----+-------+
elect sno ,sname from student
where sno in(select distinct sno from sc
where cno = '01' and score >= 80);
+-----+-------+
| sno | sname |
+-----+-------+
| 01 | 赵雷 |
| 03 | 孙风 |
+-----+-------+
方法一:嵌套子查询
select *
from student
where sno in (select distinct sno from sc
where cno = (select cno from course
where tno = (select tno from teacher
where tname = '张三')));
+-----+-------+------------+------+
| Sno | Sname | Sage | Ssex |
+-----+-------+------------+------+
| 01 | 赵雷 | 1990-01-01 | 男 |
| 02 | 钱电 | 1990-12-21 | 男 |
| 03 | 孙风 | 1990-05-20 | 男 |
| 04 | 李云 | 1990-08-06 | 男 |
| 05 | 周梅 | 1991-12-01 | 女 |
| 07 | 郑竹 | 1989-07-01 | 女 |
+-----+-------+------------+------+
方法二:串联查询
select * from student
where sno in(select sno from sc,course,teacher
where teacher.tno = course.tno and tname= '张三' and sc.cno = course.cno);
和上题思路一致,写法比较多,是求17题的补集。
select sname from student
where sno not in (select sno from sc,course,teacher
where sc.cno = course.cno and course.tno = teacher.tno and teacher.tname = '张三');
+-------+
| sname |
+-------+
| 吴兰 |
| 王菊 |
+-------+
以为查询出来可能会包含’01’同学的信息,所以要把它去掉!
select *
from student
where sno in (select distinct sno from sc
where cno in (select cno from sc where sno = '01')) and sno != '01';
+-----+-------+------------+------+
| Sno | Sname | Sage | Ssex |
+-----+-------+------------+------+
| 02 | 钱电 | 1990-12-21 | 男 |
| 03 | 孙风 | 1990-05-20 | 男 |
| 04 | 李云 | 1990-08-06 | 男 |
| 05 | 周梅 | 1991-12-01 | 女 |
| 06 | 吴兰 | 1992-03-01 | 女 |
| 07 | 郑竹 | 1989-07-01 | 女 |
+-----+-------+------------+------+
SELECT语句默认返回所有匹配的行,它们可能是指定表中的每个行。为了返回第一行或前几行,可使用LIMIT子句,以实现分页查询。
COUNT()函数:
AVG()函数():
SUM()函数:
MAX()函数:
MIN()函数:
表与表之间常用的关联方式有两种:内连接、外连接,下面以MySQL为例来说明这两种连接方式。
内连接:内连接通过INNER JOIN
来实现,它将返回两张表中满足连接条件的数据,不满足条件的数据不会查询出来。
外连接:
外连接通过OUTER JOIN来实现,它会**返回两张表中满足连接条件的数据,同时返回不满足连接条件的数据。**外连接有两种形式:左外连接(LEFT OUTER JOIN)、右外连接(RIGHT OUTER JOIN)。
**自然关联:**自关联就是一张表自己与自己相关联,为了避免表名的冲突,需要在关联时通过别名将它们当做两张表来看待。一般在表中数据具有层级(树状)时,可以采用自关联一次性查询出多层级的数据。
索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: B 树, B+树和 Hash。索引的作用就相当于书的目录。
索引的优点:
索引的缺点 :
普通索引是MySQL中的基本索引类型,允许在定义索引的列中插入重复值和空值。
唯一索引要求索引列的值必须唯一,但允许有空值,避免重复的列出现,唯一索引可以有多个,多个列都可以标识为唯一索引。。
主键索引是一种特殊的唯一索引,不允许有空值,只能有一个,只能有一列作为主键。
单列索引即一个索引只包含单个列,一个表可以有多个单列索引。
组合索引是指在表的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用。使用组合索引时遵循最左前缀集合。
全文索引类型为FULLTEXT,在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。全文索引可以在CHAR、VARCHAR或者TEXT类型的列上创建。
空间索引是对空间数据类型的字段建立的索引,MySQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING和POLYGON。MySQL使用SPATIAL关键字进行扩展,使得能够用创建正规索引类似的语法创建空间索引。创建空间索引的列,必须将其声明为NOT NULL,空间索引只能在存储引擎为MyISAM的表中创建。
在创建表的时候创建索引:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FuwpZMAs-1666488157634)(image-20220912153340732.png)]
在已经存在的表中创建索引。可以使用ALTER TABLE语句或者CREATEINDEX语句。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NcunbXP3-1666488157634)(image-20220912153359912.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rGyihuzW-1666488157634)(image-20220912153405816.png)]
建议按照如下的原则来创建索引:
在select语句前面加EXPLAIN语句查看索引是否正在使用。
EXPLAIN语句将为我们输出详细的SQL执行信息,其中:possible_keys行给出了MySQL在搜索数据记录时可选用的各个索引。key行是MySQL实际选用的索引。
建议按照如下的原则来设计索引:
不是。
下列几种情况,是不适合创建索引的:
索引底层结构通常指的是B+Tree(多路平衡搜索树), 我们平时使用的如 ,聚集索引,复合索引,唯一索引等都默
认使用的是B+TREE,除此以外还有hash索引,或二叉树索引底层结构
B 树也称 B-树,全称为 多路平衡查找树 ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 Balanced (平衡)的意思。
目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。
B 树& B+树两者有何异同呢?
B树
B+树
B+树相比于B树,做了这样的升级:B+树中的非叶子节点都不存储数据,而是只作为索引。由叶子节点存放整棵树的所有数据。而叶子节点之间构成一个从小到大有序的链表互相指向相邻的叶子节点,也就是叶子节点之间形成了有序的双向链表。如下图B+树的结构。
hash索引底层就是hash表,进行查找时,调用一次hash函数就可以获取到相应的键值,之后进行回表查询获得实际数据。B+树底层实现是多路平衡查找树,对于每一次的查询都是从根节点出发,查找到叶子节点方可以获得所查键值,然后根据查询判断是否需要回表查询数据。它们有以下的不同:
hash索引的应用场景:如果存储的数据重复度很低(也就是说基数很大),对该列数据以等值查询为主,特别适合采用哈希索引。
B+树的应用场景:大多数场景下,都会有组合查询,范围查询、排序、分组、模糊查询等查询特征,Hash 索引无法满足要求,我们就需要B+树索引进行操作。
事务可由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成。在事务中的操作,要么都执行修改,要么都不执行,这就是事务的目的,也是事务模型区别于文件系统的重要特征之一。事务需遵循ACID四个特性:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3gelSXyn-1666488157635)(image-20220912201535837.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rpenHe40-1666488157635)(image-20220912201628209.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FwxGh6mO-1666488157635)(image-20220912201657271.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6R49P7hy-1666488157636)(image-20220912201732639.png)]
隔离性追求的是并发情形下事务之间互不干扰。那么隔离性的探讨,主要可以分为两个方面。MySQL 的隔离级别基于锁和 MVCC 机制(多版本并发控制机制)共同实现的。
(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性。
要求同一时刻只能有一个事务对数据进行写操作,事务在修改数据之前,需要先获得相应的锁。事务在修改数据之前,需要先获得相应的锁。获得锁之后,事务便可以修改数据。该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。
锁可以分为表锁、行锁以及其他位于二者之间的锁。
表锁在操作数据时会锁定整张表,并发性能较差。
行锁则只锁定需要操作的数据,并发性能好。
出于性能考虑,绝大多数情况下使用的都是行锁。
(一个事务)写操作对(另一个事务)读操作的影响:MVCC (多版本并发控制机制)保证隔离性。
MySQL的InnoDB引擎,在默认的REPEATABLE READ的隔离级别下,实现了可重复读,同时也解决了幻读问题。它使用Next-Key Lock算法实现了行锁,并且不允许读取已提交的数据,所以解决了不可重复读的问题。另外,该算法包含了间隙锁,会锁定一个范围,因此也解决了幻读的问题。
在MySQL默认的配置下,事务都是自动提交和回滚的。当显示地开启一个事务时,可以使用ROLLBACK语句进行回滚。该语句有两种用法:
当事务在对某个数据对象进行操作前,先向系统发出请求,对其加锁。加锁后事务就对该数据对象有了一定的控制,在该事务释放锁之前,其他的事务不能对此数据对象进行更新操作。
意向锁是表级锁,共有两种:
在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。意向锁之间是互相兼容的。意向锁和共享锁和排它锁互斥
死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。若无外力作用,事务都将无法推进下去。
视图是从一个或几个基本表导出的表。视图本身不独立存储在数据库中,是一个虚表视图是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改,查,操作,视图通常是有一个表或者多个表的行或列的子集。对视图的修改不影响基本表。它使得我们获取数据更容易,相比多表查询。
**游标是对查询出来的结果集作为一个单元来有效的处理。可以将游标简单的看成是查询的结果集的一-个指针,可以根据需要在结果集上面来回滚动,浏览需要的数据。**游标可以定在该单元中的特定行,从结果集的当前行检索一行或多行。可以对结果集当前行做修改。一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。
遍历数据行;
保存查询结果,方便下文调用。概念中提到使用游标会保存数据行的副本,那么创建游标后,下文查询即可从副本中查询,要比直接查数据库快很多。
数据的标准化有助于消除数据库中的数据冗余
第三范式通常被认为在性能,扩展性和数据完整性方面达到了最好的平衡
降低了查询效率,因为范式等级越高,设计出来的表就越多,进行数据查询的时候就可能需要关联多张表,不仅代价昂贵,而且可能会使得一些索引失效
范式只是提出设计的标标准,实际设计的时候,我们可能为了性能和读取效率违反范式的原则,通过增加少量的冗余或重复的数据来提高数据库的读取性能,减少关联查询,实现空间换时间的目的
一般来说在高并发的场景下对架构层进行优化其效果最为明显,常见的优化手段有:分布式缓存,读写分离,分库分表等,每种优化手段又适用于不同的应用场景。
分布式缓存
有句老话说的好,性能不够,缓存来凑。当需要在架构层进行优化时我们第一时间就会想到缓存这个神器,在应用与数据库之间增加一个缓存服务,如Redis或Memcache。
当接收到查询请求后,我们先查询缓存,判断缓存中是否有数据,有数据就直接返回给应用,如若没有再查询数据库,并加载到缓存中,这样就大大减少了对数据库的访问次数,自然而然也提高了数据库性能。
不过需要注意的是,引入分布式缓存后系统需要考虑如何应对缓存穿透、缓存击穿和缓存雪崩的问题。
读写分离
一主多从,读写分离,主动同步,是一种常见的数据库架构优化手段。
一般来说当你的应用是读多写少,数据库扛不住读压力的时候,采用读写分离,通过增加从库数量可以线性提升系统读性能。
主库,提供数据库写服务;从库,提供数据库读能力;主从之间,通过binlog同步数据。
当准备实施读写分离时,为了保证高可用,需要实现故障的自动转移,主从架构会有潜在主从不一致性问题。
水平切分
水平切分,也是一种常见的数据库架构优化手段。
当你的应用业务数据量很大,单库容量成为性能瓶颈后,采用水平切分,可以降低数据库单库容量,提升数据库写性能。
当准备实施水平切分时,需要结合实际业务选取合理的分片键(sharding-key)。
架构优化小结
读写分离主要是用于解决 “数据库读性能问题”
水平切分主要是用于解决“数据库数据量大的问题”
分布式缓存架构可能比读写分离更适用于高并发、大数据量大场景。
我们使用数据库,不管是读操作还是写操作,最终都是要访问磁盘,所以说磁盘的性能决定了数据库的性能。一块PCIE固态硬盘的性能是普通机械硬盘的几十倍不止。这里我们可以从吞吐率、IOPS两个维度看一下机械硬盘、普通固态硬盘、PCIE固态硬盘之间的性能指标。
吞吐率:单位时间内读写的数据量
机械硬盘:约100MB/s ~ 200MB/s
普通固态硬盘:200MB/s ~ 500MB/s
PCIE固态硬盘:900MB/s ~ 3GB/s
IOPS:每秒IO操作的次数
机械硬盘:100 ~200
普通固态硬盘:30000 ~ 50000
PCIE固态硬盘:数十万
通过上面的数据可以很直观的看到不同规格的硬盘之间的性能差距非常大,当然性能更好的硬盘价格会更贵,在资金充足并且迫切需要提升数据库性能时,尝试更换一下数据库的硬盘不失为一个非常好的举措,你之前遇到SQL执行缓慢问题在你更换硬盘后很可能将不再是问题。
SQL执行慢有时候不一定完全是SQL问题,手动安装一台数据库而不做任何参数调整,再怎么优化SQL都无法让其性能最大化。要让一台数据库实例完全发挥其性能,首先我们就得先优化数据库的实例参数。
数据库实例参数优化遵循三句口诀:日志不能小、缓存足够大、连接要够用。
数据库事务提交后需要将事务对数据页的修改刷( fsync)到磁盘上,才能保证数据的持久性。这个刷盘,是一个随机写,性能较低,如果每次事务提交都要刷盘,会极大影响数据库的性能。数据库在架构设计中都会采用如下两个优化手法:
先将事务写到日志文件RedoLog(WAL),将随机写优化成顺序写
加一层缓存结构Buffer,将单次写优化成顺序写
所以日志跟缓存对数据库实例尤其重要。而连接如果不够用,数据库会直接抛出异常,系统无法访问。
SQL优化很容易理解,就是通过给查询字段添加索引或者改写SQL提高其执行效率,一般而言,SQL编写有以下几个通用的技巧:
合理使用索引
使用UNION ALL替代UNION
避免select * 写法
JOIN字段建议建立索引
避免复杂SQL语句
避免where 1=1写法
避免order by rand()类似写法
执行计划
要想优化SQL必须要会看执行计划,执行计划会告诉你哪些地方效率低,哪里可以需要优化。通过explain sql 可以查看执行计划
SQL优化小结
查看执行计划 explain sql
如果有告警信息,查看告警信息 show warnings;
查看SQL涉及的表结构和索引信息
根据执行计划,思考可能的优化点
按照可能的优化点执行表结构变更、增加索引、SQL改写等操作
查看优化后的执行时间和执行计划
如果优化效果不明显,重复第四步操作
1、定义:
2、相同点
3、不同点
在mysql中,货币常使用Decimal和Numric类型的字段表示,这两种类型被MySQL实现为同样的类型;它们被用于保存值,该值的准确精度是极其重要的值,例如与金钱有关的数据。
排序方法有十种,分别是:一、冒泡排序;二、选择排序;三、插入排序;四、希尔排序;五、归并排序;六、快速排序;七、堆排序;八、计数排序;九、桶排序;十、基数排序。
冒泡排序是排序算法中较为简单的一种,英文称为Bubble Sort。它遍历所有的数据,每次对相邻元素进行两两比较,如果顺序和预先规定的顺序不一致,则进行位置交换;这样一次遍历会将最大或最小的数据上浮到顶端,之后再重复同样的操作,直到所有的数据有序。
选择排序简单直观,英文称为Selection Sort,先在数据中找出最大或最小的元素,放到序列的起始;然后再从余下的数据中继续寻找最大或最小的元素,依次放到排序序列中,直到所有数据样本排序完成。
插入排序英文称为Insertion Sort,它通过构建有序序列,对于未排序的数据序列,在已排序序列中从后向前扫描,找到相应的位置并插入,类似打扑克牌时的码牌。插入排序有一种优化的算法,可以进行拆半插入。
基本思路是先将待排序序列的第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列;然后从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置,直到所有数据都完成排序;如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。
希尔排序也称递减增量排序,是插入排序的一种改进版本,英文称为Shell Sort,效率虽高,但它是一种不稳定的排序算法。
插入排序在对几乎已经排好序的数据操作时,效果是非常好的;但是插入排序每次只能移动一位数据,因此插入排序效率比较低。
希尔排序在插入排序的基础上进行了改进,它的基本思路是先将整个数据序列分割成若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时,再对全部数据进行依次直接插入排序。
归并排序英文称为Merge Sort,归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。它首先将数据样本拆分为两个子数据样本, 并分别对它们排序, 最后再将两个子数据样本合并在一起; 拆分后的两个子数据样本序列, 再继续递归的拆分为更小的子数据样本序列, 再分别进行排序, 直到最后数据序列为1,而不再拆分,此时即完成对数据样本的最终排序。
归并排序严格遵循从左到右或从右到左的顺序合并子数据序列, 它不会改变相同数据之间的相对顺序, 因此归并排序是一种稳定的排序算法.
作为一种典型的分而治之思想的算法应用,归并排序的实现分为两种方法:
1、自上而下的递归;
2、自下而上的迭代;
快速排序,英文称为Quicksort,又称划分交换排序 partition-exchange sort 简称快排。
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。首先从数列中挑出一个元素,并将这个元素称为「基准」,英文pivot。重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。之后,在子序列中继续重复这个方法,直到最后整个数据序列排序完成。
堆排序,英文称Heapsort,是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序实现分为两种方法:
1、大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
2、小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
算法步骤:
1、创建一个堆 H[0……n-1];
2、把堆首(最大值)和堆尾互换;
3、把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
4、重复步骤 2,直到堆的尺寸为 1
计数排序英文称Counting sort,是一种稳定的线性时间排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于 i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。基本的步骤如下:
1、找出待排序的数组中最大和最小的元素
2、统计数组中每个值为i的元素出现的次数,存入数组C的第i项
3、对所有的计数累加,从C中的第一个元素开始,每一项和前一项相加
4、反向填充目标数组,将每个元素i放在新数组的第C[i]项,每放一个元素就将C[i]减去1
桶排序也称为箱排序,英文称为 Bucket Sort。它是将数组划分到一定数量的有序的桶里,然后再对每个桶中的数据进行排序,最后再将各个桶里的数据有序的合并到一起。
基数排序英文称Radix sort,是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串和特定格式的浮点数,所以基数排序也仅限于整数。它首先将所有待比较数值,统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XkooLQ6J-1666488157637)(%E6%88%AA%E5%B1%8F2022-09-13%2018.59.54-16630715078703.png)]
每个结点不是红色就是黑色
根节点是黑色的
如果一个节点是红色的,则它的两个孩子结点必须是黑色的(这就约束了红黑树里面没有连续的红色节点)
对于每个结点,从该结点到其所有可到达的叶结点的路径中,均包含相同数目的黑色结点(即每条路径都有相同数量的黑色节点,注意:路径是走到 NIL 空节点)
每个 NIL 叶子结点都是黑色的(此处的叶子结点指的是空结点)
OSI七层协议包括:物理层,数据路层,网络层,运输层,会话层,表示层, 应用层
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UC8yecRv-1666488157637)(%E4%B8%83%E5%B1%82%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84%E5%9B%BE.png)]
网络层有四个协议:ARP协议,IP协议,ICMP协议,IGMP协议。ARP协议为IP协议提供服务,IP协议为ICMP协议提供服务,ICMP协议为IGMP协议提供服务。
ARP协议:
地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议。
主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。
地址解析协议是建立在网络中各个主机互相信任的基础上的,局域网络上的主机可以自主发送ARP应答消息,其他主机收到应答报文时不会检测该报文的真实性就会将其记入本机ARP缓存;由此攻击者就可以向某一主机发送伪ARP应答报文,使其发送的信息无法到达预期的主机或到达错误的主机,这就构成了一个ARP欺骗。
ARP命令可用于查询本机ARP缓存中IP地址和MAC地址的对应关系、添加或删除静态对应关系等。相关协议有RARP、代理ARP。NDP用于在IPv6中代替地址解析协议。
IP协议:
IP是Internet Protocol(网际互连协议)的缩写,是TCP/IP体系中的网络层协议。设计IP的目的是提高网络的可扩展性:一是解决互联网问题,实现大规模、异构网络的互联互通;二是分割顶层网络应用和底层网络技术之间的耦合关系,以利于两者的独立发展。根据端到端的设计原则,IP只为主机提供一种无连接、不可靠的、尽力而为的数据包传输服务。
ICMP协议:
ICMP(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议簇的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。
ICMP使用IP的基本支持,就像它是一个更高级别的协议,但是,ICMP实际上是IP的一个组成部分,必须由每个IP模块实现。
IGMP协议:
互联网组管理协议(IGMP,Internet Group Management Protocol)是因特网协议家族中的一个组播协议。
TCP/IP协议族的一个子协议,用于IP主机向任一个直接相邻的路由器报告他们的组成员情况。允许Internet主机参加多播,也是IP主机用作向相邻多目路由器报告多目组成员的协议。多目路由器是支持组播的路由器,向本地网络发送IGMP查询。主机通过发送IGMP报告来应答查询。组播路由器负责将组播包转发到所有网络中组播成员。
互联网组管理协议(IGMP)是对应于开源系统互联(OSI)七层框架模型中网络层的协议。在互联网工程任务组(The Internet Engineering Task Force,简称IETF)编写的标准文档(RFC)2236.中对Internet组管理协议(IGMP)做了详尽的描述。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4jFVRigJ-1666488157638)(watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY2hlbnl1bnFp,size_16,color_FFFFFF,t_70.png)]
TCP/IP五层协议包括:物理层,数据链路层,网络层,运输层,应用层
主要解决两台物理机之间的通信,通过二进制比特流的传输来实现,二进制数据表现为电流电压上的强弱,到达目的地再转化为二进制机器码。网卡、集线器工作在这一层。
**在不可靠的物理介质上提供可靠的传输,接收来自物理层的位流形式的数据,并封装成帧,传送到上一层;**同样,也将来自上层的数据帧,拆装为位流形式的数据转发到物理层。**这一层在物理层提供的比特流的基础上,通过差错控制、流量控制方法,使有差错的物理线路变为无差错的数据链路。**提供物理地址寻址功能。交换机工作在这一层。
将网络地址翻译成对应的物理地址,并决定如何将数据从发送方路由到接收方,通过路由选择算法为分组通过通信子网选择最佳路径。路由器工作在这一层。
传输层提供了进程间的逻辑通信,传输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看起来像是在两个传输层实体之间有一条端到端的逻辑通信信道。
**建立会话:身份验证,权限鉴定等;**保持会话:对该会话进行维护,在会话维持期间两者可以随时使用这条会话传输局;断开会话:当应用程序或应用层规定的超时时间到期后,OSI会话层才会释放这条会话。
对数据格式进行编译,对收到或发出的数据根据应用层的特征进行处理,如处理为文字、图片、音频、视频、文档等,还可以对压缩文件进行解压缩、对加密文件进行解密等。
提供应用层协议,如HTTP协议,FTP协议等等,方便应用程序之间进行通信。
TCP作为面向流的协议,提供可靠的、面向连接的运输服务,并且提供点对点通信 UDP作为面向报文的协议,不提供可靠交付,并且不需要连接,不仅仅对点对点,也支持多播和广播 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fx3H289r-1666488157638)(image-20220914102855862-16636424517972.png)]
TCP 用于在传输层有必要实现可靠传输的情况,UDP 用于对高速传输和实时性有较高要求的通信。TCP
和 UDP 应该根据应用目的按需使用。
TCP有三次握手建立连接,四次挥手关闭连接的机制。除此之外还有滑动窗口和拥塞控制算法。最最关键的是还保留超时重传的机制。对于每份报文也存在校验,保证每份报文可靠性。
检验和:
通过检验和的方式,接收端可以检测出来数据是否有差错和异常,假如有差错就会直接丢
弃TCP段,重新发送。
序列号/确认应答:
序列号的作用不仅仅是应答的作用,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据。TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文,这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。
滑动窗口:
滑动窗口既提高了报文传输的效率,也避免了发送方发送过多的数据而导致接收方无法正常处理的异常。
超时重传:
超时重传是指发送出去的数据包到接收到确认包之间的时间,如果超过了这个时间会被认为是丢包了,需要重传。最大超时时间是动态计算的。
拥塞控制:
在数据传输过程中,可能由于网络状态的问题,造成网络拥堵,此时引入拥塞控制机制,在保证TCP可靠性的同时,提高性能。
流量控制:
如果主机A 一直向主机B发送数据,不考虑主机B的接受能力,则可能导致主机B的接受缓冲区满了而无法再接受数据,从而会导致大量的数据丢包,引发重传机制。而在重传的过程中,若主机B的接收缓冲区情况仍未好转,则会将大量的时间浪费在重传数据上,降低传送数据的效率。所以引入流量控制机制,主机B通过告诉主机A自己接收缓冲区的大小,来使主机A控制发送的数据量。流量控制与TCP协议报头中的窗口大小有关。
UDP面向数据报无连接的,数据报发出去,就不保留数据备份了。仅仅在IP数据报头部加入校验和复用。UDP没有服务器和客户端的概念。UDP报文过长的话是交给IP切成小段,如果某段报废报文就废了。
TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:
UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:
TCP是面向流协议,发送的单位是字节流,因此会将多个小尺寸数据被封装在一个tcp报文中发出去的可能性。可以简单的理解成客户端调用了两次send,服务器端一个recv就把信息都读出来了。
固定发送信息长度,或在两个信息之间加入分隔符。
滑动窗口是传输层进行流量控制的一种措施,接收方通过通告发送方自己的窗口大小,从而控制发送方的发送速度,防止发送方发送速度过快而导致自己被淹没。
在进行数据传输时,如果传输的数据比较大,就需要拆分为多个数据包进行发送。TCP 协议需要对数据进行确认后,才可以发送下一个数据包。这样一来,就会在等待确认应答包环节浪费时间。为了避免这种情况TCP引入了窗口概念。窗口大小指的是不需要等待确认应答包而可以继续发送数据包的最大值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K7fJ57UR-1666488157638)(image-20220914104634239-16636424517973.png)]
可以看到滑动窗口左边的是已发送并且被确认的分组,滑动窗口右边是还没有轮到的分组。
滑动窗口里面也分为两块,一块是已经发送但是未被确认的分组,另一块是窗口内等待发送的分组。随着已发送的分组不断被确认,窗口内等待发送的分组也会不断被发送。整个窗口就会往右移动,让还没轮到的分组进入窗口内。
可以看到滑动窗口起到了一个限流的作用,也就是说当前滑动窗口的大小决定了当前 TCP 发送包的速率,而滑动窗口的大小取决于拥塞控制窗口和流量控制窗口的两者间的最小值。
拥塞是指一个或者多个交换点的数据报超载,TCP又会有重传机制,导致过载。**为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量。**TCP 一共使用了四种算法来实现拥塞控制:
当cwnd(拥塞窗口) < ssthresh(门限值) 时,使用慢开始算法。当cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。当cwnd = ssthresh 时,即可使用慢开始算法,也可使用拥塞避免算法。
慢开始:
由小到大逐渐增加拥塞窗口的大小,每接一次报文,cwnd指数增加。
拥塞避免:
cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1。
快重传:
如果在超时重传定时器溢出之前,接收到连续的三个重复冗余ACK,发送端便知晓哪个报文段在传输过程中丢失了,于是重发该报文段,不需要等待超时重传定时器溢出再发送该报文。
快恢复之前的策略:
发送方判断网络出现拥塞,就把ssthresh设置为出现拥塞时发送方窗口值的一半,继续执行慢开始,之后进行拥塞避免。
快恢复:
主要是配合快重传。当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半(为了预防网络发生拥塞),但接下来并不执行慢开始算法,因为如果网络出现拥塞的话就不会收到好几个重复的确认,收到三个重复确认说明网络状况还可以。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-riegBGw0-1666488157639)(image-20220914105715573-16636424517974.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aM4nntpP-1666488157639)(%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B2.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6dqc7cCm-1666488157639)(%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B.png)]
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。
所以三次握手就能确认双发收发功能都正常,缺一不可。
不行。TCP进行可靠传输的关键就在于维护一个序列号,三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值。 如果只是两次握手, 至多只有客户端的起始序列号能被确认, 服务器端的序列号则得不到确认。
主要有三个原因:
接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。
SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement)消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。
双方通信无误必须是两者互相发送信息都无误。传了 SYN,证明发送方到接收方的通道没有问题,但是接收方到发送方的通道还需要 ACK 信号来进行验证。
TCP握手中,当服务器处于SYN_RCVD 状态,服务器会把此种状态下请求连接放在一个队列里,该队列称为半连接队列。
SYN攻击即利用TCP协议缺陷,通过发送大量的半连接请求,占用半连接队列,耗费CPU和内存资源。
解决方法:
记录IP,若连续受到某个IP的重复SYN报文,从这个IP地址来的包会被一概丢弃。
通过防火墙、路由器等过滤网关防护。
通过加固 TCP/IP 协议栈防范,如增加最大半连接数,缩短超时时间(缩短SYN Timeout时间 )。
SYN cookies技术。SYN Cookies 是对 TCP 服务器端的三次握手做一些修改,专门用来防范 SYN
洪泛攻击的一种手段
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gJMkrHCc-1666488157640)(image-20220919101333184.png)]
断开一个 TCP 连接则需要“四次挥手”:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PK3OCIte-1666488157640)(TCP%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B.png)]
主要原因是当服务端收到客户端的 FIN 数据包后,服务端可能还有数据没发完,不会立即close。 所以服务端会先将 ACK 发过去告诉客户端我收到你的断开请求了,但请再给我一点时间,这段时间用来发送剩下的数据报文,发完之后再将 FIN 包发给客户端表示现在可以断了。之后客户端需要收到 FIN 包后发送 ACK 确认断开信息给服务端。
MSL即报文最大生存时间。设置2MSL可以保证上一次连接的报文已经在网络中消失,不会出现与新TCP连接报文冲突的情况。
总体来说分为以下几个过程:
参考链接:https://segmentfault.com/a/1190000006879700
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mWxX4bKO-1666488157641)(url%E8%BE%93%E5%85%A5%E5%88%B0%E5%B1%95%E7%A4%BA%E5%87%BA%E6%9D%A5%E7%9A%84%E8%BF%87%E7%A8%8B.jpg)]
DNS协议是基于UDP的应用层协议,它的功能是根据用户输入的域名,解析出该域名对应的IP地址,从而给客户端进行访问。
客户机发出查询请求,在本地计算机缓存查找,若没有找到,就会将请求发送给dns服务器
本地dns服务器会在自己的区域里面查找,找到即根据此记录进行解析,若没有找到,就会在本地的缓存里面查找
本地服务器没有找到客户机查询的信息,就会将此请求发送到根域名dns服务器
根域名服务器解析客户机请求的根域部分,它把包含的下一级的dns服务器的地址返回到客户机的dns服务器地址
客户机的dns服务器根据返回的信息接着访问下一级的dns服务器
这样递归的方法一级一级接近查询的目标,最后在有目标域名的服务器上面得到相应的IP信息
客户机的本地的dns服务器会将查询结果返回给我们的客户机
客户机根据得到的ip信息访问目标主机,完成解析过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3oxKzaNI-1666488157641)(view.png)]
http协议是超文本传输协议。它是基于TCP协议的应用层传输协议,即客户端和服务端进行数据传输的一种规则。该协议本身HTTP 是一种无状态的协议。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mJx83MgR-1666488157642)(%E5%90%84%E7%A7%8D%E5%8D%8F%E8%AE%AE%E4%B8%8EHTTP%E5%8D%8F%E8%AE%AE%E4%B9%8B%E9%97%B4%E7%9A%84%E5%85%B3%E7%B3%BB.png)]
HTTP 协议本身是无状态的,为了使其能处理更加复杂的逻辑,HTTP1.1 引入 Cookie 来保存状态信息。
Cookie是由服务端产生的,再发送给客户端保存,当客户端再次访问的时候,服务器可根据cookie辨识客户端是哪个,以此可以做个性化推送,免账号密码登录等等。
HTTP Cookie(也叫 Web Cookie或浏览器 Cookie)是**服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。**通常,**它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。**Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。Cookie 主要用于以下三个方面:
session用于标记特定客户端信息,存在在服务器的一个文件里。一般客户端带Cookie对服务器进行访问,可通过cookie中的session id从整个session中查询到服务器记录的关于客户端的信息。
**Session 代表着服务器和客户端一次会话的过程。Session 对象存储特定用户会话所需的属性及配置信息。**这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当客户端关闭会话,或者 Session 超时失效时会话结束。
**用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session ,请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器,浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名。**当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。
根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。
DDos全称Distributed Denial of Service,分布式拒绝服务攻击。最基本的DOS攻击过程如下:
DDoS则是采用分布式的方法,通过在网络上占领多台“肉鸡”,用多台计算机发起攻击。
DOS攻击现在基本没啥作用了,因为服务器的性能都很好,而且是多台服务器共同作用,1V1的模式黑
客无法占上风。对于DDOS攻击,预防方法有:
XSS也称 cross-site scripting,跨站脚本。这种攻击是由于服务器将攻击者存储的数据原原本本地显示给其他用户所致的。比如一个存在XSS漏洞的论坛,用户发帖时就可以引入带有<script>标签的 代码,导致恶意代码的执行。预防措施有:
SQL 注入就是在用户输入的字符串中加入 SQL 语句,如果在设计不良的程序中忽略了检查,那么这些注入进去的 SQL 语句就会被数据库服务器误认为是正常的 SQL 语句而运行,攻击者就可以执行计划外的命令或访问未被授权的数据。 SQL注入的原理主要有以下 4 点:
避免SQL注入的一些方法:
多台服务器以对称的方式组成一个服务器集合,每台服务器都具有等价的地位,能互相分担负载。
轮询法:将请求按照顺序轮流的分配到服务器上。大锅饭,不能发挥某些高性能服务器的优势。
随机法:随机获取一台,和轮询类似。
哈希法:通过ip地址哈希化来确定要选择的服务器编号。好处是,每次客户端访问的服务器都是同一
个服务器,能很好地利用session或者cookie。
加权轮询:根据服务器性能不同加权。
常见状态码:
共同点:301和302状态码都表示重定向,就是说浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取(用户看到的效果就是他输入的地址A瞬间变成了另一个地址B)。
不同点:****301表示旧地址A的资源已经被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;302表示旧地址A的资源还在(仍然可以访问),这个重定向只是临时地从旧地址A跳转到地址B,搜索引擎会抓取新的内容而保存旧的网址。 SEO中302好于301。
补充,重定向原因:
转发是服务器行为。服务器直接向目标地址访问URL,将相应内容读取之后发给浏览器,用户浏览器地址栏URL不变,转发页面和转发到的页面可以共享request里面的数据。
重定向是利用服务器返回的状态码来实现的,如果服务器返回301或者302,浏览器收到新的消息后自动跳转到新的网址重新请求资源。用户的地址栏url会发生改变,而且不能共享数据。
规定了请求头和请求尾,响应头和响应尾(get post)
每一个请求都是一个单独的连接,做不到连接的复用
HTTP1.1默认开启长连接,**在一个TCP连接上可以传送多个HTTP请求和响应。**使用 TCP 长连接的方式改善了 HTTP1.0 短连接造成的性能开销。
支持管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。
服务端无法主动push
HTTP中的长连接短连接指HTTP底层TCP的连接。
短连接:客户端与服务器进行一次HTTP连接操作,就进行一次TCP连接,连接结束TCP关闭连接。
长连接:如果HTTP头部带有参数keep-alive,即开启长连接网页完成打开后,底层用于传输数据的TCP连接不会直接关闭,会根据服务器设置的保持时间保持连接,保持时间过后连接关闭。
请求报文格式:
GET/sample.jspHTTP/1.1 请求行
Accept:image/gif.image/jpeg, 请求头部
Accept-Language:zh-cn
Connection:Keep-Alive Host:localhost
User-Agent:Mozila/4.0(compatible;MSIE5.01;Window NT5.0)
Accept-Encoding:gzip,deflate
username=jinqiao&password=1234 请求主体
响应报文:
HTTP/1.1 200 OK
Server:Apache Tomcat/5.0.12
Date:Mon,6Oct2003 13:23:42 GMT
Content-Length:112
<html>
<head>
<title>HTTP响应示例<title>
head>
<body>Hello HTTP!
body>
html>
**提出多路复用。**多路复用前,文件时串行传输的,请求a文件,b文件只能等待,并且连接数过多。引入多路复用,a文件b文件可以同时传输。
**引入了二进制数据帧。**其中帧对数据进行顺序标识,有了序列id,服务器就可以进行并行传输数据。
HTTP2.0相比HTTP1.1支持的特性:
客户端明确的请求。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G7LZuof4-1666488157643)(image-20220919152210376.png)]
优点:
缺点:
SSL全称为Secure Sockets Layer即安全套接层,其继任为TLSTransport Layer Security传输层安全协议,均用于在传输层为数据通讯提供安全支持。
可以将HTTPS协议简单理解为HTTP协议+TLS/SSL
为了方便记忆,可以将PUT、DELETE、POST、GET理解为客户端对服务端的增删改查。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BR0qXMMp-1666488157643)(image-20220919155651862.png)]
Get:指定资源请求数据,刷新无害,Get请求的数据会附加到URL中,传输数据的大小受到url的限制。
Post:向指定资源提交要被处理的数据。刷新会使数据会被重复提交。post在发送数据前会先将请求头发送给服务器进行确认,然后才真正发送数据。
一般HTTP协议里并不限制参数大小限制。但一般由于get请求是直接附加到地址栏里面的,由于浏览器地址栏有长度限制,因此使GET请求在浏览器实现层面上看会有长度限制。
REST API全称为表述性状态转移(Representational State Transfer,REST)即利用HTTP中get、post、put、delete以及其他的HTTP方法构成REST中数据资源的增删改查操作:
包含:请求方法字段、URL字段、HTTP协议版本
产生请求的浏览器类型,请求数据,主机地址。
DNS是指将网页域名翻译为对应的IP的一种方法。DNS劫持指攻击者篡改结果,使用户对域名的解析IP变成了另一个IP。
我通过以下四点向您介绍一下什么是操作系统吧!
介绍系统调用之前,我们先来了解一下用户态和系统态。
根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:
说了用户态和系统态之后,那么什么是系统调用呢?
我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的系统态级别的子功能咋办呢?那就需要系统调用了!
也就是说在我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。
这些系统调用按功能大致可分为如下几类:
操作系统的基本特征包括并发、共享、虚拟和异步。这些概念对理解和堂握操作系统的核心至关重要,将一直贯穿于各个章节中。
并发是指两个或多个事件在同一时间间隔(时间段)内发生。操作系统的并发性是指计算机系统中同时在在多个运行的程序,因此它且有处理和调度多个程序同时执行的能力。在操作系统中,引入进程的目的就是使程序能并发地执行。
注意同一时间间隔(并发)和同一时刻(并行)的区别:
并发:关注的是同一时间间隔——即时间段内发生的事件数量,比如午餐时段内,学校餐厅共接纳了2000名同学用餐,那么,该餐厅在午餐时段的并发量就是2000。
并行:并行性是指系统具有同时进行运算或操作的特性,在同一时刻能完成两种或两种以上的工作。比如说,学校餐厅有四个出餐口,那么该餐厅的并行数为4,即同一时间点最多能同时为四名同学打餐。
并行性需要有相关硬件的支持,如多流水线或多处理机硬件环境。并发性体现系统在某段时间内的工作效率,并行性体现该系统“三心二意”的能力。
在多道程序环境下,一段时间内,宏观上有多道程序在同时执行,而在每个时刻,实际仅有一道程序执行(单处理机环境下),因此微观上这些程序仍是分时交替执行的。操作系统的并发性通过分时得以实现。
共享即资源共享,是指系统中的资源可供内存中多个并发执行的进程共同使用。共享可分为以下两种资源共享方式。
(1)互斥共享方式
系统中的某些资源,如打印机、磁带机,虽然可供多个进程伸用,但为使得所打印或记录的结果不致造成混淆,应规定在一段时间内只允许一个进程访问该资源。为此,当进程A访问某个资源时,必须先提出请求,若此时该资源空闲,则系统便将之分配给进程A使用,此后有其它进程也要访问该资源时,只要A未用完,其它进程就必须等待。仅当进程A访问完并释放该资源后,才允许另一个进程对该资源进行访问。我们把这种资源共享方式称为互斥式共享,而把在一段时间内只允许一个进程访问的资源称为临界资源或独占资源,计算机系统中的大多数物理设各及其此软件中所用的栈,变最和表格,都是于临界咨源,它们都被要求互斥地进行享。
(2)同时访问方式
系统中还有另一类资源,这类资源允许在一段时间内由多个进程“同时”访问(这里所说的“同时”通常是宏观上的,而在微观上,这些进程可能是交替地对该资源进行访问即“分时共享的)。可供多个进程“同时”访问的典型资源是磁盘设备。
注意,互斥共享要求一种资源在一段时间内(哪怕是一段很小的时间)只能满足一个请求,否则就会出现数据的错乱,比如多个进程同时访问打印机,打印的内容交错地出现在不同的文件上,那太糟糕了!同时访问共享通常要求一个请求分几个时间片段间隔地完成,其效果与连续完成的效果相同。
并发和共享是操作系统两个最基本的特征,两者互为存在条件:
①资源共享是以程序的并发为条件的,若系统不允许程序并发执行,则自然不存在资源共享问题;
②若系统不能对资源共享实施有效的管理,则必将影响到程序的并发执行,甚至根本无法并发执行。
虚拟是指把一个物理上的实体变为若干逻辑上的对应物。物理实体(前者)是实际存在的,而后者是虚的,是用户“感觉上”的现象。用于实现虚拟的技术,称为虚拟技术。操作系统中利用了多种虚拟技术来实现虚拟处理器、虚拟内存和虚拟外部设备等。
虚拟处理器技术:通过多道程序设计技术,采用让多道程序并发执行的方法,来分时使用一个处理器。此时,虽然只有一个处理器,但它能同时为多个用户服务,使每个终端用户都感觉有一个中央外理器(CPU)在专门为它服务。利用多道程序设计技术把一个物理上的CPU虚拟为多个逻辑上的CPU,称为虚拟处理器。
虚拟存储器技术:将一台机器的物理存储器变为虚拟存储器,以便从逻辑上扩充存储器的容量。当然,这时用户所感觉到的内存容量是虚的。我们把用户感觉到(但实际不存在)的存储器称为虚拟存储器。
虚拟设备技术:将一台物理Ⅰ/O设备虚拟为多台逻辑上的Ⅰ/O设备,并允许每个用户占用一台逻辑上的/设备,使原来仅允许在一段时间内由一个用户访问的设备(即临界资源)变为在一段时间内允许多个用户同时访问的共享设备。
操作系统的虚拟技术可归纳为两类:
多道程序环境下,允许多个程序并发执行,但由于资源有限,进程的执行并不是一贯到底的,而是走走停停的,它以不可预知的速度向前推进,这就是进程的异步性。
异步性使得操作系统运行在一种随机的环境下,可能导致进程产生与时间有关的错误(就像对全局变量的访问顺序不当会导致程序出错一样)。然而,只要运行环境相同,操作系统就须保证多次运行进程后都能获得相同的结果。
进程的结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W7kcm1KZ-1666488157644)(image-20220920111514292.png)]
运行态→阻塞态:往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的。
阻塞态→就绪态:则是等待的条件已满足,只需分配到处理器后就能运行。
运行态→就绪态:不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。
就绪态→运行态:系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-31gKfppi-1666488157644)(image-20220920111919200.png)]
进程控制:挂起与激活
为了系统和用户观察和分析进程
挂起原语: suspend
激活原语:active
进程切换分两步:
切换页表以使用新的地址空间,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作
废了。
切换内核栈和硬件上下文。
对于linux来说,线程和进程的最大区别就在于地址空间,
对于线程切换,第1步是不需要做的,第2步是进程和线程切换都要做的。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。
进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个Cache就是TLB(translation Lookaside Buffer,TLB本质上就是一个Cache,是用来加速页表查找的)。
由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表 也要进行切换,页表切换后TLB就失效了,Cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。
管道:管道这种通讯方式有两种限制,一是半双工的通信,数据只能单向流动,二是只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。管道可以分为两类:匿名管道和命名管道。匿名管道是单向的,只能在有亲缘关系的进程间通信;命名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
信号 : 信号是一种比较复杂的通信方式,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
Linux系统中常用信号:
(1)SIGHUP:用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。
(2)SIGINT:程序终止信号。程序运行过程中,按 Ctrl+C 键将产生该信号。
(3)SIGQUIT:程序退出信号。程序运行过程中,按 Ctrl+\ \键将产生该信号。
(4)SIGBUS和SIGSEGV:进程访问非法地址。
(5)SIGFPE:运算中出现致命错误,如除零操作、数据溢出等。
(6)SIGKILL:用户终止进程执行信号。shell下执行 kill -9 发送该信号。
(7)SIGTERM:结束进程信号。shell下执行 kill 进程pid 发送该信号。
(8)SIGALRM:定时器信号。
(9)SIGCLD:子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程。
信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列:消息队列是消息的链接表,包括Posix消息队列和System V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
Socket:与其他通信机制不同的是,它可用于不同机器间的进程通信。
区别:
临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
互斥量:为协调共同对一个共享资源的单独访问而设计的。互斥量跟临界区很相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限。
信号量:为控制一个具有有限数量用户资源而设计。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。互斥量是信号量的一种特殊情况,当信号量的最大资源数=1就是互斥量了。
事件: 用来通知线程有一些事件已发生,从而启动后继任务的开始。
每个进程中访问临界资源的那段程序称为临界区,一次仅允许一个进程使用的资源称为临界资源。
解决冲突的办法:
从线程的运行空间来说,分为用户级线程(user-level thread, ULT)和内核级线程(kernel-level,KLT)
在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲就是两个或多个进程无限期的阻塞、相互等待的一种状态。
有一个条件不成立,则不会产生死锁
常用的处理死锁的方法有:死锁预防、死锁避免、死锁检测、死锁解除、鸵鸟策略。
基本思想就是确保死锁发生的四个必要条件中至少有一个不成立:
- 破除资源互斥条件
- 破除“请求与保持”条件:实行资源预分配策略,进程在运行之前,必须一次性获取所有的资源。缺点:在很多情况下,无法预知进程执行前所需的全部资源,因为进程是动态执行的,同时也会降低资源利用率,导致降低了进程的并发性。
- 破除“不可剥夺”条件:允许进程强行从占有者那里夺取某些资源。当一个已经保持了某些不可被抢占资源的进程,提出新的资源请求而不能得到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。这意味着进程已经占有的资源会被暂时被释放,或者说被抢占了。
- 破除“循环等待”条件:实行资源有序分配策略,对所有资源排序编号,按照顺序获取资源,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。
死锁预防通过约束资源请求,防止4个必要条件中至少一个的发生,可以通过直接或间接预防方法,但是都会导致低效的资源使用和低效的进程执行。而死锁避免则允许前三个必要条件,但是通过动态地检测资源分配状态,以确保循环等待条件不成立,从而确保系统处于安全状态。所谓安全状态是指:如果系统能按某个顺序为每个进程分配资源(不超过其最大值),那么系统状态是安全的,换句话说就是,如果存在一个安全序列,那么系统处于安全状态。银行家算法是经典的死锁避免的算法。
死锁预防策略是非常保守的,他们通过限制访问资源和在进程上强加约束来解决死锁的问题。死锁检测则是完全相反,它不限制资源访问或约束进程行为,只要有可能,被请求的资源就被授权给进程。但是操作系统会周期性地执行一个算法检测前面的循环等待的条件。死锁检测算法是通过资源分配图来检测是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有存在环,也就是检测到死锁的发生。
死锁解除的常用方法就是终止进程和资源抢占,回滚。所谓进程终止就是简单地终止一个或多个进程以打破循环等待,包括两种方式:终止所有死锁进程和一次只终止一个进程直到取消死锁循环为止;所谓资源抢占就是从一个或者多个死锁进程那里抢占一个或多个资源。
把头埋在沙子里,假装根本没发生问题。因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任何措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。
简单分为连续分配管理方式和非连续分配管理方式这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间,常见的如 块式管理 。同样地,非连续分配管理方式允许一个程序使用的内存分布在离散或者说不相邻的内存中,常见的如页式管理 和 段式管理。
简单来说:页是物理单位,段是逻辑单位。分页可以有效提高内存利用率,分段可以更好满足用户需求。
把内存空间划分为大小相等且固定的块,作为主存的基本单位。因为程序数据存储在不同的页面中,而页面又离散的分布在内存中,因此需要一个页表来记录映射关系,以实现从页号到物理块号的映射。
访问分页系统中内存数据需要两次的内存访问 (一次是从内存中访问页表,从中找到指定的物理块号,加上页内偏移得到实际物理地址;第二次就是根据第一次得到的物理地址访问内存取出数据)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GPnV3MvE-1666488157645)(image-20220920155822029.png)]
分页是为了提高内存利用率,而分段是为了满足程序员在编写代码的时候的一些逻辑需求(比如数据共享,数据保护,动态链接等)。
分段内存管理当中,地址是二维的,一维是段号,二维是段内地址;其中每个段的长度是不一样 的,而且每个段内部都是从0开始编址的。由于分段管理中,每个段内部是连续内存分配,但是段和段之间是离散分配的,因此也存在一个逻辑地址到物理地址的映射关系,相应的就是段表机制。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1EMGYUOu-1666488157645)(image-20220920160116926.png)]
操作系统把物理内存(physical RAM)分成一块一块的小内存,每一块内存被称为页**(page)。当内存资源不足时,Linux把某些页的内容转移至硬盘上的一块空间上,以释放内存空间。硬盘上的那块空间叫做交换空间**(swap space),而这一过程被称为交换(swapping)。物理内存和交换空间的总容量就是虚拟内存的可用容量。
用途:
地址映射过程中,若在页面中发现所要访问的页面不在内存中,则发生缺页中断 。
缺页中断 就是要访问的页不在主存,需要操作系统将其调入主存后再进行访问。 在这个时候,被内存映射的文件实际上成了一个分页交换文件。
当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。用来选择淘汰哪一页的规则叫做页面置换算法,我们可以把页面置换算法看成是淘汰页面的规则。
在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WjdbEDyU-1666488157645)(image-20220920161651902.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aM2pgAlV-1666488157645)(image-20220920201019877.png)]
开发过程中,最常见的就是互斥锁的了,互斥锁加锁失败时,会用「线程切换」来应对,当加锁失败的线程再次加锁成功后的这一过程,会有两次线程上下文切换的成本,性能损耗比较大。
如果我们明确知道被锁住的代码的执行时间很短,那我们应该选择开销比较小的自旋锁,因为自旋锁加锁失败时,并不会主动产生线程切换,而是一直忙等待,直到获取到锁,那么如果被锁住的代码执行时间很短,那这个忙等待的时间相对应也很短。
如果能区分读操作和写操作的场景,那读写锁就更合适了,它允许多个读线程可以同时持有读锁,提高了读的并发性。根据偏袒读方还是写方,可以分为读优先锁和写优先锁,读优先锁并发性很强,但是写线程会被饿死,而写优先锁会优先服务写线程,读线程也可能会被饿死,那为了避免饥饿的问题,于是就有了公平读写锁,它是用队列把请求锁的线程排队,并保证先入先出的原则来对线程加锁,这样便保证了某种线程不会被饿死,通用性也更好点。
互斥锁和自旋锁都是最基本的锁,读写锁可以根据场景来选择这两种锁其中的一个进行实现。
另外,互斥锁、自旋锁、读写锁都属于悲观锁,悲观锁认为并发访问共享资源时,冲突概率可能非常高,所以在访问共享资源前,都需要先加锁。
相反的,如果并发访问共享资源时,冲突概率非常低的话,就可以使用乐观锁,它的工作方式是,在访问共享资源时,不用先加锁,修改完共享资源后,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
但是,一旦冲突概率上升,就不适合使用乐观锁了,因为它解决冲突的重试成本非常高。
不管使用的哪种锁,我们的加锁的代码范围应该尽可能的小,也就是加锁的粒度要小,这样执行速度会比较快。再来,使用上了合适的锁,就会快上加快了。
最底层的两种就是会「互斥锁和自旋锁」,有很多高级的锁都是基于它们实现的,你可以认为它们是各种锁的地基,所以我们必须清楚它俩之间的区别和应用。
加锁的目的就是保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。
当已经有一个线程加锁后,其他线程加锁则就会失败,互斥锁和自旋锁对于加锁失败后的处理方式是不一样的:
互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞。
对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NGO6zcsd-1666488157646)(%E4%BA%92%E6%96%A5%E9%94%81%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png)]
所以,互斥锁加锁失败时,会从用户态陷入到内核态,让内核帮我们切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本。
那这个开销成本是什么呢?会有两次线程上下文切换的成本:
线程的上下文切换的是什么?当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
上下切换的耗时有大佬统计过,大概在几十纳秒到几微秒之间,如果你锁住的代码执行时间比较短,那可能上下文切换的时间都比你锁住的代码执行时间还要长。
所以,如果你能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁。
自旋锁是通过 CPU 提供的 CAS
函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。
一般加锁的过程,包含两个步骤:
CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。
比如,设锁为变量 lock,整数 0 表示锁是空闲状态,整数 pid 表示线程 ID,那么 CAS(lock, 0, pid) 就表示自旋锁的加锁操作,CAS(lock, pid, 0) 则表示解锁操作。
使用自旋锁的时候,当发生多线程竞争锁的情况,加锁失败的线程会「忙等待」,直到它拿到锁。这里的「忙等待」可以用 while
循环等待实现,不过最好是使用 CPU 提供的 PAUSE
指令来实现「忙等待」,因为可以减少循环等待时的耗电量。
自旋锁是最比较简单的一种锁,一直自旋,利用 CPU 周期,直到锁可用。需要注意,在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。
自旋锁开销少,在多核系统下一般不会主动产生线程切换,适合异步、协程等在用户态切换请求的编程方式,但如果被锁住的代码执行时间过长,自旋的线程会长时间占用 CPU 资源,所以自旋的时间和被锁住的代码执行的时间是成「正比」的关系,我们需要清楚的知道这一点。
自旋锁与互斥锁使用层面比较相似,但实现层面上完全不同:当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对。
它俩是锁的最基本处理方式,更高级的锁都会选择其中一个来实现,比如读写锁既可以选择互斥锁实现,也可以基于自旋锁实现。
读写锁从字面意思我们也可以知道,它由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。
所以,读写锁适用于能明确区分读操作和写操作的场景。
读写锁的工作原理是:
所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。
知道了读写锁的工作原理后,我们可以发现,读写锁在读多写少的场景,能发挥出优势。
另外,根据实现的不同,读写锁可以分为「读优先锁」和「写优先锁」。
读优先锁期望的是,读锁能被更多的线程持有,以便提高读线程的并发性,它的工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取写锁。如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eJx7nXnJ-1666488157646)(%E8%AF%BB%E4%BC%98%E5%85%88%E9%94%81%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png)]
而「写优先锁」是优先服务写线程,其工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取写锁。如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KeQhil9M-1666488157646)(%E5%86%99%E4%BC%98%E5%85%88%E9%94%81%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png)]
读优先锁对于读线程并发性更好,但也不是没有问题。我们试想一下,如果一直有读线程获取读锁,那么写线程将永远获取不到写锁,这就造成了写线程「饥饿」的现象。
写优先锁可以保证写线程不会饿死,但是如果一直有写线程获取写锁,读线程也会被「饿死」。
既然不管优先读锁还是写锁,对方可能会出现饿死问题,那么我们就不偏袒任何一方,搞个「公平读写锁」。
公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。
互斥锁和自旋锁都是最基本的锁,读写锁可以根据场景来选择这两种锁其中的一个进行实现。
前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。
悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
那相反的,如果多线程同时修改共享资源的概率比较低,就可以采用乐观锁。
乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
放弃后如何重试,这跟业务场景息息相关,虽然重试的成本很高,但是冲突的概率足够低的话,还是可以接受的。
可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现乐观锁全程并没有加锁,所以它也叫无锁编程。
这里举一个场景例子:在线文档。
我们都知道在线文档可以同时多人编辑的,如果使用了悲观锁,那么只要有一个用户正在编辑文档,此时其他用户就无法打开相同的文档了,这用户体验当然不好了。
那实现多人同时编辑,实际上是用了乐观锁,它允许多个用户打开同一个文档进行编辑,编辑完提交之后才验证修改的内容是否有冲突。
怎么样才算发生冲突?这里举个例子,比如用户 A 先在浏览器编辑文档,之后用户 B 在浏览器也打开了相同的文档进行编辑,但是用户 B 比用户 A 提交早,这一过程用户 A 是不知道的,当 A 提交修改完的内容时,那么 A 和 B 之间并行修改的地方就会发生冲突。
服务端要怎么验证是否冲突了呢?通常方案如下:
实际上,我们常见的 SVN 和 Git 也是用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否产生了冲突,发生了冲突的地方,需要我们自己修改后,再重新提交。
乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。
缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。
危害有以下两点:
造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入。
为什么要有虚拟地址空间呢?
先从没有虚拟地址空间的时候说起吧!没有虚拟地址空间的时候,程序直接访问和操作的都是物理内存 。但是这样有什么问题呢?
总结来说:如果直接把物理地址暴露出来的话会带来严重问题,比如可能对操作系统造成伤害以及给同时运行多个程序造成困难。
通过虚拟地址访问内存有以下优势:
回答1:
这个在我们平时使用电脑特别是 Windows 系统的时候太常见了。很多时候我们使用了很多占内存的软件,这些软件占用的内存可能已经远远超出了我们电脑本身具有的物理内存。为什么可以这样呢? 正是因为 虚拟内存 的存在,通过 虚拟内存 可以让程序可以拥有超过系统物理内存大小的可用内存空间。另外,虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间)。这样会更加有效地管理内存并减少出错。
虚拟内存就是说,让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。虚拟内存使用部分加载的技术,让一个进程或者资源的某些页面加载进内存,从而能够加载更多的进程,甚至能加载比内存大的进程,这样看起来好像内存变大了,这部分内存其实包含了磁盘或者硬盘,并且就叫做虚拟内存。
回答2:
虚拟内存是操作系统管理内存的一种技术,它使应用程序有一段连续可用的地址空间的假象,实际上是很多个物理内存碎片,它可以使应用程序拥有比实际物理内存更大的逻辑内存,实际上上将程序中不需要的数据放到硬盘上,需要时在进行数据交换。
主要原理:每个进程拥有独立地址空间,这个地址空间被分多个大小相等的快,称为页,这些页被映射到物理内存,但不需要映射的连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。
虚拟地址通过mmu(内存管理单元)进行物理地址和虚拟地址的转化。其中每个进程都有一个也表里面映射着页号与帧号,通过页号查询到帧号,cpu判断当前的帧号是否访问越界,如果是就中断,否则就通过页内偏移量和帧号计算出物理地址。但是这样访问速度较慢,所以MMU里面就会有TLB,可以看成是个缓存,操作系统会先查TLB中虚拟地址与物理地址的映射,如果有的话,就不要查页表了,否则查询页表,然后更新TLB的数据。
查询页表的时候还要检测标志为是否为1,如果是1说明有页在内存中,如果0的话说明没有页在内存中这是时候就发生缺页中断从而将该页调入内存中,此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。这时候就会用到页面置换算法 。
虚拟内存中,允许将一个作业分多次调入内存。釆用连续分配方式时,会使相当一部分内存空间都处于暂时或永久的空闲状态,造成内存资源的严重浪费,而且也无法从逻辑上扩大内存容量。因此,虚拟内存的实需要建立在离散分配的内存管理方式的基础上。虚拟内存的实现有以下三种方式:
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多 路复用适用如下场合:
用户态和系统态是操作系统的两种运行状态:
内核态:内核态运行的程序可以访问计算机的任何数据和资源,不受限制,包括外围设备,比如网卡、硬盘等。处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用CPU 不会发生抢占情况。
用户态:用户态运行的程序只能受限地访问内存,只能直接读取用户程序的数据,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。
将操作系统的运行状态分为用户态和内核态,主要是为了对访问能力进行限制,防止随意进行一些比较危险的操作导致系统的崩溃,比如设置时钟、内存清理,这些都需要在内核态下完成 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bIlNogX7-1666488157646)(image-20220913102958261.png)]
它们是构成用户应用程序主干的对象。Bean 由 Spring IoC 容器管理。它们由 Spring IoC 容器实例化,配置,装配和管理。Bean 是基于用户提供给容器的配置元数据创建。
set
方法注入、构造方法注入、字段注入,而注入类型分为值类型注入(8 种基本数据类型)和引用类型注入(将依赖对象注入)。@Controller
、@Service
、@Repository
、@Component
之外,还有一些比较常用的方式**@Configuration + @Bean**,@Import。FactoryBean
接口的类TeacherFactoryBean,然后通过 @Configuration
+ @Bean
的方式将 TeacherFactoryBean
加入到容器中。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VQCCEMON-1666488157647)(image-20220913103110078.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W8TfLvd3-1666488157647)(image-20220913103223540.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8yXhtBVG-1666488157647)(image-20220913103301339.png)]
Spring通过IoC容器来管理Bean,我们可以通过XML配置或者注解配置,来指导IoC容器对Bean的管理。因为注解配置比XML配置方便很多,所以现在大多时候会使用注解配置的方式。
以下是管理Bean时常用的一些注解:
@ComponentScan用于声明扫描策略,通过它的声明,容器就知道要扫描哪些包下带有声明的类,也可以知道哪些特定的类是被排除在外的。
@Component、@Repository、@Service、@Controller用于声明Bean,它们的作用一样,但是语义不同。@Component用于声明通用的Bean,@Repository用于声明DAO层的Bean,@Service用于声明业务层的Bean,@Controller用于声明视图层的控制器Bean,被这些注解声明的类就可以被容器扫描并创建。
@Autowired、@Qualifier用于注入Bean,即告诉容器应该为当前属性注入哪个Bean。其中,@Autowired是按照Bean的类型进行匹配的,如果这个属性的类型具有多个Bean,就可以通过@Qualifier指定Bean的名称,以消除歧义。
@Scope用于声明Bean的作用域,默认情况下Bean是单例的,即在整个容器中这个类型只有一个实例。可以通过@Scope注解指定prototype值将其声明为多例的,也可以将Bean声明为session级作用域、request级作用域等等,但最常用的还是默认的单例模式。
@PostConstruct、@PreDestroy用于声明Bean的生命周期。其中,被@PostConstruct修饰的方法将在Bean实例化后被调用,@PreDestroy修饰的方法将在容器销毁前被调用。
@Component、@Repository、@Service、@Controller用于声明Bean,它们的作用一样,但是语义不同。@Component用于声明通用的Bean,@Repository用于声明DAO层的Bean,@Service用于声明业务层的Bean,@Controller用于声明视图层的控制器Bean,被这些注解声明的类就可以被容器扫描并创建。
我们是在使用Spring框架的过程中,其实就是为了使用IOC,依赖注入,和AOP,面向切面编程,这两个是Spring的灵魂。主要用到的设计模式有工厂模式和代理模式。
IOC就是典型的工厂模式,通过sessionfactory去注入实例。AOP就是典型的代理模式的体现。
代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。
spring的IoC容器是spring的核心,spring AOP是spring框架的重要组成部分。
在传统的程序设计中,当调用者需要被调用者的协助时,通常由调用者来创建被调用者的实例。但在spring里创建被调用者的工作不再由调用者来完成,因此控制反转(IoC);创建被调用者实例的工作通 常由spring容器来完成,然后注入调用者,因此也被称为依赖注入(DI),依赖注入和控制反转是同一 个概念。
面向方面编程(AOP)是以另一个角度来考虑程序结构,通过分析程序结构的关注点来完善面向对象编程(OOP)。OOP将应用程序分解成各个层次的对象,而AOP将程序分解成多个切面。spring AOP 只实现了方法级别的连接点,在J2EE应用中,AOP拦截到方法级别的操作就已经足够。在spring中,未来使IoC方便地使用健壮、灵活的企业服务,需要利用spring AOP实现为IoC和企业服务之间建立联系。
IOC:控制反转也叫依赖注入。利用了工厂模式
将对象交给容器管理,你只需要在 spring 配置文件总配置相应的 bean ,以及设置相关的属性,让spring 容器来生成类的实例对象以及管理对象。在 spring 容器启动的时候, spring 会把你在配置文件中配置的 bean 都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些 bean 分配给你需要调用这些 bean 的类(假设这个类名是 A ),分配的方法就是调用 A的 setter 方法来注入,而不需要你在 A 里面 new 这些 bean 了。
spring ioc初始化流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PC63vGKP-1666488157648)(image-20220923205713854.png)]
AOP:面向切面编程。(Aspect-Oriented Programming)
AOP可以说是对OOP的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。
简单点解释,比方说你想在你的biz层所有类中都加上一个打印‘你好’的功能,这时就可以用aop思想来做。你先写个类写个类方法,方法经实现打印‘你好’,然后Ioc这个类 ref="biz.*"
让每个类都注入即可实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LNdWomWC-1666488157648)(image-20220913103758032.png)]
@Component、@Repository、@Service、@Controller用于声明Bean,它们的作用一样,但是语义不同。@Component用于声明通用的Bean,@Repository用于声明DAO层的Bean,@Service用于声明业务层的Bean,@Controller用于声明视图层的控制器Bean,被这些注解声明的类就可以被容器扫描并创建。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gSduqyWp-1666488157649)(image-20220913104419315.png)]
当 bean 在 Spring 容器中组合在一起时,它被称为装配或 bean 装配。Spring容器需要知道需要什么 bean 以及容器应该如何使用依赖注入来将 bean 绑定在一起,同时装配 bean。
注解装配在默认情况下是不开启的,为了使用注解装配,我们必须在 Spring 配置文件中配置 context:annotation-config/元素。
有五种自动装配的方式,可以用来指导 Spring 容器用自动装配方式来进行依赖注入。
重写:你仍需用 和 配置来定义依赖,意味着总要重写自动装配。
基本数据类型:你不能自动装配简单的属性,如基本数据类型,String 字符串,和类。
模糊特性:自动装配不如显式装配精确,如果有可能,建议使用显式装配。
@Autowired是Spring提供的注解,@Resource是JDK提供的注解。
@Autowired是只能按类型注入,如果我们想使用按名称装配,可以结合@Qualifier注解一起使用。@Resource默认按名称注入,也支持按类型注入。@Resource如果没有指定name属性,并且按照默认的名称仍然找不到依赖对象时, @Resource注解会回退到按类型装配。但一旦指定了name属性,就只能按名称装配了。
为了提高性能。
由于不会每次都新创建新对象,所以就减少了新生成实例的消耗。因为spring会通过反射或者cglib来生成bean实例这都是耗性能的操作,其次给对象分配内存也会涉及复杂算法。
减少JVM垃圾回收,由于不会给每个请求都新生成bean实例,所以自然回收的对象少了。
可以快速获取到bean,因为单例的获取bean操作除了第一次生成之外其余的都是从缓存里获取的所以很快。
缺点就是在并发环境下可能会出现线程安全问题。
在bean时,加一个scope = “singleton”;如果不写个默认是true,也就是单例的,写了就是多例的。
首先,需要明确的是spring对循环依赖的处理有三种情况:
构造器的循环依赖:这种依赖spring是处理不了的,直接抛出BeanCurrentlylnCreationException异常。
单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖。
非单例循环依赖:无法处理。
接下来,我们具体看看spring是如何处理第二种循环依赖的。Spring单例对象的初始化大略分为三步:
createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象;
populateBean:填充属性,这一步主要是多bean的依赖属性进行填充;
initializeBean:调用spring xml中的init 方法。
从上面讲述的单例bean初始化步骤我们可以知道,循环依赖主要发生在第一步、第二步。也就是构造器循环依赖和field循环依赖。 Spring为了解决单例的循环依赖问题,使用了三级缓存。这三级缓存的作用分别是:
singletonFactories : 进入实例化阶段的单例对象工厂的cache (三级缓存);
earlySingletonObjects :完成实例化但是尚未初始化的,提前暴光的单例对象的Cache (二级缓存);
singletonObjects:完成初始化的单例对象的cache(一级缓存)。
1.创建前准备
2.正式创建bean (实例化,在堆区分配内存,属性为默认值)
3.依赖注入,主要是给属性赋值 (初始化)
4.执行相关的初始化方法 (初始化)
使用bean
5.bean的销毁
Bean 的生命周期,就是一个 Bean 从创建到销毁,所经历的各种方法调用。 简单的来说,一个Bean的生命周期分为四个阶段:实例化(Instantiation)、 属性设置(populate)、初始化(Initialization)、销毁(Destruction)。
实例化:程序启动后,Spring把注解或者配置文件定义好的Bean对象转换成一个BeanDefination对象,然后完成整个BeanDefination的解析和加载的过程。Spring获取到这些完整的对象之后,会对整个BeanDefination进行实例化操作,实例化是通过反射的方式创建对象。
属性设置:实例化后的对象被封装在BeanWrapper对象中,并且此时对象仍然是一个原生的状态,并没有进行依赖注入。Spring根据BeanDefinition中的信息进行依赖注入, populateBean方法来完成属性的注入。
初始化
销毁:判断是否实现了DisposableBean接口,调用destoryMethod方法
当一个 bean 仅被用作另一个 bean 的属性时,它能被声明为一个内部 bean,为了定义 inner bean,在 Spring 的 基于 XML 的 配置元数据中,可以在或元素内使用 元素,内部 bean 通常是匿名的,它们的 Scope 一般是 prototype。
AOP(Aspect-Oriented Programming), 即面向切面编程, 它与 OOP( Object-Oriented Programming,面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角。
在 OOP 中, 我们以类(class)作为我们的基本单元,而 AOP 中的基本单元是 Aspect(切面)。
连接点(join point):对应的是具体被拦截的对象,因为Spring只能支持方法,所以被拦截的对象往往就是指特定的方法,AOP将通过动态代理技术把它织入对应的流程中。
切点(point cut):有时候,我们的切面不单单应用于单个方法,也可能是多个类的不同方法,这时,可以通过正则式和指示器的规则去定义,从而适配连接点。切点就是提供这样一个功能的概念。
通知(advice):就是按照约定的流程下的方法,分为前置通知、后置通知、环绕通知、事后返回通知和异常通知,它会根据约定织入流程中。
目标对象(target):即被代理对象。
引入(introduction):是指引入新的类和其方法,增强现有Bean的功能。
织入(weaving):它是一个通过动态代理技术,为原有服务对象生成代理对象,然后将与切点定义匹配的连接点拦截,并按约定将各类通知织入约定流程的过程。
切面(aspect):是一个可以定义切点、各类通知和引入的内容,SpringAOP将通过它的信息来增强Bean的功能或者将对应的方法织入流程。
关注点是应用中一个模块的行为,一个关注点可能会被定义成一个我们想实现的 一个功能。
横切关注点是一个关注点,此关注点是整个应用都会使用的功能,并影响整个应用,比如日志,安全和数据传输,几乎应用的每个模块都需要的功能。因此这些 都属于横切关注点。
before:前置通知,在一个方法执行前被调用。
after: 在方法执行之后调用的通知,无论方法执行是否成功。
after-returning: 仅当方法成功完成后执行的通知。
after-throwing: 在方法抛出异常退出时执行的通知。
around: 在方法执行之前和之后调用的通知。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lXCtgjh8-1666488157649)(image-20220913110121474.png)]
JDK动态代理:这是Java提供的动态代理技术,可以在运行时创建接口的代理实例。Spring AOP默认采用这种方式,在接口的代理实例中织入代码。
CGLib动态代理:采用底层的字节码技术,在运行时创建子类代理的实例。当目标对象不存在接口时,Spring AOP就会采用这种方式,在子类实例中织入代码。
在性能方面,CGLib创建的代理对象比JDK动态代理创建的代理对象高很多。但是,CGLib在创建代理对象时所花费的时间比JDK动态代理多很多。
所以,对于单例的对象因为无需频繁创建代理对象,采用CGLib动态代理比较合适。反之,对于多例的对象因为需要频繁的创建代理对象,则JDK动态代理更合适。
Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。
Spring AOP 仅支持方法级别的 PointCut;提供了完全的 AOP 支持,它还支持属性级别的 PointCut。
Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单。
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。
Spring AOP为IoC的使用提供了更多的便利,一方面,应用可以直接使用AOP的功能,设计应用的横切关注点,把跨越应用程序多个模块的功能抽象出来,并通过简单的AOP的使用,灵活地编制到模块中,比如可以通过AOP实现应用程序中的日志功能。另一方面,在Spring内部,一些支持模块也是通过Spring AOP来实现的,比如事务处理。从这两个角度就已经可以看到Spring AOP的核心地位了。
Spring AOP只能对IoC容器中的Bean进行增强,对于不受容器管理的对象不能增强。
由于CGLib采用动态创建子类的方式生成代理对象,所以不能对final修饰的类进行代理。
工厂设计模式 : Spring使用工厂模式通过 BeanFactory 、 ApplicationContext 创建 bean 对象。
代理设计模式 : Spring AOP 功能的实现。
单例设计模式 : Spring 中的 Bean 默认都是单例的。
模板方法模式 : Spring 中 jdbcTemplate 、 hibernateTemplate 等以 Template 结尾的对数据库操作
的类,它们就使用到了模板模式。
包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不
同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
观察者模式*** Spring 事件驱动模型就是观察者模式很经典的一个应用。
适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器
模式适配 Controller 。
DAO是Data Access Object的缩写,即数据访问对象,在项目中它通常作为独立的一层,专门用于访问数据库。这一层的具体实现技术有很多,常用的有Spring JDBC、Hibernate、JPA、MyBatis等,在Spring框架下无论采用哪一种技术访问数据库,它的编程模式都是统一的。
Spring主要提供了两种类型的容器:BeanFactory和ApplicationContext。
BeanFactory:是基础类型的IoC容器,提供完整的IoC服务支持。如果没有特殊指定,默认采用延迟初始化策略。只有当客户端对象需要访问容器中的某个受管对象的时候,才对该受管对象进行初始化以及依赖注入操作。所以,相对来说,容器启动初期速度较快,所需要的资源有限。对于资源有限,并且功能要求不是很严格的场景,BeanFactory是比较合适的IoC容器选择。
BeanFactory是一个类工厂,与传统类工厂不同的是,BeanFactory是类的通用工厂,它可以创建并管理各种类的对象。这些可被创建和管理的对象本身没有什么特别之处,仅是一个POJO,Spring称这些被创建和管理的Java对象为Bean。并且,Spring中所说的Bean比JavaBean更为宽泛一些,所有可以被Spring容器实例化并管理的Java类都可以成为Bean。
BeanFactory是Spring容器的顶层接口,Spring为BeanFactory提供了多种实现,最常用的是XmlBeanFactory。但它在Spring 3.2中已被废弃,建议使用XmlBeanDefinitionReader、DefaultListableBeanFactory替代。BeanFactory最主要的方法就是 getBean(String beanName),该方法从容器中返回特定名称的Bean。
Bean 工厂是工厂模式的一个实现,提供了控制反转功能,用来把应用的配置和依赖从正真的应用代码中分离。最常用的 BeanFactory 实现是 XmlBeanFactory 类。
最常用的就是 org.springframework.beans.factory.xml.XmlBeanFactory ,它根据 XML 文件中的定义加载 beans。该容器从 XML 文件读取配置元数据并用它去创建一个完全配置的系统或应用。
ApplicationContext:它是在BeanFactory的基础上构建的,是相对比较高级的容器实现,除了拥有BeanFactory的所有支持,
ApplicationContext还提供了其他高级特性,比如事件发布、国际化信息支持等。
ApplicationContext所管理的对象,在该类型容器启动之后,默认全部初始化并绑定完成。所以,相对于BeanFactory来说,ApplicationContext要求更多的系统资源,同时,因为在启动时就完成所有初始化,容器启动时间较之BeanFactory也会长一些。在那些系统资源充足,并且要求更多功能的场景中,ApplicationContext类型的容器是比较合适的选择。
WebApplicationContext 是 ApplicationContext 的扩展。它具有 Web 应用 程序所需的一些额外功能。它与普通的 ApplicationContext 在解析主题和决定 与哪个 servlet 关联的能力方面有所不同。
FileSystemXmlApplicationContext :此容器从一个 XML 文件中加载 beans 的定义,XML Bean 配置文件的全路径名必须提供给它的构造函数。
ClassPathXmlApplicationContext:此容器也从一个 XML 文件中加载 beans 的定义,这里,你需要正确设置 classpath 因为这个容器将在 classpath里找 bean 配置。
WebXmlApplicationContext:此容器加载一个 XML 文件,此文件定义了一个 WEB 应用的所有 bean。
编程式事务:
Spring提供了TransactionTemplate模板,利用该模板我们可以通过编程的方式实现事务管理,而无需关注资源获取、复用、释放、事务同步及异常处理等操作。相对于声明式事务来说,这种方式相对麻烦一些,但是好在更为灵活,我们可以将事务管理的范围控制的更为精确。
声明式事务:
Spring事务管理的亮点在于声明式事务管理,它允许我们通过声明的方式,在IoC配置中指定事务的边界和事务属性,Spring会自动在指定的事务边界上应用事务属性。相对于编程式事务来说,这种方式十分的方便,只需要在需要做事务管理的方法上,增加@Transactional注解,以声明事务特征即可。
事务的打开、回滚和提交是由事务管理器来完成的,我们使用不同的数据库访问框架,就要使用与之对应的事务管理器。在Spring Boot中,当你添加了数据库访问框架的起步依赖时,它就会进行自动配置,即自动实例化正确的事务管理器。
对于声明式事务,是使用@Transactional进行标注的。这个注解可以标注在类或者方法上。当它标注在类上时,代表这个类所有公共(public)非静态的方法都将启用事务功能。当它标注在方法上时,代表这个方法将启用事务功能。
另外,在@Transactional注解上,我们可以使用isolation属性声明事务的隔离级别,使用propagation属性声明事务的传播机制。
Spring默认的事务传播行为是PROPAGATION_REQUIRED,它适用于绝大多数情况。
TransactionDefinition 接⼝中定义了五个表示隔离级别的常量:
TransactionDefinition.ISOLATION_DEFAULT: 使⽤后端数据库默认的隔离级别,Mysql默认采⽤的 REPEATABLE_READ隔离级别, Oracle 默认采⽤的 READ_COMMITTED隔离级别.
TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻⽌脏读,但是幻读或不可重复读仍有可能发⽣
TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本身事务⾃⼰所修改,可以阻⽌脏读和不可重复读,但幻读仍有可能发⽣。
TransactionDefinition.ISOLATION_SERIALIZABLE: 最⾼的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执⾏,这样事务之间就完全不可能产⽣⼲扰,也就是说,该级别可以防⽌脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会⽤到该级别。
Spring 的 WEB 模块是构建在 application context 模块基础之上,提供一个适合 web 应用的上下文。这个模块也包括支持多种面向 web 的任务,如透明地处理多个文件上传请求和程序级请求参数的绑定到你的业务对象。它也有对 Jakarta Struts 的支持。
Spring 提供以下几种集合的配置元素:
@RequestMapping 注解用于将特定 HTTP 请求方法映射到将处理相应请求的控制器中的特定类/方法。此注释可应用于两个级别:
类级别:映射请求的 URL
方法级别:映射 URL 以及 HTTP 请求方法
Spring DAO 使得 JDBC,Hibernate 或 JDO 这样的数据访问技术更容易以一种统一的方式工作。这使得用户容易在持久性技术之间切换。它还允许您在编写代码时,无需考虑捕获每种技术不同的异常。
MVC是一种设计模式,在这种模式下软件被分为三层,即Model(模型)、View(视图)、Controller(控制器)。Model代表的是数据,View代表的是用户界面,Controller代表的是数据的处理逻辑,它是Model和View这两层的桥梁。将软件分层的好处是,可以将对象之间的耦合度降低,便于代码的维护。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e7TVeYWc-1666488157650)(image-20220913113834399.png)]
主要组件
SpingMVC的工作原理
组件 | 说明 |
---|---|
DispatcherServlet | Spring MVC 的核心组件,是请求的入口,负责协调各个组件工作 |
MultipartResolver | 内容类型( Content-Type )为 multipart/* 的请求的解析器,例如解析处理文件上传的请求,便于获取参数信息以及上传的文件 |
HandlerMapping | 请求的处理器匹配器,负责为请求找到合适的HandlerExecutionChain 处理器执行链,包含处理器( handler )和拦截器( interceptors ) |
HandlerAdapter | 处理器的适配器。因为处理器 handler 的类型是 Object 类型,需要有一个调用者来实现 handler 是怎么被执行。Spring 中的处理器的实现多变,比如用户处理器可以实现Controller 接口、HttpRequestHandler 接口,也可以用@RequestMapping 注解将方法作为一个处理器等,这就导致 Spring MVC 无法直接执行这个处理器。所以这里需要一个处理器适配器,由它去执行处理器 |
HandlerExceptionResolver | 处理器异常解析器,将处理器( handler )执行时发生的异常,解析( 转换 )成对应的 ModelAndView 结果 |
RequestToViewNameTranslator | 视图名称转换器,用于解析出请求的默认视图名 |
LocaleResolver | 本地化(国际化)解析器,提供国际化支持 |
ThemeResolver | 主题解析器,提供可设置应用整体样式风格的支持 |
ViewResolver | 视图解析器,根据视图名和国际化,获得最终的视图 View对象 |
FlashMapManager | FlashMap 管理器,负责重定向时,保存参数至临时存储(默认 Session) |
拦截器会对处理器进行拦截,这样通过拦截器就可以增强处理器的功能。Spring MVC中,所有的拦截器都需要实现HandlerInterceptor接口,该接口包含如下三个方法:preHandle()、postHandle()、afterCompletion()。
通过上图可以看出,Spring MVC拦截器的执行流程如下:
Spring MVC拦截器的开发步骤如下:
开发拦截器:实现handlerInterceptor接口,从三个方法中选择合适的方法,实现拦截时要执行的具体业务逻辑。
注册拦截器:定义配置类,并让它实现WebMvcConfigurer接口,在接口的addInterceptors方法中,注册拦截器,并定义该拦截器匹配哪些请求路径。
如果是对Controller记性拦截,则可以使用Spring MVC的拦截器。
如果是对所有的请求(如访问静态资源的请求)进行拦截,则可以使用Filter。
如果是对除了Controller之外的其他Bean的请求进行拦截,则可以使用Spring AOP。
@RequestMapping:
作用:该注解的作用就是用来处理请求地址映射的,也就是说将其中的处理器方法映射到url路径上。
属性:
method:是让你指定请求的method的类型,比如常用的有get和post。
value:是指请求的实际地址,如果是多个地址就用{}来指定就可以啦。
produces:指定返回的内容类型,当request请求头中的Accept类型中包含指定的类型才可以返回的。
consumes:指定处理请求的提交内容类型,比如一些json、html、text等的类型。
headers:指定request中必须包含那些的headed值时,它才会用该方法处理请求的。
params:指定request中一定要有的参数值,它才会使用该方法处理请求。
@RequestParam:
作用:是将请求参数绑定到你的控制器的方法参数上,是Spring MVC中的接收普通参数的注解。
属性:
value是请求参数中的名称。
required是请求参数是否必须提供参数,它的默认是true,意思是表示必须提供。
@RequestBody:
作用:如果作用在方法上,就表示该方法的返回结果是直接按写入的Http responsebody中(一般在异步获取数据时使用的注解)。
属性:required,是否必须有请求体。它的默认值是true,在使用该注解时,值得注意的当为true时get的请求方式是报错的,如果你取值为false的话,get的请求是null。
@PathVaribale:
作用:该注解是用于绑定url中的占位符,但是注意,spring3.0以后,url才开始支持占位符的,它是Spring MVC支持的rest风格url的一个重要的标志。
Mybatis是一个半ORM框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,使用 XML 或注解来配置和映射原生信息,将java对象映射成数据库中的记录,通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。
ORM是对象和关系之间的映射,包括对象->关系和关系->对象两方面。Hibernate是个完整的ORM框架,而MyBatis只完成了关系->对象,准确地说MyBatis是SQL映射框架而不是ORM框架,因为其仅有字段映射,对象数据以及对象实际关系仍然需要通过手写SQL来实现和管理。
可以让我们在xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能:if 标签 、choose 标签、when 标签、otherwise 标签、foreach、set 标签、trim 标签、prefix 、suffix 、prefixesToOverride 、suffixesToOverride 。
mybatis是使用分页插件PageHelper进行分页的, 其原理就是底层通过拦截器,进行拦截,然后根据传入的参数拼接sql中的limit,达到分页的效果。
Mybatis将所有Xml配置信息都封装到Al-In-One重量级对象Contfiguration 内部。在Xml映射文件中,标签会被解析为ParameterMap对象,其每个子元素会被解析为ParameterMapping对象。标签会被解析为ResultMap对象,其每个子元素会被解析为ResutMapping对象。每一个、、、标签均会被解析为MappedStatement对象,标签内的sql会被解析为BoundSql对象。
当不进行方法的重载时,即:每个方法都有唯一的命名时,在xml中进行映射后,就可以执行,不会出现异常。所以mybatis中mapper.xml是不会准确映射到Java中的重载方法的。最好不要在mapper接口中使用方法重载。
Dao 接口即 Mapper 接口。接口的全限名,就是映射文件中的 namespace 的值;接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值;接口方法内的参数,就是传递给 sql 的参数。Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MapperStatement。在 Mybatis 中,每一个标签,都会被解析为一个MapperStatement 对象。
Mapper 接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法,转而执行 MapperStatement 所代表的 sql,然后将 sql 执行结果返回。
不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。
原因就是namespace+id是作为Map
Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?还有很多其他的标签,、、、、加上动态sql的9个标签:trim|wherelset|foreach|if|choose|when[otherwise|bind等。其中为sql片段标签,通过标签引入sql片段为不支持自增的主键生成策略标签。
在Mybatis配置文件中,可以指定默认的ExecutorType执行器类型,也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数。
public Sqlsession openSession (ExecutorType execType)
where bar like "%"# {value}"%"
Mybatis 仅可以编写针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 这 4 种接口的插件,Mybatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandler 的 invoke()方法,当然,只会拦截那些你指定需要拦截的方法。
编写插件:实现 Mybatis 的 Interceptor 接口并复写 intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。
Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled = true|false。
它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke()方法发现 a.getB()是null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName()方法的调用。这就是延迟加载的基本原理。
创建简单的sql语句,存储对应的,比如说要插入一个name值list < string > names = new arraylist();往里面add对应的值,然后利用一个sql sessionfactory创建一个sql session,拿到对应的mapper后,利用循环往里面多次添加值,最后将sql session commit和close集合foreach标签主要是用于迭代 包括item index open close separator collection array list
insert 方法总是返回一个 int 值 ,这个值代表的是插入的行数。如果采用自增长策略,自动生成的键值在 insert 方法执行完后可以被设置到传入的参数对象中。比如说要插入对应的name值,调用insert方法后,通过name.getid可以获取,配置文件设置 usegeneratedkeys 为 true
查看进程运行状态的指令:ps命令。“ps -aux | grep PID”,用来查看某PID进程状态
cat 路径/文件名 | grep 关键词
查看内存使用情况的指令:free命令。“free -m”,命令查看内存使用情况。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0HKjk9sv-1666488157650)(%E6%88%AA%E5%B1%8F2022-09-14%2016.06.01.png)]
total:内存总数
used:已使用的内存数
free:空闲的内存数
shared:当前已废弃不用
buffers:系统分配但未被使用的缓冲区
cached:系统分配但未被使用的缓存
显示当前系统正在执行的进程的相关信息,包括进程ID、内存占用率、CPU占用率等
linux下通过进程名查看其占用端口:
(1)先查看进程pid
ps -ef | grep 进程名
(2)通过pid查看占用端口
netstat -nap | grep 进程pid
linux通过端口查看进程:
netstat -nap | grep 端口号
Linux ping命令用于检测主机。
执行ping指令会使用ICMP传输协议,发出要求回应的信息,若远端主机的网络功能没有问题,就会回应该信息,因而得知该主机运作正常。
Linux文件的基本权限就有九个,分别是owner(user)/group/others三种身份各有自己的read/write/execute权限
修改权限指令:chmod 权限名 文件名
(1)只读:表示允许读取内容,而禁止其对该文件做其他任何操作
字母表示:r
数字表示:权限值4
(2)只写:表示只允许对该文件进行编辑,而禁止对其进行其他任何操作
字母表示:w
数字表示:权限值2
(3)可执行:允许将该文件作为一个可执行程序
字母表示:x
数字表示:权限值1
(4)无任何权限
字母表示:-
数字表示:权限值0
cd命令:用于切换当前目录
ls命令:查看当前文件与目录
grep命令:该命令常用于分析一行的信息,若当中有我们所需要的信息,就将该行显示出来,该命令通常与管道命令一起使用,用于对一些命令的输出进行筛选加工。
cp命令:复制命令
mv命令:移动文件或文件夹命令
rm命令:删除文件或文件夹命令
ps命令:查看进程情况
kill命令:向进程发送终止信号
tar命令:对文件进行打包,调用gzip或bzip对文件进行压缩或解压
cat命令:查看文件内容,与less、more功能相似
top命令:可以查看操作系统的信息,如进程、CPU占用率、内存信息等
pwd命令:命令用于显示工作目录。
1.定义不同
软链接又叫符号链接,这个文件包含了另一个文件的路径名。可以是任意文件或目录,可以链接不同文件系统的文件。
硬链接就是一个文件的一个或多个文件名。把文件名和计算机文件系统使用的节点号链接起来。因此我们可以用多个文件名与同一个文件进行链接,这些文件名可以在同一目录或不同目录。
2.限制不同
硬链接只能对已存在的文件进行创建,不能交叉文件系统进行硬链接的创建;
软链接可对不存在的文件或目录创建软链接;可交叉文件系统;
3.创建方式不同
硬链接不能对目录进行创建,只可对文件创建;
软链接可对文件或目录创建;
4.影响不同
删除一个硬链接文件并不影响其他有相同 inode 号的文件。
删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接被称为死链接(即 dangling link,若被指向路径文件被重新创建,死链接可恢复为正常的软链接)。
小端模式:低的有效字节存储在低的存储器地址。小端一般为主机字节序
大端模式:高的有效字节存储在低的存储器地址。大端为网络字节序
fork函数用来创建一个子进程。对于父进程,fork()函数返回新创建的子进程的PID。对于子进程,fork()函数调用成功会返回0。如果创建出错,fork()函数返回-1。
孤儿进程:是指一个父进程退出后,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并且由init进程对它们完整状态收集工作。
僵尸进程:是指一个进程使用fork函数创建子进程,如果子进程退出,而父进程并没有调用wait()或者waitpid()将子进程释放,那么子进程的进程描述符仍然保存在系统中,占用系统资源,这种进程称为僵尸进程。
僵尸进程的危害
如何解决僵尸进程:
一般,为了防止产生僵尸进程,在fork子进程之后我们都要及时使用wait系统调用;同时,当子进程退出的时候,内核都会给父进程一个SIGCHLD信号,所以我们可以建立一个捕获SIGCHLD信号的信号处理函数,在函数体中调用wait(或waitpid),就可以清理退出的子进程以达到防止僵尸进程的目的。
守护进程:守护进程是运行在后台的一种生存期长的特殊进程。它独立于控制终端,处理一些系统级别任务。
如何实现:
(1)创建子进程,终止父进程。方法是调用fork() 产生一个子进程,然后使父进程退出。
(2)调用setsid() 创建一个新会话。
(3)将当前目录更改为根目录。使用fork() 创建的子进程也继承了父进程的当前工作目录。
(4)重设文件权限掩码。文件权限掩码是指屏蔽掉文件权限中的对应位。
(5)关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。
能。不同的进程之间,存在资源竞争或并发使用的问题,所以需要互斥量。
进程中也需要互斥量,因为一个进程中可以包含多个线程,线程与线程之间需要通过互斥的手段进行同步,避免导致共享数据修改引起冲突。可以使用互斥锁,属于互斥量的一种。
SIGHUP | 该信号让进程立即关闭.然后重新读取配置文件之后重启 |
---|---|
SIGINT | 程序中止信号,用于中止前台进程。相当于输出 Ctrl+C 快捷键 |
Sleep:sleep是一个延时函数,让进程或线程进入休眠。休眠完毕后继续运行。
Wait:wait是父进程回收子进程PCB资源的一个系统调用。进程一旦调用了wait函数,就立即阻塞自己本身,然后由wait函数自动分析当前进程的某个子进程是否已经退出,当找到一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞,直到有一个出现为止。
设计思路:
线程池在内部实际上构建了一个生产者–消费者模型,将线程和任务解耦,不直接关联。所以我们可以将线程池的运行分为两部分:线程管理、任务管理。
任务管理部分充当生产者
线程管理部分充当消费者
实现线程池有以下几个步骤:
(1)设置一个生产者消费者队列,作为临界资源。
(2)初始化n个线程,并让其运行起来,加锁去队列里取任务运行
(3)当任务队列为空时,所有线程阻塞。
(4)当生产者队列来了一个任务后,先对队列加锁,把任务挂到队列上,然后使用条件变量去通知阻塞中的一个线程来处理。
提前申请好对应的内存,放到内存池里面,用一个链表管理对应的内存池
线程池也是,提前准备好线程,放到这里面
创建线程使用的是pthread_create
使对应的线程睡眠pthread_cond_wait
唤醒对应的线程pthread_cond_signal 条件变量
线程池第一步:首先声明一个任务队列里面每一个节点存放对应的任务指针,指向相应的任务函数,然后还需要准备,任务函数需要的参数,以及下一个元素的地址
第二步:再定义一个线程池的结构体:线程池里面也需要定义对应的任务队列的头指针,尾指针,线程的数量,以及线程号,用一个指针来表示pthread_t,同时还有对应的线程号,互斥锁pthread_mutext_tmutex,还有对应的条件按变量pthread_cond_t,同时还需要线程池的状态,是否关闭
使用到互斥锁,访问队列,主线程,往队列中放任务,子线程取出对应的队列开始执行
还有任务队列 ,来了一个任务,唤醒线程,从队列中获取
第三步:在线程池的构造函数里面,我们首先申请线程池的内存,初始化线程池,然后初始化任务队列,对于第一个任务队列的节点,头指针和尾指针均指向这个任务队列
还要初始化线程数量,以及对应的线程号,线程号也需要利用pthrea_t申请多个内存空间用来存放不同的线程
接下来使用的是循环通过pthread_create来创建10个对应的线程,其中主要是传入对应的线程id以及线程的工作函数,还有对应的线程池,每一个线程运行结束之后释放内存pthread_detach
我们在对应的线程的工作函数中,传入的变量是线程池,获取对应的线程池,因为同时个线程池上锁(访问任务队列要先给线程池上锁,利用的是pthread_mytex_lock这个函数)
也要加一个while循环进行判断,如果任务队列队头与队尾相同,并且线程没有关闭,需要将线程进行睡眠,(使用的是pthread_con_wait传入对应的条件变量以及互斥锁,要把互斥锁进行释放)
第四步:创建号主线程之后,我们需要往任务队列中添加50个任务,每一个节点包括对应的函数地址,参数,并且唤醒线程池中的线程,发送对应的信号,pthread_cond_signal
唤醒所有线程后,又进入了线程池的工作函数,从任务队列中获取任务,执行节点的任务函数,然后如果所有任务被执行完了, 线程池就会继续关闭,进入休眠状态
接下来,判断任务队列如果不为空,我们需要从任务队列当中获取一个任务,同时将线程池的队列头指针指向下一个节点,再继续释放对应的互斥锁,同时唤醒相应的线程
select,poll,epoll都是IO多路复用的机制,I/O多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知应用程序进行相应的读写操作。
区别:
(1)poll与select不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。
(2)select,poll实现需要自己不断轮询所有fd(文件描述符)集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。
(3)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把当前进程往设备等待队列中挂一次,而epoll只要一次拷贝,而且把当前进程往等待队列上挂也只挂一次,这也能节省不少的开销。
select,epoll的使用场景:都是IO多路复用的机制,应用于高并发的网络编程的场景。I/O多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知应用程序进行相应的读写操作。
select,epoll的区别:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;而epoll保证了每个fd在整个过程中只会拷贝一次。
(2)每次调用select都需要在内核遍历传递进来的所有fd;而epoll只需要轮询一次fd集合,同时查看就绪链表中有没有就绪的fd就可以了。
(3)select支持的文件描述符数量太小了,默认是1024;而epoll没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048。
epoll水平触发与边缘触发的区别
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
epoll是一种更加高效的IO多路复用的方式,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。时间复杂度为O(1)。
(1)创建红黑树,调用epoll_create()创建一颗空的红黑树,用于存放FD及其感兴趣事件;
(2)注册感兴趣事件,调用epoll_ctl()向红黑树中添加节点(FD及其感兴趣事件),时间复杂度O(logN),向内核的中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它添加到就绪队列中。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到就绪队列中了;
(3)获取就绪事件,调用epoll_wait()返回就绪队列中的就绪事件,时间复杂度O(1);
在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
BIO(Blocking I/O):阻塞IO。调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
NIO(New I/O):同时支持阻塞与非阻塞模式,NIO的做法是叫一个线程不断的轮询每个IO的状态,看看是否有IO的状态发生了改变,从而进行下一步的操作。
阻塞IO:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
非阻塞IO:非阻塞等待,每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。
信号驱动IO:Linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进程收到SIGIO信号,然后处理IO事件。
IO多路复用:Linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检查。知道有数据可读或可写时,才真正调用IO操作函数。
异步IO:Linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。用户可以直接去使用数据。
答案解析
前四种模型–阻塞IO、非阻塞IO、多路复用IO和信号驱动IO都属于同步模式,因为其中真正的IO操作(函数)都将会阻塞进程,只有异步IO模型真正实现了IO操作的异步性。
异步和同步的区别就在于,异步是内核将数据拷贝到用户区,不需要用户再自己接收数据,直接使用就可以了,而同步是内核通知用户数据到了,然后用户自己调用相应函数去接收数据。
服务器端函数:
(1)socket创建一个套接字
(2)bind绑定ip和port
(3)listen使套接字变为可以被动链接
(4)accept等待客户端的链接
(5)write/read接收发送数据
(6)close关闭连接
客户端函数:
(1)创建一个socket,用函数socket()
(2)bind绑定ip和port
(3)连接服务器,用函数connect()
(4)收发数据,用函数send()和recv(),或read()和write()
(5)close关闭连接
进程是CPU分配资源的最小单位,线程是CPU调度的最小单位。
线程是比进程更小的能独立运行的基本单位,需要通过CPU调度来切换上下文,达到并发的目的。
内存和缓存是计算机不同的组成部件。
内存特性
内存也被称作内存储器,其作用是用于暂时存放CPU的运算数据,以及与硬盘等外部存储交换的数据。只要计算机在运行中,CPU就会把需要进行运算的数据调到内存中进行运算,当运算完成后CPU再将结果传送出来,内存的运行也决定了计算机的稳定运行。
缓存特性
CPU芯片面积和成本的因素影响,决定了缓存都很小。现在一般的缓存不过几M,CPU缓存的运行频率极高,一般是和处理器同频运作,工作效率远远大于系统内存和硬盘。实际工作时,CPU往往需要重复读取读取同样的数据块,而缓存容量的增大,可以大幅度提升CPU内部读取数据的命中率,而不用再到内存或者硬盘上寻找,以此提高系统性能。
当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;当需要读写文件时,也需要把文件描述符作为参数传递给相应的函数。
文件描述符是一个非负的整数,它是一个索引值,并指向在内核中每个进程打开文件的记录表。
创建文件:Creat传入文件名,以及对应的权限00400 00200表示当前用户可读可写,返回文件描述符
Int fd=Open(“hello.c”,O_RDWR)
Ssize_t size=write(fd,s,strlen(s))
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pCeWHvWv-1666488157651)(image-20220915152548830.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W9Ls0N8H-1666488157651)(image-20220915152558350.png)]
系统io拷贝:用open分别打开两个文件,再利用write往其中写入数据
标准io:采用的是标准io库比如说c语言的fopen,fwrite
标准I/O提供缓存的目的就是减少调用read和write的次数,它对每个I/O流自动进行缓存管理(标准I/O函数通常调用malloc来分配缓存) 。
它提供了三种类型的缓存:
1)全缓存。当填满标准I/O缓存后才执行I/O操作。磁盘上的文件通常是全缓存的。
2)行缓存。当输入输出遇到新行符或缓存满时,才由标准I/O库执行实际I/O操作。stdin、 stdout通常是行缓存的。
3)无缓存。相当于read、write 了。stderr通常是无缓存的,因为它必须尽快输出。
进程是动态的,程序是静态的:程序是有序代码的集合;进程是程序的执行。
进程不可在计算机之间迁移;而程序通常对应着文件、静态和可以复制
进程是暂时的,程序使长久的
进程是一个状态变化的过程, 程序可长久保存
进程与程序组成不同:进程的组成包括程序、数据和进程控制块(即进程状态信息)
进程与程序的对应关系:通过多次执行,一个程序可对应多个进程;一个进程可包括多个程序。
是指当有若干进程都要使用某一共享资源时, 任何时刻最多允许一个进程使用,其他要使用该资源的进程必须等待,直到占用该资源者释放了该资源为止;
操作系统中将一次只允许一个进程访问的资源称为临界资源;
进程中访问临界资源的那段程序代码称为临界区,为实现对临界资源的互斥访问,应保证诸进程互斥地进入各自的临界区;
vfork(void)功能:创建子进程
vfork()会产生一个新的子进程, 其子进程会共享父进程的数据与堆栈空间,并继承父进程的用户代码、组代码、环境变量、已打开的文件代码、工作目录和资源限制等;
子进程不会继承父进程的文件锁定和未处理的信号;
注意,vfork产生的子进程,一定是子进程先执行、父进程后执行。
exec用于启动一个不相关的新的进程,被执行的程序替换调用它的程序。exec和fork区别:
fork创建一个新的进程(子进程) ,产生一个新的PID.
exec启动一个新程序,替换原有的进程,因此进程的PID不会改变。
主要是msgget用来获取创建消息队列,要设置消息的类型,msgsnd和msgrcv用来发送和读取数据,主要是传递字符串的地址
int msgget(key_t key, int msgflg);
参数:
key:ftok() 返回的 key 值。
msgflg:标识函数的行为及消息队列的权限,其取值如下:
IPC_CREAT:创建消息队列;
IPC_EXCL: 检测消息队列是否存在;
位或权限位:消息队列位或权限位后可以设置消息队列的访问权限,格式和open() 函数的 mode_t 一样(open() 的使用请点此链接),但可执行权限未使用。
返回值:成功:消息队列的标识符,失败:-1。
int msgsnd( int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
msqid: 消息队列的标识符;
msgp: 待发送消息结构体的地址;
msgsz: 消息正文的字节数;
msgflg:函数的控制属性,其取值如下:
0:msgsnd() 调用阻塞直到条件满足为止。
IPC_NOWAIT:若消息没有立即发送则调用该函数的进程会立即返回。
返回值:成功:0,失败:-1。
删除消息队列msgctl
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
Redis最常用来做缓存,是实现分布式缓存的首先中间件;
Redis可以作为数据库,实现诸如点赞、关注、排行等对性能要求极高的互联网需求;
Redis可以作为计算工具,能用很小的代价,统计诸如PV/UV、用户在线天数等数据;
Redis还有很多其他的使用场景,例如:可以实现分布式锁,可以作为消息队列使用。
Redis是一种基于键值对的NoSQL数据库,而键值对的值是由多种数据结构和算法组成的。Redis的数据都存储于内存中,因此它的速度惊人,读写性能可达10万/秒,远超关系型数据库。
关系型数据库是基于二维数据表来存储数据的,它的数据格式更为严谨,并支持关系查询。关系型数据库的数据存储于磁盘上,可以存放海量的数据,但性能远不如Redis。
Redis支持5种核心的数据类型,分别是字符串、哈希、列表、集合、有序集合;
Redis还提供了Bitmap、HyperLogLog、Geo类型,但这些类型都是基于上述核心数据类型实现的;
Redis在5.0新增加了Streams数据类型,它是一个功能强大的、支持多播的、可持久化的消息队列。
对服务端程序来说,线程切换和锁通常是性能杀手,而单线程避免了线程切换和竞争所产生的消耗;
Redis的大部分操作是在内存上完成的,这是它实现高性能的一个重要原因;
Redis采用了IO多路复用机制,使其在网络IO操作中能并发处理大量的客户端请求,实现高吞吐率。
Redis是单线程的,主要是指Redis的网络IO和键值对读写是由一个线程来完成的。而Redis的其他功能,如持久化、异步删除、集群数据同步等,则是依赖其他线程来执行的。所以,说Redis是单线程的只是一种习惯的说法,事实上它的底层不是单线程的。
set:
集合中的元素是无序、不可重复的,一个集合最多能存储232-1个元素;
集合除了支持对元素的增删改查之外,还支持对多个集合取交集、并集、差集。
zset:
有序集合保留了集合元素不能重复的特点;
有序集合会给每个元素设置一个分数,并以此作为排序的依据;
有序集合不能包含相同的元素,但是不同元素的分数可以相同。
zset对象的底层数据结构包括:压缩列表、字典、跳跃表。
压缩列表:压缩列表(ziplist),是Redis为了节约内存而设计的一种线性数据结构,它是由一系列具有特殊编码的连续内存块构成的。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或一个整数值。
字典:字典(dict)又称为散列表,是一种用来存储键值对的数据结构。C语言没有内置这种数据结构,所以Redis构建了自己的字典实现。
跳跃表:跳跃表的查找复杂度为平均O(logN),最坏O(N),效率堪比红黑树,却远比红黑树实现简单。跳跃表是在链表的基础上,通过增加索引来提高查找效率的。
很多时候,要确保事务中的数据没有被其他客户端修改才执行该事务。Redis提供了watch命令来解决这类问题,这是一种乐观锁的机制。客户端通过watch命令,要求服务器对一个或多个key进行监视,如果在客户端执行事务之前,这些key发生了变化,则服务器将拒绝执行客户端提交的事务,并向它返回一个空值。
列表是线性有序的数据结构,它内部的元素是可以重复的,并且一个列表最多能存储2^32-1个元素。列表包含如下的常用命令:
lpush/rpush:从列表的左侧/右侧添加数据;
lrange:指定索引范围,并返回这个范围内的数据;
lindex:返回指定索引处的数据;
lpop/rpop:从列表的左侧/右侧弹出一个数据;
blpop/brpop:从列表的左侧/右侧弹出一个数据,若列表为空则进入阻塞状态。
热点数据不设置过期时间,使其达到“物理”上的永不过期,可以避免缓存击穿问题;
在设置过期时间时,可以附加一个随机数,避免大量的key同时过期,导致缓存雪崩。
setnx命令返回整数值,当返回1时表示设置值成果,当返回0时表示设置值失败(key已存在)。
一般我们不建议直接使用setnx命令来实现分布式锁,因为为了避免出现死锁,我们要给锁设置一个自动过期时间。而setnx命令和设置过期时间的命令不是原子的,可能加锁成果而设置过期时间失败,依然存在死锁的隐患。对于这种情况,Redis改进了set命令,给它增加了nx选项,启用该选项时set命令的效果就会setnx一样了。
在分布式的环境下,当多个server并发修改同一个资源时,为了避免竞争就需要使用分布式锁。那为什么不能使用Java自带的锁呢?因为Java中的锁是面向多线程设计的,它只局限于当前的JRE环境。而多个server实际上是多进程,是不同的JRE环境,所以Java自带的锁机制在这个场景下是无效的。
加锁:
第一版,这种方式的缺点是容易产生死锁,因为客户端有可能忘记解锁,或者解锁失败。setnx key value
第二版,给锁增加了过期时间,避免出现死锁。但这两个命令不是原子的,第二步可能会失败,依然无法避免死锁问题。setnx key value expire key seconds
第三版,通过“set…nx…”命令,将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。set key value nx ex seconds
解锁:解锁就是删除代表锁的那份数据。del key。
Redis支持RDB持久化、AOF持久化、RDB-AOF混合持久化这三种持久化方式。
RDB:RDB(Redis Database)是Redis默认采用的持久化方式,它以快照的形式将进程数据持久化到硬盘中。RDB会创建一个经过压缩的二进制文件,文件以“.rdb”结尾,内部存储了各个数据库的键值对数据等信息。RDB持久化的触发方式有两种:
手动触发:通过SAVE或BGSAVE命令触发RDB持久化操作,创建“.rdb”文件;
自动触发:通过配置选项,让服务器在满足指定条件时自动执行BGSAVE命令。
RDB持久化的优缺点如下:
优点:RDB生成紧凑压缩的二进制文件,体积小,使用该文件恢复数据的速度非常快;
缺点:BGSAVE每次运行都要执行fork操作创建子进程,属于重量级操作,不宜频繁执行,所以RDB持久化没办法做到实时的持久化。
AOF:AOF(Append Only File),解决了数据持久化的实时性,是目前Redis持久化的主流方式。AOF以独立日志的方式,记录了每次写入命令,重启时再重新执行AOF文件中的命令来恢复数据。AOF的工作流程包括:命令写入(append),在缓冲里面文件同步(sync)、Aof文件重写(rewrite)、重启加载(load):
AOF持久化的文件同步机制:
为了提高程序的写入性能,现代操作系统会把针对硬盘的多次写操作优化为一次写操作。当程序调用write对文件写入时,系统不会直接把书记写入硬盘,而是先将数据写入内存的缓冲区中;当达到特定的时间周期或缓冲区写满时,系统才会执行flush操作,将缓冲区中的数据冲洗至硬盘中;
这种优化机制虽然提高了性能,但也给程序的写入操作带来了不确定性。对于AOF这样的持久化功能来说,冲洗机制将直接影响AOF持久化的安全性;为了消除上述机制的不确定性,Redis向用户提供了appendfsync选项,来控制系统冲洗AOF的频率;
Linux的glibc提供了fsync函数,可以将指定文件强制从缓冲区刷到硬盘,上述选项正是基于此函数。
AOF持久化的优缺点如下:
优点:与RDB持久化可能丢失大量的数据相比,AOF持久化的安全性要高很多。通过使用everysec选项,用户可以将数据丢失的时间窗口限制在1秒之内。
缺点:AOF文件存储的是协议文本,它的体积要比二进制格式的”.rdb”文件大很多。AOF需要通过执行AOF文件中的命令来恢复数据库,其恢复速度比RDB慢很多。AOF在进行重写时也需要创建子进程,在数据库体积较大时将占用大量资源,会导致服务器的短暂阻塞。
哨兵:Redis Sentinel(哨兵)是一个分布式架构,它包含若干个哨兵节点和数据节点。每个哨兵节点会对数据节点和其余的哨兵节点进行监控,当发现节点不可达时,会对节点做下线标识。如果被标识的是主节点,它就会与其他的哨兵节点进行协商,当多数哨兵节点都认为主节点不可达时,它们便会选举出一个哨兵节点来完成自动故障转移的工作,同时还会将这个变化实时地通知给应用方。整个过程是自动的,不需要人工介入,有效地解决了Redis的高可用问题!
集群:使用一个或者多个哨兵(Sentinel)实例组成的系统,对redis节点进行监控,在主节点出现故障的情况下,能将从节点中的一个升级为主节点,进行故障转义,保证系统的可用性。
故障转义:当一个Master不能正常工作时,哨兵通过投票协议会开始一次自动故障迁移操作,它会将其他一个Slave升级为新的Master。该模式主从可以切换,故障可以转移,系统的可用性就会更好。但是需要额外的资源来启动哨兵进程。
主从架构 -> 读写分离 -> 支持10万+读QPS架构。
在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。常见的元数据维护方式分为:集中式和P2P方式。
Redis集群采用P2P的Gossip(流言)协议,Gossip协议的工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。通信的大致过程如下:
Gossip协议的主要职责就是信息交换Gossip消息分为:meet消息、ping消息、pong消息、fail消息等。
meet消息:用于通知新节点加入,消息发送者通知接受者加入到当前集群。meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息封装了自身节点和一部分其他节点的状态数据。
pong消息:当接收到meet、ping消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内封装了自身状态数据,节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。
Redis集群采用虚拟槽分区来实现数据分片,它把所有的键根据哈希函数映射到0-16383整数槽内,计算公式为slot=CRC16(key)&16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。虚拟槽分区具有如下特点:
解耦数据和节点之间的关系,简化了节点扩容和收缩的难度;
节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据;
支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景。
**优势:**Redis Cluster是Redis的分布式解决方案,在3.0版本正式推出,有效地解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构方案达到负载均衡的目的。
劣势:
key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mset、mget等操作可能存在于多个节点上所以不被支持。
key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
key作为数据分区的最小粒度,因此不能将一个大的键值对象(如hash、list等)映射到不同的节点。
不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即DB0。
复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
缓存穿透:
问题描述:
客户端查询根本不存在的数据,使得请求直达存储层,导致其负载过大,甚至宕机。出现这种情况的原因,可能是业务层误将缓存和库中的数据删除了,也可能是有人恶意攻击,专门访问库中不存在的数据。
解决方案:
缓存空对象:存储层未命中后,仍然将空值存入缓存层,客户端再次访问数据时,缓存层会直接返回空值。
布隆过滤器:将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在则直接返回空值。
缓存击穿:
问题描述:
一份热点数据,它的访问量非常大。在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。
解决方案:
永不过期:热点数据不设置过期时间,所以不会出现上述问题,这是“物理”上的永不过期。或者为每个数据设置逻辑过期时间,当发现该数据逻辑过期时,使用单独的线程重建缓存。
加互斥锁:对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中取值。
缓存雪崩:
问题描述:
在某一时刻,缓存层无法继续提供服务,导致所有的请求直达存储层,造成数据库宕机。可能是缓存中有大量数据同时过期,也可能是Redis节点发生故障,导致大量请求无法得到处理。
解决方案:
避免数据同时过期:设置过期时间时,附加一个随机数,避免大量的key同时过期。
启用降级和熔断措施:在发生雪崩时,若应用访问的不是核心数据,则直接返回预定义信息/空值/错误信息。或者在发生雪崩时,对于访问缓存接口的请求,客户端并不会把请求发给Redis,而是直接返回。
构建高可用的Redis服务:采用哨兵或集群模式,部署多个Redis实例,个别节点宕机,依然可以保持服务的整体可用。
想要保证缓存与数据库的双写一致,一共有4种方式,即4种同步策略:
先更新缓存,再更新数据库;
先更新数据库,再更新缓存;
先删除缓存,再更新数据库;
先更新数据库,再删除缓存。
更新缓存
优点:每次数据变化都及时更新缓存,所以查询时不容易出现未命中的情况。
缺点:更新缓存的消耗比较大。如果数据需要经过复杂的计算再写入缓存,那么频繁的更新缓存,就会影响服务器的性能。如果是写入数据频繁的业务场景,那么可能频繁的更新缓存时,却没有业务读取该数据。
删除缓存
优点:操作简单,无论更新操作是否复杂,都是将缓存中的数据直接删除。
缺点:删除缓存后,下一次查询缓存会出现未命中,这时需要重新读取一次数据库。
从上面的比较来看,一般情况下,删除缓存是更优的方案。
Redis使用psync命令完成主从数据同步,同步过程分为全量复制和部分复制。全量复制一般用于初次复制的场景,部分复制则用于处理因网络中断等原因造成数据丢失的场景。psync命令需要以下参数的支持:
复制偏移量:主节点处理写命令后,会把命令长度做累加记录,从节点在接收到写命令后,也会做累加记录;从节点会每秒钟上报一次自身的复制偏移量给主节点,而主节点则会保存从节点的复制偏移量。
积压缓冲区:保存在主节点上的一个固定长度的队列,默认大小为1M,当主节点有连接的从节点时被创建;主节点处理写命令时,不但会把命令发送给从节点,还会写入积压缓冲区;缓冲区是先进先出的队列,可以保存最近已复制的数据,用于部分复制和命令丢失的数据补救。
主节点运行ID:每个Redis节点启动后,都会动态分配一个40位的十六进制字符串作为运行ID;如果使用IP和端口的方式标识主节点,那么主节点重启变更了数据集(RDB/AOF),从节点再基于复制偏移量复制数据将是不安全的,因此当主节点的运行ID变化后,从节点将做全量复制。
Redis存的快是因为它的数据都存放在内存里,并且为了保证数据的安全性,Redis还提供了三种数据的持久化机制,即RDB持久化、AOF持久化、RDB-AOF混合持久化。若服务器断电,那么我们可以利用持久化文件,对数据进行恢复。理论上来说,AOF/RDB-AOF持久化可以将丢失数据的窗口控制在1S之内。
Redis是基于内存存储的,MySQL是基于磁盘存储的
Redis存储的是k-v格式的数据。时间复杂度是O(1),常数阶,而MySQL引擎的底层实现是B+Tree,时间复杂度是O(logn),对数阶。Redis会比MySQL快一点点。
MySQL数据存储是存储在表中,查找数据时要先对表进行全局扫描或者根据索引查找,这涉及到磁盘的查找,磁盘查找如果是按条点查找可能会快点,但是顺序查找就比较慢;而Redis不用这么麻烦,本身就是存储在内存中,会根据数据在内存的位置直接取出。
Redis是单线程的多路复用IO,单线程避免了线程切换的开销,而多路复用IO避免了IO等待的开销,在多核处理器下提高处理器的使用效率可以对数据进行分区,然后每个处理器处理不同的数据。
惰性删除:客户端访问一个key的时候,Redis会先检查它的过期时间,如果发现过期就立刻删除这个key。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
定期删除:Redis会将设置了过期时间的key放到一个独立的字典中,并对该字典进行每秒10次的过期扫描,对内存友好。
过期扫描不会遍历字典中所有的key,而是采用了一种简单的贪心策略。该策略的删除逻辑如下:
从过期字典中随机选择20个key;
删除这20个key中已过期的key;
如果已过期key的比例超过25%,则重复步骤1。
在web开发中,我们会把用户的登录信息存储在session里。而session是依赖于cookie的,即服务器创建session时会给它分配一个唯一的ID,并且在响应时创建一个cookie用于存储这个SESSIONID。当客户端收到这个cookie之后,就会自动保存这个SESSIONID,并且在下次访问时自动携带这个SESSIONID,届时服务器就可以通过这个SESSIONID得到与之对应的session,从而识别用户的身。
消息队列有很多使用场景,比较常见的有3个:解耦、异步、削峰。
解耦:传统的软件开发模式,各个模块之间相互调用,数据共享,每个模块都要时刻关注其他模块的是否更改或者是否挂掉等等,使用消息队列,可以避免模块之间直接调用,将所需共享的数据放在消息队列中,对于新增业务模块,只要对该类消息感兴趣,即可订阅该类消息,对原有系统和业务没有任何影响,降低了系统各个模块的耦合度,提高了系统的可扩展性。
异步:消息队列提供了异步处理机制,在很多时候应用不想也不需要立即处理消息,允许应用把一些消息放入消息中间件中,并不立即处理它,在之后需要的时候再慢慢处理。
削峰:在访问量骤增的场景下,需要保证应用系统的平稳性,但是这样突发流量并不常见,如果以这类峰值的标准而投放资源的话,那无疑是巨大的浪费。使用消息队列能够使关键组件支撑突发访问压力,不会因为突发的超负荷请求而完全崩溃。消息队列的容量可以配置的很大,如果采用磁盘存储消息,则几乎等于“无限”容量,这样一来,高峰期的消息可以被积压起来,在随后的时间内进行平滑的处理完成,而不至于让系统短时间内无法承载而导致崩溃。在电商网站的秒杀抢购这种突发性流量很强的业务场景中,消息队列的强大缓冲能力可以很好的起到削峰作用。
所谓生产者-消费者问题,实际上主要是包含了两类线程。一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库。生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为。而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:
如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;如果共享数据区为空的话,阻塞消费者继续消费数据。
在Java语言中,实现生产者消费者问题时,可以采用三种方式
使用 Object 的 wait/notify 的消息通知机制;
使用 Lock 的 Condition 的 await/signal 的消息通知机制;
使用 BlockingQueue 实现。
丢数据一般分为两种,一种是mq把消息丢了,一种就是消费时将消息丢了。下面从rabbitmq和kafka分别说一下,丢失数据的场景。
RabbitMQ丢失消息分为如下几种情况:
生产者丢消息:
生产者将数据发送到RabbitMQ的时候,可能在传输过程中因为网络等问题而将数据弄丢了。
RabbitMQ自己丢消息:
如果没有开启RabbitMQ的持久化,那么RabbitMQ一旦重启数据就丢了。所以必须开启持久化将消息持久化到磁盘,这样就算RabbitMQ挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢失。除非极其罕见的情况,RabbitMQ还没来得及持久化自己就挂了,这样可能导致一部分数据丢失。
消费端丢消息:
主要是因为消费者消费时,刚消费到还没有处理,结果消费者就挂了,这样你重启之后,RabbitMQ就认为你已经消费过了,然后就丢了数据。
针对上述三种情况,RabbitMQ可以采用如下方式避免消息丢失:
生产者丢消息:
(1)可以选择使用RabbitMQ提供是事务功能,就是生产者在发送数据之前开启事务,然后发送消息,如果消息没有成功被RabbitMQ接收到,那么生产者会受到异常报错,这时就可以回滚事务,然后尝试重新发送。如果收到了消息,那么就可以提交事务。这种方式有明显的缺点,即RabbitMQ事务开启后,就会变为同步阻塞操作,生产者会阻塞等待是否发送成功,太耗性能会造成吞吐量的下降。
(2)可以开启confirm模式。在生产者那里设置开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后如何写入了RabbitMQ之中,RabbitMQ会给你回传一个ack消息,告诉你这个消息发送OK了。如果RabbitMQ没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。而且你可以结合这个机制知道自己在内存里维护每个消息的id,如果超过一定时间还没接收到这个消息的回调,那么你可以进行重发。
事务机制是同步的,你提交了一个事物之后会阻塞住,但是confirm机制是异步的,发送消息之后可以接着发送下一个消息,然后RabbitMQ会回调告知成功与否。 一般在生产者这块避免丢失,都是用confirm机制。
RabbitMQ自己丢消息:
(1)设置消息持久化到磁盘,设置持久化有两个步骤:
创建queue的时候将其设置为持久化的,这样就可以保证RabbitMQ持久化queue的元数据,但是不会持久化queue里面的数据。
发送消息的时候讲消息的deliveryMode设置为2,这样消息就会被设为持久化方式,此时RabbitMQ就会将消息持久化到磁盘上。 必须要同时开启这两个才可以。
而且持久化可以跟生产的confirm机制配合起来,只有消息持久化到了磁盘之后,才会通知生产者ack,这样就算是在持久化之前RabbitMQ挂了,数据丢了,生产者收不到ack回调也会进行消息重发。
消费端丢消息:
**使用RabbitMQ提供的ack机制,首先关闭RabbitMQ的自动ack,然后每次在确保处理完这个消息之后,在代码里手动调用ack。**这样就可以避免消息还没有处理完就ack。
比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update一下。
比如你是写redis,那没问题了,反正每次都是set,天然幂等性。
比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的id,类似订单id之类的东西,然后你这里消费到了之后,先根据这个id去比如redis里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个id写redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
一般生产环境中,都会在使用MQ的时候设计两个队列:一个是核心业务队列,一个是死信队列。核心业务队列,就是比如专门用来让订单系统发送订单消息的,然后另外一个死信队列就是用来处理异常情况的。
比如说要是第三方物流系统故障了,此时无法请求,那么仓储系统每次消费到一条订单消息,尝试通知发货和配送,都会遇到对方的接口报错。**此时仓储系统就可以把这条消息拒绝访问,或者标志位处理失败!**注意,这个步骤很重要。
**一旦标志这条消息处理失败了之后,MQ就会把这条消息转入提前设置好的一个死信队列中。**然后你会看到的就是,在第三方物流系统故障期间,所有订单消息全部处理失败,全部会转入死信队列。然后你的仓储系统得专门有一个后台线程,监控第三方物流系统是否正常,能否请求的,不停的监视。一旦发现对方恢复正常,这个后台线程就从死信队列消费出来处理失败的订单,重新执行发货和配送的通知逻辑。死信队列的使用,其实就是MQ在生产实践中非常重要的一环,也就是架构设计必须要考虑的。
推模式:推模式是服务器端根据用户需要,由目的、按时将用户感兴趣的信息主动发送到用户的客户端。
优点:对用户要求低,方便用户获取需要的信息;及时性好,服务器端及时地向客户端推送更新动态信息,吞吐量大。
缺点:不能确保发送成功,推模式采用广播方式,只有服务器端和客户端在同一个频道上,推模式才有效,用户才能接收到信息;没有信息状态跟踪,推模式采用开环控制技术,一个信息推送后的状态,比如客户端是否接收等,无从得知;针对性较差。推送的信息可能并不能满足客户端的个性化需求。
拉模式:拉模式是客户端主动从服务器端获取信息。
优点:针对性强,能满足客户端的个性化需求;信息传输量较小,网络中传输的只是客户端的请求和服务器端对该请求的响应;服务器端的任务轻。服务器端只是被动接收查询,对客户端的查询请求做出响应。
缺点:实时性较差,针对于服务器端实时更新的信息,客户端难以获取实时信息;对于客户端用户的要求较高,需要对服务器端具有一定的了解。
在实际生产应用中,**通常会使用Kafka作为消息传输的数据管道,RabbitMQ作为交易数据作为数据传输管道,主要的取舍因素则是是否存在丢数据的可能。**RabbitMQ在金融场景中经常使用,具有较高的严谨性,数据丢失的可能性更小,同事具备更高的实时性。而Kafka优势主要体现在吞吐量上,虽然可以通过策略实现数据不丢失,但从严谨性角度来讲,大不如RabbitMQ。而且由于Kafka保证每条消息最少送达一次,有较小的概率会出现数据重复发送的情况。详细来说,它们之间主要有如下的区别:
应用场景方面
RabbitMQ:用于实时的,对可靠性要求较高的消息传递上。
Kafka:用于处于活跃的流式数据,大数据量的数据处理上。
架构模型方面
RabbitMQ:以broker为中心,有消息的确认机制。
Kafka:以consumer为中心,没有消息的确认机制。
吞吐量方面
RabbitMQ:支持消息的可靠的传递,支持事务,不支持批量操作,基于存储的可靠性的要求存储可以采用内存或硬盘,吞吐量小。
Kafka:内部采用消息的批量处理,数据的存储和获取是本地磁盘顺序批量操作,消息处理的效率高,吞吐量高。
集群负载均衡方面
RabbitMQ:本身不支持负载均衡,需要loadbalancer的支持。
Kafka:采用zookeeper对集群中的broker,consumer进行管理,可以注册topic到zookeeper上,通过zookeeper的协调机制,producer保存对应的topic的broker信息,可以随机或者轮询发送到broker上,producer可以基于语义指定分片,消息发送到broker的某个分片上。
字符流和字节流的使用非常相似,但是实际上字节流的操作不会经过缓冲区(内存)而是直接操作文本本身的,而字符流的操作会先经过缓冲区(内存)然后通过缓冲区再操作文件 。
以字节为单位输入输出数据,字节流按照8位传输,以字符为单位输入输出数据,字符流按照16位传输。
字节输入流转字符输入流通过 InputStreamReader 实现,该类的构造函数可以传入 InputStream 对象。
字节输出流转字符输出流通过 OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SAh68wYS-1666488157652)(image-20220921145945701-16640896969071.png)]
打开大文件,应避免直接将文件中的数据全部读取到内存中,可以采用分次读取的方式。
使用缓冲流。缓冲流内部维护了一个缓冲区,通过与缓冲区的交互,减少与设备的交互次数。使用NIO。NIO采用内存映射文件的方式来处理输入/输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了(这种方式模拟了操作系统上的虚拟内存的概念),通过这种方式来进行输入/输出比传统的输入/输出要快得多。
Java BIO: 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
Java NIO: 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
Java AIO: 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
NIO比BIO的改善之处是把一些无效的连接挡在了启动线程之前,减少了这部分资源的浪费(因为我们都知道每创建一个线程,就要为这个线程分配一定的内存空间)
AIO比NIO的进一步改善之处是将一些暂时可能无效的请求挡在了启动线程之前,比如在NIO的处理方式中,当一个请求来的话,开启线程进行处理,但这个请求所需要的资源还没有就绪,此时必须等待后端的应用资源,这时线程就被阻塞了。
Java的NIO主要由三个核心部分组成:Channel、Buffer、Selector。
基本上,所有的IO在NIO中都从一个Channel开始,数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中。
Buffer本质上是一块可以写入数据,然后可以从中读取数据的内存。Buffer对象包含三个重要的属性,分别是capacity、position、limit,其中position和limit的含义取决于Buffer处在读模式还是写模式。capacity:作为一个内存块,Buffer有个固定的最大值,就是capacity。position:当写数据到Buffer中时,position表示当前的位置。limit:在写模式下,Buffer的limit表示最多能往Buffer里写多少数据,此时limit等于capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据,此时limit会被设置成写模式下的position值。
Selector允许单线程处理多个 Channel,如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。
传统 IO 一般是一个线程等待连接,连接过来之后分配给 processor 线程,processor 线程与通道连接后如果通道没有数据过来就会阻塞(线程被动挂起)不能做别的事情。NIO 则不同,首先,在 selector 线程轮询的过程中就已经过滤掉了不感兴趣的事件,其次,在 processor处理感兴趣事件的 read 和 write 都是非阻塞操作即直接返回的,线程没有被挂起。
传统 io 的管道是单向的,nio 的管道是双向的。
两者都是同步的,也就是 java 程序亲力亲为的去读写数据,不管传统 io 还是 nio 都需要read 和 write 方法,这些都是 java 程序调用的而不是系统帮我们调用的,nio2.0 里这点得到了改观,即使用异步非阻塞 AsynchronousXXX 四个类来处理。
序列化机制可以将对象转换成字节序列,这些字节序列可以保存在磁盘上,也可以在网络中传输,并允许程序将这些字节序列再次恢复成原来的对象。其中,对象的序列化(Serialize),是指将一个Java对象写入IO流中,对象的反序列化(Deserialize),则是指从IO流中恢复该Java对象。
若对象要支持序列化机制,则它的类需要实现Serializable接口,该接口是一个标记接口,它没有提供任何方法,只是标明该类是可以序列化的,Java的很多类已经实现了Serializable接口,如包装类、String、Date等。
若要实现序列化,则需要使用对象流ObjectInputStream和ObjectOutputStream。其中,在序列化时需要调用ObjectOutputStream对象的writeObject()方法,以输出对象序列。在反序列化时需要调用ObjectInputStream对象的readObject()方法,将对象序列恢复为对象。
serialVersionUID代表序列化的版本,通过定义类的序列化版本,在反序列化时,只要对象中所存的版本和当前类的版本一致,就允许做恢复数据的操作,否则将会抛出序列化版本不一致的错误。
JSON:目前使用比较频繁的格式化数据工具,简单直观,可读性好,有jackson,gson,fastjson等等。如果不用JSON工具,该如何实现对实体类的序列化?可以使用Java原生的序列化机制,但是效率比较低一些,适合小项目;可以使用其他的一些第三方类库,比如Protobuf、Thrift、Avro等。
有两种方式:
实现 Cloneable 接口并重写 Object 类中的 clone()方法;
实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
BIO (Blocking I/O):BIO 属于同步阻塞 IO 模型 。同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
NIO: IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。
AIO (Asynchronous I/O):异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
缓冲区就是一段特殊的内存区域,很多情况下当程序需要频繁地操作一个资源(如文件或数据库)则性能会很低,所以为了提升性能就可以将一部分数据暂时读写到缓存区,以后直接从此区域中读写数据即可,这样就可以显著的提升性能。
对于 Java 字符流的操作都是在缓冲区操作的,所以如果我们想在字符流操作中主动将缓冲区刷新到文件则可以使用 flush() 方法操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-obgehf4K-1666488157652)(4de4a864434073b848a3fb3d914356dc.png)]
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java中悲观锁是通过synchronized关键字或Lock接口来实现的。
乐观锁:顾名思义,就是很乐观,**每次去拿数据的时候都认为别人不会修改,所以不会上锁,**但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
JVM为了提高锁的获取与释放效率对synchronized 进行了优化,引入了偏向锁和轻量级锁 ,从此以后锁的状态就有了四种:无锁、偏向锁、轻量级锁、重量级锁。并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,这四种锁的级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级锁。
无锁:无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
偏向锁:初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。
轻量级锁:轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。轻量级锁的获取主要由两种情况:1当关闭偏向锁功能时;2由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。
重量级锁:如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起,等待将来被唤醒。
在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。synchronized关键字经过Javac编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作为reference。如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。
在并发程序中,串行操作是会降低可伸缩性,并且上下文切换也会减低性能。在锁上发生竞争时将通水导致这两种问题,使用独占锁时保护受限资源的时候,基本上是采用串行方式—-每次只能有一个线程能访问它。所以对于可伸缩性来说最大的威胁就是独占锁。
我们一般有三种方式降低锁的竞争程度:
减少锁的持有时间;
降低锁的请求频率;
使用带有协调机制的独占锁,这些机制允许更高的并发性。
与传统锁不同的是读写锁的规则是可以共享读。
在Java中ReadWriteLock的主要实现为ReentrantReadWriteLock,其提供了以下特性:
公平性选择:支持公平与非公平(默认)的锁获取方式,吞吐量非公平优先于公平。
可重入:读线程获取读锁之后可以再次获取读锁,写线程获取写锁之后可以再次获取写锁。
可降级:写线程获取写锁之后,其还可以再次获取读锁,然后释放掉写锁,那么此时该线程是读锁状态,也就是降级操作。
JUC这个包下的类基本上包含了我们在并发编程时用到的一些工具,大致可以分为以下几类:
原子更新:Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新 数组,原子更新引用和原子更新字段。
锁和条件变量:java.util.concurrent.locks包下包含了同步器的框架 AbstractQueuedSynchronizer,基于AQS构建的Lock以及与Lock配合可以实现等待/通知模式的Condition。JUC 下的大多数工具类用到了Lock和Condition来实现并发。
**线程池:**涉及到的类比如:Executor、Executors、ThreadPoolExector、 AbstractExecutorService、Future、Callable、ScheduledThreadPoolExecutor等等。
**阻塞队列:**涉及到的类比如:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、LinkedBlockingDeque等等。
**并发容器:**涉及到的类比如:ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue、CopyOnWriteArraySet等等
**同步器:**剩下的是一些在并发编程中时常会用到的工具类,主要用来协助线程同步。比如:CountDownLatch、CyclicBarrier、Exchanger、Semaphore、FutureTask等等。
抽象队列同步器AbstractQueuedSynchronizer (以下都简称AQS),是用来构建锁或者其他同步组件的骨架类,减少了各功能组件实现的代码量,也解决了在实现同步器时涉及的大量细节问题,例如等待线程采用FIFO队列操作的顺序。在不同的同步器中还可以定义一些灵活的标准来判断某个线程是应该通过还是等待。
基于AQS实现的组件,诸如:
ReentrantLock 可重入锁(支持公平和非公平的方式获取锁);
Semaphore 计数信号量;
ReentrantReadWriteLock 读写锁。
AQS内部维护了一个int成员变量来表示同步状态,通过内置的FIFO(first-in-first-out)同步队列来控制获取共享资源的线程。AQS其实主要做了这么几件事情:
同步状态(state)的维护管理;
等待队列的维护管理;
线程的阻塞与唤醒。
核心思想:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。也就是牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。
三要素:
什么叫允许损失部分可用性呢?
响应时间上的损失: 正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。
系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。
**因为,两个不同的服务器上的服务提供的方法不在一个内存空间,所以,需要通过网络编程才能传递方法调用所需要的参数。**并且,方法调用的结果也需要通过网络编程来接收。但是,如果我们自己手动网络编程来实现这个调用过程的话工作量是非常大的,因为,我们需要考虑底层传输方式(TCP还是UDP)、序列化方式等等方面。
git config user.name "lamplcc" //设置用户名
设置邮箱
git config user.email "[email protected]"
设置全局用户名
git config --global user.name "lamplcc"
设置全局邮箱
git config --global user.email "[email protected]"
查看配置
git config --list
git init
git clone
git clone --branch [tags标签] [git地址]
git remote -v
git remote add [name] [url]
git remote rm [name]
git pull
git add
git rm
git rm --cached
git mv
git commit -m 'message'
git commit -v
git commit --amend -m [message]
git push
git log
git log -p [file]
git blame [file]
git status
git diff
git --help
git help -a
git help -g
git help
git checkout [file]
git checkout [commit] [file]
git checkout .
git reset [file]
git reset --hard
git reset --hard [commit]
git reset [commit]
git reset --keep [commit]
git revert [commit]
git branch
git branch -v
git branch [branch-name]
git checkout [branch-name]
git checkout -b [branch-name]
git branch -d [branch-name]
git merge [branch-name]
git tag
git tag -r
git tag [tag-name]
git tag -a [tag-name] -m 'message'
git tag -d [tag-name]
在Spring中,事务有两种实现方式,分别是编程式事务管理和声明式事务管理两种方式。
参考答案
从本质上来说,Spring Boot就是Spring,它做了那些没有它你自己也会去做的Spring Bean配置。Spring Boot使用“习惯优于配置”的理念让你的项目快速地运行起来,使用Spring Boot很容易创建一个能独立运行、准生产级别、基于Spring框架的项目,使用Spring Boot你可以不用或者只需要很少的Spring配置。
简而言之,Spring Boot本身并不提供Spring的核心功能,而是作为Spring的脚手架框架,以达到快速构建项目、预置三方配置、开箱即用的目的。Spring Boot有如下的优点:
可以快速构建项目;
可以对主流开发框架的无配置集成;
项目可独立运行,无需外部依赖Servlet容器;
提供运行时的应用监控;
可以极大地提高开发、部署效率;
可以与云计算天然集成。
参考答案
Spring Boot通过提供众多起步依赖(Starter)降低项目依赖的复杂度。起步依赖本质上是一个Maven项目对象模型(Project Object Model, POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。很多起步依赖的命名都暗示了它们提供的某种或某类功能。
举例来说,你打算把这个阅读列表应用程序做成一个Web应用程序。与其向项目的构建文件里添加一堆单独的库依赖,还不如声明这是一个Web应用程序来得简单。你只要添加Spring Boot的Web起步依赖就好了。
参考答案
首先,Spring Boot项目创建完成会默认生成一个名为 *Application 的入口类,我们是通过该类的main方法启动Spring Boot项目的。在main方法中,通过SpringApplication的静态方法,即run方法进行SpringApplication类的实例化操作,然后再针对实例化对象调用另外一个run方法来完成整个项目的初始化和启动。
SpringApplication调用的run方法的大致流程,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kingr0sE-1666488157653)(wps14.jpg)]
其中,SpringApplication在run方法中重点做了以下操作:
获取监听器和参数配置;
打印Banner信息;
创建并初始化容器;
监听器发送通知。
当然,除了上述核心操作,run方法运行过程中还涉及启动时长统计、异常报告、启动日志、异常处理等辅助操作。
参考答案
通过Spring Boot Starter导入包。
Spring Boot通过提供众多起步依赖(Starter)降低项目依赖的复杂度。起步依赖本质上是一个Maven项目对象模型(Project Object Model, POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。很多起步依赖的命名都暗示了它们提供的某种或某类功能。
举例来说,你打算把这个阅读列表应用程序做成一个Web应用程序。与其向项目的构建文件里添加一堆单独的库依赖,还不如声明这是一个Web应用程序来得简单。你只要添加Spring Boot的Web起步依赖就好了。
参考答案
使用Spring Boot时,我们只需引入对应的Starters,Spring Boot启动时便会自动加载相关依赖,配置相应的初始化参数,以最快捷、简单的形式对第三方软件进行集成,这便是Spring Boot的自动配置功能。Spring Boot实现该运作机制锁涉及的核心部分如下图所示:
整个自动装配的过程是:Spring Boot通过@EnableAutoConfiguration注解开启自动配置,加载spring.factories中注册的各种AutoConfiguration类,当某个AutoConfiguration类满足其注解@Conditional指定的生效条件(Starters提供的依赖、配置或Spring容器中是否存在某个Bean等)时,实例化该AutoConfiguration类中定义的Bean(组件等),并注入Spring容器,就可以完成依赖框架的自动配置。
**@SpringBootApplication注解:**在Spring Boot入口类中,唯一的一个注解就是@SpringBootApplication。它是Spring Boot项目的核心注解,用于开启自动配置,准确说是通过该注解内组合的@EnableAutoConfiguration开启了自动配置。
@EnableAutoConfiguration注解:@EnableAutoConfiguration的主要功能是启动Spring应用程序上下文时进行自动配置,它会尝试猜测并配置项目可能需要的Bean。自动配置通常是基于项目classpath中引入的类和已定义的Bean来实现的。在此过程中,被自动配置的组件来自项目自身和项目依赖的jar包中。
@Import注解:@EnableAutoConfiguration的关键功能是通过@Import注解导入的ImportSelector来完成的。从源代码得知@Import(AutoConfigurationImportSelector.class)是@EnableAutoConfiguration注解的组成部分,也是自动配置功能的核心实现者。
@Conditional注解:@Conditional注解是由Spring 4.0版本引入的新特性,可根据是否满足指定的条件来决定是否进行Bean的实例化及装配,比如,设定当类路径下包含某个jar包的时候才会对注解的类进行实例化操作。总之,就是根据一些特定条件来控制Bean实例化的行为。
**@Conditional衍生注解:**在Spring Boot的autoconfigure项目中提供了各类基于@Conditional注解的衍生注解,它们适用不同的场景并提供了不同的功能。通过阅读这些注解的源码,你会发现它们其实都组合了@Conditional注解,不同之处是它们在注解中指定的条件(Condition)不同。
@ConditionalOnBean:在容器中有指定Bean的条件下。
@ConditionalOnClass:在classpath类路径下有指定类的条件下。
@ConditionalOnCloudPlatform:当指定的云平台处于active状态时。
@ConditionalOnExpression:基于SpEL表达式的条件判断。
@ConditionalOnJava:基于JVM版本作为判断条件
@ConditionalOnJndi:在JNDI存在的条件下查找指定的位置。
@ConditionalOnMissingBean:当容器里没有指定Bean的条件时。
@ConditionalOnMissingClass:当类路径下没有指定类的条件时。
@ConditionalOnNotWebApplication:在项目不是一个Web项目的条件下。
@ConditionalOnProperty:在指定的属性有指定值的条件下。
@ConditionalOnResource:类路径是否有指定的值。
@ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个或者有多个但是指定了首选的Bean时。
@ConditionalOnWebApplication:在项目是一个Web项目的条件下。
启动类上面的注解是@SpringBootApplication,它也是SpringBoot的核心注解 主要组合包含了以下 3 个注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能, 也可以关闭某个自动配置的选项,比如关闭数据源自动配置功能:
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:Spring组件扫描。
@PropertySource
@Value
@Environment
@ConfifigurationProperties
@SpringBootTest
@ControllerAdvice
@ExceptionHandler
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JJUP8VG7-1666488157653)(wps15.jpg)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ukx7zjFe-1666488157654)(wps16.jpg)]
单独使⽤ @Controller 不加 @ResponseBody 的话⼀般使⽤在要返回⼀个视图的情况,@RestController 返回JSON 或 XML 形式数据,但 @RestController 只返回对象,对象数据直接以 JSON 或 XML 形式写⼊ HTTP 响应,(Response)中,这种情况属于 RESTful Web服务
@Controller +@ResponseBody 返回JSON 或 XML 形式数据
@ResponseBody 注解的作⽤是将 Controller 的⽅法返回的对象通过适当的转换器转换为指定的格式之后,写⼊到HTTP 响应(Response)对象的 body 中,通常⽤来返回 JSON 或者XML 数据,返回 JSON 数据的情况⽐多。
新建状态(New):
创建一个新的线程对象。
就绪状态(Runnable):
线程创建对象后,其他线程调用start()方法,该线程处于就绪状态,资源已经准备就绪,等待CPU资源。
运行状态(Running):
处于就绪状态的线程获取到CPU资源后进入运行状态。
阻塞状态(Blocked):
阻塞状态是线程由于某些原因放弃CPU使用,暂时停止运行。
等待阻塞:线程调用start()方法,JVM会把这个线程放入等待池中,该线程需要其他线程调用notify()或notifyAll()方法才能被唤醒。
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程占用,则JVM会把该线程放入锁池中。
其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
终止状态(Terminated):
线程run()方法运行完毕,该线程结束。
创建线程有三种方式,分别是继承Thread类(单继承)、实现Runnable接口(多继承)、实现Callable接口。
一、继承Thread类创建线程类
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
二、通过Runnable接口创建线程类
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
三、通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
用start方法来启动线程,真正实现了多线程运行,它的方法体代表了线程需要完成的任务。
如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。
只能对处于新建状态的线程调用start()方法,否则将引发IllegalThreadStateException异常。
Thread类常用静态方法:
currentThread():返回当前正在执行的线程;
interrupted():返回当前执行的线程是否已经被中断;
sleep(long millis):使当前执行的线程睡眠多少毫秒数;
yield():使当前执行的线程自愿暂时放弃对处理器的使用权并允许其他线程执行;
Thread类常用实例方法:
getId():返回该线程的id;
getName():返回该线程的名字;
getPriority():返回该线程的优先级;
interrupt():使该线程中断;
isInterrupted():返回该线程是否被中断;
isAlive():返回该线程是否处于活动状态;
isDaemon():返回该线程是否是守护线程;
setDaemon(boolean on):将该线程标记为守护线程或用户线程,如果不标记默认是非守护线程;
setName(String name):设置该线程的名字;
setPriority(int newPriority):改变该线程的优先级;
join():等待该线程终止;
join(long millis):等待该线程终止,至多等待多少毫秒数。
在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。
(1)当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。程序也不会执行线程的线程执行体。
(2)当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了
(3)如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。
(4)当发生如下情况时,线程将会进入阻塞状态:
线程调用sleep()方法主动放弃所占用的处理器资源。
线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
线程在等待某个通知(notify)。
程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。
(5)当发生如下特定的情况时可以解除阻塞,让该线程重新进入就绪状态:
调用sleep()方法的线程经过了指定时间。
线程调用的阻塞式IO方法已经返回。
线程成功地获得了试图取得的同步监视器。
线程正在等待某个通知时,其他线程发出了一个通知。
处于挂起状态的线程被调用了resume()恢复方法。
(6)线程会以如下三种方式结束,结束后就处于死亡状态:run()或call()方法执行完成,线程正常结束。
(1)wait()、notify()、notifyAll()
如果线程之间采用synchronized来保证线程安全,则可以利用wait()、notify()、notifyAll()来实现线程通信。这三个方法都不是Thread类中所声明的方法,而是Object类中声明的方法。另外,这三个方法都是本地方法,并且被final修饰,无法被重写。
wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
(2)await()、signal()、signalAll()
**如果线程之间采用Lock来保证线程安全,则可以利用await()、signal()、signalAll()来实现线程通信。**这三个方法都是Condition接口中的方法,该接口是在Java 1.5中出现的,它用来替代传统的wait+notify实现线程间的协作,它的使用依赖于 Lock。相比使用wait+notify,使用Condition的await+signal这种方式能够更加安全和高效地实现线程间协作。
(3)BlockingQueue
Java 5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程通信的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。线程之间需要通信,最经典的场景就是生产者与消费者模型,而BlockingQueue就是针对该模型提供的解决方案。
sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用;
sleep()不会释放锁,而wait()会释放锁,并需要通过notify()/notifyAll()重新获取锁。
Thread类提供了让一个线程等待另一个线程完成的方法_join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。
synchronized是Java多线程中经常使用的一个关键字。synchronized可以保证原子性、可见性、有序性。它包括两种用法:synchronized 方法和 synchronized 代码块。它可以用来给对象、方法或代码块进行加锁。当它锁定一个方法或者一个代码块时,同一时刻最多只有一个线程可以执行这段代码,其他线程想在此时调用该方法只能排队等候。当它锁定一个对象时,同一时刻最多只有一个线程可以对这个类进行操作,没有获得锁的线程,在该类所有对象上的任何操作都不能进行。
synchronized是Java关键字,在JVM层面实现加锁和解锁;Lock是一个接口,在代码层面实现加锁和解锁。volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取。
synchronized可以用在代码块上、方法上、变量、类;Lock只能写在代码里;volatile仅能用在变量级别。
synchronized在代码执行完或出现异常时自动释放锁;Lock不会自动释放锁,需要在finally中显示释放锁。
synchronized会导致线程拿不到锁一直等待;Lock可以设置获取锁失败的超时时间。
synchronized无法得知是否获取锁成功;Lock则可以通过tryLock得知加锁是否成功。
synchronized锁可重入、不可中断、非公平;Lock锁可重入、可中断、可公平/不公平,并可以细分读写锁以提高效率。
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程阻塞,synchronized可能会造成线程阻塞。
synchronized作用在代码块时,它的底层是通过monitorenter、monitorexit指令来实现的。
monitorenter:每个对象都是一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权。
monitorexit:执行monitorexit的线程必须是objectref所对应的monitor持有者。monitorexit指令出现了两次,第1次为同步正常退出释放锁,第2次为发生异步退出释放锁。
方法的同步并没有通过 monitorenter 和 monitorexit 指令来完成,不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的。
当一个变量被定义成volatile之后,它将具备两项特性:
保证可见性:当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去,这个写会操作会导致其他线程中的volatile变量缓存无效。
禁止指令重排:使用volatile关键字修饰共享变量可以禁止指令重排序,volatile禁止指令重排序有一些规则:
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行;
在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
首先cpu会根据共享变量是否带有Volatile字段,来决定是否使用MESI协议保证缓存一致性。
如果有Volatile,汇编层面会对变量加上Lock前缀,当一个线程修改变量的值后,会马上经过store、write等原子操作修改主内存的值(如果不加Lock前缀不会马上同步),为什么监听到修改会马上同步呢?就是为了触发cpu的嗅探机制,及时失效其他线程变量副本。
ReentrantLock是基于AQS实现的,AQS即AbstractQueuedSynchronizer的缩写,这个是个内部实现了两个队列的抽象类,分别是同步队列和条件队列。
其中同步队列是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁。
而条件队列是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾,
AQS所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作。
系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个空闲的线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()或call()方法。
Java 5新增了一个Executors工厂类来产生线程池,该工厂类包含如下几个静态工厂方法来创建线程池。创建出来的线程池,都是通过ThreadPoolExecutor类来实现的。
newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。
newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于调用newFixedThread Pool()方法时传入参数为1。
newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。
newSingleThreadScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。
判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。
判断任务队列是否已满,没满则将新提交的任务添加在工作队列。
判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和(拒绝)策略。
线程池一共有五种状态, 分别是:
RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务。
SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。
STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态。
TIDYING:如果所有的任务都已终止了,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。进入TERMINATED的条件如下:线程池不是RUNNING状态;线程池状态不是TIDYING状态或TERMINATED状态;线程池状态是SHUTDOWN并且workerQueue为空;workerCount为0;设置TIDYING状态成功。
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
DiscardPolicy:也是丢弃任务,但是不抛出异常。
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)。
CallerRunsPolicy:由调用线程处理该任务。
线程池主要有如下6个参数:
corePoolSize(核心工作线程数):若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时。
maximumPoolSize(最大线程数):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
keepAliveTime(多余线程存活时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
workQueue(队列):用于传输和保存等待执行任务的阻塞队列。
threadFactory(线程创建工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
handler(拒绝策略):当线程池和队列都满了,再加入线程会执行此策略
public ThreadPoolExecutor(
int corePoolSize, //核心池的大小。
int maximumPoolSize, //池中允许的最大线程数,这个参数表示了线程池中最多能创建的线程数量
long keepAliveTime, //当线程数大于corePoolSize时,终止前多余的空闲线程等待新任务的最长时间
TimeUnit unit, //keepAliveTime时间单位
BlockingQueue<Runnable> workQueue, //存储还没来得及执行的任务
ThreadFactory threadFactory, //执行程序创建新线程时使用的工厂
RejectedExecutionHandler handler //由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序
)
ThreadLocal、可以理解成每个线程都有自己专属的存储容器,它用来存储线程私有变量,内部真正存取是一个Map。每个线程可以通过set()和get()存取变量,多线程间无法访问各自的局部变量,相当于在每个线程间建立了一个隔板。只要线程处于活动状态,它所对应的ThreadLocal实例就是可访问的,线程被终止后,它的所有实例将被垃圾收集。总之记住一句话:ThreadLocal存储的变量属于当前线程。
ThreadLocal经典的使用场景是为每个线程分配一个 JDBC 连接 Connection,这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了B线程正在使用的 Connection。 另外ThreadLocal还经常用于管理Session会话,将Session保存在ThreadLocal中,使线程处理多次处理会话时始终是同一个Session。
Thread类中有个变量threadLocals,它的类型为ThreadLocal中的一个内部类ThreadLocalMap,这个类没有实现map接口,就是一个普通的Java类,但是实现的类似map的功能。每个线程都有自己的一个map,map是一个数组的数据结构存储数据,每个元素是一个Entry,entry的key是ThreadLocal的引用,也就是当前变量的副本,value就是set的值。
ThreadLocal中的set方法的实现逻辑,先获取当前线程,取出当前线程的ThreadLocalMap,如果不存在就会创建一个ThreadLocalMap,如果存在就会把当前的threadlocal的引用作为键,传入的参数作为值存入map中。
ThreadLocal中get方法的实现逻辑,获取当前线程,取出当前线程的ThreadLocalMap,用当前的threadlocak作为key在ThreadLocalMap查找,如果存在不为空的Entry,就返回Entry中的value,否则就会执行初始化并返回默认的值。
ThreadLocal中remove方法的实现逻辑,还是先获取当前线程的ThreadLocalMap变量,如果存在就调用ThreadLocalMap的remove方法。
工厂模式
这是一个最基础的设计模式,也是最常用的设计模式,它有简单工厂模式,工厂模式,抽象工厂模式。各有各的优缺点。
故名思意,它就是一个加工厂,不同于现实的是,此处生产的不是生活用品,而是我们面向对象编程中最重要的对象。工厂模式比简单工厂模式更弹性化,遵循了开发-封闭原则。
抽象工厂就像现实中的工厂一样,好处大家都知道,可以批量生产与定制,因为有不一样的模具,就可以生产出大家需要的各种类型产品。
软件开发中,我们更关注的是使用对象的方法,至于如何创建对象我们并不关心,抽象工厂只要定制我们所需的产品功能接口,然后让工厂实现接口生产对象即可。
单例模式
这是一个非常简单的模式,只包含了一个类,重点要管理单例实例的创建,一般为了避免使用者错误创建多余的对象,单例的构造函数和析构函数声明为私有函数。多种单例如果有依赖关系,就要仔细处理构建顺序。它有几个优点,使用简单,可以避免使用全局变量,隐藏对象的构建细节,避免多次构建容易引起的错误。总之,使用它不要急于一时的需求,因为如果将某类设计为单例就限制了可扩展性,也会形成在各种可以随意引用的一种趋向,不过这正也是它的便利之处。
装饰器模式
人靠衣装马靠鞍,好的衣服可以提升一个人的气质,但不会改变外貌与功能,这就是装饰器模式,通过装饰一个对象让它更强大却不会改变它的本质。
举一个软件开发中的例子,比如你们已经做好一个图片传送功能,也经过了测试和线上测试这个功能很完美没毛病,可是突然出现了一个新的需求,想要发送图片时,还能语音提醒,你们怎样在不影响原有的功能情况下实现它呢,现在就可以使用装饰器,也就是给图片发送类装饰一个语音功能。
适配器模式
适配器是什么?就比如耳机,它可以连接在你的手机上也可以连上别人的手机,电脑也可以,它就是一种适配器。
程序员们几乎不可能离开数据库去单独开发一款应用,所以选择什么数据库是最需要关心的事情,一旦选择错误,后期在性能上就会遇见很多瓶颈,适配器模式可以让程序员们在不用修改或者改很少代码的情况下进行数据库的随意切换。
第一步要定义好适配器接口,接着让各种数据库实现我们定义好的接口,在代码里用定义好的方法,当你想要切换数据库时,将该数据库实现对应接口的方法,就可以做到无缝连接啦。
策略模式
策略就是实现目标方案的集合,它们都是用来实现一件事情的。
在软件开发中,一个对象可以对不同场景使用不同的策略去实现同一个功能,比如在学习中老师会制定一个本学期期末目标是多少,但是每个同学怎样去完成它这个过程都是不一样的,但是结果是一样的。
某宝首页的千人千面也是策略模式,都显示了商品,但面对不同的人不同的喜好,商品就是不一样的,这就是由策略决定的。
总结
设计模式一定源于生活,其实,万物都是来源于生活,但经过我们的学习提炼之后,它便高于生活。设计模式可以帮助你解决大部分问题,使用它会让你的代码看起来更加清晰,有条理。
**可以保证系统中,应用该模式的这个类永远只有一个实例,即一个类永远只能创建一个对象。**例如任务管理器对象我们只需要一个就可以解决问题了,这样可以节省内存空间。
在用类获取对象的时候,对象已经提前为你创建好了。饿汉式更容易理解就是很着急的初始化对象,因此在创建对象的时候会直接初始化对象为对象分配内存空间(生声明对象的时候就初始化),不管这个对象要不要使用都会占据内存空间。线程安全的,效率比懒汉式高。
/** a、定义一个单例类 */
public class SingleInstance {
/** c.定义一个静态变量存储一个对象即可 :属于类,与类一起加载一次 */
public static SingleInstance instance = new SingleInstance ();
/** b.单例必须私有构造器*/
private SingleInstance (){
System.out.println("创建了一个对象");
}
}
在真正需要该对象的时候,才去创建一个对象(延迟加载对象)。懒汉式顾名思义就是比较懒,在创建对象之后并不会直接初始化这个对象,只是将对象设置为null,因此并不会占用内存,在静态方法中进行初始化对象。线程不安全的,需要加同步锁,同步锁影响效率。
/** 定义一个单例类 */
class SingleInstance{
/** 定义一个静态变量存储一个对象即可 :属于类,与类一起加载一次 */
public static SingleInstance instance ; // null
/** 单例必须私有构造器*/
private SingleInstance(){}
/** 必须提供一个方法返回一个单例对象 */
public static SingleInstance getInstance(){
...
return ...;
}
}
两种单例模式的优缺点:
饿汉式单例模式
优点:对象提前创建好了,使用的时候无需等待,效率高
缺点:对象提前创建,所以会占据一定的内存,内存占用大
以空间换时间
懒汉式单例模式
优点:使用对象时,对象才创建,所以不会提前占用内存,内存占用小
缺点:首次使用对象时,需要等待对象的创建,而且每次都需要判断对象是否为空,运行效率较低
以时间换空间
因为饿汉式单例效率高,实现简单,推荐使用饿汉式单例
饿汉式
/*
保证别人使用该类的时候,只能获取一个对象(Singleton只能有一个对象)
步骤:
1.私有类的构造器
2.提供该类的静态修饰的对象
3.提供公开的静态方法,返回该类的唯一对象
*/
public class Singleton {
//1.私有类的构造器,目的是不让外界创建对象
private Singleton(){
}
//2.提供该类的静态修饰的对象(饿汉式,对象是直接创建的)
private static Singleton single = new Singleton();
//3.提供公开的静态方法,返回该类的唯一对象
public static Singleton getInstance(){
return single;
}
}
懒汉式
/*懒汉式单例
1.私有构造方法
2.提供该类的静态变量,不是马上创建
3.提供公开的获取唯一对象的静态方法
*/
public class Single {
//1.私有构造方法
private Single(){}
//2.提供该类的静态变量,不是马上创建
private static Single single = null;
//3.提供公开的获取唯一对象的静态方法
public static Single getInstance(){
//如果对象为空,则创建,否则直接返回
if(single==null){
single = new Single();
}
return single;
}
}
普通工厂模式:普通工厂模式中,其实是对实现了同一接口的类进行实例的创建,在工厂中进行类的创建。
代码示例如下
创建一个其它类都需要继承的接口,其中接口中定义一个方法。
public interface Animal {
public void create();
}
创建两个实现类,继承接口同时重写接口中的方法。两个实现类中对重写的方法输出不同的值。
public class Cat implements Animal {
@Override
public void create() {
System.out.println("this is cat");
}
}
public class Dog implements Animal {
@Override
public void create(){
System.out.println("this is dog");
}
}
创建一个工厂,工厂中创建一个方法,在方法中通过判断来决定创建哪一个实体类。
public class AnimalFactory {
public Animal create(String type){
if ("dog".equals(type)){
return new Dog();
}else if ("cat".equals(type)){
return new Cat();
}else {
System.out.println("请输入正确的动物");
return null;
}
}
}
创建一个工厂测试类
public class FactoryTest {
public static void main(String[] args) {
AnimalFactory animalFactory = new AnimalFactory();
Animal animal = animalFactory.create("dog");
animal.create();
}
}
总结:通过工厂模式,我们不需要对我们要使用的类一个一个来创建了,我们可以通过工厂类,通过一定的算法来实现我们需要的实体类的创建。这样减轻了内存的负载,只创建需要的类,同时减少冗余。
多个工厂模式是对工厂方法的改进,在普通工厂模式中,如果传递的字符串出错,那么就无法正确的创建对象,而多个工厂模式是提供多个工厂方法,分别创建对象。
public interface Animal {
public void create();
}
public class Cat implements Animal {
@Override
public void create() {
System.out.println("this is cat");
}
}
public class Dog implements Animal {
@Override
public void create(){
System.out.println("this is dog");
}
}
public class AnimalFactory {
public Animal createDog(){
return new Dog();
}
public Animal createCat(){
return new Cat();
}
}
public class FactoryTest {
public static void main(String[] args) {
AnimalFactory animalFactory = new AnimalFactory();
Animal animal = animalFactory.createCat();
animal.create();
}
}
在多个工厂模式中我们无需通过传递字符串确定我们需要创建哪个对象,只需要通过调用接口中的实现方法就可以实现创建对象。
public interface Animal {
public void create();
}
public class Cat implements Animal {
@Override
public void create() {
System.out.println("this is cat");
}
}
public class Dog implements Animal {
@Override
public void create(){
System.out.println("this is dog");
}
}
public interface Action {
public Animal action();
}
public class CatFactory implements Action {
@Override
public Animal action() {
return new Cat();
}
}
public class DogFactory implements Action {
@Override
public Animal action() {
return new Dog();
}
}
public class Test {
public static void main(String[] args) {
Action action = new CatFactory();
Animal animal = action.action();
animal.create();
}
}
其实这个模式的好处就是,如果你现在想增加一个功能:发及时信息,则只需做一个实现类,实现Animal接口,同时做一个工厂类,实现Action接口,就OK了,无需去改动现成的代码。这样做,拓展性较好!
将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。主要的作用是实现在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂的对象。很好的解决了用户创建复杂对象的类型和内容。
public interface Animal {
public void create();
}
public class Cat implements Animal {
@Override
public void create() {
System.out.println("this is cat");
}
}
public class Dog implements Animal {
@Override
public void create(){
System.out.println("this is dog");
}
}
public class Builder {
private List<Animal> list = new ArrayList <>();
public void createCat(int count){
for (int i = 0; i < count; i++) {
list.add(new Cat());
}
System.out.println(list);
}
public void createDog(int count){
for (int i = 0; i < count; i++) {
list.add(new Dog());
}
}
}
public class Test {
public static void main(String[] args) {
Builder builder = new Builder();
builder.createCat(3);
}
}
建造者模式将很多功能集成到一个类里,这个类可以创造出比较复杂的东西。所以与工厂模式的区别就是:工厂模式关注的是创建单个产品,而建造者模式则关注创建符合对象,多个部分。因此,是选择工厂模式还是建造者模式,依实际情况而定。
c.定义一个静态变量存储一个对象即可 :属于类,与类一起加载一次 /
public static SingleInstance instance = new SingleInstance ();
/* b.单例必须私有构造器*/
private SingleInstance (){
System.out.println(“创建了一个对象”);
}
}
## 懒汉单例设计模式实现步骤?
在真正需要该对象的时候,才去创建一个对象(延迟加载对象)。懒汉式顾名思义就是比较懒,**在创建对象之后并不会直接初始化这个对象,只是将对象设置为null,因此并不会占用内存**,在静态方法中进行初始化对象。**线程不安全的,需要加同步锁,同步锁影响效率**。
- 定义一个类,把构造器私有。
- 定义一个静态变量存储一个对象。
- 提供一个返回单例对象的方法
```java
/** 定义一个单例类 */
class SingleInstance{
/** 定义一个静态变量存储一个对象即可 :属于类,与类一起加载一次 */
public static SingleInstance instance ; // null
/** 单例必须私有构造器*/
private SingleInstance(){}
/** 必须提供一个方法返回一个单例对象 */
public static SingleInstance getInstance(){
...
return ...;
}
}
两种单例模式的优缺点:
饿汉式单例模式
优点:对象提前创建好了,使用的时候无需等待,效率高
缺点:对象提前创建,所以会占据一定的内存,内存占用大
以空间换时间
懒汉式单例模式
优点:使用对象时,对象才创建,所以不会提前占用内存,内存占用小
缺点:首次使用对象时,需要等待对象的创建,而且每次都需要判断对象是否为空,运行效率较低
以时间换空间
因为饿汉式单例效率高,实现简单,推荐使用饿汉式单例
饿汉式
/*
保证别人使用该类的时候,只能获取一个对象(Singleton只能有一个对象)
步骤:
1.私有类的构造器
2.提供该类的静态修饰的对象
3.提供公开的静态方法,返回该类的唯一对象
*/
public class Singleton {
//1.私有类的构造器,目的是不让外界创建对象
private Singleton(){
}
//2.提供该类的静态修饰的对象(饿汉式,对象是直接创建的)
private static Singleton single = new Singleton();
//3.提供公开的静态方法,返回该类的唯一对象
public static Singleton getInstance(){
return single;
}
}
懒汉式
/*懒汉式单例
1.私有构造方法
2.提供该类的静态变量,不是马上创建
3.提供公开的获取唯一对象的静态方法
*/
public class Single {
//1.私有构造方法
private Single(){}
//2.提供该类的静态变量,不是马上创建
private static Single single = null;
//3.提供公开的获取唯一对象的静态方法
public static Single getInstance(){
//如果对象为空,则创建,否则直接返回
if(single==null){
single = new Single();
}
return single;
}
}
普通工厂模式:普通工厂模式中,其实是对实现了同一接口的类进行实例的创建,在工厂中进行类的创建。
代码示例如下
创建一个其它类都需要继承的接口,其中接口中定义一个方法。
public interface Animal {
public void create();
}
创建两个实现类,继承接口同时重写接口中的方法。两个实现类中对重写的方法输出不同的值。
public class Cat implements Animal {
@Override
public void create() {
System.out.println("this is cat");
}
}
public class Dog implements Animal {
@Override
public void create(){
System.out.println("this is dog");
}
}
创建一个工厂,工厂中创建一个方法,在方法中通过判断来决定创建哪一个实体类。
public class AnimalFactory {
public Animal create(String type){
if ("dog".equals(type)){
return new Dog();
}else if ("cat".equals(type)){
return new Cat();
}else {
System.out.println("请输入正确的动物");
return null;
}
}
}
创建一个工厂测试类
public class FactoryTest {
public static void main(String[] args) {
AnimalFactory animalFactory = new AnimalFactory();
Animal animal = animalFactory.create("dog");
animal.create();
}
}
总结:通过工厂模式,我们不需要对我们要使用的类一个一个来创建了,我们可以通过工厂类,通过一定的算法来实现我们需要的实体类的创建。这样减轻了内存的负载,只创建需要的类,同时减少冗余。
多个工厂模式是对工厂方法的改进,在普通工厂模式中,如果传递的字符串出错,那么就无法正确的创建对象,而多个工厂模式是提供多个工厂方法,分别创建对象。
public interface Animal {
public void create();
}
public class Cat implements Animal {
@Override
public void create() {
System.out.println("this is cat");
}
}
public class Dog implements Animal {
@Override
public void create(){
System.out.println("this is dog");
}
}
public class AnimalFactory {
public Animal createDog(){
return new Dog();
}
public Animal createCat(){
return new Cat();
}
}
public class FactoryTest {
public static void main(String[] args) {
AnimalFactory animalFactory = new AnimalFactory();
Animal animal = animalFactory.createCat();
animal.create();
}
}
在多个工厂模式中我们无需通过传递字符串确定我们需要创建哪个对象,只需要通过调用接口中的实现方法就可以实现创建对象。
public interface Animal {
public void create();
}
public class Cat implements Animal {
@Override
public void create() {
System.out.println("this is cat");
}
}
public class Dog implements Animal {
@Override
public void create(){
System.out.println("this is dog");
}
}
public interface Action {
public Animal action();
}
public class CatFactory implements Action {
@Override
public Animal action() {
return new Cat();
}
}
public class DogFactory implements Action {
@Override
public Animal action() {
return new Dog();
}
}
public class Test {
public static void main(String[] args) {
Action action = new CatFactory();
Animal animal = action.action();
animal.create();
}
}
其实这个模式的好处就是,如果你现在想增加一个功能:发及时信息,则只需做一个实现类,实现Animal接口,同时做一个工厂类,实现Action接口,就OK了,无需去改动现成的代码。这样做,拓展性较好!
将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。主要的作用是实现在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂的对象。很好的解决了用户创建复杂对象的类型和内容。
public interface Animal {
public void create();
}
public class Cat implements Animal {
@Override
public void create() {
System.out.println("this is cat");
}
}
public class Dog implements Animal {
@Override
public void create(){
System.out.println("this is dog");
}
}
public class Builder {
private List<Animal> list = new ArrayList <>();
public void createCat(int count){
for (int i = 0; i < count; i++) {
list.add(new Cat());
}
System.out.println(list);
}
public void createDog(int count){
for (int i = 0; i < count; i++) {
list.add(new Dog());
}
}
}
public class Test {
public static void main(String[] args) {
Builder builder = new Builder();
builder.createCat(3);
}
}
建造者模式将很多功能集成到一个类里,这个类可以创造出比较复杂的东西。所以与工厂模式的区别就是:工厂模式关注的是创建单个产品,而建造者模式则关注创建符合对象,多个部分。因此,是选择工厂模式还是建造者模式,依实际情况而定。