Java的I/O

一、Java的I/O

1、什么是I/O?

在生活中,你需要将U盘的文件 ,拷贝到电脑(或者将电脑的文件拷贝到其他设备), 文件是通过数据流的方式依次到达另一个设备中, 文件的拷贝就是一个输入(Input)和输出(Output)的过程

Java中提供对应的API支持对文件的输入和输出 , java.io.*

2、什么流?

生活中 也存在流的概念,例如 管道中的流水,从管道的入口到达管道出口,一滴水可以从入口流到出口,可以将“水”比作 “字节数据或字符数据”,数据也可以从一端流到另一端。

输入(Input): Java中,以“应用程序(内存)”为中心,将磁盘文件(设备端)到达内存中的过程 称为 输入

输出(Output): 以“应用程序(内存)”为中心,将数据从内存到达磁盘文件(设备端)的过程称为 输出

在这里插入图片描述

根据文件操作方式不同可以将IO分为三类

1、按照读写方向不同: 输入流(InputStream)和输出流(OutputStream)

2、按照数据类型不同: 字节流(Byte)和字符流(Char)

3、按照读写效率不同: 单一流和包装流(buffered等缓冲流)

关于流的分类Java提供 4个顶级抽象类 ,分布构建它们的子类

输入流输出流
字节流InputStream(字节输入流)OutputStream(字节输出流)
字符流Reader(字符输入流)Writer(字符的输出流)

常见的流

1、文件字节输入流和文件字节输出流 : FileInputStream 和 FileOutputStream

2、文件字符输入流和文件字符输出流: FileReader 和 FileWriter

3、缓存字节输入流和 缓存字节输出流 BufferedInputStream 和 BufferedOutputStream

4、缓存字符输入流和缓冲字符输出流 BufferedReader 和BuffereWriter

5、数据输入流和数据输出流: DataInputStream 和 DataOutputStream

6、字节数组输入流 和 字节数组输出流 : ByteArrayInputStream 和 ByteArrayOutputStream

7、字符数组输入流 和 字符数组输出流: CharArrayReader 和 CharArrayWriter

8、转换流: (字节流转成字符流) InputStreamReader 和 OutputStreamWriter

9、对象流(序列化流): ObjectInputStream 和 ObjectOutputStream

10、随机访问流(这个流既可以读,也可以写):RandomAccessFIle

在这里插入图片描述

字节流

​ 定义: 文件的输入输出以一个“字节”为单位,进行流处理

FileInputStream 和 FileOutputStream

读入: 将文件中的数据读到内存中

常用方法:

​ int read() : 一个字节的读取 ,返回字节 的asci码 ,对于汉字会分3次读取

​ int read(byte) : 按一个数组长度读取, 返回实际读取的字节长度, 数据存放在数组中

​ int read(byte , offset , len) : 读取流中指定长度的数据,并存放在指定位置,

​ available() :返回流中剩余的字节长度,如果已读完,则返回0

​ skip(long n ): 丢弃指定的字节长度,从下一个开始读取

`**available**()`  
      File file = new File("d:/aaa.txt");
        FileInputStream fis = new FileInputStream(file);
       //
		byte [] b= new byte[10];
        StringBuffer sb = new StringBuffer();
            //每次读取的长度,  b: 存放数据的数组
          int len = 0;
          while((len = fis.read(b)) !=-1){
              sb.append( new String(b,0,len));
          }

        System.out.println(sb);
        fis.close();
 public static void read2() throws IOException {
        // InputStream是抽象类
        InputStream is = new FileInputStream("d:/aaa.txt");
        //丢弃前两个字节
        is.skip(2);
        System.out.println((char)is.read());
        System.out.println("还剩下多少个字节:"+ is.available());
        // 将后面的字节继续使用字节数组读
        byte [] b = new byte[10];
        int len = is.read(b,1,4);
        // 显示数组中读取的所有数据
        System.out.println(Arrays.toString(b));
        //将数组的内容转成字符串  对于空内容不会转换
        System.out.println(new String(b));

        is.close();


    }

文件写出: 将内存的数据写出到磁盘中

构造方法:

new FileOutputStream(File/String ) : 构造文件对象的写出流, 默认覆盖写出

new FileOutputStream(File/String , append): 构造文件对象的写出流,

append:表示在原有文件上追加数据, false : 覆盖

常用方法:

​ void write(int) : 写出一个字节

​ void writer(byte []) :写出一个字节数组,这里需要指定数组的编码格式 “UTF-8”

​ void writer(byte[] , offerset,len) : 写出一个字节数组,指定数组的长度和下标。 从数组的下标开始写出,len表示写出长度

​ flush() :清空缓存,对于使用缓冲流时,将缓冲强制清空。

   //将内存的数据写出到文件   如果文件不存在,会自动创建, 默认覆盖写入  true:追加
        FileOutputStream fos = new FileOutputStream("d://aaa.txt" ,true);
        String str="今天天气还不错";
        fos.write(99);
        //写出一个字符串    字符串可以转成字节数组 或字符数组
        fos.write(str.getBytes("UTF-8"));
        // 写出指定长度
        fos.write(str.getBytes("UTF-8"),0,3); // 写出这个数组的前2个字节
        // 清空缓存
        fos.flush();
        // 关闭流
        fos.close();
        System.out.println("写出成功");

文件复制:

将文件(图片,文本,视频)从一个目录复制到另一个目录, 其中数据长度不变,通过文件读写的方式完成复制

复制过程:从源文件读取数据,然后将数据再出到目标文件中。

在这里插入图片描述

/**
     * 单个字节复制
     * @param srcFile 源文件
     * @param disFile 目标文件
     */
    public static void copyFile(File srcFile, File disFile){
        FileInputStream fis=null;
        FileOutputStream fos =null;
        try {
            // 源文件输入流
             fis = new FileInputStream(srcFile);
            // 目标文件输出流
             fos = new FileOutputStream(disFile);
            int n=0;
            while( (n =fis.read()) !=-1){
                 //将读到的n写出到 目标文件中
                 fos.write(n);
             }
            System.out.println("复制成功。。");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally{
            //无论是否发生异常 都会关闭流
            try {
                fos.close();
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

    }

    /**
     * 一个字节数组的赋值
     * @param src  源地址
     * @param disc 目标地址
     */
    public static void copyFile(String src,String disc){
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            //创建 字节输入流
            fis=new FileInputStream(src);
            fos = new FileOutputStream(disc);
            int len=0;
            byte [] b = new byte[1024];
            while( (len= fis.read(b)) !=-1){
                // 写出 实际读取的长度 ,为了避免在最后一次写出时出现多余字节
                fos.write(b,0,len);
            }

            System.out.println("复制成功");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally{
            try {
                fos.close();
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

练习一: 将d://mywork目录下的文件 赋值到 e://mywork
​ // 其中 目录下包含子目录 也按照相同的文件目录复制
​ // 思路: 1、先遍历文件夹,递归调用,
​ // 如果是文件先获取文件的父路径,如果不存在,就创建
​ // 如果存在路径就直接复制文件(调用刚才的赋值方法)
​ // 2、 创建父路径使用 mkdirs()
​ // 例如 d://mywork/aaa/1.txt
​ // 目标: e://mywork/aaa/1.txt 可以使用字符串替换 d:替换e:
​ // 先看这个路径是否存在,如果不存在 就创建

字符流

​ 字符流用于读写存储字符的文件, 以一个字符为单位,一依次读取字符 文件, 常用类以 Reader或Writer为父类, 对文件的操作使用 java.io.FileReader 和java.io.FileWriter

​ 读文件 :FileReader

常用方法:

​ new FileReader(path): 通过文件路径构建字符输入流

​ new FileReader(File):通过文件对象构建字符输入流

  • ​ int read() :读取一个字符 ,返回字符的int类型

  • ​ int read(char ):读取字符数组长度的数据 ,返回实际读取字符长度,数据存放在字符数组中

  • ​ int read(char offerset len):读取指定字符长度的数组,返回 实际读取字符的长度,数据存放在字符数组中

  • ​ mark(int) :标记流中当前位置 (读取到哪里了)

  • ​ markSupported():判断此流是否支持mark操作

  • ​ reset(): 重置流数据,(又可从头开始读取)

  • ​ skip(long) :丢弃指定长度字符

读字符文件

    // 1、创建字符输入流
        try {
            FileReader reader = new FileReader("d:/myfile.txt");
            // 丢弃字符
            reader.skip(1);
            //读一个字符
            System.out.println((char)reader.read());
            System.out.println((char)reader.read());
            //读一个字符数组长度
            char [] c = new char[10];
            System.out.println("实际长度:"+reader.read(c));
            System.out.println(new String(c));

            //继续读
            int len =  reader.read(c,0,5);
            System.out.println("字符数组:"+ Arrays.toString(c));
            System.out.println("读指定长度字符个数:"+new String(c,0,len));

            // 将字符流重置
            // reader.reset();
            // System.out.println("重置后继续读:"+ reader.read());

            //System.out.println("是否支持标记字符:"+reader.markSupported());
            //关闭流,后面就不能使用该对象
            reader.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

写文件: 将内存数据写出到文件中,在写出过程中可以 覆盖写出也可以追加写出,FileWriter类创建对象过程

​ new FileWriter(String ):指定写出文件地址

​ new FileWriter(String ,append) : 指定写出文件地址,设置是否追加写出,true表示追加,false表示覆盖

​ new FileWriter(File)指定写出文件对象

​ new FileWriter(File ,append);指向写出文件对象,设置是否可追加

常用方法:

​ writer(int) :写出一个字符

​ writer(String):写出一个字符串

​ writer(char [] ):写出一个字符数组

​ writer(char [] , offerset , len):写出一个指定长度的字符数组

​ flush() :刷新缓冲,

​ close():关闭缓冲

​ append© :将指定字符添加到此流中

   // 1、创建文件写出流 FileWriter
        try {
            // 文件不存在,可自动创建,但是不会创建目录
            File file = new File("d://myabc/aaa.txt");
            //判断文件目录不存在, 先创建目录
            if(!file.getParentFile().exists()){
                //创建该目录
                file.getParentFile().mkdirs();
            }

            FileWriter writer = new FileWriter("d://myabc/aaa.txt");
            // 写一个字符的asci
            writer.write(98);
            //写字符串
            writer.write("hello");
            //写指定长度的字符串
            writer.write("abcdef",0,3); //写abc
            char [] c = {'L','O','L'};
            //写字符数组
            writer.write(c);
            System.out.println("写出成功");
            writer.flush();
            writer.close();

        } catch (IOException e) {
            e.printStackTrace();
        }

关闭和刷新:

对于带有缓冲 功能的写出流,需要先刷新缓冲区,才能将数据写出,如果不刷新则最后不能正常写出。写出流如果刷新后还可以继续写,而关闭了则不能继续写。

面试题 flush 和close的区别?

flush: 刷新缓冲 ,流可以继续使用

close: 先刷新缓冲器,然后再释放系统资源, 关闭后不能继续使用

   try {
            FileWriter writer = new FileWriter("1.txt");
            writer.write("刷");
            writer.flush();
            writer.write("新");
            writer.flush();

            writer.write("关");
            writer.close();
            writer.write("闭"); // 这里抛出异常 , Stream closed
            writer.close();


        } catch (IOException e) {
             e.printStackTrace();
        }

关于换行符

回车符 \r 和换行符 \n :
回车符:回到一行的开头(return)。
换行符:下一行(newline)。
系统中的换行:
Windows系统里,每行结尾是 回车+换行 ,即 \r\n ;
Unix系统里,每行结尾只有 换行 ,即 \n ;
Mac系统里,每行结尾是 回车 ,即 \r 。从 Mac OS X开始与Linux统一。

一、包装流

定义: 在原始字节流或字符流的基础性,为了提高读写效率进行再次处理的流, 称为包装流/处理流

1、缓存字节流 BufferedInputStream 、BufferedOutputStream

​ 由于原始流在文件读写时 效率比较低(操作文件本身占用资源较多),可以通过创建缓冲区的方式提高读写效率, 将读取/写出的数据线放入缓冲区,到达一定数量后再次冲缓冲区读取/写出

​ mark(readLimit) 与 reset()用法

其中reset不能单独使用,必须mark(readLimit) ,readLimit表示标记后最多读取的上限,但是这里标记后读取的内容与BufferedInputStream的缓冲大小有关,比由上限决定,也就是说读取的内容超出上限可以继续重置到mark的位置。

public static void main(String[] args) throws IOException {
        //创建缓冲流
        InputStream is = new FileInputStream("d:/myfile.txt");
        BufferedInputStream bis = new BufferedInputStream(is);
        //是否支持mark  或 reset
        System.out.println(bis.markSupported());
        System.out.println((char)bis.read());//97
        //重置
        bis.mark(3);  // pos标记往后退三个  最多可以读取字节上限

        System.out.println("再次读取:"+(char)bis.read());
        System.out.println("再次读取:"+(char)bis.read());
        System.out.println("再次读取:"+(char)bis.read());
        System.out.println("再次读取:"+(char)bis.read());

       bis.reset(); // 这里 重置后 退回到3个以前的位置

        // 重置后输出
        int n =0;
        while( (n = bis.read()) !=-1){
            System.out.println("重置后;"+(char)n);
        }
        //关闭流
        bis.close();
        is.close();

    }

2、缓存字符流 (BufferedReader 、BufferedWriter)

  public static void main(String[] args) throws IOException {
        // 缓冲字符流 可以一行一行读取   、写出
        BufferedReader br = new BufferedReader(new FileReader("d:/小众网站.txt"));
        //读一行
//        System.out.println(br.readLine());
//        System.out.println(br.readLine());
//        System.out.println(br.readLine());
        String s = null;  //读的数据为空 则不需要读
        while( (s = br.readLine()) !=null){
            System.out.println(s);
        }

        br.close();

        //缓冲写出流
        FileOutputStream   pw =  new FileOutputStream("d:/abcd.txt");
        //由于字节流不能直接放入 字符缓冲区,需要将它转成字符流  使用转换流并可以指定编码格式
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(pw));
        bw.newLine();// 开启新一行(换行)
        bw.write("这是测试转换流的方式");

        bw.close();
    }

3、打印流(输出流) PrintWriter 、PrintStream

 public static void main(String[] args) throws FileNotFoundException {
        // 打印流 ,提供一些打印输出方法

        PrintWriter pw = new PrintWriter("d:/abcd.txt");
        pw.print(100);
        pw.println('a');//换行打印
        pw.print("hello");


        pw.close();

        //System.out   字节打印流 PrintStream

4、数据字节流DataInputStream、DataOutputStream

它们用于读入写出Java基本数据类型的数据到文件或其他设备端,它们也属于包装流

DataOutputStream 常用方法
  • ​ writerByte(byte):写一个字节到设备或文件

  • ​ writerChar(char):写一个字符到设备或文件

  • ​ writerInt(int):写一个4个字节的int到设备或文件

  • ​ writer(boolean):写一个boolean类型到设备或文件

  • ​ writerDouble(double):写一个double类型到设备或文件

  • ​ writerFloat(float):写一个float类型到设备或文件

  • ​ writerLong(long):写一个long类型到设备或文件

  • ​ writerShort(short):写一个short类型到设备或文件

  • ​ writerUTF(String):写一个字符串类型到设备或文件

DataInputStream: 读指定文件的数据,可以读数据类型
  • ​ int readInt() :读一个int类型

  • ​ short readShort():读一个short类型

  • readByte():读一个字节类型

  • ​ read():读一个字节类型

  • ​ readDouble(): 读一个double类型

  • ​ readFloat():读一个float类型

  • ​ readChar():读一个字符类型

  • ​ readBoolean():读一个boolean类型

  • ​ readLong() :读一个long类型

 public static void main(String[] args) throws IOException {
        //创建数据写出流
        DataOutputStream dos = new DataOutputStream(
                new FileOutputStream("d:/data.txt"));
        //写一个int类型 依次写出4个字节
        dos.writeInt(100);
        dos.writeBoolean(true);
        //关闭
        dos.close();
     
     //读取文件 创建数据读入流  ,需要按写的顺序读进来
        DataInputStream dis = new DataInputStream(
                new FileInputStream("d:/data.txt"));
        //读一个int类型 (依次读4个字节)
        int num = dis.readInt();
        System.out.println("读取的数据:"+ num);
        System.out.println("读的数据:"+dis.readBoolean());
        dis.close();


    }

5、转换流

​ 转换流是将字节流转成字符流的桥梁, 也可以在转换时指定编码格式。 InputStreamReader 和 OutputStreamWriter

   public static void main(String[] args) throws IOException {
         // 字节流转成字符流
         InputStream is = new FileInputStream("d://小众网站.txt");
         InputStreamReader isr = new InputStreamReader(is);
         //缓冲流 读取数据
         BufferedReader br = new BufferedReader(isr);
         //读一行
        String str =null;
        while( (str= br.readLine()) !=null){
            System.out.println(str);
        }
        //关闭流
        br.close();
        isr.close();
        is.close();

    }
 public static void main(String[] args) throws IOException {
        // 创建 字节转成字符的 写出流  FileOutputStream os  =
        FileOutputStream fos = new FileOutputStream("d://data.txt");
        //指定编码  GBK 格式一个汉字占2个字节   UTF-8 格式一个汉字占3个字节
        OutputStreamWriter osw = new OutputStreamWriter(fos,"UTF-8");
        //缓冲形式的
        BufferedWriter bw = new BufferedWriter(osw);

        bw.write("你好");
        bw.newLine();
        bw.write("我不好");

        bw.close();


    }

6、随机字节流

RandomAccessFile 是随机字节流,它是一个可读可写的流 ,在文件操作时指定该对象的模式(model)后,可以读数据或写数据

实现 DataInputStream和DataOutputStream类

构造器:

RandomAccessFile rm = new RandomAccessFile(File ,mode);

​ RandomAccessFile rm = new RandomAccessFile(String ,mode);

mode表示对象的模式

r: 表示该对象只能读 不能写

rw/rws/rwd :表示该 对象是可读可写的;

public static void main(String[] args) throws IOException {
        //创建可读 的流
        RandomAccessFile reader = new RandomAccessFile("d://data.txt","r");
        //创建可读可写的 的流
        RandomAccessFile writer = new RandomAccessFile("d://data-1.txt","rw");
        // 读和写的过程和之前一样
        byte [] b= new byte[10];
        int len=0;
        while( (len = reader.read(b)) !=-1){
            writer.write(b , 0 , len);
        }
        System.out.println("复制成功");
        //关闭流
        writer.close();
        reader.close();

    }

skipByte 和 seek的区别

      // 跳字节读取
        RandomAccessFile raf = new RandomAccessFile("d:/data.txt","rw");
        // 跳过2个字节
        raf.skipBytes(2);
        System.out.println((char)raf.readByte()); //3
        System.out.println("当前偏移量:"+raf.getFilePointer());//3
        // 又丢弃1个字节   从当前位置 往后偏移1位
        raf.skipBytes(1);
        System.out.println("修改后的偏移量"+raf.getFilePointer());//4
        System.out.println("偏移后的读取数据:"+(char)raf.readByte()); //5
        raf.close();


        // seek用法
        RandomAccessFile raf2 = new RandomAccessFile("d:/data.txt","rw");
        //  设置当前读取的位置  ,从0开始计算  ,指定n ,就从n的下一个字节 读取
        raf2.seek(2);
        System.out.println("seek后的数据:"+(char)raf2.readByte());//3
        raf2.seek(1); // 又从0开始 设置偏移量为1  
 		System.out.println("修改后的偏移量"+raf.getFilePointer());//1
        System.out.println("seek后的数据:"+(char)raf2.readByte())//2
        raf2.close();

7、对象序列化流

​ 对象流也称为序列化流,用于存储对象和读取对象的字节流,也是属于包装流

序列化和反序列化

​ 将内存中的对象(Object,集合类等)保存到磁盘、网络介质、其他设置的过程,并在合适的时间能获取磁盘文件/网络的数据 ,这个过程就是对象的序列化和反序列化。

在这里插入图片描述

为什么需要序列化和反序列化呢?

​ 在之前文件中存储的文本信息,这样不便于对数据的分类和操作,如果可以做到直接对对象的读和写这样可大大提高编程效率,并最大程度保证对象的完整性。

Java-IO中实现对象序列化的两种方式:

  1. 实现Serializable接口

  2. 实现Externalizable接口

    Serializable接口

    对象需要实现该接口,但是它没有任何需要实现的方法,只有一个用于标记该类可序列化的唯一标识。 任何类需要序列化都必须标记该变量

    public class User implements Serializable {
        // 对于能区分其他类的唯一表示
        private static final long serialVersionUID = 1L;
    
         private int uid;
         private String name;
         private String password;
         //  有一部分属性不能序列化
    
        public int getUid() {
            return uid;
        }
    
        public void setUid(int uid) {
            this.uid = uid;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "uid=" + uid +
                    ", name='" + name + '\'' +
                    ", password='" + password + '\'' +
                    '}';
        }
    }
    
       //创建序列化的对象流  从内存到文件
            ObjectOutputStream oos = new ObjectOutputStream(
                    new FileOutputStream("d:/user.txt"));
            User user= new User();
            user.setUid(1001);
            user.setName("admin");
            user.setPassword("123456");
            //序列化对象
            oos.writeObject(user);
            //关闭流
            oos.close();
    
            // 反序列化:  将文件中的数据 再读入到内存中 ,需要一个读的流 ObjectInputStream
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d://user.txt"));
            //  反序列化尽量只读一次 (也可以读多次, 如何写出就如何读入)
            Object obj = ois.readObject();
            if(obj instanceof  User){
                User u = (User)obj;
                System.out.println("反序列化的结果:"+u);
            }
            //关闭流
            ois.close();
    
    

​ 问题: 能否自定义序列化的属性 ,这里可以采用方式二,实现Externalizable,并重写两个方法 接口继承而来,在其基础上新增了两个未实现方法:readExternal(ObjectInputStream)和 writeExternal(ObjectOutputStreawm) ,自定义需要序列化的属性

public interface Externalizable extends java.io.Serializable

Externalizable接口

public class Student implements Externalizable {
    private  int id;
    private String name;
    private String sex;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    // 自定义可序列化的属性
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(this.id);
        out.writeUTF(this.name);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.id = in.readInt();
        this.name = in.readUTF();
    }

    public Student(int id, String name, String sex) {
        this.id = id;
        this.name = name;
        this.sex = sex;
    }

    public Student( ) {

    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }
}

 public static void main(String[] args) throws IOException, ClassNotFoundException {
         // 创建序列化类
        ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("d:/stu.txt"));
        //创建学生
        List<Student> list = new ArrayList<>();
        list.add(new Student(1001,"张飞","男"));
        list.add(new Student(1002,"刘备","男"));
        list.add(new Student(1003,"小乔","女"));
        // 将集合序列化
        oos.writeObject(list);
        //关闭
        oos.close();

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("d:/stu.txt"));
        //读
        Object obj = ois.readObject();
        if(obj instanceof  List){
            List<Student> list2 = (List<Student>)obj;
            for(Student s : list2){
                System.out.println(s);
            }
        }
        //关闭流
        ois.close();



    }

​ 问题: 哪些属性不能实现序列化

​ 1、类中的static修饰的属性不能序列化

​ 2、类中属性被transient修饰的不能序列化 例如 transient private Integer age = null;

​ 3、实现Externalizable接口的类的属性不能全部序列化,必须手动写可序列化的属性。

一、文件的压缩流和解压流

1、为什么需要使用压缩文件

文件压缩使用场景: 在文件上传或下载中需要操作多个文件时,如果一个一个复制需要花较长时间,而且比较繁琐,javaAPI提供一种压缩/解压文件方式,可以将多个文件打包成一个文件(.zip)

包: java.util.zip

常用类: ZipEntry: 表示压缩文件中的每一个实体文件

​ ZipFile: 表示压缩文件对象

​ ZipOutputStream: 表示压缩文件输出流,用于将普通文件写出到zip文件中

​ ZipInputStream: 表示解压文件的输入流,用于读zip文件中的每一个实体ZipEntry

2、压缩文件步骤

在这里插入图片描述

​ a、创建需要压缩文件的输入流(InputStream )

​ b、创建压缩包所在的路径,并指定压缩文件名,同时创建ZipOutputStream输出流

​ c、将文件对象 添加到 ZipOutputStream中的实体中(也可以指定压缩后的实体名称)

​ d、文件复制

​ e、关闭流

 public static void main(String[] args) throws IOException {
         // 1、创建文件对象
        File file = new File("d:/小众网站.txt");
        // 2、创建文件的输入流
        FileInputStream fis  = new FileInputStream(file);
        // 3、创建文件压缩流(输出流)
        ZipOutputStream zos = new ZipOutputStream(
                new FileOutputStream("d:/myfile.zip"));

        // 给压缩包中添加文件,并可自定义文件名
        zos.putNextEntry(new ZipEntry("小众网站.txt "));
        // 给压缩包设置注释
        zos.setComment("这是压缩包的注释。。。。");
        // 文件复制
        int len = 0;
        byte [] b = new byte[1024];
        while( (len = fis.read(b)) !=-1){
            zos.write(b,0,len);
        }
        System.out.println("文件压缩成功");

        zos.close();
        fis.close();

    }

压缩多个文件

 /**
     * 压缩一个文件夹  myfile
     * @param args
     */
    public static void main(String[] args) throws IOException {

        //构建压缩包的输出流
        ZipOutputStream zos = new ZipOutputStream(
                        new FileOutputStream("d:/myfile.zip"));
        File file=new File("d:/myfile");
        File [] files =  file.listFiles();

        for(File f : files){
             //构造每一个文件的输入流
            FileInputStream fis  = new FileInputStream(f);
            putZipFile(fis, zos ,f.getName());
            System.out.println(f.getName()+"文件压缩成功" );
        }

        //关闭压缩流
        zos.flush();
        zos.close();


    }

    /**
     * 将文件放入压缩流中
     * @param fis
     * @param zos
     * @param entryName
     * @throws IOException
     */
    public static void putZipFile(InputStream fis ,
                                  ZipOutputStream zos,
                                  String entryName) throws IOException {
        // 给压缩包中添加文件,并可自定义文件名
        zos.putNextEntry(new ZipEntry(entryName));
        // 给压缩包设置注释
        zos.setComment("这是压缩包的注释。。。。");
        // 文件复制
        int len = 0;
        byte [] b = new byte[1024];
        while( (len = fis.read(b)) !=-1){
            zos.write(b,0,len);
        }
        System.out.println("文件压缩成功");


        fis.close();
    }

3、解压文件步骤

​ 解压文件是将一个.zip文件的内容,复制到文件下,需要使用ZipInputStream

​ 解压文件的关键点: 获取解压文件的每一个条目ZipEntry的输入流 ,将输入流写出到指定位置。

​ 如何获取输入流: ZipFile对象 表示一个zip文件

在这里插入图片描述

步骤:

​ a、根据文件路径 创建ZipInputStream

​ b、根据文件路径创建ZipFile对象

​ c、循环遍历每一天条目, 得到它的ZipEntry

​ d、获取ZipEntry的输入流

​ e、将文件复制到指定位置

   public static void main(String[] args) throws IOException {
          //1、创建ZipInputStream
        ZipInputStream zis = new ZipInputStream(
                new FileInputStream("d:/myfile.zip"));
        // 2、创建ZipFile对象
        ZipFile zipFile = new ZipFile("d:/myfile.zip");
         // 3、获取zip中的实体
        ZipEntry en = null;
        while(  (en= zis.getNextEntry())!=null){
            System.out.println(en.getName()+"--"+en.isDirectory());
            //4、获取每一个en的输入流 (关键)
            InputStream is =   zipFile.getInputStream(en);
            copyFile(is ,"d:/my",en.getName());
        }
    }

    /**
     * 通过输入流 复制文件到指定的目录下
     * @param is  输入流
     * @param path  存放文件的路径
     * @param fileName  文件名
     */
    public static void copyFile(InputStream is , String path , String fileName) throws IOException {
          File file = new File(path);
          //判断目录是否存在, 不存在就 创建
          if(!file.exists()){
              file.mkdirs();
          }

        FileOutputStream fos = new FileOutputStream(path+File.separator+fileName);
        int len = 0 ;
          byte [] b = new byte[1024];
          while( (len = is.read(b)) !=-1){
                fos.write(b,0,len);
          }
          System.out.println("解压成功");
          fos.close();
          is.close();
    }

二、Java的多线程

1、线程的基本概念

​ 1.1 定义

​ 引入线程: 打开计算中的任务管理器,有很多条目,每一条目对应一个应用程序,这个应用程序我们称之为 “进程” ,每一个进程都占用CPU资源和内存, 在这一个进程中 包含多个任务,他们可以“同时”运行, 这里的每一个任务称为”线程“

​ 如果将Java的 应用程序比作一个进程,那么它包含的多个执行流程就是一个 线程。

​ 生活中的多线程: 你现在正在玩游戏 ,你可以一边聊天(互喷),你也可以操控游戏,还可以问候队友。玩游戏就是一个进程,你的不同的操作对于游戏本身就是一个单线程,如果你可以同时操作,就是游戏可支持 多线程。

​ 进程:进程是计算机中独立的应用程序,进程是动态,可运行的

​ 线程:在进程中运行的单一任务,是进程的子程序

​ 程序: 程序是数据描述和操作代码的集合,它是完成某个功能的代码,它是静态的

在这里插入图片描述

多线程: 一个进程中的多个子任务, 在多线程中会出现资源抢占问题, 在单核CPU下同一时间点某个进程下只能有一个线程运行。线程与线程之间会互抢资源

CPU资源分配

在这里插入图片描述

电脑可以运行多个应用程序(多进程),在同一时间片,CPU只能执行某一个任务,由于时间片切换非常快,你根本不能察觉会出现“等待”的情况,如果电脑出现 “卡死” 你可以任务资源没有获取并正在等待中。

单线程运行流程:程序只有一条运行线路,从开始到结束保持一致

在这里插入图片描述

多线程:可以有多条结束任务,对于那一条先结束无法预知

在这里插入图片描述

如何创建多线程的程序呢?

方式一: 继承Thread类

a、 定义一个类继承Thread类,重写run方法

b、创建该类的对象, 并调用start方法

public class MyThread extends  Thread {
    @Override
    public void run() {
         for(int  i=0;i<100;i++){
             //获取当前线程名
             System.out.println(this.getName()+"----"+i);
         }
    }
}

   public static void main(String[] args) {
         // 创建线程对象
         MyThread my = new MyThread();
         //开启线程
         my.start();

          for(int i = 0 ;i < 100 ;i ++){
            System.out.println("主线程的 i-----"+i);
        }
        // 结论: 对于多线程之间它们的执行过程会存在资源抢占,谁先获得cpu资源,谁就执行
    }

方式二:实现Runnable接口

a、创建一个类实现一个接口

public class MyThread2 implements Runnable {
    @Override
    public void run() {
        for(int i = 0;i<100;i++){
            //获取当前 线程的线程名
            System.out.println(Thread.currentThread().getName()+"----"+i);
        }
    }
}

b、借助Thread类开启线程

    public static void main(String[] args) {

         // 由于 MyThread2 与线程无关联,需要借助线程类完成启动
        // 创建线程需要执行的任务类
         MyThread2 my = new MyThread2();

        Thread th = new Thread(my,"线程A");
        th.start();

        //再启动一个
         Thread th2 = new Thread(my,"线程B");
         th2.start();

问题:以上两种创建线程的区别?

1、继承方式适用于没有直接父类 ,相对简单 ,是单一继承, 而接口的方式目标类既可以继承类还可以实现其他接口

2、Runnable实现方式适用于 资源共享,线程同步情况。

3、Runnable实现方式并不是线程类,而是实现线程的目标类(Target)

补充: 创建线程并非只有以上两种方式,还可以通过匿名内部的方式创建线程和 线程池的方式。

2、线程的生命周期

生命周期定义

线程从创建到销毁的整个过程,称为线程生命周期, 好比人的生命周期就是从出生到去世的整个过程中间会经历的过程包括 出生,长大,变老,离开 都是一个人要经历的。

生命周期的阶段

1、新生状态 : 程序创建该线程(实例化对象)

2、就绪状态(可运行状态) : 当线程对象调用start()方法后 ,可以抢占cpu资源,但不会立马运行run方法

3、运行状态: 当抢占到资源后,立马运行run方法

4、阻塞状态: 在运行过程中,线程遇到阻塞事件(线程休眠,wait ,IO操作,join操作等),变为阻塞状态

5、死亡状态: 线程运行完毕,或异常中断 ,此时CPU资源被释放

在这里插入图片描述

3、线程的常用方法和API

java.lang.Thread 类中提供了大量的相关的方法:

new Thread();

new Thread(name);

new Thread(Runnable,name);

new Thread(Runnable)

常用方法:

getId() :获取线程的唯一标识

getName() :获取线程名

getPriority():获取线程的优先级: 优先级从1-10 , min-priority:1 max-priority:10 norm- priority:5 注意说明优先级高的获取到cpu资源的概率越大,并不是一定会优先执行完成。

currentThread():获取当前线程的对象引用

getState():获取线程的状态 (这是返回线程状态的枚举, NEW:未启动,RUNABLE:可运行 BLOCK:阻塞状态, WAITING:等待状态TIMED-WAITIN: 等待另一个线程执行完)

interrupt():中断这个线程

isInterrupted(): 返回boolean 测试当前线程是否中断

isAlive():该线程是否处于活动状态

isDaemon():判断该线程是否是守护线程

setDaemon():设置该线程是否是守护线程

join() :合并线程,使它变为单线程

sleep(ms) :让当前线程休眠 ,休眠时间到了,自动唤醒

yield(): 让出cpu资源,使当前线程处理可运行状态(可运行状态也随时可以获取cpu资源)

案例1: 测试线程的基本属性

  System.out.println("当前主线程:"+Thread.currentThread().getName());
        System.out.println("主线程id:"+Thread.currentThread().getId());
        //设置主线程的线程级别
        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
        System.out.println("主线程的线程级别:"+Thread.currentThread().getPriority());

          //  创建线程对象
         MyThread my = new MyThread();
         //设置线程名
        my.setName("线程A");
        //设置优先级
        my.setPriority(10);
         my.start();

        MyThread my1 = new MyThread();
        my1.setName("线程B");
        //设置优先级     线程A  获取到资源的概率 大于线程B (大概率线程A优先执行完)
        my1.setPriority(1);
        //新生态
        System.out.println("线程"+my1.getName()+"状态-----"+my1.getState());
        my1.start();
        //可运行状态(就绪)
        System.out.println("线程"+my1.getName()+"状态-----"+my1.getState());


         for(int i = 0;i<100;i++){
             System.out.println("主线程------"+i);
         }

守护线程

案例2: 守护线程

线程类型分为两种 一种是用户线程一种是守护线程,用户线程是执行某一个任务的独立代码 ,守护线程是用于守护用户线程的线程, 它的特点是 当用户线程执行完毕后守护现在自动结束,当用户线程没有执行完, 守护线程也不会停止

​ 操作系统中有守护进程 ,用于操作系统的运行,只有关机进程自动结束,这里守护线程和守护进程类似。

       //创建线程对象
        DaemonThread daemonThread = new DaemonThread();
        //设置该线程为守护线程   守护的是与它并行的线程类 ,当主线程或其他线程执行完毕,守护线程自动结束
       // daemonThread.setDaemon(true);
        System.out.println("是否是守护线程:"+daemonThread.isDaemon());
        daemonThread.start();

        for(int i=0;i<100;i++){
            System.out.println("主线程i------"+i);
        }

活动的线程总数: Thread.activeCount()

线程中断

案例3: 关于终止线程

线程中止就是当线程运行时由于满足特定的条件需要停止运行,此时我们需要考虑如何安全的中止线程这里中止线程提供几个方法

方法1 : 打标记中断法

线程运行1000,当程序达到500时,中止程序

public class ThreadEnd extends  Thread {
    @Override
    public void run() {
        boolean  isOver=false;
        for(int i = 0 ;i<1000;i++){
            if(i>=500){
                isOver= true;
                return ;
            }
            System.out.println("线程结果i-----------"+i);
        }
        System.out.println("正常结束");
    }

    public static void main(String[] args) {
          ThreadEnd th = new ThreadEnd();
          th.start();
    }
}

方法2: 异常中断法

  • ​ interrupt() :给线程打一个中断标记,不会立马中断

  • ​ interrupted() : 检测线程是否中断,并清除中断标记,返回boolean ,如果线程打标记了,就返回true

  • ​ isInterrupted() : 检测线程是否中断,但不清除中断标记, 返回boolean

注意用法: interrupted() : 它所处于的位置,对应于它作用的位置 ,通过线程类名调用

​ interrupt() 和 isInterrupted() : 使用线程对象调用。

public class Thread1 extends  Thread {
    @Override
    public void run() {
        int i =0;
          while(true){
              System.out.println("线程--------------"+i);
               //判断当前线程是否有中断标记  ,但是不清除中断标记
              if(this.isInterrupted()){
                  // 通过抛出异常或 break
                  System.out.println("当前线程打中断标记,可以停止了");
                  break;
              }
              i++;
          }
    }
}
 public static void main(String[] args) throws InterruptedException {
         Thread1 th = new Thread1();
         th.start();
         // 休眠一会儿
        Thread.sleep(2000);
        //给th打中断标记
        System.out.println("打标记");
        th.interrupt(); //给th打标记

    }

3个方法的用法

     //   Thread.currentThread().interrupt();
     //   System.out.println("判断当前线程是否打标记 (清除标记):"+ Thread.interrupted());
        System.out.println("判断线程是否打标记(不清除标记)"+ Thread.currentThread().isInterrupted());
        System.out.println("判断当前线程是否打标记 (清除标记):"+ Thread.interrupted());  // 静态方法
  

join用法

案例四: join的用法: 合并当前线程 ,使其变为单线程 ,哪个线程调用join方法,就立即将该线程剩下的部分执行完成,再执行其他线程

public class ThreadJoin extends  Thread {
    @Override
    public void run() {
            ThreadJoin2 th = new ThreadJoin2();
            th.setName("线程C");
            th.start();

           for(int i=0;i<100;i++){
               try {
                   th.join();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println(Thread.currentThread().getName()+"-----"+i);
           }
    }
}
    public static void main(String[] args) throws InterruptedException {
         ThreadJoin threadJoin = new ThreadJoin();
        threadJoin.setName("线程A");
         threadJoin.start();


//        ThreadJoin threadJoin2 = new ThreadJoin();
//        threadJoin2.setName("线程B");
//        threadJoin2.start();


         for(int i=0;i<100 ;i++){
               if(i==50){
                   // 合并线程 (threadJoin线程的所有代码合并到 主线程中,先执行threadJoin线程)
                   threadJoin.join();
               }
//               if(i==70){
//                   threadJoin2.join();
//               }
             System.out.println("main---"+i);
         }
    }

sleep用法

案例五: sleep的用法: 用于休闲当前线程 ,休眠时间结束后自动唤醒继续执行,如果同时有多个线程执行 ,如果线程没有同步的情况下,相互休眠不影响,资源被公用。

  public static void main(String[] args) {

for(int i =0;i<10;i++){
            try {
                //让当前线程休眠200毫秒  200毫秒后自动唤醒线程 继续执行
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
        }
        }
class ThreadSleep implements  Runnable{

    @Override
    public void run() {
          for(int i =0;i<100;i++){
              try {
                  Thread.sleep(1000); // 当前线程休眠时 不影响其他线程执行
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName()+"----"+i);
          }
    }
}
        ThreadSleep obj = new ThreadSleep();
        Thread th1 = new Thread(obj , "线程A");
        Thread th2 = new Thread(obj , "线程B");
        th1.start();
        th2.start();

yield用法

案例六:yield的用法 : 出让cpu, 让当先线程变为可运行状态 ,并也可以继续抢占cpu资源

public static void main(String[] args) {
        ThreadYiled th = new ThreadYiled();
        th.start();
          // yield  让出cpu资源
        for(int i = 0;i<100;i++){
                if(i==50){
                    //主线程让cpu
                    System.out.println("让出cpu");
                    Thread.currentThread().yield();

                }
            System.out.println("主线程----"+i);
        }
    }

4、线程同步

线程并发场景:

在实际开发中,很多时候会出现多个线程同时访问同一个内存空间(变量)的场景,当他们同时对数据进行更新时,可能会出现数据不安全问题 ,例如经典的银行取钱案例

​ 假设有一个账户(Account) ,余额是1100元,有小明和小明的爸爸同时取钱,小明拿着银行卡取ATM机取钱,小明的爸爸拿着存折取柜台取钱 ,他们都需要取1000块,小明在取钱时 系统会判断是否余额大于1000,如果大于,可以取钱,由于取钱需要一个过程,此时正好小明的爸爸也对该账户取1000,由于小明没有完成取钱的操作,卡里的钱还没有及时更新提交,所以小明的爸爸也可以通过系统判断的验证, 余额也大于1000,小明的爸爸也可以取钱,所以现在可能会出现他们两个人都取出1000元,导致账户数据不完整,这就是线程编发导致的问题

在这里插入图片描述

使用代码模拟场景

1、先有一个账户 (卡号,余额)

2、取钱的任务 ,由于需要使用同一个账户 ,这里的任务中有一个相同的账户 。

同步的解决办法:

1、将需要操作公共资源的代码增加 “同步锁” (也叫互斥锁)

语法:

synchronized(对象锁){
    代码块
}

注意这里的对象锁必须满足 两个线程是同一个对象(同一把锁)

   public void run() {
        System.out.println("开始取钱了");
        // 增加互斥锁,协同步伐  ,这个锁必须是公有的对象
        synchronized(account) {
            //先判断账户余额是否足够
            if (account.getMoney() >= 1000) {
                System.out.println(Thread.currentThread().getName() + "可以取钱");
                System.out.println(Thread.currentThread().getName() + "正在取钱");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //更新账户余额
                account.setMoney(account.getMoney() - 1000);
                System.out.println(Thread.currentThread().getName() +
                        "取到了钱,卡里余额还剩:" + account.getMoney());
            } else {
                System.out.println("抱歉,卡里余额不足,不能取1000元");
            }
        }

        // 以上代码需要将操作通过资源的代码块 增加同步关键字
    }

2、 同步方法

在方法的返回值前面增加 “synchronize” , 此时的锁代表的是当前this对象

 public synchronized void  get(){
        //先判断账户余额是否足够
        if (account.getMoney() >= 1000) {
            System.out.println(Thread.currentThread().getName() + "可以取钱");
            System.out.println(Thread.currentThread().getName() + "正在取钱");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //更新账户余额
            account.setMoney(account.getMoney() - 1000);
            System.out.println(Thread.currentThread().getName() +
                    "取到了钱,卡里余额还剩:" + account.getMoney());
        } else {
            System.out.println("抱歉,卡里余额不足,不能取1000元");
        }
    }

同步代码块和同步方法的区别:

1、语法不同,同步代码块更灵活,可以自定义锁对象 ,而同步方法不可以指定锁对象

5、线程通信

5.1、线程死锁

线程死锁的产生

​ 线程同步可以帮助我们解决多个线程操作同一个资源而导致数据不安全的问题,但线程同步也有可能产生隐患,假如一个线程中出现多个锁对象时,可能出现锁使用不当,导致锁与锁之前相互等待对方释放资源,从而形成一种 “相互等待”的僵局,这就是线程死锁。 例如哲学家吃饭

​ 模拟线程死锁

public class DeadThread implements  Runnable {
    Object obj1 = new Object();
    Object obj2 = new Object();
    @Override
    public void run() {
        // 模拟线程死锁
        // 如果当前线程为线程A  先拿到obj1锁   ,等待obj2锁资源
        // 如果当前线程为线程B  先拿到obj2锁  ,等待obj1锁的资源
        if(Thread.currentThread().getName().equals("线程A")){
             synchronized (obj1){
                 System.out.println(Thread.currentThread().getName()+"拿到了obj1的锁");
                 try {
                     Thread.sleep(500);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println("我在等待obj2锁。。。。。");
                 synchronized (obj2){
                     System.out.println("我已经拿到了obj2锁。。。。");
                 }
             }
        }else{
            //线程B
            synchronized (obj2){
                System.out.println(Thread.currentThread().getName()+"拿到了obj2的锁");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("我在等待obj1锁。。。。");
                synchronized (obj1){
                    System.out.println("我已经拿到了obj1锁,我和开心");
                }
            }
        }

    }
}

    public static void main(String[] args) {
        DeadThread dead = new DeadThread();
        Thread th1 = new Thread(dead,"线程A");
        Thread th2 = new Thread(dead,"线程B");
        th1.start();
        th2.start();
    }

之所以会发生死锁,因为对象锁直接没有良好的“沟通”,导致互相获取对方的锁 ,进入等待中 ,可以通过线程类的几个方法 解决线程之间通信问题

5.2、线程通信的几个方法

​ wait() : 让当前线程处于等待中,会释放对象锁 ,但是不会自动唤醒,需要其他线程唤醒

​ notify() : 唤醒等待中的一个线程

​ notifyAll: 唤醒所有等待中的线程

他们都属性Object的方法,需要相同的对象 ,使用时 通过Object的对象调用

注意: 以上方法的调用必须满足两个条件: a、他们必须在同步代码块中执行, b、调用该方法的对象是锁对象

案例1:模拟3个人,张飞、李逵和刘备,来买电影票,售票员只有一张5元的钱,电影票5元钱一张。

  • 张飞拿20元一张的人民币排在李逵和刘备的前面,李逵和刘备各拿了一张5元的人民币买票。

    package com.j2008.waitnotify;
    
    /**
     * ClassName: TicketThread
     * Description:
     * date: 2020/11/10 11:27
     *
     * @author wuyafeng
     * @version 1.0   softeem.com
     */
    public class TicketThread implements  Runnable{
        //公共资源:
        int fiveCount=1;
        int twentyCount =0;
    
    
        @Override
        public void run() {
            //开始买票  如果是张飞,他是20元面值,其他都是5元
            if(Thread.currentThread().getName().equals("张飞")){
                //20元面值买票
                takeTicket(20);
            }else{
                // 5元面值买票
                takeTicket(5);
            }
    
        }
    
        /**
         * 买票过程   给方法加同步  ,锁对象默认是 方法
         * @param money
         */
        public synchronized void  takeTicket(int money){
            if(money ==20){
                // 验证 当前公共资源 中是否有3张5元
                 while(fiveCount<3){
                     //等待
                     System.out.println(Thread.currentThread().getName()+"不能买到票,要继续等待");
                     try {
                         this.wait();
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
                 // 程序执行这里,说明 fiveCount >=3
                System.out.println(Thread.currentThread().getName()+"正在买票。。");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                fiveCount-=3;
                twentyCount++;
                System.out.println(Thread.currentThread().getName()+"已经买到了票。。");
    
            }else{
                System.out.println(Thread.currentThread().getName()+"正在买票。。");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                fiveCount++;
                System.out.println(Thread.currentThread().getName()+"已经买到了票。。");
                // 唤醒等待的线程
                this.notify();
            }
        }
    
    }
    
    
      public static void main(String[] args) {
             TicketThread ticketThread = new TicketThread();
             Thread th1 = new Thread(ticketThread,"张飞");
            Thread th2 = new Thread(ticketThread,"李逵");
            Thread th3 = new Thread(ticketThread,"刘备");
            //开启线程
            th1.start();
            th2.start();
            th3.start();
        }
    

5.3、线程的生产者和消费者模式

 多个线程同时运行时,会产生线程并发可使用同步操作确保数据的安全性,如果需要各线程之间交互,可是使用线程等待和唤醒模式,在这里常用的等待唤醒中经典的模式为“生产者和消费者模式” 

 生产者和消费者由两类线程组成:  若干个生产者线程 负责提交用户的请求,若干个消费者线程负责处理生成出来的任务。 他们操作一块共享内存区进行数据通信。

在这里插入图片描述

生成/消费的产品(数据): Mobile (手机编号)

生成者线程类: Provider : 无限制的生成手机

消费者线程类:Customer : 无限制的消费手机

​ 共享存储区: Storage ( push 、pop) 存储手机的对象数组

​ 测试类

public class Mobile {
    private int id;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Mobile(int id) {
        this.id = id;
    }
}

存储区

package com.j2008.provider_customer;

/**
 * ClassName: Storage
 * Description:
 * date: 2020/11/10 15:32
 *  存储区,它是生产者和消费者共享的空间 ( 生产者和消费者将该对象作为公有锁)
 * @author wuyafeng
 * @version 1.0   softeem.com
 */
public class Storage {

    // 定义存储手机的对象数据
    Mobile [] mobiles = new Mobile[10];
    int index=0;  // 个数

    static int n=1000;
    /**
     * 存放手机
     * @param mobile
     */
    public synchronized void push(Mobile mobile){
        //考虑容器上限已满,必须等待
        while(index == mobiles.length){
            System.out.println("容器已满,需等待");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //通知消费者取消费 ,将其他线程全部唤醒
        this.notifyAll();
        mobiles[index]=mobile;
        index++;

    }

    /**
     * 取出手机      1 2 3 4
     *              1 2 3
     *              index--;
     *             mobile[index] =null
     * @return 取出的手机对象
     */
    public synchronized Mobile pop(){
        Mobile m = null;

        // 判断index是否小于0
        while(index<=0){
            //等待
            System.out.println("容器中没有手机,需要等待");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        index--;
        m = mobiles[index];
        //将容器中的这个位置 设置为空
        mobiles[index]=null;
        // 通知生产者去生产
        this.notifyAll();
        return m;

    }

    public synchronized int getSize(){
        return index;
    }

}

生产者:

package com.j2008.provider_customer;

/**
 * ClassName: Provider
 * Description:
 * date: 2020/11/10 15:54
 *
 * @author wuyafeng
 * @version 1.0   softeem.com
 */
public class Provider implements  Runnable {
    //共享存储区
    Storage storage =null;
    public Provider(Storage storage){
        this.storage = storage;
    }


    @Override
    public void run() {
        //手机编号
        int n=1000;
        //一直生产
        while(true){
            Mobile m = new Mobile(n);
            storage.push(m);
            System.out.println(Thread.currentThread().getName()+
                    "生产了一部手机,其编号:"+m.getId()+" 其库存:"+storage.getSize());

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            n++;
        }
    }
}

消费者

package com.j2008.provider_customer;

/**
 * ClassName: Customer
 * Description:
 * date: 2020/11/10 15:58
 *
 * @author wuyafeng
 * @version 1.0   softeem.com
 */
public class Customer implements  Runnable {

    Storage storage=null;
    public Customer(Storage storage){
        this.storage = storage;
    }

    @Override
    public void run() {
        while(true){
            Mobile mobile = storage.pop();
            System.out.println(Thread.currentThread().getName()+
                    "消费了一部手机,编号》》"+mobile.getId()+" 库存:"+storage.getSize());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

测试类

package com.j2008.provider_customer;

/**
 * ClassName: TestProviderCustomer
 * Description:
 * date: 2020/11/10 16:01
 *
 * @author wuyafeng
 * @version 1.0   softeem.com
 */
public class TestProviderCustomer {
    public static void main(String[] args) {
         //创建公有的存储空间
        Storage storage = new Storage();
        Provider provider1 = new Provider(storage);
        Provider provider2 = new Provider(storage);
        Provider provider3 = new Provider(storage);

        Thread th1 = new Thread(provider1,"张飞");
        Thread th2 = new Thread(provider2,"刘备");
        Thread th3 = new Thread(provider3,"关羽");

        th1.start();
        th2.start();
        th3.start();

        //消费者上场
        Customer customer1 = new Customer(storage);
        Customer customer2 = new Customer(storage);
        Customer customer3 = new Customer(storage);

        Thread th4 = new Thread(customer1,"张飞的老婆");
        Thread th5 = new Thread(customer2,"刘备的老婆");
        Thread th6 = new Thread(customer3,"关羽的老婆");

        th4.start();
        th5.start();
        th6.start();


    }
}

测试结果

关羽生产了一部手机,其编号:1000 其库存:1
张飞的老婆消费了一部手机,编号》》1000 库存:0
张飞生产了一部手机,其编号:1000 其库存:1
刘备生产了一部手机,其编号:1000 其库存:2
刘备的老婆消费了一部手机,编号》》1000 库存:1
关羽的老婆消费了一部手机,编号》》1000 库存:0
容器中没有手机,需要等待
关羽生产了一部手机,其编号:1001 其库存:0
张飞的老婆消费了一部手机,编号》》1001 库存:0
张飞生产了一部手机,其编号:1001 其库存:2
刘备生产了一部手机,其编号:1001 其库存:2
关羽的老婆消费了一部手机,编号》》1001 库存:0
刘备的老婆消费了一部手机,编号》》1001 库存:0
容器中没有手机,需要等待
关羽生产了一部手机,其编号:1002 其库存:0
张飞的老婆消费了一部手机,编号》》1002 库存:0
刘备生产了一部手机,其编号:1002 其库存:2
张飞生产了一部手机,其编号:1002 其库存:2
刘备的老婆消费了一部手机,编号》》1002 库存:1
关羽的老婆消费了一部手机,编号》》1002 库存:0

6、线程池

1、定义

​ 用于创建和管理线程的容器就是线程池 (Thread Pool) ,在线程池中的线程执行完任务后不会立马进入销毁状态,而是重置到线程池中变为“空闲线程” 。 有利于避免频繁创建线程消耗资源,提供线程复用率,有限管理该线程。

在这里插入图片描述

2、使用线程池的原因:

​ 在多线程环境下,对于不断创建和销毁效率非常消耗系统资源,对于多线程之间的切换存在线程安全问题, 这是使用统一的管理类管理一些线程是比较好的解决办法

3、线程的运行机制:

  • ​ 在线程池模式下,任务是提交给线程池,由线程池根据当前空闲线程进行分配任务,如果没有空闲线程,由管理类创建线程或者进入任务等待队列中。

  • ​ 一个线程同时只能执行一个任务,但多个任务可以同时提交给这个线程池。

线程池的常用类 (ExecutedService)

        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());

参数1: corePoolSize:核心线程数

参数2:maximumPoolSize :最大线程数

参数3: keepAliveTime : 线程活动时长

参数4: 对于参数3的单位

1、可缓存的线程池 newCacheThreadPool(n);如果线程池中没有空闲线程,则创建新线程并放入线程池中,无上限线程数,如果有空闲线程则直接使用该线程

 public static void main(String[] args) {
        // 创建可缓存线程
        ExecutorService service =Executors.newCachedThreadPool();
        // 创建10个线程
        int n=0;
        for(int i = 0 ;i < 10;i++){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            service.execute(new Runnable() {
                @Override  //你们内部类
                public void run() {
                      //任务
                    System.out.println(Thread.currentThread().getName()+"---"+n);
                    try {

                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            });
            //由于以上的线程 每次执行完成只需要500毫秒 ,会释放线程对象到线程池中
            //  1000毫秒后创建的线程 就会复用上一次的线程对象


        }

        //关闭线程池
        service.shutdown();
    }
}

2、可重用的固定线程池: newFixedThreadPool(n) ,线程数量固定,如果没有空闲线程,则存放无界队列中等待

 public static void main(String[] args) {
         //创建线程池
        ExecutorService service = Executors.newFixedThreadPool(3);
        // 连续创建10个线程, 由于只有3个线程,所有线程只能等待
        // 2秒后再执行3个
        for(int i =0;i<10;i++){
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"正在执行");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        // 结果: 每2秒执行3个线程 
    }

3、 固定长度的可执行定时任务的线程池 newScheduledThreadPool , 类似于定时器线程

   scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
                     public void run() {
                         System.out.println("延迟1秒后每3秒执行一次");
                     }
                }, 1, 3, TimeUnit.SECONDS);

4、单线程的线程池 newSingleThreadExecutor : 线程池中只有一个线程数,所有的任务都通过该线程执行,它可以保证所有的任务是FIFO模式, 满足队列结构

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值