甲乙小朋友的房子

甲乙小朋友很笨,但甲乙小朋友不会放弃

0%

多线程与高并发

是视频马士兵 多线程与高并发的学习笔记。

计算机架构

从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的锁状态会产生如下锁升级的变化。

锁升级

    • 锁升级:无锁态->偏向锁->轻量级锁->重量级锁
    • 偏向锁(适合一个线程很短时间使用后释放,且反复使用):包含JavaThread当前线程指针
    • 轻量级锁:一旦有线程竞争,就升级为自旋锁。自旋锁:包含线程栈中Lock Record的指针。好处:用户态,所以效率高;坏处:占用CPU资源
    • 重量级锁(竞争激烈,自旋超过10次或者等待线程超过CPU核数一半、或者自适应自旋,向操作系统申请锁):指向互斥量(重量级锁)的指针。好处:锁有队列,当线程没有拥有锁时不消耗CPU资源在wait(也就是自旋线程会阻塞,进入阻塞队列进行等待)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|--------------------------------------------------------------------------------------------------------------|--------------------|
| Object Header (96 bits) | State |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| Mark Word (64 bits) | Class Word (32 bits) | |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| unused:25 | identity_hashcode:31 | cms_free:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 无锁态 |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| thread:54 | epoch:2 | cms_free:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 偏向锁 |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| ptr_to_lock_record | lock:2 | OOP to metadata object | Lightweight Locked |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| ptr_to_heavyweight_monitor | lock:2 | OOP to metadata object | Heavyweight Locked |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| | lock:2 | OOP to metadata object | Marked for GC |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|

锁降级

只发生在GC。忽略。

锁消除

1
2
3
4
void Add(String s1, String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1).append(s2); // StringBuffer 的append是线程安全的,StringBuilder的append是线程不安全的
}

StringBuffer 是用synchronized修饰的线程安全的。jdk如果检测到sb不可能有线程竞争,那jvm自动消除StringBuffer对象内部的锁。

锁粗化

1
2
3
4
5
6
7
void Test(String s){
StringBuffer sb = new StringBuffer();
while (i < 100){
sb.append(s);
++i;
}
}

JVM检测到sb的锁可以拿出到while外面

synchronized 的底层实现

用了个什么插件可以看到,用synchronized修饰的锁,底层实现就调用了lock cmpxchg

实际的各个层级 - java代码:synchronized - 字节码实现:monitorenter、moniterexit。也就是在执行过程中自动升级(无锁-偏向锁-自旋锁-重量级锁) - 更底层实现:lock cmpxchg:通过自旋改变一个值

volatile

volatile的两个含义: - 线程可见性 (下面code解释了) - 禁止指令重排序(禁止乱序):指的是对这个obj的读写的指令不能乱序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* volatile 关键字,使一个变量在多个线程间可见
* A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
* 使用volatile关键字,会让所有线程都会读到变量的修改值
*
* 在下面的代码中,running是存在于堆内存的t对象中
* 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
* 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
*
* 使用volatile,将会强制所有线程都去堆内存中读取running的值
*
* 可以阅读这篇文章进行更深入的理解
* http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
*
* volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
* @author mashibing
*/

public class T01_HelloVolatile {
/*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别
void m() {
System.out.println("m start");
while(running) {
}
System.out.println("m end!");
}

public static void main(String[] args) {
T01_HelloVolatile t = new T01_HelloVolatile();

new Thread(t::m, "t1").start();

try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

t.running = false;
}

}

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(环形队列)
jpg

内存屏障 --> 禁止指令重排序

  • 代码层面 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:

比较volatile和synchronized

  • volatile 关键字用途:
    • 保证变量的可见性:当一个变量被声明为 volatile 时,它确保了变量的修改对其他线程是可见的。也就是说,如果一个线程修改了 volatile 变量的值,其他线程会立即看到这个修改。这对于标志位或状态标识非常有用。
    • 禁止指令重排序:volatile 变量的读写操作不会被重排序,这意味着 volatile 变量的写入操作不会被移到后面执行,确保了写入操作的原子性。
    • 因此它不能保证线程安全!
  • synchronized 关键字用途:
    • 实现互斥锁:synchronized 用于实现互斥锁,可以确保同一时间只有一个线程可以访问被 synchronized 修饰的代码块或方法。这可以用于保护共享资源,防止多线程同时修改它们而导致的数据竞争和不一致性。
    • 实现条件同步:synchronized 还可以与 wait() 和 notify()/notifyAll() 方法一起使用,实现线程之间的条件同步。这对于线程等待某个条件满足后再执行特定操作非常有用。

所以,根据需求和具体情况,你可以选择使用 volatile 或 synchronized: - 如果你只需要确保变量的可见性并禁止指令重排序,而不需要互斥锁或条件同步,可以使用 volatile。 - 如果你需要互斥访问共享资源或实现复杂的条件同步,应该使用 synchronized。