在学习Java多线程知识的时候,一定会涉及到线程通信这方面的知识。所谓线程通信,指的就是线程在系统内运行的时候,线程间的调度具有一定的透明性,也就是说线程具体的调度方式是不能够被程序直接获取到的,程序通常无法准确地控制线程的轮换执行,Java也提供了一些机制来保证线程的协调运行。
Java中的关键字synchronized用来修饰同步代码块或者同步方法,同步代码块的意思是,在synchronized修饰下的代码部分,只允许线程互斥的进行访问,而synchronized关键字后会显示的定义同步操作的对象,例如
synchronized(apple){
//此处省略代码;
}
当使用synchronized来修饰方法的时候,这样的方法就叫做同步方法,通过定义同步方法可以实现同步类,不同的线程可以正确安全地访问该类的对象。
1、传统的线程间通信所采用的一般是wait()、notify()、notifyAll()这三个方法,这三个方法不属于Thread类,而是属于Object类,但是这三个方法必须通过同步监视器对象(可以是synchronized修饰过的方法所在类的实例或同步代码块所指定的对象)来调用,
wait()导致当前线程等待,当前线程会释放线程所占资源,即释放对该资源的锁,开始等待,直至notify()或notifyAll()将其唤醒;
notify()唤醒此同步监视器上的一个等待的线程,若是有多个等待线程则随机唤醒一个;
notifyAll()唤醒此同步监视器上的所有等待的线程,只有当前线程放弃同步监视器的锁后才能调用该方法。
下面用一个例子来说明几种方法的用法:
public class Account { //声明一个账户类,实现存钱/取钱的进程间通信 private String accountNo;//账户名 private double balance;//余额 private boolean flag = false;//若没人存钱则为false,有人存入后设为true,取钱后设为false,存取钱只能交替进行 public Account(){} public Account (String accountNo, double balance) { this.accountNo = accountNo; this.balance = balance; } public synchronized void draw(double drawAmount) { try { if(!flag) wait();//此时不能取钱,等待 else { System.out.println(Thread.currentThread().getName()+"取钱:"+drawAmount); balance-=drawAmount; System.out.println("余额为:"+balance); flag = false;//取钱成功后,设为false notifyAll(); } } catch(InterruptedException ex) { ex.printStackTrace(); } } public synchronized void deposit(double depositAmount) { try { if(flag) wait();//此时不能存钱,等待 else { System.out.println(Thread.currentThread().getName()+"存款:"+depositAmount); balance+=depositAmount; System.out.println("余额为:"+balance); flag = true;//存钱成功后设为true notifyAll(); } } catch(InterruptedException ex) { ex.printStackTrace(); } } }
存钱/取钱操作的执行线程
public class DepositThread extends Thread{ private Account account; private double depositAmount; public DepositThread(String name, Account account, double despositAmount) { super(name); this.depositAmount = despositAmount; this.account = account; } public void run() { for(int i=0; i<10; 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.drawAmount = drawAmount; this.account = account; } public void run() { for(int i=0; i<100; i++) {//执行十次取钱操作 account.draw(drawAmount); } } }
开始进行存取钱操作,存/取钱线程各自创建两个,同时开始执行
public class DrawTest { public static void main(String args[]) { Account ac = new Account("mango",0); new DrawThread("取钱1", ac, 500).start(); new DrawThread("取钱2", ac, 500).start(); new DepositThread("存钱1", ac, 500).start(); new DepositThread("存钱2", ac, 500).start(); } }
运行部分结果如下,可以看到存取钱操作交替执行,互斥的访问账户对象
存钱1存款:500.0
余额为:500.0
取钱2取钱:500.0
余额为:0.0
存钱2存款:500.0
余额为:500.0
取钱1取钱:500.0
余额为:0.0
存钱1存款:500.0
余额为:500.0
2、使用Condition控制线程通信
如果不用synchronized关键字,我们还可以用Lock对象来保证同步。当用Lock来保证同步时,就不能使用上述的wait()、notify()等方法了,Java提供了一个Condition类来保持协调,在这种方法下,Lock代替了同步方法或者同步代码块,而Condition代替了同步监视器。使用Condition可以实现线程释放资源锁,也可以实现线程的唤醒,与上面提到的三个方法对应的Condition中的方法是await()、signal()和signalAll()。
创建Lock对象的方法为
private final Lock lock = new ReentrantLock();
通过调用Lock对象中的newCondition()方法来创建Condition
private final Condition cond = new lock.newCondition();
修改之前的代码为
public synchronized void draw(double drawAmount) { lock.lock();//加锁 try { if(!flag) cond.await();//此时不能取钱,等待 else { System.out.println(Thread.currentThread().getName()+"取钱:"+drawAmount); balance-=drawAmount; System.out.println("余额为:"+balance); flag = false;//取钱成功后,设为false cond.signalAll(); } } catch(InterruptedException ex) { ex.printStackTrace(); } finally{ lock.unlock();//使用finally块释放锁 } }
3、使用阻塞队列控制线程通信
阻塞队列(BlockingQueue)也可以实现进程间的通信控制。阻塞队列可以这样理解,有一个生产者和一个消费者,生产者负责向队列中增加产品,消费者负责从队列中取出产品。当队列为空时,消费者的消费行为便会被阻塞,直到队列中有产品的时候才会被唤醒;当队列满的时候,生产者的生产行为便会被阻塞,当队列中有空位的时候才会被唤醒。
BlockingQueue继承了Queue类,所以具备Queue的add()、remov()等方法,不过这些方法都不是同步方法,当队列出现空或满的时候会抛出异常。BlockingQueue定义了自己的增加和删除方法,put(E e)和take()分别完成入队和出队的操作。也可以运用阻塞队列来完成资源的各种互斥访问。