当前位置: 首页 > news >正文

【多线程二】——线程安全

Java并发编程核心机制深度解析

Java并发编程是构建高性能、高吞吐量应用的核心,但其复杂性也带来了诸多挑战。理解其底层机制是编写正确、高效并发程序的关键。本文将深入探讨synchronized、JMM、CAS、volatile、AQS、Lock、死锁以及ConcurrentHashMap等核心概念。

一、synchronized关键字的底层原理

synchronized是Java中最基本的互斥同步手段。它的底层原理涉及Java对象头Monitor(监视器)

  1. 实现方式

    • 同步代码块:使用monitorentermonitorenter指令实现。当线程执行到monitorenter时,会尝试获取对象的Monitor所有权。获取成功则锁计数器+1;执行到monitorexit时,计数器-1。计数器为0时,锁被释放。
    • 同步方法:方法常量池中有一个ACC_SYNCHRONIZED标志。当线程调用方法时,会检查此标志,然后自动先获取对象的Monitor,方法执行完后再释放。
  2. 对象头与Mark Word

    • Java对象在内存中分为三部分:对象头实例数据对齐填充
    • 对象头包含两部分:Mark Word和类型指针。
    • Mark Word 用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志持有锁的线程ID偏向时间戳等。它是synchronized实现锁的关键。
  3. 锁升级优化(非常重要): 为了减少获得锁和释放锁带来的性能消耗,JDK 1.6后引入了“偏向锁”、“轻量级锁”等优化,锁状态会随着竞争情况逐步升级。锁只能升级,不能降级

    • 无锁状态:初始状态。
    • 偏向锁:一段同步代码一直被一个线程访问,那么该线程会自动获取锁,降低获取锁的代价。只需在Mark Word中存储锁偏向的线程ID。
    • 轻量级锁:当有另一个线程来竞争偏向锁时,偏向锁会升级为轻量级锁。竞争线程会通过CAS自旋(循环尝试)来获取锁,不会阻塞,适用于追求响应时间、锁占用时间很短的场景。
    • 重量级锁:轻量级锁自旋到一定次数(或一个线程在自旋,另一个线程又来竞争),会升级为重量级锁。此时,竞争失败的线程会陷入内核态,被挂起,等待操作系统调度,性能损耗最大。

小结:

二、JMM(Java内存模型)

JMM(Java Memory Model)是一个抽象概念,它定义了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量的底层细节。

  1. 核心目标:解决由于多线程通过共享内存进行通信时,存在的原子性、可见性和有序性问题。

  2. 主内存与工作内存

    • 主内存:所有共享变量都存储在主内存中。
    • 工作内存:每个线程都有自己的工作内存,其中保存了该线程使用到的变量的主内存副本拷贝。
    • 线程对变量的所有操作(读、写)都必须在工作内存中进行,而不能直接读写主内存中的变量。
    • 不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
  3. JMM与硬件内存结构: JMM是逻辑上的规范,它屏蔽了底层不同硬件内存结构的差异(如多核CPU的多级缓存)。工作内存可以粗略理解为CPU的寄存器和高速缓存(L1, L2)。

  4. Happens-Before原则: 这是JMM的核心内容,用于判断数据是否存在竞争,线程是否安全。它规定了如果一个操作happens-before于另一个操作,那么第一个操作的结果对第二个操作一定是可见的。 常见的规则包括:

    • 程序次序规则:一个线程内,书写在前面的操作happens-before于后面的操作。
    • 监视器锁规则:对一个锁的解锁happens-before于随后对这个锁的加锁。
    • volatile变量规则:对一个volatile域的写happens-before于任意后续对这个volatile域的读。
    • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

三、对CAS的理解

CAS(Compare-And-Swap),即比较并交换,是一种无锁的原子操作算法。

  1. 操作过程:它包含三个操作数——内存位置(V)、预期原值(A)和新值(B)

    • 当且仅当V的值等于A时,处理器才会用B更新V的值。
    • 否则,它不会执行任何操作(或者返回当前V的值)。
    • 整个操作是一个原子操作
  2. 底层实现:CAS是CPU指令级的操作,现代CPU大都支持。在Java中,通过Unsafe类的本地方法(如compareAndSwapInt)调用CPU指令实现。

  3. 应用java.util.concurrent包中的原子类(如AtomicInteger)大量使用了CAS操作来实现并发安全。

  4. 经典问题——ABA问题

    • 描述:一个线程将变量V从A改为B,然后又改回A。另一个线程看到V的值还是A,就误以为它没有被修改过,从而进行了CAS操作。
    • 解决方案:使用AtomicStampedReferenceAtomicMarkableReference,它们不仅比较值,还会比较一个版本号(Stamp)或标记,从而避免ABA问题。

小结:

四、对 volatile 的理解

代码执行结果:

原因:

1. 可见性
  • 问题:根据JMM,普通变量被修改后,不会立即写回主内存,其他线程的工作内存中的副本也就不会立即失效,从而导致其他线程读取到的可能是旧的脏数据。
  • volatile的作用:当一个变量被声明为volatile后:
    • 写操作:对volatile变量的写操作会立即刷新到主内存。
    • 读操作:对volatile变量的读操作会使当前线程的工作内存中该变量的副本失效,从而必须从主内存中重新读取最新值。
  • 底层实现:通过在指令序列中插入内存屏障(Memory Barrier)来禁止特定类型的处理器重排序,并强制刷新CPU缓存。
2. 禁止指令重排序

测试:

@JCStressTest:多线程测试提供的测试框架

{"0,0","1,1","0,1"}   desc="ACCRPTABLE":设定x,y正常的情况

“1,0” desc="INTERSTING":属于不正常的情况

启动测试:

如果不能运行,就先在maven里面clean一下,再package后再运行

然后就可以在results文件夹下的html查看测试信息

再给y加上volatile测试

volatile应该修饰哪个变量:

  • 问题:为了提高性能,编译器和处理器常常会对指令进行重排序。在单线程下,这不会影响结果,但在多线程下,可能导致出乎意料的结果。
  • volatile的作用:通过添加内存屏障,volatile实现了禁止指令重排序优化
    • 当程序执行到volatile变量的读/写操作时,其前面的所有操作肯定已经完成,且结果对后续操作可见
    • 在进行指令优化时,不能把volatile变量前面的语句放在其后面执行,也不能把后面的语句放到其前面执行。
  • 经典应用——单例模式的双重检查锁(DCL)
    public class Singleton {private static volatile Singleton instance;public static Singleton getInstance() {if (instance == null) {                 // 第一次检查synchronized (Singleton.class) {    // 加锁if (instance == null) {         // 第二次检查instance = new Singleton(); // 关键:volatile防止这里的指令重排序}}}return instance;}}

如果没有`volatile`,`new Singleton()`这一行代码可能被重排序(先分配内存地址,后初始化对象),导致其他线程拿到一个未完全初始化的对象。`volatile`禁止了这种重排序,保证了线程安全。

五、什么是AQS

AQS(AbstractQueuedSynchronizer),即抽象队列同步器,是JUC并发包的核心基础组件。

  1. 作用:它用一个int类型的volatile变量(state 来表示同步状态,并提供了一个基于FIFO的等待队列(CLH队列的变种),用于构建锁和其他同步组件(如ReentrantLockCountDownLatchSemaphore等)。

  2. 核心思想

    • 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态(state=1)。
    • 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制。AQS使用CLH队列锁来实现,将暂时获取不到锁的线程加入到队列中。
  3. 工作方式:AQS定义了两种资源共享方式:

    • 独占(Exclusive):只有一个线程能执行,如ReentrantLock
    • 共享(Share):多个线程可同时执行,如Semaphore/CountDownLatch

    自定义同步器只需要实现对state的获取和释放即可。

公平锁和非公平锁:

小结:

六、ReentrantLock的实现原理

ReentrantLock是基于AQS实现的一个可重入的互斥锁。

  1. 可重入性:线程可以再次获取自己已经持有的锁。state变量用来记录重入次数。线程第一次获取锁时,state变为1;再次获取时,state+1;释放时,state-1,直到state为0,锁才被完全释放,其他线程才能获取。

  2. 公平性与非公平性

    • 非公平锁(默认):当锁可用时,不管等待队列中是否有其他线程在等待,新来的线程都有机会直接竞争锁。吞吐量高,但可能产生线程饥饿。
    • 公平锁:锁被释放后,严格按照FIFO的顺序从等待队列中唤醒线程,先到先得。保证了公平性,但性能相对较低,因为要维护队列顺序。
  3. 实现ReentrantLock内部有一个继承自AQS的同步器(Sync),FairSyncNonfairSync分别实现了公平和非公平的获取锁逻辑。

小结:

七、synchronized和Lock有什么区别

特性synchronizedLock (如 ReentrantLock)
存在层次Java关键字,JVM原生支持接口,位于java.util.concurrent
锁的释放自动释放(代码块执行完或异常)手动调用unlock()释放,必须在finally中执行
锁的获取线程会一直阻塞等待,无法中断提供多种获取方式:tryLock()lockInterruptibly()
可中断不可中断可中断,等待锁的线程可以响应中断
公平锁非公平两者都可(构造函数指定)
绑定条件单一,随机唤醒或全部唤醒可关联多个条件Condition),实现分组唤醒
性能JDK 1.6后优化很好,大部分场景够用高竞争环境下表现更好,API更灵活

选择:优先使用synchronized,因其简洁可靠。只有在需要Lock提供的高级特性(如可中断、超时尝试、公平锁、多个条件变量)时,才使用Lock

1.可打断

2、可超时

3、多条件变量

八、死锁产生的条件以及死锁排查方案

  1. 死锁产生的四个必要条件(缺一不可)

    • 互斥条件:资源一次只能被一个线程占用。
    • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
    • 不剥夺条件:线程已获得的资源,在未使用完之前,不能被强行剥夺。
    • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
  2. 死锁排查方案

    • 命令行
      1. 使用jps命令查找当前Java进程的PID。
      2. 使用jstack <pid>命令打印线程堆栈信息。
    • 分析堆栈信息jstack工具能自动检测并报告死锁。在输出的最后,如果有Found one Java-level deadlock:字样,下面会详细列出哪些线程在等待哪些锁,形成了循环等待。
    • 图形化工具:使用JConsole、VisualVM等工具,它们也有检测死锁的功能。

小结:

九、聊一下ConcurrentHashMap

ConcurrentHashMap是JUC包中提供的线程安全的HashMap。

  1. JDK 1.7 vs JDK 1.8

    • JDK 1.7:采用分段锁(Segment)机制。容器里包含一个Segment数组,每个Segment就是一个锁,类似于一个HashMap。不同的Segment互不干扰。默认有16个Segment。
    • JDK 1.8及以后:摒弃了分段锁,改用**synchronized + CAS + volatile**的实现方式。
      • 锁的粒度更小:只锁住链表或红黑树的头节点(桶),并发度更高。
      • 使用synchronized:得益于JDK1.6对synchronized的优化,其性能已大幅提升。
      • 大量使用CAS:用于无竞争条件下的put操作,避免加锁开销。
      • 数据结构:数组+链表+红黑树,和HashMap一样。
  2. 为什么高效

    • 读操作通常无锁(通过volatile保证可见性)。
    • 写操作只在冲突时加锁,且锁粒度非常小(一个桶)。
    • 通过CAS进行无锁化竞争,降低了线程上下文切换的开销。

十、导致并发程序出现问题的根本原因是什么

并发程序问题的根源可以归结为JMM试图解决的三大特性被破坏:

  1. 可见性:一个线程对共享变量的修改,另一个线程不能立即看到

    • 根源:CPU的多级缓存机制以及线程工作内存与主内存的同步延迟。
  2. 原子性:一个或多个操作,要么全部执行成功,要么全部不执行,不会被打断。

    • 根源:操作系统线程调度(时间片轮转),高级编程语言中的一句代码可能对应多条CPU指令,在执行过程中可能被中断。
  3. 有序性:程序执行的顺序不一定按照代码的先后顺序执行。

    • 根源:编译器和处理器为了优化性能,会对指令进行重排序(Instruction Reorder)。

总结:并发编程的所有技术和工具(锁、volatile、原子变量、AQS等),其最终目的都是为了解决可见性、原子性和有序性这三大问题,从而保证线程安全。

http://www.dtcms.com/a/435821.html

相关文章:

  • 网站建设属什么费用建网站莱阳哪家强?
  • 织梦建站系统教程网上房地产备案查询
  • 企业网站视频栏目建设方案汽车网站建设预算
  • 《嵌入式 – GD32开发实战指南(RISC-V版本)》第6章 按键
  • 《嵌入式 – GD32开发实战指南(RISC-V版本)》第4章 GD32VF103启动流程详解
  • 公司静态网站模板东乌珠穆沁旗网站建设
  • 网站怎么在微博推广个人网站 备案 攻略
  • 建设银行如何设置网站查询密码泰州网站建设策划
  • 基于django的电子商务网站开发山东城乡和住房建设厅官网
  • 无锡市网站搭建营销网站开发规划
  • 网站后台修改内容看不见了做移动端活动页面参考网站
  • 贵阳建立网站领动做的企业网站怎么样
  • VirtualBox 7.2.2安装踩坑记录
  • 重庆市工程建设信息西安seo盐城
  • 【Linux】Linux调试器----gdb/cgdb
  • 天津搜索引擎推广网站优化设计方案
  • 西安网站建设开发查派宜昌市住房和城乡建设局网站
  • 德州企业网站建设要素wordpress 纯代码 雪花
  • 柳州网站建设哪家好硬件开发是什么意思
  • linux学习笔记 (10) 进程的内存管理
  • Java学习笔记Day13
  • .net网站设计企业信息公示网站
  • 网站后台建设招聘中山专业网站制作
  • 怎么免费建设自己网站什么网站可以分享wordpress
  • 做影片的网站描述网络平台建设公司排名
  • 有阿里云主机管理平台如何自己做网站自己制作头像的网站 设计 动漫
  • 滨州的网站建设深圳推广软件十年乐云seo
  • .ent做的网站有哪些安卓市场下载app
  • 免费发外链的网站工会网站建设比较好的工会
  • 网站开发完整视频做平面设计的网站