diff --git a/README.md b/README.md index 364e399..4beca2f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# 《后端技术总结》 + +# 《大厂面试指北》 最佳阅读地址:http://notfound9.github.io/interviewGuide/ @@ -6,17 +7,14 @@ Github项目主页:https://github.com/NotFound9/interviewGuide 作者博客地址:https://juejin.im/user/5b370a42e51d4558ce5eb969 - ## 为什么要做这个开源项目? 之前在业余时间阅读技术书籍时,发现只阅读而不产出,这样收效甚微。所以就在网上找了很多常见的技术问题,根据自己的技术积累,查阅书籍,阅读文档和博客等资料,尝试着用自己的话去写了很多原创解答,最终整理开源到Github。一方面是便于自己复习巩固,一方面是将这些自己写的解答开源出来分享给大家,希望可以帮助到大家,也欢迎大家一起来完善这个项目,为开源做贡献。 -欢迎大家进群一起学习进步! -
- + ## 目录 - [首页](README.md) diff --git a/docs/ArrayList.md b/docs/ArrayList.md index 3b8cf8d..d2717bb 100644 --- a/docs/ArrayList.md +++ b/docs/ArrayList.md @@ -17,7 +17,7 @@ #### 1.底层使用的数据结构 -* Arraylist 底层使用的是**Object数组**,初始化时就会指向的会是一个static修饰的空数组,数组长度一开始为**0**,插入第一个元素时数组长度会初始化为**10**,之后每次数组空间不够进行扩容时都是增加为原来的**1.5倍**。ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间(为了避免添加元素时,数组空间不够频繁申请内存),而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放后继指针next和前驱指针pre以及数据) +* ArrayList 底层使用的是**Object数组**,初始化时就会指向的会是一个static修饰的空数组,数组长度一开始为**0**,插入第一个元素时数组长度会初始化为**10**,之后每次数组空间不够进行扩容时都是增加为原来的**1.5倍**。ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间(为了避免添加元素时,数组空间不够频繁申请内存),而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放后继指针next和前驱指针pre以及数据) * LinkedList 底层使用的数据结构是**双向链表**,每个节点保存了指向前驱节点和后继结点的指针。初始化时,不执行任何操作,添加第一个元素时,再去构造链表中的节点。 diff --git a/docs/CodingInterviews.md b/docs/CodingInterviews.md index df91109..bc294f1 100644 --- a/docs/CodingInterviews.md +++ b/docs/CodingInterviews.md @@ -895,13 +895,14 @@ public static boolean VerifySquenceOfBST(int[] sequence, int start, int end) { } else if(rightChildIndex== start) {//说明全部位于右边子树 return VerifySquenceOfBST(sequence,start,end-1); } + //继续校验 return VerifySquenceOfBST(sequence,start,rightChildIndex-1) && VerifySquenceOfBST(sequence,rightChildIndex, end-1); } ``` ## 题023 二叉树中和为某一值的路径 -输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的 list 中,数组长度大的数组靠前) +输入一颗二叉树的根节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的 list 中,数组长度大的数组靠前。) 就是递归调用每个节点的左右子树,然后将节点值相加,如果节点值和为某个预期值,并且该节点为叶子节点,那么这条路径就是要找的路径。 @@ -1067,8 +1068,6 @@ public RandomListNode Clone(RandomListNode pHead) } ``` - - ## 题027数组中出现的次数超过一半的数字 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出 2 。如果不存在则输出 0 。 @@ -1115,7 +1114,7 @@ public int MoreThanHalfNum_Solution(int [] array) { 输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。 -其实就是插入排序,只不过只是对k个数进行插入排序 +其实就是插入排序,只不过只是对k个数进行插入排序,但是这样的时间复杂度会是O(N^2),如果是使用快排来做,平均时间复杂度就是O(N)。 ```java public ArrayList GetLeastNumbers_Solution(int [] input, int k) { @@ -1150,13 +1149,13 @@ public ArrayList GetLeastNumbers_Solution(int [] input, int k) { ## 题029连续子数组的最大和 -例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1) +例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第1个开始,到第4个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1) 使用动态规划的方法来进行思考 f(n) 有两种取值 -* 当f(n-1)<=0时,取array[n] +* 当f(n-1)<=0时,取array[n],从这个元素重新开始 * 当f(n-1)>0时,取f(n-1)+array[n] @@ -1169,7 +1168,7 @@ f(n) 有两种取值 int currentSum = array[0]; int maxSum = currentSum; for (int i = 1; i < array.length; i++) { - if (currentSum<0) { + if (currentSum<0) {//前面的和是负数,就直接丢弃 currentSum = array[i]; } else { currentSum = currentSum + array[i]; @@ -1618,7 +1617,7 @@ public int IsBalanced_Solution_Depth(TreeNode root) { int right = IsBalanced_Solution_Depth(root.right); if (left!=-1 && right!= -1) { int temp = left-right; - if (temp<=1&& temp>=-1) { + if (temp<=1 && temp>=-1) { return left > right ? left + 1 : right + 1; } } @@ -1868,7 +1867,7 @@ public static void qsort(int[] array, int start ,int end) { 在n个人中间报数时,每个人的编号是 0 1 2 ... k k+1 k+2 ... n-1 当k出局以后,在n-1个人中报数时,每个人的编号是重新从k+1开始计数,原来的编号就映射为下面这样了(原来k+1变成了0,原来的n-1变成了n-1-(k+1)) -n-k+1 ... 0 1 ... n-1 -(k+1) +n-k-1 ... 0 1 ... n-1 -(k+1) 所以假设从n-1报数时的编号到n个人报数时的编号存在一个映射关系 假设f(n)代表n个人报数编号 f(n) = (f(n-1)+k+1)%n = (f(n-1)+ (m-1)%n + 1)%n = @@ -1956,7 +1955,7 @@ public int Add(int num1,int num2) { -2147483648 至 2147483647 --2的31次方 2的31次方-1 +-2的31次方 2的31次方 -1 ```java public static int StrToInt(String str) { @@ -2037,12 +2036,14 @@ public int[] multiply(int[] A) { int[] d = new int[A.length]; c[0] = 1; int result1 = c[0]; + //构建数组C for (int i = 1; i < c.length; i++) { result1 = result1 * A[i-1]; c[i] = result1; } d[d.length-1] = 1; int result2 = 1; + //构建数组D for (int i = d.length-2; i >=0; i--) { result2 = result2 * A[i+1]; d[i] = result2; @@ -2070,15 +2071,15 @@ public int[] multiply(int[] A) { } public char FirstAppearingOnce() - { - for (int i = 0; i < str.length(); i++) { - char c = str.charAt(i); - if (table[c] == 1) { - return c; - } - } - return '#'; +{ + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (table[c] == 1) { + return c; + } } + return '#'; +} ``` ## 题054 链表中环的入口节点 @@ -2088,11 +2089,11 @@ public char FirstAppearingOnce() 一种方法是遍历整个链表,将每个节点添加到HashSet中,判断是否在HashSet中出现过,第一个重复的节点就是环的入口节点。 -另一种解决方法是,假设存在环,环的长度为x,第一个指针先走x步,然后第二个指针从链表头结点出发,两个指针一起走,当第而个指针刚好走到环入口时,第一个指针正好在环中走了一圈,也在环的入口,此时的节点就是环的的入口节点, +另一种解决方法是,假设存在环,环的长度为x,第一个指针先走x步,然后第二个指针从链表头结点出发,两个指针一起走,当第二个指针刚好走到环入口时,第一个指针正好在环中走了一圈,也在环的入口,此时的节点就是环的的入口节点, 怎么得到环的长度呢,就是一个指针每次走2步,一个指针每次走一步,他们相遇时的节点肯定就是在环中的某个节点,然后这个节点在环中遍历一圈,回到原点,就可以得到环的长度count。 -两个指针从头出发,第一个指针先走count步,然后两个指针每次都只走一步,相遇的地方就是环的入口, +两个指针从头出发,第一个指针先走count步,然后两个指针每次都只走一步,相遇的地方就是环的入口。 ```java public ListNode EntryNodeOfLoop(ListNode pHead) @@ -2193,7 +2194,6 @@ public ListNode deleteDuplication(ListNode pHead) ListNode ourHead = new ListNode(0); ourHead.next = pHead; - int temp = pHead.val; ListNode preNode = ourHead; ListNode currentNode = ourHead.next; @@ -2329,7 +2329,7 @@ public ArrayList> Print(TreeNode pRoot) { int flag = 0;//代表当前遍历的是奇数层还是偶数层。区别在于添加子节点的顺序。 stack1.add(pRoot); while ((flag == 0 && stack1.size()>0) || (flag == 1 && stack2.size()>0)) { - if (flag==0) { + if (flag==0) {//代表是偶数层 ArrayList array = new ArrayList(); while (stack1.size()>0) { TreeNode node = stack1.pop(); @@ -2439,7 +2439,7 @@ ArrayList> Print(TreeNode pRoot) { 就是递归遍历每一个节点,遍历时传入深度depth,将节点加入到ArrayList中特定深度对应的数组中去。 -这种方法也可以用来进行二叉树深度遍历,遍历完之后将嵌套数组拆分成单层的数组。 +这种方法也可以用来进行二叉树先序遍历,遍历完之后将嵌套数组拆分成单层的数组。 ```java ArrayList> Print2(TreeNode pRoot) { @@ -2738,7 +2738,7 @@ boolean judge(char[] matrix, int rows, int cols, int i, int j, char[] str, int c return false; } if (charIndex==str.length-1) { return true;} - + //用来记录这个位置是否在路径中 flag[index]=true; if (judge(matrix,rows,cols,i+1, j, str, charIndex+1, flag) ||judge(matrix,rows,cols,i-1, j, str, charIndex+1, flag) @@ -2846,10 +2846,6 @@ public int cutRope(int target) { } ``` - - - - # 基础篇 ## 二分查找 diff --git a/docs/Git.md b/docs/Git.md new file mode 100644 index 0000000..83ed882 --- /dev/null +++ b/docs/Git.md @@ -0,0 +1,126 @@ +##### git命令使用 + +`git branch dev` 创建一个名称为dev的分支 + +`git checkout dev`切换到dev分支上 + + +`git checkout -b dev ` 从当前分支上创建出一个新分支dev + + +`git merge dev` 假设当前为master分支,执行这条命令后会将dev分支合并到当前master分支,合并完成后,master分支和dev分支会变成同一个分支 + +`git:(dev) git rebase master` 假设当前为dev分支,git rebase master命令会将当dev的更改进行回退,回退到与main分支交汇的地方,将这些更改暂存到rebase目录,然后将master上面的提交拿过来,让dev分支更新到master分支那样,然后再把之前暂存的更改应用上,中间如果出现冲突,需要解决冲突,解决完冲突后,使用git add 将更新添加上,然后使用git rebase --continue继续合并。如果中间需要中断合并那么可以使用git rebase —abort。 + +在rebase完成后,我们dev分支已经是最新了,但是master上还是老的代码,我们需要使用git checkout master 切换到master分支上,然后使用git rebase dev将dev上面的更改移动到master上来,然后push到远程。 +![image-20210603172535748](../static/image-20210603172535748.png) + +`git checkout HEAD^` 将当前head指向上1次提交 + +`git checkout HEAD~3` 将当前head指向上3次提交 + +`git reset HEAD~1` 回滚到上一次提交 + +`git reset HEAD` 生成一个新的提交,用于将上一个提交的更改撤销掉,等价于`git reset HEAD~1` + +`git cherry-pick C3 C4 C7`将其他分支上的C3提交,C4提交,C7提交拿到这个分支上面来 + +`git rebase -i HEAD~3` 合并最近的两次提交为一次提交记录,执行这个命令后会进入vim界面,然后会出现3次提交的记录 + +```java +pick 115e825 queryCreditsMonth +pick 4cedfe6 queryCreditsMonth +pick b3dccfd nickname +``` + +有一下操作符: +pick:保留该commit(缩写:p) +reword:保留该commit,但我需要修改该commit的注释(缩写:r) +edit:保留该commit, 但我要停下来修改该提交(不仅仅修改注释)(缩写:e) +squash:将该commit和前一个commit合并(缩写:s) +fixup:将该commit和前一个commit合并,但我不要保留该提交的注释信息(缩写:f) +exec:执行shell命令(缩写:x) +drop:我要丢弃该commit(缩写:d) + +咱们如果要把三次提交合并成一个就改成这样 + +``` +pick 115e825 queryCreditsMonth +s 4cedfe6 queryCreditsMonth +s b3dccfd nickname +``` + +然后保存退出,然后会进入填写commit说明的地方,我们直接保存就好了,这样就完成了,会生成一个新的commit + +```java +commit b3dccfd2c2173fa0a6358de604b6541c8c6c644a (HEAD -> feature-dev) +Date: Fri May 7 16:29:22 2021 +0800 + + nickname + + nickname + + change credits +``` + + + +`git commit -amend`可以修改最后一次提交的commit说明 + + + + + + + +##### rebase出现了冲突怎么办? + +假设master上跟我们改了同一行,其实就会造成冲突 +例如下面这个就是我们需要解决的冲突,<<<<< HEAD 到====== 之间为其他分支上别人的更改,======到>>>>>>> change credits之间为我们自己在dev分支上的更改,我们需要解决冲突,然后使用git add对文件进行提交 +```java + @Override + public List creditsMonthsQuery(Integer months, long year) { +<<<<<<< HEAD + List creditsMonth = creditsMonthsMapper.creditsMonthsQuery(months, year); + for(CreditsMonthsUser creditsMonthsUser : creditsMonth){ + String nickname = WechatUtil.decodeNickName(creditsMonthsUser.getNickname()); + creditsMonthsUser.setNickname(nickname); + } + return creditsMonth; +======= + Integer a = 100; + return creditsMonthsMapper.creditsMonthsQuery(months, year); +>>>>>>> change credits + } + +``` + +##### 日常开发流程应该是怎么样的呢? +1. git fetch origin dev 拉取远程的dev分支到本地,本地也会创建出dev分支 + +2.git checkout -b feature-dev 我们自己从dev分支创建出一个feature-dev分支,用于我们自己开发 + +3.我们自己在feature-dev开发了一些功能后,进行提交时,可能其他人往dev分支上提交了更新,我们需要将更新拿到本地 + +4. + +git checkout dev 切到dev分支 +git pull origin dev 将其他人在远程dev上的提交拉到本地 + +5. + +git checkout feature-dev + +git rebase dev 将dev上的更改应用到我们的feature-dev分支 + +然后可能会出现冲突,我们对冲突进行合并, + +然后对修改后的文件使用git add +文件名 进行添加, + +添加完成后使用git rebase --continue就可以继续,然后合并完成(如果需要中断rebase操作可以使用git rebase --abort) + +6.git checkout feature-dev + +git rebase feature-dev 就可以把feature-dev上合并好的更改拿到dev分支上 + +7. git push origin dev 就可以将dev分支上的更改提交到远程 \ No newline at end of file diff --git a/docs/HTTP.md b/docs/HTTP.md index c265d7b..fbd2a92 100644 --- a/docs/HTTP.md +++ b/docs/HTTP.md @@ -11,7 +11,7 @@ ![img](../static/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3p1b3lpZ2VoYWl6ZWk=,size_16,color_FFFFFF,t_70.png) -在发起连接之前,服务器会向证书机构申请SSL证书,流程是服务器将自己的公钥发给CA证书机构,CA证书机构会用自己的私钥对服务器的公钥加密,生成CA证书给服务器,服务器将SSL证书存储后供之后建立安全连接使用。 +在发起连接之前,服务器会向证书机构申请SSL证书,流程是服务器将自己的公钥发给CA证书机构,CA证书机构会用自己的私钥对服务器的公钥加密,生成SSL证书给服务器,服务器将SSL证书存储后供之后建立安全连接使用。 1.客户端发起请求,TCP三次握手,跟服务器建立连接。 如上图所示,在第 ② 步时服务器发送了一个SSL证书给客户端,SSL 证书中包含的具体内容有: @@ -174,6 +174,10 @@ TCP连接刚建立,一点一点地提速,试探一下网络的承受能力 一旦认定网络拥塞,就会将慢启动阀值ssthresh设置为发生拥塞时的窗口cwnd大小的一半。 +PS:快重传是什么? + +假设发送方发送了1,2,3,4个数据包,假设接收方收到1,2数据包,并且发送了ACK确认,没有收到3,但是收到了4。根据可靠性传输原理,由于没有收到3,即便接受到了4,它也是一个失序报文,接收方不能对它进行确认。只能等发送方在等待3的ACK的回应时间超过2MSL后,进行重发。但是在这里为了让发送方快速知道哪些数据报文丢失了,接收方在收到3时就会给他返回2的ACK,一旦收到3个重复的ACK回应,也就是2的ACK,发送方就会意识到数据包3丢了,就会进行快速重传,重发报文3。 + **快速恢复算法** 发生网络拥塞后,慢启动阀值ssthresh设置为发生拥塞时的窗口cwnd大小的一半后, @@ -346,3 +350,10 @@ Connection: 连接方式。 ![img](../static/640-4554190.jpeg) +##### HTTP状态码502,503,504各自代表着什么? + +502是指网关(一般是Nginx)从后端服务器接受到了无效的响应结果。通常是我们的后端服务器挂了之类的。 + +503是请求过载,就是请求超过了Nginx限流设置的阀值,就会返回503服务不可用。 + +504是指网关(一般是Nginx)从后端服务器接受的响应超时了。 \ No newline at end of file diff --git a/docs/HashMap.md b/docs/HashMap.md index 717fdc8..95ae725 100644 --- a/docs/HashMap.md +++ b/docs/HashMap.md @@ -929,3 +929,81 @@ void afterNodeAccess(Node e) { // move node to last } } ``` + +### JDK1.7的HashMap链表成环是怎么造成的? + +这是JDK1.7里面HashMap扩容相关的代码: + +```java +void transfer(Entry[] newTable, boolean rehash) { + int newCapacity = newTable.length; + for (Entry e : table) {//遍历原数组的每一个下标 + while(null != e) { + //将数组下标下所有元素一个一个迁移到新数组,使用头插法 + Entry next = e.next; + if (rehash) { + e.hash = null == e.key ? 0 : hash(e.key); + } + int i = indexFor(e.hash, newCapacity); + e.next = newTable[i]; + newTable[i] = e; + e = next; + } + } +} +``` + +JDK1.7版本的HashMap中,两个线程同时添加一个新的键值对,然后同时触发扩容时,两个线程都会进行扩容,就会造成前一个线程将某个数组下标的元素迁移过去后,另一个线程又进行迁移。假设原数组下标下有node1->node2->null,这样一个链表,线程A和线程B都在迁移,都拿到了node1,假设线程A先执行,将两个节点迁移到新的数组,假设node1和node2在新数组还是在同一下标下,那么迁移后的链表是node2->node1->null,此时如果线程B还在迁移,拿到node1又迁移,会让node1-next=node,从而让node1和node2形成环。 + +##### 那么JDK1.8版本的HashMap是怎么解决的呢? + +首先迁移时不是拿到一个键值对就迁移一个了,而是对一个数组下标下的链表进行遍历,根据hash值的不同,分成两个链表,然后将两个链表分别设置到新的数组的下标下。 + +```java +if (oldTab != null) { + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { + oldTab[j] = null; + if (e.next == null) + newTab[e.hash & (newCap - 1)] = e; + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + else { // preserve order + Node loHead = null, loTail = null; + Node hiHead = null, hiTail = null; + Node next; + do { + next = e.next; + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + } + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } +``` + + + +https://zhuanlan.zhihu.com/p/111501405 \ No newline at end of file diff --git a/docs/JavaJVM.md b/docs/JavaJVM.md index e2921d8..fbf2eec 100644 --- a/docs/JavaJVM.md +++ b/docs/JavaJVM.md @@ -42,9 +42,9 @@ 堆存储了几乎所有对象实例和数组,是被所有线程进行共享的区域。在逻辑上是连续的,在物理上可以是不连续的内存空间(在存储一些类似于数组的这种大对象时,基于简单和性能考虑会使用连续的内存空间)。 #### 方法区 -存储了被虚拟机加载的类型信息,常量,静态变量等数据,在JDK8以后,存储在元空间中(以前是存储在堆中的永久代中,JDK8以后已经没有永久代了)。 +存储了被虚拟机加载的**类型信息**,**常量**,**静态变量**等数据,在JDK8以后,存储在**方法区的元空间**中(以前是存储在堆中的永久代中,JDK8以后已经没有永久代了)。 -运行时常量池是方法区的一部分,会存储各种字面量和符号引用。具备动态性,运行时也可以添加新的常量入池(例如调用String的intern()方法时,如果常量池没有相应的字符串,会将它添加到常量池)。 +**运行时常量池**是方法区的一部分,会存储各种字面量和符号引用。具备动态性,运行时也可以添加新的常量入池(例如调用String的intern()方法时,如果常量池没有相应的字符串,会将它添加到常量池)。 ### 直接内存区(不属于虚拟机运行时数据区) @@ -125,7 +125,7 @@ ![在这里插入图片描述](../static/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI5NDY4NTcz,size_16,color_FFFFFF,t_70.jpeg) - **Class Pointer**:用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在32位系统占4字节,在64位系统中占8字节。 + **Class Pointer**:用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在开启了指针压缩时,占4字节。(默认是开启的) **Length**:如果是数组对象,还有一个保存数组长度的空间,占4个字节。 @@ -137,6 +137,10 @@ 因为HotSpot虚拟机的自动内存管理系统要求对象起始地址是8字节的整数倍,所以任何对象的大小必须是8字节的整数倍,而对象头部分一般是8字节的倍数,如果实力数据部分不是8字节的整数倍,需要对齐填充来补全。 +### 如何计算一个对象在内存中占多少个字节? + +由于默认是开启了指针压缩的,现在普遍的机器位数都是64位,如果对象是普通对象,非数组类型,那么就是对象头部分就是12字节(类型指针4字节,Mark Word 8字节),假设这个对象只有一个Integer变量,那么在实例数据部分就是一个引用变量的空间4字节,总共是16字节,由于正好是8的倍数,就不需要进行对齐填充了。 + ### 垃圾回收有哪些特点? @@ -584,13 +588,15 @@ Minor GC:对新生代进行回收,不会影响到年老代。因为新生代 Full GC:也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满和System.gc()被显式调用等。 ### 触发Minor GC的条件有哪些? + 1.为新对象分配内存时,新生代的Eden区空间不足。 新生代回收日志: -``` +```java 2020-05-12T16:15:10.736+0800: 7.803: [GC (Allocation Failure) 2020-05-12T16:15:10.736+0800: 7.803: [ParNew: 838912K->22016K(943744K), 0.0982676 secs] 838912K->22016K(1992320K), 0.0983280 secs] [Times: user=0.19 sys=0.01, real=0.10 secs] ``` ### 触发Full GC的条件有哪些? + 主要分为三种: #### 1.system.gc() @@ -1266,3 +1272,27 @@ public class Test018 { ![img](../static/640-20200726210058837.jpeg) +### happens-before指的是什么? +happens-before主要是为保证Java多线程操作时的有序性和可见性,防止了编译器重排序对程序结果的影响。 +因为当一个变量出现一写多读,多读多写的情况时,就是多个线程读这个变量,然后至少一个线程写这个变量,因为编译器在编译时会对指令进行重排序,如果没有happens-before原则对重排序进行一些约束,就有可能造成程序执行不正确的情况。 + +具体有8条规则: +1.**程序次序性规则**:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。一段代码在单线程中执行的结果是有序的。(只在单线程有效,一写多读时,写的变量如果是没有使用volatile修饰时,是没法保证其他线程读到的变量是最新的值) + +2.**锁定规则**:一个锁的解锁操作总是要在加锁操作之前。 + +3.**volatile规则**:如果一个写操作去写一个volatile变量,一个读操作去读volatile变量,那么写操作一定是在读操作之前。 + +4.**传递规则**:操作A happens before 操作B, B happens before C,那么A一定是happens before C的。 + +5.**线程启动规则**:线程A执行过程中修一些共享变量,然后再调用threadB.start()方法来启动线程B,那么A对那些变量的修改对线程B一定是可见的。 + +6.**线程终结规则**:线程A执行过程中调用了threadB.join()方法来等待线程B执行完毕,那么线程B在执行过程中对共享变量的修改,在join()方法返回后,对线程A可见。 + +7.**线程中断规则**:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。 + +8.**对象终结规则**:一个对象的初始化完成先行发生于他的finalize()方法的开始; + +https://www.cnblogs.com/chenssy/p/6393321.html + +http://ifeve.com/java-%e4%bd%bf%e7%94%a8-happen-before-%e8%a7%84%e5%88%99%e5%ae%9e%e7%8e%b0%e5%85%b1%e4%ba%ab%e5%8f%98%e9%87%8f%e7%9a%84%e5%90%8c%e6%ad%a5%e6%93%8d%e4%bd%9c/#more-38824 \ No newline at end of file diff --git a/docs/JavaMultiThread.md b/docs/JavaMultiThread.md index 9ab0478..ba3743f 100644 --- a/docs/JavaMultiThread.md +++ b/docs/JavaMultiThread.md @@ -3,6 +3,7 @@ #### [1.进程与线程的区别是什么?](#进程与线程的区别是什么?) #### [2.进程间如何通信?](#进程间如何通信?) + #### [3.Java中单例有哪些写法?](#Java中单例有哪些写法?) #### [4.Java中创建线程有哪些方式?](#Java中创建线程有哪些方式?) #### [5.如何解决序列化时可以创建出单例对象的问题?](#如何解决序列化时可以创建出单例对象的问题?) @@ -352,7 +353,7 @@ Callable跟Runnable类似,也是一个接口。只不过它的call方法有返 ##### Future -Future也是一个接口,Future就像是一个管理的容器一样,进一步对Runable和Callable的实例进行封装,提供了取消任务的cancel()方法,查询任务是否完成的isDone()方法,获取执行结果的get()方法,带有超时时间来获取执行结果的get()方法。 +Future也是一个接口,Future就像是一个管理的容器一样,进一步对Runable和Callable的实例进行封装,定义了一些方法。取消任务的cancel()方法,查询任务是否完成的isDone()方法,获取执行结果的get()方法,带有超时时间来获取执行结果的get()方法。 ```java public interface Future { @@ -360,13 +361,9 @@ public interface Future { //为true,如果任务已经执行,那么会调用Thread.interrupt()方法设置中断标识 //为false,如果任务已经执行,就只会将任务状态标记为取消,而不会去设置中断标识 boolean cancel(boolean mayInterruptIfRunning); - boolean isCancelled(); - boolean isDone(); - V get() throws InterruptedException, ExecutionException; - V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; } @@ -1013,7 +1010,7 @@ http://cnblogs.com/wenjunwei/p/10573289.html ### 线程间怎么通信? -##### 1.synchronized锁 +#### 1.synchronized锁 通过synchronized锁来进行同步,让一次只能一个线程来执行。 @@ -1387,7 +1384,8 @@ private static class Producer { ```java public class BlockQueueRepository extends AbstractRepository implements Repository { - public BlockQueueRepository() { + public BlockQueueRepository(int cap) { + //cap代表队列的最大容量 products = new LinkedBlockingQueue<>(cap); } @@ -1453,9 +1451,13 @@ public ThreadPoolExecutor(int corePoolSize, 从阻塞队列取任务时,如果阻塞队列为空: -**核心线程**的会一直卡在`workQueue.take`方法,被阻塞并挂起,不会占用CPU资源。 +**核心线程**的会一直卡在`workQueue.take`方法,这个take方法每种等待队列的实现各不相同,以LinkedBlockingQueue为例, + +在这个方法里会调用notEmpty队列(这是Condition实例)的await()方法一直阻塞并挂起,不会占用CPU资源。 + +**非核心线程**会调用workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)方法取任务 ,这个poll方法每种等待队列的实现各不相同,以LinkedBlockingQueue为例, -**非核心线程**会调用workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)方法取任务 ,如果超过keepAliveTime时间后还没有拿到,下一次循环判断**compareAndDecrementWorkerCount**就会返回`null`,Worker对象的`run()`方法循环体的判断为`null`,任务结束,然后线程被系统回收) +在这个方法里面会调用notEmpty队列(这是Condition实例)的awaitNanos()方法进行超时等待,如果超过keepAliveTime时间后还没有新的任务,就会返回`null`,Worker对象的`run()`方法循环体的判断为`null`,任务结束,然后线程被系统回收) ##### 2.maximumPoolSize 最大线程数 @@ -1648,7 +1650,7 @@ ThreadPoolExecutor提供了如下几个public的setter方法 说明有多余的worker线程,此时会向当前idle状态的worker线程发起中断请求以实现回收,多余的worker在下次idel的时候也会被回收。 -**对于新corePoolSize>当前工作线程数的情况:** +**对于新corePoolSize>当前工作线程数且队列中有任务的情况:** 如果当前队列中有待执行任务,则线程池会创建新的worker线程来执行队列任务。 @@ -1820,14 +1822,19 @@ final int internalNextInt(int origin, int bound) { } ``` +### 僵尸进程,孤儿进程,守护进程是什么? +僵尸进程:通常来说,使用fork()系统调用从一个父进程创建出一个子进程,子进程退出,是需要父进程调用wait()或者是waitpid()函数来回收子进程的资源,如果父进程没有调用,子进程的信息就会一直在内存中,而不会被回收,变成僵尸进程。 -### BlockingQueue的原理是怎么样的? +孤儿进程:就是父进程先退出了,它的子进程会被init进程接管,由它来收集子进程的状态。(init进程是内核启动时,创建出来的进程,是一个以root身份运行的普通用户进程,是永远不会停止的。) -https://www.cnblogs.com/tjudzj/p/4454490.html +守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免将在执行的过程中的信息打印在终端上,并且进程也不会被任何终端所产生的终端信息所打断。 +### BlockingQueue的原理是怎么样的? +https://www.cnblogs.com/tjudzj/p/4454490.html ### 进程间通信的方式 -https://network.51cto.com/art/201911/606827.htm?mobile \ No newline at end of file +https://network.51cto.com/art/201911/606827.htm?mobile + diff --git a/docs/Kafka.md b/docs/Kafka.md index 4fa29a5..f3a4529 100644 --- a/docs/Kafka.md +++ b/docs/Kafka.md @@ -82,6 +82,8 @@ - 前一条消息发送失败,后一条消息发送成功,前一条消息重试后成功,造成数据乱序。 + http://www.jasongj.com/kafka/transaction/ + ##### 消费端幂等性 只能自己从业务层面保证重复消费的幂等性,例如引入版本号机制。 @@ -193,8 +195,6 @@ https://blog.csdn.net/choumu8867/article/details/100658332 (一次上下文切换,一次DMA拷贝。) - - ##### 总结: 传统read()和write()方案:数据拷贝了4次,CPU上下文切换了很多次 @@ -210,6 +210,7 @@ log.flush.scheduler.interval.ms 定期刷盘间隔 可以通过设置 最大刷盘消息数量 和 最大刷盘时间间隔 来控制fsync系统调用的时间,但是Kafka不推荐去设置这些参数,希望让操作系统来决定刷盘的时机,这样可以支持更高的吞吐量。而且Kafka保证可用性是通过多副本来实现的,一个机器挂掉了就会选举副本作为leader。 ### Kafka什么时候进行rebalance? 1.topic下分区的数量增加了或者减少了。(这个一般是我们手动触发的) + 2.消费者的数量发生了改变,例如新增加了消费者或者有消费者挂掉了。 Kafka有一个session.timeout.ms,最大会话超时时间,最长是10s。就是如果broker与消费者之间的心跳包超过10s还没有收到回应,就会认为消费者掉线了。以及还有一个max.poll.interval.ms参数,消费者两次去broker拉取消息的间隔,默认是5分钟。如果消费者两次拉取消息的间隔超过了5分钟,就会认为消费者掉线了。 @@ -221,4 +222,32 @@ Kafka有一个session.timeout.ms,最大会话超时时间,最长是10s。就 2.可以自行在MySQL或者Redis里面存储每个分区消费的offset,然后消费者去一个新的分区拉取消息时先去读取上次消费的offset。 -3.为消息分配一个唯一的消息id,通过消息id来判定是否重复消费了。 \ No newline at end of file +3.为消息分配一个唯一的消息id,通过消息id来判定是否重复消费了。 + +##### kafka 1.1的优化 + +新版本新增了**group.initial.rebalance.delay.ms**参数。空消费组接受到成员加入请求时,不立即转化到PreparingRebalance状态来开启reblance。当时间超过**group.initial.rebalance.delay.ms**后,再把group状态改为PreparingRebalance(开启reblance),这样可以避免服务启动时,consumer陆续加入引起的频繁Rebalance。 + +##### Kafka2.3对reblance的优化 + +但对于运行过程中,consumer超时或重启引起的reblance则无法避免,其中一个原因就是,consumer重启后,它的身份标识会变。简单说就是Kafka不确认新加入的consumer是否是之前挂掉的那个。 + +在Kafka2.0中引入了静态成员ID,使得consumer重新加入时,可以保持旧的标识,这样Kafka就知道之前挂掉的consumer又恢复了,从而不需要Reblance。这样做的好处有两个: + +1. 降低了Kafka Reblance的频率 +2. 即使发生Reblance,Kafka尽量让其他consumer保持原有的partition,减少了重分配引来的耗时、幂等等问题 + +https://blog.csdn.net/weixin_37968613/article/details/104607012 + +https://blog.csdn.net/z69183787/article/details/105138782 + +https://zhuanlan.zhihu.com/p/87577979 + +https://www.cnblogs.com/runnerjack/p/12108132.html + +### kafka的选举机制 + +https://blog.csdn.net/qq_37142346/article/details/91349100 + +https://honeypps.com/mq/kafka-basic-knowledge-of-selection/ + diff --git a/docs/LeetCode.md b/docs/LeetCode.md index 323e52e..5b5c42a 100644 --- a/docs/LeetCode.md +++ b/docs/LeetCode.md @@ -4,6 +4,7 @@ ##### [第1题-两数之和](#第1题-两数之和) ##### [第206题-反转链表](#第206题-反转链表) + ##### [第2题-两数相加](#第2题-两数相加) ##### [第3题-无重复字符的最长子串](#第3题-无重复字符的最长子串) ##### [第20题-有效的括号](#第20题-有效的括号) @@ -649,8 +650,15 @@ public ListNode mergeTwoLists(ListNode l1, ListNode l2) { ### 第283题-移动零 +给定一个数组 `nums`,编写一个函数将所有 `0` 移动到数组的末尾,同时保持非零元素的相对顺序。 + 题目详情:https://leetcode-cn.com/problems/move-zeroes/ +``` +输入: [0,1,0,3,12] +输出: [1,3,12,0,0] +``` + 解题思路: 对数组进行遍历,就是找到一个为0的数,然后继续往后找,找到一个不为0的数,与它进行交换,这样就可以把0全部移动到后面去了。 @@ -763,7 +771,9 @@ int findLeftBound(int[] nums,double target) { ##### 解题思路 -我们取两个指针从数组的两端往内遍历,i从数组头部出发,j从数组尾部出发,对于两个端点i和j来说,容纳水的面积是是等于(j-i)*min(height[i],height[j]),假设height[i]是两者之间较小的那一个,那么面积等于(j-i)*height[i],假设i不移动,j向左移动,这样宽度j-i会减少,而height[j]即便变大也不会使得面积变大,因为面积是由宽度乘以两者中较小的高度决定的,所以此时的面积对于i这个端点来说,已经是最大的面积,我们可以右移端点i。 +我们取两个指针从数组的两端往内遍历,i从数组头部出发,j从数组尾部出发。每次计算最大面积,并且移动高度较小的那个端点。 + +对于两个端点i和j来说,容纳水的面积是是等于(j-i)*min(height[i],height[j]),假设height[i]是两者之间较小的那一个,那么面积等于(j-i)*height[i],假设i不移动,j向左移动,这样宽度j-i会减少,而height[j]即便变大也不会使得面积变大,因为面积是由宽度乘以两者中较小的高度决定的,所以此时的面积对于i这个端点来说,已经是最大的面积,我们可以右移高度较小的端点i。 ```java public int maxArea(int[] height) { @@ -1019,7 +1029,6 @@ public int maxDepth(TreeNode root) { public void generateParenthesis(int n,int left,int right,LinkedList stack,List totalList) { //不满足要求,进行剪枝,回退去遍历其他节点 - if(left>n || right>n || right>left) {return;} if(left==n&&right==n) {//正好匹配上了,将栈中所有值转换为 StringBuffer str = new StringBuffer(); @@ -1046,6 +1055,8 @@ PS:回朔算法的框架 ``` List result = new ArrayList<>(); void backtrack(路径, 选择列表stack): + if 超出范围 return + if 满足结束条件: result.add(路径) return @@ -1180,6 +1191,8 @@ f(0)=nums[0]; f(1)=nums[1]>nums[0]?nums[1]:nums[0];//也就是num[0]和nums[1]之间的最大值。 +状态转移方程如下: + f(n) = f(n-1) > f(n-2)+nums[n] ? f(n-1) :f(n-2)+nums[n] ```java @@ -1387,7 +1400,9 @@ public boolean canJump(int[] nums) { 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。 -问总共有多少条不同的路径? +问总共有多少条不同的路径? 右6 下2 C 2 8 + + ![img](../static/robot_maze.png) @@ -1695,7 +1710,6 @@ public ListNode mergeKLists(ListNode[] lists) { } } } - void swap(ArrayList lists,int a, int b) { ListNode temp = lists.get(a); lists.set(a,lists.get(b)); @@ -2106,6 +2120,7 @@ public List> subsets(int[] nums) { for (int i = 0; i < size; i++) { List list = new ArrayList<>(); for (int j = 0; j < nums.length; j++) { + //j是元素在数组中的位置,通过判断i的第j个二进制位是否为0,来决定是否添加这个元素 int result = i & (1<0 ; i--) { - if (nums[i-1] nums[i-1]) { min = j; } } + //进行交换 int temp = nums[i-1]; nums[i-1] = nums[min]; nums[min] = temp; flag=1; + //然后再对后面的元素进行排序 Arrays.sort(nums,i,nums.length); break; } } + //如果现在的数组就是字典序最大的,那么就排序,得到字典序最小的进行返回。 if (flag==0) { Arrays.sort(nums); } @@ -2456,3 +2475,40 @@ dp[i] = max(1,dp[k]+1); ksizeArray[maxSize]) {//如果当前元素比最长子序列的尾部元素大 + maxSize++; + sizeArray[maxSize] = nums[i]; + } else if (nums[i]==sizeArray[maxSize]) {//等于尾部元素,那么不用更新 + continue; + } else {//小于尾部元素进行更新,在sizeArray中进行二分查找 + int left = 0; + int right = maxSize; + while (left < right) { + int mid = (left+right)/2; + if (sizeArray[mid] < nums[i]) { + left = mid+1; + } else { + right = mid; + } + } + //最后left的位置肯定就是需要进行插入的位置 + //left左边的元素都比nums[i]小 + sizeArray[left]=nums[i]; + } + } + return maxSize+1; +} +``` \ No newline at end of file diff --git a/docs/LeetCode1.md b/docs/LeetCode1.md index 933a83a..bb25d1a 100644 --- a/docs/LeetCode1.md +++ b/docs/LeetCode1.md @@ -4,9 +4,12 @@ ##### [第155题-最小栈](#第155题-最小栈) ##### [第160题-相交链表](#第160题-相交链表) + ##### [第142题-环形链表II](#第142题-环形链表II) ##### [第739题-每日温度](#第739题-每日温度) + ##### [第347题-前K个高频元素](#第347题-前K个高频元素) + ##### [第49题-字母异位词分组](#第49题-字母异位词分组) ##### [第32题-最长有效括号](#第32题-最长有效括号) ##### [第543题-二叉树的直径](#第543题-二叉树的直径) @@ -204,7 +207,7 @@ public ListNode detectCycle(ListNode head) { quick = quick.next; quick = quick.next; } - //计算环的长度 + //计算环的长度 int circleLength = 1; ListNode copySlow = slow.next; while (copySlow != slow) { @@ -435,23 +438,21 @@ public int[] topKFrequent(int[] nums, int k) { ##### 解题思路 其实根节点的直径就是左节点深度+右节点深度,其他节点也是这个公示,所以可以递归求解出左右节点的深度,然后计算当前节点的直径,然后与左右节点的直径进行判断,返回最大值。 ```java - public int diameterOfBinaryTree(TreeNode root) { - if (root==null){return 0;} - int leftDepth = maxDepth(root.left); - int rightDepth = maxDepth(root.right); - int rootDiameter = leftDepth+rightDepth+1; - int leftDiameter = diameterOfBinaryTree(root.left); - int rightDiameter = diameterOfBinaryTree(root.right); - int maxDiameter = rootDiameter > leftDiameter ? rootDiameter : leftDiameter; - maxDiameter = maxDiameter > rightDiameter ? maxDiameter:rightDiameter; - return maxDiameter; - } - public int maxDepth(TreeNode root) { - if (root==null) {return 0;} - int leftDepth = maxDepth(root.left); - int rightDepth = maxDepth(root.right); - return leftDepth>rightDepth?leftDepth+1 : rightDepth+1; - } +int maxDiameter = 0; +public int diameterOfBinaryTree(TreeNode root) { + if (root==null){return 0;} + maxDepth(root); + return maxDiameter; +} + +public int maxDepth(TreeNode root) { + if (root==null) {return 0;} + int leftDepth = maxDepth(root.left); + int rightDepth = maxDepth(root.right); + int diameter = leftDepth + rightDepth; + maxDiameter = diameter > maxDiameter ? diameter : maxDiameter; + return leftDepth>rightDepth?leftDepth+1 : rightDepth+1; +} ``` ### 第79题-单词搜索 @@ -459,7 +460,7 @@ public int[] topKFrequent(int[] nums, int k) { 单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。 -  + 示例: @@ -574,7 +575,31 @@ public int numTrees(int n) { } ``` +递归解法 + +```java +int[] cacheArray; +public int numTrees(int n) { + if(n == 0 || n == 1){return 1;} + if(n==2) { + return 2; + } + if(cacheArray == null) { + cacheArray = new int[n+1]; + } else if (cacheArray[n] !=0) { + return cacheArray[n]; + } + int sum = 0; + for(int i = 0; i map = new LinkedHashMap(); - int capacity; +int capacity; - public LRUCache(int capacity) { - this.capacity = capacity; - } +public LRUCache(int capacity) { + this.capacity = capacity; +} - public int get(int key) { - Integer value = map.get(key); - if (value == null) { - return -1; - } - map.remove(key); - map.put(key,value); - return value; - } - - public void put(int key, int value) { - Integer oldValue = map.get(key); - if (oldValue!=null) { - //只是覆盖value的话,put方法不会改变键值对在链表中的顺序,所以需要先remove - map.remove(key); - map.put(key,value); - return; - } - if (map.size()>=capacity) { - map.remove(map.keySet().iterator().next()); - } - map.put(key,value); - } +public int get(int key) { + Integer value = map.get(key); + if (value == null) { + return -1; + } + map.remove(key); + map.put(key,value); + return value; +} + +public void put(int key, int value) { + Integer oldValue = map.get(key); + if (oldValue!=null) { + //只是覆盖value的话,put方法不会改变键值对在链表中的顺序,所以需要先remove + map.remove(key); + map.put(key,value); + return; + } + if (map.size()>=capacity) { + map.remove(map.keySet().iterator().next()); + } + map.put(key,value); +} ``` ### 第236题-二叉树的最近公共祖先 @@ -693,12 +718,13 @@ LinkedHashMap map = new LinkedHashMap(); 3.节点1,节点2全部位于此节点的左子树,或者是右子树 4.此节点左子树包含节点1,右子树包含节点2 所以第4种就是我们要寻找的节点,并且在二叉树中只有一个,所以我们对二叉树进行遍历,判断某个节点左子树,右子树都包含节点,那么就返回该节点。 + ```java TreeNode lowestCommonAncestor(TreeNode root, TreeNode node1, TreeNode node2) { if (root==null) { return null; } - if (root==node1 || root==node2) {//当前节点就是要找的节点之一 + if (root==node1 || root==node2) {//当前节点就是要找的节点之一 return root; } TreeNode leftNode = lowestCommonAncestor(root.left,node1,node2);//判断左子树中是否有节点 @@ -793,11 +819,19 @@ TreeNode lowestCommonAncestor(TreeNode root, TreeNode node1, TreeNode node2) { 矩形面积=height[i]*(right-left+1),例如对于第五个柱子值为2的那个柱子来说,左边界left就是值为5的柱子,右边界就是值为3的柱子。 -也可以使用一个最小栈Stack来保存过程中比height[i]小的元素,也就是Stack中的值只能是比height[i]小的元素。 +单调栈解法 -遍历时,如果现在的height[i]比栈顶的元素height[j]小,那么就进行出栈,并且对出栈的元素计算矩形面积,因为出栈的元素的left边界就是新栈顶的index+1,right边界就是现在的i-1。 +理论就是假设f(i)代表包含圆柱i的最大矩形,那么其实就是在圆柱i的左边和右边各自找到第一个高度更小的圆柱k和j,f(i) = height[i]*(j-k+1)。所以可以使用单调栈来保存比当前圆柱高度height[i]小的元素,如果栈中元素高度比height[i]大,那么那些元素就需要出栈了。 - 面积=heights[j] * (right - left+1) + + +可以使用一个栈Stack来保存左边圆柱高度比height[i]小的元素,也就是Stack中的值只能是比height[i]小的元素。栈底元素是最小的,栈顶元素高度是最大的。 + +遍历时,如果当前height[i]>栈顶元素的高度,说明当前栈顶元素还没有碰到比它小的数,所以还不能计算面积,就把height[i]入栈。 + +如果现在的height[i]<栈顶的元素高度小,说明栈顶元素碰到比它小的元素了,就需要出栈计算矩形面积, + + 面积=heights[j] * (i - left) ```java public int largestRectangleArea(int[] heights) { @@ -827,13 +861,12 @@ public int largestRectangleArea(int[] heights) { Integer index = stack.pop(); //此时area的范围是[stack.peek()+1,i-1] int leftIndex; - int rightIndex = i-1; if (stack.size()>0) { leftIndex = stack.peek() + 1; } else { leftIndex = 0; } - int area = heights[index] * (rightIndex - leftIndex+1); + int area = heights[index] * (i - leftIndex); maxArea = maxArea > area ? maxArea : area; } stack.push(i); @@ -1034,7 +1067,6 @@ public int maxProduct(int[] nums) { if (nums[i] < 0 && firstNegativeIndex == null) { firstNegativeIndex = i; } - if (nums[i] < 0) { lastNegativeIndex = i; } @@ -1325,7 +1357,7 @@ public String minWindow(String s, String t) { 两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。 给出两个整数 x 和 y,计算它们之间的汉明距离。 注意: -0 ≤ x, y < 231. +0 ≤ x, y < 2的31次方. 示例: @@ -1346,13 +1378,13 @@ public int hammingDistance(int x, int y) { int result = x^y; int num=0; while(result>0) { - if((result&1) == 1) { - num++; - } - result = result>>1; + if((result&1) == 1) { + num++; + } + result = result>>1; } return num; - } +} ``` ### 第128题-最长连续序列 @@ -1549,10 +1581,15 @@ public int longestConsecutive(int[] nums) { 其实就是判断有向图是否存在环,有两种解法 ##### 深度优先遍历 就是先根据二维数组构建一个邻接表,这里我们使用一个map来作为领接表,然后递归遍历map中的节点,对于图中每个节点有三种状态: -1.未被访问过(在statusMap中值为null) -2.已被访问过,并且它的子节点没有遍历访问完成(在statusMap中值为1) -3.已被访问过,并且子节点也遍历访问完成(在statusMap中值为2) + +1.未被访问过(在statusMap中值为null)。 + +2.已被访问过,并且它的子节点没有遍历访问完成(在statusMap中值为1)。 + +3.已被访问过,并且子节点也遍历访问完成(在statusMap中值为2)。 + 在递归遍历过程中,遇到上面第2种节点,说明就存在环。 + ```java public boolean canFinish(int numCourses, int[][] prerequisites) { HashMap> map = new HashMap<>(); @@ -1610,6 +1647,21 @@ public boolean canFinish(int numCourses, int[][] prerequisites) { 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。 + +``` java +dp[i][0]//代表不持有股票 +dp[i][1]//代表持有股票 + + //如果今天不持有股票,要么是之前没有股票,要么是卖了股票 + dp[i][0] = max(dp[i-1][0],dp[i-1][1] + value[i]) + //如果今天持有股票,要么是之前就持有,要么是今天新买的 + dp[i][1] = max(dp[i-1][1],dp[i-2][0] - value[i]) + + +``` + + + 示例: 输入: [1,2,3,0,2] @@ -1617,10 +1669,19 @@ public boolean canFinish(int numCourses, int[][] prerequisites) { 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出] ##### 解题思路 + 这个跟上一题的区别就是有冷冻期,就是当你第i天要持有股票时,要么是第i-1天已持有股票,要么是第i-1天没有买卖股票才能在第天买股票。 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。 -第i天不持有股票 dp[i][0] = max(dp[i-1][1]+prices[i], dp[i-1][0]) -第i天持有股票 dp[i][1] = max(dp[i-2][0]-prices[i],dp[i-1][1]) + +```java + //如果今天不持有股票,要么是之前没有股票,要么是第i天卖了股票 + dp[i][0] = max(dp[i-1][0],dp[i-1][1] + value[i]) + //如果今天持有股票,要么是之前就持有,要么是第i天新买的 + dp[i][1] = max(dp[i-1][1],dp[i-2][0] - value[i]) +``` + +代码 + ```java public int maxProfit(int[] prices) { if(prices==null||prices.length<=1) {return 0;} @@ -1680,7 +1741,6 @@ public boolean canPartition(int[] nums) { return canPartition(nums,nums.length-1,sum/2); } } - //使用HashMap缓存结果,避免重复计算 HashMap resultCacheMap = new HashMap(); //判断在0到i-1这些元素中进行能选择,看能否选择出的元素和为sum @@ -1795,13 +1855,13 @@ public List findDisappearedNumbers(int[] nums) { root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8 - 10 - / \ - 5 -3 - / \ \ - 3 2 11 - / \ \ -3 -2 1 + 10 + / \ + 5 -3 + / \ \ + 3 2 11 + / \ \ +3 -2 1 返回 3。和等于 8 的路径有: @@ -1819,8 +1879,8 @@ int pathNum = 0; //必须包含根节点的 pathSumMustHasRoot(root,sum,0); //不包含根节点的 - pathSum(root.left,sum); - pathSum(root.right,sum); + pathSumMustHasRoot(root.left,root.val); + pathSumMustHasRoot(root.right,root.val); return pathNum; } //对每个节点计算路径和,然后继续向下 @@ -1947,9 +2007,10 @@ public int[] countBits(int num) { 你可以将以下二叉树: - 1 - / \ - 2 3 +​ 1 + + / \ + 2 3 / \ 4 5 @@ -2080,10 +2141,10 @@ public List findAnagrams(String s, String p) { if (valid_num==p.length()) {//说明满足需求 list.add(left); } - //窗口左移 + //窗口左边界移动一个字符 Character leftChar = s.charAt(left); if (needMap.containsKey(leftChar)) { - //移除左边界字符 + //移除一个左边界字符 Integer times = windowsMap.get(leftChar); times--; windowsMap.put(leftChar,times); @@ -2156,7 +2217,7 @@ public boolean searchMatrix(int[][] matrix, int target) { 给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。 返回可以使最终数组和为目标数 S 的所有添加符号的方法数。 -示例: +示例: 输入:nums: [1, 1, 1, 1, 1], S: 3 输出:5 @@ -2281,7 +2342,7 @@ public int findUnsortedSubarray(int[] nums) { if (nums==null||nums.length==0) { return 0; } - //从左开始遍历,找到需要调整的最右边的位置的下标 + //从左往右开始遍历,记录最大值,找到需要调整的最右边的位置的下标 Integer right = null; int maxIndex = 0; for (int i = 0; i < nums.length; i++) { @@ -2290,7 +2351,7 @@ public int findUnsortedSubarray(int[] nums) { } maxIndex = nums[i] > nums[maxIndex] ? i : maxIndex; } - //从右开始遍历,找到需要调整的最右边的位置的下标 + //从右往左开始遍历,记录最小值,找到需要调整的最右边的位置的下标 Integer left = null; int minIndex = nums.length-1; for (int i = nums.length-1; i >=0; i--) { diff --git a/docs/Lock.md b/docs/Lock.md index 97a5cfb..1f1fb21 100644 --- a/docs/Lock.md +++ b/docs/Lock.md @@ -202,7 +202,7 @@ JDK采用了适应性自旋,简单来说就是线程如果自旋成功了, ### AbstractQueuedSynchronizer(缩写为AQS)是什么? AQS就是AbstractQueuedSynchronizer,抽象队列同步器,是一个可以用于实现基于先进先出等待队列的锁和同步器的框架。实现锁 -ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。 +ReentrantLock,CountDownLatch,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。 ReentrantLock其实就是有一个变量sync,Sync父类是AbstractQueuedSynchronizer @@ -222,34 +222,38 @@ ReentrantLock的非公平锁与公平锁的区别在于非公平锁在CAS更新s ### synchronized锁与ReentrantLock锁的区别? -相同点: +**相同点:** 1.可重入性 两个锁都是可重入的,持有锁的线程再次申请锁时,会对锁的计数器+1。 -不同点: +**不同点:** -1.具体实现 +**1.实现原理** -synchronized是一个Java 关键字,synchronized锁是JVM实现的,底层代码应该是C++代码,而ReenTrantLock是JDK实现的,是Java提供的一个类库,代码是Java代码,源码实现更加方便阅读。 +synchronized是一个Java 关键字,是由JVM实现的,底层代码应该是C++代码。而ReentrantLock是JDK实现的,是Java提供的一个类库,代码是Java代码,源码实现更加方便阅读。 -2.性能 +**2.性能** -在以前,synchronized锁的实现只有重量级锁一种模式,性能会比较差,后面引入了偏向锁和轻量级锁后就优化了很多。 +在以前,synchronized锁的实现只有重量级锁一种模式,性能会比较差,后面引入了偏向锁和轻量级锁后就优化了很多。根据测试结果,在线程竞争不激烈的情况下,ReentrantLock与synchronized锁持平,竞争比较激烈的情况下,ReentrantLock会效率更高一些。 -3.功能 +**3.功能** -synchronized只能修饰方法,或者用于代码块,而ReenTrantLock的加锁和解锁是调用lock和unlock方法,更加灵活。 +synchronized只能修饰方法,或者用于代码块,而ReentrantLock的加锁和解锁是调用lock和unlock方法,更加灵活。 -其次是synchronized的等待队列只有一个(调用wait()方法的线程会进入等待队列),而ReenTrantLock可以有多个条件等待队列。可以分组唤醒需要唤醒的线程们,而不是像synchronized要么用notify方法随机唤醒一个线程要么用notifyAll方法唤醒全部线程。ReenTrantLock 提供了一种能够中断等待锁的线程的机制,就是线程通过调用lock.lockInterruptibly()方法来加锁时,一旦线程被中断,就会停止等待。 +其次是synchronized的等待队列只有一个(调用wait()方法的线程会进入等待队列),而ReentrantLock可以有多个条件等待队列。可以分组唤醒需要唤醒的线程们,而不是像synchronized要么用notify方法随机唤醒一个线程要么用notifyAll方法唤醒全部线程。ReentrantLock 提供了一种能够中断等待锁的线程的机制,就是线程通过调用lock.lockInterruptibly()方法来加锁时,一旦线程被中断,就会停止等待。 -ReenTrantLock可以使用tryLock(long timeout, TimeUnit unit)方法来尝试申请锁,设置一个超时时间,超过超时时间,就会直接返回false,而不是一直等待锁。 +ReentrantLock可以使用tryLock(long timeout, TimeUnit unit)方法来尝试申请锁,设置一个超时时间,超过超时时间,就会直接返回false,而不是一直等待锁。 -4.公平性 +ReentrantLock可以响应中断,而synchronized锁不行 + +**4.公平性** synchronized锁是非公平锁,ReentrantLock有公平锁和非公平锁两种模式。 +https://www.codercto.com/a/22884.html + ### ReentrantLock的加锁流程是怎么样的? ReentrantLock非公平锁的加锁流程: @@ -276,9 +280,15 @@ ReentrantLock非公平锁的加锁流程: https://blog.csdn.net/qq_14996421/article/details/102967314 -##### 谈一谈你对AQS的理解? +https://blog.csdn.net/fuyuwei2015/article/details/83719444 + +### 谈一谈你对AQS的理解? + +AQS是AbstractQueuedSynchronizer的缩写,是一个抽象同步队列类,可以基于它实现一个锁,例如ReentrantLock,只是需要实现tryAcquire()方法(也就是获取资源的方法,判断当前线程能否申请到独占锁)和tryRelease()方法(也就是释放资源的方法,在线程释放锁前对state进行更新),AQS会根据tryAcquire()的返回结果,来进行下一步的操作, + +如果为true,代表线程获得锁了。 -AQS是AbstractQueuedSynchronizer的缩写,是一个抽象同步队列类,可以基于它实现一个锁,例如ReentrantLock,只是需要实现tryAcquire()方法(也就是获取资源的方法,判断当前线程能否申请到独占锁)和tryRelease()方法(也就是释放资源的方法,在线程释放锁前对state进行更新),AQS会根据tryAcquire()的返回结果,来进行下一步的操作,如果为true,代表线程获得锁了,如果为false,代表线程没有获得锁,由AQS负责将线程添加到CLH等待队列中,并且进行阻塞等待。当前一个线程释放锁时,AQS对这个线程进行唤醒。 +如果为false,代表线程没有获得锁,由AQS负责将线程添加到CLH等待队列中,并且进行阻塞等待。当前一个线程释放锁时,AQS对这个线程进行唤醒。 (不同的自定义同步器争用共享资源的方式也不同。**自定义同步器在实现时只需要实现资源state的获取与释放方法即可**,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了) @@ -305,7 +315,7 @@ AbstractQueuedSynchronizer.acquireQueued()//并且让这个线程进入阻塞等 -可以看到AQS的acquire()方法中是会先去调用tryAcquire()去尝试着申请独占锁资源,AQS默认的tryAcquire9)方法只有一行代码,会抛出UnsupportedOperationException异常,所以ReentrantLock的FairSync对tryAcquire()方法进行了实现。 +可以看到AQS的acquire()方法中是会先去调用tryAcquire()去尝试着申请独占锁资源,AQS默认的tryAcquire()方法只有一行代码,会抛出UnsupportedOperationException异常(强制子类对这个方法进行实现)。所以ReentrantLock的FairSync对tryAcquire()方法进行了实现。 tryAcquire()返回true就代表获取独占锁资源成功: @@ -318,16 +328,19 @@ tryAcquire()返回false代表获取独占锁资源失败, 那么就调用AQS.addWaiter()方法申请失败就将线程添加到等待队列尾部,AQS.acquireQueued()方法让这个线程进入阻塞等待状态(在阻塞之前如果等待队列只有这一个线程,是会先尝试着获取锁,失败才会进入阻塞状态。) ```java +//公平锁的tryAcquire方法实现 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { + //等待队列中没有线程,使用cas操作去抢锁,抢锁成功,就返回true if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } + //当前线程与持有锁的线程是同一个,那么进行重入 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) @@ -363,6 +376,7 @@ protected final boolean tryRelease(int releases) { if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; + //state为0就释放锁,否则只是锁的state减去releases if (c == 0) { free = true; setExclusiveOwnerThread(null); diff --git a/docs/MySQLNote.md b/docs/MySQLNote.md index 906aa0a..04c8b6b 100644 --- a/docs/MySQLNote.md +++ b/docs/MySQLNote.md @@ -79,13 +79,15 @@ innodb 引擎首先开启事务,获得一个事务ID(是一直递增的),根 因为需要保证更新后的唯一性,所以不能延迟更新,必须把数据页从磁盘加载到内存,然后判断更新后是否会数据冲突,不会的话就更新数据页。 -#### 5.写undo log(prepare状态) +#### 5.写redo log(prepare状态) -将对数据页的更改写入到redo log,将redo log设置为prepare状态。 +将对数据页的更改写入到redo log,此时redo log中这条事务的状态为prepare状态。 -#### 6.写bin log(commit状态) +#### 6.写bin log(同时将redo log设置为commit状态) -通知MySQL server已经更新操作写入到redo log 了,随时可以提交,将执行的SQL写入到bin log日志,将redo log改成commit状态,事务提交成功。 +通知MySQL server已经更新操作写入到redo log 了,随时可以提交,将执行的SQL写入到bin log日志,将redo log 中这条事务的状态改成commit状态,事务提交成功。 + +https://www.cnblogs.com/yuyue2014/p/6121114.html ##### undo log @@ -400,7 +402,7 @@ innodb_autoinc_lock_mode为2 2.第二步只需要判断当前锁是否加了表级别的意向排斥锁,因为如果加了意向排斥锁,说明正在有事务在对数据加行锁,对数据进行更新,这样避免了对每一行数据进行判断,判断是否加了行锁。 -####Innodb的锁 +#### Innodb的锁 ##### 行锁 @@ -798,7 +800,7 @@ SELECT a,b,c FROM user where a = 1 假如要取offset 为X,limitY的数据, -1.假设有四个库,需要去每个库查offset为X,limit为Y的数据,然后得到四个结果集,每个结果集的time都有一个时间最小的time_min,时间最大的time_max结果,取四个结果集中最小的time_min +1.假设有四个库,需要去每个库查offset为X/4,limit为Y的数据,然后得到四个结果集,每个结果集的time都有一个时间最小的time_min,时间最大的time_max结果,取四个结果集中最小的time_min 2.然后去每个库去查between time_min到之前每个库的time_max的结果集, @@ -1027,27 +1029,19 @@ table | type | possible_keys | key |key_len | ref | rows | Extra EXPLAIN列 const就是针对主键或者唯一性索引的等值查询,通过索引查找一次就行了。仅仅是查一条数据。 - eq_ref 唯一性索引键扫描,对于每个索引键,表中只有一条数据与其对应,例如join查询时,对于前表查出的结果集,每个结果在第二个表只有一条数据对应。 - - ref 非唯一性索引查询 此类型通常出现在多表的 join 查询, 针对于非唯一或非主键索引, 或者是使用了 `最左前缀` 规则索引的查询. 关键字:非唯一性索引 - - ref_or_null:与ref方法类似,只是增加了null值的比较。 - - range: 表示使用索引范围查询, 通过索引字段范围获取表中部分数据记录. 这个类型通常出现在 =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, IN() 操作中. - - index: 表示全索引扫描(full index scan), 关键字:查询字段和条件都是索引本身 + ref_eq 唯一性索引查询,一般使用一个表去join另外一个表的唯一性索引字段时,就是ref_eq - index_merge:表示查询使用了两个以上的索引,最后取交集或者并集,常见and ,or的条件使用了不同的索引.效率不是很高 关键字:索引合并 + ref 非唯一性索引查询,就是根据普通索引去查找数据,例如查询条件是where a = 1,a是普通索引,但是不是唯一性索引 - unique_subquery:用于where中的in形式子查询,子查询返回不重复值唯一值 + range: 表示有范围条件的全索引范围查询,跟index全索引扫描相比就是有查询条件,可以减少一些扫描的数据量。同时除了显而易见的between,and以及'>','<'外,in和or也是索引范围扫描。 - index_subquery:用于in形式子查询使用到了辅助索引或者in常数列表,子查询可能返回重复值,可以使用索引将子查询去重。 - - fulltext:全文索引检索,要注意,全文索引的优先级很高,若全文索引和普通索引同时存在时,mysql不管代价,优先选择使用全文索引 + index: 表示全索引扫描(full index scan), 这是另外一种全表扫描,例如order by 字段是索引字段,如果是直接去聚集索引下全表扫描,那么查询出来的结果集还需要在内存中排序一边,如果是去非聚集索引下进行全表扫描,然后按照扫描顺序进行回表,回表的顺序就是order by的顺序,可以减少排序的时间,但是会有回表的开销。 all:这个就是全表扫描数据文件,然后再在server层进行过滤返回符合要求的记录。 ``` +https://blog.csdn.net/dennis211/article/details/78170079 + 主要分为以下几个方法: **1.减少请求的数据量** @@ -1072,6 +1066,43 @@ table | type | possible_keys | key |key_len | ref | rows | Extra EXPLAIN列 因为大查询在查询时可能会锁住很多数据,也需要获取到这些数据的行锁才能进行查询,切分成小查询可以减少锁竞争,减少等待获取锁的时间。 +##### 1.使用show profile对一条SQL查询分析当前会话中语句执行的资源消耗情况 + +1.profiling配置默认是不开启的,可以使用set profiling = ON;命令将配置暂时打开。 + +2.执行一条查询SQL + +3.使用show profiles可以查看最近15条查询SQL及对应的查询idquery id + +4.假设查询id为9,使用这个命令show profile for query 9;可以查看每个步骤及其消耗的时间。 + +``` +mysql> show PROFILE for QUERY 9; ++----------------------+----------+ +| Status | Duration | ++----------------------+----------+ +| starting | 0.000054 | +| checking permissions | 0.000007 | +| Opening tables | 0.000116 | +| init | 0.000019 | +| System lock | 0.000009 | +| optimizing | 0.000004 | +| statistics | 0.000011 | +| preparing | 0.000010 | +| executing | 0.000002 | +| Sending data | 0.000061 | +| end | 0.000005 | +| query end | 0.000006 | +| closing tables | 0.000006 | +| freeing items | 0.000031 | +| cleaning up | 0.000010 | ++----------------------+----------+ +``` + +https://www.cnblogs.com/116970u/p/11004431.html + +https://www.jianshu.com/p/1efdddf3d461 + ### char类型与varchar类型的区别? char类型 diff --git a/docs/Nginx.md b/docs/Nginx.md index 6caa9c5..31c2a19 100644 --- a/docs/Nginx.md +++ b/docs/Nginx.md @@ -375,4 +375,7 @@ Demo3.抢购场景降级 3)(integer)14 # 漏斗剩余容量 4)(integer)-1 # 被拒绝之后,多长时间之后再试(单位:秒)-1 表示无需重试 5)(integer)2 # 多久之后漏斗完全空出来 -``` \ No newline at end of file +``` + + + diff --git a/docs/RedisBasic.md b/docs/RedisBasic.md index fa9536d..e5634f2 100644 --- a/docs/RedisBasic.md +++ b/docs/RedisBasic.md @@ -31,7 +31,7 @@ Redis是一个开源的,基于内存的,也可进行持久化的,使用C ##### (2)定时清理 -Redis配置项hz定义了serverCron任务的执行周期,默认每次清理时间为25ms,每次清理会依次遍历所有DB,从db随机取出20个key,如果过期就删除,如果其中有5个key过期,说明过期率超过了25%,那么就继续对这个db进行清理,否则开始清理下一个db。 +Redis配置项hz定义了serverCron任务的执行周期,默认每次清理时间为25ms,每次清理会依次遍历所有DB,从db的expires字典(里面保存了设置了过期时间的键值对,key就是指向键对象,value是过期时间)中随机取出20个key,如果过期就删除,如果其中有5个key过期,说明过期率超过了25%,那么就继续对这个db进行清理,否则开始清理下一个db。 ##### (3)内存不够时清理 @@ -247,6 +247,17 @@ http://blog.csdn.net/lzb348110175/article/details/98941378 https://xie.infoq.cn/article/b3816e9fe3ac77684b4f29348 +### epoll水平触发和边缘触发的区别? + +**水平触发**和**边缘触发**两种: + +- LT,默认的模式(水平触发) 只要该fd还有数据可读,每次 `epoll_wait` 都会返回它的事件,提醒用户程序去操作, +- ET是“高速”模式(边缘触发) + + 只会提示一次,直到下次再有数据流入之前都不会再提示,无论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读完,即读到read返回值小于请求值或遇到EAGAIN错误 + +epoll使用“事件”的就绪通知方式,通过`epoll_ctl`注册fd,一旦该fd就绪,内核就会采用类似回调机制激活该fd,`epoll_wait`便可收到通知。 + ### 同步与异步的区别是什么? 同步与异步的区别在于调用结果的通知方式上。 同步执行一个方法后,需要等待结果返回轮询调用结果才能继续执行,然后继续执行下去。 @@ -261,6 +272,32 @@ https://xie.infoq.cn/article/b3816e9fe3ac77684b4f29348 在消息发出去后,不会阻塞当前进/线程,而会立即返回,可以去执行其他任务。 +### BIO,NIO,AIO有什么区别? + +一次IO的读操作分为等待就绪和IO操作两个阶段,等待就绪就是等待TCP RecvBuffer里面的数据就绪好,也就是发送方的数据全部发送到网卡里面来,操作就是CPU将数据从网卡拷贝到用户空间。 + +**BIO(同步阻塞型)** + +以Java中的IO方式为例,BIO就是同步阻塞型的IO,当一个Socket连接发送数据过来以后,需要为这个Socket连接创建一个线程,由线程调用Socket的read()方法读取数据,需要先把数据从网卡拷贝到内核空间,再拷贝到用户态的内存空间,在数据读取完成前,线程都是阻塞的。这种IO方式就是比较消耗资源,假设有1000个活跃的Socket连接,需要创建出1000个线程来读取数据,读取时都是阻塞的,每个线程有自己独有的线程栈,默认大小是1M。 + +**NIO(同步非阻塞型)** + +nio就是多路io复用,就是一个线程来处理多个Socket连接,节省线程资源。以select为例,就是有一个长度为1024的数组,每个元素对应一个Socket连接,线程轮询这个数据,判断哪个Socket连接的数据是出于就绪状态,此时就将数据拷贝到用户空间然后进行处理。由于IO操作阶段是需要等待数据拷贝到用户空间完成才能返回,所以是同步的。由于每次判断内核中Socket缓冲区的数据是否就绪的函数是直接返回的,如果就绪就返回有数据,不是就绪就返回0,线程不需要阻塞等待,所以是非阻塞的。 + +**AIO(异步非阻塞型)** + +AIO就是NIO的升级版,在数据处于就绪状态时,也是异步将Socket缓冲区中的数据拷贝到用户空间,然后执行异步回调函数,所以在IO操作阶段也是异步的。 + + + +BIO里用户最关心“我要读”,NIO里用户最关心"我什么时候可以读了",在AIO模型里用户更需要关注的是“什么时候读完了,我可以直接进行处理了”。 + +https://zhuanlan.zhihu.com/p/23488863 + +https://www.cnblogs.com/sxkgeek/p/9488703.html + +https://blog.csdn.net/weixin_34378045/article/details/91930797 + ### 如何解决Redis缓存穿透问题? Redis 缓存穿透指的是攻击者故意大量请求一些Redis缓存中不存在key的数据,导致请 求打到数据库上,导致数据库压力过大。 diff --git a/docs/RedisDataStruct.md b/docs/RedisDataStruct.md index dc0f8f2..5cd40d6 100644 --- a/docs/RedisDataStruct.md +++ b/docs/RedisDataStruct.md @@ -23,6 +23,10 @@ Redis中的简单动态字符串其实是对C语言中的字符串的封装和 2.频繁修改一个字符串时,会涉及到内存的重分配,比较消耗性能。(Redis中的简单动态字符串会有内存预分配和惰性空间释放)。 +**如果字符串实际使用长度len<1M**,实际分配空间=len长度来存储字符串+1字节存末尾空字符+len长度的预分配空闲内存 + +**如果字符串实际使用长度len>1M**,实际分配空间=len长度来存储字符串+1字节存末尾空字符+1M长度的预分配空闲内存 + 所以Redis中的简单动态字符串结构,除了包含一个字符数组的属性,还包含数组的长度,数组的实际使用长度等属性,通过增加长度属性,可以保证字符串是二进制安全的,从而可以保存任意类型的数据,例如一张图片,对象序列化后的数据等等。 ##### 字符串使用场景如下: diff --git a/docs/RedisUserful.md b/docs/RedisUserful.md index c4caea9..2695ea5 100644 --- a/docs/RedisUserful.md +++ b/docs/RedisUserful.md @@ -104,7 +104,7 @@ Redis中的哨兵服务器是一个运行在哨兵模式下的Redis服务器, ![图片](../static/640-20210124192752928) -##### Redis Cluster的节点扩容和下线 +#### Redis Cluster的节点扩容和下线 ##### 扩容 @@ -147,7 +147,7 @@ https://www.cnblogs.com/youngchaolin/archive/2004/01/13/12034660.html 由于 Redis Cluster 的节点不断地与集群内的节点进行通讯,下线信息也会通过 Gossip 消息传遍所有节点。 -因此集群内的节点会不断收到下线报告,当半数以上持有槽的主节点标记了某个节点是主观下线时,便会任务节点2**客观下线**,执行后面的流程。 +因此集群内的节点会不断收到下线报告,当半数以上持有槽的主节点标记了某个节点是主观下线时,便会认为节点2**客观下线**,执行后面的流程。 ##### 3.资格检查 @@ -171,11 +171,13 @@ https://www.cnblogs.com/youngchaolin/archive/2004/01/13/12034660.html 说白了更新这个值目的是,保证所有主节点对这件“大事”保持一致。大家都统一成一个配置纪元(一个整数),表示大家都知道这个“大事”了。 -更新完配置纪元以后,每个从节点会向集群内发起广播选举的消息(FAILOVER_AUTH_REQUEST)。并且保证每个从节点在一次配置纪元中只能发起一次选举。 +更新完配置纪元以后,每个从节点会向集群内发起广播选举的消息。 ##### 6.主节点为选举投票 -参与投票的只有主节点,从节点没有投票权,超过半数的主节点通过某一个节点成为新的主节点时投票完成。 +参与投票的只有主节点,从节点没有投票权。每个主节点在收到从节点请求投票的信息后,如果它还没有为其他从节点投票,那么就会把票投给从节点。(也就是主节点的票只会投给第一个请求它选票的从节点。) + +超过半数的主节点通过某一个节点成为新的主节点时投票完成。 如果在 cluster-node-timeout*2 的时间内从节点没有获得足够数量的票数,本次选举作废,进行第二轮选举。 diff --git a/docs/Rule.md b/docs/Rule.md new file mode 100644 index 0000000..0d5e10a --- /dev/null +++ b/docs/Rule.md @@ -0,0 +1,78 @@ +##### 1.使用BigDecimal替代Float和Double + +主要Float和Double是使用类似于科学计数法那样"有效数字+指数"来表示的,所以在二进制存储时,是会丢失精度,没法做到精准的。所以浮点数之间不能使用==等值判断,浮点数包装类型之间不能使用equals + +```java +float a = 1.0f - 0.9f; +float b = 0.9f - 0.8f; //通过debug调试发现这两次减运算减下来,a和b的值存在一些差异,不是一模一样的,丢失了精度 +Boolean result = a == b;//结果是false + +Float x = 1.0F - 0.9F; +Float y = 0.9F - 0.8F; +Boolean result = x.equals(y);//结果是false +``` + +解决方案: + +1. 任何货币金额,均以最小货币单位且整型类型来进行存储。例如在数据库里面存10000,代表是100.00元。 + +2. 使用Bigdecimal来存这些值,并且进行加减。 + +```java +BigDecimal a1 = new BigDecimal("1.0"); +BigDecimal b1 = new BigDecimal("0.9"); +BigDecimal c1 = new BigDecimal("0.8"); + +BigDecimal a2 = a1.subtract(b1); +BigDecimal b2 = b1.subtract(c1); + +System.out.println(a2.equals(b2));//打印结果是true +``` + + + +2.布尔类型的变量,都不要加is前缀,否则部分框架解析会引起序列化错误。 + +一般假设我们定义一个Boolean变量为isHot,按照Bean规范生成的get方法应该是isIsHot,但是我们使用IDEA来自动生成get方法时,生成的是isHot()方法, + +> JavaBeans规范中对这些均有相应的规定,基本数据类型的属性,其getter和setter方法是getXXX()和setXXX,但是对于基本数据中布尔类型的数据,又有一套规定,其getter和setter方法是isXXX()和setXXX。但是包装类型都是以get开头。 + +```java +private boolean isHot; +public boolean isHot() { + return isHot; +} +public void setHot(boolean hot) { + isHot = hot; +} +``` + +这样子在实例对象转json时,序列化框架看到isHot()方法会去找hot实例变量,这样就会找不到变量值。 + +在与前端交互时,同意需要注意,避免Boolean类型参数是is前缀开头的,因为Spring MVC在接受参数时,看到前端json传过来的值是isHot,而set方法中没有setIsHot()方法,只有setHot()方法 + +https://www.cnblogs.com/goloving/p/13086151.html + +https://blog.csdn.net/qq_31145141/article/details/71597608 + +##### 正确的加锁方式 + +通过这种方式来加锁,保证抛出异常时可以正常解锁,同时lock()方法不能在try代码块中调用,防止线程还没有加上锁时抛出异常,然后进行解锁,抛出IllegalMonitorStateException异常(对未加锁对象调用释放锁tryRelease()方法就会抛这个异常)。 + +```java +Lock lock = new XxxLock(); // ... +lock.lock(); +try { + doSomething(); + doOthers(); +} finally { + lock.unlock(); +} +``` + +##### COUNT(*)与count(列名) + +1.【强制】不要使用count(列名)或count(常量)来替代count(*),count(*)是SQL92定义的标 准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。 + 说明:count(*)会统计值为 NULL 的行,而 count(列名)会将NULL值排除掉,不会统计此列为 NULL 值的行。 + +2.count(distinct col) 计算该列除 NULL 之外的不重复行数 \ No newline at end of file diff --git a/docs/SystemDesign.md b/docs/SystemDesign.md index 54fc890..ff1b842 100644 --- a/docs/SystemDesign.md +++ b/docs/SystemDesign.md @@ -211,7 +211,7 @@ https://www.infoq.cn/article/2018/08/rocketmq-4.3-release #### TCC -2PC是基于数据库层面的分布式事务场景,而TCC是针对所有业务场景的,TCC分为Try预留阶段,Confirm确认阶段,Cancel撤销阶段三个阶段。 +TCC分为Try预留阶段,Confirm确认阶段,Cancel撤销阶段三个阶段。 比如某个事务需要A,B,C三个业务系统各自执行一些操作,那么事务管理器会发送Try指令,会让A,B,C三个业务系统各自去申请执行操作所需的一些资源,冻结库存之类的。A,B,C预留资源成功了就会通知事务管理器Try阶段执行成功了。那么事务管理器就会发送Confirm指令给三个业务系统,告诉他们进入到Confirm阶段,让A,B,C业务系统各自执行自己真正的事务操作。如果三个业务系统都执行成功,那么事务管理器就认为执行成功,如果有一个失败那么事务管理器就认为执行失败了,会通知每个业务执行Cancel操作,进行回滚。 @@ -241,7 +241,7 @@ https://www.cnblogs.com/jajian/p/10014145.html 并且下游系统是通过消息中操作记录的主键id来防止不重复消费,保证幂等性的。就是消费消息时,发送操作记录的id已经在数据库中存在了,就代表之前已经处理过了,不处理这条消息了。 -##### 最大努力通知法(RocketMQ) +##### 可靠消息最终一致性方案 RocketMQ在4.3以后,增加了对分布式事务的支持,就是将事务的执行状态保存在RocketMQ中,由RocketMQ去负责将commit状态的消息推送给下游系统。 @@ -255,12 +255,16 @@ RocketMQ在4.3以后,增加了对分布式事务的支持,就是将事务的 4.如果一个prepare消息一直没有接受到上游系统的commit或者rollback指令,这样就判定prepare消息超时了,RocketMQ会去查询上游系统的这个事务的执行状态,是成功了,还是失败,做下一步的处理。 -底层实现原理 +**底层实现原理** -RocketMQ使用了Half topic来保存所有prepare消息,使用Operation Topic来保存commit消息和rollback消息。这样通过Operation Topic就知道哪些消息commit了,可以推送给消费者,哪些消息rollback了,不需要推送给消费者。以及那些在Half topic中有,在Operation Topic中没有的消息,就是事务超时的消息。 +RocketMQ使用了Half topic队列来保存所有prepare消息,使用Operation Topic队列来保存commit消息和rollback消息。这样通过Operation Topic就知道哪些消息commit了,可以推送给消费者,哪些消息rollback了,不需要推送给消费者。以及那些在Half topic中有,在Operation Topic中没有的消息,就是事务超时的消息。 https://www.infoq.cn/article/2018/08/rocketmq-4.3-release +##### 最大努力通知方案 + +业务系统 A 执行本地事务完成后,发送个消息到 MQ,有一个个专门消费 MQ 的服务,来消费MQ的消息,消费完会在数据库中记录下来(或者放入到内存队列),之后就一直调用系统 B 的接口,要是系统 B 执行成功就提交,执行失败或者调用超时就一直重试,直到业务系统B执行成功。 + ### 如何设计秒杀系统? 1.前端页面 diff --git a/docs/ZooKeeper.md b/docs/ZooKeeper.md index a28cfb4..b916bc3 100644 --- a/docs/ZooKeeper.md +++ b/docs/ZooKeeper.md @@ -37,12 +37,18 @@ C得票超过半数,C成为leader,之后加入的D,E也只会投给C。 每个节点都会投给自己,然后选票发给其他节点 ##### 2.处理选票信息 从其他节点B收到投票信息后,进行处理 + 2.1 如果本节点的逻辑时钟小于接受这条投票的逻辑时钟, + 说明本节点之前错过了上一轮的投票,将当前存储的选票信息清空, + 2.2 如果本节点的逻辑时钟大于接受的这条投票的逻辑时钟,那么忽略掉这条投票信息。 + 2.3 本节点的逻辑时钟等于接受的这条投票的逻辑时钟,那么进行处理,与本节点当前投票的结果进行比较 先比较ZXID(数据ID,越大代表数据越新),ZXID越大的应该当leader,ZXID相同比较SID,SID越大的当leader。 + 2.4如果比较的结果跟当前节点的投票结果不一致,那么需要更改选票,将更改后的选票结果发送给其他节点。 + 2.5 将其他节点B的投票结果记录下来 ##### 3.统计选票 diff --git a/docs/algorithm.md b/docs/algorithm.md index 65c0db3..eff4a06 100644 --- a/docs/algorithm.md +++ b/docs/algorithm.md @@ -49,9 +49,9 @@ int[] sorted(int[] array) { } } // 该趟排序中没有发生,表示已经有序 - if (0 == sortedFlag) { - break; - } + if (0 == sortedFlag) { + break; + } } return array; } @@ -171,10 +171,10 @@ int[] quickSorted(int [] array, int start,int end) { array[start] = array[i]; array[i] = base; //左边的组继续进行快排 - quickSorted(array,start,i-1); + quickSorted(array,start,i-1); //右边的组继续进行快排 - quickSorted(array,i+1,end); - return array; + quickSorted(array,i+1,end); + return array; } ``` @@ -317,6 +317,32 @@ int find_left_bound(int[] array,double target) { } ``` +寻找左边界的二分查找算法 + +```java +int findLeft(int[] array,int target) { + if (array==null|| array.length==0) { + return -1; + } + int left = 0; + int right = array.length-1; + while (left<=right) { + int mid = (left+right)/2; + if (array[mid] == target) { + if (mid==0 || array[mid-1] capacity) { // 装不下该珠宝 result = testKnapsack1(value,weight,i-1, capacity); - } else if (weight[i] <= capacity) {//可以选择当前物品的 - // 可以装下 + } else if (weight[i] <= capacity) {// 可以装下 + //选择物品i int choose = testKnapsack1(value,weight,i-1, capacity-weight[i]) + value[i]; + //不选择物品i int notChoose = testKnapsack1(value,weight,i-1, capacity); result = choose > notChoose ? choose : notChoose; } diff --git a/docs/linux.md b/docs/linux.md index a069cf3..c56e09c 100644 --- a/docs/linux.md +++ b/docs/linux.md @@ -15,4 +15,26 @@ https://www.cnblogs.com/aspirant/p/11543456.html ### 僵尸进程和孤儿进程是什么? 僵尸进程就是子进程调用exit退出或者是运行时发生致命错误,结束运行时,一般会把进程的退出状态通知给操作系统,操作系统发送SIGCHLD信号告诉父进程“子进程退出了”,父进程一般会使用wait系统调用以获得子进程的退出状态,这样内核就可以在内存中释放子进程了,但是如果父进程没有进行wait系统调用,子进程就会驻留在内存,成为僵尸进程。 -孤儿进程就是父进程退出,但是它的子进程还在进行,这些子进程就会变成孤儿进程,被init进程(进程号为1)所收养,由它来管理和收集子进程的状态。由于孤儿进程有init进程循环的wait()调用回收资源,所以不会产生什么危害。 \ No newline at end of file +孤儿进程就是父进程退出,但是它的子进程还在进行,这些子进程就会变成孤儿进程,被init进程(进程号为1)所收养,由它来管理和收集子进程的状态。由于孤儿进程有init进程循环的wait()调用回收资源,所以不会产生什么危害。 + +##### Linux指令使用 + +统计access.log中ip访问次数前十的 + +``` +cat access.log | awk '{ print $1}' | sort -n | uniq -c | sort - r |head 10 +``` + +统计当前目录下(包含子目录) java 的文件的代码总行数。 + +``` +wc -l `find . -name "*.java"` | awk '{ sum=sum+$1 } END { print sum }' +``` + +### Linux进程间通信的方式? + +《Linux 的进程间通信》 https://zhuanlan.zhihu.com/p/58489873 + +浅析进程间通信的几种方式(含实例源码) https://zhuanlan.zhihu.com/p/94856678 + +https://mp.weixin.qq.com/s/WgZaS5w5IXa3IBGRsPKtbQ \ No newline at end of file diff --git a/index.html b/index.html index 05c83a1..a1aa2b7 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - 《后端技术总结》 + 《大厂面试指北》 @@ -14,7 +14,7 @@