Java基础知识点总结

来源:互联网 发布:利润率算法 编辑:程序博客网 时间:2024/05/21 19:55

JavaSE基础

1.equals与==的区别

区别1:==是一个运算符 equals是Object类的方法
区别2:
a.用于基本类型的变量比较时:==用于比较值是否相等,equals不能直接用于基本数据类型的比较,需要转换为其对应的包装类型。
b.用于引用类型的比较时。==和equals都是比较栈内存中的地址是否相等 。相等为true 否则为false。但是通常会重写equals方法去实现对象内容的比较。

2.Object有哪些公用方法?

clone equals hashcode toString wait notify notifyAll finalize getClass
除了clone和finalize其他均为公共方法。

3.Java中的四种引用

StrongReference:JVM默认,只要StrongReference存在,GC将不会回收被引用的对象
SoftReference:尽可能长时间保留引用,直到JVM内存不足,适合某些缓存应用
WeakReference:尽可能长时间的存活于JVM内直到下一次GC发生前
PhantomReference(虚引用):它是最弱的一种引用关系,也无法通过PhantomReference取得对象的实例。仅用来当该对象被回收时收到一个通知

4.hashCode的作用

hashCode是Object类的一个方法。hashCode() 方法是相应对象整型的 hash 值。它常用于基于 hash 的集合类,如 Hashtable、HashMap、LinkedHashMap等等。它与 equals() 方法关系特别紧密。根据 Java 规范,两个使用 equals() 方法来判断相等的对象,必须具有相同的 hash code。也就是说:
a.如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等
b.如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同
c.如果两个对象的hashcode值不等,则equals方法得到的结果必定为false
d.如果两个对象的hashcode值相等,则equals方法得到的结果未知

注意的一点是:在重写equals方法的同时,必须重写hashCode方法。让equals方法和hashCode方法在逻辑上保持一致性。

为什么要重写:不重写可能会违反hashCode方法的约定,即“相等的对象必须具有相等的hashcode值”,导致对基于 hash 的集合类无法正常工作,比如说HashMap,它是使用Key对象的hashCode()和equals()方法去决定key-value对的。如果只重写equals方法,将某个对象用put方法放入散列桶中,get时可能从另外一个散列桶获取,因为两个方法的传入参数的equals虽然相同,但hashcode可能不同。

5.String、StringBuffer与StringBuilder的区别

第一点:可变和适用范围。String对象是不可变的,而StringBuffer和StringBuilder是可变字符序列。每次对String的操作相当于生成一个新的String对象,而对StringBuffer和StringBuilder的操作是对对象本身的操作,而不会生成新的对象,所以对于频繁改变内容的字符串避免使用String,因为频繁的生成对象将会对系统性能产生影响。
第二点:线程安全。String由于有final修饰,是immutable的,安全性是简单而纯粹的。StringBuilder和StringBuffer的区别在于StringBuilder不保证同步,也就是说如果需要线程安全需要使用StringBuffer,不需要同步的StringBuilder效率更高。

6.并发和并行

“并行”是指无论从微观还是宏观,二者都是一起执行的,也就是同一时刻执行。
“并发”在微观上不是同时执行的。是在同一时间间隔交替轮流执行

7.switch能否用String做参数

Java1.7开始支持,但实际这是一颗Java语法糖。除此之外,byte,short,long,枚举,boolean均可用于switch,只有浮点型不可以。

8.Java语法糖

  1. Java7的switch用字符串 - 实际为hashcode方法
  2. 伪泛型 - 实际为List原始类型
  3. 自动装箱拆箱 - 实际为Integer.valueOf和Integer.intValue
  4. foreach遍历 - 实际为Iterator迭代器实现
  5. 条件编译
  6. enum枚举类、内部类
  7. 可变参数 - 实际为数组实现
  8. 断言语言
  9. try语句中定义和关闭资源

9.try catch finally问题

1)不管有没有出现异常,finally块中代码都会执行
2)当try和catch中有return时,finally仍然会执行
3)finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,不管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前确定的
4)finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值

10.受检查异常和运行时异常

1.粉红色的是受检查的异常(checked exceptions),其必须被try…catch语句块所捕获,或者在方法签名里通过throws子句声明。受检查的异常必须在编译时被捕捉处理,命名为Checked Exception是因为Java编译器要进行检查,Java虚拟机也要进行检查,以确保这个规则得到遵守。
2.绿色的异常是运行时异常(runtime exceptions),需要程序员自己分析代码决定是否捕获和处理,比如空指针,被0除…
3.而声明为Error的,则属于严重错误,如系统崩溃、虚拟机错误、动态链接失败等,这些错误无法恢复或者不可能捕捉,将导致应用程序中断,Error不需要捕捉。

11.面向对象的三大特性:封装、继承、多态

封装

1.概念:就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。
2.好处:隐藏内部实现细节。

继承

1.概念:继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力
2.好处:提高代码的复用,缩短开发周期。

多态

1.概念:多态(Polymorphism)按字面的意思就是“多种状态,即同一个实体同时具有多种形式。一般表现形式是程序在运行的过程中,同一种类型在不同的条件下表现不同的结果。多态也称为动态绑定,一般是在运行时刻才能确定方法的具体执行对象。
2.好处:
1)将接口和实现分开,改善代码的组织结构和可读性,还能创建可拓展的程序。
2)消除类型之间的耦合关系。允许将多个类型视为同一个类型。
3)一个多态方法的调用允许有多种表现形式

12.抽象类与接口

1.一个子类只能继承一个抽象类,但能实现多个接口
2.抽象类可以有构造方法,接口没有构造方法
3.抽象类可以有普通成员变量,接口没有普通成员变量
4.抽象类和接口都可有静态成员变量,抽象类中静态成员变量访问类型任意,接口只能public static final(默认)
5.抽象类可以没有抽象方法,抽象类可以有普通方法,接口中都是抽象方法
6.抽象类可以有静态方法,接口不能有静态方法
7.抽象类中的方法可以是public、protected和默认;接口方法只有public abstract

13.静态内部类和普通内部类

静态内部类不需要有指向外部类的引用。但非静态内部类需要持有对外部类的引用。非静态内部类能够访问外部类的静态和非静态成员。静态类不能访问外部类的非静态成员。他只能访问外部类的静态成员。

集合框架

1.List集合和Set集合

List接口:
List中元素存取是有序的、可重复的。
CopyOnWriteArrayList:COW的策略,即写时复制的策略。适用于读多写少的并发场景

Set接口:
Set集合元素存取无序,且元素不可重复。
HashSet不保证迭代顺序,线程不安全;LinkedHashSet是Set接口的哈希表和链接列表的实现,保证迭代顺序,线程不安全;TreeSet可以对Set集合中的元素排序,元素以二叉树形式存放,线程不安全。

2.ArrayList、LinkedList、Vector的区别

首先它们均是List接口的实现。
ArrayList、LinkedList的区别
1.随机存取:ArrayList是基于可变大小的数组实现,LinkedList是链接列表的实现。这也就决定了对于随机访问的get和set的操作,ArrayList要优于LinkedList,因为LinkedList要移动指针。
2.插入和删除:LinkedList要好一些,因为ArrayList要移动数据,更新索引。
3.内存消耗:LinkedList需要更多的内存,因为需要维护指向后继结点的指针。
Vector从Java 1.0起就存在,在1.2时改为实现List接口,功能与ArrayList类似,但是Vector具备线程安全。

3.Map集合

Hashtable:基于Dictionary类,线程安全,速度快。底层是哈希表数据结构。是同步的。不允许null作为键,null作为值。
Properties:Hashtable的子类。用于配置文件的定义和操作,使用频率非常高,同时键和值都是字符串。

HashMap:线程不安全,底层是数组加链表实现的哈希表。允许null作为键,null作为值。HashMap去掉了contains方法。
注意:HashMap不保证元素的迭代顺序。如果需要元素存取有序,请使用LinkedHashMap

TreeMap:可以用来对Map集合中的键进行排序。
ConcurrentHashMap:是JUC包下的一个并发集合。

为什么使用ConcurrentHashMap而不是HashMap或Hashtable?
HashMap的缺点:主要是多线程同时put时,如果同时触发了rehash操作,会导致HashMap中的链表中出现循环节点,进而使得后面get的时候,会死循环,CPU达到100%,所以在并发情况下不能使HashMap。让HashMap同步:Map m = Collections.synchronizeMap(hashMap);而Hashtable虽然是同步的,使用synchronized来保证线程安全,但在线程竞争激烈的情况下Hashtable的效率非常低下。因为当一个线程访问Hashtable的同步方法,其他线程访问Hashtable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。

ConcurrentHashMap的原理:
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因在于所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap的结构:
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,当对某个HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

ConcurrentHashMap的构造、get、put操作:
构造函数:传入参数分别为 1、初始容量,默认16 2、装载因子 装载因子用于rehash的判定,就是当ConcurrentHashMap中的元素大于装载因子*最大容量时进行扩容,默认0.75 3、并发级别 这个值用来确定Segment的个数,Segment的个数是大于等于concurrencyLevel的第一个2的n次方的数。比如,如果concurrencyLevel为12,13,14,15,16这些数,则Segment的数目为16(2的4次方)。默认值为static final int DEFAULT_CONCURRENCY_LEVEL = 16;。理想情况下ConcurrentHashMap的真正的并发访问量能够达到concurrencyLevel,因为有concurrencyLevel个Segment,假如有concurrencyLevel个线程需要访问Map,并且需要访问的数据都恰好分别落在不同的Segment中,则这些线程能够无竞争地自由访问(因为他们不需要竞争同一把锁),达到同时访问的效果。这也是为什么这个参数起名为“并发级别”的原因。默认16.

初始化的一些动作:
初始化segments数组(根据并发级别得到数组大小ssize),默认16
初始化segmentShift和segmentMask(这两个全局变量在定位segment时的哈希算法里需要使用),默认情况下segmentShift为28,segmentMask为15
初始化每个Segment,这一步会确定Segment里HashEntry数组的长度.

put操作:
1、判断value是否为null,如果为null,直接抛出异常。
2、key通过一次hash运算得到一个hash值。将得到hash值向右按位移动segmentShift位,然后再与segmentMask做&运算得到segment的索引j。即segmentFor方法
3、使用Unsafe的方式从Segment数组中获取该索引对应的Segment对象。向这个Segment对象中put值,这个put操作也基本是一样的步骤(通过&运算获取HashEntry的索引,然后set)。

get操作:
1、和put操作一样,先通过key进行hash确定应该去哪个Segment中取数据。
2、使用Unsafe获取对应的Segment,然后再进行一次&运算得到HashEntry链表的位置,然后从链表头开始遍历整个链表(因为Hash可能会有碰撞,所以用一个链表保存),如果找到对应的key,则返回对应的value值,如果链表遍历完都没有找到对应的key,则说明Map中不包含该key,返回null。

定位Segment的hash算法:(hash >>> segmentShift) & segmentMask
定位HashEntry所使用的hash算法:int index = hash & (tab.length - 1);
注:tab为HashEntry数组

4.Collection 和 Collections的区别

Collection是集合类的上级接口,子接口主要有Set 和List、Queue
Collections是针对集合类的一个帮助类,提供了操作集合的工具方法:一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

5.Map、Set、List、Queue、Stack的特点与用法

Set集合类似于一个罐子,”丢进”Set集合里的多个对象之间没有明显的顺序。 List集合代表元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。 Stack是Vector提供的一个子类,用于模拟”栈”这种数据结构(LIFO后进先出) ,Queue用于模拟”队列”这种数据结构(先进先出 FIFO)。 Map用于保存具有”映射关系”的数据,因此Map集合里保存着两组值。

6.HashMap的工作原理

HashMap维护了一个Entry数组,Entry内部类有key,value,hash和next是个字段,其中next也是一个Entry类型。可以将Entry数组理解为一个个的散列桶。每一个桶实际上是一个单链表。当执行put操作时,会根据key的hashcode定位到相应的桶。遍历单链表检查该key是否已经存在,如果存在,覆盖该value,反之,新建一个新的Entry,并放在单链表的头部。当通过传递key调用get方法时,它再次使用key.hashCode()来找到相应的散列桶,然后使用key.equals()方法找出单链表中正确的Entry,然后返回它的值。

7.HashMap和Hashtable的区别

  • Hashtable是基于陈旧的Dictionary的Map接口的实现,而HashMap是基于哈希表的Map接口的实现
  • 从方法上看,HashMap去掉了Hashtable的contains方法
  • HashTable是同步的(线程安全),而HashMap线程不安全
  • HashMap允许空键值,而Hashtable不允许
  • HashMap的iterator迭代器执行快速失败机制,也就是说在迭代过程中修改集合结构,除非调用迭代器自身的remove方法,否则以其他任何方式的修改都将抛出并发修改异常。如果寻求迭代的时候修改Map,可以使用ConcurrentHashMap。而Hashtable返回的Enumeration不是快速失败的。

8.Map的实现类的介绍

HashMap基于散列表来的实现,即使用hashCode()进行快速查询元素的位置,显著提高性能。插入和查询“键值对”的开销是固定的。可以通过设置容量和装载因子,以调整容器的性能。

LinkedHashMap, 类似于HashMap,但是迭代遍历它时,保证迭代的顺序是其插入的次序,因为它使用链表维护内部次序。此外可以在构造器中设定LinkedHashMap,使之采用LRU算法。使没有被访问过的元素或较少访问的元素出现在前面,访问过的或访问多的出现在后面。这对于需要定期清理元素以节省空间的程序员来说,此功能使得程序员很容易得以实现。

TreeMap, 是基于红黑树的实现。同时TreeMap实现了SortedMap接口,该接口可以确保键处于排序状态。所以查看“键”和“键值对”时,所有得到的结果都是经过排序的,次序由自然排序或提供的Comparator决定。SortedMap接口拥有其他额外的功能,如:返回当前Map使用的Comparator比较强,firstKey(),lastKey(),headMap(toKey),tailMap(fromKey)以及可以返回一个子树的subMap()方法等。

WeakHashMap,表示弱键映射,WeakHashMap 的工作与正常的 HashMap 类似,但是使用弱引用作为 key,意思就是当 key 对象没有任何引用时,key/value 将会被回收。

ConcurrentHashMap, 在HashMap基础上分段锁机制实现的线程安全的HashMap。

IdentityHashMap 使用==代替equals() 对“键”进行比较的散列映射。专为解决特殊问题而设计。

HashTable:基于Dictionary类的Map接口的实现,它是线程安全的。

9.LinkedList 和 PriorityQueue 的区别

它们均是Queue接口的实现。拥有FIFO的特点,它们的区别在于排序行为。LinkedList 支持双向列表操作,
PriorityQueue 按优先级组织的队列,元素的出队次序由元素的自然排序或者由Comparator比较器指定。

10.线程安全的集合类。Vector、HashTable、Properties和Stack

11.BlockingQueue

Java.util.concurrent.BlockingQueue是一个队列,在进行获取元素时,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。

12.如何对一组对象进行排序

如果需要对一个对象数组进行排序,我们可以使用Arrays.sort()方法。如果我们需要排序一个对象列表,我们可以使用Collections.sort()方法。排序时是默认根据元素的自然排序(使用Comparable)或使用Comparator外部比较器。Collections内部使用数组排序方法,所有它们两者都有相同的性能,只是Collections需要花时间将列表转换为数组。

13.Comparable和Comparator接口区别

Comparator位于包java.util下,而Comparable位于包java.lang下

如果我们需要使用Arrays或Collections的排序方法对对象进行排序时,我们需要在自定义类中实现Comparable接口并重写compareTo方法,compareTo方法接收一个参数,如果this对象比传递的参数小,相等或大时分别返回负整数、0、正整数。Comparable被用来提供对象的自然排序。String、Integer实现了该接口。

Comparator比较器的compare方法接收2个参数,根据参数的比较大小分别返回负整数、0和正整数。
Comparator 是一个外部的比较器,当这个对象自然排序不能满足你的要求时,你可以写一个比较器来完成两个对象之间大小的比较。用 Comparator 是策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为。

14.与Java集合框架相关的有哪些最好的实践

(1)根据需要选择正确的集合类型。比如,如果指定了大小,我们会选用Array而非ArrayList。如果我们想根据插入顺序遍历一个Map,我们需要使用TreeMap。如果我们不想重复,我们应该使用Set。
(2)一些集合类允许指定初始容量,所以如果我们能够估计到存储元素的数量,我们可以使用它,就避免了重新哈希或大小调整。
(3)基于接口编程,而非基于实现编程,它允许我们后来轻易地改变实现。
(4)总是使用类型安全的泛型,避免在运行时出现ClassCastException。
(5)使用JDK提供的不可变类作为Map的key,可以避免自己实现hashCode()和equals()。

IO/NIO

1.IO和NIO

在以前的Java IO中,都是阻塞式IO,NIO引入了非阻塞式IO。
第一种方式:我从硬盘读取数据,然后程序一直等,数据读完后,继续操作。这种方式是最简单的,叫阻塞IO。
第二种方式:我从硬盘读取数据,然后程序继续向下执行,等数据读取完后,通知当前程序(对硬件来说叫中断,对程序来说叫回调),然后此程序可以立即处理数据,也可以执行完当前操作在读取数据。

2.流与块的比较

原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。这样做是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。
一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

3.通道与流

Channel是一个对象,可以通过它读取和写入数据。通道与流功能类似,不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而通道可以用于读、写或者同时用于读写。

4.缓冲区Buffer

在 NIO 库中,所有数据都是用缓冲区处理的。

Position: 表示下一次访问的缓冲区位置
Limit: 表示当前缓冲区存放的数据容量。
Capacity:表示缓冲区最大容量

flip()方法:它将 limit 设置为当前 position。它将 position 设置为 0

clear方法:它将 limit 设置为与 capacity 相同。它设置 position 为 0。

线程

1.什么是线程

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个任务要100 毫秒,那么用十个线程完成改任务只需 10 毫秒。Java在语言层面对多线程提供了很好的支持。

2.线程和进程有什么区别

从概念上:
进程:一个程序对一个数据集的动态执行过程,是分配资源的基本单位。
线程:存在于进程内,是进程内的基本调度单位。共享进程的资源。

从执行过程中来看:
进程:拥有独立的内存单元,而多个线程共享内存,从而提高了应用程序的运行效率。
线程:每一个独立的线程,都有一个程序运行的入口、顺序执行序列、和程序的出口。但是线程不能够独立的执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

从逻辑角度来看:(重要区别)
多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但是,操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理及资源分配。

简言之,一个程序至少有一个进程,一个进程至少有一个线程。进程是资源分配的基本单位,线程共享进程的资源。

3.如何在 Java 中实现线程

继承Thread类或实现Runnable接口。

4.用 Runnable 还是 Thread

Java 不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是实现Runnable接口好了。

5.Thread 类中的 start () 和 run () 方法有什么区别

start ()方法被用来启动新创建的线程,而且 start ()内部调用了 run ()方法,这和直接调用 run ()方法的效果不一样。当你调用 run ()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start ()方法才会启动新线程。也就是用start方法来启动线程,才是真正实现了多线程。而run方法只是一个普通方法。

6.Java 中 Runnable 和 Callable 有什么不同

Runnable和 Callable 都代表那些要在不同的线程中执行的任务。Runnable 从 JDK1.0 开始就有了,Callable 是在 JDK1.5 增加的。它们的主要区别是 Callable 的 call () 方法可以返回值和抛出异常,而 Runnable 的 run ()方法没有这些功能。

7.Java 中 CyclicBarrier 和 CountDownLatch 有什么不同

它们都是JUC下的类,CyclicBarrier 和 CountDownLatch 都可以用来让一组线程等待其它线程。区别在于CountdownLatch计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。

8.Java 内存模型是什么

Java 内存模型规定和指引Java 程序在不同的内存架构、CPU 和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系定义了一些规则让程序员在并发编程时思路更清晰。

线程内的代码能够按先后顺序执行,这被称为程序次序规则。
对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。
一个线程内的任何操作必需在这个线程的 start ()调用之后,也叫作线程启动规则。
一个线程的所有操作都会在线程终止之前,线程终止规则。
一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。

9.Java 中的 volatile 变量是什么

Java 语言提供了一种稍弱的同步机制,即volatile变量。但是volatile并不容器完全被正确、完整的理解。
一般来说,volatile具备2条语义,或者说2个特性。第一是保证volatile修饰的变量对所有线程的可见性,这里的可见性是指当一条线程修改了该变量,新值对于其它线程来说是立即可以得知的。而普通变量做不到这一点。

第二条语义是禁止指令重排序优化,这条语义在JDK1.5才被修复。

关于第一点:根据JMM,所有的变量存储在主内存,而每个线程还有自己的工作内存,线程的工作内存保存该线程使用到的变量的主内存副本拷贝,线程对变量的操作在工作内存中进行,不能直接读写主内存的变量。在volatile可见性这一点上,普通变量做不到的原因正因如此。比如,线程A修改了一个普通变量的值,然后向主内存进行回写,线程B在线程A回写完成后再从主内存读取,新变量才能对线程B可见。其实,按照虚拟机规范,volatile变量依然有工作内存的拷贝,要借助主内存来实现可见性。但由于volatile的特殊规则保证了新值能立即同步回主内存,以及每次使用从主内存刷新,以此保证了多线程操作volatile变量的可见性。

关于第二点:先说指令重排序,指令重排序是指CPU采用了允许将多条指令不按规定顺序分开发送给相应的处理单元处理,但并不是说任意重排,CPU需要正确处理指令依赖情况确保最终的正确结果,指令重排序是机器级的优化操作。那么为什么volatile要禁止指令重排序呢,又是如何去做的。举例,DCL(双重检查加锁)的单例模式。volatile修饰后,代码中将会插入许多内存屏障指令保证处理器不发生乱序执行。同时由于Happens-before规则的保证,在刚才的例子中写操作会发生在后续的读操作之前。

除了以上2点,volatile还保证对于64位long和double的读取是原子性的。因为在JMM中允许虚拟机对未被volatile修饰的64位的long和double读写操作分为2次32位的操作来执行,这也就是所谓的long和double的非原子性协定。

基于以上几点,我们知道volatile虽然有这些语义和特性在并发的情况下仍然不能保证线程安全。大部分情况下仍然需要加锁。

除非是以下2种情况,1.运算结果不依赖变量的当前值,或者能够确保只有单一线程修改变量的值;2.变量不需要与其他的状态变量共同参与不变约束。

10.Java 中,编写多线程程序的时候你会遵循哪些最佳实践?

a)给线程命名,这样可以帮助调试。
b)最小化同步的范围,而不是将整个方法同步,只对关键部分做同步。
c)如果可以,更偏向于使用 volatile 而不是 synchronized。
d)使用更高层次的并发工具,而不是使用 wait() 和 notify() 来实现线程间通信,如 BlockingQueue,CountDownLatch 及 Semeaphore。
e)优先使用并发集合,而不是对集合进行同步。并发集合提供更好的可扩展性。

11.什么是线程安全?Vector 是一个线程安全类吗

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似的 ArrayList 不是线程安全的。

12.Java 中什么是竞态条件? 举个例子说明。

竞态条件会导致程序在并发情况下出现一些 bugs。多线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的 bugs。这种 bugs 很难发现而且会重复出现,因为线程间的随机竞争。一个例子就是无序处理。

13.Java 中如何停止一个线程

当 run () 或者 call () 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用 volatile 布尔变量来退出 run ()方法的循环或者是取消任务来中断线程。其他情形:异常 - 停止执行 休眠 - 停止执行 阻塞 - 停止执行

14.一个线程运行时发生异常会怎样

简单的说,如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候 JVM 会使用 Thread.getUncaughtExceptionHandler ()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException ()方法进行处理。

15.如何在两个线程间共享数据?

通过共享对象来实现这个目的,或者是使用像阻塞队列这样并发的数据结构

16.Java 中 notify 和 notifyAll 有什么区别

notify ()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而 notifyAll ()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。

17.为什么 wait, notify 和 notifyAll 这些方法不在 thread 类里面

一个很明显的原因是 JAVA 提供的锁是对象级的而不是线程级的。如果线程需要等待某些锁那么调用对象中的 wait ()方法就有意义了。如果 wait ()方法定义在 Thread 类中,线程正在等待的是哪个锁就不明显了。简单的说,由于 wait,notify 和 notifyAll 都是锁级别的操作,所以把他们定义在 Object 类中因为锁属于对象。

18.什么是ThreadLocal

ThreadLocal,线程局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本,是线程隔离的。线程隔离的秘密在于ThreadLocalMap类(ThreadLocal的静态内部类)

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

ThreadLocal的方法:void set(T value)、T get()以及T initialValue()。

ThreadLocal是如何为每个线程创建变量的副本的?
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。初始时,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

总结:

a.实际通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的
b.为何threadLocals的键为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
c.在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法

19.什么是 FutureTask?

在 Java 并发程序中 FutureTask 表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是调用了 Runnable 接口所以它可以提交给 Executor 来执行。

20.Java 中 interrupted 和 isInterruptedd 方法的区别

interrupted是静态方法,isInterruptedd是一个普通方法

如果当前线程被中断(没有抛出中断异常,否则中断状态就会被清除),你调用interrupted方法,第一次会返回true。然后,当前线程的中断状态被方法内部清除了。第二次调用时就会返回false。如果你刚开始一直调用isInterrupted,则会一直返回true,除非中间线程的中断状态被其他操作清除了。也就是说isInterrupted 只是简单的查询中断状态,不会对状态进行修改。

21.为什么 wait 和 notify 方法要在同步块中调用

如果不这么做,代码会抛出 IllegalMonitorStateException异常。还有一个原因是为了避免 wait 和 notify 之间产生竞态条件。

22.为什么你应该在循环中检查等待条件?

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在 notify ()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用 wait ()方法效果更好的原因。

23.Java 中的同步集合与并发集合有什么区别

同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java1.5加入了并发集合像 ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。它们大部分位于JUC包下。

24.Java 中堆和栈有什么不同

每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时 volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。

25.什么是线程池? 为什么要使用它?

创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从 JDK1.5 开始,Java API 提供了 Executor 框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。

26.如何写代码来解决生产者消费者问题?

在现实中你解决的许多线程问题都属于生产者消费者模型,就是一个线程生产任务供其它线程进行消费,你必须知道怎么进行线程间通信来解决这个问题。比较低级的办法是用 wait 和 notify 来解决这个问题,比较赞的办法是用 Semaphore 或者 BlockingQueue 来实现生产者消费者模型。

27.如何避免死锁?

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:

互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。

28.Java 中活锁和死锁有什么区别?

活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。

29.怎么检测一个线程是否拥有锁

在 java.lang.Thread 中有一个方法叫 holdsLock (),当且仅当当前线程拥有某个具体对象的锁时它返回true。

30.你如何在 Java 中获取线程堆栈

eak 组合键来获取线程堆栈,Linux 下用 kill -3 命令。你也可以用 jstack 这个工具来获取,它对线程 id 进行操作,你可以用 jps 这个工具找到 id。

31.JVM内存配置参数

1.-Xmx:最大堆大小2.-Xms:初始堆大小(最小内存值)3.-Xmn:年轻代大小4.-XXSurvivorRatio:3 意思是Eden:Survivor=3:25.-Xss:栈容量6.-XX:+PrintGC 输出GC日志7.-XX:+PrintGCDetails 输出GC的详细日志

32.Java 中 synchronized 和 ReentrantLock 有什么不同

Java 在过去很长一段时间只能通过 synchronized 关键字来实现互斥,它有一些缺点。比如你不能扩展锁之外的方法或者块边界,尝试获取锁时不能中途取消等。Java 5 通过 Lock 接口提供了更复杂的控制来解决这些问题。 ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义且它还具有可扩展性。

33.有三个线程 T1,T2,T3,怎么确保它们按顺序执行

可以用线程类的 join ()方法。具体操作是在T3的run方法中调用t2.join(),让t2执行完再执行t3;T2的run方法中调用t1.join(),让t1执行完再执行t2。这样就按T1,T2,T3的顺序执行了

34.Thread 类中的 yield 方法有什么作用

Yield 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU,执行 yield ()的线程有可能在进入到暂停状态后马上又被执行。

35.Java 中 ConcurrentHashMap 的并发度是什么

ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。

36.Java 中 Semaphore是什么

JUC下的一种新的同步类,它是一个计数信号。从概念上讲,Semaphore信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个 acquire (),然后再获取该许可。每个 release ()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。信号量常常用于多线程的代码中,比如数据库连接池。

37.如果你提交任务时,线程池队列已满。会发会生什么?

这个问题问得很狡猾,许多程序员会认为该任务会阻塞直到线程池队列有空位。事实上如果一个任务不能被调度执行那么 ThreadPoolExecutor’s submit ()方法将会抛出一个 RejectedExecutionException 异常。

38.Java 线程池中 submit () 和 execute ()方法有什么区别

两个方法都可以向线程池提交任务,execute ()方法的返回类型是 void,它定义在 Executor 接口中, 而 submit ()方法可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。

39.什么是阻塞式方法?

阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket 的 accept ()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。

40.Swing 是线程安全的吗?

你可以很肯定的给出回答,Swing 不是线程安全的。你不能通过任何线程来更新 Swing 组件,如 JTable、JList 或 JPanel,事实上,它们只能通过 GUI 或 AWT 线程来更新。这就是为什么 Swing 提供 invokeAndWait() 和 invokeLater() 方法来获取其他线程的 GUI 更新请求。这些方法将更新请求放入 AWT 的线程队列中,可以一直等待,也可以通过异步更新直接返回结果。

41.Java 中 invokeAndWait 和 invokeLater 有什么区别

这两个方法是 Swing API 提供给 Java 开发者用来从当前线程而不是事件派发线程更新 GUI 组件用的。InvokeAndWait ()同步更新 GUI 组件,比如一个进度条,一旦进度更新了,进度条也要做出相应改变。如果进度被多个线程跟踪,那么就调用 invokeAndWait ()方法请求事件派发线程对组件进行相应更新。而 invokeLater ()方法是异步调用更新组件的。

42.Swing API 中那些方法是线程安全的?

虽然Swing不是线程安全的但是有一些方法是可以被多线程安全调用的。如repaint (), revalidate ()。 JTextComponent 的 setText ()方法和 JTextArea 的 insert () 和 append () 方法也是线程安全的。

43.如何在 Java 中创建 Immutable 对象

Immutable 对象可以在没有同步的情况下共享,降低了对该对象进行并发访问时的同步化开销。可是 Java 没有@Immutable 这个注解符,要创建不可变类,要实现下面几个步骤:通过构造方法初始化所有成员、对变量不要提供 setter 方法、将所有的成员声明为私有的,这样就不允许直接访问这些成员、在 getter 方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。

44.Java 中的 ReadWriteLock 是什么?

一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果。Java 中的 ReadWriteLock 是 Java 5 中新增的一个接口,一个 ReadWriteLock 维护一对关联的锁,一个用于只读操作一个用于写。在没有写线程的情况下一个读锁可能会同时被多个读线程持有。写锁是独占的,你可以使用 JDK 中的 ReentrantReadWriteLock 来实现这个规则,它最多支持 65535 个写锁和 65535 个读锁。

45.多线程中的忙循环是什么?

忙循环就是程序员用循环让一个线程等待,不像传统方法 wait (), sleep () 或 yield () 它们都放弃了 CPU 控制,而忙循环不会放弃 CPU,它就是在运行一个空循环。这么做的目的是为了保留 CPU 缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。

46.volatile 变量和 atomic 变量有什么不同

volatile 变量和 atomic 变量看起来很像,但功能却不一样。volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量那么 count++ 操作并不是原子性的。而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如 getAndIncrement ()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

47.如果同步块内的线程抛出异常会发生什么?

无论你的同步块是正常还是异常退出的,里面的线程都会释放锁,所以对比锁接口我更喜欢同步块,因为它不用我花费精力去释放锁,该功能可以在 finally block 里释放锁实现。

48.如何在 Java 中创建线程安全的 Singleton

5种,急加载,同步方法,双检锁,静态内部类,枚举

49.如何强制启动一个线程?

这个问题就像是如何强制进行 Java 垃圾回收,目前还没有觉得方法,虽然你可以使用 System.gc ()来进行垃圾回收,但是不保证能成功。在 Java 里面没有办法强制启动一个线程,它是被线程调度器控制着且 Java 没有公布相关的 API。

50.Java 中的 fork join 框架是什么?

fork join 框架是 JDK7 中出现的一款高效的工具,Java 开发人员可以通过它充分利用现代服务器上的多处理器。它是专门为了那些可以递归划分成许多子模块设计的,目的是将所有可用的处理能力用来提升程序的性能。fork join 框架一个巨大的优势是它使用了工作窃取算法,可以完成更多任务的工作线程可以从其它线程中窃取任务来执行。

51.Java 多线程中调用 wait () 和 sleep ()方法有什么不同?

Java 程序中 wait 和 sleep 都会造成某种形式的暂停,它们可以满足不同的需要。wait ()方法意味着条件等待,如果等待条件为真且其它线程被唤醒时它会释放锁,而 sleep ()方法仅仅释放 CPU 资源或者让当前线程短暂停顿,但不会释放锁。

52.双亲委派模型中的方法

findLoadedClass(),LoadClass(),findBootstrapClassOrNull(),findClass(),resolveClass()

53.NIO、AIO、BIO

BIO即同步阻塞IO,适用于连接数目较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4之前的唯一选择,但程序直观、简单、易理解。

NIO即同步非阻塞IO,适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

AIO即异步非阻塞IO,适用于连接数目多且连接比较长的架构,如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK1.7开始支持

GC、内存相关

1.对哪些区域回收
Java运行时数据区域:程序计数器、JVM栈、本地方法栈、方法区和堆。

由于程序计数器、JVM栈、本地方法栈3个区域随线程而生随线程而灭,对这几个区域内存的回收和分配具有确定性。而方法区和堆则不一样,程序需要在运行时才知道创建哪些对象,对这部分内存的分配是动态的,GC关注的也就是这部分内存。

2.如何判定对象需要回收
引用计数法:给对象加上一个计数器,当有一个地方引用它,计数器+1,引用失效时,计数器-1,当计数器为0时,判定该对象可回收。引用计数法优点是实现简单,python,flashplayer等使用引用计数法进行内存管理。引用计数法的缺点在于无法解决循环引用的问题。

在Java中使用可达性分析算法法判定对象是否“死亡”。可达性分析法是指通过称为GC-Roots的对象为起始点,从这些结点向下搜索,当从GCRoots到这个对象不可达时,被判定为可收回的对象。

3.可作为GC Roots的对象

可作为GC Roots的对象:虚拟机栈中引用的对象 方法区中静态属性引用的对象 方法区中常量引用的对象 本地方法栈中JNI引用的对象

4.对象的自我救赎

即使在可达性算法中判定为不可达时,也并非一定被回收。对象存在自我救赎的可能。要真正宣告对象的死亡,需要经历2次标记的过程。如果对象经过可达性分析法发现不可达时,对象将被第一次标记被进行筛选,筛选的条件是此对象是否有必要执行finalize方法。如果对象没有重写finalize方法或finalize方法已经被JVM调用过,则判定为不需要执行。

如果对象被判定为需要执行finalize方法,该对象将被放置在一个叫做F-Queue的队列中,JVM会建立一个低优先级的线程执行finalize方法,如果对象想要完成自我救赎需要在finalize方法中与引用链上的对象关联,比如把自己也就是this赋值给某个类变量。当GC第二次对F-Queue中对象标记时,该对象将被移出“即将回收”的集合,完成自我救赎。简言之,finalize方法是对象逃脱死亡命运的最后机会,并且任何对象的finalize方法只会被JVM调用一次。

5.垃圾回收算法

Mark-Sweep法:标记清除法 容易产生内存碎片,导致分配较大对象时没有足够的连续内存空间而提前出发GC。这里涉及到另一个问题,即对象创建时的内存分配,对象创建内存分配主要有2种方法,分别是指针碰撞法和空闲列表法。指针碰撞法:使用的内存在一侧,空闲的在另一侧,中间使用一个指针作为分界点指示器,对象内存分配时只要指针向空闲的移动对象大小的距离即可。
空闲列表法:使用的和空闲的内存相互交错无法进行指针碰撞,JVM必须维护一个列表记录哪些内存块可用,分配时从列表中找出一个足够的分配给对象,并更新列表记录。所以,当采用Mark-Sweep算法的垃圾回收器时,内存分配通常采用空闲列表法。

Copy法:将内存分为2块,每次使用其中的一块,当一块满了,将存活的对象复制到另一块,把使用过的那一块一次性清除。显然,Copy法解决了内存碎片的问题,但算法的代价是内存缩小为原来的一半。现代的垃圾收集器对新生代采用的正是Copy算法。但通常不执行1:1的策略,HotSpot虚拟机默认Eden区Survivor区8:1。每次使用Eden和其中一块Survivor区。也就是说新生代可用内存为新生代内存空间的90%。

Mark-Compact法:标记整理法。它的第一阶段与Mark-Sweep法一样,但不直接清除,而是将存活对象向一端移动,然后清除端边界以外的内存,这样也不存在内存碎片。

分代收集算法:将堆内存划分为新生代,老年代,根据新生代老年代的特点选取不同的收集算法。因为新生代对象大多朝生夕死,而老年代对象存活率高,没有额外空间进行分配担保,通常对新生代执行复制算法,老年代执行Mark-Sweep算法或Mark-Compact算法。

6.垃圾收集器

通常来说,新生代老年代使用不同的垃圾收集器。新生代的垃圾收集器有Serial(单线程)、ParNew(Serial的多线程版本)、ParallelScavenge(吞吐量优先的垃圾收集器),老年代有SerialOld(单线程老年代)、ParallelOld(与ParallelScavenge搭配的多线程执行标记整理算法的老年代收集器)、CMS(标记清除算法,容易产生内存碎片,可以开启内存整理的参数),以及当前最先进的垃圾收集器G1,G1通常面向服务器端的垃圾收集器,在我自己的Java应用程序中通过-XX:+PrintGCDetails,发现自己的垃圾收集器是使用了ParallelScavenge+ParallelOld的组合。

7.内存分配和回收的策略

a.对象优先在Eden区分配,默认Eden与Survivor的比例为8:1
b.大对象直接进入老年代
c.长期存活的进入老年代:JVM给每个对象定义一个年龄计数器,当对象在Eden区出生并躲过一次MinorGC,并且Survivor可以容纳的话,将被移入Survivor区,年龄设为1。以后每在Survivor区躲过一次MinorGC,年龄加一岁,当对象年龄加到15岁时,晋升到老年代。当然15岁的默认值可以通过-XX虚拟机参数设置。
d.动态对象年龄判定:有的时候无需到达15岁即晋升老年代。判定方法是如果Survivor区中相同年龄的所有对象大小的总和大于Survivor区空间的一半,年龄大于或等于该年龄的对象直接进入老年代
e.空间分配担保

在发生MinorGC之前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总和,如果成立,确保这次MinorGC安全。否则,虚拟机会查看HandlePromotionFailure设置是否允许担保失败。如果允许,虚拟机会接着查看老年代最大连续可用空间是否大于历次晋升到老年代对象的平均大小,如果大于,则进行一次MinorGC,尽管这次MinorGC是有风险的,如果小于或者HandlePromotionFailure设置为不允许,要改为一次FullGC

8.方法区的回收

方法区通常会与永久代划等号,实际上二者并不等价,只不过是HotSpot虚拟机设计者用永久代实现方法区,并将GC分代扩展至方法区。
永久代垃圾回收通常包括两部分内容:废弃常量和无用的类。常量的回收与堆区对象的回收类似,当没有其他地方引用该字面量时,如果有必要,将被清理出常量池。

判定无用的类的3个条件:

a.该类的所有实例都已经被回收,也就是说堆中不存在该类的任何实例
b.加载该类的ClassLoader已经被回收
c.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
当然,这也仅仅是判定,不代表立即卸载该类。

9.Java中有内存泄漏吗?

内存泄露的定义: 当某些对象不再被应用程序所使用,但是由于仍然被引用而导致垃圾收集器不能释放。
内存泄漏的原因:对象的生命周期不同。比如说
对象A引用了对象B. A的生命周期比B的要长得多,当对象B在应用程序中不会再被使用以后, 对象 A 仍然持有着B的引用. (根据虚拟机规范)在这种情况下GC不能将B从内存中释放. 这种情况很可能会引起内存问题,倘若A还持有着其他对象的引用,那么这些被引用的(无用)对象也不会被回收,并占用着内存空间。甚至有可能B也持有一大堆其他对象的引用。这些对象由于被 B 所引用,也不会被垃圾收集器所回收,所有这些无用的对象将消耗大量宝贵的内存空间。并可能导致内存泄漏

怎样防止:

1、当心集合类,比如HashMap,ArrayList等,因为这是最容易发生内存泄露的地方.当集合对象被声明为static时,他们的生命周期一般和整个应用程序一样长。

10.OOM解决办法、

内存溢出的空间:Permanent Generation和Heap Space,也就是永久代和堆区

第一种情况永久代的溢出:出现这种问题的原因可能是应用程序加载了大量的jar或class,使虚拟机装载类的空间不够,与Permanent Generation Space的大小有关。
解决办法有2种:1、通过虚拟机参数-XX:PermSize和-XX:MaxPermSize调整永久代大小 2、清理程序中的重复的Jar文件,减少类的重复加载

第二种堆区的溢出:发生这种问题的原因是java虚拟机创建的对象太多,在进行垃圾回收之间,虚拟机分配的到堆内存空间已经用满了,与Heap Space的size有关。解决这类问题有两种思路:
1、检查程序,看是否存在死循环或不必要地重复创建大量对象,定位原因,修改程序和算法。
2、通过虚拟机参数-Xms和-Xmx设置初始堆和最大堆的大小

11.DirectMemory直接内存

直接内存并不是Java虚拟机规范定义的内存区域的一部分,但是这部分内存也被频繁使用,而且也可能导致OOM异常的出现。

JDK1.4引入了NIO,这是一种基于通道和缓冲区的非阻塞IO模式,它可以使用Native函数库分配直接堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,使得在某些场合显著提高性能,因为它避免了在Java堆和本地堆之间来回复制数据。

算法与数据结构

1.如何判断链表是否有环

方法1:快慢指针法 2.设两个工作指针p、q,p总是向前走,但q每次都从头开始走,对于每个节点,看p走的步数是否和q一样。比如p从A走到D,用了4步,而q则用了14步。因而步数不等,出现矛盾,存在环。
2.红黑树

二叉搜索树:(Binary Search Tree又名:二叉查找树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉搜索树。

红黑树是一棵二叉搜索树,它在每个结点上增加一个存储位来表示结点的颜色,可以是RED或BLACK。通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树没有一条路径会比其他路径长出2倍,所以红黑树是近似平衡的,使得红黑树的查找、插入、删除等操作的时间复杂度最坏为O(log n),但需要注意到在红黑树上执行插入或删除后将不在满足红黑树性质,恢复红黑树的属性需要少量(O(log
n))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。虽然插入和删除很复杂,但操
作时间仍可以保持为 O(log n) 次。具体如何保证?引出红黑树的5个性质。

红黑树的5个性质:满足以下五个性质的二叉搜索树
1. 每个结点或是红色的或是黑色的
2. 根结点是黑色的
3. 每个叶结点是黑色的
4. 如果一个结点是红色的,则它的两个子结点是黑色的
5. 对于每个结点,从该结点到其后代叶结点的简单路径上,均包含相同数目的黑色结点

插入操作:
由于性质的约束,插入的结点都是红色的。插入时性质1、3始终保持。破坏性质2当且仅当当前插入结点为根节点。变一下颜色即可。如果是破坏性质4或5,则需要旋转和变色来继续满足红黑树的性质。下面说一说插入的几种情况,约定当前插入结点为N,其父结点为P,叔叔为U,祖父为G

情形1:树空,直接插入违反性质1,将红色改黑。
情形2:N的父结点为黑,不必修改,直接插入
从情形3开始的情形假定N结点的父结点P为红色,所以存在G,并且G为黑色。且N存在一个叔叔结点U,尽管U可能为叶结点。
情形3:P为红,U为红(G结点一定存在且为黑)这里不论P是G的左孩子还是右孩子;不论N是P的左孩子还是右孩子。
首先把P、U改黑,G改红,并以G作为一个新插入的红结点重新进行各种情况的检查,若一路检索至根节点还未结束,则将根结点变黑。
情形4:P为红,U为黑或不存在(G结点一定存在且为黑),且P为G的左孩子,N为P的左孩子(或者P为G的右孩子,N为P的右孩子,保证同向的)。
P、G右旋并将P、G变相反色。因为P取代之前黑G的位置,所以P变黑可以理解,而G变红是为了不违反性质5。
情形5:P为红,U为黑或不存在,且P为G的左孩子,N为P的右孩子(或P为G的右孩子,N为P的左孩子,保证是反向的),对N,P进行一次左旋转换为情形4

删除操作比插入复杂一些,但最多不超过三次旋转可以让红黑树恢复平衡。

其他
- 黑高从某个结点x出发(不含x)到达一个叶结点的任意一条简单路径上的黑色结点个数称为该结点的黑高。红黑树的黑高为其根结点的黑高。
- 一个具有n个内部结点的红黑树的高度h<=2lg(n+1)
- 结点的属性(五元组):color key left right p
- 动态集合操作最坏时间复杂度为O(lgn)

3.数据库索引的实现

数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。

数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。

B树:

为了描述B-Tree,首先定义一条数据记录为一个二元组[key, data],key为记录的键值,对于不同数据记录,key是互不相同的;data为数据记录。那么B-Tree是满足下列条件的数据结构:

  1. d为大于1的一个正整数,称为B-Tree的度。用来表示每个结点包含的关键字个数的上界和下界。可以证明h<=logd((N+1)/2)
  2. h为一个正整数,称为B-Tree的高度。
  3. 每个非叶子节点由n-1个key和n个指针组成,其中d<=n<=2d。
  4. 每个叶子节点最少包含一个key和两个指针,最多包含2d-1个key和2d个指针,叶节点的指针均为null 。
  5. 所有叶节点具有相同的深度,等于树高h。
  6. key和指针互相间隔,节点两端是指针。
  7. 一个节点中的key从左到右非递减排列。
  8. 所有节点组成树结构。
  9. 每个指针要么为null,要么指向另外一个节点。
  10. 如果某个指针在节点node最左边且不为null,则其指向节点的所有key小于v(key1),其中v(key1)为node的第一个key的值。
  11. 如果某个指针在节点node最右边且不为null,则其指向节点的所有key大于v(keym),其中v(keym)为node的最后一个key的值。
  12. 如果某个指针在节点node的左右相邻key分别是keyi和keyi+1且不为null,则其指向节点的所有key小于v(keyi+1)且大于v(keyi)。

由于B-Tree的特性,在B-Tree中按key检索数据的算法非常直观:首先从根节点进行二分查找,如果找到则返回对应节点的data,否则对相应区间的指针指向的节点递归进行查找,直到找到节点或找到null指针,前者查找成功,后者查找失败。

一个度为d的B-Tree,设其索引N个key,则其树高h的上限为logd((N+1)/2),检索一个key,其查找节点个数的渐进复杂度为O(logdN)。从这点可以看出,B-Tree是一个非常有效率的索引数据结构。

B+树:

B-Tree有许多变种,其中最常见的是B+Tree,例如MySQL就普遍使用B+Tree实现其索引结构。
B+树是B树的变形,它把所有的data都放在叶子结点中,只将关键字和子女指针保存于内结点,内结点完全是索引的功能。

与B-Tree相比,B+Tree有以下不同点:

1、每个节点的指针上限为2d而不是2d+1。
2、内节点不存储data,只存储key;叶子节点存储data不存储指针。

一般在数据库系统或文件系统中使用的B+Tree结构都在经典B+Tree的基础上进行了优化,增加了顺序访问指针。

在B+Tree的每个叶子节点增加一个指向相邻叶子节点的指针

例如图4中如果要查询key为从18到49的所有数据记录,当找到18后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提到了区间查询效率。

为什么B树(B+树)?
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。

这涉及到磁盘存取原理、局部性原理和磁盘预读。

先从B-Tree分析,根据B-Tree的定义,可知检索一次最多需要访问h个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:

每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。

B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为O(h)=O(logdN)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)。

综上所述,用B-Tree作为索引结构效率是非常高的。

而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。

至于B+Tree为什么更适合外存索引,原因和内节点出度d有关。

由于B+Tree内节点去掉了data域,因此可以拥有更大的出度,拥有更好的性能。

4.一致性Hash

第一:简单介绍
一致性哈希算法是分布式系统中常用的算法。比如,一个分布式的存储系统,要将对象存储到具体的节点上,如果采用普通的hash方法,将数据映射到具体的节点上,如key%N,N是机器节点数。
1、考虑到比如一个服务器down掉,服务器结点N变为N-1,映射公式必须变为key%(N-1)
2、访问量加重,需要添加服务器结点,N变为N+1,映射公式变为hash(object)%(N+1)
当出现1,2的情况意味着我们的映射都将无效,对服务器来说将是一场灾难,尤其是对缓存服务器来说,因为缓存服务器映射的失效,洪水般的访问都将冲向后台服务器。

第二点:hash算法的单调性
Hash 算法的一个衡量指标是单调性,单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
consistent hash 也是一种hash 算法,简单的说,在移除 / 添加一个结点时,它能够尽可能小的改变已存在的映射关系,尽可能的满足单调性的要求。

第三点:将对象和服务器结点分别映射到环型空间
通常的一致性哈希做法是将 value 映射到一个 32 位的 key 值,也即是 0~2^32-1 次方的数值空间;我们可以将这个空间想象成一个首( 0 )尾( 2^32-1 )相接的圆环。
我们可以通过hash函数将我们的key映射到环型空间中,同时根据相同的哈希算法把服务器也映射到环型空间中,顺便提一下服务器或者某个计算节点的 hash 计算,一般的方法可以使用机器的 IP 地址或者机器名作为 hash 输入。

第四点:将对象映射到服务器
在这个环形空间中,如果沿着顺时针方向从对象的 key 值出发,直到遇见一个 服务器结点,那么就将该对象存储在这个服务器结点上,因为对象和服务器的hash 值是固定的,因此这个 cache 必然是唯一和确定的。
这时候考察某个服务器down机或者需要添加服务器结点,也就是移除和添加的操作,我们只需要几个对象的映射。

第五点:虚拟结点
Hash 算法的另一个指标是平衡性 (Balance)。平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。
对于上述的做法,可能导致某些对象都映射到某个服务器,使得分布不平衡。为此可以采用“虚拟结点”的做法。

“虚拟结点”( virtual node )是实际节点在 hash 空间的复制品,一实际结点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以 hash 值排列。引入“虚拟结点”会让我们的映射分布更为平衡一些。

引入“虚拟结点”前:
Hash(“192.168.1.1”);

引入“虚拟结点”后:
Hash(“192.168.1.1#1”);
Hash(“192.168.1.1#2”);