Java面向对象系列[v1.0.0][线程通信]

当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但Java也提供了一些机制用于保证线程协调运行

传统的线程通信

假设两个线程一个负责存款一个负责取款,要求是存款和取钱不断的重复,并存完立即取出,不允许连续两次存款和连续两次取钱
Object类提供了wait()、notify()和notifyAll()三个方法,必须使用同步监视器对象来调用这三个方法,分为两种情况:
1、对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法
2、对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象来调用这3个方法

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

实现逻辑:

  • 通过一个Flag标记账户中是否有存款,当Flog为false时,表明账户中没有存款,存款线程可以向下执行,当存款存入账户后,将Flag设置为true,并调用notify()或者notifyAll()方法来唤醒其他线程(取款);当存款线程进入线程体后,如果Flag为true就调用wait()方法让该线程等待
  • 如果Flag为true,表明账户中已经有存款了,则取钱线程可以向下执行,当取钱线程把钱从账户中取出,则将Flag设置为Flase,并调用notify()或者notifyAll()方法来唤醒其他线程;当取钱线程进入线程体,如果Flag为false就调用wait()方法让该线程等待
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;
	}

	// accountNo的setter和getter方法
	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}
	// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
	public double getBalance()
	{
		return this.balance;
	}

	public synchronized void draw(double drawAmount)
	{
		try
		{
			// 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
			if (!flag)
			{
				wait();
			}
			else
			{
				// 执行取钱
				System.out.println(Thread.currentThread().getName()
					+ " 取钱:" + drawAmount);
				balance -= drawAmount;
				System.out.println("账户余额为:" + balance);
				// 将标识账户是否已有存款的旗标设为false。
				flag = false;
				// 唤醒其他线程
				notifyAll();
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
	}
	public synchronized void deposit(double depositAmount)
	{
		try
		{
			// 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
			if (flag)             
			{
				wait();
			}
			else
			{
				// 执行存款
				System.out.println(Thread.currentThread().getName()
					+ " 存款:" + depositAmount);
				balance += depositAmount;
				System.out.println("账户余额为:" + balance);
				// 将表示账户是否已有存款的旗标设为true
				flag = true;
				// 唤醒其他线程
				notifyAll();
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
	}

	// 下面两个方法根据accountNo来重写hashCode()和equals()方法
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if (this == obj)
			return true;
		if (obj != null
			&& obj.getClass() == Account.class)
		{
			var target = (Account) obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}

对存款线程而言,当程序进入deposit()方法后,如果flag为true则表明账户中已有存款,程序调用wait()方法阻塞,否则程序向下执行存款操作,当存款操作执行完成后,系统将flag设为true,然后调用notifyAll()方法来唤醒其他被阻塞的线程,此时如果系统中还有存款线程,存款线程也会被唤醒,但执行到判断Flag的时候仍旧会在此阻塞,只有执行draw()方法的取钱线程才可以向下执行,反过来也是如此

public class DrawThread extends Thread
{
	// 模拟用户账户
	private Account account;
	// 当前取钱线程所希望取的钱数
	private double drawAmount;
	public DrawThread(String name, Account account,
		double drawAmount)
	{
		super(name);
		this.account = account;
		this.drawAmount = drawAmount;
	}
	// 重复100次执行取钱操作
	public void run()
	{
		for (var i = 0; i < 100; i++)
		{
			account.draw(drawAmount);
		}
	}
}

public class DepositThread extends Thread
{
	// 模拟用户账户
	private Account account;
	// 当前取钱线程所希望存款的钱数
	private double depositAmount;
	public DepositThread(String name, Account account,
		double depositAmount)
	{
		super(name);
		this.account = account;
		this.depositAmount = depositAmount;
	}
	// 重复100次执行存款操作
	public void run()
	{
		for (var i = 0; i < 100; i++)
		{
			account.deposit(depositAmount);
		}
	}
}

public class DrawTest
{
	public static void main(String[] args)
	{
		// 创建一个账户
		var acct = new Account("1234567", 0);
		new DrawThread("取钱者", acct, 800).start();
		new DepositThread("存款者甲", acct, 800).start();
		new DepositThread("存款者乙", acct, 800).start();
		new DepositThread("存款者丙", acct, 800).start();
	}
}

三个存款线程,一个取款线程,很显然最后程序会处在阻塞状态无法继续执行,因为存款尝试是300次,取款尝试是100次,所以最后被阻塞,但这不是死锁,这只是取款线程已经结束,但存款线程还在等待取款,并不是等待其他线程释放同步监视器。

使用Condition控制线程通信

如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()/notify()/notifyAll()方法进行线程通信了
Lock替代了同步方法或同步代码块,而Condition则替代了同步监视器的功能
Condition类提供了如下3个方法:

  • await():类似于隐式同步监视器上的wait()方法,导致当前线程等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程,该await()方法有多个变体long awaitNanos(longnanosTimeout)、void awaitUninterruptibly()、awaitUntil(Date deadline)等等
  • signal():唤醒在此Lock对象上等待的单个线程,如果所有的线程都在该Lock对象上等待则会任意选择唤醒其中一个线程,只有当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程
  • signalAll():唤醒在此Lock对象上等待的所有线程,只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

public class Account
{
	// 显式定义Lock对象
	private final Lock lock = new ReentrantLock();
	// 获得指定Lock对象对应的Condition
	private final Condition cond = lock.newCondition();
	// 封装账户编号、账户余额的两个成员变量
	private String accountNo;
	private double balance;
	// 标识账户中是否已有存款的旗标
	private boolean flag = false;

	public Account(){}
	// 构造器
	public Account(String accountNo, double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}

	// accountNo的setter和getter方法
	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}
	// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
	public double getBalance()
	{
		return this.balance;
	}

	public void draw(double drawAmount)
	{
		// 加锁
		lock.lock();
		try
		{
			// 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
			if (!flag)
			{
				cond.await();
			}
			else
			{
				// 执行取钱
				System.out.println(Thread.currentThread().getName()
					+ " 取钱:" + drawAmount);
				balance -= drawAmount;
				System.out.println("账户余额为:" + balance);
				// 将标识账户是否已有存款的旗标设为false。
				flag = false;
				// 唤醒其他线程
				cond.signalAll();
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
		// 使用finally块来释放锁
		finally
		{
			lock.unlock();
		}
	}
	public void deposit(double depositAmount)
	{
		lock.lock();
		try
		{
			// 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
			if (flag)           
			{
				cond.await();
			}
			else
			{
				// 执行存款
				System.out.println(Thread.currentThread().getName()
					+ " 存款:" + depositAmount);
				balance += depositAmount;
				System.out.println("账户余额为:" + balance);
				// 将表示账户是否已有存款的旗标设为true
				flag = true;
				// 唤醒其他线程
				cond.signalAll();
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
		// 使用finally块来释放锁
		finally
		{
			lock.unlock();
		}
	}

	// 下面两个方法根据accountNo来重写hashCode()和equals()方法
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if (this == obj)
			return true;
		if (obj != null
			&& obj.getClass() == Account.class)
		{
			var target = (Account) obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}
public class DepositThread extends Thread
{
	// 模拟用户账户
	private Account account;
	// 当前取钱线程所希望存款的钱数
	private double depositAmount;
	public DepositThread(String name, Account account,
		double depositAmount)
	{
		super(name);
		this.account = account;
		this.depositAmount = depositAmount;
	}
	// 重复100次执行存款操作
	public void run()
	{
		for (var i = 0; i < 100; i++)
		{
			account.deposit(depositAmount);
		}
	}
}

public class DrawThread extends Thread
{
	// 模拟用户账户
	private Account account;
	// 当前取钱线程所希望取的钱数
	private double drawAmount;
	public DrawThread(String name, Account account,
		double drawAmount)
	{
		super(name);
		this.account = account;
		this.drawAmount = drawAmount;
	}
	// 重复100次执行取钱操作
	public void run()
	{
		for (var i = 0; i < 100; i++)
		{
			account.draw(drawAmount);
		}
	}
}

public class DrawTest
{
	public static void main(String[] args)
	{
		// 创建一个账户
		var acct = new Account("1234567", 0);
		new DrawThread("取钱者", acct, 800).start();
		new DepositThread("存款者甲", acct, 800).start();
		new DepositThread("存款者乙", acct, 800).start();
		new DepositThread("存款者丙", acct, 800).start();
	}
}

使用阻塞队列BlockingQueue控制线程通信

Java5提供了一个BlockingQueue接口,虽然它是Queue的子接口,但主要用途是作为线程同步工具,其特点是:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列为空,则该线程被阻塞,程序的两个线程通过交替向BlockingQueue中放入元素、取出元素即可很好的控制线程的通信
BlockingQueue提供了两个支持阻塞的方法:

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

BlockingQueue继承了Queue接口,当然也可使用Queue接口中的方法

  • 在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法会抛出异常并返回false,阻塞队列
  • 在队列头部删除并返回被删除的元素,包括remove()、poll()和take()方法,当队列为空时,这三个方法分别会抛出异常并返回false,阻塞队列
  • 在队列头部取出但不删除元素,包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常并返回false
    在这里插入图片描述
    BlcokingQueue包含5个实现类:
  • ArrayBlockingQueue:基于数组实现的BlockingQueue队列
  • LinkedBlockingQueue: 基于链表是显得BlockingQueue队列
  • PriorityBlockingQueue:它并不是标准的阻塞队列,与PriorityQueue类似,该队列调用remove()、poll()、take()等方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素,PriorityBlockingQueue判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行订制排序
  • SynchronousQueue:同步队列,对该队列的存取操作必须交替进行
  • DelayQueue: 这是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现,DelayQueue要求结合元素都实现Delay接口(该接口里只一个long getDelay()方法),DelayQueue根据集合元素的getDelay()方法的返回值进行排序
import java.util.concurrent.*;

public class BlockingQueueTest
{
	public static void main(String[] args)
		throws Exception
	{
		// 定义一个长度为2的阻塞队列
		BlockingQueue<String> bq = new ArrayBlockingQueue<>(2);
		bq.put("Java"); // 与bq.add("Java")、bq.offer("Java")相同
		bq.put("Java"); // 与bq.add("Java")、bq.offer("Java")相同
		bq.put("Java"); // 阻塞线程。
	}
}

程序定义了一个大小为2的BlockingQueue,先向该队列中放入两个元素,此时队列还没有满,两个元素都可以放入,因此使用put()、add()和offer()方法效果都一样,当程序试图放入第三个元素时,如果使用put()则会造成阻塞,如果使用offer()则会放回false,如果使用add()则会引发异常,元素不会被放入
同样的如果BlockingQueue已空的话,使用take()取出元素将会阻塞线程,使用remove()方法将会引发异常,使用poll()方法则会返回false,元素不会被删除

import java.util.concurrent.*;

class Producer extends Thread
{
	private BlockingQueue<String> bq;
	public Producer(BlockingQueue<String> bq)
	{
		this.bq = bq;
	}
	public void run()
	{
		var strArr = new String[]
		{
			"Java",
			"Struts",
			"Spring"
		};
		for (var i = 0; i < 999999999; i++)
		{
			System.out.println(getName() + "生产者准备生产集合元素!");
			try
			{
				Thread.sleep(200);
				// 尝试放入元素,如果队列已满,线程被阻塞
				bq.put(strArr[i % 3]);
			}
			catch (Exception ex) {ex.printStackTrace();}
			System.out.println(getName() + "生产完成:" + bq);
		}
	}
}
class Consumer extends Thread
{
	private BlockingQueue<String> bq;
	public Consumer(BlockingQueue<String> bq)
	{
		this.bq = bq;
	}
	public void run()
	{
		while (true)
		{
			System.out.println(getName() + "消费者准备消费集合元素!");
			try
			{
				Thread.sleep(200);
				// 尝试取出元素,如果队列已空,线程被阻塞
				bq.take();
			}
			catch (Exception ex) {ex.printStackTrace();}
			System.out.println(getName() + "消费完成:" + bq);
		}
	}
}
public class BlockingQueueTest2
{
	public static void main(String[] args)
	{
		// 创建一个容量为1的BlockingQueue
		BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
		// 启动3条生产者线程
		new Producer(bq).start();
		new Producer(bq).start();
		new Producer(bq).start();
		// 启动一条消费者线程
		new Consumer(bq).start();
	}
}

启动了3个生产者线程向BlockingQueue集合放入元素,启动了1个消费者线程从BlockingQueue集合取出元素,BlockingQueue集合容量为1,因此3个生产者线程无法连续放入元素,必须等待消费者线程取出一个元素后,生产者线程才能继续放入一个元素

你可能感兴趣的:(Java基础即高端)