From 30d86659ac55a307664b15abb811b27823a9e5da Mon Sep 17 00:00:00 2001 From: panjiahao <815057904@qq.com> Date: Mon, 26 Feb 2024 20:07:22 +0800 Subject: [PATCH] Site updated: 2024-02-26 20:07:21 --- .../index.html" | 693 ++ .../index.html" | 669 ++ .../index.html" | 709 ++ .../index.html" | 729 ++ .../index.html" | 666 ++ .../index.html" | 656 ++ .../index.html" | 660 ++ .../index.html" | 676 ++ .../index.html" | 744 ++ .../index.html" | 738 ++ .../index.html" | 669 ++ .../index.html" | 695 ++ .../index.html" | 922 ++ .../index.html" | 1037 ++ .../index.html" | 773 ++ .../index.html" | 700 ++ .../index.html" | 666 ++ .../index.html" | 697 ++ .../index.html" | 661 ++ .../index.html" | 1923 ++++ .../index.html" | 688 ++ .../index.html" | 1153 +++ .../index.html" | 871 ++ .../index.html" | 825 ++ .../index.html" | 1075 ++ .../index.html" | 841 ++ .../index.html" | 831 ++ .../index.html" | 811 ++ .../index.html" | 869 ++ .../index.html" | 864 ++ .../index.html" | 668 ++ .../index.html" | 1386 +++ .../index.html" | 2241 ++++ .../index.html" | 754 ++ .../index.html" | 1136 ++ 404.html | 574 + about/index.html | 541 + archives/2019/12/index.html | 479 + archives/2019/index.html | 479 + archives/2020/01/index.html | 529 + archives/2020/02/index.html | 459 + archives/2020/index.html | 539 + archives/2022/05/index.html | 519 + archives/2022/06/index.html | 489 + archives/2022/07/index.html | 489 + archives/2022/08/index.html | 479 + archives/2022/12/index.html | 459 + archives/2022/index.html | 575 + archives/2022/page/2/index.html | 565 + archives/2023/01/index.html | 459 + archives/2023/02/index.html | 459 + archives/2023/03/index.html | 459 + archives/2023/06/index.html | 459 + archives/2023/index.html | 489 + archives/index.html | 593 ++ archives/page/2/index.html | 575 + archives/page/3/index.html | 593 ++ archives/page/4/index.html | 543 + atom.xml | 578 ++ books/index.html | 604 ++ categories/Git/index.html | 478 + categories/Java/index.html | 576 ++ categories/Java/page/2/index.html | 512 + categories/Linux/index.html | 478 + categories/Maven/index.html | 486 + categories/MySQL/index.html | 478 + categories/index.html | 923 ++ .../index.html" | 576 ++ .../page/2/index.html" | 520 + .../index.html" | 478 + .../index.html" | 478 + .../\346\235\202\351\241\271/index.html" | 486 + .../\347\256\227\346\263\225/index.html" | 478 + content.json | 1 + css/style.css | 9213 +++++++++++++++++ css/style.min.css | 9 + css/vs2015.css | 29 + favicon.ico | Bin 0 -> 1150 bytes fonts/README.html | 8 + fonts/iconfont.eot | Bin 0 -> 23452 bytes fonts/iconfont.svg | 321 + fonts/iconfont.ttf | Bin 0 -> 23300 bytes fonts/iconfont.woff | Bin 0 -> 16108 bytes ...\345\256\214\346\225\264\347\211\210.html" | 0 images/avatar.jpg | Bin 0 -> 63059 bytes images/donate/alipayimg.png | Bin 0 -> 156172 bytes images/donate/wechatpayimg.png | Bin 0 -> 116094 bytes images/favatar/SzsFox-logo.png | Bin 0 -> 2724 bytes images/favatar/chuangzaoshi-logo.png | Bin 0 -> 851 bytes images/favatar/idesign-logo.png | Bin 0 -> 2521 bytes images/favicon.ico | Bin 0 -> 1150 bytes images/logo.png | Bin 0 -> 63059 bytes images/thumb-default.png | Bin 0 -> 3862 bytes images/xingqiu-qrcode.jpg | Bin 0 -> 25752 bytes index.html | 892 ++ js/application.js | 77 + js/application.min.js | 1 + js/insight.js | 240 + js/jquery.min.js | 5 + js/plugin.js | 2470 +++++ js/plugin.js.map | 1 + js/plugin.min.js | 3 + links/index.html | 469 + page/2/index.html | 892 ++ page/3/index.html | 892 ++ page/4/index.html | 672 ++ repository/index.html | 525 + sitemap.txt | 100 + sitemap.xml | 794 ++ tags/CORS/index.html | 553 + tags/Cryptography/index.html | 555 + tags/Dan-Boneh/index.html | 555 + tags/Effective-Java/index.html | 765 ++ tags/Git/index.html | 551 + tags/Java-Concurrency-In-Practice/index.html | 553 + tags/Java/index.html | 553 + .../index.html" | 553 + tags/LCS/index.html | 557 + tags/LeetCode/index.html | 555 + tags/Levenshtein/index.html | 557 + tags/Linux/index.html | 553 + tags/Maven/index.html | 575 + tags/MetaMask/index.html | 581 ++ tags/MySQL/index.html | 553 + tags/Shell/index.html | 553 + tags/Stream/index.html | 555 + tags/algorithm/index.html | 555 + tags/cpp/index.html | 555 + tags/diff/index.html | 557 + tags/ganache/index.html | 555 + tags/index.html | 2872 +++++ .../index.html" | 555 + tags/merge/index.html | 557 + tags/truffle/index.html | 553 + .../index.html" | 553 + .../index.html" | 553 + .../index.html" | 557 + .../index.html" | 787 ++ "tags/\345\256\236\344\271\240/index.html" | 551 + .../index.html" | 585 ++ .../index.html" | 553 + "tags/\345\274\202\345\270\270/index.html" | 553 + .../index.html" | 553 + "tags/\346\226\271\346\263\225/index.html" | 553 + "tags/\346\236\232\344\270\276/index.html" | 555 + .../index.html" | 761 ++ "tags/\346\263\233\345\236\213/index.html" | 553 + "tags/\346\263\250\350\247\243/index.html" | 555 + .../index.html" | 551 + .../index.html" | 557 + .../index.html" | 553 + .../index.html" | 557 + "tags/\350\275\254\350\264\246/index.html" | 555 + .../index.html" | 553 + .../index.html" | 557 + "tags/\351\235\242\350\257\225/index.html" | 551 + 156 files changed, 97335 insertions(+) create mode 100644 "2019/12/26/\346\257\224\347\211\271\345\270\201\347\232\204\345\257\206\347\240\201\345\255\246\345\216\237\347\220\206/index.html" create mode 100644 "2019/12/26/\346\257\224\347\211\271\345\270\201\347\232\204\346\225\260\346\215\256\347\273\223\346\236\204/index.html" create mode 100644 "2019/12/27/\346\257\224\347\211\271\345\270\201\345\215\217\350\256\256/index.html" create mode 100644 "2020/01/06/\346\257\224\347\211\271\345\270\201\345\256\236\347\216\260/index.html" create mode 100644 "2020/01/06/\346\257\224\347\211\271\345\270\201\346\214\226\347\237\277\351\232\276\345\272\246/index.html" create mode 100644 "2020/01/06/\346\257\224\347\211\271\345\270\201\347\275\221\347\273\234/index.html" create mode 100644 "2020/01/07/\346\257\224\347\211\271\345\270\201\347\232\204\345\210\206\345\217\211/index.html" create mode 100644 "2020/01/07/\346\257\224\347\211\271\345\270\201\347\232\204\345\214\277\345\220\215\346\200\247/index.html" create mode 100644 "2020/01/07/\346\257\224\347\211\271\345\270\201\347\232\204\350\204\232\346\234\254/index.html" create mode 100644 "2020/01/08/\344\273\245\345\244\252\345\235\212/index.html" create mode 100644 "2020/01/18/MetaMask\350\277\236\346\216\245\347\247\201\346\234\211\351\223\276\345\217\221\347\224\237\347\232\204\350\275\254\350\264\246\351\227\256\351\242\230/index.html" create mode 100644 "2020/02/10/truffle\351\203\250\347\275\262\345\220\210\347\272\246\345\210\260\350\277\234\347\250\213\347\247\201\346\234\211\351\223\276/index.html" create mode 100644 "2022/05/06/Git\345\255\246\344\271\240\347\254\224\350\256\260/index.html" create mode 100644 "2022/05/07/LeetCode\345\210\267\351\242\230\350\256\260\345\275\225-cpp/index.html" create mode 100644 "2022/05/07/MIT-Missing-Semester\345\255\246\344\271\240\347\254\224\350\256\260/index.html" create mode 100644 "2022/05/07/Maven\345\255\246\344\271\240\347\254\224\350\256\260/index.html" create mode 100644 "2022/05/07/Maven\351\227\256\351\242\230\350\256\260\345\275\225/index.html" create mode 100644 "2022/05/13/\345\237\272\344\272\216\345\214\272\345\235\227\351\223\276\347\232\204\345\256\211\345\205\250\347\224\265\345\255\220\351\200\211\344\270\276\347\263\273\347\273\237/index.html" create mode 100644 "2022/05/23/2022-5-23-\346\237\220\345\214\272\345\235\227\351\223\276\345\205\254\345\217\270\351\235\242\350\257\225\350\256\260\345\275\225/index.html" create mode 100644 "2022/06/04/Cryptography\342\205\240\347\254\224\350\256\260/index.html" create mode 100644 "2022/06/22/\350\267\250\345\237\237\351\227\256\351\242\230/index.html" create mode 100644 "2022/06/23/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\270\200\357\274\211/index.html" create mode 100644 "2022/06/30/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\272\214\357\274\211/index.html" create mode 100644 "2022/07/07/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\270\211\357\274\211/index.html" create mode 100644 "2022/07/14/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\345\233\233\357\274\211/index.html" create mode 100644 "2022/07/23/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\272\224\357\274\211/index.html" create mode 100644 "2022/07/30/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\345\205\255\357\274\211/index.html" create mode 100644 "2022/08/01/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\270\203\357\274\211/index.html" create mode 100644 "2022/08/06/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\345\205\253\357\274\211/index.html" create mode 100644 "2022/08/13/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\271\235\357\274\211/index.html" create mode 100644 "2022/12/03/2022.06-2022.11\345\256\236\344\271\240\346\200\273\347\273\223/index.html" create mode 100644 "2023/01/13/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\345\255\246\344\271\240\347\254\224\350\256\260/index.html" create mode 100644 "2023/02/01/MySQL\345\256\236\346\210\23045\350\256\262\345\255\246\344\271\240\347\254\224\350\256\260/index.html" create mode 100644 "2023/03/03/Excel\346\257\224\345\257\271\344\270\216\345\220\210\345\271\266\347\263\273\347\273\237/index.html" create mode 100644 "2023/06/10/\346\267\261\345\205\245\347\220\206\350\247\243Java\350\231\232\346\213\237\346\234\272\345\255\246\344\271\240\347\254\224\350\256\260/index.html" create mode 100644 404.html create mode 100644 about/index.html create mode 100644 archives/2019/12/index.html create mode 100644 archives/2019/index.html create mode 100644 archives/2020/01/index.html create mode 100644 archives/2020/02/index.html create mode 100644 archives/2020/index.html create mode 100644 archives/2022/05/index.html create mode 100644 archives/2022/06/index.html create mode 100644 archives/2022/07/index.html create mode 100644 archives/2022/08/index.html create mode 100644 archives/2022/12/index.html create mode 100644 archives/2022/index.html create mode 100644 archives/2022/page/2/index.html create mode 100644 archives/2023/01/index.html create mode 100644 archives/2023/02/index.html create mode 100644 archives/2023/03/index.html create mode 100644 archives/2023/06/index.html create mode 100644 archives/2023/index.html create mode 100644 archives/index.html create mode 100644 archives/page/2/index.html create mode 100644 archives/page/3/index.html create mode 100644 archives/page/4/index.html create mode 100644 atom.xml create mode 100644 books/index.html create mode 100644 categories/Git/index.html create mode 100644 categories/Java/index.html create mode 100644 categories/Java/page/2/index.html create mode 100644 categories/Linux/index.html create mode 100644 categories/Maven/index.html create mode 100644 categories/MySQL/index.html create mode 100644 categories/index.html create mode 100644 "categories/\345\214\272\345\235\227\351\223\276/index.html" create mode 100644 "categories/\345\214\272\345\235\227\351\223\276/page/2/index.html" create mode 100644 "categories/\345\237\272\347\241\200\347\237\245\350\257\206/index.html" create mode 100644 "categories/\345\257\206\347\240\201\345\255\246/index.html" create mode 100644 "categories/\346\235\202\351\241\271/index.html" create mode 100644 "categories/\347\256\227\346\263\225/index.html" create mode 100644 content.json create mode 100644 css/style.css create mode 100644 css/style.min.css create mode 100644 css/vs2015.css create mode 100644 favicon.ico create mode 100644 fonts/README.html create mode 100644 fonts/iconfont.eot create mode 100644 fonts/iconfont.svg create mode 100644 fonts/iconfont.ttf create mode 100644 fonts/iconfont.woff rename placeholder => "hidden/Excel\346\257\224\345\257\271\344\270\216\345\220\210\345\271\266\347\263\273\347\273\237_\345\256\214\346\225\264\347\211\210.html" (100%) create mode 100644 images/avatar.jpg create mode 100644 images/donate/alipayimg.png create mode 100644 images/donate/wechatpayimg.png create mode 100644 images/favatar/SzsFox-logo.png create mode 100644 images/favatar/chuangzaoshi-logo.png create mode 100644 images/favatar/idesign-logo.png create mode 100644 images/favicon.ico create mode 100644 images/logo.png create mode 100644 images/thumb-default.png create mode 100644 images/xingqiu-qrcode.jpg create mode 100644 index.html create mode 100644 js/application.js create mode 100644 js/application.min.js create mode 100644 js/insight.js create mode 100644 js/jquery.min.js create mode 100644 js/plugin.js create mode 100644 js/plugin.js.map create mode 100644 js/plugin.min.js create mode 100644 links/index.html create mode 100644 page/2/index.html create mode 100644 page/3/index.html create mode 100644 page/4/index.html create mode 100644 repository/index.html create mode 100644 sitemap.txt create mode 100644 sitemap.xml create mode 100644 tags/CORS/index.html create mode 100644 tags/Cryptography/index.html create mode 100644 tags/Dan-Boneh/index.html create mode 100644 tags/Effective-Java/index.html create mode 100644 tags/Git/index.html create mode 100644 tags/Java-Concurrency-In-Practice/index.html create mode 100644 tags/Java/index.html create mode 100644 "tags/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/index.html" create mode 100644 tags/LCS/index.html create mode 100644 tags/LeetCode/index.html create mode 100644 tags/Levenshtein/index.html create mode 100644 tags/Linux/index.html create mode 100644 tags/Maven/index.html create mode 100644 tags/MetaMask/index.html create mode 100644 tags/MySQL/index.html create mode 100644 tags/Shell/index.html create mode 100644 tags/Stream/index.html create mode 100644 tags/algorithm/index.html create mode 100644 tags/cpp/index.html create mode 100644 tags/diff/index.html create mode 100644 tags/ganache/index.html create mode 100644 tags/index.html create mode 100644 "tags/lambda\350\241\250\350\276\276\345\274\217/index.html" create mode 100644 tags/merge/index.html create mode 100644 tags/truffle/index.html create mode 100644 "tags/\344\273\245\345\244\252\345\235\212/index.html" create mode 100644 "tags/\345\210\233\345\273\272\345\222\214\351\224\200\346\257\201\345\257\271\350\261\241/index.html" create mode 100644 "tags/\345\214\272\345\235\227\351\223\276/index.html" create mode 100644 "tags/\345\214\272\345\235\227\351\223\276\346\212\200\346\234\257\344\270\216\345\272\224\347\224\250/index.html" create mode 100644 "tags/\345\256\236\344\271\240/index.html" create mode 100644 "tags/\345\257\206\347\240\201\345\255\246/index.html" create mode 100644 "tags/\345\257\271\350\261\241\347\232\204\351\200\232\347\224\250\346\226\271\346\263\225/index.html" create mode 100644 "tags/\345\274\202\345\270\270/index.html" create mode 100644 "tags/\346\225\260\346\215\256\345\272\223/index.html" create mode 100644 "tags/\346\226\271\346\263\225/index.html" create mode 100644 "tags/\346\236\232\344\270\276/index.html" create mode 100644 "tags/\346\257\224\347\211\271\345\270\201/index.html" create mode 100644 "tags/\346\263\233\345\236\213/index.html" create mode 100644 "tags/\346\263\250\350\247\243/index.html" create mode 100644 "tags/\346\267\261\345\205\245\347\220\206\350\247\243Java\350\231\232\346\213\237\346\234\272/index.html" create mode 100644 "tags/\347\224\265\345\255\220\351\200\211\344\270\276/index.html" create mode 100644 "tags/\347\261\273\345\222\214\346\216\245\345\217\243/index.html" create mode 100644 "tags/\347\274\226\350\276\221\350\267\235\347\246\273/index.html" create mode 100644 "tags/\350\275\254\350\264\246/index.html" create mode 100644 "tags/\351\200\232\347\224\250\347\250\213\345\272\217\350\256\276\350\256\241/index.html" create mode 100644 "tags/\351\232\220\347\247\201\350\256\241\347\256\227/index.html" create mode 100644 "tags/\351\235\242\350\257\225/index.html" diff --git "a/2019/12/26/\346\257\224\347\211\271\345\270\201\347\232\204\345\257\206\347\240\201\345\255\246\345\216\237\347\220\206/index.html" "b/2019/12/26/\346\257\224\347\211\271\345\270\201\347\232\204\345\257\206\347\240\201\345\255\246\345\216\237\347\220\206/index.html" new file mode 100644 index 0000000..325b46d --- /dev/null +++ "b/2019/12/26/\346\257\224\347\211\271\345\270\201\347\232\204\345\257\206\347\240\201\345\255\246\345\216\237\347\220\206/index.html" @@ -0,0 +1,693 @@ + + + + + + + + + + + + + + + + + + + + 比特币的密码学原理 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 比特币的密码学原理 +

+ + + + +
+
+ +

前言:这一系列的文章是看肖臻老师《区块链技术与应用》公开课做的笔记

+
+
+

比特币用到了密码学的两个功能,一个是哈希,一个是签名

+
+

哈希

+

函数

+

对于集合A中的任意一个数x,在集合B中都有唯一确定的数y与之对应

+

要注意:
+集合A中的元素a,都能找到有且仅有一个集合B中的数与之对应;即不存在f(a)=b1,又有f(a)=b2的情况。但是,存在f(a1)=b,同时又有f(a2)=b的情况,如偶函数y=x2

+

哈希函数

+

本质是一个函数,但是特殊在输入值是任意字符串,输出值是固定长度的字符串,比如比特币用的SHA256,是将输入的字符串计算成64位十六进制数(256位二进制数)

+

比特币用到的哈希函数的三个特性

+
    +
  1. +

    collision resistance(抗碰撞性)
    +什么是碰撞?
    +对于函数f(x) ,如果存在a!=b,使得f(a)==f(b),那么就称为碰撞
    +什么是抗碰撞性?
    +除了暴力枚举输入空间,很难找到会发生碰撞的两个输入值
    +抗碰撞性的作用
    +上传文件之前可以对文件计算一个hash值,下次下载下来,再计算一个hash值,如果两个hash值不同,则说明被文件篡改了。因为抗碰撞性使得修改后的文件的计算出来的hash值和原来的文件极大概率是不同的。

    +
  2. +
  3. +

    hiding property(隐藏性)
    +光给你一串hash值,除了暴力求解,你是很难知道加密前的内容是什么,这就叫隐藏性。但是hiding成立的前提是输入空间无限大,而且输入的分布比较均匀,各种取值的可能性是差不多的,所以暴力求解也是不可行的。
    +隐藏性的作用
    +假如有一个人说他预测股票很准,怎么验证他说的准不准呢?如果他公布预测结果,可能会有人故意和他反着来,操纵股市导致预测不准,所以预测结果不能提前公布。那么大家又是如何知道他预测准不准呢?这个时候可以先让这个预测的人将预测结果取个hash值公布出来,由于隐藏性,光是知道hash值很难知道加密前的内容,这样就不会影响股市。然后收盘之后,让这个预测的人把预测结果公布出来,由于抗碰撞性,预测的人是无法篡改预测结果的,要是改了的话就和当初公布的hash对不上,这样就起到了一个sealed envelope 的作用。但是这个例子有一个问题,就是股票的数量是非常有限的,不满足隐藏性成立的前提条件,所以实际操作会在预测结果后面加一串随机数,然后整个取hash

    +
  4. +
  5. +

    puzzle friendly
    +光是看输入,很难猜出来hash值是什么样的,所以你想让hash值落在某个范围之内,那只能暴力枚举了。比如想得出hash值前面k位数都是0,怎么得出呢?没有办法,不能事先知道,只能一个一个去试,所以叫puzzle friendly ,挖矿的过程就是计算H(block header)<=target。 block header是区块的信息,其中包括一个nonce(随机数),我们所要做的就是不断就试这个nonce,使得hash值落在一个target space中,所以这个挖矿的过程没有捷径,只能不断去试nonce,所以这个才可以作为工作量证明。

    +
  6. +
+

签名

+

日常生活中想要开一个银行账户,怎么开,带上证件去银行。
+那比特币怎么开账户呢?很简单,只需要一对公钥和私钥,每个用户自己决定开不开户。在本地中创建一个公钥私钥对就完成了开户。
+公钥和私钥来自非对称加密。

+

什么是对称加密?
+双方在进行信息交换的时候用同一个密钥对内容进行加密/解密,那么这个密钥就一定要在网络中传输,但是网络是有可能被窃听的,所以对称加密并不安全。

+

什么是非对称加密?
+双方都有一对公钥和私钥,我给你发信息时,我用你的公钥对内容进行加密,然后你用你的私钥对信息解密,同理你给我发信息时得用我的公钥对信息进行加密,然后我用我的私钥对你发来的信息解密。公钥不需要保密,但是私钥需要保密。解决了对称加密的密钥分发的安全问题。

+

在比特币系统中,非对称加密的真正用途其实是签名,而不是信息传输,何谓签名?比如我想发起一笔交易,别人怎么知道是我发起的而不是别人偷偷的替我发起的呢,这就需要我在发布交易的时候用我的私钥进行签名(先对交易信息进行hash),然后别人用我的公钥去验证这个签名的正确性,如果不正确,说明不是我本人发起的。

+

所以总的来说,非对称加密算法的作用是:公钥加密,私钥解密; 私钥签名,公钥验证。

+

公钥私钥对重复问题
+既然公钥私钥对始终是在本地产生,那么产生重复怎么办?

+

如大量生成公钥私钥对,然后去对比产生的公钥是不是和区块链上已有的某个公钥相同。如果相同,那么就可以用对应的私钥把账户上的钱转走。

+

这种方式理论上可行,但是实际上没法做,因为产生重复的公钥私钥对的概率非常小,可以忽略不计。到目前为止还没有能用这种方法攻击成功的先例。

+

a good source of randness
+生成公钥私钥的过程是随机的,但要求选取一个好的随机源,否则前面的分析就不成立了(就有不足够小的可能生成重复的公钥私钥对)。

+

比特币中使用的签名算法不仅在生成公钥私钥对时有好的随机源,在之后每次签名的时候也要有好的随机源。如果签名时使用的随机源不好,就有可能泄露私钥。

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2019/12/26/\346\257\224\347\211\271\345\270\201\347\232\204\346\225\260\346\215\256\347\273\223\346\236\204/index.html" "b/2019/12/26/\346\257\224\347\211\271\345\270\201\347\232\204\346\225\260\346\215\256\347\273\223\346\236\204/index.html" new file mode 100644 index 0000000..9417bde --- /dev/null +++ "b/2019/12/26/\346\257\224\347\211\271\345\270\201\347\232\204\346\225\260\346\215\256\347\273\223\346\236\204/index.html" @@ -0,0 +1,669 @@ + + + + + + + + + + + + + + + + + + + + 比特币的数据结构 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 比特币的数据结构 +

+ + + + +
+
+ +
+

比特币有两种数据结构,一个是区块链,一个是Merkle Tree

+
+

区块链

+

传统意义上的指针,保存了所指向结构体在内存中的起始位置,而hash指针除了存地址之外,还要保存这个结构体的hash值,一般用H表示这hash指针,好处是不光知道这个结构体的地址,还可以知道这个结构体有没有发生改变。
+比特币中最基本的数据结构就是区块链。区块链是什么,就是一个一个区块组成的链表,区块链和普通的链表有什么区别?一个区别就是hash指针代替了普通的指针。
+图 1
+每个区块包含一个前一个区块的hash指针,这个指针是对前一个区块整个信息进行hash出来的,包括前一个区块的hash指针,通过这种数据结构可以实现tamper-evident log(防篡改)。因为,一旦改了某个区块的信息,那么它后面的区块的hash指针就要变化,后面的区块的hash指针一变,后面的后面的hash指针也要跟着变,所以这个数据结构的好处是,只要我们保存最后一个hash指针,那么区块链上任意一个区块发生改变,我们都可以知道

+

Merkle Tree

+

比特币的另外一个结构是Merkle Tree,和二叉树类似,不过父节点保存的是左右孩子节点的hash指针
+图 2

+

比特币当中是用hash指针将各个区块连接在一起,每个区块的交易是组织成一个Merkle tree 的形式,上图底下的数据块其实就是一个个交易。每个区块分为两部分,块头和块身。block header包含root hash,但是没有交易的具体内容,block body 有交易内容。

+

Merkle Proff

+

比特币中的节点分为两类,一类是全节点,一类是轻节点,全节点保存整个区块的内容(即有block header和block body),轻节点只保存block header。轻节点不包含block body,但是交易信息是在block body里面的,那全节点如何向轻节点证明交易确实发生了呢?
+图 3

+

假设某个轻节点想要知道黄色这个交易是不是包含在了这个merkle tree里面了,轻节点没有这颗merkle tree,只有一个根hash值,轻节点向某个全节点发出请求,请求一个能够证明黄色交易被包含在这个merkle tree里面的merkle proof ,全节点收到请求之后,只要把途中标为红色的这三个hash值发给这个轻节点就行,有了这三个hash值,轻节点可以在本地计算出图中标为绿色的hash值,首先算出黄色交易的hash值,即图中最下面的绿色的hash值,然后和右边的红色hash值组合计算出上一个绿色的hash值,同理可以算出整个树的根hash值,轻节点把这个hash值和block header里面的比较,就可以知道这个交易是不是在这个merkle tree里面。

+

这个是用merkle proof证明某个交易在merkle tree里面,那么如何证明某个交易不在merkle tree里面呢?

+

一种方法是把整个merkle tree发给别人,别人验证正确后,发现没有要找的交易,即交易不在merkle tree里面。
+另外一种方法是对交易根据hash排个序,然后把要找的交易也hash一下,然后二分查找,如果找到了,merkle proof证明它确实在里面,不存在证明失败;如果没找到,对相邻的两个交易进行merkle proof,如果证明成功,说明它俩是紧邻的,也即我们要找的交易不在里面,因为如果在里面,它俩就不是紧邻的了,不存在证明成功。

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2019/12/27/\346\257\224\347\211\271\345\270\201\345\215\217\350\256\256/index.html" "b/2019/12/27/\346\257\224\347\211\271\345\270\201\345\215\217\350\256\256/index.html" new file mode 100644 index 0000000..9e9f0bb --- /dev/null +++ "b/2019/12/27/\346\257\224\347\211\271\345\270\201\345\215\217\350\256\256/index.html" @@ -0,0 +1,709 @@ + + + + + + + + + + + + + + + + + + + + 比特币协议 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 比特币协议 +

+ + + + +
+
+ +

不用区块链的数字货币的问题

+

假设央行不用区块链发行数字货币,央行可以对每个货币用私钥签名,然后交易的时候,交易双方可以用公钥去验证这个签名的正确性,正确即是央行发行的货币,可以交易,否则就是假货币。

+

此方案存在的问题是:货币的真伪性可以得到保证,但是这个数量无法控制。假如你现在有一个央行发行的货币,你虽然不能伪造但是可以复制,从而发生一钱多用的情况(花两次攻击,英文名:double spending attack)

+

改进方案:央行除了要对货币签名,还要给每个货币打上一个编号,然后维护一个数据库,这个编号的货币现在在谁手上。交易的时候,双方先用公钥验证真伪,然后准备收钱的一方访问一下央行数据库,这个货币现在是不是在你手上,在的话就可以继续交易,否则这个货币你是已经用过了,不能再用了。这样就解决了double spending attack问题。

+

改进方案存在的问题:这是中心化的方案,央行数据库压力很大。比特币系统解决的就是这种问题。

+

去中心化货币系统要解决的两个问题

+
    +
  1. +

    数字货币的发行,谁有权发?什么时候发?发多少?

    +
  2. +
  3. +

    怎么验证交易的有效性?怎么防止double spending attack问题?

    +
  4. +
+

比特币的方案

+

问题1的解决方案

+

共识算法

+

问题2的解决方案

+

有一个用户通过共识算法获得了发行货币的权力,我们管他叫铸币权,他发行了10个比特币给了A,把这交易信息写入区块链中。然后A把比特币给了B和C,一人5个,这个交易需要A的签名,证明是A同意的,同时这个交易还要说明花掉的比特币是从哪来的,也就是下图的第二个交易要说明比特币从哪来的,这里是从铸币交易输出来的。比特币交易系统中都包含了输入和输出两部分,输入部分要说明币的来源,输出部分要给出收款人的公钥的hash,比如A给B转钱,要说明B的公钥的hash是什么。然后B再把钱转给C,2个和D,3个,同样要签名,以及说明币的来源。这时候C想转给E7个币,币的来源就有两个了。见下图
+图 1
+注意:上图有两种hash指针,一种就是前面一篇文章提到的连接区块的hash指针,把区块串起来构成一个链表,还有第二种指针,是为了说明币的来源,这样可以防范double spending attack:假如上图B还想给F转比特币,单纯验证签名,貌似合法,但是币从哪来呢?还是要从上图的第二个区块来,别的节点收到这个交易后,会进行查询,从这个交易一直回溯到币的来源,回溯过程中,发现B已经把币全给C和D了,所以交易不合法,这个交易不会写入区块链中。

+
A和B交易需要的信息
+

需要有A的签名和B的地址,比特币系统中收款方的地址是通过公钥推算出来的,比如B的地址是B的公钥取hash再进行一些转过生成的,这个地址相当于银行账号,A要给B转钱,需要知道B的银行账号。那么A怎么才能知道B的地址呢?比特币系统没有提供查询某个人的地址的方法,这得需要第三方的形式,比如说电商平台支持比特币支付,那么商家要把账户地址告诉电商平台,然后消费者就知道要给谁转账了。
+另外需要知道A的地址,A要B的地址是为了知道给谁转账,B(其实是所有节点)要A的地址是为了①谁给B转的账②验证是否是A的签名(A的私钥签名,其他节点知道A的地址后就可以公钥验证)。
+问题来了?怎么才能知道A的公钥呢?
+A的公钥是交易自己给出的,交易有输入和输出,输入不仅要说明币的来源还要说明A的公钥。
+那么问题又来了,交易自己决定输入公钥,那不是有冒名顶替的风险吗?
+前面提到输入要给出币的来源和付款人的hash,而输出要给出收款人的公钥的hash,那么上面A到B的交易的币是哪里来的呢?是前面铸币交易来的,来的同时要带上付款人的公钥的hash也就是前面铸币交易的收款人的公钥的hash。也就是说第二个交易的输入的公钥要和第一个交易的输出的公钥要一致。
+在比特币系统当中,验证过程是通过脚本实现的,每个交易的输入是一段脚本,包括公钥,也是在脚本指定的,每个输出也是一段脚本,验证是否合法,就是把当前输入的脚本和币的来源的那个交易的输出的脚本拼在一起,如果能正常执行,那么就是合法的。

+

注意:上图对区块进行了简化,每个区块只包含了一个交易,实际上,一个区块可以有很多交易,所有交易构成了一个merkle tree。每个区块分为块头和块身两部分。
+块头包含这个区块宏观的一些信息,比如说用的是比特币版本的哪个协议(version),还有区块链当中指向前一个区块的hash指针(hash of previous block header),还有整个merkle tree 的根hash值(Merkle root hash),还有两个域和挖矿相关的,一个是挖矿的难度目标阈值(target),一个是随机数nonce

+

比特币中的共识协议

+

一个节点一票

+

假设系统中大部分节点是诚实的,那么就投票,比如说某一个节点提出一个候选区块,根据收到的交易信息,判断哪些交易是合法的,然后把这些交易按顺序打包到候选区块里。候选区块发布给所有节点,每个节点收到区块后,检查一下,这里面的交易是不是合法的,如果都是合法的,投赞成票,否则投反对票。最后得票超过半数,候选区块写写入区块链中。

+
存在的问题
+
    +
  1. 恶意节点不断提出有问题的区块,时间都浪费在了投票上面,区块链无法发展
  2. +
  3. 没法强迫节点投票。如果都不投票,那么区块链就陷入了瘫痪。
  4. +
  5. 效率上的问题。投票等多久决定于网络延迟
  6. +
  7. 任何基于投票的方案都要先决定谁有投票权(membership),这是区块链最大的问题。如果区块链的成员有严格定义,比如说有一种区块链叫联盟链(hyperledger fabric),只有符合条件的大公司才能加入,这种方案就可行。但是加入比特币系统是很容易的。
  8. +
+

根据算力投票

+

比特币系统是根据计算力投票的。每个节点都可以在本地组装出一个候选区块,把它认为是合法的交易放在这个区块里,然后尝试各种nonce值,即计算H(block header)<=target,nonce是个4 bytes的数。如果某个节点找到了nonce,就获得了记账权,就是往比特币去中心化的账本中写入下一个区块的权力,只有获得了记账权的节点才有权力发布下一个区块。其他节点收到区块后验证区块的合法性,比如block header里面的各个域是否正确,它里面的nBits域(时间上是target的一个编码),检查一下nBits设置的是不是符合比特币系统规定的难度要求;然后检查一下nonce是不是使得整个H(block header)小于等于target。然后验证一下block body里面的交易是不是合法的,第一要有合法签名,第二没有用过。

+

假设一个区块经过检查是合法的,那么有没有可能节点不接受它呢?

+
    +
  1. 恶意节点不接受合法区块
  2. +
  3. 假设一个获得记账权的节点发布一个合法的区块,如下图,插在了区块链的中间。这个区块是完全合法的,那么我们应不应该接受它?
  4. +
+

图 2
+首先说明这个合法区块为什么插在了中间?因为每一个区块都带了一个指向前面一个区块的hash指针(hash of prev block header)。那么插在这里有什么问题?见下图
+图 3
+假如上方这个区块有一个交易是A把钱给了B,而我们新插入的这个区块里有个交易是A把钱给了自己。这里要说明这种情况不会发生double spending。我们判断是不是发生了double spending 是判断该区块所在分支有没有发生,上方的区块和新插入的区块已经是不同分支了,所以不是double spending。这种情况相当于把A→B这个交易给回滚了。
+那么我们到底要不要接受这个新区块呢?不接受,因为它不在最长合法链上(longest valid chain)

+

比特币协议中规定接受的区块应该是在扩展最长合法链。上图这个例子是分叉攻击的例子(forking attack),通过往区块链中插入一个区块,来回滚已经发生的交易。

+

但是比特币系统在正常情况下也可能出现分叉。比如有两个节点同时获得了记账权,发布区块,这个时候会出现下图这种情况
+图 4

+

有两条最长合法链。那该接受哪一个?比特币协议中,缺省情况下,每个节点接受最早收到的那一个,由于网络延迟,有些节点可能接受1,有些节点可能接受2。什么叫接受?如果节点在1后面继续扩展,说明节点接受了1。所以如果出现系统中两个节点差不多同时发布区块的情况,这种等长的临时性的分叉会维持一段时间,直到一个分叉胜出。上图例子中,假设1区块抢先找到了下一个区块,那么上面的分叉就成了最长合法链,下面的分叉就成了orphan block,orphan block 铸币交易产生的比特币都会失效。为什么要争夺记账权?获得记账权的节点有一定权力,它可以决定哪些交易可以写到区块里,但是设计协议的时候不应该让它成为争夺记账权的主要动力,因为我们希望凡是合法的交易都应该被写到区块链里,而不是取得记账权的节点决定是否写入。比特币系统提供的出块奖励(block reward)成为争夺记账权的动力,比特币协议规定获得记账权的节点在发布的区块里可以有一个特殊的交易,就是前面提到的铸币交易(coinbase transaction),可以发布一定数量的比特币(初始50个,每21万个区块之后减半)。这样就解决了前文提到的去中心化货币系统要解决的第一个问题。铸币交易是比特币系统中发行比特币的唯一方法,其他所有交易都是比特币的转移。

+

所以比特币系统要取得的共识到底是什么?分布式hash表要取得的共识是hash表的内容,比特币系统中要取得的共识是去中心化账本里的内容。谁来决定写这个账本?取得记账权的节点写账本,所以比特币系统中的共识机制是根据算力(每秒钟能试多少个nonce,也成为hash rate)来投票的,hash rate越高,得到记账权和得到比特币的概率就越高。那么这种共识机制是如何避免女巫攻击的,因为不管创建多少账户,节点的hash rate 都不会改变,所以获得记账权和比特币的概率不会提高。

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2020/01/06/\346\257\224\347\211\271\345\270\201\345\256\236\347\216\260/index.html" "b/2020/01/06/\346\257\224\347\211\271\345\270\201\345\256\236\347\216\260/index.html" new file mode 100644 index 0000000..5a9e3f8 --- /dev/null +++ "b/2020/01/06/\346\257\224\347\211\271\345\270\201\345\256\236\347\216\260/index.html" @@ -0,0 +1,729 @@ + + + + + + + + + + + + + + + + + + + + 比特币实现 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 比特币实现 +

+ + + + +
+
+ +

UTXO

+

区块链是去中心化的账本,比特币采用的是基于交易的账本模式(transaction-based ledger)。每个区块里记录的是交易信息,有转账交易,铸币交易,但是系统当中并没有哪个地方显式的记录账户中有多少钱,这得需要通过交易记录推算。比特币系统的全节点要维护一个UTXO:Unspent Transaction Output 数据结构。区块链上有很多交易,有些交易的输出已经被花掉了,有些没有被花掉,所有交易中输出还没有花掉的组成一个集合就是UTXO。注意,一个交易可能有多个输出,所以会出现一个交易中有的输出在UTXO里面,有的不在。UTXO集合当中每个元素要给出产出这个输出的交易的hash值以及他在这个交易里是第几个输出,这两个信息就可以定位到UTXO中的输出。

+

UTXO的作用

+

为了检测double spending。没有花掉的币必须在UTXO中。全节点要在内存中维护UTXO,以便快速检测double spending 。每个交易会消耗掉一些输出,同时会产生新的输出。如果某人收到比特币的转账交易后,这个钱始终都不花,那么这个信息就要永久存储在UTXO中。

+

每个交易可以有多个输入和多个输出,所有输入的金额加起来要等于所有输出的金额,这个叫做total inputs =total outputs 。这意味着交易可能不止一个签名。有些交易的total inputs和total outputs有略微的差异,差额会作为交易费给获得记账权发布区块的节点。节点争夺记账权的动力之一是出块奖励,但是光有出块奖励是不够的,获得记账权的节点如果只打包自己的交易记录怎么办?所以,动力之二是就是交易费,就是前面提到的差额。比特币当中的交易费通常很小0.00几,也有些简单的交易是没有交易费的。目前矿工挖矿主要还是为了得到出块奖励。随着出块奖励的减少,以后的交易费会成为争夺记账权的主要动力。

+

除了比特币这种transaction-based ledger外,与之对应的还有基于账户的模式(account-based ledger),以太坊用的就是这种交易模式,在这种模式当中,系统要显示记录每个账户有多少币,这个和现实体验非常接近。比特币这种交易模式隐私保护性较好,但是也有代价,比如转账交易要说明币的来源,这就是没有账户的代价。以太坊系统就不存在这种问题。

+

比特币例子

+

图 1
+Number Of Transactions : 表示这个区块包含了686个交易
+Output Total:总的输出是4220.46616378个比特币
+Transaction Fees:总的交易费
+Height:区块序号
+TimeStamp:产出的时间戳
+Difficulty:挖矿的难度,每隔2016个区块会进行调整,保持出块时间在每10分钟产出一个
+Nonce:挖矿的随机数
+Block Reward:出块奖励
+Hash:区块块头的hash
+Previous Block:前一个区块的块头hash
+Hash 和 Previous Block 前面都有一串0,这不是偶然的,所谓的挖矿就是不断试nounce,使得整个block header的hash小于等于给定的目标阈值,这个目标阈值表示成16进制就是前面有一长串的0,所以凡是符合难度要求的区块,它的块头的hash值都带一串0
+Merkle Root:merkle tree的根hash值
+图 2

+

注意,nounce的类型是32位的无符号整数,我们知道挖矿的时候要调整nonce,但是这个nonce最多只有232次方个取值,按照比特币现在的挖矿难度,就算把这232 个的取值都遍历一遍,很可能仍然找不到符合难度要求的。因为近年来挖矿的人不断增多,挖矿难度不断增大,单纯调正nonce是很可能找不到符合难度要求的,所以还要调整block header其他的域,下图是block header 里面的各个域的描述

+

图 3
+version:当前使用的比特币协议的版本号,这个无法更改
+previous block header hash:前一个区块的块头hash,也是无法修改
+merkle root hash:merkle tree 的根hash值,可以修改
+time:区块产生时间,有一定的调整余地,比特币系统并不要求非常精确的时间,可以对时间在一定范围内调整
+nBits:挖矿时候用的目标阈值编码后的版本,4个字节,只能按照协议的要求定期调正,不能更改
+nonce:我们能改的nonce
+图 4

+

为什么merkle root hash可以改?
+每个发布的区块有一个铸币交易,这是比特币系统中产生新的比特币的唯一方式,这个交易没有输入(也就是上面的no input),因为币是凭空造出来的,它有一个CoinBase域,它可以写入任何东西,没有人管。
+图 5

+

上图是一个小型区块链的示意图,左下角是coinbase transaction,我们改变这个交易的CoinBase域之后,这个交易的hash就发生了变化,这个变化会沿着merkle tree 不断向上传播,最终会导致block header 里的Merkle Root发生变化,所以我们可以把这个域当作extra nonce,块头的4字节nonce不够用,这里有很多字节可以用,比如把CoinBase域的前8个字节当作extra nonce来用,这样搜索空间一下子增大到了296次方。真正挖矿是有两层循环的,外层循环是调整CoinBase域的extra nonce,算出block header里的Merkle Root之后,内层循环再调整nonce。
+图 6

+

上图是一个普通转账交易的记录,这个交易有两个输入,两个输出,左上方虽然写的是Output,其实对这个交易来说是输入,这里写的Output意思是说他们花掉的是之前哪个交易的Output。右边两个输出还没有被花掉,处于Unspent状态,会保存在UTXO里面

+

Inputs and Outputs中
+Total Input:输入的总金额
+Total Output:输出的总金额
+Fees:前面两者的差值,这笔交易的交易费

+

Input Scripts和Output Scripts
+输入和输出都是用脚本的形式指定,比特币系统中验证交易的合法性就是把Input Scripts 和Output Scripts配对后执行,注意上图中的Input Scripts和Output Scripts不是一对,上图的Input Scripts要和提供币的来源的交易的Output Scripts进行配对,如果输入脚本和输出脚本拼接在一起能顺利执行,那么这个交易就是合法的。

+

图 7

+

注意,上图中的所有的tx在计算时就是是一个Merkle Root

+

挖矿的过程的概率分析

+

挖矿的过程就是不断尝试各种nonce来求解puzzle,每次尝试nonce 都可以看作一个Bernoulli trial:a random experiment with binary outcome(伯努利试验:一个二元结果的随机试验)。伯努利实验的经典案例是抛硬币,如果硬币不均匀,每次实验,正面的概率就是p,反面的概率是1-p。对于挖矿来说,这两个概率相差甚远,每次尝试一个nonce,成功的概率是微乎其微的,大概率不行的,如果我们做大量的伯努利实验,每个实验都是随机的,那么这些伯努利实验就构成了一个Bernoulli process:a sequence of independent Bernoulli trial(伯努利过程:一系列独立伯努利试验)。Bernoulli process的一个特性是无记忆性(memoryless),意思是说做大量实验,前面的实验结果对后面的结果没有影响,挖矿过程就是不断尝试nonce,这种情况下Bernoulli process可以用Poisson process来近似,实验次数很多,每次成功的概率很小就可以用Poisson process来近似。我们关心的是出块时间,也就是系统里产生下一个区块的时间,这个在概率上可以推导出来,服从指数分布(exponential distribution)
+图 8

+

注意,上图的出块时间是整个系统的出块时间,并不是矿工的出块时间,整个系统的平均出块时间是10分钟,平均时间是比特币协议规定的,通过定期调整挖矿难度,使得平均出块时间维持在10分钟左右,具体到每一个矿工,出块时间取决于矿工的算力,算力大,概率就大,出块时间就短。指数分布也是无记忆的,概率密度曲线的特点有:从任何一个地方把它截断,剩下曲线的形状仍然和原来一样,仍然服从指数分布,这就是无记忆的性质,假如过去了10分钟,仍然没有人找到区块,那么接下来还要等多久呢?还是平均下来10分钟,这个和直觉不太一致。这个概率分析告诉我们将来还要挖多少时间和过去挖了多少时间是没有关系的,仍然是服从指数分布,平均还要10分钟,这个性质也叫progress free。

+

progress free 或者 memoryless有什么作用?

+

设想一下,如果有某个puzzle不满足这个性质会出现什么情况,比如说过去做的工作越多,接下来尝试nonce的时候成功的概率越大,相当于抛硬币的时候每次结果不是随机的,过去抛了好多次,都是反面朝上,下次再抛硬币的时候,正面朝上的概率会增加。如果有某一个加密货币设计出这样的puzzle,会有什么结果?算力强的矿工会有不成比例的优势,因为算力强的矿工过去的工作量更大。什么是不成比例的优势?比如系统中有两个矿工,一个的算力是另一个的10倍,理想状况下,算力强的矿工能挖到矿的概率应该是另一个矿工的10倍,这才算公平,因为算力强的矿工能尝试的nonce是另一个的10倍,这就是我们说的progress free 或者 memoryless所保证的,如果不是这样的话,算力强的矿工获得记账权的概率就会超过10倍,因为它过去尝试了更多的不成功nonce,那么下次成功的概率就会增大,这就是不成比例的优势。所以progress free 或者 memoryless保证了挖矿公平性。

+

比特币的总量

+

我们知道出块奖励是系统产生比特币的唯一途径,而这个奖励是每隔4年减半的,这样产生的比特币数量构成了一个几何序列(geometric series)。一开始的21W个区块能产生21W 50的比特币,接下来的区块能够产生21W 25,再往下就是21W12.5…… 总量就是21W 50 *(1+1/2+1/4+……)约等于2100W

+

不像寻找某个符合条件的质数这类数学难题,比特币求解puzzle除了比拼算力外并没有实际意义,比特币越来越难挖到是因为出块奖励被人为减少,比特币的稀缺性是人为造成的。虽然挖矿求解的puzzle本身没有实际意义,但是挖矿过程对于维护比特币系统的安全性是至关重要的,也叫做BitCoin is secured by mining。对于一个去中心化的没有membership控制的系统来说,挖矿提供了一种凭借算力投票的有效手段,只要大部分算力掌握在诚实的节点手里,系统的安全性就能得到保证,所以挖矿这一过程,表面上没有意义,但是这个机制对于维护系统安全性是非常有效的。

+

我们知道出块奖励每隔4年减半,是不是说挖矿的动力也会越来越小呢,从过去几年情况来看,恰恰相反,挖矿竞争越来越激烈,因为比特币的价格是飙升的。随着出块奖励越来越少,交易费也是越来越多的。

+

比特币的安全性

+

假设大部分算力是掌握在诚实的矿工手里,我们能得到什么样的安全保证?能不能保证写入区块的交易都是合法的?不能,挖矿只是概率上的保证,只能说有一个比较大的概率,下一个区块是诚实的矿工发布的,但是不能保证记账权不会落到有恶意的节点手里。比如,好的矿工占90%算力,坏的矿工占10%的算力,平均下来,10%的情况下,记账权会落到有恶意的节点手里。

+

恶意的节点掌握记账权

+

偷币

+

第一个问题,他能不能偷币?能不能把别人账上的钱转给自己?

+

不能,因为他不能伪造别人的签名,交易不合法。但是如果他强行把不合法的交易写进区块链里会出现什么情况?诚实的节点都不接受这个区块,他们会沿着上一个合法的区块继续添加区块,形成最长合法链,导致恶意节点写进去的区块作废,这样,恶意节点不仅没有偷到钱,还把出块奖励陪了。

+

双花

+

第二个问题,他能不能把已经花过的币再花一遍?

+

比如说M节点发布一个转账交易给A,现在他获得了记账权,又把钱转给自己,如果直接连在M→A的后面肯定是不合法的,因为很明显的double spending,凡是诚实的节点都不会接受这个区块,他要想发布交易就一定要插在M→A前面一个区块后面,也就是前面文章提到的分叉攻击。注意,区块插在什么位置,是要在刚开始挖矿就决定的,因为设置的block header要填上前一个区块的hash,所以M节点想插到这个位置,一开始就要把这个区块设置成前一个区块,而不是等获得了记账权再说。这种情况下会出现两个等长合法链,取决于其他节点沿着哪条链继续往下扩展,最后有一个会胜出,另一个就作废了。这种攻击有什么用?假如M是消费者,A是商家,M买了东西然后用比特币支付,M又发起一个交易,把钱转给自己,然后把下面的交易扩展成最长合法链,这样上面的区块就作废了,这种攻击的作用就是既买的了商品,又把钱收回来了,达到double spending attack的目的。
+图 9

+

怎么防范这个问题?如果M→A这个交易不在最后一个区块,而是后面跟了几个区块,那么这种攻击的难度就会大大增加,要想回滚M→A这个交易还是得在他前面一个区块后面插入新区块,然后想办法让新区块所在分支成为最长合法链,这个难度非常大,因为诚实的节点不会沿着新区块往后发展,因为他不是最长合法链。这种情况相当于两条链在赛跑,如果大部分算力是掌握在诚实节点手里,这种攻击成功的可能性很小。所以一种防范的手段是多等几个区块或者叫确认(confirmation)。M→A这个交易刚刚写进区块的时候,称为one-confirmation,以此类图,它后面的第三个区块叫做three-confirmation。比特币协议规定,缺省情况下要等6个confirmation,到了six-confirmation时,才认为前面的交易是不可篡改的,也就是要等待10分钟(平均出块时间)*6 = 1小时。
+图 10

+

我们知道区块链也被叫做不可篡改的分布式账本(irrevocable ledger),是不是说凡是写入区块链的内容就永远改不了呢?经过前面的分析我们可以知道,这种不可篡改性只是一种概率上的保证,刚刚写入区块链的内容,相对来说,还是比较容易被改掉的,经过一段等待时间之后,或者后面跟着好几个确认之后,这种被篡改的概率会指数级别下降。其实还有一种zero confirmation,这个意思是说,转账交易已经发布出去但是下一个区块还没有挖出来。拿电商购物的例子来说,相当于支付的时候发布一个转账交易,电商运行一个全节点或者委托某个全节点,监听区块链上的交易,收到转账交易后要验证交易的合法性,但是不用等到交易写到区块链里。这听起来风险很大,其实zero confirmation 在实际当中应用还是很普遍的,第一个原因是比特币协议缺省的设置是节点接受最先听到交易,两个交易有冲突,先听到哪个就接受哪个,所以zero confirmation处,M→A交易被节点收到,M→M交易较大概率不被诚实节点接受;第二个原因是很多购物网站从支付成功到发货,是有一定时间间隔的,天然有一定处理时间,比如说你要买个东西,在网上支付成功后,电商第二天才会发货,期间发现转账交易没有被写到最长合法链上,那么电商就可以取消发货。
+图 11

+

故意不把某些合法交易写进区块链

+

第三个问题是他能不能故意不把某些合法交易写进区块链里?

+

比特币协议没有规定获得记账权的节点一定要把某些交易写进区块链里,但是出现这种情况问题也不大,因为总会有诚实的节点愿意发布这些交易。区块链正常情况下也会出现合法的交易没有被写进去的情况,可能就是这段时间交易数目太多了,比特币协议规定每个区块的大小是有限制的,最多不能超过1M,所以交易放不下了就得等到下一个区块。

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2020/01/06/\346\257\224\347\211\271\345\270\201\346\214\226\347\237\277\351\232\276\345\272\246/index.html" "b/2020/01/06/\346\257\224\347\211\271\345\270\201\346\214\226\347\237\277\351\232\276\345\272\246/index.html" new file mode 100644 index 0000000..cdb074a --- /dev/null +++ "b/2020/01/06/\346\257\224\347\211\271\345\270\201\346\214\226\347\237\277\351\232\276\345\272\246/index.html" @@ -0,0 +1,666 @@ + + + + + + + + + + + + + + + + + + + + 比特币挖矿难度 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 比特币挖矿难度 +

+ + + + +
+
+ +

挖矿就是不断尝试block header里面的nonce,使得整个block header 的hash值小于等于给定的目标阈值

+

Hblockheader<=targetH(block header)<= target

+

target越小,挖矿难度越大,调整挖矿难度就是调整目标空间在整个输出空间中所占的比例,比特币用的hash算法是SHA-256,产生的hash值是256位,输出空间是2256 ,调整目标空间在输出空间的比例通俗说就是hash值前面多少个0,合法的区块要求算出来的hash前面至少70个0,这是一种通俗的说法,不是特别准确,因为目标阈值并不是说前面都是0,从某一位开始后面都是1,严格来说,上面的公式才是对的。

+

挖矿难度和目标阈值是成反比的
+图 1
+difficulty_1_target表示挖矿难度等于1的时候所对应的目标阈值,挖矿难度最小就是1,这个时候对应的目标阈值是个非常大的数。

+

为什么要调整挖矿难度

+

系统总算力不断增强,挖矿难度保持不变,出块时间就会越来越短。出块时间越来越短会有什么问题,比如说不到1s出一个区块,这个区块在网络上传输可能需要几十s,别的节点在没有收到这个区块时会继续沿着已有的区块链往下继续扩展,如果有两个节点差不多同时都收到这个区块,差不多同时都发布一个区块,这个时候会出现一个分叉。出块时间越来越短,分叉就会越来越频繁,越来越多。分叉过多对于系统达成共识是没有好处的,而且危害了系统的安全性。
+图 2
+比特币协议假设大部分算力是掌握在诚实的矿工手里,系统的总算力越强,安全性就越好,如果恶意节点掌握了51%的算力,那么它就可以做任何事情(51% attack)。假设现在出现个分叉,系统中的总算力就被分散了,节点根据在网络中位置的不同,可能会选择不同的分叉继续扩展,而有恶意的节点可以集中算力就扩展它的分叉,也就是上图的A→A,这样可以很快使得这条分叉成为最长合法链,因为好人的算力被分散了,这个时候可能都不需要51%的算力,10%的算力可能就够了,所以出块时间不是越短越好。那比特币协议设计的10分钟是不是就是最优的呢,这不一定,比如说8分钟,5分钟行不行,应该也行,这个只是说出块时间要有一个合适的波动范围。有人觉得比特币的10分钟出块间隔太长了,对于一个支付系统来说,支付要等这么长时间才能得到确认,这个有点太长了。以太坊的出块时间就降低到了15s(同样需要加大难度保持出块时间稳定),出块时间大幅下降后,以太坊就要设计一个新的控制协议,叫ghost。在这个协议当中,这些分叉产生的叫orphan block,不能简单丢弃,而是要给它一些奖励,这叫做uncle reward。

+

如何调整挖矿难度

+

比特币协议中规定每隔2016个区块,要重新调整一下目标阈值,这个大概是每两个星期调整一下,调整按照以下公式:
+图 3
+expected time 就是两个星期,actual time 是系统中最近产生2016个区块实际花费的时间。实际代码中target限制不会一次增大4倍以上,也不会一次减小到1/4以下。

+

那怎么让所有的矿工同时调整难度呢?

+

计算target的代码是写在比特币系统的代码里,每挖到2016个会自动进行调整,但是比特币代码是开源的,如果有恶意的节点就是不改target怎么办?因为block header里面有target的编码域(nBits),而没有target域,因为target有256位,直接存要32个字节,nBits只要4个字节,可以认为nBits是target的压缩编码,如果有恶意的节点不改target,检查区块的合法性就通不过,因为每个节点都要独立验证发布的区块的合法性,检查的内容包括nBits域设置的对不对。以太坊也要定期调整挖矿难度,但它不是隔几个区块进行调整,而是每个新出的区块都有可能进行调整,而且调整的方法也比比特币的复杂得多。

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2020/01/06/\346\257\224\347\211\271\345\270\201\347\275\221\347\273\234/index.html" "b/2020/01/06/\346\257\224\347\211\271\345\270\201\347\275\221\347\273\234/index.html" new file mode 100644 index 0000000..8d30c01 --- /dev/null +++ "b/2020/01/06/\346\257\224\347\211\271\345\270\201\347\275\221\347\273\234/index.html" @@ -0,0 +1,656 @@ + + + + + + + + + + + + + + + + + + + + 比特币网络 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 比特币网络 +

+ + + + +
+
+ +

用户把交易发到比特币网络上,节点收到交易后把他们打包到区块里,然后把区块发到比特币网络上。那么新发布的交易和区块在网络上是如何传输的?

+

比特币网络的工作原理

+

比特币工作在应用层(application layer),他的底层是一个P2P的Overlay network
+图 1
+比特币的P2P网络非常简单,所有节点都是对等的,不像有些P2P有所谓的超级节点(Super Node)或者主节点(Master Node)。你要加入这个网络,首先至少得知道一个种子节点(Seed Node),然后你和种子节点联系,它会告诉你它所知道的网络中的其它节点。节点之间是通过TCP通信的,这样有利于穿透防火墙,然后你离开的时候不需要通知其它节点,退出应用程序即可,别的节点没有听到你的消息,过一段时间就会把你删掉。

+

比特币网络的设计原则

+

简单、鲁棒而不是高效。每个节点维护一个邻居节点的集合,消息传播在网络中采取的是flooding方式,节点第一次听到某个消息的时候,把它传播给所有的邻居节点,同时记录一下这个消息我已经收到过,下次再收到这个消息就不再转发。邻居节点的选择是随机的,没有考虑底层的拓扑结构,比如加利福尼亚的节点选的邻居节点可能在阿根廷,这样设计的好处是增强鲁棒性,但是牺牲的是效率,你向身边的人转账和美国的人转账速度是差不多的。比特币系统中,每个节点要维护一个等待上链的交易的集合,第一次听到某个交易的时候,把这个交易加入集合,并且转发这个交易给邻居节点,以后再收到这个交易就不转发,这样避免交易在网络中无效传播,转发的前提是交易是合法的。这里有一个risk condition(风险状况),有可能两个有冲突的交易同时被广播到网络上,比如有一个交易是A→B,另外一个交易是A→C,它们用的是同一个输出,每个节点根据在网络中的位置的不同,有的可能先收到前者,有的可能先收到后者,收到后加入等待上链的交易的集合,下次收到另外一个交易的时候就认定是非法的,就不管了。集合中的交易如果被写到区块链中就要被删掉,比如说有个节点听到新发布的区块里面包含了A→B这个交易,这个交易在自己的等待上链的交易的集合中,就会被集合删掉,如果它听到的新发布的区块是A→C这个交易,也会把A→B给删掉,因为它是非法的。新发布的区块在网络中传播的方式和新发布的交易类似,每个节点除了要检查区块的内容的合法性,还要检查是不是在最长合法链上,越是大的区块在网络上传播的速度越慢,比特币协议对区块大小有一个1M的限制。

+

比特币网络的传播是属于best effort,一个交易被发布到比特币网络上,不一定所有节点都能收到,而且不同节点收到这个交易的顺序也不一定一样,网络传播存在延迟,可能会很长,而且有的节点可能不会按比特币协议的要求进行转发,比如有的该转发不转发,导致别的节点收不到合法交易;有的转发不该转发的,像不合法交易,这个是面临的实际问题。

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2020/01/07/\346\257\224\347\211\271\345\270\201\347\232\204\345\210\206\345\217\211/index.html" "b/2020/01/07/\346\257\224\347\211\271\345\270\201\347\232\204\345\210\206\345\217\211/index.html" new file mode 100644 index 0000000..528c94a --- /dev/null +++ "b/2020/01/07/\346\257\224\347\211\271\345\270\201\347\232\204\345\210\206\345\217\211/index.html" @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + 比特币的分叉 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 比特币的分叉 +

+ + + + +
+
+ +

分叉是指一条链变成了两条链。

+

原因

+

分叉可能是多种原因造成的

+

一种原因是两个节点差不多同时挖到了区块,这个时候两个节点都可以发布区块,就出现了一个临时性的分叉,这种分叉叫state fork。分叉攻击也属于state fork ,也是属于对比特币区块的当前状态产生了分歧,只不过分叉攻击的分歧是故意造成的,所以分叉攻击造成的分叉也叫deliberate fork 。

+

除此之外,比特币协议发生了改变也会造成分叉,要修改协议需要软件升级,在一个去中心化的系统里,升级软件的时候没有办法保证所有节点同时都升级软件,我们假设大部分节点升级了软件,少部分节点因为种种原因没有升级,这种情况导致的分叉叫做protocd fork。根据对协议内容修改的不同,又可以进一步分成硬分叉(hard fork)和软分叉(soft fork)。

+

硬分叉

+

如果对比特币协议增加新的特性,那些没有升级软件的节点不认可新特性,这个时候就会产生硬分叉,一个例子是比特币的区块大小限制。假如有人发布了软件更新,将区块大小限制从1M→4M,假设大多数节点都更新了,少数节点没有更新,这里的大多数不是按账户数目来算,而是按算力来算,即系统中有大多数hash算力的节点都更新了软件。新节点能挖出最大是4M的区块,但是旧节点不认可大小超过1M的区块,所以旧节点会沿着小区块一直挖下去,新节点大小区块都接受,但旧节点不接受大区块,两者没有达成共识,这样就会出现一个永远的分叉。

+

软分叉

+

如果对比特币协议加一些限制,原来合法的交易或者区块在新的协议可能不合法就会引起软分叉。假如有人发布更新将区块大小变成0.5M,大部分节点更新了软件,少部分没有更新。这时候新节点沿着一条链挖小区块,不认可大区块,旧节点沿着一条链挖大区块,同时旧节点也认可小区块,因为新节点算力更强,所以更快形成一条新旧节点都认可的最长合法链,但是新节点不会在旧节点产出的大区块后面继续挖,而是会继续分叉挖小区块,所以旧节点挖出的大区块最后都没用了,不得不更新软件。

+

软硬分叉的区别就是
+掌握大多数算力的新节点认可旧节点挖出的区块,但旧节点不认可新节点挖出的区块,那么就是硬分叉;
+掌握大多数算力的新节点不认可旧节点挖出的区块,但是旧节点认可新节点挖出的区块,那么就是软分叉。

+

实际当中出现软分叉的情况之一是给某些目前协议中没有规定的域增加新的含义,赋予新的规则,如coinbase域,将前8字节作为extra nonce,但是剩下的字节都没有被使用。有人提出剩下的字节用来存UTXO的根hash值,那么新节点不认可旧节点挖出的区块,而旧节点认可新节点,因为coinbase写啥都无所谓,这样就会出现软分叉。

+

比特币历史上有名的软分叉例子是P2SH,这个功能最初没有,是后来通过软分叉加进来的。支付的时候不是付给一个public key hash(也就是输出脚本不是给出收款人公钥hash),而是付给一个redeem script hash(赎回脚本的hash),具体流程参考前文。对于旧节点来说,他不知道这个P2SH这个特性,他只会验证赎回脚本是否正确,新节点才会做第二阶段的验证,所以旧节点认为合法的交易,新节点可能认为非法,新节点认为合法的交易旧节点也认可

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2020/01/07/\346\257\224\347\211\271\345\270\201\347\232\204\345\214\277\345\220\215\346\200\247/index.html" "b/2020/01/07/\346\257\224\347\211\271\345\270\201\347\232\204\345\214\277\345\220\215\346\200\247/index.html" new file mode 100644 index 0000000..414d602 --- /dev/null +++ "b/2020/01/07/\346\257\224\347\211\271\345\270\201\347\232\204\345\214\277\345\220\215\346\200\247/index.html" @@ -0,0 +1,676 @@ + + + + + + + + + + + + + + + + + + + + 比特币的匿名性 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 比特币的匿名性 +

+ + + + +
+
+ +

比特币的匿名性更像是一种化名、网名。
+和现金相比,匿名性不如现金,现金是完全匿名,上面没有任何人的信息,所以非法交易会用大量现金。
+和银行相比,匿名性好于银行,因为银行开户是实名制,而比特币不需要。但如果银行开户允许使用化名,那么银行的匿名性要更好,因为比特币的账本是公开的,而银行的账本是受到控制的,银行工作人员可以查到所有人账号,但是普通老百姓是查不到别人的账号的。

+

破坏匿名性的情况

+

多个账户

+

一个人可能有很多账户,这些账户可能会被关联。

+

比如交易过程中,inputs里面有两个输入,一个地址输入4比特币,一个地址输入5比特币,outputs有两个输出,一个地址输出6比特币,一个地址输出3比特币。因为像买东西这种场景,大概率是会出现零钱的,3比特币就是零钱。因为交易的两个输入受你的控制,说明这是你的账户,两个地址就被关联了。输出的第二个地址也会被关联上,因为如果买东西只需要花3个比特币,那么输入只需要一个地址即可,所以分析出来3个比特币流向的地址也是你的账户。

+

与现实世界发生联系

+

比特币和实体货币发生联系的时候都有可能泄露身份,比如买比特币的时候,可以去交易所,此时就会留下你的交易记录。又比如用比特币支付。

+

如何提高匿名性

+

洋葱路由

+

首先保证网络层的匿名性,普遍的做法是多路径转发,TOR(洋葱路由)就是这个原理,消息在网络上传输要经过许多中间节点,每个节点只知道上一个节点是谁,而不知道谁发出的,只要路径上有一个节点是诚实的,他就会把发消息的人信息隐藏掉,后面的节点就不知道发消息是谁了。

+

混币

+

在应用层上,一种做法是coin mixing ,借助这类服务提供商,将你的币和别人的币混在一起,这时候你去取币,取的就不是原来的地址;还有一种做法是应用提供的天然mixing,比如在线钱包,大家都往里面投了币,然后取得时候就不一定是用原来的地址;还有一种手段是通过比特币交易所,你在交易所里面托管了比特币,然后经过一段时间的投资,比特币→美元→以太坊→莱特币→比特币,这时候你取到的比特币可能就不是原来的地址了

+

零知识证明

+

零知识证明是指一方(证明者)向另一方(验证者)证明一个陈述是正确的,而无需透露除该陈述是正确的外的任何信息。
+一个有争议的例子:我想向别人证明这个账户是我的,也就是我持有它的私钥,但是我不能直接透露出私钥,我可以产生一个用私钥签的名,别人可以通过公钥验证签名,这个例子中,我是证明者,验证者是别人,陈述是我持有它的私钥,但是我没有透露私钥以外的信息。但这是有争议的,因为我没有透露私钥,但还是透露了用私钥产生的签名。

+

零知识证明的数学基础是同态隐藏
+图 1
+第一点说明不会发生碰撞,第二点说明隐藏性,针对第三点举例
+图 2
+图 3
+图 4
+图 5

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2020/01/07/\346\257\224\347\211\271\345\270\201\347\232\204\350\204\232\346\234\254/index.html" "b/2020/01/07/\346\257\224\347\211\271\345\270\201\347\232\204\350\204\232\346\234\254/index.html" new file mode 100644 index 0000000..aa61100 --- /dev/null +++ "b/2020/01/07/\346\257\224\347\211\271\345\270\201\347\232\204\350\204\232\346\234\254/index.html" @@ -0,0 +1,744 @@ + + + + + + + + + + + + + + + + + + + + 比特币脚本 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 比特币脚本 +

+ + + + +
+
+ +

交易实例

+

图 1

+

这个交易的下面一个输出已经被花掉了,并且这个交易得到了23个confirmation,回滚的可能性很小。
+输入脚本包含两个操作,分别把两个很长的数压入栈中,比特币使用的脚本语言非常简单,唯一能访问的内存空间只有堆栈,不像通用编程语言C++之类,有全局变量、局部变量、动态分配的内存空间等,它这里只有一个栈,所以叫基于栈的语言。输出脚本有两行,分别对应上面的两个输出,每个输出有自己单独的一段脚本。

+

交易具体内容

+

首先看一下交易的宏观信息
+图 2

+

txid:transaction id,交易id
+hash:交易的hash
+version:比特币协议版本号
+size:交易大小
+locktime:用来设定交易的生效时间,0表示立即生效,非0值表示过一段时间生效,比如说等10个区块之后才能被写入区块链中
+vin:输入部分
+vout:输出部分
+blockhash:这个交易所在区块的hash值
+confirmations:确认数
+time:交易产生时间
+blocktime:区块产生时间
+图 3

+

一个交易可能有多个输入,所以vin是个数组结构,上图只包含一个输入,每个输入都要说明输入花的币来自之前哪个交易的输出,所以前两行表示来源
+txid:之前交易的hash
+vout:之前这个交易的第几个输出
+scriptSig:输入脚本
+图 4

+

一个交易可能也有多个输出,所以vout也是一个数组结构,上图只包含两个输出
+value:输出金额,单位是比特币
+n:这个交易的第几个输出
+scriptPubKey:输出脚本
+asm:输出脚本的内容
+reqSigs:需要多少个签名
+type:输出类型,上图都是公钥的hash
+address:输出地址
+图 5

+

使用脚本验证交易合法性

+

验证这个交易的合法性就要将B→C的输入脚本和A→B的输出脚本拼接在一起执行。注意后一个交易的输入脚本在前,前一个交易的输出脚本在后,在早期的比特币系统中,这两个脚本是拼接在一起,从头到尾执行一遍,后来出于安全因素考虑,这两个脚本改为分别执行,首先执行输入脚本,如果没有出错,就执行输出脚本,如果能顺利执行,最后栈顶的结果为非0值,也就是true,这个交易就是合法的。如果执行过程中有任何错误,这个交易就是非法的。如果交易有多个输入,那么每个输入脚本都要和所对应的输出脚本进行配对验证,全都验证通过,这个交易才是合法的。
+图 6

+

输入和输出脚本的最简单形式就是P2PK,输出脚本直接给出收款人的公钥(PUSHDATA),CHECKSIG是检查签名的操作,输入脚本直接给出用私钥对输入脚本所在交易的签名。
+图 7

+

此处为了方便演示将输入脚本和输出脚本拼接在了一起,实际上是分开的。该脚本执行首先将输入脚本提供的签名压入栈中,然后将输出脚本提供的公钥压入栈,然后将栈顶两个元素弹出,用公钥检查签名是否正确,如果正确返回true
+图 8

+

上图是P2PK实例,上面这个交易的输入脚本就是把签名压入栈,下面的交易是上面交易的币的来源,输出有两行,第一行将公钥压入栈,第二行是验证。
+图 9

+

输入和输出脚本的第二种形式是P2PKH,这种形式与上一种形式的区别在于输出脚本没有直接给出收款人的公钥,给出的是公钥的hash,公钥在输入脚本给出,输入脚本既要给出签名,也要给出公钥,输出脚本其它操作是为了验证签名的正确性。这种形式是最常用的。
+图 10

+

上面两行来自输入脚本,后面来自输出脚本,还是从上往下执行,前两条语句将签名和公钥压入栈,DUP 表示将栈顶元素复制一遍,HASH160表示将栈顶元素弹出,取hash之后再压入栈,此时栈顶变成公钥的hash值,此时栈的情况如下图
+图 11

+

EQUALVERIFY是弹出两个栈顶元素,比较是否相等,相等就会消失
+图 12

+

最后 CHECKSIG 和上一种形式一致,弹出两个元素,用公钥检查签名的正确性,正确最后栈中只会留下一个true
+图 13

+

P2PKH是最常用的形式
+图 14

+

有一种最复杂的脚本形式P2SH,这种的形式的输出脚本给出的不是收款人的公钥hash,而是收款人提供的脚本的hash,叫redeemScriptHash(赎回脚本)。将来花这个钱的时候输入脚本要给出赎回脚本的具体内容,同时要给出让赎回脚本正常运行的签名
+图 15
+图 16
+图 17
+P2SH为什么这么复杂?P2SH在最初的比特币版本中并没有,后来通过软分叉的形式加进去的。常见的应用场景是对多重签名的支持。比特币系统中,一个输出可能要有多个签名才能把钱取出,比如某个公司的账户,可能要求5个合伙人中,任意3个合伙人的签名,才能把钱从公司账户中取走,这样为私钥的泄露提供了安全的保证,比如某些合伙人私钥泄露出去了,那么问题也不大,因为还需要另外两人的签名,才能取出钱来,同时也为私钥的丢失提供了冗余,5个合伙人中,即使有2人忘掉了私钥,剩下3人仍然可以把钱取出,然后转到某一个安全的账户。这个功能是通过CHECKMULTISIG实现的
+图 18
+输出脚本里给出N个公钥,同时给出一个域值M,输入脚本只要提供N个公钥中对应的签名中任意M个合法签名就可以通过验证。输入脚本的第一行红X表示压入一个多余的元素,CHECKMULTSIG有一个bug是会多弹出一个元素,因为去中心化的特性,现在不能通过软件升级改正。给出的M个签名的相对顺序要和N个公钥中的相对顺序一致才可以。
+图 19
+这个过程并没有用到P2SH,而是用的原生的CHECKMULTISIG。这样实现有许多不方便的地方,比如网上购物,某个电商平台用多重签名,要求5个合伙人中任意3个合伙人的签名才能把钱取出来,这就要求消费者在支付的时候要给出5个合伙人的公钥,同时还要给出N和M的值,这里N是5,M是3。消费者只能从平台获知这些信息,转账非常麻烦,这时候就可以采用P2SH实现多重签名
+图 20
+用P2SH实现多重签名可以将输出脚本的复杂度转移到赎回脚本里,输出脚本只要给出赎回脚本的hash,赎回脚本要给出N个公钥和N和M的值,赎回脚本在输入脚本提供,也就是说由收款人提供。
+图 21
+图 22
+现在的多重签名一般都采用这种P2SH形式

+

图 23
+最后一种脚本形式比较特殊,这种格式的输出脚本开头是RETURN操作,后面可以跟任意内容,RETURN操作的作用是无条件返回错误,所以包含这个操作的脚本永远不可能通过验证,执行到RETURN语句就出错终止。那么output的比特币岂不是永远花不出去了?确实花不出去,这种脚本的作用是证明销毁比特币,一种应用场景是有些小的币种要求销毁一定比特币才能换取这个币种,这种小币种叫AltCoin(Alternative Coin)。另外一个应用场景是往区块链里写一些内容,因为区块链是个不可篡改的账本,有人就利用这个特性,往里面添加一些永远需要保存的内容,比如前文提到的digital commitment,证明在某个时间知道某个事情,比如将知识产权的内容取hash写入RETURN后面,后面的内容反正永远不会执行,写什么都无所谓,而且放进去的是hash值,不会占太多地方也不会泄露内容,将来出现产权纠纷,可以将具体内容公布出去,证明你在某个时间已经知道某个知识了。
+图 24

+

这个交易的输入是铸币交易,第一个输出是出块奖励和交易费,第二个输出的金额是0,输出脚本就是上面那种特殊的形式,开头是RETURN,后面是一些看起来乱七八糟的东西,这个输出的目的就是往区块链里面写一些东西
+图 25

+

这是个普通的转账交易,输出脚本也是以RETURN开头。这个交易的输入是0.05个比特币,输出金额是0,说明输入金额全部用来支付交易费了,这个交易并没有销毁任何比特币,只不过把输入的比特币作为交易费转给挖到矿的矿工了,这种形式的好处是矿工看到这种脚本的时候知道它里面的输出永远不可能兑现,所以没有必要保存在UTXO里面,这样对全节点比较友好。

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2020/01/08/\344\273\245\345\244\252\345\235\212/index.html" "b/2020/01/08/\344\273\245\345\244\252\345\235\212/index.html" new file mode 100644 index 0000000..f88ca79 --- /dev/null +++ "b/2020/01/08/\344\273\245\345\244\252\345\235\212/index.html" @@ -0,0 +1,738 @@ + + + + + + + + + + + + + + + + + + + + 以太坊 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 以太坊 +

+ + + + +
+
+ +

以太坊概述

+

比特币和以太坊是两种最重要的加密货币,比特币被称为区块链1.0,以太坊被成为区块链2.0

+

以太坊在系统设计上,针对比特币运行过程中出现的一些问题进行了改进,比如说出块时间调整至15s,这是基于ghost协议的共识机制;另外一个改进是挖矿使用的mining puzzle,比特币的mining puzzle是计算密集型的,比拼的是计算hash的算力,这样会造成挖矿设备的专业化,以太坊设计的是memory hard mining puzzle ,目的是在一定程度上限制ASIC芯片的使用(ASIC resistance),将来以太坊还有一些革命性的改变,用权益证明(proof of stake)代替工作量证明(proof of work)。除此之外,以太坊还加入了重要功能——对智能合约的支持(smart contract)。

+

什么是智能合约?

+

我们知道比特币是去中心化的货币(decentralized currency),而以太坊是去中心化的合约(decentralized contract)。货币本来由政府发行,货币的价值是建立在政府公信力的基础上,政府通过司法手段来维护货币系统的正常运行。比特币的出现用技术手段把政府的职能取代了。去中心化的合约也是类似的意思,现实社会中,合约的有效性也是应该通过政府维护的,以太坊也用技术手段取代了司法手段,如果合同中的内容可以通过代码实现,代码就可以放到区块链上,通过区块链的不可篡改性来保证代码的正确运行,当然,不是所有的合同都能用编程语言实现,也不是说所有的合同条款都可以量化,但是逻辑比较简单清晰的是可以写成智能合约的。

+

去中心化的合约的好处?

+

去中心化货币的一个应用场景是跨国转账,比如人民币→小币种,手续麻烦且手续费贵,如果用比特币就很方便。智能合约也有类似的应用场景,假如合同方来自世界各地,没有一个统一的司法管辖权,比较难用司法手段维护合约的有效性,这时候可以将合约内容以代码形式写进区块链里保证合约强制执行

+

以太坊账户

+

比特币是基于交易的账本,这种模式下并没有显式记录账户中有多少钱。

+

以太坊采用的是基于账户的模式,这种模式和银行账户比较类似,系统中要显示记录账户中有多少以太币,转账的时候只要余额够就可以转账,不用说明币的来源也不用找零,这样可以防御double spending attack。以太坊虽然不用说明币的来源,但是也不能篡改账户余额,因为账户余额在所有全节点的状态数中维护,必须所有全节点认为你的账户余额发生改变,你的账户余额才发生改变。虽然能防御double spending attack,但也可能会受到replay attack(重放攻击),转账的时候收款人是有恶意的,他可能会将这次交易再次发布,导致付款人转了两次。double spending attack 和 replay attack 是相对的,double spending 是花钱的人不诚实,replay attack 是收钱的人不诚实。以太坊解决replay attack的手段是加一个nonce(交易数),交易数代表这个账户建立以来的交易次数,由全节点维护,这个交易数会在交易过程成写进交易信息一起签名保护,如果交易的交易数比全节点中维护的付款人的交易数刚好大1,说明是正常交易,如果交易被重放的话,两者会相等,属于不合法交易。

+

以太坊中有两类账户,一类是外部账户(externally owned account),外部账户类似于比特币的账户,就是一对公私钥,掌握了私钥就掌握了这个账户。外部账户有两个状态,一个是balance,一个是nonce。第二类账户是合约账户(smart contract account),合约账户不是通过公私钥对控制,除了balance和nonce,还有code,storage。

+

以太坊状态树

+

以太坊采用的是基于账户的模式,系统中显式维护每个账户的余额

+

用什么数据结构来实现这种模式?

+

我们要完成的是账户地址到账户状态的映射,addr→state,以太坊用的账户地址是160bits,一般表示成40个十六进制数,状态是外部账户的状态和合约账户的状态,包括余额,交易数,对于合约账户,还包括代码和存储。

+

第一种方案,用hash表+merkle tree。从直观上看,映射就是一个key-value对,很自然的想法是用一个hash表来实现。系统中的全节点维护一个hash表,每次有一个新的账户插入到hash表里,查询账户的余额就查hash表,如果不考虑hash碰撞,查询速度可以在常数级别,更新也很方便。这种方案的问题是,以太坊要防止所有账户的state被篡改,就要像比特币一样构建一个merkle tree,里面的交易变成账户状态,如果每次发布一个合法交易,某个账户状态发生改变,那么每个全节点都要重新构造merkle tree,代价太大。
+第二种方案,不用hash表,直接用merkle tree,修改的时候直接改merkle tree。这个方法的问题在于merkle tree没有提供高效的查找和更新的方法,另外不同节点构造的merkle tree 叶子节点顺序不同也会导致merkle root 不同 ,所以得用sorted merkle tree ,但是sorted merkle tree 也有问题,如果新创建的用户地址在hash值在中间,那么插入merkle tree之后,merkle tree几乎重构。

+

有一种数据结构叫trie(字典树或前缀树),从retrieval(检索)中来,下面是一个一些单词组织成一个trie的例子
+图 2

+

上图中,单词根据每一位的不同进行分裂,比如第二列有e和o就有两个分叉,第三列有在第二列是e的基础上只有n,所以只有一个分叉,单词有可能在非叶子节点结束。这个结构有一些特点。

+

第一个特点,在trie中,每个节点的分支数目取决于key中每个元素的取值范围,这个例子中,每个都是小写英文单词,所以每个节点的分叉数目最多27(26个小写字母+1个结束标志位)个,结束标志位表示到这个地方,这个单词就结束了。在以太坊中,地址是40位十六进制数,分叉数目有时候也叫branching factor 是17。
+第二个特点,trie的查找效率取决于key的长度,键值越长,查找需要访问内存的次数就越多,以太坊中,键值都是40位。
+第三个特点,如果用hash表存储key-value对,有可能出现碰撞,trie不会出现碰撞,只要地址不同,最后一定是不同分支
+第四个特点,mekle tree不排序的话插入的账户位置不一样导致树的结构不一样,trie不论插入顺序如何,插入内容一致,最后的树就是一样的,这个对于以太坊非常有用
+第五个特点,每次发布交易的时候,系统中大部分账户不变,只有少部分账户的状态需要更新,trie的局部更新性很好,只需要访问对应分支(注意,上图只画出了key)就可以找到value进行修改。

+

但是trie有一个缺点,trie的存储比较浪费,像上图有些节点只有一个子节点。如果能把这些节点合并,就可以减少存储的开销,也提高了查找的效率。

+

还有一种数据结构叫patricia tree或patricia trie(压缩前缀树),用上图例子进行路径压缩的结果如下
+图 3

+

直观上看,树的高度减少了,存储更密集了。但是,如果新插入一个单词,原来压缩的路径可能需要扩展开来,假设上图加入geometry,就不能压缩成EN节点了。路径压缩有时候效果明显,有时候不明显。树中插入的键值分布如果比较稀疏情况下,路径压缩效果明显。比如假如上图的每个英文单词都很长,但是一共没有几个单词(misunderstanding、decentralized、disintermediation(去中心商,意思是让系统中的价值提供者和消费者直接交互)),这个时候插入到trie中,就会变成下图
+图 4

+

如果用了压缩树,就会变成
+图 5

+

因此键值分布比较稀疏的时候,路径压缩效果较好。而在以太坊中,键值是地址,160位,总的地址空间有2160位,非常大。以太坊的账户数和2160相比微乎其微,所以键值分布非常稀疏

+

第三种方案 先提一下MPT(merkle patricia tree)。MPT和PT的区别就是连接节点之间的指针用的是hash指针,最后会保留一个关于状态树的merkle root,它的作用之一也是防止账户状态被篡改,作用之二是merkle proof证明账户余额,将账户状态所在分支发给轻节点即可证明,作用之三是证明账户不存在,如果账户存在,把应当存在的账户的所在分支发给轻节点验证,验证失败则不存在。以太坊的状态树用的就是MMPT(modified MPT),下图是一个例子。
+图 6

+

右上角有4个账户,为了简单起见,地址都非常短,就是上面的keys,账户状态只显示余额,就是上面的values。树中的节点分为三种,Extension Node,如果树的某一部位进行了压缩,就会出现一个Extension Node。因为4个地址的前两位开头都是a7,所以根节点就是Extension Node。下一层出现了分叉,所以出现了一个Branch Node。1的后面只有一个1355,所以它就是一个Leaf Node。7的后面有两个地址都是d3,所以压缩,再往下是3和9就分开来了,所以是一个Branch Node,再下面就是Leaf Node了。最右边的f后面就只有一个9365,所以是一个Leaf Node。

+

每次发布一个新的区块的时候,状态树中有一些节点的值会发生变化,这些改变不是原地修改,而是新建分支,原来的分支被保存。
+图 7

+

上图是两个相邻的区块,State Root是状态树的根hash值,虽然每一个区块只有一个State Root,但是两棵树的大部分节点是共享的,右边的树主要指向左边这棵树的节点,只有发生改变的节点需要新建分支。上图例子中是合约账户(包括nonce,balance,codehash,storage root的那个节点)发生变化,每一个合约账户的存储都是一棵小的MPT,上图交易次数nonce发生变化,balance发生变化,代码不变,所以codehash指向原来的code,存储变了,但是存储树中的大部分节点也是没有改变,唯一的改变的29变成了45,所以新建了一个分支。所以系统中要维护的不只是一颗MPT,而是每次出现一个区块,都要新建一个MPT,只不过这些状态树中大部分节点是共享的,只有少数发生变化的节点要新建分支。为什么要保留历史状态?系统当中有时候会出现分叉,临时性分叉非常普遍,假设出现一个分叉,两个节点同时获得记账权,如果上面一个节点胜出,下面的节点可以roll back(回滚对账户状态的修改)然后顺着上面的节点所在分支继续挖,这个和比特币不太一样,比特币交易类型比较简单,有的时候可以反向操作推断出前一个状态,比如说转账交易,A给B转了10个比特币,回滚只需要给A加10个比特币,B减去10个比特币,但是以太坊中不行,因为以太坊中有智能合约,智能合约执行完成后再推算之间的状态是不可能的,所以要想支持回滚就要记录历史状态。

+

以太坊中代码的数据结构!

+

图 8

+

上图是block header结构
+ParentHash:前一个区块块头的hash
+UncleHash:叔叔区块的hash,可能比Parent大好几辈
+Coinbase:挖出区块的矿工地址
+Root:状态树的根hash
+TxHash:交易树的根hash,类似于比特币中的merkle root
+ReceiptHash:收据树的根hash
+Bloom:布隆过滤器,和收据树相关,提供高效的查询符合某种条件的交易的执行结果
+Difficulty:挖矿难度
+GasLimit和GasUsed和汽油费相关,智能合约消耗汽油费,类似于比特币中的交易费
+Time:区块产生时间
+MixDigest和Nonce和挖矿过程相关
+图 9

+

上图是区块结构,header是指向block header的指针,uncles是指向叔叔区块的指针,而且是数组,transactions是交易列表
+图 10

+

上图是区块在网上发布的真实结构,其实就是区块结构的前三项。

+

我们知道状态树保存的是key-value pairs,key就是地址,value是账户状态,账户状态要经过序列化过程才能保存进状态树中,序列化用的是RLP(Recursive Length Prefix),特点是简单,只支持nested array of bytes ,意思是字节数组可以嵌套,以太坊中所有数据类型最后都要变成nested array of bytes,

+

以太坊的交易树和收据树

+

每次发布的区块中,交易会组织成一棵交易树,也是一棵merkle tree,和比特币中情况类似;每个交易执行完之后会形成一个收据,记录交易的相关信息,交易树和收据树上的节点是一一对应的,增加收据树是考虑到以太坊的智能合约执行过程比较复杂,通过增加收据树的结构有利于快速查询执行结果。从数据结构上看,交易树和收据树都是MPT,和比特币有所区别,比特币的交易树就是普通的merkle tree ,MPT也是一种merkle tree,但是和比特币中用的不是完全一样。对于状态树来说,查找账户状态所用的key是地址,对于交易树和收据树来说,查找的键值就是交易在区块中的序号,交易的排列顺序由发布区块的节点决定。这三棵树有一个重要的区别,就是交易树和收据树都是只把当前发布的区块中的交易组织起来,而状态树是把系统中所有账户的状态都组织起来,不管账户和当前区块中的交易有没有关系。从数据结构上来说,多个区块的状态树是共享节点的,每次新发布的区块时,只有区块中的的交易改变了账户状态的那些节点需要新建分支,其它节点都沿用原来状态树上的节点。相比之下不同区块的交易树和收据树都是独立的。

+

交易树和收据树的作用

+

交易树一个用途是merkle proof ,像比特币中用来证明某个交易被打包到某个区块里。收据树也是类似的,证明某个交易的执行结果,也可以在收据树里提供一个merkle proof。除此之外,以太坊还支持更复杂的查询操作,比如查询过去十天当中和某个智能合约有关的交易,这个查询方法之一是,把过去十天产生的所有区块中交易都扫描一遍,看看哪些是和这个智能合约相关的,这种方法复杂度比较高,且对轻节点不友好。以太坊中的查询是引入了bloom filter(布隆过滤器),这个数据结构支持比较高效的查找某个元素是不是在一个比较大的集合里,bloom filter给一个大的集合计算出一个很紧凑的摘要,比如说一个128位的向量,向量初始都是0,通过hash函数,把集合中的每个元素映射到向量中的某个位置,元素的映射位置都置为1,所有元素处理完后向量就是一个摘要,这个摘要比原来的集合小很多。这个过滤器的作用是,我们想查询一个元素,但集合太大我们不能保存,这时候对该元素取hash值,发现映射到向量中0的位置,说明这个元素不在集合里,但是映射到向量中1的位置,也不能说明元素在集合里,因为可能会出现hash碰撞。所以用bloom filter时,可能会出现false positive,但是不会出现false negative,意思是有可能出现误报,但是不会出现漏报,在里面一定说在里面,不在里面可能也会说在里面。bloom filter有各种各样的变种,比如说像解决hash碰撞,有的bloom filter用的不是一个hash函数,而是一组,每个hash函数独立的把元素映射到向量中的某个位置,用一组hash函数的好处是,一般不可能一组hash函数都出现碰撞。bloom filter的一个局限性不支持删除操作,因为存在hash碰撞,使得不同元素映射到向量同一个位置,如果删掉一个元素,使对应位置上的1变成0,那么和它发生碰撞的元素也被删除了,所以简单的bloom filter 不支持删除操作,可以将0和1改成计数器,记录有多少元素映射过来,而且还要考虑计数器是否会overflow,但是这样就复杂的多,和当初设计的理念就违背了,所以一般用bloom filter就不支持删除操作。以太坊中bloom filter 的作用是,每个交易执行完成后会形成一个收据,收据里面就包含了一个bloom filter ,记录这个交易的类型、地址等其它信息,发布的区块在块头里也有一个总的bloom filter ,这个总的bloom filter 是区块里所有交易的bloom filter 的并集,所以说想查询过去十天当中和某个智能合约有关的交易,先查哪个区块的块头的bloom filter里有我要的交易的类型,如果块头的bloom filter里面没有,那么这个区块里面就没有我们想要的,如果块头的bloom filter 里有,我们再去查找区块里面包含的交易所对应的收据树里面对应的bloom filter,但是可能会出现误报,如果有的话,我们再找到相对应的交易进行确认,好处是通过bloom filter能快速过滤大量无关区块,很多区块看块头的bloom filter就知道没有我们想要的交易,剩下的少数候选区块再仔细查看。轻节点只有块头信息,根据块头就能过滤掉很多信息,剩下有可能是想要的区块,问全节点要具体信息。

+

以太坊的运行过程可以看作交易驱动的状态机(transaction-driven state machine),状态机的状态指状态树中的那些账户状态,交易指交易树中那些交易,通过执行这些交易,使得系统从当前状态转移到下一个状态。比特币也可以认为是交易驱动的状态机,比特币中的状态是UTXO。这两个状态机有一个共同特点是状态转移都是确定性的,对一组给定的交易能够确定性的驱动系统转移到下一个状态,因为所有的节点都要执行同样的交易,所以状态转移必须是确定性的。

+

以太坊挖矿算法

+

对于基于工作量证明的区块链系统来说,挖矿是保障安全的重要手段。为了抵制矿机,以太坊设计了一中memory hard mining puzzle,以太坊用了两个数据集,一个是16M的cache,一个是1G的dataset叫做DAG,DAG是从cache中生成,这样设计的目的是便于轻节点验证,轻节点只需要保存16M的cache即可,只有矿工才需要保存1G的大数据集。基本思想是先用一个种子节点经过一些运算得到数组的第一个元素,然后对元素依次取hash得到后面的元素,这样得到的是一个填充了伪随机数的数组,就是一个cache,然后大数据集里面的每一个元素根据cache里的元素,依次读取256次取hash生成,求解puzzle的时候用的是大数据集,按照伪随机的顺序从大数据集中读取128个数,一开始,根据区块的块头算出一个初始的hash,根据hash映射到大数据集中的某个位置,把该数读取出来,然后进行运算得到下一个数得位置,每次读取的时候除了计算出这个元素的位置之外,还要把相邻的元素读取出来,进行64次循环,每次取出2个数,得到128个数,最后算出一个hash值,和挖矿难度的目标阈值比较一下,如果不合适就将block header里面的nonce替换一下重复上面过程

+

权益证明

+

比特币和以太坊目前用的都是基于工作量的证明,这种共识机制受到普遍的批评就是浪费电。

+

矿工挖矿是出于出块奖励,算力越大,出块奖励平均下来就越大,算力取决于设备的多少,也就是资金的投入,资金投入越多,奖励也越丰厚。那么我们可不可以不挖矿,直接比拼资金,奖励按资金比分配?这就是权益证明的思想,权益证明有时候也叫virtual mining。

+

采用权益证明的交易货币,一般会在正式发行之前预留一些货币给开发者,也会出售一部分货币来换取开发加密货币所需要的资金,将来按照权益证明的共识机制,每个人按照持有货币的数量进行投票,这种方法和工作量证明相比有一些优点,一个是不需要挖矿了,减少能耗;二是挖矿的算力从现实世界来,攻击者只要足够富裕,买大量矿机就可以发动攻击,对于小币种是致命打击,权益证明是按持有的货币数量进行投票,类似股票分红,如果某人想发动攻击,他需要先获得货币总量的51%才能发动攻击,也就是说发动攻击的资源必须从加密货币的系统中来,这样就系统形成了一个闭环,无论攻击者在系统外有多少资源,都不会对系统造成直接的影响,如果一定要发动攻击就要买大量的币,造成币的大涨,而开发者和早期矿工就可以从中获利。权益证明和工作量证明不是互斥的,有些加密货币采用的是混合模型,仍然要挖矿,但是挖矿难度和持有多少币是相关的,币越多难度越低,但是这样简单设计有一个问题就是富人挖矿越来越简单。有些两者混用的加密货币系统会将用于降低挖矿难度的币锁定一段时间,下次再挖一个区块的时候,不能用锁定的币降低难度,过几个区块才能使用,这种叫proof of deposit。

+

权益证明有许多问题,早期的权益证明有一个问题是两边下注
+图 11

+

上图出现分叉,如果挖矿的话,我们会沿着上面这条链继续挖,但是下面的链也有可能成为最长合法链,只要下面这个分支连续挖出好几个区块。但是矿工不会两边都挖,因为算力会分散。如果不挖矿,用权益证明的话,两边都可以下注,如果上面那条链成为最长合法链,下面分支锁定的币对上面分支没有影响的,所以这种情况叫nothing at stake。

+

以太坊准备采用的权益证明协议叫做Casper the Friendly Finality Gadget(FFG),它在过渡阶段也是要和工作量证明混合使用,为工作量证明提供finality,finality 是最终状态,包含在finality中的交易不会被取消,单纯基于工作量证明是有可能被回滚的,Casper协议引入validator,要想成为validator,必须投入一定数量的以太币作为保证金,这个保证金会被锁定,validator推动系统达成共识,投票决定哪条链是最长合法链,投票权重取决于保证金的大小。挖矿的时候(混用状态下)每挖出一百个区块,就作为一个epoch,然后要决定它能不能成为finality要进行投票,投票进行两轮,类似于数据库的two-phrase commit,一个是prepare message,一个是commit message,Casper规定每一轮投票都要获得2/3以上的投票才能通过。实际当中不区分投票阶段,epoch也减少至50个,每个epoch只用一轮投票就行,这轮投票对于上一个epoch来说是commit message,对于下一个epoch来说是prepare message,要连续两个epoch都得到2/3的投票才算有效。
+图 12

+

上图是早期的Casper协议,100个区块构成一个epoch,每个epoch要投两轮,都要获得2/3的票

+

图 13

+

上图是实际的Casper,这轮投票对于上一个epoch来说是commit message,对于下一个epoch来说是prepare message,要连续两个epoch都得到2/3的投票才算通过。
+验证者验证的好处是如果验证者履行职责,那么可以获得相应的奖励,就像矿工挖矿能获得出块奖励一样,验证者验证也可以得到奖励,相反,如果验证者有不良行为,要受到相应处罚,比如验证者不作为,导致系统迟迟达不成共识,这样要扣掉验证者的部分保证金,如果验证者乱作为,给两个有冲突的分叉都投票,这种情况要没收全部的保证金。没收的保证金会销毁,相当于减少了以太币的总量。每个验证者有一定的任期,任期满了之后要经过一定时间的等待期,等待期是为了让其它节点可以检举验证者的不良行为,等待期过了没有受到惩罚那么验证者可以取回保证金以及一定的奖励,这就是casper协议的过程。

+

这里有一个问题,包含在finality的交易是不是一定不会被回滚,假设有某个恶意节点发动攻击,如果他只是一个矿工,那么他是不能推翻已经达成的finality,因为finality是验证者投票投出来的。如果有大量的验证者两边下注,给前后两个有冲突的finality都下注,casper协议规定每轮投票要2/3的支持才算通过,所以至少有1/3的验证者是两条分叉都投票了。

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2020/01/18/MetaMask\350\277\236\346\216\245\347\247\201\346\234\211\351\223\276\345\217\221\347\224\237\347\232\204\350\275\254\350\264\246\351\227\256\351\242\230/index.html" "b/2020/01/18/MetaMask\350\277\236\346\216\245\347\247\201\346\234\211\351\223\276\345\217\221\347\224\237\347\232\204\350\275\254\350\264\246\351\227\256\351\242\230/index.html" new file mode 100644 index 0000000..4b29b73 --- /dev/null +++ "b/2020/01/18/MetaMask\350\277\236\346\216\245\347\247\201\346\234\211\351\223\276\345\217\221\347\224\237\347\232\204\350\275\254\350\264\246\351\227\256\351\242\230/index.html" @@ -0,0 +1,669 @@ + + + + + + + + + + + + + + + + + + + + MetaMask连接私有链发生的转账问题 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ MetaMask连接私有链发生的转账问题 +

+ + + + +
+
+ +

我用ganache-cli启了一个以太坊网络,然后我在MetaMask连接到了这个网络,并且导入了一个账户,正常显示余额是100ETH,但是转账的时候发生了错误。

+
EthQuery - RPC Error - Error: [ethjs-rpc] rpc error with payload {"id":3715053778334,"jsonrpc":"2.0","params":["0xf8720485012410110082520894c8fb523ca95721bf3408f6e6ef0ed8e7c3f65488880de0b6b3a7640000808602df64fab56ea052b79e6fe25a6bd073f48d853c053d35b0d4de1e3f1e77e7e82c520bd24d9783a059bf550c3fa7fec8a93584f77b0998545b936e7e583bb53076fb2eac9664725d"],"method":"eth_sendRawTransaction"} [object Object]
+
+

错误原因是ganache的chainId和MetaMask的chainId不同

+

解决方法:
+ganache-cli -i 1 -h 0.0.0.0 -p 7545

+

-i 指定启动的链id,我这里指定1
+-h 指定监听所有ip,因为我把ganache装在了云服务器上面,且没有注册域名,没法用nginx做代理,所以只能监听所有ip,这样我才能在本机访问到
+-p 指定端口,默认是8545,但是truffle部署合约时是用的7545,所以懒得改truffle的代码就可以指定ganache在7545启动

+

随后在metamask中添加一个自定义网络并填上相应信息,metamask建议去github上下压缩包自己添加到扩展程序中,我在chrome商店下了N回都是出错,metamask添加网络步骤如下
+图 2
+图 3
+图 4

+

网络名称随意,url如果本机就填 http://localhost:7545,如果是服务器填上对应ip,关键是ChainID一定要和ganache-cli 启动时一致,最后保存即可

+

注意:如果你的MetaMask已经连到了你本来没有指定chainId的网络了,那你再指定chainId启动网络,MetaMask转账可能仍然报错,这时候可以把MetaMask删掉重新加入扩展程序

+

转账成功的样子
+图 6

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2020/02/10/truffle\351\203\250\347\275\262\345\220\210\347\272\246\345\210\260\350\277\234\347\250\213\347\247\201\346\234\211\351\223\276/index.html" "b/2020/02/10/truffle\351\203\250\347\275\262\345\220\210\347\272\246\345\210\260\350\277\234\347\250\213\347\247\201\346\234\211\351\223\276/index.html" new file mode 100644 index 0000000..77632a5 --- /dev/null +++ "b/2020/02/10/truffle\351\203\250\347\275\262\345\220\210\347\272\246\345\210\260\350\277\234\347\250\213\347\247\201\346\234\211\351\223\276/index.html" @@ -0,0 +1,695 @@ + + + + + + + + + + + + + + + + + + + + truffle部署合约到远程私有链 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ truffle部署合约到远程私有链 +

+ + + + +
+
+ +

首先安装了truffle和truffle-hdwallet-provider

+
npm install truffle -g
+
+
npm install truffle-hdwallet-provider
+
+

安装truffle卡住的同学请在truffle后面加上@5.0.1指定版本

+

修改truffle配置文件

+
var HDWalletProvider = require("truffle-hdwallet-provider");  // 导入模块
+var mnemonic = "你的助记词";  //MetaMask的助记词。
+
+module.exports = {
+
+  networks: {
+    development: {
+      network_id: "*",       // Any network (default: none)
+      provider: function () {
+        // mnemonic表示MetaMask的助记词。 "ropsten.infura.io/v3/33..."表示Infura上的项目id
+        return new HDWalletProvider(mnemonic, "http://ip:port", 0);   // 1表示第二个账户(从0开始)
+      }
+    },
+  },
+
+  // Set default mocha options here, use special reporters etc.
+  mocha: {
+    // timeout: 100000
+  },
+
+  // Configure your compilers
+  compilers: {
+    solc: {
+      // version: "0.5.0",    // Fetch exact version from solc-bin (default: truffle's version)
+      // docker: true,        // Use "0.5.1" you've installed locally with docker (default: false)
+      // settings: {          // See the solidity docs for advice about optimization and evmVersion
+      //  optimizer: {
+      //    enabled: false,
+      //    runs: 200
+      //  },
+      //  evmVersion: "byzantium"
+      // }
+    }
+  }
+}
+
+
+

上面的 new HDWalletProvider 让我非常迷,我在metamask导入了一个有余额的账户,按理说是1,但是指定1部署的时候用的不是这个账户,所以我就给0账户转了点钱,依旧指定0账户

+

最后在终端执行部署命令

+
truffle migrate
+
+

注意,不要忘记把挖矿开起来,不然合约不会部署上去

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/05/06/Git\345\255\246\344\271\240\347\254\224\350\256\260/index.html" "b/2022/05/06/Git\345\255\246\344\271\240\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000..4c26ffb --- /dev/null +++ "b/2022/05/06/Git\345\255\246\344\271\240\347\254\224\350\256\260/index.html" @@ -0,0 +1,922 @@ + + + + + + + + + + + + + + + + + + + + Git学习笔记 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Git学习笔记 +

+ + + + +
+
+ +

新建git项目

+

在一个目录下执行

+
git init
+
+

克隆git项目

+

在一个目录下执行

+
git clone [ssh/https]address
+
+

管理修改

+

将文件修改添加到暂存区

+
git add filename
+
+

将暂存区的修改提交到本地仓库

+
git commit -m 'commitMessage' [filename]
+
+

每次修改完要先add到暂存区,然后commit才能把你修改的东西提交的本地仓库

+

提交信息规范

+

commit message格式

+
<type>(<scope>): <subject>
+
+
type(必须)
+

用于说明git commit的类别,只允许使用下面的标识。

+

feat:新功能(feature)。

+

fix/to:修复bug,可以是QA发现的BUG,也可以是研发自己发现的BUG。

+
    +
  • fix:产生diff并自动修复此问题。适合于一次提交直接修复问题
  • +
  • to:只产生diff不自动修复此问题。适合于多次提交。最终修复问题提交时使用fix
  • +
+

docs:文档(documentation)。

+

style:格式(不影响代码运行的变动)。

+

refactor:重构(即不是新增功能,也不是修改bug的代码变动)。

+

perf:优化相关,比如提升性能、体验。

+

test:增加测试。

+

chore:构建过程或辅助工具的变动。

+

revert:回滚到上一个版本。

+

merge:代码合并。

+

sync:同步主线或分支的Bug。

+
scope(可选)
+

scope用于说明 commit 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。

+

例如在Angular,可以是location,browser,compile,compile,rootScope, ngHref,ngClick,ngView等。如果你的修改影响了不止一个scope,你可以使用*代替。

+
subject(必须)
+

subject是commit目的的简短描述,不超过50个字符。

+

建议使用中文(感觉中国人用中文描述问题能更清楚一些)。

+
    +
  • 结尾不加句号或其他标点符号。
  • +
  • 根据以上规范git commit message将是如下的格式:
  • +
+
fix(DAO):用户查询缺少username属性 
+feat(Controller):用户查询接口开发
+
+

查看工作区和暂存区的不同

+
git diff [filename]
+
+

查看暂存区和版本库的不同

+
git diff --cached [filename]
+
+

删除修改

+

在文件已经加入版本库的情况下

+
    +
  1. +

    如果文件修改后没有add进暂存区,我们可以用

    +
    git checkout -- filename
    +
    +

    取消工作区的修改,恢复到和版本库一摸一样的状态

    +
  2. +
  3. +

    如果文件修改后已经add进暂存区了,我们可以用

    +
    git reset HEAD filename
    +
    +

    取消暂存区的修改,恢复到工作区有修改的状态,这是查看git status

    +

    会看到

    +

    图 6

    +

    要撤销工作区的修改就用上面的checkout

    +
  4. +
  5. +

    如果文件修改后已经commit到了本地仓库,那么我们只能使用版本回退了

    +
    git reset --hard [commitID]
    +
    +

    commitID可以通过git log 或者 git reflog查看。git log 查看当前版本的所有commit,git reflog查看所有commit,所以回退到了老版本后想回到新版本后得先git reflog 查看新版本的commitID

    +
  6. +
+

在文件没有加入版本库的时候

+

这种情况一般是新生成的文件,工作区可以任意编辑,因为版本库中没有,所以不需要checkout(checkout在没有add的时候恢复至版本库状态)

+

图 7

+

如果add进了暂存区,此时和上面一样,可以使用

+
git resset HEAD filename
+
+

恢复至工作区有修改的状态

+

图 8

+

图 9

+

删除文件

+

如果文件已经处于tracked状态(已经被add了),当你要删除文件的时候,可以采用命令:

+
rm filename
+
+

这个时候(也就是说这个时候只执行了rm test.txt)有两种情况

+

第一种情况:的确要把test.txt删掉,那么可以执行

+
git rm test.txt 或者  git add test.txt
+git commit -m "remove test.txt"
+
+

然后文件就被删掉了,这种情况想恢复只能用版本回退了,但是之前的版本也没有提交这个文件,那这个就永远消失了

+

第二种情况:删错文件了,不应该删test.txt,注意这时只执行了rm test.txt,还没有提交,所以可以执行

+
git checkout test.txt
+
+

将文件恢复。

+

并不是说执行完git commit -m "remove test.txt"后还能用checkout恢复,commit之后版本库里的文件也没了,自然没办法用checkout恢复,而是要用其他的办法(版本回退)

+

远程仓库

+

检测是否能ssh登录远程仓库

+
ssh -T git@github.com
+
+

图 10

+

添加远程仓库

+
git remote add origin git@github.com:username/reponame.git
+
+

查看远程仓库

+
git remote
+
+

抓取/合并远程仓库

+
git fetch origin
+git merge origin/master
+
+

推送到远程仓库

+
git push -u origin master
+
+

-u 表示把本地的master分支和远程仓库origin的master分支关联

+

后面就可以用

+
git push origin master
+
+

删除远程仓库关联

+
git remote rm origin
+
+

分支管理

+

新建并切换分支

+
git checkout -b dev
+
+

和下面作用一样

+
git branch dev
+git checkout dev
+
+

查看分支

+

图 11

+

*表示当前所在分支

+

分支操作

+

和上面的文件修改一个意思,只不过上面默认是在master分支操作

+

合并

+

在dev分支下执行文件修改,add、commit操作不会影响到master分支,dev开发完成之后可以切换到master分支,执行合并操作

+
git merge dev
+
+

删除分支

+
git branch -d dev
+
+

注意:在任何一条分支上做的修改必须要commit到本地仓库在能算作在dev分支上的一个版本,否则切换到别的分支,别的分支仍然能看到修改没有被添加到暂存区或者没有提交到本地仓库,别的分支可以继续处理这个修改,有一个办法是git stash保留当前分支上的工作内容,这样切到别的分支上git status就没有要处理的修改了

+

解决冲突

+

Git用<<<<<<<=======>>>>>>>标记出不同分支的内容

+

图 12

+

图 13

+

<<<<和=====之间是HEAD(当前分支)的内容,=====和>>>>>之间是feature1分支的内容

+

下图是手动修改后的冲突文件

+

查看分支合并情况

+
git log --graph
+
+

bug分支

+

保留现场

+
git stash
+
+

然后切到有bug的分支上,新建一个分支,修复好后merge到出现bug的分支上,然后查看保存的工作现场

+
git stash list
+
+

恢复现场有两种办法:

+
git stash pop
+
+

pop会同时恢复现场和删除保存的工作现场

+

也可以指定恢复现场

+

图 14

+
git stash apply stash@{0}
+
+

然后删除分支

+
git stash drop stash@{0}
+
+

复制提交

+

比如上面bug分支提交了一个修改之后merge到了master分支,我们正在开发的dev分支也有这个bug,我们可以把bug分支提交的修改复制到dev分支

+
git cherry-pick commitID
+
+

commitID通过git log查看

+

推送分支

+
git push origin branchName
+
+

多人协作

+
    +
  • 查看远程库信息,使用git remote -v
  • +
  • 本地新建的分支如果不推送到远程,对其他人就是不可见的;
  • +
  • 从本地推送分支,使用git push origin branch-name,如果推送失败,先用git pull抓取远程的新提交;
  • +
  • 在本地创建和远程分支对应的分支,使用git checkout -b branch-name origin/branch-name,本地和远程分支的名称最好一致;
  • +
  • 建立本地分支和远程分支的关联,使用git branch --set-upstream branch-name origin/branch-name
  • +
  • 从远程抓取分支,使用git pull,如果有冲突,要先处理冲突。
  • +
+

代理

+

设置代理

+
git config --global http.proxy socks5://127.0.0.1:7890
+git config --global https.proxy socks5://127.0.0.1:7890
+
+

后面的端口是你的梯子监听的端口(clash默认7890)

+

如果是在wsl2中使用,请看WSL2.md中的代理配置

+

https代理存在一个局限,那就是没有办法做身份验证,每次拉取私库或者推送代码时,都需要输入github的账号和密码,非常痛苦。
+设置ssh代理前,请确保你已经设置ssh key。可以参考在 github 上添加 SSH key 完成设置更进一步是设置ssh代理。只需要配置一个config就可以了。

+
# Linux、MacOS
+vi ~/.ssh/config
+# Windows 
+到C:\Users\your_user_name\.ssh目录下,新建一个config文件(无后缀名)
+
+

将下面内容加到config文件中即可

+

对于windows用户,代理会用到connect.exe,你如果安装了Git都会自带connect.exe,如我的路径为C:\APP\Git\mingw64\bin\connect

+
#Windows用户,注意替换你的端口号和connect.exe的路径
+ProxyCommand "C:\APP\Git\mingw64\bin\connect" -S 127.0.0.1:51837 -a none %h %p
+
+#MacOS用户用下方这条命令,注意替换你的端口号
+#ProxyCommand nc -v -x 127.0.0.1:51837 %h %p
+
+Host github.com
+  User git
+  Port 22
+  Hostname github.com
+  # 注意修改路径为你的路径
+  IdentityFile "C:\Users\Your_User_Name\.ssh\id_rsa"
+  TCPKeepAlive yes
+
+Host ssh.github.com
+  User git
+  Port 443
+  Hostname ssh.github.com
+  # 注意修改路径为你的路径
+  IdentityFile "C:\Users\Your_User_Name\.ssh\id_rsa"
+  TCPKeepAlive yes
+
+

保存后文件后测试方法如下,返回successful之类的就成功了

+
# 测试是否设置成功
+ssh -T git@github.com
+
+

取消代理

+
git config --global --unset http.proxy
+git config --global --unset https.proxy
+
+

github搜索命令

+

仓库搜索

+

in:name example 名字中有“example”

+

in:readme example readme中有“example”

+

in:description example 描述中有“example”

+

stars:>1000 star>1000

+

forks:>1000 fork>1000

+

pushed:>2019-09-01 2019年9月1日后有更新的

+

language:java 用Java编写的项目

+
+

https://docs.github.com/en/search-github/searching-on-github/searching-for-repositories

+
+

issue搜索

+

java in:title,body,comments 搜索标题中内容中评论中包含java的issue

+
+

https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests

+
+

零碎

+

已经提交的文件取消追踪

+

先修改.gitignore

+
git rm -r --cached .
+
+git add .
+
+git commit -m 'update .gitignore'
+
+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/05/07/LeetCode\345\210\267\351\242\230\350\256\260\345\275\225-cpp/index.html" "b/2022/05/07/LeetCode\345\210\267\351\242\230\350\256\260\345\275\225-cpp/index.html" new file mode 100644 index 0000000..165f7c4 --- /dev/null +++ "b/2022/05/07/LeetCode\345\210\267\351\242\230\350\256\260\345\275\225-cpp/index.html" @@ -0,0 +1,1037 @@ + + + + + + + + + + + + + + + + + + + + LeetCode刷题记录(cpp) | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ LeetCode刷题记录(cpp) +

+ + + + +
+
+ +

stringstream用法

+

需要include<sstream>,任何输入输出都会被转换成字符串

+

istringstream类用于执行C风格的串流的输入操作

+

ostringstream类用于执行C风格的串流的输出操作

+

stringstream类同时支持C风格的串流的输入输出操作

+
#include <iostream>
+#include <vector>
+#include <string>
+#include <sstream>
+using namespace std;
+// 例子1: 基本用法
+int main()
+{
+    string s;
+    stringstream ss;
+    int n = 11, m = 88;
+    // 将11放入流中
+    ss << n;
+    // 从流中提取数据到s中,自动类型转换,无需关心类型问题
+    ss >> s;
+    // 11
+    cout << s << endl;
+    s += "23";
+    // 1123
+    cout << s << endl;
+    // 清空,以便下一次使用
+    ss.clear();
+    ss.str("");
+    ss << s;
+    ss >> n;
+    // 1123
+    cout << n << endl;
+    return 0;
+}
+
+

+// 例2:按空格分隔字符串
+int main(int argc, char const *argv[])
+{
+    stringstream ss("1 2 3");
+    string s;
+    while (ss >> s)
+    {
+        cout << s << endl;
+    }
+    return 0;
+}
+
+// 例3:按分隔符分隔字符串
+const vector<string> split(const string &str, const char &delimiter)
+{
+    vector<string> result;
+    stringstream ss(str); // 等于ss.str(str);
+    string tok;
+    while (getline(ss, tok, delimiter)) // 从ss流中按delimiter为分隔符读取数据存储到tok中,直到流结束
+    {
+        result.push_back(tok);
+    }
+    return result;
+}
+
+

默认值

+

string类型的默认值是""

+

int类型的默认值是0

+

substr函数用法

+
string s("123");
+string a = s.substr(0,5);// 获得字符串s中从第0位开始的长度为5的字符串,若 pos+n>s.length(),只会截取到末尾
+
+

string.length()或者size()与变量作比较的bug

+

string.length()是unsigned型的不能与负数作比较,否则在机器数的表示上俩者都会以unsigned的编码形式比较,那就大概率结果就不对了,所以建议平时编程养成好习惯,类型不一样的数据比较或运算时一定要留一个心眼,不能直接拿来就比。用的时候可以在前面写上强转

+

链表题技巧

+
    +
  • 多用定义变量,不容易被逻辑绕晕,比如修改指针前先用first、second、third先保存下来
  • +
  • head有可能发生改动时,先增加一个假head,返回的时候返回head->next,避免修改头结点导致多出一堆逻辑
  • +
+

二叉树题技巧

+
    +
  • 先考虑递归(前中后序遍历)还是迭代(层序遍历)
  • +
  • 递归要先考虑函数是否有返回值,比如说判断是否是平衡二叉树,当前递归函数中要根据左右子树的返回值做判断,所以递归函数要返回值
  • +
  • 然后考虑是前,还是中,还是后。如果要先处理左右子树那就是后,特别地,回溯也是后,参考https://programmercarl.com/0236.二叉树的最近公共祖先.html
  • +
  • 最后再考虑递归终止条件,一般是root==nullptr就返回,也有特殊情况,比如叶子节点就返回
  • +
  • 层序遍历一般用队列。如果要一层一层的访问,在出队的时候要先记录当前层的节点数,根据节点数出队
  • +
+

数字和字符串互转

+

数字转字符串:to_string(number)

+

字符串转数字:stoi(intStr) , stol(longStr), stof(floatStr), stod(doubleStr)

+

数组初始化

+

初始化一个n*n的二维数组,用0填充

+

vector<vector<int>>v(n,vector<int>(n,0));

+

或者

+
v.resize(n);
+for(int k=0;k<n;k++){
+  v[k].resize(n);
+}
+
+

或者用memset

+
int v[n][n];
+memset(v,0,sizeof(int)*n*n); 
+
+

DFS和BFS适用条件

+

dfs适合找解的存在和所有解,bfs适合找最优解。例如dfs可以解决可达性,bfs可以解决无权图最短路径(有权使用dijkstra)。dfs需要回溯时要注意,以N皇后和解数独为例,如果当前位置的合法性依赖之前放置的位置,那么回溯时要将位置清除(N皇后),如果当前位置的合法性不依赖之前的位置,而是依赖别的辅助空间,那么回溯时只需要清除辅助空间,放置的位置可以不清除(解数独)

+

图的连通性

+

考虑dfs和并查集

+

自定义排序

+
// sort函数第三个参数是lambda表达式,[x] 表示捕获外部的变量x
+sort(arr.begin(),arr.end(),[x](const int& a,const int& b){
+       return abs(a-x)==abs(b-x)?a<b:abs(a-x)<abs(b-x);
+});
+
+

二分技巧

+

标准二分查找

+
int search(vector<int>& nums, int target)
+{
+    int left = 0;
+    int right = nums.size() - 1;
+    while (left <= right)
+    {
+        int mid = left + ((right - left) >> 1);
+        if (nums[mid] == target)
+            return mid;
+        else if (nums[mid] > target)
+        {
+            right = mid - 1;
+        }
+        else
+        {
+            left = mid + 1;
+        }
+    }
+    return -1;
+}
+
+

二分查找第一个满足XXX的位置

+
int left = 0, right = nums.size()- 1, ans = -1;
+// <= 可以判断区间长度为1时,即left==right时是否有解;若二分条件必须在区间大于等于2的情况下判断,则去掉=,参考leetcode162
+while (left <= right)
+{
+    int middle = left + (right - left) / 2;
+    if (满足XXX)
+    {
+        ans = middle;
+        right = middle - 1;
+    }
+    else
+    {
+        left = middle + 1;
+    }
+}
+return ans;
+
+

二分查找最后一个满足XXX的位置

+
int left = 0, right = nums.size()- 1, ans = -1;
+// <= 可以判断区间长度为1时,即left==right时是否有解;若二分条件必须在区间大于等于2的情况下判断,则去掉=,参考leetcode162
+while (left <= right)
+{
+    int middle = left + (right - left) / 2;
+    if (满足XXX)
+    {
+        ans = middle;
+        left = middle + 1;
+    }
+    else
+    {
+        right = middle - 1;
+    }
+}
+return ans;
+
+

STL的二分查找

+

lower_bound(begin,end,num): 从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的迭代器,不存在则返回end。通过返回的迭代器减去begin可以得到数字的下标。

+

upper_bound(begin,end,num): 从数组的begin位置到end-1位置二分查找第一个大于num 数字,找到返回该数字的迭代器,不存在则返回end。通过返回的迭代器减去begin可以得到数字的下标。

+
// 用在vector上
+vector<int> nums1{1,10,4,4,2,7};
+sort(nums1.begin(),nums1.end());
+int pos1 = lower_bound(nums1.begin(),nums1.end(),9)-nums1.begin();
+
+// 用在数组上
+int nums2[6] = {1, 10, 4, 4, 2, 7};
+sort(nums2,nums2+6);
+int pos2 = lower_bound(nums2,nums2+6,9)-nums2;
+
+// 返回vector中最靠近5的第一个小于或等于5的数
+vector<int> nums1{1,10,4,4,2,7};
+sort(nums1.rbegin(),nums1.rend());
+int pos3 = lower_bound(nums1.begin(), nums1.end(), 5,greater<int>())-nums1.begin();
+
+

优先队列

+
// 升序序列
+priority_queue<int, vector<int>, greater<int>>q;
+// 降序序列(默认,等同于priority_queue<int>)
+priority_queue<int, vector<int>, less<int>>q;
+ 
+// 自定义类型排序
+struct student
+{
+    student(int age){
+        this->age = age;
+    }
+    int age;
+};
+struct cmp{
+    bool operator() (student& s1,student& s2){
+        return s1.age<s2.age;
+    }
+};
+int main(int argc, char const *argv[])
+{
+  vector<int> ages{3,4,1,2};
+  priority_queue<student,vector<student>,cmp>a;
+  for(int age:ages){
+    student s(age);
+    q.push(s);
+  }
+  while(q.size()>0){
+      student head = q.top();q.pop();
+      cout<<head.age<<endl;
+  }
+}
+
+

位运算符技巧

+

按位与运算符(&)

+

参加运算的两个数据,按二进制位进行“与”运算

+

运算规则:0&0=0;0&1=0;1&0=0;1&1=1;

+

即:两位同时为1,结果才为1,否则为0

+

例如:3&5 即 0000 0011 & 0000 0101 = 0000 0001 因此,3&5=1

+

”与运算“的特殊用途

+
    +
  • 清零。如果想将一个单元清零,即全部二进制位归0,只要与一个各位都为0的数值相与,结果为0
  • +
  • 取一个数的指定二进制位。如果想取某个数的后四位,那么可以计算 该数&0000 1111
  • +
+

按位或运算符(|)

+

参加运算的两个数据,按二进制位进行“或”运算

+

运算规则:0|0=0;0|1=1;1|0=1;1|1=1;

+

即:参加运算的两个数据只要有一个位1,其值为1

+

例如:3|5 即 0000 0011 | 0000 0101 = 0000 0111, 因此,3|5的值为7

+

“或运算”特殊用途

+
    +
  • 常用来对一个数的某些位置置为1。如果想对某个数的后四位置为1,那么可以计算 该数 | 0000 1111
  • +
+

异或运算符(^)

+

参加运算的两个数据,按二进制位进行“异或”运算

+

运算规则:0 ^ 0=0;0 ^ 1=1;   1 ^ 0=1;   1 ^ 1=0;

+

即:参加运算的两个数据,如果两个相应位值不同,则该位位1,否则为0(模2和)

+

“异或运算”特殊用途

+
    +
  • 翻转一个数的指定位。如果将对某个数的后四位翻转, 那么可以计算 该数 ^ 0000 1111
  • +
  • 与0异或,保留原值
  • +
+

取反运算符(~)

+

参加运算的一个数据,按二进制位进行“取反”运算

+

运算规则:~1 = 0;~0 = 1;

+

即:对一个二进制数按位取反。

+

“取反运算”特殊用途

+
    +
  • 使一个数的最低位为0,可以计算 该数 & ~1
  • +
+

左移运算符(<<)

+

将一个数据的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)

+

例如:a = a << 2 将a的二进制位左移2位,右补0。若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2

+

右移运算符(>>)

+

将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。

+

例如: a = a >> 2 将a 二进制位右移2位,操作数每右移一位,相当于该数除以2

+

>> 运算符把 expression1 的所有位向右移 expression2 指定的位数。expression1 的符号位被用来填充右移后左边空出来的位。向右移出的位被丢弃。

+

例如-14(1111 0010)右移两位等于-4(11111100)

+

无符号右移运算符(>>>)

+

>>> 运算符把 expression1 的各个位向右移 expression2 指定的位数。右移后左边空出的位用零来填充。移出右边的位被丢弃。

+

例如 -14 (11111111 11111111 11111111 11110010),向右移两位后等于1073741820(00111111 11111111 11111111 11111100)

+

不同长度的数据进行位运算

+

如果两个不同长度的数据进行位运算时,系统会将二者按右端对齐,然后进行位运算

+

以“与”运算为例说明如下:我们知道在C语言中long型占4个字节,int型占2个字节,如果一个long型数据与一个int型数据进行“与”运算,右端对齐后,左边不足的位依下面三种情况补足

+
    +
  • 如果整形数据为正数,左边补16个0
  • +
  • 如果整形数据为负数,左边补16个1
  • +
  • 如果整形数据为无符号数,左边也补16个0
  • +
+

数组题技巧

+

考虑双指针,双向前缀和,单调队列

+

快排

+
void myquickSort(vector<int>& nums,int low,int high){
+    // mid不能是下标
+    int mid = nums[(low+high)/2];
+    int i = low,j = high;
+    do{
+        while(nums[i]<mid) i++;
+        while(nums[j]>mid) j--;
+        if(i<=j) swap(nums[i++],nums[j--]);
+    }while(i<=j);
+    if(low<j) myquickSort(nums,low,j);
+    if(i<high) myquickSort(nums,i,high);
+}
+
+

背包问题

+

常见的背包问题有三种

+

组合排列问题

+
    +
  • +
      +
    1. 组合总和 Ⅳ
    2. +
    +
  • +
  • +
      +
    1. 目标和
    2. +
    +
  • +
  • +
      +
    1. 零钱兑换 II
    2. +
    +
  • +
+

True、False问题

+
    +
  • +
      +
    1. 单词拆分
    2. +
    +
  • +
  • +
      +
    1. 分割等和子集
    2. +
    +
  • +
+

最大最小问题

+
    +
  • +
      +
    1. 一盒零
    2. +
    +
  • +
  • +
      +
    1. 零钱兑换
    2. +
    +
  • +
+

组合排列问题公式

+
dp[i] += dp[i-num]
+
+

True、False问题公式

+
dp[i] = dp[i] or dp[i-num]
+
+

最大最小问题公式

+
dp[i] = min(dp[i],dp[i-num]+1)或者dp[i] = max(dp[i],dp[i-num]+1)
+
+

以上三组公式是解决对应问题的核心公式

+

背包问题分析步骤

+
    +
  1. 分析是否为背包问题
  2. +
  3. 是以上三种背包问题中的哪一种
  4. +
  5. 是0-1背包问题还是完全背包问题。也就是题目给的nums数组中的元素是否可以重复使用
  6. +
  7. 如果是组合排列问题,组合问题外循环物品,内循环背包;排列相反
  8. +
+

背包问题特征

+

背包问题具有的特征:给定一个target,target可以是数字也可以是字符串,再给定一个数组nums,nums中装的可能是数组,也可能是字符串,问:能否使用nums中的元素做各种排列组合得到target

+

背包问题遍历

+
    +
  • +

    如果是0-1背包,即数组中的元素不可重复使用,nums放在外循环,target在内循环,且内循环倒序

    +
    for(int i=0;i<nums.size();i++){
    +  for(int j=target;j>=nums[i];j--){
    +    // dp公式
    +  }
    +}
    +
    +
  • +
  • +

    如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环,且内循环正序

    +
    for(int i=0;i<nums.size();i++){
    +  for(int j=nums[i];j<=target;j++){
    +    // dp公式
    +  }
    +}
    +
    +
  • +
  • +

    如果是排列问题,即需要考虑元素之间的顺序,target放在外循环,nums放在内循环

    +
    for(int i=0;i<=target;i++){
    +  for(int j=0;j<nums.size();j++){
    +    if(i>nums[j]){
    +      // dp公式
    +    }
    +  }
    +}
    +
    +
  • +
+

单调栈

+

通常是一维数组,要寻找任一元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/05/07/MIT-Missing-Semester\345\255\246\344\271\240\347\254\224\350\256\260/index.html" "b/2022/05/07/MIT-Missing-Semester\345\255\246\344\271\240\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000..3b00a59 --- /dev/null +++ "b/2022/05/07/MIT-Missing-Semester\345\255\246\344\271\240\347\254\224\350\256\260/index.html" @@ -0,0 +1,773 @@ + + + + + + + + + + + + + + + + + + + + MIT-Missing-Semester学习笔记 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ MIT-Missing-Semester学习笔记 +

+ + + + +
+
+ +

Course overview + the shell

+

图 19
+图 20

+

Shell Tools and Scripting

+

图 21
+图 22
+图 23

+

TLDR

+

TLDR可以代替man命令,查找命令说明书,TLDR可以给出详细的例子
+图 24

+

find

+

图 25
+图 26

+

ripgrep

+

可以代替grep查找文件内容符合条件的文件
+图 27

+

Ctrl+R 可以进入用子串查找历史命令的模式

+

xargs 命令,它可以使用标准输入中的内容作为参数。 例如 ls | xargs rm 会删除当前目录中的所有文件。

+

Vim

+

+

Data Wrangling

+

数据处理常用工具有grep

+

另外还有一些常用的数据处理工具

+
    +
  • sed,文件(输入流)处理工具
  • +
+

图 28

+
    +
  • sort,排序工具
  • +
+

图 29

+
    +
  • awk,作用于文件的多功能编程语言
  • +
+

图 30

+

Command-line Enviroment

+

Job Control(作业控制)

+

结束一个正在执行的程序可以按ctrl+c,实质是终端向程序发送了一个SIGINT的signal
+SIGINT可以在程序中捕获处理,因此ctrl+c也就是SIGINT信号有时候没用。

+

SIGQUIT信号可以结束程序并不会被捕获,按ctrl+\

+

SIGSTOP信号可以暂停程序到后台,按ctrl+z。命令行输入jobs查看所有程序运行状态,使用fg %程序id 或 bg %程序id 将程序从暂停状态转到前端执行或后端执行

+

尽管程序可以在后端执行,但是一旦终端关闭,作为子进程的程序会收到一个SIGHUP信号导致被挂起,可以使用nohup 程序 &来使得程序在后端执行并忽略SIGHUP信号

+

图 31

+

Terminal Multiplexers(终端复用器)

+

图 32

+

Aliases(别名)

+

图 33

+

图 34

+

直接在命令行定义别名,关闭终端后就没了,需要在.bashrc或者.zshrc这样的终端启动文件中做持久化

+

Dotfiles(点文件或者配置文件)

+

许多配置文件以’.‘开头,所以叫dotfile,默认情况下ls看不到dotfile
+图 35

+

配置文件最好用版本控制统一管理,然后将原来的配置文件路径软链接到版本控制下的配置文件路径,github上有许多dotfile仓库

+

Remote Machines(远程机器)

+

使用ssh可以登录远程终端
+图 36

+

foo是用户名,bar.mit.edu是域名,也可以直接是ip地址

+

也可以直接使用ssh执行命令,只要在上面的命令后面加上需要执行的命令即可

+

Version Control(Git)

+

+

Debugging and Profiling

+

Debugging

+

Printf debugging and Logging

+

“The most effective debugging tool is still careful thought, coupled with judiciously placed print statements” — Brian Kernighan, Unix for Beginners.
+在程序中使用logging而不用printf的好处在于

+
    +
  • 日志可以输出到文件、sockets、甚至是远程服务器而不一定是标准输出
  • +
  • 日志支持几种输出级别(例如INFO,DEBUG,WARN,ERROR)
  • +
  • 对于新出现的问题,日志有足够多的信息来排查
  • +
+

tips:输出可以按级别用不同颜色表示,例如ERROR用红色
+echo -e "\e[38;2;255;0;0mThis is red\e[0m"会打印红色的“This is red”到终端上

+

Third Party logs

+

许多第三方程序会将日志写到系统的某一处,一般是/var/log. NGINX服务器会将日志放在/var/log/nginx下. 许多linux系统使用systemd, 一个系统守护进程来控制许多事情例如某些服务的开启和运行, systemd将日志放在/var/log/journal下, 可以使用journalctl查看

+

图 37

+

Debuggers

+

+

Specialized Tools

+

Linux下可以使用strace查看程序系统调用情况
+图 38

+

tcpdump和Wireshark是网络包分析器
+web开发中,Chrome和Firefox的开发者工具十分方便

+

Profiling(分析)

+

学习分析和检测工具可以帮助理解程序中那一部分花费最多时间或资源以便优化这部分
+Timing
+三种不同的运行时间

+
    +
  • 真实时间:程序开始到结束的时间,包括阻塞时间、等待IO、网络
  • +
  • 用户态时间:CPU执行程序中用户态代码的时间
  • +
  • 系统态时间:CPU执行程序中核心态代码的时间
    +正确的程序执行时间=用户态时间+系统态时间
  • +
+

time命令可以测试程序的三种时间
+图 39

+

Metaprogramming

+

这节主要讲系统构建工具make、持续集成等

+

+

Security and Cryptography

+

Entropy(熵)

+

熵度量了不确定性并可以用于决定密码的强度
+熵的单位是比特,对于一个均匀分布的离散随机变量,熵等于log2(所有可能的个数,即n)log_2(所有可能的个数,即n)
+扔一次硬币的熵是1比特。掷一次六面骰子的熵大约为2.58比特。一般我们认为攻击者了解密码的模型(最小长度,最大长度,可能包含的字符种类等),但是不了解某个密码是如何选择的

+

https://xkcd.com/936/ 例子里面,“correcthorsebatterystaple”这个密码比“Tr0ub4dor&3”更安全,因为前者熵更大,大约40比特的熵足以对抗在线穷举攻击(受限于网络速度和应用认证机制);而对于离线穷举攻击(主要受限于计算速度),一般需要更强的密码(比如80比特)

+

Potpourri

+

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/05/07/Maven\345\255\246\344\271\240\347\254\224\350\256\260/index.html" "b/2022/05/07/Maven\345\255\246\344\271\240\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000..1b48f1e --- /dev/null +++ "b/2022/05/07/Maven\345\255\246\344\271\240\347\254\224\350\256\260/index.html" @@ -0,0 +1,700 @@ + + + + + + + + + + + + + + + + + + + + Maven学习笔记 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Maven学习笔记 +

+ + + + +
+
+ +

学习资料:Maven官方文档

+

最常用的两种打包方法

+
    +
  • clean,package(如果报错,很可能就是jar依赖的问题)
  • +
  • clean,install
  • +
+

Maven生命周期

+

Maven有三种内置的构建生命周期:default,clean,site。default生命周期处理项目部署,clean生命周期处理项目清理,site生命周期处理项目web站点的建立。每个生命周期包含一系列阶段

+

default生命周期阶段(非完整)

+

validate:验证,验证工程是否正确,所需的信息是否完整。

+

compile:编译源码,编译生成class文件,编译命令,只编译选定的目标,不管之前是否已经编译过,会在你的项目路径下生成一个target目录,在该目录中包含一个classes文件夹,里面全是生成的class文件及resources下的文件。

+

test:单元测试

+

package:打包,将工程文件打包为指定的格式,例如JAR,WAR等。这个命令会在你的项目路径下一个target目录,并且拥有compile命令的功能进行编译,同时会在target目录下生成项目的jar/war文件。如果a项目依赖于b项目,打包b项目时,只会打包到b项目下target下,编译a项目时就会报错,因为找不到所依赖的b项目,说明a项目在本地仓库是没有找到它所依赖的b项目,这时就用到install命令了

+

integration-test:将jar包部署到环境中进行单元测试

+

verify:核实,检查package是否有效、符合质量标准。

+

install:将包安装至本地仓库,以作为其它项目的dependency。该命令包含了package命令功能,不但会在项目路径下生成class文件和jar包,同时会在你的本地maven仓库生成jar文件,供其他项目使用(如果没有设置过maven本地仓库,一般在用户/.m2目录下。如果a项目依赖于b项目,那么install b项目时,会在本地仓库同时生成pom文件和jar文件,解决了上面打包package出错的问题)。

+

deploy:复制到远程仓库。

+

clean生命周期阶段(非完整)

+

clean:清理,在进行真正的构建之前进行一些清理工作,移除所有上一次构建生成的文件。执行该命令会删除项目路径下的target文件,但是不会删除本地的maven仓库已经安装的jar文件。

+

site生命周期阶段(非完整)

+

site:站点,生成项目的站点文档

+

图 15

+
+

Phases are actually mapped to underlying goals. The specific goals executed per phase is dependant upon the packaging type of the project. For example, package executes jar:jar if the project type is a JAR, and war:war if the project type is - you guessed it - a WAR.
+An interesting thing to note is that phases and goals may be executed in sequence.

+
+

mvn clean dependency:copy-dependencies package

+

这条命令执行了mvn的clean阶段,dependency插件的copy-dependencies目标,package阶段(package处于default生命周期,Maven会先顺序执行validate、compile、test)

+

阶段由插件目标构成

+

一个插件目标代表一个具体的任务(比阶段更细),它可以被绑定到0个或多个阶段。没有绑定到阶段也能直接执行,目标和阶段执行顺序取决于调用的顺序

+
+

For example, consider the command below. The clean and package arguments are build phases, while the dependency:copy-dependencies is a goal (of a plugin).

+
+

mvn clean dependency:copy-dependencies package

+

如果一个目标被绑定到一个或多个阶段,目标将在阶段中被调用。如果阶段没有绑定任何目标,那阶段就不会执行。如果阶段绑定一个或多个目标,执行这个阶段会执行所有目标,目标执行的顺序在POM中定义

+

build和compile的区别

+

点击Build Project,idea 使用自己的构建工具进行编译(编译器默认使用Javac),相当于maven的compile,点击Build Artifacts,相当于maven的Package
+图 15

+

插件和goal

+
+

a plugin is a collection of goals with a general common purpose. For example the jboss-maven-plugin, whose purpose is “deal with various jboss items”.

+
+

总结

+

mvn clean package

+

依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)等7个阶段。

+

mvn clean install

+

依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)、install等8个阶段。

+

mvn clean deploy

+

依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)、install、deploy等9个阶段。

+

注意:maven默认的生命周期中,不管是package还是install,都不会把第三方依赖打包到一个jar包中,需要自己配置插件,比如maven-assembly-plugin。若是springboot项目,有spring-boot-maven-plugin插件,执行mvn package后,先走一遍maven自身生命周期到package,然后springboot打包插件里面提供了一个repackage的goal,这样mvn package在插件的指导下先完成标准的打包流程再完成插件定义的流程

+

图 16

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/05/07/Maven\351\227\256\351\242\230\350\256\260\345\275\225/index.html" "b/2022/05/07/Maven\351\227\256\351\242\230\350\256\260\345\275\225/index.html" new file mode 100644 index 0000000..941afbd --- /dev/null +++ "b/2022/05/07/Maven\351\227\256\351\242\230\350\256\260\345\275\225/index.html" @@ -0,0 +1,666 @@ + + + + + + + + + + + + + + + + + + + + Maven问题记录 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Maven问题记录 +

+ + + + +
+
+ +
    +
  1. +

    多模块项目依赖错误
    +多模块项目直接对根模块进行install,Maven会根据依赖自动判断install顺序。install完成后需要点击Maven界面的Reload All Maven Projects
    +图 17

    +
  2. +
  3. +

    父模块的pom文件一定得写<packaging>pom</packaging>
    +因为默认打包方式是jar,<xs:element name="packaging" minOccurs="0" type="xs:string" default="jar">
    +图 1

    +
  4. +
  5. +

    有些公司内部仓库的包只有pom文件没有jar,这种是下载不下来的
    +图 1
    +只有jar包下载到了本地,maven才能找到依赖,所以这就是为什么多模块项目,被依赖的模块要先install到本地,这样依赖它的模块才能找到这个模块

    +
  6. +
+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/05/13/\345\237\272\344\272\216\345\214\272\345\235\227\351\223\276\347\232\204\345\256\211\345\205\250\347\224\265\345\255\220\351\200\211\344\270\276\347\263\273\347\273\237/index.html" "b/2022/05/13/\345\237\272\344\272\216\345\214\272\345\235\227\351\223\276\347\232\204\345\256\211\345\205\250\347\224\265\345\255\220\351\200\211\344\270\276\347\263\273\347\273\237/index.html" new file mode 100644 index 0000000..0183431 --- /dev/null +++ "b/2022/05/13/\345\237\272\344\272\216\345\214\272\345\235\227\351\223\276\347\232\204\345\256\211\345\205\250\347\224\265\345\255\220\351\200\211\344\270\276\347\263\273\347\273\237/index.html" @@ -0,0 +1,697 @@ + + + + + + + + + + + + + + + + + + + + 基于区块链的安全电子选举系统 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 基于区块链的安全电子选举系统 +

+ + + + +
+
+ +

背景:浙江大学软件学院实训课题,本组选择的课题是关于区块链和隐私计算的融合应用场景,具体方向是电子选举
+需求:投票人不暴露身份隐私的情况下完成投票,计票机构在选票合法的情况下完成计票

+
+

技术方案总结

+

图 1
+图 2

+

项目总体情况

+

图 4

+

核心功能性能测试

+

图 5

+

测试环境:x86_64,Intel® Xeon® Gold 6133 CPU @ 2.50GHz,Linux 5.4.0-96-generic
+测试结果:大规模选举时延较高,适用于小规模选举活动,但因为选举本身的非实时性,也可用于更大规模

+

亮点

+

在隐匿链上选票来自谁的情况下选民只需要通过零知识证明证明自己的合法选民身份。
+即使是对一个合法证明的微小改变,都无法通过链上以及链下的零知识证明验证,攻击者难以冒充合法选民。

+

缺陷

+
    +
  • 监管机构(后端)无法证明没有存储选民的隐私信息,例如该选民的选票号,私有盐。这里存在漏洞可以使得作恶的监管机构只需执行两次解密算法就可以获得选民的选票内容
  • +
  • 零知识证明以匿名的形式证明了选民身份,选民通过秘密ID和监管方产生的PK生成证明并公布。秘密输入应当是选民秘密持有的,因此生成证明的过程最好是在本地做。
    +当前选民需要通过可信的后端API生成证明,无法便捷地通过WASM在本地生成(原计划下libsnark难以迁移,且曲线计算迁移到wasm慢几十倍),因此使用起来较为不便。
  • +
+

系统展示

+

图 6
+图 7
+图 8
+图 9
+图 10
+图 11
+图 12
+图 13
+图 14
+图 15
+图 16

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/05/23/2022-5-23-\346\237\220\345\214\272\345\235\227\351\223\276\345\205\254\345\217\270\351\235\242\350\257\225\350\256\260\345\275\225/index.html" "b/2022/05/23/2022-5-23-\346\237\220\345\214\272\345\235\227\351\223\276\345\205\254\345\217\270\351\235\242\350\257\225\350\256\260\345\275\225/index.html" new file mode 100644 index 0000000..f8762ad --- /dev/null +++ "b/2022/05/23/2022-5-23-\346\237\220\345\214\272\345\235\227\351\223\276\345\205\254\345\217\270\351\235\242\350\257\225\350\256\260\345\275\225/index.html" @@ -0,0 +1,661 @@ + + + + + + + + + + + + + + + + + + + + 2022.05.23 某区块链公司面试记录 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 2022.05.23 某区块链公司面试记录 +

+ + + + +
+
+ +

投的后端开发实习生岗,一天面了两次技术面

+

一面

+
    +
  1. 针对简历项目提问,为什么要paillier?为什么要秘密分享?
  2. +
  3. mysql的主从备份,主从节点的事务需不需要分开执行?从节点事务执行失败,主节点如何回滚?binlog的同步和事务在主从节点的执行,两者按时间顺序如何排列?
  4. +
  5. 手写代码:求字典序第k大的数
  6. +
+

二面

+
    +
  1. 区块链共识算法有哪些?每个算法容错数量是多少?双花问题?默克尔树?默克尔证明?
  2. +
  3. Java基本数据类型和引用类型的区别
  4. +
  5. 为什么每个基本数据类型都有包装类,包装类有什么用?
  6. +
  7. 针对简历提问,秘密分享时间复杂度多少?paillier用在哪里?
  8. +
  9. 手写代码:多线程卖票,要求每个线程同时工作,不能超卖
  10. +
  11. 两个大文件,文件内容是字符串集合,内存略大于一个文件,如何对两个文件进行字符串去重?如何优化时间复杂度?
  12. +
+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/06/04/Cryptography\342\205\240\347\254\224\350\256\260/index.html" "b/2022/06/04/Cryptography\342\205\240\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000..5fb90f8 --- /dev/null +++ "b/2022/06/04/Cryptography\342\205\240\347\254\224\350\256\260/index.html" @@ -0,0 +1,1923 @@ + + + + + + + + + + + + + + + + + + + + CryptographyⅠ笔记 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ CryptographyⅠ笔记 +

+ + + + +
+
+ +

斯坦福教授Dan Boneh的密码学课程《Cryptography》
+课程链接:https://www.bilibili.com/video/BV1Ht411w7Re
+讲义:https://www.cs.virginia.edu/~evans/courses/crypto-notes.pdf

+

目标:课程结束后可以推导加密机制的安全性,可以破解不安全的加密机制

+

绪论

+

密码学用途

+
    +
  1. 安全通信,例如https,Bluetooth
  2. +
  3. 加密磁盘文件,例如EFS
  4. +
  5. 内容保护,例如DVD使用CSS
  6. +
  7. 用户认证
  8. +
+

高阶用途:

+
    +
  1. 外包计算(同态加密)
  2. +
  3. 安全多方计算
  4. +
  5. 零知识证明
  6. +
+

安全通信

+

在web服务中使用https协议进行通信,起到安全通信作用的是TLS协议,TLS协议主要包括两部分:

+
    +
  1. 握手协议:使用公钥密码体制建立共享密钥
  2. +
  3. Record层:使用共享密钥传输数据,保证数据的机密性和完整性
    +图 1
  4. +
+

加密磁盘文件

+

加密磁盘文件从哲学角度来看就是今天的Alice和明天的Alice进行安全通信
+图 2

+

对称加密

+

Alice和Bob双方共享密钥k,攻击者不知道密钥k,他们使用两个算法进行通信,分别是加密算法E,解密算法D。加密算法以原信息和密钥为输入产生相应密文;解密算法正好相反,以密文和密钥为输入,输出原信息。加密解密算法是公开的,只有密钥k是保密的。我们应当使用公开的算法,因为它的安全性经过业内人士的审查
+图 3

+

密钥使用次数

+

可以根据密钥使用次数分为一次使用的密钥,多次使用的密钥
+一次使用的密钥只用来加密一个信息,例如加密邮件,为每一封邮件都生成一个新的密钥。
+多次使用的密钥可以用来加密多个消息,例如用同一密钥加密文件系统的许多文件,多次使用的密钥需要更多机制来确保加密系统是安全的
+图 4

+

一些结论

+

任何依赖可信第三方的计算都可以不借助可信第三方完成
+图 5

+

密码算法提出三步骤

+
    +
  1. 明确描述威胁模型。例如数字签名算法中,攻击者如何攻击数字签名?伪造签名目的何在?
  2. +
  3. 提出算法构成
  4. +
  5. 证明在该威胁模型下,攻击者破解了算法等同于破解了根本性难题。意思就是算法的安全性由根本性难题保证,例如RSA依赖大整数质因数分解困难问题
  6. +
+

密码历史

+

对称密码历史

+

图 6

+
替换式密码
+

替换式密码的密钥是一张替换表
+图 1

+

例如:凯撒密码

+

凯撒密码是没有密钥或者只有一个固定密钥的替换式密码
+图 3

+

假设替换式密码的密钥只能使用26个英文字母,它的密钥空间有多大?是26!26!,约等于2882^{88},也就是说可以用88位比特表示密钥,这个密钥空间是足够大的,但是这个加密算法不安全
+图 4

+

如何破解这个加密算法?

+
    +
  1. 使用字母频率来破解。在英文文献中,出现频次最高的字母是e,那么我们统计密文中出现频次最高的那个字母,例如c,那么这个替换式密码的密钥k中很可能包含了e->c;同样的道理,出现频次第二高的是t,第三高的是a,分别找到密文中对应的字母进行还原
  2. +
  3. 使用字母配对(组合)。在英语中,最常见的二元配对有“he”,“an”,“in”,“th”。第一种方法还原出了eta,根据配对规则可以还原配对中的另一个字母。
  4. +
+

这种攻击方式叫唯密文攻击(CT only attack),替换式密码抵御不了这种攻击

+
Vigener密码
+

Vigener密码的密钥是一个单词,加密算法是将密钥重复直到和明文一样长,与明文逐位求和模26得到密文,解密算法是对密文逐位减去密钥
+图 5

+

如何破解Vigener密码?

+

首先假设攻击者已知密钥长度(未知也不影响破解,枚举长度即可),例如密钥长度为6,那么攻击者可以将密文按照每6个字母一组进行分组。然后看每组的第一个字母,他们都是由同一个字母加密的,例如上图中k的字母C。我们同样可以使用唯密文攻击,在每组的第一个字母中找到出现频次最高的字母,它们对应的明文位为E,然后对应的密钥为为密文位-E = 密钥位。同样的方法对密钥第2位直到最后一位执行。这种破解方法的实质是多次使用唯密文攻击
+图 6

+
轮轴机
+

最早的轮轴机:Hebern machine(一个轮轴)

+

本质上是一个替换式密码。按下A,假如A加密成了T,此时轮轴机转动一格,再次按下A,A就加密成了S。轮轴机一经提出很快就被破解了,同样是唯密文攻击。
+图 7

+

最著名的轮轴机:Enigma(3-5轮轴)
+图 8

+
数字时代的密码
+

1974:DES(keys = 2562^{56}, block size = 64 bits)

+

Today: AES(2001), Salsa20(2008) (and others)

+

离散概率

+

有限集合与概率分布

+

图 9

+

事件

+

事件是有限集合的子集

+

图 12

+

事件的并集发生的概率存在上界
+图 13

+

随机变量

+

随机变量是一个函数 X:U(有限集合)->V(某个集合)
+集合V是随机变量取值的地方。例如定义随机变量X:{0,1}n{0,1}X:\{0,1\}^n\longrightarrow \{0,1\};X(y)=lsb(y){0,1}X(y)=lsb(y) \in \{0,1\}
+这个随机变量将n位长度的字符串集合映射成了只有0和1的两个元素的集合,具体做法是取最低位。任意一个n位长度的字符串会被映射出0或者1
+图 14

+

均匀随机变量

+

图 15
+图 16

+

随机化算法

+

随机化算法相比于确定的算法,在原像中加入了随机因子,所以对同一条消息进行随机化,结果一般是不同的
+图 17

+

独立性

+

图 18

+

异或

+

异或就是逐位模2和
+图 20

+

异或在密码学中非常重要,在一个有限集合上有一个随机变量Y,有一个独立的均匀随机变量X,那么Z:=YXZ:=Y\bigoplus X 仍然是这个集合上的均匀随机变量
+图 21

+

生日悖论

+

图 22

+

r指的是随机变量的值

+

独立同分布举例:假设U = {00,01,10,11}表示抛硬币两次的结果,令随机变量XU{0,1}X:U\rightarrow\{0,1\}表示第一次抛硬币的结果,YU{0,1}Y:U\rightarrow\{0,1\}表示第二次抛硬币的结果。X和Y是相互独立的且概率一样,所以这两个随机变量是独立同分布的

+

流密码

+

流密码是一种对称密码。
+对称密码的严格定义:定义在K,M,C\mathcal{K,M,C}上的一对“有效”加解密算法(E,D)(E,D)
+K\mathcal{K}指密钥空间,M\mathcal{M}指明文空间,E\mathcal{E}指密文空间
+E:K×MCE: \mathcal{K\times M\rightarrow C}
+D:K×CMD:\mathcal{K\times C\rightarrow M}
+即 mM,kK:D(k,E(k,m))=m\forall m \in \mathcal{M}, k \in \mathcal{K}: D(k, E(k, m))=m
+"有效"这个词在理论密码学家看来,时间复杂度是多项式时间内的就是有效的,真正运行时间取决于输入规模。在应用密码学家看来,可能是加解密1GB数据要在10s内就算有效。
+图 23
+特别注意,加密算法是随机化算法,解密算法是确定性的算法

+

一次性密码本

+

一次性密码本的明文空间、密文空间、密钥空间均相同
+图 24
+一次性密码本的加密算法是将明文与密钥做异或运算,解密也是将密文与密钥做异或运算,根据异或运算的结合律可以知道加解密算法是正确的
+图 25

+

如何证明这个密码的安全性?

+

信息论的创始人香农提出一个观点:不能从密文得出关于明文的任何信息
+图 26
+攻击者截获一段密文,如果满足下面的等式,即不同明文经过同一个密钥加密后等于这个密文的概率是相同的,那么攻击者就无法得知真正的明文,这种密码是完美保密性的
+图 28

+

一次性密码本是完美保密性的
+图 29
+完美保密性只是意味着没有唯密文攻击,并不意味着在实际使用中是安全的
+图 30

+

香农在给出完美保密性的证明后又给出了一个定理,想要完美保密性,密钥长度要大于等于明文长度,所以完美保密性的密码是不实用的
+图 31

+

流密码(Stream ciphers)

+

流密码是使用的一次性密码本,它的思想是使用伪随机密钥代替随机密钥

+

伪随机数生成器是一个函数,它可以将s位的01比特串扩展成n位的01比特串,n>>s。注意,伪随机数生成器算法是不具备随机性的,具备随机性的是种子
+图 32

+

流密码使用伪随机数生成器(PRG),将密钥当做种子,生成真正用于加密的比特串
+图 33
+因为密钥长度远小于明文长度,所以流密码并不是完美保密性的
+图 34
+流密码的安全性不同于完美保密性,它需要另外一种安全性定义,这种安全性依赖具体的伪随机数生成器
+图 36
+安全的伪随机数生成器必须是不可预测的。
+假设伪随机数生成器是可预测的,那么存在某种算法可以根据PRG的前i位推测出后面的n-i位。在SMTP协议中,明文头是“from:” , 攻击者可以用这段已知的明文与截获的密文做异或得出PRG输出的前i位,再根据预测算法得出完整的密钥,再与密文异或得出明文
+图 37

+

严格定义PRG是可预测的:
+存在“有效”算法A,对PRG的前i位做计算,输出的值与PRG的第i+1位相同的概率大于1/2+ϵ,(ϵ1/230)1/2+\epsilon, (\epsilon \ge 1/2^{30}), ϵ\epsilon大于1/2301/2^{30}时是不可忽略的
+图 38
+图 39

+

不安全的PRG例子

+
    +
  • 线性同余法的PRG
    +图 40
  • +
+

可忽略和不可忽略
+图 42
+图 43

+

针对一次性密码本和流密码的攻击

+

两次密码本攻击

+

流密码的密钥一旦使用两次就不安全
+图 44
+图 45
+图 46

+

WEP还有一个问题就是它的PRG使用的是RC4,它的不可预测性是较弱的,存在有效算法可以从四万帧中恢复PRG的输出
+图 1
+图 2

+

完整性攻击

+

一次性密码本和流密码都只保护数据的机密性,但不保证完整性,攻击者可以修改将密文与攻击者的置换密钥做异或从而定向影响明文,这种攻击无法被检测出来
+图 3
+图 4

+

流密码实际应用

+

图 5
+图 6

+

现代流密码:eStream

+

eStream支持5种PRG,这里讲其中一种,这种PRG的输入除了种子(密钥)还有一个随机数,好处是不用每次更换密钥,因为输入还包括随机数,整体是唯一的
+图 7

+
eStream中同时支持软硬件的流密码Salsa20
+

图中的 || 并不是简单的拼接。这个PRG首先构造一个64kb的字符串,里面包含k,r,i(从0开始),通过多次一一映射h,最后与原字符串进行加法(不是异或)得出输出
+图 8
+图 9

+

PRG的安全定义

+

PRG的输出与均匀随机变量的输出不可区分
+图 10

+

统计测试

+

为了定义不可区分,首先引入统计测试,统计测试是一个算法,输入值是“随机数”,输出0表示不是真随机数,1表示是真随机数
+图 11

+

上图的例子1,这个统计测试输出1的情况当且仅当随机数中0和1的个数差小于等于随机数的长度开根号乘以10

+

统计测试算法有好有坏,可能并不随机的字符串也认为是随机的。所以我们需要评估统计测试算法的好坏

+

我们定义一个变量叫做优势,优势是统计测试算法相对于伪随机数生成器的,优势越接近1,统计测试算法越能区分伪随机数和真随机数

+

图 13
+图 14

+

PRG的密码学安全定义

+

PRG是安全的当且仅当不存在有效的统计算法,它的优势是不可忽略的。即所有统计算法都认为PRG的输出是真随机数

+

但是,我们不能构造一个PRG并证明PRG是安全的,即不存在有效的统计算法。但是我们还是有大量的PRG候选方案

+

图 15

+

我们可以证明当PRG是可预测时,PRG是不安全的
+图 16
+当存在一个好的预测算法和好的统计测试算法,如下图。那么统计测试算法可以以一个不可忽略的 ϵ\epsilon 分辨出伪随机数和真随机数
+图 17
+姚期智证明了上面命题的逆命题也成立,即当PRG是不可预测时,PRG是安全的
+图 18

+

不可区分的更通用的定义如下
+图 19

+

语义安全

+

什么是安全的密码?

+
    +
  1. 攻击者不能从密文中恢复密钥
  2. +
  3. 攻击者不能从密文中恢复明文
  4. +
+

香农认为不能从密文中获得任何关于明文的信息才是安全的密码

+

图 20

+

香农的完美保密性定义约束太强,可以用计算不可区分代替
+图 21

+

一次性密码本的语义安全

+

定义语义安全的的方式是通过两个实验,攻击者发送两个明文信息,挑战者(应该是被挑战者)随机选取密钥,做两次实验,第一次实验加密第一个信息,第二次实验加密第二个信息,攻击者判断密文对应的明文是哪个
+图 22
+上图定义了语义安全的优势,等于实验0中攻击者输出1的概率和实验1中输出1的概率之差的绝对值。简单理解一下,假如攻击者不能区分两次实验,那么实验0和实验1的输出1的概率是一样的,那么攻击者的优势为0,不能区分两次实验,意味着加密算法是语义安全的;如果攻击者能区分实验0和实验1,那么概率差不可忽略,攻击者有一定的优势区分两次实验
+图 23
+图 24

+

事实上,一次性密码本不仅是语义安全的,而且是完美保密性的
+图 25

+

安全的PRG可以构成语义安全的流密码

+

图 26

+

我们做两次实验证明流密码的语义安全,第一次使用伪随机数生成器,第二次使用真随机数
+图 27
+图 28
+图 29

+

分组密码

+

分组密码也属于对称密码,将明文分解成固定大小的分组,使用密钥加密成同样大小的密文
+图 30
+分组密码的加密过程是将密钥扩展成多个,使用轮函数多次计算分组得出密文
+图 31
+图 32

+

PRPs和PRFs

+

K表示密钥空间,X表示明文空间,Y表示密文空间
+给定密钥后,伪随机置换的加密函数是一个一一映射,也就意味着存在解密函数。伪随机置换和分组密码十分相似,有时候会混用术语
+图 33
+图 34

+

安全的PRFs

+

Funs[X,Y]表示所有从明文空间到密文空间的真随机函数的集合,易知这个集合大小等于明文空间大小的密文空间大小次,而伪随机函数的集合大小由密钥决定,一个密钥决定了一个伪随机函数,一个安全的PRF与真随机函数不可区分
+图 35
+图 36
+图 37
+上图的G问题在于x=0时,输出固定了,攻击者可以通过x=0时的输出是否为0来判断他在和真随机函数交互还是伪随机函数,因为x=0,输出为0的概率实在太低了,等于密文空间的倒数
+图 38
+可以使用安全的PRF来构造安全的PRG

+

DES

+

DES的轮(回合)函数使用的是Feistel网络,核心思想是每个分组2n bits,右边n bits原封不动变成下一层分组左边n bits,左边n bits经过伪随机函数转换再和右边n bits异或变成下一层右边n bits
+图 39
+易知这个网络是可逆的,注意,不要求伪随机函数是可逆的
+图 40
+图 41

+

定理:如果伪随机函数使用的密钥是相互独立的,那么Feistel网络是一个安全的PRP
+图 42
+回合函数f由F根据回合密钥推导出来,回合密钥由主密钥推导得来,IP和IP逆是伪随机置换,和DES安全性无关,仅仅是标准要求
+图 43
+图 44
+图 45

+

线性函数:函数可以表示成矩阵乘以入参
+图 46
+如果所有的置换盒子都是线性的,那么整个DES就是线性的,因为只有DES算法中只有置换盒子可能是非线性的,其它就是异或、位移等线性运算。如果是线性DES,那么存在一个矩阵B,DES可以写成B乘以一个包含明文和回合密钥的向量
+图 47
+图 48

+

针对DES的攻击

+
密钥穷举攻击
+

给定一些明文密文对,找到一个密钥使得明文密文配对,这个密文极大概率是唯一的
+图 49
+图 50

+

为了抵抗穷举攻击,衍生出了3DES
+图 51

+

2DES存在安全性问题
+图 52

+

针对分组密码的攻击

+

旁道攻击

+

通过测试加解密的时间或者功耗来推测密钥

+

错误攻击

+

通过外部手段影响加解密硬件,比如提高时钟频率、加热芯片,使得加密的最后一回合发生错误,根据错误信息可以推测出密钥
+这两种攻击需要先窃取到硬件,比如上图的IC卡

+

图 53

+

线性和差分攻击

+

同样是需要给定明文密文对,推测密钥,比穷举攻击效率更高

+

加密算法中使用了线性函数导致下面等式以一个不可忽略的概率成立
+图 54
+图 55
+图中的MAJ表示majority
+图 56

+

量子攻击

+

图 58
+图 59

+

AES

+

AES基于代换置换网络构建,和Feistel最大的区别在于,在这个网络的每一回合函数会影响每一位bit
+图 60
+图 61
+图 62
+图 63
+图 64
+图 65
+图 66

+

使用PRGs构造分组密码

+

分组密码实质是PRP,首先考虑能不能使用PRG构造PRF
+图 67
+图 68
+图 69
+图 70
+图 71
+使用安全的PRG可以构造一个安全的PRF,但是并不实用
+图 72
+有了安全的PRF,我们可以使用Luby-Rackoff定理转换成PRP,因此可以使用PRG来构造分组密码,但是不如AES启发性PRF实用

+

使用分组密码

+

图 1
+图 2
+很显然,真随机置换空间和伪随机置换空间大小一样,所以它是安全的PRP
+图 3
+这个PRP不是安全的PRF,因为明文空间太小了
+图 4

+

使用一次性密钥的分组密码

+

图 6
+图 7

+

ECB的问题在于相同的明文会加密成相同的密文,攻击者可能不知道明文内容,但也会从中学到明文的一些信息
+图 8
+攻击者来挑战算法,本来不应该知道两张加密图片的区别,但是ECB将头发加密成了很多1,头发又重复出现,这样密文就会出现很多1,攻击者就能根据这个区别分辨两张图片

+

图 9

+

ECB被用来加密长于一个分组的消息时不是语义安全的
+图 1

+

安全的电子密码本是为每个分组生成一个随机密钥进行加密,类似于AES的密钥扩展
+图 3
+图 4

+

使用密钥多次利用的分组密码

+

图 5

+

密钥多次利用的分组密码的选择明文攻击就是进行多次的语义安全实验(CPA安全)
+图 6
+图 7

+

确定的加密对于选择明文攻击不可能是语义安全的,所以多次使用一个密钥加密时,相同的明文,应该产生不同的输出,有两种方法。

+

第一种:随机化算法
+图 8
+图 9

+

第二种:基于新鲜值的加密

+

新鲜值不必随机但是不能重复
+图 10
+随机化的新鲜值和上面的随机化算法是一样的

+

基于新鲜值的加密的选择明文攻击下的安全性
+图 1
+图 2

+

密钥多次利用的运行方式(CBC)

+

CBC:密码分组链接模式
+图 3
+图 4
+图 5
+L是加密的明文长度,单位是分组。q是在CPA攻击下,攻击者获得的密文数,现实意义是使用某个密钥加密的明文数量
+图 6
+图 8
+图 9
+图 10
+图 11

+

密钥多次利用的运行方式(CTR)

+

CTR:计数器模式

+

CTR不使用分组密码,使用PRF足够
+图 12
+图 13
+图 14
+图 15
+图 16
+图 17

+

信息完整性

+

本节先考虑信息完整性,不考虑机密性。信息完整可以保证公开信息没有被篡改,例如广告投放商不在乎广告的机密性,但在乎广告有没有被篡改

+

MACs

+

图 9

+

图 10

+

CRC是循环冗余校验算法,为检测信息中的随机发生的错误而设计,并非针对恶意错误

+

安全的MACs

+

图 11
+图 12
+图 13

+

图 15

+

图 16

+

MAC可以帮助抵御数据篡改,但是无法抵御认证消息的交换

+

构造安全的MAC

+

可以用安全的PRF来构造安全的MAC
+图 17

+

但是,PRF的输出不能太短,不然攻击者能以一个不可忽略的概率猜出消息认证码

+

图 18

+

图 19

+

不等式右边 1/|Y| 是因为攻击者可以猜

+

图 20

+

PRF的输出不能太短,同时为了增大输入空间,需要有一些新的构造,将小输入空间的PRF转换成大输入空间的PRF
+图 21

+

图 22

+

如果PRF是安全的,那么截断PRF的输出,依然是安全的,当然,如果要用PRF构造MAC,不能截到太短

+

CBC-MAC和NMAC

+

图 23

+

n是底层PRF的分组大小

+

图 24

+

L是分组大小

+

图 25

+

这两种构造的最后一步都至关重要,没有最后一步,攻击者可以实施存在性伪造(扩展攻击)。
+第二种构造的原因很简单,攻击者询问m的函数结果,将结果与w分别作为函数的密钥和明文(F公开),计算函数结果

+

图 26
+图 27

+

第一种构造的原因
+图 29

+

图 28

+

图 30
+图 31
+图 32
+图 33
+图 34

+

MAC padding

+

当数据的长度不是分组长度的倍数时,需要填充数据

+

全部补0会有出现存在性伪造
+图 35

+

填充函数必须是一一映射的
+图 36

+

ISO的这种填充方法,不管是不是分组长度的倍数都要进行填充

+

图 37

+

并行的MAC

+

CBC-MAC和NMAC将一个处理短信息的PRF转换成一个处理长信息的PRF,这两种算法是串行的

+

P是某个有限域上的乘法
+图 38
+图 39
+图 40

+

一次性MAC

+

图 41
+图 42
+图 43
+图 44

+

HMAC(略)

+

抗碰撞章节细说

+

抗碰撞

+

抗碰撞在信息完整性中扮演着重要角色。我们说MAC系统是安全的,如果它在选择信息攻击下,是不可被存在性伪造的。前面4种MAC构造是通过PRF或随机数来构造的,现在通过抗碰撞的哈希函数来构造MAC
+图 45

+

抗碰撞:没有有效算法A,能以一个不可忽略的概率找到hash函数的碰撞值

+

图 46

+

可以用抗碰撞哈希函数和处理短信息的安全MAC组合成处理长信息的安全MAC
+图 47
+图 48

+

只用抗碰撞哈希函数也可以构造安全的MAC,而且不像之前的MAC需要密钥,但是需要一个只读空间用来存信息的hash值。这种方式非常流行
+图 49

+

针对抗碰撞哈希函数的通用攻击(生日攻击)

+

针对分组密码的通用攻击是穷举攻击,抵御穷举攻击的方法是增大密钥空间;为了抵御生日攻击,哈希函数的输出也必须大于某个下界

+

下面这种攻击算法通常几轮就能找到碰撞值
+图 50

+

生日悖论的证明

+

下面的证明在非均匀分布时,n的下界更低
+图 51

+

通用攻击

+

图 52
+图 53

+

使用Merkle-Damgard机制组建抗碰撞的哈希函数

+

图 54

+

下面的IV是永远固定的,写在代码和标准里的值,填充函数在信息长度是分组长度倍数的时候也会填充一个哑分组
+图 55

+

这种机制流行的原因是只要小hash函数(即上面的压缩函数)是抗碰撞的,那么大hash函数也是抗碰撞的

+

构建抗碰撞的压缩函数

+

使用分组密码来构建压缩函数,将信息作为密钥。SHA函数都使用了Davies-Mayer压缩函数
+图 56
+图 57
+图 58

+

另外一类压缩函数是由数论里的困难问题构建的,这类压缩函数的抗碰撞性规约于数论难题,也就是说破解了压缩函数的抗碰撞性也就破解了数论难题。但是这类压缩函数很少使用,因为分组密码更快
+图 60

+

SHA256

+

使用了Merkle-Damgard机制和Davies-Mayer压缩函数,底层分组密码用的是SHACAL-2
+图 59

+

HMAC

+

图 61
+图 62
+图 63
+图 64
+图 65

+

针对MAC验证时的计时攻击

+

图 66
+图 67
+图 68
+图 69

+

认证加密

+

到目前为止,我们了解了机密性和信息完整性, 我们将构建同时满足这两种性质的密码
+图 70

+

机密性: 在选择明文攻击下满足语义安全
+机密性只能抵抗攻击者窃听而不能抵抗篡改数据包

+

客户端加密数据后通过网络发送给服务器, 服务器解密后分发至对应端口
+图 71

+

如果没有保证完整性, 那么攻击者可以拦截客户端的数据包, 把端口改成自己能控制的服务器的端口
+图 72

+

可以抵抗CPA攻击的CBC分组密码不能抵抗篡改数据包, 攻击者可以简单地修改IV从而修改加密后的端口
+图 73

+

攻击者甚至不用进入服务器, 直接利用网络来攻击。在CTR模式下,攻击者截获数据包,将加密的校验和与t异或,将加密的数据与s异或。CTR模式的特点是对密文异或,解密后等于对明文异或。攻击者重复很多次攻击,直到得到足够数量的合法的t、s对,从而恢复数据D(插值法?)
+图 1

+

这种攻击叫做选择密文攻击。攻击者提交他选择的密文,是由他想解密的密文所推出的,然后看服务器响应,攻击者可以从中学到明文的一些信息。重复这个操作,用许多不同t、s值,攻击者可以还原明文

+

选择明文攻击下的安全不能保证主动攻击(前面两种攻击)下的安全
+如果要保证信息完整性但不需要机密性,使用MAC
+如果同时保证信息完整性和机密性,使用认证加密模式
+图 2

+

认证加密定义

+

目标:提供选择明文攻击下的语义安全和密文完整性
+图 3

+

密文完整性:攻击者不能造出合法的密文
+图 4

+

CBC是选择明文攻击(CPA)下的安全密码,它的解密算法从不输出bottom符号,所以它不能直接被用作认证加密
+图 5

+

认证加密不能抵抗重放攻击
+图 6
+图 7

+

图 8

+

选择密文攻击下的安全

+

攻击者的能力是既能选择明文攻击也能选择密文攻击,也就是说既能拿到想要的明文的加密结果,也能拿到想要的密文的解密结果
+图 9

+

攻击者选择的密文不能是CPA的返回
+图 10

+

CBC密码不是选择密文安全的,因为在选择密文时,通过对IV异或修改CPA的返回,同时由于CBC的特性,CCA的返回时对明文的异或,这样攻击者可以以1的优势赢下语义安全实验
+图 11

+

如果密码能够提供认证加密,那么它就是选择密文安全的
+图 12
+图 13

+

所以认证加密能够选择密文攻击,但是不能防止重放攻击和旁道攻击
+图 1

+

认证加密直到2000年才被正式提出,在这之前,已经有CPA安全的密码和安全的MAC,当时的工程师想将两者组合,但不是所有的组合可以作为认证加密
+图 1

+

图 2

+

图 3

+

SSH的MAC签名算法的输出会泄漏明文中的一些位;
+SSL的加密和MAC算法之间会有一些不好的互动导致选择密文攻击。IPsec无论怎么组合CPA安全的密码和安全的MAC都可以作为认证加密。SSL的特点是MAC-than-ENC,IPsec的特点是ENC-than-MAC
+图 4

+

这几种认证加密都支持AEAD(认证加密与关联数据,例如ip报文的报文头是关联数据不加密,报文体用加密,整个报文需要认证)。MAC对整个报文使用,加密只对报文体使用

+

图 5

+

aad是需要认证、但不需要加密的相关数据,data是需要认证和加密的数据
+图 6

+

图 7

+

直接从PRP构造认证加密

+

图 8

+

OCB比前面几种认证加密快的多,但没有被广泛使用因为各种各样的专利
+图 10

+

认证加密的例子

+

图 11
+图 12
+图 13
+图 15

+

图 16
+图 17

+

针对认证加密的攻击

+

图 18
+图 19
+图 20
+图 21
+图 22
+图 23
+图 24
+图 25
+图 26
+图 27

+

图 28
+图 29
+图 30

+

零碎

+

本章讲对称密码的一些零碎

+

密钥推导

+

KDF:key drivation function
+源密钥由硬件随机数生成器生成或者密钥交换协议生成
+图 31

+

CTX:参数上下文,每个进程的ctx不同
+当源密钥服从均匀分布,我们使用PRF来作为密钥推导函数(其实就是用PRF作为伪随机数生成器)
+图 32
+图 33

+

当源密钥不服从均匀分布,那么伪随机函数的输出看起来就不随机了,源密钥不服从均匀分布的原因可能是密钥交换协议的密钥空间的子集是均匀分布,或者伪随机数生成器有偏差
+图 34

+

构建KDF的机制

+

先提取再扩展

+

因为源密钥可能不是均匀的,我们使用一个提取器和一个随机选择的固定的但可以不保密的盐值将源密钥转换为服从均匀分布的密钥k,然后再使用PRF扩展密钥
+图 35

+

HMAC既用于PRF进行扩展,又用于提取器
+图 36

+

基于密码的KDF

+

PBKDF通过多次迭代哈希函数推导密钥
+图 37

+

确定性加密

+

确定性加密总是把给定明文映射到同一个密文。

+

为什么需要确定性加密?假设有个加密数据库和服务器,服务器用k1加密索引,用k2加密数据,如果加密是确定的,服务器请求数据时可以直接使用加密后的索引作为查询条件请求数据
+图 38

+

确定性加密有致命缺点就是不能抵御选择明文攻击,攻击者看到相同的密文就知道他们的明文是相同的
+图 39
+图 40

+

解决方法是不要用同一个密钥加密同一个消息两次,要么密钥从一个很大空间随机选择,要么明文就是唯一的,比如说用户id
+图 41

+

确定性加密的CPA安全

+

在标准的选择明文攻击实验基础上,Chal不会给相同的m0加密,不会给相同的m1加密。注意上面的图40不是标准的确定性加密的CPA实验,因为攻击者两次查询的m0都是同一个
+图 42

+

一个常见的错误是,固定IV的CBC不是确定性CPA安全的,0n1n0^n1^n表示消息有两个分组,第一个分组全0,第二个分组全1
+图 1

+

固定IV的CTR也是不安全的
+图 2

+

可以抵御确定性CPA的确定性加密

+

确定性加密是需要的,但是不能抵御选择明文攻击,因为攻击者看到两个相同的密文就知道了对应的明文是一样的。我们对确定性加密降低选择明文攻击的能力,加密者不使用一个密钥多次加密同样的明文,这样叫确定性CPA。
+图 3

+
构造1:合成的IV(SIV)
+

CPA安全的密码会有一个随机值,我们用PRF生成这个随机值
+图 4

+

SIV天然提供密文完整性,不需要MAC就能作为DAE(确定性认证加密),例如SIV-CTR
+图 5
+图 6

+

当需要确定性加密,特别是明文很长时,适合用SIV,如果明文很短,比如说少于16个字节,可以用构造2

+
构造2:仅仅使用一个PRP
+

实验0,攻击者看到q个随机值,实验1中,攻击者也看到q个随机值,两次实验的结果的概率分布是一样的,攻击者无法区分。这种构造不能保证密文完整性。同时只能加密16个字节
+图 7

+

我们先考虑如何将PRP扩展成一个大的PRP
+EME有两个密钥K,L,L是由K推出的。先为每个分组用L推导出一个密码本作异或,然后用PRP加密得到PPP,将所有PPP异或得到MP,再用PRP加密MP得到MC。然后计算MP异或MC,得到另外一个密钥M用于推导更多密码本,分别对PPP异或得到CCC,然后把所有这些CCC异或得到CCCO,再用PRP加密再异或密码本
+图 1

+

现在考虑增加完整性
+图 2
+图 3

+

微调加密(Tweakable encryption)

+

先以硬盘加密问题引入微调加密,
+硬盘扇区大小是固定的,明文和密文空间必须一致,我们最多可以使用确定性加密,因为随机性加密需要额外空间来放随机数,完整性需要额外空间放认证码
+定理:如果确定性CPA安全的密码的明文空间和密文空间一样,那么这个密码一定是个PRP
+图 4
+图 5
+图 6
+图 7

+

这个微调分组密码的安全实验与常规的分组密码安全实验区别在于,在常规分组密码中,攻击者只能与一个置换进行互动,目标是分辨自己在和伪随机置换交互还是在和一个真随机置换交互。而在微调分组密码的安全实验中,攻击者与|T|个随机置换交互,目标是区分这|T|个随机置换是真是伪
+图 8
+图 1
+图 2
+图 3
+图 4
+图 5

+

保格式加密(Format Preserving encryption)

+

pos机刷卡时,我们希望卡号只在终端和银行可见,但是中间又有些服务商也想得到"卡号",我们可以用将卡号加密成卡号格式的密文。
+图 6
+图 7

+

我们截断使用PRF,明文后面补0(例如AES就补到128位),密文截断,然后带入Luby-Rackoff构造PRP
+图 9
+图 10
+图 11

+

密钥交换

+

现在我们知道两个用户可以通过共享一个密钥来保护通信数据,问题是,这两个用户如何产生共享密钥,这个问题将把我们带入公钥密码的世界。我们先看一些玩具性质的密钥交换协议。
+图 12
+图 13
+图 14
+图 17
+图 18

+

能否设计出可以抵御窃听和主动攻击的没有可信第三方的密钥交换协议?可以的,这就是公钥密钥的出发点
+图 19

+

不需要TTP的密钥交换

+

首先考虑攻击者只能窃听不能篡改消息,能不能只使用对称密码体系的算法来实现不需要TTP的密钥交换?
+图 20

+

可以的,首先给出puzzle定义:需要花一些功夫解决的问题。例如已经给出AES密钥的前96位,明文固定,那么枚举2322^{32}个可能的后32位密钥可以找到能正确解密
+图 21

+

Alice准备2322^{32}个puzzle,全部发给Bob,Bob选择一个然后开始枚举,只要解密出的原文开头包含"Puzzle", 对应的k作为共享密钥,x发送给Alice,Alice就知道Bob选择了哪个
+图 22

+

这个协议不实用但是有一个很好的想法,参与者花费线性的时间,而攻击者必须花费平方的时间,当攻击者想破解这个协议,有一个“平方鸿沟”横亘在参与者与攻击者的工作之间。
+只用对称密码体系,我们不能建立一个更大的“鸿沟”
+图 23

+

我们需要具备非常特殊性质的函数,为了构建这些函数,我们必须依赖某些代数

+

Diffie-Hellman协议

+

这是第一个实用的密钥交换机制。
+同样,我们考虑攻击者只能窃听不能篡改。我们尝试建立参与者与攻击者之间的指数级鸿沟

+

Diffie-Hellman协议开创了密码学的新纪元,现在不仅仅是关于开发分组密码,而且是关于设计基于代数的协议
+图 24
+图 25

+

下面有张表是不同密钥长度的分组密码安全性等价于对应模数的DH函数安全性,如果用椭圆曲线,模数可以更小
+图 26
+图 1

+

上面的协议当存在主动攻击(中间人攻击)时就不安全
+图 2

+

事实上,上面的协议也可以改成无交互的
+图 3

+

一个开放的问题,两个参与方的密钥交换使用DH即可,三个参与方的密钥交换使用Joux提出的方法,四个及以上还没有有效方法
+图 4

+

公钥加密

+

这是另外一种密钥交换的方法
+图 5
+图 6
+图 7

+

在公钥加密中,没有必要赋予攻击者实施选择明文攻击的能力。因为在对称密钥系统中,攻击者必须请求他选择的明文的加密,而在公钥系统中,攻击者拥有公钥,所以他可以自己加密任何他想加密的明文,他不需要Chal的帮助来计算他选择的明文的加密。因此在公钥的设定中,选择明文攻击是与生俱来的,没有理由给攻击者多余的能力去实施选择明文攻击(公钥加密也是随机性的,每次密文不同)
+图 8
+图 9

+

可以抵抗窃听但也不能抵抗中间人攻击
+图 10
+图 11

+

图 12

+

数论简介

+

图 13
+图 14
+图 15
+图 16
+图 17
+图 19
+图 20

+

扩展欧几里得算法是已知最有效的求元素模逆的方法(也给了我们求模线性方程的方法)
+图 21

+

注意这里是n,不是N,n=logN
+图 22

+

费马小定理和欧拉定理

+

费马小定理给了我们另一个计算模质数逆的方法,但是与扩展欧几里得算法有两个不足,首先它只能用在质数模上,其次算法效率更低
+图 24

+

我们可以用费马小定理以极大概率生成一个随机质数(期望是几百次迭代),这是一个简单但不是最好的方法
+图 25

+

欧拉证明了ZpZ_p^*是一个循环群(p是素数),也就是g(Zp)\exist g\in (Z_p)^* 使得 $ {1,g,g2,g3,…,g{p-2}}=(Z_p)*$
+图 26

+

有限子群的阶必然整除有限群的阶
+图 27

+

欧拉定理,费马小定理的直接推广,适用于合数
+图 28

+

模高次方程

+

图 29
+图 30
+图 31

+

0也是二次剩余,所以有(p-1)/2+1
+图 32
+图 33

+

《信息安全数学基础》P146,证明当p是形如4k+3的素数时,解的形式如下。这里Dan讲了p不是这种形式的素数时仍然是有有效的随机算法来求解
+图 34
+图 35

+

当模数是合数时且指数大于1时,同余式的解并不好找
+图 36

+

一些算法

+

分组用32位表示是为了乘法不溢出
+图 37
+图 38
+图 39
+图 40

+

指数运算非常慢
+图 41

+

模运算的一些难题

+

图 42

+

质数模的难题

+

图 43
+图 44
+图 45
+图 46

+

合数模的难题

+

Z(2)(n)Z_{(2)}(n)表示两个位数相同的质数乘积的集合
+图 47
+图 48

+

基于陷门置换的公钥加密

+

图 1
+公钥密码两个作用,一是会话建立(即对称密钥交换),二是非交互式应用
+图 2
+图 3
+图 4
+图 5
+图 6
+图 7

+

选择密文攻击(CCA)下的安全,有时可以缩写成选择密文攻击下的不可区分性(IND-CCA)

+

当攻击者可以篡改密文时,将以优势1赢下这个CCA游戏
+图 8

+

构建CCA安全的公钥加密系统

+

陷门函数特点是单向的,只有私钥持有人才能做逆向计算
+图 9
+图 10
+图 11

+

单向陷门函数只加密一个随机值,随机值用来生成对称密钥,单向陷门函数的私钥持有人可以恢复随机值进而恢复密钥
+图 12
+图 13

+

不能直接用单向陷门函数加解密明文,因为算法是确定的,就不可能是语义安全的,也会存在许多类型的攻击
+图 14

+

构建一个陷门函数

+

本节构建一个经典的陷门函数叫做RSA
+图 1

+

随机选取ZNZ_N中的随机元素,这个元素很可能也在ZNZ_N^*中,即该元素很可能是可逆的
+图 2
+图 1
+图 2
+(虽然x大概率是可逆的,但是如果不可逆怎么办?)
+图 3

+

单向陷门函数是安全的,对称密码可以提供认证加密,H是random oracle,即H是某个从ZNZ_N映射到密钥空间的随机函数。那么这个公钥系统就可以抵抗选择密文攻击
+图 4

+

不要直接用RSA来加密明文!!!因为RSA是确定性的函数,因此不可能是语义安全的
+图 5
+图 6
+(直接用RSA加密密钥会被破解,但是ISO标准是用RSA加密密钥的哈希函数原项,破解出原项再hash一下就得到了密钥,这不是一样的吗?) 对称密钥的空间大小远远小于RSA的明文空间

+

PKCS1

+

ISO标准不是RSA在实际中的应用。实际使用是将对称密钥扩展然后用RSA加密
+图 7
+图 8
+图 9
+图 10

+

解决方法是,服务器解密后发现开头不是02,就认为明文只是个随机值而不是包含密钥的明文,继续协议就会发现密钥不一致从而结束会话(最常用的PKCS1)
+图 11

+

图 12
+图 13
+图 14
+图 15

+

RSA的安全性

+

如果已经知道N的因式分解,那么可以用中国剩余定理求解x
+图 16

+

计算e次根一定要因式分解吗?如果没有其它方法就说明了一个reduction(规约):
+任何有效的计算模N的e次根的算法都是有效的因式分解算法
+图 17
+图 18
+图 19
+图 20

+

实际应用中的RSA

+

最小的公钥e是3,可以但推荐还是65537。
+RSA-CRT(带中国剩余定理的RSA)。RSA的加密很快但是解密很慢
+图 21
+图 22

+

RSA数学上是正确的,但是如果没有较好实现,会出现各种旁道攻击
+图 23
+图 24

+

防火墙刚启动时种子数量少导致伪随机数生成器重复生成p,导致网络上许多设备的p相同
+图 25
+图 26

+

ElGamal

+

前一节讲了基于陷门置换函数的公钥加密系统,这节讲基于Diffle-Hellman协议的公钥加密系统
+图 27
+图 28
+图 29
+图 30
+图 31
+图 32
+图 33
+图 34
+图 35

+

ElGamal如果不做预计算,加密会比解密慢,但是因为g是固定的,意味着加密可以做预计算,当内存足够时加密是比解密快的。但是内存不够,不能预计算时,RSA更快,因为只做一次指数运算
+图 36

+

ElGamal安全性

+

这个计算Diffle-Hellman假设对于分析ElGamal系统的安全性并不理想
+图 37
+我们引入更强的哈希Diffle-Hellman假设,更强假设的意思是,攻击者的能力更强,但是我们提出的某个论断仍然成立
+图 38
+图 39
+图 40
+图 41
+语义安全是不够的,我们真正想要的是选择密文安全。
+为了证明选择密文安全,我们引入一个更强的假设叫做交互Diffle-Hellman假设
+图 42

+

交互Diffle-Hellman假设是CCA安全的,现在问题是在CDH假设上能否实现CCA安全,没有random oracle能否实现CCA安全
+图 43

+

有更好安全性分析的ElGamal变种

+

图 44

+

我们想在CDH假设上实现CCA安全,有两种办法,第一种是使用双线性群,这种群CDH和IDH是等价的;第二种是修改ElGamal系统
+图 45

+

第二种方法有一个ElGamal的变种满足CDH假设上的CCA安全
+图 1
+图 2

+

如果没有random oracle,CCA安全还成立吗?
+图 3
+图 4

+

ElGamal和RSA两种公钥系统共同遵循的原理

+

这里没有形式化给出单向函数的定义,因为要证明单向函数是否存在,也就是要证明P不等于NP(若P=NP,则公钥密码学将有根基危机)
+图 5
+图 6
+图 7
+RSA有乘法性质和陷门,陷门意味着有私钥就可以逆向计算
+图 8

+

总结:公钥加密依赖具有同态性质的单向函数和陷门
+图 9

+

课程总结

+

图 10
+图 11
+图 13
+图 14

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/06/22/\350\267\250\345\237\237\351\227\256\351\242\230/index.html" "b/2022/06/22/\350\267\250\345\237\237\351\227\256\351\242\230/index.html" new file mode 100644 index 0000000..e2568cc --- /dev/null +++ "b/2022/06/22/\350\267\250\345\237\237\351\227\256\351\242\230/index.html" @@ -0,0 +1,688 @@ + + + + + + + + + + + + + + + + + + + + 跨域问题 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 跨域问题 +

+ + + + +
+
+ +

同源策略

+

同源策略是浏览器的重要安全策略。大致来说,不同域(协议+域名/ip+端口)生成的cookie只能给这个域使用

+

跨域出现与解决

+

下面的演示是在hosts文件中添加以下配置

+
127.0.0.1   zhufeng-test.163.com
+
+

不添加的话,把zhufeng-test.163.com换成localhost或者127.0.0.1是一样的

+

假如我们只有后端,它的域是http://zhufeng-test.163.com:8080,它有一个get方法是setCookie,那么访问http://zhufeng-test.163.com:8080/setCookie,此时可以在Response Cookies中找到这个cookie,对这个域下的接口访问时会自动带上这个域所有可见的cookie(springboot开启allowCredentials,前端axios需要自己开启withCredentials: true)
+图 2

+

图 3

+

图 1

+

现在我们有前端了,前后端分离运行,前端运行在http://zhufeng-test.163.com:8081

+

图 6

+

因为后端生成的cookie的domain是 zhufeng-test.163.com,所以浏览器访问相同域名的前端也可以读取到这个cookie。
+但是前端不能访问后端的其它接口,因为它们端口不同,发生了跨域,浏览器不会带上cookie
+图 7

+

给后端配置一下跨域,.allowedOrigins(“http://zhufeng-test.163.com:8081/”)表示允许前端http://zhufeng-test.163.com:8081访问后端,不开会返回状态码200的跨域错误,.allowCredentials(true)不开时前端访问后端不会带上后端域名可见的cookie

+
@Configuration
+public class CorsConfig implements WebMvcConfigurer {
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        //项目中的所有接口都支持跨域
+        registry.addMapping("/**")
+                // 所有地址都可以访问,也可以配置具体地址
+                // .allowedOrigins("http://zhufeng-test.163.com:8081/")
+                .allowedOriginPatterns("*://*:*/")
+                // "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"
+                .allowedMethods("*")
+                // 允许前端携带后端域名可见的cookie
+                .allowCredentials(true)
+                .maxAge(3600);
+    }
+}
+
+

图 8

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/06/23/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\270\200\357\274\211/index.html" "b/2022/06/23/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\270\200\357\274\211/index.html" new file mode 100644 index 0000000..a20b95d --- /dev/null +++ "b/2022/06/23/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\270\200\357\274\211/index.html" @@ -0,0 +1,1153 @@ + + + + + + + + + + + + + + + + + + + + Effective-Java学习笔记(一) | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Effective-Java学习笔记(一) +

+ + + + +
+
+ +

第二章 创建和销毁对象

+

1. 考虑用静态工厂方法代替构造函数

+

静态工厂方法是一个返回该类实例的 public static 方法。例如,Boolean类的valueOf方法

+
public static Boolean valueOf(boolean b) {
+    return b ? Boolean.TRUE : Boolean.FALSE;
+}
+
+

要注意静态工厂方法与设计模式中的工厂方法不同。

+

静态工厂方法优点:

+
    +
  1. 静态工厂方法有确切名字,客户端实例化对象时代码更易懂。例如,BigInteger类中返回可能为素数的BigInteger对象静态工厂方法叫BigInteger.probablePrime。此外,每个类的构造函数签名是唯一的,但是程序员可以通过调整参数类型、个数或顺序修改构造函数的签名,这样会给客户端实例化对象带来困惑,因为静态工厂方法有确切名称所以不会出现这个问题
  2. +
  3. 静态工厂方法不需要在每次调用时创建新对象。例如Boolean.valueOf(boolean),true和false会返回预先创建好的对应Boolean对象。这种能力允许类在任何时候都能严格控制存在的实例,常用来实现单例
  4. +
  5. 静态工厂方法可以获取任何子类的对象。这种能力的一个应用是API可以不公开子类或者实现类的情况下返回对象。例如Collections类提供静态工厂生成不是public的子类对象,不可修改集合和同步集合等
  6. +
  7. 静态工厂方法返回对象的类型可以根据输入参数变换。例如,EnumSet类的noneOf方法,当enum的元素个数小于等于64,noneOf方法返回RegularEnumSet类型的对象,否则返回JumboEnumSet类型的对象
  8. +
  9. 静态工厂方法的返回对象的类不需要存在。这种灵活的静态工厂方法构成了服务提供者框架的基础。service provider框架有三个必要的组件:代表实现的service interface;provider registration API,提供者用来注册实现;service access API,客户端使用它来获取服务的实例,服务访问API允许客户端选择不同实现,是一个灵活的静态工厂方法。service provider第四个可选的组件是service provider interface,它描述了产生service interface实例的工厂对象。在JDBC中,Connection扮演service interface角色,DriverManager.registerDriver是provider registration API,DriverManager.getConnection是service access API,Driver是service provider interface
  10. +
+

静态工厂方法的缺点:

+
    +
  1. +

    只提供静态工厂方法而没有public或者protected的构造方法就不能被继承,例如Collections类就不能被继承

    +
  2. +
  3. +

    静态工厂方法没有构造函数那么显眼,常见的静态工厂方法名字如下:
    +from,一种类型转换方法,该方法接受单个参数并返回该类型的相应实例,例如:

    +
    Date d = Date.from(instant);
    +
    +

    of,一个聚合方法,它接受多个参数并返回一个包含这些参数的实例,例如:

    +
    Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
    +
    +

    valueOf,一种替代 from 和 of 但更冗长的方法

    +
    BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
    +
    +

    instance 或 getInstance,返回一个实例,该实例由其参数(如果有的话)描述,但不具有相同的值,例如:

    +
    StackWalker luke = StackWalker.getInstance(options);
    +
    +

    create 或 newInstance,与 instance 或 getInstance 类似,只是该方法保证每个调用都返回一个新实例,例如:

    +
    Object newArray = Array.newInstance(classObject, arrayLen);
    +
    +

    getType,类似于 getInstance,但如果工厂方法位于不同的类中,则使用此方法。其类型是工厂方法返回的对象类型,例如:

    +
    FileStore fs = Files.getFileStore(path);
    +
    +

    newType,与 newInstance 类似,但是如果工厂方法在不同的类中使用。类型是工厂方法返回的对象类型,例如:

    +
    BufferedReader br = Files.newBufferedReader(path);`
    +
    +

    type,一个用来替代 getType 和 newType 的比较简单的方式,例如:

    +
    List<Complaint> litany = Collections.list(legacyLitany);
    +
    +
  4. +
+

2. 当构造函数有多个参数时,考虑改用Builder

+

静态工厂和构造函数都有一个局限:不能对大量可选参数做很好扩展。例如,一个表示食品营养标签的类,必选字段有净含量、热量,另外有超过20个可选字段,比如反式脂肪、钠等。

+

为这种类编写构造函数,通常是使用可伸缩构造函数,这里展示四个可选字段的情况

+
// Telescoping constructor pattern - does not scale well!
+public class NutritionFacts {
+    private final int servingSize; // (mL) required
+    private final int servings; // (per container) required
+    private final int calories; // (per serving) optional
+    private final int fat; // (g/serving) optional
+    private final int sodium; // (mg/serving) optional
+    private final int carbohydrate; // (g/serving) optional
+
+    public NutritionFacts(int servingSize, int servings) {
+        this(servingSize, servings, 0);
+    }
+
+    public NutritionFacts(int servingSize, int servings, int calories) {
+        this(servingSize, servings, calories, 0);
+    }
+
+    public NutritionFacts(int servingSize, int servings, int calories, int fat) {
+        this(servingSize, servings, calories, fat, 0);
+    }
+
+    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
+        this(servingSize, servings, calories, fat, sodium, 0);
+    }
+
+    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
+        this.servingSize = servingSize;
+        this.servings = servings;
+        this.calories = calories;
+        this.fat = fat;
+        this.sodium = sodium;
+        this.carbohydrate = carbohydrate;
+    }
+}
+
+

当你想创建指定carbohydrate的实例,就必须调用最后一个构造函数,这样就必须给出calories、fat、sodium的值。当有很多可选参数时,这种模式可读性很差。

+

当遇到许多可选参数时,另一种选择是使用JavaBean模式,在这种模式中,调用一个无参数的构造函数来创建对象,然后调用setter方法来设置参数值

+
// JavaBeans Pattern - allows inconsistency, mandates mutability
+public class NutritionFacts {
+    // Parameters initialized to default values (if any)
+    private int servingSize = -1; // Required; no default value
+    private int servings = -1; // Required; no default value
+    private int calories = 0;
+    private int fat = 0;
+    private int sodium = 0;
+    private int carbohydrate = 0;
+    public NutritionFacts() { }
+    // Setters
+    public void setServingSize(int val) { servingSize = val; }
+    public void setServings(int val) { servings = val; }
+    public void setCalories(int val) { calories = val; }
+    public void setFat(int val) { fat = val; }
+    public void setSodium(int val) { sodium = val; }
+    public void setCarbohydrate(int val) { carbohydrate = val; }
+}
+
+

这种模式比可伸缩构造函数模式更易读,但有严重的缺点。因为对象的构建要调用多个set方法,所以对象可能会在构建过程中处于不一致状态(多线程下,其它线程使用了未构建完成的对象)

+

第三种选择是建造者模式,它结合了可伸缩构造函数模式的安全性和JavaBean模式的可读性。客户端不直接生成所需的对象,而是使用所有必需的参数调用构造函数或静态工厂方法生成一个builder对象。然后,客户端在builder对象上调用像set一样的方法来设置可选参数,最后调用build方法生成所需对象。Builder通常是它构建的类的静态成员类

+
// Builder Pattern
+public class NutritionFacts {
+    private final int servingSize;
+    private final int servings;
+    private final int calories;
+    private final int fat;
+    private final int sodium;
+    private final int carbohydrate;
+
+    public static class Builder {
+        // Required parameters
+        private final int servingSize;
+        private final int servings;
+        // Optional parameters - initialized to default values
+        private int calories = 0;
+        private int fat = 0;
+        private int sodium = 0;
+        private int carbohydrate = 0;
+
+        public Builder(int servingSize, int servings) {
+            this.servingSize = servingSize;
+            this.servings = servings;
+        }
+
+        public Builder calories(int val) {
+            calories = val;
+            return this;
+        }
+
+        public Builder fat(int val) {
+            fat = val;
+            return this;
+        }
+
+        public Builder sodium(int val) {
+            sodium = val;
+            return this;
+        }
+
+        public Builder carbohydrate(int val) {
+            carbohydrate = val;
+            return this;
+        }
+
+        public NutritionFacts build() {
+            return new NutritionFacts(this);
+        }
+    }
+
+    private NutritionFacts(Builder builder) {
+        servingSize = builder.servingSize;
+        servings = builder.servings;
+        calories = builder.calories;
+        fat = builder.fat;
+        sodium = builder.sodium;
+        carbohydrate = builder.carbohydrate;
+    }
+}
+
+

NutritionFacts类是不可变的。Builder的set方法返回builder对象本身,这样就可以链式调用。下面是客户端代码的样子:

+
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
+.calories(100).sodium(35).carbohydrate(27).build();
+
+

为了简介,这里省略的参数校验。参数校验在Builder的构造函数和方法中。多参数校验在build方法中

+

建造者模式也适用于抽象类,抽象类有抽象Builder

+
import java.util.EnumSet;
+import java.util.Objects;
+import java.util.Set;
+
+// Builder pattern for class hierarchies
+public abstract class Pizza {
+    public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
+
+    final Set<Topping> toppings;
+
+    abstract static class Builder<T extends Builder<T>> {
+        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
+
+        public T addTopping(Topping topping) {
+            toppings.add(Objects.requireNonNull(topping));
+            return self();
+        }
+
+        abstract Pizza build();
+
+        // Subclasses must override this method to return "this"
+        protected abstract T self();
+    }
+
+    Pizza(Builder<?> builder) {
+        toppings = builder.toppings.clone(); // See Item 50
+    }
+}
+
+
import java.util.Objects;
+
+public class NyPizza extends Pizza {
+    public enum Size {SMALL, MEDIUM, LARGE}
+
+    private final Size size;
+
+    public static class Builder extends Pizza.Builder<Builder> {
+        private final Size size;
+
+        public Builder(Size size) {
+            this.size = Objects.requireNonNull(size);
+        }
+
+        @Override
+        public NyPizza build() {
+            return new NyPizza(this);
+        }
+
+        @Override
+        protected Builder self() {
+            return this;
+        }
+    }
+
+    private NyPizza(Builder builder) {
+        super(builder);
+        size = builder.size;
+    }
+}
+
+public class Calzone extends Pizza {
+    private final boolean sauceInside;
+
+    public static class Builder extends Pizza.Builder<Builder> {
+        private boolean sauceInside = false; // Default
+
+        public Builder sauceInside() {
+            sauceInside = true;
+            return this;
+        }
+
+        @Override
+        public Calzone build() {
+            return new Calzone(this);
+        }
+
+        @Override
+        protected Builder self() {
+            return this;
+        }
+    }
+
+    private Calzone(Builder builder) {
+        super(builder);
+        sauceInside = builder.sauceInside;
+    }
+}
+
+

客户端实例化对象代码如下:

+
NyPizza pizza = new NyPizza.Builder(SMALL)
+.addTopping(SAUSAGE).addTopping(ONION).build();
+Calzone calzone = new Calzone.Builder()
+.addTopping(HAM).sauceInside().build();
+
+

建造者模式非常灵活,一个builder对象可以反复构建多个对象,可以通过builder中的方法调用生成不同的对象。建造者模式的缺点就是生成一个对象前要先创建它的builder对象,性能会稍差一些,但为了未来字段更好扩展,建议还是用建造者模式

+

3. 使用私有构造函数或枚举类型创建单例

+

单例是只实例化一次的类。当类不保存状态或状态都一致,那么它的对象本质上都是一样的,可以用单例模式创建

+

实现单例有两种方法。两者都基于私有化构造函数和对外提供 public static 成员,在第一个方法中,该成员是个用final修饰的字段

+
// Singleton with public final field
+public class Elvis {
+    public static final Elvis INSTANCE = new Elvis();
+    private Elvis() { ... }
+    public void leaveTheBuilding() { ... }
+}
+
+

私有构造函数只调用一次,用于初始化public static final 修饰的Elvis类型字段INSTANCE。一旦初始化Elvis类,就只会存在一个Elvis实例。客户端不能再创建别的实例,但是要注意的是拥有特殊权限的客户端可以利用反射调用私有构造函数生成实例

+
Constructor<?>[] constructors = Elvis.class.getDeclaredConstructors();
+AccessibleObject.setAccessible(constructors, true);
+
+Arrays.stream(constructors).forEach(name -> {
+    if (name.toString().contains("Elvis")) {
+        Elvis instance = (Elvis) name.newInstance();
+        instance.leaveTheBuilding();
+    }
+});
+
+

第二种方法,对外提供的 public static 成员是个静态工厂方法

+
// Singleton with static factory
+public class Elvis {
+    private static final Elvis INSTANCE = new Elvis();
+    private Elvis() { ... }
+    public static Elvis getInstance() { return INSTANCE; }
+    public void leaveTheBuilding() { ... }
+}
+
+

所有对getInsance()方法的调用都返回相同的对象,但是同样可以通过反射调用私有构造函数创建对象。
+这两种方法要实现可序列化,仅仅在声明中添加implements Serializable是不够的。还要声明所有实例字段未transient,并提供readResolve方法,否则,每次反序列化都会创建一个新实例。JVM在反序列化时会自动调用readResolve方法

+
// readResolve method to preserve singleton property
+private Object readResolve() {
+    // Return the one true Elvis and let the garbage collector
+    // take care of the Elvis impersonator.
+    return INSTANCE;
+}
+
+

实现单例的第三种方法时声明一个单元素枚举

+
// Enum singleton - the preferred approach
+public enum Elvis {
+    INSTANCE;
+    public void leaveTheBuilding() { ... }
+}
+
+

这种方法类似于 public 字段方法,但是它更简洁,默认提供了序列化机制,提供了对多个实例化的严格保证,即使面对复杂的序列化或反射攻击也是如此。这种方法可能有点不自然,但是单元素枚举类型通常是实现单例的最佳方法。但是,如果你的单例要继承父类,那么就不能用这种方法

+

4. 用私有构造函数实施不可实例化

+

工具类不需要实例化,它里面的方法都是public static的,可以通过私有化构造函数使类不可实例化

+
// Noninstantiable utility class
+public class UtilityClass {
+    // Suppress default constructor for noninstantiability
+    private UtilityClass() {
+        throw new AssertionError();
+    } ... // Remainder omitted
+}
+
+

AssertionError不是必须有的,但可以防止构造函数被意外调用(反射)

+

这种用法也防止了类被继承。因为所有子类构造函数都必须显示或者隐式调用父类构造函数,但父类构造函数私有化后就无法调用

+

5. 依赖注入优于硬连接资源

+

有些类依赖于一个或多个资源。例如拼写检查程序依赖于字典。错误实现如下:

+
// Inappropriate use of static utility - inflexible & untestable!
+public class SpellChecker {
+    private static final Lexicon dictionary = ...;
+    private SpellChecker() {} // Noninstantiable
+    public static boolean isValid(String word) { ... }
+    public static List<String> suggestions(String typo) { ... }
+}
+
+
// Inappropriate use of singleton - inflexible & untestable!
+public class SpellChecker {
+    private final Lexicon dictionary = ...;
+    private SpellChecker(...) {}
+    public static INSTANCE = new SpellChecker(...);
+    public boolean isValid(String word) { ... }
+    public List<String> suggestions(String typo) { ... }
+}
+
+

这两种写法分别是工具类和单例,都假定使用同一个字典,实际情况是不同拼写检查程序依赖不同的字典。

+

你可能会想取消dictionary的final修饰,并让SpellChecker类添加更改dictionary的方法。但这种方法在并发环境下会出错。工具类和单例不适用于需要参数化依赖对象

+

参数化依赖对象的一种简单模式是,创建对象时将被依赖的对象传递给构造函数。这是依赖注入的一种形式。

+
// Dependency injection provides flexibility and testability
+public class SpellChecker {
+    private final Lexicon dictionary;
+    public SpellChecker(Lexicon dictionary) {
+        this.dictionary = Objects.requireNonNull(dictionary);
+    }
+    public boolean isValid(String word) { ... }
+    public List<String> suggestions(String typo) { ... }
+}
+
+

依赖注入适用于构造函数、静态工厂方法、建造者模式。

+

依赖注入的一个有用变体是将工厂传递给构造函数,这样可以反复创建被依赖的对象。Java8中引入的Supplier<T>非常适合作为工厂。下面是一个生产瓷砖的方法

+
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
+
+

尽管依赖注入极大提高了灵活性和可测试性,但它可能会使大型项目变得混乱。通过使用依赖注入框架(如Spring、Dagger、Guice)可以消除这种混乱

+

6. 避免创建不必要的对象

+

复用对象可以加快程序运行速度,如果对象是不可变的,那么它总是可以被复用的。

+

一个不复用对象的极端例子如下

+
String s = new String("bikini"); // DON'T DO THIS!
+
+

该语句每次执行都会创建一个新的String实例。"bikini"本身就是一个String实例,改进的代码如下

+
String s = "bikini";
+
+

这个版本使用单个String实例,而不是每次执行时都会创建一个新的实例。此外,可以保证在同一虚拟机中运行的其它代码都可以复用该对象,只要它们都包含相同的字符串字面量

+

通常可以使用静态工厂方法来避免创建不必要的对象。例如,Boolean.valueOf(String) 比构造函数Boolean(String) 更可取,后者在Java9中被废弃了。

+

有些对象的创建代价很高,如果要重复使用,最好做缓存。例如,使用正则表达式确定字符串是否为有效的罗马数字

+
// Performance can be greatly improved!
+static boolean isRomanNumeral(String s) {
+    return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
+}
+
+

这个方法的问题在于它依赖String.matches方法。虽然 String.matches 是检查字符串是否与正则表达式匹配的最简单方法,但它不适合要求高性能的情况下重复使用,因为它在内部为正则表达式创建了一个Pattern实例,并且只使用了一次就垃圾回收了,创建一个Pattern实例代价很大,因为它需要将正则表达式编译成有限的状态机

+

为了提高性能,将正则表达式显示编译成Pattern实例,作为类初始化的一部分,每次调用匹配的方法时都使用这个实例

+
// Reusing expensive object for improved performance
+public class RomanNumerals {
+    private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
+    static boolean isRomanNumeral(String s) {
+        return ROMAN.matcher(s).matches();
+    }
+}
+
+

当对象是不可变的时候,我们很容易会想到复用。但是有些情况不那么容易想到复用。例如适配器模式中的适配器,因为适配器中没有它适配对象的状态,所以不需要创建多个适配器。

+

一个具体的例子是,KeySet是Map中的适配器,使得Map不仅能提供返回键值对的方法,也能返回所有键的Set,KeySet不需要重复创建,对Map的修改会同步到KeySet实例

+

7. 排除过时的对象引用

+

Java具体垃圾回收机制,会让程序员的工作轻松很多,但是并不意味着需要考虑内存管理,考虑以下简单的堆栈实现

+
import java.util.Arrays;
+import java.util.EmptyStackException;
+
+// Can you spot the "memory leak"?
+public class Stack {
+    private Object[] elements;
+    private int size = 0;
+    private static final int DEFAULT_INITIAL_CAPACITY = 16;
+
+    public Stack() {
+        elements = new Object[DEFAULT_INITIAL_CAPACITY];
+    }
+
+    public void push(Object e) {
+        ensureCapacity();
+        elements[size++] = e;
+    }
+
+    public Object pop() {
+        if (size == 0)
+            throw new EmptyStackException();
+        return elements[--size];
+    }
+
+    /**
+     * Ensure space for at least one more element, roughly
+     * doubling the capacity each time the array needs to grow.
+     */
+    private void ensureCapacity() {
+        if (elements.length == size)
+            elements = Arrays.copyOf(elements, 2 * size + 1);
+    }
+}
+
+

这段代码有一个潜在的内存泄露问题。当栈的长度增加,再收缩时,从栈中pop的对象不会被回收,因为引用仍然还在elements中。解决方法很简单,一旦pop就置空

+
public Object pop() {
+    if (size == 0)
+        throw new EmptyStackException();
+    Object result = elements[--size];
+    elements[size] = null; // Eliminate obsolete reference
+    return result;
+}
+
+

用null处理过时引用的另一个好处是,如果被意外引用的话会立刻抛NullPointerException

+

另外一个常见的内存泄漏是缓存。HashMap的key是对实际对象的强引用,不会被GC回收。WeakHashMap的key如果只有WeakHashMap本身使用,外部没有使用,那么会被GC回收

+

内存泄露第三种常见来源是监听器和其它回调。如果客户端注册了回调但是没有显式地取消它们,它们就会一直在内存中。确保回调被及时回收的一种方法是仅存储它们的弱引用,例如,将他它们作为键存储在WeakHashMap中

+

8. 避免使用终结器和清除器

+

如标题所说

+

9. 使用 try-with-resources 优于 try-finally

+

Java库中有许多必须通过调用close方法手动关闭的资源,比如InputStream、OutputStream和java.sql.Connection。

+

从历史上看,try-finally语句是确保正确关闭资源的最佳方法,即便出现异常或返回

+
// try-finally - No longer the best way to close resources!
+static String firstLineOfFile(String path) throws IOException {
+    BufferedReader br = new BufferedReader(new FileReader(path));
+    try {
+        return br.readLine();
+    } finally {
+        br.close();
+    }
+}
+
+

这种方式在资源变多时就很糟糕

+
// try-finally is ugly when used with more than one resource!
+static void copy(String src, String dst) throws IOException {
+    InputStream in = new FileInputStream(src);
+    try {
+        OutputStream out = new FileOutputStream(dst);
+    try {
+        byte[] buf = new byte[BUFFER_SIZE];
+        int n;
+        while ((n = in.read(buf)) >= 0)
+            out.write(buf, 0, n);
+    } finally {
+        out.close();
+        }
+    }
+    finally {
+        in.close();
+    }
+}
+
+

使用 try-finally 语句关闭资源的正确代码(如前两个代码示例所示)也有一个细微的缺陷。try 块和 finally 块中的代码都能够抛出异常。例如,在 firstLineOfFile 方法中,由于底层物理设备发生故障,对 readLine 的调用可能会抛出异常,而关闭的调用也可能出于同样的原因而失败。在这种情况下,第二个异常将完全覆盖第一个异常。异常堆栈跟踪中没有第一个异常的记录,这可能会使实际系统中的调试变得非常复杂(而这可能是希望出现的第一个异常,以便诊断问题)

+

Java7 引入 try-with-resources语句解决了这个问题。要使用这个结构,资源必须实现AutoCloseable接口,这个接口只有一个void close方法,下面是前两个例子的try-with-resources形式

+
// try-with-resources - the the best way to close resources!
+static String firstLineOfFile(String path) throws IOException {
+    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
+        return br.readLine();
+    }
+}
+
+
// try-with-resources on multiple resources - short and sweet
+static void copy(String src, String dst) throws IOException {
+    try (InputStream in = new FileInputStream(src);OutputStream out = new FileOutputStream(dst)) {
+        byte[] buf = new byte[BUFFER_SIZE];
+        int n;
+        while ((n = in.read(buf)) >= 0)
+            out.write(buf, 0, n);
+    }
+}
+
+

try-with-resources为开发者提供了更好的异常排查方式。考虑firstLineOfFile方法,如果异常由readLine和不可见close抛出,那么后者异常会被抑制。被抑制的异常不会被抛弃,它们会被打印在堆栈中并标记被抑制。可以通过getSuppressed方法访问它们,该方法是Java7中添加到Throwable中的

+

像try-catch-finally一样,try-with-resources也可以写catch语句。下面是firstLineOfFile方法的一个版本,它不抛出异常,但如果无法打开文件或从中读取文件,会返回一个默认值

+
// try-with-resources with a catch clause
+static String firstLineOfFile(String path, String defaultVal) {
+    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
+        return br.readLine();
+    } catch (IOException e) {
+        return defaultVal;
+    }
+}
+
+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/06/30/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\272\214\357\274\211/index.html" "b/2022/06/30/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\272\214\357\274\211/index.html" new file mode 100644 index 0000000..6f0e070 --- /dev/null +++ "b/2022/06/30/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\272\214\357\274\211/index.html" @@ -0,0 +1,871 @@ + + + + + + + + + + + + + + + + + + + + Effective-Java学习笔记(二) | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Effective-Java学习笔记(二) +

+ + + + +
+
+ +

第三章 对象的通用方法

+

10. 覆盖 equals 方法时应遵守的约定

+

不覆盖equals方法的情况

+
    +
  • +

    类的每个实例本质上都是唯一的。例如Thread类,它是活动实体类而不是值类

    +
  • +
  • +

    该类不需要提供逻辑相等测试。例如java.util.regex.Pattern可以覆盖equals方法来检查两个Pattern实例是否表示完全相同的正则表达式,但是这个类的设计人员认为客户端不需要这个功能,所以没有覆盖

    +
  • +
  • +

    父类已经覆盖了equals方法,父类的行为也适合于这个类。例如大多数Set的equals从AbstractSet继承,List从AbstractList继承,Map从AbstractMap继承

    +
  • +
  • +

    类是私有的并且你确信它的equals方法永远不会被调用。保险起见,你可以按如下方式覆盖equals方法,以确保它不会被意外调用

    +
    @Override
    +public boolean equals(Object o) {
    +    throw new AssertionError(); // Method is never called
    +}
    +
    +
  • +
+

覆盖equals方法的时机

+

当一个类有一个逻辑相等的概念,而这个概念不同于仅判断对象的同一性(相同对象的引用),并且父类没有覆盖 equals。对于值类通常是这样。值类只是表示值的类,例如 Integer 或 String。程序员希望发现它们在逻辑上是否等价,而不是它们是否引用相同的对象。覆盖 equals 方法不仅是为了满足程序员的期望,它还使实例能够作为 Map 的键或 Set 元素时,具有可预测的、理想的行为。

+

单例模式的值类不需要覆盖equals方法。例如,枚举类型就是单例值类。逻辑相等就是引用相等。

+

覆盖equals方法的规范

+
    +
  • +

    反身性:对于任何非空的参考值x,x.equals(x)必须返回true

    +
  • +
  • +

    对称性:x.equals(y)y.equals(x)的值要么都为true要么为false

    +
  • +
  • +

    传递性:对于非空引用x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也返回true

    +
  • +
  • +

    一致性:对于任何非空的引用值 x 和 y,x.equals(y) 的多次调用必须一致地返回 true 或一致地返回 false,前提是不修改 equals 中使用的信息。

    +
  • +
  • +

    非空性:对于非空引用x,x.equals(null)返回false,不需要显示判断是否为null,因为equals方法需要将参数转为成相同类型,转换之前会使用instanceof运算符来检查类型是否正确,如果为null,也会返回false

    +
    @Override
    +public boolean equals(Object o) {
    +    if (!(o instanceof MyType))
    +        return false;
    +    MyType mt = (MyType) o;
    +    ...
    +}
    +
    +
  • +
+

高质量构建equals方法的步骤

+

1、使用==检查参数是否是this对象的引用,如果是,返回true,这是一种性能优化,如果比较的开销很大,这种优化很有必要
+2、使用instanceof运算符检查参数是否有正确类型。
+3、将参数转换为正确的类型。因为在转换前进行了instanceof判断,所以肯定可以强转成功
+4、对类中的每个有意义的字段检查是否和参数的相应字段匹配

+

对于不是float和double的基本类型字段,使用==比较;对应对象引用字段,递归调用equals方法;对于float字段,使用静态方法Float.compare(float,float)方法;对于double字段,使用Double.compare(double,double)。float和double字段的特殊处理是由于Float.NaN,-0.0f 和类似的双重值的存在;对于数组字段,使用Arrays.equals方法。

+

一些对象引用字段可能允许null出现。为了避免可能出现NullPointerException,请使用Objects.equals(Object,Object)来检查对象的字段是否相等。

+

对于某些类,字段比较非常复杂,如果是这样,可以存储字段的规范形式,以便equals方法进行开销较小的比较。这种技术最适合于不可变类;如果对象可以修改,则必须使规范形式保持最新

+

equals 方法的性能可能会受到字段比较顺序的影响。为了获得最佳性能,你应该首先比较那些更可能不同、比较成本更低的字段。不能比较不属于对象逻辑状态的字段,例如用于同步操作的锁字段。派生字段可以不比较,这样可以提高性能,但是如果派生字段包括了对象的所有信息,比如说多边形面积,可以从边和顶点计算得出,那么先比较面积,面积不一样就肯定不是同一个对象,这样可以减小开销

+

一些警告

+
    +
  • 覆盖equals方法时,也要覆盖hashCode方法
  • +
  • 考虑任何形式的别名都不是一个好主意。例如,File类不应该尝试将引用同一文件的符号链接等同起来
  • +
  • 不要用别的类型替换equals方法的Object类型
  • +
+

11. 当覆盖equals方法时,总是覆盖hashCode方法

+

Object类中hashCode方法的规范

+
    +
  • 应用程序执行期间对对象重复调用hashCode方法,它必须返回相同的值,前提是不修改equals方法中用于比较的信息。这个值不需要在应用程序的不同执行之间保持一致
  • +
  • 如果equals(Object)方法返回true,那么在这两个对象上调用hashCode方法必须产生相同的整数结果
  • +
  • 如果equals(Object)方法返回false,hashCode方法的值不需要一定不同,但是,不同对象的hashCode不一样可以提高散列表性能
  • +
+

当没有覆盖hashCode方法时,将违反第二条规范:逻辑相等的对象必须有相等的散列码。两个不同的对象在逻辑上是相等的,但它们的hashCode一般不相等。例如用Item-10中的PhoneNumber类实例作为HashMap的键

+
Map<PhoneNumber, String> m = new HashMap<>();
+m.put(new PhoneNumber(707, 867, 5309), "Jenny");
+
+

此时,你可能期望m.get(new PhoneNumber(707, 867,5309)) 返回「Jenny」,但是它返回 null。因为PhoneNumber类没有覆盖hashCode方法,插入到HashMap和从HashMap中获取的实例具有不相同的散列码,这违法了hashCode方法规范。因此,get方法查找电话号码的散列桶与put方法存储电话号码的散列桶不同。

+

解决这个问题有一个最简单但很糟糕的实现

+
// The worst possible legal hashCode implementation - never use!
+@Override
+public int hashCode() { return 42; }
+
+

它确保了逻辑相等的对象具有相同的散列码。同时它也很糟糕,因为每个对象的散列码都相同了。每个对象都分配到一个存储桶中,散列表退化成链表。

+

散列算法设计步骤

+

一个好的散列算法大概率为逻辑不相等对象生成不相同的散列码。理想情况下,一个散列算法应该在所有int值上均匀合理分布所有不相等对象。实现理想情况很困难,但实现一个类似的并不难,这里有一个简单的方式:
+1、声明一个名为 result 的int变量,并将其初始化为对象中第一个重要字段的散列码c,如步骤2.a中计算的那样
+2、对象中剩余的重要字段f,执行以下操作:
+a. 为字段计算一个整数散列码c : 如果字段是基本数据类型,计算Type.hashCode(f),其中type是 f 类型对应的包装类 ; 如果字段是对象引用,并且该类的equals方法通过递归调用equals方法来比较字段,则递归调用字段上的hashCode方法。如果需要更复杂的比较,则为该字段计算一个【canonical representation】,并在canonical representation上调用hashCode方法。如果字段的值为空,则使用0(或其它常数,但0是惯用的);如果字段是一个数组,则对数组中每个重要元素都计算散列码,并用2.b步骤逐个组合。如果数组中没有重要元素,则使用常量,最好不是0。如果所有元素都很重要,那么使用Arrays.hashCode

+

b. 将2.a步骤中计算的散列码合并到result变量,如下所示

+
result = 31 * result + c;
+
+

3、返回result变量

+

步骤2.b中的乘法说明result依赖字段的顺序,如果类具有多个类似字段,那么乘法会产生更好的hash性能。例如字符串hash算法中如果省略乘法,那么不同顺序的字符串都会有相同的散列码。

+

选择31是因为它是奇素数。如果是偶数,乘法运算就会溢出,信息就会丢失,因为乘法运算等同于移位。使用素数的好处不太明显,但它是传统用法。31有一个很好的特性,可以用移位和减法来代替乘法,从而在某些体系结构上获得更好的性能:31 * i == (i <<5) – i。现代虚拟机自动进行这种优化。

+

根据前面的步骤,给PhoneNumber类写一个hashCode方法

+
// Typical hashCode method
+@Override
+public int hashCode() {
+    int result = Short.hashCode(areaCode);
+    result = 31 * result + Short.hashCode(prefix);
+    result = 31 * result + Short.hashCode(lineNum);
+    return result;
+}
+
+

因为这个方法返回一个简单的确定的计算结果,它的唯一输入是 PhoneNumber 实例中的三个重要字段,所以很明显,相等的 PhoneNumber 实例具有相等的散列码。实际上,这个方法是 PhoneNumber 的一个非常好的 hashCode 方法实现,与 Java 库中的 hashCode 方法实现相当。它很简单,速度也相当快,并且合理地将不相等的电话号码分散到不同的散列桶中。

+

虽然这个Item里的方法可以提供一个相当不错的散列算法,但它不是最先进的,对于大多数用途是足够的,如果需要不太可能产生冲突的散列算法。请参阅 Guava 的 com.google.common.hash.Hashing

+

Objects类有一个静态方法,它接受任意数量的对象并返回它们的散列码。这个名为hash的方法允许你编写只有一行代码的hashCode方法,它的质量可以与本Item提供的编写方法媲美。但不幸的是它们运行得很慢,因为它需要创建数组来传递可变数量的参数,如果有参数是原始类型的,则需要进行装箱和拆箱。推荐只在性能不重要的情况下使用这种散列算法。下面是使用这个静态方法编写的PhoneNumber的散列算法

+
// One-line hashCode method - mediocre performance
+@Override
+public int hashCode() {
+    return Objects.hash(lineNum, prefix, areaCode);
+}
+
+

缓存散列值

+

如果类是不可变的,并且计算散列码的成本非常高,那么可以考虑在对象中缓存散列码,而不是每次调用重新计算。如果这个类的对象会被用作散列键,那么应该在创建对象时就计算散列码。要不然就在第一次调用时计算散列码

+

12. 始终覆盖toString方法

+

虽然 Object 提供 toString 方法的实现,但它返回的字符串通常不是类的用户希望看到的。它由后跟「at」符号(@)的类名和散列码的无符号十六进制表示(例如 PhoneNumber@163b91)组成。toString 的通用约定是这么描述的,返回的字符串应该是「简洁但信息丰富的表示,易于阅读」。虽然有人认为 PhoneNumber@163b91 简洁易懂,但与 707-867-5309 相比,它的信息量并不大。toString 约定接着描述,「建议所有子类覆盖此方法。」好建议,确实!

+

虽然它不如遵守euals和hashCode约定(Item10和Item11)那么重要,但是提供一个好的toString方法更便于调试。当对象被传递给 println、printf、字符串连接操作符或断言或由调试器打印时,将自动调用 toString 方法。即使你从来没有调用 toString 对象,其他人也可能使用。例如,使用该对象的组件可以在日志错误消息中包含对象的字符串表示。如果你不覆盖 toString,该消息可能完全无用。

+

13. 明智地覆盖clone方法

+

Cloneable接口的作用

+

Cloneable接口的作用是声明类可克隆,但是接口不包含任何方法,类的clone方法继承自Object,并且Object类的clone方法是受保护的,无法跨包调用,虽然可以通过反射调用,但也不能保证对象具有可访问的 clone 方法(如果类没有覆盖clone方法可以通过获取父类Object调用clone方法,但是如果类没实现Cloneable接口调用会抛出CloneNotSupportedException)。

+

既然 Cloneable 接口不包含任何方法,用它来做什么呢?它决定了 Object 类受保护的 clone 实现的行为:如果一个类实现了 Cloneable 接口,Object 类的 clone 方法则返回该类实例的逐字段拷贝;没实现 Cloneable 接口调用 clone 方法会抛出 CloneNotSupportedException。这是接口非常不典型的一种使用方式,不应该效仿。通常,类实现接口可以表明类能够为其客户端做些什么。在本例中,它修改了父类上受保护的方法的行为。

+

clone 方法规范

+

虽然规范没有说明,但是在实践中,实现 Cloneable 接口的类应该提供一个功能正常的 public clone 方法。

+

clone方法的一般约定很薄弱。下面的内容是从Object规范复制过来的

+
Creates and returns a copy of this object. The precise meaning of “copy” may depend on the class of the object. The general intent is that, for any object x,the expression
+
+x.clone() != x
+will be true, and the expression
+
+x.clone().getClass() == x.getClass()
+will be true, but these are not absolute requirements. While it is typically the case that
+
+x.clone().equals(x)
+will be true, this is not an absolute requirement.
+
+

clone方法创建并返回对象的副本。「副本」的确切含义可能取决于对象的类。通常,对于任何对象 x,表达式 x.clone() != x、x.clone().getClass() == x.getClass() 以及 x.clone().equals(x) 的值都将为 true,但都不是绝对的。(equals方法应覆盖为比较对象中的字段才能得到true,默认实现是比较对象地址,结果永远为false)

+

按照约定,clone方法返回的对象应该通过调用super.clone() 来获得。如果一个类和它的所有父类(Object类除外)都遵守这个约定,表达式 x.clone().getClass() == x.getClass() 则为 true

+

按照约定,返回的对象应该独立于被克隆的对象。为了实现这种独立性,可能需要在super.clone() 前修改对象的一个或多个字段

+

这种机制有点类似于构造方法链,只是没有强制执行:

+
    +
  • 如果一个类的clone方法返回的实例不是通过调用 super.clone() 而是通过调用构造函数获得的,编译器不会报错,但是如果这个类的子类调用super.clone(),由此产生的对象将是错误的,影响子类clone方法正常工作
  • +
  • 如果覆盖clone方法的类是final修饰的,那么可以忽略这个约定,因为不会有子类
  • +
  • 如果一个final修饰的类的clone方法不调用super.clone()。该类没有理由实现Cloneable接口,因为它不依赖于Object的clone方法
  • +
+

覆盖clone方法

+
    +
  1. 如果类是不可变的,不要提供clone方法
  2. +
  3. 如果类的字段都是基本类型或不可变对象的引用,那么直接这个类的clone方法直接调用super.clone()即可
  4. +
  5. 如果类的字段包含可变对象的引用,需要递归调用可变对象的深拷贝方法
  6. +
+

一些细节

+
    +
  1. +

    和构造函数一样,不要在clone方法中调用可覆盖方法。如果clone方法调用一个在子类中被覆盖的方法,这个方法将在子类修复其在克隆中的状态之前执行,很可能导致克隆和原始对象的破坏。

    +
  2. +
  3. +

    Object的clone方法被声明为抛出CloneNotSupportedException,但是覆盖方法时 try-catch 异常就行,不抛出受检查异常的方法更容易使用

    +
  4. +
  5. +

    设计可继承的类时不要实现 Cloneable接口(如果实现了,子类就必须对外提供clone方法)。你可以选择通过实现一个功能正常的受保护克隆方法来模拟 Object 的行为,该方法声明为抛出 CloneNotSupportedException。这给子类实现 Cloneable 或不实现 Cloneable 的自由。或者,你可以选择不实现一个有效的克隆方法,并通过提供以下退化的克隆实现来防止子类实现它:

    +
    // clone method for extendable class not supporting Cloneable
    +@Override
    +protected final Object clone() throws CloneNotSupportedException {
    +    throw new CloneNotSupportedException();
    +}
    +
    +
  6. +
  7. +

    如果你编写了一个实现了 Cloneable 接口的线程安全类,请记住它的 clone 方法必须正确同步,就像其他任何方法一样。

    +
  8. +
+

总结

+

回顾一下,所有实现 Cloneable 接口的类都应该使用一个返回类型为类本身的公有方法覆盖 clone。这个方法应该首先调用 super.clone(),然后「修复」任何需要「修复」的字段。通常,这意味着复制任何包含对象内部「深层结构」的可变对象,并将克隆对象对这些对象的引用替换为对其副本的引用。虽然这些内部副本通常可以通过递归调用 clone 来实现,但这并不总是最好的方法。如果类只包含基本数据类型的字段或对不可变对象的引用,那么很可能不需要修复任何字段。这条规则也有例外。例如,表示序列号或其他唯一 ID 的字段需要修复,即使它是基本数据类型或不可变的。

+

搞这么复杂真的有必要吗?答案是否定的。如果你扩展了一个已经实现了 Cloneable 接口的类,那么除了实现行为良好的 clone 方法之外,你别无选择。否则,最好提供对象复制的替代方法。一个更好的对象复制方法是提供一个复制构造函数或复制工厂。复制构造函数是一个简单的构造函数,它接受单个参数,其类型是包含构造函数的类,例如

+
// Copy constructor
+public Yum(Yum yum) { ... };
+
+

复制工厂与复制构造函数的静态工厂(Item-1)类似:

+
// Copy factory
+public static Yum newInstance(Yum yum) { ... };
+
+

复制构造函数方法及其静态工厂变体与克隆方法相比有许多优点:

+
    +
  1. 它们不依赖于易发生风险的语言外对象创建机制(Object的clone方法是native的);
  2. +
  3. 他们不要求无法强制执行的约定(clone方法一定要先调用super.clone());
  4. +
  5. 它们与 final 字段不冲突(clone方法不能修改final字段);
  6. +
  7. 它们不会抛出不必要的 checked 异常;
  8. +
  9. 而且不需要强制类型转换。
  10. +
  11. 可以提供类型转换构造函数
  12. +
+

考虑到与 Cloneable 相关的所有问题,新的接口不应该继承它,新的可扩展类不应该实现它。通常,复制功能最好由构造函数或工厂提供。这个规则的一个明显的例外是数组,最好使用 clone 方法来复制数组。

+

14. 考虑实现 Comparable 接口

+

与本章讨论的其它方法不同,compareTo 方法不是 Object 中声明的,而是 Comparable 接口中的唯一方法。一个类实现Comparable,表明实例具有自然顺序(字母或数字或时间顺序)。Java 库中的所有值类以及所有枚举类型(Item-34)都实现了 Comparable接口

+

compareTo方法约定

+

compareTo 方法的一般约定类似于 equals 方法:
+将一个对象与指定对象进行顺序比较。当该对象小于、等于或大于指定对象时,对应返回一个负整数、零或正整数。如果指定对象的类型阻止它与该对象进行比较,则抛出 ClassCastException

+

在下面的描述中, sgn(expression) 表示数学中的符号函数,它被定义为:根据传入表达式的值是负数、零或正数,对应返回-1、0或1。

+
    +
  • 实现类必须确保所有 x 和 y 满足 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))(这意味着 x.compareTo(y) 当且仅当 y.compareTo(x) 抛出异常时才抛出异常)
  • +
  • 实现类还必须确保关系是可传递的:(x.compareTo(y) > 0 && y.compareTo(z) > 0) 意味着 x.compareTo(z) > 0
  • +
  • 最后,实现类必须确保 x.compareTo(y) == 0 时,所有的 z 满足 sgn(x.compareTo(z)) == sgn(y.compareTo(z))
  • +
  • 强烈建议 (x.compareTo(y)== 0) == (x.equals(y)) 成立,但这不是必需的。一般来说,任何实现 Comparable 接口并违法此条件的类都应该清除地注明这一事实。推荐的表述是“Note: This class has a natural ordering that is inconsistent with equals.”
  • +
+

与equals方法不同,equals方法入参是Object类型,而compareTo方法不需要和不同类型的对象比较:当遇到不同类型的对象时,允许抛出ClassCastException

+

就像违反 hashCode 约定的类可以破坏依赖 hash 的其他类一样,违反 compareTo 约定的类也可以破坏依赖 Comparable 的其他类。依赖 Comparable 的类包括排序集合 TreeSet 和 TreeMap,以及实用工具类 Collections 和 Arrays,它们都包含搜索和排序算法。

+

compareTo的约定和equals约定有相同的限制:反身性、对称性和传递性。如果要向实现 Comparable 的类中添加值组件,不要继承它;编写一个不相关的类,其中包含第一个类的实例。然后提供返回所包含实例的「视图」方法。这使你可以自由地在包含类上实现你喜欢的任何 compareTo 方法,同时允许它的客户端在需要时将包含类的实例视为被包含类的实例。

+

compareTo 约定的最后一段是一个强烈建议而不是要求,它只是简单地说明了 compareTo 方法所施加地同等性检验通常应该与 equals 方法返回相同的结果。如果一个类的 compareTo 方法强加了一个与 equals 不一致的顺序,那么这个类仍然可以工作,但是包含该类元素的有序集合可能无法遵守集合接口(Collection、Set 或 Map)的一般约定。这是因为这些接口的一般约定是根据 equals 方法定义的,但是有序集合使用 compareTo 代替了 equals 实施同等性建议,这是需要注意的地方

+

例如,考虑 BigDecimal 类,它的 compareTo 方法与 equals 不一致。如果你创建一个空的 HashSet 实例,然后添加 new BigDecimal(“1.0”) 和 new BigDecimal(“1.00”),那么该 HashSet 将包含两个元素,因为添加到该集合的两个 BigDecimal 实例在使用 equals 方法进行比较时结果是不相等的。但是,如果你使用 TreeSet 而不是 HashSet 执行相同的过程,那么该集合将只包含一个元素,因为使用 compareTo 方法比较两个 BigDecimal 实例时结果是相等的。(有关详细信息,请参阅 BigDecimal 文档。)

+

编写 compareTo 方法

+
    +
  1. +

    递归调用引用字段的 compareTo 方法。如果引用字段没有实现 Comparable,或者需要一个非标准的排序,那么应使用比较器

    +
  2. +
  3. +

    基本字段类型使用包装类的静态 compare 方法比较

    +
  4. +
  5. +

    从最重要字段开始比较

    +
  6. +
  7. +

    考虑使用 java.util.Comparator接口的比较器构造方法

    +
  8. +
+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/07/07/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\270\211\357\274\211/index.html" "b/2022/07/07/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\270\211\357\274\211/index.html" new file mode 100644 index 0000000..8a78a19 --- /dev/null +++ "b/2022/07/07/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\270\211\357\274\211/index.html" @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + Effective-Java学习笔记(三) | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Effective-Java学习笔记(三) +

+ + + + +
+
+ +

第四章 类和接口

+

15. 尽量减少类和成员的可访问性

+

信息隐藏的作用

+
    +
  1. 将API与实现完全分离。组件之间只能通过它们的API进行通信,而不知道彼此的内部工作方式。
  2. +
  3. 解耦系统的组件,允许它们被独立开发、测试、优化、使用、理解和修改。
  4. +
  5. 增加了软件的复用性,降低了构建大型系统的风险,即使系统没有成功,单个组件也可能被证明是成功的
  6. +
+

访问控制机制

+

访问控制机制指定了类、接口和成员的可访问性。

+
    +
  1. +

    对于顶级(非嵌套)类和接口,只有两个可能的访问级别:包私有和公共。如果用 public 修饰符声明,它将是公共的,即API的一部分,修改会损害客户端;否则,它将是包私有的。

    +
  2. +
  3. +

    如果包私有顶级类或接口只被一个类使用,那么可以考虑变成这个类的私有静态嵌套类(Item-24)

    +
  4. +
  5. +

    对于成员(字段、方法、嵌套类和嵌套接口),有四个访问级别,这里按可访问性依次递增的顺序列出:

    +
      +
    • 私有,成员只能从声明它的顶级类内部访问
    • +
    • 包私有,成员可以从声明它的类的包中访问,包私有就是默认访问,即如果没有指定访问修饰符(接口的成员除外,默认情况下,接口的成员是公共的),就是这个访问级别
    • +
    • 保护,成员可以从声明它的类的子类和声明它的类的包中访问
    • +
    • 公共,成员可以从任何地方访问
    • +
    +
  6. +
+

设计类和成员的可访问性

+

总体规则:让每个类或成员尽可能不可访问。换句话说,在不影响软件正常功能时,使用尽可能低的访问级别。

+
    +
  1. +

    仔细设计类的公共API,所有成员声明为私有。私有成员和包私有成员都是类实现的一部分,通常不会影响其导出的API。但是,如果类实现了Serializable(Item-86和Item-87),这些字段可能会「泄漏」到导出的 API 中

    +
  2. +
  3. +

    少用保护成员。保护成员是类导出API的一部分,必须永远支持

    +
  4. +
  5. +

    子类覆盖父类的方法,可访问性不能缩小

    +
  6. +
  7. +

    为了测试,将公共类的成员由私有变为包私有是可以接受的,但是进一步提高可访问性是不可接受的

    +
  8. +
  9. +

    带有公共可变字段的类通常不是线程安全的

    +
  10. +
  11. +

    为了得到不可变数组,不要公开或通过访问器返回长度非零的数组的引用,客户端将能够修改数组的内容。可以将公共数组设置为私有,并添加一个公共不可变 List;或者,将数组设置为私有,并添加一个返回私有数组副本的公共方法:

    +
  12. +
+

总之,你应该尽可能减少程序元素的可访问性(在合理的范围内)。在仔细设计了一个最小的公共 API 之后,你应该防止任何游离的类、接口或成员成为 API 的一部分。除了作为常量的公共静态 final 字段外,公共类应该没有公共字段。确保public static final 字段引用的对象是不可变的。

+

16. 在公共类中,使用访问器方法,而不是公共字段

+

访问器方法的作用

+

如果类是公共的,即可以在包外访问,提供访问器方法可以维持类内部数据表示形式的灵活性

+

一些建议

+
    +
  1. 如果类是包私有或者是私有嵌套类,公开数据字段比提供访问器方法更清晰
  2. +
  3. 公共类公开不可变字段的危害会小一点,但仍不建议公开字段
  4. +
+

17. 减少可变性

+

不可变类是实例创建后不能被修改的类。Java库包含许多不可变的类,包括 String、基本类型的包装类、BigInteger和BigDecimal。

+

创建不可变类的规则

+

要使类不可变,请遵守以下5条规则:

+
    +
  1. 不要提供修改对象状态的方法
  2. +
  3. 确保类不能被继承。这可以防止可变子类损害父类的不可变。防止子类化通常用 final 修饰父类
  4. +
  5. 所有字段用 final 修饰。
  6. +
  7. 所有字段设为私有
  8. +
  9. 确保对任何可变组件的独占访问。如果你的类有任何引用可变对象的字段,请确保该类的客户端无法获得对这些对象的引用。
  10. +
+

不可变类的优点

+
    +
  1. 不可变对象线程安全,复用程度高开销小,不需要防御性拷贝
  2. +
  3. 不仅可以复用不可变对象,还可以复用它们的内部实现
  4. +
  5. 不可变对象很适合作为其它对象的构建模块,例如 Map 的键和 Set 的元素
  6. +
  7. 不可变对象自带提供故障原子性
  8. +
+

不可变类的缺点

+
    +
  1. +

    不可变类的主要缺点是每个不同的值都需要一个单独的对象。不可变类BigInteger的flipBit方法会创建一个和原来对象只有一个bit不同的新对象。可变类BigSet可以在固定时间内修改对象单个bit

    +
  2. +
  3. +

    如果执行多步操作,在每一步生成一个新对象,最终丢弃除最终结果之外的所有对象,那么性能问题就会被放大。解决方法是使用伴随类,如果能预测客户端希望在不可变类上执行哪些复杂操作就可以使用包私有可变伴随可变类(BigInteger和内部的伴随类);否则提供一个公共可变伴随类,例如String和它的伴随类StringBuilder

    +
  4. +
+

设计不可变类

+
    +
  1. 为了保证类不被继承,可以用final修饰类,也可以将构造函数变为私有或包私有,通过静态工厂提供对象
  2. +
  3. BigInteger 和 BigDecimal没有声明为final,为了确保你的类依赖是不可变的BigInteger或BigDecimal,你需要检查对象类型,如果是BigInteger或BigDecimal的子类,那必须防御性复制
  4. +
  5. 适当放松不可变类所有字段必须是 final 的限制可以提供性能,例如,用可变字段缓存计算结果,例如 hashCode 方法在第一次调用时缓存了hash
  6. +
  7. 如果你选择让不可变类实现 Serializable,并且该类包含一个或多个引用可变对象的字段,那么你必须提供一个显式的 readObject 或 readResolve 方法,或者使用 ObjectOutputStream.writeUnshared 或 ObjectInputStream.readUnshared 方法
  8. +
+

18. 优先选择组合而不是继承

+

在同一个包中使用继承是安全的,因为子类和父类的实现都由相同程序员控制。在对专为继承而设计和有文档的类时使用继承也是安全的(Item-19)。然而,对普通的非抽象类进行跨包继承是危险的。与方法调用不同,继承破坏了封装性。换句话说,子类的功能正确与否依赖于它的父类的实现细节。父类的实现可能在版本之间发生变化,如果发生了变化,子类可能会崩溃,即使子类的代码没有被修改过。因此,子类必须与其父类同步更新,除非父类是专门为继承的目的而设计的,并具有很明确的文档说明。

+

继承的风险

+
    +
  1. 子类覆盖父类的多个方法,父类的多个方法之间有调用关系,因为多态,父类方法在调用其它父类方法时会调用到子类的方法
  2. +
  3. 父类可以添加新方法,新方法没有确保在添加的元素满足断言,子类没有覆盖这个方法,导致调用这个方法时添加了非法元素
  4. +
  5. 父类添加了新方法,但是子类继承原来的父类时也添加了相同签名和不同返回类型的方法,这时子类不能编译,如果签名和返回类型都相同,必须声明覆盖
  6. +
+

组合

+

为新类提供一个引用现有类实例的私有字段,这种设计称为组合,因为现有的类是新类的一个组件。新类中的每个实例方法调用现有类实例的对应方法,并返回结果,这称为转发。比较好的写法是包装类+转发类。

+

总结

+

只有子类确实是父类的子类型的情况下,继承才合适。换句话说,两个类 A、B 之间只有 B 满足「is-a」关系时才应该扩展 A。如果你想让 B 扩展 A,那就问问自己:每个 B 都是 A 吗?如果不能对这个问题给出肯定回答,B 不应该扩展 A;如果答案是否定的,通常情况下,B 应该包含 A 的私有实例并暴露不同的 API:A 不是 B 的基本组成部分,而仅仅是其实现的一个细节。

+

19. 继承要设计良好并且有文档,否则禁止使用

+

必须精确地在文档中描述覆盖任何方法的效果。文档必须指出方法调用了哪些可覆盖方法、调用顺序以及每次调用的结果如何影响后续处理过程。描述由 Javadoc 标签 @implSpec 生成

+

20. 接口优于抽象类

+

Java 有两种机制来定义允许多重实现的类型:接口和抽象类。

+

接口优势

+
    +
  1. +

    可以定义 mixin(混合类型)
    +接口是定义 mixin(混合类型)的理想工具。粗略的说,mixin 是类除了「基本类型」之外还可以实现的类型,用于声明它提供了一些可选的行为。例如,Comparable 是一个 mixin 接口,它允许类的实例可以与其他的可相互比较的对象进行排序。这样的接口称为 mixin,因为它允许可选功能「混合」到基本类型中。抽象类不能用于定义 mixin,原因是:一个类不能有多个父类,而且在类层次结构中没有插入 mixin 的合理位置。‘

    +
  2. +
  3. +

    允许构造非层次化类型框架
    +如果系统中有 n 个属性(例如唱、跳、rap),如果每个属性组合都封装成一个抽象类,组成一个层次化的类型框架,总共有2n2^n个类,而接口只需要 n 个

    +
  4. +
+

接口劣势

+
    +
  1. 接口不能给 equals 和 hashCode 方法提供默认实现
  2. +
  3. 接口不允许包含实例字段或者非公共静态成员(私有静态方法除外)
  4. +
+

结合接口和抽象类的优势

+

模板方法模式:接口定义类型,提供默认方法,抽象类(骨架实现类)实现接口其余方法。继承骨架实现类已经完成直接实现接口的大部分工作。

+

按照惯例,骨架实现类称为 AbstractInterface,其中 Interface 是它们实现的接口的名称。例如 Collections Framework 提供了一个骨架实现来配合每个主要的集合接口:AbstractCollection、AbstractSet、AbstractList 和 AbstractMap。可以说,把它们叫做 SkeletalCollection、SkeletalSet、SkeletalList 和 SkeletalMap 更合理,但 Abstract 的用法现在已经根深蒂固。

+
编写骨架实现类的过程
+

研究接口有哪些方法,哪些方法可以提供默认实现,如果都可以提供默认实现就不需要骨架实现类,否则,声明一个实现接口的骨架实现类,实现所有剩余的接口方法

+

21. 为后代设计接口

+

Java 8 之前,往接口添加方法会导致实现它的类缺少方法,编译错误。Java 8 添加了默认方法,目的是允许向现有接口添加方法,但是向现有接口添加新方法有风险。

+

默认方法的风险

+
    +
  1. 接口实现类需要同步调用每个方法,但是没有覆盖接口新加入的默认方法,导致调用默认方法时出现 ConcurrentModificationException
  2. +
  3. 接口的现有实现类可以在没有错误或警告的情况下通过编译,但是运行时会出错
  4. +
+

22. 接口只用于定义类型

+

接口只用于定义类型。类实现接口表明客户端可以用类的实例做什么。将接口定义为任何其他目的都是不合适的。

+

有一个反例是常量接口,接口内只包含 public static final 字段,它的问题在于类使用什么常量是实现细节,而实现常量接口会导致实现细节泄露到 API 中。

+

导出常量,有几个合理的选择。

+
    +
  1. 如果这些常量与现有的类或接口紧密绑定,则应该将它们添加到类或接口。例如,所有数值包装类,比如 Integer 和 Double,都导出 MIN_VALUE 和 MAX_VALUE 常量。
  2. +
  3. 枚举或者不可实例化的工具类,使用工具类的常量推荐静态导入
  4. +
+

23. 类层次结构优于带标签的类

+

标签类

+

类的实例有两种或两种以上的样式,并且包含一个标签字段来表示实例的样式。标签类有许多缺点,可读性差、内存占用多、容易出错、添加新样式复杂

+

类层次结构

+

标签类只是类层次结构的简易模仿。要将已标签的类转换为类层次结构,

+
    +
  1. 抽取标签类都有的方法、字段到一个抽象类中
  2. +
  3. 继承抽象类实现都有的方法和子类特有的方法
  4. +
+

24. 静态成员类优于非静态成员类

+

嵌套类是在另一个类中定义的类。嵌套类应该只为它的外部类服务。如果嵌套类在其它环境中有用,那么它应该是顶级类。有四种嵌套类:静态成员类、非静态成员类、匿名类和局部类。除了静态成员类,其它嵌套类被称为内部类。

+

静态成员类

+

静态成员类是最简单的嵌套类。最好把它看作是一个普通的类,只是碰巧在另一个类中声明而已,并且可以访问外部类的所有成员,甚至是那些声明为 private 的成员。静态成员类是其外部类的静态成员,并且遵守与其它静态成员相同的可访问性规则。如果声明为私有,则只能在外部类中访问,等等。

+
静态成员类作用
+
    +
  1. 作为公共的辅助类,只有与它的外部类一起使用时才有意义,例如 Calculator 类和公有静态成员类 Operation 枚举
  2. +
  3. 表示由其外部类表示的组件。例如,Map实现类内部的Entry类。Entry对象不需要访问 Map 。因此,使用非静态成员类来表示 entry 是浪费,私有静态成员类是最好的。
  4. +
+

非静态成员类

+

从语法上讲,静态成员类和非静态成员类之间的唯一区别是静态成员类在其声明中具有修饰符 static。尽管语法相似,但这两种嵌套类有很大不同。非静态成员类的每个实例都隐式地与外部类的实例相关联,非静态成员类的实例方法可以调用外部实例上的方法,或者使用受限制的 this (父类.this)构造获得对外部实例的引用。如果嵌套类的实例可以独立于外部类的实例存在,那么嵌套类必须是静态成员类;非静态成员类的实例依赖外部类的实例

+

非静态成员类实例与外部类实例之间的关联是在创建成员类实例时建立的,之后无法修改。通常,关联是通过从外部类的实例方法中调用非静态成员类构造函数自动建立的。使用 enclosingInstance.new MemberClass(args) 表达式手动建立关联是可能的,尽管这种情况很少见。正如你所期望的那样,关联占用了非静态成员类实例中的空间,并增加了构造时间。

+

如果声明的成员类不需要访问外部类的实例,那么应始终在声明中添加 static 修饰符,如果省略这个修饰符,每个实例都有一个隐藏的对其外部实例的额外引用。存储此引用需要时间和空间,更糟糕的是,外部类可能不能被垃圾回收。

+
非静态成员类作用
+

非静态成员类的一个常见用法是定义一个 Adapter,它允许外部类的实例被视为某个不相关类的实例。例如,Map 接口的实现类通常使用非静态成员类来实现它们的集合视图, Set 和 List,通常使用非静态成员类来实现它们的迭代器

+

匿名类

+

匿名类没有名称。它不是外部类的成员。它不是与其它成员一起声明的,而是在使用时同时声明和实例化。匿名类可以在代码中用在任何一个可以用表达式的地方。当且仅当它们出现在非静态环境(没有写在静态方法里面)时,匿名类才持有外部类实例。但是,即使它们出现在静态环境中,它们也不能有除常量以外的任何静态成员。

+

匿名类的使用有很多限制。你只能在声明它们的时候实例化,你不能执行 instanceof 测试,也不能执行任何其它需要命名类的操作。你不能声明一个匿名类来实现多个接口或继承一个类并同时实现一个接口。匿名类的使用者除了从父类继承的成员外,不能调用任何成员。因为匿名类出现在表达式中,所以它们必须保持简短——大约10行或更短,否则会影响可读性。

+

匿名类作用

+

在 lambda 表达式被添加的 Java 之前,匿名类是动态创建小型函数对象和进程对象的首选方法,但 lambda 表达式现在是首选方法(Item-42)。匿名类的另一个常见用法是实现静态工厂方法(参见 Item-20 中的 intArrayAsList 类)

+

局部类

+

局部类是四种嵌套类中最不常用的。局部类几乎可以在任何能够声明局部变量的地方使用,并且遵守相同的作用域规则。局部类具有与其它嵌套类相同属性。与成员类一样,它们有名称,可以重复使用呢。与匿名类一样,他们只有在非静态环境中定义的情况下才具有外部类实例,而且它们不能包含静态静态成员。和匿名类一样,它们应该保持简短,以免损害可读性。

+

嵌套类总结

+

简单回顾一下,有四种不同类型的嵌套类,每一种都有自己的用途。如果嵌套的类需要在单个方法之外可见,或者太长,不适合放入方法中,则使用成员类。除非成员类的每个实例都需要引用其外部类实例,否则让它保持静态。假设嵌套类属于方法内部,如果你只需要从一个位置创建实例,并且存在一个能够描述类的现有类型,那么将其设置为匿名类;否则,将其设置为局部类。

+

25. 源文件仅限有单个顶层类

+

虽然 Java 编译器允许你在单个源文件中定义多个顶层类,但这样做没有任何好处,而且存在重大风险。这种风险源于这样一个事实:在源文件中定义多个顶层类使得为一个类提供多个定义成为可能。所使用的定义受源文件传给编译器的顺序的影响。

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/07/14/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\345\233\233\357\274\211/index.html" "b/2022/07/14/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\345\233\233\357\274\211/index.html" new file mode 100644 index 0000000..915a7c5 --- /dev/null +++ "b/2022/07/14/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\345\233\233\357\274\211/index.html" @@ -0,0 +1,1075 @@ + + + + + + + + + + + + + + + + + + + + Effective-Java学习笔记(四) | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Effective-Java学习笔记(四) +

+ + + + +
+
+ +

第五章 泛型

+

26. 不要使用原始类型

+

泛型和原始类型

+

声明中具有一个或多个类型参数的类或接口就是泛型类或泛型接口。泛型类和泛型接口统称为泛型。

+

每个泛型都定义了一个原始类型,它是没有任何相关类型参数的泛型的名称。例如,List<E> 对应的原始类型是 List。原始类型的行为就好像所有泛型信息从类型声明中删除了一样。它们的存在主要是为了与之前的代码兼容。

+

泛型优势

+

泛型可以帮助编译器在编译过程中发现潜在的类型转换异常,而原始类型不行。

+

泛型的子类型规则

+

原始类型 List 和 参数化类型 List<Object> 都可以保存任何类型的对象 ,但是不能把其它泛型对象 , 例如List<String>对象赋给List<Object>引用而可以赋给 List 引用。泛型有子类型规则,List<String> 是原始类型 List 的子类型,而不是参数化类型 List<Object> 的子类型(Item-28)。假如List<String>可以是List<Object>的子类型,那么List<String>的对象赋给List<Object>,通过List<Object>插入非String元素,违反了List<String>只放String的约定

+

无界通配符

+

如果你想使用泛型,但不知道或不关心实际的类型参数是什么,那么可以使用无界通配符 ? 代替。例如,泛型集合 Set<E> 的无界通配符类型是 Set<?>。它是最通用的参数化集合类型,能够容纳任何集合

+

无界通配符类型 Set<?> 和原始类型 Set 之间的区别在于通配符类型是安全的,而原始类型不是。将任何元素放入具有原始类型的集合中,很容易破坏集合的类型一致性;而无界通配符类型不能放入元素(除了null)

+
Set<Integer> integerSet = new HashSet<>();
+// 无界通配符类型Set<?>可以引用任何Set<E>和Set,但是不能往里面放除了null的元素
+Set<?> set1 = integerSet;
+// 原始类型Set也可以引用任何Set<E>和Set,但是可以往里面添加元素,会存在类型转换异常
+Set set2 = integerSet;
+
+

使用泛型而不用原始类型的例外

+
    +
  1. 类字面量。该规范不允许使用参数化类型(尽管它允许数组类型和基本类型)。换句话说,List.classString[].classint.class 都是合法的,但是 List<String>.classList<?>.class 不是。
  2. +
  3. instanceof 运算符。由于泛型信息在运行时被删除,因此在不是无界通配符类型之外的参数化类型使用 instanceof 操作符是非法的。使用无界通配符类型代替原始类型不会以任何方式影响 instanceof 运算符的行为。在这种情况下,尖括号和问号只是多余的。下面的例子是使用通用类型 instanceof 运算符的首选方法:
  4. +
+
// Legitimate use of raw type - instanceof operator
+if (o instanceof Set) { // Raw type
+    Set<?> s = (Set<?>) o; // Wildcard type
+    ...
+}
+
+

总之,使用原始类型可能会在运行时导致异常,所以不要轻易使用它们。它们仅用于与引入泛型之前的遗留代码进行兼容。快速回顾一下,Set<Object> 是一个参数化类型,表示可以包含任何类型的对象的集合,Set<?> 是一个无界通配符类型,表示只能包含某种未知类型的对象的集合,Set 是一个原始类型,它没有使用泛型。前两个是安全的,后一个不安全

+

为便于参考,本条目中介绍的术语(以及后面将要介绍的一些术语)总结如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TermExampleItem
Parameterized typeList<String>Item-26
Actual type parameterStringItem-26
Generic typeList<E>Item-26, Item-29
Formal type parameterEItem-26
Unbounded wildcard typeList<?>Item-26
Raw typeListItem-26
Bounded type parameter<E extends Number>Item-29
Recursive type bound<T extends Comparable<T>>Item-30
Bounded wildcard typeList<? extends Number>Item-31
Generic methodstatic <E> List<E> asList(E[] a)Item-30
Type tokenString.classItem-33
+

27. 消除 unchecked 警告

+

消除所有 unchecked 警告可以确保代码是类型安全的,运行时不会出现 ClassCastException。如果不能消除警告,但是可以证明引发警告的代码是类型安全的,那么可以使用 SuppressWarnings(“unchecked”) 注解来抑制警告。

+

SuppressWarnings注解

+

SuppressWarnings 注解可以用于任何声明中,从单个局部变量声明到整个类。请总是在尽可能小的范围上使用 SuppressWarnings 注解。通常用在一个变量声明或一个非常短的方法或构造函数。不要在整个类中使用 SuppressWarnings。这样做可能会掩盖关键警告。

+

如果你发现自己在一个超过一行的方法或构造函数上使用 SuppressWarnings 注解,那么你可以将其移动到局部变量声明中

+

将 SuppressWarnings 注释放在 return 语句上是非法的,因为它不是声明。你可能想把注释放在整个方法上,但是不要这样做。相反,应该声明一个局部变量来保存返回值并添加注解

+

每次使用 SuppressWarnings(“unchecked”) 注解时,要添加一条注释,说明这样做是安全的。这将帮助他人理解代码,更重要的是,它将降低其他人修改代码而产生不安全事件的几率。如果你觉得写这样的注释很难,那就继续思考合适的方式,你最终可能会发现,unchecked 操作毕竟是不安全的。

+

总之,unchecked 警告很重要。不要忽视他们。每个 unchecked 警告都代表了在运行时发生 ClassCastException 的可能性。尽最大努力消除这些警告。如果不能消除 unchecked 警告,但是可以证明引发该警告的代码是类型安全的,那么可以在尽可能狭窄的范围内使用 @SuppressWarnings(“unchecked”) 注释来抑制警告。在注释中记录你决定抑制警告的理由。

+

28. list 优于数组

+

数组与泛型的区别

+
    +
  1. +

    数组是协变的, 如果 Sub 是 Super 的一个子类型,那么数组类型 Sub[] 就是数组类型 Super[] 的一个子类型。相比之下,泛型是不变的:对于任何两个不同类型 Type1 和 Type2,List<Type1> 既不是 List<Type2> 的子类型,也不是 List<Type2> 的父类型。

    +
  2. +
  3. +

    数组是具体化的。这意味着数组在运行时知道并强制执行他们的元素类型。如前所述,如果试图将 String 元素放入一个 Long 类型的数组中,就会得到 ArrayStoreException。相比之下,泛型是通过擦除来实现的,这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)元素类型信息。擦除允许泛型与不使用泛型的遗留代码自由交互操作(Item-26),确保在 Java 5 中平稳过渡

    +
  4. +
+

泛型数组的创建是非法的

+

由于这些基本差异,数组和泛型不能很好地混合。例如,创建泛型、参数化类型或类型参数的数组是非法的。因此,这些数组创建表达式都不是合法的:new List<E>[]、new List<String>[]、new E[]。所有这些都会在编译时导致泛型数组创建错误。

+

考虑以下代码片段:

+
// Why generic array creation is illegal - won't compile!
+List<String>[] stringLists = new List<String>[1]; // (1)
+List<Integer> intList = List.of(42); // (2)
+Object[] objects = stringLists; // (3)
+objects[0] = intList; // (4)
+String s = stringLists[0].get(0); // (5)
+
+

假设创建泛型数组的第 1 行是合法的。第 2 行创建并初始化一个包含单个元素的 List<Integer>。第 3 行将 List<String> 数组存储到 Object 类型的数组变量中,这是合法的,因为数组是协变的。第 4 行将 List<Integer> 存储到 Object 类型的数组的唯一元素中,这是成功的,因为泛型是由擦除实现的:List<Integer> 实例的运行时类型是 List,List<String>[] 实例的运行时类型是 List[],因此这个赋值不会生成 ArrayStoreException。现在我们有麻烦了。我们将一个 List<Integer> 实例存储到一个数组中,该数组声明只保存 List<String> 实例。在第 5 行,我们从这个数组的唯一列表中检索唯一元素。编译器自动将检索到的元素转换为 String 类型,但它是一个 Integer 类型的元素,因此我们在运行时得到一个 ClassCastException。为了防止这种情况发生,第 1 行(创建泛型数组)必须生成编译时错误。

+

用List替代数组

+

当你在转换为数组类型时遇到泛型数组创建错误或 unchecked 强制转换警告时,通常最好的解决方案是使用集合类型 List<E>,而不是数组类型 E[]。你可能会牺牲一些简洁性和性能,但作为交换,你可以获得更好地类型安全性和互操作性。

+

总之,数组和泛型有非常不同的类型规则。数组是协变的、具体化的;泛型是不可变的和可被擦除的。因此,数组提供了运行时类型安全性,而不是编译时类型安全性,对于泛型来说相反。一般来说,数组和泛型不能很好的混合,如果你发现将它们混合在一起并得到编译时错误或警告,那么你的第一个反应应该是将数组替换为 list。

+

29. 优先使用泛型

+

编写泛型

+

在将原始类型修改成泛型时, 可能会遇到不能创建泛型数组的问题,有两种解决方法

+
    +
  1. 创建 Object 数组并将其强制转换为 E[] 类型(字段的类型是 E[] ,将Object数组强转成 E[] 可以成功,但是方法返回 E[]给客户端,由编译器添加隐式强转就会失败,因为隐式强转是将Object数组转成声明的具体类型数组)。现在,编译器将发出一个警告来代替错误。这种用法是合法的,但(一般而言)不是类型安全的。编译器可能无法证明你的程序是类型安全的,但你可以。你必须说服自己,unchecked 的转换不会损害程序的类型安全性。所涉及的数组(元素)存储在私有字段中,从未返回给客户端或传递给任何其它方法(传到外面就会发生隐式强转)。添加元素时,元素也是 E 类型,因此 unchecked 的转换不会造成任何损害。一旦你证明了 unchecked 的转换是安全的,就将警告限制在尽可能小的范围内(Item-27)。
  2. +
  3. 将字段的类型从 E[] 更改为 Object[]。编译器会产生类似的错误和警告,处理方法也和上面类似
  4. +
+

方法1优势:可读性更好,因为数组声明为 E[] 类型,这清楚地表明它只包含 E 的实例。它也更简洁,只需要在创建数组的地方做一次强转,其它地方读取数组元素不用强转成 E 类型
+方法1劣势:会造成堆污染(Item-32):数组的运行时类型与其编译时类型不匹配(除非 E 恰好是 Object)

+

为啥要创建泛型数组

+

Item-28 鼓励优先使用列表而不是数组。但是在泛型中使用列表并不总是可能或可取的。Java 本身不支持列表,因此一些泛型(如ArrayList)必须在数组之上实现。其它泛型(如HashMap)用数组实现来提高性能

+

总之,泛型比需要在客户端代码中转换的类型更安全、更容易使用。在设计新类型时,请确保客户端可以在不使用类型转换的情况下使用它们。这通常意味着使类型具有通用性。如果你有任何应该是泛型但不是泛型的现有类型,请对它们进行泛化。这将使这些类型的新用户在不破坏现有客户端的情况下更容易使用。

+

30. 优先使用泛型方法

+

类可以是泛型的,方法也可以是泛型的

+

编写泛型方法

+

编写泛型方法类似于编写泛型。类型参数列表声明类型参数,它位于方法的修饰符与其返回类型之间。例如,类型参数列表为 <E>,返回类型为 Set<E>

+
// Generic method
+public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
+    Set<E> result = new HashSet<>(s1);
+    result.addAll(s2);
+    return result;
+}
+
+

至少对于简单的泛型方法,这就是(要注意细节的)全部。该方法编译时不生成任何警告,并且提供了类型安全性和易用性。这里有一个简单的程序来演示。这个程序不包含转换,编译时没有错误或警告:

+
// Simple program to exercise generic method
+public static void main(String[] args) {
+    Set<String> guys = Set.of("Tom", "Dick", "Harry");
+    Set<String> stooges = Set.of("Larry", "Moe", "Curly");
+    Set<String> aflCio = union(guys, stooges);
+    System.out.println(aflCio);
+}
+
+

当你运行程序时,它会打印出 [Moe, Tom, Harry, Larry, Curly, Dick]。(输出元素的顺序可能不同)。

+

union 方法的一个限制是,所有三个集合(输入参数和返回值)的类型必须完全相同。你可以通过使用有界通配符类型(Item-31)使方法更加灵活。

+

31. 使用有界通配符增加 API 的灵活性

+

PECS

+

为了获得满足里氏代换原则,应对表示生产者或消费者入参使用通配符类型。如果输入参数既是生产者优势消费者,使用精确的类型

+

PECS助记符:PECS 表示生产者应使用 extends,消费者应使用 super。换句话说,如果参数化类型表示 T 生产者,则使用 <? extends T>;如果它表示一个 T 消费者,则使用 <? super T>不要使用有界通配符类型作为返回类型。 它将强制用户在客户端代码中使用通配符类型,而不是为用户提供额外的灵活性

+

一个例子

+

接下来让我们将注意力转移到 Item-30 中的 max 方法,以下是原始声明:

+
public static <T extends Comparable<T>> T max(List<T> list)
+
+

下面是使用通配符类型的修正声明:

+
public static <T extends Comparable<? super T>> T max(List<? extends T> list)
+
+

这里使用了两次 PECS。第一次是参数列表,list作为生产者生成 T 的实例,所以我们将类型从 List<T> 更改为 List<? extends T>(个人认为这里没有必要改,调用一次max方法只能传入一个类型的List,如果是两个参数,第一个参数的类型决定了T,第二个参数的类型如果是List<? extends T>,那么类型参数必须是T或T的子类。但是如果是泛型类声明的T,那这个改动是有意义的)。第二次是是类型参数 T。这是我们第一次看到通配符应用于类型参数。最初,T 被指定为继承 Comparable<T>,但是 Comparable<T> 消费 T 实例。因此,将参数化类型 Comparable<T> 替换为有界通配符类型 Comparable<? super T>,Comparable 始终是消费者,所以一般应优先使用 Comparable<? super T> 而不是 Comparable<T>,比较器也是如此;因此,通常应该优先使用 Comparator<? super T> 而不是 Comparator<T>

+

修订后的 max 声明可能是本书中最复杂的方法声明。增加的复杂性真的能给你带来什么好处吗?是的。下面是一个简单的列表案例,它在原来的声明中不允许使用,但经修改的声明允许:

+
List<ScheduledFuture<?>> scheduledFutures = ... ;
+
+

不能将原始方法声明应用于此列表的原因是 ScheduledFuture 没有实现 Comparable<ScheduledFuture>。相反,它是 Delayed 的一个子接口,继承了 Comparable<Delayed>。换句话说,ScheduledFuture 的实例不仅仅可以与其它 ScheduledFuture 实例进行比较,还可以与任何 Delayed 实例比较,但是原始方法只能和 ScheduledFuture 实例比较。更通俗来说,通配符用于支持不直接实现 Comparable(或 Comparator)但继承了实现 Comparable(或 Comparator)的类型的类型。

+

类型参数和通配符的对偶性

+

类型参数和通配符之间存在对偶性,对偶性指实现方式不同但效果一样。例如,下面是swap方法的两种可能声明,第一个使用无界类型参数(Item-30),第二个使用无界通配符:

+
// Two possible declarations for the swap method
+public static <E> void swap(List<E> list, int i, int j);
+public static void swap(List<?> list, int i, int j);
+
+

这两个声明哪个更好?在公共API中第二个更好,因为它更简单(客户端可以传入原始类型)。传入一个任意列表,该方法交换索引元素,不需要担心类型参数。通常,如果类型参数在方法声明中只出现一次,则用通配符替换它。如果它是一个无界类型参数,用一个无界通配符替换它;如果它是有界类型参数,则用有界通配符替换它。

+

交换的第二个声明有一个问题。下面的实现无法编译, list 的类型是 List<?>,你不能在 List<?> 中放入除 null 以外的任何值。

+
public static void swap(List<?> list, int i, int j) {
+  list.set(i, list.set(j, list.get(i)));
+}
+
+

幸运的是,有一种方法可以实现,而无需求助于不安全的强制类型转换或原始类型。其思想是编写一个私有助手方法来捕获通配符类型。为了捕获类型,helper 方法必须是泛型方法。它看起来是这样的:

+
public static void swap(List<?> list, int i, int j) {
+  swapHelper(list, i, j);
+}
+// Private helper method for wildcard capture
+private static <E> void swapHelper(List<E> list, int i, int j) {
+  list.set(i, list.set(j, list.get(i)));
+}
+
+

swapHelper 方法指导 list 是一个 List<E>。因此,它指导它从这个列表中得到的任何值都是 E 类型的,并且将 E 类型的任何值放入这个列表中都是安全的。这个稍微复杂的实现可以正确编译。它允许我们导出基于通配符的声明,同时在内部利用更复杂的泛型方法。swap 方法的客户端不必面对更复杂的 swapHelper 声明,但它们确实从中受益。值得注意的是,helper 方法具有我们认为对于公共方法过于复杂而忽略的签名。

+

总之,在 API 中使用通配符类型虽然很棘手,但可以使其更加灵活。如果你编写的库将被广泛使用,则必须考虑通配符类型的正确使用。记住基本规则:生产者使用 extends,消费者使用 super(PECS)。还要记住,所有的 comparable 和 comparator 都是消费者

+

32. 明智地合用泛型和可变参数

+

抽象泄露

+

可变参数方法(Item-53)和泛型都是在 Java 5 中添加,因此你可能认为它们能够优雅地交互;可悲的是,它们并不能。可变参数的目的是允许客户端向方法传递可变数量的参数,但这是一个抽象泄漏:当你调用可变参数方法时,将创建一个数组来保存参数;该数组本应是实现细节,却是可见的。因此,当可变参数具有泛型或参数化类型时,会出现令人困惑的编译器警告。

+

不可具体化

+

回想一下 Item-28,不可具体化类型是指其运行时表示的信息少于其编译时表示的信息,并且几乎所有泛型和参数化类型都是不可具体化的。如果方法声明其可变参数为不可具体化类型,编译器将在声明生生成警告。如果方法是在其推断类型不可具体化的可变参数上调用的,编译器也会在调用时生成警告。生成的警告就像这样:

+
warning: [unchecked] Possible heap pollution from parameterized vararg type List<String>
+
+

堆污染

+

当参数化类型的变量引用不属于该类型的对象时,就会发生堆污染。它会导致编译器自动生成的强制类型转换失败,违反泛型系统的基本保证。

+

例如,考虑这个方法,它来自 Item-26,但做了些修改:

+
// Mixing generics and varargs can violate type safety!
+// 泛型和可变参数混合使用可能违反类型安全原则!
+static void dangerous(List<String>... stringLists) {
+    List<Integer> intList = List.of(42);
+    Object[] objects = stringLists;
+    objects[0] = intList; // Heap pollution
+    String s = stringLists[0].get(0); // ClassCastException
+}
+
+

方法声明使用泛型可变参数是合法的

+

这个例子提出了一个有趣的问题:为什么方法声明中使用泛型可变参数是合法的,而显式创建泛型数组是非法的?答案是,带有泛型或参数化类型的可变参数的方法在实际开发中非常有用,因此语言设计人员选择忍受这种不一致性。事实上,Java 库导出了几个这样的方法,包括 Arrays.asList(T... a)Collections.addAll(Collection<? super T> c, T... elements) 以及 EnumSet.of(E first, E... rest)。它们与前面显示的危险方法不同,这些库方法是类型安全的。

+

在 Java 7 之前,使用泛型可变参数的方法的作者对调用点上产生的警告无能为力。使得这些 API 难以使用。用户必须忍受这些警告,或者在每个调用点(Item-27)使用 @SuppressWarnings(“unchecked”) 注释消除这些警告。这种做法乏善可陈,既损害了可读性,也忽略了标记实际问题的警告。

+

在 Java 7 中添加了 SafeVarargs 注释,以允许使用泛型可变参数的方法的作者自动抑制客户端警告。本质上,SafeVarargs 注释构成了方法作者的一个承诺,即该方法是类型安全的。 作为这个承诺的交换条件,编译器同意不对调用可能不安全的方法的用户发出警告。

+

使用SafeVarargs的条件

+
    +
  1. 方法没有修改数组元素
  2. +
  3. 数组的引用没有逃逸(这会使不受信任的代码能够访问数组)
  4. +
+

换句话说,如果可变参数数组仅用于将可变数量的参数从调用方传输到方法(毕竟这是可变参数的目的),那么该方法是安全的。

+
一个反例
+
// UNSAFE - Exposes a reference to its generic parameter array!
+static <T> T[] toArray(T... args) {
+  return args;
+}
+
+

这个方法直接返回了泛型可变参数数组引用,违反了上面的条件2,它可以将堆污染传播到调用堆栈上。

+

考虑下面的泛型方法,该方法接受三个类型为 T 的参数,并返回一个包含随机选择的两个参数的数组:

+

+public static void main(String[] args) {
+  String[] attributes = pickTwo("Good", "Fast", "Cheap");
+}
+
+static <T> T[] pickTwo(T a, T b, T c) {
+  switch(ThreadLocalRandom.current().nextInt(3)) {
+    case 0: return toArray(a, b);
+    case 1: return toArray(a, c);
+    case 2: return toArray(b, c);
+  }
+  throw new AssertionError(); // Can't get here
+}
+
+

这段代码编译时不会生成任何警告,运行时会抛出 ClassCastException,尽管它不包含可见的强制类型转换。你没有看到的是,编译器在 pickTwo 返回的值上生成了一个隐藏的 String[] 转换。转换失败,因为 Object[] 实际指向的数组是Object类型的不是String,强转失败。

+

这个示例的目的是让人明白,让另一个方法访问泛型可变参数数组是不安全的,只有两个例外:将数组传递给另一个使用 @SafeVarargs 正确注释的可变参数方法是安全的,将数组传递给仅计算数组内容的某个函数的非可变方法也是安全的。

+
扩展
+

如果 main 方法直接调用 toArray 方法,不会出现 ClassCastException,为什么?泛型不是被擦除了吗,args在运行时不是一个Object[] 吗?非也,运行时args的类型是Object[],但是指向的数组是 String 类型的, 所以编译器添加的强转是可以成功的。下面的例子里,toArray1方法在字节码层面和toArray3方法是一样的,在调用toArray1方法前会先生成 String[],将引用传递给toArray1,

+
public static void main(String[] args) {
+    String [] arr1 = toArray1("Alice", "Bob", "Cat");
+    // ClassCastException
+    String [] arr2 = toArray2("Alice", "Bob", "Cat");
+    String [] arr3 = toArray3(new String[]{"Alice", "Bob", "Cat"});
+    // ClassCastException,多层调用泛型可变参数数组会导致编译器没有足够信息创建真实类型的对象数组(存疑,从字节码来看也是创建了String[])
+    String[] arr4 = toArray1Crust1("Alice", "Bob", "Cat");
+    String[] arr5 = toArray1Crust2(new String[]{"Alice", "Bob", "Cat"});
+    String[] arr6 = toArray1Crust3(new String[]{"Alice", "Bob", "Cat"});
+    // 泛型方法嵌套没问题
+    String s = toString1("Alice");
+}
+
+static <T> T[] toArray1(T... args) {
+    return args;
+}
+static <T> T[] toArray2(T a, T b, T c) {
+    System.out.println(a.getClass());
+    // 泛型数组实际类型是Object
+    T[] result = (T[]) new Object[3];
+    result[0] = a;
+    result[1] = b;
+    result[2] = c;
+    return result;
+}
+static <T> T[] toArray3(T[] arr){
+    return arr;
+}
+static <T> T[] toArray1Crust1(T...args){
+    return toArray1(args);
+}
+static <T> T[] toArray1Crust2(T[] args){
+    return toArray1(args);
+}
+static String[] toArray1Crust3(String[] args){
+    return toArray1(args);
+}
+
+private static <T> T toString1(T s) {
+    return toString2(s);
+}
+public static <T> T toString2(T s){
+    return s;
+}
+
+

图 1

+
一个正确例子
+

下面是一个安全使用泛型可变参数的典型示例。该方法接受任意数量的列表作为参数,并返回一个包含所有输入列表的元素的序列列表。因为该方法是用 @SafeVarargs 注释的,所以它不会在声明或调用点上生成任何警告:

+
// Safe method with a generic varargs parameter
+@SafeVarargs
+static <T> List<T> flatten(List<? extends T>... lists) {
+  List<T> result = new ArrayList<>();
+  for (List<? extends T> list : lists)
+    result.addAll(list);
+  return result;
+}
+
+

何时使用SafeVarargs

+

决定何时使用 SafeVarargs 注释的规则很简单:在每个带有泛型或参数化类型的可变参数的方法上使用 @SafeVarargs,这样它的用户就不会被不必要的和令人困惑的编译器警告所困扰。

+

请注意,SafeVarargs 注释只能出现在不能覆盖的方法上,因为不可能保证所有可能覆盖的方法都是安全的。在 Java 8 中,注释只能出现在静态方法和final实例方法;在 Java 9 中,它在私有实例方法上也是合法的。

+

使用 SafeVarargs 注释的另一种选择是接受 Item-28 的建议,并用 List 参数替换可变参数(它是一个伪装的数组)。下面是将这种方法应用到我们的 flatten 方法时的效果。注意,只有参数声明发生了更改:

+
// List as a typesafe alternative to a generic varargs parameter
+static <T> List<T> flatten(List<List<? extends T>> lists) {
+  List<T> result = new ArrayList<>();
+  for (List<? extends T> list : lists)
+    result.addAll(list);
+  return result;
+}
+
+

然后可以将此方法与静态工厂方法 List.of 一起使用,以允许可变数量的参数。注意,这种方法依赖于 List.of 声明是用 @SafeVarargs 注释的:

+
audience = flatten(List.of(friends, romans, countrymen));
+
+

这种方法的优点是编译器可以证明该方法是类型安全的。你不必使用 SafeVarargs 注释来保证它的安全性,也不必担心在确定它的安全性时可能出错。主要的缺点是客户端代码比较冗长,可能会比较慢。

+

这种技巧也可用于无法编写安全的可变参数方法的情况,如第 147 页中的 toArray 方法。它的列表类似于 List.of 方法,我们甚至不用写;Java 库的作者为我们做了这些工作。pickTwo 方法变成这样:

+
static <T> List<T> pickTwo(T a, T b, T c) {
+  switch(rnd.nextInt(3)) {
+    case 0: return List.of(a, b);
+    case 1: return List.of(a, c);
+    case 2: return List.of(b, c);
+  }
+  throw new AssertionError();
+}
+
+

main 方法是这样的:

+
public static void main(String[] args) {
+  List<String> attributes = pickTwo("Good", "Fast", "Cheap");
+}
+
+

生成的代码是类型安全的,因为它只使用泛型,而不使用数组(List在运行时没有强转,数组会强转)。

+

总之,可变参数方法和泛型不能很好地交互,并且数组具有与泛型不同的类型规则。虽然泛型可变参数不是类型安全的,但它们是合法的。如果选择使用泛型(或参数化)可变参数编写方法,首先要确保该方法是类型安全的,然后使用 @SafeVarargs 对其进行注释。

+

33. 考虑类型安全的异构容器

+

参数化容器

+

如果你需要存储某种类型的集合,例如存储String的Set,那么Set<String>就足够了,又比如存储 String-Value 键值对,那么Map<String,Integer>就足够了

+

参数化容器的键

+

如果你要存储任意类型的集合。例如,一个数据库行可以有任意多列,我们希望用一个Map保存该行每列元素。那么我们可以使用参数化容器的键

+

例子

+

Favorites 类,允许客户端存储和检索任意多种类型对象。Class 类的对象将扮演参数化键的角色。Class 对象被称为类型标记

+
// Typesafe heterogeneous container pattern - client
+public static void main(String[] args) {
+    Favorites f = new Favorites();
+    f.putFavorite(String.class, "Java");
+    f.putFavorite(Integer.class, 0xcafebabe);
+    f.putFavorite(Class.class, Favorites.class);
+    String favoriteString = f.getFavorite(String.class);
+    int favoriteInteger = f.getFavorite(Integer.class);
+    Class<?> favoriteClass = f.getFavorite(Class.class);
+    System.out.printf("%s %x %s%n", favoriteString,favoriteInteger, favoriteClass.getName());
+}
+
+

Favorites实例是类型安全的:当你向它请求一个 String 类型时,它永远不会返回一个 Integer 类型。
+Favorites实例也是异构的:所有键都是不同类型的,普通 Map 的键是固定一个类型,因此,我们将 Favorites 称为一个类型安全异构容器

+

Favorites 的实现非常简短:

+
// Typesafe heterogeneous container pattern - implementation
+public class Favorites {
+  private Map<Class<?>, Object> favorites = new HashMap<>();
+
+  public <T> void putFavorite(Class<T> type, T instance) {
+    favorites.put(Objects.requireNonNull(type), instance);
+  }
+
+  public <T> T getFavorite(Class<T> type) {
+    return type.cast(favorites.get(type));
+  }
+}
+
+

通配符类型的键意味着每个键都可以有不同的参数化类型:一个可以是 Class<String>,下一个是 Class<Integer>,等等。这就是异构的原理。

+

favorites 的值类型仅仅是 Object。换句话说,Map 不保证键和值之间的类型关系

+

putFavorite 的实现很简单:它只是将从给定 Class 对象到给定对象的映射关系放入 favorites 中。如前所述,这将丢弃键和值之间的「类型关联」;将无法确定值是键的实例。但这没关系,因为 getFavorites 方法可以重新建立这个关联。

+

getFavorite 的实现比 putFavorite 的实现更复杂。首先,它从 favorites 中获取与给定 Class 对象对应的值。这是正确的对象引用返回,但它有错误的编译时类型:它是 Object(favorites 的值类型),我们需要返回一个 T。因此,getFavorite 的实现通过使用 Class 的 cast 方法,将对象引用类型动态转化为所代表的 Class 对象。

+

cast 方法是 Java 的 cast 运算符的动态模拟。它只是检查它的参数是否是类对象表示的类型的实例。如果是,则返回参数;否则它将抛出 ClassCastException。我们知道 getFavorite 中的强制转换调用不会抛出 ClassCastException。也就是说,我们知道 favorites 中的值总是与其键的类型匹配。

+

如果 cast 方法只是返回它的参数,那么它会为我们做什么呢?cast 方法的签名充分利用了 Class 类是泛型的这一事实。其返回类型为 Class 对象的类型参数:

+
public class Class<T> {
+    T cast(Object obj);
+}
+
+

这正是 getFavorite 方法所需要的。它使我们能够使 Favorites 类型安全,而不需要对 T 进行 unchecked 的转换。

+

Favorites 类有两个值得注意的限制。

+
    +
  • +

    恶意客户端很容易通过使用原始形式的类对象破坏 Favorites 实例的类型安全。通过使用原始类型 HashSet(Item-26),可以轻松地将 String 类型放入 HashSet<Integer> 中。为了获得运行时的类型安全,让 putFavorite 方法检查实例是否是 type 表示的类型的实例,使用动态转换:

    +
    // Achieving runtime type safety with a dynamic cast
    +public <T> void putFavorite(Class<T> type, T instance) {
    +    favorites.put(type, type.cast(instance));
    +}
    +
    +
  • +
  • +

    Favorites 类不能用于不可具体化的类型(Item-28)。换句话说,你可以存储的 Favorites 实例类型为 String 或 String[],但不能存储 List<String>。原因是你不能为 List<String> 获取 Class 对象,List<String>.class 是一个语法错误

    +
  • +
+

Favorites 使用的类型标记是无界的:getFavorite 和 put-Favorite 接受任何 Class 对象。有时你可能需要限制可以传递给方法的类型。这可以通过有界类型标记来实现,它只是一个类型标记,使用有界类型参数(Item-30)或有界通配符(Item-31)对可以表示的类型进行绑定。

+

annotation API(Item-39)广泛使用了有界类型标记。例如,下面是在运行时读取注释的方法。这个方法来自 AnnotatedElement 接口,它是由表示类、方法、字段和其他程序元素的反射类型实现的:

+
public <T extends Annotation>
+    T getAnnotation(Class<T> annotationType);
+
+

参数 annotationType 是表示注释类型的有界类型标记。该方法返回该类型的元素注释(如果有的话),或者返回 null(如果没有的话)。本质上,带注释的元素是一个类型安全的异构容器,其键是注释类型。

+

假设你有一个 Class<?> 类型的对象,并且希望将其传递给一个需要有界类型标记(例如 getAnnotation)的方法。你可以将对象强制转换为 Class<? extends Annotation>,但是这个强制转换是 unchecked 的,因此它将生成一个编译时警告(Item-27)。幸运的是,Class 类提供了一个实例方法,可以安全地(动态地)执行这种类型的强制转换。该方法叫做 asSubclass,它将 Class 对象强制转换为它所调用的类对象,以表示由其参数表示的类的子类。如果转换成功,则该方法返回其参数;如果失败,则抛出 ClassCastException。

+

下面是如何使用 asSubclass 方法读取在编译时类型未知的注释。这个方法编译没有错误或警告:

+
// Use of asSubclass to safely cast to a bounded type token
+static Annotation getAnnotation(AnnotatedElement element,String annotationTypeName) {
+    Class<?> annotationType = null; // Unbounded type token
+    try {
+        annotationType = Class.forName(annotationTypeName);
+    } catch (Exception ex) {
+        throw new IllegalArgumentException(ex);
+    }
+    return element.getAnnotation(annotationType.asSubclass(Annotation.class));
+}
+
+

总之,以集合的 API 为例的泛型在正常使用时将每个容器的类型参数限制为固定数量。你可以通过将类型参数放置在键上而不是容器上来绕过这个限制。你可以使用 Class 对象作为此类类型安全异构容器的键。以这种方式使用的 Class 对象称为类型标记。还可以使用自定义键类型。例如,可以使用 DatabaseRow 类型表示数据库行(容器),并使用泛型类型 Column<T> 作为它的键

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/07/23/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\272\224\357\274\211/index.html" "b/2022/07/23/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\272\224\357\274\211/index.html" new file mode 100644 index 0000000..9c35aed --- /dev/null +++ "b/2022/07/23/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\272\224\357\274\211/index.html" @@ -0,0 +1,841 @@ + + + + + + + + + + + + + + + + + + + + Effective-Java学习笔记(五) | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Effective-Java学习笔记(五) +

+ + + + +
+
+ +

第六章 枚举和注解

+

34. 用枚举类型代替 int 常量

+

int枚举

+

int枚举类型中有一系列public static final int常量,每个常量表示一个类型。
+它有许多缺点:

+
    +
  1. 不提供类型安全性,容易误用(入参是Apple常量,但是传任何int都可以)
  2. +
  3. 常量值修改,客户端也必须修改
  4. +
  5. 调试麻烦
  6. +
+

枚举类型

+

Java 的枚举类型是成熟的类,其他语言中的枚举类型本质上是 int 值。

+

枚举优势:

+
    +
  1. +

    实例受控
    +Java 枚举类型通过 public static final 修饰的字段为每个枚举常量导出一个实例。枚举类型实际上是 final 类型,因为构造函数是私有的。客户端既不能创建枚举类型的实例,也不能继承它,所以除了声明的枚举常量之外,不能有任何实例。换句话说,枚举类型是实例受控的类(Item-1)。它们是单例(Item-3)的推广应用,单例本质上是单元素的枚举。

    +
  2. +
  3. +

    提供编译时类型安全性。
    +如果将参数声明为 Apple 枚举类型,则可以保证传递给该参数的任何非空对象引用都是 Apple 枚举值之一。尝试传递错误类型的值将导致编译时错误,将一个枚举类型的表达式赋值给另一个枚举类型的变量,或者使用 == 运算符比较不同枚举类型的值同样会导致错误。

    +
  4. +
  5. +

    名称相同的枚举类型常量能共存
    +名称相同的枚举类型常量能和平共存,因为每种类型都有自己的名称空间。你可以在枚举类型中添加或重新排序常量,而无需重新编译其客户端,因为导出常量的字段在枚举类型及其客户端之间提供了一层隔离:常量值不会像在 int 枚举模式中那样编译到客户端中。最后,你可以通过调用枚举的 toString 方法将其转换为可打印的字符串。

    +
  6. +
  7. +

    允许添加任意方法和字段并实现任意接口
    +枚举类型允许添加任意方法和字段并实现任意接口。它们提供了所有 Object 方法的高质量实现(参阅 Chapter 3),还实现了 Comparable(Item-14)和 Serializable(参阅 Chapter 12),并且它们的序列化形式被设计成能够适应枚举类型的可变性。如果方法是常量特有的,可以在枚举类型中添加抽象方法,在声明枚举实例时覆盖抽象方法

    +
    // Enum type with constant-specific class bodies and data
    +public enum Operation {
    +    PLUS("+") {
    +        public double apply(double x, double y) { return x + y; }
    +    },
    +    MINUS("-") {
    +        public double apply(double x, double y) { return x - y; }
    +    },
    +    TIMES("*") {
    +        public double apply(double x, double y) { return x * y; }
    +    },
    +    DIVIDE("/") {
    +        public double apply(double x, double y) { return x / y; }
    +    };
    +
    +    private final String symbol;
    +
    +    Operation(String symbol) { this.symbol = symbol; }
    +
    +    @Override
    +    public String toString() { return symbol; }
    +    // Implementing a fromString method on an enum type
    +    private static final Map<String, Operation> stringToEnum =Stream.of(values()).collect(toMap(Object::toString, e -> e));
    +
    +    // Returns Operation for string, if any
    +    public static Optional<Operation> fromString(String symbol) {
    +        return Optional.ofNullable(stringToEnum.get(symbol));
    +    }
    +
    +    public abstract double apply(double x, double y);
    +}
    +
    +

    上述实现的缺点是如果常量特有的方法有可以复用的代码,那么会造成许多冗余。你可以把冗余代码抽出成方法,常量覆盖抽象方法时调用抽出的方法,这种实现同样有许多冗余代码;你也可以把抽象方法改成具体方法,里面是可以复用的代码,添加常量时如果不覆盖那就用默认的,这种实现问题在于添加常量时如果忘记覆盖那就用默认方法了。

    +

    当常量特有的方法有可以复用的代码时,我们采用策略枚举模式,将可复用代码移到私有嵌套枚举中。当常量调用方法时,调用私有嵌套枚举常量的方法。

    +

    在枚举上实现特定常量的行为时 switch 语句不是一个好的选择,只有在枚举不在你的控制之下,你希望它有一个实例方法来返回每个常量的特定行为,这时候才用 switch

    +
  8. +
+

总之,枚举类型相对于 int 常量的优势是毋庸置疑的。枚举更易于阅读、更安全、更强大。许多枚举不需要显式构造函数或成员,但有些枚举则受益于将数据与每个常量关联,并提供行为受数据影响的方法。将多个行为与一个方法关联起来,这样的枚举更少。在这种相对少见的情况下,相对于使用 switch 的枚举,特定常量方法更好。如果枚举常量有一些(但不是全部)共享公共行为,请考虑策略枚举模式。

+

35. 使用实例字段替代序数

+

每个枚举常量都有一个 ordinal 方法,返回枚举常量在枚举类中的位置。不要用这个方法返回与枚举常量关联的值,一旦常量位置变动,值就是错的,所以不要用序数生成与枚举常量关联的值,而是将其存在实例字段中

+

36. 用 EnumSet 替代位字段

+

位字段

+

如果枚举类型的元素主要在 Set 中使用,传统上使用 int 枚举模式(Item34),通过不同的 2 的幂次为每个常量赋值:

+
// Bit field enumeration constants - OBSOLETE!
+public class Text {
+    public static final int STYLE_BOLD = 1 << 0; // 1
+    public static final int STYLE_ITALIC = 1 << 1; // 2
+    public static final int STYLE_UNDERLINE = 1 << 2; // 4
+    public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8
+    // Parameter is bitwise OR of zero or more STYLE_ constants
+    public void applyStyles(int styles) { ... }
+}
+
+

使用位运算的 OR 操作将几个常量组合成一个集合,这个集合叫做位字段:

+
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
+
+

位字段表示方式允许使用位运算高效地执行集合操作,如并集和交集。但是位字段具有 int 枚举常量所有缺点,甚至更多。当位字段被打印为数字时,它比简单的 int 枚举常量更难理解。没有一种简单的方法可以遍历由位字段表示的所有元素。最后,你必须预测在编写 API 时需要的最大位数,并相应地为位字段(通常是 int 或 long)选择一种类型。一旦选择了一种类型,在不更改 API 的情况下,不能超过它的宽度(32 或 64 位)。

+
EnumSet
+

一些使用枚举而不是 int 常量的程序员在需要传递常量集合时仍然坚持使用位字段。没有理由这样做,因为存在更好的选择。java.util 包提供 EnumSet 类来有效地表示从单个枚举类型中提取的值集。这个类实现了 Set 接口,提供了所有其他 Set 实现所具有的丰富性、类型安全性和互操作性。但在内部,每个 EnumSet 都表示为一个位向量。如果底层枚举类型有 64 个或更少的元素(大多数都是),则整个 EnumSet 用一个 long 表示,因此其性能与位字段的性能相当。批量操作(如 removeAll 和 retainAll)是使用逐位算法实现的,就像手动处理位字段一样。但是,你可以避免因手工修改导致产生不良代码和潜在错误:EnumSet 为你完成了这些繁重的工作。

+

当之前的示例修改为使用枚举和 EnumSet 而不是位字段时。它更短,更清晰,更安全:

+
// EnumSet - a modern replacement for bit fields
+public class Text {
+    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
+    // Any Set could be passed in, but EnumSet is clearly best
+    public void applyStyles(Set<Style> styles) { ... }
+}
+
+

下面是将 EnumSet 实例传递给 applyStyles 方法的客户端代码。EnumSet 类提供了一组丰富的静态工厂,可以方便地创建集合,下面的代码演示了其中的一个:

+
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
+
+

总之,如果枚举类型将在 Set 中使用,不要用位字段表示它。 EnumSet 类结合了位字段的简洁性和性能,以及 (Item-34) 中描述的枚举类型的许多优点。EnumSet 的一个真正的缺点是,从 Java 9 开始,它不能创建不可变的 EnumSet,在未来发布的版本中可能会纠正这一点。同时,可以用 Collections.unmodifiableSet 包装 EnumSet,但简洁性和性能将受到影响

+

37. 使用 EnumMap 替换序数索引

+

序数索引

+

当需要将枚举常量映射到其它值时,有一种方法是序数索引,使用 ordinal 方法返回的序数表示key。这种方式存在Item35提到的问题

+

EnumMap

+

EnumMap使用枚举常量作为key,功能丰富、类型安全

+

38. 使用接口模拟可扩展枚举

+

枚举不可以扩展(继承)另一个枚举,但可以实现接口。可以用接口模拟可扩展的枚举。下面的例子将Item34的枚举类型的Operation改成了接口类型。通过实现接口来模拟继承枚举。

+
// Emulated extensible enum using an interface
+public interface Operation {
+    double apply(double x, double y);
+}
+
+public enum BasicOperation implements Operation {
+    PLUS("+") {
+        public double apply(double x, double y) { return x + y; }
+    },
+    MINUS("-") {
+        public double apply(double x, double y) { return x - y; }
+    },
+    TIMES("*") {
+        public double apply(double x, double y) { return x * y; }
+    },
+    DIVIDE("/") {
+        public double apply(double x, double y) { return x / y; }
+    };
+
+    private final String symbol;
+
+    BasicOperation(String symbol) {
+        this.symbol = symbol;
+    }
+
+    @Override
+    public String toString() {
+        return symbol;
+    }
+}
+
+
// Emulated extension enum
+public enum ExtendedOperation implements Operation {
+    EXP("^") {
+        public double apply(double x, double y) {
+            return Math.pow(x, y);
+        }
+    },
+    REMAINDER("%") {
+        public double apply(double x, double y) {
+            return x % y;
+        }
+    };
+
+    private final String symbol;
+
+    ExtendedOperation(String symbol) {
+        this.symbol = symbol;
+    }
+
+    @Override
+    public String toString() {
+        return symbol;
+    }
+}
+
+

可以用接口类型引用指向不同子枚举类型实例。

+
public static void main(String[] args) {
+    Operation op = BasicOperation.DIVIDE;
+    System.out.println(op.apply(15, 3));
+    op=ExtendedOperation.EXP;
+    System.out.println(op.apply(2,5));
+}
+
+

java.nio.file.LinkOption 使用了这个Item描述的方法,它实现了 CopyOption 和 OpenOption 接口。

+

总之,枚举不可以扩展或被扩展,但是你可以通过接口模拟枚举之间的层级关系

+

39. 注解优于命名模式

+

命名模式

+

用来标明某些程序元素需要工具或框架特殊处理。
+例如JUnit3及之前,用户必须写test开头的测试方法,不然测试都不能运行。
+缺点:1.写错方法名会导致测试通过,但是实际上根本没有运行;2.不能将参数值和程序元素关联,例如测试方法只有在抛出特定异常时才成功,虽然可以精心设计命名模式,将异常名称写在测试方法名上,但是编译器无法检查异常类是否存在

+

注解

+

Junit4 开始使用注解,框架会根据注解执行测试方法,也可以将异常和测试方法绑定。注解本身不修改代码语义,而是通过反射(框架所用的技术)对其特殊处理

+

40. 坚持使用 @Override 注解

+

请在要覆盖父类声明的每个方法声明上使用 @Override 注解。只有一个例外,那就是具体类覆盖父类的抽象方法,因为具体类必须要实现父类的抽象方法,所以不必加 @Override 注解

+

41. 使用标记接口定义类型

+

标记接口

+

标记接口是一个空接口,它的作用是标记实现类具有某些属性。例如,实现 Serializable 接口表示类的实例可以写入 ObjectOutputStream(序列化)

+
标记接口VS标记注解
+

与标记注解(Item39)相比,标记接口有两个优点:

+
    +
  1. 标记接口定义的类型由标记类的实例实现;标记注解不会。因此标记接口类型的存在允许你在编译时捕获错误,标记注解只能在运行时捕获。ObjectOutputStream.writeObject方法入参必须是实现了Serializable的实例,否则会报编译错误(JDK设计缺陷,writeObject入参是Object类型)
  2. +
  3. 标记接口相对于标记注解的另一个优点是可以更精确地定位子类型。例如 Set 是一个标记接口,它继承 Collection接口,Set标记了所有它的实现类都具有Collection功能。Set相较于标记接口的特殊之处在于它还细化了Collection方法 add、equals 和 hashCode 的约定
  4. +
+

标记注解的优势:与基于使用注解的框架保持一致性

+
使用规则
+

如果标记用在类或接口之外的任何程序元素,必须用标记注解;如果框架使用注解,那就用标记注解;其它情况都用标记接口

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/07/30/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\345\205\255\357\274\211/index.html" "b/2022/07/30/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\345\205\255\357\274\211/index.html" new file mode 100644 index 0000000..0ae0991 --- /dev/null +++ "b/2022/07/30/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\345\205\255\357\274\211/index.html" @@ -0,0 +1,831 @@ + + + + + + + + + + + + + + + + + + + + Effective-Java学习笔记(六) | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Effective-Java学习笔记(六) +

+ + + + +
+
+ +

第七章 λ 表达式和流

+

42. λ 表达式优于匿名类

+

函数对象

+

在历史上,带有单个抽象方法的接口(或者抽象类,但这种情况很少)被用作函数类型。它们的实例(称为函数对象)表示函数或操作。自从 JDK 1.1 在 1997 年发布以来,创建函数对象的主要方法就是匿名类(Item-24)。下面是一个按长度对字符串列表进行排序的代码片段,使用一个匿名类来创建排序的比较函数(它强制执行排序顺序):

+
// Anonymous class instance as a function object - obsolete!
+Collections.sort(words, new Comparator<String>() {
+    public int compare(String s1, String s2) {
+        return Integer.compare(s1.length(), s2.length());
+    }
+});
+
+

函数式接口

+

在 Java 8 中官方化了一个概念,即具有单个抽象方法的接口是特殊的,应该得到特殊处理。这些接口现在被称为函数式接口

+

lambda 表达式

+

可以使用 lambda 表达式为函数式接口创建实例,lambda 表达式在功能上类似于匿名类,但更简洁

+
// Lambda expression as function object (replaces anonymous class)
+Collections.sort(words,(s1, s2) -> Integer.compare(s1.length(), s2.length()));
+
+
    +
  1. lambda 表达式的入参和返回值类型一般不写,由编译器做类型推断,类型能不写就不写。
    +Item26告诉你不要用原始类型,Item29,30告诉你要用泛型和泛型方法,编译器从泛型中获得了大部分类型推断所需的类型信息
  2. +
  3. lambda 表达式缺少名称和文档;如果一个算法较复杂,或者有很多行代码,不要把它放在 lambda 表达式中。 一行是理想的,三行是合理的最大值
  4. +
  5. lambda 表达式仅限于函数式接口。如果想创建抽象类实例或者多个抽象方法的接口,只能用匿名类
  6. +
  7. lambda 表达式编译后会成为外部类的一个私有方法,而匿名类会生成单独的class文件,所以 lambda 表达式的this是外部类实例,匿名类的this是匿名类实例
  8. +
  9. lambda表达式和匿名类都不能可靠的序列化,如果想序列化函数对象,可以用私有静态嵌套类
  10. +
+

43. 方法引用优于 λ 表达式

+

Java 提供了一种比 lambda 表达式更简洁的方法来生成函数对象:方法引用。下面这段代码的功能是,如果数字 1 不在映射中,则将其与键关联,如果键已经存在,则将关联值递增:

+
map.merge(key, 1, (count, incr) -> count + incr);
+
+

上面代码的 lambda 表达式作用是返回两个入参的和,在 Java 8 中,Integer(和所有其它基本类型的包装类)提供了一个静态方法 sum,它的作用完全相同,我们可以传入一个方法引用,并得到相同结果,同时减少视觉混乱:

+
map.merge(key, 1, Integer::sum);
+
+

函数对象的参数越多,方法引用就显得越简洁,但是 lambda 表达式指明了参数名,使得 lambda表达式比方法引用更容易阅读和维护,没有什么是方法引用能做而 lambda 表达式做不了的

+

许多方法引用引用静态方法,但是有四种方法不引用静态方法。其中两个是绑定和非绑定实例方法引用。在绑定引用中,接收对象在方法引用中指定。绑定引用在本质上与静态引用相似:函数对象接受与引用方法相同的参数。在未绑定引用中,在应用函数对象时通过方法声明参数之前的附加参数指定接收对象。在流管道中,未绑定引用通常用作映射和筛选函数(Item-45)。最后,对于类和数组,有两种构造函数引用。构造函数引用用作工厂对象。五种方法参考文献汇总如下表:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Method Ref TypeExampleLambda Equivalent
StaticInteger::parseIntstr ->
BoundInstant.now()::isAfterInstant then =Instant.now(); t ->then.isAfter(t)
UnboundString::toLowerCasestr ->str.toLowerCase()
Class ConstructorTreeMap<K,V>::new() -> new TreeMap<K,V>
Array Constructorint[]::newlen -> new int[len]
+

总之,方法引用通常为 lambda 表达式提供了一种更简洁的选择。如果方法引用更短、更清晰,则使用它们;如果没有,仍然使用 lambda 表达式。

+

44. 优先使用标准函数式接口

+

java.util.function 包提供了大量的标准函数接口。优先使用标准函数式接口而不是自己写。 通过减少 API ,使得你的 API 更容易学习,并将提供显著的互操作性优势,因为许多标准函数式接口提供了有用的默认方法

+

java.util.function 中有 43 个接口。不能期望你记住所有的接口,但是如果你记住了 6 个基本接口,那么你可以在需要时派生出其余的接口。基本接口操作对象引用类型。Operator 接口表示结果和参数类型相同的函数。Predicate 接口表示接受参数并返回布尔值的函数。Function 接口表示参数和返回类型不同的函数。Supplier 接口表示一个无参并返回(或「供应」)值的函数。最后,Consumer 表示一个函数,该函数接受一个参数,但不返回任何内容,本质上是使用它的参数。六个基本的函数式接口总结如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
InterfaceFunction SignatureExample
UnaryOperator<T>T apply(T t)String::toLowerCase
BinaryOperator<T>T apply(T t1, T t2)BigInteger::add
Predicate<T>boolean test(T t)Collection::isEmpty
Function<T,R>R apply(T t)Arrays::asList
Supplier<T>T get()Instant::now
Consumer<T>void accept(T t)System.out::println
+

还有 6 个基本接口的 3 个变体,用于操作基本类型 int、long 和 double。它们的名称是通过在基本接口前面加上基本类型前缀而派生出来的。例如,一个接受 int 的 Predicate 就是一个 IntPredicate,一个接受两个 long 值并返回一个 long 的二元操作符就是一个 LongBinaryOperator。没有变体类型是参数化的除了由返回类型参数化的 Function 变体外。例如,LongFunction<int[]> 使用 long 并返回一个 int[]。

+

Function 接口还有 9 个额外的变体,在结果类型为基本数据类型时使用。源类型和结果类型总是不同的,因为入参只有一个且与出参类型相同的函数本身都是 UnaryOperator。如果源类型和结果类型都是基本数据类型,则使用带有 SrcToResult 的前缀函数,例如 LongToIntFunction(六个变体)。如果源是一个基本数据类型,而结果是一个对象引用,则使用带前缀 SrcToObj 的 Function 接口,例如 DoubleToObjFunction(三个变体)。

+

三个基本函数式接口有两个参数版本,使用它们是有意义的:BiPredicate<T,U>BiFunction<T,U,R>BiConsumer<T,U>。也有 BiFunction 变体返回三个相关的基本类型:ToIntBiFunction<T,U>ToLongBiFunction<T,U>ToDoubleBiFunction<T,U>。Consumer 有两个参数变体,它们接受一个对象引用和一个基本类型:ObjDoubleConsumer<T>ObjIntConsumer<T>ObjLongConsumer<T>。总共有9个基本接口的双参数版本。

+

最后是 BooleanSupplier 接口,它是 Supplier 的一个变体,返回布尔值。这是在任何标准函数接口名称中唯一显式提到布尔类型的地方,但是通过 Predicate 及其四种变体形式支持布尔返回值。前面描述的 BooleanSupplier 接口和 42 个接口占了全部 43 个标准函数式接口。

+

总之,既然 Java 已经有了 lambda 表达式,你必须在设计 API 时考虑 lambda 表达式。在输入时接受函数式接口类型,在输出时返回它们。一般情况下,最好使用 java.util.function 中提供的标准函数式接口,但请注意比较少见的一些情况,在这种情况下,你最好编写自己的函数式接口。

+

45. 明智地使用流

+

Java8添加了流API,用来简化序列或并行执行批量操作,API有两个关键的抽象:流(表示有限或无限的数据元素序列)和流管道(表示对这些元素的多阶段计算)。

+

+

流中的元素可以来自任何地方。常见的源包括集合、数组、文件、正则表达式的 Pattern 匹配器、伪随机数生成器和其他流。流中的数据元素可以是对象的引用或基本数据类型。支持三种基本数据类型:int、long 和 double。

+

流管道

+

流管道由源流跟着零个或多个中间操作和一个终止操作组成。每个中间操作以某种方式转换流,例如将每个元素映射到该元素的一个函数,或者过滤掉不满足某些条件的所有元素。中间操作都将一个流转换为另一个流,其元素类型可能与输入流相同,也可能与输入流不同。终止操作对最后一次中间操作所产生的流进行最终计算,例如将其元素存储到集合中、返回特定元素、或打印其所有元素。

+
    +
  1. 流管道的计算是惰性的:直到调用终止操作时才开始计算,并且对完成终止操作不需要的数据元素永远不会计算。这种惰性的求值机制使得处理无限流成为可能。
  2. +
  3. 流 API 是流畅的:它被设计成允许使用链式调用将组成管道的所有调用写到单个表达式中。
  4. +
  5. 谨慎使用流,全部都是流操作的代码可读性不高
  6. +
  7. 谨慎命名 lambda 表达式的参数名以提高可读性,复杂表达式使用 helper 方法
  8. +
+

流 VS 迭代

+

迭代代码使用代码块表示重复计算,流管道使用函数对象(通常是 lambda 表达式或方法引用)表示重复计算。

+

迭代优势:

+
    +
  1. 代码块可以读取修改局部变量,lambda表达式只能读取final变量或实际上final变量(初始化后不再修改,编译器会帮我们声明为final)。
  2. +
  3. 代码块可以控制迭代,包括return,break,continue,throw,但是lambda不行
  4. +
+

流适合用在以下操作:

+
    +
  1. 元素序列的一致变换
  2. +
  3. 过滤元素序列
  4. +
  5. 组合元素序列(例如添加它们,连接它们或计算它们的最小值)
  6. +
  7. 聚合元素序列到一个集合中,例如按属性分组
  8. +
  9. 在元素序列中搜索满足某些条件的元素
  10. +
+

总之,有些任务最好使用流来完成,有些任务最好使用迭代来完成。许多任务最好通过结合这两种方法来完成。

+

46. 在流中使用无副作用的函数

+

考虑以下代码,它用于构建文本文件中单词的频率表

+
// Uses the streams API but not the paradigm--Don't do this!
+Map<String, Long> freq = new HashMap<>();
+try (Stream<String> words = new Scanner(file).tokens()) {
+    words.forEach(word -> {
+        freq.merge(word.toLowerCase(), 1L, Long::sum);
+    });
+}
+
+

这段代码可以得出正确答案,但它不是流代码,而是伪装成流代码的迭代代码。它比迭代代码更长,更难阅读,更难维护,问题出在:forEach修改了外部状态。正确使用的流代码如下:

+
// Proper use of streams to initialize a frequency table
+Map<String, Long> freq;
+try (Stream<String> words = new Scanner(file).tokens()) {
+    freq = words.collect(groupingBy(String::toLowerCase, counting()));
+}
+
+

forEach 操作应该只用于报告流计算的结果,而不是执行计算

+

正确的流代码使用了 Collectors 的生成收集器的方法,将元素收集到集合中的方法有三种:toList()toSet()toCollection(collectionFactory)。它们分别返回 List、Set 和程序员指定的集合类型

+

Collectors其它方法大部分是将流收集到Map中。最简单的Map收集器是toMap(keyMapper, valueMapper),它接受两个函数,一个将流元素映射到键,另一个将流元素映射到值,Item34用到了这个收集器

+
// Using a toMap collector to make a map from string to enum
+private static final Map<String, Operation> stringToEnum =Stream.of(values()).collect(toMap(Object::toString, e -> e));
+
+

这个收集器不能处理重复键的问题,我们可以加入第三个参数,即merge函数,处理重复键,它的参数类型是Map的值类型。我们还可以加入第四个参数用来指定特定的Map实现(如EnumMap或TreeMap)

+

除了toMap方法,groupingBy方法也将元素收集到Map中,键是类别,值是这个类别的所有元素的列表;groupingBy方法第二个参数是一个下游收集器,例如 counting() 作为下游收集器,最终的Map的键是类别,值是这个类别所有元素的数量;groupingBy也支持指定特定的Map实现

+

minBy和maxBy,它们接受一个Comparator并返回最小或最大元素;join方法将字符序列(如字符串)连接起来

+

总之,流管道编程的本质是无副作用的函数对象。这适用于传递给流和相关对象的所有函数对象。中间操作 forEach 只应用于报告由流执行的计算结果,而不应用于执行计算。为了正确使用流,你必须了解 collector。最重要的 collector 工厂是 toList、toSet、toMap、groupingBy 和 join。

+

47. 优先使用 Collection 而不是 Stream 作为返回类型

+

Stream 没有继承 Iterable,不能用 for-each 循环遍历。Collection 接口继承了 Iterable 而且提供了转换为流的方法,因此,Collection 或其适当的子类通常是公有返回序列的方法的最佳返回类型。

+

48. 谨慎使用并行流

+

如果流来自 Stream.iterate 或者中间操作 limit,并行化管道也不太可能提高其性能

+

通常,并行性带来的性能提升在 ArrayList、HashMap、HashSet 和 ConcurrentHashMap 实例上的流效果最好;int 数组和 long 数组也在其中。 这些数据结构的共同之处在于,它们都可以被精确且廉价地分割成任意大小的子程序,这使得在并行线程之间划分工作变得很容易。另一个共同点是,当按顺序处理时,它们提供了极好的引用位置:顺序元素引用一起存储在内存中,这些引用的引用对象在内存中可能彼此不太接近,这减少了引用的位置。引用位置对于并行化批量操作非常重要,如果没有它,线程将花费大量时间空闲,等待数据从内存传输到处理器的缓存中。具有最佳引用位置的数据结构是基本数组,因为数据本身是连续存储在内存中的。

+

如果在终止操作中完成大量工作,并且该操作本质上是顺序的,那么管道的并行化效果有限。并行化的最佳终止操作是 reduce 方法或者预先写好的reduce 方法,包括min、max、count、and。anyMatch、allMatch 和 noneMatch 的短路操作也适用于并行性。流的 collect 方法执行的操作称为可变缩减,它们不是并行化的好候选,因为组合集合的开销是昂贵的。

+

并行化流不仅会导致糟糕的性能,包括活动失败;它会导致不正确的结果和不可预知的行为(安全故障)。

+

总之,不要尝试并行化流管道,除非你有充分的理由相信它将保持计算的正确性以及提高速度。不适当地并行化流的代价可能是程序失败或性能灾难。

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/08/01/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\270\203\357\274\211/index.html" "b/2022/08/01/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\270\203\357\274\211/index.html" new file mode 100644 index 0000000..b0c7ffa --- /dev/null +++ "b/2022/08/01/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\270\203\357\274\211/index.html" @@ -0,0 +1,811 @@ + + + + + + + + + + + + + + + + + + + + Effective-Java学习笔记(七) | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Effective-Java学习笔记(七) +

+ + + + +
+
+ +

第八章 方法

+

49. 检查参数的有效性

+

大多数方法和构造函数都对入参有一些限制,例如非空非负,你应该在方法主体的开头检查参数并在Javadoc中用@throws记录参数限制

+

Java7 添加了 Objects.requireNonNull 方法执行空检查

+

Java9 在 Objects 中添加了范围检查:checkFromIndexSize、checkFromToIndex 和 checkIndex

+

对于未导出的方法,作为包的作者,你应该定制方法调用的环境,确保只传递有效的参数值。因此,非 public 方法可以使用断言检查参数

+
// Private helper function for a recursive sort
+private static void sort(long a[], int offset, int length) {
+    assert a != null;
+    assert offset >= 0 && offset <= a.length;
+    assert length >= 0 && length <= a.length - offset;
+    ... // Do the computation
+}
+
+

断言只适合用来调试,可以关闭可以打开,public 方法一定要显示检查参数并抛出异常,断言失败只会抛出AssertionError,不利于定位错误

+

总而言之,每次编写方法或构造函数时,都应该考虑参数存在哪些限制。你应该在文档中记录这些限制,并在方法主体的开头显式地检查。

+

50. 在需要时制作防御性副本

+

Java 是一种安全的语言,这是它的一大优点。这意味着在没有 native 方法的情况下,它不受缓冲区溢出、数组溢出、非法指针和其他内存损坏错误的影响,这些错误困扰着 C 和 C++ 等不安全语言。在一种安全的语言中,可以编写一个类并确定它们的不变量将保持不变,而不管在系统的任何其他部分发生了什么。在将所有内存视为一个巨大数组的语言中,这是不可能的。

+

即使使用一种安全的语言,如果你不付出一些努力,也无法与其他类隔离。你必须进行防御性的设计,并假定你的类的客户端会尽最大努力破坏它的不变量。

+

通过可变参数破坏类的不变量

+

虽然如果没有对象的帮助,另一个类是不可能修改对象的内部状态的,但是要提供这样的帮助却出奇地容易。例如,考虑下面的类,它表示一个不可变的时间段:

+
// Broken "immutable" time period class
+public final class Period {
+    private final Date start;
+    private final Date end;
+
+    /**
+    * @param start the beginning of the period
+    * @param end the end of the period; must not precede start
+    * @throws IllegalArgumentException if start is after end
+    * @throws NullPointerException if start or end is null
+    */
+    public Period(Date start, Date end) {
+        if (start.compareTo(end) > 0)
+            throw new IllegalArgumentException(start + " after " + end);
+        this.start = start;
+        this.end = end;
+    }
+
+    public Date start() {
+        return start;
+    }
+
+    public Date end() {
+        return end;
+    }
+    ... // Remainder omitted
+}
+
+

乍一看,这个类似乎是不可变的,并且要求一个时间段的开始时间不能在结束时间之后。然而,利用 Date 是可变的这一事实很容易绕过这个约束:

+
// Attack the internals of a Period instance
+Date start = new Date();
+Date end = new Date();
+Period p = new Period(start, end);
+end.setYear(78); // Modifies internals of p!
+
+

从 Java8 开始,解决这个问题可以用 Instant 或 LocalDateTime 或 ZonedDateTime 来代替 Date,因为它们是不可变类。

+

防御性副本

+

但是有时必须在 API 和内部表示中使用可变值类型,这时候可以对可变参数进行防御性副本而不是使用原始可变参数。对上面的 Period 类改进如下

+
// Repaired constructor - makes defensive copies of parameters
+public Period(Date start, Date end) {
+    this.start = new Date(start.getTime());
+    this.end = new Date(end.getTime());
+    if (this.start.compareTo(this.end) > 0)
+        throw new IllegalArgumentException(this.start + " after " + this.end);
+}
+
+
    +
  1. 先进行防御性复制,再在副本上检查参数,保证在检查参数和复制参数之间的空窗期,类不受其他线程更改参数的影响,这个攻击也叫time-of-check/time-of-use 或 TOCTOU 攻击
  2. +
  3. 对可被不受信任方子类化的参数类型,不要使用 clone 方法进行防御性复制。
  4. +
  5. 访问器也要对可变字段进行防御性复制
  6. +
  7. 如果类信任它的调用者不会破坏不变量,比如类和调用者都是同一个包下,那么应该避免防御性复制
  8. +
  9. 当类的作用就是修改可变参数时不用防御性复制,客户端承诺不直接修改对象
  10. +
  11. 破坏不变量只会对损害客户端时不用防御性复制,例如包装类模式,客户端在包装对象之后可以直接访问对象,破坏类的不变量,但这通常只会损害客户端
  12. +
+

总而言之,如果一个类具有从客户端获取或返回给客户端的可变组件,则该类必须防御性地复制这些组件。如果复制的成本过高,并且类信任它的客户端不会不适当地修改组件,那么可以不进行防御性的复制,取而代之的是在文档中指明客户端的职责是不得修改受到影响的组件。

+

51. 仔细设计方法签名

+
    +
  1. 仔细选择方法名称。目标是选择可理解的、与同一包中其它名称风格一致的名称;选择广泛认可的名字;避免长方法名
  2. +
  3. 不要提供过于便利的方法。每种方法都应该各司其职。太多的方法使得类难以学习、使用、记录、测试和维护。对于接口来说更是如此,在接口中,太多的方法使实现者和用户的工作变得复杂。对于类或接口支持的每个操作,请提供一个功能齐全的方法。
  4. +
  5. 避免长参数列表。可以通过分解方法减少参数数量;也可以通过静态成员类 helper 类来存参数;也可以从对象构建到方法调用都采用建造者模式
  6. +
  7. 参数类型优先选择接口而不是类
  8. +
  9. 双元素枚举类型优于 boolean 参数。枚举比 boolean 可读性强且可以添加更多选项
  10. +
+

52. 明智地使用重载

+

考虑下面使用了重载的代码:

+
// Broken! - What does this program print?
+public class CollectionClassifier {
+    public static String classify(Set<?> s) {
+        return "Set";
+    }
+
+    public static String classify(List<?> lst) {
+        return "List";
+    }
+
+    public static String classify(Collection<?> c) {
+        return "Unknown Collection";
+    }
+
+    public static void main(String[] args) {
+        Collection<?>[] collections = {
+            new HashSet<String>(),new ArrayList<BigInteger>(),new HashMap<String, String>().values()
+        };
+        for (Collection<?> c : collections)
+            System.out.println(classify(c));
+    }
+}
+
+

这段代码打印了三次 Unknown Collection。因为 classify 方法被重载,并且在编译时就决定了要调用哪个重载,编译时是 Collections<?> 类型,所以调用的就是第三个重载方法

+

重载VS覆盖

+

重载方法的选择是静态的,在编译时决定要调用哪个重载方法。而覆盖方法的选择是动态的, 在运行时根据调用方法的对象的运行时类型选择覆盖方法的正确版本

+

因为覆盖是常态,而重载是例外,所以覆盖满足了人们对方法调用行为的期望,重载很容易混淆这些期望。

+

重载

+
    +
  1. 安全、保守的策略是永远不导出具有相同数量参数的两个重载。你可以为方法提供不同的名称而不是重载它们。
  2. +
  3. 构造函数只能重载,我们可以用静态工厂代替构造函数
  4. +
  5. 不要在重载方法的相同参数位置上使用不同的函数式接口。不同的函数式接口并没有本质的不同
  6. +
+

总而言之,方法可以重载,但并不意味着就应该这样做。通常,最好避免重载具有相同数量参数的多个签名的方法。在某些情况下,特别是涉及构造函数的情况下,可能难以遵循这个建议。在这些情况下,你至少应该避免同一组参数只需经过类型转换就可以被传递给不同的重载方法。如果这是无法避免的,例如,因为要对现有类进行改造以实现新接口,那么应该确保在传递相同的参数时,所有重载的行为都是相同的。如果你做不到这一点,程序员将很难有效地使用重载方法或构造函数,他们将无法理解为什么它不能工作。

+

53. 明智地使用可变参数

+

可变参数首先创建一个数组,其大小是在调用点上传递的参数数量,然后将参数值放入数组,最后将数组传递给方法。

+

当你需要定义具有不确定数量参数的方法时,可变参数是非常有用的。在可变参数之前加上任何必需的参数,并注意使用可变参数可能会引发的性能后果。

+

54. 返回空集合或数组,而不是 null

+

在方法中用空集合或空数组代替 null 返回可以让客户端不用显示判空

+

55. 明智地返回 Optional

+

在 Java8 之前,方法可能无法 return 时有两种处理方法,一种是抛异常,一种是 返回 null。抛异常代价高,返回 null 需要客户端显示判空

+

Java8 添加了第三种方法来处理可能无法返回值的方法。Optional<T> 类表示一个不可变的容器,它可以包含一个非空的 T 引用,也可以什么都不包含。不包含任何内容的 Optional 被称为空。一个值被认为存在于一个非空的 Optional 中。Optional 的本质上是一个不可变的集合,它最多可以容纳一个元素。

+

理论上应返回 T,但在某些情况下可能无法返回 T 的方法可以将返回值声明为 Optional<T>。这允许该方法返回一个空结果来表明它不能返回有效的结果。具备 Optional 返回值的方法比抛出异常的方法更灵活、更容易使用,并且比返回 null 的方法更不容易出错。

+

Item-30 有一个求集合最大值方法

+
// Returns maximum value in collection - throws exception if empty
+public static <E extends Comparable<E>> E max(Collection<E> c) {
+    if (c.isEmpty())
+        throw new IllegalArgumentException("Empty collection");
+    E result = null;
+    for (E e : c)
+        if (result == null || e.compareTo(result) > 0)
+            result = Objects.requireNonNull(e);
+    return result;
+}
+
+

当入参集合为空时,这个方法会抛出 IllegalArgumentException。更好的方式是返回 Optional

+
// Returns maximum value in collection as an Optional<E>
+public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
+    if (c.isEmpty())
+        return Optional.empty();
+    E result = null;
+    for (E e : c)
+        if (result == null || e.compareTo(result) > 0)
+            result = Objects.requireNonNull(e);
+    return Optional.of(result);
+}
+
+

Optional.empty() 返回一个空的 Optional,Optional.of(value) 返回一个包含给定非空值的 Optional。将 null 传递给 Optional.of(value) 是一个编程错误。如果你这样做,该方法将通过抛出 NullPointerException 来响应。Optional.ofNullable(value) 方法接受一个可能为空的值,如果传入 null,则返回一个空的 Optional。永远不要让返回 optional 的方法返回 null : 它违背了这个功能的设计初衷。

+

为什么选择返回 Optional 而不是返回 null 或抛出异常?Optional 在本质上类似于受检查异常(Item-71),因为它们迫使 API 的用户面对可能没有返回值的事实。抛出不受检查的异常或返回 null 允许用户忽略这种可能性,抛出受检查异常会让客户端添加额外代码

+

如果一个方法返回一个 Optional,客户端可以选择如果该方法不能返回值该采取什么操作。你可以使用 orElse 方法指定一个默认值,或者使用 orElseGet 方法在必要时生成默认值;也可以使用 orElseThrow 方法抛出异常

+

关于 Optional 用法的一些Tips:

+
    +
  1. isPresent 方法可以判断 Optional中有没有值,谨慎使用 isPrensent 方法,它的许多用途可以用上面的方法代替
  2. +
  3. 并不是所有的返回类型都能从 Optional 处理中获益。容器类型,包括集合、Map、流、数组和 Optional,不应该封装在 Optional 中。 你应该简单的返回一个空的 List<T>,而不是一个空的 Optional<List<T>>
  4. +
  5. 永远不应该返回包装类的 Optional,除了「次基本数据类型」,如 Boolean、Byte、Character、Short 和 Float 之外
  6. +
  7. 在集合或数组中使用 Optional 作为键、值或元素几乎都是不合适的。
  8. +
+

总之,如果你发现自己编写的方法不能总是返回确定值,并且你认为该方法的用户在每次调用时应该考虑这种可能性,那么你可能应该让方法返回一个 Optional。但是,你应该意识到,返回 Optional 会带来实际的性能后果;对于性能关键的方法,最好返回 null 或抛出异常。最后,除了作为返回值之外,你几乎不应该以任何其他方式使用 Optional。

+

56. 为所有公开的 API 元素编写文档注释

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/08/06/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\345\205\253\357\274\211/index.html" "b/2022/08/06/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\345\205\253\357\274\211/index.html" new file mode 100644 index 0000000..8bb626b --- /dev/null +++ "b/2022/08/06/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\345\205\253\357\274\211/index.html" @@ -0,0 +1,869 @@ + + + + + + + + + + + + + + + + + + + + Effective-Java学习笔记(八) | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Effective-Java学习笔记(八) +

+ + + + +
+
+ +

第九章 通用程序设计

+

57. 将局部变量的作用域最小化

+

本条目在性质上类似于Item-15,即「最小化类和成员的可访问性」。通过最小化局部变量的范围,可以提高代码的可读性和可维护性,并降低出错的可能性。

+
    +
  1. 将局部变量的作用域最小化,最具说服力的方式就是在第一次使用它的地方声明
  2. +
  3. 每个局部变量声明都应该包含一个初始化表达式。 如果你还没有足够的信息来合理地初始化一个变量,你应该推迟声明,直到条件满足
  4. +
+

58. for-each循环优于for循环

+

for-each 更简洁更不容易出错,且没有性能损失,只有三种情况不能用for-each

+
    +
  1. 破坏性过滤。如果需要遍历一个集合并删除选定元素,则需要使用显式的迭代器,以便调用其 remove 方法。通过使用 Collection 在 Java 8 中添加的 removeIf 方法,通常可以避免显式遍历。
  2. +
  3. 转换。如果需要遍历一个 List 或数组并替换其中部分或全部元素的值,那么需要 List 迭代器或数组索引来替换元素的值。
  4. +
  5. 并行迭代。如果需要并行遍历多个集合,那么需要显式地控制迭代器或索引变量,以便所有迭代器或索引变量都可以同步执行
  6. +
+

59. 了解并使用库

+

假设你想要生成 0 到某个上界之间的随机整数,有些程序员会写出如下代码:

+
// Common but deeply flawed!
+static Random rnd = new Random();
+static int random(int n) {
+    return Math.abs(rnd.nextInt()) % n;
+}
+
+

这个方法有三个缺点:

+
    +
  1. 如果 n 是比较小的 2 的幂,随机数序列会在相当短的时间内重复
  2. +
  3. 如果 n 不是 2 的幂,一些数字出现的频率会更高,当 n 很大时效果明显
  4. +
  5. 会返回超出指定范围的数字,当nextInt返回Integer.MIN_VALUE时,abs方法也会返回Integer.MIN_VALUE,假设 n 不是 2 幂,那么Integer.MIN_VALUE % n 将返回负数
  6. +
+

我们不需要为这个需求自己编写方法,已经存在经过专家设计测试的标准库Random 的 nextInt(int)

+

从 Java 7 开始,就不应该再使用 Random。在大多数情况下,选择的随机数生成器现在是 ThreadLocalRandom。 它能产生更高质量的随机数,而且速度非常快。

+

总而言之,不要白费力气重新发明轮子。如果你需要做一些看起来相当常见的事情,那么库中可能已经有一个工具可以做你想做的事情。如果有,使用它;如果你不知道,查一下。一般来说,库代码可能比你自己编写的代码更好,并且随着时间的推移可能会得到改进。

+

60. 若需要精确答案就应避免使用 float 和 double 类型

+

float 和 double 类型主要用于科学计算和工程计算。它们执行二进制浮点运算,该算法经过精心设计,能够在很大范围内快速提供精确的近似值。但是,它们不能提供准确的结果。float 和 double 类型特别不适合进行货币计算,因为不可能将 0.1(或 10 的任意负次幂)精确地表示为 float 或 double

+

正确做法是使用 BigDecimal、int 或 long 进行货币计算。还要注意 BigDecimal 的构造函数要使用String参数而不是double,避免初始化时就用了不精确的值。int和long可以存储较小单位的值,将小数转换成整数存储

+

总之,对于任何需要精确答案的计算,不要使用 float 或 double 类型。如果希望系统来处理十进制小数点,并且不介意不使用基本类型带来的不便和成本,请使用 BigDecimal。使用 BigDecimal 的另一个好处是,它可以完全控制舍入,当执行需要舍入的操作时,可以从八种舍入模式中进行选择。如果你使用合法的舍入行为执行业务计算,这将非常方便。如果性能是最重要的,那么你不介意自己处理十进制小数点,而且数值不是太大,可以使用 int 或 long。如果数值不超过 9 位小数,可以使用 int;如果不超过 18 位,可以使用 long。如果数量可能超过 18 位,则使用 BigDecimal。

+

61. 基本数据类型优于包装类

+

Java 的类型系统有两部分,基本类型(如 int、double 和 boolean)和引用类型(如String和List)。每个基本类型都有一个对应的引用类型,称为包装类。如Integer、Double 和 Boolean

+

基本类型和包装类型之间的区别如下

+
    +
  1. 基本类型只有它们的值,而包装类型具有与其值不同的标识。换句话说,两个包装类型实例可以具有相同的值和不同的标识。
  2. +
  3. 基本类型只有全部功能值,而每个包装类型除了对应的基本类型的所有功能值外,还有一个非功能值,即 null
  4. +
  5. 基本类型比包装类型更节省时间和空间
  6. +
  7. 用 == 来比较包装类型几乎都是错的
  8. +
  9. 在操作中混用基本类型和包装类型时,包装类型会自动拆箱,可能导致 NPE,如果操作结果保存到包装类型的变量中,还会发生自动装箱,导致性能问题
  10. +
+

包装类型的用途如下:

+
    +
  1. 作为集合的元素、键和值
  2. +
  3. 泛型和泛型方法中的类型参数
  4. +
  5. 反射调用
  6. +
+

总之,只要有选择,就应该优先使用基本类型,而不是包装类型。基本类型更简单、更快。如果必须使用包装类型,请小心!自动装箱减少了使用包装类型的冗长,但没有减少危险。 当你的程序使用 == 操作符比较两个包装类型时,它会执行标识比较,这几乎肯定不是你想要的。当你的程序执行包含包装类型和基本类型的混合类型计算时,它将进行拆箱,当你的程序执行拆箱时,将抛出 NullPointerException。 最后,当你的程序将基本类型装箱时,可能会导致代价高昂且不必要的对象创建。

+

62. 其它类型更合适时应避免使用字符串

+

本条目讨论一些不应该使用字符串的场景

+
    +
  1. 字符串是枚举类型的糟糕替代品
  2. +
  3. 字符串是聚合类型的糟糕替代品。如果一个对象有多个字段,将其连接成一个字符串会出现许多问题,更好的方法是用私有静态成员类表示聚合
  4. +
  5. 字符串是功能的糟糕替代品。例如全局缓存池要求 key 不能重复,如果用字符串做 key 可能在不同线程中出现问题
  6. +
+

总之,当存在或可以编写更好的数据类型时,应避免将字符串用来表示对象。如果使用不当,字符串比其他类型更麻烦、灵活性更差、速度更慢、更容易出错。字符串经常被误用的类型包括基本类型、枚举和聚合类型。

+

63. 当心字符串连接引起的性能问题

+

字符串连接符(+)连接 n 个字符串的时间复杂度是 n2n^2。这是字符串不可变导致的

+

如果要连接的字符串数量较多,可以使用 StringBuilder 代替 String

+

64. 通过接口引用对象

+
    +
  1. +

    如果存在合适的接口类型,那么应该使用接口类型声明参数、返回值、变量和字段。好处是代码可以非常方便切换性能更好或功能更丰富的实现,例如 HashMap 替换成 LinkedHashMap 可以保证迭代顺序和插入一致

    +
  2. +
  3. +

    如果没有合适的接口存在,那么用类引用对象是完全合适的。

    +
      +
    • 值类,如 String 和 BigInteger。值类很少在编写时考虑到多个实现。它们通常是 final 的,很少有相应的接口。使用这样的值类作为参数、变量、字段或返回类型非常合适。
    • +
    • 如果一个对象属于一个基于类的框架,那么就用基类引用它
    • +
    • 接口的实现类有接口没有的方法,例如 PriorityQueue 类有 Queue 接口没有的比较器方法
    • +
    +
  4. +
+

65. 接口优于反射

+

反射有几个缺点:

+
    +
  1. 失去了编译时类型检查的所有好处,包括异常检查。如果一个程序试图反射性地调用一个不存在的或不可访问的方法,它将在运行时失败,除非你采取了特殊的预防措施(大量 try-catch)
  2. +
  3. 反射代码既笨拙又冗长。写起来很乏味,读起来也很困难
  4. +
  5. 反射调用方法比普通调用方法更慢
  6. +
+

反射优点:

+

对于许多程序,它们必须用到在编译时无法获取的类,在编译时存在一个适当的接口或父类来引用该类(Item-64)。如果是这种情况,可以用反射方式创建实例,并通过它们的接口或父类正常地访问它们。

+

例如,这是一个创建 Set<String> 实例的程序,类由第一个命令行参数指定。程序将剩余的命令行参数插入到集合中并打印出来。不管第一个参数是什么,程序都会打印剩余的参数,并去掉重复项。然而,打印这些参数的顺序取决于第一个参数中指定的类。如果你指定 java.util.HashSet,它们显然是随机排列的;如果你指定 java.util.TreeSet,它们是按字母顺序打印的,因为 TreeSet 中的元素是有序的

+
// Reflective instantiation with interface access
+public static void main(String[] args) {
+
+    // Translate the class name into a Class object
+    Class<? extends Set<String>> cl = null;
+    try {
+        cl = (Class<? extends Set<String>>) // Unchecked cast!
+        Class.forName(args[0]);
+    } catch (ClassNotFoundException e) {
+        fatalError("Class not found.");
+    }
+
+    // Get the constructor
+    Constructor<? extends Set<String>> cons = null;
+    try {
+        cons = cl.getDeclaredConstructor();
+    } catch (NoSuchMethodException e) {
+        fatalError("No parameterless constructor");
+    }
+
+    // Instantiate the set
+    Set<String> s = null;
+    try {
+        s = cons.newInstance();
+    } catch (IllegalAccessException e) {
+        fatalError("Constructor not accessible");
+    } catch (InstantiationException e) {
+        fatalError("Class not instantiable.");
+    } catch (InvocationTargetException e) {
+        fatalError("Constructor threw " + e.getCause());
+    } catch (ClassCastException e) {
+        fatalError("Class doesn't implement Set");
+    }
+
+    // Exercise the set
+    s.addAll(Arrays.asList(args).subList(1, args.length));
+    System.out.println(s);
+}
+
+private static void fatalError(String msg) {
+    System.err.println(msg);
+    System.exit(1);
+}
+
+

反射的合法用途(很少)是管理类对运行时可能不存在的其他类、方法或字段的依赖关系。如果你正在编写一个包,并且必须针对其他包的多个版本运行,此时反射将非常有用。该技术是根据支持包所需的最小环境(通常是最老的版本)编译包,并反射性地访问任何较新的类或方法。如果你试图访问的新类或方法在运行时不存在,要使此工作正常进行,则必须采取适当的操作。适当的操作可能包括使用一些替代方法来完成相同的目标,或者使用简化的功能进行操作

+

总之,反射是一种功能强大的工具,对于某些复杂的系统编程任务是必需的,但是它有很多缺点。如果编写的程序必须在编译时处理未知的类,则应该尽可能只使用反射实例化对象,并使用在编译时已知的接口或父类访问对象。

+

66. 明智地使用本地方法

+

JNI 允许 Java 调用本地方法,这些方法是用 C 或 C++ 等本地编程语言编写的。

+

历史上,本地方法主要有三个用途:

+
    +
  1. 提供对特定于平台的设施(如注册中心)的访问
  2. +
  3. 提供对现有本地代码库的访问,包括提供对遗留数据访问
  4. +
  5. 通过本地语言编写应用程序中注重性能的部分,以提高性能
  6. +
+

关于用途一:随着 Java 平台的成熟,它提供了对许多以前只能在宿主平台中上找到的特性。例如,Java 9 中添加的流 API 提供了对 OS 进程的访问。在 Java 中没有等效库时,使用本地方法来使用本地库也是合法的。

+

关于用途3:在早期版本(Java 3 之前),这通常是必要的,但是从那时起 JVM 变得更快了。对于大多数任务,现在可以在 Java 中获得类似的性能

+

本地方法的缺点:

+
    +
  1. 会受到内存损坏错误的影响
  2. +
  3. 垃圾收集器无法自动追踪本地方法的内存使用情况,导致性能下降
  4. +
+

总之,在使用本地方法之前要三思。一般很少需要使用它们来提高性能。如果必须使用本地方法来访问底层资源或本地库,请尽可能少地使用本地代码,并对其进行彻底的测试。本地代码中的一个错误就可以破坏整个应用程序。

+

67. 明智地进行优化

+

关于优化的三条名言

+
    +
  1. 以效率的名义(不一定能达到效率)犯下的计算错误比任何其他原因都要多——包括盲目的愚蠢
  2. +
  3. 不要去计较效率上的一些小小的得失,在 97% 的情况下,不成熟的优化才是一切问题的根源。
  4. +
  5. 在优化方面,我们应该遵守两条规则:规则 1:不要进行优化。规则 2 (仅针对专家):还是不要进行优化,也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化。
  6. +
+

在设计系统时,我们要仔细考虑架构,好的架构允许它在后面优化,不良的架构导致很难优化。设计中最难更改的是组件之间以及组件与外部世界交互的组件,主要是 API、线路层协议和数据持久化格式,尽量避免做限制性能的设计

+

JMH 是一个微基准测试框架,主要是基于方法层面的基准测试,精度可以达到纳秒级

+

总而言之,不要努力写快的程序,要努力写好程序;速度自然会提高。但是在设计系统时一定要考虑性能,特别是在设计API、线路层协议和持久数据格式时。当你完成了系统的构建之后,请度量它的性能。如果足够快,就完成了。如果没有,利用分析器找到问题的根源,并对系统的相关部分进行优化。第一步是检查算法的选择:再多的底层优化也不能弥补算法选择的不足。根据需要重复这个过程,在每次更改之后测量性能,直到你满意为止。

+

68. 遵守被广泛认可的命名约定

+

Java 平台有一组完善的命名约定,其中许多约定包含在《The Java Language Specification》。不严格地讲,命名约定分为两类:排版和语法。

+

排版

+

和排版有关的命名约定,包括包、类、接口、方法、字段和类型变量

+
    +
  1. 包名和模块名应该是分层的,组件之间用句点分隔。组件应该由小写字母组成,很少使用数字。任何在你的组织外部使用的包,名称都应该以你的组织的 Internet 域名开头,并将组件颠倒过来,例如,edu.cmu、com.google、org.eff。以 java 和 javax 开头的标准库和可选包是这个规则的例外。用户不能创建名称以 java 或 javax 开头的包或模块。
    +包名的其余部分应该由描述包的一个或多个组件组成。组件应该很短,通常为 8 个或更少的字符。鼓励使用有意义的缩写,例如 util 而不是 utilities。缩写词是可以接受的,例如 awt。组件通常应该由一个单词或缩写组成。
  2. +
  3. 类和接口名称,包括枚举和注释类型名称,应该由一个或多个单词组成,每个单词的首字母大写,例如 List 或 FutureTask
  4. +
  5. 方法和字段名遵循与类和接口名相同的排版约定,除了方法或字段名的第一个字母应该是小写,例如 remove 或 ensureCapacity
  6. +
  7. 前面规则的唯一例外是「常量字段」,它的名称应该由一个或多个大写单词组成,由下划线分隔,例如 VALUES 或 NEGATIVE_INFINITY
  8. +
  9. 局部变量名与成员名具有类似的排版命名约定,但允许使用缩写,也允许使用单个字符和短字符序列,它们的含义取决于它们出现的上下文,例如 i、denom、houseNum。输入参数是一种特殊的局部变量。它们的命名应该比普通的局部变量谨慎得多,因为它们的名称是方法文档的组成部分。
  10. +
  11. 类型参数名通常由单个字母组成。最常见的是以下五种类型之一:T 表示任意类型,E 表示集合的元素类型,K 和 V 表示 Map 的键和值类型,X 表示异常。函数的返回类型通常为 R。任意类型的序列可以是 T、U、V 或 T1、T2、T3。
  12. +
+

为了快速参考,下表显示了排版约定的示例。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Identifier TypeExample
Package or moduleorg.junit.jupiter.api, com.google.common.collect
Class or InterfaceStream, FutureTask, LinkedHashMap,HttpClient
Method or Fieldremove, groupingBy, getCrc
Constant FieldMIN_VALUE, NEGATIVE_INFINITY
Local Variablei, denom, houseNum
Type ParameterT, E, K, V, X, R, U, V, T1, T2
+

语法

+

语法命名约定比排版约定更灵活,也更有争议

+
    +
  1. 可实例化的类,包括枚举类型,通常使用一个或多个名词来命名,例如 Thread、PriorityQueue 或 ChessPiece
  2. +
  3. 不可实例化的工具类通常用复数名词来命名,例如 Collectors 和 Collections
  4. +
  5. 接口的名称类似于类,例如 Collection 或 Comparator,或者以 able 或 ible 结尾的形容词,例如 Runnable、Iterable 或 Accessible
  6. +
  7. 因为注解类型有很多的用途,所以没有哪部分占主导地位。名词、动词、介词和形容词都很常见,例如,BindingAnnotation、Inject、ImplementedBy 或 Singleton。
  8. +
  9. 执行某些操作的方法通常用动词或动词短语(包括对象)命名,例如,append 或 drawImage。
  10. +
  11. 返回布尔值的方法的名称通常以单词 is 或 has(通常很少用)开头,后面跟一个名词、一个名词短语,或者任何用作形容词的单词或短语,例如 isDigit、isProbablePrime、isEmpty、isEnabled 或 hasSiblings。
  12. +
  13. 返回被调用对象的非布尔函数或属性的方法通常使用以 get 开头的名词、名词短语或动词短语来命名,例如 size、hashCode 或 getTime。有一种说法是,只有第三种形式(以 get 开头)才是可接受的,但这种说法几乎没有根据。前两种形式的代码通常可读性更强 。以 get 开头的形式起源于基本过时的 Java bean 规范,该规范构成了早期可复用组件体系结构的基础。有一些现代工具仍然依赖于 bean 命名约定,你应该可以在任何与这些工具一起使用的代码中随意使用它。如果类同时包含相同属性的 setter 和 getter,则遵循这种命名约定也有很好的先例。在本例中,这两个方法通常被命名为 getAttribute 和 setAttribute。
  14. +
  15. 转换对象类型(返回不同类型的独立对象)的实例方法通常称为 toType,例如 toString 或 toArray。
  16. +
  17. 返回与接收对象类型不同的视图(Item-6)的方法通常称为 asType,例如 asList
  18. +
  19. 返回与调用它们的对象具有相同值的基本类型的方法通常称为类型值,例如 intValue
  20. +
  21. 静态工厂的常见名称包括 from、of、valueOf、instance、getInstance、newInstance、getType 和 newType
  22. +
  23. 字段名的语法约定没有类、接口和方法名的语法约定建立得好,也不那么重要,因为设计良好的 API 包含很少的公开字段。类型为 boolean 的字段的名称通常类似于 boolean 访问器方法,省略了开头「is」,例如 initialized、composite。其他类型的字段通常用名词或名词短语来命名,如 height、digits 和 bodyStyle。局部变量的语法约定类似于字段的语法约定,但要求更少。
  24. +
+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/08/13/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\271\235\357\274\211/index.html" "b/2022/08/13/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\271\235\357\274\211/index.html" new file mode 100644 index 0000000..69ca060 --- /dev/null +++ "b/2022/08/13/Effective-Java\345\255\246\344\271\240\347\254\224\350\256\260\357\274\210\344\271\235\357\274\211/index.html" @@ -0,0 +1,864 @@ + + + + + + + + + + + + + + + + + + + + Effective-Java学习笔记(九) | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Effective-Java学习笔记(九) +

+ + + + +
+
+ +

第十章 异常

+

69. 仅在确有异常条件下使用异常

+

有人认为用 try-catch 和 while(true) 遍历数组比用 for-each 性能更好,因为 for-each 由编译器隐藏了边界检查,而 try-catch 代码中不包含检查

+
// Horrible abuse of exceptions. Don't ever do this!
+try {
+    int i = 0;
+    while(true){
+        range[i++].climb();
+    }
+    catch (ArrayIndexOutOfBoundsException e) {}
+}
+
+

这个想法有三个误区:

+
    +
  1. 因为异常是为特殊情况设计的,所以 JVM 实现几乎不会让它们像显式测试一样快。
  2. +
  3. 将代码放在 try-catch 块中会抑制 JVM 可能执行的某些优化。
  4. +
  5. 遍历数组的标准习惯用法不一定会导致冗余检查。许多 JVM 实现对它们进行了优化。
  6. +
+

基于异常的循环除了不能提高性能外,还容易被异常隐藏循环中的 bug。因此,异常只适用于确有异常的情况;它们不应该用于一般的控制流程。

+

一个设计良好的 API 不能迫使其客户端为一般的控制流程使用异常。调用具有「状态依赖」方法的类,通常应该有一个单独的「状态测试」方法,表明是否适合调用「状态依赖」方法。例如,Iterator 接口具有「状态依赖」的 next 方法和对应的「状态测试」方法 hasNext。

+
for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {
+    Foo foo = i.next();
+    ...
+}
+
+

如果 Iterator 缺少 hasNext 方法,客户端将被迫这样做:

+
// Do not use this hideous code for iteration over a collection!
+try {
+    Iterator<Foo> i = collection.iterator();
+    while(true) {
+        Foo foo = i.next();
+        ...
+    }
+}
+catch (NoSuchElementException e) {
+}
+
+

这与一开始举例的对数组进行迭代的例子非常相似,除了冗长和误导之外,基于异常的循环执行效果可能很差,并且会掩盖系统中不相关部分的 bug。

+

提供单独的「状态测试」方法的另一种方式,就是让「状态依赖」方法返回一个空的 Optional 对象(Item-55),或者在它不能执行所需的计算时返回一个可识别的值,比如 null。

+

状态测试方法、Optional、可识别的返回值之间的选择如下:

+
    +
  1. 如果要在没有外部同步的情况下并发地访问对象,或者受制于外部条件的状态转换,则必须使用 Optional 或可识别的返回值,因为对象的状态可能在调用「状态测试」方法与「状态依赖」方法的间隔中发生变化。
  2. +
  3. 如果一个单独的「状态测试」方法重复「状态依赖」方法的工作,从性能问题考虑,可能要求使用 Optional 或可识别的返回值
  4. +
  5. 在所有其他条件相同的情况下,「状态测试」方法略优于可识别的返回值。它提供了较好的可读性,而且不正确的使用可能更容易被检测:如果你忘记调用「状态测试」方法,「状态依赖」方法将抛出异常,使错误显而易见;
  6. +
  7. 如果你忘记检查一个可识别的返回值,那么这个 bug 可能很难发现。但是这对于返回 Optional 对象的方式来说不是问题。
  8. +
+

总之,异常是为确有异常的情况设计的。不要将它们用于一般的控制流程,也不要编写强制其他人这样做的 API。

+

70. 对可恢复情况使用 checked 异常,对编程错误使用运行时异常

+

Java 提供了三种可抛出项:checked 异常、运行时异常和错误。决定是使用 checked 异常还是 unchecked 异常的基本规则是:使用 checked 异常的情况是为了合理地期望调用者能够从中恢复。

+

有两种 unchecked 的可抛出项:运行时异常和错误。它们在行为上是一样的:都是可抛出的,通常不需要也不应该被捕获。如果程序抛出 unchecked 异常或错误,通常情况下是不可能恢复的,如果继续执行,弊大于利。如果程序没有捕获到这样的可抛出项,它将导致当前线程停止,并发出适当的错误消息。

+

运行时异常

+

运行时异常用来指示编程错误。大多数运行时异常都表示操作违反了先决条件。违反先决条件是指使用 API 的客户端未能遵守 API 规范所建立的约定。例如,数组访问约定指定数组索引必须大于等于 0 并且小于等于 length-1 (length:数组长度)。ArrayIndexOutOfBoundsException 表示违反了此先决条件

+

这个建议存在的问题是:并不总能清楚是在处理可恢复的条件还是编程错误。例如,考虑资源耗尽的情况,这可能是由编程错误(如分配一个不合理的大数组)或真正的资源短缺造成的。如果资源枯竭是由于暂时短缺或暂时需求增加造成的,这种情况很可能是可以恢复的。对于 API 设计人员来说,判断给定的资源耗尽实例是否允许恢复是一个问题。如果你认为某个条件可能允许恢复,请使用 checked 异常;如果没有,则使用运行时异常。如果不清楚是否可以恢复,最好使用 unchecked 异常

+

错误

+

虽然 Java 语言规范没有要求,但有一个约定俗成的约定,即错误保留给 JVM 使用,以指示:资源不足、不可恢复故障或其他导致无法继续执行的条件。考虑到这种约定被大众认可,所以最好不要实现任何新的 Error 子类。因此,你实现的所有 unchecked 异常都应该继承 RuntimeException(直接或间接)。不仅不应该定义 Error 子类,而且除了 AssertionError 之外,不应该抛出它们。

+

自定义异常

+

自定义异常继承 Throwable 类,Java 语言规范把它们当做普通 checked 异常(普通 checked 异常是 Exception 的子类,但不是 RuntimeException的子类)。不要使用自定义异常,它会让 API 的用户困惑

+

异常附加信息

+

API 设计人员常常忘记异常是成熟对象,可以为其定义任意方法。此类方法的主要用途是提供捕获异常的代码,并提供有关引发异常的附加信息。如果缺乏此类方法,程序员需要自行解析异常的字符串表示以获取更多信息。这是极坏的做法

+

因为 checked 异常通常表示可恢复的条件,所以这类异常来说,设计能够提供信息的方法来帮助调用者从异常条件中恢复尤为重要。例如,假设当使用礼品卡购物由于资金不足而失败时,抛出一个 checked 异常。该异常应提供一个访问器方法来查询差额。这将使调用者能够将金额传递给购物者。

+

总而言之,为可恢复条件抛出 checked 异常,为编程错误抛出 unchecked 异常。当有疑问时,抛出 unchecked 异常。不要定义任何既不是 checked 异常也不是运行时异常的自定义异常。应该为 checked 异常设计相关的方法,如提供异常信息,以帮助恢复。

+

71. 避免不必要地使用 checked 异常

+

合理抛出 checked 异常可以提高程序可靠性。过度使用会使得调用它的方法多次 try-catch 或抛出,给 API 用户带来负担,尤其是 Java8 中,抛出checked 异常的方法不能直接在流中使用。

+

只有在正确使用 API 也无法避免异常且使用 API 的程序员在遇到异常时可以采取一些有用的操作才能使用 checked 异常,否则抛出 unchecked 异常

+

如果 checked 异常是方法抛出的唯一 checked 异常,那么 checked 异常给程序员带来的额外负担就会大得多。如果还有其他 checked 异常,则该方法一定已经在 try 块中了,因此该异常最多需要另一个 catch 块而已。如果一个方法抛出单个 checked 异常,那么这个异常就是该方法必须出现在 try 块中而不能直接在流中使用的唯一原因。在这种情况下,有必要问问自己是否有办法避免 checked 异常。

+

消除 checked 异常的最简单方法是返回所需结果类型的 Optional 对象(Item-55)。该方法只返回一个空的 Optional 对象,而不是抛出一个 checked 异常。这种技术的缺点是,该方法不能返回任何详细说明其无法执行所需计算的附加信息。相反,异常具有描述性类型,并且可以导出方法来提供附加信息(Item-70)

+

总之,如果谨慎使用,checked 异常可以提高程序的可靠性;当过度使用时,它们会使 API 难以使用。如果调用者不应从失败中恢复,则抛出 unchecked 异常。如果恢复是可能的,并且你希望强制调用者处理异常情况,那么首先考虑返回一个 Optional 对象。只有当在失败的情况下,提供的信息不充分时,你才应该抛出一个 checked 异常。

+

72. 鼓励复用标准异常

+

Java 库提供了一组异常,涵盖了大多数 API 的大多数异常抛出需求。

+

复用标准异常有几个好处:

+
    +
  1. 使你的 API 更容易学习和使用,因为它符合程序员已经熟悉的既定约定
  2. +
  3. 使用你的 API 的程序更容易阅读,因为它们不会因为不熟悉的异常而混乱
  4. +
  5. 更少的异常类意味着更小的内存占用和更少的加载类的时间
  6. +
+

常见的被复用的异常:

+
    +
  1. +

    IllegalArgumentException。通常是调用者传入不合适的参数时抛出的异常

    +
  2. +
  3. +

    IllegalStateException。如果因为接收对象的状态导致调用非法,则通常会抛出此异常。例如,调用者试图在对象被正确初始化之前使用它

    +

    可以说,每个错误的方法调用都归结为参数非法或状态非法,但是有一些异常通常用于某些特定的参数非法和状态非法。如果调用者在禁止空值的参数中传递 null,那么按照惯例,抛出 NullPointerException 而不是 IllegalArgumentException。类似地,如果调用者将表示索引的参数中的超出范围的值传递给序列,则应该抛出 IndexOutOfBoundsException,而不是 IllegalArgumentException

    +
  4. +
  5. +

    ConcurrentModificationException。如果一个对象被设计为由单个线程使用(或与外部同步),并且检测到它正在被并发地修改,则应该抛出该异常。因为不可能可靠地检测并发修改,所以该异常充其量只是一个提示。

    +
  6. +
  7. +

    UnsupportedOperationException。如果对象不支持尝试的操作,则抛出此异常。它很少使用,因为大多数对象都支持它们的所有方法。此异常用于一个类没有实现由其实现的接口定义的一个或多个可选操作。例如,对于只支持追加操作的 List 实现,试图从中删除元素时就会抛出这个异常

    +
  8. +
+

不要直接复用 Exception、RuntimeException、Throwable 或 Error

+

此表总结了最常见的可复用异常:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExceptionOccasion for Use
IllegalArgumentExceptionNon-null parameter value is inappropriate(非空参数值不合适)
IllegalStateExceptionObject state is inappropriate for method invocation(对象状态不适用于方法调用)
NullPointerExceptionParameter value is null where prohibited(禁止参数为空时仍传入 null)
IndexOutOfBoundsExceptionIndex parameter value is out of range(索引参数值超出范围)
ConcurrentModificationExceptionConcurrent modification of an object has been detected where it is prohibited(在禁止并发修改对象的地方检测到该动作)
UnsupportedOperationExceptionObject does not support method(对象不支持该方法调用)
+

其它异常如果有合适的复用场景也可以复用,例如,如果你正在实现诸如复数或有理数之类的算术对象,那么复用 ArithmeticException 和 NumberFormatException 是合适的

+

73. 抛出适合底层抽象异常的高层异常

+

当方法抛出一个与它所执行的任务没有明显关联的异常时,这是令人不安的。这种情况经常发生在由方法传播自低层抽象抛出的异常。它不仅令人不安,而且让实现细节污染了上层的 API。

+

为了避免这个问题,高层应该捕获低层异常,并确保抛出的异常可以用高层抽象解释。 这个习惯用法称为异常转换:

+
// Exception Translation
+try {
+    ... // Use lower-level abstraction to do our bidding
+} catch (LowerLevelException e) {
+    throw new HigherLevelException(...);
+}
+
+

下面是来自 AbstractSequentialList 类的异常转换示例,该类是 List 接口的一个框架实现(Item-20)。在本例中,异常转换是由 List<E> 接口中的 get 方法规范强制执行的:

+
/**
+* Returns the element at the specified position in this list.
+* @throws IndexOutOfBoundsException if the index is out of range
+* ({@code index < 0 || index >= size()}).
+*/
+public E get(int index) {
+    ListIterator<E> i = listIterator(index);
+    try {
+        return i.next();
+    }
+    catch (NoSuchElementException e) {
+        throw new IndexOutOfBoundsException("Index: " + index);
+    }
+}
+
+

如果低层异常可能有助于调试高层异常的问题,则需要一种称为链式异常的特殊异常转换形式。低层异常(作为原因)传递给高层异常,高层异常提供一个访问器方法(Throwable 的 getCause 方法)来检索低层异常:

+
// Exception Chaining
+try {
+    ... // Use lower-level abstraction to do our bidding
+}
+catch (LowerLevelException cause) {
+    throw new HigherLevelException(cause);
+}
+
+

高层异常的构造函数将原因传递给能够接收链式异常的父类构造函数,因此它最终被传递给 Throwable 的一个接收链式异常的构造函数,比如 Throwable(Throwable):

+
// Exception with chaining-aware constructor
+class HigherLevelException extends Exception {
+    HigherLevelException(Throwable cause) {
+        super(cause);
+    }
+}
+
+

大多数标准异常都有接收链式异常的构造函数。对于不支持链式异常的异常,可以使用 Throwable 的 initCause 方法设置原因。异常链不仅允许你以编程方式访问原因(使用 getCause),而且还将原因的堆栈跟踪集成到更高层异常的堆栈跟踪中。

+

虽然异常转换优于底层异常的盲目传播,但它不应该被过度使用。在可能的情况下,处理底层异常的最佳方法是确保底层方法避免异常。有时,你可以在将高层方法的参数传递到底层之前检查它们的有效性。

+

如果不可能从底层防止异常,那么下一个最好的方法就是让高层静默处理这些异常,使较高层方法的调用者免受底层问题的影响。在这种情况下,可以使用一些适当的日志工具(如 java.util.logging)来记录异常。这允许程序员研究问题,同时将客户端代码和用户与之隔离。

+

总之,如果无法防止或处理来自底层的异常,则使用异常转换,但要保证底层方法的所有异常都适用于较高层。链式异常提供了兼顾两方面的最佳服务:允许抛出适当的高层异常,同时捕获并分析失败的潜在原因

+

74. 为每个方法记录会抛出的所有异常

+
    +
  1. 始终单独声明 checked 异常,并使用 Javadoc 的 @throw 标记精确记录每次抛出异常的条件。如果一个方法抛出多个异常,不要使用快捷方式声明这些异常的父类。作为一个极端的例子,即不要在公共方法声明 throws Exception,除了只被 JVM 调用的 main方法
  2. +
  3. unchecked 异常不要声明 throws,但应该像 checked 异常一样用 Javadoc 记录他们。特别是接口中的方法要记录可能抛出的 unchecked 异常。
  4. +
+

如果一个类中的许多方法都因为相同的原因抛出异常,你可以在类的文档注释中记录异常, 而不是为每个方法单独记录异常。一个常见的例子是 NullPointerException。类的文档注释可以这样描述:「如果在任何参数中传递了 null 对象引用,该类中的所有方法都会抛出 NullPointerException」

+

总之,记录你所编写的每个方法可能引发的每个异常。对于 unchecked 异常、checked 异常、抽象方法、实例方法都是如此。应该在文档注释中采用 @throw 标记的形式。在方法的 throws 子句中分别声明每个 checked 异常,但不要声明 unchecked 异常。如果你不记录方法可能抛出的异常,其他人将很难或不可能有效地使用你的类和接口。

+

75. 异常详细消息中应包含捕获失败的信息

+

程序因为未捕获异常而失败时,系统会自动调用异常的 toString 方法打印堆栈信息,堆栈信息包含异常的类名及详细信息。异常的详细消息应该包含导致异常的所有参数和字段的值。例如,IndexOutOfBoundsException 的详细消息应该包含下界、上界和未能位于下界之间的索引值。

+

异常详细信息不用过于冗长,程序失败时可以通过阅读文档和源代码收集信息,确保异常包含足够的信息的一种方法是在构造函数中配置异常信息,例如,IndexOutOfBoundsException 构造函数不包含 String 参数,而是像这样:

+
/**
+* Constructs an IndexOutOfBoundsException.
+**
+@param lowerBound the lowest legal index value
+* @param upperBound the highest legal index value plus one
+* @param index the actual index value
+*/
+public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {
+    // Generate a detail message that captures the failure
+    super(String.format("Lower bound: %d, Upper bound: %d, Index: %d",lowerBound, upperBound, index));
+    // Save failure information for programmatic access
+    this.lowerBound = lowerBound;
+    this.upperBound = upperBound;
+    this.index = index;
+}
+
+

76. 尽力保证故障原子性

+

失败的方法调用应该使对象处于调用之前的状态。 具有此属性的方法称为具备故障原子性。

+

有几种实现故障原子性的方式:

+

关于不可变对象

+
    +
  1. 不可变对象在创建后永远处于一致状态
  2. +
+

关于可变对象:

+
    +
  1. 在修改状态前,先执行可能抛出异常的操作,例如检查状态,不合法就抛出异常
  2. +
  3. 在临时副本上操作,成功后替换原来的对象,不成功不影响原来的对象。例如,一些排序函数会将入参 list 复制到数组中,对数组进行排序,再转换成 list
  4. +
  5. 编写回滚代码,主要用于持久化的数据
  6. +
+

有些情况是不能保证故障原子性的,例如,多线程不同步修改对象,对象可能处于不一致状态,当捕获到 ConcurrentModificationException 后对象不可恢复

+

总之,作为方法规范的一部分,生成的任何异常都应该使对象保持在方法调用之前的状态。如果违反了这条规则,API 文档应该清楚地指出对象将处于什么状态。

+

77. 不要忽略异常

+

异常要么 try-catch 要么抛出,不要写空的 catch 块,如果这样做,写上注释

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2022/12/03/2022.06-2022.11\345\256\236\344\271\240\346\200\273\347\273\223/index.html" "b/2022/12/03/2022.06-2022.11\345\256\236\344\271\240\346\200\273\347\273\223/index.html" new file mode 100644 index 0000000..1149d80 --- /dev/null +++ "b/2022/12/03/2022.06-2022.11\345\256\236\344\271\240\346\200\273\347\273\223/index.html" @@ -0,0 +1,668 @@ + + + + + + + + + + + + + + + + + + + + 2022.06-2022.11实习总结 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 2022.06-2022.11实习总结 +

+ + + + +
+
+ +

基于JSON Schema的配置平台

+

背景:这是公司的新人入职练手项目,总共两周时间,实现一个基于JSON Schema的表单配置平台。
+需求:表单配置指的是通过可视化界面配置 表单的每个字段,包括名称、输入类型、输入限制等。mentor给了一个方向叫JSON Schema
+调研:JSON Schema是描述json数据的元数据,本身也是json字符串,一般两个作用,1. 后端用 JSON Schema 对前端传的json传进行格式校验;2. 前端通过 JSON Schema生成表单

+

图 3

+

开发:前端使用Vue、后端使用Spring Boot
+难点:JSON Schema和表单的双向转换。用户可以手动编辑JSON Schema生成表单项,也可以通过可视化界面增加表单项来修改JSON Schema。
+解决方案:尝试过写一套解析方案,但是dom操作太复杂作罢。调研了一些开源方案,最终选用vue-json-schema-form

+

Excel比对与合并系统

+

背景:接手的第一个项目,关于Excel比对与合并,主要参与系统的优化与维护工作
+主要工作:

+
    +
  1. 批量文件比对的多线程优化(比对方法涉及对象完全栈封闭)
  2. +
  3. 批量文件合并OOM排查(合并需要先反序列化比对结果,若有多个版本的比对结果需要合并到另一分支上的同名文件,需要循环处理,每个比对结果合并之后需要置空,否则内存无法释放,排查工具:visualvm,发现调用合并方法时,Minor GC非常快,内存居高不下导致OOM;根本原因:自己开发的Excel解析工具+Java自带的序列化导致序列化产物非常大)
  4. +
+

算法介绍可以参看《Excel比对与合并系统》

+

助理系统

+

需求:公司员工反馈行政问题都是在公司的聊天软件里反馈,反馈途径包括事业群、私聊行政助理、以及服务号反馈(类似微信公众号),行政助理需要在多个系统进行处理,助理系统为助理统一了消息来源,助理可以在助理系统中回复所有渠道的问题反馈。
+调研:spring-boot-starter-websocket,实现了客户端与服务器全双工通信
+难点:助理系统需要给每个反馈问题的员工生成唯一的对话,初次反馈消息时,快速发送消息会创建多个对话。这是因为后端多线程处理消息,每个线程都先去数据库查询此条消息的员工是否存在对话,如果不存在就创建。这里出现了并发经典错误 check-then-act。
+解决方案:给会话表的员工id字段建唯一索引,插入新会话使用insert ignore。

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2023/01/13/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\345\255\246\344\271\240\347\254\224\350\256\260/index.html" "b/2023/01/13/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\345\255\246\344\271\240\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000..b84aca3 --- /dev/null +++ "b/2023/01/13/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\345\255\246\344\271\240\347\254\224\350\256\260/index.html" @@ -0,0 +1,1386 @@ + + + + + + + + + + + + + + + + + + + + Java并发编程实战学习笔记 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Java并发编程实战学习笔记 +

+ + + + +
+
+ +

本书分成四部分,
+第一部分是基础,主要内容是并发的基础概念和线程安全,以及如何用java类库提供的并发构建块组成线程安全的类(2-5章节)

+

第二部分是构建并发应用程序,主要内容是如何利用线程来提高并发应用的吞吐量和响应能力(6-9章节)

+

第三部分是活跃性、性能和测试,主要内容是如何确保并发程序能够按照你的要求执行,并且具有可接受的性能(10-12章节)

+

第四部分是高级主题,涵盖了可能只有有经验的开发人员才会感兴趣的主题:显式锁、原子变量、非阻塞算法和开发自定义同步器(13-16章节)

+

Part 1 基础

+

Chapter 1 简介

+

1.1 并发简史

+

早期计算机没有操作系统,程序独占所有资源,一次只能有一个程序运行,效率低下

+

操作系统出现后,程序(进程)可以并发运行,由操作系统分配资源,进程互相隔离,必要时依靠粗粒度的通信机制:sockets、signal handlers、shared memory等机制通信

+

进程提高了系统吞吐量和资源利用率,线程的出现也是这个原因,线程有时被称为轻量级进程,大多数现代操作系统将线程而不是进程视为调度的基本单位,同一程序的多个线程可以同时在多个CPU上调度,如果没有同步机制协调对共享数据的访问,一个线程可能会修改另一个线程正在使用的变量,导致不可预测的结果

+

1.2 线程优势

+
    +
  • 减小开发和维护开销
  • +
  • 提高复杂应用的性能
  • +
  • 提高GUI的响应能力
  • +
  • 简化JVM实现
  • +
+

1.3 线程风险

+

竞争(多个线程以未知顺序访问资源)
+活跃性(死锁,饥饿,活锁)
+性能(频繁切换导致开销过大)

+

1.4 无处不在的线程

+
    +
  • 框架通过在框架线程中调用应用程序代码将并发性引入到应用程序中,在代码中将不可避免的访问应用程序状态,因此所有访问这些状态的代码路径都必须是线程安全的
  • +
  • Timer类,TimerTask在Timer管理的线程中执行
  • +
  • Servlet(每个请求使用一个线程同时执行Servlet)
  • +
  • RMI(由RMI负责打包拆包远程对象)
  • +
  • Swing(具有异步性)
  • +
+

Chapter 2 线程安全

+

多个线程访问同一个可变的状态变量时没有使用合适的同步机制,可以用以下方法修复:

+
    +
  • 不在线程间共享该变量
  • +
  • 将变量变为常量
  • +
  • 访问时候使用同步
  • +
+

2.1 什么是线程安全?

+

如果一个类被多线程访问,不管线程调度或交叉执行顺序如何,类的表现都是正确的,那么类是线程安全的

+

线程安全类封装任何需要的同步,因此客户端不需要提供自己的同步。

+

无状态的对象永远是线程安全的

+

2.2 原子性

+

竞争情况(race condition):由于不恰当的执行顺序导致出现不正确的结果,常发生在以下情况中:

+
    +
  • 读取-修改-写入,例子: 自增操作
  • +
  • 先检查后执行,例子:延迟初始化,不安全的单例模式,懒汉模式
  • +
+

第一种情况解决方式:使用juc里面的类,比如count可以用AtomicLong类型操作保证原子性
+第二种情况解决方式:加锁保证原子性

+

2.3 加锁

+

如果类有多个变量需要更新,即使它们的各自操作都是原子性的,也要把他们放在同一个原子操作中,方式是加锁。Java 提供了锁机制来增强原子性:synchronized

+

内置锁: synchronized 实例方法会将被调用方法的对象作为内置锁或监视锁,内置锁是互斥的,同一时刻最多只有一个线程拿到这个锁

+

可重入: 内置锁是可重入的,已经拿到锁的线程可以再次获取锁,实现方式是锁会(就是lock对象)关联一个持有者和计数值,持有者再次进入次数加一,退出减一,减到0会释放锁

+

2.4 用锁来保护状态

+

混合操作比如说读取-修改-写入和先检查后执行,需要保证原子性来避免竞争情况。

+

常见错误: 只有写入共享变量才需要同步
+原因:读取也需要同步,不然可能会看到过期值

+

每个共享的可变变量应该由同一个锁保护,常见的加锁习惯是将可变变量封装到一个对象中

+

对于不变性条件(invariant)中涉及的多个变量,这多个变量都需要用同一个锁保护,例如Servlet缓存了请求次数和请求数据(数组),不变性条件是请求数据的长度等于次数,这通过加锁来保证

+

2.5 活跃性和性能

+

给Servlet的方法声明syncronized极大降低了并发性,我们可以通过缩小同步块的范围,在保证线程安全的情况下提高并发性。合理的做法是将不影响共享状态的操作从同步块中排除

+

Chapter 3 共享对象

+

synchronized 块和方法可以确保操作的原子性执行,它还有另一个重要作用:内存可见性。我们不仅想要防止一个线程在另一个线程使用一个对象时修改它的状态,还想要确保当一个线程修改一个对象的状态时,其他线程可以看到最新更改

+

3.1 可见性

+
    +
  1. +

    过期数据(当一个线程修改数据,但其他线程不能立马看到)。 读取操作如果不同步,仍然能看到一个过期数据,这叫做最低安全性(过期数据至少是由之前的线程设置的,而不是随机值)

    +
  2. +
  3. +

    大多数变量都满足最低安全性,除了非volatile修饰的64位变量(double和long),jvm允许将64位操作拆解为2个 32位操作,读取这样的变量可能会出现过期值的高32位+新值的低32位的结果

    +
  4. +
  5. +

    内置锁保证可见性

    +
  6. +
  7. +

    volatile: 保证可见性,禁止指令重排,不保证原子性(使用场合:保证自身可见性,引用对象状态可见性,标识重要的生命周期事件)

    +

    当且仅当满足以下所有条件时,才应该使用volatile变量:

    +
      +
    • 对变量的写入不依赖于它的当前值,或者可以确保只有一个线程更新该值;
    • +
    • 该变量不会与其他状态变量一起纳入不变性条件
    • +
    • 在访问变量时,由于任何其他原因不需要加锁。
    • +
    +
  8. +
+

3.2 发布与逃逸

+

发布是指让对象在外部可见,常见方式是对象引用声明为 public static。发布对象的同时,任何通过非私有字段引用和方法调用链从发布对象中访问的对象也被发布了

+

逃逸是指对象的私有信息也对外可见了,比如发布一个对象包含一个私有数组,同时提供一个返回引用的get方法,外部可以通过引用修改内部私有数组

+

3.3 线程封闭

+

如果对象限制在一个线程中使用,即使对象不是线程安全的,也会自动线程安全

+

例子:Swing: 将组件和数据对象放到事件分发线程,其它线程访问不了这些对象;JDBC.Connection对象: 应用线程从数据库连接池中获取一个连接对象,连接对象由该线程独自使用

+

Java 提供 ThreadLocal 来实现线程封闭,程序员做的是阻止对象从线程中逃逸

+

线程封闭通常用来实现一个子系统,例如GUI,它是单线程的

+
    +
  1. +

    Ad-hoc封闭: 核线程封闭性的职责完全由程序实现来承担(脆弱,少用)

    +
  2. +
  3. +

    栈封闭: 只能通过局部变量访问对象(Java基本类型或者局部变量)

    +
  4. +
  5. +

    ThreadLocal类: 提供getter和setter,每个使用该变量的线程存有一份独立的副本

    +
  6. +
+

3.4 不可变

+

不可变对象永远是线程安全的

+

满足以下条件,对象才是不可变的:

+
    +
  • 构造函数之后状态不可修改
  • +
  • 所有域都是final
  • +
  • 对象正确创建(this引用没有在构造期间逃逸)
  • +
+

多个状态的对象需要保证线程安全,可以将状态封装到一个不可变类中,用volatile修饰不可变对象引用

+

3.5 安全发布

+
    +
  1. +

    不正确的发布对象会出现两个问题:其它线程会看到null或旧值;最糟糕的是其它线程看到最新的引用但是被引用的对象还是旧的

    +
  2. +
  3. +

    由于不可变对象很重要,Java内存模型为不可变对象的共享提供一种特殊的初始化安全性保证,不用同步也能安全发布

    +
  4. +
  5. +

    一个正确构造的对象可以通过以下方式安全发布:

    +
      +
    • 静态初始化函数中初始化一个对象引用
    • +
    • 引用保存到volatile域或者AtomicReference对象中
    • +
    • 引用保存到某个正确构造对象的final域
    • +
    • 引用保存到锁保护的域(容器也可)
    • +
    +
  6. +
  7. +

    不可变对象,可以放宽到事实不可变对象(对象在发布之后不会改变状态)

    +
  8. +
  9. +

    可变对象必须通过安全方式发布,并且必须是线程安全的或者锁保护起来

    +
  10. +
  11. +

    并发程序共享对象实用策略

    +
      +
    • 线程封闭
    • +
    • 只读共享
    • +
    • 线程安全共享:对象内部实现同步
    • +
    • 保护对象:锁机制
    • +
    +
  12. +
+

Chapter 4 组合对象

+

本章讨论如何将线程安全的组件组合成更大的组件或程序

+

4.1 设计一个线程安全的类

+
    +
  1. +

    在设计线程安全类的过程中,常会包含以下三个基本要素:

    +
      +
    • 找出构成对象状态的所有变量。
    • +
    • 找出约束状态变量的不变性条件和后验条件。
    • +
    • 建立对象状态的并发访问管理策略。
    • +
    +

    如果不了解对象的不变性条件和后验条件,就无法确保线程安全

    +
  2. +
  3. +

    依赖状态的方法需要先满足某种状态才能运行,即先验条件。java提供了 wait and notify 机制来等待先验条件成立,它依赖内置锁。更简单的实现方法是用java类库的阻塞队列或者信号量

    +
  4. +
  5. +

    一般情况下,状态所属权是封装状态的类,除非类公开可变对象的引用,这时候类只有共享权

    +
  6. +
+

4.2 实例封闭

+

在对象中封装数据,通过使用对象方法访问数据,从而更容易确保始终在持有适当锁的情况下访问数据。

+
    +
  1. Java监视器模式:封装可变状态到对象中,使用对象的内置锁保护状态,使用私有锁对象更有优势
  2. +
+

4.3 线程安全的委托

+
    +
  1. 将线程安全的职责委托给线程安全的类,例如计数器类不做同步处理,依赖AtomicLong类型达到线程安全
  2. +
  3. 可以将线程安全委托给多个基础状态变量,只要它们是独立的
  4. +
  5. 委托失效:多个变量间有不变性条件,比如大小关系等,需要加锁,除非复合操作也可以委托给变量
  6. +
  7. 如果一个状态变量是线程安全的,不参与任何限制其值的不变性条件,并且在任何操作中都没有禁止的状态转换,那么它就可以安全地发布。
  8. +
+

4.4 给现有的线程安全类加功能

+

继承方式(可能会因为子父类加的锁不一样线程不安全)

+
    +
  1. 客户端加锁,使用辅助类,若类的加锁依赖其它类,那么辅助类容易错误加锁
  2. +
  3. 组合方式,加锁策略完全由组合类提供
  4. +
+

4.5 文档化同步策略

+

为类的客户端记录线程安全保证;为其维护者记录其同步策略

+

Chapter 5 基础构建模块

+

在实际应用中,委托是创建线程安全类最有效的策略之一,本章介绍平台库的并发构建模块,例如线程安全的集合和各种可以协调线程控制流的同步器

+

5.1 同步集合

+

Vector、Hashtable,以及JDK1.2增加了 Collections.synchronizedXxx 创建同步包装类

+
    +
  1. 复合线程安全的类的方法可能不是线程安全的,例如复合方法调用size和get方法,中间可能被删掉元素导致size结果不对
  2. +
  3. 迭代器或者for-each不会锁定集合,在迭代过程中检测到集合变化时会抛出ConcurrentModificationException异常,检测是通过检测count值,但是没有同步,可能看到过期值
  4. +
  5. 隐藏的迭代器(某些操作底层隐藏着调用迭代器,比如集合的toString)
  6. +
+

5.2 并发集合

+

同步集合通过序列化对集合状态的所有访问来实现线程安全,性能低。Java 5增加了并发集合

+
    +
  1. ConcurrentHashMap,使用分段锁,具有弱一致性,同时size和isEmpty是估计并不精确,只有需要独占Map,才不建议使用该Map
  2. +
  3. CopyOnWriteArrayList,每次修改都是返回副本,建议迭代多修改少的时候使用
  4. +
+

5.3 阻塞队列和生产者-消费者模式

+

BlockingQueue,常用来实现生产者和消费者,有一个特殊的实现SynchronousQueue,它不是一个实际的队列,当生产者生产数据时直接交给消费者,适用于消费者多的场景

+

Deque,常用来实现工作窃取模式。生产者和消费者模式中,消费者共享一个队列,工作窃取模式中,消费者有独自的队列,当消费完后会偷其他人的工作。工作窃取模式可以减少对于共享队列的竞争

+

5.4 阻塞方法与中断方法

+
    +
  • 当某个方法抛出InterruptedException,说明该方法是阻塞方法,可以被中断
  • +
  • 代码中调用一个阻塞方法(阻塞方法和线程状态没有必然关系,方法可能是个长时间方法所以声明抛出InterruptedException,也有可能是会导致线程状态改变的sleep方法),必须处理中断响应. +
      +
    • 捕获/抛出异常
    • +
    • 恢复中断.调用当前线程的interrupt
    • +
    +
  • +
+

5.5 同步器

+
    +
  1. 阻塞队列
  2. +
  3. 闭锁(Latch): 延迟线程进度,直到条件满足,FutureTask也可以做闭锁
  4. +
  5. 信号量:类似发布凭证,但是任意线程都可以发布和返还
  6. +
  7. 栅栏: 阻塞一组线程,直到某个条件满足;如果有某个线程await期间中断或者超时,所有阻塞的调用都会终止并抛出BrokenBarrierException
  8. +
+

5.6 构建高效且可伸缩的缓存

+
    +
  1. 使用hashMap+synchronized,性能差
  2. +
  3. ConcurrentHashMap代替hashMap+synchronized,有重复计算问题
  4. +
  5. ConcurrentHashMap的值用FutureTask包起来,只要键已经存在,从FutureTask获取结果,因为check-then-act模式,仍然存在重复计算问题
  6. +
  7. 使用putIfAbsent设置缓存
  8. +
+

Part 2 构建并发应用程序

+

Chapter 6 任务执行

+

6.1 在线程中执行任务

+
    +
  1. +

    串行执行任务(响应会慢,服务器资源利用率低)

    +
  2. +
  3. +

    显式为每个请求申请一个线程

    +
      +
    • 任务处理线程从主线程分离,提高响应速度
    • +
    • 任务可以并行处理,提高吞吐量
    • +
    • 任务处理代码必须是线程安全的,多个线程会并发执行
    • +
    +
  4. +
  5. +

    无限制创建线程的不足

    +
      +
    • 创建销毁浪费时间
    • +
    • 浪费资源
    • +
    • 稳定性差
    • +
    +
  6. +
+

6.2 Executor框架

+
    +
  1. +

    Executor基于生产-消费模式,提交任务相当于生产者,执行任务的线程相当于消费者.

    +
  2. +
  3. +

    执行策略

    +
      +
    • What: 在什么线程中执行任务,按什么顺序执行,任务执行前后要执行什么操作
    • +
    • How Many: 多少任务并发,多少等待
    • +
    • Which: 系统过载时选择拒绝什么任务
    • +
    • How: 怎么通知任务成功/失败
    • +
    +
  4. +
  5. +

    线程池,管理一组同构工作线程的资源池,跟工作队列密切相关

    +
  6. +
  7. +

    Executor生命周期

    +
      +
    • 运行 : 对象新建时就是运行状态
    • +
    • 关闭 : 不接受新任务,同时等待已有任务完成,包括未执行的任务,关闭后任务再提交由 “被拒绝的执行处理器” 处理或者直接抛异常
    • +
    • 终止 : 关闭后任务完成
    • +
    +
  8. +
  9. +

    延迟任务和周期任务

    +

    Timer类可以负责,但是存在缺陷,应该考虑ScheduledThreadPoolExecutor代替它

    +

    Timer: 只用一个线程执行定时任务,假如某个任务耗时过长,会影响其他任务的定时准确性。除此之外,不支持抛出异常,发生异常将终止线程(已调度(scheduled)未执行的任务,线程不会执行,新任务不会调度,称为线程泄露)

    +

    DelayQueue: 阻塞队列的一种实现,为ScheduledThreadPoolExecutor提供调度策略

    +
  10. +
+

6.3 寻找可利用的并行性

+
    +
  1. +

    将耗时的IO使用别的线程获取;而不是简单的串行执行

    +
  2. +
  3. +

    Future 表示一个任务的生命周期,并提供相应的方法判断完成/取消,get会阻塞或抛异常

    +
  4. +
  5. +

    使用Callable和Future并行化下载和渲染

    +
  6. +
  7. +

    异构任务并行化获取重大性能提升很困难.

    +
      +
    • 任务大小不同
    • +
    • 负载均衡问题
    • +
    • 协调开销
    • +
    +
  8. +
  9. +

    CompletionService 将 Executor 和BlockingQueue结合在一起,Executor是生产者,CompletionService是消费者

    +
  10. +
  11. +

    使用 CompletionService 并行化下载和渲染

    +
  12. +
  13. +

    为任务设置时限

    +
  14. +
  15. +

    需要获取多个设置了时限的任务的结果可以用带上时间的 invokeAll 提交多个任务

    +
  16. +
+

Chapter 7 取消和关闭

+

本章讲解如何停止任务和线程,Java没有安全强制线程停止的方法,只有一种协作机制,中断

+

7.1 任务取消

+

有一种协作机制是在任务中设置取消位,任务定期查看该标识,假如置位就结束任务(假如线程阻塞了,就看不到取消位,那么就停不下来了)

+
    +
  1. 中断: 在取消任务或线程之外的其他操作中使用中断是不合适的 +
      +
    • 每个线程都有一个中断标志,interrupt中断目标线程,isInterrupted返回目标线程的中断状态,interrupted(糟糕的命名)清除当前线程中断;
    • +
    • Thread.sleep和Object.wait都会检查线程什么时候中断,发现时提前返回(不会立即响应,只是传递请求而已)
    • +
    +
  2. +
  3. 中断策略:尽快推迟执行流程,传递给上层代码;由于每个线程拥有各自的中断策略,除非知道中断对这个线程的含义,否则不应该中断该线程
  4. +
  5. 中断响应
    +当调用会抛出InterruptedException的阻塞方法时,有两种处理策略 +
      +
    • 传播异常,让你的方法也变成会抛出异常的阻塞方法(中断标志一直为true)
    • +
    • 恢复中断状态,以便调用堆栈上较高级的代码处理它(try-catch之后中断标志为false,可以调用当前线程的interrupt方法恢复成中断状态)。
    • +
    +
  6. +
  7. 在中断线程之前,要了解线程的中断策略
  8. +
  9. 通过Future取消任务
  10. +
  11. 处理不可中断的阻塞 +
      +
    • java.io中的同步Socket I/O.通过关闭Socket可以使阻塞线程抛出异常
    • +
    • java.io中的同步 I/O.终端一个InterruptibleChannel会抛出异常并关闭链路
    • +
    • 获取某个锁. Lock提供lockInterruptibly
    • +
    +
  12. +
  13. 通过 newTaskFor 方法进一步优化
  14. +
+

7.2 停止基于线程的服务

+

基于线程的服务:拥有线程的服务,例如线程池

+

只要拥有线程的服务的生命周期比创建它的方法的生命周期长,就提供生命周期方法。例如线程池 ExecutorService 提供了shutdown

+
    +
  1. 日志服务:多生产者写入消息到阻塞队列,单消费者从阻塞队列中取消息,停止日志服务需要正确关闭线程。需要对结束标志位和队列剩余消息数同步访问(书有错误,LoggerThread 应该 synchronized (LogService.this))
  2. +
  3. 毒丸,生产者将毒丸放在队列上,消费者拿到毒丸就结束
  4. +
  5. shutdownNow 取消正在执行的任务,返回已提交未开始的任务,可以用个集合保存执行中被取消的任务
  6. +
+

7.3 处理非正常的线程终止

+

通常是因为抛出运行时异常导致线程终止

+

处理方法:

+
    +
  1. try-catch 捕获任务异常,如果不能恢复,在finally块中通知线程拥有者
  2. +
  3. 当线程因未捕获异常而退出时,JVM会将事件报告给线程拥有者提供的UncaughtExceptionHandler,如果没有处理程序就将堆栈打印到System.err
  4. +
  5. 通过execute提交的任务的异常由UncaughtExceptionHandler处理,submit提交的任务,通过调用Future.get方法,包装在ExecutionException里面
  6. +
+

7.4 JVM关闭

+

有序关闭:最后一个非守护线程终止(可能是调用了System.exit,或者发送SIGINT或按Ctrl-C)后终止
+突然关闭:通过操作系统终止JVM进程,例如发送SIGKIll

+
    +
  1. 有序关闭中,JVM首先启动所有已注册的关闭钩子(通过Runtime.addShutdownHook注册的未启动线程)。如果应用程序线程在关闭时仍在运行,将与关闭线程并行执行。当所有关闭钩子都完成时,如果runFinalizersOnExit为true,那么jvm可能运行终结器,然后停止
  2. +
  3. 守护线程:执行辅助功能的线程,不会阻止JVM关闭。当JVM关闭时,守护线程直接关闭,不执行 finally 块,栈不会展开。守护线程适合做“内务”任务,例如清缓存
  4. +
  5. 终结器:GC在回收对象后会执行 finalize 方法释放持久资源。终结器在JVM管理的线程中运行,需要同步访问。终结器难写且性能低,除非要关闭 native 方法获取的资源,否则在 finally中显示关闭就够了
  6. +
+

Chapter 8 使用线程池

+

本章将介绍配置和调优线程池的高级选项,描述使用任务执行框架时需要注意的危险

+

8.1 任务和执行策略之间的隐式耦合

+

Executor 框架在任务提交和执行之间仍存在一些耦合:

+
    +
  1. 依赖其它任务的任务,相互依赖可能导致活跃性问题
  2. +
  3. 利用线程封闭的任务,这类任务不做同步,依赖单线程执行
  4. +
  5. 响应时间敏感的任务,可能需要多线程执行
  6. +
  7. 使用 ThreadLocal 的任务,ThreadLocal不应该用于线程池中任务之间的通信
  8. +
+

8.1.1 线程饥饿死锁

+

把相互依赖的任务提交到一个单线程的Executor一定会发生死锁。增大线程池,如果被依赖的任务在等待队列中,也会发生死锁

+

8.1.2 运行耗时长的任务

+

即使不出现死锁,也会降低性能,通过限制执行时间可以缓解

+

8.2 设置线程池大小

+

cpu数可以调用 Runtime.availableProcessors得出

+
    +
  1. 计算密集型场景,线程池大小等于cpu数+1
  2. +
  3. IO密集型场景,线程池大小等于cpu数 * cpu利用率 * (1+等待/计算时间比)
  4. +
+

8.3 配置 ThreadPoolExecutor

+

8.3.1 线程创建和销毁

+
    +
  1. corePoolSize:线程池大小,只有工作队列满了才会创建超出这个数量的线程
  2. +
  3. maximumPoolSize:最大线程数量
  4. +
  5. keepAliveTime:空闲时间超过keepAliveTime的线程会成为回收的候选线程,如果线程池的大小超过了核心的大小,线程就会被终止
  6. +
+

8.3.2 管理工作队列

+

可以分成三类:无界队列、有界队列和同步移交。队列的选择和线程池大小、内存大小的有关

+

无界队列可能会耗尽资源,有界队列会带来队列满时新任务的处理问题,同步移交只适合用在无界线程池或饱和策略可以接受

+

8.3.3 饱和策略

+

当任务提交给已经满的有界队列或已经关闭的Executor,饱和策略开始工作

+
    +
  1. Abort,默认策略,execute方法会抛RejectedExecutionException
  2. +
  3. Discard:丢弃原本下个执行的任务,并重新提交新任务
  4. +
  5. Caller-Runs:将任务给调用execute 的线程执行
  6. +
  7. 无界队列可以使用信号量进行饱和策略
  8. +
+

8.3.4 线程工厂

+

通过ThreadFactory.newThread创建线程,自定义线程工厂可以在创建线程时设置线程名、自定义异常

+

8.3.5 调用构造函数后再定制ThreadPoolExecutor

+

线程池的各项配置可以通过set方法配置,如果不想被修改,可以调用Executors.unconfigurableExecutorService
+将其包装成不可修改的线程池

+

8.4 扩展 ThreadPoolExecutor

+

ThreadPoolExecutor给子类提供了钩子方法,beforeExecute、afterExecute和terminated

+

beforeExecute和afterExecute钩子在执行任务的线程中调用,可用于添加日志记录、计时、监控或统计信息收集。无论任务从run正常返回还是抛出异常,afterExecute钩子都会被调用。如果beforeExecute抛出一个RuntimeException,任务就不会执行,afterExecute也不会被调用

+

terminated钩子在任务都完成且所有工作线程都关闭后调用,用来释放资源、执行通知或日志记录

+

8.5 递归算法并行化

+
    +
  1. 如果迭代操作之间是独立的,适合并行化
  2. +
  3. 递归不依赖于后续递归的返回值
  4. +
+

Chapter 9 GUI应用

+

9.1 为什么GUI是单线程的

+

由于竞争情况和死锁,多线程GUI框架最终都变成了单线程

+

9.1.1 串行事件处理

+

优点:代码简单
+缺点:耗时长的任务会发生无响应(委派给其它线程执行)

+

9.1.2 Swing的线程封闭

+

所有Swing组件和数据模型对象都封闭在事件线程中,任何访问它们的代码必须在事件线程里

+

9.2 短时间的GUI任务

+

事件在事件线程中产生,并冒泡到应用程序提供的监听器

+

Swing将大多数可视化组件分为两个对象(模型对象和视图对象),模型对象保存数据,可以通过引发事件表示模型发生变化,视图对象通过订阅接收事件

+

9.3 长时间的GUI任务

+

对于长时间的任务可以使用线程池

+
    +
  1. 取消 使用Future
  2. +
  3. 进度标识
  4. +
+

9.4 共享数据模型

+
    +
  1. 只要阻塞操作不会过度影响响应性,那么事件线程和后台线程就可以共享该模型
  2. +
  3. 分解数据模型.将共享的模型通过快照共享
  4. +
+

9.5 其它形式单线程

+

为了避免同步或死锁使用单线程,例如访问native方法使用单线程

+

Part 3 活跃性、性能和测试

+

Chapter 10. 避免活跃性危险

+

Java程序不能从死锁中恢复,本章讨论活跃性失效的一些原因以及预防措施

+

10.1 死锁

+

哲学家进餐问题:每个人都有另一个人需要的资源,并且等待另一个人持有的资源,在获得自己需要的资源前不会释放自己持有的资源,产生死锁

+

10.1.1 Lock-ordering死锁

+

线程之间获取锁的顺序不同导致死锁。
+解决方法:如果所有线程以一个固定的顺序获取锁就不会出现Lock-ordering死锁

+

10.1.2 动态Lock Order死锁

+

获取锁的顺序依赖参数可能导致死锁。
+解决方法:对参数进行排序,统一线程获取锁的顺序

+

10.1.3 协作对象的死锁

+

如果在持有锁时调用外部方法,将会出现活跃性问题,这个外部方法可能阻塞,加锁等导致其他线程无法获得当前被持有的锁
+解决方法:开放调用

+

10.1.4 开放调用

+

如果在方法中调用外部方法时不需要持有锁(比如调用者this),那么这种调用称为开放调用。实现方式是将调用者的方法的同步范围从方法缩小到块

+

10.1.5 资源死锁

+

和循环依赖锁导致死锁类似。例如线程持有数据库连接且等待另一个线程释放,另一个线程也是这样

+

10.2 避免和诊断死锁

+

使用两部分策略来审计代码以确保无死锁:首先,确定哪些地方可以获得多个锁(尽量使其成为一个小集合),然后对所有这些实例进行全局分析,以确保锁的顺序在整个程序中是一致的,尽可能使用开放调用简化分析

+

10.2.1 定时锁

+

另一种检测死锁并从死锁中恢复的技术是使用显示锁中的Lock.tryLock()代替内置锁

+

10.2.2 用Thread Dumps进行死锁分析

+

线程转储包含每个运行线程的堆栈信息,锁信息(持有哪些锁,从哪个栈帧中获得)以及阻塞的线程正在等待获得哪些锁

+

10.3 其它活跃性危险

+

10.3.1 饥饿

+

线程由于无法获得它所需要的资源而不能继续执行,最常见的资源是CPU

+

避免使用线程优先级,可能导致饥饿

+

10.3.2 糟糕的响应性

+

计算密集型任务会影响响应性,通过降低执行计算密集型任务的线程的优先级可以提高前台任务的响应性

+

10.3.3 活锁

+

线程执行任务失败后,任务回滚,又添加到队列头部,导致线程没有阻塞但永远不会有进展。多个相互合作的线程为了响应其它线程而改变状态也会导致活锁
+解决方法:在重试机制中引入一些随机性

+

Chapter 11. 性能和可伸缩性

+

11.1 对性能的思考

+

11.1.1 性能和可伸缩性

+

性能: 可以用任务完成快慢或者数量来衡量,具体指标包括服务时间、延迟、
+吞吐量、可伸缩性等

+

可伸缩性: 增加计算资源时提供程序吞吐量的能力

+

11.1.2 评估性能权衡

+

许多性能优化牺牲可读性和可维护性,比如违反面向对象设计原则,需要权衡

+

11.2 Amdahl定律

+

N:处理器数量
+F:必须串行执行的计算部分
+Speedup:加速比

+

$\text { Speedup } \leq \frac{1}{F+\frac{1-F}{N}} $

+

串行执行的计算部分需要仔细考虑,即使任务之间互不影响可以并行,但是线程从任务队列中需要同步,使用ConcurrentLinkedQueue比同步的LinkedList性能好

+

11.3 线程引入的开销

+
    +
  1. 上线文切换
  2. +
  3. 内存同步(同步的性能开销包括可见性保证,即内存屏障,可以用jvm逃逸分析和编译器锁粒度粗化进行优化)
  4. +
  5. 阻塞(非竞争的同步可以在JVM处理,竞争的同步需要操作系统介入,竞争失败的线程必定阻塞,JVM可以自旋等待(反复尝试获取锁,直到成功)或者被操作系统挂起进入阻塞态,短时间等待选择自旋等待,长时间等待选择挂起)
  6. +
+

11.4 减少锁的竞争

+

并发程序中,对伸缩性最主要的威胁就是独占方式的资源锁

+

三种减少锁争用的方法:

+
    +
  • 减少持有锁的时间
  • +
  • 减少请求锁的频率
  • +
  • 用允许更大并发的协调机制替换互斥锁
  • +
+

11.4.1 减小锁的范围

+

锁的范围即持有锁的时间

+

11.4.2 降低锁的力度

+

分割锁:将保护多个独立的变量的锁分割成单独的锁,这样锁的请求频率就可以降低

+

11.4.3 分段锁

+

分割锁可以扩展到可变大小的独立对象上的分段锁。例如ConcurrentHashMap使用了一个包含16个锁的数组,每个锁保护1/16的哈希桶

+

分段锁缺点:独占访问集合开销大

+

11.4.4 避免热点字段

+

热点字段:缓存
+热点字段会限制可伸缩性,例如,为了缓存Map中的元素数量,添加一个计数器,每次增删时修改计数器,size操作的开销就是O(1)。单线程没问题,多线程下又需要同步访问计数器,ConcurrentHashMap每个哈希桶一个计数器

+

11.4.5 互斥锁的替代品

+

考虑使用并发集合、读写锁、不可变对象和原子变量

+

读写锁:只要没有一个写者想要修改共享资源,多个读者可以并发访问,但写者必须独占地获得锁
+原子变量:提供细粒度的原子操作,可以降低更新热点字段的开销

+

11.4.6 监测CPU利用率

+

cpu利用率低可能是以下原因:

+
    +
  1. 负载不够,可以对程序加压
  2. +
  3. IO密集,可以通过iostat判断,还可以通过监测网络上的流量水平判断
  4. +
  5. 外部约束,可能在等待数据库或web服务的响应
  6. +
  7. 锁竞争,可以用分析工具分析哪些是“热”锁
  8. +
+

11.4.7 不要用对象池

+

现在JVM分配和回收对象已经很快了,不要用对象池

+

11.5 例子:比较Map的性能

+

ConcurrentHashMap单线程性能略好于同步的HashMap,并发时性能超好。ConcurrentHashMap对大多数成功的读操作不加锁,对写操作和少数读操作加分段锁

+

11.6 减少上下文切换

+
    +
  1. 日志记录由专门的线程负责
  2. +
  3. 请求服务时间不应该过长
  4. +
  5. 将IO移动到单个线程
  6. +
+

Chapter 12. 并发程序的测试

+

大多数并发程序测试安全性和活跃性。安全性可以理解为“永远不会发生坏事”,活跃性可以理解为“最终会有好事发生”

+

12.1 正确性测试

+

12.1.1 基础单元测试

+

和顺序程序的测试类似,调用方法,验证程序的后置条件和不变量

+

12.1.2 阻塞操作测试

+

在单独的一个线程中启动阻塞活动,等待线程阻塞,中断它,然后断言阻塞操作完成。

+

Thread.getState不可靠,因为线程阻塞不一定进入WAITING或TIMED_WAITING状态,JVM可以通过自旋等待实现阻塞。类似地,Object.wait和Condition.wait存在伪唤醒情况,处于WAITING或TIMED_WAITING状态的线程可以暂时过渡到RUNNABLE。

+

12.1.3 安全性测试

+

给并发程序编写高效的安全测试的挑战在于识别出容易检查的属性,这些属性在程序错误时出错,同时不能让检查限制并发性,最好检查属性时不需要同步。

+

生产者和消费者模式中的一种方法是校验和,单生产者单消费者可以使用顺序敏感的校验和计算入队和出队元素的校验和,多生产者多消费者要用顺序不敏感的校验和

+

12.1.4 资源管理测试

+

任何保存或管理其他对象的对象都不应该在不必要的时间内继续维护对这些对象的引用。可以用堆检查工具测试内存使用情况

+

12.1.5 使用回调

+

回调函数通常是在对象生命周期的已知时刻发出的,这是断言不变量的好机会。例如自定义线程池可以在创建销毁线程时记录线程数

+

12.1.6 产生更多的交替操作

+

Thread.yield放弃cpu,保持RUNNABLE状态,重新竞争cpu
+Thread.sleep放弃cpu进入TIME_WAITING状态,不竞争cpu,sleep较小时间比yield更稳定产生交替操作

+

tips:Java 线程的RUNNABLE 状态对应了操作系统的 ready 和 running 状态,TIME_WAITING(调用Thread.sleep) 和 WAITING(调用Object.wait) 和 BLOCKED(没有竞争到锁) 对应 waiting 状态。interrupt是种干预手段,如果interrupt一个RUNNABLE线程(可能在执行长时间方法需要终止),如果方法声明抛出InterruptedException,就表示可中断,方法会循环检查isInterrupted状态来响应interrupt,一般情况线程状态变成TERMINATED。如果interrupt一个 waiting 线程(可能是由sleep、wait方法导致,这些方法会抛出InterruptedException),线程重新进入 RUNNALBE 状态,处理InterruptedException

+

12.2 性能测试

+
    +
  1. 增加计时功能(CyclicBarrier)
  2. +
  3. 多种算法比较(LinkedBlockingQueue在多线程情况下比ArrayBlockingQueue性能好)
  4. +
  5. 衡量响应性
  6. +
+

12.3 避免性能测试的陷阱

+

12.3.1 垃圾回收

+

垃圾回收不可预测,会导致测试误差,需要长时间测试,多次垃圾回收,得到更准确结果

+

12.3.2 动态编译

+

动态编译会影响运行时间,需要运行足够长时间或者与完成动态编译后再开始计时

+

12.3.3 对代码路径的不真实采样

+

动态编译器会对单线程测试程序进行优化,最好多线程测试和单线程测试混合使用(测试用例至少用两个线程)

+

12.3.4 不真实的竞争情况

+

并发性能测试程序应该尽量接近真实应用程序的线程本地计算,并考虑并发协作。例如,多线程访问同步Map,如果本地计算过长,那么锁竞争情况就较弱,可能得出错误的性能瓶颈结果

+

12.3.5 无用代码的删除

+

无用代码:对结果没有影响的代码
+由于基准测试通常不计算任何东西,很容易被优化器删除,这样测试的执行时间就会变短
+解决方法是计算某个派生类的散列值,与任意值比较,加入相等就输出一个无用且可被忽略的消息

+

12.4 补充的测试方法

+
    +
  1. 代码审查
  2. +
  3. 静态代码分析
  4. +
  5. 面向切面的测试工具
  6. +
  7. 分析与检测工具
  8. +
+

Part 4 高级主题

+

Chapter 13 显示锁

+

Java 5 之前,对共享数据的协调访问机制只有 synchronized 和 volatile,Java 5 增加了 ReentrantLock。

+

13.1 Lock和ReentrantLock

+

Lock接口定义加锁和解锁的操作。
+ReentrantLock还提供了可重入特性

+

显示锁和内置锁很像,显示锁出现的原因是内置锁有一些功能限制

+
    +
  1. 不能中断等待锁的线程
  2. +
  3. 必须在获得锁的地方释放锁
  4. +
+

13.1.1 轮询和定时获得锁

+

tryLock:轮询和定时获得锁
+内置锁碰到死锁是致命的,唯一恢复方法是重启,唯一防御方法是统一锁获取顺序,tryLock可以概率避免死锁

+

13.1.2 可中断的获得锁

+

lockInterruptibly,调用后一直阻塞直至获得锁,但是接受中断信号

+

13.1.3 非块结构加锁

+

内置锁是块结构的加锁和自动释放锁,有时需要更大的灵活性,例如基于hash的集合可以使用分段锁

+

13.2 性能考虑

+

从Java 6 开始,内置锁已经不比显式锁性能差

+

13.3 公平性

+

内置锁不保证公平,ReentrantLock默认也不保证公平,非公平锁可以插队(不提倡,但是不阻止),性能相比公平锁会好一些

+

13.4 在 Synchronized 和 ReentrantLock 中选择

+

当你需要用到轮询和定时加锁、可中断的加锁、公平等待锁和非块结构加锁,使用 ReentrantLock,否则使用 synchronized

+

13.5 读写锁

+

读写锁:资源可以被多个读者同时访问或者单个写者访问

+

ReadWriteLock 定义读锁和写锁方法,和 Lock 类似,实现类在性能、调度、获得锁的优先条件、公平等方面可以不同

+

Chapter 14 构建自定义的同步工具

+

最简单的方式使用已有类进行构造,例如LinkedBlockingQueue、CountDown-Latch、Semaphore和FutureTask等

+

14.1 状态依赖性的管理

+

单线程中,基于状态的前置条件不满足就失败。但是多线程中,状态会被其它线程修改,所以多线程程序在不满足前置条件时可以等待直至满足前置条件

+

14.1.1 将前置条件的失败传播给调用者

+

不满足前置条件就抛异常是滥用异常。调用者可以自旋等待(RUNNABLE态,占用cpu)或者阻塞(waiting态,不占cpu),即需要调用者编写前置条件管理的代码

+

14.1.2 通过轮询和睡眠粗鲁的阻塞

+

通过轮询和睡眠完成前置条件管理,不满足是就阻塞,调用者不需要管理前置条件,但需要处理 InterruptedException

+

14.1.3 条件队列

+

Object的wait,notify 和 notifyAll构成内置条件队列的API,wait会释放锁(本质和轮询与休眠是一样的,注意sleep前要释放锁)

+

14.2 使用条件队列

+

14.2.1 条件谓词

+

条件谓词:由类的状态变量构造的表达式,例如缓冲区非空即count>0

+

给条件队列相关的条件谓词以及等待它成立的操作写Javadoc

+

条件谓词涉及状态变量,状态变量由锁保护,所以在测试条件谓词之前,需要先获得锁。锁对象和条件队列对象(调用wait和notify的对象)必须是同一个对象

+

14.2.2 过早唤醒

+

一个线程由于其它线程调用notifyAll醒来,不意味着它的等待条件谓词一定为真。每当线程醒来必须再次测试条件谓词(使用循环)

+

14.2.3 丢失的信号

+

线程必须等待一个已经为真的条件,但是在开始等待之前没有检查条件谓词,发生的原因是编码错误,正确写法是循环测试条件谓词,false就继续wait

+

14.2.4 通知

+

优先使用 notifyAll,notify 可能会出现“hijacked signal”问题,唤醒了一个条件还未真的线程,本应被唤醒的线程还在等待。只有所有线程都在等同一个条件谓词且通知最多允许一个线程继续执行才使用notify

+

14.2.5 例子:门

+

用条件队列实现一个可以重复开关的线程门

+

14.2.6 子类的安全问题

+

一个依赖状态的类应该完全向子类暴露它的等待和通知协议,或者禁止继承

+

14.2.7 封装条件队列

+

最好将条件队列封装起来,在使用它的类的外面无法访问

+

14.2.8 进入和退出协议

+

进入协议:操作的条件谓词
+退出协议:检查该操作修改的所有状态变量,确认他们是否使某个条件谓词成真,若是,通知相关队列

+

14.3 显示 Condition

+

显示锁在一些情况下比内置锁更灵活。类似地,Condition 比内置条件队列更灵活

+

内置条件队列有几个缺点:

+
    +
  1. 每个内置锁只能关联一个条件队列,即多个线程可能会在同一个条件队列上等待不同的条件谓词
  2. +
  3. 最常见的加锁模式会暴露条件队列
  4. +
+

一个 Condition 关联一个 Lock,就像内置条件队列关联一个内置锁,使用 Lock.newCondition 来创建 Condition,Condition比内置条件队列功能丰富:每个锁有多个等待集(即一个Lock可以创建多个Condition),可中断和不可中断的条件等待,基于截止时间的等待,以及公平或不公平排队的选择。

+

Condition中和wait,notify,notifyAll 对应的方法是 await,signal,signalAll

+

14.4 Synchronizer剖析

+

ReentrantLock 和 Semaphore 有很多相似的地方。ReentrantLock可以作为只许一个线程进入的 Semaphore,Semaphore 可以用 ReentrantLock 实现

+

它们和其它同步器一样依赖基类 AbstractQueuedSynchronizer(AQS)。AQS是一个用于构建锁和同步器的框架,使用它可以轻松有效地构建范围广泛的同步器。

+

14.5 AbstractQueuedSynchronizer

+

依赖AQS的同步器的基本操作是获取和释放的一些变体。获取是阻塞操作,调用者获取不到会进入WAITING或失败。对于锁或信号量,获取的意思是获取锁或许可。对于CountDownLatch,获取的意思是等待门闩到达终点。对于FutureTask,获取的意思是等待任务完成。释放不是阻塞操作。同步器还会根据各自的语义维护状态信息

+

14.6 JUC同步器类中的AQS

+

JUC许多类使用AQS,例如 ReentrantLock, Semaphore, ReentrantReadWriteLock, CountDownLatch, SynchronousQueue, FutureTask

+

Chapter 15 原子变量与非阻塞同步机制

+

非阻塞算法使用原子机器指令,例如 compare-and-swap 取代锁来实现并发下的数据完成性。它的设计比基于锁的算法复杂但可以提供更好的伸缩性和活跃性,非阻塞算法不会出现阻塞、死锁或其它活跃性问题,不会受到单个线程故障的影响

+

15.1 锁的劣势

+

JVM对非竞争锁进行优化,但是如果多个线程同时请求锁,就要借助操作系统挂起或者JVM自旋,开销很大。相比之下volatile是更轻量的同步机制,不涉及上下文切换和线程调度,然后volatile相较于锁,它不能构造原子性的复合操作,例如自增

+

锁还会出现优先级反转(阻塞线程优先级高,但是后执行),死锁等问题

+

15.2 并发操作的硬件支持

+

排它锁是悲观锁,总是假设最坏情况,只有确保其它线程不会干扰才会执行

+

乐观方法依赖碰撞检测来确定在更新过程中是否有其它线程的干扰,若有则操作失败并可以选择重试

+

为多处理器设计的cpu提供了对共享变量并发访问的特殊指令,例如 compare‐and‐swap,load-linked

+

15.2.1 Compare and Swap

+

CAS 有三个操作数:内存地址V,期待的旧值A,新值B。CAS在V的旧值是A的情况下原子更新值为B,否则什么都不做。CAS是一种乐观方法:它满怀希望更新变量,如果检测到其它线程更新了变量,它就会失败。CAS失败不会阻塞,允许重试(一般不重试,失败可能意味着别的线程已经完成该工作)

+

15.2.2 非阻塞的计数器

+

在竞争不激烈的情况下,性能比锁优秀。缺点是强制调用者处理竞争问题(重试、后退或放弃),而锁通过阻塞自动处理争用,直到锁可用

+

15.2.3 JVM对CAS的支持

+

JVM将CAS编译成底层硬件提供的方法,加入底层硬件不支持CAS,JVM会使用自旋锁。原子变量类使用了CAS

+

15.3 原子变量类

+

原子变量比锁的粒度更细,重量更轻,能提供volatile不支持的原子性

+
    +
  1. 原子变量可以作为更好的 volatile
  2. +
  3. 在高度竞争情况下,锁性能更好,正常情况下,原子变量性能更好
  4. +
+

15.4 非阻塞的算法

+

如果一个线程的故障或挂起不会导致另一个线程的故障或挂起,则该算法称为非阻塞算法;如果一个算法在每个执行步骤中都有线程能够执行,那么这个算法被称为无锁算法。如果构造正确,只使用CAS进行线程间协调的算法可以是无阻塞和无锁的

+

15.4.1 非阻塞的栈

+

创建非阻塞算法的关键是如何在保持数据一致性的同时,将原子性更改的范围限制在单个变量内。

+

非阻塞的栈使用CAS来修改顶部元素

+

15.4.2 非阻塞的链表

+

Michale-scott算法

+

15.4.3 原子字段更新器

+

原子字段更新器代表了现有 volatile 字段的基于反射的“视图”,以便可以在现有的 volatile 字段上使用CAS

+

15.4.4 ABA问题

+

大部分情况下,CAS会询问“V的值还是A吗?”,是A就更新。但是有时候,我们需要知道“从我上次观察到V是A以来,它的值有没有改变?”。对于某些算法,将V的值从A->B,再从B->A,是一种更改,需要重新执行算法。解决方法是使用版本号,即使值从A变成B再变回A,版本号也会不同

+

Chapter 16 Java 内存模型

+

16.1 内存模型是什么,为什么我需要一个内存模型?

+

在并发没有同步的情况下,有许多原因导致一个线程不能立即或永远不能再另一个线程中看到操作的结果

+
    +
  1. 编译器生成的指令顺序与源码不同
  2. +
  3. 变量存在寄存器中而不是内存中
  4. +
  5. 处理器可以并行或乱序执行指令
  6. +
  7. 缓存可能会改变写入变量到主内存的顺序
  8. +
  9. 存储在处理器本地缓存中的值可能对其它处理器不可见
  10. +
+

16.1.1 平台的内存模型

+

在共享存储的多处理器体系结构中,每个处理器都有自己的高速缓存,这些告诉缓存周期性地与主存协调。

+

体系结构的内存模型告诉程序可以从内存系统得到什么一致性保证,并制定所需的特殊指令(内存屏障或栅栏),以在共享数据时获得所需的额外内存协调保证。为了不受跨体系结构的影响,Java提供了自己的内存模型,JVM通过在适当的位置插入内存屏障来处理JMM(Java内存模型)和和底层平台的内存模型之间的差异

+

顺序一致性:程序中所有操作都有一个单一的顺序,而不管他们在什么处理器上执行,并且每次读取变量都会看到任何处理器按执行顺序对该变量的最后一次写入。

+

现代处理器没有提供顺序一致性,JMM也没有

+

16.1.2 重排

+

指令重排会使程序的行为出乎意料。同步限制了编译器、运行时和硬件在重排序时不会破坏JMM提供的可见性保证

+

16.1.3 Java 内存模型

+

Java 内存模型由一些操作指定,包括对变量的读写、监视器的锁定和解锁。JMM对所有操作定义了一个称为 happens before 的偏序规则:

+
    +
  • 程序顺序规则:线程按程序定义的顺序执行操作
  • +
  • 监视器锁规则:监视器锁的解锁必须发生在后续的加锁之前
  • +
  • volatile 变量规则:对 volatile 字段的写入操作必须发生在后续的读取之前
  • +
  • 线程启动规则:对线程调用Thread.start会在该线程所有操作之前执行
  • +
  • 线程结束规则:线程中任何操作必须在其它线程检测到该线程已经结束之前执行或者从Thread.join返回或者Thread.isAlive返回false
  • +
  • 中断规则:一个线程对另一个线程调用 interrupt 发生在被中断线程检测到中断之前
  • +
  • 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成
  • +
  • 传递性:A发生在B之前,B发生在C之前,那么A发生在C之前
  • +
+

16.1.4 借用同步

+

通过类库保证 happens-before顺序:

+
    +
  • 将元素放入线程安全的容器发生在另一个线程从集合中检索元素之前
  • +
  • 在CountDownLatch上进行倒数发生在该线程从门闩的await返回之前
  • +
  • 释放信号量的许可发生在获取之前
  • +
  • Future代表的任务执行的操作发生在另一个线程从Future.get返回之前
  • +
  • 提交Runnable或者Callable任务给执行器发生在任务开始之前
  • +
  • 线程到达CyclicBarrier或Exchanger发生在其它线程释放相同的barrier或者exchange point之前
  • +
+

16.2 发布

+

16.2.1 不安全的发布

+

当缺少happens-before关系时候,就可能出现重排序问题,这就解释了为什么在没有同步情况下发布一个对象会导致另一个线程看到一个只被部分构造的对象

+

除了不可变对象之外,使用由其他线程初始化的对象都是不安全的,除非对象的发布发生在消费线程使用它之前

+

16.2.2 安全发布

+

使用锁或者volatile变量可以确保读写操作按照 happens-before 排序

+

16.2.3 安全初始化

+

静态字段在声明时就初始化由JVM提供线程安全保证。
+延迟初始化,可以写在同步方法里面,或者使用辅助类,在辅助类中声明并初始化

+

16.2.4 双重检查锁

+

Java 5之前的双重检查锁会出现引用是新值但是对象是旧值,这意味着可以看到对象不正确的状态,Java5之后给引用声明加上 volatile 可以起到线程安全地延迟初始化作用,但是不如使用辅助类,效果一样且更容易懂

+

16.3 初始化的安全性

+

初始化安全性只能保证通过final字段可达的值从构造过程完成时开始的可见性(事实不可变对象以任何形式发布都是安全的)。对于通过非final字段可达的值,或者构成完成之后可能改变的值,必须采用同步确保可见性。

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2023/02/01/MySQL\345\256\236\346\210\23045\350\256\262\345\255\246\344\271\240\347\254\224\350\256\260/index.html" "b/2023/02/01/MySQL\345\256\236\346\210\23045\350\256\262\345\255\246\344\271\240\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000..78f3028 --- /dev/null +++ "b/2023/02/01/MySQL\345\256\236\346\210\23045\350\256\262\345\255\246\344\271\240\347\254\224\350\256\260/index.html" @@ -0,0 +1,2241 @@ + + + + + + + + + + + + + + + + + + + + MySQL实战45讲学习笔记 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ MySQL实战45讲学习笔记 +

+ + + + +
+
+ +

基础架构:一条SQL查询语句是如何执行的

+

图 1

+

MySQL可以分为Server层和存储引擎层两部分

+

Server层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。

+

而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎

+

连接器

+

连接器负责跟客户端建立连接、获取权限、维持和管理连接。建立连接后,权限修改不会影响已经存在的连接的权限

+

长连接:连接成功后,如果客户度持续有请求,一直使用同一个连接
+短连接:每次执行很少的几次查询就断开连接,下次查询再重新建立

+

建立连接比较耗时,尽量使用长连接,但是全部使用长连接会导致OOM,因为MySQL在执行过程中临时使用的内存是管理在连接对象里面,连接不断开内存不会释放

+

解决方案:

+
    +
  1. 定期断开长连接
  2. +
  3. 执行mysql_reset_connection重新初始化连接资源
  4. +
+

查询缓存

+

执行过的语句及其结果可能会以key-value对的形式,直接缓存在内存中

+

查询缓存的失效非常频繁,只要有对一个表的更新,这个表上的所有查询缓存都会被清空,因此不要使用查询缓存,MySQL 8.0删掉了此功能

+

分析器

+

分析器先做“词法分析”,识别出SQL语句中的字符串分别是什么,例如,识别“select”是查询语句,“T”识别成表名T

+

然后做“语法分析”,判断输入的SQL语句是否满足MySQL语法,如果语句不对,会收到“You have an error in your SQL syntax”的错误提醒

+

优化器

+

优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序

+

例如下面的语句执行两个表的join:

+
mysql> select * from t1 join t2 using(ID)  where t1.c=10 and t2.d=20;
+
+
    +
  • 既可以先从表t1里面取出c=10的记录的ID值,再根据ID值关联到表t2,再判断t2里面d的值是否等于20。
  • +
  • 也可以先从表t2里面取出d=20的记录的ID值,再根据ID值关联到t1,再判断t1里面c的值是否等于10。
  • +
+

这两种执行方法的结果一样但效率不同,优化器决定使用哪个方法

+

执行器

+
    +
  1. 判断有没有表的执行权限
  2. +
  3. 根据表的引擎定义调用引擎提供的接口,例如“取满足条件的第一行”,“满足条件的下一行”,数据库的慢查询日志rows_examined字段表示语句在执行过程中扫描了多少行,引擎扫描行数跟rows_examined并不是完全相同的
  4. +
+

日志系统:一条SQL更新语句是如何执行的

+

一条查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块,最后到达存储引擎。更新语句还涉及 redo log(重做日志)和 binlog(归档日志)

+

redo log

+

WAL(Write-Ahead Logging):更新记录时,InnoDB引擎会先把记录写到redo log里面并更新内存,然后在适当的时候将操作记录更新到磁盘里面

+

InnoDB的redo log是固定大小和循环写的,write pos是当前记录的位置,checkpoint是当前要擦除的位置,擦除记录前要把记录更新到数据文件

+

redo log保证即使数据库异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe

+

binlog

+

redo log是InnoDB引擎特有的日志,Server层特有的引擎是binlog(归档日志)

+

两者有三点不同

+
    +
  1. redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用
  2. +
  3. redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1”
  4. +
  5. redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志
  6. +
+

执行器和InnoDB引擎在执行update语句时的内部流程

+
    +
  1. 执行器先找引擎取ID=2这一行。ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  2. +
  3. 执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
  4. +
  5. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。
  6. +
  7. 执行器生成这个操作的binlog,并把binlog写入磁盘。
  8. +
  9. 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。
  10. +
+

下图的浅色框表示在InnoDB内部执行,深色框表示在执行器中执行

+

图 2

+

两阶段提交

+

redo log的写入分为两步骤:prepare和commit,也就是”两阶段提交“,目的是为了让两份的日志之间的逻辑一致

+

当数据库需要恢复到指定的某一秒时,可以先在临时库上这样做:

+
    +
  1. 找到最近的一次全量备份
  2. +
  3. 从备份的时间点开始,将备份的binlog依次取出来重放到指定时间
  4. +
+

如果redo log不是两阶段提交

+
    +
  1. 先写redo log后写binlog。假设在redo log写完,binlog还没写完,MySQL异常重启,数据可以恢复,但是binlog没有记录这个语句。之后用binlog恢复临时库时会缺少更新
  2. +
  3. 先写binlog后写redo log。假设binlog写完,redo log还没写,MySQL异常重启之后,这个事务无效,数据没有恢复。但是binlog里面已经有这个语句,所以之后用binlog恢复临时库会多一个事务
  4. +
+

innodb_flush_log_at_trx_commit这个参数设置成1的时候,表示每次事务的redo log都直接持久化到磁盘。这个参数建议设置成1,这样可以保证MySQL异常重启之后数据不丢失。

+

sync_binlog这个参数设置成1的时候,表示每次事务的binlog都持久化到磁盘。建议设置成1,这样可以保证MySQL异常重启之后binlog不丢失。

+

如果redo log处于prepare状态且binlog写入完成,MySQL异常重启会commit掉这个事务

+

事务隔离

+

事务保证一组数据库操作要么全部成功,要么全部失败

+

隔离性与隔离级别

+

ACID中的I指的是隔离性(Isolation)

+

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。

+

事务隔离级别包括:

+
    +
  • 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到
  • +
  • 读提交:一个事务提交之后,它做的变更才会被其他事务看到
  • +
  • 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其它事务也是不可见的
  • +
  • 串行化:对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成才能继续执行
  • +
+

数据库使用视图来实现隔离级别。在“可重复读”隔离级别下,视图是在事务开启时创建的。在“读提交”隔离级别下,视图是在每个SQL语句开始执行的时候创建的。“读未提交”直接返回记录的最新值,没有视图概念。“串行化”直接用加锁的方式

+

事务隔离的实现

+

这里展开说明“可重复读”

+

在MySQL中,每条记录在更新时都会同时记录一条回滚操作。假设一个值从1按顺序改成了2、3、4,在回滚日志里有类似下面的记录

+

图 3

+

当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。视图A、B、C对应的值分别是1、2、4,同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC)。对于视图A,要得到1,就必须将当前值依次执行图中所有的回滚操作。即使现在有另一个事务正在将4改成5,这个事务跟视图A、B、C对应的事务是不会冲突的。

+

系统会将没有比回滚日志更早的read-view时删掉这个回滚日志。因此尽量不要使用长事务,系统里面会存在很老的事务视图

+

事务的启动方式

+

MySQL的事务启动方式有如下几种:

+
    +
  1. 显式启动事务,begin 或 start transaction。配套的提交语句是commit,回滚语句是rollback
  2. +
  3. 隐式启动事务,一条SQL语句会自动开启一个事务。需要设置autocommit = 1 才会自动提交
  4. +
+

set autocommit=0,会将这个线程的自动提交关掉。事务持续存在直到你主动执行commit 或 rollback语句,或者断开连接

+

建议使用set autocommit=1,并显示启动事务

+

在autocommit为1的情况下,用begin显式启动的事务,如果执行commit则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行begin语句的开销

+

可以在information_schema库的innodb_trx这个表中查询长事务,比如下面这个语句,用于查找持续时间超过60s的事务。

+
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
+
+

如何避免长事务对业务的影响?

+

从应用端来看

+
    +
  1. 通过MySQL的general_log确保autocommit=1
  2. +
  3. 包含多个select语句的只读事务,没有一致性要求就拆分
  4. +
  5. 通过SET MAX_EXECUTION_TIME控制每个语句的最长执行时间
  6. +
+

从数据库端来看

+
    +
  1. 监控 information_schema.Innodb_trx表,设置长事务阈值,超过就kill
  2. +
  3. 推荐使用Percona的pt-kill
  4. +
  5. 业务测试阶段就输出所有general_log,分析日志提前发现问题
  6. +
  7. innodb_undo_tablespaces设置成2或更大的值
  8. +
+

深入浅出索引(上)

+

索引的出现是为了提高数据查询的效率

+

索引的常见模型

+

哈希表,只适用于只有等值查询的场景,不适用于范围查询

+

有序数组在等值查询和范围查询场景中都非常优秀,但更新数据需要挪动数组元素,成本太高。只适用于静态存储引擎(数据不再变化)

+

平衡二叉查找树的时间复杂度是O(log(N)),但是算上每次访问节点的磁盘IO开销,查询非常慢。为了减少磁盘IO次数,出现了N叉树

+

InnoDB的索引模型

+

根据叶子节点内容,索引类型分为主键索引和非主键索引

+

主键索引(聚簇索引):叶子节点存的是整行数据
+普通索引(二级索引):叶子结点存的是主键的值

+

图 4

+

基于主键索引和普通索引的查询的区别:

+
    +
  • 如果语句是select * from T where ID=500,即主键查询方式,则只需要搜索ID这棵B+树;
  • +
  • 如果语句是select * from T where k=5,即普通索引查询方式,则需要先搜索k索引树,得到ID的值为500,再到ID索引树搜索一次。这个过程称为回表。
  • +
+

索引维护

+

在插入新记录时,B+树为了维护有序性会进行页分裂和页合并

+

自增主键 VS 业务字段主键

+

性能上:自增主键按序插入,不会触发叶子节点的分裂,而业务字段做主键往往不是有序插入,导致页分裂和页合并,性能差
+存储空间上:主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。业务字段主键是身份证号(20字节)不如自增主键(4字节或8字节)

+

业务字段做主键的场景是:

+
    +
  1. 只有一个索引
  2. +
  3. 该索引必须是唯一索引
  4. +
+

这就是典型的KV场景,直接将这个字段设置为主键

+

深入浅出索引(下)

+

覆盖索引

+

覆盖索引:索引的叶子节点可以直接提供查询结果,不需要回表

+

可以为高频请求建立联合索引起到覆盖索引的作用

+

最左前缀原则

+

索引项是按照索引定义里面出现的字段的顺序排序的。满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左N个字段,也可以是字符串索引的最左M个字符

+

索引内的字段顺序评估标准:

+
    +
  1. 复用能力,如果该顺序的联合索引能少维护一个索引,那么该顺序优先使用
  2. +
  3. 空间,如果必须维护联合索引和单独索引,那么给小字段单独索引,联合索引的顺序是(大字段,小字段)
  4. +
+

索引下推

+

在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足的条件的记录,减少回表次数(联合索引在按最左匹配时碰到范围查询停止,索引下推可以对后面的索引字段做条件判断后再返回结果集)

+

全局锁和表锁

+

根据加锁的范围,MySQL里面的锁大致可以分成全局锁、表级锁和行锁三类

+

全局锁

+

全局锁是对整个数据库实例加锁,让整个库处于只读状态

+
Flush tables with read lock
+
+

全局锁的典型使用场景是不支持MVCC的引擎(MyISAM)的全库逻辑备份,如果所有表的引擎支持MVCC,可以在备份时开启事务确保拿到一致性视图(mysqldump加上参数-single-transaction)

+

让全库只读,另外一种方式是set global readonly = true,但仍然建议使用FTWRL,因为:

+
    +
  1. readonly的值可能会用来做其它逻辑,比如判断是主库还是备库
  2. +
  3. FTWRL在客户端发生异常断开时,MySQL会自动释放全局锁,而readonly会一直保持
  4. +
+

表级锁

+

表级锁有两种:表锁,元数据锁(meta data lock,MDL)

+

表锁

+

语法:lock tables … read/write

+

表锁会限制其它线程的读写,也会限定本线程的操作对象

+

例如,线程A执行lock tables t1 read, t2 write;,其它线程写t1、读写t2都会被阻塞,线程A只能执行读t1、读写t2,不能访问其它表

+

如果支持行锁,一般不使用表锁

+

元数据锁

+

MDL不需要显示使用,在访问表时会被自动加上,事务提交才释放,作用是保证读写的正确性

+

当对表做增删改查操作时,加MDL读锁;当对表结构变更时,加MDL写锁

+
    +
  • 读锁之间不互斥,因此可以有多个线程同时对一张表增删查改
  • +
  • 读写锁之间、写锁之间是互斥的。因此如果有两个线程要同时给一个表加字段,其中一个要等另一执行完再执行
  • +
+

给表加字段的方式:

+
    +
  1. kill掉长事务,事务不提交会一直占着MDL
  2. +
  3. 在alter table语句设置等待时间,如果在等待时间内能拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句。后面重试这个过程
  4. +
+

行锁

+

行锁是针对表中行记录的锁

+

两阶段锁

+

两阶段锁协议:在InnoDB事务中,行锁是在需要的时候加上的,但并不是不需要了就立刻释放,而是要等到事务结束才释放

+

如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放

+

死锁和死锁检测

+

死锁:并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源,导致这几个线程无限等待

+

死锁出现后有两种策略:

+
    +
  • 设置等待时间,修改innodb_lock_wait_timeout
  • +
  • 发起死锁检测,主动回滚死锁链条中的某一个事务,让其他事务可以继续执行。innodb_deadlock_detect设置为on表示开启
  • +
+

第一种策略,等待时间太长,业务的用户接受不了,等待时间太短会出现误伤。所以一般用死锁检测

+

死锁检测有性能问题,解决思路有几种:

+
    +
  • 如果能确保业务一定不会出现死锁,可以临时把死锁检测关掉。这种方法存在业务有损的风险,业务逻辑碰到死锁会回滚重试,但是没有死锁检测会超时导致业务有损
  • +
  • 控制并发程度。数据库Server层实现,对相同行的更新,在进入引擎之前排队
  • +
  • 将一行改成逻辑上的多行。例如账户余额等于10行之和,扣钱时随机扣一行,这种方案需要根据业务逻辑做详细设计
  • +
+

详解事务隔离

+

假如有如下表和事务A、B、C

+
mysql> CREATE TABLE `t` (
+  `id` int(11) NOT NULL,
+  `k` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB;
+insert into t(id, k) values(1,1),(2,2);
+
+

图 5

+

"快照"在MVCC里是怎么工作的

+

快照不是整个数据库的拷贝。

+

InnoDB里每个事务都有一个唯一的transaction id,是事务开始时申请的,严格递增的。每行数据有多个版本,每次事务更新数据时,都会生成一个新的数据版本,并把transaction id赋给这个数据版本的事务id,记为row trx_id。某个版本的数据可以通过当前版本和undo log计算出来

+

在实现上,InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指的是启动了但还没提交

+

数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位

+

图 6

+

对于当前事务的启动瞬间来说,一个数据版本的row trx_id,有以下几种可能:

+
    +
  1. 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
  2. +
  3. 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
  4. +
  5. 如果落在黄色部分,那就包括两种情况
    +a. 若 row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见;
    +b. 若 row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。
  6. +
+

InnoDB利用了“所有数据都有多个版本”的特性,实现了“秒级创建快照”的能力

+

可以用时间顺序来理解版本的可见性。

+

一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

+
    +
  1. 版本未提交,不可见;
  2. +
  3. 版本已提交,但是是在视图创建后提交的,不可见
  4. +
  5. 版本已提交,而且是在视图创建前提交的,可见
  6. +
+

更新逻辑

+

更新数据都是先读后写,读是当前值,称为“当前读”(current read)。所以事务B是在(1,2)上进行修改

+

select如果加锁,也是当前读,不加就是一致读,下面两个select语句分别加了读锁(S锁,共享锁)和写锁(X锁,排它锁)。行锁包括共享锁和排它锁

+
mysql> select k from t where id=1 lock in share mode;
+mysql> select k from t where id=1 for update;
+
+

假设事务C变成了事务C’

+

图 7

+

事务C’还没提交,但是生成了最新版本(1,2),根据“两阶段协议”,(1,2)这个版本上的写锁还没释放,事务B的更新是当前读,需要加锁,所以被阻塞

+

可重复读的核心就是一致性读(consistent read);而事务更新数据时只能用当前读,如果当前的记录的行锁被其他事务占用的话,就进入锁等待。

+

读提交的逻辑和可重复读的逻辑类似,主要区别是:

+
    +
  • 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
  • +
  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
  • +
+

普通索引和唯一索引之间的选择

+

普通索引VS唯一索引:两者类似,区别是唯一索引的列值不能重复,允许一个为空

+

下面从这两种索引对查询语句和更新语句的性能来分析,前提是业务保证记录的唯一性,如果业务不能保证唯一性又有唯一需求,就必须用唯一索引

+

查询过程

+

普通索引:查找到满足条件的第一个记录后,需要查找下一个记录,直到碰到第一个不满足条件的记录

+

唯一索引:由于唯一性,查找到满足条件的第一个记录后就停止

+

由于InnoDB的数据是按数据页为单位来读写,所以两者性能差距微乎其微

+

更新过程

+

change buffer:当需要更新一个数据页时,如果数据页在内存中就直接更新,如果不在,在不影响数据一致性的前提下,InnoDB会将这些更新操作缓存在change buffer中。在下次查询需要访问这个数据页时,读入内存,然后执行change buffer中与这个页有关的操作,这个过程称为merge

+

唯一索引的更新不能用change buffer,因为需要先将数据页读入内存判断操作是否违反唯一性约束

+

假如现在有个插入新记录的操作,如果要更新的目标页在内存中,普通索引和唯一索引性能差距不大。如果目标页不在内存中,对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值;对于普通索引来说,将更新记录在change buffer,此时普通索引的性能好(主键索引的数据页是一定要加载进内存做更新操作,普通索引的数据页不用进内存)

+

change buffer的使用场景

+

因为merge的时候是真正做数据更新的时候,在merge之前,change buffer记录的变更越多,收益越大

+

对于写多读少的业务,change buffer的效果最好,比如账单类、日志类的系统

+

索引的选择和实践

+

尽量使用普通索引

+

如果更新完马上查询,就关闭change buffer。否则开着能提升更新性能

+

change buffer 和 redo log

+

redo log 主要节省的是随机写磁盘的IO消耗(转成顺序写),而change buffer主要节省的则是随机读磁盘的IO消耗。

+

MySQL为什么有时候会选错索引

+

图 8

+

session B 先删除了所有数据然后调用idata存储过程插入了10万行数据。

+

然后session B 执行三条SQL:

+
// 将慢查询日志的阈值设置为0,表示这个线程接下来的语句都会被记录慢查询日志中
+set long_query_time=0;
+select * from t where a between 10000 and 20000; /*Q1*/
+select * from t force index(a) where a between 10000 and 20000;/*Q2*/
+
+

图 9

+

Q1走了全表扫描,Q2使用了正确的索引

+

优化器的逻辑

+

选择索引是优化器的工作,目的是寻找最优方案执行语句,判断标准包括扫描行数、是否使用临时表、是否排序等因素

+

上面查询语句没有涉及临时表和排序,说明扫描行数判断错误了

+

MySQL是怎么判断扫描行数的

+

MySQL在真正开始执行语句之前,并不能精确知道有多少行,而只能用统计信息估算。这个统计信息就是索引的“区分度”,索引上不同的值称为“基数”,基数越大,区分度越好。基数由采样统计得出。

+

如果统计信息不对导致行数和实际情况差距较大,可以使用analyze table 表名 来重新统计索引信息

+

索引选择异常和处理

+

由于索引统计信息不准确导致的问题,可以用analyze table来解决,其它优化器误判的解决方法如下:

+
    +
  • 使用force index强行选择索引。缺点是变更不及时,开发通常不写force index,当生产环境出现问题,再修改需要重新测试和发布
  • +
  • 修改语句,引导MySQL使用我们期望的索引。缺点是需要根据数据特征进行修改,不具备通用性
  • +
  • 新建更合适的索引或删掉误用的索引。缺点是找到更合适的索引比较困难
  • +
+

怎么给字符串字段加索引

+

可以给字符串字段建立一个普通索引,也可以给字符串前缀建立普通索引。使用前缀索引,定义好长度,就可以既节省空间,又不用额外增加太多查询成本

+

可以通过统计索引上有多少个不同的值来判断使用多长的前缀,不同值越多,区分度越高,查询性能越好

+

首先算出这列有多少不同值

+
mysql> select count(distinct email) as L from SUser;
+
+

然后选取不同长度的前缀来看这个值

+
mysql> select 
+  count(distinct left(email,4))as L4,
+  count(distinct left(email,5))as L5,
+  count(distinct left(email,6))as L6,
+  count(distinct left(email,7))as L7,
+from SUser;
+
+

前缀索引对覆盖索引的影响

+

前缀索引可能会增加扫描行数,导致影响性能,还可能导致用不上覆盖索引对查询的优化。

+

前缀索引的叶子节点只包含主键,如果查询字段不仅仅有主键,那必须回表。而用完整字符串做索引,如果查询字段只有主键和索引字段,那就不用回表

+

其它方式

+

对于邮箱来说,前缀索引的效果不错。
+但是对于身份证来说,可能需要长度12以上的前缀索引,才能满足区分度要求,但是这样又太占空间了

+

有一些占用空间更小但是查询效率相同的方法:

+
    +
  1. 倒序存储身份证号,建立长度为6的前缀索引,身份证后6位可以提供足够的区分度
  2. +
  3. 加个身份证的整型hash字段,给这个字段加索引
  4. +
+

这两种方法的相同点是都不支持范围查询,区别在于:

+
    +
  1. 倒序存储不占额外空间
  2. +
  3. 倒序每次写和读都需要额外调用一次reverse函数,hash字段需要额外调用一次crc32函数,reverse稍优
  4. +
  5. hash字段的查询性能更稳定一些
  6. +
+

为什么MySQL为“抖”一下

+

“抖”:SQL语句偶尔会执行特别慢,且随机出现,持续时间很短

+

当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。

+

平时执行很快的更新操作,其实就是在写内存和日志,“抖”可能是在刷脏页(flush),情况有以下几种:

+
    +
  1. redo log满了,系统会停止所有更新操作,把checkpoint往前推进,原位置和新位置之间的所有脏页都flush到磁盘上。尽量避免这种情况,会阻塞更新操作
  2. +
  3. 系统内存不足,淘汰脏页。尽量避免一个查询要淘汰的脏页太多
  4. +
  5. 系统空闲
  6. +
  7. 正常关闭
  8. +
+

InnoDB刷脏页的控制策略

+

使用fio测试磁盘的IOPS,并把innodb_io_capacity设置成这个值,告诉InnoDB全力刷盘可以刷多快

+
fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest
+
+

不能总是全力刷盘,InnoDB刷盘速度还要参考内存脏页比例和redo log写盘速度

+

图 10

+

脏页比例不要经常接近75%,查看命令如下:

+
mysql> select VARIABLE_VALUE into @a from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty';
+select VARIABLE_VALUE into @b from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_total';
+select @a/@b;
+
+

还有个策略是刷盘的“连坐”机制:脏页的邻居如果是脏页会一起被刷盘。这种策略对机械硬盘有大幅度性能提升,但是SSD的IOPS已不是瓶颈,推荐innodb_flush_neighbors设置成0,只刷自己

+

为什么表数据删掉一半,表文件大小不变

+

InnoDB表包含两部分:表结构定义和数据。MySQL 8.0 之前,表结构是存在以.frm为后缀的文件,MySQL 8.0 允许表结构定义放在系统数据表中

+

参数innodb_file_per_table

+

设置成OFF表示,表的数据放在系统共享表空间,也就是跟数据字典放在一起;
+设置成ON表示,每个InnoDB表数据存储在一个以.ibd为后缀的文件中

+

从MySQL 5.6.6开始默认就是ON

+

数据删除流程

+

InnoDB里的数据都是用B+数的结构组织的。

+

图 11

+

记录的复用:删除R4记录时,InnoDB会把记录标记为删除,插入ID在300到600之间的记录时可能会复用这个位置,磁盘文件大小不会缩小

+

数据页的复用:InnoDB的数据是按页存储的。如果将page A上所有记录删除以后,page A会被标记为可复用,这时候插入ID=50的记录需要使用新页时,page A会被复用。因此,delete整个表会把所有数据页都标记为可复用,但是磁盘文件不会变小

+

可以复用,而没被使用的空间,看起来就像是“空洞”,不只是删除数据会造成空洞,随机插入数据会引发索引的数据页分裂,导致空洞。更新索引上的值,可以理解为删除旧值和插入新值,也会造成空洞。解决空洞的方法是重建表

+

重建表

+

可以使用alter table A engine=InnoDB命令来重建表。MySQL 5.6是离线重建,重建期间更新会丢失。

+

图 39
+MySQL 5.6 引入了Online DDL,重建表的流程:

+
    +
  1. 建立一个临时文件,扫描表A主键的所有数据页
  2. +
  3. 用数据页中表A的记录生成B+数,存储到临时文件中
  4. +
  5. 生成临时文件的过程中,将所有对A的操作记录在一个日志文件(row log)中,对应图中state2状态
  6. +
  7. 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表A相同的数据文件,对应图中state3状态
  8. +
  9. 用临时文件替换表A的数据文件
  10. +
+

alter语句在启动时需要获取MDL写锁,这个写锁在真正拷贝数据之前就退化成读锁了,目的是实现Online,MDL读锁不会阻塞记录的增删改操作(DML)

+

推荐使用gh-ost做大表的重建

+

Online 和 inplace

+

inplace是指整个DDL过程在 InnoDB 内部完成,对于 Server层来说,没有把数据挪动到临时表,这是一个“原地”操作,这就是inplace名称由来

+

和inplace对应的是copy,也就是前面离线重建

+

DDL过程如果是 Online 的,就一定是inplace的;反过来未必,全文索引和空间索引是 inplace 的,但不是 Online 的

+

optimize table、analyze table和alter table三种方式重建表的区别:

+
    +
  • 从MySQL 5.6开始,alter table t engine=InnoDB(也就是recreate)默认就是上面引入Online DDL后的重建过程
  • +
  • analyze table t 不是重建表,只是对表的索引信息做重新统计,没有修改数据,这个过程中加了MDL读锁
  • +
  • optimize table t 等于recreate+analyze
  • +
+

count(*)慢该怎么办

+

count(*)的实现方式

+

InnoDB count(*)会遍历全表,优化器会找到最小的索引数进行计数,结果准确但有性能问题。show table status命令显示的行数是采样估算的,不准确

+

用缓存系统保存计数

+

可以用Redis来保存记录数,但是会出现逻辑上不精确的问题。根本原因是这两个不同的存储构成的系统,不支持分布式事务,无法拿到精确一致的视图

+

图 12
+这种情况是Redis的计数不精确

+

图 13
+这种情况是查询结果不精确

+

在数据库保存计数

+

将计数放在数据库里单独的一张计数表中,可以利用事务解决计数不精确的问题

+

图 14

+

在会话B读操作期间,会话A还没提交事务,因此B没有看到计数值加1的操作,因此计数值和“最近100条记录”的结果在逻辑上是一致的

+

不同的count用法

+

count(*)、count(主键id)和count(1) 都表示返回满足条件的结果集的总行数。count(字段),则表示返回满足条件的数据行里面,参数“字段”不为NULL的总个数

+

对于count(主键id)来说,InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为空的,就按行累加。

+

对于count(1)来说,InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。

+

count(1)执行得要比count(主键id)快。因为从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。

+

对于count(字段)来说:

+

如果这个“字段”是定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加;

+

如果这个“字段”定义允许为null,那么执行的时候,判断到有可能是null,还要把值取出来再判断一下,不是null才累加。

+

count(*) 是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count(*)肯定不是null,按行累加。

+

结论是:按照效率排序的话,
+count(字段) < count(主键id) < count(1) ≈ count(*),所以尽量使用count(*)。

+

orderby是怎么工作的

+

假设有SQL语句

+
select city,name,age from t where city='杭州' order by name limit 1000;
+
+

全字段排序

+

图 15

+

如果要排序的数据量小于sort_buffer_size,排序就在内存中完成,否则外部排序(归并)

+

rowid 排序

+

max_length_for_sort_data 是MySQL中专门控制用于排序的行数据的长度的参数,超过这个值就不会全字段排序,用rowid排序

+

图 16

+

全字段排序 VS rowid排序

+

如果内存够就用全字段排序,rowid排序回表多造成磁盘读,性能较差

+

并不是所有的order by语句都要排序的,如果建索引时就是有序的就不排

+

创建一个city和name的联合索引,查询过程如下:

+

图 17

+

还可以创建city、name和age的联合索引,这样就不用回表了

+

图 18

+

如何正确地显示随机消息

+

10000行记录如何随机选择3个

+

内存临时表

+

用order by rand()来实现这个逻辑

+
mysql> select word from words order by rand() limit 3;
+
+

R:随机数,W:单词,pos:rowid,对于有主键的表,rowid就是主键ID,没有主键就由系统生成

+

原表->内存临时表:扫描10000行
+内存临时表->sort_buffer:扫描10000行
+内存临时表->结果集:访问3行

+

图 19

+

order by rand()使用了内存临时表,内存临时表排序的时候使用了rowid排序方法

+

磁盘临时表

+

当内存临时表大小超过了tmp_table_size时,如果使用归并排序,内存临时表会转为磁盘临时表,如果使用优先队列排序(排序+limit操作),且维护的堆大小不超过sort_buffer_size,则不会转为磁盘临时表

+

随机排序方法

+
    +
  1. 取得整个表的行数,记为C
  2. +
  3. 取得 Y = floor(C * rand())
  4. +
  5. 再用 limit Y,1 取得一行
  6. +
+

取多个随机行就重复多次这个算法

+
mysql> select count(*) into @C from t;
+set @Y1 = floor(@C * rand());
+set @Y2 = floor(@C * rand());
+set @Y3 = floor(@C * rand());
+select * from t limit @Y11//在应用代码里面取Y1、Y2、Y3值,拼出SQL后执行
+select * from t limit @Y21;
+select * from t limit @Y31;
+
+

或者优化一下,Y1,Y2,Y3从小到大排序,这样扫描的行数就是Y3

+
id1 = select * from t limit @Y1,1;
+id2 = select * from t where id > id1 limit @Y2-@Y1,1;
+id3 = select * from t where id > id2 limit @Y3
+
+

为什么逻辑相同的SQL语句性能差异巨大

+
    +
  1. 对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器决定放弃走树搜索功能,但不是放弃索引,优化器可以选择遍历索引
  2. +
  3. 隐式类型转换可能会触发上面的规则1
  4. +
  5. 隐式字符编码转换也可能触发上面的规则1
  6. +
+

为什么只查一行的语句也执行这么慢

+

查询长时间不返回

+
    +
  1. 等MDL锁。通过查询sys.schema_table_lock_waits,可以找出造成阻塞的process id,把这个连接用kill杀掉
  2. +
  3. 等flush。可能情况是有一个flush tables命令被别的语句堵住了,然后它又堵住了查询语句,可以用show processlist 查出并杀掉阻塞flush的连接
  4. +
  5. 等行锁。通过查询sys.innodb_lock_waits 杀掉对应连接
  6. +
+

查询慢

+
    +
  1. 查询字段没有索引,走了全表扫描
  2. +
  3. 事务隔离级别为可重复读,当前事务看不到别的事务的修改,但是别的事务执行了多次修改,当前事务在查询时要根据undo log查询到应该看到的值
  4. +
+

幻读

+

幻读:一个事务在前后两次查询同一个范围时,后一次查询看到了前一次查询没有看到的行

+
    +
  1. 在可重复读隔离级别下,普通的查询时快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现
  2. +
  3. 幻读专指“新插入的行”
  4. +
+

幻读的问题

+
    +
  • +

    语义被破坏

    +

    图 20

    +

    session A在T1时刻声明了,“我要把所有d=5的行锁住,不准别的事务进行读写操作”。session B和C破坏了这个语义

    +
  • +
  • +

    数据不一致。根据binlog克隆的库与主库不一致,原因是即使给所有记录都加上锁,新记录还是没上锁

    +
  • +
+

解决幻读

+

间隙锁:锁住两行之间的间隙

+

在行扫描过程中,不仅给行加行锁,还给行间的间隙上锁

+

跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。

+

间隙锁和行锁合称next-key lock,左开右闭

+

间隙锁的引入,可能会导致同样的语句锁住更大的范围,影响并发度。

+

间隙锁只在可重复读隔离级别下才会生效

+

为什么只改一行的语句,锁这么多

+

加锁规则(可重复读隔离级别):

+
    +
  1. 原则1:加锁的基本单位是next-key lock
  2. +
  3. 原则2:查找过程中访问到的对象才会加锁
  4. +
  5. 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁
  6. +
  7. 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁
  8. +
  9. 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止
  10. +
+

假设有如下SQL语句

+
CREATE TABLE `t` (
+  `id` int(11) NOT NULL,
+  `c` int(11) DEFAULT NULL,
+  `d` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `c` (`c`)
+) ENGINE=InnoDB;
+
+insert into t values(0,0,0),(5,5,5),
+(10,10,10),(15,15,15),(20,20,20),(25,25,25);
+
+

案例一:等值查询间隙锁

+

图 21

+

由于表t中没有id=7的记录

+
    +
  1. 根据原则1,加锁单位是next-key lock,session A加锁范围就是(5,10];
  2. +
  3. 同时根据优化2,这是一个等值查询(id=7),而id=10不满足查询条件,next-key lock退化成间隙锁,因此最终加锁的范围是(5,10)。
  4. +
+

所以,session B要往这个间隙里面插入id=8的记录会被锁住,但是session C修改id=10这行是可以的。

+

案例二:非唯一索引等值锁

+

图 22

+

这里session A要给索引c上c=5的这一行加上读锁。

+
    +
  1. 根据原则1,加锁单位是next-key lock,因此会给(0,5]加上next-key lock
  2. +
  3. 要注意c是普通索引,因此仅访问c=5这一条记录是不能马上停下来的,需要向右遍历,查到c=10才放弃。根据原则2,访问到的都要加锁,因此要给(5,10]加next-key lock。
  4. +
  5. 但是同时这个符合优化2:等值判断,向右遍历,最后一个值不满足c=5这个等值条件,因此退化成间隙锁(5,10)
  6. +
  7. 根据原则2 ,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么session B的update语句可以执行完成。
  8. +
+

但session C要插入一个(7,7,7)的记录,就会被session A的间隙锁(5,10)锁住。

+

需要注意,在这个例子中,lock in share mode只锁覆盖索引,但是如果是for update就不一样了。 执行 for update时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。

+

这个例子说明,锁是加在索引上的;同时,它给我们的指导是,如果你要用lock in share mode来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段。比如,将session A的查询语句改成select d from t where c=5 lock in share mode

+

案例三:主键索引范围锁

+
mysql> select * from t where id=10 for update;
+mysql> select * from t where id>=10 and id<11 for update;
+
+

这两条语句在逻辑上是等价的,但是加锁规则不一样

+
    +
  1. 开始执行的时候,要找到第一个id=10的行,因此本该是next-key lock(5,10]。 根据优化1, 主键id上的等值条件,退化成行锁,只加了id=10这一行的行锁
  2. +
  3. 范围查找就往后继续找,找到id=15这一行停下来,因此需要加next-key lock(10,15]
  4. +
+

所以,session A这时候锁的范围就是主键索引上,行锁id=10和next-key lock(10,15]

+

需要注意一点,首次session A定位查找id=10的行的时候,是当做等值查询来判断的,而向右扫描到id=15的时候,用的是范围查询判断

+

案例四:非唯一索引范围锁

+

图 23

+

这次session A用字段c来判断,加锁规则跟主键索引范围锁的唯一不同是:在第一次用c=10定位记录的时候,索引c上加了(5,10]这个next-key lock后,由于索引c是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终sesion A加的锁是,索引c上的(5,10] 和(10,15] 这两个next-key lock

+

案例五:唯一索引范围锁bug

+

图 24

+

session A是一个范围查询,按照原则1的话,应该是索引id上只加(10,15]这个next-key lock,并且因为id是唯一键,所以循环判断到id=15这一行就应该停止了。

+

但是实现上,InnoDB会往前扫描到第一个不满足条件的行为止,也就是id=20。而且由于这是个范围扫描,因此索引id上的(15,20]这个next-key lock也会被锁上。

+

案例六:非唯一索引上存在“等值”的例子

+

现在插入一条新记录

+
mysql> insert into t values(30,10,30);
+
+

图 25

+

delete语句加锁的逻辑和 select … for update是类似的,session A在遍历的时候,先访问第一个c=10的记录。同样地,根据原则1,这里加的是(c=5,id=5)到(c=10,id=10)这个next-key lock。

+

然后,session A向右查找,直到碰到(c=15,id=15)这一行,循环才结束。根据优化2,这是一个等值查询,向右查找到了不满足条件的行,所以会退化成(c=10,id=10) 到 (c=15,id=15)的间隙锁。

+

也就是说,这个delete语句在索引c上的加锁范围,就是下图中蓝色区域覆盖的部分。蓝色区域左右两边都是虚线,表示开区间,即(c=5,id=5)和(c=15,id=15)这两行上都没有锁。

+

图 26

+

案例七:limit 语句加锁

+

图 27

+

session A的delete语句加了 limit 2。你知道表t里c=10的记录其实只有两条,因此加不加limit 2,删除的效果都是一样的,但是加锁的效果却不同。可以看到,session B的insert语句执行通过了,跟案例六的结果不同。这是因为,案例七里的delete语句明确加了limit 2的限制,因此在遍历到(c=10, id=30)这一行之后,满足条件的语句已经有两条,循环就结束了。

+

因此,索引c上的加锁范围就变成了从(c=5,id=5)到(c=10,id=30)这个前开后闭区间,如下图所示:
+图 28

+

这个例子对我们实践的指导意义就是,在删除数据的时候尽量加limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。

+

案例八:一个死锁的例子

+

本案例目的是说明:next-key lock 实际上是间隙锁和行锁加起来的结果

+

图 29

+
    +
  1. session A 启动事务后执行查询语句加lock in share mode,在索引c上加了next-key lock(5,10] 和间隙锁(10,15);
  2. +
  3. session B 的update语句也要在索引c上加next-key lock(5,10] ,进入锁等待;
  4. +
  5. 然后session A要再插入(8,8,8)这一行,被session B的间隙锁锁住。由于出现了死锁,InnoDB让session B回滚
  6. +
+

你可能会问,session B的next-key lock不是还没申请成功吗?

+

其实是这样的,session B的“加next-key lock(5,10] ”操作,实际上分成了两步,先是加(5,10)的间隙锁,加锁成功;然后加c=10的行锁,这时候才被锁住的。

+

也就是说,我们在分析加锁规则的时候可以用next-key lock来分析。但是要知道,具体执行的时候,是要分成间隙锁和行锁两段来执行的。

+

“饮鸩止渴”提高性能的方法

+

短连接风暴

+

短连接模式就是连接到数据库后,执行很少的SQL语句就断开,下次需要的时候再重连,在业务高峰期,会出现连接数暴涨的情况

+

两种有损业务的解决方法:

+
    +
  1. 处理掉占着连接但是不工作的线程。优先断开事务外空闲太久的连接
  2. +
  3. 减少连接过程的损耗。关闭权限验证
  4. +
+

慢查询性能问题

+

引发慢查询的情况有三种:

+
    +
  1. 索引没有设计好。最高效的解决方法是直接alter table建索引
  2. +
  3. SQL语句没有写好,导致没用上索引。解决方法是使用query_rewrite重写SQL语句
  4. +
  5. MySQL选错了索引。应急方案是给语句加上force index或者使用query_rewrite重写语句加上force index
  6. +
+

出现情况最多的是前两种,通过下面过程可以预先发现和避免

+
    +
  1. 上线前,在测试环境,把慢查询日志(slow log)打开,并且把long_query_time设置成0,确保每个语句都会被记录入慢查询日志
  2. +
  3. 在测试表里插入模拟线上的数据,做一遍回归测试
  4. +
  5. 观察慢查询日志里每类语句的输出,特别留意Rows_examined字段是否与预期一致
  6. +
+

QPS突增问题

+
    +
  1. 业务bug导致。可以把这个功能的SQL从白名单去掉
  2. +
  3. 如果新功能使用的是单独的数据库用户,可以用管理员账号把这个用户删掉,然后断开连接
  4. +
  5. 用query_rewrite把压力最大的SQL语句直接重写成"select 1"返回
  6. +
+

方法3存在两个副作用:

+
    +
  1. 如果别的功能也用到了这个SQL语句就会误伤
  2. +
  3. 该语句可能是业务逻辑的一部分,导致业务逻辑一起失败
  4. +
+

方法3是优先级最低的方法。方法1和2依赖于规范的运维体系:虚拟化、白名单机制、业务账号分离

+

MySQL是怎么保证数据不丢的

+

只要redo log和binlog保证持久化到磁盘,就能确保MySQL异常重启后,数据可以恢复

+

binlog的写入机制

+

binlog的写入逻辑:事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中

+

一个事务的binlog是不能被拆开的,因此不论这个事务多大,也要确保一次性写入

+

系统给binlog cache分配了一片内存,每个线程一个,参数binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘

+

事务提交的时候,执行器把binlog cache里的完整事务写入到binlog中,并清空binlog cache
+图 1

+
    +
  • 图中的write,指的是把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快
  • +
  • 图中的fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为fsync才占磁盘的IOPS
  • +
+

write和fsync的时机由参数sync_binlog控制:

+
    +
  1. sync_binlog=0,表示每次提交事务都只write,不fsync
  2. +
  3. sync_binlog=1,表示每次提交事务都会fsync
  4. +
  5. sync_binlog=N(N>1),表示每次提交事务都write,但累积N个事务后才fsync
  6. +
+

sync_binlog设置成N可以改善IO瓶颈场景的性能,但对应的风险是:如果主机发生异常重启,会丢失最近N个事务的binlog

+

redo log的写入机制

+

事务执行过程中,生成的redo log要先写到redo log buffer,但不是每次生成后都要直接持久化到磁盘,因为事务没提交,日志丢了也不会有损失。
+但是也有可能事务没有提交,redo log buffer 中的部分日志持久化到了磁盘。下图是redo log的三种状态

+

图 2

+

日志写到redo log buffer是很快的,write到page cache也快,但是持久化到磁盘就很慢。

+

InnoDB提供了innodb_flush_log_at_trx_commit参数来控制redo log的写入策略:

+
    +
  1. 设置为0表示每次事务提交时都只是把redo log 留在redo log buffer中
  2. +
  3. 设置为1表示每次事务提交时都将redo log直接持久化到磁盘
  4. +
  5. 设置为2表示每次事务提交时都只是把redo log写到page cache
  6. +
+

InnoDB有个后台线程,每隔1秒,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘

+

注意,事务执行过程中的redo log也是直接写在redo log buffer中的,这些redo log也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的redo log,也可能已经持久化到磁盘

+

除了后台线程每秒一次的轮询操作外,还有两个场景会让没提交的事务的redo log写入到磁盘

+
    +
  1. redo log buffer占用的空间即将达到 innodb_log_buffer_size一半的时候,后台线程会主动write到page cache
  2. +
  3. 并行的事务提交的时候,顺带将这个事务的redo log buffer持久化到磁盘
  4. +
+

两阶段提交的过程,时序上redo log先prepare,再写binlog,最后再把redo log commit

+

如果innodb_flush_log_at_trx_commit设置为1,那么redo log在prepare阶段就要持久化一次,因为有一个崩溃恢复逻辑是prepare的redo log + 完整的binlog

+

每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB就认为redo log在commit的时候就不需要fsync了,只会write到文件系统的page cache中就够了

+

通常我们说MySQL的“双1”配置,指的就是sync_binlog和innodb_flush_log_at_trx_commit都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare 阶段),一次是binlog

+

redo log组提交

+

日志逻辑序列号LSN:LSN单调递增,用来对应redo log的一个个写入点。每次写入长度为length的redo log,LSN的值就会加上length

+

如下图所示,是三个并发事务在prepare阶段,都写完redo log buffer,持久化到磁盘的过程中
+图 3

+

从图中可以看到,

+
    +
  1. trx1是第一个到达的,会被选为这组的leader
  2. +
  3. 等trx1要开始写盘的时候,这个组里面已经有3个事务了,这时候LSN也变成了160
  4. +
  5. trx1去写盘的时候,带的就是LSN=160,因此等trx1返回时,所有LSN小于等于160的redo log,都已经被持久化到磁盘
  6. +
  7. 这时候trx2和trx3就可以直接返回了
  8. +
+

所以,一次组提交里面,组员越多,节约磁盘IOPS的效果越好。但如果只有单线程压测,那就只能老老实实地一个事务对应一次持久化操作了。

+

在并发更新场景下,第一个事务写完redo log buffer以后,接下来这个fsync越晚调用,组员可能越多,节约IOPS的效果就越好。

+

MySQL为了让组提交的效果更好,细化了两阶段提及的顺序,让redo log的fsync往后拖

+

图 4

+

上图的顺序说明binlog也可以组提交,但是通常情况下步骤3会执行得很快,所以能集合到一起持久化的binlog比较少。可以通过设置binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count来提升binlog组提交的效果

+

性能瓶颈在IO的提升方法

+
    +
  1. 设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count参数,减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。
  2. +
  3. 将sync_binlog 设置为大于1的值(比较常见是100~1000)。这样做的风险是,主机掉电时会丢binlog日志。
  4. +
  5. 将innodb_flush_log_at_trx_commit设置为2。这样做的风险是,主机掉电的时候会丢数据。
  6. +
+

MySQL是怎么保证主备一致的

+

MySQL的主备一致依赖于binlog

+

MySQL主备的基本原理

+

主备切换流程

+

图 5

+

客户端的读写是直接访问主库,备库同步主库的更新,与主库保持一致。虽然备库不会被客户端访问,但仍推荐设置成只读模式,因为:

+
    +
  1. 有时候一些运营类的查询语句会放到备库上去查,设置为只读可以防止误操作
  2. +
  3. 防止切换逻辑有bug,比如切换过程中出现双写,造成主备不一致
  4. +
  5. 可以用readonly状态来判断节点的角色
  6. +
+

备库的只读对超级权限用户是无效的,用于同步更新的线程拥有超级权限

+

同步流程

+

主库的更新语句同步到备库的完成流程图如下

+

图 6

+

备库B跟主库A之间维持了一个长连接。主库A内部有一个线程,专门用于服务备库B的这个长连接。一个事务日志同步的完整过程如下:

+
    +
  1. 在备库B上通过change master命令,设置主库A的IP、端口、用户名、密码,以及要从哪个位置开始请求binlog,这个位置包含文件名和日志偏移量
  2. +
  3. 在备库B上执行start slave命令,这时候备库会启动两个线程,就是图中的io_thread和sql_thread。其中io_thread负责与主库建立连接。
  4. +
  5. 主库A校验完用户名、密码后,开始按照备库B传过来的位置,从本地读取binlog,发给B
  6. +
  7. 备库B拿到binlog后,写到本地文件,称为中转日志(relay log)
  8. +
  9. sql_thread读取中转日志,解析出日志里的命令,并执行
  10. +
+

binlog的三种格式对比

+

binlog有三种格式,statement、row以及前两种格式的混合mixed

+

假设有如下表:

+
mysql> CREATE TABLE `t` (
+  `id` int(11) NOT NULL,
+  `a` int(11) DEFAULT NULL,
+  `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `a` (`a`),
+  KEY `t_modified`(`t_modified`)
+) ENGINE=InnoDB;
+
+insert into t values(1,1,'2018-11-13');
+insert into t values(2,2,'2018-11-12');
+insert into t values(3,3,'2018-11-11');
+insert into t values(4,4,'2018-11-10');
+insert into t values(5,5,'2018-11-09');
+
+

statement格式就是SQL语句原文
+图 7

+

下图是该语句执行效果
+图 8

+

statement格式下,delete 带 limit,很可能出现主备数据不一致的情况,比如上面的例子:

+
    +
  1. 如果delete语句使用的是索引a,那么会根据索引a找到第一个满足条件的行,也就是说删除的是a=4这一行
  2. +
  3. 但如果使用的是索引t_modified,那么删除的就是 t_modified='2018-11-09’ 也就是a=5这一行。
  4. +
+

row格式binlog如下
+图 9

+

row格式的binlog把SQL语句替换成了两个event:Table_map和Delete_rows

+
    +
  1. Table_map event,用于说明接下来要操作的表是test库的表t;
  2. +
  3. Delete_rows event,用于定义删除的行为。
  4. +
+

借助mysqlbinlog工具查看详细的binlog

+

图 10

+

当binlog_format使用row格式的时候,binlog里面记录了真实删除行的主键id,这样binlog传到备库去的时候,就肯定会删除id=4的行,不会有主备删除不同行的问题。

+

mixed格式吸收了statement和row格式的优点,占用空间小,避免了数据不一致

+

但是现在binlog设置成row的场景更多,理由有很多,其中之一是恢复数据。

+

如果执行的是delete语句,row格式的binlog也会把被删掉的行的整行信息保存起来。所以,如果你在执行完一条delete语句以后,发现删错数据了,可以直接把binlog中记录的delete语句转成insert,把被错删的数据插入回去就可以恢复了

+

如果你是执行错了insert语句呢?那就更直接了。row格式下,insert语句的binlog里会记录所有的字段信息,这些信息可以用来精确定位刚刚被插入的那一行。这时,你直接把insert语句转成delete语句,删除掉这被误插入的一行数据就可以了。

+

如果执行的是update语句的话,binlog里面会记录修改前整行的数据和修改后的整行数据。所以,如果你误执行了update语句的话,只需要把这个event前后的两行信息对调一下,再去数据库里面执行,就能恢复这个更新操作了

+

循环复制问题

+

binlog的特性确保了主备一致性。实际生产上使用比较多的是双M结构
+图 11

+

双M结构中,节点A和B之间总是互为主备关系,在切换的时候就不用再修改主备关系

+

循环复制指的是A节点更新完,把binlog发给B,B更新完又生成binlog发给了A,解决循环复制的方法如下:

+
    +
  1. 规定两个库的server id必须不同,如果相同,则它们之间不能设定为主备关系
  2. +
  3. 一个备库接到binlog并在重放的过程中,生成与原binlog的server id相同的新的binlog
  4. +
  5. 每个库在收到从自己的主库发过来的日志后,先判断server id,如果跟自己的相同,表示这个日志是自己生成的,直接丢弃
  6. +
+

因此,双M结构的日志执行流会变成这样:

+
    +
  1. 从节点A更新的事务,binlog里面记得都是A的server id
  2. +
  3. 传到节点B执行一次以后,节点B生成的binlog的server id也是A的server id
  4. +
  5. 再传回给节点A,A判断到这个server id与自己的相同,就不会再处理这个日志。所以,死循环这里就断掉了
  6. +
+

MySQL是怎么保证高可用的

+

正常情况下,只要主库执行更新生成的所有binlog,都可以传到备库并被正确地执行,备库就能达到跟主库一致的状态,这就是最终一致性。但是MySQL要提供高可用,只有最终一致性是不够的

+

主备延迟

+

与数据同步有关的时间点主要包括以下三个:

+

T1:主库A执行完成一个事务,写入binlog
+T2:备库B接收完这个binlog
+T3:备库B执行完成这个事务

+

主备延迟:同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是T3-T1

+

seconds_behind_master表示备库延迟了多少秒

+

网络正常情况下,主备延迟的主要因素是T3-T2,直接表现是备库消费中转日志(relay log)的速度比主库生产binlog的速度慢

+

主备延迟的来源

+
    +
  1. 备库的机器性能差。解决方法是对称部署
  2. +
  3. 备库的压力大。有些统计查询语句只在备库上跑,导致备库压力大,解决方法是一主多从分担读的压力或者把binlog输送到Hadoop来提供统计查询能力
  4. +
  5. 大事务。比如一次性用delete删除太多数据或者大表DDL
  6. +
  7. 备库的并行复制能力
  8. +
+

由于主备延迟的存在,所以在主备切换的时候,有不同的策略

+

可靠性优先策略

+

在双M结构下,主备切换流程如下:

+
    +
  1. 判断备库B现在的seconds_behind_master,如果小于某个值(比如5秒)继续下一步,否则持续重试这一步;
  2. +
  3. 把主库A改成只读状态,即把readonly设置为true;
  4. +
  5. 判断备库B的seconds_behind_master的值,直到这个值变成0为止;
  6. +
  7. 把备库B改成可读写状态,也就是把readonly 设置为false;
  8. +
  9. 把业务请求切到备库B。
  10. +
+

图 12

+

步骤2直到步骤5,主库A和备库B都处于readonly状态,系统不可用(不可写)

+

可用性优先策略

+

把上面策略里的步骤4和5放到最开始执行,代价是可能出现数据不一致的情况

+

一般情况下,可靠性优于可用性。在满足数据可靠性的前提下,MySQL高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。

+

备库为什么会延迟好几个小时

+

当备库执行日志的速度持续低于主库生成日志的速度,那这个延迟就有可能成了小时级别

+

MySQL 5.6之前,备库应用日志更新数据只能使用单线程,在主库并发高、TPS高时会出现严重的主备延迟问题

+

按表分发策略

+

按表分发事务的基本思路是,如果两个事务更新不同的表,它们就可以并行

+

图 13

+

worker线程维护一张执行队列里的事务涉及的表,key是“库名.表名”,value表示队列中有多少个事务修改这个表

+

事务在分发的时候,和所有worker的冲突关系有3种:

+
    +
  1. 如果跟所有worker都不冲突,coordinator线程就会把这个事务分配给最空闲的woker;
  2. +
  3. 如果跟多于一个worker冲突,coordinator线程就进入等待状态,直到和这个事务存在冲突关系的worker只剩下1个;
  4. +
  5. 如果只跟一个worker冲突,coordinator线程就会把这个事务分配给这个存在冲突关系的worker。
  6. +
+

按表分发方案在多个表负载均衡的场景效果很好。但是碰到热点表会退化成单线程复制

+

按行分发策略

+

要解决热点表的并行复制问题,就需要一个按行并行复制方案。核心思路是:如果两个事务没有更新相同的行,它们在备库上可以并行执行。这个模式要求binlog格式必须是row

+

判断事务和worker是否冲突,用的规则不是“修改同一个表”,而是“修改同一行”。worker维护的hash表的key是“库名+表名+唯一索引的名字+唯一索引的值”

+

按行分发策略比按表分发策略需要消耗更多的计算资源,这两种方案都有一样的约束条件:

+
    +
  1. 要能够从binlog里面解析出表名、主键值和唯一索引的值。也就是说,主库的binlog格式必须是row;
  2. +
  3. 表必须有主键;
  4. +
  5. 不能有外键。表上如果有外键,级联更新的行不会记录在binlog中,这样冲突检测就不准确。
  6. +
+

MySQL 5.6版本的并行复制策略

+

官方MySQL 5.6版本支持的并行复制的力度是按库并行。hash表的key是数据库名

+

相比于按表和按行分发,有两个优势:

+
    +
  1. 构造hash值的时候很快,只需要库名;而且一个实例上DB数也不会很多,不会出现需要构造100万个项这种情况
  2. +
  3. 不要求binlog的格式。因为statement格式的binlog也可以很容易拿到库名
  4. +
+

MariaDB的并行复制策略

+

MariaDB的并行复制策略利用了redo log组提交优化的特性:

+
    +
  1. 能够在同一组里提交的事务,一定不会修改同一行
  2. +
  3. 主库上可以并行执行的事务,备库上也一定可以并行执行
  4. +
+

这个策略的目标是“模拟主库的并行模式”,但它没有实现“真正的模拟主库并发度”这个目标。在主库上,一组事务在commit的时候,下一组事务是同时处于“执行中”状态的

+

MySQL 5.7的并行复制策略

+

由参数slave-parallel-type来控制并行复制策略:

+
    +
  1. 配置为DATABASE,表示使用MySQL 5.6版本的按库并行策略;
  2. +
  3. 配置为 LOGICAL_CLOCK,表示的就是优化过的类似MariaDB的策略
  4. +
+

该优化的策略的思想是:

+
    +
  1. 同时处于prepare状态的事务,在备库执行时是可以并行的;
  2. +
  3. 处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也是可以并行的。
  4. +
+

MySQL 5.7.22的并行复制策略

+

新增了一个并行复制策略,基于WRITESET的并行复制。

+

新增参数binlog-transaction-dependency-tracking,用来控制是否启用这个新策略:

+
    +
  1. COMMIT_ORDER,表示的就是前面介绍的,根据同时进入prepare和commit来判断是否可以并行的策略。
  2. +
  3. WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的hash值,组成集合writeset。如果两个事务没有操作相同的行,也就是说它们的writeset没有交集,就可以并行
  4. +
  5. WRITESET_SESSION,是在WRITESET的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。
  6. +
+

该策略类似按行分发,但是有很大优势:

+
    +
  1. writeset是在主库生成后直接写入到binlog里面的,这样在备库执行的时候,不需要解析binlog内容(event里的行数据),节省了很多计算量;
  2. +
  3. 不需要把整个事务的binlog都扫一遍才能决定分发到哪个worker,更省内存;
  4. +
  5. 由于备库的分发策略不依赖于binlog内容,所以binlog是statement格式也是可以的。
  6. +
+

该方案对于“表上没主键”和“外键约束”的场景,也会暂时退化为单线程模型。

+

主库出问题了,从库怎么办

+

大多数互联网应用场景都是读多写少,要解决读性能问题,就要涉及:一主多从

+

图 14

+

图中,虚线箭头表示的是主备关系,也就是A和A’互为主备, 从库B、C、D指向的是主库A。一主多从的设置,一般用于读写分离,主库负责所有的写入和一部分读,其他的读请求则由从库分担。

+

下面讨论,在一主多从架构下,主库故障后的主备切换问题

+

图 15

+

相比于一主一备的切换流程,一主多从结构在切换完成后,A’会成为新的主库,从库B、C、D也要改接到A’。正是由于多了从库B、C、D重新指向的这个过程,所以主备切换的复杂性也相应增加了

+

基于位点的主备切换

+

节点B设置成节点A’ 的从库的时候,需要执行change master命令,必须设置主库的日志文件名和偏移量。A和A’的位点是不同的,从库B切换时需要先经过“找同步位点”这个逻辑

+

同步位点很难精确取到

+

取同步位点的方法如下:

+
    +
  1. 等待新主库A’ 把中转日志(relay log)全部同步完成
  2. +
  3. 在A’ 上执行show master status命令,得到当前A’ 上最新的 File 和 Position
  4. +
  5. 取原主库A故障的时刻T
  6. +
  7. 用mysqlbinlog工具解析A’的File,得到T时刻的位点
  8. +
+
mysqlbinlog File --stop-datetime=T --start-datetime=T
+
+

图 16

+

图中,end_log_pos后面的值“123”,表示的就是A’这个实例,在T时刻写入新的binlog的位置,可以把这个值作为$master_log_pos ,用在节点B的change master命令里

+

这个值并不精确,从库B的同步线程可能会出错,解决方法如下:

+
    +
  1. 通过sql_slave_skip_counter跳过出错事务
  2. +
  3. 设置slave_skip_errors,跳过指定错误,通常设置成1032,1062,对应的错误是删除数据找不到行,插入数据唯一键冲突
  4. +
+

GTID

+

前两种方式操作复杂,容易出错,MySQL 5.6 引入了GITD。

+

GTID全称是Global Transaction Identifier,也就是全局事务ID,是一个事务在提交的时候生成的,是这个事务的唯一标识,格式是:

+
GTID=server_uuid:gno
+
+
    +
  • server_uuid是一个实例第一次启动时自动生成的,是一个全局唯一的值
  • +
  • gno是一个整数,初始值是1,每次提交事务的时候分配给这个事务,并加1
  • +
+

GTID有两种生成方式:

+
    +
  1. +

    如果gtid_next=automatic,代表使用默认值。这时,MySQL就会把server_uuid:gno分配给这个事务。
    +a. 记录binlog的时候,先记录一行 SET @@SESSION.GTID_NEXT=‘server_uuid:gno’;
    +b. 把这个GTID加入本实例的GTID集合

    +
  2. +
  3. +

    如果gtid_next是一个指定的GTID的值,比如通过set gtid_next='current_gtid’指定为current_gtid,那么就有两种可能:
    +a. 如果current_gtid已经存在于实例的GTID集合中,接下来执行的这个事务会直接被系统忽略;
    +b. 如果current_gtid没有存在于实例的GTID集合中,就将这个current_gtid分配给接下来要执行的事务,也就是说系统不需要给这个事务生成新的GTID,因此gno也不用加1

    +
  4. +
+

每个MySQL实例都维护了一个GTID集合,用来对应“这个实例执行过的所有事务”

+

当从库需要跳过某个事务时,在主库上查出GTID,在从库上提交空事务,把这个GTID加入到从库的GTID集合中

+
set gtid_next='aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10';
+begin;
+commit;
+set gtid_next=automatic;
+start slave;
+
+

基于GTID的主备切换

+

切换命令指定master_auto_position=1表示这个主备关系使用的是GTID协议,不需要指定主库日志文件和偏移量

+

我们把A’ 的GTID集合记为set_a,实例B的GTID集合记为set_b,切换流程如下:

+
    +
  1. 实例B指定主库A’,基于主备协议建立连接。
  2. +
  3. 实例B把set_b发给主库A’
  4. +
  5. 实例A’算出set_a与set_b的差集,也就是所有存在于set_a,但是不存在于set_b的GITD的集合,判断A’本地是否包含了这个差集需要的所有binlog事务。
    +a. 如果不包含,表示A’已经把实例B需要的binlog给删掉了,直接返回错误;
    +b. 如果确认全部包含,A’从自己的binlog文件里面,找出第一个不在set_b的事务,发给B;
  6. +
  7. 之后就从这个事务开始,往后读文件,按顺序取binlog发给B去执行
  8. +
+

GTID和在线DDL

+

假设,这两个互为主备关系的库还是实例X和实例Y,且当前主库是X,并且都打开了GTID模式。这时的主备切换流程可以变成下面这样:

+
    +
  • +

    在实例X上执行stop slave。

    +
  • +
  • +

    在实例Y上执行DDL语句。注意,这里并不需要关闭binlog。

    +
  • +
  • +

    执行完成后,查出这个DDL语句对应的GTID,并记为 server_uuid_of_Y:gno。

    +
  • +
  • +

    到实例X上执行以下语句序列:

    +
    set GTID_NEXT="server_uuid_of_Y:gno";
    +begin;
    +commit;
    +set gtid_next=automatic;
    +start slave;
    +
    +

    这样做的目的在于,既可以让实例Y的更新有binlog记录,同时也可以确保不会在实例X上执行这条更新。

    +
  • +
  • +

    接下来,执行完主备切换,然后照着上述流程再执行一遍即可

    +
  • +
+

读写分离有哪些坑

+

由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态,这种现象称为“过期读”

+

过期读处理方案包括:

+
    +
  • 强制走主库方案
  • +
  • sleep方案
  • +
  • 判断主备无延迟方案
  • +
  • 配合semi-sync方案
  • +
  • 等主库位点方案
  • +
  • 等GTID方案
  • +
+

强制走主库方案

+

该方案将查询请求分为两类:

+
    +
  1. 对于必须要拿到最新结果的请求,强制将其发到主库上。比如,在一个交易平台上,卖家发布商品以后,马上要返回主页面,看商品是否发布成功。那么,这个请求需要拿到最新的结果,就必须走主库
  2. +
  3. 对于可以读到旧数据的请求,才将其发到从库上。在这个交易平台上,买家来逛商铺页面,就算晚几秒看到最新发布的商品,也是可以接受的。那么,这类请求就可以走从库
  4. +
+

这个方案用的最多,但是问题在于存在“所有查询都不能是过期读”的需求,比如金融类业务,那就必须放弃读写分离,所有读写压力都在主库

+

下面讨论的是:可以支持读写分离的场景下,有哪些解决过期读的方案

+

Sleep方案

+

主库更新后,读从库之前先sleep一下。这个方案假设,大多数情况下主备延迟在1s之内

+

该方案可以解决类似Ajax场景下的过期读问题。例如卖家发布商品,直接将卖家输入的内容作为新商品显示出来,并不查从库。等待卖家刷新页面,相当于sleep了一段时间,解决了过期读问题

+

该方案存在的问题是不精确:

+
    +
  1. 如果查询请求本来0.5s就可以在从库上拿到正确结果,也会等到1s
  2. +
  3. 如果延迟超过1s,还是会出现过期读
  4. +
+

判断主备无延迟方案

+

有三种方法:

+
    +
  1. 每次从库执行查询请求前,先判断seconds_behind_master是否已经等于0。如果还不等于0 ,那就必须等到这个参数变为0才能执行查询请求。
  2. +
  3. 对比位点。如果Master_Log_File和Relay_Master_Log_File、Read_Master_Log_Pos和Exec_Master_Log_Pos这两组值完全相同表示主备无延迟
  4. +
  5. 对比GITD。Retrieved_Gtid_Set和Executed_Gtid_Set相同表示是主备无延迟
  6. +
+

该方案比Sleep更准确,方法2和3比1准确,但是不能说精确。因为存在客户端已经收到提交确认,而备库还没收到日志的状态,因此备库认为主备无延迟,从而发生过期读

+

配合semi-sync

+

为解决上面的问题,引入semi-sync replication:

+

semi-sync做了这样的设计:

+
    +
  1. 事务提交的时候,主库把binlog发给从库
  2. +
  3. 从库收到binlog以后,发回给主库一个ack
  4. +
  5. 主库收到ack以后,才能给客户端返回“事务完成”的确认
  6. +
+

开启semi-sync,就表示所有给客户端发送过确认的事务,都确保备库已经收到了这个日志

+

semi-sync+判断主备无延迟方案存在两个问题:

+
    +
  1. 一主多从情况下,因为主库只要收到一个从库的ack就给客户端返回确认,其它未响应ack的从库可能会发生过期读问题
  2. +
  3. 在业务高峰期,主库的位点或者GITD集合更新很快,这种情况下,可能出现从库一直存在主备延迟导致客户端查询一直等待
  4. +
+

等主库位点方案

+

该方案解决了前面两个问题

+

命令:

+
select master_pos_wait(file, pos[, timeout]);
+
+

这条命令的逻辑如下:

+
    +
  1. 它是在从库执行的
  2. +
  3. 参数file和pos指的是主库上的文件名和位置
  4. +
  5. timeout可选,设置为正整数N表示这个函数最多等待N秒
  6. +
+

为了解决前面两个问题,流程如下:

+
    +
  1. trx1事务更新完成后,马上执行show master status得到当前主库执行到的File和Position;
  2. +
  3. 选定一个从库执行查询语句;
  4. +
  5. 在从库上执行select master_pos_wait(File, Position, 1);
  6. +
  7. 如果返回值是>=0的整数,则在这个从库执行查询语句;
  8. +
  9. 否则,到主库执行查询语句。
    +图 17
  10. +
+

GTID方案

+

等GTID也可以解决前面两个问题

+

流程如下:

+
    +
  1. trx1事务更新完成后,从返回包直接获取这个事务的GTID,记为gtid1;
  2. +
  3. 选定一个从库执行查询语句;
  4. +
  5. 在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);
  6. +
  7. 如果返回值是0,则在这个从库执行查询语句;
  8. +
  9. 否则,到主库执行查询语句。
    +图 18
  10. +
+

如何判断一个数据库是不是出问题了

+

select 1 判断

+

select 1 成功返回只能说明数据库进程还在,不能说明没问题

+

并发连接:通过show precesslist查询连接数,连接数可以远大于并发查询数量
+并发查询:“当前正在执行”的语句的数量

+

线程进入锁等待后,并发线程的计数会减一,即进入锁等待的线程不吃CPU

+

假如设置并发线程数是3,下面的情况是A、B、C在并发查询,D先select 1不占并发线程数所以能正常返回,但实际上已经不能正常查询了
+图 19

+

查表判断

+

为了能够检测InnoDB并发线程数过多导致的系统不可用情况,我们需要找一个访问InnoDB的场景。一般的做法是,在系统库(mysql库)里创建一个表,比如命名为health_check,里面只放一行数据,然后定期执行:

+
mysql> select * from mysql.health_check;
+
+

这种方法在磁盘空间满了就无效。因为更新事务要写binlog,而一旦binlog所在磁盘满了,那么所有更新语句都会堵住,但是系统仍然可以读数据

+

更新判断

+

我们把查询换成更新来作为监控语句。常见做法是放一个timestamp字段表示最后一次检测时间,这条更新语句类似于:

+
mysql> update mysql.health_check set t_modified=now();
+
+

主库和备库用同样的更新语句可能会出现行冲突,导致主备同步停止,所以mysql.health_check表不能只有一行数据

+
mysql> CREATE TABLE `health_check` (
+  `id` int(11) NOT NULL,
+  `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB;
+
+/* 检测命令 */
+insert into mysql.health_check(id, t_modified) values (@@server_id, now()) on duplicate key update t_modified=now();
+
+

MySQL规定主备的server_id必须不同,这样主备各自的检测命令就不会冲突

+

更新判断存在的问题是“判定慢”。因为更新语句在IO负载已经100%时仍然可能在超时前完成。检测系统看到update命令没有超时,就认为“系统正常”,但实际上正常SQL语句已经很慢了

+

内部统计

+

前面几种方法都是外部检测,外部检测都需要定时轮询,所以系统可能已经出问题了,但是却需要等到下一个检测发起执行语句的时候,才有可能发现问题,导致主备切换慢

+

针对磁盘利用率问题,MySQL 5.6 在file_summary_by_event_name表里统计了每次IO请求的时间,可以设置阈值作为检测逻辑

+

误删数据怎么办?

+

误删分为以下几类:

+
    +
  1. 使用delete误删数据行
  2. +
  3. 使用drop table或者truncate table误删数据表
  4. +
  5. 使用drop database误删数据库
  6. +
  7. 使用rm误删整个MySQL实例
  8. +
+

误删行

+

可以使用Flashback恢复,原理是修改binlog的内容,拿回原库重放。使用这个方案的前提是确保binlog_format=row 和 binlog_row_image=FULL

+

建议在备库上执行,再恢复回主库

+

误删库/表

+

这种情况要求线上有定期的全量备份,并且实时备份binlog

+

假如有人中午12点误删了一个库,恢复数据的流程如下:

+
    +
  1. 取最近一次全量备份,假设这个库是一天一备,上次备份是当天0点;
  2. +
  3. 用备份恢复出一个临时库;
  4. +
  5. 从日志备份里面,取出凌晨0点之后的日志;
  6. +
  7. 把这些日志,除了误删除数据的语句外,全部应用到临时库
  8. +
+

如果临时库有多个数据库,在使用mysqlbinlog时可以加上-database指定误删表所在库,加速数据恢复

+

在应用日志的时候,需要跳过12点误操作的那个语句的binlog:

+
    +
  • 如果原实例没有使用GTID模式,只能在应用到包含12点的binlog文件的时候,先用–stop-position参数执行到误操作之前的日志,然后再用–start-position从误操作之后的日志继续执行;
  • +
  • 如果实例使用了GTID模式,就方便多了。假设误操作命令的GTID是gtid1,那么只需要执行set gtid_next=gtid1;begin;commit; 先把这个GTID加到临时实例的GTID集合,之后按顺序执行binlog的时候,就会自动跳过误操作的语句
  • +
+

即使这样,使用mysqlbinlog方法恢复数据仍然不快,因为:

+
    +
  1. mysqlbinlog并不能指定只解析一个表的日志
  2. +
  3. 用mysqlbinlog解析出日志应用,应用日志的过程就只能是单线程
  4. +
+

一种加速方法是,在用备份恢复出临时实例之后,将这个临时实例设置成线上备库的从库,这样:

+
    +
  1. 在start slave之前,先通过执行
    +change replication filter replicate_do_table = (tbl_name) 命令,就可以让临时库只同步误操作的表;
  2. +
  3. 这样做也可以用上并行复制技术,来加速整个数据恢复过程。
  4. +
+

延迟复制备库

+

上面的方案存在“恢复时间不可控问题”,比如一周一备份,第6天误操作,那就需要恢复6天的日志,这个恢复时间可能按天计算

+

一般的主备复制结构存在的问题是,如果主库上有个表被误删了,这个命令很快也会被发给所有从库,进而导致所有从库的数据表也都一起被误删了。

+

延迟复制的备库是一种特殊的备库,通过 CHANGE MASTER TO MASTER_DELAY = N命令,可以指定这个备库持续保持跟主库有N秒的延迟。

+

比如你把N设置为3600,这就代表了如果主库上有数据被误删了,并且在1小时内发现了这个误操作命令,这个命令就还没有在这个延迟复制的备库执行。这时候到这个备库上执行stop slave,再通过之前介绍的方法,跳过误操作命令,就可以恢复出需要的数据。

+

这样的话,你就随时可以得到一个,只需要最多再追1小时,就可以恢复出数据的临时实例,也就缩短了整个数据恢复需要的时间

+

预防误删库/表的方法

+
    +
  1. +

    账号分离,避免写错命令

    +
      +
    • 只给业务开发同学DML权限,而不给truncate/drop权限。而如果业务开发人员有DDL需求的话,也可以通过开发管理系统得到支持
    • +
    • 即使是DBA团队成员,日常也都规定只使用只读账号,必要的时候才使用有更新权限的账号
    • +
    +
  2. +
  3. +

    指定操作规范,避免写错要删除的表名

    +
      +
    • 删除数据表之前,必须先对表做改名操作。然后,观察一段时间,确保对业务无影响以后再删除这张表。
    • +
    • 改表名的时候,要求给表名加固定的后缀(比如加_to_be_deleted),然后删除表的动作必须通过管理系统执行。并且,管理系删除表的时候,只能删除固定后缀的表。
    • +
    +
  4. +
+

rm删除数据

+

对于有高可用机制的MySQL集群,最不怕rm。只要整个集群没被删掉,HA系统会选出新主库,保证整个集群正常工作。因此备库尽量跨机房、跨城市

+

为什么还有kill不掉的语句

+

MySQL有两个kill命令:

+
    +
  • kill query+线程id,表示终止这个线程正在执行的语句
  • +
  • kill connection+线程id,connection可缺省,表示断开这个线程的连接,如果有语句正在执行,先停止语句
  • +
+

收到kill后,线程做什么

+

kill并不是马上停止,而是告诉线程,这条语句已经不需要继续执行了,可以开始“执行停止的逻辑了”

+

处理kill query命令的线程做了两件事:

+
    +
  1. 把目标线程的运行状态改成THD::KILL_QUERY(将变量killed赋值为THD::KILL_QUERY);
  2. +
  3. 给目标线程发一个信号,通知目标线程处理THD::KILL_QUERY状态。如果目标线程处于等待状态,必须是一个可以被唤醒的等待,否则不会执行到判断线程状态的“埋点”
  4. +
+

处理kill connection命令的线程做了两件事:

+
    +
  1. 把目标线程状态设置为KILL_CONNECTION
  2. +
  3. 关闭目标线程的网络连接
  4. +
+

kill无效的两类情况:

+
    +
  1. 线程没有执行到判断线程状态的逻辑。这种情况有innodb_thread_concurrency 不够用,IO压力过大
  2. +
  3. 终止逻辑耗时较长。这种情况有kill超大事务、回滚大查询、kill最后阶段的DDL命令
  4. +
+

处于Killed状态的线程,你可以通过影响系统环境来让状态尽早结束。比如并发度不够导致线程没有执行到判断线程状态的逻辑,就增大innodb_thread_concurrency。除此之外,做不了什么,只能等流程自己结束

+

大查询会不会打爆内存

+

主机内存小于表的大小,全表扫描不会用光主机内存,否则逻辑备份早就挂了

+

全表扫描对server层的影响

+

假设对200G的表 db1.t 全表扫描,需要保留结果到客户端,会使用类似命令:

+
mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t" > $target_file
+
+

服务端不保存完整的查询结果集,取数据和发数据的流程是这样的:

+
    +
  1. 获取一行,写到net_buffer中
  2. +
  3. 重复获取行,直到net_buffer写满,调用网络接口发出去
  4. +
  5. 如果发送成功,就清空net_buffer,然后继续取下一行,并写入net_buffer
  6. +
  7. 如果发送函数返回EAGAIN或WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送
  8. +
+

图 21

+

从这个流程可以看出:

+
    +
  1. 一个查询在发送过程中,占用的MySQL内部的内存最大就是net_buffer_length这么大,并不会达到200G;
  2. +
  3. socket send buffer 也不可能达到200G(默认定义/proc/sys/net/core/wmem_default),如果socket send buffer被写满,就会暂停读数据的流程。
  4. +
+

全表扫描对InnoDB层的影响

+

数据页在Buffer Pool(BP)中管理,BP可以起到加速查询的作用,作用效果依赖于一个重要指标:内存命中率

+

BP的大小由参数 innodb_buffer_pool_size 确定,一般设置成可用物理内存的60%~80%

+

如果BP满了,要从磁盘读入一个数据页,就要淘汰一个旧数据页,InnoDB内存管理用的是改进后的最近最少使用(LRU)算法

+

图 23

+

上图head指向刚刚被访问过的数据页

+

基本的LRU算法在遇到全表扫描历史数据表时,会出现内存命中率急剧下降,磁盘压力增加,SQL响应变慢的情况

+

图 22

+

InnoDB按照 5:3 将LRU链表分成young区和old区,LRU_old指向old区域第一个位置,即整个链表的5/8处

+

改进后的LRU算法如下:

+
    +
  1. 访问young区域的数据页,和之前的算法一样,移动到链表头
  2. +
  3. 访问不在链表中的数据页,淘汰tail指向的最后一页,在LRU_old处插入新数据页
  4. +
  5. 访问old区域的数据页,若这个数据页在LRU链表中存在时间超过1s,就移动到链表头部,否则不动,1s由参数innodb_old_blocks_time控制
  6. +
+

这个策略在扫描大表时不会对young区域造成影响,保证BP响应正常业务的查询命中率

+

可不可以使用join

+

先创建两个DDL一样的表

+
CREATE TABLE `t2` (
+  `id` int(11) NOT NULL,
+  `a` int(11) DEFAULT NULL,
+  `b` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `a` (`a`)
+) ENGINE=InnoDB;
+/*省略给t2插入1000行数据*/
+create table t1 like t2;
+insert into t1 (select * from t2 where id<=100)
+
+

Index Nested-Loop Join

+

有如下语句:

+
select * from t1 straight_join t2 on (t1.a=t2.a);
+
+

straight_join让MySQL使用固定的连接方式执行查询,这里t1是驱动表,t2是被驱动表

+

图 24

+

这个语句的执行流程如下:

+
    +
  1. 从表t1中读入一行数据R
  2. +
  3. 从数据行R中,取出a字段到表t2里去查
  4. +
  5. 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分
  6. +
  7. 重复执行步骤1到3,直到表t1的末尾循环结束
  8. +
+

在形式上,这个过程和我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以我们称之为“Index Nested-Loop Join”,简称NLJ

+

图 25

+

在流程里:

+
    +
  1. 对驱动表t1做了全表扫描,这个过程需要扫描100行
  2. +
  3. 对于每一行R,根据a字段去表t2查找,走的是树搜索过程。由于我们构造的数据是一一对应的,因此每次搜索都只扫描一行,也就是总共扫描100行
  4. +
  5. 所以,整个执行流程,总扫描行数是200
  6. +
+

如果不用join,上面的连接需求,用单表查询实现的话,扫描行数一样,但是交互次数多,而且客户端要自己拼接SQL语句和结果,因此不如直接join

+

假设驱动表行数是N。被驱动表行数是M,被驱动表查一行数据要先走索引a,再走主键索引,因此时间复杂度是2log2M2*log_2 M。驱动表要扫描N行,然后每行都要去被驱动表上匹配,所以整个执行过程复杂度是 N+N2log2MN+N*2*log_2 M。显然N影响更大,因此让小表做驱动表

+

Simple Nested-Loop Join

+

现在语句改成如下:

+
select * from t1 straight_join t2 on (t1.a=t2.b);
+
+

由于t2的字段b没有索引,每次到t2去匹配都要做全表扫描,因此这个查询要扫描100*1000=10万行。

+

Block Nested-Loop Join

+

当被驱动表上没有可用索引,MySQL使用的算法流程如下:

+
    +
  1. 把表t1的数据读入线程内存join_buffer中,由于我们这个语句中写的是select *,因此是把整个表t1放入了内存;
  2. +
  3. 扫描表t2,把表t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回。
  4. +
+

图 26

+

该算法和Simple Nested-Loop Join算法扫描的行数一样多,但该算法是内存操作,速度更快。碰到大表不能放入join_buffer的情况就分多次放

+

总结一下:

+

第一个问题:能不能使用join语句?

+
    +
  1. 如果可以使用Index Nested-Loop Join算法,也就是说可以用上被驱动表上的索引,其实是没问题的;
  2. +
  3. 如果使用Block Nested-Loop Join算法,扫描行数就会过多。尤其是在大表上的join操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种join尽量不要用
  4. +
+

所以在判断要不要使用join语句时,就是看explain结果里面,Extra字段里面有没有出现“Block Nested Loop”字样

+

第二个问题:如果要使用join,应该选择大表做驱动表还是选择小表做驱动表?

+

总是使用小表做驱动表。更准确地说,在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表

+

join语句怎么优化

+

创建两个表t1、t2(id int primary key, a int, b int, index(a))。给表t1插入1000行数据,每一行a=1001-id,即字段a是逆序的。给表t2插入100万行数据

+

Multi-Range Read优化

+

现在有SQL语句:

+
select * from t1 where a>=1 and a<=100;
+
+

MRR优化的设计思路是:大多数的数据都是按照主键递增顺序插入的,所以按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。使用MRR的语句的执行流程如下:

+
    +
  1. 根据索引a,定位到满足条件的记录,将id值放入read_rnd_buffer中;
  2. +
  3. 将read_rnd_buffer中的id进行递增排序
  4. +
  5. 排序后的id数组,依次到主键id索引中查记录,并作为结果返回。
  6. +
+

图 27

+

MRR能够提升性能的核心在于,这条查询语句在索引a上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键id。这样通过排序以后,再去主键索引查数据,才能体现出“顺序性”的优势

+

Batched Key Access

+

MySQL 5.6 引入Batched Key Acess(BKA)算法,这个算法是对NLJ算法的优化

+

NLJ算法执行的逻辑是:从驱动表t1,一行行地取出a的值,再到被驱动表t2去做join。也就是说,对于表t2来说,每次都是匹配一个值。这时,MRR的优势就用不上了

+

优化思路就是,从表t1里一次性多拿出些行,一起传给表t2。取出的数据先放到join_buffer

+

图 28

+

BNL算法的性能问题

+
    +
  1. 可能会多次扫描被驱动表,占用磁盘IO资源;
  2. +
  3. 判断join条件需要执行M*N次对比(M、N分别是两张表的行数),如果是大表就会占用非常多的CPU资源;
  4. +
  5. 可能会导致Buffer Pool的热数据被淘汰,影响内存命中率。
  6. +
+

如果explain命令发现优化器使用BNL算法。我们就需要优化,常见做法是,给被驱动表的join字段加上索引,把BNL算法转成BKA算法

+

BNL转BKA

+
select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000;
+
+

在索引创建资源开销大情况下,可以考虑使用临时表:

+
    +
  1. 把表t2中满足条件的数据放在临时表tmp_t中;
  2. +
  3. 为了让join使用BKA算法,给临时表tmp_t的字段b加上索引;
  4. +
  5. 让表t1和tmp_t做join操作
  6. +
+

对应的SQL语句:

+
create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
+insert into temp_t select * from t2 where b>=1 and b<=2000;
+select * from t1 join temp_t on (t1.b=temp_t.b);
+
+

扩展-hash join

+

BNL的问题是join_buffer里面维护的是一个无序数组,如果是一个hash表,可以大幅减少判断次数。可以在业务端实现这个优化:

+
    +
  1. select * from t1;取得表t1的全部1000行数据,在业务端存入一个hash结构
  2. +
  3. select * from t2 where b>=1 and b<=2000; 获取表t2中满足条件的2000行数据。
  4. +
  5. 把这2000行数据,一行一行地取到业务端,到hash结构的数据表中寻找匹配的数据。满足匹配的条件的这行数据,就作为结果集的一行
  6. +
+

为什么临时表可以重名

+

内存表和临时表的区别:

+
    +
  • 内存表,指的是使用Memory引擎的表,建表语法是create table … engine=memory。这种表的数据都保存在内存里,系统重启的时候会被清空,但是表结构还在。除了这两个特性看上去比较“奇怪”外,从其他的特征上看,它就是一个正常的表
  • +
  • 临时表,可以使用各种引擎类型 。如果是使用InnoDB引擎或者MyISAM引擎的临时表,写数据的时候是写到磁盘上的。当然,临时表也可以使用Memory引擎
  • +
+

临时表的特性

+

图 29

+

临时表在使用上有以下几个特点:

+
    +
  1. 建表语法是create temporary table …。
  2. +
  3. 一个临时表只能被创建它的session访问,对其他线程不可见。所以,图中session A创建的临时表t,对于session B就是不可见的。
  4. +
  5. 临时表可以与普通表同名
  6. +
  7. session A内有同名的临时表和普通表的时候,show create语句,以及增删改查语句访问的是临时表
  8. +
  9. show tables命令不显示临时表
  10. +
+

临时表的应用

+

由于不用担心线程之间的重名冲突,临时表经常会被用在复杂查询的优化过程中。其中,分库分表系统的跨库查询就是一个典型的使用场景。

+

一般分库分表的场景,就是要把一个逻辑上的大表分散到不同的数据库实例上。比如。将一个大表ht,按照字段f,拆分成1024个分表,然后分布到32个数据库实例上。如下图所示:

+

图 30

+

分区key的选择是以“减少跨库和跨表查询”为依据的。如果大部分的语句都会包含f的等值条件,那么就要用f做分区键

+

比如:

+
select v from ht where f=N;
+
+

可以通过分表规则(比如,N%1024)来确认需要的数据被放在了哪个分表上

+

但是,如果这个表上还有另外一个索引k,并且查询语句是这样的:

+
select v from ht where k >= M order by t_modified desc limit 100;
+
+

由于查询条件里面没有用到分区字段f,只能到所有的分区中去查找满足条件的所有行,然后统一做order by 的操作。这种情况有两种思路:

+
    +
  • 在proxy层的进程代码中实现排序。优势是快,缺点是工作量大,proxy端压力大
  • +
  • 把分库数据汇总到一个表中,再在汇总上操作。如下图所示
  • +
+

图 31

+

为什么临时表可以重名?

+
create temporary table temp_t(id int primary key)engine=innodb;
+
+

执行该语句,MySQL会创建一个frm文件保存表结构定义。该文件放在临时文件目录下,文件名的后缀是.frm,前缀是“#sql{进程id}_{线程id}_序列号”

+

图 32

+

除了文件名不同,内存里面也有一套机制区别不同的表,每个表都对应一个table_def_key

+
    +
  • 一个普通表的table_def_key的值是由“库名+表名”得到的,所以如果你要在同一个库下创建两个同名的普通表,创建第二个表的过程中就会发现table_def_key已经存在了。
  • +
  • 而对于临时表,table_def_key在“库名+表名”基础上,又加入了“server_id+thread_id”
  • +
+

临时表和主备复制

+

如果当前的binlog_format=row,那么跟临时表有关的语句,就不会记录到binlog里

+

如果binlog_format=statment/mixed,创建临时表的语句会传到备库,由备库的同步线程执行。因为主库的线程退出时会自动删除临时表,但是备库同步线程是持续运行的,所以还需要在主库上再写一个DROP TEMPORARY TABLE传给备库执行

+

主库上不同线程创建同名的临时表是没关系的,但是传到备库怎么处理?

+

图 33

+

MySQL在记录binlog的时候,会把主库执行这个语句的线程id写到binlog中。这样,在备库的应用线程就能够知道执行每个语句的主库线程id,并利用这个线程id来构造临时表的table_def_key:

+
    +
  1. session A的临时表t1,在备库的table_def_key就是:库名+t1+“M的serverid”+“session A的thread_id”;
  2. +
  3. session B的临时表t1,在备库的table_def_key就是 :库名+t1+“M的serverid”+“session B的thread_id”
  4. +
+

为什么会使用内部临时表

+

union 执行流程

+

假设有表t1:

+
create table t1(id int primary key, a int, b int, index(a));
+delimiter ;;
+create procedure idata()
+begin
+  declare i int;
+
+  set i=1;
+  while(i<=1000)do
+    insert into t1 values(i, i, i);
+    set i=i+1;
+  end while;
+end;;
+delimiter ;
+call idata();
+
+

然后执行:

+
(select 1000 as f) union (select id from t1 order by id desc limit 2);
+
+

这个语句的执行流程是这样的:

+
    +
  1. +

    创建一个内存临时表,这个临时表只有一个整型字段f,并且f是主键字段

    +
  2. +
  3. +

    执行第一个子查询,得到1000这个值,并存入临时表中

    +
  4. +
  5. +

    执行第二个子查询:

    +
      +
    • 拿到第一行id=1000,试图插入临时表中。但由于1000这个值已经存在于临时表了,违反了唯一性约束,所以插入失败,然后继续执行;
    • +
    • 取到第二行id=999,插入临时表成功
    • +
    +
  6. +
  7. +

    从临时表中按行取出数据,返回结果,并删除临时表,结果中包含两行数据分别是1000和999

    +
  8. +
+

如果使用union all,就没有去重,执行的时候是依次执行子查询,得到的结果直接作为结果集的一部分,不需要临时表

+

group by 执行流程

+
select id%10 as m, count(*) as c from t1 group by m;
+
+

这个语句的执行流程如下:

+
    +
  1. +

    创建内存临时表,表里有两个字段m和c,主键是m

    +
  2. +
  3. +

    扫描表t1的索引a,依次取出叶子节点上的id值,计算id%10的结果,记为x;

    +
      +
    • 如果临时表中没有主键为x的行,就插入一个记录(x,1);
    • +
    • 如果表中有主键为x的行,就将x这一行的c值加1
    • +
    +
  4. +
  5. +

    遍历完成后,再根据字段m做排序,得到结果集返回给客户端

    +
  6. +
+

如果不需要排序,在语句末尾加上order by null

+

当内存临时表大小达到上限时,会转成磁盘临时表,磁盘临时表默认使用的引擎是InnoDB

+

group by 优化方法 --索引

+

新增一列,给这列加索引

+
alter table t1 add column z int generated always as(id % 100), add index(z);
+
+

对这列group by:

+
select z, count(*) as c from t1 group by z;
+
+

group by 优化方法 --直接排序

+

碰到不能加索引的场景就得老老实实做排序

+

在group by语句中加入SQL_BIG_RESULT这个提示(hint),就可以告诉优化器:这个语句涉及的数据量很大,请直接用磁盘临时表

+
select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;
+
+

这个语句的执行流程如下:

+
    +
  1. 初始化sort_buffer,确定放入一个整型字段,记为m
  2. +
  3. 扫描表t1的索引a,依次取出里面的id值, 将 id%100的值存入sort_buffer中
  4. +
  5. 扫描完成后,对sort_buffer的字段m做排序(如果sort_buffer内存不够用,就会利用磁盘临时文件辅助排序)
  6. +
  7. 排序完成后,就得到了一个有序数组。顺序扫描一遍就可以得到结果
  8. +
+

基于上面的union、union all和group by语句的执行过程的分析,我们来回答文章开头的问题:MySQL什么时候会使用内部临时表?

+
    +
  1. 如果语句执行过程可以一边读数据,一边直接得到结果,是不需要额外内存的,否则就需要额外的内存,来保存中间结果
  2. +
  3. join_buffer是无序数组,sort_buffer是有序数组,临时表是二维表结构
  4. +
  5. 如果执行逻辑需要用到二维表特性,就会优先考虑使用临时表。比如我们的例子中,union需要用到唯一索引约束, group by还需要用到另外一个字段来存累积计数。
  6. +
+

都说InnoDB好,那还要不要使用Memory引擎

+

内存表的数据组织结构

+

假设有两张表t1,t2,t1使用Memory引擎,t2使用InnoDB引擎

+
create table t1(id int primary key, c int) engine=Memory;
+create table t2(id int primary key, c int) engine=innodb;
+insert into t1 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
+insert into t2 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
+
+

然后,分别执行select *from t1select* from t2。t2表的(0,0)出现在第一行,t1表出现在最后一行

+

这是因为InnoDB引擎的数据就存在主键索引上,而主键索引是有序存储的,在执行select *的时候,就会按照叶子节点从左到右扫描,所以得到的结果里,0就出现在第一行

+

而Memory引擎的数据和索引是分开的。主键索引存的是每个数据的位置。执行select *走的是全表扫描数据数组
+图 34

+

InnoDB和Memory引擎的数据组织方式是不同的:

+
    +
  • InnoDB引擎把数据放在主键索引上,其他索引上保存的是主键id。这种方式,我们称之为索引组织表
  • +
  • Memory引擎采用的是把数据单独存放,索引上保存数据位置的数据组织形式,我们称之为堆组织表
  • +
+

两个引擎的一些典型不同:

+
    +
  1. InnoDB表的数据总是有序存放的,而内存表的数据就是按照写入顺序存放的
  2. +
  3. 当数据文件有空洞的时候,InnoDB表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就可以插入新值;
  4. +
  5. 数据位置发生变化的时候,InnoDB表只需要修改主键索引,而内存表需要修改所有索引
  6. +
  7. InnoDB表用主键索引查询时需要走一次索引查找,用普通索引查询的时候,需要走两次索引查找。而内存表没有这个区别,所有索引的“地位”都是相同的
  8. +
  9. InnoDB支持变长数据类型,不同记录的长度可能不同;内存表不支持Blob 和 Text字段,并且即使定义了varchar(N),实际也当作char(N),也就是固定长度字符串来存储,因此内存表的每行数据长度相同。
  10. +
+

hash索引和B-Tree索引

+

内存表的范围查询不能走主键索引,但是可以加一个B-Tree索引,B-Tree索引类似于InnoDB的B+树索引

+
alter table t1 add index a_btree_index using btree (id);
+
+

图 35
+图 36

+

不建议在生产环境使用内存表,原因有两方面:

+
    +
  1. 锁粒度问题。内存表不支持行锁,只支持表锁
  2. +
  3. 数据持久化问题
  4. +
+

自增主键为什么不是连续的

+

自增主键可以让主键索引尽量保持递增顺序插入,避免页分裂,因此索引更紧凑,但自增主键不能保证连续递增

+

自增值保存在哪?

+

InnoDB的自增值保存在内存中。每次重启MySQL都会计算max(id)+1作为自增值。8.0版本,重启的时候依靠redo log恢复自增值

+

自增值修改机制

+

假设,某次插入的值是X,当前的自增值是Y

+
    +
  1. 如果X < Y,那么自增值不变
  2. +
  3. 如果X >= Y,将当前自增值修改为新的自增值 Z = auto_increment_offset+k*auto_increment_increment。Z > X,auto_increment_offset是自增初始值,auto_increment_increment是自增步长,k是自然数
  4. +
+

自增值的修改时机

+

自增值在真正执行插入数据的操作之前修改。如果因为唯一键冲突导致插入失败会出现id不连续,事务回滚也是类似现象

+

自增锁的优化

+

自增id锁并不是一个事务锁,而是每次申请完就马上释放,以便允许别的事务再申请。建议innodb_autoinc_lock_mode设置成2,即前面的策略,同时binlog_format=row,避免insert … select造成主备数据不一致

+

insert语句的锁为什么这么多

+

insert … select 语句

+

在可重复读隔离级别下,binlog_format=statement时,执行 insert … select 语句会对select表的需要访问的资源加锁。加锁是为了避免主备不一致

+

insert 循环写入

+

如果把select表的结果insert到select表中,会对select表全表扫描,创建一个临时表,再将select结果insert回表。这么做的原因是:这类一边遍历数据,一边更新数据的情况,如果读出来的数据直接写回原表,就可能在遍历过程中,读到刚刚插入的记录,新插入的记录如果参与计算逻辑,就跟语义不符

+

优化方法是:手动创建内存临时表,先 insert临时表select目标表,再 insert目标表select临时表,这样就不会对目标表全表扫描

+

insert 唯一键冲突

+

图 37

+

在session A执行rollback语句回滚的时候,session C几乎同时发现死锁并返回

+

这个死锁产生的逻辑是这样的:

+
    +
  1. 在T1时刻,启动session A,并执行insert语句,此时在索引c的c=5上加了记录锁。注意,这个索引是唯一索引,因此退化为记录锁
  2. +
  3. 在T2时刻,session B要执行相同的insert语句,发现了唯一键冲突,加上读锁;同样地,session C也在索引c上,c=5这一个记录上,加了读锁(共享next-key lock)
  4. +
  5. T3时刻,session A回滚。这时候,session B和session C都试图继续执行插入操作,都要加上写锁(排它next-key lock)。两个session都要等待对方的行锁,所以就出现了死锁
  6. +
+

图 38

+

insert into … on duplicate key update

+

这个语义的逻辑是,插入一行数据,如果碰到唯一键约束,就执行后面的更新语句。它给唯一索引加排它的next-key lock(写锁)

+

怎么最快地复制一张表

+

如果可以控制对原表的扫描行数和加锁范围很小的话,可以直接用insert … select。否则先将数据写到外部文件,再写回目标表,方法有三种:

+
    +
  1. +

    物理拷贝的方式速度最快,尤其对于大表拷贝来说是最快的方法。如果出现误删表的情况,用备份恢复出误删之前的临时库,然后再把临时库中的表拷贝到生产库上,是恢复数据最快的方法。但是,这种方法的使用也有一定的局限性:

    +
      +
    • 必须是全表拷贝,不能只拷贝部分数据;
    • +
    • 需要到服务器上拷贝数据,在用户无法登录数据库主机的场景下无法使用;
    • +
    • 由于是通过拷贝物理文件实现的,源表和目标表都是使用InnoDB引擎时才能使用。
    • +
    +
  2. +
  3. +

    用mysqldump生成包含INSERT语句文件的方法,可以在where参数增加过滤条件,来实现只导出部分数据。这个方式的不足之一是,不能使用join这种比较复杂的where条件写法

    +
  4. +
  5. +

    用select … into outfile的方法是最灵活的,支持所有的SQL写法。但,这个方法的缺点之一就是,每次只能导出一张表的数据,而且表结构也需要另外的语句单独备份

    +
  6. +
+

grant之后要跟着flushprivileges吗

+

grant语句会同时修改数据表和内存,判断权限的时候使用的是内存数据。因此,规范地使用grant和revoke语句,是不需要随后加上flush privileges语句的。

+

flush privileges语句本身会用数据表的数据重建一份内存权限数据,所以在权限数据可能存在不一致的情况下再使用。而这种不一致往往是由于直接用DML语句操作系统权限表导致的,所以我们尽量不要使用这类语句。

+

要不要使用分区表

+

相对于用户分表:

+

优势:对业务透明,使用分区表的业务代码更简洁,且可以很方便的清理历史数据
+劣势:第一次访问的时候需要访问所有分区;共用MDL锁

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2023/03/03/Excel\346\257\224\345\257\271\344\270\216\345\220\210\345\271\266\347\263\273\347\273\237/index.html" "b/2023/03/03/Excel\346\257\224\345\257\271\344\270\216\345\220\210\345\271\266\347\263\273\347\273\237/index.html" new file mode 100644 index 0000000..ce63fe5 --- /dev/null +++ "b/2023/03/03/Excel\346\257\224\345\257\271\344\270\216\345\220\210\345\271\266\347\263\273\347\273\237/index.html" @@ -0,0 +1,754 @@ + + + + + + + + + + + + + + + + + + + + Excel比对与合并系统 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ Excel比对与合并系统 +

+ + + + +
+
+ +

背景

+

许多游戏策划使用Excel来配置数值。策划需要保存所有版本的数值表,有时需要查看两个数值表有何差异,有时想把差异或者叫修改应用到另一张数值表中,这非常类似于版本控制,但是市面上的版本控制系统svn和git都是针对文本文件,不能用于Excel的版本控制

+

Excel比对算法

+

一维数据比对算法

+

假设有两个序列A1...AmA_1...A_mB1...BnB_1...B_n,我们可以通过对A序列进行一些列操作,使之变为B序列。对每种操作都定义个Cost,如何找到总Cost最小的使A变为B的操作序列,可以通过动态规划解决。这是一个已经被广为研究的算法问题,本文就不再整述,读者可以在网上搜索Edit编辑距离获取更多信息。

+

操作集合的定义有多种方式,一种较为常见的操作集合定义如下(Cost均为1) :

+
    +
  • 在序列中插入一个元素:
  • +
  • 在序列中删除一个元素;
  • +
+

比如,将字符串kiten变换为sitting,需要删除k,插入s,删除e,插入i,在尾部插入g。如果在原序列和目标序列中去掉删除和插入的元素,那么原序列和目标序列就是完全相同的了(比如上面的例子两者都变为itn了),因此这种编辑距离被命名为LCS (Longest Common Subsequence) 编辑距离。LeetCode 1143. 最长公共子序列

+

再回到本文要讨论的差异比较问题,要比较两个序列的差异,实际上就是要找到二者之间尽量多的公共部分,剩下的就是差异部分,所以这和最短编辑距离问题是完全等价的。

+

此外,除了LCS编辑距离之外,还有一种常用的编辑距离,允许插入、删除和修改操作,叫做Levenshtein编组距离。另外,还可以定义一种广义的Levenshtein编辑距离,删除元素AiA_i和插入元素BjB_j;的Cost由一个单参数函数决定,记为Cost(AiA_i)或Cost(BjB_j); 将AiA_i修改为BjB_j;的操作的Cost由一个双参数函数决定,记为Cost2(Ai,BjA_i, B_j)。

+
/**
+     * 比对的基本单位是单个字符
+     * @param text1 字符串1
+     * @param text2 字符串2
+     * @return levenshteinDP数组
+     */
+    static int[][] levenshteinDP(String text1, String text2) {
+        int len1 = text1.length();
+        int len2 = text2.length();
+        // dp[i][j]表示从text1[0...i-1]到text2[0...j-1]的最小编辑距离(cost)
+        dp = new int[len1 + 1][len2 + 1];
+        // path记录此方格的来源是多个此类枚举值的布尔或值
+        path = new int[len1 + 1][len2 + 1];
+
+        for (int i = 0; i < len1 + 1; i++) {
+            dp[i][0] = i;
+            path[i][0] = FROM_INIT;
+        }
+        for (int j = 0; j < len2 + 1; j++) {
+            dp[0][j] = j;
+            path[0][j] = FROM_INIT;
+        }
+
+        for (int i = 1; i < len1 + 1; i++) {
+            for (int j = 1; j < len2 + 1; j++) {
+                path[i][j] = FROM_INIT;
+                int left = dp[i][j - 1] + 1;
+                int up = dp[i - 1][j] + 1;
+                int leftUp;
+                boolean replace;
+
+                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
+                    leftUp = dp[i - 1][j - 1];
+                    replace = false;
+                } else {
+                    leftUp = dp[i - 1][j - 1] + 1;
+                    replace = true;
+                }
+
+                dp[i][j] = Math.min(Math.min(left, up), leftUp);
+
+                if (dp[i][j] == left) {
+                    path[i][j] |= FROM_LEFT;
+                }
+                if (dp[i][j] == up) {
+                    path[i][j] |= FROM_UP;
+                }
+                // 对应:两字符完全一样或者可以修改成一样
+                if (dp[i][j] == leftUp) {
+                    if (replace) {
+                        path[i][j] |= FROM_LEFT_UP_REPLACE;
+                    } else {
+                        path[i][j] |= FROM_LEFT_UP_COPY;
+                    }
+                }
+            }
+        }
+        return dp;
+    }
+
+

同样的,对于广义Levenshtein编辑距离,如果去掉删除和插入的元素,那么两个序列中剩下的元素即为一一对应的关系,每组对应的两个元素,要么是完全相同的,要么前者是被后者修改掉的。从这部分论述中我们不难看出,比较算法的核心思路实际上就是找到元素与元素之间的一一对应关系

+

二维数据比对算法

+

Excel的一个分页可以认为是一个二维数据阵列,阵列中的每个元素是对应单元格内容的字符串值。根据前面的论述,比较两个二维阵列的核心就是找到他们公共的行/列,或者说原阵列和目标阵列的行/列对应关系。比如,对于下面两张表:
+图 2

+

甲表的第1、2、3列对应乙表的1、2、4列,甲表的1、3行对应乙表的1、2行。那么这两张表的差异通过下列方式描述:

+
    +
  • 在第2列的位置插入新列
  • +
  • 删除第2行
  • +
  • 将(原表中) 第3行第3列的元素从9修改为0;
  • +
+

如何计算两张表对应的行/列,一个比较容易想到的方案是将其拆分为两个独立求解的问题,计算对应的行和计算对应的列。对于前者,我们可以把阵列的每一行当成一个元素,所有行组成一个序列,然后对这个序列进行比较:后者亦然。这样我们就把二维比较问题转化成了一维比较的问题。关于编辑距离的定义,可以采用广义Levenshtein编辑距离,定义删除、插入元素的Cost为该行(列)的元素数,定义修改元素的Cost为这两行(列)之间的LCS编辑距离.于是两个二维阵列的比较过程如下:
+找到二者对应的 (或者叫公共的) 行/列,非公共的行/列记为删除、插入行/列操作;两张表只保留公共的行/列,此时他们尺寸完全相同,对应位置的单元格逐一比较,如果值不相同,则记为单元格修改操作;
+图 3

+

算法优化

+

上一个部分介绍的二维阵列比较方案只是一个理论上可行的方案,在实际应用中,存在以下问题:

+
    +
  • 删除、插入行/列的操作都是对于整个行/列的,而计算两行/列之间的LCS编辑距离是独立计算的,因此算法本身有一定不合理性;
  • +
  • 计算修改Cost里又包含了LCS编辑距离的计算,二层嵌套,性能开销比较大;
  • +
+

针对上述问题,从游戏开发的实际应用场景出发,做了如下优化:
+首先计算列之间的对应关系,只取表头前h行(不同项目表头行数h可能不同,可以通过参数配置) ,这样就把对整列的LCS编辑距离计算优化为h个单元格逐已比较,大幅优化了效率,而且对准确度基本不会有什么影响;

+

根据上一步的计算结果,去掉非公共的列(即删除、添加的列),这样,剩下的列都是两边一一对应的了,此时再计算行的对应关系,修改操作的Cost定义就可以从LCS编辑距离改为单元格的逐一比较了,这样又大幅优化了性能

+

在上面所述基础之上,还可以再做优化,因为在实际应用中,绝大多数情况下,绝大多数行都不会有修改,因此可以先用LCS编辑距离对所有行的对应关系进行计算,即只有当两行内容完全相同时才会被当做是对应的; 然后再把剩下的未匹配的行分组用广义Levenshtein编辑距离进行对应关系匹配。这样么做的原因是LCS编辑距离比广义Levenshtein编辑距离的求解速度要快很多。

+

图 6

+

功能扩展

+

在开发过程中,我们经常会将单行或者连续若干行单元格上移或下移到另一位置,按照目前的比较逻辑,该操作会被认为是删除这些行,然后在新的位置重新插入这些行。这样的结果和不合理的。为此,我们可以引入一种新的操作: 移动行到另一位置。加入了这个新的操作之后,我们依然可以建立之前所述的行对应关系,只不过两边行的顺序可以是乱序的。这种不保序对应关系可以通过多个轮次的编辑距离匹配计算,每次匹配之后去掉匹配上的行,剩下未匹配的行组成一个新的序列进行下一轮的匹配。每轮匹配是采用LCS编辑距离还是广义Levenshtein编指距离可以灵活决定,比如前若干轮或者行数较多时采用LCS编辑距离,后面的轮次再用广义Levenshtein编辑距离。

+

图 7

+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/2023/06/10/\346\267\261\345\205\245\347\220\206\350\247\243Java\350\231\232\346\213\237\346\234\272\345\255\246\344\271\240\347\254\224\350\256\260/index.html" "b/2023/06/10/\346\267\261\345\205\245\347\220\206\350\247\243Java\350\231\232\346\213\237\346\234\272\345\255\246\344\271\240\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000..44d1d0c --- /dev/null +++ "b/2023/06/10/\346\267\261\345\205\245\347\220\206\350\247\243Java\350\231\232\346\213\237\346\234\272\345\255\246\344\271\240\347\254\224\350\256\260/index.html" @@ -0,0 +1,1136 @@ + + + + + + + + + + + + + + + + + + + + 深入理解Java虚拟机学习笔记 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 深入理解Java虚拟机学习笔记 +

+ + + + +
+
+ +

第一部分 走近Java

+

1. 走近Java

+

1.1 概述

+

Java不仅仅是一门编程语言,还是一个由一系列计算机软件和规范组成的技术体系

+

Java优点:1. 一次编写,到处运行;2. 避免了绝大部分内存泄露和指针越界问题;3. 实现了热点代码检测和运行时编译及优化,越运行性能越好;4. 完善的类库

+

1.2 Java技术体系

+

广义上,Kotlin等运行在JVM上的编程语言都属于Java技术体系
+传统上,JCP定义的Java技术体系包含:1. Java程序设计语言;2. 各种硬件平台上的Java虚拟机实现;3. Class文件格式;4 Java类库API;5. 来自商业机构和开源社区的第三方Java类库

+

JDK:Java程序设计语言+Java虚拟机+Java类库
+JRE:Java类库API中的Java SE API子集和Java虚拟机
+图 1

+

1.3 Java发展史

+

图 2

+

1.4 Java虚拟机家族

+
    +
  • Sun Classic/Exact VM。Sun Classic是世界上第一款商用Java虚拟机,纯解释执行代码,如果外挂即时编译器会完全接管执行,两者不能混合工作。Exact VM解决了许多问题但是碰上了引进的HotSpot,生命周期很短
  • +
  • HotSpot VM:使用最广泛的Java虚拟机。HotSpot继承了前者的优点,也有许多新特性,例如热点代码探测技术。JDK 8中的HotSpot融合了BEA JRockit优秀特性
  • +
  • Mobile/Embedded VM:针对移动和嵌入式市场的虚拟机
  • +
  • BEA JRockit/IBM J9 VM:JRockit专注服务端应用,不关注程序启动速度,全靠编译器编译后执行。J9定位类似HotSpot
  • +
  • BEA Liquid VM/Azul VM:和专用硬件绑定,更高性能的虚拟机
  • +
  • Apache Harmony/Google Android Dalvik VM:Harmony被吸收进IBM的JDK 7以及Android SDK,Dalvik被ART虚拟机取代
  • +
  • Microsoft JVM:Windows系统下性能最好的Java虚拟机,因侵权被抹去
  • +
+

1.5 展望Java技术的未来

+
    +
  • 无语言倾向:Graal VM可以作为“任何语言”的运行平台
  • +
  • 新一代即时编译器:Graal编译器,取代C2(HotSpot中编译耗时长但代码优化质量高的即时编译器)
  • +
  • 向Native迈进:Substrate VM提前编译代码,显著降低内存和启动时间
  • +
  • 灵活的胖子:经过一系列重构与开放,提高开放性和扩展性
  • +
  • 语言语法持续增强:增加语法糖和语言功能
  • +
+

第二部分 自动内存管理

+

2. Java内存区域与内存溢出异常

+

2.1 概述

+

C、C++程序员需要自己管理内存,Java程序员在虚拟机自动内存管理机制下不需要为每一个new操作写对应的delete/free代码,但是一旦出现内存泄露和溢出,不了解虚拟机就很难排错

+

2.2 运行时数据区域

+

图 3

+
    +
  • 程序计数器:当前线程执行的下一条指令的地址;线程私有;不会OOM
  • +
  • 虚拟机栈:Java方法执行的线程内存模型,每个方法执行时,JVM都会创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息;线程私有;会栈溢出和OOM
  • +
  • 局部变量表:存放的变量的类型有8种基本数据类型、对象引用和returnAddress类型(指向字节码指令的地址),这些变量除了64位的long和double占两个变量槽,其它占1个。局部变量表的大小在编译器确定
  • +
  • 本地方法栈:和虚拟机栈作用类似,区别在于只是为本地(Native)方法服务,HotSpot将两者合二为一
  • +
  • 堆:“几乎”所有对象实例都在此分配内存;可分代,也不分代;逻辑上连续,物理上可以不连续;会OOM
  • +
  • 方法区:也叫“非堆”,存储已加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据;实现上,之前HotSpot使用永久代实现方法区,目的是方便垃圾回收,但是这样有OOM问题,JDK8废弃永久代改为使用本地内存的元空间,主要存放类型信息,其它移到堆;内存回收目标主要是常量池和类型信息
  • +
  • 运行时常量池:Class文件中的常量池表存放编译期生成的各种字面量和符号引用,这部分内容在类加载后放到方法区的运行时常量池;具有动态性,运行时产生的常量也可以放入运行时常量池,String类的intern()利用了这个特性;会OOM
  • +
  • 直接内存:不是运行时数据区的一部分,也不是Java虚拟机规范里定义的内存区域;NIO使用Native函数库直接分配堆外内存;会OOM
  • +
+

2.3 HotSpot虚拟机对象探秘

+
2.3.1 对象的创建
+
    +
  1. 当JVM碰到new指令,首先检查指令参数能否在常量池中定位到一个类的符号引用,并且检查该类是否已被加载、解析和初始化,如果没有就进行类加载过程
  2. +
  3. JVM为新生对象分配内存。 +
      +
    • 对象所需内存大小在类加载后可确定,分配方法有两种:当垃圾收集器(Serial、ParNew)能空间压缩整理时,堆是规整的,分配内存就是把指针向空闲空间方向移动对象大小的距离,这种叫“指针碰撞”,使用CMS收集器的堆是不规整的,需要维护空闲列表来分配内存。
    • +
    • 内存分配可能线程不安全,例如线程在给A分配内存,指针来没来得及修改,另一线程创建对象B又同时使用了原来的指针来分配内存。解决方法有两个:1.对分配内存空间的动作进行同步处理,JVM采用CAS+失败重试的方式保证原子性;2.预先给每个线程分配一小块内存,称为本地线程分配缓冲(TLAB),线程分配内存先在TLAB上分配,TLAB用完了再同步分配新的缓存区
    • +
    +
  4. +
  5. JVM将分配到的内存空间(不包括对象头)初始化为零值,这步保证对象的实例字段可以不赋初始值就直接使用
  6. +
  7. JVM对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中
  8. +
  9. 执行构造函数,即Class文件中的<init>()
  10. +
+
2.3.2 对象的内存布局
+
    +
  • +

    对象头

    +
      +
    • 用于存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳,称为“Mark Word”。这部分数据长度32比特或64比特,Bitmap存储,为了在极小空间存储更多数据,不同状态的对象用不同标志位表示不同存储内容
      +图 4
    • +
    • 类型指针,即对象指向它的类型元数据的指针,JVM通过该指针来确定对象是哪个类的实例。若对象是数组,还必须记录数组长度
    • +
    +
  • +
  • +

    实例数据,包括父类继承下来的,和子类中定义的字段

    +
  • +
  • +

    对齐填充,HotSpot要求对象大小必须是8字节的整数倍,不够就对齐填充

    +
  • +
+
2.3.3 对象的访问定位
+

通过栈上的reference数据来访问堆上的具体对象,访问方式由虚拟机实现决定,主流有两种

+
    +
  • 句柄访问。Java堆会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自具体的地址信息。优点是对象被移动时只需要改变句柄中的实例数据指针
    +图 5
  • +
  • 直接指针访问。reference存储的就是对象地址,类型数据指针在对象中。优点是节省一次指针定位的时间开销,HotSpot使用此方式来访问对象
    +图 6
  • +
+

2.4 实现:OutOfMemoryError异常

+
2.4.1 Java堆溢出
+
/**
+ * VM options:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError 
+ * @author panjiahao.cs@foxmail.com
+ * @date 2023/6/18 19:54
+ */
+public class HeapOOM {
+    static class OOMObject{
+
+    }
+    public static void main(String[] args) {
+        ArrayList<OOMObject> list = new ArrayList<>();
+
+        while(true){
+            list.add(new OOMObject());
+        }
+    }
+}
+
+

图 7

+

排查思路:首先确认导致OOM的对象是否是必要的,也就是是分清楚是内存泄露了还是内存溢出了

+

如果是内存泄露了,可以通过工具查看泄露对象到GC Roots的引用链,定位到产生内存泄露的代码的具体位置

+

如果是内存溢出了,可以调大堆空间,优化生命周期过长的对象

+
2.4.2 虚拟机栈和本地方法栈溢出
+

HotSpot不区分虚拟机栈和本地方法栈,所以-Xoss(本地方法栈大小)参数没效果,栈容量由-Xss参数设定。

+

当线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;当扩展栈容量无法申请到足够内存,将抛出OutOfMemoryError异常。HotSpot不支持扩展栈容量

+
2.4.3 方法区和运行时常量池溢出
+

JDK8使用元空间取代了永久代,运行时常量池移动到了堆中,所以可能会产生堆内存的OOM

+

方法区的主要职责是存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。用CGLib不断生成增强类,可能产生元空间的OOM

+
/**
+ * VM Args:-XX:MaxMetaspaceSize=10M
+ * @author panjiahao.cs@foxmail.com
+ * @date 2023/6/18 22:13
+ */
+public class JavaMethodAreaOOM {
+    public static void main(String[] args) {
+        while(true){
+            Enhancer enhancer = new Enhancer();
+            enhancer.setSuperclass(HeapOOM.OOMObject.class);
+            enhancer.setUseCache(false);
+            enhancer.setCallback(new MethodInterceptor() {
+                @Override
+                public Object intercept(Object obj, Method method, Object[] objects, MethodProxy proxy) throws Throwable {
+                    return proxy.invokeSuper(obj,args);
+                }
+            });
+            enhancer.create();
+        }
+    }
+    static class OOMObject{
+
+    }
+}
+
+

图 8

+
2.4.4 本机直接内存溢出
+

直接内存通过-XX:MaxDirectMemorySize参数来指定,默认与Java堆最大值一致

+

虽然使用DirectByteBuffer分配内存会OOM,但它抛出异常时并没有真正向操作系统申请内存,而是通过计算得知无法分配就手动抛异常,真正申请内存的方法是Unsafe::allocateMemory()

+
/**
+ * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
+ * @author panjiahao.cs@foxmail.com
+ * @date 2023/6/19 20:38
+ */
+public class DirectMemoryOOM {
+    private static final int _1MB = 1024 * 1024;
+
+    public static void main(String[] args) throws IllegalAccessException {
+        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
+        unsafeField.setAccessible(true);
+        Unsafe unsafe = (Unsafe) unsafeField.get(null);
+        while (true) {
+            unsafe.allocateMemory(_1MB);
+        }
+    }
+}
+
+

图 9

+

由直接内存导致的内存溢出,一个明显的特征是Heap Dump文件不会看到有什么明显的异常情况。如果内存溢出后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(NIO),可以重点检查直接内存

+

3. 垃圾收集器与内存分配策略

+

3.1 概述

+

线程独占的程序计数器、虚拟机栈、本地方法栈3个区域的内存分配和回收都具备确定性。

+

而Java堆和方法区有显著的不确定性:一个接口的多个实现类需要的内存可能不一样,一个方法不同分支需要的内存也不同,只有处于运行期间,才知道程序会创建哪些对象,这部分内存的分配和回收是动态的

+

3.2 判断对象是否存活的算法

+
    +
  • 引用计数法。看似简单,但必须配合大量额外处理才能正确工作,譬如简单的引用计数法无法解决对象循环引用
  • +
  • 可达性分析算法。从GC Roots对象开始,根据引用关系向下搜索,走过的路径称为“引用链”,引用链上对象仍然存活,不在引用链上的对象可回收。
    +图 10
  • +
+

GC Roots对象包括以下几种:

+
    +
  • 虚拟机栈中引用的对象。例如方法参数、局部变量等
  • +
  • 方法区中类静态属性引用的对象。例如Java类的引用类型静态变量
  • +
  • 方法区中常量引用的对象。例如字符串常量池里的引用
  • +
  • 本地方法栈JNI引用的对象
  • +
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)、还有系统类加载器
  • +
  • 所有被同步锁(synchronized)持有的对象
  • +
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
  • +
  • 根据用户选用的垃圾收集器以及回收区域,临时加入其它对象。目的是当某个区域垃圾回收时,该区域的对象也可能被别的区域的对象引用
  • +
+

引用包含以下几种类型:

+
    +
  • 强引用:被强引用引用的对象不会被回收
  • +
  • 软引用:被软引用引用的对象在OOM前会被回收
  • +
  • 弱引用:被弱引用引用的对象在下一次垃圾回收时被回收
  • +
  • 虚引用:虚引用不会影响对象的生存时间,唯一目的是能在对象被回收时收到一个系统通知
  • +
+

即便对象已经不可达,也不是立即标记为可回收,对象真正死亡要经历两次标记过程:可达性分析发现不可达就第一次标记;如果对象重写了finalize()方法且没过JVM调用过,那么该对象会被放到队列中,由Finalizer线程去执行finalize()方法,这是对象自救的最后一次机会,只要重新与引用链上任意对象建立关联就行,譬如把this赋给某个对象的成员变量,第二次标记时就会被移除“即将回收”集合

+
/**
+ * 此代码演示了两点:
+ * 1.对象可以在被GC时自我拯救。
+ * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
+ * @author panjiahao.cs@foxmail.com
+ * @date 2023/6/20 21:52
+ */
+public class FinalizeEscapeGC {
+    public static FinalizeEscapeGC SAVE_HOOK = null;
+
+    public void isAlive(){
+        System.out.println("yes, i am still alive");
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        super.finalize();
+        System.out.println("finalize method executed! :)");
+        FinalizeEscapeGC.SAVE_HOOK = this;
+    }
+
+    public static void main(String[] args) throws InterruptedException {
+        SAVE_HOOK = new FinalizeEscapeGC();
+
+        SAVE_HOOK = null;
+        System.gc();
+        Thread.sleep(500);
+        if (SAVE_HOOK != null) {
+            SAVE_HOOK.isAlive();
+        } else {
+            System.out.println("no ,i am dead! :(");
+        }
+
+        // 自救失败
+        SAVE_HOOK = null;
+        System.gc();
+        Thread.sleep(500);
+        if (SAVE_HOOK != null) {
+            SAVE_HOOK.isAlive();
+        } else {
+            System.out.println("no ,i am dead! :(");
+        }
+    }
+}
+
+

方法区没有强制要垃圾回收,例如JDK11的ZGC不支持类卸载。

+

方法区主要回收两部分:废弃的常量和不再使用的类型。

+

回收废弃常量和回收堆中的对象非常类似
+回收“不再使用的类”需要满足三个条件:

+
    +
  • 该类所有实例已被回收,包括子类实例
  • +
  • 加载该类的类加载器已被回收,这个条件很难
  • +
  • 该类对象的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
  • +
+

3.3 垃圾收集算法

+

从如何判定对象消亡的角度出发,GC算法可以划分为“引用计数式”和“追踪式”,这两类也被称作“直接垃圾收集”和“间接垃圾收集”。本节介绍追踪式垃圾收集

+
3.3.1 分代收集理论
+

分代收集理论建立在两个假说上:

+
    +
  • 弱分代假说:绝大多数对象都是朝生夕灭的
  • +
  • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
  • +
+

分代收集在遇到对象之间存在跨代引用时需要遍历其它代的所有对象来确定是否还存在跨代引用,性能负担大,所以给分代收集理论添加第三个假说:

+
    +
  • 跨代引用假说:跨代引用相对于同代引用来说占极少数
  • +
+

分代收集的名词定义:

+
    +
  • +

    部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

    +
      +
    • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
    • +
    • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS会有单独收集老年代的行为
    • +
    • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1会有这种行为
    • +
    +
  • +
  • +

    整堆收集(Full GC):收集整个Java堆和方法区的垃圾

    +
  • +
+
3.3.2 标记-清除算法
+

标记所有需要回收的对象,标记完成后,统一回收
+图 11

+

缺点:执行效率不稳定;内存空间碎片化

+
3.3.3 标记-复制算法
+

将内存划分为大小相等的两块,每次只使用一块。当这一块的内存用完了,就将还存活着的对象复制到另一外上面,然后再把已使用过的内存空间一次性清理掉

+

图 12

+

优点:对于多数对象都是可回收的情况,算法复制开销小;没有碎片
+缺点:可用内存小了一半;需要空间担保

+

1:1划分新生代太浪费空间,HotSpot将新生代划分成Eden:Survivor0:Survivor0 = 8:1:1,每次可以用Eden和一块Survivor,垃圾回收时把存活对象写到另一块Survivor,然后清理掉Eden和已用过的Survivor,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖老年代进行分配担保

+
3.3.4 标记-整理算法
+

标记所有存活的对象,然后移动到内存空间一端,清理掉边界以外的内存
+图 13

+

优点:解决了标记-清除算法造成的空间碎片化问题
+缺点:整理期间,用户应用程序暂停,这段时间被称为“Stop The World”

+

整理即移动对象,移动则内存回收时会更复杂,不移动则内存分配会更复杂。从GC停顿时间来看,不移动对象停顿时间短;从吞吐量来看,移动对象更划算。

+

HotSpot里关注吞吐量的Parallel Scavenge收集器采用标记-整理算法,关注延迟的CMS收集器采用标记-清除算法

+

3.4 HotSpot的算法细节实现

+
3.4.1 根节点枚举
+

固定作为GC Root的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表),Java程序越来越庞大,逐个作为起点进行可达性分析会消耗大量时间

+

目前,所有收集器在根节点枚举时和整理内存碎片一样必须暂停用户线程,可达性分析算法可以和用户线程一起并发。为了降低STW时间,在不扫描全部的GC Root节点情况下,得知哪些地方存在对象引用,HotSpot提供了OopMap的数据结构保存引用的位置信息

+
3.4.2 安全点
+

OopMap可以帮助HotSpot快速完成GC Root枚举,但是如果为每条改变OomMap内容的指令都生成对应的OopMap,会需要大量额外存储空间

+

因此HotSpot没有为每条指令都生成OopMap,只在特定位置生成,这些位置称为安全点。用户程序只有在执行到安全点才能停顿下来垃圾回收。

+

安全点的选取考虑:不能太少以至于让收集器等待时间太长,也不能太频繁以至于增大内存负担

+

如何在垃圾回收时让所有线程(不包括执行JNI调用的线程)都跑到最近的安全点,然后停顿下来。有两种方案:

+
    +
  • 抢先式中断:先把用户线程全部中断,如果发现有用户线程不在安全点上就恢复这个线程,过一会再中断直至它跑到安全点。现在几乎不使用
  • +
  • 主动式中断:设置一个标志位,各个线程执行时主动轮询这个标志,一旦为真主动中断挂起。HotSpot使用内存保护陷阱的方式将轮询操作精简至只有一条汇编指令
  • +
+
3.4.3 安全区域
+

当用户线程处于Sleep或者Blocked状态时不能执行到安全点。针对这种情况,引入安全区域。

+

安全区域是指在某一段代码片段中,引用关系不会发生变化,在这个区域中任意地方开始GC都是安全的。

+

当用户线程执行到安全区域时,首先标识自己进入了安全区域,这样在GC时,虚拟机就不会去管这些已标识自己进入安全区域的线程。当线程离开安全区域时,它检查虚拟机是否完成了根节点枚举(或者其它需要暂停用户线程的阶段),如果完成了就继续执行,否则等待直到收到可以离开安全区域的信号为止。

+
3.4.4 记忆集与卡表
+

记忆集是一种记录从非收集区域指向收集区域的指针集合的抽象数据结构,用于解决对象跨代引用带来的问题

+

记忆集最简单的实现是非收集区域中所有含跨代引用的对象数组,这种结构浪费太多空间,可以粗化记录粒度:

+
    +
  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如32或64),该字包含跨代指针
  • +
  • 卡精度:每个记录精确到一块内存区域,该区域有对象含有跨代指针
  • +
+

卡精度使用卡表实现,卡表的实现是一个字节数组,字节数组每个元素都对应着一块特定大小的内存块,称为卡页。只要卡页内有一个或更多对象的字段存在跨代引用,卡表中对应的数组元素的值标识为1,称为变脏。垃圾收集时,只要筛选出卡表中变脏的元素就可以知道哪些卡页内存块有跨代引用,把它们放入GC Roots中一起扫描

+
3.4.5 写屏障
+

卡表元素变脏的时间点是引用类型字段赋值那一刻,HotSpot通过写屏障来维护卡表状态。写屏障可以看作JVM层面对“引用类型字段赋值”这个动作的AOP切面。

+

写屏障会导致伪共享问题,伪共享是指,现代CPU的缓存系统是以缓存行为单位存储的,当多线程修改相互独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低

+

伪共享的一种简单解决方法是不采用无条件的写屏障,而是先检查卡表标记,只有卡表元素未被标记时才将其变脏。HotSpot参数-XX:+UseCondCardMark决定是否开启卡表更新,开启会多一次判断开销,但能够避免伪共享带来的性能损耗

+
3.4.6 并发的可达性分析
+

可达性分析在标记阶段会暂停用户线程以在一致性的快照上进行对象图的遍历,不一致情况下会出现“对象消失”问题。原因可以由三色标记方法推导

+

图 14

+

白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚开始时,所有对象都是白的,若在分析结束阶段,仍然是白色的对象是不可达
+黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色对象代表已经扫描过,是安全存活的,如果有其它对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象
+灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过

+

当且仅当以下两个条件同时满足时,会产生“对象消失”问题,即原本应该黑色的对象被误标为白色:

+
    +
  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用
  • +
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
  • +
+

解决并发扫描时的对象消失问题,只需破坏两个条件之一即可,因此有两种方案:

+
    +
  • 增量更新。增量更新破坏条件一,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束后,再以这个黑色对象为根,重新扫描一次。可以理解为,黑色对象一旦新插入指向白色对象的引用之后,它就变回灰色对象
  • +
  • 原始快照。当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。可以理解为,无论引用关系删除与否,都会按照刚开始扫描那一刻的对象图快照进行搜索
  • +
+

对引用关系记录的插入和删除都是通过写屏障实现。

+

3.5 经典垃圾收集器

+

图 15

+

图中连线表示可以搭配使用

+
3.5.1 Serial收集器
+

图 16

+

适合资源(cpu和内存)受限的场景,新生代一两百兆以内的垃圾收集停顿时间最多一百多毫秒以内

+
3.5.2 ParNew收集器
+

实质是多线程版的Serial

+

图 17

+
3.5.3 Parallel Scavenge收集器
+

与ParNew类似,不同点是其它收集器关注停顿时间,Parallel Scavenge收集器目标是可控制的吞吐量

+

图 18

+

-XX:MaxGCPauseMillis控制最大垃圾收集停顿时间,-XX:GCTimeRatio直接设置吞吐量,-XX:+UseAdaptiveSizePolicy会根据系统运行情况动态调整虚拟机参数以获得最合适的停顿时间或者最大的吞吐量

+
3.5.4 Serial Old收集器
+

Serial收集器的老年代版本

+

图 19

+
3.5.5 Parallel Old收集器
+

Parallel Scavenge的老年代版本,在注重吞吐量或者处理器资源稀缺场合,可以考虑使用Parallel Scavenge+Parallel Old

+

图 20

+
3.5.6 CMS收集器
+

四步骤:
+1)初始标记
+2)并发标记
+3)重新标记
+4)并发清理

+

初始标记和重新标记仍然要暂停用户线程。初始标记仅仅标记GC Roots能直接关联的对象,速度很快;并发标记从关联对象开始遍历整个对象图,不需要暂停用户线程;重新标记用来修正并发标记期间的变动(增量更新)

+

图 21

+

优点:并发收集、低停顿
+缺点:

+
    +
  • 对处理器资源敏感。并发会占用处理器,降低吞吐量
  • +
  • 无法处理“浮动垃圾”。“浮动垃圾”指并发标记和清理期间用户线程产生的垃圾在下一次GC时清理,预留空间不足会,预留空间不足会导致“Concurrent Mode Failure”进而导致Full GC或者Serial Old重新回收老年代。
  • +
  • 内存空间碎片化
  • +
+
3.5.7 Garbage First(G1)收集器
+

开创面向局部收集的设计思路和基于Region的内存布局形式

+

局部收集只收集范围不是新生代或老年代,而是堆中任意部分。

+

基于Region的堆内存布局:G1不再坚持固定大小和数量的分代区域划分,而是把连续的堆划分为多个大小相等的独立区域Region,每个Region都可以根据需要扮演新生代或者老年代。超过Region容量一半的对象会被存到Humongous区域中,超过整个Region容量的对象会被存到N个连续的Humongous Region中,G1大多数行为把Humongous Region当老年代处理
+图 22

+

四个步骤:

+
    +
  • 初始标记:标记GC Roots能直接关联的对象,修改TAMS指针的值,让下一阶段用户并发运行时能正确在可用的Region中分配新对象
  • +
  • 并发标记:递归扫描对象图。扫描完成后重新处理SATB记录下的在并发时有引用变动的对象
  • +
  • 最终标记:处理并发阶段遗留的少量SATB记录
  • +
  • 筛选回收:负责更新Region统计数据,制定回收计划
  • +
+

图 23

+

3.6 低延迟垃圾收集器

+

垃圾收集器“不可能三角”:内存占用、吞吐量、延迟。延迟是最重要指标

+

下图,浅色表示挂起用户线程,深色表示gc线程和用户线程并发

+

图 24

+

Shenandoah和ZGC在可管理的堆容量下,停顿时间基本是固定的,两者被命名为低延迟垃圾收集器

+
3.6.1 Shenandoah收集器
+

目标:能在任何堆内存大小下都可以把GC停顿时间限制在10ms以内

+

Shenandoah和G1有相似的堆内存布局,在初始标记、并发标记等阶段的处理思路高度一致。
+有三个明显的不同:

+
    +
  • 支持并发的整理算法
  • +
  • 默认不分代,即不使用专门的新生代Region和老年代Region
  • +
  • 放弃G1中耗费大量内存和计算资源去维护的记忆集,改为“连接矩阵”记录跨Region的引用关系
  • +
+

图 25

+

收集器的工作分为九个阶段:

+
    +
  • 初始标记:与G1一样,首先标记GC Roots直接关联的对象,停顿时间与堆大小无关,与GC Roots数量有关
  • +
  • 并发标记:与G1一样,遍历对象图,与用户线程并发,时间取决于对象数量和对象图的结构复杂程度
  • +
  • 最终标记:与G1一样,处理剩余的SATB扫描,统计价值最高的Region组成回收集,有一小段停顿时间
  • +
  • 并发清理:清理一个存活对象都没有的Region
  • +
  • 并发回收:将回收集中的存活对象复制到未被使用的Region中,通过读屏障和“Brooks Pointers”转发指针解决和用户线程并发产生的问题
  • +
  • 初始引用更新:一个非常短暂的停顿,用于确保所有并发回收阶段中收集器线程都已完成分配对象移动任务
  • +
  • 并发引用更新:真正开始进行引用更新,与用户线程并发
  • +
  • 最终引用更新:修正存在于GC Roots中的引用,停顿时间与GC Roots的数量有关
  • +
  • 并发清理:经过并发回收和引用更新后,整个回收集的Region已经没有存货对象,回收这些Region的内存空间
  • +
+

图 26

+

对象移动和用户程序并发,原来的解决方案是在被移动对象原有的内存上设置保护陷阱,一旦用户程序访问到原有的地址就产生自陷中断,进入预设的异常处理器,将访问转发到新对象。这个方案会导致用户态频繁切换到核心态

+

Brooks Pointers是在原有对象布局结构的最前面加一个新的引用字段,移动前指向自己,移动后指向新对象。这里需要用CAS解决对象访问的并发问题

+

图 27

+
3.6.2 ZGC收集器
+

ZGC收集器是一款基于Region内存布局的,不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。收集过程全程可并发,短暂停顿时间只与GC Roots大小相关而与堆内存大小无关

+
    +
  1. +

    ZGC的Region是动态的:动态创建和销毁、动态大小

    +

    图 28

    +
  2. +
  3. +

    染色指针:把标记信息记在引用对象的指针上,标记信息有4个bit,虚拟机可以直接从指针中看到其引用对 象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问到

    +

    图 29

    +

    三大优势:

    +
      +
    • 染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用 掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理
    • +
    • 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的 目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作
    • +
    • 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以 便日后进一步提高性能
    • +
    +

    标志位影响了寻址地址,ZGC通过多重映射,将不同标志位的指针映射到同一内存区域

    +

    图 30

    +
  4. +
  5. +

    ZGC的收集过程分为四大阶段

    +

    图 31

    +
      +
    • 并发标记:与G1、Shenandoah类似,区别是标记在指针上而不是对象上,标记会更新染色指针的Marked 0、Marked 1标志位
    • +
    • 并发预备重分配:根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集
    • +
    • 并发重分配:把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系
    • +
    • 并发重映射:修正整个堆中指向重分配集中旧对象的所 有引用
    • +
    +
  6. +
+

3.7 选择合适的垃圾收集器

+
3.7.1 Epsilon收集器
+

不收集垃圾,负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责

+

响应微服务而生,在堆内存耗尽前就退出,不收集垃圾就非常合适

+
3.7.2 收集器的权衡
+

三个因素:

+
    +
  • 应用程序关注点是什么?吞吐量、延时还是内存占用
  • +
  • 基础设施如何?处理器的数量、内存大小、操作系统
  • +
  • JDK的发行商是谁?版本号是多少
  • +
+

例如直接面向用户的B/S系统,延迟是主要关注点。

+
    +
  • 预算充足就用商业的
  • +
  • 预算不足但能掌控基础设施,可以尝试ZGC
  • +
  • 对ZGC的稳定性有疑虑就考虑Shenandoah
  • +
  • 软硬件和JDK都老的考虑CMS
  • +
+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/404.html b/404.html new file mode 100644 index 0000000..0646776 --- /dev/null +++ b/404.html @@ -0,0 +1,574 @@ + + + + + + + + + + + + + + + + + + + + 404 Not Found:该页无法显示 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ +
+ + + +

+ 404 Not Found:该页无法显示 +

+ + + + +
+
+ + + + +
+ +
+ + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/about/index.html b/about/index.html new file mode 100644 index 0000000..49fbb7c --- /dev/null +++ b/about/index.html @@ -0,0 +1,541 @@ + + + + + + + + + + + + + + + + + + + + 关于 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + +
+
+
+

关于

+ +

个人简介

+ +
+
+

个人详细介绍

+ +
+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2019/12/index.html b/archives/2019/12/index.html new file mode 100644 index 0000000..47659d9 --- /dev/null +++ b/archives/2019/12/index.html @@ -0,0 +1,479 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2019/12 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2019/12

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2019/index.html b/archives/2019/index.html new file mode 100644 index 0000000..59771be --- /dev/null +++ b/archives/2019/index.html @@ -0,0 +1,479 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2019 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2019

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2020/01/index.html b/archives/2020/01/index.html new file mode 100644 index 0000000..e911835 --- /dev/null +++ b/archives/2020/01/index.html @@ -0,0 +1,529 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2020/1 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2020/1

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2020/02/index.html b/archives/2020/02/index.html new file mode 100644 index 0000000..5ecd53e --- /dev/null +++ b/archives/2020/02/index.html @@ -0,0 +1,459 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2020/2 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2020/2

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2020/index.html b/archives/2020/index.html new file mode 100644 index 0000000..45dc1d1 --- /dev/null +++ b/archives/2020/index.html @@ -0,0 +1,539 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2020 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2020

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2022/05/index.html b/archives/2022/05/index.html new file mode 100644 index 0000000..29b3d1d --- /dev/null +++ b/archives/2022/05/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2022/5 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2022/5

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2022/06/index.html b/archives/2022/06/index.html new file mode 100644 index 0000000..73d971c --- /dev/null +++ b/archives/2022/06/index.html @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2022/6 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2022/6

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2022/07/index.html b/archives/2022/07/index.html new file mode 100644 index 0000000..762820d --- /dev/null +++ b/archives/2022/07/index.html @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2022/7 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2022/7

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2022/08/index.html b/archives/2022/08/index.html new file mode 100644 index 0000000..f78cf83 --- /dev/null +++ b/archives/2022/08/index.html @@ -0,0 +1,479 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2022/8 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2022/8

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2022/12/index.html b/archives/2022/12/index.html new file mode 100644 index 0000000..a14273e --- /dev/null +++ b/archives/2022/12/index.html @@ -0,0 +1,459 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2022/12 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2022/12

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2022/index.html b/archives/2022/index.html new file mode 100644 index 0000000..3af983a --- /dev/null +++ b/archives/2022/index.html @@ -0,0 +1,575 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2022 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2022

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2022/page/2/index.html b/archives/2022/page/2/index.html new file mode 100644 index 0000000..fdf9a0a --- /dev/null +++ b/archives/2022/page/2/index.html @@ -0,0 +1,565 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2022 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2022

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2023/01/index.html b/archives/2023/01/index.html new file mode 100644 index 0000000..861fad1 --- /dev/null +++ b/archives/2023/01/index.html @@ -0,0 +1,459 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2023/1 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2023/1

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2023/02/index.html b/archives/2023/02/index.html new file mode 100644 index 0000000..036cfa8 --- /dev/null +++ b/archives/2023/02/index.html @@ -0,0 +1,459 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2023/2 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2023/2

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2023/03/index.html b/archives/2023/03/index.html new file mode 100644 index 0000000..8b16358 --- /dev/null +++ b/archives/2023/03/index.html @@ -0,0 +1,459 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2023/3 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2023/3

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2023/06/index.html b/archives/2023/06/index.html new file mode 100644 index 0000000..710cb1c --- /dev/null +++ b/archives/2023/06/index.html @@ -0,0 +1,459 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2023/6 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2023/6

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/2023/index.html b/archives/2023/index.html new file mode 100644 index 0000000..10c30a9 --- /dev/null +++ b/archives/2023/index.html @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + 归档: 2023 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档: 2023

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/index.html b/archives/index.html new file mode 100644 index 0000000..dfa4bc3 --- /dev/null +++ b/archives/index.html @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + 归档 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/page/2/index.html b/archives/page/2/index.html new file mode 100644 index 0000000..8370e59 --- /dev/null +++ b/archives/page/2/index.html @@ -0,0 +1,575 @@ + + + + + + + + + + + + + + + + + + + + 归档 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/page/3/index.html b/archives/page/3/index.html new file mode 100644 index 0000000..9ce5e9a --- /dev/null +++ b/archives/page/3/index.html @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + 归档 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archives/page/4/index.html b/archives/page/4/index.html new file mode 100644 index 0000000..4eb7916 --- /dev/null +++ b/archives/page/4/index.html @@ -0,0 +1,543 @@ + + + + + + + + + + + + + + + + + + + + 归档 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+
+

归档

+

共 35 篇文章

+
+
+ + + + + + + +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/atom.xml b/atom.xml new file mode 100644 index 0000000..c7a82fb --- /dev/null +++ b/atom.xml @@ -0,0 +1,578 @@ + + + panzun blog + + + + + + 2023-09-24T04:27:40.285Z + https://zunpan.github.io/ + + + panzun + + + + Hexo + + + 深入理解Java虚拟机学习笔记 + + https://zunpan.github.io/2023/06/10/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ + 2023-06-10T15:30:14.000Z + 2023-09-24T04:27:40.285Z + + 第一部分 走近Java

1. 走近Java

1.1 概述

Java不仅仅是一门编程语言,还是一个由一系列计算机软件和规范组成的技术体系

Java优点:1. 一次编写,到处运行;2. 避免了绝大部分内存泄露和指针越界问题;3. 实现了热点代码检测和运行时编译及优化,越运行性能越好;4. 完善的类库

1.2 Java技术体系

广义上,Kotlin等运行在JVM上的编程语言都属于Java技术体系
传统上,JCP定义的Java技术体系包含:1. Java程序设计语言;2. 各种硬件平台上的Java虚拟机实现;3. Class文件格式;4 Java类库API;5. 来自商业机构和开源社区的第三方Java类库

JDK:Java程序设计语言+Java虚拟机+Java类库
JRE:Java类库API中的Java SE API子集和Java虚拟机
图 1

1.3 Java发展史

图 2

1.4 Java虚拟机家族

  • Sun Classic/Exact VM。Sun Classic是世界上第一款商用Java虚拟机,纯解释执行代码,如果外挂即时编译器会完全接管执行,两者不能混合工作。Exact VM解决了许多问题但是碰上了引进的HotSpot,生命周期很短
  • HotSpot VM:使用最广泛的Java虚拟机。HotSpot继承了前者的优点,也有许多新特性,例如热点代码探测技术。JDK 8中的HotSpot融合了BEA JRockit优秀特性
  • Mobile/Embedded VM:针对移动和嵌入式市场的虚拟机
  • BEA JRockit/IBM J9 VM:JRockit专注服务端应用,不关注程序启动速度,全靠编译器编译后执行。J9定位类似HotSpot
  • BEA Liquid VM/Azul VM:和专用硬件绑定,更高性能的虚拟机
  • Apache Harmony/Google Android Dalvik VM:Harmony被吸收进IBM的JDK 7以及Android SDK,Dalvik被ART虚拟机取代
  • Microsoft JVM:Windows系统下性能最好的Java虚拟机,因侵权被抹去

1.5 展望Java技术的未来

  • 无语言倾向:Graal VM可以作为“任何语言”的运行平台
  • 新一代即时编译器:Graal编译器,取代C2(HotSpot中编译耗时长但代码优化质量高的即时编译器)
  • 向Native迈进:Substrate VM提前编译代码,显著降低内存和启动时间
  • 灵活的胖子:经过一系列重构与开放,提高开放性和扩展性
  • 语言语法持续增强:增加语法糖和语言功能

第二部分 自动内存管理

2. Java内存区域与内存溢出异常

2.1 概述

C、C++程序员需要自己管理内存,Java程序员在虚拟机自动内存管理机制下不需要为每一个new操作写对应的delete/free代码,但是一旦出现内存泄露和溢出,不了解虚拟机就很难排错

2.2 运行时数据区域

图 3

  • 程序计数器:当前线程执行的下一条指令的地址;线程私有;不会OOM
  • 虚拟机栈:Java方法执行的线程内存模型,每个方法执行时,JVM都会创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息;线程私有;会栈溢出和OOM
  • 局部变量表:存放的变量的类型有8种基本数据类型、对象引用和returnAddress类型(指向字节码指令的地址),这些变量除了64位的long和double占两个变量槽,其它占1个。局部变量表的大小在编译器确定
  • 本地方法栈:和虚拟机栈作用类似,区别在于只是为本地(Native)方法服务,HotSpot将两者合二为一
  • 堆:“几乎”所有对象实例都在此分配内存;可分代,也不分代;逻辑上连续,物理上可以不连续;会OOM
  • 方法区:也叫“非堆”,存储已加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据;实现上,之前HotSpot使用永久代实现方法区,目的是方便垃圾回收,但是这样有OOM问题,JDK8废弃永久代改为使用本地内存的元空间,主要存放类型信息,其它移到堆;内存回收目标主要是常量池和类型信息
  • 运行时常量池:Class文件中的常量池表存放编译期生成的各种字面量和符号引用,这部分内容在类加载后放到方法区的运行时常量池;具有动态性,运行时产生的常量也可以放入运行时常量池,String类的intern()利用了这个特性;会OOM
  • 直接内存:不是运行时数据区的一部分,也不是Java虚拟机规范里定义的内存区域;NIO使用Native函数库直接分配堆外内存;会OOM

2.3 HotSpot虚拟机对象探秘

2.3.1 对象的创建
  1. 当JVM碰到new指令,首先检查指令参数能否在常量池中定位到一个类的符号引用,并且检查该类是否已被加载、解析和初始化,如果没有就进行类加载过程
  2. JVM为新生对象分配内存。
    • 对象所需内存大小在类加载后可确定,分配方法有两种:当垃圾收集器(Serial、ParNew)能空间压缩整理时,堆是规整的,分配内存就是把指针向空闲空间方向移动对象大小的距离,这种叫“指针碰撞”,使用CMS收集器的堆是不规整的,需要维护空闲列表来分配内存。
    • 内存分配可能线程不安全,例如线程在给A分配内存,指针来没来得及修改,另一线程创建对象B又同时使用了原来的指针来分配内存。解决方法有两个:1.对分配内存空间的动作进行同步处理,JVM采用CAS+失败重试的方式保证原子性;2.预先给每个线程分配一小块内存,称为本地线程分配缓冲(TLAB),线程分配内存先在TLAB上分配,TLAB用完了再同步分配新的缓存区
  3. JVM将分配到的内存空间(不包括对象头)初始化为零值,这步保证对象的实例字段可以不赋初始值就直接使用
  4. JVM对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中
  5. 执行构造函数,即Class文件中的<init>()
2.3.2 对象的内存布局
  • 对象头

    • 用于存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳,称为“Mark Word”。这部分数据长度32比特或64比特,Bitmap存储,为了在极小空间存储更多数据,不同状态的对象用不同标志位表示不同存储内容
      图 4
    • 类型指针,即对象指向它的类型元数据的指针,JVM通过该指针来确定对象是哪个类的实例。若对象是数组,还必须记录数组长度
  • 实例数据,包括父类继承下来的,和子类中定义的字段

  • 对齐填充,HotSpot要求对象大小必须是8字节的整数倍,不够就对齐填充

2.3.3 对象的访问定位

通过栈上的reference数据来访问堆上的具体对象,访问方式由虚拟机实现决定,主流有两种

  • 句柄访问。Java堆会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自具体的地址信息。优点是对象被移动时只需要改变句柄中的实例数据指针
    图 5
  • 直接指针访问。reference存储的就是对象地址,类型数据指针在对象中。优点是节省一次指针定位的时间开销,HotSpot使用此方式来访问对象
    图 6

2.4 实现:OutOfMemoryError异常

2.4.1 Java堆溢出
/** * VM options:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError  * @author panjiahao.cs@foxmail.com * @date 2023/6/18 19:54 */public class HeapOOM {    static class OOMObject{    }    public static void main(String[] args) {        ArrayList<OOMObject> list = new ArrayList<>();        while(true){            list.add(new OOMObject());        }    }}

图 7

排查思路:首先确认导致OOM的对象是否是必要的,也就是是分清楚是内存泄露了还是内存溢出了

如果是内存泄露了,可以通过工具查看泄露对象到GC Roots的引用链,定位到产生内存泄露的代码的具体位置

如果是内存溢出了,可以调大堆空间,优化生命周期过长的对象

2.4.2 虚拟机栈和本地方法栈溢出

HotSpot不区分虚拟机栈和本地方法栈,所以-Xoss(本地方法栈大小)参数没效果,栈容量由-Xss参数设定。

当线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;当扩展栈容量无法申请到足够内存,将抛出OutOfMemoryError异常。HotSpot不支持扩展栈容量

2.4.3 方法区和运行时常量池溢出

JDK8使用元空间取代了永久代,运行时常量池移动到了堆中,所以可能会产生堆内存的OOM

方法区的主要职责是存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。用CGLib不断生成增强类,可能产生元空间的OOM

/** * VM Args:-XX:MaxMetaspaceSize=10M * @author panjiahao.cs@foxmail.com * @date 2023/6/18 22:13 */public class JavaMethodAreaOOM {    public static void main(String[] args) {        while(true){            Enhancer enhancer = new Enhancer();            enhancer.setSuperclass(HeapOOM.OOMObject.class);            enhancer.setUseCache(false);            enhancer.setCallback(new MethodInterceptor() {                @Override                public Object intercept(Object obj, Method method, Object[] objects, MethodProxy proxy) throws Throwable {                    return proxy.invokeSuper(obj,args);                }            });            enhancer.create();        }    }    static class OOMObject{    }}

图 8

2.4.4 本机直接内存溢出

直接内存通过-XX:MaxDirectMemorySize参数来指定,默认与Java堆最大值一致

虽然使用DirectByteBuffer分配内存会OOM,但它抛出异常时并没有真正向操作系统申请内存,而是通过计算得知无法分配就手动抛异常,真正申请内存的方法是Unsafe::allocateMemory()

/** * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * @author panjiahao.cs@foxmail.com * @date 2023/6/19 20:38 */public class DirectMemoryOOM {    private static final int _1MB = 1024 * 1024;    public static void main(String[] args) throws IllegalAccessException {        Field unsafeField = Unsafe.class.getDeclaredFields()[0];        unsafeField.setAccessible(true);        Unsafe unsafe = (Unsafe) unsafeField.get(null);        while (true) {            unsafe.allocateMemory(_1MB);        }    }}

图 9

由直接内存导致的内存溢出,一个明显的特征是Heap Dump文件不会看到有什么明显的异常情况。如果内存溢出后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(NIO),可以重点检查直接内存

3. 垃圾收集器与内存分配策略

3.1 概述

线程独占的程序计数器、虚拟机栈、本地方法栈3个区域的内存分配和回收都具备确定性。

而Java堆和方法区有显著的不确定性:一个接口的多个实现类需要的内存可能不一样,一个方法不同分支需要的内存也不同,只有处于运行期间,才知道程序会创建哪些对象,这部分内存的分配和回收是动态的

3.2 判断对象是否存活的算法

  • 引用计数法。看似简单,但必须配合大量额外处理才能正确工作,譬如简单的引用计数法无法解决对象循环引用
  • 可达性分析算法。从GC Roots对象开始,根据引用关系向下搜索,走过的路径称为“引用链”,引用链上对象仍然存活,不在引用链上的对象可回收。
    图 10

GC Roots对象包括以下几种:

  • 虚拟机栈中引用的对象。例如方法参数、局部变量等
  • 方法区中类静态属性引用的对象。例如Java类的引用类型静态变量
  • 方法区中常量引用的对象。例如字符串常量池里的引用
  • 本地方法栈JNI引用的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)、还有系统类加载器
  • 所有被同步锁(synchronized)持有的对象
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
  • 根据用户选用的垃圾收集器以及回收区域,临时加入其它对象。目的是当某个区域垃圾回收时,该区域的对象也可能被别的区域的对象引用

引用包含以下几种类型:

  • 强引用:被强引用引用的对象不会被回收
  • 软引用:被软引用引用的对象在OOM前会被回收
  • 弱引用:被弱引用引用的对象在下一次垃圾回收时被回收
  • 虚引用:虚引用不会影响对象的生存时间,唯一目的是能在对象被回收时收到一个系统通知

即便对象已经不可达,也不是立即标记为可回收,对象真正死亡要经历两次标记过程:可达性分析发现不可达就第一次标记;如果对象重写了finalize()方法且没过JVM调用过,那么该对象会被放到队列中,由Finalizer线程去执行finalize()方法,这是对象自救的最后一次机会,只要重新与引用链上任意对象建立关联就行,譬如把this赋给某个对象的成员变量,第二次标记时就会被移除“即将回收”集合

/** * 此代码演示了两点: * 1.对象可以在被GC时自我拯救。 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次 * @author panjiahao.cs@foxmail.com * @date 2023/6/20 21:52 */public class FinalizeEscapeGC {    public static FinalizeEscapeGC SAVE_HOOK = null;    public void isAlive(){        System.out.println("yes, i am still alive");    }    @Override    protected void finalize() throws Throwable {        super.finalize();        System.out.println("finalize method executed! :)");        FinalizeEscapeGC.SAVE_HOOK = this;    }    public static void main(String[] args) throws InterruptedException {        SAVE_HOOK = new FinalizeEscapeGC();        SAVE_HOOK = null;        System.gc();        Thread.sleep(500);        if (SAVE_HOOK != null) {            SAVE_HOOK.isAlive();        } else {            System.out.println("no ,i am dead! :(");        }        // 自救失败        SAVE_HOOK = null;        System.gc();        Thread.sleep(500);        if (SAVE_HOOK != null) {            SAVE_HOOK.isAlive();        } else {            System.out.println("no ,i am dead! :(");        }    }}

方法区没有强制要垃圾回收,例如JDK11的ZGC不支持类卸载。

方法区主要回收两部分:废弃的常量和不再使用的类型。

回收废弃常量和回收堆中的对象非常类似
回收“不再使用的类”需要满足三个条件:

  • 该类所有实例已被回收,包括子类实例
  • 加载该类的类加载器已被回收,这个条件很难
  • 该类对象的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

3.3 垃圾收集算法

从如何判定对象消亡的角度出发,GC算法可以划分为“引用计数式”和“追踪式”,这两类也被称作“直接垃圾收集”和“间接垃圾收集”。本节介绍追踪式垃圾收集

3.3.1 分代收集理论

分代收集理论建立在两个假说上:

  • 弱分代假说:绝大多数对象都是朝生夕灭的
  • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡

分代收集在遇到对象之间存在跨代引用时需要遍历其它代的所有对象来确定是否还存在跨代引用,性能负担大,所以给分代收集理论添加第三个假说:

  • 跨代引用假说:跨代引用相对于同代引用来说占极少数

分代收集的名词定义:

  • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

    • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
    • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS会有单独收集老年代的行为
    • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1会有这种行为
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾

3.3.2 标记-清除算法

标记所有需要回收的对象,标记完成后,统一回收
图 11

缺点:执行效率不稳定;内存空间碎片化

3.3.3 标记-复制算法

将内存划分为大小相等的两块,每次只使用一块。当这一块的内存用完了,就将还存活着的对象复制到另一外上面,然后再把已使用过的内存空间一次性清理掉

图 12

优点:对于多数对象都是可回收的情况,算法复制开销小;没有碎片
缺点:可用内存小了一半;需要空间担保

1:1划分新生代太浪费空间,HotSpot将新生代划分成Eden:Survivor0:Survivor0 = 8:1:1,每次可以用Eden和一块Survivor,垃圾回收时把存活对象写到另一块Survivor,然后清理掉Eden和已用过的Survivor,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖老年代进行分配担保

3.3.4 标记-整理算法

标记所有存活的对象,然后移动到内存空间一端,清理掉边界以外的内存
图 13

优点:解决了标记-清除算法造成的空间碎片化问题
缺点:整理期间,用户应用程序暂停,这段时间被称为“Stop The World”

整理即移动对象,移动则内存回收时会更复杂,不移动则内存分配会更复杂。从GC停顿时间来看,不移动对象停顿时间短;从吞吐量来看,移动对象更划算。

HotSpot里关注吞吐量的Parallel Scavenge收集器采用标记-整理算法,关注延迟的CMS收集器采用标记-清除算法

3.4 HotSpot的算法细节实现

3.4.1 根节点枚举

固定作为GC Root的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表),Java程序越来越庞大,逐个作为起点进行可达性分析会消耗大量时间

目前,所有收集器在根节点枚举时和整理内存碎片一样必须暂停用户线程,可达性分析算法可以和用户线程一起并发。为了降低STW时间,在不扫描全部的GC Root节点情况下,得知哪些地方存在对象引用,HotSpot提供了OopMap的数据结构保存引用的位置信息

3.4.2 安全点

OopMap可以帮助HotSpot快速完成GC Root枚举,但是如果为每条改变OomMap内容的指令都生成对应的OopMap,会需要大量额外存储空间

因此HotSpot没有为每条指令都生成OopMap,只在特定位置生成,这些位置称为安全点。用户程序只有在执行到安全点才能停顿下来垃圾回收。

安全点的选取考虑:不能太少以至于让收集器等待时间太长,也不能太频繁以至于增大内存负担

如何在垃圾回收时让所有线程(不包括执行JNI调用的线程)都跑到最近的安全点,然后停顿下来。有两种方案:

  • 抢先式中断:先把用户线程全部中断,如果发现有用户线程不在安全点上就恢复这个线程,过一会再中断直至它跑到安全点。现在几乎不使用
  • 主动式中断:设置一个标志位,各个线程执行时主动轮询这个标志,一旦为真主动中断挂起。HotSpot使用内存保护陷阱的方式将轮询操作精简至只有一条汇编指令
3.4.3 安全区域

当用户线程处于Sleep或者Blocked状态时不能执行到安全点。针对这种情况,引入安全区域。

安全区域是指在某一段代码片段中,引用关系不会发生变化,在这个区域中任意地方开始GC都是安全的。

当用户线程执行到安全区域时,首先标识自己进入了安全区域,这样在GC时,虚拟机就不会去管这些已标识自己进入安全区域的线程。当线程离开安全区域时,它检查虚拟机是否完成了根节点枚举(或者其它需要暂停用户线程的阶段),如果完成了就继续执行,否则等待直到收到可以离开安全区域的信号为止。

3.4.4 记忆集与卡表

记忆集是一种记录从非收集区域指向收集区域的指针集合的抽象数据结构,用于解决对象跨代引用带来的问题

记忆集最简单的实现是非收集区域中所有含跨代引用的对象数组,这种结构浪费太多空间,可以粗化记录粒度:

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如32或64),该字包含跨代指针
  • 卡精度:每个记录精确到一块内存区域,该区域有对象含有跨代指针

卡精度使用卡表实现,卡表的实现是一个字节数组,字节数组每个元素都对应着一块特定大小的内存块,称为卡页。只要卡页内有一个或更多对象的字段存在跨代引用,卡表中对应的数组元素的值标识为1,称为变脏。垃圾收集时,只要筛选出卡表中变脏的元素就可以知道哪些卡页内存块有跨代引用,把它们放入GC Roots中一起扫描

3.4.5 写屏障

卡表元素变脏的时间点是引用类型字段赋值那一刻,HotSpot通过写屏障来维护卡表状态。写屏障可以看作JVM层面对“引用类型字段赋值”这个动作的AOP切面。

写屏障会导致伪共享问题,伪共享是指,现代CPU的缓存系统是以缓存行为单位存储的,当多线程修改相互独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低

伪共享的一种简单解决方法是不采用无条件的写屏障,而是先检查卡表标记,只有卡表元素未被标记时才将其变脏。HotSpot参数-XX:+UseCondCardMark决定是否开启卡表更新,开启会多一次判断开销,但能够避免伪共享带来的性能损耗

3.4.6 并发的可达性分析

可达性分析在标记阶段会暂停用户线程以在一致性的快照上进行对象图的遍历,不一致情况下会出现“对象消失”问题。原因可以由三色标记方法推导

图 14

白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚开始时,所有对象都是白的,若在分析结束阶段,仍然是白色的对象是不可达
黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色对象代表已经扫描过,是安全存活的,如果有其它对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象
灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过

当且仅当以下两个条件同时满足时,会产生“对象消失”问题,即原本应该黑色的对象被误标为白色:

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用

解决并发扫描时的对象消失问题,只需破坏两个条件之一即可,因此有两种方案:

  • 增量更新。增量更新破坏条件一,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束后,再以这个黑色对象为根,重新扫描一次。可以理解为,黑色对象一旦新插入指向白色对象的引用之后,它就变回灰色对象
  • 原始快照。当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。可以理解为,无论引用关系删除与否,都会按照刚开始扫描那一刻的对象图快照进行搜索

对引用关系记录的插入和删除都是通过写屏障实现。

3.5 经典垃圾收集器

图 15

图中连线表示可以搭配使用

3.5.1 Serial收集器

图 16

适合资源(cpu和内存)受限的场景,新生代一两百兆以内的垃圾收集停顿时间最多一百多毫秒以内

3.5.2 ParNew收集器

实质是多线程版的Serial

图 17

3.5.3 Parallel Scavenge收集器

与ParNew类似,不同点是其它收集器关注停顿时间,Parallel Scavenge收集器目标是可控制的吞吐量

图 18

-XX:MaxGCPauseMillis控制最大垃圾收集停顿时间,-XX:GCTimeRatio直接设置吞吐量,-XX:+UseAdaptiveSizePolicy会根据系统运行情况动态调整虚拟机参数以获得最合适的停顿时间或者最大的吞吐量

3.5.4 Serial Old收集器

Serial收集器的老年代版本

图 19

3.5.5 Parallel Old收集器

Parallel Scavenge的老年代版本,在注重吞吐量或者处理器资源稀缺场合,可以考虑使用Parallel Scavenge+Parallel Old

图 20

3.5.6 CMS收集器

四步骤:
1)初始标记
2)并发标记
3)重新标记
4)并发清理

初始标记和重新标记仍然要暂停用户线程。初始标记仅仅标记GC Roots能直接关联的对象,速度很快;并发标记从关联对象开始遍历整个对象图,不需要暂停用户线程;重新标记用来修正并发标记期间的变动(增量更新)

图 21

优点:并发收集、低停顿
缺点:

  • 对处理器资源敏感。并发会占用处理器,降低吞吐量
  • 无法处理“浮动垃圾”。“浮动垃圾”指并发标记和清理期间用户线程产生的垃圾在下一次GC时清理,预留空间不足会,预留空间不足会导致“Concurrent Mode Failure”进而导致Full GC或者Serial Old重新回收老年代。
  • 内存空间碎片化
3.5.7 Garbage First(G1)收集器

开创面向局部收集的设计思路和基于Region的内存布局形式

局部收集只收集范围不是新生代或老年代,而是堆中任意部分。

基于Region的堆内存布局:G1不再坚持固定大小和数量的分代区域划分,而是把连续的堆划分为多个大小相等的独立区域Region,每个Region都可以根据需要扮演新生代或者老年代。超过Region容量一半的对象会被存到Humongous区域中,超过整个Region容量的对象会被存到N个连续的Humongous Region中,G1大多数行为把Humongous Region当老年代处理
图 22

四个步骤:

  • 初始标记:标记GC Roots能直接关联的对象,修改TAMS指针的值,让下一阶段用户并发运行时能正确在可用的Region中分配新对象
  • 并发标记:递归扫描对象图。扫描完成后重新处理SATB记录下的在并发时有引用变动的对象
  • 最终标记:处理并发阶段遗留的少量SATB记录
  • 筛选回收:负责更新Region统计数据,制定回收计划

图 23

3.6 低延迟垃圾收集器

垃圾收集器“不可能三角”:内存占用、吞吐量、延迟。延迟是最重要指标

下图,浅色表示挂起用户线程,深色表示gc线程和用户线程并发

图 24

Shenandoah和ZGC在可管理的堆容量下,停顿时间基本是固定的,两者被命名为低延迟垃圾收集器

3.6.1 Shenandoah收集器

目标:能在任何堆内存大小下都可以把GC停顿时间限制在10ms以内

Shenandoah和G1有相似的堆内存布局,在初始标记、并发标记等阶段的处理思路高度一致。
有三个明显的不同:

  • 支持并发的整理算法
  • 默认不分代,即不使用专门的新生代Region和老年代Region
  • 放弃G1中耗费大量内存和计算资源去维护的记忆集,改为“连接矩阵”记录跨Region的引用关系

图 25

收集器的工作分为九个阶段:

  • 初始标记:与G1一样,首先标记GC Roots直接关联的对象,停顿时间与堆大小无关,与GC Roots数量有关
  • 并发标记:与G1一样,遍历对象图,与用户线程并发,时间取决于对象数量和对象图的结构复杂程度
  • 最终标记:与G1一样,处理剩余的SATB扫描,统计价值最高的Region组成回收集,有一小段停顿时间
  • 并发清理:清理一个存活对象都没有的Region
  • 并发回收:将回收集中的存活对象复制到未被使用的Region中,通过读屏障和“Brooks Pointers”转发指针解决和用户线程并发产生的问题
  • 初始引用更新:一个非常短暂的停顿,用于确保所有并发回收阶段中收集器线程都已完成分配对象移动任务
  • 并发引用更新:真正开始进行引用更新,与用户线程并发
  • 最终引用更新:修正存在于GC Roots中的引用,停顿时间与GC Roots的数量有关
  • 并发清理:经过并发回收和引用更新后,整个回收集的Region已经没有存货对象,回收这些Region的内存空间

图 26

对象移动和用户程序并发,原来的解决方案是在被移动对象原有的内存上设置保护陷阱,一旦用户程序访问到原有的地址就产生自陷中断,进入预设的异常处理器,将访问转发到新对象。这个方案会导致用户态频繁切换到核心态

Brooks Pointers是在原有对象布局结构的最前面加一个新的引用字段,移动前指向自己,移动后指向新对象。这里需要用CAS解决对象访问的并发问题

图 27

3.6.2 ZGC收集器

ZGC收集器是一款基于Region内存布局的,不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。收集过程全程可并发,短暂停顿时间只与GC Roots大小相关而与堆内存大小无关

  1. ZGC的Region是动态的:动态创建和销毁、动态大小

    图 28

  2. 染色指针:把标记信息记在引用对象的指针上,标记信息有4个bit,虚拟机可以直接从指针中看到其引用对 象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问到

    图 29

    三大优势:

    • 染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用 掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理
    • 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的 目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作
    • 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以 便日后进一步提高性能

    标志位影响了寻址地址,ZGC通过多重映射,将不同标志位的指针映射到同一内存区域

    图 30

  3. ZGC的收集过程分为四大阶段

    图 31

    • 并发标记:与G1、Shenandoah类似,区别是标记在指针上而不是对象上,标记会更新染色指针的Marked 0、Marked 1标志位
    • 并发预备重分配:根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集
    • 并发重分配:把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系
    • 并发重映射:修正整个堆中指向重分配集中旧对象的所 有引用

3.7 选择合适的垃圾收集器

3.7.1 Epsilon收集器

不收集垃圾,负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责

响应微服务而生,在堆内存耗尽前就退出,不收集垃圾就非常合适

3.7.2 收集器的权衡

三个因素:

  • 应用程序关注点是什么?吞吐量、延时还是内存占用
  • 基础设施如何?处理器的数量、内存大小、操作系统
  • JDK的发行商是谁?版本号是多少

例如直接面向用户的B/S系统,延迟是主要关注点。

  • 预算充足就用商业的
  • 预算不足但能掌控基础设施,可以尝试ZGC
  • 对ZGC的稳定性有疑虑就考虑Shenandoah
  • 软硬件和JDK都老的考虑CMS
]]>
+ + + + + <h2 id="第一部分-走近java"><a class="markdownIt-Anchor" href="#第一部分-走近java"></a> 第一部分 走近Java</h2> +<h3 id="1-走近java"><a class="markdownIt-Anchor" h + + + + + + + + + +
+ + + Excel比对与合并系统 + + https://zunpan.github.io/2023/03/03/Excel%E6%AF%94%E5%AF%B9%E4%B8%8E%E5%90%88%E5%B9%B6%E7%B3%BB%E7%BB%9F/ + 2023-03-03T07:00:03.000Z + 2023-09-24T04:27:40.287Z + + 背景

许多游戏策划使用Excel来配置数值。策划需要保存所有版本的数值表,有时需要查看两个数值表有何差异,有时想把差异或者叫修改应用到另一张数值表中,这非常类似于版本控制,但是市面上的版本控制系统svn和git都是针对文本文件,不能用于Excel的版本控制

Excel比对算法

一维数据比对算法

假设有两个序列A1...AmA_1...A_mB1...BnB_1...B_n,我们可以通过对A序列进行一些列操作,使之变为B序列。对每种操作都定义个Cost,如何找到总Cost最小的使A变为B的操作序列,可以通过动态规划解决。这是一个已经被广为研究的算法问题,本文就不再整述,读者可以在网上搜索Edit编辑距离获取更多信息。

操作集合的定义有多种方式,一种较为常见的操作集合定义如下(Cost均为1) :

  • 在序列中插入一个元素:
  • 在序列中删除一个元素;

比如,将字符串kiten变换为sitting,需要删除k,插入s,删除e,插入i,在尾部插入g。如果在原序列和目标序列中去掉删除和插入的元素,那么原序列和目标序列就是完全相同的了(比如上面的例子两者都变为itn了),因此这种编辑距离被命名为LCS (Longest Common Subsequence) 编辑距离。LeetCode 1143. 最长公共子序列

再回到本文要讨论的差异比较问题,要比较两个序列的差异,实际上就是要找到二者之间尽量多的公共部分,剩下的就是差异部分,所以这和最短编辑距离问题是完全等价的。

此外,除了LCS编辑距离之外,还有一种常用的编辑距离,允许插入、删除和修改操作,叫做Levenshtein编组距离。另外,还可以定义一种广义的Levenshtein编辑距离,删除元素AiA_i和插入元素BjB_j;的Cost由一个单参数函数决定,记为Cost(AiA_i)或Cost(BjB_j); 将AiA_i修改为BjB_j;的操作的Cost由一个双参数函数决定,记为Cost2(Ai,BjA_i, B_j)。

/**     * 比对的基本单位是单个字符     * @param text1 字符串1     * @param text2 字符串2     * @return levenshteinDP数组     */    static int[][] levenshteinDP(String text1, String text2) {        int len1 = text1.length();        int len2 = text2.length();        // dp[i][j]表示从text1[0...i-1]到text2[0...j-1]的最小编辑距离(cost)        dp = new int[len1 + 1][len2 + 1];        // path记录此方格的来源是多个此类枚举值的布尔或值        path = new int[len1 + 1][len2 + 1];        for (int i = 0; i < len1 + 1; i++) {            dp[i][0] = i;            path[i][0] = FROM_INIT;        }        for (int j = 0; j < len2 + 1; j++) {            dp[0][j] = j;            path[0][j] = FROM_INIT;        }        for (int i = 1; i < len1 + 1; i++) {            for (int j = 1; j < len2 + 1; j++) {                path[i][j] = FROM_INIT;                int left = dp[i][j - 1] + 1;                int up = dp[i - 1][j] + 1;                int leftUp;                boolean replace;                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {                    leftUp = dp[i - 1][j - 1];                    replace = false;                } else {                    leftUp = dp[i - 1][j - 1] + 1;                    replace = true;                }                dp[i][j] = Math.min(Math.min(left, up), leftUp);                if (dp[i][j] == left) {                    path[i][j] |= FROM_LEFT;                }                if (dp[i][j] == up) {                    path[i][j] |= FROM_UP;                }                // 对应:两字符完全一样或者可以修改成一样                if (dp[i][j] == leftUp) {                    if (replace) {                        path[i][j] |= FROM_LEFT_UP_REPLACE;                    } else {                        path[i][j] |= FROM_LEFT_UP_COPY;                    }                }            }        }        return dp;    }

同样的,对于广义Levenshtein编辑距离,如果去掉删除和插入的元素,那么两个序列中剩下的元素即为一一对应的关系,每组对应的两个元素,要么是完全相同的,要么前者是被后者修改掉的。从这部分论述中我们不难看出,比较算法的核心思路实际上就是找到元素与元素之间的一一对应关系

二维数据比对算法

Excel的一个分页可以认为是一个二维数据阵列,阵列中的每个元素是对应单元格内容的字符串值。根据前面的论述,比较两个二维阵列的核心就是找到他们公共的行/列,或者说原阵列和目标阵列的行/列对应关系。比如,对于下面两张表:
图 2

甲表的第1、2、3列对应乙表的1、2、4列,甲表的1、3行对应乙表的1、2行。那么这两张表的差异通过下列方式描述:

  • 在第2列的位置插入新列
  • 删除第2行
  • 将(原表中) 第3行第3列的元素从9修改为0;

如何计算两张表对应的行/列,一个比较容易想到的方案是将其拆分为两个独立求解的问题,计算对应的行和计算对应的列。对于前者,我们可以把阵列的每一行当成一个元素,所有行组成一个序列,然后对这个序列进行比较:后者亦然。这样我们就把二维比较问题转化成了一维比较的问题。关于编辑距离的定义,可以采用广义Levenshtein编辑距离,定义删除、插入元素的Cost为该行(列)的元素数,定义修改元素的Cost为这两行(列)之间的LCS编辑距离.于是两个二维阵列的比较过程如下:
找到二者对应的 (或者叫公共的) 行/列,非公共的行/列记为删除、插入行/列操作;两张表只保留公共的行/列,此时他们尺寸完全相同,对应位置的单元格逐一比较,如果值不相同,则记为单元格修改操作;
图 3

算法优化

上一个部分介绍的二维阵列比较方案只是一个理论上可行的方案,在实际应用中,存在以下问题:

  • 删除、插入行/列的操作都是对于整个行/列的,而计算两行/列之间的LCS编辑距离是独立计算的,因此算法本身有一定不合理性;
  • 计算修改Cost里又包含了LCS编辑距离的计算,二层嵌套,性能开销比较大;

针对上述问题,从游戏开发的实际应用场景出发,做了如下优化:
首先计算列之间的对应关系,只取表头前h行(不同项目表头行数h可能不同,可以通过参数配置) ,这样就把对整列的LCS编辑距离计算优化为h个单元格逐已比较,大幅优化了效率,而且对准确度基本不会有什么影响;

根据上一步的计算结果,去掉非公共的列(即删除、添加的列),这样,剩下的列都是两边一一对应的了,此时再计算行的对应关系,修改操作的Cost定义就可以从LCS编辑距离改为单元格的逐一比较了,这样又大幅优化了性能

在上面所述基础之上,还可以再做优化,因为在实际应用中,绝大多数情况下,绝大多数行都不会有修改,因此可以先用LCS编辑距离对所有行的对应关系进行计算,即只有当两行内容完全相同时才会被当做是对应的; 然后再把剩下的未匹配的行分组用广义Levenshtein编辑距离进行对应关系匹配。这样么做的原因是LCS编辑距离比广义Levenshtein编辑距离的求解速度要快很多。

图 6

功能扩展

在开发过程中,我们经常会将单行或者连续若干行单元格上移或下移到另一位置,按照目前的比较逻辑,该操作会被认为是删除这些行,然后在新的位置重新插入这些行。这样的结果和不合理的。为此,我们可以引入一种新的操作: 移动行到另一位置。加入了这个新的操作之后,我们依然可以建立之前所述的行对应关系,只不过两边行的顺序可以是乱序的。这种不保序对应关系可以通过多个轮次的编辑距离匹配计算,每次匹配之后去掉匹配上的行,剩下未匹配的行组成一个新的序列进行下一轮的匹配。每轮匹配是采用LCS编辑距离还是广义Levenshtein编指距离可以灵活决定,比如前若干轮或者行数较多时采用LCS编辑距离,后面的轮次再用广义Levenshtein编辑距离。

图 7

]]>
+ + + + + <h2 id="背景"><a class="markdownIt-Anchor" href="#背景"></a> 背景</h2> +<p>许多游戏策划使用Excel来配置数值。策划需要保存所有版本的数值表,有时需要查看两个数值表有何差异,有时想把差异或者叫修改应用到另一张数值表中, + + + + + + + + + + + + + + + + + +
+ + + MySQL实战45讲学习笔记 + + https://zunpan.github.io/2023/02/01/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ + 2023-02-01T10:57:05.000Z + 2023-09-24T04:27:40.280Z + + 基础架构:一条SQL查询语句是如何执行的

图 1

MySQL可以分为Server层和存储引擎层两部分

Server层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。

而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎

连接器

连接器负责跟客户端建立连接、获取权限、维持和管理连接。建立连接后,权限修改不会影响已经存在的连接的权限

长连接:连接成功后,如果客户度持续有请求,一直使用同一个连接
短连接:每次执行很少的几次查询就断开连接,下次查询再重新建立

建立连接比较耗时,尽量使用长连接,但是全部使用长连接会导致OOM,因为MySQL在执行过程中临时使用的内存是管理在连接对象里面,连接不断开内存不会释放

解决方案:

  1. 定期断开长连接
  2. 执行mysql_reset_connection重新初始化连接资源

查询缓存

执行过的语句及其结果可能会以key-value对的形式,直接缓存在内存中

查询缓存的失效非常频繁,只要有对一个表的更新,这个表上的所有查询缓存都会被清空,因此不要使用查询缓存,MySQL 8.0删掉了此功能

分析器

分析器先做“词法分析”,识别出SQL语句中的字符串分别是什么,例如,识别“select”是查询语句,“T”识别成表名T

然后做“语法分析”,判断输入的SQL语句是否满足MySQL语法,如果语句不对,会收到“You have an error in your SQL syntax”的错误提醒

优化器

优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序

例如下面的语句执行两个表的join:

mysql> select * from t1 join t2 using(ID)  where t1.c=10 and t2.d=20;
  • 既可以先从表t1里面取出c=10的记录的ID值,再根据ID值关联到表t2,再判断t2里面d的值是否等于20。
  • 也可以先从表t2里面取出d=20的记录的ID值,再根据ID值关联到t1,再判断t1里面c的值是否等于10。

这两种执行方法的结果一样但效率不同,优化器决定使用哪个方法

执行器

  1. 判断有没有表的执行权限
  2. 根据表的引擎定义调用引擎提供的接口,例如“取满足条件的第一行”,“满足条件的下一行”,数据库的慢查询日志rows_examined字段表示语句在执行过程中扫描了多少行,引擎扫描行数跟rows_examined并不是完全相同的

日志系统:一条SQL更新语句是如何执行的

一条查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块,最后到达存储引擎。更新语句还涉及 redo log(重做日志)和 binlog(归档日志)

redo log

WAL(Write-Ahead Logging):更新记录时,InnoDB引擎会先把记录写到redo log里面并更新内存,然后在适当的时候将操作记录更新到磁盘里面

InnoDB的redo log是固定大小和循环写的,write pos是当前记录的位置,checkpoint是当前要擦除的位置,擦除记录前要把记录更新到数据文件

redo log保证即使数据库异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe

binlog

redo log是InnoDB引擎特有的日志,Server层特有的引擎是binlog(归档日志)

两者有三点不同

  1. redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用
  2. redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1”
  3. redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志

执行器和InnoDB引擎在执行update语句时的内部流程

  1. 执行器先找引擎取ID=2这一行。ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  2. 执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。
  4. 执行器生成这个操作的binlog,并把binlog写入磁盘。
  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。

下图的浅色框表示在InnoDB内部执行,深色框表示在执行器中执行

图 2

两阶段提交

redo log的写入分为两步骤:prepare和commit,也就是”两阶段提交“,目的是为了让两份的日志之间的逻辑一致

当数据库需要恢复到指定的某一秒时,可以先在临时库上这样做:

  1. 找到最近的一次全量备份
  2. 从备份的时间点开始,将备份的binlog依次取出来重放到指定时间

如果redo log不是两阶段提交

  1. 先写redo log后写binlog。假设在redo log写完,binlog还没写完,MySQL异常重启,数据可以恢复,但是binlog没有记录这个语句。之后用binlog恢复临时库时会缺少更新
  2. 先写binlog后写redo log。假设binlog写完,redo log还没写,MySQL异常重启之后,这个事务无效,数据没有恢复。但是binlog里面已经有这个语句,所以之后用binlog恢复临时库会多一个事务

innodb_flush_log_at_trx_commit这个参数设置成1的时候,表示每次事务的redo log都直接持久化到磁盘。这个参数建议设置成1,这样可以保证MySQL异常重启之后数据不丢失。

sync_binlog这个参数设置成1的时候,表示每次事务的binlog都持久化到磁盘。建议设置成1,这样可以保证MySQL异常重启之后binlog不丢失。

如果redo log处于prepare状态且binlog写入完成,MySQL异常重启会commit掉这个事务

事务隔离

事务保证一组数据库操作要么全部成功,要么全部失败

隔离性与隔离级别

ACID中的I指的是隔离性(Isolation)

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。

事务隔离级别包括:

  • 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到
  • 读提交:一个事务提交之后,它做的变更才会被其他事务看到
  • 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其它事务也是不可见的
  • 串行化:对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成才能继续执行

数据库使用视图来实现隔离级别。在“可重复读”隔离级别下,视图是在事务开启时创建的。在“读提交”隔离级别下,视图是在每个SQL语句开始执行的时候创建的。“读未提交”直接返回记录的最新值,没有视图概念。“串行化”直接用加锁的方式

事务隔离的实现

这里展开说明“可重复读”

在MySQL中,每条记录在更新时都会同时记录一条回滚操作。假设一个值从1按顺序改成了2、3、4,在回滚日志里有类似下面的记录

图 3

当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。视图A、B、C对应的值分别是1、2、4,同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC)。对于视图A,要得到1,就必须将当前值依次执行图中所有的回滚操作。即使现在有另一个事务正在将4改成5,这个事务跟视图A、B、C对应的事务是不会冲突的。

系统会将没有比回滚日志更早的read-view时删掉这个回滚日志。因此尽量不要使用长事务,系统里面会存在很老的事务视图

事务的启动方式

MySQL的事务启动方式有如下几种:

  1. 显式启动事务,begin 或 start transaction。配套的提交语句是commit,回滚语句是rollback
  2. 隐式启动事务,一条SQL语句会自动开启一个事务。需要设置autocommit = 1 才会自动提交

set autocommit=0,会将这个线程的自动提交关掉。事务持续存在直到你主动执行commit 或 rollback语句,或者断开连接

建议使用set autocommit=1,并显示启动事务

在autocommit为1的情况下,用begin显式启动的事务,如果执行commit则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行begin语句的开销

可以在information_schema库的innodb_trx这个表中查询长事务,比如下面这个语句,用于查找持续时间超过60s的事务。

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

如何避免长事务对业务的影响?

从应用端来看

  1. 通过MySQL的general_log确保autocommit=1
  2. 包含多个select语句的只读事务,没有一致性要求就拆分
  3. 通过SET MAX_EXECUTION_TIME控制每个语句的最长执行时间

从数据库端来看

  1. 监控 information_schema.Innodb_trx表,设置长事务阈值,超过就kill
  2. 推荐使用Percona的pt-kill
  3. 业务测试阶段就输出所有general_log,分析日志提前发现问题
  4. innodb_undo_tablespaces设置成2或更大的值

深入浅出索引(上)

索引的出现是为了提高数据查询的效率

索引的常见模型

哈希表,只适用于只有等值查询的场景,不适用于范围查询

有序数组在等值查询和范围查询场景中都非常优秀,但更新数据需要挪动数组元素,成本太高。只适用于静态存储引擎(数据不再变化)

平衡二叉查找树的时间复杂度是O(log(N)),但是算上每次访问节点的磁盘IO开销,查询非常慢。为了减少磁盘IO次数,出现了N叉树

InnoDB的索引模型

根据叶子节点内容,索引类型分为主键索引和非主键索引

主键索引(聚簇索引):叶子节点存的是整行数据
普通索引(二级索引):叶子结点存的是主键的值

图 4

基于主键索引和普通索引的查询的区别:

  • 如果语句是select * from T where ID=500,即主键查询方式,则只需要搜索ID这棵B+树;
  • 如果语句是select * from T where k=5,即普通索引查询方式,则需要先搜索k索引树,得到ID的值为500,再到ID索引树搜索一次。这个过程称为回表。

索引维护

在插入新记录时,B+树为了维护有序性会进行页分裂和页合并

自增主键 VS 业务字段主键

性能上:自增主键按序插入,不会触发叶子节点的分裂,而业务字段做主键往往不是有序插入,导致页分裂和页合并,性能差
存储空间上:主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。业务字段主键是身份证号(20字节)不如自增主键(4字节或8字节)

业务字段做主键的场景是:

  1. 只有一个索引
  2. 该索引必须是唯一索引

这就是典型的KV场景,直接将这个字段设置为主键

深入浅出索引(下)

覆盖索引

覆盖索引:索引的叶子节点可以直接提供查询结果,不需要回表

可以为高频请求建立联合索引起到覆盖索引的作用

最左前缀原则

索引项是按照索引定义里面出现的字段的顺序排序的。满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左N个字段,也可以是字符串索引的最左M个字符

索引内的字段顺序评估标准:

  1. 复用能力,如果该顺序的联合索引能少维护一个索引,那么该顺序优先使用
  2. 空间,如果必须维护联合索引和单独索引,那么给小字段单独索引,联合索引的顺序是(大字段,小字段)

索引下推

在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足的条件的记录,减少回表次数(联合索引在按最左匹配时碰到范围查询停止,索引下推可以对后面的索引字段做条件判断后再返回结果集)

全局锁和表锁

根据加锁的范围,MySQL里面的锁大致可以分成全局锁、表级锁和行锁三类

全局锁

全局锁是对整个数据库实例加锁,让整个库处于只读状态

Flush tables with read lock

全局锁的典型使用场景是不支持MVCC的引擎(MyISAM)的全库逻辑备份,如果所有表的引擎支持MVCC,可以在备份时开启事务确保拿到一致性视图(mysqldump加上参数-single-transaction)

让全库只读,另外一种方式是set global readonly = true,但仍然建议使用FTWRL,因为:

  1. readonly的值可能会用来做其它逻辑,比如判断是主库还是备库
  2. FTWRL在客户端发生异常断开时,MySQL会自动释放全局锁,而readonly会一直保持

表级锁

表级锁有两种:表锁,元数据锁(meta data lock,MDL)

表锁

语法:lock tables … read/write

表锁会限制其它线程的读写,也会限定本线程的操作对象

例如,线程A执行lock tables t1 read, t2 write;,其它线程写t1、读写t2都会被阻塞,线程A只能执行读t1、读写t2,不能访问其它表

如果支持行锁,一般不使用表锁

元数据锁

MDL不需要显示使用,在访问表时会被自动加上,事务提交才释放,作用是保证读写的正确性

当对表做增删改查操作时,加MDL读锁;当对表结构变更时,加MDL写锁

  • 读锁之间不互斥,因此可以有多个线程同时对一张表增删查改
  • 读写锁之间、写锁之间是互斥的。因此如果有两个线程要同时给一个表加字段,其中一个要等另一执行完再执行

给表加字段的方式:

  1. kill掉长事务,事务不提交会一直占着MDL
  2. 在alter table语句设置等待时间,如果在等待时间内能拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句。后面重试这个过程

行锁

行锁是针对表中行记录的锁

两阶段锁

两阶段锁协议:在InnoDB事务中,行锁是在需要的时候加上的,但并不是不需要了就立刻释放,而是要等到事务结束才释放

如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放

死锁和死锁检测

死锁:并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源,导致这几个线程无限等待

死锁出现后有两种策略:

  • 设置等待时间,修改innodb_lock_wait_timeout
  • 发起死锁检测,主动回滚死锁链条中的某一个事务,让其他事务可以继续执行。innodb_deadlock_detect设置为on表示开启

第一种策略,等待时间太长,业务的用户接受不了,等待时间太短会出现误伤。所以一般用死锁检测

死锁检测有性能问题,解决思路有几种:

  • 如果能确保业务一定不会出现死锁,可以临时把死锁检测关掉。这种方法存在业务有损的风险,业务逻辑碰到死锁会回滚重试,但是没有死锁检测会超时导致业务有损
  • 控制并发程度。数据库Server层实现,对相同行的更新,在进入引擎之前排队
  • 将一行改成逻辑上的多行。例如账户余额等于10行之和,扣钱时随机扣一行,这种方案需要根据业务逻辑做详细设计

详解事务隔离

假如有如下表和事务A、B、C

mysql> CREATE TABLE `t` (  `id` int(11) NOT NULL,  `k` int(11) DEFAULT NULL,  PRIMARY KEY (`id`)) ENGINE=InnoDB;insert into t(id, k) values(1,1),(2,2);

图 5

"快照"在MVCC里是怎么工作的

快照不是整个数据库的拷贝。

InnoDB里每个事务都有一个唯一的transaction id,是事务开始时申请的,严格递增的。每行数据有多个版本,每次事务更新数据时,都会生成一个新的数据版本,并把transaction id赋给这个数据版本的事务id,记为row trx_id。某个版本的数据可以通过当前版本和undo log计算出来

在实现上,InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指的是启动了但还没提交

数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位

图 6

对于当前事务的启动瞬间来说,一个数据版本的row trx_id,有以下几种可能:

  1. 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
  2. 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
  3. 如果落在黄色部分,那就包括两种情况
    a. 若 row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见;
    b. 若 row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。

InnoDB利用了“所有数据都有多个版本”的特性,实现了“秒级创建快照”的能力

可以用时间顺序来理解版本的可见性。

一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

  1. 版本未提交,不可见;
  2. 版本已提交,但是是在视图创建后提交的,不可见
  3. 版本已提交,而且是在视图创建前提交的,可见

更新逻辑

更新数据都是先读后写,读是当前值,称为“当前读”(current read)。所以事务B是在(1,2)上进行修改

select如果加锁,也是当前读,不加就是一致读,下面两个select语句分别加了读锁(S锁,共享锁)和写锁(X锁,排它锁)。行锁包括共享锁和排它锁

mysql> select k from t where id=1 lock in share mode;mysql> select k from t where id=1 for update;

假设事务C变成了事务C’

图 7

事务C’还没提交,但是生成了最新版本(1,2),根据“两阶段协议”,(1,2)这个版本上的写锁还没释放,事务B的更新是当前读,需要加锁,所以被阻塞

可重复读的核心就是一致性读(consistent read);而事务更新数据时只能用当前读,如果当前的记录的行锁被其他事务占用的话,就进入锁等待。

读提交的逻辑和可重复读的逻辑类似,主要区别是:

  • 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。

普通索引和唯一索引之间的选择

普通索引VS唯一索引:两者类似,区别是唯一索引的列值不能重复,允许一个为空

下面从这两种索引对查询语句和更新语句的性能来分析,前提是业务保证记录的唯一性,如果业务不能保证唯一性又有唯一需求,就必须用唯一索引

查询过程

普通索引:查找到满足条件的第一个记录后,需要查找下一个记录,直到碰到第一个不满足条件的记录

唯一索引:由于唯一性,查找到满足条件的第一个记录后就停止

由于InnoDB的数据是按数据页为单位来读写,所以两者性能差距微乎其微

更新过程

change buffer:当需要更新一个数据页时,如果数据页在内存中就直接更新,如果不在,在不影响数据一致性的前提下,InnoDB会将这些更新操作缓存在change buffer中。在下次查询需要访问这个数据页时,读入内存,然后执行change buffer中与这个页有关的操作,这个过程称为merge

唯一索引的更新不能用change buffer,因为需要先将数据页读入内存判断操作是否违反唯一性约束

假如现在有个插入新记录的操作,如果要更新的目标页在内存中,普通索引和唯一索引性能差距不大。如果目标页不在内存中,对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值;对于普通索引来说,将更新记录在change buffer,此时普通索引的性能好(主键索引的数据页是一定要加载进内存做更新操作,普通索引的数据页不用进内存)

change buffer的使用场景

因为merge的时候是真正做数据更新的时候,在merge之前,change buffer记录的变更越多,收益越大

对于写多读少的业务,change buffer的效果最好,比如账单类、日志类的系统

索引的选择和实践

尽量使用普通索引

如果更新完马上查询,就关闭change buffer。否则开着能提升更新性能

change buffer 和 redo log

redo log 主要节省的是随机写磁盘的IO消耗(转成顺序写),而change buffer主要节省的则是随机读磁盘的IO消耗。

MySQL为什么有时候会选错索引

图 8

session B 先删除了所有数据然后调用idata存储过程插入了10万行数据。

然后session B 执行三条SQL:

// 将慢查询日志的阈值设置为0,表示这个线程接下来的语句都会被记录慢查询日志中set long_query_time=0;select * from t where a between 10000 and 20000; /*Q1*/select * from t force index(a) where a between 10000 and 20000;/*Q2*/

图 9

Q1走了全表扫描,Q2使用了正确的索引

优化器的逻辑

选择索引是优化器的工作,目的是寻找最优方案执行语句,判断标准包括扫描行数、是否使用临时表、是否排序等因素

上面查询语句没有涉及临时表和排序,说明扫描行数判断错误了

MySQL是怎么判断扫描行数的

MySQL在真正开始执行语句之前,并不能精确知道有多少行,而只能用统计信息估算。这个统计信息就是索引的“区分度”,索引上不同的值称为“基数”,基数越大,区分度越好。基数由采样统计得出。

如果统计信息不对导致行数和实际情况差距较大,可以使用analyze table 表名 来重新统计索引信息

索引选择异常和处理

由于索引统计信息不准确导致的问题,可以用analyze table来解决,其它优化器误判的解决方法如下:

  • 使用force index强行选择索引。缺点是变更不及时,开发通常不写force index,当生产环境出现问题,再修改需要重新测试和发布
  • 修改语句,引导MySQL使用我们期望的索引。缺点是需要根据数据特征进行修改,不具备通用性
  • 新建更合适的索引或删掉误用的索引。缺点是找到更合适的索引比较困难

怎么给字符串字段加索引

可以给字符串字段建立一个普通索引,也可以给字符串前缀建立普通索引。使用前缀索引,定义好长度,就可以既节省空间,又不用额外增加太多查询成本

可以通过统计索引上有多少个不同的值来判断使用多长的前缀,不同值越多,区分度越高,查询性能越好

首先算出这列有多少不同值

mysql> select count(distinct email) as L from SUser;

然后选取不同长度的前缀来看这个值

mysql> select   count(distinct left(email,4))as L4,  count(distinct left(email,5))as L5,  count(distinct left(email,6))as L6,  count(distinct left(email,7))as L7,from SUser;

前缀索引对覆盖索引的影响

前缀索引可能会增加扫描行数,导致影响性能,还可能导致用不上覆盖索引对查询的优化。

前缀索引的叶子节点只包含主键,如果查询字段不仅仅有主键,那必须回表。而用完整字符串做索引,如果查询字段只有主键和索引字段,那就不用回表

其它方式

对于邮箱来说,前缀索引的效果不错。
但是对于身份证来说,可能需要长度12以上的前缀索引,才能满足区分度要求,但是这样又太占空间了

有一些占用空间更小但是查询效率相同的方法:

  1. 倒序存储身份证号,建立长度为6的前缀索引,身份证后6位可以提供足够的区分度
  2. 加个身份证的整型hash字段,给这个字段加索引

这两种方法的相同点是都不支持范围查询,区别在于:

  1. 倒序存储不占额外空间
  2. 倒序每次写和读都需要额外调用一次reverse函数,hash字段需要额外调用一次crc32函数,reverse稍优
  3. hash字段的查询性能更稳定一些

为什么MySQL为“抖”一下

“抖”:SQL语句偶尔会执行特别慢,且随机出现,持续时间很短

当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。

平时执行很快的更新操作,其实就是在写内存和日志,“抖”可能是在刷脏页(flush),情况有以下几种:

  1. redo log满了,系统会停止所有更新操作,把checkpoint往前推进,原位置和新位置之间的所有脏页都flush到磁盘上。尽量避免这种情况,会阻塞更新操作
  2. 系统内存不足,淘汰脏页。尽量避免一个查询要淘汰的脏页太多
  3. 系统空闲
  4. 正常关闭

InnoDB刷脏页的控制策略

使用fio测试磁盘的IOPS,并把innodb_io_capacity设置成这个值,告诉InnoDB全力刷盘可以刷多快

fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest

不能总是全力刷盘,InnoDB刷盘速度还要参考内存脏页比例和redo log写盘速度

图 10

脏页比例不要经常接近75%,查看命令如下:

mysql> select VARIABLE_VALUE into @a from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty';select VARIABLE_VALUE into @b from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_total';select @a/@b;

还有个策略是刷盘的“连坐”机制:脏页的邻居如果是脏页会一起被刷盘。这种策略对机械硬盘有大幅度性能提升,但是SSD的IOPS已不是瓶颈,推荐innodb_flush_neighbors设置成0,只刷自己

为什么表数据删掉一半,表文件大小不变

InnoDB表包含两部分:表结构定义和数据。MySQL 8.0 之前,表结构是存在以.frm为后缀的文件,MySQL 8.0 允许表结构定义放在系统数据表中

参数innodb_file_per_table

设置成OFF表示,表的数据放在系统共享表空间,也就是跟数据字典放在一起;
设置成ON表示,每个InnoDB表数据存储在一个以.ibd为后缀的文件中

从MySQL 5.6.6开始默认就是ON

数据删除流程

InnoDB里的数据都是用B+数的结构组织的。

图 11

记录的复用:删除R4记录时,InnoDB会把记录标记为删除,插入ID在300到600之间的记录时可能会复用这个位置,磁盘文件大小不会缩小

数据页的复用:InnoDB的数据是按页存储的。如果将page A上所有记录删除以后,page A会被标记为可复用,这时候插入ID=50的记录需要使用新页时,page A会被复用。因此,delete整个表会把所有数据页都标记为可复用,但是磁盘文件不会变小

可以复用,而没被使用的空间,看起来就像是“空洞”,不只是删除数据会造成空洞,随机插入数据会引发索引的数据页分裂,导致空洞。更新索引上的值,可以理解为删除旧值和插入新值,也会造成空洞。解决空洞的方法是重建表

重建表

可以使用alter table A engine=InnoDB命令来重建表。MySQL 5.6是离线重建,重建期间更新会丢失。

图 39
MySQL 5.6 引入了Online DDL,重建表的流程:

  1. 建立一个临时文件,扫描表A主键的所有数据页
  2. 用数据页中表A的记录生成B+数,存储到临时文件中
  3. 生成临时文件的过程中,将所有对A的操作记录在一个日志文件(row log)中,对应图中state2状态
  4. 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表A相同的数据文件,对应图中state3状态
  5. 用临时文件替换表A的数据文件

alter语句在启动时需要获取MDL写锁,这个写锁在真正拷贝数据之前就退化成读锁了,目的是实现Online,MDL读锁不会阻塞记录的增删改操作(DML)

推荐使用gh-ost做大表的重建

Online 和 inplace

inplace是指整个DDL过程在 InnoDB 内部完成,对于 Server层来说,没有把数据挪动到临时表,这是一个“原地”操作,这就是inplace名称由来

和inplace对应的是copy,也就是前面离线重建

DDL过程如果是 Online 的,就一定是inplace的;反过来未必,全文索引和空间索引是 inplace 的,但不是 Online 的

optimize table、analyze table和alter table三种方式重建表的区别:

  • 从MySQL 5.6开始,alter table t engine=InnoDB(也就是recreate)默认就是上面引入Online DDL后的重建过程
  • analyze table t 不是重建表,只是对表的索引信息做重新统计,没有修改数据,这个过程中加了MDL读锁
  • optimize table t 等于recreate+analyze

count(*)慢该怎么办

count(*)的实现方式

InnoDB count(*)会遍历全表,优化器会找到最小的索引数进行计数,结果准确但有性能问题。show table status命令显示的行数是采样估算的,不准确

用缓存系统保存计数

可以用Redis来保存记录数,但是会出现逻辑上不精确的问题。根本原因是这两个不同的存储构成的系统,不支持分布式事务,无法拿到精确一致的视图

图 12
这种情况是Redis的计数不精确

图 13
这种情况是查询结果不精确

在数据库保存计数

将计数放在数据库里单独的一张计数表中,可以利用事务解决计数不精确的问题

图 14

在会话B读操作期间,会话A还没提交事务,因此B没有看到计数值加1的操作,因此计数值和“最近100条记录”的结果在逻辑上是一致的

不同的count用法

count(*)、count(主键id)和count(1) 都表示返回满足条件的结果集的总行数。count(字段),则表示返回满足条件的数据行里面,参数“字段”不为NULL的总个数

对于count(主键id)来说,InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为空的,就按行累加。

对于count(1)来说,InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。

count(1)执行得要比count(主键id)快。因为从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。

对于count(字段)来说:

如果这个“字段”是定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加;

如果这个“字段”定义允许为null,那么执行的时候,判断到有可能是null,还要把值取出来再判断一下,不是null才累加。

count(*) 是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count(*)肯定不是null,按行累加。

结论是:按照效率排序的话,
count(字段) < count(主键id) < count(1) ≈ count(*),所以尽量使用count(*)。

orderby是怎么工作的

假设有SQL语句

select city,name,age from t where city='杭州' order by name limit 1000;

全字段排序

图 15

如果要排序的数据量小于sort_buffer_size,排序就在内存中完成,否则外部排序(归并)

rowid 排序

max_length_for_sort_data 是MySQL中专门控制用于排序的行数据的长度的参数,超过这个值就不会全字段排序,用rowid排序

图 16

全字段排序 VS rowid排序

如果内存够就用全字段排序,rowid排序回表多造成磁盘读,性能较差

并不是所有的order by语句都要排序的,如果建索引时就是有序的就不排

创建一个city和name的联合索引,查询过程如下:

图 17

还可以创建city、name和age的联合索引,这样就不用回表了

图 18

如何正确地显示随机消息

10000行记录如何随机选择3个

内存临时表

用order by rand()来实现这个逻辑

mysql> select word from words order by rand() limit 3;

R:随机数,W:单词,pos:rowid,对于有主键的表,rowid就是主键ID,没有主键就由系统生成

原表->内存临时表:扫描10000行
内存临时表->sort_buffer:扫描10000行
内存临时表->结果集:访问3行

图 19

order by rand()使用了内存临时表,内存临时表排序的时候使用了rowid排序方法

磁盘临时表

当内存临时表大小超过了tmp_table_size时,如果使用归并排序,内存临时表会转为磁盘临时表,如果使用优先队列排序(排序+limit操作),且维护的堆大小不超过sort_buffer_size,则不会转为磁盘临时表

随机排序方法

  1. 取得整个表的行数,记为C
  2. 取得 Y = floor(C * rand())
  3. 再用 limit Y,1 取得一行

取多个随机行就重复多次这个算法

mysql> select count(*) into @C from t;set @Y1 = floor(@C * rand());set @Y2 = floor(@C * rand());set @Y3 = floor(@C * rand());select * from t limit @Y11//在应用代码里面取Y1、Y2、Y3值,拼出SQL后执行select * from t limit @Y21select * from t limit @Y31

或者优化一下,Y1,Y2,Y3从小到大排序,这样扫描的行数就是Y3

id1 = select * from t limit @Y1,1;id2 = select * from t where id > id1 limit @Y2-@Y1,1;id3 = select * from t where id > id2 limit @Y3

为什么逻辑相同的SQL语句性能差异巨大

  1. 对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器决定放弃走树搜索功能,但不是放弃索引,优化器可以选择遍历索引
  2. 隐式类型转换可能会触发上面的规则1
  3. 隐式字符编码转换也可能触发上面的规则1

为什么只查一行的语句也执行这么慢

查询长时间不返回

  1. 等MDL锁。通过查询sys.schema_table_lock_waits,可以找出造成阻塞的process id,把这个连接用kill杀掉
  2. 等flush。可能情况是有一个flush tables命令被别的语句堵住了,然后它又堵住了查询语句,可以用show processlist 查出并杀掉阻塞flush的连接
  3. 等行锁。通过查询sys.innodb_lock_waits 杀掉对应连接

查询慢

  1. 查询字段没有索引,走了全表扫描
  2. 事务隔离级别为可重复读,当前事务看不到别的事务的修改,但是别的事务执行了多次修改,当前事务在查询时要根据undo log查询到应该看到的值

幻读

幻读:一个事务在前后两次查询同一个范围时,后一次查询看到了前一次查询没有看到的行

  1. 在可重复读隔离级别下,普通的查询时快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现
  2. 幻读专指“新插入的行”

幻读的问题

  • 语义被破坏

    图 20

    session A在T1时刻声明了,“我要把所有d=5的行锁住,不准别的事务进行读写操作”。session B和C破坏了这个语义

  • 数据不一致。根据binlog克隆的库与主库不一致,原因是即使给所有记录都加上锁,新记录还是没上锁

解决幻读

间隙锁:锁住两行之间的间隙

在行扫描过程中,不仅给行加行锁,还给行间的间隙上锁

跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。

间隙锁和行锁合称next-key lock,左开右闭

间隙锁的引入,可能会导致同样的语句锁住更大的范围,影响并发度。

间隙锁只在可重复读隔离级别下才会生效

为什么只改一行的语句,锁这么多

加锁规则(可重复读隔离级别):

  1. 原则1:加锁的基本单位是next-key lock
  2. 原则2:查找过程中访问到的对象才会加锁
  3. 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁
  4. 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁
  5. 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止

假设有如下SQL语句

CREATE TABLE `t` (  `id` int(11) NOT NULL,  `c` int(11) DEFAULT NULL,  `d` int(11) DEFAULT NULL,  PRIMARY KEY (`id`),  KEY `c` (`c`)) ENGINE=InnoDB;insert into t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);

案例一:等值查询间隙锁

图 21

由于表t中没有id=7的记录

  1. 根据原则1,加锁单位是next-key lock,session A加锁范围就是(5,10];
  2. 同时根据优化2,这是一个等值查询(id=7),而id=10不满足查询条件,next-key lock退化成间隙锁,因此最终加锁的范围是(5,10)。

所以,session B要往这个间隙里面插入id=8的记录会被锁住,但是session C修改id=10这行是可以的。

案例二:非唯一索引等值锁

图 22

这里session A要给索引c上c=5的这一行加上读锁。

  1. 根据原则1,加锁单位是next-key lock,因此会给(0,5]加上next-key lock
  2. 要注意c是普通索引,因此仅访问c=5这一条记录是不能马上停下来的,需要向右遍历,查到c=10才放弃。根据原则2,访问到的都要加锁,因此要给(5,10]加next-key lock。
  3. 但是同时这个符合优化2:等值判断,向右遍历,最后一个值不满足c=5这个等值条件,因此退化成间隙锁(5,10)
  4. 根据原则2 ,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么session B的update语句可以执行完成。

但session C要插入一个(7,7,7)的记录,就会被session A的间隙锁(5,10)锁住。

需要注意,在这个例子中,lock in share mode只锁覆盖索引,但是如果是for update就不一样了。 执行 for update时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。

这个例子说明,锁是加在索引上的;同时,它给我们的指导是,如果你要用lock in share mode来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段。比如,将session A的查询语句改成select d from t where c=5 lock in share mode

案例三:主键索引范围锁

mysql> select * from t where id=10 for update;mysql> select * from t where id>=10 and id<11 for update;

这两条语句在逻辑上是等价的,但是加锁规则不一样

  1. 开始执行的时候,要找到第一个id=10的行,因此本该是next-key lock(5,10]。 根据优化1, 主键id上的等值条件,退化成行锁,只加了id=10这一行的行锁
  2. 范围查找就往后继续找,找到id=15这一行停下来,因此需要加next-key lock(10,15]

所以,session A这时候锁的范围就是主键索引上,行锁id=10和next-key lock(10,15]

需要注意一点,首次session A定位查找id=10的行的时候,是当做等值查询来判断的,而向右扫描到id=15的时候,用的是范围查询判断

案例四:非唯一索引范围锁

图 23

这次session A用字段c来判断,加锁规则跟主键索引范围锁的唯一不同是:在第一次用c=10定位记录的时候,索引c上加了(5,10]这个next-key lock后,由于索引c是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终sesion A加的锁是,索引c上的(5,10] 和(10,15] 这两个next-key lock

案例五:唯一索引范围锁bug

图 24

session A是一个范围查询,按照原则1的话,应该是索引id上只加(10,15]这个next-key lock,并且因为id是唯一键,所以循环判断到id=15这一行就应该停止了。

但是实现上,InnoDB会往前扫描到第一个不满足条件的行为止,也就是id=20。而且由于这是个范围扫描,因此索引id上的(15,20]这个next-key lock也会被锁上。

案例六:非唯一索引上存在“等值”的例子

现在插入一条新记录

mysql> insert into t values(30,10,30);

图 25

delete语句加锁的逻辑和 select … for update是类似的,session A在遍历的时候,先访问第一个c=10的记录。同样地,根据原则1,这里加的是(c=5,id=5)到(c=10,id=10)这个next-key lock。

然后,session A向右查找,直到碰到(c=15,id=15)这一行,循环才结束。根据优化2,这是一个等值查询,向右查找到了不满足条件的行,所以会退化成(c=10,id=10) 到 (c=15,id=15)的间隙锁。

也就是说,这个delete语句在索引c上的加锁范围,就是下图中蓝色区域覆盖的部分。蓝色区域左右两边都是虚线,表示开区间,即(c=5,id=5)和(c=15,id=15)这两行上都没有锁。

图 26

案例七:limit 语句加锁

图 27

session A的delete语句加了 limit 2。你知道表t里c=10的记录其实只有两条,因此加不加limit 2,删除的效果都是一样的,但是加锁的效果却不同。可以看到,session B的insert语句执行通过了,跟案例六的结果不同。这是因为,案例七里的delete语句明确加了limit 2的限制,因此在遍历到(c=10, id=30)这一行之后,满足条件的语句已经有两条,循环就结束了。

因此,索引c上的加锁范围就变成了从(c=5,id=5)到(c=10,id=30)这个前开后闭区间,如下图所示:
图 28

这个例子对我们实践的指导意义就是,在删除数据的时候尽量加limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。

案例八:一个死锁的例子

本案例目的是说明:next-key lock 实际上是间隙锁和行锁加起来的结果

图 29

  1. session A 启动事务后执行查询语句加lock in share mode,在索引c上加了next-key lock(5,10] 和间隙锁(10,15);
  2. session B 的update语句也要在索引c上加next-key lock(5,10] ,进入锁等待;
  3. 然后session A要再插入(8,8,8)这一行,被session B的间隙锁锁住。由于出现了死锁,InnoDB让session B回滚

你可能会问,session B的next-key lock不是还没申请成功吗?

其实是这样的,session B的“加next-key lock(5,10] ”操作,实际上分成了两步,先是加(5,10)的间隙锁,加锁成功;然后加c=10的行锁,这时候才被锁住的。

也就是说,我们在分析加锁规则的时候可以用next-key lock来分析。但是要知道,具体执行的时候,是要分成间隙锁和行锁两段来执行的。

“饮鸩止渴”提高性能的方法

短连接风暴

短连接模式就是连接到数据库后,执行很少的SQL语句就断开,下次需要的时候再重连,在业务高峰期,会出现连接数暴涨的情况

两种有损业务的解决方法:

  1. 处理掉占着连接但是不工作的线程。优先断开事务外空闲太久的连接
  2. 减少连接过程的损耗。关闭权限验证

慢查询性能问题

引发慢查询的情况有三种:

  1. 索引没有设计好。最高效的解决方法是直接alter table建索引
  2. SQL语句没有写好,导致没用上索引。解决方法是使用query_rewrite重写SQL语句
  3. MySQL选错了索引。应急方案是给语句加上force index或者使用query_rewrite重写语句加上force index

出现情况最多的是前两种,通过下面过程可以预先发现和避免

  1. 上线前,在测试环境,把慢查询日志(slow log)打开,并且把long_query_time设置成0,确保每个语句都会被记录入慢查询日志
  2. 在测试表里插入模拟线上的数据,做一遍回归测试
  3. 观察慢查询日志里每类语句的输出,特别留意Rows_examined字段是否与预期一致

QPS突增问题

  1. 业务bug导致。可以把这个功能的SQL从白名单去掉
  2. 如果新功能使用的是单独的数据库用户,可以用管理员账号把这个用户删掉,然后断开连接
  3. 用query_rewrite把压力最大的SQL语句直接重写成"select 1"返回

方法3存在两个副作用:

  1. 如果别的功能也用到了这个SQL语句就会误伤
  2. 该语句可能是业务逻辑的一部分,导致业务逻辑一起失败

方法3是优先级最低的方法。方法1和2依赖于规范的运维体系:虚拟化、白名单机制、业务账号分离

MySQL是怎么保证数据不丢的

只要redo log和binlog保证持久化到磁盘,就能确保MySQL异常重启后,数据可以恢复

binlog的写入机制

binlog的写入逻辑:事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中

一个事务的binlog是不能被拆开的,因此不论这个事务多大,也要确保一次性写入

系统给binlog cache分配了一片内存,每个线程一个,参数binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘

事务提交的时候,执行器把binlog cache里的完整事务写入到binlog中,并清空binlog cache
图 1

  • 图中的write,指的是把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快
  • 图中的fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为fsync才占磁盘的IOPS

write和fsync的时机由参数sync_binlog控制:

  1. sync_binlog=0,表示每次提交事务都只write,不fsync
  2. sync_binlog=1,表示每次提交事务都会fsync
  3. sync_binlog=N(N>1),表示每次提交事务都write,但累积N个事务后才fsync

sync_binlog设置成N可以改善IO瓶颈场景的性能,但对应的风险是:如果主机发生异常重启,会丢失最近N个事务的binlog

redo log的写入机制

事务执行过程中,生成的redo log要先写到redo log buffer,但不是每次生成后都要直接持久化到磁盘,因为事务没提交,日志丢了也不会有损失。
但是也有可能事务没有提交,redo log buffer 中的部分日志持久化到了磁盘。下图是redo log的三种状态

图 2

日志写到redo log buffer是很快的,write到page cache也快,但是持久化到磁盘就很慢。

InnoDB提供了innodb_flush_log_at_trx_commit参数来控制redo log的写入策略:

  1. 设置为0表示每次事务提交时都只是把redo log 留在redo log buffer中
  2. 设置为1表示每次事务提交时都将redo log直接持久化到磁盘
  3. 设置为2表示每次事务提交时都只是把redo log写到page cache

InnoDB有个后台线程,每隔1秒,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘

注意,事务执行过程中的redo log也是直接写在redo log buffer中的,这些redo log也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的redo log,也可能已经持久化到磁盘

除了后台线程每秒一次的轮询操作外,还有两个场景会让没提交的事务的redo log写入到磁盘

  1. redo log buffer占用的空间即将达到 innodb_log_buffer_size一半的时候,后台线程会主动write到page cache
  2. 并行的事务提交的时候,顺带将这个事务的redo log buffer持久化到磁盘

两阶段提交的过程,时序上redo log先prepare,再写binlog,最后再把redo log commit

如果innodb_flush_log_at_trx_commit设置为1,那么redo log在prepare阶段就要持久化一次,因为有一个崩溃恢复逻辑是prepare的redo log + 完整的binlog

每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB就认为redo log在commit的时候就不需要fsync了,只会write到文件系统的page cache中就够了

通常我们说MySQL的“双1”配置,指的就是sync_binlog和innodb_flush_log_at_trx_commit都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare 阶段),一次是binlog

redo log组提交

日志逻辑序列号LSN:LSN单调递增,用来对应redo log的一个个写入点。每次写入长度为length的redo log,LSN的值就会加上length

如下图所示,是三个并发事务在prepare阶段,都写完redo log buffer,持久化到磁盘的过程中
图 3

从图中可以看到,

  1. trx1是第一个到达的,会被选为这组的leader
  2. 等trx1要开始写盘的时候,这个组里面已经有3个事务了,这时候LSN也变成了160
  3. trx1去写盘的时候,带的就是LSN=160,因此等trx1返回时,所有LSN小于等于160的redo log,都已经被持久化到磁盘
  4. 这时候trx2和trx3就可以直接返回了

所以,一次组提交里面,组员越多,节约磁盘IOPS的效果越好。但如果只有单线程压测,那就只能老老实实地一个事务对应一次持久化操作了。

在并发更新场景下,第一个事务写完redo log buffer以后,接下来这个fsync越晚调用,组员可能越多,节约IOPS的效果就越好。

MySQL为了让组提交的效果更好,细化了两阶段提及的顺序,让redo log的fsync往后拖

图 4

上图的顺序说明binlog也可以组提交,但是通常情况下步骤3会执行得很快,所以能集合到一起持久化的binlog比较少。可以通过设置binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count来提升binlog组提交的效果

性能瓶颈在IO的提升方法

  1. 设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count参数,减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。
  2. 将sync_binlog 设置为大于1的值(比较常见是100~1000)。这样做的风险是,主机掉电时会丢binlog日志。
  3. 将innodb_flush_log_at_trx_commit设置为2。这样做的风险是,主机掉电的时候会丢数据。

MySQL是怎么保证主备一致的

MySQL的主备一致依赖于binlog

MySQL主备的基本原理

主备切换流程

图 5

客户端的读写是直接访问主库,备库同步主库的更新,与主库保持一致。虽然备库不会被客户端访问,但仍推荐设置成只读模式,因为:

  1. 有时候一些运营类的查询语句会放到备库上去查,设置为只读可以防止误操作
  2. 防止切换逻辑有bug,比如切换过程中出现双写,造成主备不一致
  3. 可以用readonly状态来判断节点的角色

备库的只读对超级权限用户是无效的,用于同步更新的线程拥有超级权限

同步流程

主库的更新语句同步到备库的完成流程图如下

图 6

备库B跟主库A之间维持了一个长连接。主库A内部有一个线程,专门用于服务备库B的这个长连接。一个事务日志同步的完整过程如下:

  1. 在备库B上通过change master命令,设置主库A的IP、端口、用户名、密码,以及要从哪个位置开始请求binlog,这个位置包含文件名和日志偏移量
  2. 在备库B上执行start slave命令,这时候备库会启动两个线程,就是图中的io_thread和sql_thread。其中io_thread负责与主库建立连接。
  3. 主库A校验完用户名、密码后,开始按照备库B传过来的位置,从本地读取binlog,发给B
  4. 备库B拿到binlog后,写到本地文件,称为中转日志(relay log)
  5. sql_thread读取中转日志,解析出日志里的命令,并执行

binlog的三种格式对比

binlog有三种格式,statement、row以及前两种格式的混合mixed

假设有如下表:

mysql> CREATE TABLE `t` (  `id` int(11) NOT NULL,  `a` int(11) DEFAULT NULL,  `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,  PRIMARY KEY (`id`),  KEY `a` (`a`),  KEY `t_modified`(`t_modified`)) ENGINE=InnoDB;insert into t values(1,1,'2018-11-13');insert into t values(2,2,'2018-11-12');insert into t values(3,3,'2018-11-11');insert into t values(4,4,'2018-11-10');insert into t values(5,5,'2018-11-09');

statement格式就是SQL语句原文
图 7

下图是该语句执行效果
图 8

statement格式下,delete 带 limit,很可能出现主备数据不一致的情况,比如上面的例子:

  1. 如果delete语句使用的是索引a,那么会根据索引a找到第一个满足条件的行,也就是说删除的是a=4这一行
  2. 但如果使用的是索引t_modified,那么删除的就是 t_modified='2018-11-09’ 也就是a=5这一行。

row格式binlog如下
图 9

row格式的binlog把SQL语句替换成了两个event:Table_map和Delete_rows

  1. Table_map event,用于说明接下来要操作的表是test库的表t;
  2. Delete_rows event,用于定义删除的行为。

借助mysqlbinlog工具查看详细的binlog

图 10

当binlog_format使用row格式的时候,binlog里面记录了真实删除行的主键id,这样binlog传到备库去的时候,就肯定会删除id=4的行,不会有主备删除不同行的问题。

mixed格式吸收了statement和row格式的优点,占用空间小,避免了数据不一致

但是现在binlog设置成row的场景更多,理由有很多,其中之一是恢复数据。

如果执行的是delete语句,row格式的binlog也会把被删掉的行的整行信息保存起来。所以,如果你在执行完一条delete语句以后,发现删错数据了,可以直接把binlog中记录的delete语句转成insert,把被错删的数据插入回去就可以恢复了

如果你是执行错了insert语句呢?那就更直接了。row格式下,insert语句的binlog里会记录所有的字段信息,这些信息可以用来精确定位刚刚被插入的那一行。这时,你直接把insert语句转成delete语句,删除掉这被误插入的一行数据就可以了。

如果执行的是update语句的话,binlog里面会记录修改前整行的数据和修改后的整行数据。所以,如果你误执行了update语句的话,只需要把这个event前后的两行信息对调一下,再去数据库里面执行,就能恢复这个更新操作了

循环复制问题

binlog的特性确保了主备一致性。实际生产上使用比较多的是双M结构
图 11

双M结构中,节点A和B之间总是互为主备关系,在切换的时候就不用再修改主备关系

循环复制指的是A节点更新完,把binlog发给B,B更新完又生成binlog发给了A,解决循环复制的方法如下:

  1. 规定两个库的server id必须不同,如果相同,则它们之间不能设定为主备关系
  2. 一个备库接到binlog并在重放的过程中,生成与原binlog的server id相同的新的binlog
  3. 每个库在收到从自己的主库发过来的日志后,先判断server id,如果跟自己的相同,表示这个日志是自己生成的,直接丢弃

因此,双M结构的日志执行流会变成这样:

  1. 从节点A更新的事务,binlog里面记得都是A的server id
  2. 传到节点B执行一次以后,节点B生成的binlog的server id也是A的server id
  3. 再传回给节点A,A判断到这个server id与自己的相同,就不会再处理这个日志。所以,死循环这里就断掉了

MySQL是怎么保证高可用的

正常情况下,只要主库执行更新生成的所有binlog,都可以传到备库并被正确地执行,备库就能达到跟主库一致的状态,这就是最终一致性。但是MySQL要提供高可用,只有最终一致性是不够的

主备延迟

与数据同步有关的时间点主要包括以下三个:

T1:主库A执行完成一个事务,写入binlog
T2:备库B接收完这个binlog
T3:备库B执行完成这个事务

主备延迟:同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是T3-T1

seconds_behind_master表示备库延迟了多少秒

网络正常情况下,主备延迟的主要因素是T3-T2,直接表现是备库消费中转日志(relay log)的速度比主库生产binlog的速度慢

主备延迟的来源

  1. 备库的机器性能差。解决方法是对称部署
  2. 备库的压力大。有些统计查询语句只在备库上跑,导致备库压力大,解决方法是一主多从分担读的压力或者把binlog输送到Hadoop来提供统计查询能力
  3. 大事务。比如一次性用delete删除太多数据或者大表DDL
  4. 备库的并行复制能力

由于主备延迟的存在,所以在主备切换的时候,有不同的策略

可靠性优先策略

在双M结构下,主备切换流程如下:

  1. 判断备库B现在的seconds_behind_master,如果小于某个值(比如5秒)继续下一步,否则持续重试这一步;
  2. 把主库A改成只读状态,即把readonly设置为true;
  3. 判断备库B的seconds_behind_master的值,直到这个值变成0为止;
  4. 把备库B改成可读写状态,也就是把readonly 设置为false;
  5. 把业务请求切到备库B。

图 12

步骤2直到步骤5,主库A和备库B都处于readonly状态,系统不可用(不可写)

可用性优先策略

把上面策略里的步骤4和5放到最开始执行,代价是可能出现数据不一致的情况

一般情况下,可靠性优于可用性。在满足数据可靠性的前提下,MySQL高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。

备库为什么会延迟好几个小时

当备库执行日志的速度持续低于主库生成日志的速度,那这个延迟就有可能成了小时级别

MySQL 5.6之前,备库应用日志更新数据只能使用单线程,在主库并发高、TPS高时会出现严重的主备延迟问题

按表分发策略

按表分发事务的基本思路是,如果两个事务更新不同的表,它们就可以并行

图 13

worker线程维护一张执行队列里的事务涉及的表,key是“库名.表名”,value表示队列中有多少个事务修改这个表

事务在分发的时候,和所有worker的冲突关系有3种:

  1. 如果跟所有worker都不冲突,coordinator线程就会把这个事务分配给最空闲的woker;
  2. 如果跟多于一个worker冲突,coordinator线程就进入等待状态,直到和这个事务存在冲突关系的worker只剩下1个;
  3. 如果只跟一个worker冲突,coordinator线程就会把这个事务分配给这个存在冲突关系的worker。

按表分发方案在多个表负载均衡的场景效果很好。但是碰到热点表会退化成单线程复制

按行分发策略

要解决热点表的并行复制问题,就需要一个按行并行复制方案。核心思路是:如果两个事务没有更新相同的行,它们在备库上可以并行执行。这个模式要求binlog格式必须是row

判断事务和worker是否冲突,用的规则不是“修改同一个表”,而是“修改同一行”。worker维护的hash表的key是“库名+表名+唯一索引的名字+唯一索引的值”

按行分发策略比按表分发策略需要消耗更多的计算资源,这两种方案都有一样的约束条件:

  1. 要能够从binlog里面解析出表名、主键值和唯一索引的值。也就是说,主库的binlog格式必须是row;
  2. 表必须有主键;
  3. 不能有外键。表上如果有外键,级联更新的行不会记录在binlog中,这样冲突检测就不准确。

MySQL 5.6版本的并行复制策略

官方MySQL 5.6版本支持的并行复制的力度是按库并行。hash表的key是数据库名

相比于按表和按行分发,有两个优势:

  1. 构造hash值的时候很快,只需要库名;而且一个实例上DB数也不会很多,不会出现需要构造100万个项这种情况
  2. 不要求binlog的格式。因为statement格式的binlog也可以很容易拿到库名

MariaDB的并行复制策略

MariaDB的并行复制策略利用了redo log组提交优化的特性:

  1. 能够在同一组里提交的事务,一定不会修改同一行
  2. 主库上可以并行执行的事务,备库上也一定可以并行执行

这个策略的目标是“模拟主库的并行模式”,但它没有实现“真正的模拟主库并发度”这个目标。在主库上,一组事务在commit的时候,下一组事务是同时处于“执行中”状态的

MySQL 5.7的并行复制策略

由参数slave-parallel-type来控制并行复制策略:

  1. 配置为DATABASE,表示使用MySQL 5.6版本的按库并行策略;
  2. 配置为 LOGICAL_CLOCK,表示的就是优化过的类似MariaDB的策略

该优化的策略的思想是:

  1. 同时处于prepare状态的事务,在备库执行时是可以并行的;
  2. 处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也是可以并行的。

MySQL 5.7.22的并行复制策略

新增了一个并行复制策略,基于WRITESET的并行复制。

新增参数binlog-transaction-dependency-tracking,用来控制是否启用这个新策略:

  1. COMMIT_ORDER,表示的就是前面介绍的,根据同时进入prepare和commit来判断是否可以并行的策略。
  2. WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的hash值,组成集合writeset。如果两个事务没有操作相同的行,也就是说它们的writeset没有交集,就可以并行
  3. WRITESET_SESSION,是在WRITESET的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。

该策略类似按行分发,但是有很大优势:

  1. writeset是在主库生成后直接写入到binlog里面的,这样在备库执行的时候,不需要解析binlog内容(event里的行数据),节省了很多计算量;
  2. 不需要把整个事务的binlog都扫一遍才能决定分发到哪个worker,更省内存;
  3. 由于备库的分发策略不依赖于binlog内容,所以binlog是statement格式也是可以的。

该方案对于“表上没主键”和“外键约束”的场景,也会暂时退化为单线程模型。

主库出问题了,从库怎么办

大多数互联网应用场景都是读多写少,要解决读性能问题,就要涉及:一主多从

图 14

图中,虚线箭头表示的是主备关系,也就是A和A’互为主备, 从库B、C、D指向的是主库A。一主多从的设置,一般用于读写分离,主库负责所有的写入和一部分读,其他的读请求则由从库分担。

下面讨论,在一主多从架构下,主库故障后的主备切换问题

图 15

相比于一主一备的切换流程,一主多从结构在切换完成后,A’会成为新的主库,从库B、C、D也要改接到A’。正是由于多了从库B、C、D重新指向的这个过程,所以主备切换的复杂性也相应增加了

基于位点的主备切换

节点B设置成节点A’ 的从库的时候,需要执行change master命令,必须设置主库的日志文件名和偏移量。A和A’的位点是不同的,从库B切换时需要先经过“找同步位点”这个逻辑

同步位点很难精确取到

取同步位点的方法如下:

  1. 等待新主库A’ 把中转日志(relay log)全部同步完成
  2. 在A’ 上执行show master status命令,得到当前A’ 上最新的 File 和 Position
  3. 取原主库A故障的时刻T
  4. 用mysqlbinlog工具解析A’的File,得到T时刻的位点
mysqlbinlog File --stop-datetime=T --start-datetime=T

图 16

图中,end_log_pos后面的值“123”,表示的就是A’这个实例,在T时刻写入新的binlog的位置,可以把这个值作为$master_log_pos ,用在节点B的change master命令里

这个值并不精确,从库B的同步线程可能会出错,解决方法如下:

  1. 通过sql_slave_skip_counter跳过出错事务
  2. 设置slave_skip_errors,跳过指定错误,通常设置成1032,1062,对应的错误是删除数据找不到行,插入数据唯一键冲突

GTID

前两种方式操作复杂,容易出错,MySQL 5.6 引入了GITD。

GTID全称是Global Transaction Identifier,也就是全局事务ID,是一个事务在提交的时候生成的,是这个事务的唯一标识,格式是:

GTID=server_uuid:gno
  • server_uuid是一个实例第一次启动时自动生成的,是一个全局唯一的值
  • gno是一个整数,初始值是1,每次提交事务的时候分配给这个事务,并加1

GTID有两种生成方式:

  1. 如果gtid_next=automatic,代表使用默认值。这时,MySQL就会把server_uuid:gno分配给这个事务。
    a. 记录binlog的时候,先记录一行 SET @@SESSION.GTID_NEXT=‘server_uuid:gno’;
    b. 把这个GTID加入本实例的GTID集合

  2. 如果gtid_next是一个指定的GTID的值,比如通过set gtid_next='current_gtid’指定为current_gtid,那么就有两种可能:
    a. 如果current_gtid已经存在于实例的GTID集合中,接下来执行的这个事务会直接被系统忽略;
    b. 如果current_gtid没有存在于实例的GTID集合中,就将这个current_gtid分配给接下来要执行的事务,也就是说系统不需要给这个事务生成新的GTID,因此gno也不用加1

每个MySQL实例都维护了一个GTID集合,用来对应“这个实例执行过的所有事务”

当从库需要跳过某个事务时,在主库上查出GTID,在从库上提交空事务,把这个GTID加入到从库的GTID集合中

set gtid_next='aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10';begin;commit;set gtid_next=automatic;start slave;

基于GTID的主备切换

切换命令指定master_auto_position=1表示这个主备关系使用的是GTID协议,不需要指定主库日志文件和偏移量

我们把A’ 的GTID集合记为set_a,实例B的GTID集合记为set_b,切换流程如下:

  1. 实例B指定主库A’,基于主备协议建立连接。
  2. 实例B把set_b发给主库A’
  3. 实例A’算出set_a与set_b的差集,也就是所有存在于set_a,但是不存在于set_b的GITD的集合,判断A’本地是否包含了这个差集需要的所有binlog事务。
    a. 如果不包含,表示A’已经把实例B需要的binlog给删掉了,直接返回错误;
    b. 如果确认全部包含,A’从自己的binlog文件里面,找出第一个不在set_b的事务,发给B;
  4. 之后就从这个事务开始,往后读文件,按顺序取binlog发给B去执行

GTID和在线DDL

假设,这两个互为主备关系的库还是实例X和实例Y,且当前主库是X,并且都打开了GTID模式。这时的主备切换流程可以变成下面这样:

  • 在实例X上执行stop slave。

  • 在实例Y上执行DDL语句。注意,这里并不需要关闭binlog。

  • 执行完成后,查出这个DDL语句对应的GTID,并记为 server_uuid_of_Y:gno。

  • 到实例X上执行以下语句序列:

    set GTID_NEXT="server_uuid_of_Y:gno";begin;commit;set gtid_next=automatic;start slave;

    这样做的目的在于,既可以让实例Y的更新有binlog记录,同时也可以确保不会在实例X上执行这条更新。

  • 接下来,执行完主备切换,然后照着上述流程再执行一遍即可

读写分离有哪些坑

由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态,这种现象称为“过期读”

过期读处理方案包括:

  • 强制走主库方案
  • sleep方案
  • 判断主备无延迟方案
  • 配合semi-sync方案
  • 等主库位点方案
  • 等GTID方案

强制走主库方案

该方案将查询请求分为两类:

  1. 对于必须要拿到最新结果的请求,强制将其发到主库上。比如,在一个交易平台上,卖家发布商品以后,马上要返回主页面,看商品是否发布成功。那么,这个请求需要拿到最新的结果,就必须走主库
  2. 对于可以读到旧数据的请求,才将其发到从库上。在这个交易平台上,买家来逛商铺页面,就算晚几秒看到最新发布的商品,也是可以接受的。那么,这类请求就可以走从库

这个方案用的最多,但是问题在于存在“所有查询都不能是过期读”的需求,比如金融类业务,那就必须放弃读写分离,所有读写压力都在主库

下面讨论的是:可以支持读写分离的场景下,有哪些解决过期读的方案

Sleep方案

主库更新后,读从库之前先sleep一下。这个方案假设,大多数情况下主备延迟在1s之内

该方案可以解决类似Ajax场景下的过期读问题。例如卖家发布商品,直接将卖家输入的内容作为新商品显示出来,并不查从库。等待卖家刷新页面,相当于sleep了一段时间,解决了过期读问题

该方案存在的问题是不精确:

  1. 如果查询请求本来0.5s就可以在从库上拿到正确结果,也会等到1s
  2. 如果延迟超过1s,还是会出现过期读

判断主备无延迟方案

有三种方法:

  1. 每次从库执行查询请求前,先判断seconds_behind_master是否已经等于0。如果还不等于0 ,那就必须等到这个参数变为0才能执行查询请求。
  2. 对比位点。如果Master_Log_File和Relay_Master_Log_File、Read_Master_Log_Pos和Exec_Master_Log_Pos这两组值完全相同表示主备无延迟
  3. 对比GITD。Retrieved_Gtid_Set和Executed_Gtid_Set相同表示是主备无延迟

该方案比Sleep更准确,方法2和3比1准确,但是不能说精确。因为存在客户端已经收到提交确认,而备库还没收到日志的状态,因此备库认为主备无延迟,从而发生过期读

配合semi-sync

为解决上面的问题,引入semi-sync replication:

semi-sync做了这样的设计:

  1. 事务提交的时候,主库把binlog发给从库
  2. 从库收到binlog以后,发回给主库一个ack
  3. 主库收到ack以后,才能给客户端返回“事务完成”的确认

开启semi-sync,就表示所有给客户端发送过确认的事务,都确保备库已经收到了这个日志

semi-sync+判断主备无延迟方案存在两个问题:

  1. 一主多从情况下,因为主库只要收到一个从库的ack就给客户端返回确认,其它未响应ack的从库可能会发生过期读问题
  2. 在业务高峰期,主库的位点或者GITD集合更新很快,这种情况下,可能出现从库一直存在主备延迟导致客户端查询一直等待

等主库位点方案

该方案解决了前面两个问题

命令:

select master_pos_wait(file, pos[, timeout]);

这条命令的逻辑如下:

  1. 它是在从库执行的
  2. 参数file和pos指的是主库上的文件名和位置
  3. timeout可选,设置为正整数N表示这个函数最多等待N秒

为了解决前面两个问题,流程如下:

  1. trx1事务更新完成后,马上执行show master status得到当前主库执行到的File和Position;
  2. 选定一个从库执行查询语句;
  3. 在从库上执行select master_pos_wait(File, Position, 1);
  4. 如果返回值是>=0的整数,则在这个从库执行查询语句;
  5. 否则,到主库执行查询语句。
    图 17

GTID方案

等GTID也可以解决前面两个问题

流程如下:

  1. trx1事务更新完成后,从返回包直接获取这个事务的GTID,记为gtid1;
  2. 选定一个从库执行查询语句;
  3. 在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);
  4. 如果返回值是0,则在这个从库执行查询语句;
  5. 否则,到主库执行查询语句。
    图 18

如何判断一个数据库是不是出问题了

select 1 判断

select 1 成功返回只能说明数据库进程还在,不能说明没问题

并发连接:通过show precesslist查询连接数,连接数可以远大于并发查询数量
并发查询:“当前正在执行”的语句的数量

线程进入锁等待后,并发线程的计数会减一,即进入锁等待的线程不吃CPU

假如设置并发线程数是3,下面的情况是A、B、C在并发查询,D先select 1不占并发线程数所以能正常返回,但实际上已经不能正常查询了
图 19

查表判断

为了能够检测InnoDB并发线程数过多导致的系统不可用情况,我们需要找一个访问InnoDB的场景。一般的做法是,在系统库(mysql库)里创建一个表,比如命名为health_check,里面只放一行数据,然后定期执行:

mysql> select * from mysql.health_check;

这种方法在磁盘空间满了就无效。因为更新事务要写binlog,而一旦binlog所在磁盘满了,那么所有更新语句都会堵住,但是系统仍然可以读数据

更新判断

我们把查询换成更新来作为监控语句。常见做法是放一个timestamp字段表示最后一次检测时间,这条更新语句类似于:

mysql> update mysql.health_check set t_modified=now();

主库和备库用同样的更新语句可能会出现行冲突,导致主备同步停止,所以mysql.health_check表不能只有一行数据

mysql> CREATE TABLE `health_check` (  `id` int(11) NOT NULL,  `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,  PRIMARY KEY (`id`)) ENGINE=InnoDB;/* 检测命令 */insert into mysql.health_check(id, t_modified) values (@@server_id, now()) on duplicate key update t_modified=now();

MySQL规定主备的server_id必须不同,这样主备各自的检测命令就不会冲突

更新判断存在的问题是“判定慢”。因为更新语句在IO负载已经100%时仍然可能在超时前完成。检测系统看到update命令没有超时,就认为“系统正常”,但实际上正常SQL语句已经很慢了

内部统计

前面几种方法都是外部检测,外部检测都需要定时轮询,所以系统可能已经出问题了,但是却需要等到下一个检测发起执行语句的时候,才有可能发现问题,导致主备切换慢

针对磁盘利用率问题,MySQL 5.6 在file_summary_by_event_name表里统计了每次IO请求的时间,可以设置阈值作为检测逻辑

误删数据怎么办?

误删分为以下几类:

  1. 使用delete误删数据行
  2. 使用drop table或者truncate table误删数据表
  3. 使用drop database误删数据库
  4. 使用rm误删整个MySQL实例

误删行

可以使用Flashback恢复,原理是修改binlog的内容,拿回原库重放。使用这个方案的前提是确保binlog_format=row 和 binlog_row_image=FULL

建议在备库上执行,再恢复回主库

误删库/表

这种情况要求线上有定期的全量备份,并且实时备份binlog

假如有人中午12点误删了一个库,恢复数据的流程如下:

  1. 取最近一次全量备份,假设这个库是一天一备,上次备份是当天0点;
  2. 用备份恢复出一个临时库;
  3. 从日志备份里面,取出凌晨0点之后的日志;
  4. 把这些日志,除了误删除数据的语句外,全部应用到临时库

如果临时库有多个数据库,在使用mysqlbinlog时可以加上-database指定误删表所在库,加速数据恢复

在应用日志的时候,需要跳过12点误操作的那个语句的binlog:

  • 如果原实例没有使用GTID模式,只能在应用到包含12点的binlog文件的时候,先用–stop-position参数执行到误操作之前的日志,然后再用–start-position从误操作之后的日志继续执行;
  • 如果实例使用了GTID模式,就方便多了。假设误操作命令的GTID是gtid1,那么只需要执行set gtid_next=gtid1;begin;commit; 先把这个GTID加到临时实例的GTID集合,之后按顺序执行binlog的时候,就会自动跳过误操作的语句

即使这样,使用mysqlbinlog方法恢复数据仍然不快,因为:

  1. mysqlbinlog并不能指定只解析一个表的日志
  2. 用mysqlbinlog解析出日志应用,应用日志的过程就只能是单线程

一种加速方法是,在用备份恢复出临时实例之后,将这个临时实例设置成线上备库的从库,这样:

  1. 在start slave之前,先通过执行
    change replication filter replicate_do_table = (tbl_name) 命令,就可以让临时库只同步误操作的表;
  2. 这样做也可以用上并行复制技术,来加速整个数据恢复过程。

延迟复制备库

上面的方案存在“恢复时间不可控问题”,比如一周一备份,第6天误操作,那就需要恢复6天的日志,这个恢复时间可能按天计算

一般的主备复制结构存在的问题是,如果主库上有个表被误删了,这个命令很快也会被发给所有从库,进而导致所有从库的数据表也都一起被误删了。

延迟复制的备库是一种特殊的备库,通过 CHANGE MASTER TO MASTER_DELAY = N命令,可以指定这个备库持续保持跟主库有N秒的延迟。

比如你把N设置为3600,这就代表了如果主库上有数据被误删了,并且在1小时内发现了这个误操作命令,这个命令就还没有在这个延迟复制的备库执行。这时候到这个备库上执行stop slave,再通过之前介绍的方法,跳过误操作命令,就可以恢复出需要的数据。

这样的话,你就随时可以得到一个,只需要最多再追1小时,就可以恢复出数据的临时实例,也就缩短了整个数据恢复需要的时间

预防误删库/表的方法

  1. 账号分离,避免写错命令

    • 只给业务开发同学DML权限,而不给truncate/drop权限。而如果业务开发人员有DDL需求的话,也可以通过开发管理系统得到支持
    • 即使是DBA团队成员,日常也都规定只使用只读账号,必要的时候才使用有更新权限的账号
  2. 指定操作规范,避免写错要删除的表名

    • 删除数据表之前,必须先对表做改名操作。然后,观察一段时间,确保对业务无影响以后再删除这张表。
    • 改表名的时候,要求给表名加固定的后缀(比如加_to_be_deleted),然后删除表的动作必须通过管理系统执行。并且,管理系删除表的时候,只能删除固定后缀的表。

rm删除数据

对于有高可用机制的MySQL集群,最不怕rm。只要整个集群没被删掉,HA系统会选出新主库,保证整个集群正常工作。因此备库尽量跨机房、跨城市

为什么还有kill不掉的语句

MySQL有两个kill命令:

  • kill query+线程id,表示终止这个线程正在执行的语句
  • kill connection+线程id,connection可缺省,表示断开这个线程的连接,如果有语句正在执行,先停止语句

收到kill后,线程做什么

kill并不是马上停止,而是告诉线程,这条语句已经不需要继续执行了,可以开始“执行停止的逻辑了”

处理kill query命令的线程做了两件事:

  1. 把目标线程的运行状态改成THD::KILL_QUERY(将变量killed赋值为THD::KILL_QUERY);
  2. 给目标线程发一个信号,通知目标线程处理THD::KILL_QUERY状态。如果目标线程处于等待状态,必须是一个可以被唤醒的等待,否则不会执行到判断线程状态的“埋点”

处理kill connection命令的线程做了两件事:

  1. 把目标线程状态设置为KILL_CONNECTION
  2. 关闭目标线程的网络连接

kill无效的两类情况:

  1. 线程没有执行到判断线程状态的逻辑。这种情况有innodb_thread_concurrency 不够用,IO压力过大
  2. 终止逻辑耗时较长。这种情况有kill超大事务、回滚大查询、kill最后阶段的DDL命令

处于Killed状态的线程,你可以通过影响系统环境来让状态尽早结束。比如并发度不够导致线程没有执行到判断线程状态的逻辑,就增大innodb_thread_concurrency。除此之外,做不了什么,只能等流程自己结束

大查询会不会打爆内存

主机内存小于表的大小,全表扫描不会用光主机内存,否则逻辑备份早就挂了

全表扫描对server层的影响

假设对200G的表 db1.t 全表扫描,需要保留结果到客户端,会使用类似命令:

mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t" > $target_file

服务端不保存完整的查询结果集,取数据和发数据的流程是这样的:

  1. 获取一行,写到net_buffer中
  2. 重复获取行,直到net_buffer写满,调用网络接口发出去
  3. 如果发送成功,就清空net_buffer,然后继续取下一行,并写入net_buffer
  4. 如果发送函数返回EAGAIN或WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送

图 21

从这个流程可以看出:

  1. 一个查询在发送过程中,占用的MySQL内部的内存最大就是net_buffer_length这么大,并不会达到200G;
  2. socket send buffer 也不可能达到200G(默认定义/proc/sys/net/core/wmem_default),如果socket send buffer被写满,就会暂停读数据的流程。

全表扫描对InnoDB层的影响

数据页在Buffer Pool(BP)中管理,BP可以起到加速查询的作用,作用效果依赖于一个重要指标:内存命中率

BP的大小由参数 innodb_buffer_pool_size 确定,一般设置成可用物理内存的60%~80%

如果BP满了,要从磁盘读入一个数据页,就要淘汰一个旧数据页,InnoDB内存管理用的是改进后的最近最少使用(LRU)算法

图 23

上图head指向刚刚被访问过的数据页

基本的LRU算法在遇到全表扫描历史数据表时,会出现内存命中率急剧下降,磁盘压力增加,SQL响应变慢的情况

图 22

InnoDB按照 5:3 将LRU链表分成young区和old区,LRU_old指向old区域第一个位置,即整个链表的5/8处

改进后的LRU算法如下:

  1. 访问young区域的数据页,和之前的算法一样,移动到链表头
  2. 访问不在链表中的数据页,淘汰tail指向的最后一页,在LRU_old处插入新数据页
  3. 访问old区域的数据页,若这个数据页在LRU链表中存在时间超过1s,就移动到链表头部,否则不动,1s由参数innodb_old_blocks_time控制

这个策略在扫描大表时不会对young区域造成影响,保证BP响应正常业务的查询命中率

可不可以使用join

先创建两个DDL一样的表

CREATE TABLE `t2` (  `id` int(11) NOT NULL,  `a` int(11) DEFAULT NULL,  `b` int(11) DEFAULT NULL,  PRIMARY KEY (`id`),  KEY `a` (`a`)) ENGINE=InnoDB;/*省略给t2插入1000行数据*/create table t1 like t2;insert into t1 (select * from t2 where id<=100)

Index Nested-Loop Join

有如下语句:

select * from t1 straight_join t2 on (t1.a=t2.a);

straight_join让MySQL使用固定的连接方式执行查询,这里t1是驱动表,t2是被驱动表

图 24

这个语句的执行流程如下:

  1. 从表t1中读入一行数据R
  2. 从数据行R中,取出a字段到表t2里去查
  3. 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分
  4. 重复执行步骤1到3,直到表t1的末尾循环结束

在形式上,这个过程和我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以我们称之为“Index Nested-Loop Join”,简称NLJ

图 25

在流程里:

  1. 对驱动表t1做了全表扫描,这个过程需要扫描100行
  2. 对于每一行R,根据a字段去表t2查找,走的是树搜索过程。由于我们构造的数据是一一对应的,因此每次搜索都只扫描一行,也就是总共扫描100行
  3. 所以,整个执行流程,总扫描行数是200

如果不用join,上面的连接需求,用单表查询实现的话,扫描行数一样,但是交互次数多,而且客户端要自己拼接SQL语句和结果,因此不如直接join

假设驱动表行数是N。被驱动表行数是M,被驱动表查一行数据要先走索引a,再走主键索引,因此时间复杂度是2log2M2*log_2 M。驱动表要扫描N行,然后每行都要去被驱动表上匹配,所以整个执行过程复杂度是 N+N2log2MN+N*2*log_2 M。显然N影响更大,因此让小表做驱动表

Simple Nested-Loop Join

现在语句改成如下:

select * from t1 straight_join t2 on (t1.a=t2.b);

由于t2的字段b没有索引,每次到t2去匹配都要做全表扫描,因此这个查询要扫描100*1000=10万行。

Block Nested-Loop Join

当被驱动表上没有可用索引,MySQL使用的算法流程如下:

  1. 把表t1的数据读入线程内存join_buffer中,由于我们这个语句中写的是select *,因此是把整个表t1放入了内存;
  2. 扫描表t2,把表t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回。

图 26

该算法和Simple Nested-Loop Join算法扫描的行数一样多,但该算法是内存操作,速度更快。碰到大表不能放入join_buffer的情况就分多次放

总结一下:

第一个问题:能不能使用join语句?

  1. 如果可以使用Index Nested-Loop Join算法,也就是说可以用上被驱动表上的索引,其实是没问题的;
  2. 如果使用Block Nested-Loop Join算法,扫描行数就会过多。尤其是在大表上的join操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种join尽量不要用

所以在判断要不要使用join语句时,就是看explain结果里面,Extra字段里面有没有出现“Block Nested Loop”字样

第二个问题:如果要使用join,应该选择大表做驱动表还是选择小表做驱动表?

总是使用小表做驱动表。更准确地说,在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表

join语句怎么优化

创建两个表t1、t2(id int primary key, a int, b int, index(a))。给表t1插入1000行数据,每一行a=1001-id,即字段a是逆序的。给表t2插入100万行数据

Multi-Range Read优化

现在有SQL语句:

select * from t1 where a>=1 and a<=100;

MRR优化的设计思路是:大多数的数据都是按照主键递增顺序插入的,所以按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。使用MRR的语句的执行流程如下:

  1. 根据索引a,定位到满足条件的记录,将id值放入read_rnd_buffer中;
  2. 将read_rnd_buffer中的id进行递增排序
  3. 排序后的id数组,依次到主键id索引中查记录,并作为结果返回。

图 27

MRR能够提升性能的核心在于,这条查询语句在索引a上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键id。这样通过排序以后,再去主键索引查数据,才能体现出“顺序性”的优势

Batched Key Access

MySQL 5.6 引入Batched Key Acess(BKA)算法,这个算法是对NLJ算法的优化

NLJ算法执行的逻辑是:从驱动表t1,一行行地取出a的值,再到被驱动表t2去做join。也就是说,对于表t2来说,每次都是匹配一个值。这时,MRR的优势就用不上了

优化思路就是,从表t1里一次性多拿出些行,一起传给表t2。取出的数据先放到join_buffer

图 28

BNL算法的性能问题

  1. 可能会多次扫描被驱动表,占用磁盘IO资源;
  2. 判断join条件需要执行M*N次对比(M、N分别是两张表的行数),如果是大表就会占用非常多的CPU资源;
  3. 可能会导致Buffer Pool的热数据被淘汰,影响内存命中率。

如果explain命令发现优化器使用BNL算法。我们就需要优化,常见做法是,给被驱动表的join字段加上索引,把BNL算法转成BKA算法

BNL转BKA

select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000;

在索引创建资源开销大情况下,可以考虑使用临时表:

  1. 把表t2中满足条件的数据放在临时表tmp_t中;
  2. 为了让join使用BKA算法,给临时表tmp_t的字段b加上索引;
  3. 让表t1和tmp_t做join操作

对应的SQL语句:

create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;insert into temp_t select * from t2 where b>=1 and b<=2000;select * from t1 join temp_t on (t1.b=temp_t.b);

扩展-hash join

BNL的问题是join_buffer里面维护的是一个无序数组,如果是一个hash表,可以大幅减少判断次数。可以在业务端实现这个优化:

  1. select * from t1;取得表t1的全部1000行数据,在业务端存入一个hash结构
  2. select * from t2 where b>=1 and b<=2000; 获取表t2中满足条件的2000行数据。
  3. 把这2000行数据,一行一行地取到业务端,到hash结构的数据表中寻找匹配的数据。满足匹配的条件的这行数据,就作为结果集的一行

为什么临时表可以重名

内存表和临时表的区别:

  • 内存表,指的是使用Memory引擎的表,建表语法是create table … engine=memory。这种表的数据都保存在内存里,系统重启的时候会被清空,但是表结构还在。除了这两个特性看上去比较“奇怪”外,从其他的特征上看,它就是一个正常的表
  • 临时表,可以使用各种引擎类型 。如果是使用InnoDB引擎或者MyISAM引擎的临时表,写数据的时候是写到磁盘上的。当然,临时表也可以使用Memory引擎

临时表的特性

图 29

临时表在使用上有以下几个特点:

  1. 建表语法是create temporary table …。
  2. 一个临时表只能被创建它的session访问,对其他线程不可见。所以,图中session A创建的临时表t,对于session B就是不可见的。
  3. 临时表可以与普通表同名
  4. session A内有同名的临时表和普通表的时候,show create语句,以及增删改查语句访问的是临时表
  5. show tables命令不显示临时表

临时表的应用

由于不用担心线程之间的重名冲突,临时表经常会被用在复杂查询的优化过程中。其中,分库分表系统的跨库查询就是一个典型的使用场景。

一般分库分表的场景,就是要把一个逻辑上的大表分散到不同的数据库实例上。比如。将一个大表ht,按照字段f,拆分成1024个分表,然后分布到32个数据库实例上。如下图所示:

图 30

分区key的选择是以“减少跨库和跨表查询”为依据的。如果大部分的语句都会包含f的等值条件,那么就要用f做分区键

比如:

select v from ht where f=N;

可以通过分表规则(比如,N%1024)来确认需要的数据被放在了哪个分表上

但是,如果这个表上还有另外一个索引k,并且查询语句是这样的:

select v from ht where k >= M order by t_modified desc limit 100;

由于查询条件里面没有用到分区字段f,只能到所有的分区中去查找满足条件的所有行,然后统一做order by 的操作。这种情况有两种思路:

  • 在proxy层的进程代码中实现排序。优势是快,缺点是工作量大,proxy端压力大
  • 把分库数据汇总到一个表中,再在汇总上操作。如下图所示

图 31

为什么临时表可以重名?

create temporary table temp_t(id int primary key)engine=innodb;

执行该语句,MySQL会创建一个frm文件保存表结构定义。该文件放在临时文件目录下,文件名的后缀是.frm,前缀是“#sql{进程id}_{线程id}_序列号”

图 32

除了文件名不同,内存里面也有一套机制区别不同的表,每个表都对应一个table_def_key

  • 一个普通表的table_def_key的值是由“库名+表名”得到的,所以如果你要在同一个库下创建两个同名的普通表,创建第二个表的过程中就会发现table_def_key已经存在了。
  • 而对于临时表,table_def_key在“库名+表名”基础上,又加入了“server_id+thread_id”

临时表和主备复制

如果当前的binlog_format=row,那么跟临时表有关的语句,就不会记录到binlog里

如果binlog_format=statment/mixed,创建临时表的语句会传到备库,由备库的同步线程执行。因为主库的线程退出时会自动删除临时表,但是备库同步线程是持续运行的,所以还需要在主库上再写一个DROP TEMPORARY TABLE传给备库执行

主库上不同线程创建同名的临时表是没关系的,但是传到备库怎么处理?

图 33

MySQL在记录binlog的时候,会把主库执行这个语句的线程id写到binlog中。这样,在备库的应用线程就能够知道执行每个语句的主库线程id,并利用这个线程id来构造临时表的table_def_key:

  1. session A的临时表t1,在备库的table_def_key就是:库名+t1+“M的serverid”+“session A的thread_id”;
  2. session B的临时表t1,在备库的table_def_key就是 :库名+t1+“M的serverid”+“session B的thread_id”

为什么会使用内部临时表

union 执行流程

假设有表t1:

create table t1(id int primary key, a int, b int, index(a));delimiter ;;create procedure idata()begin  declare i int;  set i=1;  while(i<=1000)do    insert into t1 values(i, i, i);    set i=i+1;  end while;end;;delimiter ;call idata();

然后执行:

(select 1000 as f) union (select id from t1 order by id desc limit 2);

这个语句的执行流程是这样的:

  1. 创建一个内存临时表,这个临时表只有一个整型字段f,并且f是主键字段

  2. 执行第一个子查询,得到1000这个值,并存入临时表中

  3. 执行第二个子查询:

    • 拿到第一行id=1000,试图插入临时表中。但由于1000这个值已经存在于临时表了,违反了唯一性约束,所以插入失败,然后继续执行;
    • 取到第二行id=999,插入临时表成功
  4. 从临时表中按行取出数据,返回结果,并删除临时表,结果中包含两行数据分别是1000和999

如果使用union all,就没有去重,执行的时候是依次执行子查询,得到的结果直接作为结果集的一部分,不需要临时表

group by 执行流程

select id%10 as m, count(*) as c from t1 group by m;

这个语句的执行流程如下:

  1. 创建内存临时表,表里有两个字段m和c,主键是m

  2. 扫描表t1的索引a,依次取出叶子节点上的id值,计算id%10的结果,记为x;

    • 如果临时表中没有主键为x的行,就插入一个记录(x,1);
    • 如果表中有主键为x的行,就将x这一行的c值加1
  3. 遍历完成后,再根据字段m做排序,得到结果集返回给客户端

如果不需要排序,在语句末尾加上order by null

当内存临时表大小达到上限时,会转成磁盘临时表,磁盘临时表默认使用的引擎是InnoDB

group by 优化方法 --索引

新增一列,给这列加索引

alter table t1 add column z int generated always as(id % 100), add index(z);

对这列group by:

select z, count(*) as c from t1 group by z;

group by 优化方法 --直接排序

碰到不能加索引的场景就得老老实实做排序

在group by语句中加入SQL_BIG_RESULT这个提示(hint),就可以告诉优化器:这个语句涉及的数据量很大,请直接用磁盘临时表

select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;

这个语句的执行流程如下:

  1. 初始化sort_buffer,确定放入一个整型字段,记为m
  2. 扫描表t1的索引a,依次取出里面的id值, 将 id%100的值存入sort_buffer中
  3. 扫描完成后,对sort_buffer的字段m做排序(如果sort_buffer内存不够用,就会利用磁盘临时文件辅助排序)
  4. 排序完成后,就得到了一个有序数组。顺序扫描一遍就可以得到结果

基于上面的union、union all和group by语句的执行过程的分析,我们来回答文章开头的问题:MySQL什么时候会使用内部临时表?

  1. 如果语句执行过程可以一边读数据,一边直接得到结果,是不需要额外内存的,否则就需要额外的内存,来保存中间结果
  2. join_buffer是无序数组,sort_buffer是有序数组,临时表是二维表结构
  3. 如果执行逻辑需要用到二维表特性,就会优先考虑使用临时表。比如我们的例子中,union需要用到唯一索引约束, group by还需要用到另外一个字段来存累积计数。

都说InnoDB好,那还要不要使用Memory引擎

内存表的数据组织结构

假设有两张表t1,t2,t1使用Memory引擎,t2使用InnoDB引擎

create table t1(id int primary key, c int) engine=Memory;create table t2(id int primary key, c int) engine=innodb;insert into t1 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);insert into t2 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);

然后,分别执行select *from t1select* from t2。t2表的(0,0)出现在第一行,t1表出现在最后一行

这是因为InnoDB引擎的数据就存在主键索引上,而主键索引是有序存储的,在执行select *的时候,就会按照叶子节点从左到右扫描,所以得到的结果里,0就出现在第一行

而Memory引擎的数据和索引是分开的。主键索引存的是每个数据的位置。执行select *走的是全表扫描数据数组
图 34

InnoDB和Memory引擎的数据组织方式是不同的:

  • InnoDB引擎把数据放在主键索引上,其他索引上保存的是主键id。这种方式,我们称之为索引组织表
  • Memory引擎采用的是把数据单独存放,索引上保存数据位置的数据组织形式,我们称之为堆组织表

两个引擎的一些典型不同:

  1. InnoDB表的数据总是有序存放的,而内存表的数据就是按照写入顺序存放的
  2. 当数据文件有空洞的时候,InnoDB表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就可以插入新值;
  3. 数据位置发生变化的时候,InnoDB表只需要修改主键索引,而内存表需要修改所有索引
  4. InnoDB表用主键索引查询时需要走一次索引查找,用普通索引查询的时候,需要走两次索引查找。而内存表没有这个区别,所有索引的“地位”都是相同的
  5. InnoDB支持变长数据类型,不同记录的长度可能不同;内存表不支持Blob 和 Text字段,并且即使定义了varchar(N),实际也当作char(N),也就是固定长度字符串来存储,因此内存表的每行数据长度相同。

hash索引和B-Tree索引

内存表的范围查询不能走主键索引,但是可以加一个B-Tree索引,B-Tree索引类似于InnoDB的B+树索引

alter table t1 add index a_btree_index using btree (id);

图 35
图 36

不建议在生产环境使用内存表,原因有两方面:

  1. 锁粒度问题。内存表不支持行锁,只支持表锁
  2. 数据持久化问题

自增主键为什么不是连续的

自增主键可以让主键索引尽量保持递增顺序插入,避免页分裂,因此索引更紧凑,但自增主键不能保证连续递增

自增值保存在哪?

InnoDB的自增值保存在内存中。每次重启MySQL都会计算max(id)+1作为自增值。8.0版本,重启的时候依靠redo log恢复自增值

自增值修改机制

假设,某次插入的值是X,当前的自增值是Y

  1. 如果X < Y,那么自增值不变
  2. 如果X >= Y,将当前自增值修改为新的自增值 Z = auto_increment_offset+k*auto_increment_increment。Z > X,auto_increment_offset是自增初始值,auto_increment_increment是自增步长,k是自然数

自增值的修改时机

自增值在真正执行插入数据的操作之前修改。如果因为唯一键冲突导致插入失败会出现id不连续,事务回滚也是类似现象

自增锁的优化

自增id锁并不是一个事务锁,而是每次申请完就马上释放,以便允许别的事务再申请。建议innodb_autoinc_lock_mode设置成2,即前面的策略,同时binlog_format=row,避免insert … select造成主备数据不一致

insert语句的锁为什么这么多

insert … select 语句

在可重复读隔离级别下,binlog_format=statement时,执行 insert … select 语句会对select表的需要访问的资源加锁。加锁是为了避免主备不一致

insert 循环写入

如果把select表的结果insert到select表中,会对select表全表扫描,创建一个临时表,再将select结果insert回表。这么做的原因是:这类一边遍历数据,一边更新数据的情况,如果读出来的数据直接写回原表,就可能在遍历过程中,读到刚刚插入的记录,新插入的记录如果参与计算逻辑,就跟语义不符

优化方法是:手动创建内存临时表,先 insert临时表select目标表,再 insert目标表select临时表,这样就不会对目标表全表扫描

insert 唯一键冲突

图 37

在session A执行rollback语句回滚的时候,session C几乎同时发现死锁并返回

这个死锁产生的逻辑是这样的:

  1. 在T1时刻,启动session A,并执行insert语句,此时在索引c的c=5上加了记录锁。注意,这个索引是唯一索引,因此退化为记录锁
  2. 在T2时刻,session B要执行相同的insert语句,发现了唯一键冲突,加上读锁;同样地,session C也在索引c上,c=5这一个记录上,加了读锁(共享next-key lock)
  3. T3时刻,session A回滚。这时候,session B和session C都试图继续执行插入操作,都要加上写锁(排它next-key lock)。两个session都要等待对方的行锁,所以就出现了死锁

图 38

insert into … on duplicate key update

这个语义的逻辑是,插入一行数据,如果碰到唯一键约束,就执行后面的更新语句。它给唯一索引加排它的next-key lock(写锁)

怎么最快地复制一张表

如果可以控制对原表的扫描行数和加锁范围很小的话,可以直接用insert … select。否则先将数据写到外部文件,再写回目标表,方法有三种:

  1. 物理拷贝的方式速度最快,尤其对于大表拷贝来说是最快的方法。如果出现误删表的情况,用备份恢复出误删之前的临时库,然后再把临时库中的表拷贝到生产库上,是恢复数据最快的方法。但是,这种方法的使用也有一定的局限性:

    • 必须是全表拷贝,不能只拷贝部分数据;
    • 需要到服务器上拷贝数据,在用户无法登录数据库主机的场景下无法使用;
    • 由于是通过拷贝物理文件实现的,源表和目标表都是使用InnoDB引擎时才能使用。
  2. 用mysqldump生成包含INSERT语句文件的方法,可以在where参数增加过滤条件,来实现只导出部分数据。这个方式的不足之一是,不能使用join这种比较复杂的where条件写法

  3. 用select … into outfile的方法是最灵活的,支持所有的SQL写法。但,这个方法的缺点之一就是,每次只能导出一张表的数据,而且表结构也需要另外的语句单独备份

grant之后要跟着flushprivileges吗

grant语句会同时修改数据表和内存,判断权限的时候使用的是内存数据。因此,规范地使用grant和revoke语句,是不需要随后加上flush privileges语句的。

flush privileges语句本身会用数据表的数据重建一份内存权限数据,所以在权限数据可能存在不一致的情况下再使用。而这种不一致往往是由于直接用DML语句操作系统权限表导致的,所以我们尽量不要使用这类语句。

要不要使用分区表

相对于用户分表:

优势:对业务透明,使用分区表的业务代码更简洁,且可以很方便的清理历史数据
劣势:第一次访问的时候需要访问所有分区;共用MDL锁

]]>
+ + + + + <h2 id="基础架构一条sql查询语句是如何执行的"><a class="markdownIt-Anchor" href="#基础架构一条sql查询语句是如何执行的"></a> 基础架构:一条SQL查询语句是如何执行的</h2> +<p><img src="https://cd + + + + + + + + + + + +
+ + + Java并发编程实战学习笔记 + + https://zunpan.github.io/2023/01/13/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E6%88%98%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ + 2023-01-13T09:02:46.000Z + 2023-09-24T04:27:40.278Z + + 本书分成四部分,
第一部分是基础,主要内容是并发的基础概念和线程安全,以及如何用java类库提供的并发构建块组成线程安全的类(2-5章节)

第二部分是构建并发应用程序,主要内容是如何利用线程来提高并发应用的吞吐量和响应能力(6-9章节)

第三部分是活跃性、性能和测试,主要内容是如何确保并发程序能够按照你的要求执行,并且具有可接受的性能(10-12章节)

第四部分是高级主题,涵盖了可能只有有经验的开发人员才会感兴趣的主题:显式锁、原子变量、非阻塞算法和开发自定义同步器(13-16章节)

Part 1 基础

Chapter 1 简介

1.1 并发简史

早期计算机没有操作系统,程序独占所有资源,一次只能有一个程序运行,效率低下

操作系统出现后,程序(进程)可以并发运行,由操作系统分配资源,进程互相隔离,必要时依靠粗粒度的通信机制:sockets、signal handlers、shared memory等机制通信

进程提高了系统吞吐量和资源利用率,线程的出现也是这个原因,线程有时被称为轻量级进程,大多数现代操作系统将线程而不是进程视为调度的基本单位,同一程序的多个线程可以同时在多个CPU上调度,如果没有同步机制协调对共享数据的访问,一个线程可能会修改另一个线程正在使用的变量,导致不可预测的结果

1.2 线程优势

  • 减小开发和维护开销
  • 提高复杂应用的性能
  • 提高GUI的响应能力
  • 简化JVM实现

1.3 线程风险

竞争(多个线程以未知顺序访问资源)
活跃性(死锁,饥饿,活锁)
性能(频繁切换导致开销过大)

1.4 无处不在的线程

  • 框架通过在框架线程中调用应用程序代码将并发性引入到应用程序中,在代码中将不可避免的访问应用程序状态,因此所有访问这些状态的代码路径都必须是线程安全的
  • Timer类,TimerTask在Timer管理的线程中执行
  • Servlet(每个请求使用一个线程同时执行Servlet)
  • RMI(由RMI负责打包拆包远程对象)
  • Swing(具有异步性)

Chapter 2 线程安全

多个线程访问同一个可变的状态变量时没有使用合适的同步机制,可以用以下方法修复:

  • 不在线程间共享该变量
  • 将变量变为常量
  • 访问时候使用同步

2.1 什么是线程安全?

如果一个类被多线程访问,不管线程调度或交叉执行顺序如何,类的表现都是正确的,那么类是线程安全的

线程安全类封装任何需要的同步,因此客户端不需要提供自己的同步。

无状态的对象永远是线程安全的

2.2 原子性

竞争情况(race condition):由于不恰当的执行顺序导致出现不正确的结果,常发生在以下情况中:

  • 读取-修改-写入,例子: 自增操作
  • 先检查后执行,例子:延迟初始化,不安全的单例模式,懒汉模式

第一种情况解决方式:使用juc里面的类,比如count可以用AtomicLong类型操作保证原子性
第二种情况解决方式:加锁保证原子性

2.3 加锁

如果类有多个变量需要更新,即使它们的各自操作都是原子性的,也要把他们放在同一个原子操作中,方式是加锁。Java 提供了锁机制来增强原子性:synchronized

内置锁: synchronized 实例方法会将被调用方法的对象作为内置锁或监视锁,内置锁是互斥的,同一时刻最多只有一个线程拿到这个锁

可重入: 内置锁是可重入的,已经拿到锁的线程可以再次获取锁,实现方式是锁会(就是lock对象)关联一个持有者和计数值,持有者再次进入次数加一,退出减一,减到0会释放锁

2.4 用锁来保护状态

混合操作比如说读取-修改-写入和先检查后执行,需要保证原子性来避免竞争情况。

常见错误: 只有写入共享变量才需要同步
原因:读取也需要同步,不然可能会看到过期值

每个共享的可变变量应该由同一个锁保护,常见的加锁习惯是将可变变量封装到一个对象中

对于不变性条件(invariant)中涉及的多个变量,这多个变量都需要用同一个锁保护,例如Servlet缓存了请求次数和请求数据(数组),不变性条件是请求数据的长度等于次数,这通过加锁来保证

2.5 活跃性和性能

给Servlet的方法声明syncronized极大降低了并发性,我们可以通过缩小同步块的范围,在保证线程安全的情况下提高并发性。合理的做法是将不影响共享状态的操作从同步块中排除

Chapter 3 共享对象

synchronized 块和方法可以确保操作的原子性执行,它还有另一个重要作用:内存可见性。我们不仅想要防止一个线程在另一个线程使用一个对象时修改它的状态,还想要确保当一个线程修改一个对象的状态时,其他线程可以看到最新更改

3.1 可见性

  1. 过期数据(当一个线程修改数据,但其他线程不能立马看到)。 读取操作如果不同步,仍然能看到一个过期数据,这叫做最低安全性(过期数据至少是由之前的线程设置的,而不是随机值)

  2. 大多数变量都满足最低安全性,除了非volatile修饰的64位变量(double和long),jvm允许将64位操作拆解为2个 32位操作,读取这样的变量可能会出现过期值的高32位+新值的低32位的结果

  3. 内置锁保证可见性

  4. volatile: 保证可见性,禁止指令重排,不保证原子性(使用场合:保证自身可见性,引用对象状态可见性,标识重要的生命周期事件)

    当且仅当满足以下所有条件时,才应该使用volatile变量:

    • 对变量的写入不依赖于它的当前值,或者可以确保只有一个线程更新该值;
    • 该变量不会与其他状态变量一起纳入不变性条件
    • 在访问变量时,由于任何其他原因不需要加锁。

3.2 发布与逃逸

发布是指让对象在外部可见,常见方式是对象引用声明为 public static。发布对象的同时,任何通过非私有字段引用和方法调用链从发布对象中访问的对象也被发布了

逃逸是指对象的私有信息也对外可见了,比如发布一个对象包含一个私有数组,同时提供一个返回引用的get方法,外部可以通过引用修改内部私有数组

3.3 线程封闭

如果对象限制在一个线程中使用,即使对象不是线程安全的,也会自动线程安全

例子:Swing: 将组件和数据对象放到事件分发线程,其它线程访问不了这些对象;JDBC.Connection对象: 应用线程从数据库连接池中获取一个连接对象,连接对象由该线程独自使用

Java 提供 ThreadLocal 来实现线程封闭,程序员做的是阻止对象从线程中逃逸

线程封闭通常用来实现一个子系统,例如GUI,它是单线程的

  1. Ad-hoc封闭: 核线程封闭性的职责完全由程序实现来承担(脆弱,少用)

  2. 栈封闭: 只能通过局部变量访问对象(Java基本类型或者局部变量)

  3. ThreadLocal类: 提供getter和setter,每个使用该变量的线程存有一份独立的副本

3.4 不可变

不可变对象永远是线程安全的

满足以下条件,对象才是不可变的:

  • 构造函数之后状态不可修改
  • 所有域都是final
  • 对象正确创建(this引用没有在构造期间逃逸)

多个状态的对象需要保证线程安全,可以将状态封装到一个不可变类中,用volatile修饰不可变对象引用

3.5 安全发布

  1. 不正确的发布对象会出现两个问题:其它线程会看到null或旧值;最糟糕的是其它线程看到最新的引用但是被引用的对象还是旧的

  2. 由于不可变对象很重要,Java内存模型为不可变对象的共享提供一种特殊的初始化安全性保证,不用同步也能安全发布

  3. 一个正确构造的对象可以通过以下方式安全发布:

    • 静态初始化函数中初始化一个对象引用
    • 引用保存到volatile域或者AtomicReference对象中
    • 引用保存到某个正确构造对象的final域
    • 引用保存到锁保护的域(容器也可)
  4. 不可变对象,可以放宽到事实不可变对象(对象在发布之后不会改变状态)

  5. 可变对象必须通过安全方式发布,并且必须是线程安全的或者锁保护起来

  6. 并发程序共享对象实用策略

    • 线程封闭
    • 只读共享
    • 线程安全共享:对象内部实现同步
    • 保护对象:锁机制

Chapter 4 组合对象

本章讨论如何将线程安全的组件组合成更大的组件或程序

4.1 设计一个线程安全的类

  1. 在设计线程安全类的过程中,常会包含以下三个基本要素:

    • 找出构成对象状态的所有变量。
    • 找出约束状态变量的不变性条件和后验条件。
    • 建立对象状态的并发访问管理策略。

    如果不了解对象的不变性条件和后验条件,就无法确保线程安全

  2. 依赖状态的方法需要先满足某种状态才能运行,即先验条件。java提供了 wait and notify 机制来等待先验条件成立,它依赖内置锁。更简单的实现方法是用java类库的阻塞队列或者信号量

  3. 一般情况下,状态所属权是封装状态的类,除非类公开可变对象的引用,这时候类只有共享权

4.2 实例封闭

在对象中封装数据,通过使用对象方法访问数据,从而更容易确保始终在持有适当锁的情况下访问数据。

  1. Java监视器模式:封装可变状态到对象中,使用对象的内置锁保护状态,使用私有锁对象更有优势

4.3 线程安全的委托

  1. 将线程安全的职责委托给线程安全的类,例如计数器类不做同步处理,依赖AtomicLong类型达到线程安全
  2. 可以将线程安全委托给多个基础状态变量,只要它们是独立的
  3. 委托失效:多个变量间有不变性条件,比如大小关系等,需要加锁,除非复合操作也可以委托给变量
  4. 如果一个状态变量是线程安全的,不参与任何限制其值的不变性条件,并且在任何操作中都没有禁止的状态转换,那么它就可以安全地发布。

4.4 给现有的线程安全类加功能

继承方式(可能会因为子父类加的锁不一样线程不安全)

  1. 客户端加锁,使用辅助类,若类的加锁依赖其它类,那么辅助类容易错误加锁
  2. 组合方式,加锁策略完全由组合类提供

4.5 文档化同步策略

为类的客户端记录线程安全保证;为其维护者记录其同步策略

Chapter 5 基础构建模块

在实际应用中,委托是创建线程安全类最有效的策略之一,本章介绍平台库的并发构建模块,例如线程安全的集合和各种可以协调线程控制流的同步器

5.1 同步集合

Vector、Hashtable,以及JDK1.2增加了 Collections.synchronizedXxx 创建同步包装类

  1. 复合线程安全的类的方法可能不是线程安全的,例如复合方法调用size和get方法,中间可能被删掉元素导致size结果不对
  2. 迭代器或者for-each不会锁定集合,在迭代过程中检测到集合变化时会抛出ConcurrentModificationException异常,检测是通过检测count值,但是没有同步,可能看到过期值
  3. 隐藏的迭代器(某些操作底层隐藏着调用迭代器,比如集合的toString)

5.2 并发集合

同步集合通过序列化对集合状态的所有访问来实现线程安全,性能低。Java 5增加了并发集合

  1. ConcurrentHashMap,使用分段锁,具有弱一致性,同时size和isEmpty是估计并不精确,只有需要独占Map,才不建议使用该Map
  2. CopyOnWriteArrayList,每次修改都是返回副本,建议迭代多修改少的时候使用

5.3 阻塞队列和生产者-消费者模式

BlockingQueue,常用来实现生产者和消费者,有一个特殊的实现SynchronousQueue,它不是一个实际的队列,当生产者生产数据时直接交给消费者,适用于消费者多的场景

Deque,常用来实现工作窃取模式。生产者和消费者模式中,消费者共享一个队列,工作窃取模式中,消费者有独自的队列,当消费完后会偷其他人的工作。工作窃取模式可以减少对于共享队列的竞争

5.4 阻塞方法与中断方法

  • 当某个方法抛出InterruptedException,说明该方法是阻塞方法,可以被中断
  • 代码中调用一个阻塞方法(阻塞方法和线程状态没有必然关系,方法可能是个长时间方法所以声明抛出InterruptedException,也有可能是会导致线程状态改变的sleep方法),必须处理中断响应.
    • 捕获/抛出异常
    • 恢复中断.调用当前线程的interrupt

5.5 同步器

  1. 阻塞队列
  2. 闭锁(Latch): 延迟线程进度,直到条件满足,FutureTask也可以做闭锁
  3. 信号量:类似发布凭证,但是任意线程都可以发布和返还
  4. 栅栏: 阻塞一组线程,直到某个条件满足;如果有某个线程await期间中断或者超时,所有阻塞的调用都会终止并抛出BrokenBarrierException

5.6 构建高效且可伸缩的缓存

  1. 使用hashMap+synchronized,性能差
  2. ConcurrentHashMap代替hashMap+synchronized,有重复计算问题
  3. ConcurrentHashMap的值用FutureTask包起来,只要键已经存在,从FutureTask获取结果,因为check-then-act模式,仍然存在重复计算问题
  4. 使用putIfAbsent设置缓存

Part 2 构建并发应用程序

Chapter 6 任务执行

6.1 在线程中执行任务

  1. 串行执行任务(响应会慢,服务器资源利用率低)

  2. 显式为每个请求申请一个线程

    • 任务处理线程从主线程分离,提高响应速度
    • 任务可以并行处理,提高吞吐量
    • 任务处理代码必须是线程安全的,多个线程会并发执行
  3. 无限制创建线程的不足

    • 创建销毁浪费时间
    • 浪费资源
    • 稳定性差

6.2 Executor框架

  1. Executor基于生产-消费模式,提交任务相当于生产者,执行任务的线程相当于消费者.

  2. 执行策略

    • What: 在什么线程中执行任务,按什么顺序执行,任务执行前后要执行什么操作
    • How Many: 多少任务并发,多少等待
    • Which: 系统过载时选择拒绝什么任务
    • How: 怎么通知任务成功/失败
  3. 线程池,管理一组同构工作线程的资源池,跟工作队列密切相关

  4. Executor生命周期

    • 运行 : 对象新建时就是运行状态
    • 关闭 : 不接受新任务,同时等待已有任务完成,包括未执行的任务,关闭后任务再提交由 “被拒绝的执行处理器” 处理或者直接抛异常
    • 终止 : 关闭后任务完成
  5. 延迟任务和周期任务

    Timer类可以负责,但是存在缺陷,应该考虑ScheduledThreadPoolExecutor代替它

    Timer: 只用一个线程执行定时任务,假如某个任务耗时过长,会影响其他任务的定时准确性。除此之外,不支持抛出异常,发生异常将终止线程(已调度(scheduled)未执行的任务,线程不会执行,新任务不会调度,称为线程泄露)

    DelayQueue: 阻塞队列的一种实现,为ScheduledThreadPoolExecutor提供调度策略

6.3 寻找可利用的并行性

  1. 将耗时的IO使用别的线程获取;而不是简单的串行执行

  2. Future 表示一个任务的生命周期,并提供相应的方法判断完成/取消,get会阻塞或抛异常

  3. 使用Callable和Future并行化下载和渲染

  4. 异构任务并行化获取重大性能提升很困难.

    • 任务大小不同
    • 负载均衡问题
    • 协调开销
  5. CompletionService 将 Executor 和BlockingQueue结合在一起,Executor是生产者,CompletionService是消费者

  6. 使用 CompletionService 并行化下载和渲染

  7. 为任务设置时限

  8. 需要获取多个设置了时限的任务的结果可以用带上时间的 invokeAll 提交多个任务

Chapter 7 取消和关闭

本章讲解如何停止任务和线程,Java没有安全强制线程停止的方法,只有一种协作机制,中断

7.1 任务取消

有一种协作机制是在任务中设置取消位,任务定期查看该标识,假如置位就结束任务(假如线程阻塞了,就看不到取消位,那么就停不下来了)

  1. 中断: 在取消任务或线程之外的其他操作中使用中断是不合适的
    • 每个线程都有一个中断标志,interrupt中断目标线程,isInterrupted返回目标线程的中断状态,interrupted(糟糕的命名)清除当前线程中断;
    • Thread.sleep和Object.wait都会检查线程什么时候中断,发现时提前返回(不会立即响应,只是传递请求而已)
  2. 中断策略:尽快推迟执行流程,传递给上层代码;由于每个线程拥有各自的中断策略,除非知道中断对这个线程的含义,否则不应该中断该线程
  3. 中断响应
    当调用会抛出InterruptedException的阻塞方法时,有两种处理策略
    • 传播异常,让你的方法也变成会抛出异常的阻塞方法(中断标志一直为true)
    • 恢复中断状态,以便调用堆栈上较高级的代码处理它(try-catch之后中断标志为false,可以调用当前线程的interrupt方法恢复成中断状态)。
  4. 在中断线程之前,要了解线程的中断策略
  5. 通过Future取消任务
  6. 处理不可中断的阻塞
    • java.io中的同步Socket I/O.通过关闭Socket可以使阻塞线程抛出异常
    • java.io中的同步 I/O.终端一个InterruptibleChannel会抛出异常并关闭链路
    • 获取某个锁. Lock提供lockInterruptibly
  7. 通过 newTaskFor 方法进一步优化

7.2 停止基于线程的服务

基于线程的服务:拥有线程的服务,例如线程池

只要拥有线程的服务的生命周期比创建它的方法的生命周期长,就提供生命周期方法。例如线程池 ExecutorService 提供了shutdown

  1. 日志服务:多生产者写入消息到阻塞队列,单消费者从阻塞队列中取消息,停止日志服务需要正确关闭线程。需要对结束标志位和队列剩余消息数同步访问(书有错误,LoggerThread 应该 synchronized (LogService.this))
  2. 毒丸,生产者将毒丸放在队列上,消费者拿到毒丸就结束
  3. shutdownNow 取消正在执行的任务,返回已提交未开始的任务,可以用个集合保存执行中被取消的任务

7.3 处理非正常的线程终止

通常是因为抛出运行时异常导致线程终止

处理方法:

  1. try-catch 捕获任务异常,如果不能恢复,在finally块中通知线程拥有者
  2. 当线程因未捕获异常而退出时,JVM会将事件报告给线程拥有者提供的UncaughtExceptionHandler,如果没有处理程序就将堆栈打印到System.err
  3. 通过execute提交的任务的异常由UncaughtExceptionHandler处理,submit提交的任务,通过调用Future.get方法,包装在ExecutionException里面

7.4 JVM关闭

有序关闭:最后一个非守护线程终止(可能是调用了System.exit,或者发送SIGINT或按Ctrl-C)后终止
突然关闭:通过操作系统终止JVM进程,例如发送SIGKIll

  1. 有序关闭中,JVM首先启动所有已注册的关闭钩子(通过Runtime.addShutdownHook注册的未启动线程)。如果应用程序线程在关闭时仍在运行,将与关闭线程并行执行。当所有关闭钩子都完成时,如果runFinalizersOnExit为true,那么jvm可能运行终结器,然后停止
  2. 守护线程:执行辅助功能的线程,不会阻止JVM关闭。当JVM关闭时,守护线程直接关闭,不执行 finally 块,栈不会展开。守护线程适合做“内务”任务,例如清缓存
  3. 终结器:GC在回收对象后会执行 finalize 方法释放持久资源。终结器在JVM管理的线程中运行,需要同步访问。终结器难写且性能低,除非要关闭 native 方法获取的资源,否则在 finally中显示关闭就够了

Chapter 8 使用线程池

本章将介绍配置和调优线程池的高级选项,描述使用任务执行框架时需要注意的危险

8.1 任务和执行策略之间的隐式耦合

Executor 框架在任务提交和执行之间仍存在一些耦合:

  1. 依赖其它任务的任务,相互依赖可能导致活跃性问题
  2. 利用线程封闭的任务,这类任务不做同步,依赖单线程执行
  3. 响应时间敏感的任务,可能需要多线程执行
  4. 使用 ThreadLocal 的任务,ThreadLocal不应该用于线程池中任务之间的通信

8.1.1 线程饥饿死锁

把相互依赖的任务提交到一个单线程的Executor一定会发生死锁。增大线程池,如果被依赖的任务在等待队列中,也会发生死锁

8.1.2 运行耗时长的任务

即使不出现死锁,也会降低性能,通过限制执行时间可以缓解

8.2 设置线程池大小

cpu数可以调用 Runtime.availableProcessors得出

  1. 计算密集型场景,线程池大小等于cpu数+1
  2. IO密集型场景,线程池大小等于cpu数 * cpu利用率 * (1+等待/计算时间比)

8.3 配置 ThreadPoolExecutor

8.3.1 线程创建和销毁

  1. corePoolSize:线程池大小,只有工作队列满了才会创建超出这个数量的线程
  2. maximumPoolSize:最大线程数量
  3. keepAliveTime:空闲时间超过keepAliveTime的线程会成为回收的候选线程,如果线程池的大小超过了核心的大小,线程就会被终止

8.3.2 管理工作队列

可以分成三类:无界队列、有界队列和同步移交。队列的选择和线程池大小、内存大小的有关

无界队列可能会耗尽资源,有界队列会带来队列满时新任务的处理问题,同步移交只适合用在无界线程池或饱和策略可以接受

8.3.3 饱和策略

当任务提交给已经满的有界队列或已经关闭的Executor,饱和策略开始工作

  1. Abort,默认策略,execute方法会抛RejectedExecutionException
  2. Discard:丢弃原本下个执行的任务,并重新提交新任务
  3. Caller-Runs:将任务给调用execute 的线程执行
  4. 无界队列可以使用信号量进行饱和策略

8.3.4 线程工厂

通过ThreadFactory.newThread创建线程,自定义线程工厂可以在创建线程时设置线程名、自定义异常

8.3.5 调用构造函数后再定制ThreadPoolExecutor

线程池的各项配置可以通过set方法配置,如果不想被修改,可以调用Executors.unconfigurableExecutorService
将其包装成不可修改的线程池

8.4 扩展 ThreadPoolExecutor

ThreadPoolExecutor给子类提供了钩子方法,beforeExecute、afterExecute和terminated

beforeExecute和afterExecute钩子在执行任务的线程中调用,可用于添加日志记录、计时、监控或统计信息收集。无论任务从run正常返回还是抛出异常,afterExecute钩子都会被调用。如果beforeExecute抛出一个RuntimeException,任务就不会执行,afterExecute也不会被调用

terminated钩子在任务都完成且所有工作线程都关闭后调用,用来释放资源、执行通知或日志记录

8.5 递归算法并行化

  1. 如果迭代操作之间是独立的,适合并行化
  2. 递归不依赖于后续递归的返回值

Chapter 9 GUI应用

9.1 为什么GUI是单线程的

由于竞争情况和死锁,多线程GUI框架最终都变成了单线程

9.1.1 串行事件处理

优点:代码简单
缺点:耗时长的任务会发生无响应(委派给其它线程执行)

9.1.2 Swing的线程封闭

所有Swing组件和数据模型对象都封闭在事件线程中,任何访问它们的代码必须在事件线程里

9.2 短时间的GUI任务

事件在事件线程中产生,并冒泡到应用程序提供的监听器

Swing将大多数可视化组件分为两个对象(模型对象和视图对象),模型对象保存数据,可以通过引发事件表示模型发生变化,视图对象通过订阅接收事件

9.3 长时间的GUI任务

对于长时间的任务可以使用线程池

  1. 取消 使用Future
  2. 进度标识

9.4 共享数据模型

  1. 只要阻塞操作不会过度影响响应性,那么事件线程和后台线程就可以共享该模型
  2. 分解数据模型.将共享的模型通过快照共享

9.5 其它形式单线程

为了避免同步或死锁使用单线程,例如访问native方法使用单线程

Part 3 活跃性、性能和测试

Chapter 10. 避免活跃性危险

Java程序不能从死锁中恢复,本章讨论活跃性失效的一些原因以及预防措施

10.1 死锁

哲学家进餐问题:每个人都有另一个人需要的资源,并且等待另一个人持有的资源,在获得自己需要的资源前不会释放自己持有的资源,产生死锁

10.1.1 Lock-ordering死锁

线程之间获取锁的顺序不同导致死锁。
解决方法:如果所有线程以一个固定的顺序获取锁就不会出现Lock-ordering死锁

10.1.2 动态Lock Order死锁

获取锁的顺序依赖参数可能导致死锁。
解决方法:对参数进行排序,统一线程获取锁的顺序

10.1.3 协作对象的死锁

如果在持有锁时调用外部方法,将会出现活跃性问题,这个外部方法可能阻塞,加锁等导致其他线程无法获得当前被持有的锁
解决方法:开放调用

10.1.4 开放调用

如果在方法中调用外部方法时不需要持有锁(比如调用者this),那么这种调用称为开放调用。实现方式是将调用者的方法的同步范围从方法缩小到块

10.1.5 资源死锁

和循环依赖锁导致死锁类似。例如线程持有数据库连接且等待另一个线程释放,另一个线程也是这样

10.2 避免和诊断死锁

使用两部分策略来审计代码以确保无死锁:首先,确定哪些地方可以获得多个锁(尽量使其成为一个小集合),然后对所有这些实例进行全局分析,以确保锁的顺序在整个程序中是一致的,尽可能使用开放调用简化分析

10.2.1 定时锁

另一种检测死锁并从死锁中恢复的技术是使用显示锁中的Lock.tryLock()代替内置锁

10.2.2 用Thread Dumps进行死锁分析

线程转储包含每个运行线程的堆栈信息,锁信息(持有哪些锁,从哪个栈帧中获得)以及阻塞的线程正在等待获得哪些锁

10.3 其它活跃性危险

10.3.1 饥饿

线程由于无法获得它所需要的资源而不能继续执行,最常见的资源是CPU

避免使用线程优先级,可能导致饥饿

10.3.2 糟糕的响应性

计算密集型任务会影响响应性,通过降低执行计算密集型任务的线程的优先级可以提高前台任务的响应性

10.3.3 活锁

线程执行任务失败后,任务回滚,又添加到队列头部,导致线程没有阻塞但永远不会有进展。多个相互合作的线程为了响应其它线程而改变状态也会导致活锁
解决方法:在重试机制中引入一些随机性

Chapter 11. 性能和可伸缩性

11.1 对性能的思考

11.1.1 性能和可伸缩性

性能: 可以用任务完成快慢或者数量来衡量,具体指标包括服务时间、延迟、
吞吐量、可伸缩性等

可伸缩性: 增加计算资源时提供程序吞吐量的能力

11.1.2 评估性能权衡

许多性能优化牺牲可读性和可维护性,比如违反面向对象设计原则,需要权衡

11.2 Amdahl定律

N:处理器数量
F:必须串行执行的计算部分
Speedup:加速比

$\text { Speedup } \leq \frac{1}{F+\frac{1-F}{N}} $

串行执行的计算部分需要仔细考虑,即使任务之间互不影响可以并行,但是线程从任务队列中需要同步,使用ConcurrentLinkedQueue比同步的LinkedList性能好

11.3 线程引入的开销

  1. 上线文切换
  2. 内存同步(同步的性能开销包括可见性保证,即内存屏障,可以用jvm逃逸分析和编译器锁粒度粗化进行优化)
  3. 阻塞(非竞争的同步可以在JVM处理,竞争的同步需要操作系统介入,竞争失败的线程必定阻塞,JVM可以自旋等待(反复尝试获取锁,直到成功)或者被操作系统挂起进入阻塞态,短时间等待选择自旋等待,长时间等待选择挂起)

11.4 减少锁的竞争

并发程序中,对伸缩性最主要的威胁就是独占方式的资源锁

三种减少锁争用的方法:

  • 减少持有锁的时间
  • 减少请求锁的频率
  • 用允许更大并发的协调机制替换互斥锁

11.4.1 减小锁的范围

锁的范围即持有锁的时间

11.4.2 降低锁的力度

分割锁:将保护多个独立的变量的锁分割成单独的锁,这样锁的请求频率就可以降低

11.4.3 分段锁

分割锁可以扩展到可变大小的独立对象上的分段锁。例如ConcurrentHashMap使用了一个包含16个锁的数组,每个锁保护1/16的哈希桶

分段锁缺点:独占访问集合开销大

11.4.4 避免热点字段

热点字段:缓存
热点字段会限制可伸缩性,例如,为了缓存Map中的元素数量,添加一个计数器,每次增删时修改计数器,size操作的开销就是O(1)。单线程没问题,多线程下又需要同步访问计数器,ConcurrentHashMap每个哈希桶一个计数器

11.4.5 互斥锁的替代品

考虑使用并发集合、读写锁、不可变对象和原子变量

读写锁:只要没有一个写者想要修改共享资源,多个读者可以并发访问,但写者必须独占地获得锁
原子变量:提供细粒度的原子操作,可以降低更新热点字段的开销

11.4.6 监测CPU利用率

cpu利用率低可能是以下原因:

  1. 负载不够,可以对程序加压
  2. IO密集,可以通过iostat判断,还可以通过监测网络上的流量水平判断
  3. 外部约束,可能在等待数据库或web服务的响应
  4. 锁竞争,可以用分析工具分析哪些是“热”锁

11.4.7 不要用对象池

现在JVM分配和回收对象已经很快了,不要用对象池

11.5 例子:比较Map的性能

ConcurrentHashMap单线程性能略好于同步的HashMap,并发时性能超好。ConcurrentHashMap对大多数成功的读操作不加锁,对写操作和少数读操作加分段锁

11.6 减少上下文切换

  1. 日志记录由专门的线程负责
  2. 请求服务时间不应该过长
  3. 将IO移动到单个线程

Chapter 12. 并发程序的测试

大多数并发程序测试安全性和活跃性。安全性可以理解为“永远不会发生坏事”,活跃性可以理解为“最终会有好事发生”

12.1 正确性测试

12.1.1 基础单元测试

和顺序程序的测试类似,调用方法,验证程序的后置条件和不变量

12.1.2 阻塞操作测试

在单独的一个线程中启动阻塞活动,等待线程阻塞,中断它,然后断言阻塞操作完成。

Thread.getState不可靠,因为线程阻塞不一定进入WAITING或TIMED_WAITING状态,JVM可以通过自旋等待实现阻塞。类似地,Object.wait和Condition.wait存在伪唤醒情况,处于WAITING或TIMED_WAITING状态的线程可以暂时过渡到RUNNABLE。

12.1.3 安全性测试

给并发程序编写高效的安全测试的挑战在于识别出容易检查的属性,这些属性在程序错误时出错,同时不能让检查限制并发性,最好检查属性时不需要同步。

生产者和消费者模式中的一种方法是校验和,单生产者单消费者可以使用顺序敏感的校验和计算入队和出队元素的校验和,多生产者多消费者要用顺序不敏感的校验和

12.1.4 资源管理测试

任何保存或管理其他对象的对象都不应该在不必要的时间内继续维护对这些对象的引用。可以用堆检查工具测试内存使用情况

12.1.5 使用回调

回调函数通常是在对象生命周期的已知时刻发出的,这是断言不变量的好机会。例如自定义线程池可以在创建销毁线程时记录线程数

12.1.6 产生更多的交替操作

Thread.yield放弃cpu,保持RUNNABLE状态,重新竞争cpu
Thread.sleep放弃cpu进入TIME_WAITING状态,不竞争cpu,sleep较小时间比yield更稳定产生交替操作

tips:Java 线程的RUNNABLE 状态对应了操作系统的 ready 和 running 状态,TIME_WAITING(调用Thread.sleep) 和 WAITING(调用Object.wait) 和 BLOCKED(没有竞争到锁) 对应 waiting 状态。interrupt是种干预手段,如果interrupt一个RUNNABLE线程(可能在执行长时间方法需要终止),如果方法声明抛出InterruptedException,就表示可中断,方法会循环检查isInterrupted状态来响应interrupt,一般情况线程状态变成TERMINATED。如果interrupt一个 waiting 线程(可能是由sleep、wait方法导致,这些方法会抛出InterruptedException),线程重新进入 RUNNALBE 状态,处理InterruptedException

12.2 性能测试

  1. 增加计时功能(CyclicBarrier)
  2. 多种算法比较(LinkedBlockingQueue在多线程情况下比ArrayBlockingQueue性能好)
  3. 衡量响应性

12.3 避免性能测试的陷阱

12.3.1 垃圾回收

垃圾回收不可预测,会导致测试误差,需要长时间测试,多次垃圾回收,得到更准确结果

12.3.2 动态编译

动态编译会影响运行时间,需要运行足够长时间或者与完成动态编译后再开始计时

12.3.3 对代码路径的不真实采样

动态编译器会对单线程测试程序进行优化,最好多线程测试和单线程测试混合使用(测试用例至少用两个线程)

12.3.4 不真实的竞争情况

并发性能测试程序应该尽量接近真实应用程序的线程本地计算,并考虑并发协作。例如,多线程访问同步Map,如果本地计算过长,那么锁竞争情况就较弱,可能得出错误的性能瓶颈结果

12.3.5 无用代码的删除

无用代码:对结果没有影响的代码
由于基准测试通常不计算任何东西,很容易被优化器删除,这样测试的执行时间就会变短
解决方法是计算某个派生类的散列值,与任意值比较,加入相等就输出一个无用且可被忽略的消息

12.4 补充的测试方法

  1. 代码审查
  2. 静态代码分析
  3. 面向切面的测试工具
  4. 分析与检测工具

Part 4 高级主题

Chapter 13 显示锁

Java 5 之前,对共享数据的协调访问机制只有 synchronized 和 volatile,Java 5 增加了 ReentrantLock。

13.1 Lock和ReentrantLock

Lock接口定义加锁和解锁的操作。
ReentrantLock还提供了可重入特性

显示锁和内置锁很像,显示锁出现的原因是内置锁有一些功能限制

  1. 不能中断等待锁的线程
  2. 必须在获得锁的地方释放锁

13.1.1 轮询和定时获得锁

tryLock:轮询和定时获得锁
内置锁碰到死锁是致命的,唯一恢复方法是重启,唯一防御方法是统一锁获取顺序,tryLock可以概率避免死锁

13.1.2 可中断的获得锁

lockInterruptibly,调用后一直阻塞直至获得锁,但是接受中断信号

13.1.3 非块结构加锁

内置锁是块结构的加锁和自动释放锁,有时需要更大的灵活性,例如基于hash的集合可以使用分段锁

13.2 性能考虑

从Java 6 开始,内置锁已经不比显式锁性能差

13.3 公平性

内置锁不保证公平,ReentrantLock默认也不保证公平,非公平锁可以插队(不提倡,但是不阻止),性能相比公平锁会好一些

13.4 在 Synchronized 和 ReentrantLock 中选择

当你需要用到轮询和定时加锁、可中断的加锁、公平等待锁和非块结构加锁,使用 ReentrantLock,否则使用 synchronized

13.5 读写锁

读写锁:资源可以被多个读者同时访问或者单个写者访问

ReadWriteLock 定义读锁和写锁方法,和 Lock 类似,实现类在性能、调度、获得锁的优先条件、公平等方面可以不同

Chapter 14 构建自定义的同步工具

最简单的方式使用已有类进行构造,例如LinkedBlockingQueue、CountDown-Latch、Semaphore和FutureTask等

14.1 状态依赖性的管理

单线程中,基于状态的前置条件不满足就失败。但是多线程中,状态会被其它线程修改,所以多线程程序在不满足前置条件时可以等待直至满足前置条件

14.1.1 将前置条件的失败传播给调用者

不满足前置条件就抛异常是滥用异常。调用者可以自旋等待(RUNNABLE态,占用cpu)或者阻塞(waiting态,不占cpu),即需要调用者编写前置条件管理的代码

14.1.2 通过轮询和睡眠粗鲁的阻塞

通过轮询和睡眠完成前置条件管理,不满足是就阻塞,调用者不需要管理前置条件,但需要处理 InterruptedException

14.1.3 条件队列

Object的wait,notify 和 notifyAll构成内置条件队列的API,wait会释放锁(本质和轮询与休眠是一样的,注意sleep前要释放锁)

14.2 使用条件队列

14.2.1 条件谓词

条件谓词:由类的状态变量构造的表达式,例如缓冲区非空即count>0

给条件队列相关的条件谓词以及等待它成立的操作写Javadoc

条件谓词涉及状态变量,状态变量由锁保护,所以在测试条件谓词之前,需要先获得锁。锁对象和条件队列对象(调用wait和notify的对象)必须是同一个对象

14.2.2 过早唤醒

一个线程由于其它线程调用notifyAll醒来,不意味着它的等待条件谓词一定为真。每当线程醒来必须再次测试条件谓词(使用循环)

14.2.3 丢失的信号

线程必须等待一个已经为真的条件,但是在开始等待之前没有检查条件谓词,发生的原因是编码错误,正确写法是循环测试条件谓词,false就继续wait

14.2.4 通知

优先使用 notifyAll,notify 可能会出现“hijacked signal”问题,唤醒了一个条件还未真的线程,本应被唤醒的线程还在等待。只有所有线程都在等同一个条件谓词且通知最多允许一个线程继续执行才使用notify

14.2.5 例子:门

用条件队列实现一个可以重复开关的线程门

14.2.6 子类的安全问题

一个依赖状态的类应该完全向子类暴露它的等待和通知协议,或者禁止继承

14.2.7 封装条件队列

最好将条件队列封装起来,在使用它的类的外面无法访问

14.2.8 进入和退出协议

进入协议:操作的条件谓词
退出协议:检查该操作修改的所有状态变量,确认他们是否使某个条件谓词成真,若是,通知相关队列

14.3 显示 Condition

显示锁在一些情况下比内置锁更灵活。类似地,Condition 比内置条件队列更灵活

内置条件队列有几个缺点:

  1. 每个内置锁只能关联一个条件队列,即多个线程可能会在同一个条件队列上等待不同的条件谓词
  2. 最常见的加锁模式会暴露条件队列

一个 Condition 关联一个 Lock,就像内置条件队列关联一个内置锁,使用 Lock.newCondition 来创建 Condition,Condition比内置条件队列功能丰富:每个锁有多个等待集(即一个Lock可以创建多个Condition),可中断和不可中断的条件等待,基于截止时间的等待,以及公平或不公平排队的选择。

Condition中和wait,notify,notifyAll 对应的方法是 await,signal,signalAll

14.4 Synchronizer剖析

ReentrantLock 和 Semaphore 有很多相似的地方。ReentrantLock可以作为只许一个线程进入的 Semaphore,Semaphore 可以用 ReentrantLock 实现

它们和其它同步器一样依赖基类 AbstractQueuedSynchronizer(AQS)。AQS是一个用于构建锁和同步器的框架,使用它可以轻松有效地构建范围广泛的同步器。

14.5 AbstractQueuedSynchronizer

依赖AQS的同步器的基本操作是获取和释放的一些变体。获取是阻塞操作,调用者获取不到会进入WAITING或失败。对于锁或信号量,获取的意思是获取锁或许可。对于CountDownLatch,获取的意思是等待门闩到达终点。对于FutureTask,获取的意思是等待任务完成。释放不是阻塞操作。同步器还会根据各自的语义维护状态信息

14.6 JUC同步器类中的AQS

JUC许多类使用AQS,例如 ReentrantLock, Semaphore, ReentrantReadWriteLock, CountDownLatch, SynchronousQueue, FutureTask

Chapter 15 原子变量与非阻塞同步机制

非阻塞算法使用原子机器指令,例如 compare-and-swap 取代锁来实现并发下的数据完成性。它的设计比基于锁的算法复杂但可以提供更好的伸缩性和活跃性,非阻塞算法不会出现阻塞、死锁或其它活跃性问题,不会受到单个线程故障的影响

15.1 锁的劣势

JVM对非竞争锁进行优化,但是如果多个线程同时请求锁,就要借助操作系统挂起或者JVM自旋,开销很大。相比之下volatile是更轻量的同步机制,不涉及上下文切换和线程调度,然后volatile相较于锁,它不能构造原子性的复合操作,例如自增

锁还会出现优先级反转(阻塞线程优先级高,但是后执行),死锁等问题

15.2 并发操作的硬件支持

排它锁是悲观锁,总是假设最坏情况,只有确保其它线程不会干扰才会执行

乐观方法依赖碰撞检测来确定在更新过程中是否有其它线程的干扰,若有则操作失败并可以选择重试

为多处理器设计的cpu提供了对共享变量并发访问的特殊指令,例如 compare‐and‐swap,load-linked

15.2.1 Compare and Swap

CAS 有三个操作数:内存地址V,期待的旧值A,新值B。CAS在V的旧值是A的情况下原子更新值为B,否则什么都不做。CAS是一种乐观方法:它满怀希望更新变量,如果检测到其它线程更新了变量,它就会失败。CAS失败不会阻塞,允许重试(一般不重试,失败可能意味着别的线程已经完成该工作)

15.2.2 非阻塞的计数器

在竞争不激烈的情况下,性能比锁优秀。缺点是强制调用者处理竞争问题(重试、后退或放弃),而锁通过阻塞自动处理争用,直到锁可用

15.2.3 JVM对CAS的支持

JVM将CAS编译成底层硬件提供的方法,加入底层硬件不支持CAS,JVM会使用自旋锁。原子变量类使用了CAS

15.3 原子变量类

原子变量比锁的粒度更细,重量更轻,能提供volatile不支持的原子性

  1. 原子变量可以作为更好的 volatile
  2. 在高度竞争情况下,锁性能更好,正常情况下,原子变量性能更好

15.4 非阻塞的算法

如果一个线程的故障或挂起不会导致另一个线程的故障或挂起,则该算法称为非阻塞算法;如果一个算法在每个执行步骤中都有线程能够执行,那么这个算法被称为无锁算法。如果构造正确,只使用CAS进行线程间协调的算法可以是无阻塞和无锁的

15.4.1 非阻塞的栈

创建非阻塞算法的关键是如何在保持数据一致性的同时,将原子性更改的范围限制在单个变量内。

非阻塞的栈使用CAS来修改顶部元素

15.4.2 非阻塞的链表

Michale-scott算法

15.4.3 原子字段更新器

原子字段更新器代表了现有 volatile 字段的基于反射的“视图”,以便可以在现有的 volatile 字段上使用CAS

15.4.4 ABA问题

大部分情况下,CAS会询问“V的值还是A吗?”,是A就更新。但是有时候,我们需要知道“从我上次观察到V是A以来,它的值有没有改变?”。对于某些算法,将V的值从A->B,再从B->A,是一种更改,需要重新执行算法。解决方法是使用版本号,即使值从A变成B再变回A,版本号也会不同

Chapter 16 Java 内存模型

16.1 内存模型是什么,为什么我需要一个内存模型?

在并发没有同步的情况下,有许多原因导致一个线程不能立即或永远不能再另一个线程中看到操作的结果

  1. 编译器生成的指令顺序与源码不同
  2. 变量存在寄存器中而不是内存中
  3. 处理器可以并行或乱序执行指令
  4. 缓存可能会改变写入变量到主内存的顺序
  5. 存储在处理器本地缓存中的值可能对其它处理器不可见

16.1.1 平台的内存模型

在共享存储的多处理器体系结构中,每个处理器都有自己的高速缓存,这些告诉缓存周期性地与主存协调。

体系结构的内存模型告诉程序可以从内存系统得到什么一致性保证,并制定所需的特殊指令(内存屏障或栅栏),以在共享数据时获得所需的额外内存协调保证。为了不受跨体系结构的影响,Java提供了自己的内存模型,JVM通过在适当的位置插入内存屏障来处理JMM(Java内存模型)和和底层平台的内存模型之间的差异

顺序一致性:程序中所有操作都有一个单一的顺序,而不管他们在什么处理器上执行,并且每次读取变量都会看到任何处理器按执行顺序对该变量的最后一次写入。

现代处理器没有提供顺序一致性,JMM也没有

16.1.2 重排

指令重排会使程序的行为出乎意料。同步限制了编译器、运行时和硬件在重排序时不会破坏JMM提供的可见性保证

16.1.3 Java 内存模型

Java 内存模型由一些操作指定,包括对变量的读写、监视器的锁定和解锁。JMM对所有操作定义了一个称为 happens before 的偏序规则:

  • 程序顺序规则:线程按程序定义的顺序执行操作
  • 监视器锁规则:监视器锁的解锁必须发生在后续的加锁之前
  • volatile 变量规则:对 volatile 字段的写入操作必须发生在后续的读取之前
  • 线程启动规则:对线程调用Thread.start会在该线程所有操作之前执行
  • 线程结束规则:线程中任何操作必须在其它线程检测到该线程已经结束之前执行或者从Thread.join返回或者Thread.isAlive返回false
  • 中断规则:一个线程对另一个线程调用 interrupt 发生在被中断线程检测到中断之前
  • 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成
  • 传递性:A发生在B之前,B发生在C之前,那么A发生在C之前

16.1.4 借用同步

通过类库保证 happens-before顺序:

  • 将元素放入线程安全的容器发生在另一个线程从集合中检索元素之前
  • 在CountDownLatch上进行倒数发生在该线程从门闩的await返回之前
  • 释放信号量的许可发生在获取之前
  • Future代表的任务执行的操作发生在另一个线程从Future.get返回之前
  • 提交Runnable或者Callable任务给执行器发生在任务开始之前
  • 线程到达CyclicBarrier或Exchanger发生在其它线程释放相同的barrier或者exchange point之前

16.2 发布

16.2.1 不安全的发布

当缺少happens-before关系时候,就可能出现重排序问题,这就解释了为什么在没有同步情况下发布一个对象会导致另一个线程看到一个只被部分构造的对象

除了不可变对象之外,使用由其他线程初始化的对象都是不安全的,除非对象的发布发生在消费线程使用它之前

16.2.2 安全发布

使用锁或者volatile变量可以确保读写操作按照 happens-before 排序

16.2.3 安全初始化

静态字段在声明时就初始化由JVM提供线程安全保证。
延迟初始化,可以写在同步方法里面,或者使用辅助类,在辅助类中声明并初始化

16.2.4 双重检查锁

Java 5之前的双重检查锁会出现引用是新值但是对象是旧值,这意味着可以看到对象不正确的状态,Java5之后给引用声明加上 volatile 可以起到线程安全地延迟初始化作用,但是不如使用辅助类,效果一样且更容易懂

16.3 初始化的安全性

初始化安全性只能保证通过final字段可达的值从构造过程完成时开始的可见性(事实不可变对象以任何形式发布都是安全的)。对于通过非final字段可达的值,或者构成完成之后可能改变的值,必须采用同步确保可见性。

]]>
+ + + + + <p>本书分成四部分,<br /> +第一部分是基础,主要内容是并发的基础概念和线程安全,以及如何用java类库提供的并发构建块组成线程安全的类(2-5章节)</p> +<p>第二部分是构建并发应用程序,主要内容是如何利用线程来提高并发应用的吞吐量和响应能力(6-9章节)</p> +< + + + + + + + + + + + +
+ + + 2022.06-2022.11实习总结 + + https://zunpan.github.io/2022/12/03/2022.06-2022.11%E5%AE%9E%E4%B9%A0%E6%80%BB%E7%BB%93/ + 2022-12-03T11:29:40.000Z + 2023-09-24T04:27:40.272Z + + 基于JSON Schema的配置平台

背景:这是公司的新人入职练手项目,总共两周时间,实现一个基于JSON Schema的表单配置平台。
需求:表单配置指的是通过可视化界面配置 表单的每个字段,包括名称、输入类型、输入限制等。mentor给了一个方向叫JSON Schema
调研:JSON Schema是描述json数据的元数据,本身也是json字符串,一般两个作用,1. 后端用 JSON Schema 对前端传的json传进行格式校验;2. 前端通过 JSON Schema生成表单

图 3

开发:前端使用Vue、后端使用Spring Boot
难点:JSON Schema和表单的双向转换。用户可以手动编辑JSON Schema生成表单项,也可以通过可视化界面增加表单项来修改JSON Schema。
解决方案:尝试过写一套解析方案,但是dom操作太复杂作罢。调研了一些开源方案,最终选用vue-json-schema-form

Excel比对与合并系统

背景:接手的第一个项目,关于Excel比对与合并,主要参与系统的优化与维护工作
主要工作:

  1. 批量文件比对的多线程优化(比对方法涉及对象完全栈封闭)
  2. 批量文件合并OOM排查(合并需要先反序列化比对结果,若有多个版本的比对结果需要合并到另一分支上的同名文件,需要循环处理,每个比对结果合并之后需要置空,否则内存无法释放,排查工具:visualvm,发现调用合并方法时,Minor GC非常快,内存居高不下导致OOM;根本原因:自己开发的Excel解析工具+Java自带的序列化导致序列化产物非常大)

算法介绍可以参看《Excel比对与合并系统》

助理系统

需求:公司员工反馈行政问题都是在公司的聊天软件里反馈,反馈途径包括事业群、私聊行政助理、以及服务号反馈(类似微信公众号),行政助理需要在多个系统进行处理,助理系统为助理统一了消息来源,助理可以在助理系统中回复所有渠道的问题反馈。
调研:spring-boot-starter-websocket,实现了客户端与服务器全双工通信
难点:助理系统需要给每个反馈问题的员工生成唯一的对话,初次反馈消息时,快速发送消息会创建多个对话。这是因为后端多线程处理消息,每个线程都先去数据库查询此条消息的员工是否存在对话,如果不存在就创建。这里出现了并发经典错误 check-then-act。
解决方案:给会话表的员工id字段建唯一索引,插入新会话使用insert ignore。

]]>
+ + + + + <h2 id="基于json-schema的配置平台"><a class="markdownIt-Anchor" href="#基于json-schema的配置平台"></a> 基于JSON Schema的配置平台</h2> +<p>背景:这是公司的新人入职练手项目,总共两周时间, + + + + + + + + + +
+ + + Effective-Java学习笔记(九) + + https://zunpan.github.io/2022/08/13/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B9%9D%EF%BC%89/ + 2022-08-13T08:11:11.000Z + 2023-09-24T04:27:40.274Z + + 第十章 异常

69. 仅在确有异常条件下使用异常

有人认为用 try-catch 和 while(true) 遍历数组比用 for-each 性能更好,因为 for-each 由编译器隐藏了边界检查,而 try-catch 代码中不包含检查

// Horrible abuse of exceptions. Don't ever do this!try {    int i = 0;    while(true){        range[i++].climb();    }    catch (ArrayIndexOutOfBoundsException e) {}}

这个想法有三个误区:

  1. 因为异常是为特殊情况设计的,所以 JVM 实现几乎不会让它们像显式测试一样快。
  2. 将代码放在 try-catch 块中会抑制 JVM 可能执行的某些优化。
  3. 遍历数组的标准习惯用法不一定会导致冗余检查。许多 JVM 实现对它们进行了优化。

基于异常的循环除了不能提高性能外,还容易被异常隐藏循环中的 bug。因此,异常只适用于确有异常的情况;它们不应该用于一般的控制流程。

一个设计良好的 API 不能迫使其客户端为一般的控制流程使用异常。调用具有「状态依赖」方法的类,通常应该有一个单独的「状态测试」方法,表明是否适合调用「状态依赖」方法。例如,Iterator 接口具有「状态依赖」的 next 方法和对应的「状态测试」方法 hasNext。

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {    Foo foo = i.next();    ...}

如果 Iterator 缺少 hasNext 方法,客户端将被迫这样做:

// Do not use this hideous code for iteration over a collection!try {    Iterator<Foo> i = collection.iterator();    while(true) {        Foo foo = i.next();        ...    }}catch (NoSuchElementException e) {}

这与一开始举例的对数组进行迭代的例子非常相似,除了冗长和误导之外,基于异常的循环执行效果可能很差,并且会掩盖系统中不相关部分的 bug。

提供单独的「状态测试」方法的另一种方式,就是让「状态依赖」方法返回一个空的 Optional 对象(Item-55),或者在它不能执行所需的计算时返回一个可识别的值,比如 null。

状态测试方法、Optional、可识别的返回值之间的选择如下:

  1. 如果要在没有外部同步的情况下并发地访问对象,或者受制于外部条件的状态转换,则必须使用 Optional 或可识别的返回值,因为对象的状态可能在调用「状态测试」方法与「状态依赖」方法的间隔中发生变化。
  2. 如果一个单独的「状态测试」方法重复「状态依赖」方法的工作,从性能问题考虑,可能要求使用 Optional 或可识别的返回值
  3. 在所有其他条件相同的情况下,「状态测试」方法略优于可识别的返回值。它提供了较好的可读性,而且不正确的使用可能更容易被检测:如果你忘记调用「状态测试」方法,「状态依赖」方法将抛出异常,使错误显而易见;
  4. 如果你忘记检查一个可识别的返回值,那么这个 bug 可能很难发现。但是这对于返回 Optional 对象的方式来说不是问题。

总之,异常是为确有异常的情况设计的。不要将它们用于一般的控制流程,也不要编写强制其他人这样做的 API。

70. 对可恢复情况使用 checked 异常,对编程错误使用运行时异常

Java 提供了三种可抛出项:checked 异常、运行时异常和错误。决定是使用 checked 异常还是 unchecked 异常的基本规则是:使用 checked 异常的情况是为了合理地期望调用者能够从中恢复。

有两种 unchecked 的可抛出项:运行时异常和错误。它们在行为上是一样的:都是可抛出的,通常不需要也不应该被捕获。如果程序抛出 unchecked 异常或错误,通常情况下是不可能恢复的,如果继续执行,弊大于利。如果程序没有捕获到这样的可抛出项,它将导致当前线程停止,并发出适当的错误消息。

运行时异常

运行时异常用来指示编程错误。大多数运行时异常都表示操作违反了先决条件。违反先决条件是指使用 API 的客户端未能遵守 API 规范所建立的约定。例如,数组访问约定指定数组索引必须大于等于 0 并且小于等于 length-1 (length:数组长度)。ArrayIndexOutOfBoundsException 表示违反了此先决条件

这个建议存在的问题是:并不总能清楚是在处理可恢复的条件还是编程错误。例如,考虑资源耗尽的情况,这可能是由编程错误(如分配一个不合理的大数组)或真正的资源短缺造成的。如果资源枯竭是由于暂时短缺或暂时需求增加造成的,这种情况很可能是可以恢复的。对于 API 设计人员来说,判断给定的资源耗尽实例是否允许恢复是一个问题。如果你认为某个条件可能允许恢复,请使用 checked 异常;如果没有,则使用运行时异常。如果不清楚是否可以恢复,最好使用 unchecked 异常

错误

虽然 Java 语言规范没有要求,但有一个约定俗成的约定,即错误保留给 JVM 使用,以指示:资源不足、不可恢复故障或其他导致无法继续执行的条件。考虑到这种约定被大众认可,所以最好不要实现任何新的 Error 子类。因此,你实现的所有 unchecked 异常都应该继承 RuntimeException(直接或间接)。不仅不应该定义 Error 子类,而且除了 AssertionError 之外,不应该抛出它们。

自定义异常

自定义异常继承 Throwable 类,Java 语言规范把它们当做普通 checked 异常(普通 checked 异常是 Exception 的子类,但不是 RuntimeException的子类)。不要使用自定义异常,它会让 API 的用户困惑

异常附加信息

API 设计人员常常忘记异常是成熟对象,可以为其定义任意方法。此类方法的主要用途是提供捕获异常的代码,并提供有关引发异常的附加信息。如果缺乏此类方法,程序员需要自行解析异常的字符串表示以获取更多信息。这是极坏的做法

因为 checked 异常通常表示可恢复的条件,所以这类异常来说,设计能够提供信息的方法来帮助调用者从异常条件中恢复尤为重要。例如,假设当使用礼品卡购物由于资金不足而失败时,抛出一个 checked 异常。该异常应提供一个访问器方法来查询差额。这将使调用者能够将金额传递给购物者。

总而言之,为可恢复条件抛出 checked 异常,为编程错误抛出 unchecked 异常。当有疑问时,抛出 unchecked 异常。不要定义任何既不是 checked 异常也不是运行时异常的自定义异常。应该为 checked 异常设计相关的方法,如提供异常信息,以帮助恢复。

71. 避免不必要地使用 checked 异常

合理抛出 checked 异常可以提高程序可靠性。过度使用会使得调用它的方法多次 try-catch 或抛出,给 API 用户带来负担,尤其是 Java8 中,抛出checked 异常的方法不能直接在流中使用。

只有在正确使用 API 也无法避免异常且使用 API 的程序员在遇到异常时可以采取一些有用的操作才能使用 checked 异常,否则抛出 unchecked 异常

如果 checked 异常是方法抛出的唯一 checked 异常,那么 checked 异常给程序员带来的额外负担就会大得多。如果还有其他 checked 异常,则该方法一定已经在 try 块中了,因此该异常最多需要另一个 catch 块而已。如果一个方法抛出单个 checked 异常,那么这个异常就是该方法必须出现在 try 块中而不能直接在流中使用的唯一原因。在这种情况下,有必要问问自己是否有办法避免 checked 异常。

消除 checked 异常的最简单方法是返回所需结果类型的 Optional 对象(Item-55)。该方法只返回一个空的 Optional 对象,而不是抛出一个 checked 异常。这种技术的缺点是,该方法不能返回任何详细说明其无法执行所需计算的附加信息。相反,异常具有描述性类型,并且可以导出方法来提供附加信息(Item-70)

总之,如果谨慎使用,checked 异常可以提高程序的可靠性;当过度使用时,它们会使 API 难以使用。如果调用者不应从失败中恢复,则抛出 unchecked 异常。如果恢复是可能的,并且你希望强制调用者处理异常情况,那么首先考虑返回一个 Optional 对象。只有当在失败的情况下,提供的信息不充分时,你才应该抛出一个 checked 异常。

72. 鼓励复用标准异常

Java 库提供了一组异常,涵盖了大多数 API 的大多数异常抛出需求。

复用标准异常有几个好处:

  1. 使你的 API 更容易学习和使用,因为它符合程序员已经熟悉的既定约定
  2. 使用你的 API 的程序更容易阅读,因为它们不会因为不熟悉的异常而混乱
  3. 更少的异常类意味着更小的内存占用和更少的加载类的时间

常见的被复用的异常:

  1. IllegalArgumentException。通常是调用者传入不合适的参数时抛出的异常

  2. IllegalStateException。如果因为接收对象的状态导致调用非法,则通常会抛出此异常。例如,调用者试图在对象被正确初始化之前使用它

    可以说,每个错误的方法调用都归结为参数非法或状态非法,但是有一些异常通常用于某些特定的参数非法和状态非法。如果调用者在禁止空值的参数中传递 null,那么按照惯例,抛出 NullPointerException 而不是 IllegalArgumentException。类似地,如果调用者将表示索引的参数中的超出范围的值传递给序列,则应该抛出 IndexOutOfBoundsException,而不是 IllegalArgumentException

  3. ConcurrentModificationException。如果一个对象被设计为由单个线程使用(或与外部同步),并且检测到它正在被并发地修改,则应该抛出该异常。因为不可能可靠地检测并发修改,所以该异常充其量只是一个提示。

  4. UnsupportedOperationException。如果对象不支持尝试的操作,则抛出此异常。它很少使用,因为大多数对象都支持它们的所有方法。此异常用于一个类没有实现由其实现的接口定义的一个或多个可选操作。例如,对于只支持追加操作的 List 实现,试图从中删除元素时就会抛出这个异常

不要直接复用 Exception、RuntimeException、Throwable 或 Error

此表总结了最常见的可复用异常:

ExceptionOccasion for Use
IllegalArgumentExceptionNon-null parameter value is inappropriate(非空参数值不合适)
IllegalStateExceptionObject state is inappropriate for method invocation(对象状态不适用于方法调用)
NullPointerExceptionParameter value is null where prohibited(禁止参数为空时仍传入 null)
IndexOutOfBoundsExceptionIndex parameter value is out of range(索引参数值超出范围)
ConcurrentModificationExceptionConcurrent modification of an object has been detected where it is prohibited(在禁止并发修改对象的地方检测到该动作)
UnsupportedOperationExceptionObject does not support method(对象不支持该方法调用)

其它异常如果有合适的复用场景也可以复用,例如,如果你正在实现诸如复数或有理数之类的算术对象,那么复用 ArithmeticException 和 NumberFormatException 是合适的

73. 抛出适合底层抽象异常的高层异常

当方法抛出一个与它所执行的任务没有明显关联的异常时,这是令人不安的。这种情况经常发生在由方法传播自低层抽象抛出的异常。它不仅令人不安,而且让实现细节污染了上层的 API。

为了避免这个问题,高层应该捕获低层异常,并确保抛出的异常可以用高层抽象解释。 这个习惯用法称为异常转换:

// Exception Translationtry {    ... // Use lower-level abstraction to do our bidding} catch (LowerLevelException e) {    throw new HigherLevelException(...);}

下面是来自 AbstractSequentialList 类的异常转换示例,该类是 List 接口的一个框架实现(Item-20)。在本例中,异常转换是由 List<E> 接口中的 get 方法规范强制执行的:

/*** Returns the element at the specified position in this list.* @throws IndexOutOfBoundsException if the index is out of range* ({@code index < 0 || index >= size()}).*/public E get(int index) {    ListIterator<E> i = listIterator(index);    try {        return i.next();    }    catch (NoSuchElementException e) {        throw new IndexOutOfBoundsException("Index: " + index);    }}

如果低层异常可能有助于调试高层异常的问题,则需要一种称为链式异常的特殊异常转换形式。低层异常(作为原因)传递给高层异常,高层异常提供一个访问器方法(Throwable 的 getCause 方法)来检索低层异常:

// Exception Chainingtry {    ... // Use lower-level abstraction to do our bidding}catch (LowerLevelException cause) {    throw new HigherLevelException(cause);}

高层异常的构造函数将原因传递给能够接收链式异常的父类构造函数,因此它最终被传递给 Throwable 的一个接收链式异常的构造函数,比如 Throwable(Throwable):

// Exception with chaining-aware constructorclass HigherLevelException extends Exception {    HigherLevelException(Throwable cause) {        super(cause);    }}

大多数标准异常都有接收链式异常的构造函数。对于不支持链式异常的异常,可以使用 Throwable 的 initCause 方法设置原因。异常链不仅允许你以编程方式访问原因(使用 getCause),而且还将原因的堆栈跟踪集成到更高层异常的堆栈跟踪中。

虽然异常转换优于底层异常的盲目传播,但它不应该被过度使用。在可能的情况下,处理底层异常的最佳方法是确保底层方法避免异常。有时,你可以在将高层方法的参数传递到底层之前检查它们的有效性。

如果不可能从底层防止异常,那么下一个最好的方法就是让高层静默处理这些异常,使较高层方法的调用者免受底层问题的影响。在这种情况下,可以使用一些适当的日志工具(如 java.util.logging)来记录异常。这允许程序员研究问题,同时将客户端代码和用户与之隔离。

总之,如果无法防止或处理来自底层的异常,则使用异常转换,但要保证底层方法的所有异常都适用于较高层。链式异常提供了兼顾两方面的最佳服务:允许抛出适当的高层异常,同时捕获并分析失败的潜在原因

74. 为每个方法记录会抛出的所有异常

  1. 始终单独声明 checked 异常,并使用 Javadoc 的 @throw 标记精确记录每次抛出异常的条件。如果一个方法抛出多个异常,不要使用快捷方式声明这些异常的父类。作为一个极端的例子,即不要在公共方法声明 throws Exception,除了只被 JVM 调用的 main方法
  2. unchecked 异常不要声明 throws,但应该像 checked 异常一样用 Javadoc 记录他们。特别是接口中的方法要记录可能抛出的 unchecked 异常。

如果一个类中的许多方法都因为相同的原因抛出异常,你可以在类的文档注释中记录异常, 而不是为每个方法单独记录异常。一个常见的例子是 NullPointerException。类的文档注释可以这样描述:「如果在任何参数中传递了 null 对象引用,该类中的所有方法都会抛出 NullPointerException」

总之,记录你所编写的每个方法可能引发的每个异常。对于 unchecked 异常、checked 异常、抽象方法、实例方法都是如此。应该在文档注释中采用 @throw 标记的形式。在方法的 throws 子句中分别声明每个 checked 异常,但不要声明 unchecked 异常。如果你不记录方法可能抛出的异常,其他人将很难或不可能有效地使用你的类和接口。

75. 异常详细消息中应包含捕获失败的信息

程序因为未捕获异常而失败时,系统会自动调用异常的 toString 方法打印堆栈信息,堆栈信息包含异常的类名及详细信息。异常的详细消息应该包含导致异常的所有参数和字段的值。例如,IndexOutOfBoundsException 的详细消息应该包含下界、上界和未能位于下界之间的索引值。

异常详细信息不用过于冗长,程序失败时可以通过阅读文档和源代码收集信息,确保异常包含足够的信息的一种方法是在构造函数中配置异常信息,例如,IndexOutOfBoundsException 构造函数不包含 String 参数,而是像这样:

/*** Constructs an IndexOutOfBoundsException.**@param lowerBound the lowest legal index value* @param upperBound the highest legal index value plus one* @param index the actual index value*/public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {    // Generate a detail message that captures the failure    super(String.format("Lower bound: %d, Upper bound: %d, Index: %d",lowerBound, upperBound, index));    // Save failure information for programmatic access    this.lowerBound = lowerBound;    this.upperBound = upperBound;    this.index = index;}

76. 尽力保证故障原子性

失败的方法调用应该使对象处于调用之前的状态。 具有此属性的方法称为具备故障原子性。

有几种实现故障原子性的方式:

关于不可变对象

  1. 不可变对象在创建后永远处于一致状态

关于可变对象:

  1. 在修改状态前,先执行可能抛出异常的操作,例如检查状态,不合法就抛出异常
  2. 在临时副本上操作,成功后替换原来的对象,不成功不影响原来的对象。例如,一些排序函数会将入参 list 复制到数组中,对数组进行排序,再转换成 list
  3. 编写回滚代码,主要用于持久化的数据

有些情况是不能保证故障原子性的,例如,多线程不同步修改对象,对象可能处于不一致状态,当捕获到 ConcurrentModificationException 后对象不可恢复

总之,作为方法规范的一部分,生成的任何异常都应该使对象保持在方法调用之前的状态。如果违反了这条规则,API 文档应该清楚地指出对象将处于什么状态。

77. 不要忽略异常

异常要么 try-catch 要么抛出,不要写空的 catch 块,如果这样做,写上注释

]]>
+ + + + + <h2 id="第十章-异常"><a class="markdownIt-Anchor" href="#第十章-异常"></a> 第十章 异常</h2> +<h3 id="69-仅在确有异常条件下使用异常"><a class="markdownIt-Anchor" href="#6 + + + + + + + + + + + +
+ + + Effective-Java学习笔记(八) + + https://zunpan.github.io/2022/08/06/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AB%EF%BC%89/ + 2022-08-06T07:30:09.000Z + 2023-09-24T04:27:40.275Z + + 第九章 通用程序设计

57. 将局部变量的作用域最小化

本条目在性质上类似于Item-15,即「最小化类和成员的可访问性」。通过最小化局部变量的范围,可以提高代码的可读性和可维护性,并降低出错的可能性。

  1. 将局部变量的作用域最小化,最具说服力的方式就是在第一次使用它的地方声明
  2. 每个局部变量声明都应该包含一个初始化表达式。 如果你还没有足够的信息来合理地初始化一个变量,你应该推迟声明,直到条件满足

58. for-each循环优于for循环

for-each 更简洁更不容易出错,且没有性能损失,只有三种情况不能用for-each

  1. 破坏性过滤。如果需要遍历一个集合并删除选定元素,则需要使用显式的迭代器,以便调用其 remove 方法。通过使用 Collection 在 Java 8 中添加的 removeIf 方法,通常可以避免显式遍历。
  2. 转换。如果需要遍历一个 List 或数组并替换其中部分或全部元素的值,那么需要 List 迭代器或数组索引来替换元素的值。
  3. 并行迭代。如果需要并行遍历多个集合,那么需要显式地控制迭代器或索引变量,以便所有迭代器或索引变量都可以同步执行

59. 了解并使用库

假设你想要生成 0 到某个上界之间的随机整数,有些程序员会写出如下代码:

// Common but deeply flawed!static Random rnd = new Random();static int random(int n) {    return Math.abs(rnd.nextInt()) % n;}

这个方法有三个缺点:

  1. 如果 n 是比较小的 2 的幂,随机数序列会在相当短的时间内重复
  2. 如果 n 不是 2 的幂,一些数字出现的频率会更高,当 n 很大时效果明显
  3. 会返回超出指定范围的数字,当nextInt返回Integer.MIN_VALUE时,abs方法也会返回Integer.MIN_VALUE,假设 n 不是 2 幂,那么Integer.MIN_VALUE % n 将返回负数

我们不需要为这个需求自己编写方法,已经存在经过专家设计测试的标准库Random 的 nextInt(int)

从 Java 7 开始,就不应该再使用 Random。在大多数情况下,选择的随机数生成器现在是 ThreadLocalRandom。 它能产生更高质量的随机数,而且速度非常快。

总而言之,不要白费力气重新发明轮子。如果你需要做一些看起来相当常见的事情,那么库中可能已经有一个工具可以做你想做的事情。如果有,使用它;如果你不知道,查一下。一般来说,库代码可能比你自己编写的代码更好,并且随着时间的推移可能会得到改进。

60. 若需要精确答案就应避免使用 float 和 double 类型

float 和 double 类型主要用于科学计算和工程计算。它们执行二进制浮点运算,该算法经过精心设计,能够在很大范围内快速提供精确的近似值。但是,它们不能提供准确的结果。float 和 double 类型特别不适合进行货币计算,因为不可能将 0.1(或 10 的任意负次幂)精确地表示为 float 或 double

正确做法是使用 BigDecimal、int 或 long 进行货币计算。还要注意 BigDecimal 的构造函数要使用String参数而不是double,避免初始化时就用了不精确的值。int和long可以存储较小单位的值,将小数转换成整数存储

总之,对于任何需要精确答案的计算,不要使用 float 或 double 类型。如果希望系统来处理十进制小数点,并且不介意不使用基本类型带来的不便和成本,请使用 BigDecimal。使用 BigDecimal 的另一个好处是,它可以完全控制舍入,当执行需要舍入的操作时,可以从八种舍入模式中进行选择。如果你使用合法的舍入行为执行业务计算,这将非常方便。如果性能是最重要的,那么你不介意自己处理十进制小数点,而且数值不是太大,可以使用 int 或 long。如果数值不超过 9 位小数,可以使用 int;如果不超过 18 位,可以使用 long。如果数量可能超过 18 位,则使用 BigDecimal。

61. 基本数据类型优于包装类

Java 的类型系统有两部分,基本类型(如 int、double 和 boolean)和引用类型(如String和List)。每个基本类型都有一个对应的引用类型,称为包装类。如Integer、Double 和 Boolean

基本类型和包装类型之间的区别如下

  1. 基本类型只有它们的值,而包装类型具有与其值不同的标识。换句话说,两个包装类型实例可以具有相同的值和不同的标识。
  2. 基本类型只有全部功能值,而每个包装类型除了对应的基本类型的所有功能值外,还有一个非功能值,即 null
  3. 基本类型比包装类型更节省时间和空间
  4. 用 == 来比较包装类型几乎都是错的
  5. 在操作中混用基本类型和包装类型时,包装类型会自动拆箱,可能导致 NPE,如果操作结果保存到包装类型的变量中,还会发生自动装箱,导致性能问题

包装类型的用途如下:

  1. 作为集合的元素、键和值
  2. 泛型和泛型方法中的类型参数
  3. 反射调用

总之,只要有选择,就应该优先使用基本类型,而不是包装类型。基本类型更简单、更快。如果必须使用包装类型,请小心!自动装箱减少了使用包装类型的冗长,但没有减少危险。 当你的程序使用 == 操作符比较两个包装类型时,它会执行标识比较,这几乎肯定不是你想要的。当你的程序执行包含包装类型和基本类型的混合类型计算时,它将进行拆箱,当你的程序执行拆箱时,将抛出 NullPointerException。 最后,当你的程序将基本类型装箱时,可能会导致代价高昂且不必要的对象创建。

62. 其它类型更合适时应避免使用字符串

本条目讨论一些不应该使用字符串的场景

  1. 字符串是枚举类型的糟糕替代品
  2. 字符串是聚合类型的糟糕替代品。如果一个对象有多个字段,将其连接成一个字符串会出现许多问题,更好的方法是用私有静态成员类表示聚合
  3. 字符串是功能的糟糕替代品。例如全局缓存池要求 key 不能重复,如果用字符串做 key 可能在不同线程中出现问题

总之,当存在或可以编写更好的数据类型时,应避免将字符串用来表示对象。如果使用不当,字符串比其他类型更麻烦、灵活性更差、速度更慢、更容易出错。字符串经常被误用的类型包括基本类型、枚举和聚合类型。

63. 当心字符串连接引起的性能问题

字符串连接符(+)连接 n 个字符串的时间复杂度是 n2n^2。这是字符串不可变导致的

如果要连接的字符串数量较多,可以使用 StringBuilder 代替 String

64. 通过接口引用对象

  1. 如果存在合适的接口类型,那么应该使用接口类型声明参数、返回值、变量和字段。好处是代码可以非常方便切换性能更好或功能更丰富的实现,例如 HashMap 替换成 LinkedHashMap 可以保证迭代顺序和插入一致

  2. 如果没有合适的接口存在,那么用类引用对象是完全合适的。

    • 值类,如 String 和 BigInteger。值类很少在编写时考虑到多个实现。它们通常是 final 的,很少有相应的接口。使用这样的值类作为参数、变量、字段或返回类型非常合适。
    • 如果一个对象属于一个基于类的框架,那么就用基类引用它
    • 接口的实现类有接口没有的方法,例如 PriorityQueue 类有 Queue 接口没有的比较器方法

65. 接口优于反射

反射有几个缺点:

  1. 失去了编译时类型检查的所有好处,包括异常检查。如果一个程序试图反射性地调用一个不存在的或不可访问的方法,它将在运行时失败,除非你采取了特殊的预防措施(大量 try-catch)
  2. 反射代码既笨拙又冗长。写起来很乏味,读起来也很困难
  3. 反射调用方法比普通调用方法更慢

反射优点:

对于许多程序,它们必须用到在编译时无法获取的类,在编译时存在一个适当的接口或父类来引用该类(Item-64)。如果是这种情况,可以用反射方式创建实例,并通过它们的接口或父类正常地访问它们。

例如,这是一个创建 Set<String> 实例的程序,类由第一个命令行参数指定。程序将剩余的命令行参数插入到集合中并打印出来。不管第一个参数是什么,程序都会打印剩余的参数,并去掉重复项。然而,打印这些参数的顺序取决于第一个参数中指定的类。如果你指定 java.util.HashSet,它们显然是随机排列的;如果你指定 java.util.TreeSet,它们是按字母顺序打印的,因为 TreeSet 中的元素是有序的

// Reflective instantiation with interface accesspublic static void main(String[] args) {    // Translate the class name into a Class object    Class<? extends Set<String>> cl = null;    try {        cl = (Class<? extends Set<String>>) // Unchecked cast!        Class.forName(args[0]);    } catch (ClassNotFoundException e) {        fatalError("Class not found.");    }    // Get the constructor    Constructor<? extends Set<String>> cons = null;    try {        cons = cl.getDeclaredConstructor();    } catch (NoSuchMethodException e) {        fatalError("No parameterless constructor");    }    // Instantiate the set    Set<String> s = null;    try {        s = cons.newInstance();    } catch (IllegalAccessException e) {        fatalError("Constructor not accessible");    } catch (InstantiationException e) {        fatalError("Class not instantiable.");    } catch (InvocationTargetException e) {        fatalError("Constructor threw " + e.getCause());    } catch (ClassCastException e) {        fatalError("Class doesn't implement Set");    }    // Exercise the set    s.addAll(Arrays.asList(args).subList(1, args.length));    System.out.println(s);}private static void fatalError(String msg) {    System.err.println(msg);    System.exit(1);}

反射的合法用途(很少)是管理类对运行时可能不存在的其他类、方法或字段的依赖关系。如果你正在编写一个包,并且必须针对其他包的多个版本运行,此时反射将非常有用。该技术是根据支持包所需的最小环境(通常是最老的版本)编译包,并反射性地访问任何较新的类或方法。如果你试图访问的新类或方法在运行时不存在,要使此工作正常进行,则必须采取适当的操作。适当的操作可能包括使用一些替代方法来完成相同的目标,或者使用简化的功能进行操作

总之,反射是一种功能强大的工具,对于某些复杂的系统编程任务是必需的,但是它有很多缺点。如果编写的程序必须在编译时处理未知的类,则应该尽可能只使用反射实例化对象,并使用在编译时已知的接口或父类访问对象。

66. 明智地使用本地方法

JNI 允许 Java 调用本地方法,这些方法是用 C 或 C++ 等本地编程语言编写的。

历史上,本地方法主要有三个用途:

  1. 提供对特定于平台的设施(如注册中心)的访问
  2. 提供对现有本地代码库的访问,包括提供对遗留数据访问
  3. 通过本地语言编写应用程序中注重性能的部分,以提高性能

关于用途一:随着 Java 平台的成熟,它提供了对许多以前只能在宿主平台中上找到的特性。例如,Java 9 中添加的流 API 提供了对 OS 进程的访问。在 Java 中没有等效库时,使用本地方法来使用本地库也是合法的。

关于用途3:在早期版本(Java 3 之前),这通常是必要的,但是从那时起 JVM 变得更快了。对于大多数任务,现在可以在 Java 中获得类似的性能

本地方法的缺点:

  1. 会受到内存损坏错误的影响
  2. 垃圾收集器无法自动追踪本地方法的内存使用情况,导致性能下降

总之,在使用本地方法之前要三思。一般很少需要使用它们来提高性能。如果必须使用本地方法来访问底层资源或本地库,请尽可能少地使用本地代码,并对其进行彻底的测试。本地代码中的一个错误就可以破坏整个应用程序。

67. 明智地进行优化

关于优化的三条名言

  1. 以效率的名义(不一定能达到效率)犯下的计算错误比任何其他原因都要多——包括盲目的愚蠢
  2. 不要去计较效率上的一些小小的得失,在 97% 的情况下,不成熟的优化才是一切问题的根源。
  3. 在优化方面,我们应该遵守两条规则:规则 1:不要进行优化。规则 2 (仅针对专家):还是不要进行优化,也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化。

在设计系统时,我们要仔细考虑架构,好的架构允许它在后面优化,不良的架构导致很难优化。设计中最难更改的是组件之间以及组件与外部世界交互的组件,主要是 API、线路层协议和数据持久化格式,尽量避免做限制性能的设计

JMH 是一个微基准测试框架,主要是基于方法层面的基准测试,精度可以达到纳秒级

总而言之,不要努力写快的程序,要努力写好程序;速度自然会提高。但是在设计系统时一定要考虑性能,特别是在设计API、线路层协议和持久数据格式时。当你完成了系统的构建之后,请度量它的性能。如果足够快,就完成了。如果没有,利用分析器找到问题的根源,并对系统的相关部分进行优化。第一步是检查算法的选择:再多的底层优化也不能弥补算法选择的不足。根据需要重复这个过程,在每次更改之后测量性能,直到你满意为止。

68. 遵守被广泛认可的命名约定

Java 平台有一组完善的命名约定,其中许多约定包含在《The Java Language Specification》。不严格地讲,命名约定分为两类:排版和语法。

排版

和排版有关的命名约定,包括包、类、接口、方法、字段和类型变量

  1. 包名和模块名应该是分层的,组件之间用句点分隔。组件应该由小写字母组成,很少使用数字。任何在你的组织外部使用的包,名称都应该以你的组织的 Internet 域名开头,并将组件颠倒过来,例如,edu.cmu、com.google、org.eff。以 java 和 javax 开头的标准库和可选包是这个规则的例外。用户不能创建名称以 java 或 javax 开头的包或模块。
    包名的其余部分应该由描述包的一个或多个组件组成。组件应该很短,通常为 8 个或更少的字符。鼓励使用有意义的缩写,例如 util 而不是 utilities。缩写词是可以接受的,例如 awt。组件通常应该由一个单词或缩写组成。
  2. 类和接口名称,包括枚举和注释类型名称,应该由一个或多个单词组成,每个单词的首字母大写,例如 List 或 FutureTask
  3. 方法和字段名遵循与类和接口名相同的排版约定,除了方法或字段名的第一个字母应该是小写,例如 remove 或 ensureCapacity
  4. 前面规则的唯一例外是「常量字段」,它的名称应该由一个或多个大写单词组成,由下划线分隔,例如 VALUES 或 NEGATIVE_INFINITY
  5. 局部变量名与成员名具有类似的排版命名约定,但允许使用缩写,也允许使用单个字符和短字符序列,它们的含义取决于它们出现的上下文,例如 i、denom、houseNum。输入参数是一种特殊的局部变量。它们的命名应该比普通的局部变量谨慎得多,因为它们的名称是方法文档的组成部分。
  6. 类型参数名通常由单个字母组成。最常见的是以下五种类型之一:T 表示任意类型,E 表示集合的元素类型,K 和 V 表示 Map 的键和值类型,X 表示异常。函数的返回类型通常为 R。任意类型的序列可以是 T、U、V 或 T1、T2、T3。

为了快速参考,下表显示了排版约定的示例。

Identifier TypeExample
Package or moduleorg.junit.jupiter.api, com.google.common.collect
Class or InterfaceStream, FutureTask, LinkedHashMap,HttpClient
Method or Fieldremove, groupingBy, getCrc
Constant FieldMIN_VALUE, NEGATIVE_INFINITY
Local Variablei, denom, houseNum
Type ParameterT, E, K, V, X, R, U, V, T1, T2

语法

语法命名约定比排版约定更灵活,也更有争议

  1. 可实例化的类,包括枚举类型,通常使用一个或多个名词来命名,例如 Thread、PriorityQueue 或 ChessPiece
  2. 不可实例化的工具类通常用复数名词来命名,例如 Collectors 和 Collections
  3. 接口的名称类似于类,例如 Collection 或 Comparator,或者以 able 或 ible 结尾的形容词,例如 Runnable、Iterable 或 Accessible
  4. 因为注解类型有很多的用途,所以没有哪部分占主导地位。名词、动词、介词和形容词都很常见,例如,BindingAnnotation、Inject、ImplementedBy 或 Singleton。
  5. 执行某些操作的方法通常用动词或动词短语(包括对象)命名,例如,append 或 drawImage。
  6. 返回布尔值的方法的名称通常以单词 is 或 has(通常很少用)开头,后面跟一个名词、一个名词短语,或者任何用作形容词的单词或短语,例如 isDigit、isProbablePrime、isEmpty、isEnabled 或 hasSiblings。
  7. 返回被调用对象的非布尔函数或属性的方法通常使用以 get 开头的名词、名词短语或动词短语来命名,例如 size、hashCode 或 getTime。有一种说法是,只有第三种形式(以 get 开头)才是可接受的,但这种说法几乎没有根据。前两种形式的代码通常可读性更强 。以 get 开头的形式起源于基本过时的 Java bean 规范,该规范构成了早期可复用组件体系结构的基础。有一些现代工具仍然依赖于 bean 命名约定,你应该可以在任何与这些工具一起使用的代码中随意使用它。如果类同时包含相同属性的 setter 和 getter,则遵循这种命名约定也有很好的先例。在本例中,这两个方法通常被命名为 getAttribute 和 setAttribute。
  8. 转换对象类型(返回不同类型的独立对象)的实例方法通常称为 toType,例如 toString 或 toArray。
  9. 返回与接收对象类型不同的视图(Item-6)的方法通常称为 asType,例如 asList
  10. 返回与调用它们的对象具有相同值的基本类型的方法通常称为类型值,例如 intValue
  11. 静态工厂的常见名称包括 from、of、valueOf、instance、getInstance、newInstance、getType 和 newType
  12. 字段名的语法约定没有类、接口和方法名的语法约定建立得好,也不那么重要,因为设计良好的 API 包含很少的公开字段。类型为 boolean 的字段的名称通常类似于 boolean 访问器方法,省略了开头「is」,例如 initialized、composite。其他类型的字段通常用名词或名词短语来命名,如 height、digits 和 bodyStyle。局部变量的语法约定类似于字段的语法约定,但要求更少。
]]>
+ + + + + <h2 id="第九章-通用程序设计"><a class="markdownIt-Anchor" href="#第九章-通用程序设计"></a> 第九章 通用程序设计</h2> +<h3 id="57-将局部变量的作用域最小化"><a class="markdownIt-Ancho + + + + + + + + + + + +
+ + + Effective-Java学习笔记(七) + + https://zunpan.github.io/2022/08/01/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%83%EF%BC%89/ + 2022-08-01T11:27:09.000Z + 2023-09-24T04:27:40.273Z + + 第八章 方法

49. 检查参数的有效性

大多数方法和构造函数都对入参有一些限制,例如非空非负,你应该在方法主体的开头检查参数并在Javadoc中用@throws记录参数限制

Java7 添加了 Objects.requireNonNull 方法执行空检查

Java9 在 Objects 中添加了范围检查:checkFromIndexSize、checkFromToIndex 和 checkIndex

对于未导出的方法,作为包的作者,你应该定制方法调用的环境,确保只传递有效的参数值。因此,非 public 方法可以使用断言检查参数

// Private helper function for a recursive sortprivate static void sort(long a[], int offset, int length) {    assert a != null;    assert offset >= 0 && offset <= a.length;    assert length >= 0 && length <= a.length - offset;    ... // Do the computation}

断言只适合用来调试,可以关闭可以打开,public 方法一定要显示检查参数并抛出异常,断言失败只会抛出AssertionError,不利于定位错误

总而言之,每次编写方法或构造函数时,都应该考虑参数存在哪些限制。你应该在文档中记录这些限制,并在方法主体的开头显式地检查。

50. 在需要时制作防御性副本

Java 是一种安全的语言,这是它的一大优点。这意味着在没有 native 方法的情况下,它不受缓冲区溢出、数组溢出、非法指针和其他内存损坏错误的影响,这些错误困扰着 C 和 C++ 等不安全语言。在一种安全的语言中,可以编写一个类并确定它们的不变量将保持不变,而不管在系统的任何其他部分发生了什么。在将所有内存视为一个巨大数组的语言中,这是不可能的。

即使使用一种安全的语言,如果你不付出一些努力,也无法与其他类隔离。你必须进行防御性的设计,并假定你的类的客户端会尽最大努力破坏它的不变量。

通过可变参数破坏类的不变量

虽然如果没有对象的帮助,另一个类是不可能修改对象的内部状态的,但是要提供这样的帮助却出奇地容易。例如,考虑下面的类,它表示一个不可变的时间段:

// Broken "immutable" time period classpublic final class Period {    private final Date start;    private final Date end;    /**    * @param start the beginning of the period    * @param end the end of the period; must not precede start    * @throws IllegalArgumentException if start is after end    * @throws NullPointerException if start or end is null    */    public Period(Date start, Date end) {        if (start.compareTo(end) > 0)            throw new IllegalArgumentException(start + " after " + end);        this.start = start;        this.end = end;    }    public Date start() {        return start;    }    public Date end() {        return end;    }    ... // Remainder omitted}

乍一看,这个类似乎是不可变的,并且要求一个时间段的开始时间不能在结束时间之后。然而,利用 Date 是可变的这一事实很容易绕过这个约束:

// Attack the internals of a Period instanceDate start = new Date();Date end = new Date();Period p = new Period(start, end);end.setYear(78); // Modifies internals of p!

从 Java8 开始,解决这个问题可以用 Instant 或 LocalDateTime 或 ZonedDateTime 来代替 Date,因为它们是不可变类。

防御性副本

但是有时必须在 API 和内部表示中使用可变值类型,这时候可以对可变参数进行防御性副本而不是使用原始可变参数。对上面的 Period 类改进如下

// Repaired constructor - makes defensive copies of parameterspublic Period(Date start, Date end) {    this.start = new Date(start.getTime());    this.end = new Date(end.getTime());    if (this.start.compareTo(this.end) > 0)        throw new IllegalArgumentException(this.start + " after " + this.end);}
  1. 先进行防御性复制,再在副本上检查参数,保证在检查参数和复制参数之间的空窗期,类不受其他线程更改参数的影响,这个攻击也叫time-of-check/time-of-use 或 TOCTOU 攻击
  2. 对可被不受信任方子类化的参数类型,不要使用 clone 方法进行防御性复制。
  3. 访问器也要对可变字段进行防御性复制
  4. 如果类信任它的调用者不会破坏不变量,比如类和调用者都是同一个包下,那么应该避免防御性复制
  5. 当类的作用就是修改可变参数时不用防御性复制,客户端承诺不直接修改对象
  6. 破坏不变量只会对损害客户端时不用防御性复制,例如包装类模式,客户端在包装对象之后可以直接访问对象,破坏类的不变量,但这通常只会损害客户端

总而言之,如果一个类具有从客户端获取或返回给客户端的可变组件,则该类必须防御性地复制这些组件。如果复制的成本过高,并且类信任它的客户端不会不适当地修改组件,那么可以不进行防御性的复制,取而代之的是在文档中指明客户端的职责是不得修改受到影响的组件。

51. 仔细设计方法签名

  1. 仔细选择方法名称。目标是选择可理解的、与同一包中其它名称风格一致的名称;选择广泛认可的名字;避免长方法名
  2. 不要提供过于便利的方法。每种方法都应该各司其职。太多的方法使得类难以学习、使用、记录、测试和维护。对于接口来说更是如此,在接口中,太多的方法使实现者和用户的工作变得复杂。对于类或接口支持的每个操作,请提供一个功能齐全的方法。
  3. 避免长参数列表。可以通过分解方法减少参数数量;也可以通过静态成员类 helper 类来存参数;也可以从对象构建到方法调用都采用建造者模式
  4. 参数类型优先选择接口而不是类
  5. 双元素枚举类型优于 boolean 参数。枚举比 boolean 可读性强且可以添加更多选项

52. 明智地使用重载

考虑下面使用了重载的代码:

// Broken! - What does this program print?public class CollectionClassifier {    public static String classify(Set<?> s) {        return "Set";    }    public static String classify(List<?> lst) {        return "List";    }    public static String classify(Collection<?> c) {        return "Unknown Collection";    }    public static void main(String[] args) {        Collection<?>[] collections = {            new HashSet<String>(),new ArrayList<BigInteger>(),new HashMap<String, String>().values()        };        for (Collection<?> c : collections)            System.out.println(classify(c));    }}

这段代码打印了三次 Unknown Collection。因为 classify 方法被重载,并且在编译时就决定了要调用哪个重载,编译时是 Collections<?> 类型,所以调用的就是第三个重载方法

重载VS覆盖

重载方法的选择是静态的,在编译时决定要调用哪个重载方法。而覆盖方法的选择是动态的, 在运行时根据调用方法的对象的运行时类型选择覆盖方法的正确版本

因为覆盖是常态,而重载是例外,所以覆盖满足了人们对方法调用行为的期望,重载很容易混淆这些期望。

重载

  1. 安全、保守的策略是永远不导出具有相同数量参数的两个重载。你可以为方法提供不同的名称而不是重载它们。
  2. 构造函数只能重载,我们可以用静态工厂代替构造函数
  3. 不要在重载方法的相同参数位置上使用不同的函数式接口。不同的函数式接口并没有本质的不同

总而言之,方法可以重载,但并不意味着就应该这样做。通常,最好避免重载具有相同数量参数的多个签名的方法。在某些情况下,特别是涉及构造函数的情况下,可能难以遵循这个建议。在这些情况下,你至少应该避免同一组参数只需经过类型转换就可以被传递给不同的重载方法。如果这是无法避免的,例如,因为要对现有类进行改造以实现新接口,那么应该确保在传递相同的参数时,所有重载的行为都是相同的。如果你做不到这一点,程序员将很难有效地使用重载方法或构造函数,他们将无法理解为什么它不能工作。

53. 明智地使用可变参数

可变参数首先创建一个数组,其大小是在调用点上传递的参数数量,然后将参数值放入数组,最后将数组传递给方法。

当你需要定义具有不确定数量参数的方法时,可变参数是非常有用的。在可变参数之前加上任何必需的参数,并注意使用可变参数可能会引发的性能后果。

54. 返回空集合或数组,而不是 null

在方法中用空集合或空数组代替 null 返回可以让客户端不用显示判空

55. 明智地返回 Optional

在 Java8 之前,方法可能无法 return 时有两种处理方法,一种是抛异常,一种是 返回 null。抛异常代价高,返回 null 需要客户端显示判空

Java8 添加了第三种方法来处理可能无法返回值的方法。Optional<T> 类表示一个不可变的容器,它可以包含一个非空的 T 引用,也可以什么都不包含。不包含任何内容的 Optional 被称为空。一个值被认为存在于一个非空的 Optional 中。Optional 的本质上是一个不可变的集合,它最多可以容纳一个元素。

理论上应返回 T,但在某些情况下可能无法返回 T 的方法可以将返回值声明为 Optional<T>。这允许该方法返回一个空结果来表明它不能返回有效的结果。具备 Optional 返回值的方法比抛出异常的方法更灵活、更容易使用,并且比返回 null 的方法更不容易出错。

Item-30 有一个求集合最大值方法

// Returns maximum value in collection - throws exception if emptypublic static <E extends Comparable<E>> E max(Collection<E> c) {    if (c.isEmpty())        throw new IllegalArgumentException("Empty collection");    E result = null;    for (E e : c)        if (result == null || e.compareTo(result) > 0)            result = Objects.requireNonNull(e);    return result;}

当入参集合为空时,这个方法会抛出 IllegalArgumentException。更好的方式是返回 Optional

// Returns maximum value in collection as an Optional<E>public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {    if (c.isEmpty())        return Optional.empty();    E result = null;    for (E e : c)        if (result == null || e.compareTo(result) > 0)            result = Objects.requireNonNull(e);    return Optional.of(result);}

Optional.empty() 返回一个空的 Optional,Optional.of(value) 返回一个包含给定非空值的 Optional。将 null 传递给 Optional.of(value) 是一个编程错误。如果你这样做,该方法将通过抛出 NullPointerException 来响应。Optional.ofNullable(value) 方法接受一个可能为空的值,如果传入 null,则返回一个空的 Optional。永远不要让返回 optional 的方法返回 null : 它违背了这个功能的设计初衷。

为什么选择返回 Optional 而不是返回 null 或抛出异常?Optional 在本质上类似于受检查异常(Item-71),因为它们迫使 API 的用户面对可能没有返回值的事实。抛出不受检查的异常或返回 null 允许用户忽略这种可能性,抛出受检查异常会让客户端添加额外代码

如果一个方法返回一个 Optional,客户端可以选择如果该方法不能返回值该采取什么操作。你可以使用 orElse 方法指定一个默认值,或者使用 orElseGet 方法在必要时生成默认值;也可以使用 orElseThrow 方法抛出异常

关于 Optional 用法的一些Tips:

  1. isPresent 方法可以判断 Optional中有没有值,谨慎使用 isPrensent 方法,它的许多用途可以用上面的方法代替
  2. 并不是所有的返回类型都能从 Optional 处理中获益。容器类型,包括集合、Map、流、数组和 Optional,不应该封装在 Optional 中。 你应该简单的返回一个空的 List<T>,而不是一个空的 Optional<List<T>>
  3. 永远不应该返回包装类的 Optional,除了「次基本数据类型」,如 Boolean、Byte、Character、Short 和 Float 之外
  4. 在集合或数组中使用 Optional 作为键、值或元素几乎都是不合适的。

总之,如果你发现自己编写的方法不能总是返回确定值,并且你认为该方法的用户在每次调用时应该考虑这种可能性,那么你可能应该让方法返回一个 Optional。但是,你应该意识到,返回 Optional 会带来实际的性能后果;对于性能关键的方法,最好返回 null 或抛出异常。最后,除了作为返回值之外,你几乎不应该以任何其他方式使用 Optional。

56. 为所有公开的 API 元素编写文档注释

]]>
+ + + + + <h2 id="第八章-方法"><a class="markdownIt-Anchor" href="#第八章-方法"></a> 第八章 方法</h2> +<h3 id="49-检查参数的有效性"><a class="markdownIt-Anchor" href="#49-检查参 + + + + + + + + + + + +
+ + + Effective-Java学习笔记(六) + + https://zunpan.github.io/2022/07/30/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AD%EF%BC%89/ + 2022-07-30T06:30:09.000Z + 2023-09-24T04:27:40.276Z + + 第七章 λ 表达式和流

42. λ 表达式优于匿名类

函数对象

在历史上,带有单个抽象方法的接口(或者抽象类,但这种情况很少)被用作函数类型。它们的实例(称为函数对象)表示函数或操作。自从 JDK 1.1 在 1997 年发布以来,创建函数对象的主要方法就是匿名类(Item-24)。下面是一个按长度对字符串列表进行排序的代码片段,使用一个匿名类来创建排序的比较函数(它强制执行排序顺序):

// Anonymous class instance as a function object - obsolete!Collections.sort(words, new Comparator<String>() {    public int compare(String s1, String s2) {        return Integer.compare(s1.length(), s2.length());    }});

函数式接口

在 Java 8 中官方化了一个概念,即具有单个抽象方法的接口是特殊的,应该得到特殊处理。这些接口现在被称为函数式接口

lambda 表达式

可以使用 lambda 表达式为函数式接口创建实例,lambda 表达式在功能上类似于匿名类,但更简洁

// Lambda expression as function object (replaces anonymous class)Collections.sort(words,(s1, s2) -> Integer.compare(s1.length(), s2.length()));
  1. lambda 表达式的入参和返回值类型一般不写,由编译器做类型推断,类型能不写就不写。
    Item26告诉你不要用原始类型,Item29,30告诉你要用泛型和泛型方法,编译器从泛型中获得了大部分类型推断所需的类型信息
  2. lambda 表达式缺少名称和文档;如果一个算法较复杂,或者有很多行代码,不要把它放在 lambda 表达式中。 一行是理想的,三行是合理的最大值
  3. lambda 表达式仅限于函数式接口。如果想创建抽象类实例或者多个抽象方法的接口,只能用匿名类
  4. lambda 表达式编译后会成为外部类的一个私有方法,而匿名类会生成单独的class文件,所以 lambda 表达式的this是外部类实例,匿名类的this是匿名类实例
  5. lambda表达式和匿名类都不能可靠的序列化,如果想序列化函数对象,可以用私有静态嵌套类

43. 方法引用优于 λ 表达式

Java 提供了一种比 lambda 表达式更简洁的方法来生成函数对象:方法引用。下面这段代码的功能是,如果数字 1 不在映射中,则将其与键关联,如果键已经存在,则将关联值递增:

map.merge(key, 1, (count, incr) -> count + incr);

上面代码的 lambda 表达式作用是返回两个入参的和,在 Java 8 中,Integer(和所有其它基本类型的包装类)提供了一个静态方法 sum,它的作用完全相同,我们可以传入一个方法引用,并得到相同结果,同时减少视觉混乱:

map.merge(key, 1, Integer::sum);

函数对象的参数越多,方法引用就显得越简洁,但是 lambda 表达式指明了参数名,使得 lambda表达式比方法引用更容易阅读和维护,没有什么是方法引用能做而 lambda 表达式做不了的

许多方法引用引用静态方法,但是有四种方法不引用静态方法。其中两个是绑定和非绑定实例方法引用。在绑定引用中,接收对象在方法引用中指定。绑定引用在本质上与静态引用相似:函数对象接受与引用方法相同的参数。在未绑定引用中,在应用函数对象时通过方法声明参数之前的附加参数指定接收对象。在流管道中,未绑定引用通常用作映射和筛选函数(Item-45)。最后,对于类和数组,有两种构造函数引用。构造函数引用用作工厂对象。五种方法参考文献汇总如下表:

Method Ref TypeExampleLambda Equivalent
StaticInteger::parseIntstr ->
BoundInstant.now()::isAfterInstant then =Instant.now(); t ->then.isAfter(t)
UnboundString::toLowerCasestr ->str.toLowerCase()
Class ConstructorTreeMap<K,V>::new() -> new TreeMap<K,V>
Array Constructorint[]::newlen -> new int[len]

总之,方法引用通常为 lambda 表达式提供了一种更简洁的选择。如果方法引用更短、更清晰,则使用它们;如果没有,仍然使用 lambda 表达式。

44. 优先使用标准函数式接口

java.util.function 包提供了大量的标准函数接口。优先使用标准函数式接口而不是自己写。 通过减少 API ,使得你的 API 更容易学习,并将提供显著的互操作性优势,因为许多标准函数式接口提供了有用的默认方法

java.util.function 中有 43 个接口。不能期望你记住所有的接口,但是如果你记住了 6 个基本接口,那么你可以在需要时派生出其余的接口。基本接口操作对象引用类型。Operator 接口表示结果和参数类型相同的函数。Predicate 接口表示接受参数并返回布尔值的函数。Function 接口表示参数和返回类型不同的函数。Supplier 接口表示一个无参并返回(或「供应」)值的函数。最后,Consumer 表示一个函数,该函数接受一个参数,但不返回任何内容,本质上是使用它的参数。六个基本的函数式接口总结如下:

InterfaceFunction SignatureExample
UnaryOperator<T>T apply(T t)String::toLowerCase
BinaryOperator<T>T apply(T t1, T t2)BigInteger::add
Predicate<T>boolean test(T t)Collection::isEmpty
Function<T,R>R apply(T t)Arrays::asList
Supplier<T>T get()Instant::now
Consumer<T>void accept(T t)System.out::println

还有 6 个基本接口的 3 个变体,用于操作基本类型 int、long 和 double。它们的名称是通过在基本接口前面加上基本类型前缀而派生出来的。例如,一个接受 int 的 Predicate 就是一个 IntPredicate,一个接受两个 long 值并返回一个 long 的二元操作符就是一个 LongBinaryOperator。没有变体类型是参数化的除了由返回类型参数化的 Function 变体外。例如,LongFunction<int[]> 使用 long 并返回一个 int[]。

Function 接口还有 9 个额外的变体,在结果类型为基本数据类型时使用。源类型和结果类型总是不同的,因为入参只有一个且与出参类型相同的函数本身都是 UnaryOperator。如果源类型和结果类型都是基本数据类型,则使用带有 SrcToResult 的前缀函数,例如 LongToIntFunction(六个变体)。如果源是一个基本数据类型,而结果是一个对象引用,则使用带前缀 SrcToObj 的 Function 接口,例如 DoubleToObjFunction(三个变体)。

三个基本函数式接口有两个参数版本,使用它们是有意义的:BiPredicate<T,U>BiFunction<T,U,R>BiConsumer<T,U>。也有 BiFunction 变体返回三个相关的基本类型:ToIntBiFunction<T,U>ToLongBiFunction<T,U>ToDoubleBiFunction<T,U>。Consumer 有两个参数变体,它们接受一个对象引用和一个基本类型:ObjDoubleConsumer<T>ObjIntConsumer<T>ObjLongConsumer<T>。总共有9个基本接口的双参数版本。

最后是 BooleanSupplier 接口,它是 Supplier 的一个变体,返回布尔值。这是在任何标准函数接口名称中唯一显式提到布尔类型的地方,但是通过 Predicate 及其四种变体形式支持布尔返回值。前面描述的 BooleanSupplier 接口和 42 个接口占了全部 43 个标准函数式接口。

总之,既然 Java 已经有了 lambda 表达式,你必须在设计 API 时考虑 lambda 表达式。在输入时接受函数式接口类型,在输出时返回它们。一般情况下,最好使用 java.util.function 中提供的标准函数式接口,但请注意比较少见的一些情况,在这种情况下,你最好编写自己的函数式接口。

45. 明智地使用流

Java8添加了流API,用来简化序列或并行执行批量操作,API有两个关键的抽象:流(表示有限或无限的数据元素序列)和流管道(表示对这些元素的多阶段计算)。

流中的元素可以来自任何地方。常见的源包括集合、数组、文件、正则表达式的 Pattern 匹配器、伪随机数生成器和其他流。流中的数据元素可以是对象的引用或基本数据类型。支持三种基本数据类型:int、long 和 double。

流管道

流管道由源流跟着零个或多个中间操作和一个终止操作组成。每个中间操作以某种方式转换流,例如将每个元素映射到该元素的一个函数,或者过滤掉不满足某些条件的所有元素。中间操作都将一个流转换为另一个流,其元素类型可能与输入流相同,也可能与输入流不同。终止操作对最后一次中间操作所产生的流进行最终计算,例如将其元素存储到集合中、返回特定元素、或打印其所有元素。

  1. 流管道的计算是惰性的:直到调用终止操作时才开始计算,并且对完成终止操作不需要的数据元素永远不会计算。这种惰性的求值机制使得处理无限流成为可能。
  2. 流 API 是流畅的:它被设计成允许使用链式调用将组成管道的所有调用写到单个表达式中。
  3. 谨慎使用流,全部都是流操作的代码可读性不高
  4. 谨慎命名 lambda 表达式的参数名以提高可读性,复杂表达式使用 helper 方法

流 VS 迭代

迭代代码使用代码块表示重复计算,流管道使用函数对象(通常是 lambda 表达式或方法引用)表示重复计算。

迭代优势:

  1. 代码块可以读取修改局部变量,lambda表达式只能读取final变量或实际上final变量(初始化后不再修改,编译器会帮我们声明为final)。
  2. 代码块可以控制迭代,包括return,break,continue,throw,但是lambda不行

流适合用在以下操作:

  1. 元素序列的一致变换
  2. 过滤元素序列
  3. 组合元素序列(例如添加它们,连接它们或计算它们的最小值)
  4. 聚合元素序列到一个集合中,例如按属性分组
  5. 在元素序列中搜索满足某些条件的元素

总之,有些任务最好使用流来完成,有些任务最好使用迭代来完成。许多任务最好通过结合这两种方法来完成。

46. 在流中使用无副作用的函数

考虑以下代码,它用于构建文本文件中单词的频率表

// Uses the streams API but not the paradigm--Don't do this!Map<String, Long> freq = new HashMap<>();try (Stream<String> words = new Scanner(file).tokens()) {    words.forEach(word -> {        freq.merge(word.toLowerCase(), 1L, Long::sum);    });}

这段代码可以得出正确答案,但它不是流代码,而是伪装成流代码的迭代代码。它比迭代代码更长,更难阅读,更难维护,问题出在:forEach修改了外部状态。正确使用的流代码如下:

// Proper use of streams to initialize a frequency tableMap<String, Long> freq;try (Stream<String> words = new Scanner(file).tokens()) {    freq = words.collect(groupingBy(String::toLowerCase, counting()));}

forEach 操作应该只用于报告流计算的结果,而不是执行计算

正确的流代码使用了 Collectors 的生成收集器的方法,将元素收集到集合中的方法有三种:toList()toSet()toCollection(collectionFactory)。它们分别返回 List、Set 和程序员指定的集合类型

Collectors其它方法大部分是将流收集到Map中。最简单的Map收集器是toMap(keyMapper, valueMapper),它接受两个函数,一个将流元素映射到键,另一个将流元素映射到值,Item34用到了这个收集器

// Using a toMap collector to make a map from string to enumprivate static final Map<String, Operation> stringToEnum =Stream.of(values()).collect(toMap(Object::toString, e -> e));

这个收集器不能处理重复键的问题,我们可以加入第三个参数,即merge函数,处理重复键,它的参数类型是Map的值类型。我们还可以加入第四个参数用来指定特定的Map实现(如EnumMap或TreeMap)

除了toMap方法,groupingBy方法也将元素收集到Map中,键是类别,值是这个类别的所有元素的列表;groupingBy方法第二个参数是一个下游收集器,例如 counting() 作为下游收集器,最终的Map的键是类别,值是这个类别所有元素的数量;groupingBy也支持指定特定的Map实现

minBy和maxBy,它们接受一个Comparator并返回最小或最大元素;join方法将字符序列(如字符串)连接起来

总之,流管道编程的本质是无副作用的函数对象。这适用于传递给流和相关对象的所有函数对象。中间操作 forEach 只应用于报告由流执行的计算结果,而不应用于执行计算。为了正确使用流,你必须了解 collector。最重要的 collector 工厂是 toList、toSet、toMap、groupingBy 和 join。

47. 优先使用 Collection 而不是 Stream 作为返回类型

Stream 没有继承 Iterable,不能用 for-each 循环遍历。Collection 接口继承了 Iterable 而且提供了转换为流的方法,因此,Collection 或其适当的子类通常是公有返回序列的方法的最佳返回类型。

48. 谨慎使用并行流

如果流来自 Stream.iterate 或者中间操作 limit,并行化管道也不太可能提高其性能

通常,并行性带来的性能提升在 ArrayList、HashMap、HashSet 和 ConcurrentHashMap 实例上的流效果最好;int 数组和 long 数组也在其中。 这些数据结构的共同之处在于,它们都可以被精确且廉价地分割成任意大小的子程序,这使得在并行线程之间划分工作变得很容易。另一个共同点是,当按顺序处理时,它们提供了极好的引用位置:顺序元素引用一起存储在内存中,这些引用的引用对象在内存中可能彼此不太接近,这减少了引用的位置。引用位置对于并行化批量操作非常重要,如果没有它,线程将花费大量时间空闲,等待数据从内存传输到处理器的缓存中。具有最佳引用位置的数据结构是基本数组,因为数据本身是连续存储在内存中的。

如果在终止操作中完成大量工作,并且该操作本质上是顺序的,那么管道的并行化效果有限。并行化的最佳终止操作是 reduce 方法或者预先写好的reduce 方法,包括min、max、count、and。anyMatch、allMatch 和 noneMatch 的短路操作也适用于并行性。流的 collect 方法执行的操作称为可变缩减,它们不是并行化的好候选,因为组合集合的开销是昂贵的。

并行化流不仅会导致糟糕的性能,包括活动失败;它会导致不正确的结果和不可预知的行为(安全故障)。

总之,不要尝试并行化流管道,除非你有充分的理由相信它将保持计算的正确性以及提高速度。不适当地并行化流的代价可能是程序失败或性能灾难。

]]>
+ + + + + <h2 id="第七章-λ-表达式和流"><a class="markdownIt-Anchor" href="#第七章-λ-表达式和流"></a> 第七章 λ 表达式和流</h2> +<h3 id="42-λ-表达式优于匿名类"><a class="markdownIt-Anch + + + + + + + + + + + + + +
+ + + Effective-Java学习笔记(五) + + https://zunpan.github.io/2022/07/23/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%94%EF%BC%89/ + 2022-07-22T16:44:09.000Z + 2023-09-24T04:27:40.275Z + + 第六章 枚举和注解

34. 用枚举类型代替 int 常量

int枚举

int枚举类型中有一系列public static final int常量,每个常量表示一个类型。
它有许多缺点:

  1. 不提供类型安全性,容易误用(入参是Apple常量,但是传任何int都可以)
  2. 常量值修改,客户端也必须修改
  3. 调试麻烦

枚举类型

Java 的枚举类型是成熟的类,其他语言中的枚举类型本质上是 int 值。

枚举优势:

  1. 实例受控
    Java 枚举类型通过 public static final 修饰的字段为每个枚举常量导出一个实例。枚举类型实际上是 final 类型,因为构造函数是私有的。客户端既不能创建枚举类型的实例,也不能继承它,所以除了声明的枚举常量之外,不能有任何实例。换句话说,枚举类型是实例受控的类(Item-1)。它们是单例(Item-3)的推广应用,单例本质上是单元素的枚举。

  2. 提供编译时类型安全性。
    如果将参数声明为 Apple 枚举类型,则可以保证传递给该参数的任何非空对象引用都是 Apple 枚举值之一。尝试传递错误类型的值将导致编译时错误,将一个枚举类型的表达式赋值给另一个枚举类型的变量,或者使用 == 运算符比较不同枚举类型的值同样会导致错误。

  3. 名称相同的枚举类型常量能共存
    名称相同的枚举类型常量能和平共存,因为每种类型都有自己的名称空间。你可以在枚举类型中添加或重新排序常量,而无需重新编译其客户端,因为导出常量的字段在枚举类型及其客户端之间提供了一层隔离:常量值不会像在 int 枚举模式中那样编译到客户端中。最后,你可以通过调用枚举的 toString 方法将其转换为可打印的字符串。

  4. 允许添加任意方法和字段并实现任意接口
    枚举类型允许添加任意方法和字段并实现任意接口。它们提供了所有 Object 方法的高质量实现(参阅 Chapter 3),还实现了 Comparable(Item-14)和 Serializable(参阅 Chapter 12),并且它们的序列化形式被设计成能够适应枚举类型的可变性。如果方法是常量特有的,可以在枚举类型中添加抽象方法,在声明枚举实例时覆盖抽象方法

    // Enum type with constant-specific class bodies and datapublic enum Operation {    PLUS("+") {        public double apply(double x, double y) { return x + y; }    },    MINUS("-") {        public double apply(double x, double y) { return x - y; }    },    TIMES("*") {        public double apply(double x, double y) { return x * y; }    },    DIVIDE("/") {        public double apply(double x, double y) { return x / y; }    };    private final String symbol;    Operation(String symbol) { this.symbol = symbol; }    @Override    public String toString() { return symbol; }    // Implementing a fromString method on an enum type    private static final Map<String, Operation> stringToEnum =Stream.of(values()).collect(toMap(Object::toString, e -> e));    // Returns Operation for string, if any    public static Optional<Operation> fromString(String symbol) {        return Optional.ofNullable(stringToEnum.get(symbol));    }    public abstract double apply(double x, double y);}

    上述实现的缺点是如果常量特有的方法有可以复用的代码,那么会造成许多冗余。你可以把冗余代码抽出成方法,常量覆盖抽象方法时调用抽出的方法,这种实现同样有许多冗余代码;你也可以把抽象方法改成具体方法,里面是可以复用的代码,添加常量时如果不覆盖那就用默认的,这种实现问题在于添加常量时如果忘记覆盖那就用默认方法了。

    当常量特有的方法有可以复用的代码时,我们采用策略枚举模式,将可复用代码移到私有嵌套枚举中。当常量调用方法时,调用私有嵌套枚举常量的方法。

    在枚举上实现特定常量的行为时 switch 语句不是一个好的选择,只有在枚举不在你的控制之下,你希望它有一个实例方法来返回每个常量的特定行为,这时候才用 switch

总之,枚举类型相对于 int 常量的优势是毋庸置疑的。枚举更易于阅读、更安全、更强大。许多枚举不需要显式构造函数或成员,但有些枚举则受益于将数据与每个常量关联,并提供行为受数据影响的方法。将多个行为与一个方法关联起来,这样的枚举更少。在这种相对少见的情况下,相对于使用 switch 的枚举,特定常量方法更好。如果枚举常量有一些(但不是全部)共享公共行为,请考虑策略枚举模式。

35. 使用实例字段替代序数

每个枚举常量都有一个 ordinal 方法,返回枚举常量在枚举类中的位置。不要用这个方法返回与枚举常量关联的值,一旦常量位置变动,值就是错的,所以不要用序数生成与枚举常量关联的值,而是将其存在实例字段中

36. 用 EnumSet 替代位字段

位字段

如果枚举类型的元素主要在 Set 中使用,传统上使用 int 枚举模式(Item34),通过不同的 2 的幂次为每个常量赋值:

// Bit field enumeration constants - OBSOLETE!public class Text {    public static final int STYLE_BOLD = 1 << 0; // 1    public static final int STYLE_ITALIC = 1 << 1; // 2    public static final int STYLE_UNDERLINE = 1 << 2; // 4    public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8    // Parameter is bitwise OR of zero or more STYLE_ constants    public void applyStyles(int styles) { ... }}

使用位运算的 OR 操作将几个常量组合成一个集合,这个集合叫做位字段:

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

位字段表示方式允许使用位运算高效地执行集合操作,如并集和交集。但是位字段具有 int 枚举常量所有缺点,甚至更多。当位字段被打印为数字时,它比简单的 int 枚举常量更难理解。没有一种简单的方法可以遍历由位字段表示的所有元素。最后,你必须预测在编写 API 时需要的最大位数,并相应地为位字段(通常是 int 或 long)选择一种类型。一旦选择了一种类型,在不更改 API 的情况下,不能超过它的宽度(32 或 64 位)。

EnumSet

一些使用枚举而不是 int 常量的程序员在需要传递常量集合时仍然坚持使用位字段。没有理由这样做,因为存在更好的选择。java.util 包提供 EnumSet 类来有效地表示从单个枚举类型中提取的值集。这个类实现了 Set 接口,提供了所有其他 Set 实现所具有的丰富性、类型安全性和互操作性。但在内部,每个 EnumSet 都表示为一个位向量。如果底层枚举类型有 64 个或更少的元素(大多数都是),则整个 EnumSet 用一个 long 表示,因此其性能与位字段的性能相当。批量操作(如 removeAll 和 retainAll)是使用逐位算法实现的,就像手动处理位字段一样。但是,你可以避免因手工修改导致产生不良代码和潜在错误:EnumSet 为你完成了这些繁重的工作。

当之前的示例修改为使用枚举和 EnumSet 而不是位字段时。它更短,更清晰,更安全:

// EnumSet - a modern replacement for bit fieldspublic class Text {    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }    // Any Set could be passed in, but EnumSet is clearly best    public void applyStyles(Set<Style> styles) { ... }}

下面是将 EnumSet 实例传递给 applyStyles 方法的客户端代码。EnumSet 类提供了一组丰富的静态工厂,可以方便地创建集合,下面的代码演示了其中的一个:

text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

总之,如果枚举类型将在 Set 中使用,不要用位字段表示它。 EnumSet 类结合了位字段的简洁性和性能,以及 (Item-34) 中描述的枚举类型的许多优点。EnumSet 的一个真正的缺点是,从 Java 9 开始,它不能创建不可变的 EnumSet,在未来发布的版本中可能会纠正这一点。同时,可以用 Collections.unmodifiableSet 包装 EnumSet,但简洁性和性能将受到影响

37. 使用 EnumMap 替换序数索引

序数索引

当需要将枚举常量映射到其它值时,有一种方法是序数索引,使用 ordinal 方法返回的序数表示key。这种方式存在Item35提到的问题

EnumMap

EnumMap使用枚举常量作为key,功能丰富、类型安全

38. 使用接口模拟可扩展枚举

枚举不可以扩展(继承)另一个枚举,但可以实现接口。可以用接口模拟可扩展的枚举。下面的例子将Item34的枚举类型的Operation改成了接口类型。通过实现接口来模拟继承枚举。

// Emulated extensible enum using an interfacepublic interface Operation {    double apply(double x, double y);}public enum BasicOperation implements Operation {    PLUS("+") {        public double apply(double x, double y) { return x + y; }    },    MINUS("-") {        public double apply(double x, double y) { return x - y; }    },    TIMES("*") {        public double apply(double x, double y) { return x * y; }    },    DIVIDE("/") {        public double apply(double x, double y) { return x / y; }    };    private final String symbol;    BasicOperation(String symbol) {        this.symbol = symbol;    }    @Override    public String toString() {        return symbol;    }}
// Emulated extension enumpublic enum ExtendedOperation implements Operation {    EXP("^") {        public double apply(double x, double y) {            return Math.pow(x, y);        }    },    REMAINDER("%") {        public double apply(double x, double y) {            return x % y;        }    };    private final String symbol;    ExtendedOperation(String symbol) {        this.symbol = symbol;    }    @Override    public String toString() {        return symbol;    }}

可以用接口类型引用指向不同子枚举类型实例。

public static void main(String[] args) {    Operation op = BasicOperation.DIVIDE;    System.out.println(op.apply(15, 3));    op=ExtendedOperation.EXP;    System.out.println(op.apply(2,5));}

java.nio.file.LinkOption 使用了这个Item描述的方法,它实现了 CopyOption 和 OpenOption 接口。

总之,枚举不可以扩展或被扩展,但是你可以通过接口模拟枚举之间的层级关系

39. 注解优于命名模式

命名模式

用来标明某些程序元素需要工具或框架特殊处理。
例如JUnit3及之前,用户必须写test开头的测试方法,不然测试都不能运行。
缺点:1.写错方法名会导致测试通过,但是实际上根本没有运行;2.不能将参数值和程序元素关联,例如测试方法只有在抛出特定异常时才成功,虽然可以精心设计命名模式,将异常名称写在测试方法名上,但是编译器无法检查异常类是否存在

注解

Junit4 开始使用注解,框架会根据注解执行测试方法,也可以将异常和测试方法绑定。注解本身不修改代码语义,而是通过反射(框架所用的技术)对其特殊处理

40. 坚持使用 @Override 注解

请在要覆盖父类声明的每个方法声明上使用 @Override 注解。只有一个例外,那就是具体类覆盖父类的抽象方法,因为具体类必须要实现父类的抽象方法,所以不必加 @Override 注解

41. 使用标记接口定义类型

标记接口

标记接口是一个空接口,它的作用是标记实现类具有某些属性。例如,实现 Serializable 接口表示类的实例可以写入 ObjectOutputStream(序列化)

标记接口VS标记注解

与标记注解(Item39)相比,标记接口有两个优点:

  1. 标记接口定义的类型由标记类的实例实现;标记注解不会。因此标记接口类型的存在允许你在编译时捕获错误,标记注解只能在运行时捕获。ObjectOutputStream.writeObject方法入参必须是实现了Serializable的实例,否则会报编译错误(JDK设计缺陷,writeObject入参是Object类型)
  2. 标记接口相对于标记注解的另一个优点是可以更精确地定位子类型。例如 Set 是一个标记接口,它继承 Collection接口,Set标记了所有它的实现类都具有Collection功能。Set相较于标记接口的特殊之处在于它还细化了Collection方法 add、equals 和 hashCode 的约定

标记注解的优势:与基于使用注解的框架保持一致性

使用规则

如果标记用在类或接口之外的任何程序元素,必须用标记注解;如果框架使用注解,那就用标记注解;其它情况都用标记接口

]]>
+ + + + + <h2 id="第六章-枚举和注解"><a class="markdownIt-Anchor" href="#第六章-枚举和注解"></a> 第六章 枚举和注解</h2> +<h3 id="34-用枚举类型代替-int-常量"><a class="markdownIt-Anchor + + + + + + + + + + + + + +
+ + + Effective-Java学习笔记(四) + + https://zunpan.github.io/2022/07/14/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%9B%9B%EF%BC%89/ + 2022-07-14T12:17:09.000Z + 2023-09-24T04:27:40.276Z + + 第五章 泛型

26. 不要使用原始类型

泛型和原始类型

声明中具有一个或多个类型参数的类或接口就是泛型类或泛型接口。泛型类和泛型接口统称为泛型。

每个泛型都定义了一个原始类型,它是没有任何相关类型参数的泛型的名称。例如,List<E> 对应的原始类型是 List。原始类型的行为就好像所有泛型信息从类型声明中删除了一样。它们的存在主要是为了与之前的代码兼容。

泛型优势

泛型可以帮助编译器在编译过程中发现潜在的类型转换异常,而原始类型不行。

泛型的子类型规则

原始类型 List 和 参数化类型 List<Object> 都可以保存任何类型的对象 ,但是不能把其它泛型对象 , 例如List<String>对象赋给List<Object>引用而可以赋给 List 引用。泛型有子类型规则,List<String> 是原始类型 List 的子类型,而不是参数化类型 List<Object> 的子类型(Item-28)。假如List<String>可以是List<Object>的子类型,那么List<String>的对象赋给List<Object>,通过List<Object>插入非String元素,违反了List<String>只放String的约定

无界通配符

如果你想使用泛型,但不知道或不关心实际的类型参数是什么,那么可以使用无界通配符 ? 代替。例如,泛型集合 Set<E> 的无界通配符类型是 Set<?>。它是最通用的参数化集合类型,能够容纳任何集合

无界通配符类型 Set<?> 和原始类型 Set 之间的区别在于通配符类型是安全的,而原始类型不是。将任何元素放入具有原始类型的集合中,很容易破坏集合的类型一致性;而无界通配符类型不能放入元素(除了null)

Set<Integer> integerSet = new HashSet<>();// 无界通配符类型Set<?>可以引用任何Set<E>和Set,但是不能往里面放除了null的元素Set<?> set1 = integerSet;// 原始类型Set也可以引用任何Set<E>和Set,但是可以往里面添加元素,会存在类型转换异常Set set2 = integerSet;

使用泛型而不用原始类型的例外

  1. 类字面量。该规范不允许使用参数化类型(尽管它允许数组类型和基本类型)。换句话说,List.classString[].classint.class 都是合法的,但是 List<String>.classList<?>.class 不是。
  2. instanceof 运算符。由于泛型信息在运行时被删除,因此在不是无界通配符类型之外的参数化类型使用 instanceof 操作符是非法的。使用无界通配符类型代替原始类型不会以任何方式影响 instanceof 运算符的行为。在这种情况下,尖括号和问号只是多余的。下面的例子是使用通用类型 instanceof 运算符的首选方法:
// Legitimate use of raw type - instanceof operatorif (o instanceof Set) { // Raw type    Set<?> s = (Set<?>) o; // Wildcard type    ...}

总之,使用原始类型可能会在运行时导致异常,所以不要轻易使用它们。它们仅用于与引入泛型之前的遗留代码进行兼容。快速回顾一下,Set<Object> 是一个参数化类型,表示可以包含任何类型的对象的集合,Set<?> 是一个无界通配符类型,表示只能包含某种未知类型的对象的集合,Set 是一个原始类型,它没有使用泛型。前两个是安全的,后一个不安全

为便于参考,本条目中介绍的术语(以及后面将要介绍的一些术语)总结如下:

TermExampleItem
Parameterized typeList<String>Item-26
Actual type parameterStringItem-26
Generic typeList<E>Item-26, Item-29
Formal type parameterEItem-26
Unbounded wildcard typeList<?>Item-26
Raw typeListItem-26
Bounded type parameter<E extends Number>Item-29
Recursive type bound<T extends Comparable<T>>Item-30
Bounded wildcard typeList<? extends Number>Item-31
Generic methodstatic <E> List<E> asList(E[] a)Item-30
Type tokenString.classItem-33

27. 消除 unchecked 警告

消除所有 unchecked 警告可以确保代码是类型安全的,运行时不会出现 ClassCastException。如果不能消除警告,但是可以证明引发警告的代码是类型安全的,那么可以使用 SuppressWarnings(“unchecked”) 注解来抑制警告。

SuppressWarnings注解

SuppressWarnings 注解可以用于任何声明中,从单个局部变量声明到整个类。请总是在尽可能小的范围上使用 SuppressWarnings 注解。通常用在一个变量声明或一个非常短的方法或构造函数。不要在整个类中使用 SuppressWarnings。这样做可能会掩盖关键警告。

如果你发现自己在一个超过一行的方法或构造函数上使用 SuppressWarnings 注解,那么你可以将其移动到局部变量声明中

将 SuppressWarnings 注释放在 return 语句上是非法的,因为它不是声明。你可能想把注释放在整个方法上,但是不要这样做。相反,应该声明一个局部变量来保存返回值并添加注解

每次使用 SuppressWarnings(“unchecked”) 注解时,要添加一条注释,说明这样做是安全的。这将帮助他人理解代码,更重要的是,它将降低其他人修改代码而产生不安全事件的几率。如果你觉得写这样的注释很难,那就继续思考合适的方式,你最终可能会发现,unchecked 操作毕竟是不安全的。

总之,unchecked 警告很重要。不要忽视他们。每个 unchecked 警告都代表了在运行时发生 ClassCastException 的可能性。尽最大努力消除这些警告。如果不能消除 unchecked 警告,但是可以证明引发该警告的代码是类型安全的,那么可以在尽可能狭窄的范围内使用 @SuppressWarnings(“unchecked”) 注释来抑制警告。在注释中记录你决定抑制警告的理由。

28. list 优于数组

数组与泛型的区别

  1. 数组是协变的, 如果 Sub 是 Super 的一个子类型,那么数组类型 Sub[] 就是数组类型 Super[] 的一个子类型。相比之下,泛型是不变的:对于任何两个不同类型 Type1 和 Type2,List<Type1> 既不是 List<Type2> 的子类型,也不是 List<Type2> 的父类型。

  2. 数组是具体化的。这意味着数组在运行时知道并强制执行他们的元素类型。如前所述,如果试图将 String 元素放入一个 Long 类型的数组中,就会得到 ArrayStoreException。相比之下,泛型是通过擦除来实现的,这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)元素类型信息。擦除允许泛型与不使用泛型的遗留代码自由交互操作(Item-26),确保在 Java 5 中平稳过渡

泛型数组的创建是非法的

由于这些基本差异,数组和泛型不能很好地混合。例如,创建泛型、参数化类型或类型参数的数组是非法的。因此,这些数组创建表达式都不是合法的:new List<E>[]、new List<String>[]、new E[]。所有这些都会在编译时导致泛型数组创建错误。

考虑以下代码片段:

// Why generic array creation is illegal - won't compile!List<String>[] stringLists = new List<String>[1]; // (1)List<Integer> intList = List.of(42); // (2)Object[] objects = stringLists; // (3)objects[0] = intList; // (4)String s = stringLists[0].get(0); // (5)

假设创建泛型数组的第 1 行是合法的。第 2 行创建并初始化一个包含单个元素的 List<Integer>。第 3 行将 List<String> 数组存储到 Object 类型的数组变量中,这是合法的,因为数组是协变的。第 4 行将 List<Integer> 存储到 Object 类型的数组的唯一元素中,这是成功的,因为泛型是由擦除实现的:List<Integer> 实例的运行时类型是 List,List<String>[] 实例的运行时类型是 List[],因此这个赋值不会生成 ArrayStoreException。现在我们有麻烦了。我们将一个 List<Integer> 实例存储到一个数组中,该数组声明只保存 List<String> 实例。在第 5 行,我们从这个数组的唯一列表中检索唯一元素。编译器自动将检索到的元素转换为 String 类型,但它是一个 Integer 类型的元素,因此我们在运行时得到一个 ClassCastException。为了防止这种情况发生,第 1 行(创建泛型数组)必须生成编译时错误。

用List替代数组

当你在转换为数组类型时遇到泛型数组创建错误或 unchecked 强制转换警告时,通常最好的解决方案是使用集合类型 List<E>,而不是数组类型 E[]。你可能会牺牲一些简洁性和性能,但作为交换,你可以获得更好地类型安全性和互操作性。

总之,数组和泛型有非常不同的类型规则。数组是协变的、具体化的;泛型是不可变的和可被擦除的。因此,数组提供了运行时类型安全性,而不是编译时类型安全性,对于泛型来说相反。一般来说,数组和泛型不能很好的混合,如果你发现将它们混合在一起并得到编译时错误或警告,那么你的第一个反应应该是将数组替换为 list。

29. 优先使用泛型

编写泛型

在将原始类型修改成泛型时, 可能会遇到不能创建泛型数组的问题,有两种解决方法

  1. 创建 Object 数组并将其强制转换为 E[] 类型(字段的类型是 E[] ,将Object数组强转成 E[] 可以成功,但是方法返回 E[]给客户端,由编译器添加隐式强转就会失败,因为隐式强转是将Object数组转成声明的具体类型数组)。现在,编译器将发出一个警告来代替错误。这种用法是合法的,但(一般而言)不是类型安全的。编译器可能无法证明你的程序是类型安全的,但你可以。你必须说服自己,unchecked 的转换不会损害程序的类型安全性。所涉及的数组(元素)存储在私有字段中,从未返回给客户端或传递给任何其它方法(传到外面就会发生隐式强转)。添加元素时,元素也是 E 类型,因此 unchecked 的转换不会造成任何损害。一旦你证明了 unchecked 的转换是安全的,就将警告限制在尽可能小的范围内(Item-27)。
  2. 将字段的类型从 E[] 更改为 Object[]。编译器会产生类似的错误和警告,处理方法也和上面类似

方法1优势:可读性更好,因为数组声明为 E[] 类型,这清楚地表明它只包含 E 的实例。它也更简洁,只需要在创建数组的地方做一次强转,其它地方读取数组元素不用强转成 E 类型
方法1劣势:会造成堆污染(Item-32):数组的运行时类型与其编译时类型不匹配(除非 E 恰好是 Object)

为啥要创建泛型数组

Item-28 鼓励优先使用列表而不是数组。但是在泛型中使用列表并不总是可能或可取的。Java 本身不支持列表,因此一些泛型(如ArrayList)必须在数组之上实现。其它泛型(如HashMap)用数组实现来提高性能

总之,泛型比需要在客户端代码中转换的类型更安全、更容易使用。在设计新类型时,请确保客户端可以在不使用类型转换的情况下使用它们。这通常意味着使类型具有通用性。如果你有任何应该是泛型但不是泛型的现有类型,请对它们进行泛化。这将使这些类型的新用户在不破坏现有客户端的情况下更容易使用。

30. 优先使用泛型方法

类可以是泛型的,方法也可以是泛型的

编写泛型方法

编写泛型方法类似于编写泛型。类型参数列表声明类型参数,它位于方法的修饰符与其返回类型之间。例如,类型参数列表为 <E>,返回类型为 Set<E>

// Generic methodpublic static <E> Set<E> union(Set<E> s1, Set<E> s2) {    Set<E> result = new HashSet<>(s1);    result.addAll(s2);    return result;}

至少对于简单的泛型方法,这就是(要注意细节的)全部。该方法编译时不生成任何警告,并且提供了类型安全性和易用性。这里有一个简单的程序来演示。这个程序不包含转换,编译时没有错误或警告:

// Simple program to exercise generic methodpublic static void main(String[] args) {    Set<String> guys = Set.of("Tom", "Dick", "Harry");    Set<String> stooges = Set.of("Larry", "Moe", "Curly");    Set<String> aflCio = union(guys, stooges);    System.out.println(aflCio);}

当你运行程序时,它会打印出 [Moe, Tom, Harry, Larry, Curly, Dick]。(输出元素的顺序可能不同)。

union 方法的一个限制是,所有三个集合(输入参数和返回值)的类型必须完全相同。你可以通过使用有界通配符类型(Item-31)使方法更加灵活。

31. 使用有界通配符增加 API 的灵活性

PECS

为了获得满足里氏代换原则,应对表示生产者或消费者入参使用通配符类型。如果输入参数既是生产者优势消费者,使用精确的类型

PECS助记符:PECS 表示生产者应使用 extends,消费者应使用 super。换句话说,如果参数化类型表示 T 生产者,则使用 <? extends T>;如果它表示一个 T 消费者,则使用 <? super T>不要使用有界通配符类型作为返回类型。 它将强制用户在客户端代码中使用通配符类型,而不是为用户提供额外的灵活性

一个例子

接下来让我们将注意力转移到 Item-30 中的 max 方法,以下是原始声明:

public static <T extends Comparable<T>> T max(List<T> list)

下面是使用通配符类型的修正声明:

public static <T extends Comparable<? super T>> T max(List<? extends T> list)

这里使用了两次 PECS。第一次是参数列表,list作为生产者生成 T 的实例,所以我们将类型从 List<T> 更改为 List<? extends T>(个人认为这里没有必要改,调用一次max方法只能传入一个类型的List,如果是两个参数,第一个参数的类型决定了T,第二个参数的类型如果是List<? extends T>,那么类型参数必须是T或T的子类。但是如果是泛型类声明的T,那这个改动是有意义的)。第二次是是类型参数 T。这是我们第一次看到通配符应用于类型参数。最初,T 被指定为继承 Comparable<T>,但是 Comparable<T> 消费 T 实例。因此,将参数化类型 Comparable<T> 替换为有界通配符类型 Comparable<? super T>,Comparable 始终是消费者,所以一般应优先使用 Comparable<? super T> 而不是 Comparable<T>,比较器也是如此;因此,通常应该优先使用 Comparator<? super T> 而不是 Comparator<T>

修订后的 max 声明可能是本书中最复杂的方法声明。增加的复杂性真的能给你带来什么好处吗?是的。下面是一个简单的列表案例,它在原来的声明中不允许使用,但经修改的声明允许:

List<ScheduledFuture<?>> scheduledFutures = ... ;

不能将原始方法声明应用于此列表的原因是 ScheduledFuture 没有实现 Comparable<ScheduledFuture>。相反,它是 Delayed 的一个子接口,继承了 Comparable<Delayed>。换句话说,ScheduledFuture 的实例不仅仅可以与其它 ScheduledFuture 实例进行比较,还可以与任何 Delayed 实例比较,但是原始方法只能和 ScheduledFuture 实例比较。更通俗来说,通配符用于支持不直接实现 Comparable(或 Comparator)但继承了实现 Comparable(或 Comparator)的类型的类型。

类型参数和通配符的对偶性

类型参数和通配符之间存在对偶性,对偶性指实现方式不同但效果一样。例如,下面是swap方法的两种可能声明,第一个使用无界类型参数(Item-30),第二个使用无界通配符:

// Two possible declarations for the swap methodpublic static <E> void swap(List<E> list, int i, int j);public static void swap(List<?> list, int i, int j);

这两个声明哪个更好?在公共API中第二个更好,因为它更简单(客户端可以传入原始类型)。传入一个任意列表,该方法交换索引元素,不需要担心类型参数。通常,如果类型参数在方法声明中只出现一次,则用通配符替换它。如果它是一个无界类型参数,用一个无界通配符替换它;如果它是有界类型参数,则用有界通配符替换它。

交换的第二个声明有一个问题。下面的实现无法编译, list 的类型是 List<?>,你不能在 List<?> 中放入除 null 以外的任何值。

public static void swap(List<?> list, int i, int j) {  list.set(i, list.set(j, list.get(i)));}

幸运的是,有一种方法可以实现,而无需求助于不安全的强制类型转换或原始类型。其思想是编写一个私有助手方法来捕获通配符类型。为了捕获类型,helper 方法必须是泛型方法。它看起来是这样的:

public static void swap(List<?> list, int i, int j) {  swapHelper(list, i, j);}// Private helper method for wildcard captureprivate static <E> void swapHelper(List<E> list, int i, int j) {  list.set(i, list.set(j, list.get(i)));}

swapHelper 方法指导 list 是一个 List<E>。因此,它指导它从这个列表中得到的任何值都是 E 类型的,并且将 E 类型的任何值放入这个列表中都是安全的。这个稍微复杂的实现可以正确编译。它允许我们导出基于通配符的声明,同时在内部利用更复杂的泛型方法。swap 方法的客户端不必面对更复杂的 swapHelper 声明,但它们确实从中受益。值得注意的是,helper 方法具有我们认为对于公共方法过于复杂而忽略的签名。

总之,在 API 中使用通配符类型虽然很棘手,但可以使其更加灵活。如果你编写的库将被广泛使用,则必须考虑通配符类型的正确使用。记住基本规则:生产者使用 extends,消费者使用 super(PECS)。还要记住,所有的 comparable 和 comparator 都是消费者

32. 明智地合用泛型和可变参数

抽象泄露

可变参数方法(Item-53)和泛型都是在 Java 5 中添加,因此你可能认为它们能够优雅地交互;可悲的是,它们并不能。可变参数的目的是允许客户端向方法传递可变数量的参数,但这是一个抽象泄漏:当你调用可变参数方法时,将创建一个数组来保存参数;该数组本应是实现细节,却是可见的。因此,当可变参数具有泛型或参数化类型时,会出现令人困惑的编译器警告。

不可具体化

回想一下 Item-28,不可具体化类型是指其运行时表示的信息少于其编译时表示的信息,并且几乎所有泛型和参数化类型都是不可具体化的。如果方法声明其可变参数为不可具体化类型,编译器将在声明生生成警告。如果方法是在其推断类型不可具体化的可变参数上调用的,编译器也会在调用时生成警告。生成的警告就像这样:

warning: [unchecked] Possible heap pollution from parameterized vararg type List<String>

堆污染

当参数化类型的变量引用不属于该类型的对象时,就会发生堆污染。它会导致编译器自动生成的强制类型转换失败,违反泛型系统的基本保证。

例如,考虑这个方法,它来自 Item-26,但做了些修改:

// Mixing generics and varargs can violate type safety!// 泛型和可变参数混合使用可能违反类型安全原则!static void dangerous(List<String>... stringLists) {    List<Integer> intList = List.of(42);    Object[] objects = stringLists;    objects[0] = intList; // Heap pollution    String s = stringLists[0].get(0); // ClassCastException}

方法声明使用泛型可变参数是合法的

这个例子提出了一个有趣的问题:为什么方法声明中使用泛型可变参数是合法的,而显式创建泛型数组是非法的?答案是,带有泛型或参数化类型的可变参数的方法在实际开发中非常有用,因此语言设计人员选择忍受这种不一致性。事实上,Java 库导出了几个这样的方法,包括 Arrays.asList(T... a)Collections.addAll(Collection<? super T> c, T... elements) 以及 EnumSet.of(E first, E... rest)。它们与前面显示的危险方法不同,这些库方法是类型安全的。

在 Java 7 之前,使用泛型可变参数的方法的作者对调用点上产生的警告无能为力。使得这些 API 难以使用。用户必须忍受这些警告,或者在每个调用点(Item-27)使用 @SuppressWarnings(“unchecked”) 注释消除这些警告。这种做法乏善可陈,既损害了可读性,也忽略了标记实际问题的警告。

在 Java 7 中添加了 SafeVarargs 注释,以允许使用泛型可变参数的方法的作者自动抑制客户端警告。本质上,SafeVarargs 注释构成了方法作者的一个承诺,即该方法是类型安全的。 作为这个承诺的交换条件,编译器同意不对调用可能不安全的方法的用户发出警告。

使用SafeVarargs的条件

  1. 方法没有修改数组元素
  2. 数组的引用没有逃逸(这会使不受信任的代码能够访问数组)

换句话说,如果可变参数数组仅用于将可变数量的参数从调用方传输到方法(毕竟这是可变参数的目的),那么该方法是安全的。

一个反例
// UNSAFE - Exposes a reference to its generic parameter array!static <T> T[] toArray(T... args) {  return args;}

这个方法直接返回了泛型可变参数数组引用,违反了上面的条件2,它可以将堆污染传播到调用堆栈上。

考虑下面的泛型方法,该方法接受三个类型为 T 的参数,并返回一个包含随机选择的两个参数的数组:

public static void main(String[] args) {  String[] attributes = pickTwo("Good", "Fast", "Cheap");}static <T> T[] pickTwo(T a, T b, T c) {  switch(ThreadLocalRandom.current().nextInt(3)) {    case 0: return toArray(a, b);    case 1: return toArray(a, c);    case 2: return toArray(b, c);  }  throw new AssertionError(); // Can't get here}

这段代码编译时不会生成任何警告,运行时会抛出 ClassCastException,尽管它不包含可见的强制类型转换。你没有看到的是,编译器在 pickTwo 返回的值上生成了一个隐藏的 String[] 转换。转换失败,因为 Object[] 实际指向的数组是Object类型的不是String,强转失败。

这个示例的目的是让人明白,让另一个方法访问泛型可变参数数组是不安全的,只有两个例外:将数组传递给另一个使用 @SafeVarargs 正确注释的可变参数方法是安全的,将数组传递给仅计算数组内容的某个函数的非可变方法也是安全的。

扩展

如果 main 方法直接调用 toArray 方法,不会出现 ClassCastException,为什么?泛型不是被擦除了吗,args在运行时不是一个Object[] 吗?非也,运行时args的类型是Object[],但是指向的数组是 String 类型的, 所以编译器添加的强转是可以成功的。下面的例子里,toArray1方法在字节码层面和toArray3方法是一样的,在调用toArray1方法前会先生成 String[],将引用传递给toArray1,

public static void main(String[] args) {    String [] arr1 = toArray1("Alice", "Bob", "Cat");    // ClassCastException    String [] arr2 = toArray2("Alice", "Bob", "Cat");    String [] arr3 = toArray3(new String[]{"Alice", "Bob", "Cat"});    // ClassCastException,多层调用泛型可变参数数组会导致编译器没有足够信息创建真实类型的对象数组(存疑,从字节码来看也是创建了String[])    String[] arr4 = toArray1Crust1("Alice", "Bob", "Cat");    String[] arr5 = toArray1Crust2(new String[]{"Alice", "Bob", "Cat"});    String[] arr6 = toArray1Crust3(new String[]{"Alice", "Bob", "Cat"});    // 泛型方法嵌套没问题    String s = toString1("Alice");}static <T> T[] toArray1(T... args) {    return args;}static <T> T[] toArray2(T a, T b, T c) {    System.out.println(a.getClass());    // 泛型数组实际类型是Object    T[] result = (T[]) new Object[3];    result[0] = a;    result[1] = b;    result[2] = c;    return result;}static <T> T[] toArray3(T[] arr){    return arr;}static <T> T[] toArray1Crust1(T...args){    return toArray1(args);}static <T> T[] toArray1Crust2(T[] args){    return toArray1(args);}static String[] toArray1Crust3(String[] args){    return toArray1(args);}private static <T> T toString1(T s) {    return toString2(s);}public static <T> T toString2(T s){    return s;}

图 1

一个正确例子

下面是一个安全使用泛型可变参数的典型示例。该方法接受任意数量的列表作为参数,并返回一个包含所有输入列表的元素的序列列表。因为该方法是用 @SafeVarargs 注释的,所以它不会在声明或调用点上生成任何警告:

// Safe method with a generic varargs parameter@SafeVarargsstatic <T> List<T> flatten(List<? extends T>... lists) {  List<T> result = new ArrayList<>();  for (List<? extends T> list : lists)    result.addAll(list);  return result;}

何时使用SafeVarargs

决定何时使用 SafeVarargs 注释的规则很简单:在每个带有泛型或参数化类型的可变参数的方法上使用 @SafeVarargs,这样它的用户就不会被不必要的和令人困惑的编译器警告所困扰。

请注意,SafeVarargs 注释只能出现在不能覆盖的方法上,因为不可能保证所有可能覆盖的方法都是安全的。在 Java 8 中,注释只能出现在静态方法和final实例方法;在 Java 9 中,它在私有实例方法上也是合法的。

使用 SafeVarargs 注释的另一种选择是接受 Item-28 的建议,并用 List 参数替换可变参数(它是一个伪装的数组)。下面是将这种方法应用到我们的 flatten 方法时的效果。注意,只有参数声明发生了更改:

// List as a typesafe alternative to a generic varargs parameterstatic <T> List<T> flatten(List<List<? extends T>> lists) {  List<T> result = new ArrayList<>();  for (List<? extends T> list : lists)    result.addAll(list);  return result;}

然后可以将此方法与静态工厂方法 List.of 一起使用,以允许可变数量的参数。注意,这种方法依赖于 List.of 声明是用 @SafeVarargs 注释的:

audience = flatten(List.of(friends, romans, countrymen));

这种方法的优点是编译器可以证明该方法是类型安全的。你不必使用 SafeVarargs 注释来保证它的安全性,也不必担心在确定它的安全性时可能出错。主要的缺点是客户端代码比较冗长,可能会比较慢。

这种技巧也可用于无法编写安全的可变参数方法的情况,如第 147 页中的 toArray 方法。它的列表类似于 List.of 方法,我们甚至不用写;Java 库的作者为我们做了这些工作。pickTwo 方法变成这样:

static <T> List<T> pickTwo(T a, T b, T c) {  switch(rnd.nextInt(3)) {    case 0: return List.of(a, b);    case 1: return List.of(a, c);    case 2: return List.of(b, c);  }  throw new AssertionError();}

main 方法是这样的:

public static void main(String[] args) {  List<String> attributes = pickTwo("Good", "Fast", "Cheap");}

生成的代码是类型安全的,因为它只使用泛型,而不使用数组(List在运行时没有强转,数组会强转)。

总之,可变参数方法和泛型不能很好地交互,并且数组具有与泛型不同的类型规则。虽然泛型可变参数不是类型安全的,但它们是合法的。如果选择使用泛型(或参数化)可变参数编写方法,首先要确保该方法是类型安全的,然后使用 @SafeVarargs 对其进行注释。

33. 考虑类型安全的异构容器

参数化容器

如果你需要存储某种类型的集合,例如存储String的Set,那么Set<String>就足够了,又比如存储 String-Value 键值对,那么Map<String,Integer>就足够了

参数化容器的键

如果你要存储任意类型的集合。例如,一个数据库行可以有任意多列,我们希望用一个Map保存该行每列元素。那么我们可以使用参数化容器的键

例子

Favorites 类,允许客户端存储和检索任意多种类型对象。Class 类的对象将扮演参数化键的角色。Class 对象被称为类型标记

// Typesafe heterogeneous container pattern - clientpublic static void main(String[] args) {    Favorites f = new Favorites();    f.putFavorite(String.class, "Java");    f.putFavorite(Integer.class, 0xcafebabe);    f.putFavorite(Class.class, Favorites.class);    String favoriteString = f.getFavorite(String.class);    int favoriteInteger = f.getFavorite(Integer.class);    Class<?> favoriteClass = f.getFavorite(Class.class);    System.out.printf("%s %x %s%n", favoriteString,favoriteInteger, favoriteClass.getName());}

Favorites实例是类型安全的:当你向它请求一个 String 类型时,它永远不会返回一个 Integer 类型。
Favorites实例也是异构的:所有键都是不同类型的,普通 Map 的键是固定一个类型,因此,我们将 Favorites 称为一个类型安全异构容器

Favorites 的实现非常简短:

// Typesafe heterogeneous container pattern - implementationpublic class Favorites {  private Map<Class<?>, Object> favorites = new HashMap<>();  public <T> void putFavorite(Class<T> type, T instance) {    favorites.put(Objects.requireNonNull(type), instance);  }  public <T> T getFavorite(Class<T> type) {    return type.cast(favorites.get(type));  }}

通配符类型的键意味着每个键都可以有不同的参数化类型:一个可以是 Class<String>,下一个是 Class<Integer>,等等。这就是异构的原理。

favorites 的值类型仅仅是 Object。换句话说,Map 不保证键和值之间的类型关系

putFavorite 的实现很简单:它只是将从给定 Class 对象到给定对象的映射关系放入 favorites 中。如前所述,这将丢弃键和值之间的「类型关联」;将无法确定值是键的实例。但这没关系,因为 getFavorites 方法可以重新建立这个关联。

getFavorite 的实现比 putFavorite 的实现更复杂。首先,它从 favorites 中获取与给定 Class 对象对应的值。这是正确的对象引用返回,但它有错误的编译时类型:它是 Object(favorites 的值类型),我们需要返回一个 T。因此,getFavorite 的实现通过使用 Class 的 cast 方法,将对象引用类型动态转化为所代表的 Class 对象。

cast 方法是 Java 的 cast 运算符的动态模拟。它只是检查它的参数是否是类对象表示的类型的实例。如果是,则返回参数;否则它将抛出 ClassCastException。我们知道 getFavorite 中的强制转换调用不会抛出 ClassCastException。也就是说,我们知道 favorites 中的值总是与其键的类型匹配。

如果 cast 方法只是返回它的参数,那么它会为我们做什么呢?cast 方法的签名充分利用了 Class 类是泛型的这一事实。其返回类型为 Class 对象的类型参数:

public class Class<T> {    T cast(Object obj);}

这正是 getFavorite 方法所需要的。它使我们能够使 Favorites 类型安全,而不需要对 T 进行 unchecked 的转换。

Favorites 类有两个值得注意的限制。

  • 恶意客户端很容易通过使用原始形式的类对象破坏 Favorites 实例的类型安全。通过使用原始类型 HashSet(Item-26),可以轻松地将 String 类型放入 HashSet<Integer> 中。为了获得运行时的类型安全,让 putFavorite 方法检查实例是否是 type 表示的类型的实例,使用动态转换:

    // Achieving runtime type safety with a dynamic castpublic <T> void putFavorite(Class<T> type, T instance) {    favorites.put(type, type.cast(instance));}
  • Favorites 类不能用于不可具体化的类型(Item-28)。换句话说,你可以存储的 Favorites 实例类型为 String 或 String[],但不能存储 List<String>。原因是你不能为 List<String> 获取 Class 对象,List<String>.class 是一个语法错误

Favorites 使用的类型标记是无界的:getFavorite 和 put-Favorite 接受任何 Class 对象。有时你可能需要限制可以传递给方法的类型。这可以通过有界类型标记来实现,它只是一个类型标记,使用有界类型参数(Item-30)或有界通配符(Item-31)对可以表示的类型进行绑定。

annotation API(Item-39)广泛使用了有界类型标记。例如,下面是在运行时读取注释的方法。这个方法来自 AnnotatedElement 接口,它是由表示类、方法、字段和其他程序元素的反射类型实现的:

public <T extends Annotation>    T getAnnotation(Class<T> annotationType);

参数 annotationType 是表示注释类型的有界类型标记。该方法返回该类型的元素注释(如果有的话),或者返回 null(如果没有的话)。本质上,带注释的元素是一个类型安全的异构容器,其键是注释类型。

假设你有一个 Class<?> 类型的对象,并且希望将其传递给一个需要有界类型标记(例如 getAnnotation)的方法。你可以将对象强制转换为 Class<? extends Annotation>,但是这个强制转换是 unchecked 的,因此它将生成一个编译时警告(Item-27)。幸运的是,Class 类提供了一个实例方法,可以安全地(动态地)执行这种类型的强制转换。该方法叫做 asSubclass,它将 Class 对象强制转换为它所调用的类对象,以表示由其参数表示的类的子类。如果转换成功,则该方法返回其参数;如果失败,则抛出 ClassCastException。

下面是如何使用 asSubclass 方法读取在编译时类型未知的注释。这个方法编译没有错误或警告:

// Use of asSubclass to safely cast to a bounded type tokenstatic Annotation getAnnotation(AnnotatedElement element,String annotationTypeName) {    Class<?> annotationType = null; // Unbounded type token    try {        annotationType = Class.forName(annotationTypeName);    } catch (Exception ex) {        throw new IllegalArgumentException(ex);    }    return element.getAnnotation(annotationType.asSubclass(Annotation.class));}

总之,以集合的 API 为例的泛型在正常使用时将每个容器的类型参数限制为固定数量。你可以通过将类型参数放置在键上而不是容器上来绕过这个限制。你可以使用 Class 对象作为此类类型安全异构容器的键。以这种方式使用的 Class 对象称为类型标记。还可以使用自定义键类型。例如,可以使用 DatabaseRow 类型表示数据库行(容器),并使用泛型类型 Column<T> 作为它的键

]]>
+ + + + + <h2 id="第五章-泛型"><a class="markdownIt-Anchor" href="#第五章-泛型"></a> 第五章 泛型</h2> +<h3 id="26-不要使用原始类型"><a class="markdownIt-Anchor" href="#26-不要使 + + + + + + + + + + + +
+ + + Effective-Java学习笔记(三) + + https://zunpan.github.io/2022/07/07/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%89%EF%BC%89/ + 2022-07-07T14:31:09.000Z + 2023-09-24T04:27:40.274Z + + 第四章 类和接口

15. 尽量减少类和成员的可访问性

信息隐藏的作用

  1. 将API与实现完全分离。组件之间只能通过它们的API进行通信,而不知道彼此的内部工作方式。
  2. 解耦系统的组件,允许它们被独立开发、测试、优化、使用、理解和修改。
  3. 增加了软件的复用性,降低了构建大型系统的风险,即使系统没有成功,单个组件也可能被证明是成功的

访问控制机制

访问控制机制指定了类、接口和成员的可访问性。

  1. 对于顶级(非嵌套)类和接口,只有两个可能的访问级别:包私有和公共。如果用 public 修饰符声明,它将是公共的,即API的一部分,修改会损害客户端;否则,它将是包私有的。

  2. 如果包私有顶级类或接口只被一个类使用,那么可以考虑变成这个类的私有静态嵌套类(Item-24)

  3. 对于成员(字段、方法、嵌套类和嵌套接口),有四个访问级别,这里按可访问性依次递增的顺序列出:

    • 私有,成员只能从声明它的顶级类内部访问
    • 包私有,成员可以从声明它的类的包中访问,包私有就是默认访问,即如果没有指定访问修饰符(接口的成员除外,默认情况下,接口的成员是公共的),就是这个访问级别
    • 保护,成员可以从声明它的类的子类和声明它的类的包中访问
    • 公共,成员可以从任何地方访问

设计类和成员的可访问性

总体规则:让每个类或成员尽可能不可访问。换句话说,在不影响软件正常功能时,使用尽可能低的访问级别。

  1. 仔细设计类的公共API,所有成员声明为私有。私有成员和包私有成员都是类实现的一部分,通常不会影响其导出的API。但是,如果类实现了Serializable(Item-86和Item-87),这些字段可能会「泄漏」到导出的 API 中

  2. 少用保护成员。保护成员是类导出API的一部分,必须永远支持

  3. 子类覆盖父类的方法,可访问性不能缩小

  4. 为了测试,将公共类的成员由私有变为包私有是可以接受的,但是进一步提高可访问性是不可接受的

  5. 带有公共可变字段的类通常不是线程安全的

  6. 为了得到不可变数组,不要公开或通过访问器返回长度非零的数组的引用,客户端将能够修改数组的内容。可以将公共数组设置为私有,并添加一个公共不可变 List;或者,将数组设置为私有,并添加一个返回私有数组副本的公共方法:

总之,你应该尽可能减少程序元素的可访问性(在合理的范围内)。在仔细设计了一个最小的公共 API 之后,你应该防止任何游离的类、接口或成员成为 API 的一部分。除了作为常量的公共静态 final 字段外,公共类应该没有公共字段。确保public static final 字段引用的对象是不可变的。

16. 在公共类中,使用访问器方法,而不是公共字段

访问器方法的作用

如果类是公共的,即可以在包外访问,提供访问器方法可以维持类内部数据表示形式的灵活性

一些建议

  1. 如果类是包私有或者是私有嵌套类,公开数据字段比提供访问器方法更清晰
  2. 公共类公开不可变字段的危害会小一点,但仍不建议公开字段

17. 减少可变性

不可变类是实例创建后不能被修改的类。Java库包含许多不可变的类,包括 String、基本类型的包装类、BigInteger和BigDecimal。

创建不可变类的规则

要使类不可变,请遵守以下5条规则:

  1. 不要提供修改对象状态的方法
  2. 确保类不能被继承。这可以防止可变子类损害父类的不可变。防止子类化通常用 final 修饰父类
  3. 所有字段用 final 修饰。
  4. 所有字段设为私有
  5. 确保对任何可变组件的独占访问。如果你的类有任何引用可变对象的字段,请确保该类的客户端无法获得对这些对象的引用。

不可变类的优点

  1. 不可变对象线程安全,复用程度高开销小,不需要防御性拷贝
  2. 不仅可以复用不可变对象,还可以复用它们的内部实现
  3. 不可变对象很适合作为其它对象的构建模块,例如 Map 的键和 Set 的元素
  4. 不可变对象自带提供故障原子性

不可变类的缺点

  1. 不可变类的主要缺点是每个不同的值都需要一个单独的对象。不可变类BigInteger的flipBit方法会创建一个和原来对象只有一个bit不同的新对象。可变类BigSet可以在固定时间内修改对象单个bit

  2. 如果执行多步操作,在每一步生成一个新对象,最终丢弃除最终结果之外的所有对象,那么性能问题就会被放大。解决方法是使用伴随类,如果能预测客户端希望在不可变类上执行哪些复杂操作就可以使用包私有可变伴随可变类(BigInteger和内部的伴随类);否则提供一个公共可变伴随类,例如String和它的伴随类StringBuilder

设计不可变类

  1. 为了保证类不被继承,可以用final修饰类,也可以将构造函数变为私有或包私有,通过静态工厂提供对象
  2. BigInteger 和 BigDecimal没有声明为final,为了确保你的类依赖是不可变的BigInteger或BigDecimal,你需要检查对象类型,如果是BigInteger或BigDecimal的子类,那必须防御性复制
  3. 适当放松不可变类所有字段必须是 final 的限制可以提供性能,例如,用可变字段缓存计算结果,例如 hashCode 方法在第一次调用时缓存了hash
  4. 如果你选择让不可变类实现 Serializable,并且该类包含一个或多个引用可变对象的字段,那么你必须提供一个显式的 readObject 或 readResolve 方法,或者使用 ObjectOutputStream.writeUnshared 或 ObjectInputStream.readUnshared 方法

18. 优先选择组合而不是继承

在同一个包中使用继承是安全的,因为子类和父类的实现都由相同程序员控制。在对专为继承而设计和有文档的类时使用继承也是安全的(Item-19)。然而,对普通的非抽象类进行跨包继承是危险的。与方法调用不同,继承破坏了封装性。换句话说,子类的功能正确与否依赖于它的父类的实现细节。父类的实现可能在版本之间发生变化,如果发生了变化,子类可能会崩溃,即使子类的代码没有被修改过。因此,子类必须与其父类同步更新,除非父类是专门为继承的目的而设计的,并具有很明确的文档说明。

继承的风险

  1. 子类覆盖父类的多个方法,父类的多个方法之间有调用关系,因为多态,父类方法在调用其它父类方法时会调用到子类的方法
  2. 父类可以添加新方法,新方法没有确保在添加的元素满足断言,子类没有覆盖这个方法,导致调用这个方法时添加了非法元素
  3. 父类添加了新方法,但是子类继承原来的父类时也添加了相同签名和不同返回类型的方法,这时子类不能编译,如果签名和返回类型都相同,必须声明覆盖

组合

为新类提供一个引用现有类实例的私有字段,这种设计称为组合,因为现有的类是新类的一个组件。新类中的每个实例方法调用现有类实例的对应方法,并返回结果,这称为转发。比较好的写法是包装类+转发类。

总结

只有子类确实是父类的子类型的情况下,继承才合适。换句话说,两个类 A、B 之间只有 B 满足「is-a」关系时才应该扩展 A。如果你想让 B 扩展 A,那就问问自己:每个 B 都是 A 吗?如果不能对这个问题给出肯定回答,B 不应该扩展 A;如果答案是否定的,通常情况下,B 应该包含 A 的私有实例并暴露不同的 API:A 不是 B 的基本组成部分,而仅仅是其实现的一个细节。

19. 继承要设计良好并且有文档,否则禁止使用

必须精确地在文档中描述覆盖任何方法的效果。文档必须指出方法调用了哪些可覆盖方法、调用顺序以及每次调用的结果如何影响后续处理过程。描述由 Javadoc 标签 @implSpec 生成

20. 接口优于抽象类

Java 有两种机制来定义允许多重实现的类型:接口和抽象类。

接口优势

  1. 可以定义 mixin(混合类型)
    接口是定义 mixin(混合类型)的理想工具。粗略的说,mixin 是类除了「基本类型」之外还可以实现的类型,用于声明它提供了一些可选的行为。例如,Comparable 是一个 mixin 接口,它允许类的实例可以与其他的可相互比较的对象进行排序。这样的接口称为 mixin,因为它允许可选功能「混合」到基本类型中。抽象类不能用于定义 mixin,原因是:一个类不能有多个父类,而且在类层次结构中没有插入 mixin 的合理位置。‘

  2. 允许构造非层次化类型框架
    如果系统中有 n 个属性(例如唱、跳、rap),如果每个属性组合都封装成一个抽象类,组成一个层次化的类型框架,总共有2n2^n个类,而接口只需要 n 个

接口劣势

  1. 接口不能给 equals 和 hashCode 方法提供默认实现
  2. 接口不允许包含实例字段或者非公共静态成员(私有静态方法除外)

结合接口和抽象类的优势

模板方法模式:接口定义类型,提供默认方法,抽象类(骨架实现类)实现接口其余方法。继承骨架实现类已经完成直接实现接口的大部分工作。

按照惯例,骨架实现类称为 AbstractInterface,其中 Interface 是它们实现的接口的名称。例如 Collections Framework 提供了一个骨架实现来配合每个主要的集合接口:AbstractCollection、AbstractSet、AbstractList 和 AbstractMap。可以说,把它们叫做 SkeletalCollection、SkeletalSet、SkeletalList 和 SkeletalMap 更合理,但 Abstract 的用法现在已经根深蒂固。

编写骨架实现类的过程

研究接口有哪些方法,哪些方法可以提供默认实现,如果都可以提供默认实现就不需要骨架实现类,否则,声明一个实现接口的骨架实现类,实现所有剩余的接口方法

21. 为后代设计接口

Java 8 之前,往接口添加方法会导致实现它的类缺少方法,编译错误。Java 8 添加了默认方法,目的是允许向现有接口添加方法,但是向现有接口添加新方法有风险。

默认方法的风险

  1. 接口实现类需要同步调用每个方法,但是没有覆盖接口新加入的默认方法,导致调用默认方法时出现 ConcurrentModificationException
  2. 接口的现有实现类可以在没有错误或警告的情况下通过编译,但是运行时会出错

22. 接口只用于定义类型

接口只用于定义类型。类实现接口表明客户端可以用类的实例做什么。将接口定义为任何其他目的都是不合适的。

有一个反例是常量接口,接口内只包含 public static final 字段,它的问题在于类使用什么常量是实现细节,而实现常量接口会导致实现细节泄露到 API 中。

导出常量,有几个合理的选择。

  1. 如果这些常量与现有的类或接口紧密绑定,则应该将它们添加到类或接口。例如,所有数值包装类,比如 Integer 和 Double,都导出 MIN_VALUE 和 MAX_VALUE 常量。
  2. 枚举或者不可实例化的工具类,使用工具类的常量推荐静态导入

23. 类层次结构优于带标签的类

标签类

类的实例有两种或两种以上的样式,并且包含一个标签字段来表示实例的样式。标签类有许多缺点,可读性差、内存占用多、容易出错、添加新样式复杂

类层次结构

标签类只是类层次结构的简易模仿。要将已标签的类转换为类层次结构,

  1. 抽取标签类都有的方法、字段到一个抽象类中
  2. 继承抽象类实现都有的方法和子类特有的方法

24. 静态成员类优于非静态成员类

嵌套类是在另一个类中定义的类。嵌套类应该只为它的外部类服务。如果嵌套类在其它环境中有用,那么它应该是顶级类。有四种嵌套类:静态成员类、非静态成员类、匿名类和局部类。除了静态成员类,其它嵌套类被称为内部类。

静态成员类

静态成员类是最简单的嵌套类。最好把它看作是一个普通的类,只是碰巧在另一个类中声明而已,并且可以访问外部类的所有成员,甚至是那些声明为 private 的成员。静态成员类是其外部类的静态成员,并且遵守与其它静态成员相同的可访问性规则。如果声明为私有,则只能在外部类中访问,等等。

静态成员类作用
  1. 作为公共的辅助类,只有与它的外部类一起使用时才有意义,例如 Calculator 类和公有静态成员类 Operation 枚举
  2. 表示由其外部类表示的组件。例如,Map实现类内部的Entry类。Entry对象不需要访问 Map 。因此,使用非静态成员类来表示 entry 是浪费,私有静态成员类是最好的。

非静态成员类

从语法上讲,静态成员类和非静态成员类之间的唯一区别是静态成员类在其声明中具有修饰符 static。尽管语法相似,但这两种嵌套类有很大不同。非静态成员类的每个实例都隐式地与外部类的实例相关联,非静态成员类的实例方法可以调用外部实例上的方法,或者使用受限制的 this (父类.this)构造获得对外部实例的引用。如果嵌套类的实例可以独立于外部类的实例存在,那么嵌套类必须是静态成员类;非静态成员类的实例依赖外部类的实例

非静态成员类实例与外部类实例之间的关联是在创建成员类实例时建立的,之后无法修改。通常,关联是通过从外部类的实例方法中调用非静态成员类构造函数自动建立的。使用 enclosingInstance.new MemberClass(args) 表达式手动建立关联是可能的,尽管这种情况很少见。正如你所期望的那样,关联占用了非静态成员类实例中的空间,并增加了构造时间。

如果声明的成员类不需要访问外部类的实例,那么应始终在声明中添加 static 修饰符,如果省略这个修饰符,每个实例都有一个隐藏的对其外部实例的额外引用。存储此引用需要时间和空间,更糟糕的是,外部类可能不能被垃圾回收。

非静态成员类作用

非静态成员类的一个常见用法是定义一个 Adapter,它允许外部类的实例被视为某个不相关类的实例。例如,Map 接口的实现类通常使用非静态成员类来实现它们的集合视图, Set 和 List,通常使用非静态成员类来实现它们的迭代器

匿名类

匿名类没有名称。它不是外部类的成员。它不是与其它成员一起声明的,而是在使用时同时声明和实例化。匿名类可以在代码中用在任何一个可以用表达式的地方。当且仅当它们出现在非静态环境(没有写在静态方法里面)时,匿名类才持有外部类实例。但是,即使它们出现在静态环境中,它们也不能有除常量以外的任何静态成员。

匿名类的使用有很多限制。你只能在声明它们的时候实例化,你不能执行 instanceof 测试,也不能执行任何其它需要命名类的操作。你不能声明一个匿名类来实现多个接口或继承一个类并同时实现一个接口。匿名类的使用者除了从父类继承的成员外,不能调用任何成员。因为匿名类出现在表达式中,所以它们必须保持简短——大约10行或更短,否则会影响可读性。

匿名类作用

在 lambda 表达式被添加的 Java 之前,匿名类是动态创建小型函数对象和进程对象的首选方法,但 lambda 表达式现在是首选方法(Item-42)。匿名类的另一个常见用法是实现静态工厂方法(参见 Item-20 中的 intArrayAsList 类)

局部类

局部类是四种嵌套类中最不常用的。局部类几乎可以在任何能够声明局部变量的地方使用,并且遵守相同的作用域规则。局部类具有与其它嵌套类相同属性。与成员类一样,它们有名称,可以重复使用呢。与匿名类一样,他们只有在非静态环境中定义的情况下才具有外部类实例,而且它们不能包含静态静态成员。和匿名类一样,它们应该保持简短,以免损害可读性。

嵌套类总结

简单回顾一下,有四种不同类型的嵌套类,每一种都有自己的用途。如果嵌套的类需要在单个方法之外可见,或者太长,不适合放入方法中,则使用成员类。除非成员类的每个实例都需要引用其外部类实例,否则让它保持静态。假设嵌套类属于方法内部,如果你只需要从一个位置创建实例,并且存在一个能够描述类的现有类型,那么将其设置为匿名类;否则,将其设置为局部类。

25. 源文件仅限有单个顶层类

虽然 Java 编译器允许你在单个源文件中定义多个顶层类,但这样做没有任何好处,而且存在重大风险。这种风险源于这样一个事实:在源文件中定义多个顶层类使得为一个类提供多个定义成为可能。所使用的定义受源文件传给编译器的顺序的影响。

]]>
+ + + + + <h2 id="第四章-类和接口"><a class="markdownIt-Anchor" href="#第四章-类和接口"></a> 第四章 类和接口</h2> +<h3 id="15-尽量减少类和成员的可访问性"><a class="markdownIt-Anchor" hr + + + + + + + + + + + +
+ + + Effective-Java学习笔记(二) + + https://zunpan.github.io/2022/06/30/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89/ + 2022-06-30T11:50:05.000Z + 2023-09-24T04:27:40.274Z + + 第三章 对象的通用方法

10. 覆盖 equals 方法时应遵守的约定

不覆盖equals方法的情况

  • 类的每个实例本质上都是唯一的。例如Thread类,它是活动实体类而不是值类

  • 该类不需要提供逻辑相等测试。例如java.util.regex.Pattern可以覆盖equals方法来检查两个Pattern实例是否表示完全相同的正则表达式,但是这个类的设计人员认为客户端不需要这个功能,所以没有覆盖

  • 父类已经覆盖了equals方法,父类的行为也适合于这个类。例如大多数Set的equals从AbstractSet继承,List从AbstractList继承,Map从AbstractMap继承

  • 类是私有的并且你确信它的equals方法永远不会被调用。保险起见,你可以按如下方式覆盖equals方法,以确保它不会被意外调用

    @Overridepublic boolean equals(Object o) {    throw new AssertionError(); // Method is never called}

覆盖equals方法的时机

当一个类有一个逻辑相等的概念,而这个概念不同于仅判断对象的同一性(相同对象的引用),并且父类没有覆盖 equals。对于值类通常是这样。值类只是表示值的类,例如 Integer 或 String。程序员希望发现它们在逻辑上是否等价,而不是它们是否引用相同的对象。覆盖 equals 方法不仅是为了满足程序员的期望,它还使实例能够作为 Map 的键或 Set 元素时,具有可预测的、理想的行为。

单例模式的值类不需要覆盖equals方法。例如,枚举类型就是单例值类。逻辑相等就是引用相等。

覆盖equals方法的规范

  • 反身性:对于任何非空的参考值x,x.equals(x)必须返回true

  • 对称性:x.equals(y)y.equals(x)的值要么都为true要么为false

  • 传递性:对于非空引用x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也返回true

  • 一致性:对于任何非空的引用值 x 和 y,x.equals(y) 的多次调用必须一致地返回 true 或一致地返回 false,前提是不修改 equals 中使用的信息。

  • 非空性:对于非空引用x,x.equals(null)返回false,不需要显示判断是否为null,因为equals方法需要将参数转为成相同类型,转换之前会使用instanceof运算符来检查类型是否正确,如果为null,也会返回false

    @Overridepublic boolean equals(Object o) {    if (!(o instanceof MyType))        return false;    MyType mt = (MyType) o;    ...}

高质量构建equals方法的步骤

1、使用==检查参数是否是this对象的引用,如果是,返回true,这是一种性能优化,如果比较的开销很大,这种优化很有必要
2、使用instanceof运算符检查参数是否有正确类型。
3、将参数转换为正确的类型。因为在转换前进行了instanceof判断,所以肯定可以强转成功
4、对类中的每个有意义的字段检查是否和参数的相应字段匹配

对于不是float和double的基本类型字段,使用==比较;对应对象引用字段,递归调用equals方法;对于float字段,使用静态方法Float.compare(float,float)方法;对于double字段,使用Double.compare(double,double)。float和double字段的特殊处理是由于Float.NaN,-0.0f 和类似的双重值的存在;对于数组字段,使用Arrays.equals方法。

一些对象引用字段可能允许null出现。为了避免可能出现NullPointerException,请使用Objects.equals(Object,Object)来检查对象的字段是否相等。

对于某些类,字段比较非常复杂,如果是这样,可以存储字段的规范形式,以便equals方法进行开销较小的比较。这种技术最适合于不可变类;如果对象可以修改,则必须使规范形式保持最新

equals 方法的性能可能会受到字段比较顺序的影响。为了获得最佳性能,你应该首先比较那些更可能不同、比较成本更低的字段。不能比较不属于对象逻辑状态的字段,例如用于同步操作的锁字段。派生字段可以不比较,这样可以提高性能,但是如果派生字段包括了对象的所有信息,比如说多边形面积,可以从边和顶点计算得出,那么先比较面积,面积不一样就肯定不是同一个对象,这样可以减小开销

一些警告

  • 覆盖equals方法时,也要覆盖hashCode方法
  • 考虑任何形式的别名都不是一个好主意。例如,File类不应该尝试将引用同一文件的符号链接等同起来
  • 不要用别的类型替换equals方法的Object类型

11. 当覆盖equals方法时,总是覆盖hashCode方法

Object类中hashCode方法的规范

  • 应用程序执行期间对对象重复调用hashCode方法,它必须返回相同的值,前提是不修改equals方法中用于比较的信息。这个值不需要在应用程序的不同执行之间保持一致
  • 如果equals(Object)方法返回true,那么在这两个对象上调用hashCode方法必须产生相同的整数结果
  • 如果equals(Object)方法返回false,hashCode方法的值不需要一定不同,但是,不同对象的hashCode不一样可以提高散列表性能

当没有覆盖hashCode方法时,将违反第二条规范:逻辑相等的对象必须有相等的散列码。两个不同的对象在逻辑上是相等的,但它们的hashCode一般不相等。例如用Item-10中的PhoneNumber类实例作为HashMap的键

Map<PhoneNumber, String> m = new HashMap<>();m.put(new PhoneNumber(707, 867, 5309), "Jenny");

此时,你可能期望m.get(new PhoneNumber(707, 867,5309)) 返回「Jenny」,但是它返回 null。因为PhoneNumber类没有覆盖hashCode方法,插入到HashMap和从HashMap中获取的实例具有不相同的散列码,这违法了hashCode方法规范。因此,get方法查找电话号码的散列桶与put方法存储电话号码的散列桶不同。

解决这个问题有一个最简单但很糟糕的实现

// The worst possible legal hashCode implementation - never use!@Overridepublic int hashCode() { return 42; }

它确保了逻辑相等的对象具有相同的散列码。同时它也很糟糕,因为每个对象的散列码都相同了。每个对象都分配到一个存储桶中,散列表退化成链表。

散列算法设计步骤

一个好的散列算法大概率为逻辑不相等对象生成不相同的散列码。理想情况下,一个散列算法应该在所有int值上均匀合理分布所有不相等对象。实现理想情况很困难,但实现一个类似的并不难,这里有一个简单的方式:
1、声明一个名为 result 的int变量,并将其初始化为对象中第一个重要字段的散列码c,如步骤2.a中计算的那样
2、对象中剩余的重要字段f,执行以下操作:
a. 为字段计算一个整数散列码c : 如果字段是基本数据类型,计算Type.hashCode(f),其中type是 f 类型对应的包装类 ; 如果字段是对象引用,并且该类的equals方法通过递归调用equals方法来比较字段,则递归调用字段上的hashCode方法。如果需要更复杂的比较,则为该字段计算一个【canonical representation】,并在canonical representation上调用hashCode方法。如果字段的值为空,则使用0(或其它常数,但0是惯用的);如果字段是一个数组,则对数组中每个重要元素都计算散列码,并用2.b步骤逐个组合。如果数组中没有重要元素,则使用常量,最好不是0。如果所有元素都很重要,那么使用Arrays.hashCode

b. 将2.a步骤中计算的散列码合并到result变量,如下所示

result = 31 * result + c;

3、返回result变量

步骤2.b中的乘法说明result依赖字段的顺序,如果类具有多个类似字段,那么乘法会产生更好的hash性能。例如字符串hash算法中如果省略乘法,那么不同顺序的字符串都会有相同的散列码。

选择31是因为它是奇素数。如果是偶数,乘法运算就会溢出,信息就会丢失,因为乘法运算等同于移位。使用素数的好处不太明显,但它是传统用法。31有一个很好的特性,可以用移位和减法来代替乘法,从而在某些体系结构上获得更好的性能:31 * i == (i <<5) – i。现代虚拟机自动进行这种优化。

根据前面的步骤,给PhoneNumber类写一个hashCode方法

// Typical hashCode method@Overridepublic int hashCode() {    int result = Short.hashCode(areaCode);    result = 31 * result + Short.hashCode(prefix);    result = 31 * result + Short.hashCode(lineNum);    return result;}

因为这个方法返回一个简单的确定的计算结果,它的唯一输入是 PhoneNumber 实例中的三个重要字段,所以很明显,相等的 PhoneNumber 实例具有相等的散列码。实际上,这个方法是 PhoneNumber 的一个非常好的 hashCode 方法实现,与 Java 库中的 hashCode 方法实现相当。它很简单,速度也相当快,并且合理地将不相等的电话号码分散到不同的散列桶中。

虽然这个Item里的方法可以提供一个相当不错的散列算法,但它不是最先进的,对于大多数用途是足够的,如果需要不太可能产生冲突的散列算法。请参阅 Guava 的 com.google.common.hash.Hashing

Objects类有一个静态方法,它接受任意数量的对象并返回它们的散列码。这个名为hash的方法允许你编写只有一行代码的hashCode方法,它的质量可以与本Item提供的编写方法媲美。但不幸的是它们运行得很慢,因为它需要创建数组来传递可变数量的参数,如果有参数是原始类型的,则需要进行装箱和拆箱。推荐只在性能不重要的情况下使用这种散列算法。下面是使用这个静态方法编写的PhoneNumber的散列算法

// One-line hashCode method - mediocre performance@Overridepublic int hashCode() {    return Objects.hash(lineNum, prefix, areaCode);}

缓存散列值

如果类是不可变的,并且计算散列码的成本非常高,那么可以考虑在对象中缓存散列码,而不是每次调用重新计算。如果这个类的对象会被用作散列键,那么应该在创建对象时就计算散列码。要不然就在第一次调用时计算散列码

12. 始终覆盖toString方法

虽然 Object 提供 toString 方法的实现,但它返回的字符串通常不是类的用户希望看到的。它由后跟「at」符号(@)的类名和散列码的无符号十六进制表示(例如 PhoneNumber@163b91)组成。toString 的通用约定是这么描述的,返回的字符串应该是「简洁但信息丰富的表示,易于阅读」。虽然有人认为 PhoneNumber@163b91 简洁易懂,但与 707-867-5309 相比,它的信息量并不大。toString 约定接着描述,「建议所有子类覆盖此方法。」好建议,确实!

虽然它不如遵守euals和hashCode约定(Item10和Item11)那么重要,但是提供一个好的toString方法更便于调试。当对象被传递给 println、printf、字符串连接操作符或断言或由调试器打印时,将自动调用 toString 方法。即使你从来没有调用 toString 对象,其他人也可能使用。例如,使用该对象的组件可以在日志错误消息中包含对象的字符串表示。如果你不覆盖 toString,该消息可能完全无用。

13. 明智地覆盖clone方法

Cloneable接口的作用

Cloneable接口的作用是声明类可克隆,但是接口不包含任何方法,类的clone方法继承自Object,并且Object类的clone方法是受保护的,无法跨包调用,虽然可以通过反射调用,但也不能保证对象具有可访问的 clone 方法(如果类没有覆盖clone方法可以通过获取父类Object调用clone方法,但是如果类没实现Cloneable接口调用会抛出CloneNotSupportedException)。

既然 Cloneable 接口不包含任何方法,用它来做什么呢?它决定了 Object 类受保护的 clone 实现的行为:如果一个类实现了 Cloneable 接口,Object 类的 clone 方法则返回该类实例的逐字段拷贝;没实现 Cloneable 接口调用 clone 方法会抛出 CloneNotSupportedException。这是接口非常不典型的一种使用方式,不应该效仿。通常,类实现接口可以表明类能够为其客户端做些什么。在本例中,它修改了父类上受保护的方法的行为。

clone 方法规范

虽然规范没有说明,但是在实践中,实现 Cloneable 接口的类应该提供一个功能正常的 public clone 方法。

clone方法的一般约定很薄弱。下面的内容是从Object规范复制过来的

Creates and returns a copy of this object. The precise meaning of “copy” may depend on the class of the object. The general intent is that, for any object x,the expressionx.clone() != xwill be true, and the expressionx.clone().getClass() == x.getClass()will be true, but these are not absolute requirements. While it is typically the case thatx.clone().equals(x)will be true, this is not an absolute requirement.

clone方法创建并返回对象的副本。「副本」的确切含义可能取决于对象的类。通常,对于任何对象 x,表达式 x.clone() != x、x.clone().getClass() == x.getClass() 以及 x.clone().equals(x) 的值都将为 true,但都不是绝对的。(equals方法应覆盖为比较对象中的字段才能得到true,默认实现是比较对象地址,结果永远为false)

按照约定,clone方法返回的对象应该通过调用super.clone() 来获得。如果一个类和它的所有父类(Object类除外)都遵守这个约定,表达式 x.clone().getClass() == x.getClass() 则为 true

按照约定,返回的对象应该独立于被克隆的对象。为了实现这种独立性,可能需要在super.clone() 前修改对象的一个或多个字段

这种机制有点类似于构造方法链,只是没有强制执行:

  • 如果一个类的clone方法返回的实例不是通过调用 super.clone() 而是通过调用构造函数获得的,编译器不会报错,但是如果这个类的子类调用super.clone(),由此产生的对象将是错误的,影响子类clone方法正常工作
  • 如果覆盖clone方法的类是final修饰的,那么可以忽略这个约定,因为不会有子类
  • 如果一个final修饰的类的clone方法不调用super.clone()。该类没有理由实现Cloneable接口,因为它不依赖于Object的clone方法

覆盖clone方法

  1. 如果类是不可变的,不要提供clone方法
  2. 如果类的字段都是基本类型或不可变对象的引用,那么直接这个类的clone方法直接调用super.clone()即可
  3. 如果类的字段包含可变对象的引用,需要递归调用可变对象的深拷贝方法

一些细节

  1. 和构造函数一样,不要在clone方法中调用可覆盖方法。如果clone方法调用一个在子类中被覆盖的方法,这个方法将在子类修复其在克隆中的状态之前执行,很可能导致克隆和原始对象的破坏。

  2. Object的clone方法被声明为抛出CloneNotSupportedException,但是覆盖方法时 try-catch 异常就行,不抛出受检查异常的方法更容易使用

  3. 设计可继承的类时不要实现 Cloneable接口(如果实现了,子类就必须对外提供clone方法)。你可以选择通过实现一个功能正常的受保护克隆方法来模拟 Object 的行为,该方法声明为抛出 CloneNotSupportedException。这给子类实现 Cloneable 或不实现 Cloneable 的自由。或者,你可以选择不实现一个有效的克隆方法,并通过提供以下退化的克隆实现来防止子类实现它:

    // clone method for extendable class not supporting Cloneable@Overrideprotected final Object clone() throws CloneNotSupportedException {    throw new CloneNotSupportedException();}
  4. 如果你编写了一个实现了 Cloneable 接口的线程安全类,请记住它的 clone 方法必须正确同步,就像其他任何方法一样。

总结

回顾一下,所有实现 Cloneable 接口的类都应该使用一个返回类型为类本身的公有方法覆盖 clone。这个方法应该首先调用 super.clone(),然后「修复」任何需要「修复」的字段。通常,这意味着复制任何包含对象内部「深层结构」的可变对象,并将克隆对象对这些对象的引用替换为对其副本的引用。虽然这些内部副本通常可以通过递归调用 clone 来实现,但这并不总是最好的方法。如果类只包含基本数据类型的字段或对不可变对象的引用,那么很可能不需要修复任何字段。这条规则也有例外。例如,表示序列号或其他唯一 ID 的字段需要修复,即使它是基本数据类型或不可变的。

搞这么复杂真的有必要吗?答案是否定的。如果你扩展了一个已经实现了 Cloneable 接口的类,那么除了实现行为良好的 clone 方法之外,你别无选择。否则,最好提供对象复制的替代方法。一个更好的对象复制方法是提供一个复制构造函数或复制工厂。复制构造函数是一个简单的构造函数,它接受单个参数,其类型是包含构造函数的类,例如

// Copy constructorpublic Yum(Yum yum) { ... };

复制工厂与复制构造函数的静态工厂(Item-1)类似:

// Copy factorypublic static Yum newInstance(Yum yum) { ... };

复制构造函数方法及其静态工厂变体与克隆方法相比有许多优点:

  1. 它们不依赖于易发生风险的语言外对象创建机制(Object的clone方法是native的);
  2. 他们不要求无法强制执行的约定(clone方法一定要先调用super.clone());
  3. 它们与 final 字段不冲突(clone方法不能修改final字段);
  4. 它们不会抛出不必要的 checked 异常;
  5. 而且不需要强制类型转换。
  6. 可以提供类型转换构造函数

考虑到与 Cloneable 相关的所有问题,新的接口不应该继承它,新的可扩展类不应该实现它。通常,复制功能最好由构造函数或工厂提供。这个规则的一个明显的例外是数组,最好使用 clone 方法来复制数组。

14. 考虑实现 Comparable 接口

与本章讨论的其它方法不同,compareTo 方法不是 Object 中声明的,而是 Comparable 接口中的唯一方法。一个类实现Comparable,表明实例具有自然顺序(字母或数字或时间顺序)。Java 库中的所有值类以及所有枚举类型(Item-34)都实现了 Comparable接口

compareTo方法约定

compareTo 方法的一般约定类似于 equals 方法:
将一个对象与指定对象进行顺序比较。当该对象小于、等于或大于指定对象时,对应返回一个负整数、零或正整数。如果指定对象的类型阻止它与该对象进行比较,则抛出 ClassCastException

在下面的描述中, sgn(expression) 表示数学中的符号函数,它被定义为:根据传入表达式的值是负数、零或正数,对应返回-1、0或1。

  • 实现类必须确保所有 x 和 y 满足 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))(这意味着 x.compareTo(y) 当且仅当 y.compareTo(x) 抛出异常时才抛出异常)
  • 实现类还必须确保关系是可传递的:(x.compareTo(y) > 0 && y.compareTo(z) > 0) 意味着 x.compareTo(z) > 0
  • 最后,实现类必须确保 x.compareTo(y) == 0 时,所有的 z 满足 sgn(x.compareTo(z)) == sgn(y.compareTo(z))
  • 强烈建议 (x.compareTo(y)== 0) == (x.equals(y)) 成立,但这不是必需的。一般来说,任何实现 Comparable 接口并违法此条件的类都应该清除地注明这一事实。推荐的表述是“Note: This class has a natural ordering that is inconsistent with equals.”

与equals方法不同,equals方法入参是Object类型,而compareTo方法不需要和不同类型的对象比较:当遇到不同类型的对象时,允许抛出ClassCastException

就像违反 hashCode 约定的类可以破坏依赖 hash 的其他类一样,违反 compareTo 约定的类也可以破坏依赖 Comparable 的其他类。依赖 Comparable 的类包括排序集合 TreeSet 和 TreeMap,以及实用工具类 Collections 和 Arrays,它们都包含搜索和排序算法。

compareTo的约定和equals约定有相同的限制:反身性、对称性和传递性。如果要向实现 Comparable 的类中添加值组件,不要继承它;编写一个不相关的类,其中包含第一个类的实例。然后提供返回所包含实例的「视图」方法。这使你可以自由地在包含类上实现你喜欢的任何 compareTo 方法,同时允许它的客户端在需要时将包含类的实例视为被包含类的实例。

compareTo 约定的最后一段是一个强烈建议而不是要求,它只是简单地说明了 compareTo 方法所施加地同等性检验通常应该与 equals 方法返回相同的结果。如果一个类的 compareTo 方法强加了一个与 equals 不一致的顺序,那么这个类仍然可以工作,但是包含该类元素的有序集合可能无法遵守集合接口(Collection、Set 或 Map)的一般约定。这是因为这些接口的一般约定是根据 equals 方法定义的,但是有序集合使用 compareTo 代替了 equals 实施同等性建议,这是需要注意的地方

例如,考虑 BigDecimal 类,它的 compareTo 方法与 equals 不一致。如果你创建一个空的 HashSet 实例,然后添加 new BigDecimal(“1.0”) 和 new BigDecimal(“1.00”),那么该 HashSet 将包含两个元素,因为添加到该集合的两个 BigDecimal 实例在使用 equals 方法进行比较时结果是不相等的。但是,如果你使用 TreeSet 而不是 HashSet 执行相同的过程,那么该集合将只包含一个元素,因为使用 compareTo 方法比较两个 BigDecimal 实例时结果是相等的。(有关详细信息,请参阅 BigDecimal 文档。)

编写 compareTo 方法

  1. 递归调用引用字段的 compareTo 方法。如果引用字段没有实现 Comparable,或者需要一个非标准的排序,那么应使用比较器

  2. 基本字段类型使用包装类的静态 compare 方法比较

  3. 从最重要字段开始比较

  4. 考虑使用 java.util.Comparator接口的比较器构造方法

]]>
+ + + + + <h2 id="第三章-对象的通用方法"><a class="markdownIt-Anchor" href="#第三章-对象的通用方法"></a> 第三章 对象的通用方法</h2> +<h3 id="10-覆盖-equals-方法时应遵守的约定"><a class="markdo + + + + + + + + + + + +
+ + + Effective-Java学习笔记(一) + + https://zunpan.github.io/2022/06/23/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%80%EF%BC%89/ + 2022-06-23T10:11:15.000Z + 2023-09-24T04:27:40.273Z + + 第二章 创建和销毁对象

1. 考虑用静态工厂方法代替构造函数

静态工厂方法是一个返回该类实例的 public static 方法。例如,Boolean类的valueOf方法

public static Boolean valueOf(boolean b) {    return b ? Boolean.TRUE : Boolean.FALSE;}

要注意静态工厂方法与设计模式中的工厂方法不同。

静态工厂方法优点:

  1. 静态工厂方法有确切名字,客户端实例化对象时代码更易懂。例如,BigInteger类中返回可能为素数的BigInteger对象静态工厂方法叫BigInteger.probablePrime。此外,每个类的构造函数签名是唯一的,但是程序员可以通过调整参数类型、个数或顺序修改构造函数的签名,这样会给客户端实例化对象带来困惑,因为静态工厂方法有确切名称所以不会出现这个问题
  2. 静态工厂方法不需要在每次调用时创建新对象。例如Boolean.valueOf(boolean),true和false会返回预先创建好的对应Boolean对象。这种能力允许类在任何时候都能严格控制存在的实例,常用来实现单例
  3. 静态工厂方法可以获取任何子类的对象。这种能力的一个应用是API可以不公开子类或者实现类的情况下返回对象。例如Collections类提供静态工厂生成不是public的子类对象,不可修改集合和同步集合等
  4. 静态工厂方法返回对象的类型可以根据输入参数变换。例如,EnumSet类的noneOf方法,当enum的元素个数小于等于64,noneOf方法返回RegularEnumSet类型的对象,否则返回JumboEnumSet类型的对象
  5. 静态工厂方法的返回对象的类不需要存在。这种灵活的静态工厂方法构成了服务提供者框架的基础。service provider框架有三个必要的组件:代表实现的service interface;provider registration API,提供者用来注册实现;service access API,客户端使用它来获取服务的实例,服务访问API允许客户端选择不同实现,是一个灵活的静态工厂方法。service provider第四个可选的组件是service provider interface,它描述了产生service interface实例的工厂对象。在JDBC中,Connection扮演service interface角色,DriverManager.registerDriver是provider registration API,DriverManager.getConnection是service access API,Driver是service provider interface

静态工厂方法的缺点:

  1. 只提供静态工厂方法而没有public或者protected的构造方法就不能被继承,例如Collections类就不能被继承

  2. 静态工厂方法没有构造函数那么显眼,常见的静态工厂方法名字如下:
    from,一种类型转换方法,该方法接受单个参数并返回该类型的相应实例,例如:

    Date d = Date.from(instant);

    of,一个聚合方法,它接受多个参数并返回一个包含这些参数的实例,例如:

    Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);

    valueOf,一种替代 from 和 of 但更冗长的方法

    BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);

    instance 或 getInstance,返回一个实例,该实例由其参数(如果有的话)描述,但不具有相同的值,例如:

    StackWalker luke = StackWalker.getInstance(options);

    create 或 newInstance,与 instance 或 getInstance 类似,只是该方法保证每个调用都返回一个新实例,例如:

    Object newArray = Array.newInstance(classObject, arrayLen);

    getType,类似于 getInstance,但如果工厂方法位于不同的类中,则使用此方法。其类型是工厂方法返回的对象类型,例如:

    FileStore fs = Files.getFileStore(path);

    newType,与 newInstance 类似,但是如果工厂方法在不同的类中使用。类型是工厂方法返回的对象类型,例如:

    BufferedReader br = Files.newBufferedReader(path);`

    type,一个用来替代 getType 和 newType 的比较简单的方式,例如:

    List<Complaint> litany = Collections.list(legacyLitany);

2. 当构造函数有多个参数时,考虑改用Builder

静态工厂和构造函数都有一个局限:不能对大量可选参数做很好扩展。例如,一个表示食品营养标签的类,必选字段有净含量、热量,另外有超过20个可选字段,比如反式脂肪、钠等。

为这种类编写构造函数,通常是使用可伸缩构造函数,这里展示四个可选字段的情况

// Telescoping constructor pattern - does not scale well!public class NutritionFacts {    private final int servingSize; // (mL) required    private final int servings; // (per container) required    private final int calories; // (per serving) optional    private final int fat; // (g/serving) optional    private final int sodium; // (mg/serving) optional    private final int carbohydrate; // (g/serving) optional    public NutritionFacts(int servingSize, int servings) {        this(servingSize, servings, 0);    }    public NutritionFacts(int servingSize, int servings, int calories) {        this(servingSize, servings, calories, 0);    }    public NutritionFacts(int servingSize, int servings, int calories, int fat) {        this(servingSize, servings, calories, fat, 0);    }    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {        this(servingSize, servings, calories, fat, sodium, 0);    }    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {        this.servingSize = servingSize;        this.servings = servings;        this.calories = calories;        this.fat = fat;        this.sodium = sodium;        this.carbohydrate = carbohydrate;    }}

当你想创建指定carbohydrate的实例,就必须调用最后一个构造函数,这样就必须给出calories、fat、sodium的值。当有很多可选参数时,这种模式可读性很差。

当遇到许多可选参数时,另一种选择是使用JavaBean模式,在这种模式中,调用一个无参数的构造函数来创建对象,然后调用setter方法来设置参数值

// JavaBeans Pattern - allows inconsistency, mandates mutabilitypublic class NutritionFacts {    // Parameters initialized to default values (if any)    private int servingSize = -1; // Required; no default value    private int servings = -1; // Required; no default value    private int calories = 0;    private int fat = 0;    private int sodium = 0;    private int carbohydrate = 0;    public NutritionFacts() { }    // Setters    public void setServingSize(int val) { servingSize = val; }    public void setServings(int val) { servings = val; }    public void setCalories(int val) { calories = val; }    public void setFat(int val) { fat = val; }    public void setSodium(int val) { sodium = val; }    public void setCarbohydrate(int val) { carbohydrate = val; }}

这种模式比可伸缩构造函数模式更易读,但有严重的缺点。因为对象的构建要调用多个set方法,所以对象可能会在构建过程中处于不一致状态(多线程下,其它线程使用了未构建完成的对象)

第三种选择是建造者模式,它结合了可伸缩构造函数模式的安全性和JavaBean模式的可读性。客户端不直接生成所需的对象,而是使用所有必需的参数调用构造函数或静态工厂方法生成一个builder对象。然后,客户端在builder对象上调用像set一样的方法来设置可选参数,最后调用build方法生成所需对象。Builder通常是它构建的类的静态成员类

// Builder Patternpublic class NutritionFacts {    private final int servingSize;    private final int servings;    private final int calories;    private final int fat;    private final int sodium;    private final int carbohydrate;    public static class Builder {        // Required parameters        private final int servingSize;        private final int servings;        // Optional parameters - initialized to default values        private int calories = 0;        private int fat = 0;        private int sodium = 0;        private int carbohydrate = 0;        public Builder(int servingSize, int servings) {            this.servingSize = servingSize;            this.servings = servings;        }        public Builder calories(int val) {            calories = val;            return this;        }        public Builder fat(int val) {            fat = val;            return this;        }        public Builder sodium(int val) {            sodium = val;            return this;        }        public Builder carbohydrate(int val) {            carbohydrate = val;            return this;        }        public NutritionFacts build() {            return new NutritionFacts(this);        }    }    private NutritionFacts(Builder builder) {        servingSize = builder.servingSize;        servings = builder.servings;        calories = builder.calories;        fat = builder.fat;        sodium = builder.sodium;        carbohydrate = builder.carbohydrate;    }}

NutritionFacts类是不可变的。Builder的set方法返回builder对象本身,这样就可以链式调用。下面是客户端代码的样子:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();

为了简介,这里省略的参数校验。参数校验在Builder的构造函数和方法中。多参数校验在build方法中

建造者模式也适用于抽象类,抽象类有抽象Builder

import java.util.EnumSet;import java.util.Objects;import java.util.Set;// Builder pattern for class hierarchiespublic abstract class Pizza {    public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}    final Set<Topping> toppings;    abstract static class Builder<T extends Builder<T>> {        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);        public T addTopping(Topping topping) {            toppings.add(Objects.requireNonNull(topping));            return self();        }        abstract Pizza build();        // Subclasses must override this method to return "this"        protected abstract T self();    }    Pizza(Builder<?> builder) {        toppings = builder.toppings.clone(); // See Item 50    }}
import java.util.Objects;public class NyPizza extends Pizza {    public enum Size {SMALL, MEDIUM, LARGE}    private final Size size;    public static class Builder extends Pizza.Builder<Builder> {        private final Size size;        public Builder(Size size) {            this.size = Objects.requireNonNull(size);        }        @Override        public NyPizza build() {            return new NyPizza(this);        }        @Override        protected Builder self() {            return this;        }    }    private NyPizza(Builder builder) {        super(builder);        size = builder.size;    }}public class Calzone extends Pizza {    private final boolean sauceInside;    public static class Builder extends Pizza.Builder<Builder> {        private boolean sauceInside = false; // Default        public Builder sauceInside() {            sauceInside = true;            return this;        }        @Override        public Calzone build() {            return new Calzone(this);        }        @Override        protected Builder self() {            return this;        }    }    private Calzone(Builder builder) {        super(builder);        sauceInside = builder.sauceInside;    }}

客户端实例化对象代码如下:

NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();Calzone calzone = new Calzone.Builder().addTopping(HAM).sauceInside().build();

建造者模式非常灵活,一个builder对象可以反复构建多个对象,可以通过builder中的方法调用生成不同的对象。建造者模式的缺点就是生成一个对象前要先创建它的builder对象,性能会稍差一些,但为了未来字段更好扩展,建议还是用建造者模式

3. 使用私有构造函数或枚举类型创建单例

单例是只实例化一次的类。当类不保存状态或状态都一致,那么它的对象本质上都是一样的,可以用单例模式创建

实现单例有两种方法。两者都基于私有化构造函数和对外提供 public static 成员,在第一个方法中,该成员是个用final修饰的字段

// Singleton with public final fieldpublic class Elvis {    public static final Elvis INSTANCE = new Elvis();    private Elvis() { ... }    public void leaveTheBuilding() { ... }}

私有构造函数只调用一次,用于初始化public static final 修饰的Elvis类型字段INSTANCE。一旦初始化Elvis类,就只会存在一个Elvis实例。客户端不能再创建别的实例,但是要注意的是拥有特殊权限的客户端可以利用反射调用私有构造函数生成实例

Constructor<?>[] constructors = Elvis.class.getDeclaredConstructors();AccessibleObject.setAccessible(constructors, true);Arrays.stream(constructors).forEach(name -> {    if (name.toString().contains("Elvis")) {        Elvis instance = (Elvis) name.newInstance();        instance.leaveTheBuilding();    }});

第二种方法,对外提供的 public static 成员是个静态工厂方法

// Singleton with static factorypublic class Elvis {    private static final Elvis INSTANCE = new Elvis();    private Elvis() { ... }    public static Elvis getInstance() { return INSTANCE; }    public void leaveTheBuilding() { ... }}

所有对getInsance()方法的调用都返回相同的对象,但是同样可以通过反射调用私有构造函数创建对象。
这两种方法要实现可序列化,仅仅在声明中添加implements Serializable是不够的。还要声明所有实例字段未transient,并提供readResolve方法,否则,每次反序列化都会创建一个新实例。JVM在反序列化时会自动调用readResolve方法

// readResolve method to preserve singleton propertyprivate Object readResolve() {    // Return the one true Elvis and let the garbage collector    // take care of the Elvis impersonator.    return INSTANCE;}

实现单例的第三种方法时声明一个单元素枚举

// Enum singleton - the preferred approachpublic enum Elvis {    INSTANCE;    public void leaveTheBuilding() { ... }}

这种方法类似于 public 字段方法,但是它更简洁,默认提供了序列化机制,提供了对多个实例化的严格保证,即使面对复杂的序列化或反射攻击也是如此。这种方法可能有点不自然,但是单元素枚举类型通常是实现单例的最佳方法。但是,如果你的单例要继承父类,那么就不能用这种方法

4. 用私有构造函数实施不可实例化

工具类不需要实例化,它里面的方法都是public static的,可以通过私有化构造函数使类不可实例化

// Noninstantiable utility classpublic class UtilityClass {    // Suppress default constructor for noninstantiability    private UtilityClass() {        throw new AssertionError();    } ... // Remainder omitted}

AssertionError不是必须有的,但可以防止构造函数被意外调用(反射)

这种用法也防止了类被继承。因为所有子类构造函数都必须显示或者隐式调用父类构造函数,但父类构造函数私有化后就无法调用

5. 依赖注入优于硬连接资源

有些类依赖于一个或多个资源。例如拼写检查程序依赖于字典。错误实现如下:

// Inappropriate use of static utility - inflexible & untestable!public class SpellChecker {    private static final Lexicon dictionary = ...;    private SpellChecker() {} // Noninstantiable    public static boolean isValid(String word) { ... }    public static List<String> suggestions(String typo) { ... }}
// Inappropriate use of singleton - inflexible & untestable!public class SpellChecker {    private final Lexicon dictionary = ...;    private SpellChecker(...) {}    public static INSTANCE = new SpellChecker(...);    public boolean isValid(String word) { ... }    public List<String> suggestions(String typo) { ... }}

这两种写法分别是工具类和单例,都假定使用同一个字典,实际情况是不同拼写检查程序依赖不同的字典。

你可能会想取消dictionary的final修饰,并让SpellChecker类添加更改dictionary的方法。但这种方法在并发环境下会出错。工具类和单例不适用于需要参数化依赖对象

参数化依赖对象的一种简单模式是,创建对象时将被依赖的对象传递给构造函数。这是依赖注入的一种形式。

// Dependency injection provides flexibility and testabilitypublic class SpellChecker {    private final Lexicon dictionary;    public SpellChecker(Lexicon dictionary) {        this.dictionary = Objects.requireNonNull(dictionary);    }    public boolean isValid(String word) { ... }    public List<String> suggestions(String typo) { ... }}

依赖注入适用于构造函数、静态工厂方法、建造者模式。

依赖注入的一个有用变体是将工厂传递给构造函数,这样可以反复创建被依赖的对象。Java8中引入的Supplier<T>非常适合作为工厂。下面是一个生产瓷砖的方法

Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

尽管依赖注入极大提高了灵活性和可测试性,但它可能会使大型项目变得混乱。通过使用依赖注入框架(如Spring、Dagger、Guice)可以消除这种混乱

6. 避免创建不必要的对象

复用对象可以加快程序运行速度,如果对象是不可变的,那么它总是可以被复用的。

一个不复用对象的极端例子如下

String s = new String("bikini"); // DON'T DO THIS!

该语句每次执行都会创建一个新的String实例。"bikini"本身就是一个String实例,改进的代码如下

String s = "bikini";

这个版本使用单个String实例,而不是每次执行时都会创建一个新的实例。此外,可以保证在同一虚拟机中运行的其它代码都可以复用该对象,只要它们都包含相同的字符串字面量

通常可以使用静态工厂方法来避免创建不必要的对象。例如,Boolean.valueOf(String) 比构造函数Boolean(String) 更可取,后者在Java9中被废弃了。

有些对象的创建代价很高,如果要重复使用,最好做缓存。例如,使用正则表达式确定字符串是否为有效的罗马数字

// Performance can be greatly improved!static boolean isRomanNumeral(String s) {    return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");}

这个方法的问题在于它依赖String.matches方法。虽然 String.matches 是检查字符串是否与正则表达式匹配的最简单方法,但它不适合要求高性能的情况下重复使用,因为它在内部为正则表达式创建了一个Pattern实例,并且只使用了一次就垃圾回收了,创建一个Pattern实例代价很大,因为它需要将正则表达式编译成有限的状态机

为了提高性能,将正则表达式显示编译成Pattern实例,作为类初始化的一部分,每次调用匹配的方法时都使用这个实例

// Reusing expensive object for improved performancepublic class RomanNumerals {    private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");    static boolean isRomanNumeral(String s) {        return ROMAN.matcher(s).matches();    }}

当对象是不可变的时候,我们很容易会想到复用。但是有些情况不那么容易想到复用。例如适配器模式中的适配器,因为适配器中没有它适配对象的状态,所以不需要创建多个适配器。

一个具体的例子是,KeySet是Map中的适配器,使得Map不仅能提供返回键值对的方法,也能返回所有键的Set,KeySet不需要重复创建,对Map的修改会同步到KeySet实例

7. 排除过时的对象引用

Java具体垃圾回收机制,会让程序员的工作轻松很多,但是并不意味着需要考虑内存管理,考虑以下简单的堆栈实现

import java.util.Arrays;import java.util.EmptyStackException;// Can you spot the "memory leak"?public class Stack {    private Object[] elements;    private int size = 0;    private static final int DEFAULT_INITIAL_CAPACITY = 16;    public Stack() {        elements = new Object[DEFAULT_INITIAL_CAPACITY];    }    public void push(Object e) {        ensureCapacity();        elements[size++] = e;    }    public Object pop() {        if (size == 0)            throw new EmptyStackException();        return elements[--size];    }    /**     * Ensure space for at least one more element, roughly     * doubling the capacity each time the array needs to grow.     */    private void ensureCapacity() {        if (elements.length == size)            elements = Arrays.copyOf(elements, 2 * size + 1);    }}

这段代码有一个潜在的内存泄露问题。当栈的长度增加,再收缩时,从栈中pop的对象不会被回收,因为引用仍然还在elements中。解决方法很简单,一旦pop就置空

public Object pop() {    if (size == 0)        throw new EmptyStackException();    Object result = elements[--size];    elements[size] = null; // Eliminate obsolete reference    return result;}

用null处理过时引用的另一个好处是,如果被意外引用的话会立刻抛NullPointerException

另外一个常见的内存泄漏是缓存。HashMap的key是对实际对象的强引用,不会被GC回收。WeakHashMap的key如果只有WeakHashMap本身使用,外部没有使用,那么会被GC回收

内存泄露第三种常见来源是监听器和其它回调。如果客户端注册了回调但是没有显式地取消它们,它们就会一直在内存中。确保回调被及时回收的一种方法是仅存储它们的弱引用,例如,将他它们作为键存储在WeakHashMap中

8. 避免使用终结器和清除器

如标题所说

9. 使用 try-with-resources 优于 try-finally

Java库中有许多必须通过调用close方法手动关闭的资源,比如InputStream、OutputStream和java.sql.Connection。

从历史上看,try-finally语句是确保正确关闭资源的最佳方法,即便出现异常或返回

// try-finally - No longer the best way to close resources!static String firstLineOfFile(String path) throws IOException {    BufferedReader br = new BufferedReader(new FileReader(path));    try {        return br.readLine();    } finally {        br.close();    }}

这种方式在资源变多时就很糟糕

// try-finally is ugly when used with more than one resource!static void copy(String src, String dst) throws IOException {    InputStream in = new FileInputStream(src);    try {        OutputStream out = new FileOutputStream(dst);    try {        byte[] buf = new byte[BUFFER_SIZE];        int n;        while ((n = in.read(buf)) >= 0)            out.write(buf, 0, n);    } finally {        out.close();        }    }    finally {        in.close();    }}

使用 try-finally 语句关闭资源的正确代码(如前两个代码示例所示)也有一个细微的缺陷。try 块和 finally 块中的代码都能够抛出异常。例如,在 firstLineOfFile 方法中,由于底层物理设备发生故障,对 readLine 的调用可能会抛出异常,而关闭的调用也可能出于同样的原因而失败。在这种情况下,第二个异常将完全覆盖第一个异常。异常堆栈跟踪中没有第一个异常的记录,这可能会使实际系统中的调试变得非常复杂(而这可能是希望出现的第一个异常,以便诊断问题)

Java7 引入 try-with-resources语句解决了这个问题。要使用这个结构,资源必须实现AutoCloseable接口,这个接口只有一个void close方法,下面是前两个例子的try-with-resources形式

// try-with-resources - the the best way to close resources!static String firstLineOfFile(String path) throws IOException {    try (BufferedReader br = new BufferedReader(new FileReader(path))) {        return br.readLine();    }}
// try-with-resources on multiple resources - short and sweetstatic void copy(String src, String dst) throws IOException {    try (InputStream in = new FileInputStream(src);OutputStream out = new FileOutputStream(dst)) {        byte[] buf = new byte[BUFFER_SIZE];        int n;        while ((n = in.read(buf)) >= 0)            out.write(buf, 0, n);    }}

try-with-resources为开发者提供了更好的异常排查方式。考虑firstLineOfFile方法,如果异常由readLine和不可见close抛出,那么后者异常会被抑制。被抑制的异常不会被抛弃,它们会被打印在堆栈中并标记被抑制。可以通过getSuppressed方法访问它们,该方法是Java7中添加到Throwable中的

像try-catch-finally一样,try-with-resources也可以写catch语句。下面是firstLineOfFile方法的一个版本,它不抛出异常,但如果无法打开文件或从中读取文件,会返回一个默认值

// try-with-resources with a catch clausestatic String firstLineOfFile(String path, String defaultVal) {    try (BufferedReader br = new BufferedReader(new FileReader(path))) {        return br.readLine();    } catch (IOException e) {        return defaultVal;    }}
]]>
+ + + + + <h2 id="第二章-创建和销毁对象"><a class="markdownIt-Anchor" href="#第二章-创建和销毁对象"></a> 第二章 创建和销毁对象</h2> +<h3 id="1-考虑用静态工厂方法代替构造函数"><a class="markdownIt- + + + + + + + + + + + +
+ + + 跨域问题 + + https://zunpan.github.io/2022/06/22/%E8%B7%A8%E5%9F%9F%E9%97%AE%E9%A2%98/ + 2022-06-22T09:12:37.000Z + 2023-09-24T04:27:40.285Z + + 同源策略

同源策略是浏览器的重要安全策略。大致来说,不同域(协议+域名/ip+端口)生成的cookie只能给这个域使用

跨域出现与解决

下面的演示是在hosts文件中添加以下配置

127.0.0.1   zhufeng-test.163.com

不添加的话,把zhufeng-test.163.com换成localhost或者127.0.0.1是一样的

假如我们只有后端,它的域是http://zhufeng-test.163.com:8080,它有一个get方法是setCookie,那么访问http://zhufeng-test.163.com:8080/setCookie,此时可以在Response Cookies中找到这个cookie,对这个域下的接口访问时会自动带上这个域所有可见的cookie(springboot开启allowCredentials,前端axios需要自己开启withCredentials: true)
图 2

图 3

图 1

现在我们有前端了,前后端分离运行,前端运行在http://zhufeng-test.163.com:8081

图 6

因为后端生成的cookie的domain是 zhufeng-test.163.com,所以浏览器访问相同域名的前端也可以读取到这个cookie。
但是前端不能访问后端的其它接口,因为它们端口不同,发生了跨域,浏览器不会带上cookie
图 7

给后端配置一下跨域,.allowedOrigins(“http://zhufeng-test.163.com:8081/”)表示允许前端http://zhufeng-test.163.com:8081访问后端,不开会返回状态码200的跨域错误,.allowCredentials(true)不开时前端访问后端不会带上后端域名可见的cookie

@Configurationpublic class CorsConfig implements WebMvcConfigurer {    @Override    public void addCorsMappings(CorsRegistry registry) {        //项目中的所有接口都支持跨域        registry.addMapping("/**")                // 所有地址都可以访问,也可以配置具体地址                // .allowedOrigins("http://zhufeng-test.163.com:8081/")                .allowedOriginPatterns("*://*:*/")                // "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"                .allowedMethods("*")                // 允许前端携带后端域名可见的cookie                .allowCredentials(true)                .maxAge(3600);    }}

图 8

]]>
+ + + + + <h2 id="同源策略"><a class="markdownIt-Anchor" href="#同源策略"></a> 同源策略</h2> +<p>同源策略是浏览器的重要安全策略。大致来说,不同域(协议+域名/ip+端口)生成的cookie只能给这个域使用</p> +<h2 id= + + + + + + + + + + + +
+ + + CryptographyⅠ笔记 + + https://zunpan.github.io/2022/06/04/Cryptography%E2%85%A0%E7%AC%94%E8%AE%B0/ + 2022-06-04T06:51:30.000Z + 2023-09-24T04:27:40.272Z + + 斯坦福教授Dan Boneh的密码学课程《Cryptography》
课程链接:https://www.bilibili.com/video/BV1Ht411w7Re
讲义:https://www.cs.virginia.edu/~evans/courses/crypto-notes.pdf

目标:课程结束后可以推导加密机制的安全性,可以破解不安全的加密机制

绪论

密码学用途

  1. 安全通信,例如https,Bluetooth
  2. 加密磁盘文件,例如EFS
  3. 内容保护,例如DVD使用CSS
  4. 用户认证

高阶用途:

  1. 外包计算(同态加密)
  2. 安全多方计算
  3. 零知识证明

安全通信

在web服务中使用https协议进行通信,起到安全通信作用的是TLS协议,TLS协议主要包括两部分:

  1. 握手协议:使用公钥密码体制建立共享密钥
  2. Record层:使用共享密钥传输数据,保证数据的机密性和完整性
    图 1

加密磁盘文件

加密磁盘文件从哲学角度来看就是今天的Alice和明天的Alice进行安全通信
图 2

对称加密

Alice和Bob双方共享密钥k,攻击者不知道密钥k,他们使用两个算法进行通信,分别是加密算法E,解密算法D。加密算法以原信息和密钥为输入产生相应密文;解密算法正好相反,以密文和密钥为输入,输出原信息。加密解密算法是公开的,只有密钥k是保密的。我们应当使用公开的算法,因为它的安全性经过业内人士的审查
图 3

密钥使用次数

可以根据密钥使用次数分为一次使用的密钥,多次使用的密钥
一次使用的密钥只用来加密一个信息,例如加密邮件,为每一封邮件都生成一个新的密钥。
多次使用的密钥可以用来加密多个消息,例如用同一密钥加密文件系统的许多文件,多次使用的密钥需要更多机制来确保加密系统是安全的
图 4

一些结论

任何依赖可信第三方的计算都可以不借助可信第三方完成
图 5

密码算法提出三步骤

  1. 明确描述威胁模型。例如数字签名算法中,攻击者如何攻击数字签名?伪造签名目的何在?
  2. 提出算法构成
  3. 证明在该威胁模型下,攻击者破解了算法等同于破解了根本性难题。意思就是算法的安全性由根本性难题保证,例如RSA依赖大整数质因数分解困难问题

密码历史

对称密码历史

图 6

替换式密码

替换式密码的密钥是一张替换表
图 1

例如:凯撒密码

凯撒密码是没有密钥或者只有一个固定密钥的替换式密码
图 3

假设替换式密码的密钥只能使用26个英文字母,它的密钥空间有多大?是26!26!,约等于2882^{88},也就是说可以用88位比特表示密钥,这个密钥空间是足够大的,但是这个加密算法不安全
图 4

如何破解这个加密算法?

  1. 使用字母频率来破解。在英文文献中,出现频次最高的字母是e,那么我们统计密文中出现频次最高的那个字母,例如c,那么这个替换式密码的密钥k中很可能包含了e->c;同样的道理,出现频次第二高的是t,第三高的是a,分别找到密文中对应的字母进行还原
  2. 使用字母配对(组合)。在英语中,最常见的二元配对有“he”,“an”,“in”,“th”。第一种方法还原出了eta,根据配对规则可以还原配对中的另一个字母。

这种攻击方式叫唯密文攻击(CT only attack),替换式密码抵御不了这种攻击

Vigener密码

Vigener密码的密钥是一个单词,加密算法是将密钥重复直到和明文一样长,与明文逐位求和模26得到密文,解密算法是对密文逐位减去密钥
图 5

如何破解Vigener密码?

首先假设攻击者已知密钥长度(未知也不影响破解,枚举长度即可),例如密钥长度为6,那么攻击者可以将密文按照每6个字母一组进行分组。然后看每组的第一个字母,他们都是由同一个字母加密的,例如上图中k的字母C。我们同样可以使用唯密文攻击,在每组的第一个字母中找到出现频次最高的字母,它们对应的明文位为E,然后对应的密钥为为密文位-E = 密钥位。同样的方法对密钥第2位直到最后一位执行。这种破解方法的实质是多次使用唯密文攻击
图 6

轮轴机

最早的轮轴机:Hebern machine(一个轮轴)

本质上是一个替换式密码。按下A,假如A加密成了T,此时轮轴机转动一格,再次按下A,A就加密成了S。轮轴机一经提出很快就被破解了,同样是唯密文攻击。
图 7

最著名的轮轴机:Enigma(3-5轮轴)
图 8

数字时代的密码

1974:DES(keys = 2562^{56}, block size = 64 bits)

Today: AES(2001), Salsa20(2008) (and others)

离散概率

有限集合与概率分布

图 9

事件

事件是有限集合的子集

图 12

事件的并集发生的概率存在上界
图 13

随机变量

随机变量是一个函数 X:U(有限集合)->V(某个集合)
集合V是随机变量取值的地方。例如定义随机变量X:{0,1}n{0,1}X:\{0,1\}^n\longrightarrow \{0,1\};X(y)=lsb(y){0,1}X(y)=lsb(y) \in \{0,1\}
这个随机变量将n位长度的字符串集合映射成了只有0和1的两个元素的集合,具体做法是取最低位。任意一个n位长度的字符串会被映射出0或者1
图 14

均匀随机变量

图 15
图 16

随机化算法

随机化算法相比于确定的算法,在原像中加入了随机因子,所以对同一条消息进行随机化,结果一般是不同的
图 17

独立性

图 18

异或

异或就是逐位模2和
图 20

异或在密码学中非常重要,在一个有限集合上有一个随机变量Y,有一个独立的均匀随机变量X,那么Z:=YXZ:=Y\bigoplus X 仍然是这个集合上的均匀随机变量
图 21

生日悖论

图 22

r指的是随机变量的值

独立同分布举例:假设U = {00,01,10,11}表示抛硬币两次的结果,令随机变量XU{0,1}X:U\rightarrow\{0,1\}表示第一次抛硬币的结果,YU{0,1}Y:U\rightarrow\{0,1\}表示第二次抛硬币的结果。X和Y是相互独立的且概率一样,所以这两个随机变量是独立同分布的

流密码

流密码是一种对称密码。
对称密码的严格定义:定义在K,M,C\mathcal{K,M,C}上的一对“有效”加解密算法(E,D)(E,D)
K\mathcal{K}指密钥空间,M\mathcal{M}指明文空间,E\mathcal{E}指密文空间
E:K×MCE: \mathcal{K\times M\rightarrow C}
D:K×CMD:\mathcal{K\times C\rightarrow M}
mM,kK:D(k,E(k,m))=m\forall m \in \mathcal{M}, k \in \mathcal{K}: D(k, E(k, m))=m
"有效"这个词在理论密码学家看来,时间复杂度是多项式时间内的就是有效的,真正运行时间取决于输入规模。在应用密码学家看来,可能是加解密1GB数据要在10s内就算有效。
图 23
特别注意,加密算法是随机化算法,解密算法是确定性的算法

一次性密码本

一次性密码本的明文空间、密文空间、密钥空间均相同
图 24
一次性密码本的加密算法是将明文与密钥做异或运算,解密也是将密文与密钥做异或运算,根据异或运算的结合律可以知道加解密算法是正确的
图 25

如何证明这个密码的安全性?

信息论的创始人香农提出一个观点:不能从密文得出关于明文的任何信息
图 26
攻击者截获一段密文,如果满足下面的等式,即不同明文经过同一个密钥加密后等于这个密文的概率是相同的,那么攻击者就无法得知真正的明文,这种密码是完美保密性的
图 28

一次性密码本是完美保密性的
图 29
完美保密性只是意味着没有唯密文攻击,并不意味着在实际使用中是安全的
图 30

香农在给出完美保密性的证明后又给出了一个定理,想要完美保密性,密钥长度要大于等于明文长度,所以完美保密性的密码是不实用的
图 31

流密码(Stream ciphers)

流密码是使用的一次性密码本,它的思想是使用伪随机密钥代替随机密钥

伪随机数生成器是一个函数,它可以将s位的01比特串扩展成n位的01比特串,n>>s。注意,伪随机数生成器算法是不具备随机性的,具备随机性的是种子
图 32

流密码使用伪随机数生成器(PRG),将密钥当做种子,生成真正用于加密的比特串
图 33
因为密钥长度远小于明文长度,所以流密码并不是完美保密性的
图 34
流密码的安全性不同于完美保密性,它需要另外一种安全性定义,这种安全性依赖具体的伪随机数生成器
图 36
安全的伪随机数生成器必须是不可预测的。
假设伪随机数生成器是可预测的,那么存在某种算法可以根据PRG的前i位推测出后面的n-i位。在SMTP协议中,明文头是“from:” , 攻击者可以用这段已知的明文与截获的密文做异或得出PRG输出的前i位,再根据预测算法得出完整的密钥,再与密文异或得出明文
图 37

严格定义PRG是可预测的:
存在“有效”算法A,对PRG的前i位做计算,输出的值与PRG的第i+1位相同的概率大于1/2+ϵ,(ϵ1/230)1/2+\epsilon, (\epsilon \ge 1/2^{30}), ϵ\epsilon大于1/2301/2^{30}时是不可忽略的
图 38
图 39

不安全的PRG例子

  • 线性同余法的PRG
    图 40

可忽略和不可忽略
图 42
图 43

针对一次性密码本和流密码的攻击

两次密码本攻击

流密码的密钥一旦使用两次就不安全
图 44
图 45
图 46

WEP还有一个问题就是它的PRG使用的是RC4,它的不可预测性是较弱的,存在有效算法可以从四万帧中恢复PRG的输出
图 1
图 2

完整性攻击

一次性密码本和流密码都只保护数据的机密性,但不保证完整性,攻击者可以修改将密文与攻击者的置换密钥做异或从而定向影响明文,这种攻击无法被检测出来
图 3
图 4

流密码实际应用

图 5
图 6

现代流密码:eStream

eStream支持5种PRG,这里讲其中一种,这种PRG的输入除了种子(密钥)还有一个随机数,好处是不用每次更换密钥,因为输入还包括随机数,整体是唯一的
图 7

eStream中同时支持软硬件的流密码Salsa20

图中的 || 并不是简单的拼接。这个PRG首先构造一个64kb的字符串,里面包含k,r,i(从0开始),通过多次一一映射h,最后与原字符串进行加法(不是异或)得出输出
图 8
图 9

PRG的安全定义

PRG的输出与均匀随机变量的输出不可区分
图 10

统计测试

为了定义不可区分,首先引入统计测试,统计测试是一个算法,输入值是“随机数”,输出0表示不是真随机数,1表示是真随机数
图 11

上图的例子1,这个统计测试输出1的情况当且仅当随机数中0和1的个数差小于等于随机数的长度开根号乘以10

统计测试算法有好有坏,可能并不随机的字符串也认为是随机的。所以我们需要评估统计测试算法的好坏

我们定义一个变量叫做优势,优势是统计测试算法相对于伪随机数生成器的,优势越接近1,统计测试算法越能区分伪随机数和真随机数

图 13
图 14

PRG的密码学安全定义

PRG是安全的当且仅当不存在有效的统计算法,它的优势是不可忽略的。即所有统计算法都认为PRG的输出是真随机数

但是,我们不能构造一个PRG并证明PRG是安全的,即不存在有效的统计算法。但是我们还是有大量的PRG候选方案

图 15

我们可以证明当PRG是可预测时,PRG是不安全的
图 16
当存在一个好的预测算法和好的统计测试算法,如下图。那么统计测试算法可以以一个不可忽略的 ϵ\epsilon 分辨出伪随机数和真随机数
图 17
姚期智证明了上面命题的逆命题也成立,即当PRG是不可预测时,PRG是安全的
图 18

不可区分的更通用的定义如下
图 19

语义安全

什么是安全的密码?

  1. 攻击者不能从密文中恢复密钥
  2. 攻击者不能从密文中恢复明文

香农认为不能从密文中获得任何关于明文的信息才是安全的密码

图 20

香农的完美保密性定义约束太强,可以用计算不可区分代替
图 21

一次性密码本的语义安全

定义语义安全的的方式是通过两个实验,攻击者发送两个明文信息,挑战者(应该是被挑战者)随机选取密钥,做两次实验,第一次实验加密第一个信息,第二次实验加密第二个信息,攻击者判断密文对应的明文是哪个
图 22
上图定义了语义安全的优势,等于实验0中攻击者输出1的概率和实验1中输出1的概率之差的绝对值。简单理解一下,假如攻击者不能区分两次实验,那么实验0和实验1的输出1的概率是一样的,那么攻击者的优势为0,不能区分两次实验,意味着加密算法是语义安全的;如果攻击者能区分实验0和实验1,那么概率差不可忽略,攻击者有一定的优势区分两次实验
图 23
图 24

事实上,一次性密码本不仅是语义安全的,而且是完美保密性的
图 25

安全的PRG可以构成语义安全的流密码

图 26

我们做两次实验证明流密码的语义安全,第一次使用伪随机数生成器,第二次使用真随机数
图 27
图 28
图 29

分组密码

分组密码也属于对称密码,将明文分解成固定大小的分组,使用密钥加密成同样大小的密文
图 30
分组密码的加密过程是将密钥扩展成多个,使用轮函数多次计算分组得出密文
图 31
图 32

PRPs和PRFs

K表示密钥空间,X表示明文空间,Y表示密文空间
给定密钥后,伪随机置换的加密函数是一个一一映射,也就意味着存在解密函数。伪随机置换和分组密码十分相似,有时候会混用术语
图 33
图 34

安全的PRFs

Funs[X,Y]表示所有从明文空间到密文空间的真随机函数的集合,易知这个集合大小等于明文空间大小的密文空间大小次,而伪随机函数的集合大小由密钥决定,一个密钥决定了一个伪随机函数,一个安全的PRF与真随机函数不可区分
图 35
图 36
图 37
上图的G问题在于x=0时,输出固定了,攻击者可以通过x=0时的输出是否为0来判断他在和真随机函数交互还是伪随机函数,因为x=0,输出为0的概率实在太低了,等于密文空间的倒数
图 38
可以使用安全的PRF来构造安全的PRG

DES

DES的轮(回合)函数使用的是Feistel网络,核心思想是每个分组2n bits,右边n bits原封不动变成下一层分组左边n bits,左边n bits经过伪随机函数转换再和右边n bits异或变成下一层右边n bits
图 39
易知这个网络是可逆的,注意,不要求伪随机函数是可逆的
图 40
图 41

定理:如果伪随机函数使用的密钥是相互独立的,那么Feistel网络是一个安全的PRP
图 42
回合函数f由F根据回合密钥推导出来,回合密钥由主密钥推导得来,IP和IP逆是伪随机置换,和DES安全性无关,仅仅是标准要求
图 43
图 44
图 45

线性函数:函数可以表示成矩阵乘以入参
图 46
如果所有的置换盒子都是线性的,那么整个DES就是线性的,因为只有DES算法中只有置换盒子可能是非线性的,其它就是异或、位移等线性运算。如果是线性DES,那么存在一个矩阵B,DES可以写成B乘以一个包含明文和回合密钥的向量
图 47
图 48

针对DES的攻击

密钥穷举攻击

给定一些明文密文对,找到一个密钥使得明文密文配对,这个密文极大概率是唯一的
图 49
图 50

为了抵抗穷举攻击,衍生出了3DES
图 51

2DES存在安全性问题
图 52

针对分组密码的攻击

旁道攻击

通过测试加解密的时间或者功耗来推测密钥

错误攻击

通过外部手段影响加解密硬件,比如提高时钟频率、加热芯片,使得加密的最后一回合发生错误,根据错误信息可以推测出密钥
这两种攻击需要先窃取到硬件,比如上图的IC卡

图 53

线性和差分攻击

同样是需要给定明文密文对,推测密钥,比穷举攻击效率更高

加密算法中使用了线性函数导致下面等式以一个不可忽略的概率成立
图 54
图 55
图中的MAJ表示majority
图 56

量子攻击

图 58
图 59

AES

AES基于代换置换网络构建,和Feistel最大的区别在于,在这个网络的每一回合函数会影响每一位bit
图 60
图 61
图 62
图 63
图 64
图 65
图 66

使用PRGs构造分组密码

分组密码实质是PRP,首先考虑能不能使用PRG构造PRF
图 67
图 68
图 69
图 70
图 71
使用安全的PRG可以构造一个安全的PRF,但是并不实用
图 72
有了安全的PRF,我们可以使用Luby-Rackoff定理转换成PRP,因此可以使用PRG来构造分组密码,但是不如AES启发性PRF实用

使用分组密码

图 1
图 2
很显然,真随机置换空间和伪随机置换空间大小一样,所以它是安全的PRP
图 3
这个PRP不是安全的PRF,因为明文空间太小了
图 4

使用一次性密钥的分组密码

图 6
图 7

ECB的问题在于相同的明文会加密成相同的密文,攻击者可能不知道明文内容,但也会从中学到明文的一些信息
图 8
攻击者来挑战算法,本来不应该知道两张加密图片的区别,但是ECB将头发加密成了很多1,头发又重复出现,这样密文就会出现很多1,攻击者就能根据这个区别分辨两张图片

图 9

ECB被用来加密长于一个分组的消息时不是语义安全的
图 1

安全的电子密码本是为每个分组生成一个随机密钥进行加密,类似于AES的密钥扩展
图 3
图 4

使用密钥多次利用的分组密码

图 5

密钥多次利用的分组密码的选择明文攻击就是进行多次的语义安全实验(CPA安全)
图 6
图 7

确定的加密对于选择明文攻击不可能是语义安全的,所以多次使用一个密钥加密时,相同的明文,应该产生不同的输出,有两种方法。

第一种:随机化算法
图 8
图 9

第二种:基于新鲜值的加密

新鲜值不必随机但是不能重复
图 10
随机化的新鲜值和上面的随机化算法是一样的

基于新鲜值的加密的选择明文攻击下的安全性
图 1
图 2

密钥多次利用的运行方式(CBC)

CBC:密码分组链接模式
图 3
图 4
图 5
L是加密的明文长度,单位是分组。q是在CPA攻击下,攻击者获得的密文数,现实意义是使用某个密钥加密的明文数量
图 6
图 8
图 9
图 10
图 11

密钥多次利用的运行方式(CTR)

CTR:计数器模式

CTR不使用分组密码,使用PRF足够
图 12
图 13
图 14
图 15
图 16
图 17

信息完整性

本节先考虑信息完整性,不考虑机密性。信息完整可以保证公开信息没有被篡改,例如广告投放商不在乎广告的机密性,但在乎广告有没有被篡改

MACs

图 9

图 10

CRC是循环冗余校验算法,为检测信息中的随机发生的错误而设计,并非针对恶意错误

安全的MACs

图 11
图 12
图 13

图 15

图 16

MAC可以帮助抵御数据篡改,但是无法抵御认证消息的交换

构造安全的MAC

可以用安全的PRF来构造安全的MAC
图 17

但是,PRF的输出不能太短,不然攻击者能以一个不可忽略的概率猜出消息认证码

图 18

图 19

不等式右边 1/|Y| 是因为攻击者可以猜

图 20

PRF的输出不能太短,同时为了增大输入空间,需要有一些新的构造,将小输入空间的PRF转换成大输入空间的PRF
图 21

图 22

如果PRF是安全的,那么截断PRF的输出,依然是安全的,当然,如果要用PRF构造MAC,不能截到太短

CBC-MAC和NMAC

图 23

n是底层PRF的分组大小

图 24

L是分组大小

图 25

这两种构造的最后一步都至关重要,没有最后一步,攻击者可以实施存在性伪造(扩展攻击)。
第二种构造的原因很简单,攻击者询问m的函数结果,将结果与w分别作为函数的密钥和明文(F公开),计算函数结果

图 26
图 27

第一种构造的原因
图 29

图 28

图 30
图 31
图 32
图 33
图 34

MAC padding

当数据的长度不是分组长度的倍数时,需要填充数据

全部补0会有出现存在性伪造
图 35

填充函数必须是一一映射的
图 36

ISO的这种填充方法,不管是不是分组长度的倍数都要进行填充

图 37

并行的MAC

CBC-MAC和NMAC将一个处理短信息的PRF转换成一个处理长信息的PRF,这两种算法是串行的

P是某个有限域上的乘法
图 38
图 39
图 40

一次性MAC

图 41
图 42
图 43
图 44

HMAC(略)

抗碰撞章节细说

抗碰撞

抗碰撞在信息完整性中扮演着重要角色。我们说MAC系统是安全的,如果它在选择信息攻击下,是不可被存在性伪造的。前面4种MAC构造是通过PRF或随机数来构造的,现在通过抗碰撞的哈希函数来构造MAC
图 45

抗碰撞:没有有效算法A,能以一个不可忽略的概率找到hash函数的碰撞值

图 46

可以用抗碰撞哈希函数和处理短信息的安全MAC组合成处理长信息的安全MAC
图 47
图 48

只用抗碰撞哈希函数也可以构造安全的MAC,而且不像之前的MAC需要密钥,但是需要一个只读空间用来存信息的hash值。这种方式非常流行
图 49

针对抗碰撞哈希函数的通用攻击(生日攻击)

针对分组密码的通用攻击是穷举攻击,抵御穷举攻击的方法是增大密钥空间;为了抵御生日攻击,哈希函数的输出也必须大于某个下界

下面这种攻击算法通常几轮就能找到碰撞值
图 50

生日悖论的证明

下面的证明在非均匀分布时,n的下界更低
图 51

通用攻击

图 52
图 53

使用Merkle-Damgard机制组建抗碰撞的哈希函数

图 54

下面的IV是永远固定的,写在代码和标准里的值,填充函数在信息长度是分组长度倍数的时候也会填充一个哑分组
图 55

这种机制流行的原因是只要小hash函数(即上面的压缩函数)是抗碰撞的,那么大hash函数也是抗碰撞的

构建抗碰撞的压缩函数

使用分组密码来构建压缩函数,将信息作为密钥。SHA函数都使用了Davies-Mayer压缩函数
图 56
图 57
图 58

另外一类压缩函数是由数论里的困难问题构建的,这类压缩函数的抗碰撞性规约于数论难题,也就是说破解了压缩函数的抗碰撞性也就破解了数论难题。但是这类压缩函数很少使用,因为分组密码更快
图 60

SHA256

使用了Merkle-Damgard机制和Davies-Mayer压缩函数,底层分组密码用的是SHACAL-2
图 59

HMAC

图 61
图 62
图 63
图 64
图 65

针对MAC验证时的计时攻击

图 66
图 67
图 68
图 69

认证加密

到目前为止,我们了解了机密性和信息完整性, 我们将构建同时满足这两种性质的密码
图 70

机密性: 在选择明文攻击下满足语义安全
机密性只能抵抗攻击者窃听而不能抵抗篡改数据包

客户端加密数据后通过网络发送给服务器, 服务器解密后分发至对应端口
图 71

如果没有保证完整性, 那么攻击者可以拦截客户端的数据包, 把端口改成自己能控制的服务器的端口
图 72

可以抵抗CPA攻击的CBC分组密码不能抵抗篡改数据包, 攻击者可以简单地修改IV从而修改加密后的端口
图 73

攻击者甚至不用进入服务器, 直接利用网络来攻击。在CTR模式下,攻击者截获数据包,将加密的校验和与t异或,将加密的数据与s异或。CTR模式的特点是对密文异或,解密后等于对明文异或。攻击者重复很多次攻击,直到得到足够数量的合法的t、s对,从而恢复数据D(插值法?)
图 1

这种攻击叫做选择密文攻击。攻击者提交他选择的密文,是由他想解密的密文所推出的,然后看服务器响应,攻击者可以从中学到明文的一些信息。重复这个操作,用许多不同t、s值,攻击者可以还原明文

选择明文攻击下的安全不能保证主动攻击(前面两种攻击)下的安全
如果要保证信息完整性但不需要机密性,使用MAC
如果同时保证信息完整性和机密性,使用认证加密模式
图 2

认证加密定义

目标:提供选择明文攻击下的语义安全和密文完整性
图 3

密文完整性:攻击者不能造出合法的密文
图 4

CBC是选择明文攻击(CPA)下的安全密码,它的解密算法从不输出bottom符号,所以它不能直接被用作认证加密
图 5

认证加密不能抵抗重放攻击
图 6
图 7

图 8

选择密文攻击下的安全

攻击者的能力是既能选择明文攻击也能选择密文攻击,也就是说既能拿到想要的明文的加密结果,也能拿到想要的密文的解密结果
图 9

攻击者选择的密文不能是CPA的返回
图 10

CBC密码不是选择密文安全的,因为在选择密文时,通过对IV异或修改CPA的返回,同时由于CBC的特性,CCA的返回时对明文的异或,这样攻击者可以以1的优势赢下语义安全实验
图 11

如果密码能够提供认证加密,那么它就是选择密文安全的
图 12
图 13

所以认证加密能够选择密文攻击,但是不能防止重放攻击和旁道攻击
图 1

认证加密直到2000年才被正式提出,在这之前,已经有CPA安全的密码和安全的MAC,当时的工程师想将两者组合,但不是所有的组合可以作为认证加密
图 1

图 2

图 3

SSH的MAC签名算法的输出会泄漏明文中的一些位;
SSL的加密和MAC算法之间会有一些不好的互动导致选择密文攻击。IPsec无论怎么组合CPA安全的密码和安全的MAC都可以作为认证加密。SSL的特点是MAC-than-ENC,IPsec的特点是ENC-than-MAC
图 4

这几种认证加密都支持AEAD(认证加密与关联数据,例如ip报文的报文头是关联数据不加密,报文体用加密,整个报文需要认证)。MAC对整个报文使用,加密只对报文体使用

图 5

aad是需要认证、但不需要加密的相关数据,data是需要认证和加密的数据
图 6

图 7

直接从PRP构造认证加密

图 8

OCB比前面几种认证加密快的多,但没有被广泛使用因为各种各样的专利
图 10

认证加密的例子

图 11
图 12
图 13
图 15

图 16
图 17

针对认证加密的攻击

图 18
图 19
图 20
图 21
图 22
图 23
图 24
图 25
图 26
图 27

图 28
图 29
图 30

零碎

本章讲对称密码的一些零碎

密钥推导

KDF:key drivation function
源密钥由硬件随机数生成器生成或者密钥交换协议生成
图 31

CTX:参数上下文,每个进程的ctx不同
当源密钥服从均匀分布,我们使用PRF来作为密钥推导函数(其实就是用PRF作为伪随机数生成器)
图 32
图 33

当源密钥不服从均匀分布,那么伪随机函数的输出看起来就不随机了,源密钥不服从均匀分布的原因可能是密钥交换协议的密钥空间的子集是均匀分布,或者伪随机数生成器有偏差
图 34

构建KDF的机制

先提取再扩展

因为源密钥可能不是均匀的,我们使用一个提取器和一个随机选择的固定的但可以不保密的盐值将源密钥转换为服从均匀分布的密钥k,然后再使用PRF扩展密钥
图 35

HMAC既用于PRF进行扩展,又用于提取器
图 36

基于密码的KDF

PBKDF通过多次迭代哈希函数推导密钥
图 37

确定性加密

确定性加密总是把给定明文映射到同一个密文。

为什么需要确定性加密?假设有个加密数据库和服务器,服务器用k1加密索引,用k2加密数据,如果加密是确定的,服务器请求数据时可以直接使用加密后的索引作为查询条件请求数据
图 38

确定性加密有致命缺点就是不能抵御选择明文攻击,攻击者看到相同的密文就知道他们的明文是相同的
图 39
图 40

解决方法是不要用同一个密钥加密同一个消息两次,要么密钥从一个很大空间随机选择,要么明文就是唯一的,比如说用户id
图 41

确定性加密的CPA安全

在标准的选择明文攻击实验基础上,Chal不会给相同的m0加密,不会给相同的m1加密。注意上面的图40不是标准的确定性加密的CPA实验,因为攻击者两次查询的m0都是同一个
图 42

一个常见的错误是,固定IV的CBC不是确定性CPA安全的,0n1n0^n1^n表示消息有两个分组,第一个分组全0,第二个分组全1
图 1

固定IV的CTR也是不安全的
图 2

可以抵御确定性CPA的确定性加密

确定性加密是需要的,但是不能抵御选择明文攻击,因为攻击者看到两个相同的密文就知道了对应的明文是一样的。我们对确定性加密降低选择明文攻击的能力,加密者不使用一个密钥多次加密同样的明文,这样叫确定性CPA。
图 3

构造1:合成的IV(SIV)

CPA安全的密码会有一个随机值,我们用PRF生成这个随机值
图 4

SIV天然提供密文完整性,不需要MAC就能作为DAE(确定性认证加密),例如SIV-CTR
图 5
图 6

当需要确定性加密,特别是明文很长时,适合用SIV,如果明文很短,比如说少于16个字节,可以用构造2

构造2:仅仅使用一个PRP

实验0,攻击者看到q个随机值,实验1中,攻击者也看到q个随机值,两次实验的结果的概率分布是一样的,攻击者无法区分。这种构造不能保证密文完整性。同时只能加密16个字节
图 7

我们先考虑如何将PRP扩展成一个大的PRP
EME有两个密钥K,L,L是由K推出的。先为每个分组用L推导出一个密码本作异或,然后用PRP加密得到PPP,将所有PPP异或得到MP,再用PRP加密MP得到MC。然后计算MP异或MC,得到另外一个密钥M用于推导更多密码本,分别对PPP异或得到CCC,然后把所有这些CCC异或得到CCCO,再用PRP加密再异或密码本
图 1

现在考虑增加完整性
图 2
图 3

微调加密(Tweakable encryption)

先以硬盘加密问题引入微调加密,
硬盘扇区大小是固定的,明文和密文空间必须一致,我们最多可以使用确定性加密,因为随机性加密需要额外空间来放随机数,完整性需要额外空间放认证码
定理:如果确定性CPA安全的密码的明文空间和密文空间一样,那么这个密码一定是个PRP
图 4
图 5
图 6
图 7

这个微调分组密码的安全实验与常规的分组密码安全实验区别在于,在常规分组密码中,攻击者只能与一个置换进行互动,目标是分辨自己在和伪随机置换交互还是在和一个真随机置换交互。而在微调分组密码的安全实验中,攻击者与|T|个随机置换交互,目标是区分这|T|个随机置换是真是伪
图 8
图 1
图 2
图 3
图 4
图 5

保格式加密(Format Preserving encryption)

pos机刷卡时,我们希望卡号只在终端和银行可见,但是中间又有些服务商也想得到"卡号",我们可以用将卡号加密成卡号格式的密文。
图 6
图 7

我们截断使用PRF,明文后面补0(例如AES就补到128位),密文截断,然后带入Luby-Rackoff构造PRP
图 9
图 10
图 11

密钥交换

现在我们知道两个用户可以通过共享一个密钥来保护通信数据,问题是,这两个用户如何产生共享密钥,这个问题将把我们带入公钥密码的世界。我们先看一些玩具性质的密钥交换协议。
图 12
图 13
图 14
图 17
图 18

能否设计出可以抵御窃听和主动攻击的没有可信第三方的密钥交换协议?可以的,这就是公钥密钥的出发点
图 19

不需要TTP的密钥交换

首先考虑攻击者只能窃听不能篡改消息,能不能只使用对称密码体系的算法来实现不需要TTP的密钥交换?
图 20

可以的,首先给出puzzle定义:需要花一些功夫解决的问题。例如已经给出AES密钥的前96位,明文固定,那么枚举2322^{32}个可能的后32位密钥可以找到能正确解密
图 21

Alice准备2322^{32}个puzzle,全部发给Bob,Bob选择一个然后开始枚举,只要解密出的原文开头包含"Puzzle", 对应的k作为共享密钥,x发送给Alice,Alice就知道Bob选择了哪个
图 22

这个协议不实用但是有一个很好的想法,参与者花费线性的时间,而攻击者必须花费平方的时间,当攻击者想破解这个协议,有一个“平方鸿沟”横亘在参与者与攻击者的工作之间。
只用对称密码体系,我们不能建立一个更大的“鸿沟”
图 23

我们需要具备非常特殊性质的函数,为了构建这些函数,我们必须依赖某些代数

Diffie-Hellman协议

这是第一个实用的密钥交换机制。
同样,我们考虑攻击者只能窃听不能篡改。我们尝试建立参与者与攻击者之间的指数级鸿沟

Diffie-Hellman协议开创了密码学的新纪元,现在不仅仅是关于开发分组密码,而且是关于设计基于代数的协议
图 24
图 25

下面有张表是不同密钥长度的分组密码安全性等价于对应模数的DH函数安全性,如果用椭圆曲线,模数可以更小
图 26
图 1

上面的协议当存在主动攻击(中间人攻击)时就不安全
图 2

事实上,上面的协议也可以改成无交互的
图 3

一个开放的问题,两个参与方的密钥交换使用DH即可,三个参与方的密钥交换使用Joux提出的方法,四个及以上还没有有效方法
图 4

公钥加密

这是另外一种密钥交换的方法
图 5
图 6
图 7

在公钥加密中,没有必要赋予攻击者实施选择明文攻击的能力。因为在对称密钥系统中,攻击者必须请求他选择的明文的加密,而在公钥系统中,攻击者拥有公钥,所以他可以自己加密任何他想加密的明文,他不需要Chal的帮助来计算他选择的明文的加密。因此在公钥的设定中,选择明文攻击是与生俱来的,没有理由给攻击者多余的能力去实施选择明文攻击(公钥加密也是随机性的,每次密文不同)
图 8
图 9

可以抵抗窃听但也不能抵抗中间人攻击
图 10
图 11

图 12

数论简介

图 13
图 14
图 15
图 16
图 17
图 19
图 20

扩展欧几里得算法是已知最有效的求元素模逆的方法(也给了我们求模线性方程的方法)
图 21

注意这里是n,不是N,n=logN
图 22

费马小定理和欧拉定理

费马小定理给了我们另一个计算模质数逆的方法,但是与扩展欧几里得算法有两个不足,首先它只能用在质数模上,其次算法效率更低
图 24

我们可以用费马小定理以极大概率生成一个随机质数(期望是几百次迭代),这是一个简单但不是最好的方法
图 25

欧拉证明了ZpZ_p^*是一个循环群(p是素数),也就是g(Zp)\exist g\in (Z_p)^* 使得 $ {1,g,g2,g3,…,g{p-2}}=(Z_p)*$
图 26

有限子群的阶必然整除有限群的阶
图 27

欧拉定理,费马小定理的直接推广,适用于合数
图 28

模高次方程

图 29
图 30
图 31

0也是二次剩余,所以有(p-1)/2+1
图 32
图 33

《信息安全数学基础》P146,证明当p是形如4k+3的素数时,解的形式如下。这里Dan讲了p不是这种形式的素数时仍然是有有效的随机算法来求解
图 34
图 35

当模数是合数时且指数大于1时,同余式的解并不好找
图 36

一些算法

分组用32位表示是为了乘法不溢出
图 37
图 38
图 39
图 40

指数运算非常慢
图 41

模运算的一些难题

图 42

质数模的难题

图 43
图 44
图 45
图 46

合数模的难题

Z(2)(n)Z_{(2)}(n)表示两个位数相同的质数乘积的集合
图 47
图 48

基于陷门置换的公钥加密

图 1
公钥密码两个作用,一是会话建立(即对称密钥交换),二是非交互式应用
图 2
图 3
图 4
图 5
图 6
图 7

选择密文攻击(CCA)下的安全,有时可以缩写成选择密文攻击下的不可区分性(IND-CCA)

当攻击者可以篡改密文时,将以优势1赢下这个CCA游戏
图 8

构建CCA安全的公钥加密系统

陷门函数特点是单向的,只有私钥持有人才能做逆向计算
图 9
图 10
图 11

单向陷门函数只加密一个随机值,随机值用来生成对称密钥,单向陷门函数的私钥持有人可以恢复随机值进而恢复密钥
图 12
图 13

不能直接用单向陷门函数加解密明文,因为算法是确定的,就不可能是语义安全的,也会存在许多类型的攻击
图 14

构建一个陷门函数

本节构建一个经典的陷门函数叫做RSA
图 1

随机选取ZNZ_N中的随机元素,这个元素很可能也在ZNZ_N^*中,即该元素很可能是可逆的
图 2
图 1
图 2
(虽然x大概率是可逆的,但是如果不可逆怎么办?)
图 3

单向陷门函数是安全的,对称密码可以提供认证加密,H是random oracle,即H是某个从ZNZ_N映射到密钥空间的随机函数。那么这个公钥系统就可以抵抗选择密文攻击
图 4

不要直接用RSA来加密明文!!!因为RSA是确定性的函数,因此不可能是语义安全的
图 5
图 6
(直接用RSA加密密钥会被破解,但是ISO标准是用RSA加密密钥的哈希函数原项,破解出原项再hash一下就得到了密钥,这不是一样的吗?) 对称密钥的空间大小远远小于RSA的明文空间

PKCS1

ISO标准不是RSA在实际中的应用。实际使用是将对称密钥扩展然后用RSA加密
图 7
图 8
图 9
图 10

解决方法是,服务器解密后发现开头不是02,就认为明文只是个随机值而不是包含密钥的明文,继续协议就会发现密钥不一致从而结束会话(最常用的PKCS1)
图 11

图 12
图 13
图 14
图 15

RSA的安全性

如果已经知道N的因式分解,那么可以用中国剩余定理求解x
图 16

计算e次根一定要因式分解吗?如果没有其它方法就说明了一个reduction(规约):
任何有效的计算模N的e次根的算法都是有效的因式分解算法
图 17
图 18
图 19
图 20

实际应用中的RSA

最小的公钥e是3,可以但推荐还是65537。
RSA-CRT(带中国剩余定理的RSA)。RSA的加密很快但是解密很慢
图 21
图 22

RSA数学上是正确的,但是如果没有较好实现,会出现各种旁道攻击
图 23
图 24

防火墙刚启动时种子数量少导致伪随机数生成器重复生成p,导致网络上许多设备的p相同
图 25
图 26

ElGamal

前一节讲了基于陷门置换函数的公钥加密系统,这节讲基于Diffle-Hellman协议的公钥加密系统
图 27
图 28
图 29
图 30
图 31
图 32
图 33
图 34
图 35

ElGamal如果不做预计算,加密会比解密慢,但是因为g是固定的,意味着加密可以做预计算,当内存足够时加密是比解密快的。但是内存不够,不能预计算时,RSA更快,因为只做一次指数运算
图 36

ElGamal安全性

这个计算Diffle-Hellman假设对于分析ElGamal系统的安全性并不理想
图 37
我们引入更强的哈希Diffle-Hellman假设,更强假设的意思是,攻击者的能力更强,但是我们提出的某个论断仍然成立
图 38
图 39
图 40
图 41
语义安全是不够的,我们真正想要的是选择密文安全。
为了证明选择密文安全,我们引入一个更强的假设叫做交互Diffle-Hellman假设
图 42

交互Diffle-Hellman假设是CCA安全的,现在问题是在CDH假设上能否实现CCA安全,没有random oracle能否实现CCA安全
图 43

有更好安全性分析的ElGamal变种

图 44

我们想在CDH假设上实现CCA安全,有两种办法,第一种是使用双线性群,这种群CDH和IDH是等价的;第二种是修改ElGamal系统
图 45

第二种方法有一个ElGamal的变种满足CDH假设上的CCA安全
图 1
图 2

如果没有random oracle,CCA安全还成立吗?
图 3
图 4

ElGamal和RSA两种公钥系统共同遵循的原理

这里没有形式化给出单向函数的定义,因为要证明单向函数是否存在,也就是要证明P不等于NP(若P=NP,则公钥密码学将有根基危机)
图 5
图 6
图 7
RSA有乘法性质和陷门,陷门意味着有私钥就可以逆向计算
图 8

总结:公钥加密依赖具有同态性质的单向函数和陷门
图 9

课程总结

图 10
图 11
图 13
图 14

]]>
+ + + + + <p>斯坦福教授Dan Boneh的密码学课程《Cryptography》<br /> +课程链接:<a href="https://www.bilibili.com/video/BV1Ht411w7Re">https://www.bilibili.com/video/BV1Ht4 + + + + + + + + + + + + + +
+ + + 2022.05.23 某区块链公司面试记录 + + https://zunpan.github.io/2022/05/23/2022-5-23-%E6%9F%90%E5%8C%BA%E5%9D%97%E9%93%BE%E5%85%AC%E5%8F%B8%E9%9D%A2%E8%AF%95%E8%AE%B0%E5%BD%95/ + 2022-05-23T11:47:58.000Z + 2023-09-24T04:27:40.271Z + + 投的后端开发实习生岗,一天面了两次技术面

一面

  1. 针对简历项目提问,为什么要paillier?为什么要秘密分享?
  2. mysql的主从备份,主从节点的事务需不需要分开执行?从节点事务执行失败,主节点如何回滚?binlog的同步和事务在主从节点的执行,两者按时间顺序如何排列?
  3. 手写代码:求字典序第k大的数

二面

  1. 区块链共识算法有哪些?每个算法容错数量是多少?双花问题?默克尔树?默克尔证明?
  2. Java基本数据类型和引用类型的区别
  3. 为什么每个基本数据类型都有包装类,包装类有什么用?
  4. 针对简历提问,秘密分享时间复杂度多少?paillier用在哪里?
  5. 手写代码:多线程卖票,要求每个线程同时工作,不能超卖
  6. 两个大文件,文件内容是字符串集合,内存略大于一个文件,如何对两个文件进行字符串去重?如何优化时间复杂度?
]]>
+ + + + + <p>投的后端开发实习生岗,一天面了两次技术面</p> +<h2 id="一面"><a class="markdownIt-Anchor" href="#一面"></a> 一面</h2> +<ol> +<li>针对简历项目提问,为什么要paillier?为什么要秘密分享?</li> +< + + + + + + + + + +
+ + + 基于区块链的安全电子选举系统 + + https://zunpan.github.io/2022/05/13/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/ + 2022-05-13T13:10:09.000Z + 2023-09-24T04:27:40.282Z + + 背景:浙江大学软件学院实训课题,本组选择的课题是关于区块链和隐私计算的融合应用场景,具体方向是电子选举
需求:投票人不暴露身份隐私的情况下完成投票,计票机构在选票合法的情况下完成计票


技术方案总结

图 1
图 2

项目总体情况

图 4

核心功能性能测试

图 5

测试环境:x86_64,Intel® Xeon® Gold 6133 CPU @ 2.50GHz,Linux 5.4.0-96-generic
测试结果:大规模选举时延较高,适用于小规模选举活动,但因为选举本身的非实时性,也可用于更大规模

亮点

在隐匿链上选票来自谁的情况下选民只需要通过零知识证明证明自己的合法选民身份。
即使是对一个合法证明的微小改变,都无法通过链上以及链下的零知识证明验证,攻击者难以冒充合法选民。

缺陷

  • 监管机构(后端)无法证明没有存储选民的隐私信息,例如该选民的选票号,私有盐。这里存在漏洞可以使得作恶的监管机构只需执行两次解密算法就可以获得选民的选票内容
  • 零知识证明以匿名的形式证明了选民身份,选民通过秘密ID和监管方产生的PK生成证明并公布。秘密输入应当是选民秘密持有的,因此生成证明的过程最好是在本地做。
    当前选民需要通过可信的后端API生成证明,无法便捷地通过WASM在本地生成(原计划下libsnark难以迁移,且曲线计算迁移到wasm慢几十倍),因此使用起来较为不便。

系统展示

图 6
图 7
图 8
图 9
图 10
图 11
图 12
图 13
图 14
图 15
图 16

]]>
+ + + + + <p>背景:浙江大学软件学院实训课题,本组选择的课题是关于区块链和隐私计算的融合应用场景,具体方向是电子选举<br /> +需求:投票人不暴露身份隐私的情况下完成投票,计票机构在选票合法的情况下完成计票</p> +<hr /> +<h2 id="技术方案总结"><a class="ma + + + + + + + + + + + + + + + +
+ + + MIT-Missing-Semester学习笔记 + + https://zunpan.github.io/2022/05/07/MIT-Missing-Semester%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ + 2022-05-07T11:46:29.000Z + 2023-09-24T04:27:40.278Z + + Course overview + the shell

图 19
图 20

Shell Tools and Scripting

图 21
图 22
图 23

TLDR

TLDR可以代替man命令,查找命令说明书,TLDR可以给出详细的例子
图 24

find

图 25
图 26

ripgrep

可以代替grep查找文件内容符合条件的文件
图 27

Ctrl+R 可以进入用子串查找历史命令的模式

xargs 命令,它可以使用标准输入中的内容作为参数。 例如 ls | xargs rm 会删除当前目录中的所有文件。

Vim

Data Wrangling

数据处理常用工具有grep

另外还有一些常用的数据处理工具

  • sed,文件(输入流)处理工具

图 28

  • sort,排序工具

图 29

  • awk,作用于文件的多功能编程语言

图 30

Command-line Enviroment

Job Control(作业控制)

结束一个正在执行的程序可以按ctrl+c,实质是终端向程序发送了一个SIGINT的signal
SIGINT可以在程序中捕获处理,因此ctrl+c也就是SIGINT信号有时候没用。

SIGQUIT信号可以结束程序并不会被捕获,按ctrl+\

SIGSTOP信号可以暂停程序到后台,按ctrl+z。命令行输入jobs查看所有程序运行状态,使用fg %程序id 或 bg %程序id 将程序从暂停状态转到前端执行或后端执行

尽管程序可以在后端执行,但是一旦终端关闭,作为子进程的程序会收到一个SIGHUP信号导致被挂起,可以使用nohup 程序 &来使得程序在后端执行并忽略SIGHUP信号

图 31

Terminal Multiplexers(终端复用器)

图 32

Aliases(别名)

图 33

图 34

直接在命令行定义别名,关闭终端后就没了,需要在.bashrc或者.zshrc这样的终端启动文件中做持久化

Dotfiles(点文件或者配置文件)

许多配置文件以’.‘开头,所以叫dotfile,默认情况下ls看不到dotfile
图 35

配置文件最好用版本控制统一管理,然后将原来的配置文件路径软链接到版本控制下的配置文件路径,github上有许多dotfile仓库

Remote Machines(远程机器)

使用ssh可以登录远程终端
图 36

foo是用户名,bar.mit.edu是域名,也可以直接是ip地址

也可以直接使用ssh执行命令,只要在上面的命令后面加上需要执行的命令即可

Version Control(Git)

Debugging and Profiling

Debugging

Printf debugging and Logging

“The most effective debugging tool is still careful thought, coupled with judiciously placed print statements” — Brian Kernighan, Unix for Beginners.
在程序中使用logging而不用printf的好处在于

  • 日志可以输出到文件、sockets、甚至是远程服务器而不一定是标准输出
  • 日志支持几种输出级别(例如INFO,DEBUG,WARN,ERROR)
  • 对于新出现的问题,日志有足够多的信息来排查

tips:输出可以按级别用不同颜色表示,例如ERROR用红色
echo -e "\e[38;2;255;0;0mThis is red\e[0m"会打印红色的“This is red”到终端上

Third Party logs

许多第三方程序会将日志写到系统的某一处,一般是/var/log. NGINX服务器会将日志放在/var/log/nginx下. 许多linux系统使用systemd, 一个系统守护进程来控制许多事情例如某些服务的开启和运行, systemd将日志放在/var/log/journal下, 可以使用journalctl查看

图 37

Debuggers

Specialized Tools

Linux下可以使用strace查看程序系统调用情况
图 38

tcpdump和Wireshark是网络包分析器
web开发中,Chrome和Firefox的开发者工具十分方便

Profiling(分析)

学习分析和检测工具可以帮助理解程序中那一部分花费最多时间或资源以便优化这部分
Timing
三种不同的运行时间

  • 真实时间:程序开始到结束的时间,包括阻塞时间、等待IO、网络
  • 用户态时间:CPU执行程序中用户态代码的时间
  • 系统态时间:CPU执行程序中核心态代码的时间
    正确的程序执行时间=用户态时间+系统态时间

time命令可以测试程序的三种时间
图 39

Metaprogramming

这节主要讲系统构建工具make、持续集成等

Security and Cryptography

Entropy(熵)

熵度量了不确定性并可以用于决定密码的强度
熵的单位是比特,对于一个均匀分布的离散随机变量,熵等于log2(所有可能的个数,即n)log_2(所有可能的个数,即n)
扔一次硬币的熵是1比特。掷一次六面骰子的熵大约为2.58比特。一般我们认为攻击者了解密码的模型(最小长度,最大长度,可能包含的字符种类等),但是不了解某个密码是如何选择的

https://xkcd.com/936/ 例子里面,“correcthorsebatterystaple”这个密码比“Tr0ub4dor&3”更安全,因为前者熵更大,大约40比特的熵足以对抗在线穷举攻击(受限于网络速度和应用认证机制);而对于离线穷举攻击(主要受限于计算速度),一般需要更强的密码(比如80比特)

Potpourri

]]>
+ + + + + <h2 id="course-overview-the-shell"><a class="markdownIt-Anchor" href="#course-overview-the-shell"></a> Course overview + the shell</h2> +<p>< + + + + + + + + + + + +
+ + + Maven问题记录 + + https://zunpan.github.io/2022/05/07/Maven%E9%97%AE%E9%A2%98%E8%AE%B0%E5%BD%95/ + 2022-05-07T11:01:06.000Z + 2023-09-24T04:27:40.279Z + +
  • 多模块项目依赖错误
    多模块项目直接对根模块进行install,Maven会根据依赖自动判断install顺序。install完成后需要点击Maven界面的Reload All Maven Projects
    图 17

  • 父模块的pom文件一定得写<packaging>pom</packaging>
    因为默认打包方式是jar,<xs:element name="packaging" minOccurs="0" type="xs:string" default="jar">
    图 1

  • 有些公司内部仓库的包只有pom文件没有jar,这种是下载不下来的
    图 1
    只有jar包下载到了本地,maven才能找到依赖,所以这就是为什么多模块项目,被依赖的模块要先install到本地,这样依赖它的模块才能找到这个模块

  • ]]>
    + + + + + <ol> +<li> +<p>多模块项目依赖错误<br /> +多模块项目直接对根模块进行install,Maven会根据依赖自动判断install顺序。install完成后需要点击Maven界面的Reload All Maven Projects<br /> +<img src="ht + + + + + + + + + +
    + +
    diff --git a/books/index.html b/books/index.html new file mode 100644 index 0000000..8f285b0 --- /dev/null +++ b/books/index.html @@ -0,0 +1,604 @@ + + + + + + + + + + + + + + + + + + + + 书单 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    书单

    +

    个人收藏的书单推荐给大家 [我的豆瓣]

    + +
    +
    +
    +

    正在加载...

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/Git/index.html b/categories/Git/index.html new file mode 100644 index 0000000..ae1c0b4 --- /dev/null +++ b/categories/Git/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + 分类: Git | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    分类: Git

    + + +
    + +
    +
    + + +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/Java/index.html b/categories/Java/index.html new file mode 100644 index 0000000..588c4e2 --- /dev/null +++ b/categories/Java/index.html @@ -0,0 +1,576 @@ + + + + + + + + + + + + + + + + + + + + 分类: Java | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    分类: Java

    + + +
    + +
    + +
    +
    + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/Java/page/2/index.html b/categories/Java/page/2/index.html new file mode 100644 index 0000000..b3ab91b --- /dev/null +++ b/categories/Java/page/2/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + 分类: Java | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    分类: Java

    + + +
    + +
    + +
    +
    + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/Linux/index.html b/categories/Linux/index.html new file mode 100644 index 0000000..b8cff71 --- /dev/null +++ b/categories/Linux/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + 分类: Linux | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    分类: Linux

    + + +
    + +
    +
    + + +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/Maven/index.html b/categories/Maven/index.html new file mode 100644 index 0000000..cacf347 --- /dev/null +++ b/categories/Maven/index.html @@ -0,0 +1,486 @@ + + + + + + + + + + + + + + + + + + + + 分类: Maven | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    分类: Maven

    + + +
    + +
    +
    + + +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/MySQL/index.html b/categories/MySQL/index.html new file mode 100644 index 0000000..02a08fe --- /dev/null +++ b/categories/MySQL/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + 分类: MySQL | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    分类: MySQL

    + + +
    + +
    +
    + + +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/index.html b/categories/index.html new file mode 100644 index 0000000..17577d3 --- /dev/null +++ b/categories/index.html @@ -0,0 +1,923 @@ + + + + + + + + + + + + + + + + + + + + 分类 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    分类

    + + +
    + +
    + + + +
    + + +
    + + + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + + + +
    + + +
    + +
    + + +
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/categories/\345\214\272\345\235\227\351\223\276/index.html" "b/categories/\345\214\272\345\235\227\351\223\276/index.html" new file mode 100644 index 0000000..5b0f14f --- /dev/null +++ "b/categories/\345\214\272\345\235\227\351\223\276/index.html" @@ -0,0 +1,576 @@ + + + + + + + + + + + + + + + + + + + + 分类: 区块链 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    分类: 区块链

    + + +
    + +
    + +
    +
    + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/categories/\345\214\272\345\235\227\351\223\276/page/2/index.html" "b/categories/\345\214\272\345\235\227\351\223\276/page/2/index.html" new file mode 100644 index 0000000..0ac66a1 --- /dev/null +++ "b/categories/\345\214\272\345\235\227\351\223\276/page/2/index.html" @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + 分类: 区块链 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    分类: 区块链

    + + +
    + +
    + +
    +
    + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/categories/\345\237\272\347\241\200\347\237\245\350\257\206/index.html" "b/categories/\345\237\272\347\241\200\347\237\245\350\257\206/index.html" new file mode 100644 index 0000000..401ce2e --- /dev/null +++ "b/categories/\345\237\272\347\241\200\347\237\245\350\257\206/index.html" @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + 分类: 基础知识 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    分类: 基础知识

    + + +
    + +
    +
    + + +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/categories/\345\257\206\347\240\201\345\255\246/index.html" "b/categories/\345\257\206\347\240\201\345\255\246/index.html" new file mode 100644 index 0000000..9196df6 --- /dev/null +++ "b/categories/\345\257\206\347\240\201\345\255\246/index.html" @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + 分类: 密码学 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    分类: 密码学

    + + +
    + +
    +
    + + +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/categories/\346\235\202\351\241\271/index.html" "b/categories/\346\235\202\351\241\271/index.html" new file mode 100644 index 0000000..faf3c76 --- /dev/null +++ "b/categories/\346\235\202\351\241\271/index.html" @@ -0,0 +1,486 @@ + + + + + + + + + + + + + + + + + + + + 分类: 杂项 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    分类: 杂项

    + + +
    + +
    + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/categories/\347\256\227\346\263\225/index.html" "b/categories/\347\256\227\346\263\225/index.html" new file mode 100644 index 0000000..01ff05f --- /dev/null +++ "b/categories/\347\256\227\346\263\225/index.html" @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + 分类: 算法 | panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    +
    +

    分类: 算法

    + + +
    + +
    +
    + + +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/content.json b/content.json new file mode 100644 index 0000000..8252386 --- /dev/null +++ b/content.json @@ -0,0 +1 @@ +{"meta":{"title":"panzun blog","subtitle":"","description":"","author":"panzun","url":"https://zunpan.github.io","root":"/"},"pages":[{"title":"404 Not Found:该页无法显示","date":"2024-02-26T12:05:19.896Z","updated":"2023-09-24T04:27:40.270Z","comments":false,"path":"/404.html","permalink":"https://zunpan.github.io/404.html","excerpt":"","text":""},{"title":"关于","date":"2024-02-26T12:05:19.906Z","updated":"2023-09-24T04:27:40.286Z","comments":false,"path":"about/index.html","permalink":"https://zunpan.github.io/about/index.html","excerpt":"","text":"个人详细介绍"},{"title":"书单","date":"2024-02-26T12:05:19.906Z","updated":"2023-09-24T04:27:40.286Z","comments":false,"path":"books/index.html","permalink":"https://zunpan.github.io/books/index.html","excerpt":"","text":""},{"title":"分类","date":"2024-02-26T12:05:19.916Z","updated":"2023-09-24T04:27:40.286Z","comments":false,"path":"categories/index.html","permalink":"https://zunpan.github.io/categories/index.html","excerpt":"","text":""},{"title":"Excel比对与合并系统","date":"2023-03-03T07:00:03.000Z","updated":"2023-09-24T04:27:40.288Z","comments":true,"path":"hidden/Excel比对与合并系统_完整版.html","permalink":"https://zunpan.github.io/hidden/Excel%E6%AF%94%E5%AF%B9%E4%B8%8E%E5%90%88%E5%B9%B6%E7%B3%BB%E7%BB%9F_%E5%AE%8C%E6%95%B4%E7%89%88.html","excerpt":"","text":"背景 许多游戏策划使用Excel来配置数值。策划需要保存所有版本的数值表,有时需要查看两个数值表有何差异,有时想把差异或者叫修改应用到另一张数值表中,这非常类似于版本控制,但是市面上的版本控制系统svn和git都是针对文本文件,不能用于Excel的版本控制 Excel比对算法 一维数据比对算法 假设有两个序列A1...AmA_1...A_mA1​...Am​和B1...BnB_1...B_nB1​...Bn​,我们可以通过对A序列进行一些列操作,使之变为B序列。对每种操作都定义个Cost,如何找到总Cost最小的使A变为B的操作序列,可以通过动态规划解决。这是一个已经被广为研究的算法问题,本文就不再整述,读者可以在网上搜索Edit编辑距离获取更多信息。 操作集合的定义有多种方式,一种较为常见的操作集合定义如下(Cost均为1) : 在序列中插入一个元素: 在序列中删除一个元素; 比如,将字符串kiten变换为sitting,需要删除k,插入s,删除e,插入i,在尾部插入g。如果在原序列和目标序列中去掉删除和插入的元素,那么原序列和目标序列就是完全相同的了(比如上面的例子两者都变为itn了),因此这种编辑距离被命名为LCS (Longest Common Subsequence) 编辑距离。LeetCode 1143. 最长公共子序列 再回到本文要讨论的差异比较问题,要比较两个序列的差异,实际上就是要找到二者之间尽量多的公共部分,剩下的就是差异部分,所以这和最短编辑距离问题是完全等价的。 此外,除了LCS编辑距离之外,还有一种常用的编辑距离,允许插入、删除和修改操作,叫做Levenshtein编组距离。另外,还可以定义一种广义的Levenshtein编辑距离,删除元素AiA_iAi​和插入元素BjB_jBj​;的Cost由一个单参数函数决定,记为Cost(AiA_iAi​)或Cost(BjB_jBj​); 将AiA_iAi​修改为BjB_jBj​;的操作的Cost由一个双参数函数决定,记为Cost2(Ai,BjA_i, B_jAi​,Bj​)。 /** * 比对的基本单位是单个字符 * @param text1 字符串1 * @param text2 字符串2 * @return levenshteinDP数组 */ static int[][] levenshteinDP(String text1, String text2) { int len1 = text1.length(); int len2 = text2.length(); // dp[i][j]表示从text1[0...i-1]到text2[0...j-1]的最小编辑距离(cost) dp = new int[len1 + 1][len2 + 1]; // path记录此方格的来源是多个此类枚举值的布尔或值 path = new int[len1 + 1][len2 + 1]; for (int i = 0; i < len1 + 1; i++) { dp[i][0] = i; path[i][0] = FROM_INIT; } for (int j = 0; j < len2 + 1; j++) { dp[0][j] = j; path[0][j] = FROM_INIT; } for (int i = 1; i < len1 + 1; i++) { for (int j = 1; j < len2 + 1; j++) { path[i][j] = FROM_INIT; int left = dp[i][j - 1] + 1; int up = dp[i - 1][j] + 1; int leftUp; boolean replace; if (text1.charAt(i - 1) == text2.charAt(j - 1)) { leftUp = dp[i - 1][j - 1]; replace = false; } else { leftUp = dp[i - 1][j - 1] + 1; replace = true; } dp[i][j] = Math.min(Math.min(left, up), leftUp); if (dp[i][j] == left) { path[i][j] |= FROM_LEFT; } if (dp[i][j] == up) { path[i][j] |= FROM_UP; } // 对应:两字符完全一样或者可以修改成一样 if (dp[i][j] == leftUp) { if (replace) { path[i][j] |= FROM_LEFT_UP_REPLACE; } else { path[i][j] |= FROM_LEFT_UP_COPY; } } } } return dp; } 同样的,对于广义Levenshtein编辑距离,如果去掉删除和插入的元素,那么两个序列中剩下的元素即为一一对应的关系,每组对应的两个元素,要么是完全相同的,要么前者是被后者修改掉的。从这部分论述中我们不难看出,比较算法的核心思路实际上就是找到元素与元素之间的一一对应关系 二维数据比对算法 Excel的一个分页可以认为是一个二维数据阵列,阵列中的每个元素是对应单元格内容的字符串值。根据前面的论述,比较两个二维阵列的核心就是找到他们公共的行/列,或者说原阵列和目标阵列的行/列对应关系。比如,对于下面两张表: 甲表的第1、2、3列对应乙表的1、2、4列,甲表的1、3行对应乙表的1、2行。那么这两张表的差异通过下列方式描述: 在第2列的位置插入新列 删除第2行 将(原表中) 第3行第3列的元素从9修改为0; 如何计算两张表对应的行/列,一个比较容易想到的方案是将其拆分为两个独立求解的问题,计算对应的行和计算对应的列。对于前者,我们可以把阵列的每一行当成一个元素,所有行组成一个序列,然后对这个序列进行比较:后者亦然。这样我们就把二维比较问题转化成了一维比较的问题。关于编辑距离的定义,可以采用广义Levenshtein编辑距离,定义删除、插入元素的Cost为该行(列)的元素数,定义修改元素的Cost为这两行(列)之间的LCS编辑距离.于是两个二维阵列的比较过程如下: 找到二者对应的 (或者叫公共的) 行/列,非公共的行/列记为删除、插入行/列操作;两张表只保留公共的行/列,此时他们尺寸完全相同,对应位置的单元格逐一比较,如果值不相同,则记为单元格修改操作; 算法优化 上一个部分介绍的二维阵列比较方案只是一个理论上可行的方案,在实际应用中,存在以下问题: 删除、插入行/列的操作都是对于整个行/列的,而计算两行/列之间的LCS编辑距离是独立计算的,因此算法本身有一定不合理性; 计算修改Cost里又包含了LCS编辑距离的计算,二层嵌套,性能开销比较大; 针对上述问题,从游戏开发的实际应用场景出发,做了如下优化: 首先计算列之间的对应关系,只取表头前h行(不同项目表头行数h可能不同,可以通过参数配置) ,这样就把对整列的LCS编辑距离计算优化为h个单元格逐已比较,大幅优化了效率,而且对准确度基本不会有什么影响; 根据上一步的计算结果,去掉非公共的列(即删除、添加的列),这样,剩下的列都是两边一一对应的了,此时再计算行的对应关系,修改操作的Cost定义就可以从LCS编辑距离改为单元格的逐一比较了,这样又大幅优化了性能 在上面所述基础之上,还可以再做优化,因为在实际应用中,绝大多数情况下,绝大多数行都不会有修改,因此可以先用LCS编辑距离对所有行的对应关系进行计算,即只有当两行内容完全相同时才会被当做是对应的; 然后再把剩下的未匹配的行分组用广义Levenshtein编辑距离进行对应关系匹配。这样么做的原因是LCS编辑距离比广义Levenshtein编辑距离的求解速度要快很多。 功能扩展 在开发过程中,我们经常会将单行或者连续若干行单元格上移或下移到另一位置,按照目前的比较逻辑,该操作会被认为是删除这些行,然后在新的位置重新插入这些行。这样的结果和不合理的。为此,我们可以引入一种新的操作: 移动行到另一位置。加入了这个新的操作之后,我们依然可以建立之前所述的行对应关系,只不过两边行的顺序可以是乱序的。这种不保序对应关系可以通过多个轮次的编辑距离匹配计算,每次匹配之后去掉匹配上的行,剩下未匹配的行组成一个新的序列进行下一轮的匹配。每轮匹配是采用LCS编辑距离还是广义Levenshtein编指距离可以灵活决定,比如前若干轮或者行数较多时采用LCS编辑距离,后面的轮次再用广义Levenshtein编辑距离。 公式处理 在前面的论述中,为了简化模型,我们把单元格值当成了一个纯字符串处理。实际上单元格值由两部分组成:公式和文本。若公式不为空,那么文本就是公式的计算结果;若公式为空,文本则为填写的文字内容。因此对Excel的完整的比较过程应该也包含对公式的比较。比较公式的时候会有一个问题,因为公式里引用其他单元格用的是列坐标的形式,当有行/列增减的时候,会导致这些坐标发生变化,产生额外的比较差异。该问题可以通过如下的方式解决: 先不考虑公式,用文本内容计算出行/列的对应关系; 利用上一步计算出的对应关系,将甲表中所有公式的引用单元格进行坐标变换,变换到对应乙表中的坐标,再用变换后的公式去和乙表中的公式进行逐一比较 例如甲表中有一个公式为A1+B1,乙表在最上面插入了一行,因此该格子在乙表中的公式变成了A2+B2。借助文本内容进行匹配,得出甲表的行1对应着乙表的行2,因此甲表公式中的行1都改成行2,即A1+B1 -> A2+B2,这样就和乙表中的实际公式是一致的了,没有产生不必要的差异 关于如何对Excel公式进行引用坐标变换,一个很容易想到的方案就是借助第三方的Excel公式语法分析库将公式解析成语法树,再进行变换。但是这样做效率是非常低的,对于项目中动辄上百列上万行的大规模Excel表格是不具有可行性的。 另一个方案是通过正则表达式匹配出所有的单元格引用。Excel的单元格用格式定义如下: 行坐标: \\S?\\d+ 列坐标: \\S?[A-Z]+ 单元格引用: 行坐标列坐标(行坐标:行坐标](列坐标:列坐标) 其中\\$表示该用是绝对位置引用还是相对位置引用,二者只有在编辑Excel过程中进行单元格复制时才有区别对本文中讨论的引用坐标变换过程无影响。 这样我们就可以通过正则表达式匹配找出所有的单元格引用,并对其进行变换。在这个过程中需要注意的是要排除掉一些干扰因素: 跨分页、跨文件引用时,分页名和文件名,可能包含单元格引用格式; 字符串常量中可能包含单元格引用格式 函数名可能包含单元格引用格式 (比如ATAN2函数,就符合单元格引用的正则表达式) 排除的办法是先对上述这些项进行匹配,然后再对单元格引用格式进行匹配。 通过正则表达式法进行公示变换,实测效率为~100个每毫秒。对于规模较大的表格仍然可能成为效率瓶颈。对此可以进行进一步优化。考虑到项目中通常会有大批形式重复的公式,只是引用的单元格不同,例如下图: 因此我们可以先对原始公式做一次变换,将连续的大写字母换成单个字母A,将连续的数字替换成单个数字0这样上图中的公式全部都变成了: 我们称之为“公式模板”,之后再对这个模板进行正则表达式匹配,匹配到的单元格引用,再通过模板与原公式之间的字符位置对应关系反推出原本位置。因为大量公式都对应同一个模板,所以可以通过哈希表把计算过的模板都须存起来,大幅优化运行效率。经过优化之后,实测效率为~1000个每毫秒,比优化前提高了10倍. Excel合并算法 合并(Merge) 是版本管理系统(Version Control System,例SVN、Git等) 中的一个基本概念。设文件初始状态记为Base,“我”对Base进行了修改,记为Mine,“其他人”同时也对Base进行了修改,记为Theirs,将“我”和“其他人”的修改合到一起,文件变为Merged状态,这一过程就叫做Merge。简单来说,Merge过程就是输入Base、Mine、Theirs文件,输出Merged文件的过程,可以用于本地冲突合并、分支合并、单次提交回退等场景。Merge的通常过程是,对Base和Mine进行比较,找到所有差异,再将所有差异应用到Theirs上,即可得到Merged文件。 对于Excel文件的Merge过程,也可以运用上述思路。首先比较Base和Mine,找到增减的行/列以及单元格值的修改,再将其应用到Theirs表上。在应用增删行/列这类改动时,可以调用相应API来实现,而不是将最终结果数据直接更新到目标分页中,后者会导致表格中格式的错乱。 此外还需要注意的问题是是,我们在描述这些改动的时候,涉及到的位置相关信息,用的是在哪张表中的行数或者列数。比如在Base表中的第10行后面插入了一个新的行,那么就需要计算出这一行在Theirs表中的位置,才能将这个改动应用到Theirs表,因此需要对Base和Theirs也做一次比较,目的是建立起Base和Theirs之间的行/列对应关系 在将Mine的改动应用到Theirs的过程中,会遇到一些特殊情况,需要人手动决定以何种方式应用改动项,或者改动项无法应用。例如某个格子在Base中的值时A,在Mine中被改成了B,在Theirs中被改成了C,那么在Merged中应该取值B还是C,就不是算法可以解决的了。这种情况就叫做“冲突” (Conflict)。总结下来一共有以下几种冲突情形: 双方都修改了同一个单元格的值,且修改结果不同,该单元格取值无法确定 一方新增了行,另一方新增了列,合并后新增行列交叉点处的单元格取值无法确定 一方修改了单元格值,另一方删除了这个单元格所在的行或者列; 一方新增了行(列) ,另一方或删除了列(行) ,因此新增行(列) 中有格子被另一方删除了: 其中1和2属于取值无法确定的情况,需要手动指定取值;3和4属于改动无法应用的情况,需要给出警告提示。在实际应用场景中,4的危害是比较小的,不提示也是可以的。"},{"title":"友情链接","date":"2024-02-26T12:05:19.936Z","updated":"2023-09-24T04:27:40.288Z","comments":true,"path":"links/index.html","permalink":"https://zunpan.github.io/links/index.html","excerpt":"","text":""},{"title":"Repositories","date":"2024-02-26T12:05:19.936Z","updated":"2023-09-24T04:27:40.288Z","comments":false,"path":"repository/index.html","permalink":"https://zunpan.github.io/repository/index.html","excerpt":"","text":""},{"title":"标签","date":"2024-02-26T12:05:19.946Z","updated":"2023-09-24T04:27:40.289Z","comments":false,"path":"tags/index.html","permalink":"https://zunpan.github.io/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"深入理解Java虚拟机学习笔记","slug":"深入理解Java虚拟机学习笔记","date":"2023-06-10T15:30:14.000Z","updated":"2023-09-24T04:27:40.285Z","comments":true,"path":"2023/06/10/深入理解Java虚拟机学习笔记/","link":"","permalink":"https://zunpan.github.io/2023/06/10/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","excerpt":"","text":"第一部分 走近Java 1. 走近Java 1.1 概述 Java不仅仅是一门编程语言,还是一个由一系列计算机软件和规范组成的技术体系 Java优点:1. 一次编写,到处运行;2. 避免了绝大部分内存泄露和指针越界问题;3. 实现了热点代码检测和运行时编译及优化,越运行性能越好;4. 完善的类库 1.2 Java技术体系 广义上,Kotlin等运行在JVM上的编程语言都属于Java技术体系 传统上,JCP定义的Java技术体系包含:1. Java程序设计语言;2. 各种硬件平台上的Java虚拟机实现;3. Class文件格式;4 Java类库API;5. 来自商业机构和开源社区的第三方Java类库 JDK:Java程序设计语言+Java虚拟机+Java类库 JRE:Java类库API中的Java SE API子集和Java虚拟机 1.3 Java发展史 1.4 Java虚拟机家族 Sun Classic/Exact VM。Sun Classic是世界上第一款商用Java虚拟机,纯解释执行代码,如果外挂即时编译器会完全接管执行,两者不能混合工作。Exact VM解决了许多问题但是碰上了引进的HotSpot,生命周期很短 HotSpot VM:使用最广泛的Java虚拟机。HotSpot继承了前者的优点,也有许多新特性,例如热点代码探测技术。JDK 8中的HotSpot融合了BEA JRockit优秀特性 Mobile/Embedded VM:针对移动和嵌入式市场的虚拟机 BEA JRockit/IBM J9 VM:JRockit专注服务端应用,不关注程序启动速度,全靠编译器编译后执行。J9定位类似HotSpot BEA Liquid VM/Azul VM:和专用硬件绑定,更高性能的虚拟机 Apache Harmony/Google Android Dalvik VM:Harmony被吸收进IBM的JDK 7以及Android SDK,Dalvik被ART虚拟机取代 Microsoft JVM:Windows系统下性能最好的Java虚拟机,因侵权被抹去 1.5 展望Java技术的未来 无语言倾向:Graal VM可以作为“任何语言”的运行平台 新一代即时编译器:Graal编译器,取代C2(HotSpot中编译耗时长但代码优化质量高的即时编译器) 向Native迈进:Substrate VM提前编译代码,显著降低内存和启动时间 灵活的胖子:经过一系列重构与开放,提高开放性和扩展性 语言语法持续增强:增加语法糖和语言功能 第二部分 自动内存管理 2. Java内存区域与内存溢出异常 2.1 概述 C、C++程序员需要自己管理内存,Java程序员在虚拟机自动内存管理机制下不需要为每一个new操作写对应的delete/free代码,但是一旦出现内存泄露和溢出,不了解虚拟机就很难排错 2.2 运行时数据区域 程序计数器:当前线程执行的下一条指令的地址;线程私有;不会OOM 虚拟机栈:Java方法执行的线程内存模型,每个方法执行时,JVM都会创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息;线程私有;会栈溢出和OOM 局部变量表:存放的变量的类型有8种基本数据类型、对象引用和returnAddress类型(指向字节码指令的地址),这些变量除了64位的long和double占两个变量槽,其它占1个。局部变量表的大小在编译器确定 本地方法栈:和虚拟机栈作用类似,区别在于只是为本地(Native)方法服务,HotSpot将两者合二为一 堆:“几乎”所有对象实例都在此分配内存;可分代,也不分代;逻辑上连续,物理上可以不连续;会OOM 方法区:也叫“非堆”,存储已加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据;实现上,之前HotSpot使用永久代实现方法区,目的是方便垃圾回收,但是这样有OOM问题,JDK8废弃永久代改为使用本地内存的元空间,主要存放类型信息,其它移到堆;内存回收目标主要是常量池和类型信息 运行时常量池:Class文件中的常量池表存放编译期生成的各种字面量和符号引用,这部分内容在类加载后放到方法区的运行时常量池;具有动态性,运行时产生的常量也可以放入运行时常量池,String类的intern()利用了这个特性;会OOM 直接内存:不是运行时数据区的一部分,也不是Java虚拟机规范里定义的内存区域;NIO使用Native函数库直接分配堆外内存;会OOM 2.3 HotSpot虚拟机对象探秘 2.3.1 对象的创建 当JVM碰到new指令,首先检查指令参数能否在常量池中定位到一个类的符号引用,并且检查该类是否已被加载、解析和初始化,如果没有就进行类加载过程 JVM为新生对象分配内存。 对象所需内存大小在类加载后可确定,分配方法有两种:当垃圾收集器(Serial、ParNew)能空间压缩整理时,堆是规整的,分配内存就是把指针向空闲空间方向移动对象大小的距离,这种叫“指针碰撞”,使用CMS收集器的堆是不规整的,需要维护空闲列表来分配内存。 内存分配可能线程不安全,例如线程在给A分配内存,指针来没来得及修改,另一线程创建对象B又同时使用了原来的指针来分配内存。解决方法有两个:1.对分配内存空间的动作进行同步处理,JVM采用CAS+失败重试的方式保证原子性;2.预先给每个线程分配一小块内存,称为本地线程分配缓冲(TLAB),线程分配内存先在TLAB上分配,TLAB用完了再同步分配新的缓存区 JVM将分配到的内存空间(不包括对象头)初始化为零值,这步保证对象的实例字段可以不赋初始值就直接使用 JVM对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中 执行构造函数,即Class文件中的<init>() 2.3.2 对象的内存布局 对象头 用于存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳,称为“Mark Word”。这部分数据长度32比特或64比特,Bitmap存储,为了在极小空间存储更多数据,不同状态的对象用不同标志位表示不同存储内容 类型指针,即对象指向它的类型元数据的指针,JVM通过该指针来确定对象是哪个类的实例。若对象是数组,还必须记录数组长度 实例数据,包括父类继承下来的,和子类中定义的字段 对齐填充,HotSpot要求对象大小必须是8字节的整数倍,不够就对齐填充 2.3.3 对象的访问定位 通过栈上的reference数据来访问堆上的具体对象,访问方式由虚拟机实现决定,主流有两种 句柄访问。Java堆会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自具体的地址信息。优点是对象被移动时只需要改变句柄中的实例数据指针 直接指针访问。reference存储的就是对象地址,类型数据指针在对象中。优点是节省一次指针定位的时间开销,HotSpot使用此方式来访问对象 2.4 实现:OutOfMemoryError异常 2.4.1 Java堆溢出 /** * VM options:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * @author panjiahao.cs@foxmail.com * @date 2023/6/18 19:54 */ public class HeapOOM { static class OOMObject{ } public static void main(String[] args) { ArrayList<OOMObject> list = new ArrayList<>(); while(true){ list.add(new OOMObject()); } } } 排查思路:首先确认导致OOM的对象是否是必要的,也就是是分清楚是内存泄露了还是内存溢出了 如果是内存泄露了,可以通过工具查看泄露对象到GC Roots的引用链,定位到产生内存泄露的代码的具体位置 如果是内存溢出了,可以调大堆空间,优化生命周期过长的对象 2.4.2 虚拟机栈和本地方法栈溢出 HotSpot不区分虚拟机栈和本地方法栈,所以-Xoss(本地方法栈大小)参数没效果,栈容量由-Xss参数设定。 当线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;当扩展栈容量无法申请到足够内存,将抛出OutOfMemoryError异常。HotSpot不支持扩展栈容量 2.4.3 方法区和运行时常量池溢出 JDK8使用元空间取代了永久代,运行时常量池移动到了堆中,所以可能会产生堆内存的OOM 方法区的主要职责是存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。用CGLib不断生成增强类,可能产生元空间的OOM /** * VM Args:-XX:MaxMetaspaceSize=10M * @author panjiahao.cs@foxmail.com * @date 2023/6/18 22:13 */ public class JavaMethodAreaOOM { public static void main(String[] args) { while(true){ Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(HeapOOM.OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object obj, Method method, Object[] objects, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj,args); } }); enhancer.create(); } } static class OOMObject{ } } 2.4.4 本机直接内存溢出 直接内存通过-XX:MaxDirectMemorySize参数来指定,默认与Java堆最大值一致 虽然使用DirectByteBuffer分配内存会OOM,但它抛出异常时并没有真正向操作系统申请内存,而是通过计算得知无法分配就手动抛异常,真正申请内存的方法是Unsafe::allocateMemory() /** * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * @author panjiahao.cs@foxmail.com * @date 2023/6/19 20:38 */ public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws IllegalAccessException { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } } } 由直接内存导致的内存溢出,一个明显的特征是Heap Dump文件不会看到有什么明显的异常情况。如果内存溢出后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(NIO),可以重点检查直接内存 3. 垃圾收集器与内存分配策略 3.1 概述 线程独占的程序计数器、虚拟机栈、本地方法栈3个区域的内存分配和回收都具备确定性。 而Java堆和方法区有显著的不确定性:一个接口的多个实现类需要的内存可能不一样,一个方法不同分支需要的内存也不同,只有处于运行期间,才知道程序会创建哪些对象,这部分内存的分配和回收是动态的 3.2 判断对象是否存活的算法 引用计数法。看似简单,但必须配合大量额外处理才能正确工作,譬如简单的引用计数法无法解决对象循环引用 可达性分析算法。从GC Roots对象开始,根据引用关系向下搜索,走过的路径称为“引用链”,引用链上对象仍然存活,不在引用链上的对象可回收。 GC Roots对象包括以下几种: 虚拟机栈中引用的对象。例如方法参数、局部变量等 方法区中类静态属性引用的对象。例如Java类的引用类型静态变量 方法区中常量引用的对象。例如字符串常量池里的引用 本地方法栈JNI引用的对象 Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)、还有系统类加载器 所有被同步锁(synchronized)持有的对象 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等 根据用户选用的垃圾收集器以及回收区域,临时加入其它对象。目的是当某个区域垃圾回收时,该区域的对象也可能被别的区域的对象引用 引用包含以下几种类型: 强引用:被强引用引用的对象不会被回收 软引用:被软引用引用的对象在OOM前会被回收 弱引用:被弱引用引用的对象在下一次垃圾回收时被回收 虚引用:虚引用不会影响对象的生存时间,唯一目的是能在对象被回收时收到一个系统通知 即便对象已经不可达,也不是立即标记为可回收,对象真正死亡要经历两次标记过程:可达性分析发现不可达就第一次标记;如果对象重写了finalize()方法且没过JVM调用过,那么该对象会被放到队列中,由Finalizer线程去执行finalize()方法,这是对象自救的最后一次机会,只要重新与引用链上任意对象建立关联就行,譬如把this赋给某个对象的成员变量,第二次标记时就会被移除“即将回收”集合 /** * 此代码演示了两点: * 1.对象可以在被GC时自我拯救。 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次 * @author panjiahao.cs@foxmail.com * @date 2023/6/20 21:52 */ public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive(){ System.out.println("yes, i am still alive"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed! :)"); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws InterruptedException { SAVE_HOOK = new FinalizeEscapeGC(); SAVE_HOOK = null; System.gc(); Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no ,i am dead! :("); } // 自救失败 SAVE_HOOK = null; System.gc(); Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no ,i am dead! :("); } } } 方法区没有强制要垃圾回收,例如JDK11的ZGC不支持类卸载。 方法区主要回收两部分:废弃的常量和不再使用的类型。 回收废弃常量和回收堆中的对象非常类似 回收“不再使用的类”需要满足三个条件: 该类所有实例已被回收,包括子类实例 加载该类的类加载器已被回收,这个条件很难 该类对象的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 3.3 垃圾收集算法 从如何判定对象消亡的角度出发,GC算法可以划分为“引用计数式”和“追踪式”,这两类也被称作“直接垃圾收集”和“间接垃圾收集”。本节介绍追踪式垃圾收集 3.3.1 分代收集理论 分代收集理论建立在两个假说上: 弱分代假说:绝大多数对象都是朝生夕灭的 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡 分代收集在遇到对象之间存在跨代引用时需要遍历其它代的所有对象来确定是否还存在跨代引用,性能负担大,所以给分代收集理论添加第三个假说: 跨代引用假说:跨代引用相对于同代引用来说占极少数 分代收集的名词定义: 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为: 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS会有单独收集老年代的行为 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1会有这种行为 整堆收集(Full GC):收集整个Java堆和方法区的垃圾 3.3.2 标记-清除算法 标记所有需要回收的对象,标记完成后,统一回收 缺点:执行效率不稳定;内存空间碎片化 3.3.3 标记-复制算法 将内存划分为大小相等的两块,每次只使用一块。当这一块的内存用完了,就将还存活着的对象复制到另一外上面,然后再把已使用过的内存空间一次性清理掉 优点:对于多数对象都是可回收的情况,算法复制开销小;没有碎片 缺点:可用内存小了一半;需要空间担保 1:1划分新生代太浪费空间,HotSpot将新生代划分成Eden:Survivor0:Survivor0 = 8:1:1,每次可以用Eden和一块Survivor,垃圾回收时把存活对象写到另一块Survivor,然后清理掉Eden和已用过的Survivor,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖老年代进行分配担保 3.3.4 标记-整理算法 标记所有存活的对象,然后移动到内存空间一端,清理掉边界以外的内存 优点:解决了标记-清除算法造成的空间碎片化问题 缺点:整理期间,用户应用程序暂停,这段时间被称为“Stop The World” 整理即移动对象,移动则内存回收时会更复杂,不移动则内存分配会更复杂。从GC停顿时间来看,不移动对象停顿时间短;从吞吐量来看,移动对象更划算。 HotSpot里关注吞吐量的Parallel Scavenge收集器采用标记-整理算法,关注延迟的CMS收集器采用标记-清除算法 3.4 HotSpot的算法细节实现 3.4.1 根节点枚举 固定作为GC Root的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表),Java程序越来越庞大,逐个作为起点进行可达性分析会消耗大量时间 目前,所有收集器在根节点枚举时和整理内存碎片一样必须暂停用户线程,可达性分析算法可以和用户线程一起并发。为了降低STW时间,在不扫描全部的GC Root节点情况下,得知哪些地方存在对象引用,HotSpot提供了OopMap的数据结构保存引用的位置信息 3.4.2 安全点 OopMap可以帮助HotSpot快速完成GC Root枚举,但是如果为每条改变OomMap内容的指令都生成对应的OopMap,会需要大量额外存储空间 因此HotSpot没有为每条指令都生成OopMap,只在特定位置生成,这些位置称为安全点。用户程序只有在执行到安全点才能停顿下来垃圾回收。 安全点的选取考虑:不能太少以至于让收集器等待时间太长,也不能太频繁以至于增大内存负担 如何在垃圾回收时让所有线程(不包括执行JNI调用的线程)都跑到最近的安全点,然后停顿下来。有两种方案: 抢先式中断:先把用户线程全部中断,如果发现有用户线程不在安全点上就恢复这个线程,过一会再中断直至它跑到安全点。现在几乎不使用 主动式中断:设置一个标志位,各个线程执行时主动轮询这个标志,一旦为真主动中断挂起。HotSpot使用内存保护陷阱的方式将轮询操作精简至只有一条汇编指令 3.4.3 安全区域 当用户线程处于Sleep或者Blocked状态时不能执行到安全点。针对这种情况,引入安全区域。 安全区域是指在某一段代码片段中,引用关系不会发生变化,在这个区域中任意地方开始GC都是安全的。 当用户线程执行到安全区域时,首先标识自己进入了安全区域,这样在GC时,虚拟机就不会去管这些已标识自己进入安全区域的线程。当线程离开安全区域时,它检查虚拟机是否完成了根节点枚举(或者其它需要暂停用户线程的阶段),如果完成了就继续执行,否则等待直到收到可以离开安全区域的信号为止。 3.4.4 记忆集与卡表 记忆集是一种记录从非收集区域指向收集区域的指针集合的抽象数据结构,用于解决对象跨代引用带来的问题 记忆集最简单的实现是非收集区域中所有含跨代引用的对象数组,这种结构浪费太多空间,可以粗化记录粒度: 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如32或64),该字包含跨代指针 卡精度:每个记录精确到一块内存区域,该区域有对象含有跨代指针 卡精度使用卡表实现,卡表的实现是一个字节数组,字节数组每个元素都对应着一块特定大小的内存块,称为卡页。只要卡页内有一个或更多对象的字段存在跨代引用,卡表中对应的数组元素的值标识为1,称为变脏。垃圾收集时,只要筛选出卡表中变脏的元素就可以知道哪些卡页内存块有跨代引用,把它们放入GC Roots中一起扫描 3.4.5 写屏障 卡表元素变脏的时间点是引用类型字段赋值那一刻,HotSpot通过写屏障来维护卡表状态。写屏障可以看作JVM层面对“引用类型字段赋值”这个动作的AOP切面。 写屏障会导致伪共享问题,伪共享是指,现代CPU的缓存系统是以缓存行为单位存储的,当多线程修改相互独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低 伪共享的一种简单解决方法是不采用无条件的写屏障,而是先检查卡表标记,只有卡表元素未被标记时才将其变脏。HotSpot参数-XX:+UseCondCardMark决定是否开启卡表更新,开启会多一次判断开销,但能够避免伪共享带来的性能损耗 3.4.6 并发的可达性分析 可达性分析在标记阶段会暂停用户线程以在一致性的快照上进行对象图的遍历,不一致情况下会出现“对象消失”问题。原因可以由三色标记方法推导 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚开始时,所有对象都是白的,若在分析结束阶段,仍然是白色的对象是不可达 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色对象代表已经扫描过,是安全存活的,如果有其它对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过 当且仅当以下两个条件同时满足时,会产生“对象消失”问题,即原本应该黑色的对象被误标为白色: 赋值器插入了一条或多条从黑色对象到白色对象的新引用 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用 解决并发扫描时的对象消失问题,只需破坏两个条件之一即可,因此有两种方案: 增量更新。增量更新破坏条件一,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束后,再以这个黑色对象为根,重新扫描一次。可以理解为,黑色对象一旦新插入指向白色对象的引用之后,它就变回灰色对象 原始快照。当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。可以理解为,无论引用关系删除与否,都会按照刚开始扫描那一刻的对象图快照进行搜索 对引用关系记录的插入和删除都是通过写屏障实现。 3.5 经典垃圾收集器 图中连线表示可以搭配使用 3.5.1 Serial收集器 适合资源(cpu和内存)受限的场景,新生代一两百兆以内的垃圾收集停顿时间最多一百多毫秒以内 3.5.2 ParNew收集器 实质是多线程版的Serial 3.5.3 Parallel Scavenge收集器 与ParNew类似,不同点是其它收集器关注停顿时间,Parallel Scavenge收集器目标是可控制的吞吐量 -XX:MaxGCPauseMillis控制最大垃圾收集停顿时间,-XX:GCTimeRatio直接设置吞吐量,-XX:+UseAdaptiveSizePolicy会根据系统运行情况动态调整虚拟机参数以获得最合适的停顿时间或者最大的吞吐量 3.5.4 Serial Old收集器 Serial收集器的老年代版本 3.5.5 Parallel Old收集器 Parallel Scavenge的老年代版本,在注重吞吐量或者处理器资源稀缺场合,可以考虑使用Parallel Scavenge+Parallel Old 3.5.6 CMS收集器 四步骤: 1)初始标记 2)并发标记 3)重新标记 4)并发清理 初始标记和重新标记仍然要暂停用户线程。初始标记仅仅标记GC Roots能直接关联的对象,速度很快;并发标记从关联对象开始遍历整个对象图,不需要暂停用户线程;重新标记用来修正并发标记期间的变动(增量更新) 优点:并发收集、低停顿 缺点: 对处理器资源敏感。并发会占用处理器,降低吞吐量 无法处理“浮动垃圾”。“浮动垃圾”指并发标记和清理期间用户线程产生的垃圾在下一次GC时清理,预留空间不足会,预留空间不足会导致“Concurrent Mode Failure”进而导致Full GC或者Serial Old重新回收老年代。 内存空间碎片化 3.5.7 Garbage First(G1)收集器 开创面向局部收集的设计思路和基于Region的内存布局形式 局部收集只收集范围不是新生代或老年代,而是堆中任意部分。 基于Region的堆内存布局:G1不再坚持固定大小和数量的分代区域划分,而是把连续的堆划分为多个大小相等的独立区域Region,每个Region都可以根据需要扮演新生代或者老年代。超过Region容量一半的对象会被存到Humongous区域中,超过整个Region容量的对象会被存到N个连续的Humongous Region中,G1大多数行为把Humongous Region当老年代处理 四个步骤: 初始标记:标记GC Roots能直接关联的对象,修改TAMS指针的值,让下一阶段用户并发运行时能正确在可用的Region中分配新对象 并发标记:递归扫描对象图。扫描完成后重新处理SATB记录下的在并发时有引用变动的对象 最终标记:处理并发阶段遗留的少量SATB记录 筛选回收:负责更新Region统计数据,制定回收计划 3.6 低延迟垃圾收集器 垃圾收集器“不可能三角”:内存占用、吞吐量、延迟。延迟是最重要指标 下图,浅色表示挂起用户线程,深色表示gc线程和用户线程并发 Shenandoah和ZGC在可管理的堆容量下,停顿时间基本是固定的,两者被命名为低延迟垃圾收集器 3.6.1 Shenandoah收集器 目标:能在任何堆内存大小下都可以把GC停顿时间限制在10ms以内 Shenandoah和G1有相似的堆内存布局,在初始标记、并发标记等阶段的处理思路高度一致。 有三个明显的不同: 支持并发的整理算法 默认不分代,即不使用专门的新生代Region和老年代Region 放弃G1中耗费大量内存和计算资源去维护的记忆集,改为“连接矩阵”记录跨Region的引用关系 收集器的工作分为九个阶段: 初始标记:与G1一样,首先标记GC Roots直接关联的对象,停顿时间与堆大小无关,与GC Roots数量有关 并发标记:与G1一样,遍历对象图,与用户线程并发,时间取决于对象数量和对象图的结构复杂程度 最终标记:与G1一样,处理剩余的SATB扫描,统计价值最高的Region组成回收集,有一小段停顿时间 并发清理:清理一个存活对象都没有的Region 并发回收:将回收集中的存活对象复制到未被使用的Region中,通过读屏障和“Brooks Pointers”转发指针解决和用户线程并发产生的问题 初始引用更新:一个非常短暂的停顿,用于确保所有并发回收阶段中收集器线程都已完成分配对象移动任务 并发引用更新:真正开始进行引用更新,与用户线程并发 最终引用更新:修正存在于GC Roots中的引用,停顿时间与GC Roots的数量有关 并发清理:经过并发回收和引用更新后,整个回收集的Region已经没有存货对象,回收这些Region的内存空间 对象移动和用户程序并发,原来的解决方案是在被移动对象原有的内存上设置保护陷阱,一旦用户程序访问到原有的地址就产生自陷中断,进入预设的异常处理器,将访问转发到新对象。这个方案会导致用户态频繁切换到核心态 Brooks Pointers是在原有对象布局结构的最前面加一个新的引用字段,移动前指向自己,移动后指向新对象。这里需要用CAS解决对象访问的并发问题 3.6.2 ZGC收集器 ZGC收集器是一款基于Region内存布局的,不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。收集过程全程可并发,短暂停顿时间只与GC Roots大小相关而与堆内存大小无关 ZGC的Region是动态的:动态创建和销毁、动态大小 染色指针:把标记信息记在引用对象的指针上,标记信息有4个bit,虚拟机可以直接从指针中看到其引用对 象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问到 三大优势: 染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用 掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的 目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以 便日后进一步提高性能 标志位影响了寻址地址,ZGC通过多重映射,将不同标志位的指针映射到同一内存区域 ZGC的收集过程分为四大阶段 并发标记:与G1、Shenandoah类似,区别是标记在指针上而不是对象上,标记会更新染色指针的Marked 0、Marked 1标志位 并发预备重分配:根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集 并发重分配:把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系 并发重映射:修正整个堆中指向重分配集中旧对象的所 有引用 3.7 选择合适的垃圾收集器 3.7.1 Epsilon收集器 不收集垃圾,负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责 响应微服务而生,在堆内存耗尽前就退出,不收集垃圾就非常合适 3.7.2 收集器的权衡 三个因素: 应用程序关注点是什么?吞吐量、延时还是内存占用 基础设施如何?处理器的数量、内存大小、操作系统 JDK的发行商是谁?版本号是多少 例如直接面向用户的B/S系统,延迟是主要关注点。 预算充足就用商业的 预算不足但能掌控基础设施,可以尝试ZGC 对ZGC的稳定性有疑虑就考虑Shenandoah 软硬件和JDK都老的考虑CMS","categories":[{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/categories/Java/"}],"tags":[{"name":"深入理解Java虚拟机","slug":"深入理解Java虚拟机","permalink":"https://zunpan.github.io/tags/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA/"}]},{"title":"Excel比对与合并系统","slug":"Excel比对与合并系统","date":"2023-03-03T07:00:03.000Z","updated":"2023-09-24T04:27:40.287Z","comments":true,"path":"2023/03/03/Excel比对与合并系统/","link":"","permalink":"https://zunpan.github.io/2023/03/03/Excel%E6%AF%94%E5%AF%B9%E4%B8%8E%E5%90%88%E5%B9%B6%E7%B3%BB%E7%BB%9F/","excerpt":"","text":"背景 许多游戏策划使用Excel来配置数值。策划需要保存所有版本的数值表,有时需要查看两个数值表有何差异,有时想把差异或者叫修改应用到另一张数值表中,这非常类似于版本控制,但是市面上的版本控制系统svn和git都是针对文本文件,不能用于Excel的版本控制 Excel比对算法 一维数据比对算法 假设有两个序列A1...AmA_1...A_mA1​...Am​和B1...BnB_1...B_nB1​...Bn​,我们可以通过对A序列进行一些列操作,使之变为B序列。对每种操作都定义个Cost,如何找到总Cost最小的使A变为B的操作序列,可以通过动态规划解决。这是一个已经被广为研究的算法问题,本文就不再整述,读者可以在网上搜索Edit编辑距离获取更多信息。 操作集合的定义有多种方式,一种较为常见的操作集合定义如下(Cost均为1) : 在序列中插入一个元素: 在序列中删除一个元素; 比如,将字符串kiten变换为sitting,需要删除k,插入s,删除e,插入i,在尾部插入g。如果在原序列和目标序列中去掉删除和插入的元素,那么原序列和目标序列就是完全相同的了(比如上面的例子两者都变为itn了),因此这种编辑距离被命名为LCS (Longest Common Subsequence) 编辑距离。LeetCode 1143. 最长公共子序列 再回到本文要讨论的差异比较问题,要比较两个序列的差异,实际上就是要找到二者之间尽量多的公共部分,剩下的就是差异部分,所以这和最短编辑距离问题是完全等价的。 此外,除了LCS编辑距离之外,还有一种常用的编辑距离,允许插入、删除和修改操作,叫做Levenshtein编组距离。另外,还可以定义一种广义的Levenshtein编辑距离,删除元素AiA_iAi​和插入元素BjB_jBj​;的Cost由一个单参数函数决定,记为Cost(AiA_iAi​)或Cost(BjB_jBj​); 将AiA_iAi​修改为BjB_jBj​;的操作的Cost由一个双参数函数决定,记为Cost2(Ai,BjA_i, B_jAi​,Bj​)。 /** * 比对的基本单位是单个字符 * @param text1 字符串1 * @param text2 字符串2 * @return levenshteinDP数组 */ static int[][] levenshteinDP(String text1, String text2) { int len1 = text1.length(); int len2 = text2.length(); // dp[i][j]表示从text1[0...i-1]到text2[0...j-1]的最小编辑距离(cost) dp = new int[len1 + 1][len2 + 1]; // path记录此方格的来源是多个此类枚举值的布尔或值 path = new int[len1 + 1][len2 + 1]; for (int i = 0; i < len1 + 1; i++) { dp[i][0] = i; path[i][0] = FROM_INIT; } for (int j = 0; j < len2 + 1; j++) { dp[0][j] = j; path[0][j] = FROM_INIT; } for (int i = 1; i < len1 + 1; i++) { for (int j = 1; j < len2 + 1; j++) { path[i][j] = FROM_INIT; int left = dp[i][j - 1] + 1; int up = dp[i - 1][j] + 1; int leftUp; boolean replace; if (text1.charAt(i - 1) == text2.charAt(j - 1)) { leftUp = dp[i - 1][j - 1]; replace = false; } else { leftUp = dp[i - 1][j - 1] + 1; replace = true; } dp[i][j] = Math.min(Math.min(left, up), leftUp); if (dp[i][j] == left) { path[i][j] |= FROM_LEFT; } if (dp[i][j] == up) { path[i][j] |= FROM_UP; } // 对应:两字符完全一样或者可以修改成一样 if (dp[i][j] == leftUp) { if (replace) { path[i][j] |= FROM_LEFT_UP_REPLACE; } else { path[i][j] |= FROM_LEFT_UP_COPY; } } } } return dp; } 同样的,对于广义Levenshtein编辑距离,如果去掉删除和插入的元素,那么两个序列中剩下的元素即为一一对应的关系,每组对应的两个元素,要么是完全相同的,要么前者是被后者修改掉的。从这部分论述中我们不难看出,比较算法的核心思路实际上就是找到元素与元素之间的一一对应关系 二维数据比对算法 Excel的一个分页可以认为是一个二维数据阵列,阵列中的每个元素是对应单元格内容的字符串值。根据前面的论述,比较两个二维阵列的核心就是找到他们公共的行/列,或者说原阵列和目标阵列的行/列对应关系。比如,对于下面两张表: 甲表的第1、2、3列对应乙表的1、2、4列,甲表的1、3行对应乙表的1、2行。那么这两张表的差异通过下列方式描述: 在第2列的位置插入新列 删除第2行 将(原表中) 第3行第3列的元素从9修改为0; 如何计算两张表对应的行/列,一个比较容易想到的方案是将其拆分为两个独立求解的问题,计算对应的行和计算对应的列。对于前者,我们可以把阵列的每一行当成一个元素,所有行组成一个序列,然后对这个序列进行比较:后者亦然。这样我们就把二维比较问题转化成了一维比较的问题。关于编辑距离的定义,可以采用广义Levenshtein编辑距离,定义删除、插入元素的Cost为该行(列)的元素数,定义修改元素的Cost为这两行(列)之间的LCS编辑距离.于是两个二维阵列的比较过程如下: 找到二者对应的 (或者叫公共的) 行/列,非公共的行/列记为删除、插入行/列操作;两张表只保留公共的行/列,此时他们尺寸完全相同,对应位置的单元格逐一比较,如果值不相同,则记为单元格修改操作; 算法优化 上一个部分介绍的二维阵列比较方案只是一个理论上可行的方案,在实际应用中,存在以下问题: 删除、插入行/列的操作都是对于整个行/列的,而计算两行/列之间的LCS编辑距离是独立计算的,因此算法本身有一定不合理性; 计算修改Cost里又包含了LCS编辑距离的计算,二层嵌套,性能开销比较大; 针对上述问题,从游戏开发的实际应用场景出发,做了如下优化: 首先计算列之间的对应关系,只取表头前h行(不同项目表头行数h可能不同,可以通过参数配置) ,这样就把对整列的LCS编辑距离计算优化为h个单元格逐已比较,大幅优化了效率,而且对准确度基本不会有什么影响; 根据上一步的计算结果,去掉非公共的列(即删除、添加的列),这样,剩下的列都是两边一一对应的了,此时再计算行的对应关系,修改操作的Cost定义就可以从LCS编辑距离改为单元格的逐一比较了,这样又大幅优化了性能 在上面所述基础之上,还可以再做优化,因为在实际应用中,绝大多数情况下,绝大多数行都不会有修改,因此可以先用LCS编辑距离对所有行的对应关系进行计算,即只有当两行内容完全相同时才会被当做是对应的; 然后再把剩下的未匹配的行分组用广义Levenshtein编辑距离进行对应关系匹配。这样么做的原因是LCS编辑距离比广义Levenshtein编辑距离的求解速度要快很多。 功能扩展 在开发过程中,我们经常会将单行或者连续若干行单元格上移或下移到另一位置,按照目前的比较逻辑,该操作会被认为是删除这些行,然后在新的位置重新插入这些行。这样的结果和不合理的。为此,我们可以引入一种新的操作: 移动行到另一位置。加入了这个新的操作之后,我们依然可以建立之前所述的行对应关系,只不过两边行的顺序可以是乱序的。这种不保序对应关系可以通过多个轮次的编辑距离匹配计算,每次匹配之后去掉匹配上的行,剩下未匹配的行组成一个新的序列进行下一轮的匹配。每轮匹配是采用LCS编辑距离还是广义Levenshtein编指距离可以灵活决定,比如前若干轮或者行数较多时采用LCS编辑距离,后面的轮次再用广义Levenshtein编辑距离。","categories":[{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/categories/Java/"}],"tags":[{"name":"LCS","slug":"LCS","permalink":"https://zunpan.github.io/tags/LCS/"},{"name":"Levenshtein","slug":"Levenshtein","permalink":"https://zunpan.github.io/tags/Levenshtein/"},{"name":"编辑距离","slug":"编辑距离","permalink":"https://zunpan.github.io/tags/%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB/"},{"name":"diff","slug":"diff","permalink":"https://zunpan.github.io/tags/diff/"},{"name":"merge","slug":"merge","permalink":"https://zunpan.github.io/tags/merge/"}]},{"title":"MySQL实战45讲学习笔记","slug":"MySQL实战45讲学习笔记","date":"2023-02-01T10:57:05.000Z","updated":"2023-09-24T04:27:40.280Z","comments":true,"path":"2023/02/01/MySQL实战45讲学习笔记/","link":"","permalink":"https://zunpan.github.io/2023/02/01/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","excerpt":"","text":"基础架构:一条SQL查询语句是如何执行的 MySQL可以分为Server层和存储引擎层两部分 Server层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。 而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎 连接器 连接器负责跟客户端建立连接、获取权限、维持和管理连接。建立连接后,权限修改不会影响已经存在的连接的权限 长连接:连接成功后,如果客户度持续有请求,一直使用同一个连接 短连接:每次执行很少的几次查询就断开连接,下次查询再重新建立 建立连接比较耗时,尽量使用长连接,但是全部使用长连接会导致OOM,因为MySQL在执行过程中临时使用的内存是管理在连接对象里面,连接不断开内存不会释放 解决方案: 定期断开长连接 执行mysql_reset_connection重新初始化连接资源 查询缓存 执行过的语句及其结果可能会以key-value对的形式,直接缓存在内存中 查询缓存的失效非常频繁,只要有对一个表的更新,这个表上的所有查询缓存都会被清空,因此不要使用查询缓存,MySQL 8.0删掉了此功能 分析器 分析器先做“词法分析”,识别出SQL语句中的字符串分别是什么,例如,识别“select”是查询语句,“T”识别成表名T 然后做“语法分析”,判断输入的SQL语句是否满足MySQL语法,如果语句不对,会收到“You have an error in your SQL syntax”的错误提醒 优化器 优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序 例如下面的语句执行两个表的join: mysql> select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20; 既可以先从表t1里面取出c=10的记录的ID值,再根据ID值关联到表t2,再判断t2里面d的值是否等于20。 也可以先从表t2里面取出d=20的记录的ID值,再根据ID值关联到t1,再判断t1里面c的值是否等于10。 这两种执行方法的结果一样但效率不同,优化器决定使用哪个方法 执行器 判断有没有表的执行权限 根据表的引擎定义调用引擎提供的接口,例如“取满足条件的第一行”,“满足条件的下一行”,数据库的慢查询日志rows_examined字段表示语句在执行过程中扫描了多少行,引擎扫描行数跟rows_examined并不是完全相同的 日志系统:一条SQL更新语句是如何执行的 一条查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块,最后到达存储引擎。更新语句还涉及 redo log(重做日志)和 binlog(归档日志) redo log WAL(Write-Ahead Logging):更新记录时,InnoDB引擎会先把记录写到redo log里面并更新内存,然后在适当的时候将操作记录更新到磁盘里面 InnoDB的redo log是固定大小和循环写的,write pos是当前记录的位置,checkpoint是当前要擦除的位置,擦除记录前要把记录更新到数据文件 redo log保证即使数据库异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe binlog redo log是InnoDB引擎特有的日志,Server层特有的引擎是binlog(归档日志) 两者有三点不同 redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用 redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1” redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志 执行器和InnoDB引擎在执行update语句时的内部流程 执行器先找引擎取ID=2这一行。ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。 执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。 执行器生成这个操作的binlog,并把binlog写入磁盘。 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。 下图的浅色框表示在InnoDB内部执行,深色框表示在执行器中执行 两阶段提交 redo log的写入分为两步骤:prepare和commit,也就是”两阶段提交“,目的是为了让两份的日志之间的逻辑一致 当数据库需要恢复到指定的某一秒时,可以先在临时库上这样做: 找到最近的一次全量备份 从备份的时间点开始,将备份的binlog依次取出来重放到指定时间 如果redo log不是两阶段提交 先写redo log后写binlog。假设在redo log写完,binlog还没写完,MySQL异常重启,数据可以恢复,但是binlog没有记录这个语句。之后用binlog恢复临时库时会缺少更新 先写binlog后写redo log。假设binlog写完,redo log还没写,MySQL异常重启之后,这个事务无效,数据没有恢复。但是binlog里面已经有这个语句,所以之后用binlog恢复临时库会多一个事务 innodb_flush_log_at_trx_commit这个参数设置成1的时候,表示每次事务的redo log都直接持久化到磁盘。这个参数建议设置成1,这样可以保证MySQL异常重启之后数据不丢失。 sync_binlog这个参数设置成1的时候,表示每次事务的binlog都持久化到磁盘。建议设置成1,这样可以保证MySQL异常重启之后binlog不丢失。 如果redo log处于prepare状态且binlog写入完成,MySQL异常重启会commit掉这个事务 事务隔离 事务保证一组数据库操作要么全部成功,要么全部失败 隔离性与隔离级别 ACID中的I指的是隔离性(Isolation) 当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。 事务隔离级别包括: 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到 读提交:一个事务提交之后,它做的变更才会被其他事务看到 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其它事务也是不可见的 串行化:对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成才能继续执行 数据库使用视图来实现隔离级别。在“可重复读”隔离级别下,视图是在事务开启时创建的。在“读提交”隔离级别下,视图是在每个SQL语句开始执行的时候创建的。“读未提交”直接返回记录的最新值,没有视图概念。“串行化”直接用加锁的方式 事务隔离的实现 这里展开说明“可重复读” 在MySQL中,每条记录在更新时都会同时记录一条回滚操作。假设一个值从1按顺序改成了2、3、4,在回滚日志里有类似下面的记录 当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。视图A、B、C对应的值分别是1、2、4,同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC)。对于视图A,要得到1,就必须将当前值依次执行图中所有的回滚操作。即使现在有另一个事务正在将4改成5,这个事务跟视图A、B、C对应的事务是不会冲突的。 系统会将没有比回滚日志更早的read-view时删掉这个回滚日志。因此尽量不要使用长事务,系统里面会存在很老的事务视图 事务的启动方式 MySQL的事务启动方式有如下几种: 显式启动事务,begin 或 start transaction。配套的提交语句是commit,回滚语句是rollback 隐式启动事务,一条SQL语句会自动开启一个事务。需要设置autocommit = 1 才会自动提交 set autocommit=0,会将这个线程的自动提交关掉。事务持续存在直到你主动执行commit 或 rollback语句,或者断开连接 建议使用set autocommit=1,并显示启动事务 在autocommit为1的情况下,用begin显式启动的事务,如果执行commit则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行begin语句的开销 可以在information_schema库的innodb_trx这个表中查询长事务,比如下面这个语句,用于查找持续时间超过60s的事务。 select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60 如何避免长事务对业务的影响? 从应用端来看 通过MySQL的general_log确保autocommit=1 包含多个select语句的只读事务,没有一致性要求就拆分 通过SET MAX_EXECUTION_TIME控制每个语句的最长执行时间 从数据库端来看 监控 information_schema.Innodb_trx表,设置长事务阈值,超过就kill 推荐使用Percona的pt-kill 业务测试阶段就输出所有general_log,分析日志提前发现问题 innodb_undo_tablespaces设置成2或更大的值 深入浅出索引(上) 索引的出现是为了提高数据查询的效率 索引的常见模型 哈希表,只适用于只有等值查询的场景,不适用于范围查询 有序数组在等值查询和范围查询场景中都非常优秀,但更新数据需要挪动数组元素,成本太高。只适用于静态存储引擎(数据不再变化) 平衡二叉查找树的时间复杂度是O(log(N)),但是算上每次访问节点的磁盘IO开销,查询非常慢。为了减少磁盘IO次数,出现了N叉树 InnoDB的索引模型 根据叶子节点内容,索引类型分为主键索引和非主键索引 主键索引(聚簇索引):叶子节点存的是整行数据 普通索引(二级索引):叶子结点存的是主键的值 基于主键索引和普通索引的查询的区别: 如果语句是select * from T where ID=500,即主键查询方式,则只需要搜索ID这棵B+树; 如果语句是select * from T where k=5,即普通索引查询方式,则需要先搜索k索引树,得到ID的值为500,再到ID索引树搜索一次。这个过程称为回表。 索引维护 在插入新记录时,B+树为了维护有序性会进行页分裂和页合并 自增主键 VS 业务字段主键 性能上:自增主键按序插入,不会触发叶子节点的分裂,而业务字段做主键往往不是有序插入,导致页分裂和页合并,性能差 存储空间上:主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。业务字段主键是身份证号(20字节)不如自增主键(4字节或8字节) 业务字段做主键的场景是: 只有一个索引 该索引必须是唯一索引 这就是典型的KV场景,直接将这个字段设置为主键 深入浅出索引(下) 覆盖索引 覆盖索引:索引的叶子节点可以直接提供查询结果,不需要回表 可以为高频请求建立联合索引起到覆盖索引的作用 最左前缀原则 索引项是按照索引定义里面出现的字段的顺序排序的。满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左N个字段,也可以是字符串索引的最左M个字符 索引内的字段顺序评估标准: 复用能力,如果该顺序的联合索引能少维护一个索引,那么该顺序优先使用 空间,如果必须维护联合索引和单独索引,那么给小字段单独索引,联合索引的顺序是(大字段,小字段) 索引下推 在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足的条件的记录,减少回表次数(联合索引在按最左匹配时碰到范围查询停止,索引下推可以对后面的索引字段做条件判断后再返回结果集) 全局锁和表锁 根据加锁的范围,MySQL里面的锁大致可以分成全局锁、表级锁和行锁三类 全局锁 全局锁是对整个数据库实例加锁,让整个库处于只读状态 Flush tables with read lock 全局锁的典型使用场景是不支持MVCC的引擎(MyISAM)的全库逻辑备份,如果所有表的引擎支持MVCC,可以在备份时开启事务确保拿到一致性视图(mysqldump加上参数-single-transaction) 让全库只读,另外一种方式是set global readonly = true,但仍然建议使用FTWRL,因为: readonly的值可能会用来做其它逻辑,比如判断是主库还是备库 FTWRL在客户端发生异常断开时,MySQL会自动释放全局锁,而readonly会一直保持 表级锁 表级锁有两种:表锁,元数据锁(meta data lock,MDL) 表锁 语法:lock tables … read/write 表锁会限制其它线程的读写,也会限定本线程的操作对象 例如,线程A执行lock tables t1 read, t2 write;,其它线程写t1、读写t2都会被阻塞,线程A只能执行读t1、读写t2,不能访问其它表 如果支持行锁,一般不使用表锁 元数据锁 MDL不需要显示使用,在访问表时会被自动加上,事务提交才释放,作用是保证读写的正确性 当对表做增删改查操作时,加MDL读锁;当对表结构变更时,加MDL写锁 读锁之间不互斥,因此可以有多个线程同时对一张表增删查改 读写锁之间、写锁之间是互斥的。因此如果有两个线程要同时给一个表加字段,其中一个要等另一执行完再执行 给表加字段的方式: kill掉长事务,事务不提交会一直占着MDL 在alter table语句设置等待时间,如果在等待时间内能拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句。后面重试这个过程 行锁 行锁是针对表中行记录的锁 两阶段锁 两阶段锁协议:在InnoDB事务中,行锁是在需要的时候加上的,但并不是不需要了就立刻释放,而是要等到事务结束才释放 如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放 死锁和死锁检测 死锁:并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源,导致这几个线程无限等待 死锁出现后有两种策略: 设置等待时间,修改innodb_lock_wait_timeout 发起死锁检测,主动回滚死锁链条中的某一个事务,让其他事务可以继续执行。innodb_deadlock_detect设置为on表示开启 第一种策略,等待时间太长,业务的用户接受不了,等待时间太短会出现误伤。所以一般用死锁检测 死锁检测有性能问题,解决思路有几种: 如果能确保业务一定不会出现死锁,可以临时把死锁检测关掉。这种方法存在业务有损的风险,业务逻辑碰到死锁会回滚重试,但是没有死锁检测会超时导致业务有损 控制并发程度。数据库Server层实现,对相同行的更新,在进入引擎之前排队 将一行改成逻辑上的多行。例如账户余额等于10行之和,扣钱时随机扣一行,这种方案需要根据业务逻辑做详细设计 详解事务隔离 假如有如下表和事务A、B、C mysql> CREATE TABLE `t` ( `id` int(11) NOT NULL, `k` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; insert into t(id, k) values(1,1),(2,2); "快照"在MVCC里是怎么工作的 快照不是整个数据库的拷贝。 InnoDB里每个事务都有一个唯一的transaction id,是事务开始时申请的,严格递增的。每行数据有多个版本,每次事务更新数据时,都会生成一个新的数据版本,并把transaction id赋给这个数据版本的事务id,记为row trx_id。某个版本的数据可以通过当前版本和undo log计算出来 在实现上,InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指的是启动了但还没提交 数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位 对于当前事务的启动瞬间来说,一个数据版本的row trx_id,有以下几种可能: 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的; 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的; 如果落在黄色部分,那就包括两种情况 a. 若 row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见; b. 若 row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。 InnoDB利用了“所有数据都有多个版本”的特性,实现了“秒级创建快照”的能力 可以用时间顺序来理解版本的可见性。 一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况: 版本未提交,不可见; 版本已提交,但是是在视图创建后提交的,不可见 版本已提交,而且是在视图创建前提交的,可见 更新逻辑 更新数据都是先读后写,读是当前值,称为“当前读”(current read)。所以事务B是在(1,2)上进行修改 select如果加锁,也是当前读,不加就是一致读,下面两个select语句分别加了读锁(S锁,共享锁)和写锁(X锁,排它锁)。行锁包括共享锁和排它锁 mysql> select k from t where id=1 lock in share mode; mysql> select k from t where id=1 for update; 假设事务C变成了事务C’ 事务C’还没提交,但是生成了最新版本(1,2),根据“两阶段协议”,(1,2)这个版本上的写锁还没释放,事务B的更新是当前读,需要加锁,所以被阻塞 可重复读的核心就是一致性读(consistent read);而事务更新数据时只能用当前读,如果当前的记录的行锁被其他事务占用的话,就进入锁等待。 读提交的逻辑和可重复读的逻辑类似,主要区别是: 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图; 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。 普通索引和唯一索引之间的选择 普通索引VS唯一索引:两者类似,区别是唯一索引的列值不能重复,允许一个为空 下面从这两种索引对查询语句和更新语句的性能来分析,前提是业务保证记录的唯一性,如果业务不能保证唯一性又有唯一需求,就必须用唯一索引 查询过程 普通索引:查找到满足条件的第一个记录后,需要查找下一个记录,直到碰到第一个不满足条件的记录 唯一索引:由于唯一性,查找到满足条件的第一个记录后就停止 由于InnoDB的数据是按数据页为单位来读写,所以两者性能差距微乎其微 更新过程 change buffer:当需要更新一个数据页时,如果数据页在内存中就直接更新,如果不在,在不影响数据一致性的前提下,InnoDB会将这些更新操作缓存在change buffer中。在下次查询需要访问这个数据页时,读入内存,然后执行change buffer中与这个页有关的操作,这个过程称为merge 唯一索引的更新不能用change buffer,因为需要先将数据页读入内存判断操作是否违反唯一性约束 假如现在有个插入新记录的操作,如果要更新的目标页在内存中,普通索引和唯一索引性能差距不大。如果目标页不在内存中,对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值;对于普通索引来说,将更新记录在change buffer,此时普通索引的性能好(主键索引的数据页是一定要加载进内存做更新操作,普通索引的数据页不用进内存) change buffer的使用场景 因为merge的时候是真正做数据更新的时候,在merge之前,change buffer记录的变更越多,收益越大 对于写多读少的业务,change buffer的效果最好,比如账单类、日志类的系统 索引的选择和实践 尽量使用普通索引 如果更新完马上查询,就关闭change buffer。否则开着能提升更新性能 change buffer 和 redo log redo log 主要节省的是随机写磁盘的IO消耗(转成顺序写),而change buffer主要节省的则是随机读磁盘的IO消耗。 MySQL为什么有时候会选错索引 session B 先删除了所有数据然后调用idata存储过程插入了10万行数据。 然后session B 执行三条SQL: // 将慢查询日志的阈值设置为0,表示这个线程接下来的语句都会被记录慢查询日志中 set long_query_time=0; select * from t where a between 10000 and 20000; /*Q1*/ select * from t force index(a) where a between 10000 and 20000;/*Q2*/ Q1走了全表扫描,Q2使用了正确的索引 优化器的逻辑 选择索引是优化器的工作,目的是寻找最优方案执行语句,判断标准包括扫描行数、是否使用临时表、是否排序等因素 上面查询语句没有涉及临时表和排序,说明扫描行数判断错误了 MySQL是怎么判断扫描行数的 MySQL在真正开始执行语句之前,并不能精确知道有多少行,而只能用统计信息估算。这个统计信息就是索引的“区分度”,索引上不同的值称为“基数”,基数越大,区分度越好。基数由采样统计得出。 如果统计信息不对导致行数和实际情况差距较大,可以使用analyze table 表名 来重新统计索引信息 索引选择异常和处理 由于索引统计信息不准确导致的问题,可以用analyze table来解决,其它优化器误判的解决方法如下: 使用force index强行选择索引。缺点是变更不及时,开发通常不写force index,当生产环境出现问题,再修改需要重新测试和发布 修改语句,引导MySQL使用我们期望的索引。缺点是需要根据数据特征进行修改,不具备通用性 新建更合适的索引或删掉误用的索引。缺点是找到更合适的索引比较困难 怎么给字符串字段加索引 可以给字符串字段建立一个普通索引,也可以给字符串前缀建立普通索引。使用前缀索引,定义好长度,就可以既节省空间,又不用额外增加太多查询成本 可以通过统计索引上有多少个不同的值来判断使用多长的前缀,不同值越多,区分度越高,查询性能越好 首先算出这列有多少不同值 mysql> select count(distinct email) as L from SUser; 然后选取不同长度的前缀来看这个值 mysql> select count(distinct left(email,4))as L4, count(distinct left(email,5))as L5, count(distinct left(email,6))as L6, count(distinct left(email,7))as L7, from SUser; 前缀索引对覆盖索引的影响 前缀索引可能会增加扫描行数,导致影响性能,还可能导致用不上覆盖索引对查询的优化。 前缀索引的叶子节点只包含主键,如果查询字段不仅仅有主键,那必须回表。而用完整字符串做索引,如果查询字段只有主键和索引字段,那就不用回表 其它方式 对于邮箱来说,前缀索引的效果不错。 但是对于身份证来说,可能需要长度12以上的前缀索引,才能满足区分度要求,但是这样又太占空间了 有一些占用空间更小但是查询效率相同的方法: 倒序存储身份证号,建立长度为6的前缀索引,身份证后6位可以提供足够的区分度 加个身份证的整型hash字段,给这个字段加索引 这两种方法的相同点是都不支持范围查询,区别在于: 倒序存储不占额外空间 倒序每次写和读都需要额外调用一次reverse函数,hash字段需要额外调用一次crc32函数,reverse稍优 hash字段的查询性能更稳定一些 为什么MySQL为“抖”一下 “抖”:SQL语句偶尔会执行特别慢,且随机出现,持续时间很短 当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。 平时执行很快的更新操作,其实就是在写内存和日志,“抖”可能是在刷脏页(flush),情况有以下几种: redo log满了,系统会停止所有更新操作,把checkpoint往前推进,原位置和新位置之间的所有脏页都flush到磁盘上。尽量避免这种情况,会阻塞更新操作 系统内存不足,淘汰脏页。尽量避免一个查询要淘汰的脏页太多 系统空闲 正常关闭 InnoDB刷脏页的控制策略 使用fio测试磁盘的IOPS,并把innodb_io_capacity设置成这个值,告诉InnoDB全力刷盘可以刷多快 fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest 不能总是全力刷盘,InnoDB刷盘速度还要参考内存脏页比例和redo log写盘速度 脏页比例不要经常接近75%,查看命令如下: mysql> select VARIABLE_VALUE into @a from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty'; select VARIABLE_VALUE into @b from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_total'; select @a/@b; 还有个策略是刷盘的“连坐”机制:脏页的邻居如果是脏页会一起被刷盘。这种策略对机械硬盘有大幅度性能提升,但是SSD的IOPS已不是瓶颈,推荐innodb_flush_neighbors设置成0,只刷自己 为什么表数据删掉一半,表文件大小不变 InnoDB表包含两部分:表结构定义和数据。MySQL 8.0 之前,表结构是存在以.frm为后缀的文件,MySQL 8.0 允许表结构定义放在系统数据表中 参数innodb_file_per_table 设置成OFF表示,表的数据放在系统共享表空间,也就是跟数据字典放在一起; 设置成ON表示,每个InnoDB表数据存储在一个以.ibd为后缀的文件中 从MySQL 5.6.6开始默认就是ON 数据删除流程 InnoDB里的数据都是用B+数的结构组织的。 记录的复用:删除R4记录时,InnoDB会把记录标记为删除,插入ID在300到600之间的记录时可能会复用这个位置,磁盘文件大小不会缩小 数据页的复用:InnoDB的数据是按页存储的。如果将page A上所有记录删除以后,page A会被标记为可复用,这时候插入ID=50的记录需要使用新页时,page A会被复用。因此,delete整个表会把所有数据页都标记为可复用,但是磁盘文件不会变小 可以复用,而没被使用的空间,看起来就像是“空洞”,不只是删除数据会造成空洞,随机插入数据会引发索引的数据页分裂,导致空洞。更新索引上的值,可以理解为删除旧值和插入新值,也会造成空洞。解决空洞的方法是重建表 重建表 可以使用alter table A engine=InnoDB命令来重建表。MySQL 5.6是离线重建,重建期间更新会丢失。 MySQL 5.6 引入了Online DDL,重建表的流程: 建立一个临时文件,扫描表A主键的所有数据页 用数据页中表A的记录生成B+数,存储到临时文件中 生成临时文件的过程中,将所有对A的操作记录在一个日志文件(row log)中,对应图中state2状态 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表A相同的数据文件,对应图中state3状态 用临时文件替换表A的数据文件 alter语句在启动时需要获取MDL写锁,这个写锁在真正拷贝数据之前就退化成读锁了,目的是实现Online,MDL读锁不会阻塞记录的增删改操作(DML) 推荐使用gh-ost做大表的重建 Online 和 inplace inplace是指整个DDL过程在 InnoDB 内部完成,对于 Server层来说,没有把数据挪动到临时表,这是一个“原地”操作,这就是inplace名称由来 和inplace对应的是copy,也就是前面离线重建 DDL过程如果是 Online 的,就一定是inplace的;反过来未必,全文索引和空间索引是 inplace 的,但不是 Online 的 optimize table、analyze table和alter table三种方式重建表的区别: 从MySQL 5.6开始,alter table t engine=InnoDB(也就是recreate)默认就是上面引入Online DDL后的重建过程 analyze table t 不是重建表,只是对表的索引信息做重新统计,没有修改数据,这个过程中加了MDL读锁 optimize table t 等于recreate+analyze count(*)慢该怎么办 count(*)的实现方式 InnoDB count(*)会遍历全表,优化器会找到最小的索引数进行计数,结果准确但有性能问题。show table status命令显示的行数是采样估算的,不准确 用缓存系统保存计数 可以用Redis来保存记录数,但是会出现逻辑上不精确的问题。根本原因是这两个不同的存储构成的系统,不支持分布式事务,无法拿到精确一致的视图 这种情况是Redis的计数不精确 这种情况是查询结果不精确 在数据库保存计数 将计数放在数据库里单独的一张计数表中,可以利用事务解决计数不精确的问题 在会话B读操作期间,会话A还没提交事务,因此B没有看到计数值加1的操作,因此计数值和“最近100条记录”的结果在逻辑上是一致的 不同的count用法 count(*)、count(主键id)和count(1) 都表示返回满足条件的结果集的总行数。count(字段),则表示返回满足条件的数据行里面,参数“字段”不为NULL的总个数 对于count(主键id)来说,InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为空的,就按行累加。 对于count(1)来说,InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。 count(1)执行得要比count(主键id)快。因为从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。 对于count(字段)来说: 如果这个“字段”是定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加; 如果这个“字段”定义允许为null,那么执行的时候,判断到有可能是null,还要把值取出来再判断一下,不是null才累加。 count(*) 是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count(*)肯定不是null,按行累加。 结论是:按照效率排序的话, count(字段) < count(主键id) < count(1) ≈ count(*),所以尽量使用count(*)。 orderby是怎么工作的 假设有SQL语句 select city,name,age from t where city='杭州' order by name limit 1000; 全字段排序 如果要排序的数据量小于sort_buffer_size,排序就在内存中完成,否则外部排序(归并) rowid 排序 max_length_for_sort_data 是MySQL中专门控制用于排序的行数据的长度的参数,超过这个值就不会全字段排序,用rowid排序 全字段排序 VS rowid排序 如果内存够就用全字段排序,rowid排序回表多造成磁盘读,性能较差 并不是所有的order by语句都要排序的,如果建索引时就是有序的就不排 创建一个city和name的联合索引,查询过程如下: 还可以创建city、name和age的联合索引,这样就不用回表了 如何正确地显示随机消息 10000行记录如何随机选择3个 内存临时表 用order by rand()来实现这个逻辑 mysql> select word from words order by rand() limit 3; R:随机数,W:单词,pos:rowid,对于有主键的表,rowid就是主键ID,没有主键就由系统生成 原表->内存临时表:扫描10000行 内存临时表->sort_buffer:扫描10000行 内存临时表->结果集:访问3行 order by rand()使用了内存临时表,内存临时表排序的时候使用了rowid排序方法 磁盘临时表 当内存临时表大小超过了tmp_table_size时,如果使用归并排序,内存临时表会转为磁盘临时表,如果使用优先队列排序(排序+limit操作),且维护的堆大小不超过sort_buffer_size,则不会转为磁盘临时表 随机排序方法 取得整个表的行数,记为C 取得 Y = floor(C * rand()) 再用 limit Y,1 取得一行 取多个随机行就重复多次这个算法 mysql> select count(*) into @C from t; set @Y1 = floor(@C * rand()); set @Y2 = floor(@C * rand()); set @Y3 = floor(@C * rand()); select * from t limit @Y1,1; //在应用代码里面取Y1、Y2、Y3值,拼出SQL后执行 select * from t limit @Y2,1; select * from t limit @Y3,1; 或者优化一下,Y1,Y2,Y3从小到大排序,这样扫描的行数就是Y3 id1 = select * from t limit @Y1,1; id2 = select * from t where id > id1 limit @Y2-@Y1,1; id3 = select * from t where id > id2 limit @Y3 为什么逻辑相同的SQL语句性能差异巨大 对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器决定放弃走树搜索功能,但不是放弃索引,优化器可以选择遍历索引 隐式类型转换可能会触发上面的规则1 隐式字符编码转换也可能触发上面的规则1 为什么只查一行的语句也执行这么慢 查询长时间不返回 等MDL锁。通过查询sys.schema_table_lock_waits,可以找出造成阻塞的process id,把这个连接用kill杀掉 等flush。可能情况是有一个flush tables命令被别的语句堵住了,然后它又堵住了查询语句,可以用show processlist 查出并杀掉阻塞flush的连接 等行锁。通过查询sys.innodb_lock_waits 杀掉对应连接 查询慢 查询字段没有索引,走了全表扫描 事务隔离级别为可重复读,当前事务看不到别的事务的修改,但是别的事务执行了多次修改,当前事务在查询时要根据undo log查询到应该看到的值 幻读 幻读:一个事务在前后两次查询同一个范围时,后一次查询看到了前一次查询没有看到的行 在可重复读隔离级别下,普通的查询时快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现 幻读专指“新插入的行” 幻读的问题 语义被破坏 session A在T1时刻声明了,“我要把所有d=5的行锁住,不准别的事务进行读写操作”。session B和C破坏了这个语义 数据不一致。根据binlog克隆的库与主库不一致,原因是即使给所有记录都加上锁,新记录还是没上锁 解决幻读 间隙锁:锁住两行之间的间隙 在行扫描过程中,不仅给行加行锁,还给行间的间隙上锁 跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。 间隙锁和行锁合称next-key lock,左开右闭 间隙锁的引入,可能会导致同样的语句锁住更大的范围,影响并发度。 间隙锁只在可重复读隔离级别下才会生效 为什么只改一行的语句,锁这么多 加锁规则(可重复读隔离级别): 原则1:加锁的基本单位是next-key lock 原则2:查找过程中访问到的对象才会加锁 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止 假设有如下SQL语句 CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`) ) ENGINE=InnoDB; insert into t values(0,0,0),(5,5,5), (10,10,10),(15,15,15),(20,20,20),(25,25,25); 案例一:等值查询间隙锁 由于表t中没有id=7的记录 根据原则1,加锁单位是next-key lock,session A加锁范围就是(5,10]; 同时根据优化2,这是一个等值查询(id=7),而id=10不满足查询条件,next-key lock退化成间隙锁,因此最终加锁的范围是(5,10)。 所以,session B要往这个间隙里面插入id=8的记录会被锁住,但是session C修改id=10这行是可以的。 案例二:非唯一索引等值锁 这里session A要给索引c上c=5的这一行加上读锁。 根据原则1,加锁单位是next-key lock,因此会给(0,5]加上next-key lock 要注意c是普通索引,因此仅访问c=5这一条记录是不能马上停下来的,需要向右遍历,查到c=10才放弃。根据原则2,访问到的都要加锁,因此要给(5,10]加next-key lock。 但是同时这个符合优化2:等值判断,向右遍历,最后一个值不满足c=5这个等值条件,因此退化成间隙锁(5,10) 根据原则2 ,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么session B的update语句可以执行完成。 但session C要插入一个(7,7,7)的记录,就会被session A的间隙锁(5,10)锁住。 需要注意,在这个例子中,lock in share mode只锁覆盖索引,但是如果是for update就不一样了。 执行 for update时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。 这个例子说明,锁是加在索引上的;同时,它给我们的指导是,如果你要用lock in share mode来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段。比如,将session A的查询语句改成select d from t where c=5 lock in share mode 案例三:主键索引范围锁 mysql> select * from t where id=10 for update; mysql> select * from t where id>=10 and id<11 for update; 这两条语句在逻辑上是等价的,但是加锁规则不一样 开始执行的时候,要找到第一个id=10的行,因此本该是next-key lock(5,10]。 根据优化1, 主键id上的等值条件,退化成行锁,只加了id=10这一行的行锁 范围查找就往后继续找,找到id=15这一行停下来,因此需要加next-key lock(10,15] 所以,session A这时候锁的范围就是主键索引上,行锁id=10和next-key lock(10,15] 需要注意一点,首次session A定位查找id=10的行的时候,是当做等值查询来判断的,而向右扫描到id=15的时候,用的是范围查询判断 案例四:非唯一索引范围锁 这次session A用字段c来判断,加锁规则跟主键索引范围锁的唯一不同是:在第一次用c=10定位记录的时候,索引c上加了(5,10]这个next-key lock后,由于索引c是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终sesion A加的锁是,索引c上的(5,10] 和(10,15] 这两个next-key lock 案例五:唯一索引范围锁bug session A是一个范围查询,按照原则1的话,应该是索引id上只加(10,15]这个next-key lock,并且因为id是唯一键,所以循环判断到id=15这一行就应该停止了。 但是实现上,InnoDB会往前扫描到第一个不满足条件的行为止,也就是id=20。而且由于这是个范围扫描,因此索引id上的(15,20]这个next-key lock也会被锁上。 案例六:非唯一索引上存在“等值”的例子 现在插入一条新记录 mysql> insert into t values(30,10,30); delete语句加锁的逻辑和 select … for update是类似的,session A在遍历的时候,先访问第一个c=10的记录。同样地,根据原则1,这里加的是(c=5,id=5)到(c=10,id=10)这个next-key lock。 然后,session A向右查找,直到碰到(c=15,id=15)这一行,循环才结束。根据优化2,这是一个等值查询,向右查找到了不满足条件的行,所以会退化成(c=10,id=10) 到 (c=15,id=15)的间隙锁。 也就是说,这个delete语句在索引c上的加锁范围,就是下图中蓝色区域覆盖的部分。蓝色区域左右两边都是虚线,表示开区间,即(c=5,id=5)和(c=15,id=15)这两行上都没有锁。 案例七:limit 语句加锁 session A的delete语句加了 limit 2。你知道表t里c=10的记录其实只有两条,因此加不加limit 2,删除的效果都是一样的,但是加锁的效果却不同。可以看到,session B的insert语句执行通过了,跟案例六的结果不同。这是因为,案例七里的delete语句明确加了limit 2的限制,因此在遍历到(c=10, id=30)这一行之后,满足条件的语句已经有两条,循环就结束了。 因此,索引c上的加锁范围就变成了从(c=5,id=5)到(c=10,id=30)这个前开后闭区间,如下图所示: 这个例子对我们实践的指导意义就是,在删除数据的时候尽量加limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。 案例八:一个死锁的例子 本案例目的是说明:next-key lock 实际上是间隙锁和行锁加起来的结果 session A 启动事务后执行查询语句加lock in share mode,在索引c上加了next-key lock(5,10] 和间隙锁(10,15); session B 的update语句也要在索引c上加next-key lock(5,10] ,进入锁等待; 然后session A要再插入(8,8,8)这一行,被session B的间隙锁锁住。由于出现了死锁,InnoDB让session B回滚 你可能会问,session B的next-key lock不是还没申请成功吗? 其实是这样的,session B的“加next-key lock(5,10] ”操作,实际上分成了两步,先是加(5,10)的间隙锁,加锁成功;然后加c=10的行锁,这时候才被锁住的。 也就是说,我们在分析加锁规则的时候可以用next-key lock来分析。但是要知道,具体执行的时候,是要分成间隙锁和行锁两段来执行的。 “饮鸩止渴”提高性能的方法 短连接风暴 短连接模式就是连接到数据库后,执行很少的SQL语句就断开,下次需要的时候再重连,在业务高峰期,会出现连接数暴涨的情况 两种有损业务的解决方法: 处理掉占着连接但是不工作的线程。优先断开事务外空闲太久的连接 减少连接过程的损耗。关闭权限验证 慢查询性能问题 引发慢查询的情况有三种: 索引没有设计好。最高效的解决方法是直接alter table建索引 SQL语句没有写好,导致没用上索引。解决方法是使用query_rewrite重写SQL语句 MySQL选错了索引。应急方案是给语句加上force index或者使用query_rewrite重写语句加上force index 出现情况最多的是前两种,通过下面过程可以预先发现和避免 上线前,在测试环境,把慢查询日志(slow log)打开,并且把long_query_time设置成0,确保每个语句都会被记录入慢查询日志 在测试表里插入模拟线上的数据,做一遍回归测试 观察慢查询日志里每类语句的输出,特别留意Rows_examined字段是否与预期一致 QPS突增问题 业务bug导致。可以把这个功能的SQL从白名单去掉 如果新功能使用的是单独的数据库用户,可以用管理员账号把这个用户删掉,然后断开连接 用query_rewrite把压力最大的SQL语句直接重写成"select 1"返回 方法3存在两个副作用: 如果别的功能也用到了这个SQL语句就会误伤 该语句可能是业务逻辑的一部分,导致业务逻辑一起失败 方法3是优先级最低的方法。方法1和2依赖于规范的运维体系:虚拟化、白名单机制、业务账号分离 MySQL是怎么保证数据不丢的 只要redo log和binlog保证持久化到磁盘,就能确保MySQL异常重启后,数据可以恢复 binlog的写入机制 binlog的写入逻辑:事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中 一个事务的binlog是不能被拆开的,因此不论这个事务多大,也要确保一次性写入 系统给binlog cache分配了一片内存,每个线程一个,参数binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘 事务提交的时候,执行器把binlog cache里的完整事务写入到binlog中,并清空binlog cache 图中的write,指的是把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快 图中的fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为fsync才占磁盘的IOPS write和fsync的时机由参数sync_binlog控制: sync_binlog=0,表示每次提交事务都只write,不fsync sync_binlog=1,表示每次提交事务都会fsync sync_binlog=N(N>1),表示每次提交事务都write,但累积N个事务后才fsync sync_binlog设置成N可以改善IO瓶颈场景的性能,但对应的风险是:如果主机发生异常重启,会丢失最近N个事务的binlog redo log的写入机制 事务执行过程中,生成的redo log要先写到redo log buffer,但不是每次生成后都要直接持久化到磁盘,因为事务没提交,日志丢了也不会有损失。 但是也有可能事务没有提交,redo log buffer 中的部分日志持久化到了磁盘。下图是redo log的三种状态 日志写到redo log buffer是很快的,write到page cache也快,但是持久化到磁盘就很慢。 InnoDB提供了innodb_flush_log_at_trx_commit参数来控制redo log的写入策略: 设置为0表示每次事务提交时都只是把redo log 留在redo log buffer中 设置为1表示每次事务提交时都将redo log直接持久化到磁盘 设置为2表示每次事务提交时都只是把redo log写到page cache InnoDB有个后台线程,每隔1秒,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘 注意,事务执行过程中的redo log也是直接写在redo log buffer中的,这些redo log也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的redo log,也可能已经持久化到磁盘 除了后台线程每秒一次的轮询操作外,还有两个场景会让没提交的事务的redo log写入到磁盘 redo log buffer占用的空间即将达到 innodb_log_buffer_size一半的时候,后台线程会主动write到page cache 并行的事务提交的时候,顺带将这个事务的redo log buffer持久化到磁盘 两阶段提交的过程,时序上redo log先prepare,再写binlog,最后再把redo log commit 如果innodb_flush_log_at_trx_commit设置为1,那么redo log在prepare阶段就要持久化一次,因为有一个崩溃恢复逻辑是prepare的redo log + 完整的binlog 每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB就认为redo log在commit的时候就不需要fsync了,只会write到文件系统的page cache中就够了 通常我们说MySQL的“双1”配置,指的就是sync_binlog和innodb_flush_log_at_trx_commit都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare 阶段),一次是binlog redo log组提交 日志逻辑序列号LSN:LSN单调递增,用来对应redo log的一个个写入点。每次写入长度为length的redo log,LSN的值就会加上length 如下图所示,是三个并发事务在prepare阶段,都写完redo log buffer,持久化到磁盘的过程中 从图中可以看到, trx1是第一个到达的,会被选为这组的leader 等trx1要开始写盘的时候,这个组里面已经有3个事务了,这时候LSN也变成了160 trx1去写盘的时候,带的就是LSN=160,因此等trx1返回时,所有LSN小于等于160的redo log,都已经被持久化到磁盘 这时候trx2和trx3就可以直接返回了 所以,一次组提交里面,组员越多,节约磁盘IOPS的效果越好。但如果只有单线程压测,那就只能老老实实地一个事务对应一次持久化操作了。 在并发更新场景下,第一个事务写完redo log buffer以后,接下来这个fsync越晚调用,组员可能越多,节约IOPS的效果就越好。 MySQL为了让组提交的效果更好,细化了两阶段提及的顺序,让redo log的fsync往后拖 上图的顺序说明binlog也可以组提交,但是通常情况下步骤3会执行得很快,所以能集合到一起持久化的binlog比较少。可以通过设置binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count来提升binlog组提交的效果 性能瓶颈在IO的提升方法 设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count参数,减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。 将sync_binlog 设置为大于1的值(比较常见是100~1000)。这样做的风险是,主机掉电时会丢binlog日志。 将innodb_flush_log_at_trx_commit设置为2。这样做的风险是,主机掉电的时候会丢数据。 MySQL是怎么保证主备一致的 MySQL的主备一致依赖于binlog MySQL主备的基本原理 主备切换流程 客户端的读写是直接访问主库,备库同步主库的更新,与主库保持一致。虽然备库不会被客户端访问,但仍推荐设置成只读模式,因为: 有时候一些运营类的查询语句会放到备库上去查,设置为只读可以防止误操作 防止切换逻辑有bug,比如切换过程中出现双写,造成主备不一致 可以用readonly状态来判断节点的角色 备库的只读对超级权限用户是无效的,用于同步更新的线程拥有超级权限 同步流程 主库的更新语句同步到备库的完成流程图如下 备库B跟主库A之间维持了一个长连接。主库A内部有一个线程,专门用于服务备库B的这个长连接。一个事务日志同步的完整过程如下: 在备库B上通过change master命令,设置主库A的IP、端口、用户名、密码,以及要从哪个位置开始请求binlog,这个位置包含文件名和日志偏移量 在备库B上执行start slave命令,这时候备库会启动两个线程,就是图中的io_thread和sql_thread。其中io_thread负责与主库建立连接。 主库A校验完用户名、密码后,开始按照备库B传过来的位置,从本地读取binlog,发给B 备库B拿到binlog后,写到本地文件,称为中转日志(relay log) sql_thread读取中转日志,解析出日志里的命令,并执行 binlog的三种格式对比 binlog有三种格式,statement、row以及前两种格式的混合mixed 假设有如下表: mysql> CREATE TABLE `t` ( `id` int(11) NOT NULL, `a` int(11) DEFAULT NULL, `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `a` (`a`), KEY `t_modified`(`t_modified`) ) ENGINE=InnoDB; insert into t values(1,1,'2018-11-13'); insert into t values(2,2,'2018-11-12'); insert into t values(3,3,'2018-11-11'); insert into t values(4,4,'2018-11-10'); insert into t values(5,5,'2018-11-09'); statement格式就是SQL语句原文 下图是该语句执行效果 statement格式下,delete 带 limit,很可能出现主备数据不一致的情况,比如上面的例子: 如果delete语句使用的是索引a,那么会根据索引a找到第一个满足条件的行,也就是说删除的是a=4这一行 但如果使用的是索引t_modified,那么删除的就是 t_modified='2018-11-09’ 也就是a=5这一行。 row格式binlog如下 row格式的binlog把SQL语句替换成了两个event:Table_map和Delete_rows Table_map event,用于说明接下来要操作的表是test库的表t; Delete_rows event,用于定义删除的行为。 借助mysqlbinlog工具查看详细的binlog 当binlog_format使用row格式的时候,binlog里面记录了真实删除行的主键id,这样binlog传到备库去的时候,就肯定会删除id=4的行,不会有主备删除不同行的问题。 mixed格式吸收了statement和row格式的优点,占用空间小,避免了数据不一致 但是现在binlog设置成row的场景更多,理由有很多,其中之一是恢复数据。 如果执行的是delete语句,row格式的binlog也会把被删掉的行的整行信息保存起来。所以,如果你在执行完一条delete语句以后,发现删错数据了,可以直接把binlog中记录的delete语句转成insert,把被错删的数据插入回去就可以恢复了 如果你是执行错了insert语句呢?那就更直接了。row格式下,insert语句的binlog里会记录所有的字段信息,这些信息可以用来精确定位刚刚被插入的那一行。这时,你直接把insert语句转成delete语句,删除掉这被误插入的一行数据就可以了。 如果执行的是update语句的话,binlog里面会记录修改前整行的数据和修改后的整行数据。所以,如果你误执行了update语句的话,只需要把这个event前后的两行信息对调一下,再去数据库里面执行,就能恢复这个更新操作了 循环复制问题 binlog的特性确保了主备一致性。实际生产上使用比较多的是双M结构 双M结构中,节点A和B之间总是互为主备关系,在切换的时候就不用再修改主备关系 循环复制指的是A节点更新完,把binlog发给B,B更新完又生成binlog发给了A,解决循环复制的方法如下: 规定两个库的server id必须不同,如果相同,则它们之间不能设定为主备关系 一个备库接到binlog并在重放的过程中,生成与原binlog的server id相同的新的binlog 每个库在收到从自己的主库发过来的日志后,先判断server id,如果跟自己的相同,表示这个日志是自己生成的,直接丢弃 因此,双M结构的日志执行流会变成这样: 从节点A更新的事务,binlog里面记得都是A的server id 传到节点B执行一次以后,节点B生成的binlog的server id也是A的server id 再传回给节点A,A判断到这个server id与自己的相同,就不会再处理这个日志。所以,死循环这里就断掉了 MySQL是怎么保证高可用的 正常情况下,只要主库执行更新生成的所有binlog,都可以传到备库并被正确地执行,备库就能达到跟主库一致的状态,这就是最终一致性。但是MySQL要提供高可用,只有最终一致性是不够的 主备延迟 与数据同步有关的时间点主要包括以下三个: T1:主库A执行完成一个事务,写入binlog T2:备库B接收完这个binlog T3:备库B执行完成这个事务 主备延迟:同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是T3-T1 seconds_behind_master表示备库延迟了多少秒 网络正常情况下,主备延迟的主要因素是T3-T2,直接表现是备库消费中转日志(relay log)的速度比主库生产binlog的速度慢 主备延迟的来源 备库的机器性能差。解决方法是对称部署 备库的压力大。有些统计查询语句只在备库上跑,导致备库压力大,解决方法是一主多从分担读的压力或者把binlog输送到Hadoop来提供统计查询能力 大事务。比如一次性用delete删除太多数据或者大表DDL 备库的并行复制能力 由于主备延迟的存在,所以在主备切换的时候,有不同的策略 可靠性优先策略 在双M结构下,主备切换流程如下: 判断备库B现在的seconds_behind_master,如果小于某个值(比如5秒)继续下一步,否则持续重试这一步; 把主库A改成只读状态,即把readonly设置为true; 判断备库B的seconds_behind_master的值,直到这个值变成0为止; 把备库B改成可读写状态,也就是把readonly 设置为false; 把业务请求切到备库B。 步骤2直到步骤5,主库A和备库B都处于readonly状态,系统不可用(不可写) 可用性优先策略 把上面策略里的步骤4和5放到最开始执行,代价是可能出现数据不一致的情况 一般情况下,可靠性优于可用性。在满足数据可靠性的前提下,MySQL高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。 备库为什么会延迟好几个小时 当备库执行日志的速度持续低于主库生成日志的速度,那这个延迟就有可能成了小时级别 MySQL 5.6之前,备库应用日志更新数据只能使用单线程,在主库并发高、TPS高时会出现严重的主备延迟问题 按表分发策略 按表分发事务的基本思路是,如果两个事务更新不同的表,它们就可以并行 worker线程维护一张执行队列里的事务涉及的表,key是“库名.表名”,value表示队列中有多少个事务修改这个表 事务在分发的时候,和所有worker的冲突关系有3种: 如果跟所有worker都不冲突,coordinator线程就会把这个事务分配给最空闲的woker; 如果跟多于一个worker冲突,coordinator线程就进入等待状态,直到和这个事务存在冲突关系的worker只剩下1个; 如果只跟一个worker冲突,coordinator线程就会把这个事务分配给这个存在冲突关系的worker。 按表分发方案在多个表负载均衡的场景效果很好。但是碰到热点表会退化成单线程复制 按行分发策略 要解决热点表的并行复制问题,就需要一个按行并行复制方案。核心思路是:如果两个事务没有更新相同的行,它们在备库上可以并行执行。这个模式要求binlog格式必须是row 判断事务和worker是否冲突,用的规则不是“修改同一个表”,而是“修改同一行”。worker维护的hash表的key是“库名+表名+唯一索引的名字+唯一索引的值” 按行分发策略比按表分发策略需要消耗更多的计算资源,这两种方案都有一样的约束条件: 要能够从binlog里面解析出表名、主键值和唯一索引的值。也就是说,主库的binlog格式必须是row; 表必须有主键; 不能有外键。表上如果有外键,级联更新的行不会记录在binlog中,这样冲突检测就不准确。 MySQL 5.6版本的并行复制策略 官方MySQL 5.6版本支持的并行复制的力度是按库并行。hash表的key是数据库名 相比于按表和按行分发,有两个优势: 构造hash值的时候很快,只需要库名;而且一个实例上DB数也不会很多,不会出现需要构造100万个项这种情况 不要求binlog的格式。因为statement格式的binlog也可以很容易拿到库名 MariaDB的并行复制策略 MariaDB的并行复制策略利用了redo log组提交优化的特性: 能够在同一组里提交的事务,一定不会修改同一行 主库上可以并行执行的事务,备库上也一定可以并行执行 这个策略的目标是“模拟主库的并行模式”,但它没有实现“真正的模拟主库并发度”这个目标。在主库上,一组事务在commit的时候,下一组事务是同时处于“执行中”状态的 MySQL 5.7的并行复制策略 由参数slave-parallel-type来控制并行复制策略: 配置为DATABASE,表示使用MySQL 5.6版本的按库并行策略; 配置为 LOGICAL_CLOCK,表示的就是优化过的类似MariaDB的策略 该优化的策略的思想是: 同时处于prepare状态的事务,在备库执行时是可以并行的; 处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也是可以并行的。 MySQL 5.7.22的并行复制策略 新增了一个并行复制策略,基于WRITESET的并行复制。 新增参数binlog-transaction-dependency-tracking,用来控制是否启用这个新策略: COMMIT_ORDER,表示的就是前面介绍的,根据同时进入prepare和commit来判断是否可以并行的策略。 WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的hash值,组成集合writeset。如果两个事务没有操作相同的行,也就是说它们的writeset没有交集,就可以并行 WRITESET_SESSION,是在WRITESET的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。 该策略类似按行分发,但是有很大优势: writeset是在主库生成后直接写入到binlog里面的,这样在备库执行的时候,不需要解析binlog内容(event里的行数据),节省了很多计算量; 不需要把整个事务的binlog都扫一遍才能决定分发到哪个worker,更省内存; 由于备库的分发策略不依赖于binlog内容,所以binlog是statement格式也是可以的。 该方案对于“表上没主键”和“外键约束”的场景,也会暂时退化为单线程模型。 主库出问题了,从库怎么办 大多数互联网应用场景都是读多写少,要解决读性能问题,就要涉及:一主多从 图中,虚线箭头表示的是主备关系,也就是A和A’互为主备, 从库B、C、D指向的是主库A。一主多从的设置,一般用于读写分离,主库负责所有的写入和一部分读,其他的读请求则由从库分担。 下面讨论,在一主多从架构下,主库故障后的主备切换问题 相比于一主一备的切换流程,一主多从结构在切换完成后,A’会成为新的主库,从库B、C、D也要改接到A’。正是由于多了从库B、C、D重新指向的这个过程,所以主备切换的复杂性也相应增加了 基于位点的主备切换 节点B设置成节点A’ 的从库的时候,需要执行change master命令,必须设置主库的日志文件名和偏移量。A和A’的位点是不同的,从库B切换时需要先经过“找同步位点”这个逻辑 同步位点很难精确取到 取同步位点的方法如下: 等待新主库A’ 把中转日志(relay log)全部同步完成 在A’ 上执行show master status命令,得到当前A’ 上最新的 File 和 Position 取原主库A故障的时刻T 用mysqlbinlog工具解析A’的File,得到T时刻的位点 mysqlbinlog File --stop-datetime=T --start-datetime=T 图中,end_log_pos后面的值“123”,表示的就是A’这个实例,在T时刻写入新的binlog的位置,可以把这个值作为$master_log_pos ,用在节点B的change master命令里 这个值并不精确,从库B的同步线程可能会出错,解决方法如下: 通过sql_slave_skip_counter跳过出错事务 设置slave_skip_errors,跳过指定错误,通常设置成1032,1062,对应的错误是删除数据找不到行,插入数据唯一键冲突 GTID 前两种方式操作复杂,容易出错,MySQL 5.6 引入了GITD。 GTID全称是Global Transaction Identifier,也就是全局事务ID,是一个事务在提交的时候生成的,是这个事务的唯一标识,格式是: GTID=server_uuid:gno server_uuid是一个实例第一次启动时自动生成的,是一个全局唯一的值 gno是一个整数,初始值是1,每次提交事务的时候分配给这个事务,并加1 GTID有两种生成方式: 如果gtid_next=automatic,代表使用默认值。这时,MySQL就会把server_uuid:gno分配给这个事务。 a. 记录binlog的时候,先记录一行 SET @@SESSION.GTID_NEXT=‘server_uuid:gno’; b. 把这个GTID加入本实例的GTID集合 如果gtid_next是一个指定的GTID的值,比如通过set gtid_next='current_gtid’指定为current_gtid,那么就有两种可能: a. 如果current_gtid已经存在于实例的GTID集合中,接下来执行的这个事务会直接被系统忽略; b. 如果current_gtid没有存在于实例的GTID集合中,就将这个current_gtid分配给接下来要执行的事务,也就是说系统不需要给这个事务生成新的GTID,因此gno也不用加1 每个MySQL实例都维护了一个GTID集合,用来对应“这个实例执行过的所有事务” 当从库需要跳过某个事务时,在主库上查出GTID,在从库上提交空事务,把这个GTID加入到从库的GTID集合中 set gtid_next='aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10'; begin; commit; set gtid_next=automatic; start slave; 基于GTID的主备切换 切换命令指定master_auto_position=1表示这个主备关系使用的是GTID协议,不需要指定主库日志文件和偏移量 我们把A’ 的GTID集合记为set_a,实例B的GTID集合记为set_b,切换流程如下: 实例B指定主库A’,基于主备协议建立连接。 实例B把set_b发给主库A’ 实例A’算出set_a与set_b的差集,也就是所有存在于set_a,但是不存在于set_b的GITD的集合,判断A’本地是否包含了这个差集需要的所有binlog事务。 a. 如果不包含,表示A’已经把实例B需要的binlog给删掉了,直接返回错误; b. 如果确认全部包含,A’从自己的binlog文件里面,找出第一个不在set_b的事务,发给B; 之后就从这个事务开始,往后读文件,按顺序取binlog发给B去执行 GTID和在线DDL 假设,这两个互为主备关系的库还是实例X和实例Y,且当前主库是X,并且都打开了GTID模式。这时的主备切换流程可以变成下面这样: 在实例X上执行stop slave。 在实例Y上执行DDL语句。注意,这里并不需要关闭binlog。 执行完成后,查出这个DDL语句对应的GTID,并记为 server_uuid_of_Y:gno。 到实例X上执行以下语句序列: set GTID_NEXT="server_uuid_of_Y:gno"; begin; commit; set gtid_next=automatic; start slave; 这样做的目的在于,既可以让实例Y的更新有binlog记录,同时也可以确保不会在实例X上执行这条更新。 接下来,执行完主备切换,然后照着上述流程再执行一遍即可 读写分离有哪些坑 由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态,这种现象称为“过期读” 过期读处理方案包括: 强制走主库方案 sleep方案 判断主备无延迟方案 配合semi-sync方案 等主库位点方案 等GTID方案 强制走主库方案 该方案将查询请求分为两类: 对于必须要拿到最新结果的请求,强制将其发到主库上。比如,在一个交易平台上,卖家发布商品以后,马上要返回主页面,看商品是否发布成功。那么,这个请求需要拿到最新的结果,就必须走主库 对于可以读到旧数据的请求,才将其发到从库上。在这个交易平台上,买家来逛商铺页面,就算晚几秒看到最新发布的商品,也是可以接受的。那么,这类请求就可以走从库 这个方案用的最多,但是问题在于存在“所有查询都不能是过期读”的需求,比如金融类业务,那就必须放弃读写分离,所有读写压力都在主库 下面讨论的是:可以支持读写分离的场景下,有哪些解决过期读的方案 Sleep方案 主库更新后,读从库之前先sleep一下。这个方案假设,大多数情况下主备延迟在1s之内 该方案可以解决类似Ajax场景下的过期读问题。例如卖家发布商品,直接将卖家输入的内容作为新商品显示出来,并不查从库。等待卖家刷新页面,相当于sleep了一段时间,解决了过期读问题 该方案存在的问题是不精确: 如果查询请求本来0.5s就可以在从库上拿到正确结果,也会等到1s 如果延迟超过1s,还是会出现过期读 判断主备无延迟方案 有三种方法: 每次从库执行查询请求前,先判断seconds_behind_master是否已经等于0。如果还不等于0 ,那就必须等到这个参数变为0才能执行查询请求。 对比位点。如果Master_Log_File和Relay_Master_Log_File、Read_Master_Log_Pos和Exec_Master_Log_Pos这两组值完全相同表示主备无延迟 对比GITD。Retrieved_Gtid_Set和Executed_Gtid_Set相同表示是主备无延迟 该方案比Sleep更准确,方法2和3比1准确,但是不能说精确。因为存在客户端已经收到提交确认,而备库还没收到日志的状态,因此备库认为主备无延迟,从而发生过期读 配合semi-sync 为解决上面的问题,引入semi-sync replication: semi-sync做了这样的设计: 事务提交的时候,主库把binlog发给从库 从库收到binlog以后,发回给主库一个ack 主库收到ack以后,才能给客户端返回“事务完成”的确认 开启semi-sync,就表示所有给客户端发送过确认的事务,都确保备库已经收到了这个日志 semi-sync+判断主备无延迟方案存在两个问题: 一主多从情况下,因为主库只要收到一个从库的ack就给客户端返回确认,其它未响应ack的从库可能会发生过期读问题 在业务高峰期,主库的位点或者GITD集合更新很快,这种情况下,可能出现从库一直存在主备延迟导致客户端查询一直等待 等主库位点方案 该方案解决了前面两个问题 命令: select master_pos_wait(file, pos[, timeout]); 这条命令的逻辑如下: 它是在从库执行的 参数file和pos指的是主库上的文件名和位置 timeout可选,设置为正整数N表示这个函数最多等待N秒 为了解决前面两个问题,流程如下: trx1事务更新完成后,马上执行show master status得到当前主库执行到的File和Position; 选定一个从库执行查询语句; 在从库上执行select master_pos_wait(File, Position, 1); 如果返回值是>=0的整数,则在这个从库执行查询语句; 否则,到主库执行查询语句。 GTID方案 等GTID也可以解决前面两个问题 流程如下: trx1事务更新完成后,从返回包直接获取这个事务的GTID,记为gtid1; 选定一个从库执行查询语句; 在从库上执行 select wait_for_executed_gtid_set(gtid1, 1); 如果返回值是0,则在这个从库执行查询语句; 否则,到主库执行查询语句。 如何判断一个数据库是不是出问题了 select 1 判断 select 1 成功返回只能说明数据库进程还在,不能说明没问题 并发连接:通过show precesslist查询连接数,连接数可以远大于并发查询数量 并发查询:“当前正在执行”的语句的数量 线程进入锁等待后,并发线程的计数会减一,即进入锁等待的线程不吃CPU 假如设置并发线程数是3,下面的情况是A、B、C在并发查询,D先select 1不占并发线程数所以能正常返回,但实际上已经不能正常查询了 查表判断 为了能够检测InnoDB并发线程数过多导致的系统不可用情况,我们需要找一个访问InnoDB的场景。一般的做法是,在系统库(mysql库)里创建一个表,比如命名为health_check,里面只放一行数据,然后定期执行: mysql> select * from mysql.health_check; 这种方法在磁盘空间满了就无效。因为更新事务要写binlog,而一旦binlog所在磁盘满了,那么所有更新语句都会堵住,但是系统仍然可以读数据 更新判断 我们把查询换成更新来作为监控语句。常见做法是放一个timestamp字段表示最后一次检测时间,这条更新语句类似于: mysql> update mysql.health_check set t_modified=now(); 主库和备库用同样的更新语句可能会出现行冲突,导致主备同步停止,所以mysql.health_check表不能只有一行数据 mysql> CREATE TABLE `health_check` ( `id` int(11) NOT NULL, `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB; /* 检测命令 */ insert into mysql.health_check(id, t_modified) values (@@server_id, now()) on duplicate key update t_modified=now(); MySQL规定主备的server_id必须不同,这样主备各自的检测命令就不会冲突 更新判断存在的问题是“判定慢”。因为更新语句在IO负载已经100%时仍然可能在超时前完成。检测系统看到update命令没有超时,就认为“系统正常”,但实际上正常SQL语句已经很慢了 内部统计 前面几种方法都是外部检测,外部检测都需要定时轮询,所以系统可能已经出问题了,但是却需要等到下一个检测发起执行语句的时候,才有可能发现问题,导致主备切换慢 针对磁盘利用率问题,MySQL 5.6 在file_summary_by_event_name表里统计了每次IO请求的时间,可以设置阈值作为检测逻辑 误删数据怎么办? 误删分为以下几类: 使用delete误删数据行 使用drop table或者truncate table误删数据表 使用drop database误删数据库 使用rm误删整个MySQL实例 误删行 可以使用Flashback恢复,原理是修改binlog的内容,拿回原库重放。使用这个方案的前提是确保binlog_format=row 和 binlog_row_image=FULL 建议在备库上执行,再恢复回主库 误删库/表 这种情况要求线上有定期的全量备份,并且实时备份binlog 假如有人中午12点误删了一个库,恢复数据的流程如下: 取最近一次全量备份,假设这个库是一天一备,上次备份是当天0点; 用备份恢复出一个临时库; 从日志备份里面,取出凌晨0点之后的日志; 把这些日志,除了误删除数据的语句外,全部应用到临时库 如果临时库有多个数据库,在使用mysqlbinlog时可以加上-database指定误删表所在库,加速数据恢复 在应用日志的时候,需要跳过12点误操作的那个语句的binlog: 如果原实例没有使用GTID模式,只能在应用到包含12点的binlog文件的时候,先用–stop-position参数执行到误操作之前的日志,然后再用–start-position从误操作之后的日志继续执行; 如果实例使用了GTID模式,就方便多了。假设误操作命令的GTID是gtid1,那么只需要执行set gtid_next=gtid1;begin;commit; 先把这个GTID加到临时实例的GTID集合,之后按顺序执行binlog的时候,就会自动跳过误操作的语句 即使这样,使用mysqlbinlog方法恢复数据仍然不快,因为: mysqlbinlog并不能指定只解析一个表的日志 用mysqlbinlog解析出日志应用,应用日志的过程就只能是单线程 一种加速方法是,在用备份恢复出临时实例之后,将这个临时实例设置成线上备库的从库,这样: 在start slave之前,先通过执行 change replication filter replicate_do_table = (tbl_name) 命令,就可以让临时库只同步误操作的表; 这样做也可以用上并行复制技术,来加速整个数据恢复过程。 延迟复制备库 上面的方案存在“恢复时间不可控问题”,比如一周一备份,第6天误操作,那就需要恢复6天的日志,这个恢复时间可能按天计算 一般的主备复制结构存在的问题是,如果主库上有个表被误删了,这个命令很快也会被发给所有从库,进而导致所有从库的数据表也都一起被误删了。 延迟复制的备库是一种特殊的备库,通过 CHANGE MASTER TO MASTER_DELAY = N命令,可以指定这个备库持续保持跟主库有N秒的延迟。 比如你把N设置为3600,这就代表了如果主库上有数据被误删了,并且在1小时内发现了这个误操作命令,这个命令就还没有在这个延迟复制的备库执行。这时候到这个备库上执行stop slave,再通过之前介绍的方法,跳过误操作命令,就可以恢复出需要的数据。 这样的话,你就随时可以得到一个,只需要最多再追1小时,就可以恢复出数据的临时实例,也就缩短了整个数据恢复需要的时间 预防误删库/表的方法 账号分离,避免写错命令 只给业务开发同学DML权限,而不给truncate/drop权限。而如果业务开发人员有DDL需求的话,也可以通过开发管理系统得到支持 即使是DBA团队成员,日常也都规定只使用只读账号,必要的时候才使用有更新权限的账号 指定操作规范,避免写错要删除的表名 删除数据表之前,必须先对表做改名操作。然后,观察一段时间,确保对业务无影响以后再删除这张表。 改表名的时候,要求给表名加固定的后缀(比如加_to_be_deleted),然后删除表的动作必须通过管理系统执行。并且,管理系删除表的时候,只能删除固定后缀的表。 rm删除数据 对于有高可用机制的MySQL集群,最不怕rm。只要整个集群没被删掉,HA系统会选出新主库,保证整个集群正常工作。因此备库尽量跨机房、跨城市 为什么还有kill不掉的语句 MySQL有两个kill命令: kill query+线程id,表示终止这个线程正在执行的语句 kill connection+线程id,connection可缺省,表示断开这个线程的连接,如果有语句正在执行,先停止语句 收到kill后,线程做什么 kill并不是马上停止,而是告诉线程,这条语句已经不需要继续执行了,可以开始“执行停止的逻辑了” 处理kill query命令的线程做了两件事: 把目标线程的运行状态改成THD::KILL_QUERY(将变量killed赋值为THD::KILL_QUERY); 给目标线程发一个信号,通知目标线程处理THD::KILL_QUERY状态。如果目标线程处于等待状态,必须是一个可以被唤醒的等待,否则不会执行到判断线程状态的“埋点” 处理kill connection命令的线程做了两件事: 把目标线程状态设置为KILL_CONNECTION 关闭目标线程的网络连接 kill无效的两类情况: 线程没有执行到判断线程状态的逻辑。这种情况有innodb_thread_concurrency 不够用,IO压力过大 终止逻辑耗时较长。这种情况有kill超大事务、回滚大查询、kill最后阶段的DDL命令 处于Killed状态的线程,你可以通过影响系统环境来让状态尽早结束。比如并发度不够导致线程没有执行到判断线程状态的逻辑,就增大innodb_thread_concurrency。除此之外,做不了什么,只能等流程自己结束 大查询会不会打爆内存 主机内存小于表的大小,全表扫描不会用光主机内存,否则逻辑备份早就挂了 全表扫描对server层的影响 假设对200G的表 db1.t 全表扫描,需要保留结果到客户端,会使用类似命令: mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t" > $target_file 服务端不保存完整的查询结果集,取数据和发数据的流程是这样的: 获取一行,写到net_buffer中 重复获取行,直到net_buffer写满,调用网络接口发出去 如果发送成功,就清空net_buffer,然后继续取下一行,并写入net_buffer 如果发送函数返回EAGAIN或WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送 从这个流程可以看出: 一个查询在发送过程中,占用的MySQL内部的内存最大就是net_buffer_length这么大,并不会达到200G; socket send buffer 也不可能达到200G(默认定义/proc/sys/net/core/wmem_default),如果socket send buffer被写满,就会暂停读数据的流程。 全表扫描对InnoDB层的影响 数据页在Buffer Pool(BP)中管理,BP可以起到加速查询的作用,作用效果依赖于一个重要指标:内存命中率 BP的大小由参数 innodb_buffer_pool_size 确定,一般设置成可用物理内存的60%~80% 如果BP满了,要从磁盘读入一个数据页,就要淘汰一个旧数据页,InnoDB内存管理用的是改进后的最近最少使用(LRU)算法 上图head指向刚刚被访问过的数据页 基本的LRU算法在遇到全表扫描历史数据表时,会出现内存命中率急剧下降,磁盘压力增加,SQL响应变慢的情况 InnoDB按照 5:3 将LRU链表分成young区和old区,LRU_old指向old区域第一个位置,即整个链表的5/8处 改进后的LRU算法如下: 访问young区域的数据页,和之前的算法一样,移动到链表头 访问不在链表中的数据页,淘汰tail指向的最后一页,在LRU_old处插入新数据页 访问old区域的数据页,若这个数据页在LRU链表中存在时间超过1s,就移动到链表头部,否则不动,1s由参数innodb_old_blocks_time控制 这个策略在扫描大表时不会对young区域造成影响,保证BP响应正常业务的查询命中率 可不可以使用join 先创建两个DDL一样的表 CREATE TABLE `t2` ( `id` int(11) NOT NULL, `a` int(11) DEFAULT NULL, `b` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `a` (`a`) ) ENGINE=InnoDB; /*省略给t2插入1000行数据*/ create table t1 like t2; insert into t1 (select * from t2 where id<=100) Index Nested-Loop Join 有如下语句: select * from t1 straight_join t2 on (t1.a=t2.a); straight_join让MySQL使用固定的连接方式执行查询,这里t1是驱动表,t2是被驱动表 这个语句的执行流程如下: 从表t1中读入一行数据R 从数据行R中,取出a字段到表t2里去查 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分 重复执行步骤1到3,直到表t1的末尾循环结束 在形式上,这个过程和我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以我们称之为“Index Nested-Loop Join”,简称NLJ 在流程里: 对驱动表t1做了全表扫描,这个过程需要扫描100行 对于每一行R,根据a字段去表t2查找,走的是树搜索过程。由于我们构造的数据是一一对应的,因此每次搜索都只扫描一行,也就是总共扫描100行 所以,整个执行流程,总扫描行数是200 如果不用join,上面的连接需求,用单表查询实现的话,扫描行数一样,但是交互次数多,而且客户端要自己拼接SQL语句和结果,因此不如直接join 假设驱动表行数是N。被驱动表行数是M,被驱动表查一行数据要先走索引a,再走主键索引,因此时间复杂度是2∗log2M2*log_2 M2∗log2​M。驱动表要扫描N行,然后每行都要去被驱动表上匹配,所以整个执行过程复杂度是 N+N∗2∗log2MN+N*2*log_2 MN+N∗2∗log2​M。显然N影响更大,因此让小表做驱动表 Simple Nested-Loop Join 现在语句改成如下: select * from t1 straight_join t2 on (t1.a=t2.b); 由于t2的字段b没有索引,每次到t2去匹配都要做全表扫描,因此这个查询要扫描100*1000=10万行。 Block Nested-Loop Join 当被驱动表上没有可用索引,MySQL使用的算法流程如下: 把表t1的数据读入线程内存join_buffer中,由于我们这个语句中写的是select *,因此是把整个表t1放入了内存; 扫描表t2,把表t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回。 该算法和Simple Nested-Loop Join算法扫描的行数一样多,但该算法是内存操作,速度更快。碰到大表不能放入join_buffer的情况就分多次放 总结一下: 第一个问题:能不能使用join语句? 如果可以使用Index Nested-Loop Join算法,也就是说可以用上被驱动表上的索引,其实是没问题的; 如果使用Block Nested-Loop Join算法,扫描行数就会过多。尤其是在大表上的join操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种join尽量不要用 所以在判断要不要使用join语句时,就是看explain结果里面,Extra字段里面有没有出现“Block Nested Loop”字样 第二个问题:如果要使用join,应该选择大表做驱动表还是选择小表做驱动表? 总是使用小表做驱动表。更准确地说,在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表 join语句怎么优化 创建两个表t1、t2(id int primary key, a int, b int, index(a))。给表t1插入1000行数据,每一行a=1001-id,即字段a是逆序的。给表t2插入100万行数据 Multi-Range Read优化 现在有SQL语句: select * from t1 where a>=1 and a<=100; MRR优化的设计思路是:大多数的数据都是按照主键递增顺序插入的,所以按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。使用MRR的语句的执行流程如下: 根据索引a,定位到满足条件的记录,将id值放入read_rnd_buffer中; 将read_rnd_buffer中的id进行递增排序 排序后的id数组,依次到主键id索引中查记录,并作为结果返回。 MRR能够提升性能的核心在于,这条查询语句在索引a上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键id。这样通过排序以后,再去主键索引查数据,才能体现出“顺序性”的优势 Batched Key Access MySQL 5.6 引入Batched Key Acess(BKA)算法,这个算法是对NLJ算法的优化 NLJ算法执行的逻辑是:从驱动表t1,一行行地取出a的值,再到被驱动表t2去做join。也就是说,对于表t2来说,每次都是匹配一个值。这时,MRR的优势就用不上了 优化思路就是,从表t1里一次性多拿出些行,一起传给表t2。取出的数据先放到join_buffer BNL算法的性能问题 可能会多次扫描被驱动表,占用磁盘IO资源; 判断join条件需要执行M*N次对比(M、N分别是两张表的行数),如果是大表就会占用非常多的CPU资源; 可能会导致Buffer Pool的热数据被淘汰,影响内存命中率。 如果explain命令发现优化器使用BNL算法。我们就需要优化,常见做法是,给被驱动表的join字段加上索引,把BNL算法转成BKA算法 BNL转BKA select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000; 在索引创建资源开销大情况下,可以考虑使用临时表: 把表t2中满足条件的数据放在临时表tmp_t中; 为了让join使用BKA算法,给临时表tmp_t的字段b加上索引; 让表t1和tmp_t做join操作 对应的SQL语句: create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb; insert into temp_t select * from t2 where b>=1 and b<=2000; select * from t1 join temp_t on (t1.b=temp_t.b); 扩展-hash join BNL的问题是join_buffer里面维护的是一个无序数组,如果是一个hash表,可以大幅减少判断次数。可以在业务端实现这个优化: select * from t1;取得表t1的全部1000行数据,在业务端存入一个hash结构 select * from t2 where b>=1 and b<=2000; 获取表t2中满足条件的2000行数据。 把这2000行数据,一行一行地取到业务端,到hash结构的数据表中寻找匹配的数据。满足匹配的条件的这行数据,就作为结果集的一行 为什么临时表可以重名 内存表和临时表的区别: 内存表,指的是使用Memory引擎的表,建表语法是create table … engine=memory。这种表的数据都保存在内存里,系统重启的时候会被清空,但是表结构还在。除了这两个特性看上去比较“奇怪”外,从其他的特征上看,它就是一个正常的表 临时表,可以使用各种引擎类型 。如果是使用InnoDB引擎或者MyISAM引擎的临时表,写数据的时候是写到磁盘上的。当然,临时表也可以使用Memory引擎 临时表的特性 临时表在使用上有以下几个特点: 建表语法是create temporary table …。 一个临时表只能被创建它的session访问,对其他线程不可见。所以,图中session A创建的临时表t,对于session B就是不可见的。 临时表可以与普通表同名 session A内有同名的临时表和普通表的时候,show create语句,以及增删改查语句访问的是临时表 show tables命令不显示临时表 临时表的应用 由于不用担心线程之间的重名冲突,临时表经常会被用在复杂查询的优化过程中。其中,分库分表系统的跨库查询就是一个典型的使用场景。 一般分库分表的场景,就是要把一个逻辑上的大表分散到不同的数据库实例上。比如。将一个大表ht,按照字段f,拆分成1024个分表,然后分布到32个数据库实例上。如下图所示: 分区key的选择是以“减少跨库和跨表查询”为依据的。如果大部分的语句都会包含f的等值条件,那么就要用f做分区键 比如: select v from ht where f=N; 可以通过分表规则(比如,N%1024)来确认需要的数据被放在了哪个分表上 但是,如果这个表上还有另外一个索引k,并且查询语句是这样的: select v from ht where k >= M order by t_modified desc limit 100; 由于查询条件里面没有用到分区字段f,只能到所有的分区中去查找满足条件的所有行,然后统一做order by 的操作。这种情况有两种思路: 在proxy层的进程代码中实现排序。优势是快,缺点是工作量大,proxy端压力大 把分库数据汇总到一个表中,再在汇总上操作。如下图所示 为什么临时表可以重名? create temporary table temp_t(id int primary key)engine=innodb; 执行该语句,MySQL会创建一个frm文件保存表结构定义。该文件放在临时文件目录下,文件名的后缀是.frm,前缀是“#sql{进程id}_{线程id}_序列号” 除了文件名不同,内存里面也有一套机制区别不同的表,每个表都对应一个table_def_key 一个普通表的table_def_key的值是由“库名+表名”得到的,所以如果你要在同一个库下创建两个同名的普通表,创建第二个表的过程中就会发现table_def_key已经存在了。 而对于临时表,table_def_key在“库名+表名”基础上,又加入了“server_id+thread_id” 临时表和主备复制 如果当前的binlog_format=row,那么跟临时表有关的语句,就不会记录到binlog里 如果binlog_format=statment/mixed,创建临时表的语句会传到备库,由备库的同步线程执行。因为主库的线程退出时会自动删除临时表,但是备库同步线程是持续运行的,所以还需要在主库上再写一个DROP TEMPORARY TABLE传给备库执行 主库上不同线程创建同名的临时表是没关系的,但是传到备库怎么处理? MySQL在记录binlog的时候,会把主库执行这个语句的线程id写到binlog中。这样,在备库的应用线程就能够知道执行每个语句的主库线程id,并利用这个线程id来构造临时表的table_def_key: session A的临时表t1,在备库的table_def_key就是:库名+t1+“M的serverid”+“session A的thread_id”; session B的临时表t1,在备库的table_def_key就是 :库名+t1+“M的serverid”+“session B的thread_id” 为什么会使用内部临时表 union 执行流程 假设有表t1: create table t1(id int primary key, a int, b int, index(a)); delimiter ;; create procedure idata() begin declare i int; set i=1; while(i<=1000)do insert into t1 values(i, i, i); set i=i+1; end while; end;; delimiter ; call idata(); 然后执行: (select 1000 as f) union (select id from t1 order by id desc limit 2); 这个语句的执行流程是这样的: 创建一个内存临时表,这个临时表只有一个整型字段f,并且f是主键字段 执行第一个子查询,得到1000这个值,并存入临时表中 执行第二个子查询: 拿到第一行id=1000,试图插入临时表中。但由于1000这个值已经存在于临时表了,违反了唯一性约束,所以插入失败,然后继续执行; 取到第二行id=999,插入临时表成功 从临时表中按行取出数据,返回结果,并删除临时表,结果中包含两行数据分别是1000和999 如果使用union all,就没有去重,执行的时候是依次执行子查询,得到的结果直接作为结果集的一部分,不需要临时表 group by 执行流程 select id%10 as m, count(*) as c from t1 group by m; 这个语句的执行流程如下: 创建内存临时表,表里有两个字段m和c,主键是m 扫描表t1的索引a,依次取出叶子节点上的id值,计算id%10的结果,记为x; 如果临时表中没有主键为x的行,就插入一个记录(x,1); 如果表中有主键为x的行,就将x这一行的c值加1 遍历完成后,再根据字段m做排序,得到结果集返回给客户端 如果不需要排序,在语句末尾加上order by null 当内存临时表大小达到上限时,会转成磁盘临时表,磁盘临时表默认使用的引擎是InnoDB group by 优化方法 --索引 新增一列,给这列加索引 alter table t1 add column z int generated always as(id % 100), add index(z); 对这列group by: select z, count(*) as c from t1 group by z; group by 优化方法 --直接排序 碰到不能加索引的场景就得老老实实做排序 在group by语句中加入SQL_BIG_RESULT这个提示(hint),就可以告诉优化器:这个语句涉及的数据量很大,请直接用磁盘临时表 select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m; 这个语句的执行流程如下: 初始化sort_buffer,确定放入一个整型字段,记为m 扫描表t1的索引a,依次取出里面的id值, 将 id%100的值存入sort_buffer中 扫描完成后,对sort_buffer的字段m做排序(如果sort_buffer内存不够用,就会利用磁盘临时文件辅助排序) 排序完成后,就得到了一个有序数组。顺序扫描一遍就可以得到结果 基于上面的union、union all和group by语句的执行过程的分析,我们来回答文章开头的问题:MySQL什么时候会使用内部临时表? 如果语句执行过程可以一边读数据,一边直接得到结果,是不需要额外内存的,否则就需要额外的内存,来保存中间结果 join_buffer是无序数组,sort_buffer是有序数组,临时表是二维表结构 如果执行逻辑需要用到二维表特性,就会优先考虑使用临时表。比如我们的例子中,union需要用到唯一索引约束, group by还需要用到另外一个字段来存累积计数。 都说InnoDB好,那还要不要使用Memory引擎 内存表的数据组织结构 假设有两张表t1,t2,t1使用Memory引擎,t2使用InnoDB引擎 create table t1(id int primary key, c int) engine=Memory; create table t2(id int primary key, c int) engine=innodb; insert into t1 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0); insert into t2 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0); 然后,分别执行select *from t1和select* from t2。t2表的(0,0)出现在第一行,t1表出现在最后一行 这是因为InnoDB引擎的数据就存在主键索引上,而主键索引是有序存储的,在执行select *的时候,就会按照叶子节点从左到右扫描,所以得到的结果里,0就出现在第一行 而Memory引擎的数据和索引是分开的。主键索引存的是每个数据的位置。执行select *走的是全表扫描数据数组 InnoDB和Memory引擎的数据组织方式是不同的: InnoDB引擎把数据放在主键索引上,其他索引上保存的是主键id。这种方式,我们称之为索引组织表 Memory引擎采用的是把数据单独存放,索引上保存数据位置的数据组织形式,我们称之为堆组织表 两个引擎的一些典型不同: InnoDB表的数据总是有序存放的,而内存表的数据就是按照写入顺序存放的 当数据文件有空洞的时候,InnoDB表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就可以插入新值; 数据位置发生变化的时候,InnoDB表只需要修改主键索引,而内存表需要修改所有索引 InnoDB表用主键索引查询时需要走一次索引查找,用普通索引查询的时候,需要走两次索引查找。而内存表没有这个区别,所有索引的“地位”都是相同的 InnoDB支持变长数据类型,不同记录的长度可能不同;内存表不支持Blob 和 Text字段,并且即使定义了varchar(N),实际也当作char(N),也就是固定长度字符串来存储,因此内存表的每行数据长度相同。 hash索引和B-Tree索引 内存表的范围查询不能走主键索引,但是可以加一个B-Tree索引,B-Tree索引类似于InnoDB的B+树索引 alter table t1 add index a_btree_index using btree (id); 不建议在生产环境使用内存表,原因有两方面: 锁粒度问题。内存表不支持行锁,只支持表锁 数据持久化问题 自增主键为什么不是连续的 自增主键可以让主键索引尽量保持递增顺序插入,避免页分裂,因此索引更紧凑,但自增主键不能保证连续递增 自增值保存在哪? InnoDB的自增值保存在内存中。每次重启MySQL都会计算max(id)+1作为自增值。8.0版本,重启的时候依靠redo log恢复自增值 自增值修改机制 假设,某次插入的值是X,当前的自增值是Y 如果X < Y,那么自增值不变 如果X >= Y,将当前自增值修改为新的自增值 Z = auto_increment_offset+k*auto_increment_increment。Z > X,auto_increment_offset是自增初始值,auto_increment_increment是自增步长,k是自然数 自增值的修改时机 自增值在真正执行插入数据的操作之前修改。如果因为唯一键冲突导致插入失败会出现id不连续,事务回滚也是类似现象 自增锁的优化 自增id锁并不是一个事务锁,而是每次申请完就马上释放,以便允许别的事务再申请。建议innodb_autoinc_lock_mode设置成2,即前面的策略,同时binlog_format=row,避免insert … select造成主备数据不一致 insert语句的锁为什么这么多 insert … select 语句 在可重复读隔离级别下,binlog_format=statement时,执行 insert … select 语句会对select表的需要访问的资源加锁。加锁是为了避免主备不一致 insert 循环写入 如果把select表的结果insert到select表中,会对select表全表扫描,创建一个临时表,再将select结果insert回表。这么做的原因是:这类一边遍历数据,一边更新数据的情况,如果读出来的数据直接写回原表,就可能在遍历过程中,读到刚刚插入的记录,新插入的记录如果参与计算逻辑,就跟语义不符 优化方法是:手动创建内存临时表,先 insert临时表select目标表,再 insert目标表select临时表,这样就不会对目标表全表扫描 insert 唯一键冲突 在session A执行rollback语句回滚的时候,session C几乎同时发现死锁并返回 这个死锁产生的逻辑是这样的: 在T1时刻,启动session A,并执行insert语句,此时在索引c的c=5上加了记录锁。注意,这个索引是唯一索引,因此退化为记录锁 在T2时刻,session B要执行相同的insert语句,发现了唯一键冲突,加上读锁;同样地,session C也在索引c上,c=5这一个记录上,加了读锁(共享next-key lock) T3时刻,session A回滚。这时候,session B和session C都试图继续执行插入操作,都要加上写锁(排它next-key lock)。两个session都要等待对方的行锁,所以就出现了死锁 insert into … on duplicate key update 这个语义的逻辑是,插入一行数据,如果碰到唯一键约束,就执行后面的更新语句。它给唯一索引加排它的next-key lock(写锁) 怎么最快地复制一张表 如果可以控制对原表的扫描行数和加锁范围很小的话,可以直接用insert … select。否则先将数据写到外部文件,再写回目标表,方法有三种: 物理拷贝的方式速度最快,尤其对于大表拷贝来说是最快的方法。如果出现误删表的情况,用备份恢复出误删之前的临时库,然后再把临时库中的表拷贝到生产库上,是恢复数据最快的方法。但是,这种方法的使用也有一定的局限性: 必须是全表拷贝,不能只拷贝部分数据; 需要到服务器上拷贝数据,在用户无法登录数据库主机的场景下无法使用; 由于是通过拷贝物理文件实现的,源表和目标表都是使用InnoDB引擎时才能使用。 用mysqldump生成包含INSERT语句文件的方法,可以在where参数增加过滤条件,来实现只导出部分数据。这个方式的不足之一是,不能使用join这种比较复杂的where条件写法 用select … into outfile的方法是最灵活的,支持所有的SQL写法。但,这个方法的缺点之一就是,每次只能导出一张表的数据,而且表结构也需要另外的语句单独备份 grant之后要跟着flushprivileges吗 grant语句会同时修改数据表和内存,判断权限的时候使用的是内存数据。因此,规范地使用grant和revoke语句,是不需要随后加上flush privileges语句的。 flush privileges语句本身会用数据表的数据重建一份内存权限数据,所以在权限数据可能存在不一致的情况下再使用。而这种不一致往往是由于直接用DML语句操作系统权限表导致的,所以我们尽量不要使用这类语句。 要不要使用分区表 相对于用户分表: 优势:对业务透明,使用分区表的业务代码更简洁,且可以很方便的清理历史数据 劣势:第一次访问的时候需要访问所有分区;共用MDL锁","categories":[{"name":"MySQL","slug":"MySQL","permalink":"https://zunpan.github.io/categories/MySQL/"}],"tags":[{"name":"MySQL","slug":"MySQL","permalink":"https://zunpan.github.io/tags/MySQL/"},{"name":"数据库","slug":"数据库","permalink":"https://zunpan.github.io/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"}]},{"title":"Java并发编程实战学习笔记","slug":"Java并发编程实战学习笔记","date":"2023-01-13T09:02:46.000Z","updated":"2023-09-24T04:27:40.278Z","comments":true,"path":"2023/01/13/Java并发编程实战学习笔记/","link":"","permalink":"https://zunpan.github.io/2023/01/13/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E6%88%98%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","excerpt":"","text":"本书分成四部分, 第一部分是基础,主要内容是并发的基础概念和线程安全,以及如何用java类库提供的并发构建块组成线程安全的类(2-5章节) 第二部分是构建并发应用程序,主要内容是如何利用线程来提高并发应用的吞吐量和响应能力(6-9章节) 第三部分是活跃性、性能和测试,主要内容是如何确保并发程序能够按照你的要求执行,并且具有可接受的性能(10-12章节) 第四部分是高级主题,涵盖了可能只有有经验的开发人员才会感兴趣的主题:显式锁、原子变量、非阻塞算法和开发自定义同步器(13-16章节) Part 1 基础 Chapter 1 简介 1.1 并发简史 早期计算机没有操作系统,程序独占所有资源,一次只能有一个程序运行,效率低下 操作系统出现后,程序(进程)可以并发运行,由操作系统分配资源,进程互相隔离,必要时依靠粗粒度的通信机制:sockets、signal handlers、shared memory等机制通信 进程提高了系统吞吐量和资源利用率,线程的出现也是这个原因,线程有时被称为轻量级进程,大多数现代操作系统将线程而不是进程视为调度的基本单位,同一程序的多个线程可以同时在多个CPU上调度,如果没有同步机制协调对共享数据的访问,一个线程可能会修改另一个线程正在使用的变量,导致不可预测的结果 1.2 线程优势 减小开发和维护开销 提高复杂应用的性能 提高GUI的响应能力 简化JVM实现 1.3 线程风险 竞争(多个线程以未知顺序访问资源) 活跃性(死锁,饥饿,活锁) 性能(频繁切换导致开销过大) 1.4 无处不在的线程 框架通过在框架线程中调用应用程序代码将并发性引入到应用程序中,在代码中将不可避免的访问应用程序状态,因此所有访问这些状态的代码路径都必须是线程安全的 Timer类,TimerTask在Timer管理的线程中执行 Servlet(每个请求使用一个线程同时执行Servlet) RMI(由RMI负责打包拆包远程对象) Swing(具有异步性) Chapter 2 线程安全 多个线程访问同一个可变的状态变量时没有使用合适的同步机制,可以用以下方法修复: 不在线程间共享该变量 将变量变为常量 访问时候使用同步 2.1 什么是线程安全? 如果一个类被多线程访问,不管线程调度或交叉执行顺序如何,类的表现都是正确的,那么类是线程安全的 线程安全类封装任何需要的同步,因此客户端不需要提供自己的同步。 无状态的对象永远是线程安全的 2.2 原子性 竞争情况(race condition):由于不恰当的执行顺序导致出现不正确的结果,常发生在以下情况中: 读取-修改-写入,例子: 自增操作 先检查后执行,例子:延迟初始化,不安全的单例模式,懒汉模式 第一种情况解决方式:使用juc里面的类,比如count可以用AtomicLong类型操作保证原子性 第二种情况解决方式:加锁保证原子性 2.3 加锁 如果类有多个变量需要更新,即使它们的各自操作都是原子性的,也要把他们放在同一个原子操作中,方式是加锁。Java 提供了锁机制来增强原子性:synchronized 内置锁: synchronized 实例方法会将被调用方法的对象作为内置锁或监视锁,内置锁是互斥的,同一时刻最多只有一个线程拿到这个锁 可重入: 内置锁是可重入的,已经拿到锁的线程可以再次获取锁,实现方式是锁会(就是lock对象)关联一个持有者和计数值,持有者再次进入次数加一,退出减一,减到0会释放锁 2.4 用锁来保护状态 混合操作比如说读取-修改-写入和先检查后执行,需要保证原子性来避免竞争情况。 常见错误: 只有写入共享变量才需要同步 原因:读取也需要同步,不然可能会看到过期值 每个共享的可变变量应该由同一个锁保护,常见的加锁习惯是将可变变量封装到一个对象中 对于不变性条件(invariant)中涉及的多个变量,这多个变量都需要用同一个锁保护,例如Servlet缓存了请求次数和请求数据(数组),不变性条件是请求数据的长度等于次数,这通过加锁来保证 2.5 活跃性和性能 给Servlet的方法声明syncronized极大降低了并发性,我们可以通过缩小同步块的范围,在保证线程安全的情况下提高并发性。合理的做法是将不影响共享状态的操作从同步块中排除 Chapter 3 共享对象 synchronized 块和方法可以确保操作的原子性执行,它还有另一个重要作用:内存可见性。我们不仅想要防止一个线程在另一个线程使用一个对象时修改它的状态,还想要确保当一个线程修改一个对象的状态时,其他线程可以看到最新更改 3.1 可见性 过期数据(当一个线程修改数据,但其他线程不能立马看到)。 读取操作如果不同步,仍然能看到一个过期数据,这叫做最低安全性(过期数据至少是由之前的线程设置的,而不是随机值) 大多数变量都满足最低安全性,除了非volatile修饰的64位变量(double和long),jvm允许将64位操作拆解为2个 32位操作,读取这样的变量可能会出现过期值的高32位+新值的低32位的结果 内置锁保证可见性 volatile: 保证可见性,禁止指令重排,不保证原子性(使用场合:保证自身可见性,引用对象状态可见性,标识重要的生命周期事件) 当且仅当满足以下所有条件时,才应该使用volatile变量: 对变量的写入不依赖于它的当前值,或者可以确保只有一个线程更新该值; 该变量不会与其他状态变量一起纳入不变性条件 在访问变量时,由于任何其他原因不需要加锁。 3.2 发布与逃逸 发布是指让对象在外部可见,常见方式是对象引用声明为 public static。发布对象的同时,任何通过非私有字段引用和方法调用链从发布对象中访问的对象也被发布了 逃逸是指对象的私有信息也对外可见了,比如发布一个对象包含一个私有数组,同时提供一个返回引用的get方法,外部可以通过引用修改内部私有数组 3.3 线程封闭 如果对象限制在一个线程中使用,即使对象不是线程安全的,也会自动线程安全 例子:Swing: 将组件和数据对象放到事件分发线程,其它线程访问不了这些对象;JDBC.Connection对象: 应用线程从数据库连接池中获取一个连接对象,连接对象由该线程独自使用 Java 提供 ThreadLocal 来实现线程封闭,程序员做的是阻止对象从线程中逃逸 线程封闭通常用来实现一个子系统,例如GUI,它是单线程的 Ad-hoc封闭: 核线程封闭性的职责完全由程序实现来承担(脆弱,少用) 栈封闭: 只能通过局部变量访问对象(Java基本类型或者局部变量) ThreadLocal类: 提供getter和setter,每个使用该变量的线程存有一份独立的副本 3.4 不可变 不可变对象永远是线程安全的 满足以下条件,对象才是不可变的: 构造函数之后状态不可修改 所有域都是final 对象正确创建(this引用没有在构造期间逃逸) 多个状态的对象需要保证线程安全,可以将状态封装到一个不可变类中,用volatile修饰不可变对象引用 3.5 安全发布 不正确的发布对象会出现两个问题:其它线程会看到null或旧值;最糟糕的是其它线程看到最新的引用但是被引用的对象还是旧的 由于不可变对象很重要,Java内存模型为不可变对象的共享提供一种特殊的初始化安全性保证,不用同步也能安全发布 一个正确构造的对象可以通过以下方式安全发布: 静态初始化函数中初始化一个对象引用 引用保存到volatile域或者AtomicReference对象中 引用保存到某个正确构造对象的final域 引用保存到锁保护的域(容器也可) 不可变对象,可以放宽到事实不可变对象(对象在发布之后不会改变状态) 可变对象必须通过安全方式发布,并且必须是线程安全的或者锁保护起来 并发程序共享对象实用策略 线程封闭 只读共享 线程安全共享:对象内部实现同步 保护对象:锁机制 Chapter 4 组合对象 本章讨论如何将线程安全的组件组合成更大的组件或程序 4.1 设计一个线程安全的类 在设计线程安全类的过程中,常会包含以下三个基本要素: 找出构成对象状态的所有变量。 找出约束状态变量的不变性条件和后验条件。 建立对象状态的并发访问管理策略。 如果不了解对象的不变性条件和后验条件,就无法确保线程安全 依赖状态的方法需要先满足某种状态才能运行,即先验条件。java提供了 wait and notify 机制来等待先验条件成立,它依赖内置锁。更简单的实现方法是用java类库的阻塞队列或者信号量 一般情况下,状态所属权是封装状态的类,除非类公开可变对象的引用,这时候类只有共享权 4.2 实例封闭 在对象中封装数据,通过使用对象方法访问数据,从而更容易确保始终在持有适当锁的情况下访问数据。 Java监视器模式:封装可变状态到对象中,使用对象的内置锁保护状态,使用私有锁对象更有优势 4.3 线程安全的委托 将线程安全的职责委托给线程安全的类,例如计数器类不做同步处理,依赖AtomicLong类型达到线程安全 可以将线程安全委托给多个基础状态变量,只要它们是独立的 委托失效:多个变量间有不变性条件,比如大小关系等,需要加锁,除非复合操作也可以委托给变量 如果一个状态变量是线程安全的,不参与任何限制其值的不变性条件,并且在任何操作中都没有禁止的状态转换,那么它就可以安全地发布。 4.4 给现有的线程安全类加功能 继承方式(可能会因为子父类加的锁不一样线程不安全) 客户端加锁,使用辅助类,若类的加锁依赖其它类,那么辅助类容易错误加锁 组合方式,加锁策略完全由组合类提供 4.5 文档化同步策略 为类的客户端记录线程安全保证;为其维护者记录其同步策略 Chapter 5 基础构建模块 在实际应用中,委托是创建线程安全类最有效的策略之一,本章介绍平台库的并发构建模块,例如线程安全的集合和各种可以协调线程控制流的同步器 5.1 同步集合 Vector、Hashtable,以及JDK1.2增加了 Collections.synchronizedXxx 创建同步包装类 复合线程安全的类的方法可能不是线程安全的,例如复合方法调用size和get方法,中间可能被删掉元素导致size结果不对 迭代器或者for-each不会锁定集合,在迭代过程中检测到集合变化时会抛出ConcurrentModificationException异常,检测是通过检测count值,但是没有同步,可能看到过期值 隐藏的迭代器(某些操作底层隐藏着调用迭代器,比如集合的toString) 5.2 并发集合 同步集合通过序列化对集合状态的所有访问来实现线程安全,性能低。Java 5增加了并发集合 ConcurrentHashMap,使用分段锁,具有弱一致性,同时size和isEmpty是估计并不精确,只有需要独占Map,才不建议使用该Map CopyOnWriteArrayList,每次修改都是返回副本,建议迭代多修改少的时候使用 5.3 阻塞队列和生产者-消费者模式 BlockingQueue,常用来实现生产者和消费者,有一个特殊的实现SynchronousQueue,它不是一个实际的队列,当生产者生产数据时直接交给消费者,适用于消费者多的场景 Deque,常用来实现工作窃取模式。生产者和消费者模式中,消费者共享一个队列,工作窃取模式中,消费者有独自的队列,当消费完后会偷其他人的工作。工作窃取模式可以减少对于共享队列的竞争 5.4 阻塞方法与中断方法 当某个方法抛出InterruptedException,说明该方法是阻塞方法,可以被中断 代码中调用一个阻塞方法(阻塞方法和线程状态没有必然关系,方法可能是个长时间方法所以声明抛出InterruptedException,也有可能是会导致线程状态改变的sleep方法),必须处理中断响应. 捕获/抛出异常 恢复中断.调用当前线程的interrupt 5.5 同步器 阻塞队列 闭锁(Latch): 延迟线程进度,直到条件满足,FutureTask也可以做闭锁 信号量:类似发布凭证,但是任意线程都可以发布和返还 栅栏: 阻塞一组线程,直到某个条件满足;如果有某个线程await期间中断或者超时,所有阻塞的调用都会终止并抛出BrokenBarrierException 5.6 构建高效且可伸缩的缓存 使用hashMap+synchronized,性能差 ConcurrentHashMap代替hashMap+synchronized,有重复计算问题 ConcurrentHashMap的值用FutureTask包起来,只要键已经存在,从FutureTask获取结果,因为check-then-act模式,仍然存在重复计算问题 使用putIfAbsent设置缓存 Part 2 构建并发应用程序 Chapter 6 任务执行 6.1 在线程中执行任务 串行执行任务(响应会慢,服务器资源利用率低) 显式为每个请求申请一个线程 任务处理线程从主线程分离,提高响应速度 任务可以并行处理,提高吞吐量 任务处理代码必须是线程安全的,多个线程会并发执行 无限制创建线程的不足 创建销毁浪费时间 浪费资源 稳定性差 6.2 Executor框架 Executor基于生产-消费模式,提交任务相当于生产者,执行任务的线程相当于消费者. 执行策略 What: 在什么线程中执行任务,按什么顺序执行,任务执行前后要执行什么操作 How Many: 多少任务并发,多少等待 Which: 系统过载时选择拒绝什么任务 How: 怎么通知任务成功/失败 线程池,管理一组同构工作线程的资源池,跟工作队列密切相关 Executor生命周期 运行 : 对象新建时就是运行状态 关闭 : 不接受新任务,同时等待已有任务完成,包括未执行的任务,关闭后任务再提交由 “被拒绝的执行处理器” 处理或者直接抛异常 终止 : 关闭后任务完成 延迟任务和周期任务 Timer类可以负责,但是存在缺陷,应该考虑ScheduledThreadPoolExecutor代替它 Timer: 只用一个线程执行定时任务,假如某个任务耗时过长,会影响其他任务的定时准确性。除此之外,不支持抛出异常,发生异常将终止线程(已调度(scheduled)未执行的任务,线程不会执行,新任务不会调度,称为线程泄露) DelayQueue: 阻塞队列的一种实现,为ScheduledThreadPoolExecutor提供调度策略 6.3 寻找可利用的并行性 将耗时的IO使用别的线程获取;而不是简单的串行执行 Future 表示一个任务的生命周期,并提供相应的方法判断完成/取消,get会阻塞或抛异常 使用Callable和Future并行化下载和渲染 异构任务并行化获取重大性能提升很困难. 任务大小不同 负载均衡问题 协调开销 CompletionService 将 Executor 和BlockingQueue结合在一起,Executor是生产者,CompletionService是消费者 使用 CompletionService 并行化下载和渲染 为任务设置时限 需要获取多个设置了时限的任务的结果可以用带上时间的 invokeAll 提交多个任务 Chapter 7 取消和关闭 本章讲解如何停止任务和线程,Java没有安全强制线程停止的方法,只有一种协作机制,中断 7.1 任务取消 有一种协作机制是在任务中设置取消位,任务定期查看该标识,假如置位就结束任务(假如线程阻塞了,就看不到取消位,那么就停不下来了) 中断: 在取消任务或线程之外的其他操作中使用中断是不合适的 每个线程都有一个中断标志,interrupt中断目标线程,isInterrupted返回目标线程的中断状态,interrupted(糟糕的命名)清除当前线程中断; Thread.sleep和Object.wait都会检查线程什么时候中断,发现时提前返回(不会立即响应,只是传递请求而已) 中断策略:尽快推迟执行流程,传递给上层代码;由于每个线程拥有各自的中断策略,除非知道中断对这个线程的含义,否则不应该中断该线程 中断响应 当调用会抛出InterruptedException的阻塞方法时,有两种处理策略 传播异常,让你的方法也变成会抛出异常的阻塞方法(中断标志一直为true) 恢复中断状态,以便调用堆栈上较高级的代码处理它(try-catch之后中断标志为false,可以调用当前线程的interrupt方法恢复成中断状态)。 在中断线程之前,要了解线程的中断策略 通过Future取消任务 处理不可中断的阻塞 java.io中的同步Socket I/O.通过关闭Socket可以使阻塞线程抛出异常 java.io中的同步 I/O.终端一个InterruptibleChannel会抛出异常并关闭链路 获取某个锁. Lock提供lockInterruptibly 通过 newTaskFor 方法进一步优化 7.2 停止基于线程的服务 基于线程的服务:拥有线程的服务,例如线程池 只要拥有线程的服务的生命周期比创建它的方法的生命周期长,就提供生命周期方法。例如线程池 ExecutorService 提供了shutdown 日志服务:多生产者写入消息到阻塞队列,单消费者从阻塞队列中取消息,停止日志服务需要正确关闭线程。需要对结束标志位和队列剩余消息数同步访问(书有错误,LoggerThread 应该 synchronized (LogService.this)) 毒丸,生产者将毒丸放在队列上,消费者拿到毒丸就结束 shutdownNow 取消正在执行的任务,返回已提交未开始的任务,可以用个集合保存执行中被取消的任务 7.3 处理非正常的线程终止 通常是因为抛出运行时异常导致线程终止 处理方法: try-catch 捕获任务异常,如果不能恢复,在finally块中通知线程拥有者 当线程因未捕获异常而退出时,JVM会将事件报告给线程拥有者提供的UncaughtExceptionHandler,如果没有处理程序就将堆栈打印到System.err 通过execute提交的任务的异常由UncaughtExceptionHandler处理,submit提交的任务,通过调用Future.get方法,包装在ExecutionException里面 7.4 JVM关闭 有序关闭:最后一个非守护线程终止(可能是调用了System.exit,或者发送SIGINT或按Ctrl-C)后终止 突然关闭:通过操作系统终止JVM进程,例如发送SIGKIll 有序关闭中,JVM首先启动所有已注册的关闭钩子(通过Runtime.addShutdownHook注册的未启动线程)。如果应用程序线程在关闭时仍在运行,将与关闭线程并行执行。当所有关闭钩子都完成时,如果runFinalizersOnExit为true,那么jvm可能运行终结器,然后停止 守护线程:执行辅助功能的线程,不会阻止JVM关闭。当JVM关闭时,守护线程直接关闭,不执行 finally 块,栈不会展开。守护线程适合做“内务”任务,例如清缓存 终结器:GC在回收对象后会执行 finalize 方法释放持久资源。终结器在JVM管理的线程中运行,需要同步访问。终结器难写且性能低,除非要关闭 native 方法获取的资源,否则在 finally中显示关闭就够了 Chapter 8 使用线程池 本章将介绍配置和调优线程池的高级选项,描述使用任务执行框架时需要注意的危险 8.1 任务和执行策略之间的隐式耦合 Executor 框架在任务提交和执行之间仍存在一些耦合: 依赖其它任务的任务,相互依赖可能导致活跃性问题 利用线程封闭的任务,这类任务不做同步,依赖单线程执行 响应时间敏感的任务,可能需要多线程执行 使用 ThreadLocal 的任务,ThreadLocal不应该用于线程池中任务之间的通信 8.1.1 线程饥饿死锁 把相互依赖的任务提交到一个单线程的Executor一定会发生死锁。增大线程池,如果被依赖的任务在等待队列中,也会发生死锁 8.1.2 运行耗时长的任务 即使不出现死锁,也会降低性能,通过限制执行时间可以缓解 8.2 设置线程池大小 cpu数可以调用 Runtime.availableProcessors得出 计算密集型场景,线程池大小等于cpu数+1 IO密集型场景,线程池大小等于cpu数 * cpu利用率 * (1+等待/计算时间比) 8.3 配置 ThreadPoolExecutor 8.3.1 线程创建和销毁 corePoolSize:线程池大小,只有工作队列满了才会创建超出这个数量的线程 maximumPoolSize:最大线程数量 keepAliveTime:空闲时间超过keepAliveTime的线程会成为回收的候选线程,如果线程池的大小超过了核心的大小,线程就会被终止 8.3.2 管理工作队列 可以分成三类:无界队列、有界队列和同步移交。队列的选择和线程池大小、内存大小的有关 无界队列可能会耗尽资源,有界队列会带来队列满时新任务的处理问题,同步移交只适合用在无界线程池或饱和策略可以接受 8.3.3 饱和策略 当任务提交给已经满的有界队列或已经关闭的Executor,饱和策略开始工作 Abort,默认策略,execute方法会抛RejectedExecutionException Discard:丢弃原本下个执行的任务,并重新提交新任务 Caller-Runs:将任务给调用execute 的线程执行 无界队列可以使用信号量进行饱和策略 8.3.4 线程工厂 通过ThreadFactory.newThread创建线程,自定义线程工厂可以在创建线程时设置线程名、自定义异常 8.3.5 调用构造函数后再定制ThreadPoolExecutor 线程池的各项配置可以通过set方法配置,如果不想被修改,可以调用Executors.unconfigurableExecutorService 将其包装成不可修改的线程池 8.4 扩展 ThreadPoolExecutor ThreadPoolExecutor给子类提供了钩子方法,beforeExecute、afterExecute和terminated beforeExecute和afterExecute钩子在执行任务的线程中调用,可用于添加日志记录、计时、监控或统计信息收集。无论任务从run正常返回还是抛出异常,afterExecute钩子都会被调用。如果beforeExecute抛出一个RuntimeException,任务就不会执行,afterExecute也不会被调用 terminated钩子在任务都完成且所有工作线程都关闭后调用,用来释放资源、执行通知或日志记录 8.5 递归算法并行化 如果迭代操作之间是独立的,适合并行化 递归不依赖于后续递归的返回值 Chapter 9 GUI应用 9.1 为什么GUI是单线程的 由于竞争情况和死锁,多线程GUI框架最终都变成了单线程 9.1.1 串行事件处理 优点:代码简单 缺点:耗时长的任务会发生无响应(委派给其它线程执行) 9.1.2 Swing的线程封闭 所有Swing组件和数据模型对象都封闭在事件线程中,任何访问它们的代码必须在事件线程里 9.2 短时间的GUI任务 事件在事件线程中产生,并冒泡到应用程序提供的监听器 Swing将大多数可视化组件分为两个对象(模型对象和视图对象),模型对象保存数据,可以通过引发事件表示模型发生变化,视图对象通过订阅接收事件 9.3 长时间的GUI任务 对于长时间的任务可以使用线程池 取消 使用Future 进度标识 9.4 共享数据模型 只要阻塞操作不会过度影响响应性,那么事件线程和后台线程就可以共享该模型 分解数据模型.将共享的模型通过快照共享 9.5 其它形式单线程 为了避免同步或死锁使用单线程,例如访问native方法使用单线程 Part 3 活跃性、性能和测试 Chapter 10. 避免活跃性危险 Java程序不能从死锁中恢复,本章讨论活跃性失效的一些原因以及预防措施 10.1 死锁 哲学家进餐问题:每个人都有另一个人需要的资源,并且等待另一个人持有的资源,在获得自己需要的资源前不会释放自己持有的资源,产生死锁 10.1.1 Lock-ordering死锁 线程之间获取锁的顺序不同导致死锁。 解决方法:如果所有线程以一个固定的顺序获取锁就不会出现Lock-ordering死锁 10.1.2 动态Lock Order死锁 获取锁的顺序依赖参数可能导致死锁。 解决方法:对参数进行排序,统一线程获取锁的顺序 10.1.3 协作对象的死锁 如果在持有锁时调用外部方法,将会出现活跃性问题,这个外部方法可能阻塞,加锁等导致其他线程无法获得当前被持有的锁 解决方法:开放调用 10.1.4 开放调用 如果在方法中调用外部方法时不需要持有锁(比如调用者this),那么这种调用称为开放调用。实现方式是将调用者的方法的同步范围从方法缩小到块 10.1.5 资源死锁 和循环依赖锁导致死锁类似。例如线程持有数据库连接且等待另一个线程释放,另一个线程也是这样 10.2 避免和诊断死锁 使用两部分策略来审计代码以确保无死锁:首先,确定哪些地方可以获得多个锁(尽量使其成为一个小集合),然后对所有这些实例进行全局分析,以确保锁的顺序在整个程序中是一致的,尽可能使用开放调用简化分析 10.2.1 定时锁 另一种检测死锁并从死锁中恢复的技术是使用显示锁中的Lock.tryLock()代替内置锁 10.2.2 用Thread Dumps进行死锁分析 线程转储包含每个运行线程的堆栈信息,锁信息(持有哪些锁,从哪个栈帧中获得)以及阻塞的线程正在等待获得哪些锁 10.3 其它活跃性危险 10.3.1 饥饿 线程由于无法获得它所需要的资源而不能继续执行,最常见的资源是CPU 避免使用线程优先级,可能导致饥饿 10.3.2 糟糕的响应性 计算密集型任务会影响响应性,通过降低执行计算密集型任务的线程的优先级可以提高前台任务的响应性 10.3.3 活锁 线程执行任务失败后,任务回滚,又添加到队列头部,导致线程没有阻塞但永远不会有进展。多个相互合作的线程为了响应其它线程而改变状态也会导致活锁 解决方法:在重试机制中引入一些随机性 Chapter 11. 性能和可伸缩性 11.1 对性能的思考 11.1.1 性能和可伸缩性 性能: 可以用任务完成快慢或者数量来衡量,具体指标包括服务时间、延迟、 吞吐量、可伸缩性等 可伸缩性: 增加计算资源时提供程序吞吐量的能力 11.1.2 评估性能权衡 许多性能优化牺牲可读性和可维护性,比如违反面向对象设计原则,需要权衡 11.2 Amdahl定律 N:处理器数量 F:必须串行执行的计算部分 Speedup:加速比 $\\text { Speedup } \\leq \\frac{1}{F+\\frac{1-F}{N}} $ 串行执行的计算部分需要仔细考虑,即使任务之间互不影响可以并行,但是线程从任务队列中需要同步,使用ConcurrentLinkedQueue比同步的LinkedList性能好 11.3 线程引入的开销 上线文切换 内存同步(同步的性能开销包括可见性保证,即内存屏障,可以用jvm逃逸分析和编译器锁粒度粗化进行优化) 阻塞(非竞争的同步可以在JVM处理,竞争的同步需要操作系统介入,竞争失败的线程必定阻塞,JVM可以自旋等待(反复尝试获取锁,直到成功)或者被操作系统挂起进入阻塞态,短时间等待选择自旋等待,长时间等待选择挂起) 11.4 减少锁的竞争 并发程序中,对伸缩性最主要的威胁就是独占方式的资源锁 三种减少锁争用的方法: 减少持有锁的时间 减少请求锁的频率 用允许更大并发的协调机制替换互斥锁 11.4.1 减小锁的范围 锁的范围即持有锁的时间 11.4.2 降低锁的力度 分割锁:将保护多个独立的变量的锁分割成单独的锁,这样锁的请求频率就可以降低 11.4.3 分段锁 分割锁可以扩展到可变大小的独立对象上的分段锁。例如ConcurrentHashMap使用了一个包含16个锁的数组,每个锁保护1/16的哈希桶 分段锁缺点:独占访问集合开销大 11.4.4 避免热点字段 热点字段:缓存 热点字段会限制可伸缩性,例如,为了缓存Map中的元素数量,添加一个计数器,每次增删时修改计数器,size操作的开销就是O(1)。单线程没问题,多线程下又需要同步访问计数器,ConcurrentHashMap每个哈希桶一个计数器 11.4.5 互斥锁的替代品 考虑使用并发集合、读写锁、不可变对象和原子变量 读写锁:只要没有一个写者想要修改共享资源,多个读者可以并发访问,但写者必须独占地获得锁 原子变量:提供细粒度的原子操作,可以降低更新热点字段的开销 11.4.6 监测CPU利用率 cpu利用率低可能是以下原因: 负载不够,可以对程序加压 IO密集,可以通过iostat判断,还可以通过监测网络上的流量水平判断 外部约束,可能在等待数据库或web服务的响应 锁竞争,可以用分析工具分析哪些是“热”锁 11.4.7 不要用对象池 现在JVM分配和回收对象已经很快了,不要用对象池 11.5 例子:比较Map的性能 ConcurrentHashMap单线程性能略好于同步的HashMap,并发时性能超好。ConcurrentHashMap对大多数成功的读操作不加锁,对写操作和少数读操作加分段锁 11.6 减少上下文切换 日志记录由专门的线程负责 请求服务时间不应该过长 将IO移动到单个线程 Chapter 12. 并发程序的测试 大多数并发程序测试安全性和活跃性。安全性可以理解为“永远不会发生坏事”,活跃性可以理解为“最终会有好事发生” 12.1 正确性测试 12.1.1 基础单元测试 和顺序程序的测试类似,调用方法,验证程序的后置条件和不变量 12.1.2 阻塞操作测试 在单独的一个线程中启动阻塞活动,等待线程阻塞,中断它,然后断言阻塞操作完成。 Thread.getState不可靠,因为线程阻塞不一定进入WAITING或TIMED_WAITING状态,JVM可以通过自旋等待实现阻塞。类似地,Object.wait和Condition.wait存在伪唤醒情况,处于WAITING或TIMED_WAITING状态的线程可以暂时过渡到RUNNABLE。 12.1.3 安全性测试 给并发程序编写高效的安全测试的挑战在于识别出容易检查的属性,这些属性在程序错误时出错,同时不能让检查限制并发性,最好检查属性时不需要同步。 生产者和消费者模式中的一种方法是校验和,单生产者单消费者可以使用顺序敏感的校验和计算入队和出队元素的校验和,多生产者多消费者要用顺序不敏感的校验和 12.1.4 资源管理测试 任何保存或管理其他对象的对象都不应该在不必要的时间内继续维护对这些对象的引用。可以用堆检查工具测试内存使用情况 12.1.5 使用回调 回调函数通常是在对象生命周期的已知时刻发出的,这是断言不变量的好机会。例如自定义线程池可以在创建销毁线程时记录线程数 12.1.6 产生更多的交替操作 Thread.yield放弃cpu,保持RUNNABLE状态,重新竞争cpu Thread.sleep放弃cpu进入TIME_WAITING状态,不竞争cpu,sleep较小时间比yield更稳定产生交替操作 tips:Java 线程的RUNNABLE 状态对应了操作系统的 ready 和 running 状态,TIME_WAITING(调用Thread.sleep) 和 WAITING(调用Object.wait) 和 BLOCKED(没有竞争到锁) 对应 waiting 状态。interrupt是种干预手段,如果interrupt一个RUNNABLE线程(可能在执行长时间方法需要终止),如果方法声明抛出InterruptedException,就表示可中断,方法会循环检查isInterrupted状态来响应interrupt,一般情况线程状态变成TERMINATED。如果interrupt一个 waiting 线程(可能是由sleep、wait方法导致,这些方法会抛出InterruptedException),线程重新进入 RUNNALBE 状态,处理InterruptedException 12.2 性能测试 增加计时功能(CyclicBarrier) 多种算法比较(LinkedBlockingQueue在多线程情况下比ArrayBlockingQueue性能好) 衡量响应性 12.3 避免性能测试的陷阱 12.3.1 垃圾回收 垃圾回收不可预测,会导致测试误差,需要长时间测试,多次垃圾回收,得到更准确结果 12.3.2 动态编译 动态编译会影响运行时间,需要运行足够长时间或者与完成动态编译后再开始计时 12.3.3 对代码路径的不真实采样 动态编译器会对单线程测试程序进行优化,最好多线程测试和单线程测试混合使用(测试用例至少用两个线程) 12.3.4 不真实的竞争情况 并发性能测试程序应该尽量接近真实应用程序的线程本地计算,并考虑并发协作。例如,多线程访问同步Map,如果本地计算过长,那么锁竞争情况就较弱,可能得出错误的性能瓶颈结果 12.3.5 无用代码的删除 无用代码:对结果没有影响的代码 由于基准测试通常不计算任何东西,很容易被优化器删除,这样测试的执行时间就会变短 解决方法是计算某个派生类的散列值,与任意值比较,加入相等就输出一个无用且可被忽略的消息 12.4 补充的测试方法 代码审查 静态代码分析 面向切面的测试工具 分析与检测工具 Part 4 高级主题 Chapter 13 显示锁 Java 5 之前,对共享数据的协调访问机制只有 synchronized 和 volatile,Java 5 增加了 ReentrantLock。 13.1 Lock和ReentrantLock Lock接口定义加锁和解锁的操作。 ReentrantLock还提供了可重入特性 显示锁和内置锁很像,显示锁出现的原因是内置锁有一些功能限制 不能中断等待锁的线程 必须在获得锁的地方释放锁 13.1.1 轮询和定时获得锁 tryLock:轮询和定时获得锁 内置锁碰到死锁是致命的,唯一恢复方法是重启,唯一防御方法是统一锁获取顺序,tryLock可以概率避免死锁 13.1.2 可中断的获得锁 lockInterruptibly,调用后一直阻塞直至获得锁,但是接受中断信号 13.1.3 非块结构加锁 内置锁是块结构的加锁和自动释放锁,有时需要更大的灵活性,例如基于hash的集合可以使用分段锁 13.2 性能考虑 从Java 6 开始,内置锁已经不比显式锁性能差 13.3 公平性 内置锁不保证公平,ReentrantLock默认也不保证公平,非公平锁可以插队(不提倡,但是不阻止),性能相比公平锁会好一些 13.4 在 Synchronized 和 ReentrantLock 中选择 当你需要用到轮询和定时加锁、可中断的加锁、公平等待锁和非块结构加锁,使用 ReentrantLock,否则使用 synchronized 13.5 读写锁 读写锁:资源可以被多个读者同时访问或者单个写者访问 ReadWriteLock 定义读锁和写锁方法,和 Lock 类似,实现类在性能、调度、获得锁的优先条件、公平等方面可以不同 Chapter 14 构建自定义的同步工具 最简单的方式使用已有类进行构造,例如LinkedBlockingQueue、CountDown-Latch、Semaphore和FutureTask等 14.1 状态依赖性的管理 单线程中,基于状态的前置条件不满足就失败。但是多线程中,状态会被其它线程修改,所以多线程程序在不满足前置条件时可以等待直至满足前置条件 14.1.1 将前置条件的失败传播给调用者 不满足前置条件就抛异常是滥用异常。调用者可以自旋等待(RUNNABLE态,占用cpu)或者阻塞(waiting态,不占cpu),即需要调用者编写前置条件管理的代码 14.1.2 通过轮询和睡眠粗鲁的阻塞 通过轮询和睡眠完成前置条件管理,不满足是就阻塞,调用者不需要管理前置条件,但需要处理 InterruptedException 14.1.3 条件队列 Object的wait,notify 和 notifyAll构成内置条件队列的API,wait会释放锁(本质和轮询与休眠是一样的,注意sleep前要释放锁) 14.2 使用条件队列 14.2.1 条件谓词 条件谓词:由类的状态变量构造的表达式,例如缓冲区非空即count>0 给条件队列相关的条件谓词以及等待它成立的操作写Javadoc 条件谓词涉及状态变量,状态变量由锁保护,所以在测试条件谓词之前,需要先获得锁。锁对象和条件队列对象(调用wait和notify的对象)必须是同一个对象 14.2.2 过早唤醒 一个线程由于其它线程调用notifyAll醒来,不意味着它的等待条件谓词一定为真。每当线程醒来必须再次测试条件谓词(使用循环) 14.2.3 丢失的信号 线程必须等待一个已经为真的条件,但是在开始等待之前没有检查条件谓词,发生的原因是编码错误,正确写法是循环测试条件谓词,false就继续wait 14.2.4 通知 优先使用 notifyAll,notify 可能会出现“hijacked signal”问题,唤醒了一个条件还未真的线程,本应被唤醒的线程还在等待。只有所有线程都在等同一个条件谓词且通知最多允许一个线程继续执行才使用notify 14.2.5 例子:门 用条件队列实现一个可以重复开关的线程门 14.2.6 子类的安全问题 一个依赖状态的类应该完全向子类暴露它的等待和通知协议,或者禁止继承 14.2.7 封装条件队列 最好将条件队列封装起来,在使用它的类的外面无法访问 14.2.8 进入和退出协议 进入协议:操作的条件谓词 退出协议:检查该操作修改的所有状态变量,确认他们是否使某个条件谓词成真,若是,通知相关队列 14.3 显示 Condition 显示锁在一些情况下比内置锁更灵活。类似地,Condition 比内置条件队列更灵活 内置条件队列有几个缺点: 每个内置锁只能关联一个条件队列,即多个线程可能会在同一个条件队列上等待不同的条件谓词 最常见的加锁模式会暴露条件队列 一个 Condition 关联一个 Lock,就像内置条件队列关联一个内置锁,使用 Lock.newCondition 来创建 Condition,Condition比内置条件队列功能丰富:每个锁有多个等待集(即一个Lock可以创建多个Condition),可中断和不可中断的条件等待,基于截止时间的等待,以及公平或不公平排队的选择。 Condition中和wait,notify,notifyAll 对应的方法是 await,signal,signalAll 14.4 Synchronizer剖析 ReentrantLock 和 Semaphore 有很多相似的地方。ReentrantLock可以作为只许一个线程进入的 Semaphore,Semaphore 可以用 ReentrantLock 实现 它们和其它同步器一样依赖基类 AbstractQueuedSynchronizer(AQS)。AQS是一个用于构建锁和同步器的框架,使用它可以轻松有效地构建范围广泛的同步器。 14.5 AbstractQueuedSynchronizer 依赖AQS的同步器的基本操作是获取和释放的一些变体。获取是阻塞操作,调用者获取不到会进入WAITING或失败。对于锁或信号量,获取的意思是获取锁或许可。对于CountDownLatch,获取的意思是等待门闩到达终点。对于FutureTask,获取的意思是等待任务完成。释放不是阻塞操作。同步器还会根据各自的语义维护状态信息 14.6 JUC同步器类中的AQS JUC许多类使用AQS,例如 ReentrantLock, Semaphore, ReentrantReadWriteLock, CountDownLatch, SynchronousQueue, FutureTask Chapter 15 原子变量与非阻塞同步机制 非阻塞算法使用原子机器指令,例如 compare-and-swap 取代锁来实现并发下的数据完成性。它的设计比基于锁的算法复杂但可以提供更好的伸缩性和活跃性,非阻塞算法不会出现阻塞、死锁或其它活跃性问题,不会受到单个线程故障的影响 15.1 锁的劣势 JVM对非竞争锁进行优化,但是如果多个线程同时请求锁,就要借助操作系统挂起或者JVM自旋,开销很大。相比之下volatile是更轻量的同步机制,不涉及上下文切换和线程调度,然后volatile相较于锁,它不能构造原子性的复合操作,例如自增 锁还会出现优先级反转(阻塞线程优先级高,但是后执行),死锁等问题 15.2 并发操作的硬件支持 排它锁是悲观锁,总是假设最坏情况,只有确保其它线程不会干扰才会执行 乐观方法依赖碰撞检测来确定在更新过程中是否有其它线程的干扰,若有则操作失败并可以选择重试 为多处理器设计的cpu提供了对共享变量并发访问的特殊指令,例如 compare‐and‐swap,load-linked 15.2.1 Compare and Swap CAS 有三个操作数:内存地址V,期待的旧值A,新值B。CAS在V的旧值是A的情况下原子更新值为B,否则什么都不做。CAS是一种乐观方法:它满怀希望更新变量,如果检测到其它线程更新了变量,它就会失败。CAS失败不会阻塞,允许重试(一般不重试,失败可能意味着别的线程已经完成该工作) 15.2.2 非阻塞的计数器 在竞争不激烈的情况下,性能比锁优秀。缺点是强制调用者处理竞争问题(重试、后退或放弃),而锁通过阻塞自动处理争用,直到锁可用 15.2.3 JVM对CAS的支持 JVM将CAS编译成底层硬件提供的方法,加入底层硬件不支持CAS,JVM会使用自旋锁。原子变量类使用了CAS 15.3 原子变量类 原子变量比锁的粒度更细,重量更轻,能提供volatile不支持的原子性 原子变量可以作为更好的 volatile 在高度竞争情况下,锁性能更好,正常情况下,原子变量性能更好 15.4 非阻塞的算法 如果一个线程的故障或挂起不会导致另一个线程的故障或挂起,则该算法称为非阻塞算法;如果一个算法在每个执行步骤中都有线程能够执行,那么这个算法被称为无锁算法。如果构造正确,只使用CAS进行线程间协调的算法可以是无阻塞和无锁的 15.4.1 非阻塞的栈 创建非阻塞算法的关键是如何在保持数据一致性的同时,将原子性更改的范围限制在单个变量内。 非阻塞的栈使用CAS来修改顶部元素 15.4.2 非阻塞的链表 Michale-scott算法 15.4.3 原子字段更新器 原子字段更新器代表了现有 volatile 字段的基于反射的“视图”,以便可以在现有的 volatile 字段上使用CAS 15.4.4 ABA问题 大部分情况下,CAS会询问“V的值还是A吗?”,是A就更新。但是有时候,我们需要知道“从我上次观察到V是A以来,它的值有没有改变?”。对于某些算法,将V的值从A->B,再从B->A,是一种更改,需要重新执行算法。解决方法是使用版本号,即使值从A变成B再变回A,版本号也会不同 Chapter 16 Java 内存模型 16.1 内存模型是什么,为什么我需要一个内存模型? 在并发没有同步的情况下,有许多原因导致一个线程不能立即或永远不能再另一个线程中看到操作的结果 编译器生成的指令顺序与源码不同 变量存在寄存器中而不是内存中 处理器可以并行或乱序执行指令 缓存可能会改变写入变量到主内存的顺序 存储在处理器本地缓存中的值可能对其它处理器不可见 16.1.1 平台的内存模型 在共享存储的多处理器体系结构中,每个处理器都有自己的高速缓存,这些告诉缓存周期性地与主存协调。 体系结构的内存模型告诉程序可以从内存系统得到什么一致性保证,并制定所需的特殊指令(内存屏障或栅栏),以在共享数据时获得所需的额外内存协调保证。为了不受跨体系结构的影响,Java提供了自己的内存模型,JVM通过在适当的位置插入内存屏障来处理JMM(Java内存模型)和和底层平台的内存模型之间的差异 顺序一致性:程序中所有操作都有一个单一的顺序,而不管他们在什么处理器上执行,并且每次读取变量都会看到任何处理器按执行顺序对该变量的最后一次写入。 现代处理器没有提供顺序一致性,JMM也没有 16.1.2 重排 指令重排会使程序的行为出乎意料。同步限制了编译器、运行时和硬件在重排序时不会破坏JMM提供的可见性保证 16.1.3 Java 内存模型 Java 内存模型由一些操作指定,包括对变量的读写、监视器的锁定和解锁。JMM对所有操作定义了一个称为 happens before 的偏序规则: 程序顺序规则:线程按程序定义的顺序执行操作 监视器锁规则:监视器锁的解锁必须发生在后续的加锁之前 volatile 变量规则:对 volatile 字段的写入操作必须发生在后续的读取之前 线程启动规则:对线程调用Thread.start会在该线程所有操作之前执行 线程结束规则:线程中任何操作必须在其它线程检测到该线程已经结束之前执行或者从Thread.join返回或者Thread.isAlive返回false 中断规则:一个线程对另一个线程调用 interrupt 发生在被中断线程检测到中断之前 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成 传递性:A发生在B之前,B发生在C之前,那么A发生在C之前 16.1.4 借用同步 通过类库保证 happens-before顺序: 将元素放入线程安全的容器发生在另一个线程从集合中检索元素之前 在CountDownLatch上进行倒数发生在该线程从门闩的await返回之前 释放信号量的许可发生在获取之前 Future代表的任务执行的操作发生在另一个线程从Future.get返回之前 提交Runnable或者Callable任务给执行器发生在任务开始之前 线程到达CyclicBarrier或Exchanger发生在其它线程释放相同的barrier或者exchange point之前 16.2 发布 16.2.1 不安全的发布 当缺少happens-before关系时候,就可能出现重排序问题,这就解释了为什么在没有同步情况下发布一个对象会导致另一个线程看到一个只被部分构造的对象 除了不可变对象之外,使用由其他线程初始化的对象都是不安全的,除非对象的发布发生在消费线程使用它之前 16.2.2 安全发布 使用锁或者volatile变量可以确保读写操作按照 happens-before 排序 16.2.3 安全初始化 静态字段在声明时就初始化由JVM提供线程安全保证。 延迟初始化,可以写在同步方法里面,或者使用辅助类,在辅助类中声明并初始化 16.2.4 双重检查锁 Java 5之前的双重检查锁会出现引用是新值但是对象是旧值,这意味着可以看到对象不正确的状态,Java5之后给引用声明加上 volatile 可以起到线程安全地延迟初始化作用,但是不如使用辅助类,效果一样且更容易懂 16.3 初始化的安全性 初始化安全性只能保证通过final字段可达的值从构造过程完成时开始的可见性(事实不可变对象以任何形式发布都是安全的)。对于通过非final字段可达的值,或者构成完成之后可能改变的值,必须采用同步确保可见性。","categories":[{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/categories/Java/"}],"tags":[{"name":"Java并发编程实战","slug":"Java并发编程实战","permalink":"https://zunpan.github.io/tags/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E6%88%98/"},{"name":"Java Concurrency In Practice","slug":"Java-Concurrency-In-Practice","permalink":"https://zunpan.github.io/tags/Java-Concurrency-In-Practice/"}]},{"title":"2022.06-2022.11实习总结","slug":"2022.06-2022.11实习总结","date":"2022-12-03T11:29:40.000Z","updated":"2023-09-24T04:27:40.272Z","comments":true,"path":"2022/12/03/2022.06-2022.11实习总结/","link":"","permalink":"https://zunpan.github.io/2022/12/03/2022.06-2022.11%E5%AE%9E%E4%B9%A0%E6%80%BB%E7%BB%93/","excerpt":"","text":"基于JSON Schema的配置平台 背景:这是公司的新人入职练手项目,总共两周时间,实现一个基于JSON Schema的表单配置平台。 需求:表单配置指的是通过可视化界面配置 表单的每个字段,包括名称、输入类型、输入限制等。mentor给了一个方向叫JSON Schema 调研:JSON Schema是描述json数据的元数据,本身也是json字符串,一般两个作用,1. 后端用 JSON Schema 对前端传的json传进行格式校验;2. 前端通过 JSON Schema生成表单 开发:前端使用Vue、后端使用Spring Boot 难点:JSON Schema和表单的双向转换。用户可以手动编辑JSON Schema生成表单项,也可以通过可视化界面增加表单项来修改JSON Schema。 解决方案:尝试过写一套解析方案,但是dom操作太复杂作罢。调研了一些开源方案,最终选用vue-json-schema-form Excel比对与合并系统 背景:接手的第一个项目,关于Excel比对与合并,主要参与系统的优化与维护工作 主要工作: 批量文件比对的多线程优化(比对方法涉及对象完全栈封闭) 批量文件合并OOM排查(合并需要先反序列化比对结果,若有多个版本的比对结果需要合并到另一分支上的同名文件,需要循环处理,每个比对结果合并之后需要置空,否则内存无法释放,排查工具:visualvm,发现调用合并方法时,Minor GC非常快,内存居高不下导致OOM;根本原因:自己开发的Excel解析工具+Java自带的序列化导致序列化产物非常大) 算法介绍可以参看《Excel比对与合并系统》 助理系统 需求:公司员工反馈行政问题都是在公司的聊天软件里反馈,反馈途径包括事业群、私聊行政助理、以及服务号反馈(类似微信公众号),行政助理需要在多个系统进行处理,助理系统为助理统一了消息来源,助理可以在助理系统中回复所有渠道的问题反馈。 调研:spring-boot-starter-websocket,实现了客户端与服务器全双工通信 难点:助理系统需要给每个反馈问题的员工生成唯一的对话,初次反馈消息时,快速发送消息会创建多个对话。这是因为后端多线程处理消息,每个线程都先去数据库查询此条消息的员工是否存在对话,如果不存在就创建。这里出现了并发经典错误 check-then-act。 解决方案:给会话表的员工id字段建唯一索引,插入新会话使用insert ignore。","categories":[{"name":"杂项","slug":"杂项","permalink":"https://zunpan.github.io/categories/%E6%9D%82%E9%A1%B9/"}],"tags":[{"name":"实习","slug":"实习","permalink":"https://zunpan.github.io/tags/%E5%AE%9E%E4%B9%A0/"}]},{"title":"Effective-Java学习笔记(九)","slug":"Effective-Java学习笔记(九)","date":"2022-08-13T08:11:11.000Z","updated":"2023-09-24T04:27:40.274Z","comments":true,"path":"2022/08/13/Effective-Java学习笔记(九)/","link":"","permalink":"https://zunpan.github.io/2022/08/13/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B9%9D%EF%BC%89/","excerpt":"","text":"第十章 异常 69. 仅在确有异常条件下使用异常 有人认为用 try-catch 和 while(true) 遍历数组比用 for-each 性能更好,因为 for-each 由编译器隐藏了边界检查,而 try-catch 代码中不包含检查 // Horrible abuse of exceptions. Don't ever do this! try { int i = 0; while(true){ range[i++].climb(); } catch (ArrayIndexOutOfBoundsException e) {} } 这个想法有三个误区: 因为异常是为特殊情况设计的,所以 JVM 实现几乎不会让它们像显式测试一样快。 将代码放在 try-catch 块中会抑制 JVM 可能执行的某些优化。 遍历数组的标准习惯用法不一定会导致冗余检查。许多 JVM 实现对它们进行了优化。 基于异常的循环除了不能提高性能外,还容易被异常隐藏循环中的 bug。因此,异常只适用于确有异常的情况;它们不应该用于一般的控制流程。 一个设计良好的 API 不能迫使其客户端为一般的控制流程使用异常。调用具有「状态依赖」方法的类,通常应该有一个单独的「状态测试」方法,表明是否适合调用「状态依赖」方法。例如,Iterator 接口具有「状态依赖」的 next 方法和对应的「状态测试」方法 hasNext。 for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) { Foo foo = i.next(); ... } 如果 Iterator 缺少 hasNext 方法,客户端将被迫这样做: // Do not use this hideous code for iteration over a collection! try { Iterator<Foo> i = collection.iterator(); while(true) { Foo foo = i.next(); ... } } catch (NoSuchElementException e) { } 这与一开始举例的对数组进行迭代的例子非常相似,除了冗长和误导之外,基于异常的循环执行效果可能很差,并且会掩盖系统中不相关部分的 bug。 提供单独的「状态测试」方法的另一种方式,就是让「状态依赖」方法返回一个空的 Optional 对象(Item-55),或者在它不能执行所需的计算时返回一个可识别的值,比如 null。 状态测试方法、Optional、可识别的返回值之间的选择如下: 如果要在没有外部同步的情况下并发地访问对象,或者受制于外部条件的状态转换,则必须使用 Optional 或可识别的返回值,因为对象的状态可能在调用「状态测试」方法与「状态依赖」方法的间隔中发生变化。 如果一个单独的「状态测试」方法重复「状态依赖」方法的工作,从性能问题考虑,可能要求使用 Optional 或可识别的返回值 在所有其他条件相同的情况下,「状态测试」方法略优于可识别的返回值。它提供了较好的可读性,而且不正确的使用可能更容易被检测:如果你忘记调用「状态测试」方法,「状态依赖」方法将抛出异常,使错误显而易见; 如果你忘记检查一个可识别的返回值,那么这个 bug 可能很难发现。但是这对于返回 Optional 对象的方式来说不是问题。 总之,异常是为确有异常的情况设计的。不要将它们用于一般的控制流程,也不要编写强制其他人这样做的 API。 70. 对可恢复情况使用 checked 异常,对编程错误使用运行时异常 Java 提供了三种可抛出项:checked 异常、运行时异常和错误。决定是使用 checked 异常还是 unchecked 异常的基本规则是:使用 checked 异常的情况是为了合理地期望调用者能够从中恢复。 有两种 unchecked 的可抛出项:运行时异常和错误。它们在行为上是一样的:都是可抛出的,通常不需要也不应该被捕获。如果程序抛出 unchecked 异常或错误,通常情况下是不可能恢复的,如果继续执行,弊大于利。如果程序没有捕获到这样的可抛出项,它将导致当前线程停止,并发出适当的错误消息。 运行时异常 运行时异常用来指示编程错误。大多数运行时异常都表示操作违反了先决条件。违反先决条件是指使用 API 的客户端未能遵守 API 规范所建立的约定。例如,数组访问约定指定数组索引必须大于等于 0 并且小于等于 length-1 (length:数组长度)。ArrayIndexOutOfBoundsException 表示违反了此先决条件 这个建议存在的问题是:并不总能清楚是在处理可恢复的条件还是编程错误。例如,考虑资源耗尽的情况,这可能是由编程错误(如分配一个不合理的大数组)或真正的资源短缺造成的。如果资源枯竭是由于暂时短缺或暂时需求增加造成的,这种情况很可能是可以恢复的。对于 API 设计人员来说,判断给定的资源耗尽实例是否允许恢复是一个问题。如果你认为某个条件可能允许恢复,请使用 checked 异常;如果没有,则使用运行时异常。如果不清楚是否可以恢复,最好使用 unchecked 异常 错误 虽然 Java 语言规范没有要求,但有一个约定俗成的约定,即错误保留给 JVM 使用,以指示:资源不足、不可恢复故障或其他导致无法继续执行的条件。考虑到这种约定被大众认可,所以最好不要实现任何新的 Error 子类。因此,你实现的所有 unchecked 异常都应该继承 RuntimeException(直接或间接)。不仅不应该定义 Error 子类,而且除了 AssertionError 之外,不应该抛出它们。 自定义异常 自定义异常继承 Throwable 类,Java 语言规范把它们当做普通 checked 异常(普通 checked 异常是 Exception 的子类,但不是 RuntimeException的子类)。不要使用自定义异常,它会让 API 的用户困惑 异常附加信息 API 设计人员常常忘记异常是成熟对象,可以为其定义任意方法。此类方法的主要用途是提供捕获异常的代码,并提供有关引发异常的附加信息。如果缺乏此类方法,程序员需要自行解析异常的字符串表示以获取更多信息。这是极坏的做法 因为 checked 异常通常表示可恢复的条件,所以这类异常来说,设计能够提供信息的方法来帮助调用者从异常条件中恢复尤为重要。例如,假设当使用礼品卡购物由于资金不足而失败时,抛出一个 checked 异常。该异常应提供一个访问器方法来查询差额。这将使调用者能够将金额传递给购物者。 总而言之,为可恢复条件抛出 checked 异常,为编程错误抛出 unchecked 异常。当有疑问时,抛出 unchecked 异常。不要定义任何既不是 checked 异常也不是运行时异常的自定义异常。应该为 checked 异常设计相关的方法,如提供异常信息,以帮助恢复。 71. 避免不必要地使用 checked 异常 合理抛出 checked 异常可以提高程序可靠性。过度使用会使得调用它的方法多次 try-catch 或抛出,给 API 用户带来负担,尤其是 Java8 中,抛出checked 异常的方法不能直接在流中使用。 只有在正确使用 API 也无法避免异常且使用 API 的程序员在遇到异常时可以采取一些有用的操作才能使用 checked 异常,否则抛出 unchecked 异常 如果 checked 异常是方法抛出的唯一 checked 异常,那么 checked 异常给程序员带来的额外负担就会大得多。如果还有其他 checked 异常,则该方法一定已经在 try 块中了,因此该异常最多需要另一个 catch 块而已。如果一个方法抛出单个 checked 异常,那么这个异常就是该方法必须出现在 try 块中而不能直接在流中使用的唯一原因。在这种情况下,有必要问问自己是否有办法避免 checked 异常。 消除 checked 异常的最简单方法是返回所需结果类型的 Optional 对象(Item-55)。该方法只返回一个空的 Optional 对象,而不是抛出一个 checked 异常。这种技术的缺点是,该方法不能返回任何详细说明其无法执行所需计算的附加信息。相反,异常具有描述性类型,并且可以导出方法来提供附加信息(Item-70) 总之,如果谨慎使用,checked 异常可以提高程序的可靠性;当过度使用时,它们会使 API 难以使用。如果调用者不应从失败中恢复,则抛出 unchecked 异常。如果恢复是可能的,并且你希望强制调用者处理异常情况,那么首先考虑返回一个 Optional 对象。只有当在失败的情况下,提供的信息不充分时,你才应该抛出一个 checked 异常。 72. 鼓励复用标准异常 Java 库提供了一组异常,涵盖了大多数 API 的大多数异常抛出需求。 复用标准异常有几个好处: 使你的 API 更容易学习和使用,因为它符合程序员已经熟悉的既定约定 使用你的 API 的程序更容易阅读,因为它们不会因为不熟悉的异常而混乱 更少的异常类意味着更小的内存占用和更少的加载类的时间 常见的被复用的异常: IllegalArgumentException。通常是调用者传入不合适的参数时抛出的异常 IllegalStateException。如果因为接收对象的状态导致调用非法,则通常会抛出此异常。例如,调用者试图在对象被正确初始化之前使用它 可以说,每个错误的方法调用都归结为参数非法或状态非法,但是有一些异常通常用于某些特定的参数非法和状态非法。如果调用者在禁止空值的参数中传递 null,那么按照惯例,抛出 NullPointerException 而不是 IllegalArgumentException。类似地,如果调用者将表示索引的参数中的超出范围的值传递给序列,则应该抛出 IndexOutOfBoundsException,而不是 IllegalArgumentException ConcurrentModificationException。如果一个对象被设计为由单个线程使用(或与外部同步),并且检测到它正在被并发地修改,则应该抛出该异常。因为不可能可靠地检测并发修改,所以该异常充其量只是一个提示。 UnsupportedOperationException。如果对象不支持尝试的操作,则抛出此异常。它很少使用,因为大多数对象都支持它们的所有方法。此异常用于一个类没有实现由其实现的接口定义的一个或多个可选操作。例如,对于只支持追加操作的 List 实现,试图从中删除元素时就会抛出这个异常 不要直接复用 Exception、RuntimeException、Throwable 或 Error 此表总结了最常见的可复用异常: Exception Occasion for Use IllegalArgumentException Non-null parameter value is inappropriate(非空参数值不合适) IllegalStateException Object state is inappropriate for method invocation(对象状态不适用于方法调用) NullPointerException Parameter value is null where prohibited(禁止参数为空时仍传入 null) IndexOutOfBoundsException Index parameter value is out of range(索引参数值超出范围) ConcurrentModificationException Concurrent modification of an object has been detected where it is prohibited(在禁止并发修改对象的地方检测到该动作) UnsupportedOperationException Object does not support method(对象不支持该方法调用) 其它异常如果有合适的复用场景也可以复用,例如,如果你正在实现诸如复数或有理数之类的算术对象,那么复用 ArithmeticException 和 NumberFormatException 是合适的 73. 抛出适合底层抽象异常的高层异常 当方法抛出一个与它所执行的任务没有明显关联的异常时,这是令人不安的。这种情况经常发生在由方法传播自低层抽象抛出的异常。它不仅令人不安,而且让实现细节污染了上层的 API。 为了避免这个问题,高层应该捕获低层异常,并确保抛出的异常可以用高层抽象解释。 这个习惯用法称为异常转换: // Exception Translation try { ... // Use lower-level abstraction to do our bidding } catch (LowerLevelException e) { throw new HigherLevelException(...); } 下面是来自 AbstractSequentialList 类的异常转换示例,该类是 List 接口的一个框架实现(Item-20)。在本例中,异常转换是由 List<E> 接口中的 get 方法规范强制执行的: /** * Returns the element at the specified position in this list. * @throws IndexOutOfBoundsException if the index is out of range * ({@code index < 0 || index >= size()}). */ public E get(int index) { ListIterator<E> i = listIterator(index); try { return i.next(); } catch (NoSuchElementException e) { throw new IndexOutOfBoundsException("Index: " + index); } } 如果低层异常可能有助于调试高层异常的问题,则需要一种称为链式异常的特殊异常转换形式。低层异常(作为原因)传递给高层异常,高层异常提供一个访问器方法(Throwable 的 getCause 方法)来检索低层异常: // Exception Chaining try { ... // Use lower-level abstraction to do our bidding } catch (LowerLevelException cause) { throw new HigherLevelException(cause); } 高层异常的构造函数将原因传递给能够接收链式异常的父类构造函数,因此它最终被传递给 Throwable 的一个接收链式异常的构造函数,比如 Throwable(Throwable): // Exception with chaining-aware constructor class HigherLevelException extends Exception { HigherLevelException(Throwable cause) { super(cause); } } 大多数标准异常都有接收链式异常的构造函数。对于不支持链式异常的异常,可以使用 Throwable 的 initCause 方法设置原因。异常链不仅允许你以编程方式访问原因(使用 getCause),而且还将原因的堆栈跟踪集成到更高层异常的堆栈跟踪中。 虽然异常转换优于底层异常的盲目传播,但它不应该被过度使用。在可能的情况下,处理底层异常的最佳方法是确保底层方法避免异常。有时,你可以在将高层方法的参数传递到底层之前检查它们的有效性。 如果不可能从底层防止异常,那么下一个最好的方法就是让高层静默处理这些异常,使较高层方法的调用者免受底层问题的影响。在这种情况下,可以使用一些适当的日志工具(如 java.util.logging)来记录异常。这允许程序员研究问题,同时将客户端代码和用户与之隔离。 总之,如果无法防止或处理来自底层的异常,则使用异常转换,但要保证底层方法的所有异常都适用于较高层。链式异常提供了兼顾两方面的最佳服务:允许抛出适当的高层异常,同时捕获并分析失败的潜在原因 74. 为每个方法记录会抛出的所有异常 始终单独声明 checked 异常,并使用 Javadoc 的 @throw 标记精确记录每次抛出异常的条件。如果一个方法抛出多个异常,不要使用快捷方式声明这些异常的父类。作为一个极端的例子,即不要在公共方法声明 throws Exception,除了只被 JVM 调用的 main方法 unchecked 异常不要声明 throws,但应该像 checked 异常一样用 Javadoc 记录他们。特别是接口中的方法要记录可能抛出的 unchecked 异常。 如果一个类中的许多方法都因为相同的原因抛出异常,你可以在类的文档注释中记录异常, 而不是为每个方法单独记录异常。一个常见的例子是 NullPointerException。类的文档注释可以这样描述:「如果在任何参数中传递了 null 对象引用,该类中的所有方法都会抛出 NullPointerException」 总之,记录你所编写的每个方法可能引发的每个异常。对于 unchecked 异常、checked 异常、抽象方法、实例方法都是如此。应该在文档注释中采用 @throw 标记的形式。在方法的 throws 子句中分别声明每个 checked 异常,但不要声明 unchecked 异常。如果你不记录方法可能抛出的异常,其他人将很难或不可能有效地使用你的类和接口。 75. 异常详细消息中应包含捕获失败的信息 程序因为未捕获异常而失败时,系统会自动调用异常的 toString 方法打印堆栈信息,堆栈信息包含异常的类名及详细信息。异常的详细消息应该包含导致异常的所有参数和字段的值。例如,IndexOutOfBoundsException 的详细消息应该包含下界、上界和未能位于下界之间的索引值。 异常详细信息不用过于冗长,程序失败时可以通过阅读文档和源代码收集信息,确保异常包含足够的信息的一种方法是在构造函数中配置异常信息,例如,IndexOutOfBoundsException 构造函数不包含 String 参数,而是像这样: /** * Constructs an IndexOutOfBoundsException. ** @param lowerBound the lowest legal index value * @param upperBound the highest legal index value plus one * @param index the actual index value */ public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) { // Generate a detail message that captures the failure super(String.format("Lower bound: %d, Upper bound: %d, Index: %d",lowerBound, upperBound, index)); // Save failure information for programmatic access this.lowerBound = lowerBound; this.upperBound = upperBound; this.index = index; } 76. 尽力保证故障原子性 失败的方法调用应该使对象处于调用之前的状态。 具有此属性的方法称为具备故障原子性。 有几种实现故障原子性的方式: 关于不可变对象 不可变对象在创建后永远处于一致状态 关于可变对象: 在修改状态前,先执行可能抛出异常的操作,例如检查状态,不合法就抛出异常 在临时副本上操作,成功后替换原来的对象,不成功不影响原来的对象。例如,一些排序函数会将入参 list 复制到数组中,对数组进行排序,再转换成 list 编写回滚代码,主要用于持久化的数据 有些情况是不能保证故障原子性的,例如,多线程不同步修改对象,对象可能处于不一致状态,当捕获到 ConcurrentModificationException 后对象不可恢复 总之,作为方法规范的一部分,生成的任何异常都应该使对象保持在方法调用之前的状态。如果违反了这条规则,API 文档应该清楚地指出对象将处于什么状态。 77. 不要忽略异常 异常要么 try-catch 要么抛出,不要写空的 catch 块,如果这样做,写上注释","categories":[{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/categories/Java/"}],"tags":[{"name":"Effective-Java","slug":"Effective-Java","permalink":"https://zunpan.github.io/tags/Effective-Java/"},{"name":"异常","slug":"异常","permalink":"https://zunpan.github.io/tags/%E5%BC%82%E5%B8%B8/"}]},{"title":"Effective-Java学习笔记(八)","slug":"Effective-Java学习笔记(八)","date":"2022-08-06T07:30:09.000Z","updated":"2023-09-24T04:27:40.275Z","comments":true,"path":"2022/08/06/Effective-Java学习笔记(八)/","link":"","permalink":"https://zunpan.github.io/2022/08/06/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AB%EF%BC%89/","excerpt":"","text":"第九章 通用程序设计 57. 将局部变量的作用域最小化 本条目在性质上类似于Item-15,即「最小化类和成员的可访问性」。通过最小化局部变量的范围,可以提高代码的可读性和可维护性,并降低出错的可能性。 将局部变量的作用域最小化,最具说服力的方式就是在第一次使用它的地方声明 每个局部变量声明都应该包含一个初始化表达式。 如果你还没有足够的信息来合理地初始化一个变量,你应该推迟声明,直到条件满足 58. for-each循环优于for循环 for-each 更简洁更不容易出错,且没有性能损失,只有三种情况不能用for-each 破坏性过滤。如果需要遍历一个集合并删除选定元素,则需要使用显式的迭代器,以便调用其 remove 方法。通过使用 Collection 在 Java 8 中添加的 removeIf 方法,通常可以避免显式遍历。 转换。如果需要遍历一个 List 或数组并替换其中部分或全部元素的值,那么需要 List 迭代器或数组索引来替换元素的值。 并行迭代。如果需要并行遍历多个集合,那么需要显式地控制迭代器或索引变量,以便所有迭代器或索引变量都可以同步执行 59. 了解并使用库 假设你想要生成 0 到某个上界之间的随机整数,有些程序员会写出如下代码: // Common but deeply flawed! static Random rnd = new Random(); static int random(int n) { return Math.abs(rnd.nextInt()) % n; } 这个方法有三个缺点: 如果 n 是比较小的 2 的幂,随机数序列会在相当短的时间内重复 如果 n 不是 2 的幂,一些数字出现的频率会更高,当 n 很大时效果明显 会返回超出指定范围的数字,当nextInt返回Integer.MIN_VALUE时,abs方法也会返回Integer.MIN_VALUE,假设 n 不是 2 幂,那么Integer.MIN_VALUE % n 将返回负数 我们不需要为这个需求自己编写方法,已经存在经过专家设计测试的标准库Random 的 nextInt(int) 从 Java 7 开始,就不应该再使用 Random。在大多数情况下,选择的随机数生成器现在是 ThreadLocalRandom。 它能产生更高质量的随机数,而且速度非常快。 总而言之,不要白费力气重新发明轮子。如果你需要做一些看起来相当常见的事情,那么库中可能已经有一个工具可以做你想做的事情。如果有,使用它;如果你不知道,查一下。一般来说,库代码可能比你自己编写的代码更好,并且随着时间的推移可能会得到改进。 60. 若需要精确答案就应避免使用 float 和 double 类型 float 和 double 类型主要用于科学计算和工程计算。它们执行二进制浮点运算,该算法经过精心设计,能够在很大范围内快速提供精确的近似值。但是,它们不能提供准确的结果。float 和 double 类型特别不适合进行货币计算,因为不可能将 0.1(或 10 的任意负次幂)精确地表示为 float 或 double 正确做法是使用 BigDecimal、int 或 long 进行货币计算。还要注意 BigDecimal 的构造函数要使用String参数而不是double,避免初始化时就用了不精确的值。int和long可以存储较小单位的值,将小数转换成整数存储 总之,对于任何需要精确答案的计算,不要使用 float 或 double 类型。如果希望系统来处理十进制小数点,并且不介意不使用基本类型带来的不便和成本,请使用 BigDecimal。使用 BigDecimal 的另一个好处是,它可以完全控制舍入,当执行需要舍入的操作时,可以从八种舍入模式中进行选择。如果你使用合法的舍入行为执行业务计算,这将非常方便。如果性能是最重要的,那么你不介意自己处理十进制小数点,而且数值不是太大,可以使用 int 或 long。如果数值不超过 9 位小数,可以使用 int;如果不超过 18 位,可以使用 long。如果数量可能超过 18 位,则使用 BigDecimal。 61. 基本数据类型优于包装类 Java 的类型系统有两部分,基本类型(如 int、double 和 boolean)和引用类型(如String和List)。每个基本类型都有一个对应的引用类型,称为包装类。如Integer、Double 和 Boolean 基本类型和包装类型之间的区别如下 基本类型只有它们的值,而包装类型具有与其值不同的标识。换句话说,两个包装类型实例可以具有相同的值和不同的标识。 基本类型只有全部功能值,而每个包装类型除了对应的基本类型的所有功能值外,还有一个非功能值,即 null 基本类型比包装类型更节省时间和空间 用 == 来比较包装类型几乎都是错的 在操作中混用基本类型和包装类型时,包装类型会自动拆箱,可能导致 NPE,如果操作结果保存到包装类型的变量中,还会发生自动装箱,导致性能问题 包装类型的用途如下: 作为集合的元素、键和值 泛型和泛型方法中的类型参数 反射调用 总之,只要有选择,就应该优先使用基本类型,而不是包装类型。基本类型更简单、更快。如果必须使用包装类型,请小心!自动装箱减少了使用包装类型的冗长,但没有减少危险。 当你的程序使用 == 操作符比较两个包装类型时,它会执行标识比较,这几乎肯定不是你想要的。当你的程序执行包含包装类型和基本类型的混合类型计算时,它将进行拆箱,当你的程序执行拆箱时,将抛出 NullPointerException。 最后,当你的程序将基本类型装箱时,可能会导致代价高昂且不必要的对象创建。 62. 其它类型更合适时应避免使用字符串 本条目讨论一些不应该使用字符串的场景 字符串是枚举类型的糟糕替代品 字符串是聚合类型的糟糕替代品。如果一个对象有多个字段,将其连接成一个字符串会出现许多问题,更好的方法是用私有静态成员类表示聚合 字符串是功能的糟糕替代品。例如全局缓存池要求 key 不能重复,如果用字符串做 key 可能在不同线程中出现问题 总之,当存在或可以编写更好的数据类型时,应避免将字符串用来表示对象。如果使用不当,字符串比其他类型更麻烦、灵活性更差、速度更慢、更容易出错。字符串经常被误用的类型包括基本类型、枚举和聚合类型。 63. 当心字符串连接引起的性能问题 字符串连接符(+)连接 n 个字符串的时间复杂度是 n2n^2n2。这是字符串不可变导致的 如果要连接的字符串数量较多,可以使用 StringBuilder 代替 String 64. 通过接口引用对象 如果存在合适的接口类型,那么应该使用接口类型声明参数、返回值、变量和字段。好处是代码可以非常方便切换性能更好或功能更丰富的实现,例如 HashMap 替换成 LinkedHashMap 可以保证迭代顺序和插入一致 如果没有合适的接口存在,那么用类引用对象是完全合适的。 值类,如 String 和 BigInteger。值类很少在编写时考虑到多个实现。它们通常是 final 的,很少有相应的接口。使用这样的值类作为参数、变量、字段或返回类型非常合适。 如果一个对象属于一个基于类的框架,那么就用基类引用它 接口的实现类有接口没有的方法,例如 PriorityQueue 类有 Queue 接口没有的比较器方法 65. 接口优于反射 反射有几个缺点: 失去了编译时类型检查的所有好处,包括异常检查。如果一个程序试图反射性地调用一个不存在的或不可访问的方法,它将在运行时失败,除非你采取了特殊的预防措施(大量 try-catch) 反射代码既笨拙又冗长。写起来很乏味,读起来也很困难 反射调用方法比普通调用方法更慢 反射优点: 对于许多程序,它们必须用到在编译时无法获取的类,在编译时存在一个适当的接口或父类来引用该类(Item-64)。如果是这种情况,可以用反射方式创建实例,并通过它们的接口或父类正常地访问它们。 例如,这是一个创建 Set<String> 实例的程序,类由第一个命令行参数指定。程序将剩余的命令行参数插入到集合中并打印出来。不管第一个参数是什么,程序都会打印剩余的参数,并去掉重复项。然而,打印这些参数的顺序取决于第一个参数中指定的类。如果你指定 java.util.HashSet,它们显然是随机排列的;如果你指定 java.util.TreeSet,它们是按字母顺序打印的,因为 TreeSet 中的元素是有序的 // Reflective instantiation with interface access public static void main(String[] args) { // Translate the class name into a Class object Class<? extends Set<String>> cl = null; try { cl = (Class<? extends Set<String>>) // Unchecked cast! Class.forName(args[0]); } catch (ClassNotFoundException e) { fatalError("Class not found."); } // Get the constructor Constructor<? extends Set<String>> cons = null; try { cons = cl.getDeclaredConstructor(); } catch (NoSuchMethodException e) { fatalError("No parameterless constructor"); } // Instantiate the set Set<String> s = null; try { s = cons.newInstance(); } catch (IllegalAccessException e) { fatalError("Constructor not accessible"); } catch (InstantiationException e) { fatalError("Class not instantiable."); } catch (InvocationTargetException e) { fatalError("Constructor threw " + e.getCause()); } catch (ClassCastException e) { fatalError("Class doesn't implement Set"); } // Exercise the set s.addAll(Arrays.asList(args).subList(1, args.length)); System.out.println(s); } private static void fatalError(String msg) { System.err.println(msg); System.exit(1); } 反射的合法用途(很少)是管理类对运行时可能不存在的其他类、方法或字段的依赖关系。如果你正在编写一个包,并且必须针对其他包的多个版本运行,此时反射将非常有用。该技术是根据支持包所需的最小环境(通常是最老的版本)编译包,并反射性地访问任何较新的类或方法。如果你试图访问的新类或方法在运行时不存在,要使此工作正常进行,则必须采取适当的操作。适当的操作可能包括使用一些替代方法来完成相同的目标,或者使用简化的功能进行操作 总之,反射是一种功能强大的工具,对于某些复杂的系统编程任务是必需的,但是它有很多缺点。如果编写的程序必须在编译时处理未知的类,则应该尽可能只使用反射实例化对象,并使用在编译时已知的接口或父类访问对象。 66. 明智地使用本地方法 JNI 允许 Java 调用本地方法,这些方法是用 C 或 C++ 等本地编程语言编写的。 历史上,本地方法主要有三个用途: 提供对特定于平台的设施(如注册中心)的访问 提供对现有本地代码库的访问,包括提供对遗留数据访问 通过本地语言编写应用程序中注重性能的部分,以提高性能 关于用途一:随着 Java 平台的成熟,它提供了对许多以前只能在宿主平台中上找到的特性。例如,Java 9 中添加的流 API 提供了对 OS 进程的访问。在 Java 中没有等效库时,使用本地方法来使用本地库也是合法的。 关于用途3:在早期版本(Java 3 之前),这通常是必要的,但是从那时起 JVM 变得更快了。对于大多数任务,现在可以在 Java 中获得类似的性能 本地方法的缺点: 会受到内存损坏错误的影响 垃圾收集器无法自动追踪本地方法的内存使用情况,导致性能下降 总之,在使用本地方法之前要三思。一般很少需要使用它们来提高性能。如果必须使用本地方法来访问底层资源或本地库,请尽可能少地使用本地代码,并对其进行彻底的测试。本地代码中的一个错误就可以破坏整个应用程序。 67. 明智地进行优化 关于优化的三条名言 以效率的名义(不一定能达到效率)犯下的计算错误比任何其他原因都要多——包括盲目的愚蠢 不要去计较效率上的一些小小的得失,在 97% 的情况下,不成熟的优化才是一切问题的根源。 在优化方面,我们应该遵守两条规则:规则 1:不要进行优化。规则 2 (仅针对专家):还是不要进行优化,也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化。 在设计系统时,我们要仔细考虑架构,好的架构允许它在后面优化,不良的架构导致很难优化。设计中最难更改的是组件之间以及组件与外部世界交互的组件,主要是 API、线路层协议和数据持久化格式,尽量避免做限制性能的设计 JMH 是一个微基准测试框架,主要是基于方法层面的基准测试,精度可以达到纳秒级 总而言之,不要努力写快的程序,要努力写好程序;速度自然会提高。但是在设计系统时一定要考虑性能,特别是在设计API、线路层协议和持久数据格式时。当你完成了系统的构建之后,请度量它的性能。如果足够快,就完成了。如果没有,利用分析器找到问题的根源,并对系统的相关部分进行优化。第一步是检查算法的选择:再多的底层优化也不能弥补算法选择的不足。根据需要重复这个过程,在每次更改之后测量性能,直到你满意为止。 68. 遵守被广泛认可的命名约定 Java 平台有一组完善的命名约定,其中许多约定包含在《The Java Language Specification》。不严格地讲,命名约定分为两类:排版和语法。 排版 和排版有关的命名约定,包括包、类、接口、方法、字段和类型变量 包名和模块名应该是分层的,组件之间用句点分隔。组件应该由小写字母组成,很少使用数字。任何在你的组织外部使用的包,名称都应该以你的组织的 Internet 域名开头,并将组件颠倒过来,例如,edu.cmu、com.google、org.eff。以 java 和 javax 开头的标准库和可选包是这个规则的例外。用户不能创建名称以 java 或 javax 开头的包或模块。 包名的其余部分应该由描述包的一个或多个组件组成。组件应该很短,通常为 8 个或更少的字符。鼓励使用有意义的缩写,例如 util 而不是 utilities。缩写词是可以接受的,例如 awt。组件通常应该由一个单词或缩写组成。 类和接口名称,包括枚举和注释类型名称,应该由一个或多个单词组成,每个单词的首字母大写,例如 List 或 FutureTask 方法和字段名遵循与类和接口名相同的排版约定,除了方法或字段名的第一个字母应该是小写,例如 remove 或 ensureCapacity 前面规则的唯一例外是「常量字段」,它的名称应该由一个或多个大写单词组成,由下划线分隔,例如 VALUES 或 NEGATIVE_INFINITY 局部变量名与成员名具有类似的排版命名约定,但允许使用缩写,也允许使用单个字符和短字符序列,它们的含义取决于它们出现的上下文,例如 i、denom、houseNum。输入参数是一种特殊的局部变量。它们的命名应该比普通的局部变量谨慎得多,因为它们的名称是方法文档的组成部分。 类型参数名通常由单个字母组成。最常见的是以下五种类型之一:T 表示任意类型,E 表示集合的元素类型,K 和 V 表示 Map 的键和值类型,X 表示异常。函数的返回类型通常为 R。任意类型的序列可以是 T、U、V 或 T1、T2、T3。 为了快速参考,下表显示了排版约定的示例。 Identifier Type Example Package or module org.junit.jupiter.api, com.google.common.collect Class or Interface Stream, FutureTask, LinkedHashMap,HttpClient Method or Field remove, groupingBy, getCrc Constant Field MIN_VALUE, NEGATIVE_INFINITY Local Variable i, denom, houseNum Type Parameter T, E, K, V, X, R, U, V, T1, T2 语法 语法命名约定比排版约定更灵活,也更有争议 可实例化的类,包括枚举类型,通常使用一个或多个名词来命名,例如 Thread、PriorityQueue 或 ChessPiece 不可实例化的工具类通常用复数名词来命名,例如 Collectors 和 Collections 接口的名称类似于类,例如 Collection 或 Comparator,或者以 able 或 ible 结尾的形容词,例如 Runnable、Iterable 或 Accessible 因为注解类型有很多的用途,所以没有哪部分占主导地位。名词、动词、介词和形容词都很常见,例如,BindingAnnotation、Inject、ImplementedBy 或 Singleton。 执行某些操作的方法通常用动词或动词短语(包括对象)命名,例如,append 或 drawImage。 返回布尔值的方法的名称通常以单词 is 或 has(通常很少用)开头,后面跟一个名词、一个名词短语,或者任何用作形容词的单词或短语,例如 isDigit、isProbablePrime、isEmpty、isEnabled 或 hasSiblings。 返回被调用对象的非布尔函数或属性的方法通常使用以 get 开头的名词、名词短语或动词短语来命名,例如 size、hashCode 或 getTime。有一种说法是,只有第三种形式(以 get 开头)才是可接受的,但这种说法几乎没有根据。前两种形式的代码通常可读性更强 。以 get 开头的形式起源于基本过时的 Java bean 规范,该规范构成了早期可复用组件体系结构的基础。有一些现代工具仍然依赖于 bean 命名约定,你应该可以在任何与这些工具一起使用的代码中随意使用它。如果类同时包含相同属性的 setter 和 getter,则遵循这种命名约定也有很好的先例。在本例中,这两个方法通常被命名为 getAttribute 和 setAttribute。 转换对象类型(返回不同类型的独立对象)的实例方法通常称为 toType,例如 toString 或 toArray。 返回与接收对象类型不同的视图(Item-6)的方法通常称为 asType,例如 asList 返回与调用它们的对象具有相同值的基本类型的方法通常称为类型值,例如 intValue 静态工厂的常见名称包括 from、of、valueOf、instance、getInstance、newInstance、getType 和 newType 字段名的语法约定没有类、接口和方法名的语法约定建立得好,也不那么重要,因为设计良好的 API 包含很少的公开字段。类型为 boolean 的字段的名称通常类似于 boolean 访问器方法,省略了开头「is」,例如 initialized、composite。其他类型的字段通常用名词或名词短语来命名,如 height、digits 和 bodyStyle。局部变量的语法约定类似于字段的语法约定,但要求更少。","categories":[{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/categories/Java/"}],"tags":[{"name":"Effective-Java","slug":"Effective-Java","permalink":"https://zunpan.github.io/tags/Effective-Java/"},{"name":"通用程序设计","slug":"通用程序设计","permalink":"https://zunpan.github.io/tags/%E9%80%9A%E7%94%A8%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/"}]},{"title":"Effective-Java学习笔记(七)","slug":"Effective-Java学习笔记(七)","date":"2022-08-01T11:27:09.000Z","updated":"2023-09-24T04:27:40.273Z","comments":true,"path":"2022/08/01/Effective-Java学习笔记(七)/","link":"","permalink":"https://zunpan.github.io/2022/08/01/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%83%EF%BC%89/","excerpt":"","text":"第八章 方法 49. 检查参数的有效性 大多数方法和构造函数都对入参有一些限制,例如非空非负,你应该在方法主体的开头检查参数并在Javadoc中用@throws记录参数限制 Java7 添加了 Objects.requireNonNull 方法执行空检查 Java9 在 Objects 中添加了范围检查:checkFromIndexSize、checkFromToIndex 和 checkIndex 对于未导出的方法,作为包的作者,你应该定制方法调用的环境,确保只传递有效的参数值。因此,非 public 方法可以使用断言检查参数 // Private helper function for a recursive sort private static void sort(long a[], int offset, int length) { assert a != null; assert offset >= 0 && offset <= a.length; assert length >= 0 && length <= a.length - offset; ... // Do the computation } 断言只适合用来调试,可以关闭可以打开,public 方法一定要显示检查参数并抛出异常,断言失败只会抛出AssertionError,不利于定位错误 总而言之,每次编写方法或构造函数时,都应该考虑参数存在哪些限制。你应该在文档中记录这些限制,并在方法主体的开头显式地检查。 50. 在需要时制作防御性副本 Java 是一种安全的语言,这是它的一大优点。这意味着在没有 native 方法的情况下,它不受缓冲区溢出、数组溢出、非法指针和其他内存损坏错误的影响,这些错误困扰着 C 和 C++ 等不安全语言。在一种安全的语言中,可以编写一个类并确定它们的不变量将保持不变,而不管在系统的任何其他部分发生了什么。在将所有内存视为一个巨大数组的语言中,这是不可能的。 即使使用一种安全的语言,如果你不付出一些努力,也无法与其他类隔离。你必须进行防御性的设计,并假定你的类的客户端会尽最大努力破坏它的不变量。 通过可变参数破坏类的不变量 虽然如果没有对象的帮助,另一个类是不可能修改对象的内部状态的,但是要提供这样的帮助却出奇地容易。例如,考虑下面的类,它表示一个不可变的时间段: // Broken "immutable" time period class public final class Period { private final Date start; private final Date end; /** * @param start the beginning of the period * @param end the end of the period; must not precede start * @throws IllegalArgumentException if start is after end * @throws NullPointerException if start or end is null */ public Period(Date start, Date end) { if (start.compareTo(end) > 0) throw new IllegalArgumentException(start + " after " + end); this.start = start; this.end = end; } public Date start() { return start; } public Date end() { return end; } ... // Remainder omitted } 乍一看,这个类似乎是不可变的,并且要求一个时间段的开始时间不能在结束时间之后。然而,利用 Date 是可变的这一事实很容易绕过这个约束: // Attack the internals of a Period instance Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear(78); // Modifies internals of p! 从 Java8 开始,解决这个问题可以用 Instant 或 LocalDateTime 或 ZonedDateTime 来代替 Date,因为它们是不可变类。 防御性副本 但是有时必须在 API 和内部表示中使用可变值类型,这时候可以对可变参数进行防御性副本而不是使用原始可变参数。对上面的 Period 类改进如下 // Repaired constructor - makes defensive copies of parameters public Period(Date start, Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (this.start.compareTo(this.end) > 0) throw new IllegalArgumentException(this.start + " after " + this.end); } 先进行防御性复制,再在副本上检查参数,保证在检查参数和复制参数之间的空窗期,类不受其他线程更改参数的影响,这个攻击也叫time-of-check/time-of-use 或 TOCTOU 攻击 对可被不受信任方子类化的参数类型,不要使用 clone 方法进行防御性复制。 访问器也要对可变字段进行防御性复制 如果类信任它的调用者不会破坏不变量,比如类和调用者都是同一个包下,那么应该避免防御性复制 当类的作用就是修改可变参数时不用防御性复制,客户端承诺不直接修改对象 破坏不变量只会对损害客户端时不用防御性复制,例如包装类模式,客户端在包装对象之后可以直接访问对象,破坏类的不变量,但这通常只会损害客户端 总而言之,如果一个类具有从客户端获取或返回给客户端的可变组件,则该类必须防御性地复制这些组件。如果复制的成本过高,并且类信任它的客户端不会不适当地修改组件,那么可以不进行防御性的复制,取而代之的是在文档中指明客户端的职责是不得修改受到影响的组件。 51. 仔细设计方法签名 仔细选择方法名称。目标是选择可理解的、与同一包中其它名称风格一致的名称;选择广泛认可的名字;避免长方法名 不要提供过于便利的方法。每种方法都应该各司其职。太多的方法使得类难以学习、使用、记录、测试和维护。对于接口来说更是如此,在接口中,太多的方法使实现者和用户的工作变得复杂。对于类或接口支持的每个操作,请提供一个功能齐全的方法。 避免长参数列表。可以通过分解方法减少参数数量;也可以通过静态成员类 helper 类来存参数;也可以从对象构建到方法调用都采用建造者模式 参数类型优先选择接口而不是类 双元素枚举类型优于 boolean 参数。枚举比 boolean 可读性强且可以添加更多选项 52. 明智地使用重载 考虑下面使用了重载的代码: // Broken! - What does this program print? public class CollectionClassifier { public static String classify(Set<?> s) { return "Set"; } public static String classify(List<?> lst) { return "List"; } public static String classify(Collection<?> c) { return "Unknown Collection"; } public static void main(String[] args) { Collection<?>[] collections = { new HashSet<String>(),new ArrayList<BigInteger>(),new HashMap<String, String>().values() }; for (Collection<?> c : collections) System.out.println(classify(c)); } } 这段代码打印了三次 Unknown Collection。因为 classify 方法被重载,并且在编译时就决定了要调用哪个重载,编译时是 Collections<?> 类型,所以调用的就是第三个重载方法 重载VS覆盖 重载方法的选择是静态的,在编译时决定要调用哪个重载方法。而覆盖方法的选择是动态的, 在运行时根据调用方法的对象的运行时类型选择覆盖方法的正确版本 因为覆盖是常态,而重载是例外,所以覆盖满足了人们对方法调用行为的期望,重载很容易混淆这些期望。 重载 安全、保守的策略是永远不导出具有相同数量参数的两个重载。你可以为方法提供不同的名称而不是重载它们。 构造函数只能重载,我们可以用静态工厂代替构造函数 不要在重载方法的相同参数位置上使用不同的函数式接口。不同的函数式接口并没有本质的不同 总而言之,方法可以重载,但并不意味着就应该这样做。通常,最好避免重载具有相同数量参数的多个签名的方法。在某些情况下,特别是涉及构造函数的情况下,可能难以遵循这个建议。在这些情况下,你至少应该避免同一组参数只需经过类型转换就可以被传递给不同的重载方法。如果这是无法避免的,例如,因为要对现有类进行改造以实现新接口,那么应该确保在传递相同的参数时,所有重载的行为都是相同的。如果你做不到这一点,程序员将很难有效地使用重载方法或构造函数,他们将无法理解为什么它不能工作。 53. 明智地使用可变参数 可变参数首先创建一个数组,其大小是在调用点上传递的参数数量,然后将参数值放入数组,最后将数组传递给方法。 当你需要定义具有不确定数量参数的方法时,可变参数是非常有用的。在可变参数之前加上任何必需的参数,并注意使用可变参数可能会引发的性能后果。 54. 返回空集合或数组,而不是 null 在方法中用空集合或空数组代替 null 返回可以让客户端不用显示判空 55. 明智地返回 Optional 在 Java8 之前,方法可能无法 return 时有两种处理方法,一种是抛异常,一种是 返回 null。抛异常代价高,返回 null 需要客户端显示判空 Java8 添加了第三种方法来处理可能无法返回值的方法。Optional<T> 类表示一个不可变的容器,它可以包含一个非空的 T 引用,也可以什么都不包含。不包含任何内容的 Optional 被称为空。一个值被认为存在于一个非空的 Optional 中。Optional 的本质上是一个不可变的集合,它最多可以容纳一个元素。 理论上应返回 T,但在某些情况下可能无法返回 T 的方法可以将返回值声明为 Optional<T>。这允许该方法返回一个空结果来表明它不能返回有效的结果。具备 Optional 返回值的方法比抛出异常的方法更灵活、更容易使用,并且比返回 null 的方法更不容易出错。 Item-30 有一个求集合最大值方法 // Returns maximum value in collection - throws exception if empty public static <E extends Comparable<E>> E max(Collection<E> c) { if (c.isEmpty()) throw new IllegalArgumentException("Empty collection"); E result = null; for (E e : c) if (result == null || e.compareTo(result) > 0) result = Objects.requireNonNull(e); return result; } 当入参集合为空时,这个方法会抛出 IllegalArgumentException。更好的方式是返回 Optional // Returns maximum value in collection as an Optional<E> public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) { if (c.isEmpty()) return Optional.empty(); E result = null; for (E e : c) if (result == null || e.compareTo(result) > 0) result = Objects.requireNonNull(e); return Optional.of(result); } Optional.empty() 返回一个空的 Optional,Optional.of(value) 返回一个包含给定非空值的 Optional。将 null 传递给 Optional.of(value) 是一个编程错误。如果你这样做,该方法将通过抛出 NullPointerException 来响应。Optional.ofNullable(value) 方法接受一个可能为空的值,如果传入 null,则返回一个空的 Optional。永远不要让返回 optional 的方法返回 null : 它违背了这个功能的设计初衷。 为什么选择返回 Optional 而不是返回 null 或抛出异常?Optional 在本质上类似于受检查异常(Item-71),因为它们迫使 API 的用户面对可能没有返回值的事实。抛出不受检查的异常或返回 null 允许用户忽略这种可能性,抛出受检查异常会让客户端添加额外代码 如果一个方法返回一个 Optional,客户端可以选择如果该方法不能返回值该采取什么操作。你可以使用 orElse 方法指定一个默认值,或者使用 orElseGet 方法在必要时生成默认值;也可以使用 orElseThrow 方法抛出异常 关于 Optional 用法的一些Tips: isPresent 方法可以判断 Optional中有没有值,谨慎使用 isPrensent 方法,它的许多用途可以用上面的方法代替 并不是所有的返回类型都能从 Optional 处理中获益。容器类型,包括集合、Map、流、数组和 Optional,不应该封装在 Optional 中。 你应该简单的返回一个空的 List<T>,而不是一个空的 Optional<List<T>> 永远不应该返回包装类的 Optional,除了「次基本数据类型」,如 Boolean、Byte、Character、Short 和 Float 之外 在集合或数组中使用 Optional 作为键、值或元素几乎都是不合适的。 总之,如果你发现自己编写的方法不能总是返回确定值,并且你认为该方法的用户在每次调用时应该考虑这种可能性,那么你可能应该让方法返回一个 Optional。但是,你应该意识到,返回 Optional 会带来实际的性能后果;对于性能关键的方法,最好返回 null 或抛出异常。最后,除了作为返回值之外,你几乎不应该以任何其他方式使用 Optional。 56. 为所有公开的 API 元素编写文档注释","categories":[{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/categories/Java/"}],"tags":[{"name":"Effective-Java","slug":"Effective-Java","permalink":"https://zunpan.github.io/tags/Effective-Java/"},{"name":"方法","slug":"方法","permalink":"https://zunpan.github.io/tags/%E6%96%B9%E6%B3%95/"}]},{"title":"Effective-Java学习笔记(六)","slug":"Effective-Java学习笔记(六)","date":"2022-07-30T06:30:09.000Z","updated":"2023-09-24T04:27:40.276Z","comments":true,"path":"2022/07/30/Effective-Java学习笔记(六)/","link":"","permalink":"https://zunpan.github.io/2022/07/30/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AD%EF%BC%89/","excerpt":"","text":"第七章 λ 表达式和流 42. λ 表达式优于匿名类 函数对象 在历史上,带有单个抽象方法的接口(或者抽象类,但这种情况很少)被用作函数类型。它们的实例(称为函数对象)表示函数或操作。自从 JDK 1.1 在 1997 年发布以来,创建函数对象的主要方法就是匿名类(Item-24)。下面是一个按长度对字符串列表进行排序的代码片段,使用一个匿名类来创建排序的比较函数(它强制执行排序顺序): // Anonymous class instance as a function object - obsolete! Collections.sort(words, new Comparator<String>() { public int compare(String s1, String s2) { return Integer.compare(s1.length(), s2.length()); } }); 函数式接口 在 Java 8 中官方化了一个概念,即具有单个抽象方法的接口是特殊的,应该得到特殊处理。这些接口现在被称为函数式接口 lambda 表达式 可以使用 lambda 表达式为函数式接口创建实例,lambda 表达式在功能上类似于匿名类,但更简洁 // Lambda expression as function object (replaces anonymous class) Collections.sort(words,(s1, s2) -> Integer.compare(s1.length(), s2.length())); lambda 表达式的入参和返回值类型一般不写,由编译器做类型推断,类型能不写就不写。 Item26告诉你不要用原始类型,Item29,30告诉你要用泛型和泛型方法,编译器从泛型中获得了大部分类型推断所需的类型信息 lambda 表达式缺少名称和文档;如果一个算法较复杂,或者有很多行代码,不要把它放在 lambda 表达式中。 一行是理想的,三行是合理的最大值 lambda 表达式仅限于函数式接口。如果想创建抽象类实例或者多个抽象方法的接口,只能用匿名类 lambda 表达式编译后会成为外部类的一个私有方法,而匿名类会生成单独的class文件,所以 lambda 表达式的this是外部类实例,匿名类的this是匿名类实例 lambda表达式和匿名类都不能可靠的序列化,如果想序列化函数对象,可以用私有静态嵌套类 43. 方法引用优于 λ 表达式 Java 提供了一种比 lambda 表达式更简洁的方法来生成函数对象:方法引用。下面这段代码的功能是,如果数字 1 不在映射中,则将其与键关联,如果键已经存在,则将关联值递增: map.merge(key, 1, (count, incr) -> count + incr); 上面代码的 lambda 表达式作用是返回两个入参的和,在 Java 8 中,Integer(和所有其它基本类型的包装类)提供了一个静态方法 sum,它的作用完全相同,我们可以传入一个方法引用,并得到相同结果,同时减少视觉混乱: map.merge(key, 1, Integer::sum); 函数对象的参数越多,方法引用就显得越简洁,但是 lambda 表达式指明了参数名,使得 lambda表达式比方法引用更容易阅读和维护,没有什么是方法引用能做而 lambda 表达式做不了的 许多方法引用引用静态方法,但是有四种方法不引用静态方法。其中两个是绑定和非绑定实例方法引用。在绑定引用中,接收对象在方法引用中指定。绑定引用在本质上与静态引用相似:函数对象接受与引用方法相同的参数。在未绑定引用中,在应用函数对象时通过方法声明参数之前的附加参数指定接收对象。在流管道中,未绑定引用通常用作映射和筛选函数(Item-45)。最后,对于类和数组,有两种构造函数引用。构造函数引用用作工厂对象。五种方法参考文献汇总如下表: Method Ref Type Example Lambda Equivalent Static Integer::parseInt str -> Bound Instant.now()::isAfter Instant then =Instant.now(); t ->then.isAfter(t) Unbound String::toLowerCase str ->str.toLowerCase() Class Constructor TreeMap<K,V>::new () -> new TreeMap<K,V> Array Constructor int[]::new len -> new int[len] 总之,方法引用通常为 lambda 表达式提供了一种更简洁的选择。如果方法引用更短、更清晰,则使用它们;如果没有,仍然使用 lambda 表达式。 44. 优先使用标准函数式接口 java.util.function 包提供了大量的标准函数接口。优先使用标准函数式接口而不是自己写。 通过减少 API ,使得你的 API 更容易学习,并将提供显著的互操作性优势,因为许多标准函数式接口提供了有用的默认方法 java.util.function 中有 43 个接口。不能期望你记住所有的接口,但是如果你记住了 6 个基本接口,那么你可以在需要时派生出其余的接口。基本接口操作对象引用类型。Operator 接口表示结果和参数类型相同的函数。Predicate 接口表示接受参数并返回布尔值的函数。Function 接口表示参数和返回类型不同的函数。Supplier 接口表示一个无参并返回(或「供应」)值的函数。最后,Consumer 表示一个函数,该函数接受一个参数,但不返回任何内容,本质上是使用它的参数。六个基本的函数式接口总结如下: Interface Function Signature Example UnaryOperator<T> T apply(T t) String::toLowerCase BinaryOperator<T> T apply(T t1, T t2) BigInteger::add Predicate<T> boolean test(T t) Collection::isEmpty Function<T,R> R apply(T t) Arrays::asList Supplier<T> T get() Instant::now Consumer<T> void accept(T t) System.out::println 还有 6 个基本接口的 3 个变体,用于操作基本类型 int、long 和 double。它们的名称是通过在基本接口前面加上基本类型前缀而派生出来的。例如,一个接受 int 的 Predicate 就是一个 IntPredicate,一个接受两个 long 值并返回一个 long 的二元操作符就是一个 LongBinaryOperator。没有变体类型是参数化的除了由返回类型参数化的 Function 变体外。例如,LongFunction<int[]> 使用 long 并返回一个 int[]。 Function 接口还有 9 个额外的变体,在结果类型为基本数据类型时使用。源类型和结果类型总是不同的,因为入参只有一个且与出参类型相同的函数本身都是 UnaryOperator。如果源类型和结果类型都是基本数据类型,则使用带有 SrcToResult 的前缀函数,例如 LongToIntFunction(六个变体)。如果源是一个基本数据类型,而结果是一个对象引用,则使用带前缀 SrcToObj 的 Function 接口,例如 DoubleToObjFunction(三个变体)。 三个基本函数式接口有两个参数版本,使用它们是有意义的:BiPredicate<T,U>、BiFunction<T,U,R>、BiConsumer<T,U>。也有 BiFunction 变体返回三个相关的基本类型:ToIntBiFunction<T,U>、 ToLongBiFunction<T,U>、ToDoubleBiFunction<T,U>。Consumer 有两个参数变体,它们接受一个对象引用和一个基本类型:ObjDoubleConsumer<T>、ObjIntConsumer<T>、ObjLongConsumer<T>。总共有9个基本接口的双参数版本。 最后是 BooleanSupplier 接口,它是 Supplier 的一个变体,返回布尔值。这是在任何标准函数接口名称中唯一显式提到布尔类型的地方,但是通过 Predicate 及其四种变体形式支持布尔返回值。前面描述的 BooleanSupplier 接口和 42 个接口占了全部 43 个标准函数式接口。 总之,既然 Java 已经有了 lambda 表达式,你必须在设计 API 时考虑 lambda 表达式。在输入时接受函数式接口类型,在输出时返回它们。一般情况下,最好使用 java.util.function 中提供的标准函数式接口,但请注意比较少见的一些情况,在这种情况下,你最好编写自己的函数式接口。 45. 明智地使用流 Java8添加了流API,用来简化序列或并行执行批量操作,API有两个关键的抽象:流(表示有限或无限的数据元素序列)和流管道(表示对这些元素的多阶段计算)。 流 流中的元素可以来自任何地方。常见的源包括集合、数组、文件、正则表达式的 Pattern 匹配器、伪随机数生成器和其他流。流中的数据元素可以是对象的引用或基本数据类型。支持三种基本数据类型:int、long 和 double。 流管道 流管道由源流跟着零个或多个中间操作和一个终止操作组成。每个中间操作以某种方式转换流,例如将每个元素映射到该元素的一个函数,或者过滤掉不满足某些条件的所有元素。中间操作都将一个流转换为另一个流,其元素类型可能与输入流相同,也可能与输入流不同。终止操作对最后一次中间操作所产生的流进行最终计算,例如将其元素存储到集合中、返回特定元素、或打印其所有元素。 流管道的计算是惰性的:直到调用终止操作时才开始计算,并且对完成终止操作不需要的数据元素永远不会计算。这种惰性的求值机制使得处理无限流成为可能。 流 API 是流畅的:它被设计成允许使用链式调用将组成管道的所有调用写到单个表达式中。 谨慎使用流,全部都是流操作的代码可读性不高 谨慎命名 lambda 表达式的参数名以提高可读性,复杂表达式使用 helper 方法 流 VS 迭代 迭代代码使用代码块表示重复计算,流管道使用函数对象(通常是 lambda 表达式或方法引用)表示重复计算。 迭代优势: 代码块可以读取修改局部变量,lambda表达式只能读取final变量或实际上final变量(初始化后不再修改,编译器会帮我们声明为final)。 代码块可以控制迭代,包括return,break,continue,throw,但是lambda不行 流适合用在以下操作: 元素序列的一致变换 过滤元素序列 组合元素序列(例如添加它们,连接它们或计算它们的最小值) 聚合元素序列到一个集合中,例如按属性分组 在元素序列中搜索满足某些条件的元素 总之,有些任务最好使用流来完成,有些任务最好使用迭代来完成。许多任务最好通过结合这两种方法来完成。 46. 在流中使用无副作用的函数 考虑以下代码,它用于构建文本文件中单词的频率表 // Uses the streams API but not the paradigm--Don't do this! Map<String, Long> freq = new HashMap<>(); try (Stream<String> words = new Scanner(file).tokens()) { words.forEach(word -> { freq.merge(word.toLowerCase(), 1L, Long::sum); }); } 这段代码可以得出正确答案,但它不是流代码,而是伪装成流代码的迭代代码。它比迭代代码更长,更难阅读,更难维护,问题出在:forEach修改了外部状态。正确使用的流代码如下: // Proper use of streams to initialize a frequency table Map<String, Long> freq; try (Stream<String> words = new Scanner(file).tokens()) { freq = words.collect(groupingBy(String::toLowerCase, counting())); } forEach 操作应该只用于报告流计算的结果,而不是执行计算 正确的流代码使用了 Collectors 的生成收集器的方法,将元素收集到集合中的方法有三种:toList()、toSet() 和 toCollection(collectionFactory)。它们分别返回 List、Set 和程序员指定的集合类型 Collectors其它方法大部分是将流收集到Map中。最简单的Map收集器是toMap(keyMapper, valueMapper),它接受两个函数,一个将流元素映射到键,另一个将流元素映射到值,Item34用到了这个收集器 // Using a toMap collector to make a map from string to enum private static final Map<String, Operation> stringToEnum =Stream.of(values()).collect(toMap(Object::toString, e -> e)); 这个收集器不能处理重复键的问题,我们可以加入第三个参数,即merge函数,处理重复键,它的参数类型是Map的值类型。我们还可以加入第四个参数用来指定特定的Map实现(如EnumMap或TreeMap) 除了toMap方法,groupingBy方法也将元素收集到Map中,键是类别,值是这个类别的所有元素的列表;groupingBy方法第二个参数是一个下游收集器,例如 counting() 作为下游收集器,最终的Map的键是类别,值是这个类别所有元素的数量;groupingBy也支持指定特定的Map实现 minBy和maxBy,它们接受一个Comparator并返回最小或最大元素;join方法将字符序列(如字符串)连接起来 总之,流管道编程的本质是无副作用的函数对象。这适用于传递给流和相关对象的所有函数对象。中间操作 forEach 只应用于报告由流执行的计算结果,而不应用于执行计算。为了正确使用流,你必须了解 collector。最重要的 collector 工厂是 toList、toSet、toMap、groupingBy 和 join。 47. 优先使用 Collection 而不是 Stream 作为返回类型 Stream 没有继承 Iterable,不能用 for-each 循环遍历。Collection 接口继承了 Iterable 而且提供了转换为流的方法,因此,Collection 或其适当的子类通常是公有返回序列的方法的最佳返回类型。 48. 谨慎使用并行流 如果流来自 Stream.iterate 或者中间操作 limit,并行化管道也不太可能提高其性能 通常,并行性带来的性能提升在 ArrayList、HashMap、HashSet 和 ConcurrentHashMap 实例上的流效果最好;int 数组和 long 数组也在其中。 这些数据结构的共同之处在于,它们都可以被精确且廉价地分割成任意大小的子程序,这使得在并行线程之间划分工作变得很容易。另一个共同点是,当按顺序处理时,它们提供了极好的引用位置:顺序元素引用一起存储在内存中,这些引用的引用对象在内存中可能彼此不太接近,这减少了引用的位置。引用位置对于并行化批量操作非常重要,如果没有它,线程将花费大量时间空闲,等待数据从内存传输到处理器的缓存中。具有最佳引用位置的数据结构是基本数组,因为数据本身是连续存储在内存中的。 如果在终止操作中完成大量工作,并且该操作本质上是顺序的,那么管道的并行化效果有限。并行化的最佳终止操作是 reduce 方法或者预先写好的reduce 方法,包括min、max、count、and。anyMatch、allMatch 和 noneMatch 的短路操作也适用于并行性。流的 collect 方法执行的操作称为可变缩减,它们不是并行化的好候选,因为组合集合的开销是昂贵的。 并行化流不仅会导致糟糕的性能,包括活动失败;它会导致不正确的结果和不可预知的行为(安全故障)。 总之,不要尝试并行化流管道,除非你有充分的理由相信它将保持计算的正确性以及提高速度。不适当地并行化流的代价可能是程序失败或性能灾难。","categories":[{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/categories/Java/"}],"tags":[{"name":"Effective-Java","slug":"Effective-Java","permalink":"https://zunpan.github.io/tags/Effective-Java/"},{"name":"lambda表达式","slug":"lambda表达式","permalink":"https://zunpan.github.io/tags/lambda%E8%A1%A8%E8%BE%BE%E5%BC%8F/"},{"name":"Stream","slug":"Stream","permalink":"https://zunpan.github.io/tags/Stream/"}]},{"title":"Effective-Java学习笔记(五)","slug":"Effective-Java学习笔记(五)","date":"2022-07-22T16:44:09.000Z","updated":"2023-09-24T04:27:40.275Z","comments":true,"path":"2022/07/23/Effective-Java学习笔记(五)/","link":"","permalink":"https://zunpan.github.io/2022/07/23/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%94%EF%BC%89/","excerpt":"","text":"第六章 枚举和注解 34. 用枚举类型代替 int 常量 int枚举 int枚举类型中有一系列public static final int常量,每个常量表示一个类型。 它有许多缺点: 不提供类型安全性,容易误用(入参是Apple常量,但是传任何int都可以) 常量值修改,客户端也必须修改 调试麻烦 枚举类型 Java 的枚举类型是成熟的类,其他语言中的枚举类型本质上是 int 值。 枚举优势: 实例受控 Java 枚举类型通过 public static final 修饰的字段为每个枚举常量导出一个实例。枚举类型实际上是 final 类型,因为构造函数是私有的。客户端既不能创建枚举类型的实例,也不能继承它,所以除了声明的枚举常量之外,不能有任何实例。换句话说,枚举类型是实例受控的类(Item-1)。它们是单例(Item-3)的推广应用,单例本质上是单元素的枚举。 提供编译时类型安全性。 如果将参数声明为 Apple 枚举类型,则可以保证传递给该参数的任何非空对象引用都是 Apple 枚举值之一。尝试传递错误类型的值将导致编译时错误,将一个枚举类型的表达式赋值给另一个枚举类型的变量,或者使用 == 运算符比较不同枚举类型的值同样会导致错误。 名称相同的枚举类型常量能共存 名称相同的枚举类型常量能和平共存,因为每种类型都有自己的名称空间。你可以在枚举类型中添加或重新排序常量,而无需重新编译其客户端,因为导出常量的字段在枚举类型及其客户端之间提供了一层隔离:常量值不会像在 int 枚举模式中那样编译到客户端中。最后,你可以通过调用枚举的 toString 方法将其转换为可打印的字符串。 允许添加任意方法和字段并实现任意接口 枚举类型允许添加任意方法和字段并实现任意接口。它们提供了所有 Object 方法的高质量实现(参阅 Chapter 3),还实现了 Comparable(Item-14)和 Serializable(参阅 Chapter 12),并且它们的序列化形式被设计成能够适应枚举类型的可变性。如果方法是常量特有的,可以在枚举类型中添加抽象方法,在声明枚举实例时覆盖抽象方法 // Enum type with constant-specific class bodies and data public enum Operation { PLUS("+") { public double apply(double x, double y) { return x + y; } }, MINUS("-") { public double apply(double x, double y) { return x - y; } }, TIMES("*") { public double apply(double x, double y) { return x * y; } }, DIVIDE("/") { public double apply(double x, double y) { return x / y; } }; private final String symbol; Operation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } // Implementing a fromString method on an enum type private static final Map<String, Operation> stringToEnum =Stream.of(values()).collect(toMap(Object::toString, e -> e)); // Returns Operation for string, if any public static Optional<Operation> fromString(String symbol) { return Optional.ofNullable(stringToEnum.get(symbol)); } public abstract double apply(double x, double y); } 上述实现的缺点是如果常量特有的方法有可以复用的代码,那么会造成许多冗余。你可以把冗余代码抽出成方法,常量覆盖抽象方法时调用抽出的方法,这种实现同样有许多冗余代码;你也可以把抽象方法改成具体方法,里面是可以复用的代码,添加常量时如果不覆盖那就用默认的,这种实现问题在于添加常量时如果忘记覆盖那就用默认方法了。 当常量特有的方法有可以复用的代码时,我们采用策略枚举模式,将可复用代码移到私有嵌套枚举中。当常量调用方法时,调用私有嵌套枚举常量的方法。 在枚举上实现特定常量的行为时 switch 语句不是一个好的选择,只有在枚举不在你的控制之下,你希望它有一个实例方法来返回每个常量的特定行为,这时候才用 switch 总之,枚举类型相对于 int 常量的优势是毋庸置疑的。枚举更易于阅读、更安全、更强大。许多枚举不需要显式构造函数或成员,但有些枚举则受益于将数据与每个常量关联,并提供行为受数据影响的方法。将多个行为与一个方法关联起来,这样的枚举更少。在这种相对少见的情况下,相对于使用 switch 的枚举,特定常量方法更好。如果枚举常量有一些(但不是全部)共享公共行为,请考虑策略枚举模式。 35. 使用实例字段替代序数 每个枚举常量都有一个 ordinal 方法,返回枚举常量在枚举类中的位置。不要用这个方法返回与枚举常量关联的值,一旦常量位置变动,值就是错的,所以不要用序数生成与枚举常量关联的值,而是将其存在实例字段中 36. 用 EnumSet 替代位字段 位字段 如果枚举类型的元素主要在 Set 中使用,传统上使用 int 枚举模式(Item34),通过不同的 2 的幂次为每个常量赋值: // Bit field enumeration constants - OBSOLETE! public class Text { public static final int STYLE_BOLD = 1 << 0; // 1 public static final int STYLE_ITALIC = 1 << 1; // 2 public static final int STYLE_UNDERLINE = 1 << 2; // 4 public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8 // Parameter is bitwise OR of zero or more STYLE_ constants public void applyStyles(int styles) { ... } } 使用位运算的 OR 操作将几个常量组合成一个集合,这个集合叫做位字段: text.applyStyles(STYLE_BOLD | STYLE_ITALIC); 位字段表示方式允许使用位运算高效地执行集合操作,如并集和交集。但是位字段具有 int 枚举常量所有缺点,甚至更多。当位字段被打印为数字时,它比简单的 int 枚举常量更难理解。没有一种简单的方法可以遍历由位字段表示的所有元素。最后,你必须预测在编写 API 时需要的最大位数,并相应地为位字段(通常是 int 或 long)选择一种类型。一旦选择了一种类型,在不更改 API 的情况下,不能超过它的宽度(32 或 64 位)。 EnumSet 一些使用枚举而不是 int 常量的程序员在需要传递常量集合时仍然坚持使用位字段。没有理由这样做,因为存在更好的选择。java.util 包提供 EnumSet 类来有效地表示从单个枚举类型中提取的值集。这个类实现了 Set 接口,提供了所有其他 Set 实现所具有的丰富性、类型安全性和互操作性。但在内部,每个 EnumSet 都表示为一个位向量。如果底层枚举类型有 64 个或更少的元素(大多数都是),则整个 EnumSet 用一个 long 表示,因此其性能与位字段的性能相当。批量操作(如 removeAll 和 retainAll)是使用逐位算法实现的,就像手动处理位字段一样。但是,你可以避免因手工修改导致产生不良代码和潜在错误:EnumSet 为你完成了这些繁重的工作。 当之前的示例修改为使用枚举和 EnumSet 而不是位字段时。它更短,更清晰,更安全: // EnumSet - a modern replacement for bit fields public class Text { public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } // Any Set could be passed in, but EnumSet is clearly best public void applyStyles(Set<Style> styles) { ... } } 下面是将 EnumSet 实例传递给 applyStyles 方法的客户端代码。EnumSet 类提供了一组丰富的静态工厂,可以方便地创建集合,下面的代码演示了其中的一个: text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC)); 总之,如果枚举类型将在 Set 中使用,不要用位字段表示它。 EnumSet 类结合了位字段的简洁性和性能,以及 (Item-34) 中描述的枚举类型的许多优点。EnumSet 的一个真正的缺点是,从 Java 9 开始,它不能创建不可变的 EnumSet,在未来发布的版本中可能会纠正这一点。同时,可以用 Collections.unmodifiableSet 包装 EnumSet,但简洁性和性能将受到影响 37. 使用 EnumMap 替换序数索引 序数索引 当需要将枚举常量映射到其它值时,有一种方法是序数索引,使用 ordinal 方法返回的序数表示key。这种方式存在Item35提到的问题 EnumMap EnumMap使用枚举常量作为key,功能丰富、类型安全 38. 使用接口模拟可扩展枚举 枚举不可以扩展(继承)另一个枚举,但可以实现接口。可以用接口模拟可扩展的枚举。下面的例子将Item34的枚举类型的Operation改成了接口类型。通过实现接口来模拟继承枚举。 // Emulated extensible enum using an interface public interface Operation { double apply(double x, double y); } public enum BasicOperation implements Operation { PLUS("+") { public double apply(double x, double y) { return x + y; } }, MINUS("-") { public double apply(double x, double y) { return x - y; } }, TIMES("*") { public double apply(double x, double y) { return x * y; } }, DIVIDE("/") { public double apply(double x, double y) { return x / y; } }; private final String symbol; BasicOperation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } } // Emulated extension enum public enum ExtendedOperation implements Operation { EXP("^") { public double apply(double x, double y) { return Math.pow(x, y); } }, REMAINDER("%") { public double apply(double x, double y) { return x % y; } }; private final String symbol; ExtendedOperation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } } 可以用接口类型引用指向不同子枚举类型实例。 public static void main(String[] args) { Operation op = BasicOperation.DIVIDE; System.out.println(op.apply(15, 3)); op=ExtendedOperation.EXP; System.out.println(op.apply(2,5)); } java.nio.file.LinkOption 使用了这个Item描述的方法,它实现了 CopyOption 和 OpenOption 接口。 总之,枚举不可以扩展或被扩展,但是你可以通过接口模拟枚举之间的层级关系 39. 注解优于命名模式 命名模式 用来标明某些程序元素需要工具或框架特殊处理。 例如JUnit3及之前,用户必须写test开头的测试方法,不然测试都不能运行。 缺点:1.写错方法名会导致测试通过,但是实际上根本没有运行;2.不能将参数值和程序元素关联,例如测试方法只有在抛出特定异常时才成功,虽然可以精心设计命名模式,将异常名称写在测试方法名上,但是编译器无法检查异常类是否存在 注解 Junit4 开始使用注解,框架会根据注解执行测试方法,也可以将异常和测试方法绑定。注解本身不修改代码语义,而是通过反射(框架所用的技术)对其特殊处理 40. 坚持使用 @Override 注解 请在要覆盖父类声明的每个方法声明上使用 @Override 注解。只有一个例外,那就是具体类覆盖父类的抽象方法,因为具体类必须要实现父类的抽象方法,所以不必加 @Override 注解 41. 使用标记接口定义类型 标记接口 标记接口是一个空接口,它的作用是标记实现类具有某些属性。例如,实现 Serializable 接口表示类的实例可以写入 ObjectOutputStream(序列化) 标记接口VS标记注解 与标记注解(Item39)相比,标记接口有两个优点: 标记接口定义的类型由标记类的实例实现;标记注解不会。因此标记接口类型的存在允许你在编译时捕获错误,标记注解只能在运行时捕获。ObjectOutputStream.writeObject方法入参必须是实现了Serializable的实例,否则会报编译错误(JDK设计缺陷,writeObject入参是Object类型) 标记接口相对于标记注解的另一个优点是可以更精确地定位子类型。例如 Set 是一个标记接口,它继承 Collection接口,Set标记了所有它的实现类都具有Collection功能。Set相较于标记接口的特殊之处在于它还细化了Collection方法 add、equals 和 hashCode 的约定 标记注解的优势:与基于使用注解的框架保持一致性 使用规则 如果标记用在类或接口之外的任何程序元素,必须用标记注解;如果框架使用注解,那就用标记注解;其它情况都用标记接口","categories":[{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/categories/Java/"}],"tags":[{"name":"Effective-Java","slug":"Effective-Java","permalink":"https://zunpan.github.io/tags/Effective-Java/"},{"name":"枚举","slug":"枚举","permalink":"https://zunpan.github.io/tags/%E6%9E%9A%E4%B8%BE/"},{"name":"注解","slug":"注解","permalink":"https://zunpan.github.io/tags/%E6%B3%A8%E8%A7%A3/"}]},{"title":"Effective-Java学习笔记(四)","slug":"Effective-Java学习笔记(四)","date":"2022-07-14T12:17:09.000Z","updated":"2023-09-24T04:27:40.276Z","comments":true,"path":"2022/07/14/Effective-Java学习笔记(四)/","link":"","permalink":"https://zunpan.github.io/2022/07/14/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%9B%9B%EF%BC%89/","excerpt":"","text":"第五章 泛型 26. 不要使用原始类型 泛型和原始类型 声明中具有一个或多个类型参数的类或接口就是泛型类或泛型接口。泛型类和泛型接口统称为泛型。 每个泛型都定义了一个原始类型,它是没有任何相关类型参数的泛型的名称。例如,List<E> 对应的原始类型是 List。原始类型的行为就好像所有泛型信息从类型声明中删除了一样。它们的存在主要是为了与之前的代码兼容。 泛型优势 泛型可以帮助编译器在编译过程中发现潜在的类型转换异常,而原始类型不行。 泛型的子类型规则 原始类型 List 和 参数化类型 List<Object> 都可以保存任何类型的对象 ,但是不能把其它泛型对象 , 例如List<String>对象赋给List<Object>引用而可以赋给 List 引用。泛型有子类型规则,List<String> 是原始类型 List 的子类型,而不是参数化类型 List<Object> 的子类型(Item-28)。假如List<String>可以是List<Object>的子类型,那么List<String>的对象赋给List<Object>,通过List<Object>插入非String元素,违反了List<String>只放String的约定 无界通配符 如果你想使用泛型,但不知道或不关心实际的类型参数是什么,那么可以使用无界通配符 ? 代替。例如,泛型集合 Set<E> 的无界通配符类型是 Set<?>。它是最通用的参数化集合类型,能够容纳任何集合 无界通配符类型 Set<?> 和原始类型 Set 之间的区别在于通配符类型是安全的,而原始类型不是。将任何元素放入具有原始类型的集合中,很容易破坏集合的类型一致性;而无界通配符类型不能放入元素(除了null) Set<Integer> integerSet = new HashSet<>(); // 无界通配符类型Set<?>可以引用任何Set<E>和Set,但是不能往里面放除了null的元素 Set<?> set1 = integerSet; // 原始类型Set也可以引用任何Set<E>和Set,但是可以往里面添加元素,会存在类型转换异常 Set set2 = integerSet; 使用泛型而不用原始类型的例外 类字面量。该规范不允许使用参数化类型(尽管它允许数组类型和基本类型)。换句话说,List.class,String[].class 和 int.class 都是合法的,但是 List<String>.class 和 List<?>.class 不是。 instanceof 运算符。由于泛型信息在运行时被删除,因此在不是无界通配符类型之外的参数化类型使用 instanceof 操作符是非法的。使用无界通配符类型代替原始类型不会以任何方式影响 instanceof 运算符的行为。在这种情况下,尖括号和问号只是多余的。下面的例子是使用通用类型 instanceof 运算符的首选方法: // Legitimate use of raw type - instanceof operator if (o instanceof Set) { // Raw type Set<?> s = (Set<?>) o; // Wildcard type ... } 总之,使用原始类型可能会在运行时导致异常,所以不要轻易使用它们。它们仅用于与引入泛型之前的遗留代码进行兼容。快速回顾一下,Set<Object> 是一个参数化类型,表示可以包含任何类型的对象的集合,Set<?> 是一个无界通配符类型,表示只能包含某种未知类型的对象的集合,Set 是一个原始类型,它没有使用泛型。前两个是安全的,后一个不安全 为便于参考,本条目中介绍的术语(以及后面将要介绍的一些术语)总结如下: Term Example Item Parameterized type List<String> Item-26 Actual type parameter String Item-26 Generic type List<E> Item-26, Item-29 Formal type parameter E Item-26 Unbounded wildcard type List<?> Item-26 Raw type List Item-26 Bounded type parameter <E extends Number> Item-29 Recursive type bound <T extends Comparable<T>> Item-30 Bounded wildcard type List<? extends Number> Item-31 Generic method static <E> List<E> asList(E[] a) Item-30 Type token String.class Item-33 27. 消除 unchecked 警告 消除所有 unchecked 警告可以确保代码是类型安全的,运行时不会出现 ClassCastException。如果不能消除警告,但是可以证明引发警告的代码是类型安全的,那么可以使用 SuppressWarnings(“unchecked”) 注解来抑制警告。 SuppressWarnings注解 SuppressWarnings 注解可以用于任何声明中,从单个局部变量声明到整个类。请总是在尽可能小的范围上使用 SuppressWarnings 注解。通常用在一个变量声明或一个非常短的方法或构造函数。不要在整个类中使用 SuppressWarnings。这样做可能会掩盖关键警告。 如果你发现自己在一个超过一行的方法或构造函数上使用 SuppressWarnings 注解,那么你可以将其移动到局部变量声明中 将 SuppressWarnings 注释放在 return 语句上是非法的,因为它不是声明。你可能想把注释放在整个方法上,但是不要这样做。相反,应该声明一个局部变量来保存返回值并添加注解 每次使用 SuppressWarnings(“unchecked”) 注解时,要添加一条注释,说明这样做是安全的。这将帮助他人理解代码,更重要的是,它将降低其他人修改代码而产生不安全事件的几率。如果你觉得写这样的注释很难,那就继续思考合适的方式,你最终可能会发现,unchecked 操作毕竟是不安全的。 总之,unchecked 警告很重要。不要忽视他们。每个 unchecked 警告都代表了在运行时发生 ClassCastException 的可能性。尽最大努力消除这些警告。如果不能消除 unchecked 警告,但是可以证明引发该警告的代码是类型安全的,那么可以在尽可能狭窄的范围内使用 @SuppressWarnings(“unchecked”) 注释来抑制警告。在注释中记录你决定抑制警告的理由。 28. list 优于数组 数组与泛型的区别 数组是协变的, 如果 Sub 是 Super 的一个子类型,那么数组类型 Sub[] 就是数组类型 Super[] 的一个子类型。相比之下,泛型是不变的:对于任何两个不同类型 Type1 和 Type2,List<Type1> 既不是 List<Type2> 的子类型,也不是 List<Type2> 的父类型。 数组是具体化的。这意味着数组在运行时知道并强制执行他们的元素类型。如前所述,如果试图将 String 元素放入一个 Long 类型的数组中,就会得到 ArrayStoreException。相比之下,泛型是通过擦除来实现的,这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)元素类型信息。擦除允许泛型与不使用泛型的遗留代码自由交互操作(Item-26),确保在 Java 5 中平稳过渡 泛型数组的创建是非法的 由于这些基本差异,数组和泛型不能很好地混合。例如,创建泛型、参数化类型或类型参数的数组是非法的。因此,这些数组创建表达式都不是合法的:new List<E>[]、new List<String>[]、new E[]。所有这些都会在编译时导致泛型数组创建错误。 考虑以下代码片段: // Why generic array creation is illegal - won't compile! List<String>[] stringLists = new List<String>[1]; // (1) List<Integer> intList = List.of(42); // (2) Object[] objects = stringLists; // (3) objects[0] = intList; // (4) String s = stringLists[0].get(0); // (5) 假设创建泛型数组的第 1 行是合法的。第 2 行创建并初始化一个包含单个元素的 List<Integer>。第 3 行将 List<String> 数组存储到 Object 类型的数组变量中,这是合法的,因为数组是协变的。第 4 行将 List<Integer> 存储到 Object 类型的数组的唯一元素中,这是成功的,因为泛型是由擦除实现的:List<Integer> 实例的运行时类型是 List,List<String>[] 实例的运行时类型是 List[],因此这个赋值不会生成 ArrayStoreException。现在我们有麻烦了。我们将一个 List<Integer> 实例存储到一个数组中,该数组声明只保存 List<String> 实例。在第 5 行,我们从这个数组的唯一列表中检索唯一元素。编译器自动将检索到的元素转换为 String 类型,但它是一个 Integer 类型的元素,因此我们在运行时得到一个 ClassCastException。为了防止这种情况发生,第 1 行(创建泛型数组)必须生成编译时错误。 用List替代数组 当你在转换为数组类型时遇到泛型数组创建错误或 unchecked 强制转换警告时,通常最好的解决方案是使用集合类型 List<E>,而不是数组类型 E[]。你可能会牺牲一些简洁性和性能,但作为交换,你可以获得更好地类型安全性和互操作性。 总之,数组和泛型有非常不同的类型规则。数组是协变的、具体化的;泛型是不可变的和可被擦除的。因此,数组提供了运行时类型安全性,而不是编译时类型安全性,对于泛型来说相反。一般来说,数组和泛型不能很好的混合,如果你发现将它们混合在一起并得到编译时错误或警告,那么你的第一个反应应该是将数组替换为 list。 29. 优先使用泛型 编写泛型 在将原始类型修改成泛型时, 可能会遇到不能创建泛型数组的问题,有两种解决方法 创建 Object 数组并将其强制转换为 E[] 类型(字段的类型是 E[] ,将Object数组强转成 E[] 可以成功,但是方法返回 E[]给客户端,由编译器添加隐式强转就会失败,因为隐式强转是将Object数组转成声明的具体类型数组)。现在,编译器将发出一个警告来代替错误。这种用法是合法的,但(一般而言)不是类型安全的。编译器可能无法证明你的程序是类型安全的,但你可以。你必须说服自己,unchecked 的转换不会损害程序的类型安全性。所涉及的数组(元素)存储在私有字段中,从未返回给客户端或传递给任何其它方法(传到外面就会发生隐式强转)。添加元素时,元素也是 E 类型,因此 unchecked 的转换不会造成任何损害。一旦你证明了 unchecked 的转换是安全的,就将警告限制在尽可能小的范围内(Item-27)。 将字段的类型从 E[] 更改为 Object[]。编译器会产生类似的错误和警告,处理方法也和上面类似 方法1优势:可读性更好,因为数组声明为 E[] 类型,这清楚地表明它只包含 E 的实例。它也更简洁,只需要在创建数组的地方做一次强转,其它地方读取数组元素不用强转成 E 类型 方法1劣势:会造成堆污染(Item-32):数组的运行时类型与其编译时类型不匹配(除非 E 恰好是 Object) 为啥要创建泛型数组 Item-28 鼓励优先使用列表而不是数组。但是在泛型中使用列表并不总是可能或可取的。Java 本身不支持列表,因此一些泛型(如ArrayList)必须在数组之上实现。其它泛型(如HashMap)用数组实现来提高性能 总之,泛型比需要在客户端代码中转换的类型更安全、更容易使用。在设计新类型时,请确保客户端可以在不使用类型转换的情况下使用它们。这通常意味着使类型具有通用性。如果你有任何应该是泛型但不是泛型的现有类型,请对它们进行泛化。这将使这些类型的新用户在不破坏现有客户端的情况下更容易使用。 30. 优先使用泛型方法 类可以是泛型的,方法也可以是泛型的 编写泛型方法 编写泛型方法类似于编写泛型。类型参数列表声明类型参数,它位于方法的修饰符与其返回类型之间。例如,类型参数列表为 <E>,返回类型为 Set<E> // Generic method public static <E> Set<E> union(Set<E> s1, Set<E> s2) { Set<E> result = new HashSet<>(s1); result.addAll(s2); return result; } 至少对于简单的泛型方法,这就是(要注意细节的)全部。该方法编译时不生成任何警告,并且提供了类型安全性和易用性。这里有一个简单的程序来演示。这个程序不包含转换,编译时没有错误或警告: // Simple program to exercise generic method public static void main(String[] args) { Set<String> guys = Set.of("Tom", "Dick", "Harry"); Set<String> stooges = Set.of("Larry", "Moe", "Curly"); Set<String> aflCio = union(guys, stooges); System.out.println(aflCio); } 当你运行程序时,它会打印出 [Moe, Tom, Harry, Larry, Curly, Dick]。(输出元素的顺序可能不同)。 union 方法的一个限制是,所有三个集合(输入参数和返回值)的类型必须完全相同。你可以通过使用有界通配符类型(Item-31)使方法更加灵活。 31. 使用有界通配符增加 API 的灵活性 PECS 为了获得满足里氏代换原则,应对表示生产者或消费者入参使用通配符类型。如果输入参数既是生产者优势消费者,使用精确的类型 PECS助记符:PECS 表示生产者应使用 extends,消费者应使用 super。换句话说,如果参数化类型表示 T 生产者,则使用 <? extends T>;如果它表示一个 T 消费者,则使用 <? super T>。不要使用有界通配符类型作为返回类型。 它将强制用户在客户端代码中使用通配符类型,而不是为用户提供额外的灵活性 一个例子 接下来让我们将注意力转移到 Item-30 中的 max 方法,以下是原始声明: public static <T extends Comparable<T>> T max(List<T> list) 下面是使用通配符类型的修正声明: public static <T extends Comparable<? super T>> T max(List<? extends T> list) 这里使用了两次 PECS。第一次是参数列表,list作为生产者生成 T 的实例,所以我们将类型从 List<T> 更改为 List<? extends T>(个人认为这里没有必要改,调用一次max方法只能传入一个类型的List,如果是两个参数,第一个参数的类型决定了T,第二个参数的类型如果是List<? extends T>,那么类型参数必须是T或T的子类。但是如果是泛型类声明的T,那这个改动是有意义的)。第二次是是类型参数 T。这是我们第一次看到通配符应用于类型参数。最初,T 被指定为继承 Comparable<T>,但是 Comparable<T> 消费 T 实例。因此,将参数化类型 Comparable<T> 替换为有界通配符类型 Comparable<? super T>,Comparable 始终是消费者,所以一般应优先使用 Comparable<? super T> 而不是 Comparable<T>,比较器也是如此;因此,通常应该优先使用 Comparator<? super T> 而不是 Comparator<T>。 修订后的 max 声明可能是本书中最复杂的方法声明。增加的复杂性真的能给你带来什么好处吗?是的。下面是一个简单的列表案例,它在原来的声明中不允许使用,但经修改的声明允许: List<ScheduledFuture<?>> scheduledFutures = ... ; 不能将原始方法声明应用于此列表的原因是 ScheduledFuture 没有实现 Comparable<ScheduledFuture>。相反,它是 Delayed 的一个子接口,继承了 Comparable<Delayed>。换句话说,ScheduledFuture 的实例不仅仅可以与其它 ScheduledFuture 实例进行比较,还可以与任何 Delayed 实例比较,但是原始方法只能和 ScheduledFuture 实例比较。更通俗来说,通配符用于支持不直接实现 Comparable(或 Comparator)但继承了实现 Comparable(或 Comparator)的类型的类型。 类型参数和通配符的对偶性 类型参数和通配符之间存在对偶性,对偶性指实现方式不同但效果一样。例如,下面是swap方法的两种可能声明,第一个使用无界类型参数(Item-30),第二个使用无界通配符: // Two possible declarations for the swap method public static <E> void swap(List<E> list, int i, int j); public static void swap(List<?> list, int i, int j); 这两个声明哪个更好?在公共API中第二个更好,因为它更简单(客户端可以传入原始类型)。传入一个任意列表,该方法交换索引元素,不需要担心类型参数。通常,如果类型参数在方法声明中只出现一次,则用通配符替换它。如果它是一个无界类型参数,用一个无界通配符替换它;如果它是有界类型参数,则用有界通配符替换它。 交换的第二个声明有一个问题。下面的实现无法编译, list 的类型是 List<?>,你不能在 List<?> 中放入除 null 以外的任何值。 public static void swap(List<?> list, int i, int j) { list.set(i, list.set(j, list.get(i))); } 幸运的是,有一种方法可以实现,而无需求助于不安全的强制类型转换或原始类型。其思想是编写一个私有助手方法来捕获通配符类型。为了捕获类型,helper 方法必须是泛型方法。它看起来是这样的: public static void swap(List<?> list, int i, int j) { swapHelper(list, i, j); } // Private helper method for wildcard capture private static <E> void swapHelper(List<E> list, int i, int j) { list.set(i, list.set(j, list.get(i))); } swapHelper 方法指导 list 是一个 List<E>。因此,它指导它从这个列表中得到的任何值都是 E 类型的,并且将 E 类型的任何值放入这个列表中都是安全的。这个稍微复杂的实现可以正确编译。它允许我们导出基于通配符的声明,同时在内部利用更复杂的泛型方法。swap 方法的客户端不必面对更复杂的 swapHelper 声明,但它们确实从中受益。值得注意的是,helper 方法具有我们认为对于公共方法过于复杂而忽略的签名。 总之,在 API 中使用通配符类型虽然很棘手,但可以使其更加灵活。如果你编写的库将被广泛使用,则必须考虑通配符类型的正确使用。记住基本规则:生产者使用 extends,消费者使用 super(PECS)。还要记住,所有的 comparable 和 comparator 都是消费者 32. 明智地合用泛型和可变参数 抽象泄露 可变参数方法(Item-53)和泛型都是在 Java 5 中添加,因此你可能认为它们能够优雅地交互;可悲的是,它们并不能。可变参数的目的是允许客户端向方法传递可变数量的参数,但这是一个抽象泄漏:当你调用可变参数方法时,将创建一个数组来保存参数;该数组本应是实现细节,却是可见的。因此,当可变参数具有泛型或参数化类型时,会出现令人困惑的编译器警告。 不可具体化 回想一下 Item-28,不可具体化类型是指其运行时表示的信息少于其编译时表示的信息,并且几乎所有泛型和参数化类型都是不可具体化的。如果方法声明其可变参数为不可具体化类型,编译器将在声明生生成警告。如果方法是在其推断类型不可具体化的可变参数上调用的,编译器也会在调用时生成警告。生成的警告就像这样: warning: [unchecked] Possible heap pollution from parameterized vararg type List<String> 堆污染 当参数化类型的变量引用不属于该类型的对象时,就会发生堆污染。它会导致编译器自动生成的强制类型转换失败,违反泛型系统的基本保证。 例如,考虑这个方法,它来自 Item-26,但做了些修改: // Mixing generics and varargs can violate type safety! // 泛型和可变参数混合使用可能违反类型安全原则! static void dangerous(List<String>... stringLists) { List<Integer> intList = List.of(42); Object[] objects = stringLists; objects[0] = intList; // Heap pollution String s = stringLists[0].get(0); // ClassCastException } 方法声明使用泛型可变参数是合法的 这个例子提出了一个有趣的问题:为什么方法声明中使用泛型可变参数是合法的,而显式创建泛型数组是非法的?答案是,带有泛型或参数化类型的可变参数的方法在实际开发中非常有用,因此语言设计人员选择忍受这种不一致性。事实上,Java 库导出了几个这样的方法,包括 Arrays.asList(T... a)、Collections.addAll(Collection<? super T> c, T... elements) 以及 EnumSet.of(E first, E... rest)。它们与前面显示的危险方法不同,这些库方法是类型安全的。 在 Java 7 之前,使用泛型可变参数的方法的作者对调用点上产生的警告无能为力。使得这些 API 难以使用。用户必须忍受这些警告,或者在每个调用点(Item-27)使用 @SuppressWarnings(“unchecked”) 注释消除这些警告。这种做法乏善可陈,既损害了可读性,也忽略了标记实际问题的警告。 在 Java 7 中添加了 SafeVarargs 注释,以允许使用泛型可变参数的方法的作者自动抑制客户端警告。本质上,SafeVarargs 注释构成了方法作者的一个承诺,即该方法是类型安全的。 作为这个承诺的交换条件,编译器同意不对调用可能不安全的方法的用户发出警告。 使用SafeVarargs的条件 方法没有修改数组元素 数组的引用没有逃逸(这会使不受信任的代码能够访问数组) 换句话说,如果可变参数数组仅用于将可变数量的参数从调用方传输到方法(毕竟这是可变参数的目的),那么该方法是安全的。 一个反例 // UNSAFE - Exposes a reference to its generic parameter array! static <T> T[] toArray(T... args) { return args; } 这个方法直接返回了泛型可变参数数组引用,违反了上面的条件2,它可以将堆污染传播到调用堆栈上。 考虑下面的泛型方法,该方法接受三个类型为 T 的参数,并返回一个包含随机选择的两个参数的数组: public static void main(String[] args) { String[] attributes = pickTwo("Good", "Fast", "Cheap"); } static <T> T[] pickTwo(T a, T b, T c) { switch(ThreadLocalRandom.current().nextInt(3)) { case 0: return toArray(a, b); case 1: return toArray(a, c); case 2: return toArray(b, c); } throw new AssertionError(); // Can't get here } 这段代码编译时不会生成任何警告,运行时会抛出 ClassCastException,尽管它不包含可见的强制类型转换。你没有看到的是,编译器在 pickTwo 返回的值上生成了一个隐藏的 String[] 转换。转换失败,因为 Object[] 实际指向的数组是Object类型的不是String,强转失败。 这个示例的目的是让人明白,让另一个方法访问泛型可变参数数组是不安全的,只有两个例外:将数组传递给另一个使用 @SafeVarargs 正确注释的可变参数方法是安全的,将数组传递给仅计算数组内容的某个函数的非可变方法也是安全的。 扩展 如果 main 方法直接调用 toArray 方法,不会出现 ClassCastException,为什么?泛型不是被擦除了吗,args在运行时不是一个Object[] 吗?非也,运行时args的类型是Object[],但是指向的数组是 String 类型的, 所以编译器添加的强转是可以成功的。下面的例子里,toArray1方法在字节码层面和toArray3方法是一样的,在调用toArray1方法前会先生成 String[],将引用传递给toArray1, public static void main(String[] args) { String [] arr1 = toArray1("Alice", "Bob", "Cat"); // ClassCastException String [] arr2 = toArray2("Alice", "Bob", "Cat"); String [] arr3 = toArray3(new String[]{"Alice", "Bob", "Cat"}); // ClassCastException,多层调用泛型可变参数数组会导致编译器没有足够信息创建真实类型的对象数组(存疑,从字节码来看也是创建了String[]) String[] arr4 = toArray1Crust1("Alice", "Bob", "Cat"); String[] arr5 = toArray1Crust2(new String[]{"Alice", "Bob", "Cat"}); String[] arr6 = toArray1Crust3(new String[]{"Alice", "Bob", "Cat"}); // 泛型方法嵌套没问题 String s = toString1("Alice"); } static <T> T[] toArray1(T... args) { return args; } static <T> T[] toArray2(T a, T b, T c) { System.out.println(a.getClass()); // 泛型数组实际类型是Object T[] result = (T[]) new Object[3]; result[0] = a; result[1] = b; result[2] = c; return result; } static <T> T[] toArray3(T[] arr){ return arr; } static <T> T[] toArray1Crust1(T...args){ return toArray1(args); } static <T> T[] toArray1Crust2(T[] args){ return toArray1(args); } static String[] toArray1Crust3(String[] args){ return toArray1(args); } private static <T> T toString1(T s) { return toString2(s); } public static <T> T toString2(T s){ return s; } 一个正确例子 下面是一个安全使用泛型可变参数的典型示例。该方法接受任意数量的列表作为参数,并返回一个包含所有输入列表的元素的序列列表。因为该方法是用 @SafeVarargs 注释的,所以它不会在声明或调用点上生成任何警告: // Safe method with a generic varargs parameter @SafeVarargs static <T> List<T> flatten(List<? extends T>... lists) { List<T> result = new ArrayList<>(); for (List<? extends T> list : lists) result.addAll(list); return result; } 何时使用SafeVarargs 决定何时使用 SafeVarargs 注释的规则很简单:在每个带有泛型或参数化类型的可变参数的方法上使用 @SafeVarargs,这样它的用户就不会被不必要的和令人困惑的编译器警告所困扰。 请注意,SafeVarargs 注释只能出现在不能覆盖的方法上,因为不可能保证所有可能覆盖的方法都是安全的。在 Java 8 中,注释只能出现在静态方法和final实例方法;在 Java 9 中,它在私有实例方法上也是合法的。 使用 SafeVarargs 注释的另一种选择是接受 Item-28 的建议,并用 List 参数替换可变参数(它是一个伪装的数组)。下面是将这种方法应用到我们的 flatten 方法时的效果。注意,只有参数声明发生了更改: // List as a typesafe alternative to a generic varargs parameter static <T> List<T> flatten(List<List<? extends T>> lists) { List<T> result = new ArrayList<>(); for (List<? extends T> list : lists) result.addAll(list); return result; } 然后可以将此方法与静态工厂方法 List.of 一起使用,以允许可变数量的参数。注意,这种方法依赖于 List.of 声明是用 @SafeVarargs 注释的: audience = flatten(List.of(friends, romans, countrymen)); 这种方法的优点是编译器可以证明该方法是类型安全的。你不必使用 SafeVarargs 注释来保证它的安全性,也不必担心在确定它的安全性时可能出错。主要的缺点是客户端代码比较冗长,可能会比较慢。 这种技巧也可用于无法编写安全的可变参数方法的情况,如第 147 页中的 toArray 方法。它的列表类似于 List.of 方法,我们甚至不用写;Java 库的作者为我们做了这些工作。pickTwo 方法变成这样: static <T> List<T> pickTwo(T a, T b, T c) { switch(rnd.nextInt(3)) { case 0: return List.of(a, b); case 1: return List.of(a, c); case 2: return List.of(b, c); } throw new AssertionError(); } main 方法是这样的: public static void main(String[] args) { List<String> attributes = pickTwo("Good", "Fast", "Cheap"); } 生成的代码是类型安全的,因为它只使用泛型,而不使用数组(List在运行时没有强转,数组会强转)。 总之,可变参数方法和泛型不能很好地交互,并且数组具有与泛型不同的类型规则。虽然泛型可变参数不是类型安全的,但它们是合法的。如果选择使用泛型(或参数化)可变参数编写方法,首先要确保该方法是类型安全的,然后使用 @SafeVarargs 对其进行注释。 33. 考虑类型安全的异构容器 参数化容器 如果你需要存储某种类型的集合,例如存储String的Set,那么Set<String>就足够了,又比如存储 String-Value 键值对,那么Map<String,Integer>就足够了 参数化容器的键 如果你要存储任意类型的集合。例如,一个数据库行可以有任意多列,我们希望用一个Map保存该行每列元素。那么我们可以使用参数化容器的键 例子 Favorites 类,允许客户端存储和检索任意多种类型对象。Class 类的对象将扮演参数化键的角色。Class 对象被称为类型标记 // Typesafe heterogeneous container pattern - client public static void main(String[] args) { Favorites f = new Favorites(); f.putFavorite(String.class, "Java"); f.putFavorite(Integer.class, 0xcafebabe); f.putFavorite(Class.class, Favorites.class); String favoriteString = f.getFavorite(String.class); int favoriteInteger = f.getFavorite(Integer.class); Class<?> favoriteClass = f.getFavorite(Class.class); System.out.printf("%s %x %s%n", favoriteString,favoriteInteger, favoriteClass.getName()); } Favorites实例是类型安全的:当你向它请求一个 String 类型时,它永远不会返回一个 Integer 类型。 Favorites实例也是异构的:所有键都是不同类型的,普通 Map 的键是固定一个类型,因此,我们将 Favorites 称为一个类型安全异构容器。 Favorites 的实现非常简短: // Typesafe heterogeneous container pattern - implementation public class Favorites { private Map<Class<?>, Object> favorites = new HashMap<>(); public <T> void putFavorite(Class<T> type, T instance) { favorites.put(Objects.requireNonNull(type), instance); } public <T> T getFavorite(Class<T> type) { return type.cast(favorites.get(type)); } } 通配符类型的键意味着每个键都可以有不同的参数化类型:一个可以是 Class<String>,下一个是 Class<Integer>,等等。这就是异构的原理。 favorites 的值类型仅仅是 Object。换句话说,Map 不保证键和值之间的类型关系 putFavorite 的实现很简单:它只是将从给定 Class 对象到给定对象的映射关系放入 favorites 中。如前所述,这将丢弃键和值之间的「类型关联」;将无法确定值是键的实例。但这没关系,因为 getFavorites 方法可以重新建立这个关联。 getFavorite 的实现比 putFavorite 的实现更复杂。首先,它从 favorites 中获取与给定 Class 对象对应的值。这是正确的对象引用返回,但它有错误的编译时类型:它是 Object(favorites 的值类型),我们需要返回一个 T。因此,getFavorite 的实现通过使用 Class 的 cast 方法,将对象引用类型动态转化为所代表的 Class 对象。 cast 方法是 Java 的 cast 运算符的动态模拟。它只是检查它的参数是否是类对象表示的类型的实例。如果是,则返回参数;否则它将抛出 ClassCastException。我们知道 getFavorite 中的强制转换调用不会抛出 ClassCastException。也就是说,我们知道 favorites 中的值总是与其键的类型匹配。 如果 cast 方法只是返回它的参数,那么它会为我们做什么呢?cast 方法的签名充分利用了 Class 类是泛型的这一事实。其返回类型为 Class 对象的类型参数: public class Class<T> { T cast(Object obj); } 这正是 getFavorite 方法所需要的。它使我们能够使 Favorites 类型安全,而不需要对 T 进行 unchecked 的转换。 Favorites 类有两个值得注意的限制。 恶意客户端很容易通过使用原始形式的类对象破坏 Favorites 实例的类型安全。通过使用原始类型 HashSet(Item-26),可以轻松地将 String 类型放入 HashSet<Integer> 中。为了获得运行时的类型安全,让 putFavorite 方法检查实例是否是 type 表示的类型的实例,使用动态转换: // Achieving runtime type safety with a dynamic cast public <T> void putFavorite(Class<T> type, T instance) { favorites.put(type, type.cast(instance)); } Favorites 类不能用于不可具体化的类型(Item-28)。换句话说,你可以存储的 Favorites 实例类型为 String 或 String[],但不能存储 List<String>。原因是你不能为 List<String> 获取 Class 对象,List<String>.class 是一个语法错误 Favorites 使用的类型标记是无界的:getFavorite 和 put-Favorite 接受任何 Class 对象。有时你可能需要限制可以传递给方法的类型。这可以通过有界类型标记来实现,它只是一个类型标记,使用有界类型参数(Item-30)或有界通配符(Item-31)对可以表示的类型进行绑定。 annotation API(Item-39)广泛使用了有界类型标记。例如,下面是在运行时读取注释的方法。这个方法来自 AnnotatedElement 接口,它是由表示类、方法、字段和其他程序元素的反射类型实现的: public <T extends Annotation> T getAnnotation(Class<T> annotationType); 参数 annotationType 是表示注释类型的有界类型标记。该方法返回该类型的元素注释(如果有的话),或者返回 null(如果没有的话)。本质上,带注释的元素是一个类型安全的异构容器,其键是注释类型。 假设你有一个 Class<?> 类型的对象,并且希望将其传递给一个需要有界类型标记(例如 getAnnotation)的方法。你可以将对象强制转换为 Class<? extends Annotation>,但是这个强制转换是 unchecked 的,因此它将生成一个编译时警告(Item-27)。幸运的是,Class 类提供了一个实例方法,可以安全地(动态地)执行这种类型的强制转换。该方法叫做 asSubclass,它将 Class 对象强制转换为它所调用的类对象,以表示由其参数表示的类的子类。如果转换成功,则该方法返回其参数;如果失败,则抛出 ClassCastException。 下面是如何使用 asSubclass 方法读取在编译时类型未知的注释。这个方法编译没有错误或警告: // Use of asSubclass to safely cast to a bounded type token static Annotation getAnnotation(AnnotatedElement element,String annotationTypeName) { Class<?> annotationType = null; // Unbounded type token try { annotationType = Class.forName(annotationTypeName); } catch (Exception ex) { throw new IllegalArgumentException(ex); } return element.getAnnotation(annotationType.asSubclass(Annotation.class)); } 总之,以集合的 API 为例的泛型在正常使用时将每个容器的类型参数限制为固定数量。你可以通过将类型参数放置在键上而不是容器上来绕过这个限制。你可以使用 Class 对象作为此类类型安全异构容器的键。以这种方式使用的 Class 对象称为类型标记。还可以使用自定义键类型。例如,可以使用 DatabaseRow 类型表示数据库行(容器),并使用泛型类型 Column<T> 作为它的键","categories":[{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/categories/Java/"}],"tags":[{"name":"Effective-Java","slug":"Effective-Java","permalink":"https://zunpan.github.io/tags/Effective-Java/"},{"name":"泛型","slug":"泛型","permalink":"https://zunpan.github.io/tags/%E6%B3%9B%E5%9E%8B/"}]},{"title":"Effective-Java学习笔记(三)","slug":"Effective-Java学习笔记(三)","date":"2022-07-07T14:31:09.000Z","updated":"2023-09-24T04:27:40.274Z","comments":true,"path":"2022/07/07/Effective-Java学习笔记(三)/","link":"","permalink":"https://zunpan.github.io/2022/07/07/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%89%EF%BC%89/","excerpt":"","text":"第四章 类和接口 15. 尽量减少类和成员的可访问性 信息隐藏的作用 将API与实现完全分离。组件之间只能通过它们的API进行通信,而不知道彼此的内部工作方式。 解耦系统的组件,允许它们被独立开发、测试、优化、使用、理解和修改。 增加了软件的复用性,降低了构建大型系统的风险,即使系统没有成功,单个组件也可能被证明是成功的 访问控制机制 访问控制机制指定了类、接口和成员的可访问性。 对于顶级(非嵌套)类和接口,只有两个可能的访问级别:包私有和公共。如果用 public 修饰符声明,它将是公共的,即API的一部分,修改会损害客户端;否则,它将是包私有的。 如果包私有顶级类或接口只被一个类使用,那么可以考虑变成这个类的私有静态嵌套类(Item-24) 对于成员(字段、方法、嵌套类和嵌套接口),有四个访问级别,这里按可访问性依次递增的顺序列出: 私有,成员只能从声明它的顶级类内部访问 包私有,成员可以从声明它的类的包中访问,包私有就是默认访问,即如果没有指定访问修饰符(接口的成员除外,默认情况下,接口的成员是公共的),就是这个访问级别 保护,成员可以从声明它的类的子类和声明它的类的包中访问 公共,成员可以从任何地方访问 设计类和成员的可访问性 总体规则:让每个类或成员尽可能不可访问。换句话说,在不影响软件正常功能时,使用尽可能低的访问级别。 仔细设计类的公共API,所有成员声明为私有。私有成员和包私有成员都是类实现的一部分,通常不会影响其导出的API。但是,如果类实现了Serializable(Item-86和Item-87),这些字段可能会「泄漏」到导出的 API 中 少用保护成员。保护成员是类导出API的一部分,必须永远支持 子类覆盖父类的方法,可访问性不能缩小 为了测试,将公共类的成员由私有变为包私有是可以接受的,但是进一步提高可访问性是不可接受的 带有公共可变字段的类通常不是线程安全的 为了得到不可变数组,不要公开或通过访问器返回长度非零的数组的引用,客户端将能够修改数组的内容。可以将公共数组设置为私有,并添加一个公共不可变 List;或者,将数组设置为私有,并添加一个返回私有数组副本的公共方法: 总之,你应该尽可能减少程序元素的可访问性(在合理的范围内)。在仔细设计了一个最小的公共 API 之后,你应该防止任何游离的类、接口或成员成为 API 的一部分。除了作为常量的公共静态 final 字段外,公共类应该没有公共字段。确保public static final 字段引用的对象是不可变的。 16. 在公共类中,使用访问器方法,而不是公共字段 访问器方法的作用 如果类是公共的,即可以在包外访问,提供访问器方法可以维持类内部数据表示形式的灵活性 一些建议 如果类是包私有或者是私有嵌套类,公开数据字段比提供访问器方法更清晰 公共类公开不可变字段的危害会小一点,但仍不建议公开字段 17. 减少可变性 不可变类是实例创建后不能被修改的类。Java库包含许多不可变的类,包括 String、基本类型的包装类、BigInteger和BigDecimal。 创建不可变类的规则 要使类不可变,请遵守以下5条规则: 不要提供修改对象状态的方法 确保类不能被继承。这可以防止可变子类损害父类的不可变。防止子类化通常用 final 修饰父类 所有字段用 final 修饰。 所有字段设为私有 确保对任何可变组件的独占访问。如果你的类有任何引用可变对象的字段,请确保该类的客户端无法获得对这些对象的引用。 不可变类的优点 不可变对象线程安全,复用程度高开销小,不需要防御性拷贝 不仅可以复用不可变对象,还可以复用它们的内部实现 不可变对象很适合作为其它对象的构建模块,例如 Map 的键和 Set 的元素 不可变对象自带提供故障原子性 不可变类的缺点 不可变类的主要缺点是每个不同的值都需要一个单独的对象。不可变类BigInteger的flipBit方法会创建一个和原来对象只有一个bit不同的新对象。可变类BigSet可以在固定时间内修改对象单个bit 如果执行多步操作,在每一步生成一个新对象,最终丢弃除最终结果之外的所有对象,那么性能问题就会被放大。解决方法是使用伴随类,如果能预测客户端希望在不可变类上执行哪些复杂操作就可以使用包私有可变伴随可变类(BigInteger和内部的伴随类);否则提供一个公共可变伴随类,例如String和它的伴随类StringBuilder 设计不可变类 为了保证类不被继承,可以用final修饰类,也可以将构造函数变为私有或包私有,通过静态工厂提供对象 BigInteger 和 BigDecimal没有声明为final,为了确保你的类依赖是不可变的BigInteger或BigDecimal,你需要检查对象类型,如果是BigInteger或BigDecimal的子类,那必须防御性复制 适当放松不可变类所有字段必须是 final 的限制可以提供性能,例如,用可变字段缓存计算结果,例如 hashCode 方法在第一次调用时缓存了hash 如果你选择让不可变类实现 Serializable,并且该类包含一个或多个引用可变对象的字段,那么你必须提供一个显式的 readObject 或 readResolve 方法,或者使用 ObjectOutputStream.writeUnshared 或 ObjectInputStream.readUnshared 方法 18. 优先选择组合而不是继承 在同一个包中使用继承是安全的,因为子类和父类的实现都由相同程序员控制。在对专为继承而设计和有文档的类时使用继承也是安全的(Item-19)。然而,对普通的非抽象类进行跨包继承是危险的。与方法调用不同,继承破坏了封装性。换句话说,子类的功能正确与否依赖于它的父类的实现细节。父类的实现可能在版本之间发生变化,如果发生了变化,子类可能会崩溃,即使子类的代码没有被修改过。因此,子类必须与其父类同步更新,除非父类是专门为继承的目的而设计的,并具有很明确的文档说明。 继承的风险 子类覆盖父类的多个方法,父类的多个方法之间有调用关系,因为多态,父类方法在调用其它父类方法时会调用到子类的方法 父类可以添加新方法,新方法没有确保在添加的元素满足断言,子类没有覆盖这个方法,导致调用这个方法时添加了非法元素 父类添加了新方法,但是子类继承原来的父类时也添加了相同签名和不同返回类型的方法,这时子类不能编译,如果签名和返回类型都相同,必须声明覆盖 组合 为新类提供一个引用现有类实例的私有字段,这种设计称为组合,因为现有的类是新类的一个组件。新类中的每个实例方法调用现有类实例的对应方法,并返回结果,这称为转发。比较好的写法是包装类+转发类。 总结 只有子类确实是父类的子类型的情况下,继承才合适。换句话说,两个类 A、B 之间只有 B 满足「is-a」关系时才应该扩展 A。如果你想让 B 扩展 A,那就问问自己:每个 B 都是 A 吗?如果不能对这个问题给出肯定回答,B 不应该扩展 A;如果答案是否定的,通常情况下,B 应该包含 A 的私有实例并暴露不同的 API:A 不是 B 的基本组成部分,而仅仅是其实现的一个细节。 19. 继承要设计良好并且有文档,否则禁止使用 必须精确地在文档中描述覆盖任何方法的效果。文档必须指出方法调用了哪些可覆盖方法、调用顺序以及每次调用的结果如何影响后续处理过程。描述由 Javadoc 标签 @implSpec 生成 20. 接口优于抽象类 Java 有两种机制来定义允许多重实现的类型:接口和抽象类。 接口优势 可以定义 mixin(混合类型) 接口是定义 mixin(混合类型)的理想工具。粗略的说,mixin 是类除了「基本类型」之外还可以实现的类型,用于声明它提供了一些可选的行为。例如,Comparable 是一个 mixin 接口,它允许类的实例可以与其他的可相互比较的对象进行排序。这样的接口称为 mixin,因为它允许可选功能「混合」到基本类型中。抽象类不能用于定义 mixin,原因是:一个类不能有多个父类,而且在类层次结构中没有插入 mixin 的合理位置。‘ 允许构造非层次化类型框架 如果系统中有 n 个属性(例如唱、跳、rap),如果每个属性组合都封装成一个抽象类,组成一个层次化的类型框架,总共有2n2^n2n个类,而接口只需要 n 个 接口劣势 接口不能给 equals 和 hashCode 方法提供默认实现 接口不允许包含实例字段或者非公共静态成员(私有静态方法除外) 结合接口和抽象类的优势 模板方法模式:接口定义类型,提供默认方法,抽象类(骨架实现类)实现接口其余方法。继承骨架实现类已经完成直接实现接口的大部分工作。 按照惯例,骨架实现类称为 AbstractInterface,其中 Interface 是它们实现的接口的名称。例如 Collections Framework 提供了一个骨架实现来配合每个主要的集合接口:AbstractCollection、AbstractSet、AbstractList 和 AbstractMap。可以说,把它们叫做 SkeletalCollection、SkeletalSet、SkeletalList 和 SkeletalMap 更合理,但 Abstract 的用法现在已经根深蒂固。 编写骨架实现类的过程 研究接口有哪些方法,哪些方法可以提供默认实现,如果都可以提供默认实现就不需要骨架实现类,否则,声明一个实现接口的骨架实现类,实现所有剩余的接口方法 21. 为后代设计接口 Java 8 之前,往接口添加方法会导致实现它的类缺少方法,编译错误。Java 8 添加了默认方法,目的是允许向现有接口添加方法,但是向现有接口添加新方法有风险。 默认方法的风险 接口实现类需要同步调用每个方法,但是没有覆盖接口新加入的默认方法,导致调用默认方法时出现 ConcurrentModificationException 接口的现有实现类可以在没有错误或警告的情况下通过编译,但是运行时会出错 22. 接口只用于定义类型 接口只用于定义类型。类实现接口表明客户端可以用类的实例做什么。将接口定义为任何其他目的都是不合适的。 有一个反例是常量接口,接口内只包含 public static final 字段,它的问题在于类使用什么常量是实现细节,而实现常量接口会导致实现细节泄露到 API 中。 导出常量,有几个合理的选择。 如果这些常量与现有的类或接口紧密绑定,则应该将它们添加到类或接口。例如,所有数值包装类,比如 Integer 和 Double,都导出 MIN_VALUE 和 MAX_VALUE 常量。 枚举或者不可实例化的工具类,使用工具类的常量推荐静态导入 23. 类层次结构优于带标签的类 标签类 类的实例有两种或两种以上的样式,并且包含一个标签字段来表示实例的样式。标签类有许多缺点,可读性差、内存占用多、容易出错、添加新样式复杂 类层次结构 标签类只是类层次结构的简易模仿。要将已标签的类转换为类层次结构, 抽取标签类都有的方法、字段到一个抽象类中 继承抽象类实现都有的方法和子类特有的方法 24. 静态成员类优于非静态成员类 嵌套类是在另一个类中定义的类。嵌套类应该只为它的外部类服务。如果嵌套类在其它环境中有用,那么它应该是顶级类。有四种嵌套类:静态成员类、非静态成员类、匿名类和局部类。除了静态成员类,其它嵌套类被称为内部类。 静态成员类 静态成员类是最简单的嵌套类。最好把它看作是一个普通的类,只是碰巧在另一个类中声明而已,并且可以访问外部类的所有成员,甚至是那些声明为 private 的成员。静态成员类是其外部类的静态成员,并且遵守与其它静态成员相同的可访问性规则。如果声明为私有,则只能在外部类中访问,等等。 静态成员类作用 作为公共的辅助类,只有与它的外部类一起使用时才有意义,例如 Calculator 类和公有静态成员类 Operation 枚举 表示由其外部类表示的组件。例如,Map实现类内部的Entry类。Entry对象不需要访问 Map 。因此,使用非静态成员类来表示 entry 是浪费,私有静态成员类是最好的。 非静态成员类 从语法上讲,静态成员类和非静态成员类之间的唯一区别是静态成员类在其声明中具有修饰符 static。尽管语法相似,但这两种嵌套类有很大不同。非静态成员类的每个实例都隐式地与外部类的实例相关联,非静态成员类的实例方法可以调用外部实例上的方法,或者使用受限制的 this (父类.this)构造获得对外部实例的引用。如果嵌套类的实例可以独立于外部类的实例存在,那么嵌套类必须是静态成员类;非静态成员类的实例依赖外部类的实例 非静态成员类实例与外部类实例之间的关联是在创建成员类实例时建立的,之后无法修改。通常,关联是通过从外部类的实例方法中调用非静态成员类构造函数自动建立的。使用 enclosingInstance.new MemberClass(args) 表达式手动建立关联是可能的,尽管这种情况很少见。正如你所期望的那样,关联占用了非静态成员类实例中的空间,并增加了构造时间。 如果声明的成员类不需要访问外部类的实例,那么应始终在声明中添加 static 修饰符,如果省略这个修饰符,每个实例都有一个隐藏的对其外部实例的额外引用。存储此引用需要时间和空间,更糟糕的是,外部类可能不能被垃圾回收。 非静态成员类作用 非静态成员类的一个常见用法是定义一个 Adapter,它允许外部类的实例被视为某个不相关类的实例。例如,Map 接口的实现类通常使用非静态成员类来实现它们的集合视图, Set 和 List,通常使用非静态成员类来实现它们的迭代器 匿名类 匿名类没有名称。它不是外部类的成员。它不是与其它成员一起声明的,而是在使用时同时声明和实例化。匿名类可以在代码中用在任何一个可以用表达式的地方。当且仅当它们出现在非静态环境(没有写在静态方法里面)时,匿名类才持有外部类实例。但是,即使它们出现在静态环境中,它们也不能有除常量以外的任何静态成员。 匿名类的使用有很多限制。你只能在声明它们的时候实例化,你不能执行 instanceof 测试,也不能执行任何其它需要命名类的操作。你不能声明一个匿名类来实现多个接口或继承一个类并同时实现一个接口。匿名类的使用者除了从父类继承的成员外,不能调用任何成员。因为匿名类出现在表达式中,所以它们必须保持简短——大约10行或更短,否则会影响可读性。 匿名类作用 在 lambda 表达式被添加的 Java 之前,匿名类是动态创建小型函数对象和进程对象的首选方法,但 lambda 表达式现在是首选方法(Item-42)。匿名类的另一个常见用法是实现静态工厂方法(参见 Item-20 中的 intArrayAsList 类) 局部类 局部类是四种嵌套类中最不常用的。局部类几乎可以在任何能够声明局部变量的地方使用,并且遵守相同的作用域规则。局部类具有与其它嵌套类相同属性。与成员类一样,它们有名称,可以重复使用呢。与匿名类一样,他们只有在非静态环境中定义的情况下才具有外部类实例,而且它们不能包含静态静态成员。和匿名类一样,它们应该保持简短,以免损害可读性。 嵌套类总结 简单回顾一下,有四种不同类型的嵌套类,每一种都有自己的用途。如果嵌套的类需要在单个方法之外可见,或者太长,不适合放入方法中,则使用成员类。除非成员类的每个实例都需要引用其外部类实例,否则让它保持静态。假设嵌套类属于方法内部,如果你只需要从一个位置创建实例,并且存在一个能够描述类的现有类型,那么将其设置为匿名类;否则,将其设置为局部类。 25. 源文件仅限有单个顶层类 虽然 Java 编译器允许你在单个源文件中定义多个顶层类,但这样做没有任何好处,而且存在重大风险。这种风险源于这样一个事实:在源文件中定义多个顶层类使得为一个类提供多个定义成为可能。所使用的定义受源文件传给编译器的顺序的影响。","categories":[{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/categories/Java/"}],"tags":[{"name":"Effective-Java","slug":"Effective-Java","permalink":"https://zunpan.github.io/tags/Effective-Java/"},{"name":"类和接口","slug":"类和接口","permalink":"https://zunpan.github.io/tags/%E7%B1%BB%E5%92%8C%E6%8E%A5%E5%8F%A3/"}]},{"title":"Effective-Java学习笔记(二)","slug":"Effective-Java学习笔记(二)","date":"2022-06-30T11:50:05.000Z","updated":"2023-09-24T04:27:40.274Z","comments":true,"path":"2022/06/30/Effective-Java学习笔记(二)/","link":"","permalink":"https://zunpan.github.io/2022/06/30/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89/","excerpt":"","text":"第三章 对象的通用方法 10. 覆盖 equals 方法时应遵守的约定 不覆盖equals方法的情况 类的每个实例本质上都是唯一的。例如Thread类,它是活动实体类而不是值类 该类不需要提供逻辑相等测试。例如java.util.regex.Pattern可以覆盖equals方法来检查两个Pattern实例是否表示完全相同的正则表达式,但是这个类的设计人员认为客户端不需要这个功能,所以没有覆盖 父类已经覆盖了equals方法,父类的行为也适合于这个类。例如大多数Set的equals从AbstractSet继承,List从AbstractList继承,Map从AbstractMap继承 类是私有的并且你确信它的equals方法永远不会被调用。保险起见,你可以按如下方式覆盖equals方法,以确保它不会被意外调用 @Override public boolean equals(Object o) { throw new AssertionError(); // Method is never called } 覆盖equals方法的时机 当一个类有一个逻辑相等的概念,而这个概念不同于仅判断对象的同一性(相同对象的引用),并且父类没有覆盖 equals。对于值类通常是这样。值类只是表示值的类,例如 Integer 或 String。程序员希望发现它们在逻辑上是否等价,而不是它们是否引用相同的对象。覆盖 equals 方法不仅是为了满足程序员的期望,它还使实例能够作为 Map 的键或 Set 元素时,具有可预测的、理想的行为。 单例模式的值类不需要覆盖equals方法。例如,枚举类型就是单例值类。逻辑相等就是引用相等。 覆盖equals方法的规范 反身性:对于任何非空的参考值x,x.equals(x)必须返回true 对称性:x.equals(y)与y.equals(x)的值要么都为true要么为false 传递性:对于非空引用x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也返回true 一致性:对于任何非空的引用值 x 和 y,x.equals(y) 的多次调用必须一致地返回 true 或一致地返回 false,前提是不修改 equals 中使用的信息。 非空性:对于非空引用x,x.equals(null)返回false,不需要显示判断是否为null,因为equals方法需要将参数转为成相同类型,转换之前会使用instanceof运算符来检查类型是否正确,如果为null,也会返回false @Override public boolean equals(Object o) { if (!(o instanceof MyType)) return false; MyType mt = (MyType) o; ... } 高质量构建equals方法的步骤 1、使用==检查参数是否是this对象的引用,如果是,返回true,这是一种性能优化,如果比较的开销很大,这种优化很有必要 2、使用instanceof运算符检查参数是否有正确类型。 3、将参数转换为正确的类型。因为在转换前进行了instanceof判断,所以肯定可以强转成功 4、对类中的每个有意义的字段检查是否和参数的相应字段匹配 对于不是float和double的基本类型字段,使用==比较;对应对象引用字段,递归调用equals方法;对于float字段,使用静态方法Float.compare(float,float)方法;对于double字段,使用Double.compare(double,double)。float和double字段的特殊处理是由于Float.NaN,-0.0f 和类似的双重值的存在;对于数组字段,使用Arrays.equals方法。 一些对象引用字段可能允许null出现。为了避免可能出现NullPointerException,请使用Objects.equals(Object,Object)来检查对象的字段是否相等。 对于某些类,字段比较非常复杂,如果是这样,可以存储字段的规范形式,以便equals方法进行开销较小的比较。这种技术最适合于不可变类;如果对象可以修改,则必须使规范形式保持最新 equals 方法的性能可能会受到字段比较顺序的影响。为了获得最佳性能,你应该首先比较那些更可能不同、比较成本更低的字段。不能比较不属于对象逻辑状态的字段,例如用于同步操作的锁字段。派生字段可以不比较,这样可以提高性能,但是如果派生字段包括了对象的所有信息,比如说多边形面积,可以从边和顶点计算得出,那么先比较面积,面积不一样就肯定不是同一个对象,这样可以减小开销 一些警告 覆盖equals方法时,也要覆盖hashCode方法 考虑任何形式的别名都不是一个好主意。例如,File类不应该尝试将引用同一文件的符号链接等同起来 不要用别的类型替换equals方法的Object类型 11. 当覆盖equals方法时,总是覆盖hashCode方法 Object类中hashCode方法的规范 应用程序执行期间对对象重复调用hashCode方法,它必须返回相同的值,前提是不修改equals方法中用于比较的信息。这个值不需要在应用程序的不同执行之间保持一致 如果equals(Object)方法返回true,那么在这两个对象上调用hashCode方法必须产生相同的整数结果 如果equals(Object)方法返回false,hashCode方法的值不需要一定不同,但是,不同对象的hashCode不一样可以提高散列表性能 当没有覆盖hashCode方法时,将违反第二条规范:逻辑相等的对象必须有相等的散列码。两个不同的对象在逻辑上是相等的,但它们的hashCode一般不相等。例如用Item-10中的PhoneNumber类实例作为HashMap的键 Map<PhoneNumber, String> m = new HashMap<>(); m.put(new PhoneNumber(707, 867, 5309), "Jenny"); 此时,你可能期望m.get(new PhoneNumber(707, 867,5309)) 返回「Jenny」,但是它返回 null。因为PhoneNumber类没有覆盖hashCode方法,插入到HashMap和从HashMap中获取的实例具有不相同的散列码,这违法了hashCode方法规范。因此,get方法查找电话号码的散列桶与put方法存储电话号码的散列桶不同。 解决这个问题有一个最简单但很糟糕的实现 // The worst possible legal hashCode implementation - never use! @Override public int hashCode() { return 42; } 它确保了逻辑相等的对象具有相同的散列码。同时它也很糟糕,因为每个对象的散列码都相同了。每个对象都分配到一个存储桶中,散列表退化成链表。 散列算法设计步骤 一个好的散列算法大概率为逻辑不相等对象生成不相同的散列码。理想情况下,一个散列算法应该在所有int值上均匀合理分布所有不相等对象。实现理想情况很困难,但实现一个类似的并不难,这里有一个简单的方式: 1、声明一个名为 result 的int变量,并将其初始化为对象中第一个重要字段的散列码c,如步骤2.a中计算的那样 2、对象中剩余的重要字段f,执行以下操作: a. 为字段计算一个整数散列码c : 如果字段是基本数据类型,计算Type.hashCode(f),其中type是 f 类型对应的包装类 ; 如果字段是对象引用,并且该类的equals方法通过递归调用equals方法来比较字段,则递归调用字段上的hashCode方法。如果需要更复杂的比较,则为该字段计算一个【canonical representation】,并在canonical representation上调用hashCode方法。如果字段的值为空,则使用0(或其它常数,但0是惯用的);如果字段是一个数组,则对数组中每个重要元素都计算散列码,并用2.b步骤逐个组合。如果数组中没有重要元素,则使用常量,最好不是0。如果所有元素都很重要,那么使用Arrays.hashCode b. 将2.a步骤中计算的散列码合并到result变量,如下所示 result = 31 * result + c; 3、返回result变量 步骤2.b中的乘法说明result依赖字段的顺序,如果类具有多个类似字段,那么乘法会产生更好的hash性能。例如字符串hash算法中如果省略乘法,那么不同顺序的字符串都会有相同的散列码。 选择31是因为它是奇素数。如果是偶数,乘法运算就会溢出,信息就会丢失,因为乘法运算等同于移位。使用素数的好处不太明显,但它是传统用法。31有一个很好的特性,可以用移位和减法来代替乘法,从而在某些体系结构上获得更好的性能:31 * i == (i <<5) – i。现代虚拟机自动进行这种优化。 根据前面的步骤,给PhoneNumber类写一个hashCode方法 // Typical hashCode method @Override public int hashCode() { int result = Short.hashCode(areaCode); result = 31 * result + Short.hashCode(prefix); result = 31 * result + Short.hashCode(lineNum); return result; } 因为这个方法返回一个简单的确定的计算结果,它的唯一输入是 PhoneNumber 实例中的三个重要字段,所以很明显,相等的 PhoneNumber 实例具有相等的散列码。实际上,这个方法是 PhoneNumber 的一个非常好的 hashCode 方法实现,与 Java 库中的 hashCode 方法实现相当。它很简单,速度也相当快,并且合理地将不相等的电话号码分散到不同的散列桶中。 虽然这个Item里的方法可以提供一个相当不错的散列算法,但它不是最先进的,对于大多数用途是足够的,如果需要不太可能产生冲突的散列算法。请参阅 Guava 的 com.google.common.hash.Hashing Objects类有一个静态方法,它接受任意数量的对象并返回它们的散列码。这个名为hash的方法允许你编写只有一行代码的hashCode方法,它的质量可以与本Item提供的编写方法媲美。但不幸的是它们运行得很慢,因为它需要创建数组来传递可变数量的参数,如果有参数是原始类型的,则需要进行装箱和拆箱。推荐只在性能不重要的情况下使用这种散列算法。下面是使用这个静态方法编写的PhoneNumber的散列算法 // One-line hashCode method - mediocre performance @Override public int hashCode() { return Objects.hash(lineNum, prefix, areaCode); } 缓存散列值 如果类是不可变的,并且计算散列码的成本非常高,那么可以考虑在对象中缓存散列码,而不是每次调用重新计算。如果这个类的对象会被用作散列键,那么应该在创建对象时就计算散列码。要不然就在第一次调用时计算散列码 12. 始终覆盖toString方法 虽然 Object 提供 toString 方法的实现,但它返回的字符串通常不是类的用户希望看到的。它由后跟「at」符号(@)的类名和散列码的无符号十六进制表示(例如 PhoneNumber@163b91)组成。toString 的通用约定是这么描述的,返回的字符串应该是「简洁但信息丰富的表示,易于阅读」。虽然有人认为 PhoneNumber@163b91 简洁易懂,但与 707-867-5309 相比,它的信息量并不大。toString 约定接着描述,「建议所有子类覆盖此方法。」好建议,确实! 虽然它不如遵守euals和hashCode约定(Item10和Item11)那么重要,但是提供一个好的toString方法更便于调试。当对象被传递给 println、printf、字符串连接操作符或断言或由调试器打印时,将自动调用 toString 方法。即使你从来没有调用 toString 对象,其他人也可能使用。例如,使用该对象的组件可以在日志错误消息中包含对象的字符串表示。如果你不覆盖 toString,该消息可能完全无用。 13. 明智地覆盖clone方法 Cloneable接口的作用 Cloneable接口的作用是声明类可克隆,但是接口不包含任何方法,类的clone方法继承自Object,并且Object类的clone方法是受保护的,无法跨包调用,虽然可以通过反射调用,但也不能保证对象具有可访问的 clone 方法(如果类没有覆盖clone方法可以通过获取父类Object调用clone方法,但是如果类没实现Cloneable接口调用会抛出CloneNotSupportedException)。 既然 Cloneable 接口不包含任何方法,用它来做什么呢?它决定了 Object 类受保护的 clone 实现的行为:如果一个类实现了 Cloneable 接口,Object 类的 clone 方法则返回该类实例的逐字段拷贝;没实现 Cloneable 接口调用 clone 方法会抛出 CloneNotSupportedException。这是接口非常不典型的一种使用方式,不应该效仿。通常,类实现接口可以表明类能够为其客户端做些什么。在本例中,它修改了父类上受保护的方法的行为。 clone 方法规范 虽然规范没有说明,但是在实践中,实现 Cloneable 接口的类应该提供一个功能正常的 public clone 方法。 clone方法的一般约定很薄弱。下面的内容是从Object规范复制过来的 Creates and returns a copy of this object. The precise meaning of “copy” may depend on the class of the object. The general intent is that, for any object x,the expression x.clone() != x will be true, and the expression x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements. While it is typically the case that x.clone().equals(x) will be true, this is not an absolute requirement. clone方法创建并返回对象的副本。「副本」的确切含义可能取决于对象的类。通常,对于任何对象 x,表达式 x.clone() != x、x.clone().getClass() == x.getClass() 以及 x.clone().equals(x) 的值都将为 true,但都不是绝对的。(equals方法应覆盖为比较对象中的字段才能得到true,默认实现是比较对象地址,结果永远为false) 按照约定,clone方法返回的对象应该通过调用super.clone() 来获得。如果一个类和它的所有父类(Object类除外)都遵守这个约定,表达式 x.clone().getClass() == x.getClass() 则为 true 按照约定,返回的对象应该独立于被克隆的对象。为了实现这种独立性,可能需要在super.clone() 前修改对象的一个或多个字段 这种机制有点类似于构造方法链,只是没有强制执行: 如果一个类的clone方法返回的实例不是通过调用 super.clone() 而是通过调用构造函数获得的,编译器不会报错,但是如果这个类的子类调用super.clone(),由此产生的对象将是错误的,影响子类clone方法正常工作 如果覆盖clone方法的类是final修饰的,那么可以忽略这个约定,因为不会有子类 如果一个final修饰的类的clone方法不调用super.clone()。该类没有理由实现Cloneable接口,因为它不依赖于Object的clone方法 覆盖clone方法 如果类是不可变的,不要提供clone方法 如果类的字段都是基本类型或不可变对象的引用,那么直接这个类的clone方法直接调用super.clone()即可 如果类的字段包含可变对象的引用,需要递归调用可变对象的深拷贝方法 一些细节 和构造函数一样,不要在clone方法中调用可覆盖方法。如果clone方法调用一个在子类中被覆盖的方法,这个方法将在子类修复其在克隆中的状态之前执行,很可能导致克隆和原始对象的破坏。 Object的clone方法被声明为抛出CloneNotSupportedException,但是覆盖方法时 try-catch 异常就行,不抛出受检查异常的方法更容易使用 设计可继承的类时不要实现 Cloneable接口(如果实现了,子类就必须对外提供clone方法)。你可以选择通过实现一个功能正常的受保护克隆方法来模拟 Object 的行为,该方法声明为抛出 CloneNotSupportedException。这给子类实现 Cloneable 或不实现 Cloneable 的自由。或者,你可以选择不实现一个有效的克隆方法,并通过提供以下退化的克隆实现来防止子类实现它: // clone method for extendable class not supporting Cloneable @Override protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } 如果你编写了一个实现了 Cloneable 接口的线程安全类,请记住它的 clone 方法必须正确同步,就像其他任何方法一样。 总结 回顾一下,所有实现 Cloneable 接口的类都应该使用一个返回类型为类本身的公有方法覆盖 clone。这个方法应该首先调用 super.clone(),然后「修复」任何需要「修复」的字段。通常,这意味着复制任何包含对象内部「深层结构」的可变对象,并将克隆对象对这些对象的引用替换为对其副本的引用。虽然这些内部副本通常可以通过递归调用 clone 来实现,但这并不总是最好的方法。如果类只包含基本数据类型的字段或对不可变对象的引用,那么很可能不需要修复任何字段。这条规则也有例外。例如,表示序列号或其他唯一 ID 的字段需要修复,即使它是基本数据类型或不可变的。 搞这么复杂真的有必要吗?答案是否定的。如果你扩展了一个已经实现了 Cloneable 接口的类,那么除了实现行为良好的 clone 方法之外,你别无选择。否则,最好提供对象复制的替代方法。一个更好的对象复制方法是提供一个复制构造函数或复制工厂。复制构造函数是一个简单的构造函数,它接受单个参数,其类型是包含构造函数的类,例如 // Copy constructor public Yum(Yum yum) { ... }; 复制工厂与复制构造函数的静态工厂(Item-1)类似: // Copy factory public static Yum newInstance(Yum yum) { ... }; 复制构造函数方法及其静态工厂变体与克隆方法相比有许多优点: 它们不依赖于易发生风险的语言外对象创建机制(Object的clone方法是native的); 他们不要求无法强制执行的约定(clone方法一定要先调用super.clone()); 它们与 final 字段不冲突(clone方法不能修改final字段); 它们不会抛出不必要的 checked 异常; 而且不需要强制类型转换。 可以提供类型转换构造函数 考虑到与 Cloneable 相关的所有问题,新的接口不应该继承它,新的可扩展类不应该实现它。通常,复制功能最好由构造函数或工厂提供。这个规则的一个明显的例外是数组,最好使用 clone 方法来复制数组。 14. 考虑实现 Comparable 接口 与本章讨论的其它方法不同,compareTo 方法不是 Object 中声明的,而是 Comparable 接口中的唯一方法。一个类实现Comparable,表明实例具有自然顺序(字母或数字或时间顺序)。Java 库中的所有值类以及所有枚举类型(Item-34)都实现了 Comparable接口 compareTo方法约定 compareTo 方法的一般约定类似于 equals 方法: 将一个对象与指定对象进行顺序比较。当该对象小于、等于或大于指定对象时,对应返回一个负整数、零或正整数。如果指定对象的类型阻止它与该对象进行比较,则抛出 ClassCastException 在下面的描述中, sgn(expression) 表示数学中的符号函数,它被定义为:根据传入表达式的值是负数、零或正数,对应返回-1、0或1。 实现类必须确保所有 x 和 y 满足 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))(这意味着 x.compareTo(y) 当且仅当 y.compareTo(x) 抛出异常时才抛出异常) 实现类还必须确保关系是可传递的:(x.compareTo(y) > 0 && y.compareTo(z) > 0) 意味着 x.compareTo(z) > 0 最后,实现类必须确保 x.compareTo(y) == 0 时,所有的 z 满足 sgn(x.compareTo(z)) == sgn(y.compareTo(z)) 强烈建议 (x.compareTo(y)== 0) == (x.equals(y)) 成立,但这不是必需的。一般来说,任何实现 Comparable 接口并违法此条件的类都应该清除地注明这一事实。推荐的表述是“Note: This class has a natural ordering that is inconsistent with equals.” 与equals方法不同,equals方法入参是Object类型,而compareTo方法不需要和不同类型的对象比较:当遇到不同类型的对象时,允许抛出ClassCastException 就像违反 hashCode 约定的类可以破坏依赖 hash 的其他类一样,违反 compareTo 约定的类也可以破坏依赖 Comparable 的其他类。依赖 Comparable 的类包括排序集合 TreeSet 和 TreeMap,以及实用工具类 Collections 和 Arrays,它们都包含搜索和排序算法。 compareTo的约定和equals约定有相同的限制:反身性、对称性和传递性。如果要向实现 Comparable 的类中添加值组件,不要继承它;编写一个不相关的类,其中包含第一个类的实例。然后提供返回所包含实例的「视图」方法。这使你可以自由地在包含类上实现你喜欢的任何 compareTo 方法,同时允许它的客户端在需要时将包含类的实例视为被包含类的实例。 compareTo 约定的最后一段是一个强烈建议而不是要求,它只是简单地说明了 compareTo 方法所施加地同等性检验通常应该与 equals 方法返回相同的结果。如果一个类的 compareTo 方法强加了一个与 equals 不一致的顺序,那么这个类仍然可以工作,但是包含该类元素的有序集合可能无法遵守集合接口(Collection、Set 或 Map)的一般约定。这是因为这些接口的一般约定是根据 equals 方法定义的,但是有序集合使用 compareTo 代替了 equals 实施同等性建议,这是需要注意的地方 例如,考虑 BigDecimal 类,它的 compareTo 方法与 equals 不一致。如果你创建一个空的 HashSet 实例,然后添加 new BigDecimal(“1.0”) 和 new BigDecimal(“1.00”),那么该 HashSet 将包含两个元素,因为添加到该集合的两个 BigDecimal 实例在使用 equals 方法进行比较时结果是不相等的。但是,如果你使用 TreeSet 而不是 HashSet 执行相同的过程,那么该集合将只包含一个元素,因为使用 compareTo 方法比较两个 BigDecimal 实例时结果是相等的。(有关详细信息,请参阅 BigDecimal 文档。) 编写 compareTo 方法 递归调用引用字段的 compareTo 方法。如果引用字段没有实现 Comparable,或者需要一个非标准的排序,那么应使用比较器 基本字段类型使用包装类的静态 compare 方法比较 从最重要字段开始比较 考虑使用 java.util.Comparator接口的比较器构造方法","categories":[{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/categories/Java/"}],"tags":[{"name":"Effective-Java","slug":"Effective-Java","permalink":"https://zunpan.github.io/tags/Effective-Java/"},{"name":"对象的通用方法","slug":"对象的通用方法","permalink":"https://zunpan.github.io/tags/%E5%AF%B9%E8%B1%A1%E7%9A%84%E9%80%9A%E7%94%A8%E6%96%B9%E6%B3%95/"}]},{"title":"Effective-Java学习笔记(一)","slug":"Effective-Java学习笔记(一)","date":"2022-06-23T10:11:15.000Z","updated":"2023-09-24T04:27:40.273Z","comments":true,"path":"2022/06/23/Effective-Java学习笔记(一)/","link":"","permalink":"https://zunpan.github.io/2022/06/23/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%80%EF%BC%89/","excerpt":"","text":"第二章 创建和销毁对象 1. 考虑用静态工厂方法代替构造函数 静态工厂方法是一个返回该类实例的 public static 方法。例如,Boolean类的valueOf方法 public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; } 要注意静态工厂方法与设计模式中的工厂方法不同。 静态工厂方法优点: 静态工厂方法有确切名字,客户端实例化对象时代码更易懂。例如,BigInteger类中返回可能为素数的BigInteger对象静态工厂方法叫BigInteger.probablePrime。此外,每个类的构造函数签名是唯一的,但是程序员可以通过调整参数类型、个数或顺序修改构造函数的签名,这样会给客户端实例化对象带来困惑,因为静态工厂方法有确切名称所以不会出现这个问题 静态工厂方法不需要在每次调用时创建新对象。例如Boolean.valueOf(boolean),true和false会返回预先创建好的对应Boolean对象。这种能力允许类在任何时候都能严格控制存在的实例,常用来实现单例 静态工厂方法可以获取任何子类的对象。这种能力的一个应用是API可以不公开子类或者实现类的情况下返回对象。例如Collections类提供静态工厂生成不是public的子类对象,不可修改集合和同步集合等 静态工厂方法返回对象的类型可以根据输入参数变换。例如,EnumSet类的noneOf方法,当enum的元素个数小于等于64,noneOf方法返回RegularEnumSet类型的对象,否则返回JumboEnumSet类型的对象 静态工厂方法的返回对象的类不需要存在。这种灵活的静态工厂方法构成了服务提供者框架的基础。service provider框架有三个必要的组件:代表实现的service interface;provider registration API,提供者用来注册实现;service access API,客户端使用它来获取服务的实例,服务访问API允许客户端选择不同实现,是一个灵活的静态工厂方法。service provider第四个可选的组件是service provider interface,它描述了产生service interface实例的工厂对象。在JDBC中,Connection扮演service interface角色,DriverManager.registerDriver是provider registration API,DriverManager.getConnection是service access API,Driver是service provider interface 静态工厂方法的缺点: 只提供静态工厂方法而没有public或者protected的构造方法就不能被继承,例如Collections类就不能被继承 静态工厂方法没有构造函数那么显眼,常见的静态工厂方法名字如下: from,一种类型转换方法,该方法接受单个参数并返回该类型的相应实例,例如: Date d = Date.from(instant); of,一个聚合方法,它接受多个参数并返回一个包含这些参数的实例,例如: Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING); valueOf,一种替代 from 和 of 但更冗长的方法 BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE); instance 或 getInstance,返回一个实例,该实例由其参数(如果有的话)描述,但不具有相同的值,例如: StackWalker luke = StackWalker.getInstance(options); create 或 newInstance,与 instance 或 getInstance 类似,只是该方法保证每个调用都返回一个新实例,例如: Object newArray = Array.newInstance(classObject, arrayLen); getType,类似于 getInstance,但如果工厂方法位于不同的类中,则使用此方法。其类型是工厂方法返回的对象类型,例如: FileStore fs = Files.getFileStore(path); newType,与 newInstance 类似,但是如果工厂方法在不同的类中使用。类型是工厂方法返回的对象类型,例如: BufferedReader br = Files.newBufferedReader(path);` type,一个用来替代 getType 和 newType 的比较简单的方式,例如: List<Complaint> litany = Collections.list(legacyLitany); 2. 当构造函数有多个参数时,考虑改用Builder 静态工厂和构造函数都有一个局限:不能对大量可选参数做很好扩展。例如,一个表示食品营养标签的类,必选字段有净含量、热量,另外有超过20个可选字段,比如反式脂肪、钠等。 为这种类编写构造函数,通常是使用可伸缩构造函数,这里展示四个可选字段的情况 // Telescoping constructor pattern - does not scale well! public class NutritionFacts { private final int servingSize; // (mL) required private final int servings; // (per container) required private final int calories; // (per serving) optional private final int fat; // (g/serving) optional private final int sodium; // (mg/serving) optional private final int carbohydrate; // (g/serving) optional public NutritionFacts(int servingSize, int servings) { this(servingSize, servings, 0); } public NutritionFacts(int servingSize, int servings, int calories) { this(servingSize, servings, calories, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat) { this(servingSize, servings, calories, fat, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) { this(servingSize, servings, calories, fat, sodium, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) { this.servingSize = servingSize; this.servings = servings; this.calories = calories; this.fat = fat; this.sodium = sodium; this.carbohydrate = carbohydrate; } } 当你想创建指定carbohydrate的实例,就必须调用最后一个构造函数,这样就必须给出calories、fat、sodium的值。当有很多可选参数时,这种模式可读性很差。 当遇到许多可选参数时,另一种选择是使用JavaBean模式,在这种模式中,调用一个无参数的构造函数来创建对象,然后调用setter方法来设置参数值 // JavaBeans Pattern - allows inconsistency, mandates mutability public class NutritionFacts { // Parameters initialized to default values (if any) private int servingSize = -1; // Required; no default value private int servings = -1; // Required; no default value private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public NutritionFacts() { } // Setters public void setServingSize(int val) { servingSize = val; } public void setServings(int val) { servings = val; } public void setCalories(int val) { calories = val; } public void setFat(int val) { fat = val; } public void setSodium(int val) { sodium = val; } public void setCarbohydrate(int val) { carbohydrate = val; } } 这种模式比可伸缩构造函数模式更易读,但有严重的缺点。因为对象的构建要调用多个set方法,所以对象可能会在构建过程中处于不一致状态(多线程下,其它线程使用了未构建完成的对象) 第三种选择是建造者模式,它结合了可伸缩构造函数模式的安全性和JavaBean模式的可读性。客户端不直接生成所需的对象,而是使用所有必需的参数调用构造函数或静态工厂方法生成一个builder对象。然后,客户端在builder对象上调用像set一样的方法来设置可选参数,最后调用build方法生成所需对象。Builder通常是它构建的类的静态成员类 // Builder Pattern public class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public static class Builder { // Required parameters private final int servingSize; private final int servings; // Optional parameters - initialized to default values private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; } } NutritionFacts类是不可变的。Builder的set方法返回builder对象本身,这样就可以链式调用。下面是客户端代码的样子: NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .calories(100).sodium(35).carbohydrate(27).build(); 为了简介,这里省略的参数校验。参数校验在Builder的构造函数和方法中。多参数校验在build方法中 建造者模式也适用于抽象类,抽象类有抽象Builder import java.util.EnumSet; import java.util.Objects; import java.util.Set; // Builder pattern for class hierarchies public abstract class Pizza { public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE} final Set<Topping> toppings; abstract static class Builder<T extends Builder<T>> { EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class); public T addTopping(Topping topping) { toppings.add(Objects.requireNonNull(topping)); return self(); } abstract Pizza build(); // Subclasses must override this method to return "this" protected abstract T self(); } Pizza(Builder<?> builder) { toppings = builder.toppings.clone(); // See Item 50 } } import java.util.Objects; public class NyPizza extends Pizza { public enum Size {SMALL, MEDIUM, LARGE} private final Size size; public static class Builder extends Pizza.Builder<Builder> { private final Size size; public Builder(Size size) { this.size = Objects.requireNonNull(size); } @Override public NyPizza build() { return new NyPizza(this); } @Override protected Builder self() { return this; } } private NyPizza(Builder builder) { super(builder); size = builder.size; } } public class Calzone extends Pizza { private final boolean sauceInside; public static class Builder extends Pizza.Builder<Builder> { private boolean sauceInside = false; // Default public Builder sauceInside() { sauceInside = true; return this; } @Override public Calzone build() { return new Calzone(this); } @Override protected Builder self() { return this; } } private Calzone(Builder builder) { super(builder); sauceInside = builder.sauceInside; } } 客户端实例化对象代码如下: NyPizza pizza = new NyPizza.Builder(SMALL) .addTopping(SAUSAGE).addTopping(ONION).build(); Calzone calzone = new Calzone.Builder() .addTopping(HAM).sauceInside().build(); 建造者模式非常灵活,一个builder对象可以反复构建多个对象,可以通过builder中的方法调用生成不同的对象。建造者模式的缺点就是生成一个对象前要先创建它的builder对象,性能会稍差一些,但为了未来字段更好扩展,建议还是用建造者模式 3. 使用私有构造函数或枚举类型创建单例 单例是只实例化一次的类。当类不保存状态或状态都一致,那么它的对象本质上都是一样的,可以用单例模式创建 实现单例有两种方法。两者都基于私有化构造函数和对外提供 public static 成员,在第一个方法中,该成员是个用final修饰的字段 // Singleton with public final field public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public void leaveTheBuilding() { ... } } 私有构造函数只调用一次,用于初始化public static final 修饰的Elvis类型字段INSTANCE。一旦初始化Elvis类,就只会存在一个Elvis实例。客户端不能再创建别的实例,但是要注意的是拥有特殊权限的客户端可以利用反射调用私有构造函数生成实例 Constructor<?>[] constructors = Elvis.class.getDeclaredConstructors(); AccessibleObject.setAccessible(constructors, true); Arrays.stream(constructors).forEach(name -> { if (name.toString().contains("Elvis")) { Elvis instance = (Elvis) name.newInstance(); instance.leaveTheBuilding(); } }); 第二种方法,对外提供的 public static 成员是个静态工厂方法 // Singleton with static factory public class Elvis { private static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public static Elvis getInstance() { return INSTANCE; } public void leaveTheBuilding() { ... } } 所有对getInsance()方法的调用都返回相同的对象,但是同样可以通过反射调用私有构造函数创建对象。 这两种方法要实现可序列化,仅仅在声明中添加implements Serializable是不够的。还要声明所有实例字段未transient,并提供readResolve方法,否则,每次反序列化都会创建一个新实例。JVM在反序列化时会自动调用readResolve方法 // readResolve method to preserve singleton property private Object readResolve() { // Return the one true Elvis and let the garbage collector // take care of the Elvis impersonator. return INSTANCE; } 实现单例的第三种方法时声明一个单元素枚举 // Enum singleton - the preferred approach public enum Elvis { INSTANCE; public void leaveTheBuilding() { ... } } 这种方法类似于 public 字段方法,但是它更简洁,默认提供了序列化机制,提供了对多个实例化的严格保证,即使面对复杂的序列化或反射攻击也是如此。这种方法可能有点不自然,但是单元素枚举类型通常是实现单例的最佳方法。但是,如果你的单例要继承父类,那么就不能用这种方法 4. 用私有构造函数实施不可实例化 工具类不需要实例化,它里面的方法都是public static的,可以通过私有化构造函数使类不可实例化 // Noninstantiable utility class public class UtilityClass { // Suppress default constructor for noninstantiability private UtilityClass() { throw new AssertionError(); } ... // Remainder omitted } AssertionError不是必须有的,但可以防止构造函数被意外调用(反射) 这种用法也防止了类被继承。因为所有子类构造函数都必须显示或者隐式调用父类构造函数,但父类构造函数私有化后就无法调用 5. 依赖注入优于硬连接资源 有些类依赖于一个或多个资源。例如拼写检查程序依赖于字典。错误实现如下: // Inappropriate use of static utility - inflexible & untestable! public class SpellChecker { private static final Lexicon dictionary = ...; private SpellChecker() {} // Noninstantiable public static boolean isValid(String word) { ... } public static List<String> suggestions(String typo) { ... } } // Inappropriate use of singleton - inflexible & untestable! public class SpellChecker { private final Lexicon dictionary = ...; private SpellChecker(...) {} public static INSTANCE = new SpellChecker(...); public boolean isValid(String word) { ... } public List<String> suggestions(String typo) { ... } } 这两种写法分别是工具类和单例,都假定使用同一个字典,实际情况是不同拼写检查程序依赖不同的字典。 你可能会想取消dictionary的final修饰,并让SpellChecker类添加更改dictionary的方法。但这种方法在并发环境下会出错。工具类和单例不适用于需要参数化依赖对象 参数化依赖对象的一种简单模式是,创建对象时将被依赖的对象传递给构造函数。这是依赖注入的一种形式。 // Dependency injection provides flexibility and testability public class SpellChecker { private final Lexicon dictionary; public SpellChecker(Lexicon dictionary) { this.dictionary = Objects.requireNonNull(dictionary); } public boolean isValid(String word) { ... } public List<String> suggestions(String typo) { ... } } 依赖注入适用于构造函数、静态工厂方法、建造者模式。 依赖注入的一个有用变体是将工厂传递给构造函数,这样可以反复创建被依赖的对象。Java8中引入的Supplier<T>非常适合作为工厂。下面是一个生产瓷砖的方法 Mosaic create(Supplier<? extends Tile> tileFactory) { ... } 尽管依赖注入极大提高了灵活性和可测试性,但它可能会使大型项目变得混乱。通过使用依赖注入框架(如Spring、Dagger、Guice)可以消除这种混乱 6. 避免创建不必要的对象 复用对象可以加快程序运行速度,如果对象是不可变的,那么它总是可以被复用的。 一个不复用对象的极端例子如下 String s = new String("bikini"); // DON'T DO THIS! 该语句每次执行都会创建一个新的String实例。"bikini"本身就是一个String实例,改进的代码如下 String s = "bikini"; 这个版本使用单个String实例,而不是每次执行时都会创建一个新的实例。此外,可以保证在同一虚拟机中运行的其它代码都可以复用该对象,只要它们都包含相同的字符串字面量 通常可以使用静态工厂方法来避免创建不必要的对象。例如,Boolean.valueOf(String) 比构造函数Boolean(String) 更可取,后者在Java9中被废弃了。 有些对象的创建代价很高,如果要重复使用,最好做缓存。例如,使用正则表达式确定字符串是否为有效的罗马数字 // Performance can be greatly improved! static boolean isRomanNumeral(String s) { return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"); } 这个方法的问题在于它依赖String.matches方法。虽然 String.matches 是检查字符串是否与正则表达式匹配的最简单方法,但它不适合要求高性能的情况下重复使用,因为它在内部为正则表达式创建了一个Pattern实例,并且只使用了一次就垃圾回收了,创建一个Pattern实例代价很大,因为它需要将正则表达式编译成有限的状态机 为了提高性能,将正则表达式显示编译成Pattern实例,作为类初始化的一部分,每次调用匹配的方法时都使用这个实例 // Reusing expensive object for improved performance public class RomanNumerals { private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"); static boolean isRomanNumeral(String s) { return ROMAN.matcher(s).matches(); } } 当对象是不可变的时候,我们很容易会想到复用。但是有些情况不那么容易想到复用。例如适配器模式中的适配器,因为适配器中没有它适配对象的状态,所以不需要创建多个适配器。 一个具体的例子是,KeySet是Map中的适配器,使得Map不仅能提供返回键值对的方法,也能返回所有键的Set,KeySet不需要重复创建,对Map的修改会同步到KeySet实例 7. 排除过时的对象引用 Java具体垃圾回收机制,会让程序员的工作轻松很多,但是并不意味着需要考虑内存管理,考虑以下简单的堆栈实现 import java.util.Arrays; import java.util.EmptyStackException; // Can you spot the "memory leak"? public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) throw new EmptyStackException(); return elements[--size]; } /** * Ensure space for at least one more element, roughly * doubling the capacity each time the array needs to grow. */ private void ensureCapacity() { if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } } 这段代码有一个潜在的内存泄露问题。当栈的长度增加,再收缩时,从栈中pop的对象不会被回收,因为引用仍然还在elements中。解决方法很简单,一旦pop就置空 public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // Eliminate obsolete reference return result; } 用null处理过时引用的另一个好处是,如果被意外引用的话会立刻抛NullPointerException 另外一个常见的内存泄漏是缓存。HashMap的key是对实际对象的强引用,不会被GC回收。WeakHashMap的key如果只有WeakHashMap本身使用,外部没有使用,那么会被GC回收 内存泄露第三种常见来源是监听器和其它回调。如果客户端注册了回调但是没有显式地取消它们,它们就会一直在内存中。确保回调被及时回收的一种方法是仅存储它们的弱引用,例如,将他它们作为键存储在WeakHashMap中 8. 避免使用终结器和清除器 如标题所说 9. 使用 try-with-resources 优于 try-finally Java库中有许多必须通过调用close方法手动关闭的资源,比如InputStream、OutputStream和java.sql.Connection。 从历史上看,try-finally语句是确保正确关闭资源的最佳方法,即便出现异常或返回 // try-finally - No longer the best way to close resources! static String firstLineOfFile(String path) throws IOException { BufferedReader br = new BufferedReader(new FileReader(path)); try { return br.readLine(); } finally { br.close(); } } 这种方式在资源变多时就很糟糕 // try-finally is ugly when used with more than one resource! static void copy(String src, String dst) throws IOException { InputStream in = new FileInputStream(src); try { OutputStream out = new FileOutputStream(dst); try { byte[] buf = new byte[BUFFER_SIZE]; int n; while ((n = in.read(buf)) >= 0) out.write(buf, 0, n); } finally { out.close(); } } finally { in.close(); } } 使用 try-finally 语句关闭资源的正确代码(如前两个代码示例所示)也有一个细微的缺陷。try 块和 finally 块中的代码都能够抛出异常。例如,在 firstLineOfFile 方法中,由于底层物理设备发生故障,对 readLine 的调用可能会抛出异常,而关闭的调用也可能出于同样的原因而失败。在这种情况下,第二个异常将完全覆盖第一个异常。异常堆栈跟踪中没有第一个异常的记录,这可能会使实际系统中的调试变得非常复杂(而这可能是希望出现的第一个异常,以便诊断问题) Java7 引入 try-with-resources语句解决了这个问题。要使用这个结构,资源必须实现AutoCloseable接口,这个接口只有一个void close方法,下面是前两个例子的try-with-resources形式 // try-with-resources - the the best way to close resources! static String firstLineOfFile(String path) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } } // try-with-resources on multiple resources - short and sweet static void copy(String src, String dst) throws IOException { try (InputStream in = new FileInputStream(src);OutputStream out = new FileOutputStream(dst)) { byte[] buf = new byte[BUFFER_SIZE]; int n; while ((n = in.read(buf)) >= 0) out.write(buf, 0, n); } } try-with-resources为开发者提供了更好的异常排查方式。考虑firstLineOfFile方法,如果异常由readLine和不可见close抛出,那么后者异常会被抑制。被抑制的异常不会被抛弃,它们会被打印在堆栈中并标记被抑制。可以通过getSuppressed方法访问它们,该方法是Java7中添加到Throwable中的 像try-catch-finally一样,try-with-resources也可以写catch语句。下面是firstLineOfFile方法的一个版本,它不抛出异常,但如果无法打开文件或从中读取文件,会返回一个默认值 // try-with-resources with a catch clause static String firstLineOfFile(String path, String defaultVal) { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } catch (IOException e) { return defaultVal; } }","categories":[{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/categories/Java/"}],"tags":[{"name":"Effective-Java","slug":"Effective-Java","permalink":"https://zunpan.github.io/tags/Effective-Java/"},{"name":"创建和销毁对象","slug":"创建和销毁对象","permalink":"https://zunpan.github.io/tags/%E5%88%9B%E5%BB%BA%E5%92%8C%E9%94%80%E6%AF%81%E5%AF%B9%E8%B1%A1/"}]},{"title":"跨域问题","slug":"跨域问题","date":"2022-06-22T09:12:37.000Z","updated":"2023-09-24T04:27:40.285Z","comments":true,"path":"2022/06/22/跨域问题/","link":"","permalink":"https://zunpan.github.io/2022/06/22/%E8%B7%A8%E5%9F%9F%E9%97%AE%E9%A2%98/","excerpt":"","text":"同源策略 同源策略是浏览器的重要安全策略。大致来说,不同域(协议+域名/ip+端口)生成的cookie只能给这个域使用 跨域出现与解决 下面的演示是在hosts文件中添加以下配置 127.0.0.1 zhufeng-test.163.com 不添加的话,把zhufeng-test.163.com换成localhost或者127.0.0.1是一样的 假如我们只有后端,它的域是http://zhufeng-test.163.com:8080,它有一个get方法是setCookie,那么访问http://zhufeng-test.163.com:8080/setCookie,此时可以在Response Cookies中找到这个cookie,对这个域下的接口访问时会自动带上这个域所有可见的cookie(springboot开启allowCredentials,前端axios需要自己开启withCredentials: true) 现在我们有前端了,前后端分离运行,前端运行在http://zhufeng-test.163.com:8081 因为后端生成的cookie的domain是 zhufeng-test.163.com,所以浏览器访问相同域名的前端也可以读取到这个cookie。 但是前端不能访问后端的其它接口,因为它们端口不同,发生了跨域,浏览器不会带上cookie 给后端配置一下跨域,.allowedOrigins(“http://zhufeng-test.163.com:8081/”)表示允许前端http://zhufeng-test.163.com:8081访问后端,不开会返回状态码200的跨域错误,.allowCredentials(true)不开时前端访问后端不会带上后端域名可见的cookie @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { //项目中的所有接口都支持跨域 registry.addMapping("/**") // 所有地址都可以访问,也可以配置具体地址 // .allowedOrigins("http://zhufeng-test.163.com:8081/") .allowedOriginPatterns("*://*:*/") // "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS" .allowedMethods("*") // 允许前端携带后端域名可见的cookie .allowCredentials(true) .maxAge(3600); } }","categories":[{"name":"基础知识","slug":"基础知识","permalink":"https://zunpan.github.io/categories/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"}],"tags":[{"name":"CORS","slug":"CORS","permalink":"https://zunpan.github.io/tags/CORS/"},{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/tags/Java/"}]},{"title":"CryptographyⅠ笔记","slug":"CryptographyⅠ笔记","date":"2022-06-04T06:51:30.000Z","updated":"2023-09-24T04:27:40.272Z","comments":true,"path":"2022/06/04/CryptographyⅠ笔记/","link":"","permalink":"https://zunpan.github.io/2022/06/04/Cryptography%E2%85%A0%E7%AC%94%E8%AE%B0/","excerpt":"","text":"斯坦福教授Dan Boneh的密码学课程《Cryptography》 课程链接:https://www.bilibili.com/video/BV1Ht411w7Re 讲义:https://www.cs.virginia.edu/~evans/courses/crypto-notes.pdf 目标:课程结束后可以推导加密机制的安全性,可以破解不安全的加密机制 绪论 密码学用途 安全通信,例如https,Bluetooth 加密磁盘文件,例如EFS 内容保护,例如DVD使用CSS 用户认证 高阶用途: 外包计算(同态加密) 安全多方计算 零知识证明 安全通信 在web服务中使用https协议进行通信,起到安全通信作用的是TLS协议,TLS协议主要包括两部分: 握手协议:使用公钥密码体制建立共享密钥 Record层:使用共享密钥传输数据,保证数据的机密性和完整性 加密磁盘文件 加密磁盘文件从哲学角度来看就是今天的Alice和明天的Alice进行安全通信 对称加密 Alice和Bob双方共享密钥k,攻击者不知道密钥k,他们使用两个算法进行通信,分别是加密算法E,解密算法D。加密算法以原信息和密钥为输入产生相应密文;解密算法正好相反,以密文和密钥为输入,输出原信息。加密解密算法是公开的,只有密钥k是保密的。我们应当使用公开的算法,因为它的安全性经过业内人士的审查。 密钥使用次数 可以根据密钥使用次数分为一次使用的密钥,多次使用的密钥 一次使用的密钥只用来加密一个信息,例如加密邮件,为每一封邮件都生成一个新的密钥。 多次使用的密钥可以用来加密多个消息,例如用同一密钥加密文件系统的许多文件,多次使用的密钥需要更多机制来确保加密系统是安全的 一些结论 任何依赖可信第三方的计算都可以不借助可信第三方完成 密码算法提出三步骤 明确描述威胁模型。例如数字签名算法中,攻击者如何攻击数字签名?伪造签名目的何在? 提出算法构成 证明在该威胁模型下,攻击者破解了算法等同于破解了根本性难题。意思就是算法的安全性由根本性难题保证,例如RSA依赖大整数质因数分解困难问题 密码历史 对称密码历史 替换式密码 替换式密码的密钥是一张替换表 例如:凯撒密码 凯撒密码是没有密钥或者只有一个固定密钥的替换式密码 假设替换式密码的密钥只能使用26个英文字母,它的密钥空间有多大?是26!26!26!,约等于2882^{88}288,也就是说可以用88位比特表示密钥,这个密钥空间是足够大的,但是这个加密算法不安全 如何破解这个加密算法? 使用字母频率来破解。在英文文献中,出现频次最高的字母是e,那么我们统计密文中出现频次最高的那个字母,例如c,那么这个替换式密码的密钥k中很可能包含了e->c;同样的道理,出现频次第二高的是t,第三高的是a,分别找到密文中对应的字母进行还原 使用字母配对(组合)。在英语中,最常见的二元配对有“he”,“an”,“in”,“th”。第一种方法还原出了eta,根据配对规则可以还原配对中的另一个字母。 这种攻击方式叫唯密文攻击(CT only attack),替换式密码抵御不了这种攻击 Vigener密码 Vigener密码的密钥是一个单词,加密算法是将密钥重复直到和明文一样长,与明文逐位求和模26得到密文,解密算法是对密文逐位减去密钥 如何破解Vigener密码? 首先假设攻击者已知密钥长度(未知也不影响破解,枚举长度即可),例如密钥长度为6,那么攻击者可以将密文按照每6个字母一组进行分组。然后看每组的第一个字母,他们都是由同一个字母加密的,例如上图中k的字母C。我们同样可以使用唯密文攻击,在每组的第一个字母中找到出现频次最高的字母,它们对应的明文位为E,然后对应的密钥为为密文位-E = 密钥位。同样的方法对密钥第2位直到最后一位执行。这种破解方法的实质是多次使用唯密文攻击 轮轴机 最早的轮轴机:Hebern machine(一个轮轴) 本质上是一个替换式密码。按下A,假如A加密成了T,此时轮轴机转动一格,再次按下A,A就加密成了S。轮轴机一经提出很快就被破解了,同样是唯密文攻击。 最著名的轮轴机:Enigma(3-5轮轴) 数字时代的密码 1974:DES(keys = 2562^{56}256, block size = 64 bits) Today: AES(2001), Salsa20(2008) (and others) 离散概率 有限集合与概率分布 事件 事件是有限集合的子集 事件的并集发生的概率存在上界 随机变量 随机变量是一个函数 X:U(有限集合)->V(某个集合) 集合V是随机变量取值的地方。例如定义随机变量X:{0,1}n⟶{0,1}X:\\{0,1\\}^n\\longrightarrow \\{0,1\\}X:{0,1}n⟶{0,1};X(y)=lsb(y)∈{0,1}X(y)=lsb(y) \\in \\{0,1\\}X(y)=lsb(y)∈{0,1} 这个随机变量将n位长度的字符串集合映射成了只有0和1的两个元素的集合,具体做法是取最低位。任意一个n位长度的字符串会被映射出0或者1 均匀随机变量 随机化算法 随机化算法相比于确定的算法,在原像中加入了随机因子,所以对同一条消息进行随机化,结果一般是不同的 独立性 异或 异或就是逐位模2和 异或在密码学中非常重要,在一个有限集合上有一个随机变量Y,有一个独立的均匀随机变量X,那么Z:=Y⨁XZ:=Y\\bigoplus XZ:=Y⨁X 仍然是这个集合上的均匀随机变量 生日悖论 r指的是随机变量的值 独立同分布举例:假设U = {00,01,10,11}表示抛硬币两次的结果,令随机变量X:U→{0,1}X:U\\rightarrow\\{0,1\\}X:U→{0,1}表示第一次抛硬币的结果,Y:U→{0,1}Y:U\\rightarrow\\{0,1\\}Y:U→{0,1}表示第二次抛硬币的结果。X和Y是相互独立的且概率一样,所以这两个随机变量是独立同分布的 流密码 流密码是一种对称密码。 对称密码的严格定义:定义在K,M,C\\mathcal{K,M,C}K,M,C上的一对“有效”加解密算法(E,D)(E,D)(E,D) K\\mathcal{K}K指密钥空间,M\\mathcal{M}M指明文空间,E\\mathcal{E}E指密文空间 E:K×M→CE: \\mathcal{K\\times M\\rightarrow C}E:K×M→C D:K×C→MD:\\mathcal{K\\times C\\rightarrow M}D:K×C→M 即 ∀m∈M,k∈K:D(k,E(k,m))=m\\forall m \\in \\mathcal{M}, k \\in \\mathcal{K}: D(k, E(k, m))=m∀m∈M,k∈K:D(k,E(k,m))=m "有效"这个词在理论密码学家看来,时间复杂度是多项式时间内的就是有效的,真正运行时间取决于输入规模。在应用密码学家看来,可能是加解密1GB数据要在10s内就算有效。 特别注意,加密算法是随机化算法,解密算法是确定性的算法 一次性密码本 一次性密码本的明文空间、密文空间、密钥空间均相同 一次性密码本的加密算法是将明文与密钥做异或运算,解密也是将密文与密钥做异或运算,根据异或运算的结合律可以知道加解密算法是正确的 如何证明这个密码的安全性? 信息论的创始人香农提出一个观点:不能从密文得出关于明文的任何信息 攻击者截获一段密文,如果满足下面的等式,即不同明文经过同一个密钥加密后等于这个密文的概率是相同的,那么攻击者就无法得知真正的明文,这种密码是完美保密性的 一次性密码本是完美保密性的 完美保密性只是意味着没有唯密文攻击,并不意味着在实际使用中是安全的 香农在给出完美保密性的证明后又给出了一个定理,想要完美保密性,密钥长度要大于等于明文长度,所以完美保密性的密码是不实用的 流密码(Stream ciphers) 流密码是使用的一次性密码本,它的思想是使用伪随机密钥代替随机密钥 伪随机数生成器是一个函数,它可以将s位的01比特串扩展成n位的01比特串,n>>s。注意,伪随机数生成器算法是不具备随机性的,具备随机性的是种子 流密码使用伪随机数生成器(PRG),将密钥当做种子,生成真正用于加密的比特串 因为密钥长度远小于明文长度,所以流密码并不是完美保密性的 流密码的安全性不同于完美保密性,它需要另外一种安全性定义,这种安全性依赖具体的伪随机数生成器 安全的伪随机数生成器必须是不可预测的。 假设伪随机数生成器是可预测的,那么存在某种算法可以根据PRG的前i位推测出后面的n-i位。在SMTP协议中,明文头是“from:” , 攻击者可以用这段已知的明文与截获的密文做异或得出PRG输出的前i位,再根据预测算法得出完整的密钥,再与密文异或得出明文 严格定义PRG是可预测的: 存在“有效”算法A,对PRG的前i位做计算,输出的值与PRG的第i+1位相同的概率大于1/2+ϵ,(ϵ≥1/230)1/2+\\epsilon, (\\epsilon \\ge 1/2^{30})1/2+ϵ,(ϵ≥1/230), ϵ\\epsilonϵ大于1/2301/2^{30}1/230时是不可忽略的 不安全的PRG例子 线性同余法的PRG 可忽略和不可忽略 针对一次性密码本和流密码的攻击 两次密码本攻击 流密码的密钥一旦使用两次就不安全 WEP还有一个问题就是它的PRG使用的是RC4,它的不可预测性是较弱的,存在有效算法可以从四万帧中恢复PRG的输出 完整性攻击 一次性密码本和流密码都只保护数据的机密性,但不保证完整性,攻击者可以修改将密文与攻击者的置换密钥做异或从而定向影响明文,这种攻击无法被检测出来 流密码实际应用 现代流密码:eStream eStream支持5种PRG,这里讲其中一种,这种PRG的输入除了种子(密钥)还有一个随机数,好处是不用每次更换密钥,因为输入还包括随机数,整体是唯一的 eStream中同时支持软硬件的流密码Salsa20 图中的 || 并不是简单的拼接。这个PRG首先构造一个64kb的字符串,里面包含k,r,i(从0开始),通过多次一一映射h,最后与原字符串进行加法(不是异或)得出输出 PRG的安全定义 PRG的输出与均匀随机变量的输出不可区分 统计测试 为了定义不可区分,首先引入统计测试,统计测试是一个算法,输入值是“随机数”,输出0表示不是真随机数,1表示是真随机数 上图的例子1,这个统计测试输出1的情况当且仅当随机数中0和1的个数差小于等于随机数的长度开根号乘以10 统计测试算法有好有坏,可能并不随机的字符串也认为是随机的。所以我们需要评估统计测试算法的好坏 我们定义一个变量叫做优势,优势是统计测试算法相对于伪随机数生成器的,优势越接近1,统计测试算法越能区分伪随机数和真随机数 PRG的密码学安全定义 PRG是安全的当且仅当不存在有效的统计算法,它的优势是不可忽略的。即所有统计算法都认为PRG的输出是真随机数 但是,我们不能构造一个PRG并证明PRG是安全的,即不存在有效的统计算法。但是我们还是有大量的PRG候选方案 我们可以证明当PRG是可预测时,PRG是不安全的 当存在一个好的预测算法和好的统计测试算法,如下图。那么统计测试算法可以以一个不可忽略的 ϵ\\epsilonϵ 分辨出伪随机数和真随机数 姚期智证明了上面命题的逆命题也成立,即当PRG是不可预测时,PRG是安全的 不可区分的更通用的定义如下 语义安全 什么是安全的密码? 攻击者不能从密文中恢复密钥 攻击者不能从密文中恢复明文 香农认为不能从密文中获得任何关于明文的信息才是安全的密码 香农的完美保密性定义约束太强,可以用计算不可区分代替 一次性密码本的语义安全 定义语义安全的的方式是通过两个实验,攻击者发送两个明文信息,挑战者(应该是被挑战者)随机选取密钥,做两次实验,第一次实验加密第一个信息,第二次实验加密第二个信息,攻击者判断密文对应的明文是哪个 上图定义了语义安全的优势,等于实验0中攻击者输出1的概率和实验1中输出1的概率之差的绝对值。简单理解一下,假如攻击者不能区分两次实验,那么实验0和实验1的输出1的概率是一样的,那么攻击者的优势为0,不能区分两次实验,意味着加密算法是语义安全的;如果攻击者能区分实验0和实验1,那么概率差不可忽略,攻击者有一定的优势区分两次实验 事实上,一次性密码本不仅是语义安全的,而且是完美保密性的 安全的PRG可以构成语义安全的流密码 我们做两次实验证明流密码的语义安全,第一次使用伪随机数生成器,第二次使用真随机数 分组密码 分组密码也属于对称密码,将明文分解成固定大小的分组,使用密钥加密成同样大小的密文 分组密码的加密过程是将密钥扩展成多个,使用轮函数多次计算分组得出密文 PRPs和PRFs K表示密钥空间,X表示明文空间,Y表示密文空间 给定密钥后,伪随机置换的加密函数是一个一一映射,也就意味着存在解密函数。伪随机置换和分组密码十分相似,有时候会混用术语 安全的PRFs Funs[X,Y]表示所有从明文空间到密文空间的真随机函数的集合,易知这个集合大小等于明文空间大小的密文空间大小次,而伪随机函数的集合大小由密钥决定,一个密钥决定了一个伪随机函数,一个安全的PRF与真随机函数不可区分 上图的G问题在于x=0时,输出固定了,攻击者可以通过x=0时的输出是否为0来判断他在和真随机函数交互还是伪随机函数,因为x=0,输出为0的概率实在太低了,等于密文空间的倒数 可以使用安全的PRF来构造安全的PRG DES DES的轮(回合)函数使用的是Feistel网络,核心思想是每个分组2n bits,右边n bits原封不动变成下一层分组左边n bits,左边n bits经过伪随机函数转换再和右边n bits异或变成下一层右边n bits 易知这个网络是可逆的,注意,不要求伪随机函数是可逆的 定理:如果伪随机函数使用的密钥是相互独立的,那么Feistel网络是一个安全的PRP 回合函数f由F根据回合密钥推导出来,回合密钥由主密钥推导得来,IP和IP逆是伪随机置换,和DES安全性无关,仅仅是标准要求 线性函数:函数可以表示成矩阵乘以入参 如果所有的置换盒子都是线性的,那么整个DES就是线性的,因为只有DES算法中只有置换盒子可能是非线性的,其它就是异或、位移等线性运算。如果是线性DES,那么存在一个矩阵B,DES可以写成B乘以一个包含明文和回合密钥的向量 针对DES的攻击 密钥穷举攻击 给定一些明文密文对,找到一个密钥使得明文密文配对,这个密文极大概率是唯一的 为了抵抗穷举攻击,衍生出了3DES 2DES存在安全性问题 针对分组密码的攻击 旁道攻击 通过测试加解密的时间或者功耗来推测密钥 错误攻击 通过外部手段影响加解密硬件,比如提高时钟频率、加热芯片,使得加密的最后一回合发生错误,根据错误信息可以推测出密钥 这两种攻击需要先窃取到硬件,比如上图的IC卡 线性和差分攻击 同样是需要给定明文密文对,推测密钥,比穷举攻击效率更高 加密算法中使用了线性函数导致下面等式以一个不可忽略的概率成立 图中的MAJ表示majority 量子攻击 AES AES基于代换置换网络构建,和Feistel最大的区别在于,在这个网络的每一回合函数会影响每一位bit 使用PRGs构造分组密码 分组密码实质是PRP,首先考虑能不能使用PRG构造PRF 使用安全的PRG可以构造一个安全的PRF,但是并不实用 有了安全的PRF,我们可以使用Luby-Rackoff定理转换成PRP,因此可以使用PRG来构造分组密码,但是不如AES启发性PRF实用 使用分组密码 很显然,真随机置换空间和伪随机置换空间大小一样,所以它是安全的PRP 这个PRP不是安全的PRF,因为明文空间太小了 使用一次性密钥的分组密码 ECB的问题在于相同的明文会加密成相同的密文,攻击者可能不知道明文内容,但也会从中学到明文的一些信息 攻击者来挑战算法,本来不应该知道两张加密图片的区别,但是ECB将头发加密成了很多1,头发又重复出现,这样密文就会出现很多1,攻击者就能根据这个区别分辨两张图片 ECB被用来加密长于一个分组的消息时不是语义安全的 安全的电子密码本是为每个分组生成一个随机密钥进行加密,类似于AES的密钥扩展 使用密钥多次利用的分组密码 密钥多次利用的分组密码的选择明文攻击就是进行多次的语义安全实验(CPA安全) 确定的加密对于选择明文攻击不可能是语义安全的,所以多次使用一个密钥加密时,相同的明文,应该产生不同的输出,有两种方法。 第一种:随机化算法 第二种:基于新鲜值的加密 新鲜值不必随机但是不能重复 随机化的新鲜值和上面的随机化算法是一样的 基于新鲜值的加密的选择明文攻击下的安全性 密钥多次利用的运行方式(CBC) CBC:密码分组链接模式 L是加密的明文长度,单位是分组。q是在CPA攻击下,攻击者获得的密文数,现实意义是使用某个密钥加密的明文数量 密钥多次利用的运行方式(CTR) CTR:计数器模式 CTR不使用分组密码,使用PRF足够 信息完整性 本节先考虑信息完整性,不考虑机密性。信息完整可以保证公开信息没有被篡改,例如广告投放商不在乎广告的机密性,但在乎广告有没有被篡改 MACs CRC是循环冗余校验算法,为检测信息中的随机发生的错误而设计,并非针对恶意错误 安全的MACs MAC可以帮助抵御数据篡改,但是无法抵御认证消息的交换 构造安全的MAC 可以用安全的PRF来构造安全的MAC 但是,PRF的输出不能太短,不然攻击者能以一个不可忽略的概率猜出消息认证码 不等式右边 1/|Y| 是因为攻击者可以猜 PRF的输出不能太短,同时为了增大输入空间,需要有一些新的构造,将小输入空间的PRF转换成大输入空间的PRF 如果PRF是安全的,那么截断PRF的输出,依然是安全的,当然,如果要用PRF构造MAC,不能截到太短 CBC-MAC和NMAC n是底层PRF的分组大小 L是分组大小 这两种构造的最后一步都至关重要,没有最后一步,攻击者可以实施存在性伪造(扩展攻击)。 第二种构造的原因很简单,攻击者询问m的函数结果,将结果与w分别作为函数的密钥和明文(F公开),计算函数结果 第一种构造的原因 MAC padding 当数据的长度不是分组长度的倍数时,需要填充数据 全部补0会有出现存在性伪造 填充函数必须是一一映射的 ISO的这种填充方法,不管是不是分组长度的倍数都要进行填充 并行的MAC CBC-MAC和NMAC将一个处理短信息的PRF转换成一个处理长信息的PRF,这两种算法是串行的 P是某个有限域上的乘法 一次性MAC HMAC(略) 抗碰撞章节细说 抗碰撞 抗碰撞在信息完整性中扮演着重要角色。我们说MAC系统是安全的,如果它在选择信息攻击下,是不可被存在性伪造的。前面4种MAC构造是通过PRF或随机数来构造的,现在通过抗碰撞的哈希函数来构造MAC 抗碰撞:没有有效算法A,能以一个不可忽略的概率找到hash函数的碰撞值 可以用抗碰撞哈希函数和处理短信息的安全MAC组合成处理长信息的安全MAC 只用抗碰撞哈希函数也可以构造安全的MAC,而且不像之前的MAC需要密钥,但是需要一个只读空间用来存信息的hash值。这种方式非常流行 针对抗碰撞哈希函数的通用攻击(生日攻击) 针对分组密码的通用攻击是穷举攻击,抵御穷举攻击的方法是增大密钥空间;为了抵御生日攻击,哈希函数的输出也必须大于某个下界 下面这种攻击算法通常几轮就能找到碰撞值 生日悖论的证明 下面的证明在非均匀分布时,n的下界更低 通用攻击 使用Merkle-Damgard机制组建抗碰撞的哈希函数 下面的IV是永远固定的,写在代码和标准里的值,填充函数在信息长度是分组长度倍数的时候也会填充一个哑分组 这种机制流行的原因是只要小hash函数(即上面的压缩函数)是抗碰撞的,那么大hash函数也是抗碰撞的 构建抗碰撞的压缩函数 使用分组密码来构建压缩函数,将信息作为密钥。SHA函数都使用了Davies-Mayer压缩函数 另外一类压缩函数是由数论里的困难问题构建的,这类压缩函数的抗碰撞性规约于数论难题,也就是说破解了压缩函数的抗碰撞性也就破解了数论难题。但是这类压缩函数很少使用,因为分组密码更快 SHA256 使用了Merkle-Damgard机制和Davies-Mayer压缩函数,底层分组密码用的是SHACAL-2 HMAC 针对MAC验证时的计时攻击 认证加密 到目前为止,我们了解了机密性和信息完整性, 我们将构建同时满足这两种性质的密码 机密性: 在选择明文攻击下满足语义安全 机密性只能抵抗攻击者窃听而不能抵抗篡改数据包 客户端加密数据后通过网络发送给服务器, 服务器解密后分发至对应端口 如果没有保证完整性, 那么攻击者可以拦截客户端的数据包, 把端口改成自己能控制的服务器的端口 可以抵抗CPA攻击的CBC分组密码不能抵抗篡改数据包, 攻击者可以简单地修改IV从而修改加密后的端口 攻击者甚至不用进入服务器, 直接利用网络来攻击。在CTR模式下,攻击者截获数据包,将加密的校验和与t异或,将加密的数据与s异或。CTR模式的特点是对密文异或,解密后等于对明文异或。攻击者重复很多次攻击,直到得到足够数量的合法的t、s对,从而恢复数据D(插值法?) 这种攻击叫做选择密文攻击。攻击者提交他选择的密文,是由他想解密的密文所推出的,然后看服务器响应,攻击者可以从中学到明文的一些信息。重复这个操作,用许多不同t、s值,攻击者可以还原明文 选择明文攻击下的安全不能保证主动攻击(前面两种攻击)下的安全 如果要保证信息完整性但不需要机密性,使用MAC 如果同时保证信息完整性和机密性,使用认证加密模式 认证加密定义 目标:提供选择明文攻击下的语义安全和密文完整性 密文完整性:攻击者不能造出合法的密文 CBC是选择明文攻击(CPA)下的安全密码,它的解密算法从不输出bottom符号,所以它不能直接被用作认证加密 认证加密不能抵抗重放攻击 选择密文攻击下的安全 攻击者的能力是既能选择明文攻击也能选择密文攻击,也就是说既能拿到想要的明文的加密结果,也能拿到想要的密文的解密结果 攻击者选择的密文不能是CPA的返回 CBC密码不是选择密文安全的,因为在选择密文时,通过对IV异或修改CPA的返回,同时由于CBC的特性,CCA的返回时对明文的异或,这样攻击者可以以1的优势赢下语义安全实验 如果密码能够提供认证加密,那么它就是选择密文安全的 所以认证加密能够选择密文攻击,但是不能防止重放攻击和旁道攻击 认证加密直到2000年才被正式提出,在这之前,已经有CPA安全的密码和安全的MAC,当时的工程师想将两者组合,但不是所有的组合可以作为认证加密 SSH的MAC签名算法的输出会泄漏明文中的一些位; SSL的加密和MAC算法之间会有一些不好的互动导致选择密文攻击。IPsec无论怎么组合CPA安全的密码和安全的MAC都可以作为认证加密。SSL的特点是MAC-than-ENC,IPsec的特点是ENC-than-MAC 这几种认证加密都支持AEAD(认证加密与关联数据,例如ip报文的报文头是关联数据不加密,报文体用加密,整个报文需要认证)。MAC对整个报文使用,加密只对报文体使用 aad是需要认证、但不需要加密的相关数据,data是需要认证和加密的数据 直接从PRP构造认证加密 OCB比前面几种认证加密快的多,但没有被广泛使用因为各种各样的专利 认证加密的例子 针对认证加密的攻击 零碎 本章讲对称密码的一些零碎 密钥推导 KDF:key drivation function 源密钥由硬件随机数生成器生成或者密钥交换协议生成 CTX:参数上下文,每个进程的ctx不同 当源密钥服从均匀分布,我们使用PRF来作为密钥推导函数(其实就是用PRF作为伪随机数生成器) 当源密钥不服从均匀分布,那么伪随机函数的输出看起来就不随机了,源密钥不服从均匀分布的原因可能是密钥交换协议的密钥空间的子集是均匀分布,或者伪随机数生成器有偏差 构建KDF的机制 先提取再扩展 因为源密钥可能不是均匀的,我们使用一个提取器和一个随机选择的固定的但可以不保密的盐值将源密钥转换为服从均匀分布的密钥k,然后再使用PRF扩展密钥 HMAC既用于PRF进行扩展,又用于提取器 基于密码的KDF PBKDF通过多次迭代哈希函数推导密钥 确定性加密 确定性加密总是把给定明文映射到同一个密文。 为什么需要确定性加密?假设有个加密数据库和服务器,服务器用k1加密索引,用k2加密数据,如果加密是确定的,服务器请求数据时可以直接使用加密后的索引作为查询条件请求数据 确定性加密有致命缺点就是不能抵御选择明文攻击,攻击者看到相同的密文就知道他们的明文是相同的 解决方法是不要用同一个密钥加密同一个消息两次,要么密钥从一个很大空间随机选择,要么明文就是唯一的,比如说用户id 确定性加密的CPA安全 在标准的选择明文攻击实验基础上,Chal不会给相同的m0加密,不会给相同的m1加密。注意上面的图40不是标准的确定性加密的CPA实验,因为攻击者两次查询的m0都是同一个 一个常见的错误是,固定IV的CBC不是确定性CPA安全的,0n1n0^n1^n0n1n表示消息有两个分组,第一个分组全0,第二个分组全1 固定IV的CTR也是不安全的 可以抵御确定性CPA的确定性加密 确定性加密是需要的,但是不能抵御选择明文攻击,因为攻击者看到两个相同的密文就知道了对应的明文是一样的。我们对确定性加密降低选择明文攻击的能力,加密者不使用一个密钥多次加密同样的明文,这样叫确定性CPA。 构造1:合成的IV(SIV) CPA安全的密码会有一个随机值,我们用PRF生成这个随机值 SIV天然提供密文完整性,不需要MAC就能作为DAE(确定性认证加密),例如SIV-CTR 当需要确定性加密,特别是明文很长时,适合用SIV,如果明文很短,比如说少于16个字节,可以用构造2 构造2:仅仅使用一个PRP 实验0,攻击者看到q个随机值,实验1中,攻击者也看到q个随机值,两次实验的结果的概率分布是一样的,攻击者无法区分。这种构造不能保证密文完整性。同时只能加密16个字节 我们先考虑如何将PRP扩展成一个大的PRP EME有两个密钥K,L,L是由K推出的。先为每个分组用L推导出一个密码本作异或,然后用PRP加密得到PPP,将所有PPP异或得到MP,再用PRP加密MP得到MC。然后计算MP异或MC,得到另外一个密钥M用于推导更多密码本,分别对PPP异或得到CCC,然后把所有这些CCC异或得到CCCO,再用PRP加密再异或密码本 现在考虑增加完整性 微调加密(Tweakable encryption) 先以硬盘加密问题引入微调加密, 硬盘扇区大小是固定的,明文和密文空间必须一致,我们最多可以使用确定性加密,因为随机性加密需要额外空间来放随机数,完整性需要额外空间放认证码 定理:如果确定性CPA安全的密码的明文空间和密文空间一样,那么这个密码一定是个PRP 这个微调分组密码的安全实验与常规的分组密码安全实验区别在于,在常规分组密码中,攻击者只能与一个置换进行互动,目标是分辨自己在和伪随机置换交互还是在和一个真随机置换交互。而在微调分组密码的安全实验中,攻击者与|T|个随机置换交互,目标是区分这|T|个随机置换是真是伪 保格式加密(Format Preserving encryption) pos机刷卡时,我们希望卡号只在终端和银行可见,但是中间又有些服务商也想得到"卡号",我们可以用将卡号加密成卡号格式的密文。 我们截断使用PRF,明文后面补0(例如AES就补到128位),密文截断,然后带入Luby-Rackoff构造PRP 密钥交换 现在我们知道两个用户可以通过共享一个密钥来保护通信数据,问题是,这两个用户如何产生共享密钥,这个问题将把我们带入公钥密码的世界。我们先看一些玩具性质的密钥交换协议。 能否设计出可以抵御窃听和主动攻击的没有可信第三方的密钥交换协议?可以的,这就是公钥密钥的出发点 不需要TTP的密钥交换 首先考虑攻击者只能窃听不能篡改消息,能不能只使用对称密码体系的算法来实现不需要TTP的密钥交换? 可以的,首先给出puzzle定义:需要花一些功夫解决的问题。例如已经给出AES密钥的前96位,明文固定,那么枚举2322^{32}232个可能的后32位密钥可以找到能正确解密 Alice准备2322^{32}232个puzzle,全部发给Bob,Bob选择一个然后开始枚举,只要解密出的原文开头包含"Puzzle", 对应的k作为共享密钥,x发送给Alice,Alice就知道Bob选择了哪个 这个协议不实用但是有一个很好的想法,参与者花费线性的时间,而攻击者必须花费平方的时间,当攻击者想破解这个协议,有一个“平方鸿沟”横亘在参与者与攻击者的工作之间。 只用对称密码体系,我们不能建立一个更大的“鸿沟” 我们需要具备非常特殊性质的函数,为了构建这些函数,我们必须依赖某些代数 Diffie-Hellman协议 这是第一个实用的密钥交换机制。 同样,我们考虑攻击者只能窃听不能篡改。我们尝试建立参与者与攻击者之间的指数级鸿沟 Diffie-Hellman协议开创了密码学的新纪元,现在不仅仅是关于开发分组密码,而且是关于设计基于代数的协议 下面有张表是不同密钥长度的分组密码安全性等价于对应模数的DH函数安全性,如果用椭圆曲线,模数可以更小 上面的协议当存在主动攻击(中间人攻击)时就不安全 事实上,上面的协议也可以改成无交互的 一个开放的问题,两个参与方的密钥交换使用DH即可,三个参与方的密钥交换使用Joux提出的方法,四个及以上还没有有效方法 公钥加密 这是另外一种密钥交换的方法 在公钥加密中,没有必要赋予攻击者实施选择明文攻击的能力。因为在对称密钥系统中,攻击者必须请求他选择的明文的加密,而在公钥系统中,攻击者拥有公钥,所以他可以自己加密任何他想加密的明文,他不需要Chal的帮助来计算他选择的明文的加密。因此在公钥的设定中,选择明文攻击是与生俱来的,没有理由给攻击者多余的能力去实施选择明文攻击(公钥加密也是随机性的,每次密文不同) 可以抵抗窃听但也不能抵抗中间人攻击 数论简介 扩展欧几里得算法是已知最有效的求元素模逆的方法(也给了我们求模线性方程的方法) 注意这里是n,不是N,n=logN 费马小定理和欧拉定理 费马小定理给了我们另一个计算模质数逆的方法,但是与扩展欧几里得算法有两个不足,首先它只能用在质数模上,其次算法效率更低 我们可以用费马小定理以极大概率生成一个随机质数(期望是几百次迭代),这是一个简单但不是最好的方法 欧拉证明了Zp∗Z_p^*Zp∗​是一个循环群(p是素数),也就是∃g∈(Zp)∗\\exist g\\in (Z_p)^*∃g∈(Zp​)∗ 使得 $ {1,g,g2,g3,…,g{p-2}}=(Z_p)*$ 有限子群的阶必然整除有限群的阶 欧拉定理,费马小定理的直接推广,适用于合数 模高次方程 0也是二次剩余,所以有(p-1)/2+1 《信息安全数学基础》P146,证明当p是形如4k+3的素数时,解的形式如下。这里Dan讲了p不是这种形式的素数时仍然是有有效的随机算法来求解 当模数是合数时且指数大于1时,同余式的解并不好找 一些算法 分组用32位表示是为了乘法不溢出 指数运算非常慢 模运算的一些难题 质数模的难题 合数模的难题 Z(2)(n)Z_{(2)}(n)Z(2)​(n)表示两个位数相同的质数乘积的集合 基于陷门置换的公钥加密 公钥密码两个作用,一是会话建立(即对称密钥交换),二是非交互式应用 选择密文攻击(CCA)下的安全,有时可以缩写成选择密文攻击下的不可区分性(IND-CCA) 当攻击者可以篡改密文时,将以优势1赢下这个CCA游戏 构建CCA安全的公钥加密系统 陷门函数特点是单向的,只有私钥持有人才能做逆向计算 单向陷门函数只加密一个随机值,随机值用来生成对称密钥,单向陷门函数的私钥持有人可以恢复随机值进而恢复密钥 不能直接用单向陷门函数加解密明文,因为算法是确定的,就不可能是语义安全的,也会存在许多类型的攻击 构建一个陷门函数 本节构建一个经典的陷门函数叫做RSA 随机选取ZNZ_NZN​中的随机元素,这个元素很可能也在ZN∗Z_N^*ZN∗​中,即该元素很可能是可逆的 (虽然x大概率是可逆的,但是如果不可逆怎么办?) 单向陷门函数是安全的,对称密码可以提供认证加密,H是random oracle,即H是某个从ZNZ_NZN​映射到密钥空间的随机函数。那么这个公钥系统就可以抵抗选择密文攻击 不要直接用RSA来加密明文!!!因为RSA是确定性的函数,因此不可能是语义安全的 (直接用RSA加密密钥会被破解,但是ISO标准是用RSA加密密钥的哈希函数原项,破解出原项再hash一下就得到了密钥,这不是一样的吗?) 对称密钥的空间大小远远小于RSA的明文空间 PKCS1 ISO标准不是RSA在实际中的应用。实际使用是将对称密钥扩展然后用RSA加密 解决方法是,服务器解密后发现开头不是02,就认为明文只是个随机值而不是包含密钥的明文,继续协议就会发现密钥不一致从而结束会话(最常用的PKCS1) RSA的安全性 如果已经知道N的因式分解,那么可以用中国剩余定理求解x 计算e次根一定要因式分解吗?如果没有其它方法就说明了一个reduction(规约): 任何有效的计算模N的e次根的算法都是有效的因式分解算法 实际应用中的RSA 最小的公钥e是3,可以但推荐还是65537。 RSA-CRT(带中国剩余定理的RSA)。RSA的加密很快但是解密很慢 RSA数学上是正确的,但是如果没有较好实现,会出现各种旁道攻击 防火墙刚启动时种子数量少导致伪随机数生成器重复生成p,导致网络上许多设备的p相同 ElGamal 前一节讲了基于陷门置换函数的公钥加密系统,这节讲基于Diffle-Hellman协议的公钥加密系统 ElGamal如果不做预计算,加密会比解密慢,但是因为g是固定的,意味着加密可以做预计算,当内存足够时加密是比解密快的。但是内存不够,不能预计算时,RSA更快,因为只做一次指数运算 ElGamal安全性 这个计算Diffle-Hellman假设对于分析ElGamal系统的安全性并不理想 我们引入更强的哈希Diffle-Hellman假设,更强假设的意思是,攻击者的能力更强,但是我们提出的某个论断仍然成立 语义安全是不够的,我们真正想要的是选择密文安全。 为了证明选择密文安全,我们引入一个更强的假设叫做交互Diffle-Hellman假设 交互Diffle-Hellman假设是CCA安全的,现在问题是在CDH假设上能否实现CCA安全,没有random oracle能否实现CCA安全 有更好安全性分析的ElGamal变种 我们想在CDH假设上实现CCA安全,有两种办法,第一种是使用双线性群,这种群CDH和IDH是等价的;第二种是修改ElGamal系统 第二种方法有一个ElGamal的变种满足CDH假设上的CCA安全 如果没有random oracle,CCA安全还成立吗? ElGamal和RSA两种公钥系统共同遵循的原理 这里没有形式化给出单向函数的定义,因为要证明单向函数是否存在,也就是要证明P不等于NP(若P=NP,则公钥密码学将有根基危机) RSA有乘法性质和陷门,陷门意味着有私钥就可以逆向计算 总结:公钥加密依赖具有同态性质的单向函数和陷门 课程总结","categories":[{"name":"密码学","slug":"密码学","permalink":"https://zunpan.github.io/categories/%E5%AF%86%E7%A0%81%E5%AD%A6/"}],"tags":[{"name":"密码学","slug":"密码学","permalink":"https://zunpan.github.io/tags/%E5%AF%86%E7%A0%81%E5%AD%A6/"},{"name":"Cryptography","slug":"Cryptography","permalink":"https://zunpan.github.io/tags/Cryptography/"},{"name":"Dan Boneh","slug":"Dan-Boneh","permalink":"https://zunpan.github.io/tags/Dan-Boneh/"}]},{"title":"2022.05.23 某区块链公司面试记录","slug":"2022-5-23-某区块链公司面试记录","date":"2022-05-23T11:47:58.000Z","updated":"2023-09-24T04:27:40.271Z","comments":true,"path":"2022/05/23/2022-5-23-某区块链公司面试记录/","link":"","permalink":"https://zunpan.github.io/2022/05/23/2022-5-23-%E6%9F%90%E5%8C%BA%E5%9D%97%E9%93%BE%E5%85%AC%E5%8F%B8%E9%9D%A2%E8%AF%95%E8%AE%B0%E5%BD%95/","excerpt":"","text":"投的后端开发实习生岗,一天面了两次技术面 一面 针对简历项目提问,为什么要paillier?为什么要秘密分享? mysql的主从备份,主从节点的事务需不需要分开执行?从节点事务执行失败,主节点如何回滚?binlog的同步和事务在主从节点的执行,两者按时间顺序如何排列? 手写代码:求字典序第k大的数 二面 区块链共识算法有哪些?每个算法容错数量是多少?双花问题?默克尔树?默克尔证明? Java基本数据类型和引用类型的区别 为什么每个基本数据类型都有包装类,包装类有什么用? 针对简历提问,秘密分享时间复杂度多少?paillier用在哪里? 手写代码:多线程卖票,要求每个线程同时工作,不能超卖 两个大文件,文件内容是字符串集合,内存略大于一个文件,如何对两个文件进行字符串去重?如何优化时间复杂度?","categories":[{"name":"杂项","slug":"杂项","permalink":"https://zunpan.github.io/categories/%E6%9D%82%E9%A1%B9/"}],"tags":[{"name":"面试","slug":"面试","permalink":"https://zunpan.github.io/tags/%E9%9D%A2%E8%AF%95/"}]},{"title":"基于区块链的安全电子选举系统","slug":"基于区块链的安全电子选举系统","date":"2022-05-13T13:10:09.000Z","updated":"2023-09-24T04:27:40.282Z","comments":true,"path":"2022/05/13/基于区块链的安全电子选举系统/","link":"","permalink":"https://zunpan.github.io/2022/05/13/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/","excerpt":"","text":"背景:浙江大学软件学院实训课题,本组选择的课题是关于区块链和隐私计算的融合应用场景,具体方向是电子选举 需求:投票人不暴露身份隐私的情况下完成投票,计票机构在选票合法的情况下完成计票 技术方案总结 项目总体情况 核心功能性能测试 测试环境:x86_64,Intel® Xeon® Gold 6133 CPU @ 2.50GHz,Linux 5.4.0-96-generic 测试结果:大规模选举时延较高,适用于小规模选举活动,但因为选举本身的非实时性,也可用于更大规模 亮点 在隐匿链上选票来自谁的情况下选民只需要通过零知识证明证明自己的合法选民身份。 即使是对一个合法证明的微小改变,都无法通过链上以及链下的零知识证明验证,攻击者难以冒充合法选民。 缺陷 监管机构(后端)无法证明没有存储选民的隐私信息,例如该选民的选票号,私有盐。这里存在漏洞可以使得作恶的监管机构只需执行两次解密算法就可以获得选民的选票内容 零知识证明以匿名的形式证明了选民身份,选民通过秘密ID和监管方产生的PK生成证明并公布。秘密输入应当是选民秘密持有的,因此生成证明的过程最好是在本地做。 当前选民需要通过可信的后端API生成证明,无法便捷地通过WASM在本地生成(原计划下libsnark难以迁移,且曲线计算迁移到wasm慢几十倍),因此使用起来较为不便。 系统展示","categories":[{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"}],"tags":[{"name":"密码学","slug":"密码学","permalink":"https://zunpan.github.io/tags/%E5%AF%86%E7%A0%81%E5%AD%A6/"},{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"},{"name":"电子选举","slug":"电子选举","permalink":"https://zunpan.github.io/tags/%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE/"},{"name":"隐私计算","slug":"隐私计算","permalink":"https://zunpan.github.io/tags/%E9%9A%90%E7%A7%81%E8%AE%A1%E7%AE%97/"}]},{"title":"MIT-Missing-Semester学习笔记","slug":"MIT-Missing-Semester学习笔记","date":"2022-05-07T11:46:29.000Z","updated":"2023-09-24T04:27:40.278Z","comments":true,"path":"2022/05/07/MIT-Missing-Semester学习笔记/","link":"","permalink":"https://zunpan.github.io/2022/05/07/MIT-Missing-Semester%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","excerpt":"","text":"Course overview + the shell Shell Tools and Scripting TLDR TLDR可以代替man命令,查找命令说明书,TLDR可以给出详细的例子 find ripgrep 可以代替grep查找文件内容符合条件的文件 Ctrl+R 可以进入用子串查找历史命令的模式 xargs 命令,它可以使用标准输入中的内容作为参数。 例如 ls | xargs rm 会删除当前目录中的所有文件。 Vim 略 Data Wrangling 数据处理常用工具有grep 另外还有一些常用的数据处理工具 sed,文件(输入流)处理工具 sort,排序工具 awk,作用于文件的多功能编程语言 Command-line Enviroment Job Control(作业控制) 结束一个正在执行的程序可以按ctrl+c,实质是终端向程序发送了一个SIGINT的signal SIGINT可以在程序中捕获处理,因此ctrl+c也就是SIGINT信号有时候没用。 SIGQUIT信号可以结束程序并不会被捕获,按ctrl+\\ SIGSTOP信号可以暂停程序到后台,按ctrl+z。命令行输入jobs查看所有程序运行状态,使用fg %程序id 或 bg %程序id 将程序从暂停状态转到前端执行或后端执行 尽管程序可以在后端执行,但是一旦终端关闭,作为子进程的程序会收到一个SIGHUP信号导致被挂起,可以使用nohup 程序 &来使得程序在后端执行并忽略SIGHUP信号 Terminal Multiplexers(终端复用器) Aliases(别名) 直接在命令行定义别名,关闭终端后就没了,需要在.bashrc或者.zshrc这样的终端启动文件中做持久化 Dotfiles(点文件或者配置文件) 许多配置文件以’.‘开头,所以叫dotfile,默认情况下ls看不到dotfile 配置文件最好用版本控制统一管理,然后将原来的配置文件路径软链接到版本控制下的配置文件路径,github上有许多dotfile仓库 Remote Machines(远程机器) 使用ssh可以登录远程终端 foo是用户名,bar.mit.edu是域名,也可以直接是ip地址 也可以直接使用ssh执行命令,只要在上面的命令后面加上需要执行的命令即可 Version Control(Git) 略 Debugging and Profiling Debugging Printf debugging and Logging “The most effective debugging tool is still careful thought, coupled with judiciously placed print statements” — Brian Kernighan, Unix for Beginners. 在程序中使用logging而不用printf的好处在于 日志可以输出到文件、sockets、甚至是远程服务器而不一定是标准输出 日志支持几种输出级别(例如INFO,DEBUG,WARN,ERROR) 对于新出现的问题,日志有足够多的信息来排查 tips:输出可以按级别用不同颜色表示,例如ERROR用红色 echo -e "\\e[38;2;255;0;0mThis is red\\e[0m"会打印红色的“This is red”到终端上 Third Party logs 许多第三方程序会将日志写到系统的某一处,一般是/var/log. NGINX服务器会将日志放在/var/log/nginx下. 许多linux系统使用systemd, 一个系统守护进程来控制许多事情例如某些服务的开启和运行, systemd将日志放在/var/log/journal下, 可以使用journalctl查看 Debuggers 略 Specialized Tools Linux下可以使用strace查看程序系统调用情况 tcpdump和Wireshark是网络包分析器 web开发中,Chrome和Firefox的开发者工具十分方便 Profiling(分析) 学习分析和检测工具可以帮助理解程序中那一部分花费最多时间或资源以便优化这部分 Timing 三种不同的运行时间 真实时间:程序开始到结束的时间,包括阻塞时间、等待IO、网络 用户态时间:CPU执行程序中用户态代码的时间 系统态时间:CPU执行程序中核心态代码的时间 正确的程序执行时间=用户态时间+系统态时间 time命令可以测试程序的三种时间 Metaprogramming 这节主要讲系统构建工具make、持续集成等 略 Security and Cryptography Entropy(熵) 熵度量了不确定性并可以用于决定密码的强度 熵的单位是比特,对于一个均匀分布的离散随机变量,熵等于log2(所有可能的个数,即n)log_2(所有可能的个数,即n)log2​(所有可能的个数,即n) 扔一次硬币的熵是1比特。掷一次六面骰子的熵大约为2.58比特。一般我们认为攻击者了解密码的模型(最小长度,最大长度,可能包含的字符种类等),但是不了解某个密码是如何选择的 https://xkcd.com/936/ 例子里面,“correcthorsebatterystaple”这个密码比“Tr0ub4dor&3”更安全,因为前者熵更大,大约40比特的熵足以对抗在线穷举攻击(受限于网络速度和应用认证机制);而对于离线穷举攻击(主要受限于计算速度),一般需要更强的密码(比如80比特) Potpourri 略","categories":[{"name":"Linux","slug":"Linux","permalink":"https://zunpan.github.io/categories/Linux/"}],"tags":[{"name":"Linux","slug":"Linux","permalink":"https://zunpan.github.io/tags/Linux/"},{"name":"Shell","slug":"Shell","permalink":"https://zunpan.github.io/tags/Shell/"}]},{"title":"Maven问题记录","slug":"Maven问题记录","date":"2022-05-07T11:01:06.000Z","updated":"2023-09-24T04:27:40.279Z","comments":true,"path":"2022/05/07/Maven问题记录/","link":"","permalink":"https://zunpan.github.io/2022/05/07/Maven%E9%97%AE%E9%A2%98%E8%AE%B0%E5%BD%95/","excerpt":"","text":"多模块项目依赖错误 多模块项目直接对根模块进行install,Maven会根据依赖自动判断install顺序。install完成后需要点击Maven界面的Reload All Maven Projects 父模块的pom文件一定得写<packaging>pom</packaging> 因为默认打包方式是jar,<xs:element name="packaging" minOccurs="0" type="xs:string" default="jar"> 有些公司内部仓库的包只有pom文件没有jar,这种是下载不下来的 只有jar包下载到了本地,maven才能找到依赖,所以这就是为什么多模块项目,被依赖的模块要先install到本地,这样依赖它的模块才能找到这个模块","categories":[{"name":"Maven","slug":"Maven","permalink":"https://zunpan.github.io/categories/Maven/"}],"tags":[{"name":"Maven","slug":"Maven","permalink":"https://zunpan.github.io/tags/Maven/"}]},{"title":"Maven学习笔记","slug":"Maven学习笔记","date":"2022-05-07T10:47:28.000Z","updated":"2023-09-24T04:27:40.279Z","comments":true,"path":"2022/05/07/Maven学习笔记/","link":"","permalink":"https://zunpan.github.io/2022/05/07/Maven%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","excerpt":"","text":"学习资料:Maven官方文档 最常用的两种打包方法 clean,package(如果报错,很可能就是jar依赖的问题) clean,install Maven生命周期 Maven有三种内置的构建生命周期:default,clean,site。default生命周期处理项目部署,clean生命周期处理项目清理,site生命周期处理项目web站点的建立。每个生命周期包含一系列阶段 default生命周期阶段(非完整) validate:验证,验证工程是否正确,所需的信息是否完整。 compile:编译源码,编译生成class文件,编译命令,只编译选定的目标,不管之前是否已经编译过,会在你的项目路径下生成一个target目录,在该目录中包含一个classes文件夹,里面全是生成的class文件及resources下的文件。 test:单元测试 package:打包,将工程文件打包为指定的格式,例如JAR,WAR等。这个命令会在你的项目路径下一个target目录,并且拥有compile命令的功能进行编译,同时会在target目录下生成项目的jar/war文件。如果a项目依赖于b项目,打包b项目时,只会打包到b项目下target下,编译a项目时就会报错,因为找不到所依赖的b项目,说明a项目在本地仓库是没有找到它所依赖的b项目,这时就用到install命令了 integration-test:将jar包部署到环境中进行单元测试 verify:核实,检查package是否有效、符合质量标准。 install:将包安装至本地仓库,以作为其它项目的dependency。该命令包含了package命令功能,不但会在项目路径下生成class文件和jar包,同时会在你的本地maven仓库生成jar文件,供其他项目使用(如果没有设置过maven本地仓库,一般在用户/.m2目录下。如果a项目依赖于b项目,那么install b项目时,会在本地仓库同时生成pom文件和jar文件,解决了上面打包package出错的问题)。 deploy:复制到远程仓库。 clean生命周期阶段(非完整) clean:清理,在进行真正的构建之前进行一些清理工作,移除所有上一次构建生成的文件。执行该命令会删除项目路径下的target文件,但是不会删除本地的maven仓库已经安装的jar文件。 site生命周期阶段(非完整) site:站点,生成项目的站点文档 Phases are actually mapped to underlying goals. The specific goals executed per phase is dependant upon the packaging type of the project. For example, package executes jar:jar if the project type is a JAR, and war:war if the project type is - you guessed it - a WAR. An interesting thing to note is that phases and goals may be executed in sequence. mvn clean dependency:copy-dependencies package 这条命令执行了mvn的clean阶段,dependency插件的copy-dependencies目标,package阶段(package处于default生命周期,Maven会先顺序执行validate、compile、test) 阶段由插件目标构成 一个插件目标代表一个具体的任务(比阶段更细),它可以被绑定到0个或多个阶段。没有绑定到阶段也能直接执行,目标和阶段执行顺序取决于调用的顺序 For example, consider the command below. The clean and package arguments are build phases, while the dependency:copy-dependencies is a goal (of a plugin). mvn clean dependency:copy-dependencies package 如果一个目标被绑定到一个或多个阶段,目标将在阶段中被调用。如果阶段没有绑定任何目标,那阶段就不会执行。如果阶段绑定一个或多个目标,执行这个阶段会执行所有目标,目标执行的顺序在POM中定义 build和compile的区别 点击Build Project,idea 使用自己的构建工具进行编译(编译器默认使用Javac),相当于maven的compile,点击Build Artifacts,相当于maven的Package 插件和goal a plugin is a collection of goals with a general common purpose. For example the jboss-maven-plugin, whose purpose is “deal with various jboss items”. 总结 mvn clean package 依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)等7个阶段。 mvn clean install 依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)、install等8个阶段。 mvn clean deploy 依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)、install、deploy等9个阶段。 注意:maven默认的生命周期中,不管是package还是install,都不会把第三方依赖打包到一个jar包中,需要自己配置插件,比如maven-assembly-plugin。若是springboot项目,有spring-boot-maven-plugin插件,执行mvn package后,先走一遍maven自身生命周期到package,然后springboot打包插件里面提供了一个repackage的goal,这样mvn package在插件的指导下先完成标准的打包流程再完成插件定义的流程","categories":[{"name":"Maven","slug":"Maven","permalink":"https://zunpan.github.io/categories/Maven/"}],"tags":[{"name":"Maven","slug":"Maven","permalink":"https://zunpan.github.io/tags/Maven/"}]},{"title":"LeetCode刷题记录(cpp)","slug":"LeetCode刷题记录-cpp","date":"2022-05-07T09:00:09.000Z","updated":"2023-09-24T04:27:40.278Z","comments":true,"path":"2022/05/07/LeetCode刷题记录-cpp/","link":"","permalink":"https://zunpan.github.io/2022/05/07/LeetCode%E5%88%B7%E9%A2%98%E8%AE%B0%E5%BD%95-cpp/","excerpt":"","text":"stringstream用法 需要include<sstream>,任何输入输出都会被转换成字符串 istringstream类用于执行C风格的串流的输入操作 ostringstream类用于执行C风格的串流的输出操作 stringstream类同时支持C风格的串流的输入输出操作 #include <iostream> #include <vector> #include <string> #include <sstream> using namespace std; // 例子1: 基本用法 int main() { string s; stringstream ss; int n = 11, m = 88; // 将11放入流中 ss << n; // 从流中提取数据到s中,自动类型转换,无需关心类型问题 ss >> s; // 11 cout << s << endl; s += "23"; // 1123 cout << s << endl; // 清空,以便下一次使用 ss.clear(); ss.str(""); ss << s; ss >> n; // 1123 cout << n << endl; return 0; } // 例2:按空格分隔字符串 int main(int argc, char const *argv[]) { stringstream ss("1 2 3"); string s; while (ss >> s) { cout << s << endl; } return 0; } // 例3:按分隔符分隔字符串 const vector<string> split(const string &str, const char &delimiter) { vector<string> result; stringstream ss(str); // 等于ss.str(str); string tok; while (getline(ss, tok, delimiter)) // 从ss流中按delimiter为分隔符读取数据存储到tok中,直到流结束 { result.push_back(tok); } return result; } 默认值 string类型的默认值是"" int类型的默认值是0 substr函数用法 string s("123"); string a = s.substr(0,5);// 获得字符串s中从第0位开始的长度为5的字符串,若 pos+n>s.length(),只会截取到末尾 string.length()或者size()与变量作比较的bug string.length()是unsigned型的不能与负数作比较,否则在机器数的表示上俩者都会以unsigned的编码形式比较,那就大概率结果就不对了,所以建议平时编程养成好习惯,类型不一样的数据比较或运算时一定要留一个心眼,不能直接拿来就比。用的时候可以在前面写上强转 链表题技巧 多用定义变量,不容易被逻辑绕晕,比如修改指针前先用first、second、third先保存下来 head有可能发生改动时,先增加一个假head,返回的时候返回head->next,避免修改头结点导致多出一堆逻辑 二叉树题技巧 先考虑递归(前中后序遍历)还是迭代(层序遍历) 递归要先考虑函数是否有返回值,比如说判断是否是平衡二叉树,当前递归函数中要根据左右子树的返回值做判断,所以递归函数要返回值 然后考虑是前,还是中,还是后。如果要先处理左右子树那就是后,特别地,回溯也是后,参考https://programmercarl.com/0236.二叉树的最近公共祖先.html 最后再考虑递归终止条件,一般是root==nullptr就返回,也有特殊情况,比如叶子节点就返回 层序遍历一般用队列。如果要一层一层的访问,在出队的时候要先记录当前层的节点数,根据节点数出队 数字和字符串互转 数字转字符串:to_string(number) 字符串转数字:stoi(intStr) , stol(longStr), stof(floatStr), stod(doubleStr) 数组初始化 初始化一个n*n的二维数组,用0填充 vector<vector<int>>v(n,vector<int>(n,0)); 或者 v.resize(n); for(int k=0;k<n;k++){ v[k].resize(n); } 或者用memset int v[n][n]; memset(v,0,sizeof(int)*n*n); DFS和BFS适用条件 dfs适合找解的存在和所有解,bfs适合找最优解。例如dfs可以解决可达性,bfs可以解决无权图最短路径(有权使用dijkstra)。dfs需要回溯时要注意,以N皇后和解数独为例,如果当前位置的合法性依赖之前放置的位置,那么回溯时要将位置清除(N皇后),如果当前位置的合法性不依赖之前的位置,而是依赖别的辅助空间,那么回溯时只需要清除辅助空间,放置的位置可以不清除(解数独) 图的连通性 考虑dfs和并查集 自定义排序 // sort函数第三个参数是lambda表达式,[x] 表示捕获外部的变量x sort(arr.begin(),arr.end(),[x](const int& a,const int& b){ return abs(a-x)==abs(b-x)?a<b:abs(a-x)<abs(b-x); }); 二分技巧 标准二分查找 int search(vector<int>& nums, int target) { int left = 0; int right = nums.size() - 1; while (left <= right) { int mid = left + ((right - left) >> 1); if (nums[mid] == target) return mid; else if (nums[mid] > target) { right = mid - 1; } else { left = mid + 1; } } return -1; } 二分查找第一个满足XXX的位置 int left = 0, right = nums.size()- 1, ans = -1; // <= 可以判断区间长度为1时,即left==right时是否有解;若二分条件必须在区间大于等于2的情况下判断,则去掉=,参考leetcode162 while (left <= right) { int middle = left + (right - left) / 2; if (满足XXX) { ans = middle; right = middle - 1; } else { left = middle + 1; } } return ans; 二分查找最后一个满足XXX的位置 int left = 0, right = nums.size()- 1, ans = -1; // <= 可以判断区间长度为1时,即left==right时是否有解;若二分条件必须在区间大于等于2的情况下判断,则去掉=,参考leetcode162 while (left <= right) { int middle = left + (right - left) / 2; if (满足XXX) { ans = middle; left = middle + 1; } else { right = middle - 1; } } return ans; STL的二分查找 lower_bound(begin,end,num): 从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的迭代器,不存在则返回end。通过返回的迭代器减去begin可以得到数字的下标。 upper_bound(begin,end,num): 从数组的begin位置到end-1位置二分查找第一个大于num 数字,找到返回该数字的迭代器,不存在则返回end。通过返回的迭代器减去begin可以得到数字的下标。 // 用在vector上 vector<int> nums1{1,10,4,4,2,7}; sort(nums1.begin(),nums1.end()); int pos1 = lower_bound(nums1.begin(),nums1.end(),9)-nums1.begin(); // 用在数组上 int nums2[6] = {1, 10, 4, 4, 2, 7}; sort(nums2,nums2+6); int pos2 = lower_bound(nums2,nums2+6,9)-nums2; // 返回vector中最靠近5的第一个小于或等于5的数 vector<int> nums1{1,10,4,4,2,7}; sort(nums1.rbegin(),nums1.rend()); int pos3 = lower_bound(nums1.begin(), nums1.end(), 5,greater<int>())-nums1.begin(); 优先队列 // 升序序列 priority_queue<int, vector<int>, greater<int>>q; // 降序序列(默认,等同于priority_queue<int>) priority_queue<int, vector<int>, less<int>>q; // 自定义类型排序 struct student { student(int age){ this->age = age; } int age; }; struct cmp{ bool operator() (student& s1,student& s2){ return s1.age<s2.age; } }; int main(int argc, char const *argv[]) { vector<int> ages{3,4,1,2}; priority_queue<student,vector<student>,cmp>a; for(int age:ages){ student s(age); q.push(s); } while(q.size()>0){ student head = q.top();q.pop(); cout<<head.age<<endl; } } 位运算符技巧 按位与运算符(&) 参加运算的两个数据,按二进制位进行“与”运算 运算规则:0&0=0;0&1=0;1&0=0;1&1=1; 即:两位同时为1,结果才为1,否则为0 例如:3&5 即 0000 0011 & 0000 0101 = 0000 0001 因此,3&5=1 ”与运算“的特殊用途 清零。如果想将一个单元清零,即全部二进制位归0,只要与一个各位都为0的数值相与,结果为0 取一个数的指定二进制位。如果想取某个数的后四位,那么可以计算 该数&0000 1111 按位或运算符(|) 参加运算的两个数据,按二进制位进行“或”运算 运算规则:0|0=0;0|1=1;1|0=1;1|1=1; 即:参加运算的两个数据只要有一个位1,其值为1 例如:3|5 即 0000 0011 | 0000 0101 = 0000 0111, 因此,3|5的值为7 “或运算”特殊用途 常用来对一个数的某些位置置为1。如果想对某个数的后四位置为1,那么可以计算 该数 | 0000 1111 异或运算符(^) 参加运算的两个数据,按二进制位进行“异或”运算 运算规则:0 ^ 0=0;0 ^ 1=1; 1 ^ 0=1; 1 ^ 1=0; 即:参加运算的两个数据,如果两个相应位值不同,则该位位1,否则为0(模2和) “异或运算”特殊用途 翻转一个数的指定位。如果将对某个数的后四位翻转, 那么可以计算 该数 ^ 0000 1111 与0异或,保留原值 取反运算符(~) 参加运算的一个数据,按二进制位进行“取反”运算 运算规则:~1 = 0;~0 = 1; 即:对一个二进制数按位取反。 “取反运算”特殊用途 使一个数的最低位为0,可以计算 该数 & ~1 左移运算符(<<) 将一个数据的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0) 例如:a = a << 2 将a的二进制位左移2位,右补0。若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2 右移运算符(>>) 将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 例如: a = a >> 2 将a 二进制位右移2位,操作数每右移一位,相当于该数除以2 >> 运算符把 expression1 的所有位向右移 expression2 指定的位数。expression1 的符号位被用来填充右移后左边空出来的位。向右移出的位被丢弃。 例如-14(1111 0010)右移两位等于-4(11111100) 无符号右移运算符(>>>) >>> 运算符把 expression1 的各个位向右移 expression2 指定的位数。右移后左边空出的位用零来填充。移出右边的位被丢弃。 例如 -14 (11111111 11111111 11111111 11110010),向右移两位后等于1073741820(00111111 11111111 11111111 11111100) 不同长度的数据进行位运算 如果两个不同长度的数据进行位运算时,系统会将二者按右端对齐,然后进行位运算 以“与”运算为例说明如下:我们知道在C语言中long型占4个字节,int型占2个字节,如果一个long型数据与一个int型数据进行“与”运算,右端对齐后,左边不足的位依下面三种情况补足 如果整形数据为正数,左边补16个0 如果整形数据为负数,左边补16个1 如果整形数据为无符号数,左边也补16个0 数组题技巧 考虑双指针,双向前缀和,单调队列 快排 void myquickSort(vector<int>& nums,int low,int high){ // mid不能是下标 int mid = nums[(low+high)/2]; int i = low,j = high; do{ while(nums[i]<mid) i++; while(nums[j]>mid) j--; if(i<=j) swap(nums[i++],nums[j--]); }while(i<=j); if(low<j) myquickSort(nums,low,j); if(i<high) myquickSort(nums,i,high); } 背包问题 常见的背包问题有三种 组合排列问题 组合总和 Ⅳ 目标和 零钱兑换 II True、False问题 单词拆分 分割等和子集 最大最小问题 一盒零 零钱兑换 组合排列问题公式 dp[i] += dp[i-num] True、False问题公式 dp[i] = dp[i] or dp[i-num] 最大最小问题公式 dp[i] = min(dp[i],dp[i-num]+1)或者dp[i] = max(dp[i],dp[i-num]+1) 以上三组公式是解决对应问题的核心公式 背包问题分析步骤 分析是否为背包问题 是以上三种背包问题中的哪一种 是0-1背包问题还是完全背包问题。也就是题目给的nums数组中的元素是否可以重复使用 如果是组合排列问题,组合问题外循环物品,内循环背包;排列相反 背包问题特征 背包问题具有的特征:给定一个target,target可以是数字也可以是字符串,再给定一个数组nums,nums中装的可能是数组,也可能是字符串,问:能否使用nums中的元素做各种排列组合得到target 背包问题遍历 如果是0-1背包,即数组中的元素不可重复使用,nums放在外循环,target在内循环,且内循环倒序 for(int i=0;i<nums.size();i++){ for(int j=target;j>=nums[i];j--){ // dp公式 } } 如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环,且内循环正序 for(int i=0;i<nums.size();i++){ for(int j=nums[i];j<=target;j++){ // dp公式 } } 如果是排列问题,即需要考虑元素之间的顺序,target放在外循环,nums放在内循环 for(int i=0;i<=target;i++){ for(int j=0;j<nums.size();j++){ if(i>nums[j]){ // dp公式 } } } 单调栈 通常是一维数组,要寻找任一元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了","categories":[{"name":"算法","slug":"算法","permalink":"https://zunpan.github.io/categories/%E7%AE%97%E6%B3%95/"}],"tags":[{"name":"LeetCode","slug":"LeetCode","permalink":"https://zunpan.github.io/tags/LeetCode/"},{"name":"cpp","slug":"cpp","permalink":"https://zunpan.github.io/tags/cpp/"},{"name":"algorithm","slug":"algorithm","permalink":"https://zunpan.github.io/tags/algorithm/"}]},{"title":"Git学习笔记","slug":"Git学习笔记","date":"2022-05-06T13:47:10.000Z","updated":"2023-09-24T04:27:40.277Z","comments":true,"path":"2022/05/06/Git学习笔记/","link":"","permalink":"https://zunpan.github.io/2022/05/06/Git%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","excerpt":"","text":"新建git项目 在一个目录下执行 git init 克隆git项目 在一个目录下执行 git clone [ssh/https]address 管理修改 将文件修改添加到暂存区 git add filename 将暂存区的修改提交到本地仓库 git commit -m 'commitMessage' [filename] 每次修改完要先add到暂存区,然后commit才能把你修改的东西提交的本地仓库 提交信息规范 commit message格式 <type>(<scope>): <subject> type(必须) 用于说明git commit的类别,只允许使用下面的标识。 feat:新功能(feature)。 fix/to:修复bug,可以是QA发现的BUG,也可以是研发自己发现的BUG。 fix:产生diff并自动修复此问题。适合于一次提交直接修复问题 to:只产生diff不自动修复此问题。适合于多次提交。最终修复问题提交时使用fix docs:文档(documentation)。 style:格式(不影响代码运行的变动)。 refactor:重构(即不是新增功能,也不是修改bug的代码变动)。 perf:优化相关,比如提升性能、体验。 test:增加测试。 chore:构建过程或辅助工具的变动。 revert:回滚到上一个版本。 merge:代码合并。 sync:同步主线或分支的Bug。 scope(可选) scope用于说明 commit 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。 例如在Angular,可以是location,browser,compile,compile,rootScope, ngHref,ngClick,ngView等。如果你的修改影响了不止一个scope,你可以使用*代替。 subject(必须) subject是commit目的的简短描述,不超过50个字符。 建议使用中文(感觉中国人用中文描述问题能更清楚一些)。 结尾不加句号或其他标点符号。 根据以上规范git commit message将是如下的格式: fix(DAO):用户查询缺少username属性 feat(Controller):用户查询接口开发 查看工作区和暂存区的不同 git diff [filename] 查看暂存区和版本库的不同 git diff --cached [filename] 删除修改 在文件已经加入版本库的情况下 如果文件修改后没有add进暂存区,我们可以用 git checkout -- filename 取消工作区的修改,恢复到和版本库一摸一样的状态 如果文件修改后已经add进暂存区了,我们可以用 git reset HEAD filename 取消暂存区的修改,恢复到工作区有修改的状态,这是查看git status 会看到 要撤销工作区的修改就用上面的checkout 如果文件修改后已经commit到了本地仓库,那么我们只能使用版本回退了 git reset --hard [commitID] commitID可以通过git log 或者 git reflog查看。git log 查看当前版本的所有commit,git reflog查看所有commit,所以回退到了老版本后想回到新版本后得先git reflog 查看新版本的commitID 在文件没有加入版本库的时候 这种情况一般是新生成的文件,工作区可以任意编辑,因为版本库中没有,所以不需要checkout(checkout在没有add的时候恢复至版本库状态) 如果add进了暂存区,此时和上面一样,可以使用 git resset HEAD filename 恢复至工作区有修改的状态 删除文件 如果文件已经处于tracked状态(已经被add了),当你要删除文件的时候,可以采用命令: rm filename 这个时候(也就是说这个时候只执行了rm test.txt)有两种情况 第一种情况:的确要把test.txt删掉,那么可以执行 git rm test.txt 或者 git add test.txt git commit -m "remove test.txt" 然后文件就被删掉了,这种情况想恢复只能用版本回退了,但是之前的版本也没有提交这个文件,那这个就永远消失了 第二种情况:删错文件了,不应该删test.txt,注意这时只执行了rm test.txt,还没有提交,所以可以执行 git checkout test.txt 将文件恢复。 并不是说执行完git commit -m "remove test.txt"后还能用checkout恢复,commit之后版本库里的文件也没了,自然没办法用checkout恢复,而是要用其他的办法(版本回退) 远程仓库 检测是否能ssh登录远程仓库 ssh -T git@github.com 添加远程仓库 git remote add origin git@github.com:username/reponame.git 查看远程仓库 git remote 抓取/合并远程仓库 git fetch origin git merge origin/master 推送到远程仓库 git push -u origin master -u 表示把本地的master分支和远程仓库origin的master分支关联 后面就可以用 git push origin master 删除远程仓库关联 git remote rm origin 分支管理 新建并切换分支 git checkout -b dev 和下面作用一样 git branch dev git checkout dev 查看分支 *表示当前所在分支 分支操作 和上面的文件修改一个意思,只不过上面默认是在master分支操作 合并 在dev分支下执行文件修改,add、commit操作不会影响到master分支,dev开发完成之后可以切换到master分支,执行合并操作 git merge dev 删除分支 git branch -d dev 注意:在任何一条分支上做的修改必须要commit到本地仓库在能算作在dev分支上的一个版本,否则切换到别的分支,别的分支仍然能看到修改没有被添加到暂存区或者没有提交到本地仓库,别的分支可以继续处理这个修改,有一个办法是git stash保留当前分支上的工作内容,这样切到别的分支上git status就没有要处理的修改了 解决冲突 Git用<<<<<<<,=======,>>>>>>>标记出不同分支的内容 <<<<和=====之间是HEAD(当前分支)的内容,=====和>>>>>之间是feature1分支的内容 下图是手动修改后的冲突文件 查看分支合并情况 git log --graph bug分支 保留现场 git stash 然后切到有bug的分支上,新建一个分支,修复好后merge到出现bug的分支上,然后查看保存的工作现场 git stash list 恢复现场有两种办法: git stash pop pop会同时恢复现场和删除保存的工作现场 也可以指定恢复现场 git stash apply stash@{0} 然后删除分支 git stash drop stash@{0} 复制提交 比如上面bug分支提交了一个修改之后merge到了master分支,我们正在开发的dev分支也有这个bug,我们可以把bug分支提交的修改复制到dev分支 git cherry-pick commitID commitID通过git log查看 推送分支 git push origin branchName 多人协作 查看远程库信息,使用git remote -v; 本地新建的分支如果不推送到远程,对其他人就是不可见的; 从本地推送分支,使用git push origin branch-name,如果推送失败,先用git pull抓取远程的新提交; 在本地创建和远程分支对应的分支,使用git checkout -b branch-name origin/branch-name,本地和远程分支的名称最好一致; 建立本地分支和远程分支的关联,使用git branch --set-upstream branch-name origin/branch-name; 从远程抓取分支,使用git pull,如果有冲突,要先处理冲突。 代理 设置代理 git config --global http.proxy socks5://127.0.0.1:7890 git config --global https.proxy socks5://127.0.0.1:7890 后面的端口是你的梯子监听的端口(clash默认7890) 如果是在wsl2中使用,请看WSL2.md中的代理配置 https代理存在一个局限,那就是没有办法做身份验证,每次拉取私库或者推送代码时,都需要输入github的账号和密码,非常痛苦。 设置ssh代理前,请确保你已经设置ssh key。可以参考在 github 上添加 SSH key 完成设置更进一步是设置ssh代理。只需要配置一个config就可以了。 # Linux、MacOS vi ~/.ssh/config # Windows 到C:\\Users\\your_user_name\\.ssh目录下,新建一个config文件(无后缀名) 将下面内容加到config文件中即可 对于windows用户,代理会用到connect.exe,你如果安装了Git都会自带connect.exe,如我的路径为C:\\APP\\Git\\mingw64\\bin\\connect #Windows用户,注意替换你的端口号和connect.exe的路径 ProxyCommand "C:\\APP\\Git\\mingw64\\bin\\connect" -S 127.0.0.1:51837 -a none %h %p #MacOS用户用下方这条命令,注意替换你的端口号 #ProxyCommand nc -v -x 127.0.0.1:51837 %h %p Host github.com User git Port 22 Hostname github.com # 注意修改路径为你的路径 IdentityFile "C:\\Users\\Your_User_Name\\.ssh\\id_rsa" TCPKeepAlive yes Host ssh.github.com User git Port 443 Hostname ssh.github.com # 注意修改路径为你的路径 IdentityFile "C:\\Users\\Your_User_Name\\.ssh\\id_rsa" TCPKeepAlive yes 保存后文件后测试方法如下,返回successful之类的就成功了 # 测试是否设置成功 ssh -T git@github.com 取消代理 git config --global --unset http.proxy git config --global --unset https.proxy github搜索命令 仓库搜索 in:name example 名字中有“example” in:readme example readme中有“example” in:description example 描述中有“example” stars:>1000 star>1000 forks:>1000 fork>1000 pushed:>2019-09-01 2019年9月1日后有更新的 language:java 用Java编写的项目 https://docs.github.com/en/search-github/searching-on-github/searching-for-repositories issue搜索 java in:title,body,comments 搜索标题中内容中评论中包含java的issue https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests 零碎 已经提交的文件取消追踪 先修改.gitignore git rm -r --cached . git add . git commit -m 'update .gitignore'","categories":[{"name":"Git","slug":"Git","permalink":"https://zunpan.github.io/categories/Git/"}],"tags":[{"name":"Git","slug":"Git","permalink":"https://zunpan.github.io/tags/Git/"}]},{"title":"truffle部署合约到远程私有链","slug":"truffle部署合约到远程私有链","date":"2020-02-10T13:04:14.000Z","updated":"2023-09-24T04:27:40.281Z","comments":true,"path":"2020/02/10/truffle部署合约到远程私有链/","link":"","permalink":"https://zunpan.github.io/2020/02/10/truffle%E9%83%A8%E7%BD%B2%E5%90%88%E7%BA%A6%E5%88%B0%E8%BF%9C%E7%A8%8B%E7%A7%81%E6%9C%89%E9%93%BE/","excerpt":"","text":"首先安装了truffle和truffle-hdwallet-provider npm install truffle -g npm install truffle-hdwallet-provider 安装truffle卡住的同学请在truffle后面加上@5.0.1指定版本 修改truffle配置文件 var HDWalletProvider = require("truffle-hdwallet-provider"); // 导入模块 var mnemonic = "你的助记词"; //MetaMask的助记词。 module.exports = { networks: { development: { network_id: "*", // Any network (default: none) provider: function () { // mnemonic表示MetaMask的助记词。 "ropsten.infura.io/v3/33..."表示Infura上的项目id return new HDWalletProvider(mnemonic, "http://ip:port", 0); // 1表示第二个账户(从0开始) } }, }, // Set default mocha options here, use special reporters etc. mocha: { // timeout: 100000 }, // Configure your compilers compilers: { solc: { // version: "0.5.0", // Fetch exact version from solc-bin (default: truffle's version) // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) // settings: { // See the solidity docs for advice about optimization and evmVersion // optimizer: { // enabled: false, // runs: 200 // }, // evmVersion: "byzantium" // } } } } 上面的 new HDWalletProvider 让我非常迷,我在metamask导入了一个有余额的账户,按理说是1,但是指定1部署的时候用的不是这个账户,所以我就给0账户转了点钱,依旧指定0账户 最后在终端执行部署命令 truffle migrate 注意,不要忘记把挖矿开起来,不然合约不会部署上去","categories":[{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"}],"tags":[{"name":"MetaMask","slug":"MetaMask","permalink":"https://zunpan.github.io/tags/MetaMask/"},{"name":"truffle","slug":"truffle","permalink":"https://zunpan.github.io/tags/truffle/"}]},{"title":"MetaMask连接私有链发生的转账问题","slug":"MetaMask连接私有链发生的转账问题","date":"2020-01-18T05:26:35.000Z","updated":"2023-09-24T04:27:40.279Z","comments":true,"path":"2020/01/18/MetaMask连接私有链发生的转账问题/","link":"","permalink":"https://zunpan.github.io/2020/01/18/MetaMask%E8%BF%9E%E6%8E%A5%E7%A7%81%E6%9C%89%E9%93%BE%E5%8F%91%E7%94%9F%E7%9A%84%E8%BD%AC%E8%B4%A6%E9%97%AE%E9%A2%98/","excerpt":"","text":"我用ganache-cli启了一个以太坊网络,然后我在MetaMask连接到了这个网络,并且导入了一个账户,正常显示余额是100ETH,但是转账的时候发生了错误。 EthQuery - RPC Error - Error: [ethjs-rpc] rpc error with payload {"id":3715053778334,"jsonrpc":"2.0","params":["0xf8720485012410110082520894c8fb523ca95721bf3408f6e6ef0ed8e7c3f65488880de0b6b3a7640000808602df64fab56ea052b79e6fe25a6bd073f48d853c053d35b0d4de1e3f1e77e7e82c520bd24d9783a059bf550c3fa7fec8a93584f77b0998545b936e7e583bb53076fb2eac9664725d"],"method":"eth_sendRawTransaction"} [object Object] 错误原因是ganache的chainId和MetaMask的chainId不同 解决方法: ganache-cli -i 1 -h 0.0.0.0 -p 7545 -i 指定启动的链id,我这里指定1 -h 指定监听所有ip,因为我把ganache装在了云服务器上面,且没有注册域名,没法用nginx做代理,所以只能监听所有ip,这样我才能在本机访问到 -p 指定端口,默认是8545,但是truffle部署合约时是用的7545,所以懒得改truffle的代码就可以指定ganache在7545启动 随后在metamask中添加一个自定义网络并填上相应信息,metamask建议去github上下压缩包自己添加到扩展程序中,我在chrome商店下了N回都是出错,metamask添加网络步骤如下 网络名称随意,url如果本机就填 http://localhost:7545,如果是服务器填上对应ip,关键是ChainID一定要和ganache-cli 启动时一致,最后保存即可 注意:如果你的MetaMask已经连到了你本来没有指定chainId的网络了,那你再指定chainId启动网络,MetaMask转账可能仍然报错,这时候可以把MetaMask删掉重新加入扩展程序 转账成功的样子","categories":[{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"}],"tags":[{"name":"MetaMask","slug":"MetaMask","permalink":"https://zunpan.github.io/tags/MetaMask/"},{"name":"转账","slug":"转账","permalink":"https://zunpan.github.io/tags/%E8%BD%AC%E8%B4%A6/"},{"name":"ganache","slug":"ganache","permalink":"https://zunpan.github.io/tags/ganache/"}]},{"title":"以太坊","slug":"以太坊","date":"2020-01-07T22:53:31.000Z","updated":"2023-09-24T04:27:40.281Z","comments":true,"path":"2020/01/08/以太坊/","link":"","permalink":"https://zunpan.github.io/2020/01/08/%E4%BB%A5%E5%A4%AA%E5%9D%8A/","excerpt":"","text":"以太坊概述 比特币和以太坊是两种最重要的加密货币,比特币被称为区块链1.0,以太坊被成为区块链2.0 以太坊在系统设计上,针对比特币运行过程中出现的一些问题进行了改进,比如说出块时间调整至15s,这是基于ghost协议的共识机制;另外一个改进是挖矿使用的mining puzzle,比特币的mining puzzle是计算密集型的,比拼的是计算hash的算力,这样会造成挖矿设备的专业化,以太坊设计的是memory hard mining puzzle ,目的是在一定程度上限制ASIC芯片的使用(ASIC resistance),将来以太坊还有一些革命性的改变,用权益证明(proof of stake)代替工作量证明(proof of work)。除此之外,以太坊还加入了重要功能——对智能合约的支持(smart contract)。 什么是智能合约? 我们知道比特币是去中心化的货币(decentralized currency),而以太坊是去中心化的合约(decentralized contract)。货币本来由政府发行,货币的价值是建立在政府公信力的基础上,政府通过司法手段来维护货币系统的正常运行。比特币的出现用技术手段把政府的职能取代了。去中心化的合约也是类似的意思,现实社会中,合约的有效性也是应该通过政府维护的,以太坊也用技术手段取代了司法手段,如果合同中的内容可以通过代码实现,代码就可以放到区块链上,通过区块链的不可篡改性来保证代码的正确运行,当然,不是所有的合同都能用编程语言实现,也不是说所有的合同条款都可以量化,但是逻辑比较简单清晰的是可以写成智能合约的。 去中心化的合约的好处? 去中心化货币的一个应用场景是跨国转账,比如人民币→小币种,手续麻烦且手续费贵,如果用比特币就很方便。智能合约也有类似的应用场景,假如合同方来自世界各地,没有一个统一的司法管辖权,比较难用司法手段维护合约的有效性,这时候可以将合约内容以代码形式写进区块链里保证合约强制执行 以太坊账户 比特币是基于交易的账本,这种模式下并没有显式记录账户中有多少钱。 以太坊采用的是基于账户的模式,这种模式和银行账户比较类似,系统中要显示记录账户中有多少以太币,转账的时候只要余额够就可以转账,不用说明币的来源也不用找零,这样可以防御double spending attack。以太坊虽然不用说明币的来源,但是也不能篡改账户余额,因为账户余额在所有全节点的状态数中维护,必须所有全节点认为你的账户余额发生改变,你的账户余额才发生改变。虽然能防御double spending attack,但也可能会受到replay attack(重放攻击),转账的时候收款人是有恶意的,他可能会将这次交易再次发布,导致付款人转了两次。double spending attack 和 replay attack 是相对的,double spending 是花钱的人不诚实,replay attack 是收钱的人不诚实。以太坊解决replay attack的手段是加一个nonce(交易数),交易数代表这个账户建立以来的交易次数,由全节点维护,这个交易数会在交易过程成写进交易信息一起签名保护,如果交易的交易数比全节点中维护的付款人的交易数刚好大1,说明是正常交易,如果交易被重放的话,两者会相等,属于不合法交易。 以太坊中有两类账户,一类是外部账户(externally owned account),外部账户类似于比特币的账户,就是一对公私钥,掌握了私钥就掌握了这个账户。外部账户有两个状态,一个是balance,一个是nonce。第二类账户是合约账户(smart contract account),合约账户不是通过公私钥对控制,除了balance和nonce,还有code,storage。 以太坊状态树 以太坊采用的是基于账户的模式,系统中显式维护每个账户的余额 用什么数据结构来实现这种模式? 我们要完成的是账户地址到账户状态的映射,addr→state,以太坊用的账户地址是160bits,一般表示成40个十六进制数,状态是外部账户的状态和合约账户的状态,包括余额,交易数,对于合约账户,还包括代码和存储。 第一种方案,用hash表+merkle tree。从直观上看,映射就是一个key-value对,很自然的想法是用一个hash表来实现。系统中的全节点维护一个hash表,每次有一个新的账户插入到hash表里,查询账户的余额就查hash表,如果不考虑hash碰撞,查询速度可以在常数级别,更新也很方便。这种方案的问题是,以太坊要防止所有账户的state被篡改,就要像比特币一样构建一个merkle tree,里面的交易变成账户状态,如果每次发布一个合法交易,某个账户状态发生改变,那么每个全节点都要重新构造merkle tree,代价太大。 第二种方案,不用hash表,直接用merkle tree,修改的时候直接改merkle tree。这个方法的问题在于merkle tree没有提供高效的查找和更新的方法,另外不同节点构造的merkle tree 叶子节点顺序不同也会导致merkle root 不同 ,所以得用sorted merkle tree ,但是sorted merkle tree 也有问题,如果新创建的用户地址在hash值在中间,那么插入merkle tree之后,merkle tree几乎重构。 有一种数据结构叫trie(字典树或前缀树),从retrieval(检索)中来,下面是一个一些单词组织成一个trie的例子 上图中,单词根据每一位的不同进行分裂,比如第二列有e和o就有两个分叉,第三列有在第二列是e的基础上只有n,所以只有一个分叉,单词有可能在非叶子节点结束。这个结构有一些特点。 第一个特点,在trie中,每个节点的分支数目取决于key中每个元素的取值范围,这个例子中,每个都是小写英文单词,所以每个节点的分叉数目最多27(26个小写字母+1个结束标志位)个,结束标志位表示到这个地方,这个单词就结束了。在以太坊中,地址是40位十六进制数,分叉数目有时候也叫branching factor 是17。 第二个特点,trie的查找效率取决于key的长度,键值越长,查找需要访问内存的次数就越多,以太坊中,键值都是40位。 第三个特点,如果用hash表存储key-value对,有可能出现碰撞,trie不会出现碰撞,只要地址不同,最后一定是不同分支 第四个特点,mekle tree不排序的话插入的账户位置不一样导致树的结构不一样,trie不论插入顺序如何,插入内容一致,最后的树就是一样的,这个对于以太坊非常有用 第五个特点,每次发布交易的时候,系统中大部分账户不变,只有少部分账户的状态需要更新,trie的局部更新性很好,只需要访问对应分支(注意,上图只画出了key)就可以找到value进行修改。 但是trie有一个缺点,trie的存储比较浪费,像上图有些节点只有一个子节点。如果能把这些节点合并,就可以减少存储的开销,也提高了查找的效率。 还有一种数据结构叫patricia tree或patricia trie(压缩前缀树),用上图例子进行路径压缩的结果如下 直观上看,树的高度减少了,存储更密集了。但是,如果新插入一个单词,原来压缩的路径可能需要扩展开来,假设上图加入geometry,就不能压缩成EN节点了。路径压缩有时候效果明显,有时候不明显。树中插入的键值分布如果比较稀疏情况下,路径压缩效果明显。比如假如上图的每个英文单词都很长,但是一共没有几个单词(misunderstanding、decentralized、disintermediation(去中心商,意思是让系统中的价值提供者和消费者直接交互)),这个时候插入到trie中,就会变成下图 如果用了压缩树,就会变成 因此键值分布比较稀疏的时候,路径压缩效果较好。而在以太坊中,键值是地址,160位,总的地址空间有2160位,非常大。以太坊的账户数和2160相比微乎其微,所以键值分布非常稀疏 第三种方案 先提一下MPT(merkle patricia tree)。MPT和PT的区别就是连接节点之间的指针用的是hash指针,最后会保留一个关于状态树的merkle root,它的作用之一也是防止账户状态被篡改,作用之二是merkle proof证明账户余额,将账户状态所在分支发给轻节点即可证明,作用之三是证明账户不存在,如果账户存在,把应当存在的账户的所在分支发给轻节点验证,验证失败则不存在。以太坊的状态树用的就是MMPT(modified MPT),下图是一个例子。 右上角有4个账户,为了简单起见,地址都非常短,就是上面的keys,账户状态只显示余额,就是上面的values。树中的节点分为三种,Extension Node,如果树的某一部位进行了压缩,就会出现一个Extension Node。因为4个地址的前两位开头都是a7,所以根节点就是Extension Node。下一层出现了分叉,所以出现了一个Branch Node。1的后面只有一个1355,所以它就是一个Leaf Node。7的后面有两个地址都是d3,所以压缩,再往下是3和9就分开来了,所以是一个Branch Node,再下面就是Leaf Node了。最右边的f后面就只有一个9365,所以是一个Leaf Node。 每次发布一个新的区块的时候,状态树中有一些节点的值会发生变化,这些改变不是原地修改,而是新建分支,原来的分支被保存。 上图是两个相邻的区块,State Root是状态树的根hash值,虽然每一个区块只有一个State Root,但是两棵树的大部分节点是共享的,右边的树主要指向左边这棵树的节点,只有发生改变的节点需要新建分支。上图例子中是合约账户(包括nonce,balance,codehash,storage root的那个节点)发生变化,每一个合约账户的存储都是一棵小的MPT,上图交易次数nonce发生变化,balance发生变化,代码不变,所以codehash指向原来的code,存储变了,但是存储树中的大部分节点也是没有改变,唯一的改变的29变成了45,所以新建了一个分支。所以系统中要维护的不只是一颗MPT,而是每次出现一个区块,都要新建一个MPT,只不过这些状态树中大部分节点是共享的,只有少数发生变化的节点要新建分支。为什么要保留历史状态?系统当中有时候会出现分叉,临时性分叉非常普遍,假设出现一个分叉,两个节点同时获得记账权,如果上面一个节点胜出,下面的节点可以roll back(回滚对账户状态的修改)然后顺着上面的节点所在分支继续挖,这个和比特币不太一样,比特币交易类型比较简单,有的时候可以反向操作推断出前一个状态,比如说转账交易,A给B转了10个比特币,回滚只需要给A加10个比特币,B减去10个比特币,但是以太坊中不行,因为以太坊中有智能合约,智能合约执行完成后再推算之间的状态是不可能的,所以要想支持回滚就要记录历史状态。 以太坊中代码的数据结构! 上图是block header结构 ParentHash:前一个区块块头的hash UncleHash:叔叔区块的hash,可能比Parent大好几辈 Coinbase:挖出区块的矿工地址 Root:状态树的根hash TxHash:交易树的根hash,类似于比特币中的merkle root ReceiptHash:收据树的根hash Bloom:布隆过滤器,和收据树相关,提供高效的查询符合某种条件的交易的执行结果 Difficulty:挖矿难度 GasLimit和GasUsed和汽油费相关,智能合约消耗汽油费,类似于比特币中的交易费 Time:区块产生时间 MixDigest和Nonce和挖矿过程相关 上图是区块结构,header是指向block header的指针,uncles是指向叔叔区块的指针,而且是数组,transactions是交易列表 上图是区块在网上发布的真实结构,其实就是区块结构的前三项。 我们知道状态树保存的是key-value pairs,key就是地址,value是账户状态,账户状态要经过序列化过程才能保存进状态树中,序列化用的是RLP(Recursive Length Prefix),特点是简单,只支持nested array of bytes ,意思是字节数组可以嵌套,以太坊中所有数据类型最后都要变成nested array of bytes, 以太坊的交易树和收据树 每次发布的区块中,交易会组织成一棵交易树,也是一棵merkle tree,和比特币中情况类似;每个交易执行完之后会形成一个收据,记录交易的相关信息,交易树和收据树上的节点是一一对应的,增加收据树是考虑到以太坊的智能合约执行过程比较复杂,通过增加收据树的结构有利于快速查询执行结果。从数据结构上看,交易树和收据树都是MPT,和比特币有所区别,比特币的交易树就是普通的merkle tree ,MPT也是一种merkle tree,但是和比特币中用的不是完全一样。对于状态树来说,查找账户状态所用的key是地址,对于交易树和收据树来说,查找的键值就是交易在区块中的序号,交易的排列顺序由发布区块的节点决定。这三棵树有一个重要的区别,就是交易树和收据树都是只把当前发布的区块中的交易组织起来,而状态树是把系统中所有账户的状态都组织起来,不管账户和当前区块中的交易有没有关系。从数据结构上来说,多个区块的状态树是共享节点的,每次新发布的区块时,只有区块中的的交易改变了账户状态的那些节点需要新建分支,其它节点都沿用原来状态树上的节点。相比之下不同区块的交易树和收据树都是独立的。 交易树和收据树的作用 交易树一个用途是merkle proof ,像比特币中用来证明某个交易被打包到某个区块里。收据树也是类似的,证明某个交易的执行结果,也可以在收据树里提供一个merkle proof。除此之外,以太坊还支持更复杂的查询操作,比如查询过去十天当中和某个智能合约有关的交易,这个查询方法之一是,把过去十天产生的所有区块中交易都扫描一遍,看看哪些是和这个智能合约相关的,这种方法复杂度比较高,且对轻节点不友好。以太坊中的查询是引入了bloom filter(布隆过滤器),这个数据结构支持比较高效的查找某个元素是不是在一个比较大的集合里,bloom filter给一个大的集合计算出一个很紧凑的摘要,比如说一个128位的向量,向量初始都是0,通过hash函数,把集合中的每个元素映射到向量中的某个位置,元素的映射位置都置为1,所有元素处理完后向量就是一个摘要,这个摘要比原来的集合小很多。这个过滤器的作用是,我们想查询一个元素,但集合太大我们不能保存,这时候对该元素取hash值,发现映射到向量中0的位置,说明这个元素不在集合里,但是映射到向量中1的位置,也不能说明元素在集合里,因为可能会出现hash碰撞。所以用bloom filter时,可能会出现false positive,但是不会出现false negative,意思是有可能出现误报,但是不会出现漏报,在里面一定说在里面,不在里面可能也会说在里面。bloom filter有各种各样的变种,比如说像解决hash碰撞,有的bloom filter用的不是一个hash函数,而是一组,每个hash函数独立的把元素映射到向量中的某个位置,用一组hash函数的好处是,一般不可能一组hash函数都出现碰撞。bloom filter的一个局限性不支持删除操作,因为存在hash碰撞,使得不同元素映射到向量同一个位置,如果删掉一个元素,使对应位置上的1变成0,那么和它发生碰撞的元素也被删除了,所以简单的bloom filter 不支持删除操作,可以将0和1改成计数器,记录有多少元素映射过来,而且还要考虑计数器是否会overflow,但是这样就复杂的多,和当初设计的理念就违背了,所以一般用bloom filter就不支持删除操作。以太坊中bloom filter 的作用是,每个交易执行完成后会形成一个收据,收据里面就包含了一个bloom filter ,记录这个交易的类型、地址等其它信息,发布的区块在块头里也有一个总的bloom filter ,这个总的bloom filter 是区块里所有交易的bloom filter 的并集,所以说想查询过去十天当中和某个智能合约有关的交易,先查哪个区块的块头的bloom filter里有我要的交易的类型,如果块头的bloom filter里面没有,那么这个区块里面就没有我们想要的,如果块头的bloom filter 里有,我们再去查找区块里面包含的交易所对应的收据树里面对应的bloom filter,但是可能会出现误报,如果有的话,我们再找到相对应的交易进行确认,好处是通过bloom filter能快速过滤大量无关区块,很多区块看块头的bloom filter就知道没有我们想要的交易,剩下的少数候选区块再仔细查看。轻节点只有块头信息,根据块头就能过滤掉很多信息,剩下有可能是想要的区块,问全节点要具体信息。 以太坊的运行过程可以看作交易驱动的状态机(transaction-driven state machine),状态机的状态指状态树中的那些账户状态,交易指交易树中那些交易,通过执行这些交易,使得系统从当前状态转移到下一个状态。比特币也可以认为是交易驱动的状态机,比特币中的状态是UTXO。这两个状态机有一个共同特点是状态转移都是确定性的,对一组给定的交易能够确定性的驱动系统转移到下一个状态,因为所有的节点都要执行同样的交易,所以状态转移必须是确定性的。 以太坊挖矿算法 对于基于工作量证明的区块链系统来说,挖矿是保障安全的重要手段。为了抵制矿机,以太坊设计了一中memory hard mining puzzle,以太坊用了两个数据集,一个是16M的cache,一个是1G的dataset叫做DAG,DAG是从cache中生成,这样设计的目的是便于轻节点验证,轻节点只需要保存16M的cache即可,只有矿工才需要保存1G的大数据集。基本思想是先用一个种子节点经过一些运算得到数组的第一个元素,然后对元素依次取hash得到后面的元素,这样得到的是一个填充了伪随机数的数组,就是一个cache,然后大数据集里面的每一个元素根据cache里的元素,依次读取256次取hash生成,求解puzzle的时候用的是大数据集,按照伪随机的顺序从大数据集中读取128个数,一开始,根据区块的块头算出一个初始的hash,根据hash映射到大数据集中的某个位置,把该数读取出来,然后进行运算得到下一个数得位置,每次读取的时候除了计算出这个元素的位置之外,还要把相邻的元素读取出来,进行64次循环,每次取出2个数,得到128个数,最后算出一个hash值,和挖矿难度的目标阈值比较一下,如果不合适就将block header里面的nonce替换一下重复上面过程 权益证明 比特币和以太坊目前用的都是基于工作量的证明,这种共识机制受到普遍的批评就是浪费电。 矿工挖矿是出于出块奖励,算力越大,出块奖励平均下来就越大,算力取决于设备的多少,也就是资金的投入,资金投入越多,奖励也越丰厚。那么我们可不可以不挖矿,直接比拼资金,奖励按资金比分配?这就是权益证明的思想,权益证明有时候也叫virtual mining。 采用权益证明的交易货币,一般会在正式发行之前预留一些货币给开发者,也会出售一部分货币来换取开发加密货币所需要的资金,将来按照权益证明的共识机制,每个人按照持有货币的数量进行投票,这种方法和工作量证明相比有一些优点,一个是不需要挖矿了,减少能耗;二是挖矿的算力从现实世界来,攻击者只要足够富裕,买大量矿机就可以发动攻击,对于小币种是致命打击,权益证明是按持有的货币数量进行投票,类似股票分红,如果某人想发动攻击,他需要先获得货币总量的51%才能发动攻击,也就是说发动攻击的资源必须从加密货币的系统中来,这样就系统形成了一个闭环,无论攻击者在系统外有多少资源,都不会对系统造成直接的影响,如果一定要发动攻击就要买大量的币,造成币的大涨,而开发者和早期矿工就可以从中获利。权益证明和工作量证明不是互斥的,有些加密货币采用的是混合模型,仍然要挖矿,但是挖矿难度和持有多少币是相关的,币越多难度越低,但是这样简单设计有一个问题就是富人挖矿越来越简单。有些两者混用的加密货币系统会将用于降低挖矿难度的币锁定一段时间,下次再挖一个区块的时候,不能用锁定的币降低难度,过几个区块才能使用,这种叫proof of deposit。 权益证明有许多问题,早期的权益证明有一个问题是两边下注 上图出现分叉,如果挖矿的话,我们会沿着上面这条链继续挖,但是下面的链也有可能成为最长合法链,只要下面这个分支连续挖出好几个区块。但是矿工不会两边都挖,因为算力会分散。如果不挖矿,用权益证明的话,两边都可以下注,如果上面那条链成为最长合法链,下面分支锁定的币对上面分支没有影响的,所以这种情况叫nothing at stake。 以太坊准备采用的权益证明协议叫做Casper the Friendly Finality Gadget(FFG),它在过渡阶段也是要和工作量证明混合使用,为工作量证明提供finality,finality 是最终状态,包含在finality中的交易不会被取消,单纯基于工作量证明是有可能被回滚的,Casper协议引入validator,要想成为validator,必须投入一定数量的以太币作为保证金,这个保证金会被锁定,validator推动系统达成共识,投票决定哪条链是最长合法链,投票权重取决于保证金的大小。挖矿的时候(混用状态下)每挖出一百个区块,就作为一个epoch,然后要决定它能不能成为finality要进行投票,投票进行两轮,类似于数据库的two-phrase commit,一个是prepare message,一个是commit message,Casper规定每一轮投票都要获得2/3以上的投票才能通过。实际当中不区分投票阶段,epoch也减少至50个,每个epoch只用一轮投票就行,这轮投票对于上一个epoch来说是commit message,对于下一个epoch来说是prepare message,要连续两个epoch都得到2/3的投票才算有效。 上图是早期的Casper协议,100个区块构成一个epoch,每个epoch要投两轮,都要获得2/3的票 上图是实际的Casper,这轮投票对于上一个epoch来说是commit message,对于下一个epoch来说是prepare message,要连续两个epoch都得到2/3的投票才算通过。 验证者验证的好处是如果验证者履行职责,那么可以获得相应的奖励,就像矿工挖矿能获得出块奖励一样,验证者验证也可以得到奖励,相反,如果验证者有不良行为,要受到相应处罚,比如验证者不作为,导致系统迟迟达不成共识,这样要扣掉验证者的部分保证金,如果验证者乱作为,给两个有冲突的分叉都投票,这种情况要没收全部的保证金。没收的保证金会销毁,相当于减少了以太币的总量。每个验证者有一定的任期,任期满了之后要经过一定时间的等待期,等待期是为了让其它节点可以检举验证者的不良行为,等待期过了没有受到惩罚那么验证者可以取回保证金以及一定的奖励,这就是casper协议的过程。 这里有一个问题,包含在finality的交易是不是一定不会被回滚,假设有某个恶意节点发动攻击,如果他只是一个矿工,那么他是不能推翻已经达成的finality,因为finality是验证者投票投出来的。如果有大量的验证者两边下注,给前后两个有冲突的finality都下注,casper协议规定每轮投票要2/3的支持才算通过,所以至少有1/3的验证者是两条分叉都投票了。","categories":[{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"}],"tags":[{"name":"区块链技术与应用","slug":"区块链技术与应用","permalink":"https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E4%B8%8E%E5%BA%94%E7%94%A8/"},{"name":"以太坊","slug":"以太坊","permalink":"https://zunpan.github.io/tags/%E4%BB%A5%E5%A4%AA%E5%9D%8A/"}]},{"title":"比特币的匿名性","slug":"比特币的匿名性","date":"2020-01-07T11:59:08.000Z","updated":"2023-09-24T04:27:40.283Z","comments":true,"path":"2020/01/07/比特币的匿名性/","link":"","permalink":"https://zunpan.github.io/2020/01/07/%E6%AF%94%E7%89%B9%E5%B8%81%E7%9A%84%E5%8C%BF%E5%90%8D%E6%80%A7/","excerpt":"","text":"比特币的匿名性更像是一种化名、网名。 和现金相比,匿名性不如现金,现金是完全匿名,上面没有任何人的信息,所以非法交易会用大量现金。 和银行相比,匿名性好于银行,因为银行开户是实名制,而比特币不需要。但如果银行开户允许使用化名,那么银行的匿名性要更好,因为比特币的账本是公开的,而银行的账本是受到控制的,银行工作人员可以查到所有人账号,但是普通老百姓是查不到别人的账号的。 破坏匿名性的情况 多个账户 一个人可能有很多账户,这些账户可能会被关联。 比如交易过程中,inputs里面有两个输入,一个地址输入4比特币,一个地址输入5比特币,outputs有两个输出,一个地址输出6比特币,一个地址输出3比特币。因为像买东西这种场景,大概率是会出现零钱的,3比特币就是零钱。因为交易的两个输入受你的控制,说明这是你的账户,两个地址就被关联了。输出的第二个地址也会被关联上,因为如果买东西只需要花3个比特币,那么输入只需要一个地址即可,所以分析出来3个比特币流向的地址也是你的账户。 与现实世界发生联系 比特币和实体货币发生联系的时候都有可能泄露身份,比如买比特币的时候,可以去交易所,此时就会留下你的交易记录。又比如用比特币支付。 如何提高匿名性 洋葱路由 首先保证网络层的匿名性,普遍的做法是多路径转发,TOR(洋葱路由)就是这个原理,消息在网络上传输要经过许多中间节点,每个节点只知道上一个节点是谁,而不知道谁发出的,只要路径上有一个节点是诚实的,他就会把发消息的人信息隐藏掉,后面的节点就不知道发消息是谁了。 混币 在应用层上,一种做法是coin mixing ,借助这类服务提供商,将你的币和别人的币混在一起,这时候你去取币,取的就不是原来的地址;还有一种做法是应用提供的天然mixing,比如在线钱包,大家都往里面投了币,然后取得时候就不一定是用原来的地址;还有一种手段是通过比特币交易所,你在交易所里面托管了比特币,然后经过一段时间的投资,比特币→美元→以太坊→莱特币→比特币,这时候你取到的比特币可能就不是原来的地址了 零知识证明 零知识证明是指一方(证明者)向另一方(验证者)证明一个陈述是正确的,而无需透露除该陈述是正确的外的任何信息。 一个有争议的例子:我想向别人证明这个账户是我的,也就是我持有它的私钥,但是我不能直接透露出私钥,我可以产生一个用私钥签的名,别人可以通过公钥验证签名,这个例子中,我是证明者,验证者是别人,陈述是我持有它的私钥,但是我没有透露私钥以外的信息。但这是有争议的,因为我没有透露私钥,但还是透露了用私钥产生的签名。 零知识证明的数学基础是同态隐藏 第一点说明不会发生碰撞,第二点说明隐藏性,针对第三点举例","categories":[{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"}],"tags":[{"name":"区块链技术与应用","slug":"区块链技术与应用","permalink":"https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E4%B8%8E%E5%BA%94%E7%94%A8/"},{"name":"比特币","slug":"比特币","permalink":"https://zunpan.github.io/tags/%E6%AF%94%E7%89%B9%E5%B8%81/"}]},{"title":"比特币的分叉","slug":"比特币的分叉","date":"2020-01-07T08:07:17.000Z","updated":"2023-09-24T04:27:40.283Z","comments":true,"path":"2020/01/07/比特币的分叉/","link":"","permalink":"https://zunpan.github.io/2020/01/07/%E6%AF%94%E7%89%B9%E5%B8%81%E7%9A%84%E5%88%86%E5%8F%89/","excerpt":"","text":"分叉是指一条链变成了两条链。 原因 分叉可能是多种原因造成的 一种原因是两个节点差不多同时挖到了区块,这个时候两个节点都可以发布区块,就出现了一个临时性的分叉,这种分叉叫state fork。分叉攻击也属于state fork ,也是属于对比特币区块的当前状态产生了分歧,只不过分叉攻击的分歧是故意造成的,所以分叉攻击造成的分叉也叫deliberate fork 。 除此之外,比特币协议发生了改变也会造成分叉,要修改协议需要软件升级,在一个去中心化的系统里,升级软件的时候没有办法保证所有节点同时都升级软件,我们假设大部分节点升级了软件,少部分节点因为种种原因没有升级,这种情况导致的分叉叫做protocd fork。根据对协议内容修改的不同,又可以进一步分成硬分叉(hard fork)和软分叉(soft fork)。 硬分叉 如果对比特币协议增加新的特性,那些没有升级软件的节点不认可新特性,这个时候就会产生硬分叉,一个例子是比特币的区块大小限制。假如有人发布了软件更新,将区块大小限制从1M→4M,假设大多数节点都更新了,少数节点没有更新,这里的大多数不是按账户数目来算,而是按算力来算,即系统中有大多数hash算力的节点都更新了软件。新节点能挖出最大是4M的区块,但是旧节点不认可大小超过1M的区块,所以旧节点会沿着小区块一直挖下去,新节点大小区块都接受,但旧节点不接受大区块,两者没有达成共识,这样就会出现一个永远的分叉。 软分叉 如果对比特币协议加一些限制,原来合法的交易或者区块在新的协议可能不合法就会引起软分叉。假如有人发布更新将区块大小变成0.5M,大部分节点更新了软件,少部分没有更新。这时候新节点沿着一条链挖小区块,不认可大区块,旧节点沿着一条链挖大区块,同时旧节点也认可小区块,因为新节点算力更强,所以更快形成一条新旧节点都认可的最长合法链,但是新节点不会在旧节点产出的大区块后面继续挖,而是会继续分叉挖小区块,所以旧节点挖出的大区块最后都没用了,不得不更新软件。 软硬分叉的区别就是 掌握大多数算力的新节点认可旧节点挖出的区块,但旧节点不认可新节点挖出的区块,那么就是硬分叉; 掌握大多数算力的新节点不认可旧节点挖出的区块,但是旧节点认可新节点挖出的区块,那么就是软分叉。 实际当中出现软分叉的情况之一是给某些目前协议中没有规定的域增加新的含义,赋予新的规则,如coinbase域,将前8字节作为extra nonce,但是剩下的字节都没有被使用。有人提出剩下的字节用来存UTXO的根hash值,那么新节点不认可旧节点挖出的区块,而旧节点认可新节点,因为coinbase写啥都无所谓,这样就会出现软分叉。 比特币历史上有名的软分叉例子是P2SH,这个功能最初没有,是后来通过软分叉加进来的。支付的时候不是付给一个public key hash(也就是输出脚本不是给出收款人公钥hash),而是付给一个redeem script hash(赎回脚本的hash),具体流程参考前文。对于旧节点来说,他不知道这个P2SH这个特性,他只会验证赎回脚本是否正确,新节点才会做第二阶段的验证,所以旧节点认为合法的交易,新节点可能认为非法,新节点认为合法的交易旧节点也认可","categories":[{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"}],"tags":[{"name":"区块链技术与应用","slug":"区块链技术与应用","permalink":"https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E4%B8%8E%E5%BA%94%E7%94%A8/"},{"name":"比特币","slug":"比特币","permalink":"https://zunpan.github.io/tags/%E6%AF%94%E7%89%B9%E5%B8%81/"}]},{"title":"比特币脚本","slug":"比特币的脚本","date":"2020-01-07T06:31:45.000Z","updated":"2023-09-24T04:27:40.284Z","comments":true,"path":"2020/01/07/比特币的脚本/","link":"","permalink":"https://zunpan.github.io/2020/01/07/%E6%AF%94%E7%89%B9%E5%B8%81%E7%9A%84%E8%84%9A%E6%9C%AC/","excerpt":"","text":"交易实例 这个交易的下面一个输出已经被花掉了,并且这个交易得到了23个confirmation,回滚的可能性很小。 输入脚本包含两个操作,分别把两个很长的数压入栈中,比特币使用的脚本语言非常简单,唯一能访问的内存空间只有堆栈,不像通用编程语言C++之类,有全局变量、局部变量、动态分配的内存空间等,它这里只有一个栈,所以叫基于栈的语言。输出脚本有两行,分别对应上面的两个输出,每个输出有自己单独的一段脚本。 交易具体内容 首先看一下交易的宏观信息 txid:transaction id,交易id hash:交易的hash version:比特币协议版本号 size:交易大小 locktime:用来设定交易的生效时间,0表示立即生效,非0值表示过一段时间生效,比如说等10个区块之后才能被写入区块链中 vin:输入部分 vout:输出部分 blockhash:这个交易所在区块的hash值 confirmations:确认数 time:交易产生时间 blocktime:区块产生时间 一个交易可能有多个输入,所以vin是个数组结构,上图只包含一个输入,每个输入都要说明输入花的币来自之前哪个交易的输出,所以前两行表示来源 txid:之前交易的hash vout:之前这个交易的第几个输出 scriptSig:输入脚本 一个交易可能也有多个输出,所以vout也是一个数组结构,上图只包含两个输出 value:输出金额,单位是比特币 n:这个交易的第几个输出 scriptPubKey:输出脚本 asm:输出脚本的内容 reqSigs:需要多少个签名 type:输出类型,上图都是公钥的hash address:输出地址 使用脚本验证交易合法性 验证这个交易的合法性就要将B→C的输入脚本和A→B的输出脚本拼接在一起执行。注意后一个交易的输入脚本在前,前一个交易的输出脚本在后,在早期的比特币系统中,这两个脚本是拼接在一起,从头到尾执行一遍,后来出于安全因素考虑,这两个脚本改为分别执行,首先执行输入脚本,如果没有出错,就执行输出脚本,如果能顺利执行,最后栈顶的结果为非0值,也就是true,这个交易就是合法的。如果执行过程中有任何错误,这个交易就是非法的。如果交易有多个输入,那么每个输入脚本都要和所对应的输出脚本进行配对验证,全都验证通过,这个交易才是合法的。 输入和输出脚本的最简单形式就是P2PK,输出脚本直接给出收款人的公钥(PUSHDATA),CHECKSIG是检查签名的操作,输入脚本直接给出用私钥对输入脚本所在交易的签名。 此处为了方便演示将输入脚本和输出脚本拼接在了一起,实际上是分开的。该脚本执行首先将输入脚本提供的签名压入栈中,然后将输出脚本提供的公钥压入栈,然后将栈顶两个元素弹出,用公钥检查签名是否正确,如果正确返回true 上图是P2PK实例,上面这个交易的输入脚本就是把签名压入栈,下面的交易是上面交易的币的来源,输出有两行,第一行将公钥压入栈,第二行是验证。 输入和输出脚本的第二种形式是P2PKH,这种形式与上一种形式的区别在于输出脚本没有直接给出收款人的公钥,给出的是公钥的hash,公钥在输入脚本给出,输入脚本既要给出签名,也要给出公钥,输出脚本其它操作是为了验证签名的正确性。这种形式是最常用的。 上面两行来自输入脚本,后面来自输出脚本,还是从上往下执行,前两条语句将签名和公钥压入栈,DUP 表示将栈顶元素复制一遍,HASH160表示将栈顶元素弹出,取hash之后再压入栈,此时栈顶变成公钥的hash值,此时栈的情况如下图 EQUALVERIFY是弹出两个栈顶元素,比较是否相等,相等就会消失 最后 CHECKSIG 和上一种形式一致,弹出两个元素,用公钥检查签名的正确性,正确最后栈中只会留下一个true P2PKH是最常用的形式 有一种最复杂的脚本形式P2SH,这种的形式的输出脚本给出的不是收款人的公钥hash,而是收款人提供的脚本的hash,叫redeemScriptHash(赎回脚本)。将来花这个钱的时候输入脚本要给出赎回脚本的具体内容,同时要给出让赎回脚本正常运行的签名 P2SH为什么这么复杂?P2SH在最初的比特币版本中并没有,后来通过软分叉的形式加进去的。常见的应用场景是对多重签名的支持。比特币系统中,一个输出可能要有多个签名才能把钱取出,比如某个公司的账户,可能要求5个合伙人中,任意3个合伙人的签名,才能把钱从公司账户中取走,这样为私钥的泄露提供了安全的保证,比如某些合伙人私钥泄露出去了,那么问题也不大,因为还需要另外两人的签名,才能取出钱来,同时也为私钥的丢失提供了冗余,5个合伙人中,即使有2人忘掉了私钥,剩下3人仍然可以把钱取出,然后转到某一个安全的账户。这个功能是通过CHECKMULTISIG实现的 输出脚本里给出N个公钥,同时给出一个域值M,输入脚本只要提供N个公钥中对应的签名中任意M个合法签名就可以通过验证。输入脚本的第一行红X表示压入一个多余的元素,CHECKMULTSIG有一个bug是会多弹出一个元素,因为去中心化的特性,现在不能通过软件升级改正。给出的M个签名的相对顺序要和N个公钥中的相对顺序一致才可以。 这个过程并没有用到P2SH,而是用的原生的CHECKMULTISIG。这样实现有许多不方便的地方,比如网上购物,某个电商平台用多重签名,要求5个合伙人中任意3个合伙人的签名才能把钱取出来,这就要求消费者在支付的时候要给出5个合伙人的公钥,同时还要给出N和M的值,这里N是5,M是3。消费者只能从平台获知这些信息,转账非常麻烦,这时候就可以采用P2SH实现多重签名 用P2SH实现多重签名可以将输出脚本的复杂度转移到赎回脚本里,输出脚本只要给出赎回脚本的hash,赎回脚本要给出N个公钥和N和M的值,赎回脚本在输入脚本提供,也就是说由收款人提供。 现在的多重签名一般都采用这种P2SH形式 最后一种脚本形式比较特殊,这种格式的输出脚本开头是RETURN操作,后面可以跟任意内容,RETURN操作的作用是无条件返回错误,所以包含这个操作的脚本永远不可能通过验证,执行到RETURN语句就出错终止。那么output的比特币岂不是永远花不出去了?确实花不出去,这种脚本的作用是证明销毁比特币,一种应用场景是有些小的币种要求销毁一定比特币才能换取这个币种,这种小币种叫AltCoin(Alternative Coin)。另外一个应用场景是往区块链里写一些内容,因为区块链是个不可篡改的账本,有人就利用这个特性,往里面添加一些永远需要保存的内容,比如前文提到的digital commitment,证明在某个时间知道某个事情,比如将知识产权的内容取hash写入RETURN后面,后面的内容反正永远不会执行,写什么都无所谓,而且放进去的是hash值,不会占太多地方也不会泄露内容,将来出现产权纠纷,可以将具体内容公布出去,证明你在某个时间已经知道某个知识了。 这个交易的输入是铸币交易,第一个输出是出块奖励和交易费,第二个输出的金额是0,输出脚本就是上面那种特殊的形式,开头是RETURN,后面是一些看起来乱七八糟的东西,这个输出的目的就是往区块链里面写一些东西 这是个普通的转账交易,输出脚本也是以RETURN开头。这个交易的输入是0.05个比特币,输出金额是0,说明输入金额全部用来支付交易费了,这个交易并没有销毁任何比特币,只不过把输入的比特币作为交易费转给挖到矿的矿工了,这种形式的好处是矿工看到这种脚本的时候知道它里面的输出永远不可能兑现,所以没有必要保存在UTXO里面,这样对全节点比较友好。","categories":[{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"}],"tags":[{"name":"区块链技术与应用","slug":"区块链技术与应用","permalink":"https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E4%B8%8E%E5%BA%94%E7%94%A8/"},{"name":"比特币","slug":"比特币","permalink":"https://zunpan.github.io/tags/%E6%AF%94%E7%89%B9%E5%B8%81/"}]},{"title":"比特币挖矿难度","slug":"比特币挖矿难度","date":"2020-01-06T10:36:06.000Z","updated":"2023-09-24T04:27:40.283Z","comments":true,"path":"2020/01/06/比特币挖矿难度/","link":"","permalink":"https://zunpan.github.io/2020/01/06/%E6%AF%94%E7%89%B9%E5%B8%81%E6%8C%96%E7%9F%BF%E9%9A%BE%E5%BA%A6/","excerpt":"","text":"挖矿就是不断尝试block header里面的nonce,使得整个block header 的hash值小于等于给定的目标阈值 H(blockheader)<=targetH(block header)<= targetH(blockheader)<=target target越小,挖矿难度越大,调整挖矿难度就是调整目标空间在整个输出空间中所占的比例,比特币用的hash算法是SHA-256,产生的hash值是256位,输出空间是2256 ,调整目标空间在输出空间的比例通俗说就是hash值前面多少个0,合法的区块要求算出来的hash前面至少70个0,这是一种通俗的说法,不是特别准确,因为目标阈值并不是说前面都是0,从某一位开始后面都是1,严格来说,上面的公式才是对的。 挖矿难度和目标阈值是成反比的 difficulty_1_target表示挖矿难度等于1的时候所对应的目标阈值,挖矿难度最小就是1,这个时候对应的目标阈值是个非常大的数。 为什么要调整挖矿难度 系统总算力不断增强,挖矿难度保持不变,出块时间就会越来越短。出块时间越来越短会有什么问题,比如说不到1s出一个区块,这个区块在网络上传输可能需要几十s,别的节点在没有收到这个区块时会继续沿着已有的区块链往下继续扩展,如果有两个节点差不多同时都收到这个区块,差不多同时都发布一个区块,这个时候会出现一个分叉。出块时间越来越短,分叉就会越来越频繁,越来越多。分叉过多对于系统达成共识是没有好处的,而且危害了系统的安全性。 比特币协议假设大部分算力是掌握在诚实的矿工手里,系统的总算力越强,安全性就越好,如果恶意节点掌握了51%的算力,那么它就可以做任何事情(51% attack)。假设现在出现个分叉,系统中的总算力就被分散了,节点根据在网络中位置的不同,可能会选择不同的分叉继续扩展,而有恶意的节点可以集中算力就扩展它的分叉,也就是上图的A→A,这样可以很快使得这条分叉成为最长合法链,因为好人的算力被分散了,这个时候可能都不需要51%的算力,10%的算力可能就够了,所以出块时间不是越短越好。那比特币协议设计的10分钟是不是就是最优的呢,这不一定,比如说8分钟,5分钟行不行,应该也行,这个只是说出块时间要有一个合适的波动范围。有人觉得比特币的10分钟出块间隔太长了,对于一个支付系统来说,支付要等这么长时间才能得到确认,这个有点太长了。以太坊的出块时间就降低到了15s(同样需要加大难度保持出块时间稳定),出块时间大幅下降后,以太坊就要设计一个新的控制协议,叫ghost。在这个协议当中,这些分叉产生的叫orphan block,不能简单丢弃,而是要给它一些奖励,这叫做uncle reward。 如何调整挖矿难度 比特币协议中规定每隔2016个区块,要重新调整一下目标阈值,这个大概是每两个星期调整一下,调整按照以下公式: expected time 就是两个星期,actual time 是系统中最近产生2016个区块实际花费的时间。实际代码中target限制不会一次增大4倍以上,也不会一次减小到1/4以下。 那怎么让所有的矿工同时调整难度呢? 计算target的代码是写在比特币系统的代码里,每挖到2016个会自动进行调整,但是比特币代码是开源的,如果有恶意的节点就是不改target怎么办?因为block header里面有target的编码域(nBits),而没有target域,因为target有256位,直接存要32个字节,nBits只要4个字节,可以认为nBits是target的压缩编码,如果有恶意的节点不改target,检查区块的合法性就通不过,因为每个节点都要独立验证发布的区块的合法性,检查的内容包括nBits域设置的对不对。以太坊也要定期调整挖矿难度,但它不是隔几个区块进行调整,而是每个新出的区块都有可能进行调整,而且调整的方法也比比特币的复杂得多。","categories":[{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"}],"tags":[{"name":"区块链技术与应用","slug":"区块链技术与应用","permalink":"https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E4%B8%8E%E5%BA%94%E7%94%A8/"},{"name":"比特币","slug":"比特币","permalink":"https://zunpan.github.io/tags/%E6%AF%94%E7%89%B9%E5%B8%81/"}]},{"title":"比特币网络","slug":"比特币网络","date":"2020-01-06T08:05:03.000Z","updated":"2023-09-24T04:27:40.285Z","comments":true,"path":"2020/01/06/比特币网络/","link":"","permalink":"https://zunpan.github.io/2020/01/06/%E6%AF%94%E7%89%B9%E5%B8%81%E7%BD%91%E7%BB%9C/","excerpt":"","text":"用户把交易发到比特币网络上,节点收到交易后把他们打包到区块里,然后把区块发到比特币网络上。那么新发布的交易和区块在网络上是如何传输的? 比特币网络的工作原理 比特币工作在应用层(application layer),他的底层是一个P2P的Overlay network 比特币的P2P网络非常简单,所有节点都是对等的,不像有些P2P有所谓的超级节点(Super Node)或者主节点(Master Node)。你要加入这个网络,首先至少得知道一个种子节点(Seed Node),然后你和种子节点联系,它会告诉你它所知道的网络中的其它节点。节点之间是通过TCP通信的,这样有利于穿透防火墙,然后你离开的时候不需要通知其它节点,退出应用程序即可,别的节点没有听到你的消息,过一段时间就会把你删掉。 比特币网络的设计原则 简单、鲁棒而不是高效。每个节点维护一个邻居节点的集合,消息传播在网络中采取的是flooding方式,节点第一次听到某个消息的时候,把它传播给所有的邻居节点,同时记录一下这个消息我已经收到过,下次再收到这个消息就不再转发。邻居节点的选择是随机的,没有考虑底层的拓扑结构,比如加利福尼亚的节点选的邻居节点可能在阿根廷,这样设计的好处是增强鲁棒性,但是牺牲的是效率,你向身边的人转账和美国的人转账速度是差不多的。比特币系统中,每个节点要维护一个等待上链的交易的集合,第一次听到某个交易的时候,把这个交易加入集合,并且转发这个交易给邻居节点,以后再收到这个交易就不转发,这样避免交易在网络中无效传播,转发的前提是交易是合法的。这里有一个risk condition(风险状况),有可能两个有冲突的交易同时被广播到网络上,比如有一个交易是A→B,另外一个交易是A→C,它们用的是同一个输出,每个节点根据在网络中的位置的不同,有的可能先收到前者,有的可能先收到后者,收到后加入等待上链的交易的集合,下次收到另外一个交易的时候就认定是非法的,就不管了。集合中的交易如果被写到区块链中就要被删掉,比如说有个节点听到新发布的区块里面包含了A→B这个交易,这个交易在自己的等待上链的交易的集合中,就会被集合删掉,如果它听到的新发布的区块是A→C这个交易,也会把A→B给删掉,因为它是非法的。新发布的区块在网络中传播的方式和新发布的交易类似,每个节点除了要检查区块的内容的合法性,还要检查是不是在最长合法链上,越是大的区块在网络上传播的速度越慢,比特币协议对区块大小有一个1M的限制。 比特币网络的传播是属于best effort,一个交易被发布到比特币网络上,不一定所有节点都能收到,而且不同节点收到这个交易的顺序也不一定一样,网络传播存在延迟,可能会很长,而且有的节点可能不会按比特币协议的要求进行转发,比如有的该转发不转发,导致别的节点收不到合法交易;有的转发不该转发的,像不合法交易,这个是面临的实际问题。","categories":[{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"}],"tags":[{"name":"区块链技术与应用","slug":"区块链技术与应用","permalink":"https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E4%B8%8E%E5%BA%94%E7%94%A8/"},{"name":"比特币","slug":"比特币","permalink":"https://zunpan.github.io/tags/%E6%AF%94%E7%89%B9%E5%B8%81/"}]},{"title":"比特币实现","slug":"比特币实现","date":"2020-01-06T07:01:40.000Z","updated":"2023-09-24T04:27:40.282Z","comments":true,"path":"2020/01/06/比特币实现/","link":"","permalink":"https://zunpan.github.io/2020/01/06/%E6%AF%94%E7%89%B9%E5%B8%81%E5%AE%9E%E7%8E%B0/","excerpt":"","text":"UTXO 区块链是去中心化的账本,比特币采用的是基于交易的账本模式(transaction-based ledger)。每个区块里记录的是交易信息,有转账交易,铸币交易,但是系统当中并没有哪个地方显式的记录账户中有多少钱,这得需要通过交易记录推算。比特币系统的全节点要维护一个UTXO:Unspent Transaction Output 数据结构。区块链上有很多交易,有些交易的输出已经被花掉了,有些没有被花掉,所有交易中输出还没有花掉的组成一个集合就是UTXO。注意,一个交易可能有多个输出,所以会出现一个交易中有的输出在UTXO里面,有的不在。UTXO集合当中每个元素要给出产出这个输出的交易的hash值以及他在这个交易里是第几个输出,这两个信息就可以定位到UTXO中的输出。 UTXO的作用 为了检测double spending。没有花掉的币必须在UTXO中。全节点要在内存中维护UTXO,以便快速检测double spending 。每个交易会消耗掉一些输出,同时会产生新的输出。如果某人收到比特币的转账交易后,这个钱始终都不花,那么这个信息就要永久存储在UTXO中。 每个交易可以有多个输入和多个输出,所有输入的金额加起来要等于所有输出的金额,这个叫做total inputs =total outputs 。这意味着交易可能不止一个签名。有些交易的total inputs和total outputs有略微的差异,差额会作为交易费给获得记账权发布区块的节点。节点争夺记账权的动力之一是出块奖励,但是光有出块奖励是不够的,获得记账权的节点如果只打包自己的交易记录怎么办?所以,动力之二是就是交易费,就是前面提到的差额。比特币当中的交易费通常很小0.00几,也有些简单的交易是没有交易费的。目前矿工挖矿主要还是为了得到出块奖励。随着出块奖励的减少,以后的交易费会成为争夺记账权的主要动力。 除了比特币这种transaction-based ledger外,与之对应的还有基于账户的模式(account-based ledger),以太坊用的就是这种交易模式,在这种模式当中,系统要显示记录每个账户有多少币,这个和现实体验非常接近。比特币这种交易模式隐私保护性较好,但是也有代价,比如转账交易要说明币的来源,这就是没有账户的代价。以太坊系统就不存在这种问题。 比特币例子 Number Of Transactions : 表示这个区块包含了686个交易 Output Total:总的输出是4220.46616378个比特币 Transaction Fees:总的交易费 Height:区块序号 TimeStamp:产出的时间戳 Difficulty:挖矿的难度,每隔2016个区块会进行调整,保持出块时间在每10分钟产出一个 Nonce:挖矿的随机数 Block Reward:出块奖励 Hash:区块块头的hash Previous Block:前一个区块的块头hash Hash 和 Previous Block 前面都有一串0,这不是偶然的,所谓的挖矿就是不断试nounce,使得整个block header的hash小于等于给定的目标阈值,这个目标阈值表示成16进制就是前面有一长串的0,所以凡是符合难度要求的区块,它的块头的hash值都带一串0 Merkle Root:merkle tree的根hash值 注意,nounce的类型是32位的无符号整数,我们知道挖矿的时候要调整nonce,但是这个nonce最多只有232次方个取值,按照比特币现在的挖矿难度,就算把这232 个的取值都遍历一遍,很可能仍然找不到符合难度要求的。因为近年来挖矿的人不断增多,挖矿难度不断增大,单纯调正nonce是很可能找不到符合难度要求的,所以还要调整block header其他的域,下图是block header 里面的各个域的描述 version:当前使用的比特币协议的版本号,这个无法更改 previous block header hash:前一个区块的块头hash,也是无法修改 merkle root hash:merkle tree 的根hash值,可以修改 time:区块产生时间,有一定的调整余地,比特币系统并不要求非常精确的时间,可以对时间在一定范围内调整 nBits:挖矿时候用的目标阈值编码后的版本,4个字节,只能按照协议的要求定期调正,不能更改 nonce:我们能改的nonce 为什么merkle root hash可以改? 每个发布的区块有一个铸币交易,这是比特币系统中产生新的比特币的唯一方式,这个交易没有输入(也就是上面的no input),因为币是凭空造出来的,它有一个CoinBase域,它可以写入任何东西,没有人管。 上图是一个小型区块链的示意图,左下角是coinbase transaction,我们改变这个交易的CoinBase域之后,这个交易的hash就发生了变化,这个变化会沿着merkle tree 不断向上传播,最终会导致block header 里的Merkle Root发生变化,所以我们可以把这个域当作extra nonce,块头的4字节nonce不够用,这里有很多字节可以用,比如把CoinBase域的前8个字节当作extra nonce来用,这样搜索空间一下子增大到了296次方。真正挖矿是有两层循环的,外层循环是调整CoinBase域的extra nonce,算出block header里的Merkle Root之后,内层循环再调整nonce。 上图是一个普通转账交易的记录,这个交易有两个输入,两个输出,左上方虽然写的是Output,其实对这个交易来说是输入,这里写的Output意思是说他们花掉的是之前哪个交易的Output。右边两个输出还没有被花掉,处于Unspent状态,会保存在UTXO里面 Inputs and Outputs中 Total Input:输入的总金额 Total Output:输出的总金额 Fees:前面两者的差值,这笔交易的交易费 Input Scripts和Output Scripts 输入和输出都是用脚本的形式指定,比特币系统中验证交易的合法性就是把Input Scripts 和Output Scripts配对后执行,注意上图中的Input Scripts和Output Scripts不是一对,上图的Input Scripts要和提供币的来源的交易的Output Scripts进行配对,如果输入脚本和输出脚本拼接在一起能顺利执行,那么这个交易就是合法的。 注意,上图中的所有的tx在计算时就是是一个Merkle Root 挖矿的过程的概率分析 挖矿的过程就是不断尝试各种nonce来求解puzzle,每次尝试nonce 都可以看作一个Bernoulli trial:a random experiment with binary outcome(伯努利试验:一个二元结果的随机试验)。伯努利实验的经典案例是抛硬币,如果硬币不均匀,每次实验,正面的概率就是p,反面的概率是1-p。对于挖矿来说,这两个概率相差甚远,每次尝试一个nonce,成功的概率是微乎其微的,大概率不行的,如果我们做大量的伯努利实验,每个实验都是随机的,那么这些伯努利实验就构成了一个Bernoulli process:a sequence of independent Bernoulli trial(伯努利过程:一系列独立伯努利试验)。Bernoulli process的一个特性是无记忆性(memoryless),意思是说做大量实验,前面的实验结果对后面的结果没有影响,挖矿过程就是不断尝试nonce,这种情况下Bernoulli process可以用Poisson process来近似,实验次数很多,每次成功的概率很小就可以用Poisson process来近似。我们关心的是出块时间,也就是系统里产生下一个区块的时间,这个在概率上可以推导出来,服从指数分布(exponential distribution) 注意,上图的出块时间是整个系统的出块时间,并不是矿工的出块时间,整个系统的平均出块时间是10分钟,平均时间是比特币协议规定的,通过定期调整挖矿难度,使得平均出块时间维持在10分钟左右,具体到每一个矿工,出块时间取决于矿工的算力,算力大,概率就大,出块时间就短。指数分布也是无记忆的,概率密度曲线的特点有:从任何一个地方把它截断,剩下曲线的形状仍然和原来一样,仍然服从指数分布,这就是无记忆的性质,假如过去了10分钟,仍然没有人找到区块,那么接下来还要等多久呢?还是平均下来10分钟,这个和直觉不太一致。这个概率分析告诉我们将来还要挖多少时间和过去挖了多少时间是没有关系的,仍然是服从指数分布,平均还要10分钟,这个性质也叫progress free。 progress free 或者 memoryless有什么作用? 设想一下,如果有某个puzzle不满足这个性质会出现什么情况,比如说过去做的工作越多,接下来尝试nonce的时候成功的概率越大,相当于抛硬币的时候每次结果不是随机的,过去抛了好多次,都是反面朝上,下次再抛硬币的时候,正面朝上的概率会增加。如果有某一个加密货币设计出这样的puzzle,会有什么结果?算力强的矿工会有不成比例的优势,因为算力强的矿工过去的工作量更大。什么是不成比例的优势?比如系统中有两个矿工,一个的算力是另一个的10倍,理想状况下,算力强的矿工能挖到矿的概率应该是另一个矿工的10倍,这才算公平,因为算力强的矿工能尝试的nonce是另一个的10倍,这就是我们说的progress free 或者 memoryless所保证的,如果不是这样的话,算力强的矿工获得记账权的概率就会超过10倍,因为它过去尝试了更多的不成功nonce,那么下次成功的概率就会增大,这就是不成比例的优势。所以progress free 或者 memoryless保证了挖矿公平性。 比特币的总量 我们知道出块奖励是系统产生比特币的唯一途径,而这个奖励是每隔4年减半的,这样产生的比特币数量构成了一个几何序列(geometric series)。一开始的21W个区块能产生21W 50的比特币,接下来的区块能够产生21W 25,再往下就是21W12.5…… 总量就是21W 50 *(1+1/2+1/4+……)约等于2100W 不像寻找某个符合条件的质数这类数学难题,比特币求解puzzle除了比拼算力外并没有实际意义,比特币越来越难挖到是因为出块奖励被人为减少,比特币的稀缺性是人为造成的。虽然挖矿求解的puzzle本身没有实际意义,但是挖矿过程对于维护比特币系统的安全性是至关重要的,也叫做BitCoin is secured by mining。对于一个去中心化的没有membership控制的系统来说,挖矿提供了一种凭借算力投票的有效手段,只要大部分算力掌握在诚实的节点手里,系统的安全性就能得到保证,所以挖矿这一过程,表面上没有意义,但是这个机制对于维护系统安全性是非常有效的。 我们知道出块奖励每隔4年减半,是不是说挖矿的动力也会越来越小呢,从过去几年情况来看,恰恰相反,挖矿竞争越来越激烈,因为比特币的价格是飙升的。随着出块奖励越来越少,交易费也是越来越多的。 比特币的安全性 假设大部分算力是掌握在诚实的矿工手里,我们能得到什么样的安全保证?能不能保证写入区块的交易都是合法的?不能,挖矿只是概率上的保证,只能说有一个比较大的概率,下一个区块是诚实的矿工发布的,但是不能保证记账权不会落到有恶意的节点手里。比如,好的矿工占90%算力,坏的矿工占10%的算力,平均下来,10%的情况下,记账权会落到有恶意的节点手里。 恶意的节点掌握记账权 偷币 第一个问题,他能不能偷币?能不能把别人账上的钱转给自己? 不能,因为他不能伪造别人的签名,交易不合法。但是如果他强行把不合法的交易写进区块链里会出现什么情况?诚实的节点都不接受这个区块,他们会沿着上一个合法的区块继续添加区块,形成最长合法链,导致恶意节点写进去的区块作废,这样,恶意节点不仅没有偷到钱,还把出块奖励陪了。 双花 第二个问题,他能不能把已经花过的币再花一遍? 比如说M节点发布一个转账交易给A,现在他获得了记账权,又把钱转给自己,如果直接连在M→A的后面肯定是不合法的,因为很明显的double spending,凡是诚实的节点都不会接受这个区块,他要想发布交易就一定要插在M→A前面一个区块后面,也就是前面文章提到的分叉攻击。注意,区块插在什么位置,是要在刚开始挖矿就决定的,因为设置的block header要填上前一个区块的hash,所以M节点想插到这个位置,一开始就要把这个区块设置成前一个区块,而不是等获得了记账权再说。这种情况下会出现两个等长合法链,取决于其他节点沿着哪条链继续往下扩展,最后有一个会胜出,另一个就作废了。这种攻击有什么用?假如M是消费者,A是商家,M买了东西然后用比特币支付,M又发起一个交易,把钱转给自己,然后把下面的交易扩展成最长合法链,这样上面的区块就作废了,这种攻击的作用就是既买的了商品,又把钱收回来了,达到double spending attack的目的。 怎么防范这个问题?如果M→A这个交易不在最后一个区块,而是后面跟了几个区块,那么这种攻击的难度就会大大增加,要想回滚M→A这个交易还是得在他前面一个区块后面插入新区块,然后想办法让新区块所在分支成为最长合法链,这个难度非常大,因为诚实的节点不会沿着新区块往后发展,因为他不是最长合法链。这种情况相当于两条链在赛跑,如果大部分算力是掌握在诚实节点手里,这种攻击成功的可能性很小。所以一种防范的手段是多等几个区块或者叫确认(confirmation)。M→A这个交易刚刚写进区块的时候,称为one-confirmation,以此类图,它后面的第三个区块叫做three-confirmation。比特币协议规定,缺省情况下要等6个confirmation,到了six-confirmation时,才认为前面的交易是不可篡改的,也就是要等待10分钟(平均出块时间)*6 = 1小时。 我们知道区块链也被叫做不可篡改的分布式账本(irrevocable ledger),是不是说凡是写入区块链的内容就永远改不了呢?经过前面的分析我们可以知道,这种不可篡改性只是一种概率上的保证,刚刚写入区块链的内容,相对来说,还是比较容易被改掉的,经过一段等待时间之后,或者后面跟着好几个确认之后,这种被篡改的概率会指数级别下降。其实还有一种zero confirmation,这个意思是说,转账交易已经发布出去但是下一个区块还没有挖出来。拿电商购物的例子来说,相当于支付的时候发布一个转账交易,电商运行一个全节点或者委托某个全节点,监听区块链上的交易,收到转账交易后要验证交易的合法性,但是不用等到交易写到区块链里。这听起来风险很大,其实zero confirmation 在实际当中应用还是很普遍的,第一个原因是比特币协议缺省的设置是节点接受最先听到交易,两个交易有冲突,先听到哪个就接受哪个,所以zero confirmation处,M→A交易被节点收到,M→M交易较大概率不被诚实节点接受;第二个原因是很多购物网站从支付成功到发货,是有一定时间间隔的,天然有一定处理时间,比如说你要买个东西,在网上支付成功后,电商第二天才会发货,期间发现转账交易没有被写到最长合法链上,那么电商就可以取消发货。 故意不把某些合法交易写进区块链 第三个问题是他能不能故意不把某些合法交易写进区块链里? 比特币协议没有规定获得记账权的节点一定要把某些交易写进区块链里,但是出现这种情况问题也不大,因为总会有诚实的节点愿意发布这些交易。区块链正常情况下也会出现合法的交易没有被写进去的情况,可能就是这段时间交易数目太多了,比特币协议规定每个区块的大小是有限制的,最多不能超过1M,所以交易放不下了就得等到下一个区块。","categories":[{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"}],"tags":[{"name":"区块链技术与应用","slug":"区块链技术与应用","permalink":"https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E4%B8%8E%E5%BA%94%E7%94%A8/"},{"name":"比特币","slug":"比特币","permalink":"https://zunpan.github.io/tags/%E6%AF%94%E7%89%B9%E5%B8%81/"}]},{"title":"比特币协议","slug":"比特币协议","date":"2019-12-27T07:16:33.000Z","updated":"2023-09-24T04:27:40.282Z","comments":true,"path":"2019/12/27/比特币协议/","link":"","permalink":"https://zunpan.github.io/2019/12/27/%E6%AF%94%E7%89%B9%E5%B8%81%E5%8D%8F%E8%AE%AE/","excerpt":"","text":"不用区块链的数字货币的问题 假设央行不用区块链发行数字货币,央行可以对每个货币用私钥签名,然后交易的时候,交易双方可以用公钥去验证这个签名的正确性,正确即是央行发行的货币,可以交易,否则就是假货币。 此方案存在的问题是:货币的真伪性可以得到保证,但是这个数量无法控制。假如你现在有一个央行发行的货币,你虽然不能伪造但是可以复制,从而发生一钱多用的情况(花两次攻击,英文名:double spending attack) 改进方案:央行除了要对货币签名,还要给每个货币打上一个编号,然后维护一个数据库,这个编号的货币现在在谁手上。交易的时候,双方先用公钥验证真伪,然后准备收钱的一方访问一下央行数据库,这个货币现在是不是在你手上,在的话就可以继续交易,否则这个货币你是已经用过了,不能再用了。这样就解决了double spending attack问题。 改进方案存在的问题:这是中心化的方案,央行数据库压力很大。比特币系统解决的就是这种问题。 去中心化货币系统要解决的两个问题 数字货币的发行,谁有权发?什么时候发?发多少? 怎么验证交易的有效性?怎么防止double spending attack问题? 比特币的方案 问题1的解决方案 共识算法 问题2的解决方案 有一个用户通过共识算法获得了发行货币的权力,我们管他叫铸币权,他发行了10个比特币给了A,把这交易信息写入区块链中。然后A把比特币给了B和C,一人5个,这个交易需要A的签名,证明是A同意的,同时这个交易还要说明花掉的比特币是从哪来的,也就是下图的第二个交易要说明比特币从哪来的,这里是从铸币交易输出来的。比特币交易系统中都包含了输入和输出两部分,输入部分要说明币的来源,输出部分要给出收款人的公钥的hash,比如A给B转钱,要说明B的公钥的hash是什么。然后B再把钱转给C,2个和D,3个,同样要签名,以及说明币的来源。这时候C想转给E7个币,币的来源就有两个了。见下图 注意:上图有两种hash指针,一种就是前面一篇文章提到的连接区块的hash指针,把区块串起来构成一个链表,还有第二种指针,是为了说明币的来源,这样可以防范double spending attack:假如上图B还想给F转比特币,单纯验证签名,貌似合法,但是币从哪来呢?还是要从上图的第二个区块来,别的节点收到这个交易后,会进行查询,从这个交易一直回溯到币的来源,回溯过程中,发现B已经把币全给C和D了,所以交易不合法,这个交易不会写入区块链中。 A和B交易需要的信息 需要有A的签名和B的地址,比特币系统中收款方的地址是通过公钥推算出来的,比如B的地址是B的公钥取hash再进行一些转过生成的,这个地址相当于银行账号,A要给B转钱,需要知道B的银行账号。那么A怎么才能知道B的地址呢?比特币系统没有提供查询某个人的地址的方法,这得需要第三方的形式,比如说电商平台支持比特币支付,那么商家要把账户地址告诉电商平台,然后消费者就知道要给谁转账了。 另外需要知道A的地址,A要B的地址是为了知道给谁转账,B(其实是所有节点)要A的地址是为了①谁给B转的账②验证是否是A的签名(A的私钥签名,其他节点知道A的地址后就可以公钥验证)。 问题来了?怎么才能知道A的公钥呢? A的公钥是交易自己给出的,交易有输入和输出,输入不仅要说明币的来源还要说明A的公钥。 那么问题又来了,交易自己决定输入公钥,那不是有冒名顶替的风险吗? 前面提到输入要给出币的来源和付款人的hash,而输出要给出收款人的公钥的hash,那么上面A到B的交易的币是哪里来的呢?是前面铸币交易来的,来的同时要带上付款人的公钥的hash也就是前面铸币交易的收款人的公钥的hash。也就是说第二个交易的输入的公钥要和第一个交易的输出的公钥要一致。 在比特币系统当中,验证过程是通过脚本实现的,每个交易的输入是一段脚本,包括公钥,也是在脚本指定的,每个输出也是一段脚本,验证是否合法,就是把当前输入的脚本和币的来源的那个交易的输出的脚本拼在一起,如果能正常执行,那么就是合法的。 注意:上图对区块进行了简化,每个区块只包含了一个交易,实际上,一个区块可以有很多交易,所有交易构成了一个merkle tree。每个区块分为块头和块身两部分。 块头包含这个区块宏观的一些信息,比如说用的是比特币版本的哪个协议(version),还有区块链当中指向前一个区块的hash指针(hash of previous block header),还有整个merkle tree 的根hash值(Merkle root hash),还有两个域和挖矿相关的,一个是挖矿的难度目标阈值(target),一个是随机数nonce 比特币中的共识协议 一个节点一票 假设系统中大部分节点是诚实的,那么就投票,比如说某一个节点提出一个候选区块,根据收到的交易信息,判断哪些交易是合法的,然后把这些交易按顺序打包到候选区块里。候选区块发布给所有节点,每个节点收到区块后,检查一下,这里面的交易是不是合法的,如果都是合法的,投赞成票,否则投反对票。最后得票超过半数,候选区块写写入区块链中。 存在的问题 恶意节点不断提出有问题的区块,时间都浪费在了投票上面,区块链无法发展 没法强迫节点投票。如果都不投票,那么区块链就陷入了瘫痪。 效率上的问题。投票等多久决定于网络延迟 任何基于投票的方案都要先决定谁有投票权(membership),这是区块链最大的问题。如果区块链的成员有严格定义,比如说有一种区块链叫联盟链(hyperledger fabric),只有符合条件的大公司才能加入,这种方案就可行。但是加入比特币系统是很容易的。 根据算力投票 比特币系统是根据计算力投票的。每个节点都可以在本地组装出一个候选区块,把它认为是合法的交易放在这个区块里,然后尝试各种nonce值,即计算H(block header)<=target,nonce是个4 bytes的数。如果某个节点找到了nonce,就获得了记账权,就是往比特币去中心化的账本中写入下一个区块的权力,只有获得了记账权的节点才有权力发布下一个区块。其他节点收到区块后验证区块的合法性,比如block header里面的各个域是否正确,它里面的nBits域(时间上是target的一个编码),检查一下nBits设置的是不是符合比特币系统规定的难度要求;然后检查一下nonce是不是使得整个H(block header)小于等于target。然后验证一下block body里面的交易是不是合法的,第一要有合法签名,第二没有用过。 假设一个区块经过检查是合法的,那么有没有可能节点不接受它呢? 恶意节点不接受合法区块 假设一个获得记账权的节点发布一个合法的区块,如下图,插在了区块链的中间。这个区块是完全合法的,那么我们应不应该接受它? 首先说明这个合法区块为什么插在了中间?因为每一个区块都带了一个指向前面一个区块的hash指针(hash of prev block header)。那么插在这里有什么问题?见下图 假如上方这个区块有一个交易是A把钱给了B,而我们新插入的这个区块里有个交易是A把钱给了自己。这里要说明这种情况不会发生double spending。我们判断是不是发生了double spending 是判断该区块所在分支有没有发生,上方的区块和新插入的区块已经是不同分支了,所以不是double spending。这种情况相当于把A→B这个交易给回滚了。 那么我们到底要不要接受这个新区块呢?不接受,因为它不在最长合法链上(longest valid chain) 比特币协议中规定接受的区块应该是在扩展最长合法链。上图这个例子是分叉攻击的例子(forking attack),通过往区块链中插入一个区块,来回滚已经发生的交易。 但是比特币系统在正常情况下也可能出现分叉。比如有两个节点同时获得了记账权,发布区块,这个时候会出现下图这种情况 有两条最长合法链。那该接受哪一个?比特币协议中,缺省情况下,每个节点接受最早收到的那一个,由于网络延迟,有些节点可能接受1,有些节点可能接受2。什么叫接受?如果节点在1后面继续扩展,说明节点接受了1。所以如果出现系统中两个节点差不多同时发布区块的情况,这种等长的临时性的分叉会维持一段时间,直到一个分叉胜出。上图例子中,假设1区块抢先找到了下一个区块,那么上面的分叉就成了最长合法链,下面的分叉就成了orphan block,orphan block 铸币交易产生的比特币都会失效。为什么要争夺记账权?获得记账权的节点有一定权力,它可以决定哪些交易可以写到区块里,但是设计协议的时候不应该让它成为争夺记账权的主要动力,因为我们希望凡是合法的交易都应该被写到区块链里,而不是取得记账权的节点决定是否写入。比特币系统提供的出块奖励(block reward)成为争夺记账权的动力,比特币协议规定获得记账权的节点在发布的区块里可以有一个特殊的交易,就是前面提到的铸币交易(coinbase transaction),可以发布一定数量的比特币(初始50个,每21万个区块之后减半)。这样就解决了前文提到的去中心化货币系统要解决的第一个问题。铸币交易是比特币系统中发行比特币的唯一方法,其他所有交易都是比特币的转移。 所以比特币系统要取得的共识到底是什么?分布式hash表要取得的共识是hash表的内容,比特币系统中要取得的共识是去中心化账本里的内容。谁来决定写这个账本?取得记账权的节点写账本,所以比特币系统中的共识机制是根据算力(每秒钟能试多少个nonce,也成为hash rate)来投票的,hash rate越高,得到记账权和得到比特币的概率就越高。那么这种共识机制是如何避免女巫攻击的,因为不管创建多少账户,节点的hash rate 都不会改变,所以获得记账权和比特币的概率不会提高。","categories":[{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"}],"tags":[{"name":"区块链技术与应用","slug":"区块链技术与应用","permalink":"https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E4%B8%8E%E5%BA%94%E7%94%A8/"},{"name":"比特币","slug":"比特币","permalink":"https://zunpan.github.io/tags/%E6%AF%94%E7%89%B9%E5%B8%81/"}]},{"title":"比特币的数据结构","slug":"比特币的数据结构","date":"2019-12-26T11:43:07.000Z","updated":"2023-09-24T04:27:40.284Z","comments":true,"path":"2019/12/26/比特币的数据结构/","link":"","permalink":"https://zunpan.github.io/2019/12/26/%E6%AF%94%E7%89%B9%E5%B8%81%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/","excerpt":"","text":"比特币有两种数据结构,一个是区块链,一个是Merkle Tree 区块链 传统意义上的指针,保存了所指向结构体在内存中的起始位置,而hash指针除了存地址之外,还要保存这个结构体的hash值,一般用H表示这hash指针,好处是不光知道这个结构体的地址,还可以知道这个结构体有没有发生改变。 比特币中最基本的数据结构就是区块链。区块链是什么,就是一个一个区块组成的链表,区块链和普通的链表有什么区别?一个区别就是hash指针代替了普通的指针。 每个区块包含一个前一个区块的hash指针,这个指针是对前一个区块整个信息进行hash出来的,包括前一个区块的hash指针,通过这种数据结构可以实现tamper-evident log(防篡改)。因为,一旦改了某个区块的信息,那么它后面的区块的hash指针就要变化,后面的区块的hash指针一变,后面的后面的hash指针也要跟着变,所以这个数据结构的好处是,只要我们保存最后一个hash指针,那么区块链上任意一个区块发生改变,我们都可以知道 Merkle Tree 比特币的另外一个结构是Merkle Tree,和二叉树类似,不过父节点保存的是左右孩子节点的hash指针 比特币当中是用hash指针将各个区块连接在一起,每个区块的交易是组织成一个Merkle tree 的形式,上图底下的数据块其实就是一个个交易。每个区块分为两部分,块头和块身。block header包含root hash,但是没有交易的具体内容,block body 有交易内容。 Merkle Proff 比特币中的节点分为两类,一类是全节点,一类是轻节点,全节点保存整个区块的内容(即有block header和block body),轻节点只保存block header。轻节点不包含block body,但是交易信息是在block body里面的,那全节点如何向轻节点证明交易确实发生了呢? 假设某个轻节点想要知道黄色这个交易是不是包含在了这个merkle tree里面了,轻节点没有这颗merkle tree,只有一个根hash值,轻节点向某个全节点发出请求,请求一个能够证明黄色交易被包含在这个merkle tree里面的merkle proof ,全节点收到请求之后,只要把途中标为红色的这三个hash值发给这个轻节点就行,有了这三个hash值,轻节点可以在本地计算出图中标为绿色的hash值,首先算出黄色交易的hash值,即图中最下面的绿色的hash值,然后和右边的红色hash值组合计算出上一个绿色的hash值,同理可以算出整个树的根hash值,轻节点把这个hash值和block header里面的比较,就可以知道这个交易是不是在这个merkle tree里面。 这个是用merkle proof证明某个交易在merkle tree里面,那么如何证明某个交易不在merkle tree里面呢? 一种方法是把整个merkle tree发给别人,别人验证正确后,发现没有要找的交易,即交易不在merkle tree里面。 另外一种方法是对交易根据hash排个序,然后把要找的交易也hash一下,然后二分查找,如果找到了,merkle proof证明它确实在里面,不存在证明失败;如果没找到,对相邻的两个交易进行merkle proof,如果证明成功,说明它俩是紧邻的,也即我们要找的交易不在里面,因为如果在里面,它俩就不是紧邻的了,不存在证明成功。","categories":[{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"}],"tags":[{"name":"区块链技术与应用","slug":"区块链技术与应用","permalink":"https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E4%B8%8E%E5%BA%94%E7%94%A8/"},{"name":"比特币","slug":"比特币","permalink":"https://zunpan.github.io/tags/%E6%AF%94%E7%89%B9%E5%B8%81/"}]},{"title":"比特币的密码学原理","slug":"比特币的密码学原理","date":"2019-12-26T11:41:33.000Z","updated":"2023-09-24T04:27:40.284Z","comments":true,"path":"2019/12/26/比特币的密码学原理/","link":"","permalink":"https://zunpan.github.io/2019/12/26/%E6%AF%94%E7%89%B9%E5%B8%81%E7%9A%84%E5%AF%86%E7%A0%81%E5%AD%A6%E5%8E%9F%E7%90%86/","excerpt":"","text":"前言:这一系列的文章是看肖臻老师《区块链技术与应用》公开课做的笔记 比特币用到了密码学的两个功能,一个是哈希,一个是签名 哈希 函数 对于集合A中的任意一个数x,在集合B中都有唯一确定的数y与之对应 要注意: 集合A中的元素a,都能找到有且仅有一个集合B中的数与之对应;即不存在f(a)=b1,又有f(a)=b2的情况。但是,存在f(a1)=b,同时又有f(a2)=b的情况,如偶函数y=x2 哈希函数 本质是一个函数,但是特殊在输入值是任意字符串,输出值是固定长度的字符串,比如比特币用的SHA256,是将输入的字符串计算成64位十六进制数(256位二进制数) 比特币用到的哈希函数的三个特性 collision resistance(抗碰撞性) 什么是碰撞? 对于函数f(x) ,如果存在a!=b,使得f(a)==f(b),那么就称为碰撞 什么是抗碰撞性? 除了暴力枚举输入空间,很难找到会发生碰撞的两个输入值 抗碰撞性的作用 上传文件之前可以对文件计算一个hash值,下次下载下来,再计算一个hash值,如果两个hash值不同,则说明被文件篡改了。因为抗碰撞性使得修改后的文件的计算出来的hash值和原来的文件极大概率是不同的。 hiding property(隐藏性) 光给你一串hash值,除了暴力求解,你是很难知道加密前的内容是什么,这就叫隐藏性。但是hiding成立的前提是输入空间无限大,而且输入的分布比较均匀,各种取值的可能性是差不多的,所以暴力求解也是不可行的。 隐藏性的作用 假如有一个人说他预测股票很准,怎么验证他说的准不准呢?如果他公布预测结果,可能会有人故意和他反着来,操纵股市导致预测不准,所以预测结果不能提前公布。那么大家又是如何知道他预测准不准呢?这个时候可以先让这个预测的人将预测结果取个hash值公布出来,由于隐藏性,光是知道hash值很难知道加密前的内容,这样就不会影响股市。然后收盘之后,让这个预测的人把预测结果公布出来,由于抗碰撞性,预测的人是无法篡改预测结果的,要是改了的话就和当初公布的hash对不上,这样就起到了一个sealed envelope 的作用。但是这个例子有一个问题,就是股票的数量是非常有限的,不满足隐藏性成立的前提条件,所以实际操作会在预测结果后面加一串随机数,然后整个取hash puzzle friendly 光是看输入,很难猜出来hash值是什么样的,所以你想让hash值落在某个范围之内,那只能暴力枚举了。比如想得出hash值前面k位数都是0,怎么得出呢?没有办法,不能事先知道,只能一个一个去试,所以叫puzzle friendly ,挖矿的过程就是计算H(block header)<=target。 block header是区块的信息,其中包括一个nonce(随机数),我们所要做的就是不断就试这个nonce,使得hash值落在一个target space中,所以这个挖矿的过程没有捷径,只能不断去试nonce,所以这个才可以作为工作量证明。 签名 日常生活中想要开一个银行账户,怎么开,带上证件去银行。 那比特币怎么开账户呢?很简单,只需要一对公钥和私钥,每个用户自己决定开不开户。在本地中创建一个公钥私钥对就完成了开户。 公钥和私钥来自非对称加密。 什么是对称加密? 双方在进行信息交换的时候用同一个密钥对内容进行加密/解密,那么这个密钥就一定要在网络中传输,但是网络是有可能被窃听的,所以对称加密并不安全。 什么是非对称加密? 双方都有一对公钥和私钥,我给你发信息时,我用你的公钥对内容进行加密,然后你用你的私钥对信息解密,同理你给我发信息时得用我的公钥对信息进行加密,然后我用我的私钥对你发来的信息解密。公钥不需要保密,但是私钥需要保密。解决了对称加密的密钥分发的安全问题。 在比特币系统中,非对称加密的真正用途其实是签名,而不是信息传输,何谓签名?比如我想发起一笔交易,别人怎么知道是我发起的而不是别人偷偷的替我发起的呢,这就需要我在发布交易的时候用我的私钥进行签名(先对交易信息进行hash),然后别人用我的公钥去验证这个签名的正确性,如果不正确,说明不是我本人发起的。 所以总的来说,非对称加密算法的作用是:公钥加密,私钥解密; 私钥签名,公钥验证。 公钥私钥对重复问题 既然公钥私钥对始终是在本地产生,那么产生重复怎么办? 如大量生成公钥私钥对,然后去对比产生的公钥是不是和区块链上已有的某个公钥相同。如果相同,那么就可以用对应的私钥把账户上的钱转走。 这种方式理论上可行,但是实际上没法做,因为产生重复的公钥私钥对的概率非常小,可以忽略不计。到目前为止还没有能用这种方法攻击成功的先例。 a good source of randness 生成公钥私钥的过程是随机的,但要求选取一个好的随机源,否则前面的分析就不成立了(就有不足够小的可能生成重复的公钥私钥对)。 比特币中使用的签名算法不仅在生成公钥私钥对时有好的随机源,在之后每次签名的时候也要有好的随机源。如果签名时使用的随机源不好,就有可能泄露私钥。","categories":[{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"}],"tags":[{"name":"区块链技术与应用","slug":"区块链技术与应用","permalink":"https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E4%B8%8E%E5%BA%94%E7%94%A8/"},{"name":"比特币","slug":"比特币","permalink":"https://zunpan.github.io/tags/%E6%AF%94%E7%89%B9%E5%B8%81/"}]}],"categories":[{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/categories/Java/"},{"name":"MySQL","slug":"MySQL","permalink":"https://zunpan.github.io/categories/MySQL/"},{"name":"杂项","slug":"杂项","permalink":"https://zunpan.github.io/categories/%E6%9D%82%E9%A1%B9/"},{"name":"基础知识","slug":"基础知识","permalink":"https://zunpan.github.io/categories/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"},{"name":"密码学","slug":"密码学","permalink":"https://zunpan.github.io/categories/%E5%AF%86%E7%A0%81%E5%AD%A6/"},{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"},{"name":"Linux","slug":"Linux","permalink":"https://zunpan.github.io/categories/Linux/"},{"name":"Maven","slug":"Maven","permalink":"https://zunpan.github.io/categories/Maven/"},{"name":"算法","slug":"算法","permalink":"https://zunpan.github.io/categories/%E7%AE%97%E6%B3%95/"},{"name":"Git","slug":"Git","permalink":"https://zunpan.github.io/categories/Git/"}],"tags":[{"name":"深入理解Java虚拟机","slug":"深入理解Java虚拟机","permalink":"https://zunpan.github.io/tags/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA/"},{"name":"LCS","slug":"LCS","permalink":"https://zunpan.github.io/tags/LCS/"},{"name":"Levenshtein","slug":"Levenshtein","permalink":"https://zunpan.github.io/tags/Levenshtein/"},{"name":"编辑距离","slug":"编辑距离","permalink":"https://zunpan.github.io/tags/%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB/"},{"name":"diff","slug":"diff","permalink":"https://zunpan.github.io/tags/diff/"},{"name":"merge","slug":"merge","permalink":"https://zunpan.github.io/tags/merge/"},{"name":"MySQL","slug":"MySQL","permalink":"https://zunpan.github.io/tags/MySQL/"},{"name":"数据库","slug":"数据库","permalink":"https://zunpan.github.io/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"},{"name":"Java并发编程实战","slug":"Java并发编程实战","permalink":"https://zunpan.github.io/tags/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E6%88%98/"},{"name":"Java Concurrency In Practice","slug":"Java-Concurrency-In-Practice","permalink":"https://zunpan.github.io/tags/Java-Concurrency-In-Practice/"},{"name":"实习","slug":"实习","permalink":"https://zunpan.github.io/tags/%E5%AE%9E%E4%B9%A0/"},{"name":"Effective-Java","slug":"Effective-Java","permalink":"https://zunpan.github.io/tags/Effective-Java/"},{"name":"异常","slug":"异常","permalink":"https://zunpan.github.io/tags/%E5%BC%82%E5%B8%B8/"},{"name":"通用程序设计","slug":"通用程序设计","permalink":"https://zunpan.github.io/tags/%E9%80%9A%E7%94%A8%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/"},{"name":"方法","slug":"方法","permalink":"https://zunpan.github.io/tags/%E6%96%B9%E6%B3%95/"},{"name":"lambda表达式","slug":"lambda表达式","permalink":"https://zunpan.github.io/tags/lambda%E8%A1%A8%E8%BE%BE%E5%BC%8F/"},{"name":"Stream","slug":"Stream","permalink":"https://zunpan.github.io/tags/Stream/"},{"name":"枚举","slug":"枚举","permalink":"https://zunpan.github.io/tags/%E6%9E%9A%E4%B8%BE/"},{"name":"注解","slug":"注解","permalink":"https://zunpan.github.io/tags/%E6%B3%A8%E8%A7%A3/"},{"name":"泛型","slug":"泛型","permalink":"https://zunpan.github.io/tags/%E6%B3%9B%E5%9E%8B/"},{"name":"类和接口","slug":"类和接口","permalink":"https://zunpan.github.io/tags/%E7%B1%BB%E5%92%8C%E6%8E%A5%E5%8F%A3/"},{"name":"对象的通用方法","slug":"对象的通用方法","permalink":"https://zunpan.github.io/tags/%E5%AF%B9%E8%B1%A1%E7%9A%84%E9%80%9A%E7%94%A8%E6%96%B9%E6%B3%95/"},{"name":"创建和销毁对象","slug":"创建和销毁对象","permalink":"https://zunpan.github.io/tags/%E5%88%9B%E5%BB%BA%E5%92%8C%E9%94%80%E6%AF%81%E5%AF%B9%E8%B1%A1/"},{"name":"CORS","slug":"CORS","permalink":"https://zunpan.github.io/tags/CORS/"},{"name":"Java","slug":"Java","permalink":"https://zunpan.github.io/tags/Java/"},{"name":"密码学","slug":"密码学","permalink":"https://zunpan.github.io/tags/%E5%AF%86%E7%A0%81%E5%AD%A6/"},{"name":"Cryptography","slug":"Cryptography","permalink":"https://zunpan.github.io/tags/Cryptography/"},{"name":"Dan Boneh","slug":"Dan-Boneh","permalink":"https://zunpan.github.io/tags/Dan-Boneh/"},{"name":"面试","slug":"面试","permalink":"https://zunpan.github.io/tags/%E9%9D%A2%E8%AF%95/"},{"name":"区块链","slug":"区块链","permalink":"https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"},{"name":"电子选举","slug":"电子选举","permalink":"https://zunpan.github.io/tags/%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE/"},{"name":"隐私计算","slug":"隐私计算","permalink":"https://zunpan.github.io/tags/%E9%9A%90%E7%A7%81%E8%AE%A1%E7%AE%97/"},{"name":"Linux","slug":"Linux","permalink":"https://zunpan.github.io/tags/Linux/"},{"name":"Shell","slug":"Shell","permalink":"https://zunpan.github.io/tags/Shell/"},{"name":"Maven","slug":"Maven","permalink":"https://zunpan.github.io/tags/Maven/"},{"name":"LeetCode","slug":"LeetCode","permalink":"https://zunpan.github.io/tags/LeetCode/"},{"name":"cpp","slug":"cpp","permalink":"https://zunpan.github.io/tags/cpp/"},{"name":"algorithm","slug":"algorithm","permalink":"https://zunpan.github.io/tags/algorithm/"},{"name":"Git","slug":"Git","permalink":"https://zunpan.github.io/tags/Git/"},{"name":"MetaMask","slug":"MetaMask","permalink":"https://zunpan.github.io/tags/MetaMask/"},{"name":"truffle","slug":"truffle","permalink":"https://zunpan.github.io/tags/truffle/"},{"name":"转账","slug":"转账","permalink":"https://zunpan.github.io/tags/%E8%BD%AC%E8%B4%A6/"},{"name":"ganache","slug":"ganache","permalink":"https://zunpan.github.io/tags/ganache/"},{"name":"区块链技术与应用","slug":"区块链技术与应用","permalink":"https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E4%B8%8E%E5%BA%94%E7%94%A8/"},{"name":"以太坊","slug":"以太坊","permalink":"https://zunpan.github.io/tags/%E4%BB%A5%E5%A4%AA%E5%9D%8A/"},{"name":"比特币","slug":"比特币","permalink":"https://zunpan.github.io/tags/%E6%AF%94%E7%89%B9%E5%B8%81/"}]} \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..689134a --- /dev/null +++ b/css/style.css @@ -0,0 +1,9213 @@ +@charset "UTF-8"; + +/** + * global variables and mixins + */ + +/** + * font family + */ + +/** + * + */ + +/* Mixins */ + +/** + * bootstrap + */ + +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ + +html { + font-family: sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; +} + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} + +audio:not([controls]) { + display: none; + height: 0; +} + +[hidden], +template { + display: none; +} + +a { + background-color: transparent; +} + +a:active, +a:hover { + outline: 0; +} + +abbr[title] { + border-bottom: 1px dotted; +} + +b, +strong { + font-weight: bold; +} + +dfn { + font-style: italic; +} + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +mark { + background: #ff0; + color: #000; +} + +small { + font-size: 80%; +} + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + border: 0; +} + +svg:not(:root) { + overflow: hidden; +} + +figure { + margin: 1em 40px; +} + +hr { + -webkit-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +pre { + overflow: auto; +} + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; +} + +button { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} + +button[disabled], +html input[disabled] { + cursor: default; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +input { + line-height: normal; +} + +input[type="checkbox"], +input[type="radio"] { + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +input[type="search"] { + -webkit-appearance: textfield; + -webkit-box-sizing: content-box; + box-sizing: content-box; +} + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +legend { + border: 0; + padding: 0; +} + +textarea { + overflow: auto; +} + +optgroup { + font-weight: bold; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} + +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ + +* { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +*:before, +*:after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +html { + font-size: 10px; + -webkit-tap-highlight-color: transparent; +} + +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +a { + color: #2196f3; + text-decoration: none; +} + +a:hover, +a:focus { + color: #0a6ebd; + text-decoration: underline; +} + +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +figure { + margin: 0; +} + +img { + vertical-align: middle; +} + +.img-responsive { + display: block; + max-width: 100%; + height: auto; +} + +.img-rounded { + border-radius: 6px; +} + +.img-thumbnail { + padding: 4px; + line-height: 1.57143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + display: inline-block; + max-width: 100%; + height: auto; +} + +.img-circle { + border-radius: 50%; +} + +hr { + margin-top: 21px; + margin-bottom: 21px; + border: 0; + border-top: 1px solid #eeeeee; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} + +[role="button"] { + cursor: pointer; +} + +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} + +h1 small, +h1 .small, +h2 small, +h2 .small, +h3 small, +h3 .small, +h4 small, +h4 .small, +h5 small, +h5 .small, +h6 small, +h6 .small, +.h1 small, +.h1 .small, +.h2 small, +.h2 .small, +.h3 small, +.h3 .small, +.h4 small, +.h4 .small, +.h5 small, +.h5 .small, +.h6 small, +.h6 .small { + font-weight: normal; + line-height: 1; + color: #777777; +} + +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 21px; + margin-bottom: 10.5px; +} + +h1 small, +h1 .small, +.h1 small, +.h1 .small, +h2 small, +h2 .small, +.h2 small, +.h2 .small, +h3 small, +h3 .small, +.h3 small, +.h3 .small { + font-size: 65%; +} + +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10.5px; + margin-bottom: 10.5px; +} + +h4 small, +h4 .small, +.h4 small, +.h4 .small, +h5 small, +h5 .small, +.h5 small, +.h5 .small, +h6 small, +h6 .small, +.h6 small, +.h6 .small { + font-size: 75%; +} + +h1, +.h1 { + font-size: 30px; +} + +h2, +.h2 { + font-size: 26px; +} + +h3, +.h3 { + font-size: 22px; +} + +h4, +.h4 { + font-size: 18px; +} + +h5, +.h5 { + font-size: 14px; +} + +h6, +.h6 { + font-size: 12px; +} + +p { + margin: 0 0 10.5px; +} + +.lead { + margin-bottom: 21px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} + +small, +.small { + font-size: 85%; +} + +mark, +.mark { + background-color: #fcf8e3; + padding: .2em; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-center { + text-align: center; +} + +.text-justify { + text-align: justify; +} + +.text-nowrap { + white-space: nowrap; +} + +.text-lowercase { + text-transform: lowercase; +} + +.text-uppercase, +.initialism { + text-transform: uppercase; +} + +.text-capitalize { + text-transform: capitalize; +} + +.text-muted { + color: #777777 !important; +} + +.text-primary { + color: #2196f3; +} + +a.text-primary:hover, +a.text-primary:focus { + color: #0c7cd5; +} + +.text-success { + color: #3c763d; +} + +a.text-success:hover, +a.text-success:focus { + color: #2b542c; +} + +.text-info { + color: #31708f; +} + +a.text-info:hover, +a.text-info:focus { + color: #245269; +} + +.text-warning { + color: #8a6d3b; +} + +a.text-warning:hover, +a.text-warning:focus { + color: #66512c; +} + +.text-danger { + color: #a94442; +} + +a.text-danger:hover, +a.text-danger:focus { + color: #843534; +} + +.bg-primary { + color: #fff; +} + +.bg-primary { + background-color: #2196f3; +} + +a.bg-primary:hover, +a.bg-primary:focus { + background-color: #0c7cd5; +} + +.bg-success { + background-color: #dff0d8; +} + +a.bg-success:hover, +a.bg-success:focus { + background-color: #c1e2b3; +} + +.bg-info { + background-color: #d9edf7; +} + +a.bg-info:hover, +a.bg-info:focus { + background-color: #afd9ee; +} + +.bg-warning { + background-color: #fcf8e3; +} + +a.bg-warning:hover, +a.bg-warning:focus { + background-color: #f7ecb5; +} + +.bg-danger { + background-color: #f2dede; +} + +a.bg-danger:hover, +a.bg-danger:focus { + background-color: #e4b9b9; +} + +.page-header { + padding-bottom: 9.5px; + margin: 42px 0 21px; + border-bottom: 1px solid #eeeeee; +} + +ul, +ol { + margin-top: 0; + margin-bottom: 10.5px; +} + +ul ul, +ul ol, +ol ul, +ol ol { + margin-bottom: 0; +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + list-style: none; + margin-left: -5px; +} + +.list-inline>li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; +} + +dl { + margin-top: 0; + margin-bottom: 21px; +} + +dt, +dd { + line-height: 1.57143; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 0; +} + +.dl-horizontal dd:before, +.dl-horizontal dd:after { + content: " "; + display: table; +} + +.dl-horizontal dd:after { + clear: both; +} + +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #777777; +} + +.initialism { + font-size: 90%; +} + +blockquote { + padding: 10.5px 16px; + margin: 0 0 21px; + border: 1px dotted #eeeeee; + border-left: 3px solid #eeeeee; + background-color: #fbfbfb; +} + +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} + +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.57143; + color: #777777; +} + +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; + text-align: right; +} + +.blockquote-reverse footer:before, +.blockquote-reverse small:before, +.blockquote-reverse .small:before, +blockquote.pull-right footer:before, +blockquote.pull-right small:before, +blockquote.pull-right .small:before { + content: ''; +} + +.blockquote-reverse footer:after, +.blockquote-reverse small:after, +.blockquote-reverse .small:after, +blockquote.pull-right footer:after, +blockquote.pull-right small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +address { + margin-bottom: 21px; + font-style: normal; + line-height: 1.57143; +} + +.container { + margin-right: auto; + margin-left: auto; + padding-left: 15px; + padding-right: 15px; +} + +.container:before, +.container:after { + content: " "; + display: table; +} + +.container:after { + clear: both; +} + +.container-fluid { + margin-right: auto; + margin-left: auto; + padding-left: 15px; + padding-right: 15px; +} + +.container-fluid:before, +.container-fluid:after { + content: " "; + display: table; +} + +.container-fluid:after { + clear: both; +} + +.row { + margin-left: -15px; + margin-right: -15px; +} + +.row:before, +.row:after { + content: " "; + display: table; +} + +.row:after { + clear: both; +} + +.col-xs-1, +.col-sm-1, +.col-md-1, +.col-lg-1, +.col-xs-2, +.col-sm-2, +.col-md-2, +.col-lg-2, +.col-xs-3, +.col-sm-3, +.col-md-3, +.col-lg-3, +.col-xs-4, +.col-sm-4, +.col-md-4, +.col-lg-4, +.col-xs-5, +.col-sm-5, +.col-md-5, +.col-lg-5, +.col-xs-6, +.col-sm-6, +.col-md-6, +.col-lg-6, +.col-xs-7, +.col-sm-7, +.col-md-7, +.col-lg-7, +.col-xs-8, +.col-sm-8, +.col-md-8, +.col-lg-8, +.col-xs-9, +.col-sm-9, +.col-md-9, +.col-lg-9, +.col-xs-10, +.col-sm-10, +.col-md-10, +.col-lg-10, +.col-xs-11, +.col-sm-11, +.col-md-11, +.col-lg-11, +.col-xs-12, +.col-sm-12, +.col-md-12, +.col-lg-12 { + position: relative; + min-height: 1px; + padding-left: 15px; + padding-right: 15px; +} + +.col-xs-1, +.col-xs-2, +.col-xs-3, +.col-xs-4, +.col-xs-5, +.col-xs-6, +.col-xs-7, +.col-xs-8, +.col-xs-9, +.col-xs-10, +.col-xs-11, +.col-xs-12 { + float: left; +} + +.col-xs-1 { + width: 8.33333%; +} + +.col-xs-2 { + width: 16.66667%; +} + +.col-xs-3 { + width: 25%; +} + +.col-xs-4 { + width: 33.33333%; +} + +.col-xs-5 { + width: 41.66667%; +} + +.col-xs-6 { + width: 50%; +} + +.col-xs-7 { + width: 58.33333%; +} + +.col-xs-8 { + width: 66.66667%; +} + +.col-xs-9 { + width: 75%; +} + +.col-xs-10 { + width: 83.33333%; +} + +.col-xs-11 { + width: 91.66667%; +} + +.col-xs-12 { + width: 100%; +} + +.col-xs-pull-0 { + right: auto; +} + +.col-xs-pull-1 { + right: 8.33333%; +} + +.col-xs-pull-2 { + right: 16.66667%; +} + +.col-xs-pull-3 { + right: 25%; +} + +.col-xs-pull-4 { + right: 33.33333%; +} + +.col-xs-pull-5 { + right: 41.66667%; +} + +.col-xs-pull-6 { + right: 50%; +} + +.col-xs-pull-7 { + right: 58.33333%; +} + +.col-xs-pull-8 { + right: 66.66667%; +} + +.col-xs-pull-9 { + right: 75%; +} + +.col-xs-pull-10 { + right: 83.33333%; +} + +.col-xs-pull-11 { + right: 91.66667%; +} + +.col-xs-pull-12 { + right: 100%; +} + +.col-xs-push-0 { + left: auto; +} + +.col-xs-push-1 { + left: 8.33333%; +} + +.col-xs-push-2 { + left: 16.66667%; +} + +.col-xs-push-3 { + left: 25%; +} + +.col-xs-push-4 { + left: 33.33333%; +} + +.col-xs-push-5 { + left: 41.66667%; +} + +.col-xs-push-6 { + left: 50%; +} + +.col-xs-push-7 { + left: 58.33333%; +} + +.col-xs-push-8 { + left: 66.66667%; +} + +.col-xs-push-9 { + left: 75%; +} + +.col-xs-push-10 { + left: 83.33333%; +} + +.col-xs-push-11 { + left: 91.66667%; +} + +.col-xs-push-12 { + left: 100%; +} + +.col-xs-offset-0 { + margin-left: 0%; +} + +.col-xs-offset-1 { + margin-left: 8.33333%; +} + +.col-xs-offset-2 { + margin-left: 16.66667%; +} + +.col-xs-offset-3 { + margin-left: 25%; +} + +.col-xs-offset-4 { + margin-left: 33.33333%; +} + +.col-xs-offset-5 { + margin-left: 41.66667%; +} + +.col-xs-offset-6 { + margin-left: 50%; +} + +.col-xs-offset-7 { + margin-left: 58.33333%; +} + +.col-xs-offset-8 { + margin-left: 66.66667%; +} + +.col-xs-offset-9 { + margin-left: 75%; +} + +.col-xs-offset-10 { + margin-left: 83.33333%; +} + +.col-xs-offset-11 { + margin-left: 91.66667%; +} + +.col-xs-offset-12 { + margin-left: 100%; +} + +.btn { + display: inline-block; + margin-bottom: 0; + font-weight: normal; + text-align: center; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + white-space: nowrap; + padding: 6px 12px; + font-size: 14px; + line-height: 1.57143; + border-radius: 4px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.btn:focus, +.btn.focus, +.btn:active:focus, +.btn:active.focus, +.btn.active:focus, +.btn.active.focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn:hover, +.btn:focus, +.btn.focus { + color: #333; + text-decoration: none; +} + +.btn:active, +.btn.active { + outline: 0; + background-image: none; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} + +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + cursor: not-allowed; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; +} + +a.btn.disabled, +fieldset[disabled] a.btn { + pointer-events: none; +} + +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} + +.btn-default:focus, +.btn-default.focus { + color: #333; + background-color: #e6e6e6; + border-color: #8c8c8c; +} + +.btn-default:hover { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} + +.btn-default:active, +.btn-default.active, +.open>.btn-default.dropdown-toggle { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} + +.btn-default:active:hover, +.btn-default:active:focus, +.btn-default:active.focus, +.btn-default.active:hover, +.btn-default.active:focus, +.btn-default.active.focus, +.open>.btn-default.dropdown-toggle:hover, +.open>.btn-default.dropdown-toggle:focus, +.open>.btn-default.dropdown-toggle.focus { + color: #333; + background-color: #d4d4d4; + border-color: #8c8c8c; +} + +.btn-default:active, +.btn-default.active, +.open>.btn-default.dropdown-toggle { + background-image: none; +} + +.btn-default.disabled:hover, +.btn-default.disabled:focus, +.btn-default.disabled.focus, +.btn-default[disabled]:hover, +.btn-default[disabled]:focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default:hover, +fieldset[disabled] .btn-default:focus, +fieldset[disabled] .btn-default.focus { + background-color: #fff; + border-color: #ccc; +} + +.btn-default .badge { + color: #fff; + background-color: #333; +} + +.btn-primary { + color: #fff; + background-color: #2196f3; + border-color: #0d8aee; +} + +.btn-primary:focus, +.btn-primary.focus { + color: #fff; + background-color: #0c7cd5; + border-color: #064475; +} + +.btn-primary:hover { + color: #fff; + background-color: #0c7cd5; + border-color: #0a68b4; +} + +.btn-primary:active, +.btn-primary.active, +.open>.btn-primary.dropdown-toggle { + color: #fff; + background-color: #0c7cd5; + border-color: #0a68b4; +} + +.btn-primary:active:hover, +.btn-primary:active:focus, +.btn-primary:active.focus, +.btn-primary.active:hover, +.btn-primary.active:focus, +.btn-primary.active.focus, +.open>.btn-primary.dropdown-toggle:hover, +.open>.btn-primary.dropdown-toggle:focus, +.open>.btn-primary.dropdown-toggle.focus { + color: #fff; + background-color: #0a68b4; + border-color: #064475; +} + +.btn-primary:active, +.btn-primary.active, +.open>.btn-primary.dropdown-toggle { + background-image: none; +} + +.btn-primary.disabled:hover, +.btn-primary.disabled:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled]:hover, +.btn-primary[disabled]:focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary:hover, +fieldset[disabled] .btn-primary:focus, +fieldset[disabled] .btn-primary.focus { + background-color: #2196f3; + border-color: #0d8aee; +} + +.btn-primary .badge { + color: #2196f3; + background-color: #fff; +} + +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-success:focus, +.btn-success.focus { + color: #fff; + background-color: #449d44; + border-color: #255625; +} + +.btn-success:hover { + color: #fff; + background-color: #449d44; + border-color: #398439; +} + +.btn-success:active, +.btn-success.active, +.open>.btn-success.dropdown-toggle { + color: #fff; + background-color: #449d44; + border-color: #398439; +} + +.btn-success:active:hover, +.btn-success:active:focus, +.btn-success:active.focus, +.btn-success.active:hover, +.btn-success.active:focus, +.btn-success.active.focus, +.open>.btn-success.dropdown-toggle:hover, +.open>.btn-success.dropdown-toggle:focus, +.open>.btn-success.dropdown-toggle.focus { + color: #fff; + background-color: #398439; + border-color: #255625; +} + +.btn-success:active, +.btn-success.active, +.open>.btn-success.dropdown-toggle { + background-image: none; +} + +.btn-success.disabled:hover, +.btn-success.disabled:focus, +.btn-success.disabled.focus, +.btn-success[disabled]:hover, +.btn-success[disabled]:focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success:hover, +fieldset[disabled] .btn-success:focus, +fieldset[disabled] .btn-success.focus { + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-success .badge { + color: #5cb85c; + background-color: #fff; +} + +.btn-info { + color: #fff; + background-color: #56CCF2; + border-color: #3ec5f0; +} + +.btn-info:focus, +.btn-info.focus { + color: #fff; + background-color: #27beee; + border-color: #0d7ea3; +} + +.btn-info:hover { + color: #fff; + background-color: #27beee; + border-color: #11aee0; +} + +.btn-info:active, +.btn-info.active, +.open>.btn-info.dropdown-toggle { + color: #fff; + background-color: #27beee; + border-color: #11aee0; +} + +.btn-info:active:hover, +.btn-info:active:focus, +.btn-info:active.focus, +.btn-info.active:hover, +.btn-info.active:focus, +.btn-info.active.focus, +.open>.btn-info.dropdown-toggle:hover, +.open>.btn-info.dropdown-toggle:focus, +.open>.btn-info.dropdown-toggle.focus { + color: #fff; + background-color: #11aee0; + border-color: #0d7ea3; +} + +.btn-info:active, +.btn-info.active, +.open>.btn-info.dropdown-toggle { + background-image: none; +} + +.btn-info.disabled:hover, +.btn-info.disabled:focus, +.btn-info.disabled.focus, +.btn-info[disabled]:hover, +.btn-info[disabled]:focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info:hover, +fieldset[disabled] .btn-info:focus, +fieldset[disabled] .btn-info.focus { + background-color: #56CCF2; + border-color: #3ec5f0; +} + +.btn-info .badge { + color: #56CCF2; + background-color: #fff; +} + +.btn-warning { + color: #fff; + background-color: #F09819; + border-color: #e18b0f; +} + +.btn-warning:focus, +.btn-warning.focus { + color: #fff; + background-color: #c97c0d; + border-color: #694107; +} + +.btn-warning:hover { + color: #fff; + background-color: #c97c0d; + border-color: #a7670b; +} + +.btn-warning:active, +.btn-warning.active, +.open>.btn-warning.dropdown-toggle { + color: #fff; + background-color: #c97c0d; + border-color: #a7670b; +} + +.btn-warning:active:hover, +.btn-warning:active:focus, +.btn-warning:active.focus, +.btn-warning.active:hover, +.btn-warning.active:focus, +.btn-warning.active.focus, +.open>.btn-warning.dropdown-toggle:hover, +.open>.btn-warning.dropdown-toggle:focus, +.open>.btn-warning.dropdown-toggle.focus { + color: #fff; + background-color: #a7670b; + border-color: #694107; +} + +.btn-warning:active, +.btn-warning.active, +.open>.btn-warning.dropdown-toggle { + background-image: none; +} + +.btn-warning.disabled:hover, +.btn-warning.disabled:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled]:hover, +.btn-warning[disabled]:focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning:hover, +fieldset[disabled] .btn-warning:focus, +fieldset[disabled] .btn-warning.focus { + background-color: #F09819; + border-color: #e18b0f; +} + +.btn-warning .badge { + color: #F09819; + background-color: #fff; +} + +.btn-danger { + color: #fff; + background-color: #FF512F; + border-color: #ff3c16; +} + +.btn-danger:focus, +.btn-danger.focus { + color: #fff; + background-color: #fb2900; + border-color: #951800; +} + +.btn-danger:hover { + color: #fff; + background-color: #fb2900; + border-color: #d72300; +} + +.btn-danger:active, +.btn-danger.active, +.open>.btn-danger.dropdown-toggle { + color: #fff; + background-color: #fb2900; + border-color: #d72300; +} + +.btn-danger:active:hover, +.btn-danger:active:focus, +.btn-danger:active.focus, +.btn-danger.active:hover, +.btn-danger.active:focus, +.btn-danger.active.focus, +.open>.btn-danger.dropdown-toggle:hover, +.open>.btn-danger.dropdown-toggle:focus, +.open>.btn-danger.dropdown-toggle.focus { + color: #fff; + background-color: #d72300; + border-color: #951800; +} + +.btn-danger:active, +.btn-danger.active, +.open>.btn-danger.dropdown-toggle { + background-image: none; +} + +.btn-danger.disabled:hover, +.btn-danger.disabled:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled]:hover, +.btn-danger[disabled]:focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger:hover, +fieldset[disabled] .btn-danger:focus, +fieldset[disabled] .btn-danger.focus { + background-color: #FF512F; + border-color: #ff3c16; +} + +.btn-danger .badge { + color: #FF512F; + background-color: #fff; +} + +.btn-link { + color: #2196f3; + font-weight: normal; + border-radius: 0; +} + +.btn-link, +.btn-link:active, +.btn-link.active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} + +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} + +.btn-link:hover, +.btn-link:focus { + color: #0a6ebd; + text-decoration: underline; + background-color: transparent; +} + +.btn-link[disabled]:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:hover, +fieldset[disabled] .btn-link:focus { + color: #777777; + text-decoration: none; +} + +.btn-lg { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33333; + border-radius: 6px; +} + +.btn-sm { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.btn-xs { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.btn-block { + display: block; + width: 100%; +} + +.btn-block+.btn-block { + margin-top: 5px; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + display: none; +} + +.collapse.in { + display: block; +} + +tr.collapse.in { + display: table-row; +} + +tbody.collapse.in { + display: table-row-group; +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition-property: height, visibility; + transition-property: height, visibility; + -webkit-transition-duration: 0.35s; + transition-duration: 0.35s; + -webkit-transition-timing-function: ease; + transition-timing-function: ease; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-top: 4px solid \9; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle:focus { + outline: 0; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + font-size: 14px; + text-align: left; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + height: 1px; + margin: 9.5px 0; + overflow: hidden; + background-color: #e5e5e5; +} + +.dropdown-menu>li>a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.57143; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu>li>a:hover, +.dropdown-menu>li>a:focus { + text-decoration: none; + color: #262626; + background-color: #f5f5f5; +} + +.dropdown-menu>.active>a, +.dropdown-menu>.active>a:hover, +.dropdown-menu>.active>a:focus { + color: #fff; + text-decoration: none; + outline: 0; + background-color: #2196f3; +} + +.dropdown-menu>.disabled>a, +.dropdown-menu>.disabled>a:hover, +.dropdown-menu>.disabled>a:focus { + color: #777777; +} + +.dropdown-menu>.disabled>a:hover, +.dropdown-menu>.disabled>a:focus { + text-decoration: none; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + cursor: not-allowed; +} + +.open>.dropdown-menu { + display: block; +} + +.open>a { + outline: 0; +} + +.dropdown-menu-right { + left: auto; + right: 0; +} + +.dropdown-menu-left { + left: 0; + right: auto; +} + +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.57143; + color: #777777; + white-space: nowrap; +} + +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: 990; +} + +.pull-right>.dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px dashed; + border-bottom: 4px solid \9; + content: ""; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} + +.input-group { + position: relative; + display: table; + border-collapse: separate; +} + +.input-group[class*="col-"] { + float: none; + padding-left: 0; + padding-right: 0; +} + +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; +} + +.input-group .form-control:focus { + z-index: 3; +} + +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} + +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} + +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + color: #555555; + text-align: center; + background-color: #eeeeee; + border: 1px solid #ccc; + border-radius: 4px; +} + +.input-group-addon.input-sm, +.input-group-sm>.input-group-addon, +.input-group-sm>.input-group-btn>.input-group-addon.btn { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} + +.input-group-addon.input-lg, +.input-group-lg>.input-group-addon, +.input-group-lg>.input-group-btn>.input-group-addon.btn { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} + +.input-group-addon input[type="radio"], +.input-group-addon input[type="checkbox"] { + margin-top: 0; +} + +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child>.btn, +.input-group-btn:first-child>.btn-group>.btn, +.input-group-btn:first-child>.dropdown-toggle, +.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child>.btn-group:not(:last-child)>.btn { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} + +.input-group-addon:first-child { + border-right: 0; +} + +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child>.btn, +.input-group-btn:last-child>.btn-group>.btn, +.input-group-btn:last-child>.dropdown-toggle, +.input-group-btn:first-child>.btn:not(:first-child), +.input-group-btn:first-child>.btn-group:not(:first-child)>.btn { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.input-group-addon:last-child { + border-left: 0; +} + +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} + +.input-group-btn>.btn { + position: relative; +} + +.input-group-btn>.btn+.btn { + margin-left: -1px; +} + +.input-group-btn>.btn:hover, +.input-group-btn>.btn:focus, +.input-group-btn>.btn:active { + z-index: 2; +} + +.input-group-btn:first-child>.btn, +.input-group-btn:first-child>.btn-group { + margin-right: -1px; +} + +.input-group-btn:last-child>.btn, +.input-group-btn:last-child>.btn-group { + z-index: 2; + margin-left: -1px; +} + +.nav { + margin-bottom: 0; + padding-left: 0; + list-style: none; +} + +.nav:before, +.nav:after { + content: " "; + display: table; +} + +.nav:after { + clear: both; +} + +.nav>li { + position: relative; + display: block; +} + +.nav>li>a { + position: relative; + display: block; + padding: 10px 15px; +} + +.nav>li>a:hover, +.nav>li>a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.nav>li.disabled>a { + color: #777777; +} + +.nav>li.disabled>a:hover, +.nav>li.disabled>a:focus { + color: #777777; + text-decoration: none; + background-color: transparent; + cursor: not-allowed; +} + +.nav .open>a, +.nav .open>a:hover, +.nav .open>a:focus { + background-color: #eeeeee; + border-color: #2196f3; +} + +.nav .nav-divider { + height: 1px; + margin: 9.5px 0; + overflow: hidden; + background-color: #e5e5e5; +} + +.nav>li>a>img { + max-width: none; +} + +.nav-tabs { + border-bottom: 1px solid #f2f2f2; +} + +.nav-tabs>li { + float: left; + margin-bottom: -1px; +} + +.nav-tabs>li>a { + margin-right: 2px; + line-height: 1.57143; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} + +.nav-tabs>li>a:hover { + border-color: #eeeeee #eeeeee #f2f2f2; +} + +.nav-tabs>li.active>a, +.nav-tabs>li.active>a:hover, +.nav-tabs>li.active>a:focus { + color: #555555; + background-color: #fff; + border: 1px solid #f2f2f2; + border-bottom-color: transparent; + cursor: default; +} + +.nav-pills>li { + float: left; +} + +.nav-pills>li>a { + border-radius: 4px; +} + +.nav-pills>li+li { + margin-left: 2px; +} + +.nav-pills>li.active>a, +.nav-pills>li.active>a:hover, +.nav-pills>li.active>a:focus { + color: #fff; + background-color: #2196f3; +} + +.nav-stacked>li { + float: none; +} + +.nav-stacked>li+li { + margin-top: 2px; + margin-left: 0; +} + +.nav-justified, +.nav-tabs.nav-justified { + width: 100%; +} + +.nav-justified>li, +.nav-tabs.nav-justified>li { + float: none; +} + +.nav-justified>li>a, +.nav-tabs.nav-justified>li>a { + text-align: center; + margin-bottom: 5px; +} + +.nav-justified>.dropdown .dropdown-menu { + top: auto; + left: auto; +} + +.nav-tabs-justified, +.nav-tabs.nav-justified { + border-bottom: 0; +} + +.nav-tabs-justified>li>a, +.nav-tabs.nav-justified>li>a { + margin-right: 0; + border-radius: 4px; +} + +.nav-tabs-justified>.active>a, +.nav-tabs.nav-justified>.active>a, +.nav-tabs-justified>.active>a:hover, +.nav-tabs.nav-justified>.active>a:hover, +.nav-tabs-justified>.active>a:focus, +.nav-tabs.nav-justified>.active>a:focus { + border: 1px solid #f2f2f2; +} + +.tab-content>.tab-pane { + display: none; +} + +.tab-content>.active { + display: block; +} + +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.navbar { + position: relative; + min-height: 50px; + margin-bottom: 21px; + border: 1px solid transparent; +} + +.navbar:before, +.navbar:after { + content: " "; + display: table; +} + +.navbar:after { + clear: both; +} + +.navbar-header:before, +.navbar-header:after { + content: " "; + display: table; +} + +.navbar-header:after { + clear: both; +} + +.navbar-collapse { + overflow-x: visible; + padding-right: 15px; + padding-left: 15px; + border-top: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + -webkit-overflow-scrolling: touch; +} + +.navbar-collapse:before, +.navbar-collapse:after { + content: " "; + display: table; +} + +.navbar-collapse:after { + clear: both; +} + +.navbar-collapse.in { + overflow-y: auto; +} + +.navbar-fixed-top .navbar-collapse, +.navbar-fixed-bottom .navbar-collapse { + max-height: 340px; +} + +.container>.navbar-header, +.container>.navbar-collapse, +.container-fluid>.navbar-header, +.container-fluid>.navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} + +.navbar-static-top { + z-index: 1000; + border-width: 0 0 1px; +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} + +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} + +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; + border-width: 1px 0 0; +} + +.navbar-brand { + float: left; + padding: 14.5px 15px; + font-size: 18px; + line-height: 21px; + height: 50px; +} + +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none; +} + +.navbar-brand>img { + display: block; +} + +.navbar-toggle { + position: relative; + float: right; + margin-right: 15px; + padding: 10px 10px; + margin-top: 8px; + margin-bottom: 8px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} + +.navbar-toggle:focus { + outline: 0; +} + +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} + +.navbar-toggle .icon-bar+.icon-bar { + margin-top: 3px; +} + +.navbar-nav { + margin: 7.25px -15px; +} + +.navbar-nav>li>a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 21px; +} + +.navbar-form { + margin-left: -15px; + margin-right: -15px; + padding: 10px 15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + margin-top: 7.5px; + margin-bottom: 7.5px; +} + +.navbar-nav>li>.dropdown-menu { + margin-top: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu { + margin-bottom: 0; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.navbar-btn { + margin-top: 7.5px; + margin-bottom: 7.5px; +} + +.navbar-btn.btn-sm { + margin-top: 10px; + margin-bottom: 10px; +} + +.navbar-btn.btn-xs { + margin-top: 14px; + margin-bottom: 14px; +} + +.navbar-text { + margin-top: 14.5px; + margin-bottom: 14.5px; +} + +.navbar-default { + background-color: #f8f8f8; + border-color: #e7e7e7; +} + +.navbar-default .navbar-brand { + color: #777; +} + +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #5e5e5e; + background-color: transparent; +} + +.navbar-default .navbar-text { + color: #777; +} + +.navbar-default .navbar-nav>li>a { + color: #777; +} + +.navbar-default .navbar-nav>li>a:hover, +.navbar-default .navbar-nav>li>a:focus { + color: #333; + background-color: transparent; +} + +.navbar-default .navbar-nav>.active>a, +.navbar-default .navbar-nav>.active>a:hover, +.navbar-default .navbar-nav>.active>a:focus { + color: #555; + background-color: #e7e7e7; +} + +.navbar-default .navbar-nav>.disabled>a, +.navbar-default .navbar-nav>.disabled>a:hover, +.navbar-default .navbar-nav>.disabled>a:focus { + color: #ccc; + background-color: transparent; +} + +.navbar-default .navbar-toggle { + border-color: #ddd; +} + +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #ddd; +} + +.navbar-default .navbar-toggle .icon-bar { + background-color: #888; +} + +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #e7e7e7; +} + +.navbar-default .navbar-nav>.open>a, +.navbar-default .navbar-nav>.open>a:hover, +.navbar-default .navbar-nav>.open>a:focus { + background-color: #e7e7e7; + color: #555; +} + +.navbar-default .navbar-link { + color: #777; +} + +.navbar-default .navbar-link:hover { + color: #333; +} + +.navbar-default .btn-link { + color: #777; +} + +.navbar-default .btn-link:hover, +.navbar-default .btn-link:focus { + color: #333; +} + +.navbar-default .btn-link[disabled]:hover, +.navbar-default .btn-link[disabled]:focus, +fieldset[disabled] .navbar-default .btn-link:hover, +fieldset[disabled] .navbar-default .btn-link:focus { + color: #ccc; +} + +.navbar-inverse { + background-color: #222; + border-color: #090909; +} + +.navbar-inverse .navbar-brand { + color: #9d9d9d; +} + +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #fff; + background-color: transparent; +} + +.navbar-inverse .navbar-text { + color: #9d9d9d; +} + +.navbar-inverse .navbar-nav>li>a { + color: #9d9d9d; +} + +.navbar-inverse .navbar-nav>li>a:hover, +.navbar-inverse .navbar-nav>li>a:focus { + color: #fff; + background-color: transparent; +} + +.navbar-inverse .navbar-nav>.active>a, +.navbar-inverse .navbar-nav>.active>a:hover, +.navbar-inverse .navbar-nav>.active>a:focus { + color: #fff; + background-color: #090909; +} + +.navbar-inverse .navbar-nav>.disabled>a, +.navbar-inverse .navbar-nav>.disabled>a:hover, +.navbar-inverse .navbar-nav>.disabled>a:focus { + color: #444; + background-color: transparent; +} + +.navbar-inverse .navbar-toggle { + border-color: #333; +} + +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #333; +} + +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #fff; +} + +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} + +.navbar-inverse .navbar-nav>.open>a, +.navbar-inverse .navbar-nav>.open>a:hover, +.navbar-inverse .navbar-nav>.open>a:focus { + background-color: #090909; + color: #fff; +} + +.navbar-inverse .navbar-link { + color: #9d9d9d; +} + +.navbar-inverse .navbar-link:hover { + color: #fff; +} + +.navbar-inverse .btn-link { + color: #9d9d9d; +} + +.navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link:focus { + color: #fff; +} + +.navbar-inverse .btn-link[disabled]:hover, +.navbar-inverse .btn-link[disabled]:focus, +fieldset[disabled] .navbar-inverse .btn-link:hover, +fieldset[disabled] .navbar-inverse .btn-link:focus { + color: #444; +} + +.pager { + padding-left: 0; + margin: 21px 0; + list-style: none; + text-align: center; +} + +.pager:before, +.pager:after { + content: " "; + display: table; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager li>a, +.pager li>span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 15px; +} + +.pager li>a:hover, +.pager li>a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.pager .next>a, +.pager .next>span { + float: right; +} + +.pager .previous>a, +.pager .previous>span { + float: left; +} + +.pager .disabled>a, +.pager .disabled>a:hover, +.pager .disabled>a:focus, +.pager .disabled>span { + color: #777777; + background-color: #fff; + cursor: not-allowed; +} + +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + color: #fff; + line-height: 1; + vertical-align: middle; + white-space: nowrap; + text-align: center; + background-color: #777777; + border-radius: 10px; +} + +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.btn-xs .badge, +.btn-group-xs>.btn .badge { + top: 0; + padding: 1px 5px; +} + +.list-group-item.active>.badge, +.nav-pills>.active>a>.badge { + color: #2196f3; + background-color: #fff; +} + +.list-group-item>.badge { + float: right; +} + +.list-group-item>.badge+.badge { + margin-right: 5px; +} + +.nav-pills>li>a>.badge { + margin-left: 3px; +} + +a.badge:hover, +a.badge:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} + +.thumbnail { + display: block; + padding: 4px; + margin-bottom: 21px; + line-height: 1.57143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: border 0.2s ease-in-out; + transition: border 0.2s ease-in-out; +} + +.thumbnail>img, +.thumbnail a>img { + display: block; + max-width: 100%; + height: auto; + margin-left: auto; + margin-right: auto; +} + +.thumbnail .caption { + padding: 9px; + color: #333333; +} + +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: #2196f3; +} + +.media { + margin-top: 15px; +} + +.media:first-child { + margin-top: 0; +} + +.media, +.media-body { + zoom: 1; + overflow: hidden; +} + +.media-body { + width: 10000px; +} + +.media-object { + display: block; +} + +.media-object.img-thumbnail { + max-width: none; +} + +.media-right, +.media>.pull-right { + padding-left: 10px; +} + +.media-left, +.media>.pull-left { + padding-right: 10px; +} + +.media-left, +.media-right, +.media-body { + display: table-cell; + vertical-align: top; +} + +.media-middle { + vertical-align: middle; +} + +.media-middle .media-left, +.media-middle .media-right, +.media-middle .media-body { + vertical-align: middle; +} + +.media-bottom { + vertical-align: bottom; +} + +.media-bottom .media-left, +.media-bottom .media-right, +.media-bottom .media-body { + vertical-align: bottom; +} + +.media-heading { + margin-top: 0; + margin-bottom: 5px; +} + +.media-list { + padding-left: 0; + list-style: none; +} + +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; +} + +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + left: 0; + bottom: 0; + height: 100%; + width: 100%; + border: 0; +} + +.embed-responsive-16by9 { + padding-bottom: 56.25%; +} + +.embed-responsive-4by3 { + padding-bottom: 75%; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-lg { + padding: 24px; + border-radius: 6px; +} + +.well-sm { + padding: 9px; + border-radius: 3px; +} + +.close { + float: right; + font-size: 24px; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; + opacity: 0.5; + filter: alpha(opacity=50); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.modal-open { + overflow: hidden; +} + +.modal { + display: none; + overflow: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + -webkit-overflow-scrolling: touch; + outline: 0; +} + +.modal.fade .modal-dialog { + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + transform: translate(0, -25%); + -webkit-transition: -webkit-transform 0.3s ease-out; + transition: -webkit-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out; +} + +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); +} + +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} + +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} + +.modal-content { + position: relative; + background-color: #fff; + border-radius: 6px; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.05); + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.05); + background-clip: padding-box; + outline: 0; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; +} + +.modal-backdrop.fade { + opacity: 0; + filter: alpha(opacity=0); +} + +.modal-backdrop.in { + opacity: 0.5; + filter: alpha(opacity=50); +} + +.modal-header { + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} + +.modal-header:before, +.modal-header:after { + content: " "; + display: table; +} + +.modal-header:after { + clear: both; +} + +.modal-header .close { + margin-top: -2px; +} + +.modal-title { + margin: 0; + line-height: 1.57143; +} + +.modal-body { + position: relative; + padding: 15px; +} + +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} + +.modal-footer:before, +.modal-footer:after { + content: " "; + display: table; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn+.btn { + margin-left: 5px; + margin-bottom: 0; +} + +.modal-footer .btn-group .btn+.btn { + margin-left: -1px; +} + +.modal-footer .btn-block+.btn-block { + margin-left: 0; +} + +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} + +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-family: "PingHei", "PingFang SC", Helvetica Neue, "Work Sans", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif; + font-style: normal; + font-weight: normal; + letter-spacing: normal; + line-break: auto; + line-height: 1.57143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + font-size: 12px; + opacity: 0; + filter: alpha(opacity=0); +} + +.tooltip.in { + opacity: 0.9; + filter: alpha(opacity=90); +} + +.tooltip.top { + margin-top: -3px; + padding: 5px 0; +} + +.tooltip.right { + margin-left: 3px; + padding: 0 5px; +} + +.tooltip.bottom { + margin-top: 3px; + padding: 5px 0; +} + +.tooltip.left { + margin-left: -3px; + padding: 0 5px; +} + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} + +.tooltip.top-left .tooltip-arrow { + bottom: 0; + right: 5px; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} + +.tooltip.top-right .tooltip-arrow { + bottom: 0; + left: 5px; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} + +.tooltip.bottom-left .tooltip-arrow { + top: 0; + right: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} + +.tooltip.bottom-right .tooltip-arrow { + top: 0; + left: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} + +.clearfix:before, +.clearfix:after { + content: " "; + display: table; +} + +.clearfix:after { + clear: both; +} + +.center-block { + display: block; + margin-left: auto; + margin-right: auto; +} + +.pull-right { + float: right !important; +} + +.pull-left { + float: left !important; +} + +.hide { + display: none !important; +} + +.show { + display: block !important; +} + +.invisible { + visibility: hidden; +} + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.hidden { + display: none !important; +} + +.affix { + position: fixed; +} + +@-ms-viewport { + width: device-width; +} + +.visible-xs { + display: none !important; +} + +.visible-sm { + display: none !important; +} + +.visible-md { + display: none !important; +} + +.visible-lg { + display: none !important; +} + +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { + display: none !important; +} + +.visible-print { + display: none !important; +} + +.visible-print-block { + display: none !important; +} + +.visible-print-inline { + display: none !important; +} + +.visible-print-inline-block { + display: none !important; +} + +/** + * application + */ + +/*! + * IE10 viewport hack for Surface/desktop Windows 8 bug + * Copyright 2014-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +/* + * See the Getting Started docs for more information: + * http://getbootstrap.com/getting-started/#support-ie10-width + */ + +@-ms-viewport { + width: device-width; +} + +@-o-viewport { + width: device-width; +} + +@viewport { + width: device-width; +} + +/* + * scrollbar + */ + +::-webkit-scrollbar { + width: 6px; + height: 4px; + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.15); +} + +::-webkit-scrollbar-thumb:window-inactive { + background: rgba(0, 0, 0, 0.1); +} + +::-webkit-scrollbar-thumb:vertical { + height: 4px; + background: rgba(0, 0, 0, 0.15); +} + +::-webkit-scrollbar-thumb:horizontal { + width: 4px; + background: rgba(0, 0, 0, 0.15); +} + +::-webkit-scrollbar-thumb:vertical:hover { + background-color: rgba(0, 0, 0, 0.3); +} + +::-webkit-scrollbar-thumb:vertical:active { + background-color: rgba(0, 0, 0, 0.5); +} + +::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.1); +} + +::-webkit-scrollbar-track-piece { + background: rgba(0, 0, 0, 0.15); +} + +*, +*:before, +*:after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +html, +body { + position: relative; + overflow-x: hidden; +} + +body { + padding-right: 0 !important; + font-family: "PingHei", "PingFang SC", Helvetica Neue, "Work Sans", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif; + font-size: 14px; + line-height: 1.57143; + color: #333333; + background-color: #fff; +} + +a { + color: #333333; + text-decoration: none; +} + +a:focus, +a:hover { + color: #0a6ebd; + text-decoration: none; +} + +a.active { + color: #0a6ebd; +} + +.active>a { + color: #0a6ebd; +} + +input, +button, +select, +textarea, +.btn { + outline: none !important; +} + +input:focus, +input:hover, +input:active, +button:focus, +button:hover, +button:active, +select:focus, +select:hover, +select:active, +textarea:focus, +textarea:hover, +textarea:active, +.btn:focus, +.btn:hover, +.btn:active { + outline: none !important; +} + +/*------------------ Fluidity response ------------------------------*/ + +img, +canvas, +iframe, +video, +svg { + max-width: 100%; + height: auto; +} + +/*------------------ clear ------------------------------*/ + +.clear { + height: 0; + font-size: 0; + line-height: 0; + overflow: hidden; + clear: both; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.clearfix { + *zoom: 1; +} + +/*other*/ + +.clickable { + cursor: pointer; +} + +.scrollable { + overflow-x: hidden; + overflow-y: auto; +} + +/*transform*/ + +.transform-no { + -ms-transform: none !important; + -webkit-transform: none !important; + transform: none !important; +} + +/*---------------------------------------------------- + * color + *---------------------------------------------------*/ + +.text-dark { + color: #333 !important; +} + +.text-grey { + color: #999 !important; +} + +/*text-white*/ + +.text-white { + color: #fff !important; +} + +.text-white a:hover, +.text-white a:hover i, +.text-white:hover { + color: rgba(255, 255, 255, 0.8); +} + +.text-white a.list-group-item.active i { + color: #fff; +} + +.text-white .accordion-list p { + font-size: 12px; + height: 84px; + line-height: 21px; + color: rgba(255, 255, 255, 0.8); +} + +/*background-color*/ + +.bg-no { + background: none !important; +} + +.bg-alpha { + background-color: transparent !important; +} + +.bg-inverse, +.bg-inverse a { + color: #fff; +} + +.btn.bg-inverse:hover, +.btn.bg-inverse:focus, +.btn.bg-inverse.focus { + color: rgba(255, 255, 255, 0.8); +} + +.bg { + background-color: #f6f6f6 !important; +} + +/*---------------------------------------------------- + * z-index + *---------------------------------------------------*/ + +.z-no { + z-index: inherit; +} + +.z1 { + z-index: 1; +} + +.z2 { + z-index: 2; +} + +.z3 { + z-index: 3; +} + +.z4 { + z-index: 4; +} + +.zmin { + z-index: -1; +} + +.zmax { + z-index: 999; +} + +/*---------------------------------------------------- + * margin + *---------------------------------------------------*/ + +/* All */ + +.m { + margin: 5px !important; +} + +.m-no { + margin: 0 !important; +} + +.m-0x { + margin: 10px !important; +} + +.m-1x { + margin: 15px !important; +} + +.m-2x { + margin: 20px !important; +} + +.m-3x { + margin: 30px !important; +} + +.m-4x { + margin: 60px !important; +} + +.m-5x { + margin: 100px !important; +} + +/* Vertical */ + +.mv { + margin-top: 5px !important; + margin-bottom: 5px !important; +} + +.mv-no { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +.mv-0x { + margin-top: 10px !important; + margin-bottom: 10px !important; +} + +.mv-1x { + margin-top: 15px !important; + margin-bottom: 15px !important; +} + +.mv-2x { + margin-top: 20px !important; + margin-bottom: 20px !important; +} + +.mv-3x { + margin-top: 30px !important; + margin-bottom: 30px !important; +} + +.mv-4x { + margin-top: 60px !important; + margin-bottom: 60px !important; +} + +.mv-5x { + margin-top: 100px !important; + margin-bottom: 100px !important; +} + +/* Horizontal */ + +.mh { + margin-left: 5px !important; + margin-right: 5px !important; +} + +.mh-no { + margin-left: 0 !important; + margin-right: 0 !important; +} + +.mh-0x { + margin-left: 10px !important; + margin-right: 10px !important; +} + +.mh-1x { + margin-left: 15px !important; + margin-right: 15px !important; +} + +.mh-2x { + margin-left: 20px !important; + margin-right: 20px !important; +} + +.mh-3x { + margin-left: 30px !important; + margin-right: 30px !important; +} + +.mh-4x { + margin-left: 60px !important; + margin-right: 60px !important; +} + +.mh-5x { + margin-left: 100px !important; + margin-right: 100px !important; +} + +/* margin Top */ + +.mt { + margin-top: 5px !important; +} + +.mt-no { + margin-top: 0 !important; +} + +.mt-0x { + margin-top: 10px !important; +} + +.mt-1x { + margin-top: 15px !important; +} + +.mt-2x { + margin-top: 20px !important; +} + +.mt-3x { + margin-top: 30px !important; +} + +.mt-4x { + margin-top: 60px !important; +} + +.mt-5x { + margin-top: 100px !important; +} + +/* margin Bottom */ + +.mb { + margin-bottom: 5px !important; +} + +.mb-no { + margin-bottom: 0 !important; +} + +.mb-0x { + margin-bottom: 10px !important; +} + +.mb-1x { + margin-bottom: 15px !important; +} + +.mb-2x { + margin-bottom: 20px !important; +} + +.mb-3x { + margin-bottom: 30px !important; +} + +.mb-4x { + margin-bottom: 60px !important; +} + +.mb-5x { + margin-bottom: 100px !important; +} + +/* margin left */ + +.ml { + margin-left: 5px !important; +} + +.ml-no { + margin-left: 0 !important; +} + +.ml-0x { + margin-left: 10px !important; +} + +.ml-1x { + margin-left: 15px !important; +} + +.ml-2x { + margin-left: 20px !important; +} + +.ml-3x { + margin-left: 30px !important; +} + +.ml-4x { + margin-left: 60px !important; +} + +.ml-5x { + margin-left: 100px !important; +} + +/* margin right */ + +.mr { + margin-right: 5px !important; +} + +.mr-no { + margin-right: 0 !important; +} + +.mr-0x { + margin-right: 10px !important; +} + +.mr-1x { + margin-right: 15px !important; +} + +.mr-2x { + margin-right: 20px !important; +} + +.mr-3x { + margin-right: 30px !important; +} + +.mr-4x { + margin-right: 60px !important; +} + +.mr-5x { + margin-right: 100px !important; +} + +/*---------------------------------------------------- + * padding + *---------------------------------------------------*/ + +/* All */ + +.p { + padding: 5px !important; +} + +.p-no { + padding: 0 !important; +} + +.p-0x { + padding: 10px !important; +} + +.p-1x { + padding: 15px !important; +} + +.p-2x { + padding: 20px !important; +} + +.p-3x { + padding: 30px !important; +} + +.p-4x { + padding: 60px !important; +} + +.p-5x { + padding: 100px !important; +} + +/* Vertical */ + +.pv { + padding-top: 5px !important; + padding-bottom: 5px !important; +} + +.pv-no { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.pv-0x { + padding-top: 10px !important; + padding-bottom: 10px !important; +} + +.pv-1x { + padding-top: 15px !important; + padding-bottom: 15px !important; +} + +.pv-2x { + padding-top: 20px !important; + padding-bottom: 20px !important; +} + +.pv-3x { + padding-top: 30px !important; + padding-bottom: 30px !important; +} + +.pv-4x { + padding-top: 60px !important; + padding-bottom: 60px !important; +} + +.pv-5x { + padding-top: 100px !important; + padding-bottom: 100px !important; +} + +/* Horizontal */ + +.ph { + padding-left: 5px !important; + padding-right: 5px !important; +} + +.ph-no { + padding-left: 0 !important; + padding-right: 0 !important; +} + +.ph-0x { + padding-left: 10px !important; + padding-right: 10px !important; +} + +.ph-1x { + padding-left: 15px !important; + padding-right: 15px !important; +} + +.ph-2x { + padding-left: 20px !important; + padding-right: 20px !important; +} + +.ph-3x { + padding-left: 30px !important; + padding-right: 30px !important; +} + +.ph-4x { + padding-left: 60px !important; + padding-right: 60px !important; +} + +.ph-5x { + padding-left: 100px !important; + padding-right: 100px !important; +} + +/* padding Top */ + +.pt { + padding-top: 5px !important; +} + +.pt-no { + padding-top: 0 !important; +} + +.pt-0x { + padding-top: 10px !important; +} + +.pt-1x { + padding-top: 15px !important; +} + +.pt-2x { + padding-top: 20px !important; +} + +.pt-3x { + padding-top: 30px !important; +} + +.pt-4x { + padding-top: 60px !important; +} + +.pt-5x { + padding-top: 100px !important; +} + +/* padding Bottom */ + +.pb { + padding-bottom: 5px !important; +} + +.pb-no { + padding-bottom: 0 !important; +} + +.pb-0x { + padding-bottom: 10px !important; +} + +.pb-1x { + padding-bottom: 15px !important; +} + +.pb-2x { + padding-bottom: 20px !important; +} + +.pb-3x { + padding-bottom: 30px !important; +} + +.pb-4x { + padding-bottom: 60px !important; +} + +.pb-5x { + padding-bottom: 100px !important; +} + +/* padding left */ + +.pl { + padding-left: 5px !important; +} + +.pl-no { + padding-left: 0 !important; +} + +.pl-0x { + padding-left: 10px !important; +} + +.pl-1x { + padding-left: 15px !important; +} + +.pl-2x { + padding-left: 20px !important; +} + +.pl-3x { + padding-left: 30px !important; +} + +.pl-4x { + padding-left: 60px !important; +} + +.pl-5x { + padding-left: 100px !important; +} + +/* padding right */ + +.pr { + padding-right: 5px !important; +} + +.pr-no { + padding-right: 0 !important; +} + +.pr-0x { + padding-right: 10px !important; +} + +.pr-1x { + padding-right: 15px !important; +} + +.pr-2x { + padding-right: 20px !important; +} + +.pr-3x { + padding-right: 30px !important; +} + +.pr-4x { + padding-right: 60px !important; +} + +.pr-5x { + padding-right: 100px !important; +} + +/*---------------------------------------------------- + * border + *---------------------------------------------------*/ + +.b { + border: 1px solid #ddd; +} + +.b-no { + border: none !important; +} + +/*---------------------------------------------------- + * border-radius + *---------------------------------------------------*/ + +/*----------------------css border-radius----------------*/ + +.r-rounded { + border-radius: 2em !important; + padding-left: 1em; + padding-right: 1em; + overflow: hidden; +} + +.r-circle { + border-radius: 50% !important; + overflow: hidden; +} + +.r-no { + border-radius: 0 !important; +} + +/*---------------------------------------------------- + * width and height + *---------------------------------------------------*/ + +.w-auto { + width: auto !important; +} + +.w-full, +.w-full img { + width: 100% !important; + max-width: 100% !important; +} + +/*---------------------------------------------------- + * text + *---------------------------------------------------*/ + +.lh-2x { + line-height: 2.0; +} + +/*----------------- css text --------------------*/ + +.text-break { + word-break: break-all !important; + word-wrap: break-word !important; +} + +.text-undecorate { + text-decoration: none !important; +} + +.text-underline { + text-decoration: underline !important; +} + +.text-through { + text-decoration: line-through !important; +} + +.text-sub { + vertical-align: sub !important; +} + +.text-super { + vertical-align: super !important; +} + +.text-indent, +.text-indent p, +.text-indent div { + text-indent: 2em; +} + +/*text wrap*/ + +.text-nowrap { + /*display: block;*/ + max-width: 100%; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + word-wrap: normal !important; + -moz-binding: url("ellipsis.xml"); +} + +.text-nowrap-1x { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + -webkit-box-flex: 1; + line-height: 24px; + height: 24px; + word-break: break-all !important; + word-wrap: break-word !important; +} + +.text-nowrap-2x { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-height: 24px; + height: 48px; + word-break: break-all !important; + word-wrap: break-word !important; +} + +.text-nowrap-3x { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + line-height: 24px; + height: 72px; + word-break: break-all !important; + word-wrap: break-word !important; +} + +.text-nowrap-4x { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + line-height: 24px; + height: 96px; + word-break: break-all !important; + word-wrap: break-word !important; +} + +.text-nowrap-5x { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 5; + line-height: 24px; + height: 120px; + word-break: break-all !important; + word-wrap: break-word !important; +} + +/*---------------------------------------------------- + * thumb + *---------------------------------------------------*/ + +.thumb { + width: 16px; + display: inline-block; + overflow: hidden; +} + +.thumb-xs { + width: 32px; + display: inline-block; + overflow: hidden; +} + +.thumb-sm { + width: 48px; + display: inline-block; + overflow: hidden; +} + +.thumb-md { + width: 64px; + display: inline-block; + overflow: hidden; +} + +.thumb-lg { + width: 96px; + display: inline-block; + overflow: hidden; +} + +.thumb-xl { + width: 128px; + display: inline-block; + overflow: hidden; +} + +.thumb-wrapper { + padding: 2px; + border: 1px solid #dbe2e7; +} + +.thumb img, +.thumb-0x img, +.thumb-1x img, +.thumb-2x img, +.thumb-3x img, +.thumb-4x img, +.thumb-5x img, +.thumb-6x img, +.thumb-btn img { + height: auto; + max-width: 100%; + vertical-align: middle; +} + +/*---------------------------------------------------- + * img hover style + *---------------------------------------------------*/ + +/*img gray*/ + +img.img-gray, +.img-gray img { + -webkit-filter: grayscale(100%); + -moz-filter: grayscale(100%); + -ms-filter: grayscale(100%); + -o-filter: grayscale(100%); + filter: grayscale(100%); + -webkit-filter: gray; + filter: gray; +} + +img.img-gray:hover, +.img-gray:hover img { + -webkit-filter: grayscale(0); + -moz-filter: grayscale(0); + -ms-filter: grayscale(0); + -o-filter: grayscale(0); + filter: grayscale(0); +} + +/*img-rotate*/ + +img.img-rotate, +.img-rotate img { + -webkit-transition: -webkit-transform 0.3s ease; + transition: -webkit-transform 0.3s ease; + transition: transform 0.3s ease; + transition: transform 0.3s ease, -webkit-transform 0.3s ease; +} + +img.img-rotate:hover, +.img-rotate:hover img { + transform: rotate(360deg); + -ms-transform: rotate(360deg); + /* IE 9 */ + -moz-transform: rotate(360deg); + /* Firefox */ + -webkit-transform: rotate(360deg); + /* Safari and Chrome */ + -o-transform: rotate(360deg); +} + +/*img-burn*/ + +img.img-burn, +.img-burn img { + position: relative; + -webkit-transition: all 0.8s ease-in-out; + transition: all 0.8s ease-in-out; +} + +img.img-burn:hover, +.img-burn:hover img { + -webkit-transform: scale(1.2) rotate(2deg); + -ms-transform: scale(1.2) rotate(2deg); + transform: scale(1.2) rotate(2deg); +} + +/*hover-up*/ + +img.hover-up, +.hover-up img { + position: relative; + top: 0; + -webkit-transition: top .3s ease-out; + transition: top .3s ease-out; +} + +img.hover-up:hover, +.hover-up:hover img { + top: -6px; +} + +/*Button components*/ + +.text-active, +.active>.text, +.active>.auto .text, +.collapsed>.text, +.collapsed>.auto .text { + display: none !important; +} + +.active>.text-active, +.active>.auto .text-active, +.collapsed>.text-active, +.collapsed>.auto .text-active { + display: inline-block !important; +} + +/*Button components end here*/ + +/** + * paper + */ + +.shadow-no { + -webkit-box-shadow: none; + box-shadow: none; +} + +.shadow, +.hover-shadow:hover { + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +/*radio*/ + +.radio { + margin-left: 20px; +} + +.radio label { + display: inline-block; + position: relative; + padding-left: 5px; +} + +.radio label:before { + content: ""; + display: inline-block; + position: absolute; + width: 17px; + height: 17px; + left: 0; + margin-left: -20px; + border: 1px solid #cccccc; + border-radius: 50%; + background-color: #fff; + -webkit-transition: border 0.15s ease-in-out; + transition: border 0.15s ease-in-out; +} + +.radio label:after { + display: inline-block; + position: absolute; + content: " "; + width: 11px; + height: 11px; + left: 3px; + top: 3px; + margin-left: -20px; + border-radius: 50%; + background-color: #555555; + -webkit-transform: scale(0, 0); + -ms-transform: scale(0, 0); + transform: scale(0, 0); + -webkit-transition: -webkit-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); + transition: -webkit-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); + transition: transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); + transition: transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33), -webkit-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); +} + +.radio input[type=radio] { + display: none; +} + +.radio input[type=radio]:checked+label:after { + -webkit-transform: scale(1, 1); + -ms-transform: scale(1, 1); + transform: scale(1, 1); +} + +.radio input[type=radio]:disabled+label { + opacity: 0.65; +} + +.radio input[type=radio]:disabled+label:before { + cursor: not-allowed; +} + +.hover-grow { + -webkit-transition: all .2s linear; + transition: all .2s linear; +} + +.hover-grow:hover { + -webkit-transform: translate3d(0, -2px, 0); + transform: translate3d(0, -2px, 0); +} + +fieldset { + padding: 0; + margin: 0; + border: 0; + min-width: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 21px; + font-size: 21px; + line-height: inherit; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: bold; +} + +input[type="search"] { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + line-height: normal; +} + +input[type="file"] { + display: block; +} + +input[type="range"] { + display: block; + width: 100%; +} + +select[multiple], +select[size] { + height: auto; +} + +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +output { + display: block; + padding-top: 7px; + font-size: 14px; + line-height: 1.57143; + color: #555555; +} + +.form-control { + display: block; + width: 100%; + height: 35px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.57143; + color: #555555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + -webkit-transition: border-color ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s; + transition: border-color ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s; + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s; +} + +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); +} + +.form-control::-moz-placeholder { + color: #999; + opacity: 1; +} + +.form-control:-ms-input-placeholder { + color: #999; +} + +.form-control::-webkit-input-placeholder { + color: #999; +} + +.form-control::-ms-expand { + border: 0; + background-color: transparent; +} + +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + background-color: #eeeeee; + opacity: 1; +} + +.form-control[disabled], +fieldset[disabled] .form-control { + cursor: not-allowed; +} + +textarea.form-control { + height: auto; +} + +input[type="search"] { + -webkit-appearance: none; +} + +/* + * Component: list + * ---------------- + */ + +.list-disc { + list-style: disc !important; +} + +.list-alpha { + list-style: upper-alpha !important; +} + +.list-decimal { + list-style: decimal !important; +} + +.list-outside { + list-style-position: outside !important; +} + +.list-inside { + list-style-position: inside !important; +} + +.list-square { + list-style: none; +} + +.list-square li:before { + color: #ccc; + content: "▪"; + font-size: 12px; + margin-right: 6px; + -webkit-transition: 0.2s ease; + transition: 0.2s ease; +} + +.list-circle-num, +.list-square-num { + counter-reset: list1; +} + +.list-circle-num>li, +.list-square-num>li { + list-style: none outside none; + margin-bottom: 13px; +} + +.list-circle-num>li:before { + counter-increment: list1; + content: counter(list1) ""; + width: 24px; + height: 24px; + text-align: center; + border-radius: 12px; + font-size: 15px; + border-width: 1px; + border-style: solid; + margin: 0 16px 0 0; + display: inline-block; + vertical-align: middle; +} + +.list-square-num>li:before { + counter-increment: list1; + content: counter(list1) ""; + width: 24px; + height: 24px; + text-align: center; + border-radius: 5px; + font-size: 15px; + border-width: 1px; + border-style: solid; + margin: 0 16px 0 0; + display: inline-block; + vertical-align: middle; +} + +.list-circle-num>li>ol, +.list-square-num>li>ol { + counter-reset: list2; +} + +.list-circle-num>li>ol>li, +.list-square-num>li>ol>li { + margin-bottom: 13px; +} + +.list-circle-num>li>ol>li:before { + counter-increment: list2; + content: counter(list1) "." counter(list2) ""; + width: 24px; + height: 24px; + text-align: center; + border-radius: 12px; + font-size: 15px; + border-width: 1px; + border-style: solid; + margin: 0 16px 0 0; + display: inline-block; + vertical-align: middle; +} + +.list-square-num>li>ol>li:before { + counter-increment: list2; + content: counter(list1) "." counter(list2) ""; + width: 24px; + height: 24px; + text-align: center; + border-radius: 5px; + font-size: 15px; + border-width: 1px; + border-style: solid; + margin: 0 16px 0 0; + display: inline-block; + vertical-align: middle; +} + +.list-circle-num[class*="list-full"]>li::before, +.list-square-num[class*="list-full"]>li::before { + background: #de4a32; + color: #ffffff; +} + +.label { + display: inline-block; + padding: .3em .6em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #777777; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} + +.label:empty { + display: none; +} + +.btn .label { + position: relative; + top: -1px; +} + +a.label:hover, +a.label:focus { + color: #333333; + text-decoration: none; + cursor: pointer; +} + +.label-default { + background-color: #eeeeee; +} + +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #d5d5d5; +} + +.label-primary { + background-color: #2196f3; + color: #fff; +} + +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #0c7cd5; +} + +.label-success { + background-color: #5cb85c; + color: #fff; +} + +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} + +.label-info { + background-color: #56CCF2; + color: #fff; +} + +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #27beee; +} + +.label-warning { + background-color: #F09819; + color: #fff; +} + +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #c97c0d; +} + +.label-danger { + background-color: #FF512F; + color: #fff; +} + +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #fb2900; +} + +.panel { + margin-bottom: 21px; + background-color: #fff; + border: 1px solid #f2f2f2; +} + +.panel .article-title { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + font-size: 18px; + line-height: 32px; + height: 64px; + word-break: break-all !important; + word-wrap: break-word !important; +} + +.panel-body { + padding: 15px; +} + +.panel-body:before, +.panel-body:after { + content: " "; + display: table; +} + +.panel-body:after { + clear: both; +} + +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid #f2f2f2; +} + +.panel-heading>.dropdown .dropdown-toggle { + color: inherit; +} + +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; +} + +.panel-title>a, +.panel-title>small, +.panel-title>.small, +.panel-title>small>a, +.panel-title>.small>a { + color: inherit; + text-decoration: none; +} + +.panel-footer { + padding: 10px 15px; + border-top: 1px solid #f2f2f2; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} + +/** + * panel-group-base + */ + +.panel.b-no { + border: 0; + margin-bottom: 0; +} + +.panel.b-no .panel-heading, +.panel.b-no .panel-body, +.panel.b-no .panel-footer { + border: 0; + padding-left: 0; + padding-right: 0; +} + +/* Badger*/ + +.panel-badger { + position: relative; +} + +.panel-badger:after { + content: ""; + position: absolute; + top: 0; + width: 0; + height: 0; + border-width: 4px; + border-style: solid; + border-color: #777777 transparent transparent #777777; +} + +.panel-badger:hover:after { + border-color: #2196f3 transparent transparent #2196f3; +} + +.badger-danger:after { + border-color: #FF512F transparent transparent #FF512F; +} + +.badger-warning:after { + border-color: #F09819 transparent transparent #F09819; +} + +.badger-success:after { + border-color: #5cb85c transparent transparent #5cb85c; +} + +.badger-info:after { + border-color: #56CCF2 transparent transparent #56CCF2; +} + +.badger-primary:after { + border-color: #2196f3 transparent transparent #2196f3; +} + +/* bg shortcodes */ + +.bg-gradient-info span, +.bg-gradient-info:before { + background: #56CCF2; + background: -webkit-linear-gradient(left, #56CCF2 0%, #2F80ED 80%, #2F80ED 100%); + background: -webkit-gradient(linear, left top, right top, from(#56CCF2), color-stop(80%, #2F80ED), to(#2F80ED)); + background: linear-gradient(to right, #56CCF2 0%, #2F80ED 80%, #2F80ED 100%); +} + +.bg-gradient-primary span, +.bg-gradient-primary:before { + background: #396afc; + background: -webkit-linear-gradient(left, #396afc 0%, #2948ff 80%, #2948ff 100%); + background: -webkit-gradient(linear, left top, right top, from(#396afc), color-stop(80%, #2948ff), to(#2948ff)); + background: linear-gradient(to right, #396afc 0%, #2948ff 80%, #2948ff 100%); +} + +.bg-gradient-success span, +.bg-gradient-success:before { + background: #44ea76; + background: -webkit-linear-gradient(left, #44ea76 0%, #39fad7 80%, #39fad7 100%); + background: -webkit-gradient(linear, left top, right top, from(#44ea76), color-stop(80%, #39fad7), to(#39fad7)); + background: linear-gradient(to right, #44ea76 0%, #39fad7 80%, #39fad7 100%); +} + +.bg-gradient-warning span, +.bg-gradient-warning:before { + background: #FF512F; + background: -webkit-linear-gradient(left, #FF512F 0%, #F09819 80%, #F09819 100%); + background: -webkit-gradient(linear, left top, right top, from(#FF512F), color-stop(80%, #F09819), to(#F09819)); + background: linear-gradient(to right, #FF512F 0%, #F09819 80%, #F09819 100%); +} + +.bg-gradient-danger span, +.bg-gradient-danger:before { + background: #FF512F; + background: -webkit-linear-gradient(left, #FF512F 0%, #DD2476 80%, #DD2476 100%); + background: -webkit-gradient(linear, left top, right top, from(#FF512F), color-stop(80%, #DD2476), to(#DD2476)); + background: linear-gradient(to right, #FF512F 0%, #DD2476 80%, #DD2476 100%); +} + +/* Button fancy */ + +.btn-fancy { + display: inline-block; + font-size: 17px; + letter-spacing: 0.03em; + text-transform: uppercase; + color: #ffffff; + position: relative; +} + +.btn-fancy:before { + content: ''; + display: inline-block; + height: 40px; + position: absolute; + bottom: -5px; + left: 30px; + right: 30px; + z-index: -1; + -webkit-filter: blur(20px) brightness(0.95); + filter: blur(20px) brightness(0.95); + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + -webkit-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; +} + +.btn-fancy i { + margin-top: -1px; + margin-right: 20px; + font-size: 1.265em; + vertical-align: middle; +} + +.btn-fancy span { + display: inline-block; + padding: 18px 60px; + border-radius: 50em; + position: relative; + z-index: 2; + will-change: transform, filter; + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + -webkit-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; +} + +.btn-fancy:focus, +.btn-fancy:active { + color: #ffffff; +} + +.btn-fancy:hover { + color: #ffffff; +} + +.btn-fancy:hover span { + -webkit-filter: brightness(1.05) contrast(1.05); + filter: brightness(1.05) contrast(1.05); + -webkit-transform: scale(0.95); + -ms-transform: scale(0.95); + transform: scale(0.95); +} + +.btn-fancy:hover:before { + bottom: 0; + -webkit-filter: blur(10px) brightness(0.95); + filter: blur(10px) brightness(0.95); +} + +.btn-fancy.pop-onhover:before { + opacity: 0; + bottom: 10px; +} + +.btn-fancy.pop-onhover:hover:before { + bottom: -7px; + opacity: 1; + -webkit-filter: blur(20px); + filter: blur(20px); +} + +.btn-fancy.pop-onhover:hover span { + -webkit-transform: scale(1.04); + -ms-transform: scale(1.04); + transform: scale(1.04); +} + +.btn-fancy.pop-onhover:hover:active span { + -webkit-filter: brightness(1) contrast(1); + filter: brightness(1) contrast(1); + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; +} + +.btn-fancy.pop-onhover:hover:active:before { + bottom: 0; + -webkit-filter: blur(10px) brightness(0.95); + filter: blur(10px) brightness(0.95); + -webkit-transition: all 0.2s ease-out; + transition: all 0.2s ease-out; +} + +/* + * Component: table + * css like github + * ---------------- + */ + +table { + border: 1px solid #f2f2f2; +} + +table>thead>tr>th, +table>thead>tr>td, +table>tbody>tr>th, +table>tbody>tr>td, +table>tfoot>tr>th, +table>tfoot>tr>td { + border: 1px solid #f2f2f2; +} + +table>tbody>tr:nth-of-type(odd) { + background-color: #f8f8f8; +} + +table>tbody>tr:hover { + background-color: #fbfbfb; +} + +table { + padding: 0; + width: 100%; + max-width: 100%; + margin: 10px 0; +} + +table>thead>tr>th, +table>thead>tr>td, +table>tbody>tr>th, +table>tbody>tr>td, +table>tfoot>tr>th, +table>tfoot>tr>td { + padding: 6px 13px; +} + +table>tbody+tbody { + border-top: 2px solid #f2f2f2; +} + +table table { + background-color: #fff; +} + +.modal button.close { + position: absolute; + right: 10px; + top: 10px; + z-index: 99; +} + +.modal-small .modal-dialog { + width: 480px; +} + +@font-face { + font-family: "icon"; + src: url("iconfont.eot?t=1525101408939"); + /* IE9*/ + src: url("iconfont.eot?t=1525101408939#iefix") format("embedded-opentype"), url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff"), url("iconfont.ttf?t=1525101408939") format("truetype"), url("iconfont.svg?t=1525101408939#icon") format("svg"); +} + +.icon { + display: inline-block; + font: normal normal normal 14px/1 icon; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -webkit-text-stroke-width: 0.2px; + -moz-osx-font-smoothing: grayscale; +} + +.icon-diandian:before { + content: "\e63a"; +} + +.icon-huaban:before { + content: "\e63c"; +} + +.icon-code-fork:before { + content: "\e67a"; +} + +.icon-more:before { + content: "\e6c0"; +} + +.icon-zhihu:before { + content: "\e6d1"; +} + +.icon-linkedin:before { + content: "\e724"; +} + +.icon-eye-fill:before { + content: "\e64f"; +} + +.icon-stackexchange:before { + content: "\e8b2"; +} + +.icon-tag:before { + content: "\e6a3"; +} + +.icon-starfish:before { + content: "\e62e"; +} + +.icon-home:before { + content: "\e660"; +} + +.icon-search:before { + content: "\e61c"; +} + +.icon-project:before { + content: "\e63e"; +} + +.icon-dialog:before { + content: "\e613"; +} + +.icon-twitter:before { + content: "\ec9c"; +} + +.icon-github:before { + content: "\e70a"; +} + +.icon-time:before { + content: "\e669"; +} + +.icon-voice:before { + content: "\e65a"; +} + +.icon-google:before { + content: "\e601"; +} + +.icon-weibo:before { + content: "\e64b"; +} + +.icon-segmentfault:before { + content: "\e610"; +} + +.icon-star-fill:before { + content: "\e630"; +} + +.icon-phone:before { + content: "\e68a"; +} + +.icon-cup-fill:before { + content: "\e614"; +} + +.icon-jiaju:before { + content: "\e671"; +} + +.icon-qzone:before { + content: "\e603"; +} + +.icon-home-fill:before { + content: "\e617"; +} + +.icon-clock:before { + content: "\e618"; +} + +.icon-file:before { + content: "\e66f"; +} + +.icon-comment:before { + content: "\e61a"; +} + +.icon-cup:before { + content: "\e62c"; +} + +.icon-share:before { + content: "\e66a"; +} + +.icon-star-half:before { + content: "\e62f"; +} + +.icon-star:before { + content: "\e619"; +} + +.icon-tencent-weibo:before { + content: "\e602"; +} + +.icon-book:before { + content: "\e79d"; +} + +.icon-bitbucket:before { + content: "\e64e"; +} + +.icon-facebook:before { + content: "\e6e3"; +} + +.icon-email:before { + content: "\e667"; +} + +.icon-zcool:before { + content: "\e60c"; +} + +.icon-social-media:before { + content: "\e68b"; +} + +.icon-douban:before { + content: "\e60f"; +} + +.icon-coding:before { + content: "\e600"; +} + +.icon-github-fill:before { + content: "\e71d"; +} + +.icon-qq:before { + content: "\e611"; +} + +.icon-shu-fill:before { + content: "\e615"; +} + +.icon-pinterest:before { + content: "\e697"; +} + +.icon-tags:before { + content: "\e6c4"; +} + +.icon-bill:before { + content: "\e61b"; +} + +.icon-shu:before { + content: "\e616"; +} + +.icon-book-shelf:before { + content: "\e60d"; +} + +.icon-target:before { + content: "\e695"; +} + +.icon-profile:before { + content: "\e6e2"; +} + +.icon-alipay:before { + content: "\e938"; +} + +.icon-skype:before { + content: "\e604"; +} + +.icon-juejin:before { + content: "\e605"; +} + +.icon-code:before { + content: "\e73f"; +} + +.icon-list:before { + content: "\e61e"; +} + +.icon-map-marker:before { + content: "\e609"; +} + +.icon-stackoverflow:before { + content: "\e606"; +} + +.icon-hourglass:before { + content: "\e60e"; +} + +.icon-behance:before { + content: "\e67b"; +} + +.icon-folder-open:before { + content: "\e6b4"; +} + +.icon-folder:before { + content: "\e60a"; +} + +.icon-menu:before { + content: "\e607"; +} + +.icon-users:before { + content: "\e60b"; +} + +.icon-eye:before { + content: "\e657"; +} + +.icon-wechat:before { + content: "\e65e"; +} + +.icon-number:before { + content: "\e658"; +} + +.icon-gitlab:before { + content: "\e67c"; +} + +.icon-rss:before { + content: "\e63d"; +} + +.icon-archives:before { + content: "\e62d"; +} + +.icon-68design:before { + content: "\e608"; +} + +.icon-dribble:before { + content: "\e982"; +} + +.icon-wepay:before { + content: "\e629"; +} + +.icon-youdao-note:before { + content: "\e8a6"; +} + +.icon-book-fill:before { + content: "\e659"; +} + +.icon-hezuo:before { + content: "\e6e5"; +} + +.icon-link:before { + content: "\e635"; +} + +.icon-archives-fill:before { + content: "\e694"; +} + +.icon-anchor:before { + content: "\e858"; +} + +.icon-angle-down:before { + content: "\e85e"; +} + +.icon-angle-left:before { + content: "\e85f"; +} + +.icon-angle-up:before { + content: "\e860"; +} + +.icon-angle-right:before { + content: "\e862"; +} + +.icon-calendar:before { + content: "\e895"; +} + +.icon-calendar-check:before { + content: "\e896"; +} + +.icon-calendar-minus:before { + content: "\e897"; +} + +.icon-calendar-plus:before { + content: "\e899"; +} + +.icon-calendar-times:before { + content: "\e89a"; +} + +.icon-close:before { + content: "\e8c4"; +} + +.icon-delicious:before { + content: "\e8e2"; +} + +.icon-plus:before { + content: "\e99d"; +} + +.icon-gg:before { + content: "\e6fd"; +} + +.icon-friendship:before { + content: "\e612"; +} + +.icon-gitee:before { + content: "\e61d"; +} + +pre .comment { + color: #8e908c; +} + +pre .variable, +pre .attribute, +pre .tag, +pre .regexp, +pre .ruby .constant, +pre .xml .tag .title, +pre .xml .pi, +pre .xml .doctype, +pre .html .doctype, +pre .css .id, +pre .css .class, +pre .css .pseudo { + color: #c82829; +} + +pre .number, +pre .preprocessor, +pre .built_in, +pre .literal, +pre .params, +pre .constant { + color: #f5871f; +} + +pre .ruby .class .title, +pre .css .rules .attribute { + color: #718c00; +} + +pre .string, +pre .value, +pre .inheritance, +pre .header, +pre .ruby .symbol, +pre .xml .cdata { + color: #718c00; +} + +pre .title, +pre .css .hexcolor { + color: #3e999f; +} + +pre .function, +pre .python .decorator, +pre .python .title, +pre .ruby .function .title, +pre .ruby .title .keyword, +pre .perl .sub, +pre .javascript .title, +pre .js .title, +pre .coffeescript .title { + color: #4271ae; +} + +pre .keyword, +pre .javascript .function, +pre .js .function { + color: #8959a8; +} + +pre, +.highlight { + background: #fafafa; + margin: 10px 0; + padding: 15px 10px; + overflow: auto; + font-size: 13px; + color: #4d4d4c; + line-height: 1.5; +} + +.highlight .gutter pre, +.gist .gist-file .gist-data .line-numbers { + color: #666; +} + +code { + text-shadow: 0 1px #fff; + padding: 0.2em 0.4em; + margin: 0 0.3em; + color: #555; + background: #eee; + border-radius: 3px; + font-size: 90%; +} + +pre code { + background: none; + text-shadow: none; + padding: 0; +} + +.highlight { + position: relative; + padding: 32px 10px 0 10px; + border-radius: 4px; +} + +.highlight:before { + display: block; + content: ' '; + height: 32px; + position: absolute; + top: 0; + left: 0; + right: 0; + background-color: #1e1e1e; + padding: 0 10px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.highlight:after { + content: " "; + position: absolute; + border-radius: 50%; + background: #fc625d; + width: 10px; + height: 10px; + top: 0; + left: 15px; + margin-top: 11px; + -webkit-box-shadow: 20px 0 #fdbc40, 40px 0 #35cd4b; + box-shadow: 20px 0 #fdbc40, 40px 0 #35cd4b; +} + +.highlight pre { + border: none; + margin: 0; +} + +.highlight table { + position: relative; + border: none; + width: 100%; + margin: 0; + padding: 0; +} + +.highlight tr { + border: none; +} + +.highlight td, +.highlight th { + border: none; + padding: 0; +} + +.highlight td.code, +.highlight th.code { + width: 100% !important; +} + +.highlight figcaption { + font-size: 0.85em; + color: #8e908c; + line-height: 1em; + margin-bottom: 1em; +} + +.highlight figcaption a { + float: right; +} + +.highlight .line { + height: 24px; + line-height: 24px; +} + +.highlight .gutter pre { + text-align: right; + padding-right: 0; + padding-left: 0; + color: #ccc; +} + +/* + * Sidebar + */ + +/* Hide for mobile, show later */ + +.header { + background-color: #fbfbfb; +} + +.sidebar { + background-color: #fdfdfd; +} + +.sidebar .slimContent { + padding: 20px; +} + +.main { + position: relative; + min-height: 100vh; + padding: 15px; +} + +.main:before, +.main:after { + content: " "; + display: table; +} + +.main:after { + clear: both; +} + +.footer { + padding: 20px; + background-color: #fbfbfb; +} + +/* + * main-center + */ + +body.main-center .sidebar { + left: auto; + right: 0; + border-left: 1px solid #f6f6f6; + border-right: 0; +} + +/* + * main-left + */ + +body.main-left .header { + left: auto; + right: 0; + border-left: 1px solid #f6f6f6; + border-right: 0; +} + +body.main-left .sidebar { + left: auto; + right: 0; + border-left: 1px solid #f6f6f6; + border-right: 0; +} + +body.main-left .footer { + left: auto; + right: 0; +} + +/* + * main-right + */ + +body.no-sidebar .sidebar { + display: none !important; +} + +/** + * Sidebar navigation + */ + +.main-nav { + float: none !important; +} + +.main-nav>li { + display: block; + width: 100%; + position: relative; +} + +.main-nav>li>a { + color: #555555; +} + +.main-nav>li .menu-title { + margin-left: 15px; +} + +.main-nav>.active a, +.main-nav>.active a:hover, +.main-nav>.active a:focus { + color: #333333; + background: #f4f4f4; +} + +/** + * profile-block + */ + +.profile-block { + padding: 20px 15px 10px 15px; +} + +#avatar { + width: 64px; + height: 64px; + display: inline-block; +} + +#avatar img { + width: 100%; + max-height: 100%; + height: auto !important; +} + +#name { + font-size: 18px; + margin-top: 10px; + margin-bottom: 0; +} + +#title { + font-size: 13px; + margin-top: 5px; + margin-bottom: 5px; +} + +/** + * search + */ + +.sidebar-form { + border-radius: 3px; + border: 1px solid #eee; + margin: 0 15px 15px 15px; +} + +.sidebar-form input[type="text"], +.sidebar-form .btn { + -webkit-box-shadow: none; + box-shadow: none; + background-color: transparent; + border: 1px solid transparent; + height: 32px; +} + +.sidebar-form input[type="text"]:focus, +.sidebar-form .btn:focus { + outline: none; +} + +.sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} + +.sidebar-form input[type="text"]:focus, +.sidebar-form input[type="text"]:focus+.input-group-btn .btn { + background-color: #fff; + color: #666; +} + +.sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} + +.header { + /** + * navbar + */ +} + +.header .navbar-collapse { + padding-left: 0; + padding-right: 0; +} + +.header .navbar-collapse .navbar-nav { + margin: 0; +} + +.header .navbar-toggle .icon-bar { + background-color: #2196f3; +} + +/* + * Main content + */ + +.main .pager { + text-align: left; + margin: 10px 0; +} + +.main .pager .disabled { + cursor: not-allowed; +} + +.main .pager>a, +.main .pager>.page-number { + line-height: 32px; + float: left; +} + +.main .pager a { + color: #666; + border: 0; + line-height: 32px; + padding: 0; +} + +.main .pager a:link, +.main .pager a:visited { + background-color: transparent; +} + +.main .pager a:hover { + color: #0a6ebd; + background-color: transparent; +} + +.main .pager .prev { + margin-right: 10px; +} + +.main .pager .page-number.current { + color: #2196f3; +} + +.main .pager .page-number+.page-number { + margin-left: 10px; +} + +.main .pager .page-number+.next { + margin-left: 10px; +} + +.main .total-article { + margin: 10px 0; + line-height: 32px; + color: #999; +} + +.main .page-header { + margin-top: 0; +} + +.main .article-list article { + border-bottom: 1px solid #f2f2f2; +} + +.main .article-list article:last-child { + border-bottom: 0; +} + +.main .article-meta { + font-size: 13px; + color: #999; +} + +.main .article-meta a { + color: #999; +} + +.main .article-meta a:hover { + color: #0a6ebd; + text-decoration: none; +} + +.main .article-meta span+span { + margin-left: 10px; +} + +.main .content { + min-height: 85vh; +} + +.main.has-sticky .content { + margin-bottom: 70px; +} + +#comments .gitment-footer-container, +#comments .gitment-footer-project-link { + display: none !important; +} + +.panel .label, +.widget .label { + font-weight: normal; +} + +.widget:before, +.widget:after { + content: " "; + display: table; +} + +.widget:after { + clear: both; +} + +.widget .widget-title { + font-size: 18px; + color: #000; +} + +.widget time { + color: #999; + font-size: 12px; + text-transform: uppercase; +} + +.widget p { + margin-bottom: 0; +} + +.widget ul { + margin-left: 0; + padding-left: 0; + list-style: none; +} + +.widget .category-link { + color: #0a6ebd; +} + +.category-list-count, +.tag-list-count, +.archive-list-count { + padding-left: 5px; + color: #999; + font-size: 0.85em; +} + +.category-list-count:before, +.tag-list-count:before, +.archive-list-count:before { + content: "("; +} + +.category-list-count:after, +.tag-list-count:after, +.archive-list-count:after { + content: ")"; +} + +.category-list, +.archive-list, +.tag-list { + line-height: 1.75; +} + +.category-list li:before, +.archive-list li:before, +.tag-list li:before { + color: #ccc; + content: "▪"; + font-size: 12px; + margin-right: 6px; + -webkit-transition: 0.2s ease; + transition: 0.2s ease; +} + +.category-list-child { + padding-left: 15px; +} + +.recent-post-list li+li { + margin-top: 15px; +} + +.recent-post-list li .item-thumb, +.recent-post-list li .item-inner { + display: table-cell; + vertical-align: middle; +} + +.recent-post-list li .item-thumb { + opacity: 1; + padding-right: 10px; + -webkit-transition: all 0.2s ease; + transition: all 0.2s ease; +} + +.recent-post-list li .item-thumb .thumb { + width: 50px; + height: 50px; + display: block; + position: relative; + overflow: hidden; +} + +.recent-post-list li .item-thumb .thumb span { + width: 100%; + height: 100%; + display: block; +} + +.recent-post-list li .item-thumb .thumb .thumb-image { + position: absolute; + background-size: cover; + background-position: center; +} + +.recent-post-list li .item-thumb .thumb .thumb-none { + background-image: url("../images/thumb-default.png"); + background-size: 100% 100%; +} + +.recent-post-list li:hover .item-thumb { + opacity: 0.8; +} + +.sidebar-toc.collapse { + display: none !important; +} + +.sidebar-toc.in { + display: block !important; +} + +.tagcloud a { + display: inline-block; + margin-bottom: 0.2em; + padding: .3em .6em; + font-size: 75% !important; + line-height: 1; + background-color: #eee; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} + +.bar .pager .next>a, +.bar .pager .next>span { + float: none; +} + +.bar.bar-footer { + position: relative; + background-color: #fff; + bottom: -15px; +} + +.bar.bar-footer:before { + content: ''; + position: absolute; + width: -webkit-calc(100% + 30px); + width: calc(100% + 30px); + height: 52px; + left: -15px; + border-top: 1px solid #f6f6f6; + border-bottom: 1px solid #fff; + background-color: #fff; +} + +.bar .bar-inner { + position: relative; + z-index: 9; +} + +.bar .bar-inner:before, +.bar .bar-inner:after { + content: " "; + display: table; +} + +.bar .bar-inner:after { + clear: both; +} + +.bar .bar-right { + margin: 10px 0; + float: right; +} + +.toggle-toc { + cursor: pointer; + margin-left: 10px; +} + +.toggle-toc a { + display: inline-block; + line-height: 32px; + text-align: center; +} + +/** + * footer + */ + +.footer { + color: #999; +} + +.footer .copyright { + font-size: 12px; +} + +.footer .copyright a { + color: #999; + text-decoration: none; +} + +.footer .copyright a:hover { + color: #0a6ebd; +} + +/** + * .wave-icon + */ + +.wave-icon { + display: inline-block; + position: relative; +} + +.wave-icon .wave-circle { + display: block; + border-radius: 50%; + background-color: transparent; +} + +.wave-icon .wave-circle:before, +.wave-icon .wave-circle:after { + content: ''; + border: 10px solid #2196f3; + background: #2196f3; + border-radius: 50%; + position: absolute; + top: 50%; + left: 50%; + z-index: 1; +} + +.wave-icon .wave-circle:before { + height: 74px; + width: 74px; + -webkit-animation: pulse 5s ease-out; + animation: pulse 5s ease-out; + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; + margin-top: -37px; + margin-left: -37px; + opacity: 0; +} + +.wave-icon .wave-circle:after { + height: 98px; + width: 98px; + -webkit-animation: pulse 5s ease-out; + animation: pulse 5s ease-out; + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; + margin-top: -49px; + margin-left: -49px; + opacity: 0.3; +} + +.wave-icon .icon { + position: relative; + display: block; + width: 50px; + height: 50px; + line-height: 50px; + text-align: center; + background-color: #2196f3; + border-radius: 50%; + font-size: 24px; + color: #fff; + z-index: 2; +} + +.wave-icon.wave-icon-info .wave-circle:before, +.wave-icon.wave-icon-info .wave-circle:after { + border: 10px solid #56CCF2; + background: #56CCF2; +} + +.wave-icon.wave-icon-info .icon { + background-color: #56CCF2; +} + +.wave-icon.wave-icon-primary .wave-circle:before, +.wave-icon.wave-icon-primary .wave-circle:after { + border: 10px solid #2196f3; + background: #2196f3; +} + +.wave-icon.wave-icon-primary .icon { + background-color: #2196f3; +} + +.wave-icon.wave-icon-warning .wave-circle:before, +.wave-icon.wave-icon-warning .wave-circle:after { + border: 10px solid #F09819; + background: #F09819; +} + +.wave-icon.wave-icon-warning .icon { + background-color: #F09819; +} + +.wave-icon.wave-icon-success .wave-circle:before, +.wave-icon.wave-icon-success .wave-circle:after { + border: 10px solid #5cb85c; + background: #5cb85c; +} + +.wave-icon.wave-icon-success .icon { + background-color: #5cb85c; +} + +.wave-icon.wave-icon-danger .wave-circle:before, +.wave-icon.wave-icon-danger .wave-circle:after { + border: 10px solid #FF512F; + background: #FF512F; +} + +.wave-icon.wave-icon-danger .icon { + background-color: #FF512F; +} + +@-webkit-keyframes pulse { + 0% { + -webkit-transform: scale(0); + opacity: 0.0; + } + + 25% { + -webkit-transform: scale(0); + opacity: 0.1; + } + + 50% { + -webkit-transform: scale(0.1); + opacity: 0.3; + } + + 75% { + -webkit-transform: scale(0.5); + opacity: 0.5; + } + + 100% { + -webkit-transform: scale(1); + opacity: 0.0; + } +} + +.repo-list { + list-style: none; + padding-left: 0; +} + +.repo { + position: relative; + list-style-type: none; + border: 1px solid #f2f2f2; + margin-bottom: 15px; + overflow: hidden; +} + +.repo-title { + padding: 0 15px; + margin: 15px 0; + font-size: 16px; + font-weight: 600; +} + +.repo-body { + display: -webkit-box; + padding: 0 15px; + margin: 0 0 20px; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-height: 1.5em; + height: 3em; + word-break: break-all !important; + word-wrap: break-word !important; +} + +.repo-image { + position: relative; + display: table; + width: 101%; + height: 3px; + margin: -1px -1px 15px; + background-color: #666; +} + +.repo-meta { + padding: 0 15px; + margin-top: 5px; + margin-bottom: 15px; + color: #777; + font-size: 12px; + text-align: right; +} + +.repo-meta:before, +.repo-meta:after { + content: " "; + display: table; +} + +.repo-meta:after { + clear: both; +} + +.repo-meta .meta+.meta { + margin-left: 15px; +} + +/* + * Global add-ons + */ + +.text-collapsed { + display: none; +} + +.text-in { + display: inline-block; +} + +.collapsed .text-collapsed { + display: inline-block; +} + +.collapsed .text-in { + display: none; +} + +.sub-header { + padding-bottom: 10px; + border-bottom: 1px solid #eee; +} + +.article-header { + margin-bottom: 20px; +} + +.article-footer { + margin-top: 20px; +} + +/** + * collection + */ + +.collection { + position: relative; +} + +.collection a.collection-item { + display: block; + -webkit-transition: .25s; + transition: .25s; + color: #777777; +} + +.collection a:not(.active):hover { + color: #333333; +} + +.collection .collection-item { + padding: 8px 0; + margin: 0; +} + +.article-list .article-title { + font-size: 18px; +} + +.article-toc .toc-title { + font-size: 18px; + color: #000; +} + +.article-toc .toc { + list-style: none; + padding-left: 0; + line-height: 2.0; +} + +.article-toc .toc ol { + list-style: none; + padding-left: 10px; +} + +.article-toc .toc .toc-item { + position: relative; +} + +.article-toc .toc .toc-item .markdownIt-Anchor { + position: absolute; + left: 0; + right: 0; + top: 0; + padding: 14px 0; +} + +.marked-body h1, +.marked-body h2, +.marked-body h3, +.marked-body h4, +.marked-body h5, +.marked-body h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; +} + +.marked-body h1 { + padding-bottom: 0.3em; + font-size: 2em; + border-bottom: 1px solid #f2f2f2; +} + +.marked-body h2 { + padding-bottom: 0.3em; + font-size: 1.5em; + border-bottom: 1px solid #f2f2f2; +} + +.marked-body a { + color: #2196f3; + text-decoration: none; +} + +.marked-body a:focus, +.marked-body a:hover { + color: #0a6ebd; + text-decoration: none; +} + +.marked-body ul, +.marked-body ol { + padding-left: 0; + margin-left: 20px; +} + +/* + * We are hiding the invisible nav outside the screen + * so we need to avoid the horizontal scroll + */ + +body.okayNav-loaded { + overflow-x: hidden; +} + +.okayNav { + position: relative; +} + +.okayNav:before, +.okayNav:after { + content: " "; + display: table; +} + +.okayNav:after { + clear: both; +} + +.okayNav:not(.loaded) { + visibility: hidden; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.okayNav ul { + /* We want two navigations - one hidden and one visible */ + float: left; + padding-left: 0; +} + +.okayNav ul li { + display: inline-block; + margin-left: 15px; +} + +.okayNav a { + position: relative; + z-index: 1; +} + +.okayNav a.active { + color: #0a6ebd; +} + +.okayNav__nav--visible { + overflow: hidden; + white-space: nowrap; +} + +.okayNav__nav--visible li { + display: inline-block; + margin-left: 15px; +} + +.okayNav__nav--visible li:first-child { + margin-left: 0; +} + +.okayNav__nav--visible a { + /* Link styling for the visible part of the nav */ + display: block; + -webkit-transition: color 200ms cubic-bezier(0.55, 0, 0.1, 1); + transition: color 200ms cubic-bezier(0.55, 0, 0.1, 1); +} + +.okayNav__nav--visible:empty~.okayNav__menu-toggle { + top: 0; +} + +/* Link styling for the off-screen part of the nav */ + +.okayNav__nav--invisible { + display: none; + position: absolute; + width: 100%; + top: 24px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + padding-top: 15px; + padding-bottom: 15px; + background: #fff; +} + +.okayNav__nav--invisible li { + display: inline-block; +} + +.okayNav__nav--invisible li a { + display: block; + padding: 6px 15px; + min-width: 100px; +} + +.okayNav__nav--invisible.nav-left { + left: 0; +} + +.okayNav__nav--invisible.nav-right { + right: 0; +} + +.okayNav__nav--invisible.transition-enabled { + -webkit-transition: -webkit-transform 400ms cubic-bezier(0.55, 0, 0.1, 1); + transition: -webkit-transform 400ms cubic-bezier(0.55, 0, 0.1, 1); + transition: transform 400ms cubic-bezier(0.55, 0, 0.1, 1); + transition: transform 400ms cubic-bezier(0.55, 0, 0.1, 1), -webkit-transform 400ms cubic-bezier(0.55, 0, 0.1, 1); +} + +.okayNav__nav--invisible.nav-open { + display: block; + z-index: 99; + border: 1px solid #f2f2f2; +} + +/* Kebab icon */ + +.okayNav__menu-toggle { + position: relative; + z-index: 1; + float: right; + cursor: pointer; + -webkit-transition: -webkit-transform 400ms cubic-bezier(0.55, 0, 0.1, 1); + transition: -webkit-transform 400ms cubic-bezier(0.55, 0, 0.1, 1); + transition: transform 400ms cubic-bezier(0.55, 0, 0.1, 1); + transition: transform 400ms cubic-bezier(0.55, 0, 0.1, 1), -webkit-transform 400ms cubic-bezier(0.55, 0, 0.1, 1); +} + +.okayNav__menu-toggle.okay-invisible { + position: absolute; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + opacity: 0; +} + +.okayNav__menu-toggle span { + background: #666; + display: inline-block; + width: 2px; + height: 2px; + margin: auto 1px; + pointer-events: none; + border-radius: 50%; + vertical-align: middle; +} + +.okayNav__menu-toggle.icon--active { + /* Kebab icon when off-screen nav is open */ +} + +.okayNav__menu-toggle.icon--active span { + background: #0a6ebd; +} + +.okayNav a { + color: #2e2e33; + font-weight: 400; +} + +.okayNav a:hover { + color: #0a6ebd; +} + +/** + * social-links + */ + +.social-links { + list-style: none; + padding: 0; + text-align: left; +} + +.social-links li { + list-style: none; + display: inline-block; + margin-left: 10px; +} + +.social-links li:first-child { + margin-left: 0; +} + +.header .social-links { + padding: 10px 20px; +} + +.footer .social-links { + margin-bottom: 5px; +} + +@font-face { + font-family: "socialshare"; + src: url("../fonts/iconfont.eot"); + /* IE9*/ + src: url("../fonts/iconfont.eot?#iefix") format("embedded-opentype"), url("../fonts/iconfont.woff") format("woff"), url("../fonts/iconfont.ttf") format("truetype"), url("../fonts/iconfont.svg#iconfont") format("svg"); +} + +.social-share { + display: inline-block; + font-size: 16px; +} + +.social-share a { + position: relative; + text-decoration: none; + margin-left: 16px; + display: inline-block; + outline: none; + line-height: 32px; +} + +.social-share .social-share-icon { + position: relative; + display: inline-block; + height: 32px; + line-height: 32px; + color: #999; + text-align: center; + vertical-align: middle; + -webkit-transition: background 0.6s ease-out 0s; + transition: background 0.6s ease-out 0s; +} + +.social-share .social-share-icon:hover { + color: #666; +} + +.social-share .icon-weibo:hover { + color: #ff763b; +} + +.social-share .icon-tencent:hover { + color: #56b6e7; +} + +.social-share .icon-qq:hover { + color: #56b6e7; +} + +.social-share .icon-qzone:hover { + color: #FDBE3D; +} + +.social-share .icon-douban:hover { + color: #33b045; +} + +.social-share .icon-linkedin:hover { + color: #0077B5; +} + +.social-share .icon-facebook:hover { + color: #44619D; +} + +.social-share .icon-google:hover { + color: #db4437; +} + +.social-share .icon-twitter:hover { + color: #55acee; +} + +.social-share .icon-diandian:hover { + color: #307DCA; +} + +.social-share .icon-wechat { + position: relative; +} + +.social-share .icon-wechat:hover { + color: #7bc549; +} + +.social-share .icon-wechat .wechat-qrcode { + display: none; + border: 1px solid #eee; + position: absolute; + z-index: 9; + top: -209px; + left: -90px; + width: 200px; + height: 200px; + color: #666; + font-size: 12px; + text-align: center; + background-color: #fff; + -webkit-transition: all 200ms; + transition: all 200ms; + -webkit-tansition: all 350ms; + -moz-transition: all 350ms; +} + +.social-share .icon-wechat .wechat-qrcode.bottom { + top: 40px; + left: -84px; +} + +.social-share .icon-wechat .wechat-qrcode.bottom:after { + display: none; +} + +.social-share .icon-wechat .wechat-qrcode h4 { + font-weight: normal; + height: 26px; + line-height: 26px; + font-size: 12px; + background-color: #f3f3f3; + margin: 0; + padding: 0; + color: #777; +} + +.social-share .icon-wechat .wechat-qrcode .qrcode { + width: 105px; + margin: 15px auto; +} + +.social-share .icon-wechat .wechat-qrcode .qrcode table { + margin: 0 !important; +} + +.social-share .icon-wechat .wechat-qrcode .help p { + font-weight: normal; + line-height: 16px; + padding: 0; + margin: 0; +} + +.social-share .icon-wechat .wechat-qrcode:before { + content: ''; + position: absolute; + left: 50%; + margin-left: -6px; + bottom: -15px; + width: 0; + height: 0; + border-width: 8px 6px 6px 6px; + border-style: solid; + border-color: #eee transparent transparent transparent; +} + +.social-share .icon-wechat .wechat-qrcode:after { + content: ''; + position: absolute; + left: 50%; + margin-left: -6px; + bottom: -13px; + width: 0; + height: 0; + border-width: 8px 6px 6px 6px; + border-style: solid; + border-color: #fff transparent transparent transparent; +} + +.social-share .icon-wechat:hover .wechat-qrcode { + display: block; +} + +.btn-donate { + position: absolute; + bottom: 10px; + left: 50%; + margin-left: -25px; + width: 50px; + height: 50px; + line-height: 50px; + padding: 0; + border-radius: 50%; + font-size: 18px; + cursor: pointer; + z-index: 99; +} + +.btn-donate:focus, +.btn-donate:hover, +.btn-donate:active { + border-color: transparent !important; + outline: none !important; +} + +.btn-donate.btn-fancy { + background-color: transparent; +} + +.btn-donate.btn-fancy span { + width: 50px; + height: 50px; + padding: 0; +} + +.donate { + overflow: hidden; +} + +.donate-box { + text-align: center; + padding-top: 30px; +} + +.donate-box .donate-head { + width: 100%; + height: 80px; + text-align: center; + line-height: 60px; + color: #a3a3a3; + font-size: 16px; + position: relative; +} + +.donate-box .donate-head:before, +.donate-box .donate-head:after { + font-family: Arial, Helvetica, sans-serif; + background: none; + width: 0px; + height: 0px; + font-style: normal; + color: #eee; + font-size: 100px; + position: absolute; + top: 15px; +} + +.donate-box .donate-head:before { + content: '\201c'; + left: 30px; +} + +.donate-box .donate-head:after { + content: '\201d'; + right: 70px; +} + +.donate-box .donate-footer { + padding-top: 35px; +} + +.donate-box .donate-payimg { + display: inline-block; + padding: 10px; + border: 6px solid #ea5f00; + margin: 0 auto; + border-radius: 3px; +} + +.donate-box .donate-payimg img { + display: block; + text-align: center; + width: 140px; + height: 140px; +} + +.book .media-middle { + display: inline-block; + width: 115px; +} + +.ins-search { + display: none; +} + +.ins-search.show { + display: block; +} + +.ins-selectable { + cursor: pointer; +} + +.ins-search-mask, +.ins-search-container { + position: fixed; +} + +.ins-search-mask { + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1050; + background: rgba(0, 0, 0, 0.5); +} + +.ins-input-wrapper { + position: relative; +} + +.ins-search-input { + width: 100%; + border: none; + outline: none; + font-size: 16px; + -webkit-box-shadow: none; + box-shadow: none; + font-weight: 200; + border-radius: 0; + background: #fff; + line-height: 20px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 12px 28px 12px 20px; + border-bottom: 1px solid #e2e2e2; + font-family: "Microsoft Yahei Light", "Microsoft Yahei", Helvetica, Arial, sans-serif; +} + +.ins-close { + top: 50%; + right: 6px; + width: 20px; + height: 20px; + font-size: 24px; + margin-top: -15px; + position: absolute; + text-align: center; + opacity: 1.0; + color: #666; + display: inline-block; +} + +.ins-close:hover { + color: #006bde; +} + +.ins-search-container { + left: 50%; + top: 100px; + z-index: 1051; + bottom: 100px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + width: 540px; + margin-left: -270px; +} + +.ins-section-wrapper { + left: 0; + right: 0; + top: 45px; + bottom: 0; + overflow-y: auto; + position: absolute; +} + +.ins-section-container { + position: relative; + background: #f7f7f7; +} + +.ins-section { + font-size: 14px; + line-height: 16px; +} + +.ins-section .ins-section-header, +.ins-section .ins-search-item { + padding: 8px 15px; +} + +.ins-section .ins-section-header { + color: #9a9a9a; + border-bottom: 1px solid #e2e2e2; +} + +.ins-section .ins-slug { + margin-left: 5px; + color: #9a9a9a; +} + +.ins-section .ins-slug:before { + content: '('; +} + +.ins-section .ins-slug:after { + content: ')'; +} + +.ins-section .ins-search-item header, +.ins-section .ins-search-item .ins-search-preview { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.ins-section .ins-search-item header .icon { + margin-right: 8px; +} + +.ins-section .ins-search-item .ins-search-preview { + height: 15px; + font-size: 12px; + color: #9a9a9a; + margin: 5px 0 0 20px; +} + +.ins-section .ins-search-item:hover, +.ins-section .ins-search-item.active { + color: #fff; + background: #006bde; +} + +.ins-section .ins-search-item:hover .ins-slug, +.ins-section .ins-search-item.active .ins-slug, +.ins-section .ins-search-item:hover .ins-search-preview, +.ins-section .ins-search-item.active .ins-search-preview { + color: #fff; +} + +.theme-black .header, +.theme-blue .header, +.theme-green .header, +.theme-purple .header { + color: #fff; +} + +.theme-black .header a, +.theme-blue .header a, +.theme-green .header a, +.theme-purple .header a { + color: #efefef; +} + +.theme-black .header #location, +.theme-blue .header #location, +.theme-green .header #location, +.theme-purple .header #location { + color: rgba(255, 255, 255, 0.75) !important; +} + +.theme-black .header .navbar-toggle .icon-bar, +.theme-blue .header .navbar-toggle .icon-bar, +.theme-green .header .navbar-toggle .icon-bar, +.theme-purple .header .navbar-toggle .icon-bar { + background-color: #fff; +} + +.theme-black .footer, +.theme-blue .footer, +.theme-green .footer, +.theme-purple .footer { + color: rgba(255, 255, 255, 0.75); +} + +.theme-black .footer a, +.theme-blue .footer a, +.theme-green .footer a, +.theme-purple .footer a { + color: rgba(255, 255, 255, 0.75); +} + +.theme-black .header a:focus, +.theme-black .header a:hover, +.theme-black .header a.active, +.theme-black .footer a:focus, +.theme-black .footer a:hover, +.theme-black .footer a.active, +.theme-blue .header a:focus, +.theme-blue .header a:hover, +.theme-blue .header a.active, +.theme-blue .footer a:focus, +.theme-blue .footer a:hover, +.theme-blue .footer a.active, +.theme-green .header a:focus, +.theme-green .header a:hover, +.theme-green .header a.active, +.theme-green .footer a:focus, +.theme-green .footer a:hover, +.theme-green .footer a.active, +.theme-purple .header a:focus, +.theme-purple .header a:hover, +.theme-purple .header a.active, +.theme-purple .footer a:focus, +.theme-purple .footer a:hover, +.theme-purple .footer a.active { + color: #fff; +} + +.theme-black .main-nav>li>a:focus, +.theme-black .main-nav>li>a:hover, +.theme-black .main-nav>li>a.active, +.theme-blue .main-nav>li>a:focus, +.theme-blue .main-nav>li>a:hover, +.theme-blue .main-nav>li>a.active, +.theme-green .main-nav>li>a:focus, +.theme-green .main-nav>li>a:hover, +.theme-green .main-nav>li>a.active, +.theme-purple .main-nav>li>a:focus, +.theme-purple .main-nav>li>a:hover, +.theme-purple .main-nav>li>a.active { + color: #fff; + background: rgba(0, 0, 0, 0.15); +} + +.theme-black .main-nav>.active a, +.theme-black .main-nav>.active a:focus, +.theme-black .main-nav>.active a:hover, +.theme-black .main-nav>.active a.active, +.theme-blue .main-nav>.active a, +.theme-blue .main-nav>.active a:focus, +.theme-blue .main-nav>.active a:hover, +.theme-blue .main-nav>.active a.active, +.theme-green .main-nav>.active a, +.theme-green .main-nav>.active a:focus, +.theme-green .main-nav>.active a:hover, +.theme-green .main-nav>.active a.active, +.theme-purple .main-nav>.active a, +.theme-purple .main-nav>.active a:focus, +.theme-purple .main-nav>.active a:hover, +.theme-purple .main-nav>.active a.active { + color: #fff; + background: rgba(0, 0, 0, 0.2); +} + +.theme-black .search .sidebar-form, +.theme-blue .search .sidebar-form, +.theme-green .search .sidebar-form, +.theme-purple .search .sidebar-form { + border: 0; + background: rgba(0, 0, 0, 0.2); +} + +.theme-black .search .sidebar-form input::-webkit-input-placeholder, +.theme-blue .search .sidebar-form input::-webkit-input-placeholder, +.theme-green .search .sidebar-form input::-webkit-input-placeholder, +.theme-purple .search .sidebar-form input::-webkit-input-placeholder { + color: rgba(255, 255, 255, 0.5); +} + +.theme-black .search .sidebar-form input:-moz-placeholder, +.theme-blue .search .sidebar-form input:-moz-placeholder, +.theme-green .search .sidebar-form input:-moz-placeholder, +.theme-purple .search .sidebar-form input:-moz-placeholder { + color: rgba(255, 255, 255, 0.5); +} + +.theme-black .search .sidebar-form input::-moz-placeholder, +.theme-blue .search .sidebar-form input::-moz-placeholder, +.theme-green .search .sidebar-form input::-moz-placeholder, +.theme-purple .search .sidebar-form input::-moz-placeholder { + color: rgba(255, 255, 255, 0.5); +} + +.theme-black .search .sidebar-form input:-ms-input-placeholder, +.theme-blue .search .sidebar-form input:-ms-input-placeholder, +.theme-green .search .sidebar-form input:-ms-input-placeholder, +.theme-purple .search .sidebar-form input:-ms-input-placeholder { + color: rgba(255, 255, 255, 0.5); +} + +.theme-black .search input[type="text"], +.theme-blue .search input[type="text"], +.theme-green .search input[type="text"], +.theme-purple .search input[type="text"] { + color: #666; +} + +.theme-black .search input[type="text"]+.input-group-btn .btn, +.theme-blue .search input[type="text"]+.input-group-btn .btn, +.theme-green .search input[type="text"]+.input-group-btn .btn, +.theme-purple .search input[type="text"]+.input-group-btn .btn { + color: rgba(255, 255, 255, 0.5); +} + +.theme-black .search input[type="text"]:focus, +.theme-black .search input[type="text"]:focus+.input-group-btn .btn, +.theme-blue .search input[type="text"]:focus, +.theme-blue .search input[type="text"]:focus+.input-group-btn .btn, +.theme-green .search input[type="text"]:focus, +.theme-green .search input[type="text"]:focus+.input-group-btn .btn, +.theme-purple .search input[type="text"]:focus, +.theme-purple .search input[type="text"]:focus+.input-group-btn .btn { + background-color: #fff; + color: #666; +} + +.theme-black .header { + background: #1a2433; + background: -webkit-linear-gradient(left, #1a2433 0%, #253449 80%, #253449 100%); + background: -webkit-gradient(linear, left top, right top, from(#1a2433), color-stop(80%, #253449), to(#253449)); + background: linear-gradient(to right, #1a2433 0%, #253449 80%, #253449 100%); +} + +.theme-blue .header { + background: #0062c5; + background: -webkit-linear-gradient(left, #0062c5 0%, #0073e6 80%, #0073e6 100%); + background: -webkit-gradient(linear, left top, right top, from(#0062c5), color-stop(80%, #0073e6), to(#0073e6)); + background: linear-gradient(to right, #0062c5 0%, #0073e6 80%, #0073e6 100%); +} + +.theme-green .header { + background: #08a283; + background: -webkit-linear-gradient(left, #08a283 0%, #0ac29d 80%, #0ac29d 100%); + background: -webkit-gradient(linear, left top, right top, from(#08a283), color-stop(80%, #0ac29d), to(#0ac29d)); + background: linear-gradient(to right, #08a283 0%, #0ac29d 80%, #0ac29d 100%); +} + +.theme-purple .header { + background: #494683; + background: -webkit-linear-gradient(left, #494683 0%, #555299 80%, #555299 100%); + background: -webkit-gradient(linear, left top, right top, from(#494683), color-stop(80%, #555299), to(#555299)); + background: linear-gradient(to right, #494683 0%, #555299 80%, #555299 100%); +} + +@media (min-width: 767px) { + .modal-center { + text-align: center; + padding: 0 !important; + } + + .modal-center:before { + content: ''; + display: inline-block; + height: 100%; + vertical-align: middle; + margin-right: -4px; + } + + .modal-center .modal-dialog { + display: inline-block; + text-align: left; + vertical-align: middle; + } + + .donate-box .donate-footer { + margin: 0 -15px -16px -15px; + } +} + +@media (min-width: 768px) { + .lead { + font-size: 21px; + } + + .dl-horizontal dt { + float: left; + width: 160px; + clear: left; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .dl-horizontal dd { + margin-left: 180px; + } + + .container { + width: 750px; + } + + .col-sm-1, + .col-sm-2, + .col-sm-3, + .col-sm-4, + .col-sm-5, + .col-sm-6, + .col-sm-7, + .col-sm-8, + .col-sm-9, + .col-sm-10, + .col-sm-11, + .col-sm-12 { + float: left; + } + + .col-sm-1 { + width: 8.33333%; + } + + .col-sm-2 { + width: 16.66667%; + } + + .col-sm-3 { + width: 25%; + } + + .col-sm-4 { + width: 33.33333%; + } + + .col-sm-5 { + width: 41.66667%; + } + + .col-sm-6 { + width: 50%; + } + + .col-sm-7 { + width: 58.33333%; + } + + .col-sm-8 { + width: 66.66667%; + } + + .col-sm-9 { + width: 75%; + } + + .col-sm-10 { + width: 83.33333%; + } + + .col-sm-11 { + width: 91.66667%; + } + + .col-sm-12 { + width: 100%; + } + + .col-sm-pull-0 { + right: auto; + } + + .col-sm-pull-1 { + right: 8.33333%; + } + + .col-sm-pull-2 { + right: 16.66667%; + } + + .col-sm-pull-3 { + right: 25%; + } + + .col-sm-pull-4 { + right: 33.33333%; + } + + .col-sm-pull-5 { + right: 41.66667%; + } + + .col-sm-pull-6 { + right: 50%; + } + + .col-sm-pull-7 { + right: 58.33333%; + } + + .col-sm-pull-8 { + right: 66.66667%; + } + + .col-sm-pull-9 { + right: 75%; + } + + .col-sm-pull-10 { + right: 83.33333%; + } + + .col-sm-pull-11 { + right: 91.66667%; + } + + .col-sm-pull-12 { + right: 100%; + } + + .col-sm-push-0 { + left: auto; + } + + .col-sm-push-1 { + left: 8.33333%; + } + + .col-sm-push-2 { + left: 16.66667%; + } + + .col-sm-push-3 { + left: 25%; + } + + .col-sm-push-4 { + left: 33.33333%; + } + + .col-sm-push-5 { + left: 41.66667%; + } + + .col-sm-push-6 { + left: 50%; + } + + .col-sm-push-7 { + left: 58.33333%; + } + + .col-sm-push-8 { + left: 66.66667%; + } + + .col-sm-push-9 { + left: 75%; + } + + .col-sm-push-10 { + left: 83.33333%; + } + + .col-sm-push-11 { + left: 91.66667%; + } + + .col-sm-push-12 { + left: 100%; + } + + .col-sm-offset-0 { + margin-left: 0%; + } + + .col-sm-offset-1 { + margin-left: 8.33333%; + } + + .col-sm-offset-2 { + margin-left: 16.66667%; + } + + .col-sm-offset-3 { + margin-left: 25%; + } + + .col-sm-offset-4 { + margin-left: 33.33333%; + } + + .col-sm-offset-5 { + margin-left: 41.66667%; + } + + .col-sm-offset-6 { + margin-left: 50%; + } + + .col-sm-offset-7 { + margin-left: 58.33333%; + } + + .col-sm-offset-8 { + margin-left: 66.66667%; + } + + .col-sm-offset-9 { + margin-left: 75%; + } + + .col-sm-offset-10 { + margin-left: 83.33333%; + } + + .col-sm-offset-11 { + margin-left: 91.66667%; + } + + .col-sm-offset-12 { + margin-left: 100%; + } + + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } + + .navbar-right .dropdown-menu-left { + left: 0; + right: auto; + } + + .nav-justified>li, + .nav-tabs.nav-justified>li { + display: table-cell; + width: 1%; + } + + .nav-justified>li>a, + .nav-tabs.nav-justified>li>a { + margin-bottom: 0; + } + + .nav-tabs-justified>li>a, + .nav-tabs.nav-justified>li>a { + border-bottom: 1px solid #f2f2f2; + border-radius: 4px 4px 0 0; + } + + .nav-tabs-justified>.active>a, + .nav-tabs.nav-justified>.active>a, + .nav-tabs-justified>.active>a:hover, + .nav-tabs.nav-justified>.active>a:hover, + .nav-tabs-justified>.active>a:focus, + .nav-tabs.nav-justified>.active>a:focus { + border-bottom-color: #fff; + } + + .navbar { + border-radius: 4px; + } + + .navbar-header { + float: left; + } + + .navbar-collapse { + width: auto; + border-top: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } + + .navbar-collapse.in { + overflow-y: visible; + } + + .navbar-fixed-top .navbar-collapse, + .navbar-static-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + padding-left: 0; + padding-right: 0; + } + + .container>.navbar-header, + .container>.navbar-collapse, + .container-fluid>.navbar-header, + .container-fluid>.navbar-collapse { + margin-right: 0; + margin-left: 0; + } + + .navbar-static-top { + border-radius: 0; + } + + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } + + .navbar>.container .navbar-brand, + .navbar>.container-fluid .navbar-brand { + margin-left: -15px; + } + + .navbar-toggle { + display: none; + } + + .navbar-nav { + float: left; + margin: 0; + } + + .navbar-nav>li { + float: left; + } + + .navbar-nav>li>a { + padding-top: 14.5px; + padding-bottom: 14.5px; + } + + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + + .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + + .navbar-form .form-control-static { + display: inline-block; + } + + .navbar-form .input-group { + display: inline-table; + vertical-align: middle; + } + + .navbar-form .input-group .input-group-addon, + .navbar-form .input-group .input-group-btn, + .navbar-form .input-group .form-control { + width: auto; + } + + .navbar-form .input-group>.form-control { + width: 100%; + } + + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } + + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + + .navbar-form .radio label, + .navbar-form .checkbox label { + padding-left: 0; + } + + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + + .navbar-form .has-feedback .form-control-feedback { + top: 0; + } + + .navbar-form { + width: auto; + border: 0; + margin-left: 0; + margin-right: 0; + padding-top: 0; + padding-bottom: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + + .navbar-text { + float: left; + margin-left: 15px; + margin-right: 15px; + } + + .navbar-left { + float: left !important; + } + + .navbar-right { + float: right !important; + margin-right: -15px; + } + + .navbar-right~.navbar-right { + margin-right: 0; + } + + .modal-dialog { + width: 600px; + margin: 30px auto; + } + + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); + } + + .modal-sm { + width: 300px; + } + + .header { + position: fixed; + top: 0; + bottom: 0; + left: 0; + z-index: 1000; + display: block; + padding: 0; + overflow-y: auto; + /* Scrollable contents if viewport is shorter than content. */ + border-right: 1px solid #f6f6f6; + width: 4.16667%; + } + + .sidebar { + position: fixed; + top: 0; + bottom: 0; + left: 0; + display: block; + padding: 0; + overflow-x: hidden; + overflow-y: auto; + /* Scrollable contents if viewport is shorter than content. */ + border-right: 1px solid #f6f6f6; + width: 33.33333%; + } + + .main { + width: 62.5%; + padding-right: 20px; + padding-left: 20px; + } + + .footer { + position: fixed; + left: 0; + bottom: 0; + background-color: transparent; + z-index: 1050; + width: 4.16667%; + } + + body.main-center .main { + margin-left: 4.16667%; + } + + body.main-left .sidebar { + margin-right: 4.16667%; + } + + body.main-right .sidebar { + margin-left: 4.16667%; + } + + body.main-right .main { + margin-left: 37.5%; + } + + body.no-sidebar.main-left .main { + width: 95.83333333%; + margin-right: 4.16667%; + } + + body.no-sidebar.main-right .main, + body.no-sidebar.main-center .main { + width: 95.83333333%; + margin-left: 4.16667%; + } + + .header .navbar-header { + float: none; + } + + #avatar img { + padding: 5px; + } + + .bar.bar-footer:before { + width: -webkit-calc(100% + 40px); + width: calc(100% + 40px); + left: -20px; + } + + .header .social-links { + display: none; + } +} + +@media (min-width: 992px) { + .container { + width: 970px; + } + + .col-md-1, + .col-md-2, + .col-md-3, + .col-md-4, + .col-md-5, + .col-md-6, + .col-md-7, + .col-md-8, + .col-md-9, + .col-md-10, + .col-md-11, + .col-md-12 { + float: left; + } + + .col-md-1 { + width: 8.33333%; + } + + .col-md-2 { + width: 16.66667%; + } + + .col-md-3 { + width: 25%; + } + + .col-md-4 { + width: 33.33333%; + } + + .col-md-5 { + width: 41.66667%; + } + + .col-md-6 { + width: 50%; + } + + .col-md-7 { + width: 58.33333%; + } + + .col-md-8 { + width: 66.66667%; + } + + .col-md-9 { + width: 75%; + } + + .col-md-10 { + width: 83.33333%; + } + + .col-md-11 { + width: 91.66667%; + } + + .col-md-12 { + width: 100%; + } + + .col-md-pull-0 { + right: auto; + } + + .col-md-pull-1 { + right: 8.33333%; + } + + .col-md-pull-2 { + right: 16.66667%; + } + + .col-md-pull-3 { + right: 25%; + } + + .col-md-pull-4 { + right: 33.33333%; + } + + .col-md-pull-5 { + right: 41.66667%; + } + + .col-md-pull-6 { + right: 50%; + } + + .col-md-pull-7 { + right: 58.33333%; + } + + .col-md-pull-8 { + right: 66.66667%; + } + + .col-md-pull-9 { + right: 75%; + } + + .col-md-pull-10 { + right: 83.33333%; + } + + .col-md-pull-11 { + right: 91.66667%; + } + + .col-md-pull-12 { + right: 100%; + } + + .col-md-push-0 { + left: auto; + } + + .col-md-push-1 { + left: 8.33333%; + } + + .col-md-push-2 { + left: 16.66667%; + } + + .col-md-push-3 { + left: 25%; + } + + .col-md-push-4 { + left: 33.33333%; + } + + .col-md-push-5 { + left: 41.66667%; + } + + .col-md-push-6 { + left: 50%; + } + + .col-md-push-7 { + left: 58.33333%; + } + + .col-md-push-8 { + left: 66.66667%; + } + + .col-md-push-9 { + left: 75%; + } + + .col-md-push-10 { + left: 83.33333%; + } + + .col-md-push-11 { + left: 91.66667%; + } + + .col-md-push-12 { + left: 100%; + } + + .col-md-offset-0 { + margin-left: 0%; + } + + .col-md-offset-1 { + margin-left: 8.33333%; + } + + .col-md-offset-2 { + margin-left: 16.66667%; + } + + .col-md-offset-3 { + margin-left: 25%; + } + + .col-md-offset-4 { + margin-left: 33.33333%; + } + + .col-md-offset-5 { + margin-left: 41.66667%; + } + + .col-md-offset-6 { + margin-left: 50%; + } + + .col-md-offset-7 { + margin-left: 58.33333%; + } + + .col-md-offset-8 { + margin-left: 66.66667%; + } + + .col-md-offset-9 { + margin-left: 75%; + } + + .col-md-offset-10 { + margin-left: 83.33333%; + } + + .col-md-offset-11 { + margin-left: 91.66667%; + } + + .col-md-offset-12 { + margin-left: 100%; + } + + .modal-lg { + width: 900px; + } + + .header { + width: 16.66667%; + } + + .sidebar { + width: 25%; + } + + .sidebar .slimContent { + padding-right: 25px; + padding-left: 25px; + } + + .main { + width: 58.33333%; + padding-right: 25px; + padding-left: 25px; + } + + .footer { + width: 16.66667%; + } + + body.main-center .main { + margin-left: 16.66667%; + } + + body.main-left .sidebar { + margin-right: 16.66667%; + } + + body.main-right .sidebar { + margin-left: 16.66667%; + } + + body.main-right .main { + margin-left: 41.66667%; + } + + body.no-sidebar.main-left .main { + width: 83.33333333%; + margin-right: 16.66667%; + } + + body.no-sidebar.main-right .main, + body.no-sidebar.main-center .main { + width: 83.33333333%; + margin-left: 16.66667%; + } + + .bar.bar-footer:before { + width: -webkit-calc(100% + 50px); + width: calc(100% + 50px); + left: -25px; + } + + .marked-body .headerlink:before, + .marked-body .markdownIt-Anchor:before { + display: inline-block; + width: 18px; + content: "#"; + color: #0a6ebd; + text-align: right; + float: left; + visibility: hidden; + } + + .marked-body .headerlink:before { + margin-left: -15px; + padding-right: 2px; + } + + .marked-body .markdownIt-Anchor:before { + margin-left: -20px; + } + + .marked-body h1:hover .headerlink:before, + .marked-body h1:hover .markdownIt-Anchor:before, + .marked-body h2:hover .headerlink:before, + .marked-body h2:hover .markdownIt-Anchor:before, + .marked-body h3:hover .headerlink:before, + .marked-body h3:hover .markdownIt-Anchor:before, + .marked-body h4:hover .headerlink:before, + .marked-body h4:hover .markdownIt-Anchor:before, + .marked-body h5:hover .headerlink:before, + .marked-body h5:hover .markdownIt-Anchor:before, + .marked-body h6:hover .headerlink:before, + .marked-body h6:hover .markdownIt-Anchor:before { + visibility: visible; + } +} + +@media (min-width: 1200px) { + .container { + width: 1170px; + } + + .col-lg-1, + .col-lg-2, + .col-lg-3, + .col-lg-4, + .col-lg-5, + .col-lg-6, + .col-lg-7, + .col-lg-8, + .col-lg-9, + .col-lg-10, + .col-lg-11, + .col-lg-12 { + float: left; + } + + .col-lg-1 { + width: 8.33333%; + } + + .col-lg-2 { + width: 16.66667%; + } + + .col-lg-3 { + width: 25%; + } + + .col-lg-4 { + width: 33.33333%; + } + + .col-lg-5 { + width: 41.66667%; + } + + .col-lg-6 { + width: 50%; + } + + .col-lg-7 { + width: 58.33333%; + } + + .col-lg-8 { + width: 66.66667%; + } + + .col-lg-9 { + width: 75%; + } + + .col-lg-10 { + width: 83.33333%; + } + + .col-lg-11 { + width: 91.66667%; + } + + .col-lg-12 { + width: 100%; + } + + .col-lg-pull-0 { + right: auto; + } + + .col-lg-pull-1 { + right: 8.33333%; + } + + .col-lg-pull-2 { + right: 16.66667%; + } + + .col-lg-pull-3 { + right: 25%; + } + + .col-lg-pull-4 { + right: 33.33333%; + } + + .col-lg-pull-5 { + right: 41.66667%; + } + + .col-lg-pull-6 { + right: 50%; + } + + .col-lg-pull-7 { + right: 58.33333%; + } + + .col-lg-pull-8 { + right: 66.66667%; + } + + .col-lg-pull-9 { + right: 75%; + } + + .col-lg-pull-10 { + right: 83.33333%; + } + + .col-lg-pull-11 { + right: 91.66667%; + } + + .col-lg-pull-12 { + right: 100%; + } + + .col-lg-push-0 { + left: auto; + } + + .col-lg-push-1 { + left: 8.33333%; + } + + .col-lg-push-2 { + left: 16.66667%; + } + + .col-lg-push-3 { + left: 25%; + } + + .col-lg-push-4 { + left: 33.33333%; + } + + .col-lg-push-5 { + left: 41.66667%; + } + + .col-lg-push-6 { + left: 50%; + } + + .col-lg-push-7 { + left: 58.33333%; + } + + .col-lg-push-8 { + left: 66.66667%; + } + + .col-lg-push-9 { + left: 75%; + } + + .col-lg-push-10 { + left: 83.33333%; + } + + .col-lg-push-11 { + left: 91.66667%; + } + + .col-lg-push-12 { + left: 100%; + } + + .col-lg-offset-0 { + margin-left: 0%; + } + + .col-lg-offset-1 { + margin-left: 8.33333%; + } + + .col-lg-offset-2 { + margin-left: 16.66667%; + } + + .col-lg-offset-3 { + margin-left: 25%; + } + + .col-lg-offset-4 { + margin-left: 33.33333%; + } + + .col-lg-offset-5 { + margin-left: 41.66667%; + } + + .col-lg-offset-6 { + margin-left: 50%; + } + + .col-lg-offset-7 { + margin-left: 58.33333%; + } + + .col-lg-offset-8 { + margin-left: 66.66667%; + } + + .col-lg-offset-9 { + margin-left: 75%; + } + + .col-lg-offset-10 { + margin-left: 83.33333%; + } + + .col-lg-offset-11 { + margin-left: 91.66667%; + } + + .col-lg-offset-12 { + margin-left: 100%; + } + + .visible-lg { + display: block !important; + } + + table.visible-lg { + display: table !important; + } + + tr.visible-lg { + display: table-row !important; + } + + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } + + .visible-lg-block { + display: block !important; + } + + .visible-lg-inline { + display: inline !important; + } + + .visible-lg-inline-block { + display: inline-block !important; + } + + .hidden-lg { + display: none !important; + } + + .header { + width: 16.66667%; + } + + .sidebar { + width: 23%; + } + + .sidebar .slimContent { + padding-right: 30px; + padding-left: 30px; + } + + .main { + width: 60.33333%; + padding-right: 30px; + padding-left: 30px; + } + + .footer { + width: 16.66667%; + } + + body.main-center .main { + margin-left: 16.66667%; + } + + body.main-left .sidebar { + margin-right: 16.66667%; + } + + body.main-right .sidebar { + margin-left: 16.66667%; + } + + body.main-right .main { + margin-left: 39.66667%; + } + + body.no-sidebar.main-left .main { + width: 83.33333333%; + margin-right: 16.66667%; + } + + body.no-sidebar.main-right .main, + body.no-sidebar.main-center .main { + width: 83.33333333%; + margin-left: 16.66667%; + } + + .main-nav>li>a { + padding: 10px 20px; + } + + .bar.bar-footer:before { + width: -webkit-calc(100% + 60px); + width: calc(100% + 60px); + left: -30px; + } +} + +@media (min-width: 1440px) { + .header { + width: 16.66667%; + } + + .sidebar { + width: 21%; + } + + .main { + width: 62.33333%; + } + + .footer { + width: 16.66667%; + } + + body.main-center .main { + margin-left: 16.66667%; + } + + body.main-left .sidebar { + margin-right: 16.66667%; + } + + body.main-right .sidebar { + margin-left: 16.66667%; + } + + body.main-right .main { + margin-left: 37.66667%; + } + + body.no-sidebar.main-left .main { + width: 83.33333333%; + margin-right: 16.66667%; + } + + body.no-sidebar.main-right .main, + body.no-sidebar.main-center .main { + width: 83.33333333%; + margin-left: 16.66667%; + } + + .header #title { + font-size: 15px; + } +} + +@media (max-width: 1199px) { + .main-nav>li>a { + padding: 6px 20px; + } + + .bar .pager li a span { + display: none; + } + + .footer .copyright { + display: none; + } +} + +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + + .navbar-nav .open .dropdown-menu>li>a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + + .navbar-nav .open .dropdown-menu>li>a { + line-height: 21px; + } + + .navbar-nav .open .dropdown-menu>li>a:hover, + .navbar-nav .open .dropdown-menu>li>a:focus { + background-image: none; + } + + .navbar-form .form-group { + margin-bottom: 5px; + } + + .navbar-form .form-group:last-child { + margin-bottom: 0; + } + + .navbar-default .navbar-nav .open .dropdown-menu>li>a { + color: #777; + } + + .navbar-default .navbar-nav .open .dropdown-menu>li>a:hover, + .navbar-default .navbar-nav .open .dropdown-menu>li>a:focus { + color: #333; + background-color: transparent; + } + + .navbar-default .navbar-nav .open .dropdown-menu>.active>a, + .navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover, + .navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus { + color: #555; + background-color: #e7e7e7; + } + + .navbar-default .navbar-nav .open .dropdown-menu>.disabled>a, + .navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover, + .navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus { + color: #ccc; + background-color: transparent; + } + + .navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header { + border-color: #090909; + } + + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: #090909; + } + + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a { + color: #9d9d9d; + } + + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus { + color: #fff; + background-color: transparent; + } + + .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a, + .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus { + color: #fff; + background-color: #090909; + } + + .navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a, + .navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus { + color: #444; + background-color: transparent; + } + + .visible-xs { + display: block !important; + } + + table.visible-xs { + display: table !important; + } + + tr.visible-xs { + display: table-row !important; + } + + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } + + .visible-xs-block { + display: block !important; + } + + .visible-xs-inline { + display: inline !important; + } + + .visible-xs-inline-block { + display: inline-block !important; + } + + .hidden-xs { + display: none !important; + } + + body { + padding-top: 53px; + } + + .list-circle-num, + .list-square-num { + margin: 0 0 40px; + } + + .modal-xs-full .modal-dialog, + .modal-xs-full .modal-content { + height: 100%; + width: 100%; + margin: 0; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + + .modal-xs-full .modal-dialog .donate-box, + .modal-xs-full .modal-content .donate-box { + padding-top: 50px; + } + + .highlight table:before { + display: block; + content: ' '; + height: 38px; + position: absolute; + top: 0; + left: 0; + right: 0; + margin-left: -10px; + margin-right: -10px; + margin-top: -38px; + /*background-color: #f6f6f6;*/ + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } + + .header { + position: fixed; + left: 0; + right: 0; + top: 0; + width: 100%; + z-index: 1050; + } + + .header:before, + .header:after { + content: " "; + display: table; + } + + .header:after { + clear: both; + } + + .sidebar { + display: none; + } + + .main { + min-height: auto; + } + + .main-nav>li>a { + padding: 10px 20px; + } + + .sidebar-form { + border: 0; + margin: 9px 45px 9px 0; + } + + .sidebar-form .input-group-btn .btn { + color: #2196f3; + } + + .sidebar-form input[type="text"]:focus { + background-color: transparent; + } + + .sidebar-form input[type="text"]:focus+.input-group-btn .btn { + color: #2196f3; + background-color: transparent; + } + + .header .navbar-toggle { + position: absolute; + top: 0; + right: 0; + margin-right: 5px; + } + + .header .navbar-toggle .icon-bar { + width: 18px; + } + + .header .search { + width: 90%; + float: right; + } + + .header .profile-block { + padding: 0; + margin: 10px 0; + width: 8.33333%; + float: left; + } + + .header #avatar { + width: 32px; + height: 32px; + } + + .header #name, + .header #title, + .header #location { + display: none; + } + + #main-navbar { + position: absolute; + width: 100%; + background-color: #fbfbfb; + border-bottom: 1px solid #f6f6f6; + z-index: 99; + } + + .main .content { + min-height: auto; + } + + .sidebar-toc.in { + position: fixed; + top: 50px; + bottom: 50px; + z-index: 9; + } + + .bar.bar-footer { + top: auto !important; + bottom: 0 !important; + } + + .footer { + display: none; + position: static; + } + + .footer .copyright { + display: block; + } + + .social-links .tooltip { + display: none !important; + visibility: hidden; + } + + .theme-black .search input[type="text"]:focus, + .theme-blue .search input[type="text"]:focus, + .theme-green .search input[type="text"]:focus, + .theme-purple .search input[type="text"]:focus { + background-color: transparent; + } + + .theme-black .search input[type="text"]:focus+.input-group-btn .btn, + .theme-blue .search input[type="text"]:focus+.input-group-btn .btn, + .theme-green .search input[type="text"]:focus+.input-group-btn .btn, + .theme-purple .search input[type="text"]:focus+.input-group-btn .btn { + color: rgba(255, 255, 255, 0.5); + background-color: transparent; + } + + .theme-black #main-navbar { + background: #1a2433; + background: -webkit-linear-gradient(left, #1a2433 0%, #253449 80%, #253449 100%); + background: -webkit-gradient(linear, left top, right top, from(#1a2433), color-stop(80%, #253449), to(#253449)); + background: linear-gradient(to right, #1a2433 0%, #253449 80%, #253449 100%); + } + + .theme-blue #main-navbar { + background: #0062c5; + background: -webkit-linear-gradient(left, #0062c5 0%, #0073e6 80%, #0073e6 100%); + background: -webkit-gradient(linear, left top, right top, from(#0062c5), color-stop(80%, #0073e6), to(#0073e6)); + background: linear-gradient(to right, #0062c5 0%, #0073e6 80%, #0073e6 100%); + } + + .theme-green #main-navbar { + background: #08a283; + background: -webkit-linear-gradient(left, #08a283 0%, #0ac29d 80%, #0ac29d 100%); + background: -webkit-gradient(linear, left top, right top, from(#08a283), color-stop(80%, #0ac29d), to(#0ac29d)); + background: linear-gradient(to right, #08a283 0%, #0ac29d 80%, #0ac29d 100%); + } + + .theme-purple #main-navbar { + background: #494683; + background: -webkit-linear-gradient(left, #494683 0%, #555299 80%, #555299 100%); + background: -webkit-gradient(linear, left top, right top, from(#494683), color-stop(80%, #555299), to(#555299)); + background: linear-gradient(to right, #494683 0%, #555299 80%, #555299 100%); + } +} + +@media screen and (max-width: 559px), +screen and (max-height: 479px) { + .ins-search-container { + top: 0; + left: 0; + margin: 0; + width: 100%; + height: 100%; + background: #f7f7f7; + } +} + +@media (max-width: 480px) { + .header #avatar { + width: 24px; + height: 24px; + margin-top: 3px; + margin-left: 15px; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + + table.visible-sm { + display: table !important; + } + + tr.visible-sm { + display: table-row !important; + } + + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } + + .visible-sm-block { + display: block !important; + } + + .visible-sm-inline { + display: inline !important; + } + + .visible-sm-inline-block { + display: inline-block !important; + } + + .hidden-sm { + display: none !important; + } + + .sidebar-form { + display: none; + } + + .header .main-nav>li>a { + text-align: center; + padding-left: 0; + padding-right: 0; + } + + .header .main-nav>li>a span { + display: none; + } + + .header .profile-block { + padding-top: 10px; + padding-left: 0; + padding-right: 0; + } + + .header #avatar { + width: 32px; + height: 32px; + } + + .footer { + padding-left: 0; + padding-right: 0; + } + + .social-links { + display: block; + width: 100%; + text-align: center; + margin-bottom: 0; + } + + .social-links:before, + .social-links:after { + content: " "; + display: table; + } + + .social-links:after { + clear: both; + } + + .social-links li { + display: block; + margin-left: 0; + margin-top: 10px; + } + + .social-links li:before, + .social-links li:after { + content: " "; + display: table; + } + + .social-links li:after { + clear: both; + } + + .social-links .tooltip { + display: none !important; + visibility: hidden; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + + table.visible-md { + display: table !important; + } + + tr.visible-md { + display: table-row !important; + } + + th.visible-md, + td.visible-md { + display: table-cell !important; + } + + .visible-md-block { + display: block !important; + } + + .visible-md-inline { + display: inline !important; + } + + .visible-md-inline-block { + display: inline-block !important; + } + + .hidden-md { + display: none !important; + } +} + +@media print { + + *, + *:before, + *:after { + background: transparent !important; + color: #000 !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + text-shadow: none !important; + } + + a, + a:visited { + text-decoration: underline; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h2, + h3 { + page-break-after: avoid; + } + + .navbar { + display: none; + } + + .btn>.caret, + .dropup>.btn>.caret { + border-top-color: #000 !important; + } + + .label { + border: 1px solid #000; + } + + .table { + border-collapse: collapse !important; + } + + .table td, + .table th { + background-color: #fff !important; + } + + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } + + .visible-print { + display: block !important; + } + + table.visible-print { + display: table !important; + } + + tr.visible-print { + display: table-row !important; + } + + th.visible-print, + td.visible-print { + display: table-cell !important; + } + + .visible-print-block { + display: block !important; + } + + .visible-print-inline { + display: inline !important; + } + + .visible-print-inline-block { + display: inline-block !important; + } + + .hidden-print { + display: none !important; + } +} + +@media (max-device-width: 480px) and (orientation: landscape) { + + .navbar-fixed-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + max-height: 200px; + } +} \ No newline at end of file diff --git a/css/style.min.css b/css/style.min.css new file mode 100644 index 0000000..3f2a619 --- /dev/null +++ b/css/style.min.css @@ -0,0 +1,9 @@ +@charset "UTF-8";/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0;vertical-align:middle}svg:not(:root){overflow:hidden}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}textarea{overflow:auto}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */*,:after,:before{-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}figure{margin:0}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.57143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:21px;margin-bottom:21px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:21px;margin-bottom:10.5px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10.5px;margin-bottom:10.5px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:30px}.h2,h2{font-size:26px}.h3,h3{font-size:22px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10.5px}.lead{margin-bottom:21px;font-size:16px;font-weight:300;line-height:1.4}.small,small{font-size:85%}.mark,mark{background-color:#fcf8e3;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-lowercase{text-transform:lowercase}.initialism,.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777!important}.text-primary{color:#2196f3}a.text-primary:focus,a.text-primary:hover{color:#0c7cd5}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#2196f3}a.bg-primary:focus,a.bg-primary:hover{background-color:#0c7cd5}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9.5px;margin:42px 0 21px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10.5px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:21px}dd,dt{line-height:1.57143}dt{font-weight:700}dd{margin-left:0}.dl-horizontal dd:after,.dl-horizontal dd:before{content:" ";display:table}.dl-horizontal dd:after{clear:both}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%}blockquote{padding:10.5px 16px;margin:0 0 21px;border:1px dotted #eee;border-left:3px solid #eee;background-color:#fbfbfb}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.57143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:21px;font-style:normal;line-height:1.57143}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.container:after,.container:before{content:" ";display:table}.container:after{clear:both}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.container-fluid:after,.container-fluid:before{content:" ";display:table}.container-fluid:after{clear:both}.row{margin-left:-15px;margin-right:-15px}.row:after,.row:before{content:" ";display:table}.row:after{clear:both}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-1{width:8.33333%}.col-xs-2{width:16.66667%}.col-xs-3{width:25%}.col-xs-4{width:33.33333%}.col-xs-5{width:41.66667%}.col-xs-6{width:50%}.col-xs-7{width:58.33333%}.col-xs-8{width:66.66667%}.col-xs-9{width:75%}.col-xs-10{width:83.33333%}.col-xs-11{width:91.66667%}.col-xs-12{width:100%}.col-xs-pull-0{right:auto}.col-xs-pull-1{right:8.33333%}.col-xs-pull-2{right:16.66667%}.col-xs-pull-3{right:25%}.col-xs-pull-4{right:33.33333%}.col-xs-pull-5{right:41.66667%}.col-xs-pull-6{right:50%}.col-xs-pull-7{right:58.33333%}.col-xs-pull-8{right:66.66667%}.col-xs-pull-9{right:75%}.col-xs-pull-10{right:83.33333%}.col-xs-pull-11{right:91.66667%}.col-xs-pull-12{right:100%}.col-xs-push-0{left:auto}.col-xs-push-1{left:8.33333%}.col-xs-push-2{left:16.66667%}.col-xs-push-3{left:25%}.col-xs-push-4{left:33.33333%}.col-xs-push-5{left:41.66667%}.col-xs-push-6{left:50%}.col-xs-push-7{left:58.33333%}.col-xs-push-8{left:66.66667%}.col-xs-push-9{left:75%}.col-xs-push-10{left:83.33333%}.col-xs-push-11{left:91.66667%}.col-xs-push-12{left:100%}.col-xs-offset-0{margin-left:0}.col-xs-offset-1{margin-left:8.33333%}.col-xs-offset-2{margin-left:16.66667%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-4{margin-left:33.33333%}.col-xs-offset-5{margin-left:41.66667%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-7{margin-left:58.33333%}.col-xs-offset-8{margin-left:66.66667%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-10{margin-left:83.33333%}.col-xs-offset-11{margin-left:91.66667%}.col-xs-offset-12{margin-left:100%}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.57143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.btn-default.dropdown-toggle{color:#333;background-color:#e6e6e6;border-color:#adadad;background-image:none}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.btn-default.dropdown-toggle.focus,.open>.btn-default.dropdown-toggle:focus,.open>.btn-default.dropdown-toggle:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#2196f3;border-color:#0d8aee}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0c7cd5;border-color:#064475}.btn-primary:hover{color:#fff;background-color:#0c7cd5;border-color:#0a68b4}.btn-primary.active,.btn-primary:active,.open>.btn-primary.dropdown-toggle{color:#fff;background-color:#0c7cd5;border-color:#0a68b4;background-image:none}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.btn-primary.dropdown-toggle.focus,.open>.btn-primary.dropdown-toggle:focus,.open>.btn-primary.dropdown-toggle:hover{color:#fff;background-color:#0a68b4;border-color:#064475}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#2196f3;border-color:#0d8aee}.btn-primary .badge{color:#2196f3;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;border-color:#398439;background-image:none}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.btn-success.dropdown-toggle.focus,.open>.btn-success.dropdown-toggle:focus,.open>.btn-success.dropdown-toggle:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#56ccf2;border-color:#3ec5f0}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#27beee;border-color:#0d7ea3}.btn-info:hover{color:#fff;background-color:#27beee;border-color:#11aee0}.btn-info.active,.btn-info:active,.open>.btn-info.dropdown-toggle{color:#fff;background-color:#27beee;border-color:#11aee0;background-image:none}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.btn-info.dropdown-toggle.focus,.open>.btn-info.dropdown-toggle:focus,.open>.btn-info.dropdown-toggle:hover{color:#fff;background-color:#11aee0;border-color:#0d7ea3}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#56ccf2;border-color:#3ec5f0}.btn-info .badge{color:#56ccf2;background-color:#fff}.btn-warning{color:#fff;background-color:#f09819;border-color:#e18b0f}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#c97c0d;border-color:#694107}.btn-warning:hover{color:#fff;background-color:#c97c0d;border-color:#a7670b}.btn-warning.active,.btn-warning:active,.open>.btn-warning.dropdown-toggle{color:#fff;background-color:#c97c0d;border-color:#a7670b;background-image:none}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.btn-warning.dropdown-toggle.focus,.open>.btn-warning.dropdown-toggle:focus,.open>.btn-warning.dropdown-toggle:hover{color:#fff;background-color:#a7670b;border-color:#694107}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f09819;border-color:#e18b0f}.btn-warning .badge{color:#f09819;background-color:#fff}.btn-danger{color:#fff;background-color:#ff512f;border-color:#ff3c16}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#fb2900;border-color:#951800}.btn-danger:hover{color:#fff;background-color:#fb2900;border-color:#d72300}.btn-danger.active,.btn-danger:active,.open>.btn-danger.dropdown-toggle{color:#fff;background-color:#fb2900;border-color:#d72300;background-image:none}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.btn-danger.dropdown-toggle.focus,.open>.btn-danger.dropdown-toggle:focus,.open>.btn-danger.dropdown-toggle:hover{color:#fff;background-color:#d72300;border-color:#951800}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#ff512f;border-color:#ff3c16}.btn-danger .badge{color:#ff512f;background-color:#fff}.btn-link{color:#2196f3;font-weight:400;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#0a6ebd;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33333;border-radius:6px}.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.57143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;outline:0;background-color:#2196f3}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.57143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav:after,.nav:before{content:" ";display:table}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#2196f3}.nav .nav-divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #f2f2f2}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.57143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #f2f2f2}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;background-color:#fff;border:1px solid #f2f2f2;border-bottom-color:transparent;cursor:default}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#2196f3}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified,.nav-tabs.nav-justified{width:100%}.nav-justified>li,.nav-tabs.nav-justified>li{float:none}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}.nav-tabs-justified,.nav-tabs.nav-justified{border-bottom:0}.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #f2f2f2}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:21px;border:1px solid transparent}.navbar:after,.navbar:before{content:" ";display:table}.navbar:after{clear:both}.navbar-header:after,.navbar-header:before{content:" ";display:table}.navbar-header:after{clear:both}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse:after,.navbar-collapse:before{content:" ";display:table}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}.navbar-static-top{z-index:1000;border-width:0 0 1px}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:14.5px 15px;font-size:18px;line-height:21px;height:50px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:3px}.navbar-nav{margin:7.25px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:21px}.navbar-form{padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin:7.5px -15px}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-radius:4px 4px 0 0}.navbar-btn{margin-top:7.5px;margin-bottom:7.5px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:14.5px;margin-bottom:14.5px}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-nav>li>a,.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{background-color:#e7e7e7;color:#555}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#090909}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>li>a,.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#090909}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{background-color:#090909;color:#fff}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.pager{padding-left:0;margin:21px 0;list-style:none;text-align:center}.pager:after,.pager:before{content:" ";display:table}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;color:#fff;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#2196f3;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.thumbnail{display:block;padding:4px;margin-bottom:21px;line-height:1.57143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto;margin-left:auto;margin-right:auto}.thumbnail .caption{padding:9px;color:#333}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#2196f3}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle,.media-middle .media-body,.media-middle .media-left,.media-middle .media-right{vertical-align:middle}.media-bottom,.media-bottom .media-body,.media-bottom .media-left,.media-bottom .media-right{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:24px;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:hidden;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.05);box-shadow:0 3px 9px rgba(0,0,0,.05);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header:after,.modal-header:before{content:" ";display:table}.modal-header:after{clear:both}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.57143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:after,.modal-footer:before{content:" ";display:table}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}.tooltip{position:absolute;z-index:1070;display:block;font-family:PingHei,"PingFang SC",Helvetica Neue,"Work Sans","Hiragino Sans GB","Microsoft YaHei",SimSun,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.57143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:12px;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;right:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-print,.visible-print-block,.visible-print-inline,.visible-print-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}/*! + * IE10 viewport hack for Surface/desktop Windows 8 bug + * Copyright 2014-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */@-o-viewport{width:device-width}@viewport{width:device-width}::-webkit-scrollbar{width:6px;height:4px;background:0 0}::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15)}::-webkit-scrollbar-thumb:window-inactive{background:rgba(0,0,0,.1)}::-webkit-scrollbar-thumb:vertical{height:4px;background:rgba(0,0,0,.15)}::-webkit-scrollbar-thumb:horizontal{width:4px;background:rgba(0,0,0,.15)}::-webkit-scrollbar-thumb:vertical:hover{background-color:rgba(0,0,0,.3)}::-webkit-scrollbar-thumb:vertical:active{background-color:rgba(0,0,0,.5)}::-webkit-scrollbar-track{background:rgba(0,0,0,.1);-webkit-box-shadow:inset 0 0 3px rgba(0,0,0,.1);box-shadow:inset 0 0 3px rgba(0,0,0,.1)}::-webkit-scrollbar-track-piece{background:rgba(0,0,0,.15)}body,html{position:relative;overflow-x:hidden}body{padding-right:0!important;font-family:PingHei,"PingFang SC",Helvetica Neue,"Work Sans","Hiragino Sans GB","Microsoft YaHei",SimSun,sans-serif;font-size:14px;line-height:1.57143;color:#333;background-color:#fff}a{color:#333;text-decoration:none}a:focus,a:hover{color:#0a6ebd;text-decoration:none}.active>a,a.active{color:#0a6ebd}.btn,button,input,select,textarea{outline:0!important}.btn:active,.btn:focus,.btn:hover,button:active,button:focus,button:hover,input:active,input:focus,input:hover,select:active,select:focus,select:hover,textarea:active,textarea:focus,textarea:hover{outline:0!important}canvas,iframe,img,svg,video{max-width:100%;height:auto}.clear{height:0;font-size:0;line-height:0;overflow:hidden;clear:both}.clearfix:after,.clearfix:before{content:" ";display:table;line-height:0;content:""}.clearfix:after{clear:both}.clearfix{*zoom:1}.clickable{cursor:pointer}.scrollable{overflow-x:hidden;overflow-y:auto}.transform-no{-ms-transform:none!important;-webkit-transform:none!important;transform:none!important}.text-dark{color:#333!important}.text-grey{color:#999!important}.text-white{color:#fff!important}.text-white a:hover,.text-white a:hover i,.text-white:hover{color:rgba(255,255,255,.8)}.text-white a.list-group-item.active i{color:#fff}.text-white .accordion-list p{font-size:12px;height:84px;line-height:21px;color:rgba(255,255,255,.8)}.bg-no{background:0 0!important}.bg-alpha{background-color:transparent!important}.bg-inverse,.bg-inverse a{color:#fff}.btn.bg-inverse.focus,.btn.bg-inverse:focus,.btn.bg-inverse:hover{color:rgba(255,255,255,.8)}.bg{background-color:#f6f6f6!important}.z-no{z-index:inherit}.z1{z-index:1}.z2{z-index:2}.z3{z-index:3}.z4{z-index:4}.zmin{z-index:-1}.zmax{z-index:999}.m{margin:5px!important}.m-no{margin:0!important}.m-0x{margin:10px!important}.m-1x{margin:15px!important}.m-2x{margin:20px!important}.m-3x{margin:30px!important}.m-4x{margin:60px!important}.m-5x{margin:100px!important}.mv{margin-top:5px!important;margin-bottom:5px!important}.mv-no{margin-top:0!important;margin-bottom:0!important}.mv-0x{margin-top:10px!important;margin-bottom:10px!important}.mv-1x{margin-top:15px!important;margin-bottom:15px!important}.mv-2x{margin-top:20px!important;margin-bottom:20px!important}.mv-3x{margin-top:30px!important;margin-bottom:30px!important}.mv-4x{margin-top:60px!important;margin-bottom:60px!important}.mv-5x{margin-top:100px!important;margin-bottom:100px!important}.mh{margin-left:5px!important;margin-right:5px!important}.mh-no{margin-left:0!important;margin-right:0!important}.mh-0x{margin-left:10px!important;margin-right:10px!important}.mh-1x{margin-left:15px!important;margin-right:15px!important}.mh-2x{margin-left:20px!important;margin-right:20px!important}.mh-3x{margin-left:30px!important;margin-right:30px!important}.mh-4x{margin-left:60px!important;margin-right:60px!important}.mh-5x{margin-left:100px!important;margin-right:100px!important}.mt{margin-top:5px!important}.mt-no{margin-top:0!important}.mt-0x{margin-top:10px!important}.mt-1x{margin-top:15px!important}.mt-2x{margin-top:20px!important}.mt-3x{margin-top:30px!important}.mt-4x{margin-top:60px!important}.mt-5x{margin-top:100px!important}.mb{margin-bottom:5px!important}.mb-no{margin-bottom:0!important}.mb-0x{margin-bottom:10px!important}.mb-1x{margin-bottom:15px!important}.mb-2x{margin-bottom:20px!important}.mb-3x{margin-bottom:30px!important}.mb-4x{margin-bottom:60px!important}.mb-5x{margin-bottom:100px!important}.ml{margin-left:5px!important}.ml-no{margin-left:0!important}.ml-0x{margin-left:10px!important}.ml-1x{margin-left:15px!important}.ml-2x{margin-left:20px!important}.ml-3x{margin-left:30px!important}.ml-4x{margin-left:60px!important}.ml-5x{margin-left:100px!important}.mr{margin-right:5px!important}.mr-no{margin-right:0!important}.mr-0x{margin-right:10px!important}.mr-1x{margin-right:15px!important}.mr-2x{margin-right:20px!important}.mr-3x{margin-right:30px!important}.mr-4x{margin-right:60px!important}.mr-5x{margin-right:100px!important}.p{padding:5px!important}.p-no{padding:0!important}.p-0x{padding:10px!important}.p-1x{padding:15px!important}.p-2x{padding:20px!important}.p-3x{padding:30px!important}.p-4x{padding:60px!important}.p-5x{padding:100px!important}.pv{padding-top:5px!important;padding-bottom:5px!important}.pv-no{padding-top:0!important;padding-bottom:0!important}.pv-0x{padding-top:10px!important;padding-bottom:10px!important}.pv-1x{padding-top:15px!important;padding-bottom:15px!important}.pv-2x{padding-top:20px!important;padding-bottom:20px!important}.pv-3x{padding-top:30px!important;padding-bottom:30px!important}.pv-4x{padding-top:60px!important;padding-bottom:60px!important}.pv-5x{padding-top:100px!important;padding-bottom:100px!important}.ph{padding-left:5px!important;padding-right:5px!important}.ph-no{padding-left:0!important;padding-right:0!important}.ph-0x{padding-left:10px!important;padding-right:10px!important}.ph-1x{padding-left:15px!important;padding-right:15px!important}.ph-2x{padding-left:20px!important;padding-right:20px!important}.ph-3x{padding-left:30px!important;padding-right:30px!important}.ph-4x{padding-left:60px!important;padding-right:60px!important}.ph-5x{padding-left:100px!important;padding-right:100px!important}.pt{padding-top:5px!important}.pt-no{padding-top:0!important}.pt-0x{padding-top:10px!important}.pt-1x{padding-top:15px!important}.pt-2x{padding-top:20px!important}.pt-3x{padding-top:30px!important}.pt-4x{padding-top:60px!important}.pt-5x{padding-top:100px!important}.pb{padding-bottom:5px!important}.pb-no{padding-bottom:0!important}.pb-0x{padding-bottom:10px!important}.pb-1x{padding-bottom:15px!important}.pb-2x{padding-bottom:20px!important}.pb-3x{padding-bottom:30px!important}.pb-4x{padding-bottom:60px!important}.pb-5x{padding-bottom:100px!important}.pl{padding-left:5px!important}.pl-no{padding-left:0!important}.pl-0x{padding-left:10px!important}.pl-1x{padding-left:15px!important}.pl-2x{padding-left:20px!important}.pl-3x{padding-left:30px!important}.pl-4x{padding-left:60px!important}.pl-5x{padding-left:100px!important}.pr{padding-right:5px!important}.pr-no{padding-right:0!important}.pr-0x{padding-right:10px!important}.pr-1x{padding-right:15px!important}.pr-2x{padding-right:20px!important}.pr-3x{padding-right:30px!important}.pr-4x{padding-right:60px!important}.pr-5x{padding-right:100px!important}.b{border:1px solid #ddd}.b-no{border:none!important}.r-rounded{border-radius:2em!important;padding-left:1em;padding-right:1em;overflow:hidden}.r-circle{border-radius:50%!important;overflow:hidden}.r-no{border-radius:0!important}.w-auto{width:auto!important}.w-full,.w-full img{width:100%!important;max-width:100%!important}.lh-2x{line-height:2}.text-break{word-break:break-all!important;word-wrap:break-word!important}.text-undecorate{text-decoration:none!important}.text-underline{text-decoration:underline!important}.text-through{text-decoration:line-through!important}.text-sub{vertical-align:sub!important}.text-super{vertical-align:super!important}.text-indent,.text-indent div,.text-indent p{text-indent:2em}.text-nowrap{max-width:100%;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;word-wrap:normal!important;-moz-binding:url(ellipsis.xml)}.text-nowrap-1x{display:-webkit-box;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:1;-webkit-box-flex:1;line-height:24px;height:24px;word-break:break-all!important;word-wrap:break-word!important}.text-nowrap-2x{display:-webkit-box;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-height:24px;height:48px;word-break:break-all!important;word-wrap:break-word!important}.text-nowrap-3x{display:-webkit-box;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:3;line-height:24px;height:72px;word-break:break-all!important;word-wrap:break-word!important}.text-nowrap-4x{display:-webkit-box;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:4;line-height:24px;height:96px;word-break:break-all!important;word-wrap:break-word!important}.text-nowrap-5x{display:-webkit-box;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:5;line-height:24px;height:120px;word-break:break-all!important;word-wrap:break-word!important}.thumb{width:16px;display:inline-block;overflow:hidden}.thumb-xs{width:32px;display:inline-block;overflow:hidden}.thumb-sm{width:48px;display:inline-block;overflow:hidden}.thumb-md{width:64px;display:inline-block;overflow:hidden}.thumb-lg{width:96px;display:inline-block;overflow:hidden}.thumb-xl{width:128px;display:inline-block;overflow:hidden}.thumb-wrapper{padding:2px;border:1px solid #dbe2e7}.thumb img,.thumb-0x img,.thumb-1x img,.thumb-2x img,.thumb-3x img,.thumb-4x img,.thumb-5x img,.thumb-6x img,.thumb-btn img{height:auto;max-width:100%;vertical-align:middle}.img-gray img,img.img-gray{-webkit-filter:grayscale(100%);-moz-filter:grayscale(100%);-ms-filter:grayscale(100%);-o-filter:grayscale(100%);filter:grayscale(100%);-webkit-filter:gray;filter:gray}.img-gray:hover img,img.img-gray:hover{-webkit-filter:grayscale(0);-moz-filter:grayscale(0);-ms-filter:grayscale(0);-o-filter:grayscale(0);filter:grayscale(0)}.img-rotate img,img.img-rotate{-webkit-transition:-webkit-transform .3s ease;transition:-webkit-transform .3s ease;transition:transform .3s ease;transition:transform .3s ease,-webkit-transform .3s ease}.img-rotate:hover img,img.img-rotate:hover{transform:rotate(360deg);-ms-transform:rotate(360deg);-moz-transform:rotate(360deg);-webkit-transform:rotate(360deg);-o-transform:rotate(360deg)}.img-burn img,img.img-burn{position:relative;-webkit-transition:all .8s ease-in-out;transition:all .8s ease-in-out}.img-burn:hover img,img.img-burn:hover{-webkit-transform:scale(1.2) rotate(2deg);-ms-transform:scale(1.2) rotate(2deg);transform:scale(1.2) rotate(2deg)}.hover-up img,img.hover-up{position:relative;top:0;-webkit-transition:top .3s ease-out;transition:top .3s ease-out}.hover-up:hover img,img.hover-up:hover{top:-6px}.active>.auto .text,.active>.text,.collapsed>.auto .text,.collapsed>.text,.text-active{display:none!important}.active>.auto .text-active,.active>.text-active,.collapsed>.auto .text-active,.collapsed>.text-active{display:inline-block!important}.shadow-no{-webkit-box-shadow:none;box-shadow:none}.hover-shadow:hover,.shadow{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.radio{margin-left:20px}.radio label{display:inline-block;position:relative;padding-left:5px}.radio label:before{content:"";display:inline-block;position:absolute;width:17px;height:17px;left:0;margin-left:-20px;border:1px solid #ccc;border-radius:50%;background-color:#fff;-webkit-transition:border .15s ease-in-out;transition:border .15s ease-in-out}.radio label:after{display:inline-block;position:absolute;content:" ";width:11px;height:11px;left:3px;top:3px;margin-left:-20px;border-radius:50%;background-color:#555;-webkit-transform:scale(0,0);-ms-transform:scale(0,0);transform:scale(0,0);-webkit-transition:-webkit-transform .1s cubic-bezier(.8,-.33,.2,1.33);transition:-webkit-transform .1s cubic-bezier(.8,-.33,.2,1.33);transition:transform .1s cubic-bezier(.8,-.33,.2,1.33);transition:transform .1s cubic-bezier(.8,-.33,.2,1.33),-webkit-transform .1s cubic-bezier(.8,-.33,.2,1.33)}.radio input[type=radio]{display:none}.radio input[type=radio]:checked+label:after{-webkit-transform:scale(1,1);-ms-transform:scale(1,1);transform:scale(1,1)}.radio input[type=radio]:disabled+label{opacity:.65}.radio input[type=radio]:disabled+label:before{cursor:not-allowed}.hover-grow{-webkit-transition:all .2s linear;transition:all .2s linear}.hover-grow:hover{-webkit-transform:translate3d(0,-2px,0);transform:translate3d(0,-2px,0)}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:21px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.57143;color:#555}.form-control{display:block;width:100%;height:35px;padding:6px 12px;font-size:14px;line-height:1.57143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}.list-disc{list-style:disc!important}.list-alpha{list-style:upper-alpha!important}.list-decimal{list-style:decimal!important}.list-outside{list-style-position:outside!important}.list-inside{list-style-position:inside!important}.list-square{list-style:none}.list-square li:before{color:#ccc;content:"▪";font-size:12px;margin-right:6px;-webkit-transition:.2s ease;transition:.2s ease}.list-circle-num,.list-square-num{counter-reset:list1}.list-circle-num>li,.list-square-num>li{list-style:none;margin-bottom:13px}.list-circle-num>li:before{counter-increment:list1;content:counter(list1) "";width:24px;height:24px;text-align:center;border-radius:12px;font-size:15px;border-width:1px;border-style:solid;margin:0 16px 0 0;display:inline-block;vertical-align:middle}.list-square-num>li:before{counter-increment:list1;content:counter(list1) "";width:24px;height:24px;text-align:center;border-radius:5px;font-size:15px;border-width:1px;border-style:solid;margin:0 16px 0 0;display:inline-block;vertical-align:middle}.list-circle-num>li>ol,.list-square-num>li>ol{counter-reset:list2}.list-circle-num>li>ol>li,.list-square-num>li>ol>li{margin-bottom:13px}.list-circle-num>li>ol>li:before{counter-increment:list2;content:counter(list1) "." counter(list2) "";width:24px;height:24px;text-align:center;border-radius:12px;font-size:15px;border-width:1px;border-style:solid;margin:0 16px 0 0;display:inline-block;vertical-align:middle}.list-square-num>li>ol>li:before{counter-increment:list2;content:counter(list1) "." counter(list2) "";width:24px;height:24px;text-align:center;border-radius:5px;font-size:15px;border-width:1px;border-style:solid;margin:0 16px 0 0;display:inline-block;vertical-align:middle}.list-circle-num[class*=list-full]>li::before,.list-square-num[class*=list-full]>li::before{background:#de4a32;color:#fff}.label{display:inline-block;padding:.3em .6em;font-size:75%;font-weight:700;line-height:1;color:#777;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label:empty{display:none}.btn .label{position:relative;top:-1px}a.label:focus,a.label:hover{color:#333;text-decoration:none;cursor:pointer}.label-default{background-color:#eee}.label-default[href]:focus,.label-default[href]:hover{background-color:#d5d5d5}.label-primary{background-color:#2196f3;color:#fff}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#0c7cd5}.label-success{background-color:#5cb85c;color:#fff}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#56ccf2;color:#fff}.label-info[href]:focus,.label-info[href]:hover{background-color:#27beee}.label-warning{background-color:#f09819;color:#fff}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#c97c0d}.label-danger{background-color:#ff512f;color:#fff}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#fb2900}.panel{margin-bottom:21px;background-color:#fff;border:1px solid #f2f2f2}.panel .article-title{display:-webkit-box;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:18px;line-height:32px;height:64px;word-break:break-all!important;word-wrap:break-word!important}.panel-body{padding:15px}.panel-body:after,.panel-body:before{content:" ";display:table}.panel-body:after{clear:both}.panel-heading{padding:10px 15px;border-bottom:1px solid #f2f2f2}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit;text-decoration:none}.panel-footer{padding:10px 15px;border-top:1px solid #f2f2f2;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel.b-no{border:0;margin-bottom:0}.panel.b-no .panel-body,.panel.b-no .panel-footer,.panel.b-no .panel-heading{border:0;padding-left:0;padding-right:0}.panel-badger{position:relative}.panel-badger:after{content:"";position:absolute;top:0;width:0;height:0;border-width:4px;border-style:solid;border-color:#777 transparent transparent #777}.panel-badger:hover:after{border-color:#2196f3 transparent transparent #2196f3}.badger-danger:after{border-color:#ff512f transparent transparent #ff512f}.badger-warning:after{border-color:#f09819 transparent transparent #f09819}.badger-success:after{border-color:#5cb85c transparent transparent #5cb85c}.badger-info:after{border-color:#56ccf2 transparent transparent #56ccf2}.badger-primary:after{border-color:#2196f3 transparent transparent #2196f3}.bg-gradient-info span,.bg-gradient-info:before{background:#56ccf2;background:-webkit-linear-gradient(left,#56ccf2 0,#2f80ed 80%,#2f80ed 100%);background:-webkit-gradient(linear,left top,right top,from(#56ccf2),color-stop(80%,#2f80ed),to(#2f80ed));background:linear-gradient(to right,#56ccf2 0,#2f80ed 80%,#2f80ed 100%)}.bg-gradient-primary span,.bg-gradient-primary:before{background:#396afc;background:-webkit-linear-gradient(left,#396afc 0,#2948ff 80%,#2948ff 100%);background:-webkit-gradient(linear,left top,right top,from(#396afc),color-stop(80%,#2948ff),to(#2948ff));background:linear-gradient(to right,#396afc 0,#2948ff 80%,#2948ff 100%)}.bg-gradient-success span,.bg-gradient-success:before{background:#44ea76;background:-webkit-linear-gradient(left,#44ea76 0,#39fad7 80%,#39fad7 100%);background:-webkit-gradient(linear,left top,right top,from(#44ea76),color-stop(80%,#39fad7),to(#39fad7));background:linear-gradient(to right,#44ea76 0,#39fad7 80%,#39fad7 100%)}.bg-gradient-warning span,.bg-gradient-warning:before{background:#ff512f;background:-webkit-linear-gradient(left,#ff512f 0,#f09819 80%,#f09819 100%);background:-webkit-gradient(linear,left top,right top,from(#ff512f),color-stop(80%,#f09819),to(#f09819));background:linear-gradient(to right,#ff512f 0,#f09819 80%,#f09819 100%)}.bg-gradient-danger span,.bg-gradient-danger:before{background:#ff512f;background:-webkit-linear-gradient(left,#ff512f 0,#dd2476 80%,#dd2476 100%);background:-webkit-gradient(linear,left top,right top,from(#ff512f),color-stop(80%,#dd2476),to(#dd2476));background:linear-gradient(to right,#ff512f 0,#dd2476 80%,#dd2476 100%)}.btn-fancy{display:inline-block;font-size:17px;letter-spacing:.03em;text-transform:uppercase;color:#fff;position:relative}.btn-fancy:before{content:'';display:inline-block;height:40px;position:absolute;bottom:-5px;left:30px;right:30px;z-index:-1;-webkit-filter:blur(20px) brightness(.95);filter:blur(20px) brightness(.95);-webkit-transform-style:preserve-3d;transform-style:preserve-3d;-webkit-transition:all .3s ease-out;transition:all .3s ease-out}.btn-fancy i{margin-top:-1px;margin-right:20px;font-size:1.265em;vertical-align:middle}.btn-fancy span{display:inline-block;padding:18px 60px;border-radius:50em;position:relative;z-index:2;will-change:transform,filter;-webkit-transform-style:preserve-3d;transform-style:preserve-3d;-webkit-transition:all .3s ease-out;transition:all .3s ease-out}.btn-fancy:active,.btn-fancy:focus{color:#fff}.btn-fancy:hover{color:#fff}.btn-fancy:hover span{-webkit-filter:brightness(1.05) contrast(1.05);filter:brightness(1.05) contrast(1.05);-webkit-transform:scale(.95);-ms-transform:scale(.95);transform:scale(.95)}.btn-fancy:hover:before{bottom:0;-webkit-filter:blur(10px) brightness(.95);filter:blur(10px) brightness(.95)}.btn-fancy.pop-onhover:before{opacity:0;bottom:10px}.btn-fancy.pop-onhover:hover:before{bottom:-7px;opacity:1;-webkit-filter:blur(20px);filter:blur(20px)}.btn-fancy.pop-onhover:hover span{-webkit-transform:scale(1.04);-ms-transform:scale(1.04);transform:scale(1.04)}.btn-fancy.pop-onhover:hover:active span{-webkit-filter:brightness(1) contrast(1);filter:brightness(1) contrast(1);-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1);-webkit-transition:all .15s ease-out;transition:all .15s ease-out}.btn-fancy.pop-onhover:hover:active:before{bottom:0;-webkit-filter:blur(10px) brightness(.95);filter:blur(10px) brightness(.95);-webkit-transition:all .2s ease-out;transition:all .2s ease-out}table,table>tbody>tr>td,table>tbody>tr>th,table>tfoot>tr>td,table>tfoot>tr>th,table>thead>tr>td,table>thead>tr>th{border:1px solid #f2f2f2}table>tbody>tr:nth-of-type(odd){background-color:#f8f8f8}table>tbody>tr:hover{background-color:#fbfbfb}table{border-collapse:collapse;border-spacing:0;padding:0;width:100%;max-width:100%;margin:10px 0}table>tbody>tr>td,table>tbody>tr>th,table>tfoot>tr>td,table>tfoot>tr>th,table>thead>tr>td,table>thead>tr>th{padding:6px 13px}table>tbody+tbody{border-top:2px solid #f2f2f2}table table{background-color:#fff}.modal button.close{position:absolute;right:10px;top:10px;z-index:99}.modal-small .modal-dialog{width:480px}@font-face{font-family:icon;src:url(iconfont.eot?t=1525101408939);src:url(iconfont.eot?t=1525101408939#iefix) format("embedded-opentype"),url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff"),url(iconfont.ttf?t=1525101408939) format("truetype"),url(iconfont.svg?t=1525101408939#icon) format("svg")}.icon{display:inline-block;font:14px/1 icon;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-webkit-text-stroke-width:.2px;-moz-osx-font-smoothing:grayscale}.icon-diandian:before{content:"\e63a"}.icon-huaban:before{content:"\e63c"}.icon-code-fork:before{content:"\e67a"}.icon-more:before{content:"\e6c0"}.icon-zhihu:before{content:"\e6d1"}.icon-linkedin:before{content:"\e724"}.icon-eye-fill:before{content:"\e64f"}.icon-stackexchange:before{content:"\e8b2"}.icon-tag:before{content:"\e6a3"}.icon-starfish:before{content:"\e62e"}.icon-home:before{content:"\e660"}.icon-search:before{content:"\e61c"}.icon-project:before{content:"\e63e"}.icon-dialog:before{content:"\e613"}.icon-twitter:before{content:"\ec9c"}.icon-github:before{content:"\e70a"}.icon-time:before{content:"\e669"}.icon-voice:before{content:"\e65a"}.icon-google:before{content:"\e601"}.icon-weibo:before{content:"\e64b"}.icon-segmentfault:before{content:"\e610"}.icon-star-fill:before{content:"\e630"}.icon-phone:before{content:"\e68a"}.icon-cup-fill:before{content:"\e614"}.icon-jiaju:before{content:"\e671"}.icon-qzone:before{content:"\e603"}.icon-home-fill:before{content:"\e617"}.icon-clock:before{content:"\e618"}.icon-file:before{content:"\e66f"}.icon-comment:before{content:"\e61a"}.icon-cup:before{content:"\e62c"}.icon-share:before{content:"\e66a"}.icon-star-half:before{content:"\e62f"}.icon-star:before{content:"\e619"}.icon-tencent-weibo:before{content:"\e602"}.icon-book:before{content:"\e79d"}.icon-bitbucket:before{content:"\e64e"}.icon-facebook:before{content:"\e6e3"}.icon-email:before{content:"\e667"}.icon-zcool:before{content:"\e60c"}.icon-social-media:before{content:"\e68b"}.icon-douban:before{content:"\e60f"}.icon-coding:before{content:"\e600"}.icon-github-fill:before{content:"\e71d"}.icon-qq:before{content:"\e611"}.icon-shu-fill:before{content:"\e615"}.icon-pinterest:before{content:"\e697"}.icon-tags:before{content:"\e6c4"}.icon-bill:before{content:"\e61b"}.icon-shu:before{content:"\e616"}.icon-book-shelf:before{content:"\e60d"}.icon-target:before{content:"\e695"}.icon-profile:before{content:"\e6e2"}.icon-alipay:before{content:"\e938"}.icon-skype:before{content:"\e604"}.icon-juejin:before{content:"\e605"}.icon-code:before{content:"\e73f"}.icon-list:before{content:"\e61e"}.icon-map-marker:before{content:"\e609"}.icon-stackoverflow:before{content:"\e606"}.icon-hourglass:before{content:"\e60e"}.icon-behance:before{content:"\e67b"}.icon-folder-open:before{content:"\e6b4"}.icon-folder:before{content:"\e60a"}.icon-menu:before{content:"\e607"}.icon-users:before{content:"\e60b"}.icon-eye:before{content:"\e657"}.icon-wechat:before{content:"\e65e"}.icon-number:before{content:"\e658"}.icon-gitlab:before{content:"\e67c"}.icon-rss:before{content:"\e63d"}.icon-archives:before{content:"\e62d"}.icon-68design:before{content:"\e608"}.icon-dribble:before{content:"\e982"}.icon-wepay:before{content:"\e629"}.icon-youdao-note:before{content:"\e8a6"}.icon-book-fill:before{content:"\e659"}.icon-hezuo:before{content:"\e6e5"}.icon-link:before{content:"\e635"}.icon-archives-fill:before{content:"\e694"}.icon-anchor:before{content:"\e858"}.icon-angle-down:before{content:"\e85e"}.icon-angle-left:before{content:"\e85f"}.icon-angle-up:before{content:"\e860"}.icon-angle-right:before{content:"\e862"}.icon-calendar:before{content:"\e895"}.icon-calendar-check:before{content:"\e896"}.icon-calendar-minus:before{content:"\e897"}.icon-calendar-plus:before{content:"\e899"}.icon-calendar-times:before{content:"\e89a"}.icon-close:before{content:"\e8c4"}.icon-delicious:before{content:"\e8e2"}.icon-plus:before{content:"\e99d"}.icon-gg:before{content:"\e6fd"}.icon-friendship:before{content:"\e612"}.icon-gitee:before{content:"\e61d"}pre .comment{color:#8e908c}pre .attribute,pre .css .class,pre .css .id,pre .css .pseudo,pre .html .doctype,pre .regexp,pre .ruby .constant,pre .tag,pre .variable,pre .xml .doctype,pre .xml .pi,pre .xml .tag .title{color:#c82829}pre .built_in,pre .constant,pre .literal,pre .number,pre .params,pre .preprocessor{color:#f5871f}pre .css .rules .attribute,pre .header,pre .inheritance,pre .ruby .class .title,pre .ruby .symbol,pre .string,pre .value,pre .xml .cdata{color:#718c00}pre .css .hexcolor,pre .title{color:#3e999f}pre .coffeescript .title,pre .function,pre .javascript .title,pre .js .title,pre .perl .sub,pre .python .decorator,pre .python .title,pre .ruby .function .title,pre .ruby .title .keyword{color:#4271ae}pre .javascript .function,pre .js .function,pre .keyword{color:#8959a8}.highlight,pre{background:#fafafa;margin:10px 0;padding:15px 10px;overflow:auto;font-size:13px;color:#4d4d4c;line-height:1.5}.gist .gist-file .gist-data .line-numbers,.highlight .gutter pre{color:#666}code{text-shadow:0 1px #fff;padding:.2em .4em;margin:0 .3em;color:#555;background:#eee;border-radius:3px;font-size:85%}pre code{background:0 0;text-shadow:none;padding:0}.highlight{position:relative;padding:32px 10px 0;border-radius:4px}.highlight:before{display:block;content:' ';height:32px;position:absolute;top:0;left:0;right:0;background-color:#f6f6f6;padding:0 10px;border-top-left-radius:4px;border-top-right-radius:4px}.highlight:after{content:" ";position:absolute;border-radius:50%;background:#fc625d;width:10px;height:10px;top:0;left:15px;margin-top:11px;-webkit-box-shadow:20px 0 #fdbc40,40px 0 #35cd4b;box-shadow:20px 0 #fdbc40,40px 0 #35cd4b}.highlight pre{border:none;margin:0}.highlight table{position:relative;border:none;width:100%;margin:0;padding:0}.highlight tr{border:none}.highlight td,.highlight th{border:none;padding:0}.highlight td.code,.highlight th.code{width:100%!important}.highlight figcaption{font-size:.85em;color:#8e908c;line-height:1em;margin-bottom:1em}.highlight figcaption a{float:right}.highlight .line{height:24px;line-height:24px}.highlight .gutter pre{text-align:right;padding-right:0;padding-left:0;color:#ccc}.header{background-color:#fbfbfb}.sidebar{background-color:#fdfdfd}.sidebar .slimContent{padding:20px}.main{position:relative;min-height:100vh;padding:15px}.main:after,.main:before{content:" ";display:table}.main:after{clear:both}.footer{padding:20px;background-color:#fbfbfb;color:#999}body.main-center .sidebar,body.main-left .header,body.main-left .sidebar{left:auto;right:0;border-left:1px solid #f6f6f6;border-right:0}body.main-left .footer{left:auto;right:0}body.no-sidebar .sidebar{display:none!important}.main-nav{float:none!important}.main-nav>li{display:block;width:100%;position:relative}.main-nav>li>a{color:#555}.main-nav>li .menu-title{margin-left:15px}.main-nav>.active a,.main-nav>.active a:focus,.main-nav>.active a:hover{color:#333;background:#f4f4f4}.profile-block{padding:20px 15px 10px}#avatar{width:64px;height:64px;display:inline-block}#avatar img{width:100%;max-height:100%;height:auto!important}#name{font-size:18px;margin-top:10px;margin-bottom:0}#title{font-size:13px;margin-top:5px;margin-bottom:5px}.sidebar-form{border-radius:3px;border:1px solid #eee;margin:0 15px 15px}.sidebar-form .btn,.sidebar-form input[type=text]{-webkit-box-shadow:none;box-shadow:none;background-color:transparent;border:1px solid transparent;height:32px}.sidebar-form .btn:focus,.sidebar-form input[type=text]:focus{outline:0}.sidebar-form input[type=text]{color:#666;border-radius:2px 0 0 2px}.sidebar-form input[type=text]:focus,.sidebar-form input[type=text]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.sidebar-form .btn{color:#999;border-radius:0 2px 2px 0}.header .navbar-collapse{padding-left:0;padding-right:0}.header .navbar-collapse .navbar-nav{margin:0}.header .navbar-toggle .icon-bar{background-color:#2196f3}.main .pager{text-align:left;margin:10px 0}.main .pager .disabled{cursor:not-allowed}.main .pager>.page-number,.main .pager>a{line-height:32px;float:left}.main .pager a{color:#666;border:0;line-height:32px;padding:0}.main .pager a:link,.main .pager a:visited{background-color:transparent}.main .pager a:hover{color:#0a6ebd;background-color:transparent}.main .pager .prev{margin-right:10px}.main .pager .page-number.current{color:#2196f3}.main .pager .page-number+.next,.main .pager .page-number+.page-number{margin-left:10px}.main .total-article{margin:10px 0;line-height:32px;color:#999}.main .page-header{margin-top:0}.main .article-list article{border-bottom:1px solid #f2f2f2}.main .article-list article:last-child{border-bottom:0}.main .article-meta{font-size:13px;color:#999}.main .article-meta a{color:#999}.main .article-meta a:hover{color:#0a6ebd;text-decoration:none}.main .article-meta span+span{margin-left:10px}.main .content{min-height:85vh}.main.has-sticky .content{margin-bottom:70px}#comments .gitment-footer-container,#comments .gitment-footer-project-link{display:none!important}.panel .label,.widget .label{font-weight:400}.widget:after,.widget:before{content:" ";display:table}.widget:after{clear:both}.widget .widget-title{font-size:18px;color:#000}.widget time{color:#999;font-size:12px;text-transform:uppercase}.widget p{margin-bottom:0}.widget ul{margin-left:0;padding-left:0;list-style:none}.widget .category-link{color:#0a6ebd}.archive-list-count,.category-list-count,.tag-list-count{padding-left:5px;color:#999;font-size:.85em}.archive-list-count:before,.category-list-count:before,.tag-list-count:before{content:"("}.archive-list-count:after,.category-list-count:after,.tag-list-count:after{content:")"}.archive-list,.category-list,.tag-list{line-height:1.75}.archive-list li:before,.category-list li:before,.tag-list li:before{color:#ccc;content:"▪";font-size:12px;margin-right:6px;-webkit-transition:.2s ease;transition:.2s ease}.category-list-child{padding-left:15px}.recent-post-list li+li{margin-top:15px}.recent-post-list li .item-inner,.recent-post-list li .item-thumb{display:table-cell;vertical-align:middle}.recent-post-list li .item-thumb{opacity:1;padding-right:10px;-webkit-transition:all .2s ease;transition:all .2s ease}.recent-post-list li .item-thumb .thumb{width:50px;height:50px;display:block;position:relative;overflow:hidden}.recent-post-list li .item-thumb .thumb span{width:100%;height:100%;display:block}.recent-post-list li .item-thumb .thumb .thumb-image{position:absolute;background-size:cover;background-position:center}.recent-post-list li .item-thumb .thumb .thumb-none{background-image:url(../images/thumb-default.png);background-size:100% 100%}.recent-post-list li:hover .item-thumb{opacity:.8}.sidebar-toc.collapse{display:none!important}.sidebar-toc.in{display:block!important}.tagcloud a{display:inline-block;margin-bottom:.2em;padding:.3em .6em;font-size:75%!important;line-height:1;background-color:#eee;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.bar .pager .next>a,.bar .pager .next>span{float:none}.bar.bar-footer{position:relative;background-color:#fff;bottom:-15px}.bar.bar-footer:before{content:'';position:absolute;width:-webkit-calc(100% + 30px);width:calc(100% + 30px);height:52px;left:-15px;border-top:1px solid #f6f6f6;border-bottom:1px solid #fff;background-color:#fff}.bar .bar-inner{position:relative;z-index:9}.bar .bar-inner:after,.bar .bar-inner:before{content:" ";display:table}.bar .bar-inner:after{clear:both}.bar .bar-right{margin:10px 0;float:right}.toggle-toc{cursor:pointer;margin-left:10px}.toggle-toc a{display:inline-block;line-height:32px;text-align:center}.footer .copyright{font-size:12px}.footer .copyright a{color:#999;text-decoration:none}.footer .copyright a:hover{color:#0a6ebd}.wave-icon{display:inline-block;position:relative}.wave-icon .wave-circle{display:block;border-radius:50%;background-color:transparent}.wave-icon .wave-circle:after,.wave-icon .wave-circle:before{content:'';border:10px solid #2196f3;background:#2196f3;border-radius:50%;position:absolute;top:50%;left:50%;z-index:1}.wave-icon .wave-circle:before{height:74px;width:74px;-webkit-animation:5s ease-out infinite pulse;animation:5s ease-out infinite pulse;margin-top:-37px;margin-left:-37px;opacity:0}.wave-icon .wave-circle:after{height:98px;width:98px;-webkit-animation:5s ease-out infinite pulse;animation:5s ease-out infinite pulse;margin-top:-49px;margin-left:-49px;opacity:.3}.wave-icon .icon{position:relative;display:block;width:50px;height:50px;line-height:50px;text-align:center;background-color:#2196f3;border-radius:50%;font-size:24px;color:#fff;z-index:2}.wave-icon.wave-icon-info .wave-circle:after,.wave-icon.wave-icon-info .wave-circle:before{border:10px solid #56ccf2;background:#56ccf2}.wave-icon.wave-icon-info .icon{background-color:#56ccf2}.wave-icon.wave-icon-primary .wave-circle:after,.wave-icon.wave-icon-primary .wave-circle:before{border:10px solid #2196f3;background:#2196f3}.wave-icon.wave-icon-primary .icon{background-color:#2196f3}.wave-icon.wave-icon-warning .wave-circle:after,.wave-icon.wave-icon-warning .wave-circle:before{border:10px solid #f09819;background:#f09819}.wave-icon.wave-icon-warning .icon{background-color:#f09819}.wave-icon.wave-icon-success .wave-circle:after,.wave-icon.wave-icon-success .wave-circle:before{border:10px solid #5cb85c;background:#5cb85c}.wave-icon.wave-icon-success .icon{background-color:#5cb85c}.wave-icon.wave-icon-danger .wave-circle:after,.wave-icon.wave-icon-danger .wave-circle:before{border:10px solid #ff512f;background:#ff512f}.wave-icon.wave-icon-danger .icon{background-color:#ff512f}@-webkit-keyframes pulse{0%{-webkit-transform:scale(0);opacity:0}25%{-webkit-transform:scale(0);opacity:.1}50%{-webkit-transform:scale(.1);opacity:.3}75%{-webkit-transform:scale(.5);opacity:.5}100%{-webkit-transform:scale(1);opacity:0}}.repo-list{list-style:none;padding-left:0}.repo{position:relative;list-style-type:none;border:1px solid #f2f2f2;margin-bottom:15px;overflow:hidden}.repo-title{padding:0 15px;margin:15px 0;font-size:16px;font-weight:600}.repo-body{display:-webkit-box;padding:0 15px;margin:0 0 20px;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-height:1.5em;height:3em;word-break:break-all!important;word-wrap:break-word!important}.repo-image{position:relative;display:table;width:101%;height:3px;margin:-1px -1px 15px;background-color:#666}.repo-meta{padding:0 15px;margin-top:5px;margin-bottom:15px;color:#777;font-size:12px;text-align:right}.repo-meta:after,.repo-meta:before{content:" ";display:table}.repo-meta:after{clear:both}.repo-meta .meta+.meta{margin-left:15px}.text-collapsed{display:none}.collapsed .text-collapsed,.text-in{display:inline-block}.collapsed .text-in{display:none}.sub-header{padding-bottom:10px;border-bottom:1px solid #eee}.article-header{margin-bottom:20px}.article-footer{margin-top:20px}.collection{position:relative}.collection a.collection-item{display:block;-webkit-transition:.25s;transition:.25s;color:#777}.collection a:not(.active):hover{color:#333}.collection .collection-item{padding:8px 0;margin:0}.article-list .article-title{font-size:18px}.article-toc .toc-title{font-size:18px;color:#000}.article-toc .toc{list-style:none;padding-left:0;line-height:2}.article-toc .toc ol{list-style:none;padding-left:10px}.article-toc .toc .toc-item{position:relative}.article-toc .toc .toc-item .markdownIt-Anchor{position:absolute;left:0;right:0;top:0;padding:14px 0}.marked-body h1,.marked-body h2,.marked-body h3,.marked-body h4,.marked-body h5,.marked-body h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.marked-body h1{padding-bottom:.3em;font-size:2em;border-bottom:1px solid #f2f2f2}.marked-body h2{padding-bottom:.3em;font-size:1.5em;border-bottom:1px solid #f2f2f2}.marked-body a{color:#2196f3;text-decoration:none}.marked-body a:focus,.marked-body a:hover{color:#0a6ebd;text-decoration:none}.marked-body ol,.marked-body ul{padding-left:0;margin-left:20px}body.okayNav-loaded{overflow-x:hidden}.okayNav{position:relative}.okayNav:after,.okayNav:before{content:" ";display:table}.okayNav:after{clear:both}.okayNav:not(.loaded){visibility:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.okayNav ul{float:left;padding-left:0}.okayNav ul li{display:inline-block;margin-left:15px}.okayNav a{position:relative;z-index:1;color:#2e2e33;font-weight:400}.okayNav a.active{color:#0a6ebd}.okayNav__nav--visible{overflow:hidden;white-space:nowrap}.okayNav__nav--visible li{display:inline-block;margin-left:15px}.okayNav__nav--visible li:first-child{margin-left:0}.okayNav__nav--visible a{display:block;-webkit-transition:color .2s cubic-bezier(.55,0,.1,1);transition:color .2s cubic-bezier(.55,0,.1,1)}.okayNav__nav--visible:empty~.okayNav__menu-toggle{top:0}.okayNav__nav--invisible{display:none;position:absolute;width:100%;top:24px;overflow-y:auto;-webkit-overflow-scrolling:touch;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05);padding-top:15px;padding-bottom:15px;background:#fff}.okayNav__nav--invisible li{display:inline-block}.okayNav__nav--invisible li a{display:block;padding:6px 15px;min-width:100px}.okayNav__nav--invisible.nav-left{left:0}.okayNav__nav--invisible.nav-right{right:0}.okayNav__nav--invisible.transition-enabled{-webkit-transition:-webkit-transform .4s cubic-bezier(.55,0,.1,1);transition:-webkit-transform .4s cubic-bezier(.55,0,.1,1);transition:transform .4s cubic-bezier(.55,0,.1,1);transition:transform .4s cubic-bezier(.55,0,.1,1),-webkit-transform .4s cubic-bezier(.55,0,.1,1)}.okayNav__nav--invisible.nav-open{display:block;z-index:99;border:1px solid #f2f2f2}.okayNav__menu-toggle{position:relative;z-index:1;float:right;cursor:pointer;-webkit-transition:-webkit-transform .4s cubic-bezier(.55,0,.1,1);transition:-webkit-transform .4s cubic-bezier(.55,0,.1,1);transition:transform .4s cubic-bezier(.55,0,.1,1);transition:transform .4s cubic-bezier(.55,0,.1,1),-webkit-transform .4s cubic-bezier(.55,0,.1,1)}.okayNav__menu-toggle.okay-invisible{position:absolute;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;opacity:0}.okayNav__menu-toggle span{background:#666;display:inline-block;width:2px;height:2px;margin:auto 1px;pointer-events:none;border-radius:50%;vertical-align:middle}.okayNav__menu-toggle.icon--active span{background:#0a6ebd}.okayNav a:hover{color:#0a6ebd}.social-links{list-style:none;padding:0;text-align:left}.social-links li{list-style:none;display:inline-block;margin-left:10px}.social-links li:first-child{margin-left:0}.header .social-links{padding:10px 20px}.footer .social-links{margin-bottom:5px}@font-face{font-family:socialshare;src:url(../fonts/iconfont.eot);src:url(../fonts/iconfont.eot?#iefix) format("embedded-opentype"),url(../fonts/iconfont.woff) format("woff"),url(../fonts/iconfont.ttf) format("truetype"),url(../fonts/iconfont.svg#iconfont) format("svg")}.social-share{display:inline-block;font-size:16px}.social-share a{position:relative;text-decoration:none;margin-left:16px;display:inline-block;outline:0;line-height:32px}.social-share .social-share-icon{position:relative;display:inline-block;height:32px;line-height:32px;color:#999;text-align:center;vertical-align:middle;-webkit-transition:background .6s ease-out 0s;transition:background .6s ease-out 0s}.social-share .social-share-icon:hover{color:#666}.social-share .icon-weibo:hover{color:#ff763b}.social-share .icon-qq:hover,.social-share .icon-tencent:hover{color:#56b6e7}.social-share .icon-qzone:hover{color:#fdbe3d}.social-share .icon-douban:hover{color:#33b045}.social-share .icon-linkedin:hover{color:#0077b5}.social-share .icon-facebook:hover{color:#44619d}.social-share .icon-google:hover{color:#db4437}.social-share .icon-twitter:hover{color:#55acee}.social-share .icon-diandian:hover{color:#307dca}.social-share .icon-wechat{position:relative}.social-share .icon-wechat:hover{color:#7bc549}.social-share .icon-wechat .wechat-qrcode{display:none;border:1px solid #eee;position:absolute;z-index:9;top:-209px;left:-90px;width:200px;height:200px;color:#666;font-size:12px;text-align:center;background-color:#fff;-webkit-transition:all .2s;transition:all .2s;-webkit-tansition:all 350ms;-moz-transition:all 350ms}.social-share .icon-wechat .wechat-qrcode.bottom{top:40px;left:-84px}.social-share .icon-wechat .wechat-qrcode.bottom:after{display:none}.social-share .icon-wechat .wechat-qrcode h4{font-weight:400;height:26px;line-height:26px;font-size:12px;background-color:#f3f3f3;margin:0;padding:0;color:#777}.social-share .icon-wechat .wechat-qrcode .qrcode{width:105px;margin:15px auto}.social-share .icon-wechat .wechat-qrcode .qrcode table{margin:0!important}.social-share .icon-wechat .wechat-qrcode .help p{font-weight:400;line-height:16px;padding:0;margin:0}.social-share .icon-wechat .wechat-qrcode:before{content:'';position:absolute;left:50%;margin-left:-6px;bottom:-15px;width:0;height:0;border-width:8px 6px 6px;border-style:solid;border-color:#eee transparent transparent}.social-share .icon-wechat .wechat-qrcode:after{content:'';position:absolute;left:50%;margin-left:-6px;bottom:-13px;width:0;height:0;border-width:8px 6px 6px;border-style:solid;border-color:#fff transparent transparent}.social-share .icon-wechat:hover .wechat-qrcode{display:block}.btn-donate{position:absolute;bottom:10px;left:50%;margin-left:-25px;width:50px;height:50px;line-height:50px;padding:0;border-radius:50%;font-size:18px;cursor:pointer;z-index:99}.btn-donate:active,.btn-donate:focus,.btn-donate:hover{border-color:transparent!important;outline:0!important}.btn-donate.btn-fancy{background-color:transparent}.btn-donate.btn-fancy span{width:50px;height:50px;padding:0}.donate{overflow:hidden}.donate-box{text-align:center;padding-top:30px}.donate-box .donate-head{width:100%;height:80px;text-align:center;line-height:60px;color:#a3a3a3;font-size:16px;position:relative}.donate-box .donate-head:after,.donate-box .donate-head:before{font-family:Arial,Helvetica,sans-serif;background:0 0;width:0;height:0;font-style:normal;color:#eee;font-size:100px;position:absolute;top:15px}.donate-box .donate-head:before{content:'\201c';left:30px}.donate-box .donate-head:after{content:'\201d';right:70px}.donate-box .donate-footer{padding-top:35px}.donate-box .donate-payimg{display:inline-block;padding:10px;border:6px solid #ea5f00;margin:0 auto;border-radius:3px}.donate-box .donate-payimg img{display:block;text-align:center;width:140px;height:140px}.book .media-middle{display:inline-block;width:115px}.ins-search{display:none}.ins-search.show{display:block}.ins-selectable{cursor:pointer}.ins-search-container,.ins-search-mask{position:fixed}.ins-search-mask{top:0;left:0;width:100%;height:100%;z-index:1050;background:rgba(0,0,0,.5)}.ins-input-wrapper{position:relative}.ins-search-input{width:100%;border:none;outline:0;font-size:16px;-webkit-box-shadow:none;box-shadow:none;font-weight:200;border-radius:0;background:#fff;line-height:20px;-webkit-box-sizing:border-box;box-sizing:border-box;padding:12px 28px 12px 20px;border-bottom:1px solid #e2e2e2;font-family:"Microsoft Yahei Light","Microsoft Yahei",Helvetica,Arial,sans-serif}.ins-close{top:50%;right:6px;width:20px;height:20px;font-size:24px;margin-top:-15px;position:absolute;text-align:center;opacity:1;color:#666;display:inline-block}.ins-close:hover{color:#006bde}.ins-search-container{left:50%;top:100px;z-index:1051;bottom:100px;-webkit-box-sizing:border-box;box-sizing:border-box;width:540px;margin-left:-270px}.ins-section-wrapper{left:0;right:0;top:45px;bottom:0;overflow-y:auto;position:absolute}.ins-section-container{position:relative;background:#f7f7f7}.ins-section{font-size:14px;line-height:16px}.ins-section .ins-search-item,.ins-section .ins-section-header{padding:8px 15px}.ins-section .ins-section-header{color:#9a9a9a;border-bottom:1px solid #e2e2e2}.ins-section .ins-slug{margin-left:5px;color:#9a9a9a}.ins-section .ins-slug:before{content:'('}.ins-section .ins-slug:after{content:')'}.ins-section .ins-search-item .ins-search-preview,.ins-section .ins-search-item header{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ins-section .ins-search-item header .icon{margin-right:8px}.ins-section .ins-search-item .ins-search-preview{height:15px;font-size:12px;color:#9a9a9a;margin:5px 0 0 20px}.ins-section .ins-search-item.active,.ins-section .ins-search-item:hover{color:#fff;background:#006bde}.ins-section .ins-search-item.active .ins-search-preview,.ins-section .ins-search-item.active .ins-slug,.ins-section .ins-search-item:hover .ins-search-preview,.ins-section .ins-search-item:hover .ins-slug,.theme-black .header,.theme-blue .header,.theme-green .header,.theme-purple .header{color:#fff}.theme-black .header a,.theme-blue .header a,.theme-green .header a,.theme-purple .header a{color:#efefef}.theme-black .header #location,.theme-blue .header #location,.theme-green .header #location,.theme-purple .header #location{color:rgba(255,255,255,.75)!important}.theme-black .header .navbar-toggle .icon-bar,.theme-blue .header .navbar-toggle .icon-bar,.theme-green .header .navbar-toggle .icon-bar,.theme-purple .header .navbar-toggle .icon-bar{background-color:#fff}.theme-black .footer,.theme-black .footer a,.theme-blue .footer,.theme-blue .footer a,.theme-green .footer,.theme-green .footer a,.theme-purple .footer,.theme-purple .footer a{color:rgba(255,255,255,.75)}.theme-black .footer a.active,.theme-black .footer a:focus,.theme-black .footer a:hover,.theme-black .header a.active,.theme-black .header a:focus,.theme-black .header a:hover,.theme-blue .footer a.active,.theme-blue .footer a:focus,.theme-blue .footer a:hover,.theme-blue .header a.active,.theme-blue .header a:focus,.theme-blue .header a:hover,.theme-green .footer a.active,.theme-green .footer a:focus,.theme-green .footer a:hover,.theme-green .header a.active,.theme-green .header a:focus,.theme-green .header a:hover,.theme-purple .footer a.active,.theme-purple .footer a:focus,.theme-purple .footer a:hover,.theme-purple .header a.active,.theme-purple .header a:focus,.theme-purple .header a:hover{color:#fff}.theme-black .main-nav>li>a.active,.theme-black .main-nav>li>a:focus,.theme-black .main-nav>li>a:hover,.theme-blue .main-nav>li>a.active,.theme-blue .main-nav>li>a:focus,.theme-blue .main-nav>li>a:hover,.theme-green .main-nav>li>a.active,.theme-green .main-nav>li>a:focus,.theme-green .main-nav>li>a:hover,.theme-purple .main-nav>li>a.active,.theme-purple .main-nav>li>a:focus,.theme-purple .main-nav>li>a:hover{color:#fff;background:rgba(0,0,0,.15)}.theme-black .main-nav>.active a,.theme-black .main-nav>.active a.active,.theme-black .main-nav>.active a:focus,.theme-black .main-nav>.active a:hover,.theme-blue .main-nav>.active a,.theme-blue .main-nav>.active a.active,.theme-blue .main-nav>.active a:focus,.theme-blue .main-nav>.active a:hover,.theme-green .main-nav>.active a,.theme-green .main-nav>.active a.active,.theme-green .main-nav>.active a:focus,.theme-green .main-nav>.active a:hover,.theme-purple .main-nav>.active a,.theme-purple .main-nav>.active a.active,.theme-purple .main-nav>.active a:focus,.theme-purple .main-nav>.active a:hover{color:#fff;background:rgba(0,0,0,.2)}.theme-black .search .sidebar-form,.theme-blue .search .sidebar-form,.theme-green .search .sidebar-form,.theme-purple .search .sidebar-form{border:0;background:rgba(0,0,0,.2)}.theme-black .search .sidebar-form input::-webkit-input-placeholder,.theme-blue .search .sidebar-form input::-webkit-input-placeholder,.theme-green .search .sidebar-form input::-webkit-input-placeholder,.theme-purple .search .sidebar-form input::-webkit-input-placeholder{color:rgba(255,255,255,.5)}.theme-black .search .sidebar-form input:-moz-placeholder,.theme-blue .search .sidebar-form input:-moz-placeholder,.theme-green .search .sidebar-form input:-moz-placeholder,.theme-purple .search .sidebar-form input:-moz-placeholder{color:rgba(255,255,255,.5)}.theme-black .search .sidebar-form input::-moz-placeholder,.theme-blue .search .sidebar-form input::-moz-placeholder,.theme-green .search .sidebar-form input::-moz-placeholder,.theme-purple .search .sidebar-form input::-moz-placeholder{color:rgba(255,255,255,.5)}.theme-black .search .sidebar-form input:-ms-input-placeholder,.theme-blue .search .sidebar-form input:-ms-input-placeholder,.theme-green .search .sidebar-form input:-ms-input-placeholder,.theme-purple .search .sidebar-form input:-ms-input-placeholder{color:rgba(255,255,255,.5)}.theme-black .search input[type=text],.theme-blue .search input[type=text],.theme-green .search input[type=text],.theme-purple .search input[type=text]{color:#666}.theme-black .search input[type=text]+.input-group-btn .btn,.theme-blue .search input[type=text]+.input-group-btn .btn,.theme-green .search input[type=text]+.input-group-btn .btn,.theme-purple .search input[type=text]+.input-group-btn .btn{color:rgba(255,255,255,.5)}.theme-black .search input[type=text]:focus,.theme-black .search input[type=text]:focus+.input-group-btn .btn,.theme-blue .search input[type=text]:focus,.theme-blue .search input[type=text]:focus+.input-group-btn .btn,.theme-green .search input[type=text]:focus,.theme-green .search input[type=text]:focus+.input-group-btn .btn,.theme-purple .search input[type=text]:focus,.theme-purple .search input[type=text]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.theme-black .header{background:#1a2433;background:-webkit-linear-gradient(left,#1a2433 0,#253449 80%,#253449 100%);background:-webkit-gradient(linear,left top,right top,from(#1a2433),color-stop(80%,#253449),to(#253449));background:linear-gradient(to right,#1a2433 0,#253449 80%,#253449 100%)}.theme-blue .header{background:#0062c5;background:-webkit-linear-gradient(left,#0062c5 0,#0073e6 80%,#0073e6 100%);background:-webkit-gradient(linear,left top,right top,from(#0062c5),color-stop(80%,#0073e6),to(#0073e6));background:linear-gradient(to right,#0062c5 0,#0073e6 80%,#0073e6 100%)}.theme-green .header{background:#08a283;background:-webkit-linear-gradient(left,#08a283 0,#0ac29d 80%,#0ac29d 100%);background:-webkit-gradient(linear,left top,right top,from(#08a283),color-stop(80%,#0ac29d),to(#0ac29d));background:linear-gradient(to right,#08a283 0,#0ac29d 80%,#0ac29d 100%)}.theme-purple .header{background:#494683;background:-webkit-linear-gradient(left,#494683 0,#555299 80%,#555299 100%);background:-webkit-gradient(linear,left top,right top,from(#494683),color-stop(80%,#555299),to(#555299));background:linear-gradient(to right,#494683 0,#555299 80%,#555299 100%)}@media (min-width:767px){.modal-center{text-align:center;padding:0!important}.modal-center:before{content:'';display:inline-block;height:100%;vertical-align:middle;margin-right:-4px}.modal-center .modal-dialog{display:inline-block;text-align:left;vertical-align:middle}.donate-box .donate-footer{margin:0 -15px -16px}}@media (min-width:768px){.lead{font-size:21px}.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.container{width:750px}.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-1{width:8.33333%}.col-sm-2{width:16.66667%}.col-sm-3{width:25%}.col-sm-4{width:33.33333%}.col-sm-5{width:41.66667%}.col-sm-6{width:50%}.col-sm-7{width:58.33333%}.col-sm-8{width:66.66667%}.col-sm-9{width:75%}.col-sm-10{width:83.33333%}.col-sm-11{width:91.66667%}.col-sm-12{width:100%}.col-sm-pull-0{right:auto}.col-sm-pull-1{right:8.33333%}.col-sm-pull-2{right:16.66667%}.col-sm-pull-3{right:25%}.col-sm-pull-4{right:33.33333%}.col-sm-pull-5{right:41.66667%}.col-sm-pull-6{right:50%}.col-sm-pull-7{right:58.33333%}.col-sm-pull-8{right:66.66667%}.col-sm-pull-9{right:75%}.col-sm-pull-10{right:83.33333%}.col-sm-pull-11{right:91.66667%}.col-sm-pull-12{right:100%}.col-sm-push-0{left:auto}.col-sm-push-1{left:8.33333%}.col-sm-push-2{left:16.66667%}.col-sm-push-3{left:25%}.col-sm-push-4{left:33.33333%}.col-sm-push-5{left:41.66667%}.col-sm-push-6{left:50%}.col-sm-push-7{left:58.33333%}.col-sm-push-8{left:66.66667%}.col-sm-push-9{left:75%}.col-sm-push-10{left:83.33333%}.col-sm-push-11{left:91.66667%}.col-sm-push-12{left:100%}.col-sm-offset-0{margin-left:0}.col-sm-offset-1{margin-left:8.33333%}.col-sm-offset-2{margin-left:16.66667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.33333%}.col-sm-offset-5{margin-left:41.66667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.33333%}.col-sm-offset-8{margin-left:66.66667%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.33333%}.col-sm-offset-11{margin-left:91.66667%}.col-sm-offset-12{margin-left:100%}.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{left:0;right:auto}.nav-justified>li,.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{margin-bottom:0}.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{border-bottom:1px solid #f2f2f2;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}.navbar{border-radius:4px}.navbar-header{float:left}.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-left:0;padding-right:0}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}.navbar-toggle{display:none}.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:14.5px;padding-bottom:14.5px}.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.05);box-shadow:0 5px 15px rgba(0,0,0,.05)}.modal-sm{width:300px}.header{position:fixed;top:0;bottom:0;left:0;z-index:1000;display:block;padding:0;overflow-y:auto;border-right:1px solid #f6f6f6;width:4.16667%}.sidebar{position:fixed;top:0;bottom:0;left:0;display:block;padding:0;overflow-x:hidden;overflow-y:auto;border-right:1px solid #f6f6f6;width:33.33333%}.main{width:62.5%;padding-right:20px;padding-left:20px}.footer{position:fixed;left:0;bottom:0;background-color:transparent;z-index:1050;width:4.16667%}body.main-center .main{margin-left:4.16667%}body.main-left .sidebar{margin-right:4.16667%}body.main-right .sidebar{margin-left:4.16667%}body.main-right .main{margin-left:37.5%}body.no-sidebar.main-left .main{width:95.83333333%;margin-right:4.16667%}body.no-sidebar.main-center .main,body.no-sidebar.main-right .main{width:95.83333333%;margin-left:4.16667%}.header .navbar-header{float:none}#avatar img{padding:5px}.bar.bar-footer:before{width:-webkit-calc(100% + 40px);width:calc(100% + 40px);left:-20px}.header .social-links{display:none}}@media (min-width:992px){.container{width:970px}.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-1{width:8.33333%}.col-md-2{width:16.66667%}.col-md-3{width:25%}.col-md-4{width:33.33333%}.col-md-5{width:41.66667%}.col-md-6{width:50%}.col-md-7{width:58.33333%}.col-md-8{width:66.66667%}.col-md-9{width:75%}.col-md-10{width:83.33333%}.col-md-11{width:91.66667%}.col-md-12{width:100%}.col-md-pull-0{right:auto}.col-md-pull-1{right:8.33333%}.col-md-pull-2{right:16.66667%}.col-md-pull-3{right:25%}.col-md-pull-4{right:33.33333%}.col-md-pull-5{right:41.66667%}.col-md-pull-6{right:50%}.col-md-pull-7{right:58.33333%}.col-md-pull-8{right:66.66667%}.col-md-pull-9{right:75%}.col-md-pull-10{right:83.33333%}.col-md-pull-11{right:91.66667%}.col-md-pull-12{right:100%}.col-md-push-0{left:auto}.col-md-push-1{left:8.33333%}.col-md-push-2{left:16.66667%}.col-md-push-3{left:25%}.col-md-push-4{left:33.33333%}.col-md-push-5{left:41.66667%}.col-md-push-6{left:50%}.col-md-push-7{left:58.33333%}.col-md-push-8{left:66.66667%}.col-md-push-9{left:75%}.col-md-push-10{left:83.33333%}.col-md-push-11{left:91.66667%}.col-md-push-12{left:100%}.col-md-offset-0{margin-left:0}.col-md-offset-1{margin-left:8.33333%}.col-md-offset-2{margin-left:16.66667%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.33333%}.col-md-offset-5{margin-left:41.66667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.33333%}.col-md-offset-8{margin-left:66.66667%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.33333%}.col-md-offset-11{margin-left:91.66667%}.col-md-offset-12{margin-left:100%}.modal-lg{width:900px}.header{width:16.66667%}.sidebar{width:25%}.sidebar .slimContent{padding-right:25px;padding-left:25px}.main{width:58.33333%;padding-right:25px;padding-left:25px}.footer{width:16.66667%}body.main-center .main{margin-left:16.66667%}body.main-left .sidebar{margin-right:16.66667%}body.main-right .sidebar{margin-left:16.66667%}body.main-right .main{margin-left:41.66667%}body.no-sidebar.main-left .main{width:83.33333333%;margin-right:16.66667%}body.no-sidebar.main-center .main,body.no-sidebar.main-right .main{width:83.33333333%;margin-left:16.66667%}.bar.bar-footer:before{width:-webkit-calc(100% + 50px);width:calc(100% + 50px);left:-25px}.marked-body .headerlink:before,.marked-body .markdownIt-Anchor:before{display:inline-block;width:18px;content:"#";color:#0a6ebd;text-align:right;float:left;visibility:hidden}.marked-body .headerlink:before{margin-left:-15px;padding-right:2px}.marked-body .markdownIt-Anchor:before{margin-left:-20px}.marked-body h1:hover .headerlink:before,.marked-body h1:hover .markdownIt-Anchor:before,.marked-body h2:hover .headerlink:before,.marked-body h2:hover .markdownIt-Anchor:before,.marked-body h3:hover .headerlink:before,.marked-body h3:hover .markdownIt-Anchor:before,.marked-body h4:hover .headerlink:before,.marked-body h4:hover .markdownIt-Anchor:before,.marked-body h5:hover .headerlink:before,.marked-body h5:hover .markdownIt-Anchor:before,.marked-body h6:hover .headerlink:before,.marked-body h6:hover .markdownIt-Anchor:before{visibility:visible}}@media (min-width:1200px){.container{width:1170px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-1{width:8.33333%}.col-lg-2{width:16.66667%}.col-lg-3{width:25%}.col-lg-4{width:33.33333%}.col-lg-5{width:41.66667%}.col-lg-6{width:50%}.col-lg-7{width:58.33333%}.col-lg-8{width:66.66667%}.col-lg-9{width:75%}.col-lg-10{width:83.33333%}.col-lg-11{width:91.66667%}.col-lg-12{width:100%}.col-lg-pull-0{right:auto}.col-lg-pull-1{right:8.33333%}.col-lg-pull-2{right:16.66667%}.col-lg-pull-3{right:25%}.col-lg-pull-4{right:33.33333%}.col-lg-pull-5{right:41.66667%}.col-lg-pull-6{right:50%}.col-lg-pull-7{right:58.33333%}.col-lg-pull-8{right:66.66667%}.col-lg-pull-9{right:75%}.col-lg-pull-10{right:83.33333%}.col-lg-pull-11{right:91.66667%}.col-lg-pull-12{right:100%}.col-lg-push-0{left:auto}.col-lg-push-1{left:8.33333%}.col-lg-push-2{left:16.66667%}.col-lg-push-3{left:25%}.col-lg-push-4{left:33.33333%}.col-lg-push-5{left:41.66667%}.col-lg-push-6{left:50%}.col-lg-push-7{left:58.33333%}.col-lg-push-8{left:66.66667%}.col-lg-push-9{left:75%}.col-lg-push-10{left:83.33333%}.col-lg-push-11{left:91.66667%}.col-lg-push-12{left:100%}.col-lg-offset-0{margin-left:0}.col-lg-offset-1{margin-left:8.33333%}.col-lg-offset-2{margin-left:16.66667%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.33333%}.col-lg-offset-5{margin-left:41.66667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.33333%}.col-lg-offset-8{margin-left:66.66667%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.33333%}.col-lg-offset-11{margin-left:91.66667%}.col-lg-offset-12{margin-left:100%}.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}.visible-lg-block{display:block!important}.visible-lg-inline{display:inline!important}.visible-lg-inline-block{display:inline-block!important}.hidden-lg{display:none!important}.header{width:16.66667%}.sidebar{width:23%}.sidebar .slimContent{padding-right:30px;padding-left:30px}.main{width:60.33333%;padding-right:30px;padding-left:30px}.footer{width:16.66667%}body.main-center .main{margin-left:16.66667%}body.main-left .sidebar{margin-right:16.66667%}body.main-right .sidebar{margin-left:16.66667%}body.main-right .main{margin-left:39.66667%}body.no-sidebar.main-left .main{width:83.33333333%;margin-right:16.66667%}body.no-sidebar.main-center .main,body.no-sidebar.main-right .main{width:83.33333333%;margin-left:16.66667%}.main-nav>li>a{padding:10px 20px}.bar.bar-footer:before{width:-webkit-calc(100% + 60px);width:calc(100% + 60px);left:-30px}}@media (min-width:1440px){.header{width:16.66667%}.sidebar{width:21%}.main{width:62.33333%}.footer{width:16.66667%}body.main-center .main{margin-left:16.66667%}body.main-left .sidebar{margin-right:16.66667%}body.main-right .sidebar{margin-left:16.66667%}body.main-right .main{margin-left:37.66667%}body.no-sidebar.main-left .main{width:83.33333333%;margin-right:16.66667%}body.no-sidebar.main-center .main,body.no-sidebar.main-right .main{width:83.33333333%;margin-left:16.66667%}.header #title{font-size:15px}}@media (max-width:1199px){.main-nav>li>a{padding:6px 20px}.bar .pager li a span,.footer .copyright{display:none}}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:21px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}.visible-xs-block{display:block!important}.visible-xs-inline{display:inline!important}.visible-xs-inline-block{display:inline-block!important}.hidden-xs{display:none!important}body{padding-top:53px}.list-circle-num,.list-square-num{margin:0 0 40px}.modal-xs-full .modal-content,.modal-xs-full .modal-dialog{height:100%;width:100%;margin:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.modal-xs-full .modal-content .donate-box,.modal-xs-full .modal-dialog .donate-box{padding-top:50px}.highlight table:before{display:block;content:' ';height:38px;position:absolute;top:0;left:0;right:0;margin-left:-10px;margin-right:-10px;margin-top:-38px;background-color:#f6f6f6;border-top-left-radius:4px;border-top-right-radius:4px}.header{position:fixed;left:0;right:0;top:0;width:100%;z-index:1050}.header:after,.header:before{content:" ";display:table}.header:after{clear:both}.sidebar{display:none}.main{min-height:auto}.main-nav>li>a{padding:10px 20px}.sidebar-form{border:0;margin:9px 45px 9px 0}.sidebar-form .input-group-btn .btn{color:#2196f3}.sidebar-form input[type=text]:focus{background-color:transparent}.sidebar-form input[type=text]:focus+.input-group-btn .btn{color:#2196f3;background-color:transparent}.header .navbar-toggle{position:absolute;top:0;right:0;margin-right:5px}.header .navbar-toggle .icon-bar{width:18px}.header .search{width:90%;float:right}.header .profile-block{padding:0;margin:10px 0;width:8.33333%;float:left}.header #avatar{width:32px;height:32px}.header #location,.header #name,.header #title{display:none}#main-navbar{position:absolute;width:100%;background-color:#fbfbfb;border-bottom:1px solid #f6f6f6;z-index:99}.main .content{min-height:auto}.sidebar-toc.in{position:fixed;top:50px;bottom:50px;z-index:9}.bar.bar-footer{top:auto!important;bottom:0!important}.footer{display:none;position:static}.footer .copyright{display:block}.social-links .tooltip{display:none!important;visibility:hidden}.theme-black .search input[type=text]:focus,.theme-blue .search input[type=text]:focus,.theme-green .search input[type=text]:focus,.theme-purple .search input[type=text]:focus{background-color:transparent}.theme-black .search input[type=text]:focus+.input-group-btn .btn,.theme-blue .search input[type=text]:focus+.input-group-btn .btn,.theme-green .search input[type=text]:focus+.input-group-btn .btn,.theme-purple .search input[type=text]:focus+.input-group-btn .btn{color:rgba(255,255,255,.5);background-color:transparent}.theme-black #main-navbar{background:#1a2433;background:-webkit-linear-gradient(left,#1a2433 0,#253449 80%,#253449 100%);background:-webkit-gradient(linear,left top,right top,from(#1a2433),color-stop(80%,#253449),to(#253449));background:linear-gradient(to right,#1a2433 0,#253449 80%,#253449 100%)}.theme-blue #main-navbar{background:#0062c5;background:-webkit-linear-gradient(left,#0062c5 0,#0073e6 80%,#0073e6 100%);background:-webkit-gradient(linear,left top,right top,from(#0062c5),color-stop(80%,#0073e6),to(#0073e6));background:linear-gradient(to right,#0062c5 0,#0073e6 80%,#0073e6 100%)}.theme-green #main-navbar{background:#08a283;background:-webkit-linear-gradient(left,#08a283 0,#0ac29d 80%,#0ac29d 100%);background:-webkit-gradient(linear,left top,right top,from(#08a283),color-stop(80%,#0ac29d),to(#0ac29d));background:linear-gradient(to right,#08a283 0,#0ac29d 80%,#0ac29d 100%)}.theme-purple #main-navbar{background:#494683;background:-webkit-linear-gradient(left,#494683 0,#555299 80%,#555299 100%);background:-webkit-gradient(linear,left top,right top,from(#494683),color-stop(80%,#555299),to(#555299));background:linear-gradient(to right,#494683 0,#555299 80%,#555299 100%)}}@media screen and (max-width:559px),screen and (max-height:479px){.ins-search-container{top:0;left:0;margin:0;width:100%;height:100%;background:#f7f7f7}}@media (max-width:480px){.header #avatar{width:24px;height:24px;margin-top:3px;margin-left:15px}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}.visible-sm-block{display:block!important}.visible-sm-inline{display:inline!important}.visible-sm-inline-block{display:inline-block!important}.hidden-sm{display:none!important}.sidebar-form{display:none}.header .main-nav>li>a{text-align:center;padding-left:0;padding-right:0}.header .main-nav>li>a span{display:none}.header .profile-block{padding-top:10px;padding-left:0;padding-right:0}.header #avatar{width:32px;height:32px}.footer{padding-left:0;padding-right:0}.social-links{display:block;width:100%;text-align:center;margin-bottom:0}.social-links:after,.social-links:before{content:" ";display:table}.social-links:after{clear:both}.social-links li{display:block;margin-left:0;margin-top:10px}.social-links li:after,.social-links li:before{content:" ";display:table}.social-links li:after{clear:both}.social-links .tooltip{display:none!important;visibility:hidden}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}.visible-md-block{display:block!important}.visible-md-inline{display:inline!important}.visible-md-inline-block{display:inline-block!important}.hidden-md{display:none!important}}@media print{*,:after,:before{background:0 0!important;color:#000!important;-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}.visible-print-block{display:block!important}.visible-print-inline{display:inline!important}.visible-print-inline-block{display:inline-block!important}.hidden-print{display:none!important}}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}} \ No newline at end of file diff --git a/css/vs2015.css b/css/vs2015.css new file mode 100644 index 0000000..9cf9644 --- /dev/null +++ b/css/vs2015.css @@ -0,0 +1,29 @@ +/* highlight.js: vs2015.css */ +.hljs { + /* 其余部分略 */ + background: #1e1e1e; +} + +/* Material Theme: style.css*/ +#post-content pre { + /* 其余部分略 */ + background-color: #f7f7f7; +} + +/* 在config_css.ejs中加入的内容,使用!important提高优先级 */ +/* 如果不使用公用CDN库,可以直接修改CSS文件 */ +pre { + background-color: #1e1e1e !important; + font-size: 15px !important; +} + +.hljs { + margin: 0 0 0 0 !important; + padding-left: 0 !important; + line-height: 1.5em !important; +} + +/* (可选)把highlight.js主题加入的斜体去掉,Visual Studio的注释并不是斜体啊喂! */ +.hljs-comment { + font-style: normal; +} \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..000d3872eb6e4246edf726f8942902c984ec3e8e GIT binary patch literal 1150 zcmb7^=~Gi@6o+qTs<~MpAtI;=$>nAtEMW;tPND9O5|fj$xsv} zAc!Q&zq$lPk=lnJP4|+(o$Vznf#$*K){{6~I+xz#bc?QeMfczmU!q6%cqq)#D9&{e z4!Wo<(r5_jG?e;js>q@JN-o{iUV7?$v{h=fSGee?bWs(|qBx)t@ajaHt=#KOW3KZY zn}a&LBR=-V{TzH%z~NMgli4t*@yi@UgKQ0GeBYPJe7lwLhEzIBGl{lXSm?=M`<9QR znL=JaDB*Y^#OaeTTX)M?nJnkzei1KXdF)MiSn0Fyq}R?wvzdV^8{;h|;ytPCO*#2_ z<`Qq0b9nb%K7Xv|@^DONW+;#2xP$H68N7;T@#7sEKSk46`ohF;qsmmL%G$V@=hGH8 zCsmFfr3f}&-Z!!lGxK!9%Fg5&)*>cOS1)k9l+Mw^batn$e0#&d$R|d2W-Po}JJ0?D zGcOm6oIJDhX4A?q>uLPDVdp@!y?Z7O7ESENQ+Ty}j_0$fERU-6HCR}USa`dU!RuuU zzwc}OdFbS?BP)NOIC!^ZMhBK4Rlo58EUdH`}rB-eJb12 zX7(42{Pu&9!>1|CkJ(tiZD79dEGt6>R>xG9235Wh@4^k0;f55h*Vq`SPnGj9Guw5R zNBsu2BdSC*{dG2`T8(nfDs#OCVjTv)?lv$j=Qb+)M&*BuHd+{}PGeN&#G1_9y=rE$ zO=VuVQ=h_^=!1#cRTJZ~zFA}C<`p}=p$oJXI=EJrNq5ObI*J^8S(ZUWYOz`?k;-&N z$}{Pg+K_mn;qwfJE^&ZN^ W&UugD<4tP+t#iE|K71ei-}OHv#L3VA literal 0 HcmV?d00001 diff --git a/fonts/README.html b/fonts/README.html new file mode 100644 index 0000000..587721c --- /dev/null +++ b/fonts/README.html @@ -0,0 +1,8 @@ +

    Font Assets

    +

    If you are providing web font files, this is the place to put them. The fonts task will copy them over to the destination specified in config.json, and file names will be reved in production builds (if enabled).

    +

    If you don’t plan using web fonts, or are relying on an external service like Google Fonts, feel free to delete this folder and the tasks.fonts config in gulpfile.js/config.json.

    +

    Tasks and Files

    +
    gulpfile.js/tasks/fonts
    +
    +

    All this task does is copy fonts from ./src/fonts to ./public/fonts.

    +

    A sass +font-face mixin is included in ./src/stylesheets/base/mixins.

    diff --git a/fonts/iconfont.eot b/fonts/iconfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..6316b0b47948bf7f268f72f0d5b2472876e22c40 GIT binary patch literal 23452 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 literal 0 HcmV?d00001 diff --git a/fonts/iconfont.svg b/fonts/iconfont.svg new file mode 100644 index 0000000..bc6d8ac --- /dev/null +++ b/fonts/iconfont.svg @@ -0,0 +1,321 @@ + + + + + +Created by iconfont + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fonts/iconfont.ttf b/fonts/iconfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..41ad9903f5e5221c474dec1acc78a3eacec22353 GIT binary patch literal 23300 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 literal 0 HcmV?d00001 diff --git a/fonts/iconfont.woff b/fonts/iconfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..45ca56ea2d11b3e8a258695b1b82ba6d00c9946b GIT binary patch literal 16108 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 ZbCc#|#wp0PVteC)xl_PyVCp1*{{#O-L&E?7 literal 0 HcmV?d00001 diff --git a/placeholder "b/hidden/Excel\346\257\224\345\257\271\344\270\216\345\220\210\345\271\266\347\263\273\347\273\237_\345\256\214\346\225\264\347\211\210.html" similarity index 100% rename from placeholder rename to "hidden/Excel\346\257\224\345\257\271\344\270\216\345\220\210\345\271\266\347\263\273\347\273\237_\345\256\214\346\225\264\347\211\210.html" diff --git a/images/avatar.jpg b/images/avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5ebcf22ebe12ae56b488a975526f7e3b205632fa GIT binary patch literal 63059 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 literal 0 HcmV?d00001 diff --git a/images/donate/alipayimg.png b/images/donate/alipayimg.png new file mode 100644 index 0000000000000000000000000000000000000000..77af25bc3ae8e2ee9aeb292f99b1a2888a594e0b GIT binary patch literal 156172 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 literal 0 HcmV?d00001 diff --git a/images/donate/wechatpayimg.png b/images/donate/wechatpayimg.png new file mode 100644 index 0000000000000000000000000000000000000000..1e57afeeb69713b5dc941f18862e2a22af4d1151 GIT binary patch literal 116094 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 t1309vOAqbxx%-O1= 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 z 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 literal 0 HcmV?d00001 diff --git a/images/favatar/SzsFox-logo.png b/images/favatar/SzsFox-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..35e2838b18b293a15dd6c8d17fc8d1d31c43a9b0 GIT binary patch literal 2724 zcmbVO3se(V8je*csGv~sfq2LUC<;j?4@k&Bzz{$p0s(_k5Ogw`Jk}%=W&!~etq4`L zLYHb0q{^#%yPGqQJNJI~`@jEj z|C1bX#44NFjq0-nNu)WSnC}^+qT zBvv0Az6OL*4IPr9QiPtW(Gh48$uA&P2f>>WJz0t}C3_1@u(aGelQ zszTE-WNmtc1Ww-!^JJ6&f3jaHNDyccJw#5`sI@ql%BPI;g2dQ74N%DA5dCI8MPN2a zjtv)+g(!xQz3Chp%=BWBxjZ_P!{D*JsALv{$psi(fXSvYm>`1#vU%jmABC`n$>d;k zP{^b$;)_pF>h(Gh01O5L-N2@!m;zw(cszi?0$3~>fuP}OT0N9X)8Z~u3_%DEV=A3q zg=)!WMo5Y#>-iKy>4Xa!-88Khp9~WbFd!Au0Zcl>?9w<;22bO3$(VZFxC{mmHKIYZ zdYr&Ar?EOEsz-4p`X1`(<+ltFp$!k8w((9a8qKr`Tpyf5xG@=!ccO7gnhpV?5gbj% zU?ez&km+LfMh6Np1k$6J1Vzf@Lr$M==dVxU14Y3Ixz; z6%_Jx5J4fTMlr%Lp^p9Lyhtb%W2juECKmAMRe@wtu#m&#aX2&Ng+0e7_p>Wh{IqbY-DPG5DF)o3t(!$ z%)B3hxg41fmnP$Sd($8(K|;*1X>2*fl}g!M8OPguYJViAA`T#={=a$5$I@ImV5kZw za+)@IaMmJ8lOwf?JYFmy1e*^5p8}h+kH{#K)2eqx;CFd2L2FPV1nFGL_&c7&eizZ z8$D_YPBcFAaJu6XAJaV>(Gt`c(UB{F?s+8AER85gAW1DA?Ao|HCfMox$hCw7n`&2K z`%~w|QPIyoUhOewY-YP~lsxL_U|~ON$NcL;ft}!A?ZVqa)GgR8swaL2wB79zbv1ME z9_p`(Lw~KTtl7m|RP(G7f8Ode+}*d;c)o9_xR7@bA8LL1!0`8VwRdkZ{Abn;otK@k zTkz-iUr_p`Un-KfT)t6HTGSIrZQ}XNIRSqf7koQH>;vC0#2nm#CIvj#|4kiGR}zhg zG9wyQXDn(=meh)}Z$}R1;rr|*2f$esjZLw%K~bntm*o0&eA}|vylQr5d^VNqexjuI z$#;y(HA}E7QqPgB6`$_%%aogvpp+u4cfQ_5dsQ46A-C?xV3ombPTzgo!>hkfvNe@{ zf<`ax_<6K~Hur4D*&CftU(ZM*)sEbs_3O<85`m{K`IjG#7~Rh0IX@}5zsSERx8#&5 z?(9De>f4WARF?)6N(7yzR{Orh2C#5wpv*O<4I8;PX4|!IyZxO3>$07rX>OPNPDJ*7 zMOUWu4qvRIo!WOe-=lO*T#D=X=;6wovGuN}udj`^$N>Jmpu*vFz*xRS;M-i|u+Fq( zWaN+0`QVj$cWR3?vV|Y}WxV7s+CR+x}>WO&Hj;-|#27M!fIE9fhwDoi?;AqRPBV)cgDI9K<1m|`8*#5q^CVR5v3 zr}f?&#~z0)E9;+^{n=p3x!oH#E00p=ZGu)?BqQtcOx{LO+-}dk@REhrGY8aAT?hS= z^TJ!aU8|Ng1!r_*U3mD?)YXvx>Y%60z_t2EsrA}|65FQ0F+*O}Ngwit$5aUNS#ITM z=ele~uAO_LIIN`~ushu5dteKAt0w`f^MVEzdVK?5h)S+c-JYbr7~l9&QdDoTXI5cF ze#y3NuaHAeY*$iS5ANJs&S}4vX;uB8GPLj;EWRn>kwM_{;!My_>~c=qaHiD^T(ibP z&|k2;Lo-~q;1ylDqS7O3sG!_tw6cK_0r+WfFK zKS&gikdan(dVUeL;n|tvy=jL=qHc!Z)^XYW*+%uP4v*Q`xTfF+iu^+iTgT)U0_Ii(d-98>xx808&39!rm!nxEK(q})q)Zal~4IVxY z2evR=n`3;lA8m=ho}&4&==I$s?D$i!BiSM1tlzj^ljcAd{o5{oWd1)&5(P&DRR(U# F{11eH4Eq28 literal 0 HcmV?d00001 diff --git a/images/favatar/chuangzaoshi-logo.png b/images/favatar/chuangzaoshi-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2b85f56a0f78a28744c688b2e920046ec01058b1 GIT binary patch literal 851 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 W&UugD<4tP+t#iE|K71ei-}OHv#L3VA literal 0 HcmV?d00001 diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5ebcf22ebe12ae56b488a975526f7e3b205632fa GIT binary patch literal 63059 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 literal 0 HcmV?d00001 diff --git a/images/thumb-default.png b/images/thumb-default.png new file mode 100644 index 0000000000000000000000000000000000000000..e1ec3b436ba89cb1becb60f6ff976fd1e9e9deab GIT binary patch literal 3862 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 literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 0000000..478872a --- /dev/null +++ b/index.html @@ -0,0 +1,892 @@ + + + + + + + + + + + + + + + + + + + + panzun + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + +
    + +
    + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/application.js b/js/application.js new file mode 100644 index 0000000..4a87f3a --- /dev/null +++ b/js/application.js @@ -0,0 +1,77 @@ +$(function() { + // bootstrap tooltip + $('[data-toggle="tooltip"]').tooltip(); + + // slimscroll + if (typeof $.fn.slimScroll != 'undefined') { + $(".sidebar .slimContent").slimScroll({ + height: $(window).height(), + color: "rgba(0,0,0,0.15)", + size: "5px", + position: 'right', + // allowPageScroll: true + }); + } + + $('#collapseToc').on('shown.bs.collapse', function() { + // do something… + // slimscroll + if (typeof $.fn.slimScroll != 'undefined') { + $(".sidebar .slimContent").slimScroll().on('slimscroll'); + } + }); + + // geopattern 背景生成 + $(".geopattern").each(function() { + $(this).geopattern($(this).data('pattern-id')); + }); + + // okayNav + var navigation = $('#nav-main').okayNav({ + swipe_enabled: false, // If true, you'll be able to swipe left/right to open the navigation + }); + + // modal居中 + // $('.modal').on('shown.bs.modal', function(e) { + // $(this).show(); + // var modalDialog = $(this).find(".modal-dialog"); + // // Applying the top margin on modal dialog to align it vertically center + // modalDialog.css("margin-top", Math.max(0, ($(window).height() - modalDialog.height()) / 2)); + // }); + + // sticky + $('[data-stick-bottom]').keepInView({ + fixed: false, + parentClass: "has-sticky", + customClass: "sticky", + trigger: 'bottom', + zindex: 42, + edgeOffset: 0 + }); + + $('[data-stick-top]').keepInView({ + fixed: true, + parentClass: "has-sticky", + customClass: "sticky", + trigger: 'top', + zindex: 42, + edgeOffset: 0 + }); + + // menu auto highlight + var menuHighlight = $("ul.main-nav").hasClass('menu-highlight'); + if (menuHighlight) { + var currentPathname = location.pathname, + $menuList = $("ul.main-nav>li"), + activeIndex = -1; + for (var i = 0, length = $menuList.length; i < length; i++) { + var itemHref = $($menuList[i]).find('a').attr('href'); + if (currentPathname.indexOf(itemHref) > -1 || + (currentPathname === '/' && (itemHref === '/.' || itemHref === '/' || itemHref === 'index.html' || itemHref === '/index.html'))) { + activeIndex = i; + } + $($menuList[i]).removeClass('active'); + } + $menuList[activeIndex] && $($menuList[activeIndex]).addClass('active'); + } +}); \ No newline at end of file diff --git a/js/application.min.js b/js/application.min.js new file mode 100644 index 0000000..bcdb6ee --- /dev/null +++ b/js/application.min.js @@ -0,0 +1 @@ +$(function(){$('[data-toggle="tooltip"]').tooltip(),"undefined"!=typeof $.fn.slimScroll&&$(".sidebar .slimContent").slimScroll({height:$(window).height(),color:"rgba(0,0,0,0.15)",size:"5px",position:"right"}),$("#collapseToc").on("shown.bs.collapse",function(){"undefined"!=typeof $.fn.slimScroll&&$(".sidebar .slimContent").slimScroll().on("slimscroll")}),$(".geopattern").each(function(){$(this).geopattern($(this).data("pattern-id"))});$("#nav-main").okayNav({swipe_enabled:!1});$("[data-stick-bottom]").keepInView({fixed:!1,parentClass:"has-sticky",customClass:"sticky",trigger:"bottom",zindex:42,edgeOffset:0}),$("[data-stick-top]").keepInView({fixed:!0,parentClass:"has-sticky",customClass:"sticky",trigger:"top",zindex:42,edgeOffset:0});var t=$("ul.main-nav").hasClass("menu-highlight");if(t){for(var e=location.pathname,i=$("ul.main-nav>li"),a=-1,s=0,n=i.length;s-1||"/"===e&&("/."===o||"/"===o||"index.html"===o||"/index.html"===o))&&(a=s),$(i[s]).removeClass("active")}i[a]&&$(i[a]).addClass("active")}}); \ No newline at end of file diff --git a/js/insight.js b/js/insight.js new file mode 100644 index 0000000..74225f5 --- /dev/null +++ b/js/insight.js @@ -0,0 +1,240 @@ +/** + * Insight search plugin + * @author PPOffice { @link https://github.com/ppoffice } + */ +(function ($, CONFIG) { + var $main = $('.ins-search'); + var $input = $main.find('.ins-search-input'); + var $wrapper = $main.find('.ins-section-wrapper'); + var $container = $main.find('.ins-section-container'); + $main.parent().remove('.ins-search'); + $('body').append($main); + + function section (title) { + return $('
    ').addClass('ins-section') + .append($('
    ').addClass('ins-section-header').text(title)); + } + + function searchItem (icon, title, slug, preview, url) { + return $('
    ').addClass('ins-selectable').addClass('ins-search-item') + .append($('
    ').append($('').addClass('icon').addClass('icon-' + icon)).append(title != null && title != '' ? title : CONFIG.TRANSLATION['UNTITLED']) + .append(slug ? $('').addClass('ins-slug').text(slug) : null)) + .append(preview ? $('

    ').addClass('ins-search-preview').text(preview) : null) + .attr('data-url', url); + } + + function sectionFactory (type, array) { + var sectionTitle; + var $searchItems; + if (array.length === 0) return null; + sectionTitle = CONFIG.TRANSLATION[type]; + switch (type) { + case 'POSTS': + case 'PAGES': + $searchItems = array.map(function (item) { + // Use config.root instead of permalink to fix url issue + return searchItem('file', item.title, null, item.text.slice(0, 150), CONFIG.ROOT_URL + item.path); + }); + break; + case 'CATEGORIES': + case 'TAGS': + $searchItems = array.map(function (item) { + return searchItem(type === 'CATEGORIES' ? 'folder' : 'tag', item.name, item.slug, null, item.permalink); + }); + break; + default: + return null; + } + return section(sectionTitle).append($searchItems); + } + + function extractToSet (json, key) { + var values = {}; + var entries = json.pages.concat(json.posts); + entries.forEach(function (entry) { + if (entry[key]) { + entry[key].forEach(function (value) { + values[value.name] = value; + }); + } + }); + var result = []; + for (var key in values) { + result.push(values[key]); + } + return result; + } + + function parseKeywords (keywords) { + return keywords.split(' ').filter(function (keyword) { + return !!keyword; + }).map(function (keyword) { + return keyword.toUpperCase(); + }); + } + + /** + * Judge if a given post/page/category/tag contains all of the keywords. + * @param Object obj Object to be weighted + * @param Array fields Object's fields to find matches + */ + function filter (keywords, obj, fields) { + var result = false; + var keywordArray = parseKeywords(keywords); + var containKeywords = keywordArray.filter(function (keyword) { + var containFields = fields.filter(function (field) { + if (!obj.hasOwnProperty(field)) + return false; + if (obj[field].toUpperCase().indexOf(keyword) > -1) + return true; + }); + if (containFields.length > 0) + return true; + return false; + }); + return containKeywords.length === keywordArray.length; + } + + function filterFactory (keywords) { + return { + POST: function (obj) { + return filter(keywords, obj, ['title', 'text']); + }, + PAGE: function (obj) { + return filter(keywords, obj, ['title', 'text']); + }, + CATEGORY: function (obj) { + return filter(keywords, obj, ['name', 'slug']); + }, + TAG: function (obj) { + return filter(keywords, obj, ['name', 'slug']); + } + }; + } + + /** + * Calculate the weight of a matched post/page/category/tag. + * @param Object obj Object to be weighted + * @param Array fields Object's fields to find matches + * @param Array weights Weight of every field + */ + function weight (keywords, obj, fields, weights) { + var value = 0; + parseKeywords(keywords).forEach(function (keyword) { + var pattern = new RegExp(keyword, 'img'); // Global, Multi-line, Case-insensitive + fields.forEach(function (field, index) { + if (obj.hasOwnProperty(field)) { + var matches = obj[field].match(pattern); + value += matches ? matches.length * weights[index] : 0; + } + }); + }); + return value; + } + + function weightFactory (keywords) { + return { + POST: function (obj) { + return weight(keywords, obj, ['title', 'text'], [3, 1]); + }, + PAGE: function (obj) { + return weight(keywords, obj, ['title', 'text'], [3, 1]); + }, + CATEGORY: function (obj) { + return weight(keywords, obj, ['name', 'slug'], [1, 1]); + }, + TAG: function (obj) { + return weight(keywords, obj, ['name', 'slug'], [1, 1]); + } + }; + } + + function search (json, keywords) { + var WEIGHTS = weightFactory(keywords); + var FILTERS = filterFactory(keywords); + var posts = json.posts; + var pages = json.pages; + var tags = extractToSet(json, 'tags'); + var categories = extractToSet(json, 'categories'); + return { + posts: posts.filter(FILTERS.POST).sort(function (a, b) { return WEIGHTS.POST(b) - WEIGHTS.POST(a); }).slice(0, 5), + pages: pages.filter(FILTERS.PAGE).sort(function (a, b) { return WEIGHTS.PAGE(b) - WEIGHTS.PAGE(a); }).slice(0, 5), + categories: categories.filter(FILTERS.CATEGORY).sort(function (a, b) { return WEIGHTS.CATEGORY(b) - WEIGHTS.CATEGORY(a); }).slice(0, 5), + tags: tags.filter(FILTERS.TAG).sort(function (a, b) { return WEIGHTS.TAG(b) - WEIGHTS.TAG(a); }).slice(0, 5) + }; + } + + function searchResultToDOM (searchResult) { + $container.empty(); + for (var key in searchResult) { + $container.append(sectionFactory(key.toUpperCase(), searchResult[key])); + } + } + + function scrollTo ($item) { + if ($item.length === 0) return; + var wrapperHeight = $wrapper[0].clientHeight; + var itemTop = $item.position().top - $wrapper.scrollTop(); + var itemBottom = $item[0].clientHeight + $item.position().top; + if (itemBottom > wrapperHeight + $wrapper.scrollTop()) { + $wrapper.scrollTop(itemBottom - $wrapper[0].clientHeight); + } + if (itemTop < 0) { + $wrapper.scrollTop($item.position().top); + } + } + + function selectItemByDiff (value) { + var $items = $.makeArray($container.find('.ins-selectable')); + var prevPosition = -1; + $items.forEach(function (item, index) { + if ($(item).hasClass('active')) { + prevPosition = index; + return; + } + }); + var nextPosition = ($items.length + prevPosition + value) % $items.length; + $($items[prevPosition]).removeClass('active'); + $($items[nextPosition]).addClass('active'); + scrollTo($($items[nextPosition])); + } + + function gotoLink ($item) { + if ($item && $item.length) { + location.href = $item.attr('data-url'); + } + } + + $.getJSON(CONFIG.CONTENT_URL, function (json) { + if (location.hash.trim() === '#ins-search') { + $main.addClass('show'); + } + $input.on('input', function () { + var keywords = $(this).val(); + searchResultToDOM(search(json, keywords)); + }); + $input.trigger('input'); + }); + + + $(document).on('click focus', '.search-form-input', function () { + $main.addClass('show'); + $main.find('.ins-search-input').focus(); + }).on('click', '.ins-search-item', function () { + gotoLink($(this)); + }).on('click', '.ins-close', function () { + $main.removeClass('show'); + }).on('keydown', function (e) { + if (!$main.hasClass('show')) return; + switch (e.keyCode) { + case 27: // ESC + $main.removeClass('show'); break; + case 38: // UP + selectItemByDiff(-1); break; + case 40: // DOWN + selectItemByDiff(1); break; + case 13: //ENTER + gotoLink($container.find('.ins-selectable.active').eq(0)); break; + } + }); +})(jQuery, window.INSIGHT_CONFIG); \ No newline at end of file diff --git a/js/jquery.min.js b/js/jquery.min.js new file mode 100644 index 0000000..e836475 --- /dev/null +++ b/js/jquery.min.js @@ -0,0 +1,5 @@ +/*! jQuery v1.12.4 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="1.12.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(!l.ownFirst)for(b in a)return k.call(a,b);for(b in a);return void 0===b||k.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(h)return h.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=e.call(arguments,2),d=function(){return a.apply(b||this,c.concat(e.call(arguments)))},d.guid=a.guid=a.guid||n.guid++,d):void 0},now:function(){return+new Date},support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.addEventListener?(d.removeEventListener("DOMContentLoaded",K),a.removeEventListener("load",K)):(d.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(d.addEventListener||"load"===a.event.type||"complete"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener("DOMContentLoaded",K),a.addEventListener("load",K);else{d.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll("left")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst="0"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName("body")[0],c&&c.style&&(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement("div");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b},N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0; +}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),"object"!=typeof b&&"function"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\w:-]+)/,_=/^$|\/(?:java|ecma)script/i,aa=/^\s+/,ba="abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video";function ca(a){var b=ba.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement("div"),b=d.createDocumentFragment(),c=d.createElement("input");a.innerHTML="
    a",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,""],legend:[1,"

    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:l.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?""!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("
    +
    + + +
    +