Java鎖機制

Java多線程編程的入門篇,主要介紹volatile修飾詞、Synchronized以及Lock及其子類

多線程編程主要存在的問題是數(shù)據(jù)的同步問題,下面我們講講幾種保證數(shù)據(jù)同步的方法

volatile

要想了解volatile修飾詞就必須先說說并發(fā)編程中的3個概念:

原子性

原子性指的是:多個操作要么全部執(zhí)行不可中斷,要么全部不執(zhí)行。

可見性

可見性指的是:多線程操作同一個數(shù)據(jù)時,
其中一個線程操作完數(shù)據(jù)其他線程能立即看到數(shù)據(jù)的改變
(java中通過volatile來保證)。

有序性

有序性指的是:程序執(zhí)行過程按照代碼的順序執(zhí)行,
雖然編譯器和處理器可能會對指令進行重排序,
但是保證執(zhí)行的結(jié)果相同。

再來了解一下java的內(nèi)存模型,就可以知道java怎么通過volatile來保證數(shù)據(jù)的可見性

java的內(nèi)存模型

Java內(nèi)存模型中規(guī)定,所有的數(shù)據(jù)都存儲在主內(nèi)存中,
線程對要操作的數(shù)據(jù)從主線程拷貝到自己的工作內(nèi)存。
每個線程只能操作自己工作內(nèi)存中的數(shù)據(jù),不能直接操作主內(nèi)存中的數(shù)據(jù),
也不能訪問其他線程的工作內(nèi)存。
同時線程操作的數(shù)據(jù)可能存在緩存中,并不直接刷新到主內(nèi)存中,

從上面可以知道,如果不保證數(shù)據(jù)的可見性,多個線程一起操作同一份數(shù)據(jù)時,并不能保證線程從主線程拷貝下來的數(shù)據(jù)是最新的,這樣會讓計算結(jié)果產(chǎn)生偏差。這個時候volatile就起作用了,被volatile修飾的變量,在被線程操作時數(shù)據(jù)的改變能直接刷新到主內(nèi)存中,保證這個操作產(chǎn)生的新數(shù)據(jù)對其他線程是可見的,其他線程從主內(nèi)存讀取到是最新的數(shù)據(jù)。

同時volatile還有另外一個作用,被volatile修飾的變量將不能被編譯器和處理器指令重排序。也就是volatile之后的語句不能提前到volatile之前,同時volatile之前的語句也不能排列到volatile之后。

示例:

class Singleton{
    private volatile static Singleton instance = null;
     
    private Singleton() {
         
    }
     
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

參考文獻Java并發(fā)編程:volatile關(guān)鍵字解析

Synchronized

Synchronized同步鎖比較好理解,可以作用于代碼塊、成員方法以及靜態(tài)方。但是要求鎖住的部分,持有的是同一個鎖對象,這時才能Synchronized起作用。

作用于代碼塊

某些情況下,我們可能在方法的一部分出現(xiàn)耗時操作,為了避免鎖住整個方法可以用Synchronized同步代碼塊

public class SynchronizedTest {

    public static int sInt = 0;

    public static void main(String[] args) {

        new Thread(new MyRunnable(SynchronizedTest.class, false)).start();
        new Thread(new MyRunnable(SynchronizedTest.class, true)).start();
    }

    private static class MyRunnable implements Runnable {

        private final Object lock;

        boolean flag;

        public MyRunnable(Object lock, boolean flag) {
            this.lock = lock;
            this.flag = flag;
        }

        @Override
        public void run() {

            if (flag) {

                synchronized (lock) {

                    for (int i = 0; i < 1000; i++) {
                        sInt++;
                    }

                    System.out.println("calc finish");
                }
            } else {
                System.out.println("hello world");
            }
        }
    }
}

打印如下:

hello world 1525252712746
calc finish 1525252712747

可以看出同步訪問

作用于成員方法

這個很好理解,我們需要鎖住一整個方法,直接在方法上添加Synchronized即可

public class SynchronizedTest {

    public static int sInt = 0;

    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.Test();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.Test();
            }
        }).start();
    }

    private synchronized void Test() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("Test finish at " + System.currentTimeMillis());
        }
    }
}

打印如下:

Test finish at 1525253058767
Test finish at 1525253060770

可以看出兩者的打印差了2秒以上。

這里需要特別說明一下,當Synchronized作用在成員方法上時其實是有鎖對象的,持有的對象為當前的實例對象,這個和作用在靜態(tài)方法上時有所區(qū)別,這個等下說。先看一下,不同實例時,成員方法上的鎖是否生效。

public class SynchronizedTest {

    public static int sInt = 0;

    public static void main(String[] args) {
        SynchronizedTest testOne = new SynchronizedTest();
        SynchronizedTest testTwo = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testOne.Test();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testTwo.Test();
            }
        }).start();
    }

    private synchronized void Test() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("Test finish at " + System.currentTimeMillis());
        }
    }
}

打印如下:

Test finish at 1525253495010
Test finish at 1525253495010

同時執(zhí)行完畢,不同對象時鎖依然存在,只是持有的鎖對象不是同一個,所以才會同步打印。

作用于靜態(tài)方法

public class SynchronizedTest {

    public static int sInt = 0;

    public static void main(String[] args) {
        SynchronizedTest testOne = new SynchronizedTest();
        SynchronizedTest testTwo = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testOne.Test();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testTwo.Test();
            }
        }).start();
    }

    public synchronized static void Test() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("Test finish at " + System.currentTimeMillis());
        }
    }
}

打印如下:

Test finish at 1525253718091
Test finish at 1525253720095

這里和synchronized作用于成員方法時有區(qū)別,當synchronized作用于靜態(tài)方法時,持有的鎖對象為當前類的.class對象,所有不同的實例來操作都是同步的,因為.class對象是唯一的。

synchronized可重入性

先看一個示例

public class SynchronizedTest {

    private final Object mLock = new Object();

    public static void main(String[] args) {
        new SynchronizedTest().TestOne();
        System.out.println();
        new SynchronizedTest().TestFour();
    }


    private synchronized void TestOne() {
        System.out.println("Test One");
        TestTwo();
    }

    private synchronized void TestTwo() {
        System.out.println("Test Two");
        TestThree();
    }

    private synchronized void TestThree() {
        System.out.println("Test Three");
    }

    private void TestFour() {
        synchronized (mLock) {
            System.out.println("Test Four");
            TestFive();
        }
    }

    private void TestFive() {
        synchronized (mLock) {
            System.out.println("Test Five");
        }
    }
}

打印如下:

Test One
Test Two
Test Three

Test Four
Test Five

可以看出,程序并沒有產(chǎn)生死鎖,正常輸入,這就是synchronized可重入性


在講Lock前,先講幾個Object的方法

wait()、notify()以及notifyAll()

這幾個方法是Object中的方法,所有每個鎖對象都能調(diào)用

  1. wait()只有在同步代碼塊或者同步方法中才能調(diào)用,因為要保證當前線程持有鎖對象。wait()的作用是讓當前執(zhí)行代碼的線程進入等待狀態(tài),直到為喚起或者中斷。
  2. notify()作用是將等待狀態(tài)的線程喚起,讓它等待獲取鎖對象。不同于notifyAll(),如果有多個線程需要喚起,notify()會隨機挑選一個線程進行喚起,其他線程繼續(xù)等待。notify()也需要獲取到鎖對象,同時被喚起的線程也不能立即被執(zhí)行,因為需要等待notify()的線程執(zhí)行完畢并釋放鎖。
  3. notifyAll()喚醒所有的等待線程,等到notifyAll()釋放鎖后同步執(zhí)行

看一個notify的示例:

public class SynchronizedTest {

    public static void main(String[] args) {

        try {
            new Thread(new RunnableOne(Object.class)).start();
            Thread.sleep(1000);
            new Thread(new RunnableTwo(Object.class)).start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    private static class RunnableOne implements Runnable {

        private final Object mLock;

        RunnableOne(Object lock) {
            mLock = lock;
        }

        @Override
        public void run() {

            synchronized (mLock) {
                try {
                    System.out.println("wait start " + System.currentTimeMillis());
                    mLock.wait();
                    System.out.println("wait finish " + System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static class RunnableTwo implements Runnable {

        private final Object mLock;

        RunnableTwo(Object lock) {
            mLock = lock;
        }

        @Override
        public void run() {

            synchronized (mLock) {
                try {
                    Thread.sleep(2000);
                    mLock.notify();
                    System.out.println("notify thread " + System.currentTimeMillis());
                    Thread.sleep(2000);
                    System.out.println("notify finish " + System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

打印如下:

wait start 1525261601930
notify thread 1525261604937
notify finish 1525261606940
wait finish 1525261606941

可以清楚的看到notify結(jié)束后wait才執(zhí)行,符合我們的預期

再來看一個notifyAll的示例,將上面的代碼啟動多個RunnableOne,notify()改成notifyAll()

打印如下:

wait start 1525261838049
wait start 1525261838049
wait start 1525261838050
wait start 1525261838050
wait start 1525261838050
notify thread 1525261841060
notify finish 1525261843065
wait finish 1525261843065
wait finish 1525261843065
wait finish 1525261843065
wait finish 1525261843065
wait finish 1525261843066
死鎖

死鎖指的是某一線程一直持有鎖對象不釋放,其他線程無法獲取鎖對象的過程。產(chǎn)生死鎖一般的原因都是多個鎖對象一起使用,同時相互請求鎖對象導致的。

看個示例:

public class SynchronizedTest {

    public static final Object LOCK_ONE = new Object();
    public static final Object LOCK_TWO = new Object();

    public static void main(String[] args) {
        try {
            new Thread(new RunnableOne()).start();
            Thread.sleep(1000);
            new Thread(new RunnableTwo()).start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static class RunnableOne implements Runnable {

        @Override
        public void run() {
            try {
                synchronized (LOCK_ONE) {
                    Thread.sleep(2000);
                    synchronized (LOCK_TWO) {
                        System.out.println(System.currentTimeMillis() + "");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static class RunnableTwo implements Runnable {

        @Override
        public void run() {
            try {
                synchronized (LOCK_TWO) {
                    Thread.sleep(2000);
                    synchronized (LOCK_ONE) {
                        System.out.println(System.currentTimeMillis() + "");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

直接死鎖無打印,因為這里第一個線程啟動的時候獲取到了LOCK_ONE,接著睡眠,線程二啟動,獲取到了LOCK_TWO,睡眠。當?shù)谝粋€線程再次起來去獲取LOCK_TWO時,等待LOCK_TWO被釋放;當?shù)诙€線程再次起來去獲取LOCK_ONE時,等待LOCK_ONE釋放,相互等待對方釋放鎖,也即進入死鎖狀態(tài)。

Lock

Lock只是定義的一個接口,直接實現(xiàn)類是ReentrantLock,相對于synchronized鎖,Lock可以在需要的情況下自我中斷。下面來看一下ReentrantLock使用

public class LockTest {

    public static void main(String[] args) {
        try {
            ReentrantLock reentrantLock = new ReentrantLock();
            new Thread(new RunnableOne(reentrantLock)).start();
            Thread.sleep(1000);
            new Thread(new RunnableTwo(reentrantLock)).start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static class RunnableOne implements Runnable {

        final ReentrantLock mLock;

        public RunnableOne(ReentrantLock lock) {
            mLock = lock;
        }

        @Override
        public void run() {
            try {
                mLock.lock();
                System.out.println("RunnableOne get lock " + System.currentTimeMillis());
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                mLock.unlock();
            }
        }
    }

    private static class RunnableTwo implements Runnable {

        final ReentrantLock mLock;

        public RunnableTwo(ReentrantLock lock) {
            mLock = lock;
        }

        @Override
        public void run() {
            try {
                mLock.lock();
                System.out.println("RunnableTwo get lock " + System.currentTimeMillis());
            } finally {
                mLock.unlock();
            }
        }
    }
}

打印如下:

RunnableOne get lock 1525316728346
RunnableTwo get lock 1525316733350

線程2一直阻塞,等到線程1釋放鎖后才進行了打印,這和synchronized沒區(qū)別

公平鎖與非公平鎖

當然ReentrantLock還有其他的特性,比如可以在實例化時指定是否為公平鎖。公平鎖和非公平鎖的區(qū)別就是,公平鎖所有線程按入隊的順序排隊獲取鎖,非公平鎖就是搶占機制,先入隊并不一定獲取到鎖。

public class LockTest {

    public static void main(String[] args) {

        ReentrantLock reentrantLock = new ReentrantLock(false);
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(new RunnableOne(reentrantLock));
        }

        for (Thread thread : threads) {
            thread.start();
        }
    }

    private static class RunnableOne implements Runnable {

        final ReentrantLock mLock;

        RunnableOne(ReentrantLock lock) {
            mLock = lock;
        }

        @Override
        public void run() {

            try {
                mLock.lock();
                System.out.println(Thread.currentThread().getName() + " get lock ");
            } finally {
                mLock.unlock();
            }
        }
    }
}

非公平鎖的情況下,打印如下:

Thread-0 get lock 
Thread-4 get lock 
Thread-1 get lock 
Thread-2 get lock 
Thread-3 get lock 

提前啟動的線程并沒有先得到鎖

再來看一下公平鎖,只需要將ReentrantLock實例化時fair參數(shù)傳入true即可,看一下打?。?/p>

Thread-0 get lock 
Thread-1 get lock 
Thread-2 get lock 
Thread-3 get lock 
Thread-4 get lock 

符合先啟動的線程先獲取鎖的預期。

Lock中斷

相對于synchronized對線程中斷的不處理,Lock還有一個方法lockInterruptibly(),可以讓用戶在必要的情況下自行中斷對鎖的請求,然后繼續(xù)后續(xù)的操作。java的中斷機制后續(xù)再寫

來看一個例子

public class LockTest {

    public static void main(String[] args) {

        try {
            ReentrantLock reentrantLock = new ReentrantLock();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    reentrantLock.lock();
                }
            }).start();

            Thread.sleep(1000);

            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        reentrantLock.lockInterruptibly();
                    } catch (InterruptedException e) {
                        System.out.println("我被中斷了");
                    }
                }
            });
            thread.start();
            Thread.sleep(1000);

            thread.interrupt();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

打印如下:

我被中斷了

很明顯的一個區(qū)別,當使用synchronized時,并不需要你去捕捉異常,因為線程會一直阻塞在等待鎖的位置,就算你主動去thread.interrupt() synchronized并不會做任何處理。

lock.lockInterruptibly()使用時就需要你主動將此處的代碼try/catch,因為在線程中斷后,這里會捕捉異常并拋給你處理。這也給了你更靈活處理事物的能力。

Condition

synchronized中可以用鎖對象的wait()、notify()以及notifyAll()實現(xiàn)等待,當然Lock也可以實現(xiàn),不過子類實現(xiàn)相對應(yīng)的Condition。

相對應(yīng)的Condition里面的3個方法是await()、signal()、signalAll()。與wait()調(diào)用一樣,await()之前也需要獲取到鎖對象,也就是Lock先要lock(),直接看例子

public class LockTest {

    public static void main(String[] args) {

        try {
            ReentrantLock reentrantLock = new ReentrantLock();
            Condition condition = reentrantLock.newCondition();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        reentrantLock.lock();
                        System.out.println("wait start");
                        condition.await();
                        System.out.println("wait finish");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        reentrantLock.unlock();
                    }
                }
            }).start();


            Thread.sleep(1000);

            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        reentrantLock.lock();
                        System.out.println("signal start");
                        condition.signal();
                        System.out.println("signal finish");
                    } finally {
                        reentrantLock.unlock();
                    }
                }
            });
            thread.start();
            Thread.sleep(1000);

            thread.interrupt();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

打印如下:

wait start
signal start
signal finish
wait finish

ReentrantLock的其他諸如isLocked()等方法這里就不再花篇幅說明,有興趣的同學可以自己查看源碼

總結(jié)

總結(jié)一下這篇文章的知識點:

  1. volatile修飾詞:保證了操作的可見性,保證多線程操作時,數(shù)據(jù)從主內(nèi)存拷貝的都是最新的
  2. Synchronized修飾詞:可作用于代碼塊、成員方法已經(jīng)靜態(tài)方法上,配合鎖對象的wait()、nofity()以及nofityAll()可以實現(xiàn)等待/通知模型。
    • 作用在代碼塊上時需要指定鎖對象
    • 作用于成員方法上時鎖對象為當前實例對象也就是xxx.this
    • 作用與靜態(tài)方法上時鎖對象為當前類的class對象也就是xxx.class
    • wait()、nofity()以及nofityAll()調(diào)用前需要獲得鎖對象。
  3. Lock:與Synchronized相比較用法就是需要用戶手動上鎖以及釋放鎖對象,同時多了一個lockInterruptibly()可以讓用戶手動中斷等待線程,并捕獲異常進行后續(xù)處理。配合Condition可以實現(xiàn)與Synchronized一樣的等待/通知模型.
  4. 死鎖:死鎖指的是某一線程一直持有鎖對象不釋放,其他線程無法獲取鎖對象的過程。產(chǎn)生死鎖一般的原因都是多個鎖對象一起使用,同時相互請求鎖對象導致的。

以上是個人能想到的關(guān)于鎖的內(nèi)容,并不保證寫的完全對,如有問題可以留言給我,共同學習。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容