java鎖機制

JAVA中鎖機制的實現(xiàn)主要有兩種,一種是基于JVM層面的synchronized 另一種是基于JAVA語言層面的Lock。

synchronized

synchronized是可重入不可中斷的。synchronized的用法主要有三種?;陟o態(tài)方法,基于實例方法,基于代碼塊(需顯示指定鎖的對象)。
1.作用于實例方法,當前實例加鎖,進入同步代碼前要獲得當前實例的鎖;
2.作用于靜態(tài)方法,當前類加鎖,進去同步代碼前要獲得當前類對象的鎖;
3.作用于代碼塊,這需要指定加鎖的對象,對所給的指定對象加鎖,進入同步代碼前要獲得指定對象的鎖。

synchronized修飾實例方法(普通方法)

使用時,作用范圍為整個函數(shù),這里所謂的實例鎖就是調(diào)用該實例方法(不包括靜態(tài)方法)的對象。

【demo1】

public class SyncTest implements Runnable{
    //共享資源變量
    int count = 0;
    @Override
    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
            increaseCount();
            System.out.println(Thread.currentThread().getName()+":"+count++);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SyncTest syncTest1 = new SyncTest();
//        SyncTest syncTest2 = new SyncTest();
        Thread thread1 = new Thread(syncTest1,"thread1");
        Thread thread2 = new Thread(syncTest1, "thread2");
        thread1.start();
        thread2.start();
    }
}
 /**
     * 輸出結果
     thread1:0
     thread1:1
     thread1:2
     thread1:3
     thread1:4
     thread2:5
     thread2:6
     thread2:7
     thread2:8
     thread2:9
     */

代碼中開啟了兩個線程去操作一個變量(共享變量),count++是先讀取值,再寫回一個新值。我們想一下,如果第一個線程執(zhí)行這一過程中,第二個線程拿到寫回之前的count值,做count++操作,那么這就造成了線程不安全。所以這里在run方法加上synchronized,獲取一個對象鎖,代碼中的實例鎖就是syncTest1了。
??同時我們從輸出結果看出:當一個線程正在訪問一個對象synchronized實例方法時,別的線程是訪問不了的。一個對象一把鎖說的就是這個,當線程獲取了該對象的鎖后,其他線程無法獲取該對象的鎖,當然就訪問不了該對象的synchronized方法,但是!但是!但是!可以訪問該對象的其他未被synchronized修飾的方法。
??如果是一個線程 A 需要訪問實例對象 obj1 的 synchronized 方法 f1(當前對象鎖是obj1),另一個線程 B 需要訪問實例對象 obj2 的 synchronized 方法 f2(當前對象鎖是obj2),這樣是允許的,因為兩個實例對象鎖并不同相同,此時如果兩個線程操作數(shù)據(jù)并非共享的,線程安全是有保障的,遺憾的是如果兩個線程操作的是共享數(shù)據(jù),那么線程安全就有可能無法保證了。我們把上面代碼中的main方法中的注釋放開,表達這一線程不安全的現(xiàn)象
【demo2】

public class SyncTest implements Runnable{
    //共享資源變量
    int count = 0;
    @Override
    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+":"+count++);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        SyncTest syncTest1 = new SyncTest();
        SyncTest syncTest2 = new SyncTest();
        Thread thread1 = new Thread(syncTest1,"thread1");
        Thread thread2 = new Thread(syncTest2, "thread2");
        thread1.start();
        thread2.start();
    }
    /**
     * 輸出結果
        thread1:0
        thread2:0
        thread1:1
        thread2:1
        thread1:2
        thread2:2
        thread1:3
        thread2:3
        thread1:4
        thread2:4
     */
}

我們從輸出結果來看,兩個線程可能同時拿到共享變量去做count++操作。上述操作中雖然我們的run方法還是使用synchronized修飾,但是我們new了兩個實例。這就意味存在了兩個不同的實例鎖,thread1和thread2分別進入了syncTest1和syncTest2的實例鎖,當然保證不了線程安全。但是我們也有解決方案啦:如果synchronized修飾的是靜態(tài)方法呢?下面我們再介紹修飾靜態(tài)方法。
synchronized修飾靜態(tài)方法
??我們知道靜態(tài)方法是不屬于當前實例的,而是屬性類的,那么這個鎖就是類的class對象鎖,上述問題引刃而解,請看代碼:
【demo3】

public class SyncTest implements Runnable {
    //共享資源變量
    static int count = 0;
    @Override
    public synchronized void run() {
        increaseCount();
    }

    private synchronized static void increaseCount() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + count++);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SyncTest syncTest1 = new SyncTest();
        SyncTest syncTest2 = new SyncTest();
        Thread thread1 = new Thread(syncTest1, "thread1");
        Thread thread2 = new Thread(syncTest2, "thread2");
        thread1.start();
        thread2.start();
    }
    /**
     輸出結果
     thread1:0
     thread1:1
     thread1:2
     thread1:3
     thread1:4
     thread2:5
     thread2:6
     thread2:7
     thread2:8
     thread2:9
     */
}

瞧瞧輸出結果,問題解決了沒?同樣是new了兩個不同實例,卻保持了線程同步。那是我們synchronizd修飾的是靜態(tài)方法,run方法中調(diào)用這個靜態(tài)方法,再說一次 靜態(tài)方法不屬于當前實例,而是屬于類。所以這個方案其實是用的一個把鎖,而這個鎖就是這個類的class對象鎖。
??需要注意的是如果一個線程A調(diào)用一個實例對象的非static synchronized方法,而線程B需要調(diào)用這個實例對象所屬類的靜態(tài) synchronized方法,是允許的,不會發(fā)生互斥現(xiàn)象,因為訪問靜態(tài) synchronized 方法占用的鎖是當前類的class對象,而訪問非靜態(tài) synchronized 方法占用的鎖是當前實例對象鎖(結合demo2,demo3)。

synchronized修飾代碼塊

首先這個使用時的場景是:在某些情況下,我們編寫的方法體可能比較大,同時存在一些比較耗時的操作,而需要同步的代碼又只有一小部分,如果直接對整個方法進行同步操作,可能會得不償失,此時我們可以使用同步代碼塊的方式對需要同步的代碼進行包裹,這樣就無需對整個方法進行同步操作了。所以他的作用范圍為synchronizd(obj){}的這個大括號中
【demo4】

public class SyncTest implements Runnable {
    //共享資源變量
    static int count = 0;
    private byte[] mBytes = new byte[0];

    @Override
    public synchronized void run() {
        increaseCount();
    }

    private void increaseCount() {
        //假設省略了其他操作的代碼。
        //……………………
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + count++);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SyncTest syncTest1 = new SyncTest();
        SyncTest syncTest2 = new SyncTest();
        Thread thread1 = new Thread(syncTest1, "thread1");
        Thread thread2 = new Thread(syncTest2, "thread2");
        thread1.start();
        thread2.start();
    }
    /**
     * 輸出結果
     thread1:0
     thread2:0
     thread1:1
     thread2:2
     thread2:4
     thread1:3
     thread2:5
     thread1:5
     thread2:7
     thread1:6
     */
}

從輸出結果看出,這個demo并沒有保證線程安全,因為我們指定鎖為this,指的就是調(diào)用這個方法的實例對象。這里我們new了兩個不同的實例對象syncTest1,syncTest2,所以有兩個鎖,thread1與thread2分別進入自己傳入的對象鎖的線程執(zhí)行increaseCount方法,做成線程不安全。如果把demo4的成員變量注釋放開,并將mBytes傳入synchronized后面的括號中,也是線程不安全的結果。這里之所以加上mBytes這個對象是為了說明synchronized后面的括號中是可以指定任意對象充當鎖的,而零長度的byte數(shù)組對象創(chuàng)建起來將比任何對象都經(jīng)濟。當然,如果要使用這個經(jīng)濟實惠的鎖并保證線程安全,那就不能new出多個不同實例對象出來啦。如果你非要想new兩個不同對象出來,又想保證線程同步的話,那么synchronized后面的括號中可以填入SyncTest.class,表示這個類對象作為鎖,自然就能保證線程同步啦。使用方法為:

synchronized(xxxx.class){
//todo
}

總結

修飾普通方法 一個對象中的加鎖方法只允許一個線程訪問。但要注意這種情況下鎖的是訪問該方法的實例對象, 如果多個線程不同對象訪問該方法,則無法保證同步。

修飾靜態(tài)方法 由于靜態(tài)方法是類方法, 所以這種情況下鎖的是包含這個方法的類,也就是類對象;這樣如果多個線程不同對象訪問該靜態(tài)方法,也是可以保證同步的。

修飾代碼塊 其中普通代碼塊 如Synchronized(obj) 這里的obj 可以為類中的一個屬性、也可以是當前的對象,它的同步效果和修飾普通方法一樣;Synchronized方法 (obj.class)靜態(tài)代碼塊它的同步效果和修飾靜態(tài)方法類似。

synchronized的缺陷

我們了解到如果一個代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,并執(zhí)行該代碼塊時,其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這里獲取鎖的線程釋放鎖只會有兩種情況:

1)獲取鎖的線程執(zhí)行完了該代碼塊,然后線程釋放對鎖的占有;
2)線程執(zhí)行發(fā)生異常,此時JVM會讓線程自動釋放鎖。

那么如果這個獲取鎖的線程由于要等待IO或者其他原因(比如調(diào)用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便只能干巴巴地等待,試想一下,這多么影響程序執(zhí)行效率。

因此就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間或者能夠響應中斷),通過Lock就可以辦到。

再舉個例子:當有多個線程讀寫文件時,讀操作和寫操作不會發(fā)生沖突現(xiàn)象,寫操作和寫操作會發(fā)生沖突現(xiàn)象,但是讀操作和讀操作不會發(fā)生沖突現(xiàn)象。

但是采用synchronized關鍵字來實現(xiàn)同步的話,就會導致一個問題:

如果多個線程都只是進行讀操作,所以當一個線程在進行讀操作時,其他線程只能等待無法進行讀操作。

因此就需要一種機制來使得多個線程都只是進行讀操作時,線程之間不會發(fā)生沖突,通過Lock就可以辦到。

另外,通過Lock可以知道線程有沒有成功獲取到鎖。這個是synchronized無法辦到的。

也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點:

1)Lock不是Java語言內(nèi)置的,synchronized是Java語言的關鍵字,因此是內(nèi)置特性。Lock是一個類,通過這個類可以實現(xiàn)同步訪問;
2)Lock和synchronized有一點非常大的不同,采用synchronized不需要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執(zhí)行完之后,系統(tǒng)會自動讓線程釋放對鎖的占用;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現(xiàn)死鎖現(xiàn)象。

Lock

下面我們就來探討一下java.util.concurrent.locks包中常用的類和接口。

1.Lock

首先要說明的就是Lock,通過查看Lock的源碼可知,Lock是一個接口:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

下面來逐個講述Lock接口中每個方法的使用,lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的。unLock()方法是用來釋放鎖的。newCondition()這個方法暫且不在此講述,會在后面的線程協(xié)作一文中講述。

在Lock中聲明了四個方法來獲取鎖,那么這四個方法有何區(qū)別呢?

首先lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。

由于在前面講到如果采用Lock,必須主動去釋放鎖,并且在發(fā)生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,并且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發(fā)生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //處理任務
}catch(Exception ex){
     
}finally{
    lock.unlock();   //釋放鎖
}

tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失?。存i已被其他線程獲?。?,則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區(qū)別在于這個方法在拿不到鎖時會等待一定的時間,在時間期限之內(nèi)如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內(nèi)拿到了鎖,則返回true。

所以,一般情況下通過tryLock來獲取鎖時是這樣使用的:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //如果不能獲取鎖,則直接做其他事情
}

lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態(tài)。也就使說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那么對線程B調(diào)用threadB.interrupt()方法能夠中斷線程B的等待過程。

由于lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調(diào)用lockInterruptibly()的方法外聲明拋出InterruptedException。因此lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

注意,當一個線程獲取了鎖之后,是不會被interrupt()方法中斷的。因為本身在前面的文章中講過單獨調(diào)用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。

因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。

而用synchronized修飾的話,當一個線程處于等待某個鎖的狀態(tài),是無法被中斷的,只有一直等待下去。

2.ReentrantLock

ReentrantLock,意思是“可重入鎖”,關于可重入鎖的概念在下一節(jié)講述。ReentrantLock是唯一實現(xiàn)了Lock接口的類,并且ReentrantLock提供了更多的方法。下面通過一些實例看具體看一下如何使用ReentrantLock。

例子1,lock()的正確使用方法

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        Lock lock = new ReentrantLock();    //注意這個地方
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了鎖");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"釋放了鎖");
            lock.unlock();
        }
    }
}

各位朋友先想一下這段代碼的輸出結果是什么?

Thread-0得到了鎖
Thread-1得到了鎖
Thread-0釋放了鎖
Thread-1釋放了鎖

也許有朋友會問,怎么會輸出這個結果?第二個線程怎么會在第一個線程釋放鎖之前得到了鎖?原因在于,在insert方法中的lock變量是局部變量,每個線程執(zhí)行該方法時都會保存一個副本,那么理所當然每個線程執(zhí)行到lock.lock()處獲取的是不同的鎖,所以就不會發(fā)生沖突。

知道了原因改起來就比較容易了,只需要將lock聲明為類的屬性即可。

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意這個地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了鎖");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"釋放了鎖");
            lock.unlock();
        }
    }
}

這樣就是正確地使用Lock的方法了。

例子2,tryLock()的使用方法

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意這個地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        if(lock.tryLock()) {
            try {
                System.out.println(thread.getName()+"得到了鎖");
                for(int i=0;i<5;i++) {
                    arrayList.add(i);
                }
            } catch (Exception e) {
                // TODO: handle exception
            }finally {
                System.out.println(thread.getName()+"釋放了鎖");
                lock.unlock();
            }
        } else {
            System.out.println(thread.getName()+"獲取鎖失敗");
        }
    }
}

輸出結果:

Thread-0得到了鎖
Thread-1獲取鎖失敗
Thread-0釋放了鎖

例子3,lockInterruptibly()響應中斷的使用方法:

public class Test {
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
        Test test = new Test();
        MyThread thread1 = new MyThread(test);
        MyThread thread2 = new MyThread(test);
        thread1.start();
        thread2.start();
         
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }  
     
    public void insert(Thread thread) throws InterruptedException{
        lock.lockInterruptibly();   //注意,如果需要正確中斷等待鎖的線程,必須將獲取鎖放在外面,然后將InterruptedException拋出
        try {  
            System.out.println(thread.getName()+"得到了鎖");
            long startTime = System.currentTimeMillis();
            for(    ;     ;) {
                if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
                    break;
                //插入數(shù)據(jù)
            }
        }
        finally {
            System.out.println(Thread.currentThread().getName()+"執(zhí)行finally");
            lock.unlock();
            System.out.println(thread.getName()+"釋放了鎖");
        }  
    }
}
 
class MyThread extends Thread {
    private Test test = null;
    public MyThread(Test test) {
        this.test = test;
    }
    @Override
    public void run() {
         
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被中斷");
        }
    }
}

運行之后,發(fā)現(xiàn)thread2能夠被正確中斷。

3.ReadWriteLock

ReadWriteLock也是一個接口,在它里面只定義了兩個方法:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();
 
    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。下面的ReentrantReadWriteLock實現(xiàn)了ReadWriteLock接口。

4.ReentrantReadWriteLock

ReentrantReadWriteLock里面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。

下面通過幾個例子來看一下ReentrantReadWriteLock具體用法。

假如有多個線程要同時進行讀操作的話,先看一下synchronized達到的效果:

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public synchronized void get(Thread thread) {
        long start = System.currentTimeMillis();
        while(System.currentTimeMillis() - start <= 1) {
            System.out.println(thread.getName()+"正在進行讀操作");
        }
        System.out.println(thread.getName()+"讀操作完畢");
    }
}

這段程序的輸出結果會是,直到thread1執(zhí)行完讀操作之后,才會打印thread2執(zhí)行讀操作的信息。

Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0讀操作完畢
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1讀操作完畢

而改成用讀寫鎖的話:

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public void get(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
             
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在進行讀操作");
            }
            System.out.println(thread.getName()+"讀操作完畢");
        } finally {
            rwl.readLock().unlock();
        }
    }
}

此時打印的結果為:

Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0讀操作完畢
Thread-1讀操作完畢

說明thread1和thread2在同時進行讀操作。這樣就大大提升了讀操作的效率。
不過要注意的是,如果有一個線程已經(jīng)占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。
如果有一個線程已經(jīng)占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。
關于ReentrantReadWriteLock類中的其他方法感興趣的朋友可以自行查閱API文檔。

5.Lock和synchronized的選擇

總結來說,Lock和synchronized有以下幾點不同:

1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內(nèi)置的語言實現(xiàn);

2)synchronized在發(fā)生異常時,會自動釋放線程占有的鎖,因此不會導致死鎖現(xiàn)象發(fā)生;而Lock在發(fā)生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現(xiàn)象,因此使用Lock時需要在finally塊中釋放鎖;

3)Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;

4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。

5)Lock可以提高多個線程進行讀操作的效率。

在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優(yōu)于synchronized。所以說,在具體使用時要根據(jù)適當情況選擇。

相關概念
2、ReadWriteLock****是什么?

讀寫鎖:分為讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由jvm自己控制的,我們只要上好相應的鎖即可。如果你的代碼只讀數(shù)據(jù),可以很多人同時讀,但不能同時寫,那就上讀鎖;如果你的代碼修改數(shù)據(jù),只能有一個人在寫,且不能同時讀取,那就上寫鎖??傊x的時候上讀鎖,寫的時候上寫鎖!讀寫鎖接口:ReadWriteLock,它的具體實現(xiàn)類為:ReentrantReadWriteLock。

《ReadWriteLock場景應用》:在多線程的環(huán)境下,對同一份數(shù)據(jù)進行讀寫,會涉及到線程安全的問題。比如在一個線程讀取數(shù)據(jù)的時候,另外一個線程在寫數(shù)據(jù),而導致前后數(shù)據(jù)的不一致性;一個線程在寫數(shù)據(jù)的時候,另一個線程也在寫,同樣也會導致線程前后看到的數(shù)據(jù)的不一致性。這時候可以在讀寫方法中加入互斥鎖,任何時候只能允許一個線程的一個讀或?qū)懖僮?,而不允許其他線程的讀或?qū)懖僮鳎@樣是可以解決這樣以上的問題,但是效率卻大打折扣了。因為在真實的業(yè)務場景中,一份數(shù)據(jù),讀取數(shù)據(jù)的操作次數(shù)通常高于寫入數(shù)據(jù)的操作,而線程與線程間的讀讀操作是不涉及到線程安全的問題,沒有必要加入互斥鎖,只要在讀-寫,寫-寫期間上鎖就行了。API調(diào)用請移步

3、鎖機制有什么用

有些業(yè)務邏輯在執(zhí)行過程中要求對數(shù)據(jù)進行排他性的訪問,于是需要通過一些機制保證在此過程中數(shù)據(jù)被鎖住不會被外界修改,這就是所謂的鎖機制。

4、什么是樂觀鎖(Optimistic Locking)?如何實現(xiàn)樂觀鎖?如何避免ABA問題

悲觀鎖(Pessimistic Lock), 顧名思義,就是很悲觀,每次去拿數(shù)據(jù)的時候都認為別人會修改,所以每次在拿數(shù)據(jù)的時候都會上鎖,這樣別人想拿這個數(shù)據(jù)就會block直到它拿到鎖。傳統(tǒng)的關系型數(shù)據(jù)庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿數(shù)據(jù)的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù),可以使用版本號等機制。樂觀鎖適用于多讀的應用類型,這樣可以提高吞吐量,像數(shù)據(jù)庫如果提供類似于write_condition機制的其實都是提供的樂觀鎖。

5、解釋以下名詞:重排序,自旋鎖,偏向鎖,輕量級鎖,可重入鎖,公平鎖,非公平鎖,樂觀鎖,悲觀鎖

重入鎖(ReentrantLock)是一種遞歸無阻塞的同步機制。重入鎖,也叫做遞歸鎖,指的是同一線程 外層函數(shù)獲得鎖之后 ,內(nèi)層遞歸函數(shù)仍然有獲取該鎖的代碼,但不受影響。在JAVA環(huán)境下 ReentrantLock 和synchronized 都是 可重入鎖。

自旋鎖,由于自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠高于互斥鎖。如何旋轉呢?何為自旋鎖,就是如果發(fā)現(xiàn)鎖定了,不是睡眠等待,而是采用讓當前線程不停地的在循環(huán)體內(nèi)執(zhí)行實現(xiàn)的,當循環(huán)的條件被其他線程改變時 才能進入臨界區(qū)。

偏向鎖(Biased Locking)是Java6引入的一項多線程優(yōu)化,它會偏向于第一個訪問鎖的線程,如果在運行過程中,同步鎖只有一個線程訪問,不存在多線程爭用的情況,則線程是不需要觸發(fā)同步的,這種情況下,就會給線程加一個偏向鎖。 如果在運行過程中,遇到了其他線程搶占鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。

輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的情況下,當?shù)诙€線程加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖。

重入鎖(ReentrantLock)是一種遞歸無阻塞的同步機制,也叫做遞歸鎖,指的是同一線程 外層函數(shù)獲得鎖之后 ,內(nèi)層遞歸函數(shù)仍然有獲取該鎖的代碼,但不受影響。 在JAVA環(huán)境下 ReentrantLock 和synchronized 都是 可重入鎖。

公平鎖,就是很公平,在并發(fā)環(huán)境中,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果為空,或者當前線程線程是等待隊列的第一個,就占有鎖,否則就會加入到等待隊列中,以后會按照FIFO的規(guī)則從隊列中取到自己

非公平鎖比較粗魯,上來就直接嘗試占有鎖,如果嘗試失敗,就再采用類似公平鎖那種方式。

6、什么時候應該使用可重入鎖?

場景1:如果已加鎖,則不再重復加鎖。a、忽略重復加鎖。b、用在界面交互時點擊執(zhí)行較長時間請求操作時,防止多次點擊導致后臺重復執(zhí)行(忽略重復觸發(fā))。以上兩種情況多用于進行非重要任務防止重復執(zhí)行,(如:清除無用臨時文件,檢查某些資源的可用性,數(shù)據(jù)備份操作等)

場景2:如果發(fā)現(xiàn)該操作已經(jīng)在執(zhí)行,則嘗試等待一段時間,等待超時則不執(zhí)行(嘗試等待執(zhí)行)這種其實屬于場景2的改進,等待獲得鎖的操作有一個時間的限制,如果超時則放棄執(zhí)行。用來防止由于資源處理不當長時間占用導致死鎖情況(大家都在等待資源,導致線程隊列溢出)。

場景3:如果發(fā)現(xiàn)該操作已經(jīng)加鎖,則等待一個一個加鎖(同步執(zhí)行,類似synchronized)這種比較常見大家也都在用,主要是防止資源使用沖突,保證同一時間內(nèi)只有一個操作可以使用該資源。但與synchronized的明顯區(qū)別是性能優(yōu)勢(伴隨jvm的優(yōu)化這個差距在減小)。同時Lock有更靈活的鎖定方式,公平鎖與不公平鎖,而synchronized永遠是公平的。這種情況主要用于對資源的爭搶(如:文件操作,同步消息發(fā)送,有狀態(tài)的操作等)

場景4:可中斷鎖。synchronized與Lock在默認情況下是不會響應中斷(interrupt)操作,會繼續(xù)執(zhí)行完。lockInterruptibly()提供了可中斷鎖來解決此問題。(場景3的另一種改進,沒有超時,只能等待中斷或執(zhí)行完畢)這種情況主要用于取消某些操作對資源的占用。如:(取消正在同步運行的操作,來防止不正常操作長時間占用造成的阻塞)

7、簡述鎖的等級方法鎖、對象鎖、類鎖

方法鎖(synchronized修飾方法時)通過在方法聲明中加入 synchronized關鍵字來聲明 synchronized 方法。synchronized 方法控制對類成員變量的訪問: 每個類實例對應一把鎖,每個 synchronized 方法都必須獲得調(diào)用該方法的類實例的鎖方能執(zhí)行,否則所屬線程阻塞,方法一旦執(zhí)行,就獨占該鎖,直到從該方法返回時才將鎖釋放,此后被阻塞的線程方能獲得該鎖,重新進入可執(zhí)行狀態(tài)。這種機制確保了同一時刻對于每一個類實例,其所有聲明為 synchronized 的成員函數(shù)中至多只有一個處于可執(zhí)行狀態(tài),從而有效避免了類成員變量的訪問沖突。

對象鎖(synchronized修飾方法或代碼塊)當一個對象中有synchronized method或synchronized block的時候調(diào)用此對象的同步方法或進入其同步區(qū)域時,就必須先獲得對象鎖。如果此對象的對象鎖已被其他調(diào)用者占用,則需要等待此鎖被釋放。(方法鎖也是對象鎖)。java的所有對象都含有1個互斥鎖,這個鎖由JVM自動獲取和釋放。線程進入synchronized方法的時候獲取該對象的鎖,當然如果已經(jīng)有線程獲取了這個對象的鎖,那么當前線程會等待;synchronized方法正常返回或者拋異常而終止,JVM會自動釋放對象鎖。這里也體現(xiàn)了用synchronized來加鎖的1個好處,方法拋異常的時候,鎖仍然可以由JVM來自動釋放。

類鎖(synchronized修飾靜態(tài)的方法或代碼塊),由于一個class不論被實例化多少次,其中的靜態(tài)方法和靜態(tài)變量在內(nèi)存中都只有一份。所以,一旦一個靜態(tài)的方法被申明為synchronized。此類所有的實例化對象在調(diào)用此方法,共用同一把鎖,我們稱之為類鎖。對象鎖是用來控制實例方法之間的同步,類鎖是用來控制靜態(tài)方法(或靜態(tài)變量互斥體)之間的同步。類鎖只是一個概念上的東西,并不是真實存在的,它只是用來幫助我們理解鎖定實例方法和靜態(tài)方法的區(qū)別的。java類可能會有很多個對象,但是只有1個Class對象,也就是說類的不同實例之間共享該類的Class對象。Class對象其實也僅僅是1個java對象,只不過有點特殊而已。由于每個java對象都有1個互斥鎖,而類的靜態(tài)方法是需要Class對象。所以所謂的類鎖,不過是Class對象的鎖而已。獲取類的Class對象有好幾種,最簡單的就是[類名.class]的方式。

8、Java中活鎖和死鎖有什么區(qū)別?

死鎖:是指兩個或兩個以上的進程(或線程)在執(zhí)行過程中,因爭奪資源而造成的一種互相等待的現(xiàn)象,若無外力作用,它們都將無法推進下去。此時稱系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖,這些永遠在互相等待的進程稱為死鎖進程。死鎖發(fā)生的四個條件

1、互斥條件:線程對資源的訪問是排他性的,如果一個線程對占用了某資源,那么其他線程必須處于等待狀態(tài),直到資源被釋放。

2、請求和保持條件:線程T1至少已經(jīng)保持了一個資源R1占用,但又提出對另一個資源R2請求,而此時,資源R2被其他線程T2占用,于是該線程T1也必須等待,但又對自己保持的資源R1不釋放。

3、不剝奪條件:線程已獲得的資源,在未使用完之前,不能被其他線程剝奪,只能在使用完以后由自己釋放。

4、環(huán)路等待條件:在死鎖發(fā)生時,必然存在一個“進程-資源環(huán)形鏈”,即:{p0,p1,p2,...pn},進程p0(或線程)等待p1占用的資源,p1等待p2占用的資源,pn等待p0占用的資源。(最直觀的理解是,p0等待p1占用的資源,而p1而在等待p0占用的資源,于是兩個進程就相互等待)

活鎖:是指線程1可以使用資源,但它很禮貌,讓其他線程先使用資源,線程2也可以使用資源,但它很紳士,也讓其他線程先使用資源。這樣你讓我,我讓你,最后兩個線程都無法使用資源。

9、如何確保 N 個線程可以訪問 N 個資源同時又不導致死鎖?

預防死鎖,預先破壞產(chǎn)生死鎖的四個條件?;コ獠豢赡芷茐模杂腥缦?種方法:

1.破壞,請求和保持條件1.1)進程等所有要請求的資源都空閑時才能申請資源,這種方法會使資源嚴重浪費(有些資源可能僅在運行初期或結束時才使用,甚至根本不使用)1.2)允許進程獲取初期所需資源后,便開始運行,運行過程中再逐步釋放自己占有的資源。比如有一個進程的任務是把數(shù)據(jù)復制到磁盤中再打印,前期只需要獲得磁盤資源而不需要獲得打印機資源,待復制完畢后再釋放掉磁盤資源。這種方法比上一種好,會使資源利用率上升。

2.破壞,不可搶占條件。這種方法代價大,實現(xiàn)復雜

3.破壞,循壞等待條件。對各進程請求資源的順序做一個規(guī)定,避免相互等待。這種方法對資源的利用率比前兩種都高,但是前期要為設備指定序號,新設備加入會有一個問題,其次對用戶編程也有限制

10、死鎖與饑餓的區(qū)別?

相同點:二者都是由于競爭資源而引起的。

不同點:

從進程狀態(tài)考慮,死鎖進程都處于等待狀態(tài),忙等待(處于運行或就緒狀態(tài))的進程并非處于等待狀態(tài),但卻可能被餓死;
死鎖進程等待永遠不會被釋放的資源,餓死進程等待會被釋放但卻不會分配給自己的資源,表現(xiàn)為等待時限沒有上界(排隊等待或忙式等待);
死鎖一定發(fā)生了循環(huán)等待,而餓死則不然。這也表明通過資源分配圖可以檢測死鎖存在與否,但卻不能檢測是否有進程餓死;
死鎖一定涉及多個進程,而饑餓或被餓死的進程可能只有一個。
在饑餓的情形下,系統(tǒng)中有至少一個進程能正常運行,只是饑餓進程得不到執(zhí)行機會。而死鎖則可能會最終使整個系統(tǒng)陷入死鎖并崩潰

11、怎么檢測一個線程是否擁有鎖?

java.lang.Thread中有一個方法叫holdsLock(),它返回true如果當且僅當當前線程擁有某個具體對象的鎖

Object o = new Object(); 

@Test 
public void test1() throws Exception { 

    new Thread(new Runnable() { 

        @Override 
        public void run() { 
            synchronized(o) { 
                System.out.println("child thread: holdLock: " +  
                    Thread.holdsLock(o)); 
            } 
        } 
    }).start(); 

    System.out.println("main thread: holdLock: " + Thread.holdsLock(o)); 
    Thread.sleep(2000); 

}
main thread: holdLock: false
child thread: holdLock: true
12、如何實現(xiàn)分布式鎖?

基于數(shù)據(jù)庫實現(xiàn)分布式鎖

基于緩存(redis,memcached,tair)實現(xiàn)分布式鎖

基于Zookeeper實現(xiàn)分布式鎖

可以參考詳情《分布式鎖的幾種實現(xiàn)方式》《分布式鎖的3種方式》

13、有哪些無鎖數(shù)據(jù)結構,他們實現(xiàn)的原理是什么?

java 1.5提供了一種無鎖隊列(wait-free/lock-free)ConcurrentLinkedQueue,可支持多個生產(chǎn)者多個消費者線程的環(huán)境:網(wǎng)上別人自己實現(xiàn)的一種無鎖算法隊列,原理和jdk官方的ConcurrentLinkedQueue相似:通過volatile關鍵字來保證數(shù)據(jù)唯一性(注:java的volatile和c++的volatile關鍵字是兩碼事?。?,但是里面又用到atomic,感覺有點boost::lockfree::queue的風格,估計參考了boost的代碼來編寫這個java無鎖隊列。

14、Executors****類是什么? Executor****和Executors****的區(qū)別

正如上面所說,這三者均是 Executor 框架中的一部分。Java 開發(fā)者很有必要學習和理解他們,以便更高效的使用 Java 提供的不同類型的線程池。總結一下這三者間的區(qū)別,以便大家更好的理解:

  • Executor 和 ExecutorService 這兩個接口主要的區(qū)別是:ExecutorService 接口繼承了 Executor 接口,是 Executor 的子接口
  • Executor 和 ExecutorService 第二個區(qū)別是:Executor 接口定義了 execute()方法用來接收一個Runnable接口的對象,而 ExecutorService 接口中的 submit()方法可以接受Runnable和Callable接口的對象。
  • Executor 和 ExecutorService 接口第三個區(qū)別是 Executor 中的 execute() 方法不返回任何結果,而 ExecutorService 中的 submit()方法可以通過一個 Future 對象返回運算結果。
  • Executor 和 ExecutorService 接口第四個區(qū)別是除了允許客戶端提交一個任務,ExecutorService 還提供用來控制線程池的方法。比如:調(diào)用 shutDown() 方法終止線程池??梢酝ㄟ^ 《Java Concurrency in Practice》 一書了解更多關于關閉線程池和如何處理 pending 的任務的知識。
  • Executors 類提供工廠方法用來創(chuàng)建不同類型的線程池。比如: newSingleThreadExecutor() 創(chuàng)建一個只有一個線程的線程池,newFixedThreadPool(int numOfThreads)來創(chuàng)建固定線程數(shù)的線程池,newCachedThreadPool()可以根據(jù)需要創(chuàng)建新的線程,但如果已有線程是空閑的會重用已有線程。
Executor ExecutorService
Executor 是 Java 線程池的核心接口,用來并發(fā)執(zhí)行提交的任務 ExecutorService 是 Executor 接口的擴展,提供了異步執(zhí)行和關閉線程池的方法
提供execute()方法用來提交任務 提供submit()方法用來提交任務
execute()方法無返回值 submit()方法返回Future對象,可用來獲取任務執(zhí)行結果
不能取消任務 可以通過Future.cancel()取消pending中的任務
沒有提供和關閉線程池有關的方法 提供了關閉線程池的方法

16、什么是Java****線程轉儲(Thread Dump)****,如何得到它?

線程轉儲是一個JVM活動線程的列表,它對于分析系統(tǒng)瓶頸和死鎖非常有用。

有很多方法可以獲取線程轉儲——使用Profiler,Kill -3命令,jstack工具等等。我更喜歡jstack工具,因為它容易使用并且是JDK自帶的。由于它是一個基于終端的工具,所以我們可以編寫一些腳本去定時的產(chǎn)生線程轉儲以待分析。

17、如何在Java****中獲取線程堆棧?

Java虛擬機提供了線程轉儲(thread dump)的后門,通過這個后門可以把線程堆棧打印出來。通常我們將堆棧信息重定向到一個文件中,便于我們分析,由于信息量太大,很可能超出控制臺緩沖區(qū)的最大行數(shù)限制造成信息丟失。這里介紹一個jdk自帶的打印線程堆棧的工具,jstack用于打印出給定的Java進程ID或core file或遠程調(diào)試服務的Java堆棧信息。(Java問題定位之Java線程堆棧分析

表示輸出到文件尾部,實際運行中,往往一次dump的信息,還不足以確認問題,建議產(chǎn)生三次dump信息,如果每次dump都指向同一個問題,我們才確定問題的典型性。

18、說出 3 條在 Java 中使用線程的最佳實踐

  • 給你的線程起個有意義的名字。這樣可以方便找bug或追蹤。OrderProcessor, QuoteProcessor or TradeProcessor這種名字比Thread-1. Thread-2 and Thread-3好多了,給線程起一個和它要完成的任務相關的名字,所有的主要框架甚至JDK都遵循這個最佳實踐。
  • 避免鎖定和縮小同步的范圍鎖花費的代價高昂且上下文切換更耗費時間空間,試試最低限度的使用同步和鎖,縮小臨界區(qū)。因此相對于同步方法我更喜歡同步塊,它給我擁有對鎖的絕對控制權。
  • 多用同步類少用wait和notify首先,CountDownLatch, Semaphore, CyclicBarrier和Exchanger這些同步類簡化了編碼操作,而用wait和notify很難實現(xiàn)對復雜控制流的控制。其次,這些類是由最好的企業(yè)編寫和維護在后續(xù)的JDK中它們還會不斷優(yōu)化和完善,使用這些更高等級的同步工具你的程序可以不費吹灰之力獲得優(yōu)化。
  • 多用并發(fā)集合少用同步集合,這是另外一個容易遵循且受益巨大的最佳實踐,并發(fā)集合比同步集合的可擴展性更好,所以在并發(fā)編程時使用并發(fā)集合效果更好。如果下一次你需要用到map,你應該首先想到用ConcurrentHashMap。

volatile

多線程的三大特性
可見性:

可見性是一種復雜的屬性,因為可見性中的錯誤總是會違背我們的直覺。通常,我們無法確保執(zhí)行讀操作的線程能適時地看到其他線程寫入的值,有時甚至是根本不可能的事情。為了確保多個線程之間對內(nèi)存寫入操作的可見性,必須使用同步機制。

可見性,是指線程之間的可見性,一個線程修改的狀態(tài)對另一個線程是可見的。也就是一個線程修改的結果。另一個線程馬上就能看到。比如:用volatile修飾的變量,就會具有可見性。volatile修飾的變量不允許線程內(nèi)部緩存和重排序,即直接修改內(nèi)存。所以對其他線程是可見的。但是這里需要注意一個問題,volatile只能讓被他修飾內(nèi)容具有可見性,但不能保證它具有原子性。比如 volatile int a = 0;之后有一個操作 a++;這個變量a具有可見性,但是a++ 依然是一個非原子操作,也就是這個操作同樣存在線程安全問題。在 Java 中 volatile、synchronized 和 final 實現(xiàn)可見性。

原子性:

原子是世界上的最小單位,具有不可分割性。比如 a=0;(a非long和double類型) 這個操作是不可分割的,那么我們說這個操作時原子操作。再比如:a++; 這個操作實際是a = a + 1;是可分割的,所以他不是一個原子操作。非原子操作都會存在線程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那么我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。在 Java 中 synchronized 和在 lock、unlock 中操作保證原子性。

有序性:

Java 語言提供了 volatile 和 synchronized 兩個關鍵字來保證線程之間操作的有序性,volatile 是因為其本身包含“禁止指令重排序”的語義,synchronized 是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規(guī)則獲得的,此規(guī)則決定了持有同一個對象鎖的兩個同步塊只能串行執(zhí)行。

Volatile原理

Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程。當把變量聲明為volatile類型后,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內(nèi)存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。

在訪問volatile變量時不會執(zhí)行加鎖操作,因此也就不會使執(zhí)行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。

image

當對非 volatile 變量進行讀寫的時候,每個線程先從內(nèi)存拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味著每個線程可以拷貝到不同的 CPU cache 中。

而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內(nèi)存中讀,跳過 CPU cache 這一步。

當一個變量定義為 volatile 之后,將具備兩種特性:

1.保證此變量對所有的線程的可見性,這里的“可見性”,如本文開頭所述,當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。但普通變量做不到這點,普通變量的值在線程間傳遞均需要通過主內(nèi)存(詳見:Java內(nèi)存模型)來完成。

2.禁止指令重排序優(yōu)化。有volatile修飾的變量,賦值后多執(zhí)行了一個“l(fā)oad addl $0x0, (%esp)”操作,這個操作相當于一個內(nèi)存屏障(指令重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置),只有一個CPU訪問內(nèi)存時,并不需要內(nèi)存屏障;(什么是指令重排序:是指CPU采用了允許將多條指令不按程序規(guī)定的順序分開發(fā)送給各相應電路單元處理)。

volatile 性能:

volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因為它需要在本地代碼中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行。

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

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

  • Java多線程編程的入門篇,主要介紹volatile修飾詞、Synchronized以及Lock及其子類 多線程編...
    Lebens閱讀 363評論 2 1
  • 原文作者:拉夫德爾原文地址:原文鏈接摘抄申明:我們不占有不侵權,我們只是好文的搬運工!轉發(fā)請帶上原文申明。 樂觀鎖...
    Hey_Shaw閱讀 680評論 0 1
  • 鎖的出現(xiàn)就是為了避免在多個線程并發(fā)訪問同一個資源時出現(xiàn)異常情況。如果對多線程還不了解,可以看一看《Java 多線程...
    小道蕭兮閱讀 2,899評論 0 7
  • 在開發(fā)Java多線程應用程序中,各個線程之間由于要共享資源,必須用到鎖機制。Java提供了多種多線程鎖機制的實現(xiàn)方...
    轉身一世鉛華盡閱讀 226評論 0 2
  • 前言 回顧前面: 多線程三分鐘就可以入個門了! Thread源碼剖析 多線程基礎必要知識點!看了學習多線程事半功倍...
    Java3y閱讀 1,006評論 0 22

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