1. 多线程高并发底层原理
先看一下计算机运行架构图:
由于cpu的运行程序速度远大于主存储的速度,所以会在主存RAM和CPU之间加多级高速缓存,缓存的速度接近cpu的运行速度,这样会大大提高计算机的运行速度。
1.1 java内存模型(JMM)
JMM即为JAVA 内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从java 5开始的JSR-133发布后,已经成熟和完善起来。
JMM规定了内存主要划分为主内存和工作内存两种。
主内存:保存了所有的变量。 共享变量:如果一个变量被多个线程使用,那么这个变量会在每个线程的工作内存中保有一个副本,这种变量就是共享变量。 工作内存:每个线程都有自己的工作内存,线程独享,保存了线程用到的变量副本(主内存共享变量的一份拷贝)。工作内存负责与线程交互,也负责与主内存交互。
此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的维度上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
JMM对共享内存的操作做出了如下两条规定:
线程对共享内存的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;
不同线程无法直接访问其他线程工作内存中的变量,因此共享变量的值传递需要通过主内存完成。
内存模型的三大特性:
原子性:即不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作是原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要使用同步技术(sychronized)或者锁(Lock)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
可见性:每个线程都有自己的工作内存,所以当某个线程修改完某个变量之后,在其他的线程中,未必能观察到该变量已经被修改。在 Java 中 volatile、synchronized 和 final 实现可见性。volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。
有序性:java的有序性跟线程相关。一个线程内部所有操作都是有序的,如果是多个线程所有操作都是无序的。因为JMM的工作内存和主内存之间存在延迟,而且java会对一些指令进行重新排序。volatile和synchronized可以保证程序的有序性,很多程序员只理解这两个关键字的执行互斥,而没有很好的理解到volatile和synchronized也能保证指令不进行重排序。
1.2 volatile关键字
Java语言规范第三版中对volatile的定义如下:Java语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排它锁单独获取这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
下面通过代码验证内存可见性、顺序性、原子性
import org.omg.PortableInterceptor.INACTIVE; /** * ClassName: Demo04 * Package: com.wangbaomin.juc.day03 * Description: 测试可见性、顺序性、原子性 * * @Author 王宝民 * @Create 2023/4/27 19:53 * @Version 1.0 */ public class Demo04 { //测试可见性(volatile可解决可见性问题) private static volatile int i = 0 ; public static void main1(String[] args) { //问题:当一个线程执行时不会发现其他线程中的同一个值被修改 //创建一个线程,嵌套一个循环如果循环内i的值发生变化才跳出循环 new Thread(()->{ System.out.println("任务执行中----"+ i); while (i==0){ } System.out.println("任务执行中----"+ i); }).start(); System.out.println("i+1之前:"+i); i++; System.out.println("i+1之后:"+i); } //测试顺序性(volatile可解决顺序性问题) static volatile int a,b,x,y; public static void main2(String[] args) throws InterruptedException { /* 需求分析:假设有四个变量a,b,x,y 初始值都是0 a=b=x=y=0 两个线程 线程1中:a=1,x=b 线程2中:b=1,y=a 存在的可能:1.(x=1,y=1) 2.(x=0,y=1) 3. (x=1,y=0) 161452,x=0,y=0 */ int count = 0; while (true) { count ++ ; a=b=x=y=0; Thread thread1 = new Thread(() -> { a = 1; x = b; }); Thread thread2 = new Thread(() -> { b = 1; y = a; }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(count+",x="+x+",y="+y); if (x==0&&y==0){ break; } } } //测试原子性(volatile不能解决原子性问题) (实测加锁可以解决原子性问题) //static volatile int sum = 0 ; static int sum = 0 ; public static void main(String[] args) { for (int i1 = 0; i1 < 100000; i1++) { new Thread(()->{ System.out.println(incr()); }).start(); } } private static synchronized int incr() { sum++; return sum; } }
1.3 volatile原理
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
当一个变量定义为 volatile 之后,将具备两种特性:
保证此变量对所有的线程的可见性。
禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障。
不保证变量的原子性
volatile 性能:volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
1.4 Happen-Before
在常规的开发中,如果我们通过上述规则来分析一个并发程序是否安全,估计脑壳会很疼。因为更多时候,我们是分析一个并发程序是否安全,其实都依赖Happen-Before原则进行分析。Happen-Before被翻译成先行发生原则,意思就是当A操作先行发生于B操作,则在发生B操作的时候,操作A产生的影响能被B观察到,“影响”包括修改了内存中的共享变量的值、发送了消息、调用了方法等。
Happen-Before的规则有以下几条:
程序次序规则(Program Order Rule):在一个线程内,程序的执行规则跟程序的书写规则是一致的,从上往下执行。
管程锁定规则(Monitor Lock Rule):一个Unlock的操作肯定先于下一次Lock的操作。这里必须是同一个锁。同理我们可以认为在synchronized同步同一个锁的时候,锁内先行执行的代码,对后续同步该锁的线程来说是完全可见的。
volatile变量规则(volatile Variable Rule):对同一个volatile的变量,先行发生的写操作,肯定早于后续发生的读操作
线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作
线程中止规则(Thread Termination Rule):Thread对象的中止检测(如:Thread.join(),Thread.isAlive()等)操作,必行晚于线程中所有操作
线程中断规则(Thread Interruption Rule):对线程的interruption()调用,先于被调用的线程检测中断事件(Thread.interrupted())的发生
对象中止规则(Finalizer Rule):一个对象的初始化方法先于一个方法执行Finalizer()方法
传递性(Transitivity):如果操作A先于操作B、操作B先于操作C,则操作A先于操作C
以上这些规则保障了happen-before的顺序,如果不符合以上规则,那么在多线程环境下就不能保证执行顺序等同于代码顺序。通过这些条件的判定,仍然很难判断一个线程是否能安全执行,线程安全多数依赖于工具类的安全性来保证。想提高自己对线程是否安全的判断能力,必然需要理解所使用的框架或者工具的实现,并积累线程安全的经验。
2. CAS
CAS:Compare and Swap。比较并交换的意思。CAS操作有3个基本参数:内存地址A,旧值B,新值C。它的作用是将指定内存地址A的内容与所给的旧值B相比,如果相等,则将其内容替换为指令中提供的新值C;如果不等,则更新失败。类似于修改登陆密码的过程。当用户输入的原密码和数据库中存储的原密码相同,才可以将原密码更新为新密码,否则就不能更新。
CAS是解决多线程并发安全问题的一种乐观锁算法。因为它在对共享变量更新之前,会先比较当前值是否与更新前的值一致,如果一致则更新,如果不一致则循环执行(称为自旋锁),直到当前值与更新前的值一致为止,才执行更新。
在JUC下有个atomic包,有很多原子操作的包装类:它们都是基于CAS解决并发安全问题的(类似在线教育的数据库记录的version版本号)
基本代码演示
import java.util.concurrent.atomic.AtomicInteger; /** * ClassName: Demo05 * Package: com.wangbaomin.juc.day03 * Description: 测试CAS * * @Author 王宝民 * @Create 2023/4/27 20:40 * @Version 1.0 */ public class Demo05 { public static void main1(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(2); System.out.println(atomicInteger.compareAndSet(2, 3));//t atomicInteger的值和预期值一样,修改成功 System.out.println(atomicInteger.compareAndSet(3, 5));//t atomicInteger的值和预期值一样,修改成功 System.out.println(atomicInteger.compareAndSet(6, 7));//f atomicInteger的值和预期值不一样,修改失败 System.out.println(atomicInteger.compareAndSet(5, 7));//t atomicInteger的值和预期值一样,修改成功 } //利用AtomicInteger测试原子性 public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(); for (int i1 = 0; i1 < 100000; i1++) { new Thread(()->{ System.out.println(atomicInteger.incrementAndGet()); }).start(); } } }
底层原理:
Unsafe类是CAS的核心类,提供硬件级别的原子操作(目前所有CPU基本都支持硬件级别的CAS操作)。
// 对象、对象的属性地址偏移量、预期值、修改值 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
Unsafe简单demo:
需求:模拟AtomicInteger实现更新UnsafeDemo的number属性的并发修改安全
步骤:
1、获取Unsafe对象
2、调用Unsafe的compareAndSwapInt实现更新UnsafeDemo的number属性
import sun.misc.Unsafe; import java.lang.reflect.Field; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; /** * ClassName: Demo06 * Package: com.wangbaomin.juc.day03 * Description: CAS测试 * * @Author 王宝民 * @Create 2023/4/27 20:55 * @Version 1.0 */ public class Demo06 { //测试 public static void main1(String[] args) { /* 需求:运用AtomicInteger实现以下需求 创建1000个线程,在线程内实现一个AtomicInteger类进行随机比较线程方法中随机生成的变量 */ for (int i = 0; i < 1000; i++) { String a = i + "" ; new Thread(()->{ AtomicInteger atomicInteger = new AtomicInteger(new Random().nextInt(1000)); boolean b = atomicInteger.compareAndSet(new Random().nextInt(1000), new Random().nextInt(1000)); if (b){ System.out.println("次数-->"+a+"幸运数字"+atomicInteger); } }).start(); } } //参考AtomicInteger实现一个原子性操作的类 public static void main(String[] args) { MyAtomicInteger myAtomicInteger = new MyAtomicInteger(); new Thread(()->{ boolean b = myAtomicInteger.compareAndSet(0, 2); System.out.println(b); }).start(); } } class MyAtomicInteger{ private volatile int value ; private Unsafe unsafe; private Long valueOffset; { try { //1.获取运行时类 Class<?> clazz = Class.forName("sun.misc.Unsafe"); //2.获取属性 Field field = clazz.getDeclaredField("theUnsafe"); //3.将已声明的属性设置为true field.setAccessible(true); //4.获取对象 unsafe = (Unsafe)field.get(null); valueOffset = unsafe.objectFieldOffset (MyAtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } public boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } }
CAS 缺点
开销大:在并发量比较高的情况下,如果反复尝试更新某个变量,却又一直更新不成功,会给CPU带来较大的压力
ABA问题:当变量从A修改为B再修改回A时,变量值等于期望值A,但是无法判断是否修改,CAS操作在ABA修改后依然成功。
不能保证代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。
3. ABA问题的解决
AtomicStampedReference:版本号原子引用
AtomicStampedReference在构建的时候需要一个类似于版本号的int类型变量stamped,每一次针对共享数据的变化都会导致该 stamped 的变化(stamped 需要应用程序自身去负责,AtomicStampedReference并不提供,一般使用时间戳作为版本号)
public static void main(String[] args) { AtomicStampedReference<String> r = new AtomicStampedReference<String>("a",1); System.out.println("修改前版本号:"+r.getStamp()+" ,值: "+ r.getReference()); r.compareAndSet("a","b",1,2); System.out.println("第一次修改后版本号:"+r.getStamp()+" ,值 "+ r.getReference()); r.compareAndSet("b","a",1,3); System.out.println("第二次修改后版本号:"+r.getStamp()+" ,值 "+ r.getReference()); }
运行结果:
修改前版本号:1 ,值: a
第一次修改后版本号:2 ,值 b
第二次修改后版本号:2 ,值 b
4. AQS
AbstractQueuedSynchronizer抽象队列同步器简称AQS,它是实现同步器的基础组件(框架),juc下面Lock的实现以及一些并发工具类(Semaphore、CountDownLatch、CyclicBarrier等)就是通过AQS来实现的。具体用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类。
4.1. 框架结构
AQS框架结构如下:
AQS维护了一个volatile语义(支持多线程下的可见性)的共享资源变量state和一个FIFO(first-in-first-out)线程等待队列(多线程竞争state资源被阻塞时,会进入此队列)。
4.2. 基于AQS实现锁的思路
AQS将大部分的同步逻辑均已经实现好,继承的自定义同步器只需要实现state的获取(acquire)和释放(release)的逻辑代码就可以,主要包括下面方法:
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
也就是说:
通过AQS可以实现独占锁(只有一个线程可以获取到锁,如:ReentrantLock),也可以实现共享锁(多个线程都可以获取到锁Semaphore/CountDownLatch等)
4.3. 基于AQS实现独占锁
jdk官方文档给出了使用案例:
把jdk文档中的案例copy到工程中就可以直接使用
public class AqsDemo {
public static void main(String[] args) throws InterruptedException {
DataThree dataThree = new DataThree();
CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
new Thread(() -> {
dataThree.incr();
countDownLatch.countDown();
}, "").start();
}
countDownLatch.await();
System.out.println(dataThree.getNum());
}
}
class DataThree {
private volatile int num;
Mutex mutex = new Mutex();
public void incr(){
mutex.lock();
for (int i = 0; i < 1000; i++) {
num++;
}
mutex.unlock();
}
public int getNum(){
return num;
}
}
4.4. ReentrantLock底层原理
以ReetrantLock为例,说明AQS在锁底层的应用。
在ReentrantLock类中包含了3个AQS的实现类:
抽象类Sync
非公平锁实现类NonfaireSync
公平锁实现类FairSync
在ReetrantLock的源码中可以发现:
4.4.1. Sync抽象类
/** * 自定义方法:为非公平锁的实现提供快捷路径 */ abstract void lock(); /** * 自定义通用方法,两个子类的tryAcquire方法都需要使用非公平的trylock方法 */ final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 如果当前线程没有获取到锁 if (compareAndSetState(0, acquires)) { // 则CAS获取锁 setExclusiveOwnerThread(current); // 并把当前线程设置为拥有排他访问权限 return true; } } else if (current == getExclusiveOwnerThread()) { // 如果当前线程已经拥有锁,则重入 int nextc = c + acquires; // 每重入一次stat累加acquires if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } /** * 实现AQS的释放锁方法 */ protected final boolean tryRelease(int releases) { int c = getState() - releases; // 每释放一次stat就减releases if (Thread.currentThread() != getExclusiveOwnerThread()) // 当前线程不是有锁线程抛异常 throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { // stat减为0则释放锁 free = true; setExclusiveOwnerThread(null); } setState(c); return free; } protected final boolean isHeldExclusively() { // While we must in general read state before owner, // we don't need to do so to check if current thread is owner return getExclusiveOwnerThread() == Thread.currentThread(); }
4.4.2. NonfairSync
/** * Sync object for non-fair locks */ static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) // CAS把stat设置为1 setExclusiveOwnerThread(Thread.currentThread()); // 获取到锁 else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); // 使用了Sync抽象类的nonfairTryAcquire方法 } }
acquire(1)方法是AQS自己实现的本质就是调用tryAcquire方法,如果tryAcquire获取到锁并无法进入等待队列则中止线程。
4.4.3. FairSync
/** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && // 从线程有序等待队列中获取等待 compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 可重入 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
hasQueuedPredecessors具体实现如下:
当等待队列只有一个线程时,直接获取到锁
如果队列不止一个线程,并且下一个线程就是当前申请锁的线程,则获取锁