Pertanyaan Wawancara Konkurensi Java (+ Jawaban)

Artikel ini adalah bagian dari serial: • Pertanyaan Wawancara Koleksi Java

• Pertanyaan Wawancara Sistem Jenis Java

• Pertanyaan Wawancara Konkurensi Java (+ Jawaban) (artikel saat ini) • Struktur Kelas Java dan Pertanyaan Wawancara Inisialisasi

• Java 8 Pertanyaan Wawancara (+ Jawaban)

• Manajemen Memori dalam Pertanyaan Wawancara Java (+ Jawaban)

• Pertanyaan Wawancara Umum Java (+ Jawaban)

• Pertanyaan Wawancara Kontrol Aliran Java (+ Jawaban)

• Pertanyaan Wawancara Pengecualian Jawa (+ Jawaban)

• Pertanyaan Wawancara Anotasi Jawa (+ Jawaban)

• Pertanyaan Wawancara Top Spring Framework

1. Perkenalan

Concurrency di Java adalah salah satu topik paling kompleks dan canggih yang diangkat selama wawancara teknis. Artikel ini memberikan jawaban atas beberapa pertanyaan wawancara tentang topik yang mungkin Anda temui.

P1. Apa Perbedaan Antara Proses dan Untaian?

Baik proses maupun utas adalah unit konkurensi, tetapi keduanya memiliki perbedaan mendasar: proses tidak berbagi memori yang sama, sedangkan utas melakukannya.

Dari sudut pandang sistem operasi, proses adalah perangkat lunak independen yang berjalan di ruang memori virtualnya sendiri. Setiap sistem operasi multitasking (yang berarti hampir semua sistem operasi modern) harus memisahkan proses dalam memori sehingga satu proses yang gagal tidak akan menyeret semua proses lain ke bawah dengan mengacak memori umum.

Dengan demikian, proses biasanya terisolasi, dan bekerja sama melalui komunikasi antar-proses yang didefinisikan oleh sistem operasi sebagai semacam API perantara.

Sebaliknya, utas adalah bagian dari aplikasi yang berbagi memori yang sama dengan utas lain dari aplikasi yang sama. Menggunakan memori umum memungkinkan untuk memangkas banyak overhead, merancang utas untuk bekerja sama dan bertukar data di antara mereka lebih cepat.

P2. Bagaimana Cara Membuat Contoh Thread dan Menjalankannya?

Untuk membuat instance utas, Anda memiliki dua opsi. Pertama, teruskan instance Runnable ke konstruktornya dan panggil start () . Runnable adalah antarmuka fungsional, sehingga dapat diteruskan sebagai ekspresi lambda:

Thread thread1 = new Thread(() -> System.out.println("Hello World from Runnable!")); thread1.start();

Thread juga mengimplementasikan Runnable , jadi cara lain untuk memulai thread adalah membuat subkelas anonim, mengganti metode run () , lalu memanggil start () :

Thread thread2 = new Thread() { @Override public void run() { System.out.println("Hello World from subclass!"); } }; thread2.start();

P3. Jelaskan Status Berbeda dari sebuah Thread dan Kapan Transisi Status Terjadi.

Status Thread bisa diperiksa menggunakan metode Thread.getState () . Negara yang berbeda dari Thread dijelaskan dalam Thread.State enum. Mereka:

  • BARU -contoh Thread baruyang belum dimulai melalui Thread.start ()
  • RUNNABLE - thread yang berjalan. Ini disebut runnable karena pada waktu tertentu itu bisa berjalan atau menunggu kuantum waktu berikutnya dari penjadwal utas. Sebuahthread BARU memasuki status RUNNABLE saat Anda memanggil Thread.start () di atasnya
  • DIBLOKIR - utas yang sedang berjalan menjadi diblokir jika perlu masuk ke bagian yang disinkronkan tetapi tidak dapat melakukannya karena utas lain menahan monitor bagian ini
  • MENUNGGU - utas memasuki status ini jika menunggu utas lain melakukan tindakan tertentu. Misalnya, utas memasuki status ini saat memanggil metode Object.wait () pada monitor yang dimilikinya, atau metode Thread.join () di utas lain
  • TIMED_WAITING - sama seperti di atas, tetapi utas memasuki status ini setelah memanggil versi berjangka waktu Thread.sleep () , Object.wait () , Thread.join (), dan beberapa metode lain
  • TERMINATED - thread telah menyelesaikan eksekusimetode Runnable.run () dan dihentikan

P4. Apa Perbedaan Antara Antarmuka Runnable dan Callable? Bagaimana Mereka Digunakan?

The Runnable antarmuka memiliki satu run metode. Ini mewakili unit komputasi yang harus dijalankan di utas terpisah. The Runnable antarmuka tidak memungkinkan metode ini untuk mengembalikan nilai atau membuang pengecualian dicentang.

The Callable antarmuka memiliki satu panggilan metode dan merupakan tugas yang memiliki nilai. Itulah mengapa metode panggilan mengembalikan nilai. Itu juga bisa membuang pengecualian. Callable umumnya digunakan dalam instance ExecutorService untuk memulai tugas asinkron dan kemudian memanggil instance Future yang dikembalikan untuk mendapatkan nilainya.

P5. Apa Itu Daemon Thread, Apa Kasus Penggunaannya? Bagaimana Cara Membuat Untaian Daemon?

Utas daemon adalah utas yang tidak mencegah keluarnya JVM. Ketika semua utas non-daemon dihentikan, JVM akan mengabaikan semua utas daemon yang tersisa. Utas daemon biasanya digunakan untuk menjalankan beberapa tugas pendukung atau layanan untuk utas lain, tetapi Anda harus memperhitungkan bahwa utas tersebut dapat ditinggalkan kapan saja.

Untuk memulai utas sebagai daemon, Anda harus menggunakan metode setDaemon () sebelum memanggil start () :

Thread daemon = new Thread(() -> System.out.println("Hello from daemon!")); daemon.setDaemon(true); daemon.start();

Anehnya, jika Anda menjalankan ini sebagai bagian dari metode main () , pesan mungkin tidak dapat dicetak. Ini bisa terjadi jika utas main () akan berhenti sebelum daemon mencapai titik pencetakan pesan. Biasanya Anda tidak boleh melakukan I / O apa pun di utas daemon, karena mereka bahkan tidak dapat menjalankan blok akhirnya dan menutup sumber daya jika ditinggalkan.

P6. Apa Itu Bendera Interupsi Thread? Bagaimana Cara Mengatur dan Memeriksanya? Bagaimana Kaitannya dengan Pengecualian yang Diinterupsi?

Bendera interupsi, atau status interupsi, adalah bendera Thread internal yang ditetapkan saat thread terputus. Untuk menyetelnya, cukup panggil thread.interrupt () pada objek thread .

Jika utas saat ini berada di dalam salah satu metode yang menampilkan InterruptedException ( tunggu , gabung , tidur , dll.), Maka metode ini segera menampilkan InterruptedException. Utas bebas memproses pengecualian ini menurut logikanya sendiri.

Jika utas tidak berada di dalam metode seperti itu dan thread.interrupt () dipanggil, tidak ada yang istimewa yang terjadi. Merupakan tanggung jawab utas untuk memeriksa status interupsi secara berkala menggunakan metode Thread.interrupted () statis atau contoh isInterrupted () . Perbedaan antara metode ini adalah bahwa Thread.interrupted () statis menghapus bendera interupsi, sedangkan isInterrupted () tidak.

Q7. What Are Executor and Executorservice? What Are the Differences Between These Interfaces?

Executor and ExecutorService are two related interfaces of java.util.concurrent framework. Executor is a very simple interface with a single execute method accepting Runnable instances for execution. In most cases, this is the interface that your task-executing code should depend on.

ExecutorService extends the Executor interface with multiple methods for handling and checking the lifecycle of a concurrent task execution service (termination of tasks in case of shutdown) and methods for more complex asynchronous task handling including Futures.

For more info on using Executor and ExecutorService, see the article A Guide to Java ExecutorService.

Q8. What Are the Available Implementations of Executorservice in the Standard Library?

The ExecutorService interface has three standard implementations:

  • ThreadPoolExecutor — for executing tasks using a pool of threads. Once a thread is finished executing the task, it goes back into the pool. If all threads in the pool are busy, then the task has to wait for its turn.
  • ScheduledThreadPoolExecutor allows to schedule task execution instead of running it immediately when a thread is available. It can also schedule tasks with fixed rate or fixed delay.
  • ForkJoinPool is a special ExecutorService for dealing with recursive algorithms tasks. If you use a regular ThreadPoolExecutor for a recursive algorithm, you will quickly find all your threads are busy waiting for the lower levels of recursion to finish. The ForkJoinPool implements the so-called work-stealing algorithm that allows it to use available threads more efficiently.

Q9. What Is Java Memory Model (Jmm)? Describe Its Purpose and Basic Ideas.

Java Memory Model is a part of Java language specification described in Chapter 17.4. It specifies how multiple threads access common memory in a concurrent Java application, and how data changes by one thread are made visible to other threads. While being quite short and concise, JMM may be hard to grasp without strong mathematical background.

The need for memory model arises from the fact that the way your Java code is accessing data is not how it actually happens on the lower levels. Memory writes and reads may be reordered or optimized by the Java compiler, JIT compiler, and even CPU, as long as the observable result of these reads and writes is the same.

This can lead to counter-intuitive results when your application is scaled to multiple threads because most of these optimizations take into account a single thread of execution (the cross-thread optimizers are still extremely hard to implement). Another huge problem is that the memory in modern systems is multilayered: multiple cores of a processor may keep some non-flushed data in their caches or read/write buffers, which also affects the state of the memory observed from other cores.

To make things worse, the existence of different memory access architectures would break the Java's promise of “write once, run everywhere”. Happily for the programmers, the JMM specifies some guarantees that you may rely upon when designing multithreaded applications. Sticking to these guarantees helps a programmer to write multithreaded code that is stable and portable between various architectures.

The main notions of JMM are:

  • Actions, these are inter-thread actions that can be executed by one thread and detected by another thread, like reading or writing variables, locking/unlocking monitors and so on
  • Synchronization actions, a certain subset of actions, like reading/writing a volatile variable, or locking/unlocking a monitor
  • Program Order (PO), the observable total order of actions inside a single thread
  • Synchronization Order (SO), the total order between all synchronization actions — it has to be consistent with Program Order, that is, if two synchronization actions come one before another in PO, they occur in the same order in SO
  • synchronizes-with (SW) relation between certain synchronization actions, like unlocking of monitor and locking of the same monitor (in another or the same thread)
  • Happens-before Order — combines PO with SW (this is called transitive closure in set theory) to create a partial ordering of all actions between threads. If one action happens-before another, then the results of the first action are observable by the second action (for instance, write of a variable in one thread and read in another)
  • Happens-before consistency — a set of actions is HB-consistent if every read observes either the last write to that location in the happens-before order, or some other write via data race
  • Execution — a certain set of ordered actions and consistency rules between them

For a given program, we can observe multiple different executions with various outcomes. But if a program is correctly synchronized, then all of its executions appear to be sequentially consistent, meaning you can reason about the multithreaded program as a set of actions occurring in some sequential order. This saves you the trouble of thinking about under-the-hood reorderings, optimizations or data caching.

Q10. What Is a Volatile Field and What Guarantees Does the Jmm Hold for Such Field?

A volatile field has special properties according to the Java Memory Model (see Q9). The reads and writes of a volatile variable are synchronization actions, meaning that they have a total ordering (all threads will observe a consistent order of these actions). A read of a volatile variable is guaranteed to observe the last write to this variable, according to this order.

If you have a field that is accessed from multiple threads, with at least one thread writing to it, then you should consider making it volatile, or else there is a little guarantee to what a certain thread would read from this field.

Another guarantee for volatile is atomicity of writing and reading 64-bit values (long and double). Without a volatile modifier, a read of such field could observe a value partly written by another thread.

Q11. Which of the Following Operations Are Atomic?

  • writing to a non-volatileint;
  • writing to a volatile int;
  • writing to a non-volatile long;
  • writing to a volatile long;
  • incrementing a volatile long?

A write to an int (32-bit) variable is guaranteed to be atomic, whether it is volatile or not. A long (64-bit) variable could be written in two separate steps, for example, on 32-bit architectures, so by default, there is no atomicity guarantee. However, if you specify the volatile modifier, a long variable is guaranteed to be accessed atomically.

The increment operation is usually done in multiple steps (retrieving a value, changing it and writing back), so it is never guaranteed to be atomic, wether the variable is volatile or not. If you need to implement atomic increment of a value, you should use classes AtomicInteger, AtomicLong etc.

Q12. What Special Guarantees Does the Jmm Hold for Final Fields of a Class?

JVM basically guarantees that final fields of a class will be initialized before any thread gets hold of the object. Without this guarantee, a reference to an object may be published, i.e. become visible, to another thread before all the fields of this object are initialized, due to reorderings or other optimizations. This could cause racy access to these fields.

This is why, when creating an immutable object, you should always make all its fields final, even if they are not accessible via getter methods.

Q13. What Is the Meaning of a Synchronized Keyword in the Definition of a Method? of a Static Method? Before a Block?

The synchronized keyword before a block means that any thread entering this block has to acquire the monitor (the object in brackets). If the monitor is already acquired by another thread, the former thread will enter the BLOCKED state and wait until the monitor is released.

synchronized(object) { // ... }

A synchronized instance method has the same semantics, but the instance itself acts as a monitor.

synchronized void instanceMethod() { // ... }

For a static synchronized method, the monitor is the Class object representing the declaring class.

static synchronized void staticMethod() { // ... }

Q14. If Two Threads Call a Synchronized Method on Different Object Instances Simultaneously, Could One of These Threads Block? What If the Method Is Static?

If the method is an instance method, then the instance acts as a monitor for the method. Two threads calling the method on different instances acquire different monitors, so none of them gets blocked.

If the method is static, then the monitor is the Class object. For both threads, the monitor is the same, so one of them will probably block and wait for another to exit the synchronized method.

Q15. What Is the Purpose of the Wait, Notify and Notifyall Methods of the Object Class?

A thread that owns the object's monitor (for instance, a thread that has entered a synchronized section guarded by the object) may call object.wait() to temporarily release the monitor and give other threads a chance to acquire the monitor. This may be done, for instance, to wait for a certain condition.

When another thread that acquired the monitor fulfills the condition, it may call object.notify() or object.notifyAll() and release the monitor. The notify method awakes a single thread in the waiting state, and the notifyAll method awakes all threads that wait for this monitor, and they all compete for re-acquiring the lock.

The following BlockingQueue implementation shows how multiple threads work together via the wait-notify pattern. If we put an element into an empty queue, all threads that were waiting in the take method wake up and try to receive the value. If we put an element into a full queue, the put method waits for the call to the get method. The get method removes an element and notifies the threads waiting in the put method that the queue has an empty place for a new item.

public class BlockingQueue { private List queue = new LinkedList(); private int limit = 10; public synchronized void put(T item) { while (queue.size() == limit) { try { wait(); } catch (InterruptedException e) {} } if (queue.isEmpty()) { notifyAll(); } queue.add(item); } public synchronized T take() throws InterruptedException { while (queue.isEmpty()) { try { wait(); } catch (InterruptedException e) {} } if (queue.size() == limit) { notifyAll(); } return queue.remove(0); } }

Q16. Describe the Conditions of Deadlock, Livelock, and Starvation. Describe the Possible Causes of These Conditions.

Deadlock is a condition within a group of threads that cannot make progress because every thread in the group has to acquire some resource that is already acquired by another thread in the group. The most simple case is when two threads need to lock both of two resources to progress, the first resource is already locked by one thread, and the second by another. These threads will never acquire a lock to both resources and thus will never progress.

Livelock is a case of multiple threads reacting to conditions, or events, generated by themselves. An event occurs in one thread and has to be processed by another thread. During this processing, a new event occurs which has to be processed in the first thread, and so on. Such threads are alive and not blocked, but still, do not make any progress because they overwhelm each other with useless work.

Starvation is a case of a thread unable to acquire resource because other thread (or threads) occupy it for too long or have higher priority. A thread cannot make progress and thus is unable to fulfill useful work.

Q17. Describe the Purpose and Use-Cases of the Fork/Join Framework.

The fork/join framework allows parallelizing recursive algorithms. The main problem with parallelizing recursion using something like ThreadPoolExecutor is that you may quickly run out of threads because each recursive step would require its own thread, while the threads up the stack would be idle and waiting.

The fork/join framework entry point is the ForkJoinPool class which is an implementation of ExecutorService. It implements the work-stealing algorithm, where idle threads try to “steal” work from busy threads. This allows to spread the calculations between different threads and make progress while using fewer threads than it would require with a usual thread pool.

Informasi lebih lanjut dan contoh kode untuk fork / join framework dapat ditemukan di artikel "Panduan untuk Fork / Join Framework di Java".

Berikutnya » Struktur Kelas Java dan Inisialisasi Pertanyaan Wawancara « Pertanyaan Wawancara Sistem Jenis Java Sebelumnya