Java线程同步机制

第1章:引言

大家好,我是小黑。今天咱们来聊聊并发编程,咱们经常听说并行、并发这些词,特别是在处理大量数据、高用户负载时,这些概念就显得尤为重要了。为什么呢?因为并发编程可以帮助咱们的应用程序更有效地使用计算资源,处理更多任务,提升性能。

为什么要同步线程呢?想象一下,如果有多个线程同时对同一个数据进行读写,不加控制地乱来,结果可想而知,数据可能会乱七八糟。所以,咱们需要一种机制来确保数据的一致性和完整性。但这并不简单,同步机制的设计和使用充满了挑战,比如死锁、资源争夺、线程管理等等。

在接下来的章节里,咱们将逐一探讨Java中的各种同步机制,从基础的synchronized关键字到高级的并发工具类,比如CountDownLatchCyclicBarrierSemaphore

第2章:线程基础与同步

在Java中,线程可以看作是程序内部的独立执行路径。当你的程序启动时,Java虚拟机(JVM)会创建一个主线程,但你也可以创建自己的线程来执行特定的任务。这就像是在餐厅里,有多个服务员同时服务不同的桌子,每个服务员就像一个线程,同时处理各自的任务。

但问题来了,如果两个服务员同时去给同一桌客人上菜,可能就会乱套。同理,在Java中,如果多个线程同时操作同一个数据,就可能会引发问题。比如,一个线程在写数据,另一个同时来读,读到的可能就是不完整或者错误的数据。这就是为什么需要线程同步。

来看一个简单的例子。想象有一个共享的计数器,多个线程都要对它进行操作:

public class SharedCounter {private int count = 0;public void increment() {count++; // 增加计数器的值}public int getCount() {return count; // 获取当前计数器的值}
}

在这个例子中,如果多个线程同时调用increment()方法,count的值可能不会正确地增加,因为count++这个操作不是原子的,它包含读取count值、增加1、然后写回新值三个步骤。这就需要咱们用同步机制来确保每次只有一个线程能够执行这个操作。

第3章:深入理解synchronized

synchronized的工作原理

让我们先理解一下synchronized是怎么工作的。当一个线程访问某个对象的synchronized方法或代码块时,它会自动获取这个对象的锁。这个锁,就像是一个信号,告诉其他线程:“嘿,我正在使用这个对象,请你们等我用完。”只有当持有锁的线程执行完synchronized代码块,释放了锁,其他线程才能访问这个对象。

使用synchronized同步方法

来看一个例子。假设咱们有一个共享资源,比如一个银行账户:

public class BankAccount {private double balance; // 账户余额public BankAccount(double initialBalance) {this.balance = initialBalance;}// 同步方法来存钱public synchronized void deposit(double amount) {double newBalance = balance + amount;balance = newBalance;}// 同步方法来取钱public synchronized void withdraw(double amount) {double newBalance = balance - amount;balance = newBalance;}public double getBalance() {return balance;}
}

在这个例子中,depositwithdraw方法都是synchronized的。这意味着,当一个线程在存或取钱时,其他线程必须等它完成才能进行操作。

使用synchronized同步代码块

除了同步整个方法,咱们也可以只同步方法中的一部分代码。比如,如果只有一小部分代码访问了共享资源,咱们就可以使用`synchronized代码块:

public class Counter {private int count = 0;public void increment() {// 只同步这一小部分代码synchronized(this) {count++;}}public int getCount() {return count;}
}

这样,只有对count的增加操作是同步的,其他操作则不受影响。

synchronized的优缺点

synchronized的优点在于它简单易用,而且是Java内建的同步机制,不需要引入额外的库。但它也有缺点。比如,如果一个线程持有锁太久,其他所有需要这个锁的线程都得等着,这可能会导致效率问题。还有,如果不小心使用,可能会引发死锁。

第4章:探索锁(Locks)

锁的基本概念

在Java中,锁是用来控制多个线程对共享资源的访问的一种机制。和synchronized类似,锁也能防止多个线程同时执行特定的代码段。但不同的是,Java的java.util.concurrent.locks包提供的锁更加灵活,比如可以尝试非阻塞地获取锁,或者在尝试获取锁时等待一定的时间。

ReentrantLock的使用示例

让我们通过一个例子来看看如何使用ReentrantLock,这是一个实现了Lock接口的类:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class Counter {private final Lock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();  // 获取锁try {count++;} finally {lock.unlock();  // 释放锁}}public int getCount() {return count;}
}

在这个例子中,每当一个线程要执行increment方法时,它首先必须获取锁。如果锁已经被其他线程持有,该线程将等待(或阻塞)直到锁被释放。使用lockunlock方法,我们可以精确控制何时锁定和解锁,这比synchronized提供了更高的灵活性。

锁与synchronized的比较

那么,锁和synchronized有什么不同呢?主要区别在于灵活性和控制力。synchronized是隐式的锁定机制,而锁提供了显式的锁定和解锁操作。这意味着使用锁时,咱们可以更细粒度地控制锁的范围和时机。此外,锁还提供了其他高级功能,比如条件变量(Condition),这些功能在synchronized中是没有的。

第5章:CountDownLatch:协调多个线程

CountDownLatch的原理和使用场景

CountDownLatch是一个同步辅助类,用于延迟线程的进度直到其它线程的操作都完成。想象一下,你是一个赛跑运动员,裁判说:“预备——跑!”但你得等到所有运动员都准备好,枪声响起,比赛才正式开始。CountDownLatch就像是那个发令枪,确保所有线程都准备好了,然后一起出发。

CountDownLatch中,计数器的初始值表示需要等待的线程数量。每个线程完成它的任务后,计数器的值就减一。当计数器值降到零时,表示所有线程都完成了任务,等待在CountDownLatch上的线程就可以继续执行了。

实际代码示例

来看一个具体的例子。假设咱们有一个任务,需要等待三个服务线程都完成工作之后,主线程才能继续:

import java.util.concurrent.CountDownLatch;public class Main {public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(3); // 三个线程// 创建并启动三个线程for (int i = 1; i <= 3; i++) {new Thread(new Worker(i, latch)).start();}latch.await(); // 等待三个线程完成System.out.println("所有服务线程都已完成。主线程继续执行...");}static class Worker implements Runnable {private final int workerNumber;private final CountDownLatch latch;Worker(int workerNumber, CountDownLatch latch) {this.workerNumber = workerNumber;this.latch = latch;}@Overridepublic void run() {try {// 模拟工作System.out.println("服务线程 " + workerNumber + " 正在执行任务");Thread.sleep((long) (Math.random() * 1000 + 500));System.out.println("服务线程 " + workerNumber + " 完成任务");} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {latch.countDown(); // 完成任务,计数器减一}}}
}

在这个例子中,CountDownLatch确保主线程在所有三个工作线程完成它们的任务之前一直等待。这种方式在并发编程中非常常见,特别是在处理初始化操作或完成一组并发任务之后需要执行汇总操作的场景中。

通过CountDownLatch,咱们可以有效地协调多个线程,确保在继续执行主流程之前,所有的子任务都已经完成。这就是CountDownLatch为咱们提供的强大功能。

第6章:CyclicBarrier:循环屏障的应用

CyclicBarrier的工作机制

CyclicBarrier字面上的意思是“循环屏障”。它允许一组线程相互等待,达到一个公共屏障点(Barrier Point),然后再继续执行。不同于CountDownLatchCyclicBarrier可以重复使用,这就是“循环”(Cyclic)这个词的来源。

举个例子,想象一下接力赛跑,每个跑步者(线程)跑到接力点后要等待其他队员到齐,然后一起跑下一棒。CyclicBarrier就像是那个接力点,确保所有线程都到达后,再一起继续执行。

如何在项目中使用CyclicBarrier

来看一个具体的例子。假设咱们有一个任务,需要四个线程完成各自的部分工作,然后等大家都准备好了,一起执行下一步:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;public class Main {public static void main(String[] args) {final int THREAD_COUNT = 4;CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () ->System.out.println("所有线程都到达屏障点,继续执行下一步操作"));for (int i = 0; i < THREAD_COUNT; i++) {new Thread(new Task(i, barrier)).start();}}static class Task implements Runnable {private final int taskNumber;private final CyclicBarrier barrier;Task(int taskNumber, CyclicBarrier barrier) {this.taskNumber = taskNumber;this.barrier = barrier;}@Overridepublic void run() {try {// 模拟任务System.out.println("线程 " + taskNumber + " 正在执行任务");Thread.sleep((long) (Math.random() * 1000 + 500));System.out.println("线程 " + taskNumber + " 完成任务,等待其他线程");barrier.await(); // 在屏障处等待// 屏障打开后的操作System.out.println("线程 " + taskNumber + " 继续执行后续操作");} catch (InterruptedException | BrokenBarrierException e) {Thread.currentThread().interrupt();}}}
}

在这个例子中,每个线程完成它的部分任务后,会在barrier.await();这一行等待。直到所有线程都调用了await()方法,屏障才会打开,然后每个线程继续执行。

CountDownLatch的比较

虽然CyclicBarrierCountDownLatch都能用于线程间的协调,但CyclicBarrier可以被重置并重复使用,而CountDownLatch不能。CyclicBarrier更适用于那些多个线程必须互相等待才能继续执行的场景,而CountDownLatch则适用于一个线程必须等待其他线程完成某些操作的场景。

第7章:Semaphore:信号量的运用

信号量机制介绍

Semaphore是基于计数的同步工具,它可以维护一组许可证(Permits)。如果你想要访问一个资源,就必须先从信号量获取一个许可证。如果信号量中没有许可证可用了,那么请求许可证的线程就必须等待,直到有其他线程释放许可证。

想象一下,你在一个拥挤的餐厅等待座位。餐厅只有一定数量的座位(许可证),如果座位满了,就得等别人用完餐离开,你才能坐下。这就是信号量的工作方式。

Semaphore的实际应用示例

让我们通过一个示例来看看Semaphore是如何在实际中应用的。假设有一个打印机资源池,同时只能有限数量的用户使用打印机:

import java.util.concurrent.Semaphore;public class PrinterPool {private final Semaphore semaphore;public PrinterPool(int printerCount) {this.semaphore = new Semaphore(printerCount);}public void usePrinter() {try {semaphore.acquire(); // 获取许可证System.out.println("使用打印机进行打印工作");Thread.sleep(1000); // 模拟打印过程} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {semaphore.release(); // 释放许可证}}
}

在这个例子中,我们创建了一个Semaphore,其许可证数量对应打印机的数量。每当一个线程需要使用打印机时,它会尝试从信号量中获取一个许可证。如果所有的打印机都被占用了,线程将等待,直到有一个打印机变得可用。

适用场景和限制

Semaphore非常适用于那些需要限制对某些资源访问数量的场景。它可以保证只有固定数量的线程同时访问一个资源,这对于资源管理和控制是非常有帮助的。

然而,使用Semaphore时也需要小心。如果不正确地管理许可证的释放,可能会导致资源永远被占用,进而影响系统的稳定性。所以,在使用Semaphore时,要确保在所有情况下都能正确地释放许可证。

第8章:总结与最佳实践

各同步工具的优缺点
  • synchronized:简单易用,但在某些情况下可能导致效率问题,如过长的等待时间或死锁。
  • 锁(Locks):比synchronized提供更多的灵活性和控制力,但使用起来更复杂,且易于误用。
  • CountDownLatch:适用于等待一组操作完成的场景,但是一次性的,用完就没了。
  • CyclicBarrier:适用于在所有线程必须互相等待才能继续的场景,可以重用。
  • Semaphore:强大的资源控制工具,适合限制对资源的并发访问,但需要谨慎管理。
并发编程的最佳实践
  • 避免死锁:确保所有线程以相同的顺序获取锁,避免嵌套锁。
  • 降低锁的粒度:尽可能锁定代码的最小区域,减少等待时间。
  • 使用Java并发包的高级工具:利用java.util.concurrent包提供的工具,如ExecutorServiceFuture等,来简化线程管理和提高效率。
  • 注意资源共享:共享资源应该是线程安全的,或者在访问时进行适当的同步。
  • 避免过度同步:过多的同步可能导致性能问题,找到合适的平衡点。

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

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

相关文章

在Raspberry Pi Zero W中配置TFT LCD Framebuffer驱动

TFT LCD Framebuffer驱动配置 文章目录 TFT LCD Framebuffer驱动配置1、硬件准备2、软件配置2.1 启用SPI驱动2.2 TFT LCD设备驱动树配置 本文将以ILI9341 LCD为例&#xff0c;将详细介绍如何配置TFT LCD的Framebuffer驱动。 1、硬件准备 Raspberry Pi Zero W开发板一个&#x…

[AutoSar]基础部分 RTE 06 对runnable的触发和SWC的影响

目录 关键词平台说明一、runnable二、RTE的event2.1Mode类型event2.2周期触发类型2.3 数据交互触发 三、internal runnable value四、专属运行区指定五、per_instance memory 关键词 嵌入式、C语言、autosar、Rte 平台说明 项目ValueOSautosar OSautosar厂商vector芯片厂商T…

UG装配-动画制作

制作装配动画用到的命令是序列 制作动画前&#xff0c;先将所有约束取消 当我们在装配导航器中装配好产品后&#xff0c;可以在序列中编辑生产动态装配或爆炸动画&#xff1b; 需要注意的是&#xff0c;如果是希望创建装配或爆炸动画&#xff0c;需要先将所有约束取消&#…

mac版viso软件 流程图软件omnigraffile

OmniGraffle 是一款由 The Omni Group 开发的绘图工具&#xff0c;主要用于创建各种类型的图表、流程图、组织结构图、网站地图等。它提供了丰富的绘图工具和功能&#xff0c;包括形状、线条、文本、颜色、样式等&#xff0c;可以帮助用户轻松地创建出精美的图表和图形。 OmniG…

Micro-app 微前端框架demo介绍

Micro-app 框架 1、框架安装 npm i micro-zoe/micro-app --save2、子应用对应的view页面 <template><div><!-- name(必传)&#xff1a;应用名称url(必传)&#xff1a;应用地址&#xff0c;会被自动补全为http://localhost:3000/index.htmlbaseroute(可选)&…

react+AntDesign 之 pc端项目案例

1.环境搭建以及初始化目录 CRA是一个底层基于webpack快速创建React项目的脚手架工具 # 使用npx创建项目 npx create-react-app react-jike# 进入到项 cd react-jike# 启动项目 npm start2.安装SCSS SASS 是一种预编译的 CSS&#xff0c;支持一些比较高级的语法&#xff0c;…

3D点云上的深度学习综述

1 Title Deep Learning for 3D Point Clouds: A Survey&#xff08;Yulan Guo; Hanyun Wang; Qingyong Hu; Hao Liu; Li Liu; Mohammed Bennamoun&#xff09;【IEEE Transactions on Pattern Analysis and Machine Intelligence 2020】 2 Conclusion Deep learning on point…

Android开发编程从入门到精通,安卓技术从初级到高级全套教学

一、教程描述 本套教程基于JDK1.8版本&#xff0c;教学内容主要有&#xff0c;1、环境搭建&#xff0c;UI布局&#xff0c;基础UI组件&#xff0c;高级UI组件&#xff0c;通知&#xff0c;自定义组件&#xff0c;样式主题&#xff1b;2、四大组件&#xff0c;Intent&#xff0…

(九)One-Wire总线-DS18B20

文章目录 One-Wire总线篇复位和应答读/写0&#xff0c;1 DS18B20篇原理图概述最主要特性几个重要的寄存器&#xff08;部分要掌握&#xff09;存储有数字温度结果的2个字节宽度的温度寄存器寄存器描述&#xff1a;寄存器说明&#xff1a; 一个字节的过温和一个字节的低温&#…

msckf_vio在ubuntu20.04中的编译

1.新建catkin workspace文件夹&#xff0c;并在其中新建src文件夹&#xff0c;并将源码clone至src内。 源码地址&#xff1a;https://github.com/KumarRobotics/msckf_vio 目录层级示意如下&#xff0c;build和devel不必新建&#xff0c;后续指令会自动新建。 2. 在编译之前…

Python 面向对象之反射

Python 面向对象之反射 【一】概念 反射是指通过对象的属性名或者方法名来获取对象的属性或调用方法的能力反射还指的是在程序额运行过程中可以动态获取对象的信息(属性和方法) 【二】四个内置函数 又叫做反射函数 万物皆对象&#xff08;整数、字符串、函数、模块、类等等…

第02章_变量与进制

第02章_变量与进制 讲师&#xff1a;尚硅谷-宋红康&#xff08;江湖人称&#xff1a;康师傅&#xff09; 官网&#xff1a;http://www.atguigu.com 本章专题脉络 1、关键字(keyword) 定义&#xff1a;被C语言赋予了特殊含义&#xff0c;用做专门用途的字符串&#xff08;或单…

力扣:438. 找到字符串中所有字母异位词 题解

Problem: 438. 找到字符串中所有字母异位词 438. 找到字符串中所有字母异位词 预备知识解题思路复杂度Code其它细节推荐博客或题目博客题目滑动窗口哈希表 预备知识 此题用到了双指针算法中的滑动窗口思想&#xff0c;以及哈希表的运用。c中是unordered_map。如果对此不了解的u…

二、UI文件设计与运行机制

一、UI文件设计与运行机制 1、创建工程 2、添加控件&#xff0c;实现按钮点击 &#xff08;1&#xff09;添加控件 &#xff08;2&#xff09;添加信号和槽 2、分析项目结构 test_02test_02.pro Qt工程文件Headerswidget.h 设计的窗体类的头文件Sourcesmain.cpp 主程序入…

rk3588中编译带有ffmpeg的opencv

有朋友有工程需要&#xff0c;将视频写成mp4&#xff0c;当然最简单的方法当然是使用opencv的命令 cv::VideoWriter writer;bool bRet writer.open("./out.mp4", cv::VideoWriter::fourcc(m, p, 4, v), 15, cv::Size(640, 512), 1); 但是奈何很难编译成功&#xff…

Python | 基于Mediapipe框架的手势识别系统

一、项目要求 1、题目 本题着力于解决会商演示系统中的非接触式人机交互问题&#xff0c;具体而言&#xff0c;其核心问题就是通过计算机视觉技术实现对基于视频流的手势动作进行实时检测和识别。通过摄像头采集并识别控制者连续的手势动作&#xff0c;完成包括点击、平移、缩放…

离散数学1

注&#xff1a;线性代数已经更新了最大部分的内容&#xff0c;因此过段时间再补充剩余内容。 小王能歌善舞。因此&#xff0c;小王必须得会唱歌也必须得会跳舞&#xff0c;才满足题意 小王能唱歌或者小王能跳舞。因此&#xff0c;小王会唱歌也会跳舞满足。小王不会唱歌但会跳舞…

JavaScript常用事件演示

文章目录 一、在JavaScript中什么是事件&#xff1f;二、什么是JavaScript 常用事件&#xff1f;三、常用JS事件代码示例:四、事件总结 一、在JavaScript中什么是事件&#xff1f; JavaScript 使我们有能力创建动态页面。事件是可以被 JavaScript 侦测到的行为。 网页中的每个…

C++|【34】C++中的const

文章目录 constconst最基本的用法const和指针const和类 const const最基本的用法 用于限定a的大小&#xff0c;使a变成一个恒定不变的值。 应用场景&#xff1a;比如设置一些和标准相关的值&#xff0c;如上下限等等。 const和指针 指针总是包含两部分信息&#xff0c;一个是…

如何信任机器学习模型的预测结果?

在本篇中&#xff0c;我将通过一个例子演示在 MATLAB 如何使用 LIME 进行复杂机器学习模型预测结果的解释。 我使用数据集 carbig&#xff08;MATLAB 自带的数据集&#xff09;训练一个回归模型&#xff0c;用于预测汽车的燃油效率。数据集 carbig 是 70 年代到 80 年代生产的汽…