tifyty

pure Java, what else ?

Havi archívumok: február 2013

Codereview sample

Sok évvel ezelőtt került elém egy Java forráskód, amiben valahogy benne maradtak a code review comment-ek:

private final String TETS_USER_NAME = "Jakab Galiban";
//REV: Miért nem static a változó? JD
//REV: Miért kellene annak lennie? GIJ
//REV: Azért, mert így minden osztályhoz létrejön egy
//REV: külön példány, és így lassabb, több memóriát foglal.
//REV: Főleg ha hosszúak a string-ek.

Note:

ez sok évvel ezelőtti emlék. Akkor olvastam ezt egy forráskódban. Nem tudom kik írták bele, a név kezdőbetűkre sem emlékszem, ezért szerepel is G.I. Joe és John Doe. És a pontos megfogalmazásra sem emlékszem, az igazat megvallva arra sem, hogy magyarul volt-e a komment, vagy angolul.

Szóval a saját kommentjeim:

Az ilyen változót valóban static kulcsszóval szoktuk jelölni. Egyébként nem mindig, mert van, hogy olyan helyre írjuk, ahol magától is static lesz, anélkül, hogy ezt kiírnánk. Tehát a komment jó.

Utána jött egy kérdés az ismeretlen junior fejlesztőtől, hogy miért is kellene ennek static-nak lennie. Jogos a kérdés. Miért jogos? Mert nem tudja! Meg kell kérdezni. Teljesen korrekt.

Innen kezdve viszont elkezdődik az a játék, amelyet oly sokszor, és oly sokan játszunk, mert nem látjuk, hogy a másik kezében sem fénykard van, hanem ott is csak egy fehér bot fityeg: vak vezet világtalant. Mert valami persze van. Tényleg több memóriát fog foglalni a nem statikus verzió, és tényleg kicsinyét lassabb lesz (kivéve, ha az osztály szingleton, ugye?). De ez kit érdekel. Amikor arról van szó, hogy a pulton kitöltött féldeci pálinka csak 4 cent, akkor sem állunk neki pampogni, mert sokkal fontosabb, hogy remegő kézzel hamarabb lehörpintsük, mintsem, hogy azt a maradék centet is megkapjuk. Mennyivel lesz gyorsabb a program futása? Fél óra helyett már 29 perc, 59 másodperc és 500 milliszekundum alatt is lefut? Még a perui lámákat sem érdekli! Nekem elhihetitek.

Hogy több memóriát foglal? Hahhhh! Mennyibe kerül a memória? (Híjjnye, minnyárt kapok egy mémes pofont.) Pedig a bentlakásos iskolát nem is említettem, úgyis, mint internátus. (Gy.k: INTERN-átus)

De akkor miért? Azért, mert sok időt elvesz. De nem az olcsó processzor időből, hanem a drága fejlesztő időből, amikor valaki olvassa a kódot, és megakad rajta: Miért nem statik? Hm… Lehet, hogy a menyét elhozta a menyét? És csak gondolkodik, és gondolkodik, mert nem akarja elhamarkodottan azt a következtetést levonni, hogy azért statik, mert aki írta junior volt, mint a fű az első tavaszi vágás után.

Pedig kár gondolkodni. Át kell írni statik-ra, és lefuttatni az összes unit tesztet. Azoknak a lefedettsége 100%, így ha valahol, valami gondot okoz, hogy statik lett a változó, akkor az kiderül igen hamar. Ez csak természetes.

Postmature optimization

ern0 (@ern0plus4)
2013.02.16. 17:22
Arra meg mémes pofont osztanék ki, ha bárki azt mondja: “Nem elég gyors? Majd rakunk alája nagyobb vasat…” – slap! “Nem! Optimalizálsz!”

Ülnek a volt informatikus pásztorok este a tűz körül valahol Peruban (nem mindenki megy ám át az interjún, hogy felvegyék birkapásztornak Nepálba), és azon tanakodnak, hogy így az első tél vége felé, mióta ideköltöztek Győrből, bizony meg kellene nyírni a lámákat. Probláma azonban, hogy sok a láma, lassú a nyírás, és nem lesznek készen mire jön az átvevő, és ha a szőr rajt van a lámán, akkor nem fizet, vagy viszi a lámával együtt is. Itt a hegyen meg láma nélkül mit tereljen egy pásztor. Legfeljebb a híg levegőt.

Valaki javasolja, hogy nyírjanak agilisan, vagy párban, de mindenki tudja a múltjából, hogy ezek a dolog nem csodaszerek. Megy a hümmögés, lassan kortyolják a pisco-t.

  • Mi lenne, ha hívnánk segítséget a faluból? A helyiek értenek jól a lámanyíráshoz, besegítenének. Perszer fizetünk nekik, de még mindig jobban járunk, mint ha nem nyírjuk le a lámák szőrét. Egy munkás egy napra kap 100 Sol-t és lenyír annyi szőrt, ami akár 1000 Sol-t is megér.

Ekkor csattant el az a mémes pofon! Így a pásztorok optimalizáltak, a lámák nagy része meg szőrös maradt azon a tavaszon. A következő tavaszra persze megvolt az optimalizált gépi vágó terve, amivel le tudták volna vágni a láma szőrt, de mivel előző tavasszal kevés volt a bevétel a pásztorok fele elment mielőtt éhen halt volna, és a mindig vágyott álommunka helyett kénytelen, kelletlen elmentek informatikusnak. Enni kell. A lámák egy részét fillérekért eladták a faluban, egy részük elkóborolt, sok éhen halt. Maradt vagy húsz láma, amit meg lehetett volna nyírni akár körömvágó ollóval is. No meg pénz sem volt megépíteni a gépet. Saját elfogyott, nem is volt soha, a venture capital piac sem volt épp a toppon, de ha ott is lett volna, az előző évi mutatók alapján semmi remény nem volt kölcsönre, még a social funding arénában sem.

A két megmaradt pásztor a hegyen nyírta a húsz lámát, adta el a szőrt, és élt nyomorban, mint a faluban az indiánok, akik ugyancsak lámaterelésből éltek. A különbség csak az volt, hogy míg az indiánok esténként gombát ettek és pisco-t ittak, addig a két volt informatikus a tűz mellett egyre finomította a gépi nyíró tervét ami soha nem készült el, nagy láma csordáról álmodoztak, és elalvás előtt mémes pofonokat osztogattak egymásnak.

Itt vége is a történetnek. Aki ebből nem értette meg, hogy fel lehetett volna venni néhány embert, és azzal túlélni azt az időszakot, amíg elkészül a “nagy terv”, (ami általában sosem készül el) az osztogassa a mémes pofonokat. Ha egy megoldás nem a legjobb, de működik az sokkal jobb, mint ha nincs meg a legjobb megoldás csak félig, és nem is működik. De ha meglenne, akkor a lehető legjobb lenne.

Jobb egy híd a szakadék felett, amin át lehet sétálni, mint egy olyan, amelyik elbírna egy tankot is, csak éppen egy méterrel rövidebb, mint a szakadék. Ahogy Molnár Ferenc írta a Játék a kastélyban című darabjában, és ahogy Gál szerepében a felejthetetlen Zenthe Ferenc mondta: “nem kell a legjobbat akarni, elég a jó is”. Perszer ott a nőkről van szó, de ha mélyre ásunk, végül mindig ide jutunk. Mármint a nőkhöz.

Végül: természetesen van olyan helyzet, amikor le kell keverni azt a mémes pofont, de ilyen esetben is: Optimalizálsz fiam! Te meg közben, projekt menedzser fiam intézkedj, hogy addig is legyen nagyobb vas az optimalizálatlan szar alatt, amit ennek a programozónak csúfolt lámapásztornak sikerült magából ide erőltetni.

Optimalizálni kell (nem mindig), de addig is “The show must go on”.

P.S.: Ezeket ern0 pontosan tudja, jól ismerem az urat, dolgoztunk együtt, és ha egyszer dataflow szakértő kell majd, csak hozzá fordulok. Csak éppen egy tweet üzenet nem lehet ilyen hosszú, mint egy parttalanul ömlengő blog, és nem lehet minden aspektust leírni. Mindet még egy blogban sem. Ahhoz rövid az élet.

Tudásteszt, metódus szignatúra

Nem keverjük össze az overriding és az overloading fogalmát. Amíg az első egy adott metódus felüldefiniálása egy leszármaztatott osztályban, addig a második azt jelenti, hogy különböző metódusokat deklarálhatunk ugyanazon a néven, ugyanabban osztályban vagy interfészben, úgy, hogy csak a paraméter listában térnek el. A következők a kérdések:

final

Lehet-e, hogy két metódus csak abban tér el egymástól, hogy az egyiknek a paraméterei final módosítóval lettek megjelölve. Lehetséges-e tehát:

public class Overload {

	public void method(int a, int b) {

	}

	public void method(final int a, final int b) {

	}
}
Nem lehet, mert a final nem része a szignatúrának.

primitive vs. objektum

Mit ír ki ez a program:

public class Overload {

	public void method(int a) {
		System.out.println("int");
	}

	public void method(Integer a) {
		System.out.println("Integer");

	}
	
	public static void main(String[] args){
		new Overload().method(3);
	}
}
A válasz: int.
Amikor kiválasztja, hogy melyik metódust kell meghívnia azt választja, amelyik leginkább illeszkedik a meghívási paraméterekhez.

Hogyan lehet elérni, hogy azt írja ki Integer?

		new Overload().method((Integer)3);

És mit fog kiírni a következő program?

public class Overload {

	public void method(int a, Integer b) {
		System.out.println("int Integer");
	}

	public void method(Integer a, int b) {
		System.out.println("Integer int");

	}

	public static void main(String[] args) {
		new Overload().method(3, 3);
	}
}
Semmit, a program nem fordul le, mert nem egyértelmű, hogy melyik metódust akarjuk meghívni.

És ha kiegészítjük ?

public class Overload {

	public void method(int a, Integer b) {
		System.out.println("int Integer");
	}

	public void method(Integer a, int b) {
		System.out.println("Integer int");

	}

	public void method(int a, int b) {
		System.out.println("int int");

	}

	public static void main(String[] args) {
		new Overload().method(3, 3);
	}
}
Most már egyértelmű, azt írja ki, hogy int int

No, de mit mond a szabvány?

Overloaded methods and constructors are resolved at compile time by picking the most specific method or constructor from those which are applicable.

Magyarul az overloaded metódusok és konstruktorok közül a megfelelő kiválasztása fordítási időben történik meg, és azt választja a fordító, amelyik a legjobban illeszkedik, a legspecifikusabb.

A fordító először megkeresi az összes olyan metódust, amelyik meghívható és amelyik alkalmazható. A meghívható azt jelenti, hogy a használat helyétől és a private, protected és public kulcsszavak használatától függően elérhető-e az adott metódus, az alkalmazható pedig azt jelenti, hogy a neve stimmel-e és az argumentum listája olyan-e, hogy ha csak az a metódus lenne elérhető az adott névvel, akkor meghívható lenne-e.

Amikor az összes ilyen metódust összegyűjtötte a Java, akkor megnézi, hogy boxing és unboxing nélkül mit lehetne meghívni (mert ugye lehetnek kódok, amiket még 1.5 előtt írtak, és akkor még szelídebb volt a Java, nem boxolt, és nem jött még akkor el a karácsony sem). (int <–> Integer és tsai.)

Ha ez nem vezetett eredményre, akkor a kód 1.5 előtt sem fordult volna le, így megpróbálja ismét, de most már lehet bokszolni meg csomagot bontani.

Na, ha még ez sem sikerült, akkor bepróbálkozik még egyszer, utoljára, de most már figyelembe veszi a változó argumentumszámú metódusokat is.

Ha ezek után több metódus is van, akkor az kell meghívni, amelyik specifikusabb. A Java szabvány szerint

The informal intuition is that one method is more specific than another if any invocation handled by the first method could be passed on to the other one without a compile-time type error.

és utána jön két oldalon keresztül egy matematikai definíció, amit nem fogok elmagyarázni. Az azért kell, hogy Java fordítót lehessen írni. Ha ahhoz kell, hogy megértsed, amit a program csinál, akkor valahol, valaki, valamit nagyon elrontott. Általában az overloading egyszerű a gyakorlatban, nem szabad elrettenni tőle, és ilyen furmányos kérdéseket, mint amik fent vannak, csak a lökött job interview-kon a méglököttebb vizsgáztatók szoktak kérdezni.

Break ciklus nélkül?

Ez egy rövid blog bejegyzés!

package wierdo;

public class Wierdo {
	public static void main(String[] args) {
		label: {
			if (args.length == 0)
				break label;
			System.out.println("We happy, have argument!");
		}
		System.out.println("Hello Wierdo!");
	}
}

No?

Mit csinál ez a kód

Ez a post nem különálló poszt, hanem a posztot egészíti ki. Abban a posztban szerepel egy mintaprogram, de a magyarázat egy kicsit hosszú ahhoz, hogy abban a bejegyzésben szerepeljen, ezért itt van külön. A kódot ide is bemásoltam darabokra vágva magyarázatokkal:

import java.util.concurrent.atomic.AtomicInteger;

public class FinalTest {

	private static AtomicInteger counter = new AtomicInteger(0);

	static void waitFor(int n) {
		while (!counter.compareAndSet(n, n + 1)) {
		}
	}

Amit a kód elején látunk az egy atomi integer, amit arra használunk, hogy a később elindított két szál közül mindig csak az egyik fusson, és így pontosan tudjuk, hogy mi milyen sorrendben történik. A való életben persze éppen nem ez történik, azért indítunk el szálakat, hogy azok párhuzamosan fussanak, itt viszont pont azt akarjuk demonstrálni, hogy mi történik akkor, ha a kód futása során az egyik szál eléri a másik szál által félig létrehozott objektumot.

A waitFor metódus a program futás során tervezett n-edik állapotra vár, és ha ez elérkezett (a másik szál módosította a számlálót), akkor megnöveli a számláló értékét és visszatér. Itt kell megjegyezni, hogy az egyszerűség kedvéért itt csak egy üres while ciklus van, ami igazi kódban elfogadhatatlan. Először is, kellene bele legalább egy sleep, hogy ne egye meg az összes CPU időt azon a magon amelyiken fut, másodszor nem foglalkozik azzal, hogy a szál kap-e interruptot, harmadrészt egy ilyen pollozás nem hatékony, inkább wait és notify, notifyAll metódusokat kell használni, és végül, de elsősorban még azokat sem, hanem valami olyan megoldást, ami ennél magasabb szintű, mondjuk egy aszinkron queue.

A main előtt van két osztályunk. Az egyik a majd később elindítandó két szál kódja, (ez a T), a másik pedig az az osztály, amelyik elköveti azt a kerülendő disznóságot, hogy a konstruktor futása közben kiadja a this referenciát (ez a C).

	private static class C {
		final int x;
		int y;

		C(T other) {
			super();
			other.c = this;
			waitFor(0);
			waitFor(3);
			x = 1;
			y = 1;
			waitFor(4);
		}
	}

	private static class T extends Thread {
		public boolean constructor = false;
		public T other = null;
		public C c;

		@Override
		public void run() {
			if (constructor) {
				c = new C(other);
			} else {
				waitFor(1);
				System.out.println("x=" + c.x);
				System.out.println("y=" + c.y);
				waitFor(2);
				waitFor(5);
				System.out.println("x=" + c.x);
				System.out.println("y=" + c.y);
			}
		}
	}

A main mind a két szálat létrehozza, az egyiknek azt mondja, hogy legyen ő az, amelyik meghívja a C() konstruktort, a másik meg majd hozzányúl, és nézi, hogy mi az értéke a változóknak. A konstruktort elindító szál fog érdemi dolgot csinálni előbb, a másik első dolga az, hogy megálljon, és várjon arra, hogy a konstruktor őt továbbengedje (waitFor(1)).

A konstruktor beállítja az argumentumként megkapott szálobjektumban az other mezőt, hogy a másik szál hozzá tudjon férni az objektumhoz, ami éppen most jön létre, majd a waitFor(0) hívással nem vár, mert eleve a 0 állapotból indul a program, ellenben növeli vele a számláló értékét, és így a másik szál kódjában a waitFor(1) vissza fog térni, és hogy a konstruktor ne csináljon semmit, amíg az el nem végzett annyit, amit el kell végeznie, ezért elkezd várni a 3-as állapotra.

A másik szál fogja, és kiírja a még nem teljesen létrehozott embrió objektum x és y mezőjének értékét. Mivel ezek még nem kaptak értéket, ezért mind a kettő értéke definíció szerint a default érték. Ezt követően tovább engedi a konstruktort futtató szálat, és vár arra, hogy az befejezze, amit be kell fejeznie. A konstruktor pedig értéket ad a két változónak, majd ezt jelzi a másik szálnak, aki ismét kiírja, most már a beállított értéket a final x mező esetében, az y esetében meg vagy a default-ot, vagy a beállított értéket.

	public static void main(String[] args) {
		T t = new T();
		t.constructor = true;
		t.other = new T();
		t.start();
		t.other.start();
		try {
			t.join();
			t.other.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	};
}

A main működése triviális.

final, fin ül (update 2013-02-11 14:02)

Mire jó a final? Sok mindenre. Például segít programozni, és segít felfedezni olyan hibákat, amikor egy változó nem csak egyszer kaphat értéket, pedig mi azt szerettük volna. Ha például interfészünk van, akkor abban minden változó alapból static és final, ki sem kell írni. Check style alapbeállítás szerint (de lehet, hogy csak helyi default) a check style rád is kiabál, hogy “Ugyanmár ki ne írd! Tudja azt mindenki!” (Ja! Hiszed te! A check style nem szokott felvételiztetni!) Aztán meg a javac fordító olyan okos is tud lenni, mint ebben a bejegyzésben mutattam, hogy egy final változóról tudja már fordítási időben, hogy mi az értéke, és ezért oda se nyúl a kód, például egy int 127-es értékért, hanem egyenesen belefordítja a kódba, mint konstanst.

Szóval erre jó a final. De jó másra is. Mielőtt azonban ebbe belemennénk tisztáznunk kell valamit:

Attól, hogy egy változót final kulcsóval definiáltunk, az objektum, amit ezen keresztül el lehet érni még megváltozhat!

A final tehát nem garantál immutabilitást. Mi is az immutable, azaz megváltoztathatatlan objektum? Amit nem lehet megváltoztatni. Miért szeretjük az ilyen objektumokat? Azért, mert több szálon futó programok esetében kevésbé kell, hogy fájjon a fejünk a szinkronizálással. Ilyen a String, Integer és még sok egyéb objektum. Persze mi magunk is készíthetünk ilyen objektumokat. Ennek egyik módja például az, hogy az objektumon belül minden egyes mező legyen final. Ami presze nem garantálja, hogy azok az objektumok, amelyekre ezek a mezők tartalmaznak referenciát (emlékeztek gyerekek: milyen gyengék tudunk lenni, ha referenciákról van szó), szóval, hogy azok ne változnának. Aztán persze akkor sem járható ez az út, ha az objektum nem igazi immutable, hanem elkészítjük, csinálunk vele ezt, azt és csak ezután már többet nem változik.

Van olyan szokás is a mi utcánkban, vagy máshol (nálunk csak ha van értelme), hogy egyes osztályok két interfészt implementálnak: egy mutátor, és egy immutátor interfészt. A mutátor deklarálja az összes olyan metódust, amelyik meg tudja változtatni az objektum tartalmát, általában ki is terjeszti az immutátor interfészt, amelyik viszont csak olyan metódusokat deklarál, amelyek lekérdezésre, és még véletlenül sem objektum állapot változtatásra valók. Amikor pedig referenciát ad át egy ilyen objektumra a program egy metódusnak, akkor ha annak nem szabad megváltoztatni az objektum tartalmát, akkor az immutátor interfész típussal adja át, így (hacsak nagyon nem erőlködik) nem fog belepiszkítani a mi tiszta objektumunkba. Legfeljebb a vízből veszi ki a zoxigént.

Na de eléggé mélyen beástam magam ebbe a gondolati verembe, ki kellene poppolni innen, és mint a kismalac, nem várom meg a létrát, inkább dobok egy ellenőrizetlen kivételt, ami majd jól kivesz a veremből. Vissza a finálra.

Lokális változó is lehet final, objektum instancia változó is lehet final, és statikus változó is lehet finál. Most ne beszéljünk arról, hogy osztály is lehet final.

Lokális változók esetében főleg programozási szépség. Ez persze szubjektív. Viszont ha a metóduson belül létrehozzuk egy név nélküli osztály egy példányát, amelyikben hivatkozunk a metódusunk egy változójára, akkor annak a változónak final-kell lennie, deklaráció szempontjából is, és értéket is kellett kapnia, amikor az objektumot létrehozzuk. (Ez is megért egy misét.) Viszont a lokális változók a veremben vannak, minden szálnak saját verme van, egymás vermébe nem nyúlkálnak, ez az etika, mint a piszoárban. Ezért aztán a többszálúságot a lokális változók nem zavarják. Final, vagy fin ül, mindegy.

Nem így van azonban az osztály és az objektum változókkal! Ezeket, legyenek privát, protected, public vagy egyéb láthatóságúak (ismétlés: mi lehet még?) több szál is láthatja. Ha egy objektum változó nem final (és nem is volatile) értéket kap, akkor a lefordult kódnak szíve joga, hogy ezt az értéket berakja egy processzor regiszterbe, L1,2,3 cache-be és ne írja ki memóriába, amíg a szál azon a processzor(mag)on fut. Ő tudja, hogy mi a változó értéke, és nem jeleztük, hogy azon a szálon kívül mást is érdekelne, így akár amíg világ a világ és véget nem ér a JVM futása, akár sose nem is kerül ki az érték a memóriába, ahol egyébként az objektum van. Ha viszont az objektumváltozó final, akkor értéket kell, hogy kapjon, amikorra a konstruktor lefut, és ekkor kikerül memóriába is (vagy ha oda nem is, ez már processzor hardver kérdése, de annyi biztos, hogy a többi szál is a beállított értéket fogja látni).

A final objektumváltozó egy olyan “volatile” objektumváltozó, amelyik csak egyszer kap értéket a konstruktorban, és legkésőbb a konstruktor lefutásának a végére a többi szál számára is láthatóvá válik az értéke. (Meg ne kövezzetek! Tudom, hogy ez így pongyola megfogalmazás, de ha már kövezésről van szó, tudjátok meg, hogy ti mind egyéniségek vagytok!)

Persze nem, hanem még a volatile mezőknél is hatékonyabb. Azokat ugyanis minden egyes olvasásnál ellenőrizni kell, hogy valaki más nem módosította-e a tartalmukat, a final mezők viszont nem változhatnak, így eszébe sincs a kódnak kinézegetni a lassú memóriába, ha egy final változó értéke már bent van a processzor cache-ben.

A Java specifikáció külön meg is említi, hogy például a String minden mezője final, ezért, amikor létrejön egy új String objektum, akkor egy másik szálon (az egyik amelyik létrehozta, ehhez képest egy másikon) is használható az objektum, mert minden egyes mezője final tehát a másik szál is azt az értéket látja, amit a konstruktor létrehozott. (String témában javaslom ezt a bejegyzést először utána pedig ezt a bejegyzést, amely két bejegyzés igen mélyen belevájkál a String lelkivilágába). És hogy most már sokadik hete ne maradjunk kód nélkül, itt van egy mit ír ki:

import java.util.concurrent.atomic.AtomicInteger;

public class FinalTest {

	private static AtomicInteger counter = new AtomicInteger(0);

	static void waitFor(int n) {
		while (!counter.compareAndSet(n, n + 1)) {
		}
	}

	private static class C {
		final int x;
		int y;

		C(T other) {
			super();
			other.c = this;
			waitFor(0);
			waitFor(3);
			x = 1;
			y = 1;
			waitFor(4);
		}
	}

	private static class T extends Thread {
		public boolean constructor = false;
		public T other = null;
		public C c;

		@Override
		public void run() {
			if (constructor) {
				c = new C(other);
			} else {
				waitFor(1);
				System.out.println("x=" + c.x);
				System.out.println("y=" + c.y);
				waitFor(2);
				waitFor(5);
				System.out.println("x=" + c.x);
				System.out.println("y=" + c.y);
			}
		}
	}

	public static void main(String[] args) {
		T t = new T();
		t.constructor = true;
		t.other = new T();
		t.start();
		t.other.start();
		try {
			t.join();
			t.other.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	};
}

részletesebb magyarázat

Nos, mint többszálúság esetében ez szokott lenni, nem elegendő lefuttatni a programot, és megnézni, hogy mit ír ki. Mert amit kiír, az egy dolog, egy futáskor, egy gépen. Máskor, másik gépen esetleg mást ír ki. Az a tudomány, olyan programot írni, amelyik többszálú, de mégis garantálhatjuk, hogy minden gépen mindig ugyanazt produkálja. Ez a program nem ilyen, és ez lényeg benne. Amit ez kiír, az vagy

x=0
y=0
x=1
y=1

vagy éppen

x=0
y=0
x=1
y=0

Akinek a másodikat sikerül reprodukálnia, írja meg. Elvileg nem kizárt, és nagy terhelés esetén, amikor legkevésbé kellene, akkor fog megtörténni. No nem ez a kód, mert ez a kód, mint tapasztaltabbaknak egyből lejöhetett: nem production kód. Rövid, egybetűs, ugyan könnyen átlátható, de semmitmondó változónevek, e.printStackTrace és társai. Ami azonban a legfontosabb, hogy ez a kód direkt arra készült, hogy demonstrálja (sajnos a dolog természetéből adódóan kevés sikerrel), hogy mi az amit nem szabad. A javasolt eljárás ennek az ellentéte. A konstruktorokat úgy kell megírni, hogy azok ne adják ki a this értékét magukból, így az objektumhoz más szálból csak akkor lehessen hozzáférni, amikor a konstruktor már lefutott, és minden final mező értéke be lett állítva. Miért is tennénk mást? Mert észre sem vesszük. Készítünk egy factory-t, amelyik minden létrehozott objektumnak átadja magát, így minden objektumtól el lehet kérni, hogy melyik factory hozta létre. Aztán ahogy fejlődik a kód egyes osztályok még néhány dolgot be akarnak jegyezni magukról a factory-ban, a konstruktornak átadott factory objektumba visszahínak átadva paraméterként a this-t, és már itt is az antipattern. De azért működik a dolog, amíg egyszer egy távoli ügyfél malájföldön többszálú környezetben el nem kezdi használni, és Murfy-nek megfelelően crash. És ilyenkor tör elő a szupportos programozó szájából a másik Brian élete idézet: “Menjetek a …”

Vagyis a final egy rafinalt dolog, és nem csak szépészet!

Házi feladat: Gondold át, hogy mi a helyzet a static final mezőkkel. Gondolj arra, hogy az osztályok maguk is objektumok.

UPDATE

Kofa felhívta a figyelmemet, hogy a waitFor metódusban a meghívott compareAndSet metódus változtat egy volatile változót, aminek a hatására az y változó megváltozott értéke is garantáltan elérhetővé válik az összes szál számára. Így ez a példaprogram nem jó, soha nem fordulhat elő a

x=0
y=0
x=1
y=0

eredmény.

Tudásteszt, multi thread 1

Van egy osztály

class Test {
           static volatile int i = 0, j = 0;
           static void one() { i++; j++; }
           static void two() {
               System.out.println("i=" + i + " j=" + j);
           }
}

amelynek one nevű metódusát egy szál hívogatja, maximum Integer.MAX_VALUE esetben. Azért ez nem kevés. Egy másik szál pedig a two nevű metódust hívogatja. A kérdés az, hogy a kiíratás során milyen lehetőségek fordulhatnak elő. Több választ is meg lehet jelölni, de csak azért, hogy elbizonytalanítsalak. Lehet, hogy csak egy válasz a jó?

Extra kérdés bónuszpontért: honnan vettem a fenti mintapéldát?

UPDATE 2013. január 4 7:12

Nem gondoltam volna, hogy ilyen hamar megérkezik a teljesen korrekt válasz:


Kofa február 3, 2013 – 11:17 du. (Szerkesztés)

Szerintem mind igaz.
Az olvasó szál futhat az mindkét increment futása után, ekkor egyenlőek.
Futhat i++ után, j++ előtt, ekkor i>j.
Végül előfordulhat, hogy i-t kiolvasta, felfüggesztődik, párszor megfut a növelés, és az ezután kiolvasott j értéke magasabb lesz, mint a korábban kiolvasott j.

A tippem az volt, hogy a JLS-ből vetted, és ezt azóta ellenőriztem is. 🙂

Nem azért nem gondoltam, mert hülyének gondolom az olvasóimat, hanem mert hétvége volt. Őszintén: kellemes meglepetés. Persze Kofa-t ki kellett volna zárnom a megoldók köréből, kollégám, és naná, hogy tudja: ő tartja nálunk a Java multi-thread tréninget.

És akkor még a motivációimról, hogy miért raktam ki ezt a postot: Nekem nagy gyakorlatom van multi-thread-ben (gondoltam én gyarló módon). A puding próbája, hogy megeszik, és azért az általam írt index hirdetési szerver kilenc évig futott éles környezetben, márpedig az egy darab C-ben megírt http szerver hirdetési logikával hard core multi-thread.

Ha megnézzük néhány válasz stílusát látszik, egy-egy rossz válasszal együtt, akkor azt láthatjuk, hogy mások is hasonló cipőben járnak. Nincs ezzel semmi baj, mind a négy állítás igaz, leginkább az utolsó: a többszálú programozás mágia. (És akkor még nem is tudtam, hogy mekkora pofonba szaladok bele a következő poszttal, amire megint csak Kofa hívta fel a figyelmemet.) De vissza tértve: azért raktam ki ezt a posztot, mert azt gondoltam, hogy nekem nagy gyakorlatom van többszálú programok írásában, nemrég Kofa tréningjén át is ismételtem, megismertem, hogy a Java-nak milyen specialitásai vannak ezen a téren. Meg volt a múlt héten a listán a final használatáról egy kis burst, ami egyrészt szintaktikus cukorka, másrészt viszont pont az MT esetében van jelentősége. Miközben megírtam a postot amit szerdán szándékozom kitenni, átnéztem, hogy mit ír a JLS. Ott találtam ezt a kódot, és következő részt benne:

Therefore, the shared value for j is never greater than that for i, because each update to i must be reflected in the shared value for i before the update to j occurs. It is possible, however, that any given invocation of method two might observe a value for j that is much greater than the value observed for i, because method one might be executed many times between the moment when method two fetches the value of i and the moment when method two fetches the value of j.

Nem értettem, és el is felejtettem olvasás közben, hogy egy oldallal előbb ott szerepelt, hogy kettő szál fut csak, és arra gondoltam, hogy ha rengeteg szál hívja a one() metódust, és mind szuszpendálódik az i növelése után, akkor az i lehet sokkal nagyobb, mint a j, vagyis hibás a specifikáció. Ami a legszégyenteljesebb, még meg is írtam az ORACLE-nek. Meglepő módon még aznap egy teljesen korrekt választ kaptam. Tanulságul legyen itt a levéltitok megsértésével a magánlevelezésünk:

Subject: Java7 spec documentation error, minor
From: Peter Verhas
To: javasedocs_us@oracle.com

http://docs.oracle.com/javase/specs/jls/se7/jls7.pdf

page 210

text reads:

“Therefore, the shared value for j is never greater than that for i,
because each update to i must be reflected in the shared value for i
before the update to j occurs. It is possible, however, that any given
invocation of method two might observe a value for j that is much
greater than the value observed for i, ”

I suspect the correct sentence would be:

“Therefore, the shared value for j is never greater than that for i,
because each update to i must be reflected in the shared value for i
before the update to j occurs. It is possible, however, that any given
invocation of method two might observe a value for i that is much
greater than the value observed for j, ”

Seems that the two variables are mixed in the second part of the
sentence. Even though this is a minor mistake it makes understanding harder.


Peter Verhas

és a válasz kevesebb, mint 12 órával későbbről:

Hi Peter,

Good question, but the text is correct. We can agree that method “one” will always observe values of i and j for which i>=j holds. Method “two” observes the value of i (which is definitely >=j at the time of observation) and appends it to a string … then execution switches to method “one” for a week. When method “two” resumes, the values of i and j will have increased considerably. The invariant i>=j still holds, but method “two” observed i last week – its value was appended to a string! Method “two” now observes j, and of course j is considerably larger than either i or j a week earlier.

Alex

1. Jó pap holtig tanul.

2. Faith in humanity (ORACLE) restored.