十、同步機制解決Thread繼承安全問題
建立三個視窗買票,共100張票。用繼承來實現
-
方式一:同步程式碼塊
public class RunMainExtends { public static void main(String[] args) { Win win1 = new Win(); Win win2 = new Win(); Win win3 = new Win(); // 設定執行緒的名字 win1.setName("視窗一:"); win2.setName("視窗二:"); win3.setName("視窗三:"); // 開啟執行緒 win1.start(); win2.start(); win3.start(); } } /* 方式一:同步程式碼塊*/ class Win extends Thread { /* 注意:ticket、object必須加static。因為例項化的三個執行緒物件,是不同的物件,他們各自有自己的棧和程式計數器。只有加了static才能讓這個三個物件共享ticket、object。 */ private static int ticket = 100; private static Object object = new Object(); @Override public void run() { while (true) { synchronized (object){// 同步程式碼塊,同步監視器,必須是同一把鎖 if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"獲取到了第"+ticket+"票"); ticket--; }else{ break; } } } } }
-
方式二:同步方法
public class RunMainExtends { public static void main(String[] args) { Win win1 = new Win(); Win win2 = new Win(); Win win3 = new Win(); // 設定執行緒的名字 win1.setName("視窗一:"); win2.setName("視窗二:"); win3.setName("視窗三:"); // 開啟執行緒 win1.start(); win2.start(); win3.start(); } } /*方式二:同步方法 * */ class Win extends Thread { private static int ticket = 100; private static Object object = new Object(); @Override public void run() { while (true) { show(); if (ticket == 0) { break;// 用於跳出迴圈 } } } /* 定義一個同步方法,注意必須把這個方法定義為一靜態方法 * */ private static synchronized void show() { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"獲取到了第"+ticket+"票"); ticket--; } } }
同步方法總結:
/** * 同步機制解決“繼承Thread類”的執行緒安問題 * 關於同步方法的總結: * 1. 同步方法任然設計來到同步監視器,只是不需要我們顯示的宣告。 * 2. 非靜態同步方法,同步監視器是this * 3. 靜態同步方法,同步監視器是類本身 */
十一、執行緒的死鎖問題
11.1 死鎖簡介
- 不同的執行緒分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了執行緒的死鎖。【如:有多把鎖】
- 出現死鎖後,不會出現異常,不會出現提示,只是所有的執行緒都處於阻塞狀態,無法繼續
11.2 死鎖解決方法
-
專門的演算法、原則
-
儘量減少同步資源的定義
-
儘量避免巢狀同步
-
寫程式碼時,要儘量避免死鎖
十二、方式三:Lock(鎖)
12.1 Lock簡介
- 從JDK 5.0開始,Java提供了更強大的執行緒同步機制——通過顯式定義同步鎖物件來實現同步。同步鎖使用Lock物件充當。
- java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具。鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對Lock物件加鎖,執行緒開始訪問共享資源之前應先獲得Lock物件。
- ReentrantLock類實現了Lock,它擁有與synchronized相同的併發性和記憶體語義,在實現執行緒安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖。
總結:Lock鎖是java在5.0之後提出來的一種執行緒同步機制,我們可以把它 列為解決執行緒安全的第三種方式
12.2 列子
// 賣票
public class RunMainLock {
public static void main(String[] args) {
Wins wins = new Wins();
/*建立三個執行緒*/
Thread win1 = new Thread(wins);
Thread win2 = new Thread(wins);
Thread win3 = new Thread(wins);
/*設定執行緒名字*/
win1.setName("視窗一:");
win2.setName("視窗二:");
win3.setName("視窗三:");
/*開啟執行緒*/
win1.start();
win2.start();
win3.start();
}
}
class Wins implements Runnable{
private int ticket = 100;
/*定義Lock鎖*/
/*第一步:例項化ReentrantLock*/
private ReentrantLock lock = new ReentrantLock();/* 引數為true,執行緒先進先出;預設為false,即cpu輪到誰就誰,看運氣*/
@Override
public void run() {
while (true) {
try{
/*第二步:加鎖*/
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "賣票——" + ticket);
ticket--;
} else {
break;
}
}finally {
/*第三步:解鎖*/
lock.unlock();
}
}
}
}
12.3 synchronized與Lock的異同
-
相同點:
- 都能解決執行緒的安全問題
-
不同點
- Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖),synchronized是隱式鎖,出了作用域自動釋放
- Lock只有程式碼塊鎖,synchronized有程式碼塊鎖和方法鎖
- 使用Lock鎖,JVM將花費較少的時間來排程執行緒,效能更好。並且具有更好的擴充套件性(提供更多的子類)
-
總結:
優先使用順序(建議):
Lock>同步程式碼塊(已經進入了方法體,分配了相應資源)>同步方法(在方法體之外)
十三、執行緒的通訊問題
13.1 初步程式碼
讓執行緒執行一次之後,阻塞,把cpu資源給其他執行緒。
/**
* 使用兩個執行緒列印 1-100。執行緒1, 執行緒2 交替列印
*/
public class Test {
public static void main(String[] args) {
Test1 test1 = new Test1();
Thread thread1 = new Thread(test1);
Thread thread2 = new Thread(test1);
thread1.setName("執行緒一:");
thread2.setName("執行緒二:");
thread1.start();
thread2.start();
}
}
class Test1 implements Runnable{
private int j = 1;
@Override
public void run() {
while (true) {
synchronized (this) {// this的使用,指向呼叫該方法的物件的引用test1。在該處做為鎖
if (j <= 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":列印"+j);
j++;
try {
/*第一個執行緒列印一個數字之後,呼叫wait方法便阻塞了,同時釋放了鎖。
第二個執行緒獲得鎖之後,列印一個數字之後,呼叫wait方法也阻塞了,同時釋放了鎖。
此時這兩個執行緒都被阻塞了,因此只列印了2次
*/
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
結果:
總結wait方法:
從執行結果我們不難推斷出,wait方法至少有2個作用或者功能:
- 阻塞當前執行緒
- 釋放鎖【不釋放鎖的的話,第二個執行緒怎麼進得來】
13.2 最終程式碼
/**
* 使用兩個執行緒列印 1-100。執行緒1, 執行緒2 交替列印
*/
public class Test {
public static void main(String[] args) {
Test1 test1 = new Test1();
Thread thread1 = new Thread(test1);
Thread thread2 = new Thread(test1);
thread1.setName("執行緒一");
thread2.setName("執行緒二");
thread1.start();
thread2.start();
}
}
class Test1 implements Runnable{
private int j = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
/*程式開始:假設此時第一個執行緒拿著鎖(this)進來了,第二個執行緒在外面等待著。
第一個執行緒執行notify()方法沒有什麼影響,接著進入判斷,列印了“執行緒一:列印1”後,呼叫wait()方法後阻塞,
同時釋放鎖(this)【這步操作很重要】。此時第二個執行緒拿到了鎖進來了,執行notify()方法,喚醒第一個執行緒
【注意此時此刻,第二個執行緒還拿著鎖,所以第一個執行緒被喚醒之後,也只能在外面等待】,接著第二個執行緒進入判斷,
列印“執行緒二:列印2”後,呼叫wait()方法後阻塞,同時釋放鎖。此時此刻第一個執行緒又拿到了鎖,接著執行。
【後面就是重複的了,第一個執行緒和第二個執行緒之間,不停的被阻塞、又不停的被喚醒,直到迴圈結束】
*
* */
//第二步
notify();
if (j <= 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":列印"+j);
j++;
try {
/*第一個執行緒列印一個數字之後,呼叫wait方法便阻塞了,同時釋放了鎖。
第二個執行緒獲得鎖之後,列印一個數字之後,呼叫wait方法也阻塞了,同時釋放了鎖。
此時這兩個執行緒都被阻塞了,因此只列印了2次
*/
//第一步
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
結果:實現了交替列印
總結 notify():
- 喚醒一個執行緒【因為還有notifyAll()方法】
13.3 執行緒通訊總結
執行緒通訊涉及到的三個方法:
- wait():一旦執行此方法,當前執行緒就進入阻塞狀態,並釋放同步監視器
- notify():一旦執行此方法,就會喚醒被wait方法的一個執行緒,如果有多個執行緒被wait,就喚醒優先順序高的那個,如果優先順序相同,就隨機喚醒一個
- notifyAll():一旦執行此方法,就會喚醒被wait方法的所有執行緒,
注意:
-
wait(),notify(),notifyAll()三個方法必須使用在同步程式碼塊或同步方法中。【lock中不能使用】
-
wait(),notify(),notifyAll()三個方法的呼叫者必須是同步程式碼塊或同步方法中的同步監視器。
否則會出現IllegalMonitorStateException異常
-
wait(),notify(),notifyAll()三個方法是定義在java.lang.object類中。
為什麼是定義在Object類中?
- 因為你要保證“2.wait(),notify(),notifyAll()三個方法的呼叫者必須是同步程式碼塊或同步方法中的同步監視器。”
- 我們說過總結過任何一個物件都可以充當同步監視器,所有的物件都可以呼叫這三個方法,那麼這三個方法必然是是定義在object類中的,因為所有的類都繼承Object類。【注意Java中是單繼承,但是可以多層繼承。即如:Object---Parent---Child】