?譯者 | 李睿
審校 | 孫淑娟
人們需要了解如何在Java程序中使用懶惰實例化和急切實例化。那么,哪種方法更好?這取決于場景。
當實例化在資源使用方面開銷很大的Java對象時,用戶不希望每次使用它們時都必須進行實例化。對于提高性能來說,擁有一個可以在整個系統中共享的現成對象實例要好得多。在這種情況下,懶惰實例化策略非常有效。
懶惰實例化也有一些缺點,而在某些系統中,采用急切實例化方法更好。在急切實例化中,通常在應用程序啟動后立即實例化對象一次。這兩種方法是不同的。而在某些情況下,某種方法最有效。
本文將介紹這兩種實例化Java對象的方法。首先看到代碼示例,然后用Java代碼挑戰測試所學到的內容。此外,還將討論懶惰實例化與急切實例化的優缺點。
1.懶惰實例化的簡單方法
首先,了解創建單個實例并在系統中共享它的簡單方法:
下面是代碼中發生的情況:
開始(#A),聲明一個靜態內部類HeroesDB。將變量聲明為靜態的變量,它可以在應用程序中共享。
下一步(#B),創建一個私有構造函數,以避免從類外部直接實例化。因此,必須使用getHeroes()方法來獲取一個實例。
在下一行(#C),看到了有效地從HeroesDB返回實例的方法。
接下來(#D),檢查heroesDB實例是否為空。如果是空,將創建一個新實例。否則什么也不做。
最后(#F),返回heroesDB對象實例。
這種方法適用于小型應用程序。然而,在有許多用戶的大型多線程應用程序中,很可能會出現數據沖突。在這種情況下,對象可能會被多次實例化,即使檢查是否為空。以下進一步探討為什么會發生這種情況的原因。
2.理解競態條件
競態條件是指兩個或多個線程并發競爭同一個變量的情況,這可能會導致意外的結果。
在大型多線程應用程序中,許多進程并行并發地運行。在這種類型的應用程序中,有可能在另一個線程實例化一個空對象的同時,一個線程正在詢問一個對象是否為空。在這種情況下,有一個競態條件,這可能導致重復的實例。
可以通過使用synchronized關鍵字來修復這個問題:
這段代碼解決了線程在getHeroesDB()中存在沖突的問題。然而正在同步整個方法。這可能會影響性能,因為每次只有一個線程能夠訪問整個方法。
以下看看如何解決這個問題。
3.優化的多線程懶惰實例化
要同步getHeroesDB()方法中的策略點,需要在該方法中創建同步塊。以下是一個例子:
左右滑動查看完整代碼
在這段代碼中,只在實例為空時同步對象的創建。否則,將返回對象實例。
還要注意的是,同步了ThreadSafeSynchronized類,因為使用的是靜態方法。然后再次檢查,以確保heroesDB實例仍然為空,因為可能有另一個線程已將其實例化。如果不進行雙重檢查,可能會得到多個實例。
另一個重要的問題是,變量heroesDB是不穩定的。這意味著不會緩存變量的值。當線程更改這一變量時,它將始終具有最新更新的值。
4.何時使用急切實例化
對于可能從未使用過的開銷大的對象,最好使用懶惰實例化。然而,如果所處理的對象知道在應用程序每次啟動時都將被使用,并且就使用的系統資源來說,如果創建對象開銷很大,那么最好使用急切實例化。
假設必須創建一個開銷非常大的對象,例如人們總是需要的數據庫連接。等待該對象被使用可能會降低應用程序的運行速度。在這種情況下,急切實例化更有意義。
5.實現急切實例化的簡單方法
實現急切實例化的簡單方法如下:
這段代碼的輸出是:
注意,在本例中沒有進行是為空的檢查。當HeroesDB被聲明為HeroesDatabaseSimpleEager中的實例變量時,它就被實例化了。因此,每次訪問HeroesDatabaseSimpleEager類時,都會從HeroesDB獲得一個實例。還重寫了toString()方法,以簡化HeroesDB實例的輸出。
現在看看使用枚舉實現急切實例化的更健壯的方法。
6.使用枚舉創建急切實例化
使用枚舉是創建急切實例化對象的一種更健壯的方法。盡管實例只會在枚舉被訪問時被創建,但要注意在下面的代碼中,沒有對對象創建進行是否為空的檢查:
這段代碼的輸出將是:
這段代碼是線程安全的。它保證只創建一個實例,并且序列化對象,這意味著可以更容易地傳輸它。另一個細節是,對于枚舉有一個隱式的私有構造函數,這保證了不會不必要地創建多個實例。枚舉被認為是使用急切實例化的最佳方法之一,因為它簡單而有效。
7.懶惰實例化vs.急切實例化
當不總是需要實例化一個對象時,采用懶惰實例化更好。當知道總是需要實例化對象時,急切實例化更好。以下是每種方法的優缺點:
(1)懶惰實例化
優點:對象只會在需要的時候被實例化。
缺點:
- 需要同步才能在多線程環境中工作。
- 由于if檢查和同步,性能會變慢。
- 當需要該對象時,應用程序可能會有明顯的懶惰。
(2)急切實例化
優點:
- 在大多數情況下,對象將在應用程序啟動時被實例化。
- 使用對象時沒有延遲,因為它已經被實例化了。
- 它在多線程環境中工作良好。
缺點:使用這種方法可能會不必要地實例化對象。
8.Lazy Homer beer創作挑戰
在下面的Java代碼挑戰中,將看到多線程環境中發生的懶惰實例化。
要注意的是,正在使用ThreadPool可以直接使用Thread類,但最好使用Java并發API。
根據在本文中學到的知識,人們會認為在運行以下代碼時最可能發生什么情況?
以下是應對這一挑戰的選項。仔細查看代碼并選擇其中一個:
1.A) 1
2.B) 0
3.C) 2
4.D)拋出InterruptedException
9.發生了什么?懶惰實例化解釋
這個代碼挑戰的關鍵概念是,當兩個線程訪問同一個進程時,會出現并行性。因此,既然有一個線程。在實例化beer之前休眠,很可能會創建兩個beer實例。
線程不并發運行的可能性非常小,這取決于JVM實現。但是由于線程的原因,很有可能最終得到兩個Thread.sleep的方法。
現在再次查看代碼,注意正在使用線程池創建兩個線程,然后對這些線程運行createBear方法。
因此,這個代碼挑戰的正確答案是:C,或2的值。
10.結論
對于使用開銷大的對象優化性能來說,懶惰實例化和急切實例化是很重要的概念。以下是關于這些設計策略需要記住的要點:
- 懶惰實例化需要在實例化之前進行是否為空的檢查。
- 在多線程環境中同步對象以實現懶惰實例化。
- 急切實例化不需要對對象進行是否為空的檢查。
- 使用枚舉是一種有效且簡單的緊急實例化方法。
原文鏈接:https://www.infoworld.com/article/3675954/lazy-vs-eager-instantiation-in-java-which-is-better.htm