Multithreading

Multitasking means ability of the computer to perform multiple tasks simultaneously. For instance, while listening to music, you can simultaneously use a paint application to create art. Multitasking can process-based or thread-based.

Process-based multitasking involves running multiple process independently. For example, running different applications like a web browser, word processor, and media player simultaneously on a computer.

Thread- based multitasking involves running multiple threads within the same process. ie multiple path of execution within the same process. Threads share the same memory space and resources. For instance, a word processor application might use multiple threads to handle tasks such as spell checking, formatting, and printing simultaneously.

Threads share same address space. Hence context switching between threads is less expensive than the processes and the cost of communication between the threads is relatively low.

A thread is an independent sequential path of execution in a process.

When an application runs, a main thread is automatically created and if no child threads are spawned, the program terminates once main thread completes its execution. If the child threads or user threads are spawned, the main method can finish but program will still run until child threads are completed.

Threads can be user thread or daemon thread. The thread class marks the status of thread as daemon thread or user thread. A program will run if there are user thread is not completed but it will terminate when all the user threads and main thread are completed and daemon thread is not completed.

There are two ways to create a thread.

  • Extending the Thread class
  • Implementing Runnable interface

Main.java


public class Main {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
MultiThreading1 multiThread = new MultiThreading1();
            Thread thread = new Thread(multiThread);
            thread.start();
}
}
}

MultiThreading1.java


public class MultiThreading1 implements Runnable {
private int threadNumber;

public MultiThreading1(int threadNumber) {
this.threadNumber = threadNumber;
}

@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread Number"+ threadNumber +": " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}

Main.java


public class Main {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
MultiThreading1 thread = new MultiThreading1(i);
thread.start();
}
}
}

MultiThreading2.java


public class MultiThreading2 extends Thread{
private int threadNumber;

public MultiThreading2(int threadNumber) {
this.threadNumber = threadNumber;
}

@Override public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread Number"+ threadNumber +": " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}

Synchronization

Since thread share same memory space, they share the same resources (objects). Hence there are situations where one thread has to access the shared resource at a time. In such cases we need to use synchronization method. While a thread is inside the synchronization method of an object, all other threads that wish to execute this synchronized method or any other synchronized method of the object has to wait. This restriction does not apply for the thread that is currently executing the method. Such a method can invoke other synchronized method with any lock. The non synchronized method can be executed by any thread at any time. A thread must acquire the object lock associated with shared object before it can access it.

Race Condition

They are particularly common in concurrent programming, where multiple threads or processes are executing simultaneously and accessing shared resources, such as variables or files, leave the value in undefined or inconsistent state. The race condition can be prevented

  • Synchronized Methods: Use the synchronized keyword to make methods thread-safe. When a thread enters a synchronized method, it acquires the intrinsic lock associated with the object and releases it when the method exits.
  • Synchronized Blocks: Use synchronized blocks to protect critical sections of code. This allows for finer-grained control over synchronization compared to synchronizing entire methods.
  • Atomic Variables: Use atomic classes from the java.util.concurrent.atomic package, such as AtomicInteger, to perform atomic operations without explicit locking.
  • Lock Objects: Use explicit lock objects from the java.util.concurrent.locks package. This provides more flexibility and control over locking compared to intrinsic locks.


import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
* In this example, we have a Counter class with a count variable that represents
 * a simple counter.
* We then create two threads, thread1 and thread2, that increment the
 * counter concurrently by calling the increment() method.
* However, since both threads are accessing and modifying the count variable
 * without proper synchronization, a race condition occurs. 
 * As a result, the final count printed may not always be 2000, as expected.
 * Instead, it may vary due to the interleaved execution of the threads and the
 * unpredictable ordering of their operations.
*/
class Counter {
private int count = 0;

public void increment() {
count++;
}

public int getCount() {
return count;
}
}

/**
* Solution 1 for race condition
*
* However, since both threads are accessing and modifying the count variable with
 * proper synchronization method, the final count printed will always be 2000, 
 *  as expected.
*
*/
class CounterSynchronized {
private int count = 0;

public synchronized void increment() {
count++;
}

public int getCount() {
return count;
}
}

/**
* Solution 2 for race condition
*
* However, since both threads are accessing and modifying the count variable
 * with proper synchronization block, the final count printed will always be 2000, 
 * as expected.
*
*/
class CounterSynchronizedBlock {
private int count = 0;

public void increment() {
synchronized(this){
count++;
}
}

public int getCount() {
return count;
}
}

/**
* Solution 3 for race condition
*
* However, since both threads are accessing and modifying the count variable which
 * is Atomic variable, to perform atomic operations without explicit locking.
 * The final count printed will always be 2000, as expected.
*
*/

class CounterAtomic {
private AtomicInteger count = new AtomicInteger(0);

public void increment() {
count.incrementAndGet();
}

public int getCount() {
return count.get();
}
}

public class RaceCondition {
public static void main(String[] args) {
// Counter counter = new Counter();
// CounterSynchronizedBlock counter = new CounterSynchronizedBlock();
// CounterSynchronized counter = new CounterSynchronized();
// CounterAtomic counter = new CounterAtomic();
        CounterLock counter = new CounterLock();

// Creating two threads to increment the counter concurrently
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});

Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});

// Starting both threads
thread1.start();
thread2.start();

// Waiting for both threads to complete
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

// Printing the final count
System.out.println("Final count: " + counter.getCount());
}

}

Deadlock


Livelock





Comments

Popular posts from this blog

Luhn Algorithm

JWT (JSON Web Token)

Security in General