精华内容
下载资源
问答
  • 在任何环境中都能满足
    千次阅读 多人点赞
    2019-09-04 15:38:39

    前言

    程序运行的过程中,函数之间的是会相互调用的,在某一时刻函数之间的调用关系,可以通过函数调用堆栈表现出来,这个调用堆栈所展现的就是函数A调用了函数B,而函数B又调用了函数C,这些调用关系在代码中都是静态的,不需要程序运行就可以知道。

    既然函数之间的调用关系可以通过分析代码就可以知道,那么查看函数调用的堆栈是不是作用不大了呢?事实上恰恰相反,查看函数调用堆栈的作用非常大。因为在较大型的项目中,函数之间的调用不是简单的一条线,常常会出现复杂的网状结构,这时如果函数C被调用了,可能不是仅仅是B函数调用过来的,也有可能是D、E、F等函数调用了C函数,所以知道在程序运行时究竟是哪个函数调用了C函数显得很重要,特别是有众多函数会调用C函数的时候。

    查看函数堆栈的作用

    举个例子就明白了,假如C函数中逻辑的执行需要一些特殊条件状态,理论上执行C函数时这些条件都应该满足的,但是程序在运行的过程中有时运行C函数时条件就是不满足的,那就说明有些调用C函数的逻辑分支有问题,无法满足C函数中逻辑所需条件,这时候知道是谁调用C函数导致条件不满足就是确定问题的关键。

    如果是在VS调试状态下,在C函数不满足条件的逻辑中打一个断点,然后运行程序等待断点触发时,就可以通过VS工具自带的调用堆栈窗口,就可以看到程序从主函数main()开始怎样一步步调用的出错的函数C的。

    可实际项目中,出错的时候不总是在VS的调试状态下,也有可能发生在程序实际的工作环境中,这时没有办法通过加断点来查看调用堆栈,如果此时有一个函数,可以打印当前的函数调用堆栈那就太好了,这样我就可以在需要调试的逻辑中,调用这个函数,将当时的函数调用堆栈信息打印到文件中,方便查找程序逻辑问题,这篇文章要做的就是在Windows环境下,利用现有的API实现这样一个函数。

    实现打印堆栈信息的函数

    在Windows系统上想打印函数调用堆栈信息,需要引用头文件<dbghelp.h>,添加库引用DbgHelp.Lib,然后利用函数CaptureStackBackTraceSymFromAddrSymGetLineFromAddr64来获取当时的函数调用堆栈信息,以下的代码实现了一个简单的打印堆栈新的函数,堆栈深度最大设置为12层,实际情况肯定是越深越好,设置为12一般就可以查到问题了。

    #include <windows.h>
    #include <dbghelp.h>
    #include <stdio.h>
    
    #if _MSC_VER
    #define snprintf _snprintf
    #endif
    
    #define STACK_INFO_LEN  1024
    
    void ShowTraceStack(char* szBriefInfo)
    {
        static const int MAX_STACK_FRAMES = 12;
        void *pStack[MAX_STACK_FRAMES];
        static char szStackInfo[STACK_INFO_LEN * MAX_STACK_FRAMES];
        static char szFrameInfo[STACK_INFO_LEN];
    
        HANDLE process = GetCurrentProcess();
        SymInitialize(process, NULL, TRUE);
        WORD frames = CaptureStackBackTrace(0, MAX_STACK_FRAMES, pStack, NULL);
        strcpy(szStackInfo, szBriefInfo == NULL ? "stack traceback:\n" : szBriefInfo);
    
        for (WORD i = 0; i < frames; ++i) {
            DWORD64 address = (DWORD64)(pStack[i]);
    
            DWORD64 displacementSym = 0;
            char buffer[sizeof(SYMBOL_INFO)+MAX_SYM_NAME * sizeof(TCHAR)];
            PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)buffer;
            pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO);
            pSymbol->MaxNameLen = MAX_SYM_NAME;
    
            DWORD displacementLine = 0;
            IMAGEHLP_LINE64 line;
            line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
    
            if (SymFromAddr(process, address, &displacementSym, pSymbol) && 
            	SymGetLineFromAddr64(process, address, &displacementLine, &line))
            {
                snprintf(szFrameInfo, sizeof(szFrameInfo), "\t%s() at %s:%d(0x%x)\n", 
                	pSymbol->Name, line.FileName, line.LineNumber, pSymbol->Address);
            }
            else
            {
                snprintf(szFrameInfo, sizeof(szFrameInfo), "\terror: %d\n", GetLastError());
            }
            strcat(szStackInfo, szFrameInfo);
        }
    
        printf("%s", szStackInfo); // 输出到控制台,也可以打印到日志文件中
    }
    
    void func2()
    {
        bool isError = true;
        if (isError)
        {
            ShowTraceStack("error in func2\n");
        }
        else
        {
            printf("this is func2\n");
        }
    }
    
    void func1()
    {
        int sum = 0;
        for (int i = 0; i < 100; ++i)
            sum += i;
    
        func2();
    }
    
    
    int main(int argc, char* argv[])
    {
        printf("hello world\n");
        func1();
    
        return 0;
    }
    

    显示堆栈调用信息

    上面的测试代码中函数的调用逻辑为:main()函数调用func1()函数,然后func1()函数调用func2()函数,当func2()中发生问题的时候打印当时的堆栈信息,然后我们查看一下打印结果

    hello world
    error in func2

    ShowTraceStack() at e:\vs2013projects\trackback\windowstrackback\windowstrackback.cpp:24(0xe01440)
    func2() at e:\vs2013projects\trackback\windowstrackback\windowstrackback.cpp:59(0xe01840)
    func1() at e:\vs2013projects\trackback\windowstrackback\windowstrackback.cpp:74(0xe017c0)
    main() at e:\vs2013projects\trackback\windowstrackback\windowstrackback.cpp:82(0xe018c0)
    __tmainCRTStartup() at f:\dd\vctools\crt\crtw32\dllstuff\crtexe.c:626(0xe01d40)
    mainCRTStartup() at f:\dd\vctools\crt\crtw32\dllstuff\crtexe.c:466(0xe020c0)
    error: 487
    error: 487
    error: 487

    总结

    1. Windows平台下可以利用函数CaptureStackBackTraceSymFromAddrSymGetLineFromAddr64来获取当时的函数调用堆栈信息
    2. 使用上述函数时,需要引用头文件<dbghelp.h>,添加库引用DbgHelp.Lib

    程序源码

    打印堆栈信息–源码传送门

    更多相关内容
  • 文章目录1、关系数据库2、列式数据库3、键值数据库4、图形数据库5、分布式文档存储数据库 数据是一个企业的核心资产,几乎所有的企业应用系统都是围绕...数据积累中,虽然有些把数据作为文本文件来保存的,但大...

    数据是一个企业的核心资产,几乎所有的企业应用系统都是围绕数据来进行的,包括数据的增删改查,数据对企业,甚至国家来说,有着不可估量的价值,比如,一个企业的所有客户的信息,一个投资机构的投入和收益信息,BAT公司的应用系统的源代码,京东的物流配送信息等等。

    在数据积累中,虽然有些把数据作为文本文件来保存的,但大多数情况下,为了方便地管理和提取数据,一般都会采用数据库来积累数,随着技术的发展,业务需求的多样化,出现了各种各样的数据库类型。

    下面分别一一说明其代表的产品以及其主要特点。

    1、关系数据库

    关系型数据一直为我们所用,比如Oracle,MySQL,SQL Server,Postgress。

    保存到关系数据库的数据必须满足一定要求,用通用的话来说就是满足一定的数据范式,比如主键,外键,数据的冗余;

    举个例子,学生的信息可以保存到数据库中,班级的信息也可以保存到数据库中,而且一个班级包括很多学生,他们之间通过外键还可以建立一种1对多的对应关系,这些信息和关系可以通过关系型数据库进行存储,值得注意的是,关系型数据库,需要预先定义其表结构,而且存储前需要定义其对应的数据类型或者长度,一旦有新的属性加入,就要修改其表的结构。

    传统的关系型数据库其实就是行式数据库,就是一行一行的方式来存储信息的。

    2、列式数据库

    列式数据库一般应用于对应大量的字符串数据,实例如 HBase,cassandra,Sybase IQ,HP Vertica、EMC Greenplum等。

    列式数据库从一开始就是面向大数据环境下数据仓库的数据分析而产生,主要适合于批量数据处理和即时查询。

    下面这2张图形象的说明了什么是列式数据库,什么是行式数据库已经两者之间的区别。

    img

    img

    极高的装载速度 (最高可以等于所有硬盘IO 的总和,基本是极限了)

    适合大量的数据而不是小数据

    实时加载数据仅限于增加(删除和更新需要解压缩Block 然后计算然后重新压缩储存)

    高效的压缩率,不仅节省储存空间也节省计算内存和CPU,为什么具有高压缩率呢?因为存储的数据类型是一样的。

    非常适合做聚合操作。

    3、键值数据库

    即Key-Value存储,简称KV存储。它是NoSQL存储的一种方式。它的数据按照键值对的形式进行组织,索引和存储。KV存储非常适合不涉及过多数据关系业务关系的业务数据,同时能有效减少读写磁盘的次数,比SQL数据库存储拥有更好的读写性能。
    典型的产品有: 亚马逊的 DynamoDB,redis。

    img

    4、图形数据库

    图形数据库不是专门用来存储图形图像的,而是因为其用图状结构来维持其数据之间的关系,所以叫做图形数据库。Neo4j,Sones就是其典型代表。

    在图数据结构中,只有两种基本的数据类型 即节点(Node)和关系(Relationship),节点(Node)可以拥有属性,关系(Relationship)也可以拥有属性 ,属性都是以键值对的方式存储,节点(Node)与节点(Node)的联系通过关系(Relationship)进行建立,他们建立的关系是有方向的。

    img

    Neo4j是一个高性能的,NOSQL图形数据库,它将结构化数据存储在网络上而不是表中。Neo4j也可以被看作是一个高性能的图引擎,该引擎具有成熟数据库的所有特性。程序员工作在一个面向对象的、灵活的网络结构下而不是严格、静态的表中——但是他们可以享受到具备完全的事务特性、企业级的数据库的所有好处。Neo4j因其嵌入式、高性能、轻量级等优势,越来越受到关注。其支持几乎所有的主流的开发语言

    img

    5、分布式文档存储数据库

    不需要定义,应用灵活,文档存储支持对结构化数据的访问,不同于关系模型的是,文档存储没有强制的架构。与关系模型不同的是,文档存储模型支持嵌套结构。例如,文档存储模型支持XML和JSON文档,字段的“值”又可以嵌套存储其它文档。文档存储模型也支持数组和列值键。与键值存储不同的是,文档存储关心文档的内部结构。这使得存储引擎可以直接支持二级索引,从而允许对任意字段进行高效查询。支持文档嵌套存储的能力,使得查询语言具有搜索嵌套对象的能力,XQuery就是一个例子。

    MongoDB通过支持在查询中指定JSON字段路径实现类似的功能。比如,CouchDB,MongoDB等。

    转载: https://www.cnblogs.com/lzh-boy/p/10444695.html

    展开全文
  • 基于以上两个原因,便有了ConcurrentHashMap的登场机会 1)线程不安全的HashMap   多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以并发情况下不使用HashMap。HashMap并发...

      大家好,我是陈哈哈,北漂五年。认识我的朋友们知道,我是非科班出身,半路出家,大学也很差!这种背景来北漂,你都不知道你会经历什么🙃🙃。

      不敢苟同,相信大家和我一样,都有一个大厂梦,作为一名资深Java选手,深知面试重要性,接下来我准备用100天时间,基于Java岗面试中的高频面试题,以每日3题的形式,带你过一遍热门面试题及恰如其分的解答。当然,我不会太深入,因为我怕记不住!!

      因此,不足的地方希望各位在评论区补充疑惑、见解以及面试中遇到的奇葩问法,希望这100天能够让我们有质的飞越,一起冲进大厂!!,让我们一起学(juan)起来!!!

    在这里插入图片描述

    坐标:老铁们,这是哪里?

    作者:一叶知秋


      本栏目Java开发岗高频面试题主要出自以下各技术栈:Java基础知识集合容器并发编程JVMSpring全家桶MyBatis等ORMapping框架MySQL数据库Redis缓存RabbitMQ消息队列Linux操作技巧等。


      上回问到HashMap的线程安全问题,我们已经知道,在Java中有HashTableSynchronizedMapConcurrentHashMap这三种是实现线程安全的Map。而ConcurrentHashMap也是最常用的并发场景下Map的选择,相信面试官对其理论和实战知识也是在熟悉不过,因此如果不能深入了解,或许会轻易被问住。

    面试题1:先说一下大家为什么要选择ConcurrentHashMap?

      在并发编程中使用HashMap可能导致程序死循环。而使用线程安全的HashTable效率又非常低下,基于以上两个原因,便有了ConcurrentHashMap的登场机会

    • 1)线程不安全的HashMap

      在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。HashMap在并发执行put操作时会引起死循环,是因为多线程环境下会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,调用.next()时就会产生死循环获取Entry。

    • 2)效率低下的HashTable

      HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下(类似于数据库中的串行化隔离级别)。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,读写操作均需要获取锁,竞争越激烈效率越低。

      因此,若未明确严格要求业务遵循串行化时(如转账、支付类业务),建议不启用HashTable。

    在这里插入图片描述

    • 3)ConcurrentHashMap的分段锁技术可有效提升并发访问率

      HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在严重锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的分段锁技术。首先将数据分成一段一段地存储(一堆Segment),然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

    对于 ConcurrentHashMap 你至少要知道的几个点:

    • 默认数组大小为16
    • 扩容因子为0.75,扩容后数组大小翻倍
    • 当存储的node总数量 >= 数组长度*扩容因子时,会进行扩容(数组中的元素、链表元素、红黑树元素都是内部类Node的实例或子类实例,这里的node总数量是指所有put进map的node数量)
    • 当链表长度>=8且数组长度<64时会进行扩容
    • 当数组下是链表时,在扩容的时候会从链表的尾部开始rehash
    • 当链表长度>=8且数组长度>=64时链表会变成红黑树
    • 树节点减少直至为空时会将对应的数组下标置空,下次存储操作再定位在这个下标t时会按照链表存储
    • 扩容时树节点数量<=6时会变成链表
    • 当一个事物操作发现map正在扩容时,会帮助扩容
    • map正在扩容时获取(get等类似操作)操作还没进行扩容的下标会从原来的table获取,扩容完毕的下标会从新的table中获取

    在这里插入图片描述
    课间休息,又来秀一下来自咱们群里同学的搬砖工地,坐标:河北 秦皇岛

    作者:云野.


    面试题2:ConcurrentHashMap在JDK1.7、1.8中都有哪些优化?

      其实,JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发。

    • JDK1.7:ReentrantLock+Segment+HashEntry
    • JDK1.8:Synchronized+CAS+Node(HashEntry)+红黑树

      从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树。其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

    在这里插入图片描述

      数据结构上跟HashMap很像,从1.7到1.8版本,由于HashEntry从链表红黑树所以 concurrentHashMap的时间复杂度从O(n)到O(log(n)) ↓↓↓;

    在这里插入图片描述

      同时,也把之前的HashEntry改成了Node作用不变,当Node链表的节点数大于8时Node会自动转化为TreeNode,会转换成红黑树的结构。把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)。

    归纳一下:

    • JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
    • JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念(jdk1.8),也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
    • JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,成功代替了一定阈值的链表。

    追问1:JDK1.8为什么使用Synchronized来代替ReentrantLock?

    JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,主要有以下几点:

    1. 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
    2. JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
    3. 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个原因之一。

    追问2:讲讲ConcurrentHashMap的 get put 过程?

    JDK1.7版本的get put

      在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组多个HashEntry组成,如下图所示:

    在这里插入图片描述

      Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分段技术,而每一个Segment元素存储的HashEntry数组+链表,这个和HashMap的数据存储结构一样。

    初始化

    ConcurrentHashMap的初始化是会通过位与运算来初始化Segment的大小,用ssize来表示,源码如下所示

    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException {
            // For serialization compatibility
            // Emulate segment calculation from previous version of this class
            int sshift = 0;
            int ssize = 1;
            while (ssize < DEFAULT_CONCURRENCY_LEVEL) {
                ++sshift;
                ssize <<= 1;
            }
            int segmentShift = 32 - sshift;
            int segmentMask = ssize - 1;
    

      由此可以看出:因为ssize用位于运算来计算(ssize <<=1),所以Segment的大小取值都是以2的N次方,无关concurrencyLevel的取值,当然concurrencyLevel最大只能用16位的二进制来表示,即65536,换句话说,Segment的大小最多65536个,没有指定concurrencyLevel元素初始化,Segment的大小ssize默认为:DEFAULT_CONCURRENCY_LEVEL =16

      每一个Segment元素下的HashEntry的初始化也是按照位于运算来计算,用cap来表示,如下:

    int cap = 1;
    while (cap < c)
        cap <<= 1
    

      如上所示,HashEntry大小的计算也是2的N次方(cap <<=1), cap的初始值为1,所以HashEntry最小的容量为2

    JDK1.7 —— put操作

    对于ConcurrentHashMap的数据插入,这里要进行两次Hash去定位数据的存储位置

    static class Segment<K,V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        final float loadFactor;
        Segment(float lf) { this.loadFactor = lf; }
    }
    

      从上Segment的继承体系可以看出,Segment实现了ReentrantLock,也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承 ReentrantLock 的 tryLock() 方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。

    JDK1.7 —— get操作

      ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null

    在这里插入图片描述


    JDK1.8版本的get put

    • 改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

    • 改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。

    对于改进二的详细分析

      对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度基本都为0或者1才对
      但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n)
      因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),从而针对该种情况,改进了性能。

      JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。

      在深入JDK1.8的put和get实现之前要知道一些常量设计和数据结构,这些是构成ConcurrentHashMap实现结构的基础,下面看一下基本属性:

    // node数组最大容量:2^30=1073741824
    private static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认初始值,必须是2的幕数
    private static final int DEFAULT_CAPACITY = 16
    //数组可能最大值,需要与toArray()相关方法关联
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    //并发级别,遗留下来的,为兼容以前的版本
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    // 负载因子
    private static final float LOAD_FACTOR = 0.75f;
    // 链表转红黑树阀值,> 8 链表转换为红黑树
    static final int TREEIFY_THRESHOLD = 8;
    //树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
    static final int UNTREEIFY_THRESHOLD = 6;
    static final int MIN_TREEIFY_CAPACITY = 64;
    private static final int MIN_TRANSFER_STRIDE = 16;
    private static int RESIZE_STAMP_BITS = 16;
    // 2^15-1,help resize的最大线程数
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
    // 32-16=16,sizeCtl中记录size大小的偏移量
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
    // forwarding nodes的hash值
    static final int MOVED     = -1;
    // 树根节点的hash值
    static final int TREEBIN   = -2;
    // ReservationNode的hash值
    static final int RESERVED  = -3;
    // 可用处理器数量
    static final int NCPU = Runtime.getRuntime().availableProcessors();
    //存放node的数组
    transient volatile Node<K,V>[] table;
    /*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
     *当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
     *当为0时:代表当时的table还没有被初始化
     *当为正数时:表示初始化或者下一次进行扩容的大小*/
    

      基本属性定义了ConcurrentHashMap的一些边界以及操作时的一些控制,下面看一些内部的一些结构组成,这些是整个ConcurrentHashMap整个数据结构的核心。

      结构图改自:https://blog.csdn.net/ZOKEKAI/article/details/90085517

    该图片

    • Node

    HashEntry == Node

      Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据,Node就是一个链表,但是只允许对数据进行查找,不允许进行修改;

    • TreeNode

      TreeNode继承与Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8时会转换成红黑树的结构,他就是通过TreeNode作为存储结构代替Node来转换成黑红树。源代码如下

    • TreeBin

      TreeBin从字面含义中可以理解为存储树形结构的容器,而树形结构就是指TreeNode,所以TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制。

      现在通过一个简单的例子以debug的视角看看ConcurrentHashMap的具体操作细节

    public class TestConcurrentHashMap{   
        public static void main(String[] args){
            ConcurrentHashMap<String,String> map = new ConcurrentHashMap(); //初始化ConcurrentHashMap
            //新增个人信息
            map.put("id","1");
            map.put("name","andy");
            map.put("sex","男");
            //获取姓名
            String name = map.get("name");
            Assert.assertEquals(name,"andy");
            //计算大小
            int size = map.size();
            Assert.assertEquals(size,3);
        }
    }
    

    我们先通过new ConcurrentHashMap()来进行初始化

    public ConcurrentHashMap() {
    }
    

      由上你会发现ConcurrentHashMap的初始化其实是一个空实现,并没有做任何事,这里后面会讲到,这也是和其他的集合类有区别的地方,初始化操作并不是在构造函数实现的,而是在put操作中实现,当然ConcurrentHashMap还提供了其他的构造函数,有指定容量大小或者指定负载因子,跟HashMap一样。

    JDK1.8 —— put操作

      在上面的例子中我们新增个人信息会调用put方法,我们来看下

    public V put(K key, V value) {
        return putVal(key, value, false);
    }
    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode()); //两次hash,减少hash冲突,可以均匀分布
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) { //对这个table进行迭代
            Node<K,V> f; int n, i, fh;
            //这里就是上面构造方法没有进行初始化,在这里进行判断,为null就调用initTable进行初始化,属于懒汉模式初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果i位置没有数据,就直接无锁插入
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)//如果在进行扩容,则先进行扩容操作
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //如果以上条件都不满足,那就要进行加锁操作,也就是存在hash冲突,锁住链表或者红黑树的头结点
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) { //表示该节点是链表结构
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //这里涉及到相同的key进行put就会覆盖原先的value
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {  //插入链表尾部
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {//红黑树结构
                            Node<K,V> p;
                            binCount = 2;
                            //红黑树结构旋转插入
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) { //如果链表的长度大于8时就会进行红黑树的转换
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);//统计size,并且检查是否需要扩容
        return null;
    }
    

      这个put的过程很清晰,对当前的table进行无条件自循环直到put成功,可以分成以下六步流程来概述:

    1. 如果没有初始化就先调用initTable()方法来进行初始化过程
    2. 如果没有hash冲突就直接CAS插入
    3. 如果还在进行扩容操作就先进行扩容
    4. 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入一种是红黑树就按照红黑树结构插入
    5. 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环,默认的链表大小,超过了这个值就会转换为红黑树;
    6. 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容。

      put的流程你可以从中发现,他在并发处理中使用的是乐观锁,当有冲突的时候才进行并发处理

    JDK1.8 —— get操作

      我们现在要回到开始的例子中,我们对个人信息进行了新增之后,我们要获取所新增的信息,使用 String name = map.get(“name”) 获取新增的 name 信息,现在我们依旧用debug的方式来分析下 ConcurrentHashMap 的获取方法: get()

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode()); //计算两次hash
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {//读取首节点的Node元素
            if ((eh = e.hash) == h) { //如果该节点就是首节点就返回
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
            //查找,查找到就返回
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }
    

    ConcurrentHashMap 的 get 操作的流程很简单,也很清晰,可以分为三个步骤来描述

    1. 计算hash值,定位到该table索引位置,如果是首节点符合就返回
    2. 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
    3. 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null

    追问3:ConcurrentHashMap 的 get 方法是否要加锁,为什么?

      get 方法不需要加锁。因为 Node 的元素 value 和指针 next 是用 volatile 修饰的(可见性),在多线程环境下线程A修改节点的 value 或者新增节点的时候是对线程B可见的

      这也是它比其他并发集合比如 Hashtable、用 Collections.synchronizedMap()包装的 HashMap 效率高的原因之一。


    在这里插入图片描述

    课间休息,又来秀一下来自咱们群里同学的搬砖工地,坐标:??

    作者:xlikec


    面试题3:我们可以使用CocurrentHashMap来代替Hashtable吗?

      我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。它们都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间

      因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map

    追问1:那ConcurrentHashMap有哪些缺陷?

      ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。

    • 好处是:在保证合理的同步前提下,效率很高。
    • 坏处是:严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据,而未必是最新的数据。

    因此,若需要严格按照串行事务定需求的话,如转账、支付类业务还是使用HashTable。

    集合框架下一篇 (四)会继续沿着CocurrentHashMap深入讲解,包括CAS乐观锁原理、volatile、自旋锁等相关问题;

    • ConcurrentHashMap 不支持 key 或者 value 为 null 的原因?
    • Volatile 关键字干了那些事?Volatile的特性是什么?
    • 不安全会导致哪些问题?如何解决?
    • 有没有线程安全的并发容器?
    • ConcurrentHashMap并发度为啥好这么多?
    • CAS是啥?ABA是啥?场景有哪些,怎么解决?
    • 自旋锁是什么?解决什么问题?
    • CAS性能很高,但是为什么jdk1.8之后还是会用Synchronized?
    • 快速失败(fail-fast)是啥,应用场景有哪些?

    每日小结

      今天我们复习了面试中常考的集合框架CocurrentHashMap相关的几个问题,你做到心中有数了么?对了,如果你的朋友也在准备面试,请将这个系列扔给他,如果他认真对待,肯定会感谢你的!!好了,今天就到这里,学废了的同学,记得在评论区留言:打卡。,给同学们以激励。

    展开全文
  • 平时搭建PHP网站,可以linux服务器上搭建,也可以windows服务器上搭建,主要看你个人需求了,今天我们演示的是用PHPWAMP绿色集成环境在windows服务器上搭建php网站。   这款PHPWAMP绿色集成环境也属于WAMP...

     

    文章来源于网络转载,原作者未知。

     

    服务器版本:Windows Server 2008 R2 Enterprise

    平时搭建PHP网站,可以在linux服务器上搭建,也可以在windows服务器上搭建,主要看你个人需求了,今天我们演示的是用PHPWAMP绿色集成环境在windows服务器上搭建php网站。

     

    这款PHPWAMP绿色集成环境也属于WAMP套件,大家知道什么是WAMP吧?

    Windows下的Apache+Mysql+PHP,称为WAMP。

     

    WAMP套件其实有很多,就比如wampserver、apmserv、xampp等等,这些WAMP环境各有特点。

    本文演示的是纯绿色的PHP集成环境PHPWAMP,这款集成环境是我在网上找到的。

    所采用的版本号是2016年的8.1.8.8版本,看官方网站介绍,2017年的8.8.8.8版本估计也差不多要出来了。

     

     

    为什么要使用PHPWAMP集成环境,而不是其他集成环境呢?

    1、因为PHPWAMP解压即可使用,你甚至不需要安装VC运行库也可直接运行,而其他的集成环境都需要你额外去安装vc运行库,下载费时间,安装也费时间。

    2、PHPWAMP默认集成的各个组件都是最新版本的,而且是完整版,所以更加适合在服务器上运行(其他集成环境通常用来测试的,集成的组件阉割过,体积小很多)当然PHPWAMP也很适合在本地测试,如果包括32位和64位、线程安全与非线程安全的PHP版本,共计700多个PHP版本随便你切换。

    3、PHPWAMP可以同时运行各个不同版本的PHP网站,十分适合对比测试。多版本同时运行也适合在服务器上运行那些对php版本要求比较严格的PHP程序,可在服务器上同时运行不同php版本的网站程序。

    4、关键这货还能一键去除域名后面的端口号,实现与Apache、Nginx、IIS等WEB服务器共存时,去掉域名后面的端口号。

    5、兼容性很强,兼容XP系统、windows7、windows8、windows10也能完美兼容,在各windows服务器上兼容能力也很强。

    6、phpwamp集成环境还能与其他任意环境共存,互不影响,端口号可视化修改,强制解除端口占用等。

    7、拥有强制干掉一切环境阻碍,一键强制卸载任意php环境,瞬间轰掉阻碍(不会影响系统),简单粗暴,让你节省时间立刻完成网站配置。

    8、切换PHP版本方便,还能自定义PHP版本,提供700个PHP版本随便你换,及时满足各种程序对不同版本的需求。

    9、在phpMyAdmin官方网站,你可以看到phpMyAdmin的各个版本下载中都提示了不同版本所能兼容的PHP版本与数据库版本,而PHPWAMP自带的数据库管理工具phpMyAdmin经过特殊修改,能完美兼容任何PHP版本,无论你切换任意PHP版本,都能正常使用,不会像一些PHP环境那样,升级或者换个版本,phpMyAdmin就会出现问题。

    10、PHPWAMP拥有自动定期重启动服务器,自动定期重启动apache、mysql等服务的功能。

     

     

     

    下面演示在服务器上搭建php网站的过程,服务器版本如下图显示Windows Server 2008 R2 Enterprise

    采用多功能集成环境在windows服务器上搭建PHP网站案例

     

     

    在任意IDC服务商购买服务器后,在本地电脑点这里打开远程桌面连接,来连接远程服务器

    采用多功能集成环境在windows服务器上搭建PHP网站案例

     

     

    然后在出来的界面输入你所购买服务器的相关信息,如下图

    采用多功能集成环境在windows服务器上搭建PHP网站案例

     

     

    下图就是登陆后的样子,现在我们可以操作这台服务器了。

    采用多功能集成环境在windows服务器上搭建PHP网站案例

     

     

    然后用服务器自带的浏览器下载PHPWAMP集成环境,可以在百度搜“PHPWAMP”,找到官网下载。

    采用多功能集成环境在windows服务器上搭建PHP网站案例

     

     

    解压后打开软件

    采用多功能集成环境在windows服务器上搭建PHP网站案例

     

     

    把主界面的端口改成80

    采用多功能集成环境在windows服务器上搭建PHP网站案例

     

     

    切换任意php版本,然后打开Apache2.4站点管理

    采用多功能集成环境在windows服务器上搭建PHP网站案例

     

     

    添加站点后,我们开始一键生成对应hosts

    采用多功能集成环境在windows服务器上搭建PHP网站案例

     

     

    点击这个按钮启动网站采用多功能集成环境在windows服务器上搭建PHP网站案例

    完美启动,网站程序自己配置即可,这点不会的话,可以百度PHPWAMP使用教程

    采用多功能集成环境在windows服务器上搭建PHP网站案例

     

     

    到了这一步,网站只能在服务器上查看,别人通过互联网是访问不了的,因为还有一个重要的步骤要做,那就是域名解析,把域名解析到这个服务器上,全国各地的网名就可以通过互联网访问你这个网站了,下面我们来演示如何解析。

     

    随便在大街上找个IDC服务商,买个域名,然后进入域名管理系统进行域名解析

    采用多功能集成环境在windows服务器上搭建PHP网站案例

     

     

     

    如图进行解析,解析管理系统通常会有提示的,认真看看就会了,实在不行打电话问相关网站的相关客服人员。

    采用多功能集成环境在windows服务器上搭建PHP网站案例

     

     

    如下添加这样的两条解析记录

    采用多功能集成环境在windows服务器上搭建PHP网站案例

     

     

     

    稍等片刻,全国各地网名就都能通过你绑定的域名访问你的网站了。

    采用多功能集成环境在windows服务器上搭建PHP网站案例

    展开全文
  • Java8新特性全这儿了!!
  • 阿里工作的这段时间里,都学到了哪些东西

    万次阅读 多人点赞 2019-08-18 21:38:28
    当初我没有参加阿里巴巴的实习,而是选择了直接进行校园招聘,这也是因为当时我对实习的部门不感兴趣,于是校招的时候我就选择了自己感兴趣的部门,也就是现在我所在的蚂蚁金服。 之前就听说过阿里的工作强度,...
  • 库就像是文件夹,库中可以有很多个表 表就像是我们的excel表格文件一样 每一个表中都可以存储很多数据 mysql中可以有很多不同的库,库中可以有很多不同的表 表中可以定义不同的列(字段), 表中可以根据结构去存储很...
  • 华为KubeEdge边缘计算的实践

    万次阅读 多人点赞 2018-12-05 16:09:34
    华为KubeEdge边缘计算的实践摘要1 介绍2 相关工作3 架构和设计3.1 KubeBus3.1.1 Edge Node VPN3.1.2 将边缘节点VPN与容器网络连接...本文中,我们为Edge-cloud通信和执行环境引入了Edge基础架构(KubeEdge)...
  • 某Java大佬地表最强Java企业(阿里)面试总结

    万次阅读 多人点赞 2020-08-23 19:48:06
    面试题真的是博大精深,也通过这个面试题学到了很多...Hashtable 中的方法是Synchronize的,而HashMap中的方法缺省情况下是非Synchronize的。 HashMap把Hashtable的contains方法去掉了,改成containsValue和contains.
  • spring boot 多环境配置读取属性文件

    千次阅读 2017-09-25 08:55:04
    相信很多人选择Spring Boot主要是考虑到它既兼顾Spring的强大功能,还实现快速开发的便捷。我们Spring Boot使用过程中,最直观的感受就是没有了原来自己整合Spring应用时繁多的XML配置内容,替代它的是pom....
  • 5 个最佳的 Linux 桌面环境

    千次阅读 2018-08-13 10:53:11
    打算把每个桌面都试用一遍,但是那很费时间,而且确实有很多桌面环境可供选择,这就是我发表“最优秀的 Linux 桌面以及他们的优缺点”的目的,本文告诉你选择桌面时需要注意些什么,让我们开始吧。 1. KDE 我想...
  • 刚刚结束的美国丹佛Open Infrastructure峰会上,腾讯云对全新升级为私有全栈云的TStack进行了详细的介绍,引起峰会现场众多OpenStack专业人士侧目。那么,这款腾讯基于OpenStack倾力打造的私有云解决方案,究竟有...
  • Python 虚拟环境 看这一篇就够了

    千次阅读 多人点赞 2020-06-02 13:07:32
    强大的软件库,让开发者将精力集中业务上,而避免重复造轮子的浪费。但众多的软件库,形成了复杂的依赖关系,加上 Python2 和 Python3 旷日持久之争,对采用 Python 开发的项目造成了不少困扰,所以 Python 建议,...
  • 使用docker搭建开发环境

    万次阅读 多人点赞 2018-09-14 14:32:04
    所以就造成了我得windows下面使用虚拟机.这是最开始的办法.后面得知有vagrant这个东西之后,用了一阵子感觉还不错.但是我使用的时候动不动就会出现一些问题,所以一怒之下决定学学docker.然后使用docker来作为开发...
  • 面渣逆袭:JVM经典五十问,这下面试稳了

    万次阅读 多人点赞 2021-12-28 21:42:52
    同时JVM也是一个跨语言的平台,和语言无关,只和class的文件格式关联,任何语言,只要翻译成符合规范的字节码文件,都被JVM运行。 内存管理 2.说一下JVM的内存区域吗? JVM内存区域最粗略的划分可以分为堆和...
  • 最近一直搞Dockerfile文件的配置,需要将k8s上设置的环境变量,传入到Dockerfile里,总算是搞定了。 使用ENTRYPOINT指令来实现,如下: ENTRYPOINT ["sh","-c","java -javaagent:/skywalking-agent/agent/...
  • 1988年,为解决全企业集成问题,IBM爱尔兰公司的BarryDevlin和PaulMurphy第一次提出了“信息仓库(InformationWarehouse)”的概念,将其定义为:“一个结构化的环境支持最终用户管理其全部的业务,并支持信息...
  • 网络环境差的情况下,两次包的TCP验证数据包完整性上,有非常大的优点。 并不是所有浏览器都会POST中发送两次包,Firefox就只发送一次。我去年用Chrome浏览器测试发现也是只发送一次,所以我认为Get、POST...
  • 巨头王炸不断,硬核解读芯片技术路线

    千次阅读 多人点赞 2021-05-09 11:09:48
    上周我博客发布了一篇《龙芯自主指令集到底强何处》的文章,虽然这只是一篇临时起意之作,信息有限的拙作,不过最近整个半导体行业实在风起云涌,上周四IBM推出了2nm的芯片,苹果春季发布会上这次苹果发布会上...
  • 第十章《跟忧虑说再见》一、忧虑是健康的大敌1、焦虑和烦躁不安的人,多半不适应现实的世界,而跟周围的环境隔断了所有的关系,缩到了自己的梦想世界,以此解决他所忧虑的问题。 2、最使你轻松愉快的是,健全的...
  • 分布式事务服务   分布式事务服务(Distributed Transaction Service,DTS)是一个分布式事务框架,用来保障大规模分布式环境下事务的最终一致性。   CAP理论告诉我们分布式存储系统中,最多只能实现上面的...
  • 这可能最全的操作系统面试题

    万次阅读 多人点赞 2021-04-13 09:30:37
    文章目录操作系统简介篇解释一下什么是操作系统操作系统的主要功能软件访问硬件的几种方式解释一下操作系统的主要目的是什么操作系统的种类有哪些为什么 Linux 系统下的应用程序不直接 Windows 下运行操作系统...
  • java中都有哪些异常

    万次阅读 多人点赞 2018-01-24 10:09:38
    1. java.lang.nullpointerexception 这个异常大家肯定都经常遇到,异常的解释是"程序遇上了空指针",简单地说就是调用了未经初始化的对象或者是不存在的对象,这个错误经常出现创建图片,调用数组这些操作中,比如...
  • 使用 volatile 可以禁止 JVM 的指令重排,保证多线程环境下也正常运行。 5、怎么获取 Java 程序使用的内存?堆使用的百分比? 可以通过 java.lang.Runtime 类中与内存相关方法来获取剩余的内存,总内存及最大堆...
  • 我把ConcurrentHashMap & HashTable的知识点都整理了一下

    万次阅读 多人点赞 2019-12-17 22:32:14
    这样最好,上次我们最后聊到HashMap多线程环境下存在线程安全问题,那你一般都是怎么处理这种情况的? 美丽迷人的面试官您好,一般多线程的场景,我都会使用好几种不同的方式去代替: 使用Collections....
  • CentOS7下yum源搭建编程环境

    千次阅读 2017-01-03 00:17:35
    为方便自己和别人这里把学linux的一些环境配置,软件安装,工具应用总结记录一下,其中参考并引用了网络上一些学者技术大牛的博客,由于涉及东西较广,撰写跨度时间较长,如有侵权请及时通知,带来不便请谅解,...
  • 面试官看我简历写精通redis,让我聊聊sds是什么?

    万次阅读 多人点赞 2022-02-14 00:05:55
    SDS 生产环境中使用非常广泛,比如,我们使用 SDS 做分布式锁;将对象转成 JSON 串作为缓存等。 Redis 面试过程中一旦提及相关数据结构 SDS 一定是绕不过去的话题,它很简单(或者说看完此文后很简单),面试官...
  • 云计算发展有几十年的历史了,随着科技的进步和发展,云技术慢慢渗透到各行各业,企业上云也不再是新鲜...未来几年市场平均增长率20%左右,预计到2022年市场规模将超过2700亿美元。 显然,当“上云”成为各行各业...
  • 大家好,又到了金三银四找工作的季节,最近听说了很多公司裁员的消息,大家是不是觉得今年的就业环境更难了。这波裁员其实从年前就开始了,当时我还处于裸辞的状态,身边的朋友都劝我赶紧找工作,不然选...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 100,220
精华内容 40,088
关键字:

在任何环境中都能满足