深入了解Java虚拟机之高效并发

news2024/9/24 17:17:46

目录

Java内存模型与线程

概述

硬件的效率与一致性

Java内存模型

主内存与工作内存

内存间交互操作

对于volatile型变量的特殊规则

原子性、可见性与有序性

先行发生原则

Java与线程

线程实现

线程调度

状态切换

小结

线程安全与锁优化

概述

线程安全

Java中的线程安全

线程安全的实现方法

锁优化

总结


《深入理解Java虚拟机》是周志明先生所著,主要介绍了:Java发展历程、自动内存管理机制、垃圾收集器与内存分配策略、程序编译与代码优化和高效并发。

本文介绍高效并发。主要分为:Java内存模型与线程、线程安全与锁优化。之前笔者写过《并发编程入门》系列,可以对照学习。

Java内存模型与线程

概述

为什么要并发处理?并发处理是什么?

计算机系统中,由于 CPU 的速度比较快,而其他 I/O 设备(如磁盘、网络等)的速度比较慢,因此在处理大量任务时,CPU 往往会处于等待状态,这样会导致系统资源的浪费和响应速度的降低。而并发处理可以利用多个 CPU 核心同时处理多个任务,从而提高系统的资源利用率和响应速度。

另外,现代应用程序往往需要处理大量的并发请求和数据,例如 Web 服务器、数据库系统等。如果没有良好的并发处理机制,这些系统很容易就会出现瓶颈和性能问题,从而影响系统的可用性和可靠性。

并发处理是指在一个时间段内同时处理多个任务的能力,它可以提高系统的资源利用率、响应速度和吞吐量,从而提高系统的性能和并发能力。

硬件的效率与一致性

 处理器、高速缓存、主内存间的交互关系

由于 CPU 访问内存的速度比较慢,为了提高 CPU 的访问速度,计算机系统引入了高速缓存技术和指令重排序优化。

高速缓存是一种小而快速的存储设备,它可以存储最近经常访问的数据,从而避免了访问内存的时间延迟。需要补充的是:数据访问通常可以分为三个层次:寄存器(访问最快,容量非常有限)、内存(容量较大,访问速度相对较慢)和外部存储设备(如硬盘、网络存储等,容量大,速度非常慢)。

指令重排序技术是指处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致

高速缓存引出新问题:如何去保证缓存一致性?指令重排序引出新问题:多线程程序中的数据竞争和数据可见性问题(某些修改操作的结果对其他线程不可见)。

Java内存模型

Java内存模型屏蔽了各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。定义多线程程序中共享变量的访问和修改方式。

主内存与工作内存

  1. 所有的变量都存储在主内存中,每个线程都有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝;变量是指:实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。

  2. 线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量,线程间变量值的传递均需要通过主内存来完成。

内存间交互操作

在 Java 中,内存间交互操作包括两个基本操作:读取(load)和存储(store)。读取操作是从内存中获取变量的值,存储操作是将变量的值写回到内存中。

在读取操作中,可以分为两个阶段:首先从主内存中读取变量的值到工作内存中,然后线程从工作内存中获取该变量的值。在存储操作中,也可以分为两个阶段:首先将变量的值写入工作内存中,然后将工作内存中的值写回到主内存中。

 线程、主内存、工作内存三者的交互关系

对于volatile型变量的特殊规则

最轻量级别的同步机制。用于修饰变量,它有以下特性:

  1. 可见性:对一个 volatile 变量的写操作会立即刷新到主内存中,对该变量的读操作会从主内存中获取最新的值。这保证了 volatile 变量的可见性,即线程对变量的修改对其他线程是可见的。

  2. 有序性,禁止指令重排序:volatile 变量的读写操作具有有序性,即操作的顺序是按照程序代码的顺序执行的。这保证了 volatile 变量的操作不会受到指令重排序的影响。如何实现指令重排序?JVM 会创建内存屏障,指令重排序无法越过内存屏障,内存屏障是指字节码空操作加锁:“lock addl $0x0,(%esp)”

  3. 原子性:对于 volatile 变量的读写操作都具有原子性,即操作是不可分割的。这保证了 volatile 变量的操作是线程安全的。

需要注意的是,volatile 变量的原子性仅保证对单个变量的操作是原子的,对于多个 volatile 变量的操作,仍然需要使用其他的同步机制来保证其原子性。比如:

//最终输出结果大概率不是10000,原因是:
//volatile只能保证可见性,无法保证原子性,increase() 方法中包含了读取和写入操作,这两个操作虽然都是原子操作,但是它们并不是一个原子操作。因此,在多线程环境下,这些操作可能会出现交叉执行的情况,导致 race 的值不是预期的值。
//可通过加锁来解决。synchronized或AtomicInteger。
public class VolatileTest {
    public static volatile int race = 0;
    public static void increase() {
        race++;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 500; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 20; j++) {
                        increase();
                        System.out.println(race);
                    }
                }
            }).start();
        }
    }
}

一般来说,商用虚拟机选择把64位数据的读写操作(long和double)作为原子操作来对待。但可见性仍需volatile关键字。

原子性、可见性与有序性

  1. 原子性:原子性是指一个操作是不可中断的,要么全部执行成功,要么全部不执行。基本数据类型的读写是具备原子性的;另外,Java 中的 synchronized 关键字(monitorenter和monitorexit隐式执行)、 Lock 接口和原子操作类(AtomicXXX)也可以用来保证代码块或方法的原子性。

  2. 可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。以Volatile为例,其保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。另外,Java中的synchronized和final能保证可见性。

  3. 有序性:有序性是指程序中的操作执行顺序与代码的书写顺序一致。包括两方面:程序顺序性和指令重排序。Java 提供了volatile、synchronized和Lock来保证有序性。

先行发生原则

通过遵守 happens-before 原则,Java 程序可以保证多线程之间的操作顺序和可见性,从而避免出现数据竞争和线程安全问题。

为了保证内存的可见性和有序性,Java 内存模型定义了 happens-before 规则,用于规定在多线程环境下,一个操作的结果对另一个操作的可见性和顺序关系。所谓 happens-before :

  1. 程序顺序规则:单个线程,它的所有操作执行的顺序必须与程序代码中的顺序一致。

  2. volatile 变量规则:对一个 volatile 变量的写操作必须先于后续的读操作。这保证了 volatile 变量的可见性和有序性。

  3. 传递性规则:如果操作 A happens-before 操作 B,操作 B happens-before 操作 C,则操作 A happens-before 操作 C。

  4. synchronized 规则:对于同一个锁,线程对锁的解锁操作必须先于后续的加锁操作。这保证了 synchronized 块中的操作的可见性和有序性。

  5. 线程启动和终止规则:线程的 start() 方法必须先于线程中的任何操作;线程的所有操作必须先于线程的终止操作。

Java与线程

线程实现

线程的实现可以基于轻量级进程或者用户级线程。在基于进程的实现中,每个线程都是一个独立的进程,由操作系统来进行调度,并拥有自己的地址空间。在基于用户级线程的实现中,多个线程共享同一个进程空间,线程的调度由用户级线程库来完成,操作系统并不直接参与线程的调度。

Java 线程的底层实现是基于操作系统提供的线程机制。在 Java 中,每个线程都会被映射到操作系统的一个线程中,在操作系统中,线程是最基本的调度单位。Java 程序员不需要直接操作线程,只需要使用 Java 提供的线程库就可以实现多线程编程。

线程调度

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度和抢占式线程调度。在 Java 中,线程调度是由操作系统来完成的,因此 Java 中的线程调度策略是抢占式调度。

协同式调度是指线程自己选择何时让出 CPU 的控制权;抢占式调度是指操作系统强制剥夺正在执行的线程的 CPU 时间片,并将 CPU 交给其他优先级更高的线程执行。

状态切换

 生命周期包括新建(NEW)、就绪(RUNNABLE)、运行(RUNNING)、阻塞(BLOCKED)和销毁(TERMINATED)。详见:并发编程入门(一):多线程基础

小结

本章首先介绍了Java内存模型,分别为:主内存与工作内存是什么?它们是如何交互的?原子性、可见性与有序性;Volatile是如何保证三大特性的;先行发生原则。

其次,我们介绍了Java与线程,分别为:线程实现、线程调度以及线程状态间的切换。

线程安全与锁优化

概述

从面向过程到面向对象,提升了生产效率和软件规模。与此同时,发现存在并发问题。例如,人们很难想象现实中的对象在一项工作进行期间,会被不停地中断和切换,对象的属性(数据)可能会在中断期间被修改和变“脏”,而这些事件在计算机世界中则是很正常的事情。

线程安全

Java中的线程安全

框定范围,线程安全限定于多个线程之间存在共享数据访问这个前提,因为如果一段代码根本不会与其他线程共享数据,那么从线程安全的角度来看,程序是串行执行还是多线程执行对它来说是完全没有区别的。

按照线程安全的“安全程度”由强至弱,我们将Java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

不可变:

Java语言中,如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的(final 关键字可以用于修饰变量、方法和类,当一个变量被 final 修饰时,它的值被初始化后就不能再被改变;当一个方法被 final 修饰时,它不能被子类重写或覆盖;当一个类被 final 修饰时,它不能被继承。)。如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行。比如:枚举类型、String类的API(不会影响原来的值,返回新字符串对象)、Number的部分子类等。

绝对线程安全:

Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。Vector是一个线程安全的容器,因为它的add()、get()和size()这类方法都是被synchronized修饰的,尽管这样效率很低,但确实是安全的。但是,即使它所有的方法都被修饰成同步,也不意味着调用它的时候永远都不再需要同步手段了。在多线程的环境中,如果不在方法调用端做额外的同步措施的话,同时add或者remove是不安全的。

相对线程安全:

相对的线程安全就是我们通常意义上所讲的线程安全,需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。比如:Vector、HashTable、Collections的synchronizedCollection()。

线程兼容:

线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。比如:ArrayList和HashMap等。

线程对立:

线程对立线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。Java语言天然具备多线程特性。

线程安全的实现方法

互斥同步

同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。

  1. synchronized关键字:(1)同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。(2)synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。(3)同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。

  2. 可重入锁ReentrantLock:相比synchronized,ReentrantLock增加了一些高级功能,主要有以下3项:等待可中断(当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待)、可实现公平锁(公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。),以及锁可以绑定多个条件(绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象)。

提倡用Sychronized,因为JVM一直在优化Sychronized的性能;Sychronized使用简单,不容易死锁。

非阻塞同步

互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题;非阻塞同步是指先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止)。

CAS指令需要有3个操作数,分别是内存位置(在Java中可以简单理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新,但是无论是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作。eg:AtomicInteger的incrementAndGet()方法。

CAS存在ABA问题(线程1在操作A时,线程2可能会将A改为B,再改回A,线程1错误认为变量没有被修改),大多数ABA不会影响并发正确性。解决方案:加入时间戳纬度;使用Sychronized。

无同步方案

有些代码是天生线程安全的,未涉及共享数据,自然无需任何同步去保证正确性。

可重入代码:可以被多个任务同时调用而不会导致错误或竞争条件的代码。特征:(1)不依赖于全局变量或静态变量,或者使用局部变量或参数来存储状态信息;(2)不使用非可重入函数或系统调用,如 malloc 和 sleep 等;(3)不会在执行期间修改自身的代码或数据结构。

线程本地存储:如果一个变量要被多线程访问,可以使用volatile关键字声明它为“易变的”;如果一个变量要被某个线程独享,可以通过java.lang.ThreadLocal类来实现线程本地存储的功能。

锁优化

锁消除

可重入代码,自然无须加锁。

锁粗化

大多数情况下,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步。但是,如果一系列的连续操作都对同一个对象反复加锁和解锁,不妨锁粗化,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

轻量级锁

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

自旋锁

自旋锁不会将线程阻塞,而是通过循环等待的方式来占用 CPU 时间,直到该锁变为可用。以此为基础,延伸出自适应自旋锁,它可以自适应地调整自旋锁的自旋时间,以提高多线程程序的性能和可伸缩性。

偏向锁

锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

总结

Java中的线程安全从强到弱分别是不可变(final、String等API)、绝对线程安全(一般不存在)、相对线程安全(Vector、HashTable等,内部安全,需保证调用安全)、线程兼容(ArrayList和HashMap,对象本身不安全)和线程对立(一般不存在)。

线程安全的实现方法有两种:互斥同步(Synchronized和ReentrantLock)、非阻塞同步(CAS)和无同步方案(可重入代码和本地线程存储)。

锁优化有五种优化手段:锁消除、锁粗化、轻量级锁、自旋锁和偏向锁。

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

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

相关文章

HDR显示技术

什么是HDR? HDR&#xff08;High-Dynamic Range&#xff0c;简称HDR&#xff09;是指高动态范围图像&#xff0c;是一种能够显示更大的亮度范围和对比度的图像技术。HDR可以让暗部的细节变亮&#xff0c;亮部的细节不失真&#xff0c;呈现出更自然、更真实的画面&#xff0c;…

记一次618军演压测TPS上不去排查及优化 | 京东云技术团队

本文内容主要介绍&#xff0c;618医药供应链质量组一次军演压测发现的问题及排查优化过程。旨在给大家借鉴参考。 背景 本次军演压测背景是&#xff0c;2B业务线及多个业务侧共同和B中台联合军演。 现象 当压测商品卡片接口的时候&#xff0c;cpu达到10%&#xff0c;TPS只有…

Tomcat基本原理

1.Tomcat核心&#xff1a; Http服务器Servlet容器 组件分工&#xff1a; 连接器Connector&#xff1a;处理 Socket 连接&#xff0c;负责网络字节流与 Request 和 Response 对象的转化。容器Container&#xff1a;加载和管理 Servlet&#xff0c;以及具体处理 Request 请求。 …

静态杂波滤波算法

静态杂波滤波算法 1.零速通道置零法2.动目标显示&#xff08;MTI&#xff09;3.相量均值相消算法&#xff08;平均相消算法&#xff09;4.总结 1.零速通道置零法 零速通道置零法&#xff0c;是指在2D-FFT&#xff08;速度维FFT&#xff09;后直接将R-V谱矩阵&#xff08;RD图&…

计算机网络学习笔记-传输层

目录​​​​​​​ 概述 与网络层的区别 端口号 概述 分类 重要功能&#xff1a;复用分用 两个重要协议&#xff1a;UDPTCP UDP用户数据报协议 概述 主要特点 首部格式 TCP传输控制协议 主要特点 首部格式 运输连接管理 概述 运输层提供应用进程间的逻辑通信通…

SpringBoot—yml配置多环境(踩坑总结!)

一、实例操作 ①、创建对应的application.yml &#xff08;dev 开发&#xff1b;prod 生产&#xff1b;test 测试&#xff09;文件 ②、在application.yml文件中&#xff0c;放公共的配置部分 &#xff08;这部分最好还是复制&#xff0c;自己敲位置&#xff0c;空格不对都会报…

深入理解一下Python中的面向对象编程

Part1 如何面向“对象” 网上关于Java和**C**的面向对象编程相关介绍的博客文章已经很多了&#xff0c;那我为什么还写呢&#xff1f;因为&#xff0c;人生苦短&#xff0c;刚好我是学Python的... 今天&#xff0c;我们就来走进面向对象编程的理想国——深入理解一下Python中…

2023年6月杭州/广州/深圳NPDP产品经理认证招生简章

产品经理国际资格认证NPDP是新产品开发方面的认证&#xff0c;集理论、方法与实践为一体的全方位的知识体系&#xff0c;为公司组织层级进行规划、决策、执行提供良好的方法体系支撑。 【认证机构】 产品开发与管理协会&#xff08;PDMA&#xff09;成立于1979年&#xff0c;是…

Go语言反射编程指南

反射[1]是一种编程语言的高级特性&#xff0c;它允许程序在运行时检视自身的结构和行为。通过反射&#xff0c;程序可以动态地获取类型(type)与值(value)等信息&#xff0c;并对它们进行操作&#xff0c;诸如修改字段、调用方法等&#xff0c;这使得程序具有更大的灵活性和可扩…

【论文阅读】用于大型城市场景的网格引导神经辐射场

【论文阅读】用于大型城市场景的网格引导神经辐射场 Abstract1. Introduction2. Related Works and Background大规模场景重建和渲染体积场景表示大尺度NeRF 3. Grid-guided Neural Radiance Fields3.1. Multi-resolution Feature Grid Pre-train3.2. Grid-guided Neural Radia…

AI炒股回报率500%?内行揭秘玄机

一篇来自佛罗里达大学的研究报告震惊了金融圈&#xff1a;用ChatGPT对公司新闻进行情绪分析&#xff0c;并按此在股市做多、卖空&#xff0c;最高可获得超过500%的投资回报率。虽然坊间对这份报告中惊人的回报率数据有所怀疑&#xff0c;但金融界正在因AI的介入发生改变。 摩根…

港联证券|龙头齐聚,本周7股将申购!今年第三高价新股也要来了?

本周&#xff08;6月5日—6月9日&#xff09;&#xff0c;共有7只新股将进行申购&#xff0c;其中创业板5只&#xff08;康力源、飞沃科技、恒勃股份、威士顿、海看股份&#xff09;、科创板2只&#xff08;西高院、智翔金泰&#xff09;。 资料显示&#xff0c;康力源是国内健…

Windows下安装与使用Kafka(使用Kafka内置的ZooKeeper图文结合版)

文章目录 Windows安装Kafka1.安装JDK并配置好对应的环境变量 2.安装配置Zookeeper1.下载安装包Apache Zookeeper2.解压并进入Zookeeper目录 防止端口8080启动后被占用&#xff0c;这里考虑先配置下3.安装Kafka3.1 下载安装包3.2、 解压并进入Kafka目录&#xff0c; Windows安装…

Vue.js 中的指令自定义是什么?如何自定义指令?

Vue.js 中的指令自定义是什么&#xff1f;如何自定义指令&#xff1f; Vue.js是一种流行的前端框架&#xff0c;它提供了一种称为“指令”的技术&#xff0c;用于操作DOM元素。Vue.js中内置了一些常用的指令&#xff0c;如v-if、v-show、v-for等。除了内置指令外&#xff0c;V…

基于Tensorflow+VGG+DBN本地化批量图像识别系统(深度学习+Python)含全部工程源码+视频演示+图片数据集

目录 前言总体设计系统整体结构图系统流程图 运行环境1. Python 环境2. Tensorflow 环境3. wxPython 环境4. PIL 环境 模块实现1. 数据预处理2. 模型简化处理3. 用户界面设计4. 翻译模块调用 系统测试1.模型训练效果2. 模型测试效果 代码实现1. 用户界面设计及模型调用2. 模型搭…

如何从消失的异常堆栈定位线上问题

一、消失的异常堆栈 如何快速定位问题&#xff1f;想必大家心中都有自己的答案&#xff0c;当然最简单直接的办法还是查找异常堆栈信息。 然而有时异常堆栈并不完整&#xff0c;只有一句描述&#xff0c;如下&#xff1a; Caused by: java.lang.NullPointerException 造成这…

显存容量一键翻倍性能暴涨,N卡遗留漏洞被破解了

2K、4K 高分辨率的普及&#xff0c;加上游戏特效进步复苏&#xff0c;显存容量的需求也提升了一个台阶。 经过测试&#xff0c;某些游戏最大显存占用已经超出 12GB &#xff0c;即便 1080P 也占用不低。 再到生产力、AI &#xff0c;显存就更容易爆炸。 显存这玩意不像内存可以…

通用文字识别OCR 之实现数字化教材

引言 通用文字 OCR 识别 API 是一种功能强大的服务&#xff0c;可用于多场景、多语种的整图文字检测和识别&#xff0c;通过将OCR技术应用于学校环境&#xff0c;可以实现教育资源的数字化和学习过程的自动化。 本文将探讨通用文字识别OCR 在学校的实际应用&#xff0c;希望对…

linux进阶 --- 环境搭建、单一执行、循环执行

环境搭建 操作环境 : 创建完虚拟机&#xff0c;安装命令行界面的linux操作系统&#xff08;centos8&#xff0c;rocky8&#xff0c;rhel8&#xff09;为服务器配置白名单和安全组 &#xff1a;vim /etc/selinux/config&#xff0c;selinuxdisabled&#xff08;getenforce&…

chatgpt赋能python:Python图片处理教程

Python 图片处理教程 Python 是一种功能强大的编程语言&#xff0c;广泛应用于大量不同的行业和领域。其中之一是图像处理和分析。Python 提供了一个庞大的图像库&#xff0c;其拥有大量的工具和函数。Python 图像库具有高度的可扩展性&#xff0c;可以很容易地将其与其他库集…