likes
comments
collection

扒一扒@CallerSensitive注解,有点意思!

作者站长头像
站长
· 阅读数 12

感觉写技术文章 确实难,有很多细节推敲,这节开始搞 并发的知识。

废话不多说 走起

并发

什么时候 会有并发问题

扒一扒@CallerSensitive注解,有点意思! 原来入参为 0, 这时 CPU 01 刚刚加一变为为1, 而CPU 02 往内存中写入2,之后变为

扒一扒@CallerSensitive注解,有点意思!

本来应该为3

一句话 是因为 穿插执行导致的问题 (非原子性: 要么全都不执行,要么全都执行,在一个同一个事务中)

说到这 你会想到,原子性操作,那原子性操作有哪些?

原子性操作有哪些?

java 基础命令 比如赋值,一行基础代码 对么?

并不是,在Java中,32位或更小数量的读写保证是原子的。原子,意思是每个动作都在一个步骤中发生,不能被打断。因此,当我们有多线程应用程序时,读写操作是线程安全的,不需要进行同步。

比如 i++ 这种一行的基础代码 里面分为几步执行,也不能保证 原子性

既然i++ 不是原子性,那有原子性操作么?

有,AtomicInteger 就是原子类,那怎么证明他的操作是原子的,怎么保证的原子呢

有的时候 我想 让jar包中的类 不给业务系统使用,我怎么办?

有的时候 我们写jar包的组件,希望业务系统 引用了我们的jar ,但是别使用某个类(这里叫A类),A类 只是给我们自己组件用的。

扒一扒@CallerSensitive注解,有点意思! Unsafe 类就是这样,我们在外部使用的时候提示,在rt.jar 中

  1. 先调用 public native int getIntVolatile(Object o, long offset); 获取当前的内存中的 旧值 (这里叫 old
  2. 循环调用 while (!compareAndSwapInt(o, offset, v, v + delta));
public static Unsafe getUnsafe() {
    //返回的是 你调用Unsafe类的 类名 class org.apache.rocketmq.client.test.B
    Class<?> caller = Reflection.getCallerClass();
    // 返回 sun.misc.Launcher$AppClassLoader
    if (!VM.isSystemDomainLoader(caller.getClassLoader()))
       // 只要 有ClassLoader 就代表 不是系统的 就会抛出错误
        throw new SecurityException("Unsafe");
    return theUnsafe;
}

这里会有几个疑问

1 sun.misc.Launcher$AppClassLoader 怎么看着眼熟呢 这代表什么

没错 就是 双亲委派机制,类加载器,ExtentionClassLoader是AppclassLoader的父加载器。

加载器的加载顺序

加载器在JVM启动时的加载顺序是:

1,BootstrapClassLoader 引导类加载器

2,ExtentionClassLoader 引导类加载器

3,- 应用类加载器Application ClassLoader:应用类加载器,加载classpath下的字节码文件,用java编写,对应AppClassLoader这个类,可以通过ClassLoader类的静态方法getSystemClassLoader() 获得,所以又叫系统类加载器

那这里还有一个问题 为什么系统的classLoader 返回的是null?

很不幸,人家是 native 方法,看不到了,只能从注释中看了

Returns the class loader for the class.  Some implementations may use
     * null to represent the bootstrap class loader. This method will return
     * null in such implementations if this class was loaded by the bootstrap
     * class loader.
     
     返回类的类装入器。某些实现可能使用

    *null表示引导类加载器。此方法将返回

    *如果该类是由引导加载的,则在此类实现中为null

    *类加载器。

那么我们写的代码中是不是可以使用这种方式 判断是不是业务系统中调用的呢?试验一下

扒一扒@CallerSensitive注解,有点意思!

我靠 这个也不行啊,又是什么原因啊,怎么这么难。。。。

扒一扒@CallerSensitive注解,有点意思!

告诉我 需要 CallerSensitive 这个注解,行吧 ,我先不管 它是干什么的,你让我用这个注解,行,我自己搞一个看看

@CallerSensitive
public static Class<?> a(){
    Class<?> caller = Reflection.getCallerClass();
    return caller;
}

结果你给我返回

Exception in thread "main" java.lang.InternalError: CallerSensitive annotation expected at frame 1
	at sun.reflect.Reflection.getCallerClass(Native Method)
	at org.apache.rocketmq.client.test.B.a(B.java:46)
	at org.apache.rocketmq.client.test.B.main(B.java:35)

不行了 找资料吧,openjdk.java.net/jeps/176

扒一扒@CallerSensitive注解,有点意思!

总结就是说 jdk内有些方法,jvm的开发者认为这些方法危险,不希望开发者调用,就把这种危险的方法用 @CallerSensitive修饰,并在“jvm”级别检查。

如Reflection.getCallerClass()方法规定,调用它的对象,必须有 @CallerSensitive 注解,否则 报异常 Exception in thread "main" java.lang.InternalError: CallerSensitive annotation expected at frame 1 @CallerSensitive 有个特殊之处,必须由 启动类classloader加载(如rt.jar ),才可以被识别。 所以rt.jar下面的注解可以正常使用。 开发者自己写的@CallerSensitive 不可以被识别。 但是,可以利用jvm参数 -Xbootclasspath/a: path 假装自己的程序是启动类。


到这,你就是告诉我这个方法调用不了被,是不是。。。。

什么,你还要问 @CallerSensitive 有个特殊之处,必须由 启动类classloader加载(如rt.jar ),才可以被识别 这个怎么证明,行吧,代码如下 在 sun.reflect.Reflection 类中

public static boolean isCallerSensitive(Method m) {
    //获取 ClassLoader
    final ClassLoader loader = m.getDeclaringClass().getClassLoader();
    //必须是 BootstrapClassLoader 或者 ExtentionClassLoader 才能使用 @CallerSensitive 注解
    if (sun.misc.VM.isSystemDomainLoader(loader) || isExtClassLoader(loader))  {
        return m.isAnnotationPresent(CallerSensitive.class);
    }
    return false;
}

我知道 你可能还想问 -Xbootclasspath/a: path 为什么能 把自己当成 启动类的,淡定,先控制一下

继续往下,回到正题,如果我们自己的组件 需要让业务 不能用这个A类怎么做?

  1. 其实可以模仿@CallerSensitive 实现一个 我们自己的 自定义注解(这里叫C 注解),然后扫描自己 指定类中的 C 注解,不扫描 外部业务系统的注解,就可以让外部的不生效,报错
  2. 同时 实现自己公司的 idea 插件,扩展对 代码的检查,比如 如果在业务系统使用某一个不允许的类,进行提示,飘红,告知原因,模仿这种

扒一扒@CallerSensitive注解,有点意思!

扒一扒@CallerSensitive注解,有点意思!

redis lua命令 号称也是原子性的,我们还根据这个特性 去解决的redis 多命令的原子性问题呢。

应用场景:

1 之前做 文件服务中间件的时候 采用的 CS 架构,Server端协议用的 Raft协议,v1版本为了节省代码难度,使用的redis 保证分布式的版本控制

redis 主要存储2个值,一个是 版本号 和 对应的节点 worker的树结构,这就涉及到 2个值的数据一致性问题。 当时使用的lua 把修改这两个值的操作 通过lua 绑定到一起,解决的一致性问题

2 之前再写库存扣减的时候,为了保证高并发的性能,把库存 分为多块,用户买的时候 会根据写的lua脚本 扣减库存,可能分为了10块库存,第一块不够了,就看看能不能从别的块 扣减。

扒一扒@CallerSensitive注解,有点意思!

缓存行 分别加锁,是把锁细粒度化实现的优化,那其他地方有这个思想的其他实现么?

有,线程安全的Map List 都是 使用的锁细粒度 来优化并发问题的,分为16块,通过hash 把锁竞争的压力分发给16块。

java String 为什么是线程安全的

因为 一旦变化 就是一个新的String对象,旧的对象不变