JUnit 4 rule and annotations to run in parallel your code in multiple threads: @MultiTest, @MultiThread, @MultyEndOfCycle, @MultyEndOfSet, @MultyBefore.
What is special - all the threads will start simultaniously and run till the end of each thread. The cycle will repeat number of times again and again. This approach guarantee highest contention to access shared properties.
Also MultiTest generates statistics for each case as a simple table with time spent for the full cycle per thread set.
This is a regular Maven project based on java.util.concurrent.Phaser
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
In this example count++ invokes 1 mln times simultaneously in 2 threads. Each cycle of start of 2 threads use the same instance of MultiTestExample
- this
. You have to put it in MultiTestRule
constructor otherwise instance will be created automatically but you never get access to it.
@MultiTest(repeatNo = 1_000_000, threadSet="2")
public class MultiTestExample {
@Rule
public MultiTestRule rule = new MultiTestRule(this);
int count;
@MultiThread
public void thread(){
count++;
}
@Test
public void test(){
if( count != 2_000_000){
System.out.printf("%s Failed: count = %,d !", rule.getResult(), count);
fail("Do you really think int increment is trade safe!?");
} else {
System.out.printf("%s Ok: count = %,d !", rule.getResult(), count);
}
}
}
Here is output for i7-3630QM 2.4Ghz (4 core):
=== MultiTestExampleRule done 1,000,000 time(s) in 266.0 mls (266.0 ns/try) === Failed: count = 1,932,125 !
Here is more detailed example
count++ invokes 1 mln times simultaneously in 1,2,3,4,5,6,7,8,9,10,12,16,32 thread sets consequently. Each set runs against the same MultiTestExampleTable
object - this
.
@MultiTest(repeatNo = 1_000_000, threadSet="1,2,3,4,5,6,7,8,9,10,12,16,32")
public class MultiTestExampleTable {
@Rule
public MultiTestRule rule = new MultiTestRule(this);
int count;
@MultiThread
public void thread(){
count++;
}
@Test
public void test(){
System.out.printf("%s\nTotally incremented = %,d\n", rule.getResult(), count);
}
}
Here is output for i7-3630QM 2.4Ghz (4 core):
=== MultiTestExampleTable done 1,000,000 time(s) ===
Threads Total OneTry OneTry(ns)
------- ---------- ---------- ----------
1 63.0 mls 63.0 ns 63.000
2 187.0 mls 187.0 ns 187.000
3 282.0 mls 282.0 ns 282.000
4 359.0 mls 359.0 ns 359.000
5 500.0 mls 500.0 ns 500.000
6 533.0 mls 533.0 ns 533.000
7 641.0 mls 641.0 ns 641.000
8 750.0 mls 750.0 ns 750.000
9 6.2 sec 6.2 mks 6170.000
10 8.0 sec 8.0 mks 8034.000
12 9.1 sec 9.1 mks 9056.000
16 5.7 sec 5.7 mks 5708.000
32 13.4 sec 13.4 mks 13422.000
------- ---------- ---------- ----------
Totally incremented = 90,249,773
It is clear now that perfomance of you code getting lower and lower in multithreded environment due to contention for increment of integer variable! For 1 thread single increment cost 63 nano seconds. For 2 concurrent threads the cost get tripled - 187 ns, for 3 threads it is 500% more - 282 ns... But it is still more over lineral up to 8 threads. For 9 threads and more the cost grows exponentially!!!
Now I get it why Gurus do not advise have more threads in your application then cores per socket of your's CPU.
I think about tool which runs above test to calculate maximum threads for proper initialization of executor pool...
PS Everything about increment in java you can find here IncrementSuite.java or in details - IncrementSuiteTable.java
Here is more complicated example
There are 3 methods defined here: thread1()
, thread2()
and thread3()
. All of them run simultaneously 1 mln time in 3 separate threads. Annotation @MultiTest
contains true
for new instance... It means before each of cycle of start 3 threads new MultiTestCycleExample
instance created for them! More over after each cycle the actual a,b,c
values will save into map in endOfCycle()
method vie @MultiEndOfCycle
annotation. Finally Util.print()
shows map of keys, percentage and actual counters.
@MultiTest(repeatNo=1_000_000, newInstance=true)
public class MultiTestCycleExample {
@Rule
public MultiTestRule rule = new MultiTestRule();
int integer;
int a, b, c;
@MultiThread
public void thread1(){
integer = 1;
a = integer;
}
@MultiThread
public void thread2(){
integer = 2;
b = integer;
}
@MultiThread
public void thread3(){
c = integer;
}
static TestUtil util = new TestUtil();
@MultiEndOfCycle
public void endOfCycle(){
util.count(a+"_"+b+"_"+c);
}
@Test
public void result(){
System.out.println(rule.getResult());
util.print();
}
}
The output for i7-3630QM 2.4Ghz (4 core) below:
=== MultiTestCycleExample done 1,000,000 time(s) in 2.7 sec ( 2.7 mks/try) ===
Key Percent Actual val
------------------ ------- ------------
1_2_1 34.2 % 342,169
1_2_2 31.8 % 317,806
1_1_0 0.0 % 32
1_2_0 34.0 % 339,872
1_1_1 0.0 % 21
2_2_2 0.0 % 12
2_2_0 0.0 % 88
------------------ ------- ------------
It is quite clear now why simple code running in 3 parallel threads must be synchronized. Frankly speacking I've expected result like this a,b,c={1,2,X}
where X=0,1,2
. But how come a,b,c={1,1,X}||{2,2,X}
? Any idea? May be volatile
will help? :)
Define 3 parameters:
repeatNo = 1 by default. How many cycles must be done for each set.
threadSet = "1" by default. How many threads must be run in parallel in each cycle. You can define many sets as "1,2,4,8,16" - means run repeatNo times 1 thread, 2 threads, ..., 16 threads in parallel.
newInstance = false by default. Should new instance be created before each cycle. False means that all cycles for all threadSet runs against same instance. True means that before each cycle new instance of @MultiTest class be created.
No parameters. Annotated method become a separate thread. Total amount of running threads in each cycle for the threadSet="3" is 3*(How many methods are annotaded by @MultiThread)
No parameters. Annotated method invokes at the end of each cycle. If repeatNo=100 then @MultyEndOfCycle method invokes 100 times for each threadSet.
No parameters. Annotated method invokes at the end of each thread set. If threadSet="1,2,4,8,16" then @MultyEndOfSet method invokes 5 times.
No parameters. Annotated method invokes just once before whole test.