目 录CONTENT

文章目录

思考感悟

FatFish1
2025-08-12 / 0 评论 / 0 点赞 / 19 阅读 / 0 字 / 正在检测是否收录...

并发编程常见的几种工具

Synchronized、ReentrantLock、CAS

Synchronized锁和ReentrantLock最本质的区别是二者的实现方式:

  • Synchronized锁是通过字节码实现的,即加monitorenter和monitorexit

  • ReentrantLock是AQS的实现,其加锁的方式是基于Unsafe包下面的CAS实现的

在早期版本的JDK中,Synchronized是重量级锁,性能不佳,但是随着新版本JDK通过锁自旋、锁升级等优化,性能已经有所提升,已经不把性能作为选择锁类型的主要因素了

ReentrantLock是java代码实现锁,其底层使用的是volatile+unsafe(可见它在源码的层面上并非真正的锁),因此比Synchronized多了很多能力,包括:

  • 公平锁和非公平锁

  • 阻塞获取锁

  • 中断获取锁

  • 通过Condition实现的更灵活的条件队列

  • 丰富的api用于获取锁状态

CAS则不是锁,是unsafe包下面的一套native方法,它的含义是compareAndSet,本质上是一个原子操作,它也是ReentrantLock、ConcurrentHashMap等并发模型的底层实现,但是JDK9以后是不推荐使用unsafe的了,很多实现都切换到了新的VarHandle包

unsafe与varhandle

sun.misc.Unsafejava.lang.invoke.VarHandle 都是 Java 中用于执行底层内存操作和原子操作的机制。它们的主要目标都是为库开发者(如 java.util.concurrent 包的实现者)提供构建高性能、无锁数据结构的基础能力

相比于unsafe,varhandle的安全性更高,例如不能突破java的一些语法限制(final不再可以被修改),同时提供更多CAS操作

volatile

volatile也是字节码层面的并发控制手段,其能力主要包括:

  • 可见性保证:通过字节码增加的写回操作,要求CPU写缓存的操作必须同步刷新内存,并通过总线广播给其他线程,标记为已修改状态,在其他线程使用其值之前先从内存中重新刷新该值

  • 有序性保证:通过增加内存屏障,避免指令重排序

hashCode()与equals()的区别与联系

hashCode是语法约束上的相等,equals是业务上的相等

hashCode方法是HashSet、HashMap等集合计算哈希散列的方法,当两个对象hashCode结果相等,意味着这两个对象在HashSet中是冲突的,即使这两个对象可能并不是一个

而equals常常被用于重写逻辑并进行业务相等的比较,当我们重写了equals但不重写hashCode,有可能出现业务相等但是计算哈希散列不等的情况,导致使用HashMap或HashSet存储并判断isExist时出现问题

jdk动态代理与cglib代理的差异

jdk动态代理

cglib代理

实现方式

基于java反射,动态生成字节码

基于字节码生成技术,动态生成被代理类的子类

性能

生成慢,调用块

生成块,调用慢

生效范围

只能代理接口实现类中的实现方法

可以代理任意非final方法

使用cglib代理需要额外引入cglib.jar

怎么理解JDK8的内存模型

可以用线程独占和程序独占来分类:

  • 程序独占:即这个java进程独占的,线程共享的,包括堆内存、元数据空间、直接内存

    • 堆内存:最常见的JVM内存,存储运行时数据,存储字符串常量池,存储静态变量

    • 元数据空间:存储类加载信息,想要回收元数据空间就要卸载类,可能导致NoClassDefFoundError

    • 直接内存:存储高性能I/O过程的数据

元数据空间和直接内存都是堆外内存,都通过malloc函数申请,只是它们的用途不同,回收方法不同,OOM产生影响不同

  • 线程独占:即随着新线程产生而分配,这部分内存分配来自进程的Native Memory

    • 虚拟机栈:执行java方法时的内存空间,一个栈帧就是一个方法,其中可以再分为操作数栈、局部变量表、动态连接、方法返回地址

    • 本地方法栈:与虚拟机栈结构意义,只是用来处理native方法

    • 程序计数器:指示方法运行到字节码的第几行,用于多线程切换时能够继续前面运行状态继续执行

本地内存在传统linux环境下没有理论限制,只要不达到宿主机的理论内存上限即可,可以通过修改-Xmx、-Xss(单个线程分配内存大小)

在K8s环境下,我们知道单个pod设置limit.memory参数后,回收到linux内核cgroup的限制,即这个pod的native memory是有上限的,因此设定好堆内存、metaspace内存、directmemory后,剩下的部分才可能用于分配线程内存,这就产生了非常大的OOM的潜在风险

正常线程分配内存不足时,应该抛出java.lang.OutOfMemoryError: unable to create new native thread异常,而在K8s这个场景下,JVM 可能根本来不及记录任何错误日志就被 SIGKILL (9) 干掉了。这是诊断此类问题的一个难点

内存模型参考:

http://www.chymfatfish.cn/archives/JVMmemory#jvm%E8%BF%90%E8%A1%8C%E6%97%B6%E6%95%B0%E6%8D%AE%E5%8C%BA%E5%9F%9F

继承的本质

抛开什么private不能继承、什么super访问父类成员变量这些难理解的定义,思考下继承的本质:

关键词1. 可见性凌驾于继承之上

即一个类只能继承父类对他可见的成员变量和成员方法,这就好比儿子只会对自己了解的父亲的财产有想法

假设有两个类:Father、Son

public class Father {
    public String name;

    private int age;

    public Father(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    private int getAge() {
        return age;
    }
}
public class Son extends Father {

    public Son(String name, Integer age) {
        super(name, age);
    }

    // 因为getAge对Son不可见,因此这里就会报错
    public int getFatherAgeViaSuper() {
        return super.getAge();
    }

}

因为private关键字控制的是同一个类内才可见,因此这时Father.age对Son是不可见的,即Son无法继承age和getAge方法

而另一个场景:

public class Main {

    private final static Logger LOGGER = LoggerFactory.getLogger(Main.class);

    static class Father {
        public String name;

        private int age;

        public Father(String name, Integer age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        private int getAge() {
            return age;
        }
    }

    static class Son extends Father {

        public Son(String name, Integer age) {
            super(name, age);
        }

        // 因为是同一个类内,因此尽管是private的,仍然可以继承
        public int getFatherAgeViaSuper() {
            return super.getAge();
        }

    }

因为是在同一个类Main中,Father.age和getAge方法对Son是可见的,因此可以继承

所以说private属性和方法不能继承并不准确,更合理的说法应该是:不可见的属性和方法不能被继承

关键词2. super访问的是自己从父类那里继承来的实例和方法

当Son类继承于Father类时,Son类实际上内存是两块内容:Son类自己定义的属性和方法,Son类继承自Father类的属性和方法

而super实际上是用于访问Son类继承自Father类的属性和方法的,因此实际上访问的还是自己的方法

这么设计的好处在于,可以允许子类与父类有相同的属性和方法,并且实现二者在内存中的隔离xing

例如以下的例子:

public class Mother {
    public String name;

    private int age;

    public Mother(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}
public class Daughter extends Mother {
    
    private int age;

    public Daughter(String name, Integer age, int myAge) {
        super(name, age);
        this.age = age;
    }
    
    @Override
    public int getAge() {
        return age;
    }
    
    public int getMotherAgeViaSuper() {
        return super.getAge();
    }
}

这个案例中,Daughter与Mother有相同的属性age,这是符合业务需要的,因此Daughter用两个方法getAge和getMotherAgeViaSuper来区分访问的对象

同时,要求子类必须匹配父类的构造方法,即调用super(name, age); 方法,这是因为要求子类中属于父类的部分必须对应地完成初始化,哪怕初始化成null,也得把这个内存区域开辟出来

匹配父类的构造方法并不是说完全要写一个入参完全一样的构造方法,而是说父类构造方法必须要被调用

总结一下:

  • 子类对象中开辟两块内存区域,一块存储子类自己的成员变量和方法,一块存储父类的成员变量和方法,因此父类的构造函数必须被调用,从而使这个内存区域内开辟出来

  • super调用的内容是子类中继承于父类的那一部分方法,同理this关键字调用的是子类中自己的那一部分,这里说的父类和子类是讲的内存隔离区域,二者调用的本质都是子类实例自身的成员变量和方法

0

评论区