6. 泛型程序设计
泛型,即“参数化类型”,将原来的具体类型参数化。在不创建新类型的情况下,通过泛型指定不同的类型形参,来控制实际传入实参的具体类型。换句话说,就是在使用和调用时传入具体的类型。
为什么使用泛型?
- 能够对类型进行限定(比如集合)
- 将运行期错误提前到编译期错误
- 获取明确的限定类型时无需进行强制类型转化
- 具有良好的可读性和安全性
6.1 泛型类
泛型类的定义
一个简单的泛型类,和普通类的区别是,类名后添加了<T>
一个泛型标识,“T"类型参数(类型形参),传入的是类型实参,当然也可以用其他字母标识,但是"<>"左右尖括号必须存在。
public class Generic<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static void main(String[] args) {
/* 1.没有传入具体的类型参数,可以存放任何类型的数据【下图】
* 本质:虚拟机会对泛型代码进行类型擦除,类型擦除后Generic<T>会变为
* Generic(原始类型),后面会讲到,无限定类型的域用Object代替,即擦除
* 后T data->Object data,这也是为什么没有传入具体类型,却能存放多种
* 类型的原因。
*/
Generic generic = new Generic();
generic.setData(1);
generic.setData("String");
generic.setData(new Object());
Generic genericStr = new Generic<String>();//CORRECT [1]
genericStr.setData("hello");
genericStr.setData(1);
/*
* 2.参数化类型(初始化时传入具体的类型参数)【下图】
* 本质:由编译器进行类型转化的处理,无需人为干预。
* 当调用泛型方法时,编译器自动在调用前后插入相应的
* 强转和调用语句。
*/
Generic<String> genericString=new Generic<>();
genericString.setData("hello");
//Generic<int> genericInt=new Generic<>(); ERROR [2]
Generic<Integer> genericInt=new Generic<>();
}
}
【1】原始类型可以接受任何参数化类型,即Generic generic == new Generic<String>()
如[1]处所示。
【2】泛型的类型参数只能是类类型,不能是基本类型。如[2]处,但可以使用期包装类型。
【3】泛型参数命名规范如下:
泛型命名规范:国际惯例,类型参数的命名采用单个大写字母。
常见的泛型命名有:
T
- Type:第一类通用类型参数。S
- Type:第二类通用类型参数。U
- Type:第三类通用类型参数。V
- Type:第四类通用类型参数。E
- Element:主要用于Java集合(Collections)框架使用。K
- KeyV
- ValueN
- NumberR
- Result
6.2 泛型接口
泛型接口的定义
和泛型类定义相似,如下:
public interface GenericInterface<T> {
T getData();
T setData(T data);
}
类接口的实现
类接口的实现存在三种形式,第一种无泛型,域类型用Object定义;第二种有泛型,域变量用泛型参数定义;第三种传递具体的类型参数,域变量的类型为具体的类型。
public class GenericInterfaceImpl implements GenericInterface {
@Override
public Object getData() {
return null;
}
@Override
public Object setData(Object data) {
return null;
}
}
/*
* 实现类的类型参数也需要声明,否则编译器会报错
*/
class GenericInterfaceImpT<T> implements GenericInterface<T> {
@Override
public T getData() {
return null;
}
@Override
public T setData(T data) {
return null;
}
}
/*
* 传入具体的类型实参
*/
class GenericInterfaceImplStr implements GenericInterface<String> {
@Override
public String getData() {
return null;
}
@Override
public String setData(String data) {
return null;
}
}
6.3 泛型方法
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
1、泛型方法的定义:类型变量放在修饰符前,返回类型的后面
# 修饰符 <T> 返回值 方法名(...);
* 示例
class ArrayAlg {
/*
*修饰符与返回值(T)中间的<T>标识此方法为泛型方法
*<T>表明该方法可以使用泛型类型T,可以在形参或者方法体中声明变量
/
public static <T> T getMiddle(T...a) {
return a[a.length/2];
}
}
2、调用泛型方法,在方法名前的尖括号放入具体的类型:
String middle = ArrayAlg.<String>getMiddle("John","Q","Public");
// 类型推断:类型参数可以省略 等同于
String middle = ArrayAlg.getMiddle("John","Q","Public");
使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 类型参数推断。
3、泛型方法辨别真假
/*
* 泛型类
* 注意:下面为了介绍,不把泛型方法归类到成员方法里,泛型方法是特指!
*/
public class GenericMethod<T> {
private T data;
/*
* [成员方法:非泛型方法]
* T getData()和setData(T data) 都不是泛型方法
* 他们只是类中的成员方法,只不过是方法的返回值类型
* 和方法的形参类型是用的泛型类上的T所声明的。
*/
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
/*
* [泛型方法]
* 泛型参数可以有多个,这里的T和泛型类上的T无任何关联,但是但是
* 它和泛型类上的参数类型变量相同,这时候idea会给予一个rename提示
*/
public <T,S> T genericMethod(S...a) {
return null;
}
/*
* [泛型方法]
* 使用泛型类上的泛型变量
* 这时候的T就和泛型类的类型相关了
*/
public <V> T genericMethod$1(T a, V b) {
return null;
}
/*
* [静态方法]
* 静态方法不能使用泛型类上的类型参数
* 如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法
*/
//public static T getDataStatic(T e) { } //ERROR [1]
/*
* [泛型方法]
* 静态泛型方法
*/
public static <E> E genericMethodS(E e) {
return e;
}
}
/*
*普通类中的泛型方法
*/
class OrdinaryClass {
public <T> void sayHello() {
T a;
//...
}
}
下面以苹果为例:
public class GenericClass<T> {
//成员方法,形参类型与泛型类的类型参数相关
public void print$1(T a) {
System.out.println(a.toString());
}
//下面三个都为泛型方法
//--begin
public <T> void print$2(T a) {
System.out.println(a.toString());
}
public <S> void print$3(S a) {
System.out.println(a.toString());
}
public static <T> void print$4(T a) {
System.out.println(a.toString());
}
//--end
public static void main(String[] args) {
Apple apple = new Apple();
MacBook macBook = new MacBook();
HongFuShi hongFuShi = new HongFuShi();
// 泛型类在初始化时限定了参数类型,成员方法中若使用泛型参数将会受限
GenericClass<Apple> genericCls = new GenericClass<>();
genericCls.print$1(apple);
//MacBook是apple的一个子类
genericCls.print$1(macBook);// OK
//由于初始化指定了泛型类型,print$1形参中的参数类型和泛型类的类型参数相关联
//所以,只能打印Apple及其子类
//genericCls.print$1(hongFuShi); ERROR
//泛型方法中的泛型变量类型与泛型类中的泛型参数没有任何关联
//所以说下面都能正常执行
genericCls.print$2(apple);
genericCls.<MacBook>print$2(macBook);//类型参数可以省略 [2]
genericCls.print$2(hongFuShi);
GenericClass.print$4(hongFuShi);
}
}
class Apple {
@Override
public String toString() {
return "Apple,Steve Jobs";
}
}
class MacBook extends Apple {
@Override
public String toString() {
return "MacBook";
}
}
class HongFuShi{
@Override
public String toString() {
return "HongFushi";
}
}
小结:
【1】泛型方法的标识:方法修饰符后返回值之前有"<…>"的声明。(判断是否为泛型方法)
【2】泛型方法可以定义在普通类中,也可以定义在泛型类中。
【3】静态方法不能使用泛型类上的类型参数,如[1]处
【4】成员方法中使用的参数类型和泛型类中的声明的类型参数有关联。
【5】**泛型类中的参数类型与泛型方法中的参数类型的关联:泛型方法可以使用泛型类上的参数类型,这时候就与泛型类上的参数相关联;如果泛型方法中声明了与泛型类上相同的参数类型,那么优先使用泛型方法上的参数类型,**这时候idea会给予一个rename提示。
【6】泛型方法的使用过程中,无需对类型进行声明,它可以根据传入的参数,自动判断。[2]
【7】方法中泛型参数不是凭空而来的,要么来自于泛型类上所定义的参数类型,要么来自于泛型方法中定义的参数类型。
泛型方法能独立于类而发生变化,所以说在使用原则上,**在能达到目的的情况下,尽量使用泛型方法。**即,如果使用泛型方法可以取代将整个类泛化,那么应该有限采用泛型方法。
6.3 类型变量的限定
对于类型变量没有限定的泛型类或方法, 它是默认继承自Object
,当没有传入具体类型时,它有的能力只有Object
类中的几个默认方法实现,原因就是类型擦除。
如果某个类实现Comparable接口中的compareTo方法,我们就可以通过compareTo比较两个值的大小。比如我们要计算数组中的最小元素:
public static void main(String[] args) {
// 传入 4 , 2 , 自动装箱成Integer类
int r = max(4, 2);
}
static <T> T min(T[] a) {
if (a == null || a.length == 0)
return null;
T smallest = a[0];
for (int i = 1; i < a.length; i++)
if (smallest.compareTo(a[i]) > 0)// ERROR,因为编译器不知道T声明的变量是什么类型
smallest = a[i];
return smallest;
}
如果没有对类型进行限定,它默认只有Object能力,变量smallest类型为T,编译器不知道他是否是实现了Comparable接口(是否是Comparable类型),所以可以通过将T限定为实现了Comparable接口的类,就可以解决这一问题。
对 类型参数进行限定,让它能够默认拥有一些类的"能力"。
static <T extends Comparable> T min(T[] a){...}
类型变量限定格式:<T extends BoundingType>
【1】T应该是绑定类型的子类型(subtype)。T和绑定类型可以是类,也可以是接口。
【2】一个类型变量或通配符可以有多个限定,限定类型用”&“分隔,类型变量用逗号分隔。例如:
<T extends Comparable & Serializable>;
<T,E extends Comparable & Serializable>;
【3】在 Java 的继承中, 可以拥有多个接口超类型, 但限定中至多有一个类。 如果用 一个类作为限定, 它必须是限定列表中的第一个。
<T extends ArrayList & LinkedList>;//ERROR,限定中至多有一个类
<T extends Comparable & LinkedList>;//ERROR,必须是限定列表中的第一个
<T extends ArrayList & Comparable>;//CORRECT
【4】类型限定不仅可以在泛型方法上,也可以在泛型类上,类型限定必须与泛型的声明在一起。
public <T extends Number> T compare(Generic<T extends Comparable> a) {..}//ERROR
public <T extends Number> T compare(Generic<T> a) {..}//ERROR
6.4 类型擦除
虚拟机没有泛型类型对象—所有对象都属于普通类。
类型擦除:无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型,如果没有给定限定就用Object替换。
例如,Holder的原始类型如下:
public class Holder {
private Object holder;
public Holder(Object holder) {
this.holder = holder;
}
public Object getHolder() {
return holder;
}
}
// 类型擦除也会出现在泛型方法中。
public static <T extends Comparable> T min(T[] a);
// 擦除类型之后,只剩下
public static Comparable min(Comparable[] a);
Java泛型 转换的事实:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都用它们的限定类型替换。
- 桥方法被合成来保持多态。
- 为保持类型安全性,必要时插入强制类型转换。
6.5 泛型的约束与局限
(1)不能用基本类型实例化类型参数
其原因是当类型擦除后,Object类型的域不能存储基本类型的值。
(2)所有的类型查询只产生原始类型
List<Number> numbers = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(numbers.getClass() == integers.getClass());//true
if (integers instanceof List){//true
System.out.println(true);
}
/*if (integers instanceof List<Integer>){//compile error
System.out.println(true);
}*/
(3)不能创建一个确切的泛型类型的数组
//List<Integer>[] lists = new ArrayList<Integer>[10];//ERROR
//可以声明原始类型创建数组,但是会得到一个警告
//可以通过@SuppressWarnings("unchecked")去除
List<Integer>[] list = new ArrayList[10];
//使用通配符创建泛型数组也是可以的,但是需要强制转换
List<Integer>[] listWildcard = (List<Integer>[])new ArrayList<?>[1];
下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK
(4)不能实例化类型变量
static <T> Object init(Class<T> cls) throws Exception {
//T a = new T(); // ERROR
// 注意不存在T.class.newInstance();
T t = cls.newInstance();//Class本身也是一个泛型类
return t;
}
(5)不能构造泛型数组
(6)泛型类的静态上下文中类型变量无效
(7)不能抛出或捕获泛型类的实例
6.6 不变 协变 逆变
首先看一段代码
Number[] n = new Integer[10];
ArrayList<Number> list = new ArrayList<Integer>(); // ERROR type mismatch
为什么Number
类型的数组可以由Integer
实例化,而ArrayList<Number>
却不能被ArrayList<Integer>
实例化呢?这就涉及到将要介绍的主题。
不变协变逆变的定义:
逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果AA、BB表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类);
- f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
- f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
- f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
容易证明数组是协变的即Number[] n = new Integer[10];
泛型是不变的,即使它的类型参数存在继承关系,但是整个泛型之间没有继承关系 : ArrayList<Number> list = new ArrayList<Integer>();
6.7 泛型类型的继承规则
1. 泛型参数是继承关系的泛型类之间是没有任何继承关系的。
在java中,Number是所有数值类型的父类,任何基本类型的包装类型都继承于它。
ArrayList<Integer> integers = new ArrayList<>();
ArrayList<Number> numbers = new ArrayList<>();
numbers = integers; // ERROR,就上上面刚刚提到的泛型是不变的
**2. 泛型类可以扩展或实现其他的泛型类。**这一点和普通类没有声明区别。比如ArrayList<T>
类实现List<T>
接口。这意味着,一个ArrayList<Integer>
可以转换为一个List<Integer>
(父类指向了子类的引用),但是,一个ArrayList<Integer>
不是一个ArrayList<Number>
或List<Number>
(泛型参数继承与类无关)。
6.7 通配符
Java中引入通配符?
来实现逆变和协变,通过通配符之前的操作也能赋值成功,如下所示:
List<? extends Number> number = new ArrayList<Integer>();// CORRECT
List<? super Number> list = new ArrayList<Object>();// CORRECT 逆变的代表
使用通配符的子类型关系
ArrayList<Integer>
是ArrayList<? extends Number>
的一个子类型。
通配符的分类
-
? extends T
(上边界通配符 upper bounded wildcard)"?"是继承自T的任意子类型,表示一种约束关系。即泛型类型的范围不能超过T。
可以取元素,不能添加元素。
-
?
(无限定通配符) -
? super T
(下边界通配符 lower bounded wildcard)可以取元素,但是取出的元素是Object,可以添加元素,添加的元素,必须是T类或者其子类
记忆:上不存,下不取
示例:类型上边界通配符为什么只能添加?
ArrayList<Integer> integers = new ArrayList<>();
ArrayList<Object> objects = new ArrayList<>();
ArrayList<Number> nums = new ArrayList<>();
/*
* 1. 类型上边界通配符
* 对变量numbers赋值,引用的集合类型参数只能是Number或者其子类。
*/
ArrayList<? extends Number> numbers;
numbers = nums;
numbers = integers;
//引用的类对象类型超过了泛型类型的上边界
//numbers = objects; ERROR
integers.add(1);// 正常添加元素
//numbers.add(1); ERROR numbers只能读取,不能添加[1]
//但是可以添加null
numbers.add(null);
Number number = numbers.get(0);//可以读取元素
为什么只能读取,不能添加?[1]
? extends T 表示类型的上界,类型参数是T的子类,那么可以肯定的说,get方法返回的一定是个T(不管是T或者T的子类)编译器是可以确定知道的。但是add方法只知道传入的是个T,至于具体是T的那个子类,不知道。
转化到本例来说就是:
理解方式一:
? extends Number指定类型参数必须是Number的子类,get方法返回的一定是Number
编译器确定,但是对于ArrayList的add方法为来说add(E e)->add(? extends Number e);
调用add函数不能够确定传入add的是Number的哪个子类型。编译器不确定。
理解方式二:
List是线性表吧【线性结构的存储】,线性表是n个具有相同类型的数据元素的有限序列。假若number能够add,因为? extends Number泛型通配符,可以添加Number的任何子类型,那么numbers在get时,极有可能引发ClassCastException,比如numbers引用了<Integer>
,但是在索引0处却add了float类型的数据,取出的时候如果numbers.get(0).intValue();就会抛出异常。并且这也违背了线性表中特性,只能存放单一类型的元素。
/*
* 2. 类型下边界通配符
* numbersSuper所能引用的变量必须是Number或者其父类
*/
ArrayList<? super Number> numbersSuper;
numbersSuper = objects;// 逆变
numbersSuper = nums;
//限定了通配符的下界,类型最低是Number,Integer达不到
//下界,类型不匹配
//numbersSuper = integers; ERROR
numbersSuper.add(1); [2]
numbersSuper.add(2.0f); [3]
//numbersSuper.add(new Object()) ERROR
Object object = numbersSuper.get(0);
System.out.println(object);
? super T 表示类型的下界,类型参数是T的超类(包括T本身), 那么可以肯定的说,get方法返回的一定是个T的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以get方法返回Object。 编译器是可以确定知道的。对于add方法来说,编译器不知道它需要的确切类型,但是T和T的子类可以安全的转型为T。
为什么? super Number
就可以add了呢?[2、3]
首先要明确的一点是,add的时候只能是Number[T]及其它的子类,不要和numberSuper只能引用Number的父类所混淆了。正因为numberSuper引用了<Object>
,那么numberSuper在add的时候类型确定,都可以看作是Object类型,即Number的子类Integer和Float也是其Object的子类。但是相对于? extends T
就add就不能调用,numbers如果限定了<Integer>
,还是那句话,假若能放的话,number存放float类型的数据,取值时极易引发类型转化异常。
泛型方法和类型通配符
类型通配符所能解决的泛型方法一定也能解决
# 类型通配符
`public void func(List<? extends E> list);`
# 泛型方法
`public <T extends E> void func(List<T> list);`
* 上面两种方法可以达到同样的效果,两者的主要区别还是
i. 泛型对象是只读的,不可修改,因为?类型是不确定的,可以代表范围内任意类型;
ii. 而泛型方法中的泛型参数对象是可修改的,因为类型参数T是确定的(在调用方法时确定),因为T可以用范围内任意类型指定;
泛型方法和类型通配符(上界和下界)我们应该如何选择?
(1)泛型方法和通配符之间
修改最好使用泛型方法,在多个参数、返回值之间存在类型依赖关系就应该使用泛型方法,否则就应该使用通配符。
(2)什么时候用extends什么时候用super
PECS: producer-extends, consumer-super.
—《Effective Java》
- 要从泛型类取数据时,用extends
- 要往泛型类写数据时,用super
7. 集合
Java 集合框架简图,黄色为接口,绿色为抽象类,蓝色为具体类。虚线箭头表示实现关系,实线箭头表示继承关系。
集合和数组,为什么使用集合?
我们可以通过数组来保存一定数量的对象,类型和数量这些都是提前已知的的,但是有时程序需要根据运行时动态创建新的对象,在此之前,无法知道所需对象的数量甚至确切类型,所以集合的出现就可以解决这一问题。
7.1 基本概念
Java集合类库采用接口与实现分离的结构,为不同类型的集合定义了大量接口,Java集合框架中的接口如下图:
集合中有两个基本接口,Collection和Map
- 集合(Collection):一个独立元素的序列,这些元素都服从一条或多条规则。List 有序集合,必须以插入的顺序保存元素,集合中允许重复元素, Set 集合不保证集合有序,不能包含重复元素, Queue 按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。
- 映射(Map):一组成对的“键值对”对象。允许使用键来查找值。键唯一,可以为NULL,但只能有一个。ArrayList 使用数字来查找对象,因此在某种意义上讲,它是将数字和对象关联在一起。 map 允许我们使用一个对象来查找另一个对象,它也被称作关联数组(associative array),因为它将对象和其它对象关联在一起;或者称作字典(dictionary),因为可以使用一个键对象来查找值对象,就像在字典中使用单词查找定义一样。
List是一个有序集合(order collection),元素能被增加到容器中特定位置。List集合中的元素有两种访问方式整数索引访问和迭代器访问,整数索引访问又称为随机访问,因为可以按任一顺序访问元素,与之不同的是迭代器访问,只能顺序访问元素。List接口定义了多个随机访问的方法:
void add(int index, E element);
void remove(int index);
E get(int index);
E set(int index, E element);
在程序中使用集合时,往往在构建时才指定具体的实现类,接口指向实现,
List<Apple> apples = new ArrayList<>();
使用接口的好处是,如果你想要改变具体的实现只需要在创建时改变就可以了,如
List<Apple> apples = new LinkedList<>();
但这种方法并非总是可行,因为实现类向上转型为父类型,调用方法只能是父类通用的方法,而具体类存在父类没有的方法,例如, LinkedList 具有 List 接口中未包含的额外方法,而 TreeMap 也具有在 Map 接口中未包含的方法。如果需要使用这些方法,就不能将它们向上转型为更通用的接口。
7.2 Java库中的具体集合
集合类型 | 描述 |
---|---|
ArrayList | 一种可以动态增长和缩减的索引序列 |
LinkedList | 一种可以在任何位置进行高效地插入和删除操作的有序序列 |
ArrayDeque | 一种用循环数组实现的双端队列 |
HashSet | 一种没有重复元素的无序集合 |
TreeSet | 一种有序集 |
EnumSet | 一种包含枚举类型值的集 |
LinkedHashSet | 一种可以记住元素插入次序的集 |
PriorityQueue | 一种允许高效删除最小元素的集合 |
HashMap | 一种存储键/值关联的数据结构 |
TreeMap | 一种键值有序排列的映射表 |
EnumMap | 一种键值属于枚举类型的映射表 |
LinkedHashMap | 一种可以记住键/值项添加次序的映射表 |
WeakHashMap | 一种其值无用武之地后可以被垃圾回收器回收的映射表 |
IdentityHashMap | 一种用==而不是用equals比较值的映射表 |
除以Map结尾的类之外,其他类都实现了Collection接口,而以Map结尾的类实现了Map接口。
7.2 添加元素组&打印元素
7.4 列表List
List接口在Collection的基础上添加了许多方法,允许在list中间插入和删除元素。
List主要有两种类型的具体实现:
- ArrayList:底层数组实现,动态调整集合的大小,擅长随机访问,但在ArrayList中插入和删除元素速度较慢。
- LinkedList:底层链表实现,擅长插入和删除,对于随机访问来说相对较慢。
7.4.1 List
下面以一个例子来介绍List接口中方法的使用:首先定义一个外部类Phone,提供一个静态方法list返回一组Phone集合。
class Phone {
String name;
static List<Phone> list() {
// Exception in thread "main" java.lang.UnsupportedOperationException
//return Arrays.asList(new HuaWei(), new Nova(), new P40(), new Honor(), new IPhone(), new OnePlus());
return new ArrayList<>(Arrays.asList(new HuaWei(), new Nova(), new P40(), new Honor(), new IPhone(), new OnePlus()));
}
@Override
public String toString() {
return name;
}
}
class HuaWei extends Phone{public HuaWei() {super.name = "HuaWei";}}
class Nova extends Phone{public Nova() {super.name = "Nova";}}
class P40 extends Phone{public P40() {super.name = "P40";}}
class Honor extends Phone{public Honor() {super.name = "Honor";}}
class Honor20 extends Phone{public Honor20() {super.name = "Honor20";}}
class IPhone extends Phone{public IPhone() {super.name = "IPhone";}}
class OnePlus extends Phone{public OnePlus() {super.name = "OnePlus";}}
public class ListTest {
public static void main(String[] args) {
List<Phone> phones = Phone.list();
System.out.println(phones);
//=================[1]====================
Honor20 honor20 = new Honor20();
phones.add(honor20); // Automatically resizes
System.out.println("insert honor20->" + phones);
System.out.println("contains honor20->" + phones.contains(honor20));
phones.remove(honor20); // Remove by Object
phones.remove(1); // Remove by index
System.out.println("remove honor20 and object in 1 index->" + phones);
/*
输出:
[HuaWei, Nova, P40, Honor, IPhone, OnePlus]
insert honor20->[HuaWei, Nova, P40, Honor, IPhone, OnePlus, Honor20]
contains honor20->true
remove honor20 and object in 1 index->[HuaWei, P40, Honor, IPhone, OnePlus]
*/
//=================[2]====================
Phone p = phones.get(0);
System.out.println(p + " index:" + phones.indexOf(p));
HuaWei huaWei = new HuaWei();
// 因为集合中存有一个HuaWei对象
// 在没有将新对象huaWei加入到集合中之前,删除这个新对象,查看是否会影响集合中的HuaWei对象
System.out.println(phones.indexOf(huaWei));
System.out.println(phones.remove(huaWei));
// 删除集合中的HuaWei对象
System.out.println(phones.remove(p));
System.out.println(phones);
phones.add(0, new HuaWei()); // 在指定索引处插入对象
System.out.println(phones);
/*
输出:
HuaWei index:0
-1
false
true
[P40, Honor, IPhone, OnePlus]
[HuaWei, P40, Honor, IPhone, OnePlus]
*/
//=================[3]====================
List<Phone> sub = phones.subList(1, 4);// 求子集范围[1,4),4是开区间
System.out.println("subList: " + sub);
System.out.println("before shuffled containsAll->" + phones.containsAll(sub));
Collections.shuffle(phones); // 打乱集合
System.out.println("shuffled subList: " + sub);
System.out.println("after shuffled containsAll->" + phones.containsAll(phones)); //集合元素的顺序不影响containsAll的结果
ArrayList<Phone> copy = new ArrayList<>(phones);//[3.1]
sub = Arrays.asList(phones.get(1), phones.get(4));//[3.2]
System.out.println("copy: " + copy + " sub: " + sub);
copy.retainAll(sub); //求交集
System.out.println("retainAll(求交集之后)的copy: " + copy);
/*
输出:
subList: [P40, Honor, IPhone]
before shuffled containsAll->true
shuffled subList: [OnePlus, Honor, HuaWei]
after shuffled containsAll->true
copy: [IPhone, OnePlus, Honor, HuaWei, P40] sub: [OnePlus, P40]
retainAll(求交集之后)的copy: [OnePlus, P40]
*/
//=================[4]====================
copy = new ArrayList<>(phones);
copy.removeAll(sub);
System.out.println(copy);
copy.set(1, new Honor()); // replace an element
copy.addAll(2, sub); // 在指定索引处插入集合
System.out.println("before clear phones is empty:" + phones.isEmpty());
phones.clear();
System.out.println("clear phones->" + phones);
System.out.println("after clear phones is empty:" + phones.isEmpty());
phones.addAll(Phone.list());
Object[] objects = phones.toArray();
System.out.println(objects[3]);
Phone[] ph = phones.toArray(new Phone[0]);
System.out.println(ph[3]);
/*
输出:
[IPhone, Honor, HuaWei]
before clear phones is empty:false
clear phones->[]
after clear phones is empty:true
Honor
Honor
*/
}
}
[1]:当向List的实现ArrayList集合中插入元素时,能够动态增减大小(自动扩容调整索引),contains方法判断指定的对象是否在集合内,remove是一个重载方法,可以根据对象删除,也可以根据索引删除。
[2]:如果集合中已存在一个HuaWei对象,在没有将新对象HuaWei加入到集合中之前,删除这个新对象,查看是否会影响集合中的HuaWei对象,这是不会影响原集合的,尽管在认知上认为是同一个。contains行为依赖于equals方法。下面会介绍依赖于equals()
的点。
[3]:subList()
方法可以轻松地从更大的列表中创建切片,注意这里不包括边界,当将切片结果传递给原来这个较大的列表的 containsAll()
方法时,很自然地会得到 true。请注意,顺序并不重要,在 sub 上调用直观命名的 Collections.sort()
和 Collections.shuffle()
方法,不会影响 containsAll()
的结果。 subList()
所产生的列表的幕后支持就是原始列表,sub只持有原始列表的部分引用。
retainAll()
方法实际上是一个“集合交集”操作,在本例中,它保留了同时在 copy 和 sub 中的所有元素。请再次注意,所产生的结果行为依赖于 equals()
方法。
[3.1]、[3.2]处的代码,展示了集合是**“持有对象引用”**的,集合对象变了,但是集合中数据元素的对象引用并没有发生变化,copy、sub集合里面的对象引用和phone中的对象引用是相同的。
[4]:removeAll()
方法也是基于 equals()
方法运行的。 顾名思义,它会从 List 中删除在参数 List 中的所有元素。可以通过set()
方法替换指定索引处的元素值,clear()
用于清空集合中的元素**(清空了集合中持有的对象引用)**,isEmpty()
判断集合中是否含有对象引用(元素)。对于 List ,有一个重载的 addAll()
方法可以将新列表插入到原始列表的中间位置,而不是仅能用 Collection 的 addAll()
方法将其追加到列表的末尾。
toArray()
方法将任意的 Collection 转换为数组。这是一个重载方法,其无参版本返回一个 Object 数组,但是如果将目标类型的数组传递给这个重载版本,那么它会生成一个指定类型的数组(假设它通过了类型检查)。如果参数数组太小而无法容纳 List 中的所有元素(就像本例一样),则 toArray()
会创建一个具有合适尺寸的新数组
依赖于equals方法?持有对象引用?集合常见误区
1、依赖于equals
方法
当确定元素是否是属于某个 List ,寻找某个元素的索引,以及通过引用从 List 中删除元素时,都会用到 equals()
方法(根类 Object 的一个方法),如List.indexOf(Object obj)、List.contains(Object obj)、List.containsAll(List lists)、List.remove(Object obj)、List.removeAll(List lists)、List.retainAll(List lists)
。上面的HuaWei的例子也可以说明,新生成的HuaWei对象,当调用contains()
方法时,怎么知道集合是否包含HuaWei对象,底层就是通过调用对象的equals()
判断是否包含,因为类中都没有重写equals()
方法,所以默认调用的是父类中的equals()
(判断地址),所以当对新实例HuaWei调用indexOf时,就会返回 -1 (表示未找到),或者调用remove就会返回false。如果我们重写了 equals()
,那么结果就会有所不同。
对于其他类,
equals()
的定义可能有所不同。例如,如果两个 String 的内容相同,则这两个 String 相等。因此,为了防止出现意外,请务必注意 List 行为会根据equals()
行为而发生变化。
@Test
public void testList() {
List<String> strings = new ArrayList<>(Arrays.asList("Long", "Abc", "Qwe"));
strings.add("Long");
System.out.println(Arrays.toString(strings.toArray()));
System.out.println(strings.remove("Long"));
//System.out.println(strings.remove(new String("Long"))); 等同于上面
System.out.println(Arrays.toString(strings.toArray()));
System.out.println(strings.remove("Long"));
System.out.println(Arrays.toString(strings.toArray()));
/*
输出:
[Long, Abc, Qwe, Long]
true
[Abc, Qwe, Long]
true
[Abc, Qwe]
*/
}
从上面结果我们就可以看出,一String类重写了equals方法,所以remove方法看的效果和之前是不一样的;二因为集合中有两个与"Long"相等的数据元素,默认是从第一个开始处理的,不仅仅是对于remove
还有contains
等等都会处理第一个出现的元素。
2、持有对象引用
public static void main(String[] args) {
// 持有对象引用思想
List<Phone> phones = Phone.list();
System.out.println(phones);
// copy集合也保存了phones集合中的所持有的对象引用,
// 注意仅仅是保存了一组地址值在集合中,并不是保存了数据对象。
// 注意理解 对象引用的概念。
ArrayList<Phone> copy = new ArrayList<>(phones);
copy.clear();
System.out.println("copy->" + copy);
// copy仅仅是清空了集合中保存的地址值,并没有销毁对象,只是不在持有对象引用
// phones并没有清空引用值,所以说phone还是保留着手机对象的引用值。
System.out.println("phones->" + phones);
// 还是对象引用的概念,这里不再阐述。
Phone phone = phones.get(0);
System.out.println(phone.name);
copy = new ArrayList<>(phones);
Phone copyPhone = copy.get(0);
copyPhone.name = "Nokia";
System.out.println(phone.name);
}
3、集合常见误区?
Phone类中存在一个静态方法
static List<Phone> list() {
// Exception in thread "main" java.lang.UnsupportedOperationException
//return Arrays.asList(new HuaWei(), new Nova(), new P40(), new Honor(), new IPhone(), new OnePlus());[1]
return new ArrayList<>(Arrays.asList(new HuaWei(), new Nova(), new P40(), new Honor(), new IPhone(), new OnePlus()));
}
当使用[1]构造的集合列表,若之后对该集合列表进行add或者remove操作就会引发java.lang.UnsupportedOperationException
,这是为什么呢?来看一下Arrays.asList(T... a)
底层源码:
@SafeVarargs
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
方法返回的ArrayList,ArrayList集合不可以动态扩容吗?这就很奇怪了,当仔细观察,发现ArrayList并不是java.util.ArrayList
,而是java.util.Arrays.ArrayList
,属于Arrays
的一个私有内部类,继承了AbstractList
并重写了一些方法,add和remove方法并没有重写,那么默认会调用父类AbstractList
的方法,AbstractList
抽象类的add和remove的方法体就是抛出异常,所以说这就是为什么对Arrays.asList(T... a)
的结果进行写操作时会引发异常。另一方面来说,java.util.Arrays.ArrayList
底层是数组来存储值的,由于add和remove这两个方法会尝试修改数组大小,所以会在运行时得到“Unsupported Operation(不支持的操作)”错误:
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
7.4.2 LinkedList(链表)
数组和数组列表都有一个重大的缺陷,当从数组的中间位置删除一个元素要付出很大的代价,因为数组中处于被删除元素之后的所有元素都要向数组的前端移动,如果数据量大的话,这是十分耗时的。Java中的链表解决了这个问题,链表将对象存放在独立的结点中,每个结点保留着下一个结点的引用。
LinkedList底层结构就是链表,它实现了基本的List接口,它在List中间执行插入和删除时比ArrayList更高效,但随机访问操作效率不及ArrayList。
在Java中,所有链表实际上都是双向链表(doubly linked)—每个结点还存放着指向前驱结点的引用。
LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列(deque) 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 Queue 中)。例如:
getFirst()
和element()
是相同的,element()
底层就是调用的getFirst()
,它们都返回列表的头部(第一个元素)而并不删除它,如果 List 为空,则抛出 NoSuchElementException 异常。peek()
方法与这两个方法只是稍有差异,它在列表为空时返回 null 。removeFirst()
和remove()
也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 NoSuchElementException 异常。poll()
稍有差异,它在列表为空时返回 null 。addFirst()
在列表的开头插入一个元素。offer()
与add()
和addLast()
相同。 它们都在列表的尾部(末尾)添加一个元素。removeLast()
删除并返回列表的最后一个元素。
示例:
public class LinkedListTest {
public static void main(String[] args) {
LinkedList<Phone> phones = new LinkedList<>(Phone.list());
System.out.println(phones);
// 获取第一个元素,不同点是对empty-list的行为不同
System.out.println("getFirst:" + phones.getFirst());
System.out.println("element:" + phones.element());
System.out.println("peek:" + phones.peek());
// 删除并返回删除的元素
System.out.println("phones.remove():" + phones.remove());// 底层通过removeFirst删除
System.out.println("phones.removeFirst():" + phones.removeFirst());
System.out.println("phones.poll():" + phones.poll());
System.out.println(phones);
// 在列表头插入一个元素
phones.addFirst(Phone.get());
System.out.println("After addFirst():" + phones);
// 在列表尾插入元素 offer add addLast
phones.offer(Phone.get());
System.out.println("After offer():" + phones);
phones.add(Phone.get());
System.out.println("After add():" + phones);
phones.addLast(new Honor20());
System.out.println("After addLast(): " + phones);
/*
输出:
[HuaWei, Nova, P40, Honor, IPhone, OnePlus]
getFirst:HuaWei
element:HuaWei
peek:HuaWei
phones.remove():HuaWei
phones.removeFirst():Nova
phones.poll():P40
[Honor, IPhone, OnePlus]
After addFirst():[OnePlus, Honor, IPhone, OnePlus]
After offer():[OnePlus, Honor, IPhone, OnePlus, IPhone]
After add():[OnePlus, Honor, IPhone, OnePlus, IPhone, IPhone]
After addLast(): [OnePlus, Honor, IPhone, OnePlus, IPhone, IPhone, Honor20]
*/
}
}
7.5 Iterator
迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不知道或不关心该序列的底层结构。另外迭代器通常称为轻量级对象(lightweight object):创建它的代价小。Java的Iterator只能单向移动。
Iterator接口源码:
public interface Iterator<E> {
/*检查序列中是否还有元素*/
boolean hasNext();
/*获得序列中的下一个元素*/
E next();
/*将迭代器最近返回的那个元素删除*/
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
Iterator的简单使用
示例一:
public class IteratorTest {
public static void main(String[] args) {
// Iterator遍历元素
List<Phone> phones = Phone.list();
Iterator<Phone> it = phones.iterator();
while (it.hasNext()) {
Phone p = it.next();
System.out.print(p + " ");
}
System.out.println();
// for-each增强for循环,Collection接口扩展了Iterable接口,
// 对于任何实现了Collection接口的类都使用for-each循环
for (Phone p : phones) {
System.out.print(p + " ");
}
System.out.println();
// 利用Iterator删除元素
it = phones.iterator();
for (int i = 0; i < 3; i++) {
it.next();
it.remove();
}
System.out.println(phones);
/*
输出
HuaWei Nova P40 Honor IPhone OnePlus
HuaWei Nova P40 Honor IPhone OnePlus
[Honor, IPhone, OnePlus]
*/
}
}
根据示例一,可得知,有了Iterator,遍历元素时,我们不在关心集合的数量,会由hasNext()
和next()
帮我们处理。Iterator可以删除next()
生成的最后一个元素,需要注意,必须在next之后调用remove()
,至于为什么,下面会介绍。
示例二:
public class IteratorTestTwo {
public static void display(Iterator<Phone> it) {
while(it.hasNext()) {
Phone p = it.next();
System.out.print(p + " ");
}
System.out.println();
}
// 更通用的方法
public static void display(Iterable<Phone> iterable) {
iterable.forEach(System.out::print);
}
public static void main(String[] args) {
List<Phone> phones = Phone.list();
LinkedList<Phone> phonesLL = new LinkedList<>(phones);
HashSet<Phone> phonesHS = new HashSet<>(phones);
// 注意这里需要之前的Phone类实现Comparable接口,因为TreeSet需要比较然后按元素顺序排序
TreeSet<Phone> phonesTS = new TreeSet<>(phones);
display(phones.iterator());
display(phonesLL.iterator());
display(phonesHS.iterator());
display(phonesTS.iterator());
/*
输出:
HuaWei Nova P40 Honor IPhone OnePlus
HuaWei Nova P40 Honor IPhone OnePlus
P40 OnePlus IPhone HuaWei Honor Nova
Honor HuaWei IPhone Nova OnePlus P40
*/
display(phones); // List间接继承了Iterable接口,对于其他集合序列也一样
}
}
示例二展示了我们无需知道具体序列的类型,Iterator将遍历序列的操作与该序列的底层结构分离,或者说迭代器统一了对集合的访问方式。另外display
是一个重载方法,形参是Iterable
类型的,Iterable
可以产生Iterator
的任何方法,并且它还有一个forEach
默认方法。使用它对集合的访问显得更简单,可以直接通过display(phones)
就可以访问,因为集合都实现了Collection
,而它又扩展了Iterable
,间接继承。
Collection
类扩展了Iterable
接口,而Iterable
接口提供了获取一个Iterator
对象的方法,所以对于任何集合,都可以获取它的Iterator
对象。
7.5.1 ListIterator
ListIterator是一个更强大的Iterator子类型,它只能由各种List类生成。Iterator 只能向前移动,而 ListIterator 可以双向移动。它可以生成迭代器在列表中指向位置的后一个和前一个元素的索引,并且支持修改集合中的元素。可以通过调用集合实现类中的 listIterator()
方法来生成指向 List 开头处的 ListIterator ,还可以通过调用 listIterator(n)
创建一个一开始就指向列表索引号为 n 的元素处的 ListIterator 。
ListIterator源码
public interface ListIterator<E> extends Iterator<E> {
// Query Operations
boolean hasNext();
E next();
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
// Modification Operations
void remove();
/*set方法用一个新元素取代调用next或previous方法返回的上一个元素*/
void set(E e);
void add(E e);
}
示例:
public static void main(String[] args) {
List<Phone> phones = Phone.list();
ListIterator<Phone> it = phones.listIterator();
while (it.hasNext()) {
System.out.println(it.next() + ",nextIndex:" + it.nextIndex() + ",previousIndex:" + it.previousIndex()+";");
}
// 从后往前遍历
System.out.print("reverse traverse->" );
while (it.hasPrevious()) {
System.out.print(it.previous() + " ");
}
System.out.println();
System.out.println(phones);
it = phones.listIterator(3);
// 获得从索引3处开始的ListIterator对象
while (it.hasNext()) {
it.next();
// get()会随机得到一个Phone对象
it.set(Phone.get());
}
// 在集合尾部添加一个元素
it.add(Phone.get());
System.out.println(phones);
/*
输出:
HuaWei,nextIndex:1,previousIndex:0;
Nova,nextIndex:2,previousIndex:1;
P40,nextIndex:3,previousIndex:2;
Honor,nextIndex:4,previousIndex:3;
IPhone,nextIndex:5,previousIndex:4;
OnePlus,nextIndex:6,previousIndex:5;
reverse traverse->OnePlus IPhone Honor P40 Nova HuaWei
[HuaWei, Nova, P40, Honor, IPhone, OnePlus]
[HuaWei, Nova, P40, OnePlus, IPhone, P40, Honor]
*/
}
ListIterator
是一个接口,ArrayList
没有实现却能返回这个接口的对象?
底层发现ArrayList
并没有直接实现ListIterator
,有点和Arrays类似,也是通过一个私有匿名内部类间接实现ListIterator
,所以说就能获得该对象。
看源码!
7.5.2 迭代器注意点
7.5.2.1 迭代器解析
Java迭代器的查找操作和位置变更是紧密相连的。只能顺序next()
或者反序previous()
依次遍历。不能像get(index)那样随机访问。
因此,应该讲Java迭代器认为是位于两个元素之间。当调用next或者previous,迭代器就越过下一个元素或者上一个元素,并返回刚刚越过的那个元素的引用。
Iterator
的next方法和remove方法的调用具有相互依赖性。如果调用remove之前没有调用next将是不合法的。否则就会抛出IllegalStateException
。对于previous同样道理。
List<String> strings = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));
ListIterator<String> it = strings.listIterator(3);
while (it.hasPrevious()) {
it.hasPrevious();
it.remove();
// 不可再次调用,只消耗刚刚返回的那个元素
// it.remove();
}
这样做有什么好处,我所理解到的是避免了一定的死循环,比如
List<String> strings = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));
ListIterator<String> it = strings.listIterator();
// [1]没有死循环
while (it.hasNext()) {
it.next();
it.add("11");
}
System.out.println(Arrays.toString(strings.toArray()));
// 输出:[aaa, 11, bbb, 11, ccc, 11]
// [2]下面就会出现死循环
while (it.hasPrevious()) {
it.previous();
it.add("22");
}
System.out.println(Arrays.toString(strings.toArray()));
// 死循环
[1]如果it指针指向了当前索引而不是当前元素和下一个元素的中间位置,那么上面就会造成死循环。因为遍历的总是插入的前一个元素
[2]为什么会死循环?看源码给予了解答:
/*
*Inserts the specified element into the list (optional operation).
* The element is inserted immediately before the element that
* would be returned by {@link #next}, if any, and after the element
* that would be returned by {@link #previous}, if any
*/
插入元素在调用next()
方法返回的元素之前(如果有的话),或者调用previous()
在返回的元素之后插入(如果有的话)。当调用previous()
后,然后add()
总是会在光标(指针所在位置)之后插入,所以就会导致插入的元素总是在光标之后,从而导致了死循环。下图解释了这一现象:
7.5.2.2 多个迭代器修改访问异常
ConcurrentModificationException
得益于集合的fail-fast快速失败机制,当使用迭代器对集合进行遍历的时候,会检查expectModcount和modcount这两个变量值是否相等,如果不相等则抛出异常。
List<String> strings = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));
ListIterator<String> it1 = strings.listIterator();
ListIterator<String> it2 = strings.listIterator();
it1.next();
it1.remove();
it2.next();
// Exception in thread "main" java.util.ConcurrentModificationException
1)如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状态。如上,如果一个迭代器指向另一个迭代器刚刚删除的元素前,现在这个迭代器就是无效的,并且不应该在使用。否则抛出ConcurrentModificationException
。
2)在迭代元素的时候不能通过集合List
中的方法删除元素, 否则会抛出ConcurrentModificationException 异常. 但是可以通过Iterator接口中的remove()方法进行删除,Iterator接口中的remove()
底层是将modCount
修改为expectedModCount
。
fail-fast&fail-safe
快速失败机制和安全失败机制的区别?
Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。
7.6 集合Set
复习散列表的知识 —数据结构与算法
Set是没有重复元素的元素集合。set的add方法首先在集中查找要添加的对象,如果不存在,就将这个对象添加进去。set没有get方法,因为其元素是无序的,所以说遍历只能用迭代器。
7.6.1 HashSet
Java集合类库提供了一个HashSet类,它实现了基于散列表的集。可以用add方法添加元素。contains方法已经被重新定义,用来快速查看是否某个元素已经出现在集中。
下面是使用存放 Integer 对象的 HashSet 的示例:
public class HashSetTest {
public static void main(String[] args) {
Random random = new Random(47);
Set<Integer> intset = new HashSet<>();
for (int i = 0; i < 10000; i++)
intset.add(random.nextInt(30));
System.out.println(intset);
}
/*
输出:
[0, 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]
*/
}
在 0 到 29 之间的 10000 个随机整数被添加到 Set 中,因此可以想象每个值都重复了很多次。但是从结果中可以看到,每一个数只有一个实例出现在结果中。
7.6.2 TreeSet(树集)
TreeSet是一个有序集合(sorted collection)。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。
要使用树集,必须能够比较元素。这些元素必须实现Comparable接口,或者构造集时必须提供一个Comparator。
Set<String> set = new TreeSet<>();
set.add("Moon");
set.add("Mars");
set.add("Earth");
set.add("Jupiter");
System.out.println(set);// [Earth, Jupiter, Mars, Moon]
TODO 还需深入,这里只是介绍了如何使用
7.7 队列Queue
队列是一种先进先出(FIFO)的集合结构,队尾插入,队头删除。有两个端头的队列,即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。在JavaSE6中引入了Deque接口,由**ArrayDeque
和LinkedList
**类实现。
下面演示了队列的基本使用:
public static void main(String[] args) {
Queue<Integer> queue = new ArrayDeque<>(8);
for (int i = 0; i < 100; i++) {
queue.offer(i);
}
while (queue.peek() != null) {
System.out.println(queue.poll());
}
}
7.7.1 优先级队列
优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先级队列中最小的元素。
当在PriorityQueue上调用offer()
方法来插入一个对象时,该对象会在队列中被排序。默认的排序使用队列中对象的自然排序*自然排序*(natural order),但是可以通过提供自己的Comparator来修改这个顺序。PriorityQueue确保在调用 peek()
, poll()
或 remove()
方法时,获得的元素将是队列中优先级最高的元素。
习惯上将1设为“最高”优先级,所以数字越小优先级越高。
public class QueueTest {
public static void printQ(Queue queue) {
while(queue.peek() != null)
System.out.print(queue.remove() + " ");
System.out.println();
}
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
Random rand = new Random(47);
for(int i = 0; i < 10; i++)
priorityQueue.offer(rand.nextInt(i + 10));
printQ(priorityQueue);
List<Integer> inst = Arrays.asList(25, 22, 20, 18, 14, 9, 3, 1, 1);
priorityQueue = new PriorityQueue<>(inst);
printQ(priorityQueue);
priorityQueue = new PriorityQueue<>(inst.size(), Collections.reverseOrder());
priorityQueue.addAll(inst);
printQ(priorityQueue);
/*输出:
0 1 1 1 1 1 3 5 8 14
1 1 3 9 14 18 20 22 25
25 22 20 18 14 9 3 1 1
*/
}
}
PriorityQueue 是允许重复的,最小的值具有最高的优先级(如果是 String ,空格也可以算作值,并且比字母的优先级高)。
为了展示如何通过提供自己的 Comparator 对象来改变顺序,第三个对 PriorityQueue 构造器的调用,和第二个对 PriorityQueue 的调用使用了由 Collections.reverseOrder()
(Java 5 中新添加的)产生的反序的 Comparator 。
Integer , String 和 Character 可以与 PriorityQueue 一起使用,因为这些类已经内置了自然排序。如果想在 PriorityQueue 中使用自己的类,则必须包含额外的功能以产生自然排序,或者必须提供自己的 Comparator 。
TODO for-each iterable
7.8 映射
映射提供了一种根据键来查找值的方案。映射存放键/值对。如果提供了键,就能够查找到值。
Java类库为映射提供了两个通用的实现:HashMap和TreeMap。
散列映射对键进行散列,树映射用键的整体顺序对元素进行排序,并将其组织成搜索树。散列稍微快一些,如果不需要按照排列顺序访问键,最好选用散列。
7.8.1 基本映射操作
(1)散列中键唯一,键值可以为null,但键值只有一个为null,如果对同一个键两次调用put方法,第二个值就会取代第一个值。put将 返回用这个键参数存储的上一个值。
(2)可以使用forEach
迭代遍历映射中的键和值。
下面演示了映射的基本操作:
public class MapTest {
public static void main(String[] args) {
Map<String, Employee> staff = new HashMap<>();
staff.put("1001", new Employee("Tomes"));
staff.put("1002", new Employee("Andrew"));
staff.put("1003", new Employee("Jase"));
// print all entries
System.out.println(staff);
// remove an entry
staff.remove("1002");
// replace
staff.put("1003", new Employee("Jasen"));
System.out.println(staff.get("1003"));
System.out.println(staff.getOrDefault("1002", null));
// iterate
staff.forEach((k, v)-> {
System.out.println("key=" + k + "value" + v);
});
}
}
deafult V getOrDefault(Object key, V defaultValue)
获得与键关联的值,如果未在映射中中找到这个键,则返回defaultValue.
containsKey(Object key)
containsValue(Object value)
default void forEach(BiConsumer<? super K, ? super V> action)
7.8.2 更新映射项
现在我们要统计一个单词出现的频度, 当出现一个单词(word)时,我们让计数器增一:
Map<String, Integer> counts = new HashMap<>();
counts.put("eldest", counts.get("eldest") + 1);
考虑运行上面程序会出现上面,java.lang.NullPointerException
,当第一次get时,还没有添加该单词,导致获取的为值为空,然后还有一步自动装箱的动作,空指针异常就是发生在装箱时。所以说对于有些空值需要特殊处理,上面可以采用以下几种解决方案:
// 第一种方案
counts.put("eldest", num == null? 1 : num + 1);
// 第二种方案
counts.putIfAbsent("eldest", 0);
counts.put("eldest", num + 1);
// 第三种方案
counts.merge("eldest", 1, Integer::sum);
8. 线程
参考:
[Matrix海子]https://www.cnblogs.com/dolphin0520/p/3923737.html
[codercc]http://www.codercc.com/backend/basic/juc/
hollischuang.com/archives/category/java
—第一篇基础篇—
进程的由来
早期,计算机只能接受一些特定的指令,用户输入一个指令,计算机就做一个操作,当用户在输入数据或者思考问题的时候,计算机就在等待,显然在用户输入数据或者思考的这个过程中,CPU只能等待,浪费了资源。这就是早期的单道处理系统。
如果能把一系列需要的操作指令都预先准备好,然后一次性交给计算机进行运算,这样CPU资源能得到充分利用,批处理系统就诞生了。
但是批处理系统任然存在很大的问题,比如计算机中预执行两个任务A和B,A先得到执行,一段时间后A任务进行IO操作,A任务需要将结果写到其他存储介质上,写的过程(排除写结束和写开始)很少需要CPU的干预,这时候CPU是空闲的,如果让CPU在A写时去执行B,当A写完后通知CPU,再去执行A,CPU的资源就能得到充分的利用。以上存在的问题,计算机是如何知道当前CPU运行的是A还是B,当A写完,挂起B,重新执行A时,CPU怎么知道执行到A的哪个地方了呢?
因此,人们就发明了进程,用进程来对应每一个程序,具体来说,进程都会有自己的数据结构,进程ID可以标识一个程序。当进程(程序)之间切换的时候,当进程暂停时,它会保存当前进程的状态(比如进程标识、进程的使用的资源等),在下一次重新切换回来时,便根据之前保存的状态进行恢复,然后继续执行。
这就是并发的雏形,宏观上有多个任务同时执行,微观上还是交替执行,任意一个时刻只有一个任务占领CPU(单核CPU来说)。
进程然操作系统的并发成为可能。
线程的由来
进程虽然提升了系统的运行效率,但随着计算机的发展,人们对实时性有了要求。因为一个进程可能会有多个子任务,在没有线程时,只能挨个执行各个的子任务。比如用户点击一个按钮进行网络请求获取信息,在后台没有响应之前,用户只能在响应之后才能进行任何操作,用户体验很差。但是如果有多线程将进程分为多个任务,每个线程都可以各司其职。当用户发起一个请求后,网络请求的线程去请求后台信息,当前可以继续进行做其他的操作。
线程让进程内部的并发成为可能。
一个进程可能包括多个线程,这些线程是共享进程占有的资源和地址空间的。进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位。
多线程入门类和接口
Thread类和Runnable接口
1、继承自Thread类
public class Demo {
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
}
}
注意点:
- run方法仅是执行里面的任务,并不会开启新线程,只有调用
start()
方法之后,线程才算启动成功。 - 线程开启后,不可多次调用
start()
方法,否则抛出IllegalThreadStateException
异常。 - 在程序里面调用了start()方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用run()方法。
2、实现Runnable接口
Runnable
接口是一个函数式接口,我们可以使用Java 8的函数式编程简化代码。
public class Demo {
public static class MyThread implements Runnable {
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
new MyThread().start();
// Java 8 函数式编程,可以省略MyThread类
new Thread(() -> {
System.out.println("Java 8 匿名内部类");
}).start();
}
}
3、Thread类
/**
* Thread类实现了Runnable接口,通过在构造函数中调用init初始化线程
*/
public class Thread implements Runnable {
// Thread类源码
// 片段1 - init方法
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals)
// 片段2 - 构造函数调用init方法
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// 片段3 - 使用在init方法里初始化AccessControlContext类型的私有属性
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
// 片段4 - 两个对用于支持ThreadLocal的私有属性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
init
方法的参数介绍:
- g:线程组,指定这个线程是在哪个线程组下。
- target:指定要执行的任务对象。
- name:线程的名字,多个线程的名字可以重复。如果不指定名字,则会调用
nextThreadNum()
返回一个值。
在main方法里,我们开启两个线程,打印这两个线程的对象默认会有以下输出
Thread[Thread-0,5,main]
Thread[Thread-1,5,main]
#Thread-0:就是当前线程的名字,5表示线程的优先级,main表示线程所属组
Thread-num num是递增的,原因就是nextThreadNum每次返回一个递增的值
通常情况下,我们会直接调用下面两个构造方法:
Thread(Runnable target)
Thread(Runnable target, String name)
Thread类的几个常用方法:
-
currentThread():静态方法,返回对当前正在执行的线程对象的引用;
-
start():开始执行线程的方法,java虚拟机会调用线程内的run()方法;
-
yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的;
-
sleep():静态方法,使当前线程睡眠一段时间;
-
join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait方法实现的;
Callable、Future与FutureTask
通常来说,我们使用Runnable
和Thread
来创建一个新的线程。但是它们有一个弊端,就是run
方法是没有返回值的。而有时候我们希望开启一个线程去执行一个任务,并且这个任务执行完成后有一个返回值。
JDK提供了Callable
接口与Future
类为我们解决这个问题,这也是所谓的“异步”模型。
1、Callable接口
Callable
一般是配合线程池工具ExecutorService
来使用的。这里只介绍ExecutorService
可以使用submit
方法来让一个Callable
接口执行。它会返回一个Future
,我们后续的程序可以通过这个Future
的get
方法得到结果。
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
使用Callable接口:
// 自定义Callable
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模拟计算需要一秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]){
// 使用
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
Future<Integer> result = executor.submit(task);
// 注意调用get方法会阻塞当前线程,直到得到结果。
// 所以实际编码中建议使用可以设置超时时间的重载get方法。
System.out.println(result.get());
}
}
2、Future接口
package java.util.concurrent;
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
cancel
方法是试图取消一个线程的执行。
注意是试图取消,并不一定能取消成功。因为任务可能已完成、已取消、或者一些其它因素不能取消,存在取消失败的可能。boolean
类型的返回值是“是否取消成功”的意思。参数paramBoolean
表示是否采用中断的方式取消线程执行。
所以有时候,为了让任务有能够取消的功能,就使用Callable
来代替Runnable
。如果为了可取消性而使用 Future
但又不提供可用的结果,则可以声明 Future
形式类型、并返回 null
作为底层任务的结果。
3、FutrueTask类
FutureTask
是实现的RunnableFuture
接口的,而RunnableFuture
接口同时继承了Runnable
接口和Future
接口:
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
那FutureTask
类有什么用?为什么要有一个FutureTask
类?前面说到了Future
只是一个接口,而它里面的cancel
,get
,isDone
等方法要自己实现起来都是非常复杂的。所以JDK提供了一个FutureTask
类来供我们使用。
示例:
// 自定义Callable,与上面一样
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模拟计算需要一秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]){
// 使用
ExecutorService executor = Executors.newCachedThreadPool();
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
executor.submit(futureTask);
System.out.println(futureTask.get());
}
}
使用上与第一个Demo有一点小的区别。首先,调用submit
方法是没有返回值的。这里实际上是调用的submit(Runnable task)
方法,而上面的Demo,调用的是submit(Callable task)
方法。
然后,这里是使用FutureTask
直接取get
取值,而上面的Demo是通过submit
方法返回的Future
去取值。
在很多高并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能够在高并发环境下确保任务只执行一次。
4、FutureTask的几个状态
/**
*
* state可能的状态转变路径如下:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
state表示任务的运行状态,初始状态为NEW。运行状态只会在set、setException、cancel方法中终止。COMPLETING、INTERRUPTING是任务完成后的瞬时状态。
线程的生命周期
线程的生命周期包含5个阶段:新建、就绪、运行、阻塞、死亡。
新建状态:创建一个线程对象后,线程就处于新建状态,此时该线程和普通的Java对象一样仅仅是分配了内存空间,并没有表现出线程的任何动态特性。
就绪状态:当线程对象调用start()
方法后,线程就处于就绪状态,JVM会为其创建方法调用栈和程序计数器。处于就绪状态的线程并不会立即得到执行,此时,它只是具备了运行的条件,至于什么时候运行,则取决于JVM里线程调度器的调度。所以线程的执行是由底层平台控制, 具有一定的随机性。
运行状态:处于就绪状态的线程,得到CPU的使用权,就会执行run()
方法,线程就处于运行状态。在单处理器机器上,只能有一个线程运行;在多处理器机器上,每个处理器运行一个线程,可以有多个线程并行运行,如果进程的数目多于处理器的数目,调度器依然采用时间片机制。
阻塞状态:正在执行的线程由于某些原因暂停程序的执行,放弃处理机处于暂停状态。比如调用sleep
或者wait
方法后,程序就处于阻塞状态了,处于阻塞状态的线程需要唤醒,唤醒后的线程并不会立即运行,需要再经历就绪状态然后获得CPU后执行。
死亡:线程的run
方法执行完毕,者线程在执行过程中发生了异常或者错误或者调用了stop()方法,线程就处于死亡状态。
上下文切换
对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。
如果当前线程的任务没有执行完毕而将当前线程切出,将另外的线程切入,此时需要保存切出线程的运行时状态信息,以便下次能够再次切入的时候能继续执行上次运行到的地方。
那么需要保存哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
线程的上下文切换实际上就是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
中断线程
1、interrupt()
,中断这个线程,如果这个线程因调用wait
、join
、sleep
方法而阻塞,在调用该中断方法后会抛出InterruptedException
,并且会清除中断状态(具体表现就是调用isInterrupted
结果为true),其实这是Java提供了一种委婉的方式用来中断线程,停止线程运行的一种方式。
如果线程在等待锁,对线程对象调用interrupt()只是会设置线程的中断标志位,并不会阻止线程继续等待锁,线程依然会处于BLOCKED状态,也就是说,interrupt()并不能使一个在等待锁的线程真正”中断”。
public void interrupt()
2、interrupted()
方法,静态方法,判断当前线程,是否被中断。并且会清除中断状态,连续的两次调用将会返回false,除非当第一次调用后又一次被中断。
public static boolean interrupted() {
return currentThread().isInterrupted(true);//当前线程
}
3、isInterrupted()
方法,判断此线程是否被中断,不会清除中断状态。
public boolean isInterrupted() {
return isInterrupted(false);
}
清除中断状态或者重置中断状态位表现就是当调用中断判断函数判断当前线程是否是中断的时候,会返回false表示当前线程没有被中断。
示例:
public static void main(String[] args) {
Thread.currentThread().interrupt();// 中断main-thread
System.out.println(Thread.currentThread().isInterrupted());//true
//第二次调用isInterrupted()结果为true,说明该方法不会清除中断状态位
System.out.println(Thread.currentThread().isInterrupted());//true
}
interrupt()
方法会中断线程但不会停止线程的运行,如果遇到InterruptedException
异常,则会重置中断状态位。看如下示例:
class ThreadA extends Thread{
@Override
public void run() {
while (true) {
try {
System.out.println("interrupted:" + Thread.currentThread().isInterrupted() + " ThreadA run..."); //[2]
// Thread.sleep(3000); //[3]
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
ThreadA threadA = new ThreadA();
threadA.start();
threadA.interrupt();//[1]
}
}
当在[1]处调用中断函数使线程中断后,[2]处输出结果为true并且线程还在继续运行。
如果放开[3]处的注释语句,当再次运行程序时,刚开始输出为true,但是因为Thread.sleep()会阻塞线程,当调用中断函数时就会抛出异常并且重置中断状态位。
interrupted:true ThreadA run...
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.wjx.corejava1.thread.ThreadA.run(ThreadTest.java:19)
interrupted:false ThreadA run...
interrupted:false ThreadA run...
....false...
interrupted()
方法的应用,interrupted判断的是当前线程是否被中断。
ThreadA threadA = new ThreadA();
threadA.start();
threadA.interrupt();
// interrupted判断的是当前线程,对于threadA.interrupted()
// 来说是main线程运行了这一句,所以说判断的是main线程的中断状态位
System.out.println(threadA.interrupted()); //false
// 第一次执行,线程没有执行任何中断操作,返回结果为false
System.out.println(Thread.interrupted());
Thread.currentThread().interrupt();
// 中断main-thread之后执行,返回结果为true,因为中断了main线程
System.out.println(Thread.interrupted());//true
// 因为Thread.interrupted()执行后会清除状态位,所以说
// 第三次执行结果为false
System.out.println(Thread.interrupted());//false
线程安全问题
线程安全问题产生的原因
单线程操作任何时候都是这一个线程对数据资源操作,可以说数据的变化都是与该线程相关,所以不会出现线程安全问题。但是在多线程编程中,两个或两个以上的线程对共享数据的存取,因为线程的随机性,CPU的时间片轮转,抢占式调度算法来获得CPU的执行,可能会导致对共享数据的操作错误等问题。比如成员属性、共享变量、文件、数据库表等,由于每个线程执行的过程是不可控的,所以可能会导致数据可能和自己所预期的不一样或者直接导致程序出错。
这里面所操作的数据资源就被称为临界资源(共享资源)。
1.哪些是共享变量
在java程序中所有实例域,静态域和数组元素都是放在堆内存中(所有线程均可访问到,是可以共享的),而局部变量,方法定义参数和异常处理器参数不会在线程间共享。共享数据会出现线程安全的问题,而非共享数据不会出现线程安全的问题。关于JVM运行时内存区域在后面的文章会讲到。
所以说当多个线程同时访问临界资源是,就可能产生线程安全问题。
当多个线程执行一个方法,方法内部的局部变量并不是临界资源,因为方法是在栈上执行的,而Java栈是线程私有的,因此不会产生线程安全问题。
为了避免多线程引起的对共享数据的讹误,线程的操作必须同步存取。
示例代码:
public class UnsynchBankTest {
//账户数目
public static final int NACCOUNTS = 100;
//初始金额
public static final double INITIAL_BALANCE = 1000;
//最大转出金额
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10;
public static void main(String[] args) {
Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < NACCOUNTS; i++) {
int fromAccount = i;
Runnable r = () -> {
try {
Thread.sleep((int) (DELAY * Math.random()));
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread t = new Thread(r);
t.start();
}
}
}
class Bank {
private final double[] accounts;
public Bank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
}
/**
* 转账操作
*
* @param from 转出账户
* @param to 转入账户
* @param amount 转账金额
*/
public void transfer(int from, int to, double amount) {
if (accounts[from] < amount)
return;
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf("Total Balance %10.2f%n", getTotalBalance());
}
public double getTotalBalance() {
double sum = 0;
for (double m : accounts)
sum += m;
return sum;
}
public int size() {
return accounts.length;
}
}
上面多个线程对同一个bank对象内的共享数据进行了存取操作,我们不清楚在某一时刻某一银行账户中有多少钱。但是,所有账户的总金额应该保持不变或者银行下的总金额不变,因为存取不过就是从一个账户转移钱款到另一个账户。
程序的输出:
...
Thread[Thread-76,5,main]
74.44 from 76 to 25Total Balance 100000.00
Thread[Thread-76,5,main]
8.14 from 76 to 6Total Balance 100000.00
Thread[Thread-76,5,main]
3.71 from 76 to 6Total Balance 100000.00
Thread[Thread-76,5,main]
6.38 from 76 to 90Total Balance 100000.00
Thread[Thread-83,5,main]
Thread[Thread-76,5,main]
0.20 from 76 to 87Total Balance 99181.24
Thread[Thread-98,5,main]
28.25 from 98 to 62Total Balance 99181.24
...
不难发现,刚开始银行总金额不变,但是一会,银行里面的钱可能不翼而飞,发什么了变化,这在银行中是绝对不允许的。
当两个线程试图更新同一个账户,两个线程同时执行指令accounts[to] += amount
,问题在于这不是原子操作,该指令可能会被处理如下:
(1) 将accounts[to]加载到寄存器
(2) 增加amount
(3) 将结果写会accounts[to]
当一个线程在执行上述三个中的任意一个子操作时,都有可能在运行时被其他线程剥夺CPU的运行权,导致其他线程对数据做了操作(存、取)后,当这个线程再次运行时,数据的状态已经不在是这个线程一开始执行时的那种状态,但是线程并不知道数据的变化,最终会导致数据不一致,导致金额错误。
我们反编译代码来看一下:
account[from] -= amount;
反编译:
ALOAD 0
GETFIELD cn/wjx/corejava1/thread/Bank.accounts : [D
ILOAD 1 [load]
DUP2
DALOAD
DLOAD 3
DSUB [-]
DASTORE [store]
account[to] += amount;
ALOAD 0 [load]
GETFIELD cn/wjx/corejava1/thread/Bank.accounts : [D
ILOAD 2
DUP2
DALOAD
DLOAD 3
DADD [+]
DASTORE [store]
可以看出,账户存入和支出是由多个指令构成的,执行它们的线程可以在任何一条指令点上被中断。下面时序图展示了这一情况:
如何解决线程安全
基本上所有的并发模式在解决线程安全问题时,都采用序列化访问临界资源的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。
通常来说,当一个线程在访问临界资源时,在访问临界资源的代码前面加上一个锁,其他线程就不能访问加锁后的临界资源,只有当访问完临界资源释放锁后,其他线程才可以继续访问。通过锁来解决线程安全问题,实现对资源的互斥访问。
锁是一种抽象的概念,代码层面是如何实现的呢?
在Java中,每个对象都有一个锁(锁标记
),存在于对象头中。关于对象结构,会在下文介绍。结合synchronized
从而实现对一个临界资源的加锁解锁。
Java语言中提供了两种方式来实现同步互斥访问:synchronized
和Lock
。
synchronized
在Java中,可以使用synchronized关键字对方法或者代码块进行加锁,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。
synchronized方法或者代码块执行的条件,必须获得对象锁
synchronized使用场景:
synchronized注意点
1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。同一个对象锁只能被一个线程所持有。
2)当一个线程正在访问一个对象的synchronized方法,其他线程能访问该对象的synchronized方法。访问非synchronized方法不需要获得该对象的锁。
3)如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型,也不会产生线程安全问题,因为他们持有的锁对象不是同一个,所以不存在互斥问题。
4)synchronized代码块使用起来比synchronized方法更加灵活,因为一个方法可能只有一部分代码需要同步,如果加方法锁的话,会影响效率。
底层原理
synchronized总是依赖于对象,在Java中,每个对象都有一个锁标记(monitor
),也称为监视器或者管程,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。synchronized就是依赖于这个监视器来实现资源的同步访问的。
我们通过反编译一个实例来查看:
public class ThreadTest {
public static void main(String[] args) {
int i = 0;
synchronized (ThreadTest.class) {
i++;
}
}
}
synchronized代码块出现了两个特殊指令**monitorenter
和monitorexit
,monitorenter
指令执行时会让对象的锁计数加1,此时其他线程只能等待,而monitorexit
**指令执行时会让对象的锁计数减1,当锁计数为0时,其他线程才可以获得对象锁。其实这个与操作系统里面的PV操作很像,操作系统里面的PV操作就是用来控制多个线程对临界资源的访问。
monitor
Java对象的monitor保证了临界资源的互斥访问,那么这个monitor对象是个什么东东呢?
前面提到,monitor被叫做一个监视器或者是一个管程。就拿监视器这个含义来说,监视器主要是用来监视某种资源,转到Java多线程并发的含义就是用来管理多个线程互斥访问临界资源,保证每一个时刻只能有一个线程进入临界区。
无论是ACC_SYNCHRONIZED
还是monitorenter
、monitorexit
都是基于Monitor实现的,在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现。主要的数据结构如下:
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
关键属性:
_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放处于等待锁block状态的线程队列
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数
当多个线程同时访问一段同步代码时,首先会进入 _EntryList 队列中,当某个线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的**_owner** 变量设置为当前线程,同时monitor中的计数器 _count 加1,即获得对象锁。
若持有monitor的线程调用 wait() 方法,将释放当前持有的monitor, _owner 变量恢复为 null, _count减1,同时该线程进入 _WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图:
对于synchronized方法,执行中的线程识别该方法的 method_info 结构是否有**ACC_SYNCHRONIZED
**标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。
总结
方法级的同步是隐式的。同步方法的常量池中会有一个
ACC_SYNCHRONIZED
标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED
,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。同步代码块使用
monitorenter
和monitorexit
两个指令实现。可以把执行monitorenter
指令理解为加锁,执行monitorexit
理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter
)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit
指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。每个对象自身维护这一个被加锁次数的计数器,当计数器数字为0时表示可以被任意线程获得锁。当计数器不为0时,只有获得锁的线程才能再次获得锁。即可重入锁。
参考
https://www.jianshu.com/p/0668a1714c67
https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI3NzE0NjcwMg==&action=getalbum&album_id=1337208062180163585&scene=173#wechat_redirect
对于对象锁来说,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。。这就会涉及到核态的转化。
Java线程实际上是对操作系统线程的映射,每个线程的挂起和切换,都会涉及到内核态与用户态的切换,而状态的切换是十分耗时且耗费资源的。
如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。
锁标记是存在于对象头中的Markword中,理解这些锁状态的切换,就不得不对Java中的对象结构有所了解。
对象结构
Java对象结构由对象头、实例数据、对齐填充组成。
1、对象头
HotSpot虚拟机的对象头(Object Header)包括两部分信息:MarkWord和类型指针。
MarkWord:用于存储对象自身的运行时数据和锁标志, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits。
类型指针:即对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。
Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:
2、实例数据
实例数据保存对象中的属性和方法等信息。
3、对齐填充
由于HotSpot JVM要求对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数(1倍或者2倍),所以为了满足对象的大小是8字节的整数倍,需要对其进行一定字节的填充。它并没有什么特别含义,仅仅起着占位符的作用。
Java虚拟机的锁优化
🔖 🆕
- 四种锁状态
https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650121186&idx=1&sn=248d37be27d3bbeb103464b2a96a0ae4&chksm=f36bbec3c41c37d59277ac8539a616b65ec44637f341325056e98323e8780e09c6e4f7cc7a85&scene=21#wechat_redirect
Lock
synchronized缺陷
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
Lock比synchronized关键字提供了更多的功能,但是需要注意:
1)Lock
不是Java语言内置的,它是一个类,通过这个类可以实现同步访问,synchronized
是Java语言的关键字,是内置特性。
2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
locks包下常用类
lock接口
Lock是一个接口,其中lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的,newCondition
是与协程相关的,这里不做讲解。
public interface Lock {
/**
*lock is not available then the current thread becomes disabled for thread scheduling *purposes and lies dormant until the lock has been acquired.
*如果锁不可以用,当前线程由于调度的目的将会被禁用和休眠直到锁被获取
*/
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock()
:Lock接口中的常用方法,对代码块进行加锁,未获得锁的线程只能等待。
在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
使用Lock锁同步线程的方式如下,
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
**tryLock()
**方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,方法调用会立即得到一个返回值,而不是在那无休止的等待获取锁。
**tryLock(long time, TimeUnit unit)
**方法和tryLock()方法是类似的,区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
所以,一般情况下通过tryLock来获取锁时是这样使用的:
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
lockInterruptibly()
:当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。其实还是通过抛出异常的形式来中断线程的执行。
因为lockInterruptibly()
通过抛出异常的形式来中断线程,如果异常被内部捕获,那么最终会走unlock释放锁,对于一个没有获取锁的线程来说去调用释放锁的方法,会抛出IllegalMonitorStateException
。所以必须将异常抛出而不是被捕获在内部处理。
使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
当线程调用Thread.interrupt()方法时,lockInterruptibly和synchronized方法响应中断的表现形式是不同的:
当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
ReadWriteLock
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
读写锁接口,该接口提供了一对锁,能够支持并发读互斥写,在并发读方面,相比synchronized
方法,该接口下的readLock()
方法更有优势。
示例:使用ReentrantReadWriteLock实现同时读。
public class ThreadTest{
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private void fun(Thread thread) {
rw.readLock().lock();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在进行读操作");
}
System.out.println(thread.getName()+"读操作完毕");
} finally {
rw.readLock().unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ThreadTest t = new ThreadTest();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
t.fun(Thread.currentThread());
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
t.fun(Thread.currentThread());
}
});
t1.start();
t2.start();
}
}
Lock和synchronized的选择
总结来说,Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用unlock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
锁相关概念
1.可重入锁
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
看下面这段代码就明白了:
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁,死锁。
2.可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
3.公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
相关术语
程序进程线程
关于更多进程和线程的知识,最好看下计算机操作系统,里面从单处理系统到批处理,到为什么出现进程有很详细的介绍。
程序:用某种编程语言(java、c等)编写,能够完成一定任务或者功能的代码集合,是指令和数据的有序集合,是一段静态代码。
进程:应用程序获得内存中的空间,执行程序的过程,简单的说,一个进程就是一个在内存中执行的程序。执行中各个进程之间互不干扰。同时进程保存着程序每一个时刻运行的状态。每一个进程执行都有一个执行顺序,该顺序是一个执行单元或者叫一个控制单元。
线程: 是进程中的一个独立控制单元,控制着进程的执行,比进程更小的执行单位。一个进程中至少有一个线程,线程之间数据共享(共享同一块内存空间和系统资源)。
**多线程:**一个程序(进程)运行时产生了不止一个线程。
并发与并行
并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。
并发执行和并行执行的区别?
并行执行真正意义上的同时执行,并发执行指的是重叠时间段内的执行。下图就很好的解释了这两者
“并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。并行是并发的一种实现方案,另一种实现方案是协程。—知乎
Different concurrent designs enable different ways to parallelize.
并发设计让并发执行成为可能,而并行是并发执行的一种模式。
上下文切换
上下文切换:上下文切换(有时也称做进程切换或任务切换)是指 CPU 从一个进程(或线程)切换到另一个进程(或线程)。上下文是指某一时间点 CPU 寄存器和程序计数器的内容。
寄存器是cpu内部的少量的速度很快的闪存,通常存储和访问计算过程的中间值提高计算机程序的运行速度。
程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体实现依赖于特定的系统。
举例说明 线程A - B
1.先挂起线程A,将其在cpu中的状态保存在内存中。
2.在内存中检索下一个线程B的上下文并将其在 CPU 的寄存器中恢复,执行B线程。
3.当B执行完,根据程序计数器中指向的位置恢复线程A。
CPU通过为每个线程分配CPU时间片来实现多线程机制。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。
但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。如何减少系统中上下文切换次数,是提升多线程性能的一个重点课题。
临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程同时访问,但是每个线程访问时,临界资源被该线程锁占有,其他线程只能等待。
同步互斥
引入同步互斥机制的原因?
现在的操作系统都是多任务操作系统,系统内存在着大量的任务运行实体,当多任务实体运行时可能会发生:
- 多任务(多线程)同时访问或者使用同一种资源
- 某个任务依赖于另一个任务的运行结果,多个任务之间存在依赖关系。
同步:是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。
异步:任务之间的运行次序并没有严格的要求。比如说网络异步请求、非阻塞式IO、函数的回调都侧面展示了异步的使用。相对于同步,异步对它做了进一步的优化,减少了等待。
互斥:是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。
同步是一种更为复杂的互斥,而互斥是一种特殊的同步。互斥是两个任务之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,间接地看是一种没有顺序的同步,而同步也是不能同时运行,但他是必须要安照某种次序来运行相应的线程(也是一种互斥)!因此互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,即任务是无序的,而同步的任务之间则有顺序关系。
8.3 线程组和线程优先级
8.3.1 线程组(ThreadGroup)
Java中用ThreadGroup来表示线程组,我们可以使用线程组对线程进行批量控制。每个Thread必然存在于一个ThreadGroup中,Thread不能独立于ThreadGroup存在。执行main()方法线程的名字是main,如果在new Thread时没有显式指定,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组。
示例代码:
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread testThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("testThread当前线程组名字:" + Thread.currentThread().getThreadGroup().getName());
System.out.println("testThread当前线程名字:" + Thread.currentThread().getName());
new Thread(()->{
System.out.println(Thread.currentThread().getThreadGroup().getName());
System.out.println(Thread.currentThread().getName());
}).start();
}
});
testThread.start();
System.out.println("执行main方法线程名字:" + Thread.currentThread().getName());
}
/* 输出
* 执行main方法线程名字:main
* testThread当前线程组名字:main
* testThread当前线程名字:Thread-0
* main
* Thread-1
*/
ThreadGroup管理着它下面的Thread,ThreadGroup是一个标准的向下引用的树状结构,这样设计的原因是防止"上级"线程被"下级"线程引用而无法有效地被GC回收。
8.3.2 线程的优先级
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都支持10级优先级的划分(比如有些操作系统只支持3级划分:低,中,高),Java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。
Java默认的线程优先级为5,线程的执行顺序由调度程序来决定,线程的优先级会在线程被调用之前设定。
高优先级的线程将会比低优先级的线程有更高的几率得到执行。可以使用方法Thread
类的setPriority()
实例方法来设定线程的优先级,看下源码
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
// 判断优先级参数是否合法
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
// 得到当前线程组的优先级,如果大于线程组的优先级,则设置为线程组的优先级
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
上面代码一个关键的地方是:所以,如果某个线程优先级大于线程所在线程组的最大优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。
我是否可以通过改变线程的优先级来控制线程的执行顺序呢?答案是:No!
Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定会采纳。而真正的调用顺序,是由操作系统的线程调度算法决定的。
我们来验证一下:
public static class T1 extends Thread {
@Override
public void run() {
super.run();
System.out.printf("当前执行的线程是:%s, 优先级:%d\n", Thread.currentThread().getName(), Thread.currentThread().getPriority());
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
IntStream.range(1, 10).forEach(i -> {
Thread thread = new Thread(new T1());
thread.setPriority(i);
thread.start();
});
}
/** 输出
* 当前执行的线程是:Thread-17, 优先级:9
* 当前执行的线程是:Thread-3, 优先级:2
* 当前执行的线程是:Thread-1, 优先级:1
* 当前执行的线程是:Thread-7, 优先级:4
* 当前执行的线程是:Thread-5, 优先级:3
* 当前执行的线程是:Thread-11, 优先级:6
* 当前执行的线程是:Thread-9, 优先级:5
* 当前执行的线程是:Thread-15, 优先级:8
* 当前执行的线程是:Thread-13, 优先级:7
*/
Java提供一个线程调度器来监视和控制处于RUNNABLE状态的线程。线程的调度策略采用抢占式,优先级高的线程比优先级低的线程会有更大的几率优先执行。在优先级相同的情况下,按照“先到先得”的原则。每个Java程序都有一个默认的主线程,就是通过JVM启动的第一个线程main线程。
还有一种线程称为守护线程(Daemon),守护线程默认的优先级比较低。
如果某线程是守护线程,那如果所以的非守护线程结束,这个守护线程也会自动结束。
应用场景是:当所有非守护线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要继续关闭子线程的麻烦。
一个线程默认是非守护线程,可以通过Thread类的setDaemon(boolean on)来设置。
8.3.3 线程组的常用方法及数据结构
1、线程组的常用方法
// 获取当前的线程组名字
Thread.currentThread().getThreadGroup().getName;
// 复制一个线程数组到一个线程组
Thread[] thread = new Thread[threadGroup.activeCount()];
ThreadGroup threadGroup = new ThreadGroup();
threadGroup.enumerate(threads);
2、线程组统一异常处理
package com.func.axc.threadgroup;
public class ThreadGroupDemo {
public static void main(String[] args) {
ThreadGroup threadGroup1 = new ThreadGroup("group1") {
// 继承ThreadGroup并重新定义以下方法
// 在线程成员抛出unchecked exception
// 会执行此方法
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName() + ": " + e.getMessage());
}
};
// 这个线程是threadGroup1的一员
Thread thread1 = new Thread(threadGroup1, new Runnable() {
public void run() {
// 抛出unchecked异常
throw new RuntimeException("测试异常");
}
});
thread1.start();
}
}
上面出现了一个定义类对象并重写方法的新形势,threadGroup1并不是抽象类和接口,却可以这样写,这其实就是一个匿名内部类。
3、线程组的数据结构
线程组还可以包含其他的线程组,不仅仅是线程。
首先看下ThreadGroup
源码
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
private final ThreadGroup parent; // 父亲ThreadGroup
String name; // ThreadGroupr 的名称
int maxPriority; // 线程最大优先级
boolean destroyed; // 是否被销毁
boolean daemon; // 是否守护线程
boolean vmAllowSuspension; // 是否可以中断
int nUnstartedThreads = 0; // 还未启动的线程
int nthreads; // ThreadGroup中线程数目
Thread threads[]; // ThreadGroup中的线程
int ngroups; // 线程组数目
ThreadGroup groups[]; // 线程组数组
// 私有构造函数
private ThreadGroup() {
this.name = "system";
this.maxPriority = Thread.MAX_PRIORITY;
this.parent = null;
}
// 默认是以当前ThreadGroup传入作为parent ThreadGroup,新线程组的父线程组是目前正在运行线程的线程组。
public ThreadGroup(String name) {
this(Thread.currentThread().getThreadGroup(), name);
}
// 构造函数
public ThreadGroup(ThreadGroup parent, String name) {
this(checkParentAccess(parent), parent, name);
}
// 私有构造函数,主要的构造函数
private ThreadGroup(Void unused, ThreadGroup parent, String name) {
this.name = name;
this.maxPriority = parent.maxPriority;
this.daemon = parent.daemon;
this.vmAllowSuspension = parent.vmAllowSuspension;
this.parent = parent;
parent.add(this);
}
// 检查parent ThreadGroup
private static Void checkParentAccess(ThreadGroup parent) {
parent.checkAccess();
return null;
}
// 判断当前运行的线程是否具有修改线程组的权限
public final void checkAccess() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkAccess(this);
}
}
}
这里涉及到
SecurityManager
这个类,它是Java的安全管理器,它允许应用程序在执行一个可能不安全或敏感的操作前确定该操作是什么,以及是否是在允许执行该操作的安全上下文中执行它。应用程序可以允许或不允许该操作。比如引入了第三方类库,但是并不能保证它的安全性。
其实Thread类也有一个checkAccess()方法,不过是用来检查当前运行的线程是否有权限修改被调用的这个线程实例。(Determines if the currently running thread has permission to modify this thread.)
总结来说,线程组是一个树状的结构,每个线程组下面可以有多个线程或者线程组。线程组可以起到统一控制线程的优先级和检查线程的权限的作用。
8.4 线程的状态及主要转化方法
8.4.1 操作系统中的线程状态转换
在现在的操作系统中,线程被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态是一致的。
操作系统进程的三种基本状态:
- 就绪状态(ready):当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行,进程这时的状态就称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将他们排成一个队列,称为就绪队列。
- 执行状态(running):进程已获得CPU,其程序正在执行。在单处理器机器上,只能有一个进程运行;在多处理器机器上,每个处理器运行一个进程,可以有多个进程并行运行,如果进程的数目多于处理器的数目,调度器依然采用时间片机制。
- 阻塞状态(waiting):正在执行的进程由于发生某事件(如I/O、申请缓冲区失败等而)暂时无法继续执行时,便放弃处理机处于暂停状态。亦即程序的执行受到阻塞,把这种暂停状态称为阻塞状态,有时也称为等待状态或封锁状态。
【注】在计算机操作系统书中,进程图如下图:
8.4.2 Java线程的6个状态
// Thread.State 源码
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINALED
}
1、NEW
当用new操作符创建一个新线程时,如new Thread®,该线程还没有开始运行(未调用start方法),这意味着它的状态是new。
Thread thread = new Thread(() -> {
});
System.out.println(thread.getState());//NEW
thread.start();
System.out.println(thread.getState());//RUNNABLE
前面曾经提到过,一个线程不同反复调用start()方法,这里解释下为什么?
首先看一下start源码
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* JVM调用这个线程的run方法
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* 方法的调用结果是调用start方法的线程和run线程同时运行
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed
* execution.
* 一个线程调用多次start方法是从不合法的
*/
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
在start()内部,这里有一个threadStatus的变量。如果它不等于0,调用start()是会直接抛出异常的。
我们接着往下看,有一个native的start0()
方法。这个方法里并没有对threadStatus的处理。到了这里我们仿佛就拿这个threadStatus没辙了,我们通过debug的方式再看一下:
Thread.currentThread().start();//Main线程再次尝试start()方法
// 进入start方法,通过打断点可以看到threadStatus值为5
查看当前线程状态的源码:
// Thread.getState方法源码:
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
// sun.misc.VM 源码:
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}
所以,我们结合上面的源码可以得到引申的两个问题的结果:
两个问题的答案都是不可行,在调用一次start()之后,threadStatus的值会改变(threadStatus !=0),此时再次调用start()方法会抛出IllegalThreadStateException异常。
比如,threadStatus为2代表当前线程状态为TERMINATED。
2、RUNNABLE
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
可运行状态。一旦线程调用start方法,线程处于runnable状态。处于RUNNABLE状态的线程可能正在运行,也有可能在等待其他系统资源(比如I/O)。这就是为什么这个状态称为可运行而不是运行。
Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“可运行状态”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3、BLOCKED
阻塞状态。当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进入同步区。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
4、WAITING
等待状态。可以用操作系统中进程的等待状态来定义,即正在运行中的线程由于某事件而不能继续执行,放弃CPU,处于等待,处于等待状态的线程必须由其他线程所唤醒,否则一直等待下去。
调用如下3个方法会使线程进入等待状态:
- Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
- Thread.join():等待线程执行完毕,底层调用的是Object实例的wait方法;
- LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。
5、TIMED_WAITING
超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。
调用如下方法会使线程进入超时等待状态:
- Thread.sleep(long millis):使当前线程睡眠指定时间;
- Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
- Thread.join(long millis):等待当前线程最多执行millis毫秒,如果millis为0,则会一直执行;
- LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
- LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;
6、TERMINATED
终止状态。此时线程已执行完毕。
8.4.3 线程状态转化
线程状态转化图:
1、BLOCKED与RUNNABLE状态的转换
处于BLOCKED状态的线程是因为在等待锁的释放。假如这里有两个线程a和b,a线程提前获得了锁并且暂未释放锁,此时b就处于BLOCKED状态。我们先来看一个例子:
@Test
public void blockedTest() {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "a");
Thread b = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "b");
a.start();
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出?
System.out.println(b.getName() + ":" + b.getState()); // 输出?
}
// 同步方法争夺锁
private synchronized void testMethod() {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
初看之下,大家可能会觉得线程a会先调用同步方法,同步方法内又调用Thread.sleep()方法,必然会输出TIMED_WAITING,而线程b因为等待线程a释放锁所以必然会输出BLOCKED。
其实不然,有两点需要值得大家注意,一是在测试方法blockedTest()内还有一个main线程,二是启动线程后执行run方法还是需要消耗一定时间的。不打断点的情况下,上面代码中都应该输出RUNNABLE。
上面在JUnit测试中运行,和在main方法中运行效果也是不同的。在JUnit测试中调用方法不需要对象,而在main方法中,要直接调用方法,要么该方法是静态方法,要就必须使用对象调用方法。如下:
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new Thread(Thread01::testMethod);
Thread t2 = new Thread(Thread01::testMethod);
t1.start();
t2.start();
System.out.println(t1.getState());
System.out.println(t2.getState());
}
private static synchronized void testMethod() {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
在Main方法中调用,程序等到所有线程都结束后才结束运行,而在JUnit中,因为JUnit也是一个main入口,它测试的方法中顺序执行完所有语句后,就会结束,如果还有子进程没有运行完,也会结束。
如果我要打印出BLOCKED状态我该怎么处理?其实就是让测试方法或者main方法的main线程休息一会就可以了,打断点或者调用Thread.sleep。这里一定要注意main线程的休息时间,只要让main线程在打印语句之前稍微停顿一下就可以。
public void blockedTest() throws InterruptedException {
······
a.start();
Thread.sleep(1000L); // 需要注意这里main线程休眠了1000毫秒,而testMethod()里休眠了2000毫秒,放在a.start之后第一个sout之前就可以,但是结果并不是绝对的!
b.start();
System.out.println(a.getName() + ":" + a.getState());
System.out.println(b.getName() + ":" + b.getState());
}
/*
* 输出:结果不定
* a:TIMED_WAITING
* b:BLOCKED
*/
main线程休眠:当对main线程调用Thread.sleep方法,当前线程就会在调用Thread.sleep地方停止,程序不会再往下执行,这不影响已经开始(start)的线程,会影响在停止的代码处后面未start的线程,很好理解,main线程停止了,后面代码块肯定属于main所在线程,所以就因main的停止而不会得到执行。
8.6 锁对象&条件对象&synchronized
Java语言提供一个synchronized关键字和ReentranLock类(可重入锁)防止代码块受并发访问的干扰。
8.6.1 锁对象
ReentrantLock保护代码块的基本结构如下:
myLock.lock();// a ReentrantLock object
try {
critical section// 临界区
} finally {
myLock.unlock; // 确保抛出异常的时候能够解锁
}
使用ReentrantLock的锁对象可以保证任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。
注意点:
把解锁操作放在 finally内是至关重要的。如果临界区发生异常,这里所说的异常是因为业务逻辑错误而引发的异常,这时候已经lock了,所以确保锁必须被释放。否则,其他线程将永远阻塞。
如果使用锁,就不能使用带资源的try语句。
myLock.lock();必须放在try…finally语句块外?
如果放在try内部,如果lock()上锁发生异常,那么就会执行finally中的unlock,这时并没有获得锁对象,解锁不存在的锁对象抛出
IllegalMonitorStateException
;相反放在try外部,当获取锁发生抛出异常时,下面语句则不会执行。
使用锁来保护Bank类的transfer方法
public class Bank {
private ReentrantLock reentrantLock = new ReentrantLock();
public void transfer(int from, int to, double amount) {
reentrantLock.lock();
try {
System.out.print(Thread.currentThread());
if (accounts[from] < amount) {
return;
}
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d%n", amount, from, to);
accounts[to] += amount;
System.out.printf("Total Balance %10.2f%n", getTotalBalance());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}
假定一个线程调用transfer,在执行结束前被剥夺了运行权。假定第二个线程也调用transfer,由于第二个线程不能获得锁,将在调用lock方法时阻塞。它必须等待第一个线程完成transfer方法的执行之后才能再度被激活。
每一个Bank对象有自己的ReentrantLock对象。如果两个线程试图访问同一个Bank对象,那么锁以**串行方式**提供服务。如果两个线程访问不同的Bank对象,每一个线程得到不同的锁对象,两个线程都不会发生阻塞,也不会存在线程安全问题。
8.6.2 条件对象
条件对象又称为条件变量,线程进入临界区后,发现必须在满足某一条件之后他才能执行,这时我们可以使用条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。
现在来细化银行的模拟程序,避免选择没有足够资金的账户作为转出账户。注意不能使用下面的代码:
if (bank.getBalance(from) >= amount)
bank.transfer(from, to, amount);
同样和之前的转出转入也是非原子操作,存在多线程并发的问题。我们应该确保没有其他线程在本检查余额与转账活动之间修改余额。通过锁可以做到这一点:
public void transfer(int from, int to, double amount) {
reentrantLock.lock();
try {
while (accounts[from] < amount) {
// wait
...
}
//transfer funds
...
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}
当账户中没有足够余额时,应该做什么?等待直到另一个线程向账户中注入资金。但是,由于这一线程刚刚获得了对reentrantLock的排它性访问,因此别的线程没有进行存款操作的机会。但是我们可以使用条件对象来解决这一问题。
一个锁对象可以有一个或多个相关的条件对象,习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。
class Bank {
private Condition sufficientFunds;
...
public Bank() {
...
sufficientFunds = reentrantLock.newCondition();
}
public void transfer(int from, int to, double amount) {
reentrantLock.lock();
try {
while (accounts[from] < amount) {
// 余额不足,让出CPU执行权
sufficientFunds.await();
}
// transfer funds...
// 转账结束,通知所有等待资源的线程
sufficientFunds.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}
当一个线程调用await时,它没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致死锁现象。一旦激活,并且获得了锁之后,就从被阻塞的地方继续执行。
**signalAll不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,**要执行方法,还是用通过竞争获得对象锁从而对对象方法进行访问。
当一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用await、signalAll或signal方法。
锁和条件的总结:
- 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
- 锁可以管理试图进入被保护代码段的线程。
- 锁可以拥有一个或多个相关的条件对象。
- 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
8.6.3 synchronized关键字
1、修饰成员方法和静态方法
(1)Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。并且,该锁有一个内部条件,由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程。
(2)将静态方法声明为synchronized也是合法的。如果调用这种方法**,该方法获得相关的类对象的内部锁。**
public synchronized void method() {
method body
}
//等价于
public void method() {
this.reentrantLock.lock();
try {
method body
} finally {
this.reentrantLock.unlock();
}
}
内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。
Object.wait()/notifyAll();
//等价于
reentrantLock.await()/signalAll();
wait、notifyAll及notify方法是Object类的final方法。Condition方法必须被命名为await、signalAll和signal以便它们不会与那些方法发生冲突。
可以用内部对象锁来实现银行问题:
public synchronized void transfer(int from, int to, double amount) throws InterruptedException {
while (accounts[from] < amount) {
wait();
}
accounts[from] -= amount;
accounts[to] += amount;
notifyAll();
}
java.lang.Object 线程有关的方法
-
void notifyAll( )
解除那些在该对象上调用wait方法的线程的阻塞状态。
-
void notify( )
随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态。
-
void wait( )
导致线程进入wait等待状态直到它被通知。
-
void wait(long millis) void wait(long millis, int nanos)
导致线程进入wait等待状态直到它被通知或者经过指定的时间。
上述方法只能在同步方法或同步块内部调用(synchronized)。如果当前线程不是对象锁的持有者,该方法抛出一个
IllegalMonitorStateException
异常。
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
2、修饰代码块
被修饰的代码块称为同步代码块,其作用的范围是大括号{ }括起来的代码,作用的对象是调用这个代码块的对象。
(1)一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程被阻塞。另一个线程仍可访问该对象中的非同步代码块。
(2)当对某一确定的对象加锁时,其他试图访问该对象的线程将会阻塞,直到该线程访问该对象结束。也就是说谁先拿到访问对象的锁谁就可以运行它所控制的代码。
public void method() {
synchronized(obj) {
// critical section
...
}
}
(3)当没有明确的对象作为锁,只想让一段代码同步时,可以创建一个特殊的对象来充当锁。
public class Bank {
private byte[] lock = new byte[0];
public void method() {
synchronized(lock) {
// critical section
...
}
}
}
说明:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
8.7 volatile域
8.7.1 内存模型的相关概念
计算机中执行程序时,每条指令都是在CPU中执行,执行指令的过程必然会涉及到数据的读取和写入。而程序运行时的数据是存放在主存(物理内存)中,由于CPU的读写速度远远高于内存的速度,如果CPU直接和内存交互,会大大降低指令的执行速度,所以CPU里面就引入了高速缓存。
脑补当初学习OS时的图 CPU->内存 CPU->寄存器->内存
也就是说程序运行时,会将运算所需要的数据从主存中复制一份到高速缓存,CPU进行计算的时候可以直接从高速缓存读取和写入,当运算结束时,在将高速缓存中的数据刷新到主存。
但是如果那样必须要考虑,在[多核CPU下](https://www.zhihu.com/question/20998226)数据的一致性问题怎么保证?比如``i=i+1``,当线程执行这条时,会先从主存中读取i的值,然后复制一份到高速缓存,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。在单线程下这段代码运行不会存在问题,但如果在多线程下多核CPU中,每个CPU都有自己的高速缓存,可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。最终结果i的值是1,而不是2。这就是著名的**缓存一致性问题**。通常称这种被多个线程访问的变量为**共享变量**。
为了解决缓存不一致性问题,通常来说有以下2种解决方法:
-
通过在总线加LOCK#锁的方式
-
通过缓存一致性协议
8.7.2 原子性可见性有序性
并发编程中,通常会考虑的三个问题原子性问题、可见性问题、有序性问题。
(1)原子性:程序中的单步操作或多步操作要么全部执行并且执行的过程中不能被打断,要么都不执行。
如果程序中不具备原子性会出现哪些问题?
转账操作就是一个很好的代表,如果转账的过程中被中断,钱转出去了,由于中断,收账方却没有收到。
(2)可见性:内存可见性,指的是线程之间的可见性,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值。
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
倘若线程1从主存中读取了i的值并复制到CPU高速缓存,然后对i修改为10,这时CPU高速缓存中的i值为10,在没有将高速缓存中的值刷新到主存中时,线程2读取到的值还是0,它看不到i值的变化,这就是可见性问题。
Java提供了Volatile关键字来保证可见性,当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
(3)有序性:程序执行的顺序按照代码的先后顺序执行。
实际是这样吗?
int i = 0; //[1]
int a,b; //[2]
[2]一定会在[1]之后执行吗?不一定,在JVM中,有可能会发生指令重排序(Instruction Reorder)。如果[1]、[2]中有相互依赖,比如[2]中的数据依赖于[1]的结果,那么则不会发生指令重排序。
什么是指令重排序?
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
指令重排对于提⾼CPU处理性能⼗分必要。虽然由此带来了乱序的问题,但是这点牺牲是值得的。
指令重排可以保证串⾏语义⼀致,但是没有义务保证多线程间的语义也⼀致。所以在多线程下,指令重排序可能会导致⼀些问题。
8.7.3 Java内存模型的抽象结构
JVM可以看做是一个有OS架构的处理机,他也有自己的内存和处理器,它的内存和之前讨论的没有什么太大的差异。
Java运行时内存的划分如下:
对于每⼀个线程来说,栈都是私有的,而堆是共有的。也就是说在栈中的变量(局部变量、⽅法定义参数、异常处理器参数)不会在线程之间共享,也就不会有内存可⻅性(下⽂会说到)的问题,也不受内存模型的影
响。⽽在堆中的变量是共享的,本⽂称为共享变量。所以内存可见性针对的是共享变量。
1、既然堆是共享的,为什么在堆中会有内存不可⻅问题?
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
线程之间的共享变量存在主内存中,每个线程都有⼀个私有的本地内存,存储了该线程以读、写共享变量的副本。本地内存是Java内存模型的⼀个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器等。
Java线程之间的通信由Java内存模型(简称JMM)控制,从抽象的⻆度来说,JMM定义了线程和主内存之间的抽象关系。JMM的抽象示意图如图所示:
从图中可以看出:
- 所有共享变量都存在主内存中。
- 每个线程都保存了一份该线程使用到的共享变量的副本。
- 线程A与线程B之间的通信必须通过主存。
2、JMM与Java内存区域划分的区别与联系
-
区别
JMM是抽象的,他是⽤来描述⼀组规则,通过这个规则来控制各个变量的访问⽅式,围绕原⼦性、有序性、可⻅性等展开的。⽽Java运⾏时内存的划分是具体的,是JVM运⾏Java程序时,必要的内存划分。
-
联系
都存在私有数据区域和共享数据区域。⼀般来说,JMM中的主内存属于共享数据区域,他是包含了堆和⽅法区;同样,JMM中的本地内存属于私有数据区域,包含了程序计数器、本地⽅法栈、虚拟机栈。
原子性、可见性、有序性
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。
8.7.4 volatile的内存语义
在Java中,volatile关键字有特殊的内存语义。volatile主要有以下两个功能:
- 保证变量的内存可⻅性
- 禁⽌volatile变量与普通变量重排序(JSR133提出,Java 5 开始才有这个“增强的volatile内存语义“)
内存可见性
所谓内存可见性,指的是当一个线程对volatile
修饰的变量进行过写操作时,JMM会立即把线程对应的本地内存中的共享变量的值刷新到主内存;当一个线程对volatile
修饰的变量进行读操作时,JMM会立即把该线程对应的本地内存置为无效,从内存中从新读取共享变量的值。
禁止重排序
JMM是通过内存屏障来限制处理器对指令的重排序的。
什么是内存屏障?硬件层面,内存屏障分两种:读屏障(Load Barrier)和写屏障(Store Barrier)。内存屏障有两个作用:
- 阻止屏障两侧的指令重排序
- 强制把写缓冲区/⾼速缓存中的脏数据等写回主内存,或者让缓存中相应的数据 失效。
通俗说,通过内存屏障,可以防止指令重排序时,不会将屏障后面的指令排到之前,也不会将屏障之前的指令排到之后。
8.7.5 Volatile关键字的应用场景
单例模式下的Double-Check(双重锁检查)
public class Singleton {
public static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { //[1]
synchronized (Singleton.class) {
instance = new Singleton(); //[2]
}
}
return instance;
}
}
如果这里的变量没有使用volatile关键字,那么有可能就会发生错误。
[2]实例化对象的过程可以分为分配内存、初始化对象、引用赋值。
instance = new Singleton(); // [1]
// 可以分解为以下三个步骤
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化对象
3 s=memory //设置s指向刚分配的地址
// 上述三个步骤可能会被重排序为 1-3-2,也就是:
1 memory=allocate();// 分配内存 相当于c的malloc
3 s=memory //设置s指向刚分配的地址
2 ctorInstanc(memory) //初始化对象
如果一旦发生了上述的重排序,当程序执行了1和3,这时线程A执行了if判断,判定instance不为空,然后直接返回了一个未初始化的instance。
8.8 阻塞队列
8.8.1 阻塞队列介绍
1、什么是阻塞队列
要想知道什么是阻塞队列,那么就必须了解什么是队列,队列就是一种先进先出的数据结构,阻塞队列就是一种可阻塞的队列。
特性:
- 具有队列的基本特性(先进先出)
- 队列为空时,从队列中获取元素的操作会被阻塞。直到队列非空。
- 队列满时,向队列中添加元素的操作会被阻塞。直至队列存在空闲。
2、阻塞队列应用场景
(1) 生产者-消费者问题,生产者生产数据放入固定大小的缓冲区(队列),消费从缓冲区获取数据。这个过程中存在的问题有,生产者不可能无限制的生产,缓冲区大小固定,消费者也不可能无限制地消费,池中没有数据则不能消费。在单线程环境下,并不会出现上面问题,就有一个生产,一个消费,空了满了都会有具体的判断或者处理逻辑。但是对于多线程来说,同时刻,可能有两个线程进入了只有一个数量的池中,这样都会进行消费,这样就会带来问题。
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
(2) 阻塞队列在java中的一种典型使用场景是线程池,在线程池中,当提交的任务不能被立即得到执行的时候,线程池就会将提交的任务放到一个阻塞的任务队列中来。
总结:使用阻塞队列,我们不需要关心什么时候需要阻塞线程(池满-生产线程-停止生产,消费者线程-消费),什么时候需要唤醒线程(池空-生产者线程-生产,消费者线程停止消费),这一切BlockingQueue
都已经处理好了。
8.8.2 阻塞队列架构
队列 | 有界性 | 锁(ReentrantLock) | 数据结构 |
---|---|---|---|
ArrayBlockingQueue | bounded(有界) | 加锁 | arrayList |
LinkedBlockingQueue | optionally-bounded | 加锁 | linkedList |
PriorityBlockingQueue | unbounded | 加锁 | heap |
DelayQueue | unbounded | 加锁 | heap |
SynchronousQueue | bounded | 加锁 | 无 |
LinkedTransferQueue | unbounded | 加锁 | heap |
LinkedBlockingDeque | unbounded | 无锁 | heap |
1、BlockQueue方法
根据插入和取出两种类型的操作,具体分为下面一些类型:
操作类型 | Throws Exception | Special Value | Blocked | Timed out |
---|---|---|---|---|
插入 | add(o) | offer(o) | put(o) | offer(o, timeout, unit) |
取出(删除) | remove(o) | poll() | take() | poll(timeout, unit) |
- 抛出异常:当插入和取出在不能立即被执行的时候就会抛出异常。
- 特殊值:插入和取出在不能立即被执行的情况下会返回一个特殊的值(true或者false)
- 阻塞:插入和取出操作在不能立即被执行时会阻塞线程,直到可以操作的时候会被其他线程唤醒。
- 超时:插入和取出操作在不能立即执行的时候会被阻塞一定的时间,如果在指定的时间内没有被执行,那么会返回一个特殊值。
从阻塞和非阻塞维度划分:
阻塞方法 | 非阻塞方法 |
---|---|
put(E e) | add(E e) |
take() | remove() |
offer(E e, long timeout, TimeUnit unit) | offer(E e) |
poll(long time, TimeUnit unit) | poll() |
peek() |
2、阻塞队列浅析
看源码
阻塞队列示例-查找java文件中的类关键字:
生产者线程枚举所有子目录下的文件将其放到阻塞队列中,这个过程很快就会执行完毕。同时启动大量搜索线程,每个搜索线程从队列中取出一个文件,打开它,打印所有包含关键字的行,然后取出下一个文件。这里虚设了一个文件,如果从队列中取出的文件是虚设文件,那么说明此次队列中的文件(10个)都已经执行查找完毕,结束线程运行。
public class BlockingQueenTest {
public static final int FILE_QUEEN_SIZE = 10;
public static final int SEARCH_THREADS = 100;
public static final File DUMMY = new File(""); //虚设文件,判断是否到达队列尾
private static BlockingQueue<File> queue = new ArrayBlockingQueue<>(FILE_QUEEN_SIZE);
public static void main(String[] args) {
try (Scanner in = new Scanner(System.in)) {
System.out.println("Enter base directory (e.g. /jdk1.8/src):");
String baseDir = in.nextLine();
System.out.println("Enter keyword (e.g. volatile):");
String keyword = in.nextLine();
Runnable enumerator = () -> {
enumerate(new File(baseDir));
queue.add(DUMMY);
};
new Thread(enumerator).start();
for (int i = 0; i < SEARCH_THREADS; i++) {
Runnable searcher = () -> {
try {
boolean done = false;
while (!done) {
File file = queue.take();
if (file == DUMMY) {
queue.put(file);//这里要放回虚设对象,因为还要终结其他线程。
done = true;
} else
searchFile(file, keyword);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(searcher).start();
}
}
}
private static void searchFile(File file, String keyword) {
try (Scanner in = new Scanner(file, "utf8")) {
int lineNumber = 0;
while (in.hasNextLine()) {
lineNumber++;
String line = in.nextLine();
if (line.contains(keyword)) {
System.out.printf("%s:%d%s%n", file.getPath(), lineNumber, line);
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
private static void enumerate(File file) {
if (file.isDirectory()) {
File[] files = file.listFiles();
for (File f : files) {
enumerate(f);
}
} else {
queue.add(file);
}
}
}
GitBookhttps://redspider.gitbook.io/concurrent/di-yi-pian-ji-chu-pian/1
简书https://www.jianshu.com/p/ba068599459e
掘金https://juejin.im/post/5ab116875188255561411b8a
博客园https://www.cnblogs.com/wxd0108/p/5479442.html
CSDNhttps://blog.csdn.net/weixin_44797490/article/details/91006241
文件读写又忘了
java8新特性lambda表达式、函数式接口、流API、默认方法和新的Date以及Time API。
9. I/O
9.1 输入输出
磁盘、网络中读取到内存中的数据,我们一般称为输入,即磁盘、网络->内存。而从应用程序比如java App产生的数据保存到磁盘或者通过网络传输,称为输出,内存->磁盘、网络。
输入流读流,输出流写流
9.2 I/O流的分类
1、流的方向划分
- 输入流(InputStream)
- 输出流(OutputStream)
2、流的数据单位划分
- 字节流:1B
- 字符流:1字符,2B
3、流的功能划分
-
节点流:直接连接数据源的流,可以直接向数据源(硬盘,网络)读写数据。
-
处理流:对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写(装饰者模式)。处理流的构造方法必须依赖一个节点流。
9.3 I/O常用的五类一接口
在Java.io包中最重要的就是5个类和一个接口。5个类指的是File、OutputStream、InputStream、Writer、Reader;一个接口指的是Serializable。
9.4 四大基本抽象流
输入流 | 输出流 | |
---|---|---|
字节流 | InputStream | OutputStream |
字符流 | Reader | Writer |
ByteArrayInputStream ByteArrayOutputStream
:字节数组输入输出流在内存中创建一个字节数组缓冲区,实际就是将数据写入内存,然后从内存中读取。
- 关闭
ByteArrayInputStream
没有任何效果。 在关闭流之后,仍然可以调用此类中的方法,而不生成IOException
。
PipedInputStream
:管道字节输入流,它和PipedOutputStream一起使用,能实现多线程间的管道通信。
FilterInputStream
包含一些其他输入流,它用作其基本的数据源,可能会沿途转换数据或提供附加功能。 FilterInputStream
本身简单地覆盖了所有InputStream的方法, InputStream
版本将所有请求传递给包含的输入流。
BufferedInputStream BufferedOutputStream
:带有缓冲区的输入输出流,构造方法接受一个节点流,内部使用字节数组来充当缓冲区,每次都会等待缓冲区满了之后在发送。
BufferedInputStream
为另一个输入流添加了功能,即缓冲输入和支持mark
和reset
方法的功能。 当创建BufferedInputStream
时,将创建一个内部缓冲区数组。 当从流中读取或跳过字节时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次有多个字节。mark
操作会记住输入流中的一点,并且reset
操作会导致从最近的mark
操作之后读取的所有字节在从包含的输入流中取出新的字节之前重新读取。
DataInputStream DataOutputStream
:处理流,构造方法接收一个已存在的输入输出流,允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型。
FileInputStream FileOutputStream
:可以从文件系统中 读取/写入 诸如图像数据之类的原始字节流。
ObjectInputStream ObjectOutputStream
:对象输入输出流,可以从硬盘或者网络将序列化的对象读出,也可以将对象持久化存储到磁盘(对象存储)。
示例一:文件普通读写,BufferedInputStream重复读取。
/**
* 一次读取一字节
*/
public void copyFile1(String src, String des) throws IOException {
InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(des);
int len;
while ((len = in.read()) != -1) {
out.write(len);
}
in.close();
out.close();
// 使用处理流自带的缓冲区
BufferedInputStream bis = new BufferedInputStream(in3);
BufferedOutputStream bos = new BufferedOutputStream(out3);
}
/**
* 使用字节数组充当缓冲区实现缓冲读取
*/
public void copyFile2(String src, String des) throws IOException {
InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(des);
int len;
byte[] buff = new byte[1024];
while ((len = in.read(buff, 0, buff.length)) != -1) {
out.write(buff, 0, buff.length);
}
in.close();
out.close();
}
/**
* 使用BufferedInputStream
* 实现重复读取
* @throws IOException
*/
@Test
public void testCopyFile3(String src, String des) throws IOException {
InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(des);
int len;
BufferedInputStream bis = new BufferedInputStream(in);
BufferedOutputStream bos = new BufferedOutputStream(out);
bis.mark(0);
while ((len = bis.read()) != -1) {
System.out.println("第一次读取...");
bos.write(len);
}
bis.reset();
while ((len = bis.read()) != -1) {
System.out.println("第二次读取...");
bos.write(len);
}
bis.close();
bos.close();
}
示例二:Java基本数据类型的读和写
// 向文件中写入 java基本数据类型
private static void write(String dest) throws IOException {
//1. 创建流对象
DataOutputStream os = new DataOutputStream(new FileOutputStream(dest));
//2. 写入数据
os.writeInt(10);
os.writeChar('a');
os.writeChar('b');
os.writeDouble(12.83);
//3. 关闭流
os.close();
}
// 从文件中读取 java基本数据类型,要和写入的顺序保持一致
private static void read(String src) throws IOException {
//1. 创建数据流对象
DataInputStream in = new DataInputStream(new FileInputStream(src));
//2. 读取数据
int a = in.readInt();
char b = in.readChar();
char c = in.readChar();
double d = in.readDouble();
//3. 关闭流
in.close();
}
示例三:基于内存的数据读写
@Test
public void testByteArrayIO() throws IOException{
ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
bos.write("hello, world!".getBytes());
// 流关闭之后调用方法不受影响,因为数据保存在内存
bos.close();
byte[] bytes = bos.toByteArray();
ByteArrayInputStream bin = new ByteArrayInputStream(bytes);
BufferedReader br = new BufferedReader(new InputStreamReader(bin));
String s = br.readLine();
bin.close();
br.close();
System.out.println(s);//hello, world!
// 也可以采用下面的方法
// 通过使用命名的charset解码字节,将缓冲区的内容转换为字符串。
// bos.toString("utf-8");
}
示例四:对象序列化和反序列化
序列化:对象转化为字节序列的过程。
- 将对象的字节序列保存到磁盘中,称为持久化。
反序列化:将字节序列恢复为对象的过程称为对象的反序列化。
序列化注意点:
-
静态数据成员和标记了transient的数据成员不会被序列化。
-
serialVersionUID(long类型)
- 不写则java编译器则默认会生成一个UID,如果将对象序列化之后,对类中的代码进行了修改,那么将对象反序列化就会抛出一个异常。原因就是反序列化会比对两者的UID,当对类代码进行改动之后,UID会重新生成,不在和序列化之前的对象UID相同,编号不唯一,抛出异常。
-
子类继承父类,如果父类实现了序列化接口,序列化子类时无需在实现序列化接口;如果子类继承父类,子类实现序列化接口,父类没有实现序列化接口,那么父类必须提供无参构造,才可以序列化成功。否则抛出
java.io.InvalidClassException: full_class_name; no valid constructor
。Java反序列化的时候会在实现Serializable所在类中向上查找第一个没有实现该接口的类,然后调用其构造方法。对于无继承关系的类来说,默认会查找到Obejct,然后构造该类对象,逐层往下去设置各个序列化的属性。如果类存在继承关系,子类实现了序列化接口,父类没有实现序列化接口,这时候反序列化的时候,构造对象会查询到父类,如果父类没有实现构造方法,这时候抛出
no valid constructor
的异常。
@Test
public void testObjectOut() throws IOException{
String des = "C:\\Users\\lenovo\\Desktop\\new1.txt";
Person person = new Person("勇者", "男");
FileOutputStream fout = new FileOutputStream(des);
ObjectOutputStream objOut = new ObjectOutputStream(fout);
objOut.writeObject(person);
}
@Test
public void testObjectIn() throws IOException, ClassNotFoundException {
String des = "C:\\Users\\lenovo\\Desktop\\new1.txt";
FileInputStream fin = new FileInputStream(des);
ObjectInputStream objIn = new ObjectInputStream(fin);
Person p = (Person) objIn.readObject();
System.out.println(p);
}
serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。
Reader Writer
:所有字符输入输出流的抽象父类。
CharArrayReader CharArrayWriter
:内部实现了字符缓冲区,可用作字符的输入输出。
StringReader StringWriter
:方便快捷的将字符串写入内存,或从内存读取。
BufferedReader BufferedWriter
:带有缓冲区的字符输入输出流。
InputStreamReader OutputStreamWriter:字节流与字符流之间转换的桥梁。
示例一:InputStreamReader OutputStreamWriter的读写
/**
* 定义字符数组,充当缓冲区
* @throws IOException
*/
@Test
public void testReaderWriter() throws IOException {
String src = "C:\\Users\\lenovo\\Desktop\\char.txt";
String des = "C:\\Users\\lenovo\\Desktop\\charcopy.txt";
InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(des);
Reader reader = new InputStreamReader(in);
Writer writer = new OutputStreamWriter(out);
char[] cbuf = new char[2048];
int len;
while ((len = reader.read(cbuf, 0, cbuf.length)) != -1) {
writer.write(cbuf, 0, cbuf.length);
}
writer.close();
reader.close();
out.close();
in.close();
}
BufferedReader BufferedWriter
// 一次拷贝一个 字符
private static void copyFile1(String src, String dest) throws IOException {
//1. 创建转换流
Reader reader = new BufferedReader(new FileReader(src));
Writer writer = new BufferedWriter(new FileWriter(dest));
//2. 拷贝数据
int data = reader.read();
while (data != -1) {
writer.write(data);
data = reader.read();
}
//3.关闭流
reader.close();
writer.close();
}
// 一次拷贝一个 字符数组
private static void copyFile2(String src, String dest) throws IOException {
//1. 创建转换流
Reader reader = new BufferedReader(new FileReader(src));
Writer writer = new BufferedWriter(new FileWriter(dest));
//2. 拷贝数据
char[] buffer = new char[2048];
int len = reader.read(buffer);
while (len != -1) {
writer.write(buffer, 0 , len);
len = reader.read(buffer);
}
//3.关闭流
reader.close();
writer.close();
}
// 一次拷贝一个一整行的 字符串
private static void copyFile3(String src, String dest) throws IOException {
//1. 创建转换流
BufferedReader reader = new BufferedReader(new FileReader(src));
BufferedWriter writer = new BufferedWriter(new FileWriter(dest));
//2. 拷贝数据
String data = reader.readLine();
while (data != null) {
writer.write(data);
writer.newLine();
data = reader.readLine();
}
//3.关闭流
reader.close();
writer.close();
}
9.5 Java流的关闭
1、包装流会自动调用被包装流的关闭方法,无需自己关闭
@Test
public void testClose() throws IOException {
String src = "C:\\Users\\lenovo\\Desktop\\char.txt";
String des = "C:\\Users\\lenovo\\Desktop\\charcopy.txt";
InputStream in = new FileInputStream(src);
BufferedInputStream bis = new BufferedInputStream(in);
bis.close(); // 1
in.close(); // 2
}
以上代码编译运行没有任何错误,bis的关闭是会关闭in的关闭,2处再次调用close方法等同于in关闭了两次,关闭多次没有任何异常,源码底层设置了关闭boolean变量,如果已经关闭,就不会再次关闭,所以多次关闭不会有任何异常。我们来看一下源码:为什么bis的关闭会自动关闭in。
//
public class BufferedInputStream extends FilterInputStream {
// 上文中走下面这个构造,之后又走双参构造
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
public BufferedInputStream(InputStream in, int size) {
// 调用FilterInputStream初始化InputStream
super(in);
...
}
...
public void close() throws IOException {
byte[] buffer;
while ( (buffer = buf) != null) {
if (bufUpdater.compareAndSet(this, buffer, null)) {
// 将父类的引用赋值给input,实际最终还是关闭的InputStream的流
InputStream input = in;
in = null;
if (input != null)
input.close();
return;
}
// Else retry in case a new buf was CASed in fill()
}
}
}
可以看到bis底层最终还是会调用到InputStream的close方法,所以说我们只需要关闭包装流,那么被包装流就会也会相应关闭。
流的关闭顺序是否有要求?更改上面1处和2处的代码先后顺序经实验,程序运行没有任何影响。那么是否任何流的关闭顺序都没有要求了吗?
@Test
public void testClose() throws IOException {
String src = "C:\\Users\\lenovo\\Desktop\\char.txt";
FileOutputStream fos = new FileOutputStream(src);
OutputStreamWriter osw = new OutputStreamWriter(fos);
BufferedWriter bw = new BufferedWriter(osw);
bw.write("java IO close test");
osw.close();//1
fos.close();//2
bw.close(); //3
}
// 抛出异常
Exception:
BufferedWriter
at sun.nio.cs.StreamEncoder.ensureOpen(StreamEncoder.java:45)
at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:118)
at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
at java.io.BufferedWriter.close(BufferedWriter.java:265)
对于字符缓冲写流来(BufferedWriter)说,对于写入并不是直接写入介质中,而是先将流暂存到缓冲区,当调用close()
或者flush()
方法才会真正将缓冲区的数据写入,而BufferedWriter类中的close()
方法中会将缓冲区的数据写入到介质中,处理流的本质还是靠节点流进行读写的,就上面的程序而言,如果fos
关闭,而BufferedWriter会依赖fos
将其缓冲区中的数据写出,那么就会导致上述异常。如果把2处的代码移到3后面,运行还是会抛异常,因为osw
的关闭会关闭fos
。
总结:
-
处理流读写底层还是依赖节点流,关闭包装流就无需关闭被包装流
-
一个流可以关闭多次
-
理论上,流的关闭顺序没有要求,但是如果关闭方法中调用了
write()
方法,则会抛出异常,比如BufferedWriter。
如果非要有一种顺序的话,那就是流的关闭先关闭外层流(包装流)在关闭内层流(被包装流)。
这样会存在重复关闭流的问题,但并不会导致异常。
参考
https://blog.csdn.net/maxwell_nc/article/details/49151005
https://blog.csdn.net/sinat_37064286/article/details/86537354
https://juejin.im/post/6844903910348603405#heading-12
9.6 阻塞式I/O
第一次接触到阻塞式IO是在Socket客户端向服务端发送消息,当在server端调用BufferedReader
类中的readLine()
方法读取数据时,客户端发送的数据并不会读取到。其原因就是readLine()
方法只有当读取到换行符\n
或者\r\n
时才会将之前读取到的字符以一个字符串返回,否则会一直读取。当读取到文件末尾或者socket流关闭或者IOException则会返回null。
注意:
BufferedReader
类中的readLine()
读取成功返回时的字符串不包含最后的换行符。
10. Socket编程
未完TODO
Java中的Socket编程,一般指使用TCP协议进行网络数据包的传输。
10.1 单向传输
public class TcpClientOneWay {
public static void main(String[] args) throws IOException {
new Thread(TcpServerOneWay::startTcpServer).start();
//Socket socket = new Socket("172.17.46.123", 5005);
Socket socket = new Socket("127.0.0.1", 5005);
OutputStream out = socket.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write("hello, world!(你好,世界!)");
writer.flush();
socket.close();
writer.close();
}
}
class TcpServerOneWay {
public static void startTcpServer() {
try {
ServerSocket serverSocket = new ServerSocket(5005);
Socket accept = serverSocket.accept();
InputStream in = accept.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String s = reader.readLine();
System.out.println(s);
accept.close();
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
10.2 双向传输
public class TcpClientTwoWay {
public static void main(String[] args) throws IOException {
new Thread(TcpServerTwoWay::start).start();
Socket socket = new Socket("127.0.0.1", 5005);
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out));
bw.write("hello,world!(你好,世界)");
bw.flush();
// 通知服务器发送完毕,否则服务器会一直等待
// 通知后,只能读不能写
socket.shutdownOutput();
BufferedReader wr = new BufferedReader(new InputStreamReader(in));
String line = wr.readLine();
System.out.println(line);
bw.close();
wr.close();
socket.close();
}
}
class TcpServerTwoWay {
public static void start() {
try {
ServerSocket serverSocket = new ServerSocket(5005);
System.out.println("Tcp server start...");
Socket accept = serverSocket.accept();
InputStream in = accept.getInputStream();
OutputStream out = accept.getOutputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line = br.readLine();
// 通知客户端已经读取完毕,之后只能向客户端写数据
accept.shutdownInput();
System.out.println("Tcp server receive:" + line);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out));
bw.write("success!");
bw.flush();
bw.close();
br.close();
accept.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
10.3 ReadBuffer读取socket流阻塞问题
我们有这样一个需求,在Clinet端输入数据,Server端显示Client输入的数据。代码如下:
// Client
public class SocketClient {
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = new Socket("127.0.0.1", 7777);
Scanner scanner = new Scanner(System.in);
OutputStream out = socket.getOutputStream();
while (true) {
String msg = scanner.next();
out.write(msg.getBytes());
}
}
}
// Server
public class SocketServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(7777);
// blocked until request in
Socket accept = serverSocket.accept();
System.out.println("start accept...");
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
accept.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
首先运行Server端的代码,程序将会在serverSocket.accept()
阻塞,直到有客户端连接进来,这时运行Client端代码,阻塞解除,之后server端的代码又会在reader.readLine()
中阻塞,因为reader.readLine()
底层从流中读取数据直到遇到换行符才会把数据返回或者读到流末尾或是遇到一个异常,否则会一直阻塞其中,有兴趣的可以查看源码,大致是阻塞在fill()
方法中的do…while循环。当在client端输入信息的时候,debug模式跟踪到server端的BufferdReader是可以发现数据能发送过来,但是数据并没有返回即程序死在了reader.readLine()
中,导致server端不会输出信息。
解决办法:
(1)关闭client端的socket读,这时候服务器就会将缓冲区的流全部flush出,自动读取到流末尾,这种方式不适用于该程序。
(2)调用socket的shutdownOutput
通知server端写入完毕,但是该方法也有一个局限,就是一旦调用就不能再次写入,对于循环写入也是不可取的。
(3)采用原生读,即使用节点流InputStream
不适用处理流BufferedReader
。
(4)根据BufferedReader
读的特性,在发送数据的时候发送\n
,虽然reader.readLine()
不会再阻塞了,但是由于读不到流的末尾(reader.readLine() != null
在Socket流没有关闭总是成立),还是会死循环,还需要自定义一个结束符,当server端在处理到结束符的时候,退出while循环。
采用方式(4)更改后的代码如下:
public class SocketServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(7777);
Socket accept = serverSocket.accept();
System.out.println("start accept...");
InputStream in = accept.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line;
// 定义输入end结束符结束循环关闭流
while ((line = reader.readLine()) != null && !"end".equals(line)) {
System.out.println(line);
}
accept.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class SocketClient {
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = new Socket("127.0.0.1", 7777);
Scanner scanner = new Scanner(System.in);
OutputStream out = socket.getOutputStream();
while (true) {
String msg = scanner.next() + "\n";
out.write(msg.getBytes());
// 定义输入end结束符结束循环关闭流
if (msg.equals("end\n"))
break;
}
socket.close();
}
}
采用方式(3)更改后的代码
public class SocketClient {
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = new Socket("127.0.0.1", 7777);
Scanner scanner = new Scanner(System.in);
OutputStream out = socket.getOutputStream();
while (true) {
String msg = scanner.next();
out.write(msg.getBytes());
// 定义输入end结束符结束循环关闭流
if (msg.equals("end"))
break;
}
socket.close();
}
}
public class SocketServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(7777);
Socket accept = serverSocket.accept();
System.out.println("start accept...");
InputStream in = accept.getInputStream();
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf, 0 , buf.length)) != -1) {
System.out.println(new String(buf, 0, len));
}
accept.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
https://www.jianshu.com/p/cde27461c226
11. JDBC
11.1 什么是JDBC?
JDBC(Java Database Connectivity)Java语言规定的客户端如何与数据库服务器进行通信的应用程序编程接口,Java语言操作各种数据库的程序接口,是一种规范,屏蔽了底层数据库的差异。使调用程序的用户只需要关心如何去掉用操作数据库的连接,而不需要关注底层数据库的具体实现。
11.2 如何使用JDBC
JDBC使用步骤:
连接四大属性值:驱动(driver)、**数据库地址(**url)、用户名(username)、密码(password)
1.加载驱动
2.建立连接
3.执行SQL语句
4.获得结果集或者其他
5.关闭连接
示例:对department表进行增删改查,四个测试用例,每个建立数据库连接的方式有所不同,testRetireve
采用最原始最基本建立数据库连接的方式;testAdd
方法采用实例化jdbc驱动的具体实现类来建立数据库连接;testDelete
方法采用加载配置文件的形式来建立数据库连接;testUpdate
方法采用工具类的形式建立数据库连接,工具类将具体获得数据库连接的操作封装在了类内。
为什么会衍生出这么多种形式?这样做的目的是减少代码冗余和耦合,加速代码重构。
ResultSet获得结果集注意点:在获取结果之前,需要调用rs.next()
方法,该方法一般用做判断并且将光标移动到第一行的位置。如果获取到结果集之后,直接取数据,往往会报空指针异常。
@Test
public void testRetrieve() throws ClassNotFoundException, SQLException {
// Driver首字母大写
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/ncov";
String user = "root";
String pwd = "root";
Class.forName(driver);
Connection connection = DriverManager.getConnection(url, user, pwd);
String sql = "select * from department";
PreparedStatement ps = connection.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
String deptName = rs.getString("dept_name");
System.out.println(deptName);
}
// 关闭连接,先打开的后关闭
rs.close();
ps.close();
connection.close();
}
@Test
public void testAdd() throws SQLException {
//第一个url中文乱码,添加字符集设置
//String url = "jdbc:mysql://localhost:3306/ncov";
String url = "jdbc:mysql://localhost:3306/ncov?characterEncoding=utf-8&useUnicode=TRUE";
String user = "root";
String pwd = "root";
com.mysql.jdbc.Driver driver = new com.mysql.jdbc.Driver();
DriverManager.registerDriver(driver);
Connection conn = DriverManager.getConnection(url, user, pwd);
String sql = "insert into department(dept_name,headcount,submitted_count) values(?,?,?)";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, "心科学院");
ps.setInt(2, 30);
ps.setInt(3, 20);
int effectRows = ps.executeUpdate();
System.out.println("effectRows:" + effectRows);
ps.close();
conn.close();
}
@Test
public void testDelete() throws IOException, SQLException, ClassNotFoundException {
InputStream in = JdbcStart.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties prop = new Properties();
prop.load(in);
String driver = prop.getProperty("driver");
String url = prop.getProperty("url");
String username = prop.getProperty("username");
String password = prop.getProperty("password");
assert in != null;
in.close();
Class.forName(driver);
Connection connection = DriverManager.getConnection(url, username, password);
String sql = "delete department from department where dept_id=?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setInt(1, 5);
ps.executeUpdate();
ps.close();
connection.close();
}
@Test
public void testUpdate() throws SQLException, IOException, ClassNotFoundException {
Connection conn = JdbcUtil.getConnection();
String sql = "update department set headcount=? where dept_id=?";
/*
为什么不把PreparedStatement封装在JdbcUtil里面?
Connection类:
PreparedStatement prepareStatement(String sql)
prepareStatement方法需要一个sql参数,如果要封装prepareStatement
那么也必须将SQL语句封装,并且需要考虑参数设置等问题,有点复杂,
但是如果封装好了,那将是很方便的,这里只是简单的封装。
*/
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, 50);
ps.setInt(2, 6);
int effectedRows = ps.executeUpdate();
System.out.println(effectedRows);
JdbcUtil.release(conn, ps, null);
}
11.3 JDBC工具类的封装
JdbcUtil工具类,该类封装了数据库连接的建立和关闭,为了JavaBean属性和数据库字段能够相互对应,添加了下划线和驼峰之间的互转方法。如果学习了线程池,还可以继续优化。
/**
* Created by WuJiXian on 2020/9/18 15:08
* JDBC CRUD的封装
*/
public class JdbcUtil {
private static String driver;
private static String url;
private static String username;
private static String password;
// 静态代码块,只会编译一次
static {
InputStream in = JdbcUtil.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties prop = new Properties();
try {
prop.load(in);
driver = prop.getProperty("driver");
url = prop.getProperty("url");
username = prop.getProperty("username");
password = prop.getProperty("password");
assert in != null;
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() {
Connection conn = null;
try {
Class.forName(driver);
conn = DriverManager.getConnection(url, username, password);
} catch (Exception e) {
e.printStackTrace();
}
return conn;
}
/**
* 释放连接
* 增加了空值判断,为空无需任何操作
* @param conn
* @param st
* @param rs
*/
public static void release(Connection conn, Statement st, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* 下划线转驼峰
* @param field
* @return
*/
public static String underscoreToCamelCase(String field) {
if (!field.contains("_")) {
return field;
} else {
char c = field.charAt(field.indexOf("_") + 1);
char upperC = (char)((int) c - 32);
return field.replaceAll("_" + c, String.valueOf(upperC));
}
}
/**
* 驼峰转下划线
* @param field
* @return
*/
public static String CamelCaseToUnderscore(String field) {
Pattern humpPattern = Pattern.compile("[A-Z]");
Matcher matcher = humpPattern.matcher(field);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, "_" + matcher.group(0).toLowerCase());
}
matcher.appendTail(sb);
return sb.toString();
}
}
JdbcTemplate主要是对数据库进行CRUD操作的类,包含了两个方法
executeQuery
:结果集的查询,单条结果或者是集合类型- ``executeUpdate`:对数据库进行增删改操作的方法
具体的实现就是依靠反射加泛型和可变参数的应用。
/**
* Created by WuJiXian on 2020/9/18 16:46
*/
public class JdbcTemplate {
/**
* 增、删、改功能函数
* @param sql
* @param params
* @return
*/
public static int executeUpdate(String sql, Object... params) throws SQLException {
Connection connection = JdbcUtil.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
for (int i = 1; i <= params.length; i++) {
ps.setObject(i, params[i - 1]);
}
int effectedRow = ps.executeUpdate();
JdbcUtil.release(connection, ps, null);
return effectedRow;
}
/**
* 泛型方法实现查询
* 结果集:1)单一 2)集合
* @param sql
* @param handler
* @param params
* @param <T>
* @return
*/
public static <T> T executeQuery(String sql, ResultHandler<T> handler, Object... params) throws Exception {
Connection connection = JdbcUtil.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
for (int i = 1; i <= params.length; i++) {
ps.setObject(i, params[i - 1]);
}
ResultSet rs = ps.executeQuery();
return handler.handle(rs);
}
}
ResultHandler
方法是一个接口,用于对结果集ResutlSet
进行处理
public interface ResultHandler<T> {
T handle(ResultSet rs) throws Exception;
}
有两个实现类
1、BeanHandler
:单条结果的处理
public class BeanHandler<T> implements ResultHandler<T> {
private Class<T> cls;
public BeanHandler(Class cls) {
this.cls = cls;
}
@Override
public T handle(ResultSet rs) throws Exception {
if (rs.next()) {
// 根据传入的字节码创建传入的指定对象
T t = cls.newInstance();
// 获取Bean信息,关于BeanInfo类可以看源码或者文档
BeanInfo beanInfo = Introspector.getBeanInfo(cls, Object.class);
// 获取所有属性描述符
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
// 通过属性描述符,可以通过特定的API调用bean的读getXxx或者写setXxx方法
String colLabel = JdbcUtil.CamelCaseToUnderscore(pd.getName());
pd.getWriteMethod().invoke(t, rs.getObject(colLabel));
}
return t;
}
return null;
}
}
2、BeanListHandler
:多条结果的处理
/**
* Created by WuJiXian on 2020/9/18 16:57
* 注意参数,一个是T,一个是List<T>,泛型的强大
*/
public class BeanListHandler<T> implements ResultHandler<List<T>> {
private Class<T> cls;
public BeanListHandler(Class<T> cls) {
this.cls = cls;
}
@Override
public List<T> handle(ResultSet rs) throws Exception{
List<T> list = new ArrayList<>();
T t = null;
BeanInfo beanInfo = Introspector.getBeanInfo(cls, Object.class);
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
while (rs.next()) {
t = cls.newInstance();
for (PropertyDescriptor pd : pds) {
String colLabel = JdbcUtil.CamelCaseToUnderscore(pd.getName());
pd.getWriteMethod().invoke(t, rs.getObject(colLabel));
}
list.add(t);
}
return list;
}
}
**Introspector
**和Java的内省机制有关,内省,自我反省的意思,java内省指的是对JavaBean内部状态的检查,可以通过特定的类来获取JavaBean的属性值或者方法等状态信息。Introspector.getBeanInfo(cls, Object.class);
因为beanInfo.getPropertyDescriptors()
方法会把getXxx作为属性值,每个类中都默认含有一个getClass方法,所以说为了不把getClass也作为属性值返回,通过Introspector.getBeanInfo(cls, Object.class)
排除Object.class的getClass()方法。
测试:
@Test
public void testJdbcTemplate() throws Exception {
String sql = "insert into department(dept_name,headcount,submitted_count) values(?,?,?)";
Object[] params = {"机电学院", 2, 2};
// 增加
int effectedRows = JdbcTemplate.executeUpdate(sql, params);
sql = "select * from department where dept_name=?";
// 查询:单一结果
Department department = JdbcTemplate.executeQuery(sql, new BeanHandler<>(Department.class), "机电学院");
System.out.println(department);
sql = "select * from department";
// 查询:结果集
List<Department> departments = JdbcTemplate.executeQuery(sql, new BeanListHandler<>(Department.class));
System.out.println(departments.size());
// 修改
sql = "update department set headcount=22 where dept_name=?";
JdbcTemplate.executeUpdate(sql, "机电学院");
// 删除
sql = "delete from department where dept_name=?";
JdbcTemplate.executeUpdate(sql, "机电学院");
}
11.4 高级操作
1、获取自增主键的方法
-
Statement getGeneratedKeys()
获取自增主键(Statement),对于Statement来说,需要设置
Statement.RETURN_GENERATED_KEYS
变量
才可以获取 -
mybatis
useGeneratedKeys
-
sql语句
select last_insert_id()
2、模糊查询
/**
* 模糊查询
*/
@Test
public void testAmbiguous() throws SQLException, IOException, ClassNotFoundException {
Connection connection = JdbcUtil.getConnection();
String sql = "select * from student where name like ?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, "王%");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
String name = rs.getString("name");
System.out.println(name);
}
JdbcUtil.release(connection, ps, rs);
}
3、批量插入
/**
* 批量插入(PreparedStatement实现)
*/
@Test
public void testAddBatch() throws SQLException, IOException, ClassNotFoundException {
Connection connection = JdbcUtil.getConnection();
String sql1 = "insert into department(dept_name,headcount,submitted_count) values(?,?,?)";
PreparedStatement ps;
ps = connection.prepareStatement(sql1);
ps.setString(1, "城建学院");
ps.setInt(2, 30);
ps.setInt(3, 20);
ps.addBatch();
ps.setString(2, "外国语学院");
ps.setInt(2, 30);
ps.setInt(3, 20);
ps.addBatch();
ps.executeBatch();
JdbcUtil.release(connection, ps, null);
}
11.5 JDBC操作大数据
大数据也称之为LOB(Large Objects),LOB又分为:clob和blob,clob用于存储大文本,blob用于存储二进制数据,例如图像、声音、二进制文等。
在实际开发中,有时是需要用程序把大文本或二进制数据直接保存到数据库中进行储存的。
对MySQL而言只有blob,而没有clob,mysql存储大文本采用的是Text,Text和blob分别又分为:
TINYTEXT、TEXT、MEDIUMTEXT和LONGTEXT
TINYBLOB、BLOB、MEDIUMBLOB和LONGBLOB
示例:请参考https://www.cnblogs.com/xdp-gacl/p/3982581.html
ResultSet在获取数据之前,必须调用
resultSet.next()
方法,否则报空指针异常
11.6 编写数据库连接池
**编写连接池需实现java.sql.DataSource接口。**DataSource接口中定义了两个重载的getConnection方法:
- Connection getConnection()
- Connection getConnection(String username, String password)
实现DataSource接口,并实现连接池功能的步骤:
- 在DataSource构造函数中批量创建与数据库的连接,并把创建的连接加入LinkedList对象中。
- 实现getConnection方法,让getConnection方法每次调用时,从LinkedList中取一个Connection返回给用户。
- 当用户使用完Connection,调用Connection.close()方法时,Collection对象应保证将自己返回到LinkedList中,而不要把conn还给数据库。Collection保证将自己返回到LinkedList中是此处编程的难点。
数据库连接池核心代码
使用动态代理技术构建连接池中的connection
核心代码:
proxyConn = (Connection) Proxy.newProxyInstance(this.getClass()
.getClassLoader(), conn.getClass().getInterfaces(),
new InvocationHandler() {
//此处为内部类,当close方法被调用时将conn还回池中,其它方法直接执行
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
if (method.getName().equals("close")) {
pool.addLast(conn);
return null;
}
return method.invoke(conn, args);
}
});
/**
* Created by WuJiXian on 2020/10/24 19:47
*/
public class JdbcPool implements DataSource {
private static LinkedList<Connection> connections = new LinkedList<>();
// 静态代码块,只会编译一次
static {
InputStream in = JdbcUtil.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties prop = new Properties();
try {
prop.load(in);
String driver = prop.getProperty("driver");
String url = prop.getProperty("url");
String username = prop.getProperty("username");
String password = prop.getProperty("password");
int initialPoolSize = Integer.parseInt(prop.getProperty("initialPoolSize"));
Class.forName(driver);
for (int i = 0; i < initialPoolSize; i++) {
Connection connection = DriverManager.getConnection(url, username, password);
System.out.println("第" + i + "个连接:" + connection);
connections.add(connection);
}
assert in != null;
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Connection getConnection() throws SQLException {
if (connections.size() > 0) {
// 从池中移除一个连接
final Connection connection = connections.removeFirst();
System.out.println("数据库连接池的连接数:" + connections.size());
Connection proxyConn = (Connection) Proxy.newProxyInstance(this.getClass().getClassLoader(), connection.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (!method.getName().equals("close"))
return method.invoke(connection, args);
else {
// 调用了close方法,将连接归还到池中
System.out.println(connection + "归还到池中,池中连接数:" + connections.size());
connections.add(connection);
return null;
}
}
});
return proxyConn;
} else {
throw new RuntimeException("数据库连接忙!");
}
}
...
}
12. ThreadLocal
12.1 ThreadLocal概述
源码分析:
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
* <p>Each thread holds an implicit reference to its copy of a thread-local
* variable as long as the thread is alive and the {@code ThreadLocal}
* instance is accessible; after a thread goes away, all of its copies of
* thread-local instances are subject to garbage collection (unless other
* references to these copies exist).
*
* @author Josh Bloch and Doug Lea
* @since 1.2
*/
ThreadLocal
为每个线程创建了共享变量的副本,每个线程保留了唯一一份实例,并且对线程内共享变量的操作是不会影响到其他线程。
ThreadLocal用于解决多线程间数据隔离的问题,并不是解决多线程并发和数据共享的问题。
合理的理解
ThreadLoal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal< String >而言即为 String 类型变量),在不同的 Thread 中有不同的副本(实际是不同的实例,后文会详细阐述)。这里有几点需要注意
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题
- 既无共享,何来同步问题,又何来解决同步问题一说?
12.2 应用场景
数据库连接:某些场景下,service的操作要聚合多个dao才能完成,在使用最原始JDBC操作的前提下,ThreadLocal
保证了多个dao操作使用同一个连接Connection
,保证了数据的完整性和一致性。
实例一:Service和dao的原始模型
第一次使用三层架构时,Service持有dao的引用,下面代码是不是很熟悉
public class Service {
private static final Dao1 DAO_1 = new Dao1();
private static final Dao2 DAO_2 = new Dao2();
public void fun() {
DAO_1.fun1();
DAO_2.fun2();
}
}
dao
public class Dao1 {
public void fun1() {
/**
*对于service上层的单步操作来说获取一个新连接还可以,但是
*对于复合dao不能保证事务。
*/
// new Connection [通常的做法]
}
}
...
在上面的原始模型中,如何保证Service的fun方法下,dao对象的fun1和fun2使用的是同一个连接,这里有两种方法:
1)将获取到的数据库连接通过传参的形式传递到dao,这种方法的缺点是耦合度太高,不利于维护。
2)ThreadLocal配合数据库连接池
这里为什么用到数据库连接池,单个Connection
放在ThreadLocal中,因为其特性,每个线程都会有这一个Connection的引用,有极大可能性破坏数据完整性和一致性。但是数据库连接池,能分配到,每一个线程一个单独的connection通过ThreadLocal绑定到线程。
下面以c3p0作为数据库连接池,以一个工具类展示本地线程的使用
public class DBUtil {
private static ComboPooledDataSource dataSource;
private static ThreadLocal<Connection> threadLocal;
// 静态代码块,只会编译一次
static {
dataSource = new ComboPooledDataSource();
threadLocal = new ThreadLocal<>();
}
public static Connection getConnection() {
Connection conn = null;
if (threadLocal.get() != null) {
conn = threadLocal.get();
} else {
// 每个线程都绑定到一个Connection确保在Service层数据库的连接是同一个
//System.out.println("current thread:" + Thread.currentThread().getName());
try {
conn = dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
threadLocal.set(conn);
}
return conn;
}
/**
* 释放连接
* 增加了空值判断,为空无需任何操作
* @param st
* @param rs
*/
public static void release(Statement st, ResultSet rs) {
Connection conn = threadLocal.get();
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
threadLocal.remove();
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
其实这里还是有点问题,大家想一下,数据库连接相关的操作(开启,关闭,回滚…)是应该放到service层还是放到dao层?
[下面是个人的见解,如有错误,请不吝指正!]
假如将获取数据库的连接放到dao层,数据库事务可能得不到保证,因为连接的关闭和释放都是在同一个dao下的方法内,不能确保第二次还是同一个数据库连接;使用ThreadLocal解决了同一个连接问题,但是关闭连接呢,该如何去处理,如果第一个dao关闭了连接,对于第二个dao来说,连接根本得不到了。
如果将获取数据库的连接放到service层,以本文这种案例所做的来说,一个是通过DBUtils获取一个连接,传递到dao,耦合度太高。另一个是将连接的操作(开关、事务相关)都放入到service层去处理。
框架基本上就是将连接操作的过程提前到了service上了,
@Transaction
注解就能实现事务的完整性和一致性,当然肯定也保证了同一个连接,底层通过AOP的方式,将与业务代码无关的操作切入到方法中,这就是面向切面编程。
下面通过如下代码说明 ThreadLocal 的使用方式
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
int threads = 3;
CountDownLatch countDownLatch = new CountDownLatch(threads);
InnerClass innerClass = new InnerClass();
for(int i = 1; i <= threads; i++) {
new Thread(() -> {
for(int j = 0; j < 4; j++) {
innerClass.add(String.valueOf(j));
innerClass.print();
}
innerClass.set("hello world");
countDownLatch.countDown();
}, "thread - " + i).start();
}
countDownLatch.await();
}
private static class InnerClass {
public void add(String newStr) {
StringBuilder str = Counter.counter.get();
Counter.counter.set(str.append(newStr));
}
public void print() {
System.out.printf("Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n",
Thread.currentThread().getName(),
Counter.counter.hashCode(),
Counter.counter.get().hashCode(),
Counter.counter.get().toString());
}
public void set(String words) {
Counter.counter.set(new StringBuilder(words));
System.out.printf("Set, Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n",
Thread.currentThread().getName(),
Counter.counter.hashCode(),
Counter.counter.get().hashCode(),
Counter.counter.get().toString());
}
}
private static class Counter {
private static ThreadLocal<StringBuilder> counter = new ThreadLocal<StringBuilder>() {
@Override
protected StringBuilder initialValue() {
return new StringBuilder();
}
};
}
}
实例分析
ThreadLocal本身支持范型。该例使用了 StringBuilder 类型的 ThreadLocal 变量。可通过 ThreadLocal 的 get() 方法读取 StringBuidler 实例,也可通过 set(T t) 方法设置 StringBuilder。
上述代码执行结果如下
Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:418873098, Value:0
Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1609588821, Value:0
Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1780437710, Value:0
Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1609588821, Value:01
Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:418873098, Value:01
Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1609588821, Value:012
Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1609588821, Value:0123
Set, Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1362597339, Value:hello world
Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1780437710, Value:01
Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:418873098, Value:012
Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1780437710, Value:012
Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:418873098, Value:0123
Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1780437710, Value:0123
Set, Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:482932940, Value:hello world
Set, Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1691922941, Value:hello world
从上面的输出可看出
- 从第1-3行输出可见,每个线程通过 ThreadLocal 的 get() 方法拿到的是不同的 StringBuilder 实例
- 第1-3行输出表明,每个线程所访问到的是同一个 ThreadLocal 变量
- 从7、12、13行输出以及第30行代码可见,虽然从代码上都是对 Counter 类的静态 counter 字段进行 get() 得到 StringBuilder 实例并追加字符串,但是这并不会将所有线程追加的字符串都放进同一个 StringBuilder 中,而是每个线程将字符串追加进各自的 StringBuidler 实例内
- 对比第1行与第15行输出并结合第38行代码可知,使用 set(T t) 方法后,ThreadLocal 变量所指向的 StringBuilder 实例被替换
参考:
Java进阶(七)正确理解Thread Local的原理与适用场景
12.3 ThreadLocal原理
ThreadLocal的常用方法
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
get()
方法是用来获取ThreadLocal在当前线程中保存的变量副本
set()
用来设置当前线程中变量的副本
remove()
用来移除当前线程中变量的副本
initialValue()
是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,下面会详细说明。
ThreadLocal
为每个线程创建了共享变量的副本,要弄清原理,只要弄清楚一个线程是如何关联到一个共享变量的副本。
首先看下get方法的实现
/**
* 返回当前线本地变量的拷贝,如果没有改变量,它会调用initialValue方法进行第一次初始化
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
第一句获得当前线程对象,然后调用getMap()方法,将线程对象以函参的形式传入其中,获取到ThreadLocalMap
对象,如果不为空则通过调用map对象的getEntry()
方法,将当前ThreadLocal对象传入,返回Entry对象,不为空则返回具体的值;如果map为空,则通过setInitialValue();
初始化。
在getMap(Thread t)
方法,直接返回了一个Thread
对象中的threadLocals属性,在Thread源码中定义着一个这样的属性
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
它保留了ThreadLocal.ThreadLocalMap类型的一个实例,ThreadLocal.ThreadLocalMap
具体是ThreadLocal下的一个静态内部类,其中又定义了Entry这个key-value结构的对象,注意,key是以ThreadLocal
为值。
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
....
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
再看一下setInitialValue()
方法
private T setInitialValue() {
T value = initialValue();// 默认返回null
Thread t = Thread.currentThread();
// getMap就是上面介绍过的getMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
void createMap(Thread t, T firstValue) {
// 实例化一个ThreadLocalMap对象并将当前threadLocal和值作为参数
// 将引用赋值给当前线程的threadLocals属性
t.threadLocals属性 = new ThreadLocalMap(this, firstValue);
}
// 和setInitialvalue方法内部大致差不多
// 只不过这里的value是真实的值,并不是默认的null
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
运行流程:
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的。在调用threadLocal对象的set方法时,将当前ThreadLocal变量作为键值,value为变量副本保存如其中。(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
get流程:getMap(curThread)->curThread.localMap->getEntry(curThreadLocal)->localVariables
总结一下:
1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量;
3)在进行get之前,必须先set,否则会报空指针异常;
如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。
前面我们已经看到在没有set之前调用get返回的对象是null的,虽然其中调用了initialValue(initialValue也是默认返回null),所以下面的代码就会产生空指针异常
public class ThreadLocalDemo {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
ThreadLocal<String> stringLocal = new ThreadLocal<String>();
public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final ThreadLocalDemo test = new ThreadLocalDemo();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 = new Thread(() -> {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
});
thread1.start();
thread1.join();
System.out.println(test.getLong());
System.out.println(test.getString());
}
}
更改代码为如下,就可以不用先set而直接调用get了。
ThreadLocal<Long> longLocal = new ThreadLocal<Long>() {
@Override
protected Long initialValue() {
return Thread.currentThread().getId();
}
};
ThreadLocal<String> stringLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return Thread.currentThread().getName();
}
};