HashMap的底层使用数组+链表/红黑树实现。html
transient Node<K,V>[] table;这表示HashMap是Node数组构成,其中Node类的实现以下,能够看出这其实就是个链表,链表的每一个结点是一个<K,V>映射。 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; }
HashMap的每一个下标都存放了一条链表。java
常量/变量定义node
1 /* 常量定义 */ 2 3 // 初始容量为16 4 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 5 // 最大容量 6 static final int MAXIMUM_CAPACITY = 1 << 30; 7 // 负载因子,当键值对个数达到DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR会触发resize扩容 8 static final float DEFAULT_LOAD_FACTOR = 0.75f; 9 // 当链表长度大于8,且数组长度大于MIN_TREEIFY_CAPACITY,就会转为红黑树 10 static final int TREEIFY_THRESHOLD = 8; 11 // 当resize时候发现链表长度小于6时,从红黑树退化为链表 12 static final int UNTREEIFY_THRESHOLD = 6; 13 // 在要将链表转为红黑树以前,再进行一次判断,若数组容量小于该值,则用resize扩容,放弃转为红黑树 14 // 主要是为了在创建Map的初期,放置过多键值对进入同一个数组下标中,而致使没必要要的链表->红黑树的转化,此时扩容便可,可有效减小冲突 15 static final int MIN_TREEIFY_CAPACITY = 64; 16 17 /* 变量定义 */ 18 19 // 键值对的个数 20 transient int size; 21 // 键值对的个数大于该值时候,会触发扩容 22 int threshold; 23 // 非线程安全的集合类中几乎都有这个变量的影子,每次结构被修改都会更新该值,表示被修改的次数 24 transient int modCount;
关于modCount的做用见这篇blogmysql
在一个迭代器初始的时候会赋予它调用这个迭代器的对象的modCount,若是在迭代器遍历的过程当中,一旦发现这个对象的modCount和迭代器中存储的modCount不同那就抛异常。
Fail-Fast机制:java.util.HashMap不是线程安全的,所以若是在使用迭代器的过程当中有其余线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是经过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增长这个值,那么在迭代器初始化过程当中会将这个值赋给迭代器的expectedModCount。在迭代过程当中,判断modCount跟expectedModCount是否相等,若是不相等就表示已经有其余线程修改了Map。程序员
注意初始容量和扩容后的容量都必须是2的次幂,为何呢?算法
hash方法spring
先看散列方法sql
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
HashMap的散列方法如上,其实就是将hash值的高16位和低16位异或,咱们将立刻看到hash在与n - 1相与的时候,高位的信息也被考虑了,能使碰撞的几率减少,散列得更均匀。数据库
在JDK 8中,HashMap的putVal方法中有这么一句编程
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
关键就是这句(n - 1) & hash
,这行代码是把待插入的结点散列到数组中某个下标中,其中hash就是经过上面的方法的获得的,为待插入Node的key的hash值,n是table的容量即table.length
,2的次幂用二进制表示的话,只有最高位为1,其他为都是0。减去1,恰好就反了过来。好比16的二进制表示为10000,减去1后的二进制表示为01111,除了最高位其他各位都是1,保证了在相与时,能够使得散列值分布得更均匀(由于若是某位为0好比1011,那么结点永远不会被散列到1111这个位置),且当n为2的次幂时候有(n - 1) & hash == hash % n
, 举个例子,好比hash等于6时候,01111和00110相与就是00110,hash等于16时,相与就等于0了,多举几个例子即可以验证这一结论。最后来回答为何HashMap的容量要始终保持2的次幂
注意table.length是数组的容量,而transient int size
表示存入Map中的键值对数。
int threshold
表示临界值,当键值对的个数大于临界值,就会扩容。threshold的更新是由下面的方法完成的。
static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
该方法返回大于等于cap的最小的二次幂数值。好比cap为16,就返回16,cap为17就返回32。
put方法
put方法主要由putVal方法实现:
tab[i = (n - 1) & hash]
处新建一个结点;get方法
get方法由getNode方法实现:
remove方法的流程大体和get方法相似。
HashMap的扩容,resize()过程?
newCap = oldCap << 1
resize方法中有这么一句,说明是扩容后数组大小是原数组的两倍。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // 若是数组中只有一个元素,即只有一个头结点,从新哈希到新数组的某个下标 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order // 数组下标处的链表长度大于1,非红黑树的状况 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; // oldCap是2的次幂,最高位是1,其他为是0,哈希值和其相与,根据哈希值的最高位是1仍是0,链表被拆分红两条,哈希值最高位是0分到loHead。 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 哈希值最高位是1分到hiHead else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; // loHead挂到新数组[原下标]处; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; // hiHead挂到新数组中[原下标+oldCap]处 newTab[j + oldCap] = hiHead; } } } } } return newTab;
举个例子,好比oldCap是16,二进制表示是10000,hash值的后五位和oldCap相与,由于oldCap的最高位(从右往左数的第5位)是1其他位是0,所以hash值的该位是0的全部元素被分到一条链表,挂到新数组中原下标处,hash值该位为1的被分到另一条链表,挂到新数组中原下标+oldCap处。举个例子:桶0中的元素其hash值后五位是0XXXX的就被分到桶0种,其hash值后五位是1XXXX就被分到桶4中。
Java中的全部异常都是Throwable的子类对象,Error类和Exception类是Throwable类的两个直接子类。
Error:包括一些严重的、程序不能处理的系统错误类。这些错误通常不是程序形成的,好比StackOverflowError和OutOfMemoryError。
Exception:异常分为运行时异常和检查型异常。
首先接口Collection和Map是平级的,Map没有实现Collection。
Map的实现类常见有HashMap、TreeMap、LinkedHashMap和HashTable等。其中HashMap使用散列法实现,低层是数组,采用链地址法解决哈希冲突,每一个数组的下标都是一条链表,当长度超过8时,转换成红黑树。TreeMap使用红黑树实现,能够按照键进行排序。LinkedHashMap的实现综合了HashMap和双向链表,可保证以插入时的顺序(或访问顺序,LRU的实现)进行迭代。HashTable和HashMap比,前者是线程安全的,后者不是线程安全的。HashTable的键或者值不容许null,HashMap容许。
Collection的实现类常见的有List、Set和Queue。List的实现类有ArrayList和LinkedList以及Vector等,ArrayList就是一个可扩容的对象数组,LinkedList是一个双向链表。Vector是线程安全的(ArrayList不是线程安全的)。Set的里的元素不可重复,实现类常见的有HashSet、TreeSet、LinkedHashSet等,HashSet的实现基于HashMap,实际上就是HashMap中的Key,一样TreeSet低层由TreeMap实现,LinkedHashSet低层由LinkedHashMap实现。Queue的实现类有LinkedList,能够用做栈、队列和双向队列,另外还有PriorityQueue是基于堆的优先队列。
反射:容许任意一个类在运行时获取自身的类信息,而且能够操做这个类的方法和属性。这种动态获取类信息和动态调用对象方法的功能称为Java的反射机制。
反射的核心是JVM在运行时才动态加载类或调用方法/访问属性。它不须要事先(写代码的时候或编译期)知道运行对象是谁,如Class.ForName()
根本就没有指定某个特定的类,彻底由你传入的类全限定名决定,而经过new的方式你是知道运行时对象是哪一个类的。 反射避免了将程序“写死”。
反射能够下降程序耦合性,提升程序的灵活性。new是形成紧耦合的一大缘由。好比下面的工厂方法中,根据水果类型决定返回哪个类。
public class FruitFactory { public Fruit getFruit(String type) { Fruit fruit = null; if ("Apple".equals(type)) { fruit = new Apple(); } else if ("Banana".equals(type)) { fruit = new Banana(); } else if ("Orange".equals(type)) { fruit = new Orange(); } return fruit; } } class Fruit {} class Banana extends Fruit {} class Orange extends Fruit {} class Apple extends Fruit {}
可是咱们事先并不知道以后会有哪些类,好比新增了Mango,就须要在if-else中新增;若是之后不须要Banana了就须要从if-else中删除。这就是说只要子类变更了,咱们必须在工厂类进行修改,而后再编译。若是用反射呢?
public class FruitFactory { public Fruit getFruit(String type) { Fruit fruit = null; try { fruit = (Fruit) Class.forName(type).newInstance(); } catch (Exception e) { e.printStackTrace(); } return fruit; } } class Fruit {} class Banana extends Fruit {} class Orange extends Fruit {} class Apple extends Fruit {}
若是再将子类的全限定名存放在配置文件中。
class-type=com.fruit.Apple
那么无论新增多少子类,根据不一样的场景只需修改文件就行了,上面的代码无需修改代码、从新编译,就能正确运行。
哪些地方用到了反射?举几个例子
Java实现多态有三个必要条件:继承、重写、向上转型。
public class OOP { public static void main(String[] args) { /* * 1. Cat继承了Animal * 2. Cat重写了Animal的eat方法 * 3. 父类Animal的引用指向了子类Cat。 * 在编译期间其静态类型为Animal;在运行期间其实际类型为Cat,所以animal.eat()将选择Cat的eat方法而不是其余子类的eat方法 */ Animal animal = new Cat(); printEating(animal); } public static void printEating(Animal animal) { animal.eat(); } } abstract class Animal { abstract void eat(); } class Cat extends Animal { @Override void eat() { System.out.println("Cat eating..."); } } class Dog extends Animal { @Override void eat() { System.out.println("Dog eating..."); } }
对于不想进行序列化的变量,使用transient关键字修饰。功能是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。
== 对于基本类型,比较值是否相等,对于对象,比较的是两个对象的地址是否相同,便是否是指相同一个对象。
equals的默认实现实际上使用了==来比较两个对象是否相等,可是像Integer、String这些类对equals方法进行了重写,比较的是两个对象的内容是否相等。
对于Integer,若是依然坚持使用==来比较,有一些要注意的地方。对于[-128,127]区间里的数,有一个缓存。所以
Integer a = 127; Integer b = 127; System.out.println(a == b); // true Integer a = 128; Integer b = 128; System.out.println(a == b); // false // 不过采用new的方式,a在堆中,这里打印false Integer a = new Integer(127); Integer b = 127; System.out.println(a == b);
对于String,由于它有一个常量池。因此
String a = "gg" + "rr"; String b = "ggrr"; System.out.println(a == b); // true // 固然牵涉到new的话,该对象就在堆上建立了,因此这里打印false String a = "gg" + "rr"; String b = new String("ggrr"); System.out.println(a == b);
本质是考察Java反射,由于要实现一个通用的程序。实现可能根本不知道该类有哪些字段,因此不能经过get和set等方法来获取键-值。使用反射的getDeclaredFields()能够得到其声明的字段。若是字段是private的,须要调用该字段的f.setAccessible(true);
,才能读取和修改该字段。
import java.lang.reflect.Field; import java.util.HashMap; public class Object2Json { public static class Person { private int age; private String name; public Person(int age, String name) { this.age = age; this.name = name; } } public static void main(String[] args) throws IllegalAccessException { Person p = new Person(18, "Bob"); Class<?> classPerson = p.getClass(); Field[] fields = classPerson.getDeclaredFields(); HashMap<String, String> map = new HashMap<>(); for (Field f: fields) { // 对于private字段要先设置accessible为true f.setAccessible(true); map.put(String.valueOf(f.getName()), String.valueOf(f.get(p))); } System.out.println(map); } }
获得了map,再弄成JSON标准格式就行了。
@Test public void fun2() throws SQLException, ClassNotFoundException { // 1. 注册驱动 Class.forName("com.mysql.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=utf-8"; // 2.创建链接 Connection connection = DriverManager.getConnection(url, "root", "admin"); // 3. 执行sql语句使用的Statement或者PreparedStatment Statement statement = connection.createStatement(); String sql = "select * from stu;"; ResultSet resultSet = statement.executeQuery(sql); while (resultSet.next()) { // 第一列是id,因此从第二行开始 String name = resultSet.getString(2); // 能够传入列的索引,1表明第一行,索引不是从0开始 int age = resultSet.getInt(3); String gender = resultSet.getString(4); System.out.println("学生姓名:" + name + " | 年龄:" + age + " | 性别:" + gender); } // 关闭结果集 resultSet.close(); // 关闭statemenet statement.close(); // 关闭链接 connection.close(); }
ResultSet维持一个指向当前行记录的cursor(游标)指针
因为JDBC默认自动提交事务,每执行一个update ,delete或者insert的时候都会自动提交到数据库,没法回滚事务。因此若须要实现事务的回滚,要指定setAutoCommit(false)
。
true
:sql命令的提交(commit)由驱动程序负责false
:sql命令的提交由应用程序负责,程序必须调用commit或者rollback方法JDBC操做事务的格式以下,在捕获异常中进行事务的回滚。
就普通的实现方法来看。
public class SingletonImp { // 饿汉模式 private static SingletonImp singletonImp = new SingletonImp(); // 私有化(private)该类的构造函数 private SingletonImp() { } public static SingletonImp getInstance() { return singletonImp; } }
饿汉模式:线程安全,不能延迟加载
public class SingletonImp4 { private static volatile SingletonImp4 singletonImp4; private SingletonImp4() {} public static SingletonImp4 getInstance() { if (singletonImp4 == null) { synchronized (SingletonImp4.class) { if (singletonImp4 == null) { singletonImp4 = new SingletonImp4(); } } } return singletonImp4; } }
双重检测锁+volatile禁止语义重排。由于singletonImp4 = new SingletonImp4();
不是原子操做。
public class SingletonImp6 { private SingletonImp6() {} // 专门用于建立Singleton的静态类 private static class Nested { private static SingletonImp6 singletonImp6 = new SingletonImp6(); } public static SingletonImp6 getInstance() { return Nested.singletonImp6; } }
静态内部类,能够实现延迟加载。
最推荐的是单一元素枚举实现单例。
public enum Singleton { INSTANCE; public void anyOtherMethod() {} }
其中,先序、中序、后序遍历属于深度优先搜索(DFS),层序遍历属于广度优先搜索(BFS)
平衡二叉树首先是一棵二叉查找树,其次它须要知足其左右两棵子树的高度之差不超过1,且子树也必须是平衡二叉树,换句话说对于平衡二叉树的每一个结点,要求其左右子树高度之差都不超过1。
二叉查找树在最坏状况下,退化成链表,查找时间从平均O(lg n)降到O(n),平衡二叉树使树的结构更加平衡,提升了查找的效率;可是因为插入和删除后须要从新恢复树的平衡,因此插入和删除会慢一些。
应用场景好比在HashMap中用到了红黑树(平衡二叉树的特例),数据库索引中的B+树等。
应用场景:数组适合读多写少、事先知道元素大概个数的状况;链表适合写多读少的状况。
O(N^2)
最差O(N^2)
,空间复杂度O(1)
O(N lgN)
最差O(N^2)
,基于递归的实现因为用到了系统栈,因此平均状况下空间复杂度为O(lgN)
排序中所说的稳定是指,对于两个相同的元素,在排序后其相对位置没有发生变化。
常见的稳定排序有,冒泡、插入、归并、基数排序。选择、希尔、快排、堆排序都不是稳定的。
hash(key) = a * key + b
解决哈希冲突的方法:
堆排序使用了最大堆/最小堆,拿数组升序排序来讲,须要创建一个最大堆,基于数组实现的二叉堆能够看做一棵彻底二叉树,其知足堆中每一个父结点它左右两个结点值都大,且堆顶的元素最大。
每次调整堆的平均时间为O(lg N),所以对大小为N的数组排序,时间复杂度最差和平均都 O(N lg N).
快排序在平均状况下,比绝大多数排序算法都快些。不少编程语言的sort默认使用快排,像Java的Array.sort()就采用了双轴快速排序 。堆排序使用堆实现,空间复杂度只有O(1)。堆排序使用堆的结构,能以O(1)的时间得到最大/最小值,在处理TOP K问题时很方便,另外堆还能够实现优先队列。
时间复杂度:
空间复杂度:
放一张神图
c.next = a.next; a.next = c; c.prev = a; // 若是a不是最后一个结点,就有下面一句 c.next.prev = c;
GET和POST本质都是TCP链接。不过GET产生一个TCP数据包;POST产生两个TCP数据包。
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200 OK(返回数据);
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 OK(返回数据)。
三次握手
四次挥手
两次握手的话,只要服务端发出确认就创建链接了。有一种状况是客户端发出了两次链接请求,但因为某种缘由,使得第一次请求被滞留了。第二次请求先到达后创建链接成功,此后第一次请求终于到达,这是一个失效的请求了,服务端觉得这是一个新的请求因而赞成创建链接,可是此时客户端不搭理服务端,服务端一直处于等待状态,这样就浪费了资源。假设采用三次握手,因为服务端还须要等待客户端的确认,若客户端没有确认,服务端就能够认为客户端没有想要创建链接的意思,因而此次链接不会生效。
由于第四次握手客户端发送ACK确认后,有可能丢包了,致使服务端没有收到,服务端就会再次发送FIN = 1,若是客户端不等待当即CLOSED,客户端就不能对服务端的FIN = 1进行确认。等待的目的就是为了能在服务端再次发送FIN = 1时候能进行确认。若是在2MSL内客户端都没有收到服务端的任何消息,便认为服务端收到了确认。此时能够结束TCP链接。
好比网上购物,每一个用户有本身的购物车,当点击下单时,因为HTTP协议无状态,并不知道是哪一个用户操做的,因此服务端要为特定的用户建立特定的Session,用于标识这个用户,而且跟踪用户。
Session原理:浏览器第一次访问服务器时,服务器会响应一个cookie给浏览器。这个cookie记录的就是sessionId,以后每次访问携带着这个sessionId,服务器里查询该sessionId,即可以识别并跟踪特定的用户了。
Cookie原理:第一次访问服务器,服务器响应时,要求浏览器记住一个信息。以后浏览器每次访问服务器时候,携带第一次记住的信息访问。至关于服务器识别客户端的一个通行证。Cookie不可跨域,浏览览器判断一个网站是否能操做另外一个网站Cookie的依据是域名。Google与Baidu的域名不同,所以Google不能操做Baidu的Cookie,换句话说Google只能操做Google的Cookie。
即OSI参考模型。
还有一种TCP/IP五层模型,就是把应用层、表示层、会话层统一归到应用层。借用一张图。
网络层是针对主机与主机之间的服务。而传输层针对的是不一样主机进程之间的通讯。传输层协议将应用进程的消息传送到网络层,可是它并不涉及消息是怎么在网络层之间传送(这部分是由网络层的路由选择完成的)。网络层真正负责将数据包从源IP地址转发到目标IP地址,而传输层负责将数据包再递交给主机中对应端口的进程。
打个比方。房子A中的人要向房子B中的人写信。房子中都有专门负责将主人写好的信投递到邮箱,以及从邮箱接收信件后交到主人手中的管家。那么:
以上只是我的理解,若有误请联系更正。
可靠传输是指
TCP如何实现可靠传输:
当TCP链接创建以后,应用程序就可以使用该链接进行数据收发。应用程序将数据提交给TCP,TCP将数据放入本身的缓存,数据会被当作字节流并进行分段,而后加上TCP头部并提交给网络层。再加上IP头后被网络层提交给到目的主机,目的主机的IP层会将分组提交给TCP,TCP根据报文段的头部信息找到相应的socket,并将报文段提交给该socket,socket是和应用关联的,因而数据就提交给了应用。
对于UDP会简单些,UDP面向报文段。传输层加上UDP头部递交给网络层,再加上IP头部经路由转发到目的主机,目的主机将分组提交给UDP,UDP根据头部信息找到相应的socket,并将报文段提交给该socket,socket是和应用关联的,因而数据就提交给了应用。
序号和确认号是实现可靠传输的关键。
通讯双方经过序号和确认号,来判断数据是否丢失、是否按顺序到达、是否冗余等,以此决定要不要进行重传丢失的分组或丢弃冗余的分组。换句话说,由于有了序号、确认号和重传机制,保证了数据不丢失、不重复、有序到达。
当在浏览器输入网址www.baidu.com并敲下回车后:
HTTP协议是基于TCP协议的,客户端向服务端发送一个HTTP请求时,须要先与服务端创建TCP链接(三次握手),握手成功之后才能进行数据交互。
HTTP请求的报文格式:
HTTP响应的报文格式:
常见的状态码有:
synchronized对内置锁引入了偏向锁、轻量级锁、自旋锁、锁消除等优化。使得性能和重入锁差很少了。
首先要搞明白在I/O中的同步、异步、阻塞、非阻塞是什么意思。
同步I/O。由用户进程本身处理I/O的读写,处理过程当中不能作其余事。须要主动去询问I/O状态。
异步I/O。由系统内核完成I/O操做,完成后系统会通知用户进程。
阻塞。I/O请求操做须要的条件不知足,请求操做一直等待,直到条件知足。
非阻塞。 I/O请求操做须要的条件不知足,会当即返回一个标志,而不会一直等待。
如今来看BIO、NIO、AIO的区别。
BIO:同步并阻塞。用户进程在发起一个I/O请求后,必须等待I/O准备就绪,I/O操做也由本身来处理,在IO操做未完成以前,用户进程必须等待。
NIO:同步非阻塞。用户进程发起一个I/O请求后可当即返回去作其余任务,当I/O准备就绪时它会收到通知。接着由这个线程自行进行I/O操做,I/O操做自己仍是同步的。
AIO:异步非阻塞。用户进程发起一个I/O操做之后可当即返回去作其余任务,真正的I/O操做由内核完成后通知用户进程。
NIO和AIO的不一样:NIO是操做系统通知用户进程I/O已经准备就绪,由用户进程自行完成I/O操做;AIO是操做系统完成I/O后通知用户进程。
BIO是为每个客户端链接开启一个线程,简单说就是一个链接一个线程。
NIO主要组件有Seletor、Channel、Buffer,数据须要经过BUffer包装后才能使用Channel进行读取和写入。一个Selector能够由一个线程管理,每个Channel可看做一个客户端链接。一个Selector能够监听多个Channel,即便用一个或极少数的线程来管理大量的客户端链接。当与客户端链接的数据没有准备好时,Selector处于等待状态,一旦某个Channel的准备好了数据,Selector就能当即获得通知。
先使用synchronized实现。PrintOdd用于打印奇数;PrintEven用于打印偶数。核心就是判断当前count若是是奇数,就让PrintEven阻塞,PrintOdd打印后唤醒在lock对象上等待的PrintEven而且释放锁。此时PrintEven得到锁打印偶数再唤醒PrintOdd,两个线程如此交替唤醒对方就实现了交替打印奇偶数。
public class PrintOddEven { private static final Object lock = new Object(); private static int count = 1; static class PrintOdd implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { synchronized (lock) { try { while ((count & 1) != 1) { lock.wait(); } System.out.println(Thread.currentThread().getName() + " " +count); count++; lock.notify(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } static class PrintEven implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { synchronized (lock) { try { while ((count & 1) != 0) { lock.wait(); } System.out.println(Thread.currentThread().getName() + " " +count); count++; lock.notify(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } public static void main(String[] args) { new Thread(new PrintOdd()).start(); new Thread(new PrintEven()).start(); } }
进程是资源分配的最小单位,线程是程序执行的最小单位。 进程是线程的容器,即进程里面能够容纳多个线程,多个线程之间能够共享数据。
是指两个或两个以上的线程在执行过程当中,互相占用着对方想要的资源但都不释放,形成了互相等待,结果线程都没法向前推动。
死锁的检测:能够采用等待图(wait-for gragh)。采用深度优先搜索的算法实现,若是图中有环路就说明存在死锁。
解决死锁:
协同式线程调度:线程的执行时间以及线程的切换都是由线程自己来控制,线程把本身的任务执行完后,主动通知系统切换到另外一个线程。优势是没有线程安全的问题,缺点是线程执行的时间不可控,可能由于某一个线程不让出CPU,而致使整个程序被阻塞。
抢占式调度模式:线程的执行时间和切换都是由系统来分配和控制的。不过能够经过设置线程优先级,让优先级高的线程优先占用CPU。
Java虚拟机默认采用抢占式调度模型。
JDK 7中使用的是分段锁,内部分红了16个Segment即分段,每一个分段能够看做是一个小型的HashMap,每次put只会锁定一个分段,下降了锁的粒度:
多线程put的时候,只要被加入的键值不属于 同一个分段,就能够作到真正的并行put。对不一样的Segment则无需考虑线程同步,对于同一个Segment的操做才需考虑。
JDK 8中使用了CAS+synchronized保证线程安全,也采起了数组+链表/红黑树的结构。
put时使用synchronized锁住了桶中链表的头结点。
数组的扩容,被问到了我在看吧.....我只知道多个线程能够协助数据的迁移。
有这么一个问题,ConcurrentHashMap,有三个线程,A先put触发了扩容,扩容时间很长,此时B也put会怎么样?此时C调用get方法会怎么样?C读取到的元素是旧桶中的元素仍是新桶中的
A先触发扩容,ConcurrentHashMap迁移是在锁定旧桶的前提下进行迁移的,并无去锁定新桶。
对于共享变量,通常采起同步的方式保证线程安全。而ThreadLocal是为每个线程都提供了一个线程内的局部变量,每一个线程只能访问到属于它的副本。
实现原理,下面是set和get的实现
// set方法 public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } // 上面的getMap方法 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } // get方法 public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
sleep() 容许指定以毫秒为单位的一段时间做为参数,它使得线程在指定的时间内进入阻塞状态,不能获得CPU 时间,指定的时间一过,线程从新进入可执行状态。调用sleep后不会释放锁。
yield() 使得线程放弃CPU执行时间,可是不使线程阻塞,线程从运行状态进入就绪状态,随时可能再次分得 CPU 时间。有可能当某个线程调用了yield()方法暂停以后进入就绪状态,它又立刻抢占了CPU的执行权,继续执行。
wait()是Object的方法,会使线程进入阻塞状态,和sleep不一样,wait会同时释放锁。wait/notify在调用以前必须先得到对象的锁。
run方法只是一个普通方法调用,仍是在调用它的线程里执行。
start才是开启线程的方法,run方法里面的逻辑会在新开的线程中执行。
前三个是线程私有的,后两个是线程共享的。
字节码解释器经过改变程序计数器的值来决定下一条要执行的指令,为了在线程切换后每条线程都能正确回到上次执行的位置,由于每条线程都有本身的程序计数器。
虚拟机栈是存放Java方法内存模型,每一个方法在执行时都会建立一个栈帧,用于存储局部变量表、操做数栈、动态连接、方法返回地址等信息。方法的开始调用对应着栈帧的进栈,方法执行完成对应这栈帧的出栈。位于栈顶被称为“当前方法”。
本地方法栈和虚拟机栈相似,不过虚拟机栈针对Java方法,而本地方法栈针对Native方法。
Java堆。对象实例被分配内存的地方,也是垃圾回收的主要区域。
方法区。存放被虚拟机加载的类信息、常量(final)、静态变量(static)、即时编译期编译后的代码。方法区是用永久代实现的,这个区域的内存回收目标主要是针对常量池的回收和类型的卸载。运行时常量池是方法区的一部分,运行时常量池是Class文件中的一项信息,存放编译期生成的各类字面量和符号引用。
Java堆分为新生代和老年代。在新生代又被划分为Eden区,From Sruvivor和To Survivor区,比例是8:1:1,因此新生代可用空间其实只有其容量的90%。对象优先被分配在Eden区。
发生在新生代的GC称为Minor GC,当Eden区被占满了而又须要分配内存时,会发生一次Minor GC,通常使用复制算法,将Eden和From Survivor区中还存活的对象一块儿复制到To Survivor区中,而后一次性清理掉Eden和From Survivor中的内存,使用复制算法不会产生碎片。
老年代的GC称为Full GC或者Major GC:
在通过可达性分析后,到GC Roots不可达的对象能够被回收(但并非必定会被回收,至少要通过两次标记),此时对象被第一次标记,并进行一次判断:
所以finalize方法被调用后,对象不必定会被回收。
先说类加载器。
在Java中,系统提供了三种类加载器。
固然用户也能够自定义类加载器。
再说类加载的过程。
主要是如下几个过程:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
加载
验证
准备。
为类变量(static)分配内存并设置默认值。好比static int a = 123在准备阶段的默认值是0,可是若是有final修饰,在准备阶段就会被赋值为123了。
解析。
将常量池中的符号引用替换成直接引用的过程。包括类或接口、字段、类方法、接口方法的解析。
初始化。
按照程序员的计划初始化类变量。如static int a = 123,在准备阶段a的值被设置为默认的0,而到了初始化阶段其值被设置为123。
类加载器之间知足双亲委派模型,即:除了顶层的启动类加载器外,其余全部类加载器都必需要本身的父类加载器。当一个类加载器收到类加载请求时,本身首先不会去加载这个类,而是不断把这个请求委派给父类加载器完成,所以全部的加载请求最终都传递给了顶层的启动类加载器。只有当父类没法完成这个加载请求时,子类加载器才会尝试本身去加载。
双亲委派模型的好处?使得Java的类随着它的类加载器一块儿具有了一种带有优先级的层次关系。Java的Object类是全部类的父类,所以不管哪一个类加载器都会加载这个类,由于双亲委派模型,全部的加载请求都委派给了顶层的启动类加载器进行加载。因此Object类在任何类加载器环境中都是同一个类。
如何打破双亲委派模型?使用OSGi能够打破。OSGI(Open Services Gateway Initiative),或者通俗点说JAVA动态模块系统。能够实现代码热替换、模块热部署。在OSGi环境下,类加载器再也不是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。
CMS(Concurrent Mark Sweep) 从名字能够看出是能够进行并发标记-清除的垃圾收集器。针对老年代的垃圾收集器,目的是尽量地减小用户线程的停顿时间。
收集过程有以下几个步骤:
CMS的缺点:
CMS比较相似适合用户交互的场景,能够得到较小的响应时间。
G1(Garbage First),有以下特色:
在使用G1收集器时,Java堆的内存划分为多个大小相等的独立区域,新生代和老年代再也不是物理隔离。G1跟踪各个区域的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据容许的收集时间,优先回收价值最大的区域。
G1的收集过程和CMS有些相似:
G1的优点:可预测的停顿;实时性较强,大幅减小了长时间的gc;必定程度的高吞吐量。
由上一个问题可总结出CMS和G1的区别:
GC进行时必须暂停全部Java执行线程,这被称为Stop The World。为何要停顿呢?由于可达性分析过程当中不容许对象的引用关系还在变化,不然可达性分析的准确性就没法获得保证。因此须要STW以保证可达性分析的正确性。
程序执行时并不是在全部地方都能停顿下来开始GC,只有在“安全点”才能暂停。安全点指的是:HotSpot没有为每一条指令都生成OopMap(Ordinary Object Pointer),而是在一些特定的位置记录了这些信息。这些位置就叫安全点。
在具体解释上面的四个隔离级别前。有必要了解事务的四大特性(ACID)
事务并发可能产生的问题:
脏数据:事务对缓冲池中的行记录进行修改,可是尚未被提交。
脏读是读取到事务未提交的数据,不可重复度读读取到的是提交提交后的数据,只不过在一次事务中读取结果不同。
不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住知足条件的行,解决幻读须要锁表。
通常来讲,数据库隔离级别不同,可能出现的并发问题也不一样。级别最高的是串行化,全部问题都不会出现。可是在并发下性能极低,可重复读会只会致使幻读。
因此通常使用MySQL默认的可重复读便可。MVCC(多版本并发控制)使用undo_log使得事务能够读取到数据的快照(某个历史版本),从而实现了可重复读。MySQL采用Next-Key Lock算法,对于索引的扫描不只是锁住扫描到的索引,还锁住了这些索引覆盖的范围,避免了不可重复读和幻读的产生。
死锁是指两个或两个以上的事务在执行过程当中,因争夺锁资源而形成的一种互相等待的现象,若无外力做用两个事务都没法推动,这样就产生了死锁。下去 死锁的四个必要条件:
避免死锁能够经过破环四个必要条件之一。
解决死锁的方法:
开启慢查询,查找哪些sql语句执行得慢。使用explain查看语句的执行计划,好比有没有使用到索引,是否启用了全表扫描等。查询慢,很大多是由于没有使用索引或者索引没有被命中。还有其余的缘由,好比发生了死锁,硬件、网速等缘由。
优化手段:为相关列添加索引,而且确保索引能够被命中。优化sql语句的编写。
索引是对数据库表中一个或多个列的值进行排序的结构。MySql中索引是B+树,在查找时能够利用二分查找等高效率的查找方式,以O(lg n)的时间找到。所以索引能够加快查询速度。
哪些状况不适合创建索引?
建了一个(a,b,c)的联合索引,那么实际等于建了(a),(a,b),(a,b,c)三个索引,可是有时在条件查询时只会匹配到a或者(a, b)而不会匹配到(a, b, c)。下面的例子
SELECT * FROM table WHERE a = 1 AND c = 3; // 使用了索引a,c不走索引 SELECT * FROM table WHERE a = 1 AND b < 2 AND c = 3; // 使用到了索引(a,b),c不走索引
创建联合索引(a, b ,c),因此索引是按照a -> b -> c的顺序进行排序的。a-b-c这样的索引是先找a,而后在范围里面找b,再在范围内找c。 因此上面的语句里的c 会分散在不少个b里面且不是排序的,因此没办法走索引。
举个例子好比(a, b)联合索引,先按a排序再按b排序,获得
(1,1)->(1, 2)->(2, 1) (2, 4)->(3, 1)->(3, 2)
若是执行select a from table where b=2
,就没有使用到(a, b)这个联合索引,由于b的值1,2,1,4,1,2显然不是排序的。
具体来讲:MySQL会从左开始一直向右匹配直到遇到范围查询(>,<,BETWEEN,LIKE)就中止匹配,好比: a = 1 AND b = 2 AND c > 3 AND d = 4,若是创建 (a,b,c,d)顺序的索引,使用了索引(a, b, c),可是d是没有走索引的,若是创建(a,b,d,c)的索引,则能够命中索引(a, b, c, d),其中a,b,d的顺序能够任意调整。
等于(=)和in 能够乱序。好比,a = 1 AND b = 2 AND c = 3 创建(a,b,c)索引能够任意顺序。
a = 1 AND b = 2 AND c > 3 AND d = 4
,创建(a, b, d, c)就是不错的选择;SELECT * FROM t WHERE c = 100 and d = 'xyz' ORDER BY b
创建(c, d, b)联合索引就是不错的选择LIKE '%abc'
这样的不能命中索引;不过LIKE 'abc%'
能够命中索引。not in, <>,!=
则不会命中索引。注:<>
是不等号LIKE '%abc'
这样不能命中索引MySQL5.0以前,一个表一次只能使用一个索引,没法同时使用多个索引分别进行条件扫描。可是从5.1开始,引入了 index merge 优化技术,对同一个表能够使用多个索引分别进行条件扫描。
大多数状况下索引能大幅度提升查询效率,但数据的变动(增删改)都须要维护索引,所以更多的索引意味着更多的维护成本和更多的空间 (一本100页的书,却有50页目录?)并且太小的表,创建索引可能会更慢(读个2页的宣传手册,你还先去找目录?)
B-树是一种平衡的多路查找树。2-3树和2-3-4树都是B-树的特例。一棵M阶的B-树,除了根结点外的其余非叶子结点,最多含有M-1对键和连接,最少含有M/2对键和连接。根结点能够少于M/2,可是也不能少于2对。
B+树是B-树的变体,也是一种多路查找树。
B+ 树更适合用于数据库和操做系统的文件系统中。
假设一个结点就是一个页面,B树遍历全部记录,经过中序遍历的方式,要屡次返回到父结点,同一个结点屡次访问了,增长了磁盘I/O操做的次数。B+由于在叶子结点存放了全部的记录,并且是双向链表的结构,只需在叶子节点这一层就能遍历全部记录,大大减小了磁盘I/O操做,因此数据库索引用B+树结构更好。
COUNT(*)
和COUNT(1)
的区别?COUNT(列名)
和COUNT(*)
的区别?COUNT(*)
和COUNT(1)
没区别。COUNT(列名)
和COUNT(*)
区别在于前者不会统计列为NULL的数据,后者会统计。
悲观锁:老是假设在并发下会出现问题,即假设多个事务对同一个数据的访问会产生冲突。当其余事务想要访问数据时,会在临界区提早加锁,须要将其阻塞挂起。好比MySQL中的排他锁(X锁)、和共享锁(S锁)
乐观锁: 老是假设任务在并发下是安全的,即假设多个事务对同一个数据的访问不会发生冲突,所以不会加锁,就对数据进行修改。当遇到冲突时,采用CAS或者版本号、时间戳的方式来解决冲突。数据库中使用的乐观锁是版本号或时间戳。乐观并发控制(OCC)是一种用来解决写-写冲突的无锁并发控制,不用加锁就尝试对数据进行修改,在修改以前先检查一下版本号,真正提交事务时,再检查版本号有,若是不相同说明已经被其余事务修改了,能够选择回滚当前事务或者重试;若是版本号相同,则能够修改。
提一下乐观锁和MVCC的区别,其实MVCC也利用了版本号,和乐观锁仍是能扯上些关系。
MVCC主要解决了读-写的阻塞,由于读只能读到数据的历史版本(快照);OCC主要解决了写-写的阻塞,多个事务对数据进行修改而不加锁,更新失败的事务能够选择回滚或者重试。
当多个用户/进程/线程同时对数据库进行操做时,会出现3种冲突情形:读-读,不存在任何问题;读-写,有隔离性问题,可能遇到脏读、不可重复读 、幻读等。写-写,可能丢失更新。多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,读操做只读该事务开始前的数据库的快照,实现了一致性非锁定读。 这样在读操做不用阻塞写操做,写操做不用阻塞读操做的同时,避免了脏读和不可重复读。乐观并发控制(OCC)是一种用来解决写-写冲突的无锁并发控制,不用加锁就尝试对数据进行修改,在修改以前先检查一下版本号,真正提交事务时,再检查版本号有,若是不相同说明已经被其余事务修改了,能够选择回滚当前事务或者重试;若是版本号相同,则能够修改。
MVCC(多版本并发控制)使用undo_log使得事务能够读取到数据的快照(某个历史版本),从而实现了可重复读。MySQL采用Next-Key Lock算法,对于索引的扫描不只是锁住扫描到的索引,还锁住了这些索引覆盖的范围,避免了不可重复读和幻读的产生。
具体来讲:
在可重复读下: select....from会采用MVCC实现的一致性非锁定读,读取的是事务开始的快照,避免了不可重复读。select .....from .... for update会采用 Next-Key Locking来保证可重复读和幻读。
在读已提交下: select....from 会采用快照,读取的是最新一份的快照数据,不可以保证不可重复读和幻读;select .....from .... for update会采用Record Lock,不可以保证不可重复读/幻读。
若是一个索引包含(或覆盖)全部须要查询的字段的值,即只需扫描索引而无须回表,这称为“覆盖索引”。InnoDB的辅助索引在叶子节点中保存了部分键值信息以及指向汇集索引键的指针,若是辅助索引叶子结点中的键值信息已经覆盖了要查询的字段,就没有必要利用指向主键索引的主键,而后再经过主键索引来找到一个完整的行记录了。
UNION 操做符用于合并两个或多个 SELECT 语句的结果集。UNION 内部的 SELECT 语句必须拥有相同数量的列。列也必须拥有相同的数据类型。同时,每条 SELECT 语句中的列的顺序必须相同。默认状况下,UNION会过滤掉重复的值。使用 UNION ALL则会包含重复的值。
JOIN用于链接两个有关联的表,筛选两个表中知足条件(ON后的条件)的行记录获得一个结果集。从结果集中SELECT的字段能够是表A或者表B中的任意列。
JOIN经常使用的有LEFT JOIN、RIGHT JOIN、INNER JOIN。
INNER JOIN
FULL OUTER JOIN
LEFT JOIN
RIGHT JOIN和LEFT JOIN相似。
所谓SQL注入式攻击,就是攻击者把SQL命令插入到Web表单的输入域或页面请求的查询字符串,欺骗服务器执行恶意的SQL命令。
好比在登陆界面,若是用户名填入'xxx' OR 1=1 --
就能构造下面的SQL语句,由于OR 1=1,password被注释掉,所以不管name和password填入什么都能登陆成功。
SELECT * FROM USER WHERE NAME='xxx' OR 1=1 -- and password='xxx';
使用PrepareStatement,能够防止sql注入攻击,sql的执行须要编译,注入问题之因此出现,是由于用户填写 sql语句参与了编译。使用PrepareStatement对象在执行sql语句时,会分为两步,第一步将sql语句 "运送" 到mysql上预编译,再回到java端拿到参数运送到mysql端。预先编译好,也就是SQL引擎会预先进行语法分析,产生语法树,生成执行计划,也就是说,后面你输入的参数,不管你输入的是什么,都不会影响该sql语句的语法结构了。用户填写的 sql语句,就不会参与编译,只会当作参数来看。从而避免了sql注入问题。
AOP:面向切面编程。能够将应用各处的功能分离出来造成可重用的组件。核心业务逻辑与安全、事务、日志等这些非核心业务逻辑分离,使得业务逻辑更简洁清晰。
提供了对像关系映射(ORM)、事务管理、远程调用和Web应用的支持。
Spring使用IOC容器建立和管理对象,好比在XML中配置了类的全限定名,而后Spring使用反射+工厂来建立Bean。BeanFactory是最简单的容器,只提供了基本的DI支持,ApplicationContext基于BeanFactory建立,提供了完整的框架级的服务,所以通常使用应用上下文。
IOC(Inverse of Control)即控制反转。能够理解为控制权的转移。传统的实现中,对象的建立和依赖关系都是在程序进行控制的。而如今由Spring容器来统一管理、对象的建立和依赖关系,控制权转移到了Spring容器,这就是控制反转。
DI(Dependency Injection)依赖注入。对象的依赖关系由负责协调各个对象的第三方组件在建立对象的时候进行设定,对象无需自行建立或管理它们的依赖关系。通俗点说就是Spring容器为对象注入外部资源,设置属性值。DI的好处是使得各个组件之间松耦合,一个对象若是只用接口来代表依赖关系,这种依赖能够在对象绝不知情的状况下,用不一样的具体类进行替换。
IOC和DI实际上是对同一种的不一样表述。
AOP(Aspect-Orientid Programming)面向切面编程,能够将遍及在应用程序各个地方的功能分离出来,造成可重用的功能组件。系统的各个功能会重复出如今多个组件中,各个组件存在于核心业务中会使得代码变得混乱。使用AOP能够将这些多处出现的功能分离出来,不只能够在任何须要的地方实现重用,还能够使得核心业务变得简单,实现了将核心业务与日志、安全、事务等功能的分离。
具体来讲,散布于应用中多处的功能被称为横切关注点,这些横切关注点从概念上与应用的业务逻辑是相分离的,可是又经常会直接嵌入到应用的业务逻辑中,AOP把这些横切关注点从业务逻辑中分离出来。安全、事务、日志这些功能均可以被认为是应用中的横切关注点。
一般要重用功能,能够使用继承或者委托的方式。可是继承每每致使一个脆弱的对像体系;委托带来了复杂的调用。面向切面编程仍然能够在一个地方定义通用的功能,可是能够用声明的方法定义这个功能要在何处出现,而无需修改受到影响的类。横切关注点能够被模块化为特殊的类,这些类被称为切面(Aspect)。好处在于:
AOP术语介绍
通知:切面所作的工做称为通知。通知定义了切面是什么,以及在什么时候使用。Spring切面能够应用5种类型的通知
链接点:能够被通知的方法
切点:实际被通知的方法
切面:即通知和切点的结合,它是什么,在什么时候何处完成其功能。
引入:容许向现有的类添加新方法或属性,从而能够在无需修改这些现有的类状况下,让它们具备新的行为和状态。
织入:把切面应用到目标对象并建立新的代理对象的过程。切面在指定的链接点被织入到目标对象中。在目标对象的生命周期里有多个点能够进行织入:
Spring AOP构建在动态代理基础之上,因此Spring对AOP的支持仅限于方法拦截。
Spring的切面是由包裹了目标对象的代理类实现的。代理类封装了目标类,并拦截被通知方法的调用,当代理拦截到方法调用时,在调用目标bean方法以前,会执行切面逻辑。其实切面只是实现了它们所包装bean相同接口的代理。
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理经过反射来接收被代理的类,而且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。
若是目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,能够在运行时动态的生成某个类的子类,注意,CGLIB是经过继承的方式作的动态代理,所以若是某个类被标记为final,那么它是没法使用CGLIB作动态代理的。
Spring使用动态代理,代理类封装了目标类,当代理拦截到方法调用时,在调用目标bean的方法以前,会执行切面逻辑。
Spring建立、管理对象。Spring容器负责建立对象,装配它们,配置它们并管理它们的整个生命周期。
<bean id="">
@Bean
注解@ComponentScan
和@AutoWired
注解bean的注入方式有:
推荐对于强依赖使用构造器注入,对于弱依赖使用属性注入。
默认状况下Spring中的bean都是单例的。
Hibernate :是一个标准的ORM(对象关系映射) 框架; SQL语句是本身生成的,程序员不用本身写SQL语句。所以要对SQL语句进行优化和修改比较困难。适用于中小型项目。
MyBatis: 程序员本身编写SQL, SQL修改和优化比较自由。 MyBatis更容易掌握,上手更容易。主要应用于需求变化较多的项目,如互联网项目等。
首先要了解几种数据结构和算法:
对上亿个无重复数字的排序,或者找到没有出现过数字,注意由于无重复数字,而BitMap的0和1正好能够表示该数字有没有出现过。若是要求更小的内存,能够先分出区间,对落入区间的进行计数。必然有的区间数量未满,再遍历一次数组,只看该区间上的数字,使用BitMap,遍历完成后该区间中必然有没被设置成0的的地方,这些地方就是没出现的数。
数据在小范围内波动,好比人类年龄,并且数据容许重复,可用计数排序处理数值排序或查找没有出现过的值,计数的桶中频次为0的就是没有出现过的数。
数据是数字,要找最大的Top K,直接用大小为K的小根堆,不断淘汰最小元素便可。
数据是数字或非数字,要找频次最高的Top K。可以使用HashMap统计频次,统计出频次最大的前K个便可。统计频次选出前K的过程能够用小根堆。还能够用Hash分流的方法,即用一个合适的hash函数将数据分到不一样的机器或者文件中,由于对于一样的数据,因为hash函数的性质,必然被分配到同一个文件中,所以不存在相同的数据分布在不一样的文件这种状况。对每一个文件采用HashMap统计频次,用小根堆选出Top K,而后汇总所有文件,从全部部分结果的Top K中再利用小根堆获得最终的Top K。
查找数值的排名,好比找到中位数。好比将数划分区间,对落入每一个区间的数进行计数。而后能够得知中位数落在哪一个区间,再遍历全部数,此次只关心落在该区间的数,不划分区间的对其进行计数,就能够找出中位数。