是视频马士兵 多线程与高并发的学习笔记。
计算机架构
从CPU到: - 寄存器,<1ns - L1 Cache, 1ns - L2 Cache, 3ns - L3 Cache, 15ns - main memory, 80ns
flowchart TB subgraph CPU subgraph CPU1 Register寄存器 ArithmeticLogicUnit运算逻辑单元 L1Cache L2Cache end subgraph CPU2 end end subgraph 内存 subgraph ProgramA subgraph Thread1 Data 指令集 end subgraph Thread2 Data2 指令集2 end end end 指令集 --> Register寄存器 Data --> ArithmeticLogicUnit运算逻辑单元
多线程相关基础知识
- 别人的笔记多线程高并发
CAS Compare And Swap (Compare And Exchange)
- AtomicInteger的incrementAndGet(), 使用的是CAS-unsafe的方法-lock cmpxchg
- linux底层实现:lock cmpxchg
- lock: 操作系统&硬件层面 lock 当前内存 - 保证原子性
- cmpxchg : compare and exchange - 非原子性
Java对象布局
- Object o = new Object()
- 24字节: markword(8个字节)、class pointer(4个字节,用了UseCompressedClassPointers)、padding(4个字节,为了保证是8字节的倍数)
- 可以使用JOL(Java Object Layout)来显示打印对象的信息
- markword包括 (UseCompressedOops enabled by default)
- GC信息【分代年龄:4个bit表示,普通的15,CMS是6】
- hashCode(调用后才会有)
- markword里包含this object的锁状态!当用
asynchronous(o)
给对象o加锁时,对象o的锁状态会产生如下锁升级的变化。
- markword里包含this object的锁状态!当用
锁升级
- 锁升级:无锁态->偏向锁->轻量级锁->重量级锁
- 偏向锁(适合一个线程很短时间使用后释放,且反复使用):包含JavaThread当前线程指针
- 轻量级锁:一旦有线程竞争,就升级为自旋锁。自旋锁:包含线程栈中Lock Record的指针。好处:用户态,所以效率高;坏处:占用CPU资源
- 重量级锁(竞争激烈,自旋超过10次或者等待线程超过CPU核数一半、或者自适应自旋,向操作系统申请锁):指向互斥量(重量级锁)的指针。好处:锁有队列,当线程没有拥有锁时不消耗CPU资源在wait(也就是自旋线程会阻塞,进入阻塞队列进行等待)
1 | |--------------------------------------------------------------------------------------------------------------|--------------------| |
锁降级
只发生在GC。忽略。
锁消除
1 | void Add(String s1, String s2){ |
StringBuffer 是用synchronized修饰的线程安全的。jdk如果检测到sb不可能有线程竞争,那jvm自动消除StringBuffer对象内部的锁。
锁粗化
1 | void Test(String s){ |
JVM检测到sb的锁可以拿出到while外面
synchronized 的底层实现
用了个什么插件可以看到,用synchronized修饰的锁,底层实现就调用了lock cmpxchg
实际的各个层级 - java代码:synchronized - 字节码实现:monitorenter、moniterexit。也就是在执行过程中自动升级(无锁-偏向锁-自旋锁-重量级锁) - 更底层实现:lock cmpxchg:通过自旋改变一个值
volatile
volatile的两个含义: - 线程可见性 (下面code解释了) - 禁止指令重排序(禁止乱序):指的是对这个obj的读写的指令不能乱序
1 | /** |
volatile在JVM里怎么实现的?
结论:JVM内存屏障。 先看下面的一些概念,然后就懂了。
超线程
一个CPU有两个寄存器,一个计算单元。这样就可以两个thread“几乎同时”跑(很少有上下文切换)。所谓的四核八线程也即这个原因
缓存一致性协议 --> 保证线程可见性
- L1,L2是单个cpu内部的。就是线程本地内存!
- L3是多个cpu共享的
- CPU数据的来源从近及远依次是L1 Cache, L2 Cache, L3 Cache(多个核或者多个CPU公用)以及main memory,也就是先从近的读取,如果没有再逐渐向外读取。
- cache line 缓存行:由于程序局部性原理,会把数据按块读取。一个块就叫一个缓存行,64 bytes
- CPU层级的数据一致性volatile,是以cache line为单位的。
- 也就是一旦修改cache line里的任意data,别的L1/L2里的此cacheLine就失效了,要重读!也就是MESI Cache缓存一致性协议。x86 intel CPU的协议,表示modified、exclusive(独占整个缓存行)、shared、invalid(一个cpu修改了另一个的就是失效的)
- 缓存行对齐:对于有些特别敏感的数字,会存在线程高竞争的访问,为了保证不发生伪共享(两个数字在同一个缓存行,但长期被两个不同的线程使用),可以使用缓存行对齐的编程方式;可以补齐到64字节,避免volatile可见性带来的效率低下问题。disruptor(环形队列)
内存屏障 --> 禁止指令重排序
- 代码层面 volatile i
- 字节码层面 ACC_VOLATILE 加标志
- JVM层面是通过内存屏障来实现
- JVM定义了四种接口,LoadLoad, StoreStore, LoadStore,StoreLoad,也是四种内存屏障
- 对volatile 变量 的读写操作加:
- StoreStore, Volatile Write, StoreLoad --> 也就是write->VolatileWrite->Read 这样的指令不可以重排序
- LoadLoad, Volatile Read, LoadStore --> 也就是Read->VolatileRead->Write 这样的指令不可以重排序 (sfence mfence lfence等系统原语)。写操作前后ss和sl,读操作前后ll和ls
- 系统层面 x86:
- storeload最终是通过lock指令,锁总线。参考文献深入理解java内存屏障
比较volatile和synchronized
- volatile 关键字用途:
- 保证变量的可见性:当一个变量被声明为 volatile 时,它确保了变量的修改对其他线程是可见的。也就是说,如果一个线程修改了 volatile 变量的值,其他线程会立即看到这个修改。这对于标志位或状态标识非常有用。
- 禁止指令重排序:volatile 变量的读写操作不会被重排序,这意味着 volatile 变量的写入操作不会被移到后面执行,确保了写入操作的原子性。
- 因此它不能保证线程安全!
- synchronized 关键字用途:
- 实现互斥锁:synchronized 用于实现互斥锁,可以确保同一时间只有一个线程可以访问被 synchronized 修饰的代码块或方法。这可以用于保护共享资源,防止多线程同时修改它们而导致的数据竞争和不一致性。
- 实现条件同步:synchronized 还可以与 wait() 和 notify()/notifyAll() 方法一起使用,实现线程之间的条件同步。这对于线程等待某个条件满足后再执行特定操作非常有用。
所以,根据需求和具体情况,你可以选择使用 volatile 或 synchronized: - 如果你只需要确保变量的可见性并禁止指令重排序,而不需要互斥锁或条件同步,可以使用 volatile。 - 如果你需要互斥访问共享资源或实现复杂的条件同步,应该使用 synchronized。