Java 高併發系列2-併發鎖
接着上一篇併發文章我們繼續
Java 高併發系列1-開篇
本篇的主要內容是以下幾點:
- wait 、notify 的簡單使用
- Reentrantlock的簡單使用
- synchronized 與Reentrantlock的區別
- ThreadLocal的簡單使用
看一個面試題:
曾經的面試題:(淘寶?)
實現一個容器,提供兩個方法,add,size
寫兩個線程,線程1添加10個元素到容器中,線程2實現監控元素的個數,當個數到5個時,線程2給出提示並結束
public class MyContainer1 {
/* volatile */ List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
MyContainer1 c = new MyContainer1();
new Thread(() -> {
for(int i=0; i<10; i++) {
c.add(new Object());
System.out.println("add " + i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
new Thread(() -> {
while(true) {
if(c.size() == 5) {
break;
}
}
System.out.println("t2 結束");
}, "t2").start();
}
}
看完大概實現方法就是 使用while(true) 死循環 進行讀取容器的大小,如果不添加volatile 關鍵字 t2 沒有辦法跳出while循環,因爲容器的改變對 t2不可見。
添加volatile 字段可見之後,當然可以實現對容器大小的監聽。
但是也存在兩個問題,
第一、浪費CPU,在t1執行到5之前CPU都是在空轉。
第二、不夠精準,在循環判斷容器大小==5時 跳出循環的時候 可能容器的大小已經添加到了6 或者7 。
爲了解決這個問題, 我們再來看一條程序,
public class MyContainer3 {
//添加volatile,使t2能夠得到通知
volatile List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
MyContainer3 c = new MyContainer3();
final Object lock = new Object();
new Thread(() -> {
synchronized(lock) {
System.out.println("t2啓動");
if(c.size() != 5) {
try {//// 掛起程序 釋放鎖 lock 等待。
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 結束");
}
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("t1啓動");
synchronized(lock) {
for(int i=0; i<10; i++) {
c.add(new Object());
System.out.println("add " + i);
if(c.size() == 5) {
lock.notify();
//// 獲取鎖 lock 等待, 當size ==5 lock.notify 喚醒 t2
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1").start();
}
}
看完程序 這裏使用wait和notify做到,wait會釋放鎖,而notify不會釋放鎖
也可以 在 t1 notify之後,t1必須釋放鎖,t2退出後,也必須notify,通知t1繼續執行
缺點: 整個通信過程比較繁瑣
需要注意的是,運用這種方法,必須要保證t2先執行,也就是首先讓t2監聽纔可以。
再看一條程序
public class MyContainer5 {
// 添加volatile,使t2能夠得到通知
volatile List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
MyContainer5 c = new MyContainer5();
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
System.out.println("t2啓動");
if (c.size() != 5) {
try {
latch.await();
//也可以指定等待時間
//latch.await(5000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 結束");
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("t1啓動");
for (int i = 0; i < 10; i++) {
c.add(new Object());
System.out.println("add " + i);
if (c.size() == 5) {
// 打開門閂, 拉閘放水, 讓t2得以執行
latch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
}
}
使用CountDownLatch (門閂)替代wait notify來進行通知 好處是通信方式簡單,同時也可以指定等待時間 使用await和countdown方法替代wait和notify CountDownLatch不涉及鎖定,當count的值爲零時當前線程繼續運行 當不涉及同步,只是涉及線程通信的時候,用synchronized + wait/notify就顯得太重了
這時應該考慮 countdownlatch/cyclicbarrier/semaphore
接下來再看一下Reentrantlock
程序1
public class ReentrantLock1 {
synchronized void m1() {
for(int i=0; i<10; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
synchronized void m2() {
System.out.println("m2 ...");
}
public static void main(String[] args) {
ReentrantLock1 rl = new ReentrantLock1();
new Thread(rl::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(rl::m2).start();
}
}
Reentrantlock用於替代synchronized , 由於m1鎖定this,只有m1執行完畢的時候,m2才能執行 這也是synchronized最原始的意義。
程序2
先簡單註釋一下
public class ReentrantLock2 {
/// 聲明鎖
Lock lock = new ReentrantLock();
void m1() {
try {
/// 上鎖 ,相當於synchronized(this)
lock.lock(); //synchronized(this)
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
///// 這裏的try{}finally{} // 一定要加 , 在finally塊中 釋放鎖。
lock.unlock();
}
}
void m2() {
lock.lock();
System.out.println("m2 ...");
lock.unlock();
}
public static void main(String[] args) {
ReentrantLock2 rl = new ReentrantLock2();
new Thread(rl::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(rl::m2).start();
}
}
這裏不再過多解釋, 看註釋。
由於synchronized 是碰見異常 jvm自動釋放鎖。 而 ReentrantLock不行, 需要手動釋放。 所以一般情況下 放在finally語句裏。
** 重要的事情說三遍, 必須手動釋放, 手動釋放。必須手動釋放。 **
程序3.
public class ReentrantLock3 {
/// 聲明
Lock lock = new ReentrantLock();
void m1() {
try {
/// 鎖
lock.lock();
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
/// 釋放
lock.unlock();
}
}
/**
* 使用tryLock進行嘗試鎖定,不管鎖定與否,方法都將繼續執行
* 可以根據tryLock的返回值來判定是否鎖定
* 也可以指定tryLock的時間,由於tryLock(time)拋出異常,所以要注意unclock的處理,必須放到finally中
*/
void m2() {
/*
boolean locked = lock.tryLock();
System.out.println("m2 ..." + locked);
if(locked) lock.unlock();
<!--locked 後邊這裏可以根據是否鎖定來選擇執行相關邏輯-->
*/
boolean locked = false;
try {
locked = lock.tryLock(5, TimeUnit.SECONDS);
//////// 可以嘗試鎖定,等待5秒鐘, 超時後鎖定失敗,返回false
System.out.println("m2 ..." + locked);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(locked) lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLock3 rl = new ReentrantLock3();
new Thread(rl::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(rl::m2).start();
}
}
看起來是不是ReentrantLock 是不是高級很多,繼續
下一條程序
public class ReentrantLock4 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Thread t1 = new Thread(()->{
try {
lock.lock();
System.out.println("t1 start");
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
//// 1. 睡眠不釋放鎖, 2. 睡眠這麼長時間,相當於睡死了都, 看下個線程。
System.out.println("t1 end");
} catch (InterruptedException e) {
System.out.println("interrupted!");
} finally {
lock.unlock();
}
});
t1.start();
Thread t2 = new Thread(()->{
try {
//lock.lock();
///// 顧名思義就是 把鎖打斷, 打斷線程1的等待
lock.lockInterruptibly(); //可以對interrupt()方法做出響應
System.out.println("t2 start");
TimeUnit.SECONDS.sleep(5);
System.out.println("t2 end");
} catch (InterruptedException e) {
System.out.println("interrupted!");
} finally {
lock.unlock();
}
});
t2.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.interrupt(); //打斷線程2的等待
}
}
lockInterruptibly()獲取鎖是以排他的模式獲取,一旦被中斷就放棄等待獲取, 可以對線程interrupt方法做出響應,在一個線程等待鎖的過程中,可以被打斷。
ReentrantLock 除了是可重入鎖 還可以設置公平鎖和非公平鎖。再來一條程序
public class ReentrantLock5 extends Thread {
private static ReentrantLock lock=new ReentrantLock(true); //參數爲true表示爲公平鎖,請對比輸出結果
public void run() {
for(int i=0; i<100; i++) {
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"獲得鎖");
}finally{
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLock5 rl=new ReentrantLock5();
Thread th1=new Thread(rl);
Thread th2=new Thread(rl);
th1.start();
th2.start();
}
}
根據參數true 或者 false ,是否可以設定爲公平鎖。
所謂的公平鎖設定算法爲 調度器優先選擇等待時間長的線程執行。
而非公平鎖則沒有該設定。
再來看看ThreadLocal , 顧名思義, ThreadLocal線程局部變量
來一條程序,
public class ThreadLocal1 {
//// 聲明person 對象 volatile ,
volatile static Person p = new Person();
public static void main(String[] args) {
new Thread(()->{
try {
//// 睡2秒
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
/// 打印
System.out.println(p.name);
}).start();
new Thread(()->{
try {
/// 睡1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 賦值
p.name = "lisi";
}).start();
}
}
class Person {
String name = "zhangsan";
}
打印結果 lisi , 線程1 睡2秒, 睡醒後根據 volatile關鍵字特性, 線程修改 對其他線程可見, 既可以讀取到 線程2 修改後的值。
再來一條,
public class ThreadLocal2 {
//volatile static Person p = new Person();
///// 將聲明 封裝了Person的ThreadLocal對象
static ThreadLocal<Person> tl = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(tl.get());
}).start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
tl.set(new Person());
}).start();
}
static class Person {
String name = "zhangsan";
}
}
由於線程1 線程2 都是讀取自己Thread 對應的ThreadLocalMap 對象。
所以 打印結果 就是 當前線程取到的值 null 。
在Handler消息機制中的應用就是 Looper 通過 ThreadLocal 與currentThread 綁定了,所以才實現了通過Handler sendMessage 到指定線程。 如果想要詳細瞭解ThreadLocal的使用原理 移步我以前寫的一篇文章。
ThreadLocal源碼詳細解析
還有 在hibernate中session就存在與ThreadLocal中,避免synchronized的使用
好了, 囉裏囉嗦,說了一大通,看的雲裏霧裏。 其實我覺得如果能把代碼拿出來 敲一下,跑一跑,應該就會明白使用多線程和鎖的妙處。 東西比較多,如果有什麼不對的,請批評指正。 這篇就先說到這裏,下篇我們再見。