目录html
学习Java多久了java
使用Java作过什么东西node
项目中遇到的问题,而后我说了几个,他貌似不感兴趣,而后问了我内存溢出遇到过没有mysql
Servlet的生命周期c++
对Java的集合类了解哪一些,回答了Collection和Map这两个以及他们的子类,扯到hashMap面试
在什么状况下使用过hashMap,对hashmap的底层结构能够说一下吗算法
hashMap是线程安全的吗,回答不是,而后又扯到ConcurrentHashMap,而后问ConcurrentHashMap的细节sql
问数据库,回答学的mysql,问有几种引擎,回答有好几种,主要就两种数据库
Servlet生命周期
加载和实例化Servlet
咱们来看一下Tomcat是如何加载的:
1. 若是已配置自动装入选项,则在启动时自动载入。
2. 在服务器启动时,客户机首次向Servlet发出请求。
3. 从新装入Servlet时。
当启动Servlet容器时,容器首先查找一个配置文件web.xml,这个文件中记录了能够提供服务的Servlet。每一个Servlet被指定一个Servlet名,也就是这个Servlet实际对应的Java的完整class文件名。Servlet容器会为每一个自动装入选项的Servlet建立一个实例。因此,每一个Servlet类必须有一个公共的无参数的构造器。
初始化
当Servlet被实例化后,Servlet容器将调用每一个Servlet的init方法来实例化每一个实例,执行完init方法以后,Servlet处于“已初始化”状态。因此说,一旦Servlet被实例化,那么必将调用init方法。经过Servlet在启动后不当即初始化,而是收到请求后进行。在web.xml文件中用<load-on-statup> ...... </load-on-statup>对Servlet进行预先初始化。
初始化失败后,执行init()方法抛出ServletException异常,Servlet对象将会被垃圾回收器回收,当客户端第一次访问服务器时加载Servlet实现类,建立对象并执行初始化方法。
请求处理
Servlet 被初始化之后,就处于能响应请求的就绪状态。每一个对Servlet 的请求由一个Servlet Request 对象表明。Servlet 给客户端的响应由一个Servlet Response对象表明。对于到达客户机的请求,服务器建立特定于请求的一个“请求”对象和一个“响应”对象。调用service方法,这个方法能够调用其余方法来处理请求。
Service方法会在服务器被访问时调用,Servlet对象的生命周期中service方法可能被屡次调用,因为web-server启动后,服务器中公开的部分资源将处于网络中,当网络中的不一样主机(客户端)并发访问服务器中的同一资源,服务器将开设多个线程处理不一样的请求,多线程同时处理同一对象时,有可能出现数据并发访问的错误。
另外注意,多线程不免同时处理同一变量时(如:对同一文件进行写操做),且有读写操做时,必须考虑是否加上同步,同步添加时,不要添加范围过大,有可能使程序变为纯粹的单线程,大大削弱了系统性能;只须要作到多个线程安全的访问相同的对象就能够了。
卸载Servlet
当服务器再也不须要Servlet实例或从新装入时,会调用destroy方法,使用这个方法,Servlet能够释放掉全部在init方法申请的资源。一个Servlet实例一旦终止,就不容许再次被调用,只能等待被卸载。
Servlet一旦终止,Servlet实例便可被垃圾回收,处于“卸载”状态,若是Servlet容器被关闭,Servlet也会被卸载,一个Servlet实例只能初始化一次,但能够建立多个相同的Servlet实例。如相同的Servlet能够在根据不一样的配置参数链接不一样的数据库时建立多个实例。
两者的定义:
当你在浏览网站的时候,WEB 服务器会先送一小小资料放在你的计算机上,Cookie 会帮你在网站上所打的文字或是一些选择,都纪录下来。当下次你再光临同一个网站,WEB 服务器会先看看有没有它上次留下的 Cookie 资料,有的话,就会依据 Cookie里的内容来判断使用者,送出特定的网页内容给你。 Cookie 的使用很广泛,许多有提供我的化服务的网站,都是利用 Cookie来辨认使用者,以方便送出使用者量身定作的内容,像是 Web 接口的免费 email 网站,都要用到 Cookie。
具体来讲cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案,同时咱们也看到,因为采用服务器端保持状态的方案在客户端也须要保存一个标识,因此session机制可能须要借助于cookie机制来达到保存标识的目的,但实际上它还有其余选择。
cookie机制。正统的cookie分发是经过扩展HTTP协议来实现的,服务器经过在HTTP的响应头中加上一行特殊的指示以提示浏览器按照指示生成相应的cookie。然而纯粹的客户端脚本如JavaScript或者VBScript也能够生成cookie。而cookie的使用是由浏览器按照必定的原则在后台自动发送给服务器的。浏览器检查全部存储的cookie,若是某个cookie所声明的做用范围大于等于将要请求的资源所在的位置,则把该cookie附在请求资源的HTTP请求头上发送给服务器。
cookie的内容主要包括:名字,值,过时时间,路径和域。路径与域一块儿构成cookie的做用范围。若不设置过时时间,则表示这个cookie的生命期为浏览器会话期间,关闭浏览器窗口,cookie就消失。这种生命期为浏览器会话期的cookie被称为会话cookie。
会话cookie通常不存储在硬盘上而是保存在内存里,固然这种行为并非规范规定的。若设置了过时时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直到超过设定的过时时间。存储在硬盘上的cookie能够在不一样的浏览器进程间共享,好比两个IE窗口。而对于保存在内存里的cookie,不一样的浏览器有不一样的处理方式。
session机制是一种服务器端的机制,服务器使用一种相似于散列表的结构(也可能就是使用散列表)来保存信息。 当程序须要为某个客户端的请求建立一个session时,服务器首先检查这个客户端的请求里是否已包含了一个session标识(称为session id),若是已包含则说明之前已经为此客户端建立过session,服务器就按照session id把这个session检索出来使用(检索不到,会新建一个),若是客户端请求不包含session id,则为此客户端建立一个session而且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个session id将被在本次响应中返回给客户端保存。保存这个session id的方式能够采用cookie,这样在交互过程当中浏览器能够自动的按照规则把这个标识发送给服务器。通常这个cookie的名字都是相似于SEEESIONID。但cookie能够被人为的禁止,则必须有其余机制以便在cookie被禁止时仍然可以把session id传递回服务器。
常常被使用的一种技术叫作URL重写,就是把session id直接附加在URL路径的后面。还有一种技术叫作表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时可以把session id传递回服务器。好比:
<form name="testform" action="/xxx">
<input type="hidden" name="jsessionid" value="ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764">
<input type="text">
</form>
实际上这种技术能够简单的用对action应用URL重写来代替。
cookie 和session 的区别:
一、cookie数据存放在客户的浏览器上,session数据放在服务器上。
二、cookie不是很安全,别人能够分析存放在本地的COOKIE并进行COOKIE欺骗,考虑到安全应当使用session。
三、session会在必定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能, 考虑到减轻服务器性能方面,应当使用COOKIE。
四、单个cookie保存的数据不能超过4K,不少浏览器都限制一个站点最多保存20个cookie。
五、因此我的建议:
将登录信息等重要信息存放为SESSION
其余信息若是须要保留,能够放在COOKIE中
1. HashMap的数据结构
数据结构中有数组和链表来实现对数据的存储,但这二者基本上是两个极端。
数组
数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特色是:寻址容易,插入和删除困难;
链表
链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特色是:寻址困难,插入和删除容易。
哈希表
那么咱们能不能综合二者的特性,作出一种寻址容易,插入删除也容易的数据结构?答案是确定的,这就是咱们要提起的哈希表。哈希表((Hash table)既知足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。
哈希表有多种不一样的实现方法,我接下来解释的是最经常使用的一种方法—— 拉链法,咱们能够理解为“链表的数组” ,如图:
从上图咱们能够发现哈希表是由数组+链表组成的,一个长度为16的数组中,每一个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。通常状况是经过hash(key)%len得到,也就是元素的key的哈希值对数组长度取模获得。好比上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。因此十二、2八、108以及140都存储在数组下标为12的位置。
HashMap其实也是一个线性的数组实现的,因此能够理解为其存储数据的容器就是一个线性数组。这可能让咱们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有作一些处理。
首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value咱们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,咱们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table;
2. HashMap的存取实现
既然是线性数组,为何能随机存取?这里HashMap用了一个小算法,大体是这样实现:
// 存储时:
int hash = key.hashCode(); // 这个hashCode方法这里不详述,只要理解每一个key的hash是一个固定的int值
int index = hash % Entry[].length;
Entry[index] = value;
// 取值时:
int hash = key.hashCode();
int index = hash % Entry[].length;
return Entry[index];
1)put
疑问:若是两个key经过hash%Entry[].length获得的index相同,会不会有覆盖的危险?
这里HashMap里面用到链式数据结构的一个概念。上面咱们提到过Entry类里面有一个next属性,做用是指向下一个Entry。打个比方, 第一个键值对A进来,经过计算其key的hash获得的index=0,记作:Entry[0] = A。一会后又进来一个键值对B,经过计算其index也等于0,如今怎么办?HashMap会这样作:B.next = A,Entry[0] = B,若是又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样咱们发现index=0的地方其实存取了A,B,C三个键值对,他们经过next这个属性连接在一块儿。因此疑问不用担忧。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大体实现,咱们应该已经清楚了。
public V put(K key, V value) {
if (key == null)
return putForNullKey(value); //null老是放在数组的第一个链表中
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
//遍历链表
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//若是key在链表中已存在,则替换为新value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //参数e, 是Entry.next
//若是size超过threshold,则扩充table大小。再散列
if (size++ >= threshold)
resize(2 * table.length);
}
固然HashMap里面也包含一些优化方面的实现,这里也说一下。好比:Entry[]的长度必定后,随着map里面数据的愈来愈长,这样同一个index的链就会很长,会不会影响性能?HashMap里面设置一个因子,随着map的size愈来愈大,Entry[]会以必定的规则加长长度。
2)get
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
//先定位到数组元素,再遍历该元素处的链表
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
3)null key的存取
null key老是存放在Entry[]数组的第一个元素。
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
4)肯定数组index:hashcode % table.length取模
HashMap存取时,都须要计算当前key应该对应Entry[]数组哪一个元素,即计算数组下标;算法以下:
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
按位取并,做用上至关于取模mod或者取余%。
这意味着数组下标相同,并不表示hashCode相同。
5)table初始大小
public HashMap(int initialCapacity, float loadFactor) {
.....
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
注意table初始大小并非构造函数中的initialCapacity!!
而是 >= initialCapacity的2的n次幂!!!!
————为何这么设计呢?——
3. 解决hash冲突的办法
Java中hashmap的解决办法就是采用的链地址法。
4. 再散列rehash过程
当哈希表的容量超过默认容量时,必须调整table的大小。当容量已经达到最大可能值时,那么该方法就将容量调整到Integer.MAX_VALUE返回,这时,须要建立一张新表,将原表的映射到新表中。
/**
* Rehashes the contents of this map into a new array with a
* larger capacity. This method is called automatically when the
* number of keys in this map reaches its threshold.
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
//从新计算index
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
ConcurrentHashMap是Java 5中支持高并发、高吞吐量的线程安全HashMap实现。在这以前我对ConcurrentHashMap只有一些肤浅的理解,仅知道它采用了多个锁,大概也足够了。可是在通过一次惨痛的面试经历以后,我以为必须深刻研究它的实现。面试中被问到读是否要加锁,由于读写会发生冲突,我说必需要加锁,我和面试官也所以发生了冲突,结果可想而知。仍是闲话少说,经过仔细阅读源代码,如今总算理解ConcurrentHashMap实现机制了,其实现之精巧,使人叹服,与你们共享之。
实现原理
锁分离 (Lock Stripping)
ConcurrentHashMap容许多个修改操做并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不一样部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不一样的部分,每一个段其实就是一个小的hash table,它们有本身的锁。只要多个修改操做发生在不一样的段上,它们就能够并发进行。
有些方法须要跨段,好比size()和containsValue(),它们可能须要锁定整个表而而不只仅是某个段,这须要按顺序锁定全部段,操做完毕后,又按顺序释放全部段的锁。这里“按顺序”是很重要的,不然极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,而且其成员变量实际上也是final的,可是,仅仅是将数组声明为final的并不保证数组成员也是final的,这须要实现上的保证。这能够确保不会出现死锁,由于得到锁的顺序是固定的。不变性在多线程编程占有很重要的地位,下面还要谈到。
不变(Immutable)和易变(Volatile)
ConcurrentHashMap彻底容许多个读操做并发进行,读操做并不须要加锁。若是使用传统的技术,如HashMap中的实现,若是容许能够在hash链的中间添加或删除元素,读操做不加锁将获得不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry表明每一个hash链中的一个节点,其结构以下所示:
能够看到除了value不是final的,其它值都是final的,这意味着不能从hash链的中间或尾部添加或删除节点,由于这须要修改next引用值,全部的节点的修改只能从头部开始。对于put操做,能够一概添加到Hash链的头部。可是对于remove操做,可能须要从中间删除一个节点,这就须要将要删除节点的前面全部节点整个复制一遍,最后一个节点指向要删除结点的下一个结点。这在讲解删除操做时还会详述。为了确保读操做可以看到最新的值,将value设置成volatile,这避免了加锁。
其它
为了加快定位段以及段中hash槽的速度,每一个段hash槽的的个数都是2^n,这使得经过位运算就能够定位段和段中hash槽的位置。当并发级别为默认值16时,也就是段的个数,hash值的高4位决定分配在哪一个段中。可是咱们也不要忘记《算法导论》给咱们的教训:hash槽的的个数不该该是2^n,这可能致使hash槽分配不均,这须要对hash值从新再hash一次。(这段彷佛有点多余了 )
这是从新hash的算法,还比较复杂,我也懒得去理解了。
这是定位段的方法:
数据结构
关于Hash表的基础数据结构,这里不想作过多的探讨。Hash表的一个很重要方面就是如何解决hash冲突,ConcurrentHashMap和HashMap使用相同的方式,都是将hash值相同的节点放在一个hash链中。与HashMap不一样的是,ConcurrentHashMap使用多个子Hash表,也就是段(Segment)。下面是ConcurrentHashMap的数据成员:
全部的成员都是final的,其中segmentMask和segmentShift主要是为了定位段,参见上面的segmentFor方法。
每一个Segment至关于一个子Hash表,它的数据成员以下:
count用来统计该段数据的个数,它是volatile,它用来协调修改和读取操做,以保证读取操做可以读取到几乎最新的修改。协调方式是这样的,每次修改操做作告终构上的改变,如增长/删除节点(修改节点的值不算结构上的改变),都要写count值,每次读取操做开始都要读取count的值。这利用了Java 5中对volatile语义的加强,对同一个volatile变量的写和读存在happens-before关系。modCount统计段结构改变的次数,主要是为了检测对多个段进行遍历过程当中某个段是否发生改变,在讲述跨段操做时会还会详述。threashold用来表示须要进行rehash的界限值。table数组存储段中节点,每一个数组元素是个hash链,用HashEntry表示。table也是volatile,这使得可以读取到最新的table值而不须要同步。loadFactor表示负载因子。
实现细节
修改操做
先来看下删除操做remove(key)。
整个操做是先定位到段,而后委托给段的remove操做。当多个删除操做并发进行时,只要它们所在的段不相同,它们就能够同时进行。下面是Segment的remove方法实现:
整个操做是在持有段锁的状况下执行的,空白行以前的行主要是定位到要删除的节点e。接下来,若是不存在这个节点就直接返回null,不然就要将e前面的结点复制一遍,尾结点指向e的下一个结点。e后面的结点不须要复制,它们能够重用。下面是个示意图,我直接从这个网站 上复制的(画这样的图实在是太麻烦了,若是哪位有好的画图工具,能够推荐一下)。
删除元素以前:
删除元素3以后:
第二个图其实有点问题,复制的结点中应该是值为2的结点在前面,值为1的结点在后面,也就是恰好和原来结点顺序相反,还好这不影响咱们的讨论。
整个remove实现并不复杂,可是须要注意以下几点。第一,当要删除的结点存在时,删除的最后一步操做要将count的值减一。这必须是最后一步操做,不然读取操做可能看不到以前对段所作的结构性修改。第二,remove执行的开始就将table赋给一个局部变量tab,这是由于table是volatile变量,读写volatile变量的开销很大。编译器也不能对volatile变量的读写作任何优化,直接屡次访问非volatile实例变量没有多大影响,编译器会作相应优化。
接下来看put操做,一样地put操做也是委托给段的put方法。下面是段的put方法:
该方法也是在持有段锁的状况下执行的,首先判断是否须要rehash,须要就先rehash。接着是找是否存在一样一个key的结点,若是存在就直接替换这个结点的值。不然建立一个新的结点并添加到hash链的头部,这时必定要修改modCount和count的值,一样修改count的值必定要放在最后一步。put方法调用了rehash方法,reash方法实现得也很精巧,主要利用了table的大小为2^n,这里就不介绍了。
修改操做还有putAll和replace。putAll就是屡次调用put方法,没什么好说的。replace甚至不用作结构上的更改,实现要比put和delete要简单得多,理解了put和delete,理解replace就不在话下了,这里也不介绍了。
获取操做
首先看下get操做,一样ConcurrentHashMap的get操做是直接委托给Segment的get方法,直接看Segment的get方法:
get操做不须要锁。第一步是访问count变量,这是一个volatile变量,因为全部的修改操做在进行结构修改时都会在最后一步写count变量,经过这种机制保证get操做可以获得几乎最新的结构更新。对于非结构更新,也就是结点值的改变,因为HashEntry的value变量是volatile的,也能保证读取到最新的值。接下来就是对hash链进行遍历找到要获取的结点,若是没有找到,直接访回null。对hash链进行遍历不须要加锁的缘由在于链指针next是final的。可是头指针却不是final的,这是经过getFirst(hash)方法返回,也就是存在table数组中的值。这使得getFirst(hash)可能返回过期的头结点,例如,当执行get方法时,刚执行完getFirst(hash)以后,另外一个线程执行了删除操做并更新头结点,这就致使get方法中返回的头结点不是最新的。这是能够容许,经过对count变量的协调机制,get能读取到几乎最新的数据,虽然可能不是最新的。要获得最新的数据,只有采用彻底的同步。
最后,若是找到了所求的结点,判断它的值若是非空就直接返回,不然在有锁的状态下再读一次。这彷佛有些费解,理论上结点的值不可能为空,这是由于put的时候就进行了判断,若是为空就要抛NullPointerException。空值的惟一源头就是HashEntry中的默认值,由于HashEntry中的value不是final的,非同步读取有可能读取到空值。仔细看下put操做的语句:tab[index] = new HashEntry<K,V>(key, hash, first, value),在这条语句中,HashEntry构造函数中对value的赋值以及对tab[index]的赋值可能被从新排序,这就可能致使结点的值为空。这种状况应当很罕见,一旦发生这种状况,ConcurrentHashMap采起的方式是在持有锁的状况下再读一遍,这可以保证读到最新的值,而且必定不会为空值。
另外一个操做是containsKey,这个实现就要简单得多了,由于它不须要读取值:
跨段操做
有些操做须要涉及到多个段,好比说size(), containsValaue()。先来看下size()方法:
size方法主要思路是先在没有锁的状况下对全部段大小求和,若是不能成功(这是由于遍历过程当中可能有其它线程正在对已经遍历过的段进行结构性更新),最多执行RETRIES_BEFORE_LOCK次,若是还不成功就在持有全部段锁的状况下再对全部段大小求和。在没有锁的状况下主要是利用Segment中的modCount进行检测,在遍历过程当中保存每一个Segment的modCount,遍历完成以后再检测每一个Segment的modCount有没有改变,若是有改变表示有其它线程正在对Segment进行结构性并发更新,须要从新计算。
其实这种方式是存在问题的,在第一个内层for循环中,在这两条语句sum += segments[i].count; mcsum += mc[i] = segments[i].modCount;之间,其它线程可能正在对Segment进行结构性的修改,致使segments[i].count和segments[i].modCount读取的数据并不一致。这可能使size()方法返回任什么时候候都未曾存在的大小,很奇怪javadoc竟然没有明确标出这一点,多是由于这个时间窗口过小了吧。size()的实现还有一点须要注意,必需要先segments[i].count,才能segments[i].modCount,这是由于segment[i].count是对volatile变量的访问,接下来segments[i].modCount才能获得几乎最新的值(前面我已经说了为何只是“几乎”了)。这点在containsValue方法中获得了淋漓尽致的展示:
一样注意内层的第一个for循环,里面有语句int c = segments[i].count; 可是c却历来没有被使用过,即便如此,编译器也不能作优化将这条语句去掉,由于存在对volatile变量count的读取,这条语句存在的惟一目的就是保证segments[i].modCount读取到几乎最新的值。关于containsValue方法的其它部分就不分析了,它和size方法差很少。
跨段方法中还有一个isEmpty()方法,其实现比size()方法还要简单,也不介绍了。最后简单地介绍下迭代方法,如keySet(), values(), entrySet()方法,这些方法都返回相应的迭代器,全部迭代器都继承于Hash_Iterator类(提交时竟然提醒我不能包含sh It,只得加了下划线),里实现了主要的方法。其结构是:
nextSegmentIndex是段的索引,nextTableIndex是nextSegmentIndex对应段中中hash链的索引,currentTable是nextSegmentIndex对应段的table。调用next方法时主要是调用了advance方法:
不想再多介绍了,惟一须要注意的是跳到下一个段时,必定要先读取下一个段的count变量。
这种迭代方式的主要效果是不会抛出ConcurrentModificationException。一旦获取到下一个段的table,也就意味着这个段的头结点在迭代过程当中就肯定了,在迭代过程当中就不能反映对这个段节点并发的删除和添加,对于节点的更新是可以反映的,由于节点的值是一个volatile变量。
(1):MyISAM存储引擎
不支持事务、也不支持外键,优点是访问速度快,对事务完整性没有 要求或者以select,insert为主的应用基本上能够用这个引擎来建立表
支持3种不一样的存储格式,分别是:静态表;动态表;压缩表
静态表:表中的字段都是非变长字段,这样每一个记录都是固定长度的,优势存储很是迅速,容易缓存,出现故障容易恢复;缺点是占用的空间一般比动态表多(由于存储时会按照列的宽度定义补足空格)ps:在取数据的时候,默认会把字段后面的空格去掉,若是不注意会把数据自己带的空格也会忽略。
动态表:记录不是固定长度的,这样存储的优势是占用的空间相对较少;缺点:频繁的更新、删除数据容易产生碎片,须要按期执行OPTIMIZE TABLE或者myisamchk-r命令来改善性能
压缩表:由于每一个记录是被单独压缩的,因此只有很是小的访问开支
(2)InnoDB存储引擎*
该存储引擎提供了具备提交、回滚和崩溃恢复能力的事务安全。可是对比MyISAM引擎,写的处理效率会差一些,而且会占用更多的磁盘空间以保留数据和索引。
InnoDB存储引擎的特色:支持自动增加列,支持外键约束
(3):MEMORY存储引擎
Memory存储引擎使用存在于内存中的内容来建立表。每一个memory表只实际对应一个磁盘文件,格式是.frm。memory类型的表访问很是的快,由于它的数据是放在内存中的,而且默认使用HASH索引,可是一旦服务关闭,表中的数据就会丢失掉。
MEMORY存储引擎的表能够选择使用BTREE索引或者HASH索引,两种不一样类型的索引有其不一样的使用范围
Hash索引优势:
Hash 索引结构的特殊性,其检索效率很是高,索引的检索能够一次定位,不像B-Tree 索引须要从根节点到枝节点,最后才能访问到页节点这样屡次的IO访问,因此 Hash 索引的查询效率要远高于 B-Tree 索引。
Hash索引缺点: 那么不精确查找呢,也很明显,由于hash算法是基于等值计算的,因此对于“like”等范围查找hash索引无效,不支持;
Memory类型的存储引擎主要用于哪些内容变化不频繁的代码表,或者做为统计操做的中间结果表,便于高效地对中间结果进行分析并获得最终的统计结果,。对存储引擎为memory的表进行更新操做要谨慎,由于数据并无实际写入到磁盘中,因此必定要对下次从新启动服务后如何得到这些修改后的数据有所考虑。
(4)MERGE存储引擎
Merge存储引擎是一组MyISAM表的组合,这些MyISAM表必须结构彻底相同,merge表自己并无数据,对merge类型的表能够进行查询,更新,删除操做,这些操做其实是对内部的MyISAM表进行的。
板子以前作过2年web开发培训(入门?),得到挺多学生好评,这是蛮有成就感的一件事,准备花点时间根据当时的一些备课内容整理出一系列文章出来,但愿能给更多人带来帮助,这是系列文章的第一篇
注:科普文章一篇,大牛绕道
索引是作什么的?
索引用于快速找出在某个列中有一特定值的行。不使用索引,MySQL必须从第1条记录开始而后读完整个表直到找出相关的行。
表越大,花费的时间越多。若是表中查询的列有一个索引,MySQL能快速到达一个位置去搜寻到数据文件的中间,没有必要看全部数据。
大多数MySQL索引(PRIMARY KEY、UNIQUE、INDEX和FULLTEXT)在B树中存储。只是空间列类型的索引使用R-树,而且MEMORY表还支持hash索引。
索引好复杂,我该怎么理解索引,有没一个更形象点的例子?
有,想象一下,你面前有本词典,数据就是书的正文内容,你就是那个cpu,而索引,则是书的目录
索引越多越好?
大多数状况下索引能大幅度提升查询效率,但:
索引的字段类型问题
like 不能用索引?
想象一下,你在看一本成语词典,目录是按成语拼音顺序创建,查询需求是,你想找以 “一”字开头的成语(”一%“),和你想找包含一字的成语(“%一%”)
<,<=,=,>,>=,BETWEEN,IN
<>,not in ,!=则不行
什么样的字段不适合建索引?
一次查询能用多个索引吗?
不能
多列查询该如何建索引?
一次查询只能用到一个索引,因此 首先枪毙 a,b各建索引方案
a仍是b? 谁的区分度更高(同值的最少),建谁!
固然,联合索引也是个不错的方案,ab,仍是ba,则同上,区分度高者,在前
联合索引的问题?
where a = “xxx” 能够使用 AB 联合索引
where b = “xxx” 则不可 (再想象一下,这是书的目录?)
因此,大多数状况下,有AB索引了,就能够不用在去建一个A索引了
哪些常见状况不能用索引?
也即
select * from test where mobile = 13711112222;
但是没法用到mobile字段的索引的哦(若是mobile是char 或 varchar类型的话)
btw,千万不要尝试用int来存手机号(为何?本身想!要不本身试试)
覆盖索引(Covering Indexes)拥有更高效率
索引包含了所需的所有值的话,就只select 他们,换言之,只select 须要用到的字段,如无必要,可尽可能避免select *
NULL 的问题
NULL会致使索引形同虚设,因此在设计表结构时应避免NULL 的存在(用其余方式表达你想表达的NULL,好比 -1?)
如何查看索引信息,如何分析是否正确用到索引?
show index from tablename;
explain select ……;
关于explain,改天能够找个时间专门写一篇入门帖,在此以前,能够尝试 google
了解本身的系统,不要过早优化!
过早优化,一直是个很是讨厌而又时刻存在的问题,大多数时候就是由于不了解本身的系统,不知道本身系统真正的承载能力
好比:几千条数据的新闻表,天天几百几千次的正文搜索,大多数时候咱们能够放心的去like,而不要又去建一套全文搜索什么的,毕竟cpu仍是比人脑厉害太多
分享个小案例:
曾经有个朋友找板子,说:大师帮看看,公司网站打不开
板子笑了笑:大师可不敢当啊,待我看看再说
板子花了10分钟分析了下:中小型企业站,量不大(两三万pv天天),独立服务器,数据量不大(100M不到),应该不至于太慢
某个外包团队作的项目,年久失修,完全改造?不现实!
因而,板子花了20分钟给能够加索引的字段都加上了索引,因而,世界安静了
朋友说:另一个哥们说,优化至少得2w外包费,你只用30分钟,看来,大师你是当之无愧了,选个最好的餐馆吧
板子:那就来点西餐吧,常熟路地铁站肯德基等你!
第一范式
一、每一列属性都是不可再分的属性值,确保每一列的原子性
二、两列的属性相近或类似或同样,尽可能合并属性同样的列,确保不产生冗余数据。
若是需求知道那个省那个市并按其分类,那么显然第一个表格是不容易知足需求的,也不符合第一范式。
显然第一个表结构不但不能知足足够多物品的要求,还会在物品少时产生冗余。也是不符合第一范式的。
第二范式
每一行的数据只能与其中一列相关,即一行数据只作一件事。只要数据列中出现数据重复,就要把表拆分开来。
一我的同时订几个房间,就会出来一个订单号多条数据,这样子联系人都是重复的,就会形成数据冗余。咱们应该把他拆开来。
这样便实现啦一条数据作一件事,不掺杂复杂的关系逻辑。同时对表数据的更新维护也更易操做。
第三范式
数据不能存在传递关系,即没个属性都跟主键有直接关系而不是间接关系。像:a-->b-->c 属性之间含有这样的关系,是不符合第三范式的。
好比Student表(学号,姓名,年龄,性别,所在院校,院校地址,院校电话)
这样一个表结构,就存在上述关系。 学号--> 所在院校 --> (院校地址,院校电话)
这样的表结构,咱们应该拆开来,以下。
(学号,姓名,年龄,性别,所在院校)--(所在院校,院校地址,院校电话)
最后:
三大范式只是通常设计数据库的基本理念,能够创建冗余较小、结构合理的数据库。若是有特殊状况,固然要特殊对待,数据库设计最重要的是看需求跟性能,需求>性能>表结构。因此不能一味的去追求范式创建数据库。
一,介绍
本总结我对于JAVA多线程中线程之间的通讯方式的理解,主要以代码结合文字的方式来讨论线程间的通讯,故摘抄了书中的一些示例代码。
二,线程间的通讯方式
①同步
这里讲的同步是指多个线程经过synchronized关键字这种方式来实现线程间的通讯。
参考示例:
public class MyObject { synchronized public void methodA() { //do something.... } synchronized public void methodB() { //do some other thing } } public class ThreadA extends Thread { private MyObject object; //省略构造方法 @Override public void run() { super.run(); object.methodA(); } } public class ThreadB extends Thread { private MyObject object; //省略构造方法 @Override public void run() { super.run(); object.methodB(); } } public class Run { public static void main(String[] args) { MyObject object = new MyObject(); //线程A与线程B 持有的是同一个对象:object ThreadA a = new ThreadA(object); ThreadB b = new ThreadB(object); a.start(); b.start(); } }
因为线程A和线程B持有同一个MyObject类的对象object,尽管这两个线程须要调用不一样的方法,可是它们是同步执行的,好比:线程B须要等待线程A执行完了methodA()方法以后,它才能执行methodB()方法。这样,线程A和线程B就实现了 通讯。
这种方式,本质上就是“共享内存”式的通讯。多个线程须要访问同一个共享变量,谁拿到了锁(得到了访问权限),谁就能够执行。
②while轮询的方式
代码以下:
1 import java.util.ArrayList; 2 import java.util.List; 3 4 public class MyList { 5 6 private List<String> list = new ArrayList<String>(); 7 public void add() { 8 list.add("elements"); 9 } 10 public int size() { 11 return list.size(); 12 } 13 } 14 15 import mylist.MyList; 16 17 public class ThreadA extends Thread { 18 19 private MyList list; 20 21 public ThreadA(MyList list) { 22 super(); 23 this.list = list; 24 } 25 26 @Override 27 public void run() { 28 try { 29 for (int i = 0; i < 10; i++) { 30 list.add(); 31 System.out.println("添加了" + (i + 1) + "个元素"); 32 Thread.sleep(1000); 33 } 34 } catch (InterruptedException e) { 35 e.printStackTrace(); 36 } 37 } 38 } 39 40 import mylist.MyList; 41 42 public class ThreadB extends Thread { 43 44 private MyList list; 45 46 public ThreadB(MyList list) { 47 super(); 48 this.list = list; 49 } 50 51 @Override 52 public void run() { 53 try { 54 while (true) { 55 if (list.size() == 5) { 56 System.out.println("==5, 线程b准备退出了"); 57 throw new InterruptedException(); 58 } 59 } 60 } catch (InterruptedException e) { 61 e.printStackTrace(); 62 } 63 } 64 } 65 66 import mylist.MyList; 67 import extthread.ThreadA; 68 import extthread.ThreadB; 69 70 public class Test { 71 72 public static void main(String[] args) { 73 MyList service = new MyList(); 74 75 ThreadA a = new ThreadA(service); 76 a.setName("A"); 77 a.start(); 78 79 ThreadB b = new ThreadB(service); 80 b.setName("B"); 81 b.start(); 82 } 83 }
在这种方式下,线程A不断地改变条件,线程ThreadB不停地经过while语句检测这个条件(list.size()==5)是否成立 ,从而实现了线程间的通讯。可是这种方式会浪费CPU资源。之因此说它浪费资源,是由于JVM调度器将CPU交给线程B执行时,它没作啥“有用”的工做,只是在不断地测试 某个条件是否成立。就相似于现实生活中,某我的一直看着手机屏幕是否有电话来了,而不是: 在干别的事情,当有电话来时,响铃通知TA电话来了。关于线程的轮询的影响,可参考:JAVA多线程之当一个线程在执行死循环时会影响另一个线程吗?
这种方式还存在另一个问题:
轮询的条件的可见性问题,关于内存可见性问题,可参考:JAVA多线程之volatile 与 synchronized 的比较中的第一点“一,volatile关键字的可见性”
线程都是先把变量读取到本地线程栈空间,而后再去再去修改的本地变量。所以,若是线程B每次都在取本地的 条件变量,那么尽管另一个线程已经改变了轮询的条件,它也察觉不到,这样也会形成死循环。
③wait/notify机制
代码以下:
1 import java.util.ArrayList; 2 import java.util.List; 3 4 public class MyList { 5 6 private static List<String> list = new ArrayList<String>(); 7 8 public static void add() { 9 list.add("anyString"); 10 } 11 12 public static int size() { 13 return list.size(); 14 } 15 } 16 17 18 public class ThreadA extends Thread { 19 20 private Object lock; 21 22 public ThreadA(Object lock) { 23 super(); 24 this.lock = lock; 25 } 26 27 @Override 28 public void run() { 29 try { 30 synchronized (lock) { 31 if (MyList.size() != 5) { 32 System.out.println("wait begin " 33 + System.currentTimeMillis()); 34 lock.wait(); 35 System.out.println("wait end " 36 + System.currentTimeMillis()); 37 } 38 } 39 } catch (InterruptedException e) { 40 e.printStackTrace(); 41 } 42 } 43 } 44 45 46 public class ThreadB extends Thread { 47 private Object lock; 48 49 public ThreadB(Object lock) { 50 super(); 51 this.lock = lock; 52 } 53 54 @Override 55 public void run() { 56 try { 57 synchronized (lock) { 58 for (int i = 0; i < 10; i++) { 59 MyList.add(); 60 if (MyList.size() == 5) { 61 lock.notify(); 62 System.out.println("已经发出了通知"); 63 } 64 System.out.println("添加了" + (i + 1) + "个元素!"); 65 Thread.sleep(1000); 66 } 67 } 68 } catch (InterruptedException e) { 69 e.printStackTrace(); 70 } 71 } 72 } 73 74 public class Run { 75 76 public static void main(String[] args) { 77 78 try { 79 Object lock = new Object(); 80 81 ThreadA a = new ThreadA(lock); 82 a.start(); 83 84 Thread.sleep(50); 85 86 ThreadB b = new ThreadB(lock); 87 b.start(); 88 } catch (InterruptedException e) { 89 e.printStackTrace(); 90 } 91 } 92 }
线程A要等待某个条件知足时(list.size()==5),才执行操做。线程B则向list中添加元素,改变list 的size。
A,B之间如何通讯的呢?也就是说,线程A如何知道 list.size() 已经为5了呢?
这里用到了Object类的 wait() 和 notify() 方法。
当条件未知足时(list.size() !=5),线程A调用wait() 放弃CPU,并进入阻塞状态。---不像②while轮询那样占用CPU
当条件知足时,线程B调用 notify()通知 线程A,所谓通知线程A,就是唤醒线程A,并让它进入可运行状态。
这种方式的一个好处就是CPU的利用率提升了。
可是也有一些缺点:好比,线程B先执行,一会儿添加了5个元素并调用了notify()发送了通知,而此时线程A还执行;当线程A执行并调用wait()时,那它永远就不可能被唤醒了。由于,线程B已经发了通知了,之后再也不发通知了。这说明:通知过早,会打乱程序的执行逻辑。
④管道通讯就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通讯
具体就不介绍了。分布式系统中说的两种通讯机制:共享内存机制和消息通讯机制。感受前面的①中的synchronized关键字和②中的while轮询 “属于” 共享内存机制,因为是轮询的条件使用了volatile关键字修饰时,这就表示它们经过判断这个“共享的条件变量“是否改变了,来实现进程间的交流。
而管道通讯,更像消息传递机制,也就是说:经过管道,将一个线程中的消息发送给另外一个。