1.Lock接口
1.1 簡介、地位、作用
- 鎖是一種工具,用于控制對共享資源的訪問。
- Lock和synchronized,這兩個是最常見的鎖,它們都可以達到線程安全的目的,但是在使用上和功能上又有較大的不同。
- Lock并不是用來代替synchronized的,而是當使用synchronized不合適或不足以滿足要求的時候,來提供高級功能的。
- Lock接口最常見的實現(xiàn)類時ReentrantLock
- 通常情況下,Lock只允許一個線程來訪問這個共享資源。不過有的時候,一些特殊的實現(xiàn)也可以允許并發(fā)訪問,比如ReadWriteLock里面的ReadLock。
1.2 為什么synchronized不夠用?
1.2.1 效率低:鎖的釋放情況少、試圖獲得鎖時不能設定超時、不能中斷一個正在試圖獲得鎖的線程。
1.2.2 不夠靈活(讀寫鎖更靈活):加鎖和釋放的時機單一,每個鎖僅有單一的條件(某個對象),可能是不夠的。
1.2.3 無法知道是否成功獲取到鎖。
1.3 Lock主要方法介紹
在Lock中聲明了四個方法來獲取鎖
lock()
就是最普通的獲取鎖。如果鎖已被其他線程獲取,則進行等待
Lock不會像synchronized一樣在異常時自動釋放鎖
因此最佳實踐是,在finally中釋放鎖,以保證發(fā)生異常時鎖一定被釋放
lock()方法不能被中斷,這樣會帶來很大的隱患:一旦陷入死鎖,lock()就會陷入永久等待
tryLock()
- tryLock()用來嘗試獲取鎖,如果當前鎖沒有被其他線程占用。則獲取成功,則返回true,否則返回false,代表獲取鎖失敗
- 相比于lock,這樣的方式顯然功能更強大了,我們可以根據(jù)是否能獲取到鎖決定后續(xù)程序的行為
tryLock(long time,TimeUnit unit)
超時就放棄
- 該方法會立即返回,即便在拿不到鎖時不會一直在那等
public class TryLockDeadLock implements Runnable {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
int flag = 1;
public static void main(String[] args) {
TryLockDeadLock tryLockDeadLock1 = new TryLockDeadLock();
TryLockDeadLock tryLockDeadLock2 = new TryLockDeadLock();
tryLockDeadLock1.flag = 1;
tryLockDeadLock2.flag = 2;
new Thread(tryLockDeadLock1).start();
new Thread(tryLockDeadLock2).start();
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (flag == 1) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("線程1獲取到了鎖1");
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("線程1獲取到了鎖2");
System.out.println("線程1成功獲取到了2把鎖");
break;
} finally {
lock2.unlock();
}
} else {
System.out.println("線程1獲取鎖2失敗,已重試");
}
} finally {
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("線程1獲取鎖1失敗,已重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag == 2) {
try {
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("線程2獲取到了鎖2");
Thread.sleep(new Random().nextInt(1000));
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("線程2獲取到了鎖1");
System.out.println("線程2成功獲取到了2把鎖");
break;
} finally {
lock1.unlock();
}
} else {
System.out.println("線程2獲取鎖2失敗,已重試");
}
} finally {
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("線程2獲取鎖2失敗,已重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
線程1獲取到了鎖1
線程2獲取到了鎖2
線程1獲取鎖2失敗,已重試
線程2獲取到了鎖1
線程2成功獲取到了2把鎖
線程1獲取到了鎖1
線程1獲取到了鎖2
線程1成功獲取到了2把鎖
lockInterruptibly()
相當于tryLock(long time,TimeUnit unit)把超時時間設置為無限。在等待鎖的過程中,線程可以被中斷
public class LockInterruptibly implements Runnable {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
LockInterruptibly lockInterruptibly = new LockInterruptibly();
Thread thread0 = new Thread(lockInterruptibly);
Thread thread1 = new Thread(lockInterruptibly);
thread0.start();
thread1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread1.interrupt();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "嘗試獲取鎖");
try {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + "獲取到了鎖");
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "睡眠期間被中斷了");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "釋放了鎖");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "獲得鎖期間被中斷了");
}
}
}
輸出結果1
Thread-1嘗試獲取鎖
Thread-0嘗試獲取鎖
Thread-1獲取到了鎖
Thread-1睡眠期間被中斷了
Thread-1釋放了鎖
Thread-0獲取到了鎖
Thread-0釋放了鎖
輸出結果2
Thread-0嘗試獲取鎖
Thread-1嘗試獲取鎖
Thread-0獲取到了鎖
Thread-1獲得鎖期間被中斷了
Thread-0釋放了鎖
unlock 解鎖
1.4 可見性保證
可見性
happens-before
因為jvm會對代碼進行編譯優(yōu)化,指令會出現(xiàn)重排序的情況,為了避免編譯優(yōu)化對并發(fā)編程安全性的影響,需要happens-before規(guī)則定義一些禁止編譯優(yōu)化的場景,保證并發(fā)編程的正確性。
public class VolatileExample {
int x = 0 ;
volatile boolean v = false;
public void writer(){
x = 42;
v = true;
}
public void reader(){
if (v == true){
// 這里x會是多少呢
}
}
}
拋出問題:假設有兩個線程A和B,A執(zhí)行了writer方法,B執(zhí)行reader方法,那么B線程中獨到的變量x的值會是多少呢?
jdk1.5之前,線程B讀到的變量x的值可能是0,也可能是42,jdk1.5之后,變量x的值就是42了。原因是jdk1.5中,對volatile的語義進行了增強。來看一下happens-before規(guī)則在這段代碼中的體現(xiàn)。
jdk1.5之前,線程B讀到的變量x的值可能是0,也可能是42,jdk1.5之后,變量x的值就是42了。原因是jdk1.5中,對volatile的語義進行了增強。來看一下happens-before規(guī)則在這段代碼中的體現(xiàn)。
1. 規(guī)則一:程序的順序性規(guī)則
一個線程中,按照程序的順序,前面的操作happens-before后續(xù)的任何操作。
對于這一點,可能會有疑問。順序性是指,我們可以按照順序推演程序的執(zhí)行結果,但是編譯器未必一定會按照這個順序編譯,但是編譯器保證結果一定==順序推演的結果。
2. 規(guī)則二:volatile規(guī)則
對一個volatile變量的寫操作,happens-before后續(xù)對這個變量的讀操作。
3. 規(guī)則三:傳遞性規(guī)則
如果A happens-before B,B happens-before C,那么A happens-before C。
jdk1.5的增強就體現(xiàn)在這里。回到上面例子中,線程A中,根據(jù)規(guī)則一,對變量x的寫操作是happens-before對變量v的寫操作的,根據(jù)規(guī)則二,對變量v的寫操作是happens-before對變量v的讀操作的,最后根據(jù)規(guī)則三,也就是說,線程A對變量x的寫操作,一定happens-before線程B對v的讀操作,那么線程B在注釋處讀到的變量x的值,一定是42.
4.規(guī)則四:管程中的鎖規(guī)則
對一個鎖的解鎖操作,happens-before后續(xù)對這個鎖的加鎖操作。
這一點不難理解。
5.規(guī)則五:線程start()規(guī)則
主線程A啟動線程B,線程B中可以看到主線程啟動B之前的操作。也就是start() happens before 線程B中的操作。
6.規(guī)則六:線程join()規(guī)則
主線程A等待子線程B完成,當子線程B執(zhí)行完畢后,主線程A可以看到線程B的所有操作。也就是說,子線程B中的任意操作,happens-before join()的返回。
2.鎖的分類
3.樂觀鎖和悲觀鎖
3.1 為什么會誕生非互斥同步鎖(樂觀鎖)
互斥同步鎖(悲觀鎖)的劣勢
- 阻塞和喚醒帶來的性能劣勢
- 永久阻塞:如果持有鎖的線程被永久阻塞,比如遇到了無限循環(huán)、死鎖等活躍性問題,那么等待該線程釋放鎖的線程,將永遠也得不到執(zhí)行。
- 優(yōu)先級反轉 被阻塞的優(yōu)先級高的線程等待優(yōu)先級低的線程
3.2 什么是樂觀鎖和悲觀鎖
從是否鎖住資源的角度分類
- 悲觀鎖
如果不鎖住資源,被人就回來爭搶,就會造成數(shù)據(jù)結果錯誤,所以每次悲觀鎖為了確保結果的正確性,會在每次獲取并修改數(shù)據(jù)時,把數(shù)據(jù)鎖住,讓別人無法訪問數(shù)據(jù),這樣就就可以保證數(shù)據(jù)內容萬無一失。
Java中悲觀鎖的實現(xiàn)就是synchronized和Lock相關類
- 樂觀鎖
認為自己在處理操作的時候不會有其它線程的來干擾,所以并不會鎖住被操作對象
在更新的時候,去對比在我修改的期間數(shù)據(jù)有沒有被其他人改變過:
如果沒有改變過,就去正常修改數(shù)據(jù)。
如果數(shù)據(jù)和我一開始拿到的不一樣,就不去修改數(shù)據(jù),會選擇放棄、報錯、重試等策略。
樂觀鎖的實現(xiàn)一般都是利用CAS算法來實現(xiàn)的。
樂觀鎖Java的典型例子就是原子類、并發(fā)容器等。
樂觀鎖的其它例子:
1.Git
2.update set num = 2,version=version+1where version=1 and id = 5;
3.3 開銷對比
- 悲觀鎖的原始開銷要高于樂觀鎖,但是特點是一勞永逸,臨界區(qū)持鎖時間就算越來越差,也不會對互斥鎖的開銷造成影響
- 樂觀鎖一開始的開銷比悲觀鎖小,但是如果自旋時間很長或者不停重試,那么消耗的資源也會越來越多。
臨界區(qū)表示一種公共資源或共享數(shù)據(jù),可以被多個線程使用。但是每一次只能有一個線程使用它。一旦臨界區(qū)資源被占用,想使用該資源的其他線程必須等待。在java中通常使用下面的方式來實現(xiàn)
synchronized(syncObject) {
//critical section
}
3.4 使用場景
悲觀鎖:適合并發(fā)寫入多的情況,適用于臨界區(qū)持鎖時間比較長的情況,悲觀鎖可以避免大量的無用自旋等消耗,典型情況:
- 臨界區(qū)有IO操作
- 臨界區(qū)代碼復雜或者循環(huán)量大
- 臨界區(qū)競爭非常激烈
樂觀鎖:適合并發(fā)寫入少,大部分是讀取的場景,不加鎖能讓讀取性能大幅提高。
4.可重入鎖和非可重入鎖
非可重入鎖
所謂不可重入鎖,即若當前線程執(zhí)行某個方法已經(jīng)獲取了該鎖,那么在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞。
我們嘗試設計一個不可重入鎖:
public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
public class LockTest {
Lock lock = new Lock();
public void methodA() {
try {
lock.lock();
methodB();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println("methodA 執(zhí)行完了");
}
public void methodB() throws InterruptedException {
try {
lock.lock();
System.out.println("methodB 正在執(zhí)行...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockTest lockTest = new LockTest();
lockTest.methodA();
}
}
發(fā)生死鎖,結果無輸出
可重入鎖
可重入鎖,指的是以線程為單位,當一個線程獲取對象鎖之后,這個線程可以再次獲取本對象上的鎖,而其他的線程是不可以的。
synchronized 和 ReentrantLock 都是可重入鎖。
將前面
public class LockTest {
ReentrantLock lock = new ReentrantLock ();
public void methodA() {
try {
lock.lock();
methodB();
} finally {
lock.unlock();
}
System.out.println("methodA 執(zhí)行完了");
}
public void methodB() {
try {
lock.lock();
System.out.println("methodB 正在執(zhí)行...");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockTest lockTest = new LockTest();
lockTest.methodA();
}
}
methodB 正在執(zhí)行...
methodA 執(zhí)行完了
查看線程得到鎖的個數(shù)
public class GetHoldCount {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
}
}
0
1
2
3
2
1
0
public class RecursionDemo {
private static ReentrantLock lock = new ReentrantLock();
private static void accessResource() {
lock.lock();
try {
System.out.println("已經(jīng)對資源進行了處理");
if (lock.getHoldCount()<5) {
System.out.println(lock.getHoldCount());
accessResource();
System.out.println(lock.getHoldCount());
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
accessResource();
}
}
可重入鎖優(yōu)點
- 避免死鎖
- 提升代碼封裝性
源碼實現(xiàn)
可重入鎖ReentrantLock
非可重入鎖ThreadPoolExecutor得Worker類
ReentrantLock的其他方法介紹
- isheldbycurrentthread():查詢當前線程是否保持此鎖。
- getQueueLength():返回正等待獲取此鎖定線程數(shù),如果一共開啟了5個線程,一個線程執(zhí)行了await()方法,那么在調用此方法是,返回4,說明此時正有4個線程在等待鎖的釋放。
5.公平鎖和非公平鎖
5.1什么是公平和非公平
- 公平指的是按照線程請求的順序,來分配鎖;非公平指的是,不完全按照請求的順序,在一定的情況下,可以插隊。
- 注意:非公平也同樣不提倡“插隊”行為,這里的非公平,指的是“在合適的時機”插隊,而不是盲目插隊。
公平鎖
public class Service {
private ReentrantLock lock;
public Service(boolean isFair) {
super();
lock = new ReentrantLock(isFair);
}
public void serviceMethod() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName()
+ "獲取鎖定");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final Service service = new Service(true);
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("線程" + Thread.currentThread().getName()
+ "運行了");
service.serviceMethod();
}
};
Thread[] threadArray = new Thread[10];
for (int i = 0; i < 10; i++) {
threadArray[i] = new Thread(runnable);
}
for (int i = 0; i < 10; i++) {
threadArray[i].start();
}
}
}
線程Thread-0運行了
線程Thread-1運行了
Thread-0獲取鎖定
線程Thread-4運行了
Thread-1獲取鎖定
Thread-4獲取鎖定
線程Thread-5運行了
Thread-5獲取鎖定
線程Thread-8運行了
Thread-8獲取鎖定
線程Thread-2運行了
Thread-2獲取鎖定
線程Thread-3運行了
Thread-3獲取鎖定
線程Thread-6運行了
Thread-6獲取鎖定
線程Thread-9運行了
Thread-9獲取鎖定
線程Thread-7運行了
Thread-7獲取鎖定
非公平
上述代碼true 修改為false
final Service service = new Service(false);
線程Thread-0運行了
線程Thread-4運行了
線程Thread-5運行了
線程Thread-1運行了
Thread-0獲取鎖定
Thread-4獲取鎖定
線程Thread-2運行了
線程Thread-3運行了
Thread-5獲取鎖定
線程Thread-8運行了
Thread-8獲取鎖定
Thread-1獲取鎖定
Thread-2獲取鎖定
線程Thread-7運行了
Thread-3獲取鎖定
Thread-7獲取鎖定
線程Thread-9運行了
線程Thread-6運行了
Thread-9獲取鎖定
Thread-6獲取鎖定
5.2 什么要有非公平鎖
- 為了提高效率,避免喚醒帶來的空檔期
5.3公平和非公平的優(yōu)缺點
類型 | 優(yōu)勢 | 劣勢 |
---|---|---|
公平鎖 | 各線程公平平等,每個線程在等待一段時間后,總有執(zhí)行的機會 | 更慢,吞吐量更小 |
不公平鎖 | 更快,吞吐量更大 | 有可能產生線程饑餓,也就是某些線程在長時間內,始終得不到執(zhí)行 |
6.共享鎖和排它鎖:以ReentrantReadWriteLock讀寫鎖
排它鎖,又稱為獨占鎖,獨享鎖
共享鎖,又稱為讀鎖,獲得共享鎖之后,可以查看但無法修改和刪除數(shù)據(jù),其他線程此時也可以獲取到共享鎖,也可以查看單無法修改和刪除數(shù)據(jù)
共享鎖和排它鎖的典型是讀寫鎖ReentrantReadWriteLock,其中讀鎖是共享鎖,寫鎖是獨享鎖。
6.1讀寫鎖的作用
- 在沒有讀寫鎖之前,我們假設使用ReentrantLock,那么雖然我們保證了線程安全,但是浪費了一定資源,多個讀操作同時進行,并沒有線程安全問題
- 在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如果沒有寫鎖的情況下,讀是阻塞的,提高了程序的執(zhí)行效率。
6.2讀寫鎖的規(guī)則
- 多個線程只申請讀鎖,都可以申請到
- 如果有一個線程已經(jīng)占有了讀鎖,則此時其它線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖
- 如果有一個線程已經(jīng)占有了寫鎖,則此時其它線程如果要申請寫鎖或者讀鎖,則申請寫鎖的線程會一直等待釋放寫鎖
總結:要么一個或多個線程同時有讀鎖,要么是一個線程有寫鎖,但是兩者不會同時出現(xiàn)。
讀寫鎖只是一把鎖,可以通過兩種方式鎖定:讀鎖定和寫鎖定。讀寫鎖可以同時被一個或多個線程讀鎖定,也可以被單一線程寫鎖定。但是永遠不能同時對這把鎖進行讀鎖定和寫鎖定。
ReentrantReadWriteLock用法
public class ReadWrite {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了讀鎖,正在讀取");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "釋放讀鎖");
readLock.unlock();
}
}
private static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了寫鎖,正在寫入");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(()->read(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->write(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
}
}
Thread1得到了讀鎖,正在讀取
Thread2得到了讀鎖,正在讀取
Thread1釋放讀鎖
Thread2釋放讀鎖
Thread3得到了寫鎖,正在寫入
Thread3釋放寫鎖
Thread4得到了寫鎖,正在寫入
Thread4釋放寫鎖
可以同時讀,不能同時寫
6.3讀鎖和寫鎖的交互方式
讀鎖插隊策略
公平鎖:不允許插隊
非公平鎖下:假設線程2和線程4正在同時讀取,線程3想要寫入,拿不到鎖,于是進入等待隊列,線程5不在隊列里,現(xiàn)在過來想要讀取。此時有2中策略:
策略1:讀可以插隊
- 效率高
- 容易造成饑餓
策略2:避免饑餓
1.寫鎖可以隨時插隊
2.讀鎖僅在等待隊列頭結不是想獲取寫鎖的線程的時候可以插隊。
ReentrantReadWriteLock 選擇了策略2
6.4ReentrantReadWriteLock源碼
公平鎖的情況
非公平鎖的情況
public class ReadWriteQueue {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了讀鎖,正在讀取");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "釋放讀鎖");
readLock.unlock();
}
}
private static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了寫鎖,正在寫入");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(()->write(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->read(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
new Thread(()->read(),"Thread5").start();
}
}
Thread1得到了寫鎖,正在寫入
Thread1釋放寫鎖
Thread2得到了讀鎖,正在讀取
Thread3得到了讀鎖,正在讀取
Thread2釋放讀鎖
Thread3釋放讀鎖
Thread4得到了寫鎖,正在寫入
Thread4釋放寫鎖
Thread5得到了讀鎖,正在讀取
Thread5釋放讀鎖
非公平鎖:讀鎖僅在等待隊列頭結不是想獲取寫鎖的線程的時候可以插隊
代碼驗證如下
public class NonfairBargeDemo {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
false);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read() {
System.out.println(Thread.currentThread().getName() + "開始嘗試獲取讀鎖");
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到讀鎖,正在讀取");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
System.out.println(Thread.currentThread().getName() + "釋放讀鎖");
readLock.unlock();
}
}
private static void write() {
System.out.println(Thread.currentThread().getName() + "開始嘗試獲取寫鎖");
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到寫鎖,正在寫入");
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(()->write(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->read(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
new Thread(()->read(),"Thread5").start();
new Thread(new Runnable() {
@Override
public void run() {
Thread thread[] = new Thread[1000];
for (int i = 0; i < 1000; i++) {
thread[i] = new Thread(() -> read(), "子線程創(chuàng)建的Thread" + i);
}
for (int i = 0; i < 1000; i++) {
thread[i].start();
}
}
}).start();
}
}
6.4鎖的升降級
同一個線程中,在沒有釋放讀鎖的情況下,就去申請寫鎖,這屬于鎖升級,ReentrantReadWriteLock是不支持的。
同一個線程中,在沒有釋放寫鎖的情況下,就去申請讀鎖,這屬于鎖降級,ReentrantReadWriteLock是支持的
public class Upgrading {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
false);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void readUpgrading() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了讀鎖,正在讀取");
Thread.sleep(1000);
System.out.println("升級會帶來阻塞");
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "獲取到了寫鎖,升級成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "釋放讀鎖");
readLock.unlock();
}
}
private static void writeDowngrading() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了寫鎖,正在寫入");
Thread.sleep(1000);
readLock.lock();
System.out.println("在不釋放寫鎖的情況下,直接獲取讀鎖,成功降級");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
writeLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("先演示降級是可以的");
Thread thread1 = new Thread(() -> writeDowngrading(), "Thread1");
thread1.start();
thread1.join();
System.out.println("------------------");
System.out.println("演示升級是不行的");
Thread thread2 = new Thread(() -> readUpgrading(), "Thread2");
thread2.start();
}
}
先演示降級是可以的
Thread1得到了寫鎖,正在寫入
在不釋放寫鎖的情況下,直接獲取讀鎖,成功降級
Thread1釋放寫鎖
------------------
演示升級是不行的
Thread2得到了讀鎖,正在讀取
升級會帶來阻塞
ReentrantReadWriteLock 能降級不能升級
鎖降級中讀鎖的獲取是否必要呢?
答案是必要的。主要是為了保證數(shù)據(jù)的可見性,如果當前線程不獲取讀鎖而是直接釋放寫鎖, 假設此刻另一個線程(記作線程T)獲取了寫鎖并修改了數(shù)據(jù),那么當前線程無法感知線程T的數(shù)據(jù)更新。如果當前線程獲取讀鎖,即遵循鎖降級的步驟,則線程T將會被阻塞,直到當前線程使用數(shù)據(jù)并釋放讀鎖之后,線程T才能獲取寫鎖進行數(shù)據(jù)更新。
public class DegradeLock {
private int value;
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readLock = lock.readLock();
private Lock writeLock = lock.writeLock();
public void writeRead(){
try{
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.writeLock.lock();//1
this.value++; //2
//this.readLock.lock(); //3
this.writeLock.unlock();//4
System.out.printf("%s 讀取到value的值為 %d \n",Thread.currentThread().getName(),this.value);
// this.readLock.unlock();//5
}
public static void main(String[] args) {
DegradeLock dl = new DegradeLock();
new Thread(()->dl.writeRead()).start();
new Thread(()->dl.writeRead()).start();
new Thread(()->dl.writeRead()).start();
new Thread(()->dl.writeRead()).start();
new Thread(()->dl.writeRead()).start();
}
}
可以看到,出現(xiàn)并發(fā)問題了。
把代碼中的注釋打開之后,問題解決。
原因是3處又用讀鎖鎖了一次,4處雖然釋放了鎖,由于又加了3處的鎖,讀寫鎖互斥,其他線程仍然無法進入2處修改值,只有到5處釋放了鎖,其它線程才能競爭1處的鎖。
6.5適合場景
ReentrantReadWriteLock適用于讀多寫少的情況,合理使用可以進一步提高并發(fā)效率。
7.自旋鎖和阻塞鎖
阻塞或喚醒一個Java線程需要操作系統(tǒng)切換CPU狀態(tài)來完成,這種狀態(tài)轉換需要耗費處理器時間。
如果同步代碼塊中的內容過于簡單,狀態(tài)轉換消耗的時間有可能比用戶代碼執(zhí)行的時間還要長。
在許多場景中,同步資源的鎖定時間很短,為了這一小段時間去切換線程,線程掛起和恢復現(xiàn)場的花費可能會讓系統(tǒng)得不償失。
如果物理機器有多個處理器,能夠讓兩個或以上的線程同時并行執(zhí)行,我們就可以讓后面那個請求鎖的線程不放棄CPU的執(zhí)行時間,看看持有鎖的線程能否很快就釋放鎖。
而為了讓當前線程“稍等一下”,我們需讓當前線程進行自旋,如果在自旋完成后前面鎖定同步資源的線程已經(jīng)釋放了鎖,那么當前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷。
這就是自旋。
自旋的缺點:
不能代替阻塞。雖然避免線程切換的開銷,但要占用處理器時間。如果鎖被占用的時間很短,自旋等待的效果就會非常好。反之,如果鎖被長時間占用,那么自旋的線程只會浪費處理器資源。
所以,自旋等待的時間必須有一定的限度,如果自旋超過了限度次數(shù)(默認是10次,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應當掛起線程。
自旋鎖的實現(xiàn)原理同樣也是CAS,AtomInteger中調用unsafe進行自增操作的源碼中do-while循環(huán)就是一個自旋操作,如果修改數(shù)值失敗則通過循環(huán)來執(zhí)行自旋,直至修改成功。
public class SpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)) {
System.out.println("自旋獲取失敗,再次嘗試");
}
}
public void unlock() {
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "開始嘗試獲取自旋鎖");
spinLock.lock();
System.out.println(Thread.currentThread().getName() + "獲取到了自旋鎖");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "釋放了自旋鎖");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
自旋鎖的適用場景
自旋鎖一般用于多核的服務器,在并發(fā)度不是特別高的情況下,比阻塞鎖的效率高
自旋鎖適用于臨界區(qū)比較短小的情況,負責如果臨界區(qū)(線程一旦拿到鎖,很久以后才釋放),那也是不合適的。
8.可中斷鎖:顧名思義,就是可以響應中斷的鎖
如果某一線程A正在執(zhí)行鎖中的代碼,另一個線程B正在等待獲取該鎖,可能由于等待時間過長,線程B不想等待了,想先處理其他事情,我們可以中斷它,這就是可中斷鎖。
在Java中,synchronized就是不可中斷鎖,而Lock是可中斷鎖,因為tryLock(time)和lockInterruptibly都能響應中斷。
9.鎖優(yōu)化
9.1 Java虛擬機對鎖的優(yōu)化
- 自旋鎖和自適應
- 鎖消除
- 鎖粗化
9.2 如何優(yōu)化鎖和提高并發(fā)性能
- 縮小同步代碼塊
- 盡量不要鎖住方法
- 減少請求鎖的次數(shù)
- 避免人為制造熱點
- 鎖中盡量不要再包含鎖
- 選擇合適的鎖類型或合適的工具類