伪内存共享
伪内存共享
定义
- 伪内存共享是一种在多核处理器系统或多处理器系统中出现的性能问题。在这种情况下,多个线程看似在共享内存,但实际上它们访问的是不同的数据元素。然而,由于这些数据元素在内存中的位置布局,它们会被加载到同一个缓存行(Cache Line)中。
- 缓存行是 CPU 缓存与主内存之间数据交换的基本单位,其大小通常为 64 字节(不同的 CPU 架构可能不同)。当一个线程修改了缓存行中的某个数据时,根据缓存一致性协议(如 MESI 协议),整个缓存行在其他 CPU 核心的缓存中会被标记为无效。这样,其他线程访问同一缓存行中的数据时,就需要重新从主内存中加载数据,从而导致性能下降。
产生原因
- 数据布局导致的缓存行冲突
- 结构体成员访问:在程序中,当一个结构体中的不同成员被不同线程频繁访问,且这些成员在内存中紧密相邻,就容易出现伪内存共享。例如,有一个结构体struct Data {int a; int b;},如果线程 1 频繁访问a,线程 2 频繁访问b,由于a和b在内存中相邻,很可能位于同一个缓存行中。当线程 1 修改a时,包含a和b的缓存行在其他核心的缓存中被标记为无效,那么线程 2 访问b时就需要重新从主内存获取数据。
- 数组元素访问:对于数组,相邻元素被不同线程频繁访问也会导致伪内存共享。比如有一个int数组arr,线程 1 经常访问arr[0],线程 2 经常访问arr[1],如果它们在同一个缓存行中,就会出现类似上述结构体的问题。
- 缓存一致性协议的影响
- 现代 CPU 采用缓存一致性协议来维护多个缓存之间数据的一致性。以 MESI 协议为例,当一个 CPU 核心修改了其缓存行中的数据(从 “Modified” 状态),其他核心缓存中的相同缓存行就会被标记为 “Invalid” 状态。这种机制虽然保证了数据的一致性,但在伪内存共享的场景下,却导致了不必要的缓存行刷新和重新加载,从而影响性能。
解决方案
- 数据结构优化
- 填充(Padding)技术:在数据结构的成员之间添加填充字节,使得被不同线程频繁访问的数据元素位于不同的缓存行。以 C++ 结构体为例,对于struct Data {int a; int b;},可以修改为struct Data {int a; char padding[60]; int b;}(假设缓存行大小为 64 字节),这样可以尽量确保a和b不在同一个缓存行中。
- 数据结构拆分:将可能导致伪内存共享的大型数据结构拆分成多个较小的数据结构,使得不同线程访问的数据分离。例如,对于一个包含多个成员的结构体,将其拆分成几个独立的结构体,每个结构体由特定的线程访问。
- 线程访问策略调整
- 本地化数据访问:尽量使每个线程访问的数据在内存中相对集中,减少跨缓存行的访问。例如,在多线程处理数组时,将数组划分成多个子数组,每个线程负责处理一个子数组,这样可以降低相邻元素被不同线程频繁访问的可能性。
- 使用线程本地存储(Thread - Local Storage):如果可能,将数据设置为线程本地的,这样每个线程都有自己独立的副本,不会因为共享缓存行而产生冲突。比如在 Java 中,可以使用ThreadLocal类来实现线程本地存储,避免不同线程共享数据导致的伪内存共享。
CPU缓存设计
CPU 缓存的层次结构
-
L1 缓存(一级缓存)
- 特点:L1 缓存是最靠近 CPU 核心的缓存,通常分为数据缓存(L1 - Data Cache,L1d)和指令缓存(L1 - Instruction Cache,L1i)。它的容量最小,一般在几十 KB 到几百 KB 之间,例如,常见的 L1 缓存容量可能是 32KB 或 64KB。但它的速度极快,访问延迟通常在 1 - 3 个 CPU 时钟周期左右。
- 作用:由于其靠近 CPU 核心且速度快,主要用于存储 CPU 即将执行的指令和马上要处理的数据。对于循环中的指令和数据,L1 缓存可以快速提供服务,减少 CPU 等待数据从主内存传输的时间。例如,在一个简单的循环计算中,循环体的指令和涉及的数据变量很可能存储在 L1 缓存中,使得 CPU 能够高效地反复执行循环操作。
-
L2 缓存(二级缓存)
- 特点:L2 缓存的容量比 L1 缓存大,通常在几百 KB 到几 MB 之间,如常见的 L2 缓存容量可能是 256KB 或 512KB。它的速度稍慢于 L1 缓存,访问延迟大约在 10 - 20 个 CPU 时钟周期。L2 缓存是多个 CPU 核心共享的(在多核处理器中),不过也有一些处理器设计中 L2 缓存是每个核心独有的。
- 作用:它用于存储从主内存预取的数据以及 L1 缓存中替换出来的数据。当 L1 缓存未命中时,CPU 会首先在 L2 缓存中查找数据。例如,在处理稍微复杂一些的数据结构或者较大的数据集时,L2 缓存可以提供更广泛的数据存储,降低从主内存获取数据的频率。
-
L3 缓存(三级缓存)
- 特点:L3 缓存的容量更大,一般在几 MB 到几十 MB 之间,例如某些高端处理器的 L3 缓存可达 32MB。它的速度比 L2 缓存更慢,访问延迟可能在 30 - 60 个 CPU 时钟周期左右。L3 缓存也是多核处理器共享的缓存,它在整个处理器芯片中的位置相对更远离 CPU 核心。
- 作用:主要用于在多个核心之间共享数据,进一步减少对主内存的访问。在多线程或多核心应用中,L3 缓存可以存储被多个核心频繁访问的公共数据,提高数据的共享效率。例如,在一个多线程的科学计算应用中,多个线程可能会共享一些中间计算结果或者大型数组的部分内容,这些数据可以存储在 L3 缓存中。
-
主内存(DRAM)
- 特点:主内存的容量通常比 CPU 缓存大得多,现在常见的计算机主内存容量可以达到数 GB 甚至数十 GB。但它的速度远远慢于 CPU 缓存,访问延迟通常在几十纳秒到几百纳秒之间,相比之下,CPU 缓存的访问延迟是以 CPU 时钟周期来衡量的,要快得多。
- 作用:它是计算机存储系统的主要存储区域,用于存储操作系统、应用程序以及用户数据等所有的信息。尽管 CPU 缓存可以加速数据访问,但最终的数据来源还是主内存。
缓存行(Cache Line)机制
- 定义和大小:缓存行是 CPU 缓存与主内存之间数据交换的基本单位。其大小通常是固定的,常见的缓存行大小为 64 字节(不同的 CPU 架构可能会有所不同)。例如,当 CPU 需要从主内存读取一个字节的数据时,它不会只读取这一个字节,而是会把包含这个字节的整个 64 字节的缓存行从主内存加载到缓存中。
- 数据读取方式:这种以缓存行为单位的读取方式是基于空间局部性原理。即如果一个数据被访问,那么它附近的数据很可能也会被访问。例如,在访问一个数组元素时,如果 CPU 读取了某个元素,它会将包含该元素的缓存行都加载进来。这样,如果接下来要访问同一缓存行内的其他元素,就可以直接从缓存中获取,而不需要再从主内存读取,大大提高了数据访问效率。
- 对性能的影响:缓存行机制在多数情况下有利于提高性能,但在某些情况下也可能导致问题,如伪内存共享。当多个线程访问的数据位于同一个缓存行中时,即使它们访问的是不同的数据部分,也可能会因为缓存一致性协议的作用而导致性能下降。
缓存一致性协议
- 必要性:在多处理器或多核系统中,由于每个处理器核心都有自己的缓存,当一个核心修改了缓存中的数据后,需要保证其他核心缓存中的数据一致性。例如,在一个有两个核心的处理器中,核心 A 修改了一个变量的值,核心 B 缓存中对应的变量值也需要更新,否则就会出现数据不一致的情况。
- 常见协议类型:
- MESI 协议(Modified、Exclusive、Shared、Invalid):这是一种广泛使用的缓存一致性协议。
- Modified 状态:当一个缓存行在某个核心的缓存中处于修改状态时,表示该核心已经修改了这个缓存行的数据,并且这个数据是最新的,与主内存中的数据不一致。其他核心的缓存中不能有这个缓存行的副本。
- Exclusive 状态:表示该缓存行只在一个核心的缓存中有副本,并且与主内存中的数据一致。这个核心可以在不通知其他核心的情况下将这个缓存行修改为 Modified 状态。
- Shared 状态:表示这个缓存行在多个核心的缓存中有副本,并且所有副本与主内存中的数据一致。当一个核心要修改这个缓存行时,需要先将其他核心缓存中的这个缓存行变为 Invalid 状态。
- Invalid 状态:表示这个缓存行在该核心的缓存中是无效的,需要从其他缓存或主内存中获取最新的数据来更新。
- MOESI 协议(Modified、Owned、Exclusive、Shared、Invalid):是 MESI 协议的扩展,增加了 Owned 状态。在 MOESI 协议中,处于 Owned 状态的缓存行表示这个缓存行的数据在本核心的缓存中是最新的,并且其他核心可能有这个缓存行的只读副本。这种协议可以进一步提高缓存数据的共享效率。
- MESI 协议(Modified、Exclusive、Shared、Invalid):这是一种广泛使用的缓存一致性协议。
JAVA的对象结构
在通过现象观察中,使用填充(Padding)技术优化时,将填充对象设置为数组时,未达到优化效果。经过一些结果分析,这里有必要对对象结构进行简单了解。
对象头(Object Header)
- 概述:对象头是 Java 对象在内存中的一部分,用于存储对象的一些元数据信息。其大小在 32 位和 64 位虚拟机中有所不同。在 32 位虚拟机中,对象头一般占用 8 字节;在 64 位虚拟机中,对象头占用 12 字节或 16 字节(开启指针压缩时为 12 字节)。
- 存储内容
- 哈希码(HashCode):如果对象的hashCode()方法被调用过,对象头会存储对象的哈希码。这对于对象在哈希表(如HashMap)中的存储和查找很重要,因为哈希码用于确定对象在哈希表中的位置。
- 分代年龄(Age):在 Java 的垃圾回收机制中,对象会被划分到不同的代(年轻代、老年代)。对象头中会记录对象在年轻代中经历过的垃圾回收次数,这个次数就称为分代年龄。当分代年龄达到一定阈值时,对象会被晋升到老年代。
- 锁状态标志位(Lock State Flag):用于表示对象的锁状态,这与 Java 的多线程同步机制有关。例如,无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态等,不同的锁状态会在对象头中有不同的标记,这些标记可以帮助虚拟机在多线程环境下快速判断对象的锁状态,从而采取相应的同步策略。
- 数组长度(Length)字段
位置和大小:紧跟在对象头之后,存储数组的长度信息。在 Java 中,数组长度是一个int类型的值,因此占用 4 字节(在 32 位和 64 位 JVM 中都是如此)。这个长度字段决定了数组可以容纳的元素数量。例如,对于int[] array = new int[5];,这个4字节的长度字段存储的值就是5
实例数据(Instance Data)
- 概述:实例数据部分存储的是对象的成员变量(非静态成员变量)的值。这些成员变量的存储顺序会受到虚拟机的分配策略和变量类型的影响。
- 存储顺序和对齐规则
- 基本数据类型顺序:一般来说,相同类型的成员变量会被分配在一起,并且按照定义的顺序存储。例如,对于一个类class MyClass{int a; long b;},a和b会按照定义顺序存储在实例数据部分。
- 内存对齐(Memory Alignment):为了提高内存访问效率,虚拟机可能会对实例数据进行内存对齐。例如,int类型通常占用 4 字节,在内存中其地址可能会被对齐到 4 的倍数;long类型占用 8 字节,其地址可能会被对齐到 8 的倍数。这意味着在存储实例数据时,可能会在变量之间添加一些填充字节(Padding)来满足对齐要求。
对齐填充(Padding)
- 概述:对齐填充部分不是必须存在的,它是为了满足虚拟机的内存对齐要求而添加的额外字节。这些字节没有实际的存储内容,只是为了使对象的大小符合内存对齐规则。
- 举例说明:假设一个类只有一个char类型(占用 1 字节)的成员变量,由于内存对齐要求,虚拟机可能会在这个char变量后面添加一些填充字节,使得下一个对象(如果有的话)在内存中的存储位置符合对齐规则。例如,在 64 位虚拟机中,对象的大小可能需要是 8 的倍数,那么就可能会添加 7 个填充字节,使对象的总大小达到 8 字节。
详细的内存布局示例(以一个简单类为例)
假设我们有一个 Java 类Person,定义如下:
class Person {
private int age;
private String name;
}
在内存中的布局可能如下(示意,实际情况受虚拟机实现和配置影响):
内存区域 | 详细信息 |
---|---|
对象头(假设 64 位虚拟机,12 字节) | 包含对象的哈希码生成信息、分代年龄(初始为 0)、锁状态(初始为无锁状态)等。这部分是 Java 虚拟机用于管理对象的核心元数据区域 |
实例数据(4 字节(age) + 引用大小(name)字节) | age变量会占用 4 字节存储其整数值。name是一个String对象引用,在 64 位虚拟机(开启压缩指针时为 4 字节,未开启时为 8 字节)中存储指向String对象的内存地址 |
对齐填充(根据对齐规则可能有填充字节) | 如果实例数据部分之后的内存地址不符合对齐要求,会添加填充字节。例如,如果对象的总大小需要是 8 的倍数,而前面部分的大小不是 8 的倍数,就会添加相应的填充字节来满足这个要求 |
对于数组对象对象头还包括数组长度(Length)
了解对象结构后,我们就可以通过分析对象结构来排查,为什么将填充对象设置为数组时,未达到优化效果。经过以下结果分析。
对象填充
代码
package com;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphLayout;
public class XYJ_FalseSharingOptimizedExampleEffectiveCases {
public static void main(String[] args) throws InterruptedException {
final SharedDataOptimized data = new SharedDataOptimized();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
data.value1++;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
data.value2++;
}
});
long startTime = System.currentTimeMillis();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
long endTime = System.currentTimeMillis();
System.out.println("Optimized Execution time: " + (endTime - startTime) + "ms");
System.out.println("Execution resultvalue1: "+data.value1);
System.out.println("Execution resultvalue2: "+data.value2);
System.out.println(ClassLayout.parseInstance(data).toPrintable());
System.out.println(GraphLayout.parseInstance(data).toPrintable());
}
static class SharedDataOptimized {
public volatile long value1;
public long b1,b2,b3,b4,b5,b6,b7;
public volatile long value2;
}
}
运行结果及结构分析
com.XYJ_FalseSharingOptimizedExampleEffectiveCases$SharedDataOptimized object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c143
12 4 (alignment/padding gap)
16 8 long SharedDataOptimized.value1 100000000
24 8 long SharedDataOptimized.b1 0
32 8 long SharedDataOptimized.b2 0
40 8 long SharedDataOptimized.b3 0
48 8 long SharedDataOptimized.b4 0
56 8 long SharedDataOptimized.b5 0
64 8 long SharedDataOptimized.b6 0
72 8 long SharedDataOptimized.b7 0
80 8 long SharedDataOptimized.value2 100000000
Instance size: 88 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
com.XYJ_FalseSharingOptimizedExampleEffectiveCases$SharedDataOptimized@5a61f5dfd object externals:
ADDRESS SIZE TYPE PATH VALUE
76f03edf0 88 com.XYJ_FalseSharingOptimizedExampleEffectiveCases$SharedDataOptimized (object)
Addresses are stable after 1 tries.
我们可以看到SharedDataOptimized.value1和SharedDataOptimized.value2两个成员之间有其他的成员进行填充
数组填充
代码
package com;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphLayout;
public class XYJ_FalseSharingOptimizedExampleInvalidcase {
public static void main(String[] args) throws InterruptedException {
final SharedDataOptimized data = new SharedDataOptimized();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
data.value1++;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
data.value2++;
}
});
long startTime = System.currentTimeMillis();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
long endTime = System.currentTimeMillis();
System.out.println("Optimized Execution time: " + (endTime - startTime) + "ms");
System.out.println("Execution resultvalue1: "+data.value1);
System.out.println("Execution resultvalue2: "+data.value2);
System.out.println(ClassLayout.parseInstance(data).toPrintable());
System.out.println(GraphLayout.parseInstance(data).toPrintable());
System.out.println(ClassLayout.parseInstance(data.padding).toPrintable());
}
static class SharedDataOptimized {
public volatile long value1;
public byte[] padding = new byte[64];
public volatile long value2;
}
}
运行结果及结构分析
Optimized Execution time: 1233ms
Execution resultvalue1: 100000000
Execution resultvalue2: 100000000
com.XYJ_FalseSharingOptimizedExampleInvalidcase$SharedDataOptimized object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c143
12 4 byte[] SharedDataOptimized.padding [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
16 8 long SharedDataOptimized.value1 100000000
24 8 long SharedDataOptimized.value2 100000000
Instance size: 32 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
com.XYJ_FalseSharingOptimizedExampleInvalidcase$SharedDataOptimized@5a61f5dfd object externals:
ADDRESS SIZE TYPE PATH VALUE
76f03ed38 32 com.XYJ_FalseSharingOptimizedExampleInvalidcase$SharedDataOptimized (object)
76f03ed58 80 [B .padding [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Addresses are stable after 1 tries.
[B object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000002096442d01 (hash: 0x2096442d; age: 0)
8 4 (object header: class) 0xf80000f5
12 4 (array length) 64
16 64 byte [B.<elements> N/A
Instance size: 80 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
Process finished with exit code 0
我们可以看到SharedDataOptimized.value1和SharedDataOptimized.value2两个成员之间并有其他的成员,而是在头信息中包含一个数组的地址引用,而引用地址正是数组真正的内存地址。
通过分析打印的实例结构,发现数组填充的实例【com.XYJ_FalseSharingOptimizedExampleInvalidcase$SharedDataOptimized object internals】,
其SharedDataOptimized.value1和SharedDataOptimized.value2两个成员之间并没有进行填充,无法使不同线程频繁访问的成员位于不同的缓存行,也就无法达到伪内存共享优化的目的。
现象观察
- 不进行优化
package com;
public class XYJ_FalseSharingExample {
public static void main(String[] args) throws InterruptedException {
final SharedData data = new SharedData();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
data.value1++;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
data.value2++;
}
});
long startTime = System.currentTimeMillis();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
long endTime = System.currentTimeMillis();
System.out.println("Execution time: " + (endTime - startTime) + "ms");
System.out.println("Execution resultvalue1: "+data.value1);
System.out.println("Execution resultvalue2: "+data.value2);
}
}
class SharedData {
public volatile long value1;
public volatile long value2;
}
执行结果及执行时间
Execution time: 1357ms
Execution resultvalue1: 100000000
Execution resultvalue2: 100000000
Process finished with exit code 0
使用填充(Padding)技术优化
- 场景1
package com;
public class XYJ_FalseSharingOptimizedExampleEffectiveCases {
public static void main(String[] args) throws InterruptedException {
final SharedDataOptimized data = new SharedDataOptimized();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
data.value1++;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
data.value2++;
}
});
long startTime = System.currentTimeMillis();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
long endTime = System.currentTimeMillis();
System.out.println("Optimized Execution time: " + (endTime - startTime) + "ms");
System.out.println("Execution resultvalue1: "+data.value1);
System.out.println("Execution resultvalue2: "+data.value2);
}
static class SharedDataOptimized {
public volatile long value1;
public long b1,b2,b3,b4,b5,b6,b7;
public volatile long value2;
}
}
执行结果及执行时间
Optimized Execution time: 154ms
Execution resultvalue1: 100000000
Execution resultvalue2: 100000000
Process finished with exit code 0
- 场景2
package com;
public class XYJ_FalseSharingOptimizedExampleInvalidcase {
public static void main(String[] args) throws InterruptedException {
final SharedDataOptimized data = new SharedDataOptimized();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
data.value1++;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
data.value2++;
}
});
long startTime = System.currentTimeMillis();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
long endTime = System.currentTimeMillis();
System.out.println("Optimized Execution time: " + (endTime - startTime) + "ms");
System.out.println("Execution resultvalue1: "+data.value1);
System.out.println("Execution resultvalue2: "+data.value2);
}
static class SharedDataOptimized {
public volatile long value1;
public byte[] padding = new byte[64];
public volatile long value2;
}
}
执行结果及执行时间
Optimized Execution time: 1474ms
Execution resultvalue1: 100000000
Execution resultvalue2: 100000000
Process finished with exit code 0
原理:由于缓存行大小通常是固定的(如 64 字节),通过在两个变量之间插入足够多的字节,使得它们在内存中的距离超过缓存行大小,从而降低它们位于同一个缓存行的概率。在 Java 中,虽然没有直接控制内存布局的方式,但可以通过字节数组来模拟填充。
但是通过在两个变量之间通过增加数组方式进行填充时,并没有达到预想的优化效果。
使用线程本地存储(Thread - Local Storage)
package com;
class ThreadLocalData {
private static ThreadLocal<Integer> threadLocalValue1 = ThreadLocal.withInitial(() -> 0);
private static ThreadLocal<Integer> threadLocalValue2 = ThreadLocal.withInitial(() -> 0);
public static void incrementValue1() {
int value = threadLocalValue1.get();
value++;
threadLocalValue1.set(value);
}
public static void incrementValue2() {
int value = threadLocalValue2.get();
value++;
threadLocalValue2.set(value);
}
public static Integer getValue1() {
return threadLocalValue1.get();
}
public static Integer getValue2() {
return threadLocalValue2.get();
}
}
public class ThreadLocalDataMain {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
ThreadLocalData.incrementValue1();
}
System.out.println("Execution resultvalue1: "+ThreadLocalData.getValue1());
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
ThreadLocalData.incrementValue2();
}
System.out.println("Execution resultvalue2: "+ThreadLocalData.getValue2());
});
long startTime = System.currentTimeMillis();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
long endTime = System.currentTimeMillis();
System.out.println("Optimized Execution time: " + (endTime - startTime) + "ms");
}
}
执行结果及执行时间
Execution resultvalue2: 100000000
Execution resultvalue1: 100000000
Optimized Execution time: 849ms
Process finished with exit code 0
通过以上现象的观察,在不进行优化时,大概耗时在13 ~ 15秒,而进行优化后,用时为130 ~ 150ms,效率提升显著。