【Java併發編程】3.CAS、Lock、讀寫鎖

在這裏插入圖片描述

CAS

什麼是原子(Atom)操作:

多線程中的原子操作類似於數據庫中的同時執行AB兩個語句,要麼同時執行成功,要麼同時執行失敗。

synchronize的不足:

  1. syn是基於阻塞的鎖機制,顆粒度還是比較大 的。
  2. 如果被阻塞的線程優先級很高怎麼辦。
  3. 拿到鎖的線程一直不釋放鎖怎麼辦。
  4. 如果出現大量競爭會消耗CPU,同時帶來死鎖或其他安全隱患。

用syn也可以實現原子操作不過不太合適,目前CPU指令級別實現了將比較和交換(Conmpare And Swap)進行原則性的操作(CAS不是鎖只是CPU提供的一個原子性操作指令哦切記),它的實現步驟如下

  1. 獲得L(內存地址)上的數據初始值D1
  2. 對D1的數據進行增減後最終等到D2
  3. 嘗試將D2 放到原來L的位置上。
  4. 放之前先比較目前L裏的數據是否跟我之前取出的D1值跟版本號都對應。
  5. 對應了 我就將數據放到L中,單但有一個不對應則寫入失敗。重新執行步驟1.
  6. 上面的步驟如果失敗了就會重複進入一個1~5的死循環,俗稱自旋

CAS在語言層面不進行任何處理,直接將原則操作實現在硬件級別實現,只所以可以實現硬件級別的操作核心是因爲CAS操作類中有個核心類UnSafe類,

JavaC++語言的一個重要區別就是Java中我們無法直接操作一塊內存區域,不能像C++中那樣可以自己申請內存和釋放內存。Java中的Unsafe類爲我們提供了類似C++手動管理內存的能力。Unsafe類,全限定名是sun.misc.UnsafeUnSafe類中所有的方法都是native修飾的,也就是說UnSafe類中的方法都是直接調用操作底層資源執行響應的任務。主要功能如下:
在這裏插入圖片描述
用CAS的弊端:

  1. ABA 問題

現象:在內存中數據變化爲A==>B==>A,這樣如何判別,因爲這樣其實數據已經修改過了。
加粗樣式解決方法:引入版本號

  1. 開銷問題

如果長期不成功那就會進入自旋。

JVM支持處理器提供的pause指令,使得效率會有一定的提升,pause指令有兩個作用:

  1. 它可以延遲流水線執行指令,使CPU不會消耗過多的執行資源,
  2. 它可以避免在退出循環的時候因內存順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。
  1. 只能保證一個共享變量之間的原則性操作

問題描述:當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性。
解決辦法:從JDK5開始提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行CAS操作。

JDK中相關原子操作類的使用

  1. 更新基本類型類:AtomicBoolean,AtomicInteger,AtomicLong
  2. 更新數組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  3. 更新引用類型:AtomicStampedReference,AtomicMarkableReference,AtomicReference
  4. 原子更新字段類: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater

在這裏插入圖片描述
相互之間差別不太,我們以AtomicInteger爲例,常用方法:

  1. get()
  2. set(int)
  3. getAndIncrement()
  4. incrementAndGet()

AtomicInteger 例子:

    static AtomicInteger ai = new AtomicInteger(10);
    public static void main(String[] args) {
        System.out.println(ai.getAndIncrement());
        //10--->11
        System.out.println(ai.incrementAndGet());
        //11--->12--->out
        System.out.println(ai.get());
    }

LongAdder

本來的初衷是通過CAS操作來進行原子性的簡單累加計數功能,但是在併發很大的情況下,因爲每次CAS都只有一個線程能成功,競爭失敗的線程會非常多。失敗次數越多,循環次數就越多,很多線程的CAS操作越來越接近 自旋鎖(spin lock)。計數操作本來是一個很簡單的操作,實際需要耗費的cpu時間應該是越少越好,AtomicXXX在高併發計數時,大量的cpu時間都浪費會在 自旋 上了,這很浪費,也降低了實際的計數效率。

// jdk1.8的AtomicLong的實現代碼,這段代碼在sun.misc.Unsafe中
// 當線程競爭很激烈時,while判斷條件中的CAS會連續多次返回false,這樣就會造成無用的循環,循環中讀取volatile變量的開銷本來就是比較高的
// 因爲這樣,在高併發時,AtomicXXX並不是那麼理想的計數方式
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, v + delta));// 自旋
return v;
}

LongAdder 是根據 ConcurrentHashMap這類爲併發設計的類的基本原理(鎖分段),來實現的,它裏面維護一組按需分配的計數單元,併發計數時,不同的線程可以在不同的計數單元上進行計數,這樣減少了線程競爭,提高了併發效率。本質上是用空間換時間的思想,不過在實際高併發情況中消耗的空間可以忽略不計。
在這裏插入圖片描述

用引用類型AtomicReference包裝user對象,然後修改包裝後的對象,user本身參數是不變的這點要切記。

public class UseAtomicReference {
	static AtomicReference<UserInfo> userRef = new AtomicReference<UserInfo>();
    public static void main(String[] args) {
        UserInfo user = new UserInfo("sowhat", 14);//要修改的實體的實例
        userRef.set(user); // 引用包裝後,包裝裏面的類跟包裝前是兩個不同的對象。
        
        UserInfo updateUser = new UserInfo("liu", 12);//要變化的新實例
        userRef.compareAndSet(user, updateUser);
        System.out.println(userRef.get().getName());
        System.out.println(userRef.get().getAge());

        System.out.println(user.getName()); // 注意此時的user 屬性
        System.out.println(user.getAge());        
    }
    //定義一個實體類
    static class UserInfo {
        private String name;
        private int age;
        public UserInfo(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String getName() {
            return name;
        }
        public int getAge() {
            return age;
        }
    }
}

ABA問題:JDK提供了兩個類

  1. AtomicStampedReference: 返回Boolean值,關心的是動沒動過。
  2. AtomicMarkableReference:關心的是動過幾次。

我們以AtomicStampedReference爲例分析:
重點函數如下:

  1. AtomicStampedReference(V initialRef, int initialStamp),V表示要CAS的數據,int表示初始化版本。
  2. public V getReference() 表示獲得CAS裏面的數據
  3. public int getStamp() 表示獲得當前CAS版本號
  4. 第一個參數是原來的CAS中原來參數,第二個參數是要替換後的新參數,第三個參數是原來CAS數據對於版本號,第四個參數表示替換後的新參數版本號。
public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp)

具體demo加深理解如下:

public class UseAtomicStampedReference {

    static AtomicStampedReference<String> asr = new AtomicStampedReference<>("sowhat", 0);


    public static void main(String[] args) throws InterruptedException {
        final int oldStamp = asr.getStamp();  // 那初始的版本號
        final String oldReferenc = asr.getReference(); // 初始數據

        System.out.println(oldReferenc + "----------" + oldStamp);

        Thread rightStampThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()
                        + "當前變量值:" + oldReferenc + "當前版本戳:" + oldStamp + "-"
                        + asr.compareAndSet(oldReferenc, oldReferenc + "Java",
                        oldStamp, oldStamp + 1));
            }
        });

        Thread errorStampThread = new Thread(new Runnable() {
            @Override
            public void run() {
                String reference = asr.getReference();
                System.out.println(Thread.currentThread().getName()
                        + "當前變量值:" + reference + "當前版本戳:" + asr.getStamp() + "-"
                        + asr.compareAndSet(reference, reference + "C",
                        oldStamp, oldStamp + 1)); //此處版本號用錯了
            }
        });

        rightStampThread.setName("對的線程");
        rightStampThread.start();
        rightStampThread.join();

        errorStampThread.setName("錯的線程");
        errorStampThread.start();
        errorStampThread.join();
        System.out.println(asr.getReference() + "------------" + asr.getStamp());

    }
}

顯示鎖Lock

synchronized特性:

  1. java中的一個關鍵字,也就是說說Java語言裏的內置鎖
  2. 是獨佔鎖,加鎖和解鎖的過程自動進行,易於操作,但不夠靈活。
  3. 不可響應中斷,一個線程獲取不到鎖就一直等着。
  4. 可重入,因爲加鎖和解鎖自動進行,不必擔心最後是否釋放鎖。

因爲syn有諸多不便,尤其是不可timeout,因此在JDK5以後引入了Lock這個interface,相比於syn它具有如下特性:

  1. Lock是Java代碼級別的鎖。
  2. 獨佔鎖,加鎖和解鎖的過程需要手動進行,不易操作,但非常靈活
  3. 可重入,但加鎖和解鎖需要手動進行,且次數需一樣,否則其他線程無法獲得鎖。
  4. 關鍵是可中斷==,可以實現timeout跟interrupted

重點ReentrantLock底層實現依賴於特殊的CPU指令,比如發送lock指令和unlock指令,不需要用戶態和內核態的切換,所以效率高(這裏和volatile底層原理類似),而synchronized底層由監視器鎖(monitor)是依賴於底層的操作系統的Mutex Lock需要用戶態和內核態的切換,所以效率低。

PS 可重入含義:

同一線程外層函數獲得鎖後,內層遞歸函數仍能獲取該鎖的代碼。在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。也就是說,線程可以進入任何一個它已經擁有的鎖所同步的代碼塊。

在這裏插入圖片描述

日常經常用Lock的實現類ReentrantLock

public class LockDemo {
	private Lock lock  = new ReentrantLock();
	private int count;
	public void increament() {
		lock.lock();
		try {
			count++;
		}finally {
			lock.unlock();
		}
	}
	public synchronized void incr2() {
		count++;
		incr2();
	}
	// 可重入鎖  底層類似 累加器 鎖的調用
	public synchronized void test3() {
		incr2();
	}
}

Lock還有一個重點是可以實現公平鎖跟非公平鎖:

如果在時間上,先對鎖進行獲取對請求,一定被先滿足則鎖公平的。如果不滿足就是非公平的。

  1. 公平鎖
    比如ABC三個任務去搶同一個鎖,A先獲得 BC就要被依此掛起

掛起的含義是指:本來線程是佔用cpu資源的,但是如果掛起的話,操作系統就不給這個現成分配cpu資源,除非以後再恢復,所以線程掛起的作用就是節省cpu資源,跟wait()還有sleep()是不一樣的。

BC被掛起就相當於AE86被喊停了,等A再用完鎖,BC才獲得鎖再起步這個過程對於CPU來說是很耗時的。

  1. 非公平鎖
    比如ABC三個任務搶同一個鎖,A獲得鎖在運行但時間長,而B提交後由於非公平機制會直接進行搶鎖再執行,如果嘗試失敗,就再採用類似公平鎖那種方式。所以非公平鎖相對來說性能會更好些。
    ReentrantLock 底層默認實現爲非公平鎖:
   public ReentrantLock() {
        sync = new NonfairSync();// 默認非公平鎖
    }
 public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

syn跟Lock 使用建議:

  1. 能用syn就用syn,代碼更簡潔。
  2. 需要鎖可中斷,超時獲取鎖,嘗試獲取鎖時候 用Lock。

Condition

synchronized可用wait()notify()/notifyAll()方法相結合可以實現等待/通知模式。ReentrantLock也提供了Condition來提供類似的功能。

    public Condition newCondition() {
        return sync.newCondition();
    }

其中 Condition主要函數如下
在這裏插入圖片描述
基本跟syn的操作差別不大,唯一區別可能就是多來個a在方法前面。

讀寫鎖

互斥鎖:

在訪問共享資源之前會對資源進行加鎖操作,在訪問完成之後進行解鎖操作。 加鎖後,任何其他試圖再次加鎖的線程會被阻塞,直到當前進程解鎖。通俗來說就是共享資源某一時刻只能有一個線程訪問,其餘等待。

共享鎖:

共享鎖從字面來看也即是允許多個線程共同訪問資源。

讀寫鎖:

讀寫鎖既是互斥鎖,又是共享鎖,read模式是共享,write是互斥(排它鎖)的。
一次只有一個線程可以佔有寫模式的讀寫鎖,但是多個線程可以同時佔有讀模式的讀寫鎖。

前面說到的synLock都是獨佔鎖,JDK 還專門給我們提供了更細緻的讀寫鎖,對於讀操作因爲不改變值可以多個線程同時進行讀數據,但是對於出現寫操作的時候則將該對象進行Lock,JDK中讀寫鎖的接口是ReadWriteLock,該接口其實底層實現就是有兩個鎖,一個管讀操作,一個管寫操作,對於多度少寫的場景一般比syn性能可提速10倍。

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

具體實現類是ReentrantReadWriteLock

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    private static final long serialVersionUID = -6992448646407690164L;
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;// 單獨的 讀鎖
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    //單獨的寫鎖

降級跟升級

要實現讀寫鎖需要考慮一個問題就是鎖升級和鎖降級的問題,ReadWriteLockjavadoc中說明如下:

Can the write lock be downgraded to a read lock without allowing an intervening writer? Can a read lock be upgraded to a write lock, in preference to other waiting readers or writers?

簡言之就是說,鎖降級:從寫鎖變成讀鎖;鎖升級:從讀鎖變成寫鎖,ReadWriteLock是否支持?

  1. 鎖降級
public class Test {
    public static void main(String[] args) {
        ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
        rtLock.writeLock().lock();
        System.out.println("writeLock");

        rtLock.readLock().lock();
        System.out.println("get read lock");
    }
}

在這裏插入圖片描述
結論ReentrantReadWriteLock支持鎖降級,上面代碼不會產生死鎖。這段代碼雖然不會導致死鎖,但沒有正確的釋放鎖。從寫鎖降級成讀鎖,並不會自動釋放當前線程獲取的寫鎖,仍然需要顯示的釋放,否則別的線程永遠也獲取不到寫鎖。

  1. 鎖升級
public class Test {

    public static void main(String[] args) {
        ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
        rtLock.readLock().lock();
        System.out.println("get readLock.");
        rtLock.writeLock().lock();
        System.out.println("blocking");
    }
}

在這裏插入圖片描述
結論:結果直接卡死,因爲同一個線程中,在沒有釋放讀鎖的情況下,就去申請寫鎖,這屬於鎖升級,ReentrantReadWriteLock是不支持的。

性能測試
測試讀寫鎖跟syn性能,比如我們是買娃娃的,有總銷售額跟庫存數,沒減少庫存則銷售額會增加,我們用多線程來執行。

GoodsInfo 商品信息類

public class GoodsInfo {
    private final String name;
    private double totalMoney;//總銷售額
    private int storeNumber;//庫存數
    public GoodsInfo(String name, int totalMoney, int storeNumber) {
        this.name = name;
        this.totalMoney = totalMoney;
        this.storeNumber = storeNumber;
    }
    public void changeNumber(int sellNumber){
        this.totalMoney += sellNumber*25;
        this.storeNumber -= sellNumber;
    }
}

操作類接口

public interface GoodsService {
    GoodsInfo getNum() throws Exception;//獲得商品的信息
    void setNum(int number) throws Exception;//設置商品的數量
}

操作類實現Syn

public class UseSyn implements GoodsService {
	private GoodsInfo goodsInfo;
	public UseSyn(GoodsInfo goodsInfo) {
		this.goodsInfo = goodsInfo;
	}
	@Override
	public synchronized GoodsInfo getNum() throws Exception {
		TimeUnit.MILLISECONDS.sleep(5);
		return this.goodsInfo;
	}
	@Override
	public synchronized void setNum(int number) throws Exception {
		TimeUnit.MILLISECONDS.sleep(5);
		goodsInfo.changeNumber(number);
	}
}

操作類實現讀寫鎖

public class UseRwLock implements GoodsService {
    private GoodsInfo goodsInfo;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock getLock = lock.readLock(); //讀鎖
    private final Lock setLock = lock.writeLock(); //寫鎖

    public UseRwLock(GoodsInfo goodsInfo) {
        this.goodsInfo = goodsInfo;
    }

    @Override
    public GoodsInfo getNum() {
        getLock.lock();// 加讀鎖
        try {
            SleepTools.ms(5);
            return this.goodsInfo;
        } finally {
            getLock.unlock();
        }
    }
    @Override
    public void setNum(int number) {
        setLock.lock(); //加寫鎖
        try {
            SleepTools.ms(5);
            goodsInfo.changeNumber(number);
        } finally {
            setLock.unlock();
        }
    }
}

真正的測試性能,多度少些併發。

public class BusiApp {
    static final int readWriteRatio = 10;//讀寫線程的比例
    static final int minthreadCount = 3;//最少線程數

    //讀操作
    private static class GetThread implements Runnable {
        private GoodsService goodsService;
        public GetThread(GoodsService goodsService) {
            this.goodsService = goodsService;
        }
        @Override
        public void run() {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100; i++) {//操作100次
                try {
                    goodsService.getNum();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "讀取商品數據耗時:"
                    + (System.currentTimeMillis() - start) + "ms");
        }
    }

    //寫操做
    private static class SetThread implements Runnable {
        private GoodsService goodsService;
        public SetThread(GoodsService goodsService) {
            this.goodsService = goodsService;
        }
        @Override
        public void run() {
            long start = System.currentTimeMillis();
            Random r = new Random();
            for (int i = 0; i < 10; i++) {//操作10次
                SleepTools.ms(50);
                try {
                    goodsService.setNum(r.nextInt(10));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "寫商品數據耗時:" + (System.currentTimeMillis() - start) + "ms---------");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        GoodsInfo goodsInfo = new GoodsInfo("Cup", 100000, 10000);
        //GoodsService goodsService = new UseRwLock(goodsInfo); //單次耗時770ms  用讀寫鎖實現
        GoodsService goodsService =new UseSyn(goodsInfo); //單次耗時 17000ms  用syn實現
        for (int i = 0; i < minthreadCount; i++) {
            Thread setT = new Thread(new SetThread(goodsService));
            for (int j = 0; j < readWriteRatio; j++) {
                Thread getT = new Thread(new GetThread(goodsService));
                getT.start();
            }
            SleepTools.ms(100);
            setT.start();
        }
    }
}

Syn性能
在這裏插入圖片描述
讀寫鎖性能
在這裏插入圖片描述

互斥還是共享

通過SynReetrantReadWriteLock的測試我們可以瞭解到,讀寫鎖中的讀鎖使用共享模式,也就是說可以同時有多個線程併發地讀數據。
讀鎖跟寫鎖之間是互斥模式

public class Test {

    public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        //同時讀、寫
        ExecutorService service = Executors.newCachedThreadPool();

        service.execute(new Runnable() {
            @Override
            public void run() {
                readFile(Thread.currentThread());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                writeFile(Thread.currentThread());
            }
        });
    }

    // 讀操作
    public static void readFile(Thread thread) {
        lock.readLock().lock();
        boolean readLock = lock.isWriteLocked();
        if (!readLock) {
            System.out.println("當前爲讀鎖!");
        }
        try {
            for (int i = 0; i < 3; i++) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在進行讀操作……");
            }
            System.out.println(thread.getName() + ":讀操作完畢!");
        } finally {
            System.out.println("釋放讀鎖!");
            lock.readLock().unlock();
        }
    }

    // 寫操作
    public static void writeFile(Thread thread) {
        lock.writeLock().lock();
        boolean writeLock = lock.isWriteLocked();
        if (writeLock) {
            System.out.println("當前爲寫鎖!");
        }
        try {
            for (int i = 0; i < 3; i++) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在進行寫操作……");
            }
            System.out.println(thread.getName() + ":寫操作完畢!");
        } finally {
            System.out.println("釋放寫鎖!");
            lock.writeLock().unlock();
        }
    }
}

在這裏插入圖片描述
寫鎖跟寫鎖之間是互斥模式,跟Syn還有ReentrantLock一樣。

public class Test {

    public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        //同時寫
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(new Runnable() {
            @Override
            public void run() {
                writeFile(Thread.currentThread());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                writeFile(Thread.currentThread());
            }
        });
    }
    
    // 寫操作
    public static void writeFile(Thread thread) {
        lock.writeLock().lock();
        boolean writeLock = lock.isWriteLocked();
        if (writeLock) {
            System.out.println("當前爲寫鎖!");
        }
        try {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在進行寫操作……");
            }
            System.out.println(thread.getName() + ":寫操作完畢!");
        } finally {
            System.out.println("釋放寫鎖!");
            lock.writeLock().unlock();
        }
    }
}

在這裏插入圖片描述

結論:

  1. JUC中ReetrantReadWriteLock實現了ReadWriteLock接口並添加了可重入的特性
  2. ReetrantReadWriteLock讀寫鎖的效率明顯高於synchronized關鍵字,引入如果存在多度少寫情況儘量用讀寫鎖。
  3. ReetrantReadWriteLock讀寫鎖的實現中,讀鎖使用共享模式;寫鎖使用獨佔模式,讀鎖跟寫鎖之間鎖互斥模式。
  4. ReetrantReadWriteLock讀寫鎖的實現中,需要注意的,當有讀鎖時,寫鎖就不能獲得;而當有寫鎖時,除了獲得寫鎖的這個線程可以獲得讀鎖外,其他線程不能獲得讀鎖。

參考

UnSafe
CAS

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