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

这篇具有很好参考价值的文章主要介绍了Java并发编程(三)线程同步 上[synchronized/volatile]。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

概念

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

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

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

概述

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

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

分类

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

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

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

常用锁介绍

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

应用场景

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

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

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

同步机制

Synchronized

概述

  • synchronized是Java中的关键字,是一种同步的悲观锁
  • 常用来修饰的对象有以下几种:
    • 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
    • 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
    • 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
    • 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象
  • 通过synchronized+wait+notify实现生产者-消费者问题(在后文并发实践篇会有相关示例)

实现原理

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清空,唤醒等待该对象锁的其他线程,使它们可以继续竞争锁
Java并发编程(三)线程同步 上[synchronized/volatile],# Java,java
获取TestMultiThread单例对象(使用了DCL)
Java并发编程(三)线程同步 上[synchronized/volatile],# Java,java
javap -v .\TestMultiThread.class( 对编译后的类进行反编译)

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

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

锁优化

概述

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

锁粗化

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

假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果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 自旋操作达到一定次数仍未竞争到锁,则轻量级锁会升级为重量级锁文章来源地址https://www.toymoban.com/news/detail-643175.html

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

volatile

概述

  • 相比于synchronized(重量级锁),volitate是JVM提供的轻量级同步机制关键字,因为它不会引起线程上下文的切换和调度
  • 无法保证线程安全,因为他不具备“互斥性”,不能保证变量的原子性

特点

  • 保证可见性(缓存一致性原理)
    • 当写一个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;
    }
}

到了这里,关于Java并发编程(三)线程同步 上[synchronized/volatile]的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Java 并发编程】一文详解 Java 内置锁 synchronized

    存在共享数据; 多线程共同操作共享数。 synchronized 可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时 synchronized 可以保证一个线程的变化可见(可见性),即可以代替 volatile。 多线程编程中,有可能会出现多个线程同时访问同一个共享、可变

    2024年02月02日
    浏览(35)
  • 【并发编程】深入理解Java并发之synchronized实现原理

    分析: 通过 new MyThread() 创建了一个对象 myThread ,这时候堆中就存在了共享资源 myThread ,然后对 myThread 对象创建两个线程,那么thread1线程和thread2线程就会共享 myThread 。 thread1.start() 和 thead2.start() 开启了两个线程,CPU会随机调度这两个线程。假如 thread1 先获得 synchronized 锁,

    2024年02月04日
    浏览(60)
  • 【Java练习题汇总】《第一行代码JAVA》多线程篇,汇总Java练习题——线程及多线程概念、Thread 类及 Runnable 接口、线程状态、synchronized同步操作...

    一、填空题 Java 多线程可以依靠________ 、________ 和________ 三种方式实现。 多个线程操作同一资源的时候需要注意________,依靠________ 实现,实现手段是:________ 和________,过多的使用,则会出现________ 问题。 Java 程序运行时,至少启动________ 个线程,分别是________ 和_

    2024年02月16日
    浏览(58)
  • JUC并发编程-线程和进程、Synchronized 和 Lock、生产者和消费者问题

    源码 + 官方文档 面试高频问! java.util 工具包、包、分类 业务:普通的线程代码 Thread Runnable Runnable 没有返回值、效率相比入 Callable 相对较低! 线程、进程,如果不能使用一句话说出来的技术,不扎实! 进程:一个程序,QQ.exe Music.exe 程序的集合; 一个进程往往可以包含多

    2024年01月20日
    浏览(50)
  • Java并发编程面试题——线程池

    参考文章: 《Java 并发编程的艺术》 7000 字 + 24 张图带你彻底弄懂线程池 (1) 线程池 (ThreadPool) 是一种用于 管理和复用线程的机制 ,它是在程序启动时就预先创建一定数量的线程,将这些线程放入一个池中,并对它们进行有效的管理和复用,从而在需要执行任务时,可以从

    2024年02月07日
    浏览(51)
  • Java并发编程之线程池详解

    目录 🐳今日良言:不悲伤 不彷徨 有风听风 有雨看雨 🐇一、简介 🐇二、相关代码 🐼1.线程池代码 🐼2.自定义实现线程池 🐇三、ThreadPoolExecutor类 首先来介绍一下什么是线程池,线程池是一种利用池化技术思想来实现的线程管理技术,主要是为了复用线程、便利地管理线程

    2024年02月12日
    浏览(46)
  • 【Java 并发编程】Java 线程本地变量 ThreadLocal 详解

    先一起看一下 ThreadLocal 类的官方解释: 用大白话翻译过来,大体的意思是: ThreadLoal 提供给了 线程局部变量 。同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意: 因为每个 Thread 内有自己的实例副本,且 该副本只能由当前 Thread 使用 。

    2024年02月04日
    浏览(71)
  • 【Java 并发编程】一文读懂线程、协程、守护线程

    在 Java 线程的生命周期一文中提到了 就绪状态的线程在获得 CPU 时间片后变为运行中状态 ,否则就会在 可运行状态 或者 阻塞状态 ,那么系统是如何分配线程时间片以及实现线程的调度的呢?下面我们就来讲讲线程的调度策略。 线程调度是指系统为线程分配 CPU 执行时间片

    2023年04月08日
    浏览(60)
  • Java面试_并发编程_线程基础

    进程是正在运行程序的实例, 进程中包含了线程, 每个线程执行不同的任务 不同的进程使用不同的内存空间, 在当前进程下的所有线程可以共享内存空间 线程更轻量, 线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程) 并发是单个

    2024年02月07日
    浏览(53)
  • 【Java并发编程】变量的线程安全分析

    1.成员变量和静态变量是否线程安全? 如果他们没有共享,则线程安全 如果被共享: 只有读操作,则线程安全 有写操作,则这段代码是临界区,需要考虑线程安全 2.局部变量是否线程安全 局部变量是线程安全的 当局部变量引用的对象则未必 如果给i对象没有逃离方法的作用

    2024年02月08日
    浏览(56)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包