【Java难点】多线程-终极

Java内存模型之JMM

为什么需要JMM

计算机存储结构:从本地磁盘到主存到CPU缓存,也就是从硬盘到内存,到CPU。一般对应的程序的操作就是从数据库查数据到内存然后到CPU进行计算。

image-20240508225728103

CPU和物理主内存的速度不一致,所以设置多级缓存,CPU运行时并不会直接操作内存,当CPU读取数据时,先把内存里边的数据读到缓存,然后再从缓存中读取;当CPU写出数据时,先把数据写到缓存中,然后缓存再写到内存中。

JVM规范中定义了一种Java内存模型 (java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果

image-20240508231033593

JMM的作用:

  1. 通过JMM来实现线程和主内存之间的抽象关系。

  2. 屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果

什么是JMM

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式,并决定一个线程对共享变量的写入何时可用,以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性可见性有序性展开的。

JMM规范下的三大特性
可见性

可见性是一种即时通知机制,当一个线程修改了某一个共享变量的值,其他线程能够立即知道该变更

JMM规定了所有的变量都存储在主内存中。

image-20240508232744054

系统主内存共享变量数据修改时被写入的时间是不确定的,多线程并发下很可能出现"脏读",所以每个线程都有自己的工作内存(线程私有),线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

image-20240508233358671

线程读取变量过程:

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取賦值等)必须在工作内存中进行。首先要将变量从主内存拷贝到线程自己的工作内存空问,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

image-20240509204132072

线程和主内存之间的关系:

  1. 线程之间的共享变量存储在主内存中(从硬件角度来说就是内存条)
  2. 每个线程都有一个私有的本地工作内存,木地工作内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存等)

线程脏读:

image-20240508233904199

原子性

指一个操作是不可打断的,即多线程环境下,操作不能被其他线程干扰

有序性

重排序:

对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。但是处理器在进行重排序时必须要考虑指令之间的数据依赖性

重排序的优缺点:

优:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能

缺: 但是,指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致(即可能产生"脏读")。简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。

从源码到最终执行示例图:

image-20240508234923953

单线程环境里面可以保证程序最终执行结果与顺序执行的结果一致。但是,在多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

happens-before

Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A发生过的事情对B来说是可见的,无论 A事件和B事件是否发生在同一个线程里。

JMM的设计分为两部分:

一部分是面向我们程序员提供的,也就是happens-before规则,它通俗易懂的向我们程序员阐述了一个强内存模型,我们只要理解happens-before规则,就可以编写并发安全的程序了。

另一部分是针对JVM实现的,为了尽可能少的对编译器和处理器做约束从而提高性能,JMM在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。

我们写代码时,只需要关注前者就好了,也就是理解happens before规则即可,其它繁杂的内容有JMM规范结合操作系统给我们搞定,我们只写好代码即可。

8条规则:

  1. 次序规则:在同一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作。
  2. 锁定规则: 一个unlock操作先行发生于后面(这里的“后面”是指时间上的先后)对同一个锁的lock操作。

image-20240509210053471

  1. volatitle变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。

  2. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。

  3. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。

  4. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;可以通过Thread.interrupted()检测到是否发生中断,也就是说你要先调用interrupt()方法设置过中断标志位,我才能检测到中断发送。

  5. 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过isAlive()等手段检测线程是否己经终止执行。

  6. 对象终结规则:一个对象的初始化完成(构造函数执行结東)先行发生于它的finalize()方法的开始。

案例:

image-20240509213004706

问:假设存在线程A和B,线程A先(时间上的先后)调用了setValue(),然后线程B调用了同一个对象的getValue),那么线程B收到的返回值是什么?

答:不一定

原因:

我们就这段简单的代码一次分析happens-before的规则(规则5、6、7、8可以忽略,因为他们和这段代码毫无关系):

  1. 由于两个方法是由不同的线程调用,不在同一个线程中,所以肯定不满足规则1(次序规则);
  2. 两个方法都没有使用锁,所以不满足规则2(锁定规则);
  3. 变量不是用volatile修饰的,所以不满足规则3(volatile变量规则)
  4. 规则4(传递规则)肯定不满足;

所以我们无法通过happens-before 原则推导出线程A happens-before线程B,虽然可以确认在时间上线程A优先于线程B执行,但就是无法确认线程B获得的结果是什么,所以这段代码不是线程安全的。

那么怎么修复这段代码呢?

修复1:把getter/setter方法都定义为synchronized方法

image-20240509214228484

修复2:把value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字使用场景

image-20240509214521121

volatile与JMM

**volatile变量的2大特点:**可见性、有序性

  1. 可见性

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。

当读一个volavle变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量。

所以volatile的写内存语义是写完后立即刷新回主内存并及时发出通知,大家可以去主内存拿最新版,前面的修改对后面所有线程可见。

  1. 有序性

禁止编译器指令重排

volatile凭什么可以保证可见性和有序性???

内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性。

内存屏障之前的所有写操作都要回写到主内存,

内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。

image-20240509223835161

因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读,也叫写后读。

内存屏障粗分为2种:

读屏障:在读指令之前插入读屏障,让工作内存或CPU高速级存当中的缓存数据失效,重新回到主内存中获取最新数据

写屏障:在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中

内存屏障细分为4种:

image-20240509225117536

happens-before之volatile变量规则:

image-20240509225745692

image-20240509230430344

image-20240509230402081

volatile之可见性案例
  • 不使用volatile
import java.util.concurrent.TimeUnit;

public class JUC08 {
    static boolean flag=true;
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t -----come in");
            while(flag){}
            System.out.println(Thread.currentThread().getName()+"\t -----flag被设置为false,程序停止");
        },"t1").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        flag=false;
        System.out.println(Thread.currentThread().getName()+"\t -----修改完成:"+flag);
    }
}

image-20240509231747572

线程t1中为何看不到被主线程main修改为false的flag的值?

  1. 可能主线程修改了flag之后没有将其刷新到主内存,所以1线程看不到。

  2. 可能主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中更新获取flag最新的值。

  • 使用volatile
import java.util.concurrent.TimeUnit;

public class JUC08 {
    static volatile boolean flag=true;
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t -----come in");
            while(flag){}
            System.out.println(Thread.currentThread().getName()+"\t -----flag被设置为false,程序停止");
        },"t1").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        flag=false;
        System.out.println(Thread.currentThread().getName()+"\t -----修改完成:"+flag);
    }
}

image-20240509231846119

volatile之原子性案例
  • volatile不能保证原子性
import java.util.concurrent.TimeUnit;

public class JUC08 {
    public static void main(String[] args) {
        MyNumber myNumber = new MyNumber();
        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    myNumber.addPlusPlus();
                }
            },String.valueOf(i)).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(myNumber.number);
    }
}
class MyNumber{
    volatile int number;
    public void addPlusPlus(){
        number++;
    }
}

image-20240510002354034

  • 使用synchronized保证原子性
import java.util.concurrent.TimeUnit;

public class JUC08 {
    public static void main(String[] args) {
        MyNumber myNumber = new MyNumber();
        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    myNumber.addPlusPlus();
                }
            },String.valueOf(i)).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(myNumber.number);
    }
}
class MyNumber{
    int number;
    public synchronized void addPlusPlus(){
        number++;
    }
}

image-20240510002544423

volatile不能保证原子性的原因:

假设A线程中i=1时,读取到number为5,B线程i=1时,也读取到number为5,A线程对number进行++,A线程的工作内存中number=6,此时B线程刚好也执行完number++,此时B线程的工作内存中number=6,假设B线程正要将number=6写入主内存时,A线程先一步将number=6写入主内存,此时主内存中number=6,B线程感知到主内存中number发生了改变,B线程将会把自己工作内存中的number=6丢掉,下一次读取主内存中的number时会将从主内存中读取最新的值,B线程执行myNumber.addPlusPlus();下面的代码,而myNumber.addPlusPlus();下面没有代码,所以B线程进入下一次循环,此时B线程i=2,从主内存中读取最新的number=6,但是B线程i=1时的并没有有效的将number++,所以最终的number一定会小于10000。

image-20240510000141383

详见《深入理解Java虚拟机》12.3.3节

注意:

  1. volatile变量不适合参与到依赖当前值的运算中,如i=i+1;i++之类的,volatile通常用做保存某个状态的boolean值或int值。
  2. 由于volatile变量只能保证可见性,所以我们仍然要通过加锁(使用synchronized、 java.util.concurrent中的锁或原子类)来保证原子性。
volatile之禁重排案例

若存在数据依赖关系则禁止重排序===>重排序发生,会导致程序运行结果不同。

数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性。

编译器和处理器在重排序时,会遵守数据依赖性,不会改变存在依赖关系的两个操作的执行,但不同处理器和不同线程之间的数据性不会被编译器和处理器考虑,其只会作用于单处理器和单线程环境,下面三种情况,只要重排序 两个操作的执行顺序,程序的执行结果就会被改变

image-20240510205725732

案例

image-20240510211740759

image-20240510211538446

volatile使用场景
  • 作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束

image-20240510212529394

  • 当读远多于写,结合使用內部锁和volatile 变量来减少同步的开销

image-20240510212501101

  • DCL双端锁的发布

image-20240510213203961

总结

添加volatile关键字后,JVM为什么会加入内存屏障?

image-20240510213708543

CAS

CAS原理

定义:

CAS是compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数:内存位置、预期原值及更新值。

执行CAS操作的时候,将内存位置的值与预期原值比较:如果相匹配,那么处理器会自动将该位置值更新为新值,如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。

CAS时一种系统原语,原语属于操作系统用语范畴,是由若干指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题

原子类:

java.util.concurrent.atomic包下的所有类,原子类是CAS思想的落地。

实例:

CAS有3个操作数:V、A、B,其中V:要修改属性所在的内存地址,A:旧的预期值,B:修改后的新值。当且仅当旧的A和V对应的属性值相同时,才将V对应的属性值修改为B,否则什么都不做或重试。它重试的这种行为称为----自旋!!

CAS硬件级别的保证原子性:

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,
比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好

案例:

import java.util.concurrent.atomic.AtomicInteger;

public class JUC08 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5, 2022)+"\t"+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 2023)+"\t"+atomicInteger.get());
    }
}

image-20240510223541883

源码:

image-20240510224346539

compareAndSet()方法的源代码:

image-20240510224241652

UnSafe类

UnSafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地 (native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,Java中CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe 类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。

image-20240510225516902

变量valueOfset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

AtomicInteger的incrementAndGet方法:

image-20240510230923094

image-20240510231111339

实例

image-20240510231804975

原子引用(AtomicReference)
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.concurrent.atomic.AtomicReference;

public class JUC08 {
    public static void main(String[] args) {
        User z3 = new User("z3", 22);
        User li4 = new User("li4", 28);
        User w5 = new User("w5", 33);
        AtomicReference<User> userAtomicReference = new AtomicReference<>(z3);
        System.out.println(userAtomicReference.compareAndSet(z3, li4)+"\t"+userAtomicReference.get().toString());
        System.out.println(userAtomicReference.compareAndSet(z3, w5)+"\t"+userAtomicReference.get().toString());
    }
}
@Data
@AllArgsConstructor
class User{
    String name;
    Integer age;
}

image-20240510233053535

自旋锁

CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

自旋锁的实现:

题目:通过cAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
当前有线程持有锁,所以只能通过自旋等待,直到A释放锁后B随后抢到。

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class JUC08 {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void lock(){
        Thread thread=Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t-------come in");
        while (!atomicReference.compareAndSet(null, thread)) {} //自旋等待
    }
    public void unlock(){
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t-------task over,unLock...");
    }

    public static void main(String[] args) {
        JUC08 juc08 = new JUC08();
        new Thread(()->{
            juc08.lock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            juc08.unlock();
        },"A").start();

        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            juc08.lock();
            juc08.unlock();
        },"B").start();
    }
}

image-20240510235251142

CAS缺点
  • 循环时间长开销很大

image-20240511002022278

  • ABA问题

image-20240511002617608

解决ABA问题: 版本号时间戳原子引用(AtomicStampedReference)

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.concurrent.atomic.AtomicStampedReference;

public class JUC08 {
    public static void main(String[] args) {
        Book javaBook = new Book(1,"javaBook");
        Book mysqlBook = new Book(2,"mysqlBook");
        //第二个参数为初始流水号
        AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(javaBook, 1);
        System.out.println(stampedReference.getReference()+"\t"+stampedReference.getStamp());
        System.out.println(stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp()+1));
        System.out.println(stampedReference.getReference()+"\t"+stampedReference.getStamp());
    }
}
@Data
@AllArgsConstructor
class Book{
    private Integer id;
    private String bookName;
}

image-20240511004351983

原子类使用(18罗汉)

基本类型原子类(3)

AtomicInteger、AtomicLong、AtomicBoolean

常用方法:

public final int get() 获取当前的值
public final int getAndSet(int newValue)获取当前的值,并设置新的值
public final int getAndlncrement()获取当前的值,并自增
public final int getAndDecrement()) 获取当前的值,并自减
public final int getAndAdd(int delta) 获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) 如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)

案例:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class Juc09 {
    public static final int SIZE=50;
    public static void main(String[] args) {
        Number myNumber = new Number();
        CountDownLatch countDownLatch = new CountDownLatch(SIZE);//解决多线程之间的协同工作问题
        for(int i=1;i<=50;i++){
            new Thread(()->{
                try {
                    for (int j = 1; j <= 1000; j++) {
                        myNumber.addPlusPlus();
                    }
                }finally {
                    countDownLatch.countDown(); //countDownLatch减1
                }
            },String.valueOf(i)).start();
        }
        try {
            countDownLatch.await(); //countDownLatch为0时才执行下面的代码
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result:\t"+myNumber.atomicInteger.get());
    }
}
class Number{
    AtomicInteger atomicInteger = new AtomicInteger(0);
    public void addPlusPlus(){
        atomicInteger.getAndIncrement();
    }
}

image-20240512235229465

数组类型原子类(3)

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

常用方法:

  • 构造方法

image-20240512234649538

案例:

import java.util.concurrent.atomic.AtomicIntegerArray;

public class Juc09 {
    public static void main(String[] args) {
        AtomicIntegerArray atomicIntegerArray1 = new AtomicIntegerArray(5);
        AtomicIntegerArray atomicIntegerArray2 = new AtomicIntegerArray(new int[5]);
        AtomicIntegerArray atomicIntegerArray3 = new AtomicIntegerArray(new int[]{1,2,3,4,5});
        for (int i = 0; i < atomicIntegerArray3.length(); i++) {
            System.out.println(atomicIntegerArray3.get(i));
        }
    }
}

image-20240512235139383

  • 常用方法
import java.util.concurrent.atomic.AtomicIntegerArray;

public class Juc09 {
    public static void main(String[] args) {
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1,2,3,4,5});

        atomicIntegerArray.getAndSet(0, 1122); //获取数组的第一个元素,然后将其值更改为1122
        System.out.println("修改后的数组中第1个元素的值:\t"+atomicIntegerArray.get(0));

        atomicIntegerArray.getAndIncrement(0);
        System.out.println("修改后的数组中第1个元素的值:\t"+atomicIntegerArray.get(0)); //获取数组的第一个元素,然后将其值自增1
    }
}

image-20240512235907259

引用类型原子类(3)

AtomicReference、AtomicStampedReference、AtomicMarkableReference

AtomicStampedReference:携带版本号的引用类型原子类,可以解决ABA问题

AtomicMarkableReference:带有标记位的引用类型对象,这个类就是将版本号简化为true或者false

常用方法:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicMarkableReference;

public class Juc09 {
    static AtomicMarkableReference markableReference = new AtomicMarkableReference(100,false);//初始值为100,初始标记为false
    public static void main(String[] args) {
       new Thread(()->{
           boolean marked = markableReference.isMarked();
           System.out.println(Thread.currentThread().getName()+"\t 默认标识:"+marked);
           try {
               TimeUnit.SECONDS.sleep(1);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           markableReference.compareAndSet(100,1000,marked,!marked);//期望值为100,新值为1000,期望标记为marked,新标识为marked取反
       },"t1").start();

        new Thread(()->{
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"\t 默认标识:"+marked);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b=markableReference.compareAndSet(100,2000,marked,!marked);//期望值为100,新值为1000,期望标记为marked,新标识为marked取反
            System.out.println(Thread.currentThread().getName()+"\t t2线程CASresult:"+b);
            System.out.println(Thread.currentThread().getName()+"\t"+markableReference.isMarked());
            System.out.println(Thread.currentThread().getName()+"\t"+markableReference.getReference());
        },"t2").start();
    }
}

image-20240513221216899

修改对象属性的原子类(3)

AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

AtomicIntegerFieldUpdater:基于反射,可对指定类的指定volatile int字段进行原子更新。

AtomicLongFieldUpdater:基于反射,可对指定类的指定volatile long字段进行原子更新。

AtomicReferenceFieldUpdater:基于反射,可对指定类的指定volatile 引用字段进行原子更新。

**使用目的:**以一种线程安全的方式操作非线程安全对象内的某些字段

使用要求:

  1. 更新的对象属性必须使用public volatile修饰符
  2. 因为修改对象属性的原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性

案例:

需求1:10个线程,每个线程转账1000,不使用synchronized,尝试使用AtomicIntegerFieldUpdater来实现

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class Juc09 {
    public static void main(String[] args) throws InterruptedException {
        BankAccount bankAccount = new BankAccount();
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                try {
                    for (int j = 0; j < 1000; j++) {
                        bankAccount.add();
                    } 
                }finally {
                    countDownLatch.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println(bankAccount.money);

    }
}
class BankAccount{
    String bankName="CCB";
    public volatile int money=0;
    AtomicIntegerFieldUpdater<BankAccount> updater = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"money");
    public void add(){
        updater.getAndIncrement(this);
    }
}

image-20240513230721398

需求2:多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作。要求只能被初始化一次,只有一个线程操作成功

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class Juc09 {
    public static void main(String[] args) throws InterruptedException {
        MyVar myVar = new MyVar();
        for (int i = 1; i <= 5; i++) {
            new Thread(()->{
                    myVar.init();
            },String.valueOf(i)).start();
        }
    }
}
class MyVar{
    public volatile Boolean isInit=false;
    AtomicReferenceFieldUpdater<MyVar,Boolean> updater = AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");
    public void init(){
        if (updater.compareAndSet(this,Boolean.FALSE,Boolean.TRUE)) {
            System.out.println(Thread.currentThread().getName()+"\t---------start init");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"\t---------over init");
        }else{
            System.out.println(Thread.currentThread().getName()+"\t---------已经有线程在进行初始化工作....");
        }
    }
}

image-20240513233050315

原子操作增强类(4)

DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder

LongAdder

LongAdder只能用来计算加法,且从零开始计算。LongAdder比AtomicLong性能更好(减少乐观锁的次数)

常用API:

  • 构造方法

    LongAdder():构建一个初始总和为零的新加法器

import java.util.concurrent.atomic.LongAdder;
public class Juc09 {
    public static void main(String[] args) {
        LongAdder longAdder = new LongAdder();//初始值为0
        longAdder.increment();//自增1
        longAdder.increment();//自增1
        longAdder.increment();//自增1
        System.out.println(longAdder.sum());//3
    }
}

image-20240514212615679

LongAccumulator

LongAccumulator提供了自定义的函数操作。在低并发下,LongAdder和LongAccumulator具有相似的特征。但在高并发的情况下,LongAccumulator的预期吞吐量明显更高,但代价是空间消耗更高。

常用API:

  • 构造方法

LongAccumulator(LongBinaryOperator accumulatorFunction, long identity)使用给定的累加器函数和标识元素创建新实例。

public class Juc09 {
    public static void main(String[] args) {
        LongAccumulator longAccumulator = new LongAccumulator((x,y)->x+y,1);
        longAccumulator.accumulate(2); //1+2
        System.out.println(longAccumulator.get()); //3
        longAccumulator.accumulate(5);//3+5
        System.out.println(longAccumulator.get()); //8
    }
}

案例:

需求:50个线程,每个线程100万次,计算总点赞数

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

public class Juc09 {
    public static final int _1W = 10000;
    public static final int threadNumber = 50;
    public static void main(String[] args) throws InterruptedException {
        ClickNumber clickNumber = new ClickNumber();
        long startTime;
        long endTime;
        CountDownLatch countDownLatch1 = new CountDownLatch(threadNumber);
        CountDownLatch countDownLatch2 = new CountDownLatch(threadNumber);
        CountDownLatch countDownLatch3 = new CountDownLatch(threadNumber);
        CountDownLatch countDownLatch4 = new CountDownLatch(threadNumber);
        startTime=System.currentTimeMillis();
        for (int i = 1; i <= threadNumber; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1W; j++) {
                        clickNumber.clickBySynchronized();
                    }
                } finally {
                    countDownLatch1.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch1.await();
        endTime=System.currentTimeMillis();
        System.out.println("----costTime:"+(endTime-startTime)+"毫秒"+"\t clickBySynchronized:"+clickNumber.number);

        startTime=System.currentTimeMillis();
        for (int i = 1; i <= threadNumber; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1W; j++) {
                        clickNumber.clickByAtomicLong();
                    }
                } finally {
                    countDownLatch2.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch2.await();
        endTime=System.currentTimeMillis();
        System.out.println("----costTime:"+(endTime-startTime)+"毫秒"+"\t clickByAtomicLong:"+clickNumber.number);

        startTime=System.currentTimeMillis();
        for (int i = 1; i <= threadNumber; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1W; j++) {
                        clickNumber.clickByLongAdder();
                    }
                } finally {
                    countDownLatch3.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch3.await();
        endTime=System.currentTimeMillis();
        System.out.println("----costTime:"+(endTime-startTime)+"毫秒"+"\t clickByLongAdder:"+clickNumber.number);

        startTime=System.currentTimeMillis();
        for (int i = 1; i <= threadNumber; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1W; j++) {
                        clickNumber.clickByLongAccumulator();
                    }
                } finally {
                    countDownLatch4.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch4.await();
        endTime=System.currentTimeMillis();
        System.out.println("----costTime:"+(endTime-startTime)+"毫秒"+"\t clickByLongAccumulator:"+clickNumber.number);
    }
}
class ClickNumber{
    int number=0;
    public synchronized void clickBySynchronized(){
        number++;
    }
    AtomicLong atomicLong = new AtomicLong(0);
    public void clickByAtomicLong(){
        atomicLong.getAndIncrement();
    }
    LongAdder longAdder = new LongAdder();
    public void clickByLongAdder(){
        longAdder.increment();
    }
    LongAccumulator longAccumulator = new LongAccumulator((x,y)->x+y,0);
    public void clickByLongAccumulator(){
        longAccumulator.accumulate(1);
    }
}

image-20240514221139934

Striped64类(1)

LongAdder是Striped64类的子类

Striped64有几个比较重要的成员函数:

image-20240514232543635

Striped64中一些变量或者方法的定义:

image-20240514232911195

Cell(1)

是 java.util.concurrent.atomic 下 Striped64 的一个内部类

LongAdder高性能原理分析

原理:

LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就 被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。

sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。

LongAdder类内部有一个base变量、一个Cell[]数组,低并发时,直接累加到该变量上;高并发时,累加进各个线程自己的槽Cell[i]中。

源码:

  1. public void increment()方法:

image-20240515235217561

  1. public void add(long x)方法:

image-20240515233452129

大致流程:

image-20240515234603775

  1. final void longAccumulate(long x, LongBinaryOperator fn,boolean wasUncontended)方法:

参数说明:

image-20240515235058305

总结:

image-20240515231108380

缺点:

不能保证精确度。返回的结果是使用sum方法将base和所有Cell数组求和而来的, 最终返回时,很可能base己经被更新,而此时返回值不会更新,从而造成不一致。其次,对cell的读取也无法保证是最后一次写入的值。

AtomicLong和LongAdder比较:

AtomicLong:性能较慢,但保证返回结果的精度,高精度时使用

image-20240516230215308

LongAdder:性能高,但是不能保证返回结果的精度,当需要在高并发下有较好的表现,且对返回值的精确度要求不高时可以使用

image-20240516230309398

线程局部变量ThreadLocal

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLoca实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来,避免了线程安全的问题。

常用方法

image-20240516234014082

案例1: 5个销售卖完随机数房子,按照出单数各自统计

import java.util.Random;
import java.util.concurrent.TimeUnit;

public class JUC10 {
    public static void main(String[] args) throws InterruptedException {
        House house = new House();
        for (int i = 1; i <= 5; i++) {
            new Thread(()->{
                int size = new Random().nextInt(5)+1;
                try {
                    for (int j = 1; j <= size; j++) {
                        house.saleVolumeByThreadLocal();
                    }
                    System.out.println(Thread.currentThread().getName() + "\t" + "号销售卖出:" + house.saleVolume.get());
                }finally {
                    /*
                     * 【强制】必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会放复用,如果不清理
                     * 自定义的ThreadLocal变量,可能会影啊后续业务逻辑和造成内存泄露等问题。尽量在代理中使用
                     * try-finalLy块进行回收。
                    */
                    house.saleVolume.remove();
                }
            },String.valueOf(i)).start();;
        }
        TimeUnit.SECONDS.sleep(2);
    }
}
class House{
    ThreadLocal<Integer> saleVolume= ThreadLocal.withInitial(() -> 0);//创建线程局部变量,初始值为0。统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
    public void saleVolumeByThreadLocal(){
        saleVolume.set(1+saleVolume.get());
    }
}

image-20240516235816545

**案例2: **不回收ThreadLocal变量时,出现逻辑错误:

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class JUC10 {
    public static void main(String[] args) throws InterruptedException {
        House house = new House();
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        try {
            for (int i = 1; i <= 10; i++) {
                threadPool.submit(() -> {
                    Integer beforeInt = house.saleVolume.get();
                    house.saleVolumeByThreadLocal();
                    Integer afterInt = house.saleVolume.get();
                    System.out.println(Thread.currentThread().getName() + "\t" + "before:" + beforeInt + "\t after:" + afterInt);
                });
            }
            TimeUnit.SECONDS.sleep(2);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}
class House{
    ThreadLocal<Integer> saleVolume= ThreadLocal.withInitial(() -> 0);//创建线程局部变量,初始值为0
    public void saleVolumeByThreadLocal(){
        saleVolume.set(1+saleVolume.get());
    }
}

image-20240517002214771

案例3: 回收ThreadLocal变量

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class JUC10 {
    public static void main(String[] args) throws InterruptedException {
        House house = new House();
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        try {
            for (int i = 1; i <= 10; i++) {
                threadPool.submit(() -> {
                    try {
                        Integer beforeInt = house.saleVolume.get();
                        house.saleVolumeByThreadLocal();
                        Integer afterInt = house.saleVolume.get();
                        System.out.println(Thread.currentThread().getName() + "\t" + "before:" + beforeInt + "\t after:" + afterInt);
                    } finally {
                        house.saleVolume.remove();
                    }
                });
            }
            TimeUnit.SECONDS.sleep(2);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}
class House{
    ThreadLocal<Integer> saleVolume= ThreadLocal.withInitial(() -> 0);//创建线程局部变量,初始值为0
    public void saleVolumeByThreadLocal(){
        saleVolume.set(1+saleVolume.get());
    }
}

image-20240517002326189

Thread、ThreadLocal、ThreadLocalMap三者的关系

image-20240521223223670

image-20240521224212055

image-20240522231817898

image-20240522233735257

ThreadLocal之弱引用

内存泄漏:

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露

ThreadLocalMap与WeekReference:

强、软、弱、虚引用

image-20240521234841071

  • 强引用(默认支持模式)

image-20240521235822688

垃圾回收不会回收强引用的对象

public class JUC11 {
    public static void main(String[] args) {
        MyObject myObject = new MyObject();//强引用
        System.out.println("gc before:"+myObject);
        System.gc(); //人工启动垃圾回收
      	try {TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace();}//等待gc执行完成
        System.out.println("gc after:"+myObject);
    }
}
class MyObject{
    //这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
    @Override
    protected void finalize() throws Throwable {
        //finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操件。
        System.out.println("------invoke finalize method~!!!");
    }
}

image-20240522000452778

消除强引用后,垃圾回收会将其回收


public class JUC11 {
    public static void main(String[] args) {
        MyObject myObject = new MyObject();//强引用
        System.out.println("gc before:"+myObject);
        myObject=null; //消除强引用
        System.gc(); //人工启动垃圾回收
      	try {TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace();}//等待gc执行完成
        System.out.println("gc after:"+myObject);
    }
}
class MyObject{
    //这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
    @Override
    protected void finalize() throws Throwable {
        //finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操件。
        System.out.println("------invoke finalize method~!!!");
    }
}

image-20240522000346605

  • 软引用

image-20240522000826490

内存够用时,垃圾回收不会回收软引用对象

import java.lang.ref.SoftReference;
import java.util.concurrent.TimeUnit;

public class JUC11 {
    public static void main(String[] args) {
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());//软引用
        System.gc(); //人工启动垃圾回收
        try {TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace();}//等待gc执行完成
        System.out.println("------gc 内存够用: "+softReference.get());
    }
}
class MyObject{
    //这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
    @Override
    protected void finalize() throws Throwable {
        //finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操件。
        System.out.println("------invoke finalize method~!!!");
    }
}

image-20240522001520502

内存不足时,垃圾回收会回收软引用对象

修改jvm内存

image-20240522001909478

image-20240522001947011

image-20240522002047966

image-20240522002234814

import java.lang.ref.SoftReference;
import java.util.concurrent.TimeUnit;

public class JUC11 {
    public static void main(String[] args) {
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());//软引用
        System.gc(); //人工启动垃圾回收
        try {TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace();}//等待gc执行完成
        System.out.println("------gc 内存够用: "+softReference.get());
        try{
            byte[] bytes = new byte[20 * 1024 * 1024];//创建20MB的数组对象
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("------gc 内存不够用: "+softReference.get());
        }
    }
}
class MyObject{
    //这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
    @Override
    protected void finalize() throws Throwable {
        //finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操件。
        System.out.println("------invoke finalize method~!!!");
    }
}

image-20240522002625267

  • 弱引用

image-20240522002806104

import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;

public class JUC11 {
    public static void main(String[] args) {
        WeakReference<MyObject> softReference = new WeakReference<>(new MyObject());//弱引用
        System.out.println("------gc before 内存够用: "+softReference.get());
        System.gc(); //人工启动垃圾回收
        try {TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace();}//等待gc执行完成
        System.out.println("------gc after 内存够用: "+softReference.get());
    }
}
class MyObject{
    //这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
    @Override
    protected void finalize() throws Throwable {
        //finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操件。
        System.out.println("------invoke finalize method~!!!");
    }
}

image-20240522003109738

软引用和弱引用的使用场景:

image-20240522003344220

  • 虚引用

image-20240522004415975

构造方法:

image-20240522225621201

ThreadLocal内存泄漏问题

解决ThreadLocal内存泄漏问题的方式:

  1. 使用弱引用:

image-20240522232752221

image-20240522232428475

image-20240522233103923

image-20240522233618326

  1. 清除脏Entry

从set、get、remove方法的源码可以看出.在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry这个方法清理掉key为null的脏entry。

ThreadLocal最佳实践
  1. 使用ThreadLocal.withInitial(()->初始化值);
  2. 把ThreadLocal修饰为static
  3. 使用完要手动remove
总结

image-20240522235412256

对象内存布局

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据 (Instance Data) 和对齐填充 (Padding)

image-20240527232614316

  • 对象头

**问:**对象头由哪几部分组成?

**答:**对象标记Mark Word和类元信息(又叫类型指针)

**问:**对象标记占多少内存?

**答:**在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节。

问:Object o = new Object() 占了多少内存?

**答:**这个对象没有实例数据,只有对象头,所以只占了16个字节,

**问:**对象标记保存什么?

**答:**默认存储对象的HashCode、分代年龄和锁标志位等信息。这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。

image-20240527233241117

**问:**64位分别存储什么?

答:

image-20240528005009006

字段解释:

hashcode:保存对象的哈希码,只有调用hashcode方法后,第26位到56位才存储hashcode

age:保存对象的分代年龄

biased_block:偏向锁标识位

lock:锁状态标识位

JavaThread:保存持有偏向锁的线程ID

epoch:保存偏向时间戳

**问:**锁标志位表示的含义

**答:**最后2bit表示锁标志位,01、00、10、11、各有不同的含义

image-20240527235958114

问:Customer cust = new Customer(1001,匿名客户,new Account());这条语句各部分在内存中的位置

image-20240528002903836

  • 实例数据

**问:**实例数据保存什么?

**答:**存放类的属性(Field)数据信息,包括父类的属性信息

  • 对齐填充

**问:**为什么需要对齐填充?

**答:**虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这部分内存按8字节补充对齐。

**问:**下面语句中创建的customer对象占用多少内存?

import lombok.Data;

public class JUC11 {
    public static void main(String[] args) throws InterruptedException {
        Customer customer = new Customer();
        customer.id=1001;
        customer.setFlag(true);
    }
}
@Data
class Customer{
    int id;
    boolean flag;
}

**答:**对象头占16字节,int类型占4字节,boolean类型占1字节,所以该对象共占21字节。21字节不是8的整数倍,所以需要进行对齐填充,填充后该对象共占24字节。

:GC年龄最大为多少?

:GC年龄采用4位bit存储,最大为15,可以使用-XX:MaxTenuringThreshold=16修改

image-20240528210847937

当修改为16时,程序运行时会报错

image-20240528211006760

使用JOL分析:

添加依赖

image-20240528205021103

打印当前VM的细节详细情况

image-20240528205227858

打印对象详情

image-20240528205610077

image-20240528210135408

:JVM是否默认开启压缩类型指针

:默认开启,开启压缩类型指针后,类型指针占用4个字节。使用命令-XX:-UsecompressedclassPointers,关闭压缩类型指针可以使用命令java -XX:+PrintCommandLineFlags -version查看虚拟机开启的配置

image-20240529001357042

synchronized与锁升级

锁的升级过程:

image-20240529231132282

轻量级锁和偏向锁的由来:

image-20240529211425249

对象头各个位的含义:

image-20240529215727125

无锁状态(自由自在):

image-20240529221915723

偏向锁(唯我独尊):

JDK15取消了偏向锁。

image-20240529225039340

image-20240529224907334

image-20240529225458098

image-20240529225659498

image-20240529230650881

偏向锁代码实例:

查看JVM偏向锁相关的配置

image-20240529230939223

image-20240529231342193

image-20240529234413111

轻量级锁(楚汉争霸):

image-20240530002019504

image-20240530002547167

总体步骤流程图

20200602120540100

重量级锁(群雄逐鹿)

image-20240603232047629

image-20240603233357208

总结

锁升级为轻量级或重量级锁后,Mark Word中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,己经没有位置再保存哈希码、GC年龄了,那么这些信息被移动到哪里去了呢?

image-20240604001522190

image-20240604002657888

各种锁的优缺点

image-20240604003045831

synchronized锁升级过程和实现原理

image-20240604003428401

各种锁的使用场景

image-20240604003637391

JIT编译器对锁的优化

JIT:Just In Time Complier,即时编译器

  • 锁消除

image-20240604213904997

  • 锁粗化

image-20240604214143798

image-20240604214109085

AQS

详见:https://www.bilibili.com/video/BV1ar4y1x727?p=140&vd_source=a350c326ae9669f2aae94c1ac452c6e3

AbstractQueuedSynchronizer简称AQS,译为抽象队列同步器,AQS用来解决锁分配给谁的问题。

image-20240604214849824

Java并发大神DougLee,提出统一规范并简化了锁的实现,将其抽象出来,屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等,是一切锁和同步组件实现的公共基础部分,是JUC包的基石。

image-20240604220219423

ReentrantLock源码:

image-20240604220501611

AQS的基本原理:

image-20240604225727742

AQS同步队列的基本结构:

image-20240604230640461

AQS内部体系架构:

image-20240604230915465

AQS内部构造:

image-20240604230319588

  1. int变量

image-20240604232220497

  1. CLH双端队列:

image-20240604231308991

  1. 内部类Node–银行候客区的椅子

image-20240604232915270

Node的内部结构

image-20240604234259413

读写锁

读写锁的定义:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。写的时候不能读,读的时候不能写,不可以同时写,但是可以同时读。

image-20240613234622360

编码演示
  • 独占锁代码演示:加锁后只能有一个线程访问资源
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class JUC12 {
    HashMap<Integer, Integer> map = new HashMap<>();
    ReentrantLock lock = new ReentrantLock();//ReentrantLock等价于synchronized
    public void write(Integer key,Integer value){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"\t"+"正在写入");
            map.put(key,value);
            TimeUnit.MILLISECONDS.sleep(500);
            System.out.println(Thread.currentThread().getName()+"\t"+"完成写入");
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void read(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"\t"+"正在读取");
            TimeUnit.MILLISECONDS.sleep(200);
            System.out.println(Thread.currentThread().getName()+"\t"+"完成读取");
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        JUC12 juc12 = new JUC12();
        for (int i = 1; i <= 10; i++) {
            int finalI=i;
            new Thread(()->{
                juc12.write(finalI,finalI);
            },String.valueOf(i)).start();
        }
        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                juc12.read();
            },String.valueOf(i)).start();
        }
        TimeUnit.SECONDS.sleep(3);
    }
}

image-20240614231848230

  • 读写锁代码演示:加写锁后只能有一个线程访问资源,加读锁后可以有多个线程同时访问资源。
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class JUC12 {
    HashMap<Integer, Integer> map = new HashMap<>();
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();//ReentrantLock等价于synchronized
    public void write(Integer key,Integer value){
        try {
            lock.writeLock().lock();
            System.out.println(Thread.currentThread().getName()+"\t"+"正在写入");
            map.put(key,value);
            TimeUnit.MILLISECONDS.sleep(500);
            System.out.println(Thread.currentThread().getName()+"\t"+"完成写入");
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.writeLock().unlock();
        }
    }
    public void read(){
        try {
            lock.readLock().lock();
            System.out.println(Thread.currentThread().getName()+"\t"+"正在读取");
            TimeUnit.MILLISECONDS.sleep(200);
            System.out.println(Thread.currentThread().getName()+"\t"+"完成读取");
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.readLock().unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        JUC12 juc12 = new JUC12();
        for (int i = 1; i <= 10; i++) {
            int finalI=i;
            new Thread(()->{
                juc12.write(finalI,finalI);
            },String.valueOf(i)).start();
        }
        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                juc12.read();
            },String.valueOf(i)).start();
        }
        TimeUnit.SECONDS.sleep(3);
    }
}

image-20240614232139319

读写锁锁降级

遵循获取写锁,再获取读锁,再释放写锁的次序,写锁能够降级成为读锁。如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。

image-20240614235055629

image-20240614235015840

  • 正例:先获取写锁,再获取读锁(锁降级)
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class JUC12 {
    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
        writeLock.lock(); //获取写锁
        System.out.println("-----写入");
        readLock.lock();//获取读锁
        System.out.println("-----读取");
        writeLock.unlock(); //释放写锁
        readLock.unlock(); //释放读锁
    }
}

image-20240615002258583

  • 反例:先获取读锁,再获取写锁(锁升级–不允许)
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class JUC12 {
    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
        readLock.lock();//获取读锁
        System.out.println("-----读取");
        writeLock.lock(); //获取写锁
        System.out.println("-----写入");
        readLock.unlock(); //释放读锁
        writeLock.unlock(); //释放写锁
    }
}

image-20240615002112976

结论

写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁【锁降级】;但是获取到了读锁不能继续获取写锁【锁升级–不允许】,如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁),这是因为读写锁要保持写操作的可见性。因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。

image-20240615003528541

邮戳锁

StampedLock称为邮戳锁或票据锁,StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化(为了解决ReentrantReadWriteLock锁饥饿问题)。返回一个long类型的戳记(stamp),这个戳记代表了锁的状态。当戳记为0时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取戳记。

image-20240616103242273

特点
  1. 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功:

  2. 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;

  3. 是不可重入的锁,如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁

  4. StampedLock有三种访问模式:

    ① Reading(读模式悲观):功能和ReentrantReadWriteLock的读锁类似。

    ② Writing(写模式):功能和ReentrantReadWriteLock的写锁类似。

    ③ Optimistic Reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再升级为悲观读模式。

  5. 乐观读模式:使用tryOptimisticRead()方法进入乐观读模式,仅当锁处于非写模式时,方法tryOptimisticRead()才返回非零戳记。如果在乐观读期间,没有其他线程获取StampedLock的写锁,那么validate(Long) 返回true。

ReentrantReadWriteLock和StampedLock比较

image-20240616102753212

编码演示
  • 悲观读
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;

public class JUC12 {
    static int number = 37;
    StampedLock stampedLock = new StampedLock();

    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            number = number + 13;
        } finally {
            stampedLock.unlock(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程结束修改");
    }

    public void read() {
        long stamp = stampedLock.readLock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "读线程准备读取");
            TimeUnit.SECONDS.sleep(3);
            System.out.println(Thread.currentThread().getName() + "\t" + "读取到number:" + number + ", 读线程结束读取");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            stampedLock.unlock(stamp);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        JUC12 juc12 = new JUC12();
        new Thread(() -> {
            juc12.read();
        },"readThread").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            juc12.write();
        },"writeThread").start();
        TimeUnit.SECONDS.sleep(5);
        System.out.println(Thread.currentThread().getName()+"\t 当前number的值为:"+number);
    }
}

image-20240616110327037

  • 乐观读
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;

public class JUC12 {
    static int number = 37;
    StampedLock stampedLock = new StampedLock();

    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            number = number + 13;
        } finally {
            stampedLock.unlock(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程结束修改");
    }

    public void read() {
        long stamp = stampedLock.tryOptimisticRead(); //乐观读并没有加锁,所以不需要释放锁
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "读线程准备读取");
            System.out.println(Thread.currentThread().getName() + "\t" + "读取到number:" + number); //第一次读取number
            TimeUnit.SECONDS.sleep(3); //故意间隔3秒,很乐观认为读取中没有其它线程修改过number的值
            System.out.println(Thread.currentThread().getName() + "\t" + "判断这三秒内是否有别的线程修改过number,是否修改过(true表示无修改,false表示有修改):" + stampedLock.validate(stamp));
            if (!stampedLock.validate(stamp)) {
                try {
                    System.out.println("不好了,有人修改过number的值,将乐观读升级为悲观读,读取一下最新的number值吧");
                    stamp = stampedLock.readLock();
                    System.out.println("悲观读取number的值为:" + number);
                } finally {
                    stampedLock.unlock(stamp);
                }
            } else {
                System.out.println(Thread.currentThread().getName() + "\t" + "读取到number:" + number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        JUC12 juc12 = new JUC12();
        new Thread(() -> {
            juc12.read();
        }, "readThread").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            juc12.write();
        }, "writeThread").start();
        TimeUnit.SECONDS.sleep(5);
        System.out.println(Thread.currentThread().getName() + "\t 当前number的值为:" + number);
    }
}

image-20240616114200161

StampedLock的缺点
  1. StampedLock不支持重入,没有Re开头

  2. StampedLock的悲观读锁和写锁都不支持条件变量(Condition)

  3. 使用StampedLock一定不要调用中断操作,即不要调佣interrupt()方法

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值