Table of Contents (Click any Topic to view first)
- Multithreading in Java
- Thread Creation and Management
- Synchronization and Thread Safety
- Concurrency Utilities
Multithreading is a Java feature that allows the concurrent execution of two or more parts of a program for maximum utilization of CPU. Each part of such a program is called a thread. Threads are lightweight processes.
In Java, there are two main ways to create a thread:
-
By extending the Thread
class
-
By implementing the Runnable
interface
Extending the Thread
Class
When a class extends the Thread
class, it should override the run()
method to define the code that constitutes the new thread's task.
Example:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running.");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // Start the thread
}
}
Implementing the Runnable Interface
This approach is more flexible because the class can extend another class as well.
Example:
java
Copy code
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running.");
}
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start(); // Start the thread
}
}
Synchronization is the capability to control the access of multiple threads to shared resources. Without synchronization, it is possible for one thread to modify a shared object while another thread is in the process of using or updating the object's value.
Synchronized Methods
A synchronized method ensures that only one thread can execute it at a time.
Example:
class Table {
synchronized void printTable(int n) { // Synchronized method
for (int i = 1; i <= 5; i++) {
System.out.println(n * i);
try {
Thread.sleep(400);
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
}
class MyThread1 extends Thread {
Table t;
MyThread1(Table t) {
this.t = t;
}
public void run() {
t.printTable(5);
}
}
class MyThread2 extends Thread {
Table t;
MyThread2(Table t) {
this.t = t;
}
public void run() {
t.printTable(100);
}
}
public class TestSynchronization {
public static void main(String args[]) {
Table obj = new Table(); // Only one object
MyThread1 t1 = new MyThread1(obj);
MyThread2 t2 = new MyThread2(obj);
t1.start();
t2.start();
}
}
Synchronized Blocks
To synchronize only a specific block of code instead of the entire method, use synchronized blocks.
Example:
class Table {
void printTable(int n) {
synchronized (this) { // Synchronized block
for (int i = 1; i <= 5; i++) {
System.out.println(n * i);
try {
Thread.sleep(400);
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
}
}
class MyThread1 extends Thread {
Table t;
MyThread1(Table t) {
this.t = t;
}
public void run() {
t.printTable(5);
}
}
class MyThread2 extends Thread {
Table t;
MyThread2(Table t) {
this.t = t;
}
public void run() {
t.printTable(100);
}
}
public class TestSynchronizationBlock {
public static void main(String args[]) {
Table obj = new Table(); // Only one object
MyThread1 t1 = new MyThread1(obj);
MyThread2 t2 = new MyThread2(obj);
t1.start();
t2.start();
}
}
The java.util.concurrent
package provides a comprehensive set of classes for managing concurrent programming, making it easier to write efficient, thread-safe, and scalable code.
Executor Framework
The Executor framework helps in decoupling task submission from task execution. It provides a pool of threads to execute tasks.
Example:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class WorkerThread implements Runnable {
private String message;
public WorkerThread(String s) {
this.message = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " (Start) message = " + message);
processMessage();
System.out.println(Thread.currentThread().getName() + " (End)");
}
private void processMessage() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class TestThreadPool {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5); // Creating a pool of 5 threads
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("" + i);
executor.execute(worker); // Calling execute method of ExecutorService
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
Other Concurrency Utilities
-
Locks: More flexible and sophisticated thread synchronization compared to synchronized methods and blocks.
-
CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
-
CyclicBarrier: A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.
-
Semaphore: A counting semaphore that maintains a set of permits for controlling access to a resource.
-
Concurrent Collections: Thread-safe versions of standard collections, such as ConcurrentHashMap
, CopyOnWriteArrayList
, and BlockingQueue
.
Example of Concurrent Collection:
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
map.forEach((key, value) -> System.out.println(key + ": " + value));
map.putIfAbsent("A", 4); // This will not change the value of "A"
map.putIfAbsent("D", 5); // This will add "D" to the map
System.out.println("After putIfAbsent operations:");
map.forEach((key, value) -> System.out.println(key + ": " + value));
}
}
Conclusion
Multithreading in Java is a powerful feature that allows for concurrent execution of code, improving performance and resource utilization. By understanding thread creation and management, synchronization techniques, and the concurrency utilities provided by Java, you can write robust, efficient, and scalable concurrent applications.