在不一樣線程中,對同一變量、方法或代碼塊進行同步訪問html
咱們經過一個例子瞭解鎖的不一樣實現,開啓100個線程對同一int
變量進行++
操做1000次,在這個過程當中如何對這個變量進行同步java
未同步代碼:算法
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/** * \* Created with IntelliJ IDEA. * \* User: guohezuzi * \* Date: 2018-04-30 * \* Time: 上午11:26 * \* Description:本身編寫的多線程的栗子(多個線程添加元素到數組中) * \ * * @author guohezuzi */
public class MyExample {
private int count = 0;
class addHundredNum extends Thread {
@Override
public void run() {
//...執行其餘操做
for (int i = 0; i < 1000; i++) {
count++;
}
//...執行其餘操做
}
}
public void test() throws InterruptedException {
addHundredNum[] addHundredNums = new addHundredNum[100];
for (int i = 0; i < addHundredNums.length; i++) {
addHundredNums[i] = new addHundredNum();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.start();
}
// 等待全部addHundredNum線程執行完畢
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.join();
}
}
public static void main(String[] args) throws Exception {
MyExample example = new MyExample();
example.test();
System.out.println(example.count);
}
}
複製代碼
經過synchronized(addHundredNum.class)
給當前對象加鎖而不是synchronized(this)
給對象實例加鎖編程
public class MyExample {
private int count = 0;
class addHundredNum extends Thread {
@Override
public void run() {
//...執行其餘操做
synchronized (addHundredNum.class) {
for (int i = 0; i < 1000; i++) {
count++;
}
}
//...執行其餘操做
}
}
public void test() throws InterruptedException {
addHundredNum[] addHundredNums = new addHundredNum[100];
for (int i = 0; i < addHundredNums.length; i++) {
addHundredNums[i] = new addHundredNum();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.start();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.join();
}
}
public static void main(String[] args) throws Exception {
MyExample example = new MyExample();
example.test();
System.out.println(example.count);
}
}
複製代碼
synchronized關鍵字通過編譯以後,會在同步塊的先後分別造成monitorenter和monitorexit這兩個字節碼指令,這兩個字節碼都須要一個reference類型的參數來指明要鎖定和解鎖的對象。若是Java程序中的synchronized明確指定了對象參數,那就是這個對象的reference;若是沒有明確指定,那就根據synchronized修飾的是實例方法仍是類方法,去取對應的對象實例或Class對象來做爲鎖對象。 根據虛擬機規範的要求,在執行monitorenter指令時,首先要去嘗試獲取對象的鎖。若是這個對象沒被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1,相應地,在執行monitorexit指令時會將鎖計數器減1,當計數器爲0時,鎖就被釋放了。若是獲取對象鎖失敗了,那當前線程就要阻塞等待,直到對象鎖被另一個線程釋放爲止。數組
在JDK1.6以前,使用sysnchronized同步時,若是要掛起或者喚醒一個線程,都須要操做系統幫忙完成,而操做系統實現線程之間的切換時須要從用戶態轉換到內核態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高緩存
JDK1.6以後,JVM對sysnchronized進行了大量優化,從原來的重量級鎖到如今的鎖的不一樣階段升級 無鎖 -> 偏向鎖 -> 輕量級鎖及自旋鎖 -> 重量級鎖安全
偏向鎖多線程
當進行同步時,偏向於第一個得到它的線程,若是在接下來的執行中,該鎖沒有被其餘線程獲取,那麼持有偏向鎖的線程就不須要進行同步併發
可是對於鎖競爭比較激烈的場合,偏向鎖就失效了,由於這樣場合極有可能每次申請鎖的線程都是不相同的,此時,偏向鎖會升級爲輕量級鎖ide
輕量級鎖
是指當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級爲輕量級鎖,其餘線程會經過CAS自旋的形式嘗試獲取鎖,不會阻塞,從而提升性能。
但若是存在鎖競爭,除了互斥量開銷外,還會額外發生CAS操做,所以在有鎖競爭的狀況下,輕量級鎖比傳統的重量級鎖更慢!若是鎖競爭激烈,那麼輕量級將很快膨脹爲重量級鎖!
自旋鎖和自適應自旋鎖
輕量級鎖失敗後,虛擬機爲了不線程真實地在操做系統層面掛起,還會進行一項稱爲自旋鎖的優化手段。讓線程自旋的方式等待一段時間
自適應的自旋鎖:自旋的時間不在固定了,而是和前一次同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定。
鎖消除
指的就是虛擬機即便編譯器在運行時,若是檢測到那些共享數據不可能存在競爭,那麼就執行鎖消除。鎖消除能夠節省毫無心義的請求鎖的時間。
鎖粗化
若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,會帶來不少沒必要要的性能消耗,經過對連續操做的一次加鎖和解鎖(及鎖的粗化)來節省時間
經過JDK層面AQS實現的鎖,須要咱們經過編程實現,如調用lock()、unlock()
public class MyExample {
private int count = 0;
private final Lock lock = new ReentrantLock();
class addHundredNum extends Thread {
@Override
public void run() {
lock.lock();
try {
for (int i = 0; i < 1000; i++) {
count++;
}
} finally {
lock.unlock();
}
}
}
public void test() throws InterruptedException {
addHundredNum[] addHundredNums = new addHundredNum[100];
for (int i = 0; i < addHundredNums.length; i++) {
addHundredNums[i] = new addHundredNum();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.start();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.join();
}
}
public static void main(String[] args) throws Exception {
MyExample example = new MyExample();
example.test();
System.out.println(example.count);
}
}
複製代碼
AQS詳解參考:JAVA多線程 - AQS詳解
經過使用原子類的CAS方法來實現
public class MyExample {
private AtomicInteger count = new AtomicInteger(0);
class addHundredNum extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count.getAndAdd(1);
}
}
}
public void test() throws InterruptedException {
addHundredNum[] addHundredNums = new addHundredNum[100];
for (int i = 0; i < addHundredNums.length; i++) {
addHundredNums[i] = new addHundredNum();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.start();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.join();
}
}
public static void main(String[] args) throws Exception {
MyExample example = new MyExample();
example.test();
System.out.println(example.count);
}
}
複製代碼
JDK8可使用新增LongAdder類實現,該類自己會分紅多個區域,多線程寫入時,寫入對應區域,讀取會將整個區域統計輸入。
CAS全稱 Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的狀況下實現多線程之間的變量同步。
CAS算法涉及到三個操做數:
當且僅當 V 的值等於 A 時,CAS經過原子方式用新值B來更新V的值(「比較+更新」總體是一個原子操做),不然不會執行任何操做。通常狀況下,「更新」是一個不斷重試的操做。
volatile關鍵字使用時,只能做用於變量,且並不能保證不一樣線程中的同步,故沒法實現上面的同步的例子,接下來咱們來介紹一下volatile
關鍵字的做用:
保證不一樣線程中變量的可見性
volatile英譯易揮發的,表示修飾的變量是不穩定的,易改變,故採用volatile修飾後,會將變量放到主內存中,不會放到每一個線程的cpu高速緩存後在讀取,而是直接所用線程都經過到主內存去讀取,以保證變量在每一個線程的可見性。
然而,這並不意味着變量的線程安全,不一樣線程cpu進行運算存在時間差,如當多個線程同時對該變量進行++
操做時,可能其中一個線程讀取時變量值爲1,這時另一個線程也讀取變量值爲1,第一個線程cpu進行+1
操做運行完畢並已經寫回內存,而另外一個線程cpu才進行+1操做運算並寫入內存,此時一個線程的結果被覆蓋,致使線程不安全。
防止新建對象的重排序現象
當變量採用volatile修飾後,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。如保守策略的JMM內存屏障插入策略:
在每一個volatile寫操做的前面插入一個StoreStore屏障。
在每一個volatile寫操做的後面插入一個StoreLoad屏障。
在每一個volatile讀操做的後面插入一個LoadLoad屏障。
在每一個volatile讀操做的後面插入一個LoadStore屏障。
具體例子可參考文章雙重校驗鎖實現的單例模式中的volatile關鍵字的做用
《深刻理解Java虛擬機:JVM高級特性與最佳實踐》第十三章