一、为什么要线程通信?
1. 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
2. 当然如果我们没有使用线程通信来使用多线程共同操作同一份数据的话,虽然可以实现,但是在很大程度会造成多线程之间对同一共享变量的争夺,那样的话势必为造成很多错误和损失!
3. 所以,我们才引出了线程之间的通信,多线程之间的通信能够避免对同一共享变量的争夺。
二、什么是线程通信?
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。
就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。
于是我们引出了等待唤醒机制:wait()、notify()
就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify);
(1)wait()方法:
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行。
要确保调用wait()方法的时候拥有锁,即wait()方法的调用必须放在synchronized方法或synchronized块中。
注意:wait()和sleep()的区别:
1、这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。
sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
2、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。
3、使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
synchronized(x){
x.notify()
//或者wait()
}
4、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
(2)notify()方法:
notify()方法会唤醒一个等待当前对象的锁的线程。唤醒在此对象监视器上等待的单个线程。如果多个线程在等待,它们中的一个将会选择被唤醒。这种选择是随意的,和具体实现有关。(线程等待一个对象的锁是由于调用了wait方法中的一个),notify()方法应该是被拥有对象的锁的线程所调用。
(3)notifyAll()方法:
notifyAll()方法会唤醒在此对象监视器上等待的所有线程。
(4)以上方法都定义在类:Object中
1.因为,这些方法在操作同步中的线程的时候,都必须标示其所操作线程所持有的锁(被该锁的对象调用),
而只有同一个对象监视器下(同一个锁上)的被等待线程,可以被持有该锁的线程唤醒,(无法唤醒不同锁上的线程)
2.所以,等待和唤醒的必须是同一个对象的监视器下(同一个锁上)的线程。
而锁可以是任意已近确定的对象, 能被任意对象调用的方法应当定义在 Object类中。
注:①监视器(锁):同一个对象的监视器下(同一个锁上)的线程,一次只能执行一个:就是拥有监视器所有权(持有锁)的那一个线程。
假设A线程和B线程共同操作一个X对象(同步锁),A,B线程可以通过X对象的wait和notify方法来进行通信,流程如下:
1:当A线程执行X对象的同步方法时,A线程持有X对象的锁,B线程没有执行机会,B线程在X对象的锁池中等待.
2:A线程在同步方法中执行X.wait()方法时,A线程释放X对象的锁,进入A线程进入X对象的等待池中.
3:在X对象的锁池中等待锁的B线程获取X对象的锁,执行X的另一个同步方法.
4:B线程在同步方法中执行X.notify()方法时,JVM把A线程从X对象的等待池中移动到X对象的锁池中,等待获取锁.
5:B线程执行完同步方法,释放锁.A线程获得锁,继续执行同步方法.
三、实现线程通信的小demo
如大众所说,体现线程通信的鲜明的例子:生产者---消费者是最明显的例子之一。那我们接下来敲一个关于生产者和消费者的简单的小demo,来演示一下线程的通信:
此实例是模拟生产者生产苹果,当生产者生产出五个苹果时,就会陷入沉睡,随后立即去唤醒消费者,就像是对他说,你去吃吧,我生产好了,然后消费者就会屁颠屁颠的去吃了那五个苹果,当他吃完那五个苹果后,也会陷入沉睡,随后立即唤醒生产者,就像是对他说,你再生产五个吧,我吃完了,然后生产者就... ... 如此循环,周而复始,直到for循环结束为止。
如上所示,案例表明生产者消费者之间就是运用了wait()和notify()这两个方法,通过通信的沉睡与唤醒机制来完成两个不同线程操作统一数据之间的通信,
import java.util.LinkedList;
public class ProductorConsumerDemo {
public static void main(String[] args) {
Basket basket = new Basket();
Productor productor = new Productor(basket);
Consumer consumer = new Consumer(basket);
productor.start();
consumer.start();
}
}
// 生产者类
class Productor extends Thread{
private Basket basket = null;
public Productor(Basket basket) {
super();
this.basket = basket;
}
@Override
public void run() {
basket.pushApple();
}
}
// 消费者类
class Consumer extends Thread{
private Basket basket = null;
public Consumer(Basket basket) {
super();
this.basket = basket;
}
@Override
public void run() {
basket.popApple();
}
}
// 篮子类
class Basket{
private LinkedList<Apple> basket = new LinkedList<Apple>();
// 放4轮苹果
public synchronized void pushApple() {
for (int i = 0; i < 20; i++) {
Apple apple = new Apple(i);
push(apple);
}
}
// 取4轮苹果
public synchronized void popApple() {
for (int i = 0; i < 20; i++) {
pop();
}
}
// 向篮子里放苹果
private void push(Apple apple) {
// 当篮子当中存放了5个苹果就等待并通知消费者来消费
if (basket.size() == 5) {
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} // 等待并释放当前对象的值
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
basket.addFirst(apple);
System.out.println("存放:" + apple.toString());
notify(); // 通知消费者来消费
}
// 从篮子里取苹果
private void pop() {
// 当篮子当中苹果数为0的时候就等待并通知生产者来生产
if (basket.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} // 等待并释放当前对象的值
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Apple apple = basket.removeFirst();
System.out.println("吃掉:" + apple.toString());
notify(); // 通知生产者来生产
}
}
// 苹果类
class Apple{
private int id;
public Apple(int id) {
this.id = id;
}
@Override
public String toString() {
return "苹果" + (id + 1);
}
}
结果为:
存放:苹果1
存放:苹果2
存放:苹果3
存放:苹果4
存放:苹果5
吃掉:苹果5
吃掉:苹果4
吃掉:苹果3
吃掉:苹果2
吃掉:苹果1
存放:苹果6
存放:苹果7
存放:苹果8
存放:苹果9
存放:苹果10
吃掉:苹果10
吃掉:苹果9
吃掉:苹果8
吃掉:苹果7
吃掉:苹果6
存放:苹果11
存放:苹果12
存放:苹果13
存放:苹果14
存放:苹果15
吃掉:苹果15
吃掉:苹果14
吃掉:苹果13
吃掉:苹果12
吃掉:苹果11
存放:苹果16
存放:苹果17
存放:苹果18
存放:苹果19
存放:苹果20
吃掉:苹果20
吃掉:苹果19
吃掉:苹果18
吃掉:苹果17
吃掉:苹果16