6. Cos'è Kankarensi?
La concorrenza è una libreria di classi in Java che contiene classi speciali ottimizzate per funzionare su più thread. Queste classi sono raccolte in un pacchettojava.util.concurrent
. Possono essere divisi schematicamente in base alla funzionalità come segue: Raccolte simultanee : un insieme di raccolte che funzionano in modo più efficiente in un ambiente multi-thread rispetto alle raccolte universali standard del java.util
pacchetto. Invece di un wrapper di base Collections.synchronizedList
con blocco dell'accesso all'intera raccolta, vengono utilizzati blocchi sui segmenti di dati oppure il lavoro viene ottimizzato per la lettura parallela dei dati utilizzando algoritmi senza attesa. Code: code non bloccanti e bloccanti con supporto multi-threading. Le code non bloccanti sono progettate per la velocità e il funzionamento senza bloccare i thread. Le code di blocco vengono utilizzate quando è necessario "rallentare" i thread "Producer" o "Consumer" se alcune condizioni non sono soddisfatte, ad esempio, la coda è vuota o traboccata o non esiste un "Consumer" libero. I sincronizzatori sono utilità ausiliarie per la sincronizzazione dei thread. Sono un’arma potente nel calcolo “parallelo”. Esecutori : contiene strutture eccellenti per la creazione di pool di thread, la pianificazione di attività asincrone e l'ottenimento di risultati. Locks - rappresenta meccanismi di sincronizzazione dei thread alternativi e più flessibili rispetto a quelli synchronized
di wait
base notify
. Atomics : classi con supporto per operazioni atomiche su primitive e riferimenti. Fonte:notifyAll
7. Quali lezioni di Kankarensi conosci?
La risposta a questa domanda è perfettamente indicata in questo articolo . Non vedo il motivo di ristamparlo tutto qui, quindi descriverò solo quelle classi con cui ho avuto l'onore di familiarizzare brevemente. ConcurrentHashMap<K, V> - A differenzaHashtable
dei blocchi synhronized
su HashMap
, i dati vengono presentati sotto forma di segmenti, divisi in hash di chiavi. Di conseguenza, l'accesso ai dati avviene per segmenti anziché per singolo oggetto. Inoltre, gli iteratori rappresentano i dati per un periodo di tempo specifico e non generano file ConcurrentModificationException
. AtomicBoolean, AtomicInteger, AtomicLong, AtomicIntegerArray, AtomicLongArray - Cosa succede se in una classe è necessario sincronizzare l'accesso a una semplice variabile di tipo int
? È possibile utilizzare i costrutti con synchronized
e, quando si utilizzano operazioni atomiche set/get
, volatile
. Ma puoi fare ancora meglio usando nuove classi Atomic*
. Grazie all'utilizzo di CAS, le operazioni con queste classi sono più veloci rispetto a quelle sincronizzate tramite synchronized/volatile
. Inoltre, esistono metodi per l'addizione atomica di una determinata quantità, nonché per l'incremento/decremento.
8. Come funziona la classe ConcurrentHashMap?
Al momento della sua introduzione,ConcurrentHashMap
gli sviluppatori Java avevano bisogno della seguente implementazione della mappa hash:
- Sicurezza del filo
- Nessun blocco sull'intero tavolo durante l'accesso
- È auspicabile che non vi siano blocchi della tabella durante l'esecuzione di un'operazione di lettura
ConcurrentHashMap
sono le seguenti:
-
Elementi della mappa
A differenza degli elementi
HashMap
,Entry
inConcurrentHashMap
sono dichiarati comevolatile
. Questa è una caratteristica importante, dovuta anche ai cambiamenti in JMM .static final class HashEntry<K, V> { final K key; final int hash; volatile V value; final HashEntry<K, V> next; HashEntry(K key, int hash, HashEntry<K, V> next, V value) { this .key = key; this .hash = hash; this .next = next; this .value = value; } @SuppressWarnings("unchecked") static final <K, V> HashEntry<K, V>[] newArray(int i) { return new HashEntry[i]; } }
-
Funzione hash
ConcurrentHashMap
viene utilizzata anche una funzione di hashing migliorata.Lascia che ti ricordi com'era in
HashMap
JDK 1.2:static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
Versione da ConcurrentHashMap JDK 1.5:
private static int hash(int h) { h += (h << 15) ^ 0xffffcd7d; h ^= (h >>> 10); h += (h << 3); h ^= (h >>> 6); h += (h << 2) + (h << 14); return h ^ (h >>> 16); }
Perché è necessario rendere la funzione hash più complessa? Le tabelle in una mappa hash hanno una lunghezza determinata da una potenza di due. Per i codici hash le cui rappresentazioni binarie non differiscono nella posizione bassa e alta, avremo collisioni. Aumentare la complessità della funzione hash risolve semplicemente questo problema, riducendo la probabilità di collisioni nella mappa.
-
Segmenti
La mappa è divisa in N segmenti diversi (16 per impostazione predefinita, il valore massimo può essere 16 bit ed è una potenza di due). Ogni segmento è una tabella thread-safe di elementi della mappa. L'aumento del numero di segmenti incoraggerà le operazioni di modifica a estendersi su più segmenti, riducendo la probabilità di blocco in fase di esecuzione.
-
ConcurrencyLevel
Questo parametro influisce sull'utilizzo della scheda di memoria e sul numero di segmenti nella scheda.
Il numero di segmenti verrà scelto come potenza di due più vicina maggiore di concurrencyLevel. Abbassando il concurrencyLevel aumenta la probabilità che i thread blocchino i segmenti della mappa durante la scrittura. La sovrastima dell'indicatore porta a un uso inefficiente della memoria. Se solo un thread modificherà la mappa e il resto la leggerà, si consiglia di utilizzare il valore 1.
-
Totale
Quindi, i principali vantaggi e caratteristiche di implementazione
ConcurrentHashMap
:hashmap
La mappa ha un'interfaccia di interazione simile a- Le operazioni di lettura non richiedono blocchi e vengono eseguite in parallelo
- Spesso le operazioni di scrittura possono essere eseguite anche in parallelo senza blocchi
- Durante la creazione viene indicato quello richiesto
concurrencyLevel
, determinato leggendo e scrivendo statistiche - Gli elementi della mappa hanno un valore
value
dichiarato comevolatile
9. Cos'è la classe Lock?
Per controllare l'accesso ad una risorsa condivisa possiamo utilizzare i lock come alternativa all'operatore sincronizzato. La funzionalità di blocco è inclusa nel pacchettojava.util.concurrent.locks
. Innanzitutto, il thread tenta di accedere alla risorsa condivisa. Se è gratuito, viene posizionato un blocco sul thread. Una volta completato il lavoro, il blocco sulla risorsa condivisa viene rilasciato. Se la risorsa non è libera e su di essa è già inserito un blocco, il thread attende finché questo blocco non viene rilasciato. Le classi Lock implementano un'interfaccia Lock
che definisce i seguenti metodi:
void lock():
attende finché non viene acquisito il bloccoboolean tryLock():
tenta di acquisire un lock; se il lock viene ottenuto, restituisce true . Se il lock non viene acquisito, restituisce false . A differenza del metodo,lock()
non attende di acquisire un blocco se non ne è disponibile unovoid unlock():
rimuove la serraturaCondition newCondition():
restituisce l'oggettoCondition
associato al blocco corrente
lock()
e, dopo aver finito di lavorare con le risorse condivise, viene chiamato il metodo unlock()
, che rilascia il blocco. L'oggetto Condition
consente di gestire il blocco. Di norma, per lavorare con i blocchi, viene utilizzata una classe ReentrantLock
del pacchetto , java.util.concurrent.locks.
che implementa l'interfaccia Lock
. Diamo un'occhiata all'utilizzo dell'API Java Lock utilizzando un piccolo programma come esempio: quindi, supponiamo di avere una classe Resource
con un paio di metodi thread-safe e metodi in cui la sicurezza thread non è richiesta.
public class Resource {
public void doSomething(){
// пусть здесь происходит работа с базой данных
}
public void doLogging(){
// потокобезопасность для логгирования нам не требуется
}
}
Ora prendiamo una classe che implementa l'interfaccia Runnable
e utilizza i metodi della classe Resource
.
public class SynchronizedLockExample implements Runnable{
// экземпляр класса Resource для работы с методами
private Resource resource;
public SynchronizedLockExample(Resource r){
this.resource = r;
}
@Override
public void run() {
synchronized (resource) {
resource.doSomething();
}
resource.doLogging();
}
}
Ora riscriviamo il programma precedente utilizzando l'API Lock anziché l'API synchronized
.
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// класс для работы с Lock API. Переписан с приведенной выше программы,
// но уже без использования ключевого слова synchronized
public class ConcurrencyLockExample implements Runnable{
private Resource resource;
private Lock lock;
public ConcurrencyLockExample(Resource r){
this.resource = r;
this.lock = new ReentrantLock();
}
@Override
public void run() {
try {
// лочим на 10 секунд
if(lock.tryLock(10, TimeUnit.SECONDS)){
resource.doSomething();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
//убираем лок
lock.unlock();
}
// Для логгирования не требуется потокобезопасность
resource.doLogging();
}
}
Come puoi vedere dal programma, utilizziamo il metodo tryLock()
per assicurarci che il thread attenda solo per un certo periodo di tempo. Se non ottiene un blocco sull'oggetto, registra semplicemente ed esce. Un altro punto importante. È necessario utilizzare un blocco try-finally
per garantire che il blocco venga rilasciato anche se il metodo doSomething()
genera un'eccezione. Fonti:
11. Cos'è un mutex?
Un mutex è un oggetto speciale per la sincronizzazione di thread/processi. Possono essere necessari due stati: occupato e libero. Per semplificare, un mutex è una variabile booleana che accetta due valori: busy (true) e free (false). Quando un thread desidera la proprietà esclusiva di un oggetto, contrassegna il suo mutex come occupato e, quando ha finito di lavorare con esso, contrassegna il suo mutex come libero. Un mutex è allegato a ogni oggetto in Java. Solo la macchina Java ha accesso diretto al mutex. È nascosto al programmatore.12. Cos'è un monitor?
Un monitor è un meccanismo speciale (un pezzo di codice) - un componente aggiuntivo sul mutex, che ne garantisce il corretto funzionamento. Dopotutto, non è sufficiente indicare che l'oggetto è occupato; dobbiamo anche assicurarci che altri thread non tentino di utilizzare l'oggetto occupato. In Java, il monitor viene implementato utilizzando la parola chiavesynchronized
. Quando scriviamo un blocco sincronizzato, il compilatore Java lo sostituisce con tre pezzi di codice:
- All'inizio del blocco
synchronized
viene aggiunto il codice che contrassegna il mutex come occupato. - Alla fine del blocco
synchronized
viene aggiunto un codice che contrassegna il mutex come libero. - Prima del blocco
synchronized
viene aggiunto del codice che controlla se il mutex è occupato, poi il thread deve attendere che venga rilasciato.
GO TO FULL VERSION