Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
51f8683
Align masks with Posix policy
peter-lawrey Oct 26, 2025
0203c7e
Enable AsciiDoc section numbering
peter-lawrey Oct 26, 2025
ebdb666
Introduce quality profile and static-analysis fixes
peter-lawrey Oct 27, 2025
26276a2
Set realistic coverage gates
peter-lawrey Oct 27, 2025
7888ee7
Enable automatic section numbering in requirements doc
peter-lawrey Oct 27, 2025
a26ea4f
Add code-review config files
peter-lawrey Oct 27, 2025
0595028
Refine affinity module checks and add tests
peter-lawrey Oct 28, 2025
4cfdbde
Add cross-OS stubs and tests for affinity facades
peter-lawrey Oct 28, 2025
19e804c
Restore real affinity runs by scoping stub flags
peter-lawrey Oct 28, 2025
6bfa3ea
Refactor code comments and formatting for clarity and consistency
peter-lawrey Oct 28, 2025
f915305
Fix formatting for C/C++ references in requirements and README documents
peter-lawrey Oct 28, 2025
829afc8
Fix formatting for C/C++ references in requirements and README documents
peter-lawrey Oct 28, 2025
82a7a07
Align spotbugs annotations with JDK8
peter-lawrey Oct 28, 2025
29b7f4a
Refactor SpotBugs exclusions and remove deprecated @SuppressFBWarning…
peter-lawrey Oct 29, 2025
378ff5e
Create Posix-backed shared test module
peter-lawrey Oct 29, 2025
3b8d20a
Duplicate CPU mask utilities and keep in sync with Posix
peter-lawrey Oct 29, 2025
902b4a8
Suppress SpotBugs naming warnings for Windows stub
peter-lawrey Oct 29, 2025
8aabe21
Route Windows stub exclusions through SpotBugs filter
peter-lawrey Oct 29, 2025
b8a0a6d
Remove unused SpotBugs annotations dependency from pom.xml
peter-lawrey Oct 29, 2025
dc7562e
Move Checkstyle config under src/main/config
peter-lawrey Oct 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions LICENSE.adoc
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
== Copyright 2016-2025 chronicle.software

Licensed under the *Apache License, Version 2.0* (the "License");
you may not use this file except in compliance with the License.
Licensed under the *Apache License, Version 2.0* (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
20 changes: 16 additions & 4 deletions README.adoc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
= Thread Affinity
:sectnums:

image::docs/images/Thread-Affinity_line.png[width=20%]

Expand All @@ -7,9 +8,11 @@ image::docs/images/Thread-Affinity_line.png[width=20%]
[#image-maven]
[caption="",link=https://maven-badges.herokuapp.com/maven-central/net.openhft/affinity]
image::https://maven-badges.herokuapp.com/maven-central/net.openhft/affinity/badge.svg[]

image:https://javadoc.io/badge2/net.openhft/affinity/javadoc.svg[link="https://www.javadoc.io/doc/net.openhft/affinity/latest/index.html"]

== Overview

Lets you bind a thread to a given core, this can improve performance (this library works best on linux).

OpenHFT Java Thread Affinity library
Expand Down Expand Up @@ -62,6 +65,7 @@ To work around this problem, fork the repository, and override the `<version>` t
Or download jna.jar and jna-platform.jar from the JNA project and add them to your classpath.

=== How does CPU allocation work?

The library will read your `/proc/cpuinfo` if you have one or provide one and it will determine your CPU layout.
If you don't have one it will assume every CPU is on one CPU socket.

Expand All @@ -79,6 +83,7 @@ For example:
* `-Daffinity.reserved=2` reserves only CPU `1`.
* `-Daffinity.reserved=6` reserves CPUs `1` and `2`.
* `-Daffinity.reserved=10` reserves CPUs `1` and `3` (hexadecimal `a`).
* `-Daffinity.reserved=1_0000_0000` reserves CPU `64` on systems with more than sixty-four logical CPUs (underscore shown for readability only).

Use an appropriate mask when starting each process to avoid reserving the same cores for multiple JVMs.

Expand Down Expand Up @@ -121,6 +126,7 @@ sudo reboot
== Using AffinityLock

=== Acquiring a CPU lock for a thread

You can acquire a lock for a CPU in the following way:

.In Java 6
Expand All @@ -145,6 +151,7 @@ try (AffinityLock al = AffinityLock.acquireLock()) {
You have further options such as

=== Acquiring a CORE lock for a thread

You can reserve a whole core.
If you have hyper-threading enabled, this will use one CPU and leave it's twin CPU unused.

Expand Down Expand Up @@ -174,11 +181,12 @@ try (final AffinityLock al = AffinityLock.acquireLock()) {
});
t.start();
}
----
----

In this example, the library will prefer a free CPU on the same Socket as the first thread, otherwise it will pick any free CPU.

=== Affinity strategies

The `AffinityStrategies` enum defines hints for selecting a CPU relative to an existing lock.

[options="header",cols="1,3"]
Expand All @@ -193,6 +201,7 @@ The `AffinityStrategies` enum defines hints for selecting a CPU relative to an e
|===

=== Getting the thread id

You can get the current thread id using

[source,java]
Expand All @@ -201,6 +210,7 @@ int threadId = AffinitySupport.getThreadId();
----

=== Determining which CPU you are running on

You can get the current CPU being used by

[source,java]
Expand All @@ -215,14 +225,14 @@ The affinity of the process on start up is
[source,java]
----
long baseAffinity = AffinityLock.BASE_AFFINITY;
----
----

The available CPU for reservation is

[source,java]
----
long reservedAffinity = AffinityLock.RESERVED_AFFINITY;
----
----

If you want to get/set the affinity directly you can do

Expand Down Expand Up @@ -319,8 +329,10 @@ For an article on how much difference affinity can make and how to use it http:/
== Questions and Answers

=== Question: How to lock a specific cpuId

I am currently working on a project related to deadlock detection in multithreaded programs in java.
We are trying to run threads on different processors and thus came across your github posts regarding the same. https://github.com/peter-lawrey/Java-Thread-Affinity/wiki/Getting-started
We are trying to run threads on different processors and thus came across your github posts regarding the same.
https://github.com/peter-lawrey/Java-Thread-Affinity/wiki/Getting-started
Being a beginner, I have little knowledge and thus need your assistance.
We need to know how to run threads on specified cpu number and then switch threads when one is waiting.

Expand Down
36 changes: 36 additions & 0 deletions affinity-posix-test-support/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>net.openhft</groupId>
<artifactId>Java-Thread-Affinity</artifactId>
<version>3.27ea2-SNAPSHOT</version>
</parent>

<artifactId>affinity-posix-test-support</artifactId>
<name>OpenHFT/Java-Thread-Affinity/posix-test-support</name>
<description>Shared test fixtures for Java Thread Affinity built on the Posix library</description>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>net.openhft</groupId>
<artifactId>affinity</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>net.openhft</groupId>
<artifactId>posix</artifactId>
<version>2.27ea3-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package net.openhft.affinity.posix.testsupport;

import net.openhft.posix.PosixAPI;

/**
* Lightweight bridge for JVMs that want to share Posix-backed test fixtures.
* This module keeps Posix on the classpath beside the affinity code base,
* enabling future reuse without forcing the main runtime code to depend on it yet.
*/
public final class PosixTestSupport {

private PosixTestSupport() {
// utility
}

/**
* Returns a Posix API handle if one can be loaded, otherwise null.
* Tests can use this to decide whether to exercise Posix-backed behaviour.
*/
public static PosixAPI tryLoadPosix() {
try {
return PosixAPI.posix();
} catch (Throwable ignored) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package net.openhft.posix.internal.util;

import java.util.Arrays;
import java.util.BitSet;

/**
* DUPLICATED with net.openhft.affinity.internal.duplicated.CpuMaskConversion.
* <p>
* This class is housed in the shared test-support module for now so that both the Posix and
* Java Thread Affinity code paths can evolve independently while staying in sync. Once Posix
* ships the canonical implementation this duplicate should be removed and the affinity module
* should consume it directly.
*/
public final class CpuMaskConversion {

private CpuMaskConversion() {
}

public static int requiredBytesForLogicalProcessors(int logicalProcessors) {
long processors = Math.max(1L, logicalProcessors);
long groups = (processors + Long.SIZE - 1) / Long.SIZE;
long bytes = Math.max(1L, groups) * Long.BYTES;
if (bytes > Integer.MAX_VALUE) {
throw new IllegalArgumentException("CPU mask size exceeds integer addressable space");
}
return (int) bytes;
}

public static int requiredBytesForMask(BitSet mask, int logicalProcessorsHint) {
int requiredBits = Math.max(1, Math.max(mask.length(), logicalProcessorsHint));
return requiredBytesForLogicalProcessors(requiredBits);
}

public static void writeMask(BitSet affinity, byte[] target) {
Arrays.fill(target, (byte) 0);
byte[] source = affinity.toByteArray();
System.arraycopy(source, 0, target, 0, Math.min(source.length, target.length));
}

public static BitSet readMask(byte[] source) {
return BitSet.valueOf(source);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package net.openhft.affinity.posix.testsupport;

import net.openhft.posix.PosixAPI;
import org.junit.Test;

import java.util.BitSet;
import java.util.Random;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class PosixTestSupportTest {

@Test
public void canAttemptToLoadPosix() {
PosixAPI api = PosixTestSupport.tryLoadPosix();
assertNotNull("PosixAPI handle should be returned when the runtime offers an implementation", api);
}

@Test
public void cpuMaskConversionsStayInSync() {
int[] processorCounts = {0, 1, 2, 7, 8, 31, 32, 33, 63, 64, 65, 255, 256};
for (int count : processorCounts) {
int affinityBytes = net.openhft.affinity.internal.duplicated.CpuMaskConversion.requiredBytesForLogicalProcessors(count);
int posixBytes = net.openhft.posix.internal.util.CpuMaskConversion.requiredBytesForLogicalProcessors(count);
assertEquals("Byte requirement mismatch for processor count " + count, affinityBytes, posixBytes);
}

Random random = new Random(1234L);
for (int logicalHint : processorCounts) {
BitSet mask = randomBitSet(random, logicalHint + 32);
int affinityBytes = net.openhft.affinity.internal.duplicated.CpuMaskConversion.requiredBytesForMask(mask, logicalHint);
int posixBytes = net.openhft.posix.internal.util.CpuMaskConversion.requiredBytesForMask(mask, logicalHint);
assertEquals("Mask byte requirement mismatch", affinityBytes, posixBytes);

byte[] affinityBuffer = new byte[affinityBytes];
byte[] posixBuffer = new byte[posixBytes];
net.openhft.affinity.internal.duplicated.CpuMaskConversion.writeMask(mask, affinityBuffer);
net.openhft.posix.internal.util.CpuMaskConversion.writeMask(mask, posixBuffer);
assertEquals("Written mask mismatch", toBitString(affinityBuffer), toBitString(posixBuffer));

BitSet fromAffinity = net.openhft.affinity.internal.duplicated.CpuMaskConversion.readMask(affinityBuffer);
BitSet fromPosix = net.openhft.posix.internal.util.CpuMaskConversion.readMask(posixBuffer);
assertEquals("Reconstructed mask mismatch", fromAffinity, fromPosix);
}
}

private static BitSet randomBitSet(Random random, int maxBits) {
BitSet bitSet = new BitSet(maxBits);
for (int i = 0; i < maxBits; i++) {
if (random.nextBoolean()) {
bitSet.set(i);
}
}
return bitSet;
}

private static String toBitString(byte[] data) {
StringBuilder sb = new StringBuilder(data.length * 8);
for (byte b : data) {
for (int i = 0; i < 8; i++) {
sb.append((b >> i) & 1);
}
}
return sb.toString();
}
}
Loading