首页 > 编程语言 >Java 多线程中的通信机制

Java 多线程中的通信机制

时间:2024-10-31 20:16:02浏览次数:3  
标签:Java Thread threadName 通信 System 线程 println 多线程 out

本篇文章讲述了 “Java 多线程的通信机制”,阅读时长大约为:10 分钟

一、引言

“Java 多线程中的等待与通知机制” 是一种线程间通信方式,用来协调线程的执行顺序和资源共享。通过这样子的机制,线程可以避免忙等待,提高资源利用率和程序执行效率。

二、Java 多线程中的通信机制概述

2.1、Java 多线程通信的核心问题

核心问题:多线程情况下,线程之间可能会共享一些数据(比如缓冲区中的数据),此时就会出现协调和数据一致性的问题。主要的两个核心问题如下:

  1. 数据不一致:当多个线程操作一个共享变量时,一个线程在更改该数据,另一个线程在读该数据,那么读到的数据可能是不一致的。
  2. 线程协调:线程需要在特定的时机执行,保证操作顺序的时候,线程执行的时机就尤为重要。比如:“消费者-生产者” 模型中,消费者需要在非空(有资源)的情况下消费,生产者需要在非满(没有资源)的情况下生产。这个时候就需要有一个协调的机制了。

Java 中,实现这种机制的方式主要有以下几种:

  • synchronized 关键字以及 wait()、notify()、notifyAll() 方法
    • synchronized:独占资源,是一个独占锁。
    • wait()、notify()、notifyAll():实现线程的挂起和唤醒。
    • 操作简单方便,但是不太灵活(加锁粒度太大)。
  • ReentrantLock 和 Condition 条件变量
    • ReentrantLock 相比 synchronized:更加灵活(粒度更细)。
    • Condition:可以创建多个条件变量,灵活的控制唤醒哪一个线程,而不需要涉及到全部的线程。
  • ReentrantReadWriteLock 读写锁 
    • ReentrantReadWriteLock:读写锁,适用于读写分离的场景(读多写少)。
    • ReadLock 读锁:是共享锁(即读锁 和 读锁不互斥,能够一起读)。
    • WriteLock 写锁:是独占锁(写锁与任何锁都互斥),如果有线程拿到了写锁,那么其他线程都不能读和写。

2.2、Java 中对象监视器的概念

在我们学习本篇内容之前,需要对 对象监视器 有一个大致的概念。

对象监视器(Object Monitor)是 Java 实现线程间通信的重要机制。每一个 java 对象在 JVM 中都隐式的内置一个监视器。该监视器用来表示对象的锁状态,实现线程对资源的独占,还实现了线程的睡眠和唤醒的机制。

对象监视器主要有两个特点:

  1. 互斥锁(Mutex Lock):
    1. 当线程进入了 synchronized 的方法或者代码块后,监视器会加锁,其他的线程想要进入该 synchronized 代码块就需要等待锁释放。
    2. 该锁是独占锁,确保同一时间只有一个线程执行该段代码。
  2. 唤醒和等待队列:
    1. 监视器内部会有一个等待队列,当调用了 wait() 方法之后,线程会进入挂起状态,进入到等待队列中进行等待,
    2. 调用可以通过调用 notify()、notifyAll() 来唤醒等待队列中的线程。notify():随机唤醒等待队列中的某一个线程,notifyAll():唤醒等待队列中的所有线程。

三、Java 实现多线程通信的方法

3.1、synchronized 和 wait()、notify()、notifyAll()

相关方法:

  • synchronized:用于独占资源。
  • wait():用于线程的睡眠,进入等待队列。
  • notify():用于唤醒等待队列中的某一个线程。
  • notifyAll():用于唤醒等待队列中的全部线程。

代码示例:wait()、notify()

    public static void testWaitAndNotify() throws Throwable {

        Thread t0 = new Thread(() -> {
            synchronized (obj) {
                String threadName = Thread.currentThread().getName();
                System.out.println("当前线程: " + threadName + " 抢到了锁...");
                try {
                    System.out.println("当前线程: " + threadName + " 准备睡眠...");
                    obj.wait(); // 进入 WAITING 状态,同时释放锁
                    System.out.println(threadName + " 苏醒了, 并且 " + threadName + " 线程任务执行完毕...");

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
        });

        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                String threadName = Thread.currentThread().getName();
                System.out.println("当前线程: " + threadName + " 抢到了锁...");
                try {
                    Thread.sleep(1000); // 模拟任务过程
                    System.out.println(threadName + " 尝试去唤醒之前的线程...");
                    obj.notify(); // 唤醒线程
                    System.out.println(threadName + " 线程任务执行完毕, 准备退出 synchronized 代码块...");
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }

            }
        });

        t0.start();
        Thread.sleep(50); // 确保 t0 先开始执行
        t1.start();
        // 等待任务执行完毕
        t0.join();
        t1.join();

        System.out.println("所有线程执行完毕");



    }

输出:

注意,Thread-1 线程去唤醒其他线程的时候,会继续往下执行自己原先的代码,执行完后释放锁让 Thread-0 去抢锁。

notifyAll() 代码示例

    public static void testWaitAndNotifyAll() throws Throwable {

        // 模拟三个准备睡眠的任务
        for (int i = 0; i < 3; i++) {
            Thread waiter = new Thread(() -> {
                synchronized (obj) {
                    String threadName = Thread.currentThread().getName();
                    System.out.println("线程: " + threadName + " 得到了锁, 进入 WAITING 状态");
                    try {
                        Thread.sleep(100); // 模拟工作任务
                        obj.wait(); // 进入睡眠
                        System.out.println(threadName + " 苏醒, 任务执行完毕...");

                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            });

            waiter.start();
        }

        Thread.sleep(800); // 确保上述线程进入 WAITING 状态

        // 准备唤醒所有线程的任务
        Thread notifier = new Thread(() -> {
            synchronized (obj) {
                String threadName = Thread.currentThread().getName();
                System.out.println("唤醒者线程得到了锁, 准备唤醒所有线程");
                obj.notifyAll(); // 唤醒所有线程,但是不会马上释放锁,会先继续执行当前任务
                System.out.println("唤醒者线程唤醒了所有的线程, 并且释放了锁");
            }
        });

        notifier.start();

        notifier.join(); // 阻塞等待线程执行完毕

    }

输出:

3.2、ReentrantLock 和 Condition

ReentrantLock 相比于 synchronized ,它的加锁粒度更加细,配合 Condition 条件变量,可以灵活的控制唤醒、挂起线程。

代码示例(以 “生产者-消费者” 模型为例)

    public static void testConditions() throws Throwable {
        // 生产者
        Thread producer = new Thread(() -> {
            lock.lock();
            try {
                for (int i = 0; i < 10; i++) {
                    while(count >= 5) {
                        System.out.println("{producer} 已生产满, 等待consumer唤醒");
                        notFull.await(); // 进入睡眠, 等待consumer消费
                    }
                    ++ count;
                    System.out.println("{producer} 生产资源, 当前资源量为: " + count);
                    notEmpty.signal(); // 唤醒consumer,告知可以开始消费资源
                    Thread.sleep(20);
                }
            } catch (Exception e) {
                throw new RuntimeException();
            } finally {
                lock.unlock();
            }
        });

        producer.start();

        Thread.sleep(500);

        // 消费者
        Thread consumer = new Thread(() -> {
            lock.lock();
            try {
                for (int i = 0; i < 10; i++) {
                    while(count <= 0) {
                        System.out.println("{consumer} 已经消费完, 等待producer唤醒");
                        notEmpty.await(); // 进入睡眠, 等待生产出新的资源
                    }
                    -- count;
                    System.out.println("{consumer} 消费资源, 当前资源量为: " + count);
                    notFull.signal(); // 唤醒producer, 告知可以开始生产资源了
                    Thread.sleep(20);
                }
            } catch (Exception e) {
                throw new RuntimeException();
            } finally {
                lock.unlock();
            }
        });

        consumer.start();

        // 阻塞线程, 保证线程任务执行完毕
        consumer.join();
        producer.join();
        Thread.sleep(500);

        System.out.println("所有任务执行完毕");

    }

输出:

注意,我们上面创建了多个 Condition 条件变量(notFull、notEmpty)用来表示 非空、非满 状态。

流程解析:

lock.lock():用来占用锁,表示独占锁,我们只允许同一时间消费或者生产。

当我们生产者生产满了之后,调用 notFull.await() 进入睡眠状态,等待消费者消费。

当我们消费者消费完之后,调用 notEmpty.await() 进入睡眠状态,等待生产者生产。

notFull.signal():类似于 notify(),用来唤醒生产者生产资源。

notEmpty.signal():类似于 notify(),用来唤醒消费者消费资源。

3.3、ReentrantReadWriteLock 读写锁

ReentrantReadWriteLock 用来表示读写锁。

相关方法:

  • readLock():得到读锁对象
  • readLock().lock():尝试得到读锁
  • writeLock():得到写锁对象
  • writeLock().lock():尝试得到写锁

读写锁代码示例

    public static void testReadWriteLock() throws Throwable {

        // 创建两个读线程
        for (int i = 0; i < 2; i++) {
            Thread reader = new Thread(() -> {
                try {
                    String threadName = Thread.currentThread().getName();
                    readWriteLock.readLock().lock(); // 获取读锁
                    System.out.println(threadName + " 得到了读锁...");
                    Thread.sleep(100);
                    System.out.println("线程: " + threadName + " 读到了数据: " + sharedValue);
                    Thread.sleep(1000);
                    System.out.println(threadName + " 释放了读锁...");

                } catch (Exception e) {
                    throw new RuntimeException();
                } finally {
                    readWriteLock.readLock().unlock(); // 释放读锁
                }

            });

            reader.start();
        }

        Thread.sleep(1000); // 等待两个线程读

        // 写线程
        Thread writer = new Thread(() -> {
            try {
                String threadName = Thread.currentThread().getName();
                readWriteLock.writeLock().lock(); // 获取写锁
                Thread.sleep(100);
                System.out.println("线程: " + threadName + " 更改了数据: " + sharedValue + " ==> " + (sharedValue += "hahaha"));

            } catch (Exception e) {
                throw new RuntimeException();
            } finally {
                readWriteLock.writeLock().unlock(); // 释放写锁
            }
        });

        writer.start();

        System.out.println("写锁线程 start 了");

        writer.join();
        System.out.println("所有程序执行完毕");

    }

但是上面有一些我们需要注意的点:

比如说, readLock() 和 writeLock() 只是的到我们的锁对象,并不是尝试去获取到的锁。

此外,读锁是共享的,也就是说多个线程可以同时持有读锁。写锁是独占的,也就是说只要有一个线程得到了写锁,那么其他线程不管是尝试获得写锁还是读锁都是不可以的(写锁是独占锁),同理,只要有任何一个线程拥有了读锁(没有释放),那么就不能申请到写锁。写锁和读锁是互斥的。

四、总结

Synchronized:

优点:

  • 隐式锁:避免了手动加锁和释放锁。
  • 可重入锁:避免了死锁。
  • 简单易用:实现起来较为简单方便。

缺点:

  • 不够灵活:加锁粒度过大。
  • 效率过低:相较于显示锁,synchronized 效率过低。
  • 不可中断:其他处于 BLOCKED 状态中的线程必须要等待锁的释放才可以被中断。

ReentrantLock 和 Condition

优点:

  • 可重入锁:避免了死锁。
  • 粒度更细:可以通过多个 Condition 来实现对不同的线程的通信机制,更加灵活。
  • 较为丰富的方法:比如获取正在等待队列中的线程的数量。
  • 可以响应中断:可以中断正在 BLOCKED 状态中的线程。
  • 可以实现公平锁(创建对象的时候传入 true 参数):避免线程饥饿。

缺点:

  • 使用较为繁琐:需要手动释放锁,比如在 finally 中手动调用 unlock() 方法

ReentrantReadWriteLock 读写锁

优点:

  • 可重入锁:避免了死锁。
  • 读取效率高:读锁可以共享,提高读取数据的并发量。
  • 可以响应中断:处于 BLOCKED 状态中的线程可以被中断。
  • 可以实现公平锁(创建对象的时候传入true参数):避免线程饥饿。

缺点:

  • 使用较为繁琐:相较于 synchronized,需要手动设置 读锁、写锁 的加锁和释放锁。

彦祖,都看到这里了还不点个赞吗

标签:Java,Thread,threadName,通信,System,线程,println,多线程,out
From: https://blog.csdn.net/2401_82656016/article/details/143407178

相关文章

  • JAVA面向对象编程(详细 全部)
    概念面向对象编程(Object-orientedProgramming,OOP)是一种广泛应用于软件开发的编程范式。它通过将数据和对数据操作的方法封装在一个独立的实体中,即对象,来组织和管理代码。面向对象编程强调在编程过程中模拟真实世界中的实体和其相互关系。定义类我们需要搞清楚几件事情:对象......
  • (开题报告)django+vuejavaweb学生宿舍管理系统论文+源码
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、选题背景关于学生宿舍管理系统的研究,现有研究多集中于传统管理模式或单一功能模块的实现。在国内外,传统的学生宿舍管理方式主要依赖人工操作,效......
  • JavaScript:六.函数
    函数用于封装一段完成特定功能的代码,相当于将包含一条或多条语句的代码块“包裹”起来,用户在使用时只需关心参数和返回值,就能完成特定的功能。函数的优势在于提高代码的复用性,降低程序维护的难度。 6.1函数的定义与调用自定义函数的语法格式如下。function函数名([参......
  • 【Linux】进程间通信(命名管道、共享内存、消息队列、信号量)
    ......
  • Java的异常处理
    异常处理异常的简单了解什么是异常?指的是程序在执行过程中,出现的非正常情况,如果不处理最终会导致JVM的非正常停止。异常的抛出机制Java中把不同的异常用不同的类表示,一旦发生某种异常,就‘创建该异常类型的对象’,并且抛出(throw)。然后程序员如果没有捕捉(catch)这个异常对象,那么......
  • java+vue计算机毕设高校党建管理平台设计与现实-以西藏民族大学为例【开题+程序+论文+
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着信息技术的飞速发展,高校党建工作面临着新的机遇与挑战。特别是在西藏民族大学这样的特殊地域环境中,如何有效管理和推进党建工作,成为了一个亟待解......
  • Java基础篇 (一)(JDK安装教程 零基础谁都可以学会!!!)
    前言大家好,我是小斜。俗话说的好,十年树木,百年树人。欲成大事,必须要坚持不懈努力。学习编程也一样,只有通过日积月累地学习才能有质的飞跃!我在这里给刚入门的大伙们提几点小建议:1.知行合一,编程归根结底就是要我们把思考出来的东西,再让它通过代码实现的一个过程。如果仅仅停留......
  • Java基础篇(三)(超详细整理,建议收藏!!!)
    目录一、Java的诞生与发展历史    1.1Java的出生地:SUNMicrosystemsInc.1.2 Java技术体系   1.3 Java语言的特点1.4 Java程序的运行机制1.4.1 JVM与跨平台1.4.2 JVM、JRE、JDK1.5 java开发环境1.6 java开发流程1.6.1 结构化编程与面......
  • PHP和Java在后端开发上有哪些不同_1
    PHP和Java是两种广泛使用的后端开发语言,它们在多个方面具有显著的区别。PHP和Java在以下关键方面的不同:1.语言特性和开发环境;2.性能和速度;3.社区支持和资源;4.适用场景和项目类型;5.学习曲线和易用性。PHP作为一种动态脚本语言,被广泛用于快速开发和简单的网站项目,而Java作为一种强......
  • Python之pyserial模块 串口通信
    python之pyserial模块原文链接:https://www.cnblogs.com/sureZ-learning/p/17054481.htmlpyserial模块封装了对串口的访问,兼容各种平台(Windows、Linux、MACOS等)。其支持的特性如下:所有平台基于类的接口相同端口可以通过python来设置支持不同数据长度、停止位、奇偶校验位、流......