黑马程序员:Java基础——多线程之间的通信

------- Java EE培训java培训、期待与您交流! ----------

1.概念

    线程间通讯:其实就是多个线程在操作同一个资源,但是操作的动作不同。

    我们先来看这样一幅图:

    我们有两个线程:一个输入线程,一个输出线程,共用资源有两个变量,字符串类型的名字和字符串的性别。

    我们依照这个图写一个这样的程序:

class Res{
	public String sName;
	public String sSex;
}

class Input implements Runnable{
	private Res r;
	Input(Res r){
		this.r = r;
	}
	public void run(){
		int x = 0;
		while(true){
			if(x==0){
				r.sName="Mike";
				r.sSex="Man";
			}else{
				r.sName="Kitty";
				r.sSex="Woman";
			}
			x=(x+1)%2;
		}
	}
}

class Output implements Runnable{
	private Res r;
	Output(Res r){
		this.r = r;
	}
	public void run(){
		while(true){
			System.out.println(r.sName+"---"+r.sSex);
		}
	}
}

public class ThreadConnDemo {
    public static void main(String[] args) {
		Res r = new Res();
		Input input = new Input(r);
		Output output = new Output(r);
		
		Thread t1 = new Thread(input);
		Thread t2 = new Thread(output);
		
		t1.start();
		t2.start();
	}
}

运行一下看看:


结果很恐怖,大家可以看到名字和性别出现了排列混乱的现象。

出现的原因是什么?我们还用这张图:

通过之前的日志,我们知道线程之间一般是交替执行的,很有可能出现线程1刚把名字存进去,线程2就抢到了CPU的执行权,将现有的共用资源内的数据输出出去,所以,我们看到名字和性别出现混乱时很正常的。因此,我们称这个程序是不安全的。

2.通信问题解决方案

那么我们该怎么解决呢?

我们很快就想到了加同步锁的方法。那我们这样改:

class Input implements Runnable{
	private Res r;
	Object obj = new Object();
	Input(Res r){
		this.r = r;
	}
	public void run(){
		int x = 0;
		while(true){
			synchronized(obj){
				if(x==0){
					r.sName="Mike";
					r.sSex="Man";
				}else{
					r.sName="Kitty";
					r.sSex="Woman";
				}
				x=(x+1)%2;
			}
		}
	}
}

class Output implements Runnable{
	private Res r;
	Object obj = new Object();
	Output(Res r){
		this.r = r;
	}
	public void run(){
		while(true){
			synchronized(obj){
				System.out.println(r.sName+"---"+r.sSex);
			}
		}
	}
}

但是,当我们运行的时候发现,问题依然存在。问题出现在哪?我们回想下同步的前提:
     1.必须要有两个或者两个以上的线程
     2.必须是多个线程使用同一个锁。
其实,我们两条都不满足,我们的两个线程一个在输入,一个在输出,而且没有用同一个锁。那么我们只需要修改synchronized方法里传入的对象就行了。

     我们要同时实现那两个前提,我们把obj修改成Input.class,当然Output.class也可以。

还有一个更简单的方法,我们知道不管是Input还是Output,都在使用Res类,而且Res在Input和Output中都有声明,那么,我们只需要将Input.class改成r就可以了。

这里是完整代码:

class Res{
	public String sName;
	public String sSex;
}

class Input implements Runnable{
	private Res r;
	//Object obj = new Object();
	Input(Res r){
		this.r = r;
	}
	public void run(){
		int x = 0;
		while(true){
			synchronized(r){
				if(x==0){
					r.sName="Mike";
					r.sSex="Man";
				}else{
					r.sName="Kitty";
					r.sSex="Woman";
				}
				x=(x+1)%2;
			}
		}
	}
}

class Output implements Runnable{
	private Res r;
	//Object obj = new Object();
	Output(Res r){
		this.r = r;
	}
	public void run(){
		while(true){
			synchronized(r){
				System.out.println(r.sName+"---"+r.sSex);
			}
		}
	}
}

public class ThreadConnDemo {
    public static void main(String[] args) {
		Res r = new Res();
		Input input = new Input(r);
		Output output = new Output(r);
		
		Thread t1 = new Thread(input);
		Thread t2 = new Thread(output);
		
		t1.start();
		t2.start();
	}
}

3.等待唤醒机制

通过之前的修改我们解决了输出混乱的问题,接下来问题又来了,我们需要的是Input输入一个,就可以从Output输出一个。但是我们输出的却是成片的Mike或Kitty。这是什么原因导致的呢?我们还看这个图:

当线程1执行完输入命令后,依然可以抢到CPU的执行权,就会对共用资源里面的数据再次进行修改。此时,当线程2输出时,极有可能会输出成片的相同数据。

那么,我们该如何来解决这个问题呢?

我们在Res中声明一个用于判断的标志,并让其默认为false:

public Boolean flag = false;

那么我们分别给Input线程和Output线程分别添加入判断,代码如下:

class Res {
	public String sName;
	public String sSex;
	public boolean flag = false;
}

class Input implements Runnable {
	private Res r;

	// Object obj = new Object();
	Input(Res r) {
		this.r = r;
	}

	public void run() {
		int x = 0;
		while (true) {
			synchronized (r) {
				if (r.flag) {
					try {
						r.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				if (x == 0) {
					r.sName = "Mike";
					r.sSex = "Man";
				} else {
					r.sName = "Kitty";
					r.sSex = "Woman";
				}
				x = (x + 1) % 2;
				r.flag=true;
				r.notify();
			}
		}
	}
}

class Output implements Runnable {
	private Res r;

	// Object obj = new Object();
	Output(Res r) {
		this.r = r;
	}

	public void run() {
		while (true) {
			synchronized (r) {
				if (!r.flag) {
					try {
						r.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println(r.sName + "---" + r.sSex);
				r.flag=false;
				r.notify();
			}
		}
	}
}

public class ThreadConnDemo {
	public static void main(String[] args) {
		Res r = new Res();
		Input input = new Input(r);
		Output output = new Output(r);

		Thread t1 = new Thread(input);
		Thread t2 = new Thread(output);

		t1.start();
		t2.start();
	}
}

其中,我们用到了wait()方法和notify()方法,除了这两种方法,还有一种就是notifyAll()方法。
而wait() notify() notifyAll() 都是用在同不中,因为要对持有监视器(锁)的线程操作 所以要使用在同步中,因为只有同步才具有锁。

前面我们也有过介绍,wait()线程冻结方法,它需要notify()或notifyAll()来唤醒。而notify()是唤醒第一个被冻结的线程,notifyAll()则是唤醒全部线程。

Q&A:

    Q:为什么这些操作线程的方法要定义Object类中呢?

    A:因为这些方法在操作同步中线程时,都必须要标识它们所操作线程只有的锁, 只有同一个锁上的被等待线程,可以被同一个锁上notify唤醒。 不可以对不同所中的线程进行唤醒。 也就是说,等待和唤醒必须是同一个锁而锁可以是任意对象,随意可以被任意对象调用的方法定义Object类中。

基于上述知识点,我们总结一下我们上面的程序,我用图来表示:

这就是多线程之间通信的等待与唤醒机制。

4.代码优化

接下来我们将之前的代码进行优化:

    我们在声明String类型的姓名,性别和标志时使用的是public,这样做容易导致数据的不安全,那么,我们将它们改为private,这时问题来了,其他的类无法读取,我们可以使用setter,getter方法来实现外部读取。

    因为我们无需对外提供输出功能所以,getter方法都可以忽略不写。进过改进的代码如下:

class Resource {
	private String sName;
	private String sSex;
	private boolean flag = false;

	public synchronized void set(String sName, String sSex) {
		if (flag) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.sName = sName;
		this.sSex = sSex;
		flag = true;
		this.notify();
	}

	public synchronized void output() {
		if (!flag) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(this.sName + "---" + this.sSex);
		flag = false;
		this.notify();
	}
}

我们将设置数据以及打印数据放置到Resource类把本身之中,减少了setter和getter的代码量,而且在后面也易于调用。需要注意的是,这里我们无需再使用同步锁代码块,直接使用同步锁方法就行了。当然,使用代码块也是可以的。这里是调用代码:

class InputStr implements Runnable {
	private Resource r;
	InputStr(Resource r) {
		this.r = r;
	}

	public void run() {
		int x = 0;
		while (true) {
			if (x == 0) {
				r.set("Mike", "Man");
			} else {
				r.set("Kitty", "Woman");
			}
			x = (x + 1) % 2;
		}
	}
}

class OutputStr implements Runnable {
	private Resource r;
	OutputStr(Resource r) {
		this.r = r;
	}

	public void run() {
		while (true) {
			r.output();
		}
	}
}

在后面的这些代码中,我们只需要调用一些Resource类中本身的一些方法就可以轻松实现代码的优化。

最后在main函数中我们也可以进行优化:

public static void main(String[] args) {
	Resource r = new Resource();

	new Thread(new InputStr(r)).start();
	new Thread(new OutputStr(r)).start();
}

以下是完整的代码:

class Resource {
	private String sName;
	private String sSex;
	private boolean flag = false;

	public synchronized void set(String sName, String sSex) {
		if (flag) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.sName = sName;
		this.sSex = sSex;
		flag = true;
		this.notify();
	}

	public synchronized void output() {
		if (!flag) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(this.sName + "---" + this.sSex);
		flag = false;
		this.notify();
	}
}

class InputStr implements Runnable {
	private Resource r;
	InputStr(Resource r) {
		this.r = r;
	}

	public void run() {
		int x = 0;
		while (true) {
			if (x == 0) {
				r.set("Mike", "Man");
			} else {
				r.set("Kitty", "Woman");
			}
			x = (x + 1) % 2;
		}
	}
}

class OutputStr implements Runnable {
	private Resource r;
	OutputStr(Resource r) {
		this.r = r;
	}

	public void run() {
		while (true) {
			r.output();
		}
	}
}

public class ThreadWaitNotifyDemo {
	public static void main(String[] args) {
		Resource r = new Resource();

		new Thread(new InputStr(r)).start();
		new Thread(new OutputStr(r)).start();
	}
}

运行结果与上一节是一样的。

5.生产者与消费者

基于上一节的程序,我们将其改成生产者与消费者的程序,目的是生产一个就消费一个。

代码如下:

public class ConsumerWithProducerTest {
    public static void main(String[] args) {
		Resource res = new Resource();
		
		Consumer con = new Consumer(res);
		Producer pro = new Producer(res);
		
		Thread t1 = new Thread(con);
		Thread t2 = new Thread(pro);
		
		t1.start();
		t2.start();
	}
}

class Resource{
	private String sName;
	private int id;
	private Boolean flag=false;
	
	public synchronized void set(String sName){
        if(flag){
        	try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
        }
		this.sName=sName+"--"+id++;
		System.out.println(Thread.currentThread().getName()+"--消费者--"+this.sName);
		flag=true;
		this.notify();
	}
	
	public synchronized void out(){
		if(!flag){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread().getName()+"---生产者---"+this.sName);
		flag=false;
		this.notify();
	}
}

class Consumer implements Runnable{
	private Resource res;
	Consumer(Resource res){
		this.res=res;
	}
	
	public void run(){
		while(true){
			res.set("--电脑--");
		}
	}
}

class Producer implements Runnable{
	private Resource res;
	Producer(Resource res){
		this.res = res;
	}
	
	public void run(){
		while(true){
			res.out();
		}
	}
}

我们来运行一下:

但是,我们知道,真实开发中不可能只有两个线程,一定是多个线程。那么我们针对这种需求对我们的程序进行修改。

有人会说了,直接创建线程不就行了,然而经过验证,我们发现出了这样的问题:

我们看到,有生产两次消费一次的,也有生产一次消费两次,这样就与我们的需求相悖论。所以,我们可以认为该程序不安全。

这是为什么呢?

因为在线程中,循环N圈以后,当t1获取资格输出并放弃资格时唤醒了同为生产者(消费者)的另一个线程,此时这个线程将不会砸判断if里面的flag,而直接输出生产(消费)。这就导致了以上所述的问题。因为if只判断一次。

我们用图来说明:

那么我们该如何解决?

如果,我们将if更改为while循环时,会出现多个线程全部等待,导致死锁问题。

那么我们这样改:因为while可以被多次判断,我们不妨直接把全部线程唤醒,这样会唤醒对方的线程,判断标记并执行应该执行的代码块。这就用到了notifyAll()。源代码如下:

public class ConsumerWithProducerTest {
    public static void main(String[] args) {
		Resource res = new Resource();
		
		Consumer con = new Consumer(res);
		Producer pro = new Producer(res);
		
		Thread t1 = new Thread(con);
		Thread t2 = new Thread(pro);
		Thread t3 = new Thread(con);
		Thread t4 = new Thread(pro);
		
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

class Resource{
	private String sName;
	private int id;
	private Boolean flag=false;
	
	public synchronized void set(String sName){
        while(flag){
        	try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
        }
		this.sName=sName+"--"+id++;
		System.out.println(Thread.currentThread().getName()+"--消费者--"+this.sName);
		flag=true;
		this.notifyAll();
	}
	
	public synchronized void out(){
		while(!flag){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread().getName()+"---生产者---"+this.sName);
		flag=false;
		this.notifyAll();
	}
}

class Consumer implements Runnable{
	private Resource res;
	Consumer(Resource res){
		this.res=res;
	}
	
	public void run(){
		while(true){
			res.set("--电脑--");
		}
	}
}

class Producer implements Runnable{
	private Resource res;
	Producer(Resource res){
		this.res = res;
	}
	
	public void run(){
		while(true){
			res.out();
		}
	}
}

结果如下:


经过反复验证也没有出现问题。

可是问题又来了,我们唤醒了全部线程意味着我们把本方的线程也唤醒了,它也会去抢CPU的执行权,而我们的目的是要唤醒对方的线程,那么我们又该怎么做呢?

JDK1.5以后出现了一种可视性锁——Lock

JDK1.5中提供了多线程升级解决方案,将同步Synchronized替换成Lock操作
 * 将Object中的wait,notify,notifyAll,替换成了Condition对象(其中包括替代wait方法的await,替代notify和notifyAll的signal和signalAll),该对象可以Lock锁进行获取。

Lock 实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Conndition对象。


基本使用方法和synchronized一致,下面是改进后的代码:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class ConWithProLockTest {
    public static void main(String[] args) {
        Resource res = new Resource();
		
        new Thread(new Consumer(res)).start();
        new Thread(new Consumer(res)).start();
        new Thread(new Producer(res)).start();
        new Thread(new Producer(res)).start();
	}
}

class Resource{
	private String sName;
	private int id;
	private Boolean flag=false;
	
	private Lock lock = new ReentrantLock();       //申明一个可重入的互斥锁 Lock
	private Condition proCondition = lock.newCondition();   //返回绑定到此 Lock 实例的新 Condition 实例
	private Condition cusCondition = lock.newCondition();
	public void set(String sName) throws InterruptedException{
		lock.lock();    //上锁
		try {
			while (flag) {
				proCondition.await(); // 造成当前线程在接到信号或被中断之前一直处于等待状态。
			}
			this.sName = sName + "--" + id++;
			System.out.println(Thread.currentThread().getName() + "--消费者--" + this.sName);
			flag = true;
			cusCondition.signal(); // 唤醒一个等待线程
		} finally {
			lock.unlock(); // 释放锁,此动作一定要执行
		}
	}
	
	public void out() throws InterruptedException{
		lock.lock();
		try {
			while (!flag) {
				cusCondition.await();
			}
			System.out.println(Thread.currentThread().getName() + "---生产者---" + this.sName);
			flag = false;
			proCondition.signal();
		} finally {
			lock.unlock();
		}
	}
}

class Consumer implements Runnable{
	private Resource res;
	Consumer(Resource res){
		this.res=res;
	}
	
	public void run(){
		while(true){
			try {
				res.set("--电脑--");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

class Producer implements Runnable{
	private Resource res;
	Producer(Resource res){
		this.res = res;
	}
	
	public void run(){
		while(true){
			try {
				res.out();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

注意:一定要导包,否则无法运行。

当然,我们可以理解为,Lock和Condition其实是在给线程取名字,然后依据名字去操作相应的线程。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值