1茉贡、Java中的兩種鎖
1、synchronized
synchronized 是一種對(duì)象鎖准潭,它以對(duì)象為鎖趁俊,在之前的筆記中有過詳細(xì)解釋。如果想要獲得唯一的對(duì)象作為鎖刑然,有兩種方法寺擂。一是在類中定義一個(gè)靜態(tài)的對(duì)象,二是直接用類名.class。
2怔软、Lock
Lock是一個(gè)接口垦细,一般用它的實(shí)現(xiàn)類ReentrantLock。以下是Lock的使用示例:
Lock lock = new ReentrantLock();
lock.lock();
//中間是需要同步的代碼
同步代碼
//這個(gè)類用來控制線程的等待和喚醒挡逼,由對(duì)應(yīng)Lock創(chuàng)建
Condition cd = lock.newCondition();
//這個(gè)方法類似synchronized中的wait 可以釋放對(duì)應(yīng)線程的鎖
cd.await();
//類似synchronized中的notify 喚醒指定線程
cd.signal();
//同理 Lock也有喚醒所有線程的方法
cd.signalAll();
lock.unlock();
Lock鎖的作用是lock()和unlock()之間括改,為了對(duì)不同線程狀態(tài)進(jìn)行管理,利用newCondition()生成Condition類家坎,一個(gè)Lock對(duì)象可以實(shí)例化多個(gè)Condition對(duì)象嘱能。
2、多線程之間的協(xié)調(diào)
在之前講到Lock類時(shí)虱疏,說到了一個(gè)Condition對(duì)象惹骂,實(shí)例化多個(gè)這種對(duì)象,可以用來對(duì)多線程進(jìn)行協(xié)調(diào)做瞪,不過在此之前先看一下synchronized是如何對(duì)兩個(gè)線程進(jìn)行協(xié)調(diào)的对粪。
1、synchronized實(shí)現(xiàn)兩個(gè)線程的交替執(zhí)行
public class MyMain {
public static void main(String[] args) {
Test test = new Test();
Thread th0 = new Thread(() -> {
test.test();
});
Thread th1 = new Thread(() -> {
test.test();
});
th0.start();
th1.start();
}
}
class Test {
private static int x = 1000;
private Object obj = new Object();
public void test() {
synchronized (obj) {
while (true) {
try {
//鎖對(duì)象喚醒所有等待的線程
obj.notifyAll();
//線程到此等待并釋放鎖
obj.wait();
System.out.println(Thread.currentThread().getName() + "--" + x);
Thread.sleep(500);
x--;
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
從同步代碼塊可以看出装蓬,拿到鎖的線程先喚醒所有等待的線程著拭,然后接下來自己進(jìn)入等待狀態(tài)并釋放鎖,等到另一個(gè)線程拿到鎖喚醒它牍帚,形成線程的循環(huán)交替執(zhí)行茫死。在這個(gè)同步代碼塊中,第一次喚醒是沒有意義的履羞,因?yàn)殒i被第一次拿到的時(shí)候峦萎,沒有其它線程在等待狀態(tài),同理當(dāng)一個(gè)線程被關(guān)閉忆首,另一個(gè)線程因?yàn)闆]有被喚醒爱榔,造成死鎖,不過這種情況下一般需要執(zhí)行的業(yè)務(wù)已經(jīng)結(jié)束了糙及,所以對(duì)整個(gè)業(yè)務(wù)沒有影響详幽。
2、Lock實(shí)現(xiàn)三個(gè)線程順序交替執(zhí)行
package com.fan.test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestMain {
public static void main(String[] args) {
LockBean lockBean = new LockBean();
ThrA thrA = new ThrA(lockBean);
ThrB thrB = new ThrB(lockBean);
ThrC thrC = new ThrC(lockBean);
thrA.start();
thrB.start();
thrC.start();
}
}
//將需要傳入的參數(shù)合并為一個(gè)類浸锨,簡化傳參
class LockBean {
public Lock lock = new ReentrantLock();
public String flag = "A";
public Condition cd_a = lock.newCondition();
public Condition cd_b = lock.newCondition();
public Condition cd_c = lock.newCondition();
}
同時(shí)開啟三個(gè)線程唇聘,分別交替輸出A、B和C柱搜,這里以輸出A的線程為例:
package com.fan.test;
public class ThrA extends Thread {
private LockBean lockBean;
public ThrA(LockBean lockBean) {
this.lockBean = lockBean;
}
@Override
public void run() {
while (true) {
try {
//開始加鎖
lockBean.lock.lock();
//參數(shù)對(duì)象中有一個(gè)flag字符串參數(shù)迟郎,用來判斷此時(shí)是否由該線程輸出
if (!"A".equals(lockBean.flag)) {
//如果flag不是該線程輸出語句,就讓這個(gè)線程等待并釋放鎖
lockBean.cd_a.await();
}
System.out.println("A");
lockBean.flag = "B";
lockBean.cd_b.signal();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lockBean.lock.unlock();
}
}
}
當(dāng)flag不為A時(shí)聪蘸,利用Condition對(duì)象使線程等待宪肖;當(dāng)flag為A時(shí)表制,打印輸出A并將flag變?yōu)橄乱粋€(gè)要輸出的字符串(這里是B),然后將利用下一個(gè)線程的Condition對(duì)象將其喚醒控乾。所以么介, 每次輸出時(shí),只有對(duì)應(yīng)的線程在工作蜕衡,并且輸出完畢會(huì)更改flag壤短,使得對(duì)應(yīng)線程被喚醒,而無關(guān)線程等待慨仿。
3久脯、鎖的類型
1、公平/非公平鎖
每個(gè)線程在獲取鎖的時(shí)候镶骗,都是隨機(jī)獲取的,這種鎖叫非公平鎖躲雅。如果按照線程等待的前后順序獲得鎖鼎姊,那么就是公平鎖。在
Java中相赁,synchronized一定是非公平鎖相寇,而Lock可以設(shè)置成公平鎖或非公平鎖。
2钮科、自旋鎖
當(dāng)鎖被一個(gè)線程占用的唤衫,其他線程持續(xù)不斷的嘗試獲得鎖,這種就叫自旋鎖绵脯;如果是非自旋鎖佳励,那么其它線程會(huì)進(jìn)入阻塞狀態(tài),
等鎖被釋放的時(shí)候蛆挫,被喚醒赃承,進(jìn)入就緒啟動(dòng)狀態(tài),再去獲得鎖悴侵。線程在切換狀態(tài)的過程中瞧剖,會(huì)降低線程的執(zhí)行速度;而自旋鎖的
線程會(huì)一直占用內(nèi)存以持續(xù)嘗試獲得鎖可免。
所以抓于,當(dāng)一個(gè)鎖可能會(huì)被長時(shí)間持有,那就使用非自旋鎖浇借,降低系統(tǒng)性能的消耗捉撮;如果鎖被持有的時(shí)間不會(huì)太長的話,建議使用自旋鎖妇垢,以防止線程狀態(tài)切換而降低線程執(zhí)行效率呕缭。
3堵泽、悲觀鎖和樂觀鎖
悲觀鎖就是指,持有鎖的線程在執(zhí)行過程中恢总,悲觀地認(rèn)為一定會(huì)有其他線程干預(yù)迎罗,所以在使用共享資源前一定會(huì)先加鎖,一直等
到這個(gè)線程釋放鎖片仿,比如Java中的synchronized和ReentrantLock纹安。
而樂觀鎖則是指持有鎖的線程在執(zhí)行過程中,樂觀地認(rèn)為不會(huì)有其它線程干預(yù)砂豌,所以不會(huì)對(duì)數(shù)據(jù)上鎖厢岂,只有在更新數(shù)據(jù)的時(shí)候才
會(huì)確認(rèn)數(shù)據(jù)是不是被其它線程改變了,例如Java中的原子變量類阳距。
樂觀鎖適用于讀比較多塔粒,寫比較少的情形,這樣可以減少鎖的開銷筐摘,而如果是相反的情況卒茬,寫操作會(huì)使的鎖不停地檢查,而且一旦反生數(shù)據(jù)不一致咖熟,那么就會(huì)重新執(zhí)行沖突數(shù)據(jù)的相關(guān)操作圃酵,所以在寫比較多的時(shí)候一般使用悲觀鎖。一般高級(jí)語言無法直接實(shí)現(xiàn)樂觀鎖馍管,這需要底層對(duì)CAS的支持實(shí)現(xiàn)郭赐。在數(shù)據(jù)庫中,可以原生支持樂觀鎖确沸,具體是在數(shù)據(jù)中增加一個(gè)版本號(hào)的屬性捌锭。在查詢數(shù)據(jù)的時(shí)候,會(huì)一并讀出這個(gè)數(shù)據(jù)的版本號(hào)罗捎,當(dāng)發(fā)生非查詢操作時(shí)舀锨,都會(huì)檢查操作數(shù)據(jù)的版本號(hào)與當(dāng)前數(shù)據(jù)數(shù)據(jù)庫的版本是否一致,如果相同則版本號(hào)加一宛逗,并提交更新數(shù)據(jù)坎匿,如果不相同,則認(rèn)為是過期數(shù)據(jù)雷激,不予提交本次更新操作并重試替蔬。