執行緒通訊
對上次多執行緒程式設計步驟補充(中部):
- 建立資源類,在資源類中建立屬性和操作方法
- 在資源類裡面操作
- 判斷
- 幹活
- 通知
- 建立多個執行緒,呼叫資源類的操作方法
執行緒通訊的實現例子:
兩個執行緒,實現對一個初始變數為0進行操作,一個執行緒對其+1,一個執行緒對其-1,使得變數結果不改變
使用Synchronized實現的執行緒通訊:
package com.JUC;
/**
* 建立資源類
*/
class Share{
//初始值
private int number = 0;
//建立方法
public synchronized void incr() throws InterruptedException {
//判斷 幹活 通知
if(number != 0){
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"::"+number);
//通知其他執行緒
this.notifyAll();
//System.out.println(this.getClass());
}
public synchronized void decr() throws InterruptedException {
if(number != 1){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
//喚醒其他的執行緒,這裡的this指代在方法中指呼叫該方法的物件
this.notifyAll();
}
}
public class ThreadSignaling {
public static void main(String[] args) throws InterruptedException {
Share share = new Share();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BBB").start();
}
}
volatile和synchronized關鍵字
volatile:即可見性,當修改一個變數的時候,如果該變數是通過volatile修飾的,那麼其他所有的執行緒都會感知到該變數的變化情況。
如果不使用該關鍵字的話:
可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。
舉個簡單的例子,看下面這段程式碼:
//執行緒1執行的程式碼 int i = 0; i = 10; //執行緒2執行的程式碼 j = i;
假若執行執行緒1的是CPU1,執行執行緒2的是CPU2。由上面的分析可知,當執行緒1執行 i =10這句時,會先把i的初始值載入到CPU1的快取記憶體中,然後賦值為10,那麼在CPU1的快取記憶體當中i的值變為10了,卻沒有立即寫入到主存當中。
此時執行緒2執行 j = i,它會先去主存讀取i的值並載入到CPU2的快取當中,注意此時記憶體當中i的值還是0,那麼就會使得j的值為0,而不是10.
這就是可見性問題,執行緒1對變數i修改了之後,執行緒2沒有立即看到執行緒1修改的值
上述的解釋其實可以對應到書中的以下片段:
Java支援多個執行緒同時訪問一個物件或者物件的成員變數,由於每個執行緒可以擁有這個變數的拷貝(雖然物件以及成員變數分配的記憶體是在共享記憶體中的,但是每個執行的執行緒還是可以擁有一份拷貝,這樣做的目的是加速程式的執行,這是現代多核處理器的一個顯著特性),所以程式在執行過程中,一個執行緒看到的變數並不一定是最新的。
使用關鍵字synchronized可以修飾方法或者同步塊;
作用:確保多個執行緒在同一時刻,只能由一個執行緒處於方法或者同步塊中,它保證了執行緒對變數訪問的可見性和排他性。
任何一個物件都有其對應的監視器,當這個物件由同步塊或者同步方法呼叫的時候,需要進行以下邏輯:
任意執行緒對Object(Object由synchronized保護)的訪問,首先要獲得Object的監視器。如果獲取失敗,執行緒進入同步佇列,執行緒狀態變為BLOCKED。當訪問Object的前驅(獲得了鎖的執行緒)釋放了鎖,則該釋放操作喚醒阻塞在同步佇列中的執行緒,使其重新嘗試對監視器的獲取。
Condition的使用
與synchronized再做一個比較:
Lock
替代了 synchronized
方法和語句的使用,Condition
替代了 Object 監視器方法wait notify和notifyAll的使用;
使用Lock condition介面實現買票:
package com.JUC;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class shareDemo {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private int number = 0;
public void inc() throws InterruptedException {
lock.lock();
try{
while(number != 0){
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"::"+number);
/**
* 喚醒多有等待的執行緒
*/
condition.signalAll();
}finally {
lock.unlock();
}
}
public void sub(){
lock.lock();
try{
while(number != 1){
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
/**
* 喚醒多有等待的執行緒
*/
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ConditionLocal {
public static void main(String[] args) {
shareDemo share = new shareDemo();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.inc();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
share.sub();
}
},"BBB").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.inc();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CCC").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
share.sub();
}
},"DDD").start();
}
}
在書籍4.3.1-4.3.3對應的其實是該文章中執行緒通訊的例子。
管道輸入/輸出流:
執行緒間通訊的方式還有管道輸入/輸出流:與檔案的輸入輸出不同的是,它主要用於執行緒間的資料傳輸,傳輸的媒介是記憶體;
以下是書中的內容:
管道輸入/輸出流主要包括瞭如下4種具體實現:PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前兩種面向位元組,而後兩種面向字元。
實現例子:
對於Piped型別的流,必須先要進行繫結,也就是呼叫connect()方法,如果沒有將輸入/輸
出流繫結起來,對於該流的訪問將會丟擲異常
package com.JUC;
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
public class PipeInOut {
public static void main(String[] args) throws IOException {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
//將輸入流和輸出流進行連線,否則會出現IO錯誤
out.connect(in);
//建立print執行緒來接收Main中的輸入
Thread thread = new Thread(new Print(in),"PrintThread");
//開啟該執行緒,開始接收資料
thread.start();
int receive = 0;
try {
//接收輸入的資料並賦值
while((receive = System.in.read()) != -1){
out.write(receive);
}
}finally{
out.close();
}
}
static class Print implements Runnable {
private PipedReader in;
public Print(PipedReader in) {
this.in = in;
}
@Override
public void run() {
int receive = 0;
try {
while((receive = in.read()) != -1){
System.out.print((char) receive);
}
}catch(IOException ex){
}
}
}
}
Thread.join()的使用
書中的定義:其含義是:當前執行緒A等待thread執行緒終止之後才從thread.join()返回;感覺不太好理解;
Java 7 Concurrency Cookbook
是主執行緒等待子執行緒的終止。也就是說主執行緒的程式碼塊中,如果碰到了t.join()方法,此時主執行緒需要等待(阻塞),等待子執行緒結束了(Waits for this thread to die.),才能繼續執行t.join()之後的程式碼塊。
例子:
package com.JUC;
import java.util.concurrent.TimeUnit;
public class Join {
public static void main(String[] args) throws InterruptedException {
Thread previous = Thread.currentThread();
for (int i = 0; i < 10; i++) {
// 每個執行緒擁有前一個執行緒的引用,需要等待前一個執行緒終止,才能從等待中返回
Thread thread = new Thread(new Domino(previous), String.valueOf(i));
thread.start();
previous = thread;
}
TimeUnit.SECONDS.sleep(5);
//主執行緒
System.out.println(Thread.currentThread().getName()+"--terminate.");
}
static class Domino implements Runnable {
private Thread thread;
public Domino(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//子執行緒
System.out.println(Thread.currentThread().getName()+"---Terminate.");
}
}
}
每個執行緒終止的前提是前驅執行緒的終止,每個執行緒等待前驅執行緒終止後,才從join()方法返回;
我們檢視下join方法的原始碼可以發現其中也是用的synchronized修飾的;
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
關於書中的4.3.6ThreadLocal的使用可以看下以前寫的文章:點選進入
參考:
《JUC併發程式設計的藝術》
《【尚矽谷】大廠必備技術之JUC併發程式設計》