发布于 

Android面试题整理2(java)

第一节 Java 基础部分

1.1 抽象类与接口的区别?

大体区别如下:

  • 抽象类可以提供成员方法的实现细节,而接口中只能存在 public 抽象方法;
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的;
  • 接口中不能含有构造器、静态代码块以及静态方法,而抽象类可以有构造器、静态代码块和静态方法;
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口;
  • 抽象类访问速度比接口速度要快,因为接口需要时间去寻找在类中具体实现的方法;
  • 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。如果你往接口中添加方法,那么你必须改变实现该接口的类。
  • 接口更多的为了约束类的行为,可用于解耦,而抽象类更加侧重于代码复用。

1.2 分别讲讲 final,static,synchronized

关键字可以修饰什么,以及修饰后的作用?
static

  1. static 方法
    static 方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this 的,因为它不依附于任何对象,既然都没有对象,就谈不上 this 了。
public class StaticTest
{
    public static void a(){
        public static void main(String[]args){ StaticTest.a();}
    };
}
  1. static 变量
    static 变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

3.static 代码块
static 关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static 块可以置于类中的任何地方,类中可以有多个 static 块。在类初次被加载的时候, 会按照static块的顺序来执行每个 static块,并且只会执行一次。

public class StaticTest
{
private static int a ;
private static int b;
static {
    a = 1;
    b = 2;
}

final

  1. final变量
    凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为 final 的都叫作final 变量。
    final 变量经常和 static 关键字一起使用,作为常量。
private final int aa = 1;
static {
a = 1;
b = 2;
}
private void init(){
aa = 2;//报错编译器会提示 不能赋值。。
}
  1. final 方法
    final 也可以声明方法。方法前面加上 final 关键字,代表这个方法不可以被子类的方法重写。如果你认为一个方法的功能已经足够完整了,子类中不需要改变的话,你可以声明此方法为 final。

final 方法比非 final 方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。

public static void main(String[]args){ 
    StaticTest.a();
}
class StaticTest2 extends StaticTest{ 
public final void a(){ //这边就会编译器
    提示不能重写
}
  1. final 类
    其实更上面同个道理,使用 final 来修饰的类叫作 final类。final 类通常功能是完整的,它们不能被继承。Java 中有许多类是 final 的,譬如 String,Interger 以及其他包装类。

synchronized
synchronized 是 Java 中解决并发问题的一种最常用的方法,也是最简单的一种方法。synchronized 的作用主要有三个:

  • 确保线程互斥的访问同步代码
  • 保证共享变量的修改能够及时可见
  • 有效解决重排序问题。
  1. synchronized 方法
    有效避免了类成员变量的访问冲突:
private synchronized void
init(){ aa = 2;
}
  1. synchronized 代码块

这时锁就是对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的 instance 变量(它得是一个对象)来充当锁。

public final void a(){ 
synchronized (lock){
//代码
    }
}
@Override
public void run() {

    }
}

1.3 请简述一下String、StringBuffer和StringBuilder的区别?

  1. String 为字符串常量,StringBuffer与StringBuilder字符串变量,从而效率:
  2. String2.StringBuffer1.StringBuilder(一般情况下);
  3. StringBuffer是线程安全的,而StringBuilder为非线程安 全;
  4. String 是不可变的对象, 每次对 String 类型进行改变的等同于生成了一个新的 String 对象,经常改变内容的字符串不建议使用 String;
  5. 对StringBuffer 类改变,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用,经常改变内容的字符串建议使用 StringBuffer ;
  6. StringBuffer上的主要操作为 append和insert方法。

1.4 “equals”与“==”、“hashCode”的区别和使用场景?


== 用于比较变量所对应的内存中所存储的数值是否相同,要比较两个基本类型的数据(注意是基本类型)或两个 引用变量是否相等,只能用
操作符。假如一个变量指向的数据是对象类型的,例如Objetobj 1= new Object()那么,这时候涉及了两块内存; 变量obj1 是一个内存, new Object() 是另一个内存(堆内存),此时,变量 obj1 所对应的内存中存储的数值就是对象占用的那块内存(堆内存)的首地址。对于指向对象类型的变量,如果要比较两个变量是否指向同一个对象,即要看这两个变量所对应的内存中的数值是否相等,这时候就需要用==操作符进行比较;

equals:
equals 方法是用于比较两个独立对象的内容是否相同:

String a=new String("a");
String b=new String("a");

两条new语句创建了两个对象,然后用a/b这两个变量分别指向了其中一个对象,这是两个不同的对象,它们的首地址是不同的,即a和b中存储的数值(对应对象的首地址)是不相同的,所以,表达式a==b将返回false,而 这两个对象中的内容是相同的,所以,表达式a.equals(b)将返回true。

我们看看equal 源码:

public boolean equals(Object obj)
{
    return (this == obj);
}

如果一个类没有重写 equals 方法 那么就是调用 object的 equals 其实就是 == 。

hashCode:
因为重写的equals()里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode()进行对比, 则只要生成一个 hash 值进行比较就可以了,效率很高,那么既然效率这么高为什么还要
hashCode()呢?

使用场景:
因为equals()并不是完全可靠,有时候不同的对象也会一样(hash冲突),所以hashCode()
他们生成的hashCode()只能说是大部分时候可靠,并不是绝对可靠,所以可以得出:
equals()相等的两个对象他们的hashCode()肯定相等,也就是用equals()对比是绝对可靠的。

hashCode()相等的两个对象他们的equals()不一定相等,也就是hashCode()不是绝对可靠的。
hashCode()

所有对于需要大量并且快速的对比的话如果都用equals()去做显然效率太低,所以解决方式是,每当
需要对比的时候,首先用 hashCode() 去对比,如果hashCode() 不一样,则表示这两个对象肯定不相等(也就是不必再用 equals() 去再对比了),如果hashCode() 相同,此时再对比他们的 equals(),如果equals() 也相同,则表示这两个对象是真的相同了

1.5 Java 中深拷贝与浅拷贝的区别?

首先需要明白,浅拷贝和深拷贝都是针对一个已有对象的操作。
那先来看看浅拷贝和深拷贝的概念。在 Java 中,除了基本数据类型(元类型)之外,还存在 类的实例对象 这个引用数据类型。而一般使用 『 = 』号做赋值操作的时候。

对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际上还是指向的同一个对象。而浅拷贝和深拷贝就是在这个基础之上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。

所以到现在,就应该了解了,所谓的浅拷贝和深拷贝,只是在拷贝对象的时候,对类的实例对象 这种引用数据类型的不同操作而已。

总结来说:
1、浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
2、深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

1.6 谈谈Error和Exception的区别?

Exception是java程序运行中可预料的异常情况,咱们可以获取到这种异常,并且对这种异常进行业务外的处理。
Error是java程序运行中不可预料的异常情况,这种异常发 生以后,会直接导致JVM不可处理或者不可恢复的情况。所 以这种异常不可能抓取到,比如OutOfMemoryError、NoClassDefFoundError等。其中的Exception又分为检查性异常和非检查性异常。两个根本的区别在于,检查性异常 必须在编写代码时,使用trycatch捕获(比如:IOException异常)。非检查性异常 在代码编写时,可以忽略捕获操 作比 如 :(ArrayIndexOutOfBoundsException),这种异常是在代码编写或者使用过程中通过规
范可以避免发生的。
切记,Error是Throw不是Exception。

1.7 什么是反射机制?反射机制的应用场景有哪些?

Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。

应用场景:

  1. 逆向代码,例如反编译
  2. 与注解相结合的框架,如 Retrofit
  3. 单纯的反射机制应用框架,例如 EventBus(事件总线)
  4. 动态生成类框架 例如Gson

1.8 谈谈如何重写equals()方法?为什么还要重写hashCode()?

先来说一下hashcode()和equals方法吧。
hashcode()

  1. hashCode 的存在主要用于查找的快捷性,如Hashtable,HashMap 等,hashCode 是用来在三列存储结构中确定对象的存储地址的。
  2. 如果两个对象相同,就是适用于euqals(java.lang.Object) 方法,那么这两个对象的
    hashCode一定相同。
  3. 如果对象的euqals 方法被重写,那么对象的 hashCode也尽量重写,并且产生 hashCode使用的对象,一定要和equals 方法中使用的一致,否则就会违反上面提到的第二点。
  4. 两个对象的 hashCode相同,并不一定表示这两个对象就相同,也就是不一定适用于equals() 方法,只能够说明这两个对象在三列存储结构中,如 Hashtable.,他们存在同一个篮子里。

equals(Object obj)

  1. 如果一个类没有重写 equals(Object obj)方法,则等价于通过 == 比较两个对象,即比较的是对象在内存中的空间地址是否相等。
  2. 如果重写了equals(Object ibj)方法,则根据重写的方法内容去比较相等,返回 true 则相等,false 则不相等。
public class MyClass {
public static void main(String[] args) {
    HashSet books=new HashSet();
    books.add(new A());
    books.add(new A());
    books.add(new B());
    books.add(new B());
    books.add(new C());
    books.add(new C());
    System.out.println(books);
    }
}
class A{
//类A的 equals 方法总是返回true,但没有重写其hashCode() 方法
@Override
public boolean equals(Object o)
 
{
   return true;
    }
}

class B{
//类B 的hashCode() 方法总是返回1,但没有重写其
equals()方法
@Override
public int hashCode() {
    return 1;
    }
}
class C{ public hashCode(){
    int 2;
    return;
}
@Override
public boolean equals(Object o) {
    return true;
    }
}

结果

  1. 即使两个A 对象通过 equals() 比较返回true,但HashSet 依然把他们当成 两个对象,即使两个 B 对象的hashCode() 返回值相同,但HashSet 依然把他们当成两个对象。
  2. 即也就是,当把一个对象放入HashSet 中时,如果需要重写该对象对应类的 equals() 方法,则也应该重写其hashCode() 方法。规则是:如果两个对象通过equals() 方法比较返回true,这两个对象的hashCode值也应该相同。
  3. 如果两个对象通过euqals() 方法比较返回true,但这两个对象的 hashCode() 方法返回不同的hashCode值时,这将导致HashSet 会把这两个对象保存在 Hash表的不同位置,从而使两个对象都可以添加成功,这就与Set 集合的规则冲突了。
  4. 如果两个对象的 hashCode() 方法返回的 hasCode值相同,但他们通过 equals() 方法比较返回false 时将更麻烦:因为两个对象的hashCode值相同,

HashSet 将试图 把它们保存在同一个位置,但又不行(否则将只剩下一个对象),所以实际上会在这个位置用链式结构来保存多个对象;而HashSet 访问集合元素时也是根据元素的 hashCode 值来快速定位的,如果hashSet 中两个以上的元素具有相同的 HashCode值时,将会导致性能下降。

1.9 Java 中 IO 流分为几种?BIO,NIO,AIO有什么区别?

IO 流分为几种
Java中的流分为两种,一种是字节流,另一种是字符流, 分别由四个抽象类来表示(每种流包括输入和输出两种所以一共四个):
InputStream,OutputStream,Reader,Writer。
Java中其他多种多样变化的流均是由它们派生出来的.
字符流和字节流是根据处理数据的不同来区分的。字节流按照8位传输,字节流是最基本的,所有文件的储存是都是字节(byte)的储存,在磁盘上保留的并不是文件的字符而是先把字符编码成字节,再储存这些字节到磁盘。

  1. 字节流可用于任何类型的对象,包括二进制对象,而
    字符流只能处理字符或者字符串;
  2. 节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以。
    读文本的时候用字符流,例如txt文件。读非文本文件的时候用字节流,例如mp3。理论上任何文件都能够用字节流读取,但当读取的是文本数据时,为了能还原成文本你必须再经过一个转换的工序,相对来说字符流就省了这个麻烦,可以有方法直接读取。字符流处理的单元为2个字节的Unicode字符,分别操作字 符、字符数组或字符串,而字节流处理单元为1个字节, 操作字节和字节数组。所以字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的,所以 它对多国语言支持性比较好!
    BIO、NIO、AIO 有什么区别
    BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统IO,它的特点是模式简单使用方便,并发处理能力低。NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。

AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。

BIO是一个连接一个线程。
NIO是一个请求一个线程。
AIO是一个有效请求一个线程。

BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进 行 处 理 。
AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

适用场景分析
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

1.10 谈谈你对Java泛型中类型擦除的理解,并说说其局限性?

Java中的泛型基本上都是在编译器这个层次来实现的。在 生成的Java字节码中是不包含泛型中的类型信息的。使用 泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

如在代码中定义的List和List等类型,在编译后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM 来说是不可见的。
Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转 换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。

1.11 String为什么要设计成不可变的?

1,字符串常量池的需要
当创建一个 String 对象时,如果此字符串已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象如果允许改变,那么将导致各种逻辑错误,比如改变一个对象将会影响另一个独立对象,严格来说,这种常量池的思想是一种优化手段

2,允许String对象缓存 HashCode

java 中 String 对象的哈希码会被频繁的使用,比如在hashMap中。字符串的不变形保证了hash码的唯一性,因此可以放放心的进行缓存。这也是一种优化手段,意味着 不必没说都计算新的哈希码。在String 类中有 private int hash 来缓存hashcode

3,安全性
String 被许多的类来当做参数,如 网络url,文件路径path 等等,如果String 不是固定的,将会引起各种安全隐患

1.12 说说你对Java注解的理解?

三种注解
source
class
runtime

分别在源码,编译时,运行时存活自定义的source注解 可以用来对 一些方法 或者参数进行约束,比如说指定线程 指定参数类型class 注解 是配合apt 编写注解处理器, 对注解的类或者变量进行解析,生成.class文件 辅助工作 比方说 arouter,在注解处理器中生成代码 帮你做 路径和 activity的路由表,butterknife的注解帮你做findviewbyid的工作runtime注解 则是在runtime时还能存在的 通常配合 反射机制,把注解标注的对象 拿到进行操作,比方说 retrofit,通过反射机制 拿到注解的 接口和接口中的方法,在通过动态代理生成接口的实现类

1.13谈一谈Java成员变量,局部变量和静态变量的创建和回收时机?

成员变量:生命周期伴随类对象,类对象回收时回收 存在堆里
静态变量:不回收 在方法区 随着类的加载而加载,随着类的消失而消失,由于类需要非常长时间的不使用,不利用,不关联,才有可能会被回收机制回收,所以静态成员变量的生命周期特别长,除非是共享数据,否则不建议使用静态;
局部变量:方法调用时创建 方法结束时被标记为可回收存在栈里

1.14 请说说Java中String.length()的运作原理?

privatefinalcharvalue[];
public String(char value[]) {
    this.value=Arrays.copyOf(value,value.length);
}
public int length()
{
}
return value.length;

第二节 Java 集合

2.1 谈谈List,Set,Map的区别?

List中存储的数据是有顺序的,并且值允许重复;Map中存 储的数据是无序的,它的键是不允许重复的,但是值是允 许重复的;Set中存储的数据是无顺序的,并且不允许重 复,但元素在集合中的位置是由元素的hashcode决定,即 位置是固定的(Set集合是根据hashcode来进行数据存储 的,所以位置
是固定的,但是这个位置不是用户可以控制 的,所以对于用户来说set中的元素还是无序的)

2.2 谈ArrayList和LinkedList的区别?

  1. ArrayList是基于数组的数据结构,LinkedList是基于链 表的数据结构。
  2. ArrayList适用于查询操作,LinkedList适用于插入和删 除操作。

2.3 请说一下HashMap与HashTable的区别?

HashMap和Hashtable的比较是Java面试中的常见问题, 用来考验程序员是否能够正确使用集合类以及是否可以随机应变使用多种思路解决问题。

HashMap的工作原理、
ArrayList与Vector的比较以及这个问题是有关Java 集合框架的最经典的问题。Hashtable是个过时的集合类,存在于Java API中很久了。在Java 4中被重写了,实现了Map接口,所以自此以后也成了Java集合框架中的一部分。

Hashtable和HashMap在Java面试中相当容易被问到,甚至成为了集合框架面试题中最常被考的问题,所以在参加任何Java面试之前,都不要忘了准备这一题。

  1. 父类不同
    第一个不同主要是历史原因。Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现。
public class HashMap<K, V> extendsAbstractMap<K, V>
implements Cloneable, Serializable {...}
public class Hashtable<K, V> extends Dictionary<K, V>
implements Map<K, V>, Cloneable, Serializable {...}

而HashMap继承的抽象类AbstractMap实现了Map接口:

public abstract class AbstractMap<K, V> implements
Map<K, V> {...}
  1. 线程安全不一样
    Hashtable 中的方法是同步的,而HashMap中的方法在默认情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就要自己增加同步处理了。

  2. 允不允许null值Hashtable中,key和value都不允许出现null值,否则 会抛出NullPointerException异常。而在HashMap中,null可以作为键,这样的键只有一 个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示 HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap 中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

  3. 遍历方式的内部实现上不同

Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。

  1. 哈希值的使用不同
    HashTable直接使用对象的hashCode。而HashMap重新计算hash值。

  2. 内部实现方式的数组的初始大小和扩容的方式不一样HashTable中的hash数组初始大小是11,增加的方式是old*2+1。HashMap中hash数组的默认大小是16, 而且一定是2的指数。

2.4 谈一谈ArrayList的扩容机制?

学习目标:
初始化时元素个数是多少
如何扩容
先来看看构造方法
无参数构造方法

/**
*Constructs an empty list with an
*initial capacity of ten.
/
public ArrayList()
{
    this.elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

这是无参数的构造方法,是对elementData(元素的数组)进行赋值DEFAULTCAPACITY_EMPTY_ELEMENTDATA(空数组,我们暂时叫做默认空数组),明明是空数组,但是注释确实创建容量是10。好吧继续往下看。

再来看带参数的构造方法

public ArrayList(int initialCapacity) { if
(initialCapacity > 0) {
    //如果initialCapacity>0,创建
    initialCapacity个元素的数组
    this.elementData = new
    Object[initialCapacity];
    else if (initialCapacity == 0) {
    //等于0时,创建空数组
    this.elementData =EMPTY_ELEMENTDATA;
}else {
    //否则直接抛出异常
    throw new IllegalArgumentException("Illegal Capacity: "+
    initialCapacity);
        }
    }
}

再看另一个构造方法

public ArrayList(Collection<? extends E>
c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0)
    {
    // c.toArray might (incorrectly)
     not return Object[] (see 6260652)
    if (elementData.getClass() !=  Object[].class)
    elementData = Arrays.copyOf(elementData, size, Object[].class);
    else {
     // replace with empty array.
    this.elementData = EMPTY_ELEMENTDATA;
    }
    }
}

就是直接将集合转为数组赋值给elementData,同时对
size赋值,并且如果size不等于0时,c.toArray might(incorrectly) not return Object[] (see 6260652)。我也是一脸懵逼。等于0时,直接赋值空的数组。我们接着看add方法,先看一个参数的

public boolean add(E e)
{
    ensureCapacityInternal(size + 1);//
    Increments modCount!!
    elementData[size++] = e;
    return true;
}

就是ensureCapacityInternal(size +1) 并且将e添加到size++的位置,我们来看ensureCapacityInternal(size + 1)方法。

private void ensureCapacityInternal(int minCapacity) {
if (elementData ==
    DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    minCapacity =
    Math.max(DEFAULT_CAPACITY, minCapacity);
}
    ensureExplicitCapacity(minCapacity);
}

看这里

if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

还记得空参数的构造方法吗?DEFAULT_CAPACITY = 10。也就是当空参数时创建ArrayList时,minCapacity与DEFAULT_CAPACITY的最大值,显然创建无参数构造方法时minCapacity = 0.这时结果 = 10,然后看ensureExplicitCapacity(minCapacity)

private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
    grow(minCapacity);
}

显然调用无参数构造方法时minCapacity - elementData.length >0 是 成 立的 , 我 们 再 看grow(minCapacity)

private void grow(int minCapacity) {
/ overflow-conscious code
/
int oldCapacity = elementData.length; int
newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so
this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

oldCapacity是元素个数newCapacity是 oldCapacity+oldCapacity/2,即oldCapacity的1.5倍。当 1.5倍的元素个数小于minCapacity时newCapacity = minCapacity。显然创建无参数ArrayList就是这种情况,这就解决了开头的 第一个问题。调用无参数构造方法创建的 是 10个元素 的长度。

我们再看当1.5倍元素个数大于 MAX_ARRAY_SIZE时newCapacity = hugeCapacity(minCapacity);

private static int hugeCapacity(int
minCapacity) {
    if (minCapacity < 0) // overflow
    throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE)?Integer.MAX_VALUE :MAX_ARRAY_SIZE;
}

到这里我们可以总结一下了:
无参构造方法调用add之后创建的是10个长度的数组。有参数则直接创建指定长度的数组。扩容时,小于MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8一次扩容1.5 倍,超过则直接Integer.MAX_VALUE。

2.5 HashMap 的实现原理?

HashMap 实际上是一个“链表散列”的数据结构,即数组和链表的结合体。它是基于哈希表的 Map 接口的非同步实现。

数组:存储区间连续,占用内存严重,寻址容易,插入删除困难;
链表:存储区间离散,占用内存比较宽松,寻址困难, 插入删除容易;
Hashmap 综合应用了这两种数据结构,实现了寻址容易,插入删除也容易。

例如我们以下图为例,看一下 HashMap 的内部存储结构:

关于 HashMap 的存取过程,可参照下图:

①. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

2.6 请简述 LinkedHashMap 的工作原理和使用方式?

  • 查看LinkedHashMap源码发现是继承HashMap实现Map接口。也就是HashMap的方法LinkedMap都有。 LinkHashMap与HashMap的主要区别是:LinkedHashMap是有序的,hashmap是无序的。LinkedHashMap通过维护一个双向链表实现有序,也正是因为要维护这个链表,内存上有更大的开销。

  • 补充下有序和无序

    我们说的无序是插入顺序和输出顺序不一致。

  • 补充一下链表结构和顺序结构:线性结构分为顺序结构,和链表结构。

顺序结构:在内存中是一块完整有序内存。所以我们在查询的时候时候直接索引index,便可找到要查询的数据,速度非常快,缺点是插入删除慢。有点类似班级排队时(一列纵队),每个人都知道自己在第几个位置。老师只要说第三个位置,那这个同学立马知道老师要找的是自己。这时候要插入一个同学到第二个位置,所以之前第二个位置开始往后的每个同学的位置都要+1。所以比较慢。

链表结构:通过结点头记录该结点的上一个结点和下一个下一个结点(就是传统的双链表,单链表就是只记录下一个结点,循环链表就是最后一个结点的下一个结点指向第一个结点)。正是因为这种关系,所以
链表结构不需要一块完整的内存,而且插入删除相对快,但是查询相对慢。但是因为要维护结点头,所以内存开销相对大一点。有点类似于班级排队时,每个人虽然不知道自己的位置,但是知道自己前面是谁和后面是谁。当要插入一个同学b时到c前面时,只要c 同学记住自己之前是a,现在换成b.b记住自己前面是a,后面是c。所以想对来说插入很快。删除类似。但是当老师按位置查询时,就要先从第一个开始计数,知道找到老师要找的数字。所以查询慢。

2.7 谈谈对于ConcurrentHashMap的理解?

并发集合常见的有 ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque 等。并发集合位于java.util.concurrent包 下 ,是 jdk1.5之后才有的。

在 java 中有普通集合、同步(线程安全)的集合、并发集合。普通集合通常性能最高,但是不保证多线程的安全性和并发的可靠性。线程安全集合仅仅是给集合添加了synchronized 同步锁,严重牺牲了性能,而且对并发的效率就更低了,并发集合则通过复杂的策略不仅保证了多线程的安全又提高的并发时的效率ConcurrentHashMap 是线程安全的 HashMap 的实现,默认构造同样有initialCapacity和loadFactor属性,不过还多了一个concurrencyLevel 属性,三属性默认值分别为 16、0.75 及 16其内部使用锁分段技术,维持这锁Segment 的数组,在Segment数组中又存放着 Entity[]数组,内部 hash算法将数据较均匀分布在不同锁中。

put操作:并没有在此方法上加上 synchronized,首先对key.hashcode 进行 hash 操作,得到 key 的 hash 值。

hash操作的算法和map也不同,根据此hash值计算并获取其对应的数组中的Segment对象(继承自ReentrantLock), 接着调用此 Segment 对象的 put 方法来完成当前操作。

ConcurrentHashMap 基于 concurrencyLevel 划分出了多个Segment 来对 key-value 进行存储,从而避免每次 put 操作都得锁住整个数组。在默认的情况下,最佳情况下可 允许16 个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。

get(key) 首先对 key.hashCode 进行 hash 操作,基于其值找到对应的 Segment 对象,调用其 get 方法完成当前操作。而Segment 的 get 操作首先通过 hash值和对象数组大小减 1的值进行按位与操作来获取数组上对应位置的HashEntry。

在这个步骤中,可能会因为对象数组大小的改变,以及数组上对应位置的 HashEntry 产生不一致性,那么ConcurrentHashMap 是如何保证的?

对象数组大小的改变只有在 put 操作时有可能发生,由于HashEntry 对象数组对应的变量是 volatile 类型的,因此可以保证如 HashEntry 对象数组大小发生改变,读操作可看到最新的对象数组大小。
在获取到了 HashEntry 对象后,怎么能保证它及其 next属性构成的链表上的对象不会改变呢?

这点ConcurrentHashMap采用了一个简单的方式,即HashEntry 对象中的 hash、key、next 属性都是 final的,这也就意味着没办法插入一个HashEntry对象到基于next属性构成的链表中间或末尾。这样就可以保证当获取到HashEntry对象后,其基于 next 属性构建的链表是不会发生
变化的。
ConcurrentHashMap 默认情况下采用将数据分为 16 个段进行存储,并且 16 个段分别持有各自不同的锁Segment,锁仅用于 put 和 remove 等改变集合对象的操作,基于volatile 及 HashEntry 链表的不变性实现了读取的不加锁。这些方式使得 ConcurrentHashMap 能够保持极好的并发支持,尤其是对于读远比插入和删除频繁的 Map而言,而它采用的这些方法也可谓是对于 Java 内存模型、并发
机制深刻掌握的体现。
总结:ConcurrentHashMap 比起Hashmap,是线程安全的,比起HashTable是高效的。

Java 多线程

3.1 Java 中使用多线程的方式有哪些?

大概这么4种

extends Thread;
impl Runnable;
impl Callable 通过 FutureTask 包装器来创建线程;
使用ExecutorService、Callable、Future实现有返回结果的多线程。
extends Thread 和 impl Runnable 的线程没有返回值, 而FutureTask 和ExecutorService(ExecutorService.submit(xxx) return Future<?> )有返回值.

3.2 说一下线程的几种状态?

第一是创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。

第二是就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。

第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。

第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。

第五是死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪。

3.3 如何实现多线程中的同步?

多线程同步和异步不是一回事。
几种情况,

  1. 就是大家说的synchronized 他可以保证原子性,保证多个线程在操作同一方法时只有一个线程可以持有锁,并且操作该方法,
  2. 就是手动调用读写锁,
  3. 手动操作线程的wait和notify
  4. volatile我记得是没有原子性的,他可以保证内存可见性,

3.4 谈谈线程死锁,如何有效的避免线程死锁?

一、死锁的定义
多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题——死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

二、死锁产生的原因

  1. 系统资源的竞争
    通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争 才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。

  2. 进程推进顺序非法
    进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时, 两者都 会因为所需资源被占用而阻塞。

  3. 信号量使用不当也会造成死锁。

进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B 不是因为竞争同一资源,而是在等待对方的资源导致死锁。

  1. 死锁产生的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

  • 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。 此时若有其他进程请求该资源,则请求进程只能等待。

  • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。

  • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

  • 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。

/**
一个简单的死锁类
当DeadLock类的对象flag==1时(td1),先锁定o1,睡
眠500毫秒
*
而td1在睡眠的时候另一个flag==0的对象(td2)线程启
动,先锁定o2,睡眠500毫秒
*
td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被
td2锁定;
*
td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被
td1锁定;
*td1、td2相互等待,都需要得到对方锁定的资源才能继续执
行,从而死锁。
public class DeadLock implements Runnable { public int flag 1);
//静态对象是类的所有对象共享的
private static Object o1 = new Object(), o2 = new Object();

@Override
public void run() { System.out.println("flag=" +flag);
 if (flag == 1) {
    synchronized (o1) { try {
    Thread.sleep(500);
}
catch (Exception e)
{
    e.printStackTrace();
}
synchronized (o2)
System.out.println("1");
    }
}
if (flag == 0)
{
    synchronized (o2) {
try {
        Thread.sleep(500);
    }
}
catch (Exception e)
e.printStackTrace();
{
    synchronized (o1)
    {
            System.out.println("0");
        }
      }
    }
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;

//td1,td2都处于可执行状态,但JVM线程调度先执行哪
个线程是不确定的。
//td2的run()可能在td1的run()之前运行new
Thread(td1).start();
    new Thread(td2).start();
    }
}

三、如何避免死锁

在有些情况下死锁是可以避免的。三种用于避免死锁的技术:

  1. 加锁顺序(线程按照一定的顺序加锁)
  2. 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  3. 死锁检测
    加锁顺序
    当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。
    如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个例子
Thread 1:
lock A
lock B
Thread 2:
wait for A
lock C (when A locked)
Thread 3:
wait for A
wait for B
wait for C

如果一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。

例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C(译者注:获取锁A是获取锁C的必要条件)。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然 后在它们尝试对B或C加锁之前,必须成功地对A加了锁。
按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。
加锁时限
另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。

以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:

Thread 1 locks A
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked
Thread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis)
before retrying.
Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis)
before retrying.

在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁(除非线程2或者其它线程在线程1成功获得两个锁之前又获得其中的一些锁)。

需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任 务。此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。

(译者注:超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试, 导致新一轮的竞争,带来了新的问题。)

这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。后续的Java并发系列会涵盖自定义锁的内容。

死锁检测
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程 请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图 看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7; 线程B拥有锁7,请求锁1)。

当然,死锁一般要比两个线程互相持有对方的锁这种情况 要复杂的多。线程A等待线程B,线程B等待线程C,线程C 等待线程D,线程D又在等待线程A。线程A为了检测死锁, 它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就可以被用来检测死锁。

那么当检测出死锁时,这些线程该做些什么呢?
一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。

一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

3.5 谈谈线程阻塞的原因?

1、线程执行了Thread.sleep(int n) 方法,线程放弃CPU, 睡眠n
毫秒,然后恢复运行。

2、线程要执行一段同步代码,由于无法获得相关的同步锁,只好进入阻塞状态,等到获得了同步锁,才能恢复运行。

3、线程执行了一个对象的wait()方法,进入阻塞状态,只有等到其他线程执行了该对象的notify()或notifyAll()方法, 才可能将其唤醒。

4、线程执行I/O操作或进行远程通信时,会因为等待相关的资源而进入阻塞状态。例如,当线程执行
System.in.read()方法时,如果用户没有向控制台输入数 据,则该线程会一直等读到了用户的输入数据才从read()方 法返回。进行远程通信时,在客户程序中,线程在以下情 况可能进入阻塞状态。

5、请求与服务器建立连接时,即当线程执行Socket 的带参数的构造方法,或执行Socket的connect()方法时,会进入阻塞状态,直到连接成功,此线程才从Socket的构造方法或connect()方法返回。

6、线程从Socket的输入流读取数据时,如果没有足够的数据,就会进入阻塞状态,直到读到了足够的数据,或者到 达输入流的末尾,或者出现了异常,才从输入流的read()方 法返回或异常中断。输入流中有多少数据才算足够呢?这要看线程执行的read()方法的类型。

int read(); 只要输入流中有一个字节,就算足够。
int read(byte[] buff); 只要输入流中的字节数目与参数buff数组的长度相同,就算足够。String readLine(); 只要输入流中邮一行字符串,就算足够。值得注意的是,InputStream类并没有readLine方法,在过滤流BufferedReader类中才有此方法。

7、线程向Socket的输出流写一批数据时,可能会进入阻塞状态,等到输出了所有的数据,或者出现异常,才从输出 流的write()方法返回或异常中断。

8、调用Socket的setSoLinger()方法设置了关闭Socket的延迟时间,那么当线程执行Socket的close方法时,会进入阻塞状态,直到底层Socket发送完所有剩余数据,或者超过了setSoLinger()方法设置的延迟时间,才从close()方法返回。

3.6 请谈谈 Thread 中 run() 与 start()的区别?

run() 和普通的成员方法一样,可以被重复调用。但是如果单独调用 run 方法,则不是在子线程中执行。
start() 这个方法只能被调用一次。调用这个方法后 程序会启动一个 新的线程来 执行 run 方法。注意 :调用start 后,线程处于可运行状态(并没有运行),一旦得到 cup 时间片,就开始执行run 方法,run 方法结束后,线程则立即终止。

3.7 synchronized和volatile关键字的区别?

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)
    中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变
    量,其他线程被阻塞住。

  2. volatile仅能使用在变量级别;synchronized则可以使用 在变
    量、方法、和类级别的volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性

  3. volatile不会造成线程的阻塞;synchronized可能会造成 线程的阻塞。

  4. volatile标记的变量不会被编译器优化;synchronized标 记的变量可以被编译器优化

3.8 如何保证线程安全?

当多个线程要共享一个实例对象的值得时候,那么在考虑安全的多线程并发编程时就要保证下面3个要素:
原子性(Synchronized, Lock)
有序性(Volatile,Synchronized, Lock)
可见性(Volatile,Synchronized,Lock)
当然由于synchronized和Lock保证每个时刻只有一个线程执行同步代码,所以是线程安全的,也可以实现这一功能,但是由于线程是同步执行的,所以会影响效率。

下面是对3个要素的详细解释:
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。在Java中,基本数据类型的变量的读取和赋值操作是原子 性操作,即这些操作是不可被中断的,要么执行,要么不执行。
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
当一个共享变量被volatile修饰时,它会保证修改的值会立 即被更新到主存,当有其他线程需要读取共享变量时,它会去内存中读取新值。

普通的共享变量不能保证可见性,因为普通共享变量被修改后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

更新主存的步骤:当前线程将其他线程的工作内存中的缓存变量的缓存行设置为无效,然后当前线程将变量的值跟新到主存,更新成功后将其他线程的缓存行更新为新的主存地址其他线程读取变量时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

有序性:即程序执行的顺序按照代码的先后顺序执行。 在Java内存模型中,允许编译器和处理器对指令进行重排 序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。可以通过volatile关键字来保证一定的“有序性”。当在处理并发编程的时候,只要程序满足了原子性,可见性和有序性,那么程序就不会发生脏数据的问题。

3.9 谈谈ThreadLocal用法和原理?

ThreadLocal用于保存某个线程共享变量:对于同一个staticThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。
1、ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。
2、ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。
3、ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。
4、ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法, 返回此方法值。

3.10 Java 线程中notify 和 notifyAll有什么区别?

notify 方 法

  1. 方法调用之前由于也需要获取该对象的锁,所以使用的位置: synchronized方法中或者synchronized代码块中
  2. 通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个wait) 状 态 的 线 程 来 发 出 通 知
  3. wait()方法执行完毕之后,会立刻释放掉锁,如果没有再次使用notify,其他wait()的线程由于没有得到通知,会继续阻塞在wait()的状态,等待其他对象调用notify或者notifyAll来唤醒。

notifyAll方法

  1. 使用位置和notify一样
  2. notifyAll唤醒所有处于wait的线程

3.11 什么是线程池?如何创建一个线程池?

线程池,顾名思义一堆线程在一起,核心成员是线程,线程是干什么的?当然是执行任务的!而线程池呢?当然也是!

而用线程池有什么好处呢?举个例子,你是一家公司的老板(进程),这家公司接到了N个任务,
假如不使用线程池:你每个任务都新建一个线程去处理,就相当于每个工作都招一个人来做(创建线程),做完直接开除掉(线程销毁),这显然有问题;

使用线程池:
你手底下有或没有一些空闲的员工(执行完任务待命的线程),当任务来临时,先看看自己手底下有没有没事做的员工(空闲线程),有的话直接把任务交出去;没有的话你可以让任务等会执行(任务排队),等待有线程空闲,或者你可以多招点人来处理(创建新线程),或者拒绝执行这个任务;当有人空闲时你可以让他等待新的任务来临,等待太长的话你可以把他开掉

(线程销毁)
可见,使用线程池可以实现线程复用,以节省创建线程的所需的资源;

还有一种fork-join式线程池(任务抢占式线程池),适用于可分割的任务,大致实现和普通线程池差不多,但是其任务可以“抢占”:
比如之前很火的数一亿颗米作业,分成N个线程数,可能因为各种原因造成某个线程早早的就数完了属于这个线程任务的那部分,而其他线程可能还有一大堆的米没有数,使用普通线程池的话,这个线程就只是默默的看着,不会去继续帮忙;

fork-join式线程池,这个线程的任务执行完毕后还会帮忙一起数;

3.12 谈一谈java线程常见的几种锁?

4种Java线程锁(线程同步)

  1. synchronized
    在Java中synchronized关键字被常用于维护数据一致性。synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是顺序的 。 synchronized实现的机理依赖于软件层面上的JVM,因此其性能会随着Java版本的不断升级而提高。
    2.ReentrantLock(ReentrantLock引入两个概念:公平锁与非公平锁。)可重入锁,顾名思义,这个锁可以被线程多次重复进入进行获取操作。ReentantLock继承接口Lock并实现了接口中定义的方法, 除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程 死 锁 的方 法 。Semaphore基本能完成ReentrantLock的所有工作,使用 方法也与之类似,通过acquire()与release()方法来获得和释放临界资源。此处AtomicInteger是一系列相同类的代表之一,常见的还有AtomicLong、AtomicLong等,他们的实现原理相同, 区别在与运算对象类型的不同。

3.13 谈一谈线程sleep()和wait()的区别?

sleep是Thread类的方法,wait是Object类的方法sleep不释放锁,wait释放锁
sleep不需要Synchronized ,wait需要Synchronized sleep不需要唤醒,wait需要唤醒(除wait(int time))

3.14 什么是悲观锁和乐观锁?

锁是为了避免自己在修改资源的时候,别人同时在修改,导致同一时间产生两份修改,不知道如何处理的情况而设置的独占资源,避免多个操作同时处理同一资源的技术。
乐观锁:默认为,某个线程在自己处理共享资源的时候, 不会出现同一时刻来修改此资源的前提,只在处理完毕, 最后写入内存的时候,检测是否此资源在之前未被修改。类似于读写锁的读锁就是乐观锁。
悲观锁:默认为,某个线程在自己处理共享资源的时候, 一定会出现同一时刻来修改此资源,所以刚拿到这个资源就直接加锁,不让其他线程来操作,加锁在逻辑处理之 前。类似,synchronized关键字,条件锁,数据库的行 锁,表锁等就是悲观锁。

3.15 什么是BlockingQueue?请分析一下其内部原理并谈谈它的使用场景?

前言:
在新增的Concurrent包中,BlockingQueue很好的解决了 多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程 程序带来极大的便利。本文详细介绍了BlockingQueue家 庭中的所有成员,包括他们各自的功能以及常见使用场景。

认识BlockingQueue
阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:从上图我们可以很清楚看到,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;常用的队列主要有以下两种:(当然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue就是其中的一种)

先进先出(FI FO):先插入的队列的元素也最先出队 列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。

后进先出(LIFO):后插入队列的元素最先出队列, 这种队列优先处理最近发生的事件。

多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)

下面两幅图演示了BlockingQueue的两个常见阻塞场景: 如上图所示:当队列中没有数据的情况
下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。

如上图所示:当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
这也是我们在多线程环境下,为什么需要BlockingQueue 的原因。作为BlockingQueue的使用者,我们再也不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。既然BlockingQueue如此神通广大,让我们一起来见识下它的常用方法:

BlockingQueue的核心方法: 放入数据:
offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行
方法的线程)
offer(E o, long timeout, TimeUnit unit),可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续. 获取数据:
poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到
BlockingQueue有新的数据被加入;
drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),
通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

常见BlockingQueue
在了解了BlockingQueue的基本功能后,让我们来看看BlockingQueue家庭大致有哪些成员?

BlockingQueue成员详细介绍

  1. ArrayBlockingQueue
    基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统 中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部 锁是否采用公平锁,默认采用非公平锁。

  2. LinkedBlockingQueue
    基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速 度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

ArrayBlockingQueue和LinkedBlockingQueue是两个最普 通也是最常用的阻塞队列,一般情况下,在处理多线程间 的生产者消费者问题,使用这两个类足以。

3.16 谈一谈java线程安全的集合有哪些?

  1. 早期的线程安全集合
    Vector
    = 全部方法加 synchronized 的 ArrayList HashTable
    = 全部方法加 synchronized 的 HashMap

  2. 包装工具类
    Collections.synchronizedXXX()
    在原集合的基础上添加了锁对象,集合中的每个方法都通过这个锁对象实现同步

  3. java.util.concurrent包ConcurrentHashMap

1.7 分段锁技术,1.8 对table每行首元素加锁CopyOnWriteXXXX加了写锁,写的时候锁住的整个对象,读则可以并发执行

  1. 其他
    Stack
    继承了 Vector

3.17 Java中为什么会出现Atomic类?试分析它的原理和缺点?

一. 我们经常会使用i++操作,大家都知道这个并不是线程安全的,这时通常会使用synchroized关键字来处理并发操作,在并发量不大的情况使用synchroized性能并不是特别高。在jdk1.6以前synchroized是重量级锁,无论有没有资源竞争都会对变量加锁,在jdk1.6之后引入了偏向锁和轻量级锁,效率才有了很大的提升。atomic类使用了cas的思想,只有真正资源竞争的时候才会有资源消耗,而且atomic是通过底层硬件指令集实现的,所以并发量不大的情况下性能更高。

二. 主要原理就是CAS(比较和交换), 涉及到三个值(V,O, N),V是内存中真正的值,O是加载到线程中的预期值,N是计算后的目标结果值,当计算出目标结果值时比较V和O是否相等,不相等代表V被其他线程改写过,那么将V重新赋值给O,然后重新计算目标值,再次重复上述步骤,这个称为自旋操作。

缺点:

  1. 存在ABA问题,因为每次都比较O和V的值,如果在比较之前V被多次改写过,最终的值还是之前的V,那么仅仅比较最终的V和O是无法知道这种情况的。
  2. 只能针对一个共享变量进行原子操作。
  3. 可以看到当V和O不等的时候就需要自旋操作,当并发数量很多,资源竞争激烈时,进行自旋操作等待的时间会很长,性能会大幅度降低,这时候使用其他锁会比较合适

3.18 说说ThreadLocal的使用场景?与Synchronized相比有什么特性

ThreadLocal和Synchronized虽然都和多线程有关.但是ThreadLocal是为了多线程时,每个线程对变量的独立访问.线程间该变量值互不影响.内部是由一个ThreadLocalMap,key为当前ThreadLocal的弱引用,value为变量值.

Synchronized则是另一个意思.多线程时通过同步锁实现多个线程同时只能有一个线程对变量/方法访问.

第四节 Java 虚拟机

4.1 谈一谈JAVA垃圾回收机制?

垃圾回收即garbage collection,简称GC,作用是在某块内存不再使用时及时对其进行释放的管理机制。
GC的几个重点就是怎么找到无用对象,怎么对其进行释放,何时进行GC等等

另外说一句,Hotspot VM里堆是分代回收的(分出新生代和老年代,分别进行回收),不知道ART里有没有类似的机制怎么找到无用对象?

目前来说有两种主流机制,引用计数:最简单的寻找无用对象的机制,当一个对象被引用一次,引用计数+1,当失去引用时引用计数-1,

当此对象引用计数为0时可以直接回收。这种方法有一个显而易见的问题:无法回收被循环引用的对象。
可达性分析:从一个根对象(GC Root)开始向下搜寻,可以认为搜寻到的所有有强引用的对象都是活跃对象,所有找不到的对象都是无用对象,无用对象可能会被即刻回收,也可能进行其他操作(比如执行对象的finalize() 方法)这里还会引出一点问题:关于强引用,软引用,弱引用和虚引用的分别处理,具体可以看#27
如何释放无用对象?
这个具体是看回收器的实现
何时开始GC?
任何时候都可能,当系统觉得你内存不足了就会开始回收常见的比如分配对象内存不足时(这里的内存不足有可能不是占用真的很高,可能是内存足够,但是没有连续内存空间去放这个对象),当前堆内存占用超过阈值时,手动调用 System.gc() 建议开始GC时,系统整体内存不足时等
这里举个例子:
你要分配一个大对象(比如一张图片),如果内存不足, 可能会发生下面这个情况:

  1. 首先开始一次GC,这次GC不会回收被软引用引用的对 象
  2. 如果内存仍然不足,做点其他事情尝试让内存足够,常见的比如:

堆整理:前面说过,内存不足有可能不是占用真的很高,可能是内存足够,但是没有连续内存空间去放这个对象,那就试试整理一下堆看看出来的空间够不够增大堆内存:尝试申请更大的堆来放对象

  1. 如果内存依然不足,再发起一次GC,这次GC会回收仅 被软引用或更弱引用引用的对象(这句话好像有点乱),然后再次尝试分配对象
  2. 如果内存还是不够,那没办法了,抛出OutOfMemoryError

4.3 回答一下什么是强、软、弱、虚引用以及它们之间的区别?

强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。

软引用可以和一个引用队列 ReferenceQueue 联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象, 不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列联合使ReferenceQueue用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列联合使用。当垃圾回收器ReferenceQueue准备回收一个对象时,如果发现它还有虚引用,就会在回
收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

4.3 简述JVM中类的加载机制与加载过程?

1.1 概述

虚拟机把描述类的数据从Class文件加载到内存,并对数据 进行校验、转换解析和初始化,最终形成可以被虚拟直接使用的java类型,这就是虚拟机的类加载机制。

1.2 类加载的过程

  • 加 载
  • 验 证
  • 准 备
  • 解 析
  • 初始化
  • 使 用
  • 卸载
    其中验证,准备,解析三个部分称为连接。其中解析和初始化的顺序可能会不同(可能先解析后初始化,也可能先初始化后解析)。

1.2.1 关于初始化

5种情况会触发类的初始化

  • 遇到new,getstatic,putstatic,invokesstatic这四个字节码指令时,如果类没有被初始化
  • 使用java.lang.reflect包的方法对类进行反射时,如果类没有被初始化,则先触发其初始化
  • 当初始化一个类时,其父类没有被初始化,则需要父类先初始化
  • 虚拟机启动时,用户需要制定一个执行的主类,虚拟机会先初始化这个类
  • JDK 1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例后的解析结果
    REF_getStatic,REF_putStatic,REF_invokeStatic 的 方 法 句柄,并且这个方法句柄所对应的类没有被初始化

1.3 类加载的过程

  • 通过一个类的全限名来获取定义此类的二进制字节流
  • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据的访问入口
    这里顺带再说下对象的加载过程。

1. 对象的创建

  • java虚拟机遇到一个new指令

  • 检查new引用代表的类是否被加载,解析和初始化加载过

    • 加载过
    • 没有加载过,先执行相应类的加载过程
  • 虚拟机为对象分配内存

    • 对象所需要的内存大小在类加载过后便可以确定
    • 为对象分配空间的过程等同于把一块确定大小的内存从java堆中划分出来。
  • java堆绝对规整

    • 绝对规整解释:所有用过的内存放在一边,空闲的内存放在另一边中间放着一个指针作为分
      界点的指示器。
    • 分配过程:指针向空闲空间那边挪动一段与对象大小相等的距离。这种分配方式称为“指针碰撞”。
  • java堆不规整

    • 不规整解释:已使用的内存和未使用的内存相互交错,虚拟机维护一个列表,记录那些内存是可用的。
    • 分配过程:分配时从列表中找一块足够大的空间划分给对象实例。并更新列表上的记录。这
      种分配方式称为“空闲列表”。
    • java堆是否规整是由所采用的垃圾收集器是否带有 压缩整理功能决定的。
  • 对象创建时并发问题
    描述:对象创建在虚拟机中是非常频繁的,因此在并发情况下是线程不安全的。可能指针正在为A对象分配内存,对象B又同时使用了原来的指针来分配内存。

  • 解决方案

    • 第一种方式:
      • 分配内存的动作进行同步处理
    • 第二种方式
      • 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲。哪个线程要分配内存就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时才需要同步锁定。
  • TLAB解释:TLAB全称ThreadLocalAllocBuffer,是线程的一块私有内存,如果设置了虚拟机参数-XX:UseTLAB, 在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续 使用,这个申请动作还是需要原子操作的。

  • 虚拟机将分配到的内存空间都初始化为零值(不包括对象头),这一操作保证了对象的实例字段在java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值

  • 虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希 码,对象的GC分代年龄等信息。这些信息存在对象头(Object Header)之中。

  • 执行对象初始化,即把对象按照程序员的意愿进行初始化。

2 对象的访问定位

  • 前面的学习我们知道,java虚拟机栈保存的是对象的句柄或者对象地址。所以访问对象需要通过栈上的局部变量表(reference)来操作堆上的具体对象。因为reference只是一个只想对象的引用,并没有定义这个 引用应该通过什么方式定位,访问堆中的对象的具体位置,所以对象访问的方法取决于虚拟机的实现方式。
    目前主流的访问方式有以下两种
    • 句柄式:

      • java堆中有一块内存作为句柄池,reference存储的就是对象的句柄地址。句柄包含了对象实例数据与类型数据各自具体的地址信息。

      • 优点:对象被移动时(垃圾回收时,整理内存 时,很有可能被移动)只会改变句柄中的实例数据指针,不用改变reference

    • 直接指针访问:

      • reference存储的直接就是对象地址
      • 优点:快,省略了一次句柄到具体位置的访问时间。因为对象比较多,所以这个时间很可观

4.4 JVM、Dalvik、ART三者的原理和区别?

JVM:是Java Virtual Machine的缩写,其并不是指某个特定的虚拟机实现,而指任何能够运行Java字节码(class文件)的虚拟机实现,比如oracle的HotspotVM

Dalvik:是Google写的一个用于Android的虚拟机,但其如其字节码格式是dex而非class)该虚拟机在5.0时被ART替代
ART:是Android Runtime的缩写,严格来说并不是一个虚拟机,在4.4~6.0时采用安装时全部编译为机器码的 方式实现,7.0时默认不全部编译,采用解释执行+JIT+空闲时AOT以改善安装耗时
ART在安卓4.4时加入,5.0取代dalvik作为唯一实现直到现在。

4.5 请谈谈Java的内存回收机制?

基础
内存分为栈(stack)和堆(heap)两部分:
栈记录了方法调用,每个线程拥有一个栈,栈的每一帧中保存有该方法调用的参数、局部变量、返回地址栈中被调用方法运行结束时,相应的帧会删除,参数和局部变量占用的空间也会释放堆是用来存储对象的,堆区由所有线程共享定义
垃圾回收指的是:JVM自动清空堆中无用的对象占用的空间 的过程
垃圾回收机制有:引用计数算法、可达性分析算法、标记清除算法、复制算法、标记整理算法、分代回收算法。

a、引用计数算法中,每个对象包含一个计数器,有新的引用指向该对象时计数器加1,引用移除时计数器减1,计数器为0时回收对象,引用计数法有个缺陷是无法检测出循环引用。

b、可达性分析算法中,每个对象都有一个用于标示该对象是否可到达的标记信息。从根出发跟随所有的引用,就可 以找到所有的可到达对象,不可到达对象就是需要垃圾回 收的对象。

c、标记清除算法流程为,垃圾回收启动时,Java程序暂停 运行,JVM从根出发找到所有的可达对象并标记,然后扫描 整个堆找到不可达对象并清空。缺点是标记、清除的效率 不高;会产生内存碎片。

d、复制算法为,将内存分为大小相等的两块,每次使用一块,使用完时将存活的对象复制到另一块,清空原来那块。在对象存活率较低时回收效率高,缺点是内存空间使用率只有一半。

e、标记整理算法下,找到所有的可达对象并标记后,将所有存活对象都向内存区域的一端移动,然后清理掉边界以外的内存区域。

f-1、分代回收算法下,堆分为三代:永久代、老年代、新生代,新生代又分为三个区域:eden区、from区和to区。f-2、永久世代主要存放静态文件如Java静态类、方法等, 对于垃圾回收没有显著影响。

f-3、分代回收流程为,上次垃圾回收后创建的对象叫新生对象,存放于eden区,eden区没有空间存放新生对象时, 触发一次MinorGC,采用标记整理算法将eden区和from区的可达对象复制到to区,清空eden区和from区空间,此时from区和to区已交换;当to区也放不下eden和from区的可达对象时,将部分对象放到成熟世代;当成熟世代也已满时触发MajorGC,采用标记清除算法进行垃圾回收。
f-4、其中,即使to区没有满,JVM也会移动生命周期足够久远的对象到成熟世代;MinorGC发送频率较高,
MajorGC发生频率较低。

4.6 JMM是什么?它存在哪些问题?该如何解决?

Java内存模型
存在的问题就是多线程对共享数据的读写一致性问题可以通过synchronized、cas、并发安全的数据结构、aqs 组件来解决!