前言
上節我們介紹了線程從創建到結束的過程,介紹了幾種常見的啓動和終止方法,我們知道了如何使用一個線程,那麼今天我們再接下來看看1個或者多個線程之間是如何進行通信的?OK,讓我們一起走進今天的併發之旅吧,祝您旅途愉快。
景點一:共享變量(volatile)
關鍵字volatile可以用來修飾一個共享的變量字段,使用volatile修飾的變量,當訪問該變量時需要從共享內存中獲取最新值,而對該變量的更新,必須同步刷新回共享內存,這樣就能保證所有線程對該變量訪問的可見性;結合示例代碼更容易理解:
public class RunThread extends Thread{
private volatile static boolean isRunning = true;
private void setRunning(boolean isRunning){
RunThread.isRunning = isRunning;
}
public void run(){
System.out.println("進入run方法..");
while(isRunning == true){
}
System.out.println("線程感知isRunning值被設置成false,線程停止!!!");
}
public static void main(String[] args) throws InterruptedException {
RunThread runThread1 = new RunThread();
RunThread runThread2 = new RunThread();
runThread1.start();
runThread2.start();
// 主線程睡眠1秒之後線程1將標識設爲false
Thread.sleep(1000);
runThread1.setRunning(false);
System.out.println("isRunning的值已經被設置了false");
Thread.sleep(2000);
System.out.println(runThread1.isAlive());
System.out.println(runThread2.isAlive());
}
}
結果:
進入run方法..
進入run方法..
isRunning的值已經被設置了false
線程感知isRunning值被設置成false,線程停止!!!
線程感知isRunning值被設置成false,線程停止!!!
false
false
我們可以看到,開啓2個線程,線程運行終止的條件是isRunning是否爲false,當2個線程都啓動運行之後,主線程睡眠1秒,然後線程1將isRunning設爲false,會發現,線程1和線程2立馬感應到了isRunning值的更新,結束了線程的運行;
當去掉volatile修飾共享變量時,結果如下:2個線程都未感應isRunning的變化。
景點二:同步代碼(synchronized)
synchronized修飾的方法或者代碼塊,主要是確保多個線程在同一時刻,只能有一個線程處於方法或者代碼塊中,保證了線程對變量訪問的可見性和排他性,我們來看下面這段代碼示例:
public class SynchronizedDemo {
// 方法1 線程1將調用
public synchronized void method1() {
try {
System.out.println(Thread.currentThread().getName() + "執行method1方法,並獲取鎖,主線程睡眠4秒");
Thread.sleep(4000);
System.out.println(Thread.currentThread().getName() + "釋放鎖");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 方法2 線程2將調用
public synchronized void method2() {
System.out.println(Thread.currentThread().getName() + "執行method2方法");
}
public static void main(String[] args) throws Exception {
final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo.method1();
}
}, "線程1");
// 爲了保證線程1和2的執行順序我讓線程2延遲0.5秒再啓動
Thread.sleep(500);
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo.method2();
}
}, "線程2");
t1.start();
t2.start();
}
}
執行結果如下:
當我們把method2方法的synchronized去掉之後,再看執行結果:
從上面示例我們就能看出:當加了synchronized修飾之後線程1首先啓動並持有對象SynchronizedDemo的對象鎖,線程2啓動時,發現鎖被線程1佔用,處於等待狀態,等待3.4秒之後線程1釋放鎖,線程2獲得鎖,執行method2方法。本示例中,synchronized同步的是普通方法,所以持有的是當前實例對象鎖,如果是方法塊,鎖是synchronized包含的代碼塊。
我們去就提到過,synchronized是通過Monitor對象來實現同步的,那麼我們今天來對這點做個補充,我們看下Monitor是如何工作的,如下圖:
總結:任意一個線程對Object對象(synchronized修飾)的訪問,首先要獲得Object對象的監視器,如果獲取失敗,該線程將進入同步隊列等待,該線程狀態變爲阻塞(BLOCKED),當訪問Object的前(已經獲得鎖的線程)線程釋放了鎖,則喚醒阻塞隊列中的線程,使阻塞線程重新嘗試對監視器的獲取;
景點三:等待/通知
等待/通知機制指的是一個線程1調用了對象的wait()方法進入等待狀態,另一個線程2調用該對象的notifyAll()方法,線程1收到了通知之後從對象的wait()方法返回,進而執行後續的操作,兩個線程通過對象來完成交互,而對象上的wait和notify/notifyAll的關係就像一個開關信號一樣,用來完成等待方和通知方之間的交互工作。我們先來看看這幾個方法:
-
notify():通知一個在對象上等待的線程,由WAITING狀態變爲BLOCKING狀態,從等待隊列移動到同步隊列,等待CPU調度獲取該對象的鎖,當該線程獲取到了對象的鎖後,該線程從wait()方法返回;
-
notifyAll():通知所有等待在該對象上的線程,由WAITING狀態變爲BLOCKING狀態,等待CPU調度獲取該對象的鎖;
-
wait():調用該方法的線程進入WAITING狀態,並將當前線程放置到對象的等待隊列,只有等待另外線程的通知或被中斷纔會返回,(需要注意,調用wait()方法後,會釋放對象的鎖);
-
wait(long):超時等待一段時間,參數爲毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回;
-
wait(long,int):第一個參數爲毫秒,第二個參數爲納秒,對超時返回更細粒度的控制;
那麼這幾個方法如何使用呢?我們來看下面2個示例:
public class WaitAndNotifyDemo {
// 定義一個list屬性
private volatile static List list = new ArrayList();
// 提供一個add方法
public void add() {
list.add("justin");
}
// 獲取list的大小
public int size() {
return list.size();
}
public static void main(String[] args) {
final WaitAndNotifyDemo list2 = new WaitAndNotifyDemo();
final Object lock = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 使用synchronized對象鎖加鎖
synchronized (lock) {
System.out.println("t1啓動..");
for (int i = 0; i < 6; i++) {
// 往list中添加元素,當list中添加了5個元素之後,喚醒等待對象鎖的線程2
list2.add();
System.out.println("當前線程:" + Thread.currentThread().getName() + "添加了第"+(i+1)+"個元素..");
Thread.sleep(500);
if (list2.size() == 5) {
System.out.println("已經發出通知..");
lock.notify();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// synchronized對象鎖加鎖嗎,當list的大小不爲5時,就等待,wait會釋放lock鎖,t1才能獲得鎖
synchronized (lock) {
System.out.println("t2啓動..");
if (list2.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("當前線程:" + Thread.currentThread().getName() + "收到通知線程停止..");
throw new RuntimeException();
}
}
}, "t2");
// 爲了保證先啓動線程2執行等待,所以我給線程1加了個延遲0.5秒啓動
t2.start();
try {
Thread.sleep(500);
t1.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
執行結果如下:
我們再看另一個例子,利用等待/通知機制我們來自定義實現一個有界阻塞隊列ArrayBlockingQueue:
public class DefinedArrayBlockingQueue {
// 定義一個list
private final LinkedList<Object> list = new LinkedList<Object>();
// 用於原子操作計數器
private final AtomicInteger count = new AtomicInteger(0);
// 隊列最大值和最小值
private final int maxSize;
private final int minSize = 0;
// 用於synchronized對象鎖
private final Object lock = new Object();
public DefinedArrayBlockingQueue(int maxSize) {
this.maxSize = maxSize;
}
// 往隊列中加入元素
public void put(Object obj) {
synchronized (lock) {
// 當隊列中元素達到最大值就等待狀態
while (count.get() == maxSize) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(obj);
// 計數器+1
count.getAndIncrement();
System.out.println(" 元素 " + obj + " 被添加 ");
// 喚醒take方法的wait
lock.notify();
}
}
public Object take() {
Object temp = null;
synchronized (lock) {
while (count.get() == minSize) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 計數器-1
count.getAndDecrement();
temp = list.removeFirst();
System.out.println(" 元素 " + temp + " 被消費 ");
// 喚醒put方法裏面的wait
lock.notify();
}
return temp;
}
public int size() {
return count.get();
}
public static void main(String[] args) throws Exception {
// 定義一個自定義隊列,最大長度設置爲5
final DefinedArrayBlockingQueue m = new DefinedArrayBlockingQueue(5);
m.put("1");
m.put("2");
m.put("3");
m.put("4");
m.put("5");
System.out.println("當前元素個數:" + m.size());
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
m.put("6");
m.put("7");
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
m.take();
Thread.sleep(1000);
m.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t2");
t1.start();
Thread.sleep(1000);
t2.start();
}
}
運行結果:
我們再來分析下,等待喚醒機制,並做個總結,整個等待喚醒的過程如下:等待線程首先獲取對象鎖,然後調用對象的wait()方法,此時釋放對象鎖,由於等待線程釋放了對象鎖,喚醒線程隨後獲取到對象鎖,並調用對象的notify()方法,將等待隊列中的線程移到了同步隊列中,此時等待線程狀態的狀態變爲阻塞狀態,喚醒線程釋放了鎖之後,等待線程再次獲取到對象鎖,並從wait()方法返回繼續執行,過程如下圖:
景點四:管道輸入/輸出流
管道輸入輸出流和普通文件的輸入輸出不同之處在於,它是作用於線程之間的數據傳輸,以內存作爲傳輸媒介,主要包括下面4種具體實現:
-
PipedOutputStream(字節)
-
PipedInputStream(字節)
-
PipedReader(字符)
-
PipedWriter(字符)
我們來看下下面這個demo:
public class PipedDemo {
public static void main(String[] args) throws IOException {
// 定義一個輸入流
PipedWriter out = new PipedWriter();
// 定義一個輸出流
PipedReader in = new PipedReader();
// 輸入輸出建立連接
out.connect(in);
Thread printThread = new Thread(new PrintThread(in),"打印");
printThread.start();
int receive = 0;
try {
// 輸入鍵盤字符
System.out.println("請輸入任意字符,並按enter鍵發送:");
while ((receive = System.in.read()) != -1) {
out.write(receive);
}
}finally {
out.close();
}
}
static class PrintThread implements Runnable{
private PipedReader in;
// 讀取寫入的字符
public PrintThread(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 e) {
e.printStackTrace();
}
}
}
}
運行結果如下:
景點五:join方法
如果一個線程執行了thread.join()方法,就說明:當前主線程需要等待執行join方法的線程終止之後才從thread.join()中返回,主線程才繼續往下執行,join主要包括下面3個方法:
-
join源碼:
-
public final void join() throws InterruptedException { join(0); }
join超時返回源碼:
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; } } }
-
join超時返回源碼(更細粒度的控制超時時間):
public final synchronized void join(long millis, int nanos) throws InterruptedException { if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } if (nanos >= 500000 || (nanos != 0 && millis == 0)) { millis++; } join(millis); }
我們再看下如何使用join:
public class JoinDemo implements Runnable {
@Override
public void run() {
System.out.println("線程"+Thread.currentThread().getName()+":開始運行"+ DateUtil.getNowDate());
try{
Thread.sleep(5000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("線程"+Thread.currentThread().getName()+":結束運行"+DateUtil.getNowDate());
}
public static void main(String[] args) {
System.out.println("Main Thread Start..."+DateUtil.getNowDate());
JoinDemo joinDemo = new JoinDemo();
Thread t1 = new Thread(joinDemo,"t1");
Thread t2 = new Thread(joinDemo,"t2");
t1.start();
t2.start();
}
}
運行結果如下:
我們使用join之後再看看運行結果會怎樣:
使用join之後我們發現,線程1沒有返回之前,主線程一直在阻塞着,線程2一直沒有啓動,等到5秒線程1運行結束之後,主線程繼續,線程2啓動並執行。
景點六:ThreadLocal本地線程
線程變量,以一個ThreadLocal對象爲鍵,任意對象爲值得存儲結構,一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值,相當於每個線程都有一個獨立的本地對象,這塊內存是線程私有的。對其他線程是不可見的,線程可以利用這塊內存做自己的事,而不會受其他線程干擾。
我們來看下面的demo:
public class ThreadLocalDemo {
public static ThreadLocal<String> th = new ThreadLocal<String>();
public void setTh(String value){
th.set(value);
}
public void getTh(){
System.out.println(Thread.currentThread().getName() + "的值:" + this.th.get());
}
public static void main(String[] args) throws InterruptedException {
final ThreadLocalDemo ct = new ThreadLocalDemo();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
ct.setTh("Justin");
ct.getTh();
}
}, "線程1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
//ct.setTh("Java");
ct.getTh();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "線程2");
t1.start();
t2.start();
}
}
運行結果如下:結果很正常,對於同一個對象,線程1第一次賦值,線程2第二次賦值,分別輸出第一次和第二次的結果。
當我們把線程2賦值註釋起來,按道理是應該會拿到線程1賦的值,但是我們看下面的結果:很明顯線程2拿到個null,說明線程2沒獲取到線程1的值,這相當於,雖然是同一個對象,但是在兩個線程中卻有着不同的副本。