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

java并发编程讨论:锁的选择

java并发编程

线程堆栈大小

单线程的堆栈大小默认为1M,1000个线程内存就占了1G。所以,受制于内存上限,单纯依靠多线程难以支持大量任务并发。

上下文切换开销

ReentrantLock

2个线程交替自增一个共享变量,使用ReentrantLock,每个线程1000w次,这是vmstat的结果:

procs -----------memory---------- —swap-- -----io---- -system-- -----cpu------
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 20476 886508 207672 2901024 0 0 0 0 583 1128 0 0 100 0 0
2 0 20476 857280 207672 2901060 0 0 0 164 2612 4980 13 3 83 0 0
1 0 20476 832052 207672 2901060 0 0 0 0 7038 21799 40 2 57 0 0
3 0 20476 830336 207672 2901060 0 0 0 0 5591 14159 41 2 57 0 0
0 0 20476 887988 207672 2901060 0 0 0 0 5170 13119 28 2 70 0 0
1 0 20476 888068 207672 2901028 0 0 0 0 560 1117 0 0 100 0 0

vmstat输出参数参看:
https://www.cnblogs.com/ggjucheng/archive/2012/01/05/2312625.html

我们注意到cs(上下文切换)达到过21799的峰值,相应的,in(中断次数)、us(用户cpu时间)也随之上升,整体耗时在2.7s。
究其原因,锁的争用会触发系统调用,迫使线程进入沉睡,系统调用又增加了用户态和内核态的上下文切换次数。

CAS

2个线程交替自增一个共享变量,使用CAS,每个线程1000w次,这是vmstat的结果:
procs -----------memory---------- —swap-- -----io---- -system-- -----cpu------
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 20476 879772 207672 2901068 0 0 0 0 873 1532 2 3 95 0 0
0 0 20476 887484 207672 2901076 0 0 0 0 2559 3206 30 3 67 0 0
0 0 20476 887484 207672 2901076 0 0 0 0 587 1065 1 0 99 0 0

cs峰值只到3206,整体耗时在400ms左右。
由于CAS是用户态操作,不涉及上下文切换,所以cs次数较少,我们认为这里的数值仅仅是线程正常切换导致。

无锁

单线程自增2000w次
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 20476 878564 207676 2901108 0 0 0 0 733 1228 1 1 98 0 0
0 0 20476 886216 207676 2901108 0 0 0 0 2453 3443 11 3 86 0 0
0 0 20476 886216 207676 2901104 0 0 0 0 662 1171 0 0 99 0 0
非常快,几个毫秒跑完。本次cs与CAS下的cs差不多,印证了3000多次的cs只是正常的操作系统线程调度。然后我们会看到CAS下的us(值为30)明显高于单线程(值为11)。这是因为CAS的自增行为本质上是一个循环CAS,不会释放cpu,这是AtomicInteger自增的源码:

public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;}

我们看到getAndAddInt会反复尝试,直到自增成功为止。代码里的compareAndSwapInt就是CAS操作。

synchronized

r b swpd free buff cache si so bi bo in cs us sy id wa st
3 0 20476 885204 206452 2869528 0 0 0 0 2336 3461 14 5 81 0 0
2 0 20476 884668 206452 2869548 0 0 0 0 7332 19534 40 4 55 0 0
0 0 20476 911968 206452 2869520 0 0 0 0 3608 7762 20 3 77 0 0
0 0 20476 911968 206452 2869520 0 0 0 0 693 1290 0 0 100 0 0

耗时1.7s,cs的峰值高于CAS,但要低于ReentrantLock。具体原因我估计是因为jvm1.6之后对synchronized做过优化的缘故,synchronized并不会一开始就用lock那样的重量级锁,而是按照“偏向锁–>自旋锁–>重量级锁”的顺序来逐步升级的,前两者都是用户态的指令,并不触发cs。但由于竞争的存在,重量级锁又不可能完全避免,所以synchronized下的cs要低于ReentrantLock,但又明显高于完全用户态的CAS。

总结

1、java并发编程下锁的推荐使用顺序(越前者越推荐):
无锁 --> CAS --> synchronized --> ReentrantLock
2、上下文切换的耗时是用户态CAS指令的6~7倍,应尽量避免。

延伸讨论

对于IO密集型应用,如果无法做到“无锁编程”,最佳的并发编程模型应该是协程,而非使用多线程。我们以go语言来说明。

go语言

go的设计原则是:避免一切阻塞。
如果一个goroutine将要陷入系统调用,go调度器立刻从当前线程分离它,转而执行其他goroutine。这一点跟python的greenlet是类似的处理。
举个例子,goroutine A在等待channel的消息,阻塞的只是A,而不是执行A的线程T,T会在A被阻塞的这段时间被调度去执行goroutine B。
另外,这里的系统调用,我理解不仅仅是IO,由于锁争用导致的线程挂起也是系统调用,同样会导致goroutine的切换。总之记住一点:线程不会阻塞,阻塞的是goroutine。

volatile

volatile也是java里并发编程的手段之一。前面的例子之所以没有提到,是因为volatile不能保证自增的并发正确性(自增操作依赖于原值,其实是一个复合操作)。

首先,java字节码层面没法看出volatile与普通变量有何区别,比如下面代码:

private static volatile int race_ = 0;
public static void main(String[] args)
{race_++;
}

翻译成java字节码是:

0: getstatic     #2                  // Field race_:I
3: iconst_1
4: iadd
5: putstatic     #2                  // Field race_:I

看起来就是操作一个普通的static变量嘛。

我们只能从JIT的反汇编才能看出一些端倪:

0x000000000257ce9e: mov     rsi,0d59c01b0h    ;   {oop(a 'java/lang/Class' = 'com/lee/MainFlow')},获得类的地址,race_在类地址的偏移为0x88处0x000000000257cea8: mov     edi,dword ptr [rsi+88h]  ;*getstatic race_; - com.lee.MainFlow::myincr@0 (line 59)0x000000000257ceae: inc     edi0x000000000257ceb0: mov     dword ptr [rsi+88h],edi0x000000000257ceb6: lock add dword ptr [rsp],0h  ;*putstatic race_; - com.lee.MainFlow::myincr@5 (line 59)

race的地址是rsi+88h,dword ptr [rsi+88h]表示取得race_的内存值,通过:
mov edi,dword ptr [rsi+88h]
将race的内存值赋给edi寄存器,接着通过:
inc edi
实现自增,最后将自增的结果通过:
mov dword ptr [rsi+88h],edi
返回到内存。

由于race_是int型,所以自增操作在32位寄存器edi里就可以完成了,无需使用rdi。

注意最后一条汇编指令:
lock add dword ptr [rsp],0h
该指令在race为非volatile类型下是没有的,即非volatile版本执行完:
mov dword ptr [rsi+88h],edi
对内存的重新赋值就会返回了。

add dword ptr [rsp],0h指令把栈顶值加0,这是什么鬼?其实add是一个无意义的占位操作,只是由于lock后面必须跟特定的指令(例如ADD、XCHG等,MOV指令不能跟在lock后),所以才这么写。lock会锁内存总线,保证将cpu高速缓存(L1/L2)里当前缓存行的数据刷新到主存,同时使得其他cpu的高速缓存失效。lock之前的那条指令:
mov dword ptr [rsi+88h],edi
看似将寄存器的结果放到了内存,但由于硬件操作的异步性,有可能只是放到了cpu高速缓存里,而并未真正写到内存。一般来说,cpu对内存的写分为两种:write-through和write-back,前者同时写内存和高速缓存,后者只写高速缓存,写内存则被推迟到随后的某个时机。像linux操作系统使用的就是write-back,所以linux下的内存赋值不是立即生效的。

我们写一段伪码来表示就更容易理解了:

inc     edi
mov     dword ptr [rsi+88h],edi
flush

由上可见volatile关键字的几个特点:
原子性;
多线程间可见性。

这两个特点就来自于机器指令中的lock前缀(这里仅考虑多核情况,单核是无需lock前缀的,反正也没人跟你抢),lock会锁总线,禁止其他cpu对内存的访问(原子性),同时可能导致其他cpu缓存的失效,触发重读(多线程间可见性)。

还有一点需要特别指出,虽然volatile可以保证原子性,但反过来,指令的原子性并不是一定得靠volatile保证,例如java虚拟机规范就规定了除long和double外的基本类型的读写都是原子的,引用的读写也是原子的(见https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7),这些都无需volatile来保证其原子性,在这些基本类型上使用volatile,仅仅利用的是volatile的“多线程间可见性”(例如bool型变量的多线程感知)或者“禁止指令重排序”作用(例如double-check)。

附录

lock前缀简介

LOCK前缀导致处理器在执行指令时会置上LOCK#信号,于是该指令就被作为一个原子指令(atomic instruction)执行。在多处理器环境下,置上LOCK#信号可以确保任何一个处理器能独占使用任何共享内存。

注意:后来的Intel64和IA32处理器(包括Pentium4,Intel Xeon, P6)有时即使没有置上LOCK#信号也会产生锁动作的。

LOCK前缀只能放在下列指令前面: ADD, ADC, AND, BTC,BTR,BTS,CMPXCHG, CMPXCH8B, DEC,INC, NEG,NOT, OR, SBB, SUB, XOR, XADD以及XCHG。如果LOCK指令用在了非上述指令前则会引发#UD异常(undefined opcode exception,未定义操作数异常);而且LCOK前缀的指令的目标操作数只能是内存寻址方式,否则也会引发#UD异常的.XCHG指令不管前面有无LOCK前缀都会置上LOCK#信号,即XCHG总是作为原子指令执行。

LOCK前缀常常放在BTS前,用来实现对一个共享内存的读-修改-写(read-modify-write)原子化操作。

内存是否地址对齐并不影响LOCK前缀的功能。实际上,内存锁定对任何非对齐内存地址都起作用的。

这个指令的操作在64位和非64位模式下是一致的。

vmstat关键输出参数说明

cs 每秒上下文切换次数,例如我们调用系统函数,就要进行上下文切换,线程的切换,也要进程上下文切换,这个值要越小越好,太大了,要考虑调低线程或者进程的数目,例如在apache和nginx这种web服务器中,我们一般做性能测试时会进行几千并发甚至几万并发的测试,选择web服务器的进程可以由进程或者线程的峰值一直下调,压测,直到cs到一个比较小的值,这个进程和线程数就是比较合适的值了。系统调用也是,每次调用系统函数,我们的代码就会进入内核空间,导致上下文切换,这个是很耗资源,也要尽量避免频繁调用系统函数。上下文切换次数过多表示你的CPU大部分浪费在上下文切换,导致CPU干正经事的时间少了,CPU没有充分利用,是不可取的。

in 每秒CPU的中断次数,包括时间中断

us 用户CPU时间,我曾经在一个做加密解密很频繁的服务器上,可以看到us接近100,r运行队列达到80(机器在做压力测试,性能表现不佳)。

id 空闲 CPU时间,一般来说,id + us + sy = 100,一般我认为id是空闲CPU使用率,us是用户CPU使用率,sy是系统CPU使用率。

相关文章:

java并发编程讨论:锁的选择

java并发编程 线程堆栈大小 单线程的堆栈大小默认为1M,1000个线程内存就占了1G。所以,受制于内存上限,单纯依靠多线程难以支持大量任务并发。 上下文切换开销 ReentrantLock 2个线程交替自增一个共享变量,使用ReentrantLock&…...

大数据框架之Hadoop:MapReduce(三)MapReduce框架原理——ReduceTask工作机制

1、ReduceTask工作机制 ReduceTask工作机制,如下图所示。 (1)Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直…...

Nginx的介绍、安装与常用命令

前言:传统结构上(如下图所示)我们只会部署一台服务器用来跑服务,在并发量小,用户访问少的情况下基本够用但随着用户访问的越来越多,并发量慢慢增多了,这时候一台服务器已经不能满足我们了,需要我们增加服务…...

less基础

一、less介绍 1、介绍 是css预处理语言,让css更强大,可以实现在less里面定义变量函数运算等 2、less默认浏览器不识别 less转成csS (框架: less/sass 框架的内置了转码less-css) 3、使用语法 1.创建less文件xxx.less 后缀.less 2. less编译成css 再引入…...

电子统计台账:海量数据中导入特定行,极力减少键盘编辑工作量

1 前言从事企业统计工作的小伙伴,本来已经够忙的了,现在又要加上什么电子台账这种鬼任务,而且居然还要每月来一次,简直不能忍。如果非要捏着鼻子忍了,那么有什么办法,减轻工作量?2 问题的提出有…...

ChatGPT是如何训练得到的?通俗讲解

首先声明喔,我是没有任何人工智能基础的小白,不会涉及算法和底层原理。 我依照我自己的简易理解,总结出了ChatGPT是怎么训练得到的,非计算机专业的同学也应该能看懂。看完后训练自己的min-ChatGPT应该没问题 希望大牛如果看到这…...

刷题28-有效的变位词

32-有效的变位词 解题思路: 注意变位词的条件,当两个字符串完全相等或者长度不等时,就不是变位词。 把字符串中的字符映射成整型数组,统计每个字符出现的次数 注意数组怎么初始化: int [] s1new int[26]代码如下&a…...

JavaWeb中异步交互的关键——Ajax

文章目录1,Ajax 概述1.1 作用1.2 同步和异步1.3 案例1.3.1 分析1.3.2 后端实现1.3.3 前端实现2,axios2.1 基本使用2.2 快速入门2.2.1 后端实现2.2.2 前端实现2.3 请求方法别名3,JSON3.1 概述3.2 JSON 基础语法3.2.1 定义格式3.2.2 代码演示3.2.3 发送异步…...

python爬虫常见错误

python爬虫常见错误前言python常见错误1. AttributeError: WebDriver object has no attribute find_element_by_id1. 问题描述2. 解决办法2. selenium:DeprecationWarning: executable_path has been deprecated, please pass in1. 问题描述2. 解决办法3. 下载了包…...

AI_Papers周刊:第三期

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 2023.02.20—2023.02.26 文摘词云 Top Papers Subjects: cs.CL 1.LLaMA: Open and Efficient Foundation Language Models 标题:LLaMA:开放高效的基础语言模型 作者&#…...

在win7上用VS2008编译skysip工程

在win7上用VS2008编译skysip工程 1. 安装vs2008及相应的补丁包,主要包含以下安装包: 1.1 VS2008TeamSuite90DayTrialCHSX1429243.iso 1.2 VS2008SP1CHSX1512981.iso 1.3 VS90sp1-KB945140-CHS.exe 2. 安装Windows SDK: 6.0.6001.18000.367-KRMSDK_EN.zip 例如安装路径为…...

python 数据结构习题

旋转图像给定一个nn的二维矩阵表示一个图像。将图像顺时针旋转90度。你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。例如,给定matrix[[1,2,3],[4,5&#x…...

18、MySQL8其它新特性

文章目录1 MySQL8新特性概述1.1 MySQL8.0 新增特性1.2 MySQL8.0移除的旧特性2 新特性1:窗口函数2.1 使用窗口函数前后对比2.2 窗口函数分类2.3 语法结构2.4 分类讲解1 序号函数2 分布函数3 前后函数4 首尾函数5 其他函数2.5 小 结3 新特性2:公用表表达式…...

【Android笔记79】Android之接口请求库Retrofit的介绍及使用

这篇文章,主要介绍Android之接口请求库Retrofit的介绍及使用。 目录 一、Retrofit接口请求库 1.1、什么是Retrofit 1.2、Retrofit的使用 (1)引入依赖...

蓝桥杯 考勤打卡

问题描述 小蓝负责一个公司的考勤系统, 他每天都需要根据员工刷卡的情况来确定 每个员工是否到岗。 当员工刷卡时, 会在后台留下一条记录, 包括刷卡的时间和员工编号, 只 要在一天中员工刷过一次卡, 就认为他到岗了。 现在小蓝导出了一天中所有员工的刷卡记录, 请将所有到岗…...

逻辑回归

逻辑回归 在分类问题中,要预测的变量y为离散值(y0~1),逻辑回归模型的输出变量范围始终在 0 和 1 之间。 训练集为 {(x(1),y(1)),(x(2),y(2)),...,(x(m),y(m))}\{(x^{(1)},y^{(1)}),(x^{(2)},y^{(2)}),...,(x^{(m)},y^{(m)})\} {…...

CTFer成长之路之Python中的安全问题

Python中的安全问题CTF 1.Python里的SSRF 题目提示 尝试访问到容器内部的 8000 端口和 url path /api/internal/secret 即可获取 flag 访问url: http://f5704bb3-5869-4ecb-9bdc-58b022589224.node3.buuoj.cn/ 回显如下: 通过提示构造payload&…...

SpringBoot知识快速复习

Spring知识快速复习启动器自动装配ConfigurationImport导入组件Conditional条件装配ImportResource导入Spring配置文件ConfigurationProperties配置绑定Lombok简化开发dev-toolsyaml请求和响应处理静态资源规则与定制化请求处理-Rest映射请求处理-常用参数注解使用请求处理-Ser…...

SpringBoot+React博客论坛系统 附带详细运行指导视频

文章目录一、项目演示二、项目介绍三、项目运行截图四、主要代码一、项目演示 项目演示地址: 视频地址 二、项目介绍 项目描述:这是一个基于SpringBootReact框架开发的博客论坛系统。首先,这是一个前后端分离的项目,文章编辑器…...

C++ primer 之 extern

C primer 之 extern什么是声明什么是定义两者有什么区别ertern的作用什么是声明 就是使得名字为程序所知,一个文件如果想使用别处定义的名字就必须包含对那个名字的声明。 什么是定义 负责创建与名字关联的实体。 两者有什么区别 变量声明和声明都规定了变量的…...

Linux 练习二 (VIM编辑器 + GCC编译器 + GDB调试)

文章目录VIM命令思维导图GCC编译器1、GCC编译文件练习2、静态库动态库制作练习将此函数编译成动态库将此函数编译成静态库GCC优化选项 -OnGDB调试命令练习练习一:编写一个程序,通过gdb调试,使用到gdb的b,n,s&#xff0…...

python3 连接数据库 mysql PyMysql

python3PyMysql PyMySQL 是在 Python3.x 版本中用于连接 MySQL 服务器的一个库 , 遵循 Python 数据库 API v2.0 规范 。 PyMySQL 安装 pip install PyMySQLPyMySQL 连接数据库 import pymysql pymysql.Connect(hostlocalhost,port 3306,user root,password **…...

昇腾AI新技能,还能预防猪生病?

国药集团动物保健股份有限公司(简称“国药动保”)是专业从事动物保健产品研发、生产和销售的国家高新技术企业,是国内少数几家具备新产品原创能力的动物保健企业。其中,猪圆环病毒灭活疫苗等市场份额位居行业前列。 “猪圆环病毒…...

模板方法模式(Template Method)

模式结构图 说明 基本方法是模板方法的组成部分。基本方法分为一下三种: 抽象方法 由抽象类声明,由其具体子类实现。C中就是纯虚函数。 具体方法 由抽象类或具体类声明并实现,子类可以进行覆盖也可以继承。C中是虚函数。 钩子方法 由抽象类…...

C C++ typedef的使用

一、为基本数据类型起别名 typedef int myint; myint x 5; "myint"是"int"的别名,可以使用"myint"来代替"int"声明变量,这个很好理解,但是也很少有人这么用吧。 二、为结构体起别名 …...

Laravel框架03:DB类操作数据库

Laravel框架03:DB类操作数据库一、概述二、数据表的创建与配置三、增删改操作1. 增加信息2. 修改数据3. 删除数据四、查询操作1. 取出基本数据2. 取出单行数据3. 获取一个字段的值4. 获取多个字段的值5. 排序6. 分页五、执行任意的SQL语句一、概述 按照MVC的架构&a…...

数据结构期末复习总结(前章)

作者的话 作为一名计算机类的学生,我深知数据结构的重要性。在期末复习前,我希望通过这篇博客给大家一些复习建议。希望能帮助大家夯实数据结构的基础知识,并能够更好地掌握数据结构和算法的应用。 一、绪论 数据:信息的载体&am…...

设计环形队列

文章目录1.思路分析1.1队列空满分析1.2出队分析2.循环队列设计1.思路分析 1.1队列空满分析 首先我们假设一个长度为4的环形队列 队头front 队尾rear 当队列为空时 frontrear 当队列满时 frontrear 所以我们无法判断队列是满的或者空的 因此我们多加入一个空间使队列长度为5&am…...

面向对象之-接口鉴权

1 需求 1.1 需求背景 为了保证接口调用的安全性,我们希望设计实现一个接口调用鉴权功能,只有经过认证之后的系统才能调用我们的接口,没有认证过的系统调用我们的接口会被拒绝。 2 需求分析 2.1 基础分析 对于如何做鉴权这样一个问题&…...

Python 多进程多线程线程池进程池协程

目录 一、线程与进程很简单的介绍 1.1 线程与进程的区别 二、多进程Process 2.1 多进程与多线程的区别 2.2 多进程为啥要使用队列 2.3 控制进程运行顺序 2.3.1 join , 2.3.1 daemon 守护进程 2.4 进程id 2.5 进程 存活状态is_alive() 2.5 实现自定义多…...

上海网页设计公司怎么样/网络建站优化科技

1.刘未鹏 | Mind Hacks 文章质量很高,涉及算法、数学和计算机,很值得一看转载于:https://www.cnblogs.com/cpointer/p/4644833.html...

如何在建设银行网站预约纪念币/深圳网络营销推广招聘网

NSDecimalNumber 是NSNumber的子类。 使用 [NSNumber numberWithDouble:]方法来格式我的NSNumber,但是,由于一些值比较大,我想要用科学计数法来表示。 NSDecimalNumber就提供了科学计数法的表示方法。 什么是科学计数法? 将一个数…...

中山建公司网站/广告推广语

title: 拉刷新,上拉加载更多 date: 2018-07-09 14:37:58 tags: 前端 categories:前端继续上一内容,更新第三部分,防重复点击-节流函数的使用。 截图分享功能按钮防重复点击(节流函数应用)3. 下拉刷新,上拉加载更多(这个地方有坑) …...

如何在微信上做广告/热狗网站关键词优化

2019独角兽企业重金招聘Python工程师标准>>> 故障描述:论坛访问出现502,开始以为php端口被占用,按常规查端口发现并没有被占用。后又出现mysql无法连接,报(2002) notconnect错误,问百度可能是空间满了。查看…...

网站日记在哪里看/百度网址怎么输入?

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼现在入手一个项目,大概是JSP传来一个JSON串,然后H5页面再用ajax调用jsp,这边我想先用jsp写一些死的数据,但是不知道怎么写,网上搜了很多都不行。大概是这样的:…...

WordPress潮流媒体主题/seo优化技术招聘

Signal 顾名思义是信号的意思,为什么要用到这个东西了? 原因:由于现在在负责写网游的后台loginServer,里面写了不少配置文件,当我们的产品上线后,loginServer开启后这时配置文件的数据就被读取进去了,但是…...