[笔记] Java线程通信

4 篇文章 0 订阅

本文主要来自李刚的《疯狂Java讲义》第三版

线程通信

当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但 Java 也提供了一些机制来保证线程协调运行。

我们假设有一个存取款系统,存款跟取款是不同的线程,现在有一个比较特殊的要求,存款与取款的操作一直在进行,一旦执行了存款操作,就立刻执行取款操作,但不能连续进行存款取款操作,其中每次取款都会将所有的钱取出。

1. synchronized

对于 synchronized 修饰的同步方法或同步代码块,可以通过调用 Object 类中的 wait()、notify()、notifyAll()这三个方法来实现线程通信,这三个方法必须由同步监视器对象调用,可以分为以下两种情况:

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

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

  • wait():导致当前线程等待,直到其他线程调用该同步监视器的 notify()方法或 notifyAll()方法来唤醒该线程。该方法有三种形式:无时间参数代表无限期等待直到其他线程通知;有时间参数(带毫秒参数、带毫秒微秒参数)代表等待指定时间后自动苏醒。调用 wait()后当前线程会释放对该同步监视器的锁。
  • notify():唤醒在此同步监视器上等待的单个线程。如果多个线程在等待,则会随机唤醒其中一个线程。只有当前线程放弃对该同步监视器的锁定后,才能执行被唤醒的线程。
  • notifyAll():唤醒在此同步监视器上等待的所有线程,只有当前线程放弃对该同步监视器的锁定后,才能执行被唤醒的线程。

对于系统所要求的功能,可以设置一个 flag 标志,如果有存款,则 flag 为 true,否则为 false,根据此来调用等待唤醒线程的操作,从而实现系统所要求的方法:

账号类 Account:

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;
    }
    //省略accountNo的getset方法
    //因为账户余额不能随便修改,所以只为该参数提供get方法
    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标记
                flag = false;
                //等待本线程执行完释放锁后唤醒其他线程
                notifyAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void deposit(double depositAmount){
        try{
            //如果flag为真,表明账户中已经有人存钱进去,线程等待
            if (flag){
                wait();
            }else{
                System.out.println(Thread.currentThread().getName() + "存钱:" + depositAmount);
                balance += depositAmount;
                System.out.println("账户余额为:" + balance);
                //修改flag标记
                flag = true;
                //等待本线程执行完释放锁后唤醒其他线程
                notifyAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

取钱线程类 DrawThread:

public class DrawThread extends Thread {
    //模拟用户账户
    private Account account;
    //取钱数
    private double drawAmount;
    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    //10次取钱操作
    public void run(){
        for (int i = 0; i < 10; i++) {
            account.draw(drawAmount);
        }
    }
}

存钱线程类 DepositThread:

public class DepositThread extends Thread {
    //模拟用户账户
    private Account account;
    //存钱数
    private double depositAmount;
    public DepositThread(String name, Account account, double depositAmount) {
        super(name);
        this.account = account;
        this.depositAmount = depositAmount;
    }
    //10次存钱操作
    public void run(){
        for (int i = 0; i < 10; i++) {
            account.deposit(depositAmount);
        }
    }
}

主函数:

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

输出结果:

存钱者甲存钱:800.0
账户余额为:800.0
取钱者取钱:800.0
账户余额为:0.0
存钱者丙存钱:800.0
账户余额为:800.0
取钱者取钱:800.0
账户余额为:0.0
存钱者甲存钱:800.0
账户余额为:800.0
取钱者取钱:800.0
账户余额为:0.0
存钱者丙存钱:800.0
账户余额为:800.0
取钱者取钱:800.0
账户余额为:0.0
存钱者甲存钱:800.0
账户余额为:800.0
取钱者取钱:800.0
账户余额为:0.0
存钱者丙存钱:800.0
账户余额为:800.0

其中总共取了十次钱,取款线程执行完毕,存款线程可以看到甲丙交替执行,但如果将循环次数扩大,就会发现甲乙丙三个线程会随机的执行。因为存款线程次数比取款次数多,因此最后程序会因为线程阻塞而不会自动结束,一直在等待取款的操作,
 
 

2. Lock

如果使用 Lock 进行同步,因为不存在隐式同步监视器,就不能使用上面的三种方法进行通信了。因此 Java 提供了一个 Condition 类来保持协调,线程可以通过使用 Lock 产生的 Condition 对象,来使线程等待或唤醒其他线程。

Condition 中有三个方法,分别对应着上面介绍的三个方法:

  • await():对应之前的 wait()方法,导致当前线程暂停,等待下面两种方法唤醒。对比 wait()方法,该方法有更多的变种,能实现更多的等待操作。
  • signal():唤醒在此 Lock 对象上等待的单个线程,与 notify()方法类似。
  • signalAll():唤醒在此 Lock 对象上等待的所有线程,与 notifyAll()方法类似。

实现方式:
账号 Account 类

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;
    //获取锁对象,和Condition对象
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    public Account(){}

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }
    //省略accountNo的getset方法
    //因为账户余额不能随便修改,所以只为该参数提供get方法
    public double getBalance() {
        return balance;
    }
    public void draw(double drawAmount){
        lock.lock();
        try{
            //如果flag为假,表明账户中还没有人存钱进去,线程等待
            if (!flag){
                condition.await();
            }else{
                System.out.println(Thread.currentThread().getName() + "取钱:" + drawAmount);
                balance -= drawAmount;
                System.out.println("账户余额为:" + balance);
                //修改flag标记
                flag = false;
                //等待本线程执行完释放锁后唤醒其他线程
                condition.signalAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void deposit(double depositAmount){
        lock.lock();
        try{
            //如果flag为真,表明账户中已经有人存钱进去,线程等待
            if (flag){
                condition.await();
            }else{
                System.out.println(Thread.currentThread().getName() + "存钱:" + depositAmount);
                balance += depositAmount;
                System.out.println("账户余额为:" + balance);
                //修改flag标记
                flag = true;
                //等待本线程执行完释放锁后唤醒其他线程
                condition.signalAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

 
 

3. 阻塞队列 BlockingQueue

BlockingQueue 有一个特征:当生产者线程试图向 BlockingQueue 中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从 BlockingQueue 中取出元素时,如果该队列已空,则该线程被阻塞。

应用在线程池中时,我们是生产者,将线程放入 BlockingQueue 中,线程池是消费者,通过执行 BlockingQueue 中的线程进行消费。

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

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

BlockingQueue 继承了 Queue 接口,因此也能使用 Queue 中的方法对元素进行操作做,下表是操作的对比:

抛出异常不同返回值阻塞线程指定超时时长
队尾插入元素add(e)offer(e)put(e)offer(e, time, unit)
队头删除元素remove()poll()take()poll(time, unit)
获取、不删除元素element()peek()

 
JDK1.7 中,BlockingQueue 有五个实现类

  • ArrayBlockingQueue:基于数组实现的 BlockingQueue 队列,需要指定队列大小
  • LinkedBlockingQueue:基于链表实现的 BlockingQueue 队列,不指定大小的话默认大小为 Integer.MAX_VALUE
  • PriorityBlockingQueue:与前两种不同,该队列不是先进先出队列,在取出元素时,会根据优先级选择优先级最高的元素
  • SynchronousQueue:同步队列,对该队列的存、取操作必须交替进行
  • DelayQueue:基于 PriorityQueue,一种延时阻塞队列,DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值