Java programátoři jsou zvyklí, že jsou odstíněni od operačního systému na kterém jejich aplikace běží. Toto odstínění není ale stoprocentní. Jednou z vyjímek jsou operace se souborovým systémem. Chtěl bych se s vámi poděli o poznatky, které jsem získal při psaní projektu m2-proxy. Je to jednoduchý program, který jsem si napsal ve volném čase, abych se odreagoval od svého zaměstnání – práci která se programování bohužel jen vzdáleně podobá. m2-proxy je v jednoduchosti program, který umí lokálně cachovat vzdálenou repository pro Maven (pokud jste používali maven-proxy, tak je to něco podobného, určeného pro Maven 2). Jde o web aplikaci, která se chová následovně:
- Přijde požadavek. Podívám se jestli už mám požadovaný zdroj lokálně uložený
- Pokud ano a nevypršela jeho platnost, vrátím ho
- V opačném případě ho stáhnu z internetu, uložím si ho a vrátím
V zjednodušené podobě může kód vypadat následovně (bez kontroly platnosti souboru a zpracování vyjímek, také předstíráme, že umíme pracovat jenom s jedním zdrojem – souborem):
<font size="2"><font style="color: #808080">01</font> <strong><font style="color: #7f0055">public </font></strong>
<font style="color: #000000">InputStream downloadFile() </font>
<strong><font style="color: #7f0055">throws </font></strong>
<font style="color: #000000">IOException</font>
<font style="color: #808080">02</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">03</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//java.io.File store = ... file on the disc</font>
<font style="color: #808080">04</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//file can not be read, probably was not stored yet</font>
<font style="color: #808080">05</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">if </font></strong>
<font style="color: #000000">(!store.canRead())</font>
<font style="color: #808080">06</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">07</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//get the source from the internet</font>
<font style="color: #808080">08</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">InputStream source1 = getSourceInputStream();</font>
<font style="color: #808080">09</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//store it</font>
<font style="color: #808080">10</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileOutputStream(store);</font>
<font style="color: #808080">11</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">copyStreams(source1, out1);</font>
<font style="color: #808080">12</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//closing should be in the finally block</font>
<font style="color: #808080">13</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">source1.close();</font>
<font style="color: #808080">14</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1.close();</font>
<font style="color: #808080">15</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font>
<font style="color: #808080">16</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">if </font></strong>
<font style="color: #000000">(store.canRead())</font>
<font style="color: #808080">17</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">18</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//return stream from the disc</font>
<font style="color: #808080">19</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">return new </font></strong>
<font style="color: #000000">FileInputStream(store);</font>
<font style="color: #808080">20</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font>
<font style="color: #808080">21</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">else</font></strong>
<font style="color: #808080">22</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">23</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//safety net, returning source from the internet</font>
<font style="color: #808080">24</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">return </font></strong>
<font style="color: #000000">getSourceInputStream();</font>
<font style="color: #808080">25</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font>
<font style="color: #808080">26</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font></font>
05 – store je typu java.io.File, zastupuje lokálně uloženou verzi zdroje. Zjišťujeme, je-li zdroj už na disku a můžeme ho číst 08 – Pokud zdroj není na disku, získáme zdrojový InputStream z internetu 11 – Zkopírujeme zdroj na disk 16 – Zjistíme je-li zdroj na disku 19 – Pokud ano vrátím ho 24 – Pokud se něco pokazilo, vrátím originál z internetu
Na první pohled je vše vyřešeno, zvídavějšího čtenáře napadne, co se stane, když ten samý soubor chce ve stejný okamžik více žadatelů. Může nastat několik zajímavých situací, z nichž některé si rozebereme. Ukážeme si zdrojové kódy JUnit testů, které nám pomohou ověřit, je-li náš návrh správný.
Současné čtení a zápis
Zamysleme se nad tím, co se stane, když jedno vlákno zapisuje data do souboru a druhé ho v ten samý okamžik chce číst. Tzn. když nastane něco takového:
<font style="color: #808080">01</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">public void </font></strong>
<font style="color: #000000">testAfterSomethingWritten() </font>
<strong><font style="color: #7f0055">throws </font></strong>
<font style="color: #000000">IOException</font>
<font style="color: #808080">02</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">03</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//thread 1</font>
<font style="color: #808080">04</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertFalse(store.canRead());</font>
<font style="color: #808080">05</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//get the source from the internet</font>
<font style="color: #808080">06</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">InputStream source1 = getSourceInputStream();</font>
<font style="color: #808080">07</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileOutputStream(store);</font>
<font style="color: #808080">08</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//store just half of the content</font>
<font style="color: #808080">09</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">copyStreams(LENGTH/</font>
<font style="color: #990000">2</font>
<font style="color: #000000">, source1, out1);</font>
<font style="color: #808080">10</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//thread 2</font>
<font style="color: #808080">11</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertTrue(store.canRead());</font>
<font style="color: #808080">12</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//open local store</font>
<font style="color: #808080">13</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">InputStream in2 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileInputStream(store);</font>
<font style="color: #808080">14</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//compare whether the data in the store are ok</font>
<font style="color: #808080">15</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">int </font></strong>
<font style="color: #000000">bytesRead = compareStreams(in2, getSourceInputStream());</font>
<font style="color: #808080">16</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertEquals(LENGTH/</font>
<font style="color: #990000">2</font>
<font style="color: #000000">, bytesRead);</font>
<font style="color: #808080">17</font> <font style="color: #ffffff"> </font>
<font style="color: #808080">18</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//thread 1 can continue in writing</font>
<font style="color: #808080">19</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">copyStreams(LENGTH-LENGTH/</font>
<font style="color: #990000">2</font>
<font style="color: #000000">, source1, out1);</font>
<font style="color: #808080">20</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font>
Máme tu situaci, kdy jedno vlákno zapsalo polovinu souboru, v tom okamžiku bylo jeho vykonávání přerušeno (řádek 10) a druhé vlákno začne soubor číst. Čtení simulujeme na řádku 15, čteme in2 a porovnáváme ho se zdrojovým proudem. Když narazíme na konec proudu, vrátíme počet načtených bytů. Můžete si tipnout jak předchozí kód dopadne.
a) Dojde k chybě při porovnání (na řádku 11 File.canRead() vrátí false) b) Dojde k vyjímce při vytvoření proudu (řádek 13) c) Dojde k vyjímce při čtení proudu (řádek 15) d) Proběhne bez chyby
Nebudu vás moc napínat, d je správně. Předchozí kód proběhne bez chyby! To ovšem znamená, že námi navrhovaný postup je špatně! K žádnému automatickému vzájemnému vyloučení tu nedochází. Při souběžném běhu více vláken, může jedno vrátit jenom začátek požadovaného souboru. Jak z toho ven? Co takhle zkusit soubor explicitně zamknout.
Zamykání souboru Od verze 1.4 podporuje Java zamykání souborů. Můžeme ho zkusit použít, jenom musíme dát pozor na operační systém, protože tato funkce je silně závislá na souborovém systému. Pokud chci zamknout soubor, zavolám
<font size="2"><font style="color: #000000">FileLock lock1 = out1.getChannel().lock();</font>
<font style="color: #3f7f5f">//aquire exclusive lock</font></font>
čímž soubor zamknu. Pokud se kdokoliv jiný pokusí obdržet na něj zámek stejným způsobem, musí čekat dokud nezavolám lock1.release(). Pokud chci z proudu jen číst, mohu si vyžádat sdílený zámek. Takovýto zámek může držet zároveň více procesů, tzn. více procesů může zároveň číst. Sdílený zámek mohu obdržet například takto
<font size="2"><font style="color: #000000">FileLock readLock = in1.getChannel().tryLock(</font>
<font style="color: #990000">0L</font>
<font style="color: #000000">, Long.MAX_VALUE, </font>
<strong><font style="color: #7f0055">true</font></strong>
<font style="color: #000000">);</font></font>
tryLock se od lock liší jenom tím, že neblokuje, pokud nemůže zámek obdržet. Místo toho se z volání metody vrátí null.
Pozměněný kód pro uložení může vypadat následovně
<font size="2"><font style="color: #808080">01</font> <strong><font style="color: #7f0055">public </font></strong>
<font style="color: #000000">InputStream downloadFile() </font>
<strong><font style="color: #7f0055">throws </font></strong>
<font style="color: #000000">IOException</font>
<font style="color: #808080">02</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">03</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//file can not be read, probably is not on the disc</font>
<font style="color: #808080">04</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">if </font></strong>
<font style="color: #000000">(!store.canRead())</font>
<font style="color: #808080">05</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">06</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">InputStream source1 = getSourceInputStream();</font>
<font style="color: #808080">07</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileOutputStream(store);</font>
<font style="color: #808080">08</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">FileLock lock1 = out1.getChannel().lock();</font>
<font style="color: #3f7f5f">//aquire exclusive lock</font>
<font style="color: #808080">09</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">copyStreams(source1, out1);</font>
<font style="color: #808080">10</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">lock1.release();</font>
<font style="color: #808080">11</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">source1.close();</font>
<font style="color: #808080">12</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1.close();</font>
<font style="color: #808080">13</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font>
<font style="color: #808080">14</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">if </font></strong>
<font style="color: #000000">(store.canRead())</font>
<font style="color: #808080">15</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">16</font> <font style="color: #ffffff"> </font>
<font style="color: #808080">17</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">FileInputStream in1 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileInputStream(store);</font>
<font style="color: #808080">18</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">// try to aquire shared lock</font>
<font style="color: #808080">19</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">FileLock readLock = in1.getChannel().tryLock(</font>
<font style="color: #990000">0L</font>
<font style="color: #000000">, Long.MAX_VALUE, </font>
<strong><font style="color: #7f0055">true</font></strong>
<font style="color: #000000">);</font>
<font style="color: #808080">20</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">if </font></strong>
<font style="color: #000000">(readLock!=</font>
<strong><font style="color: #7f0055">null</font></strong>
<font style="color: #000000">)</font>
<font style="color: #808080">21</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">22</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">return </font></strong>
<font style="color: #000000">in1;</font>
<font style="color: #808080">23</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font>
<font style="color: #808080">24</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font>
<font style="color: #808080">25</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">return </font></strong>
<font style="color: #000000">getSourceInputStream();</font>
<font style="color: #808080">26</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font></font>
Zde jsme se rozhodli, že pokud nemůžeme obdržet zámek pro čtení na řádku 19, vrátíme původní proud z internetu. Kdybychom na řádku 19 použili lock, vykonání by čekalo, dokud se zámek neuvolní.
Pro otestování chování můžeme napsat následující kód
<font size="2"><font style="color: #808080">01</font> <strong><font style="color: #7f0055">public void </font></strong>
<font style="color: #000000">testReadLock() </font>
<strong><font style="color: #7f0055">throws </font></strong>
<font style="color: #000000">IOException</font>
<font style="color: #808080">02</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">03</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//thread 1</font>
<font style="color: #808080">04</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileOutputStream(store);</font>
<font style="color: #808080">05</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">lock1 = out1.getChannel().lock();</font>
<font style="color: #808080">06</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertNotNull(lock1);</font>
<font style="color: #808080">07</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//thread 2</font>
<font style="color: #808080">08</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertTrue(store.canRead());</font>
<font style="color: #808080">09</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">FileInputStream in2 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileInputStream(store);</font>
<font style="color: #808080">10</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">FileLock lock2 = in2.getChannel().tryLock(</font>
<font style="color: #990000">0</font>
<font style="color: #000000">,Long.MAX_VALUE, </font>
<strong><font style="color: #7f0055">true</font></strong>
<font style="color: #000000">);</font>
<font style="color: #808080">11</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertNull(lock2);</font>
<font style="color: #808080">12</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font></font>
První vlákno tu vytvoří soubor a zamkne ho. V tom vstoupí do hry druhé vlákno a začne soubor číst. Podívá se zda může soubor číst, vytvoří vstupní proud a pokusí se obdržet zámek. Vidíte, že na řádku 11 předpokládáme, že se to nepodaří.
Pokud předcházející kód spustíme, bude nám ve Windows (Windows XP Home Edition) opravdu na řádku 10 bude vráceno null. Ale v Linuxu (RadHat s extfs3) klidně zámek získáme a ze souboru můžeme normálně číst! Jsme tedy tam kde jsme byli. Jak to? Co se to děje? K čemu ty zámky tedy jsou? Takto se můžeme ptát, můžeme i nadávat, že je Linux k ničemu, ale chyba je jako obvykle mezi klávesnicí a židlí. Pokud se podíváme do JavaDocu k tříde FileLock dočteme se doslova toto
„File locks are held on behalf of the entire Java virtual machine. They are not suitable for controlling access to a file by multiple threads within the same virtual machine.“
Zamykání souborů tedy také nemůžeme použít (i když na Windows by to fungovalo).
Dočasné soubory
Nakonec jsem se přiklonil k použití dočasných souborů. Při stahování budu data ukládat do dočasného souboru, který po dokončení stahování přesunu na správné místo. Předpokládám, že přejmenování souboru snad proběhne atomicky a že se nikomu nepodaří soubor číst v napůl přejmenovaném stavu (i když v JavaDocu píší, že tomu tak být nemusí!). Kód pro stahování může tedy vypadat následovně
<font size="2"><font style="color: #808080">01</font> <strong><font style="color: #7f0055">public </font></strong>
<font style="color: #000000">InputStream downloadFile() </font>
<strong><font style="color: #7f0055">throws </font></strong>
<font style="color: #000000">IOException</font>
<font style="color: #808080">02</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">03</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//file can not be read, probably is not on the disc</font>
<font style="color: #808080">04</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">if </font></strong>
<font style="color: #000000">(!store.canRead())</font>
<font style="color: #808080">05</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">06</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">InputStream source1 = getSourceInputStream();</font>
<font style="color: #808080">07</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">File tmpFile = File.createTempFile(</font>
<font style="color: #2a00ff">"filetest"</font>
<font style="color: #000000">,</font>
<font style="color: #2a00ff">"tmp"</font>
<font style="color: #000000">);</font>
<font style="color: #808080">08</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileOutputStream(tmpFile);</font>
<font style="color: #808080">09</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">copyStreams(source1, out1);</font>
<font style="color: #808080">10</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">source1.close();</font>
<font style="color: #808080">11</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1.close();</font>
<font style="color: #808080">12</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">tmpFile.renameTo(store);</font>
<font style="color: #808080">13</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font>
<font style="color: #808080">14</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">if </font></strong>
<font style="color: #000000">(store.canRead())</font>
<font style="color: #808080">15</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">16</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">return new </font></strong>
<font style="color: #000000">FileInputStream(store);</font>
<font style="color: #808080">17</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font>
<font style="color: #808080">18</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">else</font></strong>
<font style="color: #808080">19</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">20</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">return </font></strong>
<font style="color: #000000">getSourceInputStream();</font>
<font style="color: #808080">21</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font>
<font style="color: #808080">22</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font></font>
Původní kód jsme rozšířili o 07 – Vytvoření dočasného souboru 12 – Přejmenování souboru
Vypadá to nadějně, zkusme to otestovat. Aby to nebylo tak jednoduché, vložíme na začátku do souboru nějakou hodnotu a tu se budeme snažit přepsat. Nasimulujeme si tak situaci, kdy je stávající soubor nahrazen novou verzí
<font size="2"><font style="color: #808080">01</font> <strong><font style="color: #7f0055">public void </font></strong>
<font style="color: #000000">testOverwrite() </font>
<strong><font style="color: #7f0055">throws </font></strong>
<font style="color: #000000">IOException</font>
<font style="color: #808080">02</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">03</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//let's pretend that there already exists some file</font>
<font style="color: #808080">04</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">FileOutputStream out = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileOutputStream(store);</font>
<font style="color: #808080">05</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">copyStreams(getOldSourceInputStream(), out);</font>
<font style="color: #808080">06</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out.close();</font>
<font style="color: #808080">07</font> <font style="color: #ffffff"> </font>
<font style="color: #808080">08</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//thread 1</font>
<font style="color: #808080">09</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">File tmpFile = File.createTempFile(</font>
<font style="color: #2a00ff">"filetest"</font>
<font style="color: #000000">,</font>
<font style="color: #2a00ff">"tmp"</font>
<font style="color: #000000">);</font>
<font style="color: #808080">10</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileOutputStream(tmpFile);</font>
<font style="color: #808080">11</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">copyStreams(getSourceInputStream(), out1);</font>
<font style="color: #808080">12</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1.close();</font>
<font style="color: #808080">13</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">tmpFile.renameTo(store);</font>
<font style="color: #808080">14</font> <font style="color: #ffffff"> </font>
<font style="color: #808080">15</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertTrue(store.canRead());</font>
<font style="color: #808080">16</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">InputStream in2 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileInputStream(store);</font>
<font style="color: #808080">17</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">int </font></strong>
<font style="color: #000000">bytesRead = compareStreams(in2, getSourceInputStream());</font>
<font style="color: #808080">18</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertEquals(LENGTH, bytesRead);</font>
<font style="color: #808080">19</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">in2.close();</font>
<font style="color: #808080">20</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font></font>
Na Linuxu to běží skvěle, pustíme-li kód na Windows, porovnání proudů na řádku 17 selže. V souboru zůstal starý obsah. Zde se nám vymstilo, že nekontrolujeme návratovou hodnotu metody renameTo. Na Windows nám vrací false, aby naznačila, že se operace nepodařila. Jednoduše nám odmítá přejmenovat soubor, pokud již nějaký se stejným jménem existuje. Existuje jednoduché řešení. Stávající soubor musíme napřed smazat (řádek 13 v následujícím výpise).
<font size="2"><font style="color: #808080">01</font> <strong><font style="color: #7f0055">public void </font></strong>
<font style="color: #000000">testDelete() </font>
<strong><font style="color: #7f0055">throws </font></strong>
<font style="color: #000000">IOException</font>
<font style="color: #808080">02</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">03</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//let's pretend that there already exists some file</font>
<font style="color: #808080">04</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">FileOutputStream out = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileOutputStream(store);</font>
<font style="color: #808080">05</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">copyStreams(getOldSourceInputStream(), out);</font>
<font style="color: #808080">06</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out.close();</font>
<font style="color: #808080">07</font> <font style="color: #ffffff"> </font>
<font style="color: #808080">08</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//thread 1</font>
<font style="color: #808080">09</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">File tmpFile = File.createTempFile(</font>
<font style="color: #2a00ff">"filetest"</font>
<font style="color: #000000">,</font>
<font style="color: #2a00ff">"tmp"</font>
<font style="color: #000000">);</font>
<font style="color: #808080">10</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileOutputStream(tmpFile);</font>
<font style="color: #808080">11</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">copyStreams(getSourceInputStream(), out1);</font>
<font style="color: #808080">12</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1.close();</font>
<font style="color: #808080">13</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">boolean </font></strong>
<font style="color: #000000">deleted = store.delete();</font>
<font style="color: #808080">14</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertTrue(deleted);</font>
<font style="color: #808080">15</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">tmpFile.renameTo(store);</font>
<font style="color: #808080">16</font> <font style="color: #ffffff"> </font>
<font style="color: #808080">17</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertTrue(store.canRead());</font>
<font style="color: #808080">18</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">InputStream in2 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileInputStream(store);</font>
<font style="color: #808080">19</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">int </font></strong>
<font style="color: #000000">bytesRead = compareStreams(in2, getSourceInputStream());</font>
<font style="color: #808080">20</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertEquals(LENGTH, bytesRead);</font>
<font style="color: #808080">21</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">in2.close();</font>
<font style="color: #808080">22</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font></font>
Protože jsem hrozný šťoura, zeptám se vás jestli náhodou nemůže selhat mazání. Kromě podivných případů, kdy je soubor na zamčeném médiu, může nastat situace, že soubor někdo čte v okamžiku kdy se ho snažíme smazat. Nasimulovat si to můžeme následovně
<font size="2"><font style="color: #808080">01</font></font>
<code><font size="2"> <strong><font style="color: #7f0055">public void </font></strong>
<font style="color: #000000">testDeleteWhenReading() </font>
<strong><font style="color: #7f0055">throws </font></strong>
<font style="color: #000000">IOException</font>
<font style="color: #808080">02</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">03</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//let's pretend that there already exists some file</font>
<font style="color: #808080">04</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">FileOutputStream out = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileOutputStream(store);</font>
<font style="color: #808080">05</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">copyStreams(getOldSourceInputStream(), out);</font>
<font style="color: #808080">06</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out.close();</font>
<font style="color: #808080">07</font> <font style="color: #ffffff"> </font>
<font style="color: #808080">08</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//reference source stream</font>
<font style="color: #808080">09</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">InputStream reference1 = getOldSourceInputStream();</font>
<font style="color: #808080">10</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//thread 1 is reading the file</font>
<font style="color: #808080">11</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertTrue(store.canRead());</font>
<font style="color: #808080">12</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">FileInputStream in1 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileInputStream(store);</font>
<font style="color: #808080">13</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//reading and comparing the stream with reference</font>
<font style="color: #808080">14</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">compareStreams(LENGTH/</font>
<font style="color: #990000">2</font>
<font style="color: #000000">, in1, reference1);</font>
<font style="color: #808080">15</font> <font style="color: #ffffff"> </font>
<font style="color: #808080">16</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//thread 2 is storing the file</font>
<font style="color: #808080">17</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">File tmpFile = File.createTempFile(</font>
<font style="color: #2a00ff">"filetest"</font>
<font style="color: #000000">,</font>
<font style="color: #2a00ff">"tmp"</font>
<font style="color: #000000">);</font>
<font style="color: #808080">18</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileOutputStream(tmpFile);</font>
<font style="color: #808080">19</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">copyStreams(getSourceInputStream(), out1);</font>
<font style="color: #808080">20</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">out1.close();</font>
<font style="color: #808080">21</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//let's delete the existing file</font>
<font style="color: #808080">22</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">boolean </font></strong>
<font style="color: #000000">deleted = store.delete();</font>
<font style="color: #808080">23</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">if </font></strong>
<font style="color: #000000">(deleted)</font>
<font style="color: #808080">24</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">25</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">System.out.println(</font>
<font style="color: #2a00ff">"The original file WAS deleted."</font>
<font style="color: #000000">);</font>
<font style="color: #808080">26</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font>
<font style="color: #808080">27</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">else</font></strong>
<font style="color: #808080">28</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">{</font>
<font style="color: #808080">29</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">System.out.println(</font>
<font style="color: #2a00ff">"The original file WAS NOT deleted."</font>
<font style="color: #000000">);</font>
<font style="color: #808080">30</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">tmpFile.delete();</font>
<font style="color: #808080">31</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">return</font></strong>
<font style="color: #000000">;</font>
<font style="color: #808080">32</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font>
<font style="color: #808080">33</font> <font style="color: #ffffff"> </font>
<font style="color: #808080">34</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">tmpFile.renameTo(store);</font>
<font style="color: #808080">35</font> <font style="color: #ffffff"> </font>
<font style="color: #808080">36</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">// thread 1 reads rest of the file</font>
<font style="color: #808080">37</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">compareStreams(LENGTH-LENGTH/</font>
<font style="color: #990000">2</font>
<font style="color: #000000">, in1, reference1);</font>
<font style="color: #808080">38</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">in1.close();</font>
<font style="color: #808080">39</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">reference1.close();</font>
<font style="color: #808080">40</font> <font style="color: #ffffff"> </font>
<font style="color: #808080">41</font> <font style="color: #ffffff"> </font>
<font style="color: #3f7f5f">//thread 2 reads the file</font>
<font style="color: #808080">42</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertTrue(store.canRead());</font>
<font style="color: #808080">43</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">InputStream in2 = </font>
<strong><font style="color: #7f0055">new </font></strong>
<font style="color: #000000">FileInputStream(store);</font>
<font style="color: #808080">44</font> <font style="color: #ffffff"> </font>
<strong><font style="color: #7f0055">int </font></strong>
<font style="color: #000000">bytesRead = compareStreams(in2, getSourceInputStream());</font>
<font style="color: #808080">45</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">assertEquals(LENGTH, bytesRead);</font>
<font style="color: #808080">46</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">in2.close();</font>
<font style="color: #808080">47</font> <font style="color: #ffffff"> </font>
<font style="color: #000000">}</font></font>
Zkoušíme tu situaci, kde první vlákno rozečte soubor, druhé vlákno se rozhodne, že soubor není platný a že je potřeba ho přepsat novou verzí (řádek 16). Pokus o smazání na řádku 22 může teoreticky skončit několika výsledky
a) Soubor bude smazán, čtoucí vlákno má smůlu b) Soubor nepůjde smazat c) Soubor se smaže, přesto ho bude moci čtoucí vlákno dočíst
Experimenty potvrdili, že dochází ke výsledkům b a c. Windows jednoduše odmítnou soubor smazat, to je pochopitelné chování, je na nás, abychom se s touto situací nějak vyrovnali. Linux (alespoň moje verze na extfs3) soubor smaže, ale čtoucí vlákno ho nějakým zázračným způsobem může dočíst. Dochází tedy k situaci, kdy dvě vlákna čtou současně jeden soubor, každé s jiným obsahem. Jelikož je ale tento obsah konzistentní, není to žádný problém, spíše naopak. Nemusíme řešit, co dělat, pokud soubor nejde smazat.
Ukázali jsme si, že i Java programátor se občas musí starat o to, na jakém systému jeho program poběží. Pocit, že na prostředí nezáleží že nás Java izoluje je v některých případech falešný. Práce ze soubory je jedním takovým případem. Při práci na vícevláknové aplikaci je dobré se zamyslet, co se může stát, když se nám dvě a více vláken střetne nad společným zdrojem. Ukázali jsme si, že
- U FileInputStreamu a FileOutputStreamu k žádnému implicitnímu zamykání nedochází, je možné do souboru zapisovat a zároveň z něho číst.
- Explicitní zámky na souborech slouží pouze k vyloučení nejavových procesů. V rámci jednoho virtuálního stroje mohou být neúčinné.
- Je nutné kontrolovat návratové hodnoty z metod java.io.File, jako renameTo a delete. Může se stát, že selžou (na různých OS různě).
Ukazuje se také, že je občas dobré si přečíst dokumentaci až do konce, před tím než se člověk vrhne po hlavě do implementace. I když na druhou stranu je docela zajímavé vyzkoušet si jak to funguje na vlastní kůži.
Chcete-li vidět verzi kódu, kterou jsem použil v projektu m2-proxy podívejte se do CVS projektu.
Zdrojové kódy příkladu jsou ke stažení zde.
Pro synchronizaci v ramce jedny JVM bych nepouzival FileLock. Dalo by se zneuzit
synchronized (file.toString().intern()) {…}
ale to by opravdu bylo zneuziti String-u. Pokud to napadne jeste nekoho jinyho, bude problem. Ale staci udelat si vlastni pool nakych objektu.
> Linux (alespoň moje verze na extfs3) soubor smaže, ale čtoucí vlákno ho nějakým zázračným způsobem může dočíst.
To neni zazrak: Tam jen pochopili, ze smazani souboru je pouze jeho odstraneni z adresare. Na fyzicke existenci souboru to nic nemeni (smazne se az nebude potreba, neco jako GC zalozeny na reference counting).