Java中的多執行緒(2)
概述:
上一篇文章簡單的介紹了什麼是執行緒,以及執行緒的生命週期,還有建立執行緒的三種方式。接著本篇文章將總結有關執行緒同步的相關知識,主要講解使用synchronized實現執行緒同步。然後總結Java中鎖機制,明確什麼是物件鎖,什麼是類鎖。然後下篇文章講解關於Lock的使用以及與synchronized的對比。
一、執行緒同步及synchronized關鍵字
1、在瞭解synchronized關鍵字實現執行緒同步之前我們先看一個程式,明確為什麼要實現執行緒同步。下面我們模擬售票系統實現四個售票點發售某日某列車的100張車票。
(1)通過繼承Thread類的方式:
package com.jwang.thread; /** * @author jwang * */ public class ThreadTestOne { public static void main(String[] args) { new MyThirdThread().start(); new MyThirdThread().start(); new MyThirdThread().start(); new MyThirdThread().start(); } } class MyThirdThread extends Thread { private int tickets=100; public void run() { while(tickets>0) { if(tickets>0) { System.out.println(Thread.currentThread().getName()+"is saling ticket"+tickets--); } } } }
執行結果如下所示:
分析:上面的程式當中我們建立了4個執行緒物件,實現了四個執行緒同步執行的效果,但是從程式的執行結果來看,四個執行緒各從100-1列印車票,也就是說同一張車票被列印了四次,即四個執行緒各自賣各自的100張票,而不是去賣共同的100張票,我們需要的是多執行緒處理同一個資源,,在上面的程式當中,我們建立了四個Thread物件,就等於建立了四個資源,每個Thread物件中都有100張票,每個執行緒獨立的處理自己的資源。這沒有達到我們的要求。
(2)通過實現Runnable介面的方式
package com.jwang.thread; /** * @author jwang * */ public class ThreadTestTwo { public static void main(String[] args) { MyFourThread tt=new MyFourThread(); new Thread(tt).start(); new Thread(tt).start(); new Thread(tt).start(); new Thread(tt).start(); } } class MyFourThread implements Runnable { private int tickets=100; public void run() { while(tickets > 0) { if(tickets>0) { System.out.println(Thread.currentThread().getName()+"is saling ticket"+tickets--); } } } }
執行結果如下所示:
分析:從執行的結果我們發現,這種方式實現了四個執行緒處理同一個資源物件。但是這幾個執行緒列印的票的順序是亂的,為什麼會發生這種狀況,我們再進一步分析。相對於上面的賣票問題而言,四個執行緒交替執行,cpu在四個執行緒之間不斷切換,而且切換的時間沒有規律可循,隨時都可能發生。這就會碰到一種意外。
if(tickets>0) { System.out.println(Thread.currentThread().getName()+"is saling ticket"+tickets--); }
假設tickets的值為1的時候,執行緒1剛執行完if(tickets>0)這行程式碼,正準備執行下面的程式碼,就在這時,作業系統將cou切換到了執行緒2上執行,此時tickets的值仍為1,執行緒2執行完上面的兩行程式碼,tickets的值為0後,cpu又切換到了執行緒1上執行,執行緒1不會再執行if(tickets>0)程式碼,因為先前已經比較過了,並且比較結果為真,執行緒1將繼續執行列印程式碼,此時打印出來的tickets的值已經為0了。即判斷的程式碼執行了一次,執行減的程式碼卻執行了兩次。也就是說while迴圈裡面的東西在同一時間只能允許一個執行緒執行。即具有原子性。Java語言中為了為了保證這種原子性提供了synchronized關鍵字。下面我們具體來看如何使用synchronized關鍵字實現執行緒同步。
使用同步程式碼塊實現執行緒同步:
package com.jwang.thread;
public class SyncTest
{
public static void main(String[] args)
{
SyncRunnable syncRunnable = new SyncRunnable();
Thread t1 = new Thread(syncRunnable, "thread1");
Thread t2 = new Thread(syncRunnable, "thread2");
Thread t3 = new Thread(syncRunnable, "thread3");
Thread t4 = new Thread(syncRunnable, "thread4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class SyncRunnable implements Runnable
{
private int tickets = 100;
@Override
public void run()
{
while (tickets > 0)
{
synchronized (this)
{
if (tickets > 0)
{
System.out.println(Thread.currentThread().getName() + " is saling ticket:" + tickets--);
}
}
}
}
}
執行結果部分截圖如下所示:
分析:我們發現四個執行緒交替執行按照100-1的順序打印出了100張車票。所謂的同步程式碼塊就是將具有原子性的程式碼放在下面的程式碼中:
//Object為任意物件,作為監視器
synchronized (Object)
{
}
同一時間內只有一個執行緒可以執行該程式碼塊中的程式碼,Object可以是任意物件。上面的示例中我們以this作為監視器,可以理解成這個物件就是一把鎖,一個執行緒獲得該鎖後才可以執行同步程式碼塊中的程式碼,與此同時其它執行緒只能等待該執行緒執行完畢釋放鎖後才可能獲得該鎖被cpu排程執行。
使用同步函式實現執行緒同步:
package com.jwang.thread;
/**
* 描述:使用同步函式實現執行緒同步
* @author jwang
*
*/
public class SyncTest
{
public static void main(String[] args)
{
SyncRunnable syncRunnable = new SyncRunnable();
Thread t1 = new Thread(syncRunnable, "thread1");
Thread t2 = new Thread(syncRunnable, "thread2");
Thread t3 = new Thread(syncRunnable, "thread3");
Thread t4 = new Thread(syncRunnable, "thread4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class SyncRunnable implements Runnable
{
private int tickets = 100;
@Override
public void run()
{
while (tickets > 0)
{
//呼叫同步函式
sale();
}
}
/**
* 同步方法
*/
public synchronized void sale()
{
if (tickets > 0)
{
System.out.println(Thread.currentThread().getName() + " is saling ticket:" + tickets--);
}
}
}
二、Java中的鎖機制
1、物件鎖:Java中每一個物件都可以作為實現執行緒同步的一個鎖,這是Java內建的特性,因此我們可以簡單的理解成Java中的每個物件都有鎖標誌,稱之為物件鎖,也即內建鎖。也就是說Java中的每個物件都可以當成一把鎖,在需要使用鎖的地方發揮鎖的作用。而執行緒同步這種場景剛好可以利用所有Java物件都可以作為一把鎖的特性實現同步功能。一般我們在使用synchronized通過同步程式碼塊或者非靜態的同步函式來實現執行緒同步的時候使用的就是物件鎖。
2、類鎖:類鎖也是一個抽象的概念,我們知道靜態的同步函式呼叫時是不需要建立物件的,所以這個時候,同步所依賴的鎖就是類鎖。類鎖是用於類的靜態方法或者一個類的class物件上的。我們知道,類的物件例項可以有很多個,但是每個類只有一個class物件,所以不同物件例項的物件鎖是互不干擾的,但是每個類只有一個類鎖。有一點必須注意的是,其實類鎖只是一個概念上的東西,並不是真實存在的,它只是用來幫助我們理解鎖定例項方法和靜態方法的區別的。
3、可重入性:當執行緒執行A類的同步方法時,獲得了A類的一個物件鎖執行同步方法裡面的程式碼,而A的同步方法中又呼叫了B類的一個同步方法,這個時候執行緒在執行到B類的同步方法時,時不需要等待獲取B類的物件鎖的,可以直接執行B類同步方法裡面的程式碼,這就是內建鎖的可重入性。
下面通過一些示例來演示同步以及鎖相關的知識
JavaLock.java中分別通過四種方式來演示synchronized的用法
package com.jwang.thread;
public class JavaLock
{
private int tickets = 10;
// 定義一個包含synchronized同步程式碼塊的函式,使用物件鎖
public void testMethod1()
{
while (tickets > 0)
{
// this為當前物件,該同步程式碼塊使用物件鎖
synchronized (this)
{
if (tickets > 0)
{
System.out.println(Thread.currentThread().getName() + " is saling ticket:" + tickets--);
}
}
}
}
public void testMethod2()
{
while (this.tickets > 0)
{
testMethod2(this);
}
}
// 定義一個用synchronized修飾的普通的同步函式,該同步函式使用物件鎖
public synchronized void testMethod2(JavaLock javalock)
{
if (javalock.tickets > 0)
{
System.out.println(Thread.currentThread().getName() + " is saling ticket:" + javalock.tickets--);
}
}
// 定義一個包含synchronized同步程式碼塊的函式,使用類鎖
public void testMethod3()
{
while (tickets > 0)
{
// this為當前物件,該同步程式碼塊使用物件鎖
synchronized (JavaLock.class)
{
if (tickets > 0)
{
System.out.println(Thread.currentThread().getName() + " is saling ticket:" + tickets--);
}
}
}
}
public static void testMethod4()
{
JavaLock jl = new JavaLock();
while (jl.tickets > 0)
{
testMethod4(jl);
}
}
// 定義一個靜態的用synchronized修飾的同步函式
public static synchronized void testMethod4(JavaLock javaLock)
{
if (javaLock.tickets > 0)
{
System.out.println(Thread.currentThread().getName() + " is saling ticket:" + javaLock.tickets--);
}
}
}
JavaLockTest.java通過不同的方式測試以上各個同步方式
package com.jwang.thread;
public class JavaLockTest
{
//定義一個成員變數,作為test4方法中執行緒的共享資源
private int i = 50;
public static void main(String[] args)
{
JavaLockTest jlt = new JavaLockTest();
//引數為true表示呼叫以物件鎖作為監視器的同步程式碼塊
jlt.test1(true);
//引數為false表示呼叫以class作為類鎖的監視器的同步程式碼塊
jlt.test1(false);
//引數為true表示呼叫以類鎖為監視器的靜態同步函式
jlt.test2(true);
//引數為false表示呼叫以物件鎖為監視器的普通同步函式
jlt.test2(false);
//分別用執行緒呼叫普通同步函式和靜態同步
jlt.test3();
//A類的同步函式呼叫B類的同步函式演示鎖的可重入性
jlt.test4();
}
/**
* 該方法測試使用物件鎖或者類鎖實現兩個執行緒同時處理統一資源的情況
* @param isClassLock
*/
public void test1(final boolean isClassLock)
{
final JavaLock javaLock = new JavaLock();
Runnable runnable = new Runnable()
{
@Override
public void run()
{
if (!isClassLock)
{
javaLock.testMethod1();
}
else
{
javaLock.testMethod3();
}
}
};
Thread thread1 = new Thread(runnable, "thread1");
Thread thread2 = new Thread(runnable, "thread2");
thread1.start();
thread2.start();
}
/**
*
* @param isClassLock
*/
public void test2(final boolean isClassLock)
{
final JavaLock javaLock = new JavaLock();
Runnable runnable = new Runnable()
{
@Override
public void run()
{
if (!isClassLock)
{
javaLock.testMethod2();
}
else
{
JavaLock.testMethod4();
}
}
};
Thread thread1 = new Thread(runnable, "thread3");
Thread thread2 = new Thread(runnable, "thread4");
thread1.start();
thread2.start();
}
//
public void test3()
{
final JavaLock javaLock = new JavaLock();
// 建立一個執行緒使用物件鎖
new Thread(new Runnable()
{
@Override
public void run()
{
javaLock.testMethod2();
}
}, "thread5").start();
// 建立一個執行緒使用類鎖
new Thread(new Runnable()
{
@Override
public void run()
{
JavaLock.testMethod4();
}
}, "thread6").start();
}
/**
* 該方法測試鎖的可重入性
*/
public void test4()
{
final JavaLockTest jtl = new JavaLockTest();
final JavaLock javaLock = new JavaLock();
Runnable runnable = new Runnable()
{
@Override
public void run()
{
while(jtl.i > 0)
{
test5(javaLock,jtl);
}
}
};
Thread thread1 = new Thread(runnable, "thread7");
Thread thread2 = new Thread(runnable, "thread8");
thread1.start();
thread2.start();
}
/**
* 該A類的同步函式呼叫B類的同步函式
* @param javaLock
* @param run
*/
public synchronized void test5(JavaLock javaLock, JavaLockTest run)
{
System.out.println(Thread.currentThread().getName()+"------------------------"+run.i--);
if(run.i == 30)
{
javaLock.testMethod2();
}
}
}
執行結果分析:
(1)從第一個測試方法的結果中我們發現,不管是使用物件鎖,還是類鎖都實現了兩個執行緒處理同一個資源的情況。控制檯上兩個執行緒交替著輸出10-1。
(2)從第二個測試方法中我們發現當兩個執行緒都使用同一物件鎖時,呼叫普通的同步函式,也輕鬆的實現了兩個執行緒交替處理同一個資源的情況,控制檯上兩個執行緒交替著輸出10-1
(3)當第二個測試方法中我們使用類鎖時,也即執行緒中呼叫靜態同步函式時,我們發現並不能實現兩個線處理同一個資源的情況,兩個執行緒各自輸出10-1。這是因為靜態函式中的變數每個執行緒都會建立一份,所以不會有多個執行緒處理統一資源的這種情況。
(4)第三個測試方法中,我們分別建立了兩個執行緒,一個使用普通的同步函式,一個使用靜態的同步函式。執行的結果就是兩個執行緒分別交替著處理各自的資源,分別交替輸出10-1。
(5)第四個方法中我們在JavaLockTest中的同步函式中呼叫了JavaLock的同步函式,發現執行緒在進入第一個同步函式後進入第二個同步函式時不需要重新獲取鎖,這便是可重入性,下邊是該測試結果的執行結果