java高并发编程之线程间通信

5.1 同步阻塞与异步非阻塞

异步非阻塞的优势非常明显,首先客户端不用等到结果处理结束之后才能返回,从而提高了系统的吞吐量和并发量;其次若服务端的线程数量在一个可控的范围之内是不会导致太多的CPU上下文切换从而带来额外的开销的;再次服务端线程可以重复利用,这样就减少了不断创建线程带来的资源浪费。但是异步处理的方式同样也存在缺陷,比如客户端想要得到结果还需要再次调用接口方法进行查询。
5.2 单线程通信

5.2.1wait和notify方法详解

wait三个重载方法源码:

 public final void wait() throws InterruptedException {
        wait(0L);
    }
.......................
 public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
        if (timeoutMillis < 0) {
            throw new IllegalArgumentException("timeoutMillis value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0 && timeoutMillis < Long.MAX_VALUE) {
            timeoutMillis++;
        }

        wait(timeoutMillis);
    }
    .......................
    public final native void wait(long timeoutMillis) throws InterruptedException;

等待/通知机制的实现:
方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法用于将当前线程置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。在调用wait()之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步代码块中调用wait()方法。在执行wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时没有持有适当的锁,则会抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch语句进行捕获异常。

方法notify()也要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,对其发出通知notify,并使他等待获取该对象的对象锁。
需要说明的是,在执行notify方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马山获取该对象锁,要等待执行notify方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才可以获取该对象锁。
当第一个获得了该对象锁的wait线程运行完毕后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态的线程由于没有得到该对象的通知,还会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。
用一句话来总结一个wait和notify:wait使线程停止运行,而notify使停止的线程继续运行。

wait():可以使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。
notify():可以随机唤醒等待队列中等待同一共享资源的“一个”线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知“一个”线程。
notifyAll():可以使所有正在等待队列中等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,因为这要取决于JVM虚拟机的实现。

当wait()状态是,调用线程的interrupt()方法会出现InterruptedException异常。

通知过早问题

就是notify()方法先执行了,这样wait()方法会永远不能唤醒。除非wait(long),等待一定时间,自动唤醒。
或者用一个boolean变量控制,当调用notify方法后,改变boolean值,这样调用就不用调用wait方法。
5.2.2 关于wait和notify的注意事项

1.wait方法是可中断方法,这也就意味着,当线程一旦调用了wait方法进入阻塞状态,其他线程是可以使用interrupt方法将其打断的。可中断方法被打断后会收到中断异常InterruptedException,同时interrupt标识也会被擦除。
2.线程执行了某个对象的wait方法以后,会加入与之对应的wait set中,每一个对象的monitor都有一个与之关联wait set
3.当线程进入wait set之后,notify方法可以可以将其唤醒,也就是从wait set中弹出,同时中断wait中的线程也会将其唤醒。
4.必须在同步方法中使用wait和notify方法,因为执行wait和notify的前提条件时必须持有同步方法的monitor的所有权。
5.同步代码的monitor必须与执行wait notify方法的对象一致,简单地说就是用哪个对象的monitor进行同步,就只能用哪个对象进行wait和notify操作。运行下面代码中的任何一个方法,同样都会抛出IllegalMonitorStateException

5.2.4 wait和sleep的区别

1.wait和sleep方法都可以使线程进入阻塞状态
2.wait和sleep方法均是可中断方法,被中断后都会收到中断异常
3.wait是Object的方法,而sleep是Thread特有的方法
4.wait方法的执行必须在同步方法中进行,而sleep则不需要。
5.线程在同步方法中执行sleep方法时,并不会释放monitor锁,而wait方法则会释放monitor锁
6.sleep方法短暂休眠之后会主动退出阻塞,而wait方法(没有指定时间)则需要被其他线程中断后才能退出阻塞。
5.3 多线程通信

线程休息室wait set
每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程。一个线程被唤醒后,才会进入就绪队列,等待CPU的调度;反之,一个线程被wait后,就会进入阻塞队列,等待下一次被唤醒。
5.4 synchronized关键字的缺陷

1.无法控制阻塞时长
2.阻塞不可中断

5.5 通过管道进行线程间的通信

在Java语言中提供了各种各样的输入/输出流Stream,使我们能够很方便对数据进行操作,其中管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读取数据。通过使用管道,实现不同线程间的通信。

在java中JDK提供了四个类来使线程间可以通信:
1.PipeInputStream 和PipeOutputStream
2.PipeReader和PipedWriter

管道字节流:

public class WriteData  {
	public void writeMethod(PipedOutputStream out) {
		System.out.println("write:");
		for(int i =0;i<300;i++) {
			String outData = ""+(i+1);
			try {
				out.write(outData.getBytes());
				System.out.print(outData);
				
			} catch (IOException e) {
				
				e.printStackTrace();
			}
			
		}
		System.out.println();
		try {
			out.close();
		} catch (IOException e) {
			
			e.printStackTrace();
		}
	}

}
.........................
public class ReadData {
	public void readMethod(PipedInputStream input) {
		System.out.println("read:");
		byte [] byteArray = new byte[20];
		int readLength;
		try {
			readLength = input.read(byteArray);
			while(readLength!=-1) {
				String newData = new String(byteArray,0,readLength);
				System.out.print(newData);
				readLength = input.read(byteArray);
				
			}
			System.out.println();
			input.close();
			
		} catch (IOException e) {
			
			e.printStackTrace();
		}
		
	}

}
.......................................
public class ThreadWrite extends Thread{
	private WriteData write;
	private PipedOutputStream out;
	public ThreadWrite(WriteData write,PipedOutputStream out) {
		super();
		this.write = write;
		this.out = out;
	}
	public void run() {
		write.writeMethod(out);
	}

}
.......................................
public class ThreadRead extends Thread{
	private ReadData read;
	private PipedInputStream input;
	public ThreadRead(ReadData read,PipedInputStream input) {
		super();
		this.read = read;
		this.input = input;
	}
	public void run() {
		read.readMethod(input);
	}
 
}
..........................................
public class Test {
	public static void main(String[] args) {
		WriteData writeData = new WriteData();
		ReadData readData = new ReadData();
		PipedInputStream inputStream = new PipedInputStream();
		PipedOutputStream outputStream = new PipedOutputStream();
		try {
			outputStream.connect(inputStream);
			ThreadRead threadRead = new ThreadRead(readData,inputStream);
			threadRead.start();
			Thread.sleep(2000);
			ThreadWrite threadWtite = new ThreadWrite(writeData,outputStream);
			threadWtite.start();
		} catch (IOException e) {
			
			e.printStackTrace();
		} catch (InterruptedException e) {
			
			e.printStackTrace();
		}
	}

}
运行结果:
read:
write:
123456789101112。。。
123456789101112。。。

使用代码inputStream.connect(outputStream)或outputStream.connect(inputStream)的作用使两个Stream之间产生通信链接,这样才可以将数据进行输出与输入。

5.2.3 实战:等待/通知之交叉备份

创建20个线程,其中10个线程是将数据备份到A数据库,另外10个线程备份到B数据库中,并且备份A数据库和B数据库是交叉进行的。

public class DBTools {
	volatile private boolean prevIsA = false;
	public synchronized void backupA() {
		
			try {
			while(prevIsA == true) {
				wait();
			}
			for(int i = 0;i<1;i++) {
				System.out.println("数据库A***");
			}
			prevIsA = true;
			notifyAll();
			} catch (InterruptedException e) {
				
				e.printStackTrace();
			}
	}
	public synchronized void backupB() {
		try {
			while(prevIsA == false) {
				wait();
			}
			for(int i = 0;i<1;i++) {
				System.out.println("数据库B###");
			}
			prevIsA = false;
			notifyAll();
			} catch (InterruptedException e) {
				
				e.printStackTrace();
			}
		
	}

}
........................................
public class BackupA extends Thread{
	private DBTools dbtools;
	public BackupA(DBTools dbtools) {
		super();
		this.dbtools = dbtools;
	}
	public void run() {
		dbtools.backupA();
	}

}
.........................
public class BackupB extends Thread{
	private DBTools dbtools;
	public BackupB(DBTools dbtools) {
		super();
		this.dbtools = dbtools;
	}
	public void run() {
		dbtools.backupB();
	}

}
..........................................
public class Test {
 public static void main(String[] args) {
	DBTools dbtools = new DBTools();
	for(int i = 0;i<20;i++) {
		BackupB output = new BackupB(dbtools);
		output.start();
		BackupA output2 = new BackupA(dbtools);
		output2.start();
	}
 
}
}
运行结果:
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###
数据库A***
数据库B###

你可能感兴趣的:(Java并发编程)