Java深刻學習(5):鎖

可重入鎖:java

簡單來講,支持重複加鎖,有可重用性算法

特徵:鎖能夠傳遞,方法遞歸傳遞安全

目的:避免了死鎖現象多線程

代碼:併發

public class Test implements Runnable {

    @Override
    public void run() {
        method1();
    }

    public synchronized void method1() {
        System.out.println("method1");
        method2();
    }

    public synchronized void method2() {
        System.out.println("method2");
    }

    public static void main(String[] args) {
        new Thread(new Test()).start();
    }

}

打印:分佈式

method1
method2

分析:若是鎖不能重用,那麼這裏將會出現死鎖問題ide

 

使用ReentrantLock鎖:高併發

public class TestLock implements Runnable {
    //重入鎖
    private Lock reentrantLock = new ReentrantLock();

    @Override
    public void run() {
        method1();
    }

    public void method1() {
        try {
            reentrantLock.lock();
            System.out.println("method1");
            method2();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }

    public void method2() {
        try {
            reentrantLock.lock();
            System.out.println("method2");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(new TestLock()).start();
    }

}

 

讀寫鎖:atom

高併發的時候,寫操做的同時應當不容許讀操做spa

(多線程中:讀-讀共存;讀-寫、寫-寫都不能夠共存)

代碼:製造讀寫操做的線程安全問題

public class TestWriteLock {

    Map<String, String> cache = new HashMap<>();

    //寫入元素
    public void put(String key, String value) {
        try {
            System.out.println("開始寫入key : " + key + " value : " + value);
            Thread.sleep(50);
            cache.put(key, value);
            System.out.println("完成寫入key : " + key + " value : " + value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //讀取元素
    public String get(String key) {
        System.out.println("開始讀取key : " + key);
        String value = cache.get(key);
        System.out.println("讀取成功key : " + key + " value : " + value);
        return value;
    }

    public static void main(String[] args) {
        TestWriteLock test = new TestWriteLock();
        Thread readThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    test.put("i", i + "");
                }
            }
        });

        Thread writeThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    test.get("i");
                }
            }
        });

        readThread.start();
        writeThread.start();
    }
}

觀察打印:發現不合理

開始寫入key : i value : 0
開始讀取key : i
讀取成功key : i value : null
.................................

分析:在沒有寫入完成的時候,就開始了讀取,獲得的結果爲空

 

解決:

1.使用synchronized,雖然能夠解決,可是效率低下,寫操做同時不能讀,產生阻塞

2.使用讀寫鎖

public class TestWriteLock {

    Map<String, String> cache = new HashMap<>();

    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

    //寫入元素
    public void put(String key, String value) {
        try {
            writeLock.lock();
            System.out.println("開始寫入key : " + key + " value : " + value);
            Thread.sleep(50);
            cache.put(key, value);
            System.out.println("完成寫入key : " + key + " value : " + value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }

    //讀取元素
    public String get(String key) {
        String value = "";
        try {
            readLock.lock();
            System.out.println("開始讀取key : " + key);
            value = cache.get(key);
            System.out.println("讀取成功key : " + key + " value : " + value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
        }
        return value;
    }

    public static void main(String[] args) {
        TestWriteLock test = new TestWriteLock();
        Thread readThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    test.put("i", i + "");
                }
            }
        });

        Thread writeThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    test.get("i");
                }
            }
        });
        readThread.start();
        writeThread.start();
    }
}

觀察打印:完美解決

 

樂觀鎖:

簡單來說,樂觀鎖就是沒有鎖,無阻塞無等待

一條SQL語句作示範:

UPDATE TABLE SET X=X+1,VERSION=VERSION+1 WHERE ID=#{id} AND VERSION=#{version}

 在高併發地狀況下,假設初始version是1,請求1到來,根據id和version能查到,因此容許更新

 請求2同時作操做,可是根據id和version已經查不到了(被請求1修改了),因此不容許更新

 

悲觀鎖:

簡單來說,重量級鎖, 會阻塞,會進行等待

能夠理解爲上鎖以後只容許一個線程來操做,也就是Java中的synchronized

 

原子類:

一段模擬線程安全問題的代碼:

public class ThreadTest implements Runnable {

    private static int count = 1;

    @Override
    public void run() {
        while (true) {
            Integer count = getCount();
            if (count >= 100) {
                break;
            }
            System.out.println(count);
        }
    }

    public synchronized Integer getCount() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return count++;
    }

    public static void main(String[] args) {
        ThreadTest t = new ThreadTest();
        new Thread(t).start();
        new Thread(t).start();
    }

}

觀察打印後發現果真出現了線程安全問題

 

一種修改方式:效率較低

    public synchronized Integer getCount() {

 

使用原子類:樂觀鎖,底層沒有加鎖,使用CAS無鎖技術

public class ThreadTest implements Runnable {

    // 線程安全
    private AtomicInteger atomicInteger = new AtomicInteger();

    @Override
    public void run() {
        while (true) {
            Integer count = getCount();
            if (count >= 100) {
                break;
            }
            System.out.println(count);
        }
    }

    public Integer getCount() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return atomicInteger.incrementAndGet();
    }

    public static void main(String[] args) {
        ThreadTest t = new ThreadTest(); 
        new Thread(t).start();
        new Thread(t).start();
    }

}

 

CAS無鎖技術(Compare And Swap)

翻譯過來爲:比較再交換

本地內存中存放共享內存的副本

好比主內存中有i=0,複製到兩個線程的本地內存中

兩個線程執行了i++,本地內存都變成i=1,而後刷新入主內存

 

CAS算法:

它包含三個參數CAS(V,E,N):

V表示要更新的變量(主內存)

E表示預期值(本地內存)

N表示新值(新值)

僅當V值等於E值時(主內存=本地內存),纔會將V的值設爲N

若是V值和E值不一樣(主內存!=本地內存),則說明已經有其餘線程作了更新,則當前線程什麼都不作

最後,CAS返回當前V的真實值。

 

觀察原子類的源碼:

    /** 
     * Atomically increments by one the current value. 
     * 
     * @return the updated value 
     */  
    public final int incrementAndGet() {  
        for (;;) {  
            //獲取當前值  
            int current = get();  
            //設置指望值  
            int next = current + 1;  
            //調用Native方法compareAndSet,執行CAS操做  
            if (compareAndSet(current, next))  
                //成功後纔會返回指望值,不然無線循環  
                return next;  
        }  
    }  

 

CAS無鎖機制的缺點:

1.死循環

2.ABA問題:若是變量V初次讀取的時候是A,而且在準備賦值的時候檢查到它仍然是A,那能說明它的值沒有被其餘線程修改過了嗎

(若是在這段期間曾經被改爲B,而後又改回A,那CAS操做就會誤認爲它歷來沒有被修改過。針對這種狀況,java併發包中提供了一個帶有標記的原子引用類AtomicStampedReference,它能夠經過控制變量值的版原本保證CAS的正確性。)

 

JVM數據同步:採用分佈式鎖

 

自旋鎖和互斥鎖的區別:

悲觀和樂觀鎖的區別,自旋鎖是死循環不會阻塞,互斥鎖是同一時間只有一個線程訪問數據

相關文章
相關標籤/搜索