多线程二(线程通信)

线程通信

一. 传统的线程通信

Object类提供了三个方法由同步监视器调用。分为两种情况

  • 对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
  • 对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。

关于这三个方法的解释如下:

  • wait():导致当前线程等待,知道其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程,该wait()方法有两种形式:无时间参数的wait(一直等待,直到其他线程通知);带时间参数的wait(等待指定时间后自动唤醒)。调用wait()方法的当前线程会释放对该同步监视器的锁定。
  • notify():唤醒此同步监视器上等待的单个线程。如果所有线程都在此同步监视器等待,则会任意唤醒其中一个线程。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
  • notifyAll():唤醒此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。

下面程序示范了存钱,取钱的两个线程。存款者和取款者不断重复存款、取钱的操作,要求存款者将钱存入指定账户后,取钱者立即取出这笔钱。不允许存款者连续两次存款,不允许取钱者两次取钱。

package com.gdut.renentrantLock;

public class Account {
    private String accountNo;
    private double balance;
    //标识账户中是否有存款的旗标
    private boolean flag = false;
    public Account(){}

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }
    //因为账户余额不允许修改,所以只提供getter方法
    public double getBalance(){
        return balance;
    }

    public synchronized void draw(double drawAmount){
        try {
            //如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
            if (!flag) {
                wait();
            } else {
                System.out.println(Thread.currentThread().getName()+"取钱"+drawAmount);
                balance -= drawAmount;
                System.out.println("账户余额为:"+balance);
                flag = false;
                notifyAll();
            }
        }catch(InterruptedException ie){
            ie.printStackTrace();
        }
    }

    public synchronized void desposit(double depositAmount) {
        try {
            if (flag) {
                //如果flag为真,表明账户中已有人存钱进去,存钱方法阻塞
                wait();
            } else {
                System.out.println(Thread.currentThread().getName() + "存款" + depositAmount);
                balance += depositAmount;
                System.out.println("账户余额为:" + balance);
                flag = true;
                notifyAll();
            }
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}
package com.gdut.renentrantLock;

public class DrawThread extends Thread {
    private Account account;
    private double drawAmount;
    public DrawThread(String name,Account acount,double drawAmount){
        super(name);
        this.account = acount;
        this.drawAmount = drawAmount;
    }

    public void run(){
        for (int i = 0; i < 100; i++) {
            account.draw(drawAmount);
        }
    }
}
package com.gdut.renentrantLock;

public class DepositThread extends Thread{

        private Account account;
        private double depositAmount;
        public DepositThread(String name,Account acount,double depositAmount){
            super(name);
            this.account = acount;
            this.depositAmount = depositAmount;
        }

        public void run(){
            for (int i = 0; i < 100; i++) {
                account.desposit(depositAmount);
            }
        }

}
package com.gdut.renentrantLock;

public class DrawTest {
    public static void main(String[] args) {
        Account account = new Account("1234567",0);
        new DrawThread("取钱者",account,800).start();
        new DepositThread("存款者甲",account,800).start();
        new DepositThread("存款者乙",account,800).start();
        new DepositThread("存款者丙",account,800).start();
    }
}

效果如图:

从结果可以看出,程序最后被阻塞无法继续向下执行,这是因为3个取款者线程共有300次操作,而一个取款者只有100次取钱操作,所以程序最后被阻塞。

二. 使用Condition通信

当使用Lock对象来保证来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。

Condition将同步监视器方法(wait(),notify()和notifyAll())分解成截然不同的对象,以便通过将这些对象与Lock对象结合使用,为每个对象提供多个等待值(wait-set)。在这种情况下,Lock代替了同步方法或同步代码块,Condition替代了同步监视器的功能。

Condition实例被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象的newCondition()方法即可,Condition对象提供了如下三个方法:

  • await():类似于隐式同步监视器的wait()方法,导致当前线程等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程。该await()方法有更多变体,如long awaitNanos(long nanosTimeout)、void awaitUninterruptibly()、awaitUntil(Date deadline)等,可以完成更丰富的等待操作。
  • signal():唤醒此Lock对象等待的单个线程。如果所有线程都在此Lock对象上等待,则会任意唤醒其中一个线程。只有当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程
  • signalAll():唤醒此Lock对象等待的单个线程。只有当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程

下面程序Account使用Lock对象来控制同步,并使用Condition对象来控制线程的协调运行。

package com.gdut.Condition;

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

public class Account {
    //显式定义Lock对象
    private final Lock lock = new ReentrantLock();
    //获得指定Lock对象的对应的Condition
    private final Condition cond= lock.newCondition();
    private String accountNo;
    private double balance;
    //标识账户中是否有存款的旗标
    private boolean flag = false;
    public Account(){}

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }
    //因为账户余额不允许修改,所以只提供getter方法
    public double getBalance(){
        return balance;
    }

    public void draw(double drawAmount){
        //加锁
        lock.lock();
        try {
            // 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
            if (!flag) {
                cond.wait();
            } else {
                System.out.println(Thread.currentThread().getName()+"取钱"+drawAmount);
                balance -= drawAmount;
                System.out.println("账户余额为:"+balance);
                flag = false;
                cond.signalAll();
            }
        }catch(InterruptedException ie){
            ie.printStackTrace();
        }finally{
            lock.unlock();
        }
    }

    public  void desposit(double depositAmount) {
        //加锁
        lock.lock();
        try {
            if (flag) {
                //如果flag为真,表明账户中已有人存钱进去,存钱方法阻塞
                cond.await();
            } else {
                System.out.println(Thread.currentThread().getName() + "存款" + depositAmount);
                balance += depositAmount;
                System.out.println("账户余额为:" + balance);
                flag = true;
                cond.signalAll();
            }
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
}

该程序的其他类和执行效果完全跟上面一样。

三. 使用阻塞队列(BlockingQueue)控制线程通信

Java 5提供一个BlockingQueue接口,它是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程同步的工具。BlockingQueue有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已满,则该线程被阻塞。

程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,则可很好的控制线程的通信。

BlockingQueue提供如下两个支持阻塞的方法:

  • put(E e):尝试把e元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
  • take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

BlockingQueue继承了Queue接口,也可以使用Queue的方法,归纳为三类

  • 在队列尾部插入元素。包括add(E e)、offer(E e)和put(E e)方法,当该队列以满时,这三个方法分别会抛出异常、返回法false、阻塞队列。
  • 在队列头部删除并返回删除的元素。包括remove()、poll()、和take()方法当该队已空时,这三个方法分别会抛出异常、返回法false、阻塞队列。
  • 在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。

对应关系如下:

BlockingQueue包含的方法之间的对应关系
  抛出异常 不同返回值 阻塞线程 指定头时时长
队尾插入元素 add(E e) offer(E e) put(E e) offer(e,time,unit)
队头删除元素 remove() poll() take() poll(time,unit)
获取、不删除元素 element() peek()

BlockingQueue有5个实现类。

  • ArrayBlockingQueue:基于数组实现的BlockingQueue队列
  • LinkkedBlockingQueue:基于链表实现的BlockingQueue队列
  • PriorityBlockingQueue:并不是标准的阻塞队列,该队列调用方法取出元素时,是取出队列中最小的元素,可使用Comparator定制排序。
  • SynchronousQueue:同步队列。对该队列的存取操作必须交替执行。
  • DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现。不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法),DelayQueue根据集合元素的getDelay()方法的返回值进行排序。

下面以ArrayBlockingQueue为例介绍阻塞队列的功能和用法。

package com.gdut.thread.blockingqueue;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

class Producer extends Thread{
    private BlockingQueue<String> bq;
    public Producer(BlockingQueue<String> bq){
        this.bq = bq;
    }

    @Override
    public void run() {
        String[] strArr = new String[]{"java","Struts","Spring"};
        for (int i = 0; i < 99 i++) {
            System.out.println("生产者准备生产集合元素!");
            try {
                Thread.sleep(200);
                //尝试放入元素,如果队列已满,则线程被阻塞
                bq.put(strArr[i%3]);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName()+"生产完成:"+bq);
        }
    }
}

class Consummer extends Thread{
    private BlockingQueue<String> bq;
    public Consummer(BlockingQueue<String> bq){
        this.bq = bq;
    }

    @Override
    public void run() {
        while(true) {
            System.out.println("消费者准备消费集合元素!");
            try {
                Thread.sleep(200);
                bq.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName()+"消费完成:"+bq);
        }
    }
}

public class BlockingqueueTest {
    public static void main(String[] args) {
        BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
        //启动三个生产者线程
        new Producer(bq).start();
        new Producer(bq).start();
        new Producer(bq).start();
        //启动一个消费者线程
        new Consummer(bq).start();
    }
}

可以看出,只要一个线程向该队列放入元素,其他生产者线程就必须等待,等待消费者线程取出队列里的元素

原文地址:https://www.cnblogs.com/yumiaoxia/p/9057973.html

时间: 2024-07-30 13:42:10

多线程二(线程通信)的相关文章

JavaSE:多线程补充--线程通信

线程通信我认为是多线程中最难掌握的部分了,这里通过两个例子来说明一下. 第一个: 使用两个线程打印 1-100. 线程1, 线程2 交替打印 public class Print implements Runnable{ int i = 1; public void run(){ while(true){ synchronized(this){ if(i<100){ notify(); System.out.println(Thread.currentThread().getName() + &qu

多线程之线程通信条件Condition

Condition是Locks锁下的另一种线程通信之间唤醒.阻塞的实现.它下面的await,和signal能够实现Object下的wait,notify和notifyAll的全部功能,除此之外改监视器和已绑定到每个条件,可以实现多条件的监听.Condition实质是被绑定到一个锁上,腰围特定的Lock实例获得Condition,即用 newCondition()方法. Condition下的await()相对于Object下的wait(); 阻塞或中断之前状况,让其处于等待状态. Conditi

Java多线程之线程通信

线程通信的例子:使用两个线程打印 1-100,线程1.线程2交替打印.涉及到的三个方法:wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器.notify():一旦执行此方法,就会唤醒被wait的一个线程.如果有多个线程被wait,就唤醒优先级高的那个.notifyAll():一旦执行此方法,就会唤醒所有被wait的线程. 说明:1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中.2.wait(),notify(),notifyA

Java多线程 二 线程间通信

线程间通信: 多个线程在处理同一资源,但是 等待唤醒机制 涉及的方法: 1.wait() 让线程处于冻结状态,被wait的线程会被存储到线程池中. 2.notify() 唤醒线程池中的一个线程(任意) 3.notifyAll() 唤醒线程池中的所有线程.. 这些方法都必须定义在同步中, 因为这些方法是用于操作线程状态的方法. 必须明确到底操作的那个锁上的线程. 为什么操作线程的方法wait notify notifyAll定义在了Object中. 因为这些方法是监视器方法,监视器其实就是锁. 锁

多线程之线程通信条件Condition二

接上一篇,实现Condition三个条件,有这样一个应用: 1. 有三个进程,第一个进程运行1次,第二个进程运行2次,第三个进程运行3次: 2. 先运行第二个进程,然后第一个,然后第三个: 3.  依次运行5次循环. 分析: 此时若用Object的wait和notify是实现不了的,我们能够用Lock锁的Condition实现,我们须要定义三个信号条件,分别控制这三个进程. 实现例如以下: package andy.thread.test; import java.util.concurrent

C# 多线程(二) 线程同步基础(上)

本系列的第一篇简单介绍了线程的概念以及对线程的一些简单的操作,从这一篇开始讲解线程同步,线程同步是多线程技术的难点.线程同步基础由以下几个部分内容组成 1.同步要领(Synchronization Essentials) 2.锁(Locking) 3.线程安全(Thread Safety) 4.事件等待句柄(Signaling with Event Wait Handles) 5.同步上下文(Synchronization Contexts) 同步要领(Synchronization Essen

多线程(二) 线程的安全隐患

有了多线程就有了资源竞争,当多个线程对同一资源进行操作时就容易出现安全隐患. 下面举一个卖票的例子来说明线程的安全隐患 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @interface ViewController () @property (assign, nonatomic)NSInteger ticke

Android 多线程及线程通信

AsyncTask AsyncTask,它使创建需要与用户界面交互的长时间运行的任务变得更简单.相对来说AsyncTask更轻量级一些,适用于简单的异步处理,不需要借助线程和Handler即可实现. AsyncTask是抽象类.AsyncTask定义了三种泛型类型 Params,Progress和Result. Params 启动任务执行的输入参数,比如HTTP请求的URL. Progress 后台任务执行的百分比. Result 后台执行任务最终返回的结果,比如String. AsyncTas

C# 多线程(二) 线程同步基础

本系列的第一篇简单介绍了线程的概念以及对线程的一些简单的操作,从这一篇开始讲解线程同步,线程同步是多线程技术的难点.线程同步基础由以下几个部分内容组成 1.同步要领(Synchronization Essentials) 2.锁(Locking) 3.线程安全(Thread Safety) 4.事件等待句柄(Signaling with Event Wait Handles) 5.同步上下文(Synchronization Contexts) 同步要领(Synchronization Essen

AsyncTask、多线程及线程通信

AsyncTask,它使创建需要与用户界面交互的长时间运行的任务变得更简单.相对来说AsyncTask更轻量级一些,适用于简单的异步处理,不需要借助线程和Handler即可实现. AsyncTask是抽象类.AsyncTask定义了三种泛型类型 Params,Progress和Result. Params 启动任务执行的输入参数,比如HTTP请求的URL. Progress 后台任务执行的百分比. Result 后台执行任务最终返回的结果,比如String. AsyncTask的执行分为四个步骤