java 多线程(三)条件对象

转载请注明出处:http://blog.csdn.net/xingjiarong/article/details/47417383

在上一篇博客中,我们学会了用ReentrantLock来控制线程訪问同一个数据,防止出现Race Condition。这一次呢。我们继续深入的学习,学习一下java中的条件对象。条件对象在多线程同步中用到的比較多。

首先,我们来介绍一下临界区。

临界区:在同步的程序设计中。临界区指的是一个訪问共用资源的程序片段,而这些共用资源又具有无法同一时候被多个线程訪问的特性。 当有线程进入临界区时,其它线程或是进程必须等待,在一些情况下必须在临界区的进入点与离开点採用一些特殊的方法。以确保这些共用资源是被相互排斥使用的。

如今我们来看一个新的样例,这是一个银行转账的样例。在Bank类中,我们声明了一个10个大小的数组,用来表示银行中的10个账户。而相应的数值就是这个账户中相应的金额。Bank类提供了转移账户资金的方法,能够从from账户,转移amount的资金到to账户。还提供了获得这些账户的总金额的方法,getTotalBalabce(),由于资金转移是发生在这10个账户中的,所以不管如何转移。总的金额应该是不变的。

Bank.java

import java.util.concurrent.locks.ReentrantLock;

public class Bank {
    /**
     * 利用数组模拟银行账户
     */
    private final double accounts[];
    private ReentrantLock lock = new ReentrantLock();

    public Bank() {
        accounts = new double[10];
        /*
         * 初始化,使每一个账户都初始有initialBalance金钱
         */
        for (int i = 0; i < 10; i++)
            accounts[i] = 1000;
    }

    /**
     * 资金转移的方法
     *
     * @param from
     *            源账户
     * @param to
     *            目标账户
     * @param amount
     *            转移金额
     */
    public void transfer(int from, int to, double amount) {
        lock.lock();
        try {
            if (accounts[from] < amount)
                return
            System.out.print(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf("%5.2f from %d to %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf(" Total Balance:%5.2f\n", getTotalBalance());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 获取全部账户的总金额
     *
     * @return
     */
    public double getTotalBalance() {
        lock.lock();
        double sum = 0;
        try {

            for (double a : accounts)
                sum += a;
        } finally {
            lock.unlock();
        }
        return sum;
    }

    /**
     * 获取如今账户的数量
     *
     * @return
     */
    public int size() {
        return accounts.length;
    }
}

TransferRunnable这个类实现了Runnable接口,我们在Main函数中拿他做线程,它的run方法就是产生一个随机的目标账户和转移的金额。然后调用Bank类的方法将资金转移到目标账户中去。

TransferRunnable.java


public class TransferRunnable implements Runnable{

    private Bank bank;
    private int fromAccount;

    public TransferRunnable(Bank b,int from){
        bank=b;
        fromAccount=from;
    }

    public void run(){
        try{
            while(true)
            {
                int toAccount = (int)(bank.size()*Math.random());
                double amount = 1000*Math.random();
                bank.transfer(fromAccount, toAccount, amount);
                Thread.sleep(1000);
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

在主程序中,我们创建了10个线程,每一个线程都从一个相应的账户转移一定的资金到随机的账户中去。

Main.java

public class Main {

    public static void main(String[] args) {
        Bank b = new Bank();
        Thread[] threadArray = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threadArray[i] = new Thread(new TransferRunnable(b, i));
        }

        for(int i=0;i<10;i++){
            threadArray[i].start();
        }
    }

}

在这个程序中有共同的变量——10个银行账户的资金,所以就有相应的临界区——Bank类的transfer方法和getTotalBalabce方法,假设不正确临界区加以保护的话,会导致多个线程同一时候进入临界区,而导致结果错误,读者能够试验一下,将ReentrantLock加锁的地方都去掉,看看总金额是不是会产生变化,在我的试验中,不但总金额的数量不正确。就连打印的语句顺序都是不正确的,说明一个线程在System.out.printf()的过程中都被别的线程打断了。

看了这个样例,相信大家对ReentrantLock的使用更加熟练了。

如今我们来考虑这样一个问题,大家看到transfer方法中,假设账户中的金额不足的时候就立马返回而不再发生资金转移,我如今不想这样处理了。我想要这样处理,假设账户资金不足的话,就一直等待,直到其它的账户向这个账户转移足够的资金了。然后再发生资金转移。

问题是。操作这个账户的线程已经获得了lock,其它的线程无法再进入lock方法,也就不可能向这个账户转移相应的资金了,所以说,这个线程必须放弃lock的控制权,让其它的线程获得。

Java中提供了条件对象,Condition类,来配合ReentrantLock实现对临界区的控制,我们对Bank类做以下的改变。

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

public class Bank {
    /**
     * 利用数组模拟银行账户
     */
    private final double accounts[];
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition;

    public Bank() {
        accounts = new double[10];
        /*
         * 初始化,使每一个账户都初始有initialBalance金钱
         */
        for (int i = 0; i < 10; i++)
            accounts[i] = 1000;

        condition = lock.newCondition();
    }

    /**
     * 资金转移的方法
     *
     * @param from
     *            源账户
     * @param to
     *            目标账户
     * @param amount
     *            转移金额
     */
    public void transfer(int from, int to, double amount) {
        lock.lock();
        try {
            while (accounts[from] < amount)
                condition.await();
            System.out.print(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf("%5.2f from %d to %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf(" Total Balance:%5.2f\n", getTotalBalance());
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 获取全部账户的总金额
     *
     * @return
     */
    public double getTotalBalance() {
        lock.lock();
        double sum = 0;
        try {

            for (double a : accounts)
                sum += a;
        } finally {
            lock.unlock();
        }
        return sum;
    }

    /**
     * 获取如今账户的数量
     *
     * @return
     */
    public int size() {
        return accounts.length;
    }
}

首先,声明了一个Condition类的对象引用变量condition,在Bank的构造方法中进行的初始化。初始化是是调用的ReentrantLock类的方法newCondition(),说明Condition对象时依赖于ReentrantLock对象的。一个ReentrantLock对象能够创建多个条件对象。名字通常以控制的条件来命名。

然后在transfer方法中,以下两行是关键:

while (accounts[from] < amount)
    condition.await();

用while循环检測账户的剩余金额,假设剩余金额不足就调用await()方法,await()会堵塞线程,并释放线程保持的锁。这时其它的线程就能够进入临界区了,当账户的剩余金额满足条件时,等待的线程也不会主动的唤醒,直到有一个线程调用了signalAll()方法。signalAll()方法唤醒全部由于不满足条件而等待的线程,可是线程不一定能够继续向下运行,由于signalAll()唤醒线程时,并非告诉线程你的条件满足了,能够继续向下运行了,而是告诉线程,如今条件改变,你能够又一次检測条件是否满足了。假设条件被满足。那么也唯独一个线程能够继续向下运行。由于一旦一个对象获得了临界区的锁,其它的线程就不能再进入临界区了。

那么应当什么时候调用signalAll()方法呢,从经验上将应该在对象的状态有利于等待线程的方向改变时调用。

这里我们再发生资金转移后调用,由于资金转移后。其它的线程有可能满足条件了。

注意:通常。await()的调用应该在例如以下形式的循环体中:

while(!(ok to proceed))
    condition.await();

这里的while循环不能换成if条件语句,由于被别的线程用signalAll方法唤醒的线程,不过条件可能满足了,而不是条件一定满足了。假设不用while循环继续检測的话,就会造成条件不满足的线程继续向下运行,从而产生错误。

当一个线程拥有某个条件的锁时。它只能够在该条件上调用await(),signalAll()。signal()这三个方法。如今我们讲讲第三个方法。

signal和signalAll的差别是,signal不会将全部的等待线程唤醒。而是随机选择一个线程唤醒,而signalAll是将全部的等待线程都唤醒。

最后附上源代码:http://download.csdn.net/detail/xingjiarong/9010675

在下一篇博客里。我会为大家介绍java的synchronized关键字,希望与大家一起学习一起进步,请大家继续关注我的博客,假设大家支持我的话,就顶我一下吧。

时间: 2024-10-14 19:46:33

java 多线程(三)条件对象的相关文章

java多线程三之线程协作与通信实例

多线程的难点主要就是多线程通信协作这一块了,前面笔记二中提到了常见的同步方法,这里主要是进行实例学习了,今天总结了一下3个实例: 1.银行存款与提款多线程实现,使用Lock锁和条件Condition.     附加 : 用监视器进行线程间通信 2.生产者消费者实现,使用LinkedList自写缓冲区. 3.多线程之阻塞队列学习,用阻塞队列快速实现生产者消费者模型.    附加:用布尔变量关闭线程        在三种线程同步方法中,我们这里的实例用Lock锁来实现变量同步,因为它比较灵活直观.

Java多线程(三)

本文主要接着前面多线程的两篇文章总结Java多线程中的线程安全问题. 一.一个典型的Java线程安全例子 1 public class ThreadTest { 2 3 public static void main(String[] args) { 4 Account account = new Account("123456", 1000); 5 DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account,

Java多线程——&lt;三&gt;简单的线程执行:Executor

一.概述 按照<Java多线程——<一><二>>中所讲,我们要使用线程,目前都是显示的声明Thread,并调用其start()方法.多线程并行,明显我们需要声明多个线程然后都调用他的start方法,这么一看,似乎有些问题:第一.线程一旦多了,声明势必是个问题:第二.多线程启动如果通过手动执行的话,那可能一个线程已经跑完了,另外一个还没起来(我推测可能会出现这个问题).所以,我们在想,如果有个管家,能够帮我们管理这么多线程,只需要把我们定义的任务交给管家,管家就能够帮我们

Java多线程对同一个对象进行操作

示例: 三个窗口同时出售20张票. 程序分析: 1.票数要使用一个静态的值. 2.为保证不会出现卖出同一张票,要使用同步锁. 3.设计思路:创建一个站台类Station,继承THread,重写run方法,在run方法内部执行售票操作! 售票要使用同步锁:即有一个站台卖这张票时,其他站台要等待这张票卖完才能继续卖票! package com.multi_thread; //站台类 public class Station extends Thread { // 通过构造方法给线程名字赋值 publ

Java 多线程 三种实现方式

Java多线程实现方式主要有三种:继承Thread类.实现Runnable接口.使用ExecutorService.Callable.Future实现有返回结果的多线程.其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的. 1.继承Thread类实现多线程继承Thread类的方法尽管被我列为一种多线程实现方式,但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法.start()方法

java 线程(三) :对象的共享

可见性: 我们希望确保一个线程修改了对象的状态后,其他线程能够看到发生的状态变化. 例:在没有同步的情况下共享变量 public class NoVisibility { private static boolean ready; private static int number; public static class ReaderThread extends Thread { public void run(){ while(!ready) Thread.yield(); System.o

JAVA多线程(三) 线程池和锁的深度化

github演示代码地址:https://github.com/showkawa/springBoot_2017/tree/master/spb-demo/src/main/java/com/kawa/thread 1.线程池 1.1 线程池是什么 Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池.在开发过程中,合理地使用线程池能够带来3个好处. 第一:降低资源消耗.通过重复利用已创建的线程降低线程创建和销毁造成的消耗. 第二:提高响应速度.当任务

java多线程(三)-Executors实现的几种线程池以及Callable

从java5开始,类库中引入了很多新的管理调度线程的API,最常用的就是Executor(执行器)框架.Executor帮助程序员管理Thread对象,简化了并发编程,它其实就是在 提供了一个中间层,方便程序员管理异步任务的执行,而又不用显式的管理线程的生命周期. Executor采用了线程池实现,也更节约开销,因为是我们启动新线程的首选方法. 示例代码:src/thread_runnable/CachedThreadPool.java 1 public class CachedThreadPo

Java多线程-静态条件与临界区

以下内容转自http://ifeve.com/race-conditions-and-critical-sections/: 在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源.如,同一内存区(变量,数组,或对象).系统(数据库,web services等)或文件.实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的. 多线程同时执行下面的代码可能会出错: public class Counter {

java多线程(三)——锁机制synchronized(同步语句块)

用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法之行一个长时间的任务,那么B线程必须等待比较长的时间,在这样的情况下可以使用synchronized同步语句快来解决. 一.用同步代码块解决同步方法的弊端 Task类 1 package com.weishiyao.learn.day4.testSynchorized.ep2; 2 3 public class Task { 4 5 private String getData1; 6 private Stri