Säikeen äkkikuolema/ debuggaus / NetBeans
tulipas sitten debugatessa tosiyllätys vastaan.
kyseessä java GUI -ohjelma, Netbeansissa tehty,käyttää swing -komponentteja.
alla koodistani rutiini, joka toteuttaa määrittelemäni TTimerEvent -interfacen:
Olen siis kirjoittanut interfacen TTimerEvent sekä TTimer -luokan, jotka yhdessä tarjoavat vastaavan ajastintoiminnallisuuden kuin Delphin TTimer.
Erona on se, että kun Delphissä OnTimer on "procedure of object" niin Javassa se on Interface.
ja tuo allaoleva
public void TimerXTimer(TTimer Sender)
on tällaisen luokan metodi:
public class TestApp1UI extends javax.swing.JFrame implements TTimerEvent
--------------------------------
private static int TC = 0;
public void TimerXTimer(TTimer Sender) {
//
try {
// a:
TC ;
lblTimerCount.setText("Timer=" TC);
TGetUrlContentItem Item;
Item = DownLoadManager.getDownloadedItem();
if ( Item != null ) {
String St1 = Item.urlContent.toString();
memLog.append(St1);
};
} catch (Exception E) {
// b:
FLastPoikkeus = E.getMessage();
lblPoikkeus.setText(FLastPoikkeus);
}
}
--------------------------------
ja nyt sitten ongelmaan:
tuo rutiini liipaistuu useita kertoja ongelmitta (niin kauan kun DownLoadManager.getDownloadedItem() palauttaa null).
rutiini liipaistuu ongelmitta myös, kun tuo getDownloadedItem() palauttaa 1.kerran jotain muuta kuin null. Tälläkään rutiinin suorituskerralla ei ilmene mitään ongelmia.
Mutta seuraavalla TimerXTimer -rutiinin suorituskerralla tapahtuukin jotain todella merkillistä:
1.
TC ;
(tämä kasvattaa TC:tä 1:llä aivan normaalisti)
lblTimerCount.setText("Timer=" TC);
(Tämäkään ei aiheuta poikkeusta, eikä muutakaan selvää virhetilannetta, mutta 1.kertaa päivitys EI vaikuta näytölle mitään, aikaisemmilla suorituskerroilla siis toimii kuten suunniteltu.)
mutta virheet senkun vaan pahenevat:
2.
tämä lause:
Item = DownLoadManager.getDownloadedItem();
aiheuttaa Timer -säikeen "pikakuoleman" !
Ei aiheuta poikkeusta, debuggerilal ei voi "astua sisään" tähän getDownloadedItem() -metodiin.
Eli jos yrittää, niin yhtäkkiä tilarivillä lukeekin "user program running", ja kaikki näyttää toimivan normaalisti, paitsi että tuo näytön sekuntilaskuri (lblTimerCount) ei enää päivity.
Ilmeisesti TimerXTimer -rutiini EI enää liipaistu.
ennen tuota säikeen "pikakuoleman" aiheuttavaa getDownloadedItem() -metodia DownLoadManager ei myöskään ole null.
Ja 1.kerralla kun tuo getDownloadedItem() -metodi palauttaa muuta kuin "null" sekä kaikilla edeltävillä kerroilla kun se palauttaa null, mitään ongelmia ei ole.
Niin, jos jäi epäselväksi, niin tässä on siis tilanne, jossa pääsäikeestä (joka hoitaa GUI -käyttöliittymää) käsin käynnistetään taustalla toimiva http get -operaatio, ja koska tuohan voiu kestää kauankin jos palvelin on ruuhkainen tai jos nettiyhteydessä on vikaa, niin siksi tuo http get hoidetaan apusäikeessä, ja tuo getDownloadedItem() siis palauttaa muuten aina null, paitsi jos on saatu jotain imuroiduksi, niin se palauttaa viittauksen TGetUrlContentItem -luokkaan.
tässä luokassa ei ole metodeja, vaan se toimii säilönä datalle, määrittely tässä:
public class TGetUrlContentItem {
/** Creates a new instance of TGetUrlContentItem */
public TGetUrlContentItem() {
}
//
public String Name;
public String url;
public StringBuffer urlContent;
public byte Status = 0;
public String ErrorMsg;
}
eli pääsäie luo tuollaisen olion ja täyttää sen url -kentän. Seuraavaksi tuo TGetUrlContentItem -olio laitetaan työjonoon. Apusäie ottaa noita työjonosta, hakee pyydetyn urlin sisällön ja laittaa http getillä imutetun datan TGetUrlContentItem -olion urlContent -kenttään (ennen laittamista apusäie tekee näin:
if ( Item.urlContent == null ) {
Item.urlContent = new StringBuffer();
};
)
lopuksi tuo TGetUrlContentItem -olio laitetaan valmiiden noutojen jonoon.
tuo getDownloadedItem() siis palauttaa tuon valmiiden noutojen jonon 1. alkion (ja poistaa sen jonosta) tai palauttaa null, jos valmiiden noutojen jono on tyhjä.
Miten ongelmaa voisi selvittää, kun siis poikkeuksia ei aiheudu ja debuggerilla ei ongelmavaiheessa pääse tuon metodin sisään ?
tulipas sitten debugatessa tosiyllätys vastaan.
kyseessä java GUI -ohjelma, Netbeansissa tehty,käyttää swing -komponentteja.
alla koodistani rutiini, joka toteuttaa määrittelemäni TTimerEvent -interfacen:
Olen siis kirjoittanut interfacen TTimerEvent sekä TTimer -luokan, jotka yhdessä tarjoavat vastaavan ajastintoiminnallisuuden kuin Delphin TTimer.
Erona on se, että kun Delphissä OnTimer on "procedure of object" niin Javassa se on Interface.
ja tuo allaoleva
public void TimerXTimer(TTimer Sender)
on tällaisen luokan metodi:
public class TestApp1UI extends javax.swing.JFrame implements TTimerEvent
--------------------------------
private static int TC = 0;
public void TimerXTimer(TTimer Sender) {
//
try {
// a:
TC ;
lblTimerCount.setText("Timer=" TC);
TGetUrlContentItem Item;
Item = DownLoadManager.getDownloadedItem();
if ( Item != null ) {
String St1 = Item.urlContent.toString();
memLog.append(St1);
};
} catch (Exception E) {
// b:
FLastPoikkeus = E.getMessage();
lblPoikkeus.setText(FLastPoikkeus);
}
}
--------------------------------
ja nyt sitten ongelmaan:
tuo rutiini liipaistuu useita kertoja ongelmitta (niin kauan kun DownLoadManager.getDownloadedItem() palauttaa null).
rutiini liipaistuu ongelmitta myös, kun tuo getDownloadedItem() palauttaa 1.kerran jotain muuta kuin null. Tälläkään rutiinin suorituskerralla ei ilmene mitään ongelmia.
Mutta seuraavalla TimerXTimer -rutiinin suorituskerralla tapahtuukin jotain todella merkillistä:
1.
TC ;
(tämä kasvattaa TC:tä 1:llä aivan normaalisti)
lblTimerCount.setText("Timer=" TC);
(Tämäkään ei aiheuta poikkeusta, eikä muutakaan selvää virhetilannetta, mutta 1.kertaa päivitys EI vaikuta näytölle mitään, aikaisemmilla suorituskerroilla siis toimii kuten suunniteltu.)
mutta virheet senkun vaan pahenevat:
2.
tämä lause:
Item = DownLoadManager.getDownloadedItem();
aiheuttaa Timer -säikeen "pikakuoleman" !
Ei aiheuta poikkeusta, debuggerilal ei voi "astua sisään" tähän getDownloadedItem() -metodiin.
Eli jos yrittää, niin yhtäkkiä tilarivillä lukeekin "user program running", ja kaikki näyttää toimivan normaalisti, paitsi että tuo näytön sekuntilaskuri (lblTimerCount) ei enää päivity.
Ilmeisesti TimerXTimer -rutiini EI enää liipaistu.
ennen tuota säikeen "pikakuoleman" aiheuttavaa getDownloadedItem() -metodia DownLoadManager ei myöskään ole null.
Ja 1.kerralla kun tuo getDownloadedItem() -metodi palauttaa muuta kuin "null" sekä kaikilla edeltävillä kerroilla kun se palauttaa null, mitään ongelmia ei ole.
Niin, jos jäi epäselväksi, niin tässä on siis tilanne, jossa pääsäikeestä (joka hoitaa GUI -käyttöliittymää) käsin käynnistetään taustalla toimiva http get -operaatio, ja koska tuohan voiu kestää kauankin jos palvelin on ruuhkainen tai jos nettiyhteydessä on vikaa, niin siksi tuo http get hoidetaan apusäikeessä, ja tuo getDownloadedItem() siis palauttaa muuten aina null, paitsi jos on saatu jotain imuroiduksi, niin se palauttaa viittauksen TGetUrlContentItem -luokkaan.
tässä luokassa ei ole metodeja, vaan se toimii säilönä datalle, määrittely tässä:
public class TGetUrlContentItem {
/** Creates a new instance of TGetUrlContentItem */
public TGetUrlContentItem() {
}
//
public String Name;
public String url;
public StringBuffer urlContent;
public byte Status = 0;
public String ErrorMsg;
}
eli pääsäie luo tuollaisen olion ja täyttää sen url -kentän. Seuraavaksi tuo TGetUrlContentItem -olio laitetaan työjonoon. Apusäie ottaa noita työjonosta, hakee pyydetyn urlin sisällön ja laittaa http getillä imutetun datan TGetUrlContentItem -olion urlContent -kenttään (ennen laittamista apusäie tekee näin:
if ( Item.urlContent == null ) {
Item.urlContent = new StringBuffer();
};
)
lopuksi tuo TGetUrlContentItem -olio laitetaan valmiiden noutojen jonoon.
tuo getDownloadedItem() siis palauttaa tuon valmiiden noutojen jonon 1. alkion (ja poistaa sen jonosta) tai palauttaa null, jos valmiiden noutojen jono on tyhjä.
Miten ongelmaa voisi selvittää, kun siis poikkeuksia ei aiheudu ja debuggerilla ei ongelmavaiheessa pääse tuon metodin sisään ?
Vastaukset 5
Vanhimmat
Ratkaisu löytyi (tavallaan)...
Sääntö #1 java -ohjelmoinnissa:
Älä ikinä kutsu sleep(xxx) synkronoidun metodin sisältä ! Moinen kutsu on varma tie ongelmiin.
Mutta tämä asia EI selvinnyt debuggerin avulla, vaan ohjelman rakennetta tarkkaan miettimällä.
Korjasin ohjelmastani tuon virheen, ja nyt se toimii.
Mutta: entä, jos en olisi itse oivaltanut vian syytä ja näinollen myös tapaa ratkaista se?
Esim. Delphissä debuggerilla pääsee myös CPU -tasolle ja ohjelmaa voi askeltaa 1 konekielikäsky kerrallaan.
Mutta Java, kun sitä ei yleensä käännetä konekieleksi, vaan Java -tavukoodiksi, niin esim. NetBeansin debuggeri on tiukoissa paikoissa aika avuton.
Eikö Javassa todella ole olemassa tehokkaita debuggereita, joilla näkee mikä on vialla, eikä niin, että ohjelma vain "mystisesti" kieltäytyy askeltamasta johonkin metodiin?
Ilmeisesti tämä ongelma on mahdollinen vain synrkonoitujen metodien kanssa.
ironista sinänsä, että toisessa ohjelmassani (joka käyttää samaa luokkaa tämän kanssa) minulla oli muita ongelmia wait() ja notifyAll() -metodien kanssa, joten korvasin NotifyAll -kutsun pelkällä kommentilla ja wait() -kutsun tällä:
sleep(500);
Muuten ihan toimiva tapa yrittää saada ohjelma toimimaan jos nuo wait() ja notifyAll() tuottavat pulmia, mutta tässä on tosiaan huomattva se, ettei sleep() -metodia pidä koskaan kutsua synkronoidun metodin sisältä.
Vinkkejä pätevämmistä Java -debuggereista saa silti laittaa !
> wait() ja notifyAll() -metodien kanssa, joten
> korvasin NotifyAll -kutsun pelkällä kommentilla ja
> wait() -kutsun tällä:
> sleep(500);
Kai tiesit, että wait- ja sleep-metodeilla on aika iso ero, eli et voi korvata toista toisella muuttamatta ohjelmasi toimintaa merkittävästi.
Wait-metodi siis odottaa jonkin monitorin vapautumista, vaikka maailman tappiin asti, jos niikseen tulee, sleep jatkaa säikeen suoritusta ajan umpeuduttua. Säikeiden synkronoinnissa sleep-metodilla ei saa aikaan kuin omituisia, vaikeasti debugattavia ongelmia, oikea keino on synkronoidut metodit, wait- ja notify-metodien käyttö. Tai sitten säikeistä luopuminen.
NetBeansin debuggerista en tiedä mitään, mutta Eclipsellä debuggaus onnistuu ainakin oikein hyvin.
"Kai tiesit, että wait- ja sleep-metodeilla on aika iso" - milloin on, milloin ei.
esimerkki:
while ( !okToGo ) {
// poista kommenttimerkki jostain seuraavasta:
// 1. sleep:
// sleep(500);
// 2. wait:
synchronized {
wait();
}
}
goThere();
ja jokin muu säie sitten näin:
1. jos käytetään sleep():
okToGo = true; // muuta ei tässä tapauksessa tarvita
2. jos käytetään wait():
okToGo = true;
synchronized {
notifyAll();
}
korjattuani tuon oman ohjelmani virheen, nyt se toimii ongelmitta (ja käyttää säikeitä ja sleep() -metodia).
Teoriassahan tuo toimii ilman tuota sleepiäkin. Mutta moinen ohjelmointi osoittaa erittäin huonoa ohjelmointitapaa, koska jos tuossa ei ole wait eikä sleep -kutsua, niin tuon okToGo -arvon muuttumisen odottelu tuhlaa 100% CPU -ajasta (ja jos säikeen prioriteetti on liian korkea, voi estää muiden säikeiden etenemisen).
Sensijaan, jos siellä on se sleep, mitä tapahtuu:
sleep odottaa 500 ms, silmukka pyörii kerran (ehkä 1-2 mikrosekuntia) ja taas odotetaan 500 ms. Koska sleep(500) EI syö CPU -aikaa, niin jokaista 500 ms kohden kuluu ehkä 1-2 mikrosekuntia CPU -aikaa, mitä voidaan pitää käytännössä merkityksettömänä. Ainoana poikkeuksena tilanne, jossa tuollaisia odotussäikeitä olisi kymmeniä tuhansia, ehkä sitten alkaisi näkyä jossain...
Javassa on outoa sekin, että miksi notifyAll -kutsun tekemiseksi pitää olla tuossa synchronized -tilassa ? - silloinhan esim. jos pääsäikeen (joka käsittelee GUI -liittymää) pitää ilmoittaa toiselle säikeelle jotain, sen on pakko ensin mennä synchronized -tilaan. Normaalitapauksessa ok, mutta jos jokin säie onnistuisi juuttumaan ollessaa synchronized -tilassa, niin sehän jumittaisi pääsäikeen myös.
Tuossa alkuperäisessä tilanteessahan ongelmana oli siis se, että sleep() --kutsuttiin synchronized -metodin sisältä, ja tämä tosiaan ON varma tie ongelmiin.
Mutta jo ennen ketjun aloitusartikkelin kirjoittamista ohjelmassa oli muita ongelmia, joiden takia muutin tuon wait() -kutsun sleepiksi.
Tuollainen muutos toimiikin, mutta vain jos se tehdään oikein, eli sleep() EI saa koskaan kutsua synchronized -tilan sisältä.
Mutta jos Eclipsen debuggeri on parempi, kokeilepa sillä tätä:
Tee tahallaan virhe, että kutsut sleep() synchronized -tilan sisältä. Yritä nyt debugata ohjelmaasi.
Saako Eclipsen debuggerilla aikaiseksi mitään järkevää kun yrität toisesta säikeestä sisään sellaisen luokan synchronized -metodiin, että samaan aikaan toinen säie ON jo ko. luokan synchronized -metodissa ja on esim. kutsunut siellä sleep(5000);
Tällainen on tietenkin ohjelmointivirhe, mutta eikö debuggerin pitäisi juuri helpottaa ohjelmointivirheiden löytämistä?
Jos debuggeri vain antaa ohjelmansuorituksen jatkua normaalisti kertomatta, mistä on kysymys, niin eikö tuo ole huonoa debuggerisuunnittelua ?
Hyvä debuggeri voisi vaikka antaa ilmoituslaatikon "yrität siirtyä synchronized -tilaan, mutta luokan X olio Y on lukittu monitorilla. jatketaanko muiden säikeiden suoritusta ja palataan debuggeriin heti, kun tämä säie saa lukittua luokan X olion Y ?"
En tiedä, mitä tuossa (ohjelmointivirheen sisältävässä) ohjelmassa tapahtui, mutta debuggeri ei pysäyttänyt (step into -toiminnolla yritettäessä synchronized -metodiin sisään) ohjelmaa sittenkään, kun toinen säie vapautti lukon !
tuota, voisin suositella sulle jotain rinnakkaisohjelmointikurssia/-kirjaa, sulla ei vissiin ole ihan selvillä, miten säikeitä käytetään. Lue ensi alkuun vaikka tämä: http://java.sun.com/docs/books/tutorial/essential/concurrency/index.html
Ensinnäkin sun esimerkki, oikea tapa menisi suunnilleen näin:
odotus:
synchronized(this) {
__while (!okToGo) {
____wait();
__}
}
goThere();
ja käyttö:
synchronized(this) {
__okToGo = true;
__notifyAll();
}
eli wait-metodi luovuttaa omistamansa monitorin ja jää odottamaan, kunnes jokin toinen säie kutsuu saman monitorin notify- tai notifyAll-metodia, joiden tarkoituksena on siis ilmoittaa muille monitoria odottaville säikeille, että monitori aiotaan vapauttaa. Eli mitään busy-looppia ei tarvita, eikä myöskään sleep-metodia hillitsemään loopin prosessorikulutusta. Ja notifyAll-metodia pitää kutsua nimenomaan synchronized-blokista, koska säikeen pitää omistaa monitori, jonka se aikoo vapauttaa.
Sleepin kutsuminen synchronized-blokin sisällä on täysin sallittua, mutta se tarkoittaa sitä, että kyseinen monitori on varattu ja mikään muu säie ei pääse monitorin suojaamiin osiin koodia.
Eclipsen debuggerille on mahdollista sanoa, pysäytetäänkö koko jvm vai pelkästään yksittäinen säie breakpointin kohdalla. Yleensä halutaan, että pysäytetään koko jvm, jottei muut säikeet pääse muuttamaan ohjelman tilaa, mutta erikoistapauksissa voi olla tarvetta yksittäisten säikeiden pysäyttämiselle.
Edellinen kirjoittaja teki erinomaisia huomautuksia.
Lisäksi mainittakoon, että:
1. Pääsäie ei hoida käyttöliittymää.
2. Käyttöliittymätapahtumat hoidetaan omassa säikeessä, joka käynnistetään automaattisesti pääsäikeen lisäksi.
3. Swing ei ole säieturvallinen eli ohjelmoijan tulee varmistaa, että käyttöliittymää muuttava koodi suoritetaan käyttöliittymätapahtumia hoitavassa säikeessä.
Jos muutat Swing-komponentin tilaa tapahtumankäsittelijässä (esim. actionPerformed), se suoritetaan käyttöliittymätapahtumia hoitavassa säikeessä ja toimii ongelmitta.
Mutta jos yrität muutat Swing-komponentin tilaa jostain muusta säikeestä käsin, se ei onnistu. Sinun pitää varmistaa, että muutokset suoritetaan oikeassa säikeessä. Se tehdään näin:
java.awt.EventQueue.invokeLater(new Runnable() {
..public void run() {
....someLabel.setText("Some text");
..}
});
Luettavaa:
http://java.sun.com/products/jfc/tsc/articles/threads/threads1.html
http://java.sun.com/products/jfc/tsc/articles/threads/threads2.html
http://java.sun.com/products/jfc/tsc/articles/threads/threads3.html