java 多线程间通信(二)

传统的线程通信

Object提供了三个方法wait(), notify(), notifyAll()在线程之间进行通信,以此来解决线程间执行顺序等问题。

  • wait():释放当前线程的同步监视控制器,并让当前线程进入阻塞状态,直到别的线程发出notify将该线程唤醒。
  • notify():唤醒在等待控制监视器的其中一个线程(随机)。只有当前线程释放了同步监视器锁(调用wait)之后,被唤醒的线程才有机会执行。
  • notifyAll():与上面notify的区别是同时唤醒多个等待线程。

值得注意的是这三个方法是属于Object而不是属于Thread的,但是调用的时候必须用同步监视器来调用,

  • 对于synchronized修饰的同步方法,由于方法所在类对象(this)就是同步监视器,因此可以直接在同步方法中调用这三个方法;
  • 对于同步代码块,synchronized(obj) { ... },则需要用空号钟的obj来调用。

生产者-消费者问题模型

在经典的生产者-消费者问题中,需要使用线程通信来解决。

假设有这么一个场景,有一个线程需要存钱进一个账户,有多个线程需要从这个账户取钱,要求是每次必须先存钱之后才能取钱,而且取钱之后必须存钱,

存钱和取钱不能同时发生两次,而是要保持顺序不变,如何实现这个需求呢。

下面是用同步方法结合线程通信的方式来实现的思路,

  • 首先在Account类中定义两个同步方法,deposit和draw用来确保存款和取款操作的原子性。
  • 在Account类中定义用标识符flag, 由deposit和draw共用。初始值为false,表示只能存款。 如果为false,表示只能取款。
  • 定义一个存款线程类,去调用Account类的同步方法deposit,在deposit中先对flag进行判断,如果不为false,则调用wait阻塞存款线程,等待取款线程发出notice。存款完成之后,将flag改为true.
  • 定义一个取款线程类,去调用Account类的同步方法draw,在draw中先对flag进行判断,如果不为true,则调用wait阻塞取款线程,等待存线程发出notice。取款完成之后,将flag改为false.
  • 定义测试类,同时启动一个(或多个)存款线程进行存款,同时启动多个取款线程去取款,存款(取款)线程之间不会有先后顺序,但是存款和取款直接会有严格的先后顺序,这就解决了生产者消费者问题

下面给出实现代码

在Account类中定义两个同步方法,draw和deposit

package threads.sync;

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;
    }

    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
    
    
    public int hashCode() {
        return accountNo.hashCode();
    }
    
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj != null && obj.getClass() == Account.class) {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
    
    public synchronized void draw(double drawAmount) {
        try {

            if (!flag) {
                wait();
            } else {
                System.out.println(Thread.currentThread().getName()
                        + "        draw money: " + drawAmount);
                balance -= drawAmount;
                System.out.println("            "
                        + " balance : " + balance);
                flag = false;
                notifyAll();
            } 
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
    
    public synchronized void deposit(double depositAmount) {
        try {

            if (flag) {
                wait();
            } else {
                System.out.println(Thread.currentThread().getName()
                        + "        deposit money: " + depositAmount);
                balance += depositAmount;
                System.out.println("            "
                        + " balance : " + balance);
                flag = true;
                notifyAll();
            } 
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }    
    }
    
}

定义一个存款线程类depositThread

package threads.sync;

public class DepositThread extends Thread {
    private Account account;
    private double depositAmount;
    public DepositThread(String name, Account account, double depositAmount) {
        super(name);
        this.setAccount(account);
        this.setDepositAmount(depositAmount);
    }
    public Account getAccount() {
        return account;
    }
    public void setAccount(Account account) {
        this.account = account;
    }
    
    public double getDepositAmount() {
        return depositAmount;
    }
    public void setDepositAmount(double depositAmount) {
        this.depositAmount = depositAmount;
    }
    
    public void run() {
        for(int i=0 ; i<10; i++) {
            account.deposit(depositAmount);
        }

    }

}

定义一个取款线程类depositThread

package threads.sync;

public class DrawThread extends Thread {
    private Account account;
    private double drawAmount;
    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        this.setAccount(account);
        this.setDrawAmount(drawAmount);
    }
    public Account getAccount() {
        return account;
    }
    public void setAccount(Account account) {
        this.account = account;
    }
    public double getDrawAmount() {
        return drawAmount;
    }
    public void setDrawAmount(double drawAmount) {
        this.drawAmount = drawAmount;
    }
    
    public void run() {
        for(int i=0 ; i<10; i++) {
            account.draw(drawAmount);
        }
    }
}

下面是测试类,存款线程中会有10次存款,三个取款线程中总共会有30次取款,

package threads.sync;

public class DrawTest {
    public static void main(String[] args) {
        Account acc = new Account("123456",1000);
        new DrawThread("DrawThread", acc, 800).start();
        new DepositThread("DepositThread-A",acc,800).start();
        new DepositThread("DepositThread-B",acc,800).start();
        new DepositThread("DepositThread-C",acc,800).start();
    }
}

执行结果,

DepositThread-A        deposit money: 800.0
                balance : 1800.0
DrawThread        draw money: 800.0
                balance : 1000.0
DepositThread-B        deposit money: 800.0
                balance : 1800.0
DrawThread        draw money: 800.0
                balance : 1000.0
DepositThread-C        deposit money: 800.0
                balance : 1800.0
DrawThread        draw money: 800.0
                balance : 1000.0
DepositThread-C        deposit money: 800.0
                balance : 1800.0
DrawThread        draw money: 800.0
                balance : 1000.0
DepositThread-C        deposit money: 800.0
                balance : 1800.0
DrawThread        draw money: 800.0
                balance : 1000.0
DepositThread-C        deposit money: 800.0
                balance : 1800.0
DrawThread        draw money: 800.0
                balance : 1000.0
DepositThread-C        deposit money: 800.0
                balance : 1800.0
DrawThread        draw money: 800.0
                balance : 1000.0
DepositThread-A        deposit money: 800.0
                balance : 1800.0

从执行结果中可以看到,三个取款线程ABC执行顺序随机,但是总是在存款完成后,才会进行取款操作,而且无论存款还是取款,都不会同时进行两次。

使用condition控制线程通信

如果程序使用lock来同步线程的话,就要使用condition来进行线程通信。

在lock同步线程中,lock 对象就是一个显示的同步监视器,但是这个显示的同步监视器不直接阻塞或者通知线程,而是通过condition——lock对象通过调用newCondition方法返回一个与lock关联的condition对象,由condition对象来控制线程阻塞(await)和发出信号(single)唤醒其他线程。

与synchronized同步线程方式对应的是,conditions方式也提供了三个方法,

await:类似于synchronized隐式同步控制器对象调用的wait方法,可以阻塞当前线程,直到在别的线程中调用了condition的singal方法唤醒该线程。

signal:随机唤醒一个被await阻塞的线程。注意只有在当前线程已经释放lock同步监视器之后,被唤醒的其他线程才有机会执行。

signalAll:与上面类似,但是是唤醒所有线程。

下面用condition的方式来实现前面的银行取钱的例子,只需要修改Account类,改用lock同步线程,condition线程通信,

package threads.sync;

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

public class Account {
    private String accountNo;
    private double balance;
    private boolean flag = false;
    //显示定义lock对象
    private final Lock lock = new ReentrantLock();
    //获取lock对象对应的condition
    private final Condition cond = lock.newCondition();
    public Account() {}
    
    
    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
    
    
    public int hashCode() {
        return accountNo.hashCode();
    }
    
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj != null && obj.getClass() == Account.class) {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
    
    public void draw(double drawAmount) {
        lock.lock();
        try {
            //if flag = false, means only deposit can be done, draw method will be blocked
            if (!flag) {
                //this.wait();
                cond.await();
            } else {
                System.out.println(Thread.currentThread().getName()
                        + "        draw money: " + drawAmount);
                balance -= drawAmount;
                System.out.println("            "
                        + "    balance : " + balance);
                flag = false;
                //this.notifyAll();
                cond.signalAll();
            } 
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    
    public void deposit(double depositAmount) {
        lock.lock();
        try {
            //if flag = false, means only draw can be done, deposit method will be blocked
            if (flag) {
                //this.wait();
                cond.await();
            } else {
                System.out.println(Thread.currentThread().getName()
                        + "        deposit money: " + depositAmount);
                balance += depositAmount;
                System.out.println("            "
                        + "    balance : " + balance);
                flag = true;
                //this.notifyAll();
                cond.signalAll();
            } 
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    
}

对比用synchronized方式同步线程的例子,前面例子中是隐式的同步监视器(this)调用wait和notify来通信,

而本例是显示同步监视器(lock)的关联对象(condition)调用await和signal来通信,执行结果与前面一样不再给出。

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

BlockingQueue是JAVA5提供的一个队列接口,但这个队列并不是用作一个容器,而是作为线程的同步工具。

它可以很好地解决生产者消费者问题,而且比前面提到的两种方式更为灵活,

BlockingQueue的特征是,

当生产者线程试图向BlockingQueue存入元素时,如果队列已满,生产者线程将会阻塞,

当消费者线程试图从BlockingQueue取出元素时,如果队列为空,消费者线程将会阻塞

对比前面线程通信的例子,synchronized同步方法/代码块和lock+condition方式中,都只能控制生产者和消费者按固定顺序执行,

但BlockingQueue则是可以通过集合中的元素个数(商品数量)来控制线程执行顺序,通过调整集合容量可以控制线程切换的条件。

集合(商品)为空时,消费者阻塞,只能执行生产者线程;集合(商品)已满时,生产者阻塞,只能执行消费者线程。

BlockingQueue接口有很多实现类,下面演示最常用的实现类ArrayBlockQueue控制线程通信,

定义一个生产者线程类Producter

package threads.sync;

import java.util.concurrent.BlockingQueue;

public class Producter extends Thread {
    private BlockingQueue<String> bq;
    
    public Producter(BlockingQueue<String> bq) {
        this.bq = bq;
    }
    
    public void run() {
        String[] strArr = new String[] {
                "Java",
                "Struts",
                "Spring"
        };
        
        for(int i = 0; i<999999; i++) {
            System.out.println(getName()+" 生产者准备生产集合元素");
            try {
                Thread.sleep(200);
                //如果队列已满,线程将阻塞
                bq.put(strArr[i % 3]);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName()+" 生产完成: " + bq);
        }
    }
}

定义一个消费者类Consumer

package threads.sync;

import java.util.concurrent.BlockingQueue;

public class Consumer extends Thread {
    private BlockingQueue<String> bq;
    
    public Consumer(BlockingQueue<String> bq) {
        this.bq = bq;
    }
    
    public void run() {
        
        while (true) {
            System.out.println(getName()+" 消费者准备消费集合元素");
            try {
                Thread.sleep(200);
                //如果队列已空,线程将阻塞
                bq.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName()+" 消费完成: " + bq);
        }
    }
}

在测试类中,定义一个容量为2的阻塞集合,

启动三个生产者线程, 每个线程都在不停生产商品,存入阻塞队列中,

启动一个消费者线程,每个线程也在不停从阻塞队列中取出商品,

package threads.sync;

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

public class BlockingQueueTest {
    public static void main(String[] args) {
        BlockingQueue<String> bq = new ArrayBlockingQueue<String>(2);
        new Producter(bq).start();
        new Producter(bq).start();
        new Producter(bq).start();
        new Consumer(bq).start();
    }
}

执行结果,从执行结果中可以看到,只要集合中有元素且集合没有满,那么生产者和消费者线程都有机会得到执行,具体谁有机会要看谁抢到CPU执行片,

但是当集合空了的时候,例如第7行(Thread-8 消费完成: []),接着又有一个消费者线程执行,但是因此集合为空而阻塞了,此时只有生产者线程能执行,

当集合满了的时候,例如第11行(Thread-7 生产完成: [Java, Java]),接着又有一个生产者线程执行,但是因为集合已满而阻塞了,此时只有消费者线程能执行。

Thread-5 生产者准备生产集合元素
Thread-6 生产者准备生产集合元素
Thread-7 生产者准备生产集合元素
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Java]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: []
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Java]
Thread-6 生产者准备生产集合元素
Thread-7 生产完成: [Java, Java]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Java]
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Java, Struts]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Struts, Struts]
Thread-6 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-7 生产完成: [Struts, Struts]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Struts, Spring]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: [Spring]
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Spring, Spring]
Thread-6 生产者准备生产集合元素
Thread-8 消费完成: [Spring]
Thread-8 消费者准备消费集合元素

上面的例子来自李刚的疯狂JAVA, 但个人认为并不是太好,因为无论在生产者还是消费者线程中,打印bq操作前后的两段日志并不是原子操作,这会导致打印的日志不准确,

例如下面的运行结果,从第5行看到(Thread-6 生产完成: []),刚刚执行完一个生产者线程中的入队操作,但是打印队列却是空的,原因就在于在打印两行日志期间,消费者线做了取元素的操作。

Thread-5 生产者准备生产集合元素
Thread-6 生产者准备生产集合元素
Thread-7 生产者准备生产集合元素
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Java]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: []
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Java]
Thread-6 生产者准备生产集合元素
Thread-7 生产完成: [Java, Java]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Java]
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Java, Struts]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Struts, Struts]
Thread-6 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-7 生产完成: [Struts, Struts]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Struts, Spring]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: [Spring]
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Spring, Spring]
Thread-6 生产者准备生产集合元素
Thread-8 消费完成: [Spring]
Thread-8 消费者准备消费集合元素

 

posted on 2018-08-30 11:12  疯狂的小萝卜头  阅读(167)  评论(0编辑  收藏  举报