Blogg
Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn
Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn
Java 25 släpptes i september 2025 som en ny LTS-version och en av de funktioner som förtjänar mer uppmärksamhet är Scoped Values (JEP 506) som blivit en färdig och permanent del av Java API:et. Den introducerades som incubator redan i Java 20 och preview i Java 21 men har nu mognat och är klar för att användas i produktion.
De flesta Java-utvecklare känner igen mönstret: du vill dela kontext — ett request-ID, en användarsession eller ett transaktions-ID — mellan metoder utan att tvingas skicka det som parameter genom hela anropskedjan. Lösningen har sedan Java 1.2 varit ThreadLocal.
Men ThreadLocal har tre stora svagheter:
ThreadLocal kan ändra dess bindning när som helst med set(). Det gör dataflödet svårt att följa och felsöka.remove(). Glömmer man det riskerar man minnesläckor och att känslig data läcker mellan förfrågningar i trådpooler.InheritableThreadLocal kopieras hela kartan av en tråds lokala värden till varje ny barntråd. Det blir snabbt en tung belastning när applikationer skapar miljontals virtuella trådar.| Egenskap | ThreadLocal |
ScopedValue |
|---|---|---|
| Datastruktur | Muterbar (set()) |
Oföränderlig (Läsbar/Read-only) |
| Livslängd | Odefinierad (kräver manuell remove()) |
Tydligt avgränsad av scopet (run/call) |
| Minnesåtgång | Kopierar data per ny barntråd | Låg (delar samma bindning i minnet) |
| Säkerhet | Alla med referens kan ändra värdet | Ingen kan ändra värdet i ett pågående scope |
En ThreadLocal lagrar ett eget värde i varje tråd. Det fungerar i traditionella trådpooler, men skalar dåligt när applikationer skapar stora mängder virtuella trådar (Project Loom).
ScopedValue är byggd för det vanligaste fallet: kontexten ska bara skickas i en riktning — från en anropande metod till de metoder den anropar.
I stället för att kopiera hela datastrukturer till nya trådar, delar ScopedValue en oföränderlig trädstruktur av bindningar. När en deluppgift startas pekar den helt enkelt tillbaka på föräldratrådens scope. Ingen datakopiering sker.
Viktig detalj vid arv av trådar: Vanliga trådar som skapas med
new Thread()ellerExecutorServiceärver inteScopedValue-bindningar. Arv sker enbart för deluppgifter som startas inom enStructuredTaskScope.
En ScopedValue låter en metod dela kontext med alla metoder den anropar, längre ned i anropskedjan, utan att skicka med parametrar. Bindningen gäller under en avgränsad körning och blir automatiskt obunden när körningen avslutas.
Själva bindningen kan inte ändras. Objektet som binds kan dock (teoretiskt) vara muterbart, så använd helst objekt som Javas record som inte kan ändras.
Grundmönstret ser ut så här:
public class Example {
private static final ScopedValue<String> TENANT_ID = ScopedValue.newInstance();
public static void main(String[] args) throws Exception {
// För operationer utan returvärde (Runnable)
ScopedValue.where(TENANT_ID, "tenant-123")
.run(Example::handleRequest);
// För operationer som returnerar ett resultat (Callable)
String result = ScopedValue.where(TENANT_ID, "tenant-456")
.call(() -> "Behandlat svar för " + TENANT_ID.get());
System.out.println(result);
}
static void handleRequest() {
System.out.println("Tenant: " + TENANT_ID.get());
}
}
Det viktiga här är run() och call(). Värdet "tenant-123" kan läsas i blocket och i alla underliggande metodanrop. Så fort run() eller call() returnerar är TENANT_ID obundet igen. Ingen risk för minnesläckor!
set()-metod — och det är en styrkaEn medveten designprincip är att ScopedValue saknar en set()-metod. Det ger vad som kallas Capability-based access control:
private static final, kan endast klassen som äger den binda eller läsa den.API:et erbjuder säkra sätt att läsa värdet:
// Standardvärde om inget är bundet
String tenant = TENANT_ID.orElse("anonymous");
// Kasta ett specifikt exception om inget är bundet
String tenant = TENANT_ID.orElseThrow(() -> new IllegalStateException("Saknar användarkontext"));
// Kontrollera om värdet är bundet innan det läses
if (TENANT_ID.isBound()) {
String tenant = TENANT_ID.get();
}
Notera: I Java 25 accepterar orElse() inte längre null som argument. Men vill man går fortfarande att binda ett faktiskt null-värde explicit med ScopedValue.where(KEY, null).
En användbar egenskap är att ScopedValue.where() kan nästlas. Ett inre block kan tillfälligt binda om samma ScopedValue till ett nytt värde.
Detta kallas skuggning (shadowing) — det fungerar precis som lokala variabler i kodblock: det inre blocket ser det nya värdet, men så fort blocket lämnas återställs det yttre värdet automatiskt, även om ett exception skulle kastas.
Exempel på rapportgenerering:
private static final ScopedValue<String> RAPPORTNIVA = ScopedValue.newInstance();
public void genereraRapport() {
ScopedValue.where(RAPPORTNIVA, "detaljerad").run(() -> {
skrivRapportHeader(); // Värdet är "detaljerad"
ScopedValue.where(RAPPORTNIVA, "sammanfattad").run(() -> {
skrivDelsummering(); // Värdet är "sammanfattad"
});
skrivRapportFooter(); // Värdet är "detaljerad" igen!
});
}
Motsvarande mönster med ThreadLocal kräver manuell hantering:
private static final ThreadLocal<String> RAPPORTNIVA_TL = new ThreadLocal<>();
String tidigare = RAPPORTNIVA_TL.get();
RAPPORTNIVA_TL.set("sammanfattad");
try {
skrivDelsummering();
} finally {
if (tidigare == null) {
RAPPORTNIVA_TL.remove();
} else {
RAPPORTNIVA_TL.set(tidigare);
}
}
Glömmer du finally-blocket i ThreadLocal-varianten kommer inte det gamla värdet att återställas. Med ScopedValue är återställningen garanterad.
ScopedValue är en permanent del av Java 25. Strukturerad concurrency (StructuredTaskScope) är däremot fortfarande i preview i Java 25 (JEP 505). För att använda StructuredTaskScope behövs därför flaggan --enable-preview.
De två funktionerna kompletterar varandra väl: delad kontext och parallella deluppgifter får en tydlig och avgränsad livslängd.
Här är ett exempel på en webbtjänst som hämtar användare och ordrar parallellt. Bägge deluppgifterna behöver kontext för loggning:
import java.util.List;
import java.util.concurrent.StructuredTaskScope;
record RequestContext(String requestId, String tenantId) {}
record User(String userId) {}
record Order(String orderId) {}
record OrderSummary(User user, List<Order> orders) {}
public class Service {
private static final ScopedValue<RequestContext> REQUEST_CTX = ScopedValue.newInstance();
public OrderSummary handleRequest(String userId, String requestId, String tenantId) throws Exception {
var ctx = new RequestContext(requestId, tenantId);
// 1. Bind kontexten
return ScopedValue.where(REQUEST_CTX, ctx).call(() -> {
try (var scope = StructuredTaskScope.open()) {
// 2. Starta deluppgifter i egna virtuella trådar.
// REQUEST_CTX ärvs automatiskt till båda uppgifterna!
var userTask = scope.fork(() -> fetchUser(userId));
var ordersTask = scope.fork(() -> fetchRecentOrders(userId));
scope.join(); // Vänta tills båda är klara
return new OrderSummary(userTask.get(), ordersTask.get());
}
});
}
private User fetchUser(String userId) {
// userId skickas som parameter (affärsdata)
// REQUEST_CTX hämtas automatiskt i log()-metoden!
log("Hämtar användare " + userId);
return new User(userId);
}
private List<Order> fetchRecentOrders(String userId) {
log("Hämtar ordrar för " + userId);
return List.of(new Order("ord-1"));
}
private void log(String message) {
// Ingen RequestContext skickades som parameter hit,
// men den finns tillgänglig i scopet!
RequestContext ctx = REQUEST_CTX.get();
System.out.printf("[%s][%s] %s%n", ctx.requestId(), ctx.tenantId(), message);
}
}
Kompilering och exekvering i Java 25 (eftersom StructuredTaskScope är preview):
javac --release 25 --enable-preview Service.java
java --enable-preview Service
(Obs: Om du enbart använder ScopedValue utan StructuredTaskScope behöver du inte --enable-preview i Java 25!)
Använd ScopedValue när:
Undvik ScopedValue när:
Scoped Values i Java 25 löser ett klassiskt problem med trådlokal data på ett säkrare, renare och betydligt mer minneseffektivt sätt än ThreadLocal.
Genom att kombinera oföränderlig kontext med tydliga exekverings-scopes minskar risken för minnesläckor och dataläckage till noll. Tillsammans med virtuella trådar och strukturerad concurrency bildar det en modern grund för samordnad programmering i Java.
Exempelkod hittas på GitHub