Java 多线程的线程间的协作

news2024/11/26 10:23:25

1. 等待与通知

为了支持多线程之间的协作,JDK 中提供了两个非常重要的方法:wait() 和 notify() ,这两个方法定义在 Object 类中,这意味着任何 Java 对象都可以调用者两个方法。如果一个线程调用了 object.wait() 方法,那么它就会进入该对象的等待队列中,这个队列中可能包含了多个线程,此时代表多个线程都在等待同一个对象;当 object.notify() 方法被调用时,它就会从这个等待队列中随机唤醒一个线程。

需要特别注意的是在调用这两个方法时,它们都必须位于对应对象的 synchronzied 语句中,因为这两个方法在调用前都需要获得对应对象的监视器(内部锁),过程如下:

使用示例如下:

public class J3_WaitAndNotify {

    private static final Object object = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (object) {
                try {
                    System.out.println("对象object等待");
                    object.wait();
                    System.out.println("线程1后续操作");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(() -> {
            synchronized (object) {
                System.out.println("线程2开始操作");
                System.out.println("对象object唤醒");
                object.notify();
            }
        }).start();
    }
}

// 输出
对象object等待
线程2开始操作
对象object唤醒
线程1后续操作

notify() 表示随机唤醒任意一个等待线程,如果想要唤醒所有等待线程,则可以使用 notifyAll() 方法:

public class J5_NotifyAll {

    private static final Object object = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (object) {
                try {
                    System.out.println("对象object在线程1等待");
                    object.wait();
                    System.out.println("线程1后续操作");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(() -> {
            synchronized (object) {
                try {
                    System.out.println("对象object在线程2等待");
                    object.wait();
                    System.out.println("线程2后续操作");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(() -> {
            synchronized (object) {
                System.out.println("线程3开始操作");
                System.out.println("对象object唤醒");
                object.notifyAll();
            }
        }).start();
    }
}

// 输出
对象object在线程1等待
对象object在线程2等待
线程3开始操作
对象object唤醒
线程2后续操作
线程1后续操作

在上面的示例中,由于有两个线程处于等待状态,所以 notifyAll() 的效果等价于调用 notify() 两次:

object.notify();
object.notify();

2. 条件变量

综上所述可以使用 wait() 和 notify() 配合内部锁 synchronized 可以实现线程间的等待与唤醒,如果你使用的是显示锁而不是内部锁,此时可以使用 Condition 来实现同样的效果。Condition 接口中定义了如下方法:

await():使得当前线程进入等待状态,类似于 object.wait()
awaitUninterruptibly():与 await() 类似,但它不会在等待过程中响应中断;
awaitNanos(long nanosTimeout) & await(long time, TimeUnit unit) & awaitUntil(Date deadline):有时间限制的等待;
signal():用于随机唤醒一个等待;
signalAll():用于唤醒所有等待。

和 object 的 wait()\notify()\notifyAll() 一样,在使用 condition 的 await()\signal()\signalAll() 前,也要求线程必须持有相关的重入锁, 示例如下:

public class AwaitAndSignal {

    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    static class IncreaseTask implements Runnable {
        @Override
        public void run() {
            try {
                lock.lock();
                String threadName = Thread.currentThread().getName();
                System.out.println(threadName + "线程等待通知...");
                condition.await();
                System.out.println(threadName + "线程后续操作");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new IncreaseTask());
        thread1.start();
        Thread.sleep(1000);
        System.out.println("主线程开始操作");
        lock.lock();
        System.out.println("主线程唤醒");
        condition.signal();
        lock.unlock();
    }
}

// 输出:
Thread-0线程等待通知...
主线程开始操作
主线程唤醒
Thread-0线程后续操作

3. Join

Thread.join() 可以让当前线程等待目标线程结束后再开始运行,示例如下:

public class J1_Normal {
    private static int j = 0;
    public static void main(String[] args)  {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                j++;
            }
        });
        thread.start();
        System.out.println(j);
    }
}
// 此时主线程不等待子线程运行完成,通常输出结果为:0

public class J2_Join {
    private static int j = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                j++;
            }
        });
        thread.start();
        thread.join();
        System.out.println(j);
    }
}
// 此时主线程需要等待子线程运行完成,输出结果为:100000

4. CountDownLatch

Thread.join() 可以让当前线程等待目标线程结束后再开始运行,但大多数时候,你只需要等待目标线程完成特定的操作,而不必等待其完全终止。此时可以使用条件变量 Condition 来实现,也可以使用更为简单的工具类 CountDownLatch 。CountDownLatch 会在内部维护一个计数器,每次完成一个任务,则计数器减 1,当计数器为 0 时,则唤醒所有的等待线程,示例如下:

public class j1_Normal {
    private static AtomicInteger integer = new AtomicInteger(0);
    static class IncreaseTask implements Runnable {
        @Override
        public void run() {
            try {
                // 假设这是一个耗时的任务
                Thread.sleep(3000);
                integer.incrementAndGet();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        IncreaseTask task = new IncreaseTask();
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for (int i = 0; i < 100; i++) {
            executorService.submit(task);
        }
        System.out.println("integer:" + integer);
        executorService.shutdown();
    }
}

// 不使用CountDownLatch 时,主线程不会子线程等待计算完成,此时输出通常为: 0


public class J2_CountDown {
    private static int number = 100;
    // 指定计数器的初始值
    private static CountDownLatch latch = new CountDownLatch(number);
    private static AtomicInteger integer = new AtomicInteger(0);
    static class IncreaseTask implements Runnable {
        @Override
        public void run() {
            try {
                // 假设这是一个耗时的任务
                Thread.sleep(3000);
                integer.incrementAndGet();
                // 计数器减1
                latch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        IncreaseTask task = new IncreaseTask();
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for (int i = 0; i < number; i++) {
            executorService.submit(task);
        }
        // 等待计数器为0时唤醒所有等待的线程
        latch.await();
        System.out.println("integer:" + integer);
        executorService.shutdown();
    }
}

// 使用CountDownLatch 时,主线程需要等待所有的子线程计算完成后再输出,计算结果为:100

5. CyclicBarrier

CyclicBarrier 和 CountDownLatch 类似,都是用于等待一个或者多个线程完成特定的任务后再执行某项操作,但不同的是它可以循环使用,示例如下:

/** 
 * 每五个人完成任务后,则算一个小组已完成
 */
public class J1_CyclicBarrier {

    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> System.out.println("五人小组任务执行完成"));

    static class Task implements Runnable {
        @Override
        public void run() {
            try {
                long l = new Double(Math.random() * 5000).longValue();
                Thread.sleep(l);
                System.out.println("任务" + Thread.currentThread().getId() + "执行完成");
                cyclicBarrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        for (int j = 0; j < 10; j++) {
            executorService.submit(new Task());
        }
        executorService.shutdown();
    }
}

// 输出如下:
任务21执行完成
任务20执行完成
任务15执行完成
任务14执行完成
任务22执行完成
五人小组任务执行完成
任务17执行完成
任务13执行完成
任务19执行完成
任务18执行完成
任务16执行完成
五人小组任务执行完成

基于 CyclicBarrier 的特性,通常可以用于在测试环境来模仿高并发,如每次等待一万个线程启动后再让其并发执行某项压力测试。

6. Semaphore

信号量(Semaphore)可以看做是锁的扩展,由于锁的排它性,所以一次只允许一个线程来访问某个特定的资源, 而 Semaphore 则允许多个线程并发的访问某个特定的资源,并且可以通过配置许可证的数量来限制并发访问的线程数,因此其可以用于流量控制等场景中:

public class J1_Semaphore {

    // 限制并发访问的线程的数量为5
    private static Semaphore semaphore = new Semaphore(5);

    static class IncreaseTask implements Runnable {
        @Override
        public void run() {
            try {
                semaphore.acquire();
                System.out.println(Thread.currentThread().getId() + "获得锁!");
                Thread.sleep(5000);
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        IncreaseTask task = new IncreaseTask();
        for (int i = 0; i < 20; i++) {
            new Thread(task).start();
        }
    }
}

// 输出如下,至多只能有五个线程并发获得锁
13获得锁!
15获得锁!
16获得锁!
18获得锁!
17获得锁!
....
19获得锁!
20获得锁!
21获得锁!
22获得锁!
23获得锁!
....

7. LockSupport

LockSupport 可以在线程内的任意位置实现阻塞。它采用和 Semaphore 类似的信号量机制:它为每个线程准备一个许可,如果许可可用,则 park() 方法会立即返回,并且消费掉这个许可,让许可不可用;此时因为许可不可用,相应的线程就会被阻塞。而 unpark() 则会使得一个许可从不可用变为可用。但和 Semaphore 不同的是:它的许可不能累加,你不可能拥有超过一个许可,它永远只有一个:

public class J1_LockSupport {

    static class Task implements Runnable {
        @Override
        public void run() {
            long id = Thread.currentThread().getId();
            System.out.println("线程" + id + "开始阻塞");
            LockSupport.park();
            System.out.println("线程" + id + "解除阻塞");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread01 = new Thread(new Task());
        Thread thread02 = new Thread(new Task());
        thread01.start();
        thread02.start();
        Thread.sleep(3000);
        System.out.println("主线程干预");
        LockSupport.unpark(thread01);
        LockSupport.unpark(thread02);
    }
}

// 输出:
线程13开始阻塞
线程14开始阻塞
主线程干预
线程13解除阻塞
线程14解除阻塞

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1173360.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

多线程JUC 第2季 多线程的内存模型

一 内存模型 1.1 概述 在hotspot虚拟机里&#xff0c;对象在堆内存中的存储布局可以划分为3个部分&#xff1a;对象头&#xff1b;实例数据&#xff0c;对齐填充。如下所示&#xff1a;

MES与ERP系统集成的一些探讨

什么是MES软件&#xff1f; 制造执行系统 &#xff08;MES&#xff09; 是一种用于控制车间复杂制造操作和数据的软件。MES软件有助于提高生产过程的质量&#xff0c;使制造商能够轻松响应需求和客户偏好的变化。 MES软件有什么作用&#xff1f; 制造执行系统允许企业跟踪、…

2、k8s 集群安装

1、kubeadm kubeadm 是官方社区推出的一个用于快速部署 kubernetes 集群的工具。 这个工具能通过两条指令完成一个 kubernetes 集群的部署&#xff1a; # 创建一个 Master 节点 $ kubeadm init # 将一个 Node 节点加入到当前集群中 $ kubeadm join <Master 节点的 IP 和端口…

操作系统——初始文件管理(王道视频p58)

1.总体概述&#xff1a; 这一节&#xff0c;主要是 作为 后续 “文件系统”的引子 我认为可以思考的点&#xff1a; &#xff08;1&#xff09;文件之间的逻辑结构——windows中采用根什么的“树状结构”&#xff0c;而文件在外存中的实际物理结构又是什么样的 &#xff08…

【并行计算】多核处理器

这张图连接了几个并行计算的思想。 从上往下。 1.两个fetch/decode部件&#xff0c;是superscalar技术&#xff0c;每个cycle可以发射多个指令。 2.多个执行单元&#xff0c;支持乱序执行&#xff0c;是ILP&#xff0c;指令级并行。 3.每个执行单元里还支持SIMD操作。 4.有…

求职应聘校招社招,面对在线测评有什么技巧?

网上测评&#xff0c;不要怕&#xff0c;关键是在于你要提前准备充分。要说技巧&#xff0c;真心没有&#xff0c;但是建议我有一点点。 1、网上测评&#xff0c;技巧就是老实做 老老实实做题&#xff0c;我一贯的作风&#xff0c;老实人不吃亏。越是心思灵巧的人&#xff0c…

MyBatis-Plus复习总结(一)

文章目录 一、环境搭键二、基本CRUD2.1 BaseMapper2.2 插入2.3 删除2.4 修改2.5 查询 三、通用Service四、常用注解4.1 雪花算法4.2 注解TableLogic 五、条件构造器和常用接口5.1 Wrapper介绍5.2 QueryWrapper5.3 UpdateWrapper5.4 condition5.5 LambdaQueryWrapper5.6 LambdaU…

二、计算机组成原理与体系结构

&#xff08;一&#xff09;数据的表示 不同进制之间的转换 R 进制转十进制使用按权展开法&#xff0c;其具体操作方式为&#xff1a;将 R 进制数的每一位数值用 Rk 形式表示&#xff0c;即幂的底数是 R &#xff0c;指数为 k &#xff0c;k 与该位和小数点之间的距离有关。当…

论文阅读—— UniDetector(cvpr2023)

arxiv&#xff1a;https://arxiv.org/abs/2303.11749 github&#xff1a;https://github.com/zhenyuw16/UniDetector 一、介绍 通用目标检测旨在检测场景那种的一切目标。现有的检测器依赖于大量数据集 通用的目标检测器应该有两个能力&#xff1a;1、可以利用多种来…

鹏城杯_2018_note

查看保护&#xff0c;就开了 PIE&#xff1a; 漏洞点&#xff1a; buf 存在溢出&#xff0c;刚好可以溢出到 idx&#xff0c;而且没有开 PIE 和 FULL RELRO&#xff0c;所以可以修改 idx 去修改相关 got 表项。 然后我就没啥思路了&#xff0c;因为在我的本地环境堆上是没有可…

Flink SQL时间属性和窗口介绍

&#xff08;1&#xff09;概述 时间属性&#xff08;time attributes&#xff09;&#xff0c;其实就是每个表模式结构&#xff08;schema&#xff09;的一部分。它可以在创建表的 DDL 里直接定义为一个字段&#xff0c;也可以在 DataStream 转换成表时定义。 一旦定义了时间…

一文通透各种注意力:从多头注意力MHA到分组查询注意力GQA、多查询注意力MQA

第一部分 多头注意力 // 待更 第二部分 LLaMA2之分组查询注意力——Grouped-Query Attention 自回归解码的标准做法是缓存序列中先前标记的键 (K) 和值 (V) 对&#xff0c;从而加快注意力计算速度 然而&#xff0c;随着上下文窗口或批量大小的增加&#xff0c;多头注意力 (MH…

Buuctf-Crypto-之深夜刷题部分wp

萌萌哒的八戒 首先下载好附件&#xff0c;解压&#xff0c;是一幅猪图&#xff0c;图的下方是一串看不懂的字&#xff0c;百度输入关键词猪、密码&#xff0c;可知这是猪圈密码&#xff0c; 手撸得WHENTHEPIGWANTTOEAT 大写不对&#xff0c;换成小写。 …

数据结构——常见简答题汇总

目录 1、绪论 2、线性表 3、栈、队列和数组 4、串 5、树与二叉树 6、图 7、查找 8、排序 1、绪论 什么是数据结构&#xff1f; 数据结构是相互之间存在一种或多种特定关系的数据元素的集合。数据结构包括三个方面&#xff1a;逻辑结构、存储结构、数据的运算。 逻辑结…

中小学智慧校园电子班牌管理系统源码

智慧校园云平台电子班牌系统&#xff0c;利用先进的云计算技术&#xff0c;将教育信息化资源和教学管理系统进行有效整合&#xff0c;实现基础数据共享、应用统一管理。借助全新的智能交互识别终端和移动化教育管理系统&#xff0c;以考勤、课表、通知、家校互通等功能为切入点…

如何将 XxlJob 集成达梦数据库

1. 前言 在某些情况下&#xff0c;你的项目可能会面临数据库选择的特殊要求&#xff0c;随着国产化的不断推进&#xff0c;达梦数据库是一个常见的选择。本篇博客将教你如何解决 XxlJob 与达梦数据库之间的 SQL 兼容性问题&#xff0c;以便你的任务调度系统能够在这个数据库中…

NNDL 作业6 卷积

一、概念 &#xff08;一&#xff09;卷积 &#xff08;1&#xff09;什么叫卷积 卷积、旋积或褶积(英语&#xff1a;Convolution)是通过两个函数f和g生成第三个函数的一种数学运算&#xff0c;其本质是一种特殊的积分变换&#xff0c;描述一个函数和另一个函数在某个维度上…

类和对象解析

导言&#xff1a; Java是一门纯面向对象的语言&#xff0c;在面对对象的世界里&#xff0c;一切皆为对象。而对象的创建又和类的定义息息相关。本文主要阐述了类和对象的使用与理解。解释类的定义方式以及对象的实例化&#xff0c;类中的成员变量和成员方法的使用&#xff0c;…

【qemu逃逸】D3CTF2021-d3dev

前言 题目给的是一个 docker 环境&#xff0c;所以起环境非常方便&#xff0c;但是该怎么调试呢&#xff1f;有无佬教教怎么在 docker 中调试&#xff1f; 我本来想着直接起一个环境进行调试&#xff0c;但是缺了好的库&#xff0c;所以就算了&#xff0c;毕竟本题也不用咋调…

044_第三代软件开发-保存PDF

第三代软件开发-保存PDF 文章目录 第三代软件开发-保存PDF项目介绍保存PDF头文件源文件使用 关键字&#xff1a; Qt、 Qml、 pdf、 painter、 打印 项目介绍 欢迎来到我们的 QML & C 项目&#xff01;这个项目结合了 QML&#xff08;Qt Meta-Object Language&#xff…