文章目录
- 基础概念
- 过程、线程、协程
- 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 (远程)
可见性并发编程的三个特征
线程将数据从主存储器加载到本地缓存器,并在以后的操作中阅读本地缓存器的数据。此时,如果有第二个线程来修改数据,第一个线程是否可以看到被修改的值是并发编程的可见性。 针对可见性问题,先说三级缓存:
三级缓存
图中有两个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:建立关联 引用对象与对象建立关联(引用类型指针指向堆位于栈内栈帧)
在实际操作中,这些指令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 它将确保可见性,因为它将在修改数据后刷新缓存,并确保线程的顺序执行 。不能保证有序性