线程通信
传统的线程通信
假设现在系统中有两个线程,这两个线程分别代表存款者和取钱者,而系统有一种特殊的要求,系统要求存款者和取钱者不断地重复存款、取钱的动作,而且要求每当存款者将钱存入指定账户后,取钱者就立即取钱。不允许存款者和取钱者操作连续超过两次。
为了实现这种功能,可以借助于Object类提供的wait()、notify()和notifyAll()三个方法,这三个方法必须由同步监视器对象来调用。
wait()
:导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll方法来唤醒该线程。调用该方法,当前线程会释放对该同步监视器的锁定。notify()
:唤醒再次同步监视器上等待的的单个线程,如果有多个线程都在等待,则唤醒任意一个。只有当前线程放弃对同步监视器的锁定后,才可以执行被唤醒的线程。notifyAll()
:唤醒再次同步监视器上等待的所有线程,只有当前线程放弃对同步监视器的锁定后,才可以执行被唤醒的线程。
案例思路
程序中可以通过一个标记来标识账户中是否已有存款,false表示没有存款,存款者线程可向下执行。
当存款者把钱存入账户之后,将标识设为true,并且调用notify或notifyAll来通知其他线程;当存款折者进入线程体之后,如果标记为true,就调用wait方法让该线程等待。
当标记为true时,表明账户已经存入了存款,则取钱者线程可以向下执行,当取钱者把钱从账户中取出来之后,则标记设为false,并调用notify()或notifyAll来唤醒其他线程,当取款折者进入线程体之后,如果标记为false,就调用wait方法让该线程等待。
package org.westos.demo7;
public class Account {
//封装账户编号,账户余额的两个成员变量
private String accountNO;
private double balance;
//表示账户中是否有存款的标记
private boolean flag=false;
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;
}
//账户余额不能随便修改,所以只提供getBalance方法
public double getBalance() {
return balance;
}
public synchronized void drow(double drawAmount){
try {
//如果flag为false,表示账户中没有人存钱进去,取钱方法为阻塞
if(!flag){
wait();
}else {
//执行取钱操作
System.out.println(Thread.currentThread().getName()
+"取钱:"+drawAmount);
balance-=drawAmount;
System.out.println("账户余额为:"+balance);
//将账户是否有存款的标记设为false
flag=false;
//唤醒其他线程
notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void deposit(Double depositAmount){
//如果flag为真,说明账户里有钱,存钱方法阻塞
try{
if(flag){
wait();
}else {
//存款操作
System.out.println(Thread.currentThread().getName()
+"存款:"+depositAmount);
balance+=depositAmount;
System.out.println("账户余额:"+balance);
//标记设为true
flag=true;
//唤醒其他线程
notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
取款操作
package org.westos.demo7;
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;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
account.drow(drawAmount);
}
}
}
存款操作
package org.westos.demo7;
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;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
account.deposit(depositAmount);
}
}
}
执行操作
package org.westos.demo7;
public class DrawTest {
public static void main(String[] args) {
//创建一个账户
Account acct = new Account("123456", 0);
new DrawThread("取钱者",acct,800).start();
new DepositThread("存钱者A",acct,800).start();
new DepositThread("存钱者B",acct,800).start();
new DepositThread("存钱者C",acct,800).start();
}
}
结论
从运行结果可以看出,存款者线程和取款者线程交替执行,只要账户里没钱就存,只要账户里有钱就取,(这就实现了存款者线程与取款者线程之间的通信)而三个存款者线程则是随机向账户里面存款,一次存款只有一个存款者操作。
使用Condition控制线程通信
如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,所以不能使用wait()、notify()、notifyAll()来进行线程通信了。
当使用Lock对象来保证同步时,Java提供了一个Contidition类来保持协调。Condition为每个对象提供了多个等待集(wait-set)。
Condition实例被绑定在一个Lock对象上,要获得特定Lock实例的Condition实例,调用newCondition()方法即可。Condition提供了如下三个方法:
await()
:类似于隐式同步监视器的wait方法;signal()
:唤醒在此Lock对象上等待的单个线程。signalAll()
:唤醒在此Lock对象上等待的所有线程。
将上述案例更改为使用Lock来保证同步:
package org.westos.demo7;
import java.time.LocalDate;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
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(String accountNO, double balance) {
this.accountNO = accountNO;
this.balance = balance;
}
public String getAccountNO() {
return accountNO;
}
public void setAccountNO(String accountNO) {
this.accountNO = accountNO;
}
//账户余额不能随便修改,所以只提供getBalance方法
public double getBalance() {
return balance;
}
public void drow(double drawAmount){
lock.lock();
try {
//如果flag为false,表示账户中没有人存钱进去,取钱方法为阻塞
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 e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void deposit(Double depositAmount){
//如果flag为真,说明账户里有钱,存钱方法阻塞
lock.lock();
try{
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 e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
与上一个案例相比,程序逻辑基本相似,只是Lock要显式定义同步监视器。
使用阻塞队列(BlockingQueue)控制线程通信
Java5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但是它不作为容器,而是作为线性同步的工具。BlockingQueue有一个特征:当生产者线程试图向队列中放入元素时,如果该队列已满,则该线程阻塞;当消费者从队列中拿出元素时,如果该队列为空,则该线程被阻塞。
BlockingQueue提供如下两个方法支持阻塞:
put(E e)
:尝试把E元素放入队列中,如果队列已满则阻塞该线程;take()
:尝试从队列的头部取出元素,如果该队列已空则阻塞该线程;
当然BlockingQueue继承了Queue接口,所以可以使用Queue接口中的方法,大概分为三种:
- 在队列尾部插元素,包括add、offer、put的方法;
- 在队列头部删除元素,并返回删除元素,包括remove、poll、take方法;
- 在队列头部取出但不删除元素,包括element()和peek方法;
抛出异常 | 不同返回值 | 阻塞线程 | 指定超时间时长 | |
---|---|---|---|---|
队尾插入元素 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
队头插入元素 | remove() | poll() | take() | poll(time,unit) |
获取,但不插入元素 | element() | peek() | 无 | 无 |
BlockingQueue包含如下5个实现类:
- ArrayBlockingQueue:基于数组实现的BlockingQueue队列;
- LinkedBlockingQueue:基于链表实现的BlockingQueue队列;
- PriorityBlockingQueue:判断元素大小,取出队列中最小的元素;
- SynchronousQueue:同步队列,对该队列的存取必须交替执行;
- DelayQueue:特殊的BlockingQueue,底层基于PriorityBlockingQueue实现;
案例:以ArrayBlockingQueue为例介绍阻塞队列:
import java.util.concurrent.ArrayBlockingQueue;
public class BlockingQueueTest {
public static void main(String[] args) throws InterruptedException {
//定义一个长度为2的阻塞队列
ArrayBlockingQueue<Object> bq = new ArrayBlockingQueue<>(2);
bq.put("Java");
bq.put("Java");
bq.put("Java");//阻塞线程
}
}
案例:使用BlockingQueue来实现线程通信:
package org.westos.demo7;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class Produer extends Thread {
private BlockingQueue<String> bq;
public Produer(String name,BlockingQueue<String> bq) {
super(name);
this.bq = bq;
}
@Override
public void run() {
String[] strArr = new String[]
{
"Java",
"Struts",
"Spring"
};
for (int i = 0; i < 999999; i++) {
System.out.println(getName()+"生产者准备生产集合元素");
try {
Thread.sleep(200);
//尝试放入元素,如果队列已满则线程阻塞
bq.put(strArr[i%3]);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"生产完成"+bq);
}
}
}
class Consumer extends Thread{
private BlockingQueue<String> bq;
public Consumer(String name,BlockingQueue<String> bq) {
super(name);
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);
}
}
}
public class BlockingQueueTest2{
public static void main(String[] args) {
//创建一个容量为1的BlockingQueue
ArrayBlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
//启动三个生产线程
new Produer("生产者者1",bq).start();
new Produer("生产者2",bq).start();
new Produer("生产者3",bq).start();
//启用一个消费线程
new Consumer("消费者C",bq).start();
}
}
由于队列容量为1,所以无法连续放入元素,只能等消费者去除队列中的元素,三个生产者才能随机的放入元素。
线程组和未处理的异常
Java使用ThreadGroup来表示线程组,他可以对一批线程进行分类管理,Java允许程序员直接对线程组进行管控制。相当于同时控制着一批线程,用户创建的所有线程都属于指定线程组,如果没有显式指定线程的线程组,则该线程属于默认线程组。默认情况下子线程和创建它的父线程处于同一线程组内。
一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到线程死亡,线程运行中途不能改变它所属的线程组。
Thread类提供的构造器来设置新创建的线程属于哪个线程组;
Thread(ThreadGroup group,Runnable target)
:以target的run方法作为线程执行体创建新线程,属于group线程组;Thread(ThreadGroup group,Runnable target,String name)
:以target的run方法作为线程执行体创建新线程,属于group线程组,线程名为name;Thread(ThreadGroup group,String name)
:创建新线程,名为name,属于group线程组。
由于线程所属组不能改变,所以只提供了一个getThreadGroup方法来返回线程所属的线程组,getThreadGroup方法返回的是ThreadGroup对象,表示一个线程组,ThreadGroup提供了两个构造器来创建实例:
ThreadGroup(String name)
:以指定的线程组名来创建新的线程组;ThreadGroup(ThreadGroup parent,String name)
:以指定的线程组名,指定的父线程组,来创建新的线程组;
ThreadGroup类提供了几个方法来操作整个线程组里的所有线程:
int activeCount()
:返回此线程组中活动线程的数目;interrupt()
:中断此线程组中的所有线程;isDaemon()
:判断该线程组是否为后台线程组;setDaemon(Boolean daemon)
;把该线程组设置为后台线程组,当后台线程组的最后一个线程执行结束或最后一个线程被销毁后,后台线程组将自动销毁。setMaxPriotity(int pri)
:设置线程组的最高优先级;
package org.westos.demo8;
class MyThread extends Thread{
//提供指定线程名的构造器
public MyThread(String name) {
super(name);
}
//提供指定线程名,指定线程组的构造器
public MyThread(ThreadGroup group,String name) {
super(group,name);
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(getName()+"线程的i变量"+i);
}
}
}
public class ThreadGroupTest {
public static void main(String[] args) {
//获取主线程所在线程组(默认线程组)
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
System.out.println("主线程组的名字:"+mainGroup);
//主线程组的名字:java.lang.ThreadGroup[name=main,maxpri=10]
System.out.println("主线程组是否为后台线程组:"+mainGroup.isDaemon());
ThreadGroup newGroup = new ThreadGroup("新线程组");
newGroup.setDaemon(true);
MyThread tt = new MyThread(newGroup, "新线程组中的线程-1");
tt.start();
new MyThread("主线程组的线程").start();
new MyThread(newGroup,"新线程组中的线程-2").start();
}
}
未处理的异常
ThreadGroup内还定义了可以处理该线程组内任意线程所抛出的未处理异常的方法:
void uncaughtException(Thread t,Throwable e)
。
Thread类提供了如下两个方法来设置异常处理器:
static setDefaultUncaughtExceptionHandler(Thread UncaughtExceptionHandler eh)
:为该线程实例设置异常处理器;setUncaughtExceptionHandler(Thread UncaughtExceptionHandler eh)
:为指定的线程实例设置异常处理器。
线程处理异常的默认流程如下:
- 如果该线程组有父线程组,则调用父线程组的UncaughtException方法来处理异常;
- 如果有默认异常处理器,就调用默认的;
- 如果该异常对象是ThreadDeath对象,则不做任何处理;否则将异常信息打印到错误输出流,并结束线程。
package org.westos.demo8;
class MyExHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t+"线程出现了异常:"+e);
//Thread[main,5,main]线程出现了异常:java.lang.ArithmeticException: / by zero
}
}
public class ExHandler{
public static void main(String[] args) {
//设置主线程的异常处理器
Thread.currentThread().setUncaughtExceptionHandler(new MyExHandler());
int a=5/0;
System.out.println("程序正常结束");
}
}