Java并发编程--多线程间的同步控制和通信

news2024/12/24 0:34:13

使用多线程并发处理,目的是为了让程序更充分地利用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

总结

同步控制的一大思路就是加锁,除了本问学习到的 sychronized 同步控制,Java 里还有 JUC 的可重入锁、读写锁这种加锁方式,这个我们后续介绍 JUC 的时候会给大家讲解。

另外一种思路是不加锁,让线程和线程之间尽量不要使用共享数据,ThreadLocal 就是这种思路,下篇我们介绍 Java 的线程本地存储 -- ThreadLocal。

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

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

相关文章

JS表达式完全攻略

在语法概念中&#xff0c;运算符属于词&#xff0c;表达式属于短语。表达式由一个或多个运算符、操作数组成的运算式。表达式的功能是执行计算&#xff0c;并返回一个值。 表达式的形式 表达式是一个比较富有弹性的运算单元。简单的表达式就是一个直接量、常量或变量。例如&a…

【优化调度】粒子群算法求解水火电调度优化问题【含Matlab源码 1181期】

⛄一、粒子群算法简介 1 引言 自然界中的鸟群和鱼群的群体行为一直是科学家的研究兴趣所在。生物学家Craig Reynolds在1987年提出了一个非常有影响的鸟群聚集模型&#xff0c;在他的仿真中&#xff0c;每一个个体都遵循&#xff1a;避免与邻域个体相撞&#xff1a;匹配邻域个体…

Java面向对象之——封装

文章目录前言一、封装的概念二、何为封装&#xff1f;三、封装拓展——包&#x1f351;1、包的概念&#x1f351;2、导入包中的类&#x1f351;3、自定义包&#x1f351;4、常见的包四、访问限定符&#x1f351;1、public修饰符&#x1f351;2、private修饰符&#x1f351;3、默…

软件产品确认测试包括哪些方面

1.技术方面 确认测试又称有效性测试&#xff0c;是在模拟的环境下&#xff0c;运用黑盒测试的方法&#xff0c;验证被测软件是否满足需求规格说明书列出的需求。 确认测试的目的是向未来的用户表明系统能够像预定要求那样工作。经集成测试后&#xff0c;已经按照设计把所有的模…

Docker 下 jitsi-meet 视频服务器 安装部署

一、参考网站 官网文档地址&#xff1a;Self-Hosting Guide - Docker | Jitsi Meet 二、Docker 版本 三、安装部署 3.1、下载并解压缩安装包 地址&#xff1a;Release stable-8044: release jitsi/docker-jitsi-meet GitHub CSDN地址&#xff1a;docker-jitsi-meet-stab…

visdom安装及使用

目录1. 安装visdom的流程2. 使用流程1. 安装visdom的流程 重点参考链接&#xff1a;visdom安装出现Downloading scripts, this may take a little while然后就不动了 先去到github直接下载了visdom的压缩包&#xff1a;https://github.com/fossasia/visdom然后将visdom-maste…

第七章 数学 6 AcWing 1593. 整数分解

第七章 数学 6 AcWing 1593. 整数分解 原题链接 AcWing 1593. 整数分解 算法标签 数学 数论 DP 背包问题 思路 类似AcWing 12. 背包问题求具体方案 把n看成背包的容积N 因为n最多不超过400&#xff0c;然而p进制最少为2 所以物品的价值最大可以取到20(因为20^2 400) 所以…

Python读取CSV文件,数值精度丢失

Excel保存为csv以后&#xff0c;大数值的列&#xff0c;会把转换为科学计数法&#xff0c;而且后边几位都会被转为0. 搞了很多方法,最后直接安装 openpyxl 组件 和 pandas&#xff0c; 读取Excel文件就行了。 data pd.read_excel("C:/work/20221111AI/cleaned_data_noTi…

第二章 计算机算术

数据表示决定了计算机所执行操作的类型&#xff0c;数据从一个位置传到另一个位置的方法&#xff0c; 以及对存储元件的特性要求。浮点运算是非常重要的&#xff0c;因为它的实现决定了计算机执行复杂图形变换和图像处理的速度&#xff0c; 而且浮点运算对计算的准确度也有很重…

高通平台开发系列讲解(mm-camera篇)MM-CAMERA框架

文章目录 一、Camera软件位置1.1、开源代码1.2、专有源码二、摄像头前端(Camera Frontend)2.1、整体框架2.2、 Camera HAL与mm-camera交互沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇章主要介绍高通平台mm-camera代码框架。 一、Camera软件位置 QTI 针对 An…

Flink 状态编程

状态编程有状态算子状态的管理状态的分类按键分区状态&#xff08;Keyed State&#xff09;支持的结构类型代码实现状态生存时间&#xff08;TTL&#xff09;算子状态&#xff08;Operator State&#xff09;基本概念和特点状态类型代码实现广播状态&#xff08;Broadcast Stat…

DFP 数据转发协议应用实例 .与其它厂商 LoRA 设备匹配

DFP 数据转发协议应用实例 5.与其它厂商 LoRA 设备匹配 DFP 是什么&#xff1f; 稳控科技编写的一套数据转发规则&#xff0c; 取自“自由转发协议 FFP&#xff08;Free Forward Protocol&#xff09;” &#xff0c;或者 DFP&#xff08;DoubleF Protocol&#xff09;&#xf…

【优化求解】粒子群算法求解仓库成本控制优化问题【含Matlab源码 1577期】

⛄一、粒子群算法简介 1 引言 自然界中的鸟群和鱼群的群体行为一直是科学家的研究兴趣所在。生物学家Craig Reynolds在1987年提出了一个非常有影响的鸟群聚集模型&#xff0c;在他的仿真中&#xff0c;每一个个体都遵循&#xff1a;避免与邻域个体相撞&#xff1a;匹配邻域个体…

基于JAVA的网上图书商城参考【数据库设计、源码、开题报告】

数据库脚本下载地址&#xff1a; https://download.csdn.net/download/itrjxxs_com/86427643 主要使用技术 Struts2HibernateJSPCSSJSMysql 功能介绍 系统有五类用户&#xff0c;分别是&#xff1a;会员用户&#xff0c;商品管理员&#xff0c;订单管理员&#xff0c;会员管…

【MSSQL】SQL SERVER导入中文乱码问题解决

公司最近承接了一个项目&#xff0c;甲方现使用旧版SiteServer框架&#xff08;以下简称“SiteCMS”&#xff09;作为门户网站&#xff0c;使用的数据源是SQL Server。 现在需要对SiteCMS进行升级&#xff0c;在升级时数据库和数据库结构也需要同时更新&#xff0c;其中数据库…

硬件顶配、数字先行,路特斯重塑「智能座舱」

诚然&#xff0c;在车联网的高度普及、5G技术的铺开以及汽车产业融合的大势下&#xff0c;传统单一驾驶与乘坐功能的汽车座舱正在被颠覆&#xff0c;更高级、更智能的“智能座舱”应运而生。 从行业发展态势来看&#xff0c;大屏多屏已经是智能汽车的基本操作&#xff0c;智能…

【网络安全】——逻辑漏洞之短信轰炸漏洞

作者名&#xff1a;Demo不是emo 主页面链接&#xff1a;主页传送门创作初心&#xff1a;舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座右…

程序的环境

文章目录[TOC](文章目录)前言一、程序环境分类二、翻译环境1.总体流程2.编译2.1预编译2.2编译2.3汇编3.链接三、运行环境总结前言 我们平时在编译器上编写代码&#xff0c;然后运行代码&#xff0c;最后得到程序的运行结果。这让我们不经好奇&#xff1a;程序在电脑中到底经过…

Java常用设计模式

前言 设计模式是对大家实际工作中写的各种代码进行高层次抽象的总结&#xff0c;其中最出名的当属 Gang of Four (GoF)的分类了&#xff0c;他们将设计模式分类为 23 种经典的模式&#xff0c;根据用途我们又可以分为三大类&#xff0c;分别为创建型模式、结构型模式和行为型模…

物联网漏洞利用整体情况

物联网 威胁分析漏洞篇物联网威胁分析—漏洞篇 引言 本章将从漏洞利用角度对物联网威胁进行分析。首先&#xff0c;我们分析了 NVD和 Exploit-DB中的物联网 年度漏洞及利用 1 变化趋势&#xff1b;之后统计了绿盟威胁捕获系统捕获到的物联网漏洞利用的整体情况&#xff1b;最…