Try using it in your preferred language.

English

  • English
  • 汉语
  • Español
  • Bahasa Indonesia
  • Português
  • Русский
  • 日本語
  • 한국어
  • Deutsch
  • Français
  • Italiano
  • Türkçe
  • Tiếng Việt
  • ไทย
  • Polski
  • Nederlands
  • हिन्दी
  • Magyar
translation

Dit is een door AI vertaalde post.

제이온

[Java] Gesynchroniseerde verzameling versus gelijktijdige verzameling

Selecteer taal

  • Nederlands
  • English
  • 汉语
  • Español
  • Bahasa Indonesia
  • Português
  • Русский
  • 日本語
  • 한국어
  • Deutsch
  • Français
  • Italiano
  • Türkçe
  • Tiếng Việt
  • ไทย
  • Polski
  • हिन्दी
  • Magyar

Samengevat door durumis AI

  • Gesynchroniseerde verzamelingen zijn veilig in een multi-threaded omgeving, maar het is beter om parallelle verzamelingen te gebruiken omdat ze meerdere bewerkingen kunnen combineren als één bewerking of prestatieverlies kunnen veroorzaken.
  • Parallelle verzamelingen garanderen gelijktijdigheid efficiënter dan gesynchroniseerde verzamelingen en zijn er in verschillende soorten, zoals CopyOnWriteArrayList en ConcurrentHashMap.
  • In het bijzonder wordt CopyOnWriteArrayList niet vergrendeld voor leesoperaties, waardoor het sneller is dan SynchronizedList, maar het kan prestatieverlies veroorzaken tijdens schrijfbewerkingen omdat het een arraykopie uitvoert.

Gesynchroniseerde verzameling

Gesynchroniseerde verzamelingen zijn er in verschillende klassen, met name:


  • Vector
  • Hashtable
  • Collections.synchronizedXXX


Al deze klassen gebruiken het `synchronized` trefwoord in hun publiekelijk gedeclareerde methoden om ervoor te zorgen dat er maar één thread tegelijk toegang heeft tot de interne waarden en zo gelijktijdigheid garanderen.


Vector

public class Vector extends AbstractList
    implements List, RandomAccess, Cloneable, java.io.Serializable {
    ...
    public synchronized boolean add(E e) {
                modCount++;
                add(e, elementData, elementCount);
                return true;
    }
    ...

Als we kijken naar de `add()` methode van de Vector-klasse, die elementen toevoegt, zien we het `synchronized` trefwoord. Dit betekent dat de invoeging van elementen in Vector intern gesynchroniseerd wordt.

Hashtable

public class Hashtable extends Dictionary
    implements Map, Cloneable, java.io.Serializable {
        ...
        public synchronized boolean contains(Object value) {
        if (value == null) {
            throw new NullPointerException();
        }

        Entry tab[] = table;
        for (int i = tab.length ; i-- > 0 ;) {
            for (Entry e = tab[i] ; e != null ; e = e.next) {
                if (e.value.equals(value)) {
                    return true;
                }
            }
        }
        return false;
    }
        ...


De `contains()` methode van de Hashtable-klasse, die controleert of een waarde bestaat, gebruikt, net zoals de Vector-klasse, het `synchronized` trefwoord.


Collections.synchronizedXXX

Laten we eens kijken naar de SynchronizedList-klasse die gemaakt wordt met de `Collections.synchronizedList()` methode.


static class SynchronizedList extends SynchronizedCollection
        implements List {

        final Object mutex;

        ...
        public E get(int index) {
                synchronized (mutex) {return list.get(index);}
        }

        public E set(int index, E element) {
                synchronized (mutex) {return list.set(index, element);}
        }

        public void add(int index, E element) {
                synchronized (mutex) {list.add(index, element);}
        }

        public E remove(int index) {
                synchronized (mutex) {return list.remove(index);}
        }
        ...


Als we kijken naar de methoden van de SynchronizedList-klasse zien we dat ze allemaal het `synchronized` trefwoord gebruiken. De synchronisatie wordt echter bereikt met een mutex via een synchronized blok. Alle methoden delen het mutex-object, dus als één thread in een synchronized blok zit, zijn de synchronized blokken van alle andere methoden vergrendeld.


Problemen met gesynchroniseerde collecties

In een multi-thread omgeving moet je soms gesynchroniseerde collecties gebruiken, maar in de meeste gevallen is het beter om andere synchronisatiemethoden te gebruiken. Er zijn twee belangrijke redenen hiervoor.


Het combineren van meerdere bewerkingen tot één bewerking

Gesynchroniseerde collectie klassen garanderen gelijktijdigheid in een multi-thread omgeving. Als je echter meerdere bewerkingen wilt combineren tot één bewerking kan er een probleem ontstaan. Met deze gesynchroniseerde collecties kan het zijn dat de bewerking niet correct uitgevoerd wordt.


final List list = Collections.synchronizedList(new ArrayList());
final int nThreads = 2;
ExecutorService es = Executors.newFixedThreadPool(nThreads);

for (int i = 0; i < nThreads; i++) {
    es.execute(new Runnable() {

        public void run() {
            while(true) {
                try {
                    list.clear();
                    list.add("888");
                    list.remove(0);
                } catch(IndexOutOfBoundsException e) {
                    e.printStackTrace();
                }
            }
        }
    });


Als je deze code uitvoert en Thread A `remove(0)` uitvoert terwijl Thread B `clear()` uitvoert, zal er een fout optreden. Je moet dus de code als volgt in een synchronized blok plaatsen.


synchronized (list) {
    list.clear();
    list.add("888");
    list.remove(0);


Prestatieverlies

Als je alle methoden die toegang hebben tot het gedeelde object als `synchronized` methoden definieert, of als je binnen de methoden hetzelfde `synchronized` blok definieert, zal een thread, zodra deze de lock heeft verkregen, alle andere threads in een blocking-status brengen en ze geen enkele synchronized methode meer kunnen gebruiken. Dit herhaalde patroon kan leiden tot prestatieverlies.


Concurrent collectie

Het `java.util.concurrent` pakket bevat verschillende parallele collecties. In deze blogpost bespreken we slechts een aantal van deze collecties.


  • CopyOnWriteArrayList
    • Een subklasse van de List-klasse, die de prestaties van het itereren door en opvragen van objecten in een lijst prioriteert.
  • ConcurrentMap
    • Een parallelle collectie. De interface definieert bewerkingen zoals `put-if-absent`, `replace` en `conditional remove`, die een element alleen toevoegen als het nog niet bestaat.
  • ConcurrentHashMap
    • Een subklasse van ConcurrentMap die parallellisme toevoegt aan HashMap.
  • ConcurrentLinkedQueue
    • Een FIFO-queue (First In, First Out) met parallellisme. Als de queue leeg is, wordt direct een waarde teruggegeven en kan de thread verder met andere taken.
  • LinkedBlockingQueue
    • Vergelijkbaar met ConcurrentLinkedQueue. Het verschil is dat, als de queue leeg is, de bewerking die een item uit de queue probeert te halen wacht tot er een item wordt toegevoegd. Omgekeerd, als de queue een bepaalde maximale grootte heeft, wacht de bewerking die een item aan de queue wil toevoegen tot er ruimte vrijkomt.
  • ConcurrentSkipListMap, ConcurrentSkipListSet
    • Verbeterde versies van SortedMap en SortedSet die parallellisme toevoegen.


Het vervangen van traditionele gesynchroniseerde collectieklassen door parallele collecties kan zonder extra risico's leiden tot een aanzienlijke prestatieverbetering.

Laten we eens kijken naar de parallele collecties in vergelijking met de gesynchroniseerde collecties.


CopyOnWriteArrayList

Er zijn twee manieren om een gesynchroniseerde ArrayList te creëren:


  • Collections.synchronizedList()
  • CopyOnWriteArrayList


`Collections.synchronizedList()` werd toegevoegd in JDK 1.2. Deze collectie synchroniseert alle lees- en schrijfbewerkingen, wat een inflexibele aanpak is. Om dit te corrigeren werd de CopyOnWriteArrayList geïntroduceerd.


Leesbewerkingen

Bij SynchronizedList worden zowel lees- als schrijfbewerkingen vergrendeld op de instantie zelf. CopyOnWriteArrayList daarentegen maakt bij schrijfbewerkingen een kopie van de elementen in de originele array en maakt een nieuwe tijdelijke array. De schrijfbewerking wordt uitgevoerd op deze tijdelijke array en vervolgens wordt de originele array bijgewerkt. Dit maakt het mogelijk dat leesbewerkingen niet worden vergrendeld, waardoor CopyOnWriteArrayList beter presteert dan SynchronizedList.


public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable {

    final transient Object lock = new Object();
    private transient volatile Object[] array;

        ...
        public E get(int index) {
        return elementAt(getArray(), index);
    }
        ...


De `get()` methode is hierboven weergegeven. Omdat er geen `synchronized` trefwoord is, is deze niet vergrendeld.


Schrijfbewerkingen

CopyOnWriteArrayList gebruikt bij schrijfbewerkingen een expliciete lock. Uiteindelijk worden beide soorten collecties vergrendeld tijdens deze bewerkingen. CopyOnWriteArrayList voert echter een array-kopieerbewerking uit, wat relatief duur is, waardoor er prestatieproblemen kunnen optreden als er veel schrijfbewerkingen plaatsvinden.


public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable {

    final transient Object lock = new Object();
    private transient volatile Object[] array;

        public void add(int index, E element) {
        synchronized (lock) {
            ...
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(es, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(es, 0, newElements, 0, index);
                System.arraycopy(es, index, newElements, index + 1,
                                 numMoved);
            }
                        ...
        }
    }


Hierboven is de `add()` methode getoond. Deze wordt vergrendeld via een synchronized blok en er wordt een array-kopieerbewerking uitgevoerd.


Iterator

De `iterator` die wordt geëxtraheerd uit CopyOnWriteArrayList, itereert over de collectiegegevens op het moment dat de `iterator` wordt opgehaald. Tijdens het itereren worden toevoegingen of verwijderingen van gegevens in de collectie toegepast op een kopie die onafhankelijk is van de iteratie. Dit betekent dat er geen problemen zijn met gelijktijdig gebruik.


CopyOnWriteArraySet

Er zijn twee manieren om een gesynchroniseerde Set te creëren:


  • Collections.synchronizedSet()
  • CopyOnWriteArraySet


Zoals de naam al doet vermoeden, is de werking van CopyOnWriteArraySet, behalve de gegevensstructuur, bijna identiek aan die van CopyOnWriteArrayList.


Leesbewerkingen

public class CopyOnWriteArraySet extends AbstractSet implements java.io.Serializable {

    private final CopyOnWriteArrayList al;

        public boolean contains(Object o) {
        return al.contains(o);
    }


De `contains()` methode is hierboven getoond. CopyOnWriteArraySet definieert intern een CopyOnWriteArrayList en gebruikt de methoden van CopyOnWriteArrayList.


public boolean contains(Object o) {
        return indexOf(o) >= 0;
}

public int indexOf(Object o) {
        Object[] es = getArray();
        return indexOfRange(o, es, 0, es.length);
}

    private static int indexOfRange(Object o, Object[] es, int from, int to) {
        if (o == null) {
            for (int i = from; i < to; i++)
                if (es[i] == null)
                    return i;
        } else {
            for (int i = from; i < to; i++)
                if (o.equals(es[i]))
                    return i;
        }
        return -1;


De `contains()` methode van CopyOnWriteArrayList is hierboven getoond. We zien dat deze niet is vergrendeld.


Schrijfbewerkingen

public class CopyOnWriteArraySet extends AbstractSet implements java.io.Serializable {

        private final CopyOnWriteArrayList al;

        public boolean add(E e) {
        return al.addIfAbsent(e);
    }


De `add()` methode maakt ook gebruik van de methoden van CopyOnWriteArrayList.


public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOfRange(e, snapshot, 0, snapshot.length) < 0
        && addIfAbsent(e, snapshot);
}

private boolean addIfAbsent(E e, Object[] snapshot) {
        synchronized (lock) {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // Optimize for lost race to another addXXX operation
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i]
                        && Objects.equals(e, current[i]))
                        return false;
                if (indexOfRange(e, current, common, len) >= 0)
                        return false;
            }
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        }


De `addIfAbsent()` methode is hierboven getoond. We zien dat bij schrijfbewerkingen de methode vergrendeld wordt en er een array-kopieerbewerking wordt uitgevoerd. Daarom is het, net als bij CopyOnWriteArrayList, ook voor CopyOnWriteArraySet beter om niet te veel schrijfbewerkingen uit te voeren.


ConcurrentHashMap

Er zijn twee manieren om een gesynchroniseerde HashMap te creëren:


  • Collections.synchronizedMap(new HashMap<>())
  • ConcurrentHashMap


ConcurrentHashMap is, net als HashMap, een Hash-gebaseerde Map. Het biedt een efficiëntere manier om gelijktijdigheid te garanderen dan synchronizedMap.


Voordat Java 8 werd uitgebracht, werd een Segment gebruikt, dat ReentrantLock erfde, om gelijktijdigheid te garanderen door de gebieden te verdelen en per gebied een lock te gebruiken.


Vanaf Java 8 wordt een andere methode gebruikt, waarbij elke tabel-bucket onafhankelijk wordt vergrendeld. Als er een nieuw element wordt toegevoegd aan een lege bucket, wordt in plaats van een lock de CAS-algoritme gebruikt. Voor andere wijzigingen wordt een gedeeltelijke lock verkregen op de eerste node van elke bucket (met een synchronized block), waardoor de contention tussen threads wordt geminimaliseerd en gelijktijdigheid wordt gegarandeerd.


Laten we eens kijken naar de code van de `putVal()` methode van ConcurrentHashMap, die een nieuw element toevoegt. Hiermee kunnen we zien hoe gelijktijdigheid wordt gegarandeerd. De volgende code is gebaseerd op Java 11.


De `putVal()` methode kan grofweg in twee gevallen (met in totaal vier vertakkingen) worden verdeeld.


  • Een nieuwe node toevoegen aan een lege hash-bucket.
  • Een nieuwe node toevoegen aan een hash-bucket die al nodes bevat.


final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node[] tab = table;;) {
            Node f; int n, i, fh; K fk; V fv;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
                        // (1)
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                                // (2)
                if (casTabAt(tab, i, null, new Node(hash, key, value)))
                    break;
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else if (onlyIfAbsent // check first node without acquiring lock
                     && fh == hash
                     && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                     && (fv = f.val) != null)
                return fv;
                        // (3)
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node e = f;; ++binCount) {
                                K ek;
                                                                // (4)
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node pred = e;
                                                                // (5)
                                if ((e = e.next) == null) {
                                    pred.next = new Node(hash, key, value);
                                    break;
                                }
                            }
                        }
                                                // (6)
                        else if (f instanceof TreeBin) {
                            Node p;
                            binCount = 2;
                            if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                                ...
            }
        }
                ...


Een nieuwe node toevoegen aan een lege hash-bucket.

(1) Om een nieuwe node toe te voegen wordt gecontroleerd of de bucket leeg is door de waarde van de betreffende bucket (`tabAt()`) op te halen.


static final  Node tabAt(Node[] tab, int i) {
        return (Node)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);


(2) Er wordt toegang gekregen tot de volatile variabele die in de node is opgeslagen. De huidige waarde (null) wordt vergeleken met de nieuwe waarde. Als deze gelijk zijn, wordt de nieuwe node opgeslagen. Als deze niet gelijk zijn, wordt de for-loop opnieuw uitgevoerd. Dit is de CAS-algoritme.


static final  boolean casTabAt(Node[] tab, int i, Node c, Node v) {
        return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);


Het CAS-algoritme wordt gebruikt om atomische bewerkingen en zichtbaarheidsproblemen op te lossen en gelijktijdigheid te garanderen.


Een nieuwe node toevoegen aan een hash-bucket die al nodes bevat.

(3) Als er al nodes in de bucket zitten, wordt een synchronized block gebruikt om te voorkomen dat meerdere threads tegelijkertijd toegang hebben. Omdat de lock wordt verkregen op de niet-lege hash-bucket van het type Node, worden threads die toegang proberen te krijgen tot dezelfde bucket in een blocking-status geplaatst.

(4) Vervang de node door een nieuwe node.

(5) Als er een hash-conflict is, wordt het element toegevoegd aan de `Separate Chaing`.

(6) Als er een hash-conflict is, wordt het element toegevoegd aan de `Tree`.


Referenties


Verwachte interviewvragen en antwoorden

Wat zijn de problemen met Vector, HashTable en Collections.SynchronziedXXX?

De klassen Vector, HashTable en SynchronziedXxx gebruiken `synchronized` methoden of blokken en delen één lock-object. Als één thread de lock heeft verkregen, kunnen andere threads geen enkele methode gebruiken en komen ze in een blocking-status. Dit kan leiden tot prestatieverlies in de applicatie.


Wat is het verschil tussen SynchronizedList en CopyOnArrayList?

SynchronizedList vergrendelt zowel lees- als schrijfbewerkingen op de instantie zelf. CopyOnArrayList daarentegen vergrendelt bij schrijfbewerkingen het betreffende blok, maakt een kopie van de elementen in de originele array en maakt een nieuwe tijdelijke array. De schrijfbewerking wordt uitgevoerd op deze tijdelijke array en vervolgens wordt de originele array bijgewerkt. Dit maakt het mogelijk dat leesbewerkingen niet worden vergrendeld, waardoor CopyOnWriteArrayList beter presteert dan SynchronizedList tijdens het lezen. De schrijfbewerkingen zijn echter duurder vanwege de array-kopieerbewerking, waardoor CopyOnWriteArrayList minder goed presteert dan SynchronizedList tijdens het schrijven.

Als er meer leesbewerkingen zijn dan schrijfbewerkingen, is het dus efficiënter om CopyOnArrayList te gebruiken.


Wat is het verschil tussen SynchronziedMap en ConcurrentHashMap?

SynchronziedMap vergrendelt zowel lees- als schrijfbewerkingen op de instantie zelf. ConcurrentHashMap daarentegen vergrendelt elke tabel-bucket onafhankelijk. Als er een nieuw element wordt toegevoegd aan een lege bucket, wordt in plaats van een lock de CAS-algoritme gebruikt. Voor andere wijzigingen wordt een gedeeltelijke lock verkregen op de eerste node van elke bucket (met een synchronized block), waardoor de contention tussen threads wordt geminimaliseerd en gelijktijdigheid wordt gegarandeerd.

제이온
제이온
제이온
제이온
[Spring] @Async gebruiken Ontdek hoe je Java-asynchrone verwerking gemakkelijk kunt implementeren met behulp van Spring @Async. Met de @Async-annotatie kun je synchrone methoden naar asynchroon converteren en de efficiëntie verhogen door thread-poolinstellingen. We behandelen ook

25 april 2024

Wat is het Java Collections Framework (JCF)? - De definitie en kenmerken van JCF (JAVA) Het Java Collections Framework (JCF) is een verzameling Java-klassen die een gestandaardiseerde manier bieden om met grote hoeveelheden gegevens te werken. JCF implementeert gegevensopslagstructuren en algoritmen als klassen om codeherbruik, prestaties en

27 april 2024

[Effectieve Java] Item 6. Vermijd onnodige objectcreatie Een gids over het verminderen van onnodige objectcreatie in Java. Voor onveranderlijke objecten zoals String en Boolean is het beter om literals te gebruiken, en voor reguliere expressies is het beter om Pattern-instanties te cachen. Autoboxing kan ook le

28 april 2024

[Concurrency] Atomaire bewerking: Memory Fence en Memory Ordering Deze blogpost bespreekt hoe geheugensorde te overwegen in atomaire bewerkingen en het belang van de Ordering-opties. We bespreken verschillende Ordering-opties zoals Relaxed, Acquire, Release, AcqRel, SecCst, inclusief een beschrijving van de voor- en nad
곽경직
곽경직
곽경직
곽경직
곽경직

12 april 2024

Hoe Rust concurrency-fouten voorkomt Rust is een krachtige taal die de uitdagingen van concurrency-programmeren aanpakt. Door het type-systeem en eigendomsmodel is het veilig om gegevens tussen threads te verzenden en te delen. Met behulp van interne veranderlijkheidspatronen zoals Mutex, Ch
곽경직
곽경직
곽경직
곽경직
곽경직

28 maart 2024

[Niet-major, overleven als ontwikkelaar] 14. Samenvatting van veelgestelde technische interviewvragen voor beginnende ontwikkelaars Deze gids is bedoeld om beginnende ontwikkelaars te helpen met de voorbereiding op technische interviews. Het behandelt concepten die vaak ter sprake komen tijdens interviews, zoals het hoofdgeheugengebied, gegevensstructuren, RDBMS en NoSQL, procedurele
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자

3 april 2024

[Javascript] Object-structuur (V8) Het JavaScript Object wordt in de V8-engine geoptimaliseerd als een structuur afhankelijk van de toestand en werkt als een Fast-modus en een Dictionary-modus die als een hashmap werkt. De Fast-modus is snel met keys en waarden in een bijna vaste vorm, maa
곽경직
곽경직
곽경직
곽경직
곽경직

18 maart 2024

Fysieke datamodelering Fysieke datamodelering is het proces van het ontwerpen van tabellen in een relationele database voor daadwerkelijk gebruik. Dit omvat het optimaliseren van de prestaties door middel van opslagruimte-efficiëntie, gegevensverdeling en indexontwerp. Trage qu
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그

9 april 2024

[Observability] Vector-uitdagingen voor logaggregatie Vector, een logaggregatie- en -verwerkingshulpmiddel ontwikkeld door DataDog, is geschreven in Rust en vereenvoudigt het schrijven van logconversiecoudes in vergelijking met Otel. Het ondersteunt integratie met Loki in Kubernetes-omgevingen via Helm. Het
Sunrabbit
Sunrabbit
Sunrabbit
Sunrabbit
Sunrabbit

9 maart 2024