Java 基础
******这些都是Java核心知识点,也是最基础的知识,一定要学明白搞清楚*****
1、Java变量
局部变量-- 属于方法
类变量(静态变量)-- 属于类
成员变量(非静态变量)-- 属于对象
2、关于枚举
/**颜色枚举*/ enum ColorEnum{ RED,GREEN,BLUE }/** * 1、带有构造方法的枚举,构造方法为只能为private(默认可不写private); * 2、含带参构造方法的枚举,枚举值必须赋值; * 3、枚举中有了其他属性或方法之后,枚举值必须定义在最前面,且需要在最后一个枚举值后面加分号";" */ enum CarEnum{ BMW("宝马",1000000),JEEP("吉普",800000),MINI("mini",200000); private String name; private int price; /** * 从这里可以看出虽然枚举值不能直接由数字组成,但是我们可以给该枚举类添加一个int类型的值,通过构造方法给其赋值,相当于间接的可以使用数值 */ private CarEnum(String name,int price){ this.name = name; this.price = price; } //添加setter、getter方法 public String getName() {。。} public void setName(String name) {。。} public int getPrice() {。。} public void setPrice(int price) {。。} } /** * 由于枚举类都继承了Enum类,故我们定义的enum都不能在继承其他类了,但是可以实现其他接口 */ enum CarSetEnum implements Car;
3、访问控制修饰符
private (私有的):同一类内 可见。
不使用修饰符 (默认): 同一包内的类 可见。
protected (受保护的):同一包内的类、所有子类(可以是不同包下的子类) 可见。
public (共有的):对所有类可见。
这里的 可见 指的是能通过 ”类的对象.变量名“的方式访问,这是因为除static声明的变量属于类变量外,其他的都属于实例变量,是属于某个对象的!
如,Person p = new Person(); p.age直接访问age变量,对于那些私有的变量,很多情况下会对外提供public的setter和getter方法来供外部访问。
要注意的是,对于有继承关系的子类来说,比如 class A extends B,A直接继承拥有了默认的(在同一包下)、protected、public的这个字段,可以直接使用该字段,而不用通过再次的实例化父类或"父类对象.字段"的形式访问,因为在实例化A类的时候父类B已经实例化好了。特别的,对于protected来说,如下形式是编译不能通过的。
package com.a
public class A extends B{
public void test(){
B b = new B();
String str = b.age;//错误!不同包下的子类不能通过实例出来的父类获取protected的变量
String str2 = age;//正确,A类继承了B,直接拥有了该字段
String str3 = b.birthday;//正确,birthday为public
}
}
package com.b
public class B{
protected String age = "20";
public String birthday = "1995";
}
4、UTF-8和GBK编码转换
实现GBK编码字节流到UTF-8编码字节流的转换
先解码再编码,先通过GBK编码还原字符串,在该字符串正确的基础上得到“UTF-8”所对应的字节串。
new String(“需要转换的字节”,"GBK").getBytes("UTF-8")
5、try、catch、finally执行顺序问题
- try catch中只要有finally语句都要执行(有特例:如果try 或 catch 里面有 exit(0)就不会执行finally了);
- finally语句在try或catch中的return语句执行之后返回之前执行,且finally里的修改语句不能影响try或catch中 return已经确定的返回值;
- 若finally里也有return语句则覆盖try或catch中的return语句直接返回;
在遵守第(2)条return的情况下,执行顺序是:try-->catch(如果有异常的话)-->finally;
6、静态代码块、子类、父类初始化顺序
B extents A
static A
static B
I’m A class
A 构造方法
I’m B class
B 构造方法
执行顺序:1.静态代码块 --> 2.普通代码块 --> 3.构造方法
需要明白的是,1是类级别的,2和3是实例级别的,所以在父子类关系中,
上述的执行顺序为:父静-->子静-->父普-->父构造-->子普-->子构;
7、关于null对象、static变量和方法
静态方法是属于类的,在对象实例创建前就已经存在了,它的使用不依赖于对象是否被创建。当我们通过类的实例来调用时,最后实际上还是将对象实例转换成了类
null可以被强制转换成任意类型对象。
8、关于线程启动
start()方法启动一个线程,使其处于就绪状态,得到了CPU就会执行。
run()方法,就相当于是普通的方法调用,会在主线程中直接运行,此时没有开启一个线程。
9、关于内部类
往外部类的代码段中插入内部类声明
静态的内部类才可以定义static方法
抽象方法中不能有方法体。
静态方法不能够引用非静态变量。
10、Final修饰符、volatile修饰符
Final修饰符,用来修饰类、方法和变量。
修饰的类不能够被继承,
修饰的方法可以被继承、重载, 不能 被子类重写(即重新定义)。
声明final方法的主要目的是防止该方法的内容被修改。
volatile修饰符,修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
一个volatile对象引用可能是null。
12、可变参数
JDK 1.5 开始,Java支持传递同类型的可变参数给一个方法,一个方法中只能指定一个可变参数,它必须是方法的最后一个参数。任何普通的参数必须在它之前声明。
如,public void getStr(String ...str){}//正确
public void getStr2(String ...str,int a){}//错误,一个方法中可变参数只能有一个,它必须是方法的最后一个参数。
13、关于异常分类
所有的异常类是从java.lang.Exception类继承的子类。
Exception类是Throwable类的子类。除了Exception类外,Throwable还有一个子类Error 。Error用来指示运行时环境发生的错误。
四种异常
- 检查性异常: 不处理编译不能通过
- 非检查性异常:不处理编译可以通过,如果有抛出直接抛到控制台。
- 运行时异常(RuntimeException): 继承自RuntimeException类的就是非检查性异常
- 非运行时异常: 就是检查性异常
下面的表中列出了15个Java的非检查性异常(RuntimeException)。
异常 描述
| ArithmeticException | 当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。 |
| ArrayIndexOutOfBoundsException | 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。 |
| ArrayStoreException | 试图将错误类型的对象存储到一个对象数组时抛出的异常。 |
| ClassCastException | 当试图将对象强制转换为不是实例的子类时,抛出该异常。 |
| IllegalArgumentException | 抛出的异常表明向方法传递了一个不合法或不正确的参数。 |
| IllegalMonitorStateException | 抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。 |
| IllegalStateException | 在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。 |
| IllegalThreadStateException | 线程没有处于请求操作所要求的适当状态时抛出的异常。 |
| IndexOutOfBoundsException | 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。 |
| NegativeArraySizeException | 如果应用程序试图创建大小为负的数组,则抛出该异常。 |
| NullPointerException | 当应用程序试图在需要对象的地方使用 null 时,抛出该异常 |
| NumberFormatException | 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。 |
| SecurityException | 由安全管理器抛出的异常,指示存在安全侵犯。 |
| StringIndexOutOfBoundsException | 此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。 |
| UnsupportedOperationException | 当不支持请求的操作时,抛出该异常。 |
下面的表中列出了Java定义在java.lang包中的检查性异常类。
| 异常 | 描述 |
| ClassNotFoundException | 应用程序试图加载类时,找不到相应的类,抛出该异常。 |
| CloneNotSupportedException | 当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。 |
| IllegalAccessException | 拒绝访问一个类的时候,抛出该异常。 |
| InstantiationException | 当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常。 |
| InterruptedException | 一个线程被另一个线程中断,抛出该异常。 |
| NoSuchFieldException | 请求的变量不存在 |
| NoSuchMethodException | 请求的方法不存在 |
14、List遍历时删除的几种方式比较
1.1、会报错的删除方式:
(1)在Iterator遍历时使用list删除
Iterator<String> it = list.iterator();
while(it.hasNext()){
String item = it.next();
list.remove(item); //报错!!!
}
(2)foreach遍历方式中删除
for(String s : list){
list.remove(s); //报错!!!
}
以上都是报java.util.ConcurrentModificationException,某个线程在 Collection 上进行迭代时,通常不允许另一个线性修改该 Collection,因为在这些情况下,迭代的结果是不确定的。
而对于foreach实际上使用的是iterator进行处理的,而iterator是不允许集合在iterator使用期间通过list删除的,也就是第一种方式,也就是说上面两种方式相当于是同一种。
1.2、不会报错,但是有可能漏删或不能完全的删除方式:
(1)漏删的情况(通过索引下标的方式)
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(2);
list.add(3);
list.add(4);
System.out.println("----------list大小1:--"+list.size());
for (int i = 0; i < list.size(); i++) {
if (2 == list.get(i)) {
list.remove(i);
}
System.out.println(list.get(i));
}
System.out.println("最后输出=" + list.toString());
输出的结果如下:
----------list大小1:--5
1
2
3
4
最后输出=[1, 2, 3, 4]
可以看到,只删除了一个2,还有一个没有完全删除,原因是:删除了第一个2后,集合里的元素个数减1,后面的元素往前移了1位,此时,第二个2已经移到了索引index=1的位置,而此时i马上i++了,list.get(i)获得的是数据3。
(2)不能完全删除的情况
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(2);
list.add(3);
list.add(4);
System.out.println("----------list大小1:--"+list.size());
for (int i = 0; i < list.size(); i++) {
list.remove(i);
}
System.out.println("最后输出=" + list.toString());
输出的结果如下:
----------list大小1:--5
最后输出=[2, 3]
可以看到,结果并没有按照我们的想法,把所有数据都删除干净。原因是:在list.remove之后,list的大小发生了变化,也就是list.size()一直在变小,而 i 却一直在加大,当 i =3时,list.size()=2,此时循环的判断条件不满足,退出了程序。
以上两种情况通过for循环遍历删除,都没有正确达到目的,都是因为在remove后list.size()发生了变化(一直在减少),同时后面的元素会往前移动,导致list中的索引index指向的数据有变化。同时我们的for中的i是一直在加大的!
1.3 List遍历过程中删除元素的推荐做法
还是使用Iterator遍历,但是不用list来remove。如下代码:
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(2);
list.add(3);
list.add(4);
System.out.println("----------list大小1:--"+list.size());
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
Integer item = it.next();
if (2 == item) {
it.remove();
}
System.out.println(item);
}
System.out.println("最后输出=" + list.toString());
输出结果:
----------list大小1:--5
1
2
2
3
4
最后输出=[1, 3, 4]
此时,两个2被全部删除了。
对于iterator的remove()方法,也有需要我们注意的地方:
1、每调用一次iterator.next()方法,只能调用一次remove()方法。
2、调用remove()方法前,必须调用过一次next()方法。
15、Java基本数据类型及包装类
| byte(字节) | 8位 | Byte |
| shot(短整型) | 16位 | Short |
| int(整型) | 32位 | Integer |
| long(长整型) | 64位 | Long |
| float(浮点型) | 32位 | Float |
| double(双精度) | 64位 | Double |
| char(字符型) | 16位 | Character |
| boolean(布尔型) | 1位 | Boolean |
各数据类型按容量大小(表数范围大小)由小到大排列为:
byte < short, char < int < long < float < double
基本类型之间的转换原则:
- 运算时,容量小的类型自动转换为容量大的类型;
- 容量大的类型转换为容量小的类型时,要加强制转换符,且精度可能丢失;如:float f = 1.2f; int ff = (int) f; System.out.println(ff);//输出为1,丢掉了小数部分
- short,char之间不会互相转换(需要强制转换),byte、short、char并且三者在计算时首先转换为int类型;
- 实数常量默认为double类型, 整数常量默认为int类型;
16、switch中的参数类型
在jdk1.7 之前switch 只能支持 byte、short、char、int或者其对应的封装类以及 Enum 类型。
enum EnumTest {
LEFT,
RIGHT
}
EnumTest e = EnumTest.LEFT;
switch (e) {
case LEFT:
System.out.println("----left-----");
break;
default:
break;
}
在jdk1.7 及1.7以后,switch也支持了String类型,如下:
String str = "abc";
switch (str) {
case "abc":
System.out.println("-----abc-----");
break;
case "aaa":
System.out.println("-----aaa-----");
break;
}
17、equals与“==”的区别
“==”是一个运算符,它比较的是数据值或内存地址
对于基本数据类型,直接比较其数据值是否相等。如果是不同的基本数据类型之间进行比较,则遵循基本数据类型间运算的转换原则。
对于引用类型,==比较的还是值,只不过此时比较的是两个对象变量的内存地址。所以,用==来比较对象,实际上是判断这两个对象是否是同一个new出来的对象,或者是否是一个对象赋值给另一个对象的情况。
equals
equals方法是属于Object类的一个方法,其实现源码如下:
public boolean equals(Object obj) {
return (this == obj);
}
其实equals方法里面用的还是==运算符,所以对于那些没有重写过Object类的equals方法来说,==和equals方法是等价的!
然而,很多类都自己去重写了equals方法,比如String类、所有基本数据类型的包装类等
String类的equals源码如下:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
首先判断是否是同一个new出来的对象,即判断内存地址是否相同;如果不同则判断对象中的内容是否相同。
Integer类的equals方法如下:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
直接转成判断值是否相等了。
因此,对于String类和所有基本数据类型的包装类来说,equals方法就是判断其内容是否相等。对于其他类来说,要具体看其是否重写了equals方法及具体业务实现。
另:对于基本数据类型来说,使用equals方法,需要用该基本类型对应的包装类,因为equals是针对对象来使用的!
18、Object公用方法
Object类中的所有方法如下:
public boolean equals(Object obj) {//判断是否同一个对象,具体见上一点总结
return (this == obj);
}
public String toString(){
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
//返回该对象的哈希码值,重写了equals方法一般都要重写hashCode方法
public native int hashCode();
/**
*wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
*调用该方法后当前线程进入睡眠状态,直到以下事件发生。
*(1)其他线程调用了该对象的notify方法。
*(2)其他线程调用了该对象的notifyAll方法。
*(3)其他线程调用了interrupt中断该线程。
*(4)时间间隔到了。
*此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
*如:Person p = new Person();
*p.wait()//使用Person p对象作为对象锁。
*/
public final void wait() throws InterruptedException {...}
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {...}
//该方法唤醒在该对象上等待的某个线程。如p.notify();
public final native void notify();
//该方法唤醒在该对象上等待的所有线程。
public final native void notifyAll();
public final native Class<?> getClass();//获得运行时类型
//创建并返回此对象的一个副本。只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
protected native Object clone() throws CloneNotSupportedException;
//用于释放资源。当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。也可手动调用,自己实现一些资源的释放。
protected void finalize() throws Throwable { }
19、运算符相关
正数的原码/反码/补码相同
10存储为00000000 00000000 00000000 00001010
~10的原码为11111111 11111111 11111111 11110101(10取反)
~10的反码为10000000 00000000 00000000 00001010(最高位符号位,不变,其余位取反)
~10的补码为10000000 00000000 00000000 00001011(负数的补码=反码+1)
所以~10 = -11
&&和&,||和|的区别:
- &&是逻辑与(短路与),当第一个判断条件不满足要求时(返回false),第二个判断条件就不会执行;只有当两个判断条件都返回true时,整个逻辑运算才返回true。
- &按位与,不论什么情况下,两边的判断条件都会执行,当两边都返回true时,按位与才返回true。
- ||逻辑或,当第一个判断条件返回true时,逻辑或直接返回true,第二个判断条件就不会执行了;
- |按位或,不论什么情况下,两边的判断条件都会执行,当有一个条件返回true时,按位或就返回true。
注意:
逻辑与、逻辑或两边的运算符必须是boolean类型的,而按位与、按位或可以是boolean类型,两边也可以是int类型的。
当按位与、按位或两边是int类型时,将是通过二进制进行按位运算,规则就是:
(1)按位与&:都为1时,返回1,其他情况返回0;
(2)按位或|:有一个为1时,返回1,都为0时才返回0;
如下例子:3&2=2,3|2=3
3-->0011
2-->0010
&-->0010=2
|-->0011=3
20、泛型详解
泛型在编译时期进行严格的类型检查,消除了绝大多数的类型转换。
泛型在集合中使用广泛,在JDK1.5之后集合框架就全部加入了泛型支持。在没有使用泛型之前,我们可以往List集合中添加任何类型的元素数据,因为此时List集合默认的元素类型为Object,而在我们使用的时候需要进行强制类型转换,这个时候如果我们往List中加入了不同类型的元素,很容易导致类型
21、变量初始化问题
对于成员变量:
对于非final修饰的类的成员变量(包括static和非static),如果开发者没有给其赋初值,在编译时,JVM自动会给非final修饰的成员变量赋初值,我们在类的成员方法中就可以直接使用、运算了。
对于final修饰的成员变量,必须在定义的时候初始化,或者在构造方法中初始化(如果类中有多个构造方法,每个构造方法中都需要进行一次初始化),否则编译不通过,
这是因为final类型的变量不能修改,必须在初始定义的时候或者new出对象时构造器里进行初始化,其他时候不能变更。
对于非成员变量,即方法中的临时变量:
方法中的临时变量,只需要在使用前保证了初始化就可以。不一定要在定义的时候就初始化,但必须要在开始使用这个变量前初始化。
22、suspend()和resume()方法
suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态
24、自动拆箱、装箱问题
对于‘==’
如果运算符两边有一方是基本类型,一方是包装类型,在进行‘==’逻辑判断时,包装类型会自动进行拆箱操作,因此i==j返回true;
如果都是包装类型,那么‘==’就是按照正常判断逻辑来,==比较的是对象的地址。
注意:在-128至127这个区间,对象缓存在一个IntegerCache里面,所以'=='比较是相等的。
对于.equals()
equals不同的对象由不同的实现,对于Integer来说,equals比较的是值。
25、对资源的自动回收管理
平常我们在使用一些资源时,一般会在finally中进行资源的释放,如下形式:
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
而1.7之后我们可以使用这种形式:
try (BufferedReader br = new BufferedReader(new FileReader(path)) {
return br.readLine();
}
直接在try中进行声明,跟finally里面的关闭资源类似; 按照声明逆序关闭资源。这些资源都需要实现java.lang.AutoCloseable接口的资源。
26、Java中的四种引用:强引用、软引用、弱引用、虚引用
四种级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用
强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。如下的定义方式:
String str = new String("abc"); //强引用,在堆中创建了String这个对象,通过栈中的变量str引用这个对象
String str2 = str; //强引用,str2也指向了堆中创建的String对象
这两个引用都是强引用.只要存在对堆中String对象的引用,gc就不会回收该对象,如果通过下面代码:str = null; str2 = null;显示的设置引用str和str2为null,则gc就会认为堆中的String对象已经不存在其他引用了,此时该对象处于可回收的状态,但是到底什么时候回收该对象,取决于gc的算法。
软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。如下使用代码:
String str= new String("abc"); //强引用
Refenrence sr = new SoftReference(str); //软引用
//引用时
if(sr!=null){
str= sr.get();
}else{
str= new String("abc");
sr = new SoftReference(str);
}
可以看到不论是强引用、软引用、弱引用或者虚引用都是针对某个对象来说的,当我们某个对象需要设置为软引用时,只需要给该对象套入到软引用对象中即可,如上面的代码SoftReference sr = new SoftReference(str);
由于软引用在内存不足时可以被回收,在内存充足时不会被回收,所以软引用经常被用来作为缓存使用。比如在Android中经常把Bitmap作为软引用来缓存图片,如HashMap<String, SoftReference<Drawable>> imageCache;的方式。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
对于软引用或者弱引用来说,gc回收软引用或弱引用对象的过程是一样的,其执行过程如下:
String str= new String("abc"); //强引用
Refenrence sr = new SoftReference(str); //软引用
1 首先将软引用或弱引用的referent设置为null(即置str = null;),不再引用堆中的对象;
2 将堆中的对象new String("abc");设置为可结束的(finalizable)。
3 当heap中的new String("abc")对象的finalize()方法被运行而且该对象占用的内存被释放, sr被添加到它的ReferenceQueue中。
可以用如下代码来说明过程:
String str = new String("abc");
SoftReference<String> soft = new SoftReference<String>(str); //软引用
str = null;
System.out.println("before gc:" + soft.get());
System.gc();
System.out.println("after gc:" + soft.get());
输出结果:before gc: abc
after gc: abc
对于弱引用:
String str = new String("abc");
WeakReference<String> soft = new WeakReference<String>(str); //弱引用
str = null;
System.out.println("before gc:" + soft.get());
System.gc();
System.out.println("after gc:" + soft.get());
输出结果:before gc :abc
after gc: null
因此可以看出,软引用和弱引用被gc回收的过程是一致的,但是最后到底会不会回收掉该对象,要分情况。对于软引用来说,如果内存不足的情况下才会回收掉;对于弱引用来说,只要gc准备回收该弱引用对象,就会被立即释放掉。
虚引用(PhantomReference)
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 建立虚引用之后通过get方法返回结果始终为null。
四种引用类型的声明周期如下:
| 引用类型 | 被垃圾回收时间 | 用途 | 生存时间 |
| 强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
| 软引用 | 在内存不足时 | 对象缓存 | 内存不足时终止 |
| 弱引用 | 在垃圾回收时 | 对象缓存 | GC运行后终止 |
| 虚引用 | Unknown | Unknown | Unknown |
27、super的作用
在Java中super指代父类对象(直接父类),super相当于是一个直接new出来的父类对象,所以可以通过它来调用父类的那些非private修饰的变量、方法(对于我们普通new出来的对象来说,也就只能访问那些非private的成员变量、方法了,这里的访问是指通过“对象名.变量名或方法名”的形式)。所以,super这个对象也就是一个普通对象,同样遵循访问控制修饰符的准则。
对于子类,子类通过继承就直接拥有了父类的非private变量、方法,也就可以在子类中直接使用,再加一个super来修饰,岂不是显得有点多余了?正常情况下来说,是有点多余了(但是可以明确提示我们这是调用的父类变量或方法),但super关键字主要是用在以下两种情况中:
(1)发生了重写的情况
一个是重写了父类的方法;一个是重写了父类的成员变量;
重写父类方法的情况:
public class A {
String name = "lly";
protected void getName(){
System.out.println("父类getName->"+ name);
}
}
public class B extends A {
String nameB = "llyB";
@Override
protected void getName() {
System.out.println("子类getName->"+nameB);
super.getName();
}
public static void main(String[] args) {
B b = new B();
b.getName();
}
}
打印如下:
子类getName->llyB
父类getName->lly
在子类B中,我们重写了父类的getName方法,如果在重写的getName方法中我们去调用了父类的相同方法,必须要通过super关键字显示的指明出来。
如果不明确出来,按照子类优先的原则,相当于还是再调用重写的getName()方法,此时就形成了死循环,执行后会报java.lang.StackOverflowError异常。
重写父类变量的情况:
我们将B类简单改造一下:
public class B extends A {
String name = "llyB";
@Override
protected void getName() {
name = super.name;
System.out.println("子类getName->"+name);
}
public static void main(String[] args) {
B b = new B();
b.getName();
}
}
此时子类B中有一个和父类一样的字段(也可以说成父类字段被隐藏了),为了获得父类的这个字段我们就必须加上super,如果没有加,直接写成name = name;不会报错,只是会警告,表示此条语句没有任何意义,因为此时都是访问的子类B里面的那么字段。
我们通过super是不能访问父类private修饰的变量和方法的,因为这个只属于父类的内部成员,一个对象是不能访问它的private成员的。
(2)在子类的构造方法中
编译器会自动在子类构造函数的第一句加上 super(); 来调用父类的无参构造器;此时可以省略不写。如果想写上的话必须在子类构造函数的第一句,可以通过super来调用父类其他重载的构造方法,只要相应的把参数传过去就好。
因此,super的作用主要在下面三种情况下:
- 调用父类被子类重写的方法;
- 调用父类被子类重定义的字段(被隐藏的成员变量);
- 调用父类的构造方法;
其他情况,由于子类自动继承了父类相应属性方法,关键字super可以不显示写出来。
28、关于构造方法
如果一个类中没有写任何的构造方法,JVM会生成一个默认的无参构造方法。在继承关系中,由于在子类的构造方法中,第一条语句默认为调用父类的无参构造方法(即默认为super(),一般这句话省略了)。所以当在父类中定义了有参构造函数,都是没有定义无参构造函数时,IDE会强制要求我们定义一个相同参数类型的构造器。
以下子类B的情形是错误不能通过编译的:
public class A {
public A(String s){ }
}
public class B extends A { //编译错误,JVM默认给B加了一个无参构造方法,而在这个方法中默认调用了super(),但是父类中并不存在该构造方法
String name = "llyB";
}
public class B extends A { //同样编译错误,相同的道理,虽然我们在子类中自己定义了一个构造方法,但是在这个构造方法中还是默认调用了super(),但是父类中并不存在该构造方法
String name = "llyB";
public B(String s){}
}
此时就需要显示的去调用父类构造方法了,如下:
public class B extends A { //正确编译
String name = "llyB";
public B(String s){
super(s);
}
}
所以,只要记住,在子类的构造方法中,只要里面没有显示的通过super去调用父类相应的构造方法,默认都是调用super(),即无参构造方法,因此要确保父类有相应的构造方法。
29、transient关键字
当用transient关键字修饰一个变量时,这个变量将不会参与序列化过程。也就是说它不会在网络操作时被传输,也不会再本地被存储下来,这对于保护一些敏感字段(如密码等...)非常有帮助。
当我们一个对象实现了Serializable接口,这个对象的所有字段和方法就可以被自动序列化。当我们持久化对象时,可能有一个一些特殊字段我们不想让它随着网络传输过去,或者在本地序列化缓存起来,这时我们就可以在这些字段前加上transient关键字修饰,被transient修饰变量的值不包括在序列化的表示中,也就不会被保存下来。这个字段的生命周期仅存在调用者的内存中。
如例子:
public class UserBean implements Serializable{
private static final long serialVersionUID = 856780694939330811L;
private String userName;
private transient String password; //此字段不需要被序列化
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
测试类:
public class Test {
public static void main(String[] args) {
UserBean bean = new UserBean();
bean.setUserName("lly");
bean.setPassword("123");
System.out.println("序列化前--->userName:"+bean.getUserName()+",password:"+bean.getPassword());
//下面序列化到本地
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("e:/userbean.txt"));
oos.writeObject(bean);//将对象序列化缓存到本地
oos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
if(oos != null){
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//下面从本地反序列化缓存出来
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:/userbean.txt"));
bean = (UserBean) ois.readObject();
ois.close();
System.out.println("反序列化后获取出的数据--->userName:"+bean.getUserName()+",password:"+bean.getPassword());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
打印结果如下:
序列化前--->userName:lly,password:123
反序列化后获取出的数据--->userName:lly,password:null
看到,password反序列化后的值为null,说明它没有被保存到本地,因为我们给它加上了transient修饰。
注意:
- 从上面可以看到,被transient修饰后,反序列化后不能获取到值;
- transient只能修饰变量,不能修饰方法,修饰我们自定义的对象变量时,这个对象一定要实现Serializable接口;
- static变量不能被序列化,即使它被transient修饰。因为static修饰的变量是属于类的,而我们序列化是去序列化对象的。
上面的例子中如果给userName加上static修饰,反序列化后依然能够获取到值,但是这个时候的值是JVM 内存中对应的static的值,因为static修饰后,它属于类不属于对象,存放在一块单独的区域,直接通过对象也是可以获取到这个值的。上面的第三点依然成立。
30、关于类的继承
Java.lang.Thread、java.lang.Number、java.lang.Double、java.lang.Math、 java.lang.ClassLoader
- 对于Thread,我们可以继承它来创建线程;
- Byte、Short、Integer、Long、Float、Double几个数字类型都是继承自Number;
- Byte、Short、Integer、Long、Float、Double、Boolean、Character几个基本类型的包装类,以及String、Math它们都是被定义为public final class,因此这几个都不能被继承;
- 我们可以自定义类加载器来实现加载功能,因此ClassLoader是可以被继承的。
31、for和foreach遍历的比较
针对for和foreach两种循环,用10万、100万、1000万级别大小的List集合数据进行了测试一下,发现整体来说foreach的执行时间和普通for循环的时间差别不大。对于这两个的比较,我网上查了一下,有说foreach效率高一点的,有说for效率高一点的。
两种方式在遍历数组的时候时间普遍都较快,在遍历集合的时候耗时会更久一些。在遍历数组时,foreach的表现要稍微好一点,在遍历集合的时候,for的表现要好一点。但是不管哪种情况,for和foreach这两种遍历方式时间都相差不大。因此对于这两者的比较在时间效率来说应该相差不会很大(上面没有测试复杂数据的情况,以及其他集合结果的情况,可能不准确)。主要是在对于两者的应用场景上的选择:
- 普通for循环可以给定下标,因此当我们需要这个信息时,我们可以选用普通for循环来操作遍历;
- foreach在代码结构上更加清晰、简单;
- foreach在遍历的时候会锁定集合中的对象,期间不能修改,而for中可以修改集合中的元素。
32、Java IO与NIO
Java NIO(Java non-blocking IO)是JDK1.4以后推出的,相对于原来的IO来说,Java NIO是一种非阻塞的IO方式,它为所有的基本类型提供了缓存支持。
一般来说,I/O操作包括:对硬盘的读写、对socket的读写以及外设的读写。
阻塞和非阻塞:
阻塞:当某个事件或者任务在执行过程中,它发出一个请求操作,但是由于该请求操作需要的条件不满足,那么就会一直在那等待,直至条件满足;
非阻塞:当某个事件或者任务在执行过程中,它发出一个请求操作,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待。
这就是阻塞和非阻塞的区别。也就是说阻塞和非阻塞的区别关键在于当发出请求一个操作时,如果条件不满足,是会一直等待还是返回一个标志信息。
阻塞IO和非阻塞IO:
当用户线程发起一个IO请求操作(本文以读请求操作为例),内核会去查看要读取的数据是否就绪,对于阻塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪;
对于非阻塞IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪。当数据就绪之后,便将数据拷贝到用户线程,这样才完成了一个完整的IO读请求操作。
也就是说一个完整的IO读请求操作包括两个阶段:
1)查看数据是否就绪;
2)进行数据拷贝(内核将数据拷贝到用户线程)。
那么阻塞(blocking IO)和非阻塞(non-blocking IO)的区别就在于第一个阶段,如果数据没有就绪,在查看数据是否就绪的过程中是一直等待,还是直接返回一个标志信息。
Java中传统的IO都是阻塞IO,比如通过socket来读数据,调用read()方法之后,如果数据没有就绪,当前线程就会一直阻塞在read方法调用那里,直到有数据才返回;而如果是非阻塞IO的话,当数据没有就绪,read()方法应该返回一个标志信息,告知当前线程数据没有就绪,而不是一直在那里等待。
同步IO和异步IO:
从字面的意思可以看出:同步IO即 如果一个线程请求进行IO操作,在IO操作完成之前,该线程会被阻塞;
而异步IO为 如果一个线程请求进行IO操作,IO操作不会导致请求线程被阻塞。
事实上,同步IO和异步IO模型是针对用户线程和内核的交互来说的:
对于同步IO:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程;
而异步IO:只有IO请求操作的发出是由用户线程来进行的,IO操作的两个阶段都是由内核自动完成,然后发送通知告知用户线程IO操作已经完成。也就是说在异步IO中,不会对用户线程产生任何阻塞。
这是同步IO和异步IO关键区别所在,同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成。所以说异步IO必须要有操作系统的底层支持。(即同步IO是用户线程不断的轮询、有数据之后进行拷贝,而异步IO是内核完成这两个步骤,与用户线程无关。)
阻塞IO和非阻塞IO是反映在当用户请求IO操作时,如果数据没有就绪,是用户线程一直等待数据就绪,还是会收到一个标志信息这一点上面的。也就是说,阻塞IO和非阻塞IO是反映在IO操作的第一个阶段,在查看数据是否就绪时是如何处理的。
注意同步IO和异步IO与阻塞IO和非阻塞IO是不同的两组概念,同步IO和异步IO考虑的是由哪个线程(用户线程or内核线程)来完成IO的处理,而阻塞IO和非阻塞IO,针对的是IO操作中的第一个阶段的处理方式,是一直等待还是直接返回状态信息。
五种IO模型:
阻塞IO,非阻塞IO,IO复用,信号驱动IO,异步IO
33、实现多线程的两种方法
继承Thread:
以卖票为例:
public class MyThread extends Thread {
private static int COUNT = 5;
private int ticket = COUNT;
private String name;
public MyThread(String s){
name = s;
}
@Override
public void run() {
for(int i = 0; i < COUNT; i++){
if(ticket > 0){
System.out.println(name + "-->" + ticket--);
}
}
}
测试使用:
MyThread thread1 = new MyThread("thread1");
MyThread thread2 = new MyThread("thread2");
thread1.start();
thread2.start();
输出:
thread1-->5
thread2-->5
thread1-->4
thread2-->4
thread1-->3
thread2-->3
thread1-->2
thread2-->2
thread1-->1
thread2-->1
可以看到,这种方式每个线程自己拥有了一份票的数量,没有实现票的数量共享。下面看实现Runnable的方式:
实现Runnable接口:
public class MyRunnable implements Runnable {
private static int COUNT = 5;
private int ticket = COUNT;
@Override
public void run() {
for(int i = 0; i < COUNT; i++){
if(ticket > 0){
System.out.println("ticket-->" + ticket--);
}
}
}
}
测试使用:
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
new Thread(runnable).start();
输出:
ticket-->5
ticket-->3
ticket-->2
ticket-->1
ticket-->4
可以看到,实现Runnable的方式可以实现同一资源的共享。
实际工作中,一般使用实现Runnable接口的方式,原因:
- 支持多个线程去处理同一资源,同时,线程代码和数据有效分离,体现了面向对象的思想。
- 避免了Java的单继承性,如果使用继承Thread的方式,那这个扩展类就不能再去继承其他类。
34、线程同步的方法
当我们有多个线程要访问同一个变量或对象时,而这些线程中既有对改变量的读也有写操作时,就会导致变量值出现不可预知的情况。如下一个取钱和存钱的场景:
没有加入同步控制的情形:
public class BankCount {
private int count = 0;//余额
public void addMoney(int money){//存钱
count += money;
System.out.println(System.currentTimeMillis() + "存入:" + money);
System.out.println("账户余额:" + count);
}
public void getMoney(int money){//取钱
if(count - money < 0){
System.out.println("余额不足");
System.out.println("账户余额:" + count);
return;
}
count -= money;
System.out.println(System.currentTimeMillis() + "取出:" + money);
System.out.println("账户余额:" + count);
}
}
测试类:
public class BankTest {
public static void main(String[] args) {
final BankCount bankCount = new BankCount();
new Thread(new Runnable() {//取钱线程
@Override
public void run() {
while(true){
bankCount.getMoney(200);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {//存钱线程
@Override
public void run() {
while(true){
bankCount.addMoney(200);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
部分打印结果如下:
余额不足
账户余额:0
1462265808958存入:200
账户余额:200
1462265809959存入:200
账户余额:200
1462265809959取出:200
账户余额:200
1462265810959取出:200
账户余额:200
1462265810959存入:200
账户余额:200
1462265811959存入:200
账户余额:200
可以看到,此时有两个线程共同使用操作了bankCount对象中的count变量,使得count变量结果不符合预期。因此需要进行同步控制,同步控制的方法有以下几种:
(1)使用synchronized关键字同步方法
每一个Java对象都有一个内置锁,使用synchronized关键字修饰的方法,会使用Java的内置锁作为锁对象,来保护该方法。每个线程在调用该方法前,都需要获得内置锁,如果该锁已被别的线程持有,当前线程就进入阻塞状态。
修改BankCount 类中的两个方法,如下:
public synchronized void addMoney(int money){//存钱
count += money;
System.out.println(System.currentTimeMillis() + "存入:" + money);
System.out.println("账户余额:" + count);
}
public synchronized void getMoney(int money){//取钱
if(count - money < 0){
System.out.println("余额不足");
System.out.println("账户余额:" + count);
return;
}
count -= money;
System.out.println(System.currentTimeMillis() + "取出:" + money);
System.out.println("账户余额:" + count);
}
运行测试打印如下结果:
余额不足
账户余额:0
1462266451171存入:200
账户余额:200
1462266452171取出:200
账户余额:0
1462266452171存入:200
账户余额:200
1462266453171存入:200
账户余额:400
1462266453171取出:200
账户余额:200
1462266454171存入:200
账户余额:400
1462266454171取出:200
账户余额:200
1462266455171取出:200
账户余额:0
可以看到,打印结果符合我们的预期。
另外,如果我们使用synchronized关键字来修饰static方法,此时调用该方法将会锁住整个类。(关于类锁、对象锁下面有介绍)
(2)使用synchronzied关键字同步代码块
使用synchronized关键字修饰的代码块,会使用对象的内置锁作为锁对象,实现代码块的同步。
改造BankCount 类的两个方法:
public void addMoney(int money){//存钱
synchronized(this){
count += money;
System.out.println(System.currentTimeMillis() + "存入:" + money);
System.out.println("账户余额:" + count);
}
}
public void getMoney(int money){//取钱
synchronized(this){
if(count - money < 0){
System.out.println("余额不足");
System.out.println("账户余额:" + count);
return;
}
count -= money;
System.out.println(System.currentTimeMillis() + "取出:" + money);
System.out.println("账户余额:" + count);
}
}
(注:这里改造后的两个方法中因为synchronized包含了方法体的整个代码语句,效率上与在方法名前加synchronized的第一种同步方法差不多,因为里面涉及到了打印money还是需要同步的字段,所以全部包含起来,仅仅是为了说明synchronized作用...)
打印结果:
余额不足
账户余额:0
1462277436178存入:200
账户余额:200
1462277437192存入:200
账户余额:400
1462277437192取出:200
账户余额:200
1462277438207取出:200
账户余额:0
1462277438207存入:200
账户余额:200
1462277439222存入:200
账户余额:400
1462277439222取出:200
账户余额:200
可以看到,执行结果也符合我们的预期。
synchronized同步方法和同步代码块的选择:
同步是一种比较消耗性能的操作,应该尽量减少同步的内容,因此尽量使用同步代码块的方式来进行同步操作,同步那些需要同步的语句(这些语句一般都访问了一些共享变量)。但是像我们上面举得这个例子,就不得不同步方法的整个代码块,因为方法中的代码每条语句都涉及了共享变量,因此此时就可以直接使用synchronized同步方法的方式。
(3)使用重入锁(ReentrantLock)实现线程同步
重入性:是指同一个线程多次试图获取它占有的锁,请求会成功,当释放锁的时候,直到重入次数为0,锁才释放完毕。
ReentrantLock是接口Lock的一个具体实现类,和synchronized关键字具有相同的功能,并具有更高级的一些功能。如下使用:
public class BankCount {
private Lock lock = new ReentrantLock();//获取可重入锁
private int count = 0;//余额
public void addMoney(int money){//存钱
lock.lock();
try {
count += money;
System.out.println(System.currentTimeMillis() + "存入:" + money);
System.out.println("账户余额:" + count);
}finally{
lock.unlock();
}
}
public void getMoney(int money){//取钱
lock.lock();
try {
if(count - money < 0){
System.out.println("余额不足");
System.out.println("账户余额:" + count);
return;
}
count -= money;
System.out.println(System.currentTimeMillis() + "取出:" + money);
System.out.println("账户余额:" + count);
} finally{
lock.unlock();
}
}
}
部分打印结果:
1462282419217存入:200
账户余额:200
1462282420217取出:200
账户余额:0
1462282420217存入:200
账户余额:200
1462282421217存入:200
账户余额:400
1462282421217取出:200
账户余额:200
1462282422217存入:200
账户余额:400
1462282422217取出:200
账户余额:200
1462282423217取出:200
账户余额:0
同样结果符合预期,说明使用ReentrantLock也是可以实现同步效果的。使用ReentrantLock时,lock()和unlock()需要成对出现,否则会出现死锁,一般unlock都是放在finally中执行。
synchronized和ReentrantLock的区别和使用选择:
1、使用synchronized获得的锁存在一定缺陷:
- 不能中断一个正在试图获得锁的线程。
- 试图获得锁时不能像ReentrantLock中的trylock那样设定超时时间 ,当一个线程获得了对象锁后,其他线程访问这个同步方法时,必须等待或阻塞,如果那个线程发生了死循环,对象锁就永远不会释放。
- 每个锁只有单一的条件,不像condition那样可以设置多个。
2、尽管synchronized存在上述的一些缺陷,在选择上还是以synchronized优先:
- 如果synchronized关键字适合程序,尽量使用它,可以减少代码出错的几率和代码数量 ;(减少出错几率是因为在执行完synchronized包含完的最后一句语句后,锁会自动释放,不需要像ReentrantLock一样手动写unlock方法;)
- 如果特别需要Lock/Condition结构提供的独有特性时,才使用他们 ;(比如设定一个线程长时间不能获取锁时设定超时时间或自我中断等功能。)
- 许多情况下可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁情况;(比如当我们在多线程环境下使用HashMap时,可以使用ConcurrentHashMap来处理多线程并发)。
下面两种同步方式都是直接针对共享变量来设置的:
(4)对共享变量使用volatile实现线程同步
- volatile关键字为变量的访问提供了一种免锁机制
- 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
- 因此每次使用该变量就要重新计算,直接从内存中获取,而不是使用寄存器中的值
- volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
修改BankCount类如下:
public class BankCount {
private volatile int count = 0;//余额
public void addMoney(int money){//存钱
count += money;
System.out.println(System.currentTimeMillis() + "存入:" + money);
System.out.println("账户余额:" + count);
}
public void getMoney(int money){//取钱
if(count - money < 0){
System.out.println("余额不足");
System.out.println("账户余额:" + count);
return;
}
count -= money;
System.out.println(System.currentTimeMillis() + "取出:" + money);
System.out.println("账户余额:" + count);
}
}
部分打印结果:
余额不足
账户余额:200
1462286786371存入:200
账户余额:200
1462286787371存入:200
账户余额:200
1462286787371取出:200
账户余额:200
1462286788371取出:200
1462286788371存入:200
账户余额:200
账户余额:200
1462286789371存入:200
账户余额:200
可以看到,使用volitale修饰变量,并不能保证线程的同步。volitale相当于一种“轻量级的synchronized”,但是它不能代替synchronized,volitale的使用有较强的限制,它要求该变量状态真正独立于程序内其他内容时才能使用 volatile。volitle的原理是每次线程要访问volatile修饰的变量时都是从内存中读取,而不是从缓存当中读取,以此来保证同步(这种原理方式正如上面例子看到的一样,多线程的条件下很多情况下还是会存在很大问题的)。因此,我们尽量不会去使用volitale。
(5)ThreadLocal实现同步局部变量
使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal的主要方法有:
- initialValue():返回当前线程赋予当前线程拷贝的局部线程变量的初始值。一般在定义ThreadLocal类的时候会重写该方法,返回初始值;
- get():返回当前线程拷贝的局部线程变量的值;
- set(T value):为当前线程拷贝的局部线程变量设置一个特定的值;
- remove():移除当前线程赋予局部线程变量的值
如下使用:
public class BankCount {
private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
protected Integer initialValue() {
return 0;
};
};//余额
public void addMoney(int money){//存钱
count.set(count.get() + money);
System.out.println(System.currentTimeMillis() + "存入:" + money);
System.out.println("账户余额:" + count.get());
}
public void getMoney(int money){//取钱
if(count.get() - money < 0){
System.out.println("余额不足");
System.out.println("账户余额:" + count.get());
return;
}
count.set(count.get() - money);
System.out.println(System.currentTimeMillis() + "取出:" + money);
System.out.println("账户余额:" + count.get());
}
}
部分打印结果:
余额不足
1462289139008存入:200
账户余额:0
账户余额:200
余额不足
账户余额:0
1462289140008存入:200
账户余额:400
余额不足
账户余额:0
1462289141008存入:200
账户余额:600
余额不足
账户余额:0
从打印结果可以看到,测试类中的两个线程分别拥有了一份count拷贝,即取钱线程和存钱线程都有一个count初始值为0的变量,因此可以一直存钱但是不能取钱。
ThreadLocal使用时机:
由于ThreadLocal管理的局部变量对于每个线程都会产生一份单独的拷贝,因此ThreadLocal适合用来管理与线程相关的关联状态,典型的管理局部变量是private static类型的,比如用户ID、事物ID,我们的服务器应用框架对于每一个请求都是用一个单独的线程中处理,所以事物ID对每一个线程是唯一的,此时用ThreadLocal来管理这个事物ID,就可以从每个线程中获取事物ID了。
ThreadLocal和前面几种同步机制的比较:
1、hreadLocal和其它所有的同步机制都是为了解决多线程中的对同一变量的访问冲突,在普通的同步机制中,是通过对象加锁来实现多个线程对同一变量的安全访问的。这时该变量是多个线程共享的,使用这种同步机制需要很细致地分析在什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放该对象的锁等等很多。所有这些都是因为多个线程共享了资源造成的。
2、ThreadLocal就从另一个角度来解决多线程的并发访问,ThreadLocal会为每一个线程维护一个和该线程绑定的变量的副本,从而隔离了多个线程的数据,每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的整个变量封装进ThreadLocal,或者把该对象的特定于线程的状态封装进ThreadLocal。
3、ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源(变量),这样当然不需要对多个线程进行同步了。所以,如果你需要进行多个线程之间进行通信,则使用同步机制;如果需要隔离多个线程之间的共享冲突,可以使用ThreadLocal,这将极大地简化你的程序,使程序更加易读、简洁。
4、锁的等级:方法锁、对象锁、类锁
Java中每个对象实例都可以作为一个实现同步的锁,也即对象锁(或内置锁),当使用synchronized修饰普通方法时,也叫方法锁(对于方法锁这个概念我觉得只是一种叫法,因为此时用来锁住方法的可能是对象锁也可能是类锁),当我们用synchronized修饰static方法时,此时的锁是类锁。
对象锁的实现方法:
- 用synchronized修饰普通方法(非static);
- 用synchronized(this){...}的形式包括代码块;
上面两种方式获得的锁是同一个锁对象,即当前的实例对象锁。(当然,也可以使用其他传过来的实例对象作为锁对象),如下实例:
public class BankCount {
public synchronized void addMoney(int money){//存钱
synchronized(this){ //同步代码块
int i = 5;
while(i-- > 0){
System.out.println(Thread.currentThread().getName() + ">存入:" + money);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public synchronized void getMoney(int money){//取钱
int i = 5;
while(i-- > 0){
System.out.println(Thread.currentThread().getName() + ">取钱:" + money);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试类:
public class BankTest {
public static void main(String[] args) {
final BankCount bankCount = new BankCount();
new Thread(new Runnable() {//取钱线程
@Override
public void run() {
bankCount.getMoney(200);
}
},"取钱线程").start();
new Thread(new Runnable() {//存钱线程
@Override
public void run() {
bankCount.addMoney(200);
}
},"存钱线程").start();
}
}
打印结果如下:
取钱线程>取钱:200
取钱线程>取钱:200
取钱线程>取钱:200
取钱线程>取钱:200
取钱线程>取钱:200
存钱线程>存入:200
存钱线程>存入:200
存钱线程>存入:200
存钱线程>存入:200
存钱线程>存入:200
打印结果表明,synchronized修饰的普通方法和代码块获得的是同一把锁,才会使得一个线程执行一个线程等待的执行结果。
类锁的实现方法:
- 使用synchronized修饰static方法
- 使用synchronized(类名.class){...}的形式包含代码块
因为static的方法是属于类的,因此synchronized修饰的static方法获取到的肯定是类锁,一个类可以有很多对象,但是这个类只会有一个.class的二进制文件,因此这两种方式获得的也是同一种类锁。
如下修改一下上面代码的两个方法:
public void addMoney(int money){//存钱
synchronized(BankCount.class){
int i = 5;
while(i-- > 0){
System.out.println(Thread.currentThread().getName() + ">存入:" + money);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static synchronized void getMoney(int money){//取钱
int i = 5;
while(i-- > 0){
System.out.println(Thread.currentThread().getName() + ">取钱:" + money);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
打印结果和上面一样。说明这两种方式获得的锁是同一种类锁。
类锁和对象锁是两种不同的锁对象,如果将addMoney方法改为普通的对象锁方式,继续测试,可以看到打印结果是交替进行的。
注:(1)一个线程获得了对象锁或者类锁,其他线程还是可以访问其他非同步方法,获得了锁只是阻止了其他线程访问使用相同锁的方法、代码块;
(2)一个获得了对象锁的线程,可以在该同步方法中继续去访问其他相同锁对象的同步方法,而不需要重新申请锁。
35、线程池ThreadPool相关
在java.util.concurrent包下,提供了一系列与线程池相关的类。合理的使用线程池,可以带来多个好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行;
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池可以应对突然大爆发量的访问,通过有限个固定线程为大量的操作服务,减少创建和销毁线程所需的时间。
与线程执行、线程池相关类的关系如图:

我们一般通过工具类Executors的静态方法(如newFixedThreadPool())来获取ThreadPoolExecutor线程池或静态方法(如newScheduledThreadPool())来获取ScheduleThreadPoolExecutor线程池。如下使用:
ExecutorService threadpool= Executors.newFixedThreadPool(10);
我们指定了获取10个数量的固定线程池,Executors中有很多重载的获取线程池的方法,比如可以通过自定义的ThreadFactory来为每个创建出来的Thread设置更为有意义的名称。Executors创建线程池的方法内部也就是new出新的ThreadPoolExecutor或ScheduleThreadPoolExecutor,给我们配置了很多默认的设置。如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
上面通过ThreadPoolExecutor的构造方法,为我们创建了一个线程池,很多参数Executors工具类自动为我们配置好了。创建一个ThreadPoolExecutor线程池一般需要以下几个参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
(1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
(2)maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
(3)keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
(4)TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS)等。
(5)workQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue
(6)threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
(7)handler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。
我们尽量优先使用Executors提供的静态方法来创建线程池,如果Executors提供的方法无法满足要求,再自己通过ThreadPoolExecutor类来创建线程池。
提交任务的两种方式:
(1)通过execute()方法,如:
ExecutorService threadpool= Executors.newFixedThreadPool(10);
threadpool.execute(new Runnable(){...});
这种方式提交没有返回值,也就不能判断任务是否被线程池执行成功。
(2)通过submit()方法,如:
Future<?> future = threadpool.submit(new Runnable(){...});
try {
Object res = future.get();
} catch (InterruptedException e) {
// 处理中断异常
e.printStackTrace();
} catch (ExecutionException e) {
// 处理无法执行任务异常
e.printStackTrace();
}finally{
// 关闭线程池
executor.shutdown();
}
使用submit 方法来提交任务,它会返回一个Future对象,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。
线程池工作流程分析

从上图我们可以看出,当提交一个新任务到线程池时,线程池的处理流程如下:
1、首先线程池判断基本线程池是否已满(< corePoolSize ?)?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
2、其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
3、最后线程池判断整个线程池是否已满(< maximumPoolSize ?)?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。
也就是说,线程池优先要创建出基本线程池大小(corePoolSize)的线程数量,没有达到这个数量时,每次提交新任务都会直接创建一个新线程,当达到了基本线程数量后,又有新任务到达,优先放入等待队列,如果队列满了,才去创建新的线程(不能超过线程池的最大数maxmumPoolSize)。
关于线程池的配置原则可阅读参考文章。
ThreadPoolExecutor简单实例:
public class BankCount {
public synchronized void addMoney(int money){//存钱
System.out.println(Thread.currentThread().getName() + ">存入:" + money);
}
public synchronized void getMoney(int money){//取钱
System.out.println(Thread.currentThread().getName() + ">取钱:" + money);
}
}
测试类:
public class BankTest {
public static void main(String[] args) {
final BankCount bankCount = new BankCount();
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.execute(new Runnable() {//存钱线程
@Override
public void run() {
int i = 5;
while(i-- > 0){
bankCount.addMoney(200);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Future<?> future = executor.submit(new Runnable() {//取钱线程
@Override
public void run() {
int i = 5;
while(i-- > 0){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
bankCount.getMoney(200);
}
}
});
try {
Object res = future.get();
System.out.println(res);
} catch (InterruptedException e) {
// 处理中断异常
e.printStackTrace();
} catch (ExecutionException e) {
// 处理无法执行任务异常
e.printStackTrace();
}finally{
// 关闭线程池
executor.shutdown();
}
}
}
打印结果如下:
pool-1-thread-1>存入:200
pool-1-thread-1>存入:200
pool-1-thread-2>取钱:200
pool-1-thread-1>存入:200
pool-1-thread-2>取钱:200
pool-1-thread-1>存入:200
pool-1-thread-2>取钱:200
pool-1-thread-1>存入:200
pool-1-thread-2>取钱:200
pool-1-thread-2>取钱:200
null
可以看到,打印出来的future.get()获取的结果为null,这是因为Runnable是没有返回值的,需要返回值要使用Callable,这里就不再细说了,具体可参考如下文章:
36、生产者和消费者模型
生产者、消费者模型,描述是:有一块缓冲区作为仓库,生产者可以将产品放入仓库,消费者可以从仓库中取走产品。解决消费者和生产者问题的核心在于保证同一资源被多个线程并发访问时的完整性。一般采用信号量或加锁机制解决。下面介绍Java中解决生产者和消费者问题主要三种仿:
(1)wait() / notify()、notifyAll()
wait和notify方法是Object的两个方法,因此每个类都会拥有这两个方法。
wait()方法:使当前线程处于等待状态,放弃锁,让其他线程执行。
notify()方法:唤醒其他等待同一个锁的线程,放弃锁,自己处于等待状态。
如下例子:
/**
* 仓库
*/
public class Storage {
private static final int MAX_SIZE = 100;//仓库的最大容量
private List<Object> data = new ArrayList<Object>();//存储载体
/**
* 生产操作
*/
public synchronized void produce(int num){
if(data.size() + num > MAX_SIZE){//如果生产这些产品将超出仓库的最大容量,则生产操作阻塞
System.out.println("生产操作-->数量:" + num + ",超出仓库容量,生产阻塞!------库存:" + data.size());
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//到这里,表示可以正常生产产品
for(int i = 0; i < num; i++){//生产num个产品
data.add(new Object());
}
System.out.println("生产操作-->数量:" + num + ",成功入库~------库存:" + data.size());
//生产完产品后,唤醒其他等待消费的线程
notify();
}
/**
* 消费操作
*/
public synchronized void consume(int num){
if(data.size() - num < 0){//如果产品数量不足
System.out.println("消费操作-->数量:" + num + ",库存不足,消费阻塞!------库存:" + data.size());
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//到这里,表示可以正常消费
for(int i = 0; i < num; i++){//消费num个产品
data.remove(0);
}
System.out.println("消费操作-->数量:" + num + ",消费成功~------库存:" + data.size());
//消费完产品后,唤醒其他等待生产的线程
notify();
}
}
生产者:
public class Producer implements Runnable{
private Storage storage;
private int num;//每次生产多少个
public Producer(Storage sto,int num){
storage = sto;
this.num = num;
}
@Override
public void run() {
storage.produce(num);
}
}
消费者:
public class Consumer implements Runnable{
private Storage storage;
private int num;//每次消费多少个
public Consumer(Storage sto,int num){
storage = sto;
this.num = num;
}
@Override
public void run() {
storage.consume(num);
}
}
测试类:
public class StorageTest {
public static void main(String[] args) {
Storage storage = new Storage();
ExecutorService taskSubmit = Executors.newFixedThreadPool(10); //来使用使用上一节我们总结的线程池知识
//给定4个消费者
taskSubmit.submit(new Consumer(storage, 30));
taskSubmit.submit(new Consumer(storage, 10));
taskSubmit.submit(new Consumer(storage, 20));
//给定6个生产者
taskSubmit.submit(new Producer(storage, 70));
taskSubmit.submit(new Producer(storage, 10));
taskSubmit.submit(new Producer(storage, 20));
taskSubmit.submit(new Producer(storage, 10));
taskSubmit.submit(new Producer(storage, 10));
taskSubmit.submit(new Producer(storage, 10));
taskSubmit.shutdown();
}
}
打印结果:
消费操作-->数量:30,库存不足,消费阻塞!------库存:0
生产操作-->数量:10,成功入库~------库存:10
生产操作-->数量:70,成功入库~------库存:80
生产操作-->数量:10,成功入库~------库存:90
生产操作-->数量:10,成功入库~------库存:100
生产操作-->数量:20,超出仓库容量,生产阻塞!------库存:100
消费操作-->数量:10,消费成功~------库存:90
生产操作-->数量:20,成功入库~------库存:110
生产操作-->数量:10,超出仓库容量,生产阻塞!------库存:110
消费操作-->数量:20,消费成功~------库存:90
消费操作-->数量:30,消费成功~------库存:60
生产操作-->数量:10,成功入库~------库存:70
在仓库中,唤醒我们使用的是notify()而没有使用notifyAll(),是因为在这里,如果测试数据设置不当很容易造成死锁(比如一下唤醒了所有的生产进程),因为使用wait和notify有一个缺陷:
逻辑本应该要这样设计的,在produce()操作后,只要唤醒等待同一把锁的消费者进程,在consume()后,唤醒等待同一把锁的生产者进程,而notify()或notifyAll()将生产者和消费者线程都唤醒了。下面的第二种方法可以解决这个问题。
wait和notify在“类消费者和生产者”问题上也很有用,比如,在A类的某个方法中调用了传进来的B对象的一个方法,A类方法的后面代码依赖于刚刚调用的B的返回值,但是B对象的这个方法是一个异步的操作,此时就可以在A方法中调用完B对象的方法后自我阻塞,即调用wait()方法,而在B对象的那个方法中,待异步操作完成后,调用notify(),唤醒处于等待同一锁对象的线程。如下:
A类的某个方法中:
XmppManager xmppManager = notificationService.getXmppManager();
if(xmppManager != null){
if(!xmppManager.isAuthenticated()){
try {
synchronized (xmppManager) {//等待客户端连接认证成功
Log.d(LOGTAG, "wait for authenticated...");
xmppManager.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//运行到此处,说明是认证成功的,有两种可能,一是运行速度很快调用notificationService.getXmppManager()后直接返回了结果,二是B中处理完了调用notify方法
Log.d(LOGTAG, "authenticated already. send SetTagsIQ now...");
B中处理完后:
//客户端连接认证成功后,唤醒拥有xmppManager锁的对象
synchronized (xmppManager) {
xmppManager.notifyAll();
}
(2)await() / signal()
在JDK1.5之引入concurrent包之后,新引入了await()和signal()方法来做同步,功能和wait()和notify()方法相同,可以完全取代,但await()和signal()需要和Lock机制(关于Lock机制前面已总结)结合使用,更加灵活。正如第一种所说,可以通过调用Lock的newCondition()方法依次获取两个条件变量,一个针对仓库空的,一个针对仓库满的条件变量,通过添加变量进行同步控制。
修改仓库类Storage:
/**
* 仓库
*/
public class Storage {
private static final int MAX_SIZE = 100;//仓库的最大容量
private List<Object> data = new ArrayList<Object>();//存储载体
private Lock lock = new ReentrantLock();//可重入锁
private Condition full = lock.newCondition();//仓库满的条件变量
private Condition empty = lock.newCondition();//仓库空时的条件变量
/**
* 生产操作
*/
public void produce(int num){
lock.lock(); //加锁
if(data.size() + num > MAX_SIZE){//如果生产这些产品将超出仓库的最大容量,则生产操作阻塞
System.out.println("生产操作-->数量:" + num + ",超出仓库容量,生产阻塞!------库存:" + data.size());
try {
full.await(); //阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//到这里,表示可以正常生产产品
for(int i = 0; i < num; i++){//生产num个产品
data.add(new Object());
}
System.out.println("生产操作-->数量:" + num + ",成功入库~------库存:" + data.size());
//生产完产品后,唤醒其他等待消费的线程
empty.signalAll();
lock.unlock(); //释放锁
}
/**
* 消费操作
*/
public void consume(int num){
lock.lock(); //加锁
if(data.size() - num < 0){//如果产品数量不足
System.out.println("消费操作-->数量:" + num + ",库存不足,消费阻塞!------库存:" + data.size());
try {
empty.await(); //阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//到这里,表示可以正常消费
for(int i = 0; i < num; i++){//消费num个产品
data.remove(0);
}
System.out.println("消费操作-->数量:" + num + ",消费成功~------库存:" + data.size());
//消费完产品后,唤醒其他等待生产的线程
full.signalAll();
lock.unlock(); //释放锁
}
}
打印结果:
消费操作-->数量:30,库存不足,消费阻塞!------库存:0
消费操作-->数量:10,库存不足,消费阻塞!------库存:0
消费操作-->数量:20,库存不足,消费阻塞!------库存:0
生产操作-->数量:70,成功入库~------库存:70
生产操作-->数量:10,成功入库~------库存:80
生产操作-->数量:10,成功入库~------库存:90
生产操作-->数量:10,成功入库~------库存:100
生产操作-->数量:10,超出仓库容量,生产阻塞!------库存:100
消费操作-->数量:30,消费成功~------库存:70
消费操作-->数量:10,消费成功~------库存:60
消费操作-->数量:20,消费成功~------库存:40
生产操作-->数量:10,成功入库~------库存:50
生产操作-->数量:20,成功入库~------库存:70
使用await和signal后,加锁解锁操作就交给了Lock,不用再使用synchronized同步(具体可看前面总结的同步的实现方法),在produce中满仓后阻塞,生产完后唤醒等待的消费线程,consume中库存不足后阻塞,消费完后唤醒等待的生产者线程,表示可以消费了。
(3)BlockingQueue阻塞队列方式
在上一节关于线程池的总结中,我们看到了要创建一个线程池如ThreadPoolExecutor,需要传入一个任务队列即BlockingQueue,BlockingQueue(接口)用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue。
>ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
>LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
>SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
>PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
BlockingQueue的所有实现类内部都是已经实现了同步的队列,实现的方式采用的是上面介绍的第二种await()/signal() + Lock同步的机制。在生成阻塞队列时,可以指定队列大小。用于阻塞操作的方法主要为:
put()方法:插入一个元素,如果超过容量则自我阻塞,等待唤醒;
take()方法:取走一个元素,如果容量不足了,自我阻塞,等待唤醒;
put和take内部自己实现了await和signal、lock的机制处理,不再需要我们做相应操作。修改Storage代码如下:
public class Storage {
private static final int MAX_SIZE = 100;//仓库的最大容量
private BlockingQueue<Object> data = new LinkedBlockingQueue<Object>(MAX_SIZE); //使用阻塞队列作为存储载体
/**
* 生产操作
*/
public void produce(int num){
if(data.size() == MAX_SIZE){//如果仓库已达最大容量
System.out.println("生产操作-->仓库已达最大容量!");
}
//到这里,表示可以正常生产产品
for(int i = 0; i < num; i++){//生产num个产品
try {
data.put(new Object()); //put内部自动实现了判断,超过最大容量自动阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产操作-->数量:" + num + ",成功入库~------库存:" + data.size());
}
/**
* 消费操作
*/
public void consume(int num){
if(data.size() == 0){//如果产品数量不足
System.out.println("消费操作--库存不足!");
}
//到这里,表示可以正常消费
for(int i = 0; i < num; i++){//消费num个产品
try {
data.take(); //take内部自动判断,消耗后库存是否充足,不足自我阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费操作-->数量:" + num + ",消费成功~------库存:" + data.size());
}
}
打印结果:
消费操作--库存不足!
消费操作--库存不足!
消费操作--库存不足!
生产操作-->数量:70,成功入库~------库存:45
消费操作-->数量:30,消费成功~------库存:45
生产操作-->数量:10,成功入库~------库存:56
生产操作-->数量:20,成功入库~------库存:75
生产操作-->数量:10,成功入库~------库存:85
生产操作-->数量:10,成功入库~------库存:89
消费操作-->数量:10,消费成功~------库存:60
生产操作-->数量:10,成功入库~------库存:70
消费操作-->数量:20,消费成功~------库存:70
可以看到,Storage中produce和consume方法中我们直接通过put和take方法往容器中添加或移除产品即可,没有进行逻辑控制(其实上面两个方法中if都可以去掉,只是为了打印效果才加上的),这是因为BlockingQueue内部已经实现了,不需要我们再次控制。
同时,我们看到打印的库存信息出现了不匹配,这个主要是因为我们的打印语句Systm.out.println()没有被同步导致的,因为同步语句只是在put和take方法内部,而我们打印语句中使用了data这个共享变量。这里因为我们需要看效果,所以才加的打印语句,并不影响我们对BlockingQueue的使用。
因此,在Java中,使用BlockingQueue阻塞队列的方式可以很方便的为我们处理生产者消费则问题,推荐使用。
在我们的编程生涯中,我们自己要去写生产者和消费者问题,多是前面第一种介绍的“类似消费者生产者问题”上。
解决生产者和消费者问题还有管道的方式,即在生产者和消费者之间建立一个管道缓冲区,Java中用PipedInputStream / PipedOutputStream实现,由于这种方式对于传输对象不易封装,因此实用性不高,就不具体介绍了。
37、sleep和wait的区别
sleep是Thread的静态方法,wait是Object的方法。两个方法都会暂停当前线程
(1)sleep使当前线程阻塞,让出CPU,给其他线程执行的机会;如果当前线程拥有锁,不会释放锁,也即“睡着我也要拥有锁”。睡眠时间一到,进入就绪状态,如果当前CPU空闲,才会继续执行。
(2)wait方法调用后,当前线程进入阻塞状态,进入到和该对象(即谁调用了wait()方法,如list.wait())相关的等待池中。,让出CPU,给其他线程执行的机会;当超时间过了或者别的线程调用了notify()或notifyAll()方法时才会唤醒当前等待同一把锁的线程。
(3)wait方法必须要放在同步块中,如syncbronized或Lock同步中。
所以sleep和wait的主要区别是:
sleep:保持锁,睡眠时间到进入就绪状态;
wait:释放锁,等待其他线程的notify操作或超时唤醒。
39、关于hashCode方法
HashMap中是不允许插入重复元素的,如果是插入的同一个元素,会将前面的元素给覆盖掉,那势必在HashMap的put方法里对key值进行了判断,检测其是否是同一个对象。其put源码如下:
public V put(K key, V value) {
if (table == EMPTY_TABLE) { //key的hashCode值放在了table里面
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key); //计算我们传进来的key的hashcode值
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //将传进来的key的hashcode值于HashMap中的table里面存放的hashCode值比较
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
可以看到这里的判断语句 if (e.hash == hash && ((k = e.key) == key || key.equals(k))),里面通过&&逻辑运算符相连,先判断e.hash == hash,即判断传进来的key的hashCode值与table中的已有的hashCode值比较,如果不存在该key值,也就不会再去执行&&后面的equals判断;当已经存在该key值时,再调用equals方法再次确定两个key值对象是否相同。从这里可以看出,hashCode方法的存在是为了减少equals方法的调用次数,从而提高程序效率。
可以看到,判断两个对象是否相同,还是要取决于equals方法,而两个对象的hashCode值是否相等是两个对象是否相同的必要条件。所以有以下结论:
(1)如果两个对象的hashCode值不等,根据必要条件理论,那么这两个对象一定不是同一个对象,即他们的equals方法一定要返回false;
(2)如果两个对象的hashCode值相等,这两个对象也不一定是同一个对象,即他们的equals方法返回值不确定;
反过来,
(1)如果equals方法返回true,即是同一个对象,它们的hashCode值一定相等;
(2)如果equals方法返回false,hashCode值也不一定不相等,即是不确定的;
(hashCode返回的值一般是对象的存储地址或者与对象存储地址相关联的hash散列值)
然而,很多时候我们可能会重写equals方法,来判断这两个对象是否相等,此时,为了保证满足上面的结论,即满足hashCode值相等是equals返回true的必要条件,我们也需要重写hashCode方法,以保证判断两个对象的逻辑一致(所谓的逻辑一致,是指equals和hashCode方法都是用来判断对象是否相等)。
如下例子:
public class Person {
private String name;
private int age;
public Person(String name,int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object obj) {
return this.name.equals(((Person)obj).name) && this.age== ((Person)obj).age;
}
}
在Person里面重写了equals方法,但是没有重写hashCode方法,如果就我们平时正常来使用的话也不会出什么问题,如:
Person p1 = new Person("lly",18);
Person p2 = new Person("lly",18);
System.out.println(p1.equals(p2)); //返回true
上面是按照了我们重写的equals方法,返回了我们想要的值。但是当我们使用HashMap来保存Person对象的时候就会出问题了,如下:
Person p1 = new Person("lly", 18);
System.out.println(p1.hashCode());
HashMap<Person, Integer> hashMap = new HashMap<Person, Integer>();
hashMap.put(p1, 1);
System.out.println(hashMap.get(new Person("lly", 18))); //此时返回了null,没有按我们的意愿返回1
这是因为,我们没有重写Person的hashCode方法,使hashCode方法与我们equals方法的逻辑功能一致,此时的Person对象调用的hashCode方法还是父类的默认实现,即返回的是和对象内存地址相关的int值,这个时候,p1对象和new Person("lly",18);对象因为内存地址不一致,所以其hashCode返回值也是不同的。故HashMap会认为这是两个不同的key,故返回null。
所以,我们想要正确的结果,只需要重写hashCode方法,让equals方法和hashCode方法始终在逻辑上保持一致性。
在《Java编程思想》一书中的P495页有如下的一段话:
“设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该产生同样的值。如果在将一个对象用put()添加进HashMap时产生一个hashCdoe值,而用get()取出时却产生了另一个hashCode值,那么就无法获取该对象了。所以如果你的hashCode方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,hashCode()方法就会生成一个不同的散列码”。
如下一个例子:
public class Person {
private String name;
private int age;
public Person(String name,int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int hashCode() {
return name.hashCode()*37+age; //hashCode的返回值依赖于对象中的易变数据
}
@Override
public boolean equals(Object obj) {
return this.name.equals(((Person)obj).name) && this.age== ((Person)obj).age;
}
}
此时我们继续测试:
Person p1 = new Person("lly", 18);
System.out.println(p1.hashCode());
HashMap<Person, Integer> hashMap = new HashMap<Person, Integer>();
hashMap.put(p1, 1);
p1.setAge(13);//改变依赖的一个值
System.out.println(hashMap.get(p1)); //此时还是返回为null,这是因为我们p1的hashCode值已经改变了
所以,在设计hashCode方法和equals方法的时候,如果对象中的数据易变,则最好在hashCode方法中不要依赖于该字段。
40、Override和Overload的区别
Override(重写):
在子类中定义与父类具有完全相同的名称和参数的方法,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了,是子类与父类之间多态性的一种体现。特点如下:
- 子类方法的访问权限只能比父类的更大,不能更小(可以相同);
- 如果父类的方法是private类型,那么,子类则不存在覆盖的限制,相当于子类中增加了一个全新的方法;
- 子类覆盖的方法所抛出的异常必须和父类被覆盖方法的所抛出的异常一致,或者是其子类;即子类的异常要少于父类被覆盖方法的异常;
Overload(重载):
同一个类中可以有多个名称相同的方法,但方法的参数个数和参数类型或者参数顺序不同;
关于重载函数返回类型能否不一样,需分情况:
- 如果几个Overloaded的方法的参数列表不一样(个数或类型),它们的返回者类型当然也可以不一样;
- 两个方法的参数列表完全一样,则不能通过让其返回类型的不同来实现重载。
- 不同的参数顺序也是可以实现重载的;
如下:
public String getName(String str,int i){
return null;
}
public String getName(int i,String str){
return null;
}
我们可以用反证法来说明这个问题,因为我们有时候调用一个方法时也可以不定义返回结果变量,即不要关心其返回结果,例如,我们调用map.remove(key)方法时,虽然remove方法有返回值,但是我们通常都不会定义接收返回结果的变量,这时候假设该类中有两个名称和参数列表完全相同的方法,仅仅是返回类型不同,java就无法确定编程者倒底是想调用哪个方法了,因为它无法通过返回结果类型来判断。
所以,Overloaded重载的方法是可以改变返回值的类型;只能通过不同的参数个数、不同的参数类型、不同的参数顺序来实现重载。
41、ArrayList、Vector、LinkedList区别
ArrayList、Vector、LinkedList都实现了List接口,其关系图如下:

三者都可以添加null元素对象,如下示例:
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add(null);
arrayList.add(null);
System.out.println(arrayList.size()); //输出为2
LinkedList<String> linkedList = new LinkedList<String>();
linkedList.add(null);
Vector<String> vectorList = new Vector<String>();
vectorList.add(null);
ArrayList和Vector相同点:
ArrayList和Vector两者在功能上基本完全相同,其底层都是通过new出的Object[]数组实现。所以当我们能够预估到数组大小的时候,我们可以指定数组初始化的大小,这样可以减少后期动态扩充数组大小带来的消耗。如下:
ArrayList<String> list= new ArrayList<String>(20);
Vector<String> list2 = new Vector<String>(15);
由于这两者的数据结构为数组,所以在获取数据方面即get()的时候比较高效,而在add()插入或者remove()的时候,由于需要移动元素,效率相对不高。(其实对于我们平常使用来说,由于一般使用add(String element)都是让其加在数组末尾,所以并不需要移动元素,效率还是很好的,如果使用add(int index, String element)指定了插入位置,此时就需要移动元素了。)
ArrayList和Vector区别:
ArrayList的所有方法都不是同步的,而Vector的大部分方法都加了synchronized同步,所以,就线程安全来说,ArrayList不是线程安全的,而Vector是线程安全的,也因此Vector效率方面相较ArrayList就会更低,所以如果我们本身程序就是安全的,ArrayList是更好的选择。
大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。
LinkedList:
LinkedList其底层是通过双向循环链表实现的,所以在大量增加或删除元素时(即add和remove操作),由于不需要移动元素有更好的性能,但是在获取数据(get操作)方面要差。
所以,在三者的使用选择上,LinkedList适合于有大量的增加/删除操作和较少随机读取操作,ArrayList适合于大规模随机读取数据,而较少插入和删除元素情景下使用,Vector在要求线程安全的情况下使用。
42、String、StringBuffer、StringBuilder区别
String(since JDK1.0):
字符串常量,不可更改,因为其内部定义的是一个final类型的数组来保存值的,如下:
private final char value[];
所以,当我们每次去“更改”String变量的值的时候(包括重新赋值或者使用String内部的一些方法),其实是重新新建了一个String对象(new String)来保存新的值,然后让我们的变量指向新的对象。因此,当我们需要频繁改变字符串的时候,使用String会带来较大的开销。
定义String的方法有两种:
(1)String str = "abc";
(2)String str2 = new String("def");
第一种方式创建的String对象“abc”是存放在字符串常量池中,创建过程是,首先在字符串常量池中查找有没有"abc"对象,如果有则将str直接指向它,如果没有就在字符串常量池中创建出来“abc”,然后在将str指向它。当有另一个String变量被赋值为abc时,直接将字符串常量池中的地址给它。如下:
<span style="white-space:pre"> </span>String a = "abc";
String b = "abc";
System.out.println(a == b); //打印 true
也就是说通过第一种方式创建的字符串在字符串常量池中,是可共享的。同时,也是不可更改的,体现在:
<span style="white-space:pre"> </span>String a = "abc";
String b = "abc";
b = b + "def";
此时,字符串常量池中存在了两个对象“abc”和“abcdef”。
第二种创建方式其实分为两步:
String s = "def";
String str2 = new String(s);
第一步就是上面的第一种情况;第二步在堆内存中new出一个String对象,将str2指向该堆内存地址,新new出的String对象内容,是在字符串常量池中找到的或创建出“def”对象,相当于此时存在两份“def”对象拷贝,一份存在字符串常量池中,一份被堆内存的String对象私有化管理着。所以使用String str2 = new String("def");这种方式创建对象,实际上创建了两个对象。
StringBuffer(since JDK1.0)和StringBuilder(since JDK1.5):
StringBuffer和StringBuilder在功能上基本完全相同,它们都继承自AbstractStringBuilder,而AbstractStringBuilder是使用非final修饰的字符数组实现的,如:char[] value; ,所以,可以对StringBuffer和StringBuilder对象进行改变,每次改变还是再原来的对象上发生的,不会重新new出新的StringBuffer或StringBuilder对象来。所以,当我们需要频繁修改字符串内容的时候,使用StringBuffer和StringBuilder是很好地选择。
两者的核心操作都是append和insert,append是直接在字符串的末尾追加,而insert(int index,String str)是在指定位置出插入字符串。 StringBuffer和StringBuilder的最主要区别就是线程安全方面,由于在StringBuffer内大部分方法都添加了synchronized同步,所以StringBuffer是线程安全的,而StringBuilder不是线程安全的。因此,当我们处于多线程的环境下时,我们需要使用StringBuffer,如果我们的程序是线程安全的使用StringBuilder在性能上就会更优一点。
三者的效率比较:
如上所述,
(1)当我们需要频繁的对字符串进行更改的时候,使用 StringBuffer或StringBuilder是优先选择,对于 StringBuffer和StringBuilder来说,只要程序是线程安全的,我们尽量使用StringBuilder来处理,要求线程安全的话只能使用StringBuffer。平常情况下使用字符串(不常更改字符串内容),String可以满足需求。
(2)有一种情况下使用String和 StringBuffer或StringBuilder的效率是差不多的,如下:
<span style="white-space:pre"> </span>String a = "abc" + "def"; //速度很快
StringBuilder sb = new StringBuilder();
sb.append("abc").append("def");
对于第一条语句,Java在编译的时候直接把a编译成 a = "abcdef",但是当我们拼接的字符串是其他已定义的字符串对象时,就不会自动编译了,如下:
<span style="white-space:pre"> </span>String a = "abc";
String b = "def";
String c = a + b;
根据String源码中的解释,这种情况下是使用concatenation操作符(+),内部是新创建StringBuffer或StringBuilder对象,利用其append方法进行字符串追加,然后利用toString方法返回String串。所以此时的效率也是不高的。
44、HashMap、HashTable、ConcurrentHashMap的区别
HashMap和HashTable都实现了Map接口,里面存放的元素不保证有序,并且不存在相同元素;
区别(线程安全和保存值是否为null方面):
(1) HashMap和HashTable在功能上基本相同,但HashMap是线程不安全的,HashTable是线程安全的;
HashMap的put源码如下:
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); //说明key和value值都是可以为null int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
(2)HashMap的key和value都是可以为null的,当get()方法返回null值时,HashMap中可能存在某个key,只不过该key值对应的value为null,也有可能是HashM中不存在该key,所以不能使用get()==null来判断是否存在某个key值,对于HashMap和HashTable,提供了containsKey()方法来判断是否存在某个key。
HashTable的put源码如下:
public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { //当value==null的时候,会抛出异常 throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry tab[] = table; int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { V old = e.value; e.value = value; return old; } } modCount++; if (count >= threshold) { // Rehash the table if the threshold is exceeded rehash(); tab = table; hash = hash(key); index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. Entry<K,V> e = tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; return null; }
(3)HashTable是不允许key和value为null的。HashTable中的方法大部分是同步的,因此HashTable是线程安全的。
拓展:
(1) 影响HashMap(或HashTable)性能的两个因素:初始容量和load factor;
HashMap中有如下描述: When the number of entries in the hash table exceeds the product of the load factor and the current capacity,
the hash table is <i>rehashed</i> (that is, internal data structures are rebuilt)
当我们Hash表中数据记录的大小超过当前容量,Hash表会进行rehash操作,其实就是自动扩容,这种操作一般会比较耗时。所以当我们能够预估Hash表大小时,在初始化的时候就尽量指定初始容量,避免中途Hash表重新扩容操作,如:
HashMap<String, Integer> map = new HashMap<String, Integer>(20);
(类似可以指定容量的还有ArrayList、Vector)
(2)使用选择上,当我们需要保证线程安全,HashTable优先选择。当我们程序本身就是线程安全的,HashMap是优先选择。
其实HashTable也只是保证在数据结构层面上的同步,对于整个程序还是需要进行多线程并发控制;在JDK后期版本中,对于HashMap,可以通过Collections获得同步的HashMap;如下:
Map m = Collections.synchronizedMap(new HashMap(...));
这种方式获得了具有同步能力的HashMap。
(3)在JDK1.5以后,出现了ConcurrentHashMap,它可以很好地解决在并发程序中使用HashMap的问题,ConcurrentHashMap和HashTable功能很像,不允许为null的key或value,但它不是通过给方法加synchronized方法进行并发控制的。
在ConcurrentHashMap中使用分段锁技术Segment,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。效率也比HashTable好的多。
45、TreeMap、HashMap、LinkedHashMap的区别
关于Map集合,前面几篇都有讲过,可以去回顾一下。而TreeMap、HashMap、LinkedHashMap都是Map的一些具体实现类,其关系图如下:
其中,HashMap和HashTable主要区别在线程安全方面和存储null值方面。HashMap前面讨论的已经比较多了,下面说说LinkedHashMap和TreeMap。
(1)LinkedHashMap保存了数据的插入顺序,底层是通过一个双链表的数据结构来维持这个插入顺序的。key和value都可以为null;
(2)TreeMap实现了SortMap接口,它保存的记录是根据键值key排序,默认是按key升序排列。也可以指定排序的Comparator。
HashMap、LinkedHashMap和TreeMap都是线程不安全的,HashTable是线程安全的。提供两种遍历Map的方法如下:
(1)推荐方式:
Map<String, Integer> map = new HashMap<String, Integer>(20);
for(Map.Entry<String, Integer> entry : map.entrySet()){ //直接遍历出Entry
System.out.println("key-->"+entry.getKey()+",value-->"+m.get(entry.getValue()));
}
这种方式相当于首先通过Set<Map.Entry<String,Integer>> set = map.entrySet();方式拿到Set集合,而Set集合是可以通过foreach的方式遍历的。
(2) 普通方式:
Map<String, Integer> map = new HashMap<String, Integer>(20);
Iterator<String> keySet = map.keySet().iterator(); //遍历Hash表中的key值集合,通过key获取value
while(keySet .hasNext()){
Object key = keySet .next();
System.out.println("key-->"+key+",value-->"+m.get(key));
}
46、Collection包结构,与Collections的区别。
Collection的包结构如下:
Statck类为Vector的子类。由于Collection类继承Iterable类,所以,所有Collection的实现类都可以通过foreach的方式进行遍历。
Collections是针对集合类的一个帮助类。提供了一系列静态方法实现对各种集合的搜索、排序、线程完全化等操作。
当于对Array进行类似操作的类——Arrays。
如,Collections.max(Collection coll); 取coll中最大的元素。
Collections.sort(List list); 对list中元素排序
47、OOM你遇到过哪些情况,SOF你遇到过哪些情况
OOM:OutOfMemoryError异常,
即内存溢出,是指程序在申请内存时,没有足够的空间供其使用,出现了Out Of Memory,也就是要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
内存溢出分为上溢和下溢,比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。
有时候内存泄露会导致内存溢出,所谓内存泄露(memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,举个例子,就是说系统的篮子(内存)是有限的,而你申请了一个篮子,拿到之后没有归还(忘记还了或是丢了),于是造成一次内存泄漏。在你需要用篮子的时候,又去申请,如此反复,最终系统的篮子无法满足你的需求,最终会由内存泄漏造成内存溢出。
遇到的OOM:
(1)Java Heap 溢出
Java堆用于存储对象实例,我们只要不断的创建对象,而又没有及时回收这些对象(即内存泄漏),就会在对象数量达到最大堆容量限制后产生内存溢出异常。
(2)方法区溢出
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
异常信息:java.lang.OutOfMemoryError:PermGen space
方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。
SOF:StackOverflow(堆栈溢出)
当应用程序递归太深而发生堆栈溢出时,抛出该错误。因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。
栈溢出的原因:
(1)递归调用
(2)大量循环或死循环
(3)全局变量是否过多
(4)数组、List、Map数据过大
OOM在Android开发中出现比较多:
场景有: 加载的图片太多或图片过大时、分配特大的数组、内存相应资源过多没有来不及释放等。
解决方法:
(1)在内存引用上做处理
软引用是主要用于内存敏感的高速缓存。在jvm报告内存不足之前会清除所有的软引用,这样以来gc就有可能收集软可及的对象,可能解决内存吃紧问题,避免内存溢出。什么时候会被收集取决于gc的算法和gc运行时可用内存的大小。
(2)对图片做边界压缩,配合软引用使用
(3)显示的调用GC来回收内存,如:
if(bitmapObject.isRecycled()==false) //如果没有回收
bitmapObject.recycle();
(4)优化Dalvik虚拟机的堆内存分配
》增强程序堆内存的处理效率
//在程序onCreate时就可以调用 即可
privatefinalstaticfloat TARGET_HEAP_UTILIZATION = 0.75f;
VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);
》设置堆内存的大小
privatefinalstaticintCWJ_HEAP_SIZE = 6* 1024* 1024;
//设置最小heap内存为6MB大小
VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);
(5)用LruCache 和 AsyncTask<>解决
从cache中去取Bitmap,如果取到Bitmap,就直接把这个Bitmap设置到ImageView上面。
如果缓存中不存在,那么启动一个task去加载(可能从文件来,也可能从网络)。
48、Java面向对象的三个特征与含义,多态的实现方式
Java中两个非常重要的概念:类和对象。类可以看做是一个模板,描述了一类对象的属性和行为;而对象是类的一个具体实现。Java面向对象的三大基本特征:
封装
属性用来描述同一类事物的特征,行为用来描述同一类事物可做的一些操作。封装就是把属于同一类事物的共性(属性和行为)归到一个类中,只保留有限的接口和方法与外部进行交互,避免了外界对对象内部属性的破坏。Java中使用访问控制符来保护对类、属性、方法的访问。
继承
子类通过这种方式来接收父类所有的非private的属性和方法(构造方法除外)。这里的接收是直接拥有的意思,即可以直接使用父类字段和方法,因此,继承相当于“扩展”,子类在拥有了父类的属性和特征后,可以专心实现自己特有的功能。
(构造方法不能被继承,因为在创建子类时,会先去自动“调用”父类的构造方法,如果真的需要子类构造函数特殊的形式,子类直接修改或重载自己的构造函数就好了。)
多态
多态是程序在运行的过程中,同一种类型在不同的条件下表现不同的结果。比如:
Animal a = new Dog(); // 子类对象当做父类对象来使用,运行时,根据对象的实际类型去找子类覆盖之后的方法
多态实现方式:
(1)设计时多态,通过方法的重载实现多态;
(2)运行时多态,通过重写父类或接口的方法实现运行时多态;
49、interface与abstract类的区别
abstract class 只能被继承extends,体现的是一种继承关系,而根据继承的特征,有继承关系的子类和父类应该是一种“is-a”的关系,也即两者在本质上应该是相同的(有共同的属性特征)。
interface 是用来实现的implements,它并不要求实现者和interface之间在本质上相同,是一种“like-a”的关系,interface只是定义了一系列的约定而已(实现者表示愿意遵守这些约定)。所以一个类可以去实现多个interface(即该类遵守了多种约定)。
很多情况下interface和abstract都能满足我们要求,在我们选择用abstract火interface的时候,尽量符合上面的要求,即如果两者间本质是一样的,是一种“is-a”的关系,尽量用abstract,当两者之间本质不同只是简单的约定行为的话,可以选择interface。
特点:
(1)abstract类其实和普通类一样,拥有有自己的数据成员和方法,只不过abstract类里面可以定义抽象abstract的方法(声明为abstract的类也可以不定义abstract的方法,直接当做普通类使用,但是这样就失去了抽象类的意义)。
(2)一个类中声明了abstract的方法,该类必须声明为abstract类。
(3)interface中只能定义常量和抽象方法。在接口中,我们定义的变量默认为public static final类型,所以不可以在显示类中修改interface中的变量;定义的方法默认为public abstract,其中abstract可以不明确写出。
50、static class 与non static class的区别
static class--静态内部类,non static class--非静态内部类,即普通内部类
普通内部类:
内部类可以直接使用外部类的所有变量(包括private、静态变量、普通变量),这也是内部类的主要优点(不用生成外部类对象而直接使用外部类变量)。如下例子:
public class OutClass {
private String mName = "lly";
static int mAge = 12;
class InnerClass{
String name;
int age;
private void getName(){
name = mName;
age = mAge;
System.out.println("name="+name+",age="+age);
}
}
public static void main(String[] args) {
//第一种初始化内部类方法
OutClass.InnerClass innerClass = new OutClass().new InnerClass();
innerClass.getName();
//第二种初始化内部类方法
OutClass out = new OutClass();
InnerClass in = out.new InnerClass();
in.getName();
}
}
输出:name=lly,age=12
可以看到,内部类里面可以直接访问外部类的静态和非静态变量,包括private变量。在内部类中,我们也可以通过外部类.this.变量名的方式访问外部类变量,如:name = OutClass.this.mName;
内部类的初始化依赖于外部类,只有外部类初始化出来了,内部类才能够初始化。
私有内部类(包括私有静态内部类和私有非静态内部类):
如果一个内部类只希望被外部类中的方法操作,那只要给该内部类加上private修饰,声明为private 的内部类只能在外部类中使用,不能在别的类中new出来。如上private class InnerClass{...},此时只能在OutClass类中使用。
静态内部类:
静态内部类只能访问外部类中的静态变量。如下:
public class OutClass { private String mName = "lly"; static int mAge = 12; static class StaticClass{ String name = "lly2"; int age; private void getName(){ // name = mName; //不能引用外部类的非静态成员变量 age = mAge; System.out.println("name="+name+",age="+age); } } public static void main(String[] args) { //第一种初始化静态内部类方法 OutClass.StaticClass staticClass = new OutClass.StaticClass(); staticClass.getName(); //或者直接使用静态内部类初始化 StaticClass staticClass2 = new StaticClass(); staticClass2.getName(); } }
输出:name=lly2,age=12
可以看到,静态内部类只能访问外部类中的静态变量,静态内部类的初始化不依赖于外部类,由于是static,类似于方法使用,OutClass.StaticClass是一个整体。
匿名内部类:
匿名内部类主要是针对抽象类和接口的具体实现。在Android的监听事件中用的很多。如:
textView.setOnClickListener(new View.OnClickListener(){ //OnClickListener为一个接口interface public void onClick(View v){ ... } });
对于抽象类:
public abstract class Animal { public abstract void getColor(); } public class Dog{ public static void main(String[] args) { Animal dog = new Animal() { @Override public void getColor() { System.out.println("黑色"); } }; dog.getColor(); } }
输出:黑色
对于接口类似,只需要把abstract class 改为interface即可。
【参考文章:
http://www.codeceo.com/article/threadlocal-usage.html
http://www.codeceo.com/article/java-multi-thread-sync.html
http://outofmemory.cn/java/java.util.concurrent/synchronized-locks-Lock-ReentrantLock
http://outofmemory.cn/java/java.util.concurrent/thread-sync-with-object-wait-notify-notifyAll
http://outofmemory.cn/java/java.util.concurrent/lock-reentrantlock-condition
http://uule.iteye.com/blog/1488356
http://langgufu.iteye.com/blog/2152608
http://itfish.net/article/22032.html https://blog.csdn.net/shakespeare001/article/details/51321498
】
浙公网安备 33010602011771号