Java多线程(线程通信)

当程序在系统中运行时,线程的调度具有一定的随机性,程序通常无法准确的控制线程的轮换执行,我们可以通过一些机制来保证线程协调运行。线程之间的协调运行就称为通信。

>传统线程通信

假设系统中有两个线程,一个代表存款者,一个代表取款者。系统对他们有特殊要求,即要求存款者与取款者不断地重复存款、取钱的动作,而且要求每当存款者将钱存入指定账户后取款者立即取出钱,不允许存款者连续两次存钱,也不允许取钱者连续两次取钱。

为实现此功能,可以借助Object类提供的wait()、notify()和notifyAll()3个方法,这3个方法必须由同步监视器对象来调用,同步监视器可以分以下两种情况:

  • 使用synchronized修饰的同步方法,同步监视器就是当前实例对象,所以此方法由this直接调用
  • 使用synchronized修饰的同步代码块,同步监视器是synchronzied后括号里的对象,所以必须使用该对象调用这3个方法。

三个方法解释如下:

  • wait():导致当前线程进入等待(阻塞状态)直到其他线程调用该同步监视器的notify()方法或者notifyAll()方法来唤醒该线程。该wait()方法有三种形式——无时间参数的wait,一直等待,直到其他线程的通知;带毫秒参数的wait和带毫秒、微秒参数的wait,这两种方法都是等待指定时间后自动苏醒。调用wait方法的当前线程会释放对该同步监视器的锁定。
  • notify():唤醒此同步监视器上等待的单个线程。如果所有的线程都在此同步监视器上等待着,则会唤醒其中一个线程。选择是任意的。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
  • notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后才可以执行被唤醒的线程。

这样看来,是同步监视器通过这三个方法来告知,并发执行的线程 “你先等一等”  或者 “你们醒一醒,等这个线程释放了(推出synchronized代码区),某个线程就可以执行了”:

(    如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
       当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或notify()方法(只随机唤醒一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
       优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。    )

程序中可以通过一个旗标来标识账户中是否已经有存款,当旗标为false时,标识账户中没有存款,存款者线程可以向下执行,当存款者把钱存入账户后,将旗标设置为true,表调用notify()或者notifyAll()方法来唤醒其他线程;当存款者线程进入线程体后,如果旗标为true就调用wait()方法让线程等待。

当旗标为true时,表明账户中已经有了存款,则取款者线程就可以向下执行,当取款者把钱从账户中取出后,将镖旗设置为false,并调用notify()或者notifyAll()方法来唤醒其他线程;当取钱者线程进入线程体后,入股旗标为false就调用wait()方法让该线程等待。

本程序为Account类提供了draw()和deposit()两个方法,分别对应该账户的取钱、存款操作。因为这两个方法可能需要并发修改Account对象的balance成员变量,因此这两个方法都使用synchrozied修饰进行同步控制,除此之外这两个方法还使用wait()、notifyAll来控制线程的协作。

账户(同步监视器):

public class Account {
	private String accountNO;
	private double balance;
	private boolean flag = false;

	public Account() {
	}

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

	public String getAccountNO() {
		return accountNO;
	}

	public void setAccountNO(String accountNO) {
		this.accountNO = accountNO;
	}

	public double getBalance() {
		return balance;
	}

	public void setBalance(double balance) {
		this.balance = balance;
	}

	public boolean isFlag() {
		return flag;
	}

	public void setFlag(boolean flag) {
		this.flag = flag;
	}

	public synchronized void drawMoney() {
		if (flag) {
			System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:800");
			this.setBalance(this.balance - 800);
			System.out.println("当前余额为:" + this.getBalance());
			this.flag = false;
			this.notifyAll();// 唤醒在此同步监视器(account对象)上等待的其他线程
		} else {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public synchronized void deposit() {
		if (flag) {
			try {
				this.wait();// this指account对象,通过account对象的wait方法让线程进入等待
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		} else {
			System.out.println(Thread.currentThread().getName() + "存钱成功:800!");
			this.setBalance(this.balance + 800);
			System.out.println("当前余额为:" + this.getBalance());
			this.flag = true;
			this.notifyAll();
		}
	}

}

取钱线程:

public class DrawThread extends Thread{
    private Account account;
	
	public DrawThread(Account account) {
		super();
		this.account = account;
	}

	public void run(){
		for (int i = 0; i < 20; i++) {
			this.account.drawMoney();
		}
	}
}

存钱线程:

public class DepositThread extends Thread {
    private Account account;
	
	public DepositThread(Account account) {
		super();
		this.account = account;
	}

	public void run(){
		for (int i = 0; i < 20; i++) {
			this.account.deposit();
		}
	}
}

测试类:

public class Run {
	public static void main(String[] args) {
		Account account=new Account("123456", 0);
		new DrawThread(account).start();
		new DepositThread(account).start();	
	}
}

运行结果:

Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0

>锁池(monitor)和等待池(waitset)

锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

>永远不要在循环的外面调用wait():在一个合格的多线程程序中,这是必须的

synchronized (obj) {
     while ()
         obj.wait();
     ... // Perform action appropriate to condition
 }

这样做的意思是只要还没有达到,该线程就一直等待,直到满足条件,可以避免不必要的唤醒——“虚假唤醒”。

比如调用notifyAll,唤醒对应锁上的所有线程,那所有wait的线程,就从wait方法返回了,但自己程序里的逻辑,是不是期望这种结果呢?所以针对每个wait,还要重复判断一下,确定此时是不是真的该醒来了,所以用for/while,而不是if判断。

>使用Condition控制线程通信

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

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

Condition将同步监视器方法(wait、notify、notifyAll)分解成截然不同的对象,以便通过将这些对象与Lock组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。

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

  • await():类似于隐式同步监视器上的wait()方法,导致当前线程等待,直到其他线程调用该Condition的singal()方法或singalAll()方法来唤醒该线程。该await()方法有更多的变体,如long awaitNanos(long nanosTimeout)、void awaitUninterruptibly()、awaitUntil(Date deadline)等,可以完成更丰富的等待操作。
  • signal():唤醒在此Lock对象上等待的单个线程。如果多有的线程都在该Lock对象上等待,则会唤醒其中一个线程。选择是任意的,只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。
  • signalAll():唤醒在此Lock对象上等待的所有线程,只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的下城。

将上面的代码改写:

public class Account {
	private String accountNO;
	private double balance;
	private boolean flag = false;
	private final ReentrantLock lock = new ReentrantLock();
	private Condition cond = lock.newCondition();

	public Account() {
	}

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

	public String getAccountNO() {
		return accountNO;
	}

	public void setAccountNO(String accountNO) {
		this.accountNO = accountNO;
	}

	public double getBalance() {
		return balance;
	}

	public void setBalance(double balance) {
		this.balance = balance;
	}

	public boolean isFlag() {
		return flag;
	}

	public void setFlag(boolean flag) {
		this.flag = flag;
	}

	public void drawMoney() {
		lock.lock();
		try {
			if (flag) {
				System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:800");
				this.setBalance(this.balance - 800);
				System.out.println("当前余额为:" + this.getBalance());
				this.flag = false;
				cond.signalAll();
			} else {
				try {
					cond.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		} catch (Exception e) {
		} finally {
			lock.unlock();
		}
	}

	public void deposit() {
		lock.lock();
		try {
			if (flag) {
				try {
					cond.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			} else {
				System.out.println(Thread.currentThread().getName() + "存钱成功:800!");
				this.setBalance(this.balance + 800);
				System.out.println("当前余额为:" + this.getBalance());
				this.flag = true;
				cond.signalAll();
			}
		} catch (Exception e) {
		} finally {
			lock.unlock();
		}

	}

}

>阻塞队列控制线程通信

Java5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程同步的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞,等待队列可用;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞,等待队列非空。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

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

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

BlockingQueue继承了Queue接口,当然也可使用Queue接口中的方法。这些方法归纳起来可分为如下三类:

  1. 在队列尾部插入元素。包括add、offer、put方法,当队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。
  2. 在队列头部删除并返回该删除的元素。包括remove、poll、take方法,当队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。
  3. 在队列头部取出但不删除元素,包括element、peek方法,当队列已空时,这两个方法分别抛出异常、返回false。

BlockingQueue包含如下5个实现类:

  1. ArrayBlockingQueue:基于数组实现的BlockingQueue队列。
  2. LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
  3. PriorityBlockingQueue:它并不是标准的阻塞队列。与前面介绍的PriorityQueue类似,该队列调用了remove、poll、take方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。
  4. SynchronousQueue:同步队里,该队列的存、取操作必须交替进行。
  5. DelayQueue:它是一个特殊BlockingQueue,底层基于PriorityBlockingQueue实现。不过DelayQueue要求集合元素都实现Delay接口,DelayQueue根据集合元素的getDelay返回值进行排序。

测试代码:一个生产线程,一个消费线程,并发操作阻塞队列:

public class MyBlockingQueue extends ArrayBlockingQueue{

	public MyBlockingQueue(int capacity) {
		super(capacity);
	}
	
	public Double take() throws InterruptedException{
		Double resule =super.take();
		System.out.println(Thread.currentThread().getName()+"取走集合元素!");
		return resule;	
	}

	public void put(Double e) throws InterruptedException{
		super.put(e);
		System.out.println(Thread.currentThread().getName()+"放入新的集合元素!");
	}

}

存入: 

public class DepositThread extends Thread {
	private BlockingQueue queue;

	public DepositThread(String name, BlockingQueue queue) {
		super(name);
		this.queue = queue;
	}

	public void run() {
		while (true) {
			try {
				Thread.sleep(500);
				this.queue.put(new Double(800));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

}

取出:

public class ATMThread extends Thread {
	private BlockingQueue queue;

	public ATMThread(String name, BlockingQueue queue) {
		super(name);
		this.queue = queue;
	}

	public void run() {
		while (true) {
			try {
				Thread.sleep(2000);
				this.queue.take();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
		MyBlockingQueue myque = new MyBlockingQueue(10);
		new ATMThread("取出线程", myque).start();
		new DepositThread("存入者", myque).start();
	}
}

 

你可能感兴趣的:(Java,多线程)