一、synchronized

  • 下面代碼m1方法鎖定的是 o 對象,也就是堆中那個真實的對象,而不是棧中的引用。m2方法鎖定的是this對象,m3方法用synchronized修飾方法,相當於m2方法的synchronized(this),synchronized修飾m4靜態方法相當於synchronized(this.getClass()),鎖定就是當前對象所屬類的位元組碼對象。

public class T {
Object o = new Object()
public void m1() {
synchronized(o) {
System.out.println(Thread.currentThread().getName() + " m1 start...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m1 end...");
}
}
public void m2() {
synchronized(this) {
System.out.println(Thread.currentThread().getName() + " m2 start...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m2 end...");
}
}
public synchronized void m3() {
System.out.println(Thread.currentThread().getName() + " m3 start...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m3 end...");
}
public synchronized static void m3() {
System.out.println(Thread.currentThread().getName() + " m3 start...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m3 end...");
}
}

  • synchronized支持可重入鎖,也就是可以在同步方法中調用使用相同鎖對象的同步方法,如下所示

public class T {
public synchronized void m1() {
System.out.println("m1 method");
m2();
}
?
public synchronized void m2() {
System.out.println("m2 method");
}

public static void main(String[] args) {
T t = new T();
new Thread(t::m1).start();;
}
}

  • synchronized執行時如果拋出異常交給jvm處理,會釋放鎖,因此碰到異常時要小心處理

public class T {
int count;
public synchronized void m() {
System.out.println(Thread.currentThread().getName() + " start...");
while(true) {
count++;
System.out.println(Thread.currentThread().getName() + " count = " +
count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
//要想拋異常鎖不被釋放,這裡應該try...catch...捕獲到異常正確的處理
//原因:自己沒有處理異常jvm就會處理,這時候jvm就會把鎖釋放
try {
int i = 1 / 0;
} catch (Exception e) {
System.out.println("出現了異常");
}
}
}
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m, "t1").start();

try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(t::m, "t2").start();
}
}

  • synchronized中的代碼越少效率越高,因為減少了線程搶佔鎖的時間

public class T {
int count = 0;

synchronized void m1() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//真正需要原子操作的只有下面這一句代碼
count++;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void m2() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(this) {
count++;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

二、ReentrantLock

  • ReentrantLock 可以用來替換synchronized ReentrantLock手動開鎖,必須要必須要必須要手動釋放鎖,因為要手動釋放鎖,所以報異常時並不會釋放鎖

public class ReentrantLock1 {
?
Lock lock = new ReentrantLock();
?
public void m1() {
lock.lock();
try {
for (int i = 1; i <= 10; i++) {
Thread.sleep(1000);
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public void m2() {
lock.lock();
System.out.println("m2...");
lock.unlock();
}

public static void main(String[] args) {
ReentrantLock1 r = new ReentrantLock1();
new Thread(r::m1).start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(r::m2).start();
}
}

  • ReentrantLock可以使用tryLock嘗試拿下鎖,根據是否拿到鎖進行不同的業務處理,也可以指定允許等待鎖的時間

public class ReentrantLock2 {

Lock lock = new ReentrantLock();

public void m1() {
lock.lock();
try {
for (int i = 1; i <= 10; i++) {
Thread.sleep(1000);
System.out.println(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public void m2() {
//嘗試拿鎖
boolean isLock = lock.tryLock();
if (isLock) {
System.out.println("執行拿到的鎖的業務邏輯:" + isLock);
lock.unlock();
} else {
System.out.println("執行沒有拿到鎖的業務邏輯:" + isLock);
}

/*boolean isLock = false;
try {
//嘗試等待5秒,看是否能拿到鎖
isLock = lock.tryLock(5, TimeUnit.SECONDS);
if (isLock) {
System.out.println("執行拿到的鎖的業務邏輯:" + isLock);
} else {
System.out.println("執行沒有拿到鎖的業務邏輯:" + isLock);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (isLock) {
lock.unlock();
}
}*/
}

public static void main(String[] args) {
ReentrantLock2 r = new ReentrantLock2();
new Thread(r::m1).start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(r::m2).start();
}
?
}

  • lockInterruptibly()可對interrupt()作出響應,這個拿鎖如果一直拿不到可被打斷

public class ReentrantLock3 {

public static void main(String[] args) {
Lock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
lock.lock();
try {
System.out.println("t1 start...");
//基本上就是在這死等著了
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
//如果釋放鎖了,t2線程就能拿到這把鎖
System.out.println("t1 end...");
}
});
t1.start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

Thread t2 = new Thread(() -> {
System.out.println("t2 start...");
try {
// lock.lock();
//對interrupt()作出響應,可被打斷的拿鎖
lock.lockInterruptibly();
} catch (Exception e) {
System.out.println("interrupted!");
} finally {
lock.unlock();
System.out.println("t2 end...");
}
});
t2.start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//使t2線程不要一直在那等待鎖了,直接停止
t2.interrupt();
}
}

  • ReentrantLock默認構造器為非公平鎖,構造器傳true可以指定為公平鎖
    • 非公平鎖:誰拿到鎖不確定,全靠CPU調度
    • 公平鎖:線程拿鎖的機會平均

public class ReentrantLock4 extends Thread {
?
// Lock lock = new ReentrantLock();
Lock lock = new ReentrantLock(true);

public void run() {
for(int i = 0; i < 100; i++) {
lock.lock();
System.out.println(Thread.currentThread().getName() + "拿到了鎖");
lock.unlock();
}
}

public static void main(String[] args) {
ReentrantLock4 r = new ReentrantLock4();
new Thread(r).start();
new Thread(r).start();
}
}

三、生產者消費者模式

自定義一個固定容量同步容器,具有put,get,getCount方法.

能夠支持2個生產者線程以及10個消費者線程的阻塞調用

1、使用synchronized

1)判斷那裡為什麼使用while而不是if?

因為while會一直判斷,而if只會判斷一次
比如有兩個生產者線程t1和t2,兩個都被喚醒了,當t1拿到鎖後往裡扔了一個,這時候容器滿了,執行完後釋放鎖,這時候t2線程又拿到了鎖,因為if判斷之前只判斷了一次,所以這時候不會判斷了,拿到鎖之後繼續執行添加,此時容器已經滿了,這裡就會出問題

2)使用notifyAll()而不是notify()

喚醒所有線程這裡使用notifyAll而不是notify,如果使用notify又喚醒了一個生產者線程而不是消費者線程,生產者線程判斷此時容器滿了它就會等待,此時程序就卡住了

public class MyContainer1<T> {

private final LinkedList<T> list = new LinkedList<>();
//容器最大容量為10
private static final int MAX = 10;
private int count = 0;

public synchronized void put(T t) {
while(list.size() == MAX) {
try {
//如果當前容量滿了就等待然後釋放鎖
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果沒有滿就往裡添加
list.add(t);
++count;
System.out.println(Thread.currentThread().getName() + "生產了第" + count + "個元
素");
this.notifyAll();
}

public synchronized T get() {
while(list.size() == 0) {
try {
//如果容器中沒有數據了就等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//有數據就減少一個並且喚醒所有線程(生產者線程進行生產),返回一個元素
System.out.println(Thread.currentThread().getName() + "消費了第" + count + "個元
素");
--count;
this.notifyAll();
return list.removeFirst();
}

public int getCount() {
return count;
}

public static void main(String[] args) {
MyContainer1<Object> c = new MyContainer1<>();
//先啟動十個消費者線程進行等待
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//一次消費5個
for (int j = 1; j <= 5; j++) {
c.get();
}
}, "c" + i).start();
}

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}

//再啟動2個生產者線程
for (int i = 0; i < 2; i++) {
new Thread(() -> {
//生產者一次生產25個
for (int j = 1; j <= 25; j++) {
c.put(j);
}
},"p" + i).start();
}
}
}

2、使用ReentrantLock實現

使用ReentrantLock實現顯然效率要高一點,這裡可以使用Condition來指定線程在哪個條件上等待,後面喚 醒的時候直接在對應的條件上喚醒,而不是喚醒所有的線程

public class MyContainer2<T> {
private final LinkedList<T> list = new LinkedList<>();
// 容器最大容量為10
private static final int MAX = 10;
private int count = 0;

private Lock lock = new ReentrantLock();
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();

public void put(T t) {
try {
lock.lock();
while (list.size() == MAX) {
//如果當前容量滿了就等待然後釋放鎖
producer.await();
}
// 如果沒有滿就往裡添加
list.add(t);
++count;
System.out.println(Thread.currentThread().getName() + "生產了第" + count + "個
元素");
// 喚醒所有在consumer條件上等待的線程
consumer.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public T get() {
T t = null;
try {
lock.lock();
while (list.size() == 0) {
//如果容器中沒有數據了就等待
consumer.await();
}
//有數據就減少一個並且喚醒所有線程(生產者線程進行生產),返回一個元素
System.out.println(Thread.currentThread().getName() + "消費了第" + count + "個
元素");
--count;
//喚醒所有在producer條件上等待的線程
producer.signalAll();
t = list.removeFirst();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return t;
}

public int getCount() {
return count;
}

public static void main(String[] args) {
MyContainer2<Object> c = new MyContainer2<>();
// 先啟動十個消費者線程進行等待
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 一個消費者可以消費5個
for (int j = 1; j <= 5; j++) {
c.get();
}
}, "c" + i).start();
}

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}

// 再啟動2個生產者線程
for (int i = 0; i < 2; i++) {
new Thread(() -> {
// 一個生產者可以生產25個
for (int j = 1; j <= 25; j++) {
c.put(j);
}
}, "p" + i).start();
}
}
}

結論:

  • ReentrantLock 可以用來替換synchronized ReentrantLock手動開鎖,必須要必須要必須要手動釋放鎖
  • synchronized報異常時jvm會手動釋放鎖,ReentrantLock不會釋放
  • ReentrantLock比synchronized要靈活
  • ReentrantLock可以指定為公平鎖
  • ReentrantLock還可以打斷拿鎖,嘗試拿鎖,指定線程在哪個條件上等待

它們都是可重入鎖,雖然ReentrantLock比synchronized看起來要好很多,但是synchronized實現簡單,語義清晰,便於JVM堆棧跟蹤,加鎖解鎖過程由JVM自動控制,具體使用哪個看實際中的業務。


推薦閱讀:
相關文章