线程安全

2022-08-06 15:55:32 浏览数 (1)

Java 内存模型

多线程风险

在 Java 程序中,存储数据的内存空间分为共享内存和本地内存。线程在读写主存的共享变量时,会先将该变量拷贝一份副本到自己的本地内存,然后在自己的本地内存中对该变量进行操作,完成操作之后再将结果同步至主内存。

类型

存储介质

数据

特征

共享内存

主内存

存放变量

多线程共享

本地内存

CPU 高速缓存、缓冲区、寄存器以及其它硬件优化

临时存放线程使用的变量副本

使用期间其它线程无法访问

  • 优势:由于 CPU 执行速度明先快于内存读写速度,将运算需要的数据拷贝到 CPU 高速缓存中运算,可以大大加快程序运行速度。
  • 劣势:主内存数据和本地内存的不同步,导致多个线程同时操作主内存里的同一个变量时,变量数据可能会遭到破坏。
代码语言:javascript复制
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.start();
        t2.start();
    }
}

class MyThread implements Runnable {
    private int x = 0;                            // 对象中的数据由线程共享
    @Override
    public void run() {
        for (int i = 0; i < 10000; i  ) {
            x  ;
        }
        System.out.println("final x: "   x);     // 最后输出的数据不一定为 20000
    }
}Copy to clipboardErrorCopied

行为规范

JMM 定义了共享内存系统中多线程程序读写操作行为的规范,用来保证共享内存的原子性、可见性、有序性。

原子性

原子性是指一个操作,要么全部执行并且执行过程不会被打断,要么就都不执行。

  • Java 语言本身只保证了基本类型变量的读取和赋值是原子性操作。
  • 简单操作的原子性可以通过 Atomic 原子类实现。
  • 通过 synchronized 和 ReenTrantLock 等锁结构可以保证更大范围的原子性。
可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

  • Java 语言会尽可能保证主内存数据和本地内存同步,但仍可能出现不可见问题。
  • 通常用 volatile 关键字来保证可见性。
  • 通过 synchronized 和 ReenTrantLock 等锁结构在释放锁之前会将对变量的修改刷新到主存当中,也能够保证可见性。
有序性

有序性是指程序执行的顺序按照代码的先后顺序执行。

  • Java 内存模型具备先天的有序性。但 Java 允许编译器和处理器对指令进行重排序,可能影响多线程并发执行时的有序性。
  • 通过 synchronized 和 ReenTrantLock 等锁结构可以保证有序性。
  • volatile 关键字可以禁止 JVM 的指令重排,也可以保证有序性。

线程锁

互斥锁和自旋锁

  • 互斥锁

阻塞锁。当线程需要获取的锁已经被其他线程占用时,该线程会被直接挂起。直到其他线程释放锁,由操作系统激活线程。

适用于锁使用者保持锁时间比较长的情况,线程挂起后不再消耗 CPU 资源。

  • 自旋锁

非阻塞锁。当线程需要获取的锁已经被其他线程占用时,该线程会不断地消耗 CPU 的时间去试图获取锁。

适用于锁使用者保持锁时间比较短的情况,没有用户态和内核态调度、上下文切换的开销和损耗。

悲观锁和乐观锁

  • 悲观锁

每次读写资源时都会给资源上锁,其他线程想获取该资源时会被阻塞,直到其释放锁。

适用于写频繁的应用场景,写资源请求不会被一直驳回。synchronized 和 ReentrantLock 等独占锁都是悲观锁。

  • 乐观锁

读资源时不会给资源上锁,多个线程可以同时读取资源。写资源时会比对数据检查其他线程有没有更新过该资源,如果未更新就写入资源并更新版本号,否则写资源请求被驳回,重新读取并写资源。

适用于读频繁的应用场景,多线程同时读取能有效提高吞吐量。CAS 算法和版本号机制都是乐观锁,悲观锁的抢占也会利用 CAS 算法。

公平锁和非公平锁

  • 公平锁

加入到队列中等待唤醒,先到者先拿到锁。

公平锁不会出现线程饥饿,迟迟无法获取锁的情况。ReentrantLock 可以实现公平锁。

  • 非公平锁

当线程要获取锁时通过两次 CAS 操作去抢锁,如果没抢到加入到队列中等待唤醒。

非公平锁的性能更好。synchronized 是非公平锁,ReentrantLock 默认情况下也是非公平锁。

可重入锁

允许一个线程对同一对象多次上锁。由 JVM 记录对象被线程加锁次数,只有当线程释放掉所有锁(加锁次数为0)时,其他线程才获准进入。

synchronized 和 ReentrantLock 等锁结构都是可重入锁。

0 人点赞