您的位置:首页 > 职场人生

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

2014-10-11 09:47 495 查看
-------
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其实是在给线程取名字,然后依据名字去操作相应的线程。


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: