Java中的线程同步

为什么要实现线程同步

线程的同步是为了保证多个线程按照特定的顺序、协调地访问共享资源,避免数据不一致和竞争条件等问题。

线程同步的方式

1.synchronized关键字

(1)同步方法

public synchronized void save(){}

注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

(2)同步代码块

通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。我们要根据具体的业务逻辑来控制锁的粒度。

public class SynchronizedThread {
 
    class Bank {
 
        private int account = 200;
 
        public int getAccount() {
            return account;
        }
 
        /**
         * 用同步方法实现
         *
         * @param money
         */
        public synchronized void save(int money) {
            account += money;
        }
 
        /**
         * 用同步代码块实现
         *
         * @param money
         */
        public void save1(int money) {
            synchronized (this) {
                account += money;
            }
        }
    }
 
    class NewThread implements Runnable {
        private Bank bank;
 
        public NewThread(Bank bank) {
            this.bank = bank;
        }
 
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                // bank.save1(10);
                bank.save(10);
                System.out.println(i + "账户余额为:" + bank.getAccount());
            }
        }
 
    }
 
    /**
     * 建立线程,调用内部类
     */
    public void useThread() {
        Bank bank = new Bank();
        NewThread new_thread = new NewThread(bank);
        System.out.println("线程1");
        Thread thread1 = new Thread(new_thread);
        thread1.start();
        System.out.println("线程2");
        Thread thread2 = new Thread(new_thread);
        thread2.start();
    }
 
    public static void main(String[] args) {
        SynchronizedThread st = new SynchronizedThread();
        st.useThread();
    }
 
}
//400

2.使用重入锁reentrantLock()

class Bank {
 
            private int account = 100;
            //需要声明这个锁
            private Lock lock = new ReentrantLock();
            public int getAccount() {
                return account;
            }
            //这里不再需要synchronized 
            public void save(int money) {
                lock.lock();
                try{
                    account += money;
                }finally{
                    lock.unlock();
                }
 
            }
        }

3.使用wait()、notify()和notifyAll()方法

  1. 同步块:首先,必须在一个同步块中调用 wait()notify(), 或 notifyAll() 方法,因为它们必须拥有目标对象的监视器锁。

  2. 调用 wait() 方法:当线程想要等待某个条件时,它会在一个同步块中调用 wait() 方法。这将使当前线程暂停执行并释放监视器锁,直到另一个线程调用 notify() 或 notifyAll()

  3. 调用 notify() 或 notifyAll() 方法:当某个线程改变了共享资源的条件,使得其他等待的线程可以继续执行时,它会调用 notify() 或 notifyAll() 方法。notify() 方法随机唤醒一个等待线程,而 notifyAll() 方法唤醒所有等待线程。

以下是一个简单的例子,演示了如何使用这些方法:

public class WaitNotifyExample {

    // 共享资源
    private Object lock = new Object();

    public void consumer() throws InterruptedException {
        synchronized (lock) {
            System.out.println("Consumer waiting for a product...");
            lock.wait(); // 等待产品
            System.out.println("Consumer got a product.");
        }
    }

    public void producer() {
        synchronized (lock) {
            System.out.println("Producer produced a product.");
            lock.notify(); // 唤醒等待的消费者线程
        }
    }

    public static void main(String[] args) {
        WaitNotifyExample example = new WaitNotifyExample();

        // 创建消费者线程
        Thread consumerThread = new Thread(() -> {
            try {
                example.consumer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 创建生产者线程
        Thread producerThread = new Thread(example::producer);

        // 启动线程
        consumerThread.start();
        producerThread.start();
    }
}

在这个例子中:

  • consumer() 方法代表消费者线程,它在同步块中调用 wait() 方法等待一个产品。
  • producer() 方法代表生产者线程,它在同步块中调用 notify() 方法来通知消费者线程产品已经准备好了。
  • lock 对象用作同步监视器。

4.使用 CountDownLatch 实现线程同步

CountDownLatch 是一个允许一个或多个线程等待其他线程完成操作的同步工具。

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3); // 初始化计数器为3

        // 创建并启动三个线程
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println("Thread " + Thread.currentThread().getId() + " is running.");
                latch.countDown(); // 每个线程完成操作后,计数器减1
            }).start();
        }

        // 主线程等待直到计数器减到0
        latch.await();
        System.out.println("All threads have finished. Main thread is running.");
    }
}

下面举一些案例:

案例一(es数据批量导入)

在我们项目上线之前,我们需要把数据库中的数据一次性的同步到es索引库中,但是当时的数据好像是1000万左右,一次性读取数据肯定不行(oom异常),当时我就想到可以使用线程池的方式导入,利用CountDownLatch来控制,就能避免一次性加载过多,防止内存溢出

整体流程就是通过CountDownLatch+线程池配合去执行

详细实现流程:

具体代码如下:

​
@Service
@Transactional
@Slf4j
public class ApArticleServiceImpl implements ApArticleService {
 
    @Autowired
    private ApArticleMapper apArticleMapper;
 
    @Autowired
    private RestHighLevelClient client;
 
    @Autowired
    private ExecutorService executorService;
 
    private static final String ARTICLE_ES_INDEX = "app_info_article";
 
    private static final int PAGE_SIZE = 2000;
 
    /**
     * 批量导入
     */
    @SneakyThrows
    @Override
    public void importAll() {
 
        //总条数
        int count = apArticleMapper.selectCount();
        //总页数
        int totalPageSize = count % PAGE_SIZE == 0 ? count / PAGE_SIZE : count / PAGE_SIZE + 1;
        //开始执行时间
        long startTime = System.currentTimeMillis();
        //一共有多少页,就创建多少个CountDownLatch的计数
        CountDownLatch countDownLatch = new CountDownLatch(totalPageSize);
 
        int fromIndex;
        List<SearchArticleVo> articleList = null;
 
        for (int i = 0; i < totalPageSize; i++) {
            //起始分页条数
            fromIndex = i * PAGE_SIZE;
            //查询文章
            articleList = apArticleMapper.loadArticleList(fromIndex, PAGE_SIZE);
            //创建线程,做批量插入es数据操作
            TaskThread taskThread = new TaskThread(articleList, countDownLatch);
            //执行线程
            executorService.execute(taskThread);
        }
 
        //调用await()方法,用来等待计数归零
        countDownLatch.await();
 
        long endTime = System.currentTimeMillis();
        log.info("es索引数据批量导入共:{}条,共消耗时间:{}秒", count, (endTime - startTime) / 1000);
    }
    //将查出的单页数据同步到es的线程
    class TaskThread implements Runnable {
 
        List<SearchArticleVo> articleList;
        CountDownLatch cdl;
 
        public TaskThread(List<SearchArticleVo> articleList, CountDownLatch cdl) {
            this.articleList = articleList;
            this.cdl = cdl;
        }
 
        @SneakyThrows
        @Override
        public void run() {
            //批量导入
            BulkRequest bulkRequest = new BulkRequest(ARTICLE_ES_INDEX);
 
            for (SearchArticleVo searchArticleVo : articleList) {
                bulkRequest.add(new IndexRequest().id(searchArticleVo.getId().toString())
                        .source(JSON.toJSONString(searchArticleVo), XContentType.JSON));
            }
            //发送请求,批量添加数据到es索引库中
            client.bulk(bulkRequest, RequestOptions.DEFAULT);
 
            //让计数减一
            cdl.countDown();
        }
    }
}
 
​

案例二(数据汇总)

在一个电商网站中,用户下单之后,需要查询数据,数据包含了三部分:订单信息、包含的商品、物流信息;这三块信息都在不同的微服务中进行实现的,我们如何完成这个业务呢?

具体代码如下:

@RestController
@RequestMapping("/order_detail")
@Slf4j
public class OrderDetailController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @Autowired
    private ExecutorService executorService;
 
 
    @SneakyThrows
    @GetMapping("/get/detail_new/{id}")
    public Map<String, Object> getOrderDetailNew() {
 
        long startTime = System.currentTimeMillis();
 
        Future<Map<String, Object>> f1 = executorService.submit(() -> {
            Map<String, Object> r =
                    restTemplate.getForObject("http://localhost:9991/order/get/{id}", Map.class, 1);
            return r;
        });
        Future<Map<String, Object>> f2 = executorService.submit(() -> {
            Map<String, Object> r =
                    restTemplate.getForObject("http://localhost:9991/product/get/{id}", Map.class, 1);
            return r;
        });
 
        Future<Map<String, Object>> f3 = executorService.submit(() -> {
            Map<String, Object> r =
                    restTemplate.getForObject("http://localhost:9991/logistics/get/{id}", Map.class, 1);
            return r;
        });
 
 
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("order", f1.get());
        resultMap.put("product", f2.get());
        resultMap.put("logistics", f3.get());
 
        long endTime = System.currentTimeMillis();
 
        log.info("接口调用共耗时:{}毫秒",endTime-startTime);
        return resultMap;
    }
 
    @SneakyThrows
    @GetMapping("/get/detail/{id}")
    public Map<String, Object> getOrderDetail() {
 
        long startTime = System.currentTimeMillis();
 
        Map<String, Object> order = restTemplate.getForObject("http://localhost:9991/order/get/{id}", Map.class, 1);
 
        Map<String, Object> product = restTemplate.getForObject("http://localhost:9991/product/get/{id}", Map.class, 1);
 
        Map<String, Object> logistics = restTemplate.getForObject("http://localhost:9991/logistics/get/{id}", Map.class, 1);
 
        long endTime = System.currentTimeMillis();
 
 
 
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("order", order);
        resultMap.put("product", product);
        resultMap.put("logistics", logistics);
 
        log.info("接口调用共耗时:{}毫秒",endTime-startTime);
        return resultMap;
    }
 
 
}
  • 在实际开发的过程中,难免需要调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能

  • 报表汇总

5.用 CyclicBarrier 实现线程同步

CyclicBarrier 是一个允许一组线程互相等待,直到所有线程都达到某个屏障点的同步工具。与 CountDownLatch 不同,CyclicBarrier 可以被重用。

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int parties = 3;
        CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
            System.out.println("All threads have reached the barrier. Barrier action executed.");
        });

        // 创建并启动三个线程
        for (int i = 0; i < parties; i++) {
            new Thread(() -> {
                try {
                    System.out.println("Thread " + Thread.currentThread().getId() + " is waiting on barrier.");
                    barrier.await(); // 线程在屏障处等待
                    System.out.println("Thread " + Thread.currentThread().getId() + " has passed the barrier.");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

网络编程+文件上传操作的理解

前言&#xff1a; 概述:在网络通信协议下,不同计算机上运行的程序,进行数据传输 比如:通信,视频通话,网游,邮件等 只要是计算机之间通过网络进行数据传输,就有网络编程的存在 &#xff08;下面单纯是在Java基础中了解了一下网络编程&#xff0c;感觉理…

如何保证数据库和redis的数据一致性

1、简介 在客户端请求数据时&#xff0c;如果能在缓存中命中数据&#xff0c;那就查询缓存&#xff0c;不用在去查询数据库&#xff0c;从而减轻数据库的压力&#xff0c;提高服务器的性能。 2、问题如何保证两者的一致性 先更新数据库在删除缓存 难点&#xff1a;如何保证…

Classifier-Free Guidance (CFG) Scale in Stable Diffusion

1.Classifier-Free Guidance Scale in Stable Diffusion 笔记来源&#xff1a; 1.How does Stable Diffusion work? 2.Classifier-Free Diffusion Guidance 3.Guide to Stable Diffusion CFG scale (guidance scale) parameter 1.1 Classifier Guidance Scale 分类器引导是…

vite配置环境变量和使用,配置正确后import.meta.env.VITE_APP_BASE_URL编译报错的解决方法

一、配置&#xff1a; 1.新增四个环境文件 .env.development .env.test .env.production .env.pre 内容为不同环境的不同参数变量必须以VITE_APP开头&#xff0c;如&#xff1a; #接口地址 VITE_APP_BASE_URL"&#xffe5;&#xffe5;&#xffe5;&#xffe5;&#xff…

嵌入式人工智能(6-树莓派4B按键输入控制LED)

1、按键 按键的原理都是一样&#xff0c;通过按键开关的按下导通&#xff0c;抬起断开的情况&#xff0c;GPIO引脚来检测其是否有电流流入。GPIO有input()方法&#xff0c;对于GPIO引脚检测电流&#xff0c;不能让其引脚悬空&#xff0c;否则引脚会受周边环境电磁干扰产生微弱…

获取欧洲时报中国板块前新闻数据(多线程版)

这里写目录标题 一.数据获取流程二.获取主页面数据并提取出文章url三.获取文章详情页的数据并提取整体代码展示 一.数据获取流程 我们首先通过抓包就能够找到我们所需数据的api 这里一共有五个参数其中只有第一个和第五个参数是变化的第一个参数就是第几页第五个是一个由时…

HCNA ICMP:因特网控制消息协议

ICMP&#xff1a;因特网控制消息协议 前言 Internet控制报文协议ICMP是网络层的一个重要协议。ICMP协议用来在网络设备间传递各种差错和控制信息&#xff0c;他对于手机各种网络信息、诊断和排除各种网络故障有至关重要的作用。使用基于ICMP的应用时&#xff0c;需要对ICMP的工…

中介者模式(行为型)

目录 一、前言 二、中介者模式 三、总结 一、前言 中介者模式&#xff08;Mediator Pattern&#xff09;是一种行为型设计模式&#xff0c;又成为调停者模式&#xff0c;用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地互相引用&#xff0c;从而使其耦合…

防火墙双机热备(接上一个NAT实验)

一、实验拓扑 二、实验需求 1、对现有网络进行改造升级&#xff0c;将当个防火墙组网改成双机热备的组网形式&#xff0c;做负载分担模式&#xff0c;游客区和DMZ区走FW3&#xff0c;生产区和办公区的流量走FW1 2、办公区上网用户限制流量不超过100M&#xff0c;其中销售部人员…

【深度学习】基于深度学习的模式识别基础

一 模式识别基础 “模式”指的是数据中具有某些相似特征或属性的事物或事件的集合。具体来说&#xff0c;模式可以是以下几种形式&#xff1a; 视觉模式 在图像或视频中&#xff0c;模式可以是某种形状、颜色组合或纹理。例如&#xff0c;人脸、文字字符、手写数字等都可以视…

【边缘计算网关教程】8.ModbusTCP采集存储Influxdb

前景回顾-【边缘计算网关教程】7.Modbus协议转MQTT协议-CSDN博客 需求概述 &#x1f4a1;注&#xff1a;使用Influxdb数据库节点&#xff0c;需要插上micro sd卡才可以 本章节主要实现一个流程&#xff1a;EG8200每10秒采集一次Modbus TCP数据存入Influxdb数据库,并且每分钟…

[日进斗金系列]用码上飞解决企微开发维修管理系统的需求

前言&#xff1a; 今天跟大家唠唠如何用小money生 大money的方法&#xff0c;首先我们需要准备一个工具。 这个工具叫码上飞CodeFlying&#xff0c;它是目前国内首发的L4级自动化智能软件开发平台。 它可以在短时间内&#xff0c;与AI进行几轮对话就能开发出一个可以解决实际…

pytorch学习(六):卷积层的使用

卷积函数的概念 卷积核从输入特征图的左上角开始&#xff0c;按照设定的步长&#xff08;Stride&#xff09;滑动。步长决定了卷积核每次滑动的像素数&#xff0c;这里我们假设步长 s1。在每次滑动时&#xff0c;卷积核与输入特征图对应位置的元素相乘&#xff0c;然后将这些乘…

ENSP中VLAN的设置

VLAN的详细介绍 VLAN&#xff08;Virtual Local Area Network&#xff09;即虚拟局域网&#xff0c;是一种将一个物理的局域网在逻辑上划分成多个广播域的技术。 以下是关于 VLAN 的一些详细介绍&#xff1a; 一、基本概念 1. 作用&#xff1a; - 隔离广播域&#xff1a…

Linux 安装 Docker Compose

Docker Compose 是一种用于定义、运行和管理多容器Docker应用程序的工具&#xff0c;通过YAML文件配置服务&#xff0c;实现一键启动和停止所有服务。 以下是如何在 Linux 系统上安装 Docker Compose 的步骤 1. 下载 Docker Compose 可执行文件 wget https://github.com/dock…

c++ primer plus 第16章string 类和标准模板库,16.1.3 使用字符串

c primer plus 第16章string 类和标准模板库,16.1.3 使用字符串 c primer plus 第16章string 类和标准模板库,16.1.3 使用字符串 文章目录 c primer plus 第16章string 类和标准模板库,16.1.3 使用字符串16.1.3 使用字符串程序清单16.3 hangman.cpp 16.1.3 使用字符串 现在&a…

暑期大数据人工智能企业项目试岗实训班

在数字化转型的浪潮中&#xff0c;大数据和人工智能等前沿技术已成为推动经济发展和科技进步的关键动力。当前&#xff0c;全球各行各业都在积极推进数字化转型&#xff0c;不仅为经济增长注入新活力&#xff0c;也对人才市场结构产生了深刻影响&#xff0c;尤其是对数字化人才…

2024.7.16作业

使用结构体完成学生&#xff08;学号、姓名、性别、成绩&#xff09;管理系统 1> 使用菜单实现 2> 功能1&#xff1a;完成对学生信息的录入&#xff0c;确定人数&#xff0c;完成输入 2> 功能2&#xff1a;完成对学生信息的输出 3> 功能3&#xff1a;输出成绩最…

Linux C | 管道open打开方式

Linux C | 管道open打开方式 1.参考 1. 管道 2.现象 是的&#xff0c;这段代码在调用 open(AUDIOIN_FIFO, O_RDONLY) 时可能会被阻塞。原因是 FIFO&#xff08;命名管道&#xff09;在以只读模式打开时&#xff0c;如果没有其他进程以写模式打开该 FIFO&#xff0c;open 调用将…

ASP.NET Core----基础学习07----ViewStart ViewImports文件的使用

文章目录 1._ViewStart.cshtml的使用2.更换Layout文件3._ViewImports.cshtml文件的使用 1._ViewStart.cshtml的使用 step1&#xff1a; 在Views文件夹下面创建_ViewStart.cshtml文件 step2&#xff1a; 删掉视图文件中的Layout设置行 step3&#xff1a; 最终显示效果&#xff…