Java Swing Thread Optimization: Replace Thread with ExecutorService

Published: 2026-06-09

Why Thread Optimization Is Necessary

In Java Swing development, background tasks must be handled carefully.

A common mistake is creating a new thread every time a user triggers an action.

This approach may work in simple cases, but it can cause serious performance problems when events happen frequently.

Examples:

  • Clicking buttons repeatedly
  • Running Git commands from a GUI
  • Executing shell commands
  • Processing network requests
  • Performing slow file operations

The correct approach is:

  • Reuse threads with `ExecutorService`
  • Keep heavy operations away from the Swing UI thread
  • Update UI components safely through `SwingUtilities.invokeLater()`

The Problem With Creating Threads Directly

A common implementation looks like this:

private void setGitMessageEvent(String cmd)
{
GitMessageCommandThread gitMessageCommandThread = new GitMessageCommandThread(cmd);
new Thread(gitMessageCommandThread).start();
}

class GitMessageCommandThread extends Thread
{
String cmd = null;

```
public GitMessageCommandThread(String cmd)
{
    this.cmd = cmd;
}

public void run()
{
    Gitgui.this.setMessage(this.cmd);
}
```

}

At first glance, this seems simple:

  1. Create a thread
  1. Execute the task
  1. Destroy the thread

However, this design has several hidden problems.

Problem 1: Frequent Thread Creation and Destruction

Every call creates a brand-new operating system thread.

Threads are not free resources.

Creating and destroying threads requires:

  • Memory allocation
  • Thread scheduling
  • Context switching
  • Operating system management overhead

If a user triggers the event repeatedly, the application may create many unnecessary threads.

For example:

  • 100 button clicks
  • Multiple Git operations
  • Slow network responses

The system may suddenly have many active threads competing for CPU and memory resources.

This reduces performance instead of improving it.

A Simple Analogy

Imagine a bank.

The Old Design: Creating Temporary Employees

Every customer arrives:

  • The bank hires a new employee
  • The employee handles one customer
  • The employee leaves immediately

This is similar to:

```java

new Thread().start();

```

If 100 customers arrive at the same time:

  • 100 temporary employees appear
  • Everyone needs workspace
  • Management overhead increases

Eventually the bank becomes inefficient.

The same thing happens inside an application:

  • More CPU scheduling
  • More memory usage
  • More context switching

The Better Design: Thread Pool

A thread pool works differently.

The bank has permanent employees.

Customers wait in a queue:

```

Customer 1

Customer 2

Customer 3

```

The available employee processes them one by one.

In Java, this is handled by:

```java

ExecutorService

```

The thread is reused instead of recreated.

Advantages:

  • Stable resource usage
  • Better performance
  • Easier task management
  • Predictable execution order

Problem 2: Unnecessary Thread Object

The original code contains:

GitMessageCommandThread gitMessageCommandThread =
new GitMessageCommandThread(cmd);

new Thread(gitMessageCommandThread).start();

The problem is:

`GitMessageCommandThread` already extends `Thread`.

It is already a thread object.

Passing it into another `Thread` creates unnecessary complexity.

A better design is using a simple task:

```java

Runnable

```

and allowing `ExecutorService` to manage execution.

Problem 3: Swing UI Thread Safety

The original method also contains:

this.textArea.setText(s);
this.textField_2.setText(cmd);

These operations modify Swing components.

Swing is not thread-safe.

UI updates should always happen inside the Swing Event Dispatch Thread (EDT).

Otherwise, applications may experience:

  • UI freezing
  • Display glitches
  • Random refresh problems
  • Unexpected behavior

The background thread should only handle heavy work.

The UI thread should update components.

Optimized Implementation

The improved version:

private final ExecutorService gitCommandExecutor =
Executors.newSingleThreadExecutor();

private void setGitMessageEvent(String cmd)
{
gitCommandExecutor.submit(() -> {
Gitgui.this.setMessage(cmd);
});
}

public void setMessage(String cmd)
{
String s = Shell.send(cmd, getIdForm());

```
javax.swing.SwingUtilities.invokeLater(() -> {

    this.textArea.setText(s);

    this.textArea.setSelectionStart(1);

    if (this.textArea.getCaretPosition() != 0) {
        this.textArea.setCaretPosition(1);
    }

    this.textField_2.setText(cmd);

});
```

}

Why This Optimization Is Better

1. Thread Reuse

Instead of:

```java

new Thread()

```

every time, the application uses:

```java

Executors.newSingleThreadExecutor()

```

The same worker thread processes tasks repeatedly.

Benefits:

  • No repeated thread creation
  • Lower memory consumption
  • Better performance
  • More stable behavior

2. Safe Swing Updates

The background thread performs:

  • Shell execution
  • Git operations
  • Network requests
  • Heavy processing

Then:

```java

SwingUtilities.invokeLater()

```

returns the UI update task to the Swing Event Dispatch Thread.

This prevents UI conflicts.

3. Cleaner Architecture

The optimized version removes:

  • Custom Thread classes
  • Duplicate thread management
  • Unnecessary object creation

The code becomes easier to read and maintain.

Why Use SingleThreadExecutor Here?

A single-thread executor is suitable when tasks should run sequentially.

Examples:

  • Git commands
  • Shell execution
  • File operations
  • Log processing
  • Background synchronization

The execution order is guaranteed:

Task A
  ↓
Task B
  ↓
Task C

This avoids race conditions between operations.

Swing Threading Model

A good Swing architecture looks like this:

User Action
      |
      v
ExecutorService
      |
      v
Background Task
      |
      v
SwingUtilities.invokeLater()
      |
      v
Update UI

Heavy work stays away from the UI thread.

The interface remains responsive.

Conclusion

Creating a new thread for every event is a common beginner mistake in Java desktop development.

Modern Java applications should use proper concurrency management.

For Swing applications, the recommended pattern is:

  • `ExecutorService` for background tasks
  • `SwingUtilities.invokeLater()` for UI updates
  • Separate business logic from interface rendering

Good thread management is not about creating more threads.

It is about creating the right number of threads and using them efficiently.

Explore More

Technology Guides →

Java 21 Virtual Threads vs Java 17 Thread Pools in Swing Applications

MariaDB thread_cache_size Explained: Thread Reuse, Performance, and Best Practices

Building a Postman-Like API Client: Go Fyne vs Java Swing

Building a Linux Command Search Tool with Java Swing

Southeast Asia Insights →