释放同步监视器的锁定
-当前线程的同步方法,同步代码块执行结束
-在同步代码块,同步方法中遇到break,return终止了该代码块或者该方法的继续执行
-在同步代码块,同步方法中出现了未处理的Error或Exception
- 执行了同步监视器对象的wait()方法
程序调用Thread.sleep(),Thread.yield()方法暂停当前线程的执行,当前线程不会释放同步监视器的锁
普通的线程间的通信
普通的线程通信机制必须使用synchronized关键字来保证同步,而不能使用ReentrantLock的对象来保证同步
当线程在系统内运行时,线程的调度具有一定的透明性,程序通产无法准确控制线程的轮换执行,但Java也提供了一些机制来保证线程协调运行。
也就是说线程的通信机制就是将原本要依靠程序的调度器去随机调度的线程变成由程序员人为指定的什么时候该干什么的线程
假设系统中有两个县城,分别代表存款者这和取款者——现在有一种特殊系统,要求存款者和取款者不断地重复存款取钱,而且要求每当存款者将钱存入指定账户,取钱者就立即取出该笔钱。不允许连续两次存钱,也不允许连续两次取钱
有三个方法:
–wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()或者notifyAll()方法。调用wait()方法的当前线程会释放对同步监视器的锁定
–notify():唤醒在此同步监视器上等待的单个线程,选择是任意的
–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;
}
// 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)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
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 (int 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 (int i = 0 ; i < 100 ; i++ )
{
account.deposit(depositAmount);
}
}
}
public class DrawTest
{
public static void main(String[] args)
{
// 创建一个账户
Account 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();
}
}
使用Condition控制线程通信
如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait(),notify(),notifyAll()方法进行线程通信了
也就是说Condition通信只能用Lock加锁
普通通信只能用Synchronized加锁Condition将同步监视器方法(wait(),notify(),notifyAll())分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供了多个等待集,这种情况下:
Lock代替了同步方法或同步代码块,Conditon代替了同步监视器的功能。
同样的Condition类也提供了如下三个方法:
await():类似于wait()方法
signal():类似于notify()方法
signalAll():类似于notifyAll()方法
Condition对象由Lock对象的newCondition()方法导出
下面程序中Account使用Lock对象来控制同步,并使用Condition对象来控制线程的协调运行:
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{
lock.unlock();
}
}
public synchronized 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{
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)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
使用阻塞队列(BlockingQueue)控制线程通信
public interface BlockingQueue extends Queue
(不知道为什么泛型显示不出来-_- )
Java5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但他的主要作用并不是作为容器,而是作为线程同步工具。
BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已经满了,则该线程则被阻塞;当消费者试图从BlockingQueue中去出元素时,如果该队列已空,则该线程被阻塞。
利用这个特性不仅可以省去普通线程通信或Condition通信的繁琐,还可以省去同步代码块。在生产者消费者问题方面甚至有更好的表现。
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。
BlockingQueue有5个实现类,重点掌握它的ArrayBlockingQueue:基于数组实现的BlockingQueue.
下面的代码是基于BlockinQueue的生产者和消费者问题:
class Producer2 extends Thread
{
private BlockingQueue<String> bq;
public Producer2(BlockingQueue<String> bq)
{
this.bq = bq;
}
public void run()
{
String[] strArr = new String[]
{
"Java",
"Struts",
"Spring"
};
for (int 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 Consumer2 extends Thread
{
private BlockingQueue<String> bq;
public Consumer2(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 Producer2(bq).start();
new Producer2(bq).start();
new Producer2(bq).start();
// 启动一条消费者线程
new Consumer2(bq).start();
}
}