User Roman Beskrovnyi
Roman Beskrovnyi
Level 35
Kharkiv

Top 50 job interview questions and answers for Java Core. Part 2

Published in the Java Developer group
Top 50 job interview questions and answers for Java Core. Part 1Top 50 job interview questions and answers for Java Core. Part 2 - 1

Multithreading

24. How do I create a new thread in Java?

One way or another, a thread is created using the Thread class. But there are various ways of doing this…
  1. Inherit java.lang.Thread.
  2. Implement the java.lang.Runnable interface — the Thread class's constructor takes a Runnable object.
Let's talk about each of them.

Inherit the Thread class

In this case, we make our class inherit java.lang.Thread. It has a run() method, and that's just what we need. All the life and logic of the new thread will be in this method. It is kind of like a main method for the new thread. After that, all that remains is to create an object of our class and call the start() method. This will create a new thread and start executing its logic. Let's take a look:

/**
* An example of how to create threads by inheriting the {@link Thread} class.
*/
class ThreadInheritance extends Thread {

   @Override
   public void run() {
       System.out.println(Thread.currentThread().getName());
   }

   public static void main(String[] args) {
       ThreadInheritance threadInheritance1 = new ThreadInheritance();
       ThreadInheritance threadInheritance2 = new ThreadInheritance();
       ThreadInheritance threadInheritance3 = new ThreadInheritance();
       threadInheritance1.start();
       threadInheritance2.start();
       threadInheritance3.start();
   }
}
The console output will be something like this:
Thread-1 Thread-0 Thread-2
That is, even here we see that threads are executed not in order, but rather as the JVM sees fit to run them :)

Implement the Runnable interface

If you are against inheritance and/or already inherit some other class, you can use the java.lang.Runnable interface. Here, we make our class implement this interface by implementing the run() method, just as in the example above. All that remains is to create Thread objects. It would seem that more lines of code are worse. But we know how pernicious inheritance is and that it is better to avoid it by all means ;) Take a look:

/**
* An example of how to create threads from the {@link Runnable} interface.
* It's easier than easy — we implement this interface and then pass an instance of our object
* to the constructor.
*/
class ThreadInheritance implements Runnable {

   @Override
   public void run() {
       System.out.println(Thread.currentThread().getName());
   }

   public static void main(String[] args) {
       ThreadInheritance runnable1 = new ThreadInheritance();
       ThreadInheritance runnable2 = new ThreadInheritance();
       ThreadInheritance runnable3 = new ThreadInheritance();

       Thread threadRunnable1 = new Thread(runnable1);
       Thread threadRunnable2 = new Thread(runnable2);
       Thread threadRunnable3 = new Thread(runnable3);

       threadRunnable1.start();
       threadRunnable2.start();
       threadRunnable3.start();
   }
}
And here's the result:
Thread-0 Thread-1 Thread-2

25. What's the difference between a process and a thread?

Top 50 job interview questions and answers for Java Core. Part 2 - 2A process and a thread are different in the following ways:
  1. A running program is called a process, but a thread is a piece of a process.
  2. Processes are independent, but threads are pieces of a process.
  3. Processes have different address spaces in memory, but threads share a common address space.
  4. Context switching between threads is faster than switching between processes.
  5. Inter-process communication is slower and more expensive than inter-thread communication.
  6. Any changes in a parent process do not affect a child process, but changes in a parent thread can affect a child thread.

26. What are the benefits of multithreading?

  1. Multithreading allows an application/program to always be responsive to input, even if it is already running some background tasks;
  2. Multithreading makes it possible to complete tasks faster, because threads run independently;
  3. Multithreading provides better use of cache memory, because threads can access shared memory resources;
  4. Multithreading reduces the number of servers required, because one server can run multiple threads simultaneously.

27. What are the states in a thread's life cycle?

Top 50 job interview questions and answers for Java Core. Part 2 - 3
  1. New: In this state, Thread object is created using the new operator, but a new thread doesn't exist yet. The thread does not start until we call the start() method.
  2. Runnable: In this state, the thread is ready to run after the start() method is called. However, it has not yet been selected by the thread scheduler.
  3. Running: In this state, the thread scheduler picks a thread from a ready state, and it runs.
  4. Waiting/Blocked: in this state, a thread isn't running, but it is still alive or waiting for another thread to complete.
  5. Dead/Terminated: when a thread exits the run() method, it is in a dead or terminated state.

28. Is it possible to run a thread twice?

No, we cannot restart a thread, because after a thread starts and runs, it goes into the Dead state. If we do try to start a thread twice, a java.lang.IllegalThreadStateException will be thrown. Let's take a look:

class DoubleStartThreadExample extends Thread {

   /**
    * Simulate the work of a thread
    */
   public void run() {
	// Something happens. At this state, this is not essential.
   }

   /**
    * Start the thread twice
    */
   public static void main(String[] args) {
       DoubleStartThreadExample doubleStartThreadExample = new DoubleStartThreadExample();
       doubleStartThreadExample.start();
       doubleStartThreadExample.start();
   }
}
There will be an exception as soon as execution comes to the second start of the same thread. Try it yourself ;) It's better to see this once than to hear about it a hundred times.

29. What if you call run() directly without calling start()?

Yes, you can certainly call the run() method, but a new thread will not be created, and the method will not run on a separate thread. In this case, we have an ordinary object calling an ordinary method. If we're talking about the start() method, then that's another matter. When this method is called, the JVM starts a new thread. This thread, in turn, calls our method ;) Don't believe it? Here, give it a try:

class ThreadCallRunExample extends Thread {

   public void run() {
       for (int i = 0; i < 5; i++) {
           System.out.print(i);
       }
   }

   public static void main(String args[]) {
       ThreadCallRunExample runExample1 = new ThreadCallRunExample();
       ThreadCallRunExample runExample2 = new ThreadCallRunExample();

       // Two ordinary methods will be called in the main thread, one after the other.
       runExample1.run();
       runExample2.run();
   }
}
And the console output will look like this:
0123401234
As you can see, no thread was created. Everything worked just as in an ordinary class. First, the method of the first object was executed, and then the second.

30. What is a daemon thread?

A daemon thread is a thread that performs tasks at a lower priority than another thread. In other words, its job is to perform auxiliary tasks that need to be done only in conjunction with another (main) thread. There are many daemon threads that run automatically, such as garbage collection, finalizer, etc.

Why does Java terminate a daemon thread?

The daemon thread's sole purpose is to provide background support to a user's thread. Accordingly, if the main thread is terminated, then the JVM automatically terminates all of its daemon threads.

Methods of the Thread class

The java.lang.Thread class provides two methods for working with a daemon thread:
  1. public void setDaemon(boolean status) — This method indicates whether this will be a daemon thread. The default is false. This means that no daemon threads will be created unless you specifically say so.
  2. public boolean isDaemon() — This method is essentially a getter for the daemon variable, which we set using the previous method.
Example:

class DaemonThreadExample extends Thread {

   public void run() {
       // Checks whether this thread is a daemon
       if (Thread.currentThread().isDaemon()) {
           System.out.println("daemon thread");
       } else {
           System.out.println("user thread");
       }
   }

   public static void main(String[] args) {
       DaemonThreadExample thread1 = new DaemonThreadExample();
       DaemonThreadExample thread2 = new DaemonThreadExample();
       DaemonThreadExample thread3 = new DaemonThreadExample();

       // Make thread1 a daemon thread.
       thread1.setDaemon(true);

       System.out.println("daemon? " + thread1.isDaemon());
       System.out.println("daemon? " + thread2.isDaemon());
       System.out.println("daemon? " + thread3.isDaemon());

       thread1.start();
       thread2.start();
       thread3.start();
   }
}
Console output:
daemon? true daemon? false daemon? false daemon thread user thread user thread
From the output, we see that inside the thread itself, we can use the static currentThread() method to find out which thread it is. Alternatively, if we have a reference to the thread object, we can also find out directly from it. This provides the necessary level of configurability.

31. Is it possible to make a thread a daemon after it has been created?

No. If you attempt to do this, you will get an IllegalThreadStateException. This means we can only create a daemon thread before it starts. Example:

class SetDaemonAfterStartExample extends Thread {

   public void run() {
       System.out.println("Working...");
   }

   public static void main(String[] args) {
       SetDaemonAfterStartExample afterStartExample = new SetDaemonAfterStartExample();
       afterStartExample.start();
      
       // An exception will be thrown here
       afterStartExample.setDaemon(true);
   }
}
Console output:
Working... Exception in thread "main" java.lang.IllegalThreadStateException at java.lang.Thread.setDaemon(Thread.java:1359) at SetDaemonAfterStartExample.main(SetDaemonAfterStartExample.java:14)

32. What is a shutdown hook?

A shutdown hook is a thread that is implicitly called before the Java virtual machine (JVM) is shut down. Thus, we can use it to release a resource or save state when the Java virtual machine shuts down normally or abnormally. We can add a shutdown hook using the following method:

Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());
As shown in the example:

/**
* A program that shows how to start a shutdown hook thread,
* which will be executed right before the JVM shuts down
*/
class ShutdownHookThreadExample extends Thread {

   public void run() {
       System.out.println("shutdown hook executed");
   }

   public static void main(String[] args) {

       Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());

       System.out.println("Now the program is going to fall asleep. Press Ctrl+C to terminate it.");
       try {
           Thread.sleep(60000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}
Console output:
Now the program is going to fall asleep. Press Ctrl+C to terminate it. shutdown hook executed

33. What is synchronization?

In Java, synchronization is the ability to control multiple threads' access to any shared resource. When multiple threads try to perform the same task simultaneously, you could get an incorrect result. To fix this problem, Java uses synchronization, which allows only one thread to run at a time. Synchronization can be achieved in three ways:
  • Synchronizing a method
  • Synchronizing a specific block
  • Static synchronization

Synchronizing a method

A synchronized method is used to lock an object for any shared resource. When a thread calls a synchronized method, it automatically acquires the object's lock and releases it when the thread completes its task. To make this work, you need to add the synchronized keyword. We can see how this works by looking at an example:

/**
* An example where we synchronize a method. That is, we add the synchronized keyword to it.
* There are two authors who want to use one printer. Each of them has composed their own poems
* And of course they don’t want their poems mixed up. Instead, they want work to be performed in * * * order for each of them
*/
class Printer {

   synchronized void print(List<String> wordsToPrint) {
       wordsToPrint.forEach(System.out::print);
       System.out.println();
   }

   public static void main(String args[]) {
       // One object for two threads
       Printer printer  = new Printer();

       // Create two threads
       Writer1 writer1 = new Writer1(printer);
       Writer2 writer2 = new Writer2(printer);

       // Start them
       writer1.start();
       writer2.start();
   }
}

/**
* Author No. 1, who writes an original poem.
*/
class Writer1 extends Thread {
   Printer printer;

   Writer1(Printer printer) {
       this.printer = printer;
   }

   public void run() {
       List<string> poem = Arrays.asList("I ", this.getName(), " Write", " A Letter");
       printer.print(poem);
   }

}

/**
* Author No. 2, who writes an original poem.
*/
class Writer2 extends Thread {
   Printer printer;

   Writer2(Printer printer) {
       this.printer = printer;
   }

   public void run() {
       List<String> poem = Arrays.asList("I Do Not ", this.getName(), " Not Write", " No Letter");
       printer.print(poem);
   }
}
And the console output is this:
I Thread-0 Write A Letter I Do Not Thread-1 Not Write No Letter

Synchronization block

A synchronized block can be used to perform synchronization on any particular resource in a method. Let's say that in a large method (yes, you shouldn't write them, but sometimes they happen) you need to synchronize only a small section for some reason. If you put all of the method's code in a synchronized block, it will work the same as a synchronized method. The syntax looks like this:

synchronized ("object to be locked") {
   // The code that must be protected
}
To avoid repeating the previous example, we will create threads using anonymous classes, i.e. we will immediately implement the Runnable interface.

/**
* This is how a synchronization block is added.
* Inside the block, you need to specify which object's mutex will be acquired.
*/
class Printer {

   void print(List<String> wordsToPrint) {
       synchronized (this) {
           wordsToPrint.forEach(System.out::print);
       }
       System.out.println();
   }

   public static void main(String args[]) {
       // One object for two threads
       Printer printer = new Printer();

       // Create two threads
       Thread writer1 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I ", "Writer1", " Write", " A Letter");
               printer.print(poem);
           }
       });
       Thread writer2 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I Do Not ", "Writer2", " Not Write", " No Letter");
               printer.print(poem);
           }
       });

       // Start them
       writer1.start();
       writer2.start();
   }
}

}
And the console output is this:
I Writer1 Write A Letter I Do Not Writer2 Not Write No Letter

Static synchronization

If you make a static method synchronized, then the locking will happen on the class, not the object. In this example, we perform static synchronization by applying the synchronized keyword to a static method:

/**
* This is how a synchronization block is added.
* Inside the block, you need to specify which object's mutex will be acquired.
*/
class Printer {

   static synchronized void print(List<String> wordsToPrint) {
       wordsToPrint.forEach(System.out::print);
       System.out.println();
   }

   public static void main(String args[]) {

       // Create two threads
       Thread writer1 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I ", "Writer1", " Write", " A Letter");
               Printer.print(poem);
           }
       });
       Thread writer2 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I Do Not ", "Writer2", " Not Write", " No Letter");
               Printer.print(poem);
           }
       });

       // Start them
       writer1.start();
       writer2.start();
   }
}
And the console output is this:
I Do Not Writer2 Not Write No Letter I Writer1 Write A Letter

34. What is a volatile variable?

In multithreaded programming, the volatile keyword is used for thread safety. When a mutable variable is modified, the change is visible to all other threads, so a variable can be used by one thread at a time. By using the volatile keyword, you can guarantee that a variable is thread-safe and stored in shared memory, and that threads won't store it in their caches. What does this look like?

private volatile AtomicInteger count;
We just add volatile to the variable. But keep in mind that this does not mean complete thread safety... After all, operations on the variable may not be atomic. That said, you can use Atomic classes that do operations atomically, i.e. in a single CPU instruction. There are many such classes in the java.util.concurrent.atomic package.

35. What is deadlock?

In Java, deadlock is something that can happen as part of multithreading. A deadlock can occur when a thread is waiting for an object's lock acquired by another thread, and the second thread is waiting for the object's lock acquired by the first thread. This means that the two threads are waiting for each other, and execution of their code cannot continue.Top 50 job interview questions and answers for Java Core. Part 2 - 4Let's consider an example that has a class that implements Runnable. Its constructor takes two resources. The run() method acquires the lock for them in order. If you create two objects of this class, and pass the resources in a different order, then you can easily run into deadlock:

class DeadLock {

   public static void main(String[] args) {
       final Integer r1 = 10;
       final Integer r2 = 15;

       DeadlockThread threadR1R2 = new DeadlockThread(r1, r2);
       DeadlockThread threadR2R1 = new DeadlockThread(r2, r1);

       new Thread(threadR1R2).start();
       new Thread(threadR2R1).start();
   }
}

/**
* A class that accepts two resources.
*/
class DeadlockThread implements Runnable {

   private final Integer r1;
   private final Integer r2;

   public DeadlockThread(Integer r1, Integer r2) {
       this.r1 = r1;
       this.r2 = r2;
   }

   @Override
   public void run() {
       synchronized (r1) {
           System.out.println(Thread.currentThread().getName() + " acquired resource: " + r1);

           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }

           synchronized (r2) {
               System.out.println(Thread.currentThread().getName() + " acquired resource: " + r2);
           }
       }
   }
}
Console output:
The first thread acquired the first resource The second thread acquired the second resource

36. How do you avoid deadlock?

Because we know how deadlock occurs, we can draw some conclusions...
  • In the example above, the deadlock occurs due to the fact that we have nested locking. That is, we have a synchronized block inside of a synchronized block. To avoid this, instead of nesting, you need to create a new higher abstraction layer, move the synchronization to the higher level, and eliminate the nested locking.
  • The more locking you do, the more likely there will be a deadlock. Therefore, each time you add a synchronized block, you need to think about whether you really need it and whether you can avoid adding a new one.
  • Using Thread.join(). You can also run into deadlock while one thread waits for another. To avoid this problem, you might consider setting a timeout for the join() method.
  • If we have one thread, then there will be no deadlock ;)

37. What is a race condition?

If real-life races involve cars, then races in multithreading involve threads. But why? :/ There are two threads that are running and can access the same object. And they may attempt to update the shared object's state at the same time. Everything is clear so far, right? Threads are executed either literally in parallel (if the processor has more than one core) or sequentially, with the processor allocating interleaved time slices. We cannot manage these processes. This means that when one thread reads data from an object we cannot guarantee that it will have time to change the object BEFORE some other thread does so. Such problems arise when we have these "check-and-act" combos. What does that mean? Suppose we have an if statement whose body changes the if-condition itself, for example:

int z = 0;

// Check
if (z < 5) {
// Act
   z = z + 5;
}
Two threads could simultaneously enter this code block when z is still zero and then both threads could change its value. As a result, we will not get the expected value of 5. Instead, we would get 10. How do you avoid this? You need to acquire a lock before checking and acting, and then release the lock afterward. That is, you need to have the first thread enter the if block, perform all the actions, change z, and only then give the next thread the opportunity to do the same. But the next thread will not enter the if block, since z will now be 5:

// Acquire the lock for z
if (z < 5) {
   z = z + 5;
}
// Release z's lock
===================================================

Instead of a conclusion

I want to say thanks to everyone who read to the end. It was a long way, but you endured! Maybe not everything is clear. This is normal. When I first started studying Java, I couldn't wrap my brain around what a static variable is. But no big deal. I slept on it, read a few more sources, and then the understanding came. Preparing for an interview is more an academic question rather than a practical one. As a result, before each interview, you should review and refresh in your memory those things that you may not use very often.

And as always, here are some useful links:

Thank you all for reading. See you soon :) My GitHub profileTop 50 job interview questions and answers for Java Core. Part 2 - 5
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION