上次通過三個(gè)例子,了解了Java并發(fā)三個(gè)特性,也分析了volatile不能解決原子性問題的原因,要解決原子性問題,就需要用到鎖
一、輕量級(jí)鎖與重量級(jí)鎖
1.鎖的概念
鎖:一個(gè)線程對(duì)共享對(duì)象進(jìn)行加鎖,別的線程訪問該對(duì)象時(shí)會(huì)處于等待狀態(tài),直到鎖被釋放,才能繼續(xù)執(zhí)行
補(bǔ)充:volatile底層也是通過lock原子性操作,但它只對(duì)寫入共享變量值時(shí)進(jìn)行了加鎖,別的線程可能已經(jīng)使用舊值副本在進(jìn)行計(jì)算了、或者已經(jīng)在寫入了等情況,導(dǎo)致不同步問題
2.輕量級(jí)鎖(自旋鎖)
由于我們希望cpu盡可能多的時(shí)間使用在執(zhí)行代碼上,而內(nèi)核線程切換會(huì)消耗較多時(shí)間,所以出現(xiàn)了對(duì)于短時(shí)間的等待操作進(jìn)行優(yōu)化
自旋鎖,在等待時(shí),不會(huì)切換到內(nèi)核態(tài)去切換線程,還是在當(dāng)前線程繼續(xù)執(zhí)行,只不過執(zhí)行的是一個(gè)循環(huán),所以稱之為自旋,這樣做爭(zhēng)對(duì)短時(shí)間的等待,性能會(huì)更高,造成的時(shí)間浪費(fèi)也短。
缺點(diǎn):對(duì)于長(zhǎng)時(shí)間的等待,它一直占用著cpu資源,別的線程得不到執(zhí)行
3.重量級(jí)鎖
重量級(jí)鎖就是切換到內(nèi)核態(tài),由OS線程調(diào)度切換到其他線程執(zhí)行,當(dāng)前線程進(jìn)入等待隊(duì)列,后面重新競(jìng)爭(zhēng)獲取鎖
二、悲觀鎖與樂觀鎖
悲觀鎖與樂觀鎖是兩種概念,是對(duì)線程同步的兩種不同實(shí)現(xiàn)方式
1. 悲觀鎖
線程對(duì)一個(gè)共享變量進(jìn)行訪問,它就自動(dòng)加鎖,所以只能有一個(gè)線程訪問它
悲觀鎖適合寫操作多的場(chǎng)景,先加鎖可以保證寫操作時(shí)數(shù)據(jù)正確。
缺點(diǎn):只有一個(gè)線程對(duì)它操作時(shí),沒有必要加鎖,造成了性能浪費(fèi)
2.樂觀鎖
線程訪問共享變量時(shí)不加鎖,當(dāng)執(zhí)行完后,同步值到內(nèi)存時(shí),使用舊值和內(nèi)存中的值進(jìn)行判斷,如果相同,那么寫入,如果不相同,重新使用新值執(zhí)行
樂觀鎖適合讀操作多的場(chǎng)景,不加鎖的特點(diǎn)能夠使其讀操作的性能大幅提升。
缺點(diǎn):
值相同的情況,可能被其他線程執(zhí)行過
操作變量頻繁時(shí),重新執(zhí)行次數(shù)多,造成性能浪費(fèi)
完成比較后,寫入前,被其他線程修改了值,導(dǎo)致不同步問題
三、Java中鎖的實(shí)現(xiàn)
1.ReentrantLock
ReentrantLock是悲觀鎖,調(diào)用ReentrantLock的lock方法后,后續(xù)的代碼能夠一個(gè)線程執(zhí)行,直到調(diào)用unlock方法
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
//操作共享變量
lock.unlock();
}
2.synchronized
使用synchronized關(guān)鍵字,修飾方法或者代碼塊,實(shí)現(xiàn)線程同步
public synchronized void test() {
}
public void test2() {
synchronized (this) {
}
}
synchronized是悲觀鎖,JDK1.2之前,使用的是重量級(jí)鎖,后續(xù)synchronized進(jìn)行了優(yōu)化:
1.最初沒有鎖,當(dāng)?shù)谝粋€(gè)線程訪問時(shí),升級(jí)為偏向鎖
偏向鎖:如果在運(yùn)行過程中,同步鎖只有一個(gè)線程訪問,不存在多線程爭(zhēng)用的情況,則線程是不需要觸發(fā)同步的,這種情況下,就會(huì)給線程加一個(gè)偏向鎖。線程第二次到達(dá)同步代碼塊時(shí),會(huì)判斷此時(shí)持有鎖的線程是否就是自己,如果是則正常往下執(zhí)行。由于之前沒有釋放鎖,這里也就不需要重新加鎖。如果自始至終使用鎖的線程只有一個(gè),很明顯偏向鎖幾乎沒有額外開銷,性能極高。
2.當(dāng)別的線程訪問時(shí),升級(jí)為輕量級(jí)鎖,升級(jí)為輕量級(jí)鎖的時(shí)候需要撤銷偏向鎖,撤銷偏向鎖的時(shí)候會(huì)導(dǎo)致STW(stop the word)操作
3.自旋達(dá)到次數(shù)時(shí),升級(jí)為重量級(jí)鎖,切換內(nèi)核態(tài)
在對(duì)象頭中存放了鎖狀態(tài)

3.CAS
JDK1.5后,新增java.util.concurrent包,上面我們知道樂觀鎖是有問題的,CAS是系統(tǒng)CPU提供的一種解決原子性問題的方案,解決了樂觀鎖的不同步問題
Java中AtomicXXX就是采用的CAS,它通過以下方法解決了樂觀鎖的問題
1.對(duì)對(duì)象增加版本號(hào),每次操作時(shí)+1,而不是使用值進(jìn)行是否重新執(zhí)行的判斷
2.自旋鎖升級(jí)為重量級(jí)鎖,防止一直自旋浪費(fèi)cpu
3.調(diào)用compareAndSwapInt,通過jni調(diào)用原子性操作,保證多個(gè)線程都能夠看到同一個(gè)變量的修改值
并發(fā)量不高以及耗時(shí)操作短時(shí),使用CAS效率比悲觀鎖效率高
static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) {
for(int i = 0;i<20;i++){
new Thread(){
@Override
public void run() {
for (int j =0;j<1000;j++){
System.out.println(atomicInteger.incrementAndGet());
}
}
}.start();
}
}
4.AQS
AQS是Java提供的同步器框架,ReentrantLock、CountDownLatch等就是使用了它,當(dāng)多個(gè)線程對(duì)同一對(duì)象進(jìn)行操作時(shí),內(nèi)部維護(hù)一個(gè)state和等待隊(duì)列,來判斷是否有鎖,如果沒有獲取到鎖,那么進(jìn)入等待隊(duì)列,等待其他線程操作完后進(jìn)行喚醒
public class VolatileTest4 {
static CountDownLatch count = new CountDownLatch(20);
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread() {
@Override
public void run() {
long millis = (long) (new Random().nextFloat() * 3000);
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
count.countDown();
System.out.println(count.getCount() + "ms:" + millis);
super.run();
}
}.start();
}
try {
count.await();
System.out.println("所有子線程執(zhí)行完畢");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}