jvm(12)-java内存模型与线程
- 1.1)problem:由于计算机的存储设备与处理器的运算速率有几个数量级的差距;
- 1.2)solution:引入一层读写速度尽可能接近处理器速度的高速缓存(cache) 来作为内存与处理器间的缓冲: 将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓冲同步回内存中,这样处理器就无须等待缓慢的内存读写了;
- 2.1)问题描述(problem): 当多个处理器的运算任务都涉及到同一块内存区域时,将可能导致各自的缓存数据不一致,那同步到内存时以谁的数据为准呢?
- 2.2)solution: 需要各个处理器遵循一些协议,在读写时要根据协议来进行操作,这类协议有 MSI, MESI,等。
- 2.3)内存模型: 可以理解为 在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象;(干货——java内存模型定义)
- 2.1)每条线程还有自己的工作内存: 线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,而不能直接读写内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量;(干货——每条线程还有自己的工作内存,工作内存定义)
- 2.2)线程间变量值的传递均需要通过主内存来完成:线程、内存、工作内存三者关系如下所示:
- 3.1)如果硬要扯上关系,则:主内存主要对应于java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域;
- 3.2)更低层次上说:主内存就直接对应于物理硬件的内存,而为了获得更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存;
- o1)lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态;
- o2)unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
- o3)read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load 动作使用;
- o4)load(载入):作用于工作内存的变量, 它把 read 操作从主内存中得到的变量放入工作内存的变量副本中;
- o5)use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
- o6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
- o7)store(存储):作用于工作内存的变量, 它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用;
- o8)write(写入):作用于主内存的变量, 它把store操作从工作内存中得到的变量的值放入主内存的变量中;
- 2.1)把变量从主内存复制到工作内存:顺序执行read和load操作(目的地是工作内存);
- 2.2)把变量从工作内存同步回主内存:顺序执行store和write操作(目的地是主内存);
- Attention)java内存模型只要求上述两个操作必须按顺序执行,没有保证是连续执行;即read和 load 之间,store和 write之间可以插入其他指令;(干货——java内存模型只要求上述两个操作必须按顺序执行,没有保证是连续执行,它们之间还可以插入其他指令)
- r1)不允许read和 load,store 和 write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现;
- r2)不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存;
- r3)不允许一个线程无原因地(没有发生过任何 assign操作)把数据从线程的工作内存同步回主内存中;
- r4)一个新变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量,换句话说,对一个变量实施 use,store操作前,必须先执行过 assign 和 load 操作;
- r5)一个变量在同一个时刻只允许一个线程对其进行lock 操作,但lock操作可以被同一条线程重复执行多次,多次执行 lock后,只有执行相同次数的unlock 操作,变量才会被解锁;
- r6)如果对一个变量执行lock 操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值;
- r7)如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去 unlock 一个被其他线程锁定住的变量;
- r8)对一个变量执行unlock 变量前,必须先把此变量同步回主内存中(执行store, write操作);
- Conclusion) 这8种内存访问操作以及上述规则限定,再加上稍后介绍的对 volatile 的一些特殊规定,就已经完全确定了java 程序中哪些内存访问操作在并发下是安全的;
- c1)保证此变量对所有线程的可见性,这里的可见性指:当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量做不到这一点,其在线程间传递需要通过主内存来完成;(干货——这就是为什么会出现数据的脏读)
- c2)对于volatile变量的可见性,有一些误解: volatile变量对所有线程都是可见的,对volatile变量所有的写操作都能立刻反应到其他线程中,即,volatile变量在各个线程中是一致的,所有基于 volatile变量的运算在并发下是安全的。上述语句中的错误在于并不能得出“基于 volatile变量的运算在并发下是安全的”这个结论。
- 2.1)看个荔枝:
- public class VolatileTest {
- public static volatile int race = 0;
- public static void increase() {
- race++;
- }
- public static final int THREADS_COUNT = 20;
- public static void main(String[] args) {
- Thread[] threads = new Thread[THREADS_COUNT];
- for (int i = 0; i < threads.length; i++) {
- threads[i] = new Thread(new Runnable() {
- @Override
- public void run() {
- for (int j = 0; j < 10000; j++) {
- increase();
- }
- }
- });
- threads[i].start();
- }
- // 等待所有累计线程都ending
- while(Thread.activeCount() > 1) {
- Thread.yield();
- }
- System.out.println(race);
- }
- }
- 对以上执行结果的分析(Analysis):
- A1)以上代码的正确输出结果是20000, 而执行的结果每次都不一样,且都小于20000;
- A2)用javap 反编译命令得到如下字节码,发现increase()方法对应4条字节码指令(return指令不算):
- 对以上字节码的分析(Analysis):
- A1)当 getstatic指令 把 race 的值取到操作栈顶时,volatile关键字保证了 race的值在此时是正确的,但是在执行 iconst_1, iadd 这些指令的时候,其他线程可能已经把race 的值加大了,而在操作栈顶的值就变成了过期的数据,所以 putstatic 指令执行后就可能把较小的race 值同步回主内存中;
- A2)客观上说,在这里使用 字节码来分析并发问题,不是很严谨。因为即使编译出来只有一条字节码指令,也不意味着执行这条指令就是一个原子操作。一条字节码指令也可能会转化成若干条本地机器码指令,此处使用 -XX:+PrintAssembly 参数输出反汇编来分析会更加严谨;(干货——因为即使编译出来只有一条字节码指令,也不意味着执行这条指令就是一个原子操作)
- scene1)运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;
- scene2)变量不需要与其他的状态变量共同参与不变约束;
- // 使用volatile变量来控制并发
- public class VolatileVariableTest {
- volatile boolean shutdownRequested; // volatile变量
- public void shutdown() {
- shutdownRequested = true;
- }
- public void doWork() {
- while(!shutdownRequested) {
- // do sth.
- }
- }
- // 指令重排序演示(伪代码)
- public void instruct_reorder() {
- Map configOptions;
- char[] configText;
- // 此变量必须为 volatile
- volatile boolean initialized = false;
- // 假设以下代码在线程A 中执行
- // 模拟读取配置信息,当读取完成后将 initialized 设置为true 已通知其他线程配置可用
- configOptions = new HashMap();
- configText = readConfigFile(filename);
- processConfigOptions(configText, configOptions);
- initialized = true;
- // 假设以下代码在线程B 中执行
- // 等待initialized 为true,代表线程A 已经把配置信息初始化完成
- while(!initialized) {
- sleep();
- }
- // 使用线程A 中初始化好的配置信息
- do_sth_with_config();
- }
- 如果定义initialized变量没有使用volatile修饰:就可能会由于指令重排序的优化,导致位于线程A 中最后一句代码“initialized=true”被提前执行;这样在线程B中使用配置信息的代码就可能出现错误,而volatile关键字则可以避免此类情况的发生;
- // 单例模式(分析volatile关键字是如何禁止指令重排序优化的)
- public class Singleton {
- private volatile static Singleton instance;
- public static Singleton getInstance() {
- if(instance == null) {
- synchronized (Singleton.class) { // 同步块
- if(instance == null) {
- instance = new Singleton();
- }
- }
- }
- return instance;
- }
- public static void main(String[] args) {
- Singleton.getInstance();
- }
- }
- 1.1)同步块——synchronized关键字:如果应用场景需要一个更大范围的原子性保证,java内存模型还提供了lock 和 unlock 操作来满足这些需求,尽管虚拟机没有把lock 和 unlock 操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式地使用这两个操作;
- 1.2)这两个字节码指令反映到java代码中就是同步块——synchronized关键字:因此在synchronized块之间的操作也具备原子性;
- 2.1)java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此;
- 2.2)普通变量与 volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新;所以volatile保证了多线程操作时变量的可见性,普通变量则不能保证这一点;
- 2.3)java还有两个关键字实现可见性: synchronized 和 final;
- 2.3.1)同步块的可见性: 是由对一个变量执行unlock 操作前,必须先把此变量同步回主内存中;
- 2.3.2)而final关键字的可见性:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this 的引用传递出去,那在其他线程中就能看见final 字段的值。
- 2.3.3)看个荔枝:
- // final 可见性测试
- public class FinalVisibilityTest {
- public static final int i;
- public final int j;
- static {
- i = 0;
- // do sth
- }
- {
- // 也可以选择在构造函数中初始化
- j = 0;
- // do sth
- }
- }
- 对以上代码的分析(Analysis):变量i 和 j 都具备可见性,它们无须同步就能被其他线程正确访问;
- 3.1)java程序中天然的有序性总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指: 线程内表现为串行的语义,后半句是指:指令重排序现象和工作内存与主内存同步延迟现象;
- 3.2)volatile和 synchronized关键字保证了线程间操作的有序性:volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由 一个变量在同一时刻只允许一条线程对其进行lock 操作这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入;
- // 先行发生 原则
- public class AheadOccurTest {
- int i = 0;
- int j = 0;
- public void test() {
- // 以下操作在线程A中执行
- i = 1;
- // 以下操作在线程 B 中执行
- j = i;
- // 以下操作在线程 C 中执行
- i = 2;
- }
- }
- 3.1)程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作;准确地说,应该是控制流顺序;
- 3.2)管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作;这里必须强调的是同一个锁,而后面是指时间上的先后顺序;
- 3.3)volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的后面是指时间上的先后顺序;
- 3.4)线程启动规则:Thread对象的start() 方法先行发生于此线程的每一个动作;
- 3.5)线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,可以通过Thread.join() 方法结束,Thread.isAlive() 的返回值等手段检测到线程已经终止运行;
- 3.6)线程中断规则:对线程interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrrupted() 方法检测到是否有中断发生;
- 3.7)对象终结规则:一个对象的初始化完成先行发生于它的finalize() 方法的开始;
- 3.8)传递性:如果操作A 先行发生于操作B, 操作B 先行发生于操作C,那就可以得出操作A 先行发生于 操作C的结论;
- // 利用先行发生 原则
- // 判定操作间是否具备顺序性,对于读写共享变量的操作来说,就是线程是否安全
- public class AheadOccurTest2 {
- private int value = 0;
- public void setValue(int value) {
- this.value = value;
- }
- public int getValue() {
- return value;
- }
- Integer i = 0;
- }
- A1)problem:假设线程A 先调用了 setValue(1), 之后线程B 调用了同一个对象的getValue() ,那么线程B 收到的value是什么?
- A2)可以判定:尽管线程A在操作时间上先于线程B, 但是无法确定线程B 中“getValue()” 方法的返回结果,换句话说,这里面的操作不是线程安全的;
- A3)solution:我们至少有两种简单的解决方案:
- solution1)要么把getter 和 setter方法都定义为 synchronized方法,这样就可以套用管程锁定规则;
- solution2)要么把value定义为 volatile变量,由于setter方法对value的修改不依赖于 value的原值,满足volatile关键字使用场景,这样就可以套用volatile变量规则来实现先行发生关系;
- A4)得出结论:一个操作时间上的先发生 不代表这个操作会是先行发生,那如果一个操作先行发生是否就能推导出这个操作必定是 时间上的先行发生呢? (显然推导不出来)。一个典型的荔枝就是多次提到的“指令重排序”。
- A4.1)看个荔枝:
- // 以下操作在同一个线程中执行
- int i = 1;
- int j = 2;
- 对上述代码的分析(Analysis): 根据程序次序规则, int i = 1 的操作先行发生于 int j =2,但 int j = 2 完全可能先被处理器执行,这并不影响先行发生原则的正确性;
- Conclusion)以上两个实例得出结论:时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准;
- 0.1)线程是比进程更轻量级的调度执行单位: 线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址,文件IO等),又可以独立调度(线程是CPU 调度的基本单位);
- 0.2)线程实现的3种方式:使用内核线程实现,使用用户线程实现,使用用户线程加轻量级进程混合实现(干货——线程实现的3种方式:使用内核线程实现+使用用户线程实现+使用用户线程和轻量级进程混合实现)
- 1.1)内核线程(KLT,Kernel-Level Thread):就是直接由操作系统内核(下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。(干货——内核线程和轻量级进程的定义)
- 1.2)程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process, LWP):轻量级进程就是我们通常意义上讲的线程,由于每个轻量级线程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程间1:1 的关系称为一对一的线程模型,如下图所示:(干货——引入轻量级进程)
- 1.3)轻量级进程有局限性:
- 1.3.1)首先:由于是基于内核线程实现的,所以各种线程操作,如创建,析构及同步,都需要进行系统调用, 而系统调用的代价相对较高,需要在用户态和内核态中来回切换;
- 1.3.2)其次:每个轻量级进程都需要有一个内存线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的;
- 3.1)在该实现方式下,既存在用户线程,也存在轻量级进程;
- 3.2)用户线程还是完全建立在用户空间中:因此用户线程的创建,切换,析构等操作依然廉价,并且可以支持大规模的用户线程并发;
- 3.3)操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁:这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞 的风险;(干货——操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁)
- 3.4)在这种混合模式中:用户线程与轻量级进程的数量比是不定的,即为 N:M 的关系,如下图所示, 这种就是多对多的线程模型;
- 2.1)其好处是:实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作读线程自己是可知的,所以没有什么线程同步的问题;
- 2.2)其坏处是:线程执行时间不可控制,甚至如果一个线程编写有问题,一直不告诉系统进行线程切换,那么程序就会一直阻塞在那里;
- 3.1)java使用的方式就是 抢占式线程调度方式;
- 3.2)虽然java 线程调度是系统自动完成的: 但我们还是可以建议系统给某些线程多分配一点执行时间,另外一些线程则可以少分配一点——这项操作可以通过设置线程优先级来完成;(干货——设置java 线程优先级)
- 3.3.)不过线程优先级并不是太靠谱:因为java的线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于 操作系统;虽然现在很多os 都提供了线程优先级,但不见得和 能与 java线程的优先级一一对应;如 Solaris中有 2^32 种优先级,而windows只有7种 ;(干货——java的线程优先级并不是太靠谱)
- 3.4)下表显示了 java线程优先级 与 windows 线程优先级之间的对应关系:
- C1)上文说到的“java线程优先级并不是太靠谱”,不仅仅是在说一些平台上不同的优先级实际会变得相同这一点,还有其他case 让我们不能太依赖优先级:优先级可能会被系统自行改变。(干货——优先级可能会被系统自行改变)
- C2)如,在windows 中存在一个称为 “优先级推进器”的功能,作用是 当系统发现一个线程执行得特别勤奋的话,可能会越过线程优先级去为它分配执行时间;
- 1.1)新建(New):创建后尚未启动的线程处于这个状态;
- 1.2)运行(Runnable):Runable包括了os 线程状态中的 Running 和 Ready,也就是处于 此状态的线程有可能正在执行,也有可能正在等待着CPU 为它分配执行时间;
- 1.3)无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式的唤醒。以下方法会让线程陷入无限期的等待状态(methods):
- m1)没有设置Timeout参数的Object.wait()方法;
- m2)没有设置Timeout参数的 Thread.join() 方法;
- m3)LockSupport.park() 方法;
- 1.4)限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU 执行时间,不过无需等待被其他线程显式唤醒,在一定时间之后,它们会由系统自动唤醒。以下方法会让线程进入限期等待状态(methods):
- m1)Thread.sleep() 方法;
- m2)设置了Timeout参数的Object.wait()方法;
- m3)设置了Timeout参数的 Thread.join() 方法;
- m4)LockSupport.parkNanos() 方法;
- m5)LockSupport.parkUntil() 方法;
- 1.5)阻塞(Blocked):线程被阻塞了, 阻塞状态与等待状态的区别是:阻塞状态在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而等待状态则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候, 线程将进入这种状态;
- 1.6)结束(Terminated):已经终止线程的线程状态,线程已经结束执行;
jvm(12)-java内存模型与线程的更多相关文章
- 深入理解JVM(6)——Java内存模型和线程
Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM)用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果(“即Ja ...
- JVM(7) Java内存模型与线程
衡量一个服务性能的高低好坏,每秒事务处理数(Transactions Per Second,TPS)是最重要的指标之一,它代表着一秒内服务端平均能响应的请求总数,而 TPS 值与程序的并发能力又有非常 ...
- 深入理解java虚拟机-第12章Java内存模型与线程
第12章 Java内存模型与线程 Java内存模型 主内存与工作内存: java内存模型规定了所有的变量都在主内存中,每条线程还有自己的工作内存. 工作内存中保存了该线程使用的主内存副本拷贝,线程对 ...
- java内存模型与线程(转) good
java内存模型与线程 参考 http://baike.baidu.com/view/8657411.htm http://developer.51cto.com/art/201309/410971_ ...
- 【JVM】Java内存模型
原文:多线程之Java内存模型(JMM)(一) 概述 多任务和高并发是衡量一台计算机处理器的能力重要指标之一.一般衡量一个服务器性能的高低好坏,使用每秒事务处理数(Transactions Per S ...
- (Java多线程系列七)Java内存模型和线程的三大特性
Java内存模型和线程的三大特性 多线程有三大特性:原子性.可见性.有序性 1.Java内存模型 Java内存模型(Java Memory Model ,JMM),决定一个线程对共享变量的写入时,能对 ...
- 一夜搞懂 | Java 内存模型与线程
前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 GIthub 博客 学习导图 一.为什么要学习内存模型与线程? 并发处理的广泛应用是 Amdah1 定律代替摩尔定律成为计 ...
- 《深入了解java虚拟机》高效并发读书笔记——Java内存模型,线程,线程安全 与锁优化
<深入了解java虚拟机>高效并发读书笔记--Java内存模型,线程,线程安全 与锁优化 本文主要参考<深入了解java虚拟机>高效并发章节 关于锁升级,偏向锁,轻量级锁参考& ...
- Java并发程序设计(三) Java内存模型和线程安全
Java内存模型和线程安全 一 .原子性 原子性是指一个操作是不可中断的.即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰. 思考:i++是原子操作吗? 二.有序性 Java代 ...
随机推荐
- 【JVM】Myecplise自带的JVM大小调整,用于Junit等测试时使用
一般在使用Junit或者一个工具类的main方法执行时,在Myecplise中运行,并不会占用多大的堆空间.如果出现OutofMemory错误,调整MyEcplise自带的JVM大小. 在Myecpl ...
- Swift,闭包
闭包(相当于匿名函数)的几种情况 利用sorted()排序方法来进行示例 1.sorted()正常引用的情况 var str=["d","a","c& ...
- 64个命令,每天一个linux命令目录, shutdown,tee,rcp,
每天一个linux命令目录 开始详细系统的学习linux常用命令,坚持每天一个命令,所以这个系列为每天一个linux命令.学习的主要参考资料为: 1.<鸟哥的linux私房菜> 2.htt ...
- centos7.2 kvm虚拟化管理平台WebVirtMgr部署
在服务器上部署kvm虚拟化,虚出多台VM出来,以应对新的测试需求.当KVM宿主机越来越多,需要对宿主机的状态进行调控,决定采用WebVirtMgr作为kvm虚拟化的web管理工具,图形化的WEB,让人 ...
- nagios系列教程地址
http://www.sosidc.com/sort/10/page/3 http://www.sosidc.com/sort/10/page/2 http://www.sosidc.com/sort ...
- Can we say objects have attributes, states and behaviors?
15down votefavorite 3 I was reading through Oracle's introduction to OOP concepts and I came across ...
- NDK官方开发指南翻译之 NDK_GDB
这几天看JNI,没有基础,那真是难受--把看到的相关资料记录一下,也分享给刚開始学习的人. 'ndk-gdb' Overview 重要:假设你要调试线程相关的程序.请阅读以下的'Thread Supp ...
- Quartz任务监听器
在Quartz框架提供了JobListener接口,可在任务执行前.任务被拒绝及任务执行完成后实现对任务的拦截,该接口的声明如下: public interface JobListener { /** ...
- lodash 工具库
lodash是一套工具库,内部封装了很多字符串.数组.对象等常见数据类型的处理函数. 1.lodash的引用 import _ from 'lodash' 用一个数组遍历来说明为什么要使用lodash ...
- libevent2源码分析之三:信号的初始化流程
libevent2对信号的响应也进行了封装,使之与socket操作一样对外提供统一的接口.这里的信号一般指linux的信号.由于信号与socket相关的编程接口有较大的不同,因此在内部实现也有一些区别 ...