Java為了解決並發的原子性,提供了以下兩個解決方案:
1、Synchronized關鍵字
2、Lock
這篇文章我們先說一下Synchronized關鍵字,Lock等着下篇文章再說。
Synchronized是隱式鎖,當編譯的時候,會自動在同步代碼的前后分別加入monitorenter和monitorexit語句。
1、Synchronized的三種用法
package juc;
public class TestSyn {
static int x = 0;
int y = 0;
public static void main(String[] args) throws InterruptedException {
TestSyn testSyn = new TestSyn();
ThreadImpl thread1 = new ThreadImpl(testSyn);
ThreadImpl thread2 = new ThreadImpl(testSyn);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(x);
}
public static void add(){
x++;
}
}
class ThreadImpl extends Thread{
TestSyn testSyn = null;
public ThreadImpl(TestSyn testSyn){
this.testSyn = testSyn;
}
@Override
public void run() {
for (int i = 0;i < 1000;i++){
testSyn.add();
}
}
}
上述的代碼,我們實現了兩個線程對變量分別加1000次的操作。
我們執行發現
這就是我們之前說的,x++不是一個原子操作,當出現多線程並發的時候,會出現線程不安全。
Synchronized是一個關鍵字修飾詞,可以修飾靜態方法、非靜態方法、代碼塊。
Synchronized是一個鎖,鎖是加載對象上的。當加上靜態方法上的時候,對應的對象是class對象。每個類只有一個class對象,是一個互斥鎖。
1.1、靜態方法
public static synchronized void add(){
x++;
}
在add方法里加上synchronized關鍵字,程序就線程安全了
當在方法頭上加了synchronized關鍵字后,同時只能有一個線程進入方法內執行。當一個線程獲取鎖后,如果其他線程進入該方法后,會被阻塞。當其他線程執行完畢后,會釋放鎖,然后喚醒對應的線程。
1.2、非靜態方法。
package juc;
import java.util.concurrent.locks.Lock;
public class TestSyn {
static int x = 0;
int y = 0;
public static void main(String[] args) throws InterruptedException {
TestSyn testSyn = new TestSyn();
ThreadImpl thread1 = new ThreadImpl(testSyn);
ThreadImpl thread2 = new ThreadImpl(testSyn);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(x);
}
public synchronized void add(){
x++;
}
}
class ThreadImpl extends Thread{
TestSyn testSyn = null;
public ThreadImpl(TestSyn testSyn){
this.testSyn = testSyn;
}
@Override
public void run() {
for (int i = 0;i < 1000;i++){
testSyn.add();
}
}
}
當Synchronized加在非靜態方法的時候,是加載this對象上的。
1.3、代碼塊
package juc;
import java.util.concurrent.locks.Lock;
public class TestSyn {
static int x = 0;
int y = 0;
final Object object = new Object();
public static void main(String[] args) throws InterruptedException {
TestSyn testSyn = new TestSyn();
ThreadImpl thread1 = new ThreadImpl(testSyn);
ThreadImpl thread2 = new ThreadImpl(testSyn);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(x);
}
public void add(){
synchronized (object){
x++;
}
}
}
class ThreadImpl extends Thread{
TestSyn testSyn = null;
public ThreadImpl(TestSyn testSyn){
this.testSyn = testSyn;
}
@Override
public void run() {
for (int i = 0;i < 1000;i++){
testSyn.add();
}
}
}
如上述代碼所示,Synchronized修飾代碼塊的時候,我們用final Object object = new Object()來鎖的對象。
鎖的對象最好用final修飾,因為鎖的對象最好不要變,否則如果鎖的對象發生變化的話,兩個線程會同時進入代碼塊內,造成了線程不安全。
2、Synchronized原理以及鎖升級
在JDK1.6之前,Synchronized鎖的原理是通過操作系統的mutex互斥鎖實現的,需要線程從用戶態切換到內核態,這個十分消耗資源的。在JDK1.6之后,Synchronized引入了三種狀態的鎖來提高了鎖的性能!
從資源消耗級別從低到高分別為:偏向鎖、輕量級鎖、重量級鎖。
Synchronized是在對象上加鎖,我們首先說下Synchronized鎖對象的內存布局。
我們都知道對象存在堆上,對象分為對象頭和實例數據。對象頭中有一個Mark Word,Mark Word中存儲了鎖相關的信息,如下圖所示。
由上圖所示,Mark Word中的最后兩位代表鎖的類別。
10代表重量級鎖、00代表輕量級鎖、01代表無鎖或者偏向鎖,這個時候需要看倒數第三位,如果倒數第三位位1代表為偏向鎖,如果為0代表無鎖。
1、偏向鎖
Synchronized的默認鎖是偏向鎖。Mark Word中與偏向鎖有關的屬性有三個:鎖標志(01)、偏向鎖標志(1)、線程ID、Epoch(偏向鎖占用的次數)
偏向鎖對象初始化的時候,線程ID為null、Epoch為0。
偏向鎖的獲取過程:
當線程獲取偏向鎖的時候,首先查看偏向鎖標志是否為0。
1、如果偏向鎖標志位0,則進行CAS操作,CAS(偏向鎖標志,鎖標志,線程ID,Epoch)。
如果CAS成功的話,就是將線程ID設置為當前線程的ID,將鎖標志設置為01,偏向鎖設置為1,Epoch設置為Epoch+1,並在當前線程的棧幀空間開辟鎖記錄lock record寫入當前線程ID。
如果CAS失敗的話,說明有其他線程同時競爭,會將偏向鎖升級為輕量鎖。
如果偏向鎖標志位1,則查看線程ID是否與自己的線程ID相同,如果相同,則直接執行同步區代碼
如果線程ID不與自己相同,則判斷擁有偏向鎖的線程是否還存活,
如果死亡的話,就將鎖標志設置為0,偏向鎖標志設置為0,線程ID設置為null,當前線程開始CAS請求。
如果存活的話,從上到下遍歷線程中的棧幀是否存在lock record,如果存在的話,就說明當前線程還擁有着偏向鎖對象,就等到安全點的時候,將擁有偏向鎖的線程暫停,將偏向鎖升級為輕量鎖。
如果不存在的話,就將鎖標志設置為0,偏向鎖標志設置為0,線程ID設置為null,這就是叫做偏向鎖撤銷
當線程使用完偏向鎖的時候,是不會將對象頭中的線程ID撤銷的,只有其他線程來獲取偏向鎖的時候,才會撤銷。
偏向鎖適用於單線程 多次獲取偏向鎖的情況,減少了線程切換的開銷。
如果存在高並發的情況下,不要設置偏向鎖,將其關閉。因為要立馬要升級鎖,白白浪費了偏向鎖創建和銷毀的資源。
當epoch大於40的時候,也會自動升級為輕量鎖。
2、輕量級鎖
輕量級鎖獲取:
假設當前輕量級鎖還未被獲取,線程將會在棧幀中創建一個鎖記錄空間lock record,然后將鎖對象的mark word拷貝到lock record中,接着
執行cas將lock record的地址寫入到鎖對象的mark word,如果鎖對象中的mark word和lock record中之前拷貝的mark record相等,cas才會成功。
然后將lock record中的owner指針指向鎖對象中的mark record。
如果cas失敗的話,說明有其他線程捷足先登了,已經將其他線程的lock record地址寫入鎖對象的mark word了,就會將鎖升級為重量級鎖,對鎖對象中的mark word改寫為重量級鎖對應的格局,線程會被阻塞。
如果當前輕量級鎖已經被獲取的話,直接就將鎖升級為重量級鎖。
輕量級鎖的釋放
當線程釋放輕量級鎖的時候,會進行cas將lock record中的mark word 寫入到鎖對象中的mark word中,如果鎖對象中的mark word和lock record的地址一樣,cas才會成功。
如果cas失敗的話,說明鎖對象中的mark word已經在升級為重量級鎖的時候被改變了。這個時候會喚醒之前等待的線程。
3、重量級鎖
重量級鎖的時候,對象markword對應着一個C++對象,稱之為Monitor(監視器),對應的類文件如下
ObjectMonitor() {
_count = 0; //用來記錄該對象被線程獲取鎖的次數
_waiters = 0;
_recursions = 0; //鎖的重入次數
_owner = NULL; //指向持有ObjectMonitor對象的線程
_WaitSet = NULL; //處於wait狀態的線程,會被加入到_WaitSet
_WaitSetLock = 0 ;
_EntryList = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
}
重量級鎖,就是利用操作系統中的mutex互斥鎖,當申請鎖的時候,首先owner和當前線程一樣或者owner為null,如果一致就將recursions+1,然后執行同步代碼段。
如果owner不和當前線程一樣,將會執行一定次數的自旋鎖來請求鎖,如果請求不成功則會阻塞,將其加入到EntryList中。
當線程調用了moniterenter時,將count+1,如果調用了monitorexit,將count-1,當count=0時,線程就要釋放鎖了,當有線程釋放鎖的時候,會喚醒waitset和entrylist中的線程。
如果獲取鎖對象的線程調用了wait方法,則將線程放入waitSet中。
3、Synchronized可重入鎖的原理
Synchronized是一個可重入鎖,看下面的例子我們理解下什么是可重入鎖
package juc;
public class TestLoad {
final static Object object = new Object();
public static void main(String[] args) {
test1();
}
public static void test1(){
synchronized (object){
System.out.println("test1");
test2();
}
}
public static void test2(){
synchronized (object){
System.out.println("test2");
}
}
}
如上述代碼所示,test1方法中調用了object鎖對象,在沒有釋放鎖對象之前我們又調用了test2方法,test2方法中也需要申請object鎖對象。
可重入鎖的意思就是線程在還沒釋放鎖對象的時候,又重新申請調用同一個鎖,如果是可重入鎖的話就可以申請成功,如上圖所示。,如果是不可重入的話就會被阻塞,然后陷入死鎖,因為當前對象在重新申請鎖對象的前並沒有釋放鎖對象,在重新申請的時候會被阻塞,等待鎖對象釋放,所以會陷入死循環。
Synchronized是怎么實現可重入的呢?
1、偏向鎖
存儲的有線程ID,如果線程ID一樣就直接重入
2、輕量級鎖
如果鎖對象頭記錄的地址是在當前線程棧幀內,就直接重入。
3、重量級鎖
如果mutex互斥鎖中的owner和當前線程一樣,則直接重入。
4、總結
名字 | 適用場景 | 原因 | 是否可以關閉 |
---|---|---|---|
偏向鎖 | 單線程申請多次鎖 | 偏向鎖的做法簡單,就只在鎖對象中的mark word中標明線程ID,只通過一次CAS來獲取鎖,通過比較線程ID來判斷鎖的沖突 | 可以關閉 |
輕量級鎖 | 多個線程不同時申請鎖 | 通過多次的CAS來申請鎖,因為CAS消耗的cpu資源肯定是小於線程阻塞,然后被喚醒的切換消耗的cpu資源的。因為線程的阻塞和喚醒需要從用戶態轉化到內核態 | 可以關閉 |
重量級鎖 | 多個線程鎖沖突劇烈 | 如果線程之間的鎖劇烈沖突,CAS消耗的cpu資源會遠遠大於線程阻塞然后喚醒的消耗的cpu資源。在阻塞前會進行一定次數的自旋操作 | 不可關閉 |
偏向鎖做法:
1、當未加鎖時,在鎖對象mark word中cas寫入線程ID。
2、當mark word中的線程ID與當前線程相等時,直接執行同步代碼
3、當線程ID不等於當前線程ID時,查看mark word中對應的線程是否存活,並且是否引用當前鎖對象,如果存活並引用,就在安全點時,將擁有鎖的線程暫停,並將其升級為輕量級鎖。
4、如果不存活,就將鎖重置,變為無鎖狀態,轉到1
2、輕量級鎖
1、當未加鎖時,當前線程將鎖對象中的mark word復制到當前線程棧幀中的lock record中,然后通過cas將lock record的地址寫入到鎖對象的mark word中,舊值是lock record中的拷貝的mark word。如果更新成功,將lock record中的owner指針指向鎖對象中的mark word。
2、如果cas不成功的話,說明有其他線程同時申請了鎖對象,並且已經捷足先登的將其lock record的地址寫入到了鎖對象中的mark word了,就將鎖升級到重量級鎖,重寫鎖對象對應的mark word。
3、如果鎖對象加鎖了,就直接升級為重量級鎖。
4、當線程釋放輕量級鎖時,將lock record中之前復制的mark word 通過cas寫入到鎖對象中的mark word,舊值為lock record的地址。
如果cas失敗,就說明發生了重量級鎖的升級。這個時候就會喚醒其他線程。
3、重量級鎖
重量級鎖利用的操作系統的mutex互斥鎖,當出現鎖沖突時,將會執行一定次數的自旋鎖來請求鎖,如果請求不成功則會阻塞,並將線程掛到阻塞隊列中。
如果線程釋放鎖的時候,就喚醒阻塞隊列中的其他線程。