跳至主要內容

Java并发

pptg大约 3 分钟

1. 常见概念

1.1 用户态和内核态

  • 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用);创建、切换成本低,但不能利用多核
  • 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问);创建、切换成本高,可以利用多核

常见的线程模型:

  • 一对一(一个用户线程对应一个内核线程)
  • 多对一(多个用户线程映射到一个内核线程)
  • 多对多(多个用户线程映射到多个内核线程)

Java大多数采用的一对一,但Solaris系统中也支持多对多

JDK1.2之前,Java线程基于绿色线程(Green Threads)自己模拟了多线程的运行,是一种用户级线程,不依赖操作系统。同时,也因此不能使用OS提供的异步IO功能、无法利用多核。

1.2 Java创建线程

一般来说,创建线程有很多种方式,例如继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture类等等。 严格来说,Java 就只有一种方式可以创建线程,那就是通过new Thread().start()创建。不管是哪种方式,最终还是依赖于new Thread().start()

1.3 线程生命周期

线程生命周期.png
线程生命周期.png

2. 锁和同步

2.1 乐观锁和悲观锁

  1. 悲观锁
    • 共享内存每次访问都会出现问题(如被修改), 每次只给一个线程使用,其它线程阻塞
    • 实现: synchronized、ReentrantLock
    • 使用: 写多读少
    • 缺点: 线程获取锁的顺序不当时,容易死锁;高并发下,大量阻塞线程导致上下文切换过多,增加系统开销
  2. 乐观锁
    • 共享内存每次访问不会出现问题, 提交修改的时候需要验证是否已被修改
    • 实现:
      • 比较与交换(Compare And Swap, CAS): 用一个预期值和要更新的变量值进行比较,两值相等才会进行更新, AtomicInteger、LongAdder
      • 版本号: 数据表中添加version字段,修改时version++;当修改提交时,当前version与读取时相同才会修改,否则会重试
    • 使用: 写少读多的单个共享变量
    • 缺点: 不存在线程阻塞问题,但如果修改冲突频繁发生,会导致频繁重试(自旋)

2.2 volatile

2.2.1 可见性

volatile指示JVM,当前变量是共享且不稳定的,每次都需要到主存中读取, volatile保证了可见性,但不具备原子性。volatile通知JVM当前变量是不稳定的,每次使用的时候都需要去主存重新读取

2.2.2 禁止指令重排

变量被声明为volatile之后,会通过插入特定的内存屏障的方式来禁止指令重排序。例如在DoubleCheck的单例模式时,对单例对象添加了volatile

public class Singleton {
    public volatile static Singleton s = null;
    
    private Singleton(){}
    
    public static Singleton getInstace(){
        if(s == null){
            synchronized (Singleton.class) {
                if(s == null) {
                    s = new Singleton();
                }
            }
        }
        return s;
    }
}

此处的s = new Singleton()分为三步:

  • 为 s 分配内存空间
  • 初始化 s
  • 将 s 指向分配的内存地址

此时如果2和3发生重排,那么就导致在多线程环境下拿到了s,但s并没有进行初始化

2.1 Synchronized

Synchronized保证了可见性、原子性和有序性,但并不保证指令重排。可以修饰实例方法静态方法代码块

  1. 修饰实例方法:给对象实例加锁
synchronized void method() {
}
  1. 修饰静态方法:给对象的类加锁
synchronized static void method() {
}
  1. 修饰代码块:给括号内的**对象(Object)、类(class)**加锁
synchronized(this) {
}