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

三相电机控制入门:端电压、相电压、线电压到底怎么测?(附实测波形对比) 本文为三相电机控制初学者提供了端电压、相电压线电压的完整测量指南。通过厘清三者的物理定义与测量方法,详细讲解了如何搭建虚拟星形点电路,并对比分析了方波、SPWMSVPWM三种主流控制模式下关键电压波形的核心特征与差异,帮助工程师掌握正确的示波器测量与波形解读技巧。 阅读详情

使用多线程并发处理,目的是为了让程序更充分地利用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 包中的 Lock 、ReadWriteLock 基本上持平。从趋势来看,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 给对象加同步锁,让同一时间只有一个线程能操作临界区的控制。接下来,我们看一下线程的挂起和唤醒,这两个操作使用被线程成功加锁的对象的 wait 和 notify 方法来完成,唤醒除了notify 外还有 notifyAll方法用来唤醒所有线程。下面我们先看一下这几个方法的解释。

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

这里有两点需要各位注意的地方, 第一个是 wait、notify、notifyAll 都是 Object 类中的方法,而不是 Thread 类的。

 

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

为什么 wait、notify、notifyAll 不定义在 Thread 类中?为什么 wait、notify、notifyAll 要配合 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

与 wait 和 notify 方法一样,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。

西门子PLC实战:基于1200/1500的工业物联网MQTT通讯全解析 本文详细解析了如何在西门子S7-1200/1500系列PLC上实现工业物联网MQTT通讯。通过从硬件准备、网络配置到TIA Portal中MQTT_Client功能块的实战编程,手把手指导工程师打通车数据桥梁,解决信息孤岛问题,实现PLC数据高效上云或设备互联。 阅读详情

相关推荐

ncode控件使用总结

数据格式txt转s3t #HEADER #TITLES Absolute_Time,COG_Ax,COG_Ay,COG_Az,LF_SPINDLE_AZ #UNITS ,g,g,g,g, #DATATYPES Double,Float,Float,Float,Float #DATA 2. 导入s3t数据 3. 输入数据控件 直接拖入数据源或者添加控件数据 4.

技术联盟 1967

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

同步控制的一大思路就是加锁,除了本问学习到的 sychronized 同步控制Java 里还有 JUC 的可重入锁、读写锁这种加锁方式,这个我们后续介绍 JUC 的时候会给大家讲解。另外一种思路是不加锁,让线程线程之尽量不要使用共享数据,ThreadLocal 就是这种思路,下篇我们介绍 Java 的线程本地存储 -- ThreadLocal。

BASK2311的博客 456

ICM20948 DMP代码详解(69)

ICM20948 DMP代码详解(69)

phmatthaus的专栏 576

数据争用(data race) 竞态条件(race condition)

在有关多线程编程的话题中,数据争用(data race) 竞态条件(race condition)是两个经常被提及的名词,它们两个有着相似的名字,也是我们在并行编程中极力避免出现的。但在处理实际问题时,我们应该能明确区分它们两个。 1.数据争用(data race) 定义:①多个线程对于同一个变量、②同时地、③进行读/写操作的现象并且④至少有一个线程进行写操作。(也就是说,如果所有线程都是只...

dwj_daiwenjie的博客 4895

what is a data race?

什么是 data race?data race 发生的充分条件1.两个或者更多线程在一个程序中,并发的访问同一数据;2.至少一个访问是写操作;3.这些线程都不使用任何互斥锁来控制这些访问。#当上面这些条件发生时,访问的顺序是不确定的,并且基于这些不同的运行顺序会导致计算产生不同的结果(这些结果也是正确的)。一些data race 是良性的,但是更多的是会产生程序 的bug。 https://docs

This is Chuanqiz's Blog 1266

别混淆数据争用(data race) 竞态条件(race condition)

本文介绍并对比解析数据争用竞态条件这两个概念,并以实际代码情景进行分析,最后给出解决方案。

hyman.lu 1万+

Java并发简述(同步注解、发布溢出、并发容器、工作窃取、锁等)

Java并发编程实战》知识点抽取

weixin_43626356的博客 1263

LockCondition实现线程同步通信

1>Lock:          Lock比传统线程模型中的Synchronied方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象.两个线程执行的代码段要实现同步互斥的效果,它们必须用同一个Lock对象,锁是在代表要操作的资源的类的内部方法中,而不是线程代码中.         上面的输出器Outputer类就可以这样改写: class Outputer2 {

shenjianxz的博客 436

data rac

最近在看effective STL, 其中看到yige

a130737的专栏 613

“深入理解Java多线程编程:并发控制与同步机制解析“

总结起来,深入理解Java多线程编程需要掌握并发控制同步机制的原理使用方法。通过合理地使用锁、原子操作、synchronized关键字等机制,可以保证多线程程序的正确性性能。Java多线程编程是指在Java程序中使用多个线程来执行任务。多线程编程可以提高程序的并发性性能,但也会带来一些并发控制同步机制的问题。下面我将详细解析这些问题。除了上述机制外,Java还提供了一些高级的并发工具数据结构,如线程池、阻塞队列等,它们能够帮助我们更方便地进行并发控制同步。

jiheng1的博客 550

Java多线程之线程同步(解决线程安全问题)

1.什么是线程安全:保证多个线程同时对某一对象或资源操作时不会出现问题 2.出现线程安全的原因? (1)存在多线程并发 (2)同时访问共享资源 (3)存在修改共享资源 例子是模拟两个人(两个线程)同时去取同一个账户里面的钱操作。 同步代码块:(5)步骤:选中核心代码块(逻辑代码)--ctrl+alt+t----选中第9个synchronized----然后在生成的括号里面加上this就好了(如果要锁更具体的,就把this换为更具体 还有另外两个方式:同步方法Lock方法

快乐学习 1028

Synchronized对象加锁防止并发

[color=brown]前言:处理简单的并发可以给对象加把锁,防止不同线程对同一资源同时进行操作,但是也需要谨慎使用"synchronized "关键字。[/color] [b]1、同步锁[/b] [code="java"]package com.boonya.concurrent; /** * Java同步锁 * * @author BOONYACHENGDU@GMAI...

boonya的博客 812

大白话C++之:一文搞懂C++多线程内存模型(Memory Order)

本文深入探讨了多线程编程中的两大核心概念:数据竞争与内存顺序。通过具体的 C++ 示例,我们首先了解了数据竞争的本质及其对程序正确性的影响,随后展示了如何利用 C++11 引入的 std::atomic 来有效解决这一问题。接着,文章转向讨论内存顺序,阐释了在并发环境下,如何处理由于编译器处理器优化带来的指令重排现象。文章最后通过示例代码清晰地说明了内存顺序在保证多线程程序正确性中的重要性。

sinat_38293503的博客 7637

线程同步控制问题

多线程的程序中,除了要放置资源冲突外,还要保证线程的同步。 通过生产者消费者模型来验证线程的同步资源共享问题。 假设有一个生产者Producer,一个消费者Consumer 生产者产生0~9的整数,将他们存储在Box中。要求生产者生产一个数字,消费者取得一个数字,这就涉及到了线程的同步的问题。 这个问题可以通过两个线程实现生产者消费者,它们共享Box对象。如果不加控制就得不到预期的效果。 1. 不同步的设计 //需要被共享的Box对象 public class Box { privat

小超 280

多线程—7种同步方法

多线程—7种同步方法 原文https://www.cnblogs.com/cxxjohnson/p/8536257.html#h3 关于线程同步(7种方式) 同步方法 同步代码块 使用重入锁实现线程同步(ReentrantLock) 使用特殊域变量(volatile)实现同步(每次重新计算,安全但并非一致) 使用局部变量实现线程同步(ThreadLocal)以空换时 使用原子变量实现线程同步(AtomicInteger(乐观锁)) 使用阻塞队列实现线程同步(BlockingQueue.

yuyeqianhen的博客 3067

webase-deploy.zip

webase-deploy.zip

不规则mask图像修复论文+数据分享

近三年,14篇针对不规则mask图像修复进行研究系列学习论文集 NVIDIA Irregular Mask Dataset(12000 mask 数据)-——感谢支持——资源价值描述可参考博文 https://positive.blog.csdn.net/article/details/109844015

上一篇: Redis缓存何以一枝独秀?以及热门面试题中Redis的核心特性
下一篇: 膜拜!清华大佬整理Java开发岗面试题,成功拿下美团电商部offer
Java佳佳
博客等级 码龄4年 216粉丝 286原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值