buguge - Keep it simple,stupid

知识就是力量,但更重要的,是运用知识的能力why buguge?

导航

栈内存溢出-StackOverflowError

java.lang.StackOverflowError-(线程)栈内存溢出错误

栈内存溢出是进行复杂运算时非常容易出现的错误。

下面test方法是一个死循环自己调自己的例子。运行这个方法,你会看到熟悉又不常见的java.lang.StackOverflowError:

 1 package com.clz;
 2 
 3 import org.junit.Test;
 4 
 5 public class TestMain {
 6     @Test public void testStackOverflowError() {
 7         testStackOverflowError();
 8     }
9 
10 }

运行结果:

java.lang.StackOverflowError
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第1条stackTrace)
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第2条stackTrace)
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第3条stackTrace)
    。。。。。。
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第500条stackTrace)
    。。。。。。
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第1022条stackTrace)
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第1023条stackTrace)
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第1024条stackTrace)

注意到以上运行结果,一共会打印出来1024条stacktrace信息。说明java中线程栈的长度是1024。

方法自己调自己,可能很少见,因为很容易发现问题。而类与类相互依赖(循环依赖)可能会出现的几率就会大一些了。尤其是诸如A->B->C->A这种链路长的,可能没那么容易发现。

如下示例中,A和B两个类出现了循环依赖,同样会抛出StackOverflowError。

// ----- class A
package jstudy.ab;

@Service
public class A {
    @Autowired    B b;

    public void testA1() {
        System.out.println("a.testA1()");
    }

    public void testA2() {
        System.out.println("a.testA2()");
        b.testB();
    }



// ----- class B
package jstudy.ab;

@Service
public class B {
    @Autowired    A a;
    public void testB(){
        System.out.println("b.testB()");
        a.testA2();
    }
}

 

执行如下testcase可以看到抛出了StackOverflowError。

package jstudy.ab;

//import org.junit.jupiter.api.Test;
import org.junit.Test;


@SpringBootTest
@RunWith(SpringRunner.class)
public class ATest {
    @Autowired    A a;

    @Test
    public void testA2() {
        a.testA2();
    }
}

 

执行结果及堆栈信息:

1	a.testA2()	
2	b.testB()	
3	a.testA2()	
4	b.testB()	
5	a.testA2()	
6	b.testB()	
…		
15038	java.lang.StackOverflowError	
15039		at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
15040		at sun.nio.cs.UTF_8.access$200(UTF_8.java:57)
15041		at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:636)
15042		at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)
15043		at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)
15044		at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
15045		at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
15046		at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
15047		at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
15048		at java.io.PrintStream.write(PrintStream.java:526)
15049		at java.io.PrintStream.print(PrintStream.java:669)
15050		at java.io.PrintStream.println(PrintStream.java:806)
15051		at jstudy.ab.A.testA2(A.java:16)
15052		at jstudy.ab.B.testB(B.java:12)
15053		at jstudy.ab.A.testA2(A.java:17)
15054		at jstudy.ab.B.testB(B.java:12)
15055		at jstudy.ab.A.testA2(A.java:17)
…		
16061		at jstudy.ab.A.testA2(A.java:17)
16062		at jstudy.ab.B.testB(B.java:12)

 

注意到以上运行结果,一共会打印出来1024条stacktrace信息(16062-15039+1=1024)。说明java中线程栈的长度是1024。

 

线程栈:thread stack。我们分析程序的jvm dump时,往往需要通过查看Thread Stack来分析问题。比如下图:

 

 

------------我是么么哒分割线----------------

 

我们来看如下代码的线程栈图示

public class Demo {
    public static void main(String[] args) {
        String message="hello JVisualVM world";
        hello(message);
    }

    private static void hello(String text) {
        System.out.println(text);
    }
}

 

 

 

 

在java中,虚拟机会为每个任务的处理分配一个线程, 在这个线程里,每次调用一个方法,都会将本次方法调用的栈桢压入虚拟机栈里,这个栈桢里保存着方法内部的局部变量和其他信息。 不过呢,每个线程的虚拟机栈的大小是固定的,默认为1MB(上面的1024)。

既然一个线程的虚拟机栈内存大小是有限的,那么假设不停的调用各种方法,对应的栈桢不停的压入栈中。当这些大量的栈桢消耗完毕这个1MB的线程栈内存,最终就会导致出现栈内存溢出——StackOverflowError。

 

 

 

而我在上周四boss开工改版时,对redis分布式锁接口方法做了调整,却因为一个失误导致了StackOverflowError。

>>先看接口定义,然后再说问题:

 1 package com.emax.zhenghe.common.concurrent.distributeRedisLock;
 2 
 3 public interface DistributedLock {
 4         
 5      boolean lock(String key);
 6     
 7      boolean lock(String key, int retryTimes);
 8     
 9      boolean lock(String key, int retryTimes, long sleepMillis);
10     
11      boolean lock(String key, long expireMillis);
12     
13      boolean lock(String key, long expireMillis, int retryTimes);
14     
15      boolean lock(String key, long expireMillis, int retryTimes, long sleepMillis);
16     
17      boolean releaseLock(String key);
18 }

 

>>接下来说问题:

分布式锁在技术层面有两种应用场景:
1. 可以保证幂等性(防重与幂等有区别:幂等通常是对并发请求的防重控制;防重除了需要分布式保证幂等以外,还需要做数据防重校验,因为重复请求可能不是并发请求过来的,有可能是隔了很长时间的重复数据提交,就是用DCL)
2. 实现进程同步(类似于线程synchronized锁):当锁存在时,需要不断尝试重试取锁,实现自旋等待。

这个接口正好也为两种应用场景定义了方法API。问题在于,我们看这些lock重载方法,比较第7行的lock(String,int)与第11行的lock(String,long),再比较第9行的lock(String,int,long)与第13行的lock(String,long,int),太容易误用了。事实也证明了这一点,项目的业务代码里有很多用来保证幂等性的逻辑调用的是lock(String,int)或lock(String,long,int),而非lock(String,long),那么,显然无法达到幂等控制的效果。

为了解决项目中现存的这种误用,并规避日后的误用,有必要重构这个接口。如下是第一版:

 1 package com.emax.zhenghe.common.concurrent.distributeRedisLock;
 2 
 3 public interface DistributedLock {
 4     boolean lock(String key);
 5 
 6 //     boolean lock(String key, int retryTimes);
 7 
 8 //    boolean lock(String key, int retryTimes, long sleepMillis);
 9 //     boolean synchronize(String key, int retryTimes, long sleepMillis);
10 
11     boolean lock(String key, long expireMillis);
12 
13     /**
14      * 注:有很多项目很多代码调用了这个方法,过渡阶段先保留这个方法api
15      * 注:此方法不再使用。请不要使用过期的方法
16      * @param key
17      * @param expireMillis
18      * @param useless 有很多地方在用,不得不定义这个寂寞参数
19      * @return
20      */
21     @Deprecated
22     default boolean lock(String key, long expireMillis,int useless){
23         return lock(key, expireMillis, 0);
24     }
25 
26     //    boolean lock(String key, long expireMillis, int retryTimes);
27     boolean synchronize(String key, long expireMillis, int retryTimes);
28 
29     //    boolean lock(String key, long expireMillis, int retryTimes, long sleepMillis);
30     boolean synchronize(String key, long expireMillis, int retryTimes, long sleepMillis);
31 
32     boolean releaseLock(String key);
33 
34     boolean releaseLockUnsafe(String key);
35 }

 

导致出现StackOverflowError的,正是第22行标记了过时的lock(String key, long expireMillis,int useless)。这个方法调用的是其自身!而我的本意是要它调用第11行的lock(key, expireMillis):

。。。
13     /**
14      * 注:有很多项目很多代码调用了这个方法,过渡阶段先保留这个方法api
15      * 注:此方法不再使用。请不要使用过期的方法
16      * @param key
17      * @param expireMillis
18      * @param useless 有很多地方在用,不得不定义这个寂寞参数
19      * @return
20      */
21     @Deprecated
22     default boolean lock(String key, long expireMillis,int useless){
23         return lock(key, expireMillis);
24     }
。。。

 

posted on 2021-05-31 19:59  buguge  阅读(685)  评论(0编辑  收藏  举报