A cosa servono i recinti di memoria in Java?


18

Mentre cercavo di capire come SubmissionPublisher( codice sorgente in Java SE 10, OpenJDK | docs ), una nuova classe aggiunta a Java SE nella versione 9, è stata implementata, mi sono imbattuto in alcune chiamate API di VarHandlecui non ero a conoscenza in precedenza:

fullFence, acquireFence, releaseFence, loadLoadFenceE storeStoreFence.

Dopo aver fatto qualche ricerca, in particolare per quanto riguarda il concetto di barriere / recinzioni di memoria (ne ho sentito parlare in precedenza, sì; ma non le ho mai usate, quindi non avevo abbastanza familiarità con la loro semantica), penso di avere una comprensione di base di ciò per cui sono . Tuttavia, poiché le mie domande potrebbero derivare da un malinteso, voglio assicurarmi di averlo capito bene in primo luogo:

  1. Le barriere di memoria stanno riordinando i vincoli relativi alle operazioni di lettura e scrittura.

  2. Le barriere di memoria possono essere classificate in due categorie principali: barriere di memoria unidirezionali e bidirezionali, a seconda che stabiliscano vincoli su letture o scritture o su entrambi.

  3. C ++ supporta una serie di barriere di memoria , tuttavia, queste non corrispondono a quelle fornite da VarHandle. Tuttavia, alcune delle barriere di memoria disponibili nel VarHandlefornire effetti di ordinamento che sono compatibili con i corrispondenti barriere memoria C ++.

    • #fullFence è compatibile con atomic_thread_fence(memory_order_seq_cst)
    • #acquireFence è compatibile con atomic_thread_fence(memory_order_acquire)
    • #releaseFence è compatibile con atomic_thread_fence(memory_order_release)
    • #loadLoadFencee #storeStoreFencenon hanno una controparte C ++ compatibile

La parola compatibile sembra davvero importante qui poiché la semantica differisce chiaramente quando si tratta di dettagli. Ad esempio, tutte le barriere C ++ sono bidirezionali, mentre le barriere Java non lo sono (necessariamente).

  1. La maggior parte delle barriere di memoria hanno anche effetti di sincronizzazione. Questi dipendono soprattutto dal tipo di barriera utilizzato e dalle istruzioni di barriera precedentemente eseguite in altri thread. Dato che le implicazioni complete di un'istruzione barriera sono specifiche dell'hardware, rimarrò con le barriere di livello superiore (C ++). In C ++, ad esempio, le modifiche apportate prima di un'istruzione di barriera di rilascio sono visibili a un thread che esegue un'istruzione di acquisizione di barriera.

I miei presupposti sono corretti? In tal caso, le mie domande risultanti sono:

  1. Le barriere di memoria disponibili VarHandlecausano qualche tipo di sincronizzazione della memoria?

  2. Indipendentemente dal fatto che causino o meno la sincronizzazione della memoria, a cosa possono essere utili i vincoli di riordino in Java? Il modello di memoria Java offre già alcune garanzie molto forti in merito all'ordinamento quando sono coinvolti campi volatili, blocchi o VarHandleoperazioni simili #compareAndSet.

Nel caso in cui tu stia cercando un esempio: la suddetta BufferedSubscription, una classe interna di SubmissionPublisher(fonte collegata sopra), ha stabilito un recinto completo nella riga 1079 (funzione growAndAdd; poiché il sito Web collegato non supporta identificatori di frammenti, solo CTRL + F per esso ). Tuttavia, non è chiaro per me a cosa serva.


1
Ho provato a rispondere, ma per dirla in parole povere, esistono perché le persone vogliono una modalità più debole di quella di Java. In ordine crescente, questi sarebbero: plain -> opaque -> release/acquire -> volatile (sequential consistency).
Eugene,

Risposte:


11

Questa è principalmente una non-risposta, davvero (inizialmente volevo farne un commento, ma come puoi vedere, è troppo lungo). È solo che l'ho messo in discussione molto da solo, ho fatto molte letture e ricerche e in questo momento posso tranquillamente dire: è complicato. Ho anche scritto più test con jcstress per capire come funzionano davvero (osservando il codice assembly generato) e mentre alcuni di loro in qualche modo avevano senso, l'argomento in generale non è affatto facile.

La prima cosa che devi capire:

Java Language Specification (JLS) non menziona barriere , ovunque. Questo, per Java, sarebbe un dettaglio di implementazione: agisce veramente in termini di accade prima semantica. Per essere in grado di specificarli correttamente in base al JMM (Java Memory Model), il JMM dovrebbe cambiare molto .

Questo è un work in progress.

In secondo luogo, se vuoi davvero graffiare la superficie qui, questa è la prima cosa da guardare . Il discorso è incredibile. La mia parte preferita è quando Herb Sutter alza le sue 5 dita e dice: "Ecco quante persone possono davvero e correttamente lavorare con queste". Ciò dovrebbe darti un indizio della complessità coinvolta. Tuttavia, ci sono alcuni esempi banali che sono facili da comprendere (come un contatore aggiornato da più thread che non si preoccupa di altre garanzie di memoria, ma si preoccupa solo che sia esso stesso incrementato correttamente).

Un altro esempio è quando (in Java) si desidera che un volatileflag controlli i thread per arrestare / avviare. Sai, il classico:

volatile boolean stop = false; // on thread writes, one thread reads this    

Se lavori con java, sapresti che senza volatile questo codice è rotto (puoi leggere perché il blocco del doppio controllo è rotto senza di esso, ad esempio). Ma sai anche che per alcune persone che scrivono codice ad alte prestazioni questo è troppo? volatileread / write garantisce anche una coerenza sequenziale - che ha alcune forti garanzie e alcune persone vogliono una versione più debole di questo.

Un flag thread-safe, ma non volatile? Sì, esattamente: VarHandle::set/getOpaque.

E ti chiederesti perché qualcuno potrebbe averne bisogno per esempio? Non tutti sono interessati a tutti i cambiamenti che sono supportati da a volatile.

Vediamo come raggiungeremo questo obiettivo in Java. Prima di tutto, tali esotiche cose già esistevano nelle API: AtomicInteger::lazySet. Questo non è specificato nel modello di memoria Java e non ha una definizione chiara ; ancora la gente lo usava (LMAX, afaik o questo per ulteriori letture ). IMHO, AtomicInteger::lazySetè VarHandle::releaseFence(o VarHandle::storeStoreFence).


Proviamo a rispondere perché qualcuno ha bisogno di questi ?

JMM ha sostanzialmente due modi per accedere a un campo: semplice e volatile (che garantisce coerenza sequenziale ). Tutti questi metodi che menzioni sono lì per portare qualcosa tra questi due - rilasciare / acquisire la semantica ; ci sono casi, immagino, in cui le persone ne hanno davvero bisogno.

Un pari rilassamento maggiore da rilascio / acquisizione sarebbe opaco , che sto ancora cercando di comprendere appieno .


Quindi linea di fondo (la tua comprensione è abbastanza corretta, tra l'altro): se hai intenzione di usarlo in Java - al momento non hanno specifiche, fallo a tuo rischio. Se vuoi capirli, le loro modalità equivalenti in C ++ sono il punto di partenza.


1
Non cercare di capire il significato del lazySetcollegamento a risposte antiche, la documentazione attuale dice esattamente cosa significa, al giorno d'oggi. Inoltre, è fuorviante affermare che il JMM ha solo due modalità di accesso. Abbiamo una lettura volatile e una scrittura volatile , che insieme possono stabilire una relazione prima .
Holger,

1
Stavo scrivendo qualcosa di più al riguardo. Considera che cas è sia una lettura che una scrittura, che agisce come una barriera completa, e potresti capire, perché si desidera rilassarlo. Ad esempio, quando si implementa un blocco, la prima azione è cas (0, 1) sul conteggio dei blocchi, ma è necessario acquisire solo la semantica (come la lettura volatile), mentre la scrittura finale di 0 da sbloccare dovrebbe avere una versione semantica (come la scrittura volatile ), quindi si verifica un evento precedente tra lo sblocco e il successivo blocco. L'acquisizione / rilascio è persino più debole della lettura / scrittura volatile per quanto riguarda i thread che utilizzano blocchi diversi.
Holger,

1
@Peter Cordes: la prima versione C con una volatileparola chiave era C99, cinque anni dopo Java, ma mancava ancora di semantica utile, anche C ++ 03 non ha un modello di memoria. Le cose che il C ++ chiama "atomico" sono anche molto più giovani di Java. E la volatileparola chiave non implica nemmeno aggiornamenti atomici. Allora perché dovrebbe essere chiamato così.
Holger,

1
@PeterCordes forse, lo sto confondendo con restrict, tuttavia, ricordo i momenti in cui dovevo scrivere __volatileper usare un'estensione del compilatore senza parole chiave. Quindi forse non ha implementato completamente C89? Non dirmi che sono così vecchio. Prima di Java 5, volatileera molto più vicino a C. Ma Java non aveva MMIO, quindi il suo scopo era sempre il multi-threading, ma la semantica pre-Java 5 non era molto utile per questo. Quindi sono stati aggiunti rilasci / acquisizioni come la semantica, ma non è atomico (gli aggiornamenti atomici sono una funzionalità aggiuntiva costruita sopra di essa).
Holger,

2
@Eugene riguardo a questo , il mio esempio era specifico per l'utilizzo di cas per il bloccaggio che sarebbe stato acquisito. Un latch di conto alla rovescia porterebbe decrementi atomici con rilascio semantico, seguito dal thread che raggiunge lo zero inserendo un recinto di acquisizione ed eseguendo l'azione finale. Naturalmente, ci sono altri casi di aggiornamenti atomici in cui è richiesta la recinzione completa.
Holger,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.