CAS
什麼是原子(Atom)操作:
多線程中的原子操作類似於數據庫中的同時執行AB兩個語句,要麼同時執行成功,要麼同時執行失敗。
synchronize的不足:
- syn是基於阻塞的鎖機制,顆粒度還是比較大 的。
- 如果被阻塞的線程優先級很高怎麼辦。
- 拿到鎖的線程一直不釋放鎖怎麼辦。
- 如果出現大量競爭會消耗CPU,同時帶來死鎖或其他安全隱患。
用syn也可以實現原子操作不過不太合適,目前CPU指令級別實現了將比較和交換(Conmpare And Swap)進行原則性的操作(CAS不是鎖只是CPU提供的一個原子性操作指令哦切記),它的實現步驟如下
- 獲得L(內存地址)上的數據初始值D1
- 對D1的數據進行增減後最終等到D2
- 嘗試將D2 放到原來L的位置上。
- 放之前先比較目前L裏的數據是否跟我之前取出的D1值跟版本號都對應。
- 對應了 我就將數據放到L中,單但有一個不對應則寫入失敗。重新執行步驟1.
- 上面的步驟如果失敗了就會重複進入一個1~5的死循環,俗稱自旋。
CAS
在語言層面不進行任何處理,直接將原則操作實現在硬件級別實現,只所以可以實現硬件級別的操作核心是因爲CAS操作類中有個核心類UnSafe
類,
Java
和C++
語言的一個重要區別就是Java中我們無法直接操作一塊內存區域,不能像C++中那樣可以自己申請內存和釋放內存。Java
中的Unsafe
類爲我們提供了類似C++手動管理內存的能力。Unsafe
類,全限定名是sun.misc.Unsafe
,UnSafe
類中所有的方法都是native
修飾的,也就是說UnSafe
類中的方法都是直接調用操作底層資源執行響應的任務。主要功能如下:
用CAS的弊端:
- ABA 問題
現象:在內存中數據變化爲A==>B==>A,這樣如何判別,因爲這樣其實數據已經修改過了。
加粗樣式解決方法:引入版本號
- 開銷問題
如果長期不成功那就會進入自旋。
JVM支持處理器提供的pause指令,使得效率會有一定的提升,pause指令有兩個作用:
- 它可以延遲流水線執行指令,使CPU不會消耗過多的執行資源,
- 它可以避免在退出循環的時候因內存順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。
- 只能保證一個共享變量之間的原則性操作
問題描述:當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性。
解決辦法:從JDK5開始提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行CAS操作。
JDK中相關原子操作類的使用
- 更新基本類型類:AtomicBoolean,AtomicInteger,AtomicLong
- 更新數組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
- 更新引用類型:AtomicStampedReference,AtomicMarkableReference,AtomicReference
- 原子更新字段類: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
相互之間差別不太,我們以AtomicInteger爲例,常用方法:
- get()
- set(int)
- getAndIncrement()
- 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提供了兩個類
- AtomicStampedReference: 返回Boolean值,關心的是動沒動過。
- AtomicMarkableReference:關心的是動過幾次。
我們以AtomicStampedReference
爲例分析:
重點函數如下:
AtomicStampedReference(V initialRef, int initialStamp)
,V表示要CAS的數據,int表示初始化版本。public V getReference()
表示獲得CAS裏面的數據public int getStamp()
表示獲得當前CAS版本號- 第一個參數是原來的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特性:
- java中的一個關鍵字,也就是說說Java語言裏的內置鎖。
- 是獨佔鎖,加鎖和解鎖的過程自動進行,易於操作,但不夠靈活。
- 不可響應中斷,一個線程獲取不到鎖就一直等着。
- 可重入,因爲加鎖和解鎖自動進行,不必擔心最後是否釋放鎖。
因爲syn有諸多不便,尤其是不可timeout
,因此在JDK5以後引入了Lock
這個interface
,相比於syn它具有如下特性:
- Lock是Java代碼級別的鎖。
- 獨佔鎖,加鎖和解鎖的過程需要手動進行,不易操作,但非常靈活
- 可重入,但加鎖和解鎖需要手動進行,且次數需一樣,否則其他線程無法獲得鎖。
- 關鍵是可中斷==,可以實現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還有一個重點是可以實現公平鎖跟非公平鎖:
如果在時間上,先對鎖進行獲取對請求,一定被先滿足則鎖公平的。如果不滿足就是非公平的。
- 公平鎖
比如ABC三個任務去搶同一個鎖,A先獲得 BC就要被依此掛起
掛起的含義是指:本來線程是佔用cpu資源的,但是如果掛起的話,操作系統就不給這個現成分配cpu資源,除非以後再恢復,所以線程掛起的作用就是節省cpu資源,跟wait()還有sleep()是不一樣的。
BC被掛起就相當於AE86被喊停了,等A再用完鎖,BC才獲得鎖再起步這個過程對於CPU來說是很耗時的。
- 非公平鎖
比如ABC三個任務搶同一個鎖,A獲得鎖在運行但時間長,而B提交後由於非公平機制會直接進行搶鎖再執行,如果嘗試失敗,就再採用類似公平鎖那種方式。所以非公平鎖相對來說性能會更好些。
ReentrantLock 底層默認實現爲非公平鎖:
public ReentrantLock() {
sync = new NonfairSync();// 默認非公平鎖
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
syn跟Lock 使用建議:
- 能用syn就用syn,代碼更簡潔。
- 需要鎖可中斷,超時獲取鎖,嘗試獲取鎖時候 用Lock。
Condition
synchronized
可用wait()
和notify()/notifyAll()
方法相結合可以實現等待/通知模式。ReentrantLock
也提供了Condition
來提供類似的功能。
public Condition newCondition() {
return sync.newCondition();
}
其中 Condition
主要函數如下
基本跟syn
的操作差別不大,唯一區別可能就是多來個a
在方法前面。
讀寫鎖
互斥鎖:
在訪問共享資源之前會對資源進行加鎖操作,在訪問完成之後進行解鎖操作。 加鎖後,任何其他試圖再次加鎖的線程會被阻塞,直到當前進程解鎖。通俗來說就是共享資源某一時刻只能有一個線程訪問,其餘等待。
共享鎖:
共享鎖從字面來看也即是允許多個線程共同訪問資源。
讀寫鎖:
讀寫鎖既是互斥鎖,又是共享鎖,read模式是共享,write是互斥(排它鎖)的。
一次只有一個線程可以佔有寫模式的讀寫鎖,但是多個線程可以同時佔有讀模式的讀寫鎖。
前面說到的syn
跟Lock
都是獨佔鎖,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;
//單獨的寫鎖
降級跟升級
要實現讀寫鎖需要考慮一個問題就是鎖升級和鎖降級的問題,ReadWriteLock
的javadoc
中說明如下:
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是否支持?
- 鎖降級
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
支持鎖降級,上面代碼不會產生死鎖。這段代碼雖然不會導致死鎖,但沒有正確的釋放鎖。從寫鎖降級成讀鎖,並不會自動釋放當前線程獲取的寫鎖,仍然需要顯示的釋放,否則別的線程永遠也獲取不到寫鎖。
- 鎖升級
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性能
讀寫鎖性能
互斥還是共享
通過Syn
跟ReetrantReadWriteLock
的測試我們可以瞭解到,讀寫鎖中的讀鎖使用共享模式,也就是說可以同時有多個線程併發地讀數據。
讀鎖跟寫鎖之間是互斥模式。
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();
}
}
}
結論:
- JUC中
ReetrantReadWriteLock
實現了ReadWriteLock接口並添加了可重入的特性ReetrantReadWriteLock
讀寫鎖的效率明顯高於synchronized關鍵字,引入如果存在多度少寫情況儘量用讀寫鎖。ReetrantReadWriteLock
讀寫鎖的實現中,讀鎖使用共享模式;寫鎖使用獨佔模式,讀鎖跟寫鎖之間鎖互斥模式。ReetrantReadWriteLock
讀寫鎖的實現中,需要注意的,當有讀鎖時,寫鎖就不能獲得;而當有寫鎖時,除了獲得寫鎖的這個線程可以獲得讀鎖外,其他線程不能獲得讀鎖。