Java多線程(二) 四、線程的同步 4.1 線程同步的引入: 多線程出現了安全問題。 問題的原因: 當多條語句在操作同一個線程共用數據時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行。導致共用數據的錯誤。例如:買票問題、銀行卡消費問題等等。 解決辦法: 對多條操作共用數據 ...
Java多線程(二)
目錄四、線程的同步
4.1 線程同步的引入:
- 多線程出現了安全問題。
- 問題的原因: 當多條語句在操作同一個線程共用數據時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行。導致共用數據的錯誤。例如:買票問題、銀行卡消費問題等等。
- 解決辦法: 對多條操作共用數據的語句,只能讓一個線程都執行完,在執行過程中,其他線程不可以參與執行。
所以,Java 對於多線程的安全問題提供了專業的解決方式:同步機制。
4.2 線程同步的方式之一:同步代碼塊
- 語法格式:
synchronized (對象/同步監視器){ //得到對象的鎖,才能操作同步代碼
// 需要被同步的代碼
}
說明:
(1)操作共用數據的代碼,即為需要被同步的代碼。 --> 不能包含代碼多了,也不能包含代碼少了。
(2)共用數據:多個線程共同操作的變數。
(3)同步監視器,俗稱:鎖。任何一個類的對象,都可以充當鎖。
(4)要求:多個線程必須要共用同一把鎖。
- 使用同步代碼塊解決在實現 Runnable 介面的方式創建多線程的線程安全問題
// 例子:創建三個視窗賣票,總票數為100張.使用實現Runnable介面的方式
class Window1 implements Runnable{
private int ticket = 100;
// Object obj = new Object();
@Override
public void run() {
// Object obj = new Object();
while(true){
// 同步代碼塊---begin
synchronized (this){ // 此時的this:唯一的 Window1 的對象w //方式二:synchronized (object) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
ticket--;
} else {
break;
}
}
// 同步代碼塊---end
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("視窗1");
t2.setName("視窗2");
t3.setName("視窗3");
t1.start();
t2.start();
t3.start();
}
}
- 使用同步代碼塊解決繼承 Thread 類的方式創建多線程的線程安全問題
class Window2 extends Thread{
private static int ticket = 100;
// private static Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (Window2.class){// Window2.class表示window2這一個類,只會載入一次
// 方式二:synchronized (obj){
// synchronized (this){ 錯誤的方式:this分別代表著t1,t2,t3三個對象
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":賣票,票號為:" + ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 t1 = new Window2();
Window2 t2 = new Window2();
Window2 t3 = new Window2();
t1.setName("視窗1");
t2.setName("視窗2");
t3.setName("視窗3");
t1.start();
t2.start();
t3.start();
}
}
4.3 線程同步的方式之二:同步方法
-
同步方法:即將操作共用數據的代碼完整的聲明在一個方法中,將該方法聲明為同步方法。
-
語法格式:
// 將synchronized放在方法聲明中,一般放在許可權符和返回類型之間,表示整個方法為同步方法
public synchronized void 方法名 (String name){
// 需要被同步的代碼
}
說明:
(1)同步方法仍然涉及到同步監視器,只是不需要我們顯式的聲明。
(2)非靜態的同步方法,同步監視器是:this。
(3)靜態的同步方法,同步監視器是:當前類本身。
- 使用同步方法解決在實現 Runnable 介面的方式創建多線程的線程安全問題
class Window3 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private synchronized void show(){ //同步監視器:this
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w = new Window3();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("視窗1");
t2.setName("視窗2");
t3.setName("視窗3");
t1.start();
t2.start();
t3.start();
}
}
- 使用同步方法解決繼承 Thread 類的方式創建多線程的線程安全問題
class Window4 extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private static synchronized void show(){ //同步監視器:Window4.class
//private synchronized void show(){ //同步監視器:t1,t2,t3。此種解決方式是錯誤的
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 t1 = new Window4();
Window4 t2 = new Window4();
Window4 t3 = new Window4();
t1.setName("視窗1");
t2.setName("視窗2");
t3.setName("視窗3");
t1.start();
t2.start();
t3.start();
}
}
總結:
通過上面四個售票例子可以看出:
在實現 Runnable 介面創建多線程的方式中,我們可以考慮使用 this 充當同步監視器。
而在繼承Thread類創建多線程的方式一般不使用 this 充當同步監視器,因為每個線程的 this 為該線程的實例對象,不滿足多個線程共用一把鎖,所以一般考慮用當前類本身充當。
4.4 同步的優勢與局限:
-
優勢:解決了線程的安全問題。
-
局限: 操作同步代碼時,只能有一個線程參與,其他線程等待。相當於是一個單線程的過程,效率低。也可能會造成死鎖問題。
4.5 線程安全的單例模式之懶漢式
public class BankTest {
}
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
//方式一:效率稍差
// synchronized (Bank.class) {
// if(instance == null){
// instance = new Bank();
// }
// return instance;
// }
//方式二:效率更高
if(instance == null){
synchronized (Bank.class) {
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
/*
方式二效率更高的原因:
不需要每個線程都要進去同步方法裡面去,可能只有最前面幾個進程進入同步方法。
而方式一是所有線程都要進入同步方法導致效率較低。
*/
4.6 同步鎖機制:
-
同步機制中的鎖在《Thinking in Java》中,是這麼說的:對於併發工作,你需要某種方式來防止兩個任務訪問相同的資源(其實就是共用資源競爭)。防止這種衝突的方法就是當資源被一個任務使用時,在其上加鎖。第一個訪問某項資源的任務必須鎖定這項資源,使其他任務在其被解鎖之前,就無法訪問它了,而在其被解鎖之時,另一個任務就可以鎖定並使用它了。
-
synchronized的鎖是什麼?
-
任意對象都可以作為同步鎖。所有對象都自動含有單一的鎖(監視器)。
-
同步方法的鎖:靜態方法(類名.class)、非靜態方法(this)。
-
同步代碼塊:可以自己指定,很多時候也是指定為this或類名.class。
-
-
註意:
- 必須確保使用同一個資源的多個線程共用一把鎖,否則就無法保證共用資源的安全 。
- 一個線程類中的所有靜態方法共用同一把鎖(類名.class),所有非靜態方法共用同一把鎖(this),同步代碼塊(指定需謹慎)。
4.7 釋放鎖的操作:
- 當前線程的同步方法、同步代碼塊執行結束。
- 當前線程在同步代碼塊、同步方法中遇到 break、return 終止了該代碼塊、該方法的繼續執行。
- 當前線程在同步代碼塊、同步方法中出現了未處理的 Error 或 Exception,導致異常結束。
- 當前線程在同步代碼塊、同步方法中執行了線程對象的 wait() 方法,當前線程暫停,並釋放鎖。
4.8 不會釋放鎖的操作:
-
線程執行同步代碼塊或同步方法時,程式調用 Thread.sleep()、 Thread.yield() 方法暫停當前線程的執行。
-
線程執行同步代碼塊時,其他線程調用了該線程的 suspend() 方法將該線程掛起,該線程不會釋放鎖(同步監視器)。
應儘量避免使用suspend()和resume()來控制線程
4.9 線程的死鎖問題
-
死鎖:
- 不同的線程分別占用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖。
- 出現死鎖後,不會出現異常,不會出現提示,只是所有的線程都處於阻塞狀態,無法繼續。
-
解決方法:
- 專門的演算法、原則。
- 儘量減少同步資源的定義。
- 儘量避免嵌套同步。
-
例子:
//死鎖的演示
class A {
public synchronized void foo(B b) { //同步監視器:A類的對象:a
System.out.println("當前線程名: " + Thread.currentThread().getName()
+ " 進入了A實例的foo方法"); // ①
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("當前線程名: " + Thread.currentThread().getName()
+ " 企圖調用B實例的last方法"); // ③
b.last();
}
public synchronized void last() {//同步監視器:A類的對象:a
System.out.println("進入了A類的last方法內部");
}
}
class B {
public synchronized void bar(A a) {//同步監視器:b
System.out.println("當前線程名: " + Thread.currentThread().getName()
+ " 進入了B實例的bar方法"); // ②
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("當前線程名: " + Thread.currentThread().getName()
+ " 企圖調用A實例的last方法"); // ④
a.last();
}
public synchronized void last() {//同步監視器:b
System.out.println("進入了B類的last方法內部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主線程");
// 調用a對象的foo方法
a.foo(b);
System.out.println("進入了主線程之後");
}
public void run() {
Thread.currentThread().setName("副線程");
// 調用b對象的bar方法
b.bar(a);
System.out.println("進入了副線程之後");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
-
註意:
不是程式運行成功就說明程式裡面沒有死鎖問題,很多時候出現死鎖是一個概率問題。在開發中應儘量避免死鎖情況。
4.10 線程同步的方式之三:Lock鎖
-
從 JDK 5.0 開始,Java提供了更強大的線程同步機制——通過顯式定義同步鎖對象來實現同步。同步鎖使用 Lock 對象充當。
-
java.util.concurrent.locks.Lock 介面是控制多個線程對共用資源進行訪問的工具。鎖提供了對共用資源的獨占訪問,每次只能有一個線程對 Lock 對象加鎖,線程開始訪問共用資源之前應先獲得 Lock 對象。
-
ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的併發性和 記憶體語義,在實現線程安全的控制中,比較常用的是ReentrantLock,可以 顯式加鎖、釋放鎖。
-
語法:
class A{
//1.實例化ReentrantLock
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
//2.調用鎖定方法lock()
lock.lock();
try{
//保證線程安全的代碼;
}
finally{
//3.調用解鎖方法:unlock()
lock.unlock();
}
}
}
-
(面試題)synchronized 與 Lock 的對比
- 相同:二者都可以解決線程安全問題。
- 不同:
- Lock 是顯式鎖(手動開啟和關閉鎖),synchronized 是隱式鎖,出了作用域自動釋放。
- Lock 只有代碼塊鎖,synchronized 有代碼塊鎖和方法鎖。
- 使用 Lock 鎖,JVM 將花費較少的時間來調度線程,性能更好。並且具有更好的擴展性(提供更多的子類)。
-
優先使用順序:
Lock —— 同步代碼塊(已經進入了方法體,分配了相應資源) —— 同步方法(在方法體之外)
4.11 (簡單介紹)公平鎖和非公平鎖
-
公平鎖(Fair):加鎖前檢查是否有排隊等待的線程,優先排隊等待的線程,先來先得。
非公平鎖(Nonfair):加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到自動到隊尾等待。
-
Java中的 ReentrantLock 預設的 lock() 方法採用的是非公平鎖。
// 源碼:
// ReentrantLock當中的lock()方法,是通過static內部類sync來進行鎖操作
public void lock()
{
sync.lock();
}
-------------------------------------------------------------------
//定義成final型的成員變數,在構造方法中進行初始化
private final Sync sync;
//無參數預設非公平鎖
public ReentrantLock()
{
sync = new NonfairSync();
}
//根據參數初始化為公平鎖或者非公平鎖
public ReentrantLock(boolean fair)
{
sync = fair ? new FairSync() : new NonfairSync();
}