Java面试

目录

1、hashMap出现是为了解决什么问题,底层源码是什么

2、与Hashtable、 TreeMap、TreeMap的区别

3、如何解决hash碰撞问题

1.链地址法

2.再哈希法

3.建立公共溢出区

4.开放地址法

4、mysql的悲观锁和乐观锁

1.悲观锁

2.乐观锁

​编辑3.总结

5、redis如何保障与数据库中数据一致

6、如何保障线程安全,底层原理是什么

7、jvm中gc垃圾回收机制原理

8、http和https的区别

9、三次握手四次挥手

三次握手:

四次挥手:


1、hashMap出现是为了解决什么问题,底层源码是什么

  • HashMap是一个散列桶(数组+链表+红黑树),它存储的内容是键值对 key-value映射

  • HashMap采用数组和链表和红黑树,能在查询和修改方便继承了数组的线性查找和链表的寻址修改

  • HashMap是线程不安全的,查找速度很快

  • HashMap可以接受null键,而Hashtable则不能(元素是equals()方法需要对象)

  • HashMap的键值是通过 (n-1)&hash的,这个公式是通过hash%length-1演化的,如果不是偶数的,则无法演变, 并且如果是奇数 -1变成偶数&的话算出的索引位置容易冲突

  • HashMap扩容的时候,元素会重新进行排序,两种可能有一种是原先的位置,一种是原先位置+原容量

  • 在jdk8 如果一条链表元素个数大于等于默认8个,并且table数组的大小大于等于64就会转换为红黑树,如果数组长度小于就进行扩容,如果链表小于6则恢复为链表

2、与Hashtable、 TreeMap、TreeMap的区别

与Hashtable区别:

  • 默认容量不同,扩容不同

    HashMap是16 加载因子0.75 超过12就扩容二倍

    Hashtable是 11 加载因子0.75 扩容是二倍+1

  • 线程安全性:HashTable 安全

  • 效率不同:HashTable 要慢,因为加锁

  • HashMap可以key和value都能存储null ,而Hashtable不能存储null

与TreeMap的区别:

  • HashMap的底层是数组+链表+红黑树,TreeMap就是红黑树

  • TreeMap是有序的

3、如何解决hash碰撞问题

解决hash冲突有以下四种方法:

  • 链地址法

  • 再哈希法

  • 建立公共溢出区

  • 开放地址法

1.链地址法

对于相同的哈希值,使用链表进行连接(HashMap使用此法)

优点:

  1. 处理冲突简单,无堆积现象。(非同义词决不会发送冲突,因此平均查找长度较短)

  2. 适合总数经常变化的情况(因为拉链法中各链表上的结点空间是动态申请的)

  3. 占用空间小。装填因子可取a>=1,且结点较大时,拉链法中增加的指针域可忽略不计

  4. 删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

缺点:

  1. 查询效率较低(存储是动态的,查询时跳转需要更多的时间)

  2. 在key-value可以预知,以及没有后序增删改查操作的时候,开放地址法性能优于链地址法

  3. 不容易序列化

2.再哈希法

提供多个哈希函数,如果第一个哈希函数计算出来的key的哈希值冲突了,则使用第二个哈希函数计算key的哈希值

优点:

  1. 不易产生聚集

缺点:

  1. 增加了计算时间

3.建立公共溢出区

将哈希表分为基本表和溢出表两部分,凡是和基本表发送冲突的元素,一律填入溢出表

4.开放地址法

当关键字key的哈希地址p =H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,若p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。

主要有三种方法

  1. 线性探测再散列 :顺序查找下一个单元,直到找到一个空单元或查遍全表

  2. 二次探测再散列:在表的左右进行跳跃式探测,直到找出一个空单元或查遍全表

  3. 伪随机探测再散列:建立一个伪随机数发生器,并给一个随机数作为起点

例如,已知哈希表长度m=11,哈希函数为:H(key)= key % 11,则H(47)=3,H(26)=4,H(60)=5,假设下一个关键字为69,则H(69)=3,与47冲突。

如果用线性探测再散列处理冲突,下一个哈希地址为H1=(3 + 1)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 + 2)% 11 = 5,还是冲突,继续找下一个哈希地址为H3=(3 + 3)% 11 = 6,此时不再冲突,将69填入5号单元。

如果用二次探测再散列处理冲突,下一个哈希地址为H1=(3 + 12)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 - 12)% 11 = 2,此时不再冲突,将69填入2号单元。

如果用伪随机探测再散列处理冲突,且伪随机数序列为:2,5,9,……..,则下一个哈希地址为H1=(3 + 2)% 11 = 5,仍然冲突,再找下一个哈希地址为H2=(3 + 5)% 11 = 8,此时不再冲突,将69填入8号单元。 优点:

  1. 容易序列化

  2. 若可预知数据总数,可以创建完美哈希数列

缺点:

  1. 占空间很大(例如使用伪随机探测再散列会造成大量空间)

  2. 删除节点很麻烦。不能简单地将被删节点的空间置为空,否则将截断在它之后填入散列表的同义词结点的查找路径。这是因为各种开放地址放中,空地址单元都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删除节点上左删除编辑,而不能真正删除结点

4、mysql的悲观锁和乐观锁

1.悲观锁

当我们要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是对该数据进行加锁以防止并发的发生。

为什么叫悲观锁?因为这是一直对数据的修改抱有悲观态度的并发控制方式。我们一般认为数据被并发修改的概率比较大,所以需要在修改之间先加锁

数据库中的行锁,表锁,读锁,写锁,以及syncrinized实现的锁均为悲观锁

2.乐观锁

乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁,只有到数据提交的时候才通过一种机制来验证数据是否存在冲突

乐观锁通常是在表中增加一个版本version或时间戳timestamp来实现,其中版本是最常用,mybatis-plus就包含这个功能,

乐观锁每次在执行数据的修改操作时,都会打上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并将版本号执行+1操作,否则执行失败

3.总结

乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这数据。乐观锁需要自己去实现,通常是在表里面加入version字端,每次修改成功值+1 ,这样每次修改的时候会先对比一下,自己拥有的version和数据库现在version是否一致,如果不一致就不修改,这样就实现了乐观锁。

悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别想拿这个数据就会阻止,直到这个锁被释放。

如何选择:

乐观锁适合于多读的场景,就是冲突发生很少的场景,此时使用乐观锁就可以大大降低锁的开销,也就使得系统的吞吐量得到提升;

如果是在多写的场景下,写入的过程可能会经常产生冲突,这时候如果使用乐观锁就会导致上层应用不断进行重试,这样反倒降低了性能,所以在多写的场景下使用悲观锁比较合适

5、redis如何保障与数据库中数据一致

  1. 先更新数据库,然后在删除缓存

    问题:如果先更新数据库,删除缓存失败了,这时候数据库中是新数据,缓存中是老数据,数据就出现不一致
  2. 先删除缓存,然后更新数据库

    在并发的情况下,可以会出现数据发生了变更,先删除缓存,然后去修改数据库,此时还没有来得及修改数据库,另一个线程过来,去读缓存,发现缓存是空的,读到了准备修改前的旧数据,并且把旧数据放入了缓存。随后数据变更程序完成了数据库的修改,就导致数据出现不一致
  3. 延时双删策略(不推荐)无法确定休眠时间

    先删除缓存
    在写入数据库(这时候可能会出现读请求因缓存未命中,读到脏数据看,再次将脏数据存入缓存)
    休眠500毫秒(需要评估自己的项目的数据操作业务逻辑的耗时)
    再次删除缓存(删除可能存入的脏数据);
  4. 异步更新缓存(最终一致性)

    MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送到redis。redis再根据binlog中的记录对缓存进行更新

         5.双写

                更新完db后,同步更新redis;更新db操作与更新redis操作是一样的不是直接删除redis中缓存称为双写

 解决方案:

1.使用分布式锁解决多个线程同时中双写业务逻辑,最终只会有一个获取分布式锁线程才可以执行,没有获取到的线程进行阻塞等待,这样确保线程执行双写不会被其他线程干扰,但是效率低

2.使用mysql的行锁,开启事务将 mysql和redis更新在一个事务中,如果redis更新成功释放锁

如果redis更新失败,采取重试策略,重试多次更新redis还是失败的话,直接回滚事务,同时释放行锁,效率也低。

6、如何保障线程安全,底层原理是什么

我从网上找的,我也不会 -_-

Java保证线程安全的方式有很多,其中较为常用的有三种,按照资源占用情况由轻到重排列,这三种保证线程安全的方式分别是原子类、volatile、锁。

1.volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,从而可以保证单个变量读写时的线程安全。可见性问题是由处理器核心的缓存导致的,每个核心均有各自的缓存,而这些缓存均要与内存进行同步。volatile具有如下的内存语义:当写一个volatile变量时,该线程本地内存中的共享变量的值会被立刻刷新到主内存;当读一个volatile变量时,该线程本地内存会被置为无效,迫使线程直接从主内存中读取共享变量。

2.原子类和volatile只能保证单个共享变量的线程安全,锁则可以保证临界区内的多个共享变量的线程安全,Java中加锁的方式有两种,分别是synchronized关键字和Lock接口。

3.synchronized是比较早期的API,在设计之初没有考虑到超时机制、非阻塞形式,以及多个条件变量。若想通过升级的方式让它支持这些相对复杂的功能,则需要大改它的语法结构,不利于兼容旧代码。因此,JDK的开发团队在1.5新增了Lock接口,并通过Lock支持了上述的功能,即:支持响应中断、支持超时机制、支持以非阻塞的方式获取锁、支持多个条件变量(阻塞队列)。

加分回答:

实现线程安全的方式有很多,除了上述三种方式之外,还有如下几种方式:

  1. 无状态设计 线程安全问题是由多线程并发修改共享变量引起的,如果在并发环境中没有设计共享变量,则自然就不会出现线程安全问题了。这种代码实现可以称作“无状态实现”,所谓状态就是指共享变量。

  1. 不可变设计 如果在并发环境中不得不设计共享变量,则应该优先考虑共享变量是否为只读的,如果是只读场景就可以将共享变量设计为不可变的,这样自然也不会出现线程安全问题了。具体来说,就是在变量前加final修饰符,使其不可被修改,如果变量是引用类型,则将其设计为不可变类型(参考String类)。

  1. 本地存储 我们也可以考虑使用ThreadLocal存储变量,ThreadLocal可以很方便地为每一个线程单独存一份数据,也就是将需要并发访问的资源复制成多份。这样一来,就可以避免多线程访问共享变量了,它们访问的是自己独占的资源,它从根本上隔离了多个线程之间的数据共享。

7、jvm中gc垃圾回收机制原理

这个说的很好,可以看看

深入理解 JVM 垃圾回收机制及其实现原理_垃圾回收机制的原理是什么_CG国斌的博客-CSDN博客

8、http和https的区别

  1. 端口:Http的默认端口是80,Https的默认端口是443

  2. 安全性和资源消耗:

    http协议运行在tcp上,所有传输内容都是明文,客户端和服务器都无法验证对方的身份;https运行在ssl/tls之上的http协议。ssl/tls是运行在tcp上的,所有传输内容都经过加密,加密采用对称加密,但对称加密的秘钥用服务方的证书进行非对称加密。

    http安全性没有https高,但是Https比http耗费更多服务器资源

    • 对称加密:密钥自由一个,加密解密为同一个密码,加密速度快

    • 非对称加密:密钥成对出现(根据公钥无法推知私钥,根据私钥无法推知公钥),加密解密使用不同的密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度比较慢

9、三次握手四次挥手

三次握手:

 为什么要三次握手,三次握手的目的是建立可靠的通信信道

第一次握手:客户端-发送带有SYN标志的数据包给服务端

第二次握手:服务端发送带有SYN/ACK标志的数据包给客户端

第三次握手:客户端发送带有ACK标志的数据包给服务端

四次挥手:

客户端发送一个FIN,用来关闭客户端到服务器的数据传送

服务器收到这个FIN,它发送一个ACK,确定需要为收到的序号加1

服务器关闭与客户端的连接,发送一个FIN给客户端

客户端发回ACK报文确认,并将确认序号设置为收到序号加1

A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,  第一次挥手

B回答“我知道了”,                           第二次挥手

但是 B可能还会有要说的话,A 不能要求 B 跟着⾃⼰的节奏结束通话,于是 B 可能⼜巴拉巴拉说了⼀通,最后 B 说“我说完了”     第三次挥手

,A 回答“知道了”,这样通话才算结束。    第四次挥手
 

最后如果有所帮助,请点赞     

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
#include #include typedef struct node { int data; struct node *next; }node; init_hash(node **A,int n) { int i; for(i=0;idata=0; A[i]->next=NULL; } } insert_hash(node **A,int value,int n) { int key; node *p,*q; key=value%n; if(A[key]->next!=NULL) { p=A[key]->next; while(p->next!=NULL) p=p->next; q=(node *)malloc(sizeof(node)); q->data=value; q->next=NULL; p->next=q; } else { q=(node *)malloc(sizeof(node)); q->data=value; q->next=NULL; A[key]->next=q; } } int search_hash(node **A,int value,int n) { int key; node *p; key=value%n; if(A[key]->next==NULL) return 0; else { p=A[key]->next; while(p!=NULL) { if(p->data==value) return 1; } return 0; } } delete_hash(node **A,int value,int n) { int key; node *p,*q; key=value%n; p=A[key]; q=A[key]->next; while(q->data!=value) { p=q; q=q->next; } p->next=q->next; free(q); } print_hash(node **A,int n) { int i; node *p; for(i=0;inext!=NULL) { p=A[i]->next; while(p!=NULL) { printf("%d ",p->data); p=p->next; } } } printf("\n"); } main() { int i,n,value,Case; node **A; printf("输入待排序元素个数:\n"); scanf("%d",&n); A=(node **)malloc(sizeof(node*)*n); //申请一个指针型数组A[n] init_hash(A,n);//初始化数组A printf("输入hash表的值(空格键分开):\n"); for(i=0;i<n;i++) //建hash表 { scanf("%d",&value); insert_hash(A,value,n); } printf("请选择hash表的操作\n1.查询\t2.插入\t3.删除\t4.输出\n"); scanf("%d",&Case); switch(Case) { case 1: { printf("请输入要查询的元素:"); scanf("%d",&value); printf(search_hash(A,value,n)?"Success!\n":"Failure!\n"); //查询 } case 2: { printf("请输入要插入的元素:"); scanf("%d",&value); insert_hash(A,value,n); //插入 } case 3: { printf("请输入要删除的元素:"); scanf("%d",&value); printf(search_hash(A,value,n)?"删除成功!\n":"表中不存在该元素!\n"); delete_hash(A,value,n); } case 4: //输出 { printf("表中元素为:\n"); print_hash(A,n); } } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值