java 知识点

来源:互联网 发布:青岛特来电公司知乎 编辑:程序博客网 时间:2024/06/05 10:56

1、HashMap的工作原理

HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。HasMap初始长度?HashMap的初始长度默认是16,并且每次自动扩展或者手动初始化时,长度必须是2的幂,之所以选择16,是为了服务于从key映射到index的hash算法。从key映射到HashMap数组对应的位置会用到下面的方式:index = HashCode(key)& (length -1). length 是2的幂,可以有效的减少index的重复。

如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?”除非你真正知道HashMap的工作原理,否则你将回答不出这道题。默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置,你了解重新调整HashMap大小存在什么问题吗?”你可能回答不上来,这时面试官会提醒你当多线程的情况下,可能产生条件竞争(race condition)。当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。主要是多线程同时put时,如果同时触发了rehash操作,会导致HashMap中的链表中出现循环节点,进而使得后面get的时候,会死循环如果条件竞争发生了,那么就死循环了。

2、ConcurrentHashMap实现原理

ConcurrentHashMap的同步是采用分段锁的形式,一个ConcurrentHashMap由多个segment组成,每一个segment都包含了一个HashEntry数组的hashtable, 每一个segment包含了对自己的hashtable的操作,比如get,put,replace等操作,这些操作发生的时候,对自己的hashtable进行锁定。由于每一个segment写操作只锁定自己的hashtable,所以可能存在多个线程同时写的情况,性能无疑好于只有一个hashtable锁定的情况。

3、ThreadLocal 工作原理

ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。它是一个以ThreadLocal对象为键、任意对象为值的存储结构。可以通过set(T)方法设置一个值,在当前线程下以get()方法获取到原先设置的值。


4、volatile关键字

一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。使用volatile关键字会强制将修改的值立即写入主存,使其它线程的工作内存中的这个变量失效,必须再次从主内存中重新读取。volatile保证原子性吗?答案是不能,根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。可以使用synchronized、加锁、和atomicInteger实现原子性。

4.1、wait和sleep

wait是Object类中的方法,而sleep是Thread类中的方法。

最主要的是sleep方法调用之后,并没有释放锁。使得线程仍然可以同步控制。sleep不会让出系统资源;

而wait是进入线程等待池中等待,让出系统资源。

调用wait方法的线程,不会自己唤醒,需要线程调用 notify / notifyAll 方法唤醒等待池中的所有线程,才会进入就绪队列中等待系统分配资源。sleep方法会自动唤醒,如果时间不到,想要唤醒,可以使用interrupt方法强行打断。

使用范围:

sleep可以在任何地方使用。而wait,notify,notifyAll只能在同步控制方法或者同步控制块中使用。

sleep必须捕获异常,而wait,notify,notifyAll的不需要捕获异常



5、为什么String要设计成不可变的?
1)字符串常量池设计的需要;字符串常量池是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。
(2)允许String对象缓存HashCode;java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。
字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.
 (3)安全性;String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,数据库密码,假若String不是固定不变的,将会引起各种安全隐患。


6、数据库的ACID是什么意思?

所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。

原子性是指事务是一个不可再分割的工作单位,事务中的操作要么都发生,要么都不发生。

 一致性是指在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。

多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。

持久性,意味着在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。


7、乐观锁和悲观锁及其适用场景

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。乐观锁解决不了脏读。

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block【阻塞】直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。


8、数据库索引

参考:http://blog.csdn.net/kennyrose/article/details/7532032

索引的实现通常使用B树及其变种B+树

创建索引可以大大提高系统的性能。

第一,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。

第二,可以大大加快数据的检索速度,这也是创建索引的最主要的原因。

缺点:

第一,索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。

第二,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,花的时间比较多。

应该创建索引的列:

(1)、在经常需要搜索的列上,可以加快搜索的速度

(2)、在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度

(3)、在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的

(4)、在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间

(5)、在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。

不应该创建索引的列:

(1)、对于那些在查询中很少使用或者参考的列不应该创建索引

(2)、对于那些只有很少数据值的列也不应该增加索引。例如性别等

(3)、对于那些定义为text, image和bit数据类型的列不应该增加索引


根据数据库的功能,可以在数据库设计器中创建三种索引:唯一索引、主键索引和聚集索引

唯一索引 

唯一索引是不允许其中任何两行具有相同索引值的索引。

主键索引

数据库表经常有一列或列组合,其值唯一标识表中的每一行。该列称为表的主键。
在数据库关系图中为表定义主键将自动创建主键索引,主键索引是唯一索引的特定类型。该索引要求主键中的每个值都唯一。当在查询中使用主键索引时,它还允许对数据的快速访问。

聚集索引

在聚集索引中,表中行的物理顺序与键值的逻辑(索引)顺序相同。一个表只能包含一个聚集索引。


9、事务

1.隔离级别

      Spring 为我们定义了5个隔离级别,在@Transactional 指定isolation,使用,如下表:
DEFAULT     使用当前数据库默认的隔离级别。READ_UNCOMMITD  
可读未提交的                 在一个事务里可以读取到另一个事务未提交的数据,会产生脏读、不可重复读、幻读。READ_COMMITTED
提交了才能读只有当一个事务提交后,才能被另一个事务读取到数据,不会产生脏读,会产生不可重复读、幻读。REPEATABLE_READ
可重复读实现了READ_COMMITTED级别,而且一个事务读取了数据,另一个事务不允许修改。不会产生脏读、不可重复读,会产生幻读。SERIALIZABLE
序列化读写要求事务按照顺序执行,不会产生脏读、不可重复读、幻读,性能极差严重影响性能。

2.事务传播

      Spring 为我们定义了7个传播行为,在@Transactional 指定propagation,如下表:
REQUIRED                                              必须要在事务下执行,如果当前存在事务则加入该事务,否则创建新的事务执行。例如:
A方法调用B方法,A方法已经在事务下了,调用B的时候,B会加入到A的事务中,处于同一事务下,如果出错所有的操作都将回滚。SUPPORTS当前如果存在事务则加入该事务,如果没有就不用事务执行。MANDATORY必须要在事务下执行,如果当前存在事务则加入该事务,如果不存在事务则抛异常。REQUIRES_NEW必须要在一个新的事务下执行,如果当前存在事务,则会创建新的事务执行,如果出现错误不会回滚其它事务。例如:A调用B,B出错不会导致A事务数据回滚。NOT_SUPPORTED不能在事务下执行,如果当前存在事务则把当前事务挂起。NEVER不能在事务下执行,如果当前存在事务则抛异常。NESTED如果当前存在事务,则会创建新的事务执行。具体的这个我也不是怎么清楚,很少用到。




10、Redis底层数据结构有哪些

在 redis 中一共有5种数据结构、String、list、list、set、sortSet,

(1)、String,String 数据结构是简单的 key-value 类型,value 不仅可以是 String,也可以是数字.

(2)、HASH,Redis 的 Hash 结构可以使你像在数据库中 Update 一个属性一样只修改某一项属性值。应用有:存储、读取、修改用户属性

(3)、List 说白了就是链表(redis 使用双端链表实现的 List)。使用 List 结构,我们可以轻松地实现最新消息排行等功能(比如新浪微博的 TimeLine )。List 的另一个应用就是消息队列,可以利用 List 的 *PUSH 操作,将任务存在 List 中,然后工作线程再用 POP 操作将任务取出进行执行。Redis 还提供了操作 List 中某一段元素的 API,你可以直接查询,删除 List 中某一段的元素。

(4)、Set,Set 就是一个集合,集合的概念就是一堆不重复值的组合。利用 Redis 提供的 Set 数据结构,可以存储一些集合性的数据。应用:1.共同好友、二度好友2.利用唯一性,可以统计访问网站的所有独立 IP 3.好友推荐的时候,根据 tag 求交集,大于某个 threshold 就可以推荐。

(5)、和Sets相比,Sorted Sets是将 Set 中的元素增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,应用:带有权重的元素,比如一个游戏的用户得分排行榜

(6) 订阅-发布系统

Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在 Redis 中,你可以设定对某一个 key 值进行消息发布及消息订阅,当一个 key 值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。

(7)、事务——Transactions

Redis 事务可以一次执行多个命令, 并且带有以下两个重要的保证:a、事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。b、事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。


11、线程池实现和原理


到这里,大部分朋友应该对任务提交给线程池之后到被执行的整个过程有了一个基本的了解,下面总结一下:

  1)首先,要清楚corePoolSize和maximumPoolSize的含义;

  2)其次,要知道Worker是用来起到什么作用的;

  3)要知道任务提交给线程池之后的处理策略,这里总结一下主要有4点:

  • 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
  • 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
  • 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;以下4种策略。
  • 1
    2
    3
    4
    ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
    ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
    ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

  • 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

一个线程池包括以下四个基本组成部分:

(1)、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
(2)、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务; 

(3)、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
(4)、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

 线程池技术可以缩短或调整创建线程和销毁线程时间的技术,从而提高服务器程序性能的。它把创建线程和销毁线程分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有创建线程和销毁线程开销了。线程池不仅调整创建线程和销毁线程产生的时间段,而且它还显著减少了创建线程的数目。

线程池工作原理:

将一个任务通过execute()方法加到线程池中后分下面4中情况来执行。

(1)、如果当前的运行的线程数小于核心线程池中的线程数,则创建一个新的线程来执行任务,(执行这一步需要获取全局锁)

(2)、如果运行的线程等于或多于核心线程池中线程数,则将任务加到任务队列中

(3)、如果任务队列已满,则创建新的线程来处理任务,(执行这一步需要获取全局锁)

(4)、如果创建新的线程后,超过线程池中最大线程数,任务被拒绝。


11、消息队列

消息队列技术是分布式应用间交换信息的一种技术。消息队列可驻留在内存或磁盘上,队列存储消息直到它们被应用程序读走。通过消息队列,应用程序可独立地执行--它们不需要知道彼此的位置、或在继续执行前不需要等待接收程序接收此消息。


12、hash一致性算法

参考:http://blog.csdn.net/cywosp/article/details/23397179/

在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了,这样严重的违反了单调性原则。接下来主要讲解一下一致性哈希算法是如何设计的。

环形Hash空间
按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。
把数据通过一定的hash算法处理后映射到环上
现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
    Hash(object1) = key1;
    Hash(object2) = key2;
    Hash(object3) = key3;
    Hash(object4) = key4;
将机器通过hash算法映射到环上
在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。
假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;
在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。
1. 节点(机器)的删除
    以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。
2. 节点(机器)的添加 
    如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持这原有的存储位置。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。
平衡性
根据上面的图解分析,一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,因为还缺少了平衡性。下面将分析一致性哈希算法是如何满足平衡性的。hash算法是不保证平衡的,如上面只部署了NODE1和NODE3的情况(NODE2被删除的图),object1存储到了NODE1中,而object2、object3、object4都存储到了NODE3中,这样就照成了非常不平衡的状态。在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。以上面只部署了NODE1和NODE3的情况(NODE2被删除的图)为例,之前的对象在机器上的分布很不均衡,现在我们以2个副本(复制个数)为例,这样整个hash环中就存在了4个虚拟节点,最后对象映射的关系图如下:根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。
虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2

13、jvm


1、jvm 的体系结构:java堆、java栈、方法区、PC寄存器。

其中java堆和方法区是被所有的线程共享的。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。当创建一个新的线程时,JVM回为这个线程创建一个Java栈,同时也会为这个线程分配一个PC寄存器,这个PC寄存器指向的是这个线程执行的第一行代码。每当调用一个新的方法时,就会在这个java栈上面创建一个新的栈帧。栈帧上面主要存放的是这个方法中定义的变量、运行是常量池的引用、方法正常返回的地址。










原创粉丝点击