多线程:线程通信

当线程在运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,为了保证线程协调运行,java提供了一些线程通信机制。

一、等待唤醒机制

Object类提供了wait()notify()notifyAll()三个方法来实现线程通信机制,但这三个方法必须由同步监视器对象来调用,这样可以分成两种情况:

  1. 对于使用synchronized修饰的同步方法,因为该类的默认实例(this)是同步监视器所以在同步方法中可以直接调用这三个方法。
  2. 对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以在必须使用该对象调用这三个方法。

关于三个方法的解释:

  1. wait():导致当前线程等待,直到在其他线程调用该同步监视器的 notify()方法或notifyAll() 方法来唤醒该线程。调用wait()方法的当前线程会释放对该同步监视器的锁定。
  2. notify():唤醒在此监视器上等待的单个线程。如果所有线程都在同步监视器对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃此同步监视器上的锁定后(使用wait()方法),才能继续执行被唤醒的线程。
  3. notifyAll():唤醒在此监视器上等待的所有线程。只有当前线程放弃此同步监视器上的锁定后,才能继续执行被唤醒的线程。

下面使用银行存款取款的例子来演示用法:

public class Account {
	private String accountNo;
	private double balance;
	// 标识账户中是否有存款的flag
	private boolean flag = false;

	public Account(String accountNo, double balance) {
		this.accountNo = accountNo;
		this.balance = balance;
	}

	@Override
	public int hashCode() {
		return accountNo.hashCode();
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Account other = (Account) obj;
		if (accountNo == null) {
			if (other.accountNo != null)
				return false;
		} else if (!accountNo.equals(other.accountNo))
			return false;
		return true;
	}

	public double getBalance() {
		return balance;
	}

	public synchronized void withdraw(double drawAccount) {
		try {
			if (!flag) {
				wait();
			} else {
				System.out.println(Thread.currentThread().getName() + "取款成功!取出:" + drawAccount + "元!");
				balance -= drawAccount;
				System.out.println("\t余额为:" + balance + "元!");
				flag = false;
				notifyAll();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public synchronized void deposit(double depositAccount) {
		try {
			if (flag) {
				wait();
			} else {
				System.out.println(Thread.currentThread().getName() + "存款成功!存入:" + depositAccount + "元!");
				balance += depositAccount;
				System.out.println("\t余额为:" + balance + "元!");
				flag = true;
				notifyAll();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

模拟取款操作的线程:

public class DrawThread extends Thread {
	private Account account;
	private double drawAccount;

	public DrawThread(String name, Account account, double drawAccount) {
		super(name); // 设置线程名
		this.account = account;
		this.drawAccount = drawAccount;
	}

	public void run() {
		for (int i = 0; i < 100; i++) {
			account.withdraw(drawAccount);
		}
	}
}

模拟存款操作的线程:

public class DepositThread extends Thread {
	private Account account;
	private double depositAccount;

	public DepositThread(String name, Account account, double depositAccount) {
		super(name);
		this.account = account;
		this.depositAccount = depositAccount;
	}

	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			account.deposit(depositAccount);
		}
	}
}

测试类:

public class DrawTest {
	
	public static void main(String[] args) {
		Account account = new Account("账户", 0);
		new DrawThread("取钱者", account, 888).start();
		new DepositThread("存款者", account, 888).start();
	}
}

运行结果:

存款者存款成功!存入:888.0元!
	余额为:888.0元!
取钱者取款成功!取出:888.0元!
	余额为:0.0元!
存款者存款成功!存入:888.0元!
...

可以看到存款线程与取款线程交替执行。

二、使用Lock、Condition控制线程通信

如果程序不使用synchronized关键字来保证线程同步,而是直接使用Lock对象,则系统不存在隐式的同步监视器,也就是不能使用wait()、notify()、notifyAll()方法进行线程通信了。

当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,也可以唤醒其他处于等待的线程。Lock替代同步代码块或同步方法,Condition则替代同步监视器的功能。

Condition实例被绑定在一个Lock对象上,要获得特定Lock的Condition实例,需要调用Lock对象的newCondition()方法即可。Condition类提供如下三个方法:

  1. await():类似于隐式同步监控器上的wait()方法,直到在其他线程调用该Condition的 signal()方法或signalAll() 方法来唤醒该线程。
  2. signal():唤醒在此Lock对象上等待的单个线程。如果所有线程都在Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃此同步监视器上的锁定后(使用await()方法),才能继续执行被唤醒的线程。
  3. signalAll():唤醒在此监视器上等待的所有线程。只有当前线程放弃此Lock对象上的锁定后,才能继续执行被唤醒的线程。

下面继续用刚刚的例子演示用法:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
	private String accountNo;
	private double balance;
	// 标识账户中是否有存款的flag
	private boolean flag = false;
	private Lock lock = new ReentrantLock();
	private Condition condition = lock.newCondition();

	public Account(String accountNo, double balance) {
		this.accountNo = accountNo;
		this.balance = balance;
	}

	@Override
	public int hashCode() {
		return accountNo.hashCode();
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Account other = (Account) obj;
		if (accountNo == null) {
			if (other.accountNo != null)
				return false;
		} else if (!accountNo.equals(other.accountNo))
			return false;
		return true;
	}

	public double getBalance() {
		return balance;
	}

	public void withdraw(double drawAccount) {
		lock.lock();
		try {
			if (!flag) {
				condition.await();
			} else {
				System.out.println(Thread.currentThread().getName() + "取款成功!取出:" + drawAccount + "元!");
				balance -= drawAccount;
				System.out.println("\t余额为:" + balance + "元!");
				flag = false;
				condition.signalAll();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void deposit(double depositAccount) {
		lock.lock();
		try {
			if (flag) {
				condition.await();
			} else {
				System.out.println(Thread.currentThread().getName() + "存款成功!存入:" + depositAccount + "元!");
				balance += depositAccount;
				System.out.println("\t余额为:" + balance + "元!");
				flag = true;
				condition.signalAll();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
}

运行结果与前面一样。

三、使用阻塞队列(BlockingQueue)控制线程

Java 5 提供了一个BlockingQueue接口(Queue的子接口),它的特征:当生产者线程试图从BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;

当消费者线程试图从BlockingQueue中取出元素时,如果该队列为空,则该线程被阻塞。

BlockingQueue提供如下两个支持阻塞的方法:

  • put(E e):尝试把E元素放入BlockingQueue中,如果该队列已满,则该线程被阻塞。
  • take():尝试从BlockingQueue的头部取出元素,如果该队列为空,则该线程被阻塞。

BlockingQueue包含的方法之间的对应关系:

 抛出异常不同返回值阻塞线程指定超时时长
队尾插入元素add(e)offer(e)put(e)offer(e,time,unit)
队头删除元素remove()poll()take()poll(time,unit)
获取、不删除元素element()peek()

下面用BlockingQueue实现生产者、消费者之间的线程通信

生产者线程:

import java.util.concurrent.BlockingQueue;

public class Producer extends Thread {
	private BlockingQueue
   
   
    
     bq;

	public Producer(BlockingQueue
    
    
     
      bq) {
		this.bq = bq;
	}

	@Override
	public void run() {
		String[] strArr = { "qwe", "asd", "zxc" };
		for (int i = 0; i < 999; i++) {
			System.out.println(getName() + "生产者准备生产!");
			try {
				Thread.sleep(200);
				bq.put(strArr[i % 3]);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(getName() + "生产完成:" + bq);
		}
	}
}
    
    
   
   

消费者线程:

import java.util.concurrent.BlockingQueue;

public class Consumer extends Thread {
	public BlockingQueue
   
   
    
     bq;

	public Consumer(BlockingQueue
    
    
     
      bq) {
		this.bq = bq;
	}

	@Override
	public void run() {
		while (true) {
			System.out.println(getName() + "消费者准备消费!");
			try {
				Thread.sleep(200);
				bq.take();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(getName() + "消费完成:" + bq);
		}
	}
}
    
    
   
   

BlockingQueue测试类:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class BlockingQueueTest {
	public static void main(String[] args) {
		BlockingQueue
   
   
    
     bq = new ArrayBlockingQueue<>(1);
		new Producer(bq).start();
		new Producer(bq).start();
		new Producer(bq).start();
		new Consumer(bq).start();
	}
}
   
   

运行结果:

Thread-0生产者准备生产!
Thread-2生产者准备生产!
Thread-1生产者准备生产!
Thread-3消费者准备消费!
Thread-2生产完成:[qwe]
Thread-2生产者准备生产!
Thread-3消费完成:[]
Thread-3消费者准备消费!
Thread-1生产完成:[qwe]
Thread-1生产者准备生产!
Thread-3消费完成:[]
...

可以看出BlockingQueue队列中生产者每次只能放进一个元素,而且必须在消费者消费完之前阻塞生产者线程,只有消费完后才能继续下一次生产。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值