了解线程间通信 - 以生产者/消费者模式实例告诉你

  • 时间:2019-06-11 06:10 作者:Android架构 来源:Android架构 阅读:473
  • 扫一扫,手机访问
摘要:本篇文章基于synchronized和ReentrantLock来讲解如何进行线程间通信,关于上述两种锁的内容,可以看我后续的文章。本篇文章关于线程间通信的讲解将围绕生产者/消费者模式以实例的形式全方位展现线程间通信的方式。(篇幅较长,主要是代码,逻辑很简单,一看就懂,不要有压力)一、线程间通信的两

本篇文章基于synchronized和ReentrantLock来讲解如何进行线程间通信,关于上述两种锁的内容,可以看我后续的文章。本篇文章关于线程间通信的讲解将围绕生产者/消费者模式以实例的形式全方位展现线程间通信的方式。(篇幅较长,主要是代码,逻辑很简单,一看就懂,不要有压力)

一、线程间通信的两种方式

1.wait()/notify()

Object类中相关的方法有notify方法和wait方法。由于wait和notify方法定义在Object类中,因而会被所有的类所继承。这些方法都是final的,即它们都是不能被重写的,不能通过子类覆写去改变它们的行为。

①wait()方法:让当前线程进入等待,并释放锁。

②wait(long)方法:让当前线程进入等待,并释放锁,不过等待时间为long,超过这个时间没有对当前线程进行唤醒,将自动唤醒。

③notify()方法:让当前线程通知那些处于等待状态的线程,当前线程执行完毕后释放锁,并从其余线程中唤醒其中一个继续执行。

④notifyAll()方法:让当前线程通知那些处于等待状态的线程,当前线程执行完毕后释放锁,将唤醒所有等待状态的线程。

wait()方法使用注意事项

①当前的线程必需拥有当前对象的monitor,也即lock,就是锁,才能调用wait()方法,否则将抛出异常java.lang.IllegalMonitorStateException。

②线程调用wait()方法,释放它对锁的拥有权,而后等待另外的线程来通知它(通知的方式是notify()或者者notifyAll()方法),这样它才能重新取得锁的拥有权和恢复执行。

③要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必需放在synchronized方法或者synchronized块中。
wait()与sleep()比较

当线程调用了wait()方法时,它会释放掉对象的锁。

Thread.sleep(),它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放掉对象的锁的。

notify()方法使用注意事项

①假如多个线程在等待,它们中的一个将会选择被唤醒。这种选择是随便的,和具体实现有关。(线程等待一个对象的锁是因为调用了wait()方法)。

②被唤醒的线程是不能被执行的,需要等到当前线程放弃这个对象的锁,当前线程会在方法执行完毕后释放锁。

wait()/notify()协作的两个注意事项

①通知过早

假如通知过早,则会打乱程序的运行逻辑。

public class MyRun {    private String lock = new String("");    public Runnable runnableA = new Runnable() {        @Override        public void run() {            try {                synchronized (lock) {                    System.out.println("begin wait");                    lock.wait();                    System.out.println("end wait");                }            } catch (InterruptedException e) {                e.printStackTrace();            }        }    };    public Runnable runnableB = new Runnable() {        @Override        public void run() {            synchronized (lock) {                System.out.println("begin notify");                lock.notify();                System.out.println("end notify");            }        }    };}

两个方法,分别执行wait()/notify()方法。

public static void main(String[] args) throws InterruptedException {        MyRun run = new MyRun();        Thread bThread = new Thread(run.runnableB);        bThread.start();        Thread.sleep(100);        Thread aThread = new Thread(run.runnableA);        aThread.start();    }

假如notify()方法先执行,将导致wait()方法释放锁进入等待状态后,永远无法被唤醒,影响程序逻辑。应避免这种情况。

②等待wait的条件发生变化

在使用wait/notify模式时,还需要注意另外一种情况,也就是wait等待条件发生了变化,也容易造成程序逻辑的混乱。

Add类,执行加法操作,而后通知Subtract类

public class Add {    private String lock;    public Add(String lock) {        super();        this.lock = lock;    }    public void add(){        synchronized (lock) {            ValueObject.list.add("anyThing");            lock.notifyAll();        }    }}

Subtract类,执行减法操作,执行完后进入等待状态,等待Add类唤醒notify

public class Subtract {    private String lock;    public Subtract(String lock) {        super();        this.lock = lock;    }    public void subtract(){        try {            synchronized (lock) {                if(ValueObject.list.size()==0){                    System.out.println("wait begin ThreadName="+Thread.currentThread().getName());                    lock.wait();                    System.out.println("wait end ThreadName="+Thread.currentThread().getName());                }                ValueObject.list.remove(0);                System.out.println("list size ="+ValueObject.list.size());            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

**线程ThreadAdd **

public class ThreadAdd extends Thread{    private Add pAdd;    public ThreadAdd(Add pAdd) {        super();        this.pAdd = pAdd;    }    @Override    public void run() {        pAdd.add();    }}

**线程ThreadSubtract **

public class ThreadSubtract extends Thread{    private Subtract rSubtract;    public ThreadSubtract(Subtract rSubtract) {        super();        this.rSubtract = rSubtract;    }    @Override    public void run() {        rSubtract.subtract();    }}

先开启两个ThreadSubtract线程,因为list中没有元素,进入等待状态。再开启一个ThreadAdd线程,向list中添加一个元素,而后唤醒两个ThreadSubtract线程

public static void main(String[] args) throws InterruptedException {        String lock = new String("");        Add add = new Add(lock);        Subtract subtract = new Subtract(lock);        ThreadSubtract subtractThread1 = new ThreadSubtract(subtract);        subtractThread1.setName("subtractThread1");        subtractThread1.start();        ThreadSubtract subtractThread2 = new ThreadSubtract(subtract);        subtractThread2.setName("subtractThread2");        subtractThread2.start();        Thread.sleep(1000);        ThreadAdd addThread = new ThreadAdd(add);        addThread.setName("addThread");        addThread.start();    }

输出结果

wait begin ThreadName=subtractThread1
wait begin ThreadName=subtractThread2
wait end ThreadName=subtractThread2
Exception in thread "subtractThread1" list size =0
wait end ThreadName=subtractThread1
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(Unknown Source)
at java.util.ArrayList.remove(Unknown Source)
at com.lvr.communication.Subtract.subtract(Subtract.java:18)
at com.lvr.communication.ThreadSubtract.run(ThreadSubtract.java:12)

当第二个ThreadSubtract线程执行减法操作时,抛出下标越界异常。

起因分析:一开始两个ThreadSubtract线程等待状态,当ThreadAdd线程增加一个元素并唤醒所有线程后,第一个ThreadSubtract线程接着原来的执行到的地点开始继续执行,删除一个元素并输出集合大小。同样,第二个ThreadSubtract线程也如此,可是此时集合中已经没有元素了,所以抛出异常。

处理办法:从等待状态被唤醒后,重新判断条件,看看能否扔需要进入等待状态,不需要进入再进行下一步操作。即把if()判断,改成while()。

public void subtract(){        try {            synchronized (lock) {                while(ValueObject.list.size()==0){                    System.out.println("wait begin ThreadName="+Thread.currentThread().getName());                    lock.wait();                    System.out.println("wait end ThreadName="+Thread.currentThread().getName());                }                ValueObject.list.remove(0);                System.out.println("list size ="+ValueObject.list.size());            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }

这是线程间协作中经常出现的一种情况,需要避免。

2.Condition实现等待/通知

关键字synchronized与wait()和notify()/notifyAll()方法相结合可以实现等待/通知模式,相似ReentrantLock也可以实现同样的功能,但需要借助于Condition对象。

关于Condition实现等待/通知就不详细详情了,可以完全类比wait()/notify(),基本使用和注意事项完全一致。
就只简单详情下类比情况:

condition.await()————>lock.wait()

condition.await(long time, TimeUnit unit)————>lock.wait(long timeout)

condition.signal()————>lock.notify()

condition.signaAll()————>lock.notifyAll()

特殊之处:synchronized相当于整个ReentrantLock对象只有一个单一的Condition对象情况。而一个ReentrantLock却可以拥有多个Condition对象,来实现通知部分线程。

具体实现方式:
假设有两个Condition对象:ConditionA和ConditionB。那么由ConditionA.await()方法进入等待状态的线程,由ConditionA.signalAll()通知唤醒;由ConditionB.await()方法进入等待状态的线程,由ConditionB.signalAll()通知唤醒。篇幅有限,代码示例就不写了。

二、生产者/消费者模式实现

1.一生产与一消费

下面情形是一个生产者,一个消费者的模式。假设场景:一个String对象,其中生产者为其设置值,消费者拿走其中的值,不断的循环往复,实现生产者/消费者的情形。

wait()/notify()实现

生产者

public class Product {    private String lock;    public Product(String lock) {        super();        this.lock = lock;    }    public void setValue(){        try {            synchronized (lock) {                if(!StringObject.value.equals("")){                    //有值,不生产                    lock.wait();                }                String  value = System.currentTimeMillis()+""+System.nanoTime();                System.out.println("set的值是:"+value);                StringObject.value = value;                lock.notify();            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

消费者

public class Consumer {    private String lock;    public Consumer(String lock) {        super();        this.lock = lock;    }    public void getValue(){        try {            synchronized (lock) {                if(StringObject.value.equals("")){                    //没值,不进行消费                    lock.wait();                }                System.out.println("get的值是:"+StringObject.value);                StringObject.value = "";                lock.notify();            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

生产者线程

public class ThreadProduct extends Thread{    private Product product;    public ThreadProduct(Product product) {        super();        this.product = product;    }    @Override    public void run() {        //死循环,不断的生产        while(true){            product.setValue();        }    }}

消费者线程

public class ThreadConsumer extends Thread{    private Consumer consumer;    public ThreadConsumer(Consumer consumer) {        super();        this.consumer = consumer;    }    @Override    public void run() {        //死循环,不断的消费        while(true){            consumer.getValue();        }    }}

开启生产者/消费者模式

public class Test {    public static void main(String[] args) throws InterruptedException {        String lock = new String("");        Product product = new Product(lock);        Consumer consumer = new Consumer(lock);        ThreadProduct pThread = new ThreadProduct(product);        ThreadConsumer cThread = new ThreadConsumer(consumer);        pThread.start();        cThread.start();    }}

输出结果:

set的值是:148827033184127168687409691
get的值是:148827033184127168687409691
set的值是:148827033184127168687449887
get的值是:148827033184127168687449887
set的值是:148827033184127168687475117
get的值是:148827033184127168687475117

Condition方式实现相似,篇幅有限不一律贴出来。

2.多生产与多消费

特殊情况:按照上述一生产与一消费的情况,通过创立多个生产者和消费者线程,实现多生产与多消费的情况,将会出现“假死”。

具体起因:多个生产者和消费者线程。当一律运行后,生产者线程生产数据后,可能唤醒的同类即生产者线程。此时可能会出现如下情况:所有生产者线程进入等待状态,而后消费者线程消费完数据后,再次唤醒的还是消费者线程,直至所有消费者线程都进入等待状态,此时将进入“假死”。

处理方法:将notify()或者signal()方法改为notifyAll()或者signalAll()方法,这样就不怕由于唤醒同类而进入“假死”状态了。

Condition方式实现
生产者

public class Product {    private ReentrantLock lock;    private Condition condition;    public Product(ReentrantLock lock, Condition condition) {        super();        this.lock = lock;        this.condition = condition;    }    public void setValue() {        try {            lock.lock();            while (!StringObject.value.equals("")) {                // 有值,不生产                condition.await();            }            String value = System.currentTimeMillis() + "" + System.nanoTime();            System.out.println("set的值是:" + value);            StringObject.value = value;            condition.signalAll();        } catch (InterruptedException e) {            e.printStackTrace();        }finally {            lock.unlock();        }    }}

消费者

public class Consumer {    private ReentrantLock lock;    private Condition condition;    public Consumer(ReentrantLock lock,Condition condition) {        super();        this.lock = lock;        this.condition = condition;    }    public void getValue(){        try {                lock.lock();                while(StringObject.value.equals("")){                    //没值,不进行消费                    condition.await();                }                System.out.println("get的值是:"+StringObject.value);                StringObject.value = "";                condition.signalAll();        } catch (InterruptedException e) {            e.printStackTrace();        }finally {            lock.unlock();        }    }}

生产者线程和消费者线程与一生产一消费的模式相同。

开启多生产/多消费模式

public static void main(String[] args) throws InterruptedException {        ReentrantLock lock = new ReentrantLock();        Condition newCondition = lock.newCondition();        Product product = new Product(lock,newCondition);        Consumer consumer = new Consumer(lock,newCondition);        for(int i=0;i<3;i++){            ThreadProduct pThread = new ThreadProduct(product);            ThreadConsumer cThread = new ThreadConsumer(consumer);            pThread.start();            cThread.start();        }    }

输出结果:

set的值是:148827212374628960540784817
get的值是:148827212374628960540784817
set的值是:148827212374628960540810047
get的值是:148827212374628960540810047

可见交替地进行get/set实现多生产/多消费模式。
注意:相比一生产一消费的模式,改动了两处。①signal()-->signalAll()避免进入“假死”状态。②if()判断-->while()循环,重新判断条件,避免逻辑混乱。

以上就是Java线程间通信的相关知识,以生产者/消费者模式为例,讲解线程间通信的使用以及注意事项。

自己是从事了七年开发的Android工程师,不少人私下问我,2019年Android进阶该怎样学,方法有没有?

没错,年初我花了一个多月的时间整理出来的学习资料,希望能帮助那些想进阶提升Android开发,却又不知道怎样进阶学习的朋友。【包括高级UI、性能优化、架构师课程、NDK、Kotlin、混合式开发(ReactNative+Weex)、Flutter等架构技术资料】,希望能帮助到您面试前的复习且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

资料获取方式:加入Android架构交流QQ群聊:513088520 ,进群即领取资料!!!

点击链接加入群聊【Android移动架构总群】:加入群聊

资料大全
  • 全部评论(0)
最新发布的资讯信息
【系统环境|】Fortigate飞塔防火墙如何开启DNS转发/DNS代理(2025-10-14 23:58)
【系统环境|】有了它,再也不用担心电脑弹窗广告和病毒啦!(2025-10-14 23:57)
【系统环境|】如何关闭恼人的电脑弹窗广告?2招搞定(2025-10-14 23:55)
【系统环境|】实用软件推荐:电脑广告弹窗多?用他,都给你屏蔽掉!(2025-10-14 23:55)
【系统环境|】Nginx篇01——基本安装配置和静态页面设置(2025-10-14 23:54)
【系统环境|】Linux端口开放,查看,删除,防火墙(2025-10-14 23:53)
【系统环境|】安全HTTP头部配置: 基于CSP与HSTS的Web安全策略(2025-10-14 23:52)
【系统环境|】老K:做私域过1000万的赛道全部都聚焦在女性身上!(2025-10-14 23:51)
【系统环境|】JavaScript跨域问题: 如何解决跨域访问和资源共享的安全策略(2025-10-14 23:51)
【系统环境|】家庭七级财务防火墙(2025-10-14 23:50)
手机二维码手机访问领取大礼包
返回顶部