多线程(十)多线程编程示例

2023-10-26

本系列文章:
  多线程(一)线程与进程、Thread
  多线程(二)Java内存模型、同步关键字
  多线程(三)线程池
  多线程(四)显式锁、队列同步器
  多线程(五)可重入锁、读写锁
  多线程(六)线程间通信机制
  多线程(七)原子操作、阻塞队列
  多线程(八)并发容器
  多线程(九)并发工具类
  多线程(十)多线程编程示例

一、交替输出1A2B3C4D5E…

  用两个线程,一个输出数字,一个输出字母,最后的结果是交替输出1A2B3C4D5E…

1.1 synchronized/wait/notify

  说到线程的运用,最先想到的可能就是“synchronized/wait/notify”的组合:

  1. 用synchronized锁住一个对象,达到同一时间只能有一个线程输出的目的;
  2. 用wait和notify进行线程间的通信,达到交替输出的目的。

  上面的思路总体上来说没有问题,不过两个线程同时启动时,获得CPU时间片的次序是不固定的,因此需要用一个变量来控制先输出数字。示例:

public class ThreadTest {
    
	private static volatile boolean t2Started = false;
    final Object lock = new Object();
    char[] arrI = "1234567".toCharArray();
    char[] arrC = "ABCDEFG".toCharArray();

    public static void main(String[] args) {
		new ThreadTest().testSyncWaitNofiy();
	}
    
    public void testSyncWaitNofiy() {
        new Thread(() -> {
            synchronized (lock) {
                while (!t2Started) {
                    try {
                    	lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                for (char c : arrC) {
                    System.out.print(c);
                    try {
                    	lock.notify();
                    	lock.wait();//让出锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();//必须,否则无法终止程序
            }
        }, "t1").start();
        
        new Thread(() -> {
            synchronized (lock) {
                for (char c : arrI) {
                    System.out.print(c);
                    t2Started = true;
                    try {
                    	lock.notify();
                    	lock.wait();//让出锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();//必须,否则无法终止程序
            }
        }, "t2").start();
    }
}

  以上是经典的交替输出写法,以上代码有两种执行顺序:

  • 1、t1先执行
      假如t1先获得CPU时间片,会走入以下逻辑:

      接着t1线程被暂停,t2线程获得锁,将t2Started变量置为true,然后打印数字,唤醒t1,t2线程自身被暂停。
      这时t1线程获得锁,开始打印字母,然后唤醒t2,自己暂停 ------> t2线程获得锁,开始打印数组,然后唤醒t1,自己暂停…
      最后,t2输出数字"7",然后唤醒t1,自己暂停。t1输出字母"G",然后唤醒t2,自己暂停。这时t1线程中第二个notify的作用就体现出来了。假如t1中没有第二个notify,在t1获得锁后,什么也不做,然后释放锁。但是由于没有唤醒其他被暂停的线程,t2将会一直被暂停!
      所以即便t1线程的数字数组arrI已经不打印任何字母了,还是需要将暂停的t2线程唤醒,达到两个线程都停止的目的。
  • 2、t2先执行
      如果t2线程先获得时间片,相当于直接进入打印数字的逻辑,t1中while循环相关的代码不会被执行。

1.2 Condition/await/signal

  用Lock和synchronized的逻辑,总体上是一样的,不过是将隐式锁换成了显式锁而已。示例:

public class ThreadTest {
    
	private static volatile boolean t2Started = false;
    char[] arrI = "1234567".toCharArray();
    char[] arrC = "ABCDEFG".toCharArray();

    public static void main(String[] args) {
		new ThreadTest().testLockCondition();
	}
    
    public void testLockCondition() {
        Lock lock = new ReentrantLock();
        Condition conditionT1 = lock.newCondition();
        Condition conditionT2 = lock.newCondition();
        new Thread(() -> {
            try {
                lock.lock();
                while (!t2Started) {
                    try {
                    	conditionT1.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                for (char c : arrI) {
                    System.out.print(c);
                    conditionT2.signal();
                    conditionT1.await();
                }
                conditionT2.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t1").start();
        
        new Thread(() -> {
            try {
                lock.lock();
                for (char c : arrC) {
                    System.out.print(c);
                    t2Started = true;
                    conditionT1.signal();
                    conditionT2.await();
                }
                conditionT1.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t2").start();
    }
}

二、生产者–消费者问题

  生产者–消费者问题在之前的文章章已经介绍过,这里再写这个例子,是为了可以将这个例子和交替输出字符串的例子放在一起对比看:交替输出字符串的问题要达到交替输出(两个线程按顺序获取锁)的目的,所以在各自的线程中在暂停自身前先唤醒对方;而生产者消费者问题不需要两个线程交替执行,所以没有在暂停自身前唤醒对方的操作
  在生产者—消费者问题中,生产者的主要职责是生产产品,产品既可以是数据,也可以是任务。消费者的主要职责是消费生产者生产的产品,这里的消费包括对产品所代表的数据进行加工处理或执行产品所代表的任务。
  生产者–消费者问题中,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据。
  为了解耦生产者和消费者的关系,通常会采用共享数据区的方式,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;消费者只需要从共享数据区中去获取数据,不再需要关心生产者的行为。这个共享数据区域中应该具备线程间并发协作的功能:

  1. 如果共享数据区已满的话,阻塞生产者继续放入数据;
  2. 如果共享数据区为空的话,阻塞消费者继续消费数据。

  在实现生产者消费者问题时,可以采用三种方式:

  1. 使用Object的wait/notify的消息通知机制;
  2. 使用Condition的await/signal的消息通知机制;
  3. 使用BlockingQueue实现(BlockingQueue在后续文章详细介绍)。

  通常,生产者和消费者的处理能力是不同的,即生产者生产产品的速率和消费者消费产品的速率是不同的,较为常见的情形是生产者的处理能力比消费者的处理能力大。这种情况下,传输通道所起的作用不仅仅作为生产者和消费者之间传递数据的中介,它在一定程度上还起到一个平衡生产者和消费者处理能力的作用。
  按照生产者数量和消费者数量的组合来划分,可以分为以下几种:

类别 生产者线程数量 消费者线程数量
单生产者-单消费者 1 1
单生产者-多消费者 1 N(N>=2)
多生产者-多消费者 N(N>=2) N(N>=2)
多生产者-单消费者 N(N>=2) 1

2.1 synchronized/wait/notify

  此种方式指的是:通过配合调用Object对象的wait方法和notify方法或notifyAll方法来实现线程间的通信,简单来说可以分为两个步骤:

  • 在线程中调用wait方法,将阻塞当前线程;
  • 其他线程调用了调用notify方法或notifyAll方法进行通知之后,当前线程才能从wait方法返回,继续执行下面的操作。

  假设有一个箱子,最多只能装10个苹果,箱子里没苹果时不能消费(简单理解为从箱子里往外取苹果),箱子里苹果的数量为10时不能生产(简单理解为向箱子里放苹果)。要实现这一效果,至少有三个点需要关注:

  1. 要达到阻塞生产者和消费者两种效果,需要用不同的条件判断+wait实现
  2. 要达到通知生产者和消费者正常运行下去的效果,需要用notify/notifyAll实现(具体用哪种通知方法看需求,notify是随机通知一个,notifyAll是通知所有);
  3. 存放共享数据,需要选择合适的集合类(该例子比较简单,并没有用集合类,而是用了一个变量来实现)。

  示例:

//箱子,存量苹果的容器
public class Box {
	//箱子中目前苹果的数量
    int num;

    public synchronized void put() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        //10是箱子中能存放苹果的最大数量
        while (num == 10) {             
            try {
                System.out.println("箱子满了,生产者暂停");
                //等待消费者消费一个才能继续生产,所以要让出锁
                this.wait();                       
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        num++;
        System.out.println("箱子未装满,开始生产,生产后箱子中的苹果数量:"+num);
        //唤醒可能因为没苹果而等待的消费者
        this.notify();     
    }
    
    public synchronized void take() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        while (num == 0) {          
            try {
                System.out.println("箱子空了,消费者暂停");
                //等待生产者生产一个才能继续消费,所以要让出锁
                this.wait();                      
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        num--;
        System.out.println("箱子中有了苹果,开始消费,消费后箱子中的苹果数量:"+num);
        //唤醒可能因为苹果满了而等待的生产者
        this.notify();                    
    }
}

//消费者
public class Consumer implements Runnable {
	 
    private Box box;
 
    public Consumer(Box box) {
        this.box= box;
    }
 
    @Override
    public void run() {
        while (true){
            box.take();
        }
    }
}

//生产者
public class Producer implements Runnable {
	 
    private Box box;
 
    public Producer(Box box) {
        this.box= box;
    }
 
    @Override
    public void run() {
        while (true){
            box.put();
        }
    }
}

public class ConsumerAndProducer {
	 
    public static void main(String[] args) {
        Box box = new Box();
        //生产者线程
        Producer p1 = new Producer(box);  
        //消费者线程
        Consumer c1 = new Consumer(box);    
 
        new Thread(p1).start();
        new Thread(c1).start();
    }
}

  结果示例:

2.2 Condition/await/signal

  该种方式实现生产者消费者,和之前一种方式原理是一样的,不过是将隐式锁换成了显式锁而已。以上个小节的功能为例,修改Box.java代码即可,示例:

//箱子,存量苹果的容器
public class Box {
	//箱子中目前苹果的数量
    int num;
    Lock lock = new ReentrantLock();
    Condition full = lock.newCondition();
    Condition empty = lock.newCondition();

    public void put() {
    	lock.lock();
    	try {
	        try {
	            Thread.sleep(1000);
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	        
	        //10是箱子中能存放苹果的最大数量
	        while (num == 10) {             
	            try {
	                System.out.println("箱子满了,生产者暂停");
	                //等待消费者消费一个才能继续生产,所以要让出锁
	                full.await();                       
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	        }
	        
	        num++;
	        System.out.println("箱子未装满,开始生产,生产后箱子中的苹果数量:"+num);
	        //唤醒可能因为没苹果而等待的消费者
	        empty.signal(); 
    	}finally {
    		lock.unlock();
		}
    }
    
    public synchronized void take() {
    	lock.lock();
    	try {
	        try {
	            Thread.sleep(1000);
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	        
	        while (num == 0) {          
	            try {
	                System.out.println("箱子空了,消费者暂停");
	                //等待生产者生产一个才能继续消费,所以要让出锁
	                empty.await();                      
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	        }
	        
	        num--;
	        System.out.println("箱子中有了苹果,开始消费,消费后箱子中的苹果数量:"+num);
	        //唤醒可能因为苹果满了而等待的生产者
	        full.signal();    
    	}finally {
    		lock.unlock();
		}
    }
}

结果示例:

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

多线程(十)多线程编程示例 的相关文章

  • 跨 JVM 同步方法

    如何跨 JVM 同步方法 我的示例是一个 Web 应用程序 它限制用户名多次登录 换句话说 第一个用户可以登录 但如果另一个用户使用相同的用户名登录 他就会被拒绝 Web 应用程序部署在多个服务器上 因此存在多个 JVM 并且用户可以尝试使
  • 易失性如何与常量一起工作?

    我有这段代码 像往常一样 变量 local 的值保持不变 因为它是const const int local 10 int ptr int local printf Initial value of local d n local ptr
  • 我是否应该同步监听器通知?

    我总是很犹豫是否要把我的锁公开 公开 我总是尝试将锁限制在我的实现范围内 我相信 不这样做就会导致僵局 我有以下课程 class SomeClass protected ArrayList
  • Java 线程/易失性

    我有一个线程 class Foo extends Thread boolean active true public void run while active do stuff public void end active false p
  • 将 String 类型引用标记为 Volatile 安全吗?

    我读过一些帖子和文章说我们不应该将java对象声明为易失性的 因为这样一来 只有引用变得易失性 这里有些例子 link 1 https stackoverflow com questions 15487544 declaring an ob
  • 线程安全的集合类

    Java中提供了许多集合类 其中有的是线程安全的 有的是线程不安全的 线程安全的集合类有 1 Vector Vector类实现了一个 动态数组 与ArrayList相似 但Vector是同步访问的 2 Stack Stack是Vector的
  • 学习Java,synchronized关键字的使用

    所以我正在测试synchronized关键词 这是我尝试过的一个例子 public class MyTest static int i 0 public static void main String args new Thread t1
  • FileReader API:如何同步读取文件

    我正在尝试读取使用 html 页面上的输入类型文件选择的文件 我已经实现了读取文件的功能 并且可以读取文件内容 但实际问题是正在读取文件内容异步地它允许执行脚本的其他功能 我将读取的文件内容存储在数组中 当移动到其他函数时 数组是空的 当引
  • “快速路径”无竞争同步是什么意思?

    来自性能和可扩展性的章节JCIP书籍 http jcip net s3 website us east 1 amazonaws com 同步机制针对无竞争的情况进行了优化 case 易失性始终是无争议的 在撰写本文时 快速路径 无竞争同步范
  • 如何摆脱波动性?

    如何摆脱波动性 我应该使用哪种 C 风格转换 Use const cast 例如 volatile sample pvs new sample sample ps const cast
  • 长赋值和双赋值不是原子的 - 这有什么关系?

    我们知道 long 和 double 赋值在 Java 中不是原子的 除非它们被声明为 volatile 我的问题是它在我们的编程实践中到底有多重要 例如 如果您看到下面的类 其对象在多个线程之间共享 The below class is
  • 线程安全,无易失性

    谁能解释为什么这个例子是线程安全的 没有易失性 http www cs umd edu pugh java memoryModel DoubleCheckedLocking html http www cs umd edu pugh jav
  • 多线程中 volatile 变量的值不会改变

    我有一个在后台运行的 winform 应用程序BackgroundWorker它有一个无限循环 每小时执行一些事情 我的用户界面Form类是这样的 public partial class frmAutoScript Form privat
  • BluetoothChat同步了Activity的onResume生命周期方法,为什么?

    我现在正在研究蓝牙 Android API 并且遇到了 BluetoothChat 示例 http developer android com resources samples BluetoothChat index html http
  • Android Studio 2.0 更新 - public static volatile com.android.tools.fd.runtime.IncrementalChange

    在我使用 Android 2 0 更新后 一个新字段已添加到我的模型对象中 public static volatile com android tools fd runtime IncrementalChange com pr4 mode
  • java 数组列表上的易失性/同步

    我的程序如下所示 public class Main private static ArrayList
  • pthread_mutex_t VS @synchronized 块?

    static pthread mutex t gLock global pthread mutex init gLock NULL in init pthread mutex lock gLock for int i 0 i lt mess
  • C 易失性变量和高速缓存

    缓存是由缓存硬件对处理器透明地控制的 因此如果我们在C程序中使用易失性变量 如何保证我的程序每次都从指定的实际内存地址读取数据而不是缓存 我的理解是 Volatile 关键字告诉编译器不应优化变量引用 而应按照代码中的编程方式读取变量引用
  • 使用易失性变量和信号量 - Java

    我从线程 信号量 易失变量等开始 我想知道当我使用信号量时是否有必要将变量定义为易失性 我的意思是 有 2 个线程 一个增加变量 另一个减少变量 例如 显然 在每次访问之前 我有一个互斥体 它随时控制只有一个线程正在 玩 变量 有必要定义为
  • 通过易失性引用/指针访问声明的非易失性对象是否会为所述访问赋予易失性规则?

    这将是一篇很长的文章 为了将其置于上下文中并提供尽可能多的信息 我必须浏览各种链接和引用 这通常是我们进入 C C 标准兔子洞的唯一方法 如果您对这篇文章有更好的引用或任何其他改进 请告诉我 但先总结一下 你可以责怪 zwol对我来说发布这

随机推荐

  • 数据隐私、AI 交互和知识管理:DB-GPT 的综合解决方案

    python telegram bot python telegram bot Stars 22 9k License GPL 3 0 这个项目是一个提供纯 Python 异步接口的 Telegram Bot API 库 它与 Python
  • extern声明外部结构体

    结构体是一种类型 定义一种类型最好是在 h文件定义 这样其他地方想用这个结构体 只需包含此 h文件即可 但是定义结构体变量的话 最好载 c文件定义 为了防止重复定义 所以不建议在 h文件中定义变量 然后 h里面extern声明 其他 c文件
  • 1 恢复MySQL误删数据

    作者 一个人的孤独自白 cnblogs com mrl p 9959365 html 相信后端研发的同学在开发过程经常会遇到产品临时修改线上数据的需求 如果手法很稳那么很庆幸可以很快完成任务 很不幸某一天突然手一抖把表里的数据修改错误或者误
  • 利用OpenLayers创建wkt字符串

    var polygon OpenLayers Geometry Polygon createRegularPolygon new OpenLayers Geometry Point 6 49 2 18 0 var feature new O
  • blob (Binary Large Object)

    在深度学习中 二进制大对象 Binary Large Object BLOB 通常指的是存储模型权重或预训练模型的文件 这些文件可以非常大 通常以二进制格式存储 并在深度学习框架中用于加载和保存模型 在深度学习中 模型的权重是模型在训练过程
  • recyclerView的滑动

    1 无感知滑动 layoutManager scrollToPositionWithOffset int position int offset 第一个参数是指第几项 第二个参数是跟顶部的距离 当你的屏幕只能显示10项就满了 但是你的dat
  • 数组以及指针数组遍历&Demo

    遍历数组以及指针数组 Demo By C include
  • 【以太坊开发】 问题 etherbase must be explicitly specified

    1 使用geth启动在私有链环境下 提示如下问题 html view plain copy Updated mining threads threads 0 INFO 08 17 21 31 30 Transaction pool pric
  • flutter pubspec添加依赖无法获取flutter_test

    pubspec yaml 文件初次添加的时候拷过来没格式化 packages get 一直失败各种尝试无果 后来格式化了一下 就可以了
  • 【rust】

    系列文章目录 rust 00 开发环境搭建 rust 01 编译并运行第一个rust程序 rust 02 语法基础 变量 不可变 和常量 rust 03 语法基础 数据类型 rust 04 语法基础 函数 rust 05 语法基础 流程控制
  • windows下启动nacos(单机配置)

    windows下启动nacos 下载nacos 找到github地址 并根据对应的版本进行下载 如果下载过慢的话 可以使用迅雷下载 1下载完成后 进行解压 之后打开 conf application properties文件 2 打开后 我
  • 机器学习实战——朴素贝叶斯

    目录 一 朴素贝叶斯理论 1 概述 2 朴素贝叶斯特点 3 贝叶斯决策理论 4 条件概率与全概率公式 5 贝叶斯推断 二 朴素贝叶斯分类器应用 拉普拉斯修正 三 垃圾邮件分类 一 朴素贝叶斯理论 1 概述 朴素贝叶斯算法是有监督的学习算法
  • vue3.0模板

    GitHub Mstian Vue Onepiece Admin vue3 elementPlus后台管理简单模板https github com Mstian Vue Onepiece Admin
  • null,default关键字

    一 null关键字 1 null是空的意思 在表中 默认情况下 所有的字段值都可以为空 1 建表期间 可以对某一字段进行非空约束 not null 在insert时 此字段必须要有数据 create table temo id number
  • libuv之基础

    TCP客户端连接步骤 连接方法 Uv loop t loop uv default loop uv tcp t client malloc uv connect t connect req malloc uv tcp init loop c
  • C++ 仿函数(一)

    目录 一 仿函数是什么 二 仿函数的特点 1 仿函数在使用时 可以像普通函数那样调用 可以有参数 可以有返回值 2 仿函数超出普通函数的概念 可以有自己的状态 编辑3 仿函数可以作为参数传递 三 谓词 一元谓词示例 二元谓词示例 总结 一
  • 银行股的分红是不是比利率要高,投十万银行股一年分红有多少啊?

    工农交建中目前股息均超5 以上 10万元投资银行股 一年分红收益能达到5500左右 银行一年定期存款1 5 10万存款年利息1500 买银行股比存银行一年多收益4000左右
  • dell服务器重装win10,戴尔dell重装win10系统后无法引导的解决方法(原创)

    戴尔新机型都采用 Intel 酷睿第八代以上处理器 戴尔8代以上cpu都不支持传统模式了 默认预装了win10系统不是很好用 想重新安装win10 但是预装win10的机型默认是UEFI引导 但戴尔电脑装win10后出现不能引导情况 一般出
  • OpenFeign配合logback链路追踪

    创建MDC上下文 public class MdcContext MDC上下文 存储tId private static final ThreadLocal
  • 多线程(十)多线程编程示例

    文章目录 一 交替输出1A2B3C4D5E 1 1 synchronized wait notify 1 2 Condition await signal 二 生产者 消费者问题 2 1 synchronized wait notify 2