转载

Java面经分类以及总结(2)--思考

hashmap是一个非线程安全的集合。

他的线程不安全出现在,并发情况下可能会出现链表成环的问题,导致程序在执行get操作时形成死循环。

hashmap成环原因的代码出现在transfer代码中,也就是扩容之后的数据迁移部分

解决问题:

使用synchronize ,或者使用collection.synchronizeXXX方法。或者使用concurrentHashmap来解决。

2. HashTable 为什么是线程安全的?

为什么Hashtable是线程安全的,因为它的remove,put,get做成了同步方法,保证了Hashtable的线程安全性。

每个操作数据的方法都进行同步控制之后,由此带来的问题任何一个时刻只能有一个线程可以操纵Hashtable,所以其效率比较低。

3. HashMap和Hashtable的区别

HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。

  • HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并 可以接受null (HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
  • HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了 ConcurrentHashMap,它是HashTable的替代 ,比HashTable的扩展性更好。
  • 另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
  • 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
  • HashMap不能保证随着时间的推移Map中的元素次序是不变的。

4. 说一下 ConcurrentHashMap

利用CAS+Synchronized来保证并发更新的安全,底层依然采用"数组+链表+红黑树"的存储结构

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

比较 A 与 V 是否相等。(比较)
如果比较相等,将 B 写入 V。否则什么都不做(交换)
返回操作是否成功。
当多个线程尝试使用CAS同时更新同一个变量的时候,只有其中一个线程能够更新变量的值。当其他线程失败后,不会像获取锁一样被挂起,而是可以再次尝试,或者不进行任何操作,这种灵活性就大大减少了锁活跃性风险。
我们知道采用锁对共享数据进行处理的话,当多个线程竞争的时候,都需要进行加锁,没有拿到锁的线程会被阻塞,以及唤醒,这些都需要用户态到核心态的转换,这个代价对阻塞线程来说代价还是蛮高的,那cas是采用无锁乐观方式进行竞争,性能上要比锁更高些才是,为何不对锁竞争方式进行替换?

在高度竞争的情况下,锁的性能将超过cas的性能,但在中低程度的竞争情况下,cas性能将超过锁的性能。多数情况下,资源竞争一般都不会那么激烈。
复制代码
Java面经分类以及总结(2)--思考

5.说一下JVM 中的可达性分析算法

JVM中的堆和方法区主要用来存放对象(方法区中也储存了一些静态变量和全局变量等信息),那么我们要使用GC算法对其进行回收时首先要考虑的就是 该对象是否应该被回收 。即判断 该对象是否还有其他的引用或者关联使得该对象处于存活状态 ,我们需要将不在存活状态的对象 标记出 ,以便GC回收。

引用计数法:

在对象头处维护一个counter,每增加一次对该对象的引用计数器自加,如果对该对象的引用失联,则计数器自减。当counter为0时,表明该对象已经被废弃,不处于存活状态。这种方式一方面无法区分软、虛、弱、强引用类别。另一方面,会造成死锁,假设两个对象相互引用始终无法释放counter,永远不能GC

可达性分析算法:

一.通过一系列为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明该对象是不可用的。如果对象在进行可行性分析后发现没有与GC Roots相连的引用链,也不会理解死亡。

二、finalize()方法最终判定对象是否存活 即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。 标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

  1. 第一次标记并进行一次筛选。 筛选的条件是此对象是否有必要执行finalize()方法。 当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。
  2. 第二次标记 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为: F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行 。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。

Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己---- 只要重新与引用链上的任何的一个对象建立关联即可 ,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。 。

Java面经分类以及总结(2)--思考

在Java中,可作为GC Root的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

标记-清除算法

该算法分为标记和清除两个阶段。标记就是把所有活动对象都做上标记的阶段;清除就是将没有做上标记的对象进行回收的阶段。如下图所示。
复制代码
Java面经分类以及总结(2)--思考

复制算法

复制算法就是将内存空间按容量分成两块。当这一块内存用完的时候,就将还存活着的对象复制到另外一块上面,然后把已经使用过的这一块一次清理掉。这样使得每次都是对半块内存进行内存回收。内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶的指针,按顺序分配内存即可,实现简单,运行高效。

Java面经分类以及总结(2)--思考

标记-压缩算法

标记-压缩算法与标记-清理算法类似,只是后续步骤是让所有存活的对象移动到一端,然后直接清除掉端边界以外的内存。
复制代码
Java面经分类以及总结(2)--思考

6.Java引用

引用分为以下四类:

  • 强引用(Strong Reference)
  • 软引用(Soft Reference)
  • 弱引用(Weak Reference)
  • 虚引用(Phantom Reference) 这四种引用从上到下,依次减弱

3.1 强引用 强引用就是指在程序代码中普遍存在的,类似Object obj = new Object()这类似的引用, 只要强引用在,垃圾搜集器永远不会搜集被引用的对象 。也就是说, 宁愿出现内存溢出,也不会回收这些对象

3.2 软引用 软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象, 只有在内存不足的时候JVM才会回收该对象 。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。

3.3 弱引用 弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时, 无论内存是否充足,都会回收被弱引用关联的对象 。在java中,用java.lang.ref.WeakReference类来表示。下面是使用示例:

import java.lang.ref.WeakReference;

WeakReference<String> sr = new WeakReference<String>(new String("hello"));
     
    System.out.println(sr.get());
    System.gc();                //通知JVM的gc进行垃圾回收
    System.out.println(sr.get());
复制代码

3.4 虚引用 虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。 如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。 要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

8. GC Roots 具体包含哪些内容

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

9. Innodb 下如何解决幻读的问题?

CREATE TABLE `sys_comp` (
  `id_` bigint(32) NOT NULL,
  `name` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id_`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

insert into sys_comp valus(1,"小明")

insert into sys_comp valus(5,"伯安")

insert into sys_comp valus(10,"小李")
复制代码

这是一个很简单的表,id是主键,name是普通的列,现在一个事务要读取name= '伯安'的值,并且用的当前读,

select * from sys_comp where name = '伯安' for update ,这句话锁住了哪一行呢,id = 5的肯定锁住了,1和10有没有锁住呢,假设没有锁住,其他事务执行 update sys_comp set name = '伯安' where id = 10 ,这样子name = '伯安'的就有两行了,就出现了幻读,那把1 和10这两行也锁住把,这样这两行就不能更改了,但是还是不行,我再执行insert into sys_comp valus(15,"伯安"),第二次读依旧出现了幻行15,把所有的行都锁住,幻读依旧没有解决,

于是引入了一个新的锁,叫间隙锁,顾名思义,它把间隙也锁住了, ____ 1 ____ 5____10___,横线就是间隙锁, 间隙锁和间隙锁是不冲突的,他们都锁间隙,防止数据的插入 ,这样 执行插入就会block住,进入等待 ,从而解决了幻读问题,这也说明你平时遇到的死锁不单单是因为行锁造成的,间隙锁的范围可大多了,所以如果业务允许,你可以将你的mysql隔离级别设计成读已提交。

原文  https://juejin.im/post/5d7089d2f265da03f47c4184
正文到此结束
Loading...