Java并发编程(三)线程同步 上[synchronized/volatile]

news2024/11/22 20:36:14

概念

当使用多个线程来访问同一个数据时,将会导致数据不准确,相互之间产生冲突,非常容易出现线程安全问题,比如多个线程都在操作同一数据,都打算修改商品库存,这样就会导致数据不一致的问题。
所以我们通过线程同步机制来保证线程安全,加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。线程同步本质就是“排队“,多个线程之间要排队,然后一个一个对共享资源进行操作,而不是同时进行操作,从而保证线程安全(即保证原子性、可见性、有序性)

概述

在Java多线程环境下我们通过锁这种方式来保证共享资源的正确、线程安全,即在线程操作某个共享资源之前先对资源加锁,保证操作期间没有其他线程访问资源,当操作完成后再对共享资源释放锁供其他线程访问

  • Java中锁是一种同步机制,用于控制多个线程对共享资源的访问。
  • 锁可以防止多个线程同时对同一个共享资源进行写操作,从而避免数据的不一致性和错误。
  • 锁是一种互斥工具,它能够确保同一时间只有一个线程可以访问共享资源
  • Java中的锁可以用来保护代码块、对象、方法、类等各种粒度的共享资源。
  • 通过锁可以让多个线程按照特定的顺序访问共享资源,从而避免死锁、竞争条件等并发问题
  • Java中常用的锁有synchronized关键字、ReentrantLock、ReadWriteLock、Semaphore等,这些锁提供了不同的功能和性能特征

分类

从并发的角度可将线程安全策略分为三种(我们日常开发主要涉及到前两种)

  • 第一种是悲观锁,核心是互斥同步(synchronized,Lock体系)
  • 第二种是乐观锁,核心是非阻塞同步,通过CAS进行原子类操作,即不加锁(底层为volatile+CAS)
  • 第三种是无同步方案,包括可重入代码和线程本地存储

常用锁介绍

  • 重入锁(ReentrantLock):可重入锁是一种可多次获取的锁,它允许一个线程在获得锁的同时再次获取锁。它提供了与synchronized关键字相同的互斥访问控制,但具有更大的灵活性和更强的功能
  • 读写锁(ReadWriteLock):读写锁是一种特殊类型的锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。在读多写少的情况下,读写锁可以提高程序的并发性能
  • 公平锁(FairLock):公平锁保证线程获取锁的顺序与线程请求锁的顺序相同。如果存在一个等待队列,那么等待时间最长的线程将获得锁
  • 互斥锁(Mutex):互斥锁是一种最简单的锁,它通过对共享资源加锁来确保同一时间只有一个线程可以访问该资源
  • 信号量(Semaphore):信号量是一种同步工具,它可以用来控制对共享资源的访问。它允许多个线程同时访问共享资源,但限制了同时访问该资源的线程数量
  • 偏向锁(Biased Locking):偏向锁是一种优化手段,它可以减少多线程环境下锁的竞争。它的基本思想是在没有竞争的情况下将锁偏向于第一个获取锁的线程,从而避免其他线程竞争锁

应用场景

多线程锁是一种用于在多线程编程中保护共享资源的同步机制。如下是适合使用多线程锁的场景:

  • 数据库访问:多个线程同时访问数据库可能导致数据一致性问题,使用锁可以保证数据的完整性和正确性
  • 文件读写:多个线程同时读写同一个文件可能会导致文件损坏或者数据丢失,使用锁可以保证文件的完整性和正确性
  • 共享内存:多个线程访问同一块共享内存时,使用锁可以保证每个线程都能正确读取或写入共享内存的数据
  • 队列操作:多个线程同时对队列进行操作可能会导致数据错乱或者数据丢失,使用锁可以保证队列的操作顺序和数据的正确性
  • 网络通信:多个线程同时进行网络通信时,使用锁可以保证数据传输的完整性和正确性

注意: 过多的锁使用会降低程序的性能。在使用锁的时候应该注意权衡锁的粒度和性能的需求

同步机制

Synchronized

概述

synchronized是Java中的关键字,也是一种同步的悲观锁;它修饰的对象有以下几种:

  • 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
  • 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  • 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
  • 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象

实现原理

synchronized是基于Java对象头中的标志位实现的,其中在Java对象头中有两个标志位用于存储synchronized锁的信息:

  • 一个是表示当前对象是否被锁定的标志位
  • 一个是表示持有锁的线程的标识符

执行过程描述

  • 当一个线程尝试获得一个被synchronized锁保护的资源时(即执行到monitorenter指令时),JVM会首先检查该对象的锁标志位,如果锁标志位为0表示该对象没有被锁定,JVM会将锁标志位设置为1,并将持有锁的线程标识符设置为当前线程的标识符。如果锁标志位为1表示该对象已经被其他线程锁定,当前线程会进入阻塞状态,等待其他线程释放锁;
  • 当一个线程释放一个被synchronized锁保护的资源时(即执行到monitorexit指令时),JVM会将锁标志位设置为0并且清空线程id释放该对象,同时JVM会唤醒等待该对象锁的其他线程,使它们可以继续竞争锁

monitor指令

通过反编译字节码文件后发现synchronized底层借助monitor指令实现同步,;monitor指令包括monitorenter和monitorexit可以理解为代码开始同步/开始加锁和结束同步/结束加锁;

  • monitorenter指令进行加锁: 进入同步代码后,每次进行操作前后,都需要获取最新的数据,执行完毕,及时写回主内存
  • monitorexit指令进行释放锁: 设置对象的锁标志为0,线程id清空,唤醒等待该对象锁的其他线程,使它们可以继续竞争锁
获取TestMultiThread单例对象(使用了DCL)
javap -v .\TestMultiThread.class( 对编译后的类进行反编译)

注意monitorexit指令为何出现2次?

  • 第一个monitorexit指令是同步代码块正常释放锁的一个标志
  • 如果同步代码块中出现Exception或者Error,则会调用第二个monitorexit指令来保证释放锁

锁优化

概述

JDK5升级到JDK6后一项重要的改进项,HotSpot虚拟机开发团队花费了大量的资源去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)等,这些技术都是为了在线程之间更高效的共享数据及解决竞争问题,从而提高程序的执行效率。

锁粗化

假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部

锁消除

锁消除即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。

  • 比如StringBuffer.append()方法使用了synchronized关键字来进行线程安全的保护.但若仅在线程内部把StringBuffer的对象当作一个局部变量来使用,其实就不会发生所谓的线程不安全的情况.此时Java以Server模式启动的,且已经开启了逃逸分析的配置,那么编译器就会将这段代码优化, 锁消除

偏向锁和轻量级锁

  • 偏向锁:在无竞争的情况下把整个同步都消除掉,也无CAS操作。简单的讲,就是在锁对象的对象头中有个ThreadId字段,这个字段如果是空的,第一次获取锁时将自身的ThreadId写入到锁的ThreadId字段内,将锁头内的是否偏向锁的状态置为1(上面的标识位),此后获取锁时直接检查ThreadId是否和自身线程Id一致,若一致则认为当前线程已经获取了锁。但当锁有竞争关系的时候,需要解除偏向锁,使锁进入竞争的状态(目前JDK偏向锁默认是开启的
  • 轻量级锁:在无竞争的情况下使用CAS操作对象头,将替换线程ID和指向锁记录的指针。成功则获得锁,失败则自旋等待获得锁。机制:每个锁都关联一个请求计数器和一个占有他的线程,当请求计数器为0时,这个锁可以被认为是unhled的,当一个线程请求一个unheld的锁时,JVM记录锁的拥有者,并把锁的请求计数加1,如果同一个线程再次请求这个锁时,请求计数器就会增加,当该线程退出syncronized块时,计数器减1,当计数器为0时锁被释放(这就保证了锁是可重入的,不会发生死锁的情况)

锁升级

概述

JDK1.6之后对synchronized进行了性能上的优化,引入了轻量级锁和偏向锁来减少性能消耗,所以不完全认为它是一个重量级锁,锁升级的过程是由JVM自动完成,JVM会根据同步竞争的情况来自动选择合适的锁级别,以提供更好的性能和效率。JDK1.6中锁有四种状态,分别是无锁、轻量级锁(自旋)、偏向锁、重量级锁升级过程从偏向锁->轻量级锁->重量级锁,而且锁升级之后不可降级

锁升级过程

当第一个线程访问同步块时,JVM将该线程ID记录在对象头部,并将对象的标记状态设置为偏向锁(偏向锁发生于同一时刻只有一个线程竞争锁的场景)。若有多个线程同时竞争锁,则偏向锁会升级为轻量级锁。如果线程的 CAS 自旋操作达到一定次数仍未竞争到锁,则轻量级锁会升级为重量级锁

  • 初始状态:对象没有锁标记,即为无锁状态
  • 偏向锁申请:当第一个线程访问同步块时,JVM将该线程ID记录在对象头部,并将对象的标记状态设置为偏向锁
  • 偏向锁撤销:当其他线程尝试获取锁时,发现对象的偏向锁被占用,会撤销偏向锁,升级为轻量级锁
  • 轻量级锁(Lightweight Locking): 轻量级锁是指当多个线程轻度竞争同步块时,JVM会将对象的锁记录存储在线程的栈帧中,而不是在对象头中。线程在进入同步块之前,通过CAS(比较并交换)自旋操作尝试获取锁。如果CAS自旋操作成功则表示获取锁成功,进入同步块,则当前锁仍然处于轻量级锁状态;如果CAS失败表示存在竞争,升级为重量级锁
  • 重量级锁是指当多个线程激烈竞争同步块时,JVM会将对象的锁升级为重量级锁,使用操作系统提供的互斥量来实现锁机制。重量级锁涉及到线程的阻塞和唤醒操作,开销较大

volatile

概述

volitate是JVM提供的轻量级同步机制关键字。volatile相比于synchronized(重量级锁),它属于是Java提供的一种轻量级的同步机制,因为它不会引起线程上下文的切换和调度;但无法保证线程安全

特点

  • 保证可见性(缓存一致性原理)
    • 当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去
    • 这个写会操作会导致其他线程中的volatile变量缓存无效
  • 保证有序性
    • 通过内存屏障相关指令(lock指令)禁止指令重排实现有序性(重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段,在单线程下一定能保证结果的正确性,但在多线程环境下结果不一定正确)
    • 内存屏障作用
      • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
      • 会强制将对缓存(线程中私有的工作内存)的修改操作立即写入主存(堆内存)
      • 如果是写操作,它会导致其他线程中对应的缓存行无效
  • 无法保证原子性
    • volatile不适合复合操作(如volatile++),就是因为无法保证原子性

常见使用场景

  • 状态量标记,如:volatile bool flag = false;对变量的读写操作,标记为volatile可以保证变量的修改对线程立刻可见,比synchronized,Lock实现有一定的效率提升
  • 单例模式中通过使用典型的双重检查锁定(DCL)保证线程安全示例
//懒汉单例模式
class Singleton{
    private volatile static Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) { //将同步的粒度降到方法内部,提高了程序的性能
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

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

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

相关文章

C++文件类(整理自C语言中文网-全)

C文件类(文件流类)及用法详解 《C输入输出流》一章中讲过,重定向后的 cin 和 cout 可分别用于读取文件中的数据和向文件中写入数据。除此之外,C 标准库中还专门提供了 3 个类用于实现文件操作,它们统称为文件流类&…

2.0 Maven基础

1. Maven概述 Maven概念 Apache Maven是一个软件项目管理工具,将项目开发和管理过程抽象程一个项目对象模型(POM,Project Object Model)。 Maven作用 项目构建 提供标准的、跨平台的自动化项目构建方式。 依赖管理 方便快捷…

【Java】数据交换 Json 和 异步请求 Ajax

🎄欢迎来到边境矢梦的csdn博文,本文主要讲解Java 中 数据交换和异步请求 Json&Ajax 的相关知识🎄 🌈我是边境矢梦,一个正在为秋招和算法竞赛做准备的学生🌈 🎆喜欢的朋友可以关注一下&#…

【枚举】CF1660 D

Problem - 1660D - Codeforces 题意: 思路: 思路巨简单,代码也wa了很多发才过,都是因为细节.... 很显然,要根据0分段处理 对于每一段,枚举去掉左边段还是右边段,左边段是 l 到第一个负数&am…

Blazor 简单组件(0):简单介绍

文章目录 前言说明环境安装 前言 Blazor 这个技术还是比较新,相关的UI组件还在完善,我这里提供一下我个人的组件开发。 说明 本UI组件是基于BootstrapBlazor(以下简称BB)开发。 BootstrapBlazor 文档 环境安装 C#小轮子:Visual Studio自…

Vue过滤器(时间戳转时间)

目录 过滤器 HTML写法: 定义过滤器: 定义全局过滤器: 过滤器串联: 带参数过滤器: 时间戳转时间 过滤器 官方地址:过滤器 — Vue.js (vuejs.org) 过滤器是指Vue.js支持在{{}}插值的尾部添加一个管道符“&#xff0…

【腾讯云 Cloud Studio 实战训练营】使用Cloud Studio构建Java、Python项目

文章目录 一、云IDE1、云IDE简介2、云IDE和云虚拟桌面区别 二、Cloud Studio 简介1、简介2、AI代码助手3、企业源代码安全 三、快速开始1、登录Cloud Studio2、新建工作空间3、代码空间 四、项目构建1、构建Java项目1.1 新建工作空间1.2 初始化项目1.3 初始化小案例1.4、测试Ja…

(贪心) 剑指 Offer 14- II. 剪绳子 II ——【Leetcode每日一题】

❓剑指 Offer 14- II. 剪绳子 II 难度:中等 给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n 都是整数,n > 1 并且 m>1 ),每段绳子的长度记为 k[0],k[1]...k[m - 1] 。请问 k[0]*k[1]*.…

【云原生】Docker 详解(三):Docker 镜像管理基础

Docker 详解(三):Docker 镜像管理基础 1.镜像的概念 镜像可以理解为应用程序的集装箱,而 Docker 用来装卸集装箱。 Docker 镜像含有启动容器所需要的文件系统及其内容,因此,其用于创建并启动容器。 Dock…

搭建Docker环境

目录 一、docker环境搭建 1、卸载旧版本docker 2、安装依赖和设置仓库 3、安装docker 4、启动并加入开机启动 5、验证是否安装成功 二、利用docker搭建nginx 1、拉取镜像 2、启动容器,部署nginx 一、docker环境搭建 1、卸载旧版本docker yum remove docke…

【Image captioning】ruotianluo/self-critical.pytorch之1—数据集的加载与使用

【Image captioning】ruotianluo/self-critical.pytorch之1—数据集的加载与使用 作者:安静到无声 个人主页 数据加载程序示意图 使用方法 示例代码 #%%from __future__ import absolute_import from __future__ import division from __future__ import print_…

Python(七十九)字符串的常用操作——字符串内容对齐操作的方法

❤️ 专栏简介:本专栏记录了我个人从零开始学习Python编程的过程。在这个专栏中,我将分享我在学习Python的过程中的学习笔记、学习路线以及各个知识点。 ☀️ 专栏适用人群 :本专栏适用于希望学习Python编程的初学者和有一定编程基础的人。无…

间歇性和连续性静息态功能磁共振成像的异同

摘要 功能磁共振成像(fMRI) block设计实验通常包括呈现认知任务的ON-block和无认知任务的OFF-block。然而,ON-block之间的OFF-block也可以看作间歇性休息的标志,从而诱发短暂的静息状态。目前仍然不知道这种间歇期的大脑活动是否反映了与连续期相同的静…

阻塞队列_线程安全版本_生产消费者模型

前言 在前面PriorityQueue优先级队列_Y君的进化史的博客-CSDN博客,我们学习了优先级队列,但是发现,当一个线程将优先级队列使用完之后,会自动退出程序,如果此时我们想使其一直等待到下一个任务的录入,就需…

nodejs+vue+elementui学生档案信息管理系统_06bg9

利用计算机网络的便利,开发一套基于nodejs的大学生信息管理系统,将会给人们的生活带来更多的便利,而且在经济效益上,也会有很大的便利!这可以节省大量的时间和金钱。学生信息管理系统是学校不可缺少的一个环节,其内容直…

案例12 Spring MVC入门案例

网页输入http://localhost:8080/hello&#xff0c;浏览器展示“Hello Spring MVC”。 1. 创建项目 选择Maven快速构建web项目&#xff0c;项目名称为case12-springmvc01。 2.配置Maven依赖 <?xml version"1.0" encoding"UTF-8"?><project xm…

Hazel 引擎学习笔记

目录 Hazel 引擎学习笔记学习方法思考引擎结构创建工程程序入口点日志系统Premake\MD没有 cpp 文件的项目会出错include 到某个库就要包含这个库的路径&#xff0c;注意头文件展开 事件系统 获取和利用派生类信息预编译头文件抽象窗口类和 GLFWgit submodule addpremake 脚本禁…

Labview控制APx(Audio Precision)进行测试测量(七)

处理集群控制子集 大多数用户不会想要设置所有的控制包括在一个大的控制集群&#xff0c;如水平和增益配置控制。例如&#xff0c;假设您只在 APx 中使用模拟不平衡输出连接器&#xff0c;而您想要做的就是控制发电机的电平和频率。在这种情况下&#xff0c;水平和增益配置集群…

【Redis】Redis内存过期策略和内存淘汰策略

【Redis】Redis内存过期策略和内存淘汰策略 文章目录 【Redis】Redis内存过期策略和内存淘汰策略1. 过期策略1.1 惰性删除1.2 周期删除1.2.1 SLOW模式1.2.2 FAST模式 2. 淘汰策略 1. 过期策略 Redis本身是一个典型的key-value内存存储数据库&#xff0c;因此所有的key、value都…