Java基礎 多線程 解決安全問題 等待喚醒機制 Lock Condition interrupt join setPriority yield

線程間通信

函數說明:

wait

public final void wait()
throws InterruptedException在其他線程調用此對象的 notify() 方法或 notifyAll() 方法前,導致當前線程等待。換句話說,此方法的行爲就好像它僅執行 wait(0) 調用一樣。
當前線程必須擁有此對象監視器。該線程發佈對此監視器的所有權並等待,直到其他線程通過調用 notify 方法,或 notifyAll 方法通知在此對象的監視器上等待的線程醒來。然後該線程將等到重新獲得對監視器的所有權後才能繼續執行。

對於某一個參數的版本,實現中斷和虛假喚醒是可能的,而且此方法應始終在循環中使用:

synchronized (obj) {
while ()
obj.wait();
… // Perform action appropriate to condition
}
此方法只應由作爲此對象監視器的所有者的線程來調用。有關線程能夠成爲監視器所有者的方法的描述,請參閱 notify 方法。

拋出:
IllegalMonitorStateException - 如果當前線程不是此對象監視器的所有者。
InterruptedException - 如果在當前線程等待通知之前或者正在等待通知時,任何線程中斷了當前線程。在拋出此異常時,當前線程的中斷狀態 被清除。


notify

public final void notify()喚醒在此對象監視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,並在對實現做出決定時發生。線程通過調用其中一個 wait 方法,在對象的監視器上等待。
直到當前線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程。被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭;例如,喚醒的線程在作爲鎖定此對象的下一個線程方面沒有可靠的特權或劣勢。

此方法只應由作爲此對象監視器的所有者的線程來調用。通過以下三種方法之一,線程可以成爲此對象監視器的所有者:

通過執行此對象的同步實例方法。
通過執行在此對象上進行同步的 synchronized 語句的正文。
對於 Class 類型的對象,可以通過執行該類的同步靜態方法。
一次只能有一個線程擁有對象的監視器。

拋出:
IllegalMonitorStateException - 如果當前線程不是此對象監視器的所有者。


notifyAll

public final void notifyAll()喚醒在此對象監視器上等待的所有線程。線程通過調用其中一個 wait 方法,在對象的監視器上等待。
直到當前線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程。被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭;例如,喚醒的線程在作爲鎖定此對象的下一個線程方面沒有可靠的特權或劣勢。

此方法只應由作爲此對象監視器的所有者的線程來調用。有關線程能夠成爲監視器所有者的方法的描述,請參閱 notify 方法。

拋出:
IllegalMonitorStateException - 如果當前線程不是此對象監視器的所有者。


解決安全問題

兩個線程操作同一資源,並防止同時操作造成的數據錯誤。

一個讀取,一個寫入。
不能一個線程正在寫入,寫入到一半時,讀取線程獲得執行權把寫入到一半的錯誤數據讀取出來。
爲了防止發生這類問題,可以使用同步 synchronized 對資源操作部分代碼進行保護。
以實際例子進行說明:

package 多線程.通信;

class Res//資源類
{
    private String name;
    private String sex;
    public synchronized void setInfo(String name,String sex)
    {//資源寫入操作,加同步 synchronized防止寫入時被讀取

            this.name=name;
            this.sex=sex;

    }
    public synchronized String[] getInfo()
    {//資源讀取操作,加同步 synchronized防止寫入時被讀取

            return new String[]{this.name,this.sex};

    }

}

class Input implements Runnable//實現Runnable接口類 Input寫入
{
    Res r;
    Input(Res r)//通過構造函數獲取到資源的引用
    {
        this.r=r;
    }
    public void run()
    {
        boolean b=true;
        while(true)
        {
            //synchronized(r){
            if(b)//設置不同的名字和性別
            {
                r.setInfo("Tom","man");

            }
            else
            {
                r.setInfo("麗麗","女");

            }
            b=!b;
            //}
        }
    }
}

class Output implements Runnable//實現Runnable接口 Output寫出數據
{
    Res r;
    Output(Res r)//通過帶參構造函數獲取資源的引用
    {
        this.r=r;
    }
    public void run()
    {
        String[] sl;
        while(true)
        {
            //synchronized(r){
            sl=r.getInfo();//獲取資源
            System.out.println(sl[0]+"----"+sl[1]);//顯示
            //}
        }
    }
}

public class InputOutputDemo {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Res res=new Res();//創建一個新的資源對象
        Output op=new Output(res);//創建一個新的讀取對象 op
        Input ip=new Input(res);//創建一個新的寫入對象 ip
        Thread t1=new Thread(op);//創建一個線程執行 op定義的run方法
        Thread t2=new Thread(ip);//創建一個線程執行 ip定義的方法
        t1.start();//啓動線程
        t2.start();//啓動線程
    }

}

等待喚醒機制wait() notify()

實現兩個線程交替運行,而非完全搶佔式執行.

package 多線程.等待喚醒;

class Res//定義資源類
{
    public String name;//資源類成員變量
    public String sex;//資源類成員變量
    public boolean flag=false;//flag意味着線程是否進行等待
}

class Input implements Runnable//實現接口
{
    Res r;
    int x=0;
    Input(Res r)//帶參構造函數傳入資源引用
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
        {
            synchronized(r)//同步代碼塊,使用r對象作爲監視器(鎖)
            {
                if(r.flag)//如果爲true
                {
                    try {
                        r.wait();//當前線程進入等待,使用r對象作爲鎖,也就是對鎖進行了操作,釋放了鎖。此時線程在這裏暫停運行,直到其他擁有r對象鎖的線程使用r.notify()通知等待的線程取消等待,纔會繼續執行。
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                if(x==0)
                {
                    r.name="mike";
                    r.sex="man";
                }
                else
                {
                    r.name="麗麗";
                    r.sex="女";
                }
                x=(x+1)%2;
                //寫入數據完畢,把標識設爲true。
                r.flag=true;//原flase設置爲true後Output可以正常執行,Input進入等待.
                r.notify();//喚醒其他在等待的一個線程

            }
        }
    }
}

class Output implements Runnable
{
    Res r;
    Output(Res r)
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
        {
            synchronized(r)
            {
                if(!r.flag)
                {
                    try {
                        r.wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                System.out.println(r.name+" "+r.sex);
                //標識設爲false意味着輸出操作完成
                r.flag=false;
                r.notify();//解除一個正在等待的線程.

            }
        }
    }
}


public class InputOutputDemo {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Res r=new Res();
        Input in=new Input(r);
        Output ot=new Output(r);
        Thread t1=new Thread(in);
        Thread t2=new Thread(ot);
        t1.start();
        t2.start();
    }

}

因爲兩個線程都受到 synchronized 同步的保護,所以不可能同時都對flag進行判斷,只有其中一個可以判斷,而且在判斷時另一個線程一定是執行完保護代碼的,也就是一定是對flag標識完成了操作。所以不必擔心flag會不同
這樣,就保證了其中一個線程準備執行受到保護的代碼時通過flag可以得知是否滿足執行條件,如果滿足才執行,不滿足則使用 監視器對象的wait()方法 進入等待狀態,並釋放保護權限。

這時保護權限解除,另一線程就可以執行他自己的受到相同監視器保護的代碼,同理,此時flag標識可得與另一線程條件相反,也就是可以繼續執行保護代碼,所以就完成執行,最後重置flag狀態,取反,以防止自己下次搶佔到執行權而不該執行時執行。最後自己放棄對象鎖定,對另一線程進行解除等待操作 notify()。之後,另一線程獲取到鎖定權限,接着執行沒有執行完的受保護代碼,依次反覆賦予權限。

最終實現了兩個線程交替執行。各執行一次,而非完全搶佔式執行。

多個線程之間通訊 使用 notifyAll()進行喚醒.

生產者/消費者例程。
實現 兩個 數據讀取 線程 和 兩個 數據寫入 線程 共同使用 同一個資源。
在這個例子中使用while(flag)的方式來對線程的wait()等待狀態進行控制,防止線程在沒有判斷flag的情況下從睡眠狀態喚醒而不加判斷就對數據進行操作,而造成數據錯誤!
上一個例子使用if語句的形式顯然在大於兩個線程時將發生錯誤,因爲線程被喚醒後直接從wait()語句之後開始執行而不進行flag判斷了.
故應使用while()語句對 wait() 方法進行控制更加安全.
下面是代碼:

class ProducerConsumerDemo
{
    public static void main(String[] args)
    {
        Resource res=new Resource();
        Producer pd=new Producer(res);
        Consumer cs=new Consumer(res);

        Thread t1=new Thread(pd);//數據寫入 線程1
        Thread t2=new Thread(pd);//數據寫入 線程2
        Thread t3=new Thread(cs);//數據讀取 線程3
        Thread t4=new Thread(cs);//數據讀取 線程4

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

class Resource{                 //資源類
    private String name;        //資源名
    private int count =1;       //計數
    private boolean flag = false;       //寫入和輸出 標識

    public synchronized void set(String name) //同步的 寫入數據
    {
        while(flag)             //注意,此處使用while 而非 if,控制線程等待更加安全有效。防止睡死
            try
            {
                wait();         //線程進入等待.
            }
            catch(Exception e)
            {

            }
        this.name=name+"--"+count++;    //計數增加

        System.out.println(Thread.currentThread().getName()+".......生產者.."+this.name);//輸出顯示寫入數據完畢
        flag=true;                      //把標誌位置 true,允許讀取
        this.notifyAll();               //喚醒所有處於wait()狀態的線程
    }

    public synchronized void out()//同步的 讀取數據
    {
        while(!flag)        //此處使用while 控制線程等待更加安全有效。防止睡死
            try
            {
                wait();         //線程進入等待.
            }
            catch(Exception e)
            {

            }

        System.out.println(Thread.currentThread().getName()+"...消費者.."+this.name);
        flag=false;             //標識置爲 false 允許寫入
        this.notifyAll();       //喚醒所有處於wait
    }
}

class Producer implements Runnable      //生產者(數據寫入)實現接口Runnable 表示這是一個可以作爲線程執行的 Runnable 子類
{
    private Resource res;
    Producer(Resource res)//構造函數,通過參數 res設置線程的共享數據 ,在創建可執行對象時就必須設置共享數據
    {
        this.res=res;
    }
    public void run()
    {
        while(true)
        {
            res.set("商品");//調用資源的寫入數據方法.
        }
    }

}

class Consumer implements Runnable//消費者(數據讀取)
{
    private Resource res;
    Consumer(Resource res)
    {
        this.res=res;
    }
    public void run()
    {
        while(true)
        {
            res.out();//調用資源的讀取數據方法.
        }
    }
}

當使用 ifflag 進行判斷時的 錯誤分析:
假設一種情況,當線程1執行完後,線程2獲得執行權,兩個線程都是數據寫入,那麼此時線程2判斷flag後會進入wait()狀態,等待讀取線程操作,線程3 讀取數據後 調用 notify(),這時數據讀取完畢,數據又可以寫入了,加入此時 寫入線程1 獲得執行權,那麼數據將被寫入一次,關鍵:寫入完畢,調用notify(),此時處於wait()狀態的線程2搶到執行權,線程2就會不經判斷flag而直接執行後邊寫入數據的代碼,但數據已經被線程1寫入完畢,這時線程2的執行就是一個錯誤,不應該對數據進行兩次寫入,不是我們預期的效果!!!


JDK5.0升級版同步 鎖(Lock) 和 監視器(Condition)

java.util.concurrent.locks Condition/Lock

接口 Lock

所有已知實現類:
ReentrantLock
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock

public interface LockLock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。此實現允許更靈活的結構,可以具有差別很大的屬性,可以支持多個相關的 Condition 對象。

鎖是控制多個線程對共享資源進行訪問的工具。通常,鎖提供了對共享資源的獨佔訪問。一次只能有一個線程獲得鎖,對共享資源的所有訪問都需要首先獲得鎖。

隨着靈活性的增加,也帶來了更多的責任。不使用塊結構鎖就失去了使用 synchronized 方法和語句時會出現的鎖自動釋放功能。在大多數情況下,應該使用以下語句:

     Lock l = ...; 
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }

void lock()
獲取鎖。

void unlock()
釋放鎖。

接口 Condition

public interface Condition
Condition 將 Object 監視器方法(wait、notify 和 notifyAll)分解成截然不同的對象,以便通過將這些對象與任意 Lock 實現組合使用,爲每個對象提供多個等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和語句的使用,Condition 替代了 Object 監視器方法的使用。

條件(也稱爲條件隊列 或條件變量)爲線程提供了一個含義,以便在某個狀態條件現在可能爲 true 的另一個線程通知它之前,一直掛起該線程(即讓其“等待”)。因爲訪問此共享狀態信息發生在不同的線程中,所以它必須受保護,因此要將某種形式的鎖與該條件相關聯。等待提供一個條件的主要屬性是:以原子方式 釋放相關的鎖,並掛起當前線程,就像 Object.wait 做的那樣。

Condition 實例實質上被綁定到一個鎖上。要爲特定 Lock 實例獲得 Condition 實例,請使用其 newCondition() 方法。

作爲一個示例,假定有一個綁定的緩衝區,它支持 put 和 take 方法。如果試圖在空的緩衝區上執行 take 操作,則在某一個項變得可用之前,線程將一直阻塞;如果試圖在滿的緩衝區上執行 put 操作,則在有空間變得可用之前,線程將一直阻塞。我們喜歡在單獨的等待 set 中保存 put 線程和 take 線程,這樣就可以在緩衝區中的項或空間變得可用時利用最佳規劃,一次只通知一個線程。可以使用兩個 Condition 實例來做到這一點。

 class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length) 
         notFull.await();
       items[putptr] = x; 
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0) 
         notEmpty.await();
       Object x = items[takeptr]; 
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   } 
 }

以下是一個實現4個線程,2個寫入,2個讀取。不能同時寫入或同時讀取。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class MyBuffer          //資源類
{
    private String name;
    Lock lock=new ReentrantLock();//定義鎖lock
    Condition cdW=lock.newCondition();//定義新的Condition(條件) cdW 寫入條件
    Condition cdR=lock.newCondition();//定義 cdR 讀取條件
    boolean flag=false;//定義讀寫標識
    public void write(String name) throws InterruptedException  //寫入方法.拋出中斷異常
    {

        lock.lock();//上鎖
        try//嘗試執行代碼
        {
            while(flag)//用flag標識判斷是否寫入
                cdW.await();//寫入條件等待
            this.name=name;
            System.out.println(Thread.currentThread().getName()+"---Write---"+this.name);
            flag=true;//標識爲可讀取
            cdR.signalAll();//讀取條件喚醒所有等待
        }
        finally//無論代碼是否有錯誤最後都必須釋放鎖.
        {
            lock.unlock();
        }

    }

    public void read() throws InterruptedException//讀取方法,拋出中斷異常,中斷異常由Condition.await()方法拋出.
    {
        lock.lock();//上鎖
        try
        {
            while(!flag)
                cdR.await();
            System.out.println(Thread.currentThread().getName()+"--Read---"+this.name);
            flag=false;
            cdW.signalAll();
        }
        finally
        {
            lock.unlock();
        }
    }
}

class MyWriter implements Runnable//實現Runnable接口,寫入線程
{
    MyBuffer buf;
    boolean b=true;
    MyWriter(MyBuffer buf)//構造函數,獲取資源
    {
        this.buf=buf;
    }

    public void run()//實現run方法
    {
        while(true)
        {
            if(b)
                try
                {
                    buf.write("Lily");
                }
                catch(InterruptedException e)//捕獲拋出的中斷異常
                {
                    e.printStackTrace();
                }
            else
                try{
                    buf.write("Tom");
                }
                catch(InterruptedException e)
                {
                    e.printStackTrace();
                }
            b=!b;
        }
    }
}
class MyReader implements Runnable//實現Runnable接口,讀取線程
{
    MyBuffer buf;
    MyReader(MyBuffer buf)//構造函數
    {
        this.buf=buf;
    }

    public void run()//實現run方法
    {
        while(true)
        {
            try
            {
                buf.read();
            }
            catch(InterruptedException e)//捕獲拋出的中斷異常
            {
                e.printStackTrace();
            }
        }
    }
}


class MyReadWrite
{
    public static void main(String[] args)
    {
        MyBuffer buf=new MyBuffer();//創建資源
        MyWriter mw=new MyWriter(buf);//創建可執行對象寫入,傳入資源buf
        MyReader mr=new MyReader(buf);//創建可執行對象讀取,傳入資源buf
        Thread t1=new Thread(mw);//創建線程1,執行寫入對象的run方法
        Thread t2=new Thread(mw);//創建線程2,執行寫入對象的run方法
        Thread t3=new Thread(mr);//創建線程3,執行讀取對象的run方法
        Thread t4=new Thread(mr);//創建線程4,執行讀取對象的run方法
        t1.start();
        t2.start();
        t3.start();
        t4.start();


    }
}

interrupt() 方法是強制中斷線程的wait()讓線程恢復運行,而不是終結線程.

Thread.interrupt();


class StopThread extends Thread
{
    private boolean flag=true;
    public synchronized void run()
    {
        while(flag)
        {
            try
            {
                wait();
            }
            catch(InterruptedException e)//線程捕獲異常並處理
            {
                System.out.println(Thread.currentThread().getName()+"---"+"Exception.");
                flag=false;//將flag置爲false,下次循環直接結束線程.
            }
            System.out.println(Thread.currentThread().getName()+"---Run.");//捕獲到異常後處理異常,並繼續執行該語句.
        }
    }
    public void changeFlag()
    {
        flag=!flag;
    }
}


public class StopThreadDemo
{
    public static void main(String[] args)
    {
        int i=0;
        StopThread t1=new StopThread();
        StopThread t2=new StopThread();
        t1.start();
        t2.start();
        while(true)
        {
            if(i++==60)
            {
                //t1.changeFlag();
                t1.interrupt();//調用interrupt叫醒線程.會產生一個InterruptException異常並被線程捕獲。
                t2.interrupt();
                break;
            }
            System.out.println("main---"+i);
        }
        System.out.println("main---over");
    }
}

Thread.setDaemon(boolean on)

setDaemon
public final void setDaemon(boolean on)將該線程標記爲守護線程或用戶線程。當正在運行的線程都是守護線程時,Java 虛擬機退出。
該方法必須在啓動線程前調用。

可以立即而爲設置爲後臺線程,當前臺線程退出,只剩後臺線程,那麼後臺線程也將停止運行,Java虛擬機退出.整個程序結束.

示例代碼:


class StopThread extends Thread
{
    private boolean flag=true;
    public void run()
    {
        while(flag)
        {
            /*
            try
            {
                wait();
            }
            catch(InterruptedException e)
            {
                System.out.println(Thread.currentThread().getName()+"---"+"Exception.");
                flag=false;
            }
            */
            System.out.println(Thread.currentThread().getName()+"---Run.");
        }
    }
    public void changeFlag()
    {
        flag=!flag;
    }
}


public class StopThreadDemo
{
    public static void main(String[] args)
    {
        int i=0;
        StopThread t1=new StopThread();
        StopThread t2=new StopThread();
        t1.setDaemon(true);
        t2.setDaemon(true);
        t1.start();
        t2.start();
        while(true)
        {
            if(i++==60)
            {
                //t1.changeFlag();
                //t1.interrupt();
                //t2.interrupt();
                break;
            }
            System.out.println("main---"+i);
        }
        System.out.println("main---over");
    }
}

Thread.join() 方法

join
public final void join()
throws InterruptedException等待該線程終止。

拋出:
InterruptedException - 如果任何線程中斷了當前線程。當拋出該異常時,當前線程的中斷狀態 被清除。

  • 當A線程執行到了B線程的.join()方法時,A就會等待。等B線程都執行完,A纔會執行。
  • join可以用來臨時加入線程執行.
class Demo implements Runnable
{
    public void run()
    {
        for(int x=0;x<70;x++)
        {
            System.out.println(Thread.currentThread().getName()+"--"+x);
        }
    }
}

class JoinDemo
{
    public static void main(String[] args) throws InterruptedException
    {
        Demo d=new Demo();
        Thread t1=new Thread(d);
        Thread t2=new Thread(d);
        t1.start();

        //t1.join();//放在此處t2.start()還未執行,要等到t1執行完,main才繼續執行,纔會啓動t2.

        t2.start();//t2啓動,可以和t1同時執行,不受到t1.join()影響.
        t1.join();//放在此處,t1,t2同時執行.而主線程要等待t1執行完才繼續執行.
        for(int x=0;x<80;x++)
        {
            System.out.println(Thread.currentThread().getName()+"..."+x);
        }
        System.out.println("Over");
    }
}

setPriority(int newPriority) 設置線程優先級

setPriority
public final void setPriority(int newPriority)更改線程的優先級。
首先調用線程的 checkAccess 方法,且不帶任何參數。這可能拋出 SecurityException。

在其他情況下,線程優先級被設定爲指定的 newPriority 和該線程的線程組的最大允許優先級相比較小的一個。

參數:
newPriority - 要爲線程設定的優先級
拋出:
IllegalArgumentException - 如果優先級不在 MIN_PRIORITY 到 MAX_PRIORITY 範圍內。
SecurityException - 如果當前線程無法修改該線程。

yield()

yield
public static void yield()暫停當前正在執行的線程對象,並執行其他線程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章