这里我们来搞官方的第三个案例,引入新的注解@State。
一、案例三代码
package com.levi;
/**
* @Description:
* @Author: Levi
* @Date: 2024/4/8 18:44
*/
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
// 预热注解,修改为只预热一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Warmup(iterations = 1,time = 1)
// 测试执行注解,修改为只执行一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Measurement(iterations = 1,time = 1)
public class JMHSample_03_01_States {
/*
* Most of the time, you need to maintain some state while the benchmark is
* running. Since JMH is heavily used to build concurrent benchmarks, we
* opted for an explicit notion of state-bearing objects.
* 在 JMH(Java 微基准测试工具)中,执行基准测试过程中需要维护状态是很重要的,特别是对于并发基准测试。
* JMH 提供了显式的状态对象概念来处理这一点。
*
* Below are two state objects. Their class names are not essential, it
* matters they are marked with @State. These objects will be instantiated
* on demand, and reused during the entire benchmark trial.
* 在您提供的代码片段中,有两个使用 @State 注解标记的状态对象。具体的类名并不重要,但 @State 注解是关键。
* 这些对象将根据需要进行实例化,并在整个基准测试试验过程中重复使用。基准测试可以访问@State标注类的声明的变量
*
* The important property is that state is always instantiated by one of
* those benchmark threads which will then have the access to that state.
* That means you can initialize the fields as if you do that in worker
* threads (ThreadLocals are yours, etc).
* 关键点是每个状态对象都是由其中一个基准测试线程实例化的。这确保了每个基准测试线程都可以访问自己的状态对象实例。
* 因此,您可以像在工作线程中那样初始化状态对象的字段。这意味着您可以在状态对象中使用 ThreadLocal 或任何其他线程特定的构造。
* 就是本地变量,线程安全。
*/
@State(Scope.Benchmark)
public static class BenchmarkState {
volatile double x = Math.PI;
}
@State(Scope.Thread)
public static class ThreadState {
volatile double x = Math.PI;
}
/*
* Benchmark methods can reference the states, and JMH will inject the
* appropriate states while calling these methods. You can have no states at
* all, or have only one state, or have multiple states referenced. This
* makes building multi-threaded benchmark a breeze.
* 基准方法可以引用这些状态,并且在调用这些方法时,JMH将注入适当的状态。您可以完全没有状态,或者只有一个状态,或者引用多个状态。
* 这使得构建多线程基准测试变得非常简单。
*
* For this exercise, we have two methods.
* 对于这个练习,我们有两个方法。
*/
@Benchmark
public void measureUnshared(ThreadState state) {
// All benchmark threads will call in this method.
// 所有的测试线程都会调用这个方法
// However, since ThreadState is the Scope.Thread, each thread,will have it's own copy of the state,
// and this benchmark will measure unshared case.
// 然而,由于ThreadState的作用域是Scope.Thread,每个线程将拥有自己的状态副本,因此这个基准将测量不共享状态的情况。
// 也就是各自操作的对象是复制了一份,不是操作的共享变量,也就是线程安全。Scope.Thread作用域是在线程本地。
// 换言之就是四个线程每个线程来调用的时候,都会自己创建一个ThreadState,不共享
state.x++;
}
@Benchmark
public void measureShared(BenchmarkState state) {
// All benchmark threads will call in this method.
//所有基准线程都将调用此方法。
// Since BenchmarkState is the Scope.Benchmark, all threads will share the state instance,
// and we will end up measuring shared case.
// 由于BenchmarkState的作用域是Scope.Benchmark,所有线程将共享状态实例,并且我们最终将测量共享情况。
// 也就是测试启动的多个线程,是操作这个共享变量的Scope.Benchmark这个作用域是再所有的Benchmark方法的
state.x++;
}
/*
* You are expected to see the drastic difference in shared and unshared cases,
* because you either contend for single memory location, or not. This effect
* is more articulated on large machines.
* 预计会看到共享和非共享情况之间的显著差异,因为您要么竞争共享内存位置,也就是共享的时候存在并发竞争,不共享的不存在。
* 这种效应在大型机器上更为显著。
*/
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_03_01_States.class.getSimpleName())
.threads(4) // 启动四个线程进行基准测试
.forks(1)
.build();
new Runner(opt).run();
}
}
二、@State
我们依然先不急着跑程序,我们看下这个代码。
@State(Scope.Benchmark)
public static class BenchmarkState {
volatile double x = Math.PI;
}
@State(Scope.Thread)
public static class ThreadState {
volatile double x = Math.PI;
}
这里声明了两个类,一个是BenchmarkState ,他的注解是@State(Scope.Benchmark),他的作用范围是所有的Benchmark方法。
另一个是ThreadState ,他的注解是@State(Scope.Thread),他的作用范围是每个线程自己的。
然后呢,我们的两个基准测试方法。
@Benchmark
public void measureUnshared(ThreadState state)
这个方法的参数是哪个线程独享范围的state类。
@Benchmark
public void measureShared(BenchmarkState state)
这个方法的参数是哪个所有Benchmark都共享的state类。
然后呢,我们会启动多个线程来启动基准测试,每个线程都会去调用这两个@Benchmark方法,不是一部分调一个,另一部分调另一个,是都会来调用。
只是调用的时候,Scope.Thread的那个类和他的变量会在每个线程都自己创建一个,他是不线程共享的。
而Scope.Benchmark这个类和他的变量是全局就一个,每个线程操作的是同一个。
所以就会产生一个结果就是:
@State(Scope.Benchmark)
public static class BenchmarkState {
volatile double x = Math.PI;
}这个方法其实是多线程竞争操作一个对象BenchmarkState的变量x,会产生并发竞争。
@State(Scope.Thread)
public static class ThreadState {
volatile double x = Math.PI;
}这个是多个线程操作的都是自己本地创建的。他们不存在并发竞争。
而且你会注意,他的每个变量x,都是加着volatile 的,他还要保证可见性在共享并发操作的时候。
现在你差不多明白了吧,这是个分别测试并发修改共享变量和每个线程各自改自己的本地变量的case。
那么不出意外的话,并发修改那指定效率不如那个共享的,因为有线程的切换,可见性的保证等等,cpu的竞争。
三、测试结果
四、@DefaultState
这个例子其实是官方文档的第四个例子,我们拿到这里,其实和上面那个一样的。只是这种把状态变量声明到自己本身里面了,没单独搞新的类。
package com.levi;
/**
* @Description:
* @Author: Levi
* @Date: 2024/4/8 19:23
*/
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
/*
* Fortunately, in many cases you just need a single state object.
* In that case, we can mark the benchmark instance itself to be
* the @State. Then, we can reference its own fields as any
* Java program does.
* 幸运的是,在许多情况下,您只需要一个单独的状态对象。在这种情况下,我们可以将基准测试实例本身标记为 @State。
* 然后,我们可以像任何 Java 程序那样引用它自己的字段。其实就是把原来单独声明变量的类,现在是放在自己本身里面当成一个变量。这里也可以搞多个,你可以再写个y,不是只能写一个x。
*
*/
@State(Scope.Thread)// 这里也可以标记,表示这个状态值是线程独享,也就是每个线程都创建
public class JMHSample_04_01_DefaultState {
double x = Math.PI;
/**
这里就等价于下面,只是没有这个语法而已。
@Benchmark
public void measure(JMHSample_04_01_DefaultState state) {
state.x++;
}
*/
@Benchmark
public void measure() {
x++;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_04_01_DefaultState.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
五、总结
没总结的,注释非常明确了。