剑指Offer——知识点储备-Java基础_剑指java——核心原理与应用实践

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`下标下,这就产生了冲突。常用的冲突解决算法是:拉链法(采用头插法)、线性探测法。
* 若不等,调用addEntry()将新创建一个元素添加到table中。创建元素entity时,要判断table的填充容量是否大于负载因子0.75,若大于就要扩容,容量扩充到两倍。
* 扩容的时候,是在内存中开辟新的内存空间,然后把原来的对象放到新的`table`数组中,这个过程叫做重新散列(`rehashing`)。但是在多线程的时候,会出现条件竞争。比如两个线程发现`hashmap`需要重新调整大小,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素次序会反过来,因为移动到新的`bucket`位置的时候,`HashMap`并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。  
 可以参考这篇文章:http://coolshell.cn/articles/9606.html
* 为了解决这个问题,采用了线程安全的`CocurrentHashMap`,它是行级锁,而`hashtable`是整体加锁。


#### 2.2 常用的hash算法有哪些?


[链接](https://bbs.csdn.net/topics/618166371)


##### 2.2.1 构造哈希函数的方法有哪些?



> 
> * 直接定址法(或直接寻址法)
> * 数字分析法
> * 平方取中法(平方散列法)
> 
> 
> 


求index是一个非常频繁的操作,而乘法运算比除法来的省事(对CPU而言),所以把除法换成乘法和一个移位操作,公式: `Index=(value*value)>>28(右移,是除以2^28. 记住:左移变大,是乘。右移变小,是除)`



> 
> * 折叠法
> * 除留余数法(这是最简单也是最常用的构造hash函数的方法)即对关键字取模Index=value%16(取模)
> * 随机数法
> 
> 
> 


##### 2.2.2 解决hash冲突的方法有哪些?



> 
> * 开放定址法(线性探测再散列、二次探测再散列)(线性探测法)
> * 再哈希法(双散列函数法):在发生冲突的时候,再次使用另一个散列函数,计算哈希函数地址,直到冲突不再发生。
> * 链地址法(拉链法)(常用重点):就是一个数组+链表(链表采用头插法)
> * 建立一个公共溢出区。
> 
> 
> 


常见的`hash`算法:



> 
> * MD4
> * MD5
> * SHA-1算法
> 
> 
> 


##### 2.2.3 HashMap为什么要扩容?


`Hashmap`初始容量是16,若实际填充的容量是`初始容量*负载因子`,若全部填满查询的开销太大。因此hashmap的容量达到75%,就会扩容。扩容的大小是原来的一倍。


注意⚠️:`jdk1.8`中若链表的结点数大于8,则会转化成红黑树(目的提高查询效率)  
 [链接](https://bbs.csdn.net/topics/618166371)


#### 2.3 load factor的作用


负载因子`load factor=hashmap`的数据量`(entry的数量)/初始容量(table数组长度)`,负载因子越高,表示初始容量中容纳的数据会越多,虽然在空间中减少了开销,但是大量数据聚集,造成查询上的大量开销。负载因子越低,表示初始容量中容纳的数据会越少,造成大量的内存空间浪费,但是查询的效率比较高。这是一个矛盾体,为了寻求平衡点,负载因子`0.75`效果是最好的。


#### 2.4 ConcurrentHashMap 的实现原理?


[链接](https://bbs.csdn.net/topics/618166371)


要知道`ConcurrentHashMap`的结构、put和get方法。


* `ConcurrentHashMap`类中包含两个静态的内部类`HashEntry`和`Segment.HashEntry`用来封装映射表的键值对;`Segment` 用来充当锁的角色,每个 `Segment`对象守护整个散列映射表的若干个桶。每个桶由若干个`HashEntry` 对象链接起来的链表。一个`ConcurrentHashMap`实例中包含由若干个 `Segment` 对象组成的数组。
* 用分离锁实现多个线程间的并发写操作(put方法实现的过程)


散列码通过`segmentFor`找到对应的`Segment`(不允许value为空)



> 
> 1. 将散列值右移segmentShift 个位,并在高位填充 0
> 2. 然后把得到的值与 segmentMask 相“与”
> 3. 从而得到 hash 值对应的 segments 数组的下标值
> 4. 根据下标值返回Segment对象
> 
> 
> 


在`Segment`中执行具体的`put`操作:



> 
> 1. 加锁(锁定某个`segment`,而非整个`ConcurrentHashMap`)
> 2. 判断`HashEntry`是否超过阀值(`负载因子*数组长度`),若超过要进行散列;
> 3. 没超过,判断键值对是否存在,采用头插法加入链表中;
> 4. 然后解锁。
> 
> 
> 


#### 2.5 HashMap与ConcurrentHashMap的关联与区别?


#### 2.6 HashTable的实现原理?与ConcurrentHashMap的区别


#### 2.7 concurrent包的并发容器有哪些?


### 三、java多线程问题


#### 3.1 实现多线程的方式


* 继承`Thread`类,重写run函数
* 实现`Runnable`接口(最常用)
* 实现`Callable`接口


三种方式的区别:



> 
> * 实现Runnable接口可以避免java单继承特性带来的局限,增强程序健壮性,代码能够被多个线程共享。
> * Thread和Runnable启动线程都是使用start方法,然后JVM将此线程放到就绪队列中,如果有处理机可用,则执行run方法。
> * 实现Callable接口要实现call方法,并且线程执行完毕后会有返回值,其他的两种方法都是重写run方法,没有返回值。Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更为强大:call()方法可以有返回值;call()方法可以声明抛出异常。
> 
> 
> 


#### 3.2 线程安全


定义:  
 (1)某个类的行为与其规范一致;  
 (2)不管多个线程是怎样的执行顺序和优先级,或是wait,sleep,join等控制方式。 如果一个类在多个线程访问下运转一切正常,并且访问类不需要进行额外的同步处理或协调,那么我们认为它是线程安全的。  
 如何保证线程安全?


* 对变量使用`volatile`;
* 对程序段进行加锁(`synchronized,lock`);


注意⚠️:非线程安全的集合在多线程环境下可以使用,但并不能作为多个线程共享的属性,可以作为某个线程独立的属性。例如:`Vector`是线程安全的,`ArrayList`不是线程安全的,如果每一个线程`new`一个`ArrayList`,而这个`ArrayList`在这个线程中使用肯定没有问题。(若再`new`一个`ArrayList`,就会线程不安全)


#### 3.3 多线程如何进行信息交互



> 
> 1. 采用object方法:`wait()`, `notify()`, `notifyAll()`;
> 2. 在`java 1.5`中采用`condition`,比传统的`wait`,`notify()`更加安全高效;
> 3. 采用生产者-消费者模式(采用队列的形式);
> 
> 
> 


生产者发布消息在队列中,消费者从队列中取任务去执行。


#### 3.4 多线程共用一个数据变量需要注意什么?


出现的问题:当我们在线程对象(`Runnable`)中定义了全局变量,run方法修改该变量时,如果有多个线程同时使用该线程对象,那么就会造成全局变量的值被同时修改,造成错误。



> 
> * 解决方法1:`ThreadLocal`是JDK引入的一种机制,它用于解决线程共享变量,使用`ThreadLocal`声明的变量,会在每个线程内产生一个变量副本,从而解决了线程不安全。
> * 解决方法2:`volatile`变量每次被线程访问时,都强迫线程从主内存中重新取该变量的最新值到工作内存中,而当该变量发生修改变化时,也会强迫线程将最新的值刷新会到主内存中。这样一来,不同的线程都能及时的看到该变量的最新值。
> 
> 
> 


#### 3.5 什么是线程池?如果让你设计一个动态大小的线程池,如何设计,应该有哪些方法?


线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建(类似于工厂设计模式),使用完毕不需要销毁线程而是返回池中,从而减少创建和销毁线程对象的开销。


设计一个动态大小的线程池,如何设计,应该有哪些方法?


一个线程池包括以下四个基本单位:



> 
> * **线程管理器**(`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()` 在保证线程池中有很大一部分线程处于空闲状态,并且空闲状态的线程在小于某一值时(就是核心池大小),减少线程池中线程的个数。


#### 3.6 volatile与synchronized区别


`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”。
> 
> 
> 


#### 3.7 sleep和wait分别是哪个类的方法,有什么区别?


`Sleep`是`Thread`类的方法。`Wait`是`object`类方法(与notify(), notifyAll();连用,即等待唤醒)


**两者区别**



> 
> * `Sleep()`方法(休眠)是线程类(`Thread`)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(cpu)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动回复(线程回到就绪状态)。
> * `Wait()`是Object类的方法,调用对象的`wait`方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait poo),只有调用对象的`notify`方法或`notifyAll`时才能唤醒等待池中的线程进入等锁池(lock  
>  pool),如果线程重新获得对象的锁就可以进入就绪状态。
> 
> 
> 


#### 3.8 synchronized与lock的区别,使用场景,看过synchronized的源码没?


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)。


#### 3.9 synchronized底层如何实现的?用在代码块和方法上有什么区别?


`Synchronized`底层如何实现的?(看源码)用在代码块和方法上有什么区别?



> 
> * `synchronized`用在代码块锁的是调用该方法的对象(this),也可以选择锁住任何一个对象。
> * `Synchronized`用在方法上锁的是调用该方法的对象。
> * `Synchronized`用在代码块可以减少锁的粒度,从而提高并发性能。
> * 无论用在代码块上还是用在方法上,都是获取对象的锁。每一个对象只有一个锁与之相关联;实现同步需要很大的系统开销为代价,甚至可能造成死锁,所以尽量避免无谓的同步控制。
> 
> 
> 


**`Synchronized`与`static Synchronized`的区别**



> 
> * `synchronized`是对类的当前实例进行加锁,防止其他线程同时访问该类的该实例的所有`synchronized`块,同一个类的两个不同实例就没有这种约束(这个对象压根就是两个不相关的东西)。
> * 那么`static synchronized`恰好就是要控制类的所有实例的访问,`static synchronized`是限制线程同时访问jvm中该类的所有实例同时访问对应的代码块。
> 
> 
> 


#### 3.10 java中的NIO、BIO、AIO分别是什么


BIO:



> 
> * 同步并阻塞(互斥同步),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个链接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善;
> * BIO方式适用于链接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,jdk1.4以前的唯一选择,但程序直观简单易理解。
> 
> 
> 


NIO:



> 
> * 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的链接请求都会注册到多路复用器上,多路复用器轮询到链接有I/O请求时才启动一个线程进行处理。
> * NIO方式适用于链接数目多且链接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,jdk1.4开始支持。
> 
> 
> 


AIO:



> 
> * 异步非阻塞,服务器实现模式为一个有效一个线程,客户端的I/O请求都是由OS(操作系统)先完成了在通知服务器应用去启动线程进行处理;
> * AIO方式使用于链接数目多且链接比较长(重操作)的架构,比如相册服务器,充分调用os参与并发操作,编程比较复杂,jdk7开始支持。
> 
> 
> 


#### 3.11 什么是java内存模型(java memory model jmm)


描述了java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的低层细节。



> 
> * 所有的变量都存储在主内存中。
> * 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)。
> 
> 
> 


![这里写图片描述](https://img-blog.csdnimg.cn/img_convert/f97b78b5e555bf562054bd95f56bda17.png)  
 Java内存模型的两条规定:



> 
> * 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;
> * 不同线程无法直接访问其他线程工作内存中的变量,线程间变量值的访问都需要通过主内存来完成。
> 
> 
> 


#### 3.12 JVM线程死锁,你该如何判断是什么原因?如果用VisualVM,dump打印线程信息出来,会有哪些信息?


常常需要在隔两分钟后再收集一次`thread dump`,如果得到的输出相同,仍然是大量`thread`都在等待给同一个地址上的锁,那么肯定是死锁了。锁分类如下:



> 
> * **悲观锁**:无论共享数据是否产生争用、是否由于争用产生冲突,都会加锁。
> * **乐观锁**:假设没有共享数据争用,就执行成功。若监测出有共享数据争用产生冲突,就进行补救措施(如:重试)。
> * **可重入锁**:一个线程加锁了,该线程中调用其他的方法,也同样加锁了,如递归;
> * **读写锁**:对一个资源的访问,可以分成读锁和写锁;
> * **可中断锁**:一个线程等待的时间太长,可以中断等待的过程,去执行其他的事情;
> * **公平锁**:尽量以锁的顺序来获取锁;
> 
> 
> 


优化方面的锁:



> 
> * **自旋锁**(自适应锁):共享数据锁定状态比较短,对于阻塞的线程不要立马挂起,而是自旋一下就可得到,避免线程切换的开销。
> * **锁消除**:有些数据是不会被其他线程访问到的,这时候就不需要加同步措施,进行锁消除。
> * **锁粗化**:同步块的作用域一般越小越好,但是对一个对象的连续操作,不停的加锁解锁,这样会出现很大的性能问题。  
>  **轻量级锁**:为了减少获得锁和释放锁所带来的性能消耗。Java 1.6有无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态四个状态。随着竞争,不断升级,不能降级。  
>  **偏向锁**:目的是消除数据在无竞争情况下的同步原语。进一步提升程序的运行性能。 偏向锁就是偏心的锁,意思是这个锁会偏向第一个获得他的线程。
> 
> 


**ES6**

*   列举常用的ES6特性:

*   箭头函数需要注意哪些地方?

*   let、const、var

*   拓展:var方式定义的变量有什么样的bug?

*   Set数据结构

*   拓展:数组去重的方法

*   箭头函数this的指向。

*   手写ES6 class继承。

![](https://img-blog.csdnimg.cn/img_convert/aac1740e50faadb9a6a7a5b97f9ccba8.png)

**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**

**微信小程序**

*   简单描述一下微信小程序的相关文件类型?

*   你是怎么封装微信小程序的数据请求?

*   有哪些参数传值的方法?

*   你使用过哪些方法,来提高微信小程序的应用速度?

*   小程序和原生App哪个好?

*   简述微信小程序原理?

*   分析微信小程序的优劣势

*   怎么解决小程序的异步请求问题?

![](https://img-blog.csdnimg.cn/img_convert/60b1dbe5c76e264468aa993416a9a031.png)
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值