【運維】記一次上線前的緊急定位與修復-獻上九條小經驗

1 簡介

本文介紹了作者所在團隊在某次上線前測試發現問題、定位問題並修復上線的過程,最後給出幾點經驗總結,希望對大家有用。

2 過程

(1)今天需要上線,但昨晚才合併了所有分支,時間很緊迫。不幸的是,打包測試后發現有一個Springboot應用(模塊R)啟動失敗,但進程沒有死,一直在輸出報錯日誌

(2)Google了相關的報錯日誌,並沒有找到相關信息。查看了模塊R的代碼變更,並沒有什麼改動,以為是環境問題;部署到其它環境后,發現問題依舊存在,而且這個問題從未出現過,基本排除環境問題,問題還是出在代碼上。

(3)啟動模塊R上一個版本(現生產環境)的代碼,正常啟動。所以問題還是出現模塊R的改動上。

(4)對比模塊R的發布包的新版本與生產版本的差異,發現第三方依賴包都一致,但自己項目的依賴包不同。

(5)想到一個有效的辦法,依次用生產版本替換掉自己項目的包,最終定位了問題出在通用模塊D上。

(6)查看模塊D的代碼變更記錄,改動比較大,比較難發現是哪裡的改動造成的。

(7)重新看日誌。為何要重看呢?並不是心血來潮,主要是想找關聯。既然已經知道了問題在哪個模塊代碼上,通過查看日誌與該模塊可能相關的信息,或許能找到蛛絲馬跡。

(8)果然!!!重新查看日誌發現,模塊R啟動時,報了一個其它錯誤ErrorA,但被後面不斷重複出現的錯誤ErrorB刷掉了,所以一開始並沒有注意到它。通過該報錯,與模塊D的代碼改動對比。終於定位出了問題!

(9)創建hotfix分支,修改代碼提交,重新merge,打包,測試通過,部署生產!!!

因為部署上線是有特定的時間窗口的,如果錯過了時間,就要下次再上線,還好及時定位,及時解決!

3 經驗總結

(1)不要放過任何日誌,特別是報錯的日誌,日誌是極其有用的。不要只看最後面的報錯,也不要只看最前面的報錯,任何報錯都可能提供新的方向和線索。如果日誌信息不夠,可以嘗試打開debug模式,會有大量的日誌信息,當然也要求你有足夠強的過濾和整理信息的能力。

(2)提取有用日誌,可以用greptailless等linux命令。

(3)組件化、模塊化很重要,能快速縮小問題範圍。能通過只回退某個模塊實現部分功能先上線。

(4)善用對比工具,如diff命令,BeyondCompare軟件等。

(5)善用代碼變更記錄,這是版本控制給我們帶來的好處,可以方便我們對比代碼改動了什麼,什麼時候改的,能加速問題定位;也能及時回退代碼。

(6)上線前要做充分的測試。這次問題的出現項目流程上的原因在於沒有進行充分的測試。(1)寫代碼的人修改了通用模塊,卻沒有測試依賴它的其它模塊的功能會不會受影響,而只測試了自己那一部分;(2)合併代碼后,沒有足夠的時間來進行測試。部署前一天,才合併了代碼打包測試。所以時間非常緊迫,在短時間要定位問題並解決,容易造成壓力。

(7)要有獨立的測試環境。這個是導致方向性錯誤的原因,經過是這樣的:A同學打包了自己的分支,這時剛好B同學稍晚一點也打包了分支,而打包的環境只有一個,B同學的包覆蓋了A同學的包。所以在A部署的時候,實際用了B同學的代碼打的包,導致啟動失敗。所以一直以為是A同學代碼的問題,這個方向性的錯誤浪費了很多時間。應該要讓每個分支可以同時打包,但不會覆蓋。

(8)不要先入為主。不要過早認定某個模塊就是有問題的,請參考上一條。

(9)團隊作戰,分工合作。整個過程全靠團隊一起協作才能快速定位並解決;打造一個開放包容、溝通順暢的團隊是多麼的重要。

If You Want to Go Fast, Go Alone. If You Want to Go Far, Go Together.

4 最後

運維和問題定位的知識很多,也非常重要,需要持續學習。本文僅講述了本次過程用到的方法。更多的知識以後慢慢再介紹…

歡迎關注公眾號<南瓜慢說>,將持續為你更新…

多讀書,多分享;多寫作,多整理。

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

【其他文章推薦】

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

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

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

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

※教你寫出一流的銷售文案?

javascript中判斷數據類型

編寫javascript代碼的時候常常要判斷變量,字面量的類型,可以用typeof,instanceof,Array.isArray(),等方法,究竟哪一種最方便,最實用,最省心呢?本問探討這個問題。

1. typeof

1.1 語法

typeof返回一個字符串,表示未經計算的操作數的類型。

語法:typeof(operand) | typeof operand
參數:一個表示對象或原始值的表達式,其類型將被返回
描述:typeof可能返回的值如下:

類型 結果
Undefined “undefined”
Null “object”
Boolean “boolean”
Number “number”
Bigint “bigint”
String “string”
Symbol “symbol”
宿主對象(由JS環境提供) 取決於具體實現
Function對象 “function”
其他任何對象 “object”

從定義和描述上來看,這個語法可以判斷出很多的數據類型,但是仔細觀察,typeof null居然返回的是“object”,讓人摸不着頭腦,下面會具體介紹,先看看這個效果:

    // 數值
    console.log(typeof 37) // number
    console.log(typeof 3.14) // number
    console.log(typeof(42)) // number
    console.log(typeof Math.LN2) // number
    console.log(typeof Infinity) // number
    console.log(typeof NaN) // number 儘管它是Not-A-Number的縮寫,實際NaN是数字計算得到的結果,或者將其他類型變量轉化成数字失敗的結果
    console.log(Number(1)) //number Number(1)構造函數會把參數解析成字面量
    console.log(typeof 42n) //bigint
    // 字符串
    console.log(typeof '') //string
    console.log(typeof 'boo') // string
    console.log(typeof `template literal`) // string
    console.log(typeof '1') //string 內容為数字的字符串仍然是字符串
    console.log(typeof(typeof 1)) //string,typeof總是返回一個字符串
    console.log(typeof String(1)) //string String將任意值轉換成字符串
    // 布爾值
    console.log(typeof true) // boolean
    console.log(typeof false) // boolean
    console.log(typeof Boolean(1)) // boolean Boolean會基於參數是真值還是虛值進行轉換
    console.log(typeof !!(1)) // boolean 兩次調用!!操作想短語Boolean()
    // Undefined
    console.log(typeof undefined) // undefined
    console.log(typeof declaredButUndefinedVariabl) // 未賦值的變量返回undefined
    console.log(typeof undeclaredVariable ) // 未定義的變量返回undefined
    // 對象
    console.log(typeof {a: 1}) //object
    console.log(typeof new Date()) //object
    console.log(typeof /s/) // 正則表達式返回object
    // 下面的例子令人迷惑,非常危險,沒有用處,應避免使用,new操作符返回的實例都是對象
    console.log(typeof new Boolean(true)) // object
    console.log(typeof new Number(1)) // object
    console.log(typeof new String('abc')) // object
    // 函數
    console.log(typeof function () {}) // function
    console.log(typeof class C { }) // function
    console.log(typeof Math.sin) // function 

1.2 迷之null

javascript誕生以來,typeof null都是返回‘object’的,這個是因為javascript中的值由兩部分組成,一部分是表示類型的標籤,另一部分是表示實際的值。對象類型的值類型標籤是0,不巧的是null表示空指針,它的類型標籤也被設計成0,於是就有這個typeof null === ‘object’這個‘惡魔之子’。

曾經有ECMAScript提案讓typeof null返回‘null’,但是該提案被拒絕了。

1.3 使用new操作符

除Function之外所有構造函數的類型都是‘object’,如下:

    var str = new String('String');
    var num = new Number(100)
    console.log(typeof str) // object
    console.log(typeof num) // object
    var func = new Function()
    console.log(typeof func) // function 

1.4 語法中的括號

typeof運算的優先級要高於“+”操作,但是低於圓括號

    var iData = 99
    console.log(typeof iData + ' Wisen') // number Wisen
    console.log(typeof (iData + 'Wisen')) // string 

1.5 判斷正則表達式的兼容性問題

typeof /s/ === 'function'; // Chrome 1-12 , 不符合 ECMAScript 5.1
typeof /s/ === 'object'; // Firefox 5+ , 符合 ECMAScript 5.1 

1.6 錯誤

ECMAScript 2015之前,typeof總能保證對任何所給的操作數都返回一個字符串,即使是沒有聲明,沒有賦值的標示符,typeof也能返回undefined,也就是說使用typeof永遠不會報錯。

但是ES6中加入了塊級作用域以及let,const命令之後,在變量聲明之前使用由let,const聲明的變量都會拋出一個ReferenceError錯誤,塊級作用域變量在塊的頭部到聲明變量之間是“暫時性死區”,在這期間訪問變量會拋出錯誤。如下:

    console.log(typeof undeclaredVariable) // 'undefined'
    console.log(typeof newLetVariable) // ReferenceError
    console.log(typeof newConstVariable) // ReferenceError
    console.log(typeof newClass) // ReferenceError

    let newLetVariable
    const newConstVariable = 'hello'
    class newClass{} 

1.7 例外

當前所有瀏覽器都暴露一個類型為undefined的非標準宿主對象document.all。typeof document.all === ‘undefined’。景觀規範允許為非標準的外來對象自定義類型標籤,單要求這些類型標籤與已有的不同,document.all的類型標籤為undefined的例子在web領域被歸類為對原ECMA javascript標準的“故意侵犯”,可能就是瀏覽器的惡作劇。

總結:typeof返回變量或者值的類型標籤,雖然對大部分類型都能返回正確結果,但是對null,構造函數實例,正則表達式這三種不太理想。 

2. instanceof

2.1 語法

instanceof運算符用於檢測實例對象(參數)的原型鏈上是否出現構造函數的prototype。

語法:object instanceof constructor
參數:object 某個實例對象
          constructor 某個構造函數
描述:instanceof運算符用來檢測constructor.property是否存在於參數object的原型鏈上。

// 定義構造函數
    function C() {
    }
    function D() {
    }
    var o = new C()
    console.log(o instanceof C) //true,因為Object.getPrototypeOf(0) === C.prototype
    console.log(o instanceof D) //false,D.prototype不在o的原型鏈上
    console.log(o instanceof Object) //true 同上

    C.prototype = {}
    var o2 = new C()
    console.log(o2 instanceof C) // true
    console.log(o instanceof C) // false C.prototype指向了一個空對象,這個空對象不在o的原型鏈上
    D.prototype = new C() // 繼承
    var o3 = new D()
    console.log(o3 instanceof D) // true
    console.log(o3 instanceof C) // true C.prototype現在在o3的原型鏈上

需要注意的是,如果表達式obj instanceof Foo返回true,則並不意味着該表達式會永遠返回true,應為Foo.prototype屬性的值可能被修改,修改之後的值可能不在obj的原型鏈上,這時表達式的值就是false了。另外一種情況,改變obj的原型鏈的情況,雖然在當前ES規範中,只能讀取對象的原型而不能修改它,但是藉助非標準的__proto__偽屬性,是可以修改的,比如執行obj.__proto__ = {}后,obj instanceof Foo就返回false了。此外ES6中Object.setPrototypeOf(),Reflect.setPrototypeOf()都可以修改對象的原型。

instanceof和多全局對象(多個iframe或多個window之間的交互)

瀏覽器中,javascript腳本可能需要在多個窗口之間交互。多個窗口意味着多個全局環境,不同全局環境擁有不同的全局對象,從而擁有不同的內置構造函數。這可能會引發一些問題。例如表達式[] instanceof window.frames[0].Array會返回false,因為Array.prototype !== window.frames[0].Array.prototype。

起初,這樣可能沒有意義,但是當在腳本中處理多個frame或多個window以及通過函數將對象從一個窗口傳遞到另一個窗口時,這就是一個非常有意義的話題。實際上,可以通過Array.isArray(myObj)或者Object.prototype.toString.call(myObj) = “[object Array]”來安全的檢測傳過來的對象是否是一個數組。

2.2 示例

String對象和Date對象都屬於Object類型(它們都由Object派生出來)。

但是,使用對象文字符號創建的對象在這裡是一個例外,雖然原型未定義,但是instanceof of Object返回true。

    var simpleStr = "This is a simple string";
    var myString  = new String();
    var newStr    = new String("String created with constructor");
    var myDate    = new Date();
    var myObj     = {};
    var myNonObj  = Object.create(null);

    console.log(simpleStr instanceof String); // 返回 false,雖然String.prototype在simpleStr的原型鏈上,但是後者是字面量,不是對象
    console.log(myString  instanceof String); // 返回 true
    console.log(newStr    instanceof String); // 返回 true
    console.log(myString  instanceof Object); // 返回 true

    console.log(myObj instanceof Object);    // 返回 true, 儘管原型沒有定義
    console.log(({})  instanceof Object);    // 返回 true, 同上
    console.log(myNonObj instanceof Object); // 返回 false, 一種創建非 Object 實例的對象的方法

    console.log(myString instanceof Date); //返回 false

    console.log( myDate instanceof Date);     // 返回 true
    console.log(myDate instanceof Object);   // 返回 true
    console.log(myDate instanceof String);   // 返回 false 

注意:instanceof運算符的左邊必須是一個對象,像”string” instanceof String,true instanceof Boolean這樣的字面量都會返回false。

下面代碼創建了一個類型Car,以及該類型的對象實例mycar,instanceof運算符表明了這個myca對象既屬於Car類型,又屬於Object類型。

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
var mycar = new Car("Honda", "Accord", 1998);
var a = mycar instanceof Car;    // 返回 true
var b = mycar instanceof Object; // 返回 true 

不是…的實例

要檢測對象不是某個構造函數的實例時,可以使用!運算符,例如if(!(mycar instanceof Car))

instanceof雖然能夠判斷出對象的類型,但是必須要求這個參數是一個對象,簡單類型的變量,字面量就不行了,很顯然,這在實際編碼中也是不夠實用。

總結:obj instanceof constructor雖然能判斷出對象的原型鏈上是否有構造函數的原型,但是只能判斷出對象類型變量,字面量是判斷不出的。

3. Object.prototype.toString()

3.1. 語法

toString()方法返回一個表示該對象的字符串。

語法:obj.toString()
返回值:一個表示該對象的字符串
描述:每個對象都有一個toString()方法,該對象被表示為一個文本字符串時,或一個對象以預期的字符串方式引用時自動調用。默認情況下,toString()方法被每個Object對象繼承,如果此方法在自定義對象中未被覆蓋,toString()返回“[object type]”,其中type是對象的類型,看下面代碼:

    var o = new Object();
    console.log(o.toString()); // returns [object Object] 

注意:如ECMAScript 5和隨後的Errata中所定義,從javascript1.8.5開始,toString()調用null返回[object, Null],undefined返回[object Undefined]

3.2. 示例

覆蓋默認的toString()方法

可以自定義一個方法,來覆蓋默認的toString()方法,該toString()方法不能傳入參數,並且必須返回一個字符串,自定義的toString()方法可以是任何我們需要的值,但如果帶有相關的信息,將變得非常有用。

下面代碼中定義Dog對象類型,並在構造函數原型上覆蓋toString()方法,返回一個有實際意義的字符串,描述當前dog的姓名,顏色,性別,飼養員等信息。

function Dog(name,breed,color,sex) {
        this.name = name;
        this.breed = breed;
        this.color = color;
        this.sex = sex;
    }
    Dog.prototype.toString = function dogToString() {
        return "Dog " + this.name + " is a " + this.sex + " " + this.color + " " + this.breed
    }

    var theDog = new Dog("Gabby", "Lab", "chocolate", "female");
    console.log(theDog.toString()) //Dog Gabby is a female chocolate Lab 

4. 使用toString()檢測數據類型

目前來看toString()方法能夠基本滿足javascript數據類型的檢測需求,可以通過toString()來檢測每個對象的類型。為了每個對象都能通過Object.prototype.toString()來檢測,需要以Function.prototype.call()或者Function.prototype.apply()的形式來檢測,傳入要檢測的對象或變量作為第一個參數,返回一個字符串”[object type]”。

    // null undefined
    console.log(Object.prototype.toString.call(null)) //[object Null] 很給力
    console.log(Object.prototype.toString.call(undefined)) //[object Undefined] 很給力

    // Number
    console.log(Object.prototype.toString.call(Infinity)) //[object Number]
    console.log(Object.prototype.toString.call(Number.MAX_SAFE_INTEGER)) //[object Number]
    console.log(Object.prototype.toString.call(NaN)) //[object Number],NaN一般是数字運算得到的結果,返回Number還算可以接受
    console.log(Object.prototype.toString.call(1)) //[object Number]
    var n = 100
    console.log(Object.prototype.toString.call(n)) //[object Number]
    console.log(Object.prototype.toString.call(0)) // [object Number]
    console.log(Object.prototype.toString.call(Number(1))) //[object Number] 很給力
    console.log(Object.prototype.toString.call(new Number(1))) //[object Number] 很給力
    console.log(Object.prototype.toString.call('1')) //[object String]
    console.log(Object.prototype.toString.call(new String('2'))) // [object String]

    // Boolean
    console.log(Object.prototype.toString.call(true)) // [object Boolean]
    console.log(Object.prototype.toString.call(new Boolean(1))) //[object Boolean]

    // Array
    console.log(Object.prototype.toString.call(new Array(1))) // [object Array]
    console.log(Object.prototype.toString.call([])) // [object Array]

    // Object
    console.log(Object.prototype.toString.call(new Object())) // [object Object]
    function foo() {}
    let a = new foo()
    console.log(Object.prototype.toString.call(a)) // [object Object]

    // Function
    console.log(Object.prototype.toString.call(Math.floor)) //[object Function]
    console.log(Object.prototype.toString.call(foo)) //[object Function]

    // Symbol
    console.log(Object.prototype.toString.call(Symbol('222'))) //[object Symbol]

    // RegExp
    console.log(Object.prototype.toString.call(/sss/)) //[object RegExp] 

上面的結果,除了NaN返回Number稍微有點差池之外其他的都返回了意料之中的結果,都能滿足實際開發的需求,於是我們可以寫一個通用的函數來檢測變量,字面量的類型。如下:

    let Type = (function () {
        let type = {};
        let typeArr = ['String', 'Object', 'Number', 'Array', 'Undefined', 'Function', 'Null', 'Symbol', 'Boolean', 'RegExp', 'BigInt'];
        for (let i = 0; i < typeArr.length; i++) {
            (function (name) {
                type['is' + name] = function (obj) {
                    return Object.prototype.toString.call(obj) === '[object ' + name + ']'
                }
            })(typeArr[i])
        }
        return type
    })()
    let s = true
    console.log(Type.isBoolean(s)) // true
    console.log(Type.isRegExp(/22/)) // true 

除了能檢測ECMAScript規定的八種數據類型(七種原始類型,BooleanNullUndefinedNumberBigIntStringSymbol,一種複合類型Object)之外,還能檢測出正則表達式RegExpFunction這兩種類型,基本上能滿足開發中的判斷數據類型需求。

5. 判斷相等

既然說道這裏,不妨說一說另一個開發中常見的問題,判斷一個變量是否等於一個值。ES5中比較兩個值是否相等,可以使用相等運算符(==),嚴格相等運算符(===),但它們都有缺點,== 會將‘4’轉換成4,後者NaN不等於自身,以及+0 !=== -0。ES6中提出”Same-value equality“(同值相等)算法,用來解決這個問題。Object.is就是部署這個算法的新方法,它用來比較兩個值是否嚴格相等,與嚴格比較運算(===)行為基本一致。

    console.log(5 == '5') // true
    console.log(NaN == NaN) // false
    console.log(+0 == -0) // true
    console.log({} == {}) // false

    console.log(5 === '5') // false
    console.log(NaN === NaN) // false
    console.log(+0 === -0) // true
    console.log({} === {}) // false

Object.js()不同之處有兩處,一是+0不等於-0,而是NaN等於自身,如下:

    let a = {}
    let b = {}
    let c = b
    console.log(a === b) // false
    console.log(b === c) // true
    console.log(Object.is(b, c)) // true 

注意兩個空對象不能判斷相等,除非是將一個對象賦值給另外一個變量,對象類型的變量是一個指針,比較的也是這個指針,而不是對象內部屬性,對象原型等。

 

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

【其他文章推薦】

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

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

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

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

※教你寫出一流的銷售文案?

紐西蘭白島火山罹難者增至19人 含2名失蹤者

摘錄自2019年12月23日中央社報導

紐西蘭白島(White Island)火山9日突然噴發,當時有47人在島上觀光,其中大部分是澳洲旅客,紐西蘭警方23日證實又一人在醫院過世後,白島火山噴發死亡人數升至19人,目前仍有25人在醫院且多人傷勢危急。死亡總數包括遺體尚未尋獲但據信已經罹難的兩人,17歲澳洲公民藍福特(Winona Langford)和40歲紐西蘭導遊馬歇爾-殷曼(Hayden Marshall-Inman)的遺體據信已被捲進海裡。

紐西蘭警方救援白島火山罹難者。照片來源:

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

【其他文章推薦】

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

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

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

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

戴姆勒 2016 年底前擬推出長續航距離電動車

據美國科技部落格 VentureBeat 報導,德國汽車廠商戴姆勒 (Daimler) 將在 2016 年 10 月份舉行的巴黎國際汽車展上,首次推出超長續航距離電動汽車,希望電動 SUV 的市場與特斯拉 Model X 一較高下。   報導中表示,戴姆勒技術研發部門負責人湯瑪斯‧韋伯 (Thomas Weber) 在出席位於德國斯圖加特舉行的記者會時宣布,該公司將會展示續航里程達 500 公里的賓士電動汽車原型車,與特斯拉 Model X 一較高下。不過, Thomas Weber 並未透露這款電動汽車何時上路行駛,只說會在 2020 年前完成這一目標。   目前,戴姆勒和歐洲其他汽車廠商正加緊對電動汽車的投資,以滿足歐盟新的排放標準,並且追趕上美國電動汽車產業的當紅炸子雞──特斯拉 (Tesla) 。另外,戴姆勒在德國本土的競爭對手,福斯旗下子公司包括奧迪與保時捷均已推出續航里程很長的電動汽車原型車,至於 BMW 也正在開發這種車型。   Thomas Weber 表示,戴姆勒將在 2016 年底推出第四代 Smart 電動汽車,包括兩座和四座兩個車型。戴姆勒目前共有 Smart 和 B 系列兩個全電動車型,還有眾多插電式混合動力車,即同時採用電池和汽油引擎驅動的車型。新款混合動力車也在開發之中。據了解,戴姆勒的目標是在 2020 年以前,每年售出超過 10 萬輛電動車。不過,Thomas Weber 拒絕提供 2015 年的銷售資料。   除了長距離續航力的電動車之外,戴姆勒還在開發燃料電池汽車,這種車型使用由氫發的電。該公司原計劃在 2014 年推出燃料電池汽車,但由於價格問題,不得不延遲發表。消息指出,戴姆勒燃料電池汽車 SUV GLC 預計將在 2017 年投產,屆時可能會與豐田 Mirai 展開競爭。   (首圖來源: CC BY 2.0)   (本文授權轉載自《》─〈〉)

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

【其他文章推薦】

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

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

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

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

大咖分享| 百度語義技術及應用全解

分享嘉賓:孫宇,百度NLP主任研發架構師、語義計算技術負責人。

本文根據作者在“2019自然語言處理前沿論壇”語義理解主題的特邀報告整理而成。

本報告提綱分為以下3個部分:

·    語義表示

·    語義匹配

·    未來重點工作

語義計算方向在百度NLP成立之初就開始研究,研究如何利用計算機對人類語言的語義進行表示、分析和計算,使機器具備語義理解能力。相關技術包含語義表示、語義匹配、語義分析、多模態計算等。

本文主要介紹百度在語義表示方向的技術發展和最新的研究成果艾尼(ERNIE),同時也會介紹工業應用價值很大、百度積累多年的語義匹配SimNet的相關內容,最後再談談未來的重點工作。

 一、語義表示

 計算機理解語言是一個具有很大挑戰的問題。人類在理解語言的過程中,除了語言符號本身的識別,還包含符號背後的語義和知識。舉個例子,當人看到“計算機”這個符號時,腦子里能迅速浮現出計算機的畫面以及和計算機相關的知識和概念,但是這對於計算機就比較困難。所以如何讓計算機能夠表示語言是研究的重點,讓其既能夠蘊含語義信息又可以計算。

當前主要有兩類方法,一是基於形式化規則的方法,如通過構建語義關係網絡來描述語義的信息;二是基於統計的方法,包括主題模型、Word Embedding等技術。

1、百度早期語義表示技術:基於檢索的表示方法

 

2007年百度便開始語義表示研究,最開始的思路是利用搜索引擎來完成。通過搜索引擎把要表示的句子或者詞語檢索出來,再根據檢索的結果通過Term的分析以及網頁的分析,把相關的詞、信息抽取出來,做成語言符號的表示。但是這個表示實際上停留在原始詞彙空間,表示的空間大小依然是詞表的維度,只是相對於One-Hot的表示來說更精細,這個方法是基於1954年Harris提出來的“上下文相似的詞,其語義也相似”的假設。

2、百度早期語義表示技術:Topic Model

 

此後,百度又研究了Topic Model的語義表示技術,這種方法的核心思路是把文檔詞彙空間降維,將文檔映射到淺層主題的分佈上,而這種主題空間要比詞的分佈空間更小一些。通過降維的方法,可以得到每個詞到主題的映射,通過這種主題的方法做語義的表示。

當時百度主要解決的問題是怎樣做這種新文檔的表示,難點是超大規模語料訓練、Online EM、MPI并行化。此外,百度還將自研的主題模型以及一些主流的主題模型整理為工業應用工具,對外開源了百度NLP主題模型工具包FAMILIA。

3、基於DNN的語義表示技術:Word Embedding

 

深度學習技術興起,基於Word Embedding的表示佔了主流,此類技術在各種NLP任務中也表現出色。從NNLM到現在BERT的技術,取得了很多進展。2013年的Word2vec成為NLP標配性的初始化向量,2018年有了上下文相關的詞向量ELMo等。

 

從2013年到2016年,百度也大力投入到Word Embedding的研究,主要研究工作是在工業界如何用大規模分佈式從海量數據中計算詞向量。比如,怎麼才能高效訓練規模為1T的語料?如何構建大規模分佈式文本計算?此外,算法上我們也有一些研究,比如,如何在一百萬超大規模的詞表裡完成Softmax分類?我們通過一些策略和技術,做成啟髮式Hierarchical Softmax的方法,從而有效地提升分類的效率。2016年,百度把訓練的1T的網頁數據和100萬詞表規模的詞向量對業界進行了開放。

4、多特徵融合的表示模型

 

BERT的核心思路還是大力出奇迹,它利用了大規模的無監督數據,同時藉助Transformer這種高性能的Encoder的能力,在MASK建模任務上做了一些優化,導致這個效果能夠在各個任務上顯著提升。

百度實際在2017年進行了這方面的探索,當時是研究基於對話的口語理解問題,這個問題的核心是做意圖的分類和槽位的標註。難點在於口語理解的問題標註語料非常少。當時想能不能利用海量的搜索語料做Pre-Training,把這個Model作為初始化模型用到下游的SLU任務里。

我們採用20億搜索的Query,通過LSTM模型做單向Language Model的預訓。我們發現在SLU任務上,在各個垂類上樣本數的增加非常顯著,從10個樣本到2000個樣本。但遺憾的是,當時研究的是一個超小規模數據上效果,即2000的數據,在2萬甚至是20萬的數據上的表現並沒有研究,同時在其他應用的通用性上的研究也不夠充分。

5、知識增強的語義表示模型

 BERT提出后,我們發現一個問題,它學習的還是基礎語言單元的Language Model,並沒有充分利用先驗語言知識,這個問題在中文很明顯,它的策略是MASK字,沒有MASK知識或者是短語。在用Transformer預測每個字的時候,很容易根據詞包含字的搭配信息預測出來。比如預測“雪”字,實際上不需要用Global的信息去預測,可以通過“冰”字預測。基於這個假設,我們做了一個簡單的改進,把它做成一個MASK詞和實體的方法,學習這個詞或者實體在句子裏面Global的信號。

 

基於上述思想我們發布了基於知識增強的語義表示ERNIE(1.0)。

 

我們在中文上做了ERNIE(1.0)實驗,找了五個典型的中文公開數據集做對比。不管是詞法分析NER、推理、自動問答、情感分析、相似度計算,ERNIE(1.0)都能夠顯著提升效果。

 

英文上驗證了推廣性,實驗表明ERNIE(1.0)在GLUE和SQuAd1.1上提升也是非常明顯的。為了驗證假設,我們做了一些定性的分析,找了完形填空的數據集,並通過ERNIE和BERT去預測,效果如上圖。

 

我們對比了ERNIE、BERT、CoVe、GPT、ELMo模型,結果如上圖所示。ELMo是早期做上下文相關表示模型的工作,但它沒有用Transformer,用的是LSTM,通過單向語言模型學習。百度的ERNIE與BERT、GPT一樣,都是做網絡上的Transformer,但是ERNIE在建模Task的時候做了一些改進,取得了很不錯的效果。

 

在應用上,ERNIE在百度發布的面向工業應用的中文NLP開源工具集進行了驗證,包括ERNIE與BERT在詞法分析、情感分類這些百度內部的任務上做了對比分析。同時也有一些產品已經落地,在廣告相關性的計算、推薦廣告的觸發、新聞推薦上都有實際應用。目前模型已經開源(

7月31日,百度艾尼(ERNIE) 再升級,發布了持續學習語義理解框架ERNIE 2.0,同時藉助飛槳(PaddlePaddle)多機多卡高效訓練優勢發布了基於此框架的ERNIE 2.0 預訓練模型。該模型在共計16个中英文任務上超越了BERT 和XLNet,取得了SOTA 效果。

二、語義匹配

1、文本語義匹配及挑戰

 

語義匹配在工業界具有非常大的技術價值,它是一個很基礎的問題,很多產品、應用場景都會用到它。很多問題也可以抽象為語義匹配問題,比如,搜索解決的是Query和Document相關性的問題,推薦解決的是User和Item關聯度、興趣匹配度的問題,檢索式問答解決的是問題與答案匹配度,以及檢索對話Query和Response的匹配問題。由於語言比較複雜,匹配靠傳統的方法是比較難的。

 

百度搜索在匹配相似度計算方面做了較多工作,包括挖掘同義詞、詞級別泛化、語義緊密度、對齊資源挖掘、共線關聯計算等。

 

2、神經網絡語義匹配模型:SimNet

2013年百度提出SimNet技術,用於解決語義匹配的問題。這個技術基於DNN框架,沿襲Word Embedding的輸入,基於End-to-End的訓練做表示和匹配,並結合Pairwise訓練。當時,微軟也提出了DSSM,中科院、CMU等研究機構也做了很多語義匹配研究工作。

 

這幾年,百度整體上從語義匹配的框架上做了升級,抽象了三個層次,改進了基礎算法,包括擴展針對不同場景的模型,比如字和語義的匹配模型;在不同的應用場景,針對問題網頁和問題答案的匹配情況分別做了針對性地優化,集成到了匹配框架里。

 

匹配算法主要有兩種範式,一種是基於表示的匹配,首先把自然語言表示成向量,然後再進行相似度計算,這方面也有一些改進,主要是做一些Attention;另一種新匹配範式Interaction-based Model,強調更細的匹配,即一個句子的表示不再是一個向量,而是原來的Term,並把原來的位置信息保留,最後以Attention的方式表示,讓匹配更加充分和精細。

 

關於SimNet技術前瞻性工作,2019年百度在IJCAI上發表了一篇論文“RLTM:An Efficient Neural IR Framework for Long Documents”,其中長文本匹配有一個很大的挑戰,就是讓Document直接做表示,如果文本太長,很多信息會丟失,計算效率也非常低。但如果我們先做一個粗匹配,選擇好相關的句子以後再做精細化的匹配,效果就比較不錯。

3、SimNet的應用

 

SimNet技術在百度應用非常廣泛,包括搜索、資訊推薦、廣告、對話平台都在使用。

 

搜索是百度非常重要的產品,搜索有兩個核心功能,下圖的左側上方是搜索的精準問答,通過問答技術把精準答案直接呈現出來;下方是自然排序,主要採用LTR框架和相關性、權威性、時效性等Features。

SimNet在百度搜索的發展可以分為三個時期。萌芽期,上線了BOW Model,這是業界第一次在搜索引擎上線DNN模型;發展期,做了CNN、RNN,並把知識融合進RNN,在語義相關性計算中,除了標題很多其他文本域在相關性建模中也很重要,所以,我們還做多文本域融合匹配的Model;拓展期,除了相關性,在權威性、點擊模型和搜索問答上都有推廣和使用。

 

在搜索中,SimNet是用超大規模用戶反饋數據訓練。那麼如何依靠海量數據來提升效果?頻次如何選?我們發現模型應用效果並不是靜態的,而是動態變化的,特別是搜索反饋的數據,隨着時間的推移,網民在搜索的時候,Term的分佈、主題的分佈會發生變化,所以數據的時效性影響還是非常大的。

 

除了模型上的融合,我們把Bigram知識也融入了進去。儘管RNN已經很厲害了,但加入知識、模型還是會有很大地提升。

4、新模型:SimNet-QC-MM

 

另外,我們還做了Query和網頁正文的建模,由於Query中每個詞都有一定的用戶意圖,所以在模型建模時,會考慮Query中每個詞被Title和正文覆蓋的情況,並基於Matching Matrix匹配方法計算。此外,搜索架構也做了配合改進,搜索也上線了基於GPU和CPU的異構計算架構。

 

上圖是一個案例,“羋殊出嫁途中遇到危險”,我們後來做了一些分析,發現“危險”和“投毒”有很強的語義關聯,就把這個結果排了上去。

5、語義模型壓縮技術

 

在模型裁減壓縮上,我們也做了很多工作,包括量化的壓縮和哈希技術的壓縮。整個語義的模型基本上已經從依靠一個Embedding 32bits來存,到現在達到Embedding一維僅需4bits,節省線上DNN匹配模型87.5%的內存消耗。這項技術,除了搜索的使用,移動端的使用也有非常大的價值。

 

SimNet技術除了百度搜索,包括Q&A,Query和Answer的匹配等方面都有一些嘗試。

三、未來重點工作

接下來我們會在通用語義表示方面進一步研究與突破,除了如何充分地利用先驗知識,利用一些弱監督信號外,模型方面也會進一步探索創新。技術拓展上,跨語言、多語言表示,面向生成、匹配等任務的表示,面向醫療、法律等領域的表示,多模態表示等都是我們的一些重點方向。

RLTM論文地址:

至此,“2019自然語言處理前沿論壇”語義計算主題《百度語義計算技術及其應用》的分享結束。如果大家想更深入地了解百度持續學習語義理解框架艾尼(ERNIE),歡迎加入ERNIE官方交流群(QQ群號:760439550)。

——————-

划重點!!!

掃碼關注百度NLP官方公眾號,獲取百度NLP技術的第一手資訊!

加入ERNIE官方技術交流群(760439550),百度工程師實時為您答疑解惑!

立即前往GitHub(  )為ERNIE點亮Star,馬上學習和使用起來吧! 

 

最強預告!!!

11月23日,艾尼(ERNIE)的巡迴沙龍將在上海加場,乾貨滿滿的現場,行業A級的導師,還有一群志同道合的小夥伴,還在等什麼?感興趣的開發者們趕緊掃描下方“二維碼”或點擊“鏈接”報名參加吧!

 

報名鏈接:

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

【其他文章推薦】

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

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

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

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

缺食物? 首次紀錄到美洲豹下海捕魚 亞馬遜科學家謹慎以對

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

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

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

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

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

高性能Web動畫和渲染原理系列(4)“Compositor-Pipeline演講PPT”學習摘要

目錄

示例代碼託管在:

博客園地址:

華為雲社區地址:

附件PPT來自開發文檔。術語里的cc指的是Chromium Compositor

一直以來都想了解瀏覽器合成層的運作機制,但是相關的中文資料大多比較關注框架和開發技術,這方面的資料實在是太少了,後來在chromium官方網站的文檔里找到了項目組成員malaykeshav在 2019年4月的一份關於瀏覽器合成流水線的演講PPT,個人感覺裏面講的非常清楚了,由於沒有找到視頻,有些部分只能自行理解,本文僅對關鍵信息做一些筆記,對此感興趣的讀者可以在文章開頭的github倉庫或附件中拿到這個PPT自行學習。

摘要

1.合成流水線

合成流水線,就是指瀏覽器處理合成層的工作流程,其基本步驟如下:

大致的流程就是說Paint環節會生成一個列表,列表裡登記了頁面元素的繪製指令,接着這個列表需要經過Raster光柵化處理,並在合成幀中處理紋理,最後的Draw環節才是將這些紋理圖展示在瀏覽器內容區。

2. 預定義UI層

chromium中預定義了一些指定類型的UI層,大致分為:

  • Not Drawn – 為了處理透明度或濾鏡效果、transform變形或者clip剪裁的非繪製層
  • Solid color layer – 固有顏色層
  • Painted texture layer – Texture紋理會在這個層執行paint渲染和後續的rasterized光柵化任務
  • Transferable resource layer – 共享資源層,可能是GPU裏面的Texture紋理也可能未來會發給GPU的位圖
  • Surface layer – 臨時佔位層,因為自頂向下遍歷layer樹時子樹都還沒處理,需要先佔位最後再填充
  • Nine patch layer – 用於實現陰影的層

3. paint是什麼意思

每個層layer是由若干個views組成的,所謂paint,就是每個views將自己對應圖形的繪製指令添加到層的可展示元素列表Display Item List里,這個列表會被添加到一個延遲執行的光柵化任務中,並最終生成當前層的texture紋理(可以理解為當前層的繪製結果),考慮到傳輸性能以及未來增量更新的需求,光柵化的結果會以tiles瓦片形式保存。在chrome中也可以看到頁面瓦片化拆分的結果:

4. 分層的優勢和劣勢

分層的優勢和劣勢也在此進行了說明,和之前我們主動思考的答案基本一致(暗爽一下)。

5. 視圖屬性及其處理方式

views中支持的屬性包含Clip剪裁,transform變換,effect效果(如半透明或濾鏡等),mask遮罩,通常按照後序遍歷的方式自底向上進行遍歷處理。

clip剪裁的處理方式是在父節點和子節點之間插入一個剪裁層,用來將其子樹的渲染結果剪裁到限定的範圍內,然後再向上與父級進行合併;

transform變換直接作用於父節點,處理到這個節點時其子樹都已經處理完畢,直接將整體應用變形即可;

effect效果一般直接作用於當前處理的節點,有時也會產生交叉依賴的場景;

PPT第40頁中在介紹effect效果處理時描述了兩種不同的透明度處理需求,從而引出了一個Render Surface的概念,它相當於一個臨時的層,它的子樹需要先繪製在這個層上,然後再向上與父節點進行合併,屏幕就是是根級的Render Surface

6. Quads

Layer遍歷處理輸出的結果被稱為Quads(從意思上理解好像就是指輸出了很多個矩形方塊),每個quad都持有它被繪製到目標緩衝區所需要的資源,根據它持有的資源不同可以分為:

  • Solid Color-固定顏色型
  • Texture– 紋理型
  • Tile– 瓦片型
  • Surface– 臨時繪圖表面型
  • Video – 視頻幀型
  • Render PassRender Surface類型的佔位區,Render Surface子樹處理完后填充到關聯的Render Pass

7. Compositor Frame

合成層真正的工作要開始了,主角概念Compositor Frame(合成幀)登場,它負責將quads合併繪製在一起,膠片里59-62頁非常清楚地展示了合成的過程,最終輸出的結果就是根節點的紋理。

chromium是多進程架構,Browser Process瀏覽器進程會對菜單欄等等容器部分的畫面生成合成幀來輸出,每個網頁的Render Process渲染進程會對頁面內容生成合成幀來輸出,最終的結果都被共享給GPU ProcessGPU進程進行聚合併生成最終完整的合成表面,接着在Display Compositor環節將最後的位圖展示在屏幕上。

8. 關於光柵化以及渲染方式

膠片里並沒有描述具體的光柵化的處理過程,但是layer輸出的quads看起來應該是光柵化以後的結果,推測應該是處理Display Item List中的繪圖指令時也和WebGL類似,經過頂點着色器片元着色器的遍歷式處理機制,並在過程中自動完成像素插值。

9.【重要】軟件渲染和硬件渲染的區別

聲明:本節內容是個人理解,僅用作技術交流,不保證對!

軟件渲染和硬件渲染的區別對筆者而言一直非常抽象,只是知道基本概念。後來在(國內可能無法訪問)中《Compositor Thread Architecture》這篇合成器線程架構的文章中找到了一些相關描述,也解開了筆者心中一直以來的疑惑,相關部分摘抄如下:

Texture Upload

One challenge with all these textures is that we rasterize them on the main thread of the renderer process, but need to actually get them into the GPU memory. This requires handing information about these textures (and their contents) to the impl thread, then to the GPU process, and once there, into the GL/D3D driver. Done naively, this causes us to copy a single texture over and over again, something we definitely don’t want to do.

We have two tricks that we use right now to make this a bit faster. To understand them, an aside on “painting” versus “rasterization.”

  • Painting is the word we use for telling webkit to dump a part of its RenderObject tree to a GraphicsContext. We can pass the painting routine a GraphicsContext implementation that executes the commands as it receives them, or we can pass it a recording context that simply writes down the commands as it receives them.
  • Rasterization is the word we use for actually executing graphics context commands. We typically execute the rasterization commands with the CPU (software rendering) but could also execute them directly with the GPU using Ganesh.
  • Upload: this is us actually taking the contents of a rasterized bitmap in main memory and sending it to the GPU as a texture.With these definitions in mind, we deal with texture upload with the following tricks:
  • Per-tile painting: we pass WebKit paint a recording context that simply records the GraphicsContext operations into an SkPicture data structure. We can then rasterize several texture tiles from that one picture.
  • SHM upload: instead of rasterizing into a void* from the renderer heap, we allocate a shared memory buffer and upload into that instead. The GPU process then issues its glTex* operations using that shared memory, avoiding one texture copy.The holy grail of texture upload is “zero copy” upload. With such a scheme, we manage to get a raw pointer inside the renderer process’ sandbox to GPU memory, which we software-rasterize directly into. We can’t yet do this anywhere, but it is something we fantasize about.

大概翻譯一下,方便英語水平一般的小夥伴理解,GPU處理圖片的方式是按照Texture進行貼圖的,對此不熟悉的小夥伴可以查看筆者以前發的有關Three.js相關的博文。

紋理上傳:
處理紋理的挑戰之一就是它是在渲染進程(可以理解為單個Tab網頁的進程)的主線程里進行的,但是最終需要將其放入GPU內存。這就需要將紋理數據遞交給合成器線程,然後再交給GPU進程(Chromium架構里有專門的GPU進程用來專門處理和GPU之間的協作任務),最後再傳遞給底層的Direct3DOpenGL(也就是圖形學的底層技術),如果只是按照常規流程來處理,就會需要一次又一次來複制生成的紋理數據,這顯然不是我們想要的。
我們現在使用了兩個小方法來使這個流程變得快一點。它們分別作用於painting(繪製)和rasterization(光柵化)兩個階段。

  • 1號知識點!!!Painting我們用來告訴webkit為RenderObject Tree的來生成對應的GraphicsContext。通過給painting routine(繪製流程)傳遞一個GraphicsContext的具體實現來執行這些已經編排好的繪製命令,也可以傳遞一個record context(記錄上下文)只是簡單地把繪圖命令都記錄下來。
  • 2號知識點!!!Rasterization(光柵化)是指Graphics context關聯的繪圖命令實際被執行的過程。通常我們使用CPU(也就是軟件渲染的方式)來執行光柵化任務,也可以直接使用GPU來渲染(也就是硬件渲染的方式)。
  • 上傳:指在主線程存儲區獲取到光柵化以後的位圖內容然後將它作為紋理上傳給GPU的過程,考慮到上述已經提及的定義,上傳過程是如下來處理的:
    • 瓦片繪製:我們在webkit中使用recording context來簡單地記錄Graphics Context的操作指令,將它存儲為SkPicture類型(直接使用軟件光柵化時生成的是SkBitmap類型),隨後可以從一張picture裏面光柵化處理得到多個紋理瓦片
    • 共享內存:在軟件渲染的方式中,光柵化的結果會被存儲在renderer進程的堆內存里,現在不這樣搞了,我們重新分配了一塊共享緩衝區,然後通過它來傳遞相關對象,GPU進程隨後在獲取紋理時直接從共享內存中獲取就行了,這樣就避免了數據的拷貝。
      總的來說,紋理上傳的過程幾乎是零拷貝的。利用這樣的結構,我們在renderer進程(也就是網頁的渲染進程)的沙箱環境內也可以獲取到指向GPU 內存的指針,而在軟件光柵化的過程中,是直接將位圖結果放在這裏的。
  • Painting: this is the process of asking Layers for their content. This is where we ask webkit to tell us what is on a layer. We might then rasterize that content into a bitmap using software, or we might do something fancier. Painting is a main thread operation.
  • Drawing: this is the process of taking the layer tree and smashing it together with OpenGL onto the screen. Drawing is an impl-thread operation.
  • painting:表示的過程是向Layers對象查詢層內容,也就是讓webkit告訴我們每一層上面到底有什麼。接下來我們就可以使用軟件光柵化的方式將這些內容處理為位圖,也可以做一些更牛的事情,painting是一個主線程行為。
  • drawing:是指將Layer中的內容用OpenGL繪製在屏幕上的過程,它是另一個線程中的操作。

概念比較多沒有基礎的讀者可能理解起來有難度,我嘗試用自己的話複述一下:

【軟件渲染】的模式下,在paint時會直接利用Graphics Context繪圖上下文將結果繪製出來,在一個SkBitmap實例中保存為位圖信息;【硬件渲染】的模式下,在paint時傳入一個SkPicture實例,將需要執行的繪圖命令保存在裏面先不執行,然後通過共享內存將它傳給GPU進程,藉助GPU來最終去執行繪圖命令,生成多個瓦片化的位圖紋理結果(OpenGL中頂點着色器向片元着色器傳遞數據時可以自動進行數據插值,完成光柵化的任務)。 純軟件渲染里嚴格說是沒有合成層概念的,因為最終輸出的只有一張位圖,按照順序從下往上畫,和畫到一個新層上再把新層貼到已有結果上其實是一樣的。

不管使用哪種途徑,paint動作都是得到位圖數據,而最終的draw這個動作是藉助OpenGL和位圖數據最終把圖形显示在显示器上。

所以【硬件渲染】就是渲染進程把要做的事情和需要的數據都寫好,然後打包遞給GPU讓它去幹活。

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

【其他文章推薦】

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

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

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

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

一邊吵架一邊對話 讓環保團體與電網公司並肩 歐洲再生能源倡議RGI辦到了

環境資訊中心記者 陳文姿 德國報導

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

【其他文章推薦】

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

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

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

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

中國北汽、日本松下合資,投入中國電動車市場

日本松下(Panasonic)、中國北京汽車宣布將共同投資數百億日圓,於中國天津成立合資公司,用於生產純電動車之核心零組件,搶攻中國龐大的電動車市場商機。

《日本經濟新聞》中文版、日本《共同社》等多家媒體報導,松下社長津賀一宏日前曾前往中國,與北汽公司商討成立合資公司一事,並達成協議。北汽旗下兩家汽車零組件廠商將出資54%、松下的中國公司則出資46%,合資公司計畫在2018年左右開始投入量產,主要產品是電動車空調的電動壓縮機,供貨對象則是北汽公司與其他公司。

電動車的空調電力來自蓄電池。為優化電動車的行駛里程,電動車空調壓縮機須能以更好的效率控制冷、暖風,因此性能需求較一般汽油車高。性能佳的空調壓縮機,將有助提高電動車的續航力。

中國市場蓬勃,松下積極搶進

在政府的大力推動下,中國已在2015年成為全球最大電動車市場之一。《日經》指出,日廠豐田決定在2018年起於中國生產插電式混和動力車(PHEV);而松下與北汽的合作,則象徵松下在中國市場的在地化布局行動。未來預計還會有更多日商在中國展開本土化生產。

松下已宣布退出電視面板製造事業,並轉而提高在住宅、汽車等兩大領域的業務著力。2015財年,松下所生產的車載導航儀與車載電池等零組件銷售額達1.3兆日圓。松下表示,到2018年度時,希望上述零組件銷售額能進一步提高到2兆日圓。

松下在電動車、鋰電池方面的海外投資,包括與美商特斯拉(Tesla)合作在美國內華達州建立的鋰電池工廠Gigafactory,以及中國大連建立的車用鋰電池工廠。大連工廠預計在2017年正式投產。

北汽成立於1953年,為國有企業。在電動車、PHEV領域,北汽是中國僅次於比亞迪(BYD)的第二大業者。北汽計畫到2020年時生產40萬輛電動車。

(照片來源:Wikipedia)

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

【其他文章推薦】

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

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

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

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

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虛擬機》

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

【其他文章推薦】

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

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

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

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