Threads volatile

sieht wie eine News aus, dürfte aber doch altbekanntes Problem sein, volatile-Variablen

die Erklärung

Im SimpleExample ist die while-Schleife des Looper-Threads oben ein solcher Hot Spot. Beim Übersetzen in Maschinen-Code wird der JIT-Compiler keine Veränderung der Variable finish innerhalb der Schleife feststellen. Da finish nicht als volatile deklariert ist, muss man bei der Optimierung nur den aktuellen Thread berücksichtigen. Daher lässt sich die Überprüfung von finish während des Schleifendurchlaufs komplett entfernen. Damit haben Änderungen an der Variable keinen Einfluss mehr auf das Verhalten der while-Schleife.

zum Code

02   static class Looper extends Thread {
03     boolean finish = false;
04
05     @Override public void run() {
06       while (!finish) {
07         // do something
08       }
09     }
10   }
11
12   public static void main(String[] args) throws Exception {
13     Looper looper = new Looper();
14     looper.start();
15     Thread.sleep(1000); // wait 1s
16     looper.finish = true;
17     System.out.println("Wait for Looper to terminate...");
18     looper.join();
19     System.out.println("Done.");
20   }
21 }

finde ich erstaunlich,

zum einen ist es das Wesen der Schleife mit boolean-Variable, auf Wechsel dessen zu warten, wie kann man die Wegoptimierung überhaupt vorschlagen?
also abgesehen von unbedachten Fehler in Version 1.0, aber es wird doch auch Updates geben,
was spart diese Überprüfung ein gegenüber welchen gigantischen denkbaren Problemen, lange nicht mehr unbekannt? gibt es das wirklich?

auch hochinteressant die Ansicht, dass irgendwer ‚feststellen‘ kann, was in der Schleife, im eigenen Code so alles passieren kann oder nicht,
wie ist dazu das Vorgehen, alle Schreibzugriffe an die Variable werden gesucht und von dort Wege zur Schleife zurückverfolgt?

  • prüft der Optimierer auch mögliche textuelle Reflection-Aufrufe der main-Methode mit dem Befehl?
  • falls man if (Zufallszahl > 0.0000001) main() mit einbaut, würde der Optimierer die Möglichkein erkennen und auf Streichen verzichten?
  • und das über theoretisch unendliche Ebenen, wer weiß wie viel an Quellcode, eigenlich auch gesamte Java-API usw. miteinzubeziehen, was ist mit dynamisch geladenen zusätzlichen Code?

Compiler und Hilfen in IDEs a la ‚Zugriff auf nicht-initialisierte Variable/ Unreachable Code‘ machen auch ein wenig in die Richtung,
wohlweislich aber nur hinsichtlich überschaubaren lokalen Code, lokale Variablen, nicht Instanzvariablen

dass ‚der Optimierer‘ beweissicher so schlau ist (und die Zeit dafür hat), halte ich für ausgeschlossen,
streicht dann also relativ pauschal, selbst mittelkomplexe Programme mit finish-Änderung im eigenen Thread (!) in Aufruf 5 Klassen entfernt im 5000. Durchlauf der Schleife gehen nicht?

oder bezieht sich der Artikel nur auf den Dummy-Code mit leerer Schleife, nur dort optimiert?
das bringt dann ja wenig Erkenntnis für reale Welt,
vielleicht falls es auch mit nur einem sleep-Befehl passiert, gibt es etwas öfter

das ist alles schwer zu glauben, war da nicht noch was mit unterschiedlichen Speicherbereichen pro Threads, lokale Kopien, selbst wenn

Entwickler vermuten als Ursache oft Prozessor-Caches, die den Wert zwischenspeichern, statt ihn in den Hauptspeicher zu schreiben. Allerdings lässt sich das Verhalten auch problemlos auf einem Single Prozessor-System nachstellen, bei dem alle Threads den gleichen Prozessor-Cache verwenden.

aus dem Artikel schon bedacht worden sein sollte?

a la

oder zählt das unter Fehlinterpretation?

naja, ärgerlich alles so oder so

Denke nicht dass der Java Compiler die Variable entfernt, soviel Compiler-Optimierungen sind eher bei C/C++ angesagt.
Laesst sich durch dekompilieren sehr schnell rausfinden…

IMHO ist die eigentliche Ursache die sog. „Thread Caches“, d.h. ohne Volitale (impliziert update/flush) bekommt das looper Objekt die Aenderungen an finish gar niocht mit, weil das looper Objekt noch mit dem alten Zustand arbeitet der im Thread Cache steckt.
Dieses Problem gibt es IME nicht auf Single Core Prozessoren, weil beim Threadwechsel asutom. die aktuellen Werte aus dem Hauptspeicher geholt werden.

Angelika Langer beschreibt dass uebrigens besser und korrekter :wink:

@SlaterB : Dass dir das jetzt so überraschend zu scheinen scheint, irritiert mich etwas: http://www.****-*****.org/allgemeine-java-themen/112143-thread-beenden-2.html#post720960 (die „Zensur“ ist von mir - der soll schließlich nicht vom Byte-Welt.net PageRank profitieren…), mit der schon dort gegebenen Empfehlung it republik - Das umfassende Portal zu allen IT-Themen

Den passenden Vortrag hätten sich interessierte gestern auf der 50 Geschenke für 3-jährige Jungs - Spielzeugtipps anhören können (komme gerade von da :D). Dort (d.h. auf der Konferenz allgemein) wird dieses Problem übrigens nicht nur von der Java-Seite aus beleuchtet (da die Konferenz selbst auch etwas auf C/C++ ausgerichtet ist (aber immer wieder mit interessanten, Abwechslungsreichen Abstechern)) : Das gleiche Problem existiert auch in C und anderen Sprachen, weil es kein Problem der Sprache der des JITs ist, sondern der Prozessorarchitektur. Also, im allgemeinen liegt es wohl an den Caches - das besondere bei Java und dem JIT ist eigentlich „nur“, dass es dort auch auftreten kann, wenn man nur einen Kern hat. Zugegeben, dass hätte ich nicht gewußt und nicht erwartet, aber wenn er nicht gegen das Java Memory Model verstößt, kann der JIT eigentlich machen, was er will.

wie ist dazu das Vorgehen, alle Schreibzugriffe an die Variable werden gesucht und von dort Wege zur Schleife zurückverfolgt?

Nun, gerade das macht er ja NICHT. WAS der JIT nun genau WIE optimiert - ja, das ist die Million Dollar Frage. Alles, was ich sagen kann, ist: Der macht ziemlich kranken Shice (und wenn Leute immer behaupten „Ich verwende C++, das ist effizienter, da kann man auch Assembler einbinden“ usw, lache ich mir innerlich immer ins Fäustchen. Wer glaubt, gut C++ und Assembler programmieren zu können, möge sich bitte bei mir melden). Natürlich ist es unmöglich, mit Sicherheit zu sagen, DASS eine bestimmte Variable durch eine Methode die in der Schleife aufgerufen wird, verändert wird (bei der angesprochenen „theoretisch unendlichen Verschachtelungstiefe“, Reflection & Co). Aber in diesem Fall ist es ja sehr einfach, zu erkennen, dass sie NICHT verändert wird.

Reflection scheint ziemlich viel von dieser Optimierung pauschal abzuschalten. Der Aufruf einer Methode

    private static void maybeChange(Looper looper)
    {
        try
        {
            Field f = Looper.class.getDeclaredField("finish");
        }
        catch (Throwable t)
        {
            t.printStackTrace();
        }
    }

aus der Schleife heraus bewirkt, dass zwar diese Methode (mit all ihrem Rattenschwanz) in die „run“-Methode geinlinet wird, aber die Optimierung fällt weg (obwohl das Field nicht geändert wird - und nebenbei auch, wenn man sich ein anderes Field holt).

Ansonsten kann da wohl ziemlich viel mit On-Stack-Replacement gelöst werden. Das ganze durch manuelle Analysen nach dem Black-Box-Schema herauszufinden, ist schwierig. Bei sowas naivem wie

class SimpleExample
{
    static class Changer
    {
        Looper looper;
        boolean change;
        
        void change()
        {
            if (change)
            {
                looper.finish = true;
            }
        }
    }
    
    static class Looper extends Thread
    {
        boolean finish = false;
        
        @Override
        public void run()
        {
            while (!finish)
            {
                Changer changer = new Changer();
                changer.change();
            }
        }
    }

    public static void main(String[] args) throws Exception
    {
        Looper looper = new Looper();
        looper.start();
        Thread.sleep(5000); // wait 
        looper.finish = true;
        System.out.println("Wait for Looper to terminate...");
        looper.join();
        System.out.println("Done.");
    }

}

Kommt für die Schleife am Ende raus:


  0x00000000026bda75: test   %eax,-0x24fda7b(%rip)        # 0x00000000001c0000
                                                ;*goto
                                                ; - SimpleExample$Looper::run@19 (line 29)
                                                ;   {poll}
  0x00000000026bda7b: jmp    0x00000000026bda75

Tja. Der Changer changt nix (warum muss ich jetzt an einen Amerikanischen Präs…(nicht ablenken!)), d.h. auch da hängt er in der Endlosschleife. Wenn man ihn ändert zu

    static class Changer
    {
        static int counter = 0;
        Changer()
        {
            counter++;
        }
        Looper looper;

        void change()
        {
            if (counter==0xBADAD)
            {
                looper.finish = true;
            }
        }
    }

terminiert es, aber der Assemblercode wird (wie Assemblercode das eben so zu tun beliebt) reichlich unübersichtlich (aber auch wieder komplett geinlinet - also das Ändern des ‚counter‘-Fields steht dann tatsächlich in der „run()“-Methode des Runners!)

Weiter hab’ ich jetzt aber noch nicht geschaut…

[QUOTE=Marco13]@SlaterB : Dass dir das jetzt so überraschend zu scheinen scheint, irritiert mich etwas: http://www.****-*****.org/allgemeine-java-themen/112143-thread-beenden-2.html#post720960 (die „Zensur“ ist von mir - der soll schließlich nicht vom Byte-Welt.net PageRank profitieren…), mit der schon dort gegebenen Empfehlung it republik - Das umfassende Portal zu allen IT-Themen
[/QUOTE]
also, volatile ist bei mir die letzten Jahre angekommen, auch nicht wieder vergessen wie anders,
obwohl ich es selber wohl noch nie benutzt habe,

mich überrascht die Optimierungs-Begründung und die scheint ja wirklich allgemein abenteuerlich zu sein,
womit ich dann mogels Link tadeln kann :wink:

falls das damals schon in irgendeinen Link stand, dann habe ich das gewiss nicht gelesen,
kannst ruhig noch deutlicher hinweisen, wäre interessante Bestätigung des aktuellen Artikels

Nun, die Bestätigung habe ich vorhin zumindest selbst nachvollzogen: Der JIT spuckt tatsächlich genau das aus


10 Check if flag is set
20 goto 10

Aber wie gesagt: Dass der JIT das macht, war mir auch neu (aber es ist ja “nur” eine Begründung, warum es auch auf einem Single-Core falsch ist - allgemein falsch ist es ja sowieso :D)