Saturday, April 26, 2025

synchronized

1. Risks of Using synchronized (this)

Using synchronized (this) can lead to several problems in multi-threaded environments, particularly:

  • Unintended Locking by External Code:
    When you use synchronized (this), the lock is exposed to external code. This can cause unwanted interference from other parts of the program that also synchronize on the same object, potentially causing threads to block or experience delays. External code synchronizing on this can block your own thread's execution, leading to unpredictable behavior, performance degradation, and even deadlocks.

  • Limited Scope for Synchronization:
    Synchronizing on this locks the entire object, which might prevent other independent operations on the same object from proceeding. This reduces concurrency and can result in unnecessary thread blocking. It can also make it difficult to trace or understand synchronization flow, which increases maintenance complexity.

  • Difficulty with Subclassing:
    If the class is subclassed, other classes can override methods and synchronize on the same this object, causing potential conflicts between parent and child classes.

2. Example and Demonstration of Issues with synchronized (this)

To demonstrate these issues, I modified an example where the main thread and multiple MyThread instances synchronize on the same this reference. This creates an interference scenario where external synchronization can block thread execution.

class MyThread extends Thread {
    // Synchronizing on 'this' (the instance of MyThread)
    @Override
    public void run() {
        synchronized (this) {
            try {
                System.out.println(Thread.currentThread().getName() + " is running.");
                // Simulate some work with a delay
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + " finished.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class SynchronizedThisExample {
    public static void main(String[] args) {
        // Create two thread instances
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        
        // Start the threads
        thread1.start();
        thread2.start();
        
        // External code synchronizing on the same 'this' instance of thread1
        synchronized (thread1) {
            try {
                // Simulating external interference
                System.out.println("Main thread is holding the lock on thread1.");
                // The main thread holds the lock while thread1 is still running
                Thread.sleep(2000);
                System.out.println("Main thread released the lock on thread1.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Explanation of the Modified Example:

  • External Synchronization: In the main method, the external synchronization on thread1 causes interference with the internal synchronized block within the run() method of MyThread. The main thread locks thread1, preventing it from executing its run() method until the lock is released, which demonstrates how external code can disrupt thread behavior.
  • Concurrency Issues: While thread2 runs independently without interference, thread1 is blocked by the main thread's lock. This could result in delays and poor performance.
  • Deadlock Risk: If the main thread synchronizes on multiple objects (thread1 and thread2), deadlocks can easily occur, especially when synchronization orders are not carefully controlled.

Output Example:

Thread-0 is running.
Main thread is holding the lock on thread1.
Thread-0 finished.
Main thread released the lock on thread1.
Thread-1 is running.
Thread-1 finished.

3. Why You Should Avoid synchronized (this)

Here’s a summary of why using synchronized (this) is risky:

  • External interference: Any external thread can synchronize on the same object (this), causing the current thread to be blocked unexpectedly.
  • Lock contention: Multiple threads synchronizing on the same object can result in lock contention, leading to reduced concurrency and performance degradation.
  • Maintenance complexity: The design can be hard to maintain and debug since synchronization is exposed to other parts of the code.

4. Best Practices and Alternative

Instead of using synchronized (this), it is recommended to use a private lock object for synchronization:

private final Object lock = new Object();

public void run() {
    synchronized (lock) {
        // Critical section code
    }
}

This provides better control over synchronization, ensuring that external code cannot interfere with the critical section, and avoiding the issues associated with synchronized (this).




Conclusion

By avoiding synchronized (this), you can reduce the risks of lock contention, deadlocks, and difficult-to-maintain code. Using a dedicated lock object improves concurrency and makes your code easier to understand and maintain.

Saturday, April 19, 2025

final

A final variable is a variable whose value doesn't change once it has had a value assigned to it. In other words, the variable is a constant. Any variable can be made final by applying the keyword final to its declaration.

    class TestClass{  
      final int x = 10;    
      final static int y = 20;   
      public static void main(final String[] args){    
      final TestClass tc = new TestClass();         
      //tc.x = 30; //will not compile.       
      //y = 40; //will not compile.     
      //args = new String[0]; //will not compile      
      //tc = new TestClass(); //will not compile   
      System.out.println(tc.x+" "+y+" "+args+" "+tc);  
      }
    }
    
 
Observe that in the above code, I have made an instance variable, a static variable, a method parameter, and a local variable final. It prints 10 20 [Ljava.lang.String;@52d1fadb TestClass@35810a60 when compiled and run.
You cannot reassign any value to a final variable, therefore, the four statements that try to modify their values won't compile. Remember that when you make a reference variable final, it only means that the reference variable cannot refer to any other object. It doesn't mean that the contents of that object can't change. For example, consider the following code:
 
      
      class Data{    
        int x = 10; 
      }
      public class TestClass {   
        public static void main(String[] args){ 
        final Data d = new Data();
        //d = new Data(); //won't compile because d is final       
        d.x = 20; //this is fine because we are not changing d here.     
       }
      }
 In the above code, we cannot make d refer to a different Data object once it is initialized because d is final, however, 
 we can certainly use d to manipulate the Data in object to which it points.     
     
      

Wednesday, April 16, 2025

Java Operators

 











The Java expression int idx = (int)(Math.random()*101) - 50; generates a random integer within the range of -50 to +50. Here's how it works step by step:

  1. Math.random() generates a random floating-point number between 0.0 (inclusive) and 1.0 (exclusive). So, the value produced by Math.random() will always lie in the range [0.0, 1.0).

  2. Math.random() * 101 multiplies the random number by 101. This means the result will be a random floating-point number in the range [0.0, 101.0). So, the number will be between 0 (inclusive) and just below 101 (exclusive).

  3. Casting to int: The (int) casting operator truncates the decimal part, effectively rounding down the value to the nearest integer. So, for example, if Math.random() produces a value of 0.99, then Math.random() * 101 results in 99.99, and casting it to an int gives 99.

  4. Subtracting 50: Finally, subtracting 50 from the integer result shifts the range of values. If the random number after casting lies between 0 and 100, subtracting 50 will shift the range to be between -50 and +50.

Range of Values:

  • The smallest value Math.random() can produce is 0, which will give 0 * 101 = 0, and subtracting 50 results in -50.

  • The largest value Math.random() can produce is just below 1.0, which will give 1 * 101 = 101, and subtracting 50 results in +51. However, since we're truncating the value when casting to int, the maximum value after the cast will be 100, and after subtracting 50, the result will be 50.

So, the possible values for idx will range from:

  • Minimum: -50 (if Math.random() generates 0)

  • Maximum: 50 (if Math.random() generates a value just below 1)

Thus, the output lies in the range from -50 to +50 inclusive.

Example:

  • If Math.random() returns 0.25, then 0.25 * 101 = 25.25, and after casting to int, you get 25. Subtracting 50 gives 25 - 50 = -25.

  • If Math.random() returns 0.75, then 0.75 * 101 = 75.75, and after casting to int, you get 75. Subtracting 50 gives 75 - 50 = 25


Sunday, April 6, 2025

AlternatingPrinter ( wait() & notify() )




class AlternatingPrinter {
   private final Object lock = new Object();
// Indicates whether it's the number thread's turn
 private volatile boolean numberTurn = true;

public static void main(String[] args) {
AlternatingPrinter printer = new AlternatingPrinter();
Thread numberThread = new Thread(() -> printer.printNumbers());
Thread letterThread = new Thread(() -> printer.printLetters());
numberThread.start();
letterThread.start();
}
public void printNumbers() {
for (int i = 1; i <= 26; i++) { // Adjust the range as needed
synchronized (lock) {
while (!numberTurn) {
try {
lock.wait(); // Wait until it's this thread's turn
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.print(i + " ");
numberTurn = false; // Pass the turn to the letter thread
lock.notifyAll(); // Notify the waiting thread
}
}
}
public void printLetters() {
for (char c = 'A'; c <= 'Z'; c++) { // Adjust the range as needed
synchronized (lock) {
while (numberTurn) {
try {
lock.wait(); // Wait until it's this thread's turn
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.print(c + " ");
numberTurn = true; // Pass the turn to the number thread
lock.notifyAll(); // Notify the waiting thread
}
}
}

} 

Integer

Integer.MAX_VALUE is the maximum value of an int can hold in Java
Both int and Integer share the same range because Integer is just an Object representaion of int
2^31 = 2,147,483,647IntegerMAX_VALUE -> refers to the max value of an int.It is defined in the Integer class for convenienceBoth int and Integer can hold values from -2,147,483,648 to 2,147,483,647(2.147 billion)

 public class IntegerTest {
    public static void main(String[] args) {
Integer a = 100, b = 100;
Integer x = 200, y = 200;

System.out.println(a == b); // (A)
System.out.println(x == y); // (B)
}
}

Answer:

true
false
  • Java caches Integer values from -128 to 127.

Deadlock

 class DeadlockExample {

public static void main(String[] args) {
final Object lock1 = new Object();
final Object lock2 = new Object();

Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1 locked lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Thread 1 locked lock2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2 locked lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Thread 2 locked lock1");
}
}
});
t1.start();
t2.start();
}
}

CompletableFuture

  Welcome back to  our concurrency series ! In our first discussion, we likely touched on the traditional models of threading. Today, we’re ...