Before delving into multithreading, it's essential to understand the concept of multitasking in computing.
Now, let's explore the concept of a "thread" in more detail:
A thread is the smallest unit of execution within a program. It represents a sequence of instructions that can be scheduled and executed independently by the computer's CPU. Threads within a process share the same memory space and resources, allowing them to work together to accomplish tasks concurrently.
Threads are commonly used to perform multiple operations simultaneously, enhance program responsiveness, and efficiently utilize modern multi-core processors.
Java provides extensive support for thread-based multitasking through its multithreading capabilities. Understanding threads and multithreading is crucial for building responsive and efficient Java applications that can perform multiple tasks concurrently.
Java provides extensive support for thread-based multitasking through its multithreading capabilities. Understanding multithreading is crucial for building responsive and efficient Java applications that can perform multiple tasks concurrently.
Multithreading is a concurrent execution process where multiple threads run independently at the same time without dependency on each other. In Java, it allows you to execute multiple threads within a single process, enhancing program performance and responsiveness by making efficient use of the CPU.
Multithreading is used to save time and increase the performance of Java applications. By dividing tasks into multiple threads, a program can execute multiple operations simultaneously, taking advantage of modern multi-core processors.
Java is commonly used in animation and game development, where multithreading allows multiple animated characters or game elements to appear on the screen simultaneously. Each character or element can be controlled by a separate thread, enabling concurrent and independent actions.
A thread is a fundamental unit of execution in a program. It represents a sequence of instructions that can run independently and concurrently with other threads. In Java, threads are instances of the pre-defined `Thread` class, available in the `java.lang` package.
Threads are often referred to as the basic units of a CPU because they can be scheduled and executed independently. Each thread shares the same memory space and resources within a process, enabling them to perform tasks simultaneously.
In Java, you can create threads in two main ways:
You can create a new thread by extending the `Thread` class and overriding its `run()` method. This approach allows you to define the thread's behavior within the `run()` method.
Another way to create threads is by implementing the `Runnable` interface. This approach separates the thread's behavior from the thread object itself. You need to provide the implementation of the `run()` method in a separate class that implements `Runnable` and then create a `Thread` object that executes the `Runnable` instance.
It's important to distinguish between multitasking and multithreading:
Multitasking refers to the concurrent execution of multiple processes or applications by the operating system. These processes can be entirely independent and may or may not share resources.
Multithreading, on the other hand, involves concurrent execution within a single process. Multiple threads within the same process share the same memory space and resources, allowing them to work together to achieve parallelism and perform tasks simultaneously.
In Java, you can create threads by extending the `Thread` class. This approach allows you to define your thread's behavior by overriding the `run()` method. Threads created using this method are also known as "child threads," and they run concurrently with the main thread of the program.
class A extends Thread {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Hello");
}
}
}
class B {
public static void main(String[] args) {
A t = new A();
t.start(); // Starting the child thread
// Code below is part of the main thread, while the code above is part of the child thread
for (int i = 1; i <= 5; i++) {
System.out.println("Hello");
}
}
}
The `run` method is already defined in the `Thread` class, but you can override it to provide your custom implementation. When you start a thread, the JVM automatically calls its `run` method.
When you run the program, the order in which the threads run is not guaranteed. Each time you execute the program, the output sequence may differ. However, what is certain is that both the main thread and the child thread run concurrently.
If you want to introduce a delay between the output, you can use the `Thread.sleep(1000)` method. However, you need to handle exceptions since `Thread.sleep()` can throw an `InterruptedException`. Here's an example:
class A extends Thread {
@Override
public void run() {
try {
for (int i = 1; i <= 5; i++) {
System.out.println("Hello");
Thread.sleep(1000); // Pause for 1 second
}
} catch (InterruptedException e) {
// Handle the interrupted exception
}
}
}
class B {
public static void main(String[] args) throws InterruptedException {
A t = new A();
t.start(); // Starting the child thread
for (int i = 1; i <= 5; i++) {
System.out.println("Hello");
}
}
}
In Java, you can create threads by implementing the `Runnable` interface. This approach provides more flexibility compared to extending the `Thread` class because it allows you to separate the thread's behavior from the thread object. Threads created using the `Runnable` interface are also known as "runnable threads."
Syntax to Define a Thread:
class A implements Runnable {
public void run() {
// Define the thread's job or task here
}
}
class B {
public static void main(String[] args) {
A obj = new A();
// obj.start(); // This won't work because we need a Thread object to start a thread
Thread t = new Thread(obj); // Create a Thread object with obj as a reference
t.start(); // Now the thread will execute the run() method defined in class A
}
}
Example:
class A implements Runnable {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("My child Thread");
}
}
}
class B {
public static void main(String[] args) {
A r = new A(); // Create an instance of class A, which implements Runnable
Thread t = new Thread(r); // Create a Thread object with the Runnable instance r
t.start(); // Start the child thread
// The main thread is responsible for running the following code
for (int i = 1; i <= 5; i++) {
System.out.println("My main Thread");
}
}
}
Here's an example to demonstrate the use of a Thread Scheduler:
class A extends Thread {
public void run() {
String name = Thread.currentThread().getName();
for (int i = 1; i <= 3; i++) {
System.out.println(name);
}
}
}
class Test {
public static void main(String[] args) {
A t1 = new A();
A t2 = new A();
A t3 = new A();
t1.setName("Thread 1");
t2.setName("Thread 2");
t3.setName("Thread 3");
t1.start();
t2.start();
t3.start();
String mainThreadName = Thread.currentThread().getName();
for (int i = 1; i <= 3; i++) {
System.out.println(mainThreadName);
}
}
}
Explanation of functions used in the program:
run()
: This method is overridden in the A class and defines the
task
that the thread will perform when started.The program demonstrates the use of multiple threads (t1, t2, t3) running concurrently and independently alongside the main thread. Each thread prints its name multiple times, and the main thread also prints its name, showing the concurrent execution of threads.
When to Use Multithreading and Why?
A thread has a well-defined life cycle during which it can transition through different states, each serving a specific purpose in the execution process:
The thread life cycle provides a structured model for understanding the various phases a thread goes through during its execution, from creation to completion.
Understanding Thread Transitions
In multithreading, thread transitions refer to the various states that a thread can move through during its lifecycle. Threads in a multithreaded program can transition between different states as they are created, started, paused, resumed, and terminated. Understanding these thread states is crucial for managing and controlling the execution of threads effectively.
Thread States:
Threads in Java can exist in several states, including:
Thread Transition Example:
Here's a simplified example illustrating thread transitions:
public class ThreadTransitionsExample {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println("Thread 1: Running");
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2: Running");
});
thread1.start(); // Thread 1 transitions from "New" to "Runnable" to "Running."
thread2.start(); // Thread 2 transitions from "New" to "Runnable" to "Running."
Thread.sleep(1000);
// Note: The following methods (suspend, resume, and stop) are deprecated and not recommended for modern multithreaded programming.
// Instead, use interrupt and join for better control over thread execution.
thread1.suspend(); // Thread 1 transitions to "Blocked." (Deprecated)
thread2.resume(); // Thread 2 transitions to "Running."
Thread.sleep(1000);
thread1.resume(); // Thread 1 transitions back to "Running." (Deprecated)
thread1.stop(); // Thread 1 transitions to "Terminated." (Deprecated)
System.out.println("Thread 1 state: " + thread1.getState());
System.out.println("Thread 2 state: " + thread2.getState());
}
}
In this example, two threads (thread1 and thread2) are created and go through various thread state transitions, including starting, suspending (deprecated), resuming (deprecated), and stopping (deprecated).
Understanding thread transitions is essential for designing reliable multithreaded applications and ensuring that threads behave as expected throughout their lifecycles.
The following code has been updated to remove the use of deprecated methods:
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println("Thread 1: Running");
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2: Running");
});
thread1.start(); // Thread 1 transitions from "New" to "Runnable" to "Running."
thread2.start(); // Thread 2 transitions from "New" to "Runnable" to "Running."
Thread.sleep(1000);
thread1.interrupt(); // Thread 1 transitions to "Blocked" if asleep, or "Runnable" if not interrupted.
Thread.sleep(1000);
thread1.join(); // Wait for Thread 1 to finish its execution.
thread2.join(); // Wait for Thread 2 to finish its execution.
System.out.println("Thread 1 state: " + thread1.getState());
System.out.println("Thread 2 state: " + thread2.getState());
}
}
Syntax:
Thread.sleep(milliseconds); <!-- Example syntax -->
// OR
Thread t = new Thread();
t.sleep(milliseconds);
The `milliseconds` represent the duration in milliseconds for which the thread should sleep.
Example Program:
class A extends Thread {
public void run() {
String threadName = Thread.currentThread().getName();
try {
for (int i = 1; i <= 3; i++) {
System.out.println("From: " + threadName + " -- " + i);
Thread.sleep(1000); // Sleep for 1 second
}
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
class Test {
public static void main(String[] args) {
A t1 = new A();
A t2 = new A();
A t3 = new A();
t1.setName("Thread 1");
t2.setName("Thread 2");
t3.setName("Thread 3");
t1.start();
t2.start();
t3.start();
}
}
In this example, the `sleep` method is used to introduce delays while printing messages from multiple threads.
Handling InterruptedException:
Here is an example of how the join() method can be used:
class A extends Thread {
public void run() {
String threadName = Thread.currentThread().getName();
for (int i = 1; i <= 3; i++) {
System.out.println("From " + threadName + ": " + i);
}
}
}
class Test {
public static void main(String[] args) {
A t1 = new A();
A t2 = new A();
A t3 = new A();
t1.setName("Thread 1");
t2.setName("Thread 2");
t3.setName("Thread 3");
t2.start(); // Start Thread 2
try {
t2.join(); // Wait for Thread 2 to complete
} catch (InterruptedException e) {
System.out.println(e);
}
t1.start(); // Start Thread 1
t3.start(); // Start Thread 3
}
}
In this example, we create three threads (t1, t2, and
t3). We
start t2 and then call t2.join()
, which causes the main thread to wait
until t2 completes its execution. This ensures that the output from
t2 is
printed before t1 and t3
start running.
Java provides two methods, sleep() and join(), for managing the execution of threads in a multi-threaded application. While both methods involve temporarily suspending the execution of a thread, they serve different purposes and are used in distinct scenarios:
Here's an example to demonstrate the use of both sleep() and join():
class MyRunnable implements Runnable {
@Override
public void run() {
try {
// Simulate some work in the child thread
Thread.sleep(2000); // Sleep for 2 seconds
System.out.println("Child thread has completed.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadExample {
public static void main(String[] args) {
// Create an instance of the Runnable implementation
Runnable myRunnable = new MyRunnable();
// Create a Thread and pass the Runnable to it
Thread childThread = new Thread(myRunnable);
// Start the child thread
childThread.start();
try {
// Parent thread waits for the child thread to complete
childThread.join(); // Wait for the child thread to finish
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Parent thread continues.");
}
}
In this example, the parent thread starts a child thread, which simulates some work using `sleep()`. The parent thread then uses `join()` to wait for the child thread to finish. As a result, the "Child thread has completed." message is guaranteed to be printed before the "Parent thread continues." message, demonstrating the coordination achieved with the `join()` method.
In Java, the suspend() and resume() methods were historically used to control the execution of threads. However, it is crucial to understand that these methods are deprecated since Java 2 and are strongly discouraged for use in modern multithreaded applications. The reason for deprecation is their inherent shortcomings, including the potential for thread deadlock and other synchronization issues. Instead, Java offers more reliable and safer mechanisms for thread control and coordination, such as the wait() and notify() methods, or higher-level concurrency utilities provided by the Java concurrency framework.
Below, we'll briefly discuss the deprecated suspend() and resume() methods and provide an example:
Example:
A t1 = new A();
A t2 = new A();
A t3 = new A();
t1.start();
t2.start();
t3.start();
t2.suspend(); // Thread t2 is suspended and will not be executed.
t2.resume(); // Thread t2 is resumed and can continue executing (deprecated).
Given the issues associated with the suspend() and resume() methods, it is strongly recommended that you avoid using them in your code. Instead, consider using more modern and safer synchronization mechanisms and concurrency utilities provided by Java, such as the wait() and notify() methods or the higher-level java.util.concurrent classes, for effective thread control and coordination.
In Java, the wait() and notify() methods are essential for thread synchronization and coordination. These methods are considered fundamental in multithreaded programming and are recommended for managing thread execution.
Example:
class SharedResource {
private boolean condition = false;
synchronized void waitForCondition() throws InterruptedException {
while (!condition) {
wait(); // Release the lock and wait for the condition to be true.
}
}
synchronized void setConditionTrue() {
condition = true;
notify(); // Notify a waiting thread that the condition has been met.
}
}
Example:
class ExampleThread extends Thread {
private SharedResource resource;
public ExampleThread(SharedResource resource) {
this.resource = resource;
}
public void run() {
try {
Thread.sleep(1000); // Simulate some work.
resource.setConditionTrue(); // Set the condition to true and notify waiting threads.
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
public class Test {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
ExampleThread thread = new ExampleThread(resource);
thread.start();
try {
resource.waitForCondition(); // Wait for the condition to be true.
System.out.println("Condition is now true. Continuing execution.");
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
In the example above, the wait() and notify() methods are used to coordinate the execution of two threads. The main thread waits for the condition to be set to true by the ExampleThread, and when the condition is met, it continues its execution.
Using wait() and notify() methods, you can create more robust and predictable multithreaded applications by allowing threads to communicate and synchronize their actions effectively.
In Java, thread priority is a way to influence the order in which threads are scheduled to run by the thread scheduler. Each thread has a priority assigned to it, and threads with higher priority values have a better chance of being executed before threads with lower priority values. However, it's important to note that thread priority should be used judiciously, as it doesn't guarantee precise execution order and can vary depending on the underlying system.
Here are some key points about thread priority:
It's important to use thread priority carefully, as relying solely on priority for thread synchronization and coordination can lead to unpredictable behavior. Thread priority should be considered as a hint to the thread scheduler, and other synchronization mechanisms should be used for precise control over thread execution.
Example:
class PriorityExample extends Thread {
public void run() {
System.out.println("Thread " + Thread.currentThread().getName() + " with priority " + Thread.currentThread().getPriority() + " is running.");
}
public static void main(String[] args) {
PriorityExample thread1 = new PriorityExample();
PriorityExample thread2 = new PriorityExample();
thread1.setPriority(Thread.MAX_PRIORITY);
thread2.setPriority(Thread.MIN_PRIORITY);
thread1.start();
thread2.start();
}
}
In this example, we create two threads with different priorities using the setPriority() method. Thread 1 is set to MAX_PRIORITY, and Thread 2 is set to MIN_PRIORITY. The output may vary depending on the thread scheduler, but Thread 1 has a higher chance of running before Thread 2 due to its higher priority.
In Java, the yield() method is a part of the Thread class and is used to allow other threads with equal or higher priority to have a chance to run. It essentially temporarily pauses the execution of the current thread, giving way to other threads in the same priority range. However, it's important to understand that the effectiveness of the yield() method may vary depending on the operating system and the thread scheduler.
Here's how the yield() method works:
Example:
class A extends Thread {
public void run() {
String threadName = Thread.currentThread().getName();
for (int i = 1; i <= 3; i++) {
System.out.println("From " + threadName + ": " + i);
}
}
}
class Test {
public static void main(String[] args) {
A t1 = new A();
A t2 = new A();
A t3 = new A();
t1.setName("Thread 1");
t2.setName("Thread 2");
t3.setName("Thread 3");
t1.setPriority(Thread.MIN_PRIORITY); // Lower priority
t2.setPriority(Thread.MAX_PRIORITY); // Highest priority
t3.setPriority(Thread.NORM_PRIORITY); // Default priority
t1.start();
t2.start();
t3.start();
t2.yield(); // Thread 2 yields, allowing other threads to run.
}
}
While the yield() method can be used for thread control, it is often better to rely on other synchronization mechanisms and thread management techniques to achieve predictable and robust multithreaded behavior.
The stop() method is a deprecated way of terminating a thread in Java. It forcefully stops a thread's execution without allowing it to finish its current work or clean up resources. However, it's strongly discouraged due to potential issues like thread instability, data corruption, and resource leaks. Instead, Java offers a safer and preferred way of thread termination using the interrupt() method.
Example:
class A extends Thread {
public void run() {
String threadName = Thread.currentThread().getName();
for (int i = 1; i <= 3; i++) {
System.out.println("From " + threadName + ": " + i);
}
if (Thread.interrupted()) { // Check for interruption.
System.out.println("Thread " + threadName + " is interrupted. Exiting gracefully.");
return;
}
}
}
class Test {
public static void main(String[] args) {
A t1 = new A();
A t2 = new A();
A t3 = new A();
t1.start();
t2.start();
t2.interrupt(); // Interrupt Thread t2.
t3.start();
}
}
In this example, we use the interrupt() method to gracefully terminate t2. The thread periodically checks its interrupt status using Thread.interrupted() and exits when interrupted, allowing for proper resource cleanup.
It's essential to use the interrupt() method and handle thread interruption correctly to ensure that your multithreaded Java applications are stable and reliable.
The isAlive() method in Java is used to check the current status of a thread. It returns true if the thread is currently running (i.e., it has been started and has not yet completed its execution), and it returns false if the thread has terminated, either by naturally completing its run() method or due to an unhandled exception.
Here's a brief overview of how the isAlive() method works:
Example:
class ExampleThread extends Thread {
public void run() {
// Simulate some work.
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
}
public static void main(String[] args) {
ExampleThread thread = new ExampleThread();
System.out.println("Before starting the thread, isAlive: " + thread.isAlive());
thread.start();
System.out.println("After starting the thread, isAlive: " + thread.isAlive());
try {
thread.join();
} catch (InterruptedException e) {
System.out.println(e);
}
System.out.println("After the thread has finished, isAlive: " + thread.isAlive());
}
}
In this example, we create a thread, start it, and use isAlive() to check its status at different points in the program. The method correctly reflects whether the thread is currently running or has completed its execution.
The isAlive() method is a valuable tool for managing and monitoring the status of threads in a multithreaded Java application.
Why Synchronization is Needed?
Multithreading is a powerful technique for concurrent execution, allowing tasks to be completed efficiently. However, it introduces challenges when multiple threads attempt to access shared resources simultaneously. This can lead to unexpected and incorrect results due to data corruption or race conditions. To address these issues, synchronization mechanisms are essential to ensure orderly and safe access to shared resources in multithreaded programs.
Challenges in Multithreading
Example
Consider a simple example to illustrate the need for synchronization:
Here is a basic analogy to the bus example:
class Bus {
private int availableSeats = 1;
public void bookSeat() {
if (availableSeats > 0) {
// Simulate some booking process.
availableSeats--;
System.out.println("Seat booked by " + Thread.currentThread().getName());
} else {
System.out.println("No seats available.");
}
}
}
class PassengerThread extends Thread {
private Bus bus;
public PassengerThread(Bus bus) {
this.bus = bus;
}
public void run() {
bus.bookSeat();
}
}
public class BusBookingApp {
public static void main(String[] args) {
Bus bus = new Bus();
PassengerThread passenger1 = new PassengerThread(bus);
PassengerThread passenger2 = new PassengerThread(bus);
PassengerThread passenger3 = new PassengerThread(bus);
passenger1.start();
passenger2.start();
passenger3.start();
}
}
In this example, multiple passenger threads try to book a seat in the bus simultaneously. Without synchronization, it's possible for more than one passenger to book the same seat, leading to incorrect results.
To address these issues and ensure orderly access to shared resources, synchronization mechanisms like locks, mutexes, and synchronized blocks are used in multithreaded programming.
What is Synchronization?
Synchronization is a crucial technique in multithreading that enables controlled access to shared resources among multiple threads. It ensures that only one thread can enter a synchronized block or method at a time, preventing concurrent and potentially problematic access to shared data.
Purpose of Synchronization
The primary purpose of synchronization is to address the challenges that arise in multithreading when multiple threads simultaneously attempt to access the same shared resource. Without synchronization, these situations can lead to undesirable outcomes, including data corruption and incorrect program behavior.
Types of Synchronization
Synchronization in Java can be broadly classified into two categories:
Method-Level Synchronization
Method-level synchronization in Java is achieved by using the synchronized keyword to declare a method. When a method is marked as synchronized, only one thread can execute that method on the same object instance at a time. This ensures that the critical section of code within the synchronized method is protected from concurrent access by multiple threads, preventing race conditions and data corruption.
Here's an example of method-level synchronization:
class Hotel {
private int guests = 0;
public synchronized void checkIn() {
// Synchronized method.
guests++;
System.out.println("Guest checked in. Total guests: " + guests);
}
public synchronized void checkOut() {
// Synchronized method.
guests--;
System.out.println("Guest checked out. Total guests: " + guests);
}
}
In this example, the checkIn() and checkOut() methods are declared as synchronized. This means that only one guest can check in or check out at a time, preventing conflicts and ensuring the guests variable is accessed safely.
Method-level synchronization is suitable when you want to protect an entire method or when the entire method needs to be atomic and thread-safe. However, it can lead to contention if multiple threads frequently access the synchronized methods concurrently, potentially causing performance bottlenecks.
It's important to choose the appropriate level of synchronization based on your application's requirements. In some cases, block-level synchronization may offer better performance and flexibility.
Block-Level Synchronization
Block-level synchronization in Java allows for more fine-grained control over synchronization compared to method-level synchronization. Instead of synchronizing entire methods, you can synchronize specific blocks of code using the synchronized keyword with an object as a monitor. This approach provides greater flexibility and can lead to improved performance by reducing contention in multithreaded applications.
Here's an example of block-level synchronization:
class Account {
private double balance = 1000;
private Object lock = new Object(); // Monitor object for synchronization.
public void withdraw(double amount) {
synchronized (lock) { // Synchronized block.
if (balance >= amount) {
balance -= amount;
System.out.println("Withdrawal of $" + amount + " successful. New balance: $" + balance);
} else {
System.out.println("Insufficient funds for withdrawal.");
}
}
}
public void deposit(double amount) {
synchronized (lock) { // Synchronized block.
balance += amount;
System.out.println("Deposit of $" + amount + " successful. New balance: $" + balance);
}
}
}
In this example, we have an Account class with withdraw() and deposit() methods. Instead of synchronizing the entire methods, we use synchronized blocks to protect critical sections of code within those methods. The lock object serves as a monitor for synchronization.
Block-level synchronization is preferred in scenarios where you want to synchronize specific code sections, allowing other parts of the methods to execute concurrently. It provides better granularity and can lead to reduced contention and improved performance.
When choosing between method-level and block-level synchronization, consider the synchronization requirements of your application to ensure both correctness and efficiency in your multithreaded code.
Introduction to Static Synchronization
In Java, static synchronization is a synchronization mechanism that allows you to control access to static methods or static data members of a class by multiple threads. It ensures that only one thread can execute a synchronized static method or access synchronized static data at a time, preventing data corruption and race conditions in a multithreaded environment.
The Need for Static Synchronization
Static synchronization was introduced to address a specific problem associated with synchronization in Java:
Static synchronization was introduced to provide a mechanism for coordinating access to static resources across multiple threads, ensuring data integrity and preventing issues associated with unsynchronized access.
Example:
Here's an example of static synchronization:
class SharedResource {
private static int counter = 0;
public static synchronized void incrementCounter() {
// Synchronized static method.
counter++;
}
}
public class StaticSynchronizationExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
SharedResource.incrementCounter();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
SharedResource.incrementCounter();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
System.out.println(e);
}
System.out.println("Final counter value: " + SharedResource.getCounter());
}
}
In this example, the incrementCounter() method of the SharedResource class is synchronized as a static method. This ensures that only one thread can increment the counter at a time, even though multiple threads are accessing it concurrently.
Static synchronization is a valuable tool for managing concurrent access to shared static resources and is commonly used in multithreaded Java applications.
Why do we need collections?
How are collections available?
The Collection class is available in the java.util package. The Collection class also provides static methods for sorting, searching, etc.
Key Concepts:
Integration of Collections and Generics:
Example:
ArrayList<String> arrlist = new ArrayList<>();
arrlist.add("Hello");
arrlist.add("World");
import java.util.ArrayList;
public class ArrayListExample {
public static void main(String[] args) {
// Creating an ArrayList
ArrayList<String> fruits = new ArrayList<>();
// Adding elements to the ArrayList
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Displaying the elements of the ArrayList
System.out.println("ArrayList elements: " + fruits);
// Getting the size of the ArrayList
int size = fruits.size();
System.out.println("Size of ArrayList: " + size);
// Accessing elements by index
String firstFruit = fruits.get(0);
System.out.println("First fruit: " + firstFruit);
// Checking if the ArrayList contains a specific element
boolean containsBanana = fruits.contains("Banana");
System.out.println("Contains Banana? " + containsBanana);
// Removing an element by value
fruits.remove("Banana");
System.out.println("After removing Banana: " + fruits);
// Removing an element by index
fruits.remove(0);
System.out.println("After removing the first element: " + fruits);
// Checking if the ArrayList is empty
boolean isEmpty = fruits.isEmpty();
System.out.println("Is ArrayList empty? " + isEmpty);
// Clearing all elements from the ArrayList
fruits.clear();
System.out.println("After clearing the ArrayList: " + fruits);
// Checking if the ArrayList is empty again
isEmpty = fruits.isEmpty();
System.out.println("Is ArrayList empty now? " + isEmpty);
}
}
Explanation of basic methods used:
import java.util.LinkedList;
public class LinkedListDemo {
public static void main(String[] args) {
// Creating a LinkedList of Strings
LinkedList linkedList = new LinkedList<>();
// Adding elements
linkedList.add("Apple");
linkedList.add("Banana");
linkedList.add("Cherry");
// Displaying the LinkedList
System.out.println("LinkedList: " + linkedList);
// Adding an element at a specific position
linkedList.add(1, "Orange");
// Displaying the updated LinkedList
System.out.println("Updated LinkedList: " + linkedList);
// Removing an element
linkedList.remove("Banana");
// Displaying the final LinkedList
System.out.println("Final LinkedList: " + linkedList);
// Accessing elements by index
System.out.println("Element at index 2: " + linkedList.get(2));
// Size of the LinkedList
System.out.println("Size of the LinkedList: " + linkedList.size());
// Clearing the LinkedList
linkedList.clear();
// Displaying the LinkedList after clearing
System.out.println("LinkedList after clearing: " + linkedList);
}
}
import java.util.Stack;
public class StackDemo {
public static void main(String[] args) {
// Creating a Stack of Integers
Stack stack = new Stack<>();
// Pushing elements onto the stack
stack.push(10);
stack.push(20);
stack.push(30);
// Displaying the Stack
System.out.println("Stack: " + stack);
// Popping an element
int poppedElement = stack.pop();
System.out.println("Popped Element: " + poppedElement);
// Displaying the updated Stack
System.out.println("Updated Stack: " + stack);
// Peeking at the top element
int topElement = stack.peek();
System.out.println("Top Element: " + topElement);
// Checking if the stack is empty
System.out.println("Is Stack empty? " + stack.empty());
// Searching for an element
int position = stack.search(10);
System.out.println("Position of 10 in the stack: " + position);
// Clearing the Stack
stack.clear();
// Displaying the Stack after clearing
System.out.println("Stack after clearing: " + stack);
}
}
import java.util.ArrayDeque;
public class ArrayDequeDemo {
public static void main(String[] args) {
// Creating an ArrayDeque of Strings
ArrayDeque arrayDeque = new ArrayDeque<>();
// Adding elements to the end of the deque
arrayDeque.add("Apple");
arrayDeque.add("Banana");
arrayDeque.add("Cherry");
// Displaying the ArrayDeque
System.out.println("ArrayDeque: " + arrayDeque);
// Adding elements at the beginning and end of the deque
arrayDeque.addFirst("Orange");
arrayDeque.addLast("Grapes");
// Displaying the updated ArrayDeque
System.out.println("Updated ArrayDeque: " + arrayDeque);
// Removing the first and last elements
String removedFirst = arrayDeque.removeFirst();
String removedLast = arrayDeque.removeLast();
// Displaying the ArrayDeque after removal
System.out.println("ArrayDeque after removal: " + arrayDeque);
// Peeking at the first and last elements
String firstElement = arrayDeque.peekFirst();
String lastElement = arrayDeque.peekLast();
System.out.println("First Element: " + firstElement);
System.out.println("Last Element: " + lastElement);
// Checking if the deque is empty
System.out.println("Is ArrayDeque empty? " + arrayDeque.isEmpty());
// Clearing the ArrayDeque
arrayDeque.clear();
// Displaying the ArrayDeque after clearing
System.out.println("ArrayDeque after clearing: " + arrayDeque);
}
}