併發編程專題四-原子操做和顯示鎖

  PS:好累啊,好晚纔到家,今天把學的併發編程的最後一點工具和概念總結下,明天正式進入aqs的源碼學習~java

1、原子操做CAS

一、什麼是原子操做atomic operation?

所謂原子操做是指不會被線程調度機制打斷的操做;這種操做一旦開始,就一直運行到結束,中間不會有任何 context switch (線程切換)。數據庫

二、java是如何實現原子操做?

一、使用synchronized對操做加鎖

存在問題:express

一、被阻塞的線程優先級很高編程

二、拿到鎖的線程一直不釋放鎖怎麼辦?數組

三、大量的競爭,消耗cpu,同時帶來死鎖或者其餘安全。安全

二、循環CAS(compare and swap)實現原子操做

Java中的CAS操做正是利用了處理器提供的CMPXCHG指令實現的。自旋CAS實現的基本思路就是循環進行CAS操做直到操做成功爲止。併發

2.一、CAS的原理dom

CAS(Compare And Swap),指令級別保證這是一個原子操做ide

三個運算符:  一個內存地址V,一個指望的值A,一個新值B工具

基本思路:若是地址V上的值和指望的值A相等,就給地址V賦給新值B,若是不是,不作任何操做。循環(死循環,自旋)裏不斷的進行CAS操做

2.二、CAS的問題

一、ABA問題

就是一個縣城可能將A改爲了B,而後又有個線程將B又改爲了A。但此時的A已經不是咱們本來的A了。

就好比喝水,我到了一杯水,而後去上了個廁所,而後同事把我水喝了而後又給我接滿了,等我回來時雖然桌子上仍是一杯水,但此時已經不是個人那杯了。因此爲了解決這個問題,咱們能夠對咱們使用的地址經過加個版本號的概念,來標識咱們的變量是否發生變化。

可以使用AtomicStampedReference和AtomicMarkableReference記錄版本號

二、開銷問題

自旋仍是很消耗性能的

三、只能保證一個共享變量的原子操做

2.三、Jdk中相關原子操做類的使用

更新基本類型類:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
更新數組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新引用類型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段類: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater (較少使用)
代碼示例:
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @Auther: BlackKingW
 * @Date: 2019/4/15 22:09
 * @Description:帶版本號的原子操做
 */
public class UseAtomicStampedReference {
	
	static AtomicStampedReference<String> asr = 
			new AtomicStampedReference<>("BlackKingW",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.start();
    	rightStampThread.join();
    	errorStampThread.start();
    	errorStampThread.join();
    	System.out.println(asr.getReference()+"==========="+asr.getStamp());
    	
    }
}

2、顯式鎖

一、Lock接口和核心方法

lock()   用來獲取鎖。若是鎖已被其餘線程獲取,則進行等待。

unlock() 釋放鎖

tryLock() 它表示用來嘗試獲取鎖,若是獲取成功,則返回true,若是獲取失敗(即鎖已被其餘線程獲取

Lock接口和synchronized的比較

synchronized:是Java語言內置關鍵字,不須要手動釋放鎖。代碼簡潔,

Lock:是實現的一個類,須要手動釋放鎖。而且獲取鎖能夠被中斷,擁有超時獲取鎖,嘗試獲取鎖等機制。

代碼示例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Auther: BlackKingW
 * @Date: 2019/4/14 12:09
 * @Description:
 */
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();
	}
	

}

如increament採用lock,代碼相對複雜,而且使用lock必定要在finally 中釋放鎖,不然可能會永遠都釋放不了,致使死鎖。

二、可重入鎖ReentrantLock

可重入意思爲:已經得到該鎖的線程,能夠再次進入被鎖定的代碼塊。內部經過計數器實現。例如上面的代碼

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Auther: BlackKingW
 * @Date: 2019/4/15 22:09
 * @Description:
 */
public class ReentrantLockDemo {
	
	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();
	}

}

在增長一個方法test3,去調用incr2,若是該鎖不能夠被重入,則沒法調用incr2。致使程序一直運行不下去。可重入鎖就是支持已經獲取鎖的線程,能夠重複進入加鎖的代碼塊。

三、公平鎖和非公平鎖。

公平鎖非公平鎖何謂公平性,是針對獲取鎖而言的,若是一個鎖是公平的,那麼鎖的獲取順序就應該符合請求上的絕對時間順序,知足FIFO

當多個線程去請求加鎖代碼塊時,同時只能有一個線程擁有鎖,那麼其餘線程若是是按照到來的前後順序,那麼這個鎖就是公平鎖。好比ReentrantLock可指定是否爲公平和非公平鎖。不然就是非公平鎖。好比synchronized。

公平鎖 VS 非公平鎖

公平鎖每次獲取到鎖爲同步隊列中的第一個節點,保證請求資源時間上的絕對順序,而非公平鎖有可能剛釋放鎖的線程下次繼續獲取該鎖,則有可能致使其餘線程永遠沒法獲取到鎖,形成「飢餓」現象。

公平鎖爲了保證時間上的絕對順序,須要頻繁的上下文切換,而非公平鎖會下降必定的上下文切換,下降性能開銷。所以,ReentrantLock默認選擇的是非公平鎖,則是爲了減小一部分上下文切換,保證了系統更大的吞吐量。

四、ReadWriteLock接口和讀寫鎖ReentrantReadWriteLock

那是否是全部的鎖都只能被一個線程所擁有呢?固然不是。例如ReentrantReadWriteLock讀寫鎖。

ReentrantReadWriteLock容許多個讀線程同時訪問,但不容許寫線程和讀線程、寫線程和寫線程同時訪問。

ReadWriteLock接口有兩個方法

 Lock readLock();  獲取讀鎖

 Lock writeLock();  獲取寫鎖

ReentrantReadWriteLock實現了ReadWriteLock接口。用於獲取讀寫鎖。

ReentrantLock和synchronized關鍵字,同時只能有一個線程持有,因此都是排他鎖,而ReentrantReadWriteLock能夠同時有多個線程去訪問,這種所也叫共享鎖

使用場景: 讀多寫少的狀況

代碼示例

/**
 * @Auther: BlackKingW
 * @Date: 2019/4/15 22:09
 * @Description:
 */
public class UseSyn implements GoodsService {
	
	private GoodsInfo goodsInfo;
	
	public UseSyn(GoodsInfo goodsInfo) {
		this.goodsInfo = goodsInfo;
	}

	@Override
	public synchronized GoodsInfo getNum() {
		SleepTools.ms(5);
		return this.goodsInfo;
	}

	@Override
	public synchronized void setNum(int number) {
		SleepTools.ms(5);
		goodsInfo.changeNumber(number);

	}

}


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Auther: BlackKingW
 * @Date: 2019/4/15 22:09
 * @Description:
 */
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();
		}
	}
}

/**
 * @Auther: BlackKingW
 * @Date: 2019/4/14 12:09
 * @Description:
 */
public interface GoodsService {

	public GoodsInfo getNum();//得到商品的信息
	public void setNum(int number);//設置商品的數量
}



/**
 * @Auther: BlackKingW
 * @Date: 2019/4/15 22:09
 * @Description:
 */
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 double getTotalMoney() {
        return totalMoney;
    }

    public int getStoreNumber() {
        return storeNumber;
    }

    public void changeNumber(int sellNumber){
        this.totalMoney += sellNumber*25;
        this.storeNumber -= sellNumber;
    }
}


import java.util.Random;
import java.util.concurrent.CountDownLatch;


/**
 * @Auther: BlackKingW
 * @Date:2019/4/15 22:09
 * @Description:
 */
public class BusiApp {
    static final int readWriteRatio = 10;//讀寫線程的比例
    static final int minthreadCount = 3;//最少線程數
    //static CountDownLatch latch= new CountDownLatch(1);

    //讀操做
    private static class GetThread implements Runnable{

        private GoodsService goodsService;
        public GetThread(GoodsService goodsService) {
            this.goodsService = goodsService;
        }

        @Override
        public void run() {
//            try {
//                latch.await();//讓讀寫線程同時運行
//            } catch (InterruptedException e) {
//            }
            long start = System.currentTimeMillis();
            for(int i=0;i<100;i++){//操做100次
                goodsService.getNum();
            }
            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() {
//            try {
//                latch.await();//讓讀寫線程同時運行
//            } catch (InterruptedException e) {
//            }
            long start = System.currentTimeMillis();
            Random r = new Random();
            for(int i=0;i<10;i++){//操做10次
            	SleepTools.ms(50);
                goodsService.setNum(r.nextInt(10));
            }
            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);/*new UseSyn(goodsInfo);*/
        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();
        }
        //latch.countDown();

    }
}

經過修改busiApp,使用讀寫鎖,

GoodsService goodsService = new UseRwLock(goodsInfo);

執行完畢時間爲

將busiApp修改成,使用synchronized關鍵字

GoodsService goodsService = new UseSyn(goodsInfo);

執行完畢時間爲

ReentrantReadWriteLock和ReentrantLock支持如下功能:

    1)支持公平和非公平的獲取鎖的方式;

    2)支持可重入。讀線程在獲取了讀鎖後還能夠獲取讀鎖;寫線程在獲取了寫鎖以後既能夠再次獲取寫鎖又能夠獲取讀鎖;

    3)還容許從寫入鎖降級爲讀取鎖,其實現方式是:先獲取寫入鎖,而後獲取讀取鎖,最後釋放寫入鎖。可是,從讀取鎖升級到寫入鎖是不容許的;

    4)讀取鎖和寫入鎖都支持鎖獲取期間的中斷;

    5)Condition支持。僅寫入鎖提供了一個 Conditon 實現;讀取鎖不支持 Conditon ,readLock().newCondition() 會拋出 UnsupportedOperationException。 

五、Condition接口

在個人併發編程專題三-線程的併發工具類這篇文章裏,講了wait和notify/notifyAll。而Condition接口的功能和wait和notify功能和相似。

Condition主要方法爲

await() 當前線程進入等待狀態

signal() 喚醒一個等待在Condition上的線程

signalAll() 喚醒全部等待在Condition上的線程

await、signal、signalAll和wait、notify、notifyAll的等待通知機制的區別

await、signal、signalAll:創建在lock之上的,使用以前須要綁定lock鎖。準確的通知須要喚醒的對象。喚醒時建議使用signal()方法

wait、notify、notifyAll:創建在Object之上的,使用以前須要獲取對象鎖,不能準確地通知須要喚醒的對象,喚醒時建議使用notifyAll()。

代碼舉例

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Auther: BlackKingW
 * @Date: 2019/4/14 12:09
 * @Description:
 */
public class ExpressCond {
    public final static String CITY = "ShangHai";
    private int km;/*快遞運輸里程數*/
    private String site;/*快遞到達地點*/
    private Lock lock = new ReentrantLock();
    private Condition keCond = lock.newCondition();
    private Condition siteCond = lock.newCondition();

    public ExpressCond() {
    }

    public ExpressCond(int km, String site) {
        this.km = km;
        this.site = site;
    }

    /* 變化千米數,而後通知處於wait狀態並須要處理千米數的線程進行業務處理*/
    public void changeKm(){
        lock.lock();
        try {
        	this.km = 101;
        	keCond.signal();
        }finally {
        	lock.unlock();
        }
    }

    /* 變化地點,而後通知處於wait狀態並須要處理地點的線程進行業務處理*/
    public  void changeSite(){
    	lock.lock();
        try {
        	this.site = "BeiJing";
        	siteCond.signal();
        }finally {
        	lock.unlock();
        }    	
    }

    /*當快遞的里程數大於100時更新數據庫*/
    public void waitKm(){
    	lock.lock();
    	try {
        	while(this.km<=100) {
        		try {
        			keCond.await();
    				System.out.println("check km thread["+Thread.currentThread().getId()
    						+"] is be notifed.");
    			} catch (InterruptedException e) {
    				// TODO Auto-generated catch block
    				e.printStackTrace();
    			}
        	}    		
    	}finally {
    		lock.unlock();
    	}

        System.out.println("the Km is "+this.km+",I will change db");
    }

    /*當快遞到達目的地時通知用戶*/
    public void waitSite(){
    	lock.lock();
        try {
        	while(CITY.equals(this.site)) {
        		try {
        			siteCond.await();
    				System.out.println("check site thread["+Thread.currentThread().getId()
    						+"] is be notifed.");
    			} catch (InterruptedException e) {
    				// TODO Auto-generated catch block
    				e.printStackTrace();
    			}
        	}
        }finally {
        	lock.unlock();
        } 
        System.out.println("the site is "+this.site+",I will call user");
    }
}


/**
 * @Auther: BlackKingW
 * @Date: 2019/4/14 12:09
 * @Description:
 */
public class TestCond {
    private static ExpressCond express = new ExpressCond(0,ExpressCond.CITY);

    /*檢查里程數變化的線程,不知足條件,線程一直等待*/
    private static class CheckKm extends Thread{
        @Override
        public void run() {
        	express.waitKm();
        }
    }

    /*檢查地點變化的線程,不知足條件,線程一直等待*/
    private static class CheckSite extends Thread{
        @Override
        public void run() {
        	express.waitSite();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<3;i++){
            new CheckSite().start();
        }
        for(int i=0;i<3;i++){
            new CheckKm().start();
        }

        Thread.sleep(1000);
        express.changeKm();//快遞里程變化
    }
}

將上篇文章的例子,進行修改,使用Condition進行通知。能夠發現,當里程數發生變化時,會準確的通知到里程數變化,進行相應的業務處理。而不像執行notify的時候,可能會喚醒等待地點變化的業務。從而致使業務員異常。

 

本章主要了解幾種顯示鎖。以及重入鎖,排它鎖,共享鎖等鎖的概念。本文的代碼裏leepTools.ms(5);均可使用Thread.Sleep代替。歡迎你們多多指點。

併發編程專題一-線程相關基礎概念

併發編程專題二-線程間的共享和協做

併發編程專題三-線程的併發工具類

相關文章
相關標籤/搜索