Java内存模型:JMM(Java Memory Model),定义了一套在多线程环境下,读写共享数据(成员变量、数组)时,对数据的可见性,有序性和原子性的规则和保障。
原子性
问题分析
【问题】:两个线程对初始值为0的静态变量操作,一个线程做自增,一个线程做自减,各做50000次,结果是0吗?
            
            
              java
              
              
            
          
          public class Demo01 {
    static int i = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 50000; ++j) {
                i++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 50000; ++j) {
                --i;
            }
        });
        t1.start();
        t2.start();
        // 让主线程等待t1和t2两个子线程执行完毕后,再执行后续代码
        t1.join();
        t2.join();
        System.out.println(i);
    }
}【
结果】:上边代码输出,每次运行的结果不一样。
【原因】:Java中对静态变量的自增自减并不是原子操作,对于i++而言:
            
            
              java
              
              
            
          
          getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法
putstatic i // 将修改后的值存入静态变量i中Java的内存模型如下,如果需要完成静态变量的自增、自减,需要在主内存和工作线程的内存中进行交换数据。 
由于当线程是按顺序执行,所以并不会出现问题。 但是在多线程下,可能出现交错运行。线程是一个抢占式的,大家都是轮流使用CPU
解决方法
            
            
              java
              
              
            
          
          synchronized(对象) {
	要作为原子操作的代码
}修正后:
            
            
              java
              
              
            
          
          public class Demo02 {
    static int i = 0;
    static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                for (int j = 0; j < 50000; ++j) {
                    i++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (obj) {
                for (int j = 0; j < 50000; ++j) {
                    --i;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}这样++i和--i的四条指令都可以作为一个整体来运行。 并且:t1和t2必须锁的是同一个obj对象(相当于这两个人进入了两个不同的房间)
Monitor(监视器)
Monitor 是一种线程同步机制,可以理解为 对象锁 的内部实现。每个 Java 对象(Object)在 JVM 内部都有一个关联的 Monitor,用于实现 synchronized 同步机制。
Monitor 的组成部分
(1) Owner(持有者)
- 作用:表示当前持有 Monitor 的线程。
- 特点 :
- 当线程进入 synchronized代码块时,会尝试获取 Monitor 的owner权限。
- 如果 owner为null(即没有线程持有锁),当前线程会成为owner。
- 如果 owner已经被其他线程持有,当前线程会进入entryList等待。
 
- 当线程进入 
(2) EntryList(入口队列)
- 作用 :存储 竞争锁的线程(即等待获取锁的线程)。
- 特点 :
- 当线程 A 持有锁时,线程 B 尝试进入 synchronized代码块,会进入entryList并进入 BLOCKED 状态。
- 当线程 A 释放锁(退出 synchronized代码块),JVM 会从entryList中唤醒一个线程,使其竞争锁。
 
- 当线程 A 持有锁时,线程 B 尝试进入 
(3) WaitSet(等待队列)
- 作用 :存储 调用了 wait()的线程(即主动放弃锁的线程)。
- 特点 :
- 当线程 A 调用 wait()时,它会释放锁,并进入waitSet,状态变为 WAITING。
- 当其他线程调用 notify()或notifyAll()时,JVM 会从waitSet中随机唤醒一个(或全部)线程,使其重新竞争锁。
 
- 当线程 A 调用 
3. Monitor 的工作流程
            
            
              java
              
              
            
          
          synchronized (obj) {  // 1. 尝试获取 Monitor 的 owner
    while (!condition) {
        obj.wait();    // 2. 释放锁,进入 waitSet
    }
    // 3. 执行同步代码
}
obj.notify();          // 4. 唤醒 waitSet 中的线程- 线程 A 进入 synchronized代码块 :- 检查 owner,如果为空,线程 A 成为owner。
- 如果 owner已被线程 B 持有,线程 A 进入entryList(BLOCKED 状态)。
 
- 检查 
- 线程 A 调用 wait():- 释放 owner,线程 A 进入waitSet(WAITING 状态)。
- JVM 从 entryList中唤醒一个线程(如线程 B),使其成为新的owner。
 
- 释放 
- 线程 B 调用 notify():- 从 waitSet中随机唤醒一个线程(如线程 A),使其重新进入entryList(BLOCKED 状态)。
- 线程 A 需要重新竞争锁(不会立即获得锁)。
 
- 从 
- 线程 B 退出 synchronized代码块 :- 释放 owner,JVM 从entryList中选择一个线程(如线程 A),使其成为新的owner。
 
- 释放 
可见性
问题分析
【问题】:main线程对于run变量的修改对t线程是不可见的,这就导致了t线程无法停止:
            
            
              java
              
              
            
          
          public class Demo03 {
    static boolean run = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (run) {
                // ...
            }
        });
        t.start();
        Thread.sleep(1000);
        run = false; // 不会停下来
    }
}【原因】:
- 初始的时候,t线程刚开始从main线程的内存中读取了run的值到工作线程
- 因为t线程要频繁的从主内存中读取run的值,JIT编译器会将run的值缓存到自己的工作内存中的高速缓冲区中,这样就可以减少对主内存的读取。
- 主线程睡眠1s后,main线程修改了run的值,并同步到贮存,而t线程仍然是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。  
解决办法
volatile(易变关键字):用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主内存。
            
            
              java
              
              
            
          
          public class Demo03 {
    static volatile boolean run = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (run) {
                // ...
            }
        });
        t.start();
        Thread.sleep(1000);
        run = false; // 不会停下来
    }
}- volatile:保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,但是不能保证原子性,只能用在一个写线程,多个读线程的情况。
- synchronized:既可以保证原子性,也能保证代码块变量的可见性。但是缺点是:synchronized属于重量级操作,性能相对更低。
【
补充】:在上边的代码中,如果不加volatile,但是在for循环里加System.out.println(),t线程也能正常看到对run变量的修改。 【原因】:System.out.println()底层使用syncronized关键字,强制要求当前的线程不要从高速缓存中获取,从主线程中获取。
有序性
问题分析------指令重排序
            
            
              java
              
              
            
          
          public class Demo04 {
    static int num = 0;
    static boolean ready = false;
    // 线程1:执行此方法
    public static void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        }else {
            r.r1 = 1;
        }
    }
    // 线程2:执行此方法
    public static void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
    public static void main(String[] args) throws InterruptedException {
        I_Result r = new I_Result();
        Thread t1 = new Thread(() -> {
            actor1(r);
        });
        Thread t2 = new Thread(() -> {
            actor2(r);
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(r.r1);
    }
}
class I_Result {
    int r1;
}上边的代码执行一共可能会有三种不同的输出:
- 正常执行:t2先执行完,t1后执行 ==> 输出4
- t1先执行完,t2后执行 ==> 输出1
- 指令重排序(导致ready先变成true,num还未赋值)
- t2先执行ready = true
- t1执行(此时num还未被t2修改):r.r1 = num + num = 0
- t2再执行num = 2(但是t1已经计算完毕,不会影响结果)
 
解决方法
如果要保证线程安全,可以:
- 使用 volatile修饰ready和num,禁止指令重排序,并保证可见性:
            
            
              java
              
              
            
          
             static volatile int num = 0;
   static volatile boolean ready = false;- 这样 t2的num = 2和ready = true不会重排序,且t1能立即看到修改。
- 可能的输出:1或4(不会出现0)。
- 使用 synchronized加锁,确保原子性:
            
            
              java
              
              
            
          
             public static synchronized void actor1(I_Result r) { ... }
   public static synchronized void actor2(I_Result r) { ... }- 这样 t1和t2不会同时执行,输出一定是1或4。
有序性理解
            
            
              java
              
              
            
          
          static int i, j;
// 在某个线程内执行:
i = ...; // 较为耗时的操作
j = ...; 由于这段代码先执行i还是先执行j对结果并不会有影响,所以上面代码的执行可以是先对i赋值,再对j赋值;也可以是先对j赋值,再对i赋值。
案例:双重检查锁
            
            
              java
              
              
            
          
          public class Singleton {
    private Singleton(){}
    private static Singleton INSTANCE = null;
    public static Singleton getInstance(){
        // 实例没创建,才会进入内部的synchronized代码块
        if(INSTANCE == null){
            synchronized (Singleton.class){
                // 也许有其他线程已经创建实例,所以再判读一次
                if(INSTANCE == null){
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}上边是通过懒汉式的方式实现单例模式,只有首次使用getInstance()才使用synchronized加锁,后续使用无需加锁。
多线程下可能的问题
但是在多线程环境 下,上边的代码是有问题的 INSTANCE = new Singleton();这行代码在JVM中并不是原子操作,它分为三个步骤:
- 分配内存空间(malloc)
- 初始化对象(调用构造方法Sington())
- 将INSTANCE指向分配的内存地址(赋值)
但是JVM可能会对指令重排序(优化执行顺序),变成:
- 分配内存空间
- 将INSTANCE指向分配的内存地址(此时INSTANCE != null,但是对象未初始化)
- 初始化对象(调用构造方法)
如果发生这种重排序,可能导致:
- 线程 A 执行 INSTANCE = new Singleton();,但只完成了 步骤 1 和 2(INSTANCE 已不为 null,但对象未初始化)。
- 线程 B 调用 getInstance(),发现 INSTANCE != null,直接返回 未初始化完成的对象,导致错误!
解决办法
使用volatile禁止指令重排序
            
            
              java
              
              
            
          
          private static volatile Singleton INSTANCE = null;happens-before
是JMM的核心规则,定义了 多线程环境下操作的可见性和顺序性,确保一个线程对共享变量的修改能被其他线程正确观察到。
Java 内存模型定义了 6 种 Happens-Before 规则:程序顺序、锁、volatile、线程启动、线程终止、传递性
(1) 程序顺序规则(Program Order Rule)
在同一个线程中,前面的操作 Happens-Before 后面的操作。
            
            
              java
              
              
            
          
          int x = 1;    // (1)
int y = x + 1; // (2) ------ (1) Happens-Before (2)- 单线程下,代码顺序执行,(1)的结果对(2)可见。
(2) 锁规则(Monitor Lock Rule)
解锁操作 Happens-Before 后续的加锁操作。
            
            
              java
              
              
            
          
          synchronized (lock) {
    x = 10;    // (1)
}              // 解锁 (1) Happens-Before 后续的加锁
synchronized (lock) {
    int y = x; // (2) ------ 能读到 x = 10
}- 线程 A 解锁后,线程 B 加锁时能看到 A 的修改。
(3) volatile 变量规则(Volatile Variable Rule)
volatile 变量的写操作 Happens-Before 后续的读操作。
            
            
              java
              
              
            
          
          volatile boolean flag = false;
// 线程 A
flag = true;   // (1) ------ 写操作
// 线程 B
if (flag) {    // (2) ------ (1) Happens-Before (2),能读到 flag = true
    // do something
}- volatile保证可见性,写操作后,读操作一定能看到最新值。
(4) 线程启动规则(Thread Start Rule)
线程的
start()方法 Happens-Before 该线程的所有操作。
            
            
              java
              
              
            
          
          int x = 0;
Thread t = new Thread(() -> {
    System.out.println(x); // (2) ------ 能读到 x = 1
});
x = 1;                    // (1)
t.start();                 // (1) Happens-Before (2)- 主线程修改 x = 1后,子线程能读到这个值。
(5) 线程终止规则(Thread Termination Rule)
线程的所有操作 Happens-Before 它的终止检测(如
join())。
            
            
              java
              
              
            
          
          int x = 0;
Thread t = new Thread(() -> {
    x = 1;                // (1)
});
t.start();
t.join();                // (2) ------ (1) Happens-Before (2)
System.out.println(x);    // 输出 1- 子线程修改 x = 1后,主线程join()后能读到最新值。
(6) 传递性规则(Transitivity Rule)
**如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
            
            
              java
              
              
            
          
          int x = 0;
volatile boolean flag = false;
// 线程 A
x = 1;            // (1)
flag = true;      // (2) ------ (1) Happens-Before (2)
// 线程 B
if (flag) {       // (3) ------ (2) Happens-Before (3)
    System.out.println(x); // 输出 1 ------ (1) Happens-Before (3)
}- 由于 (1) → (2) → (3),所以(1)对(3)可见。
CAS与原子类
CAS
CAS:Compare and Swap,是一种乐观锁的思想。 【案例】多个线程要对一个共享变量的整型变量执行 + 1操作:
            
            
              java
              
              
            
          
          // 需要不断尝试
while(true) {
	int 旧值 = 共享变量; // 旧值 = 0
	int 结果 = 旧值 + 1; // 结果 = 0 + 1 = 1
	/*
		这时候如果别的线程把共享变量改成了5,本线程的正确结果1就作废了,此时:
		compareAdnSwap:返回false,重新尝试,直到:
		compareAndSwap:返回true,表示本线程做修改的同时,其他线程没有干扰
	*/
	if(compareAndSwap(旧值, 结果)) {
		// 成功,退出循环
	}
}【
注意】: 共享变量一定要用volatile修饰,保证共享变量的可见性,当前线程拿到的共享变量必须一定要是新值。(结合CAS和volatile就可以实现无锁并发了,适用于竞争不激烈、多核CPU的场景)
- 如果竞争激烈,线程重试会频繁发生,效率会受到影响
- 因为没有使用synchronized,线程并不会陷入阻塞,这也是效率提升的因素
CAS底层依赖于Unsafe类来直接调用操作系统底层的CAS指令
乐观锁与悲观锁
CAS:最乐观的估计,不怕别的线程来修改共享变量,如果改了就重试即可。 synchronized:最悲观的估计,得防着其他线程来修改共享变量,直接给代码上锁,等执行完解开锁了,其他线程才有机会执行。
原子操作类
juc(java.util.concurrent)包下提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、AtomicBoolean...,他们的底层就是使用CAS + volatile来实现的。
            
            
              java
              
              
            
          
          public class Demo05 {
    static AtomicInteger i = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 50000; ++j) {
                i.getAndIncrement();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 50000; ++j) {
                i.getAndDecrement();
            }
        });
        t1.start();
        t2.start();
        // 让主线程等待t1和t2两个子线程执行完毕后,再执行后续代码
        t1.join();
        t2.join();
        System.out.println(i);
    }
}synchronized优化
JVM中,每个对象都有对象头(包括class指针、Mark Word)。Mark Word平时存储这个对象的哈希码、分代年龄... ,当加锁时,这些信息就会被替换成标记位、线程锁记录指针、重量级指针、线程id...
轻量级锁
如果一个对象虽然有多个线程访问,但多线程访问的时间是错开的(没有竞争),那么可以用轻量级锁来优化。
这就类似于:学生A(线程A)用课本占座,短暂的离开教室了一下(时间片到)
- 回来发现课本没变(没有竞争),就会继续上课(仍然保持轻量级锁)
- 如果期间又来了一个学生B(线程B),就会告知学生A(线程A)此时有并发访问,线程A就会升级成重量级锁,进入重量级锁的流程。
每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word。
            
            
              java
              
              
            
          
          static Object obj = new Object();
public static void method1() {
	synchronized(obj) {
		// 同步块A
		method2();
	}
}
public static void method2() {
	synchronized(obj) {
		// 同步块B
	}
}| 线程1 | 对象Mark Word | 线程2 | 
|---|---|---|
| 访问同步块A,把MarkWord赋值到线程1的锁记录 | 01(无锁) | - | 
| CAS修改MarkWord为线程1锁记录 | 01(无锁) | - | 
| 成功(加锁) | 00(轻量级锁)线程1锁记录地址 | - | 
| 执行同步块A | 00(轻量级锁)线程1锁记录地址 | - | 
| 访问同步块B,把MarkWord赋值到线程1的锁记录 | 00(轻量级锁)线程1锁记录地址 | - | 
| CAS修改MarkWord为线程1锁记录 | 00(轻量级锁)线程1锁记录地址 | - | 
| 失败(发现是自己的锁) | 00(轻量级锁)线程1锁记录地址 | - | 
| 锁重入 | 00(轻量级锁)线程1锁记录地址 | - | 
| 执行同步块B | 00(轻量级锁)线程1锁记录地址 | - | 
| 同步块B执行完毕 | 00(轻量级锁)线程1锁记录地址 | - | 
| 同步块A执行完毕 | 00(轻量级锁)线程1锁记录地址 | - | 
| 成功(解锁) | 01(无锁) | - | 
| - | 01(无锁) | 访问同步块A,把MarkWord赋值到线程2的锁记录 | 
| - | 01(无锁) | CAS修改MarkWord为线程2锁记录 | 
| - | 00(轻量级锁)线程1锁记录地址 | 成功(加锁) | 
| ... | ... | ... | 
锁膨胀
在尝试加轻量级锁的过程中,CAS操作无法成功,这时如果其他线程为这个对象加上轻量级锁(有竞争),这时就需要进行锁膨胀,将轻量级锁变为重量级锁
            
            
              java
              
              
            
          
          static Object obj = new Object();
public static void method1() {
	synchronized(obj) {
		// 同步块
	}
}| 线程1 | 对象Mark Word | 线程2 | 
|---|---|---|
| 访问同步块,把MarkWord赋值到线程1的锁记录 | 01(无锁) | - | 
| CAS修改MarkWord为线程1锁记录 | 01(无锁) | - | 
| 成功(加锁) | 00(轻量级锁)线程1锁记录地址 | - | 
| 执行同步块 | 00(轻量级锁)线程1锁记录地址 | - | 
| 执行同步块 | 00(轻量级锁)线程1锁记录地址 | 访问同步块,把MarkWord赋值到线程2 | 
| 执行同步块 | 00(轻量级锁)线程1锁记录地址 | CAS修改MarkWord为线程2锁记录 | 
| 执行同步块 | 00(轻量级锁)线程1锁记录地址 | 失败(发现别人已经占了锁) | 
| 执行同步块 | 00(轻量级锁)线程1锁记录地址 | CAS修改Mark为重量级锁 | 
| 执行同步块 | 10(重量级锁)重量锁指针 | 阻塞中 | 
| 执行完毕 | 10(重量级锁)重量锁指针 | 阻塞中 | 
| 失败(解锁) | 10(重量级锁)重量锁指针 | 阻塞中 | 
| 释放重量锁,唤起阻塞线程竞争 | 10(重量级锁)重量锁指针 | 阻塞中 | 
| - | 10(重量级锁)重量锁指针 | 竞争重量锁 | 
| - | 10(重量级锁)重量锁指针 | 成功(加锁) | 
| ... | ... | ... | 
加重量级锁是为了后边唤醒的时候,根据重量级锁的指针唤醒阻塞中的线程。
重量级锁
重量级锁竞争时,可以使用自旋来进行优化,如果当时线程自旋成功(说明此时持有锁的线程已经退出同步代码块,释放锁),此时当前线程就可以避免阻塞,直接进入运行状态。
自旋锁是自适应的
- 对象刚刚的一次自选操作成功了,那么认为这次自旋成功的可能性会高,就会多自旋几次;
- 反之,就少自旋 或 不自旋
注意,自旋会占用CPU时间,只有多核的CPU才能发挥自旋的优势。
偏向锁
只有第一次使用CAS将线程ID设置到对象的Mark Word投,之后发现这个线程ID是自己的,就表示没有竞争,不用重新CAS。
其他优化
- 较少上锁时间:同步代码块中尽量短
- 减少锁的粒度:将一个锁拆分成多个锁提高并发度
- ConcurrentHashMap:每次只锁住了一个部分,其他读取操作不会受到影响。
- LongAdder:累加工具类,分为base和cells两部分
- 没有并发争用或cells数组正在初始化时,就会使用CAS来累加到base
- 有并发争用,就会初始化cells数组,数组有多少个cell,就允许多少线程并行修改,最后将数组中每个cell累加,再加上base就是最终的值
 
- LinkedBlockingQueue:出队和入队使用的就是不同的锁,相对于LinkedBlockingArray只有一个锁效率要高
- 锁粗化:StringBuffer的append方法都会调用synchronized来进行同步保护,如果不加以限制,那么下边这段代码会重复调用三次synchronized。JVM会将多次的append的加锁操作粗化为一次(因为都是一个对象加锁,没必要重入多次)
            
            
              java
              
              
            
          
          new StringBuffer().append("a").append("b").append("c");- 锁消除:JVM会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其他线程访问到,这时就会被即时编译器忽略掉所有同步操作。
- 读写分离:CopyOnWriteArrayList、CopyOnWriteSet(读原始数组的内容;写操作会复制一份,在新数组上进行写操作)