Java multithreading is one of the most powerful features of the language, but it can also be a significant source of bugs and design issues. Even experienced developers can fall into common traps, especially when creating and starting a thread. Many seemingly simple methods can have hidden problems that lead to unexpected behavior or inefficient code.
This article reveals surprising truths about Java threads. By examining the Runnable interface and its connection to the Thread class, we’ll highlight important details that can impact your concurrent applications.
1. The "Best" Way to Create a Thread Isn't the Easiest
There are two main ways to define what a thread will do in Java: extending the Thread class or implementing the Runnable interface. At first, extending Thread seems more straightforward. You create one class, override its run() method, and you’re done. However, implementing Runnable is the recommended approach for a key reason tied to basic object-oriented design principles.
When your class extends Thread, it uses its single inheritance option. Since Java does not allow multiple class inheritance, your class becomes linked to the Thread hierarchy and cannot extend any other class. This limitation makes you forfeit an important inheritance opportunity.
On the other hand, implementing the Runnable interface keeps your class free to extend another class. This method aligns with the essential software design idea of "Composition over Inheritance." Your class defines a task (Runnable) that a worker (Thread) executes, resulting in a more flexible and decoupled design. For this reason, experts suggest that implementing Runnable is the better strategy.
2. The Critical Difference Between .start() and .run()
One of the most common and dangerous misunderstandings for developers new to concurrency is the difference between a thread’s .start() and .run() methods. Calling the wrong one does not produce an error, but it completely changes how the program behaves. The difference is crucial: one starts concurrent execution; the other is just a regular method call.
Calling t.start()
This is the correct way to start a new thread. It tells the Java Virtual Machine (JVM) to allocate resources for a new thread of execution and schedules it to run. This new thread then calls the run() method.
* On a Thread with a Runnable target: A new thread is created to run the run() method from the provided Runnable object.
* On a Thread with no Runnable target: A new thread is created that runs the Thread class's own run() method. Since this method is empty, nothing happens.
Calling t.run()
This does not start a new thread. It simply runs the code in the run() method on the current thread, just like any other normal method call. The program remains single-threaded at that point, and the entire run() method must finish before the next line of code can run.
* On a Thread with a Runnable target: No new thread is started. Instead, it runs the run() method from the Runnable object on the current thread, acting as a standard method call.
* On a Thread with no Runnable target: No new thread is started. The Thread class's empty run() method runs on the current thread, just like any normal method call.
Confusing .run() with .start() is a classic mistake that causes a multithreaded application to behave like a single-threaded one.
3. A Runnable Is a Task, Not a Worker
It’s helpful to think of a Runnable object as a task—a job to be done. The Thread object is the worker that actually executes that task. A Runnable does not have execution capability; it only contains the code that defines the job.
This separation of roles leads to a surprising result if you try to call .start() directly on a Runnable object. It simply won’t work. The code will fail to compile with a clear error message.
This compiler error reinforces the key idea. The Runnable only defines the run() method, which contains the job logic. It has no knowledge of thread scheduling or execution. As the source material logically concludes, if the Runnable object had starting capability, there would be no need for the Thread object.
This distinction is important: Runnable is the "what," and Thread is the "how" and "when." This design also allows Runnable tasks to be passed to more advanced execution tools, such as an ExecutorService, which manages thread pools for better performance and resource handling.
4. Every Thread Has an Identity You Can Change
Every thread running in the JVM has a name. This can be a default name assigned by the JVM (like "main" for the main thread or "Thread-0" for the first child thread) or a custom name you choose. This is more than just a label; it's a valuable debugging tool.
You can work with a thread's name using two methods: thread.getName() and thread.setName(). As the source material states:
Every thread in Java has a name; it may be a default name generated by the JVM or one specifically provided by the programmer.
A great example from the source material shows the practical value of this.
1. First, calling Thread.currentThread().getName() from the main method returns "main."
2. Next, the programmer changes the name using Thread.currentThread().setName("Pawan Kalyan").
3. Finally, to confirm the change worked, an ArithmeticException is triggered by dividing by zero. This clever trick prompts the JVM to print a stack trace.
The resulting stack trace provides clear proof, showing that the main thread's identity changed. Instead of the default name, the custom name appears directly in the exception message.
Giving your threads meaningful names can turn a confusing stack trace from a complex concurrent system into a clear pointer to the source of a problem.
Conclusion
Mastering Java's multithreading capabilities involves going beyond the obvious syntax and understanding the subtle mechanics behind it. The preference for Runnable over extending Thread, the critical difference between .start() and .run(), the roles of task versus worker, and the usefulness of thread naming are all details that differentiate strong concurrent code from fragile, unpredictable applications.
Now that you’ve explored the deeper aspects of creating threads, what other fundamental Java concepts might deserve another look?
No comments:
Post a Comment