1. 程式人生 > >初識指令重排序,Java 中的鎖

初識指令重排序,Java 中的鎖

本文是作者原創,版權歸作者所有.若要轉載,請註明出處.本文只貼我覺得比較重要的原始碼

指令重排序

Java語言規範JVM執行緒內部維持順序化語義,即只要程式的最終結果與它順序化情況的結果相等,那麼指令的執行順序可以與程式碼邏輯順序不一致,這個過程就叫做指令的重排序。     指令重排序的意義:使指令更加符合CPU的執行特性,最大限度的發揮機器的效能,提高程式的執行效率。 看個demo
public static void main(String[] args) throws InterruptedException {
        int j=0;
        int k=0;
        j++;
        System.out.println(k);
        System.out.println(j);
    }

上面這段程式碼可能會被重排序:如下

public static void main(String[] args) throws InterruptedException {
        int k=0;
        System.out.println(k);
        int j=0;
        j++;
        System.out.println(j);
    }

此時指令的執行順序可以與程式碼邏輯順序不一致,但不影響程式的最終結果.

再看個demo

public class ThreadExample2 {

    static int i;
    public  static boolean runing = true;

    public static void main(String[] args) throws InterruptedException {
        traditional();
        Thread.sleep(100);
        runing = false;
    }

    public static void traditional() {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (runing){
                    i++;//沒有方法,JVM會做指令重排序,激進優化
                }
            }
        };
        thread.start();
    }
    
}

執行下main方法

 

 可以看出該程式一直在跑,不會停止.

此時jvm發現traditional方法內沒有其他方法,JVM會做指令重排序,採取激進優化策略,對我們的程式碼進行了重排序

如下:

static int i;
    public  static boolean runing = true;

    public static void main(String[] args) throws InterruptedException {
        traditional();
        Thread.sleep(100);
        runing = false;
    }

    public static void traditional() {
        Thread thread = new Thread() {
            boolean temp=runing;//注意這裡,此時while的條件永遠為true
            @Override
            public void run() {
                while (temp){
                    i++;//沒有方法,JVM會做指令重排序,激進優化
                }
            }
        };
        thread.start();
    }

因此程式不會停止.

我們稍微改動下程式碼,在while 迴圈里加個方法

static int i;
    public  static boolean runing = true;

    public static void main(String[] args) throws InterruptedException {
        traditional();
        Thread.sleep(100);
        runing = false;
    }

    public static void traditional() {
        boolean temp=runing;
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (runing){//
                    i++;//沒有方法,JVM會做指令重排序,激進優化
                    //有方法,JVM認為可能存在方法溢位,不做指令重排序,保守優化策略
                    aa();
                }
            }
        };
        thread.start();
    }

    public static void aa(){
        System.out.println("hello");
    }

看下結果

 

 可以看出,程式自行停止了,因為有方法,JVM認為可能存在方法溢位,不做指令重排序,採取保守優化策略

runing = false;

全域性變數runing 改動值以後,被thread執行緒識別,while 迴圈裡值變為false,就自動停止了.

ok,繼續,我們把main方法中的sleep()註釋掉,如下

public static void main(String[] args) throws InterruptedException {
        traditional();
        //Thread.sleep(100);
        runing = false;//會優先執行主執行緒的程式碼
    }

    public static void traditional() {
        boolean temp=runing;
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (runing){//
                    i++;
                }
            }
        };
        thread.start();
    }

看下結果:

 

 此時,程式停止了,這是為什麼呢:

可能是因為thread 執行緒和main執行緒競爭cpu資源的時候,會優先分配給main執行緒(我不確定,讀者們可以自己思考一下)

Java 中的鎖

synchronized關鍵字

  在1.6版本之前,synchronized都是重量級鎖

  1.6之後,synchronized被優化,因為互斥鎖比較笨重,如果執行緒沒有互斥,那就不需要互斥鎖

重量級鎖

1.當一個執行緒要訪問一個共享變數時,先用鎖把變數鎖住,然後再操作,操作完了之後再釋放掉鎖,完成

2.當另一個執行緒也要訪問這個變數時,發現這個變數被鎖住了,無法訪問,它就會一直等待,直到鎖沒了,它再給這個變數上個鎖,然後使用,使用完了釋放鎖,以此進行

3.我們可以這麼理解:重量級鎖是呼叫作業系統的函式來實現的鎖--mutex--互斥鎖

以linux為例:

1.互斥變數使用特定的資料型別:pthread_mutex_t結構體,可以認為這是一個函式
2.可以用pthread_mutex_init進行函式動態的建立 : int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * attr)
3.對鎖的操作主要包括加鎖 pthread_mutex_lock()、解鎖pthread_mutex_unlock()和測試加鎖 pthread_mutex_trylock()三個
3.1  int pthread_mutex_tlock(pthread_mutex_t *mutex) 在暫存器中對變數操作(加/減1)
3.2  int pthread_mutex_unlock(pthread_mutex_t *mutex) 釋放鎖,狀態恢復
3.3  int pthread_mutex_trylock(pthread_mutex_t *mutex)
 pthread_mutex_trylock()語義與pthread_mutex_lock()類似,不同的是在鎖已經被佔據時返回EBUSY而不是掛起等待

函式pthread_mutex_trylock會嘗試對互斥量加鎖,如果該互斥量已經被鎖住,函式呼叫失敗,返回EBUSY,否則加鎖成功返回0,執行緒不會被阻塞

偏向鎖

偏向鎖是synchronized鎖的物件沒有資源競爭的情況下存在的,不會一直呼叫作業系統函式實現(第一次會呼叫),而重量級鎖每次都會呼叫

 

看個demo

public class SyncDemo2 {

    Object o= new Object();

    public static void main(String[] args) {
        System.out.println("pppppppppppppppppppppp");
        SyncDemo2 syncDemo = new SyncDemo2();
        syncDemo.start();
    }

    public void start() {
        Thread thread = new Thread() {
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(500);
                        sync();
                    } catch (InterruptedException e) {

                    }
                }
            }
        };

        Thread thread2 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    try {
                       Thread.sleep(500);
                        sync();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        thread.setName("t1");
        thread2.setName("t2");
        //兩個執行緒競爭時,synchronized是重量級鎖,一個執行緒時,synchronized是偏向鎖
        thread.start();
        thread2.start();
    }

    //在1.6版本之前,synchronized都是重量級鎖
    //1.6之後,synchronized被優化,因為互斥鎖比較笨重,如果執行緒沒有互斥,那就不需要互斥鎖
    public void sync() {
        synchronized (o) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

程式碼很簡單,就是啟動兩個執行緒,並且呼叫同一個同步方法,看下結果

 

 可以看到,兩個執行緒都執行了該同步方法,此時兩個執行緒競爭,synchronized是重量級鎖

我們把一個執行緒註釋掉

//兩個執行緒競爭時,synchronized是重量級鎖,一個執行緒時,synchronized是偏向鎖
        thread.start();
        //thread2.start();

看下結果:

此時synchronized是偏向鎖

 那麼怎麼證明呢:我目前沒那個實力,給個思路.

1.需要編譯並修改linux原始碼函式pthread_mutex_lock(),在函式中列印當前執行緒的pid

2.在同步方法中列印語句"current id"+當前pid(需要自己寫c語言實現),java的Thread.currentThread().getId()不能獲取作業系統級別的pid

3.兩個執行緒競爭時,執行一次

 

 說明是重量級鎖,因為每次都呼叫作業系統的函式pthread_mutex_lock()來實現

4.註釋掉一個執行緒,再執行一次

 

 說明是偏向鎖,因為第一次會呼叫pthread_mutex_lock(),後面就不呼叫系統函數了.