2015-10-15 177 views
3

我有一個簡單的類:爲什麼java併發測試失敗?

import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 

public class DummyService { 
    private final Logger logger = LoggerFactory.getLogger(getClass()); 
    private boolean dataIndexing = false; 

    public boolean isDataIndexing() { 
     logger.info("isDataIndexing: {}", dataIndexing); 
     return dataIndexing; 
    } 

    public void cancelIndexing() { 
     logger.info("cancelIndexing: {}", dataIndexing); 
     dataIndexing = false; 
    } 

    public void createIndexCorp() { 
     logger.info("createIndexCorp: {}", dataIndexing); 
     createIndex(); 
    } 

    public void createIndexEntr() { 
     logger.info("createIndexEntr: {}", dataIndexing); 
     createIndex(); 
    } 

    private void createIndex() { 
     logger.info("createIndex: {}", dataIndexing); 
     if(dataIndexing) 
      throw new IllegalStateException("Service is busy!"); 
     dataIndexing = true; 
     try { 
      while(dataIndexing) { 
       Thread.sleep(100); 
       logger.debug("I am busy..."); 
      } 
      logger.info("Indexing canceled"); 
     } catch (InterruptedException e) { 
      logger.error("Error during sleeping", e); 
     } finally { 
      dataIndexing = false; 
     } 
    } 
} 

和一個單元測試,與我想測試對象行爲:

public class CommonUnitTest 
{ 
    @Test 
    public void testCreateIndexWithoutAsync() throws InterruptedException { 
     final long sleepMillis = 500; 
     final DummyService indexService = new DummyService(); 
     assertFalse(indexService.isDataIndexing()); 
     new Thread(() -> { 
        indexService.createIndexCorp(); 
       } 
     ).start(); 
     Thread.sleep(sleepMillis); 
     assertTrue(indexService.isDataIndexing()); 
     // TaskExecutor should fails here 
     new Thread(() -> { 
        indexService.createIndexEntr(); 
        logger.error("Exception expected but not occurred"); 
       } 
     ).start(); 
     assertTrue(indexService.isDataIndexing()); 
     indexService.cancelIndexing(); 
     Thread.sleep(sleepMillis); 
     assertFalse(indexService.isDataIndexing()); 
    } 
} 

對象的行爲必須是:如果該方法createIndexCorp或createIndexEntr是由一個線程調用,那麼另一個線程必須通過嘗試調用其中一個方法來獲得異常。但是這不會發生!這裏是日誌:

2015-10-15 17:15:06.277 INFO --- [   main] c.c.o.test.DummyService     : isDataIndexing: false 
2015-10-15 17:15:06.318 INFO --- [  Thread-0] c.c.o.test.DummyService     : createIndexCorp: false 
2015-10-15 17:15:06.319 INFO --- [  Thread-0] c.c.o.test.DummyService     : createIndex: false 
2015-10-15 17:15:06.419 DEBUG --- [  Thread-0] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:06.524 DEBUG --- [  Thread-0] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:06.624 DEBUG --- [  Thread-0] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:06.724 DEBUG --- [  Thread-0] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:06.818 INFO --- [   main] c.c.o.test.DummyService     : isDataIndexing: true 
2015-10-15 17:15:06.820 INFO --- [   main] c.c.o.test.DummyService     : isDataIndexing: true 
2015-10-15 17:15:06.820 INFO --- [  Thread-1] c.c.o.test.DummyService     : createIndexEntr: true 
2015-10-15 17:15:06.820 INFO --- [   main] c.c.o.test.DummyService     : cancelIndexing: true 
2015-10-15 17:15:06.820 INFO --- [  Thread-1] c.c.o.test.DummyService     : createIndex: true 
2015-10-15 17:15:06.824 DEBUG --- [  Thread-0] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:06.921 DEBUG --- [  Thread-1] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:06.924 DEBUG --- [  Thread-0] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:07.021 DEBUG --- [  Thread-1] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:07.024 DEBUG --- [  Thread-0] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:07.121 DEBUG --- [  Thread-1] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:07.124 DEBUG --- [  Thread-0] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:07.221 DEBUG --- [  Thread-1] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:07.224 DEBUG --- [  Thread-0] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:07.321 DEBUG --- [  Thread-1] c.c.o.test.DummyService     : I am busy... 
2015-10-15 17:15:07.321 INFO --- [   main] c.c.o.test.DummyService     : isDataIndexing: true 

你可以看到第二個線程可以啓動進程,但它應該會得到異常。測試代碼中的最後一個斷言也失敗了。這怎麼可能發生?我不明白這種行爲。我試圖使用volatile和synchronized關鍵字,但沒有任何幫助。 DummyService有什麼問題?

+0

此日誌'主] c.c.o.test.DummyService:cancelIndexing:TRUE' – njzk2

+0

它來自這一行:'indexService.cancelIndexing();'。爲什麼? – njzk2

回答

2

你有3個線程,T0,T1和TM(主)。 操作的順序是這樣的:

tm starts t0 
t0 checks dataIndexing flag - false, goes into the loop, sets flag to true 
tm sleeps 
tm starts t1 
tm sets indexing flag to false 
t1 checks dataIndexing flag - false, goes into the loop, sets flag to true 
t0 continues the loop because it missed that brief period when indexing was cancelled 

如果您在主索引TM標誌設置爲false之前睡覺,那麼T1會得到異常。 您需要同步對多個線程之間共享的變量的訪問。即檢查標誌的狀態並且改變它需要在持有互斥體時完成。

+0

謝謝!現在我明白測試是錯誤的,而不是服務(儘管應該使用「同步」)。我想知道爲什麼我沒有看到它。爲什麼我的3個同事也沒有看到它;-) – philosophman1978

0

看來你打的記錄和實際執行之間的差異。線程可以想象地運行取消並在日誌和異常之間的空間中創建索引,從而第二個線程滑倒並防止取消第一個和第二個線程。

這是不可取的,允許共享資源,即private boolean dataIndexing同時變化。有兩個解決方案(至少):

1.A syncronized方法,以允許所述共享資源的改變(因此限制只訪問一個線程時間)

private synchronized void setDataIndexing(boolean value) { 
    dataIndexing = value; 
} 

2.Guarding每個改變在syncronized部分(以及= true兩個= false地)這個值:

syncronized (this) { 
    dataIndexing = /* the relevant value */; 
} 

我會建議一個單獨的方法,但良好的知道的替代品。

+0

謝謝MK。爲可能的場景:) – st2rseeker

+0

這不是一個可能的場景,它是發生了什麼 –

+1

我會建議''AtomicBoolean'與'compareAndSet'方法的使用。 – LoganMzz

0

不回答你的問題,但是這是完全不同步:

if (dataIndexing) 
    throw new IllegalStateException("Service is busy!"); 
dataIndexing = true; 

業務繁忙,如果你的執行到達throw聲明?不必要!另一個線程可能會在測試和拋出之間將dataIndexing的值從true更改爲false。

更糟的是,也許差多了,是兩個線程可能都扔在同一時間後到達的聲明

Thread A       Thread B 

tests dataIndexing, finds it to 
be false. 

            Tests dataIndexing, finds it to be false. 

sets dataIndexing = true;   sets dataIndexing = true; 
...        ... 

此外,這是不可靠的,這需要時間。

Thread.sleep(sleepMillis); 
assertTrue(indexService.isDataIndexing()); 

更好地設計你的類可測性。如果你的測試需要等到isDataIndexing(),那麼你的班級應該提供一個測試手段到wait() ......

另外,不要低估測試在儘可能少的時間內完成的重要性。當你有一個擁有數千或數萬測試用例的系統時,秒數真的開始累加起來。