当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但Java也提供了一些机制用于保证线程协调运行
假设两个线程一个负责存款一个负责取款,要求是存款和取钱不断的重复,并存完立即取出,不允许连续两次存款和连续两次取钱
Object类提供了wait()、notify()和notifyAll()三个方法,必须使用同步监视器对象来调用这三个方法,分为两种情况:
1、对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法
2、对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象来调用这3个方法
实现逻辑:
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次,所以最后被阻塞,但这不是死锁,这只是取款线程已经结束,但存款线程还在等待取款,并不是等待其他线程释放同步监视器。
如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()/notify()/notifyAll()方法进行线程通信了
Lock替代了同步方法或同步代码块,而Condition则替代了同步监视器的功能
Condition类提供了如下3个方法:
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();
}
}
Java5提供了一个BlockingQueue接口,虽然它是Queue的子接口,但主要用途是作为线程同步工具,其特点是:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列为空,则该线程被阻塞,程序的两个线程通过交替向BlockingQueue中放入元素、取出元素即可很好的控制线程的通信
BlockingQueue提供了两个支持阻塞的方法:
BlockingQueue继承了Queue接口,当然也可使用Queue接口中的方法
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个生产者线程无法连续放入元素,必须等待消费者线程取出一个元素后,生产者线程才能继续放入一个元素