interface Node { }
record Number(long number) implements Node { }
record Negate(Node node) implements Node { }
record Absolute(Node node) implements Node { }
record Add(List<Node> summands) implements Node { }
Project Amber |
Project Panama |
Project Valhalla |
Project Loom |
Slides at slides.nipafx.dev.
Smaller, productivity-oriented Java language features
Profile:
project / wiki / mailing list
launched March 2017
led by Brian Goetz
Some downsides of Java:
can be cumbersome
tends to require boilerplate
situational lack of expressiveness
Amber continuously improves that situation.
Pattern matching:
switch expressions
type pattern matching
sealed types
records
Misc:
var
text blocks
Evaluating simple arithmetic expressions.
1 + (-2) + |3 + (-4)|
interface Node { }
record Number(long number) implements Node { }
record Negate(Node node) implements Node { }
record Absolute(Node node) implements Node { }
record Add(List<Node> summands) implements Node { }
Evaluating simple arithmetic expressions.
1 + (-2) + |3 + (-4)|
Canonical way to apply operations
to a type hierarchy:
Polymorphism
interface Node {
long evaluate();
}
record Number(long number) implements Node {
long evaluate() {
return number;
}
}
record Negate(Node node) implements Node {
long evaluate() {
return -node.evaluate();
}
}
record Absolute(Node node) implements Node {
long evaluate() {
long result = node.evaluate();
return result < 0 ? -result : result;
}
}
record Add(List<Node> summands) implements Node {
long evaluate() {
return summands.stream()
.mapToLong(Node::evaluate)
.sum();
}
}
Should you implement evaluate
this way?
Probably.
But what about:
Resources estimateResourceUsage()
Strategy createComputationStrategy()
Invoice createInvoice(User user)
String prettyPrint()
(like here)
void draw(Direction d, Style s, Canvas c)
⇝ Central abstractions can be overburdened.
Separating a hierarchy from operations
is a case for the visitor pattern.
Alternative: pattern matching over sealed types.
Seal type hierarchy:
sealed interface Node
permits Number, Negate, Absolute, Add { }
record Number(long number) implements Node { }
record Negate(Node node) implements Node { }
record Absolute(Node node) implements Node { }
record Add(List<Node> summands) implements Node { }
Use type patterns in switch (JEP 420 / 2nd preview in 18):
long evaluate(Node node) {
return switch (node) {
case Number no -> no.number();
case Negate neg -> -evaluate(neg.node());
case Absolute abs && evaluate(abs.node()) < 0
-> -evaluate(abs.node());
case Absolute abs -> evaluate(abs.node());
case Add add -> add
.summands().stream()
.mapToLong(this::evaluate)
.sum();
// no default branch needed
};
}
Also use deconstruction patterns (JEP 405 / not targeted):
long evaluate(Node node) {
return switch (node) {
case Number(long no) -> no;
case Negate(var n) -> -evaluate(n);
case Absolute(var n) && evaluate(n) < 0
-> -evaluate(n);
case Absolute(var n) -> evaluate(n);
case Add(var summands) -> summands.stream()
.mapToLong(this::evaluate)
.sum();
// no default branch needed
};
}
records + sealed types + patterns = data-oriented programming
In Data Oriented programming, we model our domain using data collections, that consist of immutable data. We manipulate the data via functions that could work with any data collection.
When parsing outside data,
types are often general
(think JsonNode
).
Consider pattern matching
to tease apart the data.
Possible future changes:
template strings (white paper)
concise method bodies (JEP draft)
serialization revamp (white paper)
makes Java more expressive
reduces amount of code
makes us more productive
My personal (!) guesses (!!):
patterns in switch finalized
deconstruction patterns preview
template strings preview
more patterns preview
🎥 Java Language Futures: All Aboard Project Amber
(Nov 2017)
🎥 Java Language Futures: Late 2021 Edition (Sep 2021)
🎥 Pattern Matching in Java (17) (Sep 2021)
🎥 State of Pattern Matching with Brian Goetz (Feb 2022)
Interconnecting JVM and native code
Profile:
launched July 2014
led by Maurizio Cimadamore
vector API
foreign memory API
foreign function API
Given two float
arrays a
and b
,
compute c = - (a² + b²)
:
void compute(float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}
Vectorization - modern CPUs:
have multi-word registers (e.g. 512 bit)
can store several numbers (e.g. 16 float
s)
can execute several computations at once
⇝ Single Instruction, multiple data (SIMD)
Just-in-time compiler tries to vectorize loops.
⇝ Auto-vectorization
Works but isn’t reliable.
static final VectorSpecies<Float> VS =
FloatVector.SPECIES_PREFERRED;
void compute(float[] a, float[] b, float[] c) {
int upperBound = VS.loopBound(a.length);
for (i = 0; i < upperBound; i += VS.length()) {
var va = FloatVector.fromArray(VS, a, i);
var vb = FloatVector.fromArray(VS, b, i);
var vc = va.mul(va)
.add(vb.mul(vb))
.neg();
vc.intoArray(c, i);
}
}
Properties:
clear and concise API (given the requirements)
platform agnostic
reliable run-time compilation and performance
graceful degradation
Storing data off-heap is tough:
ByteBuffer
is limited (2GB) and inefficient
Unsafe
is… unsafe and not supported
JNI isn’t ideal:
involves several tedious artifacts (header file, impl, …)
can only interoperate with languages that align
with OS/architecture the JVM was built for
doesn’t reconcile Java/C type systems
Safe and performant foreign-memory API:
to allocate:
MemorySegment
, MemoryAddress
, SegmentAllocator
to access/manipulate: MemoryLayout
, VarHandle
control (de)allocation: MemorySession
Streamlined tooling/API for foreign functions
based on method handles:
classes to call foreign functions
CLinker
, FunctionDescriptor
, NativeSymbol
jextract
: generates method handles from header file
Official plans:
foreign function/memory API previews (JEP 424)
Vector API needs to wait for Valhalla’s
primitive types and universal generics.
Vector APIs:
Foreign APIs:
📝 design documents
🎥 Panama Update with Maurizio Cimadamore (Jul 2019)
🎥 ByteBuffers are dead, long live ByteBuffers! (Feb 2020)
🎥 The State of Project Panama with Maurizio Cimadamore (Jun 2021)
Advanced Java VM and Language feature candidates
Profile:
launched July 2014
led by Brian Goetz and John Rose
Java has a split type system:
primitives
classes
Potential downsides:
no class-building techniques
no custom primitives
can’t use with generics
Forced move in 90s to allow for
acceptable numeric performance.
Potential downsides:
mutable by default
memory access indirection
extra memory for header
allow locking and other
identity-based operations
All custom types come with identity:
mutability
layout polymorphism
equal
but !=
synchronization
etc.
All custom types come as references:
nullability
protect from tearing
But not all custom types need that!
Valhalla’s goals is to unify the type system:
value types (disavow identity)
primitive types (disavow references)
universal generics (ArrayList<int>
)
specialized generics (backed by int[]
)
value class RationalNumber {
private long nominator;
private long denominator;
// constructor, etc.
}
Codes (almost) like a class - exceptions:
class and fields are implcitly final
superclasses are limited
No identity:
some runtime operations
(e.g. synchronization)
throw exceptions
==
compares by state
References:
null
is default value
no tearing
guaranteed immutability
more expressiveness
more optimizations
The JDK (as well as other libraries) has many value-based classes, such as
Optional
andLocalDateTime
. […] We plan to migrate many value-based classes in the JDK to value classes.
primitive class ComplexNumber {
private long rational;
private long irratoinal;
// constructor, etc.
}
Codes (almost) like a value class - exception:
no field of own type
(i.e. no circularity)
No identity (like value types).
No references:
default value has all fields set to their defaults
can tear under concurrent assignment
Benefit:
performance comparable to that of today’s primitives!
Sometimes, even int
needs to be a reference:
nullability
non-tearability
self-reference
So we box to Integer
.
What about ComplexNumber
?
Each primitive class P
declares two types:
P
: as discussed so far
P.ref
: behaves like a value type
primitive class Node<T> {
T value;
Node.ref<T> nextNode;
}
[W]e want to adjust the basic primitives (
int
,double
, etc.) to behave as consistently with new primitives as possible.
On the example of int
/Integer
:
declare int
as primitive class
alias Integer
with int.ref
remove Integer
When everybody creates their own values and primitives,
boxing becomes omni-present and very painful!
Universal generics allow value/primitive
classes as type parameters:
List<long> ids = new ArrayList<>();
List<RationalNumber> numbers = new ArrayList<>();
Healing the rift in the type system is great!
But if ArrayList<int>
is backed by Object[]
,
it will still be avoided in many cases.
Specialized generics will fix that:
Generics over primitives will avoid references!
Value and primitive types plus
universal and specialized generics:
fewer trade-offs between
design and performance
no more manual specializations
better performance
can express design more clearly
more robust APIs
Makes Java more expressive and performant.
📝 State of Valhalla
🎥 The State of Project Valhalla with Brian Goetz (Aug 2021)
🎥 Valhalla Update with Brian Goetz (Jul 2019)
There’s much more going on!
Performance:
Security:
context-specific deserialization filters ⑰ (JEP 415)
Edwards-Curve Digital Signature Algorithm ⑮ (JEP 339)
ongoing enhancements (Sean Mullan’s blog)
To ease migrations:
stick to supported APIs
stick to standardized behavior
stick to well-maintained projects
keep dependencies and tools up to date
stay ahead of removals (jdeprscan
)
build on each release (including EA)
Then you, too, can enjoy these projects ASAP!
Java 11 is slowly but resolutely overtakes Java 8
adoption of 17 (from 11) looks good
always using latest is uncommon but persistent
JVM features and APIs for supporting easy-to-use, high-throughput, lightweight concurrency and new programming models
Profile:
launched January 2018
led by Ron Pressler
Imagine a hypothetical HTTP request:
interpret request
query database (blocks)
process data for response
JVM resource utilization:
good for 1. and 3.
really bad for 2.
How to implement that request?
Align application’s unit of concurrency (request)
with Java’s unit of concurrency (thread):
use thread per request
simple to write, debug, profile
blocks threads on certain calls
limited number of platform threads
⇝ bad resource utilization
⇝ low throughput
Only use threads for actual computations:
use non-blocking APIs
(with futures / reactive streams)
harder to write, challenging to debug/profile
incompatible with synchronous code
shares platform threads
⇝ great resource utilization
⇝ high throughput
Resolve the conflict between:
simplicity
throughput
A virtual thread:
is a regular Thread
low memory footprint ([k]bytes)
small switching cost
scheduled by the Java runtime
The JVM manages virtual threads:
runs them on a pool of carrier threads
makes them yield on blocking calls
(frees the carrier thread!)
continues them when calls return
Remember the hypothetical request:
interpret request
query database (blocks)
process data for response
In a virtual thread:
JVM submits task to carrier thread pool
when 2. blocks, virtual thread yields
JVM hands carrier thread back to pool
when 2. unblocks, JVM resubmits task
virtual thread continues with 3.
try (var executor = Executors
.newVirtualThreadPerTaskExecutor()) {
IntStream
.range(0, 10_000)
.forEach(number -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return number;
});
});
} // executor.close() is called implicitly, and waits
void handle(Request request, Response response)
throws InterruptedException {
try (var executor = Executors
.newVirtualThreadPerTaskExecutor()) {
var futureA = executor.submit(this::taskA);
var futureB = executor.submit(this::taskB);
response.send(futureA.get() + futureB.get());
} catch (ExecutionException ex) {
response.fail(ex);
}
}
Virtual threads aren’t "faster threads":
Each task takes the same time (same latency).
So why bother?
Parallelism | Concurrency | |
---|---|---|
Task origin | solution | problem |
Control | developer | environment |
Resource use | coordinated | competitive |
Metric | latency | throughput |
Abstraction | CPU cores | tasks |
# of threads | # of cores | # of tasks |
When workload is not CPU-bound:
start waiting as early as possible
for as many tasks as possible
⇝ Virtual threads increase throughput:
when number of concurrent tasks is high
when workload is not CPU-bound
Virtual threads are cheap and plentiful:
no pooling necessary
allows thread per task
allows liberal creation
of threads for subtasks
⇝ Enables new concurrency programming models.
Structured programming:
prescribes single entry point
and clearly defined exit points
influenced languages and runtimes
Simlarly, structured concurrency prescribes:
When the flow of execution splits into multiple concurrent flows, they rejoin in the same code block.
When the flow of execution splits into multiple concurrent flows, they rejoin in the same code block.
⇝ Threads are short-lived:
start when task begins
end on completion
⇝ Enables parent-child/sibling relationships
and logical grouping of threads.
void handle(Request request, Response response)
throws InterruptedException {
try (var executor = Executors
.newVirtualThreadPerTaskExecutor()) {
// what's the relationship between
// this and the two spawned threads?
// what happens when one of them fails?
var futureA = executor.submit(this::taskA);
var futureB = executor.submit(this::taskB);
// what if we only need the faster one?
response.send(futureA.get() + futureB.get());
} catch (ExecutionException ex) {
response.fail(ex);
}
}
void handle(Request request, Response response)
throws InterruptedException {
// define explicit success/error handling
try (var scope = new StructuredTaskScope
.ShutdownOnFailure()) {
var futureA = scope.fork(this::taskA);
var futureB = scope.fork(this::taskB);
// wait explicitly until success criteria met
scope.join();
scope.throwIfFailed();
response.send(futureA.get() + futureB.get());
} catch (ExecutionException ex) {
response.fail(ex);
}
}
forked tasks are children of the scope
creates relationship between threads
success/failure policy can be defined
across all children
Virtual threads:
code is simple to write, debug, profile
high throughput
new programing model
Structured concurrency:
clearer concurrency code
simpler failure/success policies
better debugging
My personal (!) guesses (!!):
virtual threads preview
structured concurrency API preview
more structured concurrency APIs (?)