Java基础

小记:
谈谈sychronized和volatile:
思路:

  • 1、他们出现的意义:多线程的互相干扰例子—>JVM的JIT优化编译技术,指令重排序,可见性,先行发生原则定义和规定
  • 2、同步代码块的定义,用法,特性(内置锁隐式锁可重入性)
  • 3、原子操作定义
  • 在编程中,原子操作是一次有效地发生的操作。原子作用不能中途停止:它要么完全发生,要么根本不发生。原子操作的副作用在操作完成之前是不可见的
  • 对于引用变量和大多数基本变量(除了long和double之外的所有类型),读写都是原子性的。
    对于所有声明为volatile的变量(包括long变量和double变量),读写都是原子性的
    引申:jvm的原子性操作的执行,举个例子比如栈对于c++的实现,加分项
  • 4、java内存管理多线程的变量的操作,使用volatile和synchronized取决于项目的大小和复杂度。
  • 5、volatile和synchronized在锁的场景中的应用,以及锁的实现
  • 6、锁的优化演进以及并发包的出现,和并发包中的数据结构
  • lock对象
  • Executors执行器
  • 并发容器
  • 原子变量
  • ThreadLocalRandom
  • 7、可以使用并发包工具类的实现的功能。

回答:

先答: 共性:他们的出现都是为了保证线程安全的,因为线程不安全的主要因素是可变和共享。所以核心也就是只读和加锁来保证。
本质:
volatile关键字可以确保直接从主内存读取给定的变量,并在更新时始终将其写回主内存。当使用volatile修饰变量时候,意味着任何对此便来给你的操作都会在内存中进行,不会产生副本,以保证共享变量的可见性,局部阻止了指令重排序的发生。但是如果不确定共享变量是否会被多个线程并发写,保险的做法是使用同步代码块来实现线程同步。因为所有的操作都需要同步给内存变量(从主存读取和写入比访问CPU缓存更昂贵),所以volatile一定会使线程的执行速度变慢。因此,只在真正需要强制变量可见性时才使用volatile变量或者简化代码的实现以及同步策略的验证(确保自身状态的可见性,确保所引用对象的状态的可见性,标识一些重要的程序生命周期事件的发生比如初始化或者关闭)。如果是比较复杂操作就不要使用了。比如我在应用中设置的单机关键路径日志打印开关。
synchronized实现方式和volatile略有不同,线程在得到锁时读入副本,释放时候写回内存,锁的操作尤其符号happen before原则。
volatile解决的是多线程共享变量的可见性问题,类似于synchronized但是不具备synchronized的互斥性。所以对volatile变量的操作并非都具有原子性,这是一个容易犯错误的地方。
能实现count++原子操作的其他类AtomicLong和LongAdder。JDK8推荐使用LongAdder类,比AtomicLong性能更好,有效的减少了乐观锁的重试次数。如果是一写多读那么volatile则可以修饰变量非常合适。比如Cow奶牛系列的CopyOnWriteArrayList.它在修改数据时候会把整个集合数据全部复制出来,对写操作加锁,修改完成后,再用setArray()把Array指向新的结合。使用volatile可以使得读线程尽快的感知array的修改,不进行指令重排,操作后及对其他线程可见。

1、对Runtime的了解

点击这里

2、HashMap专题和HashMap引出的变种问题

Q:hashMap,concurrentHashMap,hashtable比较
Concurrenthashmap 是怎么做到线程安全的?
ConcurrentHashmap的锁是如何加的?是不是分段越多越好----针对的是1.7锁分段技术
HashTable 你了解过吗?如何保证线程安全问题?
用hashmap实现redis有什么问题(死锁,死循环,可用ConcurrentHashmap)
hashmap的底层实现,遍历hashmap的4种方式,

public static void main(String[] args) {
  Map map=new HashMap();
        map.put("1", "value1");
        map.put("2", "value2");
        map.put("3", "value3");
        map.put("4", "value4");
        
        //第一种:普通使用,二次取值
        System.out.println("\n通过Map.keySet遍历key和value:");  
        for(String key:map.keySet())
        {
         System.out.println("Key: "+key+" Value: "+map.get(key));
        }
        
        //第二种
        System.out.println("\n通过Map.entrySet使用iterator遍历key和value: ");  
        Iterator map1it=map.entrySet().iterator();
        while(map1it.hasNext())
        {
         Map.Entry entry=(Entry) map1it.next();
         System.out.println("Key: "+entry.getKey()+" Value: "+entry.getValue());
        }
        
        //第三种:推荐,尤其是容量大时  
        System.out.println("\n通过Map.entrySet遍历key和value");  
        for(Map.Entry entry: map.entrySet())
        {
         System.out.println("Key: "+ entry.getKey()+ " Value: "+entry.getValue());
        }
        
        //第四种  
        System.out.println("\n通过Map.values()遍历所有的value,但不能遍历key");  
        for(String v:map.values())
        {
         System.out.println("The value is "+v);
        }
 }

LinkedHashmap的底层实现。

hashmap如果只有一个写其他全读会出什么问题(fail-fast和fail-safe问题引入)

Java内存模型分析
Java的并发,线程之间通信采用共享内存的方式,由Java内存模型(简称JMM)控制。JMM决定一个线程对共享变量的写入何时对其他线程可见。
JMM定义:线程之间共享的变量存储在主内存,每个线程都有一个私有的本地内存,本地内存存储了共享变量的副本。本地内存是JMM的一个抽象概念,与主内存对应的物理内存并非在同一个空间,它包含了CPU的L1 L2 L3高速缓存、写缓冲区、寄存器、或者其他硬件和编译器的优化。
Java基础_第1张图片
初步了解了JAVA内存模型,对应到上面的问题,R W线程共享了主内存中hashmap,在自己的工作内存中都存储了这个hashmap的变量副本,因此当W线程写同时R线程读可能会出现如下几种场景:
<1> A线程正在更新自己工作内存中的变量副本,还没有开始向主内存同步数据,此时B线程开始读取,读取到的数据是自己工作内存中的变量副本。JMM规定了线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据。
<2> A线程已经完成自己工作内存中变量副本的更新,并且将数据同步到了主内存,当主内存数据发生更新后,会出现缓存一致性问题,此时会通知B线程同步主内存中的最新数据到本地工作内存。此时R线程开始读取,读取到的数据是A线程最新写入的数据。
<3> A线程已经完成了自己工作内存中变量副本的更新,正在将数据同步到主内存,此时B线程开始读取,读取到的数据是自己本地工作内存中的数据。
可见,在上述探讨的问题场景中,A线程写过程,B线程读取的数据要么是A线程写之前的旧数据,要么是A线程写之后的最新数据
这里引出java集合常见的一个错误:fail-fast。它是java集合的一种错误检测机制,在集合遍历过程(iterator或者foreach),如果发生对集合add或者remove操作而迭代器不知道,就会触发fast-fail并抛出异常。
这里有两点注意:
<1> 没有说必须是多线程修改集合才会引起fast fail错误。只要是遍历过程集合发生add或者remove操作就可能发生。
<2> 只有在修改集合的时候iterator不知道才会发生fast fail错误,因此可以理解并非遍历过程就无法修改集合,通过Iterator的remove方法就可以实现。
modCount是hashmap中的成员变量。在调用put(),remove(),clear(),ensureCapacity()这些会修改数据结构的方法中都会使modCount++。在获取迭代器的时候会把modCount赋值给迭代器的expectedModCount变量。此时modCount与expectedModCount肯定相等,在迭代元素的过程中如果hashmap调用自身方法使集合发生变化,那么modCount肯定会变,此时modCount与expectedModCount肯定会不相等。在迭代过程中,只要发现modCount!=expectedModCount,则说明结构发生了变化也就没有必要继续迭代元素了。此时会抛出ConcurrentModificationException,终止迭代操作。
fastfail问题告诉我们,非线程安全集合在使用过程是需要谨慎的,我们开发过程该如何应对呢?
同样以hashmap为例:
场景1:写线程唯一、读线程不确定,没有迭代操作。使用hashmap不会存在程序不安全,最多就是发生数据不一致性的问题。
场景2:写线程唯一、读线程不确定,有迭代操作,此时不能使用hashmap,会存在fastfail问题
场景3: 读写线程是同一个,且唯一,有迭代操作,此时注意不能通过集合方法remove或者add更改,只能通过iterator内方法来更新。不然会存在fastfail问题。
怎么来解决fast fail问题
方法1: 在iterator迭代过程和写hashmap的操作都加锁
方法2:使用ConcurrentHashMap代替HashMap
方法1通过加锁实现线程同步安全,这样在迭代过程避免modCount发生改变,因此不会发生fastfail错误。
方法2,ConcurrentHashMap是一种线程安全的HashMap。查看源码,ConcurrentHashMap没有设置modCount标志,允许在迭代过程数据发生add或者remove操作。
Java基础_第2张图片
fail-safe解决fail-fast问题:
1、java.util.concurrent实现的并发包线程优势
2、vecotr是线程安全的arraylist,hashtable是线程安全的hashmap。
3、synchronizedMap使用syncronized对集合的读写操作进行加锁,缺点是性能比较低

Q:concurrenhashmap求size是如何加锁的,如果刚求完一段后这段发生了变化该如何处理?

Java基础_第3张图片
涉及到的成员变量
Java基础_第4张图片Java基础_第5张图片
Java基础_第6张图片

Q:从ConcurrentHashMap一路问到锁&锁优化->LongAdder->伪共享->缓存行填充->cas等诸多技术细节。
A:上面的这两点问题又引发出新的空间领域,点击这里。

HashMap底层执行原理,hashtable和ConcurrentHashMap如何实现线程安全?
HashMap的底层数据结构,红黑树的具体结构及实现,红黑树与查找树的区别体现,接着聊ConcurrentHashMap,底层实现, HashMap哈希函数的认识,JDK1.8采用的hash函数
hashset底层实现,hashmap的put操作过程
说说HaspMap底层原理?
再说说它跟HaspTable和ConcurrentHashMap他们之间的相同点和不同点?
画下 HashMap 的结构图?
HashMap 、 HashTable 和 ConcurrentHashMap 的区别?
HashMap 源码分析,把里面的东西问了个遍?最后问是不是线程安全?
引出 ConcurrentHashMap,ConcurrentHashMap 源码分析,
常见集合类的区别和适用场景
并发容器了解哪些?
concurrentHashMap如何实现, ConcurrentHashMap 在Java7和Java8中的区别?
为什么Java8并发效率更好?
什么情况下用HashMap,什么情况用ConcurrentHashMap?

hashmap原理,处理哈希冲突用的哪种方法?HashMap中Hash冲突是怎么解决的?

拉链法

还知道什么处理哈希冲突的方法?

1.开放地址法
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
fi(key) = (f(key) + di) MOD m (di = 1,2,3,…,m-1)
2.再哈希法
再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数 计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。
3.拉链法
每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来。
Java基础_第7张图片
4.建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

HashMap的时间复杂度?

HashMap底层采用了hash算法,
根据 key 获得 hashCode 值
HashMap 初始有很多个类似于“桶”的数据结构,比如说预设了 10 个桶,通过 hashCode 经过一定的算法(这个算法必须是快速的) 得到这个 hashCode 应存在哪个桶中,然后内部生成 Map.Entry 对象将 key 和 value 存到桶中去。
所以一般情况下HashMap的插入和查找的时间复杂度都是O(1);

链表的上一级结构是什么?-----数组和链表。当然是数组。
Java8中的HashMap有什么变化?----看下面的文章
红黑树需要比较大小才能进行插入,是依据什么进行比较的?
Q:hash和B+树的区别?分别应用于什么场景?哪个比较好?

Java基础_第8张图片

Java 中有哪些线程安全的 Map?

Java中平时用的最多的Map集合就是HashMap了,它是线程不安全的。
看下面两个场景:
1、当用在方法内的局部变量时,局部变量属于当前线程级别的变量,其他线程访问不了,所以这时也不存在线程安全不安全的问题了。
2、当用在单例对象成员变量的时候呢?这时候多个线程过来访问的就是同一个HashMap了,对同个HashMap操作这时候就存在线程安全的问题了。

HashTable
HashTable的get/put方法都被synchronized关键字修饰,说明它们是方法级别阻塞的,它们占用共享资源锁,所以导致同时只能一个线程操作get或者put,而且get/put操作不能同时执行,所以这种同步的集合效率非常低,一般不建议使用这个集合。
SynchronizedMap
这种是直接使用工具类里面的方法创建SynchronizedMap,把传入进行的HashMap对象进行了包装同步而已,这个同步方式实现也比较简单,看出SynchronizedMap的实现方式是加了个对象锁,每次对HashMap的操作都要先获取这个mutex的对象锁才能进入,所以性能也不会比HashTable好到哪里去,也不建议使用。
ConcurrentHashMap - 推荐
这个也是最推荐使用的线程安全的Map,也是实现方式最复杂的一个集合,每个版本的实现方式也不一样,在jdk8之前是使用分段加锁的一个方式,分成16个桶,每次只加锁其中一个桶,
步骤如下:
分段机制:segment,每段加reentrantLock可重入锁
定位元素:1 找segment数组下标  2 找segment的HashEntry数组下标
get方法:不需要加锁,value值使用了volatile关键字修饰
put方法:hash计算段---锁定段---hash计算HashEntry数组---若超多阈值---扩容---释放;
ConcurrentHashMap是线程安全的,那是在他们的内部操作,其外部操作还是需要自己来保证其同步的,特别是静态的ConcurrentHashMap,其有更新和查询的过程,要保证其线程安全,需要syn一个不可变的参数才能保证其原子性
而在jdk8又加入了红黑树和CAS算法来实现。
链接:https://www.jianshu.com/p/533bb7cf8901

Hashmap 线程不安全的出现场景,
点击这里

Hashmap 线程不安全的出现场景
我们都知道HashMap初始容量大小为16,一般来说,当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的元素都需要被重算一遍。这叫rehash
另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%)

HashTable
底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
初始size为11,负载因子0.75
Java基础_第9张图片
扩容rehash:newsize = oldsize2+1
Java基础_第10张图片
计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
Java基础_第11张图片
HashMap
底层数组+链表实现,可以存储null键和null值,线程不安全
初始size为16,扩容:newsize = oldsize
2,size一定为2的n次幂
Java基础_第12张图片
Java基础_第13张图片
扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
计算index方法:index = hash & (tab.length – 1)


HashMap的初始值还要考虑加载因子:
哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。
加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。
空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。


HashMap的内部结构可以看作是数组(Node[] table)和链表的复合结构,数组被分为一个个桶(bucket),通过哈希值决定了键值对在这个数组中的寻址(哈希值相同的键值对,则以链表形式存储),如下图所示。有一点需要注意,如果链表大小超过阈值(TREEIFY_THRESHOLD,8),图中的链表就会被改造为树形结构。
Java基础_第14张图片
HashMap和Hashtable都是用hash算法来决定其元素的存储,因此HashMap和Hashtable的hash表包含如下属性:
容量(capacity):hash表中桶的数量
初始化容量(initial capacity):创建hash表时桶的数量,HashMap允许在构造器中指定初始化容量
尺寸(size):当前hash表中记录的数量
负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的散列表,依此类推。轻负载的散列表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)
除此之外,hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。
HashMap和Hashtable的构造器允许指定一个负载极限,HashMap和Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。
“负载极限”的默认值(0.75)是时间和空间成本上的一种折中:
较高的“负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMap的get()与put()方法都要用到查询)
较低的“负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销
程序猿可以根据实际情况来调整“负载极限”值,但一般不建议轻易修改,因为JDK自身的默认负载因子是非常符合通用场景需求的。如果确实需要修改,建议不要设置超过0.75,因为会显著增加冲突,降低HashMap的性能。
根据容量和负载因子的关系,我们可以预先设置合适的容量大小,具体数值我们可以根据扩容发生的条件来做简单预估,计算公式如下:
负载因子 * 容量 > 元素数量
所以预先设置的容量需要大于“预估元素数量/负载因子”,同时它是2的幂数。
上面提到HashMap会树化,为什么会这样呢?
本质上这是个安全问题。因为在元素放置过程中,如果一个对象哈希冲突,都被放置到同一个桶里,则会形成一个链表,我们知道链表查询是线性的,会严重影响存取的性能。而在现实世界,构造哈希冲突的数据并不是非常复杂的事情,恶意代码就可以利用这些数据大量与服务器端交互,导致服务器端CPU大量占用,这就构成了哈希碰撞拒绝服务攻击
ConcurrentHashMap
底层采用分段的数组+链表实现,线程安全
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
Hashtable和HashMap都实现了Map接口,但是Hashtable的实现是基于Dictionary抽象类的。Java5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来存储值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞时,对象将会储存在链表的下一个节点中。HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD,8),链表就会被改造为树形结构。
在HashMap中,null可以作为键,这样的键只有一个,但可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示HashMap中没有该key,也可以表示该key所对应的value为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个key,应该用containsKey()方法来判断。而在Hashtable中,无论是key还是value都不能为null。
Hashtable是线程安全的,它的方法是同步的,可以直接用在多线程环境中。而HashMap则不是线程安全的,在多线程环境中,需要手动实现同步机制。
Hashtable与HashMap另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM
Java基础_第15张图片
从类图中可以看出来在存储结构中ConcurrentHashMap比HashMap多出了一个类Segment。ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一个可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素。当对HashEntry数组的数据进行修改时,必须首先获得与它对应的segment锁。
ConcurrentHashMap是使用了锁分段技术来保证线程安全的
锁分段技术:
首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。
Hashtable容器在竞争激烈的并发环境下表现出效率低下的原因是因为所有访问Hashtable的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其它段的数据也能被其它线程访问。
ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。

Q:hashmap put 方法存放的时候怎么判断是否是重复的

Map中判断key是否相同是通过containsKey()方法进行的,它就是先判断key的hashCode是否相同,再判断key是否相等或equals的。如果存放的key值重复那么会直接覆盖掉与原来的Key值

Q:谈谈你理解的 HashMap,讲讲其中的 get put 过程。1.8 做了什么优化?是线程安全的嘛?不安全会导致哪些问题?如何解决?有没有线程安全的并发容器?ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做?

这里是引用
先来看看 1.7 中的实现Java基础_第16张图片
这是 HashMap 中比较核心的几个成员变量;看看分别是什么意思?
初始化桶大小,因为底层是数组,所以这是数组默认的大小。
桶最大值。
默认的负载因子(0.75)
table 真正存放数据的数组。
Map 存放数量的大小。
桶大小,可在初始化时显式指定。
负载因子,可在初始化时显式指定。
重点解释下负载因子:
由于给定的 HashMap 的容量大小是固定的,比如默认初始化:
Java基础_第17张图片
给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。
根据代码可以看到其实真正存放数据的是
transient Entry[] table = (Entry[]) EMPTY_TABLE;
这个数组,那么它又是如何定义的呢?
Java基础_第18张图片
Entry 是 HashMap 中的一个内部类,从他的成员变量很容易看出:
key 就是写入时的键。
value 自然就是值。
开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。
hash 存放的是当前 key 的 hashcode。
Java基础_第19张图片
Java基础_第20张图片
Java基础_第21张图片
1.7出现的问题:
当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。
Java基础_第22张图片
Java基础_第23张图片
和 1.7 大体上都差不多,还是有几个重要的区别:
TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。
HashEntry 修改为 Node。
Node 的核心组成其实也是和 1.7 中的 HashEntry 一样,存放的都是 key value hashcode next 等数据。
Java基础_第24张图片
Java基础_第25张图片
Java基础_第26张图片
从这两个核心方法(get/put)可以看出 1.8 中对大链表做了优化,修改为红黑树之后查询效率直接提高到了 O(logn)。
但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环
在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环。
Java基础_第27张图片
ConcurrentHashMap
ConcurrentHashMap 同样也分为 1.7 、1.8 版,两者在实现上略有不同。
Java基础_第28张图片
如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。
它的核心成员变量:Java基础_第29张图片
Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:Java基础_第30张图片
看看其中 HashEntry 的组成
Java基础_第31张图片
和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。
原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment
Java基础_第32张图片
Java基础_第33张图片
虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。
首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。
Java基础_第34张图片
尝试自旋获取锁。
如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功
Java基础_第35张图片
再结合图看看 put 的流程。
将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
最后会解除在 1 中所获取当前 Segment 的锁。
Java基础_第36张图片
get 逻辑比较简单:
只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁
1.8在数据结构上做了调整来提高查询效率:
Java基础_第37张图片
看起来是不是和 1.8 HashMap 结构类似?
其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性
Java基础_第38张图片
也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。
其中的 val next 都用了 volatile 修饰,保证了可见性。Java基础_第39张图片
根据 key 计算出 hashcode 。
判断是否需要进行初始化。
f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
如果都不满足,则利用 synchronized 锁写入数据。
如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。Java基础_第40张图片根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
如果是红黑树那就按照树的方式获取值。
就不满足那就按照链表的方式遍历获取值。
1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。
Java基础_第41张图片

4、String,StringBuilder,StringBuffer

1.三者在执行速度方面的比较:StringBuilder > StringBuffer > String
线程安全方面StringBuffer
2.String <(StringBuffer,StringBuilder)的原因
    String:字符串常量
    StringBuffer:字符串变量
    StringBuilder:字符串变量
    从上面的名字可以看到,String是“字符串常量”,也就是不可改变的对象。对于这句话的理解你可能会产生这样一个疑问 ,比如这段代码:
String s = “abcd”;
s = s+1;
System.out.print(s);// result : abcd1
    我们明明就是改变了String型的变量s的,为什么说是没有改变呢? 其实这是一种欺骗,JVM是这样解析这段代码的:首先创建对象s,赋予一个abcd,然后再创建一个新的对象s用来执行第二行代码,也就是说我们之前对象s并没有变化,所以我们说String类型是不可改变的对象了,由于这种机制,每当用String操作字符串时,实际上是在不断的创建新的对象,而原来的对象就会变为垃圾被GC回收掉,可想而知这样执行效率会有多底。
    而StringBuffer与StringBuilder就不一样了,他们是字符串变量,是可改变的对象,每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,这样就不会像String一样创建一些而外的对象进行操作了,当然速度就快了。

5、对象的深浅复制

6、API接口与SDI接口的区别

API 就是乐高积木的各种颗粒

SDK就是一大包乐高积木,里面有颗粒(API),有各种小工具

APP就是最后你搭出来的一艘飞船

7、如何获取一个本地服务器上可用的端口

getLocalPort获取的是应用服务器的端口,即该应用的实际端口,无论请求经过了多少代理,转发,getLocalPort只取最后的端口,也就是应用的端口。
getServerPort获取的是URL请求的端口,比如你的请求时127.0.0.1:8080,应用服务器的端口是80,那么getServerPort得到的端口是8080。而getLocalPort得到的是80
其他从request中获取的信息
System.out.println("request.getAuthType():" + request.getAuthType()); // 获取保护servlet的认证方案名(BASIC或SSL),未受保护的servlet返回的就是null
    System.out.println("request.getCharacterEncoding():" + request.getCharacterEncoding()); // 获取字符编码
    System.out.println("request.getContentLength():" + request.getContentLength()); // 返回请求体内容的长度
    System.out.println("request.getContentType():" + request.getContentType()); // 获取内容类型
    System.out.println("request.getContextPath():" + request.getContextPath()); // 获取上下文路径,就是"/"+工程名
    System.out.println("request.getLocalAddr():" + request.getLocalAddr()); // 获取应用服务器的IP地址
    System.out.println("request.getLocalName():" + request.getLocalName()); // 获取应用服务器的主机名
    System.out.println("request.getLocalPort():" + request.getLocalPort()); // 获取应用服务器的端口号
    System.out.println("request.getMethod():" + request.getMethod()); // 获取请求方式获取请求方式(GET与POST为主,也会有PUT、DELETE、INPUT)
    System.out.println("request.getPathInfo():" + request.getPathInfo());
    System.out.println("request.getPathTranslated():" + request.getPathTranslated());
    System.out.println("request.getProtocol():" + request.getProtocol()); // 获取客户端向服务端传送数据使用的协议名称
    System.out.println("request.getQueryString():" + request.getQueryString()); // 获取追加到Url后面的查询字符串
    System.out.println("request.getRemoteAddr():" + request.getRemoteAddr()); // 获取发出请求的客户端的IP地址
    System.out.println("request.getRemoteHost():" + request.getRemoteHost()); // 获取发出请求的客户端的主机名
    System.out.println("request.getRemotePort():" + request.getRemotePort()); // 获取发出请求的客户端的端口号
    System.out.println("request.getRemoteUser():" + request.getRemoteUser()); // 如果用户已经过认证,则返回发出请求的用户登录信息 
    System.out.println("request.getRequestedSessionId():" + request.getRequestedSessionId()); // 获取sessionId
    System.out.println("request.getRequestURI():" + request.getRequestURI()); // 获取"/"+工程名+请求路径
    System.out.println("request.getRequestURL():" + request.getRequestURL()); // 获取完整的请求地址,不带queryString
    System.out.println("request.getScheme():" + request.getScheme()); // 获取当前链接使用的协议,一般应用就是http,SSL返回https
    System.out.println("request.getServerName():" + request.getServerName()); // 获取URL请求的名字(以Ip请求就是Ip,以域名请求就是域名)
    System.out.println("request.getServerPort():" + request.getServerPort()); // 获取URL请求的端口号
    System.out.println("request.getServletPath():" + request.getServletPath()); // 获取请求路径
    System.out.println("request.isSecure():" + request.isSecure()); // 获取此请求是否使用安全协议(比如https)
}

8、Java的集合都有哪些,都有什么特点(信息平台),Set 和 List 区别?

set的变形:
hashset—>hashmap,LinkedHashSet—>LinkedHashMap,TreeSet—>TreeMap;
8.1、ArrayList跟LinkedList的底层实现原理,使用场景?linkedList与arrayList区别 适用场景,array list是如何扩容的

ArrayList 和 LinkedList 区别

1.ArrayList是实现了基于动态数组的数据结构,每个元素在内存中存储地址是连续的;LinkedList基于链表的数据结构
2.LinkedList查询用的遍历,AyyayList查询用的是数组下标,所以对于查询ArrayList性能高于LinkedList,所以检索性能显然高于通过for循环来查找每个元素的LinkedList。
3.元素插入删除的效率对比,要视插入删除的位置来分析,各有优劣:在列表首位添加(删除)元素,LnkedList性能远远优于ArrayList;在列表中间位置添加(删除)元素,总的来说位置靠前则LnkedList性能优于ArrayList,靠后则相反;在列表末尾位置添加(删除)元素,性能相差不大。

如果存取相同的数据,ArrayList 和 LinkedList 谁占用空间更大?Java集合,arraylist的扩容底层原理

答:https://www.cnblogs.com/kuoAT/p/6771653.html

8.2、Set 存的顺序是有序的吗?

   我们经常听说List是有序且可重复的,Set是无序且不重复的。这是一个误区,这里所说的顺序有两个概念,一是按照添加的顺序排列,二是按,照自然顺序a-z排列。Set并不是无序的传统所说的Set无序指的是HashSet,它不能保证元素的添加顺序,更不能保证自然顺序,而Set的其他实现类是可以实现这两种顺序的。

1,LinkedHashset : 保证元素添加的自然顺序

2,TreeSet : 保证元素的自然顺序

常见 Set 的实现有哪些?TreeSet 对存入对数据有什么要求呢?HashSet 的底层实现呢,TreeSet 底层源码有看过吗?
从内部结构来看,TreeMap 本质上就是一棵“红黑树”,而 TreeMap 的每个 Entry 就是该红黑树的一个节点。
HashSet 是不是线程安全的?为什么不是线程安全的?

hashset其实就是用hashmap实现的,所以是不安全的

9、java事件机制包括哪三个 部分?分别介绍

java事件机制包括三个部分:事件、事件监听器、事件源
1、事件。一般继承自java.util.EventObject类,封装了事件源对象及跟事件相关的信息。
 /** 
 * 事件类,用于封装事件源及一些与事件相关的参数. 
 */  
public class CusEvent extends EventObject {  
    private static final long serialVersionUID = 1L;  
    private Object source;//事件源  
      
    public CusEvent(Object source){  
        super(source);  
        this.source = source;  
    }  
  
    public Object getSource() {  
        return source;  
    }  
  
    public void setSource(Object source) {  
        this.source = source;  
    }  
}  
2、事件监听器。实现java.util.EventListener接口,注册在事件源上,当事件源的属性或状态改变时,取得相应的监听器调用其内部的回调方法。
 /** 
 * 事件监听器,实现java.util.EventListener接口。定义回调方法,将你想要做的事 
 * 放到这个方法下,因为事件源发生相应的事件时会调用这个方法。 
 */  
public class CusEventListener implements EventListener {  
      
    //事件发生后的回调方法  
    public void fireCusEvent(CusEvent e){  
        EventSourceObjecteObject = (EventSourceObject)e.getSource();  
        System.out.println("My name has been changed!");  
        System.out.println("I got a new name,named \""+eObject.getName()+"\"");    }  
}  
3、事件源。事件发生的地方,由于事件源的某项属性或状态发生了改变(比如BUTTON被单击、TEXTBOX的值发生改变等等)导致某项事件发生。换句话说就是生成了相应的事件对象。因为事件监听器要注册在事件源上,所以事件源类中应该要有盛装监听器的容器(List,Set等等)。
 /** 
 * 事件源. 
 */  
public class EventSourceObject {  
    private String name;  
    //监听器容器  
    private Set listener;  
    public EventSourceObject(){  
        this.listener = new HashSet();  
        this.name = "defaultname";  
    }  
    //给事件源注册监听器  
    public void addCusListener(CusEventListener cel){  
        this.listener.add(cel);  
    }  
    //当事件发生时,通知注册在该事件源上的所有监听器做出相应的反应(调用回调方法)  
    protected void notifies(){  
        CusEventListener cel = null;  
        Iterator iterator = this.listener.iterator();  
        while(iterator.hasNext()){  
            cel = iterator.next();  
            cel.fireCusEvent(new CusEvent(this));  
        }  
    }  
    public String getName() {  
        return name;  
    }  
    //模拟事件触发器,当成员变量name的值发生变化时,触发事件。  
    public void setName(String name) {  
        if(!this.name.equals(name)){  
            this.name = name;  
            notifies();  
        }        
    }  
}  
 public class MainTest {  
  
    /** 
     * @param args 
     */  
    public static void main(String[] args) {  
        EventSourceObject object = new EventSourceObject();  
        //注册监听器  
        object.addCusListener(new CusEventListener(){  
            @Override  
            public void fireCusEvent(CusEvent e) {  
                super.fireCusEvent(e);  
            }  
        });  
        //触发事件  
        object.setName("eric");  
    }  
} 

10、什么是反射机制?说说反射机制的作用。反射机制会不会有性能问题?

反射代码实现,点击这里

1 反射机制的基本概念
  JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
2 作用
1.在运行时判断任意一个对象所属的类。
2.在运行时构造任意一个类的对象。
3.在运行时判断任意一个类所具有的成员变量和方法。
4.在运行时调用任意一个对象的方法。
反射机制的优点就是可以实现动态创建对象和编译,体现出很大的灵活性
会影响
1、代码的验证防御逻辑过于复杂,本来这块验证时在链接阶段实现的,使用反射reflect时需要在运行时进行;
2、产生过多的临时对象,影响GC的消耗;
3、由于缺少上下文,导致不能进行更多的优化,如JIT;

11、异常机制

描述一下Java异常层次结构。
Java基础_第42张图片
Q:什么是检查异常,不受检查异常,运行时异常?并分别举例说明。finally块一定会执行吗?正常情况下,当在try块或catch块中遇到return语句时,finally语句块在方法返回之前还是之后被执行?try、catch、finally语句块的执行顺序。
A:
Thorwable类所有异常和错误的超类,有两个子类Error和Exception,分别表示错误和异常。
其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常,
这两种异常有很大的区别,也称之为不检查异常(Unchecked Exception)
和检查异常(Checked Exception)。

12、Object 类中的方法,

hashcode 和 equals 方法常用地方?

Java的基类Object提供了一些方法,其中equals()方法用于判断两个对象是否相等,hashCode()方法用于计算对象的哈希码。equals()和hashCode()都不是final方法,都可以被重写(overwrite)。

对象比较是否相同,Object toString 方法常用的地方,为什么要重写该方法,Object 的 hashcode 方法重写了,equals 方法要不要改?两个对象的 hashcode 相同,是否对象相同?equal() 相同呢?object有哪些方法

答:http://blog.csdn.net/u013812939/article/details/46799139

Object 的 hashcode 方法重写了,equals 方法要不要改?
个人观点:如果我们重写了equals方法的话,我们就必须重写hashcode方法,为什么equals()相等,那么hashCode()必须相等。因为,如果两个对象的equals()方法返回true,则它们在哈希表中只应该出现一次;如果hashCode()不相等,那么它们会被散列到表中不同的位置,哈希表中不止出现一次

如果只修改了hashcode的方法,equlas方法可以不必要修改

13、什么是多态?哪里体现了多态的概念?

Java 实现多态有 3 个必要条件:继承、重写和向上转型。只有满足这 3 个条件,开发人员才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而执行不同的行为。

    继承:在多态中必须存在有继承关系的子类和父类。
    重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
    向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。

14、Java中多态是怎么实现的

靠的是父类或接口的引用指向子类或实现类的对象,

调用的方法是内存中正在运行的那个对象的方法。
参考链接:https://blog.csdn.net/huangrunqing/article/details/51996424

15、重载重写的区别?转发和重定向的区别?Override和Overload的区别,分别用在什么场景

override和overload的意义:方法的重写Overriding和重载Overloading是Java多态性的不同表现。重写Overriding是父类与子类之间多态性的一种表现,重载Overloading是一个类中多态性的一种表现。
Overload:重载:表示同一个类中可以有多个名称相同的方法,但是这些方法的参数列表各不相同(即不同的参数类型,不同的参数个数,不同的参数顺序,)。overload可以改变返回值类型。
Override:重写:子类中出现与父类一模一样的方法时,会出现覆盖操作,被称作复写或重写;
区别:
override(重写)
   1、方法名、参数、返回值相同。
   2、子类方法不能缩小父类方法的访问权限。
   3、子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。
   4、存在于父类和子类之间。
   5、方法被定义为final不能被重写。
overload(重载)
  1、参数类型、个数、顺序至少有一个不相同。
  2、不能重载只有返回值不同的方法名。
  3、存在于父类和子类、同类中。

17、自己实现一个 List,(主要实现 add等常用方法)
18、从简单的生产者消费者模式设计到如何高效健壮实现等等。
19、两个Integer的引用对象传给一个swap方法在方法内部交换引用,返回后,两个引用的值是否会发现变化

不会
可以说Java中只有值传递。
具体分析----
Java内存模型简介:
  Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
  Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(可以与前面将的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成
如上总结:
  线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。在swap方法内部交换引用,只会交换线程的工作内存中持有的方法参数,而工作内存中的方法参数是主内存中变量的副本,因此执行这样的swap方法不会改变主内存中变量的指向

20、IO会阻塞吗?readLine是不是阻塞的

同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么,到底有什么区别?这个问题其实不同的人给出的答案都可能不同,比如wiki,就认为asynchronous IO和non-blocking IO是一个东西。这其实是因为不同的人的知识背景不同,并且在讨论这个问题的时候上下文(context)也不相同。所以,为了更好的回答这个问题,我先限定一下本文的上下文。
本文讨论的背景是Linux环境下的network IO。
本文最重要的参考文献是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models ”,Stevens在这节中详细说明了各种IO的特点和区别,如果英文够好的话,推荐直接阅读。Stevens的文风是有名的深入浅出,所以不用担心看不懂。本文中的流程图也是截取自参考文献。
Stevens在文章中一共比较了五种IO Model:
blocking IO
nonblocking IO
IO multiplexing
signal driven IO
asynchronous IO
由于signal driven IO在实际中并不常用,所以我这只提及剩下的四种IO Model。
再说一下IO发生时涉及的对象和步骤。
对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:
1 等待数据准备 (Waiting for the data to be ready)
2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
记住这两点很重要,因为这些IO Model的区别就是在两个阶段上各有不同的情况。
blocking IO
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
Java基础_第43张图片
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。
non-blocking IO
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
Java基础_第44张图片
从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,用户进程其实是需要不断的主动询问kernel数据好了没有。
IO multiplexing
IO multiplexing这个词可能有点陌生,但是如果我说select,epoll,大概就都能明白了。有些地方也称这种IO方式为event driven IO。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:
Java基础_第45张图片
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(多说一句。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
Asynchronous I/O
linux下的asynchronous IO其实用得很少。先看一下它的流程:
Java基础_第46张图片
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
到目前为止,已经将四个IO Model都介绍完了。现在回过头来回答最初的那几个问题:blocking和non-blocking的区别在哪,synchronous IO和asynchronous IO的区别在哪。
先回答最简单的这个:blocking vs non-blocking。前面的介绍中其实已经很明确的说明了这两者的区别。调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。Stevens给出的定义(其实是POSIX的定义)是这样子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。有人可能会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
各个IO Model的比较如图所示:
Java基础_第47张图片
经过上面的介绍,会发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。
最后,再举几个不是很恰当的例子来说明这四个IO Model:
有A,B,C,D四个人在钓鱼:
A用的是最老式的鱼竿,所以呢,得一直守着,等到鱼上钩了再拉杆;
B的鱼竿有个功能,能够显示是否有鱼上钩,所以呢,B就和旁边的MM聊天,隔会再看看有没有鱼上钩,有的话就迅速拉杆;
C用的鱼竿和B差不多,但他想了一个好办法,就是同时放好几根鱼竿,然后守在旁边,一旦有显示说鱼上钩了,它就将对应的鱼竿拉起来;
D是个有钱人,干脆雇了一个人帮他钓鱼,一旦那个人把鱼钓上来了,就给D发个短信。
原文:https://blog.csdn.net/historyasamirror/article/details/5778378
https://www.ibm.com/developerworks/cn/linux/l-cn-edntwk/

21、字符串的格式化方法 (20,21这两个问题问的太低级了)

String.format

22、 时间的格式化方法

一般常用格式化类DateFormat和SimpleDateFormat的format(Date time)方法进行格式化日期.

23、四则运算写代码
可以参考这个链接

24、 java有哪些容器(集合,tomcat也是一种容器)
Java基础_第48张图片
tomcat容器明细如下:
Server容器:一个StandardServer类实例就表示一个Server容器

Service容器:一个StandardService类实例就表示一个Service容器

Engine容器:一个StandardEngine类实例就表示一个Engine容器。

Host容器:一个StandardHost类实例就表示一个Host容器。

Context容器:一个StandardContext类实例就表示一个Context容器。

Wrapper容器:一个StandardWrapper类实例就表示一个Wrapper容器。
Java基础_第49张图片
25、类序列化时类的版本号的用途,如果没有指定一个版本号,系统是怎么处理的?如果加了字段会怎么样?
如果没有明确指定serialVersionUID,序列化的时候会根据字段和特定的算法生成一个serialVersionUID,当属性有变化时这个id发生了变化,所以反序列化的时候就会失败。抛出“本地classd的唯一id和流中class的唯一id不匹配

序列化定义:
序列化可以将一个java对象以二进制流的方式在网络中传输并且可以被持久化到数据库、文件系统中,反序列化则是可以把之前持久化在数据库或文件系统中的二进制数据以流的方式读取出来重新构造成一个和之前相同内容的java对象。
序列化作用:
第一种:用于将java对象状态储存起来,通常放到一个文件中,使下次需要用到的时候再读取到它之前的状态信息。
第二种:可以让java对象在网络中传输。
序列化的实现:
1)、需要序列化的类需要实现Serializable接口,该接口没有任何方法,只是标示该类对象可被序列化。
2)、序列化过程:使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态)
3)、反序列化过程:使用一个输入流(如:FileInputStream)来构造一个ObjectInputStream(对象流)对象,接着,使用ObjectInputStream对象的readObject(Object obj)方法就可以将参数为obj的对象读出(即获取其状态)
序列化的特点:
1)、如果一个类可被序列化,其子类也可以,如果该类有父类,则根据父类是否实现Serializable接口,实现了则父类对象字段可以序列化,没实现,则父类对象字段不能被序列化。
2)、声明为transient类型的成员数据不能被序列化。transient代表对象的临时数据;
3)、当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;

26、java的反射是如何实现的
点击这里
27、Java I/O底层细节(注意是底层细节,而不是怎么用)

答:http://blog.csdn.net/chuntiandejiaobu10/article/details/52458804

28、序列化速度慢,使用fastjson转换就快了,那么为什么慢了,fastjson快在哪里

1、自行编写类似StringBuilder的工具类SerializeWriter。 
把java对象序列化成json文本,是不可能使用字符串直接拼接的,因为这样性能很差。比字符串拼接更好的办法是使用java.lang.StringBuilder。StringBuilder虽然速度很好了,但还能够进一步提升性能的,fastjson中提供了一个类似StringBuilder的类com.alibaba.fastjson.serializer.SerializeWriter。 

SerializeWriter提供一些针对性的方法减少数组越界检查。例如public void writeIntAndChar(int i, char c) {},这样的方法一次性把两个值写到buf中去,能够减少一次越界检查。目前SerializeWriter还有一些关键的方法能够减少越界检查的,我还没实现。也就是说,如果实现了,能够进一步提升serialize的性能。 

2、使用ThreadLocal来缓存buf。 
这个办法能够减少对象分配和gc,从而提升性能。SerializeWriter中包含了一个char[] buf,每序列化一次,都要做一次分配,使用ThreadLocal优化,能够提升性能。 

3、使用asm避免反射 
获取java bean的属性值,需要调用反射,fastjson引入了asm的来避免反射导致的开销。fastjson内置的asm是基于objectweb asm 3.3.1改造的,只保留必要的部分,fastjson asm部分不到1000行代码,引入了asm的同时不导致大小变大太多。 

使用一个特殊的IdentityHashMap优化性能。 
fastjson对每种类型使用一种serializer,于是就存在class -> JavaBeanSerizlier的映射。fastjson使用IdentityHashMap而不是HashMap,避免equals操作。我们知道HashMap的算法的transfer操作,并发时可能导致死循环,但是ConcurrentHashMap比HashMap系列会慢,因为其使用volatile和lock。fastjson自己实现了一个特别的IdentityHashMap,去掉transfer操作的IdentityHashMap,能够在并发时工作,但是不会导致死循环。 

5、缺省启用sort field输出 
json的object是一种key/value结构,正常的hashmap是无序的,fastjson缺省是排序输出的,这是为deserialize优化做准备。 

6、集成jdk实现的一些优化算法 
在优化fastjson的过程中,参考了jdk内部实现的算法,比如int to char[]算法等等。 

29、xml在jsp中的类型,.jsp中forward和redirect区别

xml在jsp中的类型
根据JSTL标签的功能,
JSTL标签可以分为以下JSTL标签库组,
可以在创建JSP页面中使用 -
核心标签格式化标签
SQL标签
XML标签
JSTL函数
.jsp中forward和redirect区别
假设你去办理某个执照
重定向:你先去了A局,A局的人说:“这个事情不归我们管,去B局”,然后,你就从A退了出来,自己乘车去了B局。
转发:你先去了A局,A局看了以后,知道这个事情其实应该B局来管,但是他没有把你退回来,而是让你坐一会儿,自己到后面办公室联系了B的人,让他们办好后,送了过来。

30、接口和抽象的区别

接口
接口是抽象方法的集合。如果一个类实现了某个接口,那么它就继承了这个接口的抽象方法。这就像契约模式,如果实现了这个接口,那么就必须确保使用这些方法。接口只是一种形式,接口自身不能做任何事情。
Java基础_第50张图片
如果你拥有一些方法并且想让它们中的一些有默认实现,那么使用抽象类吧。
如果你想实现多重继承,那么你必须使用接口。由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。因此你就可以使用接口来解决它。
如果基本功能在不断改变,那么就需要使用抽象类。如果不断改变基本功能并且使用接口,那么就需要改变所有实现了该接口的类。

31、触发器的作用

点击这里:

你可能感兴趣的:(职场@面试)