DEV Community

loading...

Java Multithreading. Basics

vrnsky profile image Yegor Voronyansky ・5 min read

Alt Text

Why do you need multithreading

In the distant past, computers did not have operating systems. They only executed one program from start to finish, and that program had direct access to all of the computer's resources. Not only was it difficult to write a program that would run on bare metal, but running only one program at a time proved to be an inefficient waste of computer resources.

With the advent of operating systems, it became possible to run several programs at once. Programs were launched in a process - that is, an isolated, independent environment for which the operating system allocated resources such as memory, file descriptors.

The need to develop operating systems with multithreading support was due to the following factors:

  • Utilization of resources. Programs sometimes have to wait for some kind of third-party operation, such as input and output, and while we waited there was no opportunity to do some useful work.
  • Equity in resource allocation. Many users and programs can have the same rights to computer resources. It is preferable to share all resources between them.
  • Convenience. It is often easier and simpler to write several programs, each of which performs a different task and then coordinate them, than to write one program that does all the tasks.

Simple thread safe counter

public class Sequence {
   private int value;

   public synchronized int getNext() {
       return value++;
   }
}
Enter fullscreen mode Exit fullscreen mode

This counter is thread safe because the getNext () method contains the synchronized keyword, which is used to ensure thread safety.

That is, in this case, each thread that will call this method will receive the actual value. If any thread has not yet executed this method, another thread will be queued and wait for the first thread to complete.

If many threads have access to variables that can change state, then your program is broken. There are three methods to fix this.

  • Restrict access to such variables 
  • Make states immutable
  • Use sync to access state variables

Atomicity

public class Counter {
    private int long value;

    public int getNext() {
         return value++;
    }
}
Enter fullscreen mode Exit fullscreen mode

Race condition

Alt Text

Imagine meeting your friend at Starbucks near the university at 12 noon. But when you arrived, it turned out that there are two Starbucks and you are not sure which meeting will take place. At 12:10 you didn't see your friend at Starbucks A, so you decided to go to Starbucks B to check if your friend was there, but as it turned out, he was not there. There are the following outcomes

  • Your friend is late and is not at any of the Starbucks
  • Your friend came to Starbucks A when you left to Starbucks B
  • Your friend was at Starbucks B and was looking for you, but he didn't find him and went to Starbucks A

Let's imagine the worst situation. At 12:15 pm, you visited both Starbucks. What will you do?

Will you go back to Starbucks A? How many times are you going to go back there?

Until the consistency protocol is developed, you both can spend all day walking between Starbucks.

This variant of the race condition can also be called - check then action. You check for some condition, for example file X does not exist, and then perform an action based on the fact that the condition file X does not exist is true. But immediately after your check, the X file may appear, which leads to unexpected problems

The most famous example of validation and then action is lazy initialization. The goal of lazy initialization is to create an object only when you really need it and another goal is to ensure that the object is initialized only once

public class LazyInitRace {
    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if (instance == null) {
           instance = new ExpensiveObject();
        }
        return instance;
    }
}
Enter fullscreen mode Exit fullscreen mode
if (!vector.contains(element)) {
    vector.add(element);
}
Enter fullscreen mode Exit fullscreen mode

Do you think this code is thread safe?

Answer: No. This attempt to put an item into a collection if there is no such item has a race condition, even if both add & contains methods are atomic. Methods with the synchronized keyword in their signature can do individual operations atomically, but additional synchronization is needed when both such methods are combined in some complex action. Do not overuse the synchronized keyword, as over-synchronization can lead to performance problems.

Volatile variables

The Java language also provides weaker synchronization — volatile variables that update predictably for other threads. It is not recommended to use this synchronization mechanism, since the code obeying such a synchronization mechanism is fragile and difficult to understand at what moment locks are used.

This type of variable is convenient, but it has certain limitations. Basically, variables marked volatile are used as completion flags, interrupt flags, or status flags. But it is important to understand that volatile does not guarantee that the increment will execute correctly (since the increment consists of three operations)

Use volatile only in the following cases

  • To describe variables that do not depend on their current value
  • This variable does not participate in invariants with other state variables
  • When accessing a variable, locking is not required for any other reason

Deadlock

Alt Text
Under deadlock, we mean the following situation: One thread is waiting for the lock taken by another thread to become available, while the first thread is waiting for the lock taken by the second thread to become available

public class TestDeadlockExample1 {  
  public static void main(String[] args) {  
    final String resource1 = "dev.to";  
    final String resource2 = "dev.architecture";  
    // t1 tries to lock resource1 then resource2  
    Thread t1 = new Thread() {  
      public void run() {  
          synchronized (resource1) {  
           System.out.println("Thread 1: locked resource 1");  

           try { Thread.sleep(150);} catch (Exception e) {}  

           synchronized (resource2) {  
            System.out.println("Thread 1: locked resource 2");  
           }  
         }  
      }  
    };  

    // t2 tries to lock resource2 then resource1  
    Thread t2 = new Thread() {  
      public void run() {  
        synchronized (resource2) {  
          System.out.println("Thread 2: locked resource 2");  

          try { Thread.sleep(150);} catch (Exception e) {}  

          synchronized (resource1) {  
            System.out.println("Thread 2: locked resource 1");  
          }  
        }  
      }  
    };  


    t1.start();  
    t2.start();  
  }  
}
Enter fullscreen mode Exit fullscreen mode

Starvation

Fasting describes a situation where a thread cannot regularly access shared resources and cannot make progress. This occurs when shared resources become unavailable for extended periods due to greedy threads. For example, suppose an object provides a synchronized method that often takes a long time to return. If one thread calls this method frequently, other threads that also require frequent synchronized access to the same object will often block.

Livelock

A thread often acts in response to the action of another thread. If the action of another thread is also a response to the action of another thread, then a live block may occur. As with the deadlock, blocked threads cannot move on. However, the threads are not blocked - they are simply too busy answering each other to resume work. This is comparable to two people trying to pass each other in the corridor: Andrey moves to the left to let Gregory pass, and Grigory moves to the right to let Andrey pass. Seeing that they are still blocking each other, Andrei moves to the right, and Gregory - to the left.

Thread Synchronization and its Application

Synchronization of threads is necessary in order to avoid the problems described above. There is a synchronized mechanism that allows threads to be synchronized

Every object in Java has an Intrinsic Lock (monitor). When a synchronized method is called from a thread, it needs to get this monitor. The monitor will be released after the thread finishes executing the method. This way we can synchronize the block of instructions working with the object by forcing the threads to get the monitor before executing the block of instructions. Remember that the monitor can only be held by one thread at a time, so other threads wishing to receive it will be suspended until the current thread exits. Only then can a specific waiting thread get the monitor and continue execution.

Discussion

pic
Editor guide
Collapse
alainvanhout profile image
Alain Van Hout

Interesting post!