海水正藍

          面朝大海,春暖花開
          posts - 145, comments - 29, trackbacks - 0, articles - 1
            BlogJava :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理

          Ehcache 是現在最流行的純Java開源緩存框架,配置簡單、結構清晰、功能強大,最初知道它,是從Hibernate的緩存開始的。網上中文的EhCache材料 以簡單介紹和配置方法居多,如果你有這方面的問題,請自行google;對于API,官網上介紹已經非常清楚,請參見官網;但是很少見到特性說明和對實現 原理的分析,因此在這篇文章里面,我會詳細介紹和分析EhCache的特性,加上一些自己的理解和思考,希望對緩存感興趣的朋友有所收獲。

           

          一、特性一覽,來自官網,簡單翻譯一下:

           

          1、快速輕量
          過去幾年,諸多測試表明Ehcache是最快的Java緩存之一。
          Ehcache的線程機制是為大型高并發系統設計的。
          大量性能測試用例保證Ehcache在不同版本間性能表現得一致性。
          很多用戶都不知道他們正在用Ehcache,因為不需要什么特別的配置。
          API易于使用,這就很容易部署上線和運行。
          很小的jar包,Ehcache 2.2.3才668kb。
          最小的依賴:唯一的依賴就是SLF4J了。

          2、伸縮性
          緩存在內存和磁盤存儲可以伸縮到數G,Ehcache為大數據存儲做過優化。
          大內存的情況下,所有進程可以支持數百G的吞吐。
          為高并發和大型多CPU服務器做優化。
          線程安全和性能總是一對矛盾,Ehcache的線程機制設計采用了Doug Lea的想法來獲得較高的性能。
          單臺虛擬機上支持多緩存管理器。
          通過Terracotta服務器矩陣,可以伸縮到數百個節點。

          3、靈活性
          Ehcache 1.2具備對象API接口和可序列化API接口。
          不能序列化的對象可以使用除磁盤存儲外Ehcache的所有功能。
          除了元素的返回方法以外,API都是統一的。只有這兩個方法不一致:getObjectValue和getKeyValue。這就使得緩存對象、序列化對象來獲取新的特性這個過程很簡單。
          支持基于Cache和基于Element的過期策略,每個Cache的存活時間都是可以設置和控制的。
          提供了LRU、LFU和FIFO緩存淘汰算法,Ehcache 1.2引入了最少使用和先進先出緩存淘汰算法,構成了完整的緩存淘汰算法。
          提供內存和磁盤存儲,Ehcache和大多數緩存解決方案一樣,提供高性能的內存和磁盤存儲。
          動態、運行時緩存配置,存活時間、空閑時間、內存和磁盤存放緩存的最大數目都是可以在運行時修改的。

          4、標準支持
          Ehcache提供了對JSR107 JCACHE API最完整的實現。因為JCACHE在發布以前,Ehcache的實現(如net.sf.jsr107cache)已經發布了。
          實現JCACHE API有利于到未來其他緩存解決方案的可移植性。
          Ehcache的維護者Greg Luck,正是JSR107的專家委員會委員。

          5、可擴展性
          監聽器可以插件化。Ehcache 1.2提供了CacheManagerEventListener和CacheEventListener接口,實現可以插件化,并且可以在ehcache.xml里配置。
          節點發現,冗余器和監聽器都可以插件化。
          分布式緩存,從Ehcache 1.2開始引入,包含了一些權衡的選項。Ehcache的團隊相信沒有什么是萬能的配置。
          實現者可以使用內建的機制或者完全自己實現,因為有完整的插件開發指南。
          緩存的可擴展性可以插件化。創建你自己的緩存擴展,它可以持有一個緩存的引用,并且綁定在緩存的生命周期內。
          緩存加載器可以插件化。創建你自己的緩存加載器,可以使用一些異步方法來加載數據到緩存里面。
          緩存異常處理器可以插件化。創建一個異常處理器,在異常發生的時候,可以執行某些特定操作。

          6、應用持久化
          在VM重啟后,持久化到磁盤的存儲可以復原數據。
          Ehcache是第一個引入緩存數據持久化存儲的開源Java緩存框架。緩存的數據可以在機器重啟后從磁盤上重新獲得。
          根據需要將緩存刷到磁盤。將緩存條目刷到磁盤的操作可以通過cache.flush()方法來執行,這大大方便了Ehcache的使用。

          7、監聽器
          緩存管理器監聽器。允許注冊實現了CacheManagerEventListener接口的監聽器:
          notifyCacheAdded()
          notifyCacheRemoved()
          緩存事件監聽器。允許注冊實現了CacheEventListener接口的監聽器,它提供了許多對緩存事件發生后的處理機制:
          notifyElementRemoved/Put/Updated/Expired

          8、開啟JMX
          Ehcache的JMX功能是默認開啟的,你可以監控和管理如下的MBean:
          CacheManager、Cache、CacheConfiguration、CacheStatistics

          9、分布式緩存
          從Ehcache 1.2開始,支持高性能的分布式緩存,兼具靈活性和擴展性。
          分布式緩存的選項包括:
          通過Terracotta的緩存集群:設定和使用Terracotta模式的Ehcache緩存。緩存發現是自動完成的,并且有很多選項可以用來調試緩存行為和性能。
          使用RMI、JGroups或者JMS來冗余緩存數據:節點可以通過多播或發現者手動配置。狀態更新可以通過RMI連接來異步或者同步完成。
          Custom:一個綜合的插件機制,支持發現和復制的能力。
          可用的緩存復制選項。支持的通過RMI、JGroups或JMS進行的異步或同步的緩存復制。
          可靠的分發:使用TCP的內建分發機制。
          節點發現:節點可以手動配置或者使用多播自動發現,并且可以自動添加和移除節點。對于多播阻塞的情況下,手動配置可以很好地控制。
          分布式緩存可以任意時間加入或者離開集群。緩存可以配置在初始化的時候執行引導程序員。
          BootstrapCacheLoaderFactory抽象工廠,實現了BootstrapCacheLoader接口(RMI實現)。
          緩存服務端。Ehcache提供了一個Cache Server,一個war包,為絕大多數web容器或者是獨立的服務器提供支持。
          緩存服務端有兩組API:面向資源的RESTful,還有就是SOAP。客戶端沒有實現語言的限制。
          RESTful緩存服務器:Ehcached的實現嚴格遵循RESTful面向資源的架構風格。
          SOAP緩存服務端:Ehcache RESTFul Web Services API暴露了單例的CacheManager,他能在ehcache.xml或者IoC容器里面配置。
          標準服務端包含了內嵌的Glassfish web容器。它被打成了war包,可以任意部署到支持Servlet 2.5的web容器內。Glassfish V2/3、Tomcat 6和Jetty 6都已經經過了測試。

          10、搜索
          標準分布式搜索使用了流式查詢接口的方式,請參閱文檔。

          11、Java EE和應用緩存
          為普通緩存場景和模式提供高質量的實現。
          阻塞緩存:它的機制避免了復制進程并發操作的問題。
          SelfPopulatingCache在緩存一些開銷昂貴操作時顯得特別有用,它是一種針對讀優化的緩存。它不需要調用者知道緩存元素怎樣被返回,也支持在不阻塞讀的情況下刷新緩存條目。
          CachingFilter:一個抽象、可擴展的cache filter。
          SimplePageCachingFilter:用于緩存基于request URI和Query String的頁面。它可以根據HTTP request header的值來選擇采用或者不采用gzip壓縮方式將頁面發到瀏覽器端。你可以用它來緩存整個Servlet頁面,無論你采用的是JSP、 velocity,或者其他的頁面渲染技術。
          SimplePageFragmentCachingFilter:緩存頁面片段,基于request URI和Query String。在JSP中使用jsp:include標簽包含。
          已經使用Orion和Tomcat測試過,兼容Servlet 2.3、Servlet 2.4規范。
          Cacheable命令:這是一種老的命令行模式,支持異步行為、容錯。
          兼容Hibernate,兼容Google App Engine。
          基于JTA的事務支持,支持事務資源管理,二階段提交和回滾,以及本地事務。

          12、開源協議
          Apache 2.0 license

           

          二、Ehcache的加載模塊列表,他們都是獨立的庫,每個都為Ehcache添加新的功能,可以在此下載

           

          • ehcache-core:API,標準緩存引擎,RMI復制和Hibernate支持
          • ehcache:分布式Ehcache,包括Ehcache的核心和Terracotta的庫
          • ehcache-monitor:企業級監控和管理
          • ehcache-web:為Java Servlet Container提供緩存、gzip壓縮支持的filters
          • ehcache-jcache:JSR107 JCACHE的實現
          • ehcache-jgroupsreplication:使用JGroup的復制
          • ehcache-jmsreplication:使用JMS的復制
          • ehcache-openjpa:OpenJPA插件
          • ehcache-server:war內部署或者單獨部署的RESTful cache server
          • ehcache-unlockedreadsview:允許Terracotta cache的無鎖讀
          • ehcache-debugger:記錄RMI分布式調用事件
          • Ehcache for Ruby:Jruby and Rails支持

          Ehcache的結構設計概覽:

          三、核心定義

           

          cache manager:緩存管理器,以前是只允許單例的,不過現在也可以多實例了

          cache:緩存管理器內可以放置若干cache,存放數據的實質,所有cache都實現了Ehcache接口

          element:單條緩存數據的組成單位

          system of record(SOR):可以取到真實數據的組件,可以是真正的業務邏輯、外部接口調用、存放真實數據的數據庫等等,緩存就是從SOR中讀取或者寫入到SOR中去的。

           

          代碼示例:

          Java代碼  收藏代碼
          1. CacheManager manager = CacheManager.newInstance("src/config/ehcache.xml");  
          2. manager.addCache("testCache");  
          3. Cache test = singletonManager.getCache("testCache");  
          4. test.put(new Element("key1", "value1"));  
          5. manager.shutdown();  

          當然,也支持這種類似DSL的配置方式,配置都是可以在運行時動態修改的:

          Java代碼  收藏代碼
          1. Cache testCache = new Cache(  
          2.   new CacheConfiguration("testCache", maxElements)  
          3.     .memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU)  
          4.     .overflowToDisk(true)  
          5.     .eternal(false)  
          6.     .timeToLiveSeconds(60)  
          7.     .timeToIdleSeconds(30)  
          8.     .diskPersistent(false)  
          9.     .diskExpiryThreadIntervalSeconds(0));  

          事務的例子:

          Java代碼  收藏代碼
          1. Ehcache cache = cacheManager.getEhcache("xaCache");  
          2. transactionManager.begin();  
          3. try {  
          4.     Element e = cache.get(key);  
          5.     Object result = complexService.doStuff(element.getValue());  
          6.     cache.put(new Element(key, result));  
          7.     complexService.doMoreStuff(result);  
          8.     transactionManager.commit();  
          9. catch (Exception e) {  
          10.     transactionManager.rollback();  
          11. }  

           

          四、一致性模型

           

          說到一致性,數據庫的一致性是怎樣的?不妨先來回顧一下數據庫的幾個隔離級別:

          未提交讀(Read Uncommitted):在讀數據時不會檢查或使用任何鎖。因此,在這種隔離級別中可能讀取到沒有提交的數據。會出現臟讀、不可重復讀、幻象讀。
          已提交讀(Read Committed):只讀取提交的數據并等待其他事務釋放排他鎖。讀數據的共享鎖在讀操作完成后立即釋放。已提交讀是數據庫的默認隔離級別。會出現不可重復讀、幻象讀。
          可重復讀(Repeatable Read):像已提交讀級別那樣讀數據,但會保持共享鎖直到事務結束。會出現幻象讀。
          可序列化(Serializable):工作方式類似于可重復讀。但它不僅會鎖定受影響的數據,還會鎖定這個范圍,這就阻止了新數據插入查詢所涉及的范圍。

           

          基于以上,再來對比思考下面的一致性模型:

           

          1、強一致性模型:系統中的某個數據被成功更新(事務成功返回)后,后續任何對該數據的讀取操作都得到更新后的值。這是傳統關系數據庫提供的一致性模型,也是關系數據庫深受人們喜愛的原因之一。強一致性模型下的性能消耗通常是最大的。

           

          2、弱一致性模型:系統中的某個數據被更新后,后續對該數據的讀取操作得到的不一定是更新后的值,這種情況下通常有個“不一致性時間窗口”存在:即數據更新完成后在經過這個時間窗口,后續讀取操作就能夠得到更新后的值。

           

          3、最終一致性模型:屬于弱一致性的一種,即某個數據被更新后,如果該數據后續沒有被再次更新,那么最終所有的讀取操作都會返回更新后的值。

           

          最終一致性模型包含如下幾個必要屬性,都比較好理解:

           

          • 讀寫一致:某線程A,更新某條數據以后,后續的訪問全部都能取得更新后的數據。
          • 會話內一致:它本質上和上面那一條是一致的,某用戶更改了數據,只要會話還存在,后續他取得的所有數據都必須是更改后的數據。
          • 單調讀一致:如果一個進程可以看到當前的值,那么后續的訪問不能返回之前的值。
          • 單調寫一致:對同一進程內的寫行為必須是保序的,否則,寫完畢的結果就是不可預期的了。

          4、Bulk Load:這種模型是基于批量加載數據到緩存里面的場景而優化的,沒有引入鎖和常規的淘汰算法這些降低性能的東西,它和最終一致性模型很像,但是有批量、高速寫和弱一致性保證的機制。

           

          這樣幾個API也會影響到一致性的結果:

           

          1、顯式鎖(Explicit Locking:如果我們本身就配置為強一致性,那么自然所有的緩存操作都具備事務性質。而如果我們配置成最終一致性時,再在外部使用顯式鎖API,也可以達到事務的效果。當然這樣的鎖可以控制得更細粒度,但是依然可能存在競爭和線程阻塞。

           

          2、無鎖可讀取視圖(UnlockedReadsView):一個允許臟讀的decorator,它只能用在強一致性的配置下,它通過申請一個特殊的寫鎖來比完全的強一致性配置提升性能。

          舉例如下,xml配置為強一致性模型:

          Xml代碼  收藏代碼
          1. <cache name="myCache"  
          2.      maxElementsInMemory="500"  
          3.      eternal="false"  
          4.      overflowToDisk="false"  
          5.    <terracotta clustered="true" consistency="strong" />  
          6. </cache>  

          但是使用UnlockedReadsView:

          Java代碼  收藏代碼
          1. Cache cache = cacheManager.getEhcache("myCache");  
          2. UnlockedReadsView unlockedReadsView = new UnlockedReadsView(cache, "myUnlockedCache");  

           

          3、原子方法(Atomic methods):方法執行是原子化 的,即CAS操作(Compare and Swap)。CAS最終也實現了強一致性的效果,但不同的是,它是采用樂觀鎖而不是悲觀鎖來實現的。在樂觀鎖機制下,更新的操作可能不成功,因為在這過程 中可能會有其他線程對同一條數據進行變更,那么在失敗后需要重新執行更新操作。現代的CPU都支持CAS原語了。

          Java代碼  收藏代碼
          1. cache.putIfAbsent(Element element);  
          2. cache.replace(Element oldOne, Element newOne);  
          3. cache.remove(Element);  

           

          五、緩存拓撲類型

           

          1、獨立緩存(Standalone Ehcache):這樣的緩存應用節點都是獨立的,互相不通信。

           

          2、分布式緩存(Distributed Ehcache):數據存儲在Terracotta的服務器陣列(Terracotta Server Array,TSA)中,但是最近使用的數據,可以存儲在各個應用節點中。

           

          邏輯視角:


          L1緩存就在各個應用節點上,而L2緩存則放在Cache Server陣列中。

           

          組網視角:

           

          模型存儲視角:


          L1級緩存是沒有持久化存儲的。另外,從緩存數據量上看,server端遠大于應用節點。

           

          3、復制式緩存(Replicated Ehcache):緩存數據時同時存放在多個應用節點的,數據復制和失效的事件以同步或者異步的形式在各個集群節點間傳播。上述事件到來時,會阻塞寫線程的操作。在這種模式下,只有弱一致性模型。

           

          它有如下幾種事件傳播機制:RMI、JGroups、JMS和Cache Server。

           

          RMI模式下,所有節點全部對等:

           

          JGroup模式:可以配置單播或者多播,協議棧和配置都非常靈活。

           

          Xml代碼  收藏代碼
          1. <cacheManagerPeerProviderFactory  
          2. class="net.sf.ehcache.distribution.jgroups.JGroupsCacheManagerPeerProviderFactory"  
          3. properties="connect=UDP(mcast_addr=231.12.21.132;mcast_port=45566;):PING:  
          4. MERGE2:FD_SOCK:VERIFY_SUSPECT:pbcast.NAKACK:UNICAST:pbcast.STABLE:FRAG:pbcast.GMS"  
          5. propertySeparator="::"  
          6. />  

           

           

          JMS模式:這種模式的核心就是一個消息隊列,每個應用節點都訂閱預先定義好的主題,同時,節點有元素更新時,也會發布更新元素到主題中去。JMS規范實現者上,Open MQ和Active MQ這兩個,Ehcache的兼容性都已經測試過。

          Cache Server模式:這種模式下存在主從節點,通信可以通過RESTful的API或者SOAP。

          無論上面哪個模式,更新事件又可以分為updateViaCopy或updateViaInvalidate,后者只是發送一個過期消息,效率要高得多。

          復制式緩存容易出現數據不一致的問題,如果這成為一個問題,可以考慮使用數據同步分發的機制。

           

          即便不采用分布式緩存和復制式緩存,依然會出現一些不好的行為,比如:

           

          緩存漂移(Cache Drift):每個應用節點只管理自己的緩存,在更新某個節點的時候,不會影響到其他的節點,這樣數據之間可能就不同步了。這在web會話數據緩存中情況尤甚。

           

          數據庫瓶頸(Database Bottlenecks ):對于單實例的應用來說,緩存可以保護數據庫的讀風暴;但是,在集群的環境下,每一個應用節點都要定期保持數據最新,節點越多,要維持這樣的情況對數據庫的開銷也越大。

           

          六、存儲方式

           

          1、堆內存儲:速度快,但是容量有限。

           

          2、堆外(OffHeapStore)存儲:被稱為 BigMemory,只在企業版本的Ehcache中提供,原理是利用nio的DirectByteBuffers實現,比存儲到磁盤上快,而且完全不受 GC的影響,可以保證響應時間的穩定性;但是direct buffer的在分配上的開銷要比heap buffer大,而且要求必須以字節數組方式存儲,因此對象必須在存儲過程中進行序列化,讀取則進行反序列化操作,它的速度大約比堆內存儲慢一個數量級。

          (注:direct buffer不受GC影響,但是direct buffer歸屬的的JAVA對象是在堆上且能夠被GC回收的,一旦它被回收,JVM將釋放direct buffer的堆外空間。)

           

          3、磁盤存儲

           

          七、緩存使用模式

           

          cache-aside:直接操作。先詢問cache某條緩存數據是否存在,存在的話直接從cache中返回數據,繞過SOR;如果不存在,從SOR中取得數據,然后再放入cache中。

           

          Java代碼  收藏代碼
          1. public V readSomeData(K key)   
          2. {  
          3.    Element element;  
          4.    if ((element = cache.get(key)) != null) {  
          5.        return element.getValue();  
          6.    }  
          7.    if (value = readDataFromDataStore(key)) != null) {  
          8.        cache.put(new Element(key, value));  
          9.    }   
          10.    return value;  
          11. }  

           

          cache-as-sor:結合了read-through、write-through或write-behind操作,通過給SOR增加了一層代理,對外部應用訪問來說,它不用區別數據是從緩存中還是從SOR中取得的。

          read-through

          write-through

          write-behind(write-back):既將寫的過程變為異步的,又進一步延遲寫入數據的過程。

           

           

          Copy Cache的兩個模式:CopyOnRead和CopyOnWrite。

          CopyOnRead指的是在讀緩存數據的請求到達時,如果發現數據已經過期,需要重新從源處獲取,發起的copy element的操作(pull);

          CopyOnWrite則是發生在真實數據寫入緩存時,發起的更新其他節點的copy element的操作(push)。

           

          前者適合在不允許多個線程訪問同一個element的時候使用,后者則允許你自由控制緩存更新通知的時機。

          更多push和pull的變化和不同,也可參見這里

           

          八、多種配置方式

           

          包括配置文件、聲明式配置、編程式配置,甚至通過指定構造器的參數來完成配置,配置設計的原則包括:

          所有配置要放到一起

          緩存的配置可以很容易在開發階段、運行時修改

          錯誤的配置能夠在程序啟動時發現,在運行時修改出錯則需要拋出運行時異常

          提供默認配置,幾乎所有的配置都是可選的,都有默認值

           

          九、自動資源控制(Automatic Resource Control,ARC):

           

          它是提供了一種智能途徑來控制緩存,調優性能。特性包括:

          內存內緩存對象大小的控制,避免OOM出現

          池化(cache manager級別)的緩存大小獲取,避免單獨計算緩存大小的消耗

          靈活的獨立基于層的大小計算能力,下圖中可以看到,不同層的大小都是可以單獨控制的

          可以統計字節大小、緩存條目數和百分比

          優化高命中數據的獲取,以提升性能,參見下面對緩存數據在不同層之間的流轉的介紹

          緩存數據的流轉包括了這樣幾種行為:

          Flush:緩存條目向低層次移動。

          Fault:從低層拷貝一個對象到高層。在獲取緩存的過程中,某一層發現自己的該緩存條目已經失效,就觸發了Fault行為。

          Eviction:把緩存條目除去。

          Expiration:失效狀態。

          Pinning:強制緩存條目保持在某一層。

          下面的圖反映了數據在各個層之間的流轉,也反映了數據的生命周期:

           

          十、監控功能

           

          監控的拓撲:


          每個應用節點部署一個監控探針,通過TCP協議與監控服務器聯系,最終將數據提供給富文本客戶端或者監控操作服務器。

           

          十一、廣域網復制

          緩存數據復制方面,Ehcache允許兩個地理位置各異的節點在廣域網下維持數據一致性,同時它提供了這樣幾種方案(注:下面的示例都只繪制了兩個節點的情形,實際可以推廣到N個節點):

           

          第一種方案:Terracotta Active/Mirror Replication


          這種方案下,服務端包含一個活躍節點,一個備份節點;各個應用節點全部靠該活躍節點提供讀寫服務。這種方式最簡單,管理容易;但是,需要寄希望于理想的網絡狀況,服務器之間和客戶端到服務器之間都存在走WAN的情況,這樣的方案其實最不穩定。

           

          第二種方案:Transactional Cache Manager Replication


          這 種方案下,數據讀取不需要經過WAN,寫入數據時寫入兩份,分別由兩個cache manager處理,一份在本地Server,一份到其他Server去。這種方案下讀的吞吐量較高而且延遲較低;但是需要引入一個XA事務管理器,兩個 cache manager寫兩份數據導致寫開銷較大,而且過WAN的寫延遲依然可能導致系統響應的瓶頸。

           

          第三種方案:Messaging based (AMQ) replication


          這 種方案下,引入了批量處理和隊列,用以減緩WAN的瓶頸出現,同時,把處理讀請求和復制邏輯從Server Array物理上就剝離開,避免了WAN情況惡化對節點讀取業務的影響。這種方案要較高的吞吐量和較低的延遲,讀/復制的分離保證了可以提供完備的消息分 發保證、沖突處理等特性;但是它較為復雜,而且還需要一個消息總線。

           

          有一些Ehcache特性應用較少或者比較邊緣化,沒有提到,例如對于JMX的支持;還有一些則是有類似的特性和介紹了,例如對于WEB的支持,請參見我這篇關于OSCache的解讀,其中的“web支持”一節有詳細的原理分析。

           

          最后,關于Ehcache的性能比對,下面這張圖來自Ehcache的創始人Greg Luck的blog

           

          put/get上Ehcache要500-1000倍快過Memcached。原因何在?他自己分析道:“In-process caching and asynchronous replication are a clear performance winner”。有關它詳細的內容還是請參閱他的blog吧。


          轉自:

          http://raychase.iteye.com/blog/1545906

          posted @ 2012-07-26 13:07 小胡子 閱讀(8366) | 評論 (0)編輯 收藏

           Windows 7,在使用 JGroups 的時候提示錯誤:

          java.lang.RuntimeException: the type of the stack (IPv6) and the user supplied addresses (IPv4) don't match: /xxx.xxx.xxx.xxx.

          在操作系統中禁用 IPv6 是沒用的,這個需要在 JVM 中禁用 IPv6 特性,加入以下參數即可:

          -Djava.net.preferIPv4Stack=true

          posted @ 2012-07-26 12:57 小胡子 閱讀(982) | 評論 (0)編輯 收藏

               摘要: Spring框架提供了構造Web應用程序的全能MVC模塊。Spring MVC分離了控制器、模型對象、分派器以及處理程序對象的角色,這種分離讓它們更容易進行制定。是一個標準的MVC框架。   那你猜一猜哪一部分應該是哪一部分? SpringMVC框架圖 SpringMVC接口解釋   DispatcherServlet接口:   Spring提供的前端...  閱讀全文

          posted @ 2012-07-24 13:52 小胡子 閱讀(17283) | 評論 (5)編輯 收藏

          看到別人的iphone壁紙那么好看,有的可以動,有的可以與應用程序結合使用,你想到沒有這種效果是怎么實現的,今天推薦給大家40個創造性的壁紙,你可以下載這些壁紙使用到你的應用程序當中,無論你是一個壁紙愛好者,還是設計者,下面的這些壁紙能夠為你的開發節省一些寶貴的時間,這些壁紙下載到電腦也可以使用,不受大小所限制,作為設計者必須使用其他方式,使壁紙的特性能夠與智能手機使用相關聯。

          Icons Skins 2 -Icons Skins 2是包括42種最適合您的應用程序圖標的最佳屏幕背景,他的分辨率大小是960×480

          圖標外觀2

          Shelf Backgrounds and Wallpapers  - 你可以自定義自己的主屏幕,450 +的發光效果,包括24 +圣誕發光效果。

          貨架背景和壁紙

          藍圖 -它看起來像一個iPhone的界面設計藍圖。

          藍圖

          Fresh Touch  -這是一套高清晰度的壁紙,。

          新鮮的觸摸

          Hanging -在這個壁紙上面可以放你的應用程序。

          掛

          Snazzy -時髦的壁紙讓你的iPhone主屏幕豐富多彩。

          時髦

          Doodle Jump -像回到學校的感覺?

          涂鴉跳轉

          Pirate Parrot  -海盜,一只鸚鵡,這些作為你的壁紙

          海盜鸚鵡

          Pencils  

          鉛筆

          Fire And Metal -看起來這個壁紙像在放火,呵呵

          消防和金屬

          Screws & Bricks  -像是回到車庫。

          螺絲磚

          Screws & Wood  -螺釘和木?是的,我們也有。

          螺絲及木

          Liege -注人?這種壁紙原來將即時貼到您的應用程序圖標

          列日

          媚俗 -女裝圖片完美的iPhone壁紙。

          媚俗

          DOTA 2 

          DOTA 2

          面料 -面料,簡單的織物壁紙作為你的iPhone主屏幕。

          布

          TRON  

          創新福星

          創新-輕便自行車 -看這個壁紙像什么

          創新福星 - 輕便自行車

          霓虹燈箭頭 -這幾乎就像是一個沒有電池排水位的動態壁紙。

          霓虹燈箭頭

          納米管 -這種壁紙與微觀管放在您的主屏幕,以容納您的應用程序圖標。

          納米管

          Grid 

          格

          塞爾達 -塞爾達在您的手機。記住他不是游戲,主屏幕壁紙!

          塞爾達

          PAC-MAN -這是Pac-Man的您的主屏幕上跑來跑去!

          PAC-MAN

          金剛 -記住金剛?是的,現在它可以為您的iPhone的壁紙。

          金剛

          挖挖 -讓另一個最喜歡的游戲添加到主屏幕,!

          掏挖

          超級瑪麗 

          超級馬里奧兄弟

          圣誕節 -當你看到這個背景時會想到當當響的聲音嗎

          圣誕節

          紅沙發 -照明和紅色的搭配

          紅沙發

          Cooliog - Cooliog增加您的應用程序圖標區的照明。

          Cooliog

          Pirate 

          海盜

          Wood Shelves

          木制棚架

          spargetts貨架

          spargetts貨架

          飛濺 

          濺

          工具箱HD -愛紅?工具箱的HD有鮮紅的貨架上

          “工具箱”HD

          貨架 -紅色為您太熱?然后再為這個寒冷的白色應用現成的壁紙。

          架

          4Shelves -這是一個的有趣trophycase應用架子

          4Shelves墻

           -行是微妙的,但它們不包含應用程序,你很難發現它們的存在。

          行

          書蟲 -喜歡你的iPhone 4革背景?試試這個壁紙,

          書呆子

          iPhone 4的紋理壁紙 - iPhone 4的紋理壁紙

          iPhone 4的紋理壁紙

          Grungy Walls -讓你的iPhone有重金屬的感覺。

          骯臟的墻壁

          3
          0
          (請您對文章做出評價)
          « 博主前一篇:把RSS訂閱到郵箱去

          原文:
          http://www.cnblogs.com/web8cn/archive/2012/07/19/2598745.html

          posted @ 2012-07-19 13:27 小胡子 閱讀(227) | 評論 (0)編輯 收藏

          引子

            “請寫一個Singleton。”面試官微笑著和我說。

            “這可真簡單。”我心里想著,并在白板上寫下了下面的Singleton實現:

           
          class Singleton  { 
            
          public: static Singleton& Instance() {  
            static Singleton singleton;  
            
          return singleton;   
          }  
            private: Singleton() { }; 
          };

            “那請你講解一下該實現的各組成。”面試官的臉上仍然帶著微笑。

            “首先要說的就是Singleton的構造函數。由于Singleton限制其類型實例有且只能有一個,因此我們應通過將構造函數設置為非公有 來保證其不會被用戶代碼隨意創建。而在類型實例訪問函數中,我們通過局部靜態變量達到實例僅有一個的要求。另外,通過該靜態變量,我們可以將該實例的創建 延遲到實例訪問函數被調用時才執行,以提高程序的啟動速度。”

           

          保護

            “說得不錯,而且更可貴的是你能注意到對構造函數進行保護。畢竟中間件代碼需要非常嚴謹才能防止用戶代碼的誤用。那么,除了構造函數以外,我們還需要對哪些組成進行保護?”

            “還需要保護的有拷貝構造函數,析構函數以及賦值運算符。或許,我們還需要考慮取址運算符。這是因為編譯器會在需要的時候為這些成員創建一個默認的實現。”

            “那你能詳細說一下編譯器會在什么情況下創建默認實現,以及創建這些默認實現的原因嗎?”面試官繼續問道。

            “在這些成員沒有被聲明的情況下,編譯器將使用一系列默認行為:對實例的構造就是分配一部分內存,而不對該部分內存做任何事情;對實例的拷貝也 僅僅是將原實例中的內存按位拷貝到新實例中;而賦值運算符也是對類型實例所擁有的各信息進行拷貝。而在某些情況下,這些默認行為不再滿足條件,那么編譯器 將嘗試根據已有信息創建這些成員的默認實現。這些影響因素可以分為幾種:類型所提供的相應成員,類型中的虛函數以及類型的虛基類。”

            “就以構造函數為例,如果當前類型的成員或基類提供了由用戶定義的構造函數,那么僅進行內存拷貝可能已經不是正確的行為。這是因為該成員的構造 函數可能包含了成員初始化,成員函數調用等眾多執行邏輯。此時編譯器就需要為這個類型生成一個默認構造函數,以執行對成員或基類構造函數的調用。另外,如 果一個類型聲明了一個虛函數,那么編譯器仍需要生成一個構造函數,以初始化指向該虛函數表的指針。如果一個類型的各個派生類中擁有一個虛基類,那么編譯器 同樣需要生成構造函數,以初始化該虛基類的位置。這些情況同樣需要在拷貝構造函數中考慮:如果一個類型的成員變量擁有一個拷貝構造函數,或者其基類擁有一 個拷貝構造函數,位拷貝就不再滿足要求了,因為拷貝構造函數內可能執行了某些并不是位拷貝的邏輯。同時如果一個類型聲明了虛函數,拷貝構造函數需要根據目 標類型初始化虛函數表指針。如基類實例經過拷貝后,其虛函數表指針不應指向派生類的虛函數表。同理,如果一個類型的各個派生類中擁有一個虛派生,那么編譯 器也應為其生成拷貝構造函數,以正確設置各個虛基類的偏移。”

            “當然,析構函數的情況則略為簡單一些:只需要調用其成員的析構函數以及基類的析構函數即可,而不需要再考慮對虛基類偏移的設置及虛函數表指針的設置。”

            “在這些默認實現中,類型實例的各個原生類型成員并沒有得到初始化的機會。但是這一般被認為是軟件開發人員的責任,而不是編譯器的責任。”說完這些,我長出一口氣,心里也暗自慶幸曾經研究過該部分內容。

            “你剛才提到需要考慮保護取址運算符,是嗎?我想知道。”

            “好的。首先要聲明的是,幾乎所有的人都會認為對取址運算符的重載是邪惡的。甚至說,boost為了防止該行為所產生的錯誤更是提供了 addressof()函數。而另一方面,我們需要討論用戶為什么要用取址運算符。Singleton所返回的常常是一個引用,對引用進行取址將得到相應 類型的指針。而從語法上來說,引用和指針的最大區別在于是否可以被delete關鍵字刪除以及是否可以為NULL。但是Singleton返回一個引用也 就表示其生存期由非用戶代碼所管理。因此使用取址運算符獲得指針后又用delete關鍵字刪除Singleton所返回的實例明顯是一個用戶錯誤。綜上所 述,通過將取址運算符設置為私有沒有多少意義。”

           

          重用

            “好的,現在我們換個話題。如果我現在有幾個類型都需要實現為Singleton,那我應怎樣使用你所編寫的這段代碼呢?”

            剛剛還在洋洋自得的我恍然大悟:這個Singleton實現是無法重用的。沒辦法,只好一邊想一邊說:“一般來說,較為流行的重用方法一共有三 種:組合、派生以及模板。首先可以想到的是,對Singleton的重用僅僅是對Instance()函數的重用,因此通過從Singleton派生以繼 承該函數的實現是一個很好的選擇。而Instance()函數如果能根據實際類型更改返回類型則更好了。因此奇異遞歸模板(CRTP,The Curiously Recurring Template Pattern)模式則是一個非常好的選擇。”于是我在白板上飛快地寫下了下面的代碼:

           1 template <typename T>  2 class Singleton  3 {  4 public:  5     static T& Instance()  6     {  7         static T s_Instance;  8         return s_Instance;  9     } 10  11 protected: 12     Singleton(void) {} 13     ~Singleton(void) {} 14  15 private: 16     Singleton(const Singleton& rhs) {} 17     Singleton& operator = (const Singleton& rhs) {} 18 };

            同時我也在白板上寫下了對該Singleton實現進行重用的方法:

          1 class SingletonInstance : public Singleton<SingletonInstance>…

            “在需要重用該Singleton實現時,我們僅僅需要從Singleton派生并將Singleton的泛型參數設置為該類型即可。”

           

          生存期管理

            “我看你在實現中使用了靜態變量,那你是否能介紹一下上面Singleton實現中有關生存期的一些特征嗎?畢竟生存期管理也是編程中的一個重要話題。”面試官提出了下一個問題。

            “嗯,讓我想一想。我認為對Singleton的生存期特性的討論需要分為兩個方面:Singleton內使用的靜態變量的生存期以及 Singleton外在用戶代碼中所表現的生存期。Singleton內使用的靜態變量是一個局部靜態變量,因此只有在Singleton的 Instance()函數被調用時其才會被創建,從而擁有了延遲初始化(Lazy)的效果,提高了程序的啟動性能。同時該實例將生存至程序執行完畢。而就 Singleton的用戶代碼而言,其生存期貫穿于整個程序生命周期,從程序啟動開始直到程序執行完畢。當然,Singleton在生存期上的一個缺陷就 是創建和析構時的不確定性。由于Singleton實例會在Instance()函數被訪問時被創建,因此在某處新添加的一處對Singleton的訪問 將可能導致Singleton的生存期發生變化。如果其依賴于其它組成,如另一個Singleton,那么對它們的生存期進行管理將成為一個災難。甚至可 以說,還不如不用Singleton,而使用明確的實例生存期管理。”

            “很好,你能提到程序初始化及關閉時單件的構造及析構順序的不確定可能導致致命的錯誤這一情況。可以說,這是通過局部靜態變量實現 Singleton的一個重要缺點。而對于你所提到的多個Singleton之間相互關聯所導致的生存期管理問題,你是否有解決該問題的方法呢?”

            我突然間意識到自己給自己出了一個難題:“有,我們可以將Singleton的實現更改為使用全局靜態變量,并將這些全局靜態變量在文件中按照特定順序排序即可。”

            “但是這樣的話,靜態變量將使用eager initialization的方式完成初始化,可能會對性能影響較大。其實,我想聽你說的是,對于具有關聯的兩個Singleton,對它們進行使用的 代碼常常局限在同一區域內。該問題的一個解決方法常常是將對它們進行使用的管理邏輯實現為Singleton,而在內部邏輯中對它們進行明確的生存期管 理。但不用擔心,因為這個答案也過于經驗之談。那么下一個問題,你既然提到了全局靜態變量能解決這個問題,那是否可以講解一下全局靜態變量的生命周期是怎 樣的呢?”

            “編譯器會在程序的main()函數執行之前插入一段代碼,用來初始化全局變量。當然,靜態變量也包含在內。該過程被稱為靜態初始化。”

            “嗯,很好。使用全局靜態變量實現Singleton的確會對性能造成一定影響。但是你是否注意到它也有一定的優點呢?”

            見我許久沒有回答,面試官主動幫我解了圍:“是線程安全性。由于在靜態初始化時用戶代碼還沒有來得及執行,因此其常常處于單線程環境下,從而保 證了Singleton真的只有一個實例。當然,這并不是一個好的解決方法。所以,我們來談談Singleton的多線程實現吧。”

           

          多線程

            “首先請你寫一個線程安全的Singleton實現。”

            我拿起筆,在白板上寫下早已爛熟于心的多線程安全實現:

           1 template <typename T>  2 class Singleton  3 {  4 public:  5     static T& Instance()  6     {  7         if (m_pInstance == NULL)  8         {  9             Lock lock; 10             if (m_pInstance == NULL) 11             { 12                 m_pInstance = new T(); 13                 atexit(Destroy); 14             } 15             return *m_pInstance; 16         } 17         return *m_pInstance; 18     } 19  20 protected: 21     Singleton(void) {} 22     ~Singleton(void) {} 23  24 private: 25     Singleton(const Singleton& rhs) {} 26     Singleton& operator = (const Singleton& rhs) {} 27  28     void Destroy() 29     { 30         if (m_pInstance != NULL) 31             delete m_pInstance; 32         m_pInstance = NULL; 33     } 34  35     static T* volatile m_pInstance; 36 }; 37  38 template <typename T> 39 T* Singleton<T>::m_pInstance = NULL;

            “寫得很精彩。那你是否能逐行講解一下你寫的這個Singleton實現呢?”

            “好的。首先,我使用了一個指針記錄創建的Singleton實例,而不再是局部靜態變量。這是因為局部靜態變量可能在多線程環境下出現問題。”

            “我想插一句話,為什么局部靜態變量會在多線程環境下出現問題?”

            “這是由局部靜態變量的實際實現所決定的。為了能滿足局部靜態變量只被初始化一次的需求,很多編譯器會通過一個全局的標志位記錄該靜態變量是否已經被初始化的信息。那么,對靜態變量進行初始化的偽碼就變成下面這個樣子:”。

          1 bool flag = false; 2 if (!flag) 3 { 4     flag = true; 5     staticVar = initStatic(); 6 }

            “那么在第一個線程執行完對flag的檢查并進入if分支后,第二個線程將可能被啟動,從而也進入if分支。這樣,兩個線程都將執行對靜態變量 的初始化。因此在這里,我使用了指針,并在對指針進行賦值之前使用鎖保證在同一時間內只能有一個線程對指針進行初始化。同時基于性能的考慮,我們需要在每 次訪問實例之前檢查指針是否已經經過初始化,以避免每次對Singleton的訪問都需要請求對鎖的控制權。”

            “同時,”我咽了口口水繼續說,“因為new運算符的調用分為分配內存、調用構造函數以及為指針賦值三步,就像下面的構造函數調用:”

          1 SingletonInstance pInstance = new SingletonInstance();

            “這行代碼會轉化為以下形式:”

          1 SingletonInstance pHeap = __new(sizeof(SingletonInstance)); 2 pHeap->SingletonInstance::SingletonInstance(); 3 SingletonInstance pInstance = pHeap;

            “這樣轉換是因為在C++標準中規定,如果內存分配失敗,或者構造函數沒有成功執行, new運算符所返回的將是空。一般情況下,編譯器不會輕易調整這三步的執行順序,但是在滿足特定條件時,如構造函數不會拋出異常等,編譯器可能出于優化的 目的將第一步和第三步合并為同一步:”

          1 SingletonInstance pInstance = __new(sizeof(SingletonInstance)); 2 pInstance->SingletonInstance::SingletonInstance();

            “這樣就可能導致其中一個線程在完成了內存分配后就被切換到另一線程,而另一線程對Singleton的再次訪問將由于pInstance已經 賦值而越過if分支,從而返回一個不完整的對象。因此,我在這個實現中為靜態成員指針添加了volatile關鍵字。該關鍵字的實際意義是由其修飾的變量 可能會被意想不到地改變,因此每次對其所修飾的變量進行操作都需要從內存中取得它的實際值。它可以用來阻止編譯器對指令順序的調整。只是由于該關鍵字所提 供的禁止重排代碼是假定在單線程環境下的,因此并不能禁止多線程環境下的指令重排。”

            “最后來說說我對atexit()關鍵字的使用。在通過new關鍵字創建類型實例的時候,我們同時通過atexit()函數注冊了釋放該實例的 函數,從而保證了這些實例能夠在程序退出前正確地析構。該函數的特性也能保證后被創建的實例首先被析構。其實,對靜態類型實例進行析構的過程與前面所提到 的在main()函數執行之前插入靜態初始化邏輯相對應。”

           

          引用還是指針

            “既然你在實現中使用了指針,為什么仍然在Instance()函數中返回引用呢?”面試官又拋出了新的問題。

            “這是因為Singleton返回的實例的生存期是由Singleton本身所決定的,而不是用戶代碼。我們知道,指針和引用在語法上的最大區 別就是指針可以為NULL,并可以通過delete運算符刪除指針所指的實例,而引用則不可以。由該語法區別引申出的語義區別之一就是這些實例的生存期意 義:通過引用所返回的實例,生存期由非用戶代碼管理,而通過指針返回的實例,其可能在某個時間點沒有被創建,或是可以被刪除的。但是這兩條 Singleton都不滿足,因此在這里,我使用指針,而不是引用。”

            “指針和引用除了你提到的這些之外,還有其它的區別嗎?”

            “有的。指針和引用的區別主要存在于幾個方面。從低層次向高層次上來說,分為編譯器實現上的,語法上的以及語義上的區別。就編譯器的實現來說, 聲明一個引用并沒有為引用分配內存,而僅僅是為該變量賦予了一個別名。而聲明一個指針則分配了內存。這種實現上的差異就導致了語法上的眾多區別:對引用進 行更改將導致其原本指向的實例被賦值,而對指針進行更改將導致其指向另一個實例;引用將永遠指向一個類型實例,從而導致其不能為NULL,并由于該限制而 導致了眾多語法上的區別,如dynamic_cast對引用和指針在無法成功進行轉化時的行為不一致。而就語義而言,前面所提到的生存期語義是一個區別, 同時一個返回引用的函數常常保證其返回結果有效。一般來說,語義區別的根源常常是語法上的區別,因此上面的語義區別僅僅是列舉了一些例子,而真正語義上的 差別常常需要考慮它們的語境。”

            “你在前面說到了你的多線程內部實現使用了指針,而返回類型是引用。在編寫過程中,你是否考慮了實例構造不成功的情況,如new運算符運行失敗?”

            “是的。在和其它人進行討論的過程中,大家對于這種問題有各自的理解。首先,對一個實例的構造將可能在兩處拋出異常:new運算符的執行以及構 造函數拋出的異常。對于new運算符,我想說的是幾點。對于某些操作系統,例如Windows,其常常使用虛擬地址,因此其運行常常不受物理內存實際大小 的限制。而對于構造函數中拋出的異常,我們有兩種策略可以選擇:在構造函數內對異常進行處理,以及在構造函數之外對異常進行處理。在構造函數內對異常進行 處理可以保證類型實例處于一個有效的狀態,但一般不是我們想要的實例狀態。這樣一個實例會導致后面對它的使用更為繁瑣,例如需要更多的處理邏輯或再次導致 程序執行異常。反過來,在構造函數之外對異常進行處理常常是更好的選擇,因為軟件開發人員可以根據產生異常時所構造的實例的狀態將一定范圍內的各個變量更 改為合法的狀態。舉例來說,我們在一個函數中嘗試創建一對相互關聯的類型實例,那么在一個實例的構造函數拋出了異常時,我們不應該在構造函數里對該實例的 狀態進行維護,因為前一個實例的構造是按照后一個實例會正常創建來進行的。相對來說,放棄后一個實例,并將前一個實例刪除是一個比較好的選擇。”

            我在白板上比劃了一下,繼續說到:“我們知道,異常有兩個非常明顯的缺陷:效率,以及對代碼的污染。在太小的粒度中使用異常,就會導致異常使用次數的增加,對于效率以及代碼的整潔型都是傷害。同樣地,對拷貝構造函數等組成常常需要使用類似的原則。”

            “反過來說,Singleton的使用也可以保持著這種原則。Singleton僅僅是一個包裝好的全局實例,對其的創建如果一旦不成功,在較高層次上保持正常狀態同樣是一個較好的選擇。”

           

          Anti-Patten

            “既然你提到了Singleton僅僅是一個包裝好的全局變量,那你能說說它和全局變量的相同與不同么?”

            “單件可以說是全局變量的替代品。其擁有全局變量的眾多特點:全局可見且貫穿應用程序的整個生命周期。除此之外,單件模式還擁有一些全局變量所 不具有的性質:同一類型的對象實例只能有一個,同時適當的實現還擁有延遲初始化(Lazy)的功能,可以避免耗時的全局變量初始化所導致的啟動速度不佳等 問題。要說明的是,Singleton的最主要目的并不是作為一個全局變量使用,而是保證類型實例有且僅有一個。它所具有的全局訪問特性僅僅是它的一個副 作用。但正是這個副作用使它更類似于包裝好的全局變量,從而允許各部分代碼對其直接進行操作。軟件開發人員需要通過仔細地閱讀各部分對其進行操作的代碼才 能了解其真正的使用方式,而不能通過接口得到組件依賴性等信息。如果Singleton記錄了程序的運行狀態,那么該狀態將是一個全局狀態。各個組件對其 進行操作的調用時序將變得十分重要,從而使各個組件之間存在著一種隱式的依賴。”

            “從語法上來講,首先Singleton模式實際上將類型功能與類型實例個數限制的代碼混合在了一起,違反了SRP。其次Singleton模式在Instance()函數中將創建一個確定的類型,從而禁止了通過多態提供另一種實現的可能。”

            “但是從系統的角度來講,對Singleton的使用則是無法避免的:假設一個系統擁有成百上千個服務,那么對它們的傳遞將會成為系統的一個災 難。從微軟所提供的眾多類庫上來看,其常常提供一種方式獲得服務的函數,如GetService()等。另外一個可以減輕Singleton模式所帶來不 良影響的方法則是為Singleton模式提供無狀態或狀態關聯很小的實現。”

            “也就是說,Singleton本身并不是一個非常差的模式,對其使用的關鍵在于何時使用它并正確的使用它。”

            面試官抬起手腕看了看時間:“好了,時間已經到了。你的C++功底已經很好了。我相信,我們會在不久的將來成為同事。”

           

          筆者注:這本是Writing Patterns Line by Line的一篇文章,但最后想想,寫模式的人太多了,我還是省省吧。。。

          下一篇回歸WPF,環境剛好。可能中間穿插些別的內容,比如HTML5,JS,安全等等。

          頭一次寫小品文,不知道效果是不是好。因為這種文章的特點是知識點分散,而且隱藏在文章的每一句話中。。。好處就是寫起來輕松,呵呵。。。


           


           

          posted @ 2012-07-19 13:09 小胡子 閱讀(207) | 評論 (0)編輯 收藏

          錯誤
          Server Tomcat v6.0 Server at localhost was unable to start within 45 seconds. If the server requires more time, try increasing the timeout in the server editor.
          修改 workspace\.metadata\.plugins\org.eclipse.wst.server.core\servers.xml文件。
          <servers>
          <server hostname="localhost" id="JBoss v5.0 at localhost" name="JBoss v5.0 at
          localhost" runtime-id="JBoss v5.0" server-type="org.eclipse.jst.server.generic.jboss5"
          server-type-id="org.eclipse.jst.server.generic.jboss5" start-timeout="1000" stop-
          timeout="15" timestamp="0">
          <map jndiPort="1099" key="generic_server_instance_properties" port="8090"
          serverAddress="127.0.0.1" serverConfig="default"/>
          </server>
          </servers>
          把 start-timeout="45" 改為 start-timeout="1000" 或者更長
          重啟eclipse就可以了。
          這個原因就是:啟動tomcat需要的時間比45秒大了,Eclipse會判斷tomcat在默認的時間是否啟動了,如果在默認45秒沒有啟動就會報錯了。

          posted @ 2012-07-18 15:36 小胡子 閱讀(4488) | 評論 (1)編輯 收藏

          在看java performance的時候看到一些同步的名詞,偏向鎖、輕量級鎖之類的,于是想先了解一下虛擬機中的鎖機制,于是找到了這篇文章。發現是《深入理解Java虛擬機:JVM高級特性與最佳實踐》一書的章節,講得干脆好懂,差點就有要去買一本的沖動-----還是明天吧。以下是文章轉載:

          ------------------------------------------------------------------------------------------------------------------------------------------------------------------

          高效并發是JDK1.6的一個重要主題,HotSpot虛擬機開發團隊在這個版本上花費了大量的精力去實現各種鎖優化技術,如適應性自旋 (AdaptiveSpinning)、鎖削除(Lock Elimination)、鎖膨脹(Lock Coarsening)、輕量級鎖(LightweightLocking)、偏向鎖(BiasedLocking)等,這些技術都是為了在線程之間更高 效地共享數據,以及解決競爭問題,從而提高程序的執行效率。


          13.3.1 自旋鎖與自適應自旋

            前面我們討論互斥同步的時候,提到了互斥同步對性能最大的影響是阻塞的實現,掛起線程和恢復線程的操作都需要轉入內核態中完成,這些操作給系統的并發 性能帶來了很大的壓力。同時,虛擬機的開發團隊也注意到在許多應用上,共享數據的鎖定狀態只會持續很短的一段時間,為了這段時間去掛起和恢復線程并不值 得。如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時并行執行,我們就可以讓后面請求鎖的那個線程“稍等一會”,但不放棄處理器的執行時間,看 看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待,我們只須讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。
            自旋鎖在JDK 1.4.2中就已經引入,只不過默認是關閉的,可以使用-XX:+UseSpinning參數來開啟,在JDK1.6中就已經改為默認開啟了。自旋等待不 能代替阻塞,且先不說對處理器數量的要求,自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時間的,所以如果鎖被占用的時間很短,自旋等待的效 果就會非常好,反之如果鎖被占用的時間很長,那么自旋的線程只會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來性能的浪費。因此自旋等待的時間 必須要有一定的限度,如果自旋超過了限定的次數仍然沒有成功獲得鎖,就應當使用傳統的方式去掛起線程了。自旋次數的默認值是10次,用戶可以使用參數 -XX:PreBlockSpin來更改。
            在JDK1.6中引入了自適應的自旋鎖。自適應意味著自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同 一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運行中,那么虛擬機就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更 長的時間,比如100個循環。另一方面,如果對于某個鎖,自旋很少成功獲得過,那在以后要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。有了 自適應自旋,隨著程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測就會越來越準確,虛擬機就會變得越來越“聰明”了。

          13.3.2 鎖削除

            鎖削除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行削除。鎖削除的主要判定依據來源于逃逸分析 的數據支持(第11章已經講解過逃逸分析技術),如果判斷到一段代碼中,在堆上的所有數據都不會逃逸出去被其他線程訪問到,那就可以把它們當作棧上數據對 待,認為它們是線程私有的,同步加鎖自然就無須進行。
            也許讀者會有疑問,變量是否逃逸,對于虛擬機來說需要使用數據流分析來確定,但是程序員自己應該是很清楚的,怎么會在明知道不存在數據爭用的情況下要 求同步呢?答案是有許多同步措施并不是程序員自己加入的,同步的代碼在Java程序中的普遍程度也許超過了大部分讀者的想象。我們來看看下面代碼清單 13-6中的例子,這段非常簡單的代碼僅僅是輸出三個字符串相加的結果,無論是源碼字面上還是程序語義上都沒有同步。

            代碼清單 13-6 一段看起來沒有同步的代碼
          Java代碼  收藏代碼
          1. public String concatString(String s1, String s2, String s3) {  
          2.     return s1 + s2 + s3;  
          3. }  
          1. public String concatString(String s1, String s2, String s3) {  
          2.     return s1 + s2 + s3;  
          3. }  
             我們也知道,由于String是一個不可變的類,對字符串的連接操作總是通過生成新的String對象來進行的,因此Javac編譯器會對String 連接做自動優化。在JDK 1.5之前,會轉化為StringBuffer對象的連續append()操作,在JDK1.5及以后的版本中,會轉化為StringBuilder對象 的連續append()操作。即代碼清單13-6中的代碼可能會變成代碼清單13-7的樣子 。

            代碼清單 13-7 Javac轉化后的字符串連接操作
          Java代碼  收藏代碼
          1. public String concatString(String s1, String s2, String s3) {  
          2.     StringBuffer sb = new StringBuffer();  
          3.     sb.append(s1);  
          4.     sb.append(s2);  
          5.     sb.append(s3);  
          6.     return sb.toString();  
          7. }  
          1. public String concatString(String s1, String s2, String s3) {  
          2.     StringBuffer sb = new StringBuffer();  
          3.     sb.append(s1);  
          4.     sb.append(s2);  
          5.     sb.append(s3);  
          6.     return sb.toString();  
          7. }  
          (注1:實事求是地說,既然談到鎖削除與逃逸分析,那虛擬機就不可能是JDK 1.5之前的版本,所以實際上會轉化為非線程安全的StringBuilder來完成字符串拼接,并不會加鎖。但是這也不影響筆者用這個例子證明Java對象中同步的普遍性。)

            現在大家還認為這段代碼沒有涉及同步嗎?每個StringBuffer.append()方法中都有一個同步塊,鎖就是sb對象。虛擬機觀察變量 sb,很快就會發現它的動態作用域被限制在concatString()方法內部。也就是sb的所有引用永遠不會“逃逸”到concatString() 方法之外,其他線程無法訪問到它,所以這里雖然有鎖,但是可以被安全地削除掉,在即時編譯之后,這段代碼就會忽略掉所有的同步而直接執行了。

          13.3.3 鎖膨脹

            原則上,我們在編寫代碼的時候,總是推薦將同步塊的作用范圍限制得盡量小——只在共享數據的實際作用域中才進行同步,這樣是為了使得需要同步的操作數量盡可能變小,如果存在鎖競爭,那等待鎖的線程也能盡快地拿到鎖。
            大部分情況下,上面的原則都是正確的,但是如果一系列的連續操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。
            上面代碼清單13-7中連續的append()方法就屬于這類情況。如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的范圍 擴展(膨脹)到整個操作序列的外部,以代碼清單13-7為例,就是擴展到第一個append()操作之前直至最后一個append()操作之后,這樣只需 要加鎖一次就可以了。

          13.3.4 輕量級鎖

            輕量級鎖是JDK1.6之中加入的新型鎖機制,它名字中的“輕量級”是相對于使用操作系統互斥量來實現的傳統鎖而言的,因此傳統的鎖機制就被稱為“重 量級”鎖。首先需要強調一點的是,輕量級鎖并不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的 性能消耗。
            要理解輕量級鎖,以及后面會講到的偏向鎖的原理和運作過程,必須從HotSpot虛擬機的對象(對象頭部分)的內存布局開始介紹。HotSpot虛擬 機的對象頭(ObjectHeader)分為兩部分信息,第一部分用于存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡 (Generational GCAge)等,這部分數據的長度在32位和64位的虛擬機中分別為32個和64個Bits,官方稱它為“MarkWord”,它是實現輕量級鎖和偏向鎖 的關鍵。另外一部分用于存儲指向方法區對象類型數據的指針,如果是數組對象的話,還會有一個額外的部分用于存儲數組長度。
            對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,MarkWord被設計成一個非固定的數據結構以便在極小的空間內存 儲盡量多的信息,它會根據對象的狀態復用自己的存儲空間。例如在32位的HotSpot虛擬機中對象未被鎖定的狀態下,MarkWord的32個Bits 空間中的25Bits用于存儲對象哈希碼(HashCode),4Bits用于存儲對象分代年齡,2Bits用于存儲鎖標志位,1Bit固定為0,在其他 狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容如表13-1所示。

            表13-1 HotSpot虛擬機對象頭Mark Word
          存儲內容 標志位 狀態
          對象哈希碼、對象分代年齡 01 未鎖定
          指向鎖記錄的指針 00 輕量級鎖定
          指向重量級鎖的指針 10 膨脹(重量級鎖定)
          空,不需要記錄信息 11 GC標記
          偏向線程ID、偏向時間戳、對象分代年齡 01 可偏向

            簡單地介紹完了對象的內存布局,我們把話題返回到輕量級鎖的執行過程上。在代碼進入同步塊的時候,如果此同步對象沒有被鎖定(鎖標志位為“01”狀 態),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的MarkWord的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced MarkWord),這時候線程堆棧與對象頭的狀態如圖13-3所示。


            圖13-3 輕量級鎖CAS操作之前堆棧與對象的狀態


            然后,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向LockRecord的指針。如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標志位(MarkWord的最后兩個Bits)將轉變為“00”,即表示此對象處于輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如圖 13-4所示。


            圖13-4 輕量級鎖CAS操作之后堆棧與對象的狀態

          注2:圖13-3和圖13-4來源于HotSpot虛擬機的一位Senior Staff Engineer——Paul Hohensee所寫的PPT《The Hotspot Java Virtual Machine》

            如果這個更新操作失敗了,虛擬機首先會檢查對象的MarkWord是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直 接進入同步塊繼續執行,否則說明這個鎖對象已經被其他線程搶占了。如果有兩條以上的線程爭用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖,鎖標志的 狀態值變為“10”,MarkWord中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態。
            上面描述的是輕量級鎖的加鎖過程,它的解鎖過程也是通過CAS操作來進行的,如果對象的MarkWord仍然指向著線程的鎖記錄,那就用CAS操作把 對象當前的Mark Word和線程中復制的Displaced MarkWord替換回來,如果替換成功,整個同步過程就完成了。如果替換失敗,說明有其他線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線 程。
            輕量級鎖能提升程序同步性能的依據是“對于絕大部分的鎖,在整個同步周期內都是不存在競爭的”,這是一個經驗數據。如果沒有競爭,輕量級鎖使用CAS 操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。

          13.3.5 偏向鎖

            偏向鎖也是JDK1.6中引入的一項鎖優化,它的目的是消除數據在無競爭情況下的同步原語,進一步提高程序的運行性能。如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連CAS操作都不做了。
            偏向鎖的“偏”,就是偏心的“偏”、偏袒的“偏”。它的意思是這個鎖會偏向于第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。
            如果讀者讀懂了前面輕量級鎖中關于對象頭MarkWord與線程之間的操作過程,那偏向鎖的原理理解起來就會很簡單。假設當前虛擬機啟用了偏向鎖(啟 用參數-XX:+UseBiasedLocking,這是JDK1.6的默認值),那么,當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標志位 設為“01”,即偏向模式。同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的MarkWord之中,如果CAS操作成功,持有偏向鎖的線程以后 每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作(例如Locking、Unlocking及對Mark Word的Update等)。
            當有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束。根據鎖對象目前是否處于被鎖定的狀態,撤銷偏向(RevokeBias)后恢復到未鎖定 (標志位為“01”)或輕量級鎖定(標志位為“00”)的狀態,后續的同步操作就如上面介紹的輕量級鎖那樣執行。偏向鎖、輕量級鎖的狀態轉化及對象 Mark Word的關系如圖13-5所示。


            圖13-5 偏向鎖、輕量級鎖的狀態轉化及對象Mark Word的關系


            偏向鎖可以提高帶有同步但無競爭的程序性能。它同樣是一個帶有效益權衡(TradeOff)性質的優化,也就是說它并不一定總是對程序運行有 利,如果程序中大多數的鎖都總是被多個不同的線程訪問,那偏向模式就是多余的。在具體問題具體分析的前提下,有時候使用參數 -XX:-UseBiasedLocking來禁止偏向鎖優化反而可以提升性能。


          作者:icyfenix@gmail.com
          來源:《深入理解Java虛擬機:JVM高級特性與最佳實踐》

          posted @ 2012-07-17 21:09 小胡子 閱讀(1068) | 評論 (0)編輯 收藏

          再談javascript圖片預加載技術

          比onload更快獲取圖片尺寸

          文章更新:2011-05-31
          lightbox類效果為了讓圖片居中顯示而使用預加載,需要等待完全加載完畢才能顯示,體驗不佳(如filick相冊的全屏效果)。javascript無法獲取img文件頭數據,真的是這樣嗎?本文通過一個巧妙的方法讓javascript獲取它。

          這是大部分人使用預加載獲取圖片大小的例子:

          01var imgLoad = function (url, callback) {
          02    var img = new Image();
          03 
          04    img.src = url;
          05    if (img.complete) {
          06        callback(img.width, img.height);
          07    } else {
          08        img.onload = function () {
          09            callback(img.width, img.height);
          10            img.onload = null;
          11        };
          12    };
          13 
          14};

          可以看到上面必須等待圖片加載完畢才能獲取尺寸,其速度不敢恭維,我們需要改進。

          web應用程序區別于桌面應用程序,響應速度才是最好的用戶體驗。如果想要速度與優雅兼得,那就必須提前獲得圖片尺寸,如何在圖片沒有加載完畢就能獲取圖片尺寸?

          十多年的上網經驗告訴我:瀏覽器在加載圖片的時候你會看到圖片會先占用一塊地然后才慢慢加載完畢,并且不需要預設width與height屬性,因 為瀏覽器能夠獲取圖片的頭部數據。基于此,只需要使用javascript定時偵測圖片的尺寸狀態便可得知圖片尺寸就緒的狀態。

          當然實際中會有一些兼容陷阱,如width與height檢測各個瀏覽器的不一致,還有webkit new Image()建立的圖片會受以處在加載進程中同url圖片影響,經過反復測試后的最佳處理方式:

          01// 更新:
          02// 05.27: 1、保證回調執行順序:error > ready > load;2、回調函數this指向img本身
          03// 04-02: 1、增加圖片完全加載后的回調 2、提高性能
          04 
          05/**
          06 * 圖片頭數據加載就緒事件 - 更快獲取圖片尺寸
          07 * @version 2011.05.27
          08 * @author  TangBin
          10 * @param   {String}    圖片路徑
          11 * @param   {Function}  尺寸就緒
          12 * @param   {Function}  加載完畢 (可選)
          13 * @param   {Function}  加載錯誤 (可選)
          14 * @example imgReady('
          15        alert('size ready: width=' + this.width + '; height=' + this.height);
          16    });
          17 */
          18var imgReady = (function () {
          19    var list = [], intervalId = null,
          20 
          21    // 用來執行隊列
          22    tick = function () {
          23        var i = 0;
          24        for (; i < list.length; i++) {
          25            list[i].end ? list.splice(i--, 1) : list[i]();
          26        };
          27        !list.length && stop();
          28    },
          29 
          30    // 停止所有定時器隊列
          31    stop = function () {
          32        clearInterval(intervalId);
          33        intervalId = null;
          34    };
          35 
          36    return function (url, ready, load, error) {
          37        var onready, width, height, newWidth, newHeight,
          38            img = new Image();
          39         
          40        img.src = url;
          41 
          42        // 如果圖片被緩存,則直接返回緩存數據
          43        if (img.complete) {
          44            ready.call(img);
          45            load && load.call(img);
          46            return;
          47        };
          48         
          49        width = img.width;
          50        height = img.height;
          51         
          52        // 加載錯誤后的事件
          53        img.onerror = function () {
          54            error && error.call(img);
          55            onready.end = true;
          56            img = img.onload = img.onerror = null;
          57        };
          58         
          59        // 圖片尺寸就緒
          60        onready = function () {
          61            newWidth = img.width;
          62            newHeight = img.height;
          63            if (newWidth !== width || newHeight !== height ||
          64                // 如果圖片已經在其他地方加載可使用面積檢測
          65                newWidth * newHeight > 1024
          66            ) {
          67                ready.call(img);
          68                onready.end = true;
          69            };
          70        };
          71        onready();
          72         
          73        // 完全加載完畢的事件
          74        img.onload = function () {
          75            // onload在定時器時間差范圍內可能比onready快
          76            // 這里進行檢查并保證onready優先執行
          77            !onready.end && onready();
          78         
          79            load && load.call(img);
          80             
          81            // IE gif動畫會循環執行onload,置空onload即可
          82            img = img.onload = img.onerror = null;
          83        };
          84 
          85        // 加入隊列中定期執行
          86        if (!onready.end) {
          87            list.push(onready);
          88            // 無論何時只允許出現一個定時器,減少瀏覽器性能損耗
          89            if (intervalId === null) intervalId = setInterval(tick, 40);
          90        };
          91    };
          92})();

          調用例子:

          1imgReady(', function () {
          2    alert('size ready: width=' + this.width + '; height=' + this.height);
          3});

          是不是很簡單?這樣的方式獲取攝影級別照片尺寸的速度往往是onload方式的幾十多倍,而對于web普通(800×600內)瀏覽級別的圖片能達到秒殺效果。看了這個再回憶一下你見過的web相冊,是否絕大部分都可以重構一下呢?好了,請觀賞令人愉悅的 DEMO :

          http://www.planeart.cn/demo/imgReady/

          (通過測試的瀏覽器:Chrome、Firefox、Safari、Opera、IE6、IE7、IE8)

          planeArt.cn原創文章,原文地址:http://www.planeart.cn/?p=1121

          其他文章:

          1、再談IE6之Fixed定位
          2、簡易的全屏透明遮罩(lightBox)解決方案

          這篇文章發表于10/03/2011 (星期四)在下午 2:33,所屬分類為javascript。 您可以通過RSS 2.0跟蹤這篇文章的評論。 您可以發表回復,或從您的網站發布引用通告

          posted @ 2012-07-15 20:28 小胡子 閱讀(142) | 評論 (0)編輯 收藏

          http://www.cnblogs.com/lhb25/archive/2011/03/09/1964344.html

          您可能還喜歡



            對于Web設計和開發人員來說,CSS是非常重要的一部分,隨著越來越多的瀏覽器對CSS3的支持及不斷完善,設計師和開發者們有了更多的選 擇。如今,用純CSS就可以實現各種各樣很酷的效果,甚至是動畫。今天這篇文章向大家推薦24款非常優秀的CSS3工具,為了獲得更佳的效果,請在 Chrome 4+, Safari 4+, Firefox 3.6+, IE9+, Opera 10.5+版本瀏覽器中瀏覽如下在線工具。

          1.CSS3 Pie

          使用CSS3 Pie可以讓IE6至IE8版本實現大多數的CSS3修飾特性,如圓角、陰影、漸變等等。

          2. CSS3 Click Chart

          非常好的CSS3效果演示,提供了示例代碼。

          3.CSS3 Please!

          非常帥的一款CSS3工具,可修改代碼,即時預覽。

          4.CSS3 Button Maker

          一個非常不錯的CSS3按鈕制作工具。

          5.CSS3 Generator

          非常不錯的CSS3代碼生成器,帶預覽效果。

          6.CSS3 Menu

          非常不錯的CSS3菜單制作工具。

          7.CSS3 Gradients

          一款非常棒的CSS3漸變效果演示工具。

          8.CSS3 Cheat Sheet

          一份不錯的CSS3屬性速查手冊(PDF格式)。

          9.CSS3 Selector Test

          非常不錯的CSS3選擇器測試工具

          10.CSS3 Transforms

          一款強大的CSS3旋轉動畫效果演示工具,即時生成代碼。

          11.CSS3 Preview

          CSS3特性介紹及效果預覽。

          12.CSS3 Generator

          一款非常不錯的CSS3代碼生成工具。

          13.CSS3 Color Names

          CSS3顏色命名對照表。

          14.Toggle CSS3 Bookmarklet

          CSS3書簽工具。

          15.CSS3 Border Radius

          一款在線CSS3圓角工具,四個角輸入值就能生成對應的效果和代碼。

          16.CSS3 Desk

          很炫的CSS3桌面。

          17.Web Browser CSS Support

          非常詳盡的瀏覽器對CSS支持情況,包括CSS2.1和CSS3。

          18.Key CSS

          讓元素以鍵盤風格顯示的樣式表。

          19.CSS3 Playground

          一款在線CSS3圓角和陰影效果演示及代碼生成工具。

          20.CSS3 Wrapping Drop Shadows

          CSS3包裝陰影效果。

          21.CSS3 Carve Me

          模仿內陰影效果,可輸入內容查看效果,中文也可以噢。

          22.Mother Effing Text Shadows

          這工具名字太奇怪了,一款文本陰影效果工具,可即時生成代碼。

          23.CSS3 Learning Tool

          在線CSS3學習工具,可即時預覽效果。

          24.CSS3 Maker

          最后壓軸的這款工具非常強大,可在線演示漸變、陰影、旋轉、動畫等非常多的效果,并生成對應效果的代碼,趕緊體驗一下吧!

            

          您可能還喜歡



          (編譯來源:夢想天空  原文來自:Ultimate Collection of CSS3 Tools For Your Next Web Development

           

          posted @ 2012-07-15 20:15 小胡子 閱讀(177) | 評論 (0)編輯 收藏

          這篇文章主要收錄了十二月份發布在夢想天空的優秀文章,特別推薦給Web開發人員和設計師閱讀。夢天空博客關注前端開發技術,展示最新 HTML5 和 CSS3 技術應用,分享實用的 jQuery 插件,推薦優秀的網頁設計案例,共享精美的設計素材和優秀的 Web 開發工具。希望這些文章能幫助到您。

          HTML5 & CSS3 應用

          jQuery 插件和教程

          網頁設計素材

          英文字體資源

          攝影作品欣賞

          其它推薦文章

          同系列文章

           

          本文鏈接:Web前端開發人員和設計師必讀文章推薦【系列七】

          文章來源:夢想天空 ◆ 關注前端開發技術 ◆ 分享網頁設計資源

          posted @ 2012-07-15 20:05 小胡子 閱讀(145) | 評論 (0)編輯 收藏

          僅列出標題
          共15頁: First 上一頁 7 8 9 10 11 12 13 14 15 下一頁 
          主站蜘蛛池模板: 阳信县| 哈密市| 东莞市| 特克斯县| 沁源县| 沧源| 堆龙德庆县| 永昌县| 祁东县| 永宁县| 高陵县| 塔河县| 和平区| 新乐市| 永济市| 怀来县| 馆陶县| 会宁县| 宁波市| 游戏| 井研县| 松溪县| 乌什县| 隆安县| 板桥市| 石林| 工布江达县| 聂荣县| 伊春市| 宝丰县| 湾仔区| 巴东县| 镇沅| 曲阳县| 乌拉特后旗| 天柱县| 黄大仙区| 和平区| 万全县| 新宾| 鄂托克前旗|