精华内容
下载资源
问答
  • RT。往大师围观。java的指针封装的c那一块的指针嘛?java比较c又一次。nnd
  • java底层知识

    2020-04-24 15:52:15
    文章目录一、谈谈你对java的理解二、平台无关性三、JVM如何...面向对象 继承 封装 多态 类库 异常处理 二、平台无关性 编译时 javac编译成class字节码 运行时 java运行class文件 javap jdk自带的反编译器 cd src...

    一、谈谈你对java的理解

    • 平台无关性,一次编译多处运行
    • GC 垃圾回收机制
    • 语言特性 泛型、反射、lambda
    • 面向对象 继承 封装 多态
    • 类库
    • 异常处理

    二、平台无关性

    • 编译时 javac编译成class字节码
    • 运行时 java运行class文件
    • javap jdk自带的反编译器
    cd src
    javac com/my/Demo.java
    java com.my.Demo
    javap -c com.my.Demo  # -c  是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息
    

    java源码首先被编译成字节码,再由不同平台的JVM进行解析,java语言在不同的平台商运行时不需要进行重新编译,java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令

    为什么JVM不直接将源码解析成机器码去执行(先编译成class文件)?

    • 准备工作:每次执行都需要各种检查
    • 兼容性:也可以将别的语言解析成字节码

    三、JVM如何加载class文件

    Java虚拟机

    Jvm是个内存的处理机
    在这里插入图片描述

    • class loader:依据特定格式,加载class文件到内存
    • Execution Engine:对命令进行解析
    • Native Interface:融合不同开发语言的原生库为java所用
    • Runtime Data Area: jvm 内存空间结构模型
    展开全文
  • 目前在Java中存在两种锁机制:synchronized和Lock,Lock接口及其实现类是JDK5增加的内容,其作者是大名鼎鼎的并发专家Doug Lea。本文并不比较synchronized与Lock孰优孰劣,只是介绍二者的实现原理。数据同步需要依赖...

    欢迎转载,请附出处:
    http://blog.csdn.net/as02446418/article/details/47125131

    目前在Java中存在两种锁机制:synchronized和Lock,Lock接口及其实现类是JDK5增加的内容,其作者是大名鼎鼎的并发专家Doug Lea。本文并不比较synchronized与Lock孰优孰劣,只是介绍二者的实现原理。

    数据同步需要依赖锁,那锁的同步又依赖谁?synchronized给出的答案是在软件层面依赖JVM,而Lock给出的方案是在硬件层面依赖特殊的CPU指令,大家可能会进一步追问:JVM底层又是如何实现synchronized的?

    本文所指说的JVM是指Hotspot的6u23版本,下面首先介绍synchronized的实现:

    synrhronized关键字简洁、清晰、语义明确,因此即使有了Lock接口,使用的还是非常广泛。其应用层的语义是可以把任何一个非null对象 作为”锁”,当synchronized作用在方法上时,锁住的便是对象实例(this);当作用在静态方法时锁住的便是对象对应的Class实例,因为 Class数据存在于永久带,因此静态方法锁相当于该类的一个全局锁;当synchronized作用于某一个对象实例时,锁住的便是对应的代码块。在 HotSpot JVM实现中,锁有个专门的名字:对象监视器。

    1. 线程状态及状态转换

    当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:

    Contention List:所有请求锁的线程将被首先放置到该竞争队列

    Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List

    Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set

    OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck

    Owner:获得锁的线程称为Owner

    !Owner:释放锁的线程

    下图反映了个状态转换关系:
    这里写图片描述
    新请求锁的线程将首先被加入到ConetentionList中,当某个拥有锁的线程(Owner状态)调用unlock之后,如果发现 EntryList为空则从ContentionList中移动线程到EntryList,下面说明下ContentionList和EntryList 的实现方式:

    1.1 ContentionList 虚拟队列
    ContentionList并不是一个真正的Queue,而只是一个虚拟队列,原因在于ContentionList是由Node及其next指 针逻辑构成,并不存在一个Queue的数据结构。ContentionList是一个后进先出(LIFO)的队列,每次新加入Node时都会在队头进行, 通过CAS改变第一个节点的的指针为新增节点,同时设置新增节点的next指向后续节点,而取得操作则发生在队尾。显然,该结构其实是个Lock- Free的队列。

    因为只有Owner线程才能从队尾取元素,也即线程出列操作无争用,当然也就避免了CAS的ABA问题。
    这里写图片描述
    1.2 EntryList

    EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发访问,为了降低对 ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到 EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给 OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在 Hotspot中把OnDeck的选择行为称之为“竞争切换”。

    OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中,考虑到公平性,在EntryList中的位置不 发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒, 则再次转移到EntryList。

    2. 自旋锁

    那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通 过pthread_mutex_lock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响 锁的性能

    缓解上述问题的办法便是自旋,其原理是:当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程可以稍微等一等(自旋), 在Owner线程释放锁后,争用线程可能会立即得到锁,从而避免了系统阻塞。但Owner运行的时间可能会超出了临界值,争用线程自旋一段时间后还是无法 获得锁,这时争用线程则会停止自旋进入阻塞状态(后退)。基本思路就是自旋,不成功再阻塞,尽量降低阻塞的可能性,这对那些执行时间很短的代码块来说有非 常重要的性能提高。自旋锁有个更贴切的名字:自旋-指数后退锁,也即复合锁。很显然,自旋在多处理器上才有意义。

    还有个问题是,线程自旋时做些啥?其实啥都不做,可以执行几次for循环,可以执行几条空的汇编指令,目的是占着CPU不放,等待获取锁的机 会。所以说,自旋是把双刃剑,如果旋的时间过长会影响整体性能,时间过短又达不到延迟阻塞的目的。显然,自旋的周期选择显得非常重要,但这与操作系统、硬 件体系、系统的负载等诸多场景相关,很难选择,如果选择不当,不但性能得不到提高,可能还会下降,因此大家普遍认为自旋锁不具有扩展性。

    自旋优化策略

    对自旋锁周期的选择上,HotSpot认为最佳时间应是一个线程上下文切换的时间,但目前并没有做到。经过调查,目前只是通过汇编暂停了几个CPU周期,除了自旋周期选择,HotSpot还进行许多其他的自旋优化策略,具体如下:

    如果平均负载小于CPUs则一直自旋

    如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞

    如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞

    如果CPU处于节电模式则停止自旋

    自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)

    自旋时会适当放弃线程优先级之间的差异

    那synchronized实现何时使用了自旋锁?答案是在线程进入ContentionList时,也即第一步操作前。线程在进入等待队列时 首先进行自旋尝试获得锁,如果不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平。还有一个不公平的地方是自旋线程可能会抢占了 Ready线程的锁。自旋锁由每个监视对象维护,每个监视对象一个。

    3. JVM1.6偏向锁

    在JVM1.6中引入了偏向锁,偏向锁主要解决无竞争下的锁性能问题,首先我们看下无竞争下锁存在什么问题:

    现在几乎所有的锁都是可重入的,也即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操 作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个 线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。但还有很多概念需要解释、很多引入的 问题需要解决:

    3.1 CAS及SMP架构

    CAS为什么会引入本地延迟?这要从SMP(对称多处理器)架构说起,下图大概表明了SMP的结构:
    这里写图片描述
    其意思是所有的CPU会共享一条系统总线(BUS),靠此总线连接主存。每个核都有自己的一级缓存,各核相对于BUS对称分布,因此这种结构称为“对称多处理器”。

    而CAS的全称为Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子地更新某个位置的值,经过调查发现, 其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的 接口。

    Core1和Core2可能会同时把主存中某个位置的值Load到自己的L1 Cache中,当Core1在自己的L1 Cache中修改这个位置的值时,会通过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现自己L1 Cache中的值失效(称为Cache命中缺失)则会通过总线从内存中加载该地址最新的值,大家通过总线的来回通信称为“Cache一致性流量”,因为总 线被设计为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,称为“Cache一致 性”,从这个层面来说,锁设计的终极目标便是减少Cache一致性流量。

    而CAS恰好会导致Cache一致性流量,如果有很多线程都共享同一个对象,当某个Core CAS成功时必然会引起总线风暴,这就是所谓的本地延迟,本质上偏向锁就是为了消除CAS,降低Cache一致性流量。

    Cache一致性:

    上面提到Cache一致性,其实是有协议支持的,现在通用的协议是MESI(最早由Intel开始支持),具体参考:http://en.wikipedia.org/wiki/MESI_protocol,以后会仔细讲解这部分。

    Cache一致性流量的例外情况:

    其实也不是所有的CAS都会导致总线风暴,这跟Cache一致性协议有关,具体参考:http://blogs.oracle.com/dave/entry/biased_locking_in_hotspot

    NUMA(Non Uniform Memory Access Achitecture)架构:

    与SMP对应还有非对称多处理器架构,现在主要应用在一些高端处理器上,主要特点是没有总线,没有公用主存,每个Core有自己的内存,针对这种结构此处不做讨论。

    3.2 偏向解除

    偏向锁引入的一个重要问题是,在多争用的场景下,如果另外一个线程争用偏向对象,拥有者需要释放偏向锁,而释放的过程会带来一些性能开销,但总体说来偏向锁带来的好处还是大于CAS代价的。

    4. 总结

    关于锁,JVM中还引入了一些其他技术比如锁膨胀等,这些与自旋锁、偏向锁相比影响不是很大,这里就不做介绍。

    通过上面的介绍可以看出,synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。


    原文地址:
    http://www.open-open.com/lib/view/open1352431526366.html

    展开全文
  • 目录 1.内存屏障 2. 本文介绍了内存屏障对多线程程序的影响,同时将研究内存屏障与JVM并发机制的关系,如易变量(volatile)、同步...本文假定读者已经充分掌握了相关概念和Java内存模型,不讨论并发互斥...

    目录

    1. 内存屏障

    2. 

     

    本文介绍了内存屏障对多线程程序的影响,同时将研究内存屏障与JVM并发机制的关系,如易变量(volatile)、同步(synchronized)和原子条件式(atomic conditional)。

    一. 内存屏障

    内存屏障,又称内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。本文假定读者已经充分掌握了相关概念和Java内存模型,不讨论并发互斥、并行机制和原子性。内存屏障用来实现并发编程中称为可见性(visibility)的同样重要的作用。

    关于JVM更多内容,请参阅:JVM详解 Java虚拟机原理与优化

    内存屏障为何重要?

    对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本,这些缓存为了性能重新排列待定内存操作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。当数据是不可变的,同时/或者数据限制在线程范围内,这些优化是无害的。

    如果把这些优化与对称多处理(symmetric multi-processing)和共享可变状态(shared mutable state)结合,那么就是一场噩梦。当基于共享可变状态的内存操作被重新排序时,程序可能行为不定。一个线程写入的数据可能被其他线程可见,原因是数据 写入的顺序不一致。适当的放置内存屏障通过强制处理器顺序执行待定的内存操作来避免这个问题。

    内存屏障的协调作用

    内存屏障不直接由JVM暴露,相反它们被JVM插入到指令序列中以维持语言层并发原语的语义。我们研究几个简单Java程序的源代码和汇编指令。首先快速看一下Dekker算法中的内存屏障。该算法利用volatile变量协调两个线程之间的共享资源访问。

    请不要关注该算法的出色细节。哪些部分是相关的?每个线程通过发信号试图进入代码第一行的关键区域。如果线程在第三行意识到冲突(两个线程都要访问),通过turn变量的操作来解决。在任何时刻只有一个线程可以访问关键区域。

    // code run by first thread     // code run by second thread  
     
     1    intentFirst = true;          intentSecond = true;  
     2  
     3    while (intentSecond)   while (intentFirst)       // volatile read  
     4     if (turn != 0) {      if (turn != 1) {       // volatile read  
     5       intentFirst = false;        intentSecond = false;  
     6       while (turn != 0) {}        while (turn != 1) {}  
     7       intentFirst = true;        intentSecond = true;  
     8     }               }  
     9  
    10    criticalSection();   criticalSection();  
    11  
    12    turn = 1;     turn = 0;                 // volatile write  
    13    intentFirst = false;   intentSecond = false;     // volatile write 

    硬件优化可以在没有内存屏障的情况下打乱这段代码,即使编译器按照程序员的想法顺序列出所有的内存操作。考虑第三、四行的两次顺序volatile读操作。每一个线程检查其他线程是否发信号想进入关键区域,然后检查轮到谁操作了。考虑第12、13行的两次顺序写操作。每一个线程把访问权释放给其他线程, 然后撤销自己访问关键区域的意图。读线程应该从不期望在其他线程撤销访问意愿后观察到其他线程对turn变量的写操作。这是个灾难。

    但是如果这些变量没有 volatile修饰符,这的确会发生!例如,没有volatile修饰符,第二个线程在第一个线程对turn执行写操作(倒数第二行)之前可能会观察到 第一个线程对intentFirst(倒数第一行)的写操作。关键词volatile避免了这种情况,因为它在对turn变量的写操作和对 intentFirst变量的写操作之间创建了一个先后关系。编译器无法重新排序这些写操作,如果必要,它会利用一个内存屏障禁止处理器重排序。让我们来 看看一些实现细节。

    PrintAssembly HotSpot选项是JVM的一个诊断标志,允许我们获取JIT编译器生成的汇编指令。这需要最新的OpenJDK版本或者新HotSpot update14或者更高版本。通过需要一个反编译插件。Kenai项目提供了用于Solaris、Linux和BSD的插件二进制文件。hsdis是另 一款可以在Windows通过源码构建的插件。

    两次顺序读操作的第一次(第三行)的汇编指令如下。指令流基于Itanium 2多处理硬件、JDK 1.6 update 17。本文的所有指令流都在左手边以行号标记。相关的读操作、写操作和内存屏障指令都以粗体标记。建议读者不要沉迷于每一行指令。

    1  0x2000000001de819c:      adds r37=597,r36;;  ;...84112554  
    2  0x2000000001de81a0:      ld1.acq r38=[r37];;  ;...0b30014a a010  
    3  0x2000000001de81a6:      nop.m 0x0     ;...00000002 00c0  
    4  0x2000000001de81ac:      sxt1 r38r38=r38;;  ;...00513004  
    5  0x2000000001de81b0:      cmp4.eq p0,p6=0,r38  ;...1100004c 8639  
    6  0x2000000001de81b6:      nop.i 0x0     ;...00000002 0003  
    7  0x2000000001de81bc:      br.cond.dpnt.many 0x2000000001de8220; 

    简短的指令流其实内容丰富。第一次volatile位于第二行。Java内存模型确保了JVM会在第二次读操作之前将第一次读操作交给处理器,也就是按照 “程序的顺序”——但是这单单一行指令是不够的,因为处理器仍然可以自由乱序执行这些操作。为了支持Java内存模型的一致性,JVM在第一次读操作上添加了注解ld.acq,也就是“载入获取”(load acquire)。通过使用ld.acq,编译器确保第二行的读操作在接下来的读操作之前完成,问题就解决了。

    请注意这影响了读操作,而不是写。内存屏障强制读或写操作顺序限制不是单向的。强制读和写操作顺序限制的内存屏障是双向的,类似于双向开的栅栏。使用ld.acq就是单向内存屏障的例子。

    一致性具有两面性。如果一个读线程在两次读操作之间插入了内存屏障而另外一个线程没有在两次写操作之间添加内存屏障又有什么用呢?线程为了协调,必须同时 遵守这个协议,就像网络中的节点或者团队中的成员。如果某个线程破坏了这个约定,那么其他所有线程的努力都白费。Dekker算法的最后两行代码的汇编指令应该插入一个内存屏障,两次volatile写之间。

    $ java -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes   
    -XX:CompileCommand=print,WriterReader.write WriterReader   
     1  0x2000000001de81c0:      adds r37=592,r36;;  ;...0b284149 0421  
     2  0x2000000001de81c6:      st4.rel [r37]=r39  ;...00389560 2380  
     3  0x2000000001de81cc:      adds r36=596,r36;;  ;...84112544  
     4  0x2000000001de81d0:      st1.rel [r36]=r0  ;...09000048 a011  
     5  0x2000000001de81d6:      mf            ;...00000044 0000  
     6  0x2000000001de81dc:      nop.i 0x0;;   ;...00040000  
     7  0x2000000001de81e0:      mov r12=r33   ;...00600042 0021  
     8  0x2000000001de81e6:      mov.ret b0=r35,0x2000000001de81e0  
     9  0x2000000001de81ec:      mov.i ar.pfs=r34  ;...00aa0220  
    10  0x2000000001de81f0:      mov r6=r32    ;...09300040 0021 

    内存屏障是特定于硬件的

    本文不想针对所有内存屏障做一综述。这将是一件不朽的功绩。但是,重要的是认识到这些指令在不同的硬件体系中迥异。下面的指令是连续写操作在多处理 Intel Xeon硬件上编译的结果。本文后面的所有汇编指令除非特殊声明否则都出自于Intel Xeon。

    1  0x03f8340c: push   %ebp               ;...55  
     2  0x03f8340d: sub    $0x8,%esp          ;...81ec0800 0000  
     3  0x03f83413: mov    $0x14c,%edi        ;...bf4c0100 00  
     4  0x03f83418: movb   $0x1,-0x505a72f0(%edi)  ;...c687108d a5af01  
     5  0x03f8341f: mfence                    ;...0faef0  
     6  0x03f83422: mov    $0x148,%ebp        ;...bd480100 00  
     7  0x03f83427: mov    $0x14d,%edx        ;...ba4d0100 00  
     8  0x03f8342c: movsbl -0x505a72f0(%edx),%ebx  ;...0fbe9a10 8da5af  
     9  0x03f83433: test   %ebx,%ebx          ;...85db  
    10  0x03f83435: jne    0x03f83460         ;...7529  
    11  0x03f83437: movl   $0x1,-0x505a72f0(%ebp)  ;...c785108d a5af01  
    12  0x03f83441: movb   $0x0,-0x505a72f0(%edi)  ;...c687108d a5af00  
    13  0x03f83448: mfence                    ;...0faef0  
    14  0x03f8344b: add    $0x8,%esp          ;...83c408  
    15  0x03f8344e: pop    %ebp               ;...5d 

    我们可以看到x86 Xeon在第11、12行执行两次volatile写操作。第二次写操作后面紧跟着mfence操作——显式的双向内存屏障,下面的连续写操作基于SPARC。

     1 0xfb8ecc84: ldub  [ %l1 + 0x155 ], %l3  ;...e60c6155  
     2 0xfb8ecc88: cmp  %l3, 0               ;...80a4e000  
     3 0xfb8ecc8c: bne,pn   %icc, 0xfb8eccb0  ;...12400009  
     4 0xfb8ecc90: nop                       ;...01000000  
     5 0xfb8ecc94: st  %l0, [ %l1 + 0x150 ]  ;...e0246150  
     6 0xfb8ecc98: clrb  [ %l1 + 0x154 ]     ;...c02c6154  
     7 0xfb8ecc9c: membar  #StoreLoad        ;...8143e002  
     8 0xfb8ecca0: sethi  %hi(0xff3fc000), %l0  ;...213fcff0  
     9 0xfb8ecca4: ld  [ %l0 ], %g0          ;...c0042000  
    10 0xfb8ecca8: ret                       ;...81c7e008  
    11 0xfb8eccac: restore                   ;...81e80000 

    我们看到在第五、六行存在两次volatile写操作。第二次写操作后面是一个membar指令——显式的双向内存屏障。x86和SPARC的指令流与Itanium的指令流存在一个重要区别。JVM在x86和SPARC上通过内存屏障跟踪连续写操作,但是在两次写操作之间没有放置内存屏障。

    另一方面,Itanium的指令流在两次写操作之间存在内存屏障。为何JVM在不同的硬件架构之间表现不一?因为硬件架构都有自己的内 存模型,每一个内存模型有一套一致性保障。某些内存模型,如x86和SPARC等,拥有强大的一致性保障。另一些内存模型,如Itanium、 PowerPC和Alpha,是一种弱保障。

    例如,x86和SPARC不会重新排序连续写操作——也就没有必要放置内存屏障。Itanium、 PowerPC和Alpha将重新排序连续写操作——因此JVM必须在两者之间放置内存屏障。JVM使用内存屏障减少Java内存模型和硬件内存模型之间的距离。

    隐式内存屏障

    显式屏障指令不是序列化内存操作的唯一方式。让我们再看一看Counter类这个例子。

    class Counter{  
     
        static int counter = 0;  
     
        public static void main(String[] _){  
            for(int i = 0; i < 100000; i++)  
                inc();  
        }  
     
        static synchronized void inc(){ counter += 1; }  
     
    } 

    Counter类执行了一个典型的读-修改-写的操作。静态counter字段不是volatile的,因为所有三个操作必须要原子可见的。因此,inc 方法是synchronized修饰的。我们可以采用下面的命令编译Counter类并查看生成的汇编指令。Java内存模型确保了synchronized区域的退出和volatile内存操作都是相同的可见性,因此我们应该预料到会有另一个内存屏障。

    $ java -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes   
    -XX:-UseBiasedLocking -XX:CompileCommand=print,Counter.inc Counter   
     1  0x04d5eda7: push   %ebp               ;...55  
     2  0x04d5eda8: mov    %esp,%ebp          ;...8bec  
     3  0x04d5edaa: sub    $0x28,%esp         ;...83ec28  
     4  0x04d5edad: mov    $0x95ba5408,%esi   ;...be0854ba 95  
     5  0x04d5edb2: lea    0x10(%esp),%edi    ;...8d7c2410  
     6  0x04d5edb6: mov    %esi,0x4(%edi)     ;...897704  
     7  0x04d5edb9: mov    (%esi),%eax        ;...8b06  
     8  0x04d5edbb: or     $0x1,%eax          ;...83c801  
     9  0x04d5edbe: mov    %eax,(%edi)        ;...8907  
    10  0x04d5edc0: lock cmpxchg %edi,(%esi)  ;...f00fb13e  
    11  0x04d5edc4: je     0x04d5edda         ;...0f841000 0000  
    12  0x04d5edca: sub    %esp,%eax          ;...2bc4  
    13  0x04d5edcc: and    $0xfffff003,%eax   ;...81e003f0 ffff  
    14  0x04d5edd2: mov    %eax,(%edi)        ;...8907  
    15  0x04d5edd4: jne    0x04d5ee11         ;...0f853700 0000  
    16  0x04d5edda: mov    $0x95ba52b8,%eax   ;...b8b852ba 95  
    17  0x04d5eddf: mov    0x148(%eax),%esi   ;...8bb04801 0000  
    18  0x04d5ede5: inc    %esi               ;...46  
    19  0x04d5ede6: mov    %esi,0x148(%eax)   ;...89b04801 0000  
    20  0x04d5edec: lea    0x10(%esp),%eax    ;...8d442410  
    21  0x04d5edf0: mov    (%eax),%esi        ;...8b30  
    22  0x04d5edf2: test   %esi,%esi          ;...85f6  
    23  0x04d5edf4: je     0x04d5ee07         ;...0f840d00 0000  
    24  0x04d5edfa: mov    0x4(%eax),%edi     ;...8b7804  
    25  0x04d5edfd: lock cmpxchg %esi,(%edi)  ;...f00fb137  
    26  0x04d5ee01: jne    0x04d5ee1f         ;...0f851800 0000  
    27  0x04d5ee07: mov    %ebp,%esp          ;...8be5  
    28  0x04d5ee09: pop    %ebp               ;...5d 

    不出意外,synchronized生成的指令数量比volatile多。第18行做了一次增操作,但是JVM没有显式插入内存屏障。相反,JVM通过在 第10行和第25行cmpxchg的lock前缀一石二鸟。cmpxchg的语义超越了本文的范畴。

    lock cmpxchg不仅原子性执行写操作,也会刷新等待的读写操作。写操作现在将在所有后续内存操作之前完成。如果我们通过java.util.concurrent.atomic.AtomicInteger 重构和运行Counter,将看到同样的手段。

    import java.util.concurrent.atomic.AtomicInteger;  
     
        class Counter{  
     
            static AtomicInteger counter = new AtomicInteger(0);  
     
            public static void main(String[] args){  
                for(int i = 0; i < 1000000; i++)  
                    counter.incrementAndGet();  
            }  
     
        }  
     
    $ java -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes   
    -XX:CompileCommand=print,*AtomicInteger.incrementAndGet Counter   
     1  0x024451f7: push   %ebp               ;...55  
     2  0x024451f8: mov    %esp,%ebp          ;...8bec  
     3  0x024451fa: sub    $0x38,%esp         ;...83ec38  
     4  0x024451fd: jmp    0x0244520a         ;...e9080000 00  
     5  0x02445202: xchg   %ax,%ax            ;...6690  
     6  0x02445204: test   %eax,0xb771e100    ;...850500e1 71b7  
     7  0x0244520a: mov    0x8(%ecx),%eax     ;...8b4108  
     8  0x0244520d: mov    %eax,%esi          ;...8bf0  
     9  0x0244520f: inc    %esi               ;...46  
    10  0x02445210: mov    $0x9a3f03d0,%edi   ;...bfd0033f 9a  
    11  0x02445215: mov    0x160(%edi),%edi   ;...8bbf6001 0000  
    12  0x0244521b: mov    %ecx,%edi          ;...8bf9  
    13  0x0244521d: add    $0x8,%edi          ;...83c708  
    14  0x02445220: lock cmpxchg %esi,(%edi)  ;...f00fb137  
    15  0x02445224: mov    $0x1,%eax          ;...b8010000 00  
    16  0x02445229: je     0x02445234         ;...0f840500 0000  
    17  0x0244522f: mov    $0x0,%eax          ;...b8000000 00  
    18  0x02445234: cmp    $0x0,%eax          ;...83f800  
    19  0x02445237: je     0x02445204         ;...74cb  
    20  0x02445239: mov    %esi,%eax          ;...8bc6  
    21  0x0244523b: mov    %ebp,%esp          ;...8be5  
    22  0x0244523d: pop    %ebp               ;...5d 

    我们又一次在第14行看到了带有lock前缀的写操作。这确保了变量的新值(写操作)会在其他所有后续内存操作之前完成。

    内存屏障能够避免

    JVM非常擅于消除不必要的内存屏障。通常JVM很幸运,因为硬件内存模型的一致性保障强于或者等于Java内存模型。在这种情况下,JVM只是简单地插 入一个no op语句,而不是真实的内存屏障。

    例如,x86和SPARC内存模型的一致性保障足够强壮以消除读volatile变量时所需的内存屏障。还记得在 Itanium上两次读操作之间的显式单向内存屏障吗?x86上的Dekker算法中连续volatile读操作的汇编指令之间没有任何内存屏障。x86平台上共享内存的连续读操作。

    1  0x03f83422: mov    $0x148,%ebp        ;...bd480100 00  
     2  0x03f83427: mov    $0x14d,%edx        ;...ba4d0100 00  
     3  0x03f8342c: movsbl -0x505a72f0(%edx),%ebx  ;...0fbe9a10 8da5af  
     4  0x03f83433: test   %ebx,%ebx          ;...85db  
     5  0x03f83435: jne    0x03f83460         ;...7529  
     6  0x03f83437: movl   $0x1,-0x505a72f0(%ebp)  ;...c785108d a5af01  
     7  0x03f83441: movb   $0x0,-0x505a72f0(%edi)  ;...c687108d a5af00  
     8  0x03f83448: mfence                    ;...0faef0  
     9  0x03f8344b: add    $0x8,%esp          ;...83c408  
    10  0x03f8344e: pop    %ebp               ;...5d  
    11  0x03f8344f: test   %eax,0xb78ec000    ;...850500c0 8eb7  
    12  0x03f83455: ret                       ;...c3  
    13  0x03f83456: nopw   0x0(%eax,%eax,1)   ;...66660f1f 840000  
    14  0x03f83460: mov    -0x505a72f0(%ebp),%ebx  ;...8b9d108d a5af  
    15  0x03f83466: test   %edi,0xb78ec000    ;...853d00c0 8eb7 

    第三行和第十四行存在volatile读操作,而且都没有伴随内存屏障。也就是说,x86和SPARC上的volatile读操作的性能下降对于代码的优化影响很小——指令本身和常规读操作一样。

    单向内存屏障本质上比双向屏障性能要好一些。JVM在确保单向屏障即可的情况下会避免使用双向屏障。本文的第一个例子展示了这点。Itanium平台上的 连续两次读操作被插入单向内存屏障。如果读操作插入显式双向内存屏障,程序仍然正确,但是延迟比较长。

    动态编译

    静态编译器在构建阶段决定的一切事情,在动态编译器那里都可以在运行时决定,甚至更多。更多信息意味着存在更多机会可以优化。例如,让我们看看JVM在单 处理器运行时如何对待内存屏障。以下指令流来自于通过Dekker算法实现两次连续volatile写操作的运行时编译。程序运行于 x86硬件上的单处理器模式中的VMWare工作站镜像。

    1  0x017b474c: push   %ebp               ;...55  
     2  0x017b474d: sub    $0x8,%esp          ;...81ec0800 0000  
     3  0x017b4753: mov    $0x14c,%edi        ;...bf4c0100 00  
     4  0x017b4758: movb   $0x1,-0x507572f0(%edi)  ;...c687108d 8aaf01  
     5  0x017b475f: mov    $0x148,%ebp        ;...bd480100 00  
     6  0x017b4764: mov    $0x14d,%edx        ;...ba4d0100 00  
     7  0x017b4769: movsbl -0x507572f0(%edx),%ebx  ;...0fbe9a10 8d8aaf  
     8  0x017b4770: test   %ebx,%ebx          ;...85db  
     9  0x017b4772: jne    0x017b4790         ;...751c  
    10  0x017b4774: movl   $0x1,-0x507572f0(%ebp)  ;...c785108d 8aaf0111   
    12  0x017b4785: add    $0x8,%esp          ;...83c408  
    13  0x017b4788: pop    %ebp               ;...5d 

    在单处理器系统上,JVM为所有内存屏障插入了一个no op指令,因为内存操作已经序列化了。每一个写操作(第10、11行)后面都跟着一个屏障。JVM针对原子条件式做了类似的优化。下面的指令流来自于同一 个VMWare镜像的AtomicInteger.incrementAndGet动态编译结果。

    1  0x036880f7: push   %ebp               ;...55  
     2  0x036880f8: mov    %esp,%ebp          ;...8bec  
     3  0x036880fa: sub    $0x38,%esp         ;...83ec38  
     4  0x036880fd: jmp    0x0368810a         ;...e9080000 00  
     5  0x03688102: xchg   %ax,%ax            ;...6690  
     6  0x03688104: test   %eax,0xb78b8100    ;...85050081 8bb7  
     7  0x0368810a: mov    0x8(%ecx),%eax     ;...8b4108  
     8  0x0368810d: mov    %eax,%esi          ;...8bf0  
     9  0x0368810f: inc    %esi               ;...46  
    10  0x03688110: mov    $0x9a3f03d0,%edi   ;...bfd0033f 9a  
    11  0x03688115: mov    0x160(%edi),%edi   ;...8bbf6001 0000  
    12  0x0368811b: mov    %ecx,%edi          ;...8bf9  
    13  0x0368811d: add    $0x8,%edi          ;...83c708  
    14  0x03688120: cmpxchg %esi,(%edi)       ;...0fb137  
    15  0x03688123: mov    $0x1,%eax          ;...b8010000 00  
    16  0x03688128: je     0x03688133         ;...0f840500 0000  
    17  0x0368812e: mov    $0x0,%eax          ;...b8000000 00  
    18  0x03688133: cmp    $0x0,%eax          ;...83f800  
    19  0x03688136: je     0x03688104         ;...74cc  
    20  0x03688138: mov    %esi,%eax          ;...8bc6  
    21  0x0368813a: mov    %ebp,%esp          ;...8be5  
    22  0x0368813c: pop    %ebp               ;...5d 

    注意第14行的cmpxchg指令。之前我们看到编译器通过lock前缀把该指令提供给处理器。由于缺少SMP,JVM决定避免这种成本——与静态编译有些不同。

    结束语

    内存屏障是多线程编程的必要装备。它们形式多样,某些是显式的,某些是隐式的。某些是双向的,某些是单向的。JVM利用这些形式在所有平台中有效地支持Java内存模型。我们希望本文能够帮助经验丰富的JVM开发人员了解一些代码在底层如何运行的知识。

    展开全文
  • java作为面向对象编程的一类语言,如何实现了面向对象编程思想),我们还需要简单了解一下具有其他编程思维的一些语言,例如机器语言,汇编语言,面向过程编程语言,了解这些语言的过程实际上也揭示了计算机语言从...

    谈到java的特性,很多程序员会在第一时间说出,“Java是面向对象编程的语言”。这句话本身是没有错的,但是,为了完全理解 Java和面向对象之间的关系(即完全理解什么是面向对象编程,与之不同的还有什么?java作为面向对象编程的一类语言,如何实现了面向对象编程思想),我们还需要简单了解一下具有其他编程思维的一些语言,例如机器语言,汇编语言,面向过程编程语言,了解这些语言的过程实际上也揭示了计算机语言从底层语言到高级语言的变化,从简陋到复杂,从晦涩到易懂的一个过程是如何演变的。

    1 机器语言

    10010100110110
    

    机器语言也就是CPU能够识别的语言。我们都知道所有的计算机程序最终都将交由CPU运行,而CPU的运行机制又由硬件所决定,电气二极管只能有两种状态,通电和不通电,因而最底层的机器语言就只是只由 0 和1 两个数字组成的一串字符,0 表示不通电, 1 表示通电。
    机器语言最大的问题在于其可读性简直是非人类的,并且不同类型的CPU识别机器语言的方式也不同,相同一串机器语言在不同的CPU中可能得出不同的结果。对于程序员而言,不仅需要使用非人类的0和1编写程序,还要准确判断这串字符能否被当前使用的CPU所正确执行,可以想见编程工作的困难程度。

    为了避免直接编写一串0和1的机器语言,进而发明出了汇编语言和汇编编译器。

    2 语言

    MOV AX,C_S
    MOV DS,AX
    LEA DX,P_S
    

    汇编语言在定义上仍然被认为是一种低级语言,这里描述的低级与高级更多的是描述其与人类语言的相似程度。汇编语言由英语单词缩写描述指令动作,由单词字符和 数字描述数据存储地址,一行指令表示一个程序动作,下面提供几个常见的汇编语言指令。

    指令释义
    MOVmove的缩写,传送字或字节
    INinput的缩写,I/O段都输入
    PUSH把字压入堆栈

    可以看到,相较于机器语言,汇编语言至少是人类可读的,这使得编程有了巨大的效率提升。但是我们的CPU只能识别由0和1组成的机器语言,还需要经由汇编编译器编译成机器码后才能执行。

    但是汇编语言仍然是有缺陷的,由于CPU指令集的指令是有限个的。程序员必须在CPU支持的指令格式下书写汇编语言,因而一行汇编代码能执行的动作是有限的,如果使用汇编语言书写一个复杂程序,需要巨大的时间成本。此外,汇编语言在错误预警和处理等方面也较为有限。

    随着社会发展的需要,计算机程序的复杂程度日益攀升,一类相较于汇编语言更具可读性的计算机编程语言应运而生,也就是我们的高级语言,高级语言在编程语法上更加接近人类语言,同时同汇编语言和机器语言之间借由编译器进行翻译一样,高级语言也借由其编译器或解释器最终将代码翻译成CPU可以识别的语言。

    3 面向过程编程

    定义:
    “面向过程”(Procedure Oriented)是一种以过程为中心的编程思想。主要要采取过程调用或函数调用的方式来进行流程控制。流程则由包涵一系列运算步骤的过程(Procedures),例程(routines),子程序(subroutines), 方法(methods),或函数(functions)来控制。在程序运行的任何一个时间点,都可以调用某个特定的程序。任何一个特定的程序,也能被任意一个程序或是它自己本身调用。

    面向过程语言的概念由荷兰科学家,图灵奖获得者 Edsger Dijkstra 在1968年提出。大名鼎鼎的C语言既是一门面向过程语言。面向过程编程语言在编程时,首先需要分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用。其专注于过程中的每个步骤的实现。此外,Edsger Dijkstra 还提出了面向过程语言的三大要素:

    1. 模块化:将一个大程序划分成一个个小的部分,这样易于管理
    2. 隐藏细节:将大程序拆分成小模块后,对于使用的人来说,只要拿来现成的模块使用就好了,不需要知道模块内部的细节。就像开车的司机,只要会操作方向盘就好,不需要明白汽车内部发动机的具体结构。
    3. 抽象:模块隐藏细节之后,人们对于一个复杂程序系统就可以提取出抽象宏观的理解。就像语文课上的提炼中心思想一样。除去了繁复的细节,对于整体理解一个庞大系统是很有帮助的。

    面向过程语言也同样具有抽象概念,即对过程中步骤的抽象,在C语言中,过程的抽象实际上由函数方法来实现,对外仅提供一个函数名称,内部实现的细节相对于调用是隐藏的。

    #include <stdio.h>
    //C语言的函数
    int sum(){
        int i, sum=0;
        for(i=1; i<=100; i++){
            sum+=i;
        }
        return sum;
    }
    
    int main(){
        //调用函数时,函数实现细节相较于调用是隐藏的
        int a = sum();
        printf("The sum is %d\n", a);
        return 0;
    }
    

    4 面向对象编程

    定义:
    面向对象程序设计是种具有对象概念的编程典范,同时也是一种程序开发的抽象方针。它可能包含数据属性代码方法。对象则指的是(class)的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。

    4.1 面向对象编程的起源和由来

    引用自:https://www.cnblogs.com/GGj147258/p/5617444.html
    OO方法起源于面向对象的编程语言(简称为OOPL)。50年代后期,在用FORTRAN语言编写大型程序时,常出现变量名在程序不同部分发生冲突的问题。鉴于此,ALGOL语言的设计者在ALGOL60中采用了以"Begin……End"为标识的程序块,使块内变量名是局部的,以避免它们与程序中块外的同名变量相冲突。这是编程语言中首次提供封装(保护)的尝试。此后程序块结构广泛用于高级语言如Pascal 、Ada、C之中。
    60年代中后期,Simula语言在ALGOL基础上研制开发,它将ALGOL的块结构概念向前发展一步,提出了对象的概念,并使用了类,也支持类继承。70年代,Smalltalk语言诞生,它取Simula的类为核心概念,它的很多内容借鉴于Lisp语言。由Xerox公司经过对Smautalk72、76持续不断的研究和改进之后,于1980年推出商品化的,它在系统设计中强调对象概念的统一,引入对象、对象类、方法、实例等概念和术语,采用动态联编和单继承机制。
    从80年代起,人们基于以往巳提出的有关信息隐蔽和抽象数据类型等概念,以及由Modula2、Ada和Smalltalk和等语言所奠定的基础,再加上客观需求的推动,进行了大量的理论研究和实践探索,不同类型的面向对象语言(如:Object-c、Eiffel、c++、Java、Object-Pascal等)逐步地发展和建立起较完整的和雨后春笋般研制开发出来,OO方法的概念理论体系和实用的软件系统。
    面向对象源出于Simula,真正的OOP由Smalltalk奠基。Smalltalk现在被认为是最纯的OOPL。
    正是通过Smalltalk80的研制与推广应用,使人们注意到OO方法所具有的模块化、信息封装与隐蔽、抽象性、继承性、多样性等独特之处,这些优异特性为研制大型软件、提高软件可靠性、可重用性、可扩充性和可维护性提供了有效的手段和途径。
    80年代以来,人们将面向对象的基本概念和运行机制运用到其它领域,获得了一系列相应领域的面向对象的技术。面向对象方法已被广泛应用于程序设计语言、形式定义、设计方法学、操作系统、分布式系统、人工智能、实时系统数据库、人机接口、计算机体系结构以及并发工程、综合集成工程等,在许多领域的应用都得到了很大的发展。1986年在美国举行了首届"面向对象编程、系统、语言和应用(OOPSLA’86)"国际会议,使面向对象受到世人瞩目,其后每年都举行一次,这进一步标志OO方法的研究已普及到全世界。

    4.2 面向对象三大特性:封装,继承,多态。

    特性释义
    封装封装是指将一个计算机系统中的数据以及与这个数据相关的一切操作语言(即描述每一个对象的属性以及其行为的程序代码)组装到一起,一并封装在一个有机的实体中,把它们封装在一个“模块”中,也就是一个类中。
    继承继承是指一个对象针对于另一个对象的某些独有的特点、能力进行复制或者延续。
    多态从宏观的角度来讲,多态性是指在面向对象技术中,当不同的多个对象同时接收到同一个完全相同的消息之后,所表现出来的动作是各不相同的,具有多种形态;从微观的角度来讲,多态性是指在一组对象的一个类中,面向对象技术可以使用相同的调用方式来对相同的函数名进行调用,即便这若干个具有相同函数名的函数所表示的函数是不同的。

    4.3 面向对象编程和面向过程编程的区别

    先给结论

    编程思想优势劣势
    面向过程编程性能高。高耦合,编程量较多,维护困难,复杂业务的实现教困难。
    面向对象编程易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护性能较低

    所谓尺有所短,寸有所长,长短之间总是相较而言的。因而如果要更透彻、全面地认识面向过程编程和面向对象编程,我们不妨进行比较。
    这里我试图通过两种编程思想,使用C和JAVA分别来模拟解决一个生活中的案例:烧开水。

    我们知道,如果要喝开水,按照我们日常的做法,就是拿个热水壶,然后装入水,接上电源加热直到水沸腾。实际上这种流程化的思维方式就是面向过程的,专注于所需完成工作本身,一步步执行。

    这里我用C语言来表示这个过程,更简化了其过程,

    1. 获得水的温度
    2. 加热水
    3. 判断水是否沸腾(达到100℃)
    4. 直到满足温度 不小于 100,结束程序。

    示例代码:

    #include <stdio.h>
    #include <stdlib.h>
    
    int get_water_temperature() {
    	int water_temperature = rand() % 100;
        return water_temperature;
    }
    
    int heat_up(int water_temperature) {
    	if (water_temperature < 100) {
            if(water_temperature >= 90) {
            	water_temperature = 100;
            } else {
            	water_temperature = water_temperature + 10;
            }
        }
        printf("Heat up to:%d\r\n", water_temperature);
        return water_temperature;
    }
    
    
    int main() {
    	int water_temperature = get_water_temperature();
        printf("Current Temperature:%d\r\n", water_temperature);
        while(water_temperature < 100) {
        	water_temperature = heat_up(water_temperature);
        }
        printf("The water boil");
        return 0;
    }
    

    但如果使用面向对象编程,我需要首先做一些长远的思考。先设想有如下几个对象

    • 液体
    • 水,水属于液体的一种
    • 加热器,可以用来加热液体的

    可以直观的感受到编程思维的不同,运用面向过程的编程思想,我们专注于解决问题的步骤, 把每个步骤以函数实现,依此调用。但是运用面向对象的编程思想,当问题摆在眼前,我们首先要去思考这个问题涉及几个相关的对象模型,也就是解决这个问题我面对的虚拟主体是什么。

    这里我使用Java编程
    构建了如下类

    • 抽象类 Liquid.class ,表示液体,液体类封装了两个成员变量,name 和 temperature
    • 继承于Liquid.class的类 Water.class,表示水
    • Boiler.class,表示加热器

    然后main函数中实例化相关的对象,并进行函数调用。为了方便阅读,这里做了很多简写,Java新手会发现代码并不能正常运行

    /**
    * 液体类
    */
    public abstract class Liquid {
        
        protected String name;
        //温度
    	protected int temperature;
        
        public Liquid(String name, int temperature) {
            this.name = name;
        	this.temperature = temperature;
        }
        
        public int getName() {
        	return this.name;
        }
        
        public int getTemperature() {
        	return this.temperature;
        }
        
        public void setTemperature(int temperature) {
            this.temperature = temperature;
        }
    }
    
    /**
    * 水类 - 液体类的子类
    */
    public class Water extends Liquid {
    	public Water(String name, int temperature) {
            this.name = name;
        	this.temperature = temperature;
        }
    }
    
    /**
    * 加热器类
    */
    public class Boiler {
    
    	public void heatUp(Liquid liquid, int targetTemperature) {
        	while(liquid.getTemperature() < targetTemperature) {
            	liquid.setTemperature(targetTemperature);
            }
            System.out.printf("%s加热到%d度了", liquid.getName(), targetTemperature);
        }
    }
    
    
    public static void main(String[] args) {
        //获取水
        Water water = new Water("水", (int) (Math.random() * 100));
        //获取热水器
        Boiler boiler = new Boiler();
        //加热
        boiler.heatUp(water, 100);
        
    }
    
    

    在上述示例中看来,似乎Java在编程实现烧水这项工作时,无论是编程量还是复杂程度都比C要多得多。
    但如果此时,情况发生变化,例如说我不仅要烧开水,我还想喝热果汁。面向对象的优势就来了。
    我可以使用Java语言马上构建一个 Juice.class 继承于 Liquid.class 我们的 Boiler.class 的 heatUp 函数是针对于 Liquid 的,因此与水同为液体的果汁也能够调用加热方法。

    public class Juice extends Liquid {
    	public Juice(String name, int temperature) {
            this.name = name;
        	this.temperature = temperature;
        }
    }
    
    public static void main(String[] args) {
        //获取水
        Liquid water = new Water("水", (int) (Math.random() * 100));
        //获取热水器
        Boiler boiler = new Boiler();
        //加热
        boiler.heatUp(water, 100);
        
        //获取果汁
        Liquid juice = new Juice("果汁", (int) (Math.random() * 100));
        //加热
        boiler.heatUp(juice, 100);
    }
    

    面向对象编程聚焦于 某类实体的 共同特征 进行封装,声明这类实体所拥有的属性和能够提供的服务,因而能够针对一类实体进行编程,这便是其可重用性。
    在这里,实体可以是某类服务,也可以指代业务问题中的某个实际物理对象。例如我们需要管理公司员工的信息,那么就可以针对于员工这个群体进行抽象,员工作为人具有姓名,身高,年龄,电话号码等信息,员工作为公司的一员有工号,薪资,任职年限等信息。对实体的抽象是面向对象编程的基本思维方式所在。

    我们理解面向过程与面向对象的区别时,需要发散思维来看,特别是已经接触过Java的程序员,不妨设想一下没有“对象”的编程语言如何解决一些问题。软件编程的一大难题是适应变化,世界无时不刻发生变化。当面对变化时,当面对较复杂的情况时,面向对象编程较面向过程编程自有其优势所在。

    5 Java的面向对象编程

    Java作为一门面向对象编程的高级语言,是如何实现面向对象的?我们可以从面向对象的三大特性来解释Java的面向对象机制。

    5.1 Java的封装

    Java的封装性体现在 class类 和 访问修饰符中。Java通过类文件将一个个对象分隔开来。并通过访问修饰符来控制其他对象的访问权限

    Java修饰符
    修饰符释义
    当前类及其子类可以访问,同包的类可以访问
    public所有类可以访问
    protected当前类及其子类可以访问
    private当前类可以访问

    程序员可以把属性和方法在类中声明,并通过访问修饰符机制控制外界的访问和函数调用,以此起到隔离封装的作用。

    5.2 Java的继承

    Java通过在类名后追加 extends 并指定父类类名来实现继承

    public class Dog extends Animal {
        //该示例表示 Dog 类 继承于 Animal 类
    }
    

    由于父类中已经进行了一次抽象并声明了其属性和提供的函数,并且可以将这部分属性和函数继承给子类,因此通过底层建筑向上的层层封装,可以将一类实体的特征按照父子类的关系进行非常良好的聚合。这在代码重用性和复杂关系的梳理上是非常有益处的。多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。初始化子类时先初始化父类,即调用构造函数时隐式执行父类构造函数。下图展示了其包含关系。
    在这里插入图片描述

    5.3 Java的多态

    多态可以理解为多形态,即一个对象的实例化可以有多种形态,java的多态是基于实现接口和继承的。我们在回顾一下关于面向对象多态的定义

    从宏观的角度来讲,多态性是指在面向对象技术中,当不同的多个对象同时接收到同一个完全相同的消息之后,所表现出来的动作是各不相同的,具有多种形态;
    从微观的角度来讲,多态性是指在一组对象的一个类中,面向对象技术可以使用相同的调用方式来对相同的函数名进行调用,即便这若干个具有相同函数名的函数所表示的函数是不同的。

    这里将多态视为宏观和微观两种角度。首先从宏观角度讲,即 接受 相同的消息 表示出不同的动作。
    如下代码

    //定义整数计算器接口
    public interface IntCalculator {
    	
    	public int calculate(int i);
    }
    
    public class Totalizer implements IntCalculator {
    	@Override
        public int calculate(int i) {
            //累加操作
        	return ++i;
        }
    }
    
    public class Reducer implements IntCalculator {
        @Override
        public int calculate(int i) {
            //累减操作
        	return --i;
        }
    }
    
    public class Test {
        public static void main(String[] args) {
    		int i = 0;
            IntCalculator totalizer = new Totalizer();
    		IntCalculator reducer = new Reducer();
    		System.out.printf("操作结果%d", totalizer.calculate(i));
            System.out.printf("操作结果%d", reducer.calculate(i));
        }
    }
    
    

    如上述代码,最终执行结果
    操作结果1
    操作结果-1
    进程已结束,退出代码为 0
    可见,通过实现接口,实例化两个不同的对象接收相同的消息,其执行动作不同。当然,通过继承也能完成。

    其次是微观角度的,即一个对象中的相同函数名的不同函数,可以用相同的调用方式进行调用。
    java类中的函数名是可以相同的,但是其参数必须不同,保证函数的唯一性。当然调用方式是可以相同的,即便在同一个函数中,借由用于判断的类型参数等也可以在函数体内通过条件判断语句进而执行不同的动作。

    如下代码示例(仅作案例讲解,请勿考虑其实际意义)

    public class ElementExcel {
    
    	public static ElementExcel of(File file, String suffix) {
        	if(suffix == null) {
                return new ElementExcel();
            }
            
            if(".xls".equals(suffix.trim())) {
            	//做相应处理
            } else {
            	//做相应处理
            }
            return new ElementExcel();
        }
    	
        public static ElementExcel of(InputStream inputStream) {
        	//做相应处理
            return new ElementExcel();
        }
        
        public static ElementExcel of(MuiltpartFile multpartFile) {
        	//做相应处理
            return new ElementExcel();
        }
    }
    

    借由java接口实现和继承以及java函数定义规则和条件判断语法支持,java很好的体现了面向对象多态特性。

    展开全文
  • java底层知识(6)--CPU、内存

    千次阅读 2016-05-06 21:57:18
    下一篇文章,我们将从底层爬回到上层去,研究一个抽象概念:虚拟内存。 主板芯片组与内存映射 原文标题:Motherboard Chipsets and the Memory Map 原文地址: http://duartes.org/gustavo/blog/ 我...
  • 阿里妹导读:肉眼看计算机是由CPU、内存、显示器这些硬件设备组成,但大部分人从事的是软件开发工作。计算机底层原理就是连通硬件和软件的桥梁,理解...文章目录结合 CPU 理解一行 Java 代码是怎么执行的中断从...
  • 深入【Java底层细节知识点

    万次阅读 多人点赞 2018-08-24 21:33:41
    实现原理: 如上,两者的类型分明不同,输出的结果却是true,这是因为,泛型它不会影响java虚拟机生成的汇编代码,在编译阶段,虚拟机就会把泛型的类型擦除,还原成没有泛型的代码,顶多编译速度稍微慢一些,执行...
  • Java代码编译后变成java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,java中所使用的并发机制依赖于JVM的实现和CPU的执行。2.1 volatile的应用 在多线程并发编程中,...
  • 汇编底层的角度看c和类c语言

    千次阅读 2015-12-29 22:55:21
    版权所有John Black,转载时记得附上标上原文地址,写那么多我也不容易。。...1.众所周知用任意一高级语言(不是脚本语言)写的代码都要经过类似:预处理->编译成汇编代码(compilation)->汇编(as
  • 从计算机底层基础语言到上层高级语言分析: 1.汇编 简要介绍一下汇编程序的 规则定义,编写和执行 DATA SEGMENT ;数据段开始 (变量定义) word DW ? ;双字 (注意高低位) str DB 'Hello world!' ;单字 串(数组...
  • 牛逼!Java 从入门到精通,超全汇总版

    万次阅读 多人点赞 2021-05-06 19:40:33
    文章目录Java 基础Head First JavaJava 核心技术卷一Java 编程思想设计模式Head First 设计模式图解设计模式设计模式重学 Java 设计模式Java 进阶Java 并发编程实战Java 并发编程艺术Java 并发编程之美图解Java多...
  • Assembly(汇编)、C、C++、Java、C#访谈

    千次阅读 2009-09-23 17:51:00
    请问Assembly,上面那些语言我都会用,但你说所有语言最终都变成汇编代码,然后才执行的,我写C++、Java等代码的时候,根本没有与Assembly打交道,你确信你说的话正确? Assembly: 你编写的只不过是C++、Java等...
  • 1. CAS实现的乐观锁 CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的,也可以理解为自旋锁 ...最终实现是汇编指令:lock...
  • ID:技术让梦想更伟大作者:李肖遥对于现在我们从事嵌入式开发的人员来说,C语言好像是必备的技能,也是最常见的开发语言.但是我们有没有想过为什么嵌入式开发要使用C语言呢?为什么不使用汇编呢...
  • Java面试题大全(2021版)

    万次阅读 多人点赞 2020-11-25 11:55:31
    发现网上很多Java面试题都没有答案,所以花了很长时间搜集整理出来了这套Java面试题大全,希望对大家有帮助哈~ 本套Java面试题大全,全的不能再全,哈哈~ 一、Java基础知识面试题 1、Java概述 ①. 何为编程 ...
  • 如何优雅地像乐鑫原厂封装esp8266底层寄存器的逻辑思维,做成自己的静态库库文件,让第三方人使用? 点我访问 34 nonos3.0 编程使用 SPI 驱动基于Max7219芯片的八位数码管,显示日期信息。 点我访问 ...
  • gcc gcc(GNU Compiler Collection,GUN编译器...gcc最初用于编译C语言,随着项目的发展gcc已经成为了能够编译C、C++、Java、Ada、fortran、Object C、Object C++、Go语言的编译器大家族。 编译命令格式: gcc [-opti
  • java分布式(java入门)

    万次阅读 热门讨论 2018-06-01 14:32:11
    【 声明:版权所有,欢迎转载,请勿用于...至于后面的c、汇编、python、js这些语言,那都是工作了之后才学的。至于这些语言有什么用,在什么场景下使用效率最高,其实说实话,当时心里不是很清楚,等到真正明白过...
  • JAVAOOP

    2021-03-28 23:55:16
    文章目录JAVA OOP第一章对象和封装第二章继承第三章多态第四章接口第五章异常第六章集合框架 JAVA OOP 第一章对象和封装 编程思想: 1,面向过程 C 汇编 2,面向对象 Java C# C++ python Php 易语言 类是一种特殊...
  • -------------------CoreJava第一阶段--------------------------- day1------------------------------------------------------- B/S:Browser/Server ...第二代编程语言:汇编。 第三代高级编程语言:Java...
  • Java并发技术学习总结

    千次阅读 2018-06-28 14:40:06
    Java并发 这篇总结主要是基于我Java并发技术系列的文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢 更多详细内容可以查看我的专栏文章:Java并发技术指南 ...
  • 总结汇编

    2013-08-10 15:45:55
    逐渐理解到,高层的语言都基本上是对低级语言的抽象和实现,封装了更为细节的东西,目前了解的语言只有C语言还显露出很多汇编语言的成分和痕迹,而汇编语言通过提供很多的伪指令也实现了很重要的抽象,过程定义和宏...
  • 选择“Win32汇编”的三大理由?

    千次阅读 2009-06-16 08:23:00
     在DOS时代,学习汇编就是学习系统底层编程的代名词,仅要成为一名入门级的汇编程序员,就需要学习从CPU结构、CPU工作方式、各种硬件的编程方法到DOS工作方式等范围很广的知识。随着Windows时代的到来,Windows像一...
  • java的平台无关性

    2020-05-19 01:57:25
    (三)Java底层知识:JVM 谈谈你对Java的理解: 1)平台无关性 2)GC 3)语言特性(泛型、反射、lamda表达式) 4)面向对象(继承、封装、多态) 5)类库(集合、并发库、网络库、IO/NIO) 6)异常处理 下面展开讲解...
  • 本Blog分为如下部分: ...第二部分:JVM底层又是如何实现synchronized的 第三部分:Java多线程锁,源代码剖析 第一部分:synchronized与static synchronized的区别 1、synchronized与static synchronize
  • 精明的程序员——封装了什么

    千次阅读 2012-05-31 21:27:13
    精明的程序员——封装了什么 我一向对JAVA开发有偏见,总是看不惯一些老师带着学生们去写各种各样的JAVA程序。前些天又和一个比较喜欢JAVA的朋友争论到底是JAVA好还是C好,争来争取也没有一个结论。最多不过是些...
  • Java核心基础知识汇总

    万次阅读 多人点赞 2018-03-31 11:25:32
     Java是一门编程语言,Java发展到今天,已经成为了一个真正意义上的语言标准,如果学习过(C、C++、Java)可以发现语法结构是很类似的,但是Java的标准指的是一种作为应用层封装的标准,使用Java可以调用一些底层的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 11,712
精华内容 4,684
关键字:

java底层封装的汇编

java 订阅