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

目录

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)和无同步方案(可重入代码和本地线程存储)。

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

简单的UDP网络程序·续写

该文承接文章 简单的UDP网络程序 对于客户端和服务端的基本源码参考上文&#xff0c;该文对服务器润色一下&#xff0c;并且实现几个基本的业务服务逻辑 目录 demo1 第一个功能&#xff1a;字典翻译 初始化字典 测试代码&#xff1a;打印 字符串分割 客户端修改 成品效果…

AI宝典:AI超强工具大整合

&#x1f604;&#x1f604;个人介绍 光子郎.进行开发工作七年以上&#xff0c;目前涉及全栈领域并进行开发。会经常跟小伙伴分享前沿技术知识&#xff0c;java后台、web前端、移动端&#xff08;Android&#xff0c;uniapp&#xff0c;小程序&#xff09;相关的知识以及经验体…

C#发送邮箱设置及源码

用C#调用发送邮箱代码之前需要邮箱开通SMTP/POP3及设置授权码&#xff0c;开通及获取方法如下&#xff1a; 1、打开邮箱&#xff0c;登录邮箱&#xff0c;进入设置&#xff0d;》帐户 2、在“帐户”设置中&#xff0c;找到服务设置项&#xff0c;进行设置&#xff0c;如下…

[Flink] Flink On Yarn(yarn-session.sh)启动错误

在Flink上启动 yarn-session.sh时出现 The number of requested virtual cores for application master 1 exceeds the maximum number of virtual cores 0 available in the Yarn Cluster.错误。 版本说明&#xff1a; Hadoop&#xff1a; 3.3.4 Flink&#xff1a;1.17.1 问题…

Python实战基础20-解密文件及目录操作

任务1 为泸州驰援湖北的89名白衣勇士点赞 【任务描述】 设计python程序&#xff0c;实现用户可以为泸州驰援湖北的89名白衣勇士点赞留言。用户点赞留言内容保存到本地txt文件中。 import os # 导入os模块 import random # 导入随机模块 import string # 导入string模块# 定义…

《Lua程序设计》--学习3

输入输出 简单I/O模型 Lua 文件 I/O | 菜鸟教程 (runoob.com) 暂留 补充知识 局部变量和代码块 Lua语言中的变量在默认情况下是全局变量&#xff0c;所有的局部变量在使用前必须声明 在交互模式中&#xff0c;每一行代码就是一个代码段&#xff08;除非不是一条完整的命…

chatgpt赋能python:Python如何将IP地址转换为整数

Python如何将IP地址转换为整数 在计算机网络中&#xff0c;IP地址是一个包含32位的二进制数字&#xff0c;通常由四个8位二进制数字&#xff08;即“点分十进制”&#xff09;表示。但在某些情况下&#xff0c;需要将IP地址转换为整数&#xff0c;例如在网络编程中检查网络连接…

Ingress详解

Ingress Service对集群外暴露端口两种方式&#xff0c;这两种方式都有一定的缺点&#xff1a; NodePort &#xff1a;会占用集群集群端口&#xff0c;当集群服务变多时&#xff0c;缺点明显LoadBalancer&#xff1a;每个Service都需要一个LB&#xff0c;并且需要k8s之外设备支…

FPGA量子类比机制-FPQA,将在量子运算设计中引发一场新的革命

1980年代现场可程式化逻辑门阵列(FPGA)的出现彻底改变了电子设计。大约40年后&#xff0c;现场可程式化量子位元阵列(FPQA)可望在量子运算电路设计中引发一场类似的革命。 1980年代现场可程式化逻辑闸阵列(FPGA)的出现彻底改变了电子设计。FPGA允许设计人员创建适合特定应用的…

ArrayList 万字长文解析:使用、优化、源码分析

文章目录 ArrayList 万字长文解析&#xff1a;使用、优化、源码分析前言ArrayList 简介ArrayList 的基本使用方法ArrayList 性能优化ArrayList 的源码分析内部结构构造方法解析扩容机制System.arraycop与 Arrays.copyof 实现方式 与 使用场景迭代器 JDK 8版本 ArrayList bug 示…

【基于Rsync实现Linux To Windows文件同步】

基于Rsync实现Linux To Windows文件同步 简介安装步骤安装Linux服务器端1.安装rsync2.启动Rsync3.验证是否启动成功4.修改rsyncd.conf重启rsync服务 安装Windows客户端1.rsync客户端安装&#xff1a;2.配置环境变量3.测试rsync命令4.创建密码文件5.密码文件授权6.查看服务端需要…

Python高光谱遥感数据处理与机器学习实践技术丨Matlab高光谱遥感数据处理与混合像元分解

目录 Python高光谱遥感数据处理与机器学习实践技术 第一章 高光谱基础 第二章 高光谱开发基础&#xff08;Python&#xff09; 第三章 高光谱机器学习技术&#xff08;python&#xff09; 第四章 典型案例操作实践 Matlab 高光谱遥感数据处理与混合像元分解 第一章 理论…

【大数据之路4】分布式计算模型 MapReduce

4. 分布式计算模型 MapReduce 1. MapReduce 概述1. 概念2. 程序演示1. 计算 WordCount2. 计算圆周率 π 3. 核心架构组件4. 编程流程与规范1. 编程流程2. 编程规范3. 程序主要配置参数4. 相关问题1. 为什么不能在 Mapper 中进行 “聚合”&#xff08;加法&#xff09;&#xff…

操作系统原理 —— 什么是基本分页存储管理?(二十二)

在操作系统中&#xff0c;一个新的进程需要载入内存当中执行&#xff0c;在装入的时候需要给该进程分配一定的运行内存&#xff0c;在之前的章节中讲解了连续分配的几种方式&#xff0c;比如&#xff1a;单一连续分配、固定分区分配、动态分区分配&#xff0c;还讲解了对应的动…

Nacos架构与原理 - 总体架构

文章目录 Nacos 起源Nacos 定位Nacos 优势Nacos 生态Nacos 总体设计设计原则架构图用户层业务层内核层插件 小结 Nacos 起源 Nacos 在阿里巴巴起源于 2008 年五彩石项目&#xff08;完成微服务拆分和业务中台建设&#xff09;&#xff0c;成长于十年双十⼀的洪峰考验&#xff…

基于遗传算法的柔性生产调度研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

软件测试金融测试岗面试热点问题

1、网上银行转账是怎么测的&#xff0c;设计一下测试用例。 回答思路&#xff1a; 宏观上可以从质量模型&#xff08;万能公式&#xff09;来考虑&#xff0c;重点需要测试转账的功能、性能与安全性。设计测试用例可以使用场景法为主&#xff0c;先列出转账的基本流和备选流。…

DHT11温湿度传感器

接口定义 传感器通信 DHT11采用简化的单总线通信。单总线仅有一根数据线&#xff08;SDA&#xff09;&#xff0c;通信所进行的数据交换、挂在单总线上的所有设备之间进行信号交换与传递均在一条通讯线上实现。 单总线上必须有一个上拉电阻&#xff08;Rp&#xff09;以实现单…

burpsuite工具的使用(详细讲解)

一&#xff09;前言 我已经在之前详细的说明了burpsuite的安装过程&#xff0c;如果不了解的可以看 burpsuite安装教程 &#xff1a;http://t.csdn.cn/uVx9X 在这了补充说明一下&#xff0c;在安装完burpsuite并设置完代理后&#xff0c;会出现如果访问的url是使用http协议的…

【建议收藏】自动化测试框架开发教程

在自动化测试项目中&#xff0c;为了实现更多功能&#xff0c;我们需要引入不同的库、框架。 首先&#xff0c;你需要将常用的这些库、框架都装上。 pip install requests pip install selenium pip install appium pip install pytest pip install pytest-rerunfailures pip …