線程通信
對上次多線程編程步驟補充(中部):
- 創建資源類,在資源類中創建屬性和操作方法
- 在資源類裏面操作
- 判斷
- 幹活
- 通知
- 創建多個線程,調用資源類的操作方法
線程通信的實現例子:
兩個線程,實現對一個初始變量爲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併發編程》