JAVA 多线程 总结
2023-07-27 10:49:11
基本概念过程、线程、协程
首先,让我们谈谈这三种差异。例如,我们启动了我们 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 (远程)
可见性并发编程的三个特征 线程将数据从主存储器加载到本地缓存器,并在以后的操作中阅读本地缓存器的数据。此时,如果有第二个线程来修改数据,第一个线程是否可以看到被修改的值是并发编程的可见性。 针对可见性问题,先说三级缓存:
三级缓存如图所示,有两个cpu,每个cpu有两个核,多个cpu共享内存,每个cpu共享L3缓存,每个cpu共享L1缓存,每个cpu共享L2缓存,cpu共享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:建立关联 引用对象与对象建立关联(引用类型指针指向堆位于栈内栈帧)
在实际操作中,这些指令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
点击unsafe,可以看到这里有几种cas方法,但这是native c++本地方法。
这里直接给大家讲结论,感兴趣的可以去整个Hotspot源码debug,这里最后到一个 Atomic::cmpxchg 在方法中,会有if_mp的判断 ,mp是multi processor(多处理器)的含义。它将在汇编语言中执行 lock cmpxchg指令。cmpxchg(不是原子) 所以lock的指令给了一个锁,锁定了一个信号。(锁定北桥信号)
悲观锁(sychronized)补充知识用户态和核心态度内核态: 执行在核心空间,可访问所有指令用户态度: 对于系统来说,只能访问用户可以访问的指令jvm只是一个用户态程序。然而,锁资源的申请必须通过kernnel和系统调用。因此,jdk早期的sychronized称为重量级锁
对象的内存布局可以看出,对象的锁状态记录在markword中,具体情况如下图所示。锁状态主要取决于后三个橙色部分
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());
锁的升级过程先说一下大致的流程,具体的字段意思稍后再说。
1. 创建一个对象,当锁未启动时,它将是一个普通对象
Object o = new Object();System.out.println(ClassLayout.parseInstance(o).toPrintable());
此时的锁状态是无锁状态,markword最后三位是001:
2.锁定对象,锁将升级为轻量级锁(无锁、自旋锁)
Object o = new Object();System.out.println(ClassLayout.parseInstance(o).toPrintable()); synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); }
此时锁的状态为轻量级锁,markword后两位为00:
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());
可以看出,此时偏向锁已经启动,最后三个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()); }
可以看出,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 它将确保可见性,因为它将在修改数据后刷新缓存,并确保线程的顺序执行 。不能保证有序性