学习

类加载器详解

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

回顾一下类加载过程 #

开始介绍类加载器和双亲委派模型之前,简单回顾一下类加载过程。

  • 类加载过程:加载->连接->初始化
  • 连接过程又可分为三步:验证->准备->解析

[类加载过程

加载是类加载过程的第一步,主要完成下面 3 件事情:

  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

类加载器 #

类加载器介绍 #

类加载器从 JDK 1.0 就出现了,最初只是为了满足 Java Applet(已经被淘汰) 的需要。后来,慢慢成为 Java 程序中的一个重要组成部分,赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。

根据官方 API 文档的介绍:

A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a “class file” of that name from a file system.

...

类加载过程

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

类的声明周期 #

ly-20241212141959924

类加载过程 #

  • Class文件,需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些Class文件呢
  • 系统加载Class类文件需要三步:加载->连接->初始化。连接过程又分为三步:验证->准备->解析
    ly-20241212142000194

加载 #

类加载的第一步,主要完成3件事情

构造与类相关联的方法表

  1. 通过全类名获取定义此类的二进制字节流
  2. 字节流所代表的静态存储结构,转换为方法区运行时数据结构
  3. 在内存中生成一个该类的Class对象,作为方法区这些数据的访问入口

虚拟机规范对上面3点不具体,比较灵活

  1. 对于1 没有具体指明从哪里获取、怎样获取。可以从ZIP包读取 (JAR/EAR/WAR格式的基础)、其他文件生成(JSP)等
  • 非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器控制字节流的获取方式(重写一个类加载器的**loadClass()**方法
  • 数组类型不通过类加载器创建,它由Java虚拟机直接创建

加载阶段连接阶段的部分内容是交叉执行的,即加载阶段尚未结束,连接阶段就可能已经开始了

验证 #

验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全

验证阶段主要由四个检验阶段组成:

  1. 文件格式验证(Class 文件格式检查)
  2. 元数据验证(字节码语义检查)
  3. 字节码验证(程序语义检查)
  4. 符号引用验证(类的正确性检查)

ly-20241212142000337

准备 #

  • 准备阶段是正式为类变量分配内存设置类变量初始值的阶段,这些内存都将在方法区中分配,注意:

    1. 这时候进行内存分配的包括类变量ClassVariables,即静态变量:被static关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。

      实例变量会在对象实例化时,随着对象一块分配到Java堆

    2. 从概念上讲,类变量所使用的内存都应当在 方法区 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代字符串常量池静态变量等移动到中,这个时候类变量则会随着 **Class 对象(上面有提到,内存区生成Class对象)**一起存放在 Java 堆中

    3. 这里所设置的初始值**“通常情况”下是数据类型默认的零值(如 0、0L、null、false 等**),比如我们定义了**public static int value=111** ,那么 value 变量在准备阶段的初始值就是 0 而不是 111初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111

      ...

java垃圾回收

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

前言 #

需要排查各种内存溢出问题、当垃圾收集成为系统达到更高并发的瓶颈时,我们就需要对这些**“自动化”的技术实施必要的监控调节**

堆空间的基本结构 #

  • Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。且Java自动内存管理最核心的功能是内存中的对象分配回收

  • Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)

  • 垃圾回收的角度来说,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆被划分为了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法

  • JDK7版本及JDK7版本之前,堆内存被通常分为下面三部分:

    1. 新生代内存(Young Generation)
    2. 老生代(Old Generation)
    3. 永久代(Permanent Generation)

hotspot-heap-structure

JDK8版本之后PermGen(永久)已被Metaspace(元空间)取代,且已经不在堆里面了,元空间使用的是直接内存

内存分配和回收原则 #

对象优先在Eden区分配 #

  • 多数情况下,对象在新生代中Eden区分配。当Eden区没有足够空间进行分配时,会触发一次MinorGC 首先,先添加一下参数打印GC详情:-XX:+PrintGCDetails

    public class GCTest {
    	public static void main(String[] args) {
    		byte[] allocation1, allocation2;
    		allocation1 = new byte[30900*1024];//会用掉3万多K
    	}
    } 
    

    运行后的结果(这里应该是配过xms和xmx了,即堆内存大小) img 如上,Eden区内存几乎被分配完全(即使程序什么都不做,新生代也会使用2000多K)

    注: PSYoungGen 为 38400K ,= 33280K + 5120K (Survivor区总会有一个是空的,所以只加了一个5120K )

    假如我们再为allocation2分配内存会怎么样(不处理的话,年轻代会溢出)

    allocation2 = new byte[900 * 1024];
    

    img 在给allocation2分配内存之前,Eden区内存几乎已经被分配完。所以当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。GC期间虚拟机又发现allocation1无法存入空间,所以只好通过分配担保机制,把新生代的对象提前转移到老年代去,老年代的空间足够存放allocation1,所以不会出现Full GC(这里可能是之前的说法,可能只是要表达老年代的GC,而不是Full GC(整堆GC) )  

    执行MinorGC后,后面分配的对象如果能够存在Eden区的话,还是会在Eden区分配内存
    执行如下代码验证:

  public class GCTest {
  
  	public static void main(String[] args) {
  		byte[] allocation1, allocation2,allocation3,allocation4,allocation5;
  		allocation1 = new byte[32000*1024];
  		allocation2 = new byte[1000*1024];
  		allocation3 = new byte[1000*1024];
  		allocation4 = new byte[1000*1024];
  		allocation5 = new byte[1000*1024];
  	}
  } 

大对象直接进入老年代 #

  • 大对象就是需要连续空间的对象(字符串数组等)
  • 大对象直接进入老年代,主要是为了避免为大对象分配内存时,由于分配担保机制(这好像跟分配担保机制没有太大关系)带来的复制而降低效率
  • 假设大对象最后会晋升老年代,而新生代是基于复制算法来回收垃圾的,由两个Survivor区域配合完成复制算法,如果新生代中出现大对象且能屡次躲过GC,那这个对象就会在两个Survivor区域中来回复制,直至最后升入老年代,而大对象在内存里来回复制移动,就会消耗更多的时间。

    ...

jvm-intro

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

原文地址: https://juejin.im/post/5e1505d0f265da5d5d744050#heading-28 感谢原作者分享!!

JVM的基本介绍 #

  • JVM,JavaVirtualMachine的缩写,虚拟出来的计算机,通过在实际的计算机上仿真模拟各类计算机功能实现
  • JVM类似一台小电脑,运行在windows或者linux这些真实操作系统环境下直接和操作系统交互,与硬件不直接交互,操作系统帮我们完成和硬件交互的工作

img

Java文件是如何运行的 #

场景假设:我们写了一个HelloWorld.java,这是一个文本文件。JVM不认识文本文件,所以需要一个编译,让其(xxx.java)成为一个JVM会读的二进制文件—> HelloWorld.class

  1. 类加载器 如果JVM想要执行这个.class文件,需要将其**(这里应该指的二进制文件)装进类加载器**中,它就像一个搬运工一样,会把所有的.class文件全部搬进JVM里面 img

  2. 方法区

    类加载器将.class文件搬过来,就是先丢到这一块上

    方法区是用于存放类似于元数据信息方面的数据的,比如类信息常量静态变量编译后代码…等

  3. 堆 堆主要放一些存储的数据,比如对象实例数组…等,它和方法区都同属于线程共享区域,即它们都是线程不安全

  4. 线程独享
    栈是我们代码运行空间,我们编写的每一个方法都会放到里面运行。
    名词:本地方法栈本地方法接口,不过我们基本不会涉及这两块内容,这两底层使用C进行工作,和Java没有太大关系

  5. 程序计数器 主要就是完成一个加载工作,类似于一个指针一样的,指向下一行我们需要执行的代码。和栈一样,都是线程独享的,就是每一个线程都会自己对应的一块区域而不会存在并发和多线程问题。

  6. 小总结 img

    1. Java文件经过编译后编程.class字节码文件
    2. 字节码文件通过类加载器被搬运到 JVM虚拟机中
    3. 虚拟机主要的5大块:方法区、堆 都为线程共享区域,有线程安全问题;本地方法栈计数器都是独享区域,不存在线程安全问题,而JVM的调优主要就是围绕两大块进行

简单的代码例子 #

一个简单的学生类及main方法:

public class Student {
    public String name;

    public Student(String name) {
        this.name = name;
    }

    public void sayName() {
        System.out.println("student's name is : " + name);
    }
}

main方法:

public class App {
    public static void main(String[] args) {
        Student student = new Student("tellUrDream");
        student.sayName();
    }
}

★★ 执行main方法的步骤如下

...

memory-area

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

如果没有特殊说明,针对的都是HotSpot虚拟机

前言 #

  • 对于Java程序员,虚拟机自动管理机制,不需要像C/C++程序员为每一个new 操作去写对应的delete/free 操作,不容易出现内存泄漏内存溢出问题
  • 但由于内存控制权交给Java虚拟机,一旦出现内存泄漏和溢出方面问题,如果不了解虚拟机是怎么样使用内存,那么很难排查任务

运行时数据区域 #

Java虚拟机在执行Java程序的过程中,会把它管理的内存,划分成若干个不同的数据区域

JDK1.8之前:

  1. 线程共享 堆,方法区【永久代】(包括运行时常量池)
  2. 线程私有 虚拟机栈、本地方法栈、程序计数器
  3. 本地内存(包括直接内存)

ly-20241212141952681

JDK1.8之后:
Java 运行时数据区域(JDK1.8 之后) 1.8之后整个永久代改名叫"元空间",且移到了本地内存中

规范(概括):
线程私有:程序计数器,虚拟机栈,本地方法栈

线程共享:堆,方法区,直接内存(非运行时数据区的一部分)

Java虚拟机规范对于运行时数据区域的规定是相当宽松的,以堆为例:

  1. 堆可以是连续,也可以不连续
  2. 大小可以固定,也可以运行时按需扩展
  3. 虚拟机实现者可以使用任何垃圾回收算法管理堆,设置不进行垃圾收集

程序计数器 #

  • 是一块较小内存空间,看作是当前线程所执行的字节码行号指示器

  • java程序流程
    ly-20241212141953092

  • 字节码解释器,工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令

    分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器

  • 而且,为了线程切换后恢复到正确执行位置,每条线程需要一个独立程序计数器,各线程计数器互不影响,独立存储,我们称这类内存区域为**“线程私有”**的内存

  • 总结,程序计数器的作用

    • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制
    • 多线程情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切回来的时候能够知道该线程上次运行到哪

    程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随线程创建而创建,线程结束而死亡

Java虚拟机栈 #

  • Java虚拟机栈,简称"栈",也是线程私有的,生命周期和线程相同,随线程创建而创建,线程死亡而死亡
  • 除了Native方法调用的是通过本地方法栈实现的,其他所有的Java方法调用都是通过来实现的(需要和其他运行时数据区域比如程序计数器配合)
  • 方法调用的数据需要通过栈进行传递每一次方法调用都会有一个对应的栈帧被压入栈,每一个方法调用结束后,都会有一个栈帧被弹出
  • 栈由一个个栈帧组成,每个栈帧包括局部变量表操作数栈动态链接方法返回地址。 栈为先进后出,且只支持出栈入栈

Java 虚拟机栈

  • 局部变量表:存放编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是一个指向一个代表对象的句柄或其他与此对象相关的位置) 局部变量表

  • 操作数栈 作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。计算过程中产生的临时变量也放在操作数栈中

  • 动态链接 主要服务一个方法需要调用其他方法的场景。

    在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用

    ...

completablefuture-intro

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

Java8被引入的一个非常有用的用于异步编程的类【没看

简单介绍 #

CompletableFuture同时实现了FutureCompletionStage接口

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
}

CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,还提供了函数式编程的能力。

img

Future接口有5个方法:

  • boolean cancel(boolean mayInterruptIfRunning) :尝试取消执行任务。
  • boolean isCancelled() :判断任务是否被取消。
  • boolean isDone() : 判断任务是否已经被执行完成。
  • get() :等待任务执行完成并获取运算结果。
  • get(long timeout, TimeUnit unit) :多了一个超时时间。

img

CompletionStage<T> 接口中的方法比较多,CompoletableFuture的函数式能力就是这个接口赋予的,大量使用Java8引入的函数式编程

常见操作 #

创建CompletableFuture #

两种方法:new关键字或 CompletableFuture自带的静态工厂方法 runAysnc()supplyAsync()

  1. 通过new关键字 这个方式,可以看作是将CompletableFuture当作Future来使用,如下:

    我们通过创建了一个结果值类型为 RpcResponse<Object>CompletableFuture,你可以把 resultFuture 看作是异步运算结果的载体

    CompletableFuture<RpcResponse<Object>> resultFuture = new CompletableFuture<>();
    

    如果后面某个时刻,得到了最终结果,可以调用complete()方法传入结果,表示resultFuture已经被完成:

    // complete() 方法只能调用一次,后续调用将被忽略。
    resultFuture.complete(rpcResponse);
    

    通过isDone()检查是否完成:

    public boolean isDone() {
        return result != null;
    }
    

    获取异步结果,使用get() ,调用get()方法的线程会阻塞 直到CompletableFuture完成运算: rpcResponse = completableFuture.get();

    ...

ThreadLocal详解

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

本文来自一枝花算不算浪漫投稿, 原文地址: https://juejin.cn/post/6844904151567040519open in new window。 感谢作者!

思维导图
img

目录 #

ThreadLocal代码演示 #

简单使用

public class ThreadLocalTest {
    private List<String> messages = Lists.newArrayList();

    public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);

    public static void add(String message) {
        holder.get().messages.add(message);
    }

    public static List<String> clear() {
        List<String> messages = holder.get().messages;
        holder.remove();

        System.out.println("size: " + holder.get().messages.size());
        return messages;
    }

    public static void main(String[] args) {
        ThreadLocalTest.add("一枝花算不算浪漫");
        System.out.println(holder.get().messages);
        ThreadLocalTest.clear();
    }
}
/* 结果 
[一枝花算不算浪漫]
size: 0
*/

简单使用2

...

Atomic原子类介绍

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

文章开头先用例子介绍几种类型的api使用

package com.aqs;

import lombok.*;

import java.util.concurrent.atomic.*;

@Data
@Getter
@Setter
@AllArgsConstructor
@ToString
class User {
    private String name;
    //如果要为atomicReferenceFieldUpdater服务,必须加上volatile修饰
    public volatile Integer age;
}

public class AtomicTest {
    public static void main(String[] args) {
        System.out.println("原子更新数值---------------");
        AtomicInteger atomicInteger = new AtomicInteger();
        int i1 = atomicInteger.incrementAndGet();
        System.out.println("原子增加后为" + i1);
        System.out.println("原子更新数组---------------");

        int[] a = new int[3];
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(a);

        int i = atomicIntegerArray.addAndGet(1, 3);
        System.out.println("数组元素[" + 1 + "]增加后为" + i);
        System.out.println("数组为" + atomicIntegerArray);
        System.out.println("原子更新对象---------------");
        User user1 = new User("ly1", 10);
        User user2 = new User("ly2", 20);
        User user3 = new User("ly3", 30);
        AtomicReference<User> atomicReference = new AtomicReference<>(user1);
        boolean b = atomicReference.compareAndSet(user2, user3);
        System.out.println("更新" + (b ? "成功" : "失败"));
        System.out.println("引用里值为"+atomicReference.get());
        boolean b1 = atomicReference.compareAndSet(user1, user3);
        System.out.println("更新" + (b1 ? "成功" : "失败"));
        System.out.println("引用里值为"+atomicReference.get());
        System.out.println("原子更新对象属性---------------");
        User user4=new User("ly4",40);
        AtomicReferenceFieldUpdater<User, Integer> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(User.class, Integer.class, "age");
        boolean b2 = atomicReferenceFieldUpdater.compareAndSet(user4, 41, 400);
        System.out.println("更新"+(b2?"成功":"失败"));
        System.out.println("引用里user4值为"+atomicReferenceFieldUpdater.get(user4));
        boolean b3 = atomicReferenceFieldUpdater.compareAndSet(user4, 40, 400);
        System.out.println("更新"+(b3?"成功":"失败"));
        System.out.println("引用里user4值为"+atomicReferenceFieldUpdater.get(user4));
        System.out.println("其他使用---------------");
        User user5=new User("ly5",50);
        User user6=new User("ly6",60);
        User user7=new User("ly7",70);
        AtomicMarkableReference<User> userAtomicMarkableReference=new AtomicMarkableReference<>(user5,true);
        boolean b4 = userAtomicMarkableReference.weakCompareAndSet(user6, user7, true, false);
        System.out.println("更新"+(b4?"成功":"失败"));
        System.out.println("引用里值为"+userAtomicMarkableReference.getReference());
        boolean b5 = userAtomicMarkableReference.weakCompareAndSet(user5, user7, false, true);
        System.out.println("更新"+(b5?"成功":"失败"));
        System.out.println("引用里值为"+userAtomicMarkableReference.getReference());
        boolean b6 = userAtomicMarkableReference.weakCompareAndSet(user5, user7, true, false);
        System.out.println("更新"+(b6?"成功":"失败"));
        System.out.println("引用里值为"+userAtomicMarkableReference.getReference());
        System.out.println("AtomicStampedReference使用---------------");
        User user80=new User("ly8",80);
        User user90=new User("ly9",90);
        User user100=new User("ly10",100);
        AtomicStampedReference<User> userAtomicStampedReference=new AtomicStampedReference<>(user80,80);//版本80
        //...每次更改stamp都加1
        //这里假设中途被改成81了
        boolean b7 = userAtomicStampedReference.compareAndSet(user80, user100,81,90);
        System.out.println("更新"+(b7?"成功":"失败"));
        System.out.println("引用里值为"+userAtomicStampedReference.getReference());
        boolean b8 = userAtomicStampedReference.compareAndSet(user80, user100,80,90);
        System.out.println("更新"+(b8?"成功":"失败"));
        System.out.println("引用里值为"+userAtomicStampedReference.getReference());
    }
}
/*
原子更新数值---------------
原子增加后为1
原子更新数组---------------
数组元素[1]增加后为3
数组为[0, 3, 0]
原子更新对象---------------
更新失败
引用里值为User(name=ly1, age=10)
更新成功
引用里值为User(name=ly3, age=30)
原子更新对象属性---------------
更新失败
引用里user4值为40
更新成功
引用里user4值为400
其他使用---------------
更新失败
引用里值为User(name=ly5, age=50)
更新失败
引用里值为User(name=ly5, age=50)
更新成功
引用里值为User(name=ly7, age=70)
AtomicStampedReference使用---------------
更新失败
引用里值为User(name=ly8, age=80)
更新成功
引用里值为User(name=ly10, age=100)

Process finished with exit code 0

*/

原子类介绍 #

  • 在化学上,原子是构成一般物质的最小单位,化学反应中是不可分割的,Atomic指一个操作是不可中断的,即使在多个线程一起执行时,一个操作一旦开始就不会被其他线程干扰
  • 原子类–>具有原子/原子操作特征的类
  • 并发包java.util.concurrent 的原子类都放着java.util.concurrent.atomicly-20241212141944757
  • 根据操作的数据类型,可以将JUC包中的原子类分为4类(基本类型、数组类型、引用类型、对象的属性修改类型)
    • 基本类型 使用原子方式更新基本类型,包括AtomicInteger 整型原子类AtomicLong 长整型原子类,AtomicBoolean 布尔型原子类

      ...

aqs详解

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

Semaphore [ˈseməfɔː(r)]

  • 何为 AQS?AQS 原理了解吗?
  • CountDownLatchCyclicBarrier 了解吗?两者的区别是什么?
  • 用过 Semaphore 吗?应用场景了解吗?
  • ……

AQS简单介绍 #

AQS,AbstractQueueSyschronizer,即抽象队列同步器,这个类在java.util.concurrent.locks包下面

ly-20241212141944167

AQS是一个抽象类,主要用来构建同步器

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
} 

AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLockSemaphore,其他的诸如 ReentrantReadWriteLockSynchronousQueueFutureTask(jdk1.7) 等等皆是基于 AQS 的。

AQS原理 #

AQS核心思想 #

面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来

AQS 核心思想是,如果被请求的共享资源(AQS内部)空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
[ 搜索了一下,CLH好像是人名 ] 在 CLH 同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
CLH队列结构
ly-20241212141944445

  • AQS(AbstractQueuedSynchronized)原理图
    ly-20241212141944597

    AQS使用一个int成员变量来表示同步状态,通过内置的线程等待队列来获取资源线程的排队工作。
    state 变量由 volatile 修饰,用于展示当前临界资源的获锁情况。

    ...

java常见并发容器

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

JDK提供的容器,大部分在java.util.concurrent包中

  • ConcurrentHashMap:线程安全的HashMap
  • CopyOnWriteArrayList:线程安全的List,在读多写少的场合性能非常好,远好于Vector
  • ConcurrentLinkedQueue:高效的并发队列,使用链表实现,可以看作一个线程安全的LinkedList,是一个非阻塞队列
  • BlockingQueue:这是一个接口,JDK内部通过链表、数组等方式实现了该接口。表示阻塞队列,非常适合用于作为数据共享的通道
  • ConcorrentSkipListMap:跳表的实现,是一个Map,使用跳表的数据结构进行快速查找

ConcurrentHashMap #

  • HashMap是线程不安全的,并发场景下要保证线程安全,可以使用Collections.synchronizedMap()方法来包装HashMap,但这是通过使用一个全局的锁同步不同线程间的并发访问,因此会带来性能问题
  • 建议使用ConcurrentHashMap,不论是读操作还是写操作都能保证高性能:读操作(几乎)不需要加锁,而写操作时通过锁分段(这里说的是JDK1.7?)技术,只对所操作的段加锁而不影响客户端对其他段的访问

CopyOnWriteArrayList #

//源码
public class CopyOnWriteArrayList<E>
extends Object
implements List<E>, RandomAccess, Cloneable, Serializable
 
  • 在很多应用场景中,读操作可能会远远大于写操作
  • 我们应该允许多个线程同时访问List内部数据(针对读)
  • 与ReentrantReadWriteLock读写锁思想非常类似,即读读共享写写互斥读写互斥写读互斥
  • 不一样的是,CopyOnWriteArrayList读取时完全不需要加锁,且写入也不会阻塞读取操作,只有写入和写入之间需要同步等待

CopyOnWriteArrayList是如何做到的 #

  • CopyOnWriteArrayList 类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。
  • CopyOnWriteArrayList 的名字就能看出 CopyOnWriteArrayList 是满足 CopyOnWrite
  • 在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存(注意,是指向,而不是重新拷贝★重要★),原来的内存就可以被回收掉了

CopyOnWriteArrayList 读取和写入源码简单分析 #

  • CopyOnWriteArrayList读取操作的实现 读取操作没有任何同步控制操作,理由就是内部数组array不会发生修改,只会被另一个array替换,因此可以保证数据安全

      /** The array, accessed only via getArray/setArray. */
        private transient volatile Object[] array;
        public E get(int index) {
            return get(getArray(), index);
        }
        @SuppressWarnings("unchecked")
        private E get(Object[] a, int index) {
            return (E) a[index];
        }
        final Object[] getArray() {
            return array;
        }
    
  • CopyOnWriteArrayList写入操作的实现 在添加集合的时候加了锁,保证同步,避免多线程写的时候会copy出多个副本

    ...