-
Notifications
You must be signed in to change notification settings - Fork 0
/
synchronisation.html
18 lines (17 loc) · 17.2 KB
/
synchronisation.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html><html lang="de-ch"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Thread Synchronisation - Finecloud</title><meta name="description" content="Schon in einem einfachen Beispiel entstehen schnell Inkonsistenzen: public class MusikStatistik { private long gesamtgroesse; private int anzahl; private double durchschnitt; public void fuegeHinzu(File datei){ gesamtgroesse += datei.length(); anzahl ++; durchschnitt = gesamtgroesse / anzahl; System.out.println(anzahl + ' Dateien, ' + gesamtgroesse + ' Byte…"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="stylesheet" href="https://www.finecloud.ch/media/plugins/syntaxHighlighter/prism-black.css"><link rel="canonical" href="https://www.finecloud.ch/synchronisation.html"><link rel="alternate" type="application/atom+xml" href="https://www.finecloud.ch/feed.xml"><link rel="alternate" type="application/json" href="https://www.finecloud.ch/feed.json"><meta property="og:title" content="Thread Synchronisation"><meta property="og:site_name" content="Finecloud"><meta property="og:description" content="Schon in einem einfachen Beispiel entstehen schnell Inkonsistenzen: public class MusikStatistik { private long gesamtgroesse; private int anzahl; private double durchschnitt; public void fuegeHinzu(File datei){ gesamtgroesse += datei.length(); anzahl ++; durchschnitt = gesamtgroesse / anzahl; System.out.println(anzahl + ' Dateien, ' + gesamtgroesse + ' Byte…"><meta property="og:url" content="https://www.finecloud.ch/synchronisation.html"><meta property="og:type" content="article"><link rel="shortcut icon" href="https://www.finecloud.ch/media/website/finecloud.png" type="image/png"><link rel="stylesheet" href="https://www.finecloud.ch/assets/css/style.css?v=39da73365516a098a9b73b721fc970e2"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Article","mainEntityOfPage":{"@type":"WebPage","@id":"https://www.finecloud.ch/synchronisation.html"},"headline":"Thread Synchronisation","datePublished":"2022-06-19T06:11","dateModified":"2022-06-20T07:08","description":"Schon in einem einfachen Beispiel entstehen schnell Inkonsistenzen: public class MusikStatistik { private long gesamtgroesse; private int anzahl; private double durchschnitt; public void fuegeHinzu(File datei){ gesamtgroesse += datei.length(); anzahl ++; durchschnitt = gesamtgroesse / anzahl; System.out.println(anzahl + ' Dateien, ' + gesamtgroesse + ' Byte…","author":{"@type":"Person","name":"Finecloud","url":"https://www.finecloud.ch/authors/finecloud/"},"publisher":{"@type":"Organization","name":"Finecloud"}}</script><meta name="google-site-verification" content="seFY9U12uiEq5U3_MyZiX6XWzk0AVFl9zITr2ZKsytY"></head><body><div class="site-container"><header class="top" id="js-header"><a class="logo" href="https://www.finecloud.ch/">Finecloud</a><nav class="navbar js-navbar"><button class="navbar__toggle js-toggle" aria-label="Menu" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle-box"><span class="navbar__toggle-inner">Menu</span></span></button><ul class="navbar__menu"><li><a href="https://www.finecloud.ch/" target="_self">Blog</a></li><li><a href="https://www.finecloud.ch/tags/" target="_self">Tags</a></li></ul></nav><div class="search"><div class="search__overlay js-search-overlay"><div class="search__overlay-inner"><form action="https://www.finecloud.ch/search.html" class="search__form"><input class="search__input js-search-input" type="search" name="q" placeholder="search..." aria-label="search..." autofocus="autofocus"></form><button class="search__close js-search-close" aria-label="Close">Close</button></div></div><button class="search__btn js-search-btn" aria-label="Search"><svg role="presentation" focusable="false"><use xlink:href="https://www.finecloud.ch/assets/svg/svg-map.svg#search"/></svg></button></div></header><main><article class="post"><div class="hero"><figure class="hero__image hero__image--overlay"><img src="https://www.finecloud.ch/media/website/download.jpg" srcset="https://www.finecloud.ch/media/website/responsive/download-xs.jpg 300w, https://www.finecloud.ch/media/website/responsive/download-sm.jpg 480w, https://www.finecloud.ch/media/website/responsive/download-md.jpg 768w, https://www.finecloud.ch/media/website/responsive/download-lg.jpg 1024w, https://www.finecloud.ch/media/website/responsive/download-xl.jpg 1360w, https://www.finecloud.ch/media/website/responsive/download-2xl.jpg 1600w" sizes="100vw" loading="eager" alt=""></figure><header class="hero__content"><div class="wrapper"><div class="post__meta"><time datetime="2022-06-19T06:11">Juni 19, 2022</time></div><h1>Thread Synchronisation</h1></div></header></div><div class="wrapper post__entry"><p>Schon in einem einfachen Beispiel entstehen schnell Inkonsistenzen:</p><p><code>public class MusikStatistik {</code><br><code> private long gesamtgroesse;</code><br><code> private int anzahl;</code><br><code> private double durchschnitt;</code><br><code> </code><br><code> public void fuegeHinzu(File datei){</code><br><code> gesamtgroesse += datei.length();</code><br><code> anzahl ++;</code><br><code> durchschnitt = gesamtgroesse / anzahl;</code><br><code> System.out.println(anzahl + " Dateien, " + gesamtgroesse + </code><br><code> " Byte gesamt. Durchschnitt: " + durchschnitt + </code><br><code> " (Letzte Datei: " + datei.getAbsolutePath()+")");</code><br><code> }</code><br><code> </code><br><code> public static void main(String[] args) throws Exception {</code><br><code> MusikStatistik statistik = new MusikStatistik();</code><br><code> for (File root : File.listRoots()){</code><br><code> new Thread(() -> new Musikfinder(root).</code><br><code> findeMusik(statistik::fuegeHinzu)).start();</code><br><code> }</code><br><code> } </code><br><code>}</code></p><p>Dieses Beispiel benutzt die Klasse Musikfinder, um die Durchschnittsgrösse Ihrer Musikdateien zu berechnen. Um den Vorgang zu beschleunigen, wird für jedes Wurzelverzeichnis ein Thread gestartet. Natürlich bringt das nicht viel, weil auf einem Unix-System nur ein Root Verzeichnis existiert, es geht hier aber nur um ein Code-Beispiel für Thread-Probleme.</p><p>In diesem Beispiel helfen die <em>Atomic</em>-Klassen nicht mehr weiter, denn dass die Additionen auf <em>gesamtgroesse</em> und <em>anzahl</em> nicht atomar sind, ist nur ein Teil des Problems. Es kommt erschwerend hinzu, dass die Werte der beiden Variabeln auch zusammenpassen müssen, denn sonst kann folgende Situation entstehen:</p><ol><li>Thread 1 fügt die erste Datei hinzu, ihre Grösse ist 5MB.</li><li>Thread 1 addiert 5 MB zu <em>gesamtgroesse. </em>Der neue Wert ist 5.242.880.</li><li>Thread 1 inkrementiert <em>anzahl</em>. Der neue Wert ist 1.</li><li>Thread 2 fügt seine erste Datei hinzu, ihre Grösse ist ebenfalls 5MB.</li><li>Thread 2 addiert 5MB zu <em>gemsamtgroesse</em>. Der neue Wert ist 10.485.760.</li><li>Thread 1 berechnet den Durchschnitt und gibt ihn aus: 10.485.760 / 1 = 10.485.760.</li></ol><p>Der ausgegebene Durchschnitt ist offensichtlich falsch: In <em>gesamtgroessee</em> steht bereits die Summe von zwei Dateien, es wird aber nur durch eins geteilt, weil die Anzahl noch nicht inkrementiert wurde.</p><h3>"synchronized" als Modifikator für Methoden</h3><p>Um diese Art von Problem zu vermeiden, gibt es in Java das Schlüsselwort synchronized. Es stellt sicher, dass Operationen auf den Objekten immer vollständig ausgeführt werden, bevor ein anderer Thread eine Operation auf diesem Objekt durchführen kann:</p><p><code>public synchronized void fuegeHinzu(File datei){…}</code></p><p>Diese einfache Erweiterung führt dazu, dass niemals zwei Threads gleichzeitig die Methode <em>fuegeHinzu</em> ausführen können. Das wird durch ein <em>exclusive Lock </em>auf dem Objekt realisiert, eine Sperre, durch die nur ein Thread Zugang zum so geschützten Code erhalten kann. Ein zweiter Thread, der <em>fuegeHinzu</em> aufrufen will, wird solange zum Warten gezwungen, bis der erste Thread sein Lock wieder freigibt, erst dann kann der zweite weiter ausgeführt werden. Eine Sperre, die durch den synchronized-Modifikator an einer Methode eingerichtet wird, gilt für das ganze Objekt, zu dem die Methode gehört. Auch wenn mehrere synchronized-Methoden in einem Objekt vorhanden sind, gibt es nur ein Lock. </p><p>Eine wichtige Grundregel, wenn man ein Objekt <em>threadsicher</em>, also sicher für den Zugriff aus mehreren Threads, machen, lautet deshalb: sei Gründlich. Fehlende Synchronisierung kann zu inkonsistenten Zuständen des Objekts führen, zu viel Synchronisierung verlangsamt im besten Fall das Programm und führt im schlimmsten Fall zu Deadlocks.</p><h3>Das "synchronized"-Statement</h3><p><code>public synchronized void fuegeHinzu(File datei){</code><br><code> gesamtlaenge += berechneLaenge(datei);</code><br><code> anzahl ++;</code><br><code> durchschnitt = gesamtlaenge / anzahl;</code><br><code>}</code></p><p>Dieses Beispiel lässt sich optimieren, indem man die teile, welche nicht synchronisiert werden müssten aus dem synchronized Block entfernt. Um das zu erreichen, gibt es eine zweite Art, das Schlüsselwort synchronized anzuwenden: als Statement, das nur seinen Block mit einem Lock versieht:</p><p><code>public void fuegeHinzu(File datei){</code><br><code> int laenge = berechneLaenge(datei);</code><br><code> synchronized(this){</code><br><code> gesamtlaenge += berechneLaenge(datei);</code><br><code> anzahl ++;</code><br><code> durchschnitt = gesamtlaenge / anzahl;</code><br><code> }</code><br><code>}</code></p><p>Jetzt können beliebig viele Threads gleichzeitig die Länge ihrer Dateien berechnen, nur mit dem Updaten der Variabeln müssen sie warten, bis sie den alleinigen Zugriff haben.</p><p>Das synchronized-Statement ist aber auch etwas komplexer in der Benutzung als eine synchronisierte Methode, denn Sie müssen selbst das Objekt angeben, durch das die Sperre realisiert werden soll, den sogenannten <em>Monitor</em>. Als Analogie hilft es, sich den Monitor als »Redestab« in einer Gruppensitzung vorzustellen: Nur wer den Redestab hält, darf sprechen. Genauso darf nur, wer den Monitor besitzt, den synchronized-Block betreten. Das oben angesprochene Lock entspricht dabei dem abstrakten »Recht zu reden«, der Monitor ist genau wie der Redestab das Werkzeug, mit dem dieses Recht umgesetzt wird – man sagt auch: »das Objekt, auf dem gesperrt wird«.</p><p>synchronized(this) verwendet das umgebende Objekt als Monitor, genau wie eine synchronisierte Methode. Man kann aber auch auf einem beliebigen anderen Objekt sperren, und auch dafür gibt es nützliche Anwendungsfälle:</p><p><code>public static void main(String[] args) throws Exception {</code><br><code> MusikStatistik statistik = new MusikStatistik();</code><br><code> Thread watcher = new Thread(() -> {</code><br><code> while (true){</code><br><code> synchronized (statistik){</code><br><code> System.out.println(statistik.getAnzahl() </code><br><code> + " Dateien, " + statistik.getGesamtgroesse() </code><br><code> + " Byte gesamt. Durchschnitt: " </code><br><code> + statistik.getDurchschnitt());</code><br><code> }</code><br><code> try {</code><br><code> Thread.sleep(10000);</code><br><code> } catch (InterruptedException ex) {</code><br><code> }</code><br><code> }</code><br><code> }, "Beobachter");</code><br><code> watcher.setDaemon(true);</code><br><code> watcher.start();</code><br><code> for (File root : File.listRoots()){</code><br><code> new Thread(() -> </code><br><code> new Musikfinder(root).findeMusik(statistik::fuegeHinzu), </code><br><code> "Crawler " + root.getName()</code><br><code> ).start();</code><br><code> }</code><br><code>} </code></p><p>Die Ausgabe wird jetzt in einem eigenen Thread realisiert, der alle 10 Sekunden tätig wird und den aktuellen Stand der Daten ausgibt. Das Warten wird durch Thread.sleep mit der Wartezeit in Millisekunden umgesetzt.</p><p>Erfolgte die Ausgabe ohne Synchronisation, würde Sie wieder in das alte Problem laufen: Während Sie die Ausgabe machen, aktualisiert ein anderer Thread die Daten und die ausgegeben Daten sind nicht konsistent. Die Getter zu synchronisieren, würde keinen Unterschied machen, denn es wäre nur jeder Getter für sich synchronisiert, zwischen zwei Aufrufen könnte aber nach wie vor ein anderer Thread dazwischenschreiben.</p><p>Um konsistente Ausgaben sicherzustellen, muss die Sperre auf dem Objekt bestehen bleiben, bis Sie alle Daten ausgegeben haben. Genau das erreichen Sie, indem Sie die Ausgabe in einem Block machen, der mit dem Objekt <em>statistik</em> synchronisiert ist. Andere Threads können die synchronisierte Methode <em>fuegeHinzu</em> während der Ausgabe nicht betreten, sie müssen auf den Lock warten.</p><h3>Deadlocks</h3><p>Der Fluch der parallelen Programmierung: Deadlocks - eine Klasse von Fehlern, von der Sie sicher sein können, dass Sie ihr früher oder später begegnen werden. Ein Deadlock kann immer dann auftreten, wenn Sie mit mehreren Monitor-Objekten gleichzeitig sperren müssen und dabei nicht vorsichtig sind. Dann kann es zu folgender Situation kommen, wenn beide Threads versuchen auf denselben Objekten zu synchronisieren. Achten Sie darauf, wo der Unterschied zwischen den beiden Threads liegt:</p><p><code>Thread t1 = new Thread(() -> {</code><br><code> synchronized(objektEins){</code><br><code> synchronized(objektZwei){</code><br><code> … </code><br><code> }</code><br><code> }</code><br><code>});</code><br><code>Thread t2 = new Thread(() -> {</code><br><code> synchronized(objektZwei){</code><br><code> synchronized(objektEins){</code><br><code> … </code><br><code> }</code><br><code> }</code><br><code>});</code><br><code>t1.start();</code><br><code>t2.start();</code></p><p>Der Unterschied zwischen beiden Threads ist zwar subtil, aber wenn Sie den Code aufmerksam lesen, schnell zu finden: beide Threads versuchen, die Locks in unterschiedlicher Reihenfolge zu erhalten. Nun kann das passieren:</p><ol><li>t1 sperrt auf <em>objektEins</em>.</li><li>t2 sperrt auf <em>objektZwei</em>.</li><li>t1 wartet, bis das Lock auf <em>objektZwei</em> freigegeben wird.</li><li>t2 wartet, bis das Lock auf <em>objektEins</em> freigeben wird.</li></ol><p>Da es kein Time-out auf Sperren gibt, geben beide Threads die Locks, die sie halten, erst frei, wenn der jeweilige äussere synchronized-Block beendet ist. Der synchronized-Block kann aber nicht weiter ausgeführt werden, ohne den inneren synchronized-Block auszuführen. Und der kann nicht ausgeführt werden, ohne dass der andere Thread seine Sperre aufgibt. Und so ist es an dieser Stelle sicher, dass keiner der beiden Threads weiter ausgeführt werden kann, beide werden warte, bis der Java-Prozess beendet wird - und wenn es keine Deamon-Threads sind, dann ist das nur noch mit Gewalt möglich.</p><p>Deshalb sollte man versuchen, Deadlocks von vornherein zu vermeiden und der beste Ansatz dazu ist, die synchronized-Blöcke so kurz wie möglich zu halten.</p><p> </p><p> </p></div><footer class="wrapper post__footer"><p class="post__last-updated">This article was updated on Juni 20, 2022</p><ul class="post__tag"><li><a href="https://www.finecloud.ch/tags/java/">java</a></li><li><a href="https://www.finecloud.ch/tags/parallel/">parallel</a></li><li><a href="https://www.finecloud.ch/tags/softwareentwicklung/">software development</a></li></ul><div class="post__share"></div></footer></article></main><footer class="footer"><div class="footer__copyright"><p>Powered by Publii</p></div><button onclick="backToTopFunction()" id="backToTop" class="footer__bttop" aria-label="Back to top" title="Back to top"><svg><use xlink:href="https://www.finecloud.ch/assets/svg/svg-map.svg#toparrow"/></svg></button></footer></div><script>window.publiiThemeMenuConfig = {
mobileMenuMode: 'sidebar',
animationSpeed: 300,
submenuWidth: 'auto',
doubleClickTime: 500,
mobileMenuExpandableSubmenus: true,
relatedContainerForOverlayMenuSelector: '.top',
};</script><script defer="defer" src="https://www.finecloud.ch/assets/js/scripts.min.js?v=6ca8b60e6534a3888de1205e82df8528"></script><script>var images = document.querySelectorAll('img[loading]');
for (var i = 0; i < images.length; i++) {
if (images[i].complete) {
images[i].classList.add('is-loaded');
} else {
images[i].addEventListener('load', function () {
this.classList.add('is-loaded');
}, false);
}
}</script><script defer="defer" src="https://www.finecloud.ch/media/plugins/syntaxHighlighter/prism.js"></script></body></html>