当线程在系统内部运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但我们可以通过一些机制来保证线程协调运行
线程的协调运行
为了实现这种功能,可以借助Object类提供的wait().notify()和notifyAll()三个方法,这三个方法并不属于Thread类,而是属于Object类,所以这三个方法必须由同步监视器来对象来调用,分为以下两种情况:
- 对于使用synchronized修饰的同步方法,因为同步监视器是this,所以可以直接使用这三个方法
- 对于使用synchronized修饰的同步代码块,必须使用同步对象来调用这三个对象
关于这三个方法的解释如下:
- wait(): 导致当前线程等待,直到其他线程调用该同步监视器notify()或notifyAll()方法来唤醒该线程.无时间参数的wait()(一直等待,直到其他线程通知),带时间参数的wait()(等待指定时间后自动苏醒).调用wait()方法的当前线程会释放对该同步监视器的锁定.
- notify(): 唤醒在此同步监视器上等待的单个线程,如果所有线程都在该监视器上等待,则任意唤醒其中一个
- notifyAll(): 唤醒所有在此同步监视器上等待的线程
(注意被notify唤醒的线程会从上次wait的地方开始执行,并不是从头开始执行)
借用之前的一片文章线程安全中的银行取钱问题来演示,现在有两条线程,一个取钱,一个存钱;这两个线程交替执行,任何一个不允许连续执行两次,程序中用一个flag来标是否有存款
账户
public class Account {
private int account;
private String name;
private Boolean flag = false;
public Account(String name, int account) {
this.account = account;
this.name = name;
}
public synchronized void draw(int money) {
try {
while (!flag) {//flag=false表示还没有人把钱存进去,则阻塞线程
wait();
}
account = account - money;
System.out.println(Thread.currentThread().getName() + "取钱成功!" + " 取出" + money + ",剩余" + account);
flag = false;
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void deposit(int money) {
try {
while (flag) {//flag=true表示已经有人把钱存进去了,不可以再存了,阻塞线程
wait();
}
account = account + money;
System.out.println(Thread.currentThread().getName() + "存钱成功!" + " 存入" + money + ",剩余" + account);
flag = true;
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
注意wait那里用一个无限循环来判断,因为被notify唤醒时会从wait的地方从执行,所以还要判断flag
取钱线程
public class DrawThread extends Thread{
private Account account;
private int draw;
public DrawThread( String threadname, Account account,int draw){
super(threadname);
this.account=account;
this.draw=draw;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
account.draw(draw);
}
}
}
存钱线程
public class DepositThread extends Thread{
private Account account;
private int deposit;
public DepositThread(String name,Account account,int deposit){
super(name);
this.account=account;
this.deposit=deposit;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
account.deposit(deposit);
}
}
}
测试
public class TestThread {
public static void main(String []args){
Account account=new Account("Eric",600);
DrawThread A=new DrawThread("A",account,600);
A.start();
DrawThread B=new DrawThread("B",account,600);
B.start();
DepositThread C=new DepositThread("C",account,600);
C.start();
DepositThread D=new DepositThread("D",account,600);
D.start();
}
}
输出结果:
D存钱成功! 存入600,剩余1200
A取钱成功! 取出600,剩余600
D存钱成功! 存入600,剩余1200
B取钱成功! 取出600,剩余600
D存钱成功! 存入600,剩余1200
A取钱成功! 取出600,剩余600
结果是正确的,取钱和存钱交替执行,也没有出现账户余额不正常的问题.
使用条件变量控制协调
如果程序使用Lock对象来保证同步,则系统中不存在同步监视器对象,也就不能使用wait(),notify(),notifyAll()来协调线程的运行.
当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程.
Condition实例实质被绑定在一个Lock对象上.要获得特定Lock对象的Condition实例,调用Lock对象newCondition()即可.Condition提供如下三个方法来协调线程
- await(): 类似于wait()方法,导致当前线程等待,await()有更多的变体,ondition.awaitNanos(long nanosTimeout);ondition.awaitUntil(Date deadline);ndition.awaitUninterruptibly()等可以完成更丰富的操作
- singal(): 唤醒在此Lock对象上等待的单个线程.类似notify任意唤醒其中的一个线程
- singalAll(): 唤醒在此Lock对象上等待的所有线程,类似notifyAll
接下来演示一下,修改Account,使用Lock对象来控制同步,Condition对象来控制线程的协调运行
public class Account {
private int account;
private String name;
private Boolean flag = false;
private final ReentrantLock lock=new ReentrantLock();
private final Condition condition=lock.newCondition();
public Account(String name, int account) {
this.account = account;
this.name = name;
}
public void draw(int money) {
lock.lock();
try {
while (!flag) {//flag=false表示还没有人把钱存进去,则阻塞线程
condition.await();
}
account = account - money;
System.out.println(Thread.currentThread().getName() + "取钱成功!" + " 取出" + money + ",剩余" + account);
flag = false;
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void deposit(int money) {
lock.lock();
try {
while (flag) {//flag=true表示已经有人把钱存进去了,不可以再存了,阻塞线程
condition.await();
}
account = account + money;
System.out.println(Thread.currentThread().getName() + "存钱成功!" + " 存入" + money + ",剩余" + account);
flag = true;
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
测试运行正确,Lock和Condition搭配显示的进行了控制
使用管道流
前面的两种方法不算是线程之间真正的通信,称之为协调运行策略更准确.如果两条线程之间进行更多的信息交互,则可以考虑使用管道流进行通信,管道流有3种存在形式
- PipedInputStream和PipedOutputStream 字节流
- PipeReader和PipeWriter 字符流
- Pipe.SinkChannel和Pipe.SourceChannel
使用管道流实现多线程通信可按照一下步骤
- 使用new创建管道输入流和管道输出流.
- 使用管道输入流或管道输出流的connect方法把两个输入流和输出流连接起来.
- 将管道输入,输出流分别传入两个线程
- 两个线程根据自己得到的管道输入流, 管道输出流进行通信
下面来进行演示
public class ReaderThread extends Thread {
private PipedReader pipedReader;
private BufferedReader bufferedReader;
public ReaderThread(PipedReader pipedReader) {
this.pipedReader = pipedReader;
this.bufferedReader = new BufferedReader(pipedReader);
}
@Override
public void run() {
String line = null;
try {
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class WriterThread extends Thread {
private PipedWriter pipedWriter;
private String[] args =
{"我是小红",
"ABCDEFG",
"我是小李",
"abcdefg"};
public WriterThread(PipedWriter pipedWriter) {
this.pipedWriter = pipedWriter;
}
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
pipedWriter.write(args[i % 4] + "\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
pipedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class CommunicationTest {
public static void main(String []args){
PipedWriter pipedWriter;
PipedReader pipedReader;
try{
pipedWriter=new PipedWriter();
pipedReader=new PipedReader();
//连接两个管道流
pipedWriter.connect(pipedReader);
new ReaderThread(pipedReader).start();
new WriterThread(pipedWriter).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果
ABCDEFG
我是小李
abcdefg
我是小红
ABCDEFG
我是小李
abcdefg
其中管道流的输入输出和java的IO操作一样