Spring在SingleTon模式下的線程安全詳解

1、有狀態的bean與無狀態的bean

  • 有狀態bean:每個用戶有自己特有的一個實例,在用戶的生存期內,bean保存瞭用戶的信息,即有狀態;一旦用戶滅亡(調用結束或實例結束),bean的生命期也告結束。即每個用戶最初都會得到一個初始的bean。
  • 無狀態bean:bean一旦實例化就被加進會話池中,各個用戶都可以共用。即使用戶已經消亡,bean的生命期也不一定結束,它可能依然存在於會話池中,供其他用戶調用。由於沒有特定的用戶,那麼也就不能保持某一用戶的狀態,所以叫無狀態bean。但無狀態會話bean 並非沒有狀態,如果它有自己的屬性(變量)。

有狀態就是有數據存儲功能。有狀態對象(Stateful Bean),就是有實例變量的對象 ,可以保存數據,是非線程安全的。在不同方法調用間不保留任何狀態。

無狀態就是一次操作不能保存數據。無狀態對象(Stateless Bean),就是沒有實例變量的對象 ,不能保存數據是不變類,是線程安全的。

在Spring的Bean配置中,存在這樣兩種情況:

<bean id="testManager" class="com.sw.TestManagerImpl" scope="singleton" />  
<bean id="testManager" class="com.sw.TestManagerImpl" scope="prototype" /> 

當然,scope的值不止這兩種,還包括瞭request、session 等。但用的最多的還是singleton單態與prototype多態。

singleton表示該bean全局隻有一個實例,Spring中bean的scope默認也是singleton。

prototype表示該bean在每次被註入的時候,都要重新創建一個實例,這種情況適用於有狀態的Bean。

下面是一個有狀態的Bean示例

public class TestManagerImpl implements TestManager {  
    private User user;
    public void deleteUser(User e) throws Exception {  
        user = e;    //1
        prepareData(e);
    }
    public void prepareData(User e) throws Exception {  
        user = getUserByID(e.getId());     //2
        //使用user.getId();                //3
    }
}

如果該Bean配置為singleton,會出現什麼樣的狀況呢?

如果有2個用戶訪問,都調用到瞭該Bean,假定為user1、user2。

當user1調用到程序中的步驟1的時候,該Bean的私有變量user被賦值為user1,當user1的程序走到步驟2的時候,該Bean的私有變量user被重新賦值為user1_create,理想的狀況,當user1走到3步驟的時候,私有變量user應該為user1_create;但如果在user1調用到3步驟之前,user2開始運行到瞭步驟1瞭,由於單態的資源共享,則私有變量user被修改為user2;這種情況下,user1的步驟3用到的user.getId()實際用到是user2的對象。

即將有狀態的bean配置成singleton會造成資源混亂問題(線程安全問題),而如果是prototype的話,就不會出現資源共享的問題,即不會出現線程安全的問題。

註:如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,那麼代碼就是線程安全的。

通過上面分析,大傢已經對有狀態和無狀態有瞭一定的理解。無狀態的Bean適合用不變模式,技術就是單例模式,這樣可以共享實例,提高性能。有狀態的Bean,多線程環境下不安全,那麼適合用Prototype原型模式(解決多線程問題),每次對bean的請求都會創建一個新的bean實例。

2、Spring中的單例

Spring中的單例與設計模式裡面的單例略有不同,設計模式的單例是在整個應用中隻有一個實例,而Spring中的單例是在一個IOC容器中就隻有一個實例。

大多數時候客戶端都在訪問我們應用中的業務對象,為瞭減少並發控制,在這個時候我們不應該在業務對象中設置那些容易造成出錯的成員變量。在並發訪問時候,這些成員變量將會是並發線程中的共享對象,那麼這個時候就會出現意外情況。

成員變量的解決方式:

  • 方法的參數局部變量(在方法中new)
  • 使用Threadlocal
  • 設置bean的scope=prototype

3、Spring使用ThreadLocal解決線程安全問題案例

Spring作為一個IOC容器,幫助我們管理瞭許許多多的bean。但其實,Spring並沒有保證這些對象的線程安全,需要由開發者自己編寫解決線程安全問題的代碼。

在使用Spring時,很多人可能對Spring中為什麼DAO和Service對象采用單實例方式很迷惑,這些讀者是這麼認為的。

DAO對象必須包含一個數據庫的連接Connection,而這個Connection不是線程安全的,所以每個DAO都要包含一個不同的Connection對象實例,這樣一來DAO對象就不能是單實例的瞭。

上述觀點對瞭一半。對的是“每個DAO都要包含一個不同的Connection對象實例”這句話,錯的是“DAO對象就不能是單實例”。其實Spring在實現Service和DAO對象時,使用瞭ThreadLocal這個類,這個是一切的核心!

  • ThreadLocal
  • 每個線程中都有一個自己的ThreadLocalMap類對象,可以將線程自己的對象保持到其中,各管各的,線程可以正確的訪問到自己的對象。
  • 將一個共用的ThreadLocal靜態實例作為key,將不同對象的引用保存到不同線程的ThreadLocalMap中,然後在線程執行的各處通過這個靜態ThreadLocal實例的get()方法取得自己線程保存的那個對象,避免瞭將這個對象作為參數傳遞的麻煩。

一般的Web應用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過接口向上層開放功能調用。在一般情況下,從接收請求到返回響應所經過的所有程序調用都同屬於一個線程。這樣你就可以根據需要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調用線程中,所有關聯的對象引用到的都是同一個變量。

下面的實例能夠體現Spring對有狀態Bean的改造思路:

public class TopicDao {
    //①一個非線程安全的變量
  private Connection conn;
    //②引用非線程安全變量
  public void addTopic() {
        Statement stat = conn.createStatement();
  }
}

由於①處的conn是成員變量,因為addTopic()方法是非線程安全的,必須在使用時創建一個新TopicDao實例(非singleton)。下面使用ThreadLocal對conn這個非線程安全的狀態進行改造:

public class TopicDao {  
    //①使用ThreadLocal保存Connection變量  
    private static ThreadLocal <Connection>connThreadLocal = newThreadLocal<Connection>();  
    public static Connection getConnection() {  
       // ②如果connThreadLocal沒有本線程對應的Connection創建一個新的Connection,  
       // 並將其保存到線程本地變量中。  
       if (connThreadLocal.get() == null) {  
           Connection conn = getConnection();  
           connThreadLocal.set(conn);  
           return conn;  
       }
       // ③直接返回線程本地變量
       return connThreadLocal.get();  
    }
    public void addTopic() {  
       // ④從ThreadLocal中獲取線程對應的Connection  
       try {
           Statement stat = getConnection().createStatement();  
       } catch (SQLException e) {  
           e.printStackTrace();  
       }
    }
}

不同的線程在使用TopicDao時,先判斷connThreadLocal是否是null,如果是null,則說明當前線程還沒有對應的Connection對象,這時創建一個Connection對象並添加到本地線程變量中;如果不為null,則說明當前的線程已經擁有瞭Connection對象,直接使用就可以瞭。這樣,就保證瞭不同的線程使用線程相關的Connection,而不會使用其它線程的Connection。因此,這個TopicDao就可以做到singleton共享瞭。

Spring中DAO和Service都是以單實例的bean形式存在,Spring通過ThreadLocal類將有狀態的變量(例如數據庫連接Connection)本地線程化,從而做到多線程狀況下的安全。在一次請求響應的處理線程中, 該線程貫通展示、服務、數據持久化三層,通過ThreadLocal使得所有關聯的對象引用到的都是同一個變量。

當然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在DAO隻能做到本DAO的多個方法共享Connection時不發生線程安全問題,但無法和其它DAO共用同一個Connection,要做到同一事務多DAO共享同一Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。

Web應用劃分為展現層、服務層和持久層,controller中引入xxxService作為成員變量,xxxService中又引入xxxDao作為成員變量,這些對象都是單例而且會被多個線程並發訪問,可我們訪問的是它們裡面的方法,這些類裡面通常不會含有成員變量,dao實例是在MyBatis等ORM框架裡面封裝好的,已經被測試,不會出現線程同步問題瞭。所以出問題的地方就是我們自己系統裡面的業務對象,所以我們一定要註意自定義的業務對象裡面千萬不能出現獨立成員變量,否則會有線程安全問題。

通常我們在應用中的業務對象如下例子,controller中擁有成員變量list和paperService。

public class TestPaperController extends BaseController {
    private static final int list = 0;
    @Autowired
    @Qualifier("papersService")
    private TestPaperService papersService ;
    public Page queryPaper(int pageSize, int page,TestPaper paper) throws EicException {
      RowSelection localRowSelection = getRowSelection(pageSize, page);
      List<TestPaper> paperList = papersService.queryPaper(paper,localRowSelection);
      Page localPage = new Page(page, localRowSelection.getTotalRows(), paperList);
      return localPage;
    }
}

service裡面又引入瞭成員變量ibatisEntityDao

@SuppressWarnings("unchecked")
@Service("papersService")
@Transactional(rollbackFor = {Exception.class})
public class TestPaperServiceImpl implements TestPaperService {
    @Autowired
    @Qualifier("ibatisEntityDao")
    private IbatisEntityDao ibatisEntityDao;
    private static final String NAMESPACE_TESTPAPER = "com.its.exam.testpaper.model.TestPaper";
    private static final String BO_NAME[] = { "試卷倉庫" };
    private static final String BO_NAME2[] = { "試卷配置試題" };
    private static final String BO_NAME1[] = { "試卷試題類型" };
    private static final String NAMESPACE_TESTQUESTION="com.its.exam.testpaper.model.TestQuestion";
    public List<TestPaper> queryPaper(TestPaper paper,RowSelection paramRowSelection) throws EicException {
      try {
       return (List<TestPaper>) ibatisEntityDao.queryForListWithPage(NAMESPACE_TESTPAPER, "queryPaper", paper,paramRowSelection);
      } catch (Exception e) {
       e.printStackTrace();
       throw new EicException(e, "eic", "0001", BO_NAME);
      }
    }
}

由上面例子可以看出,雖然我們這個應用裡面含有成員變量,但是並不會出現線程同步方面的問題,controller裡面的成員變量papersService被註入後,是為瞭訪問該service類的方法,papersService裡面註入的成員變量ibatisEntityDao是ORM框架封裝好的,其線程同步問題已解決。

4、ThreadLocal與線程同步機制的比較

ThreadLocal和線程同步機制相比有什麼優勢呢?ThreadLocal和線程同步機制都是為瞭解決多線程中相同變量的訪問沖突問題。

在同步機制中,通過對象的鎖機制保證同一時間隻有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析什麼時候對變量進行讀寫,什麼時候需要鎖定某個對象,什麼時候釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。

而ThreadLocal則從另一個角度來解決多線程的並發訪問。ThreadLocal會為每一個線程提供一個獨立的變量副本,從而隔離瞭多個線程對數據的訪問沖突。因為每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步瞭。ThreadLocal提供瞭線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。

由於ThreadLocal中可以持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,需要強制類型轉換。但JDK 5.0通過泛型很好的解決瞭這個問題,在一定程度地簡化ThreadLocal的使用。

概括起來說,對於多線程資源共享的問題,同步機制采用瞭以時間換空間”的方式,而ThreadLocal采用瞭以空間換時間的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而後者為每一個線程都提供瞭一份變量,因此可以同時訪問而互不影響。

ThreadLocal是解決線程安全問題一個很好的思路,它通過為每個線程提供一個獨立的變量副本解決瞭變量並發訪問的沖突問題。在很多情況下,ThreadLocal比直接使用synchronized同步機制解決線程安全問題更簡單,更方便,且結果程序擁有更高的並發性。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: