侧边栏壁纸
博主头像
Dioxide-CN博主等级

茶边话旧,看几许星迢露冕,从淮海南来。

  • 累计撰写 54 篇文章
  • 累计创建 30 个标签
  • 累计收到 24 条评论

目 录CONTENT

文章目录

线程的安全性分析

Dioxide-CN
2023-01-08 / 0 评论 / 5 点赞 / 34 阅读 / 3,793 字
温馨提示:
本文最后更新于 2023-01-08,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

线程的安全性分析

Java内存模型

Java 内存模型是一种抽象结构,它提供了合理的禁用缓存以及禁止重排序的方法来解决可见性、有序性问题。

image-1673188310178

JMM 的抽象模型

线程A工作内存线程B工作内存共享变量 主内存

JMM和硬件模型的对应简图

image-1673188325105

同步关键字synchronized

synchronized锁的范围

  • 对于普通同步方法,锁是当前的实例对象(锁家里的门)
public class Main {  
    // 对象锁 同一个对象访问同步方法才有效  
    public synchronized void demo() {}
      
    public static void main(String[] args) {  
        Main main1 = new Main();  
        Main main2 = new Main();  
        Main main3 = new Main();  
        // 无法实现两个线程的互斥 均可以执行  
        new Thread(main1::demo).start(); // 锁的是对象main1
        new Thread(main2::demo).start(); // 锁的是对象main2
        // 下面这种情况才会发生互斥 使第二个方法发生线程阻塞  
        new Thread(main3::demo).start(); // 锁的是对象main3
        new Thread(main3::demo).start(); // 发生阻塞
    }  
}
  • 对于静态同步方法,锁是当前类的 class 对象(锁小区的门)
public class Main {
    // 类级别锁 Main.class
    public static synchronized void demo() {}
	
    public static void main(String[] args) {
        Main main1 = new Main();
        Main main2 = new Main();
        // 访问静态同步方法会发生线程互斥
        new Thread(() -> main1.demo()).start();
        new Thread(() -> main2.demo()).start();
    }
}
  • 对于同步方法块,锁是 synchronized 括号里配置的对象(锁的范围是可控的)
public class Main {
	// 方法内加同步代码块
    public void demo() {  
        // TODO  
        synchronized (Main.class) {  
            // 等价于 static synchronized void demo()
		}  
        // TODO  
        synchronized (this) {  
            // 等价于 synchronized void demo()
		}  
        // TODO  
    }  
	  
    public static void main(String[] args) {  
        Main main = new Main();  
        // 调用方法时只会对方法内的同步代码块进行加锁同步  
        new Thread(main::demo).start();  
    }  
}

synchronized的本质

public void demo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: ldc           #7      // class Main
         2: dup
         3: astore_1
         4: monitorenter          // 获得锁
         5: aload_1
         6: monitorexit           // 释放锁
         7: goto          15
        10: astore_2
        11: aload_1
        12: monitorexit           // 异常情况下释放锁
	......

image-1673188344690

synchronized的优化

在 jdk1.6 以前 synchronized 是一种重量锁对性能开销比较大,1.6 后对 synchronized 进行了如下优化措施:

  1. 引入自适应自旋锁
  2. 引入偏向锁、轻量级锁
  3. 引入锁消除、锁粗化概念

并发编程问题的源头:原子性、可见性、有序性

如何理解线程安全

当多个线程访问某个对象时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为(符合我们的预期和结果),那么就称这个类是线程安全的。

image-1673188357723

原子性

public class Main {
    static int count = 0;
    static void incr() {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        /**
         * getstatic
         * iadd
         * putstatic
         */
        count++;
    }
	
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(Main::incr).start();
        }
        Thread.sleep(4000);
        System.out.println("result : " + count);
    }
}

输出的结果一定是一个 <= 1000 的值,原因就是在于原子性问题,解决方案为使用原子类

static void incr();
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=3, locals=1, args_size=0
         0: lconst_1
         1: invokestatic  #7                  // Method java/lang/Thread.sleep:(J)V    
         4: goto          17
         7: astore_0
         8: new           #15                 // class java/lang/RuntimeException      
        11: dup
        12: aload_0
        13: invokespecial #17                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/Throwable;)V
        16: athrow
        17: getstatic     #20                 // Field count:I
        20: iconst_1
        21: iadd
        22: putstatic     #20                 // Field count:I
        25: return
......

image-1673188373541

原子性问题的本质是缓存的不一致性和线程竞争

可见性

传统计算机架构优化成果:

  1. CPU 增加了高速缓存,均衡与内存的速度差异
  2. 操作系统增加进程、线程、以及分时复用 CPU,均衡 CPU 与 I/O 设备的速度差异
  3. 编译程序优化指令的执行顺序,使得能够更加合理地利用缓存

问题的源头:在多核 CPU 时代,每个 CPU 都有自己的高速缓存,那么此时 CPU 的缓存与内存之间的速度差异导致数据一致性难以解决。当多个线程在不同 CPU 上执行的时候,线程操作的是不同的 CPU 缓存。如下图,线程 A 操作 CPU-1 内的变量 X 对于线程 B 操作 CPU-2 内的变量 X 就不具备可见性。

线程 A[变量X] CPU-1线程 B[变量X] CPU-2[变量X] 内存
public class Main {  
    static boolean stop = false;  
	  
    public static void main(String[] args) throws InterruptedException {  
        new Thread(() -> {  
            int i = 0;  
            while (!stop) {  
                i++;  
            }  
        }).start();  
        System.out.println("begin start thread!");  
        Thread.sleep(1000);  
        stop = true; // 目的是主线程中修改stop的值使得线程停止  
    } // 结果是无法修改 原因是对子线程不可见
} // 解决方法 使用 volatile 修饰变量

有序性

image-1673188388967

其中 1 是编译器级别的重排序,2 和 3 是处理器级别的重排序,会导致多线程程序出现执行顺序问题

volatile关键字分析

volatile 可以用来解决可见性和有序性问题,通过汇编指令分析可以发现 volatile 会给操作加上 Lock 指令(0x000000000372caf3: lock add dword ptr [rsp], 0h),而 Lock 指令的作用包括:

  1. 将当前处理器缓存行的数据写回到系统内存
  2. 这个写回内存内存的操作会使在其他 CPU 里缓存了该内存地址的数据失效

本质是使用禁用缓存的机制来解决可见性问题,从软件层面调用硬件层面的指令。但是 volatile 不能保证原子性

CPU 层面的内存屏障

  • Store Barrier:强制所有在 store 屏障指令之前的 store 指令,都在该 store 屏障指令执行之前被执行,并把 store 缓冲区的数据都刷到 CPU 缓存
  • Load Barrier:强制所有在 load 屏障指令之后的 load 指令,都在该 load 屏障指令执行之后被执行,并且一直等到 load 缓冲区被该 CPU 读完才能执行之后的 load 指令
  • Full Barrier:全屏障,复合了 load 和 store 屏障的功能
value = 3;
void exec2CPU0() {
	value = 10;
	storeBarrier(); // 写屏障 之前的指令一定在之后执行 禁止了指令重排序
	isFinish = true; // 此时 value = 10 一定成立
}
void exec2CPU1() {
	if (isFinish) {
		loadBarrier(); // 读屏障
		assert value == 10;
	}
}

image-1673188409179

volatile总结

本质上来说,volatile 实际上就是通过内存屏障来防止指令重排序以及禁止 CPU 高速缓存来解决可见性问题。
而 #Lock 指令,它本意上是禁止高速缓存解决可见性问题,但实际上在这里,它表示的是一种内存屏障的功能。也就是说针对当前的硬件环境,JMM 层面采用 Lock 指令作为内存屏障来解决可见性问题。

final域

概念

final 在 Java 中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量。一旦你将引用声明为 final,你将不能再改变这个引用。

final域和线程安全

对于 final 域,编译器和处理器要遵守两个重排序规则:

  1. 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
  2. 初次一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序
public class FinalExample {
	int i;                              /* 普通变量 */
	final int j;                        /* final变量 */
	static FinalExample obj;
	public FinalExample() {             /* 构造函数 */
		i = 1;                          /* 写普通域 */
		j = 2;                          /* 写final域 */
	}
	public static void writer() {       /* 写线程A执行 */
		obj = new FinalExample();
	}
	public static void reader() {       /* 读线程B执行 */
		FinalExample object = obj;      /* 读对象引用 */
		int a = object.i;               /* 读普通域 */
		int b = object.j;               /* 读final域 */
	}
}

写 final 域的重排序规则:

  1. JMM 禁止编译器把 final 域的写重排序到构造函数之外
  2. 编译器会在 final 与的写之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数之外

image-1673188428307

读 final 域的重排序规则:

  1. 在一个县城中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作,编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障

image-1673188440420

逃逸带来的重排序问题

public class Main {
	final int i;
	static Main obj;
	
	public Main() {
		i = 1;                       // 1. 写final域
		obj = this;                  // 2. this 引用逃逸
	}
	public static void writer() {
		new Main();
	}
	public static void reader() {
		if (obj != null) {           // 3. 
			int temp = obj.i;        // 4. 
		}
	}
}

image-1673188456088

Happens-Before规则

Happens-Before 是一种可见性规则,它表达的含义是前面一个操作的结果对后续操作是可见的。

六大规则和性质:

  1. 程序顺序规则(单线程规则 as-if-serial)
    • 解释:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
    • 同一个线程中前面的所有写操作对后面的操作可见。
    class VolatileExample {
    	int x = 0;
    	volatile boolean v = false;
    	public void writer() {
    		x = 42;
    		v = true;
    	}
    	public void reader() {
    		if (v == true) {
    		}
    	}
    }
    
  2. 监视器锁规则(synchronized, Lock等)
    • 解释:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
    • 如果线程1解锁了 monitor a,接着线程2锁定了 monitor a,那么,线程1解锁a之前的写操作对线程2都可见(线程1和线程2可以是同一个线程)。
    synchronized (this) { // 此处自动加锁
    	// x 是共享变量,初始值是 10
    	if (this.x < 12) {
    		this.x = 12;
    	}
    } // 此处自动解锁
    
  3. volatile 变量规则
    • 解释:堆一个 volatile 域的写操作,happens-before 于任意后续对这个 volatile 域的读操作。
    • 如果线程1写入了 volatile 变量 v(临界资源),接着线程2读取了 v,那么,线程1写入 v 及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)。
  4. start() 规则
    • 解释:如果线程 A 执行启动线程 B 的操作 ThreadB.start(),那么 A 线程的 ThreadB.start() 操作 happens-before 于线程 B 中的任意操作。
    • 假定线程 A 在执行过程中,通过执行 ThreadB.start() 来启东线程 B,那么线程 A 对共享变量的修改在接下来线程 B 开始执行前对线程 B 可见。主意:线程 B 启动之后,线程 A 再对变量进行修改的操作对线程 B 未必可见。
    Thread B = new Thread(() -> {
    	// 主线程调用 B.start() 之前
    	// 所有对共享变量的修改,此处皆可见
    	// 此案例中 var == 77
    });
    // 此处对共享变量 var 进行修改
    var = 77;
    // 主线程启动子线程
    B.start();
    
  5. join() 规则
    • 解释:如果线程A执行操作 ThreadB.join() 并成功返回,那么线程B中的任意操作都 happens-before 于线程A从 ThreadB.join() 操作成功返回。
    • 线程A写入的所有变量,在任意其他线程B调用 A.join(),或者 A.isAlive() 成功返回后,都对 B 可见。
    Thread B = new Thread(() -> {
    	// 此处对共享变量 var 进行修改
    	var = 66;
    });
    // 例如此处对共享变量修改
    // 则这个修改结果对线程 B 可见
    // 主线程启动子线程
    B.start();
    B.join();
    // 子线程所有对共享变量的修改
    // 在主线程调用 B.join() 之后皆可见
    // 此案例中 var == 66
    
  6. 传递性
    • 解释:如果 A happens-before B,且 B happens-before C,那么 A happens-before C,即 (A h-b B) + (B h-b C) = (A h-b C)
    class VolatileExample {
    	int x = 0;
    	volatile boolean v = false;
    	public void writer() {
    		x = 42;    // happens-before v = true
    		v = true;  // happens-before v == true
    	}
    	public void reader() {
    		if (v == true) {
    			// 由传递性规则 x 一定等于 42
    		}
    	}
    }
    

原子类Atomic

原子性问题的解决的方案:

  1. synchronized、Lock
  2. JUC 包下的 Atomic 类

Atomic的实现原理

  • Unsafe 类:增强了 Java 底层操作的能力
private static final Unsafe U = Unsafe.getUnsafe();  
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;
@IntrinsicCandidate  
public final int getAndAddInt(Object o, long offset, int delta) {  
    int v;  
    do {  
        v = getIntVolatile(o, offset);  
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));  
    return v;  
}
  • CAS:compareAndSwapInt(object, offset, expect, update) 本质是乐观锁,如果相等就执行修改,不相等就不执行修改。也是不断去内存中取值,再不断地去内存中进行旧值的比较,修改失败就进入下一次循环。

Atomic分类

  1. 原子更新基本类型
  2. 原子更新数组
  3. 原子更新引用类型
  4. 原子更新字段类
5

评论区