掌握线程安全及多线程问题是我们编写高性能代码的基础,下面将从理论到实践,一层一层的解开。

1. 什么是线程安全?

我们用《java concurrency in practice 》中的一句话来表述:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其它的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。从这句话中我们可以知道几层意思:

  1. 线程安全是和对象密切绑定的;
  2. 线程的安全性是由于线程调度和交替执行造成的;
  3. 线程安全的目的是实现正确的结果

2. 避免线程安全问题

由于 CPU 的执行速度和内存的存取速度严重不匹配,为了优化性能及充分利用运算能力,基于时间局部性、空间局部性等局部性原理,CPU 在和内存间增加了多层高速缓存,当需要取数据时,CPU 会先到高速缓存中查找对应的缓存是否存在,存在则直接返回,如果不存在则到内存中取出并保存在高速缓存中。

现在多核处理器越基本已经成为标配,这时每个处理器都有自己的缓存,这就带来了缓存一致性的问题:cpu 计算时数据读取顺序优先级:寄存器 -> 高速缓存 -> 内存,计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。

Java 内存模型规定所有的变量都存储在主内存 (Main Memory) 中。每条线程还有自己的工作内存 (Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作 (读取,赋值等) 都必须是工作内存中进行,而不能直接读写主内存中的变量。线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。当多个线程同时读写某个内存数据时,各个线程都从主内存中获取数据,线程之间数据是不可见的,就可能会产生寄存器 / 高速缓存 / 内存之间的同步问题。

如何破?

线程安全的前提是该变量是否被多个线程访问,只要有多于一个的线程操作给定的状态变量,此时就可能产生多线程问题。jvm 层面避免线程安全问题主要围绕着并发过程中的原子性、可见性、有序性这三个特征。

2.1 原子性

原子性就是操作不能被线程调度机制中断,要么全部执行完毕要么不执行。java 内存模型确保基本类型数据的访问大都是原子操作,即多个线程在并发访问的时候是线程非安全的。比如”a = 2”、 “return a;” 都具有原子性。但是类似”a += b”、”i++” 的操作不具有原子性。

注意:在 32 位平台下,对 64 位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性,这就导致了 long、double 类型的变量在 32 位虚拟机中是非原子操作。

可以使用 AtomicXXX、synchronized 和 Lock 保证原子性。synchronized 和 Lock 能够保证任一时刻只有一个线程执行该代码块,自然就不存在原子性问题了。

2.2 可见性

可见性就是一个线程对共享变量做了修改之后,其他的线程立即能够看到修改后的值。Java 内存模型将工作内存中的变量修改后的值同步到主内存,在读取变量前从主内存刷新最新值到工作内存中,这种依赖主内存的方式来实现可见性的。

Java 提供了 volatile 关键字来保证可见性。当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中,非 volatile 变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。而声明变量是 volatile 的,会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会跳过 CPU cache 这一步去内存中读取新值。volatile 只确保了可见性,并不能确保原子性

2.3 有序性

为了尽可能减少内存操作速度远慢于 CPU 运行速度所带来的 CPU 空置的影响,**编译器和处理器常常会对指令做重排序。**有序性:即程序执行的顺序按照代码的先后顺序执行。CPU 虽然并不保证完全按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。重排序过程不会影响到单线程程序的执行,但会影响到多线程并发执行的正确性。通过 volatile 关键字来保证一定的 “有序性”,volatile 关键字本身就包含了禁止指令重排序的语义。另外可以通过 synchronized 和 Lock 来保证有序性,synchronized 和 Lock 保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

3. synchronized 方案

1.synchronized 能够把任何一个非 null 对象当成锁,实现由两种方式:

  • 类锁,当 synchronized 作用于静态方法时是给 class 加锁
  • 对象锁,当 synchronized 作用于一个对象实例时或非静态方法时

2.synchronized 锁又称为对象监视器(object)。

  1. 当多个线程一起访问某个对象监视器的时候,对象监视器会将这些请求存储在不同的容器中。
  • Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中
  • Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中
  • Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里
  • OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck
  • Owner:当前已经获取到所资源的线程被称为 Owner
  • !Owner:当前释放锁的线程

synchronized 在 jdk1.6 之后提供了多种优化方案

1. 锁升级

jvm 自动进行偏向锁 -> 轻量级锁 -> 重量级锁升级的过程

2. 锁消除

即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,依据来源于逃逸分析的数据支持,那么是什么是逃逸分析?对于虚拟机来说需要使用数据流分析来确定是否消除变量底层框架的同步代码,因为有许多同步的代码不是自己写的。

public static String concatString(String s1, String s2, String s3) {  return s1 + s2 + s3;
}
/***由于 String 是一个不可变的类,对字符串的连接操作总是通过生成新的 String 对象来进行的,因此 Javac 编译器会对 String 连接做自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作,这里的stringBuilder.append是线程不同步的(假设是同步)*/
public static String concatString(String s1, String s2, String s3) {  StringBuffer sb = new StringBuffer();  sb.append(s1);  sb.append(s2);  sb.append(s3);  return sb.toString();
}

4. 锁粗化

将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。

4. lock 方案

与 synchronized 不同的是 lock 是纯 java 手写的,与底层的 JVM 无关。在 java.util.concurrent.locks 包中有很多 Lock 的实现类,常用的有 ReenTrantLock、ReadWriteLock(实现类有 ReenTrantReadWriteLock),其实现都依赖 AbstractQueuedSynchronizer 类(简称 AQS),实现思路都大同小异,因此我们以 ReentrantLock 作为讲解切入点。

AQS 是我们后面将要提到的 CountDownLatch/FutureTask/ReentrantLock/RenntrantReadWriteLock/Semaphore 的基础,因此 AQS 也是 Lock 和 Excutor 实现的基础。它的基本思想就是一个同步器,支持获取锁和释放锁两个操作。

要支持上面锁获取、释放锁就必须满足下面的条件:

  • 状态位必须是原子操作的
  • 阻塞和唤醒线程
  • 一个有序的队列,用于支持锁的公平性

场景:可定时的、可轮询的与可中断的锁获取操作,公平队列,或者非块结构的锁。
主要从以下几个特点介绍:

  1. 可重入锁,如果锁具备可重入性,则称作为可重入锁。像 synchronized 和 ReentrantLock 都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。
  2. 可中断锁,顾名思义,就是可以相应中断的锁。在 Java 中,synchronized 就不是可中断锁,而 Lock 是可中断锁。如果某一线程 A 正在执行锁中的代码,另一线程 B 正在等待获取该锁,可能由于等待时间过长,线程 B 不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
  3. 公平锁和非公平锁,公平锁以请求锁的顺序来获取锁,非公平锁则是无法保证按照请求的顺序执行。synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于 ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。参数为 true 时表示公平锁,不传或者 false 都是为非公平锁。
  4. 读写锁,读写锁将对一个资源(比如文件)的访问分成了 2 个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。ReadWriteLock 就是读写锁,它是一个接口,ReentrantReadWriteLock 实现了这个接口。可以通过 readLock() 获取读锁,通过 writeLock() 获取写锁。

5. lock 与 synchronized 区别

类别 synchronized Lock
存在层次 Java 的关键字,在 jvm 层面上 是一个类
锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm 会让线程释放锁 在 finally 中必须释放锁,不然容易造成线程死锁
锁的获取 假设 A 线程获得锁,B 线程等待。如果 A 线程阻塞,B 线程会一直等待 分情况而定,Lock 有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
锁状态 无法判断 可以判断
锁类型 可重入 不可中断 非公平 可重入 可判断 可公平(两者皆可)
性能 少量同步 大量同步

总结

  • 1.synchronized
    优点:实现简单,语义清晰,便于 JVM 堆栈跟踪,加锁解锁过程由 JVM 自动控制,提供了多种优化方案,使用更广泛
    缺点:不能进行高级功能
  • 2.lock
    优点:可定时的、可轮询的与可中断的锁获取操作,提供了读写锁、公平锁和非公平锁
    缺点:需手动释放锁 unlock,不适合 JVM 进行堆栈跟踪
    1. 相同点
      都是可重入锁

Java 内存模型如何保证多线程安全相关推荐

  1. 【JAVA】Java 内存模型中的 happen-before

    前言 Java 语言在设计之初就引入了线程的概念,以充分利用现代处理器的计算能力,这既带来了强大.灵活的多线程机制,也带来了线程安全等令人混淆的问题,而 Java 内存模型(Java Memory M ...

  2. java内存模型按照线程隔离性_深入理解Java多线程与并发框(第③篇)——Java内存模型与原子性、可见性、有序性...

    一.Java内存模型 Java Memory Modle,简称 JMM,中文名称 Java内存模型,它是一个抽象的概念,用来描述或者规范访问内存变量的方式.因为各中计算机的操作系统和硬件不同,方式机制 ...

  3. java 线程 原子性_深入理解Java多线程与并发框架——Java内存模型与原子性、可见性、有序性...

    欢迎关注专栏<Java架构筑基>--专注于Java技术的研究与分享!Java架构筑基​zhuanlan.zhihu.comJava架构筑基--专注于Java技术的研究与分享! 后续文章将首 ...

  4. Java内存模型与线程

    一.一致性 高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,但也存在缓存一致性(cache coherence)问题 二.java内存模型 内存模型:对特定的内存或高速缓存进行读写访问的过程抽象 ...

  5. Java 内存模型与线程

    when ? why ? how ? what ? 计算机的运行速度和它的存储和通信子系统速度的差距太大,大量的时间都花费在磁盘I/O .网络通信或者数据库访问上.如何把处理器的运算能力"压 ...

  6. 01.java内存模型

    文章目录 1. 简述 2. JAVA 内存模型的规则 2.1. 对线程的非共享变量不做任何处理 2.2. 线程共享变量提供同步机制 2.2.1 同步顺序 2.2.2. HAPPENS-BEFORE 参 ...

  7. 深入理解Java虚拟机(第三版)-13.Java内存模型与线程

    13.Java内存模型与线程 1.Java内存模型 Java 内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到主内存和从内存中取出变量值的底层细节 该变量指的是 实例字 ...

  8. Java内存模型(二)

    volatile型变量的特殊规则 volatile是Java虚拟机提供的最轻量级的同步机制,当一个变量被定义成volatile后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的" ...

  9. Java 内存模型详解

    概述 Java的内存模型(Java Memory Model )简称JMM.首先应该明白,Java内存模型是一个规范,主要规定了以下两点: 规定了一个线程如何以及何时可以看到其他线程修改过后的共享变量 ...

最新文章

  1. 电子学会青少年编程等级考试Python案例08
  2. cmake通过命令行构建静态库/动态库
  3. Android Studio安卓开发中使用json来作为网络数据传输格式
  4. Unity学习笔记3 简易2D横版RPG游戏制作(三)
  5. SAP修改消息内容和报错类型(SE91和OBA5)
  6. C++volatile
  7. Java:ThreadPoolExecutor解析续--Executors
  8. 人造流星这种生日礼物,你有过吗?现在国外有了
  9. qt商业版和开源版的区别_微擎商业版系统V2.0.9全开源版纯净框架
  10. oracle安装 衍生进程已退出,linux安装oracle 出现问题
  11. config.class.php,The EventConfig class - PHP 7 中文文档
  12. asp判断ajax请求 -asp.net,一个asp注册验证用户名是否重复的Ajax实例
  13. 创建型设计模式(1)—— 单例模式(Singleton Pattern)
  14. 第一次装TFS的曲折经历
  15. [javaSE] 集合工具类(Collections-sort)
  16. 浅谈C++设计模式之抽象工厂(Abstract Factory)
  17. Oracle错误——user ** lacks CREATE SESSION privilege logon denied
  18. linux shell 发邮件
  19. 2021-08-22dc6靶机实战wp插件漏洞利用+suid提权+rockyou+sudo -l换命令写shell+nmap运行nse提权(转)
  20. 车载以太网工具链,你了解多少?

热门文章

  1. 现场工程师出手-PCAPHub与云SSH隧道稳妥实现异地LAN IIoT联测
  2. 安卓_手机卫士_第五天(手机定位,设备管理器,电话归属地)
  3. 智能开关继电器-选型篇2
  4. 批量获取指定文件夹下,多个同结构excel表固定位置的内容,并保存
  5. 重庆万豪行政公寓:经典焕新,传奇永续
  6. java.lang.IllegalStateException: Underflow in restore - more restores than saves
  7. 无人值守u盘安装linux,U盘无人值守安装Linux操作系统
  8. 华为mate40和mate40pro哪个更值得入手-华为mate40和mate40pro的性价比-华为mate40和mate40pro的区别
  9. 如何获取篮球比赛即时赔率
  10. ESP32+DHT11+Arduino连接phpstudy的本地数据库