
大家好,又见面了,我是你们的朋友全栈君。
回答主要是三个方面:
hashmap基本原理;hashmap的put存源码解读;hashmap的get取源码解读;hashmap是基于hash算法的key-value键值对,通过key可以快速的找到value值,解决了数组的增加和删除以及链表的查询效率低的问题。
public V put(K key, V value) {
if (key == null)//如果key为空,调用putForNullKey()处理
return putForNullKey(value);
int hash = hash(key);//通过key值获得hash码(看hash函数,是通过右移位,这种方式使数据散列均匀)
//通过indexFor()获得对应table中的索引
int i = indexFor(hash, table.length);//源码采用&的方式
//取出table表中的元素,并循环单链表,判断key是否存在
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//hash码相同,且对象相同key值相同
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
{
//新值替换旧值,并返回旧值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//key不存在,加入新的元素
modCount++;
addEntry(hash, key, value, i);//hash码每个key都对应唯一的hash码,i是table的索引(通过hash函数算出来的)
return null;
}Hashmap通过调用put函数,实现键值对的存储。
key的唯一性,一般选用final修饰的类型,比如基本类型的引用类型、String。问:如果不用这些类型,别人把key都改了,取出来的值肯定就是错。
hash值int hash hash(key);先得到key的hashcode值(因为每一个key的hashcode值都是唯一的),然后通过hash算法(底层是通过移位实现的),hash算法的目的就是让hashcode值能均匀的填充table表,而不是造成大量的集中冲突。
hash值与table数组的长度进行与运算,获得table数组下标int i = indexFor(hash,table.length);
问题:传统的方法是通过hashcode与table数组长度相除取余,这个使得table表中的数据分布不均匀,产生大量集中冲突。
hashmap定义的一个类entity,基本结构包含三个元素key、value和指向下一个entity的next),若不同的hash值算在同一个table下标下,这就产生了冲突。常用的冲突解决算法是:拉链法(采用头插法)、线性探测法。table数组中,这个过程叫做重新散列(rehashing)。但是在多线程的时候,会出现条件竞争。比如两个线程发现hashmap需要重新调整大小,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。
可以参考这篇文章:http://coolshell.cn/articles/9606.htmlCocurrentHashMap,它是行级锁,而hashtable是整体加锁。链接
求index是一个非常频繁的操作,而乘法运算比除法来的省事(对CPU而言),所以把除法换成乘法和一个移位操作,公式: Index=(value*value)>>28(右移,是除以2^28. 记住:左移变大,是乘。右移变小,是除)
常见的hash算法:
Hashmap初始容量是16,若实际填充的容量是初始容量*负载因子,若全部填满查询的开销太大。因此hashmap的容量达到75%,就会扩容。扩容的大小是原来的一倍。
注意⚠️:jdk1.8中若链表的结点数大于8,则会转化成红黑树(目的提高查询效率)
链接
负载因子load factor=hashmap的数据量(entry的数量)/初始容量(table数组长度),负载因子越高,表示初始容量中容纳的数据会越多,虽然在空间中减少了开销,但是大量数据聚集,造成查询上的大量开销。负载因子越低,表示初始容量中容纳的数据会越少,造成大量的内存空间浪费,但是查询的效率比较高。这是一个矛盾体,为了寻求平衡点,负载因子0.75效果是最好的。
要知道ConcurrentHashMap的结构、put和get方法。
ConcurrentHashMap类中包含两个静态的内部类HashEntry和Segment.HashEntry用来封装映射表的键值对;Segment 用来充当锁的角色,每个 Segment对象守护整个散列映射表的若干个桶。每个桶由若干个HashEntry 对象链接起来的链表。一个ConcurrentHashMap实例中包含由若干个 Segment 对象组成的数组。散列码通过segmentFor找到对应的Segment(不允许value为空)
在Segment中执行具体的put操作:
segment,而非整个ConcurrentHashMap)HashEntry是否超过阀值(负载因子*数组长度),若超过要进行散列;Thread类,重写run函数Runnable接口(最常用)Callable接口三种方式的区别:
定义: (1)某个类的行为与其规范一致; (2)不管多个线程是怎样的执行顺序和优先级,或是wait,sleep,join等控制方式。 如果一个类在多个线程访问下运转一切正常,并且访问类不需要进行额外的同步处理或协调,那么我们认为它是线程安全的。 如何保证线程安全?
volatile;synchronized,lock);注意⚠️:非线程安全的集合在多线程环境下可以使用,但并不能作为多个线程共享的属性,可以作为某个线程独立的属性。例如:Vector是线程安全的,ArrayList不是线程安全的,如果每一个线程new一个ArrayList,而这个ArrayList在这个线程中使用肯定没有问题。(若再new一个ArrayList,就会线程不安全)
wait(), notify(), notifyAll();java 1.5中采用condition,比传统的wait,notify()更加安全高效;生产者发布消息在队列中,消费者从队列中取任务去执行。
出现的问题:当我们在线程对象(Runnable)中定义了全局变量,run方法修改该变量时,如果有多个线程同时使用该线程对象,那么就会造成全局变量的值被同时修改,造成错误。
ThreadLocal是JDK引入的一种机制,它用于解决线程共享变量,使用ThreadLocal声明的变量,会在每个线程内产生一个变量副本,从而解决了线程不安全。volatile变量每次被线程访问时,都强迫线程从主内存中重新取该变量的最新值到工作内存中,而当该变量发生修改变化时,也会强迫线程将最新的值刷新会到主内存中。这样一来,不同的线程都能及时的看到该变量的最新值。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建(类似于工厂设计模式),使用完毕不需要销毁线程而是返回池中,从而减少创建和销毁线程对象的开销。
设计一个动态大小的线程池,如何设计,应该有哪些方法?
一个线程池包括以下四个基本单位:
ThreadPool):用于创建并管理线程池,包括创建线程,销毁线程池,添加新任务;PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定任务的入口,任务执行完成后的收尾工作,任务的执行状态等。TaskQueue):用于存放没有处理的任务,提供一种缓冲机制。所包含的方法:
private ThreadPool()创建线程池;Public static ThreadPool getThreadPool()获得一个默认线程个数的线程池;Public void execute(Runnable task)执行任务,其实只是把任务加入任务队列,什么时候执行由线程池管理器确定;Public void execute(Runnable[] task) 批量执行任务,其实只是把任务加入任务队列,什么时候执行由线程管理器确定。Public void destroy() 销毁线程池,该方法保证所有任务都完成的情况下才销毁所有线程,否则等待任务完成销毁。Public int getWorkThreadNumber() 返回工作线程的个数。Public int getFinishedTasknumber()返回已完成任务的个数,这里的已完成是指出了任务队列的任务个数,可能该任务并没有实际执行完成。Public void addTread() 在保证线程池中所有线程正在执行,并且要执行线程的个数大于某一值时(就是核心池大小),增加线程池中线程的个数(最大是线程池大小)。Public void reduceThread() 在保证线程池中有很大一部分线程处于空闲状态,并且空闲状态的线程在小于某一值时(就是核心池大小),减少线程池中线程的个数。Volatile 关键字的作用
注意⚠️:volatile并不保证原子性。
内存的可见性:
Volatile保证可见性的原理是在每次访问变量时都会进行刷新,因此每次访问都是在主内存中得到最新的版本。所以volatile关键字的作用之一就是保证变量修改的实时可见性。
当且仅当满足以下所有条件时,才应该使用volatile变量:
volatile。当需要访问的变量已在synchronized代码块中,或者为常量,没必要使用volatitle;volatile屏蔽了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。Volatile和synchronized区别
volatile不会进行加锁操作。Volatile变量是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。(其实volatitle在读写时,就相当于加锁)Volatile变量作用类似于同步变量读写操作。从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。volatile不如synchronized安全。在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比加锁的代码更加脆弱,也更加难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。一般来说,用同步机制更安全些。volatile无法同时保证内存可见性和原子性。加锁机制(即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性,原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说以下的表达式都不是原子操作:“count++”“count=count+1”。Sleep是Thread类的方法。Wait是object类方法(与notify(), notifyAll();连用,即等待唤醒)
两者区别
Sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(cpu)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动回复(线程回到就绪状态)。Wait()是Object类的方法,调用对象的wait方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait poo),只有调用对象的notify方法或notifyAll时才能唤醒等待池中的线程进入等锁池(lock
pool),如果线程重新获得对象的锁就可以进入就绪状态。Synchronized与lock的区别 (1)(用法)synchronized(隐式锁):在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中。 (2)(用法)Lock(显示锁):需要显示指定起始位置和终止位置,一般使用ReentrantLock类做为锁,多个线程中必须要使用一个 ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般在finally块中写unlock以防死锁。 (3)(性能)synchronized是托管给JVM执行的,而lock是java写的控制锁代码。在java1.5中,synchronized是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用java提供的lock对象,性能更高一些。但是到了java1.6,发生了变化。Synchronized在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在java1.8上synchronized的性能并不比lock差。 (4)(机制)synchronized原始采用的是cpu悲观锁机制,即线程获得是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,乐观锁实现的机制就是CAS操作(Compare and Swap)。
Synchronized底层如何实现的?(看源码)用在代码块和方法上有什么区别?
synchronized用在代码块锁的是调用该方法的对象(this),也可以选择锁住任何一个对象。Synchronized用在方法上锁的是调用该方法的对象。Synchronized用在代码块可以减少锁的粒度,从而提高并发性能。Synchronized与static Synchronized的区别
synchronized是对类的当前实例进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块,同一个类的两个不同实例就没有这种约束(这个对象压根就是两个不相关的东西)。static synchronized恰好就是要控制类的所有实例的访问,static synchronized是限制线程同时访问jvm中该类的所有实例同时访问对应的代码块。BIO:
NIO:
AIO:
描述了java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的低层细节。

Java内存模型的两条规定:
常常需要在隔两分钟后再收集一次thread dump,如果得到的输出相同,仍然是大量thread都在等待给同一个地址上的锁,那么肯定是死锁了。锁分类如下:
优化方面的锁:
Throwable是java语言中所有错误和异常的基类,它由两个子类:Error, Exception.
异常种类
Error: Error为错误,是程序无法处理的,如OutOfMemoryError,ThreadDeath等,出现这种情况你唯一能做的就是听之任之,交由JVM来处理,不过大多数情况下会选择终止线程。Exception: Exception是程序可以处理的异常,它又分为两种CheckedException(受检异常)和unCheckedException(不受检异常)。其中,CheckedException发生在编译阶段,必须要使用try……catch(或则throws)否则编译不通过;unCheckedException发生在运行期,具有不确定性,主要是由于程序的逻辑问题所引起的,难以排查,我们一般都需要纵观全局才能够发现这类的异常错误。常见异常的基类:
IOExceptionRuntimeException

Static
父类静态代码块--->子类静态代码块---->父类非静态代码块---->父类构造方法----->子类非静态代码块----->子类构造方法Final
final形参不可变;final类,都不允许被继承;String长度是不可变动的,StringBuffer、StringBuilder长度是可变的;StringBuffer是线程安全的(在StringBuilder方法上添加关键字synchronized),StringBuilder线程不安全;StringBuilder比StringBuffer拥有更好的性能;String类型的字符串,在编译时就可以确定是一个字符串常量,则编译完成之后,字符串会自动拼接成一个常量,此时String的速度比StringBuilder和StringBuffer的性能好的多。String不变性的理解
String类是被final进行修饰的,不能被继承;+号连接字符串的时候会创建新的字符串;String s=new String(“helle world”);可能创建两个对象也可能创建一个对象。如果静态区中有“hello world”字符串常量对象的话,则仅仅在堆中创建一个对象。如果静态区没有“hello world”对象,则在堆和静态区中都创建对象。java中,通过使用“+”符合来链接字符串的时候,实际底层会转成通过StringBuilder实例的append()方法实现。有重写这些方法。
当equals方法被重写,通常有必要重写hashCode方法,才能保证相等。如:object1.equal(object2)为true, object1.hashCode==object2.hashCode()为true。
两个对象内容相等,那么hashCode指向的是同一个内容,返回的哈希值是相同的;
object1.hashCode==object2.hashCode()为false时,object1.equal(object2)为false
两个hashCode不等,那么两个对象的内容必然不同(每个对象的哈希值是唯一的);
object1.hashCode==object2.hashCode()为true时,object1.equal(object2)不一定为true;
比如hashmap,hashCode是数组的下标,但是会产生hash冲突,比如一个数组下标后连接一个链表;
重写equals不重写hashcode会出现什么问题?
在存储散列集合时(如set类),如果原对象.equals(新对象),但没有对hashcode重写,即两个对象拥有不同的hashcode,则在集合中会存储两个值相同的对象,从而导致混淆,因此在重写equals方法时,必须重写hashcode方法。
需要重写equals方法和hashcode,必须保证对象的属性改变时,其hashcode不能改变。
将那些实现Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象,序列化可以弥补不同操作系统之间的差异。
Java序列化的作用
javaBeans进行序列化如何实现系列化和反序列化
readObject()和writeObject()方法,则采取默认的序列化机制,如果添加了这两个方法之后还想利用java默认的序列化机制,则在这两个方法中分别调用defaultReadObject()和defaultWriteObject()两个方法;transient关键字进行修饰不必序列化的属性。因为在反序列化时,private修饰的属性也能查看到。ExternalSerializable方法
自己对要序列化的内容进行控制,控制哪些属性被序列化,哪些不能被序列化;Serializable接口的对象在反序列化时不需要调用对象所在类的构造方法,完全基于字节;ExternalSerializable接口的方法在反序列化时会调用构造方法;注意事项⚠️
static修饰的属性不能被序列化;class文件;serializableID,因为在不同的JVM之间,默认生成serializableID;可能不同,会造成反序列化失败。常见的序列化协议有哪些?
COMCORBAXML&SOAPJSONThrift
……java的四个基本特性
多态的理解(多态的实现方式)
overload)实现编译时的多态性(也称为前绑定)(1、类型不同 2、参数个数不同 3、与返回值无关)。override)实现运行时的多态(也称为后绑定)。(核心精髓)项目中多态的应用:
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/169257.html原文链接:https://javaforall.cn