一、Java基础知识

1 什么是面向对象,谈谈你对面向对象的理解

对比面向过程,是两种不同的处理问题的角度,面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者、及各自需要做什么。

三大特性:封装、继承、多态

  • 封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项,内部细节对外部调用透明,外部调用无需修改或者关心内部实现。例:JavaBean
  • 继承:继承基类的方法,并做出自己的改变和/或扩展。子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的。
  • 多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。关键在于继承、方法重写,并且父类引用指向子类对象,但无法调用子类特有的功能。
父类类型 变量名 = new 子类对象;
变量名.方法名();

2 ==和equals比较

==对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址
equalsobject中默认也是采用==比较,通常会重写

public class StringDemo {public static void main(String args[]) {String str1 = "Hello";String str2 = new String("Hello");String str3 = str2;System.out.println(str1 == str2);//falseSystem.out.println(str1 == str3);//falseSystem.out.println(str2 == str3);//trueSystem.out.println(str1.equals(str2));//trueSystem.out.println(str1.equals(str3));//trueSystem.out.println(str2.equals(str3));//true}
}

3 Java的char是两个字节,是怎么存UTF-8字符的

UTF-8、ASCII码、Unicode

  • ASCII码

128个字符的编码,占用了一个字节(一个字节为8位256种状态)的后面7位,最前面的1位统一规定为0。

  • Unicode

Unicode是一种所有符号的编码,是一个符号集,它规定了符号的二进制代码,没有规定这个二进制代码应该如何存储。它造成的结果是:

  1. 出现了Unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示Unicode。
  2. Unicode在很长一段时间内无法推广。
  • UTF-8、UTF-16

UTF-8是Unicode的实现方式之一。UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。对于英语字母,UTF-8编码和ASCII码是相同的。UTF-8最小单位为1个字节,UTF-16最小单位为2个字节,char用UTF-16存储。

一个utf-8数字占1个字节,一个utf-8英文字母占1个字节,少数汉字每个占用3个字节,多数占用4个字节。字符串长度不等于字符数(如emoji字符)

总结

  • Java char不存储UTF-8的字节,而是UTF-16的
  • Unicode是字符集,不是编码,作用类似于ASCII码
  • Java String的length不是字符数

4 Java String可以有多长

  • 字符串有多长是指字符数还是字节数
  • 字符串有几种存在形式
  • 字符串的不同形式受到何种限制

字符串有几种存在形式

源文件:.java文件

`String longString = "aaa...aaa";`

字节码:.class文件

CONSTANT_Utf8_info{u1 tag;//0~65535实际存储65535个latin字符会报错,原因是编译器判断时用的是'<',kotlin可以存储65535//非latin字符可以存储65535,因为编译器对于汉字的判断用'>'u2 length;u1 bytes[length];//最多65535个字节
}

存储在虚拟机方法区的常量池中。

byte[] bytes = loadFromFile(new File("superLongText.txt"));
String superLongString new String(bytes);

受到虚拟机指令限制,字符数理论上限为Integer.MAX_VALUE,实际上限可能小于Integer.MAX_VALUE,如果堆内存较小,也会受到堆内存的限制。

总结

Java String字面量形式

  • 字节码中CONSTANT_Utf8_info的限制
  • Javac源码逻辑的限制
  • 方法区大小的限制

Java String运行时创建在堆上的形式

  • Java虚拟机指令newarray的限制
  • Java虚拟机堆内存大小的限制

5 final有什么作用,为什么局部内部类和匿名内部类只能访问局部final变量,匿名内部类有什么限制

  • 修饰类:表示类不可被继承
  • 修饰方法:表示方法不可被子类覆盖
  • 修饰变量:表示变量一旦被赋值就不可以更改,如果是引用类型的变量,初始化后不能再让其指向另一个对象,但是引用的值是可变的
public class FinalReferenceTest {public static void main() {final Person p = new Person(25);p.setAge(24);//合法p = null;//非法}
}

当外部类的方法运行结束时,方法中的局部变量就销毁了,但是内部类对象可能还存在,这时内部类对象就会访问一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类访问的是局部变量的快照。所以为了保证局部变量和内部类的成员变量一致,需要将局部变量设置为final。

  • 匿名内部类的名字

外部类+$N,N是匿名内部类的顺序

  • 匿名内部类的构造方法

第一种情形:

public class Client {public void run() {InnerClass innerClass = new Outerclass().new InnerClass(){...};}
}public class OuterClass {public abstract class InnerClass {abstract void test();}
}

编译结果:

public class Client$1{//非静态方法有自己的外部类实例引用,也有非静态父类的外部类实例引用public Client$1(Client client, OuterClass outerClass){...}
}

第二种情形:

public class Client {public void run() {InnerClass innerClass = new Outerclass().InnerClass(){...};}
}public class OuterClass {public interface InnerClass {void test();}
}

编译结果:

public class Client$1{public Client$1(Client client){...}
}

第三种情形(静态方法):

public class Client {public static void run() {InnerClass innerClass = new Outerclass().InnerClass(){...};}
}public class OuterClass {public interface InnerClass {void test();}
}

编译结果:

public class Client$1{public Client$1(){...}
}

捕获外部变量:

public class Client {public static void run() {final Object object = new Object();InnerClass innerClass = new Outerclass().InnerClass(){@Overridevoid test() {System.out.println(object.toString());}};}
}public class OuterClass {public interface InnerClass {void test();}
}

编译结果:

public class Client$1{public Client$1(Object object){...}
}

匿名内部类的构造方法总结

  1. 编译器生成
  2. 参数列表包括:
    · 外部对象(定义在非静态域内)
    · 父类的外部对象(父类非静态)
    · 父类的构造方法参数(父类有构造方法且参数列表不为空)
    · 外部捕获的变量(方法体内有引用外部final变量)
  • Lambda转换(SAM类型):single abstract method,只能代替接口类型并只能有一个方法

总结

  • 没有人类认知意义上的名字
  • 只能继承一个父类或实现一个接口
  • 父类是非静态的类型,则需父类外部实例来初始化
  • 如果定义在非静态作用域内,会引用外部类实例
  • 只能捕获外部作用域内的final变量
  • 创建时只有单一方法的接口可以用Lambda转换

6 String、StringBuffer、StringBuilder区别及源码分析

  • String
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {/** The value is used for character storage. */private final char value[];...
}

Stringfinal修饰的,不可变,每次操作都会产生新的String对象。

例如String s = new String(“xyz”);的执行过程是,先在常量池中找xyz这个对象,如果没有,先在常量池中创建这个字符串对象,然后在堆中创建常量池中这个对象的拷贝对象,栈中的局部变量s再指向堆中的对象。所以,执行这个语句有可能产生一个(常量池已经有xyz)或两个(常量池中没有xyz)对象。

  • StringBuilder/StringBuffer
public final class StringBufferextends AbstractStringBuilderimplements java.io.Serializable, CharSequence
{/*** Constructs a string buffer with no characters in it and an* initial capacity of 16 characters.*/public StringBuffer() {super(16);}...
}
public final class StringBuilderextends AbstractStringBuilderimplements java.io.Serializable, CharSequence
{/*** Constructs a string builder with no characters in it and an* initial capacity of 16 characters.*/public StringBuilder() {super(16);}
}
abstract class AbstractStringBuilder implements Appendable, CharSequence {/*** The value is used for character storage.*/char[] value;
}

可以发现StringbuilderStringbuffer都是继承了abstractStringbuilder这个抽象类,都是通过一个char类型的数组进行存储字符串的,但是是String类中的char数组是final修饰的,是不可变的,而StringBuilderStringBuffer中的char数组没有被final修饰,是可变的。

再看两个类的append方法,易知StringBuffer是线程安全的,而StringBuilder是线程不安全的

 @Overridepublic synchronized StringBuffer append(Object obj) {toStringCache = null;super.append(String.valueOf(obj));return this;}@Overridepublic synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this;}
    @Overridepublic StringBuilder append(Object obj) {return append(String.valueOf(obj));}@Overridepublic StringBuilder append(String str) {super.append(str);return this;}
    public AbstractStringBuilder append(String str) {if (str == null)return appendNull();int len = str.length();ensureCapacityInternal(count + len);str.getChars(0, len, value, count);count += len;//非原子操作,导致线程不安全return this;}private void ensureCapacityInternal(int minimumCapacity) {// overflow-conscious codeif (minimumCapacity - value.length > 0) {value = Arrays.copyOf(value,newCapacity(minimumCapacity));}}private int newCapacity(int minCapacity) {// overflow-conscious code//新数组的容量是原来的2倍+2int newCapacity = (value.length << 1) + 2;if (newCapacity - minCapacity < 0) {newCapacity = minCapacity;}return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)? hugeCapacity(minCapacity): newCapacity;}

源码中可以看出,如果原数组容纳不下新的字符串,将会创建一个新数组,其大小是原数组大小的两倍+2。

7 重载和重写的区别,怎样理解Java的方法分派

重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同、方法返回值和访问修饰符可以不同,发生在编译时。重载的条件是,参数类型不同、参数个数不同、参数顺序不同。而返回值不同不可以构成重载,原因是调用方法时往往是忽略返回值的,此时编译器就不能判断调用哪个方法,容易造成错误。

重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类(有返回值的可以改成void),抛出的异常范围小于等于父类,访问修饰符范围大于等于父类(protected修饰符可以改成public),如果父类访问修饰符为private则子类就不能重写该方法。

class SuperClass {public String getName() {return "Super"'}
}class SubClass {public String getName() {return "Sub"'}
}
public class Question4 {public static void main(String... args) {SuperClass superClass = new SubClass();//调用的重载方法取决于声明类型printHello(superClass);}public static void printHello(SuperClass superClass) {//调用的覆写方法取决于运行的实际类型System.out.println("Hello " + superClass.getName());}public static void printHello(SubClass subClass) {System.out.println("Hello " + subClass.getName());}
}
  • 静态分派-方法重载分派:编译期确定,依据调用者的声明类型和方法参数类型
  • 动态分派-方法覆写分派:运行时确定,依据调用者的实际类型分派

8 接口与抽象类

  • 抽象类中可以有普通成员方法,而接口中只能存在public abstract方法
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的
  • 抽象类只能继承一个,借口可以实现多个

9 ArrayList、LinkedList源码分析

ArrayList

  • 构造方法
  /*** Shared empty array instance used for empty instances.*/private static final Object[] EMPTY_ELEMENTDATA = {};/*** Shared empty array instance used for default sized empty instances. We* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when* first element is added.*/private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};/*** The array buffer into which the elements of the ArrayList are stored.* The capacity of the ArrayList is the length of this array buffer. Any* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA* will be expanded to DEFAULT_CAPACITY when the first element is added.*/// Android-note: Also accessed from java.util.Collectionstransient Object[] elementData; // non-private to simplify nested class access/*** Constructs an empty list with an initial capacity of ten.*/public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}/*** Constructs an empty list with the specified initial capacity.** @param  initialCapacity  the initial capacity of the list* @throws IllegalArgumentException if the specified initial capacity*         is negative*/public ArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}}

构造方法主要用到上述两种,分别指定与不指定初始容量,而两种构造方法在构造初始容量为0时分别使用了两个不同的空数组EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA,根据注释表明其用来区分加入第一个元素时用到不同的扩容策略。

  • 扩容策略
private static final int DEFAULT_CAPACITY = 10;public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;
}
private void ensureCapacityInternal(int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}ensureExplicitCapacity(minCapacity);
}private void ensureExplicitCapacity(int minCapacity) {//ArrayList被修改的次数,用于Fail-Fast机制检测modCount++;// 容量不足,需要扩容if (minCapacity - elementData.length > 0)grow(minCapacity);
}private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;//当前容量int newCapacity = oldCapacity + (oldCapacity >> 1);//设定扩容到当前的1.5倍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);//新建一个数组,将原数组复制到新数组,完成扩容
}

对于不指定初始容量的ArrayList(即空数组采用DEFAULTCAPACITY_EMPTY_ELEMENTDATA),最小容量取10和当前需要容量的最大值,否则将数组大小直接扩容至所需最小容量。由扩容机制可知,ArrayList扩容时需要将原来的数组拷贝到新数组,效率较低,所以ArrayList不适用于add次数过多的场景。

Fail-Fast机制懒得整理了,我的理解是,在迭代时修改了Collection,导致迭代失败,不限于单线程和多线程。贴个参考文章:Fail-Fast机制
链接文章中提到的避免出现Fail-Fast的方法:
1.采用迭代器的修改方法而不是集合类的修改方法进行修改
2.采用java并发包(java.util.concurrent)中的类来代替 ArrayList 和hashMap

LinkedList

  • 构造方法
public LinkedList() {}

LindedList构造方法是一个空方法【摊手.gif

  • add
public boolean add(E e) {linkLast(e);return true;
}void linkLast(E e) {final Node<E> l = last;final Node<E> newNode = new Node<>(l, e, null);last = newNode;if (l == null)first = newNode;elsel.next = newNode;size++;modCount++;
}

添加元素时,分配空间,并将元素放入末端。性能实验中发现顺序添加元素时LinkedList以不同量级远差于ArrayList,盲猜就是由于每一次添加元素LinkedList都要分配空间。

总结

  • ArrayList因为是基于动态数组去实现,在随机存取时,有着良好的性能。而增删时需要扩容,整块移动元素,所以相对较慢。但在数据量很大,顺序添加时是个例外,这种情况下它的性能优于LinkedList。
  • LinkedList因为是基于链表实现,随机增删较快,而存取时需要遍历查询,相对于ArrayList会更慢。

10 HashMap、HashTable区别,HashMap源码分析

概述

  • HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全
  • HashMap允许keyvaluenull,而HashTable不允许

HashMap源码分析

  • 构造方法
 transient Node<K,V>[] table;static final float DEFAULT_LOAD_FACTOR = 0.75f;static final int MAXIMUM_CAPACITY = 1 << 30;static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}

构造方法有四个,分别可以设置初始容量、负载因子等,其中有几个重要参数:

  • loadFactor:默认为0.75
  • threshold:扩容阈值,计算方法为哈希桶长度*loadFactor
  • MAXIMUM_CAPACITY:最大容量,为2的30次方
  • DEFAULT_INITIAL_CAPACITY:默认初始容量为16
static final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

此方法将初始容量转换为2的n次方的形式,转换方式为将初始容量从第一位算起,所有低位都用1填满,然后再加1,最后将2的n次方形式的计算结果返回给哈希桶扩容阈值

  • put方法
static final int TREEIFY_THRESHOLD = 8;static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//判断哈希表是否为空,为空则直接扩容if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//(n-1)&hash为取模运算,相当于hash%length,但效率更高,这里用于判断节点是否已有值if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;//hash值相等,key也相等,则直接覆盖此键值对if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//不是覆盖操作,且当前哈希值下为红黑树的处理else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//不是覆盖操作,而是发生了哈希冲突else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {//遍历当前哈希值下的链表,找不到相同key值,则在结尾增加一个节点p.next = newNode(hash, key, value, null);//链表长度大于等于8,转换为红黑树处理if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}//找到相应key值的节点,覆盖value值if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;//用于fail-fast机制检验集合修改次数if (++size > threshold)//超过扩容阈值,进行扩容resize();afterNodeInsertion(evict);//官方注释为:Callbacks to allow LinkedHashMap post-actionsreturn null;
}
  • 扩容方法
    final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;//旧容量int oldThr = threshold;//旧扩容阈值int newCap, newThr = 0;if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}//不超过最大容量的话扩容到原来的2倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; }else if (oldThr > 0) //hashmap为空但有阈值,说明是构造方法初始化时指定了newCap = oldThr;//新容量指定为旧阈值else {//构造方法未进行设置,则指定为默认初始容量为16,默认阈值为12               newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//构建新哈希桶table = newTab;if (oldTab != null) {//将原哈希桶中的元素复制过来,过程与put过程类似,不同主要是,由于扩容到原来的两倍,所以发生哈希碰撞后产生链表后的节点,会被分配到原位(低位)或者两倍处的高位for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {//利用哈希值&旧的容量,可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {//将低位链表存放在原位loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {//将高位链表存放在高位hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}

11 ConcurrentHashMap原理

先贴一篇别人的分析:ConcurrentHashMap源码分析

  • JDK7、JDK8的区别

JDK7中的ConcurrentHashMapSegmentHashEntry组成,把哈希桶数组切分成小数组,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现并发访问。
JDK8中的ConcurrentHashMap选择了与HashMap相同的Node数组+链表+红黑树的结构。在锁的实现上,抛弃了原有的Segment分段锁,采用CAS+synchronized实现更加细粒度的锁,将锁的级别控制在了哈希桶数组元素级别,只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他哈希桶数组元素的读写,大大提高了并发度

  • ConcurrentHashMapget方法需要加锁吗
static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;volatile V val;volatile Node<K,V> next;
}

由于Node的元素val和指针nextvolatile修饰,在多线程环境下线程A修改节点的value或者新增节点对线程B可见,所以get方法不需要加锁

  • ConcurrentHashMap不支持keyvaluenull的原因是什么

key不作过多讨论,可能是作者习惯问题。
如果value为空,多线程环境下无法判断key不存在还是值为空,而单线程环境下,HashMap可以用containsKey(key)判断是否包含这个key,多线程环境下无法保证containsKey方法的同步性

  • HashMap迭代器强一致性不同,ConcurrentHashMap迭代器是弱一致性的

在改变时会new出新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据,这样读线程可以使用老数据进行遍历,写线程可以并发地完成数据修改,提升了并发性

12 Java四大引用

  • 强引用

使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机抛出OOM错误,使程序异常终止,也不会回收这种对象。

  • 弱引用

JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。可以在缓存中使用弱引用。

  • 软引用

如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它。软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。

  • 虚引用

虚引用是四种引用中最弱的一种引用。我们永远无法从虚引用中拿到对象,被虚引用引用的对象就跟不存在一样。虚引用一般用来跟踪垃圾回收情况,或者可以完成垃圾收集器之外的一些定制化操作。

二、多线程基础

1 线程状态

线程通常有五种状态:创建,就绪,运行,阻塞和死亡状态。
阻塞的情况分为三种:

  • 等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,不能自动唤醒,必须依靠其他线程调用notifynotifyAll方法才能被唤醒,waitobject类的方法
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,JVM会把该线程放入“锁池”中
  • 其他阻塞:运行的线程执行sleepjoin方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时,线程重新转入就绪状态。

Java中的线程状态:

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。

注意:调用start方法,该线程变成可运行状态,等待CPU分配,而不是直接运行。

Java线程状态切换(网图):

2 sleep()、wait()、join()、yield()的区别

  • 锁池

所有需要竞争同步锁的线程都会放到锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,等前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列等待CPU资源分配。

  • 等待池

当调用wait()方法后,线程会放到等待池当中,等待池中的线程不会去竞争同步锁,只有调用了notify()notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放入锁池,notifyAll()是将等待池所有线程放到锁池。

  • sleep()wait()的区别
  1. sleepThread类的静态本地方法,waitObject类的本地方法

  2. sleep方法不会释放锁,但wait会释放,而且会加入等待队列中

  3. sleep不依赖于synchronized,但wait需要与synchronized配套使用

  4. sleep不需要被唤醒,而wait需要

  5. sleep一般用于当前线程休眠,或者轮询暂停,wait则用于多线程通信

  • yield()方法

执行后线程直接进入就绪状态,马上释放了CPU的执行权,但保留了CPU的执行资格,所以有可能CPU下次线程调度·还会让这个线程获取到执行权继续执行

  • join()方法

join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程(即线程A插队)

public static void main(String[] args)throws InterruptedException {Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.pringln("t1->run");}});t1.start();t1.join();System.out.println("main->run");
}

运行结果:

t1->run
main->run

3 停止线程的方法stop()、interrupt()、interrupted()有什么区别

  • stop()方法

stop()方法为暴力停止线程,会立即释放线程所有资源并终止线程,会导致例如,锁异常释放、下载中断产生内存碎片等不良后果,目前Java已废除该方法,根据源码,调用会抛出UnsupportedOperationException异常。

  • interrupt()方法

interrupt()会为线程打上一个中断标记,但不会立刻中断线程。其他情况:

  1. 线程处于sleepwaitjoin状态下,调用此方法会清除中断标记,并抛出InterruptedException异常
  2. 在线程处于IO方法阻塞时被调用(java.nio.channels.InterruptibleChannel)
    通道将被关闭,将会抛出ClosedByInterruptException异常并设置中断状态为true
  3. 在线程处于选择器中被调用(java.nio.channels.Selector)
    中断状态将被设置为true,线程立即从选择操作中返回,可能有一个非0值,就像选择器的(java.nio.channels.Selector#wakeup)方法被调用一样(没用个这个玩意,这句注释没看懂)
  • interrupted()方法

静态方法,测试当前线程是否被设置了中断标记,并清除标记。

  • isInterrupted()方法

测试线程是否被设置了中断标记,不清除此标记。

4 关于线程安全的理解

线程安全,其实是内存安全,堆是共享内存,可以被所有线程访问。

当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。

堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了需要归还给操作系统,否则就是发生了内存泄漏。

在Java中,堆是虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

栈是每个线程独有的,保存其运行状态和局部变量。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里显式分配和释放。

5 守护线程

守护线程守护整个JVM中所有用户线程,依赖整个进程运行,如果其他线程全部结束,守护线程会被中断。
GC垃圾回收线程就是一个经典的守护线程,当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
守护线程中产生的新线程也是守护线程。
Java自带的多线程框架,会将守护线程转换为用户线程,所以如果要使用后台线程就不能用Java线程池。

6 ThreadLocal原理

  • ThreadLocal

ThreadLocal为线程本地变量。
Thread类中有一个成员变量threadLocals

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMapThreadLocal的内部类,将ThreadLocal对象以弱引用存储在key中,结构如下:

    static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}}

set()方法会将当前线程的ThreadLocalMap对象取出,以当前ThreadLocal对象为key存储进ThreadLocalMap对象中

    public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}

使用ThreadLocal可以打破多层次传递约束,减少代码冗余。例如在View.java中,setBackgroundDrawable方法就会将padding值存储在threadLocal中,方便随时读取。而且每一个线程都存储了ThreadLocalMap对象,无需使用同步机制就实现了线程间数据隔离。

  • ThreadLocal内存泄漏

如果一个ThreadLocal不存在外部强引用,key就会被GC回收,这样就会导致ThreadLocalMap中的keynull,而value还存在线程变量的引用。只有线程结束,强引用链才会断掉,如果线程不结束,keynullvalue就会一直存在强引用。

为什么ThreadLocalMap的key不使用强引用

因为如果使用强引用,ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除就不会被回收,导致Entry内存泄漏。

解决办法

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据
  • ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到value值,进而清除掉。

7 线程池及相关参数

使用线程池,可以降低资源消耗,提高响应速度。它提高了线程利用率,降低了创建和销毁线程的消耗。

  • corePoolSize:代表核心线程数。这些线程创建后并不会消除,而是常驻线程
  • maximumPoolSize:代表最大线程数。它与核心线程数对应,表示最大允许被创建的线程数,当核心线程数用完还无法满足需求时,会创建新的线程,但线程池内线程总数不会超过最大线程数
  • keepAliveTimeunit:表示超出核心线程数之外线程的空闲存活时间,unit为时间单位。可以通过setKeepAliveTime来设置空闲时间
  • workQueue:用来存放待执行的任务,当核心线程已被使用,还有任务进来则全部放入队列,直到队列被放满但任务还持续进入,则会开始创建新的线程。
  • Handler:任务拒绝策略。线程池达到最大线程数,任务队列也满时,应用任务拒绝策略。
  • ThreadFactory:线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂。

创建新线程时,需要获取全局锁,这时其它的就得阻塞,影响了整体效率,所以当核心线程数满了之后,任务来了先进阻塞队列。

几种常用线程池使用场景

Executor executor1 = Executors.newCachedThreadPool();
Executor executor2 = Executors.newSingleThreadPool();
Executor executor3 = Executors.newFixedThreadPool();
Executor executor4 = Executors.newScheduledThreadPool();
  • 单线程线程池:用来进行单一任务的处理(如取消)
  • 指定线程数的线程池:集中处理瞬时爆发的任务,如处理大量图片,使用后需要及时关闭

8 单例的实现原理

单例的基本实现思路是,每次调用单例对象时,先检查有没有初始化,如果没有才进行初始化,否则直接使用,基本实现如下:

class SingleMan {private static SingleMan sInstance;private SingleMan {}static SingleMan newInstance {if (sInstance == null) {sInstance = new SingleMan();}return sInstance;}
}

此写法有明显线程同步的问题。A、B线程同时检查sInstance == null后,A线程new出新的对象,可能先返回使用,B线程再new出新的对象,线程A返回使用的是一个无效对象。所以,会想到将方法加一个synchronized

class SingleMan {private static SingleMan sInstance;private SingleMan {}static synchronized SingleMan newInstance {if (sInstance == null) {sInstance = new SingleMan();}return sInstance;}
}

但这样效率比较低,初始化成功后,每次取单例对象都要检查同步锁,所以考虑先检查单例是否初始化,如果没有初始化,再加上同步锁进行初始化:

class SingleMan {private static SingleMan sInstance;private SingleMan {}static SingleMan newInstance {if (sInstance == null) {synchronized (SingleMan.class) {sInstance = new SingleMan();}}return sInstance;}
}

这种写法的问题是,当线程A、B同时检查到空时,线程A获得锁,创建对象,线程B等待,等待线程A创建完成后线程B获得锁重新创建一遍对象。于是,在同步锁中还需要检查一遍是否为空:

class SingleMan {private static SingleMan sInstance;private SingleMan {}static SingleMan newInstance {if (sInstance == null) {synchronized (SingleMan.class) {if (sInstance == null) {sInstance = new SingleMan();}}}return sInstance;}
}

这种写法也会有问题。当初始化过程发生指令重排,进行了创建新对象,而构造方法没有完成时,对象在虚拟机中被标记为可用,此时另外一个线程检查空时会拿到不完整的的对象。因此,正确的单例写法应该是:

class SingleMan {private static volatile SingleMan sInstance;private SingleMan {}static SingleMan newInstance {if (sInstance == null) {synchronized (SingleMan.class) {if (sInstance == null) {sInstance = new SingleMan();}}}return sInstance;}
}

Android面试准备之Java基础相关推荐

  1. java如何创造一个整数的类_【技术干货】Java 面试宝典:Java 基础部分(1)

    原标题:[技术干货]Java 面试宝典:Java 基础部分(1) Java基础部分: 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io 的 ...

  2. 《Java 后端面试经》Java 基础篇

    <Java 后端面试经>专栏文章索引: <Java 后端面试经>Java 基础篇 <Java 后端面试经>Java EE 篇 <Java 后端面试经>数 ...

  3. java基本特性_Java面试总结之Java基础

    无论是工作多年的高级开发人员还是刚入职场的新人,在换工作面试的过程中,Java基础是必不可少的面试题之一.能不能顺利通过面试,拿到自己理想的offer,在准备面试的过程中,Java基础也是很关键的.对 ...

  4. java实现次方的运算_【技术干货】Java 面试宝典:Java 基础部分(1)

    海牛学院的 | 第 616 期 本文预计阅读 |18 分钟 Java 基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io 的语法, ...

  5. JAVA中两个char类型相加_【技术干货】Java 面试宝典:Java 基础部分(1)

    海牛学院的 | 第 616 期 本文预计阅读 |18 分钟 Java 基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io 的语法, ...

  6. 【笑小枫-面试篇】Java基础面试题整理,努力做全网最全

    写在前面 或许你只是想白嫖内容,或许你也会忽略这段文字,但我还是想弱弱的说 题目整理耗费了大量精力,希望可以给博主点赞收藏,谢谢大家啦 我呢,笑小枫,一个努力的普通人,也希望可以花1秒钟记住我一下 也 ...

  7. java中override快捷键_【基础回溯1】面试又被 Java 基础难住了?推荐你看看这篇文章。...

    本文已经收录自 https://github.com/Snailclimb/JavaGuide  (59k+ Star):[Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识. ...

  8. JAVA面试整理之——JAVA基础

    1.     HashMap的源码,实现原理,JDK8中对HashMap做了怎样的优化. 在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存 ...

  9. 非常全面的阿里的Java面试题目,涵盖Java基础+高级+架构

    阿里技术一面 自我介绍 Java中多态是怎么实现的 Java中的几种锁 数据库隔离级别 脏读 幻读 ACID mysql的隔离级别 mysql索引实现,如何解决慢查询 数据库锁是怎么实现的 死锁的条件 ...

最新文章

  1. Github 高赞的 YOLOv5 引发争议?Roboflow 和开发者这样说...
  2. 【机器学习】银行贷款违约预测
  3. 分分合合分分,谷歌医疗走向大败退
  4. nodeJs配置相关以及JSON.parse
  5. WEB前端必须掌握的一些算法题
  6. 机器学习工程师 - Udacity 癌症检测深度学习
  7. K210 / Openmv实现 大津法/Otsu最大类间方差法 自适应二值化
  8. 混合模型简介与高斯混合模型
  9. python调用接口获取数据_python:接口间数据传递与调用方法
  10. 块裁剪后的矩形边界如何去掉_如何3分钟剪辑出满意的视频号视频?
  11. 拓端tecdat|R语言使用马尔可夫链Markov Chain, MC来模拟抵押违约
  12. Ubuntu18.04中安装virtualenv和virtualenvwrapper
  13. java集成微信扫码登录
  14. 本工具仅仅交流之用,把黑群晖洗白用,如果对此感兴趣请支持正版,请勿用于违法,作者不承担法律和相关连带责任,工具内有详细sn算号器,可供使用还有教程
  15. 远程windows蓝屏解决办法
  16. Linux 的7种文件类型及各颜色代表含义
  17. 红米3s进不了recovery_红米 3S中文Recovery刷机教程
  18. 中国第一个 Apache 顶级开源项目的突围之路!
  19. java.util.concurrent.TimeoutExceptiofor com.alibaba.nacos.shaded.io.grpc.stub.ClientCalls$GrpcFuture
  20. muParser公式库使用简介

热门文章

  1. android 设置iptv vlan tag的命令,IPTV+VLAN设置教程
  2. R语言编写自定义函数计算分类模型评估指标:准确度、特异度、敏感度、PPV、NPV、数据数据为模型预测后的混淆矩阵、比较多个分类模型分类性能(逻辑回归、决策树、随机森林、支持向量机)
  3. ftp 下载 工具,5款小白都能使用的ftp 下载 工具
  4. 计算机桌面有什么,电脑桌面是什么
  5. gazebo机器人电池仿真
  6. 在数据帧转发过程中源/目标IP地址,源/目标MAC地址的变化释疑---划重点!!!
  7. 矩阵分析:广义逆矩阵,{1}逆,MP逆,D逆
  8. 位运算常用技巧分析汇总(算法进阶)
  9. Docker容器之harbor私有仓库部署与管理
  10. oracle怎样一次多选,如何在Apex Oracle中创建和使用多选列表?