JavaSE 有这一篇就够(呕心狂敲41k字,只为博君一点赞!)(二):https://developer.aliyun.com/article/1555487
五.IO流
File类
○ 知识概要:
• 一个File类的对象可以表示一个具体的文件或目录 .
• File对象可以对文件或目录的属性进行操作,如:文件名、最后修改日期、文件大小等.
• File对象无法操作文件的具体数据,即不能直接对文件进行读/写操作
○ File的构造方法:
File( String pathname) 指明详细的文件或目录的路径
//指明详细的路径以及文件名 File file =new File("E:/demo1.txt"); //指明详细的路径以及目录名 File directory =new File("E:/temp"); //注意使用/或\\来区分路径等级
○ File类的常用方法:
注意:delete删除一个文件夹时,文件夹中必须是空的
○ 使用File类创建文件及目录:
① 创建文件
//指明详细的路径以及文件名 File file =new File("E:/demo3.txt"); if (!file.exists()){//判断指向文件是否存在 file.createNewFile();//通过createNewFile()方法创建文件 } file.delete();//删除文件
② 创建文件夹(单级文件夹和多级文件夹)
mkdir( ) 创建单级文件夹 mkdirs( )创建多级文件夹
//指明详细的路径以及目录名 File directory =new File("E:/demo"); directory.mkdir();//mkdir用来创建单级文件夹 File directorise =new File("E:/demo/demo1/demo2"); directorise.mkdirs();//mkdirs用来创建多级文件夹
○ 如何得到一个目录中的所有文件:
思路:对目录进行遍历, 遇到文件直接输出, 遇到子目录再对子目录遍历, 直到遇见文件
import java.io.File; import java.io.IOException; public class Review1 { public static void main(String[] args) throws IOException { //指明详细的路径以及目录名 File directory =new File("E:/IO"); search(directory); } //创建一个方法用来遍历目录里的文件 public static void search(File file){ File[] files =file.listFiles();获取到当前给定目录的子级文件/文件夹 for (File f:files){ if (f.isFile()){//若是文件,直接输出 System.out.println(f); }else { search(f);//若是目录,继续向下查找,直到遇到文件 } } } }
输入(I)与输出(O)
• 输入和输出是一个相对概念,输入和输出是相对于我们的程序 。
○ 输入---Input
- 把电脑硬盘上的数据读到程序中,称为输入,即input,进行数据的read操作.
○ 输出---Output
- 从程序往外部设备写数据,称为输出,即output,进行数据的write操作.
● 如图起连接作用的通道也就是我们通常所说的流 .
字节流与字符流
体系图
○ 知识概要:
字节流:读取时以字节为单位,可以读取任意文件.
字符流:读取时以字符为单位,只能读取文本文件.
字节流中常用类:
- 字节输入流 InputStream
- 字节输出流 OutputStream
字符流中常用类:
- 字符输入流 Reader
- 字符输出流 Writer
常用类的基本方法
📌 InputStream和OutputStream的子类都是字节流, 可以读写二进制文件,主要处理音频、图片、歌曲、字节流处理单元为1个字节。
○ InputStream的基本方法:
- 读取一个字节并以整数的形式返回(0~255),如果返回-1已到输入流的末尾。
- int read() throws IOException
- 读取一系列字节并存储到一个数组buffer, 返回实际读取的字节数,如果读取前已到输入流的末尾返回-1
- int read(byte[] buffer) throws IOException
- 关闭流释放内存资源
- void close() throws IOException
○ OutputStream的基本方法:
- 向输出流中写入一个字节数据,该字节数据为参数b的低8位
- void write(int b) throws IOException
- 将一个字节类型的数组中的从指定位置(off)开始的 len个字节写入到输出流
- void write(byte[] b, int off, int len) throws IOException
关闭流释放内存资源
- void close() throws IOException
- 📌 Reader和Writer的子类都是字符流, 主要处理字符或字符串, 字符流处理单元为1个字符。
○ Reader 的基本方法:
- 读取一个字符并以整数的形式返回, 如果返回-1已到输入流的末尾。
- int read() throws IOException
- 读取一系列字符并存储到一个数组buffer, 返回实际读取的字符数, 如果读取前已到输入流的末尾返回-1。
int read( char[] cbuf) throws IOException
关闭 void close() throws IOException
○ Writer 的基本方法:
- 向输出流中写入一个字符数据,该字节数据为参数b的16位
- void write(int c) throws IOException
- 将一个字符类型的数组中的从指定位置(off set)开始的 length个字符写入到输出流
- void write( char[] cbuf, int off set, int length) throws IOException
- 关闭 void close() throws IOException
字节流读写文件
① 首先们在D盘中创建一个ddd.docx文件(9.86KB),再在E盘中创建一个eee.docx文件(空)
② 代码实现 :
public static void main(String[] args) throws IOException { FileInputStream inputStream1 =new FileInputStream("D:/ddd.docx");//输入流 FileOutputStream outputStream1 =new FileOutputStream("E:/eee.docx");//输出流 int a =0 ; while((a=inputStream1.read())!=-1){//只要a!=-1说明还未读完 outputStream1.write(a);//写内容(以字节为单位) } //关闭通道,否则文件一直处于打开(占用)状态 inputStream1.close(); outputStream1.close(); }
③ 运行程序,可以发现我们成功将ddd.docx文件内容写到了eee.docx文件中 ( 0字节➝9.86KB )
每次读写完后记得关闭通道,否则则文件一直处于打开(占用)状态.
○ 高效文件读写 :
创建一个byte数组,一次读byte数组长度个字节,便于提高读写效率。
public static void main(String[] args) throws IOException { FileInputStream inputStream =new FileInputStream("D:/demo1.txt"); FileOutputStream outputStream=new FileOutputStream("E:/IO.txt"); byte[] bytes =new byte[10]; //read(bytes) 一次读byte数组长度个字节,文件内容读取完后返回-1 //size:每次实际往数组中装入的元素的个数 int size =0; while((size=inputStream.read(bytes))!=-1){ //一次向外写出一个byte数组长度个字节内容,从指定位置开始写,写size个 outputStream.write(bytes,0,size); } inputStream.close(); outputStream.close(); }
字符流读写文件
字符流只能读取文本文件
① 我们先创建一个char1.txt文本, 并写入内容进行读写测试
② 通过代码实现将char1.txt文本的内容读写到char2.txt文本
public static void main(String[] args) throws IOException { FileReader reader =new FileReader("E:/char1.txt"); FileWriter writer =new FileWriter("E:/char2.txt"); int b = 0; while((b=reader.read())!=-1) { System.out.println(b);//打印字符编码 writer.write(b); } reader.close(); writer.close(); }
字符流读取时以字符为单位,会将读到字节结合编码表转换为一个字符编码
③ 我们可以将每次读取到的字符对应的编码打印出来
④ 成功读写内容到char2.txt文本
📌 上述写法的弊端:
每次运行程序会将之前所读写的内容覆盖带掉,不能做到在原内容的基础上续写
○ 解决方法:在字符输出流的对象路径后加上true, 表示可续写.
○ 此时当我们多次运行程序时, 发现之前的所读写的内容依然存在
📌 那么我们怎样进行换行操作?
public static void main(String[] args) throws IOException { FileReader reader =new FileReader("E:/char1.txt"); //保留原来的内容,在原内容基础上向后追加(续写) FileWriter writer =new FileWriter("E:/char2.txt",true); BufferedReader bufferedReader =new BufferedReader(reader); BufferedWriter bufferedWriter =new BufferedWriter(writer); String line = null ; while((line=bufferedReader.readLine())!=null){//只要每次读取不为空,则读取一行 bufferedWriter.write(line);//写一行 bufferedWriter.newLine();//插入换行符 } bufferedReader.close(); bufferedWriter.flush(); bufferedWriter.close();//记得在读写完毕后关闭通道 }
○ 注意:
由于读取一行的方法readLine( )在BufferedReader中,换行方法newLine( )在BufferedWriter类中, 所以需要用到缓冲字符输入输出流。
○ 此时当我们多次运行程序时, 会将每次读取到的内容进行换行读写, 便于记录数据。
节点流与处理流
● 按 封装类型 流又分为:
- 节点流:直接封装的是文件, 数据.
- 处理流:封装的是其他节点流对象; 可以提供缓冲功能, 提高读写效率.
● 节点流中常用类:
- 字节输入流 FileInputStream
- 字节输出流 FileOutputStream
- 字符输入流 FileReader
- 字符输出流 FileWriter
● 处理流中常用类:
- 缓冲字节输出流 BufferedOutputStream
- 缓冲字节输入流 BufferedInputStream
- 缓冲字符输入流 BufferedReader
- 缓冲字符输出流 BufferedWriter
代码演示:
public static void main(String[] args) throws IOException { FileInputStream inputStream = new FileInputStream("D:/demo1.txt"); //封装的是一个节点流对象,可以提供缓冲功能,称为处理流/包装流 BufferedInputStream bufferedInputStream =new BufferedInputStream(inputStream,20); FileOutputStream outputStream =new FileOutputStream("E:/IO.txt"); BufferedOutputStream bufferedOutputStream =new BufferedOutputStream(outputStream,20); int size =0; byte[] bytes =new byte[10]; while ((size=bufferedInputStream.read(bytes))!=-1){ bufferedOutputStream.write(bytes,0,size); } bufferedInputStream.close(); bufferedOutputStream.close(); }
缓存区流底层代码:
对象输入输出流 ( 序列化 )
基础理论知识
📌怎么理解对象输入输出流 ?
○ 把java中的对象输出到文件中,从文件中把对象输入到程序中.
📌为什么要这样做(目的) ?
当我们创建一个对象时, 如new Student( "小张",20 ); 数据存储在对象中, 对象是在内存中存储的,一旦程序运行结束, 对象就会销毁, 有时需要将对象的信息长久保存,就需要将对象输入到文件中。
( 例如系统升级,关闭服务器时将对象保存起来,升级完毕后再重新把数据还原回来.)
📌对象的序列化和反序列化:
○ 把对象输入到文件的过程也称为对象的序列化.
○ 把对象从文件输入到程序的过程称为对象的反序列化, 反序列化时会生成一个新的对象, 所以反序列化也是创建对象的一种方式.
📌注意:
当一个类的对象需要被序列化到文件时, 这个类必须要生成一个序列化编号。
○ 如果一个类需要被序列化到文件中, 那么这个类就需要实现Serializable接口, 实现后, 会自动的为该类生成一个序列化编号.
📌关于序列化编号的意义:
○ 编号是类的唯一标识,但是自动生成的编号在类信息改变后会重新为类生成一个编号。
○ 可以在类中显示的生成一个编号,这样类信息修改后,编号也不会改变。
📌常用类及基本方法:
- 对象的输出流: ObjectOutputStream
- 对象的输入流: ObjectInputStream
- 在ObjectInputStream中用readObject()方法可以直接读取一个对象。
- 在ObjectOutputStream中用writeObject()方法可以直接将对象保存到输出流中。
生成序列化ID教程
📌 如何在IDEA中设置, 使可以在类中生成序列化ID ? ( 教程 )
📌 设置成功后, 当我们把鼠标移至类名处, 点击serialVersionUID即可生成编号
代码实践与测试
① 我们首先创建一个学生类,需要将学生信息序列化到文件中,切记需要实现Serializable接口.
import java.io.Serializable; //如果一个类需要被序列化到文件中,那么这个类就需要实现Serializable接口 public class Student implements Serializable { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
② 进行序列化操作,将对象数据输出到文件中,使对象的信息可以做到持久化。
//对象输出 (对象的序列化) Student student =new Student("小魏",20);//创建对象 FileOutputStream fileOutputStream = new FileOutputStream("E:/obj.txt"); ObjectOutput output = new ObjectOutputStream(fileOutputStream); output.writeObject(student); output.flush(); output.close();
③ 运行程序后发现, 对象信息保存在了文件obj.txt中(格式为乱码,但不影响最后反序列化输入结果)
④ 进行反序列化操作, 将之前保存在文件obj.txt的对象信息输入到程序中。
public static void main(String[] args) throws IOException, ClassNotFoundException { //对象输出 (对象的序列化) /* Student student =new Student("小魏",20);//创建对象 FileOutputStream fileOutputStream = new FileOutputStream("E:/obj.txt"); ObjectOutput output = new ObjectOutputStream(fileOutputStream); output.writeObject(student); output.flush(); output.close(); */ //对象输入 (对象的反序列化) FileInputStream inputStream = new FileInputStream("E:/obj.txt"); ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); Student student = (Student) objectInputStream.readObject();//强制类型转换 System.out.println(student);//打印对象信息 objectInputStream.close();//记得关闭流通道 }
☞ 运行结果:
六. 异常处理机制
本章学习内容:使用异常处理机制,对程序运行过程中出现的异常情况进行捕捉并处理.
📌 Java异常概述
○ 异常的概念:
• 程序在运行过程中出现的不正常情况;
例如用户输入数据有问题,读写文件时文件被强制删除了,网络传输过程中突然断网...
• 出现异常后,会导致jvm(虚拟机)停止运行,后续程序无法执行.
○ 注意:
• 异常指的并不是语法错误. (语法错误,编译不通过,不会产生字节码文件,根本不能运行)
○ 异常的抛出机制 :
java中把不同的异常用不同的类表示, 一旦发生某种异常, 就创建该异常类型的对象, 并且抛出;
然后程序员可以捕获到这个异常对象, 并处理; 如果没有捕获这个异常对象, 那么这个异常将会
导致程序终止。
○ java中默认的异常处理机制:
- 将出现的异常,按不同的类型分类,为每种异常封装了一个类来进行标识.
- 当出现某种类型的异常情况时,会抛出此类的对象,然后终止虚拟机的运行.
○ 异常信息:
• 异常的类型 (在哪种情况下出现,结合API定位) • 异常原因 • 异常位置
📌 Java异常体系结构
○ java.lang.Throwable : 异常体系的超类
○ Error 错误:
- 是虚拟机和Java代码无法解决的问题,例如虚拟机内部异常,内存不够用了.
- 堆溢出: OutOfMemoryError
- 栈溢出: StackOverflowError
○ Exception 异常:
这类异常时可以通过异常处理机制进行处理的一般性问题.
📌 常见的异常
ArithmeticException 算术异常
ArrayIndexOutOfBoundsException 数组索引越界
StringIndexOutOfBoundsException 字符串索引越界
ClassCastException 类型转换异常
NumberFormatException 数字格式化异常
NullPointerException 空指针异常
○ 代码演示:
public static void main(String[] args) { /* int a = 10; int b =0 ; System.out.println(a/b); System.out.println("*****"); */ //ArithmeticException 算术异常 /* int[] c=new int[2]; c[3] = 0; */ //ArrayIndexOutOfBoundsException 数组索引越界 /* String s = "abc"; s.charAt(4); */ //StringIndexOutOfBoundsException 字符串索引越界 /* Object o = new Integer(10); String so = (String) o; */ //ClassCastException 类型转换异常 /* Integer.parseInt("abc"); */ //NumberFormatException 数字格式化异常 /* String ss = null; ss.length(); */ //NullPointerException 使用null中的方法称为空指针异常 }
📌 异常处理
⦁ Java中使用异常处理机制为程序提供了错误处理的能力
⦁ 在编码时,就针对可能出现问题的代码,预先编写一些处理机制,当程序运行出现异常时执行处理机制,之后继续执行后续的程序。
● Java的异常处理是通过5个关键字来实现的:try、catch、 finally、throw、throws
○ try{...}catch(){...}
try{
编写可能出现异常的代码
}catch(异常类型){
处理机制
}
代码演示:
public static void main(String[] args) { int a =10; int b = 0; try{ int c = a/b; }catch (ArithmeticException aindex){ aindex.printStackTrace(); //打印异常信息到控制台 /* 一般在开发调试阶段使用,供开发人员定位异常问题 */ System.out.println("算术异常"); } }
运行结果:
一个try代码块可以对应多个catch
public static void main(String[] args) { try { String s =null; s.length(); int num =Integer.parseInt("1a"); int[] b =new int[5]; for (int i = 0; i <=b.length ; i++) { System.out.println(b[i]); } } catch (ArrayIndexOutOfBoundsException aindex) { aindex.printStackTrace();//打印异常信息 System.out.println("数组索引越界,越界索引:"+aindex.getMessage()); //getMessage()得到异常原因 }catch (NumberFormatException nex){ nex.printStackTrace(); System.out.println("数字格式化异常:"+nex.getMessage()); }catch (Exception e){//在具体捕获类型后catch Exception,可以捕获任意类型,但是必须放在最后面 e.printStackTrace(); System.out.println("系统繁忙~请稍后再试!"); } System.out.println("继续执行后续程序"); }
当我们不确定捕获类型时可以使用Exception; Exception可以捕获任意类型, 但是需要放在具体捕获类型后catch, 否则放到首部会覆盖其他具体的捕获类型。
○ finally{...}
finally块中的内容总是会执行的, 且只能有一个finally语句.
try{
编写可能出现异常的代码
}catch(异常类型){
处理机制
}finally{
代码总能执行
}
○ 使用finally的两种情形:
- 当catch错误, 异常没有被捕获到, 后面的代码无法执行, 但finally中的代码是可以执行的.
public static void main(String[] args) { int[] a = new int[5]; try { for (int i = 0; i <= a.length; i++) { System.out.println(a[i]); } }catch (NumberFormatException nex){ nex.printStackTrace(); System.out.println("数字格式化异常:"+nex.getMessage()); }finally { System.out.println("继续执行后续程序"); } }
- 确保在出现异常的情况下, 依然最终把流对象关闭掉 .
public static void main(String[] args) throws IOException { FileInputStream inputStream =null; try{ inputStream =new FileInputStream("F:/demo.txt"); //文件若找不到,就会出现异常 }catch (FileNotFoundException e){ e.printStackTrace(); System.out.println("文件找不到异常"); }finally { //确保在出现异常时,仍然可以把流对象关闭掉 if (inputStream!=null){ inputStream.close(); } } }
七 .网络编程
1. 网络编程概述
✍什么是计算机网络?
把分布在不同地理区域的计算机设备, 通过物理线路连接起来,最终实现数据传输, 资源共享.
📌 网络编程概述
✍什么是计算机网络?
把分布在不同地理区域的计算机设备, 通过物理线路连接起来,最终实现数据传输, 资源共享.
✍什么是网络编程?
在网络的基础上,开发的程序能够进行数据传输.
java语言是支持网络的, 并且将网络连接的细节都封装起来了,对外提供一套网络库,就可以进行统一环境的网络编程
✍要进行网络数据传输的核心问题?
如何找到网络世界中的目标主机和程序?(IP和端口)
找到后如何高效安全的进行数据传输?(协议)
OK那就让我们带着这两个问题来一探究竟吧!
📌 网络模型
✎ " 理论有七层网络模型-----现实中是四层模型 "
应用层 —— 内容 传输层 —— 加入协议控制
网络层 —— IP 物理层 —— 网线 光纤
📖 网络通信的三要素: IP 端口号 通信协议
○ IP:
- IP地址是指互联网协议地址, 可以理解为计算机的地址 ; 如 ipconfig 局域网IP 192.168 , 本机回环地址127.0.0.1 (访问自己电脑)
○ 端口号:
- 计算机运行的每个程序都对应分配一个整数的编号, 不能重复 ; 范围: 0~65535 ; 一般0~1024已经被系统使用或保留, 例如MySQl的端口号是3306.
📖 端口对应程序 IP对应设备
○ 通信协议:
- 是一种规范和约定,计算机网络中实现通信必须有一些约定,即通信协议;对速率,传输代码,代码结构,出错后如何应对...等制定规则.
📌 TCP协议
✍Transmission Control Protocol 传输控制协议
安全可靠,但传输效率低
① 客户端向服务器端发送数据前,首先要建立连接(测试网络是否通畅)
三次握手
1. 客户端向服务器发送连接请求
2. 当服务器接收到客户端连接请求后,给客户端做出一个回应
3. 客户端为服务器的回应再次做出确认回应
② 正式传输数据
③ 断开时需相互确认
四次挥手
1. 客户端向服务器发送一个断开请求
2. 服务器向客户端做出一个回应
3. 服务器端把没有传完的数据传输完毕,再向客户端做出回应
4. 客户端向服务器端的回应做出回应
断开...
📌 UDP协议
✍User Datagram Protocol 用户数据协议
• 把要发送的数据封装成一个数据包;数据包包含数据、对方的IP、 对方端口。
• 只管发送即可,是否发送成功是不知道的 ( 由于没有建立连接,故发送信息成功与否不晓得 )
优点:不需要建立双方连接,传输速度快.
缺陷:由于没有建立连接,它是不安全的.
📌 TCP网络编程
✎ 搭建服务器
• new ServerSocket(端口)
• ServerSocket中的accept()方法来监听客户端的连接(此时程序阻塞)
try { //创建服务器对象 ServerSocket serverSocket =new ServerSocket(8888);//(端口) System.out.println("服务器启动成功!"); //监听客户端是否发送链接请求服务器 Socket socket= serverSocket.accept();//等待监听客户端的连接 System.out.println("有客户连接到了服务器!"); } catch (IOException e) { e.printStackTrace(); }
✎ 搭建客户端
• new Socket(服务器IP,端口)
try { //创建客户端 Socket socket = new Socket("127.0.0.1",8888);// ("服务器的IP",端口) } catch (IOException e) { e.printStackTrace(); }
✎后续的操作就需要用到IO流了
在客户端输出一个字符串, 并转换为字节的形式写入程序
public static void main(String[] args) { //创建客户端 try { Socket socket=new Socket("127.0.0.1",8888);//服务器的IP 端口 String s="你好世界"; OutputStream outputStream=socket.getOutputStream(); outputStream.write(s.getBytes()); } catch (IOException e) { e.printStackTrace(); } }
在服务器输入客户端输出的字节,并将其转为字符串进行打印
public static void main(String[] args) { //创建服务器对象 try { ServerSocket serverSocket =new ServerSocket(8888); System.out.println("服务器启动成功!"); //监听客户端是否发送链接请求服务器 Socket socket = serverSocket.accept();//监听客户端的连接 System.out.println("有客户端连接到了服务器"); //后续操作... InputStream inputStream=socket.getInputStream(); byte[] bytes =new byte[100]; int size =inputStream.read(bytes); String s =new String(bytes,0,size); System.out.println(s); } catch (IOException e) { e.printStackTrace(); System.out.println("服务器启动失败,端口已被占用"); } }
进阶:实现单线程聊天
○ 我们通过while循环和Scanner控制台输入实现服务器与客户端交替聊天
○ 我们使用包装流DateInputStream,DateOutputStream来输入输出,可以直接读到一个字符串, 无需进行字符串与字节的转换.
/* 服务器 */ static Scanner scanner =new Scanner(System.in); public static void main(String[] args) { try { //创建服务器 ServerSocket server = new ServerSocket(20049); System.out.println("服务器启动成功!"); //监听客户端的连接 Socket socket = server.accept(); System.out.println("有客户端连接到了服务器!"); //服务器接收客户端的消息 while (true) { DataInputStream inputStream = new DataInputStream(socket.getInputStream()); String string = inputStream.readUTF(); System.out.println("服务器:" + string); //向客户端发送消息 System.out.println("---->客户端:"); String str = scanner.next(); DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream()); dataOutputStream.writeUTF(str); } } catch (IOException e) { e.printStackTrace(); } }
/* 客户端 */ static Scanner scanner =new Scanner(System.in); public static void main(String[] args) { try { Socket socket = new Socket("127.0.0.1", 20049); while (true) { System.out.println("--->服务器:"); String s = scanner.next(); DataOutputStream output = new DataOutputStream(socket.getOutputStream()); output.writeUTF(s); DataInputStream inputStream = new DataInputStream(socket.getInputStream()); String string = inputStream.readUTF(); System.out.println("服务器:" + string); } } catch (IOException e) { e.printStackTrace(); } }
📖 补充:
✎ 本机回环地址:127.0.0.1
✎ 获取服务器的IP ( ipconfig 局域网IP 以 192.168 开头 )
八.线程
1. 区分程序、进程、线程
程序:为实现某种功能,使用计算机语言编写的一系列指令的集合。
指的是静态的代码(安装在电脑上的那些文件)
进程:是运行中的程序(如运行中的原神)进程是操作系统进行资源分配的最小单位。
线程:进程可以进一步细化为线程,是进程中一个最小的执行单元,是cpu进行调度的最小单元
例如:QQ中的一个聊天窗口
● 进程和线程的关系:
⑴ 一个进程中可以包含多个线程 (一个QQ程序可以有多个聊天窗口)
⑵ 一个线程只能隶属于一个进程 (QQ的聊天窗口只能属于QQ进程)
⑶ 每一个进程至少包含一个线程,也就是我们的主线程(像java中的main方法就是来启动主线程的)在主线程中可以创建并启动其他线程.
⑷ 一个进程的线程共享该进程的内存资源.
🪅 创建线程
①通过继承Thread来创建线程
• 写一个类继承java.lang.Thread
• 重写run( )
线程中要执行的任务都要写在run( )中,或在run( )中进行调用.
public class Demo1 extends Thread{//继承Thread类 @Override public void run() {//重写run方法 for (int i = 1; i <= 200; i++) { System.out.println("run"+i); } } }
public static void main(String[] args) { //创建线程 Demo1 demo1 = new Demo1(); //启动线程 demo1.start(); for (int i = 1; i <= 200; i++) { System.out.println("main"+i); } }
📖 启动线程调用的是start() ; 不是run()
run()这不是启动线程,只是一个方法调用,没有启动线程,还是单线程模式的。
②通过实现Runnable接口来创建线程
• 创建任务,只先创建线程要执行的任务,创建一个类,实现Runnable接口.
• 重写任务执行的Run()
• 创建线程,并为线程指定执行任务.
public class Demo2 implements Runnable {//实现Runnable接口 @Override public void run() {//重写run方法 for (int i = 0; i < 200; i++) { System.out.println("自定义线程"); } } }
public static void main(String[] args) { //创建任务 Demo2 demo2 = new Demo2(); //创建线程,并指定执行任务 Thread thread = new Thread(demo2); thread.start(); }
● 实现Runnable接口创建的优点:
1. 因为java是单继承,一旦继承一个类就不能在继承其他类,避免单继承的局限。
2. 适合多线程来处理同一份资源时使用
🪅 Thread类中的方法
run() 用来定义线程要执行的任务代码.
start() 启动线程
currentThread() 获取到当前线程(.得到具体信息)
setName() 为线程设置名字
getState() 获取状态
getPriority() setPriority 获取/设置 优先级
sleep() 让当前线程休眠指定时间.
join() 等待当前线程执行完毕,其他线程再执行.
yield() 主动礼让,退出cpu重新回到等待序列.
📖 关于优先级:
【java中默认优先级为5, 设置优先级范围为1~10】 (作用:为操作系统调度算法提供的)
🪅 线程生命周期
线程状态:
新建:刚刚创建了一个线程对象,并没有启动
就绪:调用start() 后线程就进入到了就绪状态(可运行状态),进入到了操作系统的调度队列
运行状态:获得了cpu执行权,进入到cpu执行
阻塞状态:例如调用sleep() ,有线程调用了join(),线程中进行Scanner输入...
死亡/销毁:run()方法中的任务执行完毕了
状态关系图:
🪅 多线程的概念
• 顾名思义指:在一个程序中可以创建多个线程执行.
【优点】提高程序执行效率(多个任务可以在不同的线程中同时执行)
提高了cpu的利用率
改善程序结构,将复杂任务拆分成若干个小任务
【缺点】线程也是程序,线程越多占用内存也越多,cpu开销变大(扩充内存或升级cpu)
线程之间同时对共享资源的访问会相互影响,若不加以控制会导致数据出错.
那么如何解决多线程操作共享数据的问题?
🪅 线程同步与锁(Lock)
多个线程同时访问操作同一个共享的数据( 例如买票、抢购等 )时,可能会引起冲突,所以引入线程 “同步” 机制,即各线程间要有先来后到。
即通过【 排队+锁 】 在关键的步骤处,多个线程只能一个一个的执行.
● synchronized(同步锁)
同步锁对象作用:用来记录有没有线程进入到同步代码块,如果有线程进入同步代码块,那么其他线程就不能进入同步代码块,直到上一个线程执行完同步代码块的内容,释放锁之后,其他线程才能进入。
同步锁对象要求:同步锁对象必须是唯一的.
synchronized(同步锁对象) {
同步代码块
}
● synchronized修饰方法时,同步锁对象不需要我们指定,同步锁对象会默认提供:
1. 非静态方法 ------ 默认是this
2. 静态方法 ------ 锁对象是当前类的class对象(一个类的对象只有一个)
● ReentrantLock与synchronized区别?
- synchronized是一个关键字,控制依靠底层编译后的指令去实现.
- synchronized可以修饰一个方法或一个代码块.
- synchronized是隐式的加锁和释放锁,一旦方法或代码块出现异常,会自动释放锁.
- ReentrantLock是一个类,依靠java底层代码去控制 (底层有一个同步队列)
- ReentrantLock只能修饰代码块.
- ReentrantLock需要手动的加锁和释放锁, 所以释放锁最好写在finally中, 一旦出现异常, 保证锁能释放.
误区:不是只要有线程就需要加锁,只有多个线程对同一资源共享时才加锁
模拟卖票
两个窗口分别售票,票数为10张
public class MyThread extends Thread{//我们使用了继承Thread的方法 static int num =10; //票总数10,且为共享资源,要用static修饰 static String obj = new String();//可以是任意类对象,但必须唯一。 /* synchronized(同步锁对象) { 同步代码块 } */ @Override public void run() {//线程要执行的代码块要写在run()中 while (true){ synchronized (obj){//加锁,一次只能执行一个线程 if(num>0){ try { Thread.sleep(800);//此处加入休眠为了让运行结果更明显,也可不加 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票"); num--; //每抢一张票,总数num(10)就减1 }else{ break; } } } } }
在Main方法中创建线程并启动:
public static void main(String[] args) { //创建两个线程,分别对应两个窗口 MyThread myThread1 = new MyThread(); myThread1.setName("窗口1");//线程1 myThread1.start(); MyThread myThread2 = new MyThread(); myThread2.setName("窗口2");//线程2 myThread2.start(); }
运行结果: