synchronized原理

前提

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字節的整數,可能會選一個合適長度的字段填充到頭部。

填充

對象填充是虛擬機提升性能的一個優化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章