首頁  >  文章  >  Java  >  深入解析Java的執行緒同步以及執行緒間通信

深入解析Java的執行緒同步以及執行緒間通信

高洛峰
高洛峰原創
2017-01-05 15:18:311259瀏覽

Java執行緒同步
當兩個或兩個以上的執行緒需要共享資源,它們需要某種方法來決定資源在某一刻只被一個執行緒佔用。達到此目的的過程叫做同步(synchronization)。像你所看到的,Java為此提供了獨特的,語言層次上的支援。

同步的關鍵是管程(也叫信號量semaphore)的概念。管程是互斥獨佔鎖定的對象,或稱為互斥體(mutex)。在給定的時間,僅有一個執行緒可以獲得管程。當一個執行緒需要鎖定,它必須進入管程。所有其他的試圖進入已經鎖定的管程的執行緒必須掛起直到第一個執行緒退出管程。這些其他的執行緒被稱為等待管程。一個擁有管程的線程如果願意的話可以再次進入相同的管程。

如果你用其他語言例如C或C++時用過同步,你會知道它用起來有一點詭異。這是因為很多語言它們自己不支援同步。相反,對同步線程,程式必須利用作業系統來源語。幸運的是Java透過語言元素實現同步,大多數的與同步相關的複雜性都被消除。

你可以用兩種方法同步化程式碼。兩者都包含synchronized關鍵字的運用,以下分別說明這兩種方法。
使用同步方法

Java中同步是簡單的,因為所有物件都有它們與之對應的隱式管程。進入某一物件的管程,就是呼叫被synchronized關鍵字修飾的方法。當一個執行緒在一個同步方法內部,所有試圖呼叫該方法(或其他同步方法)的相同實例的其他執行緒必須等待。為了退出管程,並放棄對物件的控制權給其他等待的線程,擁有管程的線程僅需從同步方法中返回。

為理解同步的必要性,讓我們從一個應該使用同步卻沒有用的簡單例子開始。下面的程式有三個簡單類別。首先是Callme,它有一個簡單的方法call( )。 call( )方法有一個名為msg的String參數。該方法試圖在方括號內列印msg 字串。有趣的事是在呼叫call( ) 列印左括號和msg字串後,呼叫Thread.sleep(1000),該方法使目前執行緒暫停1秒。

下一個類別的建構子Caller,引用了Callme的一個實例以及一個String,它們被分別存在target 和 msg 中。建構函數也建立了一個呼叫該物件的run( )方法的新執行緒。該線程立即啟動。 Caller類別的run( )方法透過參數msg字串呼叫Callme實例target的call( ) 方法。最後,Synch類別由創建Callme的一個簡單實例和Caller的三個具有不同訊息字串的實例開始。

Callme的相同實例傳給每個Caller實例。

// This program is not synchronized.
class Callme {
  void call(String msg) {
    System.out.print("[" + msg);
    try {
      Thread.sleep(1000);
    } catch(InterruptedException e) {
      System.out.println("Interrupted");
    }
    System.out.println("]");
  }
}
 
class Caller implements Runnable {
  String msg;
  Callme target;
  Thread t;
  public Caller(Callme targ, String s) {
    target = targ;
    msg = s;
    t = new Thread(this);
    t.start();
  }
  public void run() {
    target.call(msg);
  }
}
 
class Synch {
  public static void main(String args[]) {
    Callme target = new Callme();
    Caller ob1 = new Caller(target, "Hello");
    Caller ob2 = new Caller(target, "Synchronized");
    Caller ob3 = new Caller(target, "World");
    // wait for threads to end
    try {
     ob1.t.join();
     ob2.t.join();
     ob3.t.join();
    } catch(InterruptedException e) {
     System.out.println("Interrupted");
    }
  }
}

   

該程式的輸出如下:

Hello[Synchronized[World]
]
]

   

在本例中,透過呼叫執行到另一個例子,call( ),允許執行轉換到另一個方法。該結果是三個訊息字串的混合輸出。在該程式中,沒有阻止三個執行緒同時呼叫相同物件的相同方法的方法存在。這是一種競爭,因為三個執行緒爭著完成方法。例題用sleep( )使此影響重複且明顯。在大多數情況,競爭是更為複雜和不可預測的,因為你不能確定何時上下文轉換會發生。這使程式時而運作正常時而出錯。

為達到上例所想達到的目的,必須有權連續的使用call( )。也就是說,在某一時刻,必須限制只有一個執行緒可以支配它。為此,你只需在call( ) 定義前加上關鍵字synchronized,如下:

class Callme {
  synchronized void call(String msg) {
    ...

   

這防止了在一個執行緒使用call( )時其他執行緒進入call( )。在synchronized加到call( )前面以後,程式輸出如下:

[Hello]
[Synchronized]
[World]

   

任何時候在多執行緒情況下,你有一個方法或多個方法操縱關鍵字的內部狀態,都必須用synchronized 關鍵字來防止狀態出現競爭。記住,一旦執行緒進入實例的同步方法,沒有其他執行緒可以進入相同實例的同步方法。然而,該實例的其他不同步方法卻仍然可以被呼叫。
同步語句

儘管在創建的類別的內部創建同步方法是獲得同步的簡單和有效的方法,但它並非在任何時候都有效。這其中的原因,請跟著思考。假設你想獲得不為多執行緒訪問設計的類別物件的同步訪問,也就是,該類別沒有用到synchronized方法。而且,該類別不是你自己,而是第三方創建的,你不能獲得它的原始碼。這樣,你不能在相關方法前面加上synchronized修飾符。怎樣才能使該類別的一個物件同步化呢?很幸運,解決方法很簡單:你只需將對這個類別定義的方法的呼叫放入一個synchronized區塊內就可以了。

下面是synchronized語句的普通形式:

synchronized(object) {
  // statements to be synchronized
}

   

其中,object是被同步对象的引用。如果你想要同步的只是一个语句,那么不需要花括号。一个同步块确保对object成员方法的调用仅在当前线程成功进入object管程后发生。

下面是前面程序的修改版本,在run( )方法内用了同步块:

// This program uses a synchronized block.
class Callme {
  void call(String msg) {
    System.out.print("[" + msg);
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      System.out.println("Interrupted");
    }
    System.out.println("]");
  }
}
 
class Caller implements Runnable {
  String msg;
  Callme target;
  Thread t;
  public Caller(Callme targ, String s) {
    target = targ;
    msg = s;
    t = new Thread(this);
    t.start();
  }
 
  // synchronize calls to call()
  public void run() {
    synchronized(target) { // synchronized block
      target.call(msg);
    }
  }
}
 
class Synch1 {
  public static void main(String args[]) {
    Callme target = new Callme();
    Caller ob1 = new Caller(target, "Hello");
    Caller ob2 = new Caller(target, "Synchronized");
    Caller ob3 = new Caller(target, "World");
 
    // wait for threads to end
    try {
      ob1.t.join();
      ob2.t.join();
      ob3.t.join();
    } catch(InterruptedException e) {
      System.out.println("Interrupted");
    }
  }
}

   

这里,call( )方法没有被synchronized修饰。而synchronized是在Caller类的run( )方法中声明的。这可以得到上例中同样正确的结果,因为每个线程运行前都等待先前的一个线程结束。

Java线程间通信
多线程通过把任务分成离散的和合乎逻辑的单元代替了事件循环程序。线程还有第二优点:它远离了轮询。轮询通常由重复监测条件的循环实现。一旦条件成立,就要采取适当的行动。这浪费了CPU时间。举例来说,考虑经典的序列问题,当一个线程正在产生数据而另一个程序正在消费它。为使问题变得更有趣,假设数据产生器必须等待消费者完成工作才能产生新的数据。在轮询系统,消费者在等待生产者产生数据时会浪费很多CPU周期。一旦生产者完成工作,它将启动轮询,浪费更多的CPU时间等待消费者的工作结束,如此下去。很明显,这种情形不受欢迎。

为避免轮询,Java包含了通过wait( ),notify( )和notifyAll( )方法实现的一个进程间通信机制。这些方法在对象中是用final方法实现的,所以所有的类都含有它们。这三个方法仅在synchronized方法中才能被调用。尽管这些方法从计算机科学远景方向上来说具有概念的高度先进性,实际中用起来是很简单的:
wait( ) 告知被调用的线程放弃管程进入睡眠直到其他线程进入相同管程并且调用notify( )。
notify( ) 恢复相同对象中第一个调用 wait( ) 的线程。
notifyAll( ) 恢复相同对象中所有调用 wait( ) 的线程。具有最高优先级的线程最先运行。

这些方法在Object中被声明,如下所示:

final void wait( ) throws InterruptedException
final void notify( )
final void notifyAll( )

   


wait( )存在的另外的形式允许你定义等待时间。

下面的例子程序错误的实行了一个简单生产者/消费者的问题。它由四个类组成:Q,设法获得同步的序列;Producer,产生排队的线程对象;Consumer,消费序列的线程对象;以及PC,创建单个Q,Producer,和Consumer的小类。

// An incorrect implementation of a producer and consumer.
class Q {
  int n;
  synchronized int get() {
    System.out.println("Got: " + n);
    return n;
  }
  synchronized void put(int n) {
    this.n = n;
    System.out.println("Put: " + n);
  }
}
class Producer implements Runnable {
  Q q;
  Producer(Q q) {
    this.q = q;
    new Thread(this, "Producer").start();
  }
  public void run() {
    int i = 0;
    while(true) {
      q.put(i++);
    }
  }
}
class Consumer implements Runnable {
  Q q;
  Consumer(Q q) {
    this.q = q;
    new Thread(this, "Consumer").start();
  }
  public void run() {
    while(true) {
      q.get();
    }
  }
}
class PC {
  public static void main(String args[]) {
    Q q = new Q();
    new Producer(q);
    new Consumer(q);
    System.out.println("Press Control-C to stop.");
  }
}

   

尽管Q类中的put( )和get( )方法是同步的,没有东西阻止生产者超越消费者,也没有东西阻止消费者消费同样的序列两次。这样,你就得到下面的错误输出(输出将随处理器速度和装载的任务而改变):

Put: 1
Got: 1
Got: 1
Got: 1
Got: 1
Got: 1
Put: 2
Put: 3
Put: 4
Put: 5
Put: 6
Put: 7
Got: 7

   

生产者生成1后,消费者依次获得同样的1五次。生产者在继续生成2到7,消费者没有机会获得它们。

用Java正确的编写该程序是用wait( )和notify( )来对两个方向进行标志,如下所示:

// A correct implementation of a producer and consumer.
class Q {
  int n;
  boolean valueSet = false;
  synchronized int get() {
    if(!valueSet)
      try {
        wait();
      } catch(InterruptedException e) {
        System.out.println("InterruptedException caught");
      }
      System.out.println("Got: " + n);
      valueSet = false;
      notify();
      return n;
    }
    synchronized void put(int n) {
      if(valueSet)
      try {
        wait();
      } catch(InterruptedException e) {
        System.out.println("InterruptedException caught");
      }
      this.n = n;
      valueSet = true;
      System.out.println("Put: " + n);
      notify();
    }
  }
  class Producer implements Runnable {
    Q q;
    Producer(Q q) {
    this.q = q;
    new Thread(this, "Producer").start();
  }
  public void run() {
    int i = 0;
    while(true) {
      q.put(i++);
    }
  }
}
class Consumer implements Runnable {
  Q q;
  Consumer(Q q) {
    this.q = q;
    new Thread(this, "Consumer").start();
  }
  public void run() {
    while(true) {
      q.get();
    }
  }
}
class PCFixed {
  public static void main(String args[]) {
    Q q = new Q();
    new Producer(q);
    new Consumer(q);
    System.out.println("Press Control-C to stop.");
  }
}

   

内部get( ), wait( )被调用。这使执行挂起直到Producer 告知数据已经预备好。这时,内部get( ) 被恢复执行。获取数据后,get( )调用notify( )。这告诉Producer可以向序列中输入更多数据。在put( )内,wait( )挂起执行直到Consumer取走了序列中的项目。当执行再继续,下一个数据项目被放入序列,notify( )被调用,这通知Consumer它应该移走该数据。

下面是该程序的输出,它清楚的显示了同步行为:

Put: 1
Got: 1
Put: 2
Got: 2
Put: 3
Got: 3
Put: 4
Got: 4
Put: 5
Got: 5

    

更多深入解析Java的线程同步以及线程间通信相关文章请关注PHP中文网!


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn