一、java基础
JVM,JRE,JDK 的区别
JVM:英文名称(Java Virtual Machine)Java 虚拟机,只能够运行.class文件,将class文件中的字节码指令进行识别,并调用操作系统向上的API完成动作。
JRE:英文名称(Java Runtime Environment),java运行时环境,主要包含两个部分JVM和java的标准的类库。
JDK:英文名称(Java Development Kit),java开发工具包,是java开发的核心,它和JRE比较起来多了一些好用的小工具,如javac.exe,java.exe等;
一个字节有等于几比特位;
1byte = 8bit
基本数据类型分别占有几个字节
javac和java的作用
javac编译一个java文件,java运行一个java程序。
比如说我写了一个Server.java文件,先用javac Server.java将此java文件编译为Server.class。然后java Server运行class文件(直接类名就可以,不加后缀)。
“ == ” 和 equals的区别是什么
== 一般用于基本数据类型的比较,比较的是两者的值是否相等;当用于比较引用数据类型的时候会比较内存地址是否相等。
equals如果不重写的话同样比较的是hashCode值是否相等。重写之后一般比较的是对象内部的属性的值是否相等。
两个对象的hashCode()相等,那么两个对象用equals比较时也会返回为true。这种说法对吗?
不对,两个对象的hashCode()相等,equals方法不一定为true,equals是自定义的,不一定要相等。
两个对象使用Equals比较返回true,则两个对象的hashcode值也应该相等。
以上说法正确。因为在java中规定重写equals方法时请必须重写hashcode,以保证equals方法相等时两个对象hashcode返回相同的值。
final,finally,finalize 分别是什么意思?
final是java中关键字
用于修饰类时本类是最终类,不可以被继承。
修饰方法时,方法不能被重写。
修饰成员变量时,成员变量必须有初始值且不能更改。
修饰局部变量时此变量声明时可以没有初始值,但是此局部变量只能更改一次。
修饰方法参数是,此参数同样只能被赋值一次(即调用方法时传入的实参);
finally配合try使用;
是用于包含最终执行的代码块(比如释放一些资源)。
finalize();
是定义在Object类中的一个方法,也就是说所有的类都有这个方法,这个方法在gc启动,该对象被回收的时候被调用。但是一般不推荐使用。
try catch finally
来看以下的例子
public static void main(String[] args) {
System.out.println("myMethod() = " + myMethod(0));
}
private static Integer myMethod(Integer a) {
// finally中有return的话就不会执行try catch 中的return。
try {
int i = 10 / a;
System.out.println("try执行了");
return i;
} catch (Exception e) {
System.out.println("catch执行了");
return a;
} finally {
System.out.println("finally执行了");
return 99999;
}
}
// 不管参数中传什么都是以下执行结果
// try执行了(或者是 catch执行了)
// finally执行了
// myMethod() = 99999
例子2
public class ThreadCommunication {
public static void main(String[] args) {
System.out.println("m() = " + m());
}
public static int m() {
Integer ss = 0;
try {
ss = 1 / 0;
return ss += 1;
} catch (Exception e) {
return ss += 3;
} finally {
return ss;
}
}
}
那思考下,finally中的代码一定会执行吗?
答案是不一定,比如以下
public static void main(String[] args) {
System.out.println("myMethod() = " + myMethod(0));
}
private static Integer myMethod(Integer a) {
// finally中有return的话就不会执行try catch 中的return。
try {
int i = 10 / a;
System.out.println("try执行了");
return i;
} catch (Exception e) {
System.out.println("catch执行了");
System.exit(0); // 终止Java 虚拟机的运行
// 或者是当一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed),与其相对应的 finally 语句块可能不会执行。
return a;
} finally {
System.out.println("finally执行了");
return 99999;
}
}
Math.round(11.5)和Math.round(-11.5)是多少?
前者是12,后者是-11,java中四舍五入的原理就是加0.5取整
11. String,StringBuffer,StringBuilder三者的区别
String是只读字符串,一旦声明不会被更改,就算是重新赋值也会创建一个新的字符串对象或者指向已有字符串的内存地址。而剩下的两个是都可以对原来的字符串对象本身进行更改的,且两者的方法都是相同的。但是StringBuilder是没有被synchronied修饰的,所以是线程不安全的,运行速度会更快的。所以当涉及到字符串频繁的被更改时,最好不要使用String,根据线程是否安全选择后两者。
String str = new String(“aaa”);和String str = “aaa”;一样吗?
不一样,前者是将对象放在了堆内存中,后者是将字符串放在了常量池中。
字符串方法调用时值的传递
public class Test {
// 值的传递 面试题
String str = "word";
char[] arr = new char[]{'h', 'e', 'l', 'l', 'o'};
public static void main(String[] args) {
ThreadCommunication t = new ThreadCommunication();
System.out.println("before change ----> t.str.hashCode() = " + t.str.hashCode());
System.out.println("before change ----> t.arr.hashCode() = " + t.arr.hashCode());
t.change(t.str, t.arr);
System.out.println("after change ----> t.str.hashCode() = " + t.str.hashCode());
System.out.println("after change ----> t.arr.hashCode() = " + t.arr.hashCode());
System.out.println(t.str);
System.out.println(t.arr);
//before change ----> t.str.hashCode() = 3655434
//before change ----> t.arr.hashCode() = 666988784
//change方法中更改参数s值之前的hashCode() = 3655434
//change方法中更改参数s值之后的hashCode() = 108212
//------------------------------------------------------------------------
//change方法中更改参数chars值之前的hashCode() = 666988784
//change方法中更改参数chars值之后的hashCode() = 666988784
//------------------------------------------------------------------------
//change方法中更改参数chars值之前的hashCode() = 666988784
//change方法中更改参数chars值之后的hashCode() = 1414644648
//after change ----> t.str.hashCode() = 3655434
//after change ----> t.arr.hashCode() = 666988784
//word
//Hello
}
public void change(String s, char[] chars) {
// 因为字符创本身时不可变的,所以此时s已经指向新的内存地址, 而入参的t.str内存地址还是指向原来的字符串的内存地址
System.out.println("change方法中更改参数s值之前的hashCode() = " + s.hashCode());
s = "mls";
System.out.println("change方法中更改参数s值之后的hashCode() = " + s.hashCode());
System.out.println("------------------------------------------------------------------------");
// chars的内存地址一直是没有改变的
System.out.println("change方法中更改参数chars值之前的hashCode() = " + chars.hashCode());
chars[0] = 'H';
System.out.println("change方法中更改参数chars值之后的hashCode() = " + chars.hashCode());
System.out.println("------------------------------------------------------------------------");
// chars的内存地址一直是没有改变的
System.out.println("change方法中更改参数chars值之前的hashCode() = " + chars.hashCode());
chars = new char[]{'h', 'h'};
System.out.println("change方法中更改参数chars值之后的hashCode() = " + chars.hashCode());
}
}
接口和抽象类
(1)接口:
接口使用interface修饰的;
类和接口之间是实现关系;
一个接口可以继承多个接口;
一个类可以实现多个接口;
接口中是没有构造方法的;
接口是不可以被实例化的;
接口中的成员变量都是静态的公开的最终的;
jdk1.8之后接口中只有default修饰的方法和公开的静态的方法是可以有方法体的。
接口中所有的抽象方法方法都是公开的;
一个类实现某个接口的话必须要实现这个接口中所有的所有方法,Java8后有改变;
接口中不允许出现private修饰符
(2)抽象类
抽象类是使用abstract修饰的;
抽象类和普通类之间是继承关系;
抽象类只能继承一个类
一个类只能有继承一个抽象类;
抽象类中是可以有构造方法的;
抽象类是不可以被实例化的;
抽象类中可以有非静态的成员变量,也可以有静态的成员变量;
抽象类中可以有抽象的方法和普通的方法;
抽象类的抽象方法不可以是私有的,因为一旦私有就无法继承,不能继承是不能实现方法的。
一个类继承某个抽象类的话是不需要实现抽象类中所有的抽象方法的,但是如果没有全部实现的话这个类也将是抽象类;
有抽象方法的类一定是抽象类,抽象类不一定有抽象的方法。
short s1=1; s1=s1+1; 有错吗?short s1=1; s1+=1;有错吗?
前者有错,因为1是int类型,在进行计算 s1+1时,s1会自动向上转型转换为int类型,所以运算结果是int类型,所以s1=…进行赋值时,需要强转为short类型。后者没有错误,可以正常编译,因为s1+=1;相当于 s1=(short)s1+1。
基本数据类型和包装数据类型有什么区别
包装数据类型是对象,拥有字段和方法,基本数据类型不是。
包装数据类型是引用的传递,而基本数据类型是值的传递。
包装数据类型每次声明都需要new一个对象,而基本数据类型不需要。
包装数据类型一般用于集合类,比如List、Map
两者的初始值不同,包装数据类型的初始值是null,而基本数据类型的值会有特定的值,比如int 的初始值是0,boolean的初始值是false;
什么时候使用基本数据类型,什么时候使用包装数据类型?
(1). 集合类中存放的数据类型都是引用数据类型,不能使用基本数据类型;
(2). 方法中的局部变量一般使用基本数据类型
(3). RPC 方法的返回值和参数必须使用包装数据类型。
(4). 所有的 POJO 类属性必须使用包装数据类型。
其实最后还是根据情况使用,比如有的地方需要为null的数据,这种情况就不能用基本数据类型了。有的地方不需要为null,需要默认值是0,在这里用基本数据类型就比较合适了。
public static void main(String[] args) { Integer a = 100, b = 100, c = 129, d = 129;System.out.println(a == b);System.out.println(c == d);} 输出的结果是什么?
输出得结果是 true false,原因如下
Integer直接声明的话,如果直接声明的值在-128到127之间,是不会new对象的,是直接引用常量池中的对象,但是如果超过这个范围的话是会new一个新的对象的。
Java中&、|、&&、||详解
java中&是按位与,&&是短路与
&既是位运算符,又是逻辑运算符,&两侧既可以是int类型的数据,又可以是Boolean类型的数据,当&两侧是int类型的数据时,会将两侧的数据转换为二进制数据进行计算。
例如 12 & 5 结果是什么 ? 其中12的二进制是1100, 5的二进制是0101 , 则12&5应该是0100,结果应该是4
&&为短路与, 两侧的数据类型必须是Boolean类型;
&&是短路运算,如果&&左边的表达式是false,右边的表达式会直接被短路掉,不会进行运算,例如当判断一个字符串是否为null且不为"“时,应写为if(str!=null&&str,equals(”")){},&&两边的表达式的位置是不能改变的,而且是绝对不能使用&的,因为如果左边条件不成立,就不能进行下一步的执行,否则会产生空指针异常。
jvm内存划分
堆内存,方法区内存,栈内存,本地方法栈,程序计数器
怎么实现clone一个对象
浅拷贝 实现Cloneable接口,重写clone方法。如果想要实现深拷贝且属性中有引用数据类型,也需要实现Cloneable接口,重写clone方法。
深拷贝 一般使用的就是实现序列化接口,然后重写clone方法,使用输入流输出流的方法实现对象的深拷贝。具体如下:
@Data
@ToString
public class TestDeepClone implements Serializable {
private String p1;
private String p2;
private A a;
@Override
public TestDeepClone clone() {
TestDeepClone copy = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
//将流序列化成对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
copy = (TestDeepClone) ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return copy;
}
@Data
@ToString
public static class A implements Serializable {
String str;
String s;
}
}
cookie和session的区别
(1). cookie是存储到客户端的;session是存储到服务器的,
(2). cookie的最大存储值是3K;session默认情况下数据存储在web服务器上的内存中,受内存大小限制。如果内存中数据过多会影响服务器性能。
(3). cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗考虑到安全应当使用session。
(4). 设置cookie时间可以使cookie过期。但是使用session-destory(),我们将会销毁会话。
(5). session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能考虑到减轻服务器性能方面,应当使用cookie。
(6). 两者最大的区别在于生存周期,一个是IE启动到IE关闭.(浏览器页面一关 ,session就消失了),一个是预先设置的生存周期,或永久的保存于本地的文件。(cookie)
禁用cookie后session还可以用吗,可以用的话,怎么实现
可以用。
我们需要先了解cookie和session的关系,Session的id是依赖于cookie来进行存储的,浏览器关闭id就会失效。但是我们发送请求的时候想办法给服务器不就可以了,所以禁用cookie时候,可以sessionId作为参数传送给服务器,或者是放在请求头中都是可以的。
访问修饰符
修饰符 | 当前类 | 同包 | 子类 | 其他包 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
Math.round(11.5)和Math.round(-11.5)是多少?
前者是12,后者是-11,java中四舍五入的原理就是加0.5取整
用最有效率的方法计算3乘以8
3<<3 左移3位是乘以2的三次方,右移三位是除以2的三次方
构造器不能被重写,但是可以被重载
String 类是final类,不可以被继承
为什么String可以直接被赋值
因为当声明一个字符串时,若字符串常量池中存在此字符串,引用会直接指向此字符串的内存地址,没有的话会创建一个字符串对象,然后指向此字符串的内存地址。
String,StringBuffer,StringBuilder三者的区别
String是只读字符串,一旦声明不会被更改,就算是重新赋值也会创建一个新的字符串对象或者指向已有字符串的内存地址。而剩下的两个是都可以对原来的字符串对象本身进行更改的,且两者的方法都是相同的。但是StringBuilder是没有被synchronied修饰的,所以是线程不安全的,运行速度会更快的。所以当涉及到字符串频繁的被更改时,最好不要使用String,根据线程是否安全选择后两者。
final关键字
可以用来修饰类、方法,成员变量,局部变量,方法参数。
用来修饰类时,该类不能被继承;
用来修饰方法时,方法不能被重写;
用来修饰成员变量时,必须给与初始值否则报错;
用来修饰局部变量时,只能进行一次赋值操作;
用来修饰方法参数时,只能在给方法传入实参时赋值;
注意
用来修饰引用数据类型时,final起到的作用是引用数据类型的变量不能再重新指向新的引用数据类型,但是是可以给其内部的属性进行多次赋值的。
例如
final Map map = new HashMap();
public void myMethod() {
map.put("1", "zs");
map.put("2", "ls");
}
还有
final Count c = new Count(12);
`public void myMethod() {
c.setId(11);
c.setId(12);
}
static关键字
(1). static可以用来修饰成员变量,方法,静态代码块;
public class MuTest {
static String staticString = "staticString";
String string = "string";
static {
System.out.println("这是静态代码块");
}
public static String getMyString11() {
//getMyString();//此行会报错,因为静态方法只能调用静态的成员变量
return "sss";
}
public String getMyString() {
getMyString11();
return "这是普通的String:" + string + ",这是static的String:" + staticString;
}
}
public class MuTest11 {
public static void main(String[] args) {
MuTest.staticString = "changeStatic";
MuTest muTest1 = new MuTest();
muTest1.string = "0000";
String myString1 = muTest1.getMyString();
System.out.println("muTest1:" + myString1);
MuTest.staticString = "changeStaticAgain";
MuTest muTest2 = new MuTest();
muTest2.string = "1111";
String myString2 = muTest2.getMyString();
System.out.println("muTest2:" + myString2);
}
}
public class MuTest11 {
public static void main(String[] args) {
MuTest.staticString = "changeStatic";
MuTest muTest1 = new MuTest();
muTest1.string = "0000";
String myString1 = muTest1.getMyString();
System.out.println("muTest1:" + myString1);
MuTest.staticString = "changeStaticAgain";
MuTest muTest2 = new MuTest();
muTest2.string = "1111";
String myString2 = muTest2.getMyString();
System.out.println("muTest2:" + myString2);
}
}
(2). 当static修饰成员变量时
则证明该类的所有对象所需的该变量的值都是相同的。赋值情况:①直接使用“类名.静态成员变量名”进行赋值,进行赋值之后,此类的所有对象都会使用这个值(前提是不进行再次赋值操作);②直接使用静态成员变量的初始值,只要不进行方式①的赋值操作,所有对象都会使用该默认值。以上简单点说就是静态成员变量只在类初始化阶段进行赋值,且只赋值一次。
static修饰的变量也称为静态变量,静态变量和非静态变量的区别是:静态变量被所有对象共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
(3). 当static用来修饰代码块时
该代码块会在类初次初始化的时候执行一次。
(4). 当类修饰方法时
该方法被称为静态方法,静态方法是不依赖于任何对象的,直接“类名.静态方法名”就可以调用。
注意:被static修饰的方法只能直接调用或者使用被static修饰的方法或者变量。
注意:Java中的static关键字不会影响到变量或者方法的作用域。在Java中能够影响到访问权限的只有private、public、protected(包括包访问权限)这几个关键字。
(5). 面试题
面试题:
public class Test {
public static void main(String[] args) {
Son t = new Son();
System.out.println(t);
//Father static
//Son static
//Father normal
//Father constructor
//Son normal
//Son constructor
//com.mu.test.thread.Son@2c7b84de
}
}
class Son extends Father {
static {
System.out.println("Son static");
}
{
System.out.println("Son normal");
}
public Son() {
System.out.println("Son constructor");
}
}
class Father {
static {
System.out.println("Father static");
}
{
System.out.println("Father normal");
}
public Father() {
System.out.println("Father constructor");
}
}
输出结果是
base static
test static
base constructor
test constructor
首先先执行父类中静态代码块,然后执行子类中的静态代码块,然后执行父类的构造方法,最后执行子类的构造方法。
Class.forName()和ClassLoader
(1). Class.forName()除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。当然还可以指定是否执行静态块。
(2). classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
public static void main(String[] args) {
try {
//获取到MuTest的Class对象,并加载静态代码块
Class muTest = Class.forName("org.MuTest");
//以下两行是获取MuTest的Class对象,不加载静态代码块
ClassLoader loader = ClassLoader.getSystemClassLoader();
loader.loadClass("org.MuTest");
} catch (Exception e) {
e.printStackTrace();
}
}
方法重载和重写的区别
方法重载:发生在本类中,方法名相同,参数不同,返回值和修饰符也可以不同
方法重写:发生在继承过程中,方法名,参数,返回值,以及方法的访问权限必须大于父类的。
jvm加载class文件时的原理机制
jvm中类的装载是由类加载器(Classloader)和他的子类来实现的,java中类加载器是一个重要的java运行时系统组件,它负责在运行时查找和装入类文件中的类。类的加载是把类的class文件中的数据读入到内存中去,通常是创建一个字节数组读入class文件。
GC
GC是垃圾回收的意思,垃圾回收可以有效的防止内存泄漏,有效的使用可以使用的内存
常见的运行时异常
下标越界异常 IndexOutOfBoundsException
sql运行出错异常 SQLException
空指针异常 NullPointerException
类转换异常 ClassCastException
非法参数异常 IllegalArgumentException
数字格式异常 NumberFormatException
方法不存在异常 NoSuchMethodError
指定的类不存在异常 ClassNotFoundException
List Set Map是否都及继承于Collection接口
List和Set继承于Collection接口,它们的顶级接口是Iterable;但是Map不是,Map是顶级接口;
List和Set的区别
List存储数据的特点是有序可以重复,存的时候是什么顺序去的时候就是什么顺序。
Set存储特点:存储的数据是无序不可重复的。
ArrayList LinkedList vector
ArrayList 底层采用的数组存储元素,所以适合查询,不适合增删
LinkedList采用的是双向链表的形式存储数据,增删比较快,查询比较慢
Vector底层和ArrayList相同,但是Vector是线程安全的,效率也就比较低。
ArrayList的扩容机制
/**
*共享空数组实例用于默认大小的空实例。我们
*将其与EMPTY_ELEMENTDATA区分开来,以便知道何时膨胀多少
*添加第一个元素。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
*存储ArrayList元素的数组缓冲区。
* ArrayList的容量是这个数组缓冲区的长度。任何
*空ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
*当添加第一个元素时,
*将扩展为DEFAULT_CAPACITY。
* transient表示不会被序列化
*/
transient Object[] elementData; // non-private to simplify nested class access
//集合长度
private int size;
/**
* 构建一个空数组
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
①由以上源码可以看出,现在当new一个ArrayList时,创建的是一个空的数组对象。但是源码中的注释是“Constructs an empty list with an initial capacity of ten.”我认为是应该是忘改注释了,具体原因如下
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 构造一个初始容量为10的空列表。将指定的元素追加到列表的末尾。
* @param e元素将追加到此列表
* @return <tt>true</tt>(由{@link Collection#add}指定)
*/
public boolean add(E e) {
// 确定内部容量是否足够 不够的话进行扩容的操作
// 参数是add之后集合长度
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 确保明确的大小,如果是第一次添加 就返回默认长度10,如果不是,就返回add之后数组具体长度
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//判断现在集合中的元素是否为空,为空就返回10,也就是集合初始化长度为10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
// protected transient int modCount = 0;
//继承于父类AbstractList中的变量
modCount++;
/*
大概意思是该字段表示list结构上被修改的次数。
结构上的修改指的是那些改变了list的长度大或者使得遍历过程中产生不正确的结果的其它方式。
该字段被Iterator以及ListIterator的实现类所使用,如果该值被意外更改,
Iterator或者ListIterator 将抛出ConcurrentModificationException异常,
这是jdk在面对迭代遍历的时候为了避免不确定性而采取的快速失败原则。
子类对此字段的使用是可选的,如果子类希望支持快速失败,只需要覆盖该字段相关的所有方法即可
单线程调用不能添加删除terator正在遍历的对象,
否则将可能抛出ConcurrentModificationException异常,
如果子类不希望支持快速失败,该字段可以直接忽略。
*/
// overflow-conscious code
//注意!!!!!!!!!!
//第一次添加的时候minCapacity 的大小为10,而elementData.length 的大小为0
//所以第一次添加的时候可以进入判断,执行grow方法
//当第二次到第十次执行到这里的时候minCapacity为2到10,
//判断为false。所以不能执行扩容方法。
if (minCapacity - elementData.length > 0)
//集合扩容方法
//第一次执行此方方法的时候minCapacity的大小是0,
//第二次执行此方法时minCapacity的大小是11
grow(minCapacity);
}
/**
*要分配的数组的最大大小。
*一些虚拟机在数组中保留一些头字。
*尝试分配更大的数组可能导致
* OutOfMemoryError:请求的数组大小超过VM限制
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
//第一次执行此方方法的时候minCapacity的大小是0,
//第二次执行此方法时minCapacity的大小是11
//获取集合的长度,第一次添加集合的长度是0
int oldCapacity = elementData.length;
//新的长度等于旧的长度加上旧的长度右移1位,即旧的长度加上原来长度的一半
//第一次添加时oldCapacity 为0,所以newCapacity 为0
//第二次执行的时候oldCapacity为10,所以newCapacity =15
int newCapacity = oldCapacity + (oldCapacity >> 1);
//第一次添加时newCapacity 为0 ,minCapacity为10,所以可以进入判断 newCapacity=10
//第二次执行newCapacity =15,minCapacity =11,不可以进入判断。newCapacity=1.5
//所以得到的结果是初始化长度是10,每次扩容时是以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的初始化长度是10.并且每次扩容时以1.5倍扩容。每次进行扩容时都会将原来集合中的数据拷贝到扩容后的集合中去。
③grow方法中,如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8。
LinkedList的源码简单分析
(1). add方法
public static void main(String[] args) {
LinkedList<Integer> integers = new LinkedList<>();
integers.add(11);
/**
* // 新增方法的逻辑
* public boolean add(E e) {
* linkLast(e);
* return true;
* }
*
* // 关联到最后一个
* void linkLast(E e) {
* //1. 将数组add之前的最后一个数据进行备份
* final Node<E> l = last;
* //2. 将新增的元素进行封装
* // 注意这里设计很巧妙 我们看一下Node的构造方法 封装元素的时候就将 要add的节点 的 上一个节点 指向add之前的节点了,
* // 所以后续操作我们就只需要将add的节点设置为最后一个节点,然后老的最后一个节点指向新增的节点就可以了
* final Node<E> newNode = new Node<>(l, e, null);
* (
* Node(Node<E> prev, E element, Node<E> next) {
* this.item = element;
* this.next = next;
* this.prev = prev;
* }
* )
*
* // 3. 将数组的最后一个节点指向新的元素
* last = newNode;
* // 4. 判断最后一个节点是否为空
* if (l == null)
* // 如果数组add之前的最后一个数据是null,则证明数组add之前就是空的,那么就将第一个节点也指向新增的这个数据
* first = newNode;
* else
* // 如果数组add之前的最后一个数据不是null,则证明当前数组add之前不是空的,
* // 那么就将数组add之前的最后一个节点的下一个节点指向新的节点
* l.next = newNode;
* // 5. 数组长度递增
* size++;
* // 改变次数递增
* modCount++;
* }
*/
integers.add(13);
integers.add(14);
integers.add(15);
}
(2). remove方法(无参)
public E remove() {
return removeFirst();
}
// remove无参方法调用此方法,当数组为空的时候直接抛出异常
// 如果数组不为空,就调用unlinkFirst方法,这个方法我们最后再进行解析
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
(3). remove方法(有参)
public boolean remove(Object o) {
// 分为null和非null 但是逻辑是一样的
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
// 此for循环之前没有见过,意思就是先给x的值为头结点,
// 如果x!=null才能进循环,执行完逻辑之后如果没有打破循环,
// 将x值赋值为链表下一个值,就这样直到x==null或者逻辑打破循环时才停止
// 意思就是从头到尾遍历链表
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
// 当遍历到的节点是
unlink(x);
return true;
}
}
}
return false;
}
简单手写一个双向链表
package com.mu.common.utils;
import lombok.Data;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.NoSuchElementException;
@Data
public class LinkedList01<E> {
Node<E> first;
Node<E> end;
Integer size = 0;
/**
* 新增方法
*
* @param e
* @return
*/
public boolean add(E e) {
// 1. 每次在链表结尾新增数据肯定是需要重新封装一个新的节点的
// 所以进入到add方法,不管别的,先封装
final Node oEnd = end;
// 上一个节点是老的最后一个节点,下一个节点是null,然后将新节点设置为最后一个节点
Node<E> nNode = new Node<E>(oEnd, e, null);
// 最后一个节点肯定是当前的节点
end = nNode;
// 将老的最后一个节点的下一个节点指向新的最后一个节点
if (first == null) {
first = nNode;
} else {
oEnd.next = nNode;
}
size++;
return true;
}
/**
* 根据下标获取
*
* @param
* @return
*/
public E get(int index) {
if (index >= size) {
throw new IndexOutOfBoundsException("下标越界->" + index);
}
E e = null;
int i = 0;
for (Node n = first; n != null; n = n.next) {
if (i == index) {
e = (E) n.e;
break;
}
i++;
}
return e;
}
/**
* 根据下标移除 在这里参数要用int,只有这样当传值为int和Integer时,此方法和remove(Object o)方法才能分清
*
* @param
* @return
*/
public E remove(int index) {
if (index >= size) {
throw new IndexOutOfBoundsException("下标越界->" + index);
}
E e = null;
int i = 0;
for (Node n = first; n != null; n = n.next) {
if (i == index) {
e = removeNode(n);
break;
}
i++;
}
return e;
}
/**
* 根据下标移除 在这里参数要用int,只有这样当传值为int和Integer时,此方法和remove(Object o)方法才能分清
*
* @param
* @return
*/
public E remove(E ele) {
E e = null;
for (Node n = first; n != null; n = n.next) {
if (n.e.equals(ele)) {
e = removeNode(n);
break;
}
}
return e;
}
/**
* 弹出第一个 弹完之后有异常
*
* @return
*/
public E pop() {
if (size == 0) {
throw new NoSuchElementException();
}
return removeNode(first);
}
/**
* 弹出第一个 弹完之后返回null
*
* @return
*/
public E poll() {
return size == 0 ? null : removeNode(first);
}
public void push(E e) {
addFirst(e);
}
public void addFirst(E e) {
Node<E> f = first;
Node<E> eNode = new Node<>(null, e, f);
first = eNode;
if (f == null) {
end = eNode;
} else {
f.pre = eNode;
}
size++;
}
/**
* 移除指定node并返回
*
* @param n
* @return
*/
private E removeNode(Node n) {
E e = (E) n.e;
// 将n的上一个节点指向n的下一个节点
if (n.pre != null) {
n.pre.next = n.next;
} else {
// 如果n.pre为空,则证明移除的是头节点, 在这里头结点要更换
first = n.next;
}
// 将n的下一个节点的上一个节点指向n的上一个节点
if (n.next != null) {
n.next.pre = n.pre;
} else {
// 如果n.next,则证明移除的是尾节点, 在这里尾结点要更换
end = n.pre;
}
size--;
n.e = null;
return e;
}
/**
* 移除最后一个
*
* @param
* @return
*/
public E remove() {
return unLinkLast();
}
private E unLinkLast() {
if (end == null) {
throw new RuntimeException("集合为空没有可以移除的元素");
}
Node l = end;
end = l.pre;
if (end != null)
end.next = null;
return (E) l.e;
}
@Override
public String toString() {
StringBuffer s = new StringBuffer();
s.append("LinkedList01:[");
for (Node n = first; n != null; n = n.next) {
s.append(n.next == null ? n.toString() + "]" : n.toString() + ",");
}
return s.toString();
}
public static class Node<E> {
public Node<E> pre;
public E e;
public Node<E> next;
public Node(Node<E> pre, E e, Node<E> next) {
this.pre = pre;
this.e = e;
this.next = next;
}
@Override
public String toString() {
return e.toString();
}
}
}
public static void main(String[] args) {
LinkedList01<Integer> list01 = new LinkedList01<>();
list01.add(1);
list01.add(2);
list01.add(3);
list01.add(4);
list01.add(5);
list01.add(6);
System.out.println("list01 = " + list01);
// System.out.println("list01.get(5) = " + list01.get(5));
// System.out.println("list01.remove() 1= " + list01.remove());
// System.out.println("list01.remove() 2= " + list01.remove());
// System.out.println("list01.remove() 3= " + list01.remove());
// System.out.println("list01.remove() 4= " + list01.remove());
// System.out.println("list01.remove() 5= " + list01.remove());
// System.out.println("list01.remove() 6= " + list01.remove());
// System.out.println("list01.remove() 7= " + list01.remove());
// System.out.println("list01.remove(0) = " + list01.remove(0));
// System.out.println("list01.remove(Integer.valueOf(1)) = " + list01.remove((Integer) 2));
// System.out.println("list01.remove(null) = " + list01.remove(0));
// System.out.println("list01.pop() = " + list01.pop());
// System.out.println("list01.pop() = " + list01.pop());
// System.out.println("list01.pop() = " + list01.pop());
// System.out.println("list01.pop() = " + list01.pop());
// System.out.println("list01.pop() = " + list01.pop());
// System.out.println("list01.pop() = " + list01.pop());
// System.out.println("list01.pop() = " + list01.pop());
// System.out.println("list01.poll() = " + list01.poll());
// System.out.println("list01.poll() = " + list01.poll());
// System.out.println("list01.poll() = " + list01.poll());
// System.out.println("list01.poll() = " + list01.poll());
// System.out.println("list01.poll() = " + list01.poll());
// System.out.println("list01.poll() = " + list01.poll());
// System.out.println("list01.poll() = " + list01.poll());
// System.out.println("list01.poll() = " + list01.poll());
list01.push(0);
System.out.println("list01 = " + list01);
}
对HashSet的理解
HashSet的底层就是HashMap,HashSet实际上就是HashMap的key。具体见一下源码:
public HashSet() {
map = new HashMap<>();
}
当在HashSet中添加元素时
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
通过以上代码和注释我们可以了解到当在HashSet中添加数据时,是直接调用map的put方法将key填入,value是添加的一个空的对象常量;然后根据特定的算法计算出插入数据的Hash值,如果Hash表中不存在这个Hash值,就会直接添加该元素,如果存在,会调用插入值的Equals方法,返回false就添加,否则就放弃添加。
对HashMap的理解
HashMap的数据结构是数组和链表的结合体;
HashMap数组的默认长度是16,因为在小数据量的情况下,16相对于来说更能减少key之间的碰撞,加快查询效率,当Map中存入的数据超过16*0.75=12时就会扩容到原来的2倍。
当在HashMap中put元素时,会通过key的值计算出该key在数组中的下标,如果在这个下标所在的位置是没有值的,就直接添加数据,如果有值,就会以链表的形式存放,在jdk1.8之后新添加的元素是放在链尾,且链表的长度超过8之后会以红黑树的形式存储数据。
HashMap是线程安全的吗
HashMap不是线程安全的,HashTable是和ConcurrentHashMap是线程安全的。
HashMap中put方法源码分析
HashMap中treeifyBin(Node<K,V>[] tab, int hash);链表转红黑树方法详解
// TODO
HashMap中putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v);在红黑树中插入数据详解
// TODO
HashMap是怎么解决Hash冲突的
(1). 什么是hash冲突?
hash冲突就是我们用不同key的hashcode值去根据hash(key)这个函数计算出的hash值是相等的,这确认Map集合中下标位置时,就会有冲突的现象
(2). 那在HashMap中是怎么解决的呢?
在HashMap中数据结构是数组+链表,就是当有Hash冲突的时候, 在这个下标位置会以链表的形式进行数据的存储。
HashMap中put时怎么确定元素下标位置的?
(1). 通过hash函数计算出一个散列值
static final int hash(Object key) {
int h;
// 获取k的hashCode值 然后和hashCode向右位移16位的值 进行按位异或的计算,计算出hash值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(2). 通过 (数组的长度-1) &(按位与) (散列hash值) 计算出下标位置
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 数组长度-1 然后和散列值进行按位与的计算
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
为什么HashMap的初始容量必须是2的n次幂?
tab[i = (n - 1) & hash]
(1). 因为通过key计算出的hash值在确定下标时, 会和数组的长度-1 进行按位与的计算,如果不是2的n次幂,计算出的下标在数组中不是散列的,就很容易造成hash冲突,导致HashMap数据结构变得复杂
(2). 使用什么算法把容量大小变为最小的大于输入参数的2的n次幂的
通过右移运算和或运算
static final int MAXIMUM_CAPACITY = 1 << 30;
static final int tableSizeFor(int cap) {
int n = cap - 1;
// 假设 cap = 10
// n = 9
n |= n >>> 1;
// n = 9 二进制 00000000 00001001 (9)
//向右位移一位 00000000 00000100 (4)
//以上两个数9 或 4 00000000 00001101 (13)
// 此时n=13
n |= n >>> 2;
// n = 13 二进制 00000000 00001101 (13)
//向右位移一位 00000000 00000011 (3)
//以上两个数13或3 00000000 00001111 (15)
// 此时n=15
n |= n >>> 4;
// n = 15 二进制 00000000 00001111 (15)
//向右位移一位 00000000 00000000 (0)
//以上两个数15或0 00000000 00001111 (15)
// 此时n=15
n |= n >>> 8;
// n = 15 二进制 00000000 00001111 (15)
//向右位移一位 00000000 00000000 (0)
//以上两个数15或0 00000000 00001111 (15)
// 此时n=15
n |= n >>> 16;
// n = 15 二进制 00000000 00001111 (15)
//向右位移一位 00000000 00000000 (0)
//以上两个数15或0 00000000 00001111 (15)
// 此时n=15
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
jdk1.8以后为什么选择8作为链表转红黑树的边界值
ConcurrentHashMap简单解析
(1). 底层数据结构
同样也是数组+链表+红黑树
(2). 为什么是线程安全的?(简单说明)
底层使用了大量的CAS(Unsafe的compareAndSwap*()方法)操作,而且设计到线程安全的成员变量都使用了volatile修饰,保证当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。
HashMap和HashTable的区别
(1). JDK8以后HashMap底层是链表+数组+红黑树,HashTable底层是链表+数组;
(2). HashMap在new的时候不创建数组,在put的时候创建数组对象;HashTable在new的时候就会创建数组对象
(3). HashMap无参构造初始值是16,HashTable无参构造初始值是11;
(4). HashMap是通过int i = (tab.length - 1) & (h = key.hashCode()) ^ (h >>> 16)计算下标,位运算效率较高; HashTable是通过 int index = (key.hashCode() & 0x7FFFFFFF) % tab.length;计算下标,这样计算效率较低;(其实两者计算结果就是hashCode对数组长度取余)
(5). HashMap线程不安全,HashTable线程安全
RESTful 风格的接口
(1)传统的接口用URL来描述行为,而RESTful风格的接口用URL针对的是资源.传统的接口在URL中定义行为,从路径中我们可以看出这个API是做的什么操作;而RESTful在URL中是没有具体的操作的,在请求的接口后面直接跟的是请求的参数.
(2)传统的接口Http请求方式只用get和Post. RESTful风格的接口是使用Http请求方式来描述行为的,其中get表示查询,delete表示删除,put表示修改,post表示添加.
(3)传统的接口不论调用成功或者是不成功,返回的状态码都可能是200,只是在返回的数据中,有某一个字段可以用来判断是否调用成功,而RESTfulApi是通过Http状态码来表示不同的结果,比如200表示调用成功,400表示调用失败,500表示调用的服务器内部发生异常.
(4)传统的Api可能使用字符串拼接、xml等各种形式进行数据的交换。而在RESTful风格的接口中都是使用json进行数据的交互。
二、多线程
什么是线程
线程是操作系统中能够进行运算调度的最小单位,,它被包含在进程之中,是进程的实际运作单位,可以使用多线程进行运算提速。
程序 进程 线程
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
进程是执行程序的一次执行过程,是一个动态的概念。是系统资源分配的单位。
一个进程可以有多个线程,一个进程中至少有一个线程。线程是CPU调度和执行的单位。
进程和进程之间是相互独立的,数据是不共享的。线程和线程之间的数据是共享的。
真正的多线程是有多个CPU的。在单个CPU的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有执行时的错觉。
创建线程
继承Thread类
实现Runnable接口
实现Callable接口
线程的生命周期
新建:创建线程对象 ,也就是new一个线程对象。
就绪:线程调用start方法。
运行:线程调用run方法。
阻塞:当调用sleep,wait,或者同步锁定时,线程就进入阻塞状态,代码执行到此处会暂停,阻塞事件结束之后,重新进入就绪状态,等待cpu调度执行。
结束(死亡):线程一旦死亡,就不能再次启动。
线程的六大状态
- NEW 线程刚创建
- RUNNABLE 在JVM中正在运行的线程
- BLOCKED 当前线程运行到需要锁对象时,当前锁对象已经被其他线程持有,那么当前线程进入锁阻塞状态!
- WAITING 等待状态,调用wait(),join(),LockSupport.park方法进入不计时等待。
- TIMED_WAITING 调用sleep(timeout) join(timeout) wait(timeout)方法可能导致线程处于等待状态,和WAITING 状态不同的是,此等待是过了指定时间就会自动唤醒,或者在超时前被唤醒信号唤醒。
- TERMINATED 线程执行完毕,已经退出
怎么停止线程
不推荐使用jdk官方提供的stop方法和destory方法【已废弃】
建议让线程运行完毕自己停止。
实际实现方法:
- 在线程中定义线程体(即run方法)的启用标志,
- 在线程体中使用该启用标志,
- 对外提供公开的方法来改变该启用标志的值。
public class ThreadTest {
public static void main(String[] args) {
ThreadStop threadStop = new ThreadStop();
new Thread(threadStop).start();
// 停止以上线程
threadStop.stop();
}
// @Test
// public void threadStop() {
// }
}
class ThreadStop implements Runnable {
private AtomicBoolean flag = new AtomicBoolean(true);
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始执行了");
try {
// 假设这里逻辑需要处理好长时间
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("以上耗时的逻辑执行完毕");
if (flag.get()) { // 此判断根据业务进行处理
System.out.println("可以执行新的逻辑,开始执行");
} else {
System.out.println("不可以执行新的逻辑,结束执行");
}
}
public void stop() {
this.flag.set(false);
}
}
一个java程序至少有几个线程
2个,分别是main线程和垃圾回收的线程。
什么时候会产生线程安全的问题
多个线程在操作共享的数据
操作共享数据的线程体的代码有多条
做个线程会对共享的数据有更改的操作
如下
public class TestThread {
static int ticNum = 10;
public static void main(String[] args) throws InterruptedException {
Runnable r1 = () -> {
while (true) {
if (ticNum <= 0) {
break;
}
try { b
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticNum-- + "张票");
}
};
new Thread(r1,"小米").start();
new Thread(r1,"小红").start();
new Thread(r1,"小明").start();
}
//小明抢到了第10张票
//小米抢到了第8张票
//小红抢到了第9张票
//小米抢到了第7张票
//小红抢到了第7张票
//小明抢到了第7张票
//小米抢到了第6张票
//小明抢到了第5张票
//小红抢到了第4张票
//小米抢到了第3张票
//小明抢到了第2张票
//小红抢到了第1张票
//小米抢到了第0张票
//小明抢到了第-1张票
}
线程同步
在有一个线程对共享的数据进行操作时,其他线程都不可以对这个数据进行操作,必须处于等待的状态。直到线程对此共享的数据操作完成之后,其他线程才可以进行操作。
线程同步机制
(1)同步代码块(synchronized(要同步的对象){})
(2)同步方法(在方法上面添加关键字synchronized)
(3)同步锁(ReenreatLock)
(4)特殊域变量(volatile)
(5)局部变量(ThreadLocal)
(6)阻塞队列(LinkedBlockingQueue)
(7)原子变量(Atomic*)
同步代码块,同步方法,同步锁
同步代码块:写法synchronized(obj){ 逻辑代码},当线程拿到obj这个对象时,代码块中的代码才能够运行
同步方法:在方法上面添加synchronized关键字,当方法不是静态方法时,锁对象就是新创建的实例,当方法时静态方法时,锁对象就是当前类。
同步锁:比以上两种都要强大,更加常用。ReentrantLock 可重入锁(即锁是可以重复使用的),当new一个实例时,可传入Boolean类型的参数,当传入的参数是true时,表示创建的是一个公平锁,多个线程都有公平的权利去执行;false表示是非公平锁,也是默认值。公平锁的优势是可以让业务有更高的可控性。非公平锁的优势是可以有更多的机会去抢占锁,能更加充分的利用CPU。
注意:同步锁使用时lock()方法和unLock()方法时成对出现的。并且unLock()方法时必须要执行的。所以可以将上锁之后的代码使用try finally ,这样就可以保证锁能够释放。
Lock lock = new ReentrantLock(true);
public void run() {
while (true) {
if (ticNum > 0) {
lock.lock();
try {
String threadName = Thread.currentThread().getName();
System.out.println("售票员" + threadName + "卖出了第" + ticNum-- + "张票");
} finally {
lock.unlock();
}
}
}
}
synchronized和Lock的区别
- synchronize是java内置的关键字,Lock则是Java提供的类。
- synchronize无法判断是否获取到锁,Lock可以。
- Lock锁性能更好,并且Lock锁有更多实现的子类,扩展性更好
- synchronize可以自动释放锁,无论是在执行完同步代码块之后和线程执行完成后都可以自动释放锁。而Lock必须需要手动释放锁,否则容易造成死锁。
- 使用synchronize关键字的线程1和线程2,如果线程1获得锁,线程2会等待。如果线程1阻塞,则线程2会一直等待下去。而lock锁就不一定会一直的等待。
- synchronize的锁可重入,不可判断,非公平,而lock锁可重入,可判断,可公平也可非公平。
- synchronize适合少量代码同步的问题,Lock锁适合大量代码的同步问题。
同步方法
非静态的同步方法锁的是当前对象
静态的同步方法所得是当前类
- 非静态同步方法方法问题
public class ThreadTest {
public static void main(String[] args) {
// 获取银行卡
BankCard bankCard = new BankCard("1111111", 1000);
// 银行A
Bank bankA = new Bank();
// 银行B
Bank bankB = new Bank();
// 小明在A银行取钱
Runnable xm = () -> {
bankA.drawMoney(bankCard, 500, "小明");
};
// 小红在B银行取钱
Runnable xh = () -> {
bankB.drawMoney(bankCard, 800, "小红");
};
// 开始去取钱
new Thread(xm).start();
new Thread(xh).start();
// 小明从银行卡1111111取了500元, 余额为-300
// 小红从银行卡1111111取了800元, 余额为-300
}
}
class BankCard {
// 卡号
private String num;
// 余额
private Integer balance;
public BankCard(String num, Integer balance) {
this.num = num;
this.balance = balance;
}
public String getNum() {
return num;
}
public Integer getBalance() {
return balance;
}
public void setBalance(Integer balance) {
this.balance = balance;
}
}
class Bank {
/**
* 取款逻辑
*
* @param bankCard 银行卡
* @param money 取钱数目
* @param name 取款人姓名
*/
public synchronized void drawMoney(BankCard bankCard, int money, String name) {
if (money > bankCard.getBalance()) {
// 取款金额大于余额
throw new RuntimeException("银行卡余额不足");
} else {
try {
// 模拟网络延迟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bankCard.setBalance(bankCard.getBalance() - money);
System.out.println(name + "从银行卡" + bankCard.getNum() + "取了" + money + "元, 余额为" + bankCard.getBalance());
}
}
}
发现在银行的取款方法上面加上了synchronized锁,数据还是异常的, 因为在这个方法上面加锁只是相当于分别在银行A和银行B的取款业务上面加了一个锁,其他人暂时不能在这两个银行进行取款了, 而1234567890这个账户上面并没有加上锁,所以还是会出现异常问题的,所以应该在银行卡上面加锁,这就需要用到同步代码块了;
- 解决非静态同步方法方法问题
public class ThreadTest {
public static void main(String[] args) {
// 获取银行卡
BankCard bankCard = new BankCard("1111111", 1000);
// 银行A
Bank bankA = new Bank();
// 银行B
Bank bankB = new Bank();
// 小明在A银行取钱
Runnable xm = () -> {
synchronized (bankCard) {
bankA.drawMoney(bankCard, 500, "小明");
}
};
// 小红在B银行取钱
Runnable xh = () -> {
synchronized (bankCard) {
bankB.drawMoney(bankCard, 800, "小红");
}
};
// 开始去取钱
new Thread(xm).start();
new Thread(xh).start();
// 小明从银行卡1111111取了500元, 余额为-300
// 小红从银行卡1111111取了800元, 余额为-300
}
}
class BankCard {
// 卡号
private String num;
// 余额
private Integer balance;
public BankCard(String num, Integer balance) {
this.num = num;
this.balance = balance;
}
public String getNum() {
return num;
}
public Integer getBalance() {
return balance;
}
public void setBalance(Integer balance) {
this.balance = balance;
}
}
class Bank {
/**
* 取款逻辑
*
* @param bankCard 银行卡
* @param money 取钱数目
* @param name 取款人姓名
*/
public void drawMoney(BankCard bankCard, int money, String name) {
if (money > bankCard.getBalance()) {
// 取款金额大于余额
throw new RuntimeException("银行卡余额不足");
} else {
try {
// 模拟网络延迟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bankCard.setBalance(bankCard.getBalance() - money);
System.out.println(name + "从银行卡" + bankCard.getNum() + "取了" + money + "元, 余额为" + bankCard.getBalance());
}
}
}
- 那在我们实际的应用层怎么写呢? (在这里我们使用Lock读写锁)
(1).搭建一个web工程,提供取款接口
@Autowired
BankService bankService;
@PostMapping("drawMoney")
public Integer drawMoney(@RequestBody DrawMoneyDTO dto) {
return bankService.drawMoney(dto.getNum(), dto.getPwd(), dto.getMoney());
}
@PostMapping("initData")
public void initData() {
bankService.initAccount();
}
(2). 业务逻辑
@Service
public class BankService {
private static ReadWriteLock lock = new ReentrantReadWriteLock();
static Lock writeLock = lock.writeLock();
static Lock readLock = lock.readLock();
List<Map<String, String>> accounts;
@LogAspect("initAccount")
public void initAccount() {
accounts = new ArrayList(4);
Map<String, String> map = new HashMap(3);
map.put("num", "111111");
map.put("pwd", "111111");
map.put("balance", "1000");
Map<String, String> map1 = new HashMap(3);
map1.put("num", "222222");
map1.put("pwd", "222222");
map1.put("balance", "2000");
Map<String, String> map2 = new HashMap(3);
map2.put("num", "333333");
map2.put("pwd", "333333");
map2.put("balance", "1800");
Map<String, String> map3 = new HashMap(3);
map3.put("num", "444444");
map3.put("pwd", "444444");
map3.put("balance", "1500");
accounts.add(map);
accounts.add(map1);
accounts.add(map2);
accounts.add(map3);
}
@LogAspect("drawMoney")
public Integer drawMoney(String num, String pwd, int money) {
Integer nBalance = null;
try {
writeLock.lock();
// 获取账户信息 这里可能会有线程安全问题
Map<String, String> account = getAccountByNumAndPwd(num, pwd);
if (account == null) {
throw new RuntimeException("密码不正确");
}
// 获取账户余额
int balance = Integer.parseInt(account.get("balance"));
if (money > balance) { // 取款金额大于余额
throw new RuntimeException("no Money");
} else { // 这里是扣减账户余额并设置账户余额
// 模拟网络延迟
sleep(1000);
// 这里可以是更改数据库数据
account.put("balance", "" + (balance - money));
nBalance = Integer.parseInt(account.get("balance"));
System.out.println(Thread.currentThread().getName() + "从银行卡" + account.get("num") + "取了" + money + "元, 余额为" + nBalance);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
writeLock.unlock();
}
return nBalance;
}
private Map getAccountByNumAndPwd(String num, String pwd) {
// 模拟网络延迟
sleep(1000);
for (Map account : accounts) {
if (account.get("num").equals(num) && account.get("pwd").equals(pwd)) {
return account;
}
}
return null;
}
private void sleep(Integer m) {
try {
// 模拟网络延迟
Thread.sleep(m);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
死锁实例
首先要知道的是,同步代码块中要同步
的资源只能是没有被同步的资源
现在有这样一个情景,爸爸手里面有手机,儿子手里面有卷子,爸爸拿到卷子才会给儿子手机,儿子拿到手机后才会给爸爸卷子。现在两人都有对方需要的资源,但是对方都不想让出自己手中的资源,所以这样就造成了死锁。
public class TestThread {
static String paper = "0分的卷子";
static String mobile = "好玩的手机";
public static void main(String[] args) throws InterruptedException {
Runnable dad = () -> {
// 父亲已经锁上了手机,还想要锁卷子
synchronized (mobile) {
System.out.println(Thread.currentThread().getName() + "->让我看看你的卷子,我就给你玩手机");
synchronized (paper) {
System.out.println(Thread.currentThread().getName() + "->0分!!!打屎你个龟孙");
}
}
};
Runnable son = () -> {
// 儿子已经锁上了卷子,还想要锁手机
synchronized (paper) {
System.out.println(Thread.currentThread().getName() + "->让我玩手机, 我就让你看卷子");
synchronized (mobile) {
System.out.println(Thread.currentThread().getName() + "->手机真好玩");
}
}
};
new Thread(dad, "父亲").start();
new Thread(son, "儿子").start();
}
}
产生死锁的必要条件
- 一个资源只能被一个线程使用
- 一个线程因请求的资源获取不到而阻塞时, 对当前已经获得的资源保持不放
- 线程已经获得的资源,在未使用完时,不能强行剥夺
- 若干线程之间形成一种头尾连接的循环等待资源的关系
线程中sleep方法和wait方法的区别
- sleep方法是Thread类中的静态方法,可以使线程暂停指定的时间,目的是不让当前线程霸占CPU资源,让出执行机会给其他线程,但是监控状态依然保持,暂停时间过去后会自动恢复;在一个同步代码块中调用sleep方法不会释放对象锁。在sleep方法休眠结束后线程会不会立即到执行的状态,会先到就绪状态,等待当前线程执行完毕后抢夺CPU的资源,抢夺到了就会执行当前线程。
- sleep方法Thread类的静态方法,在任何地方都是可以使用的。
- sleep 方法可以让线程进入 waiting 状态,并且不占用 cpu 资源,但是不会释放锁,直到规定时间后再执行
- wait方法是Object类中的方法,当一个线程执行到该方法时,线程就会进入到一个和该对象相关的等待池中,同时释放了锁,其他线程可以访问线程之间共享的资源。
- wait方法是和notify方法组合使用的,notify方法是用来唤醒当前等待池中的线程的。
并行并发
并行:在某一个时间点同时发生A事件和B事件
并发:在某一个时间段A事件和B事件发生。
线程池的作用
1.降低资源的消耗,通过重负利用已经创建好的线程降低线程频繁的创建和销毁造成的资源消耗。
2.提高响应的速度。当任务到达时,任务可以直接执行,不需要等待线程的创建。
3.方便线程的批量管理。使用线程池可以对线程进行统一的分配,调优和监控.
4.控制最大并发数
5.实现任务线程队列缓存策略和拒绝机制.(不懂)
6.实现某些和时间相关的功能,例如定时执行,周期执行.
7.能够隔离线程环境,例如:交易服务和搜索服务在同一台服务器上,通过配置独立的线程池,将较慢的交易服务和搜索服务隔离开,避免各服务线程相互影响.
线程池的类之间的关系
Executor:为顶级父接口 ,接口中只有一个方法为
void execute(Runnable command);
ExecutorService继承于Executor,且添加了更多的方法.
ScheduledExecutorService继承于ExecutorService接口,是支持定时任务的
AbstractExecutorService是实现了ExecutorService接口的抽象类
ThreadPoolExecutor继承于AbstractExecutorService抽象类
ScheduledThreadPoolExecutor实现了ScheduledExecutorService接口,继承了ThreadPoolExecutor类的定时任务线程池.
Executors是一个创建线程池的工具类
创建线程池的方法
使用Executors的静态方法创建线程池(不推荐使用)
-
Executors.newFixedThreadPool(10);创建一个最大线程数量为10个线程的线程池,每当提交一个任务就创建一个线程,直到最大数量,这时线程的规模就不会再变化,当线程发生未预期的错误而结束时线程池会补充一个新的线程。
-
Executors.newSingleThreadExecutor(); 创建单个线程来执行任务,如果这个线程异常结束,会创建一个新的线程来替代它,特点是能够确保任务能够在队列中的顺序执行。
-
Executors.newCachedThreadPool();创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新的线程。线程池的规模不收任何限制。
-
Executors.newScheduledThreadPool(1000);创建一个可定时及周期性执行任务的线程池。
直接使用七大参数创建线程池
@Bean(value = "myThreadPoolExecutor")
public ThreadPoolExecutor sas() {
return new ThreadPoolExecutor(
2,// 队列没满时,线程最大并发数
4, // 队列满后线程能够达到的最大并发数
2, // 空闲线程过多久被回收的时间限制
TimeUnit.SECONDS,// keepAliveTime 的时间单位
new LinkedBlockingDeque(10), // 阻塞队列
Executors.defaultThreadFactory(), // 线程工厂:创建线程的,一般 不用动
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
}
corePoolSize,maximumPoolSize,workQueue之间关系。
当线程池中线程数小于corePoolSize时,新提交的任务将创建一个新的线程,即使此时线程池中存在空闲线程
当线程池中线程数达到corePoolSize时,新提交的任务将会存放到workQueue中,等待线程池任务调度执行
当workQueue已满,且maximumPoolSize>corePoolSize时,新提交的任务将会创建新的线程执行任务。
当提交的任务数大于maximumPoolSize+workQueue时,新提交的任务将由RejectedExecutionHandler处理
new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异 常
new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会 抛出异常!
线程编排
CompletableFuture supplyAsync(Supplier supplier, Executor executor);异步执行线程且有返回值
CompletableFuture runAsync(Runnable runnable, Executor executor) 异步执行线程没有返回值
// 异步任务1
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println("线程future1开始执行");
int i = 10 / 1;
sleepSeconds(1);
System.out.println("线程future1执行结束");
return i;
}, myExecutor).whenComplete((res, ex) -> { // 并不异步执行 // 当上一个线程执行完成后执行,res->上一个线程的执行结果,ex->上一个线程的运行时异常;无返回值
System.out.println("res = " + res);
System.out.println("ex = " + ex);
}).exceptionally(throwable -> 110); // 当以上所有的线程有异常时执行,throwable ->异常信息;返回值->可以自定义返回值。
future.thenAcceptAsync(thenRes->{ //获取以上线程执行结果并异步执行 // 线程执行完成之后没有返回值
System.out.println("thenRes = " + thenRes);
},myExecutor);
// 异步任务2
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
System.out.println("线程future2开始执行");
int i = 101 / 1;
sleepSeconds(5);
System.out.println("线程future2执行结束");
return i++;
}, myExecutor);
// 异步任务3
future2.acceptEitherAsync(// 当future1或future2执行完成之后执行异步 新异步任务 前提是future1和future2的返回值必须一致 不然新的异步任务无法获取返回值并执行
future1, // 异步任务1
res -> { // 新异步任务
System.out.println("线程3333开始执行:" + Thread.currentThread().getName());
System.out.println("res = " + res);
sleepSeconds(1);
System.out.println("线程3333执行结束:" + Thread.currentThread().getName());
},
myExecutor // 线程池
);
//线程future1开始执行
//线程future2开始执行
//线程future1执行结束
//res = 10
//ex = null
//线程3333开始执行:pool-1-thread-1
//res = 10
//线程3333执行结束:pool-1-thread-1
//线程future2执行结束
future2.thenAcceptBoth(future1, (res2, res1) -> { // 当future2和future1都执行完成后执行新线程 其中future2结果对应res2 future1结果对应res1
System.out.println("线程4444开始执行:" + Thread.currentThread().getName());
System.out.println("res1 = " + res1);
System.out.println("res2 = " + res2);
sleepSeconds(1);
System.out.println("线程4444执行结束:" + Thread.currentThread().getName());
// 执行结果
//线程future1开始执行
//线程future2开始执行
//线程future1执行结束
//res = 10
//ex = null
//线程future2执行结束
//线程4444开始执行:pool-1-thread-2
//res1 = 10
//res2 = 102
//线程4444执行结束:pool-1-thread-2
});
future2.applyToEither(future1, res -> { // 和acceptEither()方法的区别在于此方法是有返回值的
System.out.println("线程555开始执行:" + Thread.currentThread().getName());
System.out.println("res = " + res);
sleepSeconds(1);
System.out.println("线程555执行结束:" + Thread.currentThread().getName());
return 322;
});
future2.thenCombine(future1,(res2,res1)->{ // 和thenAcceptBoth()方法的区别在于此方法是有返回值的
System.out.println("线程4444开始执行:" + Thread.currentThread().getName());
System.out.println("res1 = " + res1);
System.out.println("res2 = " + res2);
sleepSeconds(1);
System.out.println("线程4444执行结束:" + Thread.currentThread().getName());
return 322;
});
future2.runAfterEither(future1,()->{ // 不用接收结果去执行 两个线程只要有一个执行完成就行
System.out.println("线程666开始执行:" + Thread.currentThread().getName());
sleepSeconds(1);
System.out.println("线程666执行结束:" + Thread.currentThread().getName());
});
future2.runAfterBoth(future1,()->{ // 不用接收结果去执行 两个线程都需要执行完成
System.out.println("线程666开始执行:" + Thread.currentThread().getName());
sleepSeconds(1);
System.out.println("线程666执行结束:" + Thread.currentThread().getName());
});
//线程排版
future2.thenCompose(res -> {
System.out.println("线程7777开始执行:" + Thread.currentThread().getName());
System.out.println("res = " + res);
sleepSeconds(1);
System.out.println("线程7777执行结束:" + Thread.currentThread().getName());
return CompletableFuture.runAsync(() -> {
System.out.println("线程8888开始执行:" + Thread.currentThread().getName());
System.out.println("res = " + res);
sleepSeconds(10);
System.out.println("线程8888执行结束:" + Thread.currentThread().getName());
});
//线程future1开始执行
//线程future2开始执行
//线程future1执行结束
//res = 10
//ex = null
//线程future2执行结束
//线程7777开始执行:pool-1-thread-2
//res = 102
//线程7777执行结束:pool-1-thread-2
//线程8888开始执行:ForkJoinPool.commonPool-worker-1
//res = 102
//线程8888执行结束:ForkJoinPool.commonPool-worker-1
});
静态代理
静态代理模式
真实对象和代理对象都要实现一个接口,代理对象实现的方法中要调用真实对象的方法,代理对象实现的方法中一般会比真实对象的方法中的操作更多一些,代理对象要做一些真实对象都要去做的事情
public class Sssss {
@Test
public void testStaticPro() {
Boy xm = new Boy("小明");
Girl xh = new Girl("小红");
xm.setGirl(xh);
xh.setBoy(xm);
new MarryProxy(xm, xh).marry();
}
}
// 结婚接口
interface MarryInterface {
// 结婚
void marry();
}
class Boy implements MarryInterface {
private String name;
private Girl girl;
public Boy(String name) {
this.name = name;
}
@Override
public void marry() {
if (girl.getBoy() == this) {
System.out.println(name + "娶了" + girl.getName());
} else {
System.out.println("特么的, 你结婚证上不是我, 不结了");
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Girl getGirl() {
return girl;
}
public void setGirl(Girl girl) {
this.girl = girl;
}
}
class Girl implements MarryInterface {
private String name;
private Boy boy;
public Girl(String name) {
this.name = name;
}
@Override
public void marry() {
if (boy.getGirl() == this) {
System.out.println(name + "嫁给了" + boy.getName());
} else {
System.out.println("特么的, 你结婚证上不是我, 不结了");
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Boy getBoy() {
return boy;
}
public void setBoy(Boy boy) {
this.boy = boy;
}
}
// 婚庆公司
class MarryProxy implements MarryInterface {
// 男方
private Boy boy;
// 女方
private Girl girl;
public MarryProxy(Boy boy, Girl girl) {
this.girl = girl;
this.boy = boy;
}
private void before() {
System.out.println("准备婚礼场地,布置婚礼现场");
}
@Override
public void marry() {
before();
boy.marry();
girl.marry();
after();
}
private void after() {
System.out.println("婚礼结束了, 收拾婚礼场地");
}
}
线程礼让
- 当前执行的线程暂停, 但是不阻塞
- 让线程的运行状态转为暂停状态
- 让CPU重新调度,礼让不一定成功
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + "开始运行");
Thread.yield();
System.out.println(Thread.currentThread().getName() + "运行结束");
};
new Thread(r, "线程1").start();
new Thread(r, "线程2").start();
}
线程的join方法
线程调用join方法之后,会先执行此线程,其他线程等待直到join线程执行完毕,可以想象成插队。
public static void main(String[] args) throws Exception {
Runnable r1 = () -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
};
Runnable r2 = () -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
};
Thread thread1 = new Thread(r1, "线程1");
thread1.start();
Thread thread2 = new Thread(r2, "线程2");
thread2.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main线程->" + i);
if (i == 10) {
// 线程1 和 线程2 是不分前后的 谁先抢占到CPU的使用权 谁就先运行
// 不调用join方法的话.正常情况下是主线程执行完成,线程1和线程2才有执行的机会
thread1.join();
thread2.join();
}
}
}
//main线程->9
//main线程->10
//线程2---->0
//线程2---->1
//线程2---->2
//线程2---->3
//线程2---->4
//线程1---->0
//线程1---->1
//线程1---->2
//线程1---->3
//线程1---->4
//main线程->11
线程的start方法
启动一个线程使用,但是这个方法同一个线程不能调用两次,也就是说一个线程不能启动两次
线程优先级
- java提供一个线程调度器来监控程序中启动后进入就绪状态的线程, 线程调度器按照优先级决定应该调度哪个线程来执行
- 线程的优先级决定应该调度哪个线程来执行
- 线程的优先级使用数字来表示, 范围是1-10
- 使用以下方法改变或者获取线程优先级
getPriority()和setPriority(int) - 建议在线程启动之前设置线程的优先级
- 一个线程的默认优先级大小是5
public class TestThread {
static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
};
Thread thread1 = new Thread(runnable, "线程1->");
thread1.setPriority(1);
thread1.start();
Thread thread2 = new Thread(runnable, "线程2->");
thread2.setPriority(10);
thread2.start();
Thread thread3 = new Thread(runnable, "线程3->");
thread3.setPriority(9);
thread3.start();
Thread thread4 = new Thread(runnable, "线程4->");
thread4.setPriority(2);
thread4.start();
}
//main--->5
//线程2->--->10
//线程3->--->9
//线程1->--->1
//线程4->--->2
}
守护(Daemon)线程
- 在java中有两类线程: 用户线程和守护线程
- 任何一个守护线程都是整个JVM中非守护线程的保姆
- 虚拟机必须等待非守护线程执行完毕才能停止; 但是守护线程是和JVM一起结束工作的.
- 守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
- thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
- 在Daemon线程中产生的新线程也是Daemon的
- 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑
- 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且
周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;
创建一个守护线程
// 用户线程逻辑
Runnable r1 = () -> {
for (int i = 1; i < 36500; i++) {
try {
System.out.println(Thread.currentThread().getName() + "活了第" + i + "天");
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
}
};
// 声明用户线程
Thread xm = new Thread(r1, "小明");
// 用户线程启动
xm.start();
// 守护线程逻辑
Runnable r2 = () -> {
while (true) {
System.out.println(Thread.currentThread().getName() + "活着");
}
};
// 守护线程
Thread god = new Thread(r2, "god");
// 设置为守护线程
god.setDaemon(true);
// 启动守护线程
god.start();