Java: Threads - The Basics Before Going Reactive

Understanding how Java handles multiple things at once — and how Quarkus changes the game.

In my Back to Java journey, I’ve been rediscovering parts of the language that I used years ago but never really understood deeply.
Today’s topic is one of the most fundamental — and honestly, one of the most misunderstood: Threads.

Threads are the foundation of concurrency in Java.
And before I dive deeper into Quarkus’ reactive and non-blocking world, I want to understand what happens underneath.


What Are Threads (in general)?

A thread is like a worker inside your program — an independent flow of execution that can run code in parallel with others.

When you run a Java program, it always starts with one main thread:

public class MainExample {
    public static void main(String[] args) {
        System.out.println("Running in: " + Thread.currentThread().getName());
    }
}

Output:

Running in: main

That’s your main worker.
Every additional thread you create runs alongside it.

Creating Threads in Java

There are two common ways to create threads:

1. Extend the Thread class

public class MyThread extends Thread {
    public void run() {
        System.out.println("Running in: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start(); // Start a new thread
        System.out.println("Main: " + Thread.currentThread().getName());
    }
}
  • start() creates a new thread and calls run() asynchronously.
  • If you call run() directly, it just runs on the same thread — no concurrency.

2. Use a Runnable (preferred approach)

public class RunnableExample {
    public static void main(String[] args) {
        Runnable task = () -> System.out.println("Running in: " + Thread.currentThread().getName());
        Thread thread = new Thread(task);
        thread.start();
    }
}

Runnable separates the work (the code you want to run) from the worker (the Thread).

Threads in Practice: A Quick Hands-On

Let’s simulate doing two things at once — something slow, like making coffee ☕ and toasting bread 🍞.

public class Breakfast {
    public static void main(String[] args) {
        Thread coffee = new Thread(() -> makeCoffee());
        Thread toast = new Thread(() -> toastBread());

        coffee.start();
        toast.start();
    }

    static void makeCoffee() {
        try {
            Thread.sleep(2000);
            System.out.println("☕ Coffee is ready!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static void toastBread() {
        try {
            Thread.sleep(3000);
            System.out.println("🍞 Bread is toasted!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Output (order may vary):

☕ Coffee is ready!
🍞 Bread is toasted!

Both tasks run at the same time — this is concurrency in action.
Each task gets its own thread, and the program doesn’t wait for one to finish before starting the other.

Thread Pools: Managing Many Tasks

Creating new threads every time can be expensive.
A better way is to reuse threads using an ExecutorService (a thread pool).

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 5; i++) {
            int taskId = i;
            pool.submit(() ->
                System.out.println("Running task " + taskId + " on " + Thread.currentThread().getName())
            );
        }

        pool.shutdown();
    }
}

This allows multiple tasks to share a limited number of threads efficiently — something frameworks like Quarkus also do under the hood.

Java Threads vs JavaScript Async (Same Goal, Different Path)

Java uses multiple threads to handle concurrency.

JavaScript (and TypeScript) runs on a single thread but uses an event loop to handle async operations (like fetch, setTimeout, etc.).

Feature Java JavaScript / TypeScript
Execution Model Multi-threaded Single-threaded with event loop
Parallel Execution Yes (real threads) No (cooperative async via callbacks/promises)
Example new Thread() setTimeout(), fetch(), Promise
Blocking Behavior Can block (Thread.sleep) Never blocks (async callback queued)

💡 So both aim to “do many things at once,” but JavaScript uses asynchronous scheduling, while Java can literally run multiple threads in parallel.

Threads and Quarkus: Same Idea, Smarter Execution

Quarkus applications still use threads — but they’re used more efficiently.

Traditional Java apps (like Spring or Servlet-based apps) use a thread per request model:

  • Every incoming HTTP request gets its own thread.
  • If a thread waits for a DB or network call, it’s blocked.

Quarkus, on the other hand, is built for non-blocking I/O:

  • Threads don’t sit idle waiting for I/O.
  • A small pool of threads can handle thousands of concurrent requests.
  • Reactive frameworks (like Mutiny and Vert.x) schedule work when data arrives — instead of holding a thread hostage.
Concept Traditional Java Quarkus Reactive
Model Thread per request Event-driven, non-blocking
Threads used Many Few (efficient reuse)
Behavior Blocking Reactive
Library Thread, ExecutorService Uni, Multi (Mutiny)

👉 So, threads are still there — Quarkus just uses them smarter.

Real-Life Scenario: 1000 Requests Hitting the Same Endpoint

Imagine this common situation:
You’ve built an endpoint /users/profile that calls an external user service (maybe a third-party authentication API).
Sometimes it’s fast (50 ms), sometimes slow (2 s).
Now — 1000 users open the app at once.

Traditional Java (Blocking I/O)

@Path("/users")
public class UserResource {

    @GET
    @Path("/profile")
    public String getProfile() {
        // Blocking call to external API
        String user = externalService.getUserData(); 
        return "Profile: " + user;
    }
}
  • Each request is assigned its own thread from a pool (say, 200 threads).
  • The thread makes the external call and then waits.
  • Once all 200 threads are busy, new requests are queued until one is free.

If 1000 requests come in:

  • 200 active threads, 800 waiting.
  • Threads consume memory even while idle.
  • Response time grows as requests queue up.
  • CPU usage stays low (most threads are sleeping).

Quarkus (Reactive / Non-Blocking I/O)

@Path("/users")
public class ReactiveUserResource {

    @GET
    @Path("/profile")
    public Uni<String> getProfile() {
        return externalService.getUserDataAsync() // returns Uni<String>
            .onItem().transform(user -> "Profile: " + user)
            .onFailure().recoverWithItem("Fallback profile");
    }
}
  • The call is non-blocking — it sends the HTTP request and releases the thread.
  • The same few event-loop threads handle other requests while waiting for responses.
  • When the API responds, the result is processed asynchronously.

If 1000 requests come in:

  • The same 8–16 event-loop threads can manage them all.
  • No threads sit idle; they’re constantly reused.
  • Memory footprint stays low, and response times remain stable even if some APIs are slow.
Step Traditional Java (Blocking) Quarkus Reactive (Non-Blocking)
Request arrives Takes a thread from pool Uses small event-loop thread
External call Thread waits Thread released immediately
While waiting Idle Reused for other requests
Response arrives Thread resumes work Async callback resumes pipeline
1000 concurrent requests Needs ~1000 threads (or queues) Same small pool handles all

💬 In short:

Traditional Java: many threads that spend most time waiting.
Quarkus: fewer threads that never wait — they react when data is ready.

What This Means in Real Life

  • Predictable performance: Even if a third-party API slows down, Quarkus keeps serving others.
  • Lower cost: Fewer threads → lower memory, better CPU utilization.
  • Massive scalability: A single service can handle thousands of concurrent users.
  • Still Java: It’s still built on threads — just orchestrated smarter.

☕ Analogy

Think of a coffee shop:

  • Traditional Java: one barista per customer — efficient until the queue grows.
  • Quarkus: a few baristas multitasking, preparing drinks while others brew — no one stands idle.

Scenarios Where Threads Are Useful

  1. Parallel processing: Doing multiple independent computations (e.g., processing files).
  2. Background tasks: Logging, cleanup, or asynchronous notifications.
  3. Simulations or CPU-bound tasks: Heavy calculations that benefit from multiple cores.
  4. Learning async behavior: Understanding how frameworks like Quarkus optimize concurrency.

Bringing It All Together

  • Threads are workers that help Java do multiple things at once.
  • Quarkus still relies on them — but in a reactive, efficient way.
  • JavaScript handles concurrency differently — single-threaded but event-driven.
  • And understanding these fundamentals helps make sense of why reactive programming feels so different (yet familiar).

Closing

Before learning Quarkus’ reactive style, I used to think “threads” were just a low-level concept from the old Java days.
But understanding how they work — and how Quarkus builds on top of them — makes the reactive magic feel more logical than mysterious.

October 20, 2025 · 7 min

Java: Decimals

Revisiting the basics — understanding floating-point numbers in Java.

In my ongoing Back to Java series, I’ve been rediscovering parts of the language that I used years ago but never really thought deeply about.
Today’s topic: double vs float — two simple types that can quietly cause big differences in performance and precision.


What Are float and double?

Both are floating-point data types used for storing decimal numbers.

The key differences are in their size, precision, and use cases.

Type Size Approx. Range Decimal Precision Default Literal Suffix
float 32-bit ±3.4 × 10³⁸ ~6–7 digits f or F
double 64-bit ±1.7 × 10³⁰⁸ ~15–16 digits none or d

So, double literally means double-precision floating point — twice the size and precision of float.


Double Is the Default for Decimals in Java

One thing that often surprises new Java developers:

In Java, all decimal literals are treated as double by default.

That’s why you can write this:

double pi = 3.14159; // ✅ works fine

But this won’t compile:

float price = 19.99; // ❌ error: possible lossy conversion from double to float

You have to explicitly mark it as a float:

float price = 19.99f; // ✅ correct

Java does this to favor precision over size — it assumes you want a double unless you tell it otherwise.

When to Use Which

✅ Use float when:

  • Memory is limited (e.g., large arrays, 3D graphics, sensor data).
  • You don’t need extreme precision (6–7 digits is enough).

✅ Use double when:

  • Precision matters (scientific calculations, analytics, or financial models).
  • You want the default — most math operations and decimal constants in Java use double.

💡 Rule of thumb:

  • Use double by default. Use float only when you know you need to save space.

Precision Pitfalls

Floating-point numbers can’t always represent decimals exactly.

double a = 0.1;
double b = 0.2;
System.out.println(a + b); // prints 0.30000000000000004

Why? Binary floating-point can’t represent 0.1 precisely — it’s a repeating fraction in base 2. This tiny difference often doesn’t matter, but it can accumulate in calculations.

A Quick Look at BigDecimal

When you need exact decimal precision, especially in financial or accounting systems, you should use BigDecimal — a class from the java.math package.

import java.math.BigDecimal;

BigDecimal price = new BigDecimal("19.99");
BigDecimal quantity = new BigDecimal("3");
BigDecimal total = price.multiply(quantity);

System.out.println(total); // prints 59.97 exactly
  • BigDecimal stores numbers as unscaled integers + scale (not binary floating point).
  • It avoids rounding errors that can happen with float or double.
  • Downside: it’s slower and uses more memory, but you get perfect accuracy.

👉 That’s why it’s commonly used in banking, billing, and currency calculations.

Use Case Recommended Type
General math or analytics double
High-performance graphics / sensors float
Exact financial or monetary values BigDecimal
Integer-only math int or

For years, I used double by habit — it worked, so I never questioned it. But learning about precision again reminded me that choosing the right type is part of writing reliable code.

Sometimes you need speed (float), sometimes you need safety (BigDecimal), and most of the time, double is that balanced middle ground — it’s even the default for decimals in Java.

October 19, 2025 · 3 min

Java: Primitive Data Types vs Wrapper Classes

In the previous post, we looked at the difference between long (a primitive) and Long (its wrapper class). That was just one example — but in fact, every primitive type in Java has a wrapper class.

So in this post, let’s zoom out and cover the bigger picture:

  • What are primitive data types?
  • What are wrapper classes?
  • How are they initialized?
  • What are the benefits, differences, and when should you use one over the other?

By the end, you’ll have a simple rule of thumb that will save you from confusion: stick to primitives by default, use wrappers only when you need object features.

1. Primitive Data Types

Primitives are the basic building blocks of data in Java. They are not objects and store their values directly in memory (usually on the stack).

Java provides 8 primitive types:

  • byte, short, int, long (integers)
  • float, double (floating-point numbers)
  • char (character)
  • boolean (true/false)

Example:

int number = 10;
boolean isActive = true;

They are fast, memory-efficient, and always hold an actual value.

2. Wrapper Classes

For every primitive, Java provides a corresponding wrapper class in the java.lang package. These are objects that “wrap” a primitive inside a class. • Byte, Short, Integer, Long • Float, Double • Character • Boolean

Integer numberObj = Integer.valueOf(10);
Boolean isActiveObj = Boolean.TRUE;

Wrappers are essential when:

  • Working with Collections (e.g., List<Integer> instead of List<int>).
  • You need to represent null (absence of a value).
  • You want to use utility methods (like parsing strings into numbers).

2.5 Initializing Primitives vs Wrappers

Primitive Initialization

  • Direct and straightforward.
  • Local variables must be initialized before use.
  • Class fields get a default value (int → 0, boolean → false).
int x = 10;        // explicit initialization
boolean flag;      // flag must be assigned before use

Wrapper Initialization

  • Wrappers are objects, so they can be null.
  • Default value for wrapper fields is null.
  • Different ways to initialize:
Integer a = new Integer(10);     // old (not recommended)
Integer b = Integer.valueOf(10); // preferred
Integer c = 10;   // autoboxing (simplest)

Similar but Different

  • int x = 0; → raw value stored directly.
  • Integer y = 0; → an object reference pointing to an Integer.

So while syntax can look similar, the memory model and behavior are not the same.

3. Key Differences

Primitive Wrapper Class
Stored directly in memory (stack) Stored as an object reference (heap)
Faster and more memory-efficient Slightly slower, more memory use
Cannot be null Can be null
No methods available Comes with utility methods
Value can be reassigned directly Immutable object (once created, can’t be changed)

4. Autoboxing & Unboxing

Java makes conversion between primitives and wrappers seamless.

  • Autoboxing: primitive → wrapper
  • Unboxing: wrapper → primitive
Integer obj = 5;   // autoboxing
int num = obj;     // unboxing

This is convenient, but can introduce performance overhead if overused.

5. Benefits of Wrapper Classes

  • Collections & Generics: You can’t store int in a List, but you can store Integer.
List<Integer> numbers = new ArrayList<>();
numbers.add(5);
  • Utility Methods:
int parsed = Integer.parseInt("123");
  • Null Handling: Sometimes you need null to represent “no value”.

6. When to Use

  • Primitives → default choice. Use them when performance matters (loops, counters, math).
  • Wrappers → when you need:
    • Collections
    • Nullability
    • Utility methods

6.5 Rule of Thumb

  • Default to primitives – they are faster, memory-friendly, and straightforward.
  • Use wrappers only when necessary, such as:
    • You need to store them in Collections / Generics.
    • You need to represent null (e.g., database values).
    • You want to leverage utility methods (Integer.parseInt, Boolean.valueOf, etc.).

👉 In short: always use primitive unless there’s a clear reason to use the wrapper.

⚡ Bonus: What About String?

If you’re wondering where String fits in — it’s not a primitive, nor a wrapper. String is a regular class in java.lang, but Java gives it special treatment so it behaves almost like a primitive in many cases.

For example:

String name = "Hazriq";

looks as simple as assigning an int or boolean. But under the hood, String is an object, immutable, and stored differently from primitives.

This is why you can do things like:

int length = name.length();   // methods available

So:

  • Primitives = raw values
  • Wrappers = object versions of primitives
  • String = class, not a primitive, but commonly treated as a “basic type” in day-to-day Java

7. Best Practices & Gotchas

  • Avoid unnecessary boxing/unboxing in performance-critical code.
  • Be careful comparing wrappers:
Integer a = 1000;
Integer b = 1000;

System.out.println(a == b);      // false (different objects)
System.out.println(a.equals(b)); // true
  • Remember: wrapper classes and String are immutable. Once created, their value never changes — any “modification” actually creates a new object. (We’ll explore immutability in depth in a future post.)

Primitives are simple and fast, wrappers are flexible and object-friendly, and String is a special class that feels primitive but isn’t.

Understanding when to use each is one of those small but important skills that makes you write cleaner and more efficient Java code.

September 27, 2025 · 4 min