day10:
一.Object类
protected void finalize() 回收系统资源
1.使用时机:
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
2.资源释放时机:
当使用的资源对象变成垃圾时,finalize方法则刚刚好,是在对象变成垃圾,并且被垃圾回收器回收的时候会调用这个方法。但是,垃圾回收器回收垃圾对象的回收时机不确定。
3.使用场景:
子类重写finalize方法,以配置系统资源或执行其他清除
二:String类
1.String类的构造方法:
1. public String()——空字符串 ""1. public String(byte[] bytes)——利用字节数组,创建字节数组所表示的字符串1. public String(byte[] bytes,int offset,int length)——利用字节数数组的一部分,创建字符序列, 从byte数组的offset开始的length个字节值1. <br />1. public String(char[] value)——利用一个字符数组创建字符数组,代表的字符序列1. public String(char[] value,int offset,int count)——创建value字符数组中,从第offset位置开始的count个字符,所代表的字符串对象
2.String类的判断功能:
1. boolean equals(Object obj) ——用来比较字符串的内容1. boolean equalsIgnoreCase(String str)——忽略字符串的大小进行比较1. boolean contains(String str)——判断当前字符串对象是否包含,目标字符串的字符序列1. boolean startsWith(String str) ——判断当前字符串对象,是否已目标字符串的字符序列开头1. boolean endsWith(String str)——判断当前字符串,是否以目标字符串对象的字符序列结尾1. boolean isEmpty() ——判断一个字符串,是不是空字符串
3.String类的获取功能
1. int length() —— 获取当前字符串对象中,包含的字符个数1. char charAt(int index) —— 获取字符串对象代表字符序列中,指定位置的字符1. int indexOf(int ch) —— 在当前字符串对象中,查找指定的字符,如果找到就返回字符,首次出现的位置,如果没找到返回-11. int indexOf(String str) ——查找当前字符串中,目标字符串首次出现的位置(如果包含)找不到,返回-1, 这里的位置是指目标字符串的第一个字符在当前字符串对象中的位置1. int indexOf(int ch,int fromIndex) ——指定从当前字符串对象的指定位置开始,查找首次出现的指定字符的位置(如果没找到返回-1)1. int indexOf(String str,int fromIndex)——指定从当前字符串对象的指定位置开始,查找首次出现的指定字符串的位置(如果没找到返回-1)1. String substring(int start)——返回字符串该字符串只包含当前字符串中,从指定位置开始(包含指定位置字符),的那部分字符串1. String substring(int start,int end)——返回字符串,只包含当前字符串中,从start位置开始(包含),到end(不包含)指定的位置,
4.String类的转换方法
1. byte[] getBytes()——获取一个用来表示字符串对象字符序列的,字节数组1. char[] toCharArray() ——获取的是用来表示字符串对象字符序列的,字符数组1. static String valueOf(char[] chs)1. static String valueOf(int i)1. String toLowerCase() ——把字符串全部转化为小写1. String toUpperCase()——把字符串全部装华为大写
5.String类的替换功能
1. String replace(char old,char new)——在新的字符串中,用新(new)字符,替换旧(old)字符1. String replace(String old, String new)——在新的字符串中,用新的字符串(new), 替换旧(old)字符串1. String trim()——在新的字符串中,去掉开头和结尾的空格字符
6.String类的比较功能
1. _int compareTo(String str)_1. _int compareToIgnoreCase(String str)_
字符串的比较:按照字典序,比较字符串的大小。字典序原本的含义是指,英文单词在字典中出现的先后顺序(在字典中,先出现的字符串小,后出现的字符串大)具体到编程语言,是根据两个字符串字符串从左往右数,第一个对应位置的不同字符,来决定两字符串的大小helloheadfpublic interface Comparable {int compareTo(参数)}compareTo 方法定义一种简单的,表示比较大小的结果的映射规则结果:> 0 正整数: 表示比较结果中的 > 关系(当前对象 > 参数对象)< 0 负整数: 表示比较结果中的 < 关系(当前对象 < 参数对象)= 0 : 表示比较结果中的 = 关系(当前对象 = 参数对象)
三:异常
1.异常概述:
- 简单来说异常就是用来表示Java程序运行过程中的错误(信息)- 由来:1.C语言时代的错误处理(效果并不好,依赖程序员的自觉性)- java处理异常的基本理念:尽量把一起错误摒弃在jvm之外,最好在程序编译运行之前发现程序错误(编译器),但是有一部分错误 java程序不运行,编译器发现不了- 错误恢复机制(java异常处理机制):java语言退而求其次可以让java程序运行的时候出错,但是同时,java语言本身又提供了一种通用的错误处理机制——>异常处理机制- 异常处理机制:异常的发现和异常的处理(一致性的错误报告模型)- 错误恢复机制:简单来说,就是一旦发生错误,就把该错误信息,层层向上报告,如果上层知道怎么处理这个错误,上层可以捕获该错误信息,并处理。如果上层不知道不知道该怎么处理,于是可以将错误信息继续上报。
2.异常的分类:
1. Exception:在程序运行中能够处理的错误1. 常见处理方式(根据错误处理方式的不同):1. 编译时的异常_(Checkable Exception)_:可预见的,语法层面强制在代码编写时处理(Exception的所有除RuntimeException之外的所有直接子类)1. 运行时的异常_(RuntimeException):_不可意见的,不要求在代码编写时必须处理(RuntimeException 及其所有子类)2. Error:程序层面无法处理的错误(致命的错误)
3.异常的处理:
1. jvm默认异常处理流程:1. 当我们代码在执行到发生错误的地方1. 一旦发生错误,jvm就会终止我们自己的程序执行,转而执行jvm自己的错误处理流程1. 在发生错误的地方,收集错误信息,产生一个描述错误的对象1. 访问收集到的错误信息,将错误信息,输出到控制台窗口2. 异常处理语法:
try {//可能出现异常的,正常的代码逻辑} catch() {//每一个catch分支对应一个异常处理器//在catch分支中处理具体类型的代码异常}1. 如果try中代码运行时发生了错误,jvm在发生错误的代码处,收集错误信息2. try 块中在错误代码之后的代码,就不会在运行,jvm会跳转到相应的错误处理器中,执行由开发者自己写的,错误处理代码3. 错误处理器中的代码,一旦执行完毕紧接着,程序继续正常执行,执行的是整个try-catch代码块之后的代码注意:catch代码块中的代码,只有try块中的代码执行出错时,才会执行!
在我们的try代码块中,可能会发生不同类型的许多错误,我们通常需要的不同的异常类型,有专门的有针对性的处理。针对不同类型的错误,定义不同的异常处理分支——>多分支的异常处理。
- 我们需要声明分支中的代码,声明每个catch代码块处理不同类型的异常- jvm 不知道知道把异常对象交给哪个异常分支,并执行那个异常分支中的代码,这就存在一个多分支异常处理的匹配问题:
1.根据实际的异常对象的类型,和异常分支(异常处理器)声明的异常类型,从上到下一次做类型匹配
2. 一旦通过类型匹配,发现实际异常对象的类型和Catch分支(异常处理器)声明的异常类型,类型匹配,就把异常对象交过
这个异常分支(异常处理器)
3. 多分支的异常处理的执行,有点类似于多分支if-else的执行,一次匹配,只会执行多个catch分支中的一个
注意事项:
1.如果说,在多catch分支的情况下,如果不同的catch分支,处理的异常类型,有父子关系
那么就一定要注意,处理子类的异常分支写在前面,父类的异常分支写在后面
2. 不是包裹在try块中的代码,一旦产生了异常,都是自己来处理,只有try中异常类型,
有对应类型的异常处理器的时
day11:
day12:
I/O模型:
1.IO概述:
- 在操作系统中,一切需要永久保存的数据都以文件的形式存储,需要长时间永久保存的文件数据,存储在外部设备
- 要显示和运行这些数据,必须将其读取到内存
内存的大小有限,因此常常需要在内存和外设之间交换数据,即I/O
2.IO特点:
数据都是在数据传输通道中传输的,数据传输通道由流对象负责创建
- 数据传输通道一旦创建,我们只需要让待传输的数据,从数据的传输通道的一端流向另一端
让数据流动起来:
1.input:read
2.output: write
3.流的分类:
1. 按照数据流动方向:1. 输入流(input):外设 ——>内存1. 输出流(output): 内存 ——>外设1. (这里的入和出都是以jvm为参照来说的)2. 根据流中流动的数据内容:1. 字节流:流中,流动的数据,是以字节为单位的二进制数据1. 字符流:流中,流动的数据,是以字符为单位的字符数据
- 字符流与字节流的特征:
- 字符流专门用来传输文本数据,简单来说文本数据,就是用文本编辑器打开的时候,我们人可以看懂的数据
- 字节流可以用来传输一切数据类型。主不过针对文本(字符)数据,在某些情况下,字节流操作起来不太方便,除了文本数据之外的其他数据,都是用字节流
- 当我们不确定所要传输的数据类型的时候,一律使用字节流
- 我们要学习的流有4种:
- 字节流:
- 字节输入流:InputStream
- 字节输出流:OutputStream
- 字符流:
- 字符输入流:Reader
- 字符输出流:Writer
- 字节流:
字节流:
1.FileOutputStream的构造方法:
- 创建一个向,由File对象所指定的文件中,写入数据文件字节输出流:FileOutputStream(File file)
创建一个向,由路径名字字符串所指定文件中,写入数据文件字节输出流:FileOutputStream(String name)
2.OutputStream中定义的write方法
- public void write(int b):
- 将指定的字节写入此输入流
- write的常规协定是:向输出流写入一个字节,要写入的字节是参数b的八个低位。b的24个高位将被忽略
- public void write(byte[] b)
- 将b.length个字节从指定的byte数组写入此输出
- 通常将字符串转化为字节数组时,需要用到byte[] bytes = xxx.getBytes()
public void write(byte[] b,int off, int len)
创建文件字节输出流到底做了哪些事情?
- 创建FIleOutputStream对象的时候,jvm首先到操作系统,查找目标文件
- 当发现目标文件不存在的时候,jvm会首先创建该目标文件(内容为空,前提是目标文件的父目录是存在的)
- 当发现目标文件存在的时候,jvm会默认首先清空目标文件的内容,最好准备让FileOutputStream从文件头开始写入数据
- 在内存中,创建FileOutPutStream对象
- 在FileOutputStream对象和目标文件之间建立数据传输通道
- 创建FIleOutputStream对象的时候,jvm首先到操作系统,查找目标文件
- 数据写成功后,为什么要close()? void close()
- 关闭此输出流
- 并释放与此流有关的所有系统资源
- 如何实现数据的换行?
- 换行符在不同的操作系统中,表示方式是不一样的:
- 类unix操作系统中:‘\n’
- windows默认的换行表示:‘\r’+ ‘\n’ 用Windows自带的文本编辑器对于\n是否有换行效果,还和操作系统的版本有关系
- String System.lineSeparator()
- 换行符在不同的操作系统中,表示方式是不一样的:
- 如何实现数据的追加写入?
- 对于同一输出流对象,多次写入的数据,会一次写入目标文件,先写入的数据在前,后写入的数据在后
- 所谓追加写入,针对的是不同的输出流对象,向文件写入数据的情况。指当使用不同的输出流对象向文件中写入数据的时候,从文件已有内容开始写入
- FileOutputStream(File file,boolean append):创建一个向指定file对象表示的文件中写入数据的文件输出流,如果第二个参数为true,则将字节写入文件末尾处,而不是写入文件开始处
-
4.FileInputStream的构造方法:
FileInputStream(File flie):创建文件字节输入流对象,从有File对象指定的目标文件读取字节数据到内存
FileInputStream(String name):创建文件字节输入流对象,从由路径名字符串指定的目标文件读取字节数据到内存
5:创建一个FileInputStream对象,jvm做的工作:
FileInputStream对象在被创建之前,jvm会首先到操作系统中找目标文件
- 找到,就不做任何额外工作
- 找不到,抛出异常FileNotFoundException
- 在jvm内存中,创建FileInputStream对象
在FileInputStream对象和目标文件之间建立数据传输通道
6.InputStream中定义的read方法:
int read():从输入流中读入数据的下一个字节。返回0到255范围内的int字节值,如果因为已经达到流的末尾没有返回值,则返回-1
- 返回值:a.下一个数据字节 b.如果达到流的末尾,则返回-1
- int read(byte[] b):从输入流读取一定数量的字节,并将其存储在缓冲数组b中
- 返回值:a.读入缓冲区的字节数 b.如果因为已到达流末尾而不再有数据可用,则返回-1
- int read(byte[] b, int off, int len):将输入流中最多len个数据字节读入byte数组(从off个位置开始填充)
- public void write(int b):
- 一次复制一个字节数组效率高的原因:
- 在复制文件的时候,我们可以一次复制一个字节,也可以一次复制一个字节数组
- 从read和write的付出额外时间的角度:
1.每次的读或者写,都要付出额外的通信代价(jvm无法独立实现read\write功能,只能请求操作系统)
2.如果一次复制一个字节,这意味着每个字节的读或写都要付出额外的时间代价
3.如果一次复制一个字节数组,意味着,读取多个字节,才付出一次的额外时间代价,平均到每个字节,每个字节付出的额外的时间代价就会小的多。
2.字节缓冲流:
字节流一次读写一个数组的速度明显比一次读写一个字节的速度快很多,这是加入了数组这样的缓冲区的效果,java本身在设计的时候,也考虑到这样的情况,所以提供了字节流缓冲区
- 缓冲字节输出流:BufferOutputStream(OutputStream out) //创建一个新的缓冲输出流,以将数据写入指定的底层输出流
- 字节缓冲输入流:BufferInputStream(InputStream in) //创建一个BufferInputStream并保存其参数,即输入流in,以便将来使用
3.包装流:基于一个已有的底层流,才能创建除新的流,统统称之为包装流
- 对于包装流而言,我们**只需要关闭最上层**的**包装流**即可,因为包装流自己负责去关闭,它所包装的底层流
3.void flush:
刷新此缓冲的输入流
- 缓冲流的close方法:1. 刷新缓冲区(调用flush方法完成)1. 关闭流并释放资源
二:字符流:
1.字符流:
1.引例:练习 读取中文字符可能会出现乱码,不能正常显示
1. 不管英引文字符还是数字字符,它们对应的整数值,就是一个字节值1. 对于中文而言,一个中文字符对应的整数值通常有多个字节值来表示
2.本质:字符流 = 字节流 + 编码表(根据指定编码表编码的过程)
字符流是一种包装流
编解码只针对字符数据,非字符数据(byte,short,long,float,及其相应数组都没有编解码)
java语言层面主要针对字符串:
3.字符流产生原因:数据单位不一致
4.字符:
- 字符在二进制以其对应的整数值存储表示- 字符和其对应的整数值是由字符集或者编码表来指定的(ASCII表,中文编码表,日文编码表)
2.字符数据的编解码:
1. 编码:已知字符,将字符转换为对应的整数值(基于某个字符集完成)(如在键盘将数据输入到计算机就会发生编码过程,计算机中真正存储的是该字符对应的整数值)1. 解码:已知字符对应的整数值求它对应的字符(也必须基于某个字符集完成)(如当我们要输出,计算机中的字符数据的时候,此时需要将字符对应的整数值转换为对应的字符来输出)
乱码问题产生的原因:编解码所使用的字符集不一致
Unnicode字符集:该字符集包含了当今世界上所有的已知字符,并给每一个字符分配了一个唯一的编码值,因为Unicode字符集只规定了编码值,但没有规定编码值的在计算机的存储方式, —-> 于是根据不同的编码值的存储方式出现了Unicode字符集的变种
如:UTF-8(可变长度表示一个字符) 和 UTF-16(也是用两个字节表示一个字符)
ASCII:用一个字符来编码字符(只编码128)
ISO8859-1:利用了ASCII的第一位,欧洲的拉丁字母表
GB2312 GBK 中文表
3.编解码对应的api:
编码:String类中定义的getBytes(String chasrsetName)方法完成编码
解码:String类的构造方法 String(byte[] b,int off,int len, String charsetName)
常识:
1. 一个中文字符,在gbk字节中,由两个字节表示其编码值1. 一个中文字符,在utf-8字符集中,3个字节表示其编码值
默认字符集:jdk中,当我们自己在编解码时,没有指定的编解码所基于的字符集,此时在编解码过程中默认使用的字符集 (Charset.defaultCharset()) 获取默认字符集
1. 在idea中我们看到默认字符集是UTF-8,因为IDEA做了设置1. 在原生的情况下,默认字符集和操作系统有关系
4.转换字符输出流
1.OutputStreamWriter的构造方法:
public OutputStreamWriter(OutputStream out, String charsetName)//创建使用指定字符集的OutputSteamWriterpublic OutputStreamWriter(OutputStream out)
2.所有的字符流,自带(小)缓冲区,是在编解码时使用的
1. 当我们向字符流写入少数数据的时候,数据有可能在自带的缓冲区中,没有写入底层流1. 为了保证即使是少数的字符数据,也能及时的将少数字符写入应当急即使的刷新或关闭流
6.转换字符输入流
Reader是抽象类,我们只能使用其子类InputStreamReader
1.InputStreamReader的构造方法:
public InputStreamReader(InputStream in)//基于字节输入流,创建出基于默认字符集进行默认解码的转化字符输入流对象public InputStreamReader(InputStream in,String charsetName)
2.字符流的read方法:
int read()//读取单个字符://返回:1.作为整数读取的字符,范围在0-65535之间(0x00-0xfff)// 2.如果已达到流的末尾,则返回-1int read(char[] buff)//将字符读入数组//返回:读取的字符数,如果已经达到末尾,则返回-1int read(char[] buff, int off, int len)//将字符读入数组的某一部分//返回:读取的字符数,如果已经达到末尾,则返回-1
3.常见的乱码问题:
1. 利用转换字符输入流,读取操作系统中已经错在的文本文件的内容1. 通常我们自己在操作系统中穿建出的文件,其存储字符数据,是操作系统本地字符集(gbk)1. 如果我们在解码的过程中没有指定,或者指定的字符集不是gbk就会产生乱码2. 利用字符输入输出流读取或者写入数据的时候,输入|输出流本身的编解码不一致
4.不能处理视频图片文件的原因:
1. 对于图片和视频数据,他们都有自己特殊的编码格式,它们所使用的编码格式和我们前面讲的基于字符集,对于字符数据进行的编解码,没有任何关系。1. 所以当我们使用字符输入流来读取图片和视频数据的时候,当字符输入流试图对图片和视频数据进行基于字符串集进行**解码**的时候,会发现一些二进制数值,无法对应到字符集中对应的字符。1. 此时字符输入流要么不然是这些编码,要么把这些没有在字符集中匹配到的编码值替换成特殊编号对应的字符??
这意味着,字符输入流,在读取视频和图片的时候就已经修改了原来的视频和图片
7.FIleReader和FileWriter
1.FileWriter的构造方法:
public FileWriter(String filaName)public FileWriter(String fileName,boolean append) //实现文件字符流的追加写入
2.FileReader的构造方法:
public FileReader(String FileName)public FileReader(String FileName, boolean append)
8.转换流和文件字符流的对比
1. 文件字符流创建字符流对象的语法简单,但他失去了灵活性,无法指定其编解码所使用的的字符集1. 转换流,比较灵活,可以指定编解码所使用的字符集,但是创建转换流对象的语法相对复杂
文件字符流的简单运用:
public static void main(String[] args){Reader reader = new FileReader("a.txt");Writer writer = new FileWriter("d.txt");int len;char[] charBUf = new char[1024];while((len = reader.read(charBUf)) != -1 ){writer.writer(charBUf,0.len);}reader.close;writer.close;
三.字符缓冲流(buffer.p):
1. 出于效率考虑1. 缓冲字符流中定义了一些Reader或Writer父类中没有的方法(newLine和readLine)
BufferWriter(Writer out) //包装流,基于底层字符流Writer(需要通过包装字符流(包装字节流)创建)new BufferWriter(new OutputStreamWriter(new FileOutputStream("")))//构造方法void newLine() //写入一个行分隔符BufferReader(Reader in) //真正读取数据的是底层流,Buffer只负责维护缓冲区String readLine() //按行读取文本内容,通过换行回车“\r”换行"\n"或回车后直接跟着换行// 返回 a.包含该行内容的字符串,不包含任何运行终止符// b.如果已将达到流的末尾,则返回null//按行复制文本的运用while((line = br.readLine() != null)){xxx.write(line);xxx.newLine();}
day14:
一:其他流:
1.标准输入输出流(standard.p):
1. 标准输入流(System.in):代表系统的标准输入,默认输入设备是键盘1. 标准输出流(System.out):代表系统的标准输出,默认输出设备是显示器
practice:利用System.in完成Scanner的nextLine()功能:
//用BufferReader包装System.in//容易产生阻塞,阻塞一直持续到键盘有数据输入,要结束阻塞(等待) 依赖于自定义协议结束阻塞public static void main(String[] args){BufferedReader br = new BufferedReader(new InputStreamReader(System.in));String s;while((s = br.readLine()) != null){if("886".equal(s)){break;} //自定义协议判断输入截止条件System.out.println(s);}}
二:多线程:
1.线程的理解(代码执行角度):
1. 所有的java代码 都是执行在某一条执行路径中,一条执行路径就对应一个线程1. 在同一执行路径中执行的代码,按照代码书写的先后顺序,先后依次执行。1. 在不同执行路径中执行的代码,可以相互独立,“同时”执行,互不影响
2.两个问题:
a. java命令启动Java程序的过程:
1. java命令启动了一个jvm进程1. 该jvm进程,在执行的时候,首先会创建一个线程,main线程1. 在main线程中,运用主类中的main方法代码
b. jvm是多线程
1. 至少在jvm启动之后,还在另外一个线程中同时执行者垃圾回收器的功能
c.不是所有引用程序运行在同一个jvm中 ,一个java程序运行在一个jvm中。启动多少个应用程序,就各自运行在各自的jvm中。
3.多线程相关api:
1. public final String getName: 获取线程名称1. public final void setName : 修改线程名称
如何获取main线程的名称:
Thread.currentThread() //获取当前线程对象的引用
4.线程的优先级
线程的两种主要调度模型:
1. 协同式线程调度:1. 如果使用协同式调度的多线程,线程的执行时间,由线程本身控制。线程把自己的工作执行完之后主动通知系统切到另外一个线程上去1. 最大的好处是实现简单,坏处是执行时间不可控2. 抢占式调度(**由线程的优先级决定 **线程的静态 + 线程的动态优先级)(java采用此调度方式):1. 如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不是由线程本身决定。1. 最大的好处是,线程的执行时间是可控的。
三:线程相关api
1.多线程优先级的api(没啥用):
1. public final int getPriority() //获取优先级1. public final void setPriority() //改变优先级1. 注意事项:1. 多线程的优先级的取值范围为1-101. 线程的默认优先级为5
然而java语言设置的优先级仅仅被看做是一种对操作系统的“建议”,实际上操作系统本身又它自己的一套线程优先级(静态+动态)
2.线程控制api:
1. public static void sleep(long millis) //在指定的毫秒数内让当前正在执行的线程休眠(暂停执行) //TimeUnit.SECONDS.sleep()1. public final void join() //等待该线程终止 --> 1.当前线程等待,即调用join方法的线程
2.在哪个线程对象上调用jioin方法,当前线程就等待哪个线程调用join方法先执行
c. public static void yield(): //暂停当前正在执行的线程,并执行其他线程 //但真正使用起来线程很难控制执行其他线程
d.public void setDaemon(boolean on) //将该线程标记为守护线程或用户线程,传递true为 守护线程,传递为flase为用户线程
// 当正在运行的线程都是守护线程时,java虚拟机退 出
//使用场景:垃圾回收器适合运行在守护线程中
e.public void interrupt() //中断线程的阻塞状态 如果线程在调用Object类的join,sleep 方法受阻,并且会收到一个InterruptException异常 这个 方法偏向底层,我们短期内并不会用到
四:线程的生命周期:
线程刚被创建时处于新建状态,当调用start方法时,线程会处于就绪状态(万事具备,只欠CPU);通过线程调度获得CPU后处于执行态;当线程调用了sleep,join方法时,操作系统会剥夺线程使用的CPU进而变为阻塞状态(处理缺少CPU,还需要满足程序执行所需的其他条件);当它所等待的条件重新发生的时候,再处于就绪状态
当线程中的run方法执行完毕,此线程就处于结束状态。处于结束状态的线程,该线程对象等待被垃圾回收器回收。
五:线程实现方式:
1. 实现Thread的方法(方式一)
1. 继承Thread1. 重写子类的run方法1. 创建该子类对象1. 启动线程 Start()- 注意事项:5. 只有一个Thread类对象才代表一个线程5. 重写run方法的原因:只有Thread的run方法才能执行在程序中,所以要将我们写的run代码运行在程序中必须重写run方法5. 在要我们在**run**方法中调用其他方法,也可以让其他方法的代码运行在子线程中,一个方法,**在哪个线程中被调用,被调用的方法就运行在调用它的线程中。**5. 启动线程,必须使用start方法启动线程(普通方法的调用,该方法在main方法中调用,此次方法被调用在main方法的主线程中)5. 一个线程只能被启动一次,同一个线程不能启动多次;创建多个线程对象可以启动多个线程
2.线程的第二种实现方式(thread.p.basic.demo2)
1. 定义实现Runnable接口的子类1. 实现Runnable接口的run方法1. 创建该子类对象1. 创建线程,在创建Runnable子类对象作为初始化参数,传递给Thread1. 启动Thread对象
第二种实现方式的特点:
线程的第二种执行方式逻辑十分清晰:
1. 线程就是一条执行路径,至于在线程这条执行路径上,究竟执行的是什么样的具体代码应该和线程本身没有关系1. 也就是说,线程和在线程(执行路径)上执行的任务应该是没有什么直接关系的1. 线程实现的第二种方式,把线程(Thread对象代表的线程)和在 线程上执行的任务(Runnable子类对象)分开
3.两种实现方式的对比:
1. 方式一实现步骤较方式二少1. 方式一的实现方式,存在单重继承的局限性1. 方式二将线程和任务解耦1. 方式二,便于多线程**数据的共享**
多线程的数据安全问题:
//需求:多线程仿真如下场景:// 假设A电影院正在上映某电影,该电影有100张电影票可供出售,// 现在假设有3个窗口售票。请设计程序模拟窗口售票的场景。// 分析:// 3个窗口售票,互不影响,同时进行。 3个窗口需要用3个线程来模拟// 3个窗口共同出售这100张电影票。 3个窗口(3个线程的数据共享)//线程实现方式一来模拟:public static void main(String[] args) {SalesWindow window1 = new SalesWindow("窗口一");SalesWindow window2 = new SalesWindow("窗口二");SalesWindow window3 = new SalesWindow("窗口三");window1.start();window2.start();window3.start();}}class SalesWindow extends Thread{static int ticket= 100;public SalesWindow(String name){super(name);}@Overridepublic void run(){while(ticket > 0){System.out.println(getName() + "售出了第" + ticket-- + "张票");}}}//线程实现方式二来模拟:public static void main(String[] args) {SalesTask salesTask = new SalesTask();Thread window1 = new Thread(salesTask, "窗口一");Thread window2 = new Thread(salesTask, "窗口二");Thread window3 = new Thread(salesTask, "窗口三");window1.start();window2.start();window3.start();}}class SalesTask implements Runnable {// 模拟待售出的100张票int tickets = 100;@Overridepublic void run() {while (this.tickets > 0) {// 模拟售票延时try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println( Thread.currentThread().getName() + "售出了第" + tickets-- + "张票");}}}//出现的问题: 1.同一张票,被售出多次窗口1售出了第94张票窗口2售出了第94张票2.售出了不存的票窗口2售出了第1张票窗口3售出了第2张票窗口1售出了第0张票
day15
一:多线程的数据安全问题
1.两种常见的数据安全问题
1. 多卖问题:1. 前提:tickets--1. 假设当前tickets是841. 窗口一线程:拼接待输出的字符串:窗口1售出了第84,发生了线程切换。1. 切换到窗口二的线程:拼接待输出的字符串:窗口二售出了第84张票2. 超卖问题:1. 假设当前的tickets的值是11. 窗口二线程,1>0 窗口二线程认为有票,进入循环准备卖票,发生了进程切换1. 切换到窗口一线程,1>0,窗口一也认为有票,进入循环准备卖票
3.不管是多卖问题还是超卖问题都属于多线程数据安全问题——> 多线程运行环境下,多线程访问共 享数据访问到了错误的共享数据
2.上述问题产生原因:
1. 多线程运行环境1. 数据共享1. 共享数据的非原子操作(原子操作:一组操作,要么不执行,要么一次执行完毕)
3.解决方法:
1. 多线程运行环境(需求决定,打破不了)1. 数据共享(需求决定,打破不了)1. 将一个线程对共享数据的**非原子**操作变成**原子**操作
=>多线程安全问题的解决就变成如果将一组操作变成原子操作的问题
思路1:
如果我们能在一个线程中对共享变量的一组操作的执行过程,能够阻止线程切换,那么很自然这一组操作就变成了原子操作。但是这种思路我们实现不了,在抢占式线程调度中,代码层面无法控制线程调度
思路2:
1. 只有加锁的线程能够访问到共享变量1. 而且,在加锁线程,没有完成对共享变量的一组操作之前,不会释放锁1. 只要不释放所,其他线程即使被调度执行,也无法访问共享变量
Java中构建原子操作,最简单的方式—同步代码块
4.同步代码块:
1.synchronized(锁对象){
需要同步的代码块(通常是,访问共享变量的一组操作)
}
2.同步代码块的细节:
- **synchronize代码块中的锁对象**,可以是java语言中的任何一个对象,任何一个对象都可以充当锁的角色,仅限synchronize代码块中- 因为java中所有对象,内部都存在一个标志位,表示加锁和解锁的状态,- 所以其实锁的对象就充当着锁的角色。所谓的加锁解锁- 一个对象在堆上,每个对象内部都存在标志位,用来模拟加锁和释放- 锁的状态其实就是设置随对象的标志位,来表示加锁的状态。- 我们的代码块都是执行在某一条执行路径中,当某个线程执行同步代码块时,会尝试在当前线程中,对锁对象加锁- 此时,如果对象处于未加锁状态,jvm就会设置锁对象的标志位,来表示加锁的状态- **一个锁对象,同时只能被一个线程加锁成功**- 此时,如果锁对象已经被加锁,且加锁的线程不是当前线程,系统会让当前线程处于阻塞状态,直到加锁线程,执行完成了对共享变量的一组操作,并释放锁。- **加锁线程如何释放锁:**当加锁线程,执行完成同步代码块中的代码,在退出同步代码块之前,jvm会自动清除锁对象的标志位,将锁对象变为未上锁状态(释放锁)
通过加锁释放锁,构造同步代码快,实现线程同步,解决多线程数据问题
5.线程同步和异步:
同步:你走我不走,你不走我走。所有加锁失败的线程,步调变的一致了,都需要等待锁对象被释放
异步:你走你的,我走我的。多线程天生异步,不同的线程,相互独立互不影响,各自按照步骤
线程同步的优缺点:
1. 优点:解决了多线程数据安全问题1. 缺点:相比于异步,因为等待锁资源而引发的阻塞,降低了程序运行的效率
6.同步方法:
- ** private synchronized void 同步方法的方法体可以被看成是一个大的同步代码快,问题的关键在与同步方法的锁对象是谁**- 同步方法都有锁对象,锁对象隐式给出:- 普通成员方法而言,其同步方法的锁对象就是this- 静态成员方法依赖于类,所以我们的锁对象需要表示一个类,所以静态方法的锁对象,就是定义静态那个类的对应的class对象
二.线程锁:
1.Lock锁:
- 与synchronized锁的**区别**:synchronize只提供了用来模拟锁状态的标志位(加锁和释放锁),但是加锁和释放锁都是由jvm隐式完成的,所以synchronize不是一把完整的锁- 一个Lock对象,就代表一把锁,而且是一把完整的锁。- Lock对象,它如果要实现加锁和释放锁,不需要synchronize关键字配合,它自己就可以完成- Lock(接口):lock()加锁 unlock释放锁- 两种锁对象,实现完全不同- Lock锁对象在任何时刻都表示一把锁,但是任何一个java对象,只有配合synchronize才被看做是一把锁。
2.两种锁的选择:
推荐使用synchronize代码块:
- 两种方式实现线程同步,效果相同,但是使用synchroinze代码块的方式要简单的多- 虽然说jdk早期版本中,两种方式加锁和释放锁,确实由效率上的差别,Lock锁机制释放锁和加锁效率高一些,但是在今天jdk版本中,两种加锁方式和释放锁的效率已经相差无几。
3.二者的联系:
都可以实现线程同步。
synchronized(锁对象) {需要同步的代码}lock.lock()需要同步的代码lock.unlock()Lock l = ...;l.lock();try {// access the resource protected by this lock} finally {l.unlock();}
