面向对象程序设计(JAVA)复习笔记(下)
五、接口和多态
接口
接口
与抽象类一样都是定义多个类的共同属性
使抽象的概念更深入了一层,是一个“纯”抽象类,它只提供一种形式,并不提供实现
允许创建者规定方法的基本形式:方法名
、参数列表
以及返回类型
,但不规定方法主体
也可以包含基本数据类型的数据成员,但它们都默认为static
和final
所有方法都是抽象方法,接口口里面的所有属性都是常量。
接口的作用
统一开发标准(规范)。接口就是定义了一个标准,使用这个接口的类就是标准的使用者,实现了这个接口的类就是这个标准的实现者。接口的出现,把使用者和实现者之间的直接联系转化为间接联系,把强耦合转换为弱耦合,从而实现解耦
的目的。
实现多继承,同时免除C++中的多继承那样的复杂性
抽象类统一开发标准,但是是单继承
保险公司的例子
具有车辆保险、人员保险、公司保险等多种保险业务,在对外提供服务方面具有相似性,如都需要计算保险费(premium)
等,因此可声明一个Insurable
接口
在UML图中,实现接口用带有空三角形的虚线表示
接口的语法
声明格式为
[接口修饰符] interface 接口名称 [extends 父接口名]{
…//方法的原型声明或静态常量
}
接口的数据成员一定要赋初值,且此值将不能再更改,允许省略final关键字
接口中的方法必须是“抽象方法”,不能有方法体,允许省略public及abstract关键字
接口实例:
接口的实现
接口不能用new运算符直接产生对象,必须利用其特性设计新的类,再用新类来创建对象
利用接口设计类的过程,称为接口的实现,使用implements关键字
语法如下
public class 类名称 implements 接口名称 {
/* Bodies for the interface methods */
/* Own data and methods. */
}
注意:
- 必须实现接口中的所有方法
- 来自接口的方法必须声明成
public
对象转型
对象可以被转型为其所属类实现的接口类型
举例:
getPolicyNumber
、calculatePremium
是Insurable
接口中声明的方法
getMileage
是Car类新添加的方法,Insurable
接口中没有声明此方法
Car jetta = new Car();
Insurable item = (Insurable)jetta; //对象转型为接口类型
item.getPolicyNumber();
item.calculatePremium();
item.getMileage(); // 接口中没有声明此方法,不可以
jetta.getMileage(); // 类中有此方法,可以
((Car)item).getMileage(); // 转型回原类,可调用此方法了
多重继承
Java的设计以简单实用为导向,不允许一个类有多个父类
但允许一个类可以实现多个接口,通过这种机制可实现多重继承
一个类实现多个接口的语法如下
[类修饰符] class 类名称 implements 接口1,接口2, …
{
… …
}
接口的扩展
接口的扩展
接口可通过扩展的技术派生出新的接口
原来的接口称为基接口(base interface)
或父接口(super interface)
派生出的接口称为派生接口(derived interface)
或子接口(sub interface)
派生接口不仅可以保有父接口的成员,同时也可加入新成员以满足实际问题的需要
实现接口的类也必须实现此接口的父接口
接口扩展的语法
interface 子接口名称 extends 父接口名1,父接口名2,…
{
… …
}
Shape是父接口,Shape2D与Shape3D是其子接口。Circle类及Rectangle类实现接口Shape2D,而Box类及Sphere类实现接口Shape3D
实例:
运行:
塑型
塑型(type-casting)
又称为类型转换
方式:
隐式(自动)的类型转换
显式(强制)的类型转换
塑型的对象包括
基本数据类型
将值从一种形式转换成另一种形式
引用变量
将对象暂时当成更一般的对象来对待,并不改变其类型
只能被塑型为
任何一个父类类型
对象所属的类实现的一个接口
被塑型为父类或接口后,再被塑型回其本身所在的类
举例:
隐式(自动)的类型转换
基本数据类型
相容类型之间存储容量低的自动向存储容量高的类型转换
引用变量
被塑型成更一般的类
Employee emp;
emp = new Manager(); //将Manager类型的对象直接赋给
//Employee类的引用变量,系统会自动将Manage对象塑
//型为Employee类
被塑型为对象所属类实现的接口类型
Car jetta = new Car();
Insurable item = jetta;
显式(强制)的类型转换
基本数据类型
(int)871.34354; // 结果为 871
(char)65; // 结果为‘A’
(long)453; // 结果为453L
引用变量:还原为本来的类型
Employee emp;
Manager man;
emp = new Manager();
man = (Manager)emp; //将emp强制塑型为本来的类型
塑型应用的场合包括
- 赋值转换
赋值号右边的表达式类型或对象转换为左边的类型 - 方法调用转换
实参的类型转换为形参的类型 - 算术表达式转换
算数混合运算时,不同类型的项转换为相同的类型再进行运算 - 字符串转换
字符串连接运算时,如果一个操作数为字符串,一个操作数为数值型,则会自动将数值型转换为字符串
当一个类对象被塑型为其父类后,它提供的方法会减少
当Manager
对象被塑型为Employee
之后,它只能接收getName()
,getEmployeeNumber()
方法,不能接收getSalary()
方法
将其塑型为本来的类型后,又能接收getSalary()
方法了
如果在塑型前和塑型后的类中都提供了相同的方法,如果将此方法发送给塑型后的对象,那么系统将会调用哪一个类中的方法?
根据实际情况有两种方法查找方式:
实例方法的查找
类方法的查找
实例方法的查找:
从对象创建时的类开始,沿类层次向上查找
Manager man = new Manager();
Employee emp1 = new Employee();
Employee emp2 = (Employee)man;
emp1.computePay(); // 调用Employee类中的computePay()方法
man.computePay(); // 调用Manager类中的computePay()方法
emp2.computePay(); // 调用Manager类中的computePay()方法
类方法的查找
在引用变量声明时所属的类中进行查找
Manager man = new Manager();
Employee emp1 = new Employee();
Employee emp2 = (Employee)man;
man.expenseAllowance(); //in Manager
emp1.expenseAllowance(); //in Employee
emp2.expenseAllowance(); //in Employee!!!
多态
多态是指不同类型的对象可以响应相同的消息,执行同一个行为,会表现出不同的行为特征。
从相同的基类派生出来的多个类型可被当作同一种类型对待,可对这些不同的类型进行同样的处理,由于多态性,这些不同派生类对象响应同一方法时的行为是有所差别的
例如
所有的Object类的对象都响应toString()方法
所有的BankAccount类的对象都响应deposit()方法
多态的目的
所有的对象都可被塑型为相同的类型,响应相同的消息
使代码变得简单且容易理解
使程序具有很好的“扩展性”
多态的常见形式
多态的前提
有继承/实现关系;
有父类引用指向子类对象;
有方法重写。
一个例子:
绘图——直接的方式
希望能够画出任意子类型对象的形状,可以在Shape
类中声明几个绘图方法,对不同的实际对象,采用不同的画法
if (aShape instanceof Circle) aShape.drawCircle();
if (aShape instanceof Triangle) aShape.drawTriangle();
if (aShape instanceof Rectangle) aShape.drawRectangle();
绘图——更好的方式(多态)
在每个子类中都声明同名的draw()
方法
以后绘图可如下进行
Shape s = new Circle();
s.draw();
Circle属于Shape的一种,系统会执行自动塑型
当调用方法draw时,实际调用的是Circle.draw()
在程序运行时才进行绑定,接下来介绍绑定的概念
绑定指将一个方法调用同一个方法主体连接到一起
根据绑定时期的不同,可分为
早期绑定
程序运行之前执行绑定
晚期绑定
也叫作“动态绑定”或“运行期绑定”
基于对象的类别,在程序运行时执行绑定
一个例子:
多态的应用
技术基础
向上塑型技术:一个父类的引用变量可以指向不同的子类对象
动态绑定技术:运行时根据父类引用变量所指对象的实际类型执行相应的子类方法,从而实现多态性
构造方法与多态
构造方法与其他方法是有区别的
构造方法并不具有多态性,但仍然非常有必要理解构造方法如何在复杂的分级结构中随同多态性一同使用的情况
构造方法的调用顺序
- 调用基类构造方法
- 按声明顺序调用成员构造方法
- 调用自身构造方法
在构造派生类的时候,必须能假定基类所有成员都是有效的。
在构造方法内部,必须保证使用的所有成员都已初始化。
因此唯一的办法就是首先调用基类构造方法,然后在进入派生类构造方法之前,初始化所有能够访问的成员
构造方法中的多态方法
在构造方法内调用准备构造的那个对象的动态绑定方法(抽象方法-抽象类/接口)
会调用位于派生类里的一个方法
被调用方法要操纵的成员可能尚未得到正确的初始化
可能造成一些难于发现的程序错误
内部类
内部类
在另一个类或方法的定义中定义的类
可访问其外部类中的所有数据成员和方法成员
可对逻辑上相互联系的类进行分组
对于同一个包中的其他类来说,能够隐藏
可非常方便地编写事件驱动程序
声明方式
命名的内部类:可在类的内部多次使用
匿名内部类:可在new关键字后声明内部类,立即创建一个对象
假设外层类名为Myclass,则该类的内部类名为
Myclass$c1.class (c1为命名的内部类名)
Myclass$1.class (表示类中声明的第一个匿名内部类)
内部类实现接口
可以完全不被看到,而且不能被调用
可以方便实现“隐藏实现细则”。你所能得到的仅仅是指向基类(base class)或者接口的一个引用
方法中的内部类
在方法内定义一个内部类
为实现某个接口,产生并返回一个引用
为解决一个复杂问题,需要建立一个类,而又不想它为外界所用
六、I/O与文件
输入输出流(java.io)
输入流
为了从信息源获取信息,程序打开一个输入流,程序可从输入流读取信息
输出流
当程序需要向目标位置写信息时,便需要打开一个输出流,程序通过输出流向这个目标位置写信息
输入/输出流可以从以下几个方面进行分类
从流的方向划分
输入流
输出流
从流的分工划分
节点流
处理流
从流的内容划分
面向字符的流
面向字节的流
面向字符的流:专门用于字符数据
面向字节的流:用于一般目的
面向字符的流
面向字符的流
针对字符数据的特点进行过优化,提供一些面向字符的有用特性
源或目标通常是文本文件
面向字符的抽象类——Reader
和Writer
java.io包中所有字符流的抽象基类
Reader提供了输入字符的API
Writer提供了输出字符的API
它们的子类又可分为两大类
节点流:从数据源读入数据或往目的地写出数据
处理流:对数据执行某种处理
多数程序使用这两个抽象类的一系列子类来读入/写出文本信息
例如FileReader/FileWriter用来读/写文本文件
面向字节的流
InputStream
和OutputStream
是用来处理8位字节流的抽象基类,程序使用这两个类的子类来读写8位的字节信息
分为两部分
节点流
处理流
标准输入输出流
标准输入输出流对象
System类的静态成员变量
包括
System.in
: InputStream类型的,代表标准输入流,这个流是已经打开了的,默认状态对应于键盘输入。
System.out
:PrintStream类型的,代表标准输出流,默认状态对应于屏幕输出
System.err
:PrintStream类型的,代表标准错误信息输出流,默认状态对应于屏幕输出
System.in
程序启动时由Java系统自动创建的流对象,它是原始的字节流,不能直接从中读取字符,需要对其进行进一步的处理
InputStreamReader(System.in)
以System.in
为参数创建一个InputStreamReader
流对象,相当于字节流和字符流之间的一座桥梁,读取字节并将其转换为字符
BufferedReader in
对InputStreamReader
处理后的信息进行缓冲,以提高效率
IO异常
多数IO方法在遇到错误时会抛出异常,因此调用这些方法时必须
在方法头声明抛出throws IOException
异常
或者在try块中执行IO,然后捕获catch IOException
文件
在给出 File 对象的情况下构造一个 FileWriter 对象。
FileWriter(File file, boolean append)
在给出文件名的情况下构造 FileWriter 对象,它具有指示是否追加写入数据的 boolean 值。
FileWriter(String fileName, boolean append)
方法:
public void write(int c) throws IOException
写入单个字符c
public void write(char [] c, int offset, int len)
写入字符数组中开始为offset、长度为len的某一部分
public void write(String s, int offset, int len)
写入字符串中开始为offset、长度为len的某一部分
示例:
FileReader类
从文本文件中读取字符
继承自Reader
抽象类的子类InputStreamReader
BufferedReader类
读文本文件的缓冲器类
具有readLine()
方法,可以对换行符进行鉴别,一行一行地读取输入流中的内容
继承自Reader
二进制文件
示例:
FILE类
表示磁盘文件信息
定义了一些与平台无关的方法来操纵文件
创建、删除文件
重命名文件
判断文件的读写权限及是否存在
设置和查询文件的最近修改时间等
构造文件流可以使用File类的对象作为参数
类 java.io.File
提供文件与路径的各种有用信息
并不打开文件,或处理文件内容
示例:
File f1 = new File("/etc/passwd");
File f2 = new File("/etc", "passwd");
上下级文件夹之间使用分隔符分开:
在Windows中分隔符为\
,在Unix/Linux中分隔符为/
。
跨平台的目录分隔符
更专业的做法是使用File.separatorChar
,这个值就会根据系统得到的相应的分割符。
例:new File("c:" + File.separatorChar + "a.txt");
注意:
如果是使用"",则需要进行转义,写为"\"才可以,如果是两个"",则写为"\\"。
本章其余内容略,掌握实验内容即可。(我们期考这部分不是重点,所以这里就简写了不少,需要的请自行参考其他博客)
七、数组集合
对象数组
先了解两种排序:
自然排序和客户化排序
数组
在Java提供的存储及随机访问对象序列的各种方法中,数组是效率最高的一种
类型检查
边界检查
优点
数组知道其元素的类型
编译时的类型检查
大小已知
代价
数组对象的大小是固定的,在生存期内大小不可变
对象数组
数组元素是类的对象
所有元素具有相同的类型
每个元素都是一个对象的引用
集合
把具有相同性质的一类东西,汇聚成一个整体
Java集合不能存放基本数据类型,而只能存放对象。
它们被组织在以Collection及Map接口为根的层次结构中,称为集合框架
集合框架(Java Collections Framework)
为表示和操作集合而规定的一种统一的标准的体系结构
提供了一些现成的数据结构可供使用,程序员可以利用集合框架快速编写代码,并获得优良性能
包含三大块内容
对外的接口:表示集合的抽象数据类型,使集合的操作与表示分开
接口的实现:指实现集合接口的Java类,是可重用的数据结构
对集合运算的算法:是指执行运算的方法,例如在集合上进行查找和排序
集合框架接口
声明了对各种集合类型执行的一般操作
包括Collection、Set、List、SortedSet、Map、SortedMap
Java集合包括三种类型:Set(集)
,List(列表)
,Map(映射)
所有Java集合类都位于java.util
包中
Java集合框架—Collection接口
Collection
接口
声明时可以使用一个参数类型,即Collection<E>
(泛型)
声明了一组操作成批对象的抽象方法:查询方法、修改方法
查询方法
int size() – 返回集合对象中包含的元素个数
boolean isEmpty() – 判断集合对象中是否还包含元素,如果没有任何元素,则返回true
boolean contains(Object obj) – 判断对象是否在集合中
boolean containsAll(Collection c) –判断方法的接收者对象是否包含集合中的所有元素
修改方法包括
boolean add(Object obj) – 向集合中增加对象
boolean addAll(Collection<?> c) – 将参数集合中的所有元素增加到接收者集合中
boolean remove(Object obj) –从集合中删除对象
boolean removeAll(Collection c) -将参数集合中的所有元素从接收者集合中删除
boolean retainAll(Collection c) – 在接收者集合中保留参数集合中的所有元素,其它元素都删除
void clear() – 删除集合中的所有元素
Java集合框架—Set、SortedSet接口
Set接口
扩展了Collection
禁止重复的元素,是数学中“集合”的抽象
对equals
和hashCode
操作有更强的约定,如果两个Set对象包含同样的元素,二者便是相等的
实现它的两个主要类是哈希集合(HashSet)
及树集合(TreeSet)
SortedSet接口
一种特殊的Set
其中的元素是升序
排列的,还增加了与次序
相关的操作
通常用于存放词汇表这样的内容
Set的一般用法
Set举例:
Set<String> set=new HashSet<String>();
String s1=new String(“Hello”);
String s2=new String(“Hello”);
String s3=new String(“World”);
set.add(s1);
set.add(s2);
set.add(s3);
System.out.println(set.size()); //对象的数目为2
HashSet类
HashSet类按照哈希算法来存取集合中的对象,具有很好的存取和查找性能。
当向集合中加入一个对象时,HashSet会调用对象的hashCode()
方法来获得哈希码,然后根据哈希码进一步计算出对象在集合中的位置。
Hash,一般翻译做散列
、杂凑,或音译为哈希,是把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值。
这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间。
不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。
Hash算法也被称为散列算法,Hash算法虽然被称为算法,但实际上它更像是一种思想。Hash算法没有一个固定的公式,只要符合散列思想的算法都可以被称为是Hash算法。
可参考:
哈希
Iterator类
Iterable
接口:实现该接口的集合对象支持迭代,可以配合foreach使用。
Iterator:迭代器,提供迭代机制的对象,具体如何迭代是Iterator接口规范的。包含三个方法:hasNext、next、remove。
如果集合中的元素没有排序,Iterator遍历集合中元素的顺序是任意的,并不一定与集合中加入元素的顺序是一致的。
public interface Iterable<T>
{
Iterator<T> iterator();
}
Iterator功能比较简单,并且只能单向移动:
使用方法iterator()
要求容器返回一个Iterator
。第一次调用Iterator
的next()
方法时,它返回序列的第一个元素。注意:iterator()
方法属于java.lang.Iterable
接口,被Collection
继承。
使用next()
获得序列中的下一个元素。
使用hasNext()
检查序列中是否还有元素。
使用remove()
将迭代器新返回的元素删除。
Iterator是Java迭代器最简单的实现,为List设计的ListIterator具有更多的功能,它可以从两个方向遍历List,也可以从List中插入和删除元素。
TreeSet类
TreeSet类实现了SortedSet
接口,能够对集合中的对象进行排序。
当向集合中加入一个对象时,会把它插入到有序的对象集合中。
TreeSet支持两种排序方式:
自然排序
客户化排序
默认情况下采用自然排序
。
自然排序
对于表达式x.compareTo(y)
,
如果返回值为0,则表示x和y相等
如果返回值大于0,则表示x大于y
如果返回值小于0,则表示x小于y
TreeSet调用对象的compareTo()
方法比较集合中对象的大小,然后进行升序排列,这种方式称为自然排序。
正常是按升序排列,在返回前面加个负号就可以降序排列
JDK类库中实现了Comparable接口的一些类的排序方式。
使用自然排序时,只能向TreeSet集合中加入同一类型的对象,并且这些对象必须实现Comparable
接口。
客户化排序
Java.util.Comparator<Type>
接口提供具体的排序方式,<Type>
指定被比较的对象的类型,Comparator
有个compare(Type x,Type y)
方法,用于比较两个对象的大小。
compare(x, y)
返回值为0,则表示x和y相等
如果返回值大于0,则表示x大于y
如果返回值小于0,则表示x小于y
如果希望TreeSet按照Student对象的name属性进行降序排列,可以先创建一个实现Comparator接口的类StudentComparator。
StudentComparator类就是一个自定义的比较器。
public native int hashCode()
默认返回对象的32位jvm内存地址。
仅当创建某个“类的散列表”时,该类的hashCode()
才有用(作用是:确定该类的每一个对象在散列表中的位置;)
其它情况下(例如,创建类的单个对象,或者创建类的对象数组等等),类的hashCode()
没有作用。
散列表指的是:Java集合中本质是散列表的类,如HashMap,Hashtable,HashSet。
以HashSet为例,来深入说明hashCode()的作用。
hashCode() 和 equals() 的关系
- 不会创建“类对应的散列表”
- 会创建“类对应的散列表”
Java集合框架—List接口
List接口
扩展了Collection接口
可包含重复元素
元素是有顺序的,每个元素都有一个index
值(从0开始)标明元素在列表中的位置
在声明时可以带有一个参数,即List<E>
四个主要实现类
Vector
ArrayList:一种类似数组的形式进行存储,因此它的随机访问速度极快
LinkedList:内部实现是链表,适合在链表中间需要频繁进行插入和删除操作
栈Stack
List的排序
List只能对集合中的对象按索引顺序排序,如果希望对List中的对象按照其他特定的方式排序,可以借助Comparator
接口和Collections
类。
Collections类是对Java集合类库中的辅助类,它提供操纵集合的各种静态方法。
sort(List list)
:对List中的对象进行自然排序。
sort(List list,Comparator comparator)
:对List中的对象进行客户化排序, comparator
参数指定排序方式。
Java集合框架—Map、SortedMap接口
Map接口
不是Collection接口的继承
用于维护键/值对(key/value pairs)
描述从不重复的键到值的映射,是一个从关键字到值的映射对象
其中不能有重复的关键字key,每个关键字最多能够映射到一个值
声明时可以带有两个参数,即Map<K, V>
,其中K表示关键字的类型,V表示值的类型
SortedMap接口
一种特殊的Map,其中的关键字是升序排列的
与SortedSet
对等的Map,通常用于词典和电话目录等
在声明时可以带有两个参数,即SortedMap<K, V>
,其中K表示关键字的类型,V表示值的类型
Map(映射):集合中的每一个元素包含一对键对象和值对象,集合中没有重复的键对象,值对象可以重复。
向Map集合中加入元素时,必须提供一对键对象和值对象。
Map的两个主要实现类:HashMap
和TreeMap
。
Map最基本的用法,就是提供类似字典
的能力。
在Map中检索元素时,只要给出键对象
,就会返回值对象
。
public Set keySet(): 返回键的集合。
public Set<Map.Entry<k,v>> entrySet(): 返回“键值对”的集合。
//Map.Entry的对象代表一个“词条”,就是一个键值对。可以从中取值或键。
HashMap
按照哈希算法来存取键值对象。
为了保证HashMap
能正常工作,和HashSet
一样,要求当两个键对象通过equals()
方法比较为true时,这两个键对象的hashCode()
方法的返回的哈希码也一样。
Map及Map.Entry详解
TreeMap实现了SortedSet接口,能够对键对象进行排序。支持自然排序和客户化排序。
接口的实现
向量
向量(Vector, ArrayList)
实现了Collection接口的具体类
能够存储任意对象,但通常情况下,这些不同类型的对象都具有相同的父类或接口
不能存储基本类型(`primitive`)的数据,除非将这些数据包裹在包裹类中
其容量能够根据空间需要自动扩充
增加元素方法的效率较高,除非空间已满,在这种情况下,在增加之前需要先扩充容量
Vector方法是同步的,线程安全
ArrayList
方法是非同步的,效率较高
Vector类可以实现动态的对象数组。几乎与ArrayList相同。
由于Vector在各个方面都没有突出的性能,所以现在已经不提倡使用。
集合的遍历
遍历的四种方式
增强for循环的遍历:
哈希表
八、线程
多线程编程基础
多道程序设计
在计算机内存中同时存放几道相互独立的程序,使它们在管理程序控制之下,相互穿插地运行。 两个或两个以上程序在计算机系统中同处于开始到结束之间的状态。
多道程序技术运行的特征
多道
宏观上并行
微观上串行(单核CPU)
程序的并发执行
一组在逻辑上互相独立的程序或程序段在执行过程中其执行时间在客观上互相重叠,即一个程序段的执行尚未结束,另一个程序段的执行已经开始的执行方式。
程序并发执行时具有如下特征:
间断性
失去封闭性
不可再现性
进程的定义
进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。是操作系统能够进行资源分配的单位。
进程的特征
结构特征
动态性
并发性
独立性
异步性
线程的定义
线程(thread)
是操作系统能够进行CPU运算调度的最小单位。
它被包含在进程之中,是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务:
减少开销
提高效率
进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程:是进程的一个执行单元,是操作系统能够进行运算调度的最小单位。线程也被称为轻量级进程。
一个程序至少一个进程,一个进程至少一个线程。
为什么会有线程?
每个进程都有自己的地址空间,即进程空间,在网络或多用户交换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。
进程线程的区别:
地址空间
:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。资源拥有
:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。- 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
- 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
- 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
线程是处理器调度的基本单位,但是进程不是。 - 两者均可并发执行。
进程的3种基本状态及其转换
阻塞:
就是程序运行到某些函数或过程后等待某些事件发生而暂时停止CPU占用的情况;也就是说,是一种CPU闲等状态。
什么情况下考虑使用线程?
使用线程的好处有以下几点:
-
使用线程可以把占据长时间的程序中的任务放到后台去处理。
-
用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。
-
程序的运行速度可能加快。
-
在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下可以释放一些珍贵的资源如内存占用等等。
-
……
示例:
多线程最多的场景:
Web服务器
各种专用服务器(如游戏服务器)
后台任务,例如:定时向大量(100w以上)的用户发送邮件
异步处理,例如:发微博、记录日志等
分布式计算、高性能计算
程序、进程、线程
程序(Program)
:是一段静态的代码,它是应用软件执行的蓝本。
进程(Process)
:是程序的一次执行过程,是系统运行程序的基本单位,系统分配管理资源的最小单位。在操作系统中能同时运行多个任务(程序);
线程(Thread)
:是比进程更小的执行单位,相当于一个任务中的一条执行路径,系统运算调度的最小单位。在同一个应用程序中有多个顺序流同时执行。
多线程的目的是为了最大限度的利用CPU资源。
Java中实现多线程的方法有两种:
- 继承
java.lang
包中的Thread
类。 - 用户在定义自己的类中实现
Runnable
接口。
继承java.lang包中的Thread类
Thread类
直接继承了Object类,并实现了Runnable
接口。位于java.lang
包中
封装了线程对象需要的属性和方法
继承Thread
类——创建多线程的方法之一
从Thread
类派生一个子类,并创建子类的对象
子类应该重写Thread
类的run
方法,写入需要在新线程中执行的语句段。
调用start
方法来启动新线程,自动进入run
方法。
定义一个Thread的子类并重写其run方法如:
class MyThread extends Thread {
@Override
public void run() {...}
}
生成该类的对象:
MyThread myThread = new MyThread();
启动或运行线程,Java虚拟机会自动启动线程,从而由Java虚拟机进一步统一调度线程,实现各个线程一起并发地运行。
public void start()
myThread.start();
举例:
public class ThreadCreateDemo1 {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); //该方法调用多次,出现IllegalThreadStateException
}
}
class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("hellow_world!");
}
}
实现Runnable接口
Runnable接口
Thread类
实现了Runnable
接口
只有一个run()方法
更便于多个线程共享资源
Java不支持多继承,如果已经继承了某个基类,便需要实现Runnable接口来生成多线程
以实现runnable的对象为参数建立新的线程
start
方法启动线程就会运行run()
方法
举例:
public class ThreadCreateDemo2 {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
class MyRunnable implements Runnable {
public void run() {
System.out.println("通过Runnable创建的线程!");
}
}
继承Thread和实现Runnable接口的区别:
a.实现Runnable接口避免多继承局限
b.实现Runnable()可以更好的体现共享的概念
线程间的数据共享
用同一个实现了Runnable
接口的对象作为参数创建多个线程
多个线程共享同一对象中的相同的数据
独立的同时运行的线程有时需要共享一些数据并且考虑到彼此的状态和动作。这样就存在了互相干扰数据的问题(共享数据错误)
多线程的同步控制
有时线程之间彼此不独立、需要同步
线程间的互斥
同时运行的几个线程需要共享一个(些)数据
共享的数据,在某一时刻只允许一个线程对其进行操作
“生产者/消费者” 问题
假设有一个线程负责往数据区写数据,另一个线程从同一数据区中读数据,两个线程可以并行执行
如果数据区已满,生产者要等消费者取走一些数据后才能再写
当数据区空时,消费者要等生产者写入一些数据后再取
线程同步Synchronization
互斥
:许多线程在同一个共享数据上操作而互不干扰,同一时刻只能有一个线程访问该共享数据。因此有些方法或程序段在同一时刻只能被一个线程执行,称之为监视区(临界区)。
协作
:多个线程可以有条件地同时操作共享数据。执行监视区代码的线程在条件满足的情况下可以允许其它线程进入监视区。
多线程的线程同步机制实际上是靠锁的概念来控制的。
多线程的锁同步机制
对象锁
。锁住对象,不同实例的锁互不影响。
- 用synchronized修饰实例方法
- 用synchronized修饰代码块
类锁
。对象都共用同一把锁,同步执行,一个线程执行结束、其他对象线程才能够调用同步的部分。不同的类锁互不影响。
- 用synchronized修饰静态方法
- 用synchronized修饰代码块
对象锁的两种形式:(注意synchronized
)
public class Test
{
// 对象锁:形式1(修饰实例方法)
public synchronized void Method1()
{
System.out.println("我是对象锁也是方法锁");
try
{
Thread.sleep(500);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 对象锁:形式2(修饰代码块)
public void Method2()
{
synchronized (this)
{
System.out.println("我是对象锁");
try
{
Thread.sleep(500);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
类锁:
public class Test
{
// 类锁:形式1
public static synchronized void Method1()
{
System.out.println("我是类锁一号");
try
{
Thread.sleep(500);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 类锁:形式2
public void Method2()
{
synchronized (Test.class)
{
System.out.println("我是类锁二号");
try
{
Thread.sleep(500);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
Java 使用监视器机制
每个对象只有一个锁旗标
,利用多线程对锁旗标
的争夺实现线程间的互斥
当线程A获得了一个对象的锁旗标后,线程B必须等待线程A完成规定的操作、并释放出锁旗标后,才能获得该对象的锁旗标,并执行线程B中的操作
将需要互斥的语句段放入synchronized(object){}
语句中,且两处的object是相同的
线程之间的通信
为了更有效地协调不同线程的工作,需要在线程间建立沟通渠道,通过线程间的“对话”来解决线程间的同步问题
java.lang.Object
类的一些方法为线程间的通讯提供了有效手段
wait()
如果当前状态不适合本线程执行,正在执行同步代码(synchronized
)的某个线程A调用该方法(在对象x上),该线程暂停执行而进入对象x的等待池,并释放已获得的对象x的锁旗标。线程A要一直等到其他线程在对象x上调用notify
或notifyAll
方法,才能够再重新获得对象x的锁旗标后继续执行(从wait语句后继续执行)
notify()
随机唤醒一个等待的线程,本线程继续执行
线程被唤醒以后,还要等发出唤醒消息者释放监视器,这期间关键数据仍可能被改变
被唤醒的线程开始执行时,一定要判断当前状态是否适合自己运行
notifyAll()
唤醒所有等待的线程,本线程继续执行
wait、notify/notifyAll 详解
在多线程中要测试某个条件的变化,使用if 还是while?
显然,只有当前值满足需要值的时候,线程才可以往下执行,所以,必须使用while 循环阻塞。注意,wait()
当被唤醒时候,只是让while循环继续往下走.如果此处用if的话,意味着if继续往下走,会跳出if语句块。但是,notifyAll
只是负责唤醒线程,并不保证条件,所以需要手动来保证程序的逻辑。
线程 死锁
阻塞:暂停一个线程的执行以等待某个条件发生。
IO阻塞:
DatagramSocket.recive(); ServerSocket.recive()。
线程阻塞
synchronized(obj)等待obj解锁;
wait(),等待其他线程的notify()。
临界资源
多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用。一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如输入机、打印机、磁带机等。
各进程采取互斥的方式,实现共享的资源称作临界资源。
属于临界资源的硬件有打印机、磁带机等,软件有消息缓冲队列、变量、数组、缓冲区等。
每个进程中访问临界资源的那段代码称为临界区。显然,若能保证诸进程互斥地进入自己的临界区,便可实现诸进程对临界资源的互斥访问。
死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
四个必要条件:
互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
循环等待,即存在一个等待队列:
P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
一个例子:
避免死锁
sychronized
信号量Semaphore
一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire()
,然后再获取该许可。每个release()
添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,
Semaphore
只对可用许可的号码进行计数,并采取相应的行动。拿到信号量的线程可以进入代码,否则就等待。通过acquire()
和release()
获取和释放访问许可。
临界资源:在某一时刻只能独占使用(互斥)、在不同时刻又允许多人共享的资源。
例子:
公司的银行账户,由公司授权多人存、取钱,但每次只允许一个人存取。
手机等产品,先由生产者生产,后才卖给消费者,一个产品不能同时处于生产和消费中。
线程的同步和互斥:处理临界资源,涉及多线程之间相互协作(步骤协调)。
synchronized锁定方法
,犹如对临界资源加“锁”,令方法执行过程对临界资源进行“同步”(协同好步骤)操作。
synchronized
锁定临界资源,语法:
synchronized( 临界资源对象 ){ 操作代码 }
生产者与消费者模型
(1)有一个具有一定容量的存放产品的仓库。
(2)生产者不断生产产品,产品保存在仓库中(产品入仓)。
(3)消费者不断购买仓库中的产品(产品出仓)。
(4)只有仓库有空间,生产者才能生产,否则只能等待。
(5)只有仓库存在产品,消费者才能购买(消费)。
在生产者与消费者模型中,涉及4个概念:产品
、仓库
、生产者
和消费者
,关键是后3个:
仓库,是临界资源,仓库的产品不能同时入仓和出仓。
生产者线程:工人。
消费者线程:购买产品的人。
现实世界中,许多问题都可归结为生产者与消费者模型,比如公司银行账户的存取款操作,银行账户相当于一个海量仓库,生产(存款)能力没有限制。
后台线程
后台线程
也叫守护线程,通常是为了辅助其它线程而运行的线程
它不妨碍程序终止
一个进程中只要还有一个前台线程在运行,这个进程就不会结束;如果一个进程中的所有前台线程都已经结束,那么无论是否还有未结束的后台线程,这个进程都会结束
“垃圾回收”便是一个后台线程
如果对某个线程对象在启动(调用start方法)之前调用了setDaemon(true)
方法,这个线程就变成了后台线程
Java中有两类线程:User Thread(用户线程)
、Daemon Thread(守护线程)
用户线程:运行在前台的线程,也叫普通线程,只完成用户自己想要完成的任务,不提供公共服务。
创建一个无限循环的后台线程,验证主线程结束后,程序结束
运行程序,则发现整个程序在主线程结束时就随之中止运行了,如果注释掉t.setDaemon(true)
语句,则程序永远不会结束
线程的存活周期
线程的生命周期
线程从产生到消亡的过程
一个线程在任何时刻都处于某种线程状态(thread state)
控制线程的生命
结束线程的生命
用
stop
方法可以结束线程的生命
但如果一个线程正在操作共享数据段,操作过程没有完成就用stop结束的话,将会导致数据的不完整,因此并不提倡使用此方法
通常,可通过控制run方法中循环条件的方式来结束一个线程
线程的优先级
线程调度
在单CPU的系统中,多个线程需要共享CPU,在任何时间点上实际只能有一个线程在运行
控制多个线程在同一个CPU上以某种顺序运行称为线程调度
Java虚拟机支持一种非常简单的、确定的调度算法,叫做固定优先级算法。这个算法基于线程的优先级对其进行调度
线程的优先级
每个Java线程都有一个优先级,其范围都在1和10
之间。默认情况下,每个线程的优先级都设置为5
在线程A运行过程中创建的新的线程对象B,初始状态具有和线程A相同的优先级
如果A是个后台线程,则B也是个后台线程
可在线程创建之后的任何时候,通过setPriority(int priority)
方法改变其原来的优先级
基于线程优先级的线程调度
具有较高优先级的线程比优先级较低的线程优先执行
对具有相同优先级的线程,Java的处理是随机的
底层操作系统支持的优先级可能要少于10个,这样会造成一些混乱。因此,只能将优先级作为一种很粗略的工具使用。最后的控制可以通过明智地使用yield()函数来完成
我们只能基于效率的考虑来使用线程优先级,而不能依靠线程优先级来保证算法的正确性
假设某线程正在运行,则只有出现以下情况之一,才会使其暂停运行
- 一个具有更高优先级的线程变为就绪状态(Ready);
- 由于输入/输出(或其他一些原因)、调用sleep、wait、yield方法使其发生阻塞;
- 对于支持时间分片的系统,时间片的时间期满
本文来自作者:CK_0ff,转载请注明原文链接:https://www.cnblogs.com/Ck-0ff/p/16386121.html