多线程间的同步控制和通信

news2024/11/16 23:53:05

用多线程并发处理,目的是为了让程序更充分地利用CPU ,好能加快程序的处理速度和用户体验。如果每个线程各自处理的部分互不相干,那真是极好的,我们在程序主线程要做的同步控制最多也就是等待几个工作线程的执行完毕,如果不 Care 结果的话,连同步等待都能省去,主线程撒开手让这些线程干就行了。

不过,现实还是很残酷的,大部分情况下,多个线程是会有竞争操作同一个对象的情况的,这个时候就会导致并发常见的一个问题--数据竞争(Data Racing)。

这篇文章我们就来讨论一下这个并发导致的问题,以及多线程间进行同步控制和通信的知识,本文大纲如下:

 

并发导致的Data Racing问题

怎么理解这个问题呢,拿一个多个线程同时对累加器对象进行累加的例子来解释吧。

package com.learnthread;

public class DataRacingTest {
    
    public static void main(String[] args) throws InterruptedException {
        final DataRacingTest test = new DataRacingTest();
        // 创建两个线程,执行 add100000() 操作
        // 创建Thread 实例时的 Runnable 接口实现,这里直接使用了 Lambda
        Thread th1 = new Thread(()-> test.add100000());
        Thread th2 = new Thread(()-> test.add100000());
        // 启动两个线程
        th1.start();
        th2.start();
        // 等待两个线程执行结束
        th1.join();
        th2.join();
        System.out.println(test.count);
    }

    private long count = 0;

    // 想复现 Data Racing,去掉这里的 synchronized
    private  void add100000() {
        int idx = 0;
        while(idx++ < 100000) {
            count += 1;
        }
    }
}

上面这个例程,如果我们不启动 th2 线程,只用 th1 一个线程进行累加操作的话结果是 100000。按照这个思维,如果我们启动两个线程那么最后累加的结果就应该是 200000。 但实际上并不是,我们运行一下上面的例程,得到的结果是:

168404

Process finished with exit code 0

当然这个在每个人的机器上的结果是不一样的,而且也是有可能恰好等于 200000,需要多运行几次,或者是多开几个线程执行累加,出现 Data Racing 的几率才高。

程序出现 Data Racing 的现象,就意味着最终拿到的数据是不正确的。那么为了避免这个问题就需要通过加锁来解决了,让同一时间只有持有锁的线程才能对数据对象进行操作。当然针对简单的运算、赋值等操作我们也能直接使用原子操作实现无锁解决 Data Racing, 我们为了示例足够简单易懂才举了一个累加的例子,实际上如果是一段业务逻辑操作的话,就只能使用加锁来保证不会出现 Data Racing了。

加锁,只是线程并发同步控制的一种,还有释放锁、唤醒线程、同步等待线程执行完毕等操作,下面我们会逐一进行学习。

同步控制--synchronized

开头的那个例程,如果想避免 Data Racing,那么就需要加上同步锁,让同一个时间只能有一个线程操作数据对象。 针对我们的例程,我们只需要在 add100000 方法的声明中加上 synchronized 即可。

    // 想复现 Data Racing,去掉这里的 synchronized
    private synchronized void add100000() {
        int idx = 0;
        while(idx++ < 100000) {
            count += 1;
        }
    }

是不是很简单,当然 synchronized 的用法远不止这个,它可以加在实例方法、静态方法、代码块上,如果使用的不对,就不能正确地给需要同步锁保护的对象加上锁。

synchronized 是 Java 中的关键字,是利用锁的机制来实现互斥同步的。 synchronized 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块。 如果不需要 Lock 、读写锁ReadWriteLock 所提供的高级同步特性,应该优先考虑使用synchronized 这种方式加锁,主要原因如下:

  • Java 自 1.6 版本以后,对 synchronized 做了大量的优化,其性能已经与 JUC 包中的 LockReadWriteLock 基本上持平。从趋势来看,Java 未来仍将继续优化 synchronized ,而不是 ReentrantLock 。
  • ReentrantLock 是 Oracle JDK 的 API,在其他版本的 JDK 中不一定支持;而 synchronized 是 JVM 的内置特性,所有 JDK 版本都提供支持。

synchronized 可以应用在实例方法、静态方法和代码块上:

  • synchronized 关键字修饰实例方法,即为同步实例方法,锁是当前的实例对象。
  • synchronized 关键字修饰类的静态方法,即为同步静态方法,锁是当前的类的 Class 对象。
  • 如果把 synchronized 应用在代码块上,锁是 synchronized 括号里配置的对象,synchronized(this) {..} 锁就是代码块所在实例的对象,synchronized(类名.class) {...} ,锁就是类的 Class 对象。

同步实例方法和代码块

上面我们已经看过怎么给实例方法加 synchronized 让它变成同步方法了。下面我们看一下,synchronized 给实例方法加锁时,不能保证资源被同步锁保护的例子。

class Account {
  private int balance;
  // 转账
  synchronized void transfer(Account target, int amt){
    if (this.balance > amt) {
      this.balance -= amt;
      target.balance += amt;
    }
  }
}

在这段代码中,临界区内有两个资源,分别是转出账户的余额 this.balance 和转入账户的余额 target.balance,并且用的是一把实例对象的锁。问题就出在 this 这把锁上,this 这把锁可以保护自己的余额 this.balance,却保护不了别人的余额 target.balance,就像你不能用自家的锁来保护别人家的资产一个道理。

应该保证使用的锁能保护所有应受保护资源。我们可以使用Account.class 作为加锁的对象Account.class 是所有 Account 类的对象共享的,而且是 Java 虚拟机在加载 Account 类的时候创建的,保证了它的全局唯一性。

class Account {
  private int balance;
  // 转账
  void transfer(Account target, int amt){
    synchronized(Account.class) {
      if (this.balance > amt) {
        this.balance -= amt;
        target.balance += amt;
      }
    }
  }
}

用 synchronized 给 Account.class 加锁,这样就保证出账、入账两个 Account 对象在同步代码块里都能收到保护。

当然我们也可以使用这笔转账的交易对象作为加锁的对象,保证只有这比交易的两个 Account 对象受保护,这样就不会影响到其他转账交易里的出账、入账 Account 对象了。

class Account {
  private Trans trans;
  private int balance;
  private Account();
  // 创建 Account 时传入同一个 交易对象作为 lock 对象
  public Account(Trans trans) {
    this.trans = trans;
  }
  // 转账
  void transfer(Account target, int amt){
    // 此处检查所有对象共享的锁
    synchronized(trans) {
      if (this.balance > amt) {
        this.balance -= amt;
        target.balance += amt;
      }
    }
  }
}

通过解决上面这个问题我们顺道就把 synchronized 修饰同步代码块的知识点学了, 现在我们来看 synchronized 的最后一个用法--修饰同步静态方法。

同步静态方法

静态方法的同步是指,用 synchronized 修饰的静态方法,与使用所在类的 Class 对象实现的同步代码块,效果类似。因为在 JVM 中一个类只能对应一个类的 Class 对象,所以同时只允许一个线程执行同一个类中的静态同步方法。

对于同一个类中的多个静态同步方法,持有锁的线程可以执行每个类中的静态同步方法而无需等待。不管类中的哪个静态同步方法被调用,一个类只能由一个线程同时执行。

package com.learnthread;

public class SynchronizedStatic implements Runnable {

    private static final int MAX = 100000;

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        SynchronizedStatic instance = new SynchronizedStatic();
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        // 等待工作线程执行结束
        t1.join();
        t2.join();
        System.out.println(count);
    }

    @Override
    public void run() {
        for (int i = 0; i < MAX; i++) {
            increase();
        }
    }

    /**
     * synchronized 修饰静态方法
     */
    public synchronized static void increase() {
        count++;
    }

}

线程挂起和唤醒

上面我们看了使用 synchronized 给对象加同步锁,让同一时间只有一个线程能操作临界区的控制。接下来,我们看一下线程的挂起和唤醒,这两个操作使用被线程成功加锁的对象的 waitnotify 方法来完成,唤醒除了notify 外还有 notifyAll方法用来唤醒所有线程。下面我们先看一下这几个方法的解释。

  • wait - wait 会自动释放当前线程占有的对象锁,并请求操作系统挂起当前线程,让线程从 Running 状态转入 Waiting 状态,等待被 notify / notifyAll 来唤醒。如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步代码块中,那么就无法执行 notify 或者 notifyAll 来唤醒挂起的线程,会造成死锁。
  • notify - 唤醒一个正在 Waiting 状态的线程,并让它拿到对象锁,具体唤醒哪一个线程由 JVM 控制 。
  • notifyAll - 唤醒所有正在 Waiting 状态的线程,接下来它们需要竞争对象锁。

这里有两点需要各位注意的地方, 第一个是 waitnotifynotifyAll 都是 Object 类中的方法,而不是 Thread 类的。 因为 Object 是始祖类,是不是意味着所有类的对象都能调用这几个方法呢?是,也不是... 因为 wait、notify、notifyAll 只能用在 synchronized 方法或者 synchronized 代码块中使用,否则会在运行时抛出 IllegalMonitorStateException。换句话说,只有被 synchronized 加上锁的对象,才能调用这三个方法。

 

为什么 waitnotifynotifyAll 不定义在 Thread 类中?为什么 waitnotifynotifyAll 要配合 synchronized 使用? 理解为什么这么设计,需要了解几个基本知识点:

  • 每一个 Java 对象都有一个与之对应的监视器(monitor)
  • 每一个监视器里面都有一个 对象锁 、一个 等待队列、一个 同步队列

了解了以上概念,我们回过头来理解前面两个问题。

为什么这几个方法不定义在 Thread 中?

  • 由于每个对象都拥有对象锁,让当前线程等待某个对象锁,自然应该基于这个对象(Object)来操作,而非使用当前线程(Thread)来操作。因为当前线程可能会等待多个线程释放锁,如果基于线程(Thread)来操作,就非常复杂了。

为什么 wait、notify、notifyAll 要配合 synchronized 使用?

  • 如果调用某个对象的 wait 方法,当前线程必须拥有这个对象的对象锁,因此调用 wait 方法必须在 synchronized 方法和 synchronized 代码块中。

下面看一个 wait、notify、notifyAll 的一个经典使用案例,实现一个生产者、消费者模式:

package com.learnthread;

import java.util.PriorityQueue;

public class ThreadWaitNotifyDemo {

    private static final int QUEUE_SIZE = 10;
    private static final PriorityQueue<Integer> queue = new PriorityQueue<>(QUEUE_SIZE);

    public static void main(String[] args) {
        new Producer("生产者A").start();
        new Producer("生产者B").start();
        new Consumer("消费者A").start();
        new Consumer("消费者B").start();
    }

    static class Consumer extends Thread {

        Consumer(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.size() == 0) {
                        try {
                            System.out.println("队列空,等待数据");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notifyAll();
                        }
                    }
                    queue.poll(); // 每次移走队首元素
                    queue.notifyAll();
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 从队列取走一个元素,队列当前有:" + queue.size() + "个元素");
                }
            }
        }
    }

    static class Producer extends Thread {

        Producer(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.size() == QUEUE_SIZE) {
                        try {
                            System.out.println("队列满,等待有空余空间");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notifyAll();
                        }
                    }
                    queue.offer(1); // 每次插入一个元素
                    queue.notifyAll();
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 向队列取中插入一个元素,队列当前有:" + queue.size() + "个元素");
                }
            }
        }
    }
}

上面的例程有两个生产者和两个消费者。生产者向队列中放数据,每次向队列中放入数据后使用 notifyAll 唤醒消费者线程,当队列满后生产者会 wait 让出线程,等待消费者取走数据后再被唤醒 (消费者取数据后也会调用 notifyAll )。同理消费者在队列空后也会使用 wait 让出线程,等待生产者向队列中放入数据后被唤醒。

线程等待--join

waitnotify 方法一样,join 是另一种线程间同步机制。当我们调用线程对象 join 方法时,调用线程会进入等待状态,它会一直处于等待状态,直到被引用的线程执行结束。在上面的几个例子中,我们已经使用过了 join 方法

   ...
	public static void main(String[] args) throws InterruptedException {
        SynchronizedStatic instance = new SynchronizedStatic();
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        // 等待工作线程执行结束
        t1.join();
        t2.join();
        System.out.println(count);
    }

这个例子里,主线程调用 t1 和 t2 的 join 方法后,就会一直等待,直到他们两个执行结束。如果 t1 或者 t2 线程处理时间过长,调用它们 join 方法的主线程将一直等待,程序阻塞住。为了避免这些情况,可以使用能指定超时时间的重载版本的 join 方法。

    t2.join(1000); // 最长等待1s

如果引用的线程被中断,join方法也会返回。在这种情况下,还会触发 InterruptedException。所以上面的main方法为了演示方便,直接选择抛出了 InterruptedException

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

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

相关文章

weston input 概述

weston input 概述 零、前言 本文描述了有关于 weston (基于 wayland 协议一种显示服务器的实现) 中有关于输入设备管理的部分;为了聚焦于此,本文不会对 weston 整体或 wayland 协议进行过多的阐述. 考虑到读者可能存在不同的需求,采用分层次的描述方式,主要面向以下两类人群…

Android Studio 导入opencv异常报错紧急救援

Download OpenCV from SourceForge.net 1、下载Android demo之后导入Android Studio 如下图所示 报错信息如下 A problem occurred configuring root project opencv_samples. > Could not resolve all artifacts for configuration :classpath.> Could not find org.j…

校园失物招领毕业设计,学生失物招领系统设计与实现,毕业设计怎么写论文源码开题报告需求分析怎么做

项目背景和意义 目的&#xff1a;本课题主要目标是设计并能够实现一个基于web网页的失物招领网站系统&#xff0c;整个网站项目使用了B/S架构&#xff0c;基于java的springboot框架下开发&#xff1b;管理员通过后台录入信息、管理信息&#xff0c;设置网站信息&#xff0c;管理…

8_4、Java基本语法之线程的通信

一、问题的引入 使用两个线程打印 1-100。线程1, 线程2交替打印&#xff1f; 二、解决问题涉及的方法 涉及到的三个方法&#xff1a; 1.wait():一旦执行此方法&#xff0c;那么调用此方法的线程就会进入阻塞状态&#xff0c;并释放同步监视器。 2.notify():一个线程…

如何使用htmlq提取html文件内容

htmlq能够对 HTML 数据进行 sed 或 grep 操作。我们可以使用 htmlq 搜索、切片和过滤 HTML 数据。让我们看看如何在 Linux 或 Unix 上安装和使用这个方便的工具并处理 HTML 数据。 什么是htmlq&#xff1f; htmlq类似于 jq&#xff0c;但用于 HTML。使用 CSS 选择器从 HTML 文…

[安装] HIVE搭建环境

一、生产环境hive集群架构 参考&#xff1a; hive2.3.7安装记录 hive基础入门与环境的搭建 基础篇七 Hive-2.3.9安装与配置 大数据之Hive 集群搭建 完整使用 数仓&#xff08;十&#xff09;hive的Metastore机制 二、前言快读 Hive安装分类 主要是metastore的服务搭建方…

[rsync] 基于rsync的同步

环境 Linux&#xff1a;CentOs7.5 rsync: 3.1.2 rsync安装 一般安装系统时会自带rsync&#xff0c;可通过如下命令查看已经安装的版本信息 rsync --version如果系统未安装&#xff0c;可通过如下方式安装 yum安装【建议】 使用root用户执行yum安装 yum install -y rsync安…

代码随想录刷题记录day37 0-1背包+分割等和子集

代码随想录刷题记录day37 0-1背包分割等和子集 0-1背包 问题&#xff1a;有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i]&#xff0c;得到的价值是value[i] 。每件物品只能用一次&#xff0c;求解将哪些物品装入背包里物品价值总和最大。 例题&#xf…

操作系统实验五 进程间通信-管道通信

实验目的 1.掌握利用管道机制实现进程间的通信的方法 2.了解利用消息缓冲队列机制实现进程间的通信的方法 3..了解利用共享存储区机制实现进程间的通信的方法 五个题目如下 1. 函数int pipe(int fd[2])创建一个管道&#xff0c;管道两端可分别用描述字fd[0]以及fd[1]来描述。需…

多元宇宙算法求解电力系统多目标优化问题(Matlab实现)【电气期刊论文复现与创新】

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f389;作者研究&#xff1a;&#x1f3c5;&#x1f3c5;&#x1f3c5;本科计算机专业&#xff0c;研究生电气学硕…

SpringCloud项目使用Nacos进行服务的注册

本篇介绍Spring cloud项目使用Nacos作为注册中心来进行服务注册及服务发现&#xff0c;并进行简单的测试来验证。 一、简介 nacos是一个集服务发现、服务配置、服务元数据以及流量管理于一体的管理中心&#xff0c;能帮助我们更好的发现、配置和管理微服务。 注意&#xff1…

家政公司网站毕业设计,家政服务系统设计与实现,毕业设计论文源码开题报告需求分析

项目背景和意义 目的&#xff1a;本课题主要目标是设计并能够实现一个基于web网页的家政服务预约系统&#xff0c;整个网站项目使用了B/S架构&#xff0c;基于java的springboot框架下开发&#xff1b;管理员通过后台录入信息、管理信息&#xff0c;设置网站信息&#xff0c;管理…

JS项目打包之ROLLUP.JS入门

一、目的 Rollup是一个用于JavaScript的模块打包器&#xff0c;它将小块代码编译成更大、更复杂的东西&#xff0c;例如库或应用程序。它为JavaScript ES6版本中包含的代码模块使用了新的标准化格式&#xff0c;而不是以前的特殊解决方案&#xff0c;如CommonJS和AMD。ES模块可…

Win10安装Nacos

Win10安装Nacos 文章目录Win10安装Nacos前言下载Nacos安装Nacos验证前言 最近在学微服务的东西&#xff0c;使用的是 Spring Cloud Alibaba 生态&#xff0c;Nacos就是其中关键的一环。 这是 Nacos 的官网地址&#xff1a;https://nacos.io/zh-cn/index.html 官网的文档对于…

Python中用PyTorch机器学习神经网络分类预测银行客户流失模型

分类问题属于机器学习问题的类别&#xff0c;其中给定一组特征&#xff0c;任务是预测离散值。分类问题的一些常见示例是&#xff0c;预测肿瘤是否为癌症&#xff0c;或者学生是否可能通过考试。 最近我们被客户要求撰写关于银行客户流失的研究报告&#xff0c;包括一些图形和…

@Scheduled定时任务搭配Redis防止多实例定时重复调用

有个Redis安装使用教程&#xff0c;可视化界面&#xff0c;有需要的话&#xff0c;可以打开这个链接去看一下 https://blog.csdn.net/weixin_45507672/article/details/105973279 创建个maven项目&#xff0c;在pom.xml文件加上以下依赖 <dependency><groupId>or…

4EVERLAND专用网关公告,免费体验

我们很高兴地宣布发布 4EVERLAND 专用 IPFS 网关&#xff01;与 4EVERLAND 公共网关一起&#xff0c;4EVERLAND 专用网关将为全世界的开发者和用户提供更快、更稳定地访问更能体现其品牌形象的 IPFS 内容。 专用网关的好处&#xff1a; 全球分布的边缘节点提供全球加速无速率…

[附源码]计算机毕业设计快转二手品牌包在线交易系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Spring Boot 入门到精通(二)

文章目录五、SpringBoot整合MyBatis5.1 mapper 配置5.2 mapper映射配置&#xff1a;配置文件方式5.3 注解配置方式六. 自定义部分SpringMvc配置。6.1 SpringBoot整合日期转换器6.1.1 配置原理6.1.2 日期转换器整合6.2 SpringBoot整合拦截器七. Spring Boot 自定义日志配置&…

C++11特性-类的改进

1.构造函数 1.委托构造函数&#xff1a;允许同一个类的构造函数调用其他构造函数&#xff0c;简化变量初始化 class BB { public:BB() {}BB(int max) {this->m_max max > 0 ? max : 100;cout << "max " << this->m_max << endl;}BB(i…