11 svar
67 visningar
sampledragon5 är nöjd med hjälpen
sampledragon5 495
Postad: 19 feb 10:49 Redigerad: 19 feb 10:57

Variabeln Counter skriver ibland ut ett tal som är mindre än 10 000.

 

public class App  {
    private  static int counter = 0;

    public static void main(String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 10_000; i++) {
                    counter++;
                }
            }
        });

        t1.start();

        for(int i = 0; i < 10_000; i++) {
            counter++;
        }
        System.out.println(counter); // *****

    }
}

 

Innan jag körde koden så förväntade jag mig att ett tal mellan [10 000 , 20 000] skulle printas.

Anledingen till att jag trodde att ett tal i det intervallet skulle printas är för att om OS schedular låter main tråden och t1 tråden köras på två seperata kärnor i CPU:n så kommer counter variabeln att läsas samtidigt, öka med 1 och sedan kommer ökningen att sparas i counter samtidigt, vilket borde resultera i att counter = 10 000 i det scenariot.   

Men, om t1 och main tråden aldrig körs i två seperata kärnor, dvs. att OS schedular först låter ena tråden köras i x antal tidsenheter och sedan switchar över till den andra tråden i y tidsenheter (dvs. att trådarna körs concurrently) samt att main tråden är klar med sin loop före t1, så kommer counter att ligga mellan (10 000 , 20 000) eftersom att main tråden har ändrat  på counter till 10 000 samt så har t1 ökat counter under tiden då main tråden inte vara klar med sin loop och därav bör counter vara mer än 10 000. Men eftersom t1 inte hann färdigt med sin loop innan counter printades, så måste countern vara mindre än  20 000. 

Sedan tänkte jag att counter även skulle kunna vara 20 000 för om t1 hinner klart med sin loop före main tråden, då har ju redan t1 ökat counter till 10 000 så med andra ord kommer även main tråden göra det dvs. 10 000 + 10 000 = 20 000.

 

MEN när jag körde koden så var counter ibland MINDRE ÄN  10 000. Jag förstår inte logiken till hur counter kan bli mindre än 10 000. Kan ni snälla ge mig ett senario där countern skulle kunna bli mindre än 10 000 ? Dessutom undrar jag om countern kan bli större än 20 000 i praktiken 🤔 ( jag har kört koden många gånger men har aldrig sett att countern blir större än 20 000, countern har som max varit 20 000 de gånger jag har kört koden) 

Notera, jag vet hur man löser problemet (vi behöver en synchronized metod) , jag vill bara ha hjälp med att förstå hur countern kan bli mindre än 10 000 samt om countern i praktiken kan bli större än 20 000 🤔 

Laguna 28708
Postad: 19 feb 14:16

När man utför counter++ gör man det förmodligen genom att hämta värdet till ett register, inkrementera registret och sedan spara tillbaka värdet, för det brukar inte finnas instruktioner som gör aritmetik direkt på primärminnet (det skulle kunna göra det i Javas abstrakta maskin, men det kanske inte gör det).

Så för en tråd blir det

r = counter

r++

counter = r

Om nu två trådar gör det här samtidigt, med samma counter men olika r så kan det bli så här

r1 = counter

r2 = counter

r1++

r2++

counter = r1

counter = r2

Du inser kanske att då har counter bara fått värdet counter+1, inte counter+2.

Det här händer nog ganska ofta, eftersom trådarna gör exakt samma sak samtidigt. På slutet kanske main-tråden har råkat hamna lite före, så den skulle skriva ut 10000, men t1 är inte klar och har just skrivit dit 9998.

Större än 20000 ska det inte kunna bli.

sampledragon5 495
Postad: 19 feb 14:49
Laguna skrev:

När man utför counter++ gör man det förmodligen genom att hämta värdet till ett register, inkrementera registret och sedan spara tillbaka värdet, för det brukar inte finnas instruktioner som gör aritmetik direkt på primärminnet (det skulle kunna göra det i Javas abstrakta maskin, men det kanske inte gör det).

Så för en tråd blir det

r = counter

r++

counter = r

Om nu två trådar gör det här samtidigt, med samma counter men olika r så kan det bli så här

r1 = counter

r2 = counter

r1++

r2++

counter = r1

counter = r2

Du inser kanske att då har counter bara fått värdet counter+1, inte counter+2.

Det här händer nog ganska ofta, eftersom trådarna gör exakt samma sak samtidigt. På slutet kanske main-tråden har råkat hamna lite före, så den skulle skriva ut 10000, men t1 är inte klar och har just skrivit dit 9998.

Större än 20000 ska det inte kunna bli.

"Om nu två trådar gör det här samtidigt, med samma counter men olika r så kan det bli så här"

Hur kan r vara olika samt vad står r för ? dessutom, hur vet du att svaret inte kan bli större än 20 000 ? 😕

Laguna 28708
Postad: 19 feb 15:16

Om de hade samma r så skulle de krocka helt och hållet i allt de gör. Nu är counter det enda de har gemensamt. Variabeln i har de också en var, för den är lokal. Den ligger antagligen i ett register hela tiden.

Hur mycket har ni lärt er om hur en processor fungerar, hur det går till när ett Java-program körs? (Eller program i andra språk.)


Tillägg: 19 feb 2024 15:17

Man gör bara 20000 inkrementoperationer sammanlagt.

 

sampledragon5 495
Postad: 19 feb 16:29
Laguna skrev:

Om de hade samma r så skulle de krocka helt och hållet i allt de gör. Nu är counter det enda de har gemensamt. Variabeln i har de också en var, för den är lokal. Den ligger antagligen i ett register hela tiden.

Hur mycket har ni lärt er om hur en processor fungerar, hur det går till när ett Java-program körs? (Eller program i andra språk.)


Tillägg: 19 feb 2024 15:17

Man gör bara 20000 inkrementoperationer sammanlagt.

 

"Det här händer nog ganska ofta, eftersom trådarna gör exakt samma sak samtidigt. På slutet kanske main-tråden har råkat hamna lite före, så den skulle skriva ut 10000, men t1 är inte klar och har just skrivit dit 9998." Jag förstår inte hur main tråden kan ha hunnit före om main tråden och den andra tråden körs samtidigt...

Är det möjligt att detta har hänt ? 

1. OS schedular bestämmer att t1 ska köras i låt oss säga 0,1 ms och under den tiden är counter = 567 men när t1 har läst värdet av countern (det vill säga nästa steg var att öka countern med 1, stoppades exekveringen av t1 (eftersom 0,1 ms har passerat).

2. Sedan körs main tråden i låt oss säga 1 ms. Och under den tiden är counter = 5699 men till skillnad från t1, stoppades exekveringen av main tråden direkt efter att main tråden ökade counter variablen.

3. Sedan körs t1 men OBS ... eftersom t1 stoppades efter att ha läst counter variablen , kommer t1-variabeln att använda värdet av vad den läste i steg 1. det vill säga 567. Sedan låt oss säga att t1 ökar countern från 567 till 3056.

4. OBS eftersom huvudtråden inte läste countern variablen  vid steg 2. så kommer huvudtråden att använda vad countern är lika med just nu, vilket är 3056, trotts att countern vid steg 2. var ett större tal, 5699 och därför förklarar detta logiken varför mitt program ibland skriver ut ett tal mindre än 10 000.

 

Det jag har lärt mig är att java kod först översätts till byte kod av kompilatorn ( när vi skriver javac App.java) sedan så översätts byte koden till maskin kod ( komandot: java App)  av JVM. Maskin kod förstår CPU:n ( CPU:n är hårdvaran som faktiskt kör alla program) och på så sätt så körs java program. 

App.java filen består av två stycken trådar... JVM skapar main tråden när komandot java App körs. Så det CPU:n gör är att köra denna main tråd, som i sin tur kör all kod som finns i main metoden. Sedan, när main tråden kör t1.start() , så kommer CPU:n att köra två trådar, main tråden och t1 tråden. 

Det CPU:n dessutom kör är OS schedular. Det OS schedular gör är att ge CPU tid till alla trådar som körs, dvs. main och t1. Om CPU:n består utav flera kärnor, så kan CPU schedular låta t1 och main att köras samtidigt på olika kärnor ( om två kärnor inte är upptagna). Annars, om alla kärnor är upptagna förutom en så kommer main och t1 att köras på den kärnan genom att OS schedular kommer låta ena tråden att köras i x antal tidsenheter, efter att dessa x antal tidsenheter har gått så kommer OS schedular att switcha till den andra tråden och låta den köras i y antal tidsenheter. Switchen från en tråd till en annan är så snabb att vi människor  inte märker av detta med blotta ögat och därav ser det ut som att trådarna körs samtidigt, när de inte egentligen gör det. Om v

 

Dessutom har jag lärt mig om cache hårdvaran, den sparar fields som ständigt blir läst i RAM minnet för att programmet ska köras fortare. Detta sker genom att en kopia av denna field skapas och sedan sparas i cache minnet ( detta kallas för data caching). Att läsa data från samtliga cache minnen ( L1, L2, L3) är en snabbare process än att läsa data från minnet. Dock så kan detta leda till problem som jag sedan löste genom att deklarera variabeln som volatile, volatile ser till så att variablen inte blir cachad. 

 

Mer än så har jag inte lärt mig just nu om CPU:n... vad mer tycker du jag borde kunna för att förstå mig på mer om trådar ?? Tack för hjälpen förresten ! 

Laguna 28708
Postad: 19 feb 22:42

Jag skulle säga att Javas bytekod inte översätts till maskinkod av JVM, utan att den exekveras byte för byte av JVM. Bytekoden går att översätta till maskinkod, men då behöver inte JVM vara med när man kör koden. JVM själv är ett program, skrivet i något mer hårdvarunära, t.ex. C eller C++ eller assembler (bara troligt för små delar).

Det finns många sätt att implementera trådar, med eller utan stöd från OS:et, och de har olika egenskaper.

Om du får tillfälle så lär dig om assembler/maskinspråk och något relativt maskinnära språk, t.ex. C.

Programmeringsspråksmiljöerna gör vad de kan för att slippa göra onödiga uppdateringar i minnet, så den globala variabeln counter kan vara i ett register hela tiden, och då missar man förstås att den andra tråden kan uppdatera counter så att det egna värdet inte stämmer. För att undvika det kan man använda olika deklarationer i programmeringsspråken, t.ex. volatile. Det jag beskrev i exemplet tidigare kan hända i alla fall, för att det inte finns det som kallas atomära (atomic) operationer på variabler i minnet. Då får man använda någon låsmekanism.

Jag kan en del om det här, men inte allt, t.ex. inte hur det är i Java, och hur det brukar fungera med multipla kärnor. Cachen som processorn använder bör vara helt transparent för användarkod, tycker jag, men det är möjligt att man kan gå farliga genvägar om man har flera kärnor.

sampledragon5 495
Postad: 20 feb 13:15
Laguna skrev:

Jag skulle säga att Javas bytekod inte översätts till maskinkod av JVM, utan att den exekveras byte för byte av JVM. Bytekoden går att översätta till maskinkod, men då behöver inte JVM vara med när man kör koden. JVM själv är ett program, skrivet i något mer hårdvarunära, t.ex. C eller C++ eller assembler (bara troligt för små delar).

Det finns många sätt att implementera trådar, med eller utan stöd från OS:et, och de har olika egenskaper.

Om du får tillfälle så lär dig om assembler/maskinspråk och något relativt maskinnära språk, t.ex. C.

Programmeringsspråksmiljöerna gör vad de kan för att slippa göra onödiga uppdateringar i minnet, så den globala variabeln counter kan vara i ett register hela tiden, och då missar man förstås att den andra tråden kan uppdatera counter så att det egna värdet inte stämmer. För att undvika det kan man använda olika deklarationer i programmeringsspråken, t.ex. volatile. Det jag beskrev i exemplet tidigare kan hända i alla fall, för att det inte finns det som kallas atomära (atomic) operationer på variabler i minnet. Då får man använda någon låsmekanism.

Jag kan en del om det här, men inte allt, t.ex. inte hur det är i Java, och hur det brukar fungera med multipla kärnor. Cachen som processorn använder bör vara helt transparent för användarkod, tycker jag, men det är möjligt att man kan gå farliga genvägar om man har flera kärnor.

Hmmm... men asså  jag fattar fortfarande inte hur counter kan bli mindre än 10 000 :( Om trådarna körs samtidigt (dvs. i seperata kärnor)  så borde ju counter vara 10 000 ? 

Laguna 28708
Postad: 20 feb 13:44

Det är inte säkert att de börjar samtidigt, på mikrosekunden.

Vad får du typiskt för tal som är mindre än 10000?

sampledragon5 495
Postad: 20 feb 14:45 Redigerad: 20 feb 14:56
Laguna skrev:

Det är inte säkert att de börjar samtidigt, på mikrosekunden.

Vad får du typiskt för tal som är mindre än 10000?

jag kan få typ 6000, sedan 9000, sedan 4000 ( finns inget mönster ) "

Det är inte säkert att de börjar samtidigt, på mikrosekunden."

Låt oss säga att t1 körs först enda tills i = 7, då är counter = 6. Men om main börjar sedan köras (dvs. att t1 och main nu körs parallellt, då kommer ju t1 och main öka counter samtidigt, dvs. counter kommer att öka med 1 och inte 2 dvs. när main tråden har körts färdigt, då borde ju counter = 10 006 ? 

Låt oss säga att main körs först enda tills i = 7, då är counter = 6. Men om t1 börjar sedan köras (dvs. att t1 och main nu körs parallellt, då kommer ju t1 och main öka counter samtidigt, dvs. counter kommer att öka med 1 och inte 2 dvs. när main tråden har körts färdigt, då borde ju counter vara minst 10 000 för när vi håller på att printa counter, så kan t1 hinna öka counter med 6, eftersom lokal variabeln i hoss t1:s stack borde vara: 10 000 - 7 = 9993) när main tråden har körts färdigt. Alltså borde counter som max i detta exempel kunna vara 10 006 ? 

så fattar inte hur "Det är inte säkert att de börjar samtidigt, på mikrosekunden." resulterar i ett tal mindre än 10 000 😕

Laguna 28708
Postad: 20 feb 15:48

Jag misstänker att man inte använder två kärnor utan bara en, och att trådarna får ungefär så lång tid i taget att de hinner inkrementera counter några tusen gånger.

Vad är mönstret om du använder 100000 i stället för 10000?

sampledragon5 495
Postad: 20 feb 16:37
Laguna skrev:

Jag misstänker att man inte använder två kärnor utan bara en, och att trådarna får ungefär så lång tid i taget att de hinner inkrementera counter några tusen gånger.

Vad är mönstret om du använder 100000 i stället för 10000?

Mönstret blir densamma, dvs. att counter kan bli mindre än 100 000 (jag får helt olika värden, ibland 200 000, ibland 100 00, och ibland mindre än 100 000. 

Men, jag tänker såhär... tänk att vi har ett senario där main tråden och t1 först körs i separata kärnor ett tag men sedan körs parallellt. Såhär tänker jag:

Tänk om main tråden körs först. Om jag inte har fel så kopieras först counter från RAM minnet till registret. Sedan, i registret ökas denna kopia med 1 (dvs. just nu är counter fortfarande lika med 0 i RAM minnet men counter är lika med 1 i registret).

Tänk om sedan main tråden och t1 körs parallellt ( counter variabeln i RAM minnet har inte hunnit uppdaterats). Då kommer i = 1 hoss main stacken.

Eftersom att: 

 System.out.println(counter);

Sker efter att main tråden har kört färdigt sin loop samt att main tråden och t1 nu körs parallellt, så kommer detta resultera i att counter = 9999 efter att main tråden har kört sin loop.

Jag tänker självfallet att detta senario har ägt rum många gånger och därav printas tal som är mindre än 10 000 ibland. Är det möjligt att detta har hänt ? 

Laguna 28708
Postad: 20 feb 17:39

Det är svårt att förklara alla detaljer i det du ser, men det kan hända att en tråd precis har hämtat counter som då är 1000, och då får vänta ett tag medan den andra tråden körs. Den har under tiden ökat counter till 2000, men nu kommer den första tråden tillbaka och skriver 1001 i counter.

Svara Avbryt
Close