伊成博客
伊成博客

Java基础笔记整合

记录最基础 最容易忘记的笔记

一.JAVA基础

1.1 JAVA异常知识点

Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。下一层分为Error和Exception;

V0OBZt.png

1.1.1 Error

Error类是指java运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。

1.1.2 Exception

Exception又有两个分支,一个是运行时异常RuntimeException,一个是CheckedException。

1.1.2.1 RuntimeException

如:NullPointerException、ClassCastException;一个是检查异常CheckedException,如I/O错误导致的IOException、SQLException。 RuntimeException是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。
如果出现RuntimeException,那么一定是程序员的错误。

1.1.2.2 CheckedException

一般是外部错误,这种异常都发生在编译阶段,Java编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行try catch,该类异常一般包括几个方面:

  1. 试图在文件尾部读取数据
  2. 试图打开一个错误格式的URL
  3. 试图根据给定的字符串查找class对象,而这个字符串表示的类并不存在

1.1.3 异常的处理方式

抛出异常有三种形式,一是throw,一个throws,还有一种系统自动抛异常

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) { 
String s = "abc";
if(s.equals("abc")) {
throw new NumberFormatException();
} else {
System.out.println(s);
}
}

int div(int a,int b) throws Exception{ return a/b;}

1.1.4 Throw和throws的区别

  • 位置不同
  1. throws用在函数上,后面跟的是异常类,可以跟多个;而throw用在函数内,后面跟的是异常对象。
  • 功能不同:
  1. throws用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;
    throw抛出具体的问题对象,执行到throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。
    也就是说throw语句独立存在时,下面不要定义其他语句,因为执行不到。

  2. throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常对象。

  3. 两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。

2.1 值类型,引用类型..

2.1.1 值类型(基本数据类型)

就是基本数据类型 基本数据类型常被称为四类八种

四类:
1,整型 2,浮点型 3,字符型 4,逻辑型

八种:

1,整型3种 byte,short,int,long

2,浮点型2种 float,double

3,字符型1种 char

4,逻辑型1种 boolean

tips:
Byte、Short、Integer、Long、Character这5种包装类都默认创建了数值[-128,127]的缓存数据

当对这5个类型的数据不在这个区间内的时候,将会去创建新的对象,并且不会将这些新的对象放入常量池中。

Float 和Double 没有实现常量池。

2.1.2 引用类型(引用数据类型)

四类八种基本类型外,所有的类型都称为引用类型(数组,类,接口,字符串)

2.1.3 值传递

基本数据类型赋值都属于值传递,值传递传递的是实实在在的变量值,是传递原参数的拷贝,
值传递后,实参传递给形参的值,形参发生改变而不影响实参。

对应的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ReferencePkValue2 {

public static void main(String[] args) {
ReferencePkValue2 t = new ReferencePkValue2();
int a=99;
t.test1(a);//这里传递的参数a就是按值传递
System.out.println(a);

MyObj obj=new MyObj();
t.test2(obj);//这里传递的参数obj就是引用传递
System.out.println(obj.b);
}

public void test1(int a){
a=a++;
System.out.println(a);
}

public void test2(MyObj obj){
obj.b=100;
System.out.println(obj.b);
}
}

输出是:
99
99
100
100

tips:String, Integer, Double等immutable的类型特殊处理,可以理解为传值,最后的操作不会修改实参对象。(JAVA不可变类(immutable)机制与String的不可变性)

2.1.4 引用传递

引用类型之间赋值属于引用传递。引用传递传递的是对象的引用地址,也就是它的本身(自己最通俗的理解)。
引用传递:传的是地址,就是将实参的地址传递给形参,形参改变了,实参当然被改变了,因为他们指向相同的地址。
对应的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ReferencePkValue1 {
public static void main(String[] args){
ReferencePkValue1 pk=new ReferencePkValue1();
//String类似基本类型,值传递,不会改变实际参数的值
String test1="Hello";
pk.change(test1);
System.out.println(test1);

//StringBuffer和StringBuilder等是引用传递
StringBuffer test2=new StringBuffer("Hello");
pk.change(test2);

System.out.println(test2.toString());
}

public void change(String str){
str=str+"world"; // 但是在这里 System.out.println(str);的话 结果就是 Helloworld
}

public void change(StringBuffer str){
str.append("world");
}
}


输出是:
Hello
Helloworld

3.1 Java克隆对象的三种方式

1 直接赋值
2 浅克隆(浅拷贝)
3 深克隆(深拷贝)

3.1.1 浅克隆(浅拷贝)

定义:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝

3.1.2 深克隆(深拷贝)

定义:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容

tips: 深克隆和浅克隆的区别由定义既可知,在引用数据类型是进行了引用的传递还是创建新的对象。

3.1.3 工具类克隆对象

3.1.3.1 apache的BeanUtils 工具类

使用org.apache.commons.beanutils.BeanUtils进行对象深入复制时候,主要通过向BeanUtils框架注入新的类型转换器,因为默认情况下,BeanUtils对复杂对象的复制是引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void beanUtilsTest() throws Exception {
// 注册转化器
BeanUtilsBean.getInstance().getConvertUtils().register(new ArbitrationConvert(), ArbitrationDO.class);
Wrapper wrapper = new Wrapper();
wrapper.setName("copy");
wrapper.setNameDesc("copy complex object!");
wrapper.setArbitration(newArbitrationDO());
Wrapper dest = new Wrapper();
// 对象复制
BeanUtils.copyProperties(dest, wrapper);
// 属性验证
wrapper.getArbitration().setBizId("1");
System.out.println(wrapper.getArbitration() == dest.getArbitration());
System.out.println(wrapper.getArbitration().getBizId().equals(dest.getArbitration().getBizId()));
}

public class ArbitrationConvert implements Converter {

@Override
public <T> T convert(Class<T> type, Object value) {
if (ArbitrationDO.class.equals(type)) {
try {
return type.cast(BeanUtils.cloneBean(value));
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}

实验发现,使用org.apache.commons.beanutils.BeanUtils复制引用时,主和源的引用为同一个,即改变了主的引用属性会影响到源的引用,所以这是一种浅拷贝

tips:需要注意的是,apache的BeanUtils中,以下类型如果为空,会报错(org.apache.commons.beanutils.ConversionException: No value specified for…)

3.1.3.2 apache的PropertyUtils 工具类

PropertyUtils的copyProperties()方法几乎与BeanUtils.copyProperties()相同,主要的区别在于后者提供类型转换功能,即发现两个JavaBean的同名属性为不同类型时,
在支持的数据类型范围内进行转换,PropertyUtils不支持这个功能,它仍然属于浅拷贝

tips: Apache提供了 SerializationUtils.clone(T),T对象需要实现 Serializable 接口,他属于深拷贝

3.1.3.3 spring的BeanUtils 工具类

Spring中的BeanUtils,其中实现的方式很简单,就是对两个对象中相同名字的属性进行简单get/set,仅检查属性的可访问性。
源码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public static void copyProperties(Object source, Object target) throws BeansException {
copyProperties(source, target, (Class)null, (String[])null);
}

public static void copyProperties(Object source, Object target, Class<?> editable) throws BeansException {
copyProperties(source, target, editable, (String[])null);
}

public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {
copyProperties(source, target, (Class)null, ignoreProperties);
}

private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties) throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class actualEditable = target.getClass();
if(editable != null) {
if(!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");
}

actualEditable = editable;
}

PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List ignoreList = ignoreProperties != null?Arrays.asList(ignoreProperties):null;
PropertyDescriptor[] var7 = targetPds;
int var8 = targetPds.length;

for(int var9 = 0; var9 < var8; ++var9) {
PropertyDescriptor targetPd = var7[var9];
Method writeMethod = targetPd.getWriteMethod();
if(writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if(sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if(readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if(!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}

Object ex = readMethod.invoke(source, new Object[0]);
if(!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}

writeMethod.invoke(target, new Object[]{ex});
} catch (Throwable var15) {
throw new FatalBeanException("Could not copy property \'" + targetPd.getName() + "\' from source to target", var15);
}
}
}
}
}

}

tips: 成员变量赋值是基于目标对象的成员列表, 并且会跳过ignore的以及在源对象中不存在的, 所以这个方法是安全的, 不会因为两个对象之间的结构差异导致错误, 但是必须保证同名的两个成员变量类型相同。

3.1.3.4 dozer [Dozer(http://dozer.sourceforge.net/)]

能够实现深拷贝。Dozer是基于反射来实现对象拷贝,反射调用set/get 或者是直接对成员变量赋值 。 该方式通过invoke执行赋值,实现时一般会采用beanutil, Javassist等开源库。

3.1.3.5 MapStrcut

MapStrcut属于编译期的对象复制方案,它能够动态生成set/get代码的class文件 ,在运行时直接调用该class文件。该方式实际上扔会存在set/get代码,只是不需要自己写了。

1
2
3
4
5
@Mapper(componentModel = "spring")
public interface MonitorAppGroupIdcDTOMapper {
MonitorAppGroupIdcDTOMapper MAPPER = Mappers.getMapper(MonitorAppGroupIdcDTOMapper.class);
void mapping(MonitorAppGroupIdcDTO source, @MappingTarget MonitorAppGroupIdcDTO dest);
}

3.1.3.6 BeanCopier

基于CGlib实现Bean拷贝,可以通过缓存BeanCopier的实例来提高性能。

1
2
3
4
5
6
7
8
BeanCopier b = getFromCache(sourceClass,targetClass); //从缓存中取
long start = System.currentTimeMillis();
List<ShopCouponModel> modelList = new ArrayList<>();
for (ShopCouponEntity src : entityList) {
ShopCouponModel dest = new ShopCouponModel();
b.copy(src, dest, null);
modelList.add(dest);
}

3.1.3.7 fastjson和GSON

使用fastjson和GSON主要是通过对象json序列化和反序列化来完成对象复制,这里只是提供一种不一样的对象拷贝的思路

3.1.3.8 序列化(深clone一中实现)

在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建对象。

性能测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
NewNovelMode des = null ;
NewNovelMode ori = buildModel();
Gson gson = new Gson();
int count = 100000;
//org.springframework.beans.BeanUtils.copyProperties
long s = System.currentTimeMillis();
for(int i=0;i<count;i++){
des = new NewNovelMode();
org.springframework.beans.BeanUtils.copyProperties(ori, des);
}
System.out.println("springframework BeanUtils cost:"+(System.currentTimeMillis() - s));
// System.out.println(new Gson().toJson(des));

//org.apache.commons.beanutils.BeanUtils
s = System.currentTimeMillis();
for(int i=0;i<count;i++){
des = new NewNovelMode();
org.apache.commons.beanutils.BeanUtils.copyProperties(des, ori);
}
System.out.println("apache BeanUtils cost:"+(System.currentTimeMillis() - s));
// System.out.println(new Gson().toJson(des));

//gson转换
s = System.currentTimeMillis();
for(int i=0;i<count;i++){
des = gson.fromJson(gson.toJson(ori), NewNovelMode.class);
}
System.out.println("gson cost:"+(System.currentTimeMillis() - s));
// System.out.println(new Gson().toJson(des));

//Pojo转换类
s = System.currentTimeMillis();
PojoUtils<NewNovelMode, NewNovelMode> pojoUtils = new PojoUtils<NewNovelMode, NewNovelMode>();
for(int i=0;i<count;i++){
des = new NewNovelMode();
pojoUtils.copyPojo(ori,des);
}
System.out.println("Pojoconvert cost:"+(System.currentTimeMillis() - s));
// System.out.println(new Gson().toJson(des));

性能对比: BeanCopier > BeanUtils. 其中BeanCopier的性能高出另外两个100数量级。

  • BeanUtils(简单,易用)
  • BeanCopier(加入缓存后和手工set的性能接近)
  • Dozer(深拷贝)
  • fastjson(特定场景下使用)

4.1 JAVA 集合

Java 集合类存放在 Java.util包中,主要有3中:Set(集),list(列表包括Queue),Map(映射)

  • Collection: Collection是集合List,Set,Queue的最基本的接口。
  • Iterator: 迭代器,可以通过迭代器遍历集合中的数据。
  • Map: 是映射表的最基础的接口。

[E8RVdf.md.png]

[E8Rh6A.md.png]

4.1.1 List

Java的List是非常常用的数据类型。

List是有序的Collection。

Java List一共三个实现类:分别是ArrayListVectorLinkedList
[E8Rh6A.md.png]

4.1.2 ArrayList(数组)

ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。

数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。

当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。

因此,它适合随机查找和遍历,不适合插入和删除。

4.1.3 Vector(数组实现、线程同步)

Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,

避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,

因此,访问它比访问ArrayList慢。

4.1.4 LinkList(链表)

LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。

另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,

可以当作堆栈、队列和双向队列使用。

名字 描述
List 1. 需要保留存储顺序,并保留重复数据,使用 List
‘’ 2.查询较多,使用ArrayList
‘’ 3.存储较多,使用LinkedList
‘’ 4.线程安全,使用Vector

4.1.2 Set

Set注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复

对象的相等性本质是对象hashCode值(java是依据对象的内存地址计算出的此序号)判断的,

如果想要让两个不同的对象视为相等的,就必须覆盖Object的hashCode方法和equals方法。

E8OAIJ.png

4.1.2.1 HashSet(Hash表)

哈希表边存放的是哈希值。HashSet存储元素的顺序并不是按照存入时的顺序(和List显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。

元素的哈希值是通过元素的hashcode方法来获取的, HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法

如果 equls结果为true ,HashSet就视为同一个元素。如果equals 为false就不是同一个元素。

4.1.2.2 TreeSet(二叉树)

  1. TreeSet()是使用二叉树的原理对新add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。

  2. Integer和String对象都可以进行默认的TreeSet排序,而自定义类的对象是不可以的,自己定义的类必须实现Comparable接口,并且覆写相应的compareTo()函数,才可以正常使用。

  3. 在覆写compare()函数时,要返回相应的值才能使TreeSet按照一定的规则来排序

  4. 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。

4.1.2.3 LinkHashSet(HashSet+LinkedHashMap)

对于LinkedHashSet而言,它继承与HashSet、又基于LinkedHashMap来实现的。

LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet相同

因此LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个LinkedHashMap来实现,

在相关操作上与父类HashSet的操作相同,直接调用父类HashSet的方法即可。

4.1.3 Map

E8ORyV.png

4.1.3.1 HashMap(数组+链表+红黑树)

HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。

HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。

如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

E8OvwD.png
HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。

上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。

  1. capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。

  2. loadFactor:负载因子,默认为 0.75。

  3. threshold:扩容的阈值,等于 capacity * loadFactor

E8XF6P.png
Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。

根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,

需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。

为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,

在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

4.1.3.2 HashTable(线程安全)

Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,

并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。

Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

4.1.3.3 TreeMap(可排序)

TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。

如果使用排序的映射,建议使用TreeMap。

在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

4.1.3.4 LinkHashMap(记录插入顺序)

LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

4.1.3.5 ConcurrentHashMap

Segment段

ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。整个 ConcurrentHashMap 由一个个 Segment 组成,

Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了“槽”来代表一个 segment。

线程安全(Segment 继承 ReentrantLock 加锁)

ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。

E8vSxI.png
并行度(默认16) concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。

默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment上。

这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。

E8vsFe.png
Java8 对 ConcurrentHashMap 进行了比较大的改动,Java8 也引入了红黑树。

5.1 Java 多线程并发

5.1.0 JAVA并发知识库图

EG8jRe.png

5.1.1 JAVA线程实现/创建方式

5.1.1.1 继承Thread类

Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。

启动线程的唯一方法就是通过Thread类的start()实例方法。

start()方法是一个native方法,它将启动一个新线程,并执行run()方法。

1
2
3
4
5
6
7
8
9
10
public class MyThread extends Thread { 

public void run() {
System.out.println("MyThread.run()");
}

}

MyThread myThread1 = new MyThread();
myThread1.start();

5.1.1.2 实现Runnable接口

如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyThread extends OtherClass implements Runnable { 
public void run() {
System.out.println("MyThread.run()");
}
}

//启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:

MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run()
public void run() {
if (target != null) {
target.run();
}
}

5.1.1.3 ExecutorService、Callable、Future有返回值线程

有返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。

执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取Future对象
Future f = pool.submit(c); list.add(f); }
// 关闭线程池 pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println("res:" + f.get().toString());
}

5.1.1.4 基于线程池的方式

线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建线程池 
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while(true) {
threadPool.execute(new Runnable() {
// 提交多个线程任务,并执行
@Override public void run() {
System.out.println(Thread.currentThread().getName() + " is running ..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}

5.1.2 四种线程池

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

EGYU56.png

5.1.2.1 newCachedThreadPool

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。

调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。

终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源

5.1.2.2 newFixedThreadPool

创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。

如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,

那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

工作代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 线程池
private ExecutorService pool = Executors.newFixedThreadPool(50);

/**
* 三期拆相册
* @param queryModel
* @param userId
* @return
*/
public int[] countOrderNum2(String queryModel, int userId) {
int[] ret = new int[] { 0, 0, 0, 0 };
Map<String, Object> map = getParamMap(queryModel, userId, null, null);


map.put("userId", userId);
CompletableFuture<Integer> integerCompletableFuture = CompletableFuture.supplyAsync(() -> {
HashMap<String,Object> map2 = new HashMap<>();
map2.putAll(map);
map2.put("queryType", 1);
return orderMapper.countOrderNum2(map2);
}, pool);


ret[0]=integerCompletableFuture.join();
return ret;
}

5.1.2.3 newScheduledThreadPool

创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

工作代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   private static ScheduledExecutorService SCHEDULE_POOL = Executors.newScheduledThreadPool(2);

/**
* 计算云相册使用费和委拍服务费
*/
private void recalculatePhotographerOrderFee(Long orderId, Long userId) {

/**
* xxx demo...
*/


// 因为事务的问题 需延迟重算摄影师人数 这段代码执行之后 事务需要在1s内提交完成 所以需要保证这段代码 是在事务的最后执行
SCHEDULE_POOL.schedule(() -> {
// 重新计算摄影师人数
itOrderSourceNumService.recalculatePhotographerOrderSourceNum(orderId);
}, 1, TimeUnit.SECONDS);
}

5.1.2.4 newSingleThreadExecutor

Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!

5.1.3 线程生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。

在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。

尤其是当线程启动以后,它不可能一直”霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

5.1.3.1 新建状态(NEW)

当代码中使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。

5.1.3.2 就绪状态(RUNNABLE)

当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

5.1.3.3 运行状态(RUNNING)

如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。

5.1.3.4 阻塞状态(BLOCKED)

阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。

阻塞的情况分三种:

等待阻塞(o.wait->等待对列):
运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

同步阻塞(lock->锁池) 运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

其他阻塞(sleep/join) 运行(running)的线程执行Thread.sleep(longms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。

当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

5.1.3.5 线程死亡(DEAD)

线程会以下面三种方式结束,结束后就是死亡状态。

正常结束 1. run()或call()方法执行完成,线程正常结束。

异常结束 2. 线程抛出一个未捕获的Exception或Error。

调用stop 3. 直接调用该线程的stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。

EyOhZT.png

5.1.4 终止线程4种方式

  • 正常运行结束
    程序运行结束,线程自动结束。

  • 使用退出标志退出线程
    一般run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。

使用一个变量来控制循环,例如:最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出,

代码示例:

1
2
3
4
5
6
7
8
public class ThreadSafe extends Thread { 
public volatile boolean exit = false;
public void run() {
while (!exit){
//do something
}
}
}

  • Interrupt方法结束线程
    使用interrupt()方法来中断线程有两种情况:
    1.线程处于阻塞状态:如使用了sleep,同步锁的wait,socket中的receiver,accept等方法时,会使线程处于阻塞状态。当调用线程的interrupt()方法时,会抛出InterruptException异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用interrupt方法线程就会结束,实际上是错的, 一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正常结束run方法。

2.线程未处于阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。

1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
try{
Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行break跳出循环
}
}
}
}

  • stop方法终止线程(线程不安全)
    程序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,

不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。

一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,

其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程。

5.1.5 线程相关的两个问题

5.1.5.1 sleep与wait 区别

  • 对于sleep()方法,该方法是属于Thread类。而wait()方法,则是属于Object类。
  • sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态
  • 在调用sleep()方法的过程中,线程不会释放对象锁。
  • 而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

5.1.5.2 start与run区别

  • start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码。
  • 通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
  • 方法run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行run函数当中的代码。 Run方法运行结束, 此线程终止。然后CPU再调度其它线程。

如有你觉得也是基础的未写出来的,可以给我留言哦!我会加上来的!

支付宝打赏 微信打赏

如果本文对你有所帮助,请打赏 1元就足够感动我 :)