Java 学习之路 之 线程通信(七十七)

2024-03-05 16:48

本文主要是介绍Java 学习之路 之 线程通信(七十七),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

1,传统的线程通信

假设现在系统中有两个线程,这两个线程分别代表存款者和取钱者——现在假设系统有一种特殊的要求,系统要求存款者和取钱者不断地重复存款、取钱的动作,而且要求每当存款者将钱存入指定账户后,取钱者就立即取出该笔钱。不允许存款者连续两次存钱,也不允许取钱者连续两次取钱。

为了实现这种功能,可以借助于 Object 类提供的 wait()、notify() 和 notifyAII() 3 个方法,这 3 个方法并不属于 Thread 类,而是属于 Object 类。但这 3 个方法必须由同步监视器对象来调用,这可分成以下两种情况。

对于使用 synchronized 修饰的同步方法,因为该类的默认实例(this)就是同步监视器.所以可以在同步方法中直接调用这 3 个方法。

对于使用 synchronized 修饰的同步代码块,同步监视器是 synchronized 后括号里的对象,所以必须使用该对象调用这 3 个方法。

关于这 3 个方法的解释如下。

wait():导致当前线程等待,直到其他线程调用该同步监视器的 notify() 方法或 notifyAII() 方法来唤醒该线程。该 wait() 方法有 3 种形式——无时间参数的 wait(一直等待,直到其他线程通知),带毫秒参数的 wait 和带毫秒、毫微秒参数的 wait(这两种方法都是等待指定时间后自动苏醒)。调用 wait() 方法的当前线程会释放对该同步监视器的锁定。

notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后(使用 wait() 方法),才可以执行被唤醒的线程。

notifyAII():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

程序中可以通过一个旗标来标识账户中是否已有存款,当旗标为 false 时,表明账户中没有存款,存款者线程可以向下执行,当存款者把钱存入账户后,将旗标设为 true,并调用 notiy() 或 notifyAII() 方法来唤醒其他线程;当存款者线程进入线程体后,如果旗标为 true 就调用 wait() 方法让该线程等待。

当旗标为 true 时,表明账户中已经存入了存款,则取钱者线程可以向下执行,当取钱者把钱从账户中取出后,将旗标设为 false,并调用 notify() 或 notifyAII() 方法来唤醒其他线程:当取钱者线程进入线程体后,如果旗标为 false 就调用 wait() 方法让该线程等待。

本程序为 Account 类提供 draw() 和 deposit() 两个方法,分别对应该账户的取钱、存款等操作,因为这两个方法可能需要并发修改 Account 类的 balance Field,所以这两个方法都使用 synchronized 修饰成同步方法。除此之外,这两个方法还使用了 wait()、notifyAII() 来控制线程的协作。

public class Account
{// 封装账户编号、账户余额两个Fieldprivate String accountNo;private double balance;//标识账户中是否已有存款的旗标private boolean flag = false;public Account(){}// 构造器public Account(String accountNo , double balance){this.accountNo = accountNo;this.balance = balance;}// accountNo的setter和getter方法public void setAccountNo(String accountNo){this.accountNo = accountNo;}public String getAccountNo(){return this.accountNo;}// 因此账户余额不允许随便修改,所以只为balance提供getter方法,public double getBalance(){return this.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);// 将标识账户是否已有存款的旗标设为false。flag = false;// 唤醒其他线程notifyAll();}}catch (InterruptedException ex){ex.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);// 将表示账户是否已有存款的旗标设为trueflag = true;// 唤醒其他线程notifyAll();}}catch (InterruptedException ex){ex.printStackTrace();}}// 下面两个方法根据accountNo来重写hashCode()和equals()方法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;}
}

上面程序中的使用 wait() 和 notifyAII() 进行了控制,对存款者线程而言,当程序进入 deposit() 方法后,如果 flag 为 true,则表明账户中已有存款,程序调用 wait() 方法阻塞:否则程序向下执行存款操作,当存款操作执行完成后,系统将 flag 设为 true,然后调用 notifyAII() 来唤醒其他被阻塞的线程——如果系统中有存款者线程,存款者线程也会被唤醒,但该存款者线程执行到①号代码处时再次进入阻塞状态,只有执行 draw() 方法的取钱者线程才可以向下执行。同理,取钱者线程的运行流程也是如此。

程序中的存款者线程循环 100 次重复存款,而取钱者线程则循环 100 次重复取钱,存款者线程和取钱者线程分别调用 Account 对象的 deposit()、draw() 方法来实现。

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;}// 重复100次执行取钱操作public void run(){for (int i = 0 ; i < 100 ; i++ ){account.draw(drawAmount);}}
}

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;}// 重复100次执行存款操作public void run(){for (int i = 0 ; i < 100 ; i++ ){account.deposit(depositAmount);}}
}

主程序可以启动任意多个存款线程和取钱线程,可以看到所有的取钱线程必须等存款线程存钱后才可以向下执行,而存款线程也必须等取钱线程取钱后才可以向下执行。主程序代码如下。

public class DrawTest
{public static void main(String[] args) {// 创建一个账户Account acct = new Account("1234567" , 0);new DrawThread("取钱者" , acct , 800).start();new DepositThread("存款者甲" , acct , 800).start();new DepositThread("存款者乙" , acct , 800).start();new DepositThread("存款者丙" , acct , 800).start();}
}

运行该程序,可以看到存款者线程、取钱者线程交替执行的情形,每当存款者向账户中存入 800 元之后,取钱者线程立即从账户中取出这笔钱。存款完成后账户余额总是 800 元,取钱结束后账户余额总是 0 元。运行该程序,会看到如图 16.12 所示的结果。


从图 16.12 中可以看出,3 个存款者线程随机地向账户中存款,只有 1 个取钱者线程执行取钱操作。只有当取钱者取钱后,存款者才可以存款;同理,只有等存款者存款后,取钱者线程才可以取钱。

图 16.12 显示程序最后被阻塞无法继续向下执行,这是因为 3 个存款者线程共有 300 次存款操作,但 1 个取钱者线程只有 100 次取钱操作,所以程序最后被阻塞!

如图 16.12 所示的阻塞并不是死锁,对于这种情况,取钱者线程已经执行结束,而存款者线程只是在等待其他线程采取钱而已,并不是等待其他线程释放同步监视器。不要把死锁和程序阻塞等同起来!

2,使用 Condition 控制线程通信

如果程序不使用 synchronized 关键字来保证同步,而是直接使用 Lock 对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用 wait()、notify0、notifyAll() 方法进行线程通信了。

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

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

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

await():类似于隐式同步监视器上的 wait() 方法,导致当前线程等待,直到其他线程调用该 Condition 的 signal() 方法或 signaIAII() 方法来唤醒该线程。该 await() 方法有更多变体,如 long awaitNanos(long nanosTimeout)、void awaitUninterruptibly()、awaitUntiI(Date deadline) 等,可以完成更丰富的等待操作。

signal():唤醒在此 Lock 对象上等待的单个线程。如果所有线程都在该 Lock 对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该 Lock 对象的锁定后(使用 await() 方法),才可以执行被唤醒的线程。

signalAII():唤醒在此 Lock 对象上等待的所有线程。只有当前线程放弃对该 Lock 对象的锁定后,才可以执行被唤醒的线程。

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

public class Account
{// 显式定义Lock对象private final Lock lock = new ReentrantLock();// 获得指定Lock对象对应的Conditionprivate final Condition cond  = lock.newCondition(); // 封装账户编号、账户余额两个Fieldprivate String accountNo;private double balance;//标识账户中是否已有存款的旗标private boolean flag = false;public Account(){}// 构造器public Account(String accountNo , double balance){this.accountNo = accountNo;this.balance = balance;}// accountNo的setter和getter方法public void setAccountNo(String accountNo){this.accountNo = accountNo;}public String getAccountNo(){return this.accountNo;}// 因此账户余额不允许随便修改,所以只为balance提供getter方法,public double getBalance(){return this.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);// 将标识账户是否已有存款的旗标设为false。flag = false;// 唤醒其他线程cond.signalAll();}}catch (InterruptedException ex){ex.printStackTrace();}// 使用finally块来释放锁finally{lock.unlock();}}public void deposit(double depositAmount){lock.lock();try{// 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞if (flag)             //①{cond.wait();}else{// 执行存款System.out.println(Thread.currentThread().getName()+ " 存款:" +  depositAmount);balance += depositAmount;System.out.println("账户余额为:" + balance);// 将表示账户是否已有存款的旗标设为trueflag = true;// 唤醒其他线程cond.signalAll();}}catch (InterruptedException ex){ex.printStackTrace();}// 使用finally块来释放锁finally{lock.unlock();}}// 下面两个方法根据accountNo来重写hashCode()和equals()方法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;}
}

用该程序与上面的 Account.java 进行对比,不难发现这两个程序的逻辑基本相似,只是现在显式地使用 Lock 对象来充当同步监视器,则需要使用 Condition 对象来暂停、唤醒指定线程。

该示例程序的其他类与前一个示例程序的其他类完全一样。运行该程序的效果与前一个示例程序的运行效果完全一样,此处不再赘述。

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

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

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

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

put(E e):尝试把 E 元素放入 BlockingQueue 中,如果该队列的元素已满,则阻塞该线程。

take():尝试从 BlockingQueue 的头部取出元素,如果该队列的元素已空‘,则阻塞该线程。

BlockingQueue 继承了 Queue 接口,当然也可使用 Queue 接口中的方法。这些方法归纳起来可分为如下 3 组。

在队列尾部插入元素。包括add(E e)、offer(E e) 和 put(E e) 方法。当该队列已满时,这 3 个方法分别会抛出异常、返回 false、阻塞队列。

在队列头部删除并返回删除的元素。包括 remove()、poll() 和 take() 方法。当该队列已空时,这 3 个方法分别会抛出异常、返回 false、阻塞队列。

在队列头部取出但不删除元素。包括 element() 和 peek() 方法,当队列已空时,这两个方法分别抛出异常、返回 false。

BlockingQueue 包含的方法之间的对应关系如表 16.1 所示。


BlockingQueue 与其实现类之间的类图如图 16.13 所示。


图 16.13 中以黑色方框框出的都是 Java 7 新增的阻塞队列。从图 16.13 可以看到,BlockingQueue 包含如下 5 个实现类。

ArrayBlockingQueue:基于数组实现的 BlockingQueue 队列。

LinkedBlockingQueue:基于链表实现的 BlockingQueue 队列。

PriorityBlockingQueue:它并不是标准的阻塞队列。与前面介绍的 PriorityQueue 类似,该队列调用 remove()、poll()、take() 等方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。PriorityBlockingQueue 判断元素的大小即可根据元素(实现 Comparable 接口)的本身大小来自然排序,也可使用 Comparator 进行定制排序。

SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。

DelayQueue:它是一个特殊的 BlockingQueue,底层基于 PriorityBlockingQueue 实现。不过,DelayQueue 要求集合元素都实现 Delay 接口(该接口里只有一个 long getDelay() 方法),DelayQueue 根据集合元素的 getDalay() 方法的返回值进行排序。

下面以 ArrayBlockingQueue 为例介绍阻塞队列的功能和用法。下面先用一个最简单的程序来测试 BlockingQueue 的 put() 方法。

public class BlockingQueueTest
{public static void main(String[] args)throws Exception{// 定义一个长度为2的阻塞队列BlockingQueue<String> bq = new ArrayBlockingQueue<>(2);bq.put("Java");//与bq.add("Java"、bq.offer("Java")相同bq.put("Java");//与bq.add("Java"、bq.offer("Java")相同bq.put("Java");//① 阻塞线程。}
}

上面程序先定义一个大小为 2 的 BlockingQueue,程序先向该队列中放入 2 个元素,此时队列还没有满,两个元素都可以放入,因此使用 put()、add() 和 offer() 方法效果完全一样。当程序试图放入第三个元素时.如果使用 put() 方法尝试放入元素将会阻塞线程,如上面程序①号代码所示。如果使用 add() 方法尝试放入元素将会引发异常;如果使用 offer() 方法尝试放入元素则会返回 false,元素不会被放入。

与此类似的是,在 BlockingQueue 己空的情况下,程序使用 take()方法尝试取出元素将会阻塞线程;使用 remove() 方法尝试取出元素将引发异常;使用 poll() 方法尝试取出元素将返回 false,元素不会被删除。

掌握了 BlockingQueue 阻塞队列的特性之后,下面程序就可以利用 BlockingQueue 来实现线程通信了。

class Producer extends Thread
{private BlockingQueue<String> bq;public Producer(BlockingQueue<String> bq){this.bq = bq;}public void run(){String[] strArr = new String[]{"Java","Struts","Spring"};for (int i = 0 ; i < 999999999 ; i++ ){System.out.println(getName() + "生产者准备生产集合元素!");try{Thread.sleep(200);// 尝试放入元素,如果队列已满,线程被阻塞bq.put(strArr[i % 3]);}catch (Exception ex){ex.printStackTrace();}System.out.println(getName() + "生产完成:" + bq);}}
}
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 (Exception ex){ex.printStackTrace();}System.out.println(getName() + "消费完成:" + bq);}}
}
public class BlockingQueueTest2
{public static void main(String[] args){// 创建一个容量为3的BlockingQueueBlockingQueue<String> bq = new ArrayBlockingQueue<>(1);// 启动3条生产者线程new Producer(bq).start();new Producer(bq).start();new Producer(bq).start();// 启动一条消费者线程new Consumer(bq).start();}
}

上面程序启动了 3 个生产者线程向 BlockingQueue 集合放入元素,启动了 1 个消费者线程从 BlockingQueue 集合取出元素。本程序的 BlockingQueue 集合容量为 1,因此 3 个生产者线程无法连续放入元素,必须等待消费者线程取出一个元素后,3 个生产者线程的其中之一才能放入一个元素。运行该程序,会看到如图 16.14 所示的结果。

从图 16.14 可以看出.3 个生产者线程都想向 BlockingQueue 中放入元素,但只要其中一个线程向该队列中放入元素之后,其他生产者线程就必须等待,等待消费者线程取出 BlockingQueue 队列里的元素。


这篇关于Java 学习之路 之 线程通信(七十七)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/777158

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06