Key Takeaways
- Threads are lightweight execution contexts within a process, sharing the same memory space.
- Java provides two primary methods to implement multithreading: extending the Thread class and implementing the Runnable interface.
- Efficient multithreading helps handle concurrent tasks, such as web server requests.
- Avoiding deadlock requires careful management of resource locks, often aided by libraries like
java.util.concurrent. - The number of threads your Java application can run is influenced by system resources and JVM configuration.
What is Multithreading in Java?
In Java, a thread is the most lightweight execution unit that a program can manage. Multiple threads can execute concurrently within a single process, sharing the same memory space. This makes it possible to execute parts of a program simultaneously, increasing efficiency.
Unlike processes, which operate in their own isolated environments with separate memory spaces and resources, threads run within a shared environment, making thread communication easier but resource management more complex.
When switching between processes, the system incurs more overhead because it needs to save the state of the current process and load the state of the next. With threads, this overhead is reduced, making thread-based multitasking a much more efficient way to manage parallel execution.
Two Ways to Implement Multithreading in Java
Java provides two primary methods for creating threads:
1) Extend the Thread class
public class MyThread extends Thread {
public void run() {
System.out.println("This thread is running");
}
}
public class Main {
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
}
}
Extending the Thread class allows you to directly create an instance of your class as a thread. However, it limits you from extending any other class since Java does not support multiple inheritance.
2) Implement the Runnable interface
public class MyRunnable implements Runnable {
public void run() {
System.out.println("This runnable is running");
}
}
public class Main {
public static void main(String[] args) {
Thread myThread = new Thread(new MyRunnable());
myThread.start();
}
}
Implementing the Runnable interface is more flexible because it allows you to implement multiple interfaces. It requires you to pass an instance of your class to a Thread object.
Practical Example of Multithreading in Java
Handling concurrent requests in a web server environment perfectly shows the power of multithreading. Here’s a simple example:
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MultiThreadedWebServer {
private static final int PORT = 8080;
private static final int NUM_THREADS = 10;
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(PORT);
ExecutorService threadPool = Executors.newFixedThreadPool(NUM_THREADS);
while (true) {
Socket clientSocket = serverSocket.accept();
Runnable requestHandler = new RequestHandler(clientSocket);
threadPool.execute(requestHandler);
}
}
static class RequestHandler implements Runnable {
private Socket clientSocket;
public RequestHandler(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
try {
// handle the request
// ...
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
Using a thread pool via the ExecutorService framework efficiently manages server requests, minimizing the complexity of individual thread management.
How to Avoid Deadlock in Java
Understanding Deadlock
Deadlock occurs when two or more threads get stuck waiting for each other to release resources, leading to a complete halt in execution. Imagine two threads, each holding a lock on a resource the other needs.
public class Main {
public static void main(String[] args) {
Integer resource1 = 1;
Integer resource2 = 2;
Thread thread1 = new Thread() {
public void run() {
synchronized (resource1) {
System.out.println("Thread 1 locking resource 1");
try { Thread.sleep(50); } catch (Exception e) {}
synchronized (resource2) {
System.out.println("Thread 1 locking resource 2");
}
}
}
};
Thread thread2 = new Thread() {
public void run() {
synchronized (resource2) {
System.out.println("Thread 2 locking resource 2");
try { Thread.sleep(50); } catch (Exception e) {}
synchronized (resource1) {
System.out.println("Thread 2 locking resource 1");
}
}
}
};
thread1.start();
thread2.start();
}
}
The issue arises because each thread holds a resource the other needs to complete, creating a cycle of resource contention.
Strategies to Avoid Deadlock
1) Use High-level Concurrency Utilities
Leverage Java's java.util.concurrent package, which provides synchronized collections and concurrent execution utilities, helping you avoid manual locking.
2) Avoid Nested Locks
Try to eliminate nested locking by acquiring all needed locks at once or following a clear, defined locking order.
3) Use Thread Pools
As seen in the server example, thread pools from the Executor framework manage thread scheduling, reducing deadlock risk.
4) Implement Timeouts
Set timeouts on locks so threads can eventually abort attempts and progress if they can’t acquire their lock, sidestepping deadlocks.
How Many Threads Can Run at Once in Java?
The JVM doesn’t impose a strict limit on the number of threads; it’s largely determined by the underlying hardware resources like CPU and memory. Your application can run as many threads as the system can handle efficiently. That being said, creating an excessive number of threads can lead to high overhead and reduce application performance.
FAQ
What’s the difference between a thread and a process?
A thread is a subset of a process. Processes are independent and contain their own memory, while threads share memory within the same process, making communication more natural but requiring careful resource management.
How do I choose between extending Thread and implementing Runnable?
If your class doesn’t need to extend another class, extending Thread is straightforward. Use Runnable for more flexibility, especially if you need to inherit from another class.
Are there libraries better suited for multithreading than standard Java?
For many applications, the java.util.concurrent package suffices. However, libraries like Akka offer actor-based concurrency models if you're dealing with complex asynchronous messaging.
