synchronized 鎖的三類對象
- Class鎖, 常見有
synchronized(this.getClass())
, 以及靜態方法加鎖 - 對象鎖, 常見有
synchronized(this)
, 以及實例方法加鎖 - 屬性鎖
八鎖現象
兩個線程持有同一把鎖, 後搶到鎖的線程需要等待鎖的釋放:
public class Test1 {
public static void main(String[] args){
Phone phone = new Phone();
new Thread(()->{
phone.sendSms();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
}, "B").start();
}
}
class Phone {
private Integer num =0;
// 鎖當前的 Phone對象實例
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發短信");
}
public void call(){
// this 也表示當前對象實例, 所以兩個線程共用鎖
synchronized (this) {
System.out.println("打電話");
}
}
}
一個線程持有鎖, 另一個線程不持有, 無需等待鎖釋放
上例中, 去掉 call() 方法中的 synchronized 塊, 則 “打電話” 先於 “發短信”
兩個線程持有不同種類的鎖, 無需彼此等待鎖釋放
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.sendSms();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone {
private Integer num = 0;
// 鎖對象
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發短信");
}
public void call() {
// 鎖屬性
synchronized (num) {
System.out.println("打電話");
}
}
}
同一種鎖, 鎖不同的對象, 無需彼此等待
public class Test1 {
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone1.sendSms();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.call();
}, "B").start();
}
}
class Phone {
private Integer num = 0;
// 雖然都是鎖 Phone的實例, 但是一個是 phone1, 一個是phone2, 不是同一把鎖
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發短信");
}
public synchronized void call() {
System.out.println("打電話");
}
}
static 修飾的方法上加鎖, 等於鎖類的Class對象
public class Test1 {
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone1.sendSms();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.call();
}, "B").start();
}
}
class Phone {
private Integer num = 0;
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發短信");
}
public static synchronized void call() {
System.out.println("打電話");
}
}
上例中, 兩個線程鎖的都是 Phone.Class 對象, 共用同一把鎖, 所以先 “發短信”, 再"打電話"
下面例子, 線程仍共用一把鎖, 能證明鎖 static 方法就是鎖 Class對象
public class Test1 {
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone1.sendSms();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.call();
}, "B").start();
}
}
class Phone {
private Integer num = 0;
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發短信");
}
public void call() {
synchronized (Phone.class) {
System.out.println("打電話");
}
}
}
線程的狀態
wait() 和 sleep() 的區別
wait() 使得線程釋放鎖, 進入等待隊列, 只有被 notify() 或 notifyAll () 喚醒, 進入鎖池; sleep() 的線程不會釋放鎖
wait() 和 notify() 必須在同步塊內出現, 而 sleep() 不一定
生產者消費者模式
核心: 利用wait() 和 notify() 控制線程之間通信, 比如, 如何控制兩個線程, 使他們交替工作?
/**
* 線程操作資源類
*/
public class Data {
private int num = 0;
public synchronized void increment() throws InterruptedException {
if (num > 0) {
wait();
}
System.out.println(Thread.currentThread().getName() + " =>" + (++num));
notifyAll();
}
public synchronized void decrement() throws InterruptedException {
if (num <= 0) {
wait();
}
System.out.println(Thread.currentThread().getName() + " =>" + (--num));
notifyAll();
}
}
/**
* 生產者- 消費者模式 線程通信
*/
public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for(int i=1;i<=100;i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for(int i=1;i<=100;i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
}
}
可以看到, num>0時, A等待B將num變爲0再喚醒自己; num<=0 時, B等待A將num變爲1, 再喚醒自己; 兩個線程交替輸出0和1
虛假喚醒問題
如果, 在上例的Test 類中, 啓動如下四個線程:
/**
* 生產者- 消費者模式 線程通信
*/
public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for(int i=1;i<=100;i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for(int i=1;i<=100;i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for(int i=1;i<=100;i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for(int i=1;i<=100;i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
<=0 則BD線程 等待, >0則AC 線程等待, 那 B =>-1從何而來呢? 這就涉及 虛假喚醒
問題
某一時刻, 當 num==0 時, D搶到鎖, 進入等待隊列, B再搶到鎖, 進入等待隊列, 四個線程狀態如下:
等待隊列: B,D
鎖池: A,C
假設 A獲得鎖, 執行 ++num, 並用 notifyAll() 喚醒所有等待隊列中的線程, 則 BD都進入鎖池, 如果下一時刻, D搶到鎖, 執行 --num, num爲0
下一時刻, 如果B搶到鎖, 繼續執行B線程, 由於B之前停在 wait(), 此時無需做 if判斷, 可以直接 --num, 造成了 num爲負數
public synchronized void decrement() throws InterruptedException {
if (num <= 0) {
wait(); // 線程被喚醒時, 從這裏往後走
}
System.out.println(Thread.currentThread().getName() + " =>" + (--num));
notifyAll();
}
解決虛假喚醒的方法
虛假喚醒的原因: wait()
在 if 代碼塊中, 喚醒時不再經過 if 的判斷
解決方法: if 替換爲 while; 或者加 else{}
Lock 鎖的基本 API
我們用 Lock 鎖的寫法, 替換上面例子的 Data 資源類
/**
* 多線程操作資源類
*/
public class Data {
private int num =0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition(); // 將 Condition 和 Lock綁定
public void increment(){
lock.lock(); // 手動鎖代碼
try {
while (num > 0) {
condition.await();
}
System.out.println(Thread.currentThread().getName() + " =>" + (++num));
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); // 手動釋放鎖
}
}
public void decrement(){
lock.lock();
try {
while (num <= 0) {
condition.await();
}
System.out.println(Thread.currentThread().getName() + " =>" + (--num));
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
使用 Condition 監控類, 精確喚醒線程
Lock 鎖和 synchronized 鎖的最大區別, 就在於 Lock 結合 Condition 可以精確喚醒線程
/**
* A,B,C,A,B,C 順序執行
*/
public class Data {
private volatile int num = 0;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public int getNum() {
return num;
}
public void printA() {
lock.lock();
try {
if (num < 100 && num % 3 != 0) {
condition1.await();
} else{
System.out.println(Thread.currentThread().getName() + " =>" + (++num));
condition2.signal();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
if (num < 100 && num % 3 != 1) {
condition2.await();
} else{
System.out.println(Thread.currentThread().getName() + " =>" + (++num));
condition3.signal();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
if (num < 100 && num % 3 != 2) {
condition3.await();
} else{
System.out.println(Thread.currentThread().getName() + " =>" + (++num));
condition1.signal();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
while (data.getNum()<100) {
data.printA();
}
}, "A").start();
new Thread(() -> {
while (data.getNum()<100) {
data.printB();
}
}, "B").start();
new Thread(() -> {
while (data.getNum()<100) {
data.printC();
}
}, "C").start();
}
}