源码调用了内存加载PE模块。内存中直接运行DLL,绝对不会释放DLL出来。所要加载的DLL无需处理,直接可用。支持加壳后的DLL。
以下内容无关:
——————————————-分割线———————————————
事实上,物理机遇到的并发问题与虚拟机中的情况有很多相似之处,物理机对并发的处理方案对虚拟机的实现也有相当大的参考意义,因此,我们有必要学习下物理机中处理问题的方法。
上文说过可以使用并发编程来充分利用 CPU 的资源,其中一个主要原因就是计算机的存储设备与 CPU 的运算速度有着几个数量级的差距,这样 CPU 不得不花费大量的时间去等待其他资源。
这是软件层面,而在硬件层面上,现代计算机系统都会在内存与 CPU 之间加入一层或多层读写速度尽可能接近 CPU 运算速度的高速缓存来作为缓冲。
将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
为此,这不可避免的带来了一个新的问题:缓存一致性(Cache Coherence)。
就是说当多个 CPU 的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。如果真的发生这种情况,那同步回到主内存时该以谁的缓存数据为准呢?
为了解决一致性的问题,需要各个 CPU 访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。于是,我们引出了内存模型的概念。
在物理机层面,内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。
显然,不同架构的物理机器可以拥有不一样的内存模型,而 Java 虚拟机也拥有自己的内存模型,称为 Java 内存模型(Java Memory Model,JMM),其目的就是为了屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
当然了,JMM 与这里我们介绍的物理机的内存模型具有高度的可类比性。
Java 内存模型
JMM 规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory)。
线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。
此处的主内存可以与前面所说的物理机的主内存类比,当然,实际上它仅是虚拟机内存的一部分,工作内存可与前面讲的高速缓存类比。
《Java 并发编程的艺术》中把 “工作内存” 称为 “本地内存”(Local Memory)。 “工作内存” 是《深入理解 Java 虚拟机 – 第 3 版》这本书中的写法。
多提一嘴,这里的变量其实和我们日常编程中所说的变量不一样,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后面这俩是线程私有的,不会被共享,自然就不会存在竞争问题。各位知道就好,不必太过深究。
原子性
什么是原子性
类比物理机,拥有缓存一致性协议来规定主内存和高速缓存之间的操作逻辑,那么 JMM 中主内存与工作内存之间有没有具体的交互协议呢?
Of Course!JMM 中定义了以下 8 种操作规范来完成一个变量从主内存拷贝到工作内存、以及从工作内存同步回主内存这一类的实现细节。Java 虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
暂时放下到底是哪 8 种操作,我们先谈何为原子?
原子(atomic)本意是 “不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为 “不可被中断的一个或一系列操作”。
举个经典的简单例子,银行转账,A 像 B 转账 100 元。转账这个操作其实包含两个离散的步骤:
步骤 1:A 账户减去 100
步骤 2:B 账户增加 100
我们要求转账这个操作是原子性的,也就是说步骤 1 和步骤 2 是顺续执行且不可被打断的,要么全部执行成功、要么执行失败。
试想一下,如果转账操作不具备原子性会导致什么问题呢?
比如说步骤 1 执行成功了,但是步骤 2 没有执行或者执行失败,就会导致 A 账户少了 100 但是 B 账户并没有相应的多出 100。
对于上述这种情况,符合原子性的转账操作应该是如果步骤 2 执行失败,那么整个转账操作就会失败,步骤 1 就会回滚,并不会将 A 账户减少 100。
OK,了解了原子性的概念后,我们再来看 JMM 定义的 8 种原子操作具体是啥,以下了解即可,没必要死记:
lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量
事实上,对于 double 和 long 类型的变量来说,load、store、read 和 write 操作在某些平台上允许有例外,称为 “long 和 double 的非原子性协定”,不过一般不需要我们特别注意,这里就不再过多赘述了。
这 8 种操作当然不是可以随便用的,为了保证 Java 程序中的内存访问操作在并发下仍然是线程安全的,JMM 规定了在执行上述 8 种基本操作时必须满足的一系列规则。
这我就不一一列举了,多提这么一嘴的原因就是下文会涉及一些这其中的规则,为了防止大家看的时候云里雾里,所以先前说明白比较好。
上面我们举了一个转账的例子,那么,在具体的代码中,非原子性操作可能会导致什么问题呢?
看下面这段代码,各位不妨考虑一个的问题,如果两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果一定是 0 吗?
耳熟能详的问题,我们无法保证这段代码执行结果的一定性(正确性),可能是正数、也可能是负数、当然也可能是 0。
那么,我们就把这段代码称为线程不安全的,就是说在单线程环境下正常运行的一段代码,在多线程环境中可能发生各种意外情况,导致无法得到正确的结果。
从线程安全的角度来反向理解线程不安全的概念可能更容易点,这里参考《Java 并发编程实践》上面的一句话:
一段代码在被多个线程访问后,它仍然能够进行正确的行为,那这段代码就是线程安全的。
至于这段代码线程不安全的原因,就是 Java 中对静态变量自增和自减操作并不是原子操作,它俩其实都包含三个离散的操作:
步骤 1:读取当前 i 的值
步骤 2:将 i 的值加 1(减 1)
步骤 3:写回新值
可以看出来这是一个 读 – 改 – 写 的操作。
以 i ++ 操作为例,我们来看看它对应的字节码指令:
上方这段代码对应的字节码是这样的:
简单解释下这些字节码指令的含义:
getstatic i:获取静态变量 i 的值
iconst_1:准备常量 1
iadd:自增(自减操作对应 isub)
putstatic i:将修改后的值存入静态变量 i
如果是在单线程的环境下,先自增 5000 次,然后再自减 5000 次,那当然不会发生任何问题。
原文链接:https://blog.csdn.net/weixin_43322764/article/details/116462375?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167042854316800180664292%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=167042854316800180664292&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~times_rank-11-116462375-null-null.nonecase&utm_term=%E6%98%93%E8%AF%AD%E8%A8%80%E6%BA%90%E7%A0%81