In depth analysis of thread synchronization and thread to thread communication in Java

  • 2020-04-01 04:12:26
  • OfStack

Java thread synchronization
When two or more threads need to share a resource, they need some way to determine that the resource is occupied by only one thread at a time. The process to achieve this is called synchronization. As you can see, Java provides unique, language-level support for this.

The key to synchronization is the concept of pipe routines (also known as semaphore). A pipe is a mutex, or mutex, that is exclusively locked. At a given time, only one thread can obtain the pipe program. When a thread needs to be locked, it must enter the pipe. All other threads attempting to enter a locked pipe must hang until the first thread exits the pipe. These other threads are called wait pipe routines. A thread with a pipe routine can enter the same pipe routine again if it wishes.

If you've ever used synchronization in a language like C or C++, you know it's a little weird to use. This is because many languages do not support synchronization on their own. Instead, for synchronized threads, the program must make use of the operating system source language. Fortunately, Java implements synchronization through language elements, and most of the synchronization related complexity is eliminated.

You can synchronize your code in two ways. Both include the use of the synchronized keyword, which is described below.
Using the synchronization method

Synchronization in Java is simple because all objects have their implicit pipe routines. A pipe into an object is a method that is called and modified by the synchronized keyword. When a thread is inside a synchronized method, all other threads of the same instance that attempt to invoke that method (or some other synchronized method) must wait. To exit the pipe and relinquish control of the object to another waiting thread, the thread that owns the pipe simply returns from the synchronized method.

To understand the need for synchronization, let's start with a simple example where synchronization should be used but isn't. The following program has three simple classes. The first is Callme, which has a simple method called (). The call() method has a String parameter called MSG. This method attempts to print the MSG string in square brackets. The interesting thing is that after calling call() to print the left parenthesis and MSG string, thread.sleep (1000) is called, which pauses the current Thread for 1 second.

The Caller, the constructor of the next class, references an instance of Callme and a String, which are stored in target and MSG, respectively. The constructor also creates a new thread that calls the run() method of the object. The thread starts immediately. The run() method of the Caller class calls the call() method of the Callme instance target through the MSG string. Finally, the Synch class starts by creating a simple instance of Callme and three instances of Caller with different message strings.

The same instance of Callme is passed to each Caller instance.


// This program is not synchronized.
class Callme {
  void call(String msg) {
    System.out.print("[" + msg);
    try {
      Thread.sleep(1000);
    } catch(InterruptedException e) {
      System.out.println("Interrupted");
    }
    System.out.println("]");
  }
}

class Caller implements Runnable {
  String msg;
  Callme target;
  Thread t;
  public Caller(Callme targ, String s) {
    target = targ;
    msg = s;
    t = new Thread(this);
    t.start();
  }
  public void run() {
    target.call(msg);
  }
}

class Synch {
  public static void main(String args[]) {
    Callme target = new Callme();
    Caller ob1 = new Caller(target, "Hello");
    Caller ob2 = new Caller(target, "Synchronized");
    Caller ob3 = new Caller(target, "World");
    // wait for threads to end
    try {
     ob1.t.join();
     ob2.t.join();
     ob3.t.join();
    } catch(InterruptedException e) {
     System.out.println("Interrupted");
    }
  }
}

The output of the program is as follows:


Hello[Synchronized[World]
]
]

In this case, the sleep(), call() method allows the execution of the transition to another thread. The result is a mixed output of three message strings. In this program, there are no methods that prevent three threads from calling the same method on the same object at the same time. This is a race because three threads are competing to complete the method. The example USES sleep() to make the effect repetitive and obvious. In most cases, the competition is more complex and unpredictable because you can't be sure when context transitions will occur. This causes the program to run properly and to fail.

In order to achieve the purpose of the above example, you must have the right to use call() continuously. That is, at some point, you must limit only one thread to be able to control it. To do this, you simply add the keyword synchronized before the call() definition, as follows:


class Callme {
  synchronized void call(String msg) {
    ...

This prevents other threads from entering call() while one thread is using call(). After synchronized is added before call(), the program outputs the following:


[Hello]
[Synchronized]
[World]

Any time you have a method or methods that manipulate the internal state of an object in a multithreaded situation, you must use the synchronized keyword to prevent state contention. Remember, once a thread enters the instance's synchronization method, no other thread can enter the same instance's synchronization method. However, other out-of-sync methods for the instance can still be called.
Synchronized statements

While creating a synchronized method within the created class is an easy and efficient way to get synchronized, it is not valid at all times. Why? Think about it. Suppose you want to get synchronized access to a class object that is not designed for multithreaded access, that is, the class does not use synchronized methods. Also, the class is not created by yourself, but by a third party, and you cannot get the source code. This way, you cannot add a synchronized modifier before the associated method. How do you synchronize an object of this class? Fortunately, the solution is simple: you simply put a call to a method defined by the class into a synchronized block.

The following is the normal form of a synchronized statement:


synchronized(object) {
  // statements to be synchronized
}

Where, object is a reference to the synchronized object. If all you want to synchronize is one statement, you don't need curly braces. A synchronized block ensures that a call to an object member method occurs only after the current thread successfully enters the object pipe.

Here is a modified version of the previous program that USES a synchronized block in the run() method:


// This program uses a synchronized block.
class Callme {
  void call(String msg) {
    System.out.print("[" + msg);
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      System.out.println("Interrupted");
    }
    System.out.println("]");
  }
}

class Caller implements Runnable {
  String msg;
  Callme target;
  Thread t;
  public Caller(Callme targ, String s) {
    target = targ;
    msg = s;
    t = new Thread(this);
    t.start();
  }

  // synchronize calls to call()
  public void run() {
    synchronized(target) { // synchronized block
      target.call(msg);
    }
  }
}

class Synch1 {
  public static void main(String args[]) {
    Callme target = new Callme();
    Caller ob1 = new Caller(target, "Hello");
    Caller ob2 = new Caller(target, "Synchronized");
    Caller ob3 = new Caller(target, "World");

    // wait for threads to end
    try {
      ob1.t.join();
      ob2.t.join();
      ob3.t.join();
    } catch(InterruptedException e) {
      System.out.println("Interrupted");
    }
  }
}

Here, the call() method is not modified by synchronized. Synchronized is declared in the run() method of the Caller class. This results in the same exact result as in the previous example, because each thread waits for the previous thread to finish before running.

Communication between Java threads
Multithreading replaces event-looping programs by dividing tasks into discrete and logical units. Threads have a second advantage: they are far from polling. Polling is usually done by repeating a loop of monitoring conditions. Once the conditions are in place, take appropriate action. This wastes CPU time. For example, consider the classic sequence problem, when one thread is producing data and another program is consuming it. To make the problem more interesting, assume that the data producer must wait for the consumer to complete his work before producing new data. In a polling system, consumers waste a lot of CPU cycles waiting for the producer to produce data. Once the producer is done, it starts polling, wasting more CPU time waiting for the consumer's work to finish, and so on. Clearly, this is not welcome.

To avoid polling, Java includes an interprocess communication mechanism implemented through the wait(), notify(), and notifyAll() methods. These methods are implemented in objects with final methods, so all classes contain them. These three methods can only be called in synchronized methods. Although these methods are conceptually advanced from a computer science perspective, they are simple to use in practice:
Wait () tells the called thread to abandon the routine and go to sleep until another thread enters the same routine and calls notify().
Notify () restores the first thread in the same object to call wait().
NotifyAll () restores all threads in the same object that call wait(). The thread with the highest priority runs first.

These methods are declared in Object as follows:


  final void wait( ) throws InterruptedException
  final void notify( )
  final void notifyAll( )


Another form of wait() allows you to define the wait time.

The following example incorrectly implements a simple producer/consumer problem. It consists of four classes: Q, which seeks to obtain a synchronized sequence; The Producer generates queued thread objects; Consumer, the thread object of the consumption sequence; And a PC, creating a single small class of Q, Producer, and Consumer.


// An incorrect implementation of a producer and consumer.
class Q {
  int n;
  synchronized int get() {
    System.out.println("Got: " + n);
    return n;
  }
  synchronized void put(int n) {
    this.n = n;
    System.out.println("Put: " + n);
  }
}
class Producer implements Runnable {
  Q q;
  Producer(Q q) {
    this.q = q;
    new Thread(this, "Producer").start();
  }
  public void run() {
    int i = 0;
    while(true) {
      q.put(i++);
    }
  }
}
class Consumer implements Runnable {
  Q q;
  Consumer(Q q) {
    this.q = q;
    new Thread(this, "Consumer").start();
  }
  public void run() {
    while(true) {
      q.get();
    }
  }
}
class PC {
  public static void main(String args[]) {
    Q q = new Q();
    new Producer(q);
    new Consumer(q);
    System.out.println("Press Control-C to stop.");
  }
}

Although the put() and get() methods in the Q class are synchronized, nothing prevents the producer from overtaking the consumer, and nothing prevents the consumer from consuming the same sequence twice. In this way, you get the following error output (the output will vary depending on the processor speed and load of the task) :


Put: 1
Got: 1
Got: 1
Got: 1
Got: 1
Got: 1
Put: 2
Put: 3
Put: 4
Put: 5
Put: 6
Put: 7
Got: 7

After the producer produces 1, the consumer gets the same 1 five times in turn. Producers continue to produce 2 to 7, and consumers don't have a chance to get them.

The correct way to write this program in Java is to flag both directions with wait() and notify(), as shown below:


// A correct implementation of a producer and consumer.
class Q {
  int n;
  boolean valueSet = false;
  synchronized int get() {
    if(!valueSet)
      try {
        wait();
      } catch(InterruptedException e) {
        System.out.println("InterruptedException caught");
      }
      System.out.println("Got: " + n);
      valueSet = false;
      notify();
      return n;
    }
    synchronized void put(int n) {
      if(valueSet)
      try {
        wait();
      } catch(InterruptedException e) {
        System.out.println("InterruptedException caught");
      }
      this.n = n;
      valueSet = true;
      System.out.println("Put: " + n);
      notify();
    }
  }
  class Producer implements Runnable {
    Q q;
    Producer(Q q) {
    this.q = q;
    new Thread(this, "Producer").start();
  }
  public void run() {
    int i = 0;
    while(true) {
      q.put(i++);
    }
  }
}
class Consumer implements Runnable {
  Q q;
  Consumer(Q q) {
    this.q = q;
    new Thread(this, "Consumer").start();
  }
  public void run() {
    while(true) {
      q.get();
    }
  }
}
class PCFixed {
  public static void main(String args[]) {
    Q q = new Q();
    new Producer(q);
    new Consumer(q);
    System.out.println("Press Control-C to stop.");
  }
}

Internal get(), wait() are called. This suspends execution until the Producer tells the Producer that the data is ready. At this point, the internal get() is resumed execution. After getting the data, get() calls notify(). This tells the Producer to input more data into the sequence. Inside put(), wait() suspends execution until the Consumer fetches the item in the sequence. When execution continues, the next data item is put into the sequence, and the notify() is called, which informs the Consumer that it should move the data.

Here is the output of the program, which clearly shows the synchronization behavior:


Put: 1
Got: 1
Put: 2
Got: 2
Put: 3
Got: 3
Put: 4
Got: 4
Put: 5
Got: 5


Related articles: