Irbisa · Шпаргалка

Многопоточность и коллекции JVM

Потоки и Java Memory Model, volatile и synchronized, блокировки, executors, потокобезопасные коллекции, устройство HashMap

Middle-разработчик · 18 вопросов

  1. 01

    Чем процесс отличается от потока, и в каких состояниях может находиться поток JVM?

    Процесс представляет собой запущенную программу со своей памятью; потоки работают внутри процесса, делят его кучу, и у каждого есть свой стек.

  2. 02

    Где в JVM живут локальные переменные, примитивы и объекты, в стеке или в куче, и что из этого могут делить два потока?

    У каждого потока свой стек кадров вызовов с локальными переменными, параметрами и ссылками; все объекты лежат в одной куче, общей для всех потоков.

    fun countInParallel(): Int {    var count = 0                        // captured var: becomes a Ref.IntRef on the heap    val workers = List(4) {        Thread { repeat(100_000) { count++ } }    }    workers.forEach { it.start() }…
  3. 03

    Как до корутин Android-приложения уводили работу с главного потока (Thread, HandlerThread, Executor, AsyncTask), и почему AsyncTask объявили устаревшим?

    Через голый Thread, HandlerThread со своим Looper, ExecutorService плюс Handler главного потока или AsyncTask, который упаковывал эту пару.

    // Java-era pattern: an executor for the work, a main-thread Handler for the resultclass UserLoader(private val api: UserApi) {    private val executor = Executors.newSingleThreadExecutor()    private val main = Handler(Looper.getMainLooper())     fun load(id: Long, onResult: (User) -> Unit) {…
  4. 04

    ArrayList или LinkedList: сколько стоят get, add и remove в каждом, и почему ArrayList почти всегда быстрее?

    ArrayList устроен как растущий массив: get(i) за O(1), добавление в конец за амортизированное O(1), вставка или удаление в середине за O(n).

    fun demo() {    val linked = LinkedList((1..20_000).toList())    var sum = 0L    for (i in linked.indices) sum += linked[i]   // wrong: each get(i) walks the chain, O(n²)    for (x in linked) sum += x                   // right: one walk, O(n)…
  5. 05

    HashMap, LinkedHashMap или TreeMap (и их множества): какой порядок и какую стоимость поиска даёт каждый, и что возвращает mutableMapOf()?

    HashMap даёт поиск в среднем за O(1) без определённого порядка, LinkedHashMap вдобавок сохраняет порядок вставки, а TreeMap держит ключи отсортированными за O(log n) на операцию.

    fun demo() {    val scores = mapOf("mia" to 1, "zoe" to 3, "adam" to 5)        // LinkedHashMap    println(scores.keys)                                          // [mia, zoe, adam]: insertion order    println(scores.toSortedMap().keys)                            // [adam, mia, zoe]: TreeMap    println(hashMapOf("mia" to 1, "zoe" to 3, "adam" to 5).keys)  // [zoe, adam, mia]: bucket order…
  6. 06

    Как HashMap устроен внутри: бакеты, hashCode и equals, коллизии, load factor, ресайз и превращение бакета в дерево?

    HashMap хранит массив бакетов: hashCode ключа выбирает бакет, а equals находит запись внутри него.

  7. 07

    Что такое happens-before в модели памяти Java, и что гарантируют synchronized, @Volatile и AtomicInteger?

    Happens-before определяет в Java Memory Model, когда один поток обязан увидеть записи другого; без такого ребра JIT и процессор могут кешировать или переставлять их.

    class Stats {    private val lock = Any()    private var total = 0L                       // guarded by lock     @Volatile var lastUser: String? = null       // one writer: visibility is enough    private val hits = AtomicInteger()           // atomic read-modify-write…
  8. 08

    Найдите баг: stop() выставляет running = false, но иногда рабочий поток так и не выходит из цикла.

    class Worker {    private var running = true    private var processed = 0L     fun start() {        Thread {            while (running) {                processed++        // stands in for work with no calls or locks            }            println("stopped after $processed steps")        }.start()    }     fun stop() {        running = false            // called from the main thread    }}

    running объявлено обычным полем, поэтому ничто не гарантирует, что рабочий поток вообще увидит запись главного: компилятор может прочитать поле один раз до цикла и вечно крутиться на этой копии.

  9. 09

    Как правильно остановить поток, что на самом деле делает interrupt(), и почему нельзя глотать InterruptedException?

    Поток просят остановиться, и он останавливается сам: interrupt() лишь выставляет флаг целевому потоку.

    class Uploader(private val queue: BlockingQueue<File>) {    private val worker = Thread {        try {            while (!Thread.currentThread().isInterrupted) {                upload(queue.take())             // take() throws InterruptedException            }…
  10. 10

    synchronized, ReentrantLock или ReentrantReadWriteLock: что добавляет явная блокировка, и что значит «реентерабельная»?

    Все три реентерабельны: поток, который держит блокировку, может взять её повторно и не заблокировать сам себя.

    class TokenStore {    private val lock = ReentrantLock()    private var token: String? = null     fun tryRead(): String? =        if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {…
  11. 11

    Два потока одновременно вызывают transfer(a, b) и transfer(b, a), и оба зависают навсегда. Почему, и как от этого защищают порядок блокировок и tryLock?

    class Account(val id: Long, var balance: Long) fun transfer(from: Account, to: Account, amount: Long) {    synchronized(from) {        synchronized(to) {            from.balance -= amount            to.balance += amount        }    }} // thread 1: transfer(a, b, 10)// thread 2: transfer(b, a, 5)

    Поток 1 держит a и ждёт b, поток 2 держит b и ждёт a: из этого цикла не выйдет ни один, и так возникает deadlock (взаимная блокировка).

  12. 12

    Как передавать работу от потоков-производителей потоку-потребителю: wait/notify или BlockingQueue, и почему wait() должен стоять в цикле?

    Берите BlockingQueue: put блокируется, пока очередь полна, take блокируется, пока она пуста, а всю синхронизацию делает библиотека.

    val jobs = ArrayBlockingQueue<Job>(100) fun produce(job: Job) = jobs.put(job)       // blocks while 100 jobs are waitingfun consumeLoop() {    while (true) process(jobs.take())       // blocks while the queue is empty}…
  13. 13

    Как работает пул потоков, чем отличаются newFixedThreadPool, newCachedThreadPool и newSingleThreadExecutor, и что происходит, когда задачи копятся?

    Пул держит потоки живыми и кормит их задачами из очереди, так что за создание потока вы платите один раз.

    val pool = ThreadPoolExecutor(    2, 4,                                   // core and maximum threads    30, TimeUnit.SECONDS,                   // idle extra threads stop after 30 s    ArrayBlockingQueue(100),                // bounded: at most 100 waiting tasks    ThreadPoolExecutor.CallerRunsPolicy(),  // full: the caller runs the task itself)…
  14. 14

    ConcurrentHashMap, Collections.synchronizedMap и CopyOnWriteArrayList: как каждый обеспечивает потокобезопасность, и чего это стоит?

    synchronizedMap оборачивает каждый вызов в одну блокировку, и все потоки стоят к ней в очереди.

    val counts = ConcurrentHashMap<String, Int>()fun record(event: String) {    counts.merge(event, 1, Int::plus)           // atomic: no lost increments} val sessions = ConcurrentHashMap<String, Session>()…
  15. 15

    Найдите баг: два экрана одновременно запрашивают одного пользователя, и кеш дважды грузит его из сети, хотя мапа у него ConcurrentHashMap.

    class UserCache(private val api: UserApi) {    private val users = ConcurrentHashMap<Long, User>()     // Called from several threads at once.    fun user(id: Long): User =        users.getOrPut(id) { api.loadUser(id) }   // blocking network call}

    getOrPut работает по схеме «проверить, потом действовать»: читает мапу и при промахе запускает загрузку и сохраняет результат через putIfAbsent.

  16. 16

    Как атомики на CAS вроде AtomicReference работают без блокировки, и где они перестают помогать: конкуренция, ABA, обновление нескольких полей?

    Compare-and-set выполняется одной атомарной операцией процессора: записать новое значение, только если в поле всё ещё лежит ожидаемое, и сообщить, получилось ли.

    data class Stats(val count: Int, val total: Long) class Metrics {    private val stats = AtomicReference(Stats(0, 0))     fun record(ms: Long) {…
  17. 17

    Что может пойти не так, когда два потока одновременно пишут в обычный HashMap, и почему недостаточно обернуть в synchronized только put?

    Записи молча пропадают: два put в один бакет перезаписывают ссылки друг друга, запись во время ресайза попадает в старый массив, а size расходится с реальным числом записей.

    class SessionRegistry {    private val lock = Any()    private val sessions = HashMap<String, Session>()     // wrong: writers take the lock, the reader does not    fun addRacy(id: String, s: Session) = synchronized(lock) { sessions[id] = s }…
  18. 18

    Что такое ThreadLocal, как он приводит к утечкам или подсовывает устаревшие значения следующей задаче в пуле потоков, и что меняется внутри корутин?

    ThreadLocal даёт каждому потоку свою копию значения: она хранится в мапе внутри объекта Thread, а ключом служит сам ThreadLocal.

    private val requestId = ThreadLocal<String?>() // On a pool: always clean up, or the next task on this thread inherits the valuefun handle(pool: ExecutorService, id: String, work: () -> Unit) = pool.execute {    requestId.set(id)    try {…