Android面试准备之Java基础
一、Java基础知识
1 什么是面向对象,谈谈你对面向对象的理解
对比面向过程,是两种不同的处理问题的角度,面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者、及各自需要做什么。
三大特性:封装、继承、多态
- 封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项,内部细节对外部调用透明,外部调用无需修改或者关心内部实现。例:
JavaBean
- 继承:继承基类的方法,并做出自己的改变和/或扩展。子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的。
- 多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。关键在于继承、方法重写,并且父类引用指向子类对象,但无法调用子类特有的功能。
父类类型 变量名 = new 子类对象;
变量名.方法名();
2 ==和equals比较
==
对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址
equals
:object
中默认也是采用==
比较,通常会重写
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是一种所有符号的编码,是一个符号集,它规定了符号的二进制代码,没有规定这个二进制代码应该如何存储。它造成的结果是:
- 出现了Unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示Unicode。
- 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){...}
}
匿名内部类的构造方法总结
- 编译器生成
- 参数列表包括:
· 外部对象(定义在非静态域内)
· 父类的外部对象(父类非静态)
· 父类的构造方法参数(父类有构造方法且参数列表不为空)
· 外部捕获的变量(方法体内有引用外部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[];...
}
String
是final
修饰的,不可变,每次操作都会产生新的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;
}
可以发现Stringbuilder
和Stringbuffer
都是继承了abstractStringbuilder
这个抽象类,都是通过一个char
类型的数组进行存储字符串的,但是是String
类中的char
数组是final
修饰的,是不可变的,而StringBuilder
和StringBuffer
中的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_ELEMENTDATA
和DEFAULTCAPACITY_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
允许key
和value
为null
,而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.75threshold
:扩容阈值,计算方法为哈希桶长度*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中的ConcurrentHashMap
由Segment
和HashEntry
组成,把哈希桶数组切分成小数组,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现并发访问。
JDK8中的ConcurrentHashMap
选择了与HashMap
相同的Node
数组+链表+红黑树的结构。在锁的实现上,抛弃了原有的Segment
分段锁,采用CAS
+synchronized
实现更加细粒度的锁,将锁的级别控制在了哈希桶数组元素级别,只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他哈希桶数组元素的读写,大大提高了并发度
ConcurrentHashMap
的get
方法需要加锁吗
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
和指针next
用volatile
修饰,在多线程环境下线程A修改节点的value
或者新增节点对线程B可见,所以get
方法不需要加锁
ConcurrentHashMap
不支持key
或value
为null
的原因是什么
对key
不作过多讨论,可能是作者习惯问题。
如果value
为空,多线程环境下无法判断key
不存在还是值为空,而单线程环境下,HashMap
可以用containsKey(key)
判断是否包含这个key
,多线程环境下无法保证containsKey
方法的同步性
- 与
HashMap
迭代器强一致性不同,ConcurrentHashMap
迭代器是弱一致性的
在改变时会new
出新的数据从而不影响原有的数据 ,iterator
完成后再将头指针替换为新的数据,这样读线程可以使用老数据进行遍历,写线程可以并发地完成数据修改,提升了并发性
12 Java四大引用
- 强引用
使用最普遍的引用(new
),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机抛出OOM错误,使程序异常终止,也不会回收这种对象。
- 弱引用
JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。可以在缓存中使用弱引用。
- 软引用
如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它。软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。
- 虚引用
虚引用是四种引用中最弱的一种引用。我们永远无法从虚引用中拿到对象,被虚引用引用的对象就跟不存在一样。虚引用一般用来跟踪垃圾回收情况,或者可以完成垃圾收集器之外的一些定制化操作。
二、多线程基础
1 线程状态
线程通常有五种状态:创建,就绪,运行,阻塞和死亡状态。
阻塞的情况分为三种:
- 等待阻塞:运行的线程执行
wait
方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,不能自动唤醒,必须依靠其他线程调用notify
或notifyAll
方法才能被唤醒,wait
是object
类的方法 - 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,JVM会把该线程放入“锁池”中
- 其他阻塞:运行的线程执行
sleep
或join
方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep
状态超时、join
等待线程终止或者超时,线程重新转入就绪状态。
Java中的线程状态:
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。- 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
注意:调用start方法,该线程变成可运行状态,等待CPU分配,而不是直接运行。
Java线程状态切换(网图):
2 sleep()、wait()、join()、yield()的区别
- 锁池
所有需要竞争同步锁的线程都会放到锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,等前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列等待CPU资源分配。
- 等待池
当调用wait()
方法后,线程会放到等待池当中,等待池中的线程不会去竞争同步锁,只有调用了notify()
或notifyAll()
后等待池的线程才会开始去竞争锁,notify()
是随机从等待池选出一个线程放入锁池,notifyAll()
是将等待池所有线程放到锁池。
sleep()
和wait()
的区别
sleep
是Thread
类的静态本地方法,wait
是Object
类的本地方法sleep
方法不会释放锁,但wait
会释放,而且会加入等待队列中sleep
不依赖于synchronized
,但wait
需要与synchronized
配套使用sleep
不需要被唤醒,而wait
需要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()
会为线程打上一个中断标记,但不会立刻中断线程。其他情况:
- 线程处于
sleep
、wait
、join
状态下,调用此方法会清除中断标记,并抛出InterruptedException
异常 - 在线程处于IO方法阻塞时被调用(
java.nio.channels.InterruptibleChannel
)
通道将被关闭,将会抛出ClosedByInterruptException
异常并设置中断状态为true
- 在线程处于选择器中被调用(
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;
ThreadLocalMap
是ThreadLocal
的内部类,将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
中的key
为null
,而value
还存在线程变量的引用。只有线程结束,强引用链才会断掉,如果线程不结束,key
为null
的value
就会一直存在强引用。
为什么ThreadLocalMap的key不使用强引用
因为如果使用强引用,ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除就不会被回收,导致Entry
内存泄漏。
解决办法
- 每次使用完
ThreadLocal
都调用它的remove()
方法清除数据 - 将
ThreadLocal
变量定义成private static
,这样就一直存在ThreadLocal
的强引用,也就能保证任何时候都能通过ThreadLocal
的弱引用访问到value
值,进而清除掉。
7 线程池及相关参数
使用线程池,可以降低资源消耗,提高响应速度。它提高了线程利用率,降低了创建和销毁线程的消耗。
corePoolSize
:代表核心线程数。这些线程创建后并不会消除,而是常驻线程maximumPoolSize
:代表最大线程数。它与核心线程数对应,表示最大允许被创建的线程数,当核心线程数用完还无法满足需求时,会创建新的线程,但线程池内线程总数不会超过最大线程数keepAliveTime
、unit
:表示超出核心线程数之外线程的空闲存活时间,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基础相关推荐
- java如何创造一个整数的类_【技术干货】Java 面试宝典:Java 基础部分(1)
原标题:[技术干货]Java 面试宝典:Java 基础部分(1) Java基础部分: 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io 的 ...
- 《Java 后端面试经》Java 基础篇
<Java 后端面试经>专栏文章索引: <Java 后端面试经>Java 基础篇 <Java 后端面试经>Java EE 篇 <Java 后端面试经>数 ...
- java基本特性_Java面试总结之Java基础
无论是工作多年的高级开发人员还是刚入职场的新人,在换工作面试的过程中,Java基础是必不可少的面试题之一.能不能顺利通过面试,拿到自己理想的offer,在准备面试的过程中,Java基础也是很关键的.对 ...
- java实现次方的运算_【技术干货】Java 面试宝典:Java 基础部分(1)
海牛学院的 | 第 616 期 本文预计阅读 |18 分钟 Java 基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io 的语法, ...
- JAVA中两个char类型相加_【技术干货】Java 面试宝典:Java 基础部分(1)
海牛学院的 | 第 616 期 本文预计阅读 |18 分钟 Java 基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io 的语法, ...
- 【笑小枫-面试篇】Java基础面试题整理,努力做全网最全
写在前面 或许你只是想白嫖内容,或许你也会忽略这段文字,但我还是想弱弱的说 题目整理耗费了大量精力,希望可以给博主点赞收藏,谢谢大家啦 我呢,笑小枫,一个努力的普通人,也希望可以花1秒钟记住我一下 也 ...
- java中override快捷键_【基础回溯1】面试又被 Java 基础难住了?推荐你看看这篇文章。...
本文已经收录自 https://github.com/Snailclimb/JavaGuide (59k+ Star):[Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识. ...
- JAVA面试整理之——JAVA基础
1. HashMap的源码,实现原理,JDK8中对HashMap做了怎样的优化. 在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存 ...
- 非常全面的阿里的Java面试题目,涵盖Java基础+高级+架构
阿里技术一面 自我介绍 Java中多态是怎么实现的 Java中的几种锁 数据库隔离级别 脏读 幻读 ACID mysql的隔离级别 mysql索引实现,如何解决慢查询 数据库锁是怎么实现的 死锁的条件 ...
最新文章
- Github 高赞的 YOLOv5 引发争议?Roboflow 和开发者这样说...
- 【机器学习】银行贷款违约预测
- 分分合合分分,谷歌医疗走向大败退
- nodeJs配置相关以及JSON.parse
- WEB前端必须掌握的一些算法题
- 机器学习工程师 - Udacity 癌症检测深度学习
- K210 / Openmv实现 大津法/Otsu最大类间方差法 自适应二值化
- 混合模型简介与高斯混合模型
- python调用接口获取数据_python:接口间数据传递与调用方法
- 块裁剪后的矩形边界如何去掉_如何3分钟剪辑出满意的视频号视频?
- 拓端tecdat|R语言使用马尔可夫链Markov Chain, MC来模拟抵押违约
- Ubuntu18.04中安装virtualenv和virtualenvwrapper
- java集成微信扫码登录
- 本工具仅仅交流之用,把黑群晖洗白用,如果对此感兴趣请支持正版,请勿用于违法,作者不承担法律和相关连带责任,工具内有详细sn算号器,可供使用还有教程
- 远程windows蓝屏解决办法
- Linux 的7种文件类型及各颜色代表含义
- 红米3s进不了recovery_红米 3S中文Recovery刷机教程
- 中国第一个 Apache 顶级开源项目的突围之路!
- java.util.concurrent.TimeoutExceptiofor com.alibaba.nacos.shaded.io.grpc.stub.ClientCalls$GrpcFuture
- muParser公式库使用简介
热门文章
- android 设置iptv vlan tag的命令,IPTV+VLAN设置教程
- R语言编写自定义函数计算分类模型评估指标:准确度、特异度、敏感度、PPV、NPV、数据数据为模型预测后的混淆矩阵、比较多个分类模型分类性能(逻辑回归、决策树、随机森林、支持向量机)
- ftp 下载 工具,5款小白都能使用的ftp 下载 工具
- 计算机桌面有什么,电脑桌面是什么
- gazebo机器人电池仿真
- 在数据帧转发过程中源/目标IP地址,源/目标MAC地址的变化释疑---划重点!!!
- 矩阵分析:广义逆矩阵,{1}逆,MP逆,D逆
- 位运算常用技巧分析汇总(算法进阶)
- Docker容器之harbor私有仓库部署与管理
- oracle怎样一次多选,如何在Apex Oracle中创建和使用多选列表?