From 20b69f5c336e4e8a1fc714025a7ce9d902e5cf98 Mon Sep 17 00:00:00 2001 From: whx123 <327658337@qq.com> Date: Sat, 16 May 2020 16:00:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=9A=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...42\347\232\204\345\216\237\347\220\206.md" | 0 ...27\345\237\272\347\241\200\345\233\276.md" | 0 ...00\346\265\201\347\250\213\345\233\276.md" | 0 ...56\347\237\245\350\257\206\347\202\271.md" | 499 +++++++++++++++ ...53\347\247\215\346\226\271\346\241\210.md" | 295 +++++++++ ...va\345\272\217\345\210\227\345\214\226.md" | 432 +++++++++++++ ...75\344\273\244\350\247\243\346\236\220.md" | 280 ++++++++ ...21\345\260\261\345\244\237\345\225\246.md" | 510 +++++++++++++++ ...48\344\270\252\351\227\256\351\242\230.md" | 514 +++++++++++++++ ...33\345\236\213\350\247\243\346\236\220.md" | 514 +++++++++++++++ ...13\346\261\240\350\247\243\346\236\220.md" | 490 ++++++++++++++ ...55\345\244\247\346\227\266\346\234\272.md" | 273 ++++++++ ...15\345\210\260\345\216\237\347\220\206.md" | 361 +++++++++++ ...10\345\214\272\345\210\253\357\274\237.md" | 4 +- ...00\351\235\242\350\257\225\351\242\230.md" | 161 ----- ...76\345\216\237\347\220\206\345\233\276.md" | 0 ...24\347\246\273\347\272\247\345\210\253.md" | 473 ++++++++++++++ ...30\346\235\241\345\273\272\350\256\256.md" | 603 ++++++++++++++++++ ...01\345\244\247\346\235\202\347\227\207.md" | 291 +++++++++ ...06\346\236\220\350\277\207\347\250\213.md" | 0 ...06\346\236\220\350\277\207\347\250\213.md" | 0 ...41\345\237\272\347\241\200\347\257\207.md" | 316 +++++++++ ...70\350\277\234\350\256\260\345\276\227.md" | 49 ++ ...00\346\254\241\345\256\236\350\267\265.md" | 52 ++ ...04\345\215\201\344\270\252\345\235\221.md" | 391 ++++++++++++ ...52\345\260\217\346\212\200\345\267\247.md" | 238 +++++++ ...64\347\250\213\345\272\217\345\221\230.md" | 359 +++++++++++ ...34\347\274\223\345\255\230\357\274\237.md" | 119 ++++ ...66\345\217\221\351\224\231\350\257\257.md" | 272 ++++++++ ...01\345\244\247\345\273\272\350\256\256.md" | 82 +++ 30 files changed, 7414 insertions(+), 164 deletions(-) delete mode 100644 "Elasticsearch\351\235\242\350\257\225/6. Lucene\345\205\250\346\226\207\346\220\234\347\264\242\347\232\204\345\216\237\347\220\206.md" rename "Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\346\265\201\347\250\213\345\233\276/JVM\347\263\273\345\210\227\345\237\272\347\241\200\345\233\276.md" => "Java\345\237\272\347\241\200\345\255\246\344\271\240/JVM\347\263\273\345\210\227\345\237\272\347\241\200\345\233\276.md" (100%) rename "Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\346\265\201\347\250\213\345\233\276/Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\345\237\272\347\241\200\346\265\201\347\250\213\345\233\276.md" => "Java\345\237\272\347\241\200\345\255\246\344\271\240/Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\345\237\272\347\241\200\346\265\201\347\250\213\345\233\276.md" (100%) create mode 100644 "Java\345\237\272\347\241\200\345\255\246\344\271\240/Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\357\274\232\345\274\202\345\270\270\347\232\204\345\215\201\344\270\252\345\205\263\351\224\256\347\237\245\350\257\206\347\202\271.md" create mode 100644 "Java\345\237\272\347\241\200\345\255\246\344\271\240/if-else\344\273\243\347\240\201\344\274\230\345\214\226\347\232\204\345\205\253\347\247\215\346\226\271\346\241\210.md" create mode 100644 "Java\345\237\272\347\241\200\345\255\246\344\271\240/java\345\272\217\345\210\227\345\214\226.md" create mode 100644 "Java\345\237\272\347\241\200\345\255\246\344\271\240/jstack\345\221\275\344\273\244\350\247\243\346\236\220.md" create mode 100644 "Java\345\237\272\347\241\200\345\255\246\344\271\240/\346\216\214\346\217\241Java\346\236\232\344\270\276\350\277\231\345\207\240\344\270\252\347\237\245\350\257\206\347\202\271\357\274\214\346\227\245\345\270\270\345\274\200\345\217\221\345\260\261\345\244\237\345\225\246.md" create mode 100644 "Java\345\237\272\347\241\200\345\255\246\344\271\240/\346\234\211\345\205\263\344\272\216Java Map\357\274\214\345\272\224\350\257\245\346\216\214\346\217\241\347\232\2048\344\270\252\351\227\256\351\242\230.md" create mode 100644 "Java\345\237\272\347\241\200\345\255\246\344\271\240/\346\263\233\345\236\213\350\247\243\346\236\220.md" create mode 100644 "Java\345\237\272\347\241\200\345\255\246\344\271\240/\347\272\277\347\250\213\346\261\240\350\247\243\346\236\220.md" create mode 100644 "Java\345\237\272\347\241\200\345\255\246\344\271\240/\350\247\246\345\217\221\347\261\273\345\212\240\350\275\275\347\232\204\345\205\255\345\244\247\346\227\266\346\234\272.md" create mode 100644 "Java\345\237\272\347\241\200\345\255\246\344\271\240/\350\260\210\350\260\210Java\345\217\215\345\260\204\357\274\232\344\273\216\345\205\245\351\227\250\345\210\260\345\256\236\350\267\265\357\274\214\345\206\215\345\210\260\345\216\237\347\220\206.md" rename "Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\346\265\201\347\250\213\345\233\276/mysql\346\225\260\346\215\256\345\272\223\347\233\270\345\205\263\346\265\201\347\250\213\345\233\276\345\216\237\347\220\206\345\233\276.md" => "Mysql\345\237\272\347\241\200\345\255\246\344\271\240/mysql\346\225\260\346\215\256\345\272\223\347\233\270\345\205\263\346\265\201\347\250\213\345\233\276\345\216\237\347\220\206\345\233\276.md" (100%) create mode 100644 "Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\344\270\200\346\226\207\345\275\273\345\272\225\350\257\273\346\207\202MySQL\344\272\213\345\212\241\347\232\204\345\233\233\345\244\247\351\232\224\347\246\273\347\272\247\345\210\253.md" create mode 100644 "Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\344\271\246\345\206\231\351\253\230\350\264\250\351\207\217SQL\347\232\20430\346\235\241\345\273\272\350\256\256.md" create mode 100644 "Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\345\220\216\347\253\257\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\357\274\232\347\264\242\345\274\225\345\244\261\346\225\210\347\232\204\345\215\201\345\244\247\346\235\202\347\227\207.md" rename "\346\205\242\346\205\242\346\235\245\357\274\214\344\270\215\346\200\225Mysql\346\255\273\351\224\201\345\225\246/insert on duplicate\346\255\273\351\224\201\344\270\200\346\254\241\346\216\222\346\237\245\345\210\206\346\236\220\350\277\207\347\250\213.md" => "Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\346\205\242\346\205\242\346\235\245\357\274\214\344\270\215\346\200\225Mysql\346\255\273\351\224\201\345\225\246/insert on duplicate\346\255\273\351\224\201\344\270\200\346\254\241\346\216\222\346\237\245\345\210\206\346\236\220\350\277\207\347\250\213.md" (100%) rename "\346\205\242\346\205\242\346\235\245\357\274\214\344\270\215\346\200\225Mysql\346\255\273\351\224\201\345\225\246/\346\255\273\351\224\201\345\210\206\346\236\220\350\277\207\347\250\213.md" => "Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\346\205\242\346\205\242\346\235\245\357\274\214\344\270\215\346\200\225Mysql\346\255\273\351\224\201\345\225\246/\346\255\273\351\224\201\345\210\206\346\236\220\350\277\207\347\250\213.md" (100%) create mode 100644 "\345\210\206\345\270\203\345\274\217/\345\220\216\347\253\257\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\357\274\232\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\345\237\272\347\241\200\347\257\207.md" create mode 100644 "\345\216\237\345\210\233\350\257\227\351\233\206/\345\244\217\345\244\251\347\232\204\351\243\216\346\210\221\346\260\270\350\277\234\350\256\260\345\276\227.md" create mode 100644 "\345\267\245\344\275\234\346\200\273\347\273\223/CAS\344\271\220\350\247\202\351\224\201\350\247\243\345\206\263\345\271\266\345\217\221\351\227\256\351\242\230\347\232\204\344\270\200\346\254\241\345\256\236\350\267\265.md" create mode 100644 "\345\267\245\344\275\234\346\200\273\347\273\223/Java\346\227\245\346\234\237\345\244\204\347\220\206\346\230\223\350\270\251\347\232\204\345\215\201\344\270\252\345\235\221.md" create mode 100644 "\345\267\245\344\275\234\346\200\273\347\273\223/\344\274\230\345\214\226\344\273\243\347\240\201\347\232\204\345\207\240\344\270\252\345\260\217\346\212\200\345\267\247.md" create mode 100644 "\345\267\245\344\275\234\346\200\273\347\273\223/\345\206\231\344\273\243\347\240\201\346\234\211\350\277\231\344\272\233\346\203\263\346\263\225\357\274\214\345\220\214\344\272\213\346\211\215\344\270\215\344\274\232\350\256\244\344\270\272\344\275\240\346\230\257\345\244\215\345\210\266\347\262\230\350\264\264\347\250\213\345\272\217\345\221\230.md" create mode 100644 "\345\267\245\344\275\234\346\200\273\347\273\223/\345\271\266\345\217\221\347\216\257\345\242\203\344\270\213\357\274\214\345\205\210\346\223\215\344\275\234\346\225\260\346\215\256\345\272\223\350\277\230\346\230\257\345\205\210\346\223\215\344\275\234\347\274\223\345\255\230\357\274\237.md" create mode 100644 "\345\267\245\344\275\234\346\200\273\347\273\223/\346\227\245\345\270\270\345\267\245\344\275\234\344\270\255\346\234\200\345\256\271\346\230\223\347\212\257\347\232\204\345\207\240\344\270\252\345\271\266\345\217\221\351\224\231\350\257\257.md" create mode 100644 "\347\250\213\345\272\217\344\272\272\347\224\237&\351\235\242\350\257\225\345\273\272\350\256\256/\351\207\221\344\270\211\351\223\266\345\233\233\357\274\214\347\273\231\351\235\242\350\257\225\350\200\205\347\232\204\345\215\201\345\244\247\345\273\272\350\256\256.md" diff --git "a/Elasticsearch\351\235\242\350\257\225/6. Lucene\345\205\250\346\226\207\346\220\234\347\264\242\347\232\204\345\216\237\347\220\206.md" "b/Elasticsearch\351\235\242\350\257\225/6. Lucene\345\205\250\346\226\207\346\220\234\347\264\242\347\232\204\345\216\237\347\220\206.md" deleted file mode 100644 index e69de29..0000000 diff --git "a/Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\346\265\201\347\250\213\345\233\276/JVM\347\263\273\345\210\227\345\237\272\347\241\200\345\233\276.md" "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/JVM\347\263\273\345\210\227\345\237\272\347\241\200\345\233\276.md" similarity index 100% rename from "Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\346\265\201\347\250\213\345\233\276/JVM\347\263\273\345\210\227\345\237\272\347\241\200\345\233\276.md" rename to "Java\345\237\272\347\241\200\345\255\246\344\271\240/JVM\347\263\273\345\210\227\345\237\272\347\241\200\345\233\276.md" diff --git "a/Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\346\265\201\347\250\213\345\233\276/Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\345\237\272\347\241\200\346\265\201\347\250\213\345\233\276.md" "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\345\237\272\347\241\200\346\265\201\347\250\213\345\233\276.md" similarity index 100% rename from "Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\346\265\201\347\250\213\345\233\276/Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\345\237\272\347\241\200\346\265\201\347\250\213\345\233\276.md" rename to "Java\345\237\272\347\241\200\345\255\246\344\271\240/Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\345\237\272\347\241\200\346\265\201\347\250\213\345\233\276.md" diff --git "a/Java\345\237\272\347\241\200\345\255\246\344\271\240/Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\357\274\232\345\274\202\345\270\270\347\232\204\345\215\201\344\270\252\345\205\263\351\224\256\347\237\245\350\257\206\347\202\271.md" "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\357\274\232\345\274\202\345\270\270\347\232\204\345\215\201\344\270\252\345\205\263\351\224\256\347\237\245\350\257\206\347\202\271.md" new file mode 100644 index 0000000..743dbab --- /dev/null +++ "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\357\274\232\345\274\202\345\270\270\347\232\204\345\215\201\344\270\252\345\205\263\351\224\256\347\237\245\350\257\206\347\202\271.md" @@ -0,0 +1,499 @@ +## 前言 + +总结了Java异常十个关键知识点,面试或者工作中都有用哦,加油。 + +### 一. 异常是什么 + +**异常是指阻止当前方法或作用域继续执行的问题**。比如你读取的文件不存在,数组越界,进行除法时,除数为0等都会导致异常。 + +一个**文件找不到的异常**: +``` +public class TestException { + public static void main(String[] args) throws IOException { + InputStream is = new FileInputStream("jaywei.txt"); + int b; + while ((b = is.read()) != -1) { + + } + } +} +``` +**运行结果:** +``` +Exception in thread "main" java.io.FileNotFoundException: jaywei.txt (系统找不到指定的文件。) + at java.io.FileInputStream.open0(Native Method) + at java.io.FileInputStream.open(FileInputStream.java:195) + at java.io.FileInputStream.(FileInputStream.java:138) + at java.io.FileInputStream.(FileInputStream.java:93) + at exception.TestException.main(TestException.java:10) +``` + +### 二. 异常的层次结构 + + +![](https://user-gold-cdn.xitu.io/2019/11/9/16e50d8a376c1b56?w=1370&h=953&f=png&s=122496) + +从前从前,有位老人,他的名字叫**Throwable**,他生了两个儿子,大儿子叫**Error**,二儿子叫**Exception**。 + +#### Error +表示编译时或者系统错误,如虚拟机相关的错误,OutOfMemoryError等,error是无法处理的。 + +#### Exception +代码异常,Java程序员关心的基类型通常是Exception。它能被程序本身可以处理,这也是它跟Error的区别。 + +它可以分为RuntimeException(运行时异常)和CheckedException(可检查的异常)。 + +**常见的RuntimeException异常:** +``` +- NullPointerException 空指针异常 +- ArithmeticException 出现异常的运算条件时,抛出此异常 +- IndexOutOfBoundsException 数组索引越界异常 +- ClassNotFoundException 找不到类异常 +- IllegalArgumentException(非法参数异常) +``` + +**常见的 Checked Exception 异常:** + +``` +- IOException (操作输入流和输出流时可能出现的异常) +- ClassCastException(类型转换异常类) +``` +- Checked Exception就是编译器要求你必须处置的异常。 +- 与之相反的是,Unchecked Exceptions,它指编译器不要求强制处置的异常,它包括Error和RuntimeException 以及他们的子类。 + + +### 三、异常处理 + +当异常出现后,会在堆上创建异常对象。当前的执行路径被终止,并且从当前环境中弹出对异常对象的引用。这时候**异常处理**程序,使程序从错误状态恢复,使程序继续运行下去。 + +异常处理主要有抛出异常、捕获异常、声明异常。如图: + +![](https://user-gold-cdn.xitu.io/2019/11/10/16e55f2c321d909d?w=1050&h=619&f=png&s=74396) + + + +#### 捕获异常 + +``` +try{ +// 程序代码 +}catch(Exception e){ +//Catch 块 +}finaly{ + //无论如何,都会执行的代码块 +} +``` + +我们可以通过```try...catch...```捕获异常代码,再通过```finaly```执行最后的操作,如关闭流等操作。 + +#### 声明抛出异常 +除了```try...catch...```捕获异常,我们还可以通过throws声明抛出异常。 + +当你定义了一个方法时,可以用```throws```关键字声明。使用了```throws```关键字表明,该方法不处理异常,而是把异常留给它的调用者处理。是不是觉得TA不负责任? + +哈哈,看一下demo吧 +``` +//该方法通过throws声明了IO异常。 + private void readFile() throws IOException { + InputStream is = new FileInputStream("jaywei.txt"); + int b; + while ((b = is.read()) != -1) { + + } + } +``` + +从方法中声明抛出的任何异常都必须使用throws子句。 + +#### 抛出异常 +throw关键字作用是抛出一个```Throwable```类型的异常,它一般出现在函数体中。在异常处理中,try语句要捕获的是一个异常对象,其实此异常对象也可以自己抛出。 + +例如抛出一个 RuntimeException 类的异常对象: +``` +throw new RuntimeException(e); +``` + +任何Java代码都可以通过 Java 的throw语句抛出异常。 + +#### 注意点 +- 非检查异常(Error、RuntimeException 或它们的子类)不可使用 throws 关键字来声明要抛出的异常。 +- 一个方法出现编译时异常,就需要 try-catch/ throws 处理,否则会导致编译错误。 + + +### 四、try-catch-finally-return执行顺序 + +**try-catch-finally-return 执行描述** + +- 如果不发生异常,不会执行catch部分。 +- 不管有没有发生异常,finally都会执行到。 +- 即使try和catch中有return时,finally仍然会执行 +- finally是在return后面的表达式运算完后再执行的。(此时并没有返回运算后的值,而是先把要返回的值保存起来,若finally中无return,则不管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),该情况下函数返回值是在finally执行前确定的) +- finally部分就不要return了,要不然,就回不去try或者catch的return了。 + +**看一个例子** +``` + public static void main(String[] args) throws IOException { + System.out.println("result:" + test()); + } + + private static int test() { + int temp = 1; + try { + System.out.println("start execute try,temp is:"+temp); + return ++temp; + } catch (Exception e) { + System.out.println("start execute catch temp is: "+temp); + return ++temp; + } finally { + System.out.println("start execute finally,temp is:" + temp); + ++temp; + } + } +``` + +**运行结果:** +``` +start execute try,temp is:1 +start execute finally,temp is:2 +result:2 +``` +**分析** +- 先执行try部分,输出日志,执行```++temp```表达式,temp变为2,这个值被保存起来。 +- 因为没有发生异常,所以catch代码块跳过。 +- 执行finally代码块,输出日志,执行```++temp```表达式. +- 返回try部分保存的值2. + + +### 五、Java异常类的几个重要方法 + +先来喵一眼异常类的所有方法,如下图: +![](https://user-gold-cdn.xitu.io/2019/11/18/16e7efaaee8e0f23?w=2447&h=987&f=png&s=289531) + +#### getMessage +``` +Returns the detail message string of this throwable. +``` + +getMessage会返回Throwable的```detailMessage```属性,而```detailMessage```就表示发生异常的详细消息描述。 + +举个例子,```FileNotFoundException```异常发生时,这个```detailMessage```就包含这个找不到文件的名字。 + +#### getLocalizedMessage + +``` +Creates a localized description of this throwable.Subclasses may override this +method in order to produce alocale-specific message. For subclasses that do not +override thismethod, the default implementation returns the same result +as getMessage() +``` + +throwable的本地化描述。子类可以重写此方法,以生成特定于语言环境的消息。对于不覆盖此方法的子类,默认实现返回与相同的结果 getMessage()。 + +#### getCause + +``` +Returns the cause of this throwable or null if thecause is nonexistent or unknown. +``` + +返回此可抛出事件的原因,或者,如果原因不存在或未知,返回null。 + + +#### printStackTrace + +``` +Prints this throwable and its backtrace to thestandard error stream. + +The first line of output contains the result of the toString() method for +this object.Remaining lines represent data previously recorded by the +method fillInStackTrace(). +``` + +该方法将堆栈跟踪信息打印到标准错误流。 + +输出的第一行,包含此对象toString()方法的结果。剩余的行表示,先前被方法fillInStackTrace()记录的数据。如下例子: + +``` + java.lang.NullPointerException + at MyClass.mash(MyClass.java:9) + at MyClass.crunch(MyClass.java:6) + at MyClass.main(MyClass.java:3) +``` + + +### 六、自定义异常 +自定义异常通常是定义一个继承自 Exception 类的子类。 + +**那么,为什么需要自定义异常?** + +- Java提供的异常体系不可能预见所有的错误。 +- 业务开发中,使用自定义异常,可以让项目代码更加规范,也便于管理。 + +下面是我司自定义异常类的一个简单demo + +``` +public class BizException extends Exception { + //错误信息 + private String message; + //错误码 + private String errorCode; + + public BizException() { + } + + public BizException(String message, String errorCode) { + this.message = message; + this.errorCode = errorCode; + } + + @Override + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getErrorCode() { + return errorCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } +} +``` + +**跑个main方测试一下** +``` + +public class TestBizException { + + public static void testBizException() throws BizException { + System.out.println("throwing BizException from testBizException()"); + throw new BizException("100","哥,我错了"); + } + + public static void main(String[] args) { + try { + testBizException(); + } catch (BizException e) { + System.out.println("自己定义的异常"); + e.printStackTrace(); + } + } +} + +``` +**运行结果:** + +``` +exception.BizException: 100 +throwing BizException from testBizException() +自己定义的异常 + at exception.TestBizException.testBizException(TestBizException.java:7) + at exception.TestBizException.main(TestBizException.java:12) +``` + +### 七、Java7 新的 try-with-resources语句 +try-with-resources,是Java7提供的一个新功能,它用于自动资源管理。 +- 资源是指在程序用完了之后必须要关闭的对象。 +- try-with-resources保证了每个声明了的资源在语句结束的时候会被关闭 +- 什么样的对象才能当做资源使用呢?只要实现了java.lang.AutoCloseable接口或者java.io.Closeable接口的对象,都OK。 + +**在```try-with-resources```出现之前** +``` +try{ + //open resources like File, Database connection, Sockets etc +} catch (FileNotFoundException e) { + // Exception handling like FileNotFoundException, IOException etc +}finally{ + // close resources +} +``` + +**Java7,```try-with-resources```出现之后,使用资源实现** +``` +try(// open resources here){ + // use resources +} catch (FileNotFoundException e) { + // exception handling +} +// resources are closed as soon as try-catch block is executed. +``` + +**Java7使用资源demo** +``` +public class Java7TryResourceTest { + public static void main(String[] args) { + try (BufferedReader br = new BufferedReader(new FileReader( + "C:/jaywei.txt"))) { + System.out.println(br.readLine()); + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +**使用了```try-with-resources```的好处** +- 代码更加优雅,行数更少。 +- 资源自动管理,不用担心内存泄漏问题。 + + +### 八、异常链 +我们常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称为**异常链**。 + +```throw``` 抛出的是一个新的异常信息,这样会导致原有的异常信息丢失。在JDk1.4以前,程序员必须自己编写代码来保存原始异常信息。现在所有```Throwable```子类在构造器中都可以接受一个```cause(异常因由)```对象作为参数。 + +这个```cause```就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。 + +使用方式如下: + +``` +public class TestChainException { + + public void readFile() throws MyException{ + try { + InputStream is = new FileInputStream("jay.txt"); + Scanner in = new Scanner(is); + while (in.hasNext()) { + System.out.println(in.next()); + } + } catch (FileNotFoundException e) { + //e 保存异常信息 + throw new MyException("文件在哪里呢", e); + } + } + + public void invokeReadFile() throws MyException{ + try { + readFile(); + } catch (MyException e) { + //e 保存异常信息 + throw new MyException("文件找不到", e); + } + } + + public static void main(String[] args) { + TestChainException t = new TestChainException(); + try { + t.invokeReadFile(); + } catch (MyException e) { + e.printStackTrace(); + } + } + +} + +//MyException 构造器 +public MyException(String message, Throwable cause) { + super(message, cause); + } +``` + +**运行结果:** + +![](https://user-gold-cdn.xitu.io/2019/11/19/16e80e7cd41a3a2b?w=1030&h=469&f=png&s=85977) + +我们可以看到异常信息有保存下来的,如果把cause(也就是FileNotFoundException 的e)去掉呢,看一下运行结果: + +![](https://user-gold-cdn.xitu.io/2019/11/19/16e80eebc99b5ea8?w=891&h=253&f=png&s=41192) + +可以发现,少了```Throwable cause```,原始异常信息不翼而飞了。 + + +### 九、异常匹配 + +抛出异常的时候,异常处理系统会按照代码的书写顺序找出"最近"的处理程序。 找到匹配的处理程序之后,它就认为异常将得到处理,然后就不再继续查找。 + +查找的时候并不要求抛出的异常同处理程序的异常完全匹配。派生类的对象也可以配备其基类的处理程序 + +看demo +``` +package exceptions; +//: exceptions/Human.java +// Catching exception hierarchies. + +class Annoyance extends Exception {} +class Sneeze extends Annoyance {} + +public class Human { + public static void main(String[] args) { + // Catch the exact type: + try { + throw new Sneeze(); + } catch(Sneeze s) { + System.out.println("Caught Sneeze"); + } catch(Annoyance a) { + System.out.println("Caught Annoyance"); + } + // Catch the base type: + try { + throw new Sneeze(); + } catch(Annoyance a) { + System.out.println("Caught Annoyance"); + } + } +} +``` + +运行结果: + +![](https://user-gold-cdn.xitu.io/2019/11/19/16e810d144630ab3?w=540&h=198&f=png&s=14568) + +catch(Annoyance a)会捕获Annoyance以及所有从它派生的异常。 +捕获基类的异常,就可以匹配所有派生类的异常 + + +``` +try { + throw new Sneeze(); + } catch(Annoyance a) { + } catch(Sneeze s) { //这句编译器会报错,因为异常已由前面catch子句处理 + } +``` + +### 十、Java常见异常 +#### NullPointerException +空指针异常,最常见的一个异常类。简言之,调用了未经初始化的对象或者是不存在的对象,就会产生该异常。 +#### ArithmeticException +算术异常类,程序中出现了除数为0这样的运算,就会出现这样的异常。 +#### ClassCastException +类型强制转换异常,它是JVM在检测到两个类型间转换不兼容时引发的运行时异常。 +#### ArrayIndexOutOfBoundsException +数组下标越界异常,跟数组打交道时,需要注意一下这个异常。 +#### FileNotFoundException +文件未找到异常,一般是要读或者写的文件,找不到,导致该异常。 +#### SQLException +操作数据库异常,它是Checked Exception(检查异常); +#### IOException +IO异常,一般跟读写文件息息相关,它也是Checked Exception(检查异常)。平时读写文件,记得IO流关闭! +#### NoSuchMethodException +方法未找到异常 +#### NumberFormatException +字符串转换为数字异常 + +## 总结 +这个总结独辟蹊径,以几道经典异常面试题结束吧,以帮助大家复习一下,嘻嘻。 +- java 异常有哪几种,特点是什么?(知识点二可答) +- 什么是Java中的异常?(知识点一可答) +- error和exception有什么区别?(知识点二可答) +- 什么是异常链?(知识点八可答) +- try-catch-finally-return执行顺序(知识点四可答) +- 列出常见的几种RunException (知识点二可答) +- Java异常类的重要方法是什么?(知识点五可答) +- error和exception的区别,CheckedException,RuntimeException的区别。(知识点二可答) +- 请列出5个运行时异常。(知识点二可答) +- Java 7 新的 try-with-resources 语句(知识点七可答) +- 怎么自定义异常?(知识点六可答) +- 说一下常见异常以及产生原因(知识点十可答) +- 谈谈异常匹配(知识点九可答) +- 谈谈异常处理(知识点三可答) + +## 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。 +- 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。 diff --git "a/Java\345\237\272\347\241\200\345\255\246\344\271\240/if-else\344\273\243\347\240\201\344\274\230\345\214\226\347\232\204\345\205\253\347\247\215\346\226\271\346\241\210.md" "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/if-else\344\273\243\347\240\201\344\274\230\345\214\226\347\232\204\345\205\253\347\247\215\346\226\271\346\241\210.md" new file mode 100644 index 0000000..03740f3 --- /dev/null +++ "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/if-else\344\273\243\347\240\201\344\274\230\345\214\226\347\232\204\345\205\253\347\247\215\346\226\271\346\241\210.md" @@ -0,0 +1,295 @@ +## 前言 +代码中如果if-else比较多,阅读起来比较困难,维护起来也比较困难,很容易出bug,接下来,本文将介绍优化if-else代码的八种方案。 + + +![](https://user-gold-cdn.xitu.io/2020/3/7/170b325db76e6d7f?w=1202&h=594&f=png&s=91815) + +### 优化方案一:提前return,去除不必要的else +如果if-else代码块包含return语句,可以考虑通过提前return,把多余else干掉,使代码更加优雅。 + +**优化前:** + +``` +if(condition){ + //doSomething +}else{ + return ; +} +``` +**优化后:** + +``` +if(!condition){ + return ; +} +//doSomething +``` + +### 优化方案二:使用条件三目运算符 +使用条件三目运算符可以简化某些if-else,使代码更加简洁,更具有可读性。 + +**优化前:** +``` +int price ; +if(condition){ + price = 80; +}else{ + price = 100; +} +``` +优化后: +``` +int price = condition?80:100; +``` + + +### 优化方案三:使用枚举 +在某些时候,使用枚举也可以优化if-else逻辑分支,按个人理解,它也可以看做一种**表驱动方法**。 + +**优化前:** + +``` +String OrderStatusDes; +if(orderStatus==0){ + OrderStatusDes ="订单未支付"; +}else if(OrderStatus==1){ + OrderStatusDes ="订单已支付"; +}else if(OrderStatus==2){ + OrderStatusDes ="已发货"; +} +... +``` +**优化后:** + +先定义一个枚举 +``` +: +public enum OrderStatusEnum { + UN_PAID(0,"订单未支付"),PAIDED(1,"订单已支付"),SENDED(2,"已发货"),; + + private int index; + private String desc; + + public int getIndex() { + return index; + } + + public String getDesc() { + return desc; + } + + OrderStatusEnum(int index, String desc){ + this.index = index; + this.desc =desc; + } + + OrderStatusEnum of(int orderStatus) { + for (OrderStatusEnum temp : OrderStatusEnum.values()) { + if (temp.getIndex() == orderStatus) { + return temp; + } + } + return null; + } +} + + +``` +有了枚举之后,以上if-else逻辑分支,可以优化为一行代码 +``` +String OrderStatusDes = OrderStatusEnum.0f(orderStatus).getDesc(); +``` +### 优化方案四:合并条件表达式 +如果有一系列条件返回一样的结果,可以将它们合并为一个条件表达式,让逻辑更加清晰。 + +**优化前** +``` + double getVipDiscount() { + if(age<18){ + return 0.8; + } + if("深圳".equals(city)){ + return 0.8; + } + if(isStudent){ + return 0.8; + } + //do somethig + } +``` +**优化后** + +``` + double getVipDiscount(){ + if(age<18|| "深圳".equals(city)||isStudent){ + return 0.8; + } + //doSomthing + } +``` +### 优化方案五:使用 Optional +有时候if-else比较多,是因为非空判断导致的,这时候你可以使用java8的Optional进行优化。 + +**优化前:** +``` +String str = "jay@huaxiao"; +if (str != null) { + System.out.println(str); +} else { + System.out.println("Null"); +} +``` +**优化后:** + +``` +Optional strOptional = Optional.of("jay@huaxiao"); +strOptional.ifPresentOrElse(System.out::println, () -> System.out.println("Null")); +``` + + +### 优化方案六:表驱动法 +**表驱动法**,又称之为表驱动、表驱动方法。表驱动方法是一种使你可以在表中查找信息,而不必用很多的逻辑语句(if或Case)来把它们找出来的方法。以下的demo,把map抽象成表,在map中查找信息,而省去不必要的逻辑语句。 + +**优化前:** + +``` +if (param.equals(value1)) { + doAction1(someParams); +} else if (param.equals(value2)) { + doAction2(someParams); +} else if (param.equals(value3)) { + doAction3(someParams); +} +// ... +``` + +**优化后:** + +``` +Map action> actionMappings = new HashMap<>(); // 这里泛型 ? 是为方便演示,实际可替换为你需要的类型 + +// 初始化 +actionMappings.put(value1, (someParams) -> { doAction1(someParams)}); +actionMappings.put(value2, (someParams) -> { doAction2(someParams)}); +actionMappings.put(value3, (someParams) -> { doAction3(someParams)}); + +// 省略多余逻辑语句 +actionMappings.get(param).apply(someParams); +``` + +### 优化方案七:优化逻辑结构,让正常流程走主干 + +**优化前:** +``` +public double getAdjustedCapital(){ + if(_capital <= 0.0 ){ + return 0.0; + } + if(_intRate > 0 && _duration >0){ + return (_income / _duration) *ADJ_FACTOR; + } + return 0.0; +} +``` +**优化后:** +``` +public double getAdjustedCapital(){ + if(_capital <= 0.0 ){ + return 0.0; + } + if(_intRate <= 0 || _duration <= 0){ + return 0.0; + } + + return (_income / _duration) *ADJ_FACTOR; +} +``` +将条件反转使异常情况先退出,让正常流程维持在主干流程,可以让代码结构更加清晰。 + +### 优化方案八:策略模式+工厂方法消除if else + +假设需求为,根据不同勋章类型,处理相对应的勋章服务,优化前有以下代码: +``` + String medalType = "guest"; + if ("guest".equals(medalType)) { + System.out.println("嘉宾勋章"); + } else if ("vip".equals(medalType)) { + System.out.println("会员勋章"); + } else if ("guard".equals(medalType)) { + System.out.println("展示守护勋章"); + } + ... +``` + +首先,我们把每个条件逻辑代码块,抽象成一个公共的接口,可以得出以下代码: + +``` +//勋章接口 +public interface IMedalService { + void showMedal(); +} +``` +我们根据每个逻辑条件,定义相对应的策略实现类,可得以下代码: +``` +//守护勋章策略实现类 +public class GuardMedalServiceImpl implements IMedalService { + @Override + public void showMedal() { + System.out.println("展示守护勋章"); + } +} +//嘉宾勋章策略实现类 +public class GuestMedalServiceImpl implements IMedalService { + @Override + public void showMedal() { + System.out.println("嘉宾勋章"); + } +} +//VIP勋章策略实现类 +public class VipMedalServiceImpl implements IMedalService { + @Override + public void showMedal() { + System.out.println("会员勋章"); + } +} +``` +接下来,我们再定义策略工厂类,用来管理这些勋章实现策略类,如下: + +``` +//勋章服务工产类 +public class MedalServicesFactory { + + private static final Map map = new HashMap<>(); + static { + map.put("guard", new GuardMedalServiceImpl()); + map.put("vip", new VipMedalServiceImpl()); + map.put("guest", new GuestMedalServiceImpl()); + } + public static IMedalService getMedalService(String medalType) { + return map.get(medalType); + } +} +``` +使用了策略+工厂模式之后,代码变得简洁多了,如下: +``` +public class Test { + public static void main(String[] args) { + String medalType = "guest"; + IMedalService medalService = MedalServicesFactory.getMedalService(medalType); + medalService.showMedal(); + } +} +``` + + +## 参考与感谢 +- [6个实例详解如何把if-else代码重构成高质量代码](https://blog.csdn.net/qq_35440678/article/details/77939999) +- [如何 “干掉” if...else](https://www.jianshu.com/p/1db0bba283f0) + +## 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 觉得写得好的小伙伴给个点赞+关注啦,谢谢~ +- 同时非常期待小伙伴们能够关注我公众号,后面慢慢推出更好的干货~嘻嘻 +- github地址:https://github.com/whx123/JavaHome \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200\345\255\246\344\271\240/java\345\272\217\345\210\227\345\214\226.md" "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/java\345\272\217\345\210\227\345\214\226.md" new file mode 100644 index 0000000..fddfdc4 --- /dev/null +++ "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/java\345\272\217\345\210\227\345\214\226.md" @@ -0,0 +1,432 @@ +### 前言 +相信大家日常开发中,经常看到Java对象“implements Serializable”。那么,它到底有什么用呢?本文从以下几个角度来解析序列这一块知识点~ +- 什么是Java序列化? +- 为什么需要序列化? +- 序列化用途 +- Java序列化常用API +- 序列化的使用 +- 序列化底层 +- 日常开发序列化的注意点 +- 序列化常见面试题 + + +### 一、什么是Java序列化? + +- 序列化:把Java对象转换为字节序列的过程 +- 反序列:把字节序列恢复为Java对象的过程 +![](https://user-gold-cdn.xitu.io/2020/4/16/1718072401688be6?w=1618&h=615&f=png&s=97837) + +### 二、为什么需要序列化? +Java对象是运行在JVM的堆内存中的,如果JVM停止后,它的生命也就戛然而止。 + +![](https://user-gold-cdn.xitu.io/2020/4/18/17188de5182865ab?w=1322&h=802&f=png&s=94894) +如果想在JVM停止后,把这些对象保存到磁盘或者通过网络传输到另一远程机器,怎么办呢?磁盘这些硬件可不认识Java对象,它们只认识二进制这些机器语言,所以我们就要把这些对象转化为字节数组,这个过程就是序列化啦~ + +> 打个比喻,作为大城市漂泊的码农,搬家是常态。当我们搬书桌时,桌子太大了就通不过比较小的门,因此我们需要把它拆开再搬过去,这个拆桌子的过程就是序列化。 而我们把书桌复原回来(安装)的过程就是反序列化啦。 + +### 三、序列化用途 +序列化使得对象可以脱离程序运行而独立存在,它主要有两种用途: + +![](https://user-gold-cdn.xitu.io/2020/4/19/17190104b236e38c?w=886&h=444&f=png&s=33182) +- 1) 序列化机制可以让对象地保存到硬盘上,减轻内存压力的同时,也起了持久化的作用; + +> 比如 Web服务器中的Session对象,当有 10+万用户并发访问的,就有可能出现10万个Session对象,内存可能消化不良,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。 + +- 2) 序列化机制让Java对象在网络传输不再是天方夜谭。 +> 我们在使用Dubbo远程调用服务框架时,需要把传输的Java对象实现Serializable接口,即让Java对象序列化,因为这样才能让对象在网络上传输。 + + +### 四、Java序列化常用API + +``` +java.io.ObjectOutputStream +java.io.ObjectInputStream +java.io.Serializable +java.io.Externalizable +``` +#### Serializable 接口 +Serializable接口是一个标记接口,没有方法或字段。一旦实现了此接口,就标志该类的对象就是可序列化的。 + +``` +public interface Serializable { +} +``` +#### Externalizable 接口 +Externalizable继承了Serializable接口,还定义了两个抽象方法:writeExternal()和readExternal(),如果开发人员使用Externalizable来实现序列化和反序列化,需要重写writeExternal()和readExternal()方法 +``` +public interface Externalizable extends java.io.Serializable { + void writeExternal(ObjectOutput out) throws IOException; + void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; +} +``` + +#### java.io.ObjectOutputStream类 +表示对象输出流,它的writeObject(Object obj)方法可以对指定obj对象参数进行序列化,再把得到的字节序列写到一个目标输出流中。 + +#### java.io.ObjectInputStream +表示对象输入流, +它的readObject()方法,从输入流中读取到字节序列,反序列化成为一个对象,最后将其返回。 + + +### 五、序列化的使用 +序列化如何使用?来看一下,序列化的使用的几个关键点吧: +- 声明一个实体类,实现Serializable接口 +- 使用ObjectOutputStream类的writeObject方法,实现序列化 +- 使用ObjectInputStream类的readObject方法,实现反序列化 + +#### 声明一个Student类,实现Serializable +``` +public class Student implements Serializable { + + private Integer age; + private String name; + + public Integer getAge() { + return age; + } + public void setAge(Integer age) { + this.age = age; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } +} +``` +#### 使用ObjectOutputStream类的writeObject方法,对Student对象实现序列化 +把Student对象设置值后,写入一个文件,即序列化,哈哈~ +``` +ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("D:\\text.out")); +Student student = new Student(); +student.setAge(25); +student.setName("jayWei"); +objectOutputStream.writeObject(student); + +objectOutputStream.flush(); +objectOutputStream.close(); +``` +看看序列化的可爱模样吧,test.out文件内容如下(使用UltraEdit打开): +![](https://user-gold-cdn.xitu.io/2020/4/18/1718cb65a1d08785?w=896&h=311&f=png&s=59011) +#### 使用ObjectInputStream类的readObject方法,实现反序列化,重新生成student对象 +再把test.out文件读取出来,反序列化为Student对象 +``` +ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out")); +Student student = (Student) objectInputStream.readObject(); +System.out.println("name="+student.getName()); +``` +![](https://user-gold-cdn.xitu.io/2020/4/18/1718cb9ec7ca958f?w=1227&h=395&f=png&s=46746) +### 六、序列化底层 + +#### Serializable底层 +Serializable接口,只是一个空的接口,没有方法或字段,为什么这么神奇,实现了它就可以让对象序列化了? +``` +public interface Serializable { +} +``` +为了验证Serializable的作用,把以上demo的Student对象,去掉实现Serializable接口,看序列化过程怎样吧~ + +![](https://user-gold-cdn.xitu.io/2020/4/18/1718cbbb01a02999?w=607&h=542&f=png&s=46516) + +序列化过程中抛出异常啦,堆栈信息如下: +``` +Exception in thread "main" java.io.NotSerializableException: com.example.demo.Student + at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184) + at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348) + at com.example.demo.Test.main(Test.java:13) +``` +顺着堆栈信息看一下,原来有重大发现,如下~ +![](https://user-gold-cdn.xitu.io/2020/4/18/1718c980c904c2ee?w=802&h=636&f=png&s=69506) +**原来底层是这样:** +ObjectOutputStream 在序列化的时候,会判断被序列化的Object是哪一种类型,String?array?enum?还是 Serializable,如果都不是的话,抛出 NotSerializableException异常。所以呀,**Serializable真的只是一个标志,一个序列化标志**~ + +#### writeObject(Object) +序列化的方法就是writeObject,基于以上的demo,我们来分析一波它的核心方法调用链吧~(建议大家也去debug看一下这个方法,感兴趣的话) +![](https://user-gold-cdn.xitu.io/2020/4/18/1718d1fbad278f8f?w=668&h=1003&f=png&s=60938) + +writeObject直接调用的就是writeObject0()方法, + +``` +public final void writeObject(Object obj) throws IOException { + ...... + writeObject0(obj, false); + ...... +} +``` +writeObject0 主要实现是对象的不同类型,调用不同的方法写入序列化数据,这里面如果对象实现了Serializable接口,就调用writeOrdinaryObject()方法~ +``` +private void writeObject0(Object obj, boolean unshared) + throws IOException + { + ...... + //String类型 + if (obj instanceof String) { + writeString((String) obj, unshared); + //数组类型 + } else if (cl.isArray()) { + writeArray(obj, desc, unshared); + //枚举类型 + } else if (obj instanceof Enum) { + writeEnum((Enum) obj, desc, unshared); + //Serializable实现序列化接口 + } else if (obj instanceof Serializable) { + writeOrdinaryObject(obj, desc, unshared); + } else{ + //其他情况会抛异常~ + if (extendedDebugInfo) { + throw new NotSerializableException( + cl.getName() + "\n" + debugInfoStack.toString()); + } else { + throw new NotSerializableException(cl.getName()); + } + } + ...... +``` + +writeOrdinaryObject()会先调用writeClassDesc(desc),写入该类的生成信息,然后调用writeSerialData方法,写入序列化数据 +``` + private void writeOrdinaryObject(Object obj, + ObjectStreamClass desc, + boolean unshared) + throws IOException + { + ...... + //调用ObjectStreamClass的写入方法 + writeClassDesc(desc, false); + // 判断是否实现了Externalizable接口 + if (desc.isExternalizable() && !desc.isProxy()) { + writeExternalData((Externalizable) obj); + } else { + //写入序列化数据 + writeSerialData(obj, desc); + } + ..... + } +``` +writeSerialData()实现的就是写入被序列化对象的字段数据 + +``` + private void writeSerialData(Object obj, ObjectStreamClass desc) + throws IOException + { + for (int i = 0; i < slots.length; i++) { + if (slotDesc.hasWriteObjectMethod()) { + //如果被序列化的对象自定义实现了writeObject()方法,则执行这个代码块 + slotDesc.invokeWriteObject(obj, this); + } else { + // 调用默认的方法写入实例数据 + defaultWriteFields(obj, slotDesc); + } + } + } +``` +defaultWriteFields()方法,获取类的基本数据类型数据,直接写入底层字节容器;获取类的obj类型数据,循环递归调用writeObject0()方法,写入数据~ +``` + private void defaultWriteFields(Object obj, ObjectStreamClass desc) + throws IOException + { + // 获取类的基本数据类型数据,保存到primVals字节数组 + desc.getPrimFieldValues(obj, primVals); + //primVals的基本类型数据写到底层字节容器 + bout.write(primVals, 0, primDataSize, false); + + // 获取对应类的所有字段对象 + ObjectStreamField[] fields = desc.getFields(false); + Object[] objVals = new Object[desc.getNumObjFields()]; + int numPrimFields = fields.length - objVals.length; + // 获取类的obj类型数据,保存到objVals字节数组 + desc.getObjFieldValues(obj, objVals); + //对所有Object类型的字段,循环 + for (int i = 0; i < objVals.length; i++) { + ...... + //递归调用writeObject0()方法,写入对应的数据 + writeObject0(objVals[i], + fields[numPrimFields + i].isUnshared()); + ...... + } + } +``` + +### 七、日常开发序列化的一些注意点 +- static静态变量和transient 修饰的字段是不会被序列化的 +- serialVersionUID问题 +- 如果某个序列化类的成员变量是对象类型,则该对象类型的类必须实现序列化 +- 子类实现了序列化,父类没有实现序列化,父类中的字段丢失问题 + + +#### static静态变量和transient 修饰的字段是不会被序列化的 +static静态变量和transient 修饰的字段是不会被序列化的,我们来看例子分析一波~ Student类加了一个类变量gender和一个transient修饰的字段specialty +``` +public class Student implements Serializable { + + private Integer age; + private String name; + + public static String gender = "男"; + transient String specialty = "计算机专业"; + + public String getSpecialty() { + return specialty; + } + + public void setSpecialty(String specialty) { + this.specialty = specialty; + } + + @Override + public String toString() { + return "Student{" +"age=" + age + ", name='" + name + '\'' + ", gender='" + gender + '\'' + ", specialty='" + specialty + '\'' + + '}'; + } + ...... +``` +打印学生对象,序列化到文件,接着修改静态变量的值,再反序列化,输出反序列化后的对象~ +![](https://user-gold-cdn.xitu.io/2020/4/19/1718e0bcd6cdcbfe?w=1240&h=527&f=png&s=83206) +运行结果: + +``` +序列化前Student{age=25, name='jayWei', gender='男', specialty='计算机专业'} +序列化后Student{age=25, name='jayWei', gender='女', specialty='null'} +``` +对比结果可以发现: + +- 1)序列化前的静态变量性别明明是‘男’,序列化后再在程序中修改,反序列化后却变成‘女’了,**what**?显然这个静态属性并没有进行序列化。其实,**静态(static)成员变量是属于类级别的,而序列化是针对对象的~所以不能序列化哦**。 +- 2)经过序列化和反序列化过程后,specialty字段变量值由'计算机专业'变为空了,为什么呢?其实是因为transient关键字,**它可以阻止修饰的字段被序列化到文件中**,在被反序列化后,transient 字段的值被设为初始值,比如int型的值会被设置为 0,对象型初始值会被设置为null。 + +#### serialVersionUID问题 +serialVersionUID 表面意思就是**序列化版本号ID**,其实每一个实现Serializable接口的类,都有一个表示序列化版本标识符的静态变量,或者默认等于1L,或者等于对象的哈希码。 +``` +private static final long serialVersionUID = -6384871967268653799L; +``` +**serialVersionUID有什么用?** + +JAVA序列化的机制是通过判断类的serialVersionUID来验证版本是否一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,如果相同,反序列化成功,如果不相同,就抛出InvalidClassException异常。 + +接下来,我们来验证一下吧,修改一下Student类,再反序列化操作 + +![](https://user-gold-cdn.xitu.io/2020/4/19/1718f8eb4c01274f?w=593&h=315&f=png&s=29002) + +``` +Exception in thread "main" java.io.InvalidClassException: com.example.demo.Student; +local class incompatible: stream classdesc serialVersionUID = 3096644667492403394, +local class serialVersionUID = 4429793331949928814 + at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687) + at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1876) + at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1745) + at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2033) + at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1567) + at java.io.ObjectInputStream.readObject(ObjectInputStream.java:427) + at com.example.demo.Test.main(Test.java:20) +``` +从日志堆栈异常信息可以看到,文件流中的class和当前类路径中的class不同了,它们的serialVersionUID不相同,所以反序列化抛出InvalidClassException异常。那么,如果确实需要修改Student类,又想反序列化成功,怎么办呢?可以手动指定serialVersionUID的值,一般可以设置为1L或者,或者让我们的编辑器IDE生成 + +``` +private static final long serialVersionUID = -6564022808907262054L; +``` +实际上,阿里开发手册,强制要求序列化类新增属性时,不能修改serialVersionUID字段~ +![](https://user-gold-cdn.xitu.io/2020/4/19/1718f78ab09a17cd?w=925&h=140&f=png&s=39827) + +#### 如果某个序列化类的成员变量是对象类型,则该对象类型的类必须实现序列化 +给Student类添加一个Teacher类型的成员变量,其中Teacher是没有实现序列化接口的 + +``` +public class Student implements Serializable { + + private Integer age; + private String name; + private Teacher teacher; + ... +} +//Teacher 没有实现 +public class Teacher { +...... +} +``` +序列化运行,就报NotSerializableException异常啦 +``` +Exception in thread "main" java.io.NotSerializableException: com.example.demo.Teacher + at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184) + at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548) + at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509) + at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432) + at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178) + at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348) + at com.example.demo.Test.main(Test.java:16) +``` +其实这个可以在上小节的底层源码分析找到答案,一个对象序列化过程,会循环调用它的Object类型字段,递归调用序列化的,也就是说,序列化Student类的时候,会对Teacher类进行序列化,但是对Teacher没有实现序列化接口,因此抛出NotSerializableException异常。所以如果某个实例化类的成员变量是对象类型,则该对象类型的类必须实现序列化 +![](https://user-gold-cdn.xitu.io/2020/4/19/1718fa928a6ff178?w=852&h=415&f=png&s=52155) + +#### 子类实现了Serializable,父类没有实现Serializable接口的话,父类不会被序列化。 +子类Student实现了Serializable接口,父类User没有实现Serializable接口 +``` +//父类实现了Serializable接口 +public class Student extends User implements Serializable { + + private Integer age; + private String name; +} +//父类没有实现Serializable接口 +public class User { + String userId; +} + +Student student = new Student(); +student.setAge(25); +student.setName("jayWei"); +student.setUserId("1"); + +ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\text.out")); +objectOutputStream.writeObject(student); + +objectOutputStream.flush(); +objectOutputStream.close(); + +//反序列化结果 +ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out")); +Student student1 = (Student) objectInputStream.readObject(); +System.out.println(student1.getUserId()); +//output +/** + * null + */ +``` +从反序列化结果,可以发现,父类属性值丢失了。因此子类实现了Serializable接口,父类没有实现Serializable接口的话,父类不会被序列化。 + + +### 八、序列化常见面试题 +- 序列化的底层是怎么实现的? +- 序列化时,如何让某些成员不要序列化? +- 在 Java 中,Serializable 和 Externalizable 有什么区别 +- serialVersionUID有什么用? +- 是否可以自定义序列化过程, 或者是否可以覆盖 Java 中的默认序列化过程? +- 在 Java 序列化期间,哪些变量未序列化? + +#### 1.序列化的底层是怎么实现的? +本文第六小节可以回答这个问题,如回答Serializable关键字作用,序列化标志啦,源码中,它的作用啦~还有,可以回答writeObject几个核心方法,如直接写入基本类型,获取obj类型数据,循环递归写入,哈哈~ + +#### 2.序列化时,如何让某些成员不要序列化? +可以用transient关键字修饰,它可以阻止修饰的字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如int型的值会被设置为 0,对象型初始值会被设置为null。 +#### 3.在 Java 中,Serializable 和 Externalizable 有什么区别 +Externalizable继承了Serializable,给我们提供 writeExternal() 和 readExternal() 方法, 让我们可以控制 Java的序列化机制, 不依赖于Java的默认序列化。正确实现 Externalizable 接口可以显著提高应用程序的性能。 + +#### 4.serialVersionUID有什么用? +可以看回本文第七小节哈,JAVA序列化的机制是通过判断类的serialVersionUID来验证版本是否一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,如果相同,反序列化成功,如果不相同,就抛出InvalidClassException异常。 + +#### 5.是否可以自定义序列化过程, 或者是否可以覆盖 Java 中的默认序列化过程? +可以的。我们都知道,对于序列化一个对象需调用 ObjectOutputStream.writeObject(saveThisObject), 并用 ObjectInputStream.readObject() 读取对象, 但 Java 虚拟机为你提供的还有一件事, 是定义这两个方法。如果在类中定义这两种方法, 则 JVM 将调用这两种方法, 而不是应用默认序列化机制。同时,可以声明这些方法为私有方法,以避免被继承、重写或重载。 +#### 6.在 Java 序列化期间,哪些变量未序列化? +static静态变量和transient 修饰的字段是不会被序列化的。静态(static)成员变量是属于类级别的,而序列化是针对对象的。transient关键字修字段饰,可以阻止该字段被序列化到文件中。 + +### 参考与感谢 +- [Java基础学习总结——Java对象的序列化和反序列化](https://www.cnblogs.com/xdp-gacl/p/3777987.html) +- [10个艰难的Java面试题与答案](https://segmentfault.com/a/1190000019962661) + +### 个人公众号 + + +![](https://user-gold-cdn.xitu.io/2020/5/12/172066fcb54c1643?w=900&h=500&f=png&s=133410) +- 觉得写得好的小伙伴给个点赞+关注啦,谢谢~ +- 如果有写得不正确的地方,麻烦指出,感激不尽。 +- 同时非常期待小伙伴们能够关注我公众号,后面慢慢推出更好的干货~嘻嘻 +- - github地址:https://github.com/whx123/JavaHome \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200\345\255\246\344\271\240/jstack\345\221\275\344\273\244\350\247\243\346\236\220.md" "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/jstack\345\221\275\344\273\244\350\247\243\346\236\220.md" new file mode 100644 index 0000000..03244e4 --- /dev/null +++ "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/jstack\345\221\275\344\273\244\350\247\243\346\236\220.md" @@ -0,0 +1,280 @@ +## 前言 +如果有一天,你的Java程序长时间停顿,也许是它病了,需要用jstack拍个片子分析分析,才能诊断具体什么病症,是死锁综合征,还是死循环等其他病症,本文我们一起来学习jstack命令~ + +- jstack 的功能 +- jstack用法 +- 线程状态等基础回顾 +- 实战案例1:jstack 分析死锁 +- 实战案例2:jstack 分析CPU 过高 + +## jstack 的功能 +jstack是JVM自带的Java堆栈跟踪工具,它用于打印出给定的java进程ID、core file、远程调试服务的Java堆栈信息. + +``` +jstack prints Java stack traces of Java threads for a given Java process or +core file or a remote debug server. +``` + +> - jstack命令用于生成虚拟机当前时刻的线程快照。 +> - 线程快照是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因, +如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。 +> - 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 +> - 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。 +> - 另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。 + + +## jstack用法 +**jstack 命令格式如下** + +``` +jstack [ option ] pid +jstack [ option ] executable core +jstack [ option ] [server-id@]remote-hostname-or-IP +``` +- executable Java executable from which the core dump was produced.(可能是产生core dump的java可执行程序) +- core 将被打印信息的core dump文件 +- remote-hostname-or-IP 远程debug服务的主机名或ip +- server-id 唯一id,假如一台主机上多个远程debug服务 + +**最常用的是** +``` +jstack [option] // 打印某个进程的堆栈信息 +``` +**option参数说明如下:** + +![](https://user-gold-cdn.xitu.io/2020/5/8/171efde05078a6bb?w=874&h=298&f=png&s=125941) + +| 选项 | 作用 | +|-----|-----| +| -F | 当正常输出的请求不被响应时,强制输出线程堆栈 | +| -m | 如果调用到本地方法的话,可以显示C/C++的堆栈 | +| -l | 除堆栈外,显示关于锁的附加信息,在发生死锁时可以用jstack -l pid来观察锁持有情况 | + +## 线程状态等基础回顾 +### 线程状态简介 +jstack用于生成线程快照的,我们分析线程的情况,需要复习一下线程状态吧,拿小凳子坐好,复习一下啦~ +![](https://user-gold-cdn.xitu.io/2020/5/3/171d9db3c2b90ad3?w=1280&h=617&f=png&s=254427) + +**Java语言定义了6种线程池状态:** +- New:创建后尚未启动的线程处于这种状态,不会出现在Dump中。 +- RUNNABLE:包括Running和Ready。线程开启start()方法,会进入该状态,在虚拟机内执行的。 +- Waiting:无限的等待另一个线程的特定操作。 +- Timed Waiting:有时限的等待另一个线程的特定操作。 +- 阻塞(Blocked):在程序等待进入同步区域的时候,线程将进入这种状态,在等待监视器锁。 +- 结束(Terminated):已终止线程的线程状态,线程已经结束执行。 + +**Dump文件的线程状态一般其实就以下3种:** +- RUNNABLE,线程处于执行中 +- BLOCKED,线程被阻塞 +- WAITING,线程正在等待 + +### Monitor 监视锁 +因为Java程序一般都是多线程运行的,Java多线程跟监视锁环环相扣,所以我们分析线程状态时,也需要回顾一下Monitor监视锁知识。 + +有关于线程同步关键字Synchronized与监视锁的爱恨情仇,有兴趣的伙伴可以看一下我这篇文章 +[Synchronized解析——如果你愿意一层一层剥开我的心](https://juejin.im/post/5d5374076fb9a06ac76da894#heading-18) + +Monitor的工作原理图如下: + +![](https://user-gold-cdn.xitu.io/2020/5/10/171fe505a1154da0?w=1280&h=765&f=png&s=265369) +- 线程想要获取monitor,首先会进入Entry Set队列,它是Waiting Thread,线程状态是Waiting for monitor entry。 +- 当某个线程成功获取对象的monitor后,进入Owner区域,它就是Active Thread。 +- 如果线程调用了wait()方法,则会进入Wait Set队列,它会释放monitor锁,它也是Waiting Thread,线程状态in Object.wait() +- 如果其他线程调用 notify() / notifyAll() ,会唤醒Wait Set中的某个线程,该线程再次尝试获取monitor锁,成功即进入Owner区域。 + +### Dump 文件分析关注重点 + +- runnable,线程处于执行中 +- deadlock,死锁(重点关注) +- blocked,线程被阻塞 (重点关注) +- Parked,停止 +- locked,对象加锁 +- waiting,线程正在等待 +- waiting to lock 等待上锁 +- Object.wait(),对象等待中 +- waiting for monitor entry 等待获取监视器(重点关注) +- Waiting on condition,等待资源(重点关注),最常见的情况是线程在等待网络的读写 + +## 实战案例1:jstack 分析死锁问题 + +- 什么是死锁? +- 如何用jstack排查死锁? + +### 什么是死锁? +![](https://user-gold-cdn.xitu.io/2020/5/10/171fdedc6f195055?w=731&h=730&f=png&s=75164) + +死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法进行下去。 + + +### 如何用如何用jstack排查死锁问题 +先来看一段会产生死锁的Java程序,源码如下: +``` +/** + * Java 死锁demo + */ +public class DeathLockTest { + private static Lock lock1 = new ReentrantLock(); + private static Lock lock2 = new ReentrantLock(); + + public static void deathLock() { + Thread t1 = new Thread() { + @Override + public void run() { + try { + lock1.lock(); + System.out.println(Thread.currentThread().getName() + " get the lock1"); + Thread.sleep(1000); + lock2.lock(); + System.out.println(Thread.currentThread().getName() + " get the lock2"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + Thread t2 = new Thread() { + @Override + public void run() { + try { + lock2.lock(); + System.out.println(Thread.currentThread().getName() + " get the lock2"); + Thread.sleep(1000); + lock1.lock(); + System.out.println(Thread.currentThread().getName() + " get the lock1"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + //设置线程名字,方便分析堆栈信息 + t1.setName("mythread-jay"); + t2.setName("mythread-tianluo"); + t1.start(); + t2.start(); + } + public static void main(String[] args) { + deathLock(); + } +} +``` +运行结果: +![](https://user-gold-cdn.xitu.io/2020/5/10/171fdbeb51a543c1?w=795&h=209&f=png&s=24069) +显然,线程jay和线程tianluo都是只执行到一半,就陷入了阻塞等待状态~ + +### jstack排查Java死锁步骤 +- 在终端中输入jsp查看当前运行的java程序 +- 使用 jstack -l pid 查看线程堆栈信息 +- 分析堆栈信息 + +### 在终端中输入jsp查看当前运行的java程序 + +![](https://user-gold-cdn.xitu.io/2020/5/10/171fdc5ad1be5bf0?w=877&h=171&f=png&s=21046) +通过使用 jps 命令获取需要监控的进程的pid,我们找到了```23780 DeathLockTest``` + +### 使用 jstack -l pid 查看线程堆栈信息 +![](https://user-gold-cdn.xitu.io/2020/5/10/171fddacc8bd2aa9?w=1177&h=181&f=png&s=32290) +由上图,可以清晰看到**死锁**信息: +- mythread-tianluo 等待这个锁 “0x00000000d61ae3a0”,这个锁是由于mythread-jay线程持有。 +- mythread-jay线程等待这个锁“0x00000000d61ae3d0”,这个锁是由mythread-tianluo 线程持有。 + +### 还原死锁真相 +![](https://user-gold-cdn.xitu.io/2020/5/10/171fdd465657c327?w=1312&h=625&f=png&s=143955) +**“mythread-tianluo"线程堆栈信息分析如下:** +- mythread-tianluo的线程处于等待(waiting)状态,持有“0x00000000d61ae3d0”锁,等待“0x00000000d61ae3a0”的锁 + +**“mythread-jay"线程堆栈信息分析如下:** +- mythread-tianluo的线程处于等待(waiting)状态,持有“0x00000000d61ae3a0”锁,等待“0x00000000d61ae3d0”的锁 + +![](https://user-gold-cdn.xitu.io/2020/5/10/171febb0f72f1efb?w=604&h=602&f=png&s=67435) + + +## 实战案例2:jstack 分析CPU过高问题 +来个导致CPU过高的demo程序,一个死循环,哈哈~ +``` +/** + * 有个导致CPU过高程序的demo,死循环 + */ +public class JstackCase { + + private static ExecutorService executorService = Executors.newFixedThreadPool(5); + + public static void main(String[] args) { + + Task task1 = new Task(); + Task task2 = new Task(); + executorService.execute(task1); + executorService.execute(task2); + } + + public static Object lock = new Object(); + + static class Task implements Runnable{ + + public void run() { + synchronized (lock){ + long sum = 0L; + while (true){ + sum += 1; + } + } + } + } +} +``` + +### jstack 分析CPU过高步骤 +- 1. top +- 2. top -Hp pid +- 3. jstack pid +- 4. jstack -l [PID] >/tmp/log.txt +- 5. 分析堆栈信息 + +### 1.top + +在服务器上,我们可以通过top命令查看各个进程的cpu使用情况,它默认是按cpu使用率由高到低排序的 +![](https://user-gold-cdn.xitu.io/2020/5/10/171fcebd6b26a42a?w=793&h=532&f=png&s=319420) +由上图中,我们可以找出pid为21340的java进程,它占用了最高的cpu资源,凶手就是它,哈哈! + +### 2. top -Hp pid + +通过top -Hp 21340可以查看该进程下,各个线程的cpu使用情况,如下: +![](https://user-gold-cdn.xitu.io/2020/5/10/171fcf02e458cc8d?w=842&h=219&f=png&s=145135) +可以发现pid为21350的线程,CPU资源占用最高~,嘻嘻,小本本把它记下来,接下来拿jstack给它拍片子~ + +### 3. jstack pid + +通过top命令定位到cpu占用率较高的线程之后,接着使用jstack pid命令来查看当前java进程的堆栈状态,```jstack 21350```后,内容如下: +![](https://user-gold-cdn.xitu.io/2020/5/10/171fcf603e5adf63?w=1114&h=409&f=png&s=278796) + + + +### 4. jstack -l [PID] >/tmp/log.txt + +其实,前3个步骤,堆栈信息已经出来啦。但是一般在生成环境,我们可以把这些堆栈信息打到一个文件里,再回头仔细分析哦~ + +### 5. 分析堆栈信息 + +我们把占用cpu资源较高的线程pid(本例子是21350),将该pid转成16进制的值 + +![](https://user-gold-cdn.xitu.io/2020/5/10/171fd4e1e7da64e4?w=903&h=280&f=png&s=22071) +在thread dump中,每个线程都有一个nid,我们找到对应的nid(5366),发现一直在跑(24行) + +![](https://user-gold-cdn.xitu.io/2020/5/10/171fd50679c83f44?w=1114&h=409&f=png&s=278796) + +这个时候,可以去检查代码是否有问题啦~ 当然,也建议隔段时间再执行一次stack命令,再一份获取thread dump,毕竟两次拍片结果(jstack)对比,更准确嘛~ + +## 参考与感谢 +- [jvm 性能调优工具之 jstack](https://www.jianshu.com/p/025cb069cb69) +- [如何使用jstack分析线程状态](https://www.jianshu.com/p/6690f7e92f27) +- [Java命令学习系列(二)——Jstack](http://www.hollischuang.com/archives/110) + +## 个人公众号 + + + +![](https://user-gold-cdn.xitu.io/2020/5/12/1720639baa43485e?w=900&h=500&f=png&s=133410) +- 觉得写得好的小伙伴给个点赞+关注啦,谢谢~ +- 如果有写得不正确的地方,麻烦指出,感激不尽。 +- 同时非常期待小伙伴们能够关注我公众号,后面慢慢推出更好的干货~嘻嘻 +- github地址:https://github.com/whx123/JavaHome + + diff --git "a/Java\345\237\272\347\241\200\345\255\246\344\271\240/\346\216\214\346\217\241Java\346\236\232\344\270\276\350\277\231\345\207\240\344\270\252\347\237\245\350\257\206\347\202\271\357\274\214\346\227\245\345\270\270\345\274\200\345\217\221\345\260\261\345\244\237\345\225\246.md" "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/\346\216\214\346\217\241Java\346\236\232\344\270\276\350\277\231\345\207\240\344\270\252\347\237\245\350\257\206\347\202\271\357\274\214\346\227\245\345\270\270\345\274\200\345\217\221\345\260\261\345\244\237\345\225\246.md" new file mode 100644 index 0000000..cfe80ef --- /dev/null +++ "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/\346\216\214\346\217\241Java\346\236\232\344\270\276\350\277\231\345\207\240\344\270\252\347\237\245\350\257\206\347\202\271\357\274\214\346\227\245\345\270\270\345\274\200\345\217\221\345\260\261\345\244\237\345\225\246.md" @@ -0,0 +1,510 @@ +### 前言 +春节来临之际,祝大家新年快乐。整理了Java枚举的相关知识,算是比较基础的,希望大家一起学习进步。 +![](https://user-gold-cdn.xitu.io/2020/1/23/16fd1949a15e2788?w=1223&h=508&f=png&s=108187) + + +> [本章节所有代码demo已上传github](https://github.com/whx123/EnumTest) + +### 一、枚举类型是什么? +JDK5引入了一种新特性,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这就是枚举类型。 + +一个枚举的简单例子 + +``` +enum SeasonEnum { + SPRING,SUMMER,FALL,WINTER; +} +``` +### 二、 枚举类的常用方法 +Enum常用方法有以下几种: +- name(); 返回enum实例声明时的名字。 +- ordinal(); 返回一个int值,表示enum实例在声明的次序。 +- equals(); 返回布尔值,enum实例判断相等 +- compareTo() 比较enum实例与指定对象的顺序 +- values(); 返回enum实例的数组 +- valueOf(String name) 由名称获取枚举类中定义的常量 + +直接看例子吧: +``` +enum Shrubbery { + GROUND,CRAWLING, HANGING +} +public class EnumClassTest { + public static void main(String[] args) { + //values 返回enum实例的数组 + for (Shrubbery temp : Shrubbery.values()) { + // name 返回实例enum声明的名字 + System.out.println(temp.name() + " ordinal is " + temp.ordinal() + " ,equal result is " + + Shrubbery.CRAWLING.equals(temp) + ",compare result is " + Shrubbery.CRAWLING.compareTo(temp)); + } + //由名称获取枚举类中定义的常量值 + System.out.println(Shrubbery.valueOf("CRAWLING")); + } +} + +``` +运行结果: + +``` +GROUND ordinal is 0 ,equal result is false,compare result is 1 +CRAWLING ordinal is 1 ,equal result is true,compare result is 0 +HANGING ordinal is 2 ,equal result is false,compare result is -1 +CRAWLING +``` + + +### 三、枚举类的真面目 +枚举类型到底是什么类呢?我们新建一个简单枚举例子,看看它的庐山真面目。如下: +``` +public enum Shrubbery { + GROUND,CRAWLING, HANGING +} +``` +使用javac编译上面的枚举类,可得Shrubbery.class文件。 + +``` +javac Shrubbery.java +``` +再用javap命令,反编译得到字节码文件。如:执行```javap Shrubbery.class```可到以下字节码文件。 +``` +Compiled from "Shrubbery.java" +public final class enumtest.Shrubbery extends java.lang.Enum { + public static final enumtest.Shrubbery GROUND; + public static final enumtest.Shrubbery CRAWLING; + public static final enumtest.Shrubbery HANGING; + public static enumtest.Shrubbery[] values(); + public static enumtest.Shrubbery valueOf(java.lang.String); + static {}; +} +``` +从字节码文件可以发现: +- Shrubbery枚举变成了一个final修饰的类,也就是说,它不能被继承啦。 +- Shrubbery是java.lang.Enum的子类。 +- Shrubbery定义的枚举值都是public static final修饰的,即都是静态常量。 + +为了看得更仔细,javap反编译加多个参数-c,执行如下命令: + +``` +javap -c Shrubbery.class +``` +静态代码块的字节码文件如下: +``` + static {}; + Code: + 0: new #4 // class enumtest/Shrubbery + 3: dup + 4: ldc #7 // String GROUND + 6: iconst_0 + 7: invokespecial #8 // Method "":(Ljava/lang/String;I)V + 10: putstatic #9 // Field GROUND:Lenumtest/Shrubbery; + 13: new #4 // class enumtest/Shrubbery + 16: dup + 17: ldc #10 // String CRAWLING + 19: iconst_1 + 20: invokespecial #8 // Method "":(Ljava/lang/String;I)V + 23: putstatic #11 // Field CRAWLING:Lenumtest/Shrubbery; + 26: new #4 // class enumtest/Shrubbery + 29: dup + 30: ldc #12 // String HANGING + 32: iconst_2 + 33: invokespecial #8 // Method "":(Ljava/lang/String;I)V + 36: putstatic #13 // Field HANGING:Lenumtest/Shrubbery; + 39: iconst_3 + 40: anewarray #4 // class enumtest/Shrubbery + 43: dup + 44: iconst_0 + 45: getstatic #9 // Field GROUND:Lenumtest/Shrubbery; + 48: aastore + 49: dup + 50: iconst_1 + 51: getstatic #11 // Field CRAWLING:Lenumtest/Shrubbery; + 54: aastore + 55: dup + 56: iconst_2 + 57: getstatic #13 // Field HANGING:Lenumtest/Shrubbery; + 60: aastore + 61: putstatic #1 // Field $VALUES:[Lenumtest/Shrubbery; + 64: return +} +``` +- 0-39行实例化了Shrubbery枚举类的GROUND,CRAWLING, HANGING; +- 40-64为创建Shrubbery[]数组$VALUES,并将上面的三个实例化对象放入数组的操作。 +- 因此,枚举类方法values()返回enum枚举实例的数组是不是豁然开朗啦。 + +### 四、枚举类的优点 +枚举类有什么优点呢?就是我们为什么要选择使用枚举类呢?因为它可以增强**代码的可读性,可维护性**,同时,它也具有**安全**性。 + +#### 枚举类可以增强可读性、可维护性 +假设现在有这样的业务场景:订单完成后,通知买家评论。很容易有以下代码: +``` +//订单已完成 +if(3==orderStatus){ +//do something +} +``` +很显然,这段代码出现了魔法数,如果你没写注释,谁知道3表示订单什么状态呢,不仅阅读起来比较困难,维护起来也很蛋疼?如果使用**枚举类**呢,如下: +``` +public enum OrderStatusEnum { + UNPAID(0, "未付款"), PAID(1, "已付款"), SEND(2, "已发货"), FINISH(3, "已完成"),; + + private int index; + + private String desc; + + public int getIndex() { + return index; + } + + public String getDesc() { + return desc; + } + + OrderStatusEnum(int index, String desc) { + this.index = index; + this.desc = desc; + } +} + + //订单已完成 + if(OrderStatusEnum.FINISH.getIndex()==orderStatus){ + //do something + } +``` +可见,枚举类让这段代码可读性更强,也比较好维护,后面加个新的订单状态,直接添加多一种枚举状态就可以了。有些朋友认为,```public static final int```这种静态常量也可以实现该功能呀,如下: + +``` +public class OrderStatus { + //未付款 + public static final int UNPAID = 0; + public static final int PAID = 1; + public static final int SENDED = 2; + public static final int FINISH = 3; + +} + + //订单已完成 + if(OrderStatus.FINISH==orderStatus){ + //do something + } +``` +当然,静态常量这种方式实现,可读性是没有任何问题的,日常工作中代码这样写也无可厚非。但是,定义int值相同的变量,容易混淆,如你定义```PAID```和```SENDED```状态都是2,编译器是不会报错的。 + +因此,枚举类第一个优点就是**可读性,可维护性都不错**,所以推荐。 + + +#### 枚举类安全性 +除了可读性、可维护性外,枚举类还有个巨大的优点,就是安全性。 + +从上一节枚举类字节码分析,我们知道: +- 一个枚举类是被final关键字修饰的,不能被继承。 +- 并且它的变量都是public static final修饰的,都是静态变量。 + +当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。 + +### 五、枚举的常见用法 +#### enum组织常量 +在JDK5之前,常量定义都是这样,先定义一个类或者接口,属性类型都是public static final...,有了枚举之后,可以把常量组织到枚举类了,如下: + +``` +enum SeasonEnum { + SPRING,SUMMER,FALL,WINTER,; +} +``` +#### enum与switch 环环相扣 +一般来说,switch-case中只能使用整数值,但是枚举实例天生就具备整数值的次序,因此,在switch语句中是可以使用enum的,如下: +``` +enum OrderStatusEnum { + UNPAID, PAID, SEND, FINISH +} +public class OrderStatusTest { + public static void main(String[] args) { + changeByOrderStatus(OrderStatusEnum.FINISH); + } + + static void changeByOrderStatus(OrderStatusEnum orderStatusEnum) { + switch (orderStatusEnum) { + case UNPAID: + System.out.println("老板,你下单了,赶紧付钱吧"); + break; + case PAID: + System.out.println("我已经付钱啦"); + break; + case SENDED: + System.out.println("已发货"); + break; + case FINISH: + System.out.println("订单完成啦"); + break; + } + } +} + +``` +在日常开发中,enum与switch一起使用,会让你的代码可读性更好哦。 + +#### 向枚举中添加新的方法 +可以向枚举类添加新方法的,如get方法,普通方法等,以下是日常工作最常用的一种枚举写法: +``` +public enum OrderStatusEnum { + UNPAID(0, "未付款"), PAID(1, "已付款"), SENDED(2, "已发货"), FINISH(3, "已完成"),; + + //成员变量 + private int index; + private String desc; + + //get方法 + public int getIndex() { + return index; + } + + public String getDesc() { + return desc; + } + + //构造器方法 + OrderStatusEnum(int index, String desc) { + this.index = index; + this.desc = desc; + } + + //普通方法 + public static OrderStatusEnum of(int index){ + for (OrderStatusEnum temp : values()) { + if (temp.getIndex() == index) { + return temp; + } + } + return null; + } +} + +``` + +#### 枚举实现接口 +所有枚举类都继承于java.lang.Enum,所以枚举不能再继承其他类了。但是枚举可以实现接口呀,这给枚举增添了不少色彩。如下: + +``` +public interface ISeasonBehaviour { + + void showSeasonBeauty(); + + String getSeasonName(); +} + +public enum SeasonEnum implements ISeasonBehaviour { + SPRING(1,"春天"),SUMMER(2,"夏天"),FALL(3,"秋天"),WINTER(4,"冬天"), + ; + + private int index; + private String name; + + SeasonEnum(int index, String name) { + this.index = index; + this.name = name; + } + + public int getIndex() { + return index; + } + public String getName() { + return name; + } + + //接口方法 + @Override + public void showSeasonBeauty() { + System.out.println("welcome to " + this.name); + } + + //接口方法 + @Override + public String getSeasonName() { + return this.name; + } +} +``` + +#### 使用接口组织枚举 +``` +public interface Food { + enum Coffee implements Food{ + BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO + } + enum Dessert implements Food{ + FRUIT, CAKE, GELATO + } +} +``` + +### 六、枚举类比较是用==还是equals? +先看一个例子,如下: +``` +public class EnumTest { + public static void main(String[] args) { + + Shrubbery s1 = Shrubbery.CRAWLING; + Shrubbery s2 = Shrubbery.GROUND; + Shrubbery s3 = Shrubbery.CRAWLING; + + System.out.println("s1==s2,result: " + (s1 == s2)); + System.out.println("s1==s3,result: " + (s1 == s3)); + System.out.println("Shrubbery.CRAWLING.equals(s1),result: "+Shrubbery.CRAWLING.equals(s1)); + System.out.println("Shrubbery.CRAWLING.equals(s2),result: "+Shrubbery.CRAWLING.equals(s2)); + + } +} +``` +运行结果: + +``` +s1==s2,result: false +s1==s3,result: true +Shrubbery.CRAWLING.equals(s1),result: true +Shrubbery.CRAWLING.equals(s2),result: false +``` + +可以发现不管用==还是equals,都是可以的。其实枚举的equals方法,就是用==比较的,如下: + +``` +public final boolean equals(Object other) { + return this==other; +} +``` + +### 七、枚举实现的单例 +effective java提过,**最佳的单例实现模式就是枚举模式**。单例模式的实现有好几种方式,为什么是枚举实现的方式最佳呢? + +因为枚举实现的单例有以下优点: +- 枚举单例写法简单 +- 枚举可解决线程安全问题 +- 枚举可解决反序列化会破坏单例的问题 + +一个枚举单例demo如下: +``` +public class SingletonEnumTest { + public enum SingletonEnum { + INSTANCE,; + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + public static void main(String[] args) { + SingletonEnum.INSTANCE.setName("jay@huaxiao"); + System.out.println(SingletonEnum.INSTANCE.getName()); + } +} +``` + +有关于枚举实现单例,想深入了解的朋友可以看Hollis大神这篇文章,写得真心好![为什么我墙裂建议大家使用枚举来实现单例。](https://juejin.im/post/5b285d236fb9a00e9b39fdd2#heading-0) + + +### 八、EnumSet 和EnumMap + +#### EnumSet + +先来看看EnumSet的继承体系图 + +![](https://user-gold-cdn.xitu.io/2020/1/23/16fcfec7f74422dc?w=798&h=495&f=png&s=38785) + +显然,EnumSet也实现了set接口,相比于HashSet,它有以下优点: +- 消耗较少的内存 +- 效率更高,因为是位向量实现的。 +- 可以预测的遍历顺序(enum常量的声明顺序) +- 拒绝加null + +**EnumSet就是set的高性能实现,它的要求就是存放必须是同一枚举类型。** EnumSet的常用方法: +- allof() 创建一个包含指定枚举类里所有枚举值的EnumSet集合 +- range() 获取某个范围的枚举实例 +- of() 创建一个包括参数中所有枚举元素的EnumSet集合 +- complementOf() 初始枚举集合包括指定枚举集合的补集 + +看实例,最实际: +``` +public class EnumTest { + public static void main(String[] args) { + // Creating a set + EnumSet set1, set2, set3, set4; + + // Adding elements + set1 = EnumSet.of(SeasonEnum.SPRING, SeasonEnum.FALL, SeasonEnum.WINTER); + set2 = EnumSet.complementOf(set1); + set3 = EnumSet.allOf(SeasonEnum.class); + set4 = EnumSet.range(SeasonEnum.SUMMER,SeasonEnum.WINTER); + System.out.println("Set 1: " + set1); + System.out.println("Set 2: " + set2); + System.out.println("Set 3: " + set3); + System.out.println("Set 4: " + set4); + } +} +``` +输出结果: + +``` +Set 1: [SPRING, FALL, WINTER] +Set 2: [SUMMER] +Set 3: [SPRING, SUMMER, FALL, WINTER] +Set 4: [SUMMER, FALL, WINTER] +``` + +#### EnumMap +EnumMap的继承体系图如下: + +![](https://user-gold-cdn.xitu.io/2020/1/23/16fd0815ebfd3f03?w=689&h=332&f=png&s=23426) + +EnumMap也实现了Map接口,相对于HashMap,它也有这些优点: +- 消耗较少的内存 +- 效率更高 +- 可以预测的遍历顺序 +- 拒绝null + +**EnumMap就是map的高性能实现。** 它的常用方法跟HashMap是一致的,唯一约束是枚举相关。 + +看实例,最实际: +``` +public class EnumTest { + public static void main(String[] args) { + Map map = new EnumMap<>(SeasonEnum.class); + map.put(SeasonEnum.SPRING, "春天"); + map.put(SeasonEnum.SUMMER, "夏天"); + map.put(SeasonEnum.FALL, "秋天"); + map.put(SeasonEnum.WINTER, "冬天"); + System.out.println(map); + System.out.println(map.get(SeasonEnum.SPRING)); + } +} +``` +运行结果 + +``` +{SPRING=春天, SUMMER=夏天, FALL=秋天, WINTER=冬天} +春天 +``` + +### 九、待更新 +有关于枚举关键知识点,亲爱的朋友,你有没有要补充的呢? + +### 参考与感谢 +- [关于Java中枚举Enum的深入剖析](https://droidyue.com/blog/2016/11/29/dive-into-enum/) +- [深度分析Java的枚举类型—-枚举的线程安全性及序列化问题](http://www.hollischuang.com/archives/197) +- [为什么我墙裂建议大家使用枚举来实现单例。](https://juejin.im/post/5b285d236fb9a00e9b39fdd2#heading-3) +- [深入理解Java枚举类型(enum)](https://blog.csdn.net/javazejian/article/details/71333103#enumset%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E5%89%96%E6%9E%90) +- [深入理解 Java 枚举](https://juejin.im/post/5c90efacf265da60ce379e17#heading-16) +- [EnumSet in Java](https://www.geeksforgeeks.org/enumset-class-java/) +- [Java 枚举(enum) 详解7种常见的用法](https://blog.csdn.net/qq_27093465/article/details/52180865) +- 《Java编程思想》 + +### 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。 +- 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。 + + diff --git "a/Java\345\237\272\347\241\200\345\255\246\344\271\240/\346\234\211\345\205\263\344\272\216Java Map\357\274\214\345\272\224\350\257\245\346\216\214\346\217\241\347\232\2048\344\270\252\351\227\256\351\242\230.md" "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/\346\234\211\345\205\263\344\272\216Java Map\357\274\214\345\272\224\350\257\245\346\216\214\346\217\241\347\232\2048\344\270\252\351\227\256\351\242\230.md" new file mode 100644 index 0000000..b208fa6 --- /dev/null +++ "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/\346\234\211\345\205\263\344\272\216Java Map\357\274\214\345\272\224\350\257\245\346\216\214\346\217\241\347\232\2048\344\270\252\351\227\256\351\242\230.md" @@ -0,0 +1,514 @@ +### 前言 +最近几天看了几篇有关于Java Map的外国博文,写得非常不错,所以整理了Java map 应该掌握的8个问题,都是日常开发司空见惯的问题,希望对大家有帮助;如果有不正确的地方,欢迎提出,万分感谢哈~ + +> [本章节所有代码demo已上传github](https://github.com/whx123/MapTest) + +### 1、如何把一个Map转化为List +日常开发中,我们经常遇到这种场景,把一个Map转化为List。map转List有以下三种转化方式: +- 把map的键key转化为list +- 把map的值value转化为list +- 把map的键值key-value转化为list + +**伪代码如下:** +``` +// key list +List keyList = new ArrayList(map.keySet()); +// value list +List valueList = new ArrayList(map.values()); +// key-value list +List entryList = new ArrayList(map.entrySet()); +``` +**示例代码:** +``` +public class Test { + public static void main(String[] args) { + Map map = new HashMap<>(); + map.put(2, "jay"); + map.put(1, "whx"); + map.put(3, "huaxiao"); + //把一个map的键转化为list + List keyList = new ArrayList<>(map.keySet()); + System.out.println(keyList); + //把map的值转化为list + List valueList = new ArrayList<>(map.values()); + System.out.println(valueList); + 把map的键值转化为list + List entryList = new ArrayList(map.entrySet()); + System.out.println(entryList); + + } +} +``` +运行结果: + +``` +[1, 2, 3] +[whx, jay, huaxiao] +[1=whx, 2=jay, 3=huaxiao] +``` + +### 2、如何遍历一个Map +我们经常需要遍历一个map,可以有以下两种方式实现: + +#### 通过entrySet+for实现遍历 +``` +for(Entry entry: map.entrySet()) { + // get key + K key = entry.getKey(); + // get value + V value = entry.getValue(); +} +``` +**实例代码:** + +``` +public class EntryMapTest { + public static void main(String[] args) { + Map map = new HashMap<>(); + map.put(2, "jay"); + map.put(1, "whx"); + map.put(3, "huaxiao"); + + for(Map.Entry entry: map.entrySet()) { + // get key + Integer key = (Integer) entry.getKey(); + // get value + String value = (String) entry.getValue(); + + System.out.println("key:"+key+",value:"+value); + } + } +} +``` +#### 通过Iterator+while实现遍历 +``` +Iterator itr = map.entrySet().iterator(); +while(itr.hasNext()) { + Entry entry = itr.next(); + // get key + K key = entry.getKey(); + // get value + V value = entry.getValue(); +} +``` +**实例代码:** + +``` +public class IteratorMapTest { + public static void main(String[] args) { + Map map = new HashMap<>(); + map.put(2, "jay"); + map.put(1, "whx"); + map.put(3, "huaxiao"); + + Iterator itr = map.entrySet().iterator(); + while(itr.hasNext()) { + Map.Entry entry = (Map.Entry) itr.next(); + // get key + Integer key = (Integer) entry.getKey(); + // get value + String value = (String) entry.getValue(); + + System.out.println("key:"+key+",value:"+value); + } + } +} +``` +**运行结果:** + +``` +key:1,value:whx +key:2,value:jay +key:3,value:huaxiao +``` +### 3、如何根据Map的keys进行排序 +对Map的keys进行排序,在日常开发很常见,主要有以下两种方式实现。 +#### 把Map.Entry放进list,再用Comparator对list进行排序 +``` +List list = new ArrayList(map.entrySet()); +Collections.sort(list, (Entry e1, Entry e2)-> { + return e1.getKey().compareTo(e2.getKey()); +}); +``` +**实例代码:** +``` +public class SortKeysMapTest { + public static void main(String[] args) { + Map map = new HashMap<>(); + map.put("2010", "jay"); + map.put("1999", "whx"); + map.put("3010", "huaxiao"); + + List> list = new ArrayList<>(map.entrySet()); + Collections.sort(list, (Map.Entry e1, Map.Entry e2)-> { + return e1.getKey().toString().compareTo(e2.getKey().toString()); + }); + + for (Map.Entry entry : list) { + System.out.println("key:" + entry.getKey() + ",value:" + entry.getValue()); + } + + } +} +``` + +#### 使用SortedMap+TreeMap+Comparator实现 +``` +SortedMap sortedMap = new TreeMap(new Comparator() { + @Override + public int compare(K k1, K k2) { + return k1.compareTo(k2); + } +}); +sortedMap.putAll(map); +``` +**实例代码:** + +``` +public class SortKeys2MapTest { + public static void main(String[] args) { + Map map = new HashMap<>(); + map.put("2010", "jay"); + map.put("1999", "whx"); + map.put("3010", "huaxiao"); + + SortedMap sortedMap = new TreeMap(new Comparator() { + @Override + public int compare(String k1, String k2) { + return k1.compareTo(k2); + } + }); + sortedMap.putAll(map); + + Iterator itr = sortedMap.entrySet().iterator(); + while(itr.hasNext()) { + Map.Entry entry = (Map.Entry) itr.next(); + // get key + String key = (String) entry.getKey(); + // get value + String value = (String) entry.getValue(); + + System.out.println("key:"+key+",value:"+value); + } + } +} + +``` +**运行结果:** + +``` +key:1999,value:whx +key:2010,value:jay +key:3010,value:huaxiao +``` + +### 4、如何对Map的values进行排序 + +``` +List list = new ArrayList(map.entrySet()); +Collections.sort(list, (Entry e1, Entry e2) ->{ + return e1.getValue().compareTo(e2.getValue()); + }); +``` + +**实例代码:** +``` +public class SortValuesMapTest { + public static void main(String[] args) { + Map map = new HashMap<>(); + map.put("2010", "jay"); + map.put("1999", "whx"); + map.put("3010", "huaxiao"); + + List >list = new ArrayList<>(map.entrySet()); + Collections.sort(list, (Map.Entry e1, Map.Entry e2)-> { + return e1.getValue().toString().compareTo(e2.getValue().toString()); + } + ); + + for (Map.Entry entry : list) { + System.out.println("key:" + entry.getKey() + ",value:" + entry.getValue()); + } + } +} +``` +运行结果: + +``` +key:3010,value:huaxiao +key:2010,value:jay +key:1999,value:whx +``` + +### 5、如何初始化一个静态/不可变的Map +初始化一个静态不可变的map,单单static final+static代码还是不行的,如下: +``` +public class Test1 { + private static final Map map; + static { + map = new HashMap(); + map.put(1, "one"); + map.put(2, "two"); + } + public static void main(String[] args) { + map.put(3, "three"); + Iterator itr = map.entrySet().iterator(); + while(itr.hasNext()) { + Map.Entry entry = (Map.Entry) itr.next(); + // get key + Integer key = (Integer) entry.getKey(); + // get value + String value = (String) entry.getValue(); + + System.out.println("key:"+key+",value:"+value); + } + } +} +``` +这里面,map继续添加元素(3,"three"),发现是OK的,运行结果如下: +``` +key:1,value:one +key:2,value:two +key:3,value:three +``` +真正实现一个静态不可变的map,需要Collections.unmodifiableMap,代码如下: + +``` +public class Test2 { + private static final Map map; + static { + Map aMap = new HashMap<>(); + aMap.put(1, "one"); + aMap.put(2, "two"); + map = Collections.unmodifiableMap(aMap); + } + + public static void main(String[] args) { + map.put(3, "3"); + Iterator itr = map.entrySet().iterator(); + while(itr.hasNext()) { + Map.Entry entry = (Map.Entry) itr.next(); + // get key + Integer key = (Integer) entry.getKey(); + // get value + String value = (String) entry.getValue(); + + System.out.println("key:"+key+",value:"+value); + } + } + +} +``` +运行结果如下: + +![](https://user-gold-cdn.xitu.io/2020/2/3/1700b88b92e849c2?w=901&h=255&f=png&s=30907) + +可以发现,继续往map添加元素是会报错的,实现真正不可变的map。 + +### 6、HashMap, TreeMap, and Hashtable,ConcurrentHashMap的区别 +| | HashMap| TreeMap | Hashtable| ConcurrentHashMap | +| ------ | ------ | ------ |-------- |----------- | +| 有序性 | 否 | 是 | 否| 否 | +| null k-v | 是-是 | 否-是|否-否|否-否| +| 线性安全 | 否 | 否 |是 | 是 | +| 时间复杂度 | O(1) | O(log n) | O(1) | O(log n)| +| 底层结构 | 数组+链表+红黑树 | 红黑树 |数组+链表| 数组+链表+红黑树| + + +### 7、如何创建一个空map +如果map是不可变的,可以这样创建: + +``` +Map map=Collections.emptyMap(); +or +Map map=Collections.emptyMap(); +//map1.put("1", "1"); 运行出错 +``` +如果你希望你的空map可以添加元素的,可以这样创建 + +``` +Map map = new HashMap(); +``` + +### 8、有关于map的复制 +有关于hashmap的复制,在日常开发中,使用也比较多。主要有```=,clone,putAll```,但是他们都是浅复制,使用的时候注意啦,可以看一下以下例子: + +**例子一,使用=复制一个map:** +``` +public class CopyMapAssignTest { + public static void main(String[] args) { + + Map userMap = new HashMap<>(); + + userMap.put(1, new User("jay", 26)); + userMap.put(2, new User("fany", 25)); + + //Shallow clone + Map clonedMap = userMap; + + //Same as userMap + System.out.println(clonedMap); + + System.out.println("\nChanges reflect in both maps \n"); + + //Change a value is clonedMap + clonedMap.get(1).setName("test"); + + //Verify content of both maps + System.out.println(userMap); + System.out.println(clonedMap); + } +} +``` +运行结果: + +``` +{1=User{name='jay', age=26}, 2=User{name='fany', age=25}} + +Changes reflect in both maps + +{1=User{name='test', age=26}, 2=User{name='fany', age=25}} +{1=User{name='test', age=26}, 2=User{name='fany', age=25}} +``` +从运行结果看出,对cloneMap修改,两个map都改变了,所以=是浅复制。 + +**例子二,使用hashmap的clone复制:** + +``` +public class CopyCloneMapTest { + public static void main(String[] args) { + HashMap userMap = new HashMap<>(); + + userMap.put(1, new User("jay", 26)); + userMap.put(2, new User("fany", 25)); + + //Shallow clone + HashMap clonedMap = (HashMap) userMap.clone(); + + //Same as userMap + System.out.println(clonedMap); + + System.out.println("\nChanges reflect in both maps \n"); + + //Change a value is clonedMap + clonedMap.get(1).setName("test"); + + //Verify content of both maps + System.out.println(userMap); + System.out.println(clonedMap); + } +} + +``` +运行结果: + +``` +{1=User{name='jay', age=26}, 2=User{name='fany', age=25}} + +Changes reflect in both maps + +{1=User{name='test', age=26}, 2=User{name='fany', age=25}} +{1=User{name='test', age=26}, 2=User{name='fany', age=25}} +``` +从运行结果看出,对cloneMap修改,两个map都改变了,所以hashmap的clone也是浅复制。 + +**例子三,通过putAll操作** + +``` +public class CopyPutAllMapTest { + public static void main(String[] args) { + HashMap userMap = new HashMap<>(); + + userMap.put(1, new User("jay", 26)); + userMap.put(2, new User("fany", 25)); + + //Shallow clone + HashMap clonedMap = new HashMap<>(); + clonedMap.putAll(userMap); + + //Same as userMap + System.out.println(clonedMap); + + System.out.println("\nChanges reflect in both maps \n"); + + //Change a value is clonedMap + clonedMap.get(1).setName("test"); + + //Verify content of both maps + System.out.println(userMap); + System.out.println(clonedMap); + } +} + +``` + +运行结果: + +``` +{1=User{name='jay', age=26}, 2=User{name='fany', age=25}} + +Changes reflect in both maps + +{1=User{name='test', age=26}, 2=User{name='fany', age=25}} +{1=User{name='test', age=26}, 2=User{name='fany', age=25}} +``` +从运行结果看出,对cloneMap修改,两个map都改变了,所以putAll还是浅复制。 + +**那么,如何实现深度复制呢?** + +可以使用序列化实现,如下为谷歌Gson序列化HashMap,实现深度复制的例子: + +``` +public class CopyDeepMapTest { + + public static void main(String[] args) { + HashMap userMap = new HashMap<>(); + + userMap.put(1, new User("jay", 26)); + userMap.put(2, new User("fany", 25)); + + //Shallow clone + Gson gson = new Gson(); + String jsonString = gson.toJson(userMap); + + Type type = new TypeToken>(){}.getType(); + HashMap clonedMap = gson.fromJson(jsonString, type); + + //Same as userMap + System.out.println(clonedMap); + + System.out.println("\nChanges DO NOT reflect in other map \n"); + + //Change a value is clonedMap + clonedMap.get(1).setName("test"); + + //Verify content of both maps + System.out.println(userMap); + System.out.println(clonedMap); + } +} + +``` +运行结果: + +``` +{1=User{name='jay', age=26}, 2=User{name='fany', age=25}} + +Changes DO NOT reflect in other map + +{1=User{name='jay', age=26}, 2=User{name='fany', age=25}} +{1=User{name='test', age=26}, 2=User{name='fany', age=25}} +``` +从运行结果看出,对cloneMap修改,userMap没有被改变,所以是深度复制。 + +### 参考与感谢 +- [Top 9 questions about Java Maps](https://www.programcreek.com/2013/09/top-9-questions-for-java-map/) +- [Best way to create an empty map in Java](https://stackoverflow.com/questions/636126/best-way-to-create-an-empty-map-in-java) +- [How to clone HashMap – Shallow and Deep Copy](https://howtodoinjava.com/java/collections/hashmap/shallow-deep-copy-hashmap/) + +### 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。 +- 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。 \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200\345\255\246\344\271\240/\346\263\233\345\236\213\350\247\243\346\236\220.md" "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/\346\263\233\345\236\213\350\247\243\346\236\220.md" new file mode 100644 index 0000000..94ae0f4 --- /dev/null +++ "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/\346\263\233\345\236\213\350\247\243\346\236\220.md" @@ -0,0 +1,514 @@ +### 前言 +整理了Java泛型的相关知识,算是比较基础的,希望大家一起学习进步。 +![](https://user-gold-cdn.xitu.io/2020/1/18/16fb94807a022b0a?w=1111&h=458&f=png&s=67897) +### 一、什么是Java泛型 +Java 泛型(generics)是 JDK 5 中引入的一个新特性,其本质是参数化类型,解决不确定具体对象类型的问题。其所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 + +#### 泛型类 +泛型类(generic class) 就是具有**一个或多个类型变量**的类。一个泛型类的简单例子如下: +``` +//常见的如T、E、K、V等形式的参数常用于表示泛型,编译时无法知道它们类型,实例化时需要指定。 +public class Pair { + private K first; + private V second; + + public Pair(K first, V second) { + this.first = first; + this.second = second; + } + + public K getFirst() { + return first; + } + + public void setFirst(K first) { + this.first = first; + } + + public V getSecond() { + return second; + } + + public void setSecond(V second) { + this.second = second; + } + + public static void main(String[] args) { + // 此处K传入了Integer,V传入String类型 + Pair pairInteger = new Pair<>(1, "第二"); + System.out.println("泛型测试,first is " + pairInteger.getFirst() + + " ,second is " + pairInteger.getSecond()); + } +} + + +``` + +运行结果如下: +``` +泛型测试,first is 1 ,second is 第二 +``` + +#### 泛型接口 +泛型也可以应用于接口。 +``` +public interface Generator { + T next(); +} +``` +实现类去实现这个接口的时候,可以指定泛型T的具体类型。 + +指定具体类型为Integer的实现类: + +``` +public class NumberGenerator implements Generator { + + @Override + public Integer next() { + return new Random().nextInt(); + } +} +``` +指定具体类型为String的实现类: + +``` +public class StringGenerator implements Generator { + + @Override + public String next() { + return "测试泛型接口"; + } +} +``` + +#### 泛型方法 +具有一个或多个类型变量的方法,称之为泛型方法。 + +``` +public class GenericMethods { + + public void f(T x){ + System.out.println(x.getClass().getName()); + } + + public static void main(String[] args) { + GenericMethods gm = new GenericMethods(); + gm.f("字符串"); + gm.f(666); + } +} +``` +运行结果: + +``` +java.lang.String +java.lang.Integer +``` +### 二、泛型的好处 +Java语言引入泛型的好处是**安全简单**。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。 + +我们先来看看一个只能持有单个对象的类。 +``` +public class Holder1 { + private Automobile a; + + public Holder1(Automobile a) { + this.a = a; + } + + public Automobile getA() { + return a; + } +} +``` +我们可以发现,这个类的重用性不怎样。要使它持有其他类型的任何对象,在jdk1.5泛型之前,可以把类型设置为Object,如下: + +``` +public class Holder2 { + private Object a; + + public Holder2(Object a) { + this.a = a; + } + + public Object getA() { + return a; + } + + public void setA(Object a) { + this.a = a; + } + + public static void main(String[] args) { + Holder2 holder2 = new Holder2(new Automobile()); + //强制转换 + Automobile automobile = (Automobile) holder2.getA(); + holder2.setA("测试泛型"); + String s = (String) holder2.getA(); + } +} + +``` +我们引入泛型,实现功能那个跟Holder2类一致的Holder3,如下: + +``` +public class Holder3 { + + private T a; + + public T getA() { + return a; + } + + public void setA(T a) { + this.a = a; + } + + public Holder3(T a) { + this.a = a; + } + + public static void main(String[] args) { + Holder3 holder3 = new Holder3<>(new Automobile()); + Automobile automobile = holder3.getA(); + } +} + +``` + +因此,泛型的好处很明显了: +- 不用强制转换,因此代码比较简洁;(简洁性) +- 代替Object来表示其他类型对象,与ClassCastException异常划清界限。(安全性) +- 泛型使代码可读性增强。(可读性) + + +### 三、泛型通配符 +我们定义泛型时,经常碰见T,E,K,V,?等通配符。本质上这些都是通配符,是编码时一种约定俗成的东西。当然,你换个A-Z中另一个字母表示没有关系,但是为了可读性,一般有以下定义: +- ? 表示不确定的 java 类型 +- T (type) 表示具体的一个java类型 +- K V (key value) 分别代表java键值中的Key Value +- E (element) 代表Element + +**为什么需要引入通配符**呢,我们先来看一个例子: +``` +class Fruit{ + public int getWeigth(){ + return 0; + } +} +//Apple是水果Fruit类的子类 +class Apple extends Fruit { + public int getWeigth(){ + return 5; + } +} + +public class GenericTest { + //数组的传参 + static int sumWeigth(Fruit[] fruits) { + int weight = 0; + for (Fruit fruit : fruits) { + weight += fruit.getWeigth(); + } + return weight; + } + + static int sumWeight1(List fruits) { + int weight = 0; + for (Fruit fruit : fruits) { + weight += fruit.getWeigth(); + } + return weight; + } + static int sumWeigth2(List fruits){ + int weight = 0; + for (Fruit fruit : fruits) { + weight += fruit.getWeigth(); + } + return weight; + } + + public static void main(String[] args) { + Fruit[] fruits = new Apple[10]; + sumWeigth(fruits); + List apples = new ArrayList<>(); + sumWeight1(apples); + //报错 + sumWeigth2(apples); + } +} +``` +我们可以发现,Fruit[]与Apple[]是兼容的。```List```与```List```不兼容的,集合List是不能协变的,会报错,而List与List 是OK的,这就是通配符的魅力所在。通配符通常分三类: +- 无边界通配符,如List +- 上边界限定通配符,如; +- 下边界通配符,如; + +#### ?无边界通配符 +无边界通配符,它的使用形式是一个单独的问号:List,也就是没有任何限定。 + +看个例子: + +``` +public class GenericTest { + + public static void printList(List list) { + for (Object object : list) { + System.out.println(object); + } + } + + public static void main(String[] args) { + List list1 = new ArrayList<>(); + list1.add("A"); + list1.add("B"); + List list2 = new ArrayList<>(); + list2.add(100); + list2.add(666); + //报错,List不能添加任何类型 + List list3 = new ArrayList<>(); + list3.add(666); + } +} +``` +无界通配符()可以适配任何引用类型,看起来与原生类型等价,但与原生类型还是有区别,使用 +**无界通配符则表明在使用泛型** 。同时,List list不可以添加任何类型,因为并不知道实际是哪种类型。但是List list因为持有的是Object类型对象,所以可以add任何类型的对象。 + +#### 上边界限定通配符 < ? extends E> +使用 形式的通配符,就是**上边界限定通配符**。 extends关键字表示这个泛型中的参数必须是 E 或者 E 的子类,请看demo: +``` +class apple extends Fruit{} +static int sumWeight1(List fruits) { + int weight = 0; + for (Fruit fruit : fruits) { + weight += fruit.getWeigth(); + } + return weight; +} +public static void main(String[] args) { + List apples = new ArrayList<>(); + sumWeight1(apples); +} +``` + +但是,以下这段代码是**不可行**的: +``` +static int sumWeight1(List fruits){ + //报错 + fruits.add(new Fruit()); + //报错 + fruits.add(new Apple()); +} +``` +- 在```List ```里只能添加Fruit类对象及其子类对象(如Apple对象,Oragne对象),在```List```里只能添加Apple类和其子类对象。 +- 我们知道```List、List```等都是List<? extends Fruit>的子类型。假设一开始传参是```List list```,两个添加没问题,那如果传来```List list```,添加就失败了,编译器为了保护自己,直接禁用添加功能了。 +- 实际上,不能往List 添加任意对象,除了null。 + +#### 下边界限定通配符 < ? super E> +使用 形式的通配符,就是**下边界限定通配符**。 super关键字表示这个泛型中的参数必须是所指定的类型E,或者是此类型的父类型,直至 Object。 + +``` +public class GenericTest { + + private static void test(List dst, List src){ + for (T t : src) { + dst.add(t); + } + } + + public static void main(String[] args) { + List apples = new ArrayList<>(); + List fruits = new ArrayList<>(); + test(fruits, apples); + } +} + +``` +可以发现,List添加是没有问题的,因为子类是可以指向父类的,它添加并不像List会出现安全性问题,所以可行。 + + +### 四、泛型擦除 +#### 什么是类型擦除 +什么是Java**泛型擦除**呢? +先来看demo: +``` +Class c1 = new ArrayList().getClass(); +Class c2 = new ArrayList().getClass(); +System.out.println(c1 == c2); +/* Output +true +*/ +``` +```ArrayList ``` 和```ArrayList ``` 很容易被认为是不同的类型。但是这里输出结果是true,这是因为Java泛型是使用擦除实现的,不管是``` ArrayList()``` 还是``` new ArrayList()```,在编译生成的字节码中都不包含泛型中的类型参数,即都擦除成了ArrayList,也就是被擦除成“原生类型”,这就是泛型擦除。 + +#### 类型擦除底层 +Java泛型在编译期完成,它是依赖编译器实现的。其实,编译器主要做了这些工作: +- set()方法的类型检验 +- get()处的类型转换,编译器插入了一个checkcast语句, + +再看个例子: +``` +public class GenericTest { + + private T t; + + public T get() { + return t; + } + + public void set(T t) { + this.t = t; + } + + public static void main(String[] args) { + GenericTest test = new GenericTest(); + test.set("jay@huaxiao"); + String s = test.get(); + System.out.println(s); + } +} +/* Output +jay@huaxiao +*/ + +``` + +javap -c GenericTest.class反编译GenericTest类可得 + +``` +public class generic.GenericTest { + public generic.GenericTest(); + Code: + 0: aload_0 + 1: invokespecial #1 // Method java/lang/Object."":()V + 4: return + + public T get(); + Code: + 0: aload_0 + 1: getfield #2 // Field t:Ljava/lang/Object; + 4: areturn + + public void set(T); + Code: + 0: aload_0 + 1: aload_1 + 2: putfield #2 // Field t:Ljava/lang/Object; + 5: return + + public static void main(java.lang.String[]); + Code: + 0: new #3 // class generic/GenericTest + 3: dup + 4: invokespecial #4 // Method "":()V + 7: astore_1 + 8: aload_1 + 9: ldc #5 // String jay@huaxiao + 11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V + 14: aload_1 + 15: invokevirtual #7 // Method get:()Ljava/lang/Object; + 18: checkcast #8 // class java/lang/String + 21: astore_2 + 22: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; + 25: aload_2 + 26: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V + 29: return +} +``` +- 看第11,set进去的是原始类型Object(#6); +- 看第15,get方法获得也是Object类型(#7),说明类型被擦出了。 +- 再看第18,它做了一个checkcast操作,是一个String类型,强转。 + +### 五、泛型的限制与局限 +使用Java泛型需要考虑以下一些约束与限制,其实几乎都跟泛型擦除有关。 +#### 不能用基本类型实例化类型化参数 +不能用类型参数代替基本类型。因此, 没有 ```Pair```, 只 有``` Pair```。 当然, 其原因是类型擦除。擦除之后, Pair 类含有 Object 类型的域, 而 Object 不能存储 double值。 + +#### 运行时类型查询只适用于原始类型 +如,getClass()方法等只返回原始类型,因为JVM根本就不知道泛型这回事,它只知道原始类型。 +``` +if(a instanceof Pair) //ERROR,仅测试了a是否是任意类型的一个Pair,会看到编译器ERROR警告 + +if(a instanceof Pair) //ERROR + +Pair p = (Pair) a;//WARNING,仅测试a是否是一个Pair + +Pair stringPair = ...; +Pair employeePair = ...; +if(stringPair.getClass() == employeePair.getClass()) //会得到true,因为两次调用getClass都将返回Pair.class +``` + +#### 不能创建参数化类型的数组 +不能实例化参数化类型的数组, 例如: +``` +Pair[] table = new Pair[10]; // Error +``` + +#### 不能实例化类型变量 +不能使用像 new T(...),newT[...] 或 T.class 这样的表达式中的类型变量。例如, 下面的``` Pair``` 构造器就是非法的: +``` +public Pair() { first = new T(); second = new T(); } // Error +``` +#### 使用泛型接口时,需要避免重复实现同一个接口 + +``` +interface Swim {} + +class Duck implements Swim {} + +class UglyDuck extends Duck implements Swim {} +``` +#### 可以消除对受查异常的检查 + +``` +@SuppressWamings("unchecked") +public static extends BaseResponse { + private static final long serialVersionUID = -xxx; + + private T data; + + private String code; + + public Response() { + } + + public T getData() { + return this.data; + } + + public void setData(T data,String code ) { + this.data = data; + this.code = code; + } +} +``` + +### 六、Java泛型常见面试题 +Java泛型常见几道面试题 +- Java中的泛型是什么 ? 使用泛型的好处是什么?(第一,第二小节可答) +- Java的泛型是如何工作的 ? 什么是类型擦除 ? (第四小节可答) +- 什么是泛型中的限定通配符和非限定通配符 ? (第三小节可答) +- List和List 之间有什么区别 ?(第三小节可答) +- 你了解泛型通配符与上下界吗?(第三小节可答) + +### 参考与感谢 +- 《Java编程思想》 +- 《Java核心技术》 +- [聊一聊-JAVA 泛型中的通配符 T,E,K,V,?](https://juejin.im/post/5d5789d26fb9a06ad0056bd9) +- [Java泛型的局限和使用经验](https://www.jianshu.com/p/a58da9011f85) +- [Java泛型之类型擦除](https://zhuanlan.zhihu.com/p/31741402) + +### 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。 +- 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。 + + + + diff --git "a/Java\345\237\272\347\241\200\345\255\246\344\271\240/\347\272\277\347\250\213\346\261\240\350\247\243\346\236\220.md" "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/\347\272\277\347\250\213\346\261\240\350\247\243\346\236\220.md" new file mode 100644 index 0000000..08256db --- /dev/null +++ "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/\347\272\277\347\250\213\346\261\240\350\247\243\346\236\220.md" @@ -0,0 +1,490 @@ + +![](https://user-gold-cdn.xitu.io/2019/7/15/16bf4399bfe4b7b4?w=2048&h=1280&f=jpeg&s=142516) +## 前言 +掌握线程池是后端程序员的基本要求,相信大家求职面试过程中,几乎都会被问到有关于线程池的问题。我在网上搜集了几道经典的线程池面试题,并以此为切入点,谈谈我对线程池的理解。如果有哪里理解不正确,非常希望大家指出,接下来大家一起分析学习吧。 + +## 经典面试题 +- 面试问题1:Java的线程池说一下,各个参数的作用,如何进行的? +- 面试问题2:按线程池内部机制,当提交新任务时,有哪些异常要考虑。 +- 面试问题3:线程池都有哪几种工作队列? +- 面试问题4:使用无界队列的线程池会导致内存飙升吗? +- 面试问题5:说说几种常见的线程池及使用场景? + +## 线程池概念 +**线程池:** 简单理解,它就是一个管理线程的池子。 +- **它帮我们管理线程,避免增加创建线程和销毁线程的资源损耗**。因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的。 +- **提高响应速度。** 如果任务到达了,相对于从线程池拿线程,重新去创建一条线程执行,速度肯定慢很多。 +- **重复利用。** 线程用完,再放回池子,可以达到重复利用的效果,节省资源。 + +## 线程池的创建 +线程池可以通过ThreadPoolExecutor来创建,我们来看一下它的构造函数: +```java +public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) +``` +几个核心参数的作用: +- **corePoolSize:** 线程池核心线程数最大值 +- **maximumPoolSize:** 线程池最大线程数大小 +- **keepAliveTime:** 线程池中非核心线程空闲的存活时间大小 +- **unit:** 线程空闲存活时间单位 +- **workQueue:** 存放任务的阻塞队列 +- **threadFactory:** 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。 +- **handler:** 线城池的饱和策略事件,主要有四种类型。 + +## 任务执行 +### 线程池执行流程,即对应execute()方法: +![](https://user-gold-cdn.xitu.io/2019/7/7/16bca03a5a6fd78f?w=800&h=370&f=jpeg&s=20069) + +- 提交一个任务,线程池里存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。 +- 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。 +- 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。 +- 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。 + +### 四种拒绝策略 +- AbortPolicy(抛出一个异常,默认的) +- DiscardPolicy(直接丢弃任务) +- DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池) +- CallerRunsPolicy(交给线程池调用所在的线程进行处理) + + +### 为了形象描述线程池执行,我打个比喻: +- 核心线程比作公司正式员工 +- 非核心线程比作外包员工 +- 阻塞队列比作需求池 +- 提交任务比作提需求 +![](https://user-gold-cdn.xitu.io/2019/7/13/16bea86ad171713d?w=766&h=875&f=png&s=62662) +- 当产品提个需求,正式员工(核心线程)先接需求(执行任务) +- 如果正式员工都有需求在做,即核心线程数已满),产品就把需求先放需求池(阻塞队列)。 +- 如果需求池(阻塞队列)也满了,但是这时候产品继续提需求,怎么办呢?那就请外包(非核心线程)来做。 +- 如果所有员工(最大线程数也满了)都有需求在做了,那就执行拒绝策略。 +- 如果外包员工把需求做完了,它经过一段(keepAliveTime)空闲时间,就离开公司了。 + +好的,到这里。**面试问题1->Java的线程池说一下,各个参数的作用,如何进行的?** 是否已经迎刃而解啦, +我觉得这个问题,回答:**线程池构造函数的corePoolSize,maximumPoolSize等参数,并且能描述清楚线程池的执行流程** 就差不多啦。 + +## 线程池异常处理 +在使用线程池处理任务的时候,任务代码可能抛出RuntimeException,抛出异常后,线程池可能捕获它,也可能创建一个新的线程来代替异常的线程,我们可能无法感知任务出现了异常,因此我们需要考虑线程池异常情况。 + +### 当提交新任务时,异常如何处理? +我们先来看一段代码: +```java + ExecutorService threadPool = Executors.newFixedThreadPool(5); + for (int i = 0; i < 5; i++) { + threadPool.submit(() -> { + System.out.println("current thread name" + Thread.currentThread().getName()); + Object object = null; + System.out.print("result## "+object.toString()); + }); + } + +``` +显然,这段代码会有异常,我们再来看看执行结果 + +![](https://user-gold-cdn.xitu.io/2019/7/14/16bee347085e360b?w=1445&h=768&f=png&s=121166) + +虽然没有结果输出,但是没有抛出异常,所以我们无法感知任务出现了异常,所以需要添加try/catch。 +如下图: +![](https://user-gold-cdn.xitu.io/2019/7/14/16bee41a1dfeef91?w=1251&h=904&f=png&s=126163) +OK,线程的异常处理,**我们可以直接try...catch捕获。** + +### 线程池exec.submit(runnable)的执行流程 +通过debug上面有异常的submit方法(**建议大家也去debug看一下,图上的每个方法内部是我打断点的地方**),处理有异常submit方法的主要执行流程图: + +![](https://user-gold-cdn.xitu.io/2019/7/14/16bef895fec0d45b?w=783&h=953&f=png&s=63367) + +```java + //构造feature对象 + /** + * @throws RejectedExecutionException {@inheritDoc} + * @throws NullPointerException {@inheritDoc} + */ + public Future submit(Runnable task) { + if (task == null) throw new NullPointerException(); + RunnableFuture ftask = newTaskFor(task, null); + execute(ftask); + return ftask; + } + protected RunnableFuture newTaskFor(Runnable runnable, T value) { + return new FutureTask(runnable, value); + } + public FutureTask(Runnable runnable, V result) { + this.callable = Executors.callable(runnable, result); + this.state = NEW; // ensure visibility of callable + } + public static Callable callable(Runnable task, T result) { + if (task == null) + throw new NullPointerException(); + return new RunnableAdapter(task, result); + } + //线程池执行 + public void execute(Runnable command) { + if (command == null) + throw new NullPointerException(); + int c = ctl.get(); + if (workerCountOf(c) < corePoolSize) { + if (addWorker(command, true)) + return; + c = ctl.get(); + } + if (isRunning(c) && workQueue.offer(command)) { + int recheck = ctl.get(); + if (! isRunning(recheck) && remove(command)) + reject(command); + else if (workerCountOf(recheck) == 0) + addWorker(null, false); + } + else if (!addWorker(command, false)) + reject(command); + } + //捕获异常 + public void run() { + if (state != NEW || + !UNSAFE.compareAndSwapObject(this, runnerOffset, + null, Thread.currentThread())) + return; + try { + Callable c = callable; + if (c != null && state == NEW) { + V result; + boolean ran; + try { + result = c.call(); + ran = true; + } catch (Throwable ex) { + result = null; + ran = false; + setException(ex); + } + if (ran) + set(result); + } + } finally { + // runner must be non-null until state is settled to + // prevent concurrent calls to run() + runner = null; + // state must be re-read after nulling runner to prevent + // leaked interrupts + int s = state; + if (s >= INTERRUPTING) + handlePossibleCancellationInterrupt(s); + } +``` +通过以上分析,**submit执行的任务,可以通过Future对象的get方法接收抛出的异常,再进行处理。** +我们再通过一个demo,看一下Future对象的get方法处理异常的姿势,如下图: + +![](https://user-gold-cdn.xitu.io/2019/7/14/16bef9bb609bbe31?w=1349&h=996&f=png&s=129696) + +### 其他两种处理线程池异常方案 +除了以上**1.在任务代码try/catch捕获异常,2.通过Future对象的get方法接收抛出的异常,再处理**两种方案外,还有以上两种方案: +#### 3.为工作者线程设置UncaughtExceptionHandler,在uncaughtException方法中处理异常 +我们直接看这样实现的正确姿势: +```java +ExecutorService threadPool = Executors.newFixedThreadPool(1, r -> { + Thread t = new Thread(r); + t.setUncaughtExceptionHandler( + (t1, e) -> { + System.out.println(t1.getName() + "线程抛出的异常"+e); + }); + return t; + }); + threadPool.execute(()->{ + Object object = null; + System.out.print("result## " + object.toString()); + }); +``` +运行结果: +![](https://user-gold-cdn.xitu.io/2019/7/14/16bf00f61b40c749?w=1183&h=662&f=png&s=98577) + +#### 4.重写ThreadPoolExecutor的afterExecute方法,处理传递的异常引用 +这是jdk文档的一个demo: +```java +class ExtendedExecutor extends ThreadPoolExecutor { + // 这可是jdk文档里面给的例子。。 + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + if (t == null && r instanceof Future) { + try { + Object result = ((Future) r).get(); + } catch (CancellationException ce) { + t = ce; + } catch (ExecutionException ee) { + t = ee.getCause(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); // ignore/reset + } + } + if (t != null) + System.out.println(t); + } +}} +``` + +### 因此,被问到线程池异常处理,如何回答? +![](https://user-gold-cdn.xitu.io/2019/7/14/16bec33ca5559c93?w=985&h=728&f=png&s=87035)。 + +## 线程池的工作队列 +**线程池都有哪几种工作队列?** +- ArrayBlockingQueue +- LinkedBlockingQueue +- DelayQueue +- PriorityBlockingQueue +- SynchronousQueue + +### ArrayBlockingQueue +ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。 +### LinkedBlockingQueue +LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列 +### DelayQueue +DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。 +### PriorityBlockingQueue +PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列; +### SynchronousQueue +SynchronousQueue(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。 + +针对面试题:**线程池都有哪几种工作队列?** 我觉得,**回答以上几种ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue等,说出它们的特点,并结合使用到对应队列的常用线程池(如newFixedThreadPool线程池使用LinkedBlockingQueue),进行展开阐述,** 就可以啦。 + +## 几种常用的线程池 +- newFixedThreadPool (固定数目线程的线程池) +- newCachedThreadPool(可缓存线程的线程池) +- newSingleThreadExecutor(单线程的线程池) +- newScheduledThreadPool(定时及周期执行的线程池) + +### newFixedThreadPool +```java + public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { + return new ThreadPoolExecutor(nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + threadFactory); + } + ``` + +#### 线程池特点: +- 核心线程数和最大线程数大小一样 +- 没有所谓的非空闲时间,即keepAliveTime为0 +- 阻塞队列为无界队列LinkedBlockingQueue + +#### 工作机制: + +![](https://user-gold-cdn.xitu.io/2019/7/14/16bf109b642bc12e?w=1022&h=472&f=png&s=58870) +- 提交任务 +- 如果线程数少于核心线程,创建核心线程执行任务 +- 如果线程数等于核心线程,把任务添加到LinkedBlockingQueue阻塞队列 +- 如果线程执行完任务,去阻塞队列取任务,继续执行。 + +#### 实例代码 +```java + ExecutorService executor = Executors.newFixedThreadPool(10); + for (int i = 0; i < Integer.MAX_VALUE; i++) { + executor.execute(()->{ + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + //do nothing + } + }); +``` +IDE指定JVM参数:-Xmx8m -Xms8m : + +![](https://user-gold-cdn.xitu.io/2019/7/15/16bf2b9599369487?w=1105&h=765&f=png&s=105712) + +run以上代码,会抛出OOM: + +![](https://user-gold-cdn.xitu.io/2019/7/15/16bf2baa0728e61f?w=1292&h=715&f=png&s=92358) + +因此,**面试题:使用无界队列的线程池会导致内存飙升吗?** + +答案 **:会的,newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升,** 最终导致OOM。 + +#### 使用场景 +FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。 + +### newCachedThreadPool +```java + public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue(), + threadFactory); + } +``` +#### 线程池特点: +- 核心线程数为0 +- 最大线程数为Integer.MAX_VALUE +- 阻塞队列是SynchronousQueue +- 非核心线程空闲存活时间为60秒 + +当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。 + +#### 工作机制 + +![](https://user-gold-cdn.xitu.io/2019/7/15/16bf2d1734c8f96d?w=832&h=839&f=png&s=84306) + +- 提交任务 +- 因为没有核心线程,所以任务直接加到SynchronousQueue队列。 +- 判断是否有空闲线程,如果有,就去取出任务执行。 +- 如果没有空闲线程,就新建一个线程执行。 +- 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。 + +#### 实例代码 +```java + ExecutorService executor = Executors.newCachedThreadPool(); + for (int i = 0; i < 5; i++) { + executor.execute(() -> { + System.out.println(Thread.currentThread().getName()+"正在执行"); + }); + } +``` +运行结果: + +![](https://user-gold-cdn.xitu.io/2019/7/15/16bf2ddffd11762b?w=1238&h=608&f=png&s=84727) + +#### 使用场景 +用于并发执行大量短期的小任务。 + +### newSingleThreadExecutor +```java + public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { + return new FinalizableDelegatedExecutorService + (new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + threadFactory)); + } +``` + +#### 线程池特点 +- 核心线程数为1 +- 最大线程数也为1 +- 阻塞队列是LinkedBlockingQueue +- keepAliveTime为0 + +#### 工作机制 + +![](https://user-gold-cdn.xitu.io/2019/7/15/16bf2e62d7dbaaea?w=668&h=619&f=png&s=50060) + +- 提交任务 +- 线程池是否有一条线程在,如果没有,新建线程执行任务 +- 如果有,讲任务加到阻塞队列 +- 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个人(一条线程)夜以继日地干活。 + +#### 实例代码 +```java + ExecutorService executor = Executors.newSingleThreadExecutor(); + for (int i = 0; i < 5; i++) { + executor.execute(() -> { + System.out.println(Thread.currentThread().getName()+"正在执行"); + }); + } +``` +运行结果: + +![](https://user-gold-cdn.xitu.io/2019/7/15/16bf2ecf78989289?w=1247&h=550&f=png&s=97217) + +#### 使用场景 +适用于串行执行任务的场景,一个任务一个任务地执行。 + +### newScheduledThreadPool +```java + public ScheduledThreadPoolExecutor(int corePoolSize) { + super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, + new DelayedWorkQueue()); + } +``` +#### 线程池特点 +- 最大线程数为Integer.MAX_VALUE +- 阻塞队列是DelayedWorkQueue +- keepAliveTime为0 +- scheduleAtFixedRate() :按某种速率周期执行 +- scheduleWithFixedDelay():在某个延迟后执行 + +#### 工作机制 +- 添加一个任务 +- 线程池中的线程从 DelayQueue 中取任务 +- 线程从 DelayQueue 中获取 time 大于等于当前时间的task +- 执行完后修改这个 task 的 time 为下次被执行的时间 +- 这个 task 放回DelayQueue队列中 + +#### 实例代码 +```java + /** + 创建一个给定初始延迟的间隔性的任务,之后的下次执行时间是上一次任务从执行到结束所需要的时间+* 给定的间隔时间 + */ + ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); + scheduledExecutorService.scheduleWithFixedDelay(()->{ + System.out.println("current Time" + System.currentTimeMillis()); + System.out.println(Thread.currentThread().getName()+"正在执行"); + }, 1, 3, TimeUnit.SECONDS); +``` +运行结果: + +![](https://user-gold-cdn.xitu.io/2019/7/15/16bf315061f0ed2d?w=1501&h=717&f=png&s=131686) + +```java + /** + 创建一个给定初始延迟的间隔性的任务,之后的每次任务执行时间为 初始延迟 + N * delay(间隔) + */ + ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); + scheduledExecutorService.scheduleAtFixedRate(()->{ + System.out.println("current Time" + System.currentTimeMillis()); + System.out.println(Thread.currentThread().getName()+"正在执行"); + }, 1, 3, TimeUnit.SECONDS);; +``` +#### 使用场景 +周期性执行任务的场景,需要限制线程数量的场景 + +回到面试题:**说说几种常见的线程池及使用场景?** + +回答这四种经典线程池 **:newFixedThreadPool,newSingleThreadExecutor,newCachedThreadPool,newScheduledThreadPool,分线程池特点,工作机制,使用场景分开描述,再分析可能存在的问题,比如newFixedThreadPool内存飙升问题** 即可 + +## 线程池状态 +线程池有这几个状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED。 +```java + //线程池状态 + private static final int RUNNING = -1 << COUNT_BITS; + private static final int SHUTDOWN = 0 << COUNT_BITS; + private static final int STOP = 1 << COUNT_BITS; + private static final int TIDYING = 2 << COUNT_BITS; + private static final int TERMINATED = 3 << COUNT_BITS; +``` + +### 线程池各个状态切换图: + +![](https://user-gold-cdn.xitu.io/2019/7/15/16bf3b10e39a52d0?w=1227&h=494&f=png&s=96115) + +**RUNNING** +- 该状态的线程池会接收新任务,并处理阻塞队列中的任务; +- 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态; +- 调用线程池的shutdownNow()方法,可以切换到STOP状态; + +**SHUTDOWN** +- 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务; +- 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态; + + +**STOP** +- 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务; +- 线程池中执行的任务为空,进入TIDYING状态; + +**TIDYING** +- 该状态表明所有的任务已经运行终止,记录的任务数量为0。 +- terminated()执行完毕,进入TERMINATED状态 + +**TERMINATED** +- 该状态表示线程池彻底终止 + + +## 参考与感谢 +- Java线程池异常处理方案:https://www.jianshu.com/p/30e488f4e021 +- Java线程池 https://www.hollischuang.com/archives/2888 +- 关于线程池的面试题 https://www.jianshu.com/p/9710b899e749 +- 线程池的五种状态 https://blog.csdn.net/l_kanglin/article/details/57411851 +- 深入分析java线程池的实现原理 https://www.jianshu.com/p/87bff5cc8d8c/ + +## 个人公众号 + + +- 欢迎大家关注,大家一起学习,一起讨论。 +- github地址:https://github.com/whx123/JavaHome \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200\345\255\246\344\271\240/\350\247\246\345\217\221\347\261\273\345\212\240\350\275\275\347\232\204\345\205\255\345\244\247\346\227\266\346\234\272.md" "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/\350\247\246\345\217\221\347\261\273\345\212\240\350\275\275\347\232\204\345\205\255\345\244\247\346\227\266\346\234\272.md" new file mode 100644 index 0000000..1b0f413 --- /dev/null +++ "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/\350\247\246\345\217\221\347\261\273\345\212\240\350\275\275\347\232\204\345\205\255\345\244\247\346\227\266\346\234\272.md" @@ -0,0 +1,273 @@ + +![](https://user-gold-cdn.xitu.io/2019/9/22/16d59927154281d2?w=500&h=334&f=jpeg&s=30619) +## 前言 +什么情况下会触发类加载的进行呢?本文将结合代码demo谈谈几种情况,希望对大家有帮助。 + +## 类加载时机 +什么情况需要开始类加载过程的第一阶段:加载?Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则严格规定了以下几种情况必须立即对类进行初始化,如果类没有进行过初始化,则需要先触发其初始化。 + + +![](https://user-gold-cdn.xitu.io/2019/9/22/16d593cdb1dee573?w=815&h=650&f=png&s=73353) +## 创建类的实例 +为了验证类加载,我们先配置一个JVM参数 +``` +-XX:+TraceClassLoading 监控类的加载 +``` +在IDE配置如下: + +![](https://user-gold-cdn.xitu.io/2019/9/22/16d58ccf239f17d5?w=1269&h=840&f=png&s=96736) + +demo代码: +``` +public class ClassLoadInstance { + + static { + System.out.println("ClassLoadInstance类初始化时就会被执行!"); + } + + public ClassLoadInstance() { + System.out.println("ClassLoadInstance构造函数!"); + } +} + +public class ClassLoadTest { + + public static void main(String[] args) { + ClassLoadInstance instance = new ClassLoadInstance(); + } +} + +``` +运行结果: + +![](https://user-gold-cdn.xitu.io/2019/9/22/16d58d7a7735d914?w=1218&h=119&f=png&s=23340) + + +**结论:** + +new ClassLoadInstance实例时,发现ClassLoadInstance被加载了,因此 new创建实例对象,会触发类加载进行。 + +## 访问类的静态变量 +demo代码: +``` +public class ClassLoadStaticVariable { + + static { + System.out.println("ClassLoadStaticVariable类初始化时就会被执行!"); + } + + public static int i = 100; + + public ClassLoadStaticVariable() { + System.out.println("ClassLoadStaticVariable构造函数!"); + } + +} + +public class ClassLoadTest { + + public static void main(String[] args) { + System.out.println(ClassLoadStaticVariable.i); + } +} +``` +运行结果: + +![](https://user-gold-cdn.xitu.io/2019/9/22/16d58e23d0d91b82?w=1240&h=112&f=png&s=17651) + +**结论:** + +访问类ClassLoadStaticVariable的静态变量i时,发现ClassLoadStaticVariable类被加载啦,因此访问类的静态变量会触发类加载。 + +**注意:** + +访问final修饰的静态变量时,不会触发类加载,因为在编译期已经将此常量放在常量池了。 + +## 访问类的静态方法 +demo代码: +``` +public class ClassLoadStaticMethod { + + static { + System.out.println("ClassLoadStaticMethod类初始化时就会被执行!"); + } + + public static void method(){ + System.out.println("静态方法被调用"); + } + + public ClassLoadStaticMethod() { + System.out.println("ClassLoadStaticMethod构造函数!"); + } + +} + +public class ClassLoadTest { + + public static void main(String[] args) { + ClassLoadStaticMethod.method(); + } +} +``` +运行结果: + +![](https://user-gold-cdn.xitu.io/2019/9/22/16d58effc2119fa4?w=1299&h=109&f=png&s=21427) + +结论: + +访问类ClassLoadStaticMethod的静态方法method时,发现ClassLoadStaticMethod类被加载啦,因此访问类的静态方法会触发类加载。 + +## 反射 +demo代码: +``` +package classload; + +public class ClassLoadStaticReflect { + + static { + System.out.println("ClassLoadStaticReflect类初始化时就会被执行!"); + } + + public static void method(){ + System.out.println("静态方法被调用"); + } + + public ClassLoadStaticReflect() { + System.out.println("ClassLoadStaticReflect构造函数!"); + } + +} + +public class ClassLoadTest { + + public static void main(String[] args) throws ClassNotFoundException { + Class.forName("classload.ClassLoadStaticReflect"); + } +} +``` + +运行结果: + +![](https://user-gold-cdn.xitu.io/2019/9/22/16d5900d7dfcb244?w=1227&h=82&f=png&s=17698) + +**结论:** + +反射得到类ClassLoadStaticReflect时,发现ClassLoadStaticReflect类被加载啦,因此反射会触发类加载。 + +## 当初始化一个类时,发现其父类还未初始化,则先触发父类的初始化 +demo代码: +``` +//父类 +public class ClassLoadSuper { + static { + System.out.println("ClassLoadSuper类初始化时就会被执行!这是父类"); + } + + public static int superNum = 100; + + public ClassLoadSuper() { + System.out.println("父类ClassLoadSuper构造函数!"); + } +} +//子类 +public class ClassLoadSub extends ClassLoadSuper { + + static { + System.out.println("ClassLoadSub类初始化时就会被执行!这是子类"); + } + + public static int subNum = 100; + + public ClassLoadSub() { + System.out.println("子类ClassLoadSub构造函数!"); + } + +} + +public class ClassLoadTest { + + public static void main(String[] args) throws ClassNotFoundException { + ClassLoadSub classLoadSub = new ClassLoadSub(); + } +} +``` + +运行结果: + +![](https://user-gold-cdn.xitu.io/2019/9/22/16d5913a74736e7d?w=1259&h=209&f=png&s=45708) +看了运行结果,是不是发现,网上那道经典面试题(**讲讲类的实例化顺序**?)也很清晰啦。 +先父类静态变量/静态代码块-> 再子类静态变量/静态代码块->父类构造器->子类构造器 + +**结论:** + +实例化子类ClassLoadSub的时候,发现父类ClassLoadSuper先被加载,因此当初始化一个类时,发现其父类还未初始化,则先触发父类的初始化 + +## 虚拟机启动时,定义了main()方法的那个类先初始化 +demo代码: +``` +package classload; + +public class ClassLoadTest { + + public static void main(String[] args) { + System.out.println(ClassLoadSub.subNum); + } +} +``` + +运行结果: + +![](https://user-gold-cdn.xitu.io/2019/9/22/16d5922bdec72ac7?w=1391&h=371&f=png&s=83213) + +**结论:** + +虚拟机启动时,即使有ClassLoadSub,ClassLoadSuper,ClassLoadTest等类被加载, 但ClassLoadTest最先被加载,即定义了main()方法的那个类会先触发类加载。 + +## 练习与小结 +触发类加载的六大时机,我们都分析完啦,是不是不做个题都觉得意犹未尽呢?接下来,我们来分析类加载一道经典面试题吧。 + +``` +class SingleTon { + private static SingleTon singleTon = new SingleTon(); + public static int count1; + public static int count2 = 0; + + private SingleTon() { + count1++; + count2++; + } + + public static SingleTon getInstance() { + return singleTon; + } +} + +public class ClassLoadTest { + public static void main(String[] args) { + SingleTon singleTon = SingleTon.getInstance(); + System.out.println("count1=" + singleTon.count1); + System.out.println("count2=" + singleTon.count2); + } +} + +``` + +运行结果: + +![](https://user-gold-cdn.xitu.io/2019/9/22/16d592b67bfbed36?w=1332&h=268&f=png&s=50004) + +**分析:** + +1. SingleTon.getInstance(),调用静态方法,触发SingleTon类加载。 +2. SingleTon类加载初始化,按顺序初始化静态变量。 +3. 先执行private static SingleTon singleTon = new SingleTon(); ,调用构造器后,count1,count2均为1; +4. 按顺序执行 public static int count1; 没有赋值,所以count1依旧为1; +5. 按顺序执行 public static int count2 = 0;所以count2变为0. + +## 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +欢迎大家关注,大家一起学习,一起讨论。 + + diff --git "a/Java\345\237\272\347\241\200\345\255\246\344\271\240/\350\260\210\350\260\210Java\345\217\215\345\260\204\357\274\232\344\273\216\345\205\245\351\227\250\345\210\260\345\256\236\350\267\265\357\274\214\345\206\215\345\210\260\345\216\237\347\220\206.md" "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/\350\260\210\350\260\210Java\345\217\215\345\260\204\357\274\232\344\273\216\345\205\245\351\227\250\345\210\260\345\256\236\350\267\265\357\274\214\345\206\215\345\210\260\345\216\237\347\220\206.md" new file mode 100644 index 0000000..cb25ff9 --- /dev/null +++ "b/Java\345\237\272\347\241\200\345\255\246\344\271\240/\350\260\210\350\260\210Java\345\217\215\345\260\204\357\274\232\344\273\216\345\205\245\351\227\250\345\210\260\345\256\236\350\267\265\357\274\214\345\206\215\345\210\260\345\216\237\347\220\206.md" @@ -0,0 +1,361 @@ +## 前言 +反射是Java底层框架的灵魂技术,学习反射非常有必要,本文将从入门概念,到实践,再到原理讲解反射,希望对大家有帮助。 + +## 反射理解 + +### 官方解析 +[Oracle 官方对反射](https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/index.html)的解释是: +``` +Reflection is commonly used by programs which require the ability to examine or +modify the runtime behavior of applications running in the Java virtual machine. +This is a relatively advanced feature and should be used only by developers who +have a strong grasp of the fundamentals of the language. With that caveat in +mind, reflection is a powerful technique and can enable applications to perform +operations which would otherwise be impossible. +``` + +Java 的**反射机制**是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。 + +### 白话理解 +#### 正射 +万物有阴必有阳,有正必有反。既然有反射,就必有“正射”。 + +那么**正射**是什么呢? + +我们在编写代码时,当需要使用到某一个类的时候,都会先了解这个类是做什么的。然后实例化这个类,接着用实例化好的对象进行操作,这就是正射。 +``` +Student student = new Student(); +student.doHomework("数学"); +``` + +#### 反射 +反射就是,一开始并不知道我们要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。 +``` + Class clazz = Class.forName("reflection.Student"); + Method method = clazz.getMethod("doHomework", String.class); + Constructor constructor = clazz.getConstructor(); + Object object = constructor.newInstance(); + method.invoke(object, "语文"); +``` + +#### 正射与反射对比 +以上两段代码,执行效果是一样的,如图 + +![](https://user-gold-cdn.xitu.io/2019/12/13/16efff4ccf899e66?w=1071&h=716&f=png&s=91450) + +但是,其实现的过程还是有很大的差别的: + +- 第一段代码在未运行前就已经知道了要运行的类是```Student```; +- 第二段代码则是到整个程序运行的时候,从字符串```reflection.Student```,才知道要操作的类是```Student```。 + + +#### 结论 + +反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。 + +## Class 对象理解 +要理解Class对象,我们先来了解一下**RTTI**吧。 +**RTTI(Run-Time Type Identification)运行时类型识别**,其作用是在运行时识别一个对象的类型和类的信息。 + +Java是如何让我们在运行时识别对象和类的信息的?主要有两种方式: +一种是传统的**RRTI**,它假定我们在编译期已知道了所有类型。 +另一种是反射机制,它允许我们在运行时发现和使用类的信息。 + +**每个类都有一个Class对象**,每当编译一个新类就产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。比如创建一个Student类,那么,JVM就会创建一个Student对应Class类的Class对象,该Class对象保存了Student类相关的类型信息。 + + +![](https://user-gold-cdn.xitu.io/2019/12/21/16f286b0e2fce054?w=1066&h=538&f=png&s=60629) + +**Class类的对象作用**是运行时提供或获得某个对象的类型信息 + +## 反射的基本使用 + +### 获取 Class 类对象 + +获取反射中的Class对象有三种方法。 + +**第一种,使用 Class.forName 静态方法。** +``` +Class class1 = Class.forName("reflection.TestReflection"); +``` + +**第二种,使用类的.class 方法** +``` +Class class2 = TestReflection.class; +``` + +**第三种,使用实例对象的 getClass() 方法。** + +``` +TestReflection testReflection = new TestReflection(); +Class class3 = testReflection.getClass(); +``` + +![](https://user-gold-cdn.xitu.io/2019/12/16/16f0c05f6b2a341a?w=1065&h=628&f=png&s=85470) + + +### 反射创造对象,获取方法,成员变量,构造器 +本小节学习反射的基本API用法,如获取方法,成员变量等。 +#### 反射创造对象 +通过反射创建类对象主要有两种方式: +![](https://user-gold-cdn.xitu.io/2019/12/19/16f1b85fc34ce537?w=657&h=366&f=png&s=23704) + +**实例代码:** +``` +//方式一 +Class class1 = Class.forName("reflection.Student"); +Student student = (Student) class1.newInstance(); +System.out.println(student); + +//方式二 +Constructor constructor = class1.getConstructor(); +Student student1 = (Student) constructor.newInstance(); +System.out.println(student1); +``` +运行结果: + +![](https://user-gold-cdn.xitu.io/2019/12/19/16f1ede509708fec?w=868&h=525&f=png&s=68487) + +#### 反射获取类的构造器 + + +![](https://user-gold-cdn.xitu.io/2019/12/18/16f1662225a43b9e?w=938&h=479&f=png&s=52644) + +**看一个例子吧:** + +``` +Class class1 = Class.forName("reflection.Student"); +Constructor[] constructors = class1.getDeclaredConstructors(); +for (int i = 0; i < constructors.length; i++) { + System.out.println(constructors[i]); + } +``` +![](https://user-gold-cdn.xitu.io/2019/12/18/16f165e1e8d36c90?w=1104&h=641&f=png&s=81960) + +#### 反射获取类的成员变量 + +![](https://user-gold-cdn.xitu.io/2019/12/18/16f19b116e71c366?w=1018&h=528&f=png&s=52897) + +**看demo:** + +``` +// student 一个私有属性age,一个公有属性email +public class Student { + + private Integer age; + + public String email; +} + +public class TestReflection { + public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException { + Class class1 = Class.forName("reflection.Student"); + Field email = class1.getField("email"); + System.out.println(email); + Field age = class1.getField("age"); + System.out.println(age); + } +} + +``` +**运行结果:** + +![](https://user-gold-cdn.xitu.io/2019/12/18/16f19bc4e032ace2?w=1385&h=663&f=png&s=94943) +即```getField(String name)``` 根据参数变量名,返回一个具体的具有public属性的成员变量,如果该变量**不是public属性**,则报异常。 + + +#### 反射获取类的方法 + +![](https://user-gold-cdn.xitu.io/2019/12/19/16f19c250c645069?w=1022&h=463&f=png&s=43225) + +**demo** + +``` +public class Student { + + private void testPrivateMethod() { + + } + public void testPublicMethod() { + + } +} + +public class TestReflection { + public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException { + Class class1 = Class.forName("reflection.Student"); + + Method[] methods = class1.getMethods(); + for (int i = 0; i < methods.length; i++) { + System.out.println(methods[i]); + } + } +} + +``` +**运行结果:** + +![](https://user-gold-cdn.xitu.io/2019/12/19/16f19c70f7446654?w=1346&h=721&f=png&s=112136) + + +## 反射的实现原理 +通过上一小节学习,我们已经知道反射的基本API用法了。接下来,跟着一个例子,学习反射方法的执行链路。 + +``` +public class TestReflection { + public static void main(String[] args) throws Exception { + Class clazz = Class.forName("reflection.TestReflection"); + Method method = clazz.getMethod("target", String.class); + method.invoke(null, "666"); + } + + public static void target(String str) { + //打印堆栈信息 + new Exception("#" +str).printStackTrace(); + System.out.println("invoke target method"); + } +} +``` + +**堆栈信息反映出反射调用链路:** +``` +java.lang.Exception: #666 +invoke target method + at reflection.TestReflection.target(TestReflection.java:17) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.lang.reflect.Method.invoke(Method.java:498) + at reflection.TestReflection.main(TestReflection.java:11) +``` +**invoke方法执行时序图** + +![](https://user-gold-cdn.xitu.io/2019/12/21/16f27996f9e5ed0f?w=1794&h=1030&f=png&s=154261) + +**我们跟着反射链路去看一下源码,先看Method的invoke方法:** + +``` +public Object invoke(Object obj, Object... args) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException +{ + //校验权限 + if (!override) { + if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { + Class caller = Reflection.getCallerClass(); + checkAccess(caller, clazz, obj, modifiers); + } + } + MethodAccessor ma = methodAccessor; // read volatile + if (ma == null) { + ma = acquireMethodAccessor(); //获取MethodAccessor + } + //返回MethodAccessor.invoke + return ma.invoke(obj, args); +} + +``` + +由上可知道,Method 的 invoke 方法,其实是返回接口MethodAccessor的invoke方法。MethodAccessor接口有三个实现类,到底调用的是哪个类的 invoke 方法呢? +![](https://user-gold-cdn.xitu.io/2019/12/21/16f2882973a9ed4b?w=1232&h=466&f=png&s=55007) + +进入acquireMethodAccessor方法,可以看到MethodAccessor由ReflectionFactory 的 newMethodAccessor方法决定。 + +![](https://user-gold-cdn.xitu.io/2019/12/21/16f288699bf4fce4?w=963&h=536&f=png&s=56491) + +再进ReflectionFactory的newMethodAccessor方法,我们可以看到返回的是DelegatingMethodAccessorImpl对象,也就是说调用的是它的invoke方法。 + +![](https://user-gold-cdn.xitu.io/2019/12/21/16f289806c3e4768?w=1975&h=416&f=png&s=63263) + +再看DelegatingMethodAccessorImpl的invoke方法 + +![](https://user-gold-cdn.xitu.io/2019/12/21/16f26933b822b50e?w=1395&h=577&f=png&s=63941) + +DelegatingMethodAccessorImpl的invoke方法返回的是MethodAccessorImpl的invoke方法,而MethodAccessorImpl的invoke方法,由它的子类NativeMethodAccessorImpl重写,这时候返回的是本地方法invoke0,如下 + +![](https://user-gold-cdn.xitu.io/2019/12/21/16f269db5ee96133?w=1925&h=762&f=png&s=113499) + +因此,Method的invoke方法,是由本地方法invoke0决定的,再底层就是c++相关了,有兴趣的朋友可以继续往下研究。 + + +## 反射的一些应用以及问题 +### 反射应用 +反射是Java框架的灵魂技术,很多框架都使用了反射技术,如spring,Mybatis,Hibernate等。 + +#### JDBC 的数据库的连接 +在JDBC连接数据库中,一般包括**加载驱动,获得数据库连接**等步骤。而加载驱动,就是引入相关Jar包后,通过**Class.forName()** 即反射技术,加载数据库的驱动程序。 + +#### Spring 框架的使用 +Spring 通过 XML 配置模式装载 Bean,也是反射的一个典型例子。 + +**装载过程:** + +- 将程序内XML 配置文件加载入内存中 +- Java类解析xml里面的内容,得到相关字节码信息 +- 使用反射机制,得到Class实例 +- 动态配置实例的属性,使用 + +**这样做当然是有好处的:** + +不用每次都去new实例了,并且可以修改配置文件,比较灵活。 + + +### 反射存在的问题 +#### 性能问题 +java反射的性能并不好,原因主要是编译器没法对反射相关的代码做优化。 +有兴趣的朋友,可以看一下这个文章[java-reflection-why-is-it-so-slow](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) + +#### 安全问题 +我们知道单例模式的设计过程中,会强调**将构造器设计为私有**,因为这样可以防止从外部构造对象。但是反射可以获取类中的域、方法、构造器,**修改访问权限**。所以这样并不一定是安全的。 + +看个例子吧,通过反射使用私有构造器实例化。 + +``` +public class Student { + private String name; + private Student(String name) { + System.out.println("我是私有构造器,我被实例化了"); + this.name = name; + } + public void doHomework(String subject) { + System.out.println("我的名字是" + name); + System.out.println("我在做"+subject+"作业"); + } +} +public class TestReflection { + public static void main(String[] args) throws Exception { + Class clazz = Class.forName("reflection.Student"); + // 获取私有构造方法对象 + Constructor constructor = clazz.getDeclaredConstructor(String.class); + // true指示反射的对象在使用时应该取消Java语言访问检查。 + constructor.setAccessible(true); + Student student = (Student) constructor.newInstance("jay@huaxiao"); + student.doHomework("数学"); + } +} +``` +**运行结果:** + +![](https://user-gold-cdn.xitu.io/2019/12/21/16f283232305ee69?w=1165&h=605&f=png&s=104011) +显然,反射不管你是不是私有,一样可以调用。 +所以,使用反射通常需要程序的运行没有**安全限制**。如果一个程序对安全性有强制要求,最好不要使用反射啦。 + +## 参考与感谢 +- [反射的实现原理](https://www.jianshu.com/p/c8de67db8adb) +- [通过反射获取私有构造方法并使用](https://blog.csdn.net/wangyanming123/article/details/51355251) +- [大白话说Java反射:入门、使用、原理](https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html) +- [设计模式之单例模式六(防反射攻击)](https://blog.csdn.net/Wenlong_L/article/details/82811996) +- [Reflection:Java反射机制的应用场景](https://segmentfault.com/a/1190000010162647) +- [深入理解Java类型信息(Class对象)与反射机制](https://blog.csdn.net/javazejian/article/details/70768369) +- 《Java编程思想》 + +## 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。 +- 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。 + + + + diff --git "a/Java\351\235\242\350\257\225\351\242\230\351\233\206\347\273\223\345\217\267/Java\351\233\206\345\220\210/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230\347\255\224\346\241\210/42. HashSet\345\222\214TreeSet\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237.md" "b/Java\351\235\242\350\257\225\351\242\230\351\233\206\347\273\223\345\217\267/Java\351\233\206\345\220\210/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230\347\255\224\346\241\210/42. HashSet\345\222\214TreeSet\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237.md" index 3ebcf40..a3ffac7 100644 --- "a/Java\351\235\242\350\257\225\351\242\230\351\233\206\347\273\223\345\217\267/Java\351\233\206\345\220\210/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230\347\255\224\346\241\210/42. HashSet\345\222\214TreeSet\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237.md" +++ "b/Java\351\235\242\350\257\225\351\242\230\351\233\206\347\273\223\345\217\267/Java\351\233\206\345\220\210/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230\347\255\224\346\241\210/42. HashSet\345\222\214TreeSet\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237.md" @@ -1,3 +1 @@ -HashSet是由一个hash表来实现的,因此,它的元素是无序的。add(),remove(),contains()方法的时间复杂度是O(1)。 -另一方面,TreeSet是由一个树形的结构来实现的,它里面的元素是有序的。因此,add(),remove(),contains()方法的时间复杂度是O(logn)。 - +160+ 50 +80 + 51 + 100 + 70+ 40+ 30 + 20 +20 +50 +20 +20 +12 +10 +25+10 +20+10+33 \ No newline at end of file diff --git "a/Java\351\235\242\350\257\225\351\242\230\351\233\206\347\273\223\345\217\267/java \345\237\272\347\241\200/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" "b/Java\351\235\242\350\257\225\351\242\230\351\233\206\347\273\223\345\217\267/java \345\237\272\347\241\200/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" index 9dfb467..e69de29 100644 --- "a/Java\351\235\242\350\257\225\351\242\230\351\233\206\347\273\223\345\217\267/java \345\237\272\347\241\200/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" +++ "b/Java\351\235\242\350\257\225\351\242\230\351\233\206\347\273\223\345\217\267/java \345\237\272\347\241\200/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" @@ -1,161 +0,0 @@ -### Java -1. equals== -2. final, finally, finalize -3. غд -4. hashCode()ͬ equals()ǷҲһΪ true -5. ͽӿʲô -6. BIONIOAIO ʲô -7. StringStringbufferStringBuilder -8. JAVAеļֻʲôռöֽ -9. ComparatorComparableʲô -10. Stringܱ̳Ϊʲô -11. ˵˵Javaж̬ʵԭ -12. JavaͺͲ -13. intInteger ʲô𣬻Integerʵ -14. ˵˵;ʵԭJavaȡַ -15. -16. &&& -17. JavaIOΪ? -18. ʵ˳򣬱縸ྲ̬ݣ캯ྲ̬ݣ캯 -19. Javaмַʽ -20. νGB2312ַתΪISO-8859-1ַأ -21. ػ߳ʲôʲôʵػ߳ -22. notify() notifyAll()ʲô -23. Javaδ쳣ģؼthrowsthrowtrycatchfinallyôʹã -24. ̸̸Java쳣νṹ -25. ̬ڲǾ̬ڲʲô -26. String snew Stringʲô -27. УClass.forNameClassLoader -28. JDK̬cglibʵֵ -29. errorexceptionCheckedExceptionRuntimeException -30. dz -31. JDK JRE ʲô -32. String ij÷Щأ -33. ̸̸Զעijʵ -34. ˵˵ϤģʽЩ -35. 󹤳͹ģʽ -36. ʲôֵݺôݣ -37. staticзʷstatic -38. Javaֶ֧̳ô,Ϊʲô -39. Чʵķ28 -40. Ƿɱд -41. charͱܴܲһĺ֣Ϊʲô -42. ʵֶ¡ -43. objectжЩ -44. hashCodeʲô -45. for-each볣forѭЧʶԱ -46. дֵģʽʵ֣ģʽͶģʽ -47. г 5 ʱ쳣 -48. 2ȵĶпܾͬ hashcode -49. ηpublic,private,protected,Լdefault -50. ̸̸finaljavaеã -51. javaеMath.round(-1.5) ڶأ -52. Stringڻ -53. νַתأ -54. ̬ļʵַʽǷֱʲôȱ -55. ԼĴУһjava.lang.String࣬ǷԱأΪʲô -56. ̸̸java.lang.ObjecthashCodeequals⡣ʲôҪʵ -57. jdk1.5У˷ͣ͵Ĵʲô⡣ -58. ʲôлôлأ -59. java8ԡ -60. ڲʲôη涨ıأ -61. breakcontinueʲô -62. String s = "Hello";s = s + " world!";дִкԭʼ String еǷı䣿 -63. GB2312ַתΪISO-8859-1ַ -64. try-catch-finally-returnִ˳ -65. Java 7µ try-with-resources䣬ƽʱʹ -66. һġԭһ򡱡 -67. switchǷbyte ϣǷlong ϣǷStringϣ -68. ûlength()Stringûlength() -69. ǷԴһ̬staticڲԷǾ̬non-staticĵã -70. String s = new String("jay");˼ַ -71. ڲǷԼ̳ࣿǷʵֽӿڣ -72. ܽintǿתΪ byte͵ıֵbyte ͵ķΧʲô -73. float f=3.4;ȷ -74. дһʽжһַǷһ -75. ReaderInputStream -76. оٳJAVA6Ƚϳõİ -77. JDK 7Щ -78. ͬ첽ʲô -79. ʵʿУJavaһʹʲô۸ -80. 64 λ JVM Уint ijǶ -81. java8 -82. ַֽ -83. Java ¼ưֱ֣¡ -84. Ϊʲôȴ֪ͨ Object Thread ģ -85. ÿ󶼿 Object Thread Ϊʲôأ -86. Ϊʲôchar Javaе String ʺϴ洢룿 -87. ʹ˫ؼ Java д̰߳ȫĵ -88. SerializableһлijԱᷢʲôνģ -89. ʲôserialVersionUID 㲻, ᷢʲô -90. Java УMaven antgradle ʲô -91. лЭЩ -92. @transactionalעʲô»ʧЧΪʲô -93. Java УDOM SAX ʲôͬ -94. ڴη䣻 -95. ʲô Busy spinΪʲôҪʹ -96. Java ôȡһ߳ dump ļ -97. ľ̬ܷд -98. ʲôDzɱ -99. ȷ˳Ƕѭ -100. SimpleDateFormat̰߳ȫ?һôʽ -101. Ҫг󷽷 -102. ôʵֶ̬ЩӦ -103. ʲôڲࣿڲ -104. extendssuper -105. ڲм֣ĿеЩӦ -106. utf-8еռֽڣintͼֽڣ -107. ˵˵Javaע -108. Java java.util.Date java.sql.Date ʲô -109. ˵һʽת -110. ʹfinal -111. һϿԭģʽ -112. Filesij÷Щ -113. Java УSerializableExternalizable -114. JavaЩ࣬ǶЩ -115. ķǷͬʱǾ̬ģ,ǷͬʱDZطǷͬʱ synchronized Σ -116. һ.javaԴļǷ԰ࣨڲࣩʲôƣ -117. ˵˵ʵԭ -118. ˽ģʽ˵˵jdkԴЩõ˵ģʽ -119. ʲôB/SܹʲôC/Sܹ -120. JavaЩƽ̨أ -121. JavaڲΪʲôԷʵⲿأ -122. Javaֵ֧ЩʲôԶװأ -123. ߳мֲͬķʽ -124. hashCode()equals()Ҫʲôط -125. ͨȡö˽ֶεֵ -126. ͨöķ -127. һ"ԭһ" -128. Java ʹʱΪʲôҪֵΪ null -129. ʲôʱöԣassert -130. AJAXΪʲôȫ -131. һJavaַежٸַ? -132. StringBuilderΪʲô̲߳ȫ -133. ¡dz¡ -134. һģʽĻԭ -135. Java ܷԶһ java.lang.System -136. Javaе쳣ʲôʲô -137. JavaExceptionErrorʲô -138. throwthrowsʲô -139. 쳣ԺExceptionᷢʲô仯 -140. ʲôRMI -141. SerializationDeserialization -142. PathClassPathʲô -143. ַͳַ -144. ConstructorǷɱoverride -145. ʲôǷķֵֵķʲô -146. һĹ췽ʲôһû췽ijȷִΪʲô -147. ̬ʵкβͬ -148. ָǵȣʲôͬ -149. Java дһ̰߳ȫĵģʽ -150. ڲǿת½һ doubleֵֵ long͵ı -151. java öǷԼ̳ (final) -152. Cloneable ӿʵԭ -153. ̳к;ۺϵ -154. JavaǾ̬дͬķDZʱ -155. ʲôJavaࣿӦóСкβͬ -156. instanceof ʹù -157. JavaԴ̳߳ж̳߳ǷѾеķʲô -158. ԱֲЩ -159. һʲô? ʵкβͬ? -160. һJavaִеģ \ No newline at end of file diff --git "a/Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\346\265\201\347\250\213\345\233\276/mysql\346\225\260\346\215\256\345\272\223\347\233\270\345\205\263\346\265\201\347\250\213\345\233\276\345\216\237\347\220\206\345\233\276.md" "b/Mysql\345\237\272\347\241\200\345\255\246\344\271\240/mysql\346\225\260\346\215\256\345\272\223\347\233\270\345\205\263\346\265\201\347\250\213\345\233\276\345\216\237\347\220\206\345\233\276.md" similarity index 100% rename from "Java\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\347\232\204\346\265\201\347\250\213\345\233\276/mysql\346\225\260\346\215\256\345\272\223\347\233\270\345\205\263\346\265\201\347\250\213\345\233\276\345\216\237\347\220\206\345\233\276.md" rename to "Mysql\345\237\272\347\241\200\345\255\246\344\271\240/mysql\346\225\260\346\215\256\345\272\223\347\233\270\345\205\263\346\265\201\347\250\213\345\233\276\345\216\237\347\220\206\345\233\276.md" diff --git "a/Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\344\270\200\346\226\207\345\275\273\345\272\225\350\257\273\346\207\202MySQL\344\272\213\345\212\241\347\232\204\345\233\233\345\244\247\351\232\224\347\246\273\347\272\247\345\210\253.md" "b/Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\344\270\200\346\226\207\345\275\273\345\272\225\350\257\273\346\207\202MySQL\344\272\213\345\212\241\347\232\204\345\233\233\345\244\247\351\232\224\347\246\273\347\272\247\345\210\253.md" new file mode 100644 index 0000000..0f1b137 --- /dev/null +++ "b/Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\344\270\200\346\226\207\345\275\273\345\272\225\350\257\273\346\207\202MySQL\344\272\213\345\212\241\347\232\204\345\233\233\345\244\247\351\232\224\347\246\273\347\272\247\345\210\253.md" @@ -0,0 +1,473 @@ +## 前言 +之前分析一个死锁问题,发现自己对数据库隔离级别理解还不够清楚,所以趁着这几天假期,整理一下MySQL事务的四大隔离级别相关知识,希望对大家有帮助~ + + +![](https://user-gold-cdn.xitu.io/2020/4/5/171498a008c91b4c?w=985&h=620&f=png&s=57841) + +## 事务 +### 什么是事务? +事务,由一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。 +> 假如A转账给B 100 元,先从A的账户里扣除 100 元,再在 B 的账户上加上 100 元。如果扣完A的100元后,还没来得及给B加上,银行系统异常了,最后导致A的余额减少了,B的余额却没有增加。所以就需要事务,将A的钱回滚回去,就是这么简单。 + +### 事务的四大特性 +![](https://user-gold-cdn.xitu.io/2020/3/30/1712b5213446a402?w=626&h=466&f=png&s=127652) +- **原子性:** 事务作为一个整体被执行,包含在其中的对数据库的操作要么全部都执行,要么都不执行。 +- **一致性:** 指在事务开始之前和事务结束以后,数据不会被破坏,假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的。 +- **隔离性:** 多个事务并发访问时,事务之间是相互隔离的,一个事务不应该被其他事务干扰,多个并发事务之间要相互隔离。。 +- **持久性:** 表示事务完成提交后,该事务对数据库所作的操作更改,将持久地保存在数据库之中。 + +## 事务并发存在的问题 +事务并发执行存在什么问题呢,换句话说就是,一个事务是怎么干扰到其他事务的呢?看例子吧~ + +假设现在有表: +``` +CREATE TABLE `account` ( + `id` int(11) NOT NULL, + `name` varchar(255) DEFAULT NULL, + `balance` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `un_name_idx` (`name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` +表中有数据: + +![](https://user-gold-cdn.xitu.io/2020/4/2/171381960fd0f119?w=1368&h=316&f=png&s=35205) + + +### 脏读(dirty read) + +假设现在有两个事务A、B: +- 假设现在A的余额是100,事务A正在准备查询Jay的余额 +- 这时候,事务B先扣减Jay的余额,扣了10 +- 最后A 读到的是扣减后的余额 + +![](https://user-gold-cdn.xitu.io/2020/4/2/1713824c77723cd4?w=2249&h=488&f=png&s=119766) + +由上图可以发现,事务A、B交替执行,事务A被事务B干扰到了,因为事务A读取到事务B未提交的数据,这就是**脏读**。 + +### 不可重复读(unrepeatable read) +假设现在有两个事务A和B: +- 事务A先查询Jay的余额,查到结果是100 +- 这时候事务B 对Jay的账户余额进行扣减,扣去10后,提交事务 +- 事务A再去查询Jay的账户余额发现变成了90 + + +![](https://user-gold-cdn.xitu.io/2020/4/2/1713829b86401900?w=2205&h=744&f=png&s=179865) + +事务A又被事务B干扰到了!在事务A范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是**不可重复读**。 + +### 幻读 + +假设现在有两个事务A、B: +- 事务A先查询id大于2的账户记录,得到记录id=2和id=3的两条记录 +- 这时候,事务B开启,插入一条id=4的记录,并且提交了 +- 事务A再去执行相同的查询,却得到了id=2,3,4的3条记录了。 + +![](https://user-gold-cdn.xitu.io/2020/4/2/171382b2bdd28029?w=2239&h=688&f=png&s=158963) + +事务A查询一个范围的结果集,另一个并发事务B往这个范围中插入/删除了数据,并静悄悄地提交,然后事务A再次查询相同的范围,两次读取得到的结果集不一样了,这就是**幻读**。 + +## 事务的四大隔离级别实践 + +既然并发事务存在**脏读、不可重复、幻读**等问题,InnoDB实现了哪几种事务的隔离级别应对呢? +- 读未提交(Read Uncommitted) +- 读已提交(Read Committed) +- 可重复读(Repeatable Read) +- 串行化(Serializable) + +### 读未提交(Read Uncommitted) +想学习一个知识点,最好的方式就是实践之。好了,我们去数据库给它设置**读未提交**隔离级别,实践一下吧~ + +![](https://user-gold-cdn.xitu.io/2020/4/5/17148fd0f161aea8?w=1240&h=606&f=png&s=90024) + +先把事务隔离级别设置为read uncommitted,开启事务A,查询id=1的数据 +``` +set session transaction isolation level read uncommitted; +begin; +select * from account where id =1; +``` +结果如下: + +![](https://user-gold-cdn.xitu.io/2020/4/5/17148f9415ffc166?w=538&h=215&f=png&s=15480) +这时候,另开一个窗口打开mysql,也把当前事务隔离级别设置为read uncommitted,开启事务B,执行更新操作 +``` +set session transaction isolation level read uncommitted; +begin; +update account set balance=balance+20 where id =1; +``` +接着回事务A的窗口,再查account表id=1的数据,结果如下: + +![](https://user-gold-cdn.xitu.io/2020/4/5/17148faf1e5d5f48?w=520&h=162&f=png&s=10474) + +可以发现,在**读未提交(Read Uncommitted)** 隔离级别下,一个事务会读到其他事务未提交的数据的,即存在**脏读**问题。事务B都还没commit到数据库呢,事务A就读到了,感觉都乱套了。。。实际上,读未提交是隔离级别最低的一种。 + +### 已提交读(READ COMMITTED) +为了避免脏读,数据库有了比**读未提交**更高的隔离级别,即**已提交读**。 + +![](https://user-gold-cdn.xitu.io/2020/4/5/17148c908b12084b?w=1394&h=655&f=png&s=104468) + +把当前事务隔离级别设置为已提交读(READ COMMITTED),开启事务A,查询account中id=1的数据 +``` +set session transaction isolation level read committed; +begin; +select * from account where id =1; +``` + +另开一个窗口打开mysql,也把事务隔离级别设置为read committed,开启事务B,执行以下操作 + +``` +set session transaction isolation level read committed; +begin; +update account set balance=balance+20 where id =1; +``` +接着回事务A的窗口,再查account数据,发现数据没变: + +![](https://user-gold-cdn.xitu.io/2020/4/3/1713d69e118832d2?w=408&h=154&f=png&s=9410) + +我们再去到事务B的窗口执行commit操作: + +``` +commit; +``` +最后回到事务A窗口查询,发现数据变了: + +![](https://user-gold-cdn.xitu.io/2020/4/3/1713d68ad5a2fd47?w=436&h=153&f=png&s=9828) + +由此可以得出结论,隔离级别设置为**已提交读(READ COMMITTED)** 时,已经不会出现脏读问题了,当前事务只能读取到其他事务提交的数据。但是,你站在事务A的角度想想,存在其他问题吗? + +**提交读的隔离级别会有什么问题呢?** + +在同一个事务A里,相同的查询sql,读取同一条记录(id=1),读到的结果是不一样的,即**不可重复读**。所以,隔离级别设置为read committed的时候,还会存在**不可重复读**的并发问题。 + + +### 可重复读(Repeatable Read) +如果你的老板要求,在同个事务中,查询结果必须是一致的,即老板要求你解决不可重复的并发问题,怎么办呢?老板,臣妾办不到?来实践一下**可重复读(Repeatable Read)** 这个隔离级别吧~ + +![](https://user-gold-cdn.xitu.io/2020/4/4/17144b324064255a?w=1340&h=616&f=png&s=87958) + +哈哈,步骤1、2、6的查询结果都是一样的,即**repeatable read解决了不可重复读问题**,是不是心里美滋滋的呢,终于解决老板的难题了~ + +**RR级别是否解决了幻读问题呢?** + +再来看看网上的一个热点问题,有关于RR级别下,是否解决了幻读问题?我们来实践一下: + +![](https://user-gold-cdn.xitu.io/2020/4/5/1714911d7b42c350?w=1518&h=615&f=png&s=102713) +由图可得,步骤2和步骤6查询结果集没有变化,看起来RR级别是已经解决幻读问题了~ +但是呢,**RR级别还是存在这种现象**: +![](https://user-gold-cdn.xitu.io/2020/4/4/17142571375bd709?w=1348&h=715&f=png&s=118174) + +其实,上图如果事务A中,没有```update account set balance=200 where id=5;```这步操作,```select * from account where id>2```查询到的结果集确实是不变,这种情况没有**幻读**问题。但是,有了update这个骚操作,同一个事务,相同的sql,查出的结果集不同,这个是符合了**幻读**的定义~ + +这个问题,亲爱的朋友,你觉得它算幻读问题吗? + +### 串行化(Serializable) +前面三种数据库隔离级别,都有一定的并发问题,现在放大招吧,实践SERIALIZABLE隔离级别。 + +把事务隔离级别设置为Serializable,开启事务A,查询account表数据 +``` +set session transaction isolation level serializable; +select @@tx_isolation; +begin; +select * from account; +``` +另开一个窗口打开mysql,也把事务隔离级别设置为Serializable,开启事务B,执行插入一条数据: +``` +set session transaction isolation level serializable; +select @@tx_isolation; +begin; +insert into account(id,name,balance) value(6,'Li',100); +``` +执行结果如下: + +![](https://user-gold-cdn.xitu.io/2020/4/4/1714282f3cb7f7fa?w=1444&h=637&f=png&s=104234) + +由图可得,当数据库隔离级别设置为serializable的时候,事务B对表的写操作,在等事务A的读操作。其实,这是隔离级别中最严格的,读写都不允许并发。它保证了最好的安全性,性能却是个问题~ + +## MySql隔离级别的实现原理 + +实现隔离机制的方法主要有两种: +- 读写锁 +- 一致性快照读,即 MVCC + +MySql使用不同的锁策略(Locking Strategy)/MVCC来实现四种不同的隔离级别。RR、RC的实现原理跟MVCC有关,RU和Serializable跟锁有关。 + +### 读未提交(Read Uncommitted) + +**官方说法:** +> SELECT statements are performed in a nonlocking fashion, but a possible earlier version of a row might be used. Thus, using this isolation level, such reads are not consistent. + +读未提交,采取的是读不加锁原理。 +- 事务读不加锁,不阻塞其他事务的读和写 +- 事务写阻塞其他事务写,但不阻塞其他事务读; + +### 串行化(Serializable) + +**官方的说法:** +> InnoDB implicitly converts all plain SELECT statements to SELECT ... FOR SHARE if autocommit is disabled. If autocommit is enabled, the SELECT is its own transaction. It therefore is known to be read only and can be serialized if performed as a consistent (nonlocking) read and need not block for other transactions. (To force a plain SELECT to block if other transactions have modified the selected rows, disable autocommit.) + +- 所有SELECT语句会隐式转化为```SELECT ... FOR SHARE```,即加共享锁。 +- 读加共享锁,写加排他锁,读写互斥。如果有未提交的事务正在修改某些行,所有select这些行的语句都会阻塞。 + +### MVCC的实现原理 + +MVCC,中文叫**多版本并发控制**,它是通过读取历史版本的数据,来降低并发事务冲突,从而提高并发性能的一种机制。它的实现依赖于**隐式字段、undo日志、快照读&当前读、Read View**,因此,我们先来了解这几个知识点。 + +#### 隐式字段 + +对于InnoDB存储引擎,每一行记录都有两个隐藏列**DB_TRX_ID、DB_ROLL_PTR**,如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列**DB_ROW_ID**。 +- DB_TRX_ID,记录每一行最近一次修改(修改/更新)它的事务ID,大小为6字节; +- DB_ROLL_PTR,这个隐藏列就相当于一个指针,指向回滚段的undo日志,大小为7字节; +- DB_ROW_ID,单调递增的行ID,大小为6字节; + +![](https://user-gold-cdn.xitu.io/2020/4/5/1714794c14a7e14f?w=1250&h=144&f=png&s=13429) + +#### undo日志 + +> - 事务未提交的时候,修改数据的镜像(修改前的旧版本),存到undo日志里。以便事务回滚时,恢复旧版本数据,撤销未提交事务数据对数据库的影响。 +>- undo日志是逻辑日志。可以这样认为,当delete一条记录时,undo log中会记录一条对应的insert记录,当update一条记录时,它记录一条对应相反的update记录。 +> - 存储undo日志的地方,就是**回滚段**。 + +多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(DB_ROLL_PTR)连一条**Undo日志链**。 + +我们通过例子来看一下~ +``` +mysql> select * from account ; ++----+------+---------+ +| id | name | balance | ++----+------+---------+ +| 1 | Jay | 100 | ++----+------+---------+ +1 row in set (0.00 sec) +``` +- 假设表accout现在只有一条记录,插入该该记录的事务Id为100 +- 如果事务B(事务Id为200),对id=1的该行记录进行更新,把balance值修改为90 + +事务B修改后,形成的**Undo Log链**如下: + +![](https://user-gold-cdn.xitu.io/2020/4/5/171478d1a4a873d6?w=1107&h=345&f=png&s=29689) + +#### 快照读&当前读 + +**快照读:** + +读取的是记录数据的可见版本(有旧的版本),不加锁,普通的select语句都是快照读,如: +``` +select * from account where id>2; +``` + +**当前读:** + +读取的是记录数据的最新版本,显示加锁的都是当前读 +``` +select * from account where id>2 lock in share mode; +select * from account where id>2 for update; +``` + +#### Read View +- Read View就是事务执行**快照读**时,产生的读视图。 +- 事务执行快照读时,会生成数据库系统当前的一个快照,记录当前系统中还有哪些活跃的读写事务,把它们放到一个列表里。 +- Read View主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据~ + +为了下面方便讨论Read View可见性规则,先定义几个变量 +> - m_ids:当前系统中那些活跃的读写事务ID,它数据结构为一个List。 +> - min_limit_id:m_ids事务列表中,最小的事务ID +> - max_limit_id:m_ids事务列表中,最大的事务ID + +- 如果DB_TRX_ID < min_limit_id,表明生成该版本的事务在生成ReadView前已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。 +- 如果DB_TRX_ID > m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。 +- 如果 min_limit_id =2的所有记录。 +- 再开启事务B,插入id=5的一条数据。 +- 事务B插入数据成功后,再修改id=3的记录 +- 回到事务A,再次执行id>2的当前读查询 + +![](https://user-gold-cdn.xitu.io/2020/4/5/171496a6382f56e7?w=1640&h=601&f=png&s=120697) +- 事务B可以插入id=5的数据,却更新不了id=3的数据,陷入阻塞。证明事务A在执行当前读的时候在id =3和id=4这两条记录上加了锁,但是并没有对 id > 2 这个范围加锁~ +- 事务B陷入阻塞后,切回事务A执行当前读操作时,死锁出现。因为事务B在 insert 的时候,会在新纪录(id=5)上加锁,所以事务A再次执行当前读,想获取id> 3 的记录,就需要在 id=3,4,5 这3条记录上加锁,但是 id = 5这条记录已经被事务B 锁住了,于是事务A被事务B阻塞,同时事务B还在等待 事务A释放 id = 3上的锁,最终产生了死锁。 + +![](https://user-gold-cdn.xitu.io/2020/4/5/171497ae540bdebf?w=1800&h=955&f=png&s=195481) + +因此,我们可以发现,RC隔离级别下,加锁的select, update, delete等语句,使用的是记录锁,其他事务的插入依然可以执行,因此会存在幻读~ + +### RR 级别解决幻读分析 +因为RR是解决幻读问题的,怎么解决的呢,分析一波吧~ + +假设account表有4条数据,RR级别。 +- 开启事务A,执行当前读,查询id>2的所有记录。 +- 再开启事务B,插入id=5的一条数据。 +![](https://user-gold-cdn.xitu.io/2020/4/5/17149563ecc072ba?w=1515&h=652&f=png&s=106753) +可以发现,事务B执行插入操作时,阻塞了~因为事务A在执行select ... lock in share mode的时候,不仅在 id = 3,4 这2条记录上加了锁,而且在id > 2 这个范围上也加了间隙锁。 + +因此,我们可以发现,RR隔离级别下,加锁的select, update, delete等语句,会使用间隙锁+ 临键锁,锁住索引记录之间的范围,避免范围间插入记录,以避免产生幻影行记录。 + +## 参考与感谢 +- [ 解决死锁之路 - 学习事务与隔离级别](https://www.aneasystone.com/archives/2017/10/solving-dead-locks-one.html) +- [五分钟搞清楚MySQL事务隔离级别](https://www.jianshu.com/p/4e3edbedb9a8) +- [4种事务的隔离级别,InnoDB如何巧妙实现?](https://mp.weixin.qq.com/s/x_7E2R2i27Ci5O7kLQF0UA) +- [MySQL事务隔离级别和MVCC](https://juejin.im/post/5c9b1b7df265da60e21c0b57#heading-6) +- [MySQL InnoDB MVCC 机制的原理及实现](https://chenjiayang.me/2019/06/22/mysql-innodb-mvcc/) +- [MVCC多版本并发控制](https://www.jianshu.com/p/8845ddca3b23) + +## 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 觉得写得好的小伙伴给个点赞+关注啦,谢谢~ +- 如果有写得不正确的地方,麻烦指出,感激不尽。 +- 同时非常期待小伙伴们能够关注我公众号,后面慢慢推出更好的干货~嘻嘻 +- github地址:https://github.com/whx123/JavaHome diff --git "a/Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\344\271\246\345\206\231\351\253\230\350\264\250\351\207\217SQL\347\232\20430\346\235\241\345\273\272\350\256\256.md" "b/Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\344\271\246\345\206\231\351\253\230\350\264\250\351\207\217SQL\347\232\20430\346\235\241\345\273\272\350\256\256.md" new file mode 100644 index 0000000..4f7e78a --- /dev/null +++ "b/Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\344\271\246\345\206\231\351\253\230\350\264\250\351\207\217SQL\347\232\20430\346\235\241\345\273\272\350\256\256.md" @@ -0,0 +1,603 @@ +### 前言 +本文将结合实例demo,阐述30条有关于优化SQL的建议,多数是实际开发中总结出来的,希望对大家有帮助。 + +### 1、查询SQL尽量不要使用select *,而是select具体字段。 + +反例子: +``` +select * from employee; +``` + +正例子: + +``` +select id,name from employee; +``` + +理由: +- 只取需要的字段,节省资源、减少网络开销。 +- select * 进行查询时,很可能就不会使用到覆盖索引了,就会造成回表查询。 + +### 2、如果知道查询结果只有一条或者只要最大/最小一条记录,建议用limit 1 +假设现在有employee员工表,要找出一个名字叫jay的人. +``` +CREATE TABLE `employee` ( + `id` int(11) NOT NULL, + `name` varchar(255) DEFAULT NULL, + `age` int(11) DEFAULT NULL, + `date` datetime DEFAULT NULL, + `sex` int(1) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` +反例: + +``` +select id,name from employee where name='jay' +``` +正例 +``` +select id,name from employee where name='jay' limit 1; +``` +理由: +- 加上limit 1后,只要找到了对应的一条记录,就不会继续向下扫描了,效率将会大大提高。 +- 当然,如果name是唯一索引的话,是不必要加上limit 1了,因为limit的存在主要就是为了防止全表扫描,从而提高性能,如果一个语句本身可以预知不用全表扫描,有没有limit ,性能的差别并不大。 + +### 3、应尽量避免在where子句中使用or来连接条件 +新建一个user表,它有一个普通索引userId,表结构如下: + +``` +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `userId` int(11) NOT NULL, + `age` int(11) NOT NULL, + `name` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_userId` (`userId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` +假设现在需要查询userid为1或者年龄为18岁的用户,很容易有以下sql + +反例: + +``` +select * from user where userid=1 or age =18 +``` + +正例: + +``` +//使用union all +select * from user where userid=1 +union all +select * from user where age = 18 + +//或者分开两条sql写: +select * from user where userid=1 +select * from user where age = 18 +``` +理由: +- 使用or可能会使索引失效,从而全表扫描。 +>对于or+没有索引的age这种情况,假设它走了userId的索引,但是走到age查询条件时,它还得全表扫描,也就是需要三步过程: 全表扫描+索引扫描+合并 +如果它一开始就走全表扫描,直接一遍扫描就完事。 +mysql是有优化器的,处于效率与成本考虑,遇到or条件,索引可能失效,看起来也合情合理。 + +### 4、优化limit分页 +我们日常做分页需求时,一般会用 limit 实现,但是当偏移量特别大的时候,查询效率就变得低下。 + +反例: + +``` +select id,name,age from employee limit 10000,10 +``` +正例: + +``` +//方案一 :返回上次查询的最大记录(偏移量) +select id,name from employee where id>10000 limit 10. + +//方案二:order by + 索引 +select id,name from employee order by id limit 10000,10 + +//方案三:在业务允许的情况下限制页数: +``` + +理由: +- 当偏移量最大的时候,查询效率就会越低,因为Mysql并非是跳过偏移量直接去取后面的数据,而是先把偏移量+要取的条数,然后再把前面偏移量这一段的数据抛弃掉再返回的。 +- 如果使用优化方案一,返回上次最大查询记录(偏移量),这样可以跳过偏移量,效率提升不少。 +- 方案二使用order by+索引,也是可以提高查询效率的。 +- 方案三的话,建议跟业务讨论,有没有必要查这么后的分页啦。因为绝大多数用户都不会往后翻太多页。 + + +### 5、优化你的like语句 +日常开发中,如果用到模糊关键字查询,很容易想到like,但是like很可能让你的索引失效。 + +反例: + +``` +select userId,name from user where userId like '%123'; +``` +正例: + +``` +select userId,name from user where userId like '123%'; +``` + +理由: +- 把%放前面,并不走索引,如下: +![](https://user-gold-cdn.xitu.io/2020/3/20/170f7f2739040e5b?w=1280&h=330&f=png&s=129628) +- 把% 放关键字后面,还是会走索引的。如下: +![](https://user-gold-cdn.xitu.io/2020/3/20/170f7f224aa3dfed?w=1280&h=314&f=png&s=126373) + +### 6、使用where条件限定要查询的数据,避免返回多余的行 +假设业务场景是这样:查询某个用户是否是会员。曾经看过老的实现代码是这样。。。 + +反例: + +``` +List userIds = sqlMap.queryList("select userId from user where isVip=1"); +boolean isVip = userIds.contains(userId); +``` +正例: + +``` +Long userId = sqlMap.queryObject("select userId from user where userId='userId' and isVip='1' ") +boolean isVip = userId!=null; +``` + +理由: +- 需要什么数据,就去查什么数据,避免返回不必要的数据,节省开销。 + +### 7、尽量避免在索引列上使用mysql的内置函数 +业务需求:查询最近七天内登陆过的用户(假设loginTime加了索引) + +反例: + +``` +select userId,loginTime from loginuser where Date_ADD(loginTime,Interval 7 DAY) >=now(); +``` +正例: + +``` +explain select userId,loginTime from loginuser where loginTime >= Date_ADD(NOW(),INTERVAL - 7 DAY); +``` +理由: +- 索引列上使用mysql的内置函数,索引失效 + +![](https://user-gold-cdn.xitu.io/2020/3/21/170fd5f19265afa9?w=1179&h=278&f=png&s=28534) + +- 如果索引列不加内置函数,索引还是会走的。 +![](https://user-gold-cdn.xitu.io/2020/3/20/170f875955e8b7c0?w=1201&h=290&f=png&s=28564) + + +### 8、应尽量避免在 where 子句中对字段进行表达式操作,这将导致系统放弃使用索引而进行全表扫 +反例: + +``` +select * from user where age-1 =10; +``` +正例: + +``` +select * from user where age =11; +``` + +理由: +- 虽然age加了索引,但是因为对它进行运算,索引直接迷路了。。。 +![](https://user-gold-cdn.xitu.io/2020/3/20/170f85b4a47dc153?w=1280&h=265&f=png&s=118891) + +### 9、Inner join 、left join、right join,优先使用Inner join,如果是left join,左边表结果尽量小 + +>- Inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集 +>- left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。 +>- right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。 + +都满足SQL需求的前提下,推荐优先使用Inner join(内连接),如果要使用left join,左边表数据结果尽量小,如果有条件的尽量放到左边处理。 + +反例: + +``` +select * from tab1 t1 left join tab2 t2 on t1.size = t2.size where t1.id>2; +``` +正例: + +``` +select * from (select * from tab1 where id >2) t1 left join tab2 t2 on t1.size = t2.size; +``` +理由: +- 如果inner join是等值连接,或许返回的行数比较少,所以性能相对会好一点。 +- 同理,使用了左连接,左边表数据结果尽量小,条件尽量放到左边处理,意味着返回的行数可能比较少。 + +### 10、应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。 +反例: +``` +select age,name from user where age <>18; +``` +正例: + +``` +//可以考虑分开两条sql写 +select age,name from user where age <18; +select age,name from user where age >18; +``` +理由: +- 使用!=和<>很可能会让索引失效 + +![](https://user-gold-cdn.xitu.io/2020/3/21/170f8d5a32598527?w=1124&h=275&f=png&s=23714) + + +### 11、使用联合索引时,注意索引列的顺序,一般遵循最左匹配原则。 +表结构:(有一个联合索引idx_userid_age,userId在前,age在后) + +``` +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `userId` int(11) NOT NULL, + `age` int(11) DEFAULT NULL, + `name` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_userid_age` (`userId`,`age`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; + +``` +反例: + +``` +select * from user where age = 10; +``` +![](https://user-gold-cdn.xitu.io/2020/3/21/170fabb3abde4936?w=1280&h=179&f=png&s=110711) + +正例: + +``` +//符合最左匹配原则 +select * from user where userid=10 and age =10; +//符合最左匹配原则 +select * from user where userid =10; +``` + +![](https://user-gold-cdn.xitu.io/2020/3/21/170fda546249a690?w=1318&h=289&f=png&s=26076) +![](https://user-gold-cdn.xitu.io/2020/3/21/170fabd29ed198a8?w=1352&h=244&f=png&s=24752) +理由: +- 当我们创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。 +- 联合索引不满足最左原则,索引一般会失效,但是这个还跟Mysql优化器有关的。 + +### 12、对查询进行优化,应考虑在 where 及 order by 涉及的列上建立索引,尽量避免全表扫描。 + +反例: + +``` +select * from user where address ='深圳' order by age ; +``` + +![](https://user-gold-cdn.xitu.io/2020/3/21/170fac5e1650f9b4?w=1090&h=275&f=png&s=28394) +正例: + +``` +添加索引 +alter table user add index idx_address_age (address,age) +``` + +![](https://user-gold-cdn.xitu.io/2020/3/21/170facab45b2d9a6?w=1064&h=246&f=png&s=30933) + +### 13、如果插入数据过多,考虑批量插入。 + +反例: + +``` +for(User u :list){ + INSERT into user(name,age) values(#name#,#age#) +} +``` +正例: + +``` +//一次500批量插入,分批进行 +insert into user(name,age) values + + (#{item.name},#{item.age}) + +``` +理由: +- 批量插入性能好,更加省时间 +>打个比喻:假如你需要搬一万块砖到楼顶,你有一个电梯,电梯一次可以放适量的砖(最多放500),你可以选择一次运送一块砖,也可以一次运送500,你觉得哪个时间消耗大? + +### 14、在适当的时候,使用覆盖索引。 +覆盖索引能够使得你的SQL语句不需要回表,仅仅访问索引就能够得到所有需要的数据,大大提高了查询效率。 + +反例: +``` +// like模糊查询,不走索引了 +select * from user where userid like '%123%' +``` + +![](https://user-gold-cdn.xitu.io/2020/3/21/170fb02be8584b0a?w=1137&h=294&f=png&s=24494) +正例: + +``` +//id为主键,那么为普通索引,即覆盖索引登场了。 +select id,name from user where userid like '%123%'; +``` +![](https://user-gold-cdn.xitu.io/2020/3/21/170fafe4a0d3d5e6?w=1488&h=353&f=png&s=30036) + +### 15、慎用distinct关键字 +distinct 关键字一般用来过滤重复记录,以返回不重复的记录。在查询一个字段或者很少字段的情况下使用时,给查询带来优化效果。但是在字段很多的时候使用,却会大大降低查询效率。 + +反例: + +``` +SELECT DISTINCT * from user; +``` +正例: +``` +select DISTINCT name from user; +``` + +理由: +- 带distinct的语句cpu时间和占用时间都高于不带distinct的语句。因为当查询很多字段时,如果使用distinct,数据库引擎就会对数据进行比较,过滤掉重复数据,然而这个比较,过滤的过程会占用系统资源,cpu时间。 + + +### 16、删除冗余和重复索引 +反例: + +``` + KEY `idx_userId` (`userId`) + KEY `idx_userId_age` (`userId`,`age`) +``` +正例: + +``` + //删除userId索引,因为组合索引(A,B)相当于创建了(A)和(A,B)索引 + KEY `idx_userId_age` (`userId`,`age`) +``` +理由: +- 重复的索引需要维护,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能的。 + +### 17、如果数据量较大,优化你的修改/删除语句。 +避免同时修改或删除过多数据,因为会造成cpu利用率过高,从而影响别人对数据库的访问。 + +反例: + +``` +//一次删除10万或者100万+? +delete from user where id <100000; +//或者采用单一循环操作,效率低,时间漫长 +for(User user:list){ + delete from user; +} +``` +正例: + +``` +//分批进行删除,如每次500 +delete user where id<500 +delete product where id>=500 and id<1000; +``` +理由: +- 一次性删除太多数据,可能会有lock wait timeout exceed的错误,所以建议分批操作。 + +### 18、where子句中考虑使用默认值代替null。 + +反例: + +``` +select * from user where age is not null; +``` +![](https://user-gold-cdn.xitu.io/2020/3/21/170fbaec810f084f?w=1176&h=215&f=png&s=22551) +正例: +``` +//设置0为默认值 +select * from user where age>0; +``` + +![](https://user-gold-cdn.xitu.io/2020/3/21/170fbb088234cc77?w=1190&h=225&f=png&s=23147) +理由: +- 并不是说使用了is null 或者 is not null 就会不走索引了,这个跟mysql版本以及查询成本都有关。 +> 如果mysql优化器发现,走索引比不走索引成本还要高,肯定会放弃索引,这些条件```!=,>is null,is not null```经常被认为让索引失效,其实是因为一般情况下,查询的成本高,优化器自动放弃的。 +- 如果把null值,换成默认值,很多时候让走索引成为可能,同时,表达意思会相对清晰一点。 + +### 19、不要有超过5个以上的表连接 + +- 连表越多,编译的时间和开销也就越大。 +- 把连接表拆开成较小的几个执行,可读性更高。 +- 如果一定需要连接很多表才能得到数据,那么意味着糟糕的设计了。 + +### 20、exist & in的合理利用 + +假设表A表示某企业的员工表,表B表示部门表,查询所有部门的所有员工,很容易有以下SQL: + +``` +select * from A where deptId in (select deptId from B); +``` +这样写等价于: +> 先查询部门表B +> +> select deptId from B +> +> 再由部门deptId,查询A的员工 +> +> select * from A where A.deptId = B.deptId + +可以抽象成这样的一个循环: + +``` + List<> resultSet ; + for(int i=0;i select * from A,先从A表做循环 +> +> select * from B where A.deptId = B.deptId,再从B表做循环. + +同理,可以抽象成这样一个循环: +``` + List<> resultSet ; + for(int i=0;i= Date_sub(now(),Interval 1 Y) +``` +正例: + +``` +//分页查询 +select * from LivingInfo where watchId =useId and watchTime>= Date_sub(now(),Interval 1 Y) limit offset,pageSize + +//如果是前端分页,可以先查询前两百条记录,因为一般用户应该也不会往下翻太多页, +select * from LivingInfo where watchId =useId and watchTime>= Date_sub(now(),Interval 1 Y) limit 200 ; +``` + +### 26、当在SQL语句中连接多个表时,请使用表的别名,并把别名前缀于每一列上,这样语义更加清晰。 + +反例: +``` +select * from A inner +join B on A.deptId = B.deptId; +``` +正例: +``` +select memeber.name,deptment.deptName from A member inner +join B deptment on member.deptId = deptment.deptId; +``` + +### 27、尽可能使用varchar/nvarchar 代替 char/nchar。 +反例: +``` + `deptName` char(100) DEFAULT NULL COMMENT '部门名称' +``` +正例: + +``` + `deptName` varchar(100) DEFAULT NULL COMMENT '部门名称' +``` +理由: +- 因为首先变长字段存储空间小,可以节省存储空间。 +- 其次对于查询来说,在一个相对较小的字段内搜索,效率更高。 + +### 28、为了提高group by 语句的效率,可以在执行到该语句前,把不需要的记录过滤掉。 +反例: +``` +select job,avg(salary) from employee group by job having job ='president' +or job = 'managent' +``` +正例: +``` +select job,avg(salary) from employee where job ='president' +or job = 'managent' group by job; +``` + +### 29、如何字段类型是字符串,where时一定用引号括起来,否则索引失效 + +反例: + +``` +select * from user where userid =123; +``` + +![](https://user-gold-cdn.xitu.io/2020/3/21/170fd45766e57cdc?w=1280&h=279&f=png&s=119169) +正例: + +``` +select * from user where userid ='123'; +``` + +![](https://user-gold-cdn.xitu.io/2020/3/21/170fd46de786dce3?w=1280&h=296&f=png&s=135401) +理由: +- 为什么第一条语句未加单引号就不走索引了呢? 这是因为不加单引号时,是字符串跟数字的比较,它们类型不匹配,MySQL会做隐式的类型转换,把它们转换为浮点数再做比较。 + +### 30、使用explain 分析你SQL的计划 + +日常开发写SQL的时候,尽量养成一个习惯吧。用explain分析一下你写的SQL,尤其是走不走索引这一块。 + +``` +explain select * from user where userid =10086 or age =18; +``` + +![](https://user-gold-cdn.xitu.io/2020/3/21/170fd29c57512897?w=1140&h=188&f=png&s=22140) + +### 参考与感谢 +- [Mysql优化原则_小表驱动大表IN和EXISTS的合理利用](https://segmentfault.com/a/1190000014509559) +- [sql语句的优化分析](https://www.cnblogs.com/knowledgesea/p/3686105.html) + +### 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 觉得写得好的小伙伴给个点赞+关注啦,谢谢~ +- 如果有写得不正确的地方,麻烦指出,感激不尽。 +- 同时非常期待小伙伴们能够关注我公众号,后面慢慢推出更好的干货~嘻嘻 +- github地址:https://github.com/whx123/JavaHome \ No newline at end of file diff --git "a/Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\345\220\216\347\253\257\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\357\274\232\347\264\242\345\274\225\345\244\261\346\225\210\347\232\204\345\215\201\345\244\247\346\235\202\347\227\207.md" "b/Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\345\220\216\347\253\257\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\357\274\232\347\264\242\345\274\225\345\244\261\346\225\210\347\232\204\345\215\201\345\244\247\346\235\202\347\227\207.md" new file mode 100644 index 0000000..598075f --- /dev/null +++ "b/Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\345\220\216\347\253\257\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\357\274\232\347\264\242\345\274\225\345\244\261\346\225\210\347\232\204\345\215\201\345\244\247\346\235\202\347\227\207.md" @@ -0,0 +1,291 @@ +### 背景 +最近生产爆出一条慢sql,原因是用了or和!=,导致索引失效。于是,总结了索引失效的十大杂症,希望对大家有帮助,加油。 + + +### 一、查询条件包含or,可能导致索引失效 + +新建一个user表,它有一个普通索引userId,结构如下: +``` +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `userId` int(11) NOT NULL, + `age` int(11) NOT NULL, + `name` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_userId` (`userId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` + +1. 执行一条查询sql,它是会走索引的,如下图所示: +![](https://user-gold-cdn.xitu.io/2019/12/7/16ee05205553eca8?w=1171&h=274&f=png&s=23198) +2. 把or条件+没有索引的age加上,并不会走索引,如图: +![](https://user-gold-cdn.xitu.io/2019/12/7/16ee05890dadd492?w=1176&h=268&f=png&s=24193) + +**分析&结论:** + +- 对于or+没有索引的age这种情况,假设它走了userId的索引,但是走到age查询条件时,它还得全表扫描,也就是需要三步过程: 全表扫描+索引扫描+合并 +- 如果它一开始就走全表扫描,直接一遍扫描就完事。 +- mysql是有优化器的,处于效率与成本,遇到or条件,索引可能失效,看起来也合情合理。 + +**注意:** 如果or条件的列都加了索引,索引可能会走的,大家可以自己试一试。 + + +### 二、如何字段类型是字符串,where时一定用引号括起来,否则索引失效 + +假设demo表结构如下: +``` +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `userId` varchar(32) NOT NULL, + `name` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_userId` (`userId`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; +``` + +userId为字符串类型,是B+树的普通索引,如果查询条件传了一个数字过去,它是不走索引的,如图所示: +![](https://user-gold-cdn.xitu.io/2019/12/7/16ee09af4fcea760?w=1365&h=297&f=png&s=26114) + +如果给数字加上'',也就是传一个字符串呢,当然是走索引,如下图: + +![](https://user-gold-cdn.xitu.io/2019/12/7/16ee09fb8187b744?w=1329&h=307&f=png&s=26008) + +**分析与结论:** + +**为什么第一条语句未加单引号就不走索引了呢?** +这是因为不加单引号时,是字符串跟数字的比较,它们类型不匹配,MySQL会做**隐式的类型转换**,把它们转换为浮点数再做比较。 + + +### 三、like通配符可能导致索引失效。 +并不是用了like通配符,索引一定失效,而是like查询是以%开头,才会导致索引失效。 + +表结构: +``` +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `userId` varchar(32) NOT NULL, + `name` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_userId` (`userId`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; +``` + +like查询以%开头,索引失效,如图: +![](https://user-gold-cdn.xitu.io/2019/12/7/16ee0cbcfd861ad0?w=1362&h=351&f=png&s=27789) + +把%放后面,发现索引还是正常走的,如下: +![](https://user-gold-cdn.xitu.io/2019/12/7/16ee0cdaa1e4e3ba?w=1418&h=348&f=png&s=28178) + +把%加回来,改为只查索引的字段(**覆盖索引**),发现还是走索引,惊不惊喜,意不意外 +![](https://user-gold-cdn.xitu.io/2019/12/7/16ee0d0053d631d6?w=1488&h=353&f=png&s=30036) + +**结论:** + +like查询以%开头,会导致索引失效。可以有两种方式优化: +- 使用覆盖索引 +- 把%放后面 + +**附:** 索引包含所有满足查询需要的数据的索引,称为覆盖索引(Covering Index)。 + +### 四、联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。 +表结构:(有一个联合索引`idx_userid_age`,`userId`在前,`age`在后) +``` +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `userId` int(11) NOT NULL, + `age` int(11) DEFAULT NULL, + `name` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_userid_age` (`userId`,`age`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; +``` +在联合索引中,查询条件满足**最左匹配原则**时,索引是正常生效的。请看demo: + +![](https://user-gold-cdn.xitu.io/2019/12/7/16ee1115291be614?w=1318&h=289&f=png&s=26076) + +![](https://user-gold-cdn.xitu.io/2019/12/7/16ee11320240b06d?w=1352&h=244&f=png&s=24752) + +如果条件列不是联合索引中的第一个列,索引失效,如下: + +![](https://user-gold-cdn.xitu.io/2019/12/7/16ee1160ba900688?w=1450&h=203&f=png&s=22934) + +**分析与结论:** + +- 当我们创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。 +- 联合索引不满足最左原则,索引一般会失效,但是这个还跟Mysql优化器有关的。 + + + +### 五、在索引列上使用mysql的内置函数,索引失效。 +表结构: +``` +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `userId` varchar(32) NOT NULL, + `loginTime` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_userId` (`userId`) USING BTREE, + KEY `idx_login_time` (`loginTime`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; +``` + +虽然loginTime加了索引,但是因为使用了mysql的内置函数Date_ADD(),索引直接GG,如图: +![](https://user-gold-cdn.xitu.io/2019/12/7/16ee0ea3f2480fa9?w=1385&h=348&f=png&s=31108) + +### 六、对索引列运算(如,+、-、*、/),索引失效。 +表结构: +``` +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `userId` varchar(32) NOT NULL, + `age` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_age` (`age`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; +``` +虽然age加了索引,但是因为它进行运算,索引直接迷路了。。。 +如图: + +![](https://user-gold-cdn.xitu.io/2019/12/7/16ee0fbd476c1faa?w=1405&h=291&f=png&s=25315) + +### 七、索引字段上使用(!= 或者 < >,not in)时,可能会导致索引失效。 +表结构: +``` +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `userId` int(11) NOT NULL, + `age` int(11) DEFAULT NULL, + `name` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_age` (`age`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; +``` +虽然age加了索引,但是使用了!= 或者 < >,not in这些时,索引如同虚设。如下: + +![](https://user-gold-cdn.xitu.io/2019/12/8/16ee1474432f64cc?w=1443&h=220&f=png&s=24093) + +![](https://user-gold-cdn.xitu.io/2019/12/8/16ee1489157b7181?w=1434&h=285&f=png&s=26778) + + +### 八、索引字段上使用is null, is not null,可能导致索引失效。 +表结构: +``` +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `card` varchar(255) DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_name` (`name`) USING BTREE, + KEY `idx_card` (`card`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; +``` + +单个name字段加上索引,并查询name为非空的语句,其实会走索引的,如下: + +![](https://user-gold-cdn.xitu.io/2019/12/8/16ee16273576109d?w=1516&h=251&f=png&s=25893) + +单个card字段加上索引,并查询name为非空的语句,其实会走索引的,如下: +![](https://user-gold-cdn.xitu.io/2019/12/8/16ee169934434177?w=1180&h=242&f=png&s=23687) + +但是它两用or连接起来,索引就失效了,如下: + +![](https://user-gold-cdn.xitu.io/2019/12/8/16ee16b069fd566d?w=1220&h=283&f=png&s=25460) + + + +### 九、左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。 +新建两个表,一个user,一个user_job +``` +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL, + `age` int(11) NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_name` (`name`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; + +CREATE TABLE `user_job` ( + `id` int(11) NOT NULL, + `userId` int(11) NOT NULL, + `job` varchar(255) DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_name` (`name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +``` + +user 表的name字段编码是utf8mb4,而user_job表的name字段编码为utf8。 + +![](https://user-gold-cdn.xitu.io/2019/12/8/16ee2cda2c47eb0c?w=1038&h=352&f=png&s=25812) + +![](https://user-gold-cdn.xitu.io/2019/12/8/16ee2d07a2e78ded?w=1033&h=415&f=png&s=29243) + +执行左外连接查询,user_job表还是走全表扫描,如下: + +![](https://user-gold-cdn.xitu.io/2019/12/8/16ee2d1749af07c0?w=1187&h=313&f=png&s=31938) + +如果把它们改为name字段编码一致,还是会走索引。 + +![](https://user-gold-cdn.xitu.io/2019/12/8/16ee2d4145752fb7?w=1138&h=315&f=png&s=31574) + + +### 十、mysql估计使用全表扫描要比使用索引快,则不使用索引。 + +- 当表的索引被查询,会使用最好的索引,除非优化器使用全表扫描更有效。优化器优化成全表扫描取决与使用最好索引查出来的数据是否超过表的30%的数据。 + +- 不要给'性别'等增加索引。如果某个数据列里包含了均是"0/1"或“Y/N”等值,即包含着许多重复的值,就算为它建立了索引,索引效果不会太好,还可能导致全表扫描。 + +Mysql出于效率与成本考虑,估算全表扫描与使用索引,哪个执行快。这跟它的优化器有关,来看一下它的逻辑架构图吧(图片来源网上) + +![](https://user-gold-cdn.xitu.io/2019/12/8/16ee2dbf2878b2d2?w=911&h=537&f=png&s=258744) + + +### 总结 + +总结了索引失效的十大杂症,在这里来个首尾呼应吧,分析一下我们生产的那条慢sql。 +模拟的表结构与肇事sql如下: +``` +CREATE TABLE `user_session` ( + `user_id` varchar(32) CHARACTER SET utf8mb4 NOT NULL, + `device_id` varchar(64) NOT NULL, + `status` varchar(2) NOT NULL, + `create_time` datetime NOT NULL, + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`user_id`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` + +``` +explain +update user_session set status =1 +where (`user_id` = '1' and `device_id`!='2') +or (`user_id` != '1' and `device_id`='2') +``` + +**分析:** + +- 执行的sql,使用了`or`条件,因为组合主键(`user_id`,`device_id`),看起来像是每一列都加了索引,索引会生效。 +- 但是出现`!=`,可能导致索引失效。也就是`or`+`!=`两大综合症,导致了慢更新sql。 + + +**解决方案:** + +那么,怎么解决呢?我们是把`or`条件拆掉,分成两条执行。同时给`device_id`加一个普通索引。 + + +最后,总结了索引失效的十大杂症,希望大家在工作学习中,参考这十大杂症,**多点结合执行计划`expain`和场景,具体分析** ,而不是**按部就班,墨守成规**,认定哪个情景一定索引失效。 + +### 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。 +- 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。 + + + + + + + diff --git "a/\346\205\242\346\205\242\346\235\245\357\274\214\344\270\215\346\200\225Mysql\346\255\273\351\224\201\345\225\246/insert on duplicate\346\255\273\351\224\201\344\270\200\346\254\241\346\216\222\346\237\245\345\210\206\346\236\220\350\277\207\347\250\213.md" "b/Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\346\205\242\346\205\242\346\235\245\357\274\214\344\270\215\346\200\225Mysql\346\255\273\351\224\201\345\225\246/insert on duplicate\346\255\273\351\224\201\344\270\200\346\254\241\346\216\222\346\237\245\345\210\206\346\236\220\350\277\207\347\250\213.md" similarity index 100% rename from "\346\205\242\346\205\242\346\235\245\357\274\214\344\270\215\346\200\225Mysql\346\255\273\351\224\201\345\225\246/insert on duplicate\346\255\273\351\224\201\344\270\200\346\254\241\346\216\222\346\237\245\345\210\206\346\236\220\350\277\207\347\250\213.md" rename to "Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\346\205\242\346\205\242\346\235\245\357\274\214\344\270\215\346\200\225Mysql\346\255\273\351\224\201\345\225\246/insert on duplicate\346\255\273\351\224\201\344\270\200\346\254\241\346\216\222\346\237\245\345\210\206\346\236\220\350\277\207\347\250\213.md" diff --git "a/\346\205\242\346\205\242\346\235\245\357\274\214\344\270\215\346\200\225Mysql\346\255\273\351\224\201\345\225\246/\346\255\273\351\224\201\345\210\206\346\236\220\350\277\207\347\250\213.md" "b/Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\346\205\242\346\205\242\346\235\245\357\274\214\344\270\215\346\200\225Mysql\346\255\273\351\224\201\345\225\246/\346\255\273\351\224\201\345\210\206\346\236\220\350\277\207\347\250\213.md" similarity index 100% rename from "\346\205\242\346\205\242\346\235\245\357\274\214\344\270\215\346\200\225Mysql\346\255\273\351\224\201\345\225\246/\346\255\273\351\224\201\345\210\206\346\236\220\350\277\207\347\250\213.md" rename to "Mysql\345\237\272\347\241\200\345\255\246\344\271\240/\346\205\242\346\205\242\346\235\245\357\274\214\344\270\215\346\200\225Mysql\346\255\273\351\224\201\345\225\246/\346\255\273\351\224\201\345\210\206\346\236\220\350\277\207\347\250\213.md" diff --git "a/\345\210\206\345\270\203\345\274\217/\345\220\216\347\253\257\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\357\274\232\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\345\237\272\347\241\200\347\257\207.md" "b/\345\210\206\345\270\203\345\274\217/\345\220\216\347\253\257\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\357\274\232\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\345\237\272\347\241\200\347\257\207.md" new file mode 100644 index 0000000..e639fab --- /dev/null +++ "b/\345\210\206\345\270\203\345\274\217/\345\220\216\347\253\257\347\250\213\345\272\217\345\221\230\345\277\205\345\244\207\357\274\232\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\345\237\272\347\241\200\347\257\207.md" @@ -0,0 +1,316 @@ +## 前言 +最近看了几篇有关于分布式事务的博文,做一下笔记。哈哈~ + + +![](https://user-gold-cdn.xitu.io/2020/2/28/1708c36f8bdae442?w=1123&h=479&f=png&s=66214) + +## 数据库事务 +数据库事务(**简称:事务**),是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成,这些操作**要么全部执行,要么全部不执**行,是一个**不可分割**的工作单位。 + +数据库事务的几个典型特性:原子性(Atomicity )、一致性( Consistency )、隔离性( Isolation)和持久性(Durabilily),简称就是ACID。 + + +![](https://user-gold-cdn.xitu.io/2020/2/23/170709c59894dabd?w=626&h=466&f=png&s=48562) +- **原子性:** 事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。 +- **一致性:** 指在事务开始之前和事务结束以后,数据不会被破坏,假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的。 +- **隔离性:** 多个事务并发访问时,事务之间是相互隔离的,即一个事务不影响其它事务运行效果。简言之,就是事务之间是进水不犯河水的。 +- **持久性:** 表示事务完成以后,该事务对数据库所作的操作更改,将持久地保存在数据库之中。 + +## 事务的实现原理 +### 本地事务 +传统的单服务器,单关系型数据库下的事务,就是本地事务。本地事务由资源管理器管理,JDBC事务就是一个非常典型的本地事务。 +![](https://user-gold-cdn.xitu.io/2020/2/23/170716766fd18b24?w=832&h=520&f=png&s=53822) + +### 事务日志 +innodb事务日志包括redo log和undo log。 + +#### redo log(重做日志) + +redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样,它用来恢复提交后的物理数据页。 + +#### undo log(回滚日志) + +undo log是逻辑日志,和redo log记录物理日志的不一样。可以这样认为,当delete一条记录时,undo log中会记录一条对应的insert记录,当update一条记录时,它记录一条对应相反的update记录。 + +### 事务ACID特性的实现思想 +- 原子性:是使用 undo log来实现的,如果事务执行过程中出错或者用户执行了rollback,系统通过undo log日志返回事务开始的状态。 +- 持久性:使用 redo log来实现,只要redo log日志持久化了,当系统崩溃,即可通过redo log把数据恢复。 +- 隔离性:通过锁以及MVCC,使事务相互隔离开。 +- 一致性:通过回滚、恢复,以及并发情况下的隔离性,从而实现一致性。 + +## 分布式事务 + +**分布式事务:** 就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单来说,分布式事务指的就是分布式系统中的事务,它的存在就是为了保证不同数据库节点的数据一致性。 + +为什么需要分布式事务?接下来分两方面阐述: + +### 微服务架构下的分布式事务 + +随着互联网的快速发展,轻盈且功能划分明确的微服务,登上了历史舞台。比如,一个用户下订单,购买直播礼物的服务,被拆分成三个service,分别是金币服务(coinService),下订单服务(orderService)、礼物服务(giftService)。这些服务都部署在不同的机器上(节点),对应的数据库(金币数据库、订单数据库、礼物数据库)也在不同节点上。 + +![](https://user-gold-cdn.xitu.io/2020/2/23/170713f8f725ab91?w=790&h=709&f=png&s=69304) + + +用户下单购买礼物,礼物数据库、金币数据库、订单数据库在不同节点上,用本地事务是不可以的,那么如何保证不同数据库(节点)上的数据一致性呢?这就需要分布式事务啦~ + +### 分库分表下的分布式事务 +随着业务的发展,数据库的数据日益庞大,超过千万级别的数据,我们就需要对它分库分表(以前公司是用mycat分库分表,后来用sharding-jdbc)。一分库,数据又分布在不同节点上啦,比如有的在深圳机房,有的在北京机房~你再想用本地事务去保证,已经无动于衷啦~还是需要分布式事务啦。 + +比如A转10块给B,A的账户数据是在北京机房,B的账户数据是在深圳机房。流程如下: + +![](https://user-gold-cdn.xitu.io/2020/2/20/1706322f36bfdf51?w=626&h=673&f=png&s=42564) + +## CAP 理论&BASE 理论 +学习分布式事务,当然需要了解 CAP 理论和BASE 理论。 + +### CAP理论 +CAP理论作为分布式系统的基础理论,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),这三个要素最多只能同时实现两点。 +![](https://user-gold-cdn.xitu.io/2019/7/20/16c0f6675fee8222?w=302&h=289&f=png&s=25862) + +**一致性(C:Consistency):** + +一致性是指数据在多个副本之间能否保持一致的特性。例如一个数据在某个分区节点更新之后,在其他分区节点读出来的数据也是更新之后的数据。 + +**可用性(A:Availability):** + +可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。这里的重点是"有限时间内"和"返回结果"。 + +**分区容错性(P:Partition tolerance):** + +分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务。 + +选择 | 说明 | +-|-| +CA | 放弃分区容错性,加强一致性和可用性,其实就是传统的单机数据库的选择 | +AP | 放弃一致性,分区容错性和可用性,这是很多分布式系统设计时的选择 | +CP | 放弃可用性,追求一致性和分区容错性,网络问题会直接让整个系统不可用 | + +### BASE 理论 + +BASE 理论, 是对CAP中AP的一个扩展,对于我们的业务系统,我们考虑牺牲一致性来换取系统的可用性和分区容错性。BASE是Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写。 + +**Basically Available** + +基本可用:通过支持局部故障而不是系统全局故障来实现的。如将用户分区在 5 个数据库服务器上,一个用户数据库的故障只影响这台特定主机那 20% 的用户,其他用户不受影响。 + +**Soft State** + +软状态,状态可以有一段时间不同步 + +**Eventually Consistent** + +最终一致,最终数据是一致的就可以了,而不是时时保持强一致。 + + +## 分布式事务的几种解决方案 + +分布式事务解决方案主要有以下这几种: +- 2PC(二阶段提交)方案 +- TCC(Try、Confirm、Cancel) +- 本地消息表 +- 最大努力通知 +- Saga事务 + + +### 二阶段提交方案 + +二阶段提交方案是常用的分布式事务解决方案。事务的提交分为两个阶段:准备阶段和提交执行方案。 + +#### 二阶段提交成功的情况 + +**准备阶段**,事务管理器向每个资源管理器发送准备消息,如果资源管理器的本地事务操作执行成功,则返回成功。 + +**提交执行阶段**,如果事务管理器收到了所有资源管理器回复的成功消息,则向每个资源管理器发送提交消息,RM 根据 TM 的指令执行提交。如图: + +![](https://user-gold-cdn.xitu.io/2020/2/22/1706bf8fa81abf45?w=1208&h=425&f=png&s=63545) + +#### 二阶段提交失败的情况 + +**准备阶段**,事务管理器向每个资源管理器发送准备消息,如果资源管理器的本地事务操作执行成功,则返回成功,如果执行失败,则返回失败。 + +**提交执行阶段**,如果事务管理器收到了任何一个资源管理器失败的消息,则向每个资源管理器发送回滚消息。资源管理器根据事务管理器的指令回滚本地事务操作,释放所有事务处理过程中使用的锁资源。 + +![](https://user-gold-cdn.xitu.io/2020/2/22/1706bfc3e9b3968c?w=1298&h=458&f=png&s=69883) + +#### 二阶段提交优缺点 +2PC方案实现起来简单,成本较低,但是主要有以下**缺点**: +- 单点问题:如果事务管理器出现故障,资源管理器将一直处于锁定状态。 +- 性能问题:所有资源管理器在事务提交阶段处于同步阻塞状态,占用系统资源,一直到提交完成,才释放资源,容易导致性能瓶颈。 +- 数据一致性问题:如果有的资源管理器收到提交的消息,有的没收到,那么会导致数据不一致问题。 + + +### TCC(补偿机制) +TCC 采用了补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。 +#### TCC(Try-Confirm-Cancel)模型 +TCC(Try-Confirm-Cancel)是通过对业务逻辑的分解来实现分布式事务。针对一个具体的业务服务,TCC 分布式事务模型需要业务系统都实现一下三段逻辑: + +**try阶段**: 尝试去执行,完成所有业务的一致性检查,预留必须的业务资源。 + +**Confirm阶段**:该阶段对业务进行确认提交,不做任何检查,因为try阶段已经检查过了,默认Confirm阶段是不会出错的。 + +**Cancel 阶段**:若业务执行失败,则进入该阶段,它会释放try阶段占用的所有业务资源,并回滚Confirm阶段执行的所有操作。 + + + +![](https://user-gold-cdn.xitu.io/2020/2/22/1706d1749decc6ce?w=1468&h=979&f=png&s=1128862) + +TCC 分布式事务模型包括三部分:**主业务服务、从业务服务、业务活动管理器**。 + +- 主业务服务:主业务服务负责发起并完成整个业务活动。 +- 从业务服务:从业务服务是整个业务活动的参与方,实现Try、Confirm、Cancel操作,供主业务服务调用。 +- 业务活动管理器:业务活动管理器管理控制整个业务活动,包括记录事务状态,调用从业务服务的 Confirm 操作,调用从业务服务的 Cancel 操作等。 + + +下面再拿用户下单购买礼物作为例子来模拟TCC实现分布式事务的过程: + + +> 假设用户A余额为100金币,拥有的礼物为5朵。A花了10个金币,下订单,购买10朵玫瑰。余额、订单、礼物都在不同数据库。 + +**TCC的Try阶段:** +- 生成一条订单记录,订单状态为待确认。 +- 将用户A的账户金币中余额更新为90,冻结金币为10(预留业务资源) +- 将用户的礼物数量为5,预增加数量为10。 +- Try成功之后,便进入Confirm阶段 +- Try过程发生任何异常,均进入Cancel阶段 + +![](https://user-gold-cdn.xitu.io/2020/2/22/1706cf4d0578b364?w=1553&h=830&f=png&s=101136) + +**TCC的Confirm阶段:** +- 订单状态更新为已支付 +- 更新用户余额为90,可冻结为0 +- 用户礼物数量更新为15,预增加为0 +- Confirm过程发生任何异常,均进入Cancel阶段 +- Confirm过程执行成功,则该事务结束 + + +![](https://user-gold-cdn.xitu.io/2020/2/22/1706d101f3da66ac?w=1565&h=805&f=png&s=123868) +**TCC的Cancel阶段:** +- 修改订单状态为已取消 +- 更新用户余额回100 +- 更新用户礼物数量为5 + + +![](https://user-gold-cdn.xitu.io/2020/2/22/1706d0f4dc07c44f?w=1612&h=814&f=png&s=135316) + +#### TCC优缺点 +TCC方案让应用可以自定义数据库操作的粒度,降低了锁冲突,可以提升性能,但是也有以下缺点: +- 应用侵入性强,try、confirm、cancel三个阶段都需要业务逻辑实现。 +- 需要根据网络、系统故障等不同失败原因实现不同的回滚策略,实现难度大,一般借助TCC开源框架,ByteTCC,TCC-transaction,Himly。 + + +### 本地消息表 +ebay最初提出本地消息表这个方案,来解决分布式事务问题。业界目前使用这种方案是比较多的,它的核心思想就是将分布式事务拆分成本地事务进行处理。可以看一下基本的实现流程图: + +![](https://user-gold-cdn.xitu.io/2020/2/22/1706d35dba36801f?w=1355&h=648&f=png&s=159895) + +#### 基本实现思路 + +**发送消息方:** +- 需要有一个消息表,记录着消息状态相关信息。 +- 业务数据和消息表在同一个数据库,即要保证它俩在同一个本地事务。 +- 在本地事务中处理完业务数据和写消息表操作后,通过写消息到MQ消息队列。 +- 消息会发到消息消费方,如果发送失败,即进行重试。 + +**消息消费方:** +- 处理消息队列中的消息,完成自己的业务逻辑。 +- 此时如果本地事务处理成功,则表明已经处理成功了。 +- 如果本地事务处理失败,那么就会重试执行。 +- 如果是业务上面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚等操作。 + +生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。 + +#### 优点&缺点: + +该方案的优点是很好地解决了分布式事务问题,实现了最终一致性。缺点是消息表会耦合到业务系统中。 + +### 最大努力通知 +#### 什么是最大通知 +最大努力通知也是一种分布式事务解决方案。下面是企业网银转账一个例子 + +![](https://user-gold-cdn.xitu.io/2020/2/28/170879e04cfd9d00?w=964&h=861&f=png&s=115120) + +- 企业网银系统调用前置接口,跳转到转账页 +- 企业网银调用转账系统接口 +- 转账系统完成转账处理,向企业网银系统发起转账结果通知,若通知失败,则转账系统按策略进行重复通知。 +- 企业网银系统未接收到通知,会主动调用转账系统的接口查询转账结果。 +- 转账系统会遇到退汇等情况,会定时回来对账。 + +最大努力通知方案的目标,就是**发起通知方通过一定的机制,最大努力将业务处理结果通知到接收方**。最大努力通知实现机制如下: + +![](https://user-gold-cdn.xitu.io/2020/2/28/1708ba72abeb5e05?w=660&h=440&f=png&s=45177) + +#### 最大努力通知解决方案 + +要实现最大努力通知,可以采用MQ的ack机制。 + +**方案** + +![](https://user-gold-cdn.xitu.io/2020/2/28/1708c187f8e1608d?w=1555&h=561&f=png&s=81088) +- 1.发起方将通知发给MQ。 +- 2.接收通知方监听MQ消息。 +- 3.接收通知方收到消息后,处理完业务,回应ack。 +- 4.接收通知方若没有回应ack,则MQ会间隔1min、5min、10min等重复通知。 +- 5.接受通知方可用消息校对接口,保证消息的一致性。 + +转账业务实现流程图: + +![](https://user-gold-cdn.xitu.io/2020/2/28/1708c2a3a6a04190?w=1045&h=559&f=png&s=77360) +交互流程如下: +- 1、用户请求转账系统进行转账。 +- 2、转账系统完成转账,将转账结果发给MQ。 +- 3、企业网银系统监听MQ,接收转账结果通知,如果接收不到消息,MQ会重复发送通知。接收到转账结果,更新转账状态。 +- 4、企业网银系统也可以主动查询转账系统的转账结果查询接口,更新转账状态。 + + + +### Saga事务 +Saga事务由普林斯顿大学的Hector Garcia-Molina和Kenneth Salem提出,其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。 + +**saga简介** +- Saga = Long Live Transaction (LLT,长活事务) +- LLT = T1 + T2 + T3 + ... + Ti(Ti为本地短事务) +- 每个本地事务Ti 有对应的补偿 Ci + +**Saga的执行顺序** +- 正常情况:T1 T2 T3 ... Tn +- 异常情况:T1 T2 T3 C3 C2 C1 + +**Saga两种恢复策略** +- 向后恢复,如果任意本地子事务失败,补偿已完成的事务。如异常情况的执行顺序T1 T2 Ti Ci C2 C1. +- 向前恢复,即重试失败的事务,假设最后每个子事务都会成功。执行顺序:T1, T2, ..., Tj(失败), Tj(重试),..., Tn。 + +举个例子,假设用户下订单,花10块钱购买了10多玫瑰,则有 + + T1=下订单 ,T2=扣用户10块钱,T3=用户加10朵玫瑰, T4=库存减10朵玫瑰 + + C1=取消订单 ,C2= 给用户加10块钱,C3 =用户减10朵玫瑰, C4=库存加10朵玫瑰 + + +![](https://user-gold-cdn.xitu.io/2020/2/25/1707ccc17917ef04?w=1194&h=571&f=png&s=50655) + +假设事务执行到T4发生异常回滚,在C4的要把玫瑰给库存加回去的时候,发现用户的玫瑰都用掉了,这是**Saga的一个缺点**,由于事务之间没有隔离性导致的问题。 + +**可以通过以下方案解决这个问题**: +- 在应⽤层⾯加⼊逻辑锁的逻辑。 +- Session层⾯隔离来保证串⾏化操作。 +- 业务层⾯采⽤预先冻结资⾦的⽅式隔离此部分资⾦。 +- 业务操作过程中通过及时读取当前状态的⽅式获取更新。 + + +## 参考与感谢 +- [干货 | 一篇文章带你学习分布式事务](https://mp.weixin.qq.com/s/RDnf637MY0IVgv2NpNVByw) +- [再有人问你分布式事务,把这篇扔给他](https://juejin.im/post/5b5a0bf9f265da0f6523913b#heading-16) +- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html) +- [Mysql事务实现原理](https://juejin.im/post/5cb2e3b46fb9a0686e40c5cb#comment) +- [详细分析MySQL事务日志(redo log和undo log)](https://juejin.im/entry/5ba0a254e51d450e735e4a1f) +- [《Saga分布式事务解决⽅案与实践》](https://servicecomb.apache.org/assets/slides/20180422/QConBeijing2018-Saga.pdf) +- [分布式事务解决方案之最大努力通知](https://www.cnblogs.com/zeussbook/p/11799017.html) + +## 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 觉得写得好的小伙伴给个点赞+关注啦,谢谢~ +- 同时非常期待小伙伴们能够关注我公众号,后面慢慢推出更好的干货~嘻嘻 +- github地址:https://github.com/whx123/JavaHome + diff --git "a/\345\216\237\345\210\233\350\257\227\351\233\206/\345\244\217\345\244\251\347\232\204\351\243\216\346\210\221\346\260\270\350\277\234\350\256\260\345\276\227.md" "b/\345\216\237\345\210\233\350\257\227\351\233\206/\345\244\217\345\244\251\347\232\204\351\243\216\346\210\221\346\260\270\350\277\234\350\256\260\345\276\227.md" new file mode 100644 index 0000000..500af79 --- /dev/null +++ "b/\345\216\237\345\210\233\350\257\227\351\233\206/\345\244\217\345\244\251\347\232\204\351\243\216\346\210\221\346\260\270\350\277\234\350\256\260\345\276\227.md" @@ -0,0 +1,49 @@ +夜深了,宁静了。 +朝南的窗, +我轻轻地打开了。 +夏夜的风, +温柔且粘人, +穿过头发, +吻着耳朵, +感觉特别舒服。 + +想起了小时候, +老家屋子热得发烫~ +我牵着母亲的大手, +抱着小枕头, +裹着小凉席, +一步两步三步, +走向楼顶,睡觉。 + +那时候, +漫天的繁星活泼且迷人, +对着我不停眨着眼, +像大眼睛的小女孩子, +害羞地浅笑。 +月亮也圆圆大大的, +像个暖心的大姐姐, +马上要抱着我入眠。 + +那时候, +萤火虫似一盏盏小油灯, +照亮的我小脸蛋。 +知了在吵闹过后, +也安静了下来, +周围像人过港空的街道。 +飞机划过苍穹, +留下隆隆的声音, +给这个夜,留下美好的快照。 + +母亲在我耳旁喃喃细语, +赶紧睡觉吧, +明天跟我去田里, +收割水稻。 + +一听,我心就乐了。 +想到田里有活泼的稻花鱼, +有早出晚归的小田螺, +还有喜欢偷吃稻谷的小麻雀。 +慢慢地,进入了梦乡, +带着对母亲的思念~ + +最后,提前祝天底下所有的母亲,母亲节快乐~ \ No newline at end of file diff --git "a/\345\267\245\344\275\234\346\200\273\347\273\223/CAS\344\271\220\350\247\202\351\224\201\350\247\243\345\206\263\345\271\266\345\217\221\351\227\256\351\242\230\347\232\204\344\270\200\346\254\241\345\256\236\350\267\265.md" "b/\345\267\245\344\275\234\346\200\273\347\273\223/CAS\344\271\220\350\247\202\351\224\201\350\247\243\345\206\263\345\271\266\345\217\221\351\227\256\351\242\230\347\232\204\344\270\200\346\254\241\345\256\236\350\267\265.md" new file mode 100644 index 0000000..cd63291 --- /dev/null +++ "b/\345\267\245\344\275\234\346\200\273\347\273\223/CAS\344\271\220\350\247\202\351\224\201\350\247\243\345\206\263\345\271\266\345\217\221\351\227\256\351\242\230\347\232\204\344\270\200\346\254\241\345\256\236\350\267\265.md" @@ -0,0 +1,52 @@ +## 前言 +最近做新项目,货币充值消耗,送礼竞争勋章等都使用了CAS解决并发问题,所以做一下笔记,谈谈CAS,大家一起互相学习。 + +## 乐观锁,悲观锁: + +讨论CAS的话,先来说有一下乐观锁,悲观锁。 + +**悲观锁**:每次去取数据,很悲观,都觉得会被别人修改,所以在拿数据的时候都会上锁。简言之,共享资源每次都只给一个线程使用,其他线程阻塞,等第一个线程用完后再把资源转让给其他线程。synchronized和ReentranLock等都是悲观锁思想的体现。 + +**乐观锁**:每次去取数据,都很乐观,觉得不会被被人修改。因此每次都不上锁,但是在更新的时候,就会看别人有没有在这期间去更新这个数据,如果有更新就重新获取,再进行判断,一直循环,直到拿到没有被修改过的数据。CAS(Compare and Swap 比较并交换)就是乐观锁的一种实现方式。 + +## CAS算法: +### CAS涉及三个操作数 + +1.需要读写的内存地址V + +2.进行比较的预期原值A + +3.拟写入的新值B + +如果内存位置的值V与预期原A值相匹配,那么处理器会自动将该位置值更新为新值B。CAS思想:要进行更新时,认为位置V上的值还是跟A值相等,如果是是相等,就认为它没有被别的线程更改过,即可更新为B值。否则,认为它已经被别的线程修改过,不更新为B的值,返回当前位置V最新的值。 + +### JDK源码中,CAS思想体现: +反编译Unsafe类(用Java Decompiler工具) +![](https://user-gold-cdn.xitu.io/2019/6/16/16b60b9ba784610d?w=1319&h=856&f=png&s=54787) +从源码中可以发现,内部使用自旋的方式进行CAS更新 + +## 业务场景以及CAS的应用: +假设多人A,B,C等给D送礼,送总价值最多的那个人,可以成为佩带D的守护皇冠,D的守护皇冠有且只有一个。如果他们同时在给D送礼,送礼价值互相超越,即存在并发问题。 + +**解决思路:** 参考乐观锁原理 +- 设置乐观锁失败后尝试次数n次 +- 先查询旧的守护者,即旧的送礼最大价值者。 +- 如果当前旧的守护者不为空,构造当前送礼者为新守护者。 +- 将新的守护者去跟旧的守护者比较送礼的价值,尝试更新数据库。 +- 如果发现更新时,旧的最大送礼价值发生改变了,放弃更新,退出循环,重新尝试(n--)。 +- 如果当前旧的守护者为空,表示以前还没有守护,直接将新的守护插入表。 +- 如果插入表失败,表示在插入过程中,数据被更改了,表明有新的记录抢先成为守护。 +- 那么,重新尝试(n--),直到次数n用完。 + +**抢占守护流程图:** +![](https://user-gold-cdn.xitu.io/2019/6/16/16b60d362b5f3670?w=856&h=940&f=png&s=50030) + +**代码实现:** + +![](https://user-gold-cdn.xitu.io/2019/6/16/16b60d434c953379?w=1494&h=1033&f=png&s=656168) + +## CAS存在的一些问题: +### 1.ABA问题, +并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。 +### 2.CPU开销 +自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。所以上面抢占守护的例子,设置了尝试的执行次数n,避免一直循环 \ No newline at end of file diff --git "a/\345\267\245\344\275\234\346\200\273\347\273\223/Java\346\227\245\346\234\237\345\244\204\347\220\206\346\230\223\350\270\251\347\232\204\345\215\201\344\270\252\345\235\221.md" "b/\345\267\245\344\275\234\346\200\273\347\273\223/Java\346\227\245\346\234\237\345\244\204\347\220\206\346\230\223\350\270\251\347\232\204\345\215\201\344\270\252\345\235\221.md" new file mode 100644 index 0000000..8a3eec4 --- /dev/null +++ "b/\345\267\245\344\275\234\346\200\273\347\273\223/Java\346\227\245\346\234\237\345\244\204\347\220\206\346\230\223\350\270\251\347\232\204\345\215\201\344\270\252\345\235\221.md" @@ -0,0 +1,391 @@ +### 前言 +整理了Java日期处理的十个坑,希望对大家有帮助。 + +### 一、用Calendar设置时间的坑 +**反例:** +``` +Calendar c = Calendar.getInstance(); +c.set(Calendar.HOUR, 10); +System.out.println(c.getTime()); +``` +**运行结果:** + +``` +Thu Mar 26 22:28:05 GMT+08:00 2020 +``` +**解析:** + +我们设置了10小时,但运行结果是22点,而不是10点。因为Calendar.HOUR默认是按12小时制处理的,需要使用Calendar.HOUR_OF_DAY,因为它才是按24小时处理的。 + +**正例:** + +``` +Calendar c = Calendar.getInstance(); +c.set(Calendar.HOUR_OF_DAY, 10); +``` +### 二、Java日期格式化YYYY的坑 + +**反例:** + +``` +Calendar calendar = Calendar.getInstance(); +calendar.set(2019, Calendar.DECEMBER, 31); + +Date testDate = calendar.getTime(); + +SimpleDateFormat dtf = new SimpleDateFormat("YYYY-MM-dd"); +System.out.println("2019-12-31 转 YYYY-MM-dd 格式后 " + dtf.format(testDate)); +``` +**运行结果:** + +``` +2019-12-31 转 YYYY-MM-dd 格式后 2020-12-31 +``` +**解析:** + +为什么明明是2019年12月31号,就转了一下格式,就变成了2020年12月31号了?因为YYYY是基于周来计算年的,它指向当天所在周属于的年份,一周从周日开始算起,周六结束,只要本周跨年,那么这一周就算下一年的了。正确姿势是使用yyyy格式。 + +![](https://user-gold-cdn.xitu.io/2020/3/26/171176d2ef2535d0?w=553&h=553&f=png&s=48060) + +**正例:** + +``` +Calendar calendar = Calendar.getInstance(); +calendar.set(2019, Calendar.DECEMBER, 31); + +Date testDate = calendar.getTime(); + +SimpleDateFormat dtf = new SimpleDateFormat("yyyy-MM-dd"); +System.out.println("2019-12-31 转 yyyy-MM-dd 格式后 " + dtf.format(testDate)); +``` + +### 三、Java日期格式化hh的坑。 + +**反例:** + +``` +String str = "2020-03-18 12:00"; +SimpleDateFormat dtf = new SimpleDateFormat("yyyy-MM-dd hh:mm"); +Date newDate = dtf.parse(str); +System.out.println(newDate); +``` +**运行结果:** + +``` +Wed Mar 18 00:00:00 GMT+08:00 2020 +``` +**解析:** + +设置的时间是12点,为什么运行结果是0点呢?因为hh是12制的日期格式,当时间为12点,会处理为0点。正确姿势是使用HH,它才是24小时制。 + +**正例:** + +``` +String str = "2020-03-18 12:00"; +SimpleDateFormat dtf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); +Date newDate = dtf.parse(str); +System.out.println(newDate); +``` + +### 四、Calendar获取的月份比实际数字少1即(0-11) +**反例:** + +``` +//获取当前月,当前是3月 +Calendar calendar = Calendar.getInstance(); +System.out.println("当前"+calendar.get(Calendar.MONTH)+"月份"); +``` +**运行结果:** + +``` +当前2月份 +``` +**解析:** + +``` +The first month of the year in the Gregorian and Julian calendars +is JANUARY which is 0; +也就是1月对应的是下标 0,依次类推。因此获取正确月份需要加 1. +``` + +**正例:** + +``` +//获取当前月,当前是3月 +Calendar calendar = Calendar.getInstance(); +System.out.println("当前"+(calendar.get(Calendar.MONTH)+1)+"月份"); +``` + +### 五、Java日期格式化DD的坑 + +**反例:** + +``` +Calendar calendar = Calendar.getInstance(); +calendar.set(2019, Calendar.DECEMBER, 31); + +Date testDate = calendar.getTime(); + +SimpleDateFormat dtf = new SimpleDateFormat("yyyy-MM-DD"); +System.out.println("2019-12-31 转 yyyy-MM-DD 格式后 " + dtf.format(testDate)); +``` +**运行结果:** + +``` +2019-12-31 转 yyyy-MM-DD 格式后 2019-12-365 +``` +**解析:** + +DD和dd表示的不一样,DD表示的是一年中的第几天,而dd表示的是一月中的第几天,所以应该用的是dd。 + +**正例:** + +``` +Calendar calendar = Calendar.getInstance(); +calendar.set(2019, Calendar.DECEMBER, 31); + +Date testDate = calendar.getTime(); + +SimpleDateFormat dtf = new SimpleDateFormat("yyyy-MM-dd"); +System.out.println("2019-12-31 转 yyyy-MM-dd 格式后 " + dtf.format(testDate)); +``` + +### 六、SimleDateFormat的format初始化问题 +**反例:** + +``` +SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); +System.out.println(sdf.format(20200323)); +``` +**运行结果:** + +``` +1970-01-01 +``` +**解析:** + +用format格式化日期是,要输入的是一个Date类型的日期,而不是一个整型或者字符串。 + +**正例:** + +``` +Calendar calendar = Calendar.getInstance(); +calendar.set(2020, Calendar.MARCH, 23); +SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); +System.out.println(sdf.format(calendar.getTime())); +``` + +### 七、日期本地化问题 + +**反例:** +``` +String dateStr = "Wed Mar 18 10:00:00 2020"; +DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy"); +LocalDateTime dateTime = LocalDateTime.parse(dateStr, formatter); +System.out.println(dateTime); +``` +**运行结果:** + +``` +Exception in thread "main" java.time.format.DateTimeParseException: Text 'Wed Mar 18 10:00:00 2020' could not be parsed at index 0 + at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949) + at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851) + at java.time.LocalDateTime.parse(LocalDateTime.java:492) + at com.example.demo.SynchronizedTest.main(SynchronizedTest.java:19) +``` +**解析:** + +DateTimeFormatter 这个类默认进行本地化设置,如果默认是中文,解析英文字符串就会报异常。可以传入一个本地化参数(Locale.US)解决这个问题 + +**正例:** + +``` +String dateStr = "Wed Mar 18 10:00:00 2020"; +DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy",Locale.US); +LocalDateTime dateTime = LocalDateTime.parse(dateStr, formatter); +System.out.println(dateTime); +``` + +### 八、SimpleDateFormat 解析的时间精度问题 + +**反例:** +``` +SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); +String time = "2020-03"; +System.out.println(sdf.parse(time)); +``` +**运行结果:** + +``` +Exception in thread "main" java.text.ParseException: Unparseable date: "2020-03" + at java.text.DateFormat.parse(DateFormat.java:366) + at com.example.demo.SynchronizedTest.main(SynchronizedTest.java:19) +``` +**解析:** + +SimpleDateFormat 可以解析长于/等于它定义的时间精度,但是不能解析小于它定义的时间精度。 + +**正例:** + +``` +SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM"); +String time = "2020-03"; +System.out.println(sdf.parse(time)); +``` + +### 九、SimpleDateFormat 的线性安全问题 + +**反例:** + +``` +public class SimpleDateFormatTest { + + private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public static void main(String[] args) { + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1000)); + + while (true) { + threadPoolExecutor.execute(() -> { + String dateString = sdf.format(new Date()); + try { + Date parseDate = sdf.parse(dateString); + String dateString2 = sdf.format(parseDate); + System.out.println(dateString.equals(dateString2)); + } catch (ParseException e) { + e.printStackTrace(); + } + }); + } + } +``` +**运行结果:** + +``` +Exception in thread "pool-1-thread-49" java.lang.NumberFormatException: For input string: "5151." + at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) + at java.lang.Long.parseLong(Long.java:589) + at java.lang.Long.parseLong(Long.java:631) + at java.text.DigitList.getLong(DigitList.java:195) + at java.text.DecimalFormat.parse(DecimalFormat.java:2051) + at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162) + at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) + at java.text.DateFormat.parse(DateFormat.java:364) + at com.example.demo.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:19) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) + at java.lang.Thread.run(Thread.java:748) +Exception in thread "pool-1-thread-47" java.lang.NumberFormatException: For input string: "5151." + at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) + at java.lang.Long.parseLong(Long.java:589) + at java.lang.Long.parseLong(Long.java:631) + at java.text.DigitList.getLong(DigitList.java:195) + at java.text.DecimalFormat.parse(DecimalFormat.java:2051) + at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162) + at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) + at java.text.DateFormat.parse(DateFormat.java:364) + at com.example.demo.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:19) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) + at java.lang.Thread.run(Thread.java:748) +``` +**解析:** + +全局变量的SimpleDateFormat,在并发情况下,存在安全性问题。 +- SimpleDateFormat继承了 DateFormat +- DateFormat类中维护了一个全局的Calendar变量 +- sdf.parse(dateStr)和sdf.format(date),都是由Calendar引用来储存的。 +- 如果SimpleDateFormat是static全局共享的,Calendar引用也会被共享。 +- 又因为Calendar内部并没有线程安全机制,所以全局共享的SimpleDateFormat不是线性安全的。 + +**解决SimpleDateFormat线性不安全问题,有三种方式:** +- 将SimpleDateFormat定义为局部变量 +- 使用ThreadLocal。 +- 方法加同步锁synchronized。 + +**正例:** + +``` +public class SimpleDateFormatTest { + + private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + private static ThreadLocal threadLocal = new ThreadLocal(); + + public static DateFormat getDateFormat() { + DateFormat df = threadLocal.get(); + if(df == null){ + df = new SimpleDateFormat(DATE_FORMAT); + threadLocal.set(df); + } + return df; + } + + public static String formatDate(Date date) throws ParseException { + return getDateFormat().format(date); + } + + public static Date parse(String strDate) throws ParseException { + return getDateFormat().parse(strDate); + } + + public static void main(String[] args) { + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1000)); + + while (true) { + threadPoolExecutor.execute(() -> { + try { + String dateString = formatDate(new Date()); + Date parseDate = parse(dateString); + String dateString2 = formatDate(parseDate); + System.out.println(dateString.equals(dateString2)); + } catch (ParseException e) { + e.printStackTrace(); + } + }); + } + } +} +``` + +### 十、Java日期的夏令时问题 + +**反例:** + +``` +TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); + +SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); +System.out.println(sdf.parse("1986-05-04 00:30:00")); +``` +**运行结果:** + +``` +Sun May 04 01:30:00 CDT 1986 +``` +**解析:** + +先了解一下夏令时 +> - 夏令时,表示为了节约能源,人为规定时间的意思。 +> - 一般在天亮早的夏季人为将时间调快一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。 +> - 各个采纳夏时制的国家具体规定不同。目前全世界有近110个国家每年要实行夏令时。 +> - 1986年4月,中国中央有关部门发出“在全国范围内实行夏时制的通知”,具体作法是:每年从四月中旬第一个星期日的凌晨2时整(北京时间),将时钟拨快一小时。(1992年起,夏令时暂停实行。) +> - 夏时令这几个时间可以注意一下哈,1986-05-04, 1987-04-12, 1988-04-10, 1989-04-16, 1990-04-15, 1991-04-14. + +结合demo代码,中国在1986-05-04当天还在使用夏令时,时间被拨快了1个小时。所以0点30分打印成了1点30分。如果要打印正确的时间,可以考虑修改时区为东8区。 + +**正例:** + +``` +TimeZone.setDefault(TimeZone.getTimeZone("GMT+8")); + +SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); +System.out.println(sdf.parse("1986-05-04 00:30:00")); +``` + +### 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 觉得写得好的小伙伴给个点赞+关注啦,谢谢~ +- 如果有写得不正确的地方,麻烦指出,感激不尽。 +- 同时非常期待小伙伴们能够关注我公众号,后面慢慢推出更好的干货~嘻嘻 \ No newline at end of file diff --git "a/\345\267\245\344\275\234\346\200\273\347\273\223/\344\274\230\345\214\226\344\273\243\347\240\201\347\232\204\345\207\240\344\270\252\345\260\217\346\212\200\345\267\247.md" "b/\345\267\245\344\275\234\346\200\273\347\273\223/\344\274\230\345\214\226\344\273\243\347\240\201\347\232\204\345\207\240\344\270\252\345\260\217\346\212\200\345\267\247.md" new file mode 100644 index 0000000..5f26563 --- /dev/null +++ "b/\345\267\245\344\275\234\346\200\273\347\273\223/\344\274\230\345\214\226\344\273\243\347\240\201\347\232\204\345\207\240\344\270\252\345\260\217\346\212\200\345\267\247.md" @@ -0,0 +1,238 @@ +## 前言 +最近看了《重构-改善既有代码的设计》这本书,总结了优化代码的几个小技巧,给大家分享一下。 +## 提炼函数(适当抽取小函数) + +### 定义 +**提炼函数**就是将一段代码放进一个独立函数中,并让函数名称解释该函数用途。 + +一个过于冗长的函数或者一段需要注释才能让人理解用途的代码,可以考虑把它切分成一个**功能明确的函数单元**,并定义**清晰简短的函数名**,这样会让代码变得更加优雅。 + + +### 优化例子 +**提炼函数之前:** +``` + private String name; + private Vector orders = new Vector(); + + public void printOwing() { + //print banner + System.out.println("****************"); + System.out.println("*****customer Owes *****"); + System.out.println("****************"); + + //calculate totalAmount + Enumeration env = orders.elements(); + double totalAmount = 0.0; + while (env.hasMoreElements()) { + Order order = (Order) env.nextElement(); + totalAmount += order.getAmout(); + } + + //print details + System.out.println("name:" + name); + System.out.println("amount:" + totalAmount); + } +``` +**提炼函数之后:** + +以上那段代码,可以抽成**print banner,calculate totalAmount,print details三个功能**的单一函数,如下: +``` + private String name; + private Vector orders = new Vector(); + + public void printOwing() { + + //print banner + printBanner(); + //calculate totalAmount + double totalAmount = getTotalAmount(); + //print details + printDetail(totalAmount); + } + + void printBanner(){ + System.out.println("****************"); + System.out.println("*****customer Owes *****"); + System.out.println("****************"); + } + + double getTotalAmount(){ + Enumeration env = orders.elements(); + double totalAmount = 0.0; + while (env.hasMoreElements()) { + Order order = (Order) env.nextElement(); + totalAmount += order.getAmout(); + } + return totalAmount; + } + + void printDetail(double totalAmount){ + System.out.println("name:" + name); + System.out.println("amount:" + totalAmount); + } + +``` + +## 内联函数(适当去除多余函数) +### 定义 +**内联函数**就是在函数调用点插入函数本体,然后移除该函数。 + +上一小节介绍了**提炼函数**代码优化方式,以**简短清晰的小函数**为荣。但是呢,小函数是不是越多越好呢?肯定不是啦,有时候你会遇到某些函数,其内部代码和函数名称同样清晰,这时候呢你可以考虑**内联函数**优化一下了。 + +### 优化例子 +**内联函数之前** +``` + int getRating(){ + return moreThanFiveDeliveries() ? 2 : 1; + } + boolean moreThanFiveDeliveries(){ + return numberOfLateDeliveries >5; + } +``` +**内联函数之后** +``` + int getRating(){ + return numberOfLateDeliveries >5 ? 2 : 1; + } +``` + +## 内联临时变量(去除多余临时变量) + +### 定义 +**内联临时变量**将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。 + +### 优化例子 +**内联临时变量之前** +``` +double basePice = anOrder.basePrice(); +return basePice >888; +``` +**内联临时变量之后** +``` + return anOrder.basePrice() >888; +``` + +## 引入解释性变量 + +### 定义 +**引入解释性变量** 就是将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。 + +有些表达式可能非常复杂难于阅读,在这种情况下,临时变量可以帮助你将表达式分解为可读的形式。 + +在比较复杂的条件逻辑中,你可以用**引入解释性变量**将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。 + +### 优化例子 +**引入解释性变量之前** +``` +if ((platform.toUpperCase().indexOf("mac") > -1) && + (brower.toUpperCase().indexOf("ie") > -1) && + wasInitializes() && resize > 0) { + ...... + } +``` + +**引入解释性变量之后** +``` +final boolean isMacOS = platform.toUpperCase().indexOf("mac") > -1; +final boolean isIEBrowser = brower.toUpperCase().indexOf("ie") > -1; +final boolean wasResized = resize > 0; + +if (isMacOS && isIEBrowser && wasInitializes() && wasResized) { + ...... +} +``` + +## 以字面常量取代魔法数 +### 定义 +创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。 + +所谓魔法数是指拥有特殊意义,却又不能明确表现出这种意义的数字。如果你需要在**不同的地点引用同一个逻辑数**,每当该数字要修改时,会特别头疼,因为很可能会改漏。而**字面常量取代魔法数**可以解决这个头疼问题。 + +### 优化例子 +**以字面常量取代魔法数之前** +``` +double getDiscountPrice(double price){ + return price * 0.88; + } +``` +**以字面常量取代魔法数之后** +``` + static final double DISCOUNT_CONSTANT=0.88; + + double getDiscountPrice(double price){ + return price * DISCOUNT_CONSTANT; + } +``` + + +## 用多态替代switch语句 +### 定义 +**用多态替换switch语句** 就是利用Java面向对象的多态特点,使用state模式来替换switch语句。 + +### 优化例子 +**用多态替换switch语句之前** +``` + int getArea() { + switch (shape){ + case SHAPE.CIRCLE: + return 3.14 * _r * _r; break; + case SHAPE.RECTANGEL; + return width *,heigth; + } + } +``` +**用多态替换switch语句之后** +``` + class Shape { + int getArea(){}; + } + + class Circle extends Shape { + int getArea() { + return 3.14 * r * r; + } + } + + class Rectangel extends Shape { + int getArea() { + return width * heigth; + } + } +``` + +## 将过多的参数对象化 +### 定义 +**将过多的参数对象化**就是把涉及过多参数封装成一个对象传参。 + +一个方法有太多传参时,即难以阅读又难于维护。尤其针对dubbo远程调用这些方法,如果有过多参数,增加或者减少一个参数,都要修改接口,真的坑。如果把这些参数封装成一个对象,就很好维护了,不用修改接口。 + + + +### 优化例子 +**将过多的参数对象化之前:** +``` +public int register(String username,String password,Integer age,String phone); +``` +**将过多的参数对象化之后:** +``` + public int register(RegisterForm from ); + + class RegisterForm{ + private String username; + private String password; + private Integer age; + private String phone; + } +``` + +## 参考与感谢 + +- 《重构-改善既有代码的设计》 + + +## 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。 +- 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。 \ No newline at end of file diff --git "a/\345\267\245\344\275\234\346\200\273\347\273\223/\345\206\231\344\273\243\347\240\201\346\234\211\350\277\231\344\272\233\346\203\263\346\263\225\357\274\214\345\220\214\344\272\213\346\211\215\344\270\215\344\274\232\350\256\244\344\270\272\344\275\240\346\230\257\345\244\215\345\210\266\347\262\230\350\264\264\347\250\213\345\272\217\345\221\230.md" "b/\345\267\245\344\275\234\346\200\273\347\273\223/\345\206\231\344\273\243\347\240\201\346\234\211\350\277\231\344\272\233\346\203\263\346\263\225\357\274\214\345\220\214\344\272\213\346\211\215\344\270\215\344\274\232\350\256\244\344\270\272\344\275\240\346\230\257\345\244\215\345\210\266\347\262\230\350\264\264\347\250\213\345\272\217\345\221\230.md" new file mode 100644 index 0000000..5bbfde2 --- /dev/null +++ "b/\345\267\245\344\275\234\346\200\273\347\273\223/\345\206\231\344\273\243\347\240\201\346\234\211\350\277\231\344\272\233\346\203\263\346\263\225\357\274\214\345\220\214\344\272\213\346\211\215\344\270\215\344\274\232\350\256\244\344\270\272\344\275\240\346\230\257\345\244\215\345\210\266\347\262\230\350\264\264\347\250\213\345\272\217\345\221\230.md" @@ -0,0 +1,359 @@ +## 前言 +最近做完12月份版本需求,有一些思考不够深入的代码,因此写一下总结,希望大家日常写代码多点思考,多点总结,加油!同时哪里有不对的,也望指出。 + +### 一、复杂的逻辑条件,是否可以调整顺序,让程序更高效呢。 +假设业务需求是这样:会员,第一次登陆时,需要发一条感谢短信。如果没有经过思考,代码直接这样写了 +``` +if(isUserVip && isFirstLogin){ + sendMsg(); +} +``` +**假设总共有5个请求,isUserVip通过的有3个请求,isFirstLogin通过的有1个请求。** +那么以上代码,isUserVip执行的次数为5次,isFirstLogin执行的次数也是3次,如下: + +![](https://user-gold-cdn.xitu.io/2019/12/27/16f4312ee2db2f80?w=1074&h=210&f=png&s=20562) + +**如果调整一下isUserVip和isFirstLogin的顺序呢?** +``` +if(isFirstLogin && isUserVip ){ + sendMsg(); +} +``` +isFirstLogin执行的次数是5次,isUserVip执行的次数是1次,如下: + +![](https://user-gold-cdn.xitu.io/2019/12/27/16f4319b9710a1a9?w=1027&h=246&f=png&s=20820) + +**酱紫你的程序是否更高效呢?** + +### 二、你的程序是否不经意间创建了不必要的对象。 +举个粟子吧,判断用户会员是否处于有效期,通常有以下类似代码: +``` +//判断用户会员是否在有效期 +public boolean isUserVIPValid() { + Date now = new Date(); + Calendar gmtCal = Calendar.getInstance(); + gmtCal.set(2019, Calendar.JANUARY, 1, 0, 0, 0); + Date beginTime = gmtCal.getTime(); + gmtCal.set(2020, Calendar.JANUARY, 1, 0, 0, 0); + Date endTime= gmtCal.getTime(); + return now.compareTo(beginTime) >= 0 && now.compareTo(endTime) <= 0; +} + +``` +但是呢,每次调用isUserVIPValid方法,都会创建Calendar和Date对象。其实吧,除了New Date,其他对象都是不变的,我们可以**抽出全局变量**,**避免创建了不必要的对象**,从而提高程序效率,如下: +``` +public class Test { + + private static final Date BEGIN_TIME; + private static final Date END_TIME; + static { + Calendar gmtCal = Calendar.getInstance(); + gmtCal.set(2019, Calendar.JANUARY, 1, 0, 0, 0); + BEGIN_TIME = gmtCal.getTime(); + gmtCal.set(2020, Calendar.JANUARY, 1, 0, 0, 0); + END_TIME = gmtCal.getTime(); + } + + //判断用户会员是否在有效期 + public boolean isUserVIPValid() { + Date now = new Date(); + return now.compareTo(BEGIN_TIME) >= 0 && now.compareTo(END_TIME) <= 0; + } +} +``` + +### 三、查询数据库时,你有没有查多了数据? +大家都知道,查库是比较耗时的操作,尤其数据量大的时候。所以,查询DB时,我们取所需就好,没有必要大包大揽。 + +假设业务场景是这样:查询某个用户是否是会员。曾经看过实现代码是这样。。。 +``` +List userIds = sqlMap.queryList("select userId from user where vip=1"); +boolean isVip = userIds.contains(userId); +``` +为什么先把所有会有查出来,再判断是否包含这个useId,来确定useId是否是会员呢?直接把userId传进sql,它不香吗?如下: +``` +Long userId = sqlMap.queryObject("select userId from user where userId='userId' and vip='1' ") +boolean isVip = userId!=null; +``` + +实际上,我们除了把查询条件都传过去,避免数据库查多余的数据回来,还可以通过**select 具体字段**代替```select *```,从而使程序更高效。 + +### 四、加了一行通知类的代码,总不能影响到主要流程吧。 +假设业务流程这样:需要在用户登陆时,添加个短信通知它的粉丝。 +很容易想到的实现流程如下: + +![](https://user-gold-cdn.xitu.io/2019/12/27/16f47ecebb5949d7?w=468&h=662&f=png&s=40806) + +假设提供sendMsgNotify服务的**系统挂了**,或者**调用sendMsgNotify失败**了,那么用户登陆就失败了。。。 + +一个通知功能导致了登陆主流程不可用,明显的捡了芝麻丢西瓜。那么有没有鱼鱼熊掌兼得的方法呢?有的,给发短信接口**捕获异常处理**,或者**另开线程异步处理**,如下: + +![](https://user-gold-cdn.xitu.io/2019/12/27/16f47f59d45320c4?w=744&h=639&f=png&s=49493) + +因此,我们添加通知类等不是非主要,可降级的接口时,应该静下心来考虑是否会影响主要流程,思考怎么处理最好。 + +### 五、对空指针保持嗅觉,如使用equals比较时,常量或确定值放左边。 +NullPointException在Java世界早已司空见惯,我们在写代码时,可以三思而后写,尽量避免低级的空指针问题。 + +比如有以下业务场景,判断用户是否是会员,经常可见如下代码: +``` +boolean isVip = user.getUserFlag().equals("1"); +``` +如果让这个行代码上生产环境,待君蓦然回首,可能那空指针bug,就在灯火阑珊处。显然,这样可能会产生空指针异常,因为user.getUserFlag()可能是null。 + +怎样避免空指针问题呢?把常量1放到左边就可以啦,如下: +``` +boolean isVip = "1".equals(user.getUserFlag()); +``` + +### 六、你的关键业务代码是否有日志保驾护航? +关键业务代码无论身处何地,都应该有足够的**日志**保驾护航。 + +比如:**你实现转账业务,转个几百万,然后转失败了,接着客户投诉,然后你还没有打印到日志,想想那种水深火热的困境下,你却毫无办法**。。。 + +那么,你的转账业务都**需要那些日志信息**呢?至少,方法调用前,入参需要打印需要吧,接口调用后,需要捕获一下异常吧,同时打印异常相关日志吧,如下: + +``` +public void transfer(TransferDTO transferDTO){ + log.info("invoke tranfer begin"); + //打印入参 + log.info("invoke tranfer,paramters:{}",transferDTO); + try { + res= transferService.transfer(transferDTO); + }catch(Exception e){ + log.error("transfer fail,cifno:{},account:{}",transferDTO.getCifno(), + transferDTO.getaccount()) + log.error("transfer fail,exception:{}",e); + } + log.info("invoke tranfer end"); + } +``` +除了打印足够的日志,我们还需要注意一点是,**日志级别别混淆使用**,别本该打印info的日志,你却打印成error级别,告警半夜三更催你起来排查问题就不好了。 + +### 七、对于行数比较多的函数,是否可以划分小函数来优化呢? +我们在维护老代码的时候,经常会见到一坨坨的代码,有些**函数几百行甚至上千行**,阅读起来比较吃力。 + +假设现在有以下代码 +``` +public class Test { + private String name; + private Vector orders = new Vector(); + + public void printOwing() { + //print banner + System.out.println("****************"); + System.out.println("*****customer Owes *****"); + System.out.println("****************"); + + //calculate totalAmount + Enumeration env = orders.elements(); + double totalAmount = 0.0; + while (env.hasMoreElements()) { + Order order = (Order) env.nextElement(); + totalAmount += order.getAmout(); + } + + //print details + System.out.println("name:" + name); + System.out.println("amount:" + totalAmount); + } +} +``` +**划分为功能单一的小函数后:** +``` +public class Test { + private String name; + private Vector orders = new Vector(); + + public void printOwing() { + + //print banner + printBanner(); + //calculate totalAmount + double totalAmount = getTotalAmount(); + //print details + printDetail(totalAmount); + } + + void printBanner(){ + System.out.println("****************"); + System.out.println("*****customer Owes *****"); + System.out.println("****************"); + } + + double getTotalAmount(){ + Enumeration env = orders.elements(); + double totalAmount = 0.0; + while (env.hasMoreElements()) { + Order order = (Order) env.nextElement(); + totalAmount += order.getAmout(); + } + return totalAmount; + } + + void printDetail(double totalAmount){ + System.out.println("name:" + name); + System.out.println("amount:" + totalAmount); + } + +} +``` +一个过于**冗长**的函数或者一段**需要注释才能让人理解**用途的代码,可以考虑把它切分成一个功能明确的函数单元,并定义清晰简短的函数名,这样会让代码变得更加优雅。 + +### 八、某些可变因素,如红包皮肤等等,做成配置化是否会更好呢。 +假如产品提了个红包需求,圣诞节的时候,红包皮肤为圣诞节相关的,春节的时候,红包皮肤等。 + +如果在代码写死控制,可有类似以下代码: +``` +if(duringChristmas){ + img = redPacketChristmasSkin; +}else if(duringSpringFestival){ + img = redSpringFestivalSkin; +} +...... +``` +如果到了元宵节的时候,运营小姐姐突然又有想法,红包皮肤换成灯笼相关的,这时候,是不是要去修改代码了,重新发布了? + +从一开始,实现一张红包皮肤的配置表,将红包皮肤做成配置化呢?更换红包皮肤,只需修改一下表数据就好了。 + + +### 九、多余的import 类,局部变量,没引用是不是应该删除 +如果看到代码存在没使用的import 类,没被使用到的局部变量等,就删掉吧,如下这些: +![](https://user-gold-cdn.xitu.io/2019/12/28/16f4b8634695bf0b?w=1367&h=418&f=png&s=56949) + +这些没被引用的局部变量,如果没被使用到,就删掉吧,它又不是陈年的女儿红,留着会越发醇香。它还是会一起被编译的,就是说它还是耗着资源的呢。 + +### 十、查询大表时,是否加了索引,你的sql走了索引嘛。 +查询数据量比较大的表时,我们需要确认三点: +- 你的表是否建了索引 +- 你的查询sql是否命中索引 +- 你的sql是否还有优化余地 + +一般情况下,数据量超过10万的表,就要考虑给表加索引了。哪些情况下,索引会失效呢?like通配符、索引列运算等会导致索引失效。有兴趣的朋友可以看一下我这篇文章。 +[后端程序员必备:索引失效的十大杂症](https://juejin.im/post/5de99dd2518825125e1ba49d) + + +### 十一、你的方法到底应该返回空集合还是 null呢? +如果返回null,调用方在忘记检测的时候,可能会抛出空指针异常。返回一个空集合呢,就省去该问题了。 + +mybatis查询的时候,如果返回一个集合,结果为空时也会返回一个空集合,而不是null。 + +**正例** +``` +public static List getUserResultList(){ + return Collections.EMPTY_LIST; +} +``` + +### 十二、初始化集合时尽量指定其大小 +阿里开发手册推荐了这一点 +![](https://user-gold-cdn.xitu.io/2019/12/28/16f4c63cf25c4b13?w=977&h=252&f=png&s=110156) + +假设你的map要存储的元素个数是15个左右,最优写法如下 +``` + //initialCapacity = 15/0.75+1=21 + Map map = new HashMap(21); + 又因为hashMap的容量跟2的幂有关,所以可以取32的容量 + Map map = new HashMap(32); +``` + +### 十三、查询数据库时,如果数据返回过多,考虑分批进行。 +假设你的订单表有10万数据要更新状态,不能一次性查询所有未更新的订单,要分批。 + +**反例:** +``` +List list = sqlMap.queryList("select * from Order where status='0'"); +for(Order order:list){ + order.setStatus(1); + sqlMap.update(order); +} +``` + +**正例:** +``` +Integer count = sqlMap.queryCount(select count(1) from Order where status ='0'); +while(true){ + int size=sqlMap.batchUpdate(params); + if(size<500){ + break; + } +} +``` + +### 十四、你的接口是否考虑到幂等性,并发情况呢? +**幂等性是什么?** +一次和多次请求某一个资源对于资源本身应该具有同样的结果。就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。 + +**为什么需要幂等性?** +- 用户在APP上连续点击了多次提交订单,总不能生成多个订单吧 +- 用户因为网络卡了,连续点击发送消息,接受者总不能收到重复的同一条消息吧。 + + +**假设有业务场景:** + +用户点击下载按钮,系统开始下载文件,用户再次点击下载,会提示文件正在下载中。 + +有一部分人会这样实现: +``` +Integer count = sqlMap.selectCount("select count(1) from excel where state=1"); +if(count<=0){ + Excel.setStatus(1); + updateExcelStatus(); + downLoadExcel(); +}else{ + "文件正在下载中" +} +``` +我们可以看一下,两个请求过来可能会有什么问题? + + + +![](https://user-gold-cdn.xitu.io/2019/12/28/16f4d0dc9516f996?w=1278&h=887&f=png&s=106484) + +执行流程: +- 第一步,A查询没有下载中的文件。 +- 第二步,B查询没有下载中的文件。 +- 第三步,A开始下载文件 +- 第四部,B 开始下载文件 + +显然,这样有问题,同时两个文件在下载了。正确的实现方式呢? +``` +if(updateExcelStatus(1){ + downLoadExcel(); +}else{ + "文件正在下载中" +} +``` +### 十五、用一个私有构造器强化你的工具类,此不美哉? +工具类的方法都是静态方法,通过类来直接调用即可。但是有些调用方可能会先实例化,再用对象去调用,而这就不好了。怎么避免这种情况,让你的工具类到达可控状态呢,**添加私有构造器** + +``` +public class StringUtis{ + private StringUtis(){} ///私有构造类,防止意外实例出现 + public static bool validataString(String str){ + + } +} +``` + +### 十六、基本不变的用户数据,缓存起来,性能是否有所提升呢 +假设你的接口需要查询很多次数据库,获取到各中数据,然后再根据这些数据进行各种排序等等操作,这一系列猛如虎的操作下来,接口性能肯定不好。典型应用场景比如:直播列表这些。 + +那么,怎么优化呢?剖析你排序的各部分数据,实时变的数据,继续查DB,不变的数据,如用户年龄这些,搞个定时任务,把它们从DB拉取到缓存,直接走缓存。 + +因此,这个点的思考就是,在恰当地时机,适当的使用缓存。 + +### 待补充... + +### 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。 +- 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。 +- github地址:https://github.com/whx123/JavaHome + + + + + diff --git "a/\345\267\245\344\275\234\346\200\273\347\273\223/\345\271\266\345\217\221\347\216\257\345\242\203\344\270\213\357\274\214\345\205\210\346\223\215\344\275\234\346\225\260\346\215\256\345\272\223\350\277\230\346\230\257\345\205\210\346\223\215\344\275\234\347\274\223\345\255\230\357\274\237.md" "b/\345\267\245\344\275\234\346\200\273\347\273\223/\345\271\266\345\217\221\347\216\257\345\242\203\344\270\213\357\274\214\345\205\210\346\223\215\344\275\234\346\225\260\346\215\256\345\272\223\350\277\230\346\230\257\345\205\210\346\223\215\344\275\234\347\274\223\345\255\230\357\274\237.md" new file mode 100644 index 0000000..6bcf165 --- /dev/null +++ "b/\345\267\245\344\275\234\346\200\273\347\273\223/\345\271\266\345\217\221\347\216\257\345\242\203\344\270\213\357\274\214\345\205\210\346\223\215\344\275\234\346\225\260\346\215\256\345\272\223\350\277\230\346\230\257\345\205\210\346\223\215\344\275\234\347\274\223\345\255\230\357\274\237.md" @@ -0,0 +1,119 @@ +## 前言 +在分布式系统中,缓存和数据库同时存在时,如果有写操作的时候,先操作数据库还是先操作缓存呢?先思考一下,可能会存在哪些问题,再往下看。下面我分几种方案阐述。 + +## 缓存维护方案一 +假设有一写(线程A)一读(线程B)操作,**先操作缓存,在操作数据库**。,如下流程图所示: +![](https://user-gold-cdn.xitu.io/2019/7/19/16c09e4c5c718c7a?w=849&h=513&f=png&s=52606) + +1)线程A发起一个写操作,第一步del cache + +2)线程A第二步写入新数据到DB + +3)线程B发起一个读操作,cache miss, + +4)线程B从DB获取最新数据 + +5)请求B同时set cache + +**这样看,没啥问题**。我们再看第二个流程图,如下: + +![](https://user-gold-cdn.xitu.io/2019/7/19/16c09e12153a79b8?w=803&h=517&f=png&s=52726) + +1)线程A发起一个写操作,第一步del cache + +2)此时线程B发起一个读操作,cache miss + +3)线程B继续读DB,读出来一个老数据 + +4)然后老数据入cache + +5)线程A写入了最新的数据 + +OK,酱紫,就有问题了吧,老数据入到缓存了,**每次读都是老数据啦,缓存与数据与数据库数据不一致**。 + +## 缓存维护方案二 +双写操作,**先操作缓存,在操作数据库**。 + +![](https://user-gold-cdn.xitu.io/2019/7/19/16c09f09e4c1c292?w=610&h=512&f=png&s=45931) + +1)线程A发起一个写操作,第一步set cache + +2)线程A第二步写入新数据到DB + +3)线程B发起一个写操作,set cache, + +4)线程B第二步写入新数据到DB + +**这样看,也没啥问题。**,但是有时候可能事与愿违,我们再看第二个流程图,如下: + +![](https://user-gold-cdn.xitu.io/2019/7/20/16c0eb335ee9b878?w=1001&h=676&f=png&s=71130) + +1)线程A发起一个写操作,第一步set cache + +2)线程B发起一个写操作,第一步setcache + +3)线程B写入数据库到DB + +4)线程A写入数据库到DB + +执行完后,缓存保存的是B操作后的数据,数据库是A操作后的数据,**缓存和数据库数据不一致**。 + +## 缓存维护方案三 + +一写(线程A)一读(线程B)操作,**先操作数据库,再操作缓存**。 + +![](https://user-gold-cdn.xitu.io/2019/7/20/16c0ec1c874c19eb?w=1076&h=637&f=png&s=79727) + +1)线程A发起一个写操作,第一步write DB + +2)线程A第二步del cache + +3)线程B发起一个读操作,cache miss + +4)线程B从DB获取最新数据 + +5)线程B同时set cache + +这种方案**没有明显的并发问题**,但是有可能**步骤二删除缓存失败**,虽然概率比较小,**优于方案一和方案二**,平时工作中也是使用方案三。 + +综上对比,我们一般采用方案三,但是有没有完美全解决方案三的弊端的方法呢? + +## 缓存维护方案四 +这个是方案三的改进方案,都是先操作数据库再操作缓存,我们来看一下流程图: +![](https://user-gold-cdn.xitu.io/2019/7/20/16c0f4116f0a062f?w=995&h=778&f=png&s=88742) + +通过数据库的**binlog**来**异步淘汰key**,以mysql为例 +可以**使用阿里的canal将binlog日志采集发送到MQ队列**里面,然后**通过ACK机制 +确认处理** 这条更新消息,删除缓存,保证数据缓存一致性。 + +但是呢还有个**问题,如果是主从数据库呢**? + +## 缓存维护方案五 + +主从DB问题:因为主从DB同步存在同时延时时间如果删除缓存之后,数据同步到备库之前已经有请求过来时,**会从备库中读到脏数据**,如何解决呢?解决方案如下流程图: + +![](https://user-gold-cdn.xitu.io/2019/7/20/16c0f481dee559a5?w=1019&h=908&f=png&s=109517) + +## 缓存维护总结 + 综上所述,在分布式系统中,缓存和数据库同时存在时,如果有写操作的时候,**先操作数据库,再操作缓存**。如下: + +(1)读取缓存中是否有相关数据 + +(2)如果缓存中有相关数据value,则返回 + +(3)如果缓存中没有相关数据,则从数据库读取相关数据放入缓存中key->value,再返回 + +(4)如果有更新数据,则先更新数据,再删除缓存 + +(5)为了保证第四步删除缓存成功,使用binlog异步删除 + +(6)如果是主从数据库,binglog取自于从库 + +(7)如果是一主多从,每个从库都要采集binlog,然后消费端收到最后一台binlog数据才删除缓存 + +## 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 欢迎大家关注,大家一起学习,一起讨论哈。 +- github地址:https://github.com/whx123/JavaHome \ No newline at end of file diff --git "a/\345\267\245\344\275\234\346\200\273\347\273\223/\346\227\245\345\270\270\345\267\245\344\275\234\344\270\255\346\234\200\345\256\271\346\230\223\347\212\257\347\232\204\345\207\240\344\270\252\345\271\266\345\217\221\351\224\231\350\257\257.md" "b/\345\267\245\344\275\234\346\200\273\347\273\223/\346\227\245\345\270\270\345\267\245\344\275\234\344\270\255\346\234\200\345\256\271\346\230\223\347\212\257\347\232\204\345\207\240\344\270\252\345\271\266\345\217\221\351\224\231\350\257\257.md" new file mode 100644 index 0000000..6ada66f --- /dev/null +++ "b/\345\267\245\344\275\234\346\200\273\347\273\223/\346\227\245\345\270\270\345\267\245\344\275\234\344\270\255\346\234\200\345\256\271\346\230\223\347\212\257\347\232\204\345\207\240\344\270\252\345\271\266\345\217\221\351\224\231\350\257\257.md" @@ -0,0 +1,272 @@ +### 前言 +列举大家平时在工作中最容易犯的几个并发错误,都是在实际项目代码中看到的鲜活例子,希望对大家有帮助。 + +### First Blood +线上总是出现:**ERROR 1062 (23000) Duplicate entry 'xxx' for key 'yyy'**,我们来看一下有问题的这段代码: +```Java +UserBindInfo info = selectFromDB(userId); +if(info == null){ + info = new UserBindInfo(userId,deviceId); + insertIntoDB(info); +}else{ + info.setDeviceId(deviceId); + updateDB(info); + } +``` +在**并发情况**下,**第一步判断都为空,就会有2个或者多个线程进入插入数据库操作,** +这时候就出现了同一个ID插入多次。 + +**正确处理姿势:** +```SQL +insert into UserBindInfo values(#{userId},#{deviceId}) on duplicate key update deviceId=#{deviceId}多次的情况,导致插入失败。 +``` +一般情况下,可以用**insert...on duplicate key update...** 解决这个问题。 + +**注意:** 如果UserBindInfo表存在**主键以及一个以上的唯一索引**,在并发情况下,使用insert...on duplicate key,可能会产生死锁(Mysql5.7),可以这样处理: +```Java +try{ + UserBindInfoMapper.insertIntoDB(userBindInfo); +}catch(DuplicateKeyException ex){ + UserBindInfoMapper.update(userBindInfo); +} +``` +### Double Kill +**小心你的全局变量**,如下面这段代码: + +``` +public class GlobalVariableConcurrentTest { + + private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public static void main(String[] args) throws InterruptedException { + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1000)); + + while (true){ + threadPoolExecutor.execute(()->{ + String dateString = sdf.format(new Date()); + try { + Date parseDate = sdf.parse(dateString); + String dateString2 = sdf.format(parseDate); + System.out.println(dateString.equals(dateString2)); + } catch (ParseException e) { + e.printStackTrace(); + } + }); + } + + } + +} +``` +可以看到有异常抛出 +![](https://user-gold-cdn.xitu.io/2019/12/22/16f2b89cbdff0933?w=1434&h=620&f=png&s=139563) + +全局变量的SimpleDateFormat,在并发情况下,存在安全性问题,阿里Java规约明确要求谨慎使用它。 + +**除了SimpleDateFormat,其实很多时候,面对全局变量,我们都需要考虑并发情况是否存在问题,如下** +```Java +@Component +public class Test { + + public static List desc = new ArrayList<>(); + + public List getDescByUserType(int userType) { + if (userType == 1) { + desc.add("普通会员不可以发送和查看邮件,请购买会员"); + return desc; + } else if (userType == 2) { + desc.add("恭喜你已经是VIP会员,尽情的发邮件吧"); + return desc; + }else { + desc.add("你的身份未知"); + return desc; + } + } +} +``` +因为desc是全局变量,在并发情况下,请求getDescByUserType方法,得到的可能并不是你想要的结果。 + + +### Trible Kill +假设现在有如下业务:控制同一个用户访问某个接口的频率不能小于5秒。一般很容易想到**使用redis的 setnx操作来控制并发访问**,于是有以下代码: +```Java +if(RedisOperation.setnx(userId, 1)){ + RedisOperation.expire(userId,5,TimeUnit.SECONDS)); + //执行正常业务逻辑 +}else{ + return “访问过于频繁”; +} +``` +假设执行完setnx操作,还没来得及设置expireTime,机器重启或者突然崩溃,将会发生死锁。该用户id,后面执行setnx永远将为false,**这可能让你永远损失那个用户**。 + +那么怎么解决这个问题呢,可以考虑用**SET key value NX EX max-lock-time** ,它是一种在 Redis 中实现锁的方法,是原子性操作,不会像以上代码分两步执行,先set再expire,它是**一步到位**。 + +**客户端执行以上的命令:** +- 如果服务器返回 OK ,那么这个客户端获得锁。 +- 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。 +- 设置的过期时间到达之后,锁将自动释放 + +### Quadra Kill +我们看一下有关ConcurrentHashMap的一段代码,如下: +```Java +//全局变量 +Map map = new ConcurrentHashMap(); + +Integer value = count.get(k); +if(value == null){ + map.put(k,1); +}else{ + map.put(k,value+1); +} +``` +假设两条线程都进入 **value==null**,这一步,得出的结果是不是会变小?OK,客官先稍作休息,闭目养神一会,我们验证一下,请看一个demo: +```Java + public static void main(String[] args) { + for (int i = 0; i < 1000; i++) { + testConcurrentMap(); + } + } + private static void testConcurrentMap() { + final Map count = new ConcurrentHashMap<>(); + ExecutorService executorService = Executors.newFixedThreadPool(2); + final CountDownLatch endLatch = new CountDownLatch(2); + Runnable task = ()-> { + for (int i = 0; i < 5; i++) { + Integer value = count.get("k"); + if (null == value) { + System.out.println(Thread.currentThread().getName()); + count.put("k", 1); + } else { + count.put("k", value + 1); + } + } + endLatch.countDown(); + }; + + executorService.execute(task); + executorService.execute(task); + + try { + endLatch.await(); + if (count.get("k") < 10) { + System.out.println(count); + } + } catch (Exception e) { + e.printStackTrace(); + } +``` +表面看,运行结果应该都是10对吧,好的,我们再看运行结果 +: +![](https://user-gold-cdn.xitu.io/2019/7/20/16c0d5d23cea0033?w=729&h=267&f=png&s=36205) + + +运行结果出现了5,所以这样实现是有并发问题的,那么正确的实现姿势是啥呢? +```java +Map map = new ConcurrentHashMap(); +V v = map.get(k); +if(v == null){ + v = new V(); + V old = map. putIfAbsent(k,v); + if(old != null){ + v = old; + } +} + +``` +可以考虑使用**putIfAbsent**解决这个问题 + +(1)如果key是新的记录,那么会向map中添加该键值对,并返回null。 + +(2)如果key已经存在,那么不会覆盖已有的值,返回已经存在的值 + +我们再来看看以下代码以及运行结果: +``` + public static void main(String[] args) { + for (int i = 0; i < 1000; i++) { + testConcurrentMap(); + } + } + + private static void testConcurrentMap() { + ExecutorService executorService = Executors.newFixedThreadPool(2); + final Map map = Maps.newConcurrentMap(); + final CountDownLatch countDownLatch = new CountDownLatch(2); + + Runnable task = ()-> { + AtomicInteger oldValue; + for (int i = 0; i < 5; i++) { + oldValue = map.get("k"); + if (null == oldValue) { + AtomicInteger initValue = new AtomicInteger(0); + oldValue = map.putIfAbsent("k", initValue); + if (oldValue == null) { + oldValue = initValue; + } + } + oldValue.incrementAndGet(); + } + countDownLatch.countDown(); + }; + + executorService.execute(task); + executorService.execute(task); + + try { + countDownLatch.await(); + System.out.println(map); + } catch (Exception e) { + e.printStackTrace(); + } + } +``` +![](https://user-gold-cdn.xitu.io/2019/7/20/16c0e5565754c1f6?w=768&h=520&f=png&s=52989) + + +### Penta Kill +现有如下业务场景:用户手上有一张现金券,可以兑换相应的现金, +#### 错误示范一 +```Java +if(isAvailable(ticketId){ + 1、给现金增加操作 + 2、deleteTicketById(ticketId) +}else{ + return “没有可用现金券” +} +``` +**解析:** 假设有两条线程A,B兑换现金,执行顺序如下: + +![](https://user-gold-cdn.xitu.io/2019/7/20/16c0e675c7ea6200?w=720&h=801&f=png&s=74320) + +- 1.线程A加现金 +- 2.线程B加现金 +- 3.线程A删除票标志 +- 4.线程B删除票标志 + +显然,**这样有问题了,已经给用户加了两次现金了**。 + +#### 错误示范2 +``` +if(isAvailable(ticketId){ + 1、deleteTicketById(ticketId) + 2、给现金增加操作 +}else{ + return “没有可用现金券” +} +``` +并发情况下,如果一条线程,第一步deleteTicketById删除失败了,也会多添加现金。 + +#### 正确处理方案 +``` +if(deleteAvailableTicketById(ticketId) == 1){ + 1、给现金增加操作 +}else{ + return “没有可用现金券” +} +``` + +### 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。 +- 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。 \ No newline at end of file diff --git "a/\347\250\213\345\272\217\344\272\272\347\224\237&\351\235\242\350\257\225\345\273\272\350\256\256/\351\207\221\344\270\211\351\223\266\345\233\233\357\274\214\347\273\231\351\235\242\350\257\225\350\200\205\347\232\204\345\215\201\345\244\247\345\273\272\350\256\256.md" "b/\347\250\213\345\272\217\344\272\272\347\224\237&\351\235\242\350\257\225\345\273\272\350\256\256/\351\207\221\344\270\211\351\223\266\345\233\233\357\274\214\347\273\231\351\235\242\350\257\225\350\200\205\347\232\204\345\215\201\345\244\247\345\273\272\350\256\256.md" new file mode 100644 index 0000000..6909f79 --- /dev/null +++ "b/\347\250\213\345\272\217\344\272\272\347\224\237&\351\235\242\350\257\225\345\273\272\350\256\256/\351\207\221\344\270\211\351\223\266\345\233\233\357\274\214\347\273\231\351\235\242\350\257\225\350\200\205\347\232\204\345\215\201\345\244\247\345\273\272\350\256\256.md" @@ -0,0 +1,82 @@ +### 一、提前复习好你的专业知识 +专业知识是最为重要的一点,拥有了坚实的专业基础,你才能迈向成功的彼岸。 + +因此,面试之前,一定一定要复习好专业知识。对自己学过的知识,要做一个概括,放在脑海中。茶余饭后,复习一下,做到随便看到一道基础题目,心中都能有个答案。 + +比如,一道最基本基础题,ArrayList和LinkedList有什么区别?如果你是做Java后台开发的,应该都会了吧,哈哈,不会的赶紧复习一下,哈哈哈。 + +所以,面试前还是像图下这位小伙子一样,好好复习,哈哈~ +![](https://user-gold-cdn.xitu.io/2020/2/14/17043f9e65f6c1a2?w=1919&h=1080&f=png&s=1153224) + +### 二、对简历上写的项目一定要足够了解,把握其中的亮点。 +你在简历上的信息,就是面试官了解你的窗口。你写上去的项目,自己一定一定要了解清楚来龙去脉。如果把别人很厉害的项目copy上去,面试官一问你三不知,那就露馅啦~ + +同时,简历上需要沉淀一些有内容的东西,需要有些亮点。当然,简历上的亮点并不一定是酝酿百年的女儿红,也可以是你自己含辛茹苦酿造出的米酒,只要有你汗水的味道体现在里面就可以啦。 + +也就是说,你的简历不一定就需要是github上几百star的项目,也可以是你自己负责设计的一些有意思的项目,甚至一个小小的挂号系统,只要你能在里面,倾注了你的思考与汗水,并且让面试官感受得到,那面试这场战役就赢一半了。 + +最后,放一下我之前项目中(个人觉得是个小亮点),用CAS思想解决实际并发问题的实践~ + +[CAS乐观锁解决并发问题的一次实践](https://juejin.im/post/5d0616ade51d457756536791) + + +### 三、面试一定要杜绝过度紧张,要心平气和去对待。 +面试过程一定要杜绝过度紧张,紧张可能会导致你发挥失常,让你基础的问题都忘了怎么回答,最后与理想offer失之交臂。 + +有点小紧张也是可以接受的,这一点会促使你认真地准备,但是过度紧张就适得其反啦。 + +因此,面试前两天,你可以跑一下步,深呼吸几下,或者心理面想一下过去一些美妙的事情,或者运动一下,或者唱个歌,弹一下琴等都可以。 + +平时如果觉得生活压抑,或者紧张的话,我会弹弹吉他,唱唱歌,哈哈 +![](https://user-gold-cdn.xitu.io/2020/2/14/17043f1531a9fc6e?w=1080&h=2248&f=png&s=580499) +### 四、面试前充分了解公司以及工作岗位内容 +面试前,多点了解公司是做什么业务的,以及工作岗位的主要工作内容。结合招聘要求,提前想一下面试官可能问的问题,换位思考以及延伸思考。比如,你面的是一间银行的开发岗,该银行用到自研的MQ通讯,那么,你需要准备好https相关的面试题,消息中间件的相关面试题等等。 + +如:https原理是什么?谈谈RocketMQ消息顺序和重复消费问题等。 + +### 五、面试过程中,把面试官引到自己熟悉的领域,重拳出击。 + +面试过程中,需要学会把面试官引到自己熟悉的领域。 + +打个比方,假如你对索引这一块特别熟悉的话,面试官让你介绍你做了项目/什么项目优化时,你可以举例通过索引做了慢查询优化等等,这时候,面试官十之八九会问你索引相关问题,这时候,你可以把覆盖索引、最左匹配原则、聚族索引、回表、查询优化统统搬出来啦。 + +最好就是结合一些流程图、原理图分析自己优化过程,让面试官知道你的思考轨迹,这时候,面试官才更容易认可你。 + +在这里,我忍不住想分享自己之前话的美美的一张图,InnoDb 逻辑存储结构图,哈哈,如下所示: +![](https://user-gold-cdn.xitu.io/2020/2/14/17043eabf1e65973?w=1215&h=901&f=png&s=396084) + +### 六、有认识的人内推比在boss直聘、拉钩等,通过概率会高点。 + +如果你要面一个大厂,有认识的人内推最好不过啦,其实内推过得概率大很多的。因为,大家都懂一个道理,优秀的人旁边,也是一些优秀的人,正所谓**物以类聚,人以群分**。 +所以,多数HR也是这样挑人的,如果你通过内推获得面试机会,好好表现吧,骚年。 + + +![](https://user-gold-cdn.xitu.io/2020/2/14/1704426243d9ab07?w=255&h=255&f=png&s=104800) + +### 七、在工作时一点一字积累,在面试时,一字一词表现。 + +这句话意思就是说,在学习工作过程中,我们需要一点一点积累,尤其实一些细节的地方,容易犯错的地方。然后,面试的时候,把这些细节,在面试官面前展示出来,吐字清晰,一字一词地表现。酱紫的话,面试官内心会对你加分的,觉得你是个有心人. + +比如,一下是我工作中代码细节的总结,有兴趣可以看看哈~ + +[写代码有这些想法,同事才不会认为你是复制粘贴程序员](https://juejin.im/post/5dfe2e72518825125f39a2de) +### 八、面试尽量拿多几个offer,这样才能掌握主动权,不然HR可能会压榨你。 +面试找工作,对待offer。需要吃着碗里的,看着锅里的。要不然,如果你只有一个offer,HR跟你谈薪的时候,很可能会压榨你的价值。拿多几个offer,可以有谈薪的资本。 + +![](https://user-gold-cdn.xitu.io/2020/2/14/1704423b8e7982ac?w=800&h=450&f=png&s=613615) + +### 九、多点刷专业笔试题,程序员的话,争取成为leetcode的常客。 +阿里、腾讯、头条这些公司,面试经常要求手动写编程题,所以,作为面试者,要想过关,一定需要多点刷题,尤其是leetcode官网上面的题,不求每道题都能背下来,但是至少,每种类型的题目,你都需要知道思路,需要知道大概怎么实现吧,如:动态规划问题、链表操作等等。 + +### 十、学习一门乐器,坚持一项运动。 +学习一门乐器,是为了让生活多一份诗意,坚持一项运动,是为了身体健康。 + +学习一门乐器,在面试官看来会加分的,因为年会可以上去表演哈哈~ 坚持一项运动,也是加分的,因为一般公司都有运动小组,篮球小组,或者羽毛球,如果面试官也跟你一样爱好篮球,说不定,你们就可以聊聊今年湖人总冠军了,哈哈。并且,运动的人最阳光啦,哈哈,不信你看我~ +![](https://user-gold-cdn.xitu.io/2020/2/14/170440b9ea8d6349?w=1620&h=1080&f=png&s=2382365) + +### 个人公众号 + +![](https://user-gold-cdn.xitu.io/2019/7/28/16c381c89b127bbb?w=344&h=344&f=jpeg&s=8943) + +- 觉得写得好的小伙伴给个点赞+关注啦,谢谢~ +- 同时非常期待小伙伴们能够关注我公众号,后面慢慢推出更好的干货~嘻嘻