java多線程

理解程序啦租、進程、線程的概念
程序可以理解為靜態(tài)的代碼
進程可以理解為執(zhí)行中的程序

線程可以理解為進程的進一步細分日丹,程序的一條執(zhí)行路徑

使用多線程的優(yōu)點:

提高應用程序的響應鲤桥。對圖形化界面更有意義,可增強用戶體驗拢操。
提高計算機系統(tǒng)CPU的利用率
改善程序結(jié)構(gòu)枉圃。將既長又復雜的進程分為多個線程,獨立運行庐冯,利于理解和修改

在java中要想實現(xiàn)多線程孽亲,有兩種手段,一種是繼續(xù)Thread類展父,另外一種是實現(xiàn)Runable接口

繼承java.lang.Thread類
下面來看一個簡單的實例:

class Thread1 extends Thread{
    private String name;
    public Thread1(String name) {
       this.name=name;
    }
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "運行  :  " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
       
    }
}
public class Main {

    public static void main(String[] args) {
        Thread1 mTh1=new Thread1("A");
        Thread1 mTh2=new Thread1("B");
        mTh1.start();
        mTh2.start();

    }

}

輸出

輸出:
A運行  :  0
B運行  :  0
A運行  :  1
A運行  :  2
A運行  :  3
A運行  :  4
B運行  :  1
B運行  :  2
B運行  :  3
B運行  :  4
再運行一下:
A運行  :  0
B運行  :  0
B運行  :  1
B運行  :  2
B運行  :  3
B運行  :  4
A運行  :  1
A運行  :  2
A運行  :  3
A運行  :  4

說明:
程序啟動運行main時候返劲,java虛擬機啟動一個進程,主線程main在main()調(diào)用時候被創(chuàng)建栖茉。隨著調(diào)用Thread1的兩個對象的start方法篮绿,另外兩個線程也啟動了,這樣吕漂,整個應用就在多線程下運行亲配。

以下是關系到線程運行狀態(tài)的幾個方法:

1)start方法

start()用來啟動一個線程,當調(diào)用start方法后惶凝,系統(tǒng)才會開啟一個新的線程來執(zhí)行用戶定義的子任務吼虎,在這個過程中,會為相應的線程分配需要的資源苍鲜。

2)run方法

run()方法是不需要用戶來調(diào)用的思灰,當通過start方法啟動一個線程之后,當線程獲得了CPU執(zhí)行時間混滔,便進入run方法體去執(zhí)行具體的任務洒疚。注意歹颓,繼承Thread類必須重寫run方法,在run方法中定義具體要執(zhí)行的任務油湖。

3)sleep方法

sleep相當于讓線程睡眠巍扛,交出CPU,讓CPU去執(zhí)行其他的任務乏德。
實現(xiàn)java.lang.Runnable接口
用Runnable也是非常常見的一種电湘,我們只需要重寫run方法即可。下面也來看個實例:

class Thread2 implements Runnable{
    private String name;

    public Thread2(String name) {
        this.name=name;
    }

    @Override
    public void run() {
          for (int i = 0; i < 5; i++) {
                System.out.println(name + "運行  :  " + i);
                try {
                    Thread.sleep((int) Math.random() * 10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        
    }
    
}
public class Main {

    public static void main(String[] args) {
        new Thread(new Thread2("C")).start();
        new Thread(new Thread2("D")).start();
    }

}

輸出

輸出:
C運行  :  0
D運行  :  0
D運行  :  1
C運行  :  1
D運行  :  2
C運行  :  2
D運行  :  3
C運行  :  3
D運行  :  4
C運行  :  4

說明:
Thread2類通過實現(xiàn)Runnable接口鹅经,使得該類有了多線程類的特征。run()方法是多線程程序的一個約定怎诫。所有的多線程代碼都在run方法里面瘾晃。Thread類實際上也是實現(xiàn)了Runnable接口的類。

在啟動的多線程的時候幻妓,需要先通過Thread類的構(gòu)造方法Thread(Runnable target) 構(gòu)造出對象蹦误,然后調(diào)用Thread對象的start()方法來運行多線程代碼。

實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的肉津。因此强胰,不管是擴展Thread類還是實現(xiàn)Runnable接口來實現(xiàn)多線程,最終還是通過Thread的對象的API來控制線程的妹沙,熟悉Thread類的API是進行多線程編程的基礎偶洋。

Thread和Runnable的區(qū)別
對比一下繼承的方式 vs 實現(xiàn)的方式

1.聯(lián)系:public class Thread implements Runnable(繼承的方式的Thread也實現(xiàn)了Runnable接口)
2.哪個方式好?
實現(xiàn)的方式優(yōu)于繼承的方式 why?
① 避免了java單繼承的局限性
② 如果多個線程要操作同一份資源(或數(shù)據(jù))距糖,更適合使用實現(xiàn)的方式

看一個例子:

//模擬火車站售票窗口玄窝,開啟三個窗口售票,總票數(shù)為100張
//存在線程的安全問題

class Window extends Thread {
    int ticket = 100;

    public void run() {
        while (true) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "售票悍引,票號為:"+ ticket--);
            } else {
                break;
            }
        }
    }
}

public class TestWindow {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }

}
class Window implements Runnable {
     int ticket = 100;//要將全局變量聲明為靜態(tài)恩脂,不然每個對象都有這個屬性,會賣出300張票

    public void run() {
        while (true) {
            if (ticket > 0) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "售票趣斤,票號為:"+ ticket--);
            } else {
                break;
            }
        }
    }
}

public class Main {

    //模擬火車站售票窗口俩块,開啟三個窗口售票,總票數(shù)為100張
//存在線程的安全問題


    public static void main(String[] args) {
        Window w1 = new Window();
        Thread t1 = new Thread(w1, "t1");
        Thread t2 = new Thread(w1, "t2");
        Thread t3 = new Thread(w1, "t3");


        t1.start();
        t2.start();
        t3.start();
    }

}

問題原因:
某個線程執(zhí)行完輸出ticket后浓领,還沒有來得及ticket--玉凯,CPU時間片被分配給了另外一個線程,導致同一個票號被輸出2次联贩。
另外一種情況壮啊,打印到ticket=1時,有2個線程同時進入到了條件里撑蒜,導致-1的票號被輸出毅否。
解決辦法旁涤,為關鍵代碼段笨鸡,加鎖,參見后文藤树。

中斷線程

當線程的run方法執(zhí)行方法體中最后一條語句后,并經(jīng)由return語句返回時拓萌,或者出現(xiàn)了在方法中過沒有捕獲的異常時岁钓,線程將被終止。

線程同步

根據(jù)各線程訪問數(shù)據(jù)的次序微王,可能會產(chǎn)生訛誤的對象屡限。這樣的一個情況稱為競爭條件(race condition)。

1)競爭條件的一個例子

銀行例程:多線程操作時炕倘,本應恒等的余額總值發(fā)生了變化钧大。

public class SynchBankTest
{
    public static final int NACCOUNTS = 100;
    public static final double INITIAL_BALANCE = 1000;

    public static void main(String[] args)
    {
        Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
        int i;
        for (i = 0; i < NACCOUNTS; i++)
        {
            TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
            Thread t = new Thread(r);
            t.start();
        }
    }
}

public class Bank
{
    private final double[] accounts;

    public Bank(int n, double initialBalance)
    {
        accounts = new double[n];
        for (int i = 0; i < accounts.length; i++)
            accounts[i] = initialBalance;
    }

    public void transfer(int from, int to, double amount) throws InterruptedException
    {
        if (accounts[from] >= amount) {
            System.out.print(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf(" %10.2f from %d to %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
        }
    }
    public double getTotalBalance()
    {
        double sum = 0;

        for (double a : accounts)
            sum += a;

        return sum;
    }

    public int size()
    {
        return accounts.length;
    }
}

public class TransferRunnable implements Runnable
{
    private Bank bank;
    private int fromAccount;
    private double maxAmount;
    private int DELAY = 10;

    public TransferRunnable(Bank b, int from, double max)
    {
        bank = b;
        fromAccount = from;
        maxAmount = max;
    }

    public void run()
    {
        try
        {
            while (true)
            {
                int toAccount = (int) (bank.size() * Math.random());
                double amount = maxAmount * Math.random();
                bank.transfer(fromAccount, toAccount, amount);
                Thread.sleep((int) (DELAY * Math.random()));
            }
        }
        catch (InterruptedException e)
        {
        }
    }
}

2)詳解競爭條件

假定兩個線程同時執(zhí)行指令
accounts[to] += amount;
這不是原子操作。該指令可能被處理如下:
1)將accounts[to]加載到寄存器
2)增加amount
3)將結(jié)果寫回accounts[to]
假定第一個線程執(zhí)行步驟1和2罩旋,然后啊央,它被剝奪了運行權。假定第二個線程被喚醒并修改了accounts數(shù)組中的同一項涨醋。然后瓜饥,第一個線程被喚醒并完成其第三步。這一動作擦去了第二個線程所做的更新浴骂。

線程同步

當使用多個線程來訪問同一個數(shù)據(jù)時乓土,非常容易出現(xiàn)線程安全問題(比如多個線程都在操作同一數(shù)據(jù)導致數(shù)據(jù)不一致),所以我們用同步機制來解決這些問題。

鎖和條件的關鍵之處:

鎖用來保護代碼片段溯警,任意時刻只能有一個線程執(zhí)行被保護的代碼帐我。
鎖可以管理試圖進入保護代碼片段的線程
鎖可以擁有一個或者多個相關的條件對象
每個條件對象管理那些已經(jīng)進入被保護的代碼段但還不能運行的線程。
鎖對象

在Java SE5.0引入ReentrantLock類愧膀。Lock是Java.util.concurrent.locks包下的接口拦键,Lock 實現(xiàn)提供了比使用synchronized 方法和語句可獲得的更廣泛的鎖定操作。

用ReentrantLock保護代碼塊的基本結(jié)構(gòu)如下:

myLock.lock(); //a ReentrantLock object  
try  
{  
    critical section  
}  
finally  
{  
    myLock.unlock();//確保代碼拋出異常鎖必須被釋放  
}  

這一結(jié)構(gòu)確保任何時刻只有一個線程進入臨界區(qū)檩淋。一旦一個線程封鎖了鎖對象芬为,其他任何線程都無法通過lock語句。當其他線程調(diào)用lock時蟀悦,他們被阻塞媚朦,直到第一個線程釋放鎖對象。把解鎖操作放在finally子句之內(nèi)是至關重要的日戈。如果在臨界區(qū)的代碼拋出異常询张,鎖必須釋放。否則浙炼,其他線程將永遠阻塞份氧。

public class Bank  
{  
   private Lock bankLock= new ReentrantLock();
     
   public void transfer(int from, int to, double amount)   
   {  
      bankLock.lock();  
      try  
      {  
         System.out.print(Thread.currentThread());  
         accounts[from] -= amount;  
         System.out.printf(" %10.2f from %d to %d", amount, from, to);  
         accounts[to] += amount;  
         System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());  
      }  
      finally  
      {  
         bankLock.unlock();  
      }  
   }  
      
    public double getTotalBalance()  
   {  
      bankLock.lock();  
      try  
      {  
         double sum = 0;  
  
         for (double a : accounts)  
            sum += a;  
  
         return sum;  
      }  
      finally  
      {  
         bankLock.unlock();  
      }  
   }  
  
}  

假定一個線程調(diào)用transfer唯袄,在執(zhí)行結(jié)束前被剝奪運行權。假定第二個線程也調(diào)用了transfer蜗帜,由于第二個線程并不能獲得鎖恋拷,將在調(diào)用lock方法時被阻塞。他必須等待第一個線程完成transfer方法之后才能再度被激活厅缺。當?shù)谝粋€線程釋放鎖時蔬顾,第二個線程才能開始運行。這樣余額就不會出錯了湘捎。
每個Bank對象有自己的ReentrantLock對象诀豁,如果兩個線程試圖訪問同一個Bank對象,那么鎖以串行方式提供服務窥妇。但是舷胜,如果兩個線程訪問不同的Bank對象,每個線程得到不同的鎖對象秩伞,兩個線程都不會發(fā)生阻塞。兩個線程在操作不同的Bank實例的時候欺矫,線程之間不會相互影響纱新。
鎖是可重入的,因為線程可以重復的獲得已經(jīng)持有的鎖穆趴。鎖保持一個持有計數(shù)來跟蹤對Lock方法的嵌套調(diào)用脸爱。線程每一次調(diào)用都用unlock來釋放鎖。由于這一特性未妹,被一個鎖保護的代碼可以調(diào)用另一個使用相同鎖的方法簿废。例如,transfer方法調(diào)用getTotalBalance方法络它,這也會封鎖bankLock對象族檬,此時bankLock對象的持有計數(shù)為2,當getTotalBalance方法退出時化戳,持有計數(shù)變?yōu)?单料,當transfer方法退出時,持有計數(shù)變?yōu)?点楼。線程釋放鎖扫尖。通常,可能想要保護需若干個操作來更新或者檢查共享對象的代碼塊掠廓。要確保這些操作完成后换怖,另一個線程才能使用相同的對象。
要留心臨界區(qū)的代碼蟀瞧,不要因為異常的拋出二跳出了臨界區(qū)沉颂。如果在臨界區(qū)代碼結(jié)束之前拋出了異常条摸,finally字句釋放鎖,但會使對象可能出于一種受損狀態(tài)兆览。

條件對象

通常屈溉,線程進入臨界區(qū),卻發(fā)現(xiàn)在某一條件滿足后才能執(zhí)行抬探。要使用一個條件對象來管理那些已經(jīng)獲得了一個鎖子巾,但是不能做有用工作的線程。(條件對象經(jīng)常被稱為條件變量)

分析上文中的銀行模擬程序小压,

if(bank.getBalance(from)>=amount)  
transfer(from, to, amount);  

如果當前程序通過if條件判斷线梗,且在調(diào)用transfer之前被中斷,在線程再次運行前怠益,賬戶余額可能已經(jīng)低于提款金額仪搔。必須確保沒有其他線程在本檢查余額與轉(zhuǎn)賬活動之間修改余額。通過使用鎖來保護檢查與轉(zhuǎn)賬動作來做到這一點:

public void transfer(int from, int to, double amount)   
   {  
      bankLock.lock();  
      try  
      {  
         while (accounts[from] < amount)  
         {   
             //wait()  
          }  
         //transfer funds  
       ........  
      finally  
      {  
         bankLock.unlock();  
      }  
   }  

現(xiàn)在蜻牢,當賬戶中沒有足夠的余額時烤咧,等待直到另一個線程向賬戶中注入資金。但是抢呆,這一線程剛剛獲得了bankLock的排他性訪問煮嫌,因此別的線程沒有進行存款操作的機會,這就是為什么需要用條件對象的原因抱虐。
一個鎖對象可以有一個或者多個相關的條件對象昌阿。可以用newCondition方法獲得一個條件對象恳邀。習慣的給每一個條件對象命名為可以反應它所表達條件的名字懦冰。如sufficientFunds = bankLock.newCondition();

如果transfer方法發(fā)現(xiàn)余額不足,它調(diào)用sufficientFunds.await();當前線程被他阻塞了谣沸,并放棄鎖刷钢。我們希望這樣可以等待另一個線程進行增加賬戶余額的操作。
等待獲得鎖的線程和調(diào)用await方法的線程在本質(zhì)上存在不同乳附。一旦一個線程調(diào)用await方法闯捎,他進入該條件的等待集。當該鎖可用時许溅,該線程不能馬上解除阻塞瓤鼻。相反,它處于阻塞狀態(tài)贤重,直到另一個線程調(diào)用同一條件上的signalAll方法時為止茬祷。
當另一個線程轉(zhuǎn)賬時,它應該調(diào)用sufficientFunds.signalAll();這一調(diào)用重新激活因為這一條件等待的所有線程并蝗。當這些線程從等待集當中移除時祭犯,他們再次成為可運行的秸妥,調(diào)度器將再次激活它們。同時沃粗,他們試圖重新進入該對象粥惧。一旦鎖成為可用,他們將從await調(diào)用返回最盅,獲得該鎖并從被阻塞的地方繼續(xù)執(zhí)行突雪。
此時,線程應該再次測試該條件涡贱。由于無法確保該條件被滿足咏删,signalAll方法僅僅通知正在等待的線程:此時有可能已經(jīng)滿足條件,值得再次去檢測條件问词。
最關重要的是最終需要某個其他線程調(diào)用signalAll方法督函。當一個線程調(diào)用await時,他沒有辦法自己激活自身激挪,它寄希望于其他線程辰狡。如果沒有其他線程重新來激活等待的線程,他就永遠不再運行垄分。導致死鎖宛篇。如果所有其他線程被阻塞,最后一個線程再解除其他阻塞線程之前就調(diào)用await锋喜,那么它也被阻塞些己。沒有線程解除其他阻塞線程豌鸡,那么該程序就掛起嘿般。
應該何時調(diào)用signalAll呢,在本例中涯冠,當一個賬戶的余額發(fā)生改變時炉奴,等待的線程就有機會檢查余額。調(diào)用signalAll不會立即激活一個等待線程蛇更。它僅僅解除等待線程的阻塞瞻赶,以便這些線程可以在當前線程同步推出后,通過競爭實現(xiàn)對對象的訪問派任。當一個線程擁有某個條件的鎖時砸逊,它僅僅可以在該條件上調(diào)用await,signalAll和signal方法掌逛。

以下為完整的例子:

synch/Bank.java

package synch;  

import java.util.concurrent.locks.*;  

public class Bank  
{  
 private final double[] accounts;  
 private Lock bankLock;  
 private Condition sufficientFunds;  

 public Bank(int n, double initialBalance)  
 {  
    accounts = new double[n];  
    for (int i = 0; i < accounts.length; i++)  
       accounts[i] = initialBalance;  
    bankLock = new ReentrantLock();  
    sufficientFunds = bankLock.newCondition();  
 }  

 public void transfer(int from, int to, double amount) throws InterruptedException  
 {  
    bankLock.lock();  
    try  
    {  
       while (accounts[from] < amount)  
          sufficientFunds.await();  
       System.out.print(Thread.currentThread());  
       accounts[from] -= amount;  
       System.out.printf(" %10.2f from %d to %d", amount, from, to);  
       accounts[to] += amount;  
       System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());  
       sufficientFunds.signalAll();  
    }  
    finally  
    {  
       bankLock.unlock();  
    }  
 }  
 public double getTotalBalance()  
 {  
    bankLock.lock();  
    try  
    {  
       double sum = 0;  

       for (double a : accounts)  
          sum += a;  

       return sum;  
    }  
    finally  
    {  
       bankLock.unlock();  
    }  
 }  

 public int size()  
 {  
    return accounts.length;  
 }  
}  

[java] view plain copy
synch/TransferRunnable.java  

package synch;  

public class TransferRunnable implements Runnable  
{  
 private Bank bank;  
 private int fromAccount;  
 private double maxAmount;  
 private int DELAY = 10;  

 public TransferRunnable(Bank b, int from, double max)  
 {  
    bank = b;  
    fromAccount = from;  
    maxAmount = max;  
 }  

 public void run()  
 {  
    try  
    {  
       while (true)  
       {  
          int toAccount = (int) (bank.size() * Math.random());  
          double amount = maxAmount * Math.random();  
          bank.transfer(fromAccount, toAccount, amount);  
          Thread.sleep((int) (DELAY * Math.random()));  
       }  
    }  
    catch (InterruptedException e)  
    {  
    }  
 }  
}  

synch/SynchBankTest

package synch;  

public class SynchBankTest  
{  
 public static final int NACCOUNTS = 100;  
 public static final double INITIAL_BALANCE = 1000;  

 public static void main(String[] args)  
 {  
    Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);  
    int i;  
    for (i = 0; i < NACCOUNTS; i++)  
    {  
       TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);  
       Thread t = new Thread(r);  
       t.start();  
    }  
 }  
}  

生產(chǎn)者消費者問題

import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


class Buffer {
    private  final Lock lock;
    private  final Condition notFull;
    private  final Condition notEmpty;
    private int maxSize;
    private List<Date> storage;
    Buffer(int size){
        //使用鎖lock师逸,并且創(chuàng)建兩個condition,相當于兩個阻塞隊列
        lock=new ReentrantLock();
        notFull=lock.newCondition();
        notEmpty=lock.newCondition();
        maxSize=size;
        storage=new LinkedList<>();
    }
    public void put()  {
        lock.lock();
        try {   
            while (storage.size() ==maxSize ){//如果隊列滿了
                System.out.print(Thread.currentThread().getName()+": wait \n");;
                notFull.await();//阻塞生產(chǎn)線程
            }
            storage.add(new Date());
            System.out.print(Thread.currentThread().getName()+": put:"+storage.size()+ "\n");
            Thread.sleep(1000);         
            notEmpty.signalAll();//喚醒消費線程
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{   
            lock.unlock();
        }
    }

    public  void take() {       
        lock.lock();
        try {  
            while (storage.size() ==0 ){//如果隊列滿了
                System.out.print(Thread.currentThread().getName()+": wait \n");;
                notEmpty.await();//阻塞消費線程
            }
            Date d=((LinkedList<Date>)storage).poll();
            System.out.print(Thread.currentThread().getName()+": take:"+storage.size()+ "\n");
            Thread.sleep(1000);         
            notFull.signalAll();//喚醒生產(chǎn)線程

        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    } 
}

class Producer implements Runnable{
    private Buffer buffer;
    Producer(Buffer b){
        buffer=b;
    }
    @Override
    public void run() {
        while(true){
            buffer.put();
        }
    }   
}
class Consumer implements Runnable{
    private Buffer buffer;
    Consumer(Buffer b){
        buffer=b;
    }
    @Override
    public void run() {
        while(true){
            buffer.take();
        }
    }   
}
public class Main{
    public static void main(String[] arg){
        Buffer buffer=new Buffer(10);
        Producer producer=new Producer(buffer);
        Consumer consumer=new Consumer(buffer);
        for(int i=0;i<3;i++){
            new Thread(producer,"producer-"+i).start();
        }
        for(int i=0;i<3;i++){
            new Thread(consumer,"consumer-"+i).start();
        }
    }
}

synchronized關鍵字

Lock和condition接口為程序設計人員提供了高度的鎖定控制豆混。大多數(shù)情況下并不需要那樣的控制篓像,并且可以使用一種嵌入到Java語言內(nèi)部的機制动知。從Java1.0版本開始,Java中的每個對象都有一個內(nèi)部鎖员辩。如果一個方法用synchronized關鍵字聲明盒粮,那么對象的鎖將保護整個方法。也就是說奠滑,要調(diào)用該方法丹皱,線程必須獲得內(nèi)部的對象鎖。使用synchronized修飾的方法或者代碼塊可以看成是一個原子操作养叛。

synchronized方法是一種粗粒度的并發(fā)控制种呐,某一時刻,只能有一個線程執(zhí)行該synchronized方法;

synchronized塊則是一種細粒度的并發(fā)控制弃甥,只會將塊中的代碼同步爽室,位于方法內(nèi)、synchronized塊之外的代碼是可以被多個線程同時訪問到的淆攻。

用同步方法實現(xiàn)的銀行例子:

  
package synch2;  
  
public class Bank  
{  
   private final double[] accounts;  
  
   public Bank(int n, double initialBalance)  
   {  
      accounts = new double[n];  
      for (int i = 0; i < accounts.length; i++)  
         accounts[i] = initialBalance;  
   }  
   public synchronized void transfer(int from, int to, double amount) throws InterruptedException  
   {  
      while (accounts[from] < amount)  
         wait();  
      System.out.print(Thread.currentThread());  
      accounts[from] -= amount;  
      System.out.printf(" %10.2f from %d to %d", amount, from, to);  
      accounts[to] += amount;  
      System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());  
      notifyAll();  
   }  
   public synchronized double getTotalBalance()  
   {  
      double sum = 0;  
  
      for (double a : accounts)  
         sum += a;  
  
      return sum;  
   }  
   public int size()  
   {  
      return accounts.length;  
   }  
}  
public class TransferRunnable implements Runnable  
{  
   private Bank bank;  
   private int fromAccount;  
   private double maxAmount;  
   private int DELAY = 10;  
  
   public TransferRunnable(Bank b, int from, double max)  
   {  
      bank = b;  
      fromAccount = from;  
      maxAmount = max;  
   }  
   public void run()  
   {  
      try  
      {  
         while (true)  
         {  
            int toAccount = (int) (bank.size() * Math.random());  
            double amount = maxAmount * Math.random();  
            bank.transfer(fromAccount, toAccount, amount);  
            Thread.sleep((int) (DELAY * Math.random()));  
         }  
      }  
      catch (InterruptedException e)  
      {  
      }  
   }  
}  
public class SynchBankTest2  
{  
   public static final int NACCOUNTS = 100;  
   public static final double INITIAL_BALANCE = 1000;  
  
   public static void main(String[] args)  
   {  
      Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);  
      int i;  
      for (i = 0; i < NACCOUNTS; i++)  
      {  
         TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);  
         Thread t = new Thread(r);  
         t.start();  
      }  
   }  
}  

線程狀態(tài)

線程可以有以下6中狀態(tài)

  • New(新創(chuàng)建)
  • Runnable(可運行)
  • Blocked(被阻塞)
  • Waiting(等待)
  • Timed waiting(計時等待)
  • Terminated(被終止)

要想確定一個線程當前的狀態(tài)阔墩,可調(diào)用getState方法。

image

由上圖可以看出瓶珊,一個線程由出生到死亡分為五個階段:

1).創(chuàng)建狀態(tài)

?當用new操作符創(chuàng)建一個新的線程對象時啸箫,該線程處于創(chuàng)建狀態(tài)。

?處于創(chuàng)建狀態(tài)的線程只是一個空的線程對象伞芹,系統(tǒng)不為它分配資源

2). 可運行狀態(tài)

? 執(zhí)行線程的start()方法將為線程分配必須的系統(tǒng)資源忘苛,安排其運行,并調(diào)用線程體—run()方法唱较,這樣就使得該線程處于可運行( Runnable )狀態(tài)扎唾。

? 這一狀態(tài)并不是運行中狀態(tài)(Running ),因為線程也許實際上并未真正運行南缓。這取決于操作系統(tǒng)給線程提供的運行時間胸遇。

一旦一個線程開始運行,它不必始終保持運行汉形。事實上纸镊,運行中的線程被中斷,目的是為了讓其他線程獲得運行機會概疆。線程調(diào)度的細節(jié)依賴于操作系統(tǒng)提供的服務逗威。搶占式調(diào)度系統(tǒng)給每一個可運行線程一個時間片來執(zhí)行任務。當時間片用完岔冀,操作系統(tǒng)剝奪該線程的運行權凯旭,并給另一個線程機會運行。

3).被阻塞的線程或等待線程

  • 當一個線程試圖獲取一個內(nèi)部的對象鎖,而該鎖被其他的線程持有尽纽,則該線程進入阻塞狀態(tài)咐蚯。當所有其他線程釋放該鎖,并且線程調(diào)度器允許本線程持有它的時候弄贿,該線程將變?yōu)榉亲枞麪顟B(tài)春锋。

  • 當線程等待另一個線程通知調(diào)度器一個條件時,它自己 進入等待狀態(tài)差凹。

  • 方法有一個超時參數(shù)時期奔。調(diào)用他們導致線程進入計時等待狀態(tài)。這一狀態(tài)將一直保持到超時期滿或者收到適當?shù)耐ㄖD颉в谐瑫r參數(shù)的方法有:Thread.sleep

4). 被終止的線程

兩個原因之一而被終止:

當線程的run方法執(zhí)行結(jié)束后呐萌,該線程自然消亡。

因為一個沒有捕獲的異常終止了run方法而意外死亡谊娇。

阻塞隊列

對于實際貶稱過來說肺孤,應該盡可能原理底層結(jié)構(gòu)。使用由并發(fā)處理的專業(yè)人士實現(xiàn)的較高層次的結(jié)構(gòu)要方便和安全的多济欢。
許多線程問題可以通過使用一個或多個隊列以優(yōu)雅且安全的方式將其形式化赠堵。生產(chǎn)者線程向隊列插入元素,消費者線程則取出它們法褥。使用隊列茫叭,可以安全地從一個線程向另一個線程傳遞數(shù)據(jù)。例如半等,轉(zhuǎn)賬程序中揍愁,轉(zhuǎn)賬線程將轉(zhuǎn)賬指令對象插入一個隊列中,而不是直接訪問銀行對象杀饵。另一個線程從隊列中取出指令執(zhí)行轉(zhuǎn)賬莽囤。只有該線程可以訪問該銀行對象的內(nèi)部。因此不需要同步凹髓。(當然烁登,線程安全的隊列類的實現(xiàn)者不能不考慮鎖和條件怯屉。)
當試圖向隊列添加元素而隊列已滿蔚舀,或是想從隊列移出元素而隊列為空的時候,阻塞隊列(BlockingQueue)導致線程阻塞锨络。隊列會自動地平衡負載赌躺。
LinkedBlockingQueue的容量在默認下是沒有上邊界的,也可設置之羡儿。
阻塞隊列方法:

方法 正常動作 特殊情況下的動作
add 添加一個元素 隊列滿時拋出IllegalStateException異常
element 返回隊列的頭元素 隊列空時拋出NoSuchElementException異常
offer 添加一個元素并返回true 如果隊列滿礼患,則返回false
peek 返回隊列的頭元素 如果隊列空,則返回null
poll 移出并返回隊列的頭元素 如果隊列空,則返回null
put 添加一個元素 如果隊列滿缅叠,則阻塞
remove 移出并返回頭元素 隊列空時拋出NoSuchElementException異常
take 移出并返回頭元素 如果隊列空悄泥,則阻塞

poll和peek方法返回空來指示失敗,因此肤粱,向這些隊列中插入null值是非法的弹囚。
還有帶有超時的offer方法和poll方法的變體。例如

    boolean success = q.offer(x, 100, TimeUnit.MILLISECONDS);

嘗試在100ms內(nèi)在隊列的尾部插入一個元素领曼。如果成功返回true鸥鹉;否則,達到超時時返回false庶骄。類似地毁渗,下面的調(diào)用:

    Object head = q.poll(100, TimeUnit.MILLISECONDS);

嘗試在100ms內(nèi)移除隊列的頭元素;如果成功返回頭元素单刁,否則灸异,達到超時時返回false。

阻塞隊列練習羔飞,開啟一個線程讀取目錄結(jié)構(gòu)绎狭,把文件添加到阻塞隊列中,另外數(shù)個線程從隊列中讀取文件褥傍,并把文件中包含已設定關鍵字的行打印出來:

package learn.test.object;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class BlockingQueueTest 
{
    public static void main(String[] args)
    {
        BlockingQueue<File> q = new LinkedBlockingQueue<File>(Q_SIZE);

        FilePicker aFilePicker = new FilePicker(q, ROOT_DIR);
        new Thread(aFilePicker).start();

        for(int i=0; i< THREAD_COUNT; i++)
        {
            FileAnalyzer analyzer = new FileAnalyzer(q, KEY_WORD);
            new Thread(analyzer).start();
        }
    }
    public static final String ROOT_DIR = "/home/joseph/Documents/WorkSpace/Source/Java";
    public static final String KEY_WORD = "public";
    public static final int THREAD_COUNT = 10;
    public static final int Q_SIZE = 10; 
}

class FilePicker implements Runnable
{
    public FilePicker(BlockingQueue<File> q, String rootDir)
    {
        this.q = q;
        this.rootDir = rootDir;
    }

    public void run()
    {
        try 
        {
            pickFile();
        } 
        catch (InterruptedException e) 
        {
            e.printStackTrace();
        }
    }

    public void pickFile() throws InterruptedException
    {
        File root = new File(rootDir);
        if(!root.exists())
            System.out.println("the selected file/dir do not exists");
        pickFile(root);
        q.put(END_FLAG);
    }

    public void pickFile(File file) throws InterruptedException
    {
        if(file.isDirectory())
        {
            for(File subFile : file.listFiles())
                pickFile(subFile);
        }
        else
        {
            q.put(file);
        }
    }

    private BlockingQueue<File> q;
    private String rootDir;
    public static final File END_FLAG = new File("");
}

class FileAnalyzer implements Runnable
{
    public FileAnalyzer(BlockingQueue<File> q, String key)
    {
        this.key = key;
        this.q = q;
    }

    public void run()
    {

        try 
        {
            while((curFile = q.take()) != FilePicker.END_FLAG)
            {
                Scanner in = new Scanner(new FileInputStream(curFile));
                int lineCount = 1;
                while(in.hasNext())
                {
                    String str = in.nextLine();
                    if(str.contains(key))
                        System.out.printf("thread [%s] line [%d] in file [%s] : %s%n",Thread.currentThread().getName(), lineCount++, curFile.getName(), str);
                }
            }
            q.put(FilePicker.END_FLAG);
        } 
        catch (InterruptedException e) 
        {

            e.printStackTrace();
        }
        catch (FileNotFoundException e) 
        {
            e.printStackTrace();
        }
    }

    private File curFile;
    private BlockingQueue<File> q;
    private String key;
}

線程安全的集合

[ArrayList線程不安全分析]

一個 ArrayList 儡嘶,在添加一個元素的時候,它可能會有兩步來完成:
1. 在 Items[Size] 的位置存放此元素恍风;
2. 增大 Size 的值蹦狂。
在單線程運行的情況下,如果 Size = 0朋贬,添加一個元素后凯楔,此元素在位置 0,而且 Size=1锦募;
而 如果是在多線程情況下摆屯,比如有兩個線程,線程 A 先將元素存放在位置 0糠亩。但是此時 CPU 調(diào)度線程A暫停虐骑,線程 B 得到運行的機會。線程B也向此 ArrayList 添加元素赎线,因為此時 Size 仍然等于 0 (注意哦廷没,我們假設的是添加一個元素是要兩個步驟哦,而線程A僅僅完成了步驟1)垂寥,所以線程B也將元素存放在位置0颠黎。

線程不安全的例子:

public class ArrayListInThread implements Runnable {
    List<String> list1 = new ArrayList<String>(1);// not thread safe

//    List<String> list1 = Collections.synchronizedList(new ArrayList<String>());// thread safe
    public void run() {
        try {
            Thread.sleep((int)(Math.random() * 2));
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        list1.add(Thread.currentThread().getName());
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadGroup group = new ThreadGroup("testgroup");
        ArrayListInThread t = new ArrayListInThread();
        for (int i = 0; i < 10000; i++) {
            Thread th = new Thread(group, t, String.valueOf(i));
            th.start();
        }

        while (group.activeCount() > 0) {
            Thread.sleep(10);
        }
        System.out.println();
        System.out.println(t.list1.size()); // it should be 10000 if thread safe collection is used.
    }
}

作業(yè)

使用同步機制修正窗口賣票問題另锋。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市狭归,隨后出現(xiàn)的幾起案子夭坪,更是在濱河造成了極大的恐慌,老刑警劉巖过椎,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件台舱,死亡現(xiàn)場離奇詭異,居然都是意外死亡潭流,警方通過查閱死者的電腦和手機竞惋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灰嫉,“玉大人拆宛,你說我怎么就攤上這事∷先觯” “怎么了浑厚?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長根盒。 經(jīng)常有香客問我钳幅,道長,這世上最難降的妖魔是什么炎滞? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任敢艰,我火速辦了婚禮,結(jié)果婚禮上册赛,老公的妹妹穿的比我還像新娘钠导。我一直安慰自己,他們只是感情好森瘪,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布牡属。 她就那樣靜靜地躺著,像睡著了一般扼睬。 火紅的嫁衣襯著肌膚如雪逮栅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天窗宇,我揣著相機與錄音措伐,去河邊找鬼。 笑死担映,一個胖子當著我的面吹牛废士,可吹牛的內(nèi)容都是我干的叫潦。 我是一名探鬼主播蝇完,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了短蜕?” 一聲冷哼從身側(cè)響起氢架,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎朋魔,沒想到半個月后岖研,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡警检,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年孙援,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扇雕。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡拓售,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出镶奉,到底是詐尸還是另有隱情础淤,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布哨苛,位于F島的核電站鸽凶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏建峭。R本人自食惡果不足惜玻侥,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望亿蒸。 院中可真熱鬧使碾,春花似錦、人聲如沸祝懂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽砚蓬。三九已至矢门,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間灰蛙,已是汗流浹背祟剔。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留摩梧,地道東北人物延。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像仅父,于是被迫代替她去往敵國和親叛薯。 傳聞我的和親對象是個殘疾皇子浑吟,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355