2018的初冬,派卧底去阿里、京东、美团、滴滴带回来的面试题及答案

今天有时间顺便整理了一下面试带回来的答案,整理的答案有什么问题的请及时告诉我方便他人的阅读

面试题

    1开发中java用了比较多的数据结构有哪些?

数据元素相互之间的关系称为结构。有四类基本结构:集合、线性结构、树形结构、图状结构;

 

集合结构:除了同属于一种类型外,别无其它关系

线性结构:元素之间存在一对一关系常见类型有: 数组,链表,队列,栈,它们之间在操作上有所区别.例如:链表可在任意位置插入或删除元素,而队列在队尾插入元素,队头删除元素,栈只能在栈顶进行插入,删除操作.

树形结构:元素之间存在一对多关系,常见类型有:树(有许多特例:二叉树、平衡二叉树、查找树等)

图形结构:元素之间存在多对多关系,图形结构中每个结点的前驱结点数和后续结点多个数可以任意

https://i-blog.csdnimg.cn/blog_migrate/5816409b96a474043587c99b55264303.png

https://i-blog.csdnimg.cn/blog_migrate/4666e7e34b4fef028a0c6eb1420d5e98.png

1、几个常用类的区别:

1.ArrayList: 元素单个,效率高,多用于查询 

2.Vector: 元素单个,线程安全,多用于查询 

3.LinkedList:元素单个,多用于插入和删除 

4.HashMap: 元素成对,元素可为空 

5.HashTable: 元素成对,线程安全,元素不可为空

二、Vector、ArrayList和LinkedList

大多数情况下,从性能上来说ArrayList最好,但是当集合内的元素需要频繁插入、删除时LinkedList会有比较好的表现,但是它们三个性能都比不上数组,另外Vector是线程同步的。所以: 

如果能用数组的时候(元素类型固定,数组长度固定),请尽量使用数组来代替List; 

如果没有频繁的删除插入操作,又不用考虑多线程问题,优先选择ArrayList; 

如果在多线程条件下使用,可以考虑Vector; 

如果需要频繁地删除插入,LinkedList就有了用武之地; 

如果你什么都不知道,用ArrayList没错。

三、Collections和Arrays 

在 Java集合类框架里有两个类叫做Collections(注意,不是Collection!)和Arrays,这是JCF里面功能强大的工具,但初学者往往会忽视。按JCF文档的说法,这两个类提供了封装器实现(Wrapper Implementations)、数据结构算法和数组相关的应用。 

想必大家不会忘记上面谈到的“折半查找”、“排序”等经典算法吧,Collections类提供了丰富的静态方法帮助我们轻松完成这些在数据结构课上烦人的工作: 

binarySearch:折半查找。 

sort:排序,这里是一种类似于快速排序的方法,效率仍然是O(n * log n),但却是一种稳定的排序方法。 

reverse:将线性表进行逆序操作,这个可是从前数据结构的经典考题哦! 

rotate:以某个元素为轴心将线性表“旋转”。 

swap:交换一个线性表中两个元素的位置。 

…… 

Collections还有一个重要功能就是“封装器”(Wrapper),它提供了一些方法可以把一个集合转换成一个特殊的集合,如下: 

unmodifiableXXX:转换成只读集合,这里XXX代表六种基本集合接口:Collection、List、Map、Set、SortedMap和SortedSet。如果你对只读集合进行插入删除操作,将会抛出UnsupportedOperationException异常。 

synchronizedXXX:转换成同步集合。 

singleton:创建一个仅有一个元素的集合,这里singleton生成的是单元素Set, 

singletonList和singletonMap分别生成单元素的List和Map。 

空集:由Collections的静态属性EMPTY_SET、EMPTY_LIST和EMPTY_MAP表示

2谈谈对HashMap的理解,底层的基本实现。HashMap怎么解决碰撞问题的?这些数据结构中是线程安全的吗?假如你回答HashMap不是线程安全的,HashTab是线程安全的,接着问你有没有线程安全的map,接下来问了concurren包。

HashMap是我们使用非常多的集合,它是基于哈希表的 Map 接口的实现,以key-value的形式存在。在HashMap中,key-value总是会当做一个整体来处理,系统会根据hash算法来来计算key-value的存储位置。

https://i-blog.csdnimg.cn/blog_migrate/d914efc0483f14f662dbaef57e1b341b.png

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

Java中HashMap是利用“拉链法”处理HashCode的碰撞问题。在调用HashMap的put方法或get方法时,都会首先调用hashcode方法,去查找相关的key,当有冲突时,再调用equals方法。hashMap基于hasing原理,我们通过put和get方法存取对象。当我们将键值对传递给put方法时,他调用键对象的hashCode()方法来计算hashCode,然后找到bucket(哈希桶)位置来存储对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当碰撞发生了,对象将会存储在链表的下一个节点中。hashMap在每个链表节点存储键值对对象。当两个不同的键却有相同的hashCode时,他们会存储在同一个bucket位置的链表中。键对象的equals()来找到键值对。

JAVA中线程安全的map有:Hashtable、synchronizedMap、ConcurrentHashMap。

java.util.concurrent 包含许多线程安全、测试良好、高性能的并发构建块。不客气地说,创建 java.util.concurrent 的目的就是要实现 Collection 框架对数据结构所执行的并发操作。通过提供一组可靠的、高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性。

concurrent包是常用多线程的相关包,最近由于开发sdn程序,对于多线程使用比以前多了很多,现简单总结下。

第一类  原子类:用在多个线程共同操作一个计数的情况

AtomicLong

AtomicInteger

第二类 lock和condition

condition是从lock中得到的,所以在使用时,在执行了lock.lock()后才进行condition的操作,condition常用的两个方法await和signal。

常用在多个线程操作一个共同的资源,一个线程执行结束后,另一个线程才能执行的情况。

另外,lock应该代替以前的synchronized关键字。synchronized属于jvm层面的同步策略,由jvm进行锁的分配和释放。但是据说高并发量时,需要频繁切换线程栈,性能不好。

从今以后,代码中应该使用lock实现同步。

第三类  多线程任务执行ExecutorService

这部分大体上涉及到三个概念,

Callable     被执行的任务

Executor  执行任务()

Future      异步提交任务的返回数据 FutureTask为具体实现

线程资源是系统很珍贵的资源,的确不应该由程序员随意的new Thread方式,自己启动线程。如果项目上有自己的框架,应该使用项目框架中的线程工具,如果没有最好使用jdk提供的ExecutorService工具类。

常见的线程池

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();   -- 超时会销毁池中的线程

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);   -- 线程池中的线程即使空闲也不销毁

ExecutorService singleThreadPool = Executors.newSingleThreadExecutor(); 

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3); -- 能够定时执行任务的线程池,比如可以用在数据采集功能,每隔一小时执行一次

第四类  工具类

比如 CyclicBarrier, CountDownLatch 用来协调多个线程,执行顺序的

第五类  线程安全的集合类 比如BlockingQueue ConcurrentMap

3对JVM熟不熟悉?简单说说类加载过程,里面执行的那些操作?问了GC和内存管理,平时在tomcat里面有没有进行过相关配置。

       类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。

       java堆(JavaHeap)1.用来存放对象的,几乎所有对象都放在这里,被线程共享的,或者说是被栈共享的2.堆又可以分为新生代和老年代,实际还有一个区域叫永久代,但是jdk1.7已经去永久代了,所以可以当作没有,永久代是当jvm启动时就存放的JDK自身的类和接口数据,关闭则释放。新生代可以分为Eden区和两个幸存区,这么设计是为了更好地利用内存  之前的设计是只分为两部分一样一半  后来发现这样只利用到了一半的内存  才改为按比例分成三个区的,使用的是复制回收算法,两个幸存区是较小的区域。逻辑是每次使用Eden区和其中一个幸存区,回收时将其还存活着的对象一次性的复制到另一个幸存区中,最后清理到刚才使用的Eden和其中一个幸存区。美团的面试官也问了这个问题,他也说了他的理解,我感觉可能是不准确的。新建对象就在Eden区,Eden就是伊甸,顾名思义。但是并不是对象最活跃的区域,对象最活跃的区域是老年代,因为经过各种垃圾回收之后对象都跑到这里来了。3.内存溢出内存溢出其实没什么好讲的,满了就会溢出。怎么才能满呢,不断创建对象,那问题又来了,创建多了被回收怎么办,好办,将新建的对象存到list里去,就不会回收了,为什么呢,因为jvm判定一个对象的死活就是根据对象是不是被引用。此外堆跟随jvm的,有jvm就有堆。堆也是垃圾回收的主要区域,又叫GC堆,垃圾堆,玩笑。jvm栈1.要说栈是用来存什么的,其实我感觉不严谨,栈是运行时创建的,是跟随线程的,它不是用来存什么的,那它用来干什么的,它是用来存栈帧的。每个栈帧其实可以理解为一个方法,我是这么理解的,之间的关系就是调用。2.栈的好处就是不需要垃圾回收,随着线程结束内存就释放。3.但是并不是说就不会内存溢出,那么栈的内存溢出是怎么产生的呢,肯定也是满了,这个满了怎么理解呢,一是要申请的不够了,二是jvm内存太小,这是个有趣的问题。但是产生的错误却是不一样的,如果创建一个void方法调用自身,错误是stackoverflowError,如果不断创建线程则会outOfMemoryError。这里就有一个比较高级的问题了,对于第二种多线程内存溢出该怎么解决呢,深入理解jvm一书中给出的解决方案是这样的,通过减小最大堆和栈容量来换取更多的线程。方法区和运行时常量池1.方法区是堆的一个逻辑区域,但是又叫非堆。运行时常量池又是方法区的一部分,真正的一部分。方法区并不是存方法的,存方法的应该是栈或者栈帧。方法区存的是类信息、常量、静态变量等,也是被线程共享的区域。运行时常量池存放的是编译期生产的各种字面量和符号引用。2.这块内存区域的回收没啥好说的,因为我也不太清楚,我只知道HotSpot的设计团队选择把GC分代扩展至方法区了,或者是使用永久代实现方法区。3.内存是肯定会溢出的,不断创建类会导致方法区内存溢出,而不断将常量放入常量池(String.intern()),常量池也会内存溢出。

首先,在代码编译后,就会生成JVM(Java虚拟机)能够识别的二进制字节流文件(*.class)。而JVM把Class文件中的类描述数据从文件加载到内存,并对数据进行校验、转换解析、初始化,使这些数据最终成为可以被JVM直接使用的Java类型,这个说来简单但实际复杂的过程叫做JVM的类加载机制。

      常见配置汇总

0.    堆设置

§  -Xms:初始堆大小

§  -Xmx:最大堆大小

§  -XX:NewSize=n:设置年轻代大小

§  -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4

§  -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

§  -XX:MaxPermSize=n:设置持久代大小

1.    收集器设置

§  -XX:+UseSerialGC:设置串行收集器

§  -XX:+UseParallelGC:设置并行收集器

§  -XX:+UseParalledlOldGC:设置并行年老代收集器

§  -XX:+UseConcMarkSweepGC:设置并发收集器

2.    垃圾回收统计信息

§  -XX:+PrintGC

§  -XX:+PrintGCDetails

§  -XX:+PrintGCTimeStamps

§  -Xloggc:filename

3.    并行收集器设置

§  -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。

§  -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间

§  -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

4.    并发收集器设置

§  -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

§  -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

4http协议,get和post的基本区别,接着tcp/ip协议,三次握手,窗口滑动机制。

       HTTP协议的主要特点可概括如下:

1.支持客户/服务器模式。

2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。

3.灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。

4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。

5.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

get参数通过url传递,post放在request body中。

get请求在url中传递的参数是有长度限制的,而post没有。

get比post更不安全,因为参数直接暴露在url中,所以不能用来传递敏感信息。

get请求只能进行url编码,而post支持多种编码方式

get请求会浏览器主动cache,而post支持多种编码方式。

get请求参数会被完整保留在浏览历史记录里,而post中的参数不会被保留。

GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。

GET产生一个TCP数据包;POST产生两个TCP数据包。

对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

TCP协议:如果说IP协议是找到对方的详细地址。那么TCP协议就是把安全的把东西带给对方。各有分工,互不冲突。

按层次分,TCP属于传输层,提供可靠的字节流服务。什么叫字节流服务呢?这个名字听起来让人不知所以然,下面听下我通俗的解释。所谓的字节流,其实就类似于信息切割。比如你是一个卖自行车的,你要去送货。安装好的自行车,太过庞大,又不稳定,容易损伤。不如直接把自行车拆开来,每个零件上都贴上收货人的姓名。最后送到后按照把属于同一个人的自行车再组装起来,这个拆解、运输、拼装的过程其实就是TCP字节流的过程。

三次握手:

https://i-blog.csdnimg.cn/blog_migrate/4afe56e1cc2c9f6e1cc9e702e1af9620.png

窗口滑动机制:   滑动窗口通俗来讲就是一种流量控制技术。

它本质上是描述接收方的TCP数据报缓冲区大小的数据,发送方根据这个数据来计算自己最多能发送多长的数据,如果发送方收到接收方的窗口大小为0TCP数据报,那么发送方将停止发送数据,等到接收方发送窗口大小不为0的数据报的到来。

https://i-blog.csdnimg.cn/blog_migrate/144c879c314912546a2e463585ea1d00.png

第一次发送数据这个时候的窗口大小是根据链路带宽的大小来决定的。

假设这时候的窗口是3.这个时候接收方收到数据以后会对数据进行确认告诉哦发送方我下次希望收到的数据是多少。

在上图中:我们看到接收方发送的ACK = 3(这是对发送方发送序列2的回答确认,下一次接收方期望接收到的是3序列信号),这个时候发送方收到这个数据以后就知道我第一次发送的3个数据对方只收到了两个,就知道第三个数据对方没有收到,下次返送的时候就从第3个数据开始发。这时候窗口大小就变为了2.

如下图所示:

https://i-blog.csdnimg.cn/blog_migrate/3c77af311c7e622ee14a83697c8c1b8c.png

看到接收方发送的ACK5就表示他下一次希望收到的数据是5,发送方就知道我刚才发送的2个数据对方收到了,这个时候开始发送第5个数据。

当链路变好或者变差,这个窗口还会发生变化,并不是第一次协商好了以后就永远不会变化了。

5开发中用了哪些数据库?回答mysql,存储引擎有哪些?然后问了我悲观锁和乐观锁问题使用场景,分布式集群实现的原理。

  MySQL有多种存储引擎,每种存储引擎有各自的优缺点,可以择优选择使用:

  MyISAM、InnoDB、MERGE、MEMORY(HEAP)、BDB(BerkeleyDB)、EXAMPLE、FEDERATED、ARCHIVE、CSV、BLACKHOLE。

  MySQL支持数个存储引擎作为对不同表的类型的处理器。MySQL存储引擎包括处理事务安全表的引擎和处理非事务安全表的引擎:

  · MyISAM管理非事务表。它提供高速存储和检索,以及全文搜索能力。MyISAM在所有MySQL配置里被支持,它是默认的存储引擎,除非你配置MySQL默认使用另外一个引擎。

  · MEMORY存储引擎提供“内存中”表。MERGE存储引擎允许集合将被处理同样的MyISAM表作为一个单独的表。就像MyISAM一样,MEMORY和MERGE存储引擎处理非事务表,这两个引擎也都被默认包含在MySQL中。

  注释:MEMORY存储引擎正式地被确定为HEAP引擎。

  · InnoDB和BDB存储引擎提供事务安全表。BDB被包含在为支持它的操作系统发布的MySQL-Max二进制分发版里。InnoDB也默认被包括在所 有MySQL 5.1二进制分发版里,你可以按照喜好通过配置MySQL来允许或禁止任一引擎。

  · EXAMPLE存储引擎是一个“存根”引擎,它不做什么。你可以用这个引擎创建表,但没有数据被存储于其中或从其中检索。这个引擎的目的是服务,在 MySQL源代码中的一个例子,它演示说明如何开始编写新存储引擎。同样,它的主要兴趣是对开发者。

  · NDB Cluster是被MySQL Cluster用来实现分割到多台计算机上的表的存储引擎。它在MySQL-Max 5.1二进制分发版里提供。这个存储引擎当前只被Linux, Solaris, 和Mac OS X 支持。

悲观锁(Pessimistic Lock):

每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。

乐观锁(Optimistic Lock):

每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。

适用场景:

悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。

像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适,之所以用悲观锁就是因为两个用户更新同一条数据的概率高,也就是冲突比较严重的情况下,所以才用悲观锁。

悲观锁比较适合强一致性的场景,但效率比较低,特别是读的并发低。乐观锁则适用于读多写少,并发冲突少的场景。

mysql分布式集群实现的原理:

有人会问mysql集群,根分表有什么关系吗?虽然它不是实际意义上的分表,但是它启到了分表的作用,做集群的意义是什么呢?为一个数据库减轻负担,说白了就是减少sql排队队列中的sql的数量,举个例子:有10sql请求,如果放在一个数据库服务器的排队队列中,他要等很长时间,如果把这10sql请求,分配到5个数据库服务器的排队队列中,一个数据库服务器的队列中只有2个,这样等待时间是不是大大的缩短了呢?这已经很明显了。所以我把它列到了分表的范围以内。

6然后问了springmvc和mybatis的工作原理,有没有看过底层源码?

https://i-blog.csdnimg.cn/blog_migrate/14816509c39c3d8ba0c732a07768ea4c.jpeg

mybatis工作原理:

mybatis应用程序通过SqlSessionFactoryBuilder从mybatis-config.xml配置文件(也可以用Java文件配置的方式,需要添加@Configuration)中构建出SqlSessionFactory(SqlSessionFactory是线程安全的);

然后,SqlSessionFactory的实例直接开启一个SqlSession,再通过SqlSession实例获得Mapper对象并运行Mapper映射的SQL语句,完成对数据库的CRUD和事务提交,之后关闭SqlSession。

说明:SqlSession是单线程对象,因为它是非线程安全的,是持久化操作的独享对象,类似jdbc中的Connection,底层就封装了jdbc连接。

https://i-blog.csdnimg.cn/blog_migrate/1981dde5018ab26c82fe0847f50b5e26.png

7.redis中基本的储存类型、事务、使用场景

redis常用五种数据类型:string,hash,list,set,zset(sorted set).

事务提供了一种“将多个命令打包,然后一次性、按顺序地执行”的机制,它提供两个保证:

• 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断

• 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行

MULTI,EXEC,DISCARD,WATCH 四个命令是 redis 事务的四个基础命令。其中:

MULTI,告诉 redis 服务器开启一个事务。注意,只是开启,而不是执行

EXEC,告诉 redis 开始执行事务

DISCARD,告诉 redis 取消事务

WATCH,监视某一个键值对,它的作用是在事务执行之前如果监视的键值被修改,事务会被取消。

1.String类型的应用场景

String是最常用的一种数据类型,普通的key/value存储.

2.list类型的应用场景

比较适用于列表式存储且顺序相对比较固定,例如:

省份、城市列表

品牌、厂商、车系、车型等列表

拆车坊专题列表…

3.set类型的应用场景

Set对外提供的功能与list类似,当需要存储一个列表数据,又不希望出现重复数据时,可选用set

4.zset(sorted set)类型的应用场景

zset的使用场景与set类似,区别是set不是自动有序的,而zset可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序.当你需要一个有序的并且不重复的集合列表,那么可以选择zset数据结构。例如:

根据PV排序的热门车系车型列表

根据时间排序的新闻列表

5.hash类型的应用场景

类似于表记录的存储

页面视图所需数据的存储

8Dubbo超时重试;Dubbo超时时间设置

Dubbo超时重试机制:

1、请求服务超时,但是最终程序执行了3次,对于提交订单的业务,只能是新增一个订单,这样是不可以的.

2、dubbo:provider 可以设置超时时间 timout,以及如果超时允许被重连的次数 retries.

3、dubbo:reference  可以设置超时时间,以及如果超时 timout,允许重连服务的次数 retries;如果服务方有设置retries,消费方可以不设置该参数.

4、dubbo:reference retries 的默认值和consumer一样,而consumer默认为2次

.超时设置

DUBBO消费端设置超时时间需要根据业务实际情况来设定,

如果设置的时间太短,一些复杂业务需要很长时间完成,导致在设定的超时时间内无法完成正常的业务处理。

这样消费端达到超时时间,那么dubbo会进行重试机制,不合理的重试在一些特殊的业务场景下可能会引发很多问题,需要合理设置接口超时时间。

比如发送邮件,可能就会发出多份重复邮件,执行注册请求时,就会插入多条重复的注册数据。

   (1)合理配置超时和重连的思路

    1.对于核心的服务中心,去除dubbo超时重试机制,并重新评估设置超时时间。

    2.业务处理代码必须放在服务端,客户端只做参数验证和服务调用,不涉及业务流程处理

Dubbo消费端

全局超时配置

<dubbo:consumer timeout="5000" />

指定接口以及特定方法超时配置

<dubbo:reference interface="com.foo.BarService" timeout="2000"> <dubbo:method name="sayHello" timeout="3000" /> </dubbo:reference>

Dubbo服务端

全局超时配置

<dubbo:provider timeout="5000" />

指定接口以及特定方法超时配置

<dubbo:provider interface="com.foo.BarService" timeout="2000"> <dubbo:method name="sayHello" timeout="3000" /> </dubbo:provider>

9如何保障请求执行顺序

保证线程的执行顺序使用join()方法可以解决,

保证请求得执行顺序最好使用消息中间件来解决例如:activeMQ、rabbitMQ(天生抗并发)、kafka

10分布式事务与分布式锁

目前比较多的解决方案有几个:

一、结合MQ消息中间件实现的可靠消息最终一致性

二、TCC补偿性事务解决方案

三、最大努力通知型方案

第一种方案:可靠消息最终一致性,需要业务系统结合MQ消息中间件实现,在实现过程中需要保证消息的成功发送及成功消费。即需要通过业务系统控制MQ的消息状态

第二种方案:TCC补偿性,分为三个阶段TRYING-CONFIRMING-CANCELING。每个阶段做不同的处理。

TRYING阶段主要是对业务系统进行检测及资源预留

CONFIRMING阶段是做业务提交,通过TRYING阶段执行成功后,再执行该阶段。默认如果TRYING阶段执行成功,CONFIRMING就一定能成功。

CANCELING阶段是回对业务做回滚,在TRYING阶段中,如果存在分支事务TRYING失败,则需要调用CANCELING将已预留的资源进行释放。

第三种方案:最大努力通知xing型,这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现,例如:通过MQ发送http请求,设置最大通知次数。达到通知次数后即不再通知

分布式锁实现:

完全分布式锁是全局同步的,这意味着在任何时刻没有两个客户端会同时认为它们都拥有相同的锁,使用 Zookeeper 可以实现分布式锁,需要首先定义一个锁节点(lock root node)。

需要获得锁的客户端按照以下步骤来获取锁:

  1. 保证锁节点(lock root node)这个父根节点的存在,这个节点是每个要获取lock客户端共用的,这个节点是PERSISTENT的。

2.  第一次需要创建本客户端要获取lock的节点,调用 create( ),并设置节点为EPHEMERAL_SEQUENTIAL类型,表示该节点         为临时的和顺序的。如果获取锁的节点挂掉,则该节点自动失效,可以让其他节点获取锁。

3.   在父锁节点(lockroot node)上调用 getChildren( ) ,不需要设置监视标志。 (为了避免“羊群效应”).

4.    按照Fair竞争的原则,将步骤3中的子节点(要获取锁的节点)按照节点顺序的大小做排序,取出编号最小的一个节点做为lock的owner,判断自己的节点id

     是否就为owner id,如果是则返回,lock成功。如果不是则调用 exists( )监听比自己小的前一位的id,关注它锁释放的操作(也就是exist watch)。  

5.   如果第4步监听exist的watch被触发,则继续按4中的原则判断自己是否能获取到lock。 

6.   释放锁:需要释放锁的客户端只需要删除在第2步中创建的节点即可。

注意事项:

一个节点的删除只会导致一个客户端被唤醒,因为每个节点只被一个客户端watch,这避免了“羊群效应”。

11分布式session设置

12执行某操作,前50次成功,第51次失败a全部回滚b前50此提交第51此抛异常,ab场景分别如何设置Spring

        <!-- 配置事务属性 -->  
        
<property name="transactionAttributes">  
            
<props>  
                
<prop key="*">PROPAGATION_REQUIRED</prop>
            </props>  
        
</property>  

        <!-- 配置事务属性 -->  
        
<property name="transactionAttributes">  
            
<props>  
                
<prop key="*">PROPAGATION_ NESTED</prop>
            </props>  
        
</property> 

13Zookeeper有哪些作用

Zookeeper主要可以干哪些事情:配置管理,名字服务,提供分布式同步以及集群管理。那这些服务又到底是什么呢?我们为什么需要这样的服务?我们又为什么要使用Zookeeper来实现呢,使用Zookeeper有什么优势?接下来我会挨个介绍这些到底是什么,以及有哪些开源系统中使用了。

 

配置管理

在我们的应用中除了代码外,还有一些就是各种配置。比如数据库连接等。一般我们都是使用配置文件的方式,在代码中引入这些配置文件。但是当我们只有一种配置,只有一台服务器,并且不经常修改的时候,使用配置文件是一个很好的做法,但是如果我们配置非常多,有很多服务器都需要这个配置,而且还可能是动态的话使用配置文件就不是个好主意了。这个时候往往需要寻找一种集中管理配置的方法,我们在这个集中的地方修改了配置,所有对这个配置感兴趣的都可以获得变更。比如我们可以把配置放在数据库里,然后所有需要配置的服务都去这个数据库读取配置。但是,因为很多服务的正常运行都非常依赖这个配置,所以需要这个集中提供配置服务的服务具备很高的可靠性。一般我们可以用一个集群来提供这个配置服务,但是用集群提升可靠性,那如何保证配置在集群中的一致性呢? 这个时候就需要使用一种实现了一致性协议的服务了。Zookeeper就是这种服务,它使用Zab这种一致性协议来提供一致性。现在有很多开源项目使用Zookeeper来维护配置,比如在HBase中,客户端就是连接一个Zookeeper,获得必要的HBase集群的配置信息,然后才可以进一步操作。还有在开源的消息队列Kafka中,也使用Zookeeper来维护broker的信息。在Alibaba开源的SOA框架Dubbo中也广泛的使用Zookeeper管理一些配置来实现服务治理。

 

名字服务

名字服务这个就很好理解了。比如为了通过网络访问一个系统,我们得知道对方的IP地址,但是IP地址对人非常不友好,这个时候我们就需要使用域名来访问。但是计算机是不能是别域名的。怎么办呢?如果我们每台机器里都备有一份域名到IP地址的映射,这个倒是能解决一部分问题,但是如果域名对应的IP发生变化了又该怎么办呢?于是我们有了DNS这个东西。我们只需要访问一个大家熟知的(known)的点,它就会告诉你这个域名对应的IP是什么。在我们的应用中也会存在很多这类问题,特别是在我们的服务特别多的时候,如果我们在本地保存服务的地址的时候将非常不方便,但是如果我们只需要访问一个大家都熟知的访问点,这里提供统一的入口,那么维护起来将方便得多了。

 

分布式锁

其实在第一篇文章中已经介绍了Zookeeper是一个分布式协调服务。这样我们就可以利用Zookeeper来协调多个分布式进程之间的活动。比如在一个分布式环境中,为了提高可靠性,我们的集群的每台服务器上都部署着同样的服务。但是,一件事情如果集群中的每个服务器都进行的话,那相互之间就要协调,编程起来将非常复杂。而如果我们只让一个服务进行操作,那又存在单点。通常还有一种做法就是使用分布式锁,在某个时刻只让一个服务去干活,当这台服务出问题的时候锁释放,立即fail over到另外的服务。这在很多分布式系统中都是这么做,这种设计有一个更好听的名字叫Leader Election(leader选举)。比如HBase的Master就是采用这种机制。但要注意的是分布式锁跟同一个进程的锁还是有区别的,所以使用的时候要比同一个进程里的锁更谨慎的使用。

 

集群管理

在分布式的集群中,经常会由于各种原因,比如硬件故障,软件故障,网络问题,有些节点会进进出出。有新的节点加入进来,也有老的节点退出集群。这个时候,集群中其他机器需要感知到这种变化,然后根据这种变化做出对应的决策。比如我们是一个分布式存储系统,有一个中央控制节点负责存储的分配,当有新的存储进来的时候我们要根据现在集群目前的状态来分配存储节点。这个时候我们就需要动态感知到集群目前的状态。还有,比如一个分布式的SOA架构中,服务是一个集群提供的,当消费者访问某个服务时,就需要采用某种机制发现现在有哪些节点可以提供该服务(这也称之为服务发现,比如Alibaba开源的SOA框架Dubbo就采用了Zookeeper作为服务发现的底层机制)。还有开源的Kafka队列就采用了Zookeeper作为Cosnumer的上下线管理。

 

14JVM内存模型
https://i-blog.csdnimg.cn/blog_migrate/2d06c68d8eafc25f0515ff84933b3188.png

15数据库垂直和水平拆分

        一个数据库由很多表的构成,每个表对应着不同的业务,垂直切分是指按照业务将表进行分类,分布到不同的数据库上面,这样也就将数据或者说压力分担到不同的库上面,如下图:

https://i-blog.csdnimg.cn/blog_migrate/c2a43e7bcb2f46e6f5225a5ebb1c3962.png

优点:

        1. 拆分后业务清晰,拆分规则明确。

        2. 系统之间整合或扩展容易。

        3. 数据维护简单。

缺点:

        1. 部分业务表无法join,只能通过接口方式解决,提高了系统复杂度。

        2. 受每种业务不同的限制存在单库性能瓶颈,不易数据扩展跟性能提高。

        3. 事务处理复杂。

水平拆分

        垂直拆分后遇到单机瓶颈,可以使用水平拆分。相对于垂直拆分的区别是:垂直拆分是把不同的表拆到不同的数据库中,而水平拆分是把同一个表拆到不同的数据库中。

        相对于垂直拆分,水平拆分不是将表的数据做分类,而是按照某个字段的某种规则来分散到多个库之中,每个表中包含一部分数据。简单来说,我们可以将数据的水平切分理解为是按照数据行的切分,就是将表中 的某些行切分到一个数据库,而另外的某些行又切分到其他的数据库中,主要有分表,分库两种模式,如图:

https://i-blog.csdnimg.cn/blog_migrate/d1b45133d0b48115a8d86fa0b2a7c37d.png

https://i-blog.csdnimg.cn/blog_migrate/dae216f2743ce184d7c1ac705c2ba304.png

优点:

        1. 不存在单库大数据,高并发的性能瓶颈。

        2. 对应用透明,应用端改造较少。     

        3. 按照合理拆分规则拆分,join操作基本避免跨库。

        4. 提高了系统的稳定性跟负载能力。

 

缺点:

        1. 拆分规则难以抽象。

        2. 分片事务一致性难以解决。

        3. 数据多次扩展难度跟维护量极大。

        4. 跨库join性能较差。

16MyBatis如何分页;如何设置缓存;MySQL分页

MyBatis分页:

1使用Map来进行包装数据实现分页功能:

        1),在SQL语句映射的ResultType返回的是你要查询得到的实体类

        2),穿进去的参数parameterType是你自己包装的Map类型

        3),首先你传进来的参数要和SQL语句中的字段名要保持一致

        4),在实体DAO层还需要把查询数据的起始下标,和查询多少条数据都put进Map中。

2使用RowBounds来实现分页:

        1),只需要设置一个返回值为User实体类型

        2),RowBounds rowBounds= newRowBounds((currentPage-1)*pageSize,pageSize);

        3),就是上一步多了一个创建一个RowBounds对象,然后需要传入SQL语句中需要的参数就行了。

        4),然后sqlSession在执行selectList的时候把那个rowBounds对象直接传进去就可以了。

MyBatis一级缓存和二级缓存:

       MyBatis 默认开启了一级缓存,一级缓存是在SqlSession 层面进行缓存的。即,同一个SqlSession ,多次调用同一个Mapper和同一个方法的同一个参数,只会进行一次数据库查询,然后把数据缓存到缓冲中,以后直接先从缓存中取出数据,不会直接去查数据库。

​ 但是不同的SqlSession对象,因为不用的SqlSession都是相互隔离的,所以相同的Mapper、参数和方法,他还是会再次发送到SQL到数据库去执行,返回结果。

       默认二级缓存是不开启的,需要手动进行配置。

例如:<cache/>

​ 如果这样配置的话,很多其他的配置就会被默认进行,如:

映射文件所有的select 语句会被缓存。

映射文件的所有的insert、update和delete语句会刷新缓存。

缓存会使用默认的Least Recently Used(LRU,最近最少使用原则)的算法来回收缓存空间。

根据时间表,比如No Flush Interval,(CNFI,没有刷新间隔),缓存不会以任何时间顺序来刷新。

缓存会存储列表集合或对象(无论查询方法返回什么)的1024个引用。

缓存会被视为是read/write(可读/可写)的缓存,意味着对象检索不是共享的,而且可以很安全的被调用者修改,不干扰其他调用者或县城所作的潜在修改。

可以在开启二级缓存时候,手动配置一些属性:

例如:<cache eviction="LRU" flushInterval="100000" size="1024" readOnly="true"/>

各个属性意义如下:

eviction:缓存回收策略

- LRU:最少使用原则,移除最长时间不使用的对象

- FIFO:先进先出原则,按照对象进入缓存顺序进行回收

- SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象

- WEAK:弱引用,更积极的移除移除基于垃圾回收器状态和弱引用规则的对象

flushInterval:刷新时间间隔,单位为毫秒,这里配置的100毫秒。如果不配置,那么只有在进行数据库修改操作才会被动刷新缓存区

size:引用额数目,代表缓存最多可以存储的对象个数

readOnly:是否只读,如果为true,则所有相同的sql语句返回的是同一个对象(有助于提高性能,但并发操作同一条数据时,可能不安全),如果设置为false,则相同的sql,后面访问的是cache的clone副本。

可以在Mapper的具体方法下设置对二级缓存的访问意愿:

useCache配置

​ 如果一条语句每次都需要最新的数据,就意味着每次都需要从数据库中查询数据,可以把这个属性设置为false,如:

<select id="selectAll" resultMap="BaseResultMap" useCache="false">

刷新缓存(就是清空缓存)

​ 二级缓存默认会在insert、update、delete操作后刷新缓存,可以手动配置不更新缓存,如下:

<update id="updateById" parameterType="User" flushCache="false" />

MySQL实现分页查询:

limit 基本实现方式:一般情况下,客户端通过传递 pageNo(页码)、pageSize(每页条数)两个参数去分页查询数据库中的数据,在数据量较小(元组百/千级)时使用 MySQL自带的 limit 来解决这个问题:

收到客户端{pageNo:1,pagesize:10}

select * from table limit (pageNo-1)*pageSize, pageSize;

收到客户端{pageNo:5,pageSize:30}

select * from table limit (pageNo-1)*pageSize,pageSize;

建立主键或者唯一索引:在数据量较小的时候简单的使用 limit 进行数据分页在性能上面不会有明显的缓慢,但是数据量达到了 万级到百万级 sql语句的性能将会影响数据的返回。这时需要利用主键或者唯一索引进行数据分页;

假设主键或者唯一索引为 good_id

收到客户端{pageNo:5,pagesize:10}

select * from table where good_id > (pageNo-1)*pageSize limit pageSize;

–返回good_id为40到50之间的数据

基于数据再排序:当需要返回的信息为顺序或者倒序时,对上面的语句基于数据再排序。order by ASC/DESC 顺序或倒序 默认为顺序;

select * from table where good_id > (pageNo-1)*pageSize order by good_id limit pageSize;

–返回good_id为40到50之间的数据,数据依据good_id顺序排列

17熟悉IO么?与NIO的区别,阻塞与非阻塞的区别

IO

NIO

面向流

面向缓冲

阻塞IO

非阻塞IO

 

选择器

阻塞和非阻塞:

  Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

18分布式session一致性

1.session复制(同步)

思路:多个web-server之间相互同步session,这样每个web-server之间都包含全部的session

优点:web-server支持的功能,应用程序不需要修改代码

不足:

session的同步需要数据传输,占内网带宽,有时延

所有web-server都包含所有session数据,数据量受内存限制,无法水平扩展

有更多web-server时要歇菜

2.客户端存储法

思路:服务端存储所有用户的session,内存占用较大,可以将session存储到浏览器cookie中,每个端只要存储一个用户的数据了

优点:服务端不需要存储

缺点:

每次http请求都携带session,占外网带宽

数据存储在端上,并在网络传输,存在泄漏、篡改、窃取等安全隐患

session存储的数据大小受cookie限制

“端存储”的方案虽然不常用,但确实是一种思路。

3.反向代理hash一致性

思路:web-server为了保证高可用,有多台冗余,反向代理层能不能做一些事情,让同一个用户的请求保证落在一台web-server上呢?

方案一:四层代理hash

反向代理层使用用户ip来做hash,以保证同一个ip的请求落在同一个web-server上

方案二:七层代理hash

反向代理使用http协议中的某些业务属性来做hash,例如sid,city_id,user_id等,能够更加灵活的实施hash策略,以保证同一个浏览器用户的请求落在同一个web-server上

优点:

只需要改nginx配置,不需要修改应用代码

负载均衡,只要hash属性是均匀的,多台web-server的负载是均衡的

可以支持web-server水平扩展(session同步法是不行的,受内存限制)

不足:

如果web-server重启,一部分session会丢失,产生业务影响,例如部分用户重新登录

如果web-server水平扩展,rehash后session重新分布,也会有一部分用户路由不到正确的session

session一般是有有效期的,所有不足中的两点,可以认为等同于部分session失效,一般问题不大。

对于四层hash还是七层hash,个人推荐前者:让专业的软件做专业的事情,反向代理就负责转发,尽量不要引入应用层业务属性,除非不得不这么做(例如,有时候多机房多活需要按照业务属性路由到不同机房的web-server)。

19分布式接口的幂等性设计

为了解决重复请求问题,就需要保证接口的幂等性,接口的幂等性实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。有些接口可以天然的实现幂等性,比如查询接口,对于查询来说,你查询一次和两次,对于系统来说,没有任何影响,查出的结果也是一样。

全局唯一ID

如果使用全局唯一ID,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID,存储到存储系统中,比如数据库、redis等。如果存在则表示该方法已经执行。

从工程的角度来说,使用全局ID做幂等可以作为一个业务的基础的微服务存在,在很多的微服务中都会用到这样的服务,在每个微服务中都完成这样的功能,会存在工作量重复。另外打造一个高可靠的幂等服务还需要考虑很多问题,比如一台机器虽然把全局ID先写入了存储,但是在写入之后挂了,这就需要引入全局ID的超时机制。

使用全局唯一ID是一个通用方案,可以支持插入、更新、删除业务操作。但是这个方案看起来很美但是实现起来比较麻烦,下面的方案适用于特定的场景,但是实现起来比较简单。

去重表

这种方法适用于在业务中有唯一标的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据和写入去去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。

插入或更新

这种方法插入并且有唯一索引的情况,比如我们要关联商品品类,其中商品的ID和品类的ID可以构成唯一索引,并且在数据表中也增加了唯一索引。这时就可以使用InsertOrUpdate操作。

多版本控制

这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等

状态机控制

这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等。

20JVM老年代和新生代的比例?

默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。

http://images.cnitblog.com/blog/587773/201409/061921034534396.png

21YGC和FGC发生的具体场景?

1.YGC和FGC是什么

   YGC :对新生代堆进行gc。频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收。性能耗费较小。

   FGC :全堆范围的gc。默认堆空间使用到达80%(可调整)的时候会触发fgc。以我们生产环境为例,一般比较少会触发fgc,有时10天或一周左右会有一次。

2.什么时候执行YGC和FGC

   a.edn空间不足,执行 young gc

   b.old空间不足,perm空间不足,调用方法System.gc() ,ygc时的悲观策略, dump live的内存信息时(jmap –dump:live),都会执行full gc

22jstack,jmap,jutil分别的意义?如何线上排查JVM的相关问题?

jstack能得到运行java程序的java stack和native stack的信息。可以轻松得知当前线程的运行情况。

jmap得到运行java程序的内存分配的详细情况。例如实例个数,大小等。

常见问题定位过程

频繁GC问题或内存溢出问题

一、使用jps查看线程ID

二、使用jstat -gc 3331 250 20 查看gc情况,一般比较关注PERM区的情况,查看GC的增长情况。

三、使用jstat -gccause:额外输出上次GC原因

四、使用jmap -dump:format=b,file=heapDump 3331生成堆转储文件

五、使用jhat或者可视化工具(Eclipse Memory Analyzer 、IBM HeapAnalyzer)分析堆情况。

六、结合代码解决内存溢出或泄露问题。

死锁问题

一、使用jps查看线程ID

二、使用jstack 3331:查看线程情况

23线程池的构造类的方法的5个参数的具体意义?

corePoolSize

在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,(除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程)。

默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。

maxPoolSize

当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。

keepAliveTime

当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。

allowCoreThreadTimeout

是否允许核心线程空闲退出,默认值为false。

queueCapacity

任务队列容量。从maxPoolSize的描述上可以看出,任务队列的容量会影响到线程的变化,因此任务队列的长度也需要恰当的设置。

24单机上一个线程池正在处理服务如果忽然断电怎么办(正在处理和阻塞队列里的请求怎么处理)?

使用activemq,持久性模式: 服务器断电(关闭)后,使用持久性模式时,没有被消费的消息会继续消费;

25使用无界阻塞队列会出现什么问题?

内存耗尽。

26接口如何处理重复请求?

在处理请求前,前面来个队列,根据你当前请求的参数生产一个Hash值,存入队列,每个请求加入队列前,判断是否已存在该请求。

27如果保证共享变量修改时的原子性?

使用Synchronized加锁,对操作共享变量的过程进行加锁,Synchronized是通过对线程加锁(独占锁)控制线程同步,被Synchronized修饰的内存只允许一个线程访问。

28设计一个对外服务的接口实现类,在1,2,3这三个主机(对应不同IP)上实现负载均衡和顺序轮询机制(考虑并发)

/**

 * 負載均衡算法,輪詢法

 * @author guoy

 *

 */

public class TestRoundRobin {

 

      

       static Map<String,Integer> serverWeigthMap  = new HashMap<String,Integer>();

 

        static{

              serverWeigthMap.put("192.168.1.12", 1);

              serverWeigthMap.put("192.168.1.13", 1);

              serverWeigthMap.put("192.168.1.14", 2);

              serverWeigthMap.put("192.168.1.15", 2);

              serverWeigthMap.put("192.168.1.16", 3);

              serverWeigthMap.put("192.168.1.17", 3);

              serverWeigthMap.put("192.168.1.18", 1);

              serverWeigthMap.put("192.168.1.19", 2);

       }

        Integer  pos = 0;

        public  String roundRobin()

              {

                     //重新建立一個map,避免出現由於服務器上線和下線導致的並發問題

                     Map<String,Integer> serverMap  = new HashMap<String,Integer>();

                     serverMap.putAll(serverWeigthMap);

                     //獲取ip列表list

                     Set<String> keySet = serverMap.keySet();

                     ArrayList<String> keyList = new ArrayList<String>();

                     keyList.addAll(keySet);

                    

                     String server = null;

                    

                     synchronized (pos) {

                            if(pos >=keySet.size()){

                                   pos = 0;

                            }

                            server = keyList.get(pos);

                            pos ++;

                     }

                     return server;

              }

             

              public static void main(String[] args) {

                     TestRoundRobin robin = new TestRoundRobin();

                     for (int i = 0; i < 20; i++) {

                            String serverIp = robin.roundRobin();

                            System.out.println(serverIp);

                     }

              }

}

29Dubbo的底层原理

分布式SOA架构涉及到了dubbo,它有2部分,服务的提供方和服务的消费方,官方推荐用zookeeper作为一个注册中心,具体怎么用呢?首先服务的提供方暴露出他所提供的服务接口,提供给zookeeper注册中心进行注册进行管理,当消费方需要使用的时候,它会到zookeeper中查询相应的服务接口是否存在,如果找到了,那么zookeeper注册中心把服务消息的提供方的具体IP地址返回给服务的消费方,然后由你的服务的消费方直接去服务的提供方上去使用,原理就是这么个过程。但是dubbo是存在问题的,

1事务和异常问题,举个例子,假设它调用了方法A,方法A查询了数据库,他的返回值被方法B使用了,那么方法B又查询了一次数据库,假设我的数据库初始化字段是0,那么方法A中进行加1的处理,然后又调用了方法B,方法B又进行了加2的处理,又回到方法A,又进行了加1的处理,假设都没有异常的话,最终会变成4,那如果进行方法B的时候出现了异常,那异常怎么办?怎么管理?就一直抛出异常。这就是事务和异常的相关管理,事务的管理不明确。是外层管理理论还是内层管理需要定义传播行为。

2代码的耦合性问题,一旦那边的方法接口改变了,这边就失败了,你就必须重新修改你的端口,修改你调用的方法。

3效率问题,比如一个电商平台,商品上架的时候,同时用到了2个技术,保存在solr索引库中和页面静态化。那么此时2个是同步操作, 如果还有其他的操作,它也会一个个执行,先进行solr保存啦,然后freemarker静态化啦、这个时候用的时间很多,这个时候用到消息服务中间件,JMS也就是一个消息服务的消息列,我们还要使用activeMQ技术之类,效率不高。

30ZooKeeper是什么

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

31concurrentMap的机制

1、synchronized关键字加锁的原理,其实是对对象加锁,不论你是在方法前加synchronized还是语句块前加,锁住的都是对象整体,但是ConcurrentHashMap的同步机制和这个不同,它不是加synchronized关键字,而是基于lock操作的,这样的目的是保证同步的时候,锁住的不是整个对象。一个ConcurrentHashMap由多个segment组成,每个segment包含一个Entity的数组。这里比HashMap多了一个segment类。该类继承了ReentrantLock类,所以本身是一个锁。当多线程对ConcurrentHashMap操作时,不是完全锁住map,而是锁住相应的segment。这样提高了并发效率。

2、不可以有null键。

3、缺点:当遍历ConcurrentMap中的元素时,需要获取所有的segment 的锁,使用遍历时慢。锁的增多,占用了系统的资源。使得对整个集合进行操作的一些方法(例如 size() 或 isEmpty() )的实现更加困难,因为这些方法要求一次获得许多的锁,并且还存在返回不正确的结果的风险。

32TreeMap

TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。

TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。

TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。

TreeMap 实现了Cloneable接口,意味着它能被克隆。

TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。

 

TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。

TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。

另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。

33Volatile关键字

一、写在前面

前段时间把几年前带过的一个项目架构演进的过程整理了一个系列出来,参见(《亿级流量架构系列专栏总结》)。

不过很多同学看了之后,后台反馈说文章太烧脑,看的云里雾里。其实这个也正常,文章承载的信息毕竟有限,而架构的东西细节太多,想要仅仅通过文章看懂一个系统架构的设计和落地,确实难度不小。

所以接下来用大白话跟大家聊点轻松的话题,比较易于理解,而且对大家工作和面试都很有帮助。

 

二、场景引入,问题初现

很多同学出去面试,都会被问到一个常见的问题:说说你对volatile的理解?

不少初出茅庐的同学可能会有点措手不及,因为可能就是之前没关注过这个。但是网上百度一下呢,不少文章写的很好,但是理论扎的太深,文字太多,图太少,让人有点难以理解。

基于上述痛点,这篇文章尝试站在年轻同学的角度,用最简单的大白话,加上多张图给大家说一下,volatile到底是什么?

当然本文不会把理论扎的太深,因为一下子扎深了文字太多,很多同学还是会不好理解。

本文仅仅是定位在用大白话的语言将volatile这个东西解释清楚,而涉及到特别底层的一些原理和技术问题,以后有机会开文再写。

首先,给大家上一张图,咱们来一起看看:

 

如上图,这张图说的是java内存模型中,每个线程有自己的工作内存,同时还有一个共享的主内存。

举个例子,比如说有两个线程,他们的代码里都需要读取data这个变量的值,那么他们都会从主内存里加载data变量的值到自己的工作内存,然后才可以使用那个值。

好了,现在大家从图里看到,每个线程都把data这个变量的副本加载到了自己的工作内存里了,所以每个线程都可以读到data = 0这个值。

这样,在线程代码运行的过程中,对data的值都可以直接从工作内存里加载了,不需要再从主内存里加载了。

那问题来了,为啥一定要让每个线程用一个工作内存来存放变量的副本以供读取呢?我直接让线程每次都从主内存加载变量的值不行吗?

很简单!因为线程运行的代码对应的是一些指令,是由CPU执行的!但是CPU每次执行指令运算的时候,也就是执行我们写的那一大坨代码的时候,要是每次需要一个变量的值,都从主内存加载,性能会比较差!

所以说后来想了一个办法,就是线程有工作内存的概念,类似于一个高速的本地缓存。

这样一来,线程的代码在执行过程中,就可以直接从自己本地缓存里加载变量副本,不需要从主内存加载变量值,性能可以提升很多!

但是大家思考一下,这样会有什么问题?

我们来设想一下,假如说线程1修改了data变量的值为1,然后将这个修改写入自己的本地工作内存。那么此时,线程1的工作内存里的data值为1。

然而,主内存里的data值还是为0!线程2的工作内存里的data值还是0啊?!

 

这可尴尬了,那接下来,在线程1的代码运行过程中,他可以直接读到data最新的值是1,但是线程2的代码运行过程中读到的data的值还是0!

这就导致,线程1和线程2其实都是在操作一个变量data,但是线程1修改了data变量的值之后,线程2是看不到的,一直都是看到自己本地工作内存中的一个旧的副本的值!

这就是所谓的java并发编程中的可见性问题

多个线程并发读写一个共享变量的时候,有可能某个线程修改了变量的值,但是其他线程看不到!也就是对其他线程不可见!

三、volatile的作用及背后的原理

那如果要解决这个问题怎么办呢?这时就轮到volatile闪亮登场了!你只要给data这个变量在定义的时候加一个volatile,就直接可以完美的解决这个可见性的问题。

比如下面的这样的代码,在加了volatile之后,会有啥作用呢?

 

完整的作用就不给大家解释了,因为我们定位就是大白话,要是把底层涉及的各种内存屏障、指令重排等概念在这里带出来,不少同学又要蒙圈了!

我们这里,就说说他最关键的几个作用是啥?

1

第一,一旦data变量定义的时候前面加了volatile来修饰的话,那么线程1只要修改data变量的值,就会在修改完自己本地工作内存的data变量值之后,强制将这个data变量最新的值刷回主内存,必须让主内存里的data变量值立马变成最新的值!

整个过程,如下图所示:

2

第二,如果此时别的线程的工作内存中有这个data变量的本地缓存,也就是一个变量副本的话,那么会强制让其他线程的工作内存中的data变量缓存直接失效过期,不允许再次读取和使用了!

 

整个过程,如下图所示:

3

第三,如果线程2在代码运行过程中再次需要读取data变量的值,此时尝试从本地工作内存中读取,就会发现这个data = 0已经过期了!

此时,他就必须重新从主内存中加载data变量最新的值!那么不就可以读取到data = 1这个最新的值了!整个过程,参见下图:

 

 

bingo!好了,volatile完美解决了java并发中可见性的问题!

对一个变量加了volatile关键字修饰之后,只要一个线程修改了这个变量的值,立马强制刷回主内存。

接着强制过期其他线程的本地工作内存中的缓存,最后其他线程读取变量值的时候,强制重新从主内存来加载最新的值!

这样就保证,任何一个线程修改了变量值,其他线程立马就可以看见了!这就是所谓的volatile保证了可见性的工作原理!

 

 

四、总结 & 提醒

最后给大家提一嘴,volatile主要作用是保证可见性以及有序性。

有序性涉及到较为复杂的指令重排、内存屏障等概念,本文没提及,但是volatile是不能保证原子性的

也就是说,volatile主要解决的是一个线程修改变量值之后,其他线程立马可以读到最新的值,是解决这个问题的,也就是可见性!

但是如果是多个线程同时修改一个变量的值,那还是可能出现多线程并发的安全问题,导致数据值修改错乱,volatile是不负责解决这个问题的,也就是不负责解决原子性问题!

原子性问题,得依赖synchronized、ReentrantLock等加锁机制来解决。

34快速排序

快速排序的原理:选择一个关键值作为基准值。比基准值小的都在左边序列(一般是无序的),比基准值大的都在右边(一般是无序的)。一般选择序列的第一个元素。

一次循环:从后往前比较,用基准值和最后一个值比较,如果比基准值小的交换位置,如果没有继续比较下一个,直到找到第一个比基准值小的值才交换。找到这个值之后,又从前往后开始比较,如果有比基准值大的,交换位置,如果没有继续比较下一个,直到找到第一个比基准值大的值才交换。直到从前往后的比较索引>从后往前比较的索引,结束第一次循环,此时,对于基准值来说,左右两边就是有序的了。

接着分别比较左右两边的序列,重复上述的循环。

35广度优先搜索(队列实现)

1.选定一个起始节点;

2.以选定节点为中心,所有与该节点相邻节点为备选节点(其中,在之前已经访问过的节点不得再纳入相邻节点),并将这些备选节点放入一个先进先出队列中,;

3.依次取出先进先出队列中的节点,并求得该节点的相邻节点放入先进先出队列中;

4.循环进行23步骤;知道先进先出队列为空(搜索结束的标志);

代码示例:

import java.util.LinkedList;

public class Main {

    /*****重要组成-方向******/

    int[][] direct={{0,1},{0,-1},{-1,0},{1,0}};//四个方向,上下左右

    /******输入数组*****/

    int[][] array={

            {0,0,0,0},

            {0,0,1,0},

            {0,0,1,0},

            {0,0,1,0}

           };

    public static void main(String[] args) throws InterruptedException {

        new Main().BFS();

    }

    /*****重要组成-封装数组点,用坐标表示位置******/

     class Node{

         int row;

         int column;

         int round;

         Node pre;

         Node(int row,int column,int round,Node pre) {

            this.row=row;

            this.column=column;

            this.round=round;

            this.pre=pre;

        }

    }

    public void BFS(){//广度搜索算法

        Node start=new Node(0,0,0,null);

        /*****重要组成-待搜索队列的每个对象都是接下来要所搜的值******/

        LinkedList<Node> queue=new LinkedList<>();//待搜索队列

        queue.offer(start);

        /*****重要组成-持续搜索的标志。待搜索队列里有东西******/

        while(!queue.isEmpty()){

            Node temp=queue.poll();

            for(int i=0;i<4;i++){//尝试搜索四个方向的点,如果满足就加入待搜索队列中

                int new_row=temp.row+direct[i][0];

                int new_column=temp.column+direct[i][1];

                if(new_row<0||new_column<0||new_row>=4||new_column>=4)

                    continue;//该方向上出界,考虑下一方向

                if(array[new_row][new_column]==1)continue;

                Node next=new Node(new_row, new_column,temp.round+1,temp);

                if(new_row==3&&new_column==3)//找到了出口

                {

                    queue.clear();

                    queue.offerFirst(next);

                    while(next.pre!=null){

                        queue.offerFirst(next.pre);//以前获取父节点

                        next=next.pre;

                    }

                    for(Node node:queue)

                    {

                        System.out.println("("+node.row+","+node.column+"),");

                    }

                }

                array[new_row][new_column]=1;

                queue.offer(next);

            }

        }

    }

}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值