为什么第一个逃逸了,第二个没有逃逸?
public static StringBuffer craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
第一个方法 返回了 StringBuffer 类的引用, 那外围的程序就可以修改它的内容,也就是 它 对于这个方法来讲是 逃逸出来了
第二个方法 返回的是 String 字符串, StringBuffer这个类对其他调用者来讲 是无法访问的,也就是 StringBuffer 是无法逃逸出 当前方法的。
垃圾回收是否涉及栈内存?
不需要。因为栈内存无非就是一次次的方法调用所产生的栈帧内存,而栈帧内存在每一次方法调用结束后,都会被弹出栈,也就是会被自动的回收掉,所以根本不需要垃圾回收来管理栈内存。
栈内存分配越大越好吗?
栈内存可以在运行代码时通过虚拟机参数指定,即“-Xss”这个参数,他是给我们栈内存指定大小,如果不指定,Linux、macOS、Solaris等系统默认每个栈的大小都是1024KB(一兆),Windows比较特殊,他是Windows的虚拟内存影响栈的大小。栈内存设置的越大,反而会让线程数变得更少,因为物理内存的大小是一定的,比如说我一个线程使用的是一个栈内存,那一个线程假如用了一兆内存,假如总共的物理内存假设有500兆,那理论上可以有五百个线程同时运行,但假如把每个线程的栈内存设置了2M内存,那么理论上只能同时运行250个线程。所以栈内存并不是划分的越大越好。把他设置大了,通常只是能够进行更多次的方法递归调用,而不会增强运行的效率,反而会影响到线程数目的变少。所以不建议设置为太大,一般采用系统默认的即可。
方法内的局部变量是否线程安全?
看一个变量是不是线程安全,要看多个线程对这个变量是共享的还是这个变量被每个线程是私有的。比如代码示例如下:
// 局部变量的线程安全问题
public class Demo {
// 多个线程同时执行此方法
static void m1() {
int x = 0;
for(int i = 0; i < 5000; i ++) {
x ++;
}
print(x);
}
}
这个方法内有局部变量x,循环5000次自增,然后打印。当多个线程同时执行这个方法时,比如有两个线程都来调这个方法,那么会不会造成最后x值的混乱呢?答案是不会。因为x是方法内的局部变量,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,比如线程1调用这个方法,那么他就会在栈1里开栈帧1,里面有局部变量x,初始值是0,接下来线程2也调用m1方法,那么会在栈2里再产生一个栈帧2,在栈帧2里也有自己的局部变量x,x的初始值也是0,相当于每个线程都有自己私有的x这个局部变量,所以他们各自加5000,互不干扰,最后每个线程内都会得出x结果是5000。所以,在这个例子里是不会有线程安全问题。
当然,如果这个变量改成static变量,结果就会大不一样了。
因为这样的话就相当于共享了。所以要考虑的问题是,如果是共享的,
就要考虑线程安全问题,不是共享的而是每个线程私有的,就算了。
再看看下面的代码示例:
public class Demo {
public static void main(String[] args) {
}
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
print(sb.toString());
}
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
print(sb.toString());
}
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
这里有三个方法 m1 m2 m3,都做用stringbuilder做字符串的拼接操作。那么这三个方法会不会存在线程安全问题呢。
m1是不会有线程安全问题的,这个跟第一个代码示例中的分析是一个道理,即在m1的sb是线程内的局部变量,可以说是每个线程私有的。
m2和m1的不同之处是sb不是方法内的局部变量,而是方法的参数,这段代码是线程不安全的,因为既然sb是作为方法的参数传递进来的,那么就意味着有可能有其他的线程访问到sb,所以这时候sb不再是线程私有的了,有可能是多个线程都能访问到他,这意味着这里的sb对多个线程是共享的。
比如,在main方法内添加如下代码示例:
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(()->{
m2(sb);
}).start();
即在主方法里创建了sb,然后开了新的线程,新的线程里,把sb传给了m2方法,那么主线程和新的线程里都在修改sb,所以两个线程在使用者同一个sb对象,这样的话在这个场景下不再是线程私有的了,而是多个线程共享了同一个对象,所以不再是线程安全的了。所以这时候,不能用stringbuilder了,应该改成stringbuffer。
那么m3是不是线程安全呢?答案也是不行。因为,虽然sb是方法内的局部变量,但是他把他当做返回结果,返回了,那就意味着其他的线程有可能会拿到这个引用,去并发的修改他,所以也会造成线程安全的问题。
要想判断一个变量是不是线程安全的,不仅要看他是不是方法内的局部
变量,还要看他是否逃离了这个方法的作用范围,如果像m3那样逃离了
方法作用范围,那么他就有可能会被别的线程访问到了,就不再是线程
安全的了。(当然了,基本类型除外。)
逃逸分析是一种优化技术,它可以判断一个对象在程序中是否会被其他线程访问到,如果不会被其他线程访问到,那么可以进行一些优化处理,如栈上分配、标量替换等,从而减少垃圾回收的频率,提升程序的性能。
在Java中,可以通过设置JVM参数来启用逃逸分析,例如:-XX:+DoEscapeAnalysis。
逃逸分析的好处主要有两个方面: 1. 减少堆上对象的分配:如果一个对象不会逃逸出方法外部,在编译优化时可以将其分配在栈上,避免了在堆上的分配和垃圾回收的开销。 2. 提高程序的并发性:通过逃逸分析可以发现不逃逸的对象,可以进行一些线程安全的优化,如锁消除、同步省略等,从而提高程序的并发性能。
下面以一个示例来说明如何进行逃逸分析:
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 1000000; i++) {
allocateObject();
}
}
public static void allocateObject() {
Object obj = new Object(); // 这里的对象可能会逃逸出方法外部
System.out.println(obj.toString());
}
}
在该示例中,allocateObject方法在每次调用时都会创建一个新的对象,并打印其toString方法。如果对象在方法内部不会逃逸出来,那么可以进行优化,将对象的内存分配在栈上,而不是堆上。
需要注意的是,逃逸分析是由JVM自动进行的,我们无法直接控制其具体的优化策略和结果。
参考资料: - 段落0: 3、深入理解JVM-张龙: www.bilibili.com/video/av75247289