一個多線程的程序如果是通過Runnable接口實現的,則意味着類中的屬性將被多個線程享用,那麼就造成一個問題,如果這個多線程要同時操作同一資源時就有可能出現資源的同步問題。例如前面的賣票程序,多個線程同時執行時就會把票數賣爲負數(線程的實現)。
- 問題的引出
現在通過Runnable接口來實現多線程,共產生3個線程對象,同時賣出5張票。
【觀察程序的問題】
class MyThread implements Runnable
{
private int ticket=5;
public void run(){
for (int i=0;i<10 ;i++ )
{
if (ticket>0)
{
try
{
Thread.sleep(300);
}
catch (Exception e)
{
}
System.out.println(Thread.currentThread().getName()+"線程,賣票:ticket="+ticket--);
}
}
}
}
public class SyncDemo01
{
public static void main(String args[])
{
MyThread my=new MyThread();
new Thread(my).start();
new Thread(my).start();
new Thread(my).start();
}
}
運行結果可能出現:
Thread-2線程,賣票:ticket=5
Thread-0線程,賣票:ticket=4
Thread-1線程,賣票:ticket=4
Thread-1線程,賣票:ticket=3
Thread-2線程,賣票:ticket=2
Thread-0線程,賣票:ticket=3
Thread-2線程,賣票:ticket=0
Thread-1線程,賣票:ticket=1
Thread-0線程,賣票:ticket=-1
票數出現負數。
從程序中可以看出在程序中加入了延遲操作,所以在運行的最後出現了負數情況,那麼爲什麼會這種問題吶?
從上面的操作代碼中可以發現對於票數的操作步驟如下:
(1)判斷票數是否大於0,大於0則表示還有票可以賣
(2)如果票數大於0,則將票賣出
但是,在上面的操作代碼中,在步驟之間加入了延遲操作,那麼就有可能在一個線程進行休眠時,另一個線程進行了操作,之後線程又進行了減減操作,導致出現負數的情況。
- 使用同步解決問題
解決資源共享的同步操作,可以使用同步代碼塊和同步方法兩種方式完成。
- 同步代碼塊
在前面講到代碼塊的概念代碼塊所謂的代碼塊就是指使用“{}”括起來的一段代碼,根據使用位置和聲明的不同可以分爲普通代碼塊、構造塊,靜態塊3種,如果在代碼塊前加上synchronized關鍵字,則此代碼塊就稱爲同步代碼塊。同步代碼塊使用:
【使用同步代碼塊解決同步問題】
- 同步代碼塊
class MyThread implements Runnable
{
private int ticket=5;
public void run(){
for (int i=0;i<10 ;i++ )
{
synchronized(this){
if (ticket>0)
{
try
{
Thread.sleep(300);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"賣票:ticket="+ticket--);
}
}
}
}
};
public class SyncDemo02
{
public static void main(String args[])
{
MyThread my=new MyThread();
new Thread(my,"A").start();
new Thread(my).start();
new Thread(my).start();
}
}
運行結果:
A賣票:ticket=5
A賣票:ticket=4
Thread-1賣票:ticket=3
Thread-0賣票:ticket=2
Thread-0賣票:ticket=1
從程序運行中可以看出,以上代碼將取值和修改的操作進行了同步,所以不會再出現賣出負數的情況了。
2. 同步方法
除了可以將需要的代碼設置成同步代碼塊外,還可以使用synchronized關鍵字將一個方法聲明爲同步方法。
【使用同步方法解決上面問題】
class MyThread implements Runnable
{
private int ticket=5;
public void run(){
for (int i=0;i<10 ;i++ )
{
this.size();
}
}
public synchronized void size(){
if (ticket>0)
{
try
{
Thread.sleep(300);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"賣票:ticket="+ticket--);
}
}
};
public class SyncDemo02
{
public static void main(String args[])
{
MyThread my=new MyThread();
new Thread(my,"A").start();
new Thread(my).start();
new Thread(my).start();
}
}
運行結果:
A賣票:ticket=5
A賣票:ticket=4
A賣票:ticket=3
Thread-1賣票:ticket=2
Thread-1賣票:ticket=1
從程序的運行結果可以發現,此代碼完成了和之前同步代碼塊同樣的功能。
學習完了synchronized關鍵字之後,既可以給出java中方法定義的完整格式:
訪問權限 {public|default|protected|private} [final][static][synchronized]
返回值類型|void 方法名稱(參數類型 參數名稱,....)[throws Exception1,Exception2 ]{
[return [返回值|返回調用處]]
}
- 死鎖
同步可以保證資源共享操作的正確性,但是過多的同步會產生死鎖問題。所謂的死鎖問題就是指在兩個線程都在等待對方先完成,造成了程序的停滯,一般程序的死鎖都是在程序運行時出現的。下面觀察一個死鎖的範例
class Zhangsan{ // 定義張三類
public void say(){
System.out.println("張三對李四說:“你給我畫,我就把書給你。”") ;
}
public void get(){
System.out.println("張三得到畫了。") ;
}
};
class Lisi{ // 定義李四類
public void say(){
System.out.println("李四對張三說:“你給我書,我就把畫給你”") ;
}
public void get(){
System.out.println("李四得到書了。") ;
}
};
public class ThreadDeadLock implements Runnable{
private static Zhangsan zs = new Zhangsan() ; // 實例化static型對象
private static Lisi ls = new Lisi() ; // 實例化static型對象
private boolean flag = false ; // 聲明標誌位,判斷那個先說話
public void run(){ // 覆寫run()方法
if(flag){
synchronized(zs){ // 同步張三
zs.say() ;
try{
Thread.sleep(500) ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
synchronized(ls){
zs.get() ;
}
}
}else{
synchronized(ls){
ls.say() ;
try{
Thread.sleep(500) ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
synchronized(zs){
ls.get() ;
}
}
}
}
public static void main(String args[]){
ThreadDeadLock t1 = new ThreadDeadLock() ; // 控制張三
ThreadDeadLock t2 = new ThreadDeadLock() ; // 控制李四
t1.flag = true ;
t2.flag = false ;
Thread thA = new Thread(t1) ;
Thread thB = new Thread(t2) ;
thA.start() ;
thB.start() ;
}
};
運行結果:
李四對張三說:“你給我書,我就把畫給你”
張三對李四說:“你給我畫,我就把書給你。”
[一下代碼不再執行,程序進入死鎖狀態]
從程序的運行結果來看,兩個線程都在彼此等待對方的執行完成,這樣,程序就無法在進行下去了,從而造成了死鎖的現象。
注意的是,多線程共享同一個資源時需要進行同步,以保證資源操作的完整性,但是過多的同步可能會造成死鎖。