Java并发
大约 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 线程生命周期
2. 锁和同步
2.1 乐观锁和悲观锁
- 悲观锁
- 共享内存每次访问都会出现问题(如被修改), 每次只给一个线程使用,其它线程阻塞
- 实现: synchronized、ReentrantLock
- 使用: 写多读少
- 缺点: 线程获取锁的顺序不当时,容易死锁;高并发下,大量阻塞线程导致上下文切换过多,增加系统开销
- 乐观锁
- 共享内存每次访问不会出现问题, 提交修改的时候需要验证是否已被修改
- 实现:
- 比较与交换(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保证了可见性、原子性和有序性,但并不保证指令重排。可以修饰实例方法、静态方法和代码块
- 修饰实例方法:给对象实例加锁
synchronized void method() {
}
- 修饰静态方法:给对象的类加锁
synchronized static void method() {
}
- 修饰代码块:给括号内的**对象(Object)、类(class)**加锁
synchronized(this) {
}