Thread newThread = new Thread(() -> System.out.println("Hello from new thread"));
start()
method is called on a new thread, it enters the runnable state. The thread is ready to be executed by the Java Virtual Machine (JVM) scheduler.newThread.start();
wait()
, join()
, or park()
. It will remain in this state until another thread notifies it.sleep(long millis)
and wait(long timeout)
can put a thread in the timed - waiting state.Synchronization is used to control access to shared resources in a multithreaded environment. Java provides the synchronized
keyword to achieve this. For example, consider the following code for a shared counter:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Running thread: " + Thread.currentThread().getName());
}
}
public class ThreadClassExample {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Running thread: " + Thread.currentThread().getName());
}
}
public class RunnableInterfaceExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
The Executor framework provides a higher - level abstraction for managing threads. It includes the ExecutorService
interface and its implementations like ThreadPoolExecutor
.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorFrameworkExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 5; i++) {
final int taskId = i;
executorService.submit(() -> {
System.out.println("Executing task " + taskId + " on thread " + Thread.currentThread().getName());
});
}
executorService.shutdown();
}
}
Thread pooling is a technique where a fixed number of threads are created and maintained in a pool. When a new task arrives, it is assigned to an available thread from the pool. This reduces the overhead of creating and destroying threads for each task. For example, using Executors.newFixedThreadPool(int nThreads)
as shown in the previous example.
Asynchronous processing allows tasks to be executed independently without blocking the main thread. Java provides the CompletableFuture
class for asynchronous programming.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class AsyncProcessingExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Task completed";
});
System.out.println("Main thread continues...");
String result = future.get();
System.out.println(result);
}
}
Load balancing distributes the workload evenly across multiple threads or servers. In a multithreaded Java application, we can use techniques like task partitioning to divide the work among threads. For example, if we have a large data set to process, we can split it into smaller chunks and assign each chunk to a different thread.
Locking can cause contention and reduce the performance of a multithreaded application. Try to use lock - free data structures like ConcurrentHashMap
instead of HashMap
when multiple threads need to access a shared map.
import java.util.concurrent.ConcurrentHashMap;
public class LockFreeExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.put("key2", 2);
System.out.println(map.get("key1"));
}
}
Java provides many thread - safe data structures in the java.util.concurrent
package. These data structures are designed to handle concurrent access efficiently. For example, CopyOnWriteArrayList
can be used when multiple threads need to read and write to a list concurrently.
In a multithreaded environment, errors can occur in any thread. It is important to handle exceptions properly to prevent the entire application from crashing. For example, when using the ExecutorService
, we can use Future.get()
to get the result of a task and handle any exceptions that may occur during its execution.
Multithreading is a powerful tool for creating scalable Java applications. By understanding the fundamental concepts of multithreading, using the appropriate usage methods, following common practices, and adhering to best practices, developers can build applications that can handle a large number of concurrent requests efficiently. However, multithreading also introduces complexity, such as race conditions and deadlocks, which need to be carefully managed. With proper design and implementation, Java multithreading can significantly enhance the performance and scalability of applications.