精华内容
下载资源
问答
  • c代码编译器工作原理

    千次阅读 2012-09-05 20:33:09
    C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。链接是把目标文件、操作...
    C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。链接是把目标文件、操作系统的启动代码和用到的库文件进行组织形成最终生成可执行代码的过程。过程图解如下:

    c代码编译器工作原理 - 大唐猎人 - nieshenglinie的博客


      从图上可以看到,整个代码的编译过程分为编译和链接两个过程,编译对应图中的大括号括起的部分,其余则为链接过程。

      编译过程

      编译过程又可以分成两个阶段:编译和会汇编。

      编译

      编译是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,源文件的编译过程包含两个主要阶段:

      第一个阶段是预处理阶段,在正式的编译阶段之前进行。预处理阶段将根据已放置在文件中的预处理指令来修改源文件的内容。如#include指令就是一个预处理指令,它把头文件的内容添加到.cpp文件中。这个在编译之前修改源文件的方式提供了很大的灵活性,以适应不同的计算机和操作系统环境的限制。一个环境需要的代码跟另一个环境所需的代码可能有所不同,因为可用的硬件或操作系统是不同的。在许多情况下,可以把用于不同环境的代码放在同一个文件中,再在预处理阶段修改代码,使之适应当前的环境。

      主要是以下几方面的处理:

      (1)宏定义指令,如 #define a b

      对于这种伪指令,预编译所要做的是将程序中的所有a用b替换,但作为字符串常量的 a则不被替换。还有 #undef,则将取消对某个宏的定义,使以后该串的出现不再被替换。

      (2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等。

      这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。

      (3) 头文件包含指令,如#include "FileName"或者#include <FileName>等。

      在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在 /usr/include目录下。在程序中#include它们要使用尖括号(< >)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号("")。

      (4)特殊符号,预编译程序可以识别一些特殊的符号。

      例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

      预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输出而被翻译成为机器指令。

      第二个阶段编译、优化阶段,经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,{,}, +,-,*,\等等。

      编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。

      优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。

      对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。

      后一种类型的优化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放的有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。

      汇编

      汇编实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段:

      代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。

      数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

      UNIX环境下主要有三种类型的目标文件:

      (1)可重定位文件

      其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。

      (2)共享的目标文件

      这种文件存放了适合于在两种上下文里链接的代码和数据。第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个 目标文件;第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。

      (3)可执行文件

      它包含了一个可以被操作系统创建一个进程来执行之的文件。汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。

      链接过程

      由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。

      例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。

      链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。

      根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:

      (1)静态链接

      在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。

      (2) 动态链接

      在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。

      对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

      我们在linux使用的gcc编译器便是把以上的几个过程进行捆绑,使用户只使用一次命令就把编译工作完成,这的确方便了编译工作,但对于初学者了解编译过程就很不利了,下图便是gcc代理的编译过程:

    c代码编译器工作原理 - 大唐猎人 - nieshenglinie的博客


      从上图可以看到:

      预编译

      将.c 文件转化成 .i文件

      使用的gcc命令是:gcc –E

      对应于预处理命令cpp

      编译

      将.c/.h文件转换成.s文件

      使用的gcc命令是:gcc –S

      对应于编译命令 cc –S

      汇编

      将.s 文件转化成 .o文件

      使用的gcc 命令是:gcc –c

      对应于汇编命令是 as

      链接

      将.o文件转化成可执行程序

      使用的gcc 命令是: gcc

      对应于链接命令是 ld

      总结起来编译过程就上面的四个过程:预编译、编译、汇编、链接。Lia了解这四个过程中所做的工作,对我们理解头文件、库等的工作过程是有帮助的,而且清楚的了解编译链接过程还对我们在编程时定位错误,以及编程时尽量调动编译器的检测错误会有很大的帮助的。

     
     
    展开全文
  • ConcurrentHashMap 工作原理代码实现 ConcurrentHashMap 采用了非常精妙的“分段锁”策略。 ConcurrentHashMap 的主干是一个segment数组,Segment 继承了 ReentrantLock,所以它就是一种可重入锁(ReentrantLock)...

    ConcurrentHashMap 工作原理及代码实现

    • ConcurrentHashMap 采用了非常精妙的“分段锁”策略。
    • ConcurrentHashMap 的主干是一个segment数组,Segment 继承了 ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。一个 segment 是一个子哈希表,segment 同时维护 HashEntry 数组。
    • 并发环境下,对于不同segment的操作时不存在锁竞争的。
    展开全文
  • 舵机工作原理及STM32驱动代码

    千次阅读 2020-04-14 18:44:10
    舵机工作原理及STM32驱动代码 1.舵机接线 舵机上有三根线,分别为VCC(红色正极)、GND(棕褐色负极)、信号线(橙色信号线)。标准PWM(脉冲宽度调制)信号的周期固定为20ms(50Hz)。VCC、GND需要另外接驱动给舵机...

    舵机工作原理及STM32驱动代码

    1.舵机接线
    舵机上有三根线,分别为VCC(红色正极)、GND(棕褐色负极)、信号线(橙色信号线)。标准PWM(脉冲宽度调制)信号的周期固定为20ms(50Hz)。VCC、GND需要另外接驱动给舵机供电,而且得和开发板共地。
    在这里插入图片描述
    2.工作原理:
    舵机的控制一般需要一个20ms左右的时基脉冲,该脉冲的高电平部分一般为0.5ms-2.5ms范围,总间隔为2ms。脉冲的宽度将决定马达转动的距离。**注意:舵机角度与占空比无关。**以180度角度舵机为例,那么对应的控制关系是这样的:
    0.5ms————–0度;
    1.0ms————45度;
    1.5ms————90度;
    2.0ms———–135度;
    2.5ms———–180度;
    在这里插入图片描述

    二、STM32驱动程序

    1.TIM3 PWM初始化:

    void TIM3_PWM_Init(u16 arr,u16 psc)
    {  
    	GPIO_InitTypeDef GPIO_InitStructure;
    	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    	TIM_OCInitTypeDef  TIM_OCInitStructure;
    	
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);	//使能定时器3时钟
     	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB  | RCC_APB2Periph_AFIO, ENABLE);  //使能GPIO外设和AFIO复用功能模块时钟
    	
    	GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); //Timer3部分重映射  TIM3_CH1->PB4,TIM3_CH2->PB5    
     
       //设置该引脚为复用输出功能,输出TIM3 CH1|CH2的PWM脉冲波形	
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_5; //TIM_CH1|TIM_CH2
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIO
     
       //初始化TIM3
    	TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
    	TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值 
    	TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
    	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
    	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
    	
    	//初始化TIM3 Channel2 PWM模式	 
    	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式1
     	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
    	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高
    	TIM_OC2Init(TIM3, &TIM_OCInitStructure);  //根据T指定的参数初始化外设TIM3 OC2
    	TIM_OC1Init(TIM3, &TIM_OCInitStructure);
    	
    	TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
    	TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);  //使能TIM3在CCR2上的预装载寄存器
    	 
    	TIM_ARRPreloadConfig(TIM3, ENABLE); //使能TIMx在ARR上的预装载寄存器
     
    	TIM_Cmd(TIM3, ENABLE);  //使能TIM3
    }
    

    2.主函数:

    #include "delay.h"
    #include "sys.h"
    #include "pwm.h"
     int main(void)
     {    
        delay_init();            //延时函数初始化    
        TIM1_PWM_Init(199,7199);//(7200*200)/72000000=0.02=20ms
        while(1)
        {   
        	TIM_SetCompare1(TIM3,10); //45度,1ms
        	TIM_SetCompare1(TIM3,10); //45度,1ms
        	delay_ms(1000); 
        	TIM_SetCompare1(TIM3,15); //90,1.5ms
        	TIM_SetCompare1(TIM3,15); //90,1.5ms
         	delay_ms(1000);        
        } 
    }
    

    3.STM32定时器定时计算公式:

    • Tout = ((arr+1)*(psc+1))/Tclk ;
      其中:
      Tclk:定时器的输入时钟频率(单位MHZ)
      Tout:定时器溢出时间(单位为us)
      .TIM_Period = arr; 自动重装载值
      .TIM_Prescaler = psc; 时钟预分频系数
      例如:arr=4999,psc=7199,Tout = ((4999+1)×(7199+1))/72 = 500000us = 500ms

    初始化定时器的时候指定我们分频系数psc,这里是将我们的系统时钟(72MHz)进行分频,然后指定重装载值arr,这个重装载值的意思就是当我们的定时器的计数值达到这个arr时,定时器就会重新装载其他值。
    例如当我们设置定时器为向上计数时,定时器计数的值等于arr之后就会被清0重新计数,定时器计数的值被重装载一次被就是一个更新(Update)

    Tclk是定时器时钟源,在这里就是72Mhz,我们将分配的时钟进行分频,指定分频值为psc,就将我们的Tclk分了(psc+1),我们定时器的最终频率就是Tclk/(psc+1) MHz,这里的频率的意思就是1s中记 Tclk/(psc+1)M个数 (1M=10的6次方) ,每记一个数的时间为(psc+1)/Tclk ,很好理解频率的倒数是周期,这里每一个数的周期就是(psc+1)/Tclk 秒,然后我们从0记到arr 就是 (arr+1)*(psc+1)/Tclk。
    举例:比如我们设置arr=7199,psc=9999
    我们将72MHz (1M等于10的6次方) 分成了(9999+1)等于 7200Hz,每记录一个数就是1/7200秒
    我们这里记录7200个数进入定时器更新(7199+1)x(1/7200)=1s,也就是1s进入一次更新Update。

    展开全文
  • ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现(若对HashMap的实现原理还不甚了解,可参考另一篇文章HashMap实现原理及源码分析),ConcurrentHashMap在并发编程的场景中使用频率非常之高...

    ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现(若对HashMap的实现原理还不甚了解,可参考另一篇文章HashMap实现原理及源码分析),ConcurrentHashMap在并发编程的场景中使用频率非常之高,本文就来分析下ConcurrentHashMap的实现原理,并对其实现原理进行分析(JDK1.7).


    众所周知,哈希表是中非常高效,复杂度为O(1)的数据结构,在Java开发中,我们最常见到最频繁使用的就是HashMap和HashTable,但是在线程竞争激烈的并发场景中使用都不够合理。

      HashMap :先说HashMap,HashMap是线程不安全的,在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的。

      HashTable : HashTable和HashMap的实现原理几乎一样,差别无非是1.HashTable不允许key和value为null;2.HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。




      HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想。

      

    ConcurrentHashMap源码分析   

    ConcurrentHashMap采用了非常精妙的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组。

     final Segment<K,V>[] segments;

      Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。(就按默认的ConcurrentLeve为16来讲,理论上就允许16个线程并发执行,有木有很酷)

      所以,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。

    Segment类似于HashMap,一个Segment维护着一个HashEntry数组

     transient volatile HashEntry<K,V>[] table;

    HashEntry是目前我们提到的最小的逻辑处理单元了。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。

    复制代码
     static final class HashEntry<K,V> {
            final int hash;
            final K key;
            volatile V value;
            volatile HashEntry<K,V> next;
            //其他省略
    }    
    复制代码

    我们说Segment类似哈希表,那么一些属性就跟我们之前提到的HashMap差不离,比如负载因子loadFactor,比如阈值threshold等等,看下Segment的构造方法

    Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
                this.loadFactor = lf;//负载因子
                this.threshold = threshold;//阈值
                this.table = tab;//主干数组即HashEntry数组
            }

    我们来看下ConcurrentHashMap的构造方法

    复制代码
     1  public ConcurrentHashMap(int initialCapacity,
     2                                float loadFactor, int concurrencyLevel) {
     3           if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
     4               throw new IllegalArgumentException();
     5           //MAX_SEGMENTS 为1<<16=65536,也就是最大并发数为65536
     6           if (concurrencyLevel > MAX_SEGMENTS)
     7               concurrencyLevel = MAX_SEGMENTS;
     8           //2的sshif次方等于ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
     9          int sshift = 0;
    10          //ssize 为segments数组长度,根据concurrentLevel计算得出
    11          int ssize = 1;
    12          while (ssize < concurrencyLevel) {
    13              ++sshift;
    14              ssize <<= 1;
    15          }
    16          //segmentShift和segmentMask这两个变量在定位segment时会用到,后面会详细讲
    17          this.segmentShift = 32 - sshift;
    18          this.segmentMask = ssize - 1;
    19          if (initialCapacity > MAXIMUM_CAPACITY)
    20              initialCapacity = MAXIMUM_CAPACITY;
    21          //计算cap的大小,即Segment中HashEntry的数组长度,cap也一定为2的n次方.
    22          int c = initialCapacity / ssize;
    23          if (c * ssize < initialCapacity)
    24              ++c;
    25          int cap = MIN_SEGMENT_TABLE_CAPACITY;
    26          while (cap < c)
    27              cap <<= 1;
    28          //创建segments数组并初始化第一个Segment,其余的Segment延迟初始化
    29          Segment<K,V> s0 =
    30              new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
    31                               (HashEntry<K,V>[])new HashEntry[cap]);
    32          Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    33          UNSAFE.putOrderedObject(ss, SBASE, s0); 
    34          this.segments = ss;
    35      }
    复制代码

      初始化方法有三个参数,如果用户不指定则会使用默认值,initialCapacity为16,loadFactor为0.75(负载因子,扩容时需要参考),concurrentLevel为16。

      从上面的代码可以看出来,Segment数组的大小ssize是由concurrentLevel来决定的,但是却不一定等于concurrentLevel,ssize一定是大于或等于concurrentLevel的最小的2的次幂。比如:默认情况下concurrentLevel是16,则ssize为16;若concurrentLevel为14,ssize为16;若concurrentLevel为17,则ssize为32。为什么Segment的数组大小一定是2的次幂?其实主要是便于通过按位与的散列算法来定位Segment的index。至于更详细的原因,有兴趣的话可以参考我的另一篇文章HashMap实现原理及源码分析,其中对于数组长度为什么一定要是2的次幂有较为详细的分析。

      接下来,我们来看看put方法

    复制代码
     public V put(K key, V value) {
            Segment<K,V> s;
            //concurrentHashMap不允许key/value为空
            if (value == null)
                throw new NullPointerException();
            //hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
            int hash = hash(key);
            //返回的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment
            int j = (hash >>> segmentShift) & segmentMask;
            if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
                 (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
                s = ensureSegment(j);
            return s.put(key, hash, value, false);
        }
    复制代码

     从源码看出,put的主要逻辑也就两步:1.定位segment并确保定位的Segment已初始化 2.调用Segment的put方法

     关于segmentShift和segmentMask

      segmentShift和segmentMask这两个全局变量的主要作用是用来定位Segment,int j =(hash >>> segmentShift) & segmentMask。

      segmentMask:段掩码,假如segments数组长度为16,则段掩码为16-1=15;segments长度为32,段掩码为32-1=31。这样得到的所有bit位都为1,可以更好地保证散列的均匀性

      segmentShift:2的sshift次方等于ssize,segmentShift=32-sshift。若segments长度为16,segmentShift=32-4=28;若segments长度为32,segmentShift=32-5=27。而计算得出的hash值最大为32位,无符号右移segmentShift,则意味着只保留高几位(其余位是没用的),然后与段掩码segmentMask位运算来定位Segment。

      get/put方法

      get方法

    复制代码
     public V get(Object key) {
            Segment<K,V> s; 
            HashEntry<K,V>[] tab;
            int h = hash(key);
            long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    //先定位Segment,再定位HashEntry
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) { K k; if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; }
    复制代码

      get方法无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。

      来看下concurrentHashMap代理到Segment上的put方法,Segment中的put方法是要加锁的。只不过是锁粒度细了而已。

    复制代码
    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
                HashEntry<K,V> node = tryLock() ? null :
                    scanAndLockForPut(key, hash, value);//tryLock不成功时会遍历定位到的HashEnry位置的链表(遍历主要是为了使CPU缓存链表),若找不到,则创建HashEntry。tryLock一定次数后(MAX_SCAN_RETRIES变量决定),则lock。若遍历过程中,由于其他线程的操作导致链表头结点变化,则需要重新遍历。
                V oldValue;
                try {
                    HashEntry<K,V>[] tab = table;
                    int index = (tab.length - 1) & hash;//定位HashEntry,可以看到,这个hash值在定位Segment时和在Segment中定位HashEntry都会用到,只不过定位Segment时只用到高几位。
                    HashEntry<K,V> first = entryAt(tab, index);
                    for (HashEntry<K,V> e = first;;) {
                        if (e != null) {
                            K k;
                            if ((k = e.key) == key ||
                                (e.hash == hash && key.equals(k))) {
                                oldValue = e.value;
                                if (!onlyIfAbsent) {
                                    e.value = value;
                                    ++modCount;
                                }
                                break;
                            }
                            e = e.next;
                        }
                        else {
                            if (node != null)
                                node.setNext(first);
                            else
                                node = new HashEntry<K,V>(hash, key, value, first);
                            int c = count + 1;
                  //若c超出阈值threshold,需要扩容并rehash。扩容后的容量是当前容量的2倍。这样可以最大程度避免之前散列好的entry重新散列,具体在另一篇文章中有详细分析,不赘述。扩容并rehash的这个过程是比较消耗资源的。
    if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); } return oldValue; }
    复制代码

     总结

      ConcurrentHashMap作为一种线程安全且高效的哈希表的解决方案,尤其其中的"分段锁"的方案,相比HashTable的全表锁在性能上的提升非常之大。本文对ConcurrentHashMap的实现原理进行了详细分析,并解读了部分源码,希望能帮助到有需要的童鞋。

     



    展开全文
  • ConcurrentHashMap 的工作原理代码实现 ConcurrentHashMap 的工作原理代码实现 HashTable里使用的是synchronized关键字,这其实是对对象加锁,锁住的都是对象整体,当Hashtable的大小增加到一定的时候,性能会...
  • 看不清的话自己把图片存下来把,个人认为这都是我从网上总结下来,然后抄到本子上的,懒得再敲一遍,直接就上图了,第二张图片网上有,大家可以去看一下...
  • MG995舵机工作原理及基于STM32的驱动源代码

    万次阅读 多人点赞 2018-01-29 14:55:43
    MG995舵机工作原理及基于STM32的驱动源代码 一·MG995舵机工作原理 1.MG995舵机简介 产品型号 MG995 产品尺寸 40.7*19.7*42.9mm 产品重量 55g 工作扭矩 13KG/cm 反应转速 53-62R/M 使用温度 -30~+60° ...
  • Android代码混淆ProGuard工作原理简介

    千次阅读 2016-10-27 19:39:24
    ProGuard能够对Java类中的代码进行压缩(Shrink),优化(Optimize),混淆(Obfuscate),预检(Preveirfy)。  1. 压缩(Shrink): 在压缩处理这一步中,用于检测和删除没有使用的类,字段,方法和属性。  2. 优化...
  • 概述 从本文你可以学习到: ...你知道HashMap的工作原理吗? 你知道get和put的原理吗?equals()和hashCode()的都有什么作用? 你知道hash的实现吗?为什么要这样实现? 如果HashMap的大小超过了负载因子(lo...
  • 1.什么叫多任务? 什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。...
  • KVM源代码分析1:基本工作原理

    千次阅读 2016-08-04 16:45:52
    13年的时候准备挖“KVM源代码分析”的坑,陆陆续续2年过去了,坑也没有填上,当时是因为对KVM了解的肤浅,真正的理解必然要深入到代码级别,所谓“摈弃皮毛,看到血肉,看到真相”,当时计划写KVM基本工作原理、...
  • 静态显示原理:LED显示器工作方式有两种:静态显示方式和动态显示方式。静态显示的特点是每个数码管的段选必须接一个8位数据线来保持显示的字形码。当送入一次字形码后,显示字形可一直保持,直到送入新字形码为止。...
  • RO代码跟踪 之 服务端工作原理

    千次阅读 2010-07-13 22:40:00
    分析到这里,准备工作应该都完成了,剩下就看IndyServer监控网络并调用工厂类对象工作了.直觉上感觉应该是 IndyServer开一个线程监控网络并在接收新连接时开新线程,接收信息后调用工厂类生成接口实现对象来响应请求. ...
  • 一、移位寄存器工作原理代码分析(以MAX7219芯片为例) 首先要明白DIN管脚的含义,其为串行数据输入端口,在时钟上升沿时数据被载入内部的 16 位寄存器。 而CLK即为时钟序列输入端,所以当要输入数据时,先得把...
  • 动态数码管的工作原理代码实现

    千次阅读 2019-09-30 18:14:27
    数码管动态显示原理:动态显示的特点是将所有数码管的段选并联在一起,有位选控制是哪一位数码管有效。选亮数码管采用动态扫描显示。所谓动态扫描显示即轮流向各位数码管送出字形码和相应的位选,利用发光管的余晖和...
  • 客户端网络通信的工作原理
  • SVN工作原理(帮助实现代码管理)

    千次阅读 2016-04-26 15:08:32
    svn工作原理:在svn服务器上每个源代码文件都有一个版本(1、2、3),假设服务器上的代码文件原始版本是1,张三下载下来,修改后这个版本自动+1,变成了2,他把这个2上传到服务器。李四之前也下载了版本为1的这个...
  • HashMap参考:JavaHashMap工作原理及实现 http://www.importnew.com/18633.html HashMap的底层数组长度总是2的n次方,在构造函数中存在:capacity &lt;&lt;= 1;这样做总是能够保证HashMap的底层数组长度为2...
  • 1 Topic类型工作原理 1.1原理: 前面讲到direct类型的Exchange路由规则是完全匹配binding key与routing key,但这种严格的匹配方式在很多情况下不能满足实际业务需求。 topic类型的Exchange在匹配规则上进行...
  • Gerrit 代码审核服务器的工作流和原理               aa
  • Ajax工作原理,Eclipse,Java实现样例代码

    千次阅读 多人点赞 2019-03-19 14:35:36
    Ajax工作机制 1. 认识Ajax Ajax是Asynchronous JavaScript and XML的简称,即异步的 JavaScript 和 XML。AJAX 不是新的编程语言,而是一种使用现有标准的新方法。最大的优点是在不重新加载整个页面的情况下,可以与...
  • 1 fanout类型工作原理 fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中: 当生成者将消息投递到exchange_fanout_clevercode。交互机exchange_fanout_...
  • 1 RabbitMQ交换机Direct类型工作原理 2 PHP使用样例 http://php.net/manual/fa/book.amqp.php,使用文档。 消费者进程consumer.php &amp;amp;amp;amp;amp;lt;?php //配置信息 $conn_args = array( '...
  • 常用ASP代码加密工具的工作原理

    千次阅读 2012-02-06 16:22:30
    打开ASP文件,经常看到类似这样的代码,创建一个组件,然后传入一大堆没有意义的字符,如下: Dim obj Set obj = Server.CreateObject("AspDeCode.DeCode") obj.AddCode "PCUNCm9wdGlvbiBleHBsaWNpdA0KQ29uc3...
  • xhprof的工作原理  xhprof是php的第三方扩展,工作在zend层,以动态加载的方式被加载。php在解释执行的过程中,首先被解释成对应的token码,而后,编译成字节码(这里的字节码不是机器码,在PHP里面...
  • jQuery工作原理解析以及源代码示例 jQuery的开篇声明里有一段非常重要的话:jQuery是为了改变javascript的编码方式而设计的.从这段话可以看出jQuery本身并不是UI组件库或其他的一般AJAX类库.jQuery改变javascript...
  • Ogre源代码浅析——插件(Plugin)工作原理  Ogre引擎由多个模块组成,从不同角度来划分可以得到不同的结果。从功能上看Ogre可大致分为资源管理、场景管理和渲染管理三大模块;而从可执行部分的组织方式看,Ogre...
  •  要在服务端使用对象池的功能,需要在XXX_Impl中uses uROClassFactories单元,并将其initialization小节中代码 TROClassFactory.Create('FirstSampleService', Create_FirstSampleService, ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 18,833
精华内容 7,533
关键字:

代码工作原理