当前位置: 首页 > 图灵资讯 > 技术篇> java 多线程处理大批量数据 java多线程汇总结果

java 多线程处理大批量数据 java多线程汇总结果

来源:图灵教育
时间:2023-05-17 11:39:18

文章目录

  • 基础概念
  • 过程、线程、协程
  • Ques:
  • 单核cpu设置多线程有意义吗?
  • cpu密集型和 io 密集型
  • 工作线程数(线程池中的线程数)越大越好吗?设置多少合适?
  • 并发编程的三个特点
  • 可见性
  • 三级缓存
  • volatile 保证可见性
  • 有序性
  • 创建对象的过程
  • volatile 防止指令重排
  • 原子性
  • 乐观锁(无锁、自旋锁)
  • CAS 的 ABA 问题
  • CAS 如何在比较和交换过程中保证线程安全?
  • 悲观锁(sychronized)
  • 补充知识
  • 用户态和内核态
  • 对象的内存布局
  • JOL
  • 锁的升级过程
  • 几个概念
  • 偏向锁
  • 偏向锁是否一定比自旋锁效率高? ?
  • 乐观锁和悲观锁谁效率更高?

 

基本概念过程、线程、协程
首先,让我们谈谈这三种差异。例如,我们启动了我们 xx.exe ,首先,我们将在内存中开辟一个空间,将程序加载到内存中。如果我们想启动它,我们的系统应该找到程序内部的主线程进行操作。定义:流程是操作系统资源分配的基本单位线程,是调度执行的基本单位。多个线程共享一个流程的资源协程/纤程 是绿色线程,即用户管理而不是操作系统管理的线程
Ques:单核cpu设置多线程有意义吗?
有意义的是,有些线程操作(等数据,调io什么的)不消耗cpu
cpu密集型和 io 密集型
cpu密集型程序是指cpu利用率高(cpu计算时间大)的io密集型(cpu利用率低(io调度时间大)
工作线程数(线程池中的线程数)越大越好吗?设置多少合适?
不是 ,具体线程数一般通过模拟实际情况进行压力测量   公式: N(threads) = N(cpu) *U(cpu)*(1+W/C)               N(cpu) 处理器的核数               U(cpu) CPU利用率的预期               W/C  等待时间与计算时间的比率   W如何确定/C?    Profiler(性能分析工具) 测算   java JProfiler       Arthas (远程)
可见性并发编程的三个特征
线程将数据从主存储器加载到本地缓存器,并在以后的操作中阅读本地缓存器的数据。此时,如果有第二个线程来修改数据,第一个线程是否可以看到被修改的值是并发编程的可见性。  针对可见性问题,先说三级缓存:
三级缓存

java 多线程处理大批量数据 java多线程汇总结果_数据

图中有两个cpu,每个cpu有两个核,

多个cpu共享内存,每个cpu共享L3缓存,

CPU内核共享L1、L2缓存、L3缓存。

当每个线程读取数据时,将从内存中奖数据加载到L3缓存->L2->L1,读的时候从L1开始->l2->L3到内存

补充: 局部空间原理:当我们使用某个值时,我们很快就会使用相邻值;      时间局部性是指如果程序中的指令执行,指令可能在不久的将来再次执行;如果数据被访问,数据可能在不久的将来再次被访问。      因此,在阅读数据时,会将周围的值读入缓存中。一般是缓存行的大小。缓存行64字节。因此,在操作数据时,为了防止数据不一致,存在缓存一致性原理。      (java8中有@contended注释(后续版本取消)。可以理解,前后填写数据,保证只读取数据,浪费空间换时间。)注意:缓存一致性协议≠MESI,MESI只是微软缓存一致性协议,比较有名。感兴趣的人可以去了解
volatile 保证可见性
1.volatile修改的内存每次修改都可以看到--->修改还同时修改主存中的数据,并通知其他用于重新load数据的线程 2.volatile修饰引用类型 (其他线程仍然看不见)-->volatile修饰引用类型一般不会出现
有序性
并发编程有序性问题:CPU可能会乱序执行,以提高执行效率:as-if-serial ->不影响单线程的最终一致性
创建对象的过程
解释几个重要的指令  0::new->申请内存,设置默认值(半初始化状态)       3:特殊调用,这里调用初始化方法       4:建立关联  引用对象与对象建立关联(引用类型指针指向堆位于栈内栈帧)

java 多线程处理大批量数据 java多线程汇总结果_java_02

 

在实际操作中,这些指令3、4的顺序可能会被重新排序–>this溢出可能会发生(没有初始化已经联系起来,当场获得值是第一步初始化的默认值)

this 溢出是指在实例化完成之前,对象已返回引用。happens-before原则 JVM规定8种情况不允许重新安排(这方面的内容在这里跳过,有兴趣的可以去百度搜索)
volatile 防止指令重排
volatile实现细节 jvm层面           将内存屏障添加到指令之间,屏障两侧的指令不会重新排列           JVM层级   LOADLOAD   读读屏障  上下读不允许换序   其他不限制                    STORESTORE 写写屏障                       LOADSTORE   读写屏障                       STORELOAD   写读屏障
原子性
原子性是指一个线程的操作不能被其他线程打断,同时只有一个线程来操作一个变量。 这里先介绍几个基本概念1.rare condition 竞争条件--->多个线程访问共享数据的竞争.unconsistency  3.上锁的本质:并发编程序列化,将并发操作转化为顺序操作            将锁内的东西作为原子执行4.monitor  管程 (锁)5.critical section 临界区 锁定的大括号内部    如果临界区执行时间长,句子多,称为锁的粒度较粗,一般指锁的粒度较细

所谓上锁,就是保证临界区操作的原子性(atomicity)

乐观锁(无锁、自旋锁)
CAS操作:compare and swap/set/exchange  比较和交换
也就是说,当一个线程操作数据时,操作后将我的原始值与内存中的值进行比较。如果相同,则将新值更新到内存中 (细心的朋友可能会发现,这里的操作在阅读和更新之间仍然可能有一个坑。如果此时有人先更新,会有多线程问题吗?如何解决这个问题,让我们等一下)
CAS 的 ABA 问题

告诉我一个流行的例子。你出差了,然后在离开前看了看家里的样子。在出差的过程中,你的一个亲戚卖掉了房子,中间转了99只手。最后,你的亲戚感到内疚,又买了房子。回来后,对比一下,家还是那个家,但总觉得有些地方不对劲。这就是casaba的问题

aba问题的解决方案也很简单。只需添加一个版本号。也就是说,当你回来看的时候,这个房子的版本号99+和你的不一样,然后你就会知道有问题。比较原始数据时,同时比较版本号
CAS 如何在比较和交换过程中保证线程安全?
可以debug下atomic类,举个例子。atomicInteger

 

java 多线程处理大批量数据 java多线程汇总结果_System_04

 

点击unsafe,可以看到这里有几种cas方法,但这是native c++本地方法。

 

java 多线程处理大批量数据 java多线程汇总结果_System_05

 

这里直接给大家讲结论,感兴趣的可以去整个Hotspot源码debug,这里最后到一个   Atomic::cmpxchg 在方法中,会有if_mp的判断 ,mp是multi processor(多处理器)的含义。它将在汇编语言中执行  lock cmpxchg指令。cmpxchg(不是原子)   所以lock的指令给了一个锁,锁定了一个信号。(锁定北桥信号)
悲观锁(sychronized)补充知识用户态和核心态度
内核态: 执行在核心空间,可访问所有指令用户态度: 对于系统来说,只能访问用户可以访问的指令jvm只是一个用户态程序。然而,锁资源的申请必须通过kernnel和系统调用。因此,jdk早期的sychronized称为重量级锁
对象的内存布局

java 多线程处理大批量数据 java多线程汇总结果_数据_06

可以看出,对象的锁状态记录在markword中,具体情况如下图所示。锁状态主要取决于后三个橙色部分

java 多线程处理大批量数据 java多线程汇总结果_java_07

JOL

使用JOL可以看到对象的内存布局,具体的maven依赖以下内存布局

<dependency>            <groupId>org.openjdk.jol</groupId>            <artifactId>jol-core</artifactId>            <version>0.8</version>       </dependency>

使用

Object o = new Object();        System.out.println(ClassLayout.parseInstance(o).toPrintable());
锁的升级过程

java 多线程处理大批量数据 java多线程汇总结果_java_08

 

先说一下大致的流程,具体的字段意思稍后再说。

1. 创建一个对象,当锁未启动时,它将是一个普通对象
Object o = new Object();System.out.println(ClassLayout.parseInstance(o).toPrintable());
此时的锁状态是无锁状态,markword最后三位是001:

java 多线程处理大批量数据 java多线程汇总结果_System_09

2.锁定对象,锁将升级为轻量级锁(无锁、自旋锁)
Object o = new Object();System.out.println(ClassLayout.parseInstance(o).toPrintable()); synchronized (o){    System.out.println(ClassLayout.parseInstance(o).toPrintable()); }
此时锁的状态很轻级锁,markword后两个是00:

java 多线程处理大批量数据 java多线程汇总结果_java_10

3.竞争加剧:有线程自旋10次以上,-XX:PreBlockSpin(1.6之前 自旋次数可以通过此指令控制),或者自旋线程数超过cpu核数的一半可以升级为重量级锁。  加入Adapative后,加入Adapative Self Spinning(自适应自旋)
7.如果偏向锁已经启动,new的对象就会匿名偏向锁

Jvm通过 -XX:BiasedLockingStarupDelay (偏向于锁默认启动延迟 4s)设置偏向锁启动时间,默认情况下,4s后启动由于jvm启动过程中会有很多线程竞争,默认情况下不会打开偏向锁

Thread.sleep(5000);Object t = new Object();System.out.println(ClassLayout.parseInstance(t).toPrintable());

java 多线程处理大批量数据 java多线程汇总结果_数据_11

可以看出,此时偏向锁已经启动,最后三个markword是101
8.sychronized 加锁匿名偏向锁后,就是偏向锁。
Thread.sleep(5000);  Object t = new Object();  System.out.println(ClassLayout.parseInstance(t).toPrintable());  synchronized (t){       System.out.println(ClassLayout.parseInstance(t).toPrintable());  }

java 多线程处理大批量数据 java多线程汇总结果_java_12

可以看出,markword的后三位是101 偏向锁

5.轻度竞争:只要有一个线程来争夺这个锁,就会从偏向锁升级为轻量级锁。6.重度竞争:竞争加剧:有线程自旋10次以上,-XX:PreBlockSpin(1.6之前 自旋次数可以通过此指令控制),或者自旋线程数超过cpu核数的一半可以升级为重量级锁。
4.普通对象升级为偏向锁

以class为单位,维护每个class的偏向锁取消计数器 LR 。每次class的对象偏向于取消操作时,添加一个LR(LR有一个指针指向displacedMarkword)。当该值达到重偏差阈值(默认20)时,jvm会觉得class偏差锁有问题,因此会进行批重偏差。若达到40,将出现批量重撤销。如果您感兴趣,可以了解epoch的批量偏差和批量重撤销

几个概念偏向于锁
偏向锁的目标是减少使用轻量级锁产生的性能消耗,而无竞争,只使用一个线程。 。 每次申请和释放轻量级锁时,至少需要一次CAS,但偏向锁只需要一次CAS才能初始化。 “偏见”是指, 偏向锁假设未来只有第一个申请锁的线程会使用锁 (申请锁不会有任何线程)
偏向锁是否一定比自旋锁效率高? ?
不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及到取消锁的过程。此时,直接关闭偏向锁,直接使用自旋锁
乐观锁和悲观锁谁效率更高?
临界区执行长等待线程较多,建议悲观  建议等待线程少,临界执行短。   悲观锁线程等待是在队列中等待操作系统的调度,而不消耗cpu资源。乐观锁更消耗cpu资源  实战--->建议直接 sychronized (现在优化得很好)    sychronized 它将确保可见性,因为它将在修改数据后刷新缓存,并确保线程的顺序执行 。不能保证有序性