diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 3cce28e5..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index c5217a32..48b1d15d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,8 @@ hs_err_pid* out # iml -*.iml \ No newline at end of file +*.iml + +.DS_Store +.aider* +.env diff --git a/Java/.DS_Store b/Java/.DS_Store deleted file mode 100644 index 84f1385a..00000000 Binary files a/Java/.DS_Store and /dev/null differ diff --git a/Java/alg/.DS_Store b/Java/alg/.DS_Store deleted file mode 100644 index 77e194cf..00000000 Binary files a/Java/alg/.DS_Store and /dev/null differ diff --git "a/Java/alg/lc/102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" "b/Java/alg/lc/102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" index 5befa3f2..30b6fefb 100644 --- "a/Java/alg/lc/102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" +++ "b/Java/alg/lc/102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" @@ -1,6 +1,6 @@ # 102.二叉树的层序遍历 -[url](https://leetcode-cn.com/problems/unique-binary-search-trees/) +[url](https://leetcode-cn.com/problems/binary-tree-level-order-traversal/) ## 题目 diff --git "a/Java/alg/lc/2.\344\270\244\346\225\260\347\233\270\345\212\240.md" "b/Java/alg/lc/2.\344\270\244\346\225\260\347\233\270\345\212\240.md" index b89af548..b0c9dd43 100644 --- "a/Java/alg/lc/2.\344\270\244\346\225\260\347\233\270\345\212\240.md" +++ "b/Java/alg/lc/2.\344\270\244\346\225\260\347\233\270\345\212\240.md" @@ -1,4 +1,4 @@ -# 1.两数之和 +# 2.两数相加 [url](https://leetcode-cn.com/problems/add-two-numbers/) diff --git "a/Java/alg/lc/27.\347\247\273\351\231\244\345\205\203\347\264\240.md" "b/Java/alg/lc/27.\347\247\273\351\231\244\345\205\203\347\264\240.md" index 2632e35b..9a3aa7a1 100644 --- "a/Java/alg/lc/27.\347\247\273\351\231\244\345\205\203\347\264\240.md" +++ "b/Java/alg/lc/27.\347\247\273\351\231\244\345\205\203\347\264\240.md" @@ -4,6 +4,7 @@ ## 题目 +``` 给你一个数组 `nums `和一个值 `val`,你需要 原地 移除所有数值等于 `val` 的元素,并返回移除后数组的新长度。 不要使用额外的数组空间,你必须仅使用 `O(1) `额外空间并 原地修改输入数组。 @@ -23,6 +24,9 @@ 注意这五个元素可为任意顺序。 你不需要考虑数组中超出新长度后面的元素。 +``` + + ## 方法 diff --git "a/Java/alg/lc/39.\347\273\204\345\220\210\346\200\273\345\222\214.md" "b/Java/alg/lc/39.\347\273\204\345\220\210\346\200\273\345\222\214.md" index c300cffe..fdfe760d 100644 --- "a/Java/alg/lc/39.\347\273\204\345\220\210\346\200\273\345\222\214.md" +++ "b/Java/alg/lc/39.\347\273\204\345\220\210\346\200\273\345\222\214.md" @@ -8,8 +8,6 @@ candidates 中的数字可以无限制重复被选取。 - - ## 方法 diff --git "a/Java/alg/lc/86.\345\210\206\351\232\224\351\223\276\350\241\250.md" "b/Java/alg/lc/86.\345\210\206\351\232\224\351\223\276\350\241\250.md" index 8a693814..0a705da5 100644 --- "a/Java/alg/lc/86.\345\210\206\351\232\224\351\223\276\350\241\250.md" +++ "b/Java/alg/lc/86.\345\210\206\351\232\224\351\223\276\350\241\250.md" @@ -1,6 +1,6 @@ # 86. 分隔链表 -[url](https://leetcode-cn.com/problems/partition-list/)) +[url](https://leetcode-cn.com/problems/partition-list/) ## 题目 diff --git a/Java/bishi/.DS_Store b/Java/bishi/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/Java/bishi/.DS_Store and /dev/null differ diff --git a/Java/classify/.DS_Store b/Java/classify/.DS_Store deleted file mode 100644 index 45a288f1..00000000 Binary files a/Java/classify/.DS_Store and /dev/null differ diff --git a/Java/classify/basis/Object.md b/Java/classify/basis/Object.md index fb363d4a..38fd81c1 100644 --- a/Java/classify/basis/Object.md +++ b/Java/classify/basis/Object.md @@ -10,4 +10,5 @@ public final native void notify(); // 唤醒正在该对象的监视器上等待 public final native void notifyAll(); // 唤醒正在该对象的监视器上等待的全部线程。 public final native void wait(); // 使当前线程等待,直到另一个线程调用此对象的方法或方法。 protected void finalize(); // 当垃圾回收确定不再有对对象的引用时,由对象上的垃圾回收器调用。 -``` \ No newline at end of file +``` + diff --git a/Java/codes/.DS_Store b/Java/codes/.DS_Store deleted file mode 100644 index dd7723cb..00000000 Binary files a/Java/codes/.DS_Store and /dev/null differ diff --git "a/Java/codes/basics/IO\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/basics/IO\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index b1a1d0d6..00000000 --- "a/Java/codes/basics/IO\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,30 +0,0 @@ -## IO的一些问题 - -### 键盘输入 - -> 在牛客网撕代码的时候,经常获取键盘输入 - -如: -```java -Scanner sc = new Scanner(System.in); -int a = sc.nextInt() -String s = sc.nextLine(); -``` - -### IO流 -- **按照流的流向**:可以分为**输入流**和**输出流**; -- **按照操作单元**:可以划分为**字节流**和**字符流**; -- **按照流的角色**:可以分为**节点流**和**处理流**。 - -Java IO流共涉及40多个类,这些类看上去很杂乱,哥们我也记不住,就是咱一直不用,就是记不住,那咋办捏。害,烧脑... - -- `InputSream/Reader`:所有的输入流的基类,前者是字节输入流,后者是字符输入流。 -- `OutputSream/Writer`:所有的输出流的基类,前者是字节输出流,后者是字符输出流。 - -### 一直问BIO、NIO和AIO... -> 问我都有点烦了... 但是又没办法呀... -- **BIO (Blocking I/O)**:**同步阻塞I/O模式**,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 -- **NIO (New I/O)**:NIO是一种**同步非阻塞的I/O模型**,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 `Channel` , `Selector`,`Buffer`等抽象。NIO中的N可以理解为`Non-blocking`,不单纯是New。它支持**面向缓冲**的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。 -> 有兴趣的,可以学学Netty,这哥们的源码倒是挺难的... - -- **AIO (Asynchronous I/O)**: AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是**异步非阻塞的IO模型**。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 diff --git "a/Java/codes/basics/Java\345\200\274\344\274\240\351\200\222\347\232\204\351\227\256\351\242\230.md" "b/Java/codes/basics/Java\345\200\274\344\274\240\351\200\222\347\232\204\351\227\256\351\242\230.md" deleted file mode 100644 index b5079b3a..00000000 --- "a/Java/codes/basics/Java\345\200\274\344\274\240\351\200\222\347\232\204\351\227\256\351\242\230.md" +++ /dev/null @@ -1,93 +0,0 @@ -## Java值传递的问题 - -> 按值调用(call by value)表示方法接收的是**调用者提供的值**,而按引用调用(call by reference)表示方法接收的是**调用者提供的变量地址**。一个方法可以修改**传递引用所对应的变量值**,而**不能修改传递值调用所对应的变量值**。 - -### 基本类型传递 - -```java -public static void main(String[] args) { - int num1 = 10; - int num2 = 20; - - swap(num1, num2); // 交换 - - System.out.println("num1 = " + num1); // 10 - System.out.println("num2 = " + num2); // 20 -} - -public static void swap(int a, int b) { - int temp = a; - a = b; - b = temp; - - System.out.println("a = " + a); // 20 - System.out.println("b = " + b); // 10 -} -// a = 20 -// b = 10 -// num1 = 10 -// num2 = 20 - -``` -整个图就更理解了嘛: -![](http://media.dreamcat.ink/uPic/%E5%9F%BA%E6%9C%AC%E7%B1%BB%E5%9E%8B%E4%BC%A0%E9%80%92%20.png) -> 在 swap 方法中,**a、b 的值进行交换,并不会影响到 num1、num2**。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,**a、b 相当于 num1、num2 的副本**,副本的内容无论怎么修改,都不会影响到原件本身。 - -### 数组 -```java - public static void main(String[] args) { - int[] arr = { 1, 2, 3, 4, 5 }; - System.out.println(arr[0]); // 1 - change(arr); - System.out.println(arr[0]); // 0 - // 得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。 - } - - private static void change(int[] array) { - // 修改数组中的一个元素 - array[0] = 0; - } -// 1 -// 0 -``` -当然也可以用图助你一臂之力: -![](http://media.dreamcat.ink/uPic/%E6%95%B0%E7%BB%84%E7%B1%BB%E5%9E%8B%E4%BC%A0%E9%80%92.png) -> array 被初始化 arr 的拷贝也就是**一个对象的引用**,也就是说 array 和 arr 指向的是**同一个数组对象**。 因此,外部对引用对象的改变会反映到所对应的对象上。 - -通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,**方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象**。 - -### 再看 -```java - public static void main(String[] args) { - // 有些程序员认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。 - Student s1 = new Student("Mai"); - Student s2 = new Student("Feng"); - swap2(s1, s2); - System.out.println("s1:" + s1.getName()); // Mai - System.out.println("s2:" + s2.getName()); // Feng - // 方法并没有改变存储在变量 s1 和 s2 中的对象引用。 - // swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝 - } - - private static void swap2(Student x, Student y) { - Student temp = x; - x = y; - y = temp; - System.out.println("x:" + x.getName()); // Feng - System.out.println("y:" + y.getName()); // Mai - } -// x:Feng -// y:Mai -// s1:Mai -// s2:Feng -``` -请看图: -![](http://media.dreamcat.ink/uPic/%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%E4%BC%A0%E9%80%92.png) - -> 方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝 - -### 总结 -- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。**第一个例子** -- 一个方法可以改变一个对象参数的状态。**第二个例子** -- 一个方法不能让对象参数引用一个新的对象。**第三个例子** - diff --git "a/Java/codes/basics/String\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/basics/String\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index 03b6ec62..00000000 --- "a/Java/codes/basics/String\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,134 +0,0 @@ -## String的一些问题 - -### String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的? - -先简单聊一下它们仨 - -#### 可变性 -> 简单的来说:`String` 类中使用 `final` 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 `String` 对象是不可变的。 -> `StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串char[]value 但是没有用 `final` 关键字修饰,所以这两种对象都是可变的。 - -#### 线程安全 -> `String` 中的对象是不可变的,也就可以理解为常量,线程安全。 -> `AbstractStringBuilder` 是 `StringBuilder` 与 `StringBuffer` 的公共父类,定义了一些字符串的基本操作,如 `expandCapacity`、`append`、`insert`、`indexOf` 等公共方法。`StringBuffer` 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。`StringBuilder` 并没有对方法进行加同步锁,所以是非线程安全的。 - - -#### 性能 -> 每次对 `String` 类型进行改变的时候,都会生成一个新的 `String` 对象,然后将指针指向新的 `String` 对象。 -> `StringBuffer` 每次都会对 `StringBuffer` 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 - -#### 总结一波 -- 操作少量的数据: 适用String -- 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder -- 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer - -#### 举个例子 -```java -public static void main(String[] args) { - // String - String str = "hello"; - long start = System.currentTimeMillis(); - for (int i = 0; i < 100000; i++) { - str += i; // 创建多少个对象,, - } - System.out.println("String: " + (System.currentTimeMillis() - start)); - - // StringBuffer - StringBuffer sb = new StringBuffer("hello"); - long start1 = System.currentTimeMillis(); - for (int i = 0; i < 1000000; i++) { - sb.append(i); - } - System.out.println("StringBuffer: " + (System.currentTimeMillis() - start1)); - - - // StringBuilder - StringBuilder stringBuilder = new StringBuilder("hello"); - long start2 = System.currentTimeMillis(); - for (int i = 0; i < 1000000; i++) { - stringBuilder.append(i); - } - System.out.println("StringBuilder: " + (System.currentTimeMillis() - start2)); -} -``` - -### String A = "123"; String B = new String("123");生成几个对象? -> 如果常量池中,原来没有“123”那么就是生成了2个对象,如果常量池中有“123”那么只要1个对象生成 - -这个问题,简单。 但是接下来这个可以看看`String.intern()`方法 - -举个例子: -```java -public class StringTest { - public static void main(String[] args) { - String str1 = "todo"; // 常量池 - String str2 = "todo"; // 从常量池找了str1 - String str3 = "to"; // 常量池 - String str4 = "do"; // 常量池 - String str5 = str3 + str4; // 内部用StringBuilder拼接了一波。 因此, 并非常量池 - String str6 = new String(str1); // 创建对象了, 那还能是常量池的引用? - - System.out.println("------普通String测试结果------"); - System.out.print("str1 == str2 ? "); - System.out.println( str1 == str2); // true - System.out.print("str1 == str5 ? "); - System.out.println(str1 == str5); // false - System.out.print("str1 == str6 ? "); - System.out.print(str1 == str6); // false - System.out.println(); - - System.out.println("---------intern测试结果---------"); // intern 直奔常量池上找 - System.out.print("str1.intern() == str2.intern() ? "); - System.out.println(str1.intern() == str2.intern()); // true - System.out.print("str1.intern() == str5.intern() ? "); - System.out.println(str1.intern() == str5.intern()); // true - System.out.print("str1.intern() == str6.intern() ? "); - System.out.println(str1.intern() == str6.intern()); // true - System.out.print("str1 == str6.intern() ? "); - System.out.println(str1 == str6.intern()); // true - } - -``` -结果一目了然, 但需要知道原理: -``` -------普通String测试结果------ -str1 == str2 ? true -str1 == str5 ? false -str1 == str6 ? false ----------intern测试结果--------- -str1.intern() == str2.intern() ? true -str1.intern() == str5.intern() ? true -str1.intern() == str6.intern() ? true -str1 == str6.intern() ? true -``` - -待我分析一波: -> jvm你可要知道哇。 -> Java语言会使用常量池保存那些在编译期就已确定的已编译的class文件中的一份数据。主要有类、接口、方法中的常量,以及一些以文本形式出现的符号引用,如类和接口的全限定名、字段的名称和描述符、方法和名称和描述符等。 - -- 因此在编译完Intern类后,生成的class文件中会在常量池中保存“todo”、“to”和“do”三个String常量。 -- 变量str1和str2均保存的是常量池中“todo”的引用,所以str1==str2成立; -- 在执行 str5 = str3 + str4这句时,JVM会先创建一个StringBuilder对象,通过StringBuilder.append()方法将str3与str4的值拼接,然后通过StringBuilder.toString()返回一个堆中的String对象的引用,赋值给str5,因此str1和str5指向的不是同一个String对象,str1 == str5不成立; -- String str6 = new String(str1)一句显式创建了一个新的String对象,因此str1 == str6不成立便是显而易见的事了。 - -重要的一点就是intern了: - -> String.intern()是一个Native方法,底层调用C++的 StringTable::intern方法实现。当通过语句str.intern()调用intern()方法后,JVM 就会在当前类的常量池中查找是否存在与str等值的String, - -- 若存在则直接返回常量池中相应Strnig的引用;若不存在,则会在常量池中创建一个等值的String,然后返回这个String在常量池中的引用。 -- 因此,只要是等值的String对象,使用intern()方法返回的都是常量池中同一个String引用,所以,这些等值的String对象通过intern()后使用==是可以匹配的。 -- 由此就可以理解上面代码中------intern------部分的结果了。因为str1、str5和str6是三个等值的String,所以通过intern()方法,**他们均会指向常量池中的同一个String引用**,因此str1.intern() == str5.intern() == str6.intern()均为true。 - -额外补充一哈: - -#### JDK6 -> Jdk6中常量池位于PermGen(永久代)中,PermGen是一块主要用于存放已加载的类信息和字符串池的大小固定的区域。执行intern()方法时,**若常量池中不存在等值的字符串,JVM就会在常量池中创建一个等值的字符串,然后返回该字符串的引用**。除此以外,JVM 会自动在常量池中保存一份之前已使用过的字符串集合。Jdk6中使用intern()方法的主要问题就在于常量池被保存在PermGen中: -> 首先,PermGen是一块大小固定的区域,一般不同的平台PermGen的默认大小也不相同,大致在32M到96M之间。所以不能对不受控制的运行时字符串(如用户输入信息等)使用intern()方法,否则很有可能会引发PermGen内存溢出;其次String对象保存在Java堆区,Java堆区与PermGen是物理隔离的,因此如果对多个不等值的字符串对象执行intern操作,则会导致内存中存在许多重复的字符串,会造成性能损失。 - -#### JDK7 -> Jdk7将常量池从PermGen区移到了Java堆区,执行intern操作时,如果常量池已经存在该字符串,则直接返回字符串引用,否则**复制该字符串对象的引用到常量池中并返回**。堆区的大小一般不受限,所以将常量池从PremGen区移到堆区使得常量池的使用不再受限于固定大小。 -> 除此之外,位于堆区的常量池中的对象可以被垃圾回收。当常量池中的字符串不再存在指向它的引用时,JVM就会回收该字符串。可以使用 -XX:StringTableSize 虚拟机参数设置字符串池的map大小。字符串池内部实现为一个HashMap,所以当能够确定程序中需要intern的字符串数目时,可以将该map的size设置为所需数目*2(减少hash冲突),这样就可以使得String.intern()每次都只需要常量时间和相当小的内存就能够将一个String存入字符串池中。 - - -#### 适应场景 -Jdk6中常量池位于PermGen区,大小受限,所以不建议适用intern()方法,当需要字符串池时,需要自己使用HashMap实现。Jdk7、8中,常量池由PermGen区移到了堆区,还可以通过-XX:StringTableSize参数设置StringTable的大小,常量池的使用不再受限,由此可以重新考虑使用intern()方法。intern()方法优点:执行速度非常快,直接使用==进行比较要比使用equals()方法快很多;内存占用少。虽然intern()方法的优点看上去很诱人,但若不是在恰当的场合中使用该方法的话,便非但不能获得如此好处,反而还可能会有性能损失。 diff --git "a/Java/codes/basics/equals\345\222\214hashcode.md" "b/Java/codes/basics/equals\345\222\214hashcode.md" deleted file mode 100644 index aee59082..00000000 --- "a/Java/codes/basics/equals\345\222\214hashcode.md" +++ /dev/null @@ -1,115 +0,0 @@ -## equals和hashcode - -### == -它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象: -- 基本数据类型==比较的是值 -- 引用数据类型==比较的是内存地址 - -### equals -它的作用也是判断两个对象是否相等。但它一般有两种使用情况: -- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过`==`比较这两个对象。 -- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 - -```java -public class test1 { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` - -注意:这里String类重写了equals方法。 -- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - -```java -public boolean equals(Object anObject) { - if (this == anObject) { // 比较两个对象的地址 - return true; - } - if (anObject instanceof String) { // 地址不相同,开始比较内容 - String anotherString = (String)anObject; - int n = value.length; - if (n == anotherString.value.length) { - char v1[] = value; - char v2[] = anotherString.value; - int i = 0; - while (n-- != 0) { - if (v1[i] != v2[i]) - return false; - i++; - } - return true; - } - } - return false; - } -``` - -### hashcode -> hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。 -> 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) - -#### 比如 -> 我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode: 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置 -> 这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - -```java -public class Demo { - public static void main(String[] args) { - HashSet set = new HashSet<>(); - set.add("1"); - set.add("2"); - set.add("1"); - System.out.println(set.toString()); - } -} -``` -可以看一下HashSet源码中的add,实际上是HashMap的put方法。 - -```java -public boolean add(E e) { - return map.put(e, PRESENT)==null; -} -``` - -接着看HashMap的put方法 -```java -public V put(K key, V value) { - return putVal(hash(key), key, value, false, true); -} -``` -其中,hash的这个方法的源码,你可看 -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` -也就是你传入这个键的对象的hashcode,本例是String,可以看:也是重写了hashcode的。所以,像使用集合,判断对象是否相等,还是务必重写hashcode和equals。put具体细节源码就不贴了 -```java -public int hashCode() { - int h = hash; - if (h == 0 && value.length > 0) { - char val[] = value; - - for (int i = 0; i < value.length; i++) { - h = 31 * h + val[i]; - } - hash = h; - } - return h; -} -``` \ No newline at end of file diff --git "a/Java/codes/basics/\345\217\215\345\260\204\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/basics/\345\217\215\345\260\204\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index ed29af72..00000000 --- "a/Java/codes/basics/\345\217\215\345\260\204\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,208 +0,0 @@ -# 反射 -> Java反射机制是在**运行状态**中,对于任意一个类,都能够知道**这个类的所有属性和方法**;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动**态获取的信息以及动态调用对象的方法的功能**成为java语言的反射机制。 - -## 静态编译和动态编译 -- 静态编译:在编译时确定类型,绑定对象 -- 动态编译:运行时确定类型,绑定对象 - -## 反射机制优缺点 -- 优点:运行期间类型的判断,动态加载类,提高代码的灵活度 -- 缺点:性能瓶颈:反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的java代码要慢很多 - -## 反射的应用场景 -> 在我们平时的项目开发过程中,基本上很少会直接使用的反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如**模块化的开发**,通过反射去调用对应的字节码;**动态代理设计模型也采用了反射机制**,还有我们日常使用的`Spring` / `Hibernate`等框架也大量使用到了反射机制。 - -## 反射得到的Class对象的三种方式 -首先是Student类 -```java -public class Student { - - private String name; - - private Integer age; - - public String gender; - - // 无参数的构造方法 - public Student() { - System.out.println("调用了公有、无参构造方法执行了。。。"); - } - - // 默认的构造方法 - public Student(String str) { - System.out.println("(默认)的构造方法 s = " + str); - } - - // 有一个参数的构造方法 - public Student(char name) { - System.out.println("姓名:" + name); - } - - // 有多个参数的构造方法 - public Student(String name, Integer age) { - this.name = name; - this.age = age; - } - - //受保护的构造方法 - protected Student(boolean n){ - System.out.println("受保护的构造方法 n = " + n); - } - - //私有构造方法 - private Student(int age){ - System.out.println("私有的构造方法 年龄:"+ age); - } - - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Integer getAge() { - return age; - } - - public void setAge(Integer age) { - this.age = age; - } - - public String getGender() { - return gender; - } - - public void setGender(String gender) { - this.gender = gender; - } - - //**************成员方法***************// - public void show1(String s){ - System.out.println("调用了:公有的,String参数的show1(): s = " + s); - } - protected void show2(){ - System.out.println("调用了:受保护的,无参的show2()"); - } - void show3(){ - System.out.println("调用了:默认的,无参的show3()"); - } - private String show4(int age){ - System.out.println("调用了,私有的,并且有返回值的,int参数的show4(): age = " + age); - return "abcd"; - } - - @Override - public String toString() { - return "Student{" + - "name='" + name + '\'' + - ", age=" + age + - ", gender='" + gender + '\'' + - '}'; - } -} -``` -测试: -```java -public class ReflectDemo { - public static void main(String[] args) throws ClassNotFoundException { - // 第一种方式获取Class对象 - Student student = new Student(); // 这一new 产生一个Student对象,一个Class对象。 - Class studentClass = student.getClass(); - System.out.println(studentClass.getName()); // com.reflect.Student - - // 第二种方式获取Class对象 - Class studentClass2 = Student.class; - System.out.println(studentClass == studentClass2); //判断第一种方式获取的Class对象和第二种方式获取的是否是同一个 - - //第三种方式获取Class对象 - Class studentClass3 = Class.forName("com.reflect.Student"); //注意此字符串必须是真实路径,就是带包名的类路径,包名.类名 - System.out.println(studentClass3 == studentClass2); // //判断三种方式是否获取的是同一个Class对象 - - // 三种方式常用第三种,第一种对象都有了还要反射干什么。 - // 第二种需要导入类的包,依赖太强,不导包就抛编译错误。 - // 一般都第三种,一个字符串可以传入也可写在配置文件中等多种方法。 - } -} -``` -## 反射访问并调用构造方法 -```java -public class ConstructorsDemo { - public static void main(String[] args) throws Exception { - // 1. 加载Class对象 - Class clazz = Class.forName("com.reflect.Student"); - - // 2. 获取所有公有构造方法 - System.out.println("**********************所有公有构造方法*********************************"); - Constructor[] constructors = clazz.getConstructors(); - for (Constructor constructor : constructors) { - System.out.println(constructor); - } - - // 3. - System.out.println("************所有的构造方法(包括:私有、受保护、默认、公有)***************"); - Constructor[] declaredConstructors = clazz.getDeclaredConstructors(); - for (Constructor declaredConstructor : declaredConstructors) { - System.out.println(declaredConstructor); - } - - // 4. - System.out.println("*****************获取公有、无参的构造方法*******************************"); - Constructor constructor = clazz.getConstructor(); - System.out.println(constructor); - - // 调用构造方法 - Object object = constructor.newInstance(); - System.out.println(object); - - // - System.out.println("******************获取私有构造方法,并调用*******************************"); - Constructor constructor1 = clazz.getDeclaredConstructor(char.class); - System.out.println(constructor1); - // 调用构造方法 - constructor1.setAccessible(true); // 暴力访问 - Object object2 = constructor1.newInstance('买'); - System.out.println(object2); - } -} -``` - -## 反射访问并调用成员变量 -```java -public class FieldDemo { - public static void main(String[] args) throws Exception { - // 1. 获取class对象 - Class clazz = Class.forName("com.reflect.Student"); - // 2. 获取所有字段 - System.out.println("************获取所有公有的字段********************"); - Field[] fields = clazz.getFields(); - for (Field field : fields) { - System.out.println(field); - } - // - System.out.println("************获取所有的字段(包括私有、受保护、默认的)********************"); - Field[] fields1 = clazz.getDeclaredFields(); - for (Field field : fields1) { - System.out.println(field); - } - // - System.out.println("*************获取公有字段**并调用***********************************"); - Field gender = clazz.getField("gender"); - System.out.println(gender); - // 获取一个对象 - Object o = clazz.getConstructor().newInstance(); - gender.set(o, "男"); - Student stu = (Student) o; - System.out.println("验证性别:" + stu.getGender()); - // - System.out.println("*************获取公有字段**并调用***********************************"); - Field name = clazz.getDeclaredField("name"); - System.out.println(name); - name.setAccessible(true); //暴力反射,解除私有限定 - name.set(o, "买"); - System.out.println("验证姓名:" + stu); - } -} -``` diff --git "a/Java/codes/basics/\345\270\270\350\247\201\345\205\263\351\224\256\345\255\227\351\227\256\351\242\230.md" "b/Java/codes/basics/\345\270\270\350\247\201\345\205\263\351\224\256\345\255\227\351\227\256\351\242\230.md" deleted file mode 100644 index 7e061e4b..00000000 --- "a/Java/codes/basics/\345\270\270\350\247\201\345\205\263\351\224\256\345\255\227\351\227\256\351\242\230.md" +++ /dev/null @@ -1,18 +0,0 @@ -## 常见的关键字问题 -### final - -> final关键字主要用在三个地方:变量、方法、类。 - -1. **对于一个final变量**,如果是基本数据类型的变量,则其**数值一旦在初始化之后便不能更改**;如果是引用类型的变量,则在对其**初始化之后便不能再让其指向另一个对象**。 -2. **使用final方法的原因**有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。 -3. **当用final修饰一个类时**,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。 - -好处: -1. final的关键字提高了性能,JVM和java应用会缓存final变量; -2. final变量可以在多线程环境下保持线程安全; -3. 使用final的关键字提高了性能,JVM会对方法变量类进行优化; - -### static -1. **修饰成员变量和成员方法**: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:类名.静态变量名 类名.静态方法名() -2. **静态代码块**: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块**只执行一次**. -3. **静态内部类**(static修饰类的话只能修饰内部类):静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。 diff --git "a/Java/codes/basics/\345\274\202\345\270\270\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/basics/\345\274\202\345\270\270\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index 7f0d1583..00000000 --- "a/Java/codes/basics/\345\274\202\345\270\270\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,67 +0,0 @@ -# 异常的一些问题 -> 在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 Throwable类。 -- **Exception(异常)** -- **Error(错误)** - -## 思维导图 -![Throwable](http://media.dreamcat.ink/uPic/%E5%BC%82%E5%B8%B8%E5%88%86%E7%B1%BB.png) - -## Error -**是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。比如:`OutOfMemoryError` - -## Exception -**是程序本身可以处理的异常。** 比如: - -- `RuntimeException` 异常由Java虚拟机抛出。 -- `NullPointerException`(要访问的变量没有引用任何对象时,抛出该异常) -- `ArithmeticException`(算术运算异常,一个整数除以0时,抛出该异常) -- `ArrayIndexOutOfBoundsException` (下标越界异常)。 - -## 处理 - -- `try` 块: 用于捕获异常。其后可接零个或多个`catch`块,如果没有`catch`块,则必须跟一个`finally`块。 -- `catch` 块: 用于处理`try`捕获到的异常。 -- `finally` 块: 无论是否捕获或处理异常,`finally`块里的语句都会被执行。当在`try`块或`catch`块中遇到`return`语句时,`finally`语句块将在方法返回之前被执行。 - -> 注意:当try语句和finally语句中都有return语句时,在方法返回之前,finally语句的内容将被执行,并且finally语句的返回值将会**覆盖原始的返回值**。如下: - -```java - public static int f(int value) { - try { - return value * value; - } finally { - if (value == 2) { - return 0; - } - } - } -``` - -## throw和throws -> 抛出异常 - -throw的例子: - -```java -public class Test1 { - public static void main(String[] args) { - if(true) { - throw new NumberFormatException(); // 抛出一个异常 - } else { - ... - } - } -} -``` - -throws是方法可能抛出异常的声明。(用在声明方法时,表示该方法可能要抛出异常),throws例子: - -```java -public class Test2 { - public void fun() throws NumberFormatException { - if(true) { - throw new NumberFormatException(); - } - } -} -``` \ No newline at end of file diff --git "a/Java/codes/basics/\346\265\205\346\213\267\350\264\235\345\222\214\346\267\261\346\213\267\350\264\235\344\276\213\345\255\220.md" "b/Java/codes/basics/\346\265\205\346\213\267\350\264\235\345\222\214\346\267\261\346\213\267\350\264\235\344\276\213\345\255\220.md" deleted file mode 100644 index 1f4f2078..00000000 --- "a/Java/codes/basics/\346\265\205\346\213\267\350\264\235\345\222\214\346\267\261\346\213\267\350\264\235\344\276\213\345\255\220.md" +++ /dev/null @@ -1,145 +0,0 @@ -## 深拷贝和浅拷贝 - -> 1. **浅拷贝**:对**基本数据类型进行值传递**,对**引用数据类型进行引用传递般的拷贝**,此为浅拷贝。 -> 2. **深拷贝**:对**基本数据类型进行值传递**,对**引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝** -> 3. 也就二者对引用数据类型有区别 - -### 举例子 -1. 首先看浅拷贝: - -Subject类: -```java -public class Subject { - - private String name; - - public Subject(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public String toString() { - return "Subject{" + - "name='" + name + '\'' + - '}'; - } -} -``` - -Student类: -```java -public class Student implements Cloneable{ - - // 对象的引用 - private Subject subject; - - private String name; - - public Student(Subject s, String name) { - this.subject = s; - this.name = name; - } - - public Subject getSubject() { - return subject; - } - - public String getName() { - return name; - } - - public void setSubject(Subject subject) { - this.subject = subject; - } - public void setName(String name) { - this.name = name; - } - - @Override - public String toString() { - return "Student{" + - "subject=" + subject + - ", name='" + name + '\'' + - '}'; - } - - @Override - protected Object clone() throws CloneNotSupportedException { - // 浅拷贝 -// return super.clone(); - } -} -``` -测试: -```java -public class ShallowCopyDemo { - public static void main(String[] args) throws CloneNotSupportedException { - // 原始对象 - Student student = new Student(new Subject("code"), "dream"); - System.out.println("原始对象: " + student.getName() + " - " + student.getSubject().getName()); // dream-code - - // 拷贝对象 - Student cloneStu = (Student) student.clone(); - System.out.println("拷贝对象: " + cloneStu.getName() + " - " + cloneStu.getSubject().getName()); // dream-code - - // 原始对象和拷贝对象是否一样: - System.out.println("原始对象和拷贝对象是否一样: " + (student == cloneStu)); // false - - // 原始对象和拷贝对象的name属性是否一样 - System.out.println("原始对象和拷贝对象的name属性是否一样: " + (student.getName() == cloneStu.getName())); // true - - // 原始对象和拷贝对象的subj属性是否一样 - System.out.println("原始对象和拷贝对象的subj属性是否一样: " + (student.getSubject() == cloneStu.getSubject())); // true - - student.setName("cat"); - student.getSubject().setName("eat"); - System.out.println("更新后的原始对象: " + student.getName() + " - " + student.getSubject().getName()); // cat-eat - System.out.println("更新原始对象后的克隆对象: " + cloneStu.getName() + " - " + cloneStu.getSubject().getName()); // dream-eat - // 在这个例子中,让要拷贝的类Student实现了Clonable接口并重写Object类的clone()方法,然后在方法内部调用super.clone()方法。 - // 从输出结果中我们可以看到,对原始对象stud的"name"属性所做的改变并没有影响到拷贝对象clonedStud; - // 但是对引用对象subj的"name"属性所做的改变影响到了拷贝对象clonedStud。 - } -} -``` -运行结果: -```shell -原始对象: dream - code -拷贝对象: dream - code -原始对象和拷贝对象是否一样: false -原始对象和拷贝对象的name属性是否一样: true -原始对象和拷贝对象的subj属性是否一样: true -更新后的原始对象: cat - eat -更新原始对象后的克隆对象: dream - eat -``` - -2. 深拷贝 - -只需要重写Student的clone方法 -```java - @Override - protected Object clone() throws CloneNotSupportedException { - Student student = new Student(new Subject(subject.getName()), name); // 新建对象,拷贝内容 - return student; - // 因为它是深拷贝,所以你需要创建拷贝类的一个对象。 - // 因为在Student类中有对象引用,所以需要在Student类中实现Cloneable接口并且重写clone方法。 - } -``` -测试结果: -```shell -原始对象: dream - code -拷贝对象: dream - code -原始对象和拷贝对象是否一样: false -原始对象和拷贝对象的name属性是否一样: true -原始对象和拷贝对象的subj属性是否一样: false -更新后的原始对象: cat - eat -更新原始对象后的克隆对象: dream - code -``` -晓得了吧? \ No newline at end of file diff --git "a/Java/codes/basics/\347\261\273\345\236\213\350\275\254\346\215\242\347\232\204\351\227\256\351\242\230.md" "b/Java/codes/basics/\347\261\273\345\236\213\350\275\254\346\215\242\347\232\204\351\227\256\351\242\230.md" deleted file mode 100644 index 39423d20..00000000 --- "a/Java/codes/basics/\347\261\273\345\236\213\350\275\254\346\215\242\347\232\204\351\227\256\351\242\230.md" +++ /dev/null @@ -1,94 +0,0 @@ -## 类型转换的问题 - - -### 比如 - -```java -short s1 = 1; -s1 = s1 + 1;有什么错? -short s1 = 1; -s1 += 1; 有什么错? -``` - - -- 对于 `short s1 = 1; s1 = s1 + 1;` 由于 s1+1 运算时会自动提升表达式的类型,所以结果是 int 型,再赋值给 short 类型 s1 时,编译器将报告需要强制转换类型的错误。 -- 对于 `short s1 = 1; s1 += 1;`由于 += 是 **java 语言规定的运算符**,java 编译器会对它进行特殊处理,因此 可以正确编译。 - -```java -public class TypeConvert { - public static void main(String[] args) { - // 字面量属于 double 类型 - // 不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型 - // Java 不能隐式执行向下转型,因为这会使得精度降低。 - // float f = 1.1; - float f = 1.1f; - - // 因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。 - short s1 = 1; - // s1 = s1 + 1; - - // 但是使用 += 运算符可以执行隐式类型转换。 - s1 += 1; - // 上面的语句相当于将 s1 + 1 的计算结果进行了向下转型 如下: - s1 = (short) (s1 + 1); - - } -} - -``` - -### 聊聊整形包装类的缓存池 -举个例子: -```java -public class IntegerPackDemo { - public static void main(String[] args) { - Integer x = 3; // 装箱,3自动包装为Integer - int z = x; // 拆箱 x 拆箱为基本类型 - Integer y = 3; - System.out.println(x == y); // true 基本类型比较可用== - // ------------------------- - Integer a = new Integer(3); - Integer b = new Integer(3); - System.out.println(a == b); // false 老生常谈了,就不说为什么了 - System.out.println(a.equals.(b)); // true // 这里是用重写了equals方法,比较的是值,而不是对象的地址 - // ------------------------ - // 缓存池 - Integer aa = Integer.valueOf(123); - Integer bb = Integer.valueOf(123); - System.out.println(aa == bb); // true - /** - * valueOf的源码 - * public static Integer valueOf(int i) { - * // 判断是否在Integer的范围内 - * if (i >= IntegerCache.low && i <= IntegerCache.high) - * return IntegerCache.cache[i + (-IntegerCache.low)]; - * return new Integer(i); - * } - */ - } -} - -``` -个人还是喜欢贴Integer的equals源码: -```java -public boolean equals(Object obj) { - if (obj instanceof Integer) { - return value == ((Integer)obj).intValue(); // 这里比的就是值了 - } - return false; -} -``` -继续看valueOf方法的源码: - -```java -// 源码描述: -// This method will always cache values in the range -128 to 127, -public static Integer valueOf(int i) { - if (i >= IntegerCache.low && i <= IntegerCache.high) // 条件 - return IntegerCache.cache[i + (-IntegerCache.low)];// 取缓存, - // Integeer的源码中: - // static final int low = -128; IntegerCache.low = -128 - return new Integer(i); -} -``` -> 当使用自动装箱方式创建一个Integer对象时,当数值在-128 ~127时,会将创建的 Integer 对象缓存起来,当下次再出现该数值时,直接从缓存中取出对应的Integer对象。所以上述代码中,x和y引用的是相同的Integer对象。 diff --git "a/Java/codes/basics/\351\235\242\345\220\221\345\257\271\350\261\241\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/basics/\351\235\242\345\220\221\345\257\271\350\261\241\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index a42fb42c..00000000 --- "a/Java/codes/basics/\351\235\242\345\220\221\345\257\271\350\261\241\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,141 +0,0 @@ -## 三大特性 -### 封装 -> 封装把一个对象的**属性私有化**,同时提供一些可以**被外界访问的属性的方法**,**如果属性不想被外界访问,我们大可不必提供方法给外界访问**。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 - -代码举个例子也妥: -```java -class Person{ - private String name; // 属性私有,并不想暴漏 - private Integer age; // 属性私有 - - // 给外界一个访问方法,给name赋值 - public void setName(String name) { - this.name = name; - } - - // 给外界一个访问方法,获取name的值 - public String getName() { - return this.name; - } -} -``` - -### 继承 -> 继承是使用**已存在的类**的定义作为基础建立新类的技术,新类的定义可以增加**新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类**。通过使用继承我们能够非常方便地复用以前的代码。 - -代码举个例子撒: -```java -import java.util.*; - -public class Person { - private String name; - - private Integer age; - - public void setName(String name) { - this.name = name; - } - - public void setAge(Integer age) { - this.age = age; - } - - public String getName() { - return this.name; - } - - public Integer getAge() { - return this.age; - } - - public static void main(String[] args) { - Student s = new Student(); - s.setName("Dream"); - s.setAge(18); - s.setSchool("家里蹲"); - s.say(); - } -} - -class Student extends Person{ // 集成父类 - private String school; - - public void setSchool(String school) { - this.school = school; - } - - public String getSchool() { - return this.school; - } - - public void say(){ - System.out.println(this.getName() + " say: 我tm" + this.getAge() + "岁,不服?不服就来" + this.getSchool()); - } -} - -``` - -注意: -- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。 -- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 -- 子类可以用自己的方式实现父类的方法。(以后介绍)。 - -### 多态 -> 所谓多态就是指程序中定义的**引用变量**所指向的**具体类型**和通过**该引用变量发出的方法调用**在编程时并不确定,而是在**程序运行期间才确定**,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 -> 说白了,就是任何事物的多个姿态,多个形态。比如,你说一个猫在吃东西,同样的,你也能说一个动物在吃东西。 - -一般两种方式: -- 继承(多个子类对同一方法的重写) -- 接口(实现接口并覆盖接口中同一方法) - -举个例子吧: -定义一个动物类 -```java -class Animal { - public void eat() { - System.out.println("动物在吃东西..."); - } -} -``` - -定义一个猫 -```java -class Cat extends Animal{ - @Override - public void eat() { - System.out.println("猫在吃东西..."); - } -} -``` -测试 -```java -public class Test { - public static void main(String[] args){ - Animal animal = new Cat(); - animal.eat() // 猫也会吃饭 - // 你看到了一只猫,同样它也是动物 - // 比如有很多其他种类继承了动物哈, - // 当编译期间的animal引用变量,到底指的哪个实例对象, - // 或者该引用调用的eat方法,到底是哪个实例对象的eat,编译期间恐怕不知道哦 - // 只有运行期间,哦哦, 原来是猫的eat方法哇... - } -} -``` - -### 重载和重写的区别 - -- 重载 -> 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 -> 例子的话,你看源码的时候,那里面的方法,你晓得伐? - -- 重写 -> 重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。也就是说方法提供的行为改变,而方法的外貌并没有改变。 -> 当你看源码出现继承的时候, 你晓得伐? - -### 接口和抽象类的区别是什么? -1. 方法:接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。 -2. 变量:接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。 -3. 实现:一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过implement关键字扩展多个接口。 -4. 修饰符:接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。 - - diff --git "a/Java/codes/bus/Redis\347\232\204key\350\277\207\346\234\237\344\272\213\344\273\266.md" "b/Java/codes/bus/Redis\347\232\204key\350\277\207\346\234\237\344\272\213\344\273\266.md" deleted file mode 100644 index 4e2167ac..00000000 --- "a/Java/codes/bus/Redis\347\232\204key\350\277\207\346\234\237\344\272\213\344\273\266.md" +++ /dev/null @@ -1,68 +0,0 @@ -# 订单自动取消 - -> 如果系统的并发量不是特别高的,或者日访问量不是特别高的话,可以使用Redis的key过期事件,如果访问量过高,还是采用延迟队列吧。 - -## Redis的配置 -``` -# -# notify-keyspace-events Ex -# -# By default all notifications are disabled because most users don't need -# this feature and the feature has some overhead. Note that if you don't -# specify at least one of K or E, no events will be delivered. -notify-keyspace-events "Ex" -``` - -## RedisListenerConfig配置 -```java -@Configuration -public class RedisListenerConfig { - @Bean - RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) { - RedisMessageListenerContainer container = new RedisMessageListenerContainer(); - container.setConnectionFactory(connectionFactory); - return container; - } -} -``` - -## RedisKeyExpirationListener监听事件 - -```java -@Component -@Slf4j -public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener { - public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) { - super(listenerContainer); - } - - @Autowired - private IOrderService orderService; - - /** - * 监听key过期事件 - * @param message - * @param pattern - */ - @Override - public void onMessage(Message message, byte[] pattern) { - super.onMessage(message, pattern); - String expiredKey = message.toString(); - log.info("redis key过期:{}",expiredKey); - String orderCancleKey = RedisConstants.ORDER_CANCLE_EXPIRE.getKey(); - if (expiredKey.startsWith(orderCancleKey)) { - // 获取订单id - String[] strings = expiredKey.split(orderCancleKey); - String orderId = strings[1]; - log.warn("过期订单ID:" + orderId); - // 得到过期订单。 - // 1。 更改订单状态就完事了 - OrderRequest request = new OrderRequest(); - request.setOrderStatus("2"); // 关闭订单 - request.setUuid(Convert.toLong(orderId)); - orderService.updateOrderStatus(request); - log.warn("过期订单已处理:" + orderId); - } - } -} -``` \ No newline at end of file diff --git "a/Java/codes/bus/Redis\347\273\221\345\256\232Token.md" "b/Java/codes/bus/Redis\347\273\221\345\256\232Token.md" deleted file mode 100644 index 550fbd30..00000000 --- "a/Java/codes/bus/Redis\347\273\221\345\256\232Token.md" +++ /dev/null @@ -1,201 +0,0 @@ -# Redis绑定Token -> 先介绍一下概念 - -## cookie -HTTP 协议是**无状态的**,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 - -Cookie 是服务器发送到用户浏览器并保存在**本地的一小块数据**,它会在浏览器之后向同一服务器**再次发起请求时被携带上**,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 - -### 用途 -- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) -- 个性化设置(如用户自定义设置、主题等) -- 浏览器行为跟踪(如跟踪分析用户行为等) - -## session -除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 - -Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。 - -使用 Session 维护用户登录状态的过程如下: -1. 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; -2. 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; -3. 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; -4. 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 - -> 注意:Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 - -### session和cookie选择 -- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选 Session; -- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; -- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 - -## Token -实际上,Token是在服务端将用户信息经过Base64Url编码过后传给在客户端。每次用户请求的时候都会带上这一段信息,因此服务端拿到此信息进行解密后就知道此用户是谁了,这个方法叫做JWT(Json Web Token)。 - -## JWT -> JWT(json web token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。 - -cookie+session这种模式通常是保存在内存中,而且服务从**单服务到多服务会面临的session共享问题**,随着用户量的增多,开销就会越大。而JWT不是这样的,只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可。 - -### 构成 -第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。详情请见[官网](https://jwt.io/introduction/) - -简单说一下 - -#### header -是一个Json对象,描述JWT的元数据,通常是下面这样子的: -```json -{ - "alg": "HS256", - "typ": "JWT" -} -``` -上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。 - - -#### payload -Payload部分也是一个Json对象,用来存放实际需要传输的数据,JWT官方规定了下面几个官方的字段供选用。 - -- iss (issuer):签发人 -- exp (expiration time):过期时间 -- sub (subject):主题 -- aud (audience):受众 -- nbf (Not Before):生效时间 -- iat (Issued At):签发时间 -- jti (JWT ID):编号 - -也可以定义私有属性 - -#### signature -Signature部分是对前面的两部分的数据进行签名,防止数据篡改。 - -首先需要定义一个秘钥,这个秘钥只有服务器才知道,不能泄露给用户,然后使用Header中指定的签名算法(默认情况是HMAC SHA256),算出签名以后将Header、Payload、Signature三部分拼成一个字符串,每个部分用.分割开来,就可以返给用户了。 - -## 在项目中如何写 - -### JwtTokenUtil -这里是使用guns搭建的项目,自带的工具类,我就不详细赘述了,主要是一下我们的业务逻辑,以及如何使用。 - - -### AuthController -> sb-gateway-auth-AuthController - -能请求到AuthContoroller,说明两种情况: -1. 你没有携带token -2. 你token过期了 - -而这个业务处理,是为了生成token - -```java - @ApiOperation(value = "获取token接口", notes = "每调用一次,就会随机生成一串token", response = ResponseData.class) - @RequestMapping(value = "${jwt.auth-path}") - public ResponseData createAuthenticationToken(@Validated AuthRequest authRequest, BindingResult bindingResult) { - //Validated 的作用是抵消了使用很多if判断 - if (bindingResult.hasErrors()) { - for (FieldError fieldError : bindingResult.getFieldErrors()) { - log.error("参数:{}校验失败,原因:{}", fieldError.getField(), fieldError.getDefaultMessage()); - } - return new ResponseUtil<>().setErrorMsg("用户参数设置错误:" + CommonBindingResult.getErrors(bindingResult)); - } - // 获取用户的账号和密码 - UserLoginRequst req = new UserLoginRequst(); - req.setUsername(authRequest.getUserName()); - req.setPassword(authRequest.getPassword()); - // 调用userAPI.login业务获取用户id - UserLoginResponse res = userAPI.login(req); - String userId = "" + res.getUserId(); - if (res.getUserId() != 0) { - // 如果id不等于0,说明账户存 - // 从redis看一下userId在不在 - if (redisUtils.hasKey(userId)) { - // 如果存在,两种情况:1.你在其他登陆,2.别人登陆 - // 删除redis中的键为userId的 - redisUtils.del(userId); - } - // 以下两步,针对于userId生成token - res.setRandomKey(jwtTokenUtil.getRandomKey()); - String token = jwtTokenUtil.generateToken(userId, res.getRandomKey()); - res.setToken(token); - // 写进redis - redisUtils.set(userId, token, RedisConstants.TOKEN_EXPIRE.getTime()); - // 返回给用户 - return new ResponseUtil<>().setData(res); - } else { - // 如果id等于0,那么说明两种情况:1.账号密码错误,2.账户不存在 - return new ResponseUtil<>().setErrorMsg("账号密码错误"); - } - } -``` - -### AuthFilter -> sb-gateway->auth->filter->AuthFileter - -#### 忽略列表 -```java -// 配置忽略列表 -String ignoreUrl = jwtProperties.getIgnoreUrl(); // 获取忽略的url -String[] ignoreUrls = ignoreUrl.split(","); -for(int i=0;i().setData(response1)); - return; - } else { - // 如果存在,去取token - String token = (String) redisUtils.get(userId); - // 判断redis的token和当前的浏览器的token是否一致 - if(!token.equals(authToken)) { - // 如果不相等,说明别人已经登录了,你当前的token无效,需要重新登录,将它替换掉。 - // 比如,两个手机不能同时登录同一个账户的QQ - CommonResponse response1 = new CommonResponse(); - response1.setCode(SbCode.TOKEN_VALID_FAILED.getCode()); - response1.setMsg(SbCode.TOKEN_VALID_FAILED.getMessage()); - RenderUtil.renderJson(response, new ResponseUtil<>().setData(response1)); - return; - } - } - } catch (Exception e) { - // 如果发生异常,所携带的token有问题,防止爬虫造token - CommonResponse response1 = new CommonResponse(); - response1.setCode(SbCode.TOKEN_VALID_FAILED.getCode()); - response1.setMsg(SbCode.TOKEN_VALID_FAILED.getMessage()); - RenderUtil.renderJson(response, new ResponseUtil<>().setData(response1)); - return; - } - } else { - //header没有带Bearer字段 - CommonResponse response1 = new CommonResponse(); - response1.setCode(SbCode.TOKEN_VALID_FAILED.getCode()); - response1.setMsg(SbCode.TOKEN_VALID_FAILED.getMessage()); - RenderUtil.renderJson(response, new ResponseUtil<>().setData(response1)); - return; - } - chain.doFilter(request, response); - } -``` - -**以上流程图,日后补充** \ No newline at end of file diff --git "a/Java/codes/bus/RocketMQ\346\234\200\347\273\210\344\270\200\350\207\264\346\200\247.md" "b/Java/codes/bus/RocketMQ\346\234\200\347\273\210\344\270\200\350\207\264\346\200\247.md" deleted file mode 100644 index 60dd5547..00000000 --- "a/Java/codes/bus/RocketMQ\346\234\200\347\273\210\344\270\200\350\207\264\346\200\247.md" +++ /dev/null @@ -1,363 +0,0 @@ -# RocketMQ - -> 该项目主要在添加订单,支付和回退的时候采用了RocketMQ消息中间件。 目的是系统发生异常时,保持系统的最终一致性。 - -## MQTags -> 异常种类 -```java -public enum MqTags { - ORDER_CANCEL("order_cancel", "订单取消异常"), - ORDER_SEATS_CANCEL("order_seats_cancel", "判断座位异常"), - ORDER_ADD_SEATS_CANCLE("order_add_seats_cancle", "更新座位异常"), - ORDER_CALC_MONEY_CANCLE("order_calc_money_cancle", "计算总金额异常"), - ORDER_ADD_CANCLE("order_add_cancle", "添加订单异常"), - PAY_CANCLE("pay_cancle", "支付异常"), - PAY_CHECK_CANCLE("pay_check_cancle", "校验支付密码和余额"), - PAY_MONEY_CANCLE("pay_money_cancle", "支付余额写入异常"), - ; - private String tag; - private String message; - - - MqTags(String tag, String message) { - this.tag = tag; - this.message = message; - } - - public String getTag() { - return tag; - } - - public void setTag(String tag) { - this.tag = tag; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } -} -``` - -## 添加订单 -### addOrder -```java - /** - * 添加订单,这里比较重要 - * @param request - * @return - */ - @Override - public AddOrderResponse addOrder(AddOrderRequest request) { - // 判断座位,如果重复,直接退出,否则更新场次的座位信息 - AddOrderResponse response = new AddOrderResponse(); - // 全局orderId - Long orderId = UUIDUtils.flakesUUID(); - // 1。 判断座位,如果重复,直接退出,否则下一步 - // 2。 更新座位,如果没有异常,这是写操作 - // 3。 计算总金额,如果没有异常 - // 4。 添加订单,如果异常,这是写操作 - try { - // 1。 判断座位,如果重复,直接退出,否则下一步 - tag = MqTags.ORDER_SEATS_CANCEL.getTag(); - boolean repeatSeats = busService.repeatSeats(request.getSeatsIds(), request.getCountId()); - if (repeatSeats) { - // b:true 说明重复 - response.setCode(SbCode.SELECTED_SEATS.getCode()); - response.setMsg(SbCode.SELECTED_SEATS.getMessage()); - return response; - } -// CastException.cast(SbCode.SYSTEM_ERROR); - // 2。 更新座位,如果没有异常,这是写操作 - // 用tags来过滤消息 - tag = MqTags.ORDER_ADD_SEATS_CANCLE.getTag(); - boolean addSeats = busService.addSeats(request.getSeatsIds(), request.getCountId()); - if (!addSeats) { - response.setCode(SbCode.DB_EXCEPTION.getCode()); - response.setMsg(SbCode.DB_EXCEPTION.getMessage()); - return response; - } - // 模拟系统异常 -// CastException.cast(SbCode.SYSTEM_ERROR); - // 3。 计算总金额,如果没有异常 - tag = MqTags.ORDER_CALC_MONEY_CANCLE.getTag(); - String seatIds = request.getSeatsIds(); - Integer seatNumber = seatIds.split(",").length; - Double countPrice = request.getCountPrice(); - Double totalPrice = getTotalPrice(seatNumber, countPrice); - -// CastException.cast(SbCode.SYSTEM_ERROR); - // 4。 添加订单,如果异常,这是写操作 - Order order = orderConvertver.res2Order(request); - order.setOrderPrice(totalPrice); - order.setEvaluateStatus("0"); // 未评价 - order.setOrderStatus("0"); // 未支付 - order.setUuid(orderId); // 唯一id - tag = MqTags.ORDER_ADD_CANCLE.getTag(); - int insert = orderMapper.insert(order);// 插入 不判断了 -// CastException.cast(SbCode.SYSTEM_ERROR); - // 这里就不读了,耗时 -// QueryWrapper wrapper = new QueryWrapper<>(); -// wrapper.eq("so.uuid", order.getUuid()); -// OrderDto orderDto = orderMapper.selectOrderById(wrapper); - response.setCode(SbCode.SUCCESS.getCode()); - response.setMsg(SbCode.SUCCESS.getMessage()); - response.setOrderId(orderId); -// response.setOrderDto(orderDto); - // 这里放redis 未支付缓存,时间前端给定 - redisUtils.set(RedisConstants.ORDER_CANCLE_EXPIRE.getKey() + orderId, orderId, request.getExpireTime()); - return response; - } catch (Exception e) { - // 以上操作如果程序都不发生异常的话, 是不会执行这里的代码的 - // 也就是说不会发送回退消息的。 - // 目的是在高并发的情况下,程序内部发生异常,依然高可用 -// e.printStackTrace(); - log.error("订单业务发生异常"); - // 发消息,将座位退回,将订单退回 - MQDto mqDto = new MQDto(); - mqDto.setOrderId(orderId); - mqDto.setCountId(request.getCountId()); - mqDto.setSeatsIds(request.getSeatsIds()); - try { - String key = RedisConstants.ORDER_EXCEPTION_CANCLE_EXPIRE.getKey() + Convert.toStr(orderId); - sendCancelOrder(topic,tag, key, JSON.toJSONString(mqDto)); - log.warn("订单回退消息发送成功..." + mqDto); - } catch (Exception ex) { - ex.printStackTrace(); - } - response.setCode(SbCode.SYSTEM_ERROR.getCode()); - response.setMsg(SbCode.SYSTEM_ERROR.getMessage()); - return response; - } - } -``` - -### sendCancelOrder -```java - /** - * 发送订单回退消息 - * @param topic - * @param tag - * @param keys - * @param body - * @throws Exception - */ - private void sendCancelOrder(String topic, String tag, String keys, String body) throws Exception{ - // 封装消息 - Message message = new Message(topic,tag,keys,body.getBytes()); - // 消息生产者发送消息,默认用自带的消息生产者 - rocketMQTemplate.getProducer().send(message); - } -``` - -### OrderSeatsCancleListener(座位异常) -```java - /** - * 回退座位 - * @param messageExt - */ - @Override - public void onMessage(MessageExt messageExt) { - try { - // 1. 解析消息 - String tags = messageExt.getTags(); - String orderTag = MqTags.ORDER_SEATS_CANCEL.getTag(); - // 过滤标签 - if (tags.equals(orderTag)) { - return; - } - String key = messageExt.getKeys(); - System.out.println("取消订单消息:" + key); - // Redis保持消费幂等性 - if (!redisUtils.hasKey(key)) { - String body = new String(messageExt.getBody(), "UTF-8"); - log.warn("收到订单服务异常:" + body); - MQDto mqDto = JSON.parseObject(body, MQDto.class); - // 判断需要的值在不在 - if (mqDto.getCountId() != null && mqDto.getSeatsIds() != null) { - // 2. 调用业务,回退座位 - boolean b = busService.filterRepeatSeats(mqDto.getSeatsIds(), mqDto.getCountId()); - if (b) { - log.warn("回退座位成功"); - redisUtils.set(key, mqDto.getOrderId(), RedisConstants.ORDER_EXCEPTION_CANCLE_EXPIRE.getTime()); - } - } - } - } catch (UnsupportedEncodingException e) { - log.error("座位回退程序崩了...好好检查程序吧", e); - } - } -``` - -### OrderAddCancleListener(订单异常) -```java - /** - * 取消订单 - * @param messageExt - */ - @Override - public void onMessage(MessageExt messageExt) { - try { - // 1. 解析消息 - String tags = messageExt.getTags(); - String orderAddTag = MqTags.ORDER_ADD_CANCLE.getTag(); - // 过滤标签 - if (!tags.equals(orderAddTag)) { - return; - } - String key = messageExt.getKeys(); - // Redis保持消费幂等性 - if (!redisUtils.hasKey(key)) { - String body = new String(messageExt.getBody(), "UTF-8"); - log.warn("收到订单服务异常:" + body); - MQDto mqDto = JSON.parseObject(body, MQDto.class); - if (mqDto.getOrderId() != null) { - // 2. 程序异常或者系统内部异常导致的订单,因此我认为删除该订单。 - // 该订单有可能没有插入成功程序就异常了。 - orderService.deleteOrderById(mqDto.getOrderId()); - log.warn("异常订单已删除"); - redisUtils.set(key, mqDto.getOrderId(), RedisConstants.ORDER_EXCEPTION_CANCLE_EXPIRE.getTime()); - } - } - } catch (UnsupportedEncodingException e) { - log.error("订单消费信息程序崩...", e); - } - } -``` - -## 支付 -### pay -```java - /** - * 支付业务逻辑 - * @param requset - * @return - */ - @Override - public PayResponse pay(PayRequset requset) { - PayResponse payResponse = new PayResponse(); - Long userId = requset.getUserId(); - Double userMoney = null; - try { - // 1. 先核对支付密码是否正确 - tag = MqTags.PAY_CHECK_CANCLE.getTag(); - String key = RedisConstants.USER_INFO_EXPIRE.getKey() + userId; - UserResponse userResponse = new UserResponse(); - if (redisUtils.hasKey(key)) { - userResponse = (UserResponse) redisUtils.get(key); - } else { - UserRequest request = new UserRequest(); - request.setId(userId); - // 获取用户信息 - userResponse = userService.getUserById(request); - } - - // 支付密码不对 - if (!userResponse.getUserDto().getPayPassword().equals(requset.getPayPassword())) { - payResponse.setCode(SbCode.PAY_PASSWORD_ERROR.getCode()); - payResponse.setMsg(SbCode.PAY_PASSWORD_ERROR.getMessage()); - return payResponse; - } - // 2。 核对余额是否够 - userMoney = userResponse.getUserDto().getMoney(); - Double subMoney = NumberUtil.sub(userMoney, requset.getTotalMoney()); - BigDecimal round = NumberUtil.round(subMoney, 2); - if (round.doubleValue() < 0) { - payResponse.setCode(SbCode.MONEY_ERROR.getCode()); - payResponse.setMsg(SbCode.MONEY_ERROR.getMessage()); - return payResponse; - } - // 3。 够,就写入 - UserUpdateInfoRequest request = new UserUpdateInfoRequest(); - request.setId(userId); - request.setMoney(round.doubleValue()); - tag = MqTags.PAY_MONEY_CANCLE.getTag(); - userService.updateUserInfo(request); // 暂时先不接受返回信息 - // 模拟异常 -// CastException.cast(SbCode.SYSTEM_ERROR); - payResponse.setCode(SbCode.SUCCESS.getCode()); - payResponse.setMsg(SbCode.SUCCESS.getMessage()); - // 4. 按道理讲,这边更改订单状态...... - return payResponse; - } catch (Exception e) { - log.error("支付业务发生异常"); - MQDto mqDto = new MQDto(); - mqDto.setUserId(userId); - mqDto.setUserMoney(userMoney); - // 发送消息 - try { - String key = RedisConstants.PAY_EXCEPTION_CANCLE_EXPIRE.getKey() + Convert.toStr(userId); - sendCancelPay(topic,tag,key, JSON.toJSONString(mqDto)); - log.warn("支付回退消息已发送"); - } catch (Exception ex) { - ex.printStackTrace(); - log.error("支付消息都崩的话..."); - } - payResponse.setCode(SbCode.SYSTEM_ERROR.getCode()); - payResponse.setMsg(SbCode.SYSTEM_ERROR.getMessage()); - return payResponse; - } - - } -``` -### sendCancelPay -```java - /** - * 发送支付消息 - * @param topic - * @param tag - * @param keys - * @param body - * @throws Exception - */ - private void sendCancelPay(String topic, String tag, String keys, String body) throws Exception { - Message message = new Message(topic,tag,keys,body.getBytes()); - rocketMQTemplate.getProducer().send(message); - } -``` - -### PayMoneyCancleListener -```java - /** - * 支付金额异常 - * @param messageExt - */ - @Override - public void onMessage(MessageExt messageExt) { - try { - // 1. 解析消息 - String tags = messageExt.getTags(); - String payCancleTag = MqTags.PAY_MONEY_CANCLE.getTag(); - if (!tags.equals(payCancleTag)) { - return; - } - // 2. 拿到key - String key = messageExt.getKeys(); - if (!redisUtils.hasKey(key)) { - String body = new String(messageExt.getBody(), "UTF-8"); - log.warn("收到订单服务异常:" + body); - MQDto mqDto = JSON.parseObject(body, MQDto.class); - if (mqDto.getUserId() != null && mqDto.getUserMoney() != null) { - UserUpdateInfoRequest request = new UserUpdateInfoRequest(); - request.setId(mqDto.getUserId()); - request.setMoney(mqDto.getUserMoney()); - userService.updateUserInfo(request); - log.warn("余额已恢复"); - redisUtils.set(key, mqDto.getUserId(), RedisConstants.PAY_EXCEPTION_CANCLE_EXPIRE.getTime()); - } - } - } catch (Exception e) { - log.error("支付消费信息程序崩...\n", e); - } - } -``` - -## 回退 -### payBack -> 这里可以写的,我省了... - -## 再谈 -关于项目中只是采用了RocketMQ维持了系统的最终一致性,其他的优点,限流等都没有用上,也可以用上的。以上流程图我也没时间画,来不及。 \ No newline at end of file diff --git "a/Java/codes/bus/\344\270\212\347\272\277\351\201\207\345\210\260\347\232\204bug.md" "b/Java/codes/bus/\344\270\212\347\272\277\351\201\207\345\210\260\347\232\204bug.md" deleted file mode 100644 index dd379ca9..00000000 --- "a/Java/codes/bus/\344\270\212\347\272\277\351\201\207\345\210\260\347\232\204bug.md" +++ /dev/null @@ -1,200 +0,0 @@ -## 上线bug - -> 项目上线之后,大概将近一个月,我点击车次列表页面,突然什么都没有了,于是就开始找哪个不顺眼的家伙搞的鬼。 - -### 起因 - -访问主页[http://47.104.22.225:8080/home](http://47.104.22.225:8080/home) - -![](https://imgkr.cn-bj.ufileos.com/8c563c2e-49f8-4a96-8d02-0109bc956449.png) - -如图: - -一直加载中,于是我看一下响应信息。 - -```json -code: 200 -message: "success" -result: {code: "003099", msg: "系统错误"} -success: true -timestamp: "1589715142518" -``` - -### 查看一下gateway的日志 -- 终端显示不长,于是我将日志用vscode打开看 - -```log -org.apache.dubbo.rpc.RpcException: No provider available from registry 39.108.93.119:2181 for service com.stylefeng.guns.rest.bus.IBusService on consumer 192.168.31.221 use dubbo version 2.7.4.1, please check status of providers(disabled, not registered or in blacklist). -``` - -类似于这样的信息,说我们的bus服务没有注册。 - -> 注意:出事之前,几个服务都在的呀,怎么今天bus突然不在了。于是,我不相信,我就去dubbo后台看了一下... - -- 进入文件夹`dubbo-admin -> dubbo-admin-ui` -- 执行`npm run dev` -- 在我目前的mac上chrome浏览器输入`http://dubbo.dreamcat.ink:2020/` - -**此时,还真没有bus服务** -![](https://imgkr.cn-bj.ufileos.com/3eb38910-64b5-439c-8437-c155d50857d2.png) - -**哭晕在厕所**。 - -![](https://imgkr.cn-bj.ufileos.com/e391627b-dce7-4146-b6d7-78cbd1fee35e.jpg) - -**于是乎,俺又不想重新启动服务,毕竟你看** - -- 终端输入`ps -ef | grep guns-bus` -- 我们看到了惊人的一幕 -```shell -pch 2003942 1 0 4月16 ? 06:10:54 java -jar guns-bus-0.0.1.jar -``` - -**又哭晕在厕所...** - -### 猜测 - -#### DubboAdmin展示? -好像没问题吧?这样的话,其他三个也应该不存在的啊 - -#### 注册中心,bus节点丢了 - -- 查看日志,当天出现zk出现了大量的超时,原因是当天的zk**主节点**宕机了。 - -#### 找原因 - -**问题是否出现在了dubbo对zk重连恢复数据这块,开始查源码。注册中心源码ZookeeperRegistry。** - -1. 连接注册zk:通过zkclient添加zk状态监听。并且继承了FailbackRegistry各种失败重试。 - -```java -public class ZookeeperRegistry extends FailbackRegistry { - public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) { - ... - // 1. 连接zk - zkClient = zookeeperTransporter.connect(url); - // 2. 添加zk状态监听 - zkClient.addStateListener(new StateListener() { - @Override - public void stateChanged(int state) { - // 3. 重新连接后恢复动作,将当前的注册服务于订阅任务添加至重试列表中等待重试 - if (state == RECONNECTED) { - try { - recover(); - } catch (Exception e) { - logger.error(e.getMessage(), e); - } - } - } - }); - } -} - -``` - -2. zk客户端:默认使用CuratorZookeeperClient实现 - -```java -public class CuratorZookeeperClient extends AbstractZookeeperClient { - public CuratorZookeeperClient(URL url) { - ... - client = builder.build(); - // dubbo对接zk连接状态监听器 - client.getConnectionStateListenable().addListener(new ConnectionStateListener() { - @Override - public void stateChanged(CuratorFramework client, ConnectionState state) { - if (state == ConnectionState.LOST) { - CuratorZookeeperClient.this.stateChanged(StateListener.DISCONNECTED); - } else if (state == ConnectionState.CONNECTED) { - CuratorZookeeperClient.this.stateChanged(StateListener.CONNECTED); - } else if (state == ConnectionState.RECONNECTED) { - CuratorZookeeperClient.this.stateChanged(StateListener.RECONNECTED); - } - } - }); - client.start(); - ... - } -} -``` - -3. 重试任务:注册重新失败重连任务FailbackRegistry中的DubboRegistryFailedRetryTimer,默认5秒检查一次是否需要失败恢复 - -```java -public FailbackRegistry(URL url) { - super(url); - this.retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD); - this.retryFuture = retryExecutor.scheduleWithFixedDelay(new Runnable() { - @Override - public void run() { - // Check and connect to the registry - try { - // failedRegistered失败注册重试,failedUnregistered失败注销重试,failedSubscribed失败订阅重试,failedUnsubscribed失败取消订阅重试,failedNotified失败通知重试 - retry(); - } catch (Throwable t) { // Defensive fault tolerance - logger.error("Unexpected error occur at failed retry, cause: " + t.getMessage(), t); - } - } - }, retryPeriod, retryPeriod, TimeUnit.MILLISECONDS); -} - -``` - -#### 总结一波 -通过三部分的代码我们可以推断,如果zk状态监听与恢复部分出现问题可能会导致数据丢失问题。于是查看相关的api并且尝试查看dubbo社区的问题与bug,果然发现了类似问题的修改与原因分析:[https://github.com/apache/dubbo/pull/5135](https://github.com/apache/dubbo/pull/5135) - -原因: -> 如果ZNode数据已经存在,在会话超时期间,此时我们将重建一个数据节点,这个重复的异常原因可能是由于zk server中老的超时会话依然持有节点导致该节点的delete删除事件延迟,并且zk server还没有来得及去执行删除,可能由这种场景引起。在这个情景下,我们可以本地删除节点后再创建恢复节点数据。 - -其实说白了: -> 如果会话断开连接又重新连接成功。断开连接发出的删除节点事件,**因为延迟原因走在了重新连接恢复节点事件的后面**。**导致重新连接后没能成功恢复节点**。也就是我么见到的,provider有三个节点服务正常,但是zk注册中心中的提供者节点数据丢失,导致出现该节点对其他订阅者不可见的现象 - -```java - protected void createEphemeral(String path, String data) { - byte[] dataBytes = data.getBytes(CHARSET); - - try { - ((ACLBackgroundPathAndBytesable)this.client.create().withMode(CreateMode.EPHEMERAL)).forPath(path, dataBytes); - } catch (NodeExistsException var5) { - logger.warn("ZNode " + path + " already exists, since we will only try to recreate a node on a session expiration, this duplication might be caused by a delete delay from the zk server, which means the old expired session may still holds this ZNode and the server just hasn't got time to do the deletion. In this case, we can just try to delete and create again.", var5); - this.deletePath(path); - this.createEphemeral(path, data); - } catch (Exception var6) { - throw new IllegalStateException(var6.getMessage(), var6); - } - - } -``` - -**人家源码的warn写的是真清楚**... - -### 定位cpu过高的线程或者位置 - -> 这里要用到几个命令了,比如top,jstack等。 - -#### 查看一波 -- 终端输入`top -H` - -![](https://imgkr.cn-bj.ufileos.com/e69d6335-c260-4d69-9f4d-840b7427cce5.png) -- 类似于长这样,但是有些含义就不解释了,请上互联网 -- 我就挑它吧`1227027` -- 终端输入`top -Hp 1227027` -![](https://imgkr.cn-bj.ufileos.com/d703c8ae-a3a8-4eed-ad57-6c3440dd4ea7.png) -- 这里我也就不介绍了,找一个`1227029` -- `printf "%x\n" 1227029`结果是:`12b915` -- `jstack -l 1227027| grep 0x12b915 -A 10` - -```shell -"DestroyJavaVM" #84 prio=5 os_prio=0 tid=0x00007f69f800b7c0 nid=0x12b915 waiting on condition [0x0000000000000000] - java.lang.Thread.State: RUNNABLE - - Locked ownable synchronizers: - - None - -"BrokerFastFailureScheduledThread1" #83 prio=5 os_prio=0 tid=0x00007f69f8b29050 nid=0x12ba0a runnable [0x00007f68b6bec000] - java.lang.Thread.State: TIMED_WAITING (parking) - at sun.misc.Unsafe.park(Native Method) - - parking to wait for <0x0000000080594a60> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) - at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215) -``` -**以上就是举个例子** ... \ No newline at end of file diff --git "a/Java/codes/bus/\344\270\232\345\212\241\351\200\273\350\276\221SQL\350\257\255\345\217\245.md" "b/Java/codes/bus/\344\270\232\345\212\241\351\200\273\350\276\221SQL\350\257\255\345\217\245.md" deleted file mode 100644 index d869ac93..00000000 --- "a/Java/codes/bus/\344\270\232\345\212\241\351\200\273\350\276\221SQL\350\257\255\345\217\245.md" +++ /dev/null @@ -1,210 +0,0 @@ -## Explain - -> 分别对应: -> -> id select_type table type possible_keys key key_len ref rows Extra -> -> 参考:[https://juejin.im/post/5ec4e4a5e51d45786973b357](https://juejin.im/post/5ec4e4a5e51d45786973b357) - -## 用户服务 - -### checkUsername - -```sql -SELECT - * -FROM - sb_user su -WHERE - user_name = 'mai'; -``` - -- 唯一索引 - -``` -1 "SIMPLE" "su" "const" "user_name" "user_name" "152" "const" 1 "" -``` - -- 普通索引 - -```sql -1 "SIMPLE" "su" "ref" "user_name" "user_name" "152" "const" 1 "Using index condition" -``` - -- 不加索引 - -```sql -1 "SIMPLE" "su" "ALL" NULL NULL NULL NULL 11 "Using where" -``` - -### register省略 - -### login省略 - -### getUserById省略 - -### updateUserInfo省略 - -## 班车服务 - -### getBus省略 - -### getCount - -```sql -SELECT - sc.uuid, - sc.begin_date, - sc.begin_time, - sc.bus_id, - sc.bus_status, - sc.seat_status -FROM - sb_count sc -WHERE - sc.begin_date = '2020-06-03' - AND sc.begin_time >= '14:00' - AND sc.bus_status = '1'; -``` - -- 不加索引 - -``` -1 "SIMPLE" "sc" "ALL" NULL NULL NULL NULL 581 "Using where" - -53ms -``` - -- 给begin_date加普通索引 - -``` -1 "SIMPLE" "sc" "ref" "begin_date" "begin_date" "402" "const" 17 "Using index condition; Using where" - -53ms // 数据太少,体现不出来... -``` - -- 给begin_time加普通索引 - -``` -1 "SIMPLE" "sc" "ref" "begin_date,begin_time" "begin_date" "402" "const" 17 "Using index condition; Using where" - -53ms -``` - -- 给bus_status加普通索引 - -``` -1 "SIMPLE" "sc" "index_merge" "begin_date,begin_time,bus_status" "begin_date,bus_status" "402,152" NULL 7 "Using intersect(begin_date,bus_status); Using where" - -53ms -``` - -### getCountDetailById省略 - -## 订单服务 - -### getNoTakeOrdersById - -```sql -SELECT - so.uuid, - so.bus_status, - so.seats_ids, - so.order_user, - so.order_status, - so.order_time, - sc.bus_id, - CONCAT(sc.begin_date, ' ', sc.begin_time) begin_time -FROM - sb_order so - LEFT JOIN sb_count sc ON so.count_id = sc.uuid -WHERE - so.user_id = '4' - AND so.order_status = '1' - AND sc.begin_date >= '2020-06-03' - AND sc.begin_time >= '15:00'; -``` - -- 不加普通索引 - -``` -1 "SIMPLE" "so" "ALL" NULL NULL NULL NULL 45 "Using where" -1 "SIMPLE" "sc" "eq_ref" "PRIMARY,begin_date,begin_time" "PRIMARY" "8" "school_bus.so.count_id" 1 "Using where" -``` - -- user_id+order_status加普通索引 - -``` -1 "SIMPLE" "so" "ref" "order_status,user_id" "order_status" "152" "const" 10 "Using index condition; Using where" -1 "SIMPLE" "sc" "eq_ref" "PRIMARY,begin_date,begin_time" "PRIMARY" "8" "school_bus.so.count_id" 1 "Using where" -``` - -### getEvaluateOrdersById - -```sql -SELECT - so.uuid, - so.bus_status, - so.seats_ids, - so.order_user, - so.order_time, - sc.bus_id, - so.evaluate_status, - so.comment, - CONCAT(sc.begin_date, ' ', sc.begin_time) begin_time -FROM - sb_order so - LEFT JOIN sb_count sc ON so.count_id = sc.uuid -WHERE - so.user_id = 4 - AND so.evaluate_status = "1" - AND so.order_status = "1" - AND (sc.begin_date = "2020-05-30" AND sc.begin_time < "21:00" OR sc.begin_date < "2020-05-30") -``` - -- 带了普通索引 - -``` -1 "SIMPLE" "so" "ref" "user_id_order_eval_status" "user_id_order_eval_status" "312" "const,const,const" 1 "Using index condition" -1 "SIMPLE" "sc" "eq_ref" "PRIMARY" "PRIMARY" "8" "school_bus.so.count_id" 1 "Using where" -``` - -### getNoPayOrdersById - -```sql -SELECT - so.uuid, - so.bus_status, - so.seats_ids, - so.order_user, - so.order_status, - so.order_time, - sc.bus_id, - CONCAT(sc.begin_date, ' ', sc.begin_time) begin_time -FROM - sb_order so - LEFT JOIN sb_count sc ON so.count_id = sc.uuid -WHERE - so.user_id = '4' - AND so.order_status = '0' - AND sc.begin_date >= '2020-06-03' - AND sc.begin_time >= '20:00'; -``` - -- 带了普通索引 - -``` -1 "SIMPLE" "so" "ref" "order_status,user_id" "order_status" "152" "const" 8 "Using index condition; Using where" -1 "SIMPLE" "sc" "eq_ref" "PRIMARY,begin_date,begin_time" "PRIMARY" "8" "school_bus.so.count_id" 1 "Using where" -``` - -### addOrder省略 - -### selectOrderById省略 - -### updateOrderStatus省略 - -### deleteOrderById省略 - -## 支付服务省略 - diff --git "a/Java/codes/bus/\346\224\257\344\273\230\346\234\215\345\212\241.md" "b/Java/codes/bus/\346\224\257\344\273\230\346\234\215\345\212\241.md" deleted file mode 100644 index 4f1bfb8b..00000000 --- "a/Java/codes/bus/\346\224\257\344\273\230\346\234\215\345\212\241.md" +++ /dev/null @@ -1,141 +0,0 @@ -# 支付服务 - -## PayController - -### pay -> 省略 - -### payBack -> 退款 -```java -// 删除个人详细信息缓存 -if (redisUtils.hasKey(userInfoKey)) { - redisUtils.del(userInfoKey); -} -// 删除未支付订单列表缓存 -if (redisUtils.hasKey(noTakeKey)) { - redisUtils.del(noTakeKey); -} -// 删除订单详情缓存 -if (redisUtils.hasKey(selectOrderKey)) { - redisUtils.del(selectOrderKey); -} -// 删除场次详情缓存 -if (redisUtils.hasKey(countDetailKey)) { - redisUtils.del(countDetailKey); -} -``` - -## PayServiceImpl - -### pay -```java -public PayResponse pay(PayRequset requset) { - PayResponse payResponse = new PayResponse(); - Long userId = requset.getUserId(); - Double userMoney = null; - try { - // 1. 先核对支付密码是否正确 - tag = MqTags.PAY_CHECK_CANCLE.getTag(); - String key = RedisConstants.USER_INFO_EXPIRE.getKey() + userId; - UserResponse userResponse = new UserResponse(); - if (redisUtils.hasKey(key)) { - userResponse = (UserResponse) redisUtils.get(key); - } else { - UserRequest request = new UserRequest(); - request.setId(userId); - // 获取用户信息 - userResponse = userService.getUserById(request); - } - - // 支付密码不对 - if (!userResponse.getUserDto().getPayPassword().equals(requset.getPayPassword())) { - payResponse.setCode(SbCode.PAY_PASSWORD_ERROR.getCode()); - payResponse.setMsg(SbCode.PAY_PASSWORD_ERROR.getMessage()); - return payResponse; - } - // 2。 核对余额是否够 - userMoney = userResponse.getUserDto().getMoney(); - Double subMoney = NumberUtil.sub(userMoney, requset.getTotalMoney()); - BigDecimal round = NumberUtil.round(subMoney, 2); - if (round.doubleValue() < 0) { - payResponse.setCode(SbCode.MONEY_ERROR.getCode()); - payResponse.setMsg(SbCode.MONEY_ERROR.getMessage()); - return payResponse; - } - // 3。 够,就写入 - UserUpdateInfoRequest request = new UserUpdateInfoRequest(); - request.setId(userId); - request.setMoney(round.doubleValue()); - tag = MqTags.PAY_MONEY_CANCLE.getTag(); - userService.updateUserInfo(request); // 暂时先不接受返回信息 - // 模拟异常 -// CastException.cast(SbCode.SYSTEM_ERROR); - payResponse.setCode(SbCode.SUCCESS.getCode()); - payResponse.setMsg(SbCode.SUCCESS.getMessage()); - // 4. 按道理讲,这边更改订单状态...... - return payResponse; - } catch (Exception e) { - log.error("支付业务发生异常"); - MQDto mqDto = new MQDto(); - mqDto.setUserId(userId); - mqDto.setUserMoney(userMoney); - // 发送消息 - try { - String key = RedisConstants.PAY_EXCEPTION_CANCLE_EXPIRE.getKey() + Convert.toStr(userId); - sendCancelPay(topic,tag,key, JSON.toJSONString(mqDto)); - log.warn("支付回退消息已发送"); - } catch (Exception ex) { - ex.printStackTrace(); - log.error("支付消息都崩的话..."); - } - payResponse.setCode(SbCode.SYSTEM_ERROR.getCode()); - payResponse.setMsg(SbCode.SYSTEM_ERROR.getMessage()); - return payResponse; - } - -} -``` - -### payBack - -```java -public PayResponse payBack(PayBackRequest request) { - PayResponse response = new PayResponse(); - try { - // 1. 退回金额 - // 读用户金额 - UserRequest userRequest = new UserRequest(); - userRequest.setId(request.getUserId()); - UserResponse userResponse = userService.getUserById(userRequest); - UserDto userDto = userResponse.getUserDto(); - // 计算金额 - BigDecimal add = NumberUtil.add(userDto.getMoney() + request.getTotalMoney()); - BigDecimal round = NumberUtil.round(add, 2); - // 写回 - UserUpdateInfoRequest userUpdateInfoRequest = new UserUpdateInfoRequest(); - userUpdateInfoRequest.setId(request.getUserId()); - userUpdateInfoRequest.setMoney(round.doubleValue()); - userService.updateUserInfo(userUpdateInfoRequest); - // 2. 退回座位 - busService.filterRepeatSeats(request.getSeatsIds(), request.getCoundId()); - // 3. 更改订单状态:关闭 - OrderRequest orderRequest = new OrderRequest(); - orderRequest.setUuid(request.getOrderId()); - orderRequest.setOrderStatus("3"); - orderService.updateOrderStatus(orderRequest); - response.setCode(SbCode.SUCCESS.getCode()); - response.setMsg(SbCode.SUCCESS.getMessage()); - return response; - } catch (Exception e) { -// e.printStackTrace(); - log.error("退款业务异常"); - // 这里可以发消息, 此处先省略 - response.setCode(SbCode.SYSTEM_ERROR.getCode()); - response.setMsg(SbCode.SYSTEM_ERROR.getMessage()); - return response; - } -} -``` - -这两个业务逻辑异常都采用了消息队列,日后再写。 \ No newline at end of file diff --git "a/Java/codes/bus/\347\216\257\345\242\203\346\220\255\345\273\272\346\226\207\346\241\243.md" "b/Java/codes/bus/\347\216\257\345\242\203\346\220\255\345\273\272\346\226\207\346\241\243.md" deleted file mode 100644 index e6839718..00000000 --- "a/Java/codes/bus/\347\216\257\345\242\203\346\220\255\345\273\272\346\226\207\346\241\243.md" +++ /dev/null @@ -1,64 +0,0 @@ -# 项目环境搭建 -> 主要搭建的技术:Java、MySQL、Redis、zookeeper、Dubbo、RocketMQ - -## 引言 -首先说明,MySQL和Redis放到了我的阿里云服务器,负载并不大,其他全部服务搭建在实验室的服务器主机上,并采用内网穿透映射端口,让大家访问,大家注意一点哈。请谅解。 - -还有,本项目是对本校的班车预约服务的架构进行了重构,采用的比较热门的技术,如README所示。 - -## 技术选型 - - -![选型介绍图](https://imgkr.cn-bj.ufileos.com/56250623-c12e-4440-abe9-bc0277c4e3be.png) - -从选型图上可以清晰的看到: -- **Web层**:sb-gateway网关,控制整个项目的请求分发与转接等。 -- **接口层**:sb-api定义各个服务的接口,比如用户、班车、订单等。 -- **服务层**:有着四大服务,分别如上图所示,用户、班车、订单和支付,并且其中均采用第三方中间件Zookeeper、Dubbo和RocketMQ等。 -- **存储层**,实际上,MySQL作为存储,而Redis做为MySQL上一层的缓存层 - -## 架构路线 - - -![架构路线图](https://imgkr.cn-bj.ufileos.com/18606af1-be51-44ce-9c06-f2abff84a294.png) - -首先,一条请求从用户移动端或者PC端发出,经过前端的Nginx的代理转发到sb-gateway的网关,网关进一步的将请求进行路由分发到对应的Controller,从而执行相应的业务逻辑,其中对应的Controller中分为两步,第一步:找缓存,若存在,直接返回,若不存在。第二步:业务逻辑方法向Dubbo请求远程调用(RPC),得到结果,另外一边业务执行相应的底层如MySQL等,将结果返回给网关,并写入缓存等。 - -## 环境端口 - - -![端口定义](https://imgkr.cn-bj.ufileos.com/9a949053-2417-468a-b7cb-0490c5c78b51.png) - - -![环境启动](https://imgkr.cn-bj.ufileos.com/53607d10-75a7-47bc-815c-adbeef4193f5.png) - - - -## Java -> 我在博客上写好了,可以参考。[http://dreamcat.ink/2019/07/08/windows-mac-he-linux-an-zhuang-java/](http://dreamcat.ink/2019/07/08/windows-mac-he-linux-an-zhuang-java/) - -## MySQL -> 我也在博客上写好了,可以参考,项目采用的MySQL8版本。[http://dreamcat.ink/2019/05/27/windows-mac-he-linux-an-zhuang-mysql/](http://dreamcat.ink/2019/05/27/windows-mac-he-linux-an-zhuang-mysql/) - -## Redis -> 我还在博客上写好了,可以参考。[http://dreamcat.ink/2019/05/28/windows-mac-he-linux-an-zhuang-redis/](http://dreamcat.ink/2019/05/28/windows-mac-he-linux-an-zhuang-redis/) - -## zookeeper -- [下载地址](https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/) -- 选择3.4.14版本下载即可,放进和刚才的myapps下。 -- 解压:tar -zxf zookeeper-3.4.14.tar.gz -- 将配置文件cp -r zoo_sample.cfg zoo.cfg -- 启动zookeeper./zkServer.sh start - -## Dubbo -- [采用的是最新版本](https://github.com/apache/dubbo-admin) -- 下载解压后修配置文件 `dubbo-admin-server/src/main/resources/application.properties` -- 配置文件修改`zookeeper`地址,dubbo控制台端口默认8080,(参考端口图) -- 可以修改为其他端口例如 server.port=9898,以免与其他服务端口冲突。(参考端口图) -- 在主目录dubbo-admin-server目录下,执行 `mvn clean package -Dmaven.test.skip=true` -- `cd dubbo-admin-server/target java -jar dubbo-admin-0.1.jar` 启动即可 -- 最新版本采用前后端分离,那么前端可以到`dubbo-admin-ui`目录下,可在config目录下找到index.js中修改端口,修改过后可以终端输入`npm install` -- 安装依赖过后,可以`npm run dev`启动 - -## RocketMQ -> 我早有记录:[http://dreamcat.ink/2020/02/21/centos7-an-zhuang-rocketmq-ji-pei-zhi/](http://dreamcat.ink/2020/02/21/centos7-an-zhuang-rocketmq-ji-pei-zhi/) \ No newline at end of file diff --git "a/Java/codes/bus/\347\217\255\350\275\246\346\234\215\345\212\241.md" "b/Java/codes/bus/\347\217\255\350\275\246\346\234\215\345\212\241.md" deleted file mode 100644 index 070b58fc..00000000 --- "a/Java/codes/bus/\347\217\255\350\275\246\346\234\215\345\212\241.md" +++ /dev/null @@ -1,357 +0,0 @@ -# 班车服务 - -## BusController - -### getCount -> 这部分缓存,优化的时候采用了Redis的list,发现我用Jmeter测试并发的时候,发现了Redis中出现了非常多的异常数据,我当时还没有找到问题,暂时先采取以下方案,以下方案在并发时候,并没有出现数据异常。 -```java -if (redisUtils.hasKey(key)) { - // 如果缓存存在 - Object obj = redisUtils.get(key); - log.warn("getCount->redis\n"); - // 返回数据 - return new ResponseUtil().setData(obj); -} - -// 写 -if (!redisUtils.hasKey(key)) { - // 如果缓存不存在,就写,注意与数据库数据一致性 - redisUtils.set(key, response, RedisConstants.COUNTS_EXPIRE.getTime()); -} -``` - -### getCountDetailById -```java -// 和上面一样 -if (redisUtils.hasKey(key)) { - Object obj = redisUtils.get(key); - log.warn("getCountDetailById->redis\n"); - return new ResponseUtil().setData(obj); -} -// 这里不判断了,上面已经判断过了 -redisUtils.set(key, response, RedisConstants.COUNT_DETAIL_EXPIRE.getTime()); -``` - -## BusServiceImpl -### getBus -> 这里基本没调用过,它是获取班车人物信息的。所以分页查询即可 - -```java -// MyBatis plus的分页查询 -IPage busIPage = new Page<>(request.getCurrentPage(), request.getPageSize()); -busIPage = busMapper.selectPage(busIPage, null); -``` - -### getCount -> 获取场次列表 - -```java -// 分页插件,这里自定义分页查询 -IPage countIPage = new Page<>(request.getCurrentPage(), request.getPageSize()); -QueryWrapper queryWrapper = new QueryWrapper<>(); -// 获取时间 -String currHours = DateUtil.getHours(); -String day = DateUtil.getDay(); -System.out.println("当前时间:"+currHours); -System.out.println("当前日期:"+day); -// 判断条件 -// 1. 找出符合当前天(比如,5.30) -// 2. 找出大于等于当前时间的场次(比如,数据库8点有一场,目前时间为7点,它就符合) -// 3. 找出状态为getBusStatus的场次,一般是还未发车的场次,(比如0,1) -queryWrapper - .eq("begin_date", day) - .ge("begin_time", currHours) - .eq("bus_status", request.getBusStatus()) - .orderByAsc("begin_time");// 时间 -countIPage = countMapper.selectCounts(countIPage, queryWrapper); -``` -**自定义分页CountMapper接口** -```java -public interface CountMapper extends BaseMapper { - /** - * - * @param page - * @param wrapper - * @return - */ - IPage selectCounts(IPage page, @Param(Constants.WRAPPER) Wrapper wrapper); - - /** - * - * @param wrapper - * @return - */ - CountDetailDto selectCountDetailById(@Param(Constants.WRAPPER) Wrapper wrapper); -} -``` -**xml** -```xml - -``` - -### getCountDetailById -> 查询场次详情信息 -```java -// 自定义查询,涉及到联表查询 -QueryWrapper wrapper = new QueryWrapper<>(); -wrapper.eq("sc.uuid", request.getCountId()); -``` -**xml** -```xml - -``` - -### repeatSeats -> 判断座位是否已重复 -```java -public boolean repeatSeats(String seats, Long coundId) { - // 查查数据库, 找到座位字段 - boolean b = false; // false:不重复,true:重复 - try { - Count count = countMapper.selectById(coundId); - // 比如,selectedSeats 是1,2 - // dbSeats:"", - // dbSeats:"1,2,3", - // dbSeats: "4,5" - // 前端传来的selectedSeats, 前端判断是否为空,要不然后端也判断一下得了 - if (seats.equals("")) { - return true; - } - if (count.getSelectedSeats().equals("")) { - return false; - } - String[] ss = seats.split(","); - String[] cs = count.getSelectedSeats().split(","); - // 这里考虑并发问题 - HashSet hashSet = new HashSet<>(Arrays.asList(cs)); - for (String s : ss) { - if (hashSet.contains(s)) return true; - } - } catch (Exception e) { - e.printStackTrace(); - log.error("selectedSeats", e); - return true; // 异常就算是重复 - } - return b; -} -``` -### addSeats -```java -if (!StrUtil.isEmpty(selectedSeats)) { - // 这里可以优化,字符串拼接,这样的方式爆内存 - // StringBuffer - newSelectedSeats = selectedSeats + "," + newSelectedSeats; -} -``` - -### filterRepeatSeats -> 回退座位 -```java -Count count = countMapper.selectById(coundId); -String[] ss = seats.split(","); -String[] cs = count.getSelectedSeats().split(","); -// 并发问题,注意 -HashSet hashSet = new HashSet<>(Arrays.asList(cs)); -for (String s : ss) { - if (hashSet.contains(s)) { - hashSet.remove(s); - } -} -if (hashSet.isEmpty()) { - count.setSelectedSeats(""); -} -// 考虑了并发 -StringBuffer sb = new StringBuffer(); -for (String s : hashSet) { - sb.append(s); - sb.append(","); -} -// 上面的方案可以用String的replace的方法替换,遍历要回退的座位,依次替换即可 -count.setSelectedSeats(sb.toString()); -countMapper.updateById(count); -``` -看一下String的replace的源码(1.8),感觉遍历还挺多,但的确不用自己去写了 -```java -public String replace(char oldChar, char newChar) { - if (oldChar != newChar) { - int len = value.length; - int i = -1; - char[] val = value; /* avoid getfield opcode */ - - while (++i < len) { - if (val[i] == oldChar) { - break; - } - } - if (i < len) { - char buf[] = new char[len]; - for (int j = 0; j < i; j++) { - buf[j] = val[j]; - } - while (i < len) { - char c = val[i]; - buf[i] = (c == oldChar) ? newChar : c; - i++; - } - return new String(buf, true); - } - } - return this; -} -``` - -## BusSchedule定时器 -> 这里可以使用redis的延时队列或者RocketMQ的消息队列 - -### schedulChangeBusStatus - -```java -/** - * 每天上午7点到晚上21点,每隔30分钟执行一次 - */ -@Scheduled(cron = "0 0/30 7-21 * * ?") -private void schedulChangeBusStatus() { - log.warn("schedulChangeBusStatus执行"); - busService.schedulChangeBusStatus(); -} -``` -看一下业务逻辑 -```java -public void schedulChangeBusStatus() { - // 获取时间 - String currTime = DateUtil.getHours(); - // 获取日期 - String day = DateUtil.getDay(); - log.warn("schedulChangeBusStatus->目前时间:" + currTime); - log.warn("schedulChangeBusStatus->目前时间:" + day); - System.out.println("目前时间:"+ currTime); - System.out.println("目前时间:"+ day); - QueryWrapper queryWrapper = new QueryWrapper<>(); - // 先取出beingtime和now相等的表或者end_time和now相等到表 - // 1. 取出当天 - // 2. 取出开始时间或者结束时间符合当前时间 - queryWrapper - .eq("begin_date", day) // 取出当天 - .and(o -> o.eq("begin_time", currTime) // 当前时间 - .or() - .eq("end_time", currTime)); - List counts = countMapper.selectList(queryWrapper); - log.warn("schedulChangeBusStatus->查询到的:" + counts.toString()); - // 开始作妖 - for (Count count : counts) { - String busStatus = count.getBusStatus(); - String beginTime = count.getBeginTime(); - String endTime = count.getEndTime(); - if (currTime.equals(beginTime)) { - if (busStatus.equals("0")) { // 沙河空闲 - count.setBusStatus("2"); // 沙河->清水河 - } - if (busStatus.equals("1")) { // 清水河空闲 - count.setBusStatus("3"); // 清水河->沙河 - } - count.setSelectedSeats(""); // 清空座位 - } - if (currTime.equals(endTime)) { - if (busStatus.equals("2")) { // 沙河->清水河 - count.setBusStatus("1"); // 清水河空闲 - } - if (busStatus.equals("3")) { // 清水河->沙河 - count.setBusStatus("0"); // 沙河空闲 - } - } - System.out.println("修改的:" + count); - log.warn("schedulChangeBusStatus->修改的:" + count); - // 写入数据库 - countMapper.updateById(count); - } - // 删缓存,这里非常重要...不删后果很严重 - String key1 = RedisConstants.COUNTS_EXPIRE + "0"; - String key2 = RedisConstants.COUNTS_EXPIRE + "1"; - if (redisUtils.hasKey(key1)) { - redisUtils.del(key1); - } - if (redisUtils.hasKey(key2)) { - redisUtils.del(key2); - } -} -``` -> 这里每隔30分钟扫库进行一次io,这里可以有优化... 假如我使用Redis的延迟队列 -1. 在用定时器添加场次的时候,可以将这些场次存入Redis的zset的队列中,元素为场次ID,元素对应的score是出发时间,这样就有17个(定时器每天凌晨添加17个) -2. 还是用定时器轮询,采用每隔半小时轮询一次,我们取出队列中当前时间大于队列中权重的时间的场次ID,开始进行业务逻辑判断(更改场次状态) -3. 更改过后,删除或者更改该场次的score的时间(结束时间) -4. 以此类推,如果是结束时间的话,直接删除该场次id和score。 - -### addCounts -> 这个项目,没有后台,因此场次需要定时器添加即可 -```java -/** - * 每天凌晨0点2分执行 - */ -@Scheduled(cron = "0 2 0 * * ? ") -private void addCounts(){ - log.warn("addCounts执行"); - busService.addCounts(); -} -``` -具体的业务逻辑: -```java -public void addCounts() { - // 获取日期 - String day = DateUtil.getDay(); - // 获取前17个场次 - QueryWrapper wrapper = new QueryWrapper<>(); - wrapper.last("limit 17"); - List counts = countMapper.selectList(wrapper); - // 开始修改 这里可以用java8 的特性, 还不是很熟悉,后期优化一下 - for (Count count : counts) { - // 更改日期 - count.setBeginDate(day); - // 更改uuid - count.setUuid(UUIDUtils.flakesUUID()); - // 清空座位 - count.setSelectedSeats(""); - // 将走位状态清零 - count.setSeatStatus("0"); - // 插入 - countMapper.insert(count); - } - - // 删缓存,不删后果依然很严重 - String key1 = RedisConstants.COUNTS_EXPIRE + "0"; - String key2 = RedisConstants.COUNTS_EXPIRE + "1"; - if (redisUtils.hasKey(key1)) { - redisUtils.del(key1); - } - if (redisUtils.hasKey(key2)) { - redisUtils.del(key2); - } -} - -``` diff --git "a/Java/codes/bus/\347\224\250\346\210\267\346\234\215\345\212\241.md" "b/Java/codes/bus/\347\224\250\346\210\267\346\234\215\345\212\241.md" deleted file mode 100644 index be1fc864..00000000 --- "a/Java/codes/bus/\347\224\250\346\210\267\346\234\215\345\212\241.md" +++ /dev/null @@ -1,74 +0,0 @@ -# 用户服务 - - -## UserController - -### checkUsername -> 这个我就不需要再说了,这个没啥可说的 - - -### register -> 这个注意请求体的参数即可,也没什么可说的 - - -### getUserById -> 注意缓存是否存在,存在直接返回即可,没有就调用业务`getUserById`获取结果 - -### updateUserInfo -> 注意请求体的参数即可,并且若是更新数据,那么自然要删除UserInfo的缓存,保持数据一致,写不写缓存这一点都行,下次请求getUserById会写缓存。 - -### logout -> 注意redis删除缓存即可 - - -## UserServiceImpl - -### checkUsername -```java -QueryWrapper queryWrapper = new QueryWrapper<>(); -// 查询user_name列与getUsername相匹配的数据 -queryWrapper.eq("user_name", request.getUsername()); -User user = userMapper.selectOne(queryWrapper); -``` - -### regsiter -```java -// 密码采用了md5加密 -String md5Password = MD5Util.encrypt(user.getUserPwd()); -user.setUserPwd(md5Password); -``` - -### login -```java -QueryWrapper queryWrapper = new QueryWrapper<>(); -// 根据user_name查询用户是否存在 -queryWrapper.eq("user_name", request.getUsername()); -User user = userMapper.selectOne(queryWrapper); -if (user != null && user.getUuid() > 0) { - // 用户不为空,用户id > 0 - String md5Password = MD5Util.encrypt(request.getPassword()); - if (user.getUserPwd().equals(md5Password)) { - // 数据库密码和所加密的md5密码一致 - res.setUserId(user.getUuid()); - res.setCode(SbCode.SUCCESS.getCode()); - res.setMsg(SbCode.SUCCESS.getMessage()); - } -} -``` - -### getUserById -```java -//根据id查询用户 -User user = userMapper.selectById(request.getId()); -UserDto userDto = userConverter.User2Res(user); -``` - -### updateUserInfo -```java -// 这里一采用了mapstruct映射,有兴趣的可以去网上找资料学习学习 -User user = userConverter.res2User(request); -``` - -## 总结 - -用户服务,没有什么比较复杂的业务,复杂的业务在订单服务、场次服务、支付服务、 diff --git "a/Java/codes/bus/\350\256\242\345\215\225\346\234\215\345\212\241.md" "b/Java/codes/bus/\350\256\242\345\215\225\346\234\215\345\212\241.md" deleted file mode 100644 index 857d0fbe..00000000 --- "a/Java/codes/bus/\350\256\242\345\215\225\346\234\215\345\212\241.md" +++ /dev/null @@ -1,282 +0,0 @@ -# 订单服务 -## OrderController - -### getNoTakeOrdersById -> 未乘坐订单 - -```java -if (redisUtils.hasKey(key)) { - // redis是否有该缓存 - Object obj = redisUtils.get(key); - NoTakeBusResponse response = (NoTakeBusResponse) obj; - for (NoTakeDto noTakeDto : response.getNoTakeDtos()) { - // 如果场次发车时间-当前时间是大0的,那么说明已经发车了 - String beginTime = noTakeDto.getBeginTime(); - if (beginTime.compareTo(DateUtil.getHours()) <= -1) { - // 删掉当前缓存 - redisUtils.del(key); - // 重新获取最新的数据 - response = orderService.getNoTakeOrdersById(request); - // 写缓存 - redisUtils.set(key, response, RedisConstants.NO_TAKE_OREDERS_EXPIRE.getTime()); - return new ResponseUtil().setData(response); - } - } - log.warn("getNoTakeOrdersById->redis\n"); - return new ResponseUtil().setData(obj); -} -``` - -### getNoPayOrdersById -> 获取未支付订单接口 -```java -// 从redis中是否有缓存 -if (redisUtils.hasKey(key)) { - // 有就获取 - Object obj = redisUtils.get(key); - log.warn("getNoPayOrdersById->redis\n"); - return new ResponseUtil().setData(obj); -} -``` -### getEvaluateOrdersById -> 根据评价状态获取用户订单接口 -略 - -### addOrder -> 添加订单接口 - -```java -String countKey = RedisConstants.COUNT_DETAIL_EXPIRE.getKey() + request.getCountId(); -// 座位缓存失效 -if (redisUtils.hasKey(countKey)) { - redisUtils.del(countKey); -} -String noPayKey = RedisConstants.NO_PAY_ORDERS_EXPIRE.getKey() + userId; -// 未支付列表缓存失效 -if (redisUtils.hasKey(noPayKey)) { - redisUtils.del(noPayKey); -} -``` -### selectOrderById -> 根据订单id获取详情订单 -略 - -### updateOrderStatus -> 更改订单状态 -```java -// 删掉订单详情缓存 -if (redisUtils.hasKey(selectOrderKey)) { - redisUtils.del(selectOrderKey); -} -// 删除未乘坐缓存 -if (redisUtils.hasKey(noTakeKey)) { - redisUtils.del(noTakeKey); -} -// 删除未支付缓存 -if (redisUtils.hasKey(noPayKey)) { - redisUtils.del(noPayKey); -} -// 删除评价缓存 -if (redisUtils.hasKey(evaluateKey)) { - redisUtils.del(evaluateKey); -} -``` - -## OrderServiceImpl - -### getNoTakeOrdersById -```java -IPage orderIPage = new Page<>(request.getCurrentPage(), request.getPageSize()); -QueryWrapper queryWrapper = new QueryWrapper<>(); -// 获取系统年月日 -// 比如5.30 -String day = DateUtil.getDay(); -// 比如20:00 -String hours = DateUtil.getHours(); -System.out.println("当前日期:" + day); -System.out.println("当前时间:" + hours); -queryWrapper - .eq("user_id", request.getUserId()) // 用户id - .eq("order_status", "1")// 1:已经支付 - .ge("sc.begin_date", day) // 比如订单日期大于等于今天 - .ge("sc.begin_time", hours) // 订单时间大于等于当前时间 - .orderByAsc("sc.begin_time") // 排序 - .orderByDesc("so.order_time"); // 排序 -``` - -### getEvaluateOrdersById -```java -IPage orderIPage = new Page<>(request.getCurrentPage(), request.getPageSize()); -QueryWrapper queryWrapper = new QueryWrapper<>(); -// 获取系统年月日 -String day = DateUtil.getDay(); -String hours = DateUtil.getHours(); -System.out.println("当前日期:" + day); -System.out.println("当前时间:" + hours); -queryWrapper - .eq("user_id", request.getUserId()) // 用户di - .eq("order_status", "1") // 状态为1 - // 两种情况: - // 1. 符合当天日期,并且订单场次发车时间小于当前日期 - // 2. 订单场次的发车日期小于当前日期 - .and(o -> o.eq("sc.begin_date", day) - .lt("sc.begin_time", hours) - .or().lt("sc.begin_date", day)) - .eq("evaluate_status", request.getEvaluateStatus()) // 评价状态 - .orderByDesc("sc.begin_time") // 排序 - .orderByDesc("so.order_time"); // 排序 -``` - -### getNoPayOrdersById -```java -IPage noPayDtoIPage = new Page<>(request.getCurrentPage(), request.getPageSize()); -QueryWrapper queryWrapper = new QueryWrapper<>(); -// 获取系统年月日 -String day = DateUtil.getDay(); -String hours = DateUtil.getHours(); -System.out.println("当前日期:" + day); -System.out.println("当前时间:" + hours); -queryWrapper - .eq("so.user_id", request.getUserId()) // 用户id - .eq("so.order_status", "0") // 未支付状态 - .ge("sc.begin_date", day) // 比如,订单场次日期大于当前日期 - .ge("sc.begin_time", hours)// 订单场次时间大于当前日期 - .orderByDesc("sc.begin_time") // 排序 - .orderByDesc("so.order_time"); // 未支付 -``` - -### addOrder -```java -public AddOrderResponse addOrder(AddOrderRequest request) { - // 判断座位,如果重复,直接退出,否则更新场次的座位信息 - AddOrderResponse response = new AddOrderResponse(); - // 全局orderId - Long orderId = UUIDUtils.flakesUUID(); - // 1。 判断座位,如果重复,直接退出,否则下一步 - // 2。 更新座位,如果没有异常,这是写操作 - // 3。 计算总金额,如果没有异常 - // 4。 添加订单,如果异常,这是写操作 - try { - // 1。 判断座位,如果重复,直接退出,否则下一步 - tag = MqTags.ORDER_SEATS_CANCEL.getTag(); - boolean repeatSeats = busService.repeatSeats(request.getSeatsIds(), request.getCountId()); - if (repeatSeats) { - // b:true 说明重复 - response.setCode(SbCode.SELECTED_SEATS.getCode()); - response.setMsg(SbCode.SELECTED_SEATS.getMessage()); - return response; - } -// CastException.cast(SbCode.SYSTEM_ERROR); - // 2。 更新座位,如果没有异常,这是写操作 - // 用tags来过滤消息 - tag = MqTags.ORDER_ADD_SEATS_CANCLE.getTag(); - boolean addSeats = busService.addSeats(request.getSeatsIds(), request.getCountId()); - if (!addSeats) { - response.setCode(SbCode.DB_EXCEPTION.getCode()); - response.setMsg(SbCode.DB_EXCEPTION.getMessage()); - return response; - } - // 模拟系统异常 -// CastException.cast(SbCode.SYSTEM_ERROR); - // 3。 计算总金额,如果没有异常 - tag = MqTags.ORDER_CALC_MONEY_CANCLE.getTag(); - String seatIds = request.getSeatsIds(); - Integer seatNumber = seatIds.split(",").length; - Double countPrice = request.getCountPrice(); - Double totalPrice = getTotalPrice(seatNumber, countPrice); - -// CastException.cast(SbCode.SYSTEM_ERROR); - // 4。 添加订单,如果异常,这是写操作 - Order order = orderConvertver.res2Order(request); - order.setOrderPrice(totalPrice); - order.setEvaluateStatus("0"); // 未评价 - order.setOrderStatus("0"); // 未支付 - order.setUuid(orderId); // 唯一id - tag = MqTags.ORDER_ADD_CANCLE.getTag(); - int insert = orderMapper.insert(order);// 插入 不判断了 -// CastException.cast(SbCode.SYSTEM_ERROR); - // 这里就不读了,耗时 -// QueryWrapper wrapper = new QueryWrapper<>(); -// wrapper.eq("so.uuid", order.getUuid()); -// OrderDto orderDto = orderMapper.selectOrderById(wrapper); - response.setCode(SbCode.SUCCESS.getCode()); - response.setMsg(SbCode.SUCCESS.getMessage()); - response.setOrderId(orderId); -// response.setOrderDto(orderDto); - // 这里放redis 未支付缓存,时间前端给定 - redisUtils.set(RedisConstants.ORDER_CANCLE_EXPIRE.getKey() + orderId, orderId, request.getExpireTime()); - return response; - } catch (Exception e) { - // 以上操作如果程序都不发生异常的话, 是不会执行这里的代码的 - // 也就是说不会发送回退消息的。 - // 目的是在高并发的情况下,程序内部发生异常,依然高可用 -// e.printStackTrace(); - log.error("订单业务发生异常"); - // 发消息,将座位退回,将订单退回 - MQDto mqDto = new MQDto(); - mqDto.setOrderId(orderId); - mqDto.setCountId(request.getCountId()); - mqDto.setSeatsIds(request.getSeatsIds()); - try { - String key = RedisConstants.ORDER_EXCEPTION_CANCLE_EXPIRE.getKey() + Convert.toStr(orderId); - sendCancelOrder(topic,tag, key, JSON.toJSONString(mqDto)); - log.warn("订单回退消息发送成功..." + mqDto); - } catch (Exception ex) { - ex.printStackTrace(); - } - response.setCode(SbCode.SYSTEM_ERROR.getCode()); - response.setMsg(SbCode.SYSTEM_ERROR.getMessage()); - return response; - } -} -``` - -### selectOrderById -> 省略 - -### updateOrderStatus -```java -public OrderResponse updateOrderStatus(OrderRequest request) { - OrderResponse response = new OrderResponse(); - try { - // 获取orderDto - QueryWrapper wrapper = new QueryWrapper<>(); - wrapper.eq("so.uuid", request.getUuid()); - OrderDto orderDto = orderMapper.selectOrderById(wrapper); - // 1, 检查状态是否为2 - if (request.getOrderStatus().equals("2")) { - // 说明关闭订单,回退座位 - busService.filterRepeatSeats(orderDto.getSeatsIds(), orderDto.getCountId()); - redisUtils.del(RedisConstants.COUNT_DETAIL_EXPIRE.getKey() - + orderDto.getCountId()); - // 清除场次详情的缓存 - } - if (request.getOrderStatus().equals("1")) { - // 说明已经支付,删掉5分钟的订单缓存 - redisUtils.del(RedisConstants.ORDER_CANCLE_EXPIRE.getKey() + request.getUuid()); - } - Order order = orderConvertver.res2Order(request); - // 更新状态 - orderMapper.updateById(order); - // 暂时就不获取了 - response.setCode(SbCode.SUCCESS.getCode()); - response.setMsg(SbCode.SUCCESS.getMessage()); - redisUtils.del(RedisConstants.NO_PAY_ORDERS_EXPIRE.getKey()+order.getUserId()); - redisUtils.del(RedisConstants.SELECT_ORDER_EXPIRE.getKey() + request.getUuid()); - } catch (Exception e) { - log.error("updateOrderStatus", e); - response.setCode(SbCode.DB_EXCEPTION.getCode()); - response.setMsg(SbCode.DB_EXCEPTION.getMessage()); - return response; - } - return response; -} -``` - -### deleteOrderById -> 省略 - -### sendCancelOrder -> 发送订单回退消息 - -后边会单独介绍消息队列 \ No newline at end of file diff --git "a/Java/codes/collection/ArrayList\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/collection/ArrayList\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index 7a8d9fa1..00000000 --- "a/Java/codes/collection/ArrayList\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,123 +0,0 @@ -# ArrayList -## 一些特点 - -- ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入`null`元素,底层通过**数组**实现。 -- 每个ArrayList都有一个容量(capacity),表示**底层数组的实际大小**,容器内存储元素的个数不能多于当前容量。 -- 当向容器中添加元素时,如果容量不足,**容器会自动增大底层数组的大小**。 -- Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。 - -## 底层数据结构 -```java -private static final int DEFAULT_CAPACITY = 10; // 默认容量 -transient Object[] elementData; // Object 数组 -private int size; // 大小 -``` - -## 构造函数 - -### 有参 -```java -public ArrayList(int initialCapacity) { - if (initialCapacity > 0) { - this.elementData = new Object[initialCapacity]; - } else if (initialCapacity == 0) { - this.elementData = EMPTY_ELEMENTDATA; // 默认10 - } else { - throw new IllegalArgumentException("Illegal Capacity: "+ - initialCapacity); - } -} -``` - -### 无参 -```java -public ArrayList() { - this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 默认10 -} -``` - -## 扩容 -常问,看一下源码吧 - -```java -public void ensureCapacity(int minCapacity) { - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - - if (minCapacity > minExpand) { - ensureExplicitCapacity(minCapacity); - } -} -``` -继续看ensureExplicitCapacity -```java -private void ensureExplicitCapacity(int minCapacity) { - modCount++; - - // overflow-conscious code - if (minCapacity - elementData.length > 0) - grow(minCapacity); // 扩容最关键的方法 -} -``` -其实可以之和看grow(jdk1.8) - -```java - private void grow(int minCapacity) { - // overflow-conscious code - int oldCapacity = elementData.length; - int newCapacity = oldCapacity + (oldCapacity >> 1); // old + old * 2 就在这... - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); -} -``` - -## add -还是看直接看源码方便一些 -```java -public boolean add(E e) { - // 扩容判断 - ensureCapacityInternal(size + 1); // Increments modCount!! 这个参数,起到并发异常作用。 - elementData[size++] = e; // 这一步非原子性,并发容易出错,好几种情况。 下次分析 - return true; -} -``` - -## get -```java -一般获取元素,第一步都要判断索引是否越界 -public E get(int index) { - rangeCheck(index); // 判断给定索引是否越界 - return elementData(index); -} -``` - -## 聊一下并发容易出现的问题 -### 首先是数组越界,举个例子 -1. 列表大小为9,即size=9 -2. 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。 -3. 线程B此时也进入add方法,它和获取的size的值也为9,也开始调用ensureCapacityInternal方法。 -4. 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。 -5. 线程B也发现需要大小为10,也可以容纳,返回。 -6. 好了,**问题来了哈** -7. 线程A开始进行设置值操作,elementData[size++] = e操作。此时size变为10。 -8. 线程B也开始进行设置值操作,它尝试设置elementData[10] = e, 而elementData没有进行过扩容,它的下标最大为9,于是此时会报出一个数组越界的异常`ArrayIndexOutOfBoundsException`。 - -### null值,举个例子 -1. 列表大小为10,即size=0 -2. 线程A开始添加元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。也就是说,线程挂在了`element[0] = e`上。 -3. 接着线程B刚好也要开始天极爱一个值为B的元素,且走到了第一条的操作。此时线程B获取的size的值依然为0,于是它将B也放在了elementData下标为0的位置上。 -4. **问题来了**,其实上面也是问题,覆盖了。。。 -5. 线程A将size的值增加为1 -6. 线程B开始将size的值增加为2 -7. 当你获取1索引的时候,那不就是null了? - -## Fail-Fast机制 -ArrayList也采用了快速失败的机制,**通过记录modCount参数来实现**。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。 \ No newline at end of file diff --git "a/Java/codes/collection/BlockingQueue\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/collection/BlockingQueue\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index d8970013..00000000 --- "a/Java/codes/collection/BlockingQueue\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,158 +0,0 @@ -# BlockingQueue -看一下官方1.8的官方文档 -![已知实现类](https://imgkr.cn-bj.ufileos.com/1c08311d-77a7-4f60-a4dd-2e293f43c8d0.png) - -官方文档告诉我们它们有这些特点: -- BlockingQueue实现被设计为主要用于生产者 - 消费者队列 -- BlockingQueue实现是线程安全的。 所有排队方法使用内部锁或其他形式的并发控制在原子上实现其效果。 -- BlockingQueue方法有四种形式,具有不同的操作方式,不能立即满足,但可能在将来的某个时间点满足: - - 一个抛出异常 - - 返回一个特殊值( null或false ,具体取决于操作) - - 第三个程序将无限期地阻止当前线程,直到操作成功为止 - - 在放弃之前只有给定的最大时限。 - -## 看源码之旅 - -### ArrayBlockingQueue -> 看一下官方文档的解释->一个有限的blocking queue由数组支持。 这个队列排列元素FIFO(先进先出)。 队列的头部是队列中最长的元素。 队列的尾部是队列中最短时间的元素。 新元素插入队列的尾部,队列检索操作获取队列头部的元素。这是一个经典的“有界缓冲区”,其中固定大小的数组保存由生产者插入的元素并由消费者提取。 创建后,容量无法更改。 尝试put成满的队列的元件将导致在操作阻挡; 尝试take从空队列的元件将类似地阻塞。此类支持可选的公平策略,用于订购等待的生产者和消费者线程。 默认情况下,此订单不能保证。 然而,以公平设置为true的队列以FIFO顺序授予线程访问权限。 公平性通常会降低吞吐量,但会降低变异性并避免饥饿。 - -#### 常见变量 -```java -int count; // 队列元素数量 -final ReentrantLock lock; // 内部线程安全性用了ReentrantLock -private final Condition notEmpty; // takes方法的等待条件 -private final Condition notFull; // puts方法的等待条件 -``` -#### 构造方法 -```java -public ArrayBlockingQueue(int capacity, boolean fair) { - if (capacity <= 0) - throw new IllegalArgumentException(); // 这里就不说了 - this.items = new Object[capacity]; // 初始容量 - lock = new ReentrantLock(fair); // fair参数决定是否公平锁 - notEmpty = lock.newCondition(); // 上面已提到 - notFull = lock.newCondition(); // 上面已提到 -} -``` -#### add -```java -public boolean add(E e) { - return super.add(e); // 该方法添加失败抛出异常 -} -// AbstractQueue -public boolean add(E e) { - if (offer(e)) - return true; - else - throw new IllegalStateException("Queue full"); // 在这 -} -``` -**remove就不分析了** - -#### offer -```java -public boolean offer(E e) { - checkNotNull(e); - final ReentrantLock lock = this.lock; // 上面的add其实内部也调用了offer,当时我还觉得奇怪,add没上锁?。 原来offer上了锁的 - lock.lock(); - try { - if (count == items.length) - return false; // 满了,就false - else { - enqueue(e); // 否则,添加即可 - return true; // 返回true,并不会抛出异常 - } - } finally { - lock.unlock(); - } -} -``` -**poll不分析了** - -#### put -```java -public void put(E e) throws InterruptedException { - checkNotNull(e); // 检查是否为空,为空就抛出空异常 - final ReentrantLock lock = this.lock; // 上锁哦 - lock.lockInterruptibly(); // 锁中断 - try { - while (count == items.length) // 满了,挂起阻塞 - notFull.await(); - enqueue(e); // 否则添加 - } finally { - lock.unlock(); - } -} -``` - -#### take -```java -public E take() throws InterruptedException { - final ReentrantLock lock = this.lock; // 上锁 - lock.lockInterruptibly(); // 锁中断 - try { - while (count == 0) - notEmpty.await(); // 没有元素,挂起 - return dequeue(); - } finally { - lock.unlock(); - } -} -``` - -#### 带时间的offer和poll -> 其实就是用了`long nanos = unit.toNanos(timeout);` - - -### LinkedBlockingQueue -> 四个方法和ArrayBlockingQueue的差不多,就不多分析了。它的特点之一在于,如果不指定容量,那么默认是等于Integer.MAX_VALUE。可以看一下源码 - -#### 常见的参数 -```java -/** The capacity bound, or Integer.MAX_VALUE if none */ -private final int capacity; // 一看注释,就晓得了 -// 还有一些常见的和上面的差不多 -``` - -#### 构造方法 -```java -public LinkedBlockingQueue() { - this(Integer.MAX_VALUE); // 在这里, 嘿嘿嘿。 -} -``` - -### LinkedTransferQueue -> 基于链接节点的无界TransferQueue 。 这个队列相对于任何给定的生产者订购元素FIFO(先进先出)。 队列的头部是那些已经排队的元素是一些生产者的最长时间。 队列的尾部是那些已经在队列上的元素是一些生产者的最短时间。 - -**四个方法就暂时不提了,大部分操作用了cas并且要关注transfer方法** - -先介绍几个标志参数: -```java -private static final int NOW = 0; // for untimed poll, tryTransfer -private static final int ASYNC = 1; // for offer, put, add -private static final int SYNC = 2; // for transfer, take -private static final int TIMED = 3; // for timed poll, tryTransfer -``` - -```java -// 如果可能,立即将元素转移到等待的消费者。 -public boolean tryTransfer(E e) { - return xfer(e, true, NOW, 0) == null; // xfer的now参数 -} -// 还有一个重载参数的,带时间的。 -``` - -```java -// 将元素传输到消费者,必要时等待。 -public void transfer(E e) throws InterruptedException { - if (xfer(e, true, SYNC, 0) != null) { // SYNC - Thread.interrupted(); // failure possible only due to interrupt - throw new InterruptedException(); - } -} -``` -### PriorityBlockingQueue -> 一看名字就是优先级阻塞队列,它的优先级是由堆实现的,所以该类中有很多堆的方法。源码暂时就不看了,很多都是差不多的。 - -### SynchronousQueue -> 官方文档:每个插入操作必须等待另一个线程相应的删除操作,反之亦然。 同步队列没有任何内部容量,甚至没有一个容量。 个人感觉是生产者生产一个元素,消费者必须消费,生产者才能继续生产。内部也维护了一个TransferQueue,其中部分操作是利用cas。源码就不贴了。有兴趣的可以进去看看哦。 diff --git "a/Java/codes/collection/ConcurrentHashMap\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/collection/ConcurrentHashMap\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index 608032e8..00000000 --- "a/Java/codes/collection/ConcurrentHashMap\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,238 +0,0 @@ -# ConcurrentHashMap -> 1.7和1.8的源码还不太一样,也就是说加锁的方式不一样。 - -## 1.7 -> 分段锁 - -![图示意图](https://i.loli.net/2019/05/08/5cd1d2c5ce95c.jpg) - -### Segment的数据结构 -```java -/** - * Segment 数组,存放数据时首先需要定位到具体的 Segment 中。 - */ -final Segment[] segments; -transient Set keySet; -transient Set> entrySet; -``` - -Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下: - -```java -static final class Segment extends ReentrantLock implements Serializable { - private static final long serialVersionUID = 2249069246763182397L; - - // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶 - transient volatile HashEntry[] table; // volatile修饰 - transient int count; - transient int modCount; - transient int threshold; - final float loadFactor; - -} -``` -- 唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。 -- ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 `ReentrantLock`。 -- 不会像HashTable那样不管是put还是get操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。 -- **每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。** - -### put -```java -public V put(K key, V value) { - Segment s; - if (value == null) - throw new NullPointerException(); - int hash = hash(key); - int j = (hash >>> segmentShift) & segmentMask; - if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck - (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment - s = ensureSegment(j); - return s.put(key, hash, value, false); -} -``` -通过key定位到Segment,之后在对应的Segment中进行具体的put -```java -final V put(K key, int hash, V value, boolean onlyIfAbsent) { - HashEntry node = tryLock() ? null : - scanAndLockForPut(key, hash, value); // 1. 加锁处理 - V oldValue; - try { - HashEntry[] tab = table; - int index = (tab.length - 1) & hash; - HashEntry first = entryAt(tab, index); - for (HashEntry e = first;;) { - if (e != null) { - K k; - if ((k = e.key) == key || // 2. 遍历该HashEntry,如果不为空则判断传入的key和当前遍历的key是否相等,相等则覆盖旧的value - (e.hash == hash && key.equals(k))) { - oldValue = e.value; - if (!onlyIfAbsent) { - e.value = value; - ++modCount; - } - break; - } - e = e.next; - } - else { // 3. 不为空则需要新建一个HashEntry并加入到Segment中,同时会先判断是否需要扩容 - if (node != null) - node.setNext(first); - else - node = new HashEntry(hash, key, value, first); - int c = count + 1; - if (c > threshold && tab.length < MAXIMUM_CAPACITY) - rehash(node); - else - setEntryAt(tab, index, node); - ++modCount; - count = c; - oldValue = null; - break; - } - } - } finally { - unlock(); // 4. 解锁 - } - return oldValue; -} -``` -- 虽然HashEntry中的value是用volatile关键字修饰的,但是并不能保证并发的原子性,所以put操作仍然需要加锁处理。 -- 首先第一步的时候会尝试获取锁,如果获取失败肯定就是其他线程存在竞争,则利用 `scanAndLockForPut()` 自旋获取锁。 - - 尝试获取自旋锁 - - 如果重试的次数达到了`MAX_SCAN_RETRIES` 则改为阻塞锁获取,保证能获取成功。 - -总的来说: -- 将当前的Segment中的table通过key的hashcode定位到HashEntry -- 遍历该HashEntry,如果不为空则判断传入的key和当前遍历的key是否相等,相等则覆盖旧的value -- 不为空则需要新建一个HashEntry并加入到Segment中,同时会先判断是否需要扩容 -- 最后会解除在1中所获取当前Segment的锁。 - -### get -```java -public V get(Object key) { - Segment s; // manually integrate access methods to reduce overhead - HashEntry[] tab; - int h = hash(key); - long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; - if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && - (tab = s.table) != null) { - for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile - (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); - e != null; e = e.next) { - K k; - if ((k = e.key) == key || (e.hash == h && key.equals(k))) - return e.value; - } - } - return null; -} -``` -- 只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。 -- 由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。 -- ConcurrentHashMap 的 get 方法是非常高效的,**因为整个过程都不需要加锁。** - -## 1.8 -> 1.7 查询遍历链表效率太低。其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性 - -### put -```java - final V putVal(K key, V value, boolean onlyIfAbsent) { - if (key == null || value == null) throw new NullPointerException(); - int hash = spread(key.hashCode()); - int binCount = 0; - for (Node[] tab = table;;) { // 1. 根据key计算出hashcode - Node f; int n, i, fh; - if (tab == null || (n = tab.length) == 0) // 2. 判断是否需要进行初始化 - tab = initTable(); - else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 3. f即为当前key定位出的Node,如果为空表示当前位置可以写入数据,利用CAS尝试写入,失败则自旋保证成功。 - if (casTabAt(tab, i, null, - new Node(hash, key, value, null))) - break; // no lock when adding to empty bin - } - else if ((fh = f.hash) == MOVED) // 4. 如果当前位置的`hashcode == MOVED == -1`,则需要进行扩容 - tab = helpTransfer(tab, f); - else { - V oldVal = null; - synchronized (f) { // 5. 如果都不满足,则利用synchronized锁写入数据 - if (tabAt(tab, i) == f) { - if (fh >= 0) { - binCount = 1; - for (Node e = f;; ++binCount) { - K ek; - if (e.hash == hash && - ((ek = e.key) == key || - (ek != null && key.equals(ek)))) { - oldVal = e.val; - if (!onlyIfAbsent) - e.val = value; - break; - } - Node pred = e; - if ((e = e.next) == null) { - pred.next = new Node(hash, key, - value, null); - break; - } - } - } - else if (f instanceof TreeBin) { - Node p; - binCount = 2; - if ((p = ((TreeBin)f).putTreeVal(hash, key, - value)) != null) { - oldVal = p.val; - if (!onlyIfAbsent) - p.val = value; - } - } - } - } - if (binCount != 0) { - if (binCount >= TREEIFY_THRESHOLD) // 6. 如果数量大于`TREEIFY_THRESHOLD` 则要转换为红黑树。 - treeifyBin(tab, i); - if (oldVal != null) - return oldVal; - break; - } - } - } - addCount(1L, binCount); - return null; - } - -``` -- 根据key计算出hashcode -- 判断是否需要进行初始化 -- f即为当前key定位出的Node,如果为空表示当前位置可以写入数据,**利用CAS尝试写入,失败则自旋保证成功。** -- 如果当前位置的`hashcode == MOVED == -1`,则需要进行扩容 -- 如果都不满足,则利用`synchronized`锁写入数据 -- 如果数量大于TREEIFY_THRESHOLD 则要转换为红黑树。 - -### get -```java - public V get(Object key) { - Node[] tab; Node e, p; int n, eh; K ek; - int h = spread(key.hashCode()); - if ((tab = table) != null && (n = tab.length) > 0 && - (e = tabAt(tab, (n - 1) & h)) != null) { - if ((eh = e.hash) == h) { - if ((ek = e.key) == key || (ek != null && key.equals(ek))) - return e.val; - } - else if (eh < 0) - return (p = e.find(h, key)) != null ? p.val : null; - while ((e = e.next) != null) { - if (e.hash == h && - ((ek = e.key) == key || (ek != null && key.equals(ek)))) - return e.val; - } - } - return null; - } -``` -- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 -- 如果是红黑树那就按照树的方式获取值。 -- 就不满足那就按照链表的方式遍历获取值。 - -## 总结 -1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。 \ No newline at end of file diff --git "a/Java/codes/collection/HashMap\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/collection/HashMap\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index a1905e14..00000000 --- "a/Java/codes/collection/HashMap\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,173 +0,0 @@ -# HashMap -> 谈到这个,真的是面试高频呀,处处问,关键HashMap在jdk1.7和1.8的源码是我不一样的我这边就只放1.8的源码。 - -## 底层结构 - -> HashMap的底层结构是是**数组+链表**。关于为什么是链表,那是因为哈希冲突,采取链表一种方式。 - -## 常见参数 -```java -static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 初始容量 -static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子0.75 -// 给定的默认容量为16,负载因子为0.75. -// Map在使用过程中不断的往里面存放数据,当数量达到了16 * 0.75 = 12就需要将当前16的容量进行扩容, -// 而扩容这个过程涉及到rehash(重新哈希)、复制数据等操作,所有非常消耗性能。 -static final int TREEIFY_THRESHOLD = 8;// 成为红黑树的阈值,为什么是8? -static final int UNTREEIFY_THRESHOLD = 6; // 红黑树转链表阈值6 -static final int MIN_TREEIFY_CAPACITY = 64; -// 个人感觉,8是经验值,或者链表为8的时候,性能开始下降,大家知道链表查找慢,当元素过多的时候,非常影响性能的。 -// 因此采取为8的时候转成红黑树。元素过少,其实没必要直接红黑树,元素过少,链表查找的时间也不是很能影响性能。 -// 在 hash 函数设计合理的情况下,发生 hash 碰撞 8 次的几率为百万分之 6,概率说话。 -// 6是因为如果 hash 碰撞次数在 8 附近徘徊,会一直发生链表和红黑树的转化,为了预防这种情况的发生。 -``` - -## hash -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` -hash 函数是先拿到通过 key 的 hashcode,是 32 位的 int 值,然后让 hashcode 的高 16 位和低 16 位进行异或操作。这个也叫扰动函数,这么设计有二点原因: -- 一定要尽可能降低 hash 碰撞,越分散越好; -- 算法一定要尽可能高效,因为这是高频操作, 因此采用位运算; - -## put -> 这里介绍1.8的,1.7的其实很简单,被1.8包括了 -```java -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, - boolean evict) { - Node[] tab; Node p; int n, i; - if ((tab = table) == null || (n = tab.length) == 0) // 1. 判断当前桶是否为空,空的就需要初始化(resize中会判断是否进行初始化) - n = (tab = resize()).length; - if ((p = tab[i = (n - 1) & hash]) == null) // 2. 根据当前key的hashcode定位到具体的桶中并判断是否为空,为空则表明没有Hash冲突,就直接在当前位置创建一个新桶 - tab[i] = newNode(hash, key, value, null); - else { - Node e; K k; - if (p.hash == hash && // 3. 如果当前桶有值(Hash冲突),那么就要比较当前桶中的key、key的hashcode与写入的key是否相等,相等就赋值给e,在第8步的时候会统一进行赋值及返回 - ((k = p.key) == key || (key != null && key.equals(k)))) - e = p; - else if (p instanceof TreeNode) // 4. 如果当前桶为红黑树,那就要按照红黑树的方式写入数据 - e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); - else { // 5. 如果是个链表,就需要将当前的key、value封装称一个新节点写入到当前桶的后面形成链表。 - for (int binCount = 0; ; ++binCount) { - if ((e = p.next) == null) { - p.next = newNode(hash, key, value, null); - if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 6. 接着判断当前链表的大小是否大于预设的阈值,大于就要转换成为红黑树 -- - treeifyBin(tab, hash); - break; - } - if (e.hash == hash && // 7. 如果在遍历过程中找到key相同时直接退出遍历。 - ((k = e.key) == key || (key != null && key.equals(k)))) - break; - p = e; - } - } - if (e != null) { // existing mapping for key 8. 如果`e != null`就相当于存在相同的key,那就需要将值覆盖。 - V oldValue = e.value; - if (!onlyIfAbsent || oldValue == null) - e.value = value; - afterNodeAccess(e); - return oldValue; - } - } - ++modCount; - if (++size > threshold) // 9. 最后判断是否需要进行扩容。 - resize(); - afterNodeInsertion(evict); - return null; -} -``` -看代码有点晕乎! - -![put过程](https://imgkr.cn-bj.ufileos.com/b2206341-08ce-4e67-87bd-3a467ccac31e.png) - -1. 判断数组是否为空,为空进行初始化; -2. 不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index; -3. 查看 table[index] 是否存在数据,没有数据就构造一个 Node 节点存放在 table[index] 中; - -4. 存在数据,说明发生了 hash 冲突(存在二个节点 key 的 hash 值一样), 继续判断 key 是否相等,相等,用新的 value 替换原数据(onlyIfAbsent 为 false); - -5. 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中; -6. 如果不是树型节点,创建普通 Node 加入链表中;判断链表长度是否大于 8, 大于的话链表转换为红黑树; - -7. 插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。 - -## get -```java -public V get(Object key) { - Node e; - return (e = getNode(hash(key), key)) == null ? null : e.value; -} -final Node getNode(int hash, Object key) { - Node[] tab; Node first, e; int n; K k; - if ((tab = table) != null && (n = tab.length) > 0 && - (first = tab[(n - 1) & hash]) != null) { - if (first.hash == hash && // always check first node - ((k = first.key) == key || (key != null && key.equals(k)))) - return first; - if ((e = first.next) != null) { - if (first instanceof TreeNode) - return ((TreeNode)first).getTreeNode(hash, key); - do { - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } while ((e = e.next) != null); - } - } - return null; -} -``` -- 首先将key hash之后取得所定位的桶 -- 如果桶为空,则直接返回null -- 否则判断桶的第一个位置(有可能是链表、红黑树)的key是否为查询的key,是就直接返回value -- 如果第一个不匹配,则判断它的下一个是红黑树还是链表 -- 红黑树就按照树的查找方式返回值 -- 不然就按照链表的方式遍历匹配返回值 - -## 1.8的优化 -1. 数组+链表改成了数组+链表或红黑树; -2. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7 将新元素放到数组中,原始节点作为新节点的后继节点,1.8 遍历链表,将元素放置到链表的最后; -3. 扩容的时候 1.7 需要对原数组中的元素进行重新 hash 定位在新数组的位置,1.8 采用更简单的判断逻辑,位置不变或索引+旧容量大小; -4. 在插入时,1.7 先判断是否需要扩容,再插入,1.8 先进行插入,插入完成再判断是否需要扩容; - -## 并发问题 -- HashMap扩容的时候会调用resize()方法,就是这里的并发操作容易在一个桶上形成环形链表 -- 这样当获取一个不存在的key时,计算出的index正好是环形链表的下标就会出现死循环。 -- **但是1.7的头插法造成的问题,1.8改变了插入顺序,就解决了这个问题,但是为了内存可见性等安全性,还是需要ConCurrentHashMap** - -### Java 中有 HashTable、Collections.synchronizedMap、以及 ConcurrentHashMap 可以实现线程安全的 Map。 -- HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个数组,粒度比较大 -- Collections.synchronizedMap 是使用 Collections 集合工具的内部类,通过传入 Map 封装出一个 SynchronizedMap 对象,内部定义了一个对象锁,方法内通过对象锁实现 -- ConcurrentHashMap 使用分段锁,降低了锁粒度,让并发度大大提高。(jdk1.8 CAS+ synchronized) - -[死循环分析](https://zhuanlan.zhihu.com/p/67915754) - -## 哈希遍历 - -```java -Iterator> entryIterator = map.entrySet().iterator(); - while (entryIterator.hasNext()) { - Map.Entry next = entryIterator.next(); - System.out.println("key=" + next.getKey() + " value=" + next.getValue()); - } - -Iterator iterator = map.keySet().iterator(); - while (iterator.hasNext()){ - String key = iterator.next(); - System.out.println("key=" + key + " value=" + map.get(key)); - } -``` -- 建议使用第一种,同时可以把key value取出。 -- 第二种还需要通过key取一次key,效率较低。 - -# HashSet -> HashSet中不允许有重复元素,这是因为HashSet是基于HashMap实现的,HashSet中的元素都存放在HashMap的key上面,而value中的值都是统一的一个`private static final Object PRESENT = new Object();`。 HashSet跟HashMap一样,都是一个存放链表的数组。 - -# TreeMap -> TreeMap 是按照 Key 的自然顺序或者 Comprator 的顺序进行排序,内部是通过红黑树来实现。所以要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了 Comparator 接口的比较器,传给 TreeMap 用户 key 的比较。 - -# LinkedHashMap -> LinkedHashMap 内部维护了一个单链表,有头尾节点,同时 LinkedHashMap 节点 Entry 内部除了继承 HashMap 的 Node 属性,还有 before 和 after 用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。 \ No newline at end of file diff --git "a/Java/codes/collection/LinkedList\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/collection/LinkedList\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index 4a9ae73a..00000000 --- "a/Java/codes/collection/LinkedList\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,109 +0,0 @@ -# LinkedList -> LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack)。 关于栈或队列,现在的首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)有着更好的性能。 关于特点,想必大家都知道了。 - -## 底层数据 -> LinkedList底层通过双向链表实现。双向链表的每个节点用内部类Node表示。LinkedList通过**first和last引用分别指向链表的第一个和最后一个元素**。注意这里没有所谓的哑元,当链表为空的时候**first和last都指向null**。 -```java -transient int size = 0; -transient Node first; // 经常用 -transient Node last; // 经常用 -// Node是私有的内部类 -private static class Node { - E item; - Node next; // 这里就不多说了 - Node prev; // - - Node(Node prev, E element, Node next) { - this.item = element; - this.next = next; - this.prev = prev; - } -} -``` -构造函数就不用看了... - -## getFirst和getLast -> 这个挺简单的 -```java -public E getFirst() { - final Node f = first; // 第一个节点 - if (f == null) - throw new NoSuchElementException(); - return f.item; -} -public E getLast() { - final Node l = last; // 最后一个节点 - if (l == null) - throw new NoSuchElementException(); - return l.item; -} -``` - -**removeFirst和removeLast**和上面的源码差不多,挺简单。链表的删除,脑子里画图思考思考就明白了,添加也是一样。 - -## add调用的是`linkLast` -```java -void linkLast(E e) { - final Node l = last; // 要在尾部添加,所以要找last - final Node newNode = new Node<>(l, e, null); // last->e->null - last = newNode; - if (l == null) - first = newNode; // 如果l为空,说明没有链表没有元素,那么first和last都指向e, - else - l.next = newNode; // 否则就将l.next指向新节点 - size++; - modCount++; // 依然存在并发危险哦 -} -``` - -## 看一下remove -```java -public boolean remove(Object o) { - if (o == null) { // 将链表存在null的给移除掉 - for (Node x = first; x != null; x = x.next) { - if (x.item == null) { - unlink(x); - return true; - } - } - } else { - for (Node x = first; x != null; x = x.next) { - if (o.equals(x.item)) { // 否则直接遍历与符合当前对象,unlink即可。 - unlink(x); - return true; - } - } - } - return false; -} -``` -## unlink得看 -> 这个脑子思考一下那个图,其实就明白,很源码的话,也没问题的。 -```java -E unlink(Node x) { - // assert x != null; - final E element = x.item; // 当前值 - final Node next = x.next; // 后 - final Node prev = x.prev; // 前 - - if (prev == null) { // 如果x的前是null,说明,x是首节点,直接让first指向next - first = next; - } else { // x不是首节点,意味着pre是有值哦。 - prev.next = next; // prev.next的箭头指向x的next - x.prev = null; // 将x.prev指向null 双向链表? - } - - if (next == null) { // 同上,说明x是最后节点了 - last = prev; // 让last指向x的prev - } else { - next.prev = prev; // 否则x不是最后节点, 让next.prev 指向prev - x.next = null; // x.next 指向null , 双向链表 - } - - x.item = null; // GC回收 - size--; - modCount++; - return element; -} -``` -**其他几个方法的源码中核心都有这些...** \ No newline at end of file diff --git "a/Java/codes/collection/\346\211\213\345\206\231LRU\347\256\227\346\263\225.md" "b/Java/codes/collection/\346\211\213\345\206\231LRU\347\256\227\346\263\225.md" deleted file mode 100644 index 8995e5ea..00000000 --- "a/Java/codes/collection/\346\211\213\345\206\231LRU\347\256\227\346\263\225.md" +++ /dev/null @@ -1,73 +0,0 @@ -# LRU算法 -> LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。 - -## 一般过程 -![过程图](https://pic4.zhimg.com/80/v2-71b21233c615b1ce899cd4bd3122cbab_1440w.jpg) - -- 新数据插入到链表头部; -- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部; -- 当链表满的时候,将链表尾部的数据丢弃。 - -再来来个具体的过程 -![具体过程](https://pic3.zhimg.com/80/v2-998b52e7534278b364e439bbeaf61d5e_1440w.jpg) - -1. 最开始时,内存空间是空的,因此依次进入A、B、C是没有问题的 -2. 当加入D时,就出现了问题,内存空间不够了,因此根据LRU算法,内存空间中A待的时间最为久远,选择A,将其淘汰 -3. 当再次引用B时,内存空间中的B又处于活跃状态,而C则变成了内存空间中,近段时间最久未使用的 -4. 当再次向内存空间加入E时,这时内存空间又不足了,选择在内存空间中待的最久的C将其淘汰出内存,这时的内存空间存放的对象就是E->B->D - -## 伪代码实现 - -### 思路 -> 双向链表+HashMap,Java中的LinkedHashMap就实现了该算法。 - -### 定义节点 -```java -class Node { - int key; - int value; - Node pre; - Node next; -} -``` -### 定义HashMap -`HashMap map = new HashMap<>();` - -### get -> 其实挺简单的。 -```java -public int get(int key) { - if (map.containsKey(key)) { - Node n = map.get(key); // 获取内存中存在的值,比如A - remove(n); //使用链表的方法,移除该节点 - setHead(n); //依然使用链表的方法,将该节点放入头部 - return n.value; - } - return -1; -} -``` -- 由于当一个节点通过key来访问到这个节点,那么这个节点就是刚被访问了, -- 就把这个节点删除掉,然后放到队列头,这样队列的头部都是最近访问的, -- 队列尾部是最近没有被访问的。 - -### set -```java -public void set(int key, int value) { - if (map.containsKey(key)) { - Node old = map.get(key); - old.value = value; - remove(old); // 移除旧节点 - setHead(old); // 放到队头 - } else { - Node created = new Node(key, value); - if (map.size() >= capacity) { - map.remove(end.key); // clear该key - remove(end); //链表也是依次 - setHead(created); // 将created放入队头 - } else { - setHead(created); // 如果没满,直接放入队头 - } - map.put(key,created); - } -} -``` \ No newline at end of file diff --git "a/Java/codes/collection/\351\233\206\345\220\210\346\200\273\350\247\210.md" "b/Java/codes/collection/\351\233\206\345\220\210\346\200\273\350\247\210.md" deleted file mode 100644 index b0b28954..00000000 --- "a/Java/codes/collection/\351\233\206\345\220\210\346\200\273\350\247\210.md" +++ /dev/null @@ -1,15 +0,0 @@ -# 集合总述 -通过查看JDK8的Collection的文档源码: - -![有点凶](https://imgkr.cn-bj.ufileos.com/98753cd6-dba3-4291-83c3-7964b39c5da3.png) - -再看一Map的: - - -![也挺凶](https://imgkr.cn-bj.ufileos.com/0af46521-0eb6-4666-898e-3536cc3b7c4e.png) - -不说了,来个更清晰一点的: - -![清晰uml图](https://www.pdai.tech/_images/java_collections_overview.png) - -因此,容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。关于各自有什么特点,待后边带着源码详细介绍先。 \ No newline at end of file diff --git "a/Java/codes/jvm/\345\240\206\346\272\242\345\207\272\347\232\204\345\260\217\344\276\213\345\255\220.md" "b/Java/codes/jvm/\345\240\206\346\272\242\345\207\272\347\232\204\345\260\217\344\276\213\345\255\220.md" deleted file mode 100644 index 568af31e..00000000 --- "a/Java/codes/jvm/\345\240\206\346\272\242\345\207\272\347\232\204\345\260\217\344\276\213\345\255\220.md" +++ /dev/null @@ -1,26 +0,0 @@ -# 堆溢出例子 -> 启动参数:` -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError` - -```java -public class Test { - public static void main(String[] args) { - List list = new ArrayList<>(); - int i = 0; - while (true) { - list.add(new byte[5*1024*1024]); - System.out.println("分配次数:" + (++i)); - } - } -} -``` -结果: -``` -分配次数:1 -分配次数:2 -分配次数:3 -java.lang.OutOfMemoryError: Java heap space -Dumping heap to java_pid39413.hprof ... -Heap dump file created [16921133 bytes in 0.013 secs] -Exception in thread "main" java.lang.OutOfMemoryError: Java heap space - at Test.Test.main(Test.java:18) -``` \ No newline at end of file diff --git "a/Java/codes/jvm/\345\276\252\347\216\257\345\274\225\347\224\250\347\232\204\345\260\217\344\276\213\345\255\220.md" "b/Java/codes/jvm/\345\276\252\347\216\257\345\274\225\347\224\250\347\232\204\345\260\217\344\276\213\345\255\220.md" deleted file mode 100644 index 90dbf83d..00000000 --- "a/Java/codes/jvm/\345\276\252\347\216\257\345\274\225\347\224\250\347\232\204\345\260\217\344\276\213\345\255\220.md" +++ /dev/null @@ -1,14 +0,0 @@ -# 引用计数循环引用小例子 - -```java -public class Test { - public static void main(String[] args) { - Object object1 = new Object(); - Object object2 = new Object(); - object1 = object2; // obj1 引用obj2 - object2 = object1; // obj2 引用obj1 循环引用 - object1 = null; - object2 = null; - } -} -``` \ No newline at end of file diff --git "a/Java/codes/jvm/\346\237\245\347\234\213\345\217\215\347\274\226\350\257\221\346\272\220\347\240\201.md" "b/Java/codes/jvm/\346\237\245\347\234\213\345\217\215\347\274\226\350\257\221\346\272\220\347\240\201.md" deleted file mode 100644 index 10e93658..00000000 --- "a/Java/codes/jvm/\346\237\245\347\234\213\345\217\215\347\274\226\350\257\221\346\272\220\347\240\201.md" +++ /dev/null @@ -1,68 +0,0 @@ -# 查看反编译源码 - -## 例子 -JavaBean.java -```java -public class JavaBean { - int value = 3; - final int value2 = 5; - static int value3 = 8; - static int value4 = -1; - String s = "买"; - - void getValue() { - int temp = 4; - } -} -``` -- 终端输入`javac JavaBean.java`得到JavaBean.class文件 -- 终端继续输入`javap -c JavaBean > JavaBean.txt` - -``` -Compiled from "JavaBean.java" -public class JavaBean { - int value; - - final int value2; - - static int value3; - - static int value4; - - java.lang.String s; - - public JavaBean(); - Code: - 0: aload_0 - 1: invokespecial #1 // Method java/lang/Object."":()V - 4: aload_0 - 5: iconst_3 - 6: putfield #2 // Field value:I - 9: aload_0 - 10: iconst_5 - 11: putfield #3 // Field value2:I - 14: aload_0 - 15: ldc #4 // String 买 - 17: putfield #5 // Field s:Ljava/lang/String; - 20: return - - void getValue(); - Code: - 0: iconst_4 - 1: istore_1 - 2: return - - static {}; - Code: - 0: bipush 8 - 2: putstatic #6 // Field value3:I - 5: iconst_m1 - 6: putstatic #7 // Field value4:I - 9: return -} -``` -暂时就不详细解释了。 - -当然如果查看更信息反编译文件,终端可以输入`javap -p JavaBean > JavaBean.txt` -内容我就不放了,它可以查看更多的字面量等,内容比较多。 - diff --git "a/Java/codes/jvm/\346\240\210\346\272\242\345\207\272\347\232\204\345\260\217\344\276\213\345\255\220.md" "b/Java/codes/jvm/\346\240\210\346\272\242\345\207\272\347\232\204\345\260\217\344\276\213\345\255\220.md" deleted file mode 100644 index 829e98fa..00000000 --- "a/Java/codes/jvm/\346\240\210\346\272\242\345\207\272\347\232\204\345\260\217\344\276\213\345\255\220.md" +++ /dev/null @@ -1,15 +0,0 @@ -# 模拟栈溢出的小例子 -> 启动参数: `-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError` -```java -public class Test { - - public void m() { - System.out.println("stack test overflow..."); - m(); - } - - public static void main(String[] args) { - new T1().m(); - } -} -``` \ No newline at end of file diff --git "a/Java/codes/modes/\344\273\243\347\220\206\346\250\241\345\274\217.md" "b/Java/codes/modes/\344\273\243\347\220\206\346\250\241\345\274\217.md" deleted file mode 100644 index afaa720e..00000000 --- "a/Java/codes/modes/\344\273\243\347\220\206\346\250\241\345\274\217.md" +++ /dev/null @@ -1,86 +0,0 @@ -## 代理模式 - -> 所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象。 - 一般是因为客户端不想直接访问实际的对象,或者访问实际的对象存在困难,因此通过一个代理对象来完成间接的访问。 - -### 静态代理 - -> 定义主题 - -```java -interface Subject { - void visit(); -} -``` - -> 实现subject的两个类 - -```java -class RealSubject implements Subject { - private String name = "dreamcat"; - @Override - public void visit() { - System.out.println(name); - } -} -``` -> 代理类 -```java -class ProxySubject implements Subject { - - private Subject subject; - - public ProxySubject(Subject subject) { - this.subject = subject; - } - - @Override - public void visit() { - subject.visit(); - } -} -``` -缺点: -> 代理类接受一个Subject接口的对象,任何实现该接口的对象,都可以通过代理类进行代理,增加了通用性。 - 每一个代理类都必须实现一遍委托类(也就是realsubject)的接口,如果接口增加方法,则代理类也必须跟着修改。 - 代理类每一个接口对象对应一个委托对象,如果委托对象非常多,则静态代理类就非常臃肿,难以胜任。 - -### 动态代理 -> 动态代理是实现方式,是通过反射来实现的,借助Java自带的java.lang.reflect.Proxy,通过固定的规则生成。 - -> 动态代理 - -```java -class DynamicProxy implements InvocationHandler { - - private Object object; - - public DynamicProxy(Object object) { - this.object = object; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - Object result = method.invoke(object, args); - return result; - } -} -``` - -### 测试 -```java -public class ProxyMode { - public static void main(String[] args) { - // 静态代理 - ProxySubject proxySubject = new ProxySubject(new RealSubject()); - proxySubject.visit(); - - // 动态代理 - RealSubject realSubject = new RealSubject(); - DynamicProxy dynamicProxy = new DynamicProxy(realSubject); - ClassLoader classLoader = realSubject.getClass().getClassLoader(); - Subject subject = (Subject) Proxy.newProxyInstance(classLoader, new Class[]{Subject.class}, dynamicProxy); - subject.visit(); - } -} -``` diff --git "a/Java/codes/modes/\345\215\225\344\276\213\346\250\241\345\274\217.md" "b/Java/codes/modes/\345\215\225\344\276\213\346\250\241\345\274\217.md" deleted file mode 100644 index d74b210b..00000000 --- "a/Java/codes/modes/\345\215\225\344\276\213\346\250\241\345\274\217.md" +++ /dev/null @@ -1,87 +0,0 @@ -## 单例模式 -> 单例就不多说了 - -### 饿汉 - -```java -public class Singleton { - private static Singleton instance = new Singleton(); - private Singleton(){} - - public static Singleton getInstance() { - return instance; - } -} -``` -### 饿汉变种 - -```java -class Singleton { - private static Singleton instance = null; - static { - instance = new Singleton(); - } - private Singleton() {} - - public static Singleton getInstance() { - return instance; - } -} -``` - -### 懒汉(线程不安全) - -```java -class Singleton { - private static Singleton instance = null; - private Singleton(){} - - public static Singleton getInstance() { - if (instance == null){ - instance = new Singleton(); - } - return instance; - - } -} -``` - -### 懒汉(线程安全,但消耗资源较为严重) - -```java -class Singleton { - private static Singleton instance = null; - - private Singleton() { - } - - public static synchronized Singleton getInstance() { - if (instance == null) { - instance = new Singleton(); - } - return instance; - } -} -``` - -### 懒汉(线程安全,双重校验) - -```java -class Singleton { - private static volatile Singleton instance = null; - - private Singleton() { - } - - public static Singleton getInstance() { - if (instance == null) { - synchronized (Singleton.class) { - if (instance == null) { - instance = new Singleton(); - } - } - } - return instance; - } -} -``` \ No newline at end of file diff --git "a/Java/codes/modes/\345\267\245\345\216\202\346\250\241\345\274\217.md" "b/Java/codes/modes/\345\267\245\345\216\202\346\250\241\345\274\217.md" deleted file mode 100644 index 3ef26dba..00000000 --- "a/Java/codes/modes/\345\267\245\345\216\202\346\250\241\345\274\217.md" +++ /dev/null @@ -1,81 +0,0 @@ -## 工厂模式 -> 大概意思就不要说了,直接举个例子,看例子讲解就知道是什么意思了。 - -### 举例子 - -> 定义一个面条抽象类 - -```java -abstract class INoodles { - /** - * 描述每种面条长什么样的... - */ - public abstract void desc(); -} -``` - -> 定义一份兰州拉面(具体产品) - -```java -class LzNoodles extends INoodles { - - @Override - public void desc() { - System.out.println("兰州拉面,成都的好贵 家里的才5-6块钱一碗"); - } -} -``` - -> 定义一份泡面(程序员挺喜欢的) - -```java -class PaoNoodles extends INoodles { - - @Override - public void desc() { - System.out.println("泡面可还行..."); - } -} -``` - -> 不得不说家乡的杂酱面了,好吃得不得了 - -```java -class ZaNoodles extends INoodles { - - @Override - public void desc() { - System.out.println("杂酱面,嗯? 真香..."); - } -} -``` - -> 重头戏,开面条馆了。(工厂) - -```java -class SimpleNoodlesFactory { - public static final int TYPE_LZ = 1; // 兰州拉面 - public static final int TYPE_PAO = 2; // 泡面撒 - public static final int TYPE_ZA = 3; // 杂酱面 - // 提供静态方法 - public static INoodles createNoodles(int type) { - switch (type) { - case TYPE_LZ:return new LzNoodles(); - case TYPE_PAO:return new PaoNoodles(); - case TYPE_ZA:return new ZaNoodles(); - default:return new ZaNoodles(); - } - } -} -``` - -> 测试 - -```java -public class FactoryMode { - public static void main(String[] args) { - INoodles noodles = SimpleNoodlesFactory.createNoodles(SimpleNoodlesFactory.TYPE_ZA); - noodles.desc(); - } -} -``` \ No newline at end of file diff --git "a/Java/codes/modes/\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" "b/Java/codes/modes/\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" deleted file mode 100644 index cb19467b..00000000 --- "a/Java/codes/modes/\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" +++ /dev/null @@ -1,63 +0,0 @@ -## 模板方法模式 - -> 定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的 - -例如: -> 去银行办业务,银行给我们提供了一个模板就是:先取号,排对,办理业务(核心部分我们子类完成),给客服人员评分,完毕。 - 这里办理业务是属于子类来完成的,其他的取号,排队,评分则是一个模板。 - -再例如: -> 去餐厅吃饭,餐厅给提供的一套模板就是:先点餐,等待,吃饭(核心部分我们子类完成),买单 - 这里吃饭是属于子类来完成的,其他的点餐,买单则是餐厅提供给我们客户的一个模板。 - -所以: -> 实现一些操作时,整体步骤很固定,但是呢。就是其中一小部分容易变,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。 - - -### 举例子 -> 银行办理业务抽象类 - -```java -abstract class BankTemplateMethod { - // 1. 取号排队 - public void takeNumber() { - System.out.println("取号排队..."); - } - - // 2. 每个子类不同的业务实现,各由子类来实现 - abstract void transact(); - - // 3. 评价 - public void evaluate() { - System.out.println("反馈评价..."); - } - - public void process() { - takeNumber(); - transact(); - evaluate(); - } -} -``` - -> 具体的业务,比如取钱 -```java -class DrawMoney extends BankTemplateMethod { - - @Override - void transact() { - System.out.println("我要取款..."); - } -} -``` - -### 测试 - -```java -public class TemplateMode { - public static void main(String[] args) { - BankTemplateMethod drawMoney = new DrawMoney(); - drawMoney.process(); - } -} -``` \ No newline at end of file diff --git "a/Java/codes/modes/\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217.md" "b/Java/codes/modes/\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217.md" deleted file mode 100644 index 1513ee9d..00000000 --- "a/Java/codes/modes/\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217.md" +++ /dev/null @@ -1,136 +0,0 @@ -## 装饰器模式 - -> 按照单一职责原则,某一个对象只专注于干一件事,而如果要扩展其职能的话,不如想办法分离出一个类来“包装”这个对象,而这个扩展出的类则专注于实现扩展功能。 - 装饰器模式就可以将新功能动态地附加于现有对象而不改变现有对象的功能。 - -> 代理模式专注于对被代理对象的访问; - 装饰器模式专注于对被装饰对象附加额外功能。 - -### 举例子 -> 假设我去买咖啡,首先服务员给我冲了一杯原味咖啡,我希望服务员给我加些牛奶和白糖混合入原味咖啡中。使用装饰器模式就可以解决这个问题。 - -> 咖啡接口 - -```java -interface Coffee { - // 获取价格 - double getCost(); - - // 获取配料 - String getIngredients(); -} -``` - -> 原味咖啡 - -```java -class SimpleCoffee implements Coffee { - - @Override - public double getCost() { - return 1; - } - - @Override - public String getIngredients() { - return "Coffee"; - } -} -``` -> 咖啡对象的装饰器类 -```java -abstract class CoffeeDecorator implements Coffee { - protected final Coffee decoratedCoffee; - - /** - * 在构造方法中,初始化咖啡对象的引用 - */ - - protected CoffeeDecorator(Coffee coffee) { - this.decoratedCoffee = coffee; - } - - /** - * 装饰器父类中直接转发"请求"至引用对象 - */ - public double getCost() { - return decoratedCoffee.getCost(); - } - - public String getIngredients() { - return decoratedCoffee.getIngredients(); - } -} -``` - -注意: -> 具体的装饰器类,负责往咖啡中“添加”牛奶,注意看getCost()方法和getIngredients()方法,可以在转发请求之前或者之后,增加功能。 - 如果是代理模式,这里的结构就有所不同,通常代理模式根据运行时的条件来判断是否转发请求。 - - -> 混合牛奶 - -```java -class WithMilk extends CoffeeDecorator { - - public WithMilk(Coffee coffee) { - super(coffee); - } - - @Override - public double getCost() { - double additionalCost = 0.5; - return super.getCost() + additionalCost; - } - - @Override - public String getIngredients() { - String additionalIngredient = "milk"; - return super.getIngredients() + ", " + additionalIngredient; - } -} -``` - -```java -class WithSugar extends CoffeeDecorator { - - public WithSugar(Coffee coffee) { - super(coffee); - } - - @Override - public double getCost() { - return super.getCost() + 1; - } - - @Override - public String getIngredients() { - return super.getIngredients() + ", Sugar"; - } -} -``` - -### 测试 -```java -public class DecoratorMode { - static void print(Coffee c) { - System.out.println("花费了: " + c.getCost()); - System.out.println("配料: " + c.getIngredients()); - System.out.println("============"); - } - - public static void main(String[] args) { - //原味咖啡 - Coffee c = new SimpleCoffee(); - print(c); - - //增加牛奶的咖啡 - c = new WithMilk(c); - print(c); - - //再加一点糖 - c = new WithSugar(c); - print(c); - } -} -``` \ No newline at end of file diff --git "a/Java/codes/modes/\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" "b/Java/codes/modes/\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" deleted file mode 100644 index 4e209295..00000000 --- "a/Java/codes/modes/\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" +++ /dev/null @@ -1,113 +0,0 @@ -## 观察者模式 -> 在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新。 - 其实就是发布订阅模式,发布者发布信息,订阅者获取信息,订阅了就能收到信息,没订阅就收不到信息。 -> 四个角色 -> - 抽象被观察者角色 -> - 抽象观察者角色 -> - 具体被观察者角色 -> - 具体观察者角色 - -### 举例子(如:微信公众号) - -> 定义一个抽象被观察者接口,声明了添加、删除、通知观察者方法。 - -```java -interface Observerable { - void registerObserver(Observer o); - void removeObserver(Observer o); - void notifyObserver(); -} -``` - -> 定义一个抽象观察者接口,定义了一个update()方法,当被观察者调用notifyObservers()方法时,观察者的update()方法会被回调。 - -```java -interface Observer { - void update(String message); -} -``` - -> 定义被观察者,实现三个接口,并且list保存注册的Observer - -```java -class WechatObserver implements Observerable { - - private List list; - private String message; - - public WechatObserver() { - list = new ArrayList(); - } - - @Override - public void registerObserver(Observer o) { - list.add(o); - } - - @Override - public void removeObserver(Observer o) { - list.remove(o); - } - - @Override - public void notifyObserver() { - for (Observer observer : list) { - observer.update(message); - } - } - - public void setMessage(String message) { - this.message = message; - System.out.println("微信服务更新消息:" + message); - // 消息更新,通知所有观察者 - notifyObserver(); - } -} -``` -> 定义具体观察者,User - -```java -class User implements Observer { - - private String name; - private String message; - - public User(String name) { - this.name = name; - } - - - @Override - public void update(String message) { - this.message = message; - read(); - } - - void read() { - System.out.println(name+ " 收到推送消息:" + message); - } -} -``` - -### 测试 - -```java -public class ObserverMode { - public static void main(String[] args) { - WechatObserver wechatObserver = new WechatObserver(); - User maifeng = new User("Maifeng"); - User xiaofeng = new User("Xiaofeng"); - User fengfeng = new User("Fengfeng"); - - wechatObserver.registerObserver(maifeng); - wechatObserver.registerObserver(xiaofeng); - wechatObserver.registerObserver(fengfeng); - wechatObserver.setMessage("PHP是世界上最好的语言..."); - - System.out.println("---------------"); - - wechatObserver.removeObserver(fengfeng); - wechatObserver.setMessage("JAVA是世界上最好的语言..."); - } -} -``` \ No newline at end of file diff --git "a/Java/codes/mysql/InnoDB\345\222\214MyISAM.md" "b/Java/codes/mysql/InnoDB\345\222\214MyISAM.md" deleted file mode 100644 index 2e0e9b92..00000000 --- "a/Java/codes/mysql/InnoDB\345\222\214MyISAM.md" +++ /dev/null @@ -1,22 +0,0 @@ -# InnoDB和MyISAM - -## InnoDB -- 是 MySQL 默认的**事务型存储引擎**,只有在需要它不支持的特性时,才考虑使用其它存储引擎。 -- 实现了四个标准的隔离级别,默认级别是**可重复读(REPEATABLE READ)**。在可重复读隔离级别下,通过**多版本并发控制**(MVCC)+ **间隙锁**(Next-Key Locking)**防止幻影读**。 -- 主索引是**聚簇索引**,在**索引中保存了数据**,从而避免直接读取磁盘,因此对查询性能有很大的提升。 -- 内部做了很多优化,包括从磁盘读取数据时采用的**可预测性读**、能够加快读操作并且自动创建的**自适应哈希索引**、能够加速插入操作的**插入缓冲区**等。 -- 支持真正的在**线热备份**。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 - -## MyISAM -- 设计简单,数据以**紧密格式存储**。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。 -- 提供了大量的特性,包括**压缩表、空间数据索引**等。 -- **不支持事务**。 -- **不支持行级锁,只能对整张表加锁**,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。 - -## 简单对比 -- **事务**: InnoDB 是事务型的,可以使用 `Commit` 和 `Rollback` 语句。 -- **并发**: MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 -- **外键**: InnoDB 支持外键。 -- **备份**: InnoDB 支持在线热备份。 -- **崩溃恢复**: MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 -- **其它特性**: MyISAM 支持压缩表和空间数据索引。 \ No newline at end of file diff --git "a/Java/codes/mysql/MVCC\347\232\204\347\274\272\347\202\271.md" "b/Java/codes/mysql/MVCC\347\232\204\347\274\272\347\202\271.md" deleted file mode 100644 index 672aea39..00000000 --- "a/Java/codes/mysql/MVCC\347\232\204\347\274\272\347\202\271.md" +++ /dev/null @@ -1,8 +0,0 @@ -# MVCC的缺点 -> MVCC在大多数情况下代替了行锁,实现了对读的非阻塞,读不加锁,读写不冲突。缺点是每行记录都需要额外的存储空间,需要做更多的行维护和检查工作。 要知道的,MVCC机制下,会在更新前建立undo log,根据各种策略读取时非阻塞就是MVCC,undo log中的行就是MVCC中的多版本。 而undo log这个关键的东西,记载的内容是串行化的结果,记录了多个事务的过程,不属于多版本共存。 这么一看,似乎mysql的mvcc也并没有所谓的多版本共存 - -## InnoDB的实现 -1. 事务以**排他锁**的形式修改原始数据 -2. 把修改前的数据存放于**undo log**,通过回滚指针与主数据关联 -3. 修改成功(commit),数据放到**redo log**中,失败则恢复**undo log**中的数据(rollback) - diff --git "a/Java/codes/mysql/MySQL\346\230\257\345\246\202\344\275\225\346\211\247\350\241\214\344\270\200\346\235\241SQL\350\257\255\345\217\245\347\232\204.md" "b/Java/codes/mysql/MySQL\346\230\257\345\246\202\344\275\225\346\211\247\350\241\214\344\270\200\346\235\241SQL\350\257\255\345\217\245\347\232\204.md" deleted file mode 100644 index 92c0c654..00000000 --- "a/Java/codes/mysql/MySQL\346\230\257\345\246\202\344\275\225\346\211\247\350\241\214\344\270\200\346\235\241SQL\350\257\255\345\217\245\347\232\204.md" +++ /dev/null @@ -1,20 +0,0 @@ -# MySQL是如何执行一条SQL的 - -先附上一张图: - -![执行过程图](http://media.dreamcat.ink/uPic/SQL%E6%89%A7%E8%A1%8C%E7%9A%84%E5%85%A8%E9%83%A8%E8%BF%87%E7%A8%8B.png) - -**MySQL内部可以分为服务层和存储引擎层两部分:** -1. **服务层包括连接器、查询缓存、分析器、优化器、执行器等**,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。 -2. **存储引擎层负责数据的存储和提取**,其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认的存储引擎。 - -**Server层按顺序执行sql的步骤为**: -客户端请求->**连接器**(验证用户身份,给予权限) -> **查询缓存**(存在缓存则直接返回,不存在则执行后续操作)->**分析器**(对SQL进行词法分析和语法分析操作) -> **优化器**(主要对执行的sql优化选择最优的执行方案方法) -> **执行器**(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口)->**去引擎层获取数据返回**(如果开启查询缓存则会缓存查询结果) - -简单概括: -- **连接器**:管理连接、权限验证; -- **查询缓存**:命中缓存则直接返回结果; -- **分析器**:对SQL进行词法分析、语法分析;(判断查询的SQL字段是否存在也是在这步) -- **优化器**:执行计划生成、选择索引; -- **执行器**:操作引擎、返回结果; -- **存储引擎**:存储数据、提供读写接口。 \ No newline at end of file diff --git "a/Java/codes/mysql/MySQL\347\232\204ACID\345\216\237\347\220\206.md" "b/Java/codes/mysql/MySQL\347\232\204ACID\345\216\237\347\220\206.md" deleted file mode 100644 index db848b0d..00000000 --- "a/Java/codes/mysql/MySQL\347\232\204ACID\345\216\237\347\220\206.md" +++ /dev/null @@ -1,74 +0,0 @@ -# MySQL的ACID原理 - -> ACID嘛,原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)! - -## 举例子 -我们以从A账户转账50元到B账户为例进行说明一下ACID,四大特性。 - -### 原子性 -> 根据定义,原子性是指一个事务是一个不可分割的工作单位,**其中的操作要么都做,要么都不做**。即要么转账成功,要么转账失败,是不存在中间的状态! - -**如果无法保证原子性会怎么样?** - -OK,就会出现数据不一致的情形,A账户减去50元,而B账户增加50元操作失败。系统将无故丢失50元~ - -### 隔离性 -> 根据定义,隔离性是指**多个事务并发执行的时候,事务内部的操作与其他事务是隔离的**,并发执行的各个事务之间不能互相干扰。 - -**如果无法保证隔离性会怎么样?** - -OK,假设A账户有200元,B账户0元。A账户往B账户转账两次,金额为50元,分别在两个事务中执行。如果无法保证隔离性,会出现下面的情形 - - -![事务隔离](https://imgkr.cn-bj.ufileos.com/b3d04d1e-6bf1-4d49-b670-7e8a4801b385.png) - -如图所示,如果不保证隔离性,A扣款两次,而B只加款一次,凭空消失了50元,依然出现了数据不一致的情形! - -### 持久性 -> 根据定义,**持久性是指事务一旦提交,它对数据库的改变就应该是永久性的**。接下来的其他操作或故障不应该对其有任何影响。 - -**如果无法保证持久性会怎么样?** - -在MySQL中,为了解决CPU和磁盘速度不一致问题,MySQL是将磁盘上的数据加载到内存,对内存进行操作,然后再回写磁盘。好,假设此时宕机了,在内存中修改的数据全部丢失了,持久性就无法保证。 - -设想一下,系统提示你转账成功。但是你发现金额没有发生任何改变,此时数据出现了不合法的数据状态,我们将这种状态认为是**数据不一致**的情形。 - -### 一致性 -> 根据定义,一致性是指事务执行前后,数据处于一种合法的状态,这种状态是语义上的而不是语法上的。 那什么是合法的数据状态呢? oK,这个状态是满足预定的约束就叫做合法的状态,再通俗一点,这状态是由你自己来定义的。**满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的!** - -**如果无法保证一致性会怎么样?** - -- 例一:A账户有200元,转账300元出去,此时A账户余额为-100元。你自然就发现了此时数据是不一致的,为什么呢?因为你定义了一个状态,余额这列必须大于0。 -- 例二:A账户200元,转账50元给B账户,A账户的钱扣了,但是B账户因为各种意外,余额并没有增加。你也知道此时数据是不一致的,为什么呢?因为你定义了一个状态,要求A+B的余额必须不变。 - -## 如何保证 - -### 保证一致性 -OK,这个问题分为两个层面来说。 - -- **从数据库层面**,数据库通过原子性、隔离性、持久性来保证一致性。也就是说ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段。**数据库必须要实现AID三大特性,才有可能实现一致性**。例如,原子性无法保证,显然一致性也无法保证。 -- **从应用层面**,通过代码判断数据库数据是否有效,然后决定回滚还是提交数据! - -### 保证原子性 -OK,是利用Innodb的**undo log**。 **undo log**名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的sql语句,他需要记录你要回滚的相应日志信息。 例如 -- 当你delete一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert这条旧数据 -- 当你update一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行update操作 -- 当年insert一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行delete操作 - -**undo log**记录了这些回滚需要的信息,当事务执行失败或调用了**rollback**,导致事务需要回滚,便可以利用**undo log**中的信息将数据回滚到修改之前的样子。 - -### 保证持久性 -OK,是利用Innodb的**redo log**。 正如之前说的,MySQL是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上。如果此时突然宕机,内存中的数据就会丢失。 怎么解决这个问题? 简单啊,事务提交前直接把数据写入磁盘就行啊。 这么做有什么问题? -- 只修改一个页面里的一个字节,就要将整个页面刷入磁盘,太浪费资源了。毕竟一个页面16kb大小,你只改其中一点点东西,就要将16kb的内容刷入磁盘,听着也不合理。 -- 毕竟一个事务里的SQL可能牵涉到多个数据页的修改,而这些数据页可能不是相邻的,也就是属于随机IO。显然操作随机IO,速度会比较慢。 - -于是,决定采用**redo log**解决上面的问题。当做数据修改的时候,不仅在内存中操作,还会在**redo log**中记录这次操作。当事务提交的时候,会将**redo log**日志进行刷盘(**redo log**一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据**undo log**和**binlog**内容决定回滚数据还是提交数据。 - -**采用redo log的好处?** - -其实好处就是将**redo log**进行刷盘比对数据页刷盘效率高,具体表现如下: -- **redo log**体积小,毕竟只记录了哪一页修改了啥,因此体积小,刷盘快。 -- **redo log**是一直往末尾进行追加,属于顺序IO。效率显然比随机IO来的快。 - -### 保证隔离 -**利用的是锁和MVCC机制。** \ No newline at end of file diff --git "a/Java/codes/mysql/MySQL\350\257\273\345\206\231\345\210\206\347\246\273\344\270\273\344\273\216\345\244\215\345\210\266\345\216\237\347\220\206.md" "b/Java/codes/mysql/MySQL\350\257\273\345\206\231\345\210\206\347\246\273\344\270\273\344\273\216\345\244\215\345\210\266\345\216\237\347\220\206.md" deleted file mode 100644 index acd6d144..00000000 --- "a/Java/codes/mysql/MySQL\350\257\273\345\206\231\345\210\206\347\246\273\344\270\273\344\273\216\345\244\215\345\210\266\345\216\237\347\220\206.md" +++ /dev/null @@ -1,11 +0,0 @@ -# MySQL读写分离主从复制原理 - -主库(master)将变更写**binlog**日志,然后从库(slave)连接到主库之后,从库有一个**IO线程**,将主库的binlog日志**拷贝到自己本地**,写入一个中继日志中。接着从库中有一个SQL线程会从中继日志读取binlog,然后执行binlog日志中的内容,也就是在自己本地再次执行一遍SQL,这样就可以保证自己跟主库的数据是一样的。 - -这里有一个非常重要的一点,就是从库同步主库数据的过程是**串行化**的,也就是说**主库上并行**的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行SQL的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。 - -而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。 - -所以mysql实际上在这一块有两个机制,一个是**半同步复制**,用来解决主库数据丢失问题;一个是**并行复制**,用来解决主从同步延时问题。 - -所谓并行复制,指的是从库**开启多个线程,并行读取relay log中不同库的日志**,然后并行重放不同库的日志,这是库级别的并行。 \ No newline at end of file diff --git "a/Java/codes/mysql/\345\271\266\345\217\221\344\272\213\345\212\241\345\270\246\346\235\245\347\232\204\351\227\256\351\242\230.md" "b/Java/codes/mysql/\345\271\266\345\217\221\344\272\213\345\212\241\345\270\246\346\235\245\347\232\204\351\227\256\351\242\230.md" deleted file mode 100644 index 7f910eab..00000000 --- "a/Java/codes/mysql/\345\271\266\345\217\221\344\272\213\345\212\241\345\270\246\346\235\245\347\232\204\351\227\256\351\242\230.md" +++ /dev/null @@ -1,51 +0,0 @@ -# 并发事务带来的问题 - -## 脏读 - -![脏读](https://www.pdai.tech/_images/pics/dd782132-d830-4c55-9884-cfac0a541b8e.png) - -第一个事务首先读取var变量为50,接着准备更新为100的时,并未提交,第二个事务已经读取var为100,此时第一个事务做了回滚。最终第二个事务读取的var和数据库的var不一样。 - -## 丢弃修改 -![丢弃修改](https://www.pdai.tech/_images/pics/88ff46b3-028a-4dbb-a572-1f062b8b96d3.png) - -T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。例如:事务1读取某表中的数据A=50,事务2也读取A=50,事务1修改A=A+50,事务2也修改A=A+50,最终结果A=100,事务1的修改被丢失。 - -## 不可重复读 - -![不可重复读](https://www.pdai.tech/_images/pics/c8d18ca9-0b09-441a-9a0c-fb063630d708.png) - -T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 - -## 幻读 - -![幻读](https://www.pdai.tech/_images/pics/72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png) - -T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。 - -## 不可重复读和幻读区别 -**不可重复读的重点是修改,幻读的重点在于新增或者删除。** - -- 例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导致A再读自己的工资时工资变为 2000;这就是不可重复读。 -- 例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记 录就变为了5条,这样就导致了幻读。 - -## 隔离级别 -- **未提交读**,事务中发生了修改,即使没有提交,其他事务也是可见的,比如对于一个数A原来50修改为100,但是我还没有提交修改,另一个事务看到这个修改,而这个时候原事务发生了回滚,这时候A还是50,但是另一个事务看到的A是100.**可能会导致脏读、幻读或不可重复读** -- **提交读**,对于一个事务从开始直到提交之前,所做的任何修改是其他事务不可见的,举例就是对于一个数A原来是50,然后提交修改成100,这个时候另一个事务在A提交修改之前,读取的A是50,刚读取完,A就被修改成100,这个时候另一个事务再进行读取发现A就突然变成100了;**可以阻止脏读,但是幻读或不可重复读仍有可能发生** -- **重复读**,就是对一个记录读取多次的记录是相同的,比如对于一个数A读取的话一直是A,前后两次读取的A是一致的;**可以阻止脏读和不可重复读,但幻读仍有可能发生** -- **可串行化读**,在并发情况下,和串行化的读取的结果是一致的,没有什么不同,比如不会发生脏读和幻读;**该级别可以防止脏读、不可重复读以及幻读** - -| 隔离级别 | 脏读 | 不可重复读 | 幻影读 | -| ---------------- | ---- | ---------- | ------ | -| READ-UNCOMMITTED | √ | √ | √ | -| READ-COMMITTED | × | √ | √ | -| REPEATABLE-READ | × | × | √ | -| SERIALIZABLE | × | × | × | - -MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ**(可重读) - -> **这里需要注意的是**:与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别 下使用的是**Next-Key Lock 锁**算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以 说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要 求,即达到了 SQL标准的SERIALIZABLE(可串行化)隔离级别。 - -因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内 容):,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)并不会有任何性能损失**。 - -InnoDB 存储引擎在分布式事务 的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。 \ No newline at end of file diff --git "a/Java/codes/mysql/\346\225\260\346\215\256\345\272\223\347\273\223\346\236\204\344\274\230\345\214\226.md" "b/Java/codes/mysql/\346\225\260\346\215\256\345\272\223\347\273\223\346\236\204\344\274\230\345\214\226.md" deleted file mode 100644 index 0649118a..00000000 --- "a/Java/codes/mysql/\346\225\260\346\215\256\345\272\223\347\273\223\346\236\204\344\274\230\345\214\226.md" +++ /dev/null @@ -1,26 +0,0 @@ -# 数据库结构优化 -- **范式优化**: 比如消除冗余(节省空间。。) -- **反范式优化**:比如适当加冗余等(减少join) -- **限定数据的范围**: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内。 -- **读/写分离**: 经典的数据库拆分方案,主库负责写,从库负责读; -- **拆分表**:分区将数据在物理上分隔开,不同分区的数据可以制定保存在处于不同磁盘上的数据文件里。这样,当对这个表进行查询时,只需要在表分区中进行扫描,而不必进行全表扫描,明显缩短了查询时间,另外处于不同磁盘的分区也将对这个表的数据传输分散在不同的磁盘I/O,一个精心设置的分区可以将数据传输对磁盘I/O竞争均匀地分散开。对数据量大的时时表可采取此方法。可按月自动建表分区。 - -## 拆分表 -**拆分其实又分垂直拆分和水平拆分**: - -举例子: - -简单购物系统暂设涉及如下表: -1. 产品表(数据量10w,稳定) -2. 订单表(数据量200w,且有增长趋势) -3. 用户表 (数据量100w,且有增长趋势) - -### 垂直拆分 -- 解决问题:表与表之间的io竞争 -- 不解决问题:单表中数据量增长出现的压力 -- 方案: 把产品表和用户表放到一个server上 订单表单独放到一个server上 - -### 水平拆分 -- 解决问题:单表中数据量增长出现的压力 -- 不解决问题:表与表之间的io争夺 -- 方案:**用户表**通过性别拆分为**男用户表**和**女用户表**,**订单表**通过已完成和完成中拆分为**已完成订单**和**未完成订单**,**产品表未完成订单放一个server上**,**已完成订单表盒男用户表放一个server上**,**女用户表放一个server上**(女的爱购物 哈哈)。 \ No newline at end of file diff --git "a/Java/codes/mysql/\346\225\260\346\215\256\345\272\223\350\214\203\345\274\217.md" "b/Java/codes/mysql/\346\225\260\346\215\256\345\272\223\350\214\203\345\274\217.md" deleted file mode 100644 index 6a3b7eb6..00000000 --- "a/Java/codes/mysql/\346\225\260\346\215\256\345\272\223\350\214\203\345\274\217.md" +++ /dev/null @@ -1,10 +0,0 @@ -# 数据库范式 - -## 第一范式 -在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。 所谓第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。在第一范式(1NF)中表的每一行只包含一个实例的信息。简而言之,**第一范式就是无重复的列**。 - -## 第二范式 -第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。第二范式(2NF)要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。这个惟一属性列被称为主关键字或主键、主码。 第二范式(2NF)要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。简而言之,第二范式就是**非主属性非部分依赖于主关键字**。 - -## 第三范式 -满足第三范式(3NF)必须先满足第二范式(2NF)。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。例如,存在一个部门信息表,其中每个部门有部门编号(dept_id)、部门名称、部门简介等信息。那么在员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。简而言之,第三范式就是属性不依赖于其它非主属性。(我的理解是消除冗余)**外键** \ No newline at end of file diff --git "a/Java/codes/mysql/\347\264\242\345\274\225\347\247\215\347\261\273.md" "b/Java/codes/mysql/\347\264\242\345\274\225\347\247\215\347\261\273.md" deleted file mode 100644 index f03a8639..00000000 --- "a/Java/codes/mysql/\347\264\242\345\274\225\347\247\215\347\261\273.md" +++ /dev/null @@ -1,30 +0,0 @@ -# 索引种类 - -## 索引类型 -- **FULLTEXT** :即为全文索引,目前只有MyISAM引擎支持。其可以在CREATE TABLE ,ALTER TABLE ,CREATE INDEX 使用,不过目前只有 CHAR、VARCHAR ,TEXT 列上可以创建全文索引。 -- **HASH** :由于HASH的唯一(几乎100%的唯一)及类似键值对的形式,很适合作为索引。 HASH索引可以一次定位,不需要像树形索引那样逐层查找,因此具有极高的效率。但是,这种高效是有条件的,即只在“=”和“in”条件下高效,对于范围查询、排序及组合索引仍然效率不高。 -- **BTREE** :BTREE索引就是一种将索引值按一定的算法,存入一个树形的数据结构中(二叉树),每次查询都是从树的入口root开始,依次遍历node,获取leaf。这是MySQL里默认和最常用的索引类型。 -- **RTREE** :RTREE在MySQL很少使用,仅支持geometry数据类型,支持该类型的存储引擎只有MyISAM、BDb、InnoDb、NDb、Archive几种。 相对于BTREE,RTREE的优势在于范围查找。 - -## 索引种类 -- **普通索引**:仅加速查询 -- **唯一索引**:加速查询 + 列值唯一(可以有null) -- **主键索引**:加速查询 + 列值唯一(不可以有null)+ 表中只有一个 -- **组合索引**:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并 -- **全文索引**:对文本的内容进行分词,进行搜索 -- **索引合并**:使用多个单列索引组合搜索 -- **覆盖索引**:select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖 -- **聚簇索引**:表数据是和主键一起存储的,主键索引的叶结点存储行数据(包含了主键值),二级索引的叶结点存储行的主键值。使用的是B+树作为索引的存储结构,非叶子节点都是索引关键字,但非叶子节点中的关键字中不存储对应记录的具体内容或内容地址。叶子节点上的数据是主键与具体记录(数据内容) - - -## 为什么使用索引 -- 通过创建唯一性索引,可以保证数据库表中每一行数据的**唯一性**。 -- 可以大大加快数据的**检索速度**,这也是创建索引的最主要的原因。 -- 帮助服务器**避免排序和临时表**。 -- 将**随机IO变为顺序IO**。 -- 可以**加速表和表之间的连接**,特别是在实现数据的参考完整性方面特别有意义。 - -## 索引缺点 -- 当对表中的数据进行增加、删除和修改的时候,**索引也要动态的维护**,这样就降低了数据的维护速度。 -- 索引需要**占物理空间**,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立簇索引,那么需要的空间就会更大。 -- **创建索引和维护索引要耗费时间**,这种时间随着数据量的增加而增加 \ No newline at end of file diff --git "a/Java/codes/mysql/\347\272\242\351\273\221\346\240\221\343\200\201B\346\240\221\345\222\214B+\346\240\221.md" "b/Java/codes/mysql/\347\272\242\351\273\221\346\240\221\343\200\201B\346\240\221\345\222\214B+\346\240\221.md" deleted file mode 100644 index 4282598a..00000000 --- "a/Java/codes/mysql/\347\272\242\351\273\221\346\240\221\343\200\201B\346\240\221\345\222\214B+\346\240\221.md" +++ /dev/null @@ -1,43 +0,0 @@ -# 红黑树、B树和B+树 - -## 红黑树 -> 红黑树也是一种自平衡的二叉查找树。 - -![红黑树](https://www.pdai.tech/_images/alg/alg-tree-14.png) - -### 特点 -- 每个结点要么是红的要么是黑的。(红或黑) -- 根结点是黑的。 (根黑) -- 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。 (叶黑) -- 如果一个结点是红的,那么它的两个儿子都是黑的。 (红子黑) -- 对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。(路径下黑相同) - -### 适用场景 -- Java `ConcurrentHashMap` & `TreeMap` -- C++ STL: map & set -- linux进程调度Completely Fair Scheduler,用红黑树管理进程控制块 -- epoll在内核中的实现,用红黑树管理事件块 -- nginx中,用红黑树管理timer等 - -## B树 -> B树(英语: B-tree)是一种自平衡的树,能够保持数据有序。这种数据结构能够让**查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成**。B树,概括来说是一个**一般化的二叉查找树**(binary search tree),可以拥有最多2个子节点。与自平衡二叉查找树不同,B树适用于读写相对大的数据块的存储系统,例如磁盘。其中,概念较为复杂,给个简单的图理解: - - -![简单B树](https://imgkr.cn-bj.ufileos.com/e30bb7ee-f0ec-4fd6-a4f2-8f82405dfe9a.png) - - - -## B+树 -- 在B树基础上,为**叶子结点增加链表指针**(B树+叶子有序链表),所有关键字都在叶子结点中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中。 -- B+树的**非叶子节点不保存数据**,只保存**子树的临界值**(最大或者最小),所以同样大小的节点,**B+树相对于B树能够有更多的分支,使得这棵树更加矮胖,查询时做的IO操作次数也更少**。 - -给个图: - - -![B+](https://imgkr.cn-bj.ufileos.com/0dd56365-0cc0-4f95-973b-5465c9664f71.png) - - -## 为什么用B+树而不用hash和B-Tree -- 利用Hash需要把数据全部**加载到内存中**,如果数据量大,是一件很**消耗内存**的事,而采用B+树,是基于**按照节点分段加载,由此减少内存消耗**。 -- 和业务场景有段,**对于唯一查找**(查找一个值),Hash确实更快,**但数据库中经常查询多条数据**,这时候由于B+数据的有序性,与叶子节点又有链表相连,他的查询效率会比Hash快的多。 -- b+树的**非叶子节点不保存数据**,**只保存子树的临界值**(最大或者最小),所以同样大小的节点,**b+树相对于b树能够有更多的分支,使得这棵树更加矮胖,查询时做的IO操作次数也更少**。 \ No newline at end of file diff --git "a/Java/codes/other/Sentinel\347\233\270\345\205\263\346\246\202\345\277\265.md" "b/Java/codes/other/Sentinel\347\233\270\345\205\263\346\246\202\345\277\265.md" deleted file mode 100644 index a6437463..00000000 --- "a/Java/codes/other/Sentinel\347\233\270\345\205\263\346\246\202\345\277\265.md" +++ /dev/null @@ -1,24 +0,0 @@ -# Sentinel? -> Sentinel是一个面试分布式架构的轻量级服务保护框架,主要以流量控制、熔断降级、系统负载保护等多个维度。 - -## 服务限流 -> 当系统资源不够,不足以应对大量请求,对系统按照预设的规则进行流量限制或功能限制 - -## 服务熔断 -> 当调用目标服务的请求和调用大量超时或失败,服务调用方为避免造成长时间的阻塞造成影响其他服务,后续对该服务接口的调用不再经过进行请求,直接执行本地的默认方法 - -## 服务降级 -> 为了保证核心业务在大量请求下能正常运行,根据实际业务情况及流量,对部分服务降低优先级,有策略的不处理或用简单的方式处理 - -## 为什么熔断降级 -> 系统承载的访问量是有限的,如果不做流量控制,会导致系统资源占满,服务超时,从而所有用户无法使用,通过服务限流控制请求的量,服务降级省掉非核心业务对系统资源的占用,最大化利用系统资源,尽可能服务更多用户 - -## 工作原理 -![](https://www.javazhiyin.com/wp-content/uploads/2019/10/java6-1572258892.png) - -## 对比 -![](https://imgkr.cn-bj.ufileos.com/8f2cf909-4c41-47c3-89d9-d6eb2676a519.png) - -**值得补充的是**:相比 Hystrix 基于线程池隔离进行限流,这种方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。 - -Sentinel 并发线程数限流不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目,如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离 diff --git "a/Java/codes/redis/Redis\345\201\232\345\273\266\350\277\237\351\230\237\345\210\227.md" "b/Java/codes/redis/Redis\345\201\232\345\273\266\350\277\237\351\230\237\345\210\227.md" deleted file mode 100644 index c8a69ffd..00000000 --- "a/Java/codes/redis/Redis\345\201\232\345\273\266\350\277\237\351\230\237\345\210\227.md" +++ /dev/null @@ -1,218 +0,0 @@ -# Redis做延迟队列 - -## 引言 -> 班车预约平台,订单自动取消任务,采用的是监听Redis的key键消失的策略,属于被动轮询。而用Redis做延迟队列,需要主动轮询。班车平台的github:[https://github.com/DreamCats/school-bus](https://github.com/DreamCats/school-bus) - -## 实现思路 -1. 将整个Redis当做消息池,以kv形式存储消息,key为id,value为具体的消息body -2. 使用ZSET做优先队列,按照score维持优先级(用当前时间+需要延时的时间作为score) -3. 轮询ZSET,拿出score比当前时间戳大的数据(已过期的) -4. 根据id拿到消息池的具体消息进行消费 -5. 消费成功,删除改队列和消息 -6. 消费失败,让该消息重新回到队列 - - -## 代码实现 - -### RedisMessage - -```java -public class RedisMessage { - - /** - * 消息id - */ - private String id; - - /** - * 消息延迟/毫秒 - */ - private long delay; - - /** - * 消息存活时间 - */ - private int ttl; - - /** - * 消息体,对应业务内容 - */ - private String body; - - /** - * 创建时间,如果只有优先级没有延迟,可以设置创建时间为0 - * 用来消除时间的影响 - */ - private long createTime; - -} -``` - -### RedisMQ队列 -```java -@Component -public class RedisMQ { - - /** - * 消息池前缀,以此前缀加上传递的消息id作为key,以消息{@link RedisMessage} - * 的消息体body作为值存储 - */ - public static final String MSG_POOL = "Message:Pool:"; - /** - * zset队列 名称 queue - */ - public static final String QUEUE_NAME = "Message:Queue:"; - - private static final int SEMIH = 30*60; - - @Autowired - private RedisUtils redisUtils; - - /** - * 存入消息池 - * @param message - * @return - */ - public boolean addMsgPool(RedisMessage message) { - - if (null != message) { - return redisUtils.set(MSG_POOL + message.getId(), message.getBody(), Long.valueOf(message.getTtl() + SEMIH)); - } - return false; - } - - /** - * 从消息池中删除消息 - * @param id - */ - public void deMsgPool(String id) { - redisUtils.del(MSG_POOL + id); - } - - /** - * 向队列中添加消息 - * @param key - * @param score - * @param val - */ - public void enMessage(String key, long score, String val) { - redisUtils.zsset(key, val, score); - } - - /** - * 从队列额删除消息 - * @param key - * @param id - * @return - */ - public boolean deMessage(String key, String id) { - return redisUtils.zdel(key, id); - } -} -``` - -### Redis的工具类 -可以去[github地址](https://github.com/DreamCats/school-bus/blob/master/school-bus/guns-order/src/test/java/com/stylefeng/guns/rest/RedisUtils.java) - -### 生产者(用户) -```java -@Component -public class MessageProvider { - - private static int delay = 30;//30秒,可自己动态传入 - - @Resource - private RedisMQ redisMQ; - - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - - //改造成redis - public void sendMessage(String messageContent) { - try { - if (messageContent != null){ - String seqId = UUID.randomUUID().toString(); - // 将有效信息放入消息队列和消息池中 - RedisMessage message = new RedisMessage(); - // 可以添加延迟配置 - message.setDelay(delay*1000); - message.setCreateTime(System.currentTimeMillis()); - message.setBody(messageContent); - message.setId(seqId); - // 设置消息池ttl,防止长期占用 - message.setTtl(delay + 360); - redisMQ.addMsgPool(message); - //当前时间加上延时的时间,作为score - Long delayTime = message.getCreateTime() + message.getDelay(); - String d = sdf.format(message.getCreateTime()); - System.out.println("当前时间:" + d+",消费的时间:" + sdf.format(delayTime)); - redisMQ.enMessage(RedisMQ.QUEUE_NAME,delayTime, message.getId()); - }else { - System.out.println("消息内容为空"); - } - }catch (Exception e){ - e.printStackTrace(); - } - } -} -``` - -### 消费者 -```java -@Component -public class RedisMQConsumer { - - @Resource - private RedisMQ redisMQ; - - @Autowired - private RedisUtils redisUtils; - - @Autowired - private MessageProvider provider; - - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - - - /** - * 消息队列监听器 - * 也可以开个线程池 - * 主动轮询 - */ - @Scheduled(cron = "*/1 * * * * *") - public void monitor() { - // 取出0到当前时间的权重值 - Set set = redisUtils.rangeByScore(RedisMQ.QUEUE_NAME, 0, System.currentTimeMillis()); - if (null != set) { - // 如果不为空 - // 获取当前时间 - long current = System.currentTimeMillis(); - for (Object id : set) { - long score = redisUtils.getScore(RedisMQ.QUEUE_NAME, (String) id).longValue(); - if (current >= score) { - // 已超时的消息拿出来消费 - String str = ""; - try { - // 根据id取出消息 - str = (String) redisUtils.get(RedisMQ.MSG_POOL + id); - System.out.println("消费了:" + str+ ",消费的时间:" + sdf.format(System.currentTimeMillis())); - } catch (Exception e) { - e.printStackTrace(); - //如果取消息发生了异常,则将消息重新放回队列 - System.out.println("消费异常,重新回到队列"); - provider.sendMessage(str); - } finally { - // 不管消费成功与非,都要删除当前消息 - redisMQ.deMessage(RedisMQ.QUEUE_NAME, (String) id); - redisMQ.deMsgPool((String) id); - } - } - } - } - } -} -``` - -### 结合班车预约平台思路 -1. 当前端发一个带延迟时间戳的请求,我们后端可以在zset当做延迟队列,订单ID为元素的key,时间戳为当前时间+延迟时间戳当做score -2. 可以使用Springboot定时器或者线程池延迟主动轮询拉取队列中符合条件的score,根据符合条件score的订单id -3. 根据符合条件的订单id,进行回退座位。 \ No newline at end of file diff --git "a/Java/codes/redis/\346\267\261\347\251\266Redis\347\232\204\345\272\225\345\261\202\347\273\223\346\236\204.md" "b/Java/codes/redis/\346\267\261\347\251\266Redis\347\232\204\345\272\225\345\261\202\347\273\223\346\236\204.md" deleted file mode 100644 index dec79def..00000000 --- "a/Java/codes/redis/\346\267\261\347\251\266Redis\347\232\204\345\272\225\345\261\202\347\273\223\346\236\204.md" +++ /dev/null @@ -1,185 +0,0 @@ -# 深究Redis的底层结构 - -## 简单动态字符串(SDS) -> Redis没有直接使用C语言传统的字符串(以空字符结尾的字符数组,以下简称C字符串),而是自己构建了一种名为简单动态字符串(Simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。 - -### 看一下SDS的定义 -```c -struct sdshdr { - // 记录buf数组中已使用字节的数量 - // 等于sds所保存字符串的长度 - int len; - - // 记录buf数组中未使用字节的数量 - int free; - - // 字节数组,用于保存字符串 - char buf[]; -} -``` - -### 有何优点 -1. 获取字符串长度的复杂度为O(1)。 -2. 杜绝缓冲区溢出。 -3. 减少修改字符串长度时所需要的内存重分配次数。 -4. 二进制安全。 -5. 兼容部分C字符串函数。 - -## 链表 -> 毫无疑问,Redis也不会不适用链表的,毕竟链表这么好的结构,用起来很香的。当有一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的额字符串时,Redis就会使用链表作为列表建的底层实现。 - -### 节点底层结构 -```c -typedef struct listNode { - // 前置节点 - struct listNode *prev; - // 后置节点 - struct listNode *next; - // 节点的值 - void *value; -} listNode; -``` -和其他的节点结构没啥区别... - -### list底层结构 -```c -typedef struct list { - // 表头节点 - listNode *head; - // 表尾节点 - listNode *tail; - // 链表所包含的节点数量 - unsigned long len; - // 节点值复制函数 - void *(*dup)(void *ptr); - // 节点值是放过函数 - void (*free)(void *ptr); - // 节点值对比函数 - int(*match)(void *ptr, void *key); -} list; -``` - -### 特性 -1. 链表被广泛用于实现Redis的各种功能,比如列表建、发布与订阅、慢查询、监视器等。 -2. 每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以Redis的链表实现是双端链表。 -3. 每个链表使用一个list结构表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度等信息。 -4. 因为链表表头的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表。 -5. 通过为链表设置不同的类型特定函数,Redis的链表可以用于保存各种不同类型的值。 - -## 字典 -> 说白了, 字典就是键值对。Redis的字典的底层就是哈希表而已。 - -### 哈希表 -```c -typedef struct dictht { - // 哈希表数组 - dictEntry **table; - // 哈希表大小 - unsigned long size; - // 哈希表大小掩码,用于计算索引值 - // 总是等于size-1 - unsigned long sizemark; - // 该哈希表已有节点的数量 - unsigned long used; -} dichht; -``` - -那么,哈希表数组的结构table呢? - -### dictEntry的结构 -```c -typedef struct dictEntry { - // 键 - void *key; - // 值 - union { - void *val; - uint64_t u64; - int64_t s64; - } v; - // 指向下个哈希表节点,形成链表 - struct dictEntry *nect; -} dictEntry; -``` -dict的底层结构省略。 - -### 哈希算法 -当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash算法。这种算法的优点在于即使输入的键是规律的,算法仍能给出一个个很好的随机分布性,并且算法的计算速度非常快。 - -### 哈希冲突 -Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用这个单向链表连接起来,这就解决了键冲突的问题。 - -### 特性 -1. 字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键。 -2. Redis中的字典使用哈希表作为底层结构实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。 -3. Redis使用MurmurHash2算法来计算键的哈希值。 -4. 哈希表使用链地址法来解决键冲突。 - -## 跳跃表 - -先看这样一张图: -![](https://user-gold-cdn.xitu.io/2019/4/18/16a30ca4a17c6280?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -如上图,我们要查找一个元素,就需要从头节点开始遍历,直到找到对应的节点或者是第一个大于要查找的元素的节点(没找到)。时间复杂度为O(N)。 - -这个查找效率是比较低的,但如果我们把列表的某些节点拔高一层,如下图,例如把每两个节点中有一个节点变成两层。那么第二层的节点只有第一层的一半,查找效率也就会提高。 - -![](https://user-gold-cdn.xitu.io/2019/4/18/16a30ca4aa58e4bb?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -查找的步骤是从头节点的顶层开始,查到第一个大于指定元素的节点时,退回上一节点,在下一层继续查找。 - -比如我们要查找16: -1. 从头节点的最顶层开始,先到节点7。 -2. 7的下一个节点是39,大于16,因此我们退回到7 -3. 从7开始,在下一层继续查找,就可以找到16。 - -这个例子中遍历的节点不比一维列表少,但是当节点更多,查找的数字更大时,这种做法的优势就体现出来了。还是上面的例子,如果我们要**查找的是39**,那么只需要访问两个节点(7、39)就可以找到了。这比一维列表要减少一半的数量。 - -为了避免插入操作的时间复杂度是O(N),skiplist每层的数量不会严格按照2:1的比例,而是对每个要插入的元素随机一个层数。 - -随机层数的计算过程如下: -1. 每个节点都有第一层 -2. 那么它有第二层的概率是p,有第三层的概率是p*p -3. 不能超过最大层数 - -### zskiplistNode -```c -typedef struct zskiplistNode { - // 后退指针 - struct zskiplistNode *backward; - // 分值 权重 - double score; - // 成员对象 - robj *obj; - // 层 - struct zskiplistLevel { - // 前进指针 - struct zskiplistNode *forward; - // 跨度 - unsigned int span; - } leval[]; -} zskiplistNode; -``` -一般来说,层的数量越多,访问其他节点的速度越快。 - -### zskipList -```c -typedef struct zskiplist { - // 表头节点和表尾节点 - struct zskiplistNode *header, *tail; - // 表中节点的数量 - unsigned long length; - // 表中层数最大的节点的层数 - int leval; -} zskiplist; -``` - -### 特性 -1. 跳跃表是有序集合的底层实现之一 -2. Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点 -3. 每个跳跃表节点的层高都是1至32之间的随机数 -4. 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。 -5. 跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。 - -## 压缩列表 -> 一看名字,就是为了节省内存造的列表结构。压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。 \ No newline at end of file diff --git "a/Java/codes/spring/\346\211\213\345\206\231SpringAOP\344\273\243\347\240\201.md" "b/Java/codes/spring/\346\211\213\345\206\231SpringAOP\344\273\243\347\240\201.md" deleted file mode 100644 index 284133a7..00000000 --- "a/Java/codes/spring/\346\211\213\345\206\231SpringAOP\344\273\243\347\240\201.md" +++ /dev/null @@ -1,116 +0,0 @@ -# 手写SpringAOP - -## 定义advice接口 - -```java -public interface Advice extends InvocationHandler { -} -``` - -## 定义方法回调接口 -```java -public interface MethodInvocation { - void invoke(); -} -``` - -## 定义前置通知 -```java -public class BeforeAdvice implements Advice { - - private Object bean; // bean对象 - - private MethodInvocation methodInvocation; // 方法调用 - - public BeforeAdvice(Object bean, MethodInvocation methodInvocation) { - this.bean = bean; - this.methodInvocation = methodInvocation; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - // 在目标方法前调用,前置 - methodInvocation.invoke(); - return method.invoke(bean, args); - } -} -``` - -## 定义后置通知 -```java -public class AfterAdvice implements Advice { - - private Object bean; - - private MethodInvocation methodInvocation; - - public AfterAdvice(Object bean, MethodInvocation methodInvocation) { - this.bean = bean; - this.methodInvocation = methodInvocation; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - // 业务逻辑调用 - Object invoke = method.invoke(bean, args); - // 后置通知调用 - methodInvocation.invoke(); - return invoke; - } -} -``` - -## 定义目标方法接口 -```java -public interface HelloService { - void sayHelloWorld(); -} -``` - -## 实现目标方法 -```java -public class HelloServiceImpl implements HelloService { - - @Override - public void sayHelloWorld() { - System.out.println("hello world..."); - } -} -``` - -## SpringAOP的实现 -```java -public class SimpleAOP { - // 反射代理 - public static Object getProxy(Object bean, Advice advice) { - return Proxy.newProxyInstance(SimpleAOP.class.getClassLoader() - , bean.getClass().getInterfaces(), advice); - } -} -``` - -## 简单测试 -```java -public class SimpleAOPTest { - public static void main(String[] args) { - // 1. 创建一个 MethodInvocation 实现类 切面逻辑类 - MethodInvocation logTask = () -> System.out.println("log task start"); - MethodInvocation logTaskEnd = () -> System.out.println("log task end"); - - // 业务逻辑类 - HelloServiceImpl helloService = new HelloServiceImpl(); - - // 2. 创建一个Advice 切入点 - BeforeAdvice beforeAdvice = new BeforeAdvice(helloService, logTask); - AfterAdvice afterAdvice = new AfterAdvice(helloService, logTaskEnd); - - // 3. 为目标对象生成代理 - HelloService helloServiceImplProxy = (HelloService) SimpleAOP.getProxy(helloService, beforeAdvice); - HelloService helloServiceImplProxyAfter = (HelloService) SimpleAOP.getProxy(helloService, afterAdvice); - - helloServiceImplProxy.sayHelloWorld(); - helloServiceImplProxyAfter.sayHelloWorld(); - - } -} -``` diff --git "a/Java/codes/thread/AQS\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/thread/AQS\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index a731c1bd..00000000 --- "a/Java/codes/thread/AQS\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,318 +0,0 @@ -# AQS -> Java的`AbstractQueuedSynchronizer`抽象类,抽象同队队列 - - -## 原理 -> AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。 -```java -private volatile int state;//共享变量,使用volatile修饰保证线程可见性 -``` -状态信息通过 protected 类型的`getState`,`setState`,`compareAndSetState`进行操作 -```java -//返回同步状态的当前值 -protected final int getState() { - return state; -} - // 设置同步状态的值 -protected final void setState(int newState) { - state = newState; -} -//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) -protected final boolean compareAndSetState(int expect, int update) { - return unsafe.compareAndSwapInt(this, stateOffset, expect, update); -} -``` - -**AQS 定义两种资源共享方式** - -1. **Exclusive**(独占)只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁。 - - 总结:公平锁和非公平锁只有两处不同: - - 1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。 - 2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。 - -2. **Share**(共享)多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、 CyclicBarrier、ReadWriteLock 。 - -**AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:** -```java -isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 -tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 -tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 -tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 -tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 -``` - -## ReentrantLock -> 我们知道,我们需要在哪里上锁,一般都是调用`xxx.lock()`,在finally的代码块中释放锁`xxx.unlock()`,那么我们来看一下源码。 - -### 构造方法 -```java -// 默认是非公平锁 -public ReentrantLock() { - sync = new NonfairSync(); -} -// 可选参数,是否公平 -public ReentrantLock(boolean fair) { - sync = fair ? new FairSync() : new NonfairSync(); -} -``` - -#### 公平锁的实现 -```java -protected final boolean tryAcquire(int acquires) { - final Thread current = Thread.currentThread(); - int c = getState(); - if (c == 0) { - if (!hasQueuedPredecessors() && // 判断队列是否轮到该线程 - compareAndSetState(0, acquires)) { // 利用cas更换状态 - setExclusiveOwnerThread(current); // 如果都ok,就设置独占锁 - return true; - } - } - else if (current == getExclusiveOwnerThread()) {// 判断当前独占锁是否还是当前线程 - int nextc = c + acquires;// 状态累加 - if (nextc < 0) - throw new Error("Maximum lock count exceeded"); - setState(nextc); // 设置状态 - return true; - } // 否则false - return false; -} -``` -#### 非公平锁的实现 -> 比公平锁少了队列的判断而已。 -```java -final boolean nonfairTryAcquire(int acquires) { - final Thread current = Thread.currentThread(); - int c = getState(); - if (c == 0) { - if (compareAndSetState(0, acquires)) {// 在这里... - setExclusiveOwnerThread(current); - return true; - } - } - else if (current == getExclusiveOwnerThread()) { - int nextc = c + acquires; - if (nextc < 0) // overflow - throw new Error("Maximum lock count exceeded"); - setState(nextc); - return true; - } - return false; -} -``` - -## CountDownLatch -CountDownLatch是共享锁的一种实现,它默认构造 `AQS` 的 `state` 值为 `count`。当线程使用`countDown`方法时,其实使用了`tryReleaseShared`方法以CAS的操作来减少`state`,直至`state`为0就代表所有的线程都调用了`countDown`方法。当调用`await`方法的时候,如果`state`不为0,就代表仍然有线程没有调用countDown方法,那么就把已经调用过countDown的线程都放入**阻塞队列Park,并自旋CAS判断state == 0**,直至最后一个线程调用了countDown,使得state == 0,于是阻塞的线程便判断成功,全部往下执行。 - -### 构造方法 -```java -public CountDownLatch(int count) { - if (count < 0) throw new IllegalArgumentException("count < 0"); - this.sync = new Sync(count); // 这里设置state的次数 -} -``` - -### countDown -```java -public void countDown() { - sync.releaseShared(1); -} -``` -### tryReleaseShared -```java -protected boolean tryReleaseShared(int releases) { - // Decrement count; signal when transition to zero - for (;;) { - int c = getState(); - if (c == 0) - return false; - int nextc = c-1; // 每执行一次该方法,状态减一 - if (compareAndSetState(c, nextc)) - return nextc == 0; - } -} -``` - -### 三种用法: - -1. 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :`new CountDownLatch(n)`,每当一个任务线程执行完毕,就将计数器减 1 `countdownlatch.countDown()`,当计数器的值变为 0 时,在`CountDownLatch上 await()` 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。 -2. 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 `CountDownLatch` 对象,将其计数器初始化为 1 :`new CountDownLatch(1)`,多个线程在开始执行任务前首先 `coundownlatch.await()`,当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。 -3. 死锁检测:一个非常方便的使用场景是,你可以使用 n 个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。 - -### 举个例子 -> 主线程等待多个子线程 -```java -public class CountDownLatchDemo { - - public static void main(String[] args) throws InterruptedException { - countDownLatchTest(); -// general(); - } - - public static void general() { - for (int i = 0; i < 6; i++) { - new Thread(() -> { - System.out.println(Thread.currentThread().getName() + " 上完自习,离开教师"); - }, "Thread --> " + i).start(); - } - while (Thread.activeCount() > 2) { // 一般方式 - try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } - System.out.println(Thread.currentThread().getName() + " ====班长最后走人"); - } - } - - public static void countDownLatchTest() throws InterruptedException { - CountDownLatch countDownLatch = new CountDownLatch(6); - for (int i = 0; i < 6; i++) { - new Thread(() -> { - System.out.println(Thread.currentThread().getName() + " 上完自习,离开教师"); - countDownLatch.countDown(); // 每个线程执行完都会调用countDown - }, "Thread --> " + i).start(); - } - countDownLatch.await(); // 阻塞主线程 - System.out.println(Thread.currentThread().getName() + " ====班长最后走人"); - } -} -``` - -## CyclicBarrier -> CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用`await`方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 - -### 举个例子 -```java -public class CyclicBarrierDemo { - public static void main(String[] args) { - cyclicBarrierTest(); - } - - public static void cyclicBarrierTest() { - CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { - System.out.println("====召唤神龙===="); // 拦截到一定数量,执行该代码 - }); - for (int i = 0; i < 7; i++) { - final int tempInt = i; - new Thread(() -> { - System.out.println(Thread.currentThread().getName() + " 收集到第" + tempInt + "颗龙珠"); - try { - cyclicBarrier.await(); // 拦截 - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (BrokenBarrierException e) { - e.printStackTrace(); - } - }, "" + i).start(); - } - } -} -``` -当调用 `CyclicBarrier` 对象调用 `await()` 方法时,实际上调用的是`dowait(false, 0L)`方法。 `await()` 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 parties 的值时,栅栏才会打开,线程才得以通过执行。 -看一下源码: -```java - // 当线程数量或者请求数量达到 count 时 await 之后的方法才会被执行。上面的示例中 count 的值就为 7。 - private int count; - /** - * Main barrier code, covering the various policies. - */ - private int dowait(boolean timed, long nanos) - throws InterruptedException, BrokenBarrierException, - TimeoutException { - final ReentrantLock lock = this.lock; - // 锁住 - lock.lock(); - try { - final Generation g = generation; - - if (g.broken) - throw new BrokenBarrierException(); - - // 如果线程中断了,抛出异常 - if (Thread.interrupted()) { - breakBarrier(); - throw new InterruptedException(); - } - // cout减1 - int index = --count; - // 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了,也就是达到了可以执行await 方法之后的条件 - if (index == 0) { // tripped - boolean ranAction = false; - try { - final Runnable command = barrierCommand; - if (command != null) - command.run(); - ranAction = true; - // 将 count 重置为 parties 属性的初始化值 - // 唤醒之前等待的线程 - // 下一波执行开始 - nextGeneration(); - return 0; - } finally { - if (!ranAction) - breakBarrier(); - } - } - - // loop until tripped, broken, interrupted, or timed out - for (;;) { - try { - if (!timed) - trip.await(); - else if (nanos > 0L) - nanos = trip.awaitNanos(nanos); - } catch (InterruptedException ie) { - if (g == generation && ! g.broken) { - breakBarrier(); - throw ie; - } else { - // We're about to finish waiting even if we had not - // been interrupted, so this interrupt is deemed to - // "belong" to subsequent execution. - Thread.currentThread().interrupt(); - } - } - - if (g.broken) - throw new BrokenBarrierException(); - - if (g != generation) - return index; - - if (timed && nanos <= 0L) { - breakBarrier(); - throw new TimeoutException(); - } - } - } finally { - lock.unlock(); - } - } -``` -总结:`CyclicBarrier` 内部通过一个 count 变量作为计数器,count 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。 - -## Semaphore -> **synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。** - -### 举个例子 -```java -public class SemaphoreDemo { - public static void main(String[] args) { - Semaphore semaphore = new Semaphore(3);// 模拟三个停车位 - for (int i = 0; i < 6; i++) { // 模拟6部汽车 - new Thread(() -> { - try { - semaphore.acquire(); - System.out.println(Thread.currentThread().getName() + " 抢到车位"); - // 停车3s - try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } - System.out.println(Thread.currentThread().getName() + " 停车3s后离开车位"); - } catch (Exception e) { - e.printStackTrace(); - } finally { - semaphore.release(); - } - }, "Car " + i).start(); - } - } -} -``` diff --git "a/Java/codes/thread/CAS\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/thread/CAS\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index c01f3e17..00000000 --- "a/Java/codes/thread/CAS\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,89 +0,0 @@ -# CAS -> **我们在读Concurrent包下的类的源码时,发现无论是**ReenterLock内部的AQS,还是各种Atomic开头的原子类,内部都应用到了`CAS` - -```java -public class Test { - - public AtomicInteger i; - - public void add() { - i.getAndIncrement(); - } -} -``` -**我们来看`getAndIncrement`的内部:** -```java -public final int getAndIncrement() { - return unsafe.getAndAddInt(this, valueOffset, 1); -} -``` -**再深入到`getAndAddInt`():** -```java -public final int getAndAddInt(Object var1, long var2, int var4) { - int var5; - do { - var5 = this.getIntVolatile(var1, var2); - } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); - - return var5; -} -``` -**现在重点来了,`compareAndSwapInt(var1, var2, var5, var5 + var4)`其实换成`compareAndSwapInt(obj, offset, expect, update)`比较清楚,意思就是如果`obj`内的`value`和`expect`相等,就证明没有其他线程改变过这个变量,那么就更新它为`update`,如果这一步的`CAS`没有成功,那就采用自旋的方式继续进行`CAS`操作,取出乍一看这也是两个步骤了啊,其实在`JNI`里是借助于一个`CPU`指令完成的。所以还是原子操作。** - -```c -UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) - UnsafeWrapper("Unsafe_CompareAndSwapInt"); - oop p = JNIHandles::resolve(obj); - jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); - return (jint)(Atomic::cmpxchg(x, addr, e)) == e; -UNSAFE_END -``` -**p是取出的对象,addr是p中offset处的地址,最后调用了`Atomic::cmpxchg(x, addr, e)`, 其中参数x是即将更新的值,参数e是原内存的值。代码中能看到cmpxchg有基于各个平台的实现。** - -## ABA问题 -> 描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时变量 x 的值还是 A,所以 compareAndSet 操作是成功。 - -**目前在JDK的atomic包里提供了一个类`AtomicStampedReference`来解决ABA问题。** - -```java -public class ABADemo { - static AtomicInteger atomicInteger = new AtomicInteger(100); - static AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100, 1); - - public static void main(String[] args) { - System.out.println("=====ABA的问题产生====="); - new Thread(() -> { - atomicInteger.compareAndSet(100, 101); - atomicInteger.compareAndSet(101, 100); - }, "t1").start(); - - new Thread(() -> { - // 保证线程1完成一次ABA问题 - try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } - System.out.println(atomicInteger.compareAndSet(100, 2020) + " " + atomicInteger.get()); - }, "t2").start(); - try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } - - System.out.println("=====解决ABA的问题====="); - new Thread(() -> { - int stamp = atomicStampedReference.getStamp(); // 第一次获取版本号 - System.out.println(Thread.currentThread().getName() + " 第1次版本号" + stamp); - try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } - atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); - System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp()); - atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); - System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp()); - }, "t3").start(); - - new Thread(() -> { - int stamp = atomicStampedReference.getStamp(); - System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp); - try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } - boolean result = atomicStampedReference.compareAndSet(100, 2020, stamp, stamp + 1); - System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp()); - System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference()); - }, "t4").start(); - - } -} -``` \ No newline at end of file diff --git "a/Java/codes/thread/Java\351\224\201\347\232\204\344\273\213\347\273\215.md" "b/Java/codes/thread/Java\351\224\201\347\232\204\344\273\213\347\273\215.md" deleted file mode 100644 index e8c3f0a3..00000000 --- "a/Java/codes/thread/Java\351\224\201\347\232\204\344\273\213\347\273\215.md" +++ /dev/null @@ -1,98 +0,0 @@ -# 各种锁 -## 公平锁/非公平锁 -**公平锁指多个线程按照申请锁的顺序来获取锁。非公平锁指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象(很长时间都没获取到锁-非洲人...),ReentrantLock,了解一下。** - -## 可重入锁 -**可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,典型的synchronized,了解一下** -```java -synchronized void setA() throws Exception { - Thread.sleep(1000); - setB(); // 因为获取了setA()的锁,此时调用setB()将会自动获取setB()的锁,如果不自动获取的话方法B将不会执行 -} -synchronized void setB() throws Exception { - Thread.sleep(1000); -} -``` - -## 独享锁/共享锁 -- 独享锁:是指该锁一次只能被一个线程所持有。 -- 共享锁:是该锁可被多个线程所持有。 - -## 互斥锁/读写锁 -**上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是其具体的实现** - -## 乐观锁/悲观锁 - -1. **乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待兵法同步的角度。** -2. **悲观锁认为对于同一个人数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出现问题。** -3. **乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作时没有事情的。** -4. **悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁带来大量的性能提升。** -5. **悲观锁在Java中的使用,就是利用各种锁。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子类操作的更新。重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁** - -## 分段锁 - -1. **分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来哦实现高效的并发操作。** -2. **以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是ReentrantLock(Segment继承了ReentrantLock)** -3. **当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。** -4. **分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。** - -## 偏向锁/轻量级锁/重量级锁 - -1. **这三种锁是锁的状态,并且是针对Synchronized。在Java5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。** -2. **偏向锁的适用场景:始终只有一个线程在执行代码块,在它没有执行完释放锁之前,没有其它线程去执行同步快,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;在有锁竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向锁的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用。** -3. **轻量级锁是指当锁是偏向锁的时候,被另一个线程锁访问,偏向锁就会升级为轻量级锁,其他线程会通过自选的形式尝试获取锁,不会阻塞,提高性能。** -4. **重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。** - -## 自旋锁 - -1. **在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。** -2. **自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。** -3. **自旋锁尽可能的减少线程的阻塞,适用于锁的竞争不激烈,且占用锁时间非常短的代码来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗。** -4. **但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适用使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cpu的线程又不能获取到cpu,造成cpu的浪费。** - -### 手写自旋锁的例子 -```java -public class SpinLock { - - // 原子引用线程 - AtomicReference atomicReference = new AtomicReference<>(); - - public void mylock() { - Thread thread = Thread.currentThread(); - System.out.println(Thread.currentThread().getName() + " como in..."); - while (!atomicReference.compareAndSet(null, thread)) { -// System.out.println("不爽,重新获取一次值瞧瞧..."); - } - } - - public void myUnlock() { - Thread thread = Thread.currentThread(); - atomicReference.compareAndSet(thread, null); - System.out.println(Thread.currentThread().getName() + " invoke myUnLock..."); - } - - public static void main(String[] args) { - SpinLock spinLock = new SpinLock(); - new Thread(() -> { - spinLock.mylock(); - try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } - spinLock.myUnlock(); - }, "t1").start(); - try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } - - new Thread(() -> { - spinLock.mylock(); - spinLock.myUnlock(); - }, "t2").start(); - } - -} -``` - -## Java锁总结 - -**Java锁机制可归为Sychornized锁和Lock锁两类。Synchronized是基于JVM来保证数据同步的,而Lock则是硬件层面,依赖特殊的CPU指令来实现数据同步的。** - -- Synchronized是一个非公平、悲观、独享、互斥、可重入的重量级锁。 -- ReentrantLock是一个默认非公平但可实现公平的、悲观、独享、互斥、可重入、重量级锁。 -- ReentrantReadWriteLock是一个默认非公平但可实现公平的、悲观、写独享、读共享、读写、可重入、重量级锁。 \ No newline at end of file diff --git "a/Java/codes/thread/Runnable\343\200\201Callable\345\222\214FutureTask.md" "b/Java/codes/thread/Runnable\343\200\201Callable\345\222\214FutureTask.md" deleted file mode 100644 index d140c65c..00000000 --- "a/Java/codes/thread/Runnable\343\200\201Callable\345\222\214FutureTask.md" +++ /dev/null @@ -1,61 +0,0 @@ -# Runnable、Callable和FutureTask -> Runnable自Java 1.0以来一直存在,但Callable仅在Java 1.5中引入,目的就是为了来处理**Runnable不支持的用例**。**Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以**。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。 - -## Runnable源码 -```java -@FunctionalInterface -public interface Runnable { - /** - * 被线程执行,没有返回值也无法抛出异常 - */ - public abstract void run(); -} -``` - -## Callable源码 -```java -@FunctionalInterface -public interface Callable { - /** - * 计算结果,或在无法这样做时抛出异常。 - * @return 计算得出的结果 - * @throws 如果无法计算结果,则抛出异常 - */ - V call() throws Exception; -} -``` - -## 线程池的submit - -![看图说话](https://imgkr.cn-bj.ufileos.com/16a8713e-b2d1-408d-82e2-ac411a51b6e0.png) - -## 再瞧Thread - - -![](https://imgkr.cn-bj.ufileos.com/ff6bdf55-1616-4887-b320-1b21576f8211.png) - -有个疑问,没有FutureTask的参数哇? - -再看FutureTask: - -![](https://imgkr.cn-bj.ufileos.com/6f63d5a6-50a5-4412-b301-1bab8ce91b4d.png) - -它的参数有Callable和Runnable。 - -`public class FutureTask implements RunnableFuture` -源码实现了RunnableFuture,那我们跟进再看: - -```java -public interface RunnableFuture extends Runnable, Future { - /** - * Sets this Future to the result of its computation - * unless it has been cancelled. - */ - void run(); -} -``` -RunnableFuture继承了Runnable和Future。意味着FutureTask既有Runnable和Future的特性,Future的方法: - -![](https://imgkr.cn-bj.ufileos.com/5c856df9-feb9-43c0-9329-97db80239de0.png) - -可看出,Future的特性的get方法了。 diff --git "a/Java/codes/thread/ThreadLocal\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/thread/ThreadLocal\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index 6ca02b79..00000000 --- "a/Java/codes/thread/ThreadLocal\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,38 +0,0 @@ -# ThreadLocal -**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。****如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** - -## 原理 -```java -public class Thread implements Runnable { - ...... -//与此线程有关的ThreadLocal值。由ThreadLocal类维护 -ThreadLocal.ThreadLocalMap threadLocals = null; - -//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 -ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; - ...... -} -``` -从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。 - -`ThreadLocal`类的`set()`方法 - -```java -public void set(T value) { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - if (map != null) - map.set(this, value); - else - createMap(t, value); -} -ThreadLocalMap getMap(Thread t) { - return t.threadLocals; -} -``` -**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。** - -**每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。** 比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。`ThreadLocal` 是 map结构是为了让每个线程可以关联多个 `ThreadLocal`变量。这也就解释了ThreadLocal声明的变量为什么在每一个线程都有自己的专属本地变量。 - -## 内存泄露 -`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候会 key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法。 \ No newline at end of file diff --git "a/Java/codes/thread/synchronized\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/thread/synchronized\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index e6d70f2c..00000000 --- "a/Java/codes/thread/synchronized\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,97 +0,0 @@ -# synchronized - -## 修饰范围 -先看例子: -```java -public class Test { - private final Object lock = new Object(); - - private static int money = 0; - - // 非静态方法 - public synchronized void noStaticMethod() { - money++; - } - // 静态方法 - public static synchronized void staticMethod() { - money++; - } - public void codeBlock() { - // 代码块 - synchronized(lock) { - money++; - } - } -} -``` -- **修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁** -- **修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁** -- **修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁** - -## synchronized和ReentrantLock对比 -1. **两者都是可重入锁**:两者都是可重入锁。“可重入锁”概念是:**自己可以再次获取自己的内部锁**。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 -2. **Synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API**:synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 -3. **ReenTrantLock 比 Synchronized 增加了一些高级功能** - 1. **等待可中断**:过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 - 2. **可实现公平锁** - 3. **可实现选择性通知(锁可以绑定多个条件)**:线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” - 4. **性能已不是选择标准**:在jdk1.6之前synchronized 关键字吞吐量随线程数的增加,下降得非常严重。1.6之后,**synchronized 和 ReenTrantLock 的性能基本是持平了。** - -## 底层原理 -> **synchronized 关键字底层原理属于 JVM 层面。** - -1. **synchronized 同步语句块的情况** - -**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 - -1. **synchronized 修饰方法的的情况** - -synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 - -## 1.6版本的优化 -> 在 Java 早期版本中,`synchronized` 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 **Mutex Lock** 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要**操作系统**帮忙完成,而操作系统实现线程之间的切换时需要从**用户态转换到内核态**,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如**自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等**技术来减少锁操作的开销。 - -锁主要存在四中状态,依次是:**无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态**,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 - -1. **偏向锁** - **引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉**。 - 偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步! - 但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。**升级过程:** - 1. 访问Mark Word中**偏向锁的标识是否设置成1**,**锁标识位是否为01**,确认偏向状态 - 2. 如果为可偏向状态,则判断**当前线程ID是否为偏向线程** - 3. 如果偏向线程未当前线程,则通过**cas操作竞争锁**,如果竞争成功则操作Mark Word中线程ID设置为当前线程ID - 4. 如果cas偏向锁获取失败,则挂起当前偏向锁线程,偏向锁升级为**轻量级锁** - -3. **轻量级锁** - 倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。**轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。** - **轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!升级过程** : - 1. 线程由偏向锁升级为轻量级锁时,会先把**锁的对象头MarkWord复制一份到线程的栈帧中,建立一个名为锁记录空间(Lock Record),用于存储当前Mark Word的拷贝**。 - 2. 虚拟机使用cas操作尝试将**对象的Mark Word指向Lock Record的指针,并将Lock record里的owner指针指对象的Mark Word**。 - 3. 如果cas操作成功,则该线程拥有了对象的轻量级锁。第二个线程cas自旋锁等待锁线程释放锁。 - 4. 如果多个线程竞争锁,轻量级锁要膨胀为**重量级锁**,**Mark Word中存储的就是指向重量级锁(互斥量)的指针**。其他等待线程进入阻塞状态。 - -5. **自旋锁和自适应自旋** - 轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。 - 互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。 - - **一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。 - - **在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。 - -6. **锁消除** - - 锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。 - -7. **锁粗化** - - 原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。 - -8. **总结升级过程**: - - 1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁 - 2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1 - 3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。 - 4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁 - 5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 - 6. 如果自旋成功则依然处于轻量级状态。 - 7. 如果自旋失败,则升级为重量级锁。 diff --git "a/Java/codes/thread/volatile\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/thread/volatile\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index fbba865f..00000000 --- "a/Java/codes/thread/volatile\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,164 +0,0 @@ -# volatile -> 那肯定介绍的它的两大特性:内存可见性和可序性。当然,还会介绍它的底层原理 - -## 内存可见性 -```java -public class Test { - private volatile boolean isStart = true; - void m() { - System.out.println(Thread.currentThread().getName() + " start..."); - while (isStart) {} - System.out.println(Thread.currentThread().getName() + " end..."); - } - - public static void main(String[] args) { - Test t1 = new Test(); - new Thread(t1::m, "t1").start(); - try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } - t1.isStart = false; - } -} -``` - -分析:一开始isReady为true,m方法中的while会一直循环,而主线程开启开线程之后会延迟1s将isReady赋值为false,若不加volatile修饰,则程序一直在运行,若加了volatile修饰,则程序最后会输出t1 m end... - -## 可序性 -> 有序性是指程序代码的执行是按照代码的实现顺序来按序执行的;volatile的有序性特性则是指禁止JVM指令重排优化。 -```java -public class Test { - private volatile static Test instance = null; - private Test(){} - - private static Test getInstance() { - if (instance != null) { - synchronized (Test.class) { - if (instance != null) { - instance = new Test(); - } - } - } - return instance; - } -} -``` -上面的代码是一个很常见的单例模式实现方式,但是上述代码在多线程环境下是有问题的。为什么呢,问题出在instance对象的初始化上,因为`instance = new Singleton();`这个初始化操作并不是原子的,在JVM上会对应下面的几条指令: -``` -memory =allocate(); //1. 分配对象的内存空间 -ctorInstance(memory); //2. 初始化对象 -instance =memory; //3. 设置instance指向刚分配的内存地址 -``` -上面三个指令中,步骤2依赖步骤1,但是步骤3不依赖步骤2,所以JVM可能针对他们进行指令重拍序优化,重排后的指令如下: -``` -memory =allocate(); //1. 分配对象的内存空间 -instance =memory; //3. 设置instance指向刚分配的内存地址 -ctorInstance(memory); //2. 初始化对象 -``` -这样优化之后,内存的初始化被放到了instance分配内存地址的后面,这样的话当线程1执行步骤3这段赋值指令后,刚好有另外一个线程2进入getInstance方法判断instance不为null,这个时候线程2拿到的instance对应的内存其实还未初始化,这个时候拿去使用就会导致出错。 - -所以我们在用这种方式实现单例模式时,会使用volatile关键字修饰instance变量,这是因为volatile关键字除了可以保证变量可见性之外,还具有防止指令重排序的作用。当用volatile修饰instance之后,JVM执行时就不会对上面提到的初始化指令进行重排序优化,这样也就不会出现多线程安全问题了。 - -## 不能保证原子性 -> 20个线程,每个线程count累加10000次,最后count理论上是200000 -```java -public class Test { - volatile int count = 0; - private CountDownLatch latch; - - public Test(CountDownLatch latch) { - this.latch = latch; - } - - void m() { - for (int i = 0; i < 10000; i++) { - count++; - } - latch.countDown(); - } - - public static void main(String[] args) throws InterruptedException { - CountDownLatch latch = new CountDownLatch(20); - Test t1 = new Test(latch); - for (int i = 0; i < 20; i++) { - new Thread(t1::m, "Thread " + i).start(); - } - latch.await(); - System.out.println(t1.count); - } -// 85121 -``` -分析图:[https://www.processon.com/view/link/5e130e51e4b07db4cfac9d2c](https://www.processon.com/view/link/5e130e51e4b07db4cfac9d2c) - -## 整一波内存屏障 -> **Java的Volatile的特征是任何读都能读到最新值,本质上是JVM通过内存屏障来实现的;为了实现volatile内存语义,JMM会分别限制重排序类型。下面是JMM针对编译器制定的volatile重排序规则表:** - - | 是否能重排序 | 第二个操作 | | | -| :----------: | :--------: | :--------: | :--------: | - | 第一个操作 | 普通读/写 | volatile读 | volatile写 | - | 普通读/写 | | | no | - | volatile读 | no | no | no | - | volatile写 | | no | no | - -**从上表我们可以看出**: -- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。 -- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。 -- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。 -**为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:** -- 在每个volatile写操作的前面插入一个StoreStore屏障。 -- 在每个volatile写操作的后面插入一个StoreLoad屏障。 -- 在每个volatile读操作的后面插入一个LoadLoad屏障。 -- 在每个volatile读操作的后面插入一个LoadStore屏障。 - -**volatile写插入内存指令图:** -![volatile写插入](https://image-static.segmentfault.com/416/041/416041851-5ac871370fec9_articlex) - -**上图中的StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。** - -> **这里比较有意思的是volatile写后面的StoreLoad屏障。这个屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面,是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确实现volatile的内存语义,JMM在这里采取了保守策略:在每个volatile写的后面或在每个volatile读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里我们可以看到JMM在实现上的一个特点:首先确保正确性,然后再去追求执行效率。** - -**volatile读插入内存指令图:** -![volatile读](https://image-static.segmentfault.com/288/764/2887649856-5ac871c442f52_articlex) - -**上图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。** - -**上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面我们通过具体的示例代码来说明:** - -```java -class VolatileBarrierExample { - int a; - volatile int v1 = 1; - volatile int v2 = 2; - - void readAndWrite() { - int i = v1; //第一个volatile读 - int j = v2; // 第二个volatile读 - a = i + j; //普通写 - v1 = i + 1; // 第一个volatile写 - v2 = j * 2; //第二个 volatile写 - } -} -``` -针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化: -![readAndWrite](https://image-static.segmentfault.com/178/456/1784565222-5ac871e5e6dec_articlex) - -**注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器常常会在这里插入一个StoreLoad屏障。** - -## volatile的汇编 -```c -0x000000011214bb49: mov %rdi,%rax -0x000000011214bb4c: dec %eax -0x000000011214bb4e: mov %eax,0x10(%rsi) -0x000000011214bb51: lock addl $0x0,(%rsp) ;*putfield v1 - ; - com.earnfish.VolatileBarrierExample::readAndWrite@21 (line 35) - -0x000000011214bb56: imul %edi,%ebx -0x000000011214bb59: mov %ebx,0x14(%rsi) -0x000000011214bb5c: lock addl $0x0,(%rsp) ;*putfield v2 - ; - com.earnfish.VolatileBarrierExample::readAndWrite@28 (line 36) -``` -**可见其本质是通过一个lock指令来实现的。那么lock是什么意思呢?** - -**它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。** - -- 锁住内存 -- 任何读必须在写完成之后再执行 -- 使其它线程这个值的栈缓存失效 diff --git "a/Java/codes/thread/\344\270\273\347\272\277\347\250\213\347\255\211\345\276\205\345\244\232\344\270\252\345\255\220\347\272\277\347\250\213\347\232\204\344\276\213\345\255\220.md" "b/Java/codes/thread/\344\270\273\347\272\277\347\250\213\347\255\211\345\276\205\345\244\232\344\270\252\345\255\220\347\272\277\347\250\213\347\232\204\344\276\213\345\255\220.md" deleted file mode 100644 index 41239eb7..00000000 --- "a/Java/codes/thread/\344\270\273\347\272\277\347\250\213\347\255\211\345\276\205\345\244\232\344\270\252\345\255\220\347\272\277\347\250\213\347\232\204\344\276\213\345\255\220.md" +++ /dev/null @@ -1,73 +0,0 @@ -# 主线程等待多个子线程的例子 - -## sleep -> 这个不常用,但是简单一些 -```java -public class Test { - void m() { - System.out.println(Thread.currentThread().getName()); - } - - public static void main(String[] args) { - Test t1 = new Test(); - for (int i = 0; i < 5; i++) { - new Thread(t1::m, "Thread " + i).start(); - } - try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } - System.out.println("main thread"); - } -} -``` - -## join - -```java -public class Test { - void m() { - System.out.println(Thread.currentThread().getName()); - } - - public static void main(String[] args) { - Test t1 = new Test(); - ArrayList threads = new ArrayList<>(); - for (int i = 0; i < 5; i++) { - threads.add(new Thread(t1::m, "Thread " + i)); - } - threads.forEach(o -> o.start()); - threads.forEach(o -> { - try { - o.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }); - System.out.println("main thread"); - } -} -``` - -## CountDownLatch -```java -public class Test { - private CountDownLatch latch; - - public Test(CountDownLatch latch) { - this.latch = latch; - } - - void m() { - System.out.println(Thread.currentThread().getName()); - latch.countDown(); - } - - public static void main(String[] args) throws InterruptedException { - CountDownLatch countDownLatch = new CountDownLatch(5); - Test t1 = new Test(countDownLatch); - for (int i = 0; i < 5; i++) { - new Thread(t1::m, "Thread " + i).start(); - } - countDownLatch.await(); - System.out.println("main thread"); - } -} -``` \ No newline at end of file diff --git "a/Java/codes/thread/\344\270\276\344\270\252\345\244\232\347\272\277\347\250\213\347\232\204\345\260\217\344\276\213\345\255\220.md" "b/Java/codes/thread/\344\270\276\344\270\252\345\244\232\347\272\277\347\250\213\347\232\204\345\260\217\344\276\213\345\255\220.md" deleted file mode 100644 index eed0016c..00000000 --- "a/Java/codes/thread/\344\270\276\344\270\252\345\244\232\347\272\277\347\250\213\347\232\204\345\260\217\344\276\213\345\255\220.md" +++ /dev/null @@ -1,94 +0,0 @@ -# 举个多线程的小例子 -> 开启10个线程,每个线程对同一个变量进行1000次加1操作。 - -## V1 -```java -public class Test { - int count; - void m() { - for(int i = 0; i < 1000; i++) { - count++; - } - } - - public static void main(String[] args) { - Test t1 = new Test(); - for (int i = 0; i < 10; i++){ - new Thread(t1::m, "Thread " + i).start(); - } - // 等待完成,这里有多种方式... - try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } - System.out.println(t1.count); - } -} -// 运行结果 -// 9378 -// 想知道原因,得需要知道Java内存模型... -``` -## V2(volatile) -```java -public class Test { - volatile int count; - void m() { - for(int i = 0; i < 1000; i++) { - count++; - } - } - - public static void main(String[] args) { - Test t1 = new Test(); - for (int i = 0; i < 10; i++){ - new Thread(t1::m, "Thread " + i).start(); - } - // 等待完成,这里有多种方式... - try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } - System.out.println(t1.count); - } -} -``` -count++是非原子性操作,即使使用volatile保证内存可见性,但是无法保证原子性,因此,还是凉凉 - -## V3(synchronized) -```java -public class Test { - int count; - synchronized void m() { - for(int i = 0; i < 1000; i++) { - count++; - } - } - - public static void main(String[] args) { - Test t1 = new Test(); - for (int i = 0; i < 10; i++){ - new Thread(t1::m, "Thread " + i).start(); - } - // 等待完成,这里有多种方式... - try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } - System.out.println(t1.count); - } -} -``` -这里可以得到线程安全同步,但是效率似乎有点慢,毕竟这个操作是自增。 - -## V4(CAS) -```java -public class Test { - AtomicInteger count = new AtomicInteger(0); - void m() { - count.incrementAndGet(); - } - - public static void main(String[] args) { - Test t1 = new Test(); - for (int i = 0; i < 10; i++){ - new Thread(t1::m, "Thread " + i).start(); - } - // 等待完成,这里有多种方式... - try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } - System.out.println(t1.count); - } -} -``` - -像Volatile、synchronized、CAS稍后再说... \ No newline at end of file diff --git "a/Java/codes/thread/\346\255\273\351\224\201\344\276\213\345\255\220.md" "b/Java/codes/thread/\346\255\273\351\224\201\344\276\213\345\255\220.md" deleted file mode 100644 index f63c7eed..00000000 --- "a/Java/codes/thread/\346\255\273\351\224\201\344\276\213\345\255\220.md" +++ /dev/null @@ -1,38 +0,0 @@ -# 死锁例子 - -## 死锁的四个条件 -- 互斥条件:该资源任意一个时刻只由一个线程占用。(同一时刻,这个碗是我的,你不能碰) -- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(我拿着这个碗一直不放) -- 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。(我碗中的饭没吃完,你不能抢,释放权是我自己的,我想什么时候放就什么时候放) -- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(我拿了A碗,你拿了B碗,但是我还想要你的B碗,你还想我的A碗) - -## 例子 -```java -public class Test { - private static Object res1 = new Object(); - private static Object res2 = new Object(); - - public static void main(String[] args) { - new Thread(() -> { - synchronized (res1) { - System.out.println(Thread.currentThread().getName() + " res1"); - // 延迟一下, 确保B拿到了res2 - try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } - synchronized (res2) { - System.out.println(Thread.currentThread().getName() + " res2"); - } - } - }, "ThreadA").start(); - - new Thread(() -> { - synchronized (res2) { - System.out.println(Thread.currentThread().getName() + " res2"); - // 延迟一下,确保A拿到了res1 - synchronized (res1) { - System.out.println(Thread.currentThread().getName() + " res1"); - } - } - }, "ThreadB").start(); - } -} -``` \ No newline at end of file diff --git "a/Java/codes/thread/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205\347\232\204\344\270\211\347\247\215\344\276\213\345\255\220.md" "b/Java/codes/thread/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205\347\232\204\344\270\211\347\247\215\344\276\213\345\255\220.md" deleted file mode 100644 index d68976cc..00000000 --- "a/Java/codes/thread/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205\347\232\204\344\270\211\347\247\215\344\276\213\345\255\220.md" +++ /dev/null @@ -1,192 +0,0 @@ -# 生产者和消费者 -> 在这里有三种方式实现:synchronized、ReentrantLock和BlockingQueue - -## synchronized -```java -public class ProdConsumerSynchronized { - - private final LinkedList lists = new LinkedList<>(); - - public synchronized void put(String s) { - while (lists.size() != 0) { // 用while怕有存在虚拟唤醒线程 - // 满了, 不生产了 - try { - this.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - lists.add(s); - System.out.println(Thread.currentThread().getName() + " " + lists.peekFirst()); - this.notifyAll(); // 这里可是通知所有被挂起的线程,包括其他的生产者线程 - } - - public synchronized void get() { - while (lists.size() == 0) { - try { - this.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - System.out.println(Thread.currentThread().getName() + " " + lists.removeFirst()); - this.notifyAll(); // 通知所有被wait挂起的线程 用notify可能就死锁了。 - } - - public static void main(String[] args) { - ProdConsumerSynchronized prodConsumerSynchronized = new ProdConsumerSynchronized(); - - // 启动消费者线程 - for (int i = 0; i < 5; i++) { - new Thread(prodConsumerSynchronized::get, "ConsA" + i).start(); - } - - // 启动生产者线程 - for (int i = 0; i < 5; i++) { - int tempI = i; - new Thread(() -> { - prodConsumerSynchronized.put("" + tempI); - }, "ProdA" + i).start(); - } - } -} -``` - -## ReentrantLock -```java -public class ProdConsumerReentrantLock { - - private LinkedList lists = new LinkedList<>(); - - private Lock lock = new ReentrantLock(); - - private Condition prod = lock.newCondition(); - - private Condition cons = lock.newCondition(); - - public void put(String s) { - lock.lock(); - try { - // 1. 判断 - while (lists.size() != 0) { - // 等待不能生产 - prod.await(); - } - // 2.干活 - lists.add(s); - System.out.println(Thread.currentThread().getName() + " " + lists.peekFirst()); - // 3. 通知 - cons.signalAll(); - } catch (Exception e) { - e.printStackTrace(); - } finally { - lock.unlock(); - } - } - - public void get() { - lock.lock(); - try { - // 1. 判断 - while (lists.size() == 0) { - // 等待不能消费 - cons.await(); - } - // 2.干活 - System.out.println(Thread.currentThread().getName() + " " + lists.removeFirst()); - // 3. 通知 - prod.signalAll(); - } catch (Exception e) { - e.printStackTrace(); - } finally { - lock.unlock(); - } - } - - public static void main(String[] args) { - ProdConsumerReentrantLock prodConsumerReentrantLock = new ProdConsumerReentrantLock(); - for (int i = 0; i < 5; i++) { - int tempI = i; - new Thread(() -> { - prodConsumerReentrantLock.put(tempI + ""); - }, "ProdA" + i).start(); - } - for (int i = 0; i < 5; i++) { - new Thread(prodConsumerReentrantLock::get, "ConsA" + i).start(); - } - } -} -``` -## BlockingQueue -```java -public class ProdConsumerBlockingQueue { - - private volatile boolean flag = true; - - private AtomicInteger atomicInteger = new AtomicInteger(); - - BlockingQueue blockingQueue = null; - - public ProdConsumerBlockingQueue(BlockingQueue blockingQueue) { - this.blockingQueue = blockingQueue; - } - - public void myProd() throws Exception { - String data = null; - boolean retValue; - while (flag) { - data = atomicInteger.incrementAndGet() + ""; - retValue = blockingQueue.offer(data, 2, TimeUnit.SECONDS); - if (retValue) { - System.out.println(Thread.currentThread().getName() + " 插入队列" + data + " 成功"); - } else { - System.out.println(Thread.currentThread().getName() + " 插入队列" + data + " 失败"); - } - TimeUnit.SECONDS.sleep(1); - } - System.out.println(Thread.currentThread().getName() + " 大老板叫停了,flag=false,生产结束"); - } - - public void myConsumer() throws Exception { - String result = null; - while (flag) { - result = blockingQueue.poll(2, TimeUnit.SECONDS); - if (null == result || result.equalsIgnoreCase("")) { - flag = false; - System.out.println(Thread.currentThread().getName() + " 超过2s没有取到蛋糕,消费退出"); - return; - } - System.out.println(Thread.currentThread().getName() + " 消费队列" + result + "成功"); - } - } - - public void stop() { - flag = false; - } - - public static void main(String[] args) { - ProdConsumerBlockingQueue prodConsumerBlockingQueue = new ProdConsumerBlockingQueue(new ArrayBlockingQueue<>(10)); - new Thread(() -> { - System.out.println(Thread.currentThread().getName() + " 生产线程启动"); - try { - prodConsumerBlockingQueue.myProd(); - } catch (Exception e) { - e.printStackTrace(); - } - }, "Prod").start(); - - new Thread(() -> { - System.out.println(Thread.currentThread().getName() + " 消费线程启动"); - try { - prodConsumerBlockingQueue.myConsumer(); - } catch (Exception e) { - e.printStackTrace(); - } - }, "Consumer").start(); - - try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } - System.out.println("5s后main叫停,线程结束"); - prodConsumerBlockingQueue.stop(); - } -} -``` \ No newline at end of file diff --git "a/Java/codes/thread/\347\272\277\347\250\213\346\261\240\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/thread/\347\272\277\347\250\213\346\261\240\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index b5afec2a..00000000 --- "a/Java/codes/thread/\347\272\277\347\250\213\346\261\240\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,136 +0,0 @@ -# 线程池 -线程池的好处 -- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 -- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - -## FixedThreadPool -`FixedThreadPool` 被称为可重用固定线程数的线程池。通过 Executors 类中的相关源代码来看一下相关实现: -```java -/** - * 创建一个可重用固定数量线程的线程池 - */ -public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { - return new ThreadPoolExecutor(nThreads, nThreads, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(), - threadFactory); -} -``` -**从上面源代码可以看出新创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 都被设置为 nThreads,这个 nThreads 参数是我们使用的时候自己传递的。** -1. 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务; -2. 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 `LinkedBlockingQueue`; -3. 线程池中的线程执行完 手头的任务后,会在循环中反复从 `LinkedBlockingQueue` 中获取任务来执行; - -不推荐使用 - -**`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :** - -1. 当线程池中的线程数达到 `corePoolSize` 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize; -2. 由于使用无界队列时 `maximumPoolSize` 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 `FixedThreadPool`的源码可以看出创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 被设置为同一个值。 -3. 由于 1 和 2,使用无界队列时 `keepAliveTime` 将是一个无效参数; -4. 运行中的 `FixedThreadPool`(未执行 `shutdown()`或 `shutdownNow()`)不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。 - -## SingleThreadExecutor -```java -/** - *返回只有一个线程的线程池 - */ -public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { - return new FinalizableDelegatedExecutorService - (new ThreadPoolExecutor(1, 1, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(), - threadFactory)); -} -``` -和上面一个差不多,只不过core和max都被设置为1 - -## CachedThreadPool -```java -/** - * 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。 - */ -public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { - return new ThreadPoolExecutor(0, Integer.MAX_VALUE, - 60L, TimeUnit.SECONDS, - new SynchronousQueue(), - threadFactory); -} -``` -`CachedThreadPool` 的` corePoolSize` 被设置为空(0),`maximumPoolSize `被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 `maximumPool` 中线程处理任务的速度时,`CachedThreadPool` 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。 - -1. 首先执行 `SynchronousQueue.offer(Runnable task)` 提交任务到任务队列。如果当前 `maximumPool` 中有闲线程正在执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`,那么主线程执行 offer 操作与空闲线程执行的 `poll` 操作配对成功,主线程把任务交给空闲线程执行,`execute()`方法执行完成,否则执行下面的步骤 2; -2. 当初始 `maximumPool` 为空,或者 `maximumPool` 中没有空闲线程时,将没有线程执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`。这种情况下,步骤 1 将失败,此时 `CachedThreadPool` 会创建新线程执行任务,execute 方法执行完成; - -## ThreadPoolExecutor(重点) -```java -/** - * 用给定的初始参数创建一个新的ThreadPoolExecutor。 - */ -public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 - int maximumPoolSize,//线程池的最大线程数 - long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间 - TimeUnit unit,//时间单位 - BlockingQueue workQueue,//任务队列,用来储存等待执行任务的队列 - ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可 - RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务 - ) -``` - -**`ThreadPoolExecutor` 3 个最重要的参数:** - -- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 -- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。 - -`ThreadPoolExecutor`其他常见参数: - -- **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁; -- **`unit`** : `keepAliveTime` 参数的时间单位。 -- **`threadFactory`** :executor 创建新线程的时候会用到。 -- **`handler`** :饱和策略。关于饱和策略下面单独介绍一下. - -![线程池各个参数的关系](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/线程池各个参数的关系.jpg) - -- **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 -- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 -- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 - -> Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。) - -**Executors 返回线程池对象的弊端如下**: - -- **`FixedThreadPool` 和 `SingleThreadExecutor`** : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。 -- **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 - -```java -public class ThreadPoolExecutorDemo { - public static void main(String[] args) { - ExecutorService threadpools = new ThreadPoolExecutor( - 3, - 5, - 1l, - TimeUnit.SECONDS, - new LinkedBlockingDeque<>(3), - Executors.defaultThreadFactory(), - new ThreadPoolExecutor.AbortPolicy()); -//new ThreadPoolExecutor.AbortPolicy(); -//new ThreadPoolExecutor.CallerRunsPolicy(); -//new ThreadPoolExecutor.DiscardOldestPolicy(); -//new ThreadPoolExecutor.DiscardPolicy(); - try { - for (int i = 0; i < 8; i++) { - threadpools.execute(() -> { - System.out.println(Thread.currentThread().getName() + "\t办理业务"); - }); - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - threadpools.shutdown(); - } - } -} -``` \ No newline at end of file diff --git "a/Java/codes/thread/\347\272\277\347\250\213\347\224\237\345\221\275\345\221\250\346\234\237\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/codes/thread/\347\272\277\347\250\213\347\224\237\345\221\275\345\221\250\346\234\237\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index c6d28b1e..00000000 --- "a/Java/codes/thread/\347\272\277\347\250\213\347\224\237\345\221\275\345\221\250\346\234\237\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,90 +0,0 @@ -# 线程的生命周期 - -先放一张图: -![线程周期图](https://www.pdai.tech/_images/pics/ace830df-9919-48ca-91b5-60b193f593d2.png) - -线程创建之后它将处于**New**(新建)状态,调用 **start()** 方法后开始运行,线程这时候处于 **READY**(可运行) 状态。可运行状态的线程获得了 **CPU 时间片**(timeslice)后就处于 **RUNNING**(运行) 状态。 - -- 当线程执行 **wait()**方法之后,线程进入 **WAITING**(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态 -- 而 **TIME_WAITING**(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 **sleep**(long millis)方法或 **wait**(long millis)方法可以将 Java 线程置于 **TIMED_WAITING** 状态。当超时时间到达后 Java 线程将会返回到 **RUNNABLE** 状态。 -- 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞)状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。 - -## 上下文切换 - -> 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,**CPU 采取的策略是为每个线程分配时间片并轮转的形式**。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 -实际上就是任务从保存到再加载的过程就是一次上下文切换。 上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 - -## 直接调用Thread的run方法不行吗? -### 区别 -> Java中启动线程有两种方法,继承Thread类和实现Runnable接口,由于Java无法实现多重继承,所以一般通过实现Runnable接口来创建线程。但是无论哪种方法都可以通过start()和run()方法来启动线程,下面就来介绍一下他们的区别。 - -#### start -通过该方法启动线程的同时**也创建了一个线程,真正实现了多线程**。**无需等待run()方法中的代码执行完毕,就可以接着执行下面的代码**。此时start()的这个线程处于**就绪状态**,当得到CPU的时间片后就会执行其中的run()方法。这个run()方法包含了要执行的这个线程的内容,run()方法运行结束,此线程也就终止了。 - -#### run -通过run方法启动线程其实就是调用一个类中的方法,**当作普通的方法的方式调用**。并没有创建一个线程,程序中依旧只有一个主线程,必须等到run()方法里面的代码执行完毕,才会继续执行下面的代码,这样就没有达到写线程的目的。 - -```java -public class Test { - public static void main(String[] args) { - Thread t = new Thread(){ - public void run() { - cat(); - } - }; - t.run(); - System.out.println("dream"); - } - - static void cat() { - System.out.println("cat"); - } -} -// 结果: -// cat -// dream -``` -如果将`t.run()`改成`t.start()`; -```java -dream -cat -``` - -### 探究一下源码 -看一下start方法源码: -```java -public synchronized void start() { - if (threadStatus != 0) - throw new IllegalThreadStateException(); - group.add(this); - - boolean started = false; - try { - start0(); - started = true; - } finally { - try { - if (!started) { - group.threadStartFailed(this); - } - } catch (Throwable ignore) { - /* do nothing. If start0 threw a Throwable then - it will be passed up the call stack */ - } - } -} - -private native void start0(); -``` - 可以看到,当一个线程启动的时候,它的状态(threadStatus)被设置为0,如果不为0,则抛出`IllegalThreadStateException`异常。正常的话,将该**线程加入线程组**,最后尝试调用start0方法,**而start0方法是私有的native方法**(Native Method是一个java调用非java代码的接口)。 - -我们再看一下run方法源码: -```java -public void run() { - if (target != null) { - target.run(); - } -} -``` -target的定义:`private Runnable target;` -所以其实就是一个Runnable接口,正如上面代码中new Thread的部分,其实我们就是在实现它的run()方法。所以如果**直接调用run,就和一个普通的方法没什么区别,是不会创建新的线程的,因为压根就没执行start0方法**。 diff --git "a/Java/codes/thread/\347\272\277\347\250\213\347\232\204\345\220\257\345\212\250\346\226\271\345\274\217.md" "b/Java/codes/thread/\347\272\277\347\250\213\347\232\204\345\220\257\345\212\250\346\226\271\345\274\217.md" deleted file mode 100644 index 08795ae1..00000000 --- "a/Java/codes/thread/\347\272\277\347\250\213\347\232\204\345\220\257\345\212\250\346\226\271\345\274\217.md" +++ /dev/null @@ -1,53 +0,0 @@ -# 线程的启动方式 - -## Thread -```java -public class Test extents Thread { - public void run() { - // 重写Thread的run方法 - System.out.println("dream"); - } - - public static void main(String[] args) { - new Test().start(); - } -} -``` - -## Runnable -```java -public class Test { - public static void main(String[] args) { - new Thread(() -> { - System.out.println("dream"); - }).start(); - } -} -``` - -## Callable -```java -public class Test { - public static void main(String[] args) { - // FutureTask 构造方法包装了Callable和Runnable。 - FutureTask task = new FutureTask<>(() -> { - System.out.println("dream"); - return 0; - }); - new Thread(task).start(); - } -} -``` - -## 线程池 -```java -public class Test { - public static void main(String[] args) { - ExecutorService threadPool = Executors.newFixedThreadPool(1); - threadPool.submit(() -> { - System.out.println("dream"); - }); - threadPool.shutdown(); - } -} -``` diff --git a/Java/crazy/.DS_Store b/Java/crazy/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/Java/crazy/.DS_Store and /dev/null differ diff --git "a/Java/linux/linux-\345\237\272\347\241\200.md" "b/Java/linux/linux-\345\237\272\347\241\200.md" deleted file mode 100644 index fb815ec4..00000000 --- "a/Java/linux/linux-\345\237\272\347\241\200.md" +++ /dev/null @@ -1,695 +0,0 @@ -## 引言 - -**linux-基础** - -## 常用操作以及概念 - -### 快捷键 - -- Tab: 命令和文件名补全; -- Ctrl+C: 中断正在运行的程序; -- Ctrl+D: 结束键盘输入(End Of File,EOF) - -### 求助 - - - -#### --help - -指令的基本用法与选项介绍。 - -#### man - -man 是 manual 的缩写,将指令的具体信息显示出来。 - -#### info - -info 与 man 类似,但是 info 将文档分成一个个页面,每个页面可以进行跳转。 - -#### doc - -/usr/share/doc 存放着软件的一整套说明文件。 - -### 关机 - -#### who - -在关机前需要先使用 who 命令查看有没有其它用户在线。 - -#### sync - -为了加快对磁盘文件的读写速度,位于内存中的文件数据不会立即同步到磁盘上,因此关机之前需要先进行 sync 同步操作。 - -#### shutdown - -```bash -## shutdown [-krhc] 时间 [信息] --k : 不会关机,只是发送警告信息,通知所有在线的用户 --r : 将系统的服务停掉后就重新启动 --h : 将系统的服务停掉后就立即关机 --c : 取消已经在进行的 shutdown 指令内容 -``` - -### PATH - -可以在环境变量 PATH 中声明可执行文件的路径,路径之间用 : 分隔。 - -`/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin` - -### sudo - -sudo 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令. - -### 包管理工具 - -RPM 和 DPKG 为最常见的两类软件包管理工具: - -- RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。 -- 与 RPM 进行竞争的是基于 Debian 操作系统 (Ubuntu) 的 DEB 软件包管理工具 DPKG,全称为 Debian Package,功能方面与 RPM 相似。 - -YUM 基于 RPM,具有依赖管理功能,并具有软件升级的功能。 - -### 发行版 - -Linux 发行版是 Linux 内核及各种应用软件的集成版本。 - -| 基于的包管理工具 | 商业发行版 | 社区发行版 | -| :--------------: | :--------: | :-------------: | -| RPM | Red Hat | Fedora / CentOS | -| DPKG | Ubuntu | Debian | - -### VIM - -- 一般指令模式(Command mode): VIM 的默认模式,可以用于移动游标查看内容; -- 编辑模式(Insert mode): 按下 "i" 等按键之后进入,可以对文本进行编辑; -- 指令列模式(Bottom-line mode): 按下 ":" 按键之后进入,用于保存退出等操作。 - -在指令列模式下,有以下命令用于离开或者保存文件。 - -| 命令 | 作用 | -| :--: | :----------------------------------------------------------: | -| :w | 写入磁盘 | -| :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 | -| :q | 离开 | -| :q! | 强制离开不保存 | -| :wq | 写入磁盘后离开 | -| :wq! | 强制写入磁盘后离开 | - -## 磁盘 - -### 磁盘接口 - -#### IDE - -IDE(ATA)全称 Advanced Technology Attachment,接口速度最大为 133MB/s,因为并口线的抗干扰性太差,且排线占用空间较大,不利电脑内部散热,已逐渐被 SATA 所取代。 - -#### SATA - -SATA 全称 Serial ATA,也就是使用串口的 ATA 接口,抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能。SATA-II 的接口速度为 300MiB/s,而新的 SATA-III 标准可达到 600MiB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。 - -#### SCSI - -SCSI 全称是 Small Computer System Interface(小型机系统接口),经历多代的发展,从早期的 SCSI-II 到目前的 Ultra320 SCSI 以及 Fiber-Channel(光纤通道),接口型式也多种多样。SCSI 硬盘广为工作站级个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。 - -#### SAS - -SAS(Serial Attached SCSI)是新一代的 SCSI 技术,和 SATA 硬盘相同,都是采取序列式技术以获得更高的传输速度,可达到 6Gb/s。此外也透过缩小连接线改善系统内部空间等。 - -## 分区 - -### 分区表 - -磁盘分区表主要有两种格式,一种是限制较多的 MBR 分区表,一种是较新且限制较少的 GPT 分区表。 - -#### MBR - -MBR 中,第一个扇区最重要,里面有主要开机记录(Master boot record, MBR)及分区表(partition table),其中主要开机记录占 446 bytes,分区表占 64 bytes。 - -分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它使用其它扇区用记录额外的分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。 - -Linux 也把分区当成文件,分区文件的命名方式为: 磁盘文件名 + 编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。 - -#### GPT - -不同的磁盘有不同的扇区大小,例如 512 bytes 和最新磁盘的 4 k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA),LBA 默认大小为 512 bytes。 - -GPT 第 1 个区块记录了主要开机记录(MBR),紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。这 33 个区块第一个为 GPT 表头纪录,这个部份纪录了分区表本身的位置与大小和备份分区的位置,同时放置了分区表的校验码 (CRC32),操作系统可以根据这个校验码来判断 GPT 是否正确。若有错误,可以使用备份分区进行恢复。 - -GPT 没有扩展分区概念,都是主分区,每个 LAB 可以分 4 个分区,因此总共可以分 4 * 32 = 128 个分区。 - -MBR 不支持 2.2 TB 以上的硬盘,GPT 则最多支持到 233 TB = 8 ZB。 - -### 开机检测程序 - -#### BIOS - -BIOS(Basic Input/Output System,基本输入输出系统),它是一个固件(嵌入在硬件中的软件),BIOS 程序存放在断电后内容不会丢失的只读内存中。 - -BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的主要开机记录(MBR),由主要开机记录(MBR)执行其中的开机管理程序,这个开机管理程序会加载操作系统的核心文件。 - -主要开机记录(MBR)中的开机管理程序提供以下功能: 选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动开机管理程序时,就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。 - -下图中,第一扇区的主要开机记录(MBR)中的开机管理程序提供了两个选单: M1、M2,M1 指向了 Windows 操作系统,而 M2 指向其它分区的启动扇区,里面包含了另外一个开机管理程序,提供了一个指向 Linux 的选单。 - -安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉主要开机记录(MBR),而 Linux 可以选择将开机管理程序安装在主要开机记录(MBR)或者其它分区的启动扇区,并且可以设置开机管理程序的选单。 - -#### UEFI - -BIOS 不可以读取 GPT 分区表,而 UEFI 可以。 - -## 文件系统 - -### 分区与文件系统 - -对分区进行格式化是为了在分区上建立文件系统。一个分区通常只能格式化为一个文件系统,但是磁盘阵列等技术可以将一个分区格式化为多个文件系统。 - -### 组成 - -最主要的几个组成部分如下: - -- inode: 一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号; -- block: 记录文件的内容,文件太大时,会占用多个 block。 -- superblock: 记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等; -- block bitmap: 记录 block 是否被使用的位域。 - -### 文件读取 - -对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block,然后把所有 block 的内容读出来。 - -### 磁盘碎片 - -指一个文件内容所在的 block 过于分散。 - -### Block - -在 Ext2 文件系统中所支持的 block 大小有 1K,2K 及 4K 三种,不同的大小限制了单个文件和文件系统的最大大小。 - -一个 block 只能被一个文件所使用,未使用的部分直接浪费了。因此如果需要存储大量的小文件,那么最好选用比较小的 block。 - -### inode - -inode 具体包含以下信息: - -- 权限 (read/write/excute); -- 拥有者与群组 (owner/group); -- 容量; -- 建立或状态改变的时间 (ctime); -- 最近一次的读取时间 (atime); -- 最近修改的时间 (mtime); -- 定义文件特性的旗标 (flag),如 SetUID...; -- 该文件真正内容的指向 (pointer)。 - -inode 具有以下特点: - -- 每个 inode 大小均固定为 128 bytes (新的 ext4 与 xfs 可设定到 256 bytes); -- 每个文件都仅会占用一个 inode。 - -inode 中记录了文件内容所在的 block 编号,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block 编号。因此引入了间接、双间接、三间接引用。间接引用是指,让 inode 记录的引用 block 块记录引用信息。 - -### 目录 - -建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。 - -可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。 - -### 日志 - -如果突然断电,那么文件系统会发生错误,例如断电前只修改了 block bitmap,而还没有将数据真正写入 block 中。 - -ext3/ext4 文件系统引入了日志功能,可以利用日志来修复文件系统。 - -### 挂载 - -挂载利用目录作为文件系统的进入点,也就是说,进入目录之后就可以读取文件系统的数据。 - -### 目录配置 - -为了使不同 Linux 发行版本的目录结构保持一致性,Filesystem Hierarchy Standard (FHS) 规定了 Linux 的目录结构。最基础的三个目录如下: - -- / (root, 根目录) -- /usr (unix software resource): 所有系统默认软件都会安装到这个目录; -- /var (variable): 存放系统或程序运行过程中的数据文件。 - -## 文件 - -### 文件属性 - -用户分为三种: 文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。 - -使用 ls 查看一个文件时,会显示一个文件的信息,例如 `drwxr-xr-x. 3 root root 17 May 6 00:14 .config`,对这个信息的解释如下: - -- drwxr-xr-x: 文件类型以及权限,第 1 位为文件类型字段,后 9 位为文件权限字段 -- 3: 链接数 -- root: 文件拥有者 -- root: 所属群组 -- 17: 文件大小 -- May 6 00:14: 文件最后被修改的时间 -- .config: 文件名 - -常见的文件类型及其含义有: - -- d: 目录 -- -: 文件 -- l: 链接文件 - -9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。 - -文件时间有以下三种: - -- modification time (mtime): 文件的内容更新就会更新; -- status time (ctime): 文件的状态(权限、属性)更新就会更新; -- access time (atime): 读取文件时就会更新。 - -### 文件与目录的基本操作 - -#### ls - -列出文件或者目录的信息,目录的信息就是其中包含的文件。 - -```bash -## ls [-aAdfFhilnrRSt] file|dir --a : 列出全部的文件 --d : 仅列出目录本身 --l : 以长数据串行列出,包含文件的属性与权限等等数据 - -``` - -#### cd - -更新当前目录 - -`cd [相对路径或者绝对路径]` - -#### mkdir - -创建目录 - -```bash -## mkdir [-mp] 目录名称 --m : 配置目录权限 --p : 递归创建目录 -``` - -#### rmdir - -删除目录,目录必须为空 - -```bash -rmdir [-p] 目录名称 --p : 递归删除目录 -``` - -#### touch - -更新文件时间或者建立新文件 - -```bash -## touch [-acdmt] filename --a : 更新 atime --c : 更新 ctime,若该文件不存在则不建立新文件 --m : 更新 mtime --d : 后面可以接更新日期而不使用当前日期,也可以使用 --date="日期或时间" --t : 后面可以接更新时间而不使用当前时间,格式为[YYYYMMDDhhmm] - -``` - -#### cp - -复制文件 - -如果源文件有两个以上,则目的文件一定要是目录才行。 - -```bash -cp [-adfilprsu] source destination --a : 相当于 -dr --preserve=all 的意思,至于 dr 请参考下列说明 --d : 若来源文件为链接文件,则复制链接文件属性而非文件本身 --i : 若目标文件已经存在时,在覆盖前会先询问 --p : 连同文件的属性一起复制过去 --r : 递归持续复制 --u : destination 比 source 旧才更新 destination,或 destination 不存在的情况下才复制 ---preserve=all : 除了 -p 的权限相关参数外,还加入 SELinux 的属性, links, xattr 等也复制了 - -``` - -#### rm - -删除文件 - -```bash -## rm [-fir] 文件或目录 --r : 递归删除 -``` - -#### mv - -移动文件 - -```bash -## mv [-fiu] source destination -## mv [options] source1 source2 source3 .... directory --f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖 -``` - -### 修改权限 - -可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1,即每个权限对应的数字权值为 r : 4、w : 2、x : 1。 - -`## chmod [-R] xyz dirname/filename` - -示例: 将 .bashrc 文件的权限修改为 -rwxr-xr--。 - -`## chmod 754 .bashrc` - -也可以使用符号来设定权限。 - -```bash -## chmod [ugoa] [+-=] [rwx] dirname/filename -- u: 拥有者 -- g: 所属群组 -- o: 其他人 -- a: 所有人 -- +: 添加权限 -- -: 移除权限 -- =: 设定权限 -``` - -示例: 为 .bashrc 文件的所有用户添加写权限。 - -`## chmod a+w .bashrc` - -### 文件默认权限 - -- 文件默认权限: 文件默认没有可执行权限,因此为 666,也就是 -rw-rw-rw- 。 -- 目录默认权限: 目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。 - -可以通过 umask 设置或者查看文件的默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r--。 - -### 目录的权限 - -文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。 - -目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。 - -### 链接 - -```bash -## ln [-sf] source_filename dist_filename --s : 默认是 hard link,加 -s 为 symbolic link --f : 如果目标文件存在时,先删除目标文件 -``` - -### 实体链接 - -在目录下创建一个条目,记录着文件名与 inode 编号,这个 inode 就是源文件的 inode。 - -删除任意一个条目,文件还是存在,只要引用数量不为 0。 - -有以下限制: 不能跨越文件系统、不能对目录进行链接。 - -```bash -## ln /etc/crontab . -## ll -i /etc/crontab crontab -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 crontab -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab -``` - -### 符号链接 - -符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 Windows 的快捷方式。 - -当源文件被删除了,链接文件就打不开了。 - -可以为目录建立链接。 - -```bash -## ll -i /etc/crontab /root/crontab2 -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab -53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -> /etc/crontab -``` - -### 获取文件内容 - -#### cat - -取得文件内容。 - -```bash -## cat [-AbEnTv] filename --n : 打印出行号,连同空白行也会有行号,-b 不会 -``` - -#### tac - -是 cat 的反向操作,从最后一行开始打印。 - -#### more - -和 cat 不同的是它可以一页一页查看文件内容,比较适合大文件的查看。 - -#### less - -和 more 类似,但是多了一个向前翻页的功能。 - -#### head - -取得文件前几行。 - -```bash -## head [-n number] filename --n : 后面接数字,代表显示几行的意思 -``` - -#### tail - -是head的反向操作,之水取得是后几行 - -#### od - -以字符或者十六进制的形式显示二进制文件。 - -### 指令与文件搜索 - -#### which - -指令搜索 - -```bash -## which [-a] command --a : 将所有指令列出,而不是只列第一个 -``` - -#### whereis - -文件搜索。速度比较快,因为它只搜索几个特定的目录。 - -```bash -## whereis [-bmsu] dirname/filename -``` - -#### locate - -文件搜索。可以用关键字或者正则表达式进行搜索。 - -locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。 - -```bash -## locate [-ir] keyword --r: 正则表达式 -``` - -#### find - -文件搜索。可以使用文件的属性和权限进行搜索。 - -```bash -## find [basedir] [option] -example: find . -name "shadow*" -``` - -## 压缩与打包 - -### 压缩文件名 - -#### gzip - -gzip 是 Linux 使用最广的压缩指令,可以解开 compress、zip 与 gzip 所压缩的文件。 - -经过 gzip 压缩过,源文件就不存在了。 - -有 9 个不同的压缩等级可以使用。 - -可以使用 zcat、zmore、zless 来读取压缩文件的内容。 - -```bash -$ gzip [-cdtv#] filename --c : 将压缩的数据输出到屏幕上 --d : 解压缩 --t : 检验压缩文件是否出错 --v : 显示压缩比等信息 --## : ## 为数字的意思,代表压缩等级,数字越大压缩比越高,默认为 6 -``` - -#### bzip2 - -提供比 gzip 更高的压缩比。 - -查看命令: bzcat、bzmore、bzless、bzgrep。 - -#### xz - -提供比 bzip2 更佳的压缩比。 - -可以看到,gzip、bzip2、xz 的压缩比不断优化。不过要注意的是,压缩比越高,压缩的时间也越长。 - -查看命令: xzcat、xzmore、xzless、xzgrep。 - -### 打包 - -压缩指令只能对一个文件进行压缩,而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包,也可以使用 gip、bzip2、xz 将打包文件进行压缩。 - -```bash -$ tar [-z|-j|-J] [cv] [-f 新建的 tar 文件] filename... ==打包压缩 -$ tar [-z|-j|-J] [tv] [-f 已有的 tar 文件] ==查看 -$ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩 --z : 使用 zip; --j : 使用 bzip2; --J : 使用 xz; --c : 新建打包文件; --t : 查看打包文件里面有哪些文件; --x : 解打包或解压缩的功能; --v : 在压缩/解压缩的过程中,显示正在处理的文件名; --f : filename: 要处理的文件; --C 目录 : 在特定目录解压缩。 -``` - -| 使用方式 | 命令 | -| :------: | ----------------------------------------------------- | -| 打包压缩 | tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称 | -| 查 看 | tar -jtv -f filename.tar.bz2 | -| 解压缩 | tar -jxv -f filename.tar.bz2 -C 要解压缩的目录 | - -## bash - -可以通过 Shell 请求内核提供服务,Bash 正是 Shell 的一种。 - -### 特性 - -- 命令历史:记录使用过的命令 -- 命令与文件补全:快捷键:tab -- shell scripts -- 通配符: 例如 ls -l /usr/bin/X* 列出 /usr/bin 下面所有以 X 开头的文件 - -### 变量操作 - -对一个变量赋值直接使用 =。 - -对变量取用需要在变量前加上 $ ,也可以用 ${} 的形式; - -输出变量使用 echo 命令。 - -```bash -$ x=abc -$ echo $x -$ echo ${x} -``` - -变量内容如果有空格,必须使用双引号或者单引号。 - -- 双引号内的特殊字符可以保留原本特性,例如 x="lang is $LANG",则 x 的值为 lang is zh_TW.UTF-8; -- 单引号内的特殊字符就是特殊字符本身,例如 x='lang is $LANG',则 x 的值为 lang is $LANG。 - -可以使用 `指令` 或者 $(指令) 的方式将指令的执行结果赋值给变量。例如 version=$(uname -r),则 version 的值为 4.15.0-22-generic。 - -可以使用 `指令` 或者 $(指令) 的方式将指令的执行结果赋值给变量。例如 version=$(uname -r),则 version 的值为 4.15.0-22-generic。 - -Bash 的变量可以声明为数组和整数数字。注意数字类型没有浮点数。如果不进行声明,默认是字符串类型。变量的声明使用 declare 命令: - -```bash -$ declare [-aixr] variable --a : 定义为数组类型 --i : 定义为整数类型 --x : 定义为环境变量 --r : 定义为 readonly 类型 -``` - -对数组操作 - -```bash -$ array[1]=a -$ array[2]=b -$ echo ${array[1]} -``` - -## 正则 - -g/re/p(globally search a regular expression and print),使用正则表示式进行全局查找并打印。 - -```bash -$ grep [-acinv] [--color=auto] 搜寻字符串 filename --c : 统计个数 --i : 忽略大小写 --n : 输出行号 --v : 反向选择,也就是显示出没有 搜寻字符串 内容的那一行 ---color=auto : 找到的关键字加颜色显示 -``` - -示例: 把含有 the 字符串的行提取出来(注意默认会有 --color=auto 选项,因此以下内容在 Linux 中有颜色显示 the 字符串) - -```bash -$ grep -n 'the' regular_express.txt -8:I can't finish the test. -12:the symbol '*' is represented as start. -15:You are the best is mean you are the no. 1. -16:The world Happy is the same with "glad". -18:google is the best tools for search keyword -``` - -## 进程管理 - -### 查看进程 - -#### ps - -查看某个时间点的进程信息 - -示例一: 查看自己的进程`ps -l` - -示例二: 查看系统所有进程`ps aux` - -示例三: 查看特定的进程`ps aux | grep python` - -#### top - -实时显示进程信息 - -示例: 两秒钟刷新一次`top -d 2` - -#### pstree - -查看进程树 - -示例: 查看所有进程树`pstree -A` - -#### netstat - -查看占用端口的进程 - -示例: 查看特定端口的进程`netstat -anp | grep port` - -### 孤儿进程 - -一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。 - -孤儿进程将被init进程(进程号为1)所收养,并有init进程对它们完成状态收集工作。 - -由于孤儿进程会被init进程收养,所以孤儿进程不会对系统造成危害。 - -### 僵尸进程 - -一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过wait()或者waitpid()获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用wait()或waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。 - -僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)。 - -系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。 - -要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。 \ No newline at end of file diff --git a/Java/mianjing/.DS_Store b/Java/mianjing/.DS_Store deleted file mode 100644 index 6b4a9308..00000000 Binary files a/Java/mianjing/.DS_Store and /dev/null differ diff --git "a/Java/other/\345\270\270\347\224\250sql.md" "b/Java/other/\345\270\270\347\224\250sql.md" deleted file mode 100644 index 43109e85..00000000 --- "a/Java/other/\345\270\270\347\224\250sql.md" +++ /dev/null @@ -1,1247 +0,0 @@ - -### 创建数据库并制定编码 - -- ```sql - Create database 数据库名 default character set utf8; - Create database ssm default character set utf8; - ``` - -### 删除数据库 - -- ```sql - drop database 数据名; - drop database ssm; - ``` - - ### 创建表 - -- 一般情况: - -- ```sql - Create table 表名 ( - 列名 类型 约束 auto_increment comment '备注'; - ); - - create tablie flower ( - id int(10) primary key auto_increment comment '编号', - name varchar(30) not null comment '花名', - price float not null comment '价格', - production varchar(30) not null comment '原产地' - ); - - create tablie flower ( - `id` int(10) not null auto_increment comment '编号', - `name` varchar(30) not null comment '花名', - `price` float not null comment '价格', - `production` varchar(30) not null comment '原产地', - primary key (`id`) - ); - ``` - -### 插入信息 - -- ```sql - insert into 表明(姓名,性别,年龄) values('李一', '女', '18'); - insert into `flower` (`id`, `name`, `price`, `production`) values (defalut, '矮牵牛', 2.5, '南美阿根廷') - ``` - -### 查询信息 - -- ```sql - select * from 表名; - select * from flower; - select * from 表名 where ID=5 - select * from flower where ID=1 - ``` - -### 更新信息 - -- ```sql - update 表名 set name='李一' where name='王五'; - update flower set price=1.2 where id=1; - ``` - -### 删除信息 - -- ```sql - delete from 表名 where id=1; - delete from flower where id=1; - ``` - -### 删除表 - -- ```sql - drop table 表名; - drop table flower; - ``` - -### 查看表信息 - -- ```sql - desc 表名; - desc flower; - ``` - -### 一对多关系 - -- 可能是写的不熟练吧,所以要做个笔记,以免之后再次忘掉。 - -- 例子:假如有这样的一对多关系,首先有一个User模型,其中该模型有id、name和gender字段。还有一个Artical模型,其中该模型有id、title、content和user_id字段,而user_id则是外键。如下图: - -- ![](https://raw.githubusercontent.com/DreamCats/PicBed/master/20191121170733.png) - -- 那么在mysql或者mysql的一些工具下创建输入以下代码: - -- ```sql - create database demo default character set utf8; -- 创建数据库 - - use demo; - - -- 创建user表格 - create table user ( - `id` int(10) not null auto_increment comment '编号', - `name` varchar(16) not null comment '姓名', - `gender` varchar(4) not null comment '性别', - primary key (`id`) - ); - - -- 创建artical - create table artical ( - `id` int(10) not null auto_increment comment '文章编号', - `title` varchar(32) not null comment '标题', - `content` text default null comment '内容', - `user_id` int(10) not null comment '用户编号', - primary key (`id`), - key (`user_id`), - constraint `artical_idfk_1` foreign key (`user_id`) references `user` (`id`) -- 这句话非常重要 constraint ··· 约束 - ); - - -- 在user中插入数据 - insert into user (`id`,`name`,`gender`) values(default, 'Maifeng', '男'); - insert into user (`id`,`name`,`gender`) values(default, 'Liumeng', '女'); - - -- 在artical中插入数据 - insert into `artical` (`id`, `title`, `content`, `user_id`) values (default, '我是mm', 'null', 1); - insert into `artical` (`id`, `title`, `content`, `user_id`) values (default, '我是xxx', 'null', 2); - ``` - -- 如下图user和artical表 - -- ![](https://raw.githubusercontent.com/DreamCats/PicBed/master/20191121170817.png) - -- ![](https://raw.githubusercontent.com/DreamCats/PicBed/master/20191121170856.png) - - - -### 更改auto_increment - -- ```sql - alter table 表名 auto_increment=5; - alter table flower auto_increment=5; - ``` - -- - -### 常用sql语句 - -#### 数据库 - -```mysql -# 查看所有数据库 -show databases; - -# 创建一个数据库 -create database demo; - -# 删除一个数据库 -drop dababase demo; - -# 使用这个数据库 -use demo; - -``` - -#### 表 - -```mysql -# 查看所有的表 -show tables; - -# 创建一个表 -create table n(`id` int, `name` varchar(10)); -create table m(`id` int, `name` varchar(10), primary key(`id`), foreign key(`id`) references n(`id`), unique); -create table m(`id` int, `name` varchar(10)); - -# 直接将查询结果导入或复制到新创建的表 -create table n select * from m; - -# 将创建的表与一个存在的表的数据结构类似 -create table m like n; - -# 创建一个临时表 -# 临时表将在你连接MySQL期间存在。当断开连接时,MySQL将自动删除表并释放所用的空间。也可手动删除。 -create temporary table l(`id` int, name varchar(10)); -# 直接将查询结果导入或复制到新的临时表 -create temporary table tt select * from n; - -# 删除一个已经存在的表 -drop table if exists m; - -# 更改存在表的名称 -alter table n rename m; -rename table n to m; - -# 查看表的结构(5种,效果相同) -desc n; -describe n; -show columns in n; -show columns from n; -explain n; -# 查看表的创建语句 -show create table n; -``` - -#### 表的结构 - -```mysql -# 添加字段 -alter table n add age varchar(2); -# 删除字段 -alter table n drop age; -# 更改字段属性和属性 -alter table n change age a int; -# 只更改字段属性 -alter table n modify age varchar(7); -``` - -#### 表的数据 - -```mysql -# 增加数据 -insert into n values(1, 'tom', '23'), (2, 'john', '22'); -insert into n select * from n; # 把数据复制一遍重新插入 -# 删除数据 -delete from n where id = 2; -# 更改数据 -update n set name = 'tom' where id = 2; -# 数据查找 -select * from n where name like '%h%'; -# 数据排序(反序) -select * from n order by name, id desc; -``` - -#### 键 - -```mysql -# 添加主键 -alter table n add primary key(id); -# 删除主键 -alter table n drop primary key; -# 添加外键 -alter table m add foreign key(id) references n(id); # 自动生成键名m_ibfk_1 -# 自定义名称的外键 -alter table m add constraint fk_id foreign key(id) references n(id); -# 删除外键 -alter table m drop foreign key `fk_id`; -# 修改外键 -alter table m drop foreign key `fk_id`, add constraint fk_id2 foreign key(id) references n(id); -# 添加唯一键 -alter table n add unique (name); -alter table n add unique u_name (name); -alter table n add unique index u_name (name); -alter table n add constraint u_name unique (name); -create unique index u_name on n(name); -# 添加索引 -alter table n add index (age); -alter table n add index i_age (age); -create index i_age on n(age); - -# 删除索引或唯一键 -drop index u_name on n; -drop index i_age on n; -``` - -#### 视图 - -```mysql -# 创建视图 -create view v as select id, name from n; -create view v(id, name) as select id, name from n; -# 查看视图(与表操作类似) -select * from v; -desc v; -# 查看视图语句 -show create view v; -# 更改视图 -CREATE OR REPLACE VIEW v AS SELECT name, age FROM n; -ALTER VIEW v AS SELECT name FROM n; -# 删除视图 -drop view if exists v; -``` - -#### 联接 - -```mysql -# 内联接 -select * from m inner join n on m.id = n.id; -# 左外连接 -select * from m left join n on m.id = n.id; -# 右外连接 -select * from m right join n on m.id = n.id; -# 交叉连接 -select * from m cross join n; # 标准写法 -select * from m,n; -# 类似于全连接full join 的联接用法 -select id, name from m -union -select id, name from n -``` - -#### 函数 - -```mysql -# 聚合函数 -select count(id) as total from n; # 总数 -select sum(age) as all_age from n; # 总和 -select avg(age) as all_age from n; # 平均值 -select max(age) as all_age from n; # 最大值 -select min(age) as all_age from n; # 最小值 -# 数学函数 -select abs(-5); # 绝对值 -select bin(15), oct(15), hex(15); # 二进制,八进制,十六进制 -SELECT pi(); # 圆周率3.141593 -SELECT ceil(5.5); # 大于x的最小整数值6 -SELECT floor(5.5); # 小于x的最大整数值5 -SELECT greatest(3,1,4,1,5,9,2,6); # 返回集合中最大的值9 -SELECT least(3,1,4,1,5,9,2,6); # 返回集合中最小的值1 -SELECT mod(5,3); # 余数2 -SELECT rand(); # 返回0到1内的随机值,每次不一样 -SELECT rand(5); # 提供一个参数(种子)使RAND()随机数生成器生成一个指定的值。 -SELECT round(1415.1415); # 四舍五入1415 -SELECT round(1415.1415, 3); # 四舍五入三位数1415.142 -SELECT round(1415.1415, -1); # 四舍五入整数位数1420 -SELECT truncate(1415.1415, 3); # 截短为3位小数1415.141 -SELECT truncate(1415.1415, -1); # 截短为-1位小数1410 -SELECT sign(-5); # 符号的值负数-1 -SELECT sign(5); # 符号的值正数1 -SELECT sqrt(9); # 平方根3 -SELECT sqrt(9); # 平方根3 -# 字符串函数 -SELECT concat('a', 'p', 'p', 'le'); # 连接字符串-apple -SELECT concat_ws(',', 'a', 'p', 'p', 'le'); # 连接用','分割字符串-a,p,p,le -SELECT insert('chinese', 3, 2, 'IN'); # 将字符串'chinese'从3位置开始的2个字符替换为'IN'-chINese -SELECT left('chinese', 4); # 返回字符串'chinese'左边的4个字符-chin -SELECT right('chinese', 3); # 返回字符串'chinese'右边的3个字符-ese - -SELECT substring('chinese', 3); # 返回字符串'chinese'第三个字符之后的子字符串-inese -SELECT substring('chinese', -3); # 返回字符串'chinese'倒数第三个字符之后的子字符串-ese -SELECT substring('chinese', 3, 2); # 返回字符串'chinese'第三个字符之后的两个字符-in -SELECT trim(' chinese '); # 切割字符串' chinese '两边的空字符-'chinese' -SELECT ltrim(' chinese '); # 切割字符串' chinese '两边的空字符-'chinese ' -SELECT rtrim(' chinese '); # 切割字符串' chinese '两边的空字符-' chinese' -SELECT repeat('boy', 3); # 重复字符'boy'三次-'boyboyboy' -SELECT reverse('chinese'); # 反向排序-'esenihc' -SELECT length('chinese'); # 返回字符串的长度-7 -SELECT upper('chINese'), lower('chINese'); # 大写小写 CHINESE chinese -SELECT ucase('chINese'), lcase('chINese'); # 大写小写 CHINESE chinese -SELECT position('i' IN 'chinese'); # 返回'i'在'chinese'的第一个位置-3 -SELECT position('e' IN 'chinese'); # 返回'i'在'chinese'的第一个位置-5 -SELECT strcmp('abc', 'abd'); # 比较字符串,第一个参数小于第二个返回负数- -1 -SELECT strcmp('abc', 'abb'); # 比较字符串,第一个参数大于第二个返回正数- 1 -# 时间函数 -SELECT current_date, current_time, now(); # 2018-01-13 12:33:43 2018-01-13 12:33:43 -SELECT hour(current_time), minute(current_time), second(current_time); # 12 31 34 -SELECT year(current_date), month(current_date), week(current_date); # 2018 1 1 -SELECT quarter(current_date); # 1 -SELECT monthname(current_date), dayname(current_date); # January Saturday -SELECT dayofweek(current_date), dayofmonth(current_date), dayofyear(current_date); # 7 13 13 -# 控制流函数 -SELECT if(3>2, 't', 'f'), if(3<2, 't', 'f'); # t f -SELECT ifnull(NULL, 't'), ifnull(2, 't'); # t 2 -SELECT isnull(1), isnull(1/0); # 0 1 是null返回1,不是null返回0 -SELECT nullif('a', 'a'), nullif('a', 'b'); # null a 参数相同或成立返回null,不同或不成立则返回第一个参数 -SELECT CASE 2 - WHEN 1 THEN 'first' - WHEN 2 THEN 'second' - WHEN 3 THEN 'third' - ELSE 'other' - END ; # second -# 系统信息函数 -SELECT database(); # 当前数据库名-test -SELECT connection_id(); # 当前用户id-306 -SELECT user(); # 当前用户-root@localhost -SELECT version(); # 当前mysql版本 -SELECT found_rows(); # 返回上次查询的检索行数 -``` - -#### 用户 - -```mysql -# 增加用户 -CREATE USER 'test'@'localhost' IDENTIFIED BY 'test'; -INSERT INTO mysql.user(Host, User, Password) VALUES ('localhost', 'test', Password('test')); # 在用户表中插入用户信息,直接操作User表不推荐 -# 删除用户 -DROP USER 'test'@'localhost'; -DELETE FROM mysql.user WHERE User='test' AND Host='localhost'; -FLUSH PRIVILEGES ; -# 更改用户密码 -SET PASSWORD FOR 'test'@'localhost' = PASSWORD('test'); -UPDATE mysql.user SET Password=Password('t') WHERE User='test' AND Host='localhost'; -FLUSH PRIVILEGES ; -# 用户授权 -GRANT ALL PRIVILEGES ON *.* TO test@localhost IDENTIFIED BY 'test'; -# 授予用'test'密码登陆成功的test@localhost用户操作所有数据库的所有表的所有的权限 -FLUSH PRIVILEGES ; # 刷新系统权限表,使授予权限生效 -# 撤销用户授权 -REVOKE DELETE ON *.* FROM 'test'@'localhost'; # 取消该用户的删除权限 -``` - -#### 其他语句 - -```mysql -# 查看所有的表信息(包括视图) -SHOW TABLE STATUS; -``` - -### 小破站学习的mysql - -- [美女老师](https://www.bilibili.com/video/av49181542?p=1) -- [sql表](https://blog.csdn.net/GongmissYan/article/details/102937816) - -#### 先贴sql - -```sql -CREATE TABLE `t_user` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `user_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '账户', - `password` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '密码', - `gender` varchar(11) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', - `age` int(11) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; -INSERT INTO `t_user` (`id`, `user_name`, `password`, `gender`, `age`) -VALUES - (1, 'abc', '123', '0', 20), - (2, 'a', '123', '0', 20), - (3, 'b', '123', '1', 21), - (4, 'c', '123', '0', 22), - (5, 'dae', '123', '1', 23), - (6, 'e', '123', '0', 24), - (7, 'f', '123', '0', 25); - -``` - -### 基础查询 - -```sql -/* -语法: - select 查询列表 from 表明; -特点: - 1. 查询列表可以是:表中的字段、常量值、表达式、函数 - 2. 查询的结构是一个虚拟的表格 -*/ - /* - 别名 - ①便于理解 - ②如果要查询的字段有重名的情况,使用别名可以区分开来 - - */ - # +号的作用 - /* -select 100+90; 两个操作数都为数值型,则做加法运算 -select '123'+90;只要其中一方为字符型,试图将字符型数值转换成数值型 - 如果转换成功,则继续做加法运算 -select 'john'+90; 如果转换失败,则将字符型数值转换成0 - -select null+10; 只要其中一方为null,则结果肯定为null - */ -# 题目 - -#1. 下面的语句是否可以执行成功 :能 - SELECT - last_name , - job_id , - salary AS sal -FROM - employees; -#2.下面的语句是否可以执行成功 :能 - SELECT - * -FROM - employees; -#3.找出下面语句中的错误 :有个中文标点符号 - SELECT - employee_id , - last_name, - salary * 12 AS "ANNUAL SALARY" -FROM - employees; - -#4.显示表departments的结构,并查询其中的全部数据 -DESC departments ; -SELECT * FROM departments; -#5.显示出表employees中的全部job_id(不能重复) -SELECT DISTINCT job_id FROM employees; - -``` - -### 条件查询 - -```sql -# 2: 条件查询 -/* - -语法: - select - 查询列表 - from - 表名 - where - 筛选条件; - -分类: - 一、按条件表达式筛选 - - 简单条件运算符:> < = != <> >= <= - - 二、按逻辑表达式筛选 - 逻辑运算符: - 作用:用于连接条件表达式 - && || ! - and or not - - &&和and:两个条件都为true,结果为true,反之为false - ||或or: 只要有一个条件为true,结果为true,反之为false - !或not: 如果连接的条件本身为false,结果为true,反之为false - - 三、模糊查询 - like - between and - in - is null -*/ -# 1. 查询年龄大于22的用户信息 -SELECT - * -FROM - `t_user` -WHERE - `age` > 22; - -# 2. 查询年龄不等于22的用户和密码 -SELECT - `user_name`, - `password` -FROM - `t_user` -WHERE - `age` <> 20; - -# 查询年龄在21到23之间的用户名和密码 -SELECT - `user_name`, - `password` -FROM - `t_user` -WHERE - `age` >=21 AND `age` <= 23; -SELECT - `user_name`, - `password`, - `age` -FROM - `t_user` -WHERE - `age` BETWEEN 21 AND 23; - -# 查询年龄21和23的用户名和密码 -SELECT - `user_name`, - `password` -FROM - `t_user` -WHERE - `age` IN(21, 23); - -# 模糊查询 like -/* -特点: -①一般和通配符搭配使用 - 通配符: - % 任意多个字符,包含0个字符 - _ 任意单个字符 -*/ -# 查询用户名包字符a的用户信息 -SELECT - * -FROM - `t_user` -WHERE - `user_name` LIKE '%a%'; - -SELECT - * -FROM - `t_user` -WHERE - `user_name` LIKE '_a%'; - -``` - -### 排序查询 - -```sql -/* -排序 - select 查询列表 - from 表 - order by 排序列表 asc|desc. asc:升序,desc降序 - 可以放单个字段,也可以放多个字段,可以表达式,也可以放函数 - order by 放最后 除limit语句 -*/ -题目: -#1.查询员工的姓名和部门号和年薪,按年薪降序 按姓名升序 -SELECT - last_name, - department_id, - salary*12*(1+IFNULL(commission_pct,0)) 年薪 -FROM - employees -ORDER BY 年薪 DESC, last_name ASC; - -#2.选择工资不在8000到17000的员工的姓名和工资,按工资降序 -SELECT - last_name , - salary -FROM employees -WHERE - salary NOT BETWEEN 8000 AND 17000 -ORDER BY salary DESC ; -#3.查询邮箱中包含e的员工信息,并先按邮箱的字节数降序,再按部门号升序 -SELECT - *,LENGTH(email) -FROM employees -WHERE email LIKE '%e%' -ORDER BY LENGTH(email) DESC,department_id ASC; - - -``` - -### 常见函数 - -```sql -/* - -概念:类似于java的方法,将一组逻辑语句封装在方法体中,对外暴露方法名 -好处:1、隐藏了实现细节 2、提高代码的重用性 -调用:select 函数名(实参列表) 【from 表】; -特点: - ①叫什么(函数名) - ②干什么(函数功能) - -分类: - 1、单行函数 - 如 concat、length、ifnull等 - 2、分组函数 - - 功能:做统计使用,又称为统计函数、聚合函数、组函数 - -常见函数: - 一、单行函数 - 字符函数: - length:获取字节个数(utf-8一个汉字代表3个字节,gbk为2个字节) - concat - substr - instr - trim - upper - lower - lpad - rpad - replace - - 数学函数: - round - ceil - floor - truncate - mod - - 日期函数: - now - curdate - curtime - year - month - monthname - day - hour - minute - second - str_to_date - date_format - 其他函数: - version - database - user - 控制函数 - if - case -*/ -# 1. 字符函数 -SELECT LENGTH('join'); -SELECT LENGTH('张三丰hahaha'); -SHOW VARIABLES LIKE '%char%'; -# 2. 拼接字符串 -SELECT CONCAT(`user_name`, '_', `password`) infos FROM t_user; -#3.upper、lower -SELECT UPPER('john'); -SELECT LOWER('joHn'); -#示例:将姓变大写,名变小写,然后拼接 -SELECT CONCAT(UPPER(`user_name`), LOWER(`user_name`)) infos FROM t_user; -#4.substr、substring -#注意:索引从1开始 -#截取从指定索引处后面所有字符 -SELECT SUBSTR('我是dreamcat',7) out_put; -#截取从指定索引处指定字符长度的字符 -SELECT SUBSTR('我是dreamcat',1,3) out_put; - -SELECT -CONCAT(UPPER(SUBSTR(`user_name`, 1, 1)), '_', LOWER(SUBSTR(`user_name`, 2))) out_put -FROM t_user; -#5.instr 返回子串第一次出现的索引,如果找不到返回0 -SELECT INSTR('我是dreamcat', 'dream') out_put; -#6.trim -SELECT -LENGTH( - TRIM( - ' dreamcat. ' - ) -) out_put; -SELECT TRIM('aa' FROM 'aaaaaaaaa张aaaaaaaaaaaa翠山aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') AS out_put; -#7.lpad 用指定的字符实现左填充指定长度 -SELECT LPAD('殷素素',5,'*') AS out_put; -#8.rpad 用指定的字符实现右填充指定长度 -SELECT RPAD('殷素素',12,'ab') AS out_put; -#9.replace 替换 -SELECT REPLACE('周芷若周芷若周芷若周芷若张无忌爱上了周芷若','周芷若','赵敏') AS out_put; - - -#二、数学函数 -#round 四舍五入 -SELECT ROUND(-1.55); -SELECT ROUND(1.567,2); -#ceil 向上取整,返回>=该参数的最小整数 -SELECT CEIL(-1.02); -SELECT CEIL(1.02); -#floor 向下取整,返回<=该参数的最大整数 -SELECT FLOOR(-9.99); -SELECT FLOOR(9.99); -#truncate 截断 -SELECT TRUNCATE(1.69999,1); -#mod取余 -SELECT MOD(10,-3); -SELECT 10%3; - - -#三、日期函数 -#now 返回当前系统日期+时间 -SELECT NOW(); -#curdate 返回当前系统日期,不包含时间 -SELECT CURDATE(); -#curtime 返回当前时间,不包含日期 -SELECT CURTIME(); -#可以获取指定的部分,年、月、日、小时、分钟、秒 -SELECT YEAR(NOW()) 年; -SELECT YEAR('1998-1-1') 年; -SELECT MONTH(NOW()) 月; -SELECT MONTHNAME(NOW()) 月; -#str_to_date 将字符通过指定的格式转换成日期 -SELECT STR_TO_DATE('1998-3-2','%Y-%c-%d') AS out_put; -#date_format 将日期转换成字符 -SELECT DATE_FORMAT(NOW(),'%y年%m月%d日') AS out_put; - -#四、其他函数 - -SELECT VERSION(); -SELECT DATABASE(); -SELECT USER(); - -#五、流程控制函数 -#1.if函数: if else 的效果 - -SELECT IF(10<5,'大','小'); - -SELECT - *, -IF(`age` < 24, 'young', 'old') 'new age' -FROM - t_user; - -#2.case函数的使用一: switch case 的效果 -/* -java中 -switch(变量或表达式){ - case 常量1:语句1;break; - ... - default:语句n;break; - - -} - -mysql中 - -case 要判断的字段或表达式 -when 常量1 then 要显示的值1或语句1; -when 常量2 then 要显示的值2或语句2; -... -else 要显示的值n或语句n; -end -*/ - -SELECT - *, -CASE `age` -WHEN 21 THEN 21+1 -WHEN 22 THEN 22+2 -WHEN 23 THEN 23+3 -ELSE age -END 'NEW age' -FROM - t_user; -题目: -#1. 显示系统时间(注:日期+时间) -SELECT NOW(); - -#2. 查询员工号,姓名,工资,以及工资提高百分之20%后的结果(new salary) -SELECT - employee_id , - last_name , - salary , - salary * 1.2 'new salary' -FROM employees; - -#3. 将员工的姓名按首字母排序,并写出姓名的长度(length) -SELECT - LENGTH (last_name) 长度, SUBSTR(last_name,1,1) 首字母, last_name -FROM employees -ORDER BY 首字母; - -#4. 做一个查询,产生下面的结果 -# earns monthly but wants -#Dream Salary -#King earns 24000 monthly but wants 72000 -SELECT - CONCAT(last_name, ' earns ', salary, ' monthly but wants ', salary * 3) AS 'Dream Salary' -FROM employees -WHERE salary = 24000; - -``` - -### 分组函数 - -```sql -#二、分组函数 -/* -功能:用作统计使用,又称为聚合函数或统计函数或组函数 - -分类: -sum 求和、avg 平均值、max 最大值 、min 最小值 、count 计算个数 - -特点: -1、sum、avg一般用于处理数值型 - max、min、count可以处理任何类型 -2、以上分组函数都忽略null值 - -3、可以和distinct搭配实现去重的运算 - -4、count函数的单独介绍 -一般使用count(*)用作统计行数 - -5、和分组函数一同查询的字段要求是group by后的字段 - -*/ -#1、简单 的使用 -SELECT SUM(age) FROM t_user; -SELECT AVG(age) FROM t_user; -SELECT MIN(age) FROM t_user; -SELECT MAX(age) FROM t_user; -SELECT COUNT(age) FROM t_user; -SELECT SUM(user_name) FROM t_user; -SELECT AVG(user_name) FROM t_user; -SELECT COUNT(user_name) FROM t_user; - -SELECT SUM(DISTINCT password ) FROM t_user; -SELECT COUNT(DISTINCT password) FROM t_user; - -SELECT COUNT(*) FROM t_user; -SELECT COUNT(1) FROM t_user; -``` - -### 分组查询 - -```sql - -#进阶5:分组查询 - /* -语法: - -select 查询列表 -from 表 -【where 筛选条件】 -group by 分组的字段 -【order by 排序的字段】; - -特点: -1、和分组函数一同查询的字段必须是group by后出现的字段 -2、筛选分为两类:分组前筛选和分组后筛选 - 针对的表 位置 连接的关键字 -分组前筛选 原始表 group by前 where - -分组后筛选 group by后的结果集 group by后 having - -问题1:分组函数做筛选能不能放在where后面 -答:不能 - -问题2:where——group by——having - -一般来讲,能用分组前筛选的,尽量使用分组前筛选,提高效率 - -3、分组可以按单个字段也可以按多个字段 -4、可以搭配着排序使用 -*/ -SELECT - COUNT(*) -FROM - t_user -WHERE - password = '123'; -SELECT - AVG(age), - password -FROM - t_user -GROUP BY password; - -SELECT - AVG(age), - gender -FROM - t_user -GROUP BY gender ; -题目: -#1.查询各job_id的员工工资的最大值,最小值,平均值,总和,并按job_id升序 -SELECT - MAX(salary), - MIN(salary), - AVG(salary), - SUM(salary) -FROM employees -GROUP BY job_id -ORDER BY job_id ; - -#2.查询员工最高工资和最低工资的差距(DIFFERENCE) -SELECT - MAX(salary) - MIN(salary) AS DIFFERENCE -FROM employees; -#3.查询各个管理者手下员工的最低工资,其中最低工资不能低于6000,没有管理者的员工不计算在内 - -SELECT - MIN(salary), manager_id -FROM employees -WHERE manager_id IS NOT NULL -GROUP BY manager_id -HAVING MIN(salary) >= 6000; - -#4.查询所有部门的编号,员工数量和工资平均值,并按平均工资降序 - - -SELECT - department_id , - COUNT(*), - AVG(salary) -FROM - employees -GROUP BY department_id -ORDER BY AVG(salary) DESC; - -#5.选择具有各个job_id的员工人数 - -SELECT - COUNT(*), - job_id -FROM - employees -GROUP BY job_id; -``` - -### 连接查询 - -```sql - -#进阶6:连接查询 - /* -含义:又称多表查询,当查询的字段来自于多个表时,就会用到连接查询 - -笛卡尔乘积现象:表1 有m行,表2有n行,结果=m*n行 - -发生原因:没有有效的连接条件 -如何避免:添加有效的连接条件 - -分类: - - 按年代分类: - sql92标准:仅仅支持内连接 - sql99标准【推荐】:支持内连接+外连接(左外和右外)+交叉连接 - - 按功能分类: - 内连接: - 等值连接 - 非等值连接 - 自连接 - 外连接: - 左外连接 - 右外连接 - 全外连接 - - 交叉连接 -*/ -#1.显示所有员工的姓名,部门号和部门名称。 - SELECT - e.last_name , - d.department_id , - d.department_name -FROM - employees e , - departments d -WHERE - e.department_id = d.department_id ; -#2.查询90号部门员工的job_id和90号部门的location_id - SELECT - e.job_id , - d.location_id -FROM - employees e , - departments d -WHERE - e.department_id = d.department_id - AND e.department_id = 90; -#3. 选择所有有奖金的员工的 last_name , department_name , location_id , city - SELECT - last_name , - department_name , - l.location_id , - city -FROM - employees e, - departments d, - locations l -WHERE - e.department_id = d.department_id - AND d.location_id = l.location_id - AND e.commission_pct IS NOT NULL; -#4.选择city在Toronto工作的员工的 last_name , job_id , department_id , department_name - SELECT - e.last_name , - e.job_id , - e.department_id , - d.department_name -FROM - employees e, - departments d, - locations l -WHERE - e.department_id = d.department_id - AND d.location_id = l.location_id - AND l.city = 'Toronto'; -#5.查询每个工种、每个部门的部门名、工种名和最低工资 - SELECT - d.department_name , - j.job_title , - MIN(e.salary) -FROM - employees e , - departments d , - jobs j -WHERE - e.department_id = d.department_id - AND e.job_id = j.job_id -GROUP BY - d.department_name , - j.job_title ; -#6.查询每个国家下的部门个数大于2的国家编号 - SELECT - l.country_id , - COUNT(*) -FROM - locations l , - departments d -WHERE - d.location_id = l.location_id -GROUP BY - country_id -HAVING - COUNT(*) > 2; - -#7 、选择指定员工的姓名,员工号,以及他的管理者的姓名和员工号,结果类似于下面的格式 -SELECT - e.last_name employees, - e.employee_id "Emp#", - m.last_name manager, - m.employee_id "Mgr#" -FROM - employees e, - employees m -WHERE - e.manager_id = m.employee_id - AND e.last_name = 'kochhar'; - -#二、sql99语法 -/* -语法: - select 查询列表 - from 表1 别名 【连接类型】 - join 表2 别名 - on 连接条件 - 【where 筛选条件】 - 【group by 分组】 - 【having 筛选条件】 - 【order by 排序列表】 - - -分类: -内连接(★):inner -外连接 - 左外(★):left 【outer】 - 右外(★):right 【outer】 - 全外:full【outer】 -交叉连接:cross - -*/ -#一)内连接 -/* -语法: - -select 查询列表 -from 表1 别名 -inner join 表2 别名 -on 连接条件; - -分类: -等值 -非等值 -自连接 - -特点: -①添加排序、分组、筛选 -②inner可以省略 -③ 筛选条件放在where后面,连接条件放在on后面,提高分离性,便于阅读 -④inner join连接和sql92语法中的等值连接效果是一样的,都是查询多表的交集 -*/ -#二、外连接 - - /* - 应用场景:用于查询一个表中有,另一个表没有的记录 - - 特点: - 1、外连接的查询结果为主表中的所有记录 - 如果从表中有和它匹配的,则显示匹配的值 - 如果从表中没有和它匹配的,则显示null - 外连接查询结果=内连接结果+主表中有而从表没有的记录 - 2、左外连接,left join左边的是主表 - 右外连接,right join右边的是主表 - 3、左外和右外交换两个表的顺序,可以实现同样的效果 - 4、全外连接=内连接的结果+表1中有但表2没有的+表2中有但表1没有的 - */ -``` - -### 子查询 - -```sql -#进阶7:子查询 -/* -含义: -出现在其他语句中的select语句,称为子查询或内查询 -外部的查询语句,称为主查询或外查询 - -分类: -按子查询出现的位置: - select后面: - 仅仅支持标量子查询 - - from后面: - 支持表子查询 - where或having后面:★ - 标量子查询(单行) √ - 列子查询 (多行) √ - - 行子查询 - - exists后面(相关子查询) - 表子查询 -按结果集的行列数不同: - 标量子查询(结果集只有一行一列) - 列子查询(结果集只有一列多行) - 行子查询(结果集有一行多列) - 表子查询(结果集一般为多行多列) -*/ -#一、where或having后面 -/* -1、标量子查询(单行子查询) -2、列子查询(多行子查询) - -3、行子查询(多列多行) - -特点: -①子查询放在小括号内 -②子查询一般放在条件的右侧 -③标量子查询,一般搭配着单行操作符使用 -> < >= <= = <> - -列子查询,一般搭配着多行操作符使用 -in、any/some、all - -④子查询的执行优先于主查询执行,主查询的条件用到了子查询的结果 - -*/ -``` - -### 分页查询 - -```sql -#进阶8:分页查询 ★ -/* - -应用场景:当要显示的数据,一页显示不全,需要分页提交sql请求 -语法: - select 查询列表 - from 表 - 【join type join 表2 - on 连接条件 - where 筛选条件 - group by 分组字段 - having 分组后的筛选 - order by 排序的字段】 - limit 【offset,】size; - - offset要显示条目的起始索引(起始索引从0开始) - size 要显示的条目个数 -特点: - ①limit语句放在查询语句的最后 - ②公式 - 要显示的页数 page,每页的条目数size - - select 查询列表 - from 表 - limit (page-1)*size,size; - - size=10 - page - 1 0 - 2 10 - 3 20 -*/ -``` - - diff --git "a/Java/other/\345\274\200\346\272\220github.md" "b/Java/other/\345\274\200\346\272\220github.md" deleted file mode 100644 index 4449e13b..00000000 --- "a/Java/other/\345\274\200\346\272\220github.md" +++ /dev/null @@ -1,119 +0,0 @@ -> **一些有趣的github项目总结,其中包括终端、Python、Java、笔试&面试、效率软件等。** - - -## Java - -- [Awesome Java]() A curated list of awesome frameworks, libraries and software for the Java programming language. **star:21651** -- [CS-Notes]() 技术面试必备基础知识、Leetcode 题解、Java、C++、Python、后端面试、操作系统、计算机网络、系统设计。**star:67433** -- [spring-boot-examples]() 对于初学者学习Spring-boot,是个非常不错的例子。**star:16408** -- [SpringAll]()循序渐进,学习Spring Boot、Spring Boot & Shiro、Spring Cloud、Spring Security & Spring Security OAuth2,博客Spring系列源码。**star:6181** -- [interest]() Vue+Spring boot前后端分离博客项目。**star:494** -- [litemall]() 一个小商城。litemall = Spring Boot后端 + Vue管理员前端 + 微信小程序用户前端 + Vue用户移动端。**star:7586** -- [vhr]()微人事是一个前后端分离的人力资源管理系统,项目采用SpringBoot+Vue开发。**star:4705** -- [mybatis]() MyBatis SQL mapper framework for Java **star:11335** -- [miaosha]() ⭐⭐⭐⭐秒杀系统设计与实现.互联网工程师进阶与分析🙋🐓 **star:9400** -- [spring-boot-plus]()🔥spring-boot-plus集成Spring Boot 2.1.6,Mybatis,Mybatis Plus,Druid,FastJson,Redis,Rabbit MQ,Kafka等,可使用代码生成器快速开发项目. **star:551** -- [hope-boot]() 🌱🚀一款现代化的脚手架项目。🍻整合Springboot2 **star:1543** -- [spring-boot-demo]() spring boot demo 是一个用来学习 spring boot 的项目,总共包含 57 个集成demo,已经完成 47 个。**star:2149** -- [spring-boot-api-project-seed]() 🌱🚀一个基于Spring Boot & MyBatis的种子项目,用于快速构建中小型API、RESTful API项目~ **star:1543** -- [White-Jotter]()白卷是一款使用 Vue+Spring Boot 开发的前后端分离的图书管理项目 **star:115** -- [eladmin]() 项目基于 Spring Boot 2.1.0 、 Jpa、 Spring Security、redis、Vue的前后端分离的后台管理系统,项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,支持动态路由 **start:3163** -- [dbblog]() 基于SpringBoot2.x+Vue2.x+ElementUI+Iview+Elasticsearch+RabbitMQ+Redis+Shiro的多模块前后端分离的博客项目 **star:375** -- [spring-analysis]() Spring源码阅读 **star:4045** -- [mall]() mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现。**star:21926** -- [后端架构师技术图谱](https://github.com/xingshaocheng/architect-awesome) **star:42900** -- [mall-swarm](https://github.com/macrozheng/mall-swarm)mall-swarm是一套微服务商城系统,采用了 Spring Cloud Greenwich、Spring Boot 2、MyBatis、Docker、Elasticsearch等核心技术,同时提供了基于Vue的管理后台方便快速搭建系统。**star:1400** -- [jeecg-boot](https://github.com/zhangdaiscott/jeecg-boot)一款基于代码生成器的JAVA快速开发平台,开源界“小普元”超越传统商业企业级开发平台!**star:9600** -- [dbblog](https://github.com/llldddbbb/dbblog) 基于SpringBoot2.x+Vue2.x+ElementUI+Iview+Elasticsearch+RabbitMQ+Redis+Shiro的多模块前后端分离的博客项目 [http://www.dblearn.cn](http://www.dblearn.cn/) **star:643** -- [springboot-seckill](https://github.com/zaiyunduan123/springboot-seckill) 基于SpringBoot + MySQL + Redis + RabbitMQ + Guava开发的高并发商品限时秒杀系统 **star:943** -- [MeetingFilm](https://github.com/daydreamdev/MeetingFilm)基于微服务架构的在线电影购票平台 **star:111** -- [guava](https://github.com/google/guava) Google core libraries for Java **star:36400** -- [hutool](https://github.com/looly/hutool) A set of tools that keep Java sweet. [http://www.hutool.cn](http://www.hutool.cn/) **star:10800** -- [JavaGuide]() 一份涵盖大部分Java程序员所需要掌握的核心知识。**star:45159** -- [advanced-java]() 互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务等领域知识,后端同学必看,前端同学也可学习。**star:22747** -- [technology-talk]() 汇总java生态圈常用技术框架等。**star:6229** -- [interview_internal_reference]() 2019年最新总结,阿里,腾讯,百度,美团,头条等技术面试题目,以及答案,专家出题人分析汇总。 **star:14335** -- [interviews]() Everything you need to know to get the job. **star:37598** -- [interview_internal_reference]()2019年最新总结,阿里,腾讯,百度,美团,头条等技术面试题目,以及答案,专家出题人分析汇总。 **star:15570** -- [reverse-interview-zh]() 技术面试最后反问面试官的话 **star:4500** -- [JavaFamily](https://github.com/AobingJava/JavaFamily)【互联网一线大厂面试+学习指南】进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务等领域知识,作者风格幽默,看起来津津有味,把学习当做一种乐趣,何乐而不为,后端同学必看,前端同学我保证你也看得懂,看不懂你加我微信骂我渣男就好了。 **star:7100** -- [Java-Interview](https://github.com/gzc426/Java-Interview) Java 面试必会 直通BAT **star:3500** -- [arthas](https://github.com/alibaba/arthas):Alibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas **star:24200** -- [Sentinel](https://github.com/alibaba/Sentinel)A powerful flow control component enabling reliability, resilience and monitoring for microservices. (面向云原生微服务的高可用流控防护组件) **star:14900** - -## 算法 -- [LeetcodeTop](https://github.com/afatcoder/LeetcodeTop)About汇总各大互联网公司容易考察的高频leetcode题🔥 **star:5000** -- [fucking-algorithm](https://github.com/labuladong/fucking-algorithm) 手把手撕LeetCode题目,扒各种算法套路的裤子,not only how,but also why. English version supported! https://labuladong.gitbook.io/algo/ **star:78200** -- [LeetCodeAnimation]() LeetCode用动画的形式的呈现。**star:32650** -- [leetcode]() LeetCode Solutions: A Record of My Problem Solving Journey.( leetcode题解,记录自己的leetcode解题之路。) -- [algo](https://github.com/wangzheng0822/algo) 数据结构和算法必知必会的50个代码实现 **star:10700** - -## Vue&&前端 - -- [awesome-vue]() awesome things related to Vue.js **star:46634** -- [vue-form-making]()基于Vue的表单设计器,让表单开发简单而高效。 **star:1347** -- [fe-interview]() 前端面试每日 3+1,以面试题来驱动学习,提倡每日学习与思考,每天进步一点!每天早上5点纯手工发布面试题(死磕自己,愉悦大家)**star:6667** -- [vue2-elm]() 基于 vue2 + vuex 构建一个具有 45 个页面的大型单页面应用 **star:30300** -- [Web]() 前端入门和进阶学习笔记,超详细的Web前端学习图文教程。从零开始学前端,做一名精致的前端工程师。持续更新… **star:55300** -- [javascript-algorithms]() Algorithms and data structures implemented in JavaScript with explanations and links to further readings **star:55277** -- [nodebestpractices]() ✅ The largest Node.js best practices list (September 2019) **star:35000** -- [gods-pen](https://github.com/ymm-tech/gods-pen) 基于vue的高扩展在线网页制作平台,可自定义组件,可添加脚本,可数据统计。A mobile page builder/editor, similar with amolink. [https://godspen.ymm56.com](https://godspen.ymm56.com/) **star:1200** -- [Daily-Interview-Question](https://github.com/Advanced-Frontend/Daily-Interview-Question) 我是木易杨,公众号「高级前端进阶」作者,每天搞定一道前端大厂面试题,祝大家天天进步,一年后会看到不一样的自己。**star:17000** - -## Python - -- [awesome-python-login-model]() 模拟登陆一些大型网站的demo,个人觉得不错,值得学习。**star:8403** -- [pyppeteer]() 模拟动态加载js,比selenium稍微好用一些。**star:1924** -- [requests]() Python HTTP Requests for Humans™ ✨🍰✨ **star:39860** -- [requests-html]() Pythonic HTML Parsing for Humans™ **star:10111** -- [httpx]() A next generation HTTP client for Python. 🦋 **star:1900** -- [PySimpleGUI]() 做一些简单的GUI,可以用这个,简单应用。**star:1608** -- [bokeh]() Interactive Web Plotting for Python。 **star:10701** -- [wxpy]() 微信机器人 / 可能是最优雅的微信个人号 API ✨✨ [http://wxpy.readthedocs.io](http://wxpy.readthedocs.io/) **star:10700** - - -## 效率软件 - -- [frp]() 内网穿透,你懂的。**star:24539** -- [musicbox]() 网易云音乐命令行版本。**star:7601** -- [motrix]() 配合百度云有着奇淫技巧。 **star:11098** -- [Electronic WeChat]() 该项目虽然不再维护,但挺好用的(linux)。**star:12622** -- [hexo]() 搭建博客系统框架,挺好用。**star:26868** -- [awesome-mac]()推荐的非常给力且实用。 **star:29814** -- [蓝灯]() 免费的vpn。 **star:9520** -- [LazyDocker]() The lazier way to manage everything docker. **star:10906** -- [postwoman]() 👽 API request builder - Helps you create your requests faster, saving you precious time on your development. **star:2190** -- [x64dbg]()An open-source x64/x32 debugger for windows. **star:34260** -- [google-access-helper]() 谷歌访问助手破解版 **star:3887** -- [v2ray-core]() A platform for building proxies to bypass network restrictions. **star:21444** -- [v2rayN]() vpn **star:1249** -- [Alfred-collection]() A collection of all known Alfred3 workflows **star:411** -- [RevokeMsgPatcher](https://github.com/huiyadanli/RevokeMsgPatcher) editor for WeChat/QQ/TIM - PC版微信/QQ/TIM防撤回补丁(我已经看到了,撤回也没用了) **star:1500** -- [lx-music-desktop](https://github.com/lyswhut/lx-music-desktop) 一个基于 electron 的音乐软件 **star:1300** -- [UnblockNeteaseMusic](https://github.com/nondanee/UnblockNeteaseMusic) Revive unavailable songs for Netease Cloud Music **star:7900** -- [CodeVar](https://github.com/xudaolong/CodeVar) 生成可用的代码变量 (CodeVar that return u a better variable from Chinese to English . ) **star:684** -- [Bob](https://github.com/ripperhe/Bob) Bob 是一款 Mac 端翻译软件,支持划词翻译、截图翻译以及手动输入翻译。 **star:3100** - -## 其他 - -- [ChineseBQB](ChineseBQB)🇨🇳Chinese sticker pack,More joy / 中国表情包大集合,更欢乐 **star:2319** -- [free-programming-books-zh_CN]()📚 免费的计算机编程类中文书籍,欢迎投稿 **star:55296** -- [freeCodeCamp]() The [https://www.freeCodeCamp.org](https://www.freecodecamp.org/) open source codebase and curriculum. Learn to code for free together with millions of people. **star:304920** -- [hosts]() 镜像 **star:15582** -- [free-api]() 收集免费的接口服务,做一个api的搬运工 **star:6000** -- [fanhaodaquan](https://github.com/imfht/fanhaodaquan) 番号大全。 **star:1200** -- [BullshitGenerator](https://github.com/menzi11/BullshitGenerator) Needs to generate some texts to test if my GUI rendering codes good or not. so I made this. **star:3700** -- [howto-make-more-money](https://github.com/easychen/howto-make-more-money): 程序员如何优雅的挣零花钱 **star:8200** -- [marktext](https://github.com/marktext/marktext) A simple and elegant markdown editor, available for Linux, macOS and Windows. [https://marktext.app](https://marktext.app/) **star:14800** -- [FreePAC](https://github.com/xiaoming2028/FreePAC) 科学上网/梯子/自由上网/翻墙 SS/SSR/V2Ray/Brook 搭建教程 **star:2146** - -- [The Art of Command Line]() :系统的学习命令行的用法。**star:57509** -- [oh-my-zsh]()这玩意不用我简单介绍了吧?**star:90516** -- [git-tips](https://github.com/521xueweihan/git-tips) Git的奇技淫巧 **star:11400** -- [powerline-fonts]() mac挺好用的一款终端字体。**star:169** -- [powerlevel9k]() oh-my-zsh的一款皮肤。**star:9952** -- [zsh-syntax-highlighting]() 终端输入的命令有语法高亮效果。**star:7326** -- [zsh-autosuggestions]()终端代码补全插件。**star:6662** -- [howto-make-more-money](https://github.com/easychen/howto-make-more-money): 程序员如何优雅的挣零花钱,2.0版,升级为小书了。Most of this not work outside China , so no English translate **star:13800** -- [yapi](https://github.com/YMFE/yapi)YApi 是一个可本地部署的、打通前后端及QA的、可视化的接口管理平台 **star:19400** -- [algorithm-visualizer](https://github.com/algorithm-visualizer/algorithm-visualizer):🎆Interactive Online Platform that Visualizes Algorithms from Code **star:33000** \ No newline at end of file diff --git "a/Java/spring-books/MyBatis\346\241\206\346\236\266\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230.md" "b/Java/spring-books/MyBatis\346\241\206\346\236\266\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230.md" new file mode 100644 index 00000000..717ee102 --- /dev/null +++ "b/Java/spring-books/MyBatis\346\241\206\346\236\266\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230.md" @@ -0,0 +1,154 @@ +## 引言 +> 项目中,经常用到MyBatis,面试也需要掌握滴! + + +## 基本概念 + +### 数据持久化 + +**数据持久化是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称。例如,文件的存储、数据的读取等都是数据持久化操作。数据模型可以是任何数据结构或对象的模型、XML、二进制流等。** + +**当我们编写应用程序操作数据库,对表数据进行增删改查的操作的时候就是数据持久化的操作。** + +### Mybatis框架简介 + +- **MyBatis框架是一个开源的数据持久层框架。** +- **它的内部封装了通过JDBC访问数据库的操作,支持普通的SQL查询、存储过程和高级映射,几乎消除了所有的JDBC代码和参数的手工设置以及结果集的检索。** +- **MyBatis作为持久层框架,其主要思想是将程序中的大量SQL语句剥离出来,配置在配置文件当中,实现SQL的灵活配置。** +- **这样做的好处是将SQL与程序代码分离,可以在不修改代码的情况下,直接在配置文件当中修改SQL。** + +### ORM + +**ORM(Object/Relational Mapping)即对象关系映射,是一种数据持久化技术。它在对象模型和关系型数据库直接建立起对应关系,并且提供一种机制,通过JavaBean对象去操作数据库表的数据。** + +**MyBatis通过简单的XML或者注解的方式进行配置和原始映射,将实体类和SQL语句之间建立映射关系,是一种半自动(之所以说是半自动,因为我们要自己写SQL)的ORM实现。** + +## MyBatis框架的优缺点及其适用的场合 + +### 优点 + +1. 与JDBC相比,减少了50%以上的代码量。 +2. MyBatis是嘴加单的持久层框架,小巧并且简单易学。 +3. MyBatis相当灵活,不会对应用程序或者数据库的现有设计强加任何影响,SQL写在XML文件里,从程序代码中彻底分离,降低耦合度,便于统一的管理和优化,并可重用。 +4. 提供XML标签,支持编写动态的SQL,满足不同的业务需求。 +5. 提供映射标签,支持对象与数据库的ORM字段关系映射。 + +### 缺点 + +1. SQL语句的编写工作量较大,对开发人员编写SQL的能力有一定的要求。 +2. SQL语句依赖于数据库,导致数据库不具有好的移植性,不可以随便更换数据库。 + +### 适用场合 + +**MyBatis专注于SQL自身,是一个足够灵活的DAO层解决方案。对性能的要求很高,或者需求变化较多的项目,例如Web项目,那么MyBatis是不二的选择。** + +## MyBatis与Hibernate有哪些不同? + +1. Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。 +2. Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql映射文件,工作量大。 +3. Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用hibernate开发可以节省很多代码,提高效率。 + +## **#{}和${}的区别是什么?** + +1. \#{} 是预编译处理,${}是字符串替换。 +2. Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值; +3. Mybatis在处理\${}时,就是把\${}替换成变量的值。 + +4. 使用#{}可以有效的防止SQL注入,提高系统安全性。 + +## 当实体类中的属性名和表中的字段名不一样 ,怎么办 ? + +1. 第1种: 通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。 +2. 第2种: 通过 `` 来映射字段名和实体类属性名的一一对应的关系。 + +## 模糊查询like语句该怎么写? + +1. 第1种:在Java代码中添加sql通配符。 +2. 第2种:在sql语句中拼接通配符,会引起sql注入 + +## 通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗? + +**Dao接口即Mapper接口。接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。** +**Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中每`