前提
CAS的概念,如果這個概念不理解將很難搞明白synchronized的原理。關於CAS的概念和原理,可以參考文章https://www.cnblogs.com/javalyy/p/8882172.html
概念和使用
synchronized是由JVM實現,java語言規範規定,要理解synchronized關鍵詞的原理,首先理解它能用來幹啥?
Oracle官方文檔,Java語言規範規定了synchronized的語義:https://docs.oracle.com/javase/specs/jls/se8/html/index.html
簡單的講,保證多線程操作共享資源的互斥,達到保護共享資源數據,實現線程安全的操作的目的。
synchronized用法簡介
1、直接修飾方法(靜態or非靜態),官方文檔第8章。
2、作爲同步塊使用,修飾需要加鎖的對象,官方文檔第14章。
上面兩種方式的本質是一樣的,其實都是給某一對象加互斥鎖,加在方法上實質是給ClassName.class或者this對象加鎖
synchronized(this) {
// TODO
}
所以,要理清楚synchronized的原理,由使用來看(因爲它傳入的參數就是一個對象),不難看出我們需要從它修飾的對象出發,搞清楚對象裏面究竟保存了什麼樣的數據,即對象的結構,通過對象裏的數據如何幫助我們實現Java語言規範規定的語義。
Java對象的結構
先推理一下
1、平時我們定義一個類,然後通過new關鍵字新建一個對象,不難想象,對象中肯定開闢了空間用於保存我們在class類中定義的字段
2、另一方面,我們必須知道這個對象的結構,即它是由哪個class抽象的,熟悉jvm運行時數據區的,我們可以知道class的定義在方法區。那麼對象中需要保存一個指向該方法區定義的該class的指針。
3、既然我們同步關鍵字需要來操作對象,那麼可以推測,對象中還保存有鎖相關的一些數據。
4、其它可能需要的信息
Hotspot虛擬機內的對象結構
先上圖:
對象結構主要包含3部分:
1、對象頭,圖中黃低背景(這裏面就有我們剛纔推理出來的鎖相關、類型指針等數據)
2、實例數據,我們自己定義的字段數據或者引用存儲,圖中藍底背景
3、對齊填充,灰色部分。
對象頭
不難看出,我們同步關鍵字synchronized的原理的關鍵就在對象頭部分,這裏以32位虛擬機舉例(64位差不多,區別是多餘的內存可能就浪費了,所以虛擬機參數提供壓縮選項,開啓後,可以壓縮對象),由上面的圖從右至左爲低位到高位的順序。
1、Markword
markword是對象頭中一個32位長度的存儲區,用來存儲鎖狀態,gc狀態、hashcode等對象關鍵數據。爲了讓Markword存儲更多的信息,最低的2位爲標誌位,不同的標誌位對應不同的狀態。第3位(從低到高)爲偏向鎖狀態。
a、無鎖狀態(標誌位=01)
剩餘bit位,從低到高依次爲:偏向鎖狀態=0(1位)、gc年齡(4位)、hashcode(25位)
b、偏向鎖(標誌位=01)
如果虛擬機開啓了偏向鎖優化,當有線程第一次來到synchronized同步塊時,會直接獲取到偏向鎖,對象會進入到偏向鎖狀態,此時除最低兩位爲01外,剩餘bit位,從低到高依次爲:偏向鎖狀態=1(1位)、gc年齡(4位)、epoch偏向鎖時間戳(2位)、偏向鎖持有線程ID(23位)。這裏高位的23位如果爲空,則代表當前對象可偏向,但是未鎖定也未偏向;如果高位23位保存了某個線程的ID,則表示當前對象處於鎖定且偏向狀態,此時,如果線程自己釋放了偏向鎖,它不會發生任何變化,而如果該線程再次來獲取鎖,也不會有CAS操作,只需要判斷這裏的線程id是否是自己即可(這是JVM做的優化);而如果有其它線程來獲取鎖,當判斷到這裏的線程ID不是自己,然後進行CAS搶鎖,因爲這裏已經被別的線程佔有了,肯定會失敗,於是會進行鎖升級;
偏向鎖升級過程:
1)、先進行偏向鎖撤銷
2)、等待佔有偏向鎖線程進入到安全點後,暫停原線程
3)、再次檢查偏向鎖狀態,鎖已釋放,則進入不可偏向對象無鎖狀態、喚醒原線程繼續執行。鎖未釋放,升級爲輕量級鎖的狀態(這裏就是輕量級鎖機制、在原線程生成lock record,保存鎖對象的mark word和owner,而對象的mark word則用lock record指針替換,標誌位修改等工作)、喚醒原線程繼續執行。
c、輕量級鎖(標誌位=00)
輕量級鎖採用CAS實現,進入輕量級鎖狀態的對象,剩餘bit位,執行持有鎖線程執行棧幀中的lock record地址。這個lock record是線程再搶輕量級鎖時創建,裏面保存有用於釋放鎖時恢復鎖對象的mark word,owner指向持有鎖的對象地址。
如果沒有開啓偏向鎖優化(JDK1.6以後默認開啓),則線程來搶鎖,直接進入搶輕量級鎖的流程,搶輕量級鎖的流程實質就是採用CAS操作修改對象頭Markword的過程,首先線程執行到synchronized的臨界區時,在線程堆棧創建lock record信息,把synchronized修飾的對象的對象頭中的markword(前提是沒有別的線程獲取到鎖)複製到lock record中,然後採用CAS操作將lock record + 末位00,這樣一個32位的數據替換到對象頭的markword位置,如果成功,代表搶到了鎖,則記錄lock record中的owner=對象的地址。
d、重量級鎖(標誌位=10)
輕量級鎖有一個缺陷,如果同時很多線程通過CAS自旋搶鎖,那麼可能存在有線程一直在自旋佔用CPU而搶不到鎖,會浪費大量的cpu時間,嚴重影響程序性能,那麼虛擬機有機制將輕量級鎖升級爲重量級鎖,重量級鎖的狀態爲,剩餘bit爲,指向每個對象都會有一個與之對象的monitor,重量級鎖不會存在搶不到鎖一直佔用cpu資源的情況,它的實現原理類似Java的ReentrantLock,可以參見我簡單參照Java源碼實現的一個ReentrantLock。
```java
package com.study.lock;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
/**
* 利用CommonMash實現
* @author Administrator
*
*/
public class YbjReentrantLock implements Lock
{
private boolean isfair;
public YbjReentrantLock(boolean isfair) {
this.isfair = isfair;
}
/**
* 模板方法模式,實現鎖的公共邏輯
* @author Administrator
*
*/
public static class CommonMash
{
protected AtomicInteger readCount = new AtomicInteger(0);
protected AtomicInteger writeCount = new AtomicInteger(0);
//只有寫線程能成爲owner
protected AtomicReference<Thread> owner = new AtomicReference<>();
protected volatile LinkedBlockingQueue<WaitNode> lockWaitors = new LinkedBlockingQueue<>();
public static class WaitNode {
Thread thread;
boolean write;
int arg;
public WaitNode(Thread thread, boolean write, int arg) {
this.thread = thread;
this.write = write;
this.arg = arg;
}
}
public void lock()
{
int acqurie = 1;
if(!tryLock(acqurie)) {
//放入隊列,用什麼方法?
WaitNode node = new WaitNode(Thread.currentThread(), true, acqurie);
lockWaitors.offer(node);
while(true) {
node = lockWaitors.peek();
if (node != null && node.thread == Thread.currentThread()) {//爲什麼必須判斷頭部是當前線程本身?
//因爲,程序代碼這裏當前是在爲執行到這裏的線程本身搶鎖,搶到鎖之後,應該移除隊列的也必須是當前線程,否則不是本身的話
//就相當於我線程搶到了鎖,但是我把你從隊列裏移除了
if(tryLock(acqurie)) {
lockWaitors.poll();
return;
} else {
LockSupport.park();//因爲park和unpark不分先後,即先unpark,再park不會導致卡死,所以及時沒有獲取到鎖,但是在park之前又有線程釋放了鎖,導致先unpark了,不會存在卡死,沒有問題
}
} else {
LockSupport.park();
}
}
}
}
public boolean tryLock(int acqurie)
{
int rc = readCount.get();
if (rc != 0) {
return false;//爲什麼直接只判斷寫鎖不爲0就返回,這和jdk的讀寫鎖實現是一致的,不允許同一個線程讀鎖,升級寫鎖//如果rc==1,是否能判斷這個獲取了唯一讀鎖的線程是否是來搶鎖的線程,貌似判斷不了
}
int count = writeCount.get();
if (count == 0) {
//利用原子操作,去搶寫鎖(設置writeCount=1)但是這裏與上面readCount的判斷會有原子性問題,可能此時readCount被別的線程修改了
//所以需要一個判斷read,write,和設置write的原子操作,JDK是將readCount和WriteCount用一個整形的高半位和低半位分別來表示實現的。
//這裏爲了簡單,先不管
//搶鎖
//bug1,不要把參數傳反了,否則不會成功,bug2,應該設置爲獲取的count + acqurie
//bug2 boolean success = writeCount.compareAndSet(0, acqurie);
boolean success = writeCount.compareAndSet(count, count + acqurie);
//成功則設置當前線程爲owner
if (success) {
owner.set(Thread.currentThread());//bug,這裏搶成功了沒有返回true,那麼會一直搶不成功
return true;
}
} else {
//能直接返回嗎,不能
if(owner.get() == Thread.currentThread()) {//寫鎖重入
writeCount.set(count + acqurie);//這裏可以直接修改值
}
return false;
}
return false;
}
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
throw new UnsupportedOperationException();
}
public void unlock()
{
int acquire = 1;
if (tryUnlock(acquire)) {
WaitNode next = lockWaitors.peek();
if (next != null) {
Thread t = next.thread;
LockSupport.unpark(t);
}
}
System.out.println("writeCount"+writeCount);
System.out.println("readCount"+readCount);
}
public boolean tryUnlock(int acquire) {
if (Thread.currentThread() != owner.get()) {
throw new IllegalMonitorStateException();
} else {
int count = writeCount.get();
writeCount.set(count - acquire);
if (writeCount.get() == 0) {
//爲什麼要用原子操作
//按理說只有獲得到鎖的線程才能走到這裏,owner也不會被獲取鎖的地方改變
//1。不會被釋放鎖的改變,2、搶鎖的線程呢?其它線程此時能搶鎖嗎,能,因爲writeCount==0
//因爲writeCount先被修改爲0,此時其它線程可以去搶寫鎖,搶到後owner被修改爲其它線程,若不採用CAS操作,可能會覆蓋成功搶鎖的owner爲空,但是此時鎖確實另外一個線程的
//所以要用原子操作,防止覆蓋
owner.compareAndSet(Thread.currentThread(), null);
return true;
}
return false;
}
}
public void lockShared()
{
throw new UnsupportedOperationException();
}
public boolean tryLockShared(int acqurie)
{
throw new UnsupportedOperationException();
}
public boolean tryLockShared(long time, TimeUnit unit) throws InterruptedException
{
throw new UnsupportedOperationException();
}
public void unlockSharedBadPratice()
{
throw new UnsupportedOperationException();
}
public void unlockShared()
{
throw new UnsupportedOperationException();
}
public boolean tryUnlockShared(int acquire) {
throw new UnsupportedOperationException();
}
}
private CommonMash common = new CommonMash(){
public boolean tryLock(int acquire)
{
return tryLock(acquire, isfair);
}
private boolean tryLock(int acqurie,boolean isfair)
{
int rc = readCount.get();
if (rc != 0) {
return false;//爲什麼直接只判斷寫鎖不爲0就返回,這和jdk的讀寫鎖實現是一致的,不允許同一個線程讀鎖,升級寫鎖//如果rc==1,是否能判斷這個獲取了唯一讀鎖的線程是否是來搶鎖的線程,貌似判斷不了
}
int count = writeCount.get();
if (count == 0) {
CommonMash.WaitNode node = null;
if (isfair) {
return tryLock0(count, count+acqurie);
} else if((node = lockWaitors.peek()) !=null && Thread.currentThread() == node.thread) {
return tryLock0(count, count+acqurie);
}
} else if(owner.get() == Thread.currentThread()) {
writeCount.set(count + acqurie);//這裏可以直接修改值
return true;
}
return false;
}
private boolean tryLock0(int expect, int update) {
if (writeCount.compareAndSet(expect, update)) {
owner.set(Thread.currentThread());
return true;
}
return false;
}
};
@Override
public void lock()
{
common.lock();
}
@Override
public void lockInterruptibly() throws InterruptedException
{
// TODO Auto-generated method stub
}
@Override
public boolean tryLock()
{
return common.tryLock(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
// TODO Auto-generated method stub
return false;
}
@Override
public void unlock()
{
common.unlock();
}
@Override
public Condition newCondition()
{
// TODO Auto-generated method stub
return null;
}
}
e、gc(標誌位=11)
該對象可以被gc啦
2、類型指針
對象頭第二部分,通過類型指針,對象可以知道該對象的抽象類,可以知道對象是什麼類型以及對象的結構。
3、數組長度
如果對象是數組,那麼對象頭中還存儲了數組的長度。
數據區
僞共享
Jvm編譯時,會對成員變量進行優化排序,基本的排序規則是越長的類型在月前面,如果64位開啓了對象頭壓縮,對象頭長度不是8字節的整數,可能會選一個合適長度的字段填充到頭部。
填充
對象填充是虛擬機提升性能的一個優化。