JAVA基础相关知识点(二)

文章目录

  • File,IO流
    • file
      • 常用方法
    • IO流
      • 字节流
        • 字节输入流
        • 字节输出流
        • 文件复制(适用于大多数文件复制操作)
        • 资源释放的方式
          • 对上面的文件复制进行修改,确保不会有问题
      • 字符流
        • 字符输入流
        • 字符输出流
      • 缓冲流
        • 字节缓冲输入输出流
        • 字符缓冲输入输出流
      • 转换流
          • 引出问题-不同编码读取时会乱码,如下
          • 字符输入转换流
          • 字符转换输出流
      • 打印流
      • 数据流
        • 数据输出流
        • 数据输入流
      • 序列化流
        • 对象字节输出流
        • 对象字节输入流
      • IO框架
  • 特殊文件-日志
    • properties文件概述
  • 多线程
    • 多线程的创建方式一:继承Thread类
    • 多线程的创建方式二:实现Runnable接口
    • 多线程的第三种创建方式Callable
    • Thread提供的常用方法
    • 线程安全问题
    • 线程同步(解决线程安全问题)
      • 认识线程同步
      • 方式一:同步代码块
      • 方式二:同步方法
      • 方法三:Lock锁
    • 线程池
      • 方式一的使用
        • 线程池处理Runnable任务
        • 线程池处理Callable任务
      • 乐观锁悲观锁概念
  • 网络编程
  • Java高级
    • Junit单元测试

File,IO流

file

  • File是java.iO.包下的类,File类的对象,用于代表当前操作系统的文件(可以是文件、或文件夹
  • 注意:File类只能对文件本身进行操作,不能读写文件里面存储的数据,
  • File对象既可以代表文件、也可以代表文件夹。
  • File封装的对象仅仅是一个路径名,这个路径可以是存在的,也允许是不存在的。

常用方法

  • 判断文件类型获取文件信息
package org.example.FileIO;

import java.io.File;
import java.text.SimpleDateFormat;

public class Test {
    public static void main(String[] args) {
        // 创建对象
        // 根据文件路径创建文件对象
        File file = new File("E:\\studyproject\\text1\\demo02\\src\\aaa.txt");
        System.out.println(file.getName()); // 文件名
        System.out.println(file.length()); // 文件字节大小
        // 也可以指向文件夹
        File file1 = new File("E:\\studyproject\\text1\\demo02\\src");
        System.out.println(file1.length()); // 文件夹的大小
        // 注意:File对象可以指代一个不存在的文件路径
        File file2 = new File("E:\\studyproject\\text1\\demo02\\src\\bbb.txt");
        System.out.println(file2.length()); // 不存在的文件默认0
        System.out.println(file2.exists()); // false 判断文件是否存在
        System.out.println("-----------");
        // 判断当前文件对象对应的文件路径是否存在 存在返回true
        System.out.println(file.exists()); // true

        // 判断当前文件对象指代的是否是文件,是文件返回true
        System.out.println(file.isFile()); // true

        // 判断当前文件对象指代的是否是文件夹,是文件夹返回true
        System.out.println(file.isDirectory()); // false

        // 获取文件的名称(包含后缀)
        System.out.println(file.getName());

        // 获取文件的大小,返回字节个数
        System.out.println(file.length());

        // 获取文件的最后修改时间(毫秒时间戳)
        long time = file.lastModified();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        System.out.println(simpleDateFormat.format(time));

        // 获取创建文件对象时,使用的路径
        System.out.println(file.getPath());

        // 获取绝对路径
        System.out.println(file.getAbsolutePath());
    }
}

  • 创建文件删除文件
package org.example.FileIO;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;

public class Test1 {
    public static void main(String[] args) throws IOException {
        // 创建文件,创建成功返回true(文件内容为空)
        File file = new File("E:\\studyproject\\java\\aaa.txt");
        System.out.println(file.createNewFile()); // true,可以看到文件夹下创建了aaa.txt(存在就创建失败,不存在则创建成功)

        // 创建文件夹,注意只能创建一级文件夹
        File file1 = new File("E:\\studyproject\\java\\bbb\\ccc");
        System.out.println(file1.mkdir()); // fasle 因为创建了两层bbb和ccc文件夹

        // 创建文件夹,注意可以创建多级文件夹
        File file2 = new File("E:\\studyproject\\java\\bbb\\ccc");
        System.out.println(file1.mkdirs()); // true

        // 删除文件或者空文件,注意:不能删除非空文件夹(删除文件不进入回收站)
        File file3 = new File("E:\\studyproject\\java\\aaa.txt");
        System.out.println(file3.delete()); // true
    }
}
  • 遍历文件夹
package org.example.FileIO;

import java.io.File;
import java.io.IOException;

public class Test2 {
    public static void main(String[] args) throws IOException {
        // 获取当前目录下所有的1一级文件名称"到一个字符电数组中去返回。
        File file = new File("E:\\studyproject\\java");
        String[] list = file.list();
        for (String s : list) {
            System.out.println(s); // 目录名
        }

        // (重点)获取当前目录下所有的"一级文件对象"到一个文件对象数组中去返回(重点)
        // 没有权限的话会返回null
        // 主调是文件,或者路径不存在时返回null
        // 当为空文件夹时,返回空数组
        // 当为文件夹时,且里面有隐藏文件时,将里面所有文件和文件夹的路径放在file数组中返回,包含隐藏文件
        // 当主调是一个有内容的文件夹时,且里面有隐藏文件时,将里面所有一级文件和文件夹的路径放在file数组中返回
        File[] files = file.listFiles();
        for (File file1 : files) {
            // file1.delete(); // 删除遍历到的每个文件 // 不建议用
            System.out.println(file1);  // 全部一级文件对象的路径
            System.out.println(file1.getAbsolutePath()); // 获取绝对路径
        }

    }
}

IO流

  • 用于读写数据的(可以读写文件,或网络中的数据…)
  • I指Input,称为输入流:负责把数据读到内存中去
  • O指Output,称为输出流:负责写数据出去

**分类

  • 字节流:适合操作所有类型的文件 比如:音频、视频、图片 文本文件的复制,转移等
  • 字符流: 只适合操作纯文本文件 比如:读写txt、java文件等
分为四大类
   字节输入流(InputStream,FilelnputStream),
   字节输出流(Outputstream,FileOutputSteam)
   字符输入流(Reader,FileReader),
   字符输出流(Writer,FileWriter)
   括号内是(抽象类,实现类)

字节流

字节输入流
字节输出流
package org.example.FileIO;

import java.io.*;

public class Test3 {
    public static void main(String[] args) throws IOException {
        // 字节流
        // 创建管道
        FileInputStream is = new FileInputStream("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji.txt");

        // 文件字节输入流: 每次读取一个字节(下面文件有:ab)
        // System.out.println((char) is.read()); // a
        // System.out.println((char) is.read()); // b
        // System.out.println(is.read()); // -1  读取不到返回-1

        // 文件字节输入流: 每次读取多个字节
        // 1.读取性能差的案例
        // 读取汉字会乱码,无法避免的
        // int b;//用于记录已经读取的字节
        // while ((b=is.read())!=-1){
        //     System.out.println((char) b); // a b
        // }
        // // 流使用之后必须关闭,释放系统资源
        // is.close();


        // 2.文件字节输入流: 一次读取完全部字节
        // 不做演示代码了,直接使用循环改造
        // 性能直接得到提升,依旧读取汉字会乱码,无法避免的
        // byte[] bufer = new byte[3]; // 记住每次读取了多少个字节。
        // int len;
        // while ((len= is.read(bufer))!=-1){
        //     // 注意:读取多少,倒出多少。
        //     String rs = new String(bufer,0,len);
        //     System.out.println(rs);
        // }



        // 3.一次读取多行文件,更优雅写法(中文不会乱码)
        // 准备一个字节数组,大小与文件的大小正好一样大
        // File file = new File("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji.txt");
        // long s = file.length(); // 获取文件的字节
        // byte[] buff = new byte[(int) s]; // 不适合读取超大文件
        // int len1 = is.read(buff);
        // System.out.println(new String(buff));
        // System.out.println(s);
        // System.out.println(len1);

        // 4. 一次读取全部字节(中文不会有乱码,推荐使用)
        byte[] bytes = is.readAllBytes();
        System.out.println(new String(bytes));

        is.close();


        // 文件字节输出流: 写字节出去
        // 创建管道(创建一个电脑内不存在的文件,会自动生成)
        // 覆盖管道,每次写入数据都会覆盖里面原有的内容
        // OutputStream os = new FileOutputStream("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt");
        // 追加管道,每次写入数据都会向里面追加内容(只需要将第二个参数改为true)
        OutputStream os = new FileOutputStream("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt",true);
        os.write(97); // 代表a
        os.write('b'); // 代表b
        os.write('靳'); // 汉字会发生乱码
        os.write("我爱你".getBytes()); // 将字节数组作为参数传入进去,字符串.getBytes会转成byte字节数组
        os.write("从字节数组哪个地方开始读取到哪个地方结束".getBytes(),0,15); // 一个汉字占用三个字节
        // 写完数据后换行(如果是对文件进行追加内容的话,建议加个换行符)
        os.write("\r\n".getBytes());
        os.close(); // 执行完新创建的目标文件应该就有了:abs我爱你从字节数组
    }
}
文件复制(适用于大多数文件复制操作)
package org.example.FileIO;

import java.io.*;

public class Test5 {
    public static void main(String[] args) throws IOException {
        InputStream is = new FileInputStream("E:\\studyproject\\java\\JAVA相关知识点.md"); // 源文件路径
        OutputStream os = new FileOutputStream("E:\\studyproject\\java\\222.md"); // 复制到目标文件的路径
        // 读取源文件数据写入到目标文件
        // 创建一个字节数组,负责转移字节数据
        byte[] bytes = new byte[1024]; // 1kb
        // 读多少写多少
        int len; // 记住每次读取了多少字节
        while ((len=is.read(bytes))!=-1){
            os.write(bytes,0,len); // 向目标文件写入读取到的数据
        }
        // 关闭资源(先创建谁的流就最后再关,最后创建的先关)
        os.close();
        is.close();
        System.out.println("复制完成,此方法适用于大部分文件复制操作");

    }
}

字节流非常适合做一切文件的复制操作

  • 任何文件的底层都是字节,字节流做复制,是一字不漏的转移完全部字节,只要复制后的文件格式一致就没问题!
资源释放的方式
  • 上面代码释放流会有问题,例如读取写出流过程中出异常了,则程序不会执行到下面的释放流代码(之前做的方式都是直接抛出异常了)
  • 使用try-catch-finally和try-catch-resource解决
对上面的文件复制进行修改,确保不会有问题
  • try-catch-finally的方式
package org.example.FileIO;

import java.io.*;

public class Test6 {
    public static void main(String[] args) {
        InputStream is = null;
        OutputStream os = null;
        try {
            is = new FileInputStream("E:\\studyproject\\java\\JAVA相关知识点.md"); // 源文件路径
            os = new FileOutputStream("E:\\studyproject\\java\\222.md"); // 复制到目标文件的路径
            // 读取源文件数据写入到目标文件
            // 创建一个字节数组,负责转移字节数据
            byte[] bytes = new byte[1024]; // 1kb
            // 读多少写多少
            int len; // 记住每次读取了多少字节
            while ((len=is.read(bytes))!=-1){
                os.write(bytes,0,len); // 向目标文件写入读取到的数据
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            // 关闭资源(先创建谁的流就最后再关,最后创建的先关)
            try {
                if(os!=null) os.close();
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
            try {
                if(is!=null) is.close();
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
            System.out.println("复制完成,此方法适用于大部分文件复制操作");
        }

    }
}
  • try-catch-resource的方式
package org.example.FileIO;

import java.io.*;

public class Test7 {
    public static void main(String[] args) {
        try (
                // 资源异常处理的资源类都放到这里
                // 使用资源异常处理不需要手动关闭释放资源,会自动释放,也就是说我们只需要关心代码逻辑即可
                InputStream is = new FileInputStream("E:\\studyproject\\java\\JAVA相关知识点.md"); // 源文件路径
                OutputStream os = new FileOutputStream("E:\\studyproject\\java\\222.md"); // 复制到目标文件的路径
                ){
            // 读取源文件数据写入到目标文件
            // 创建一个字节数组,负责转移字节数据
            byte[] bytes = new byte[1024]; // 1kb
            // 读多少写多少
            int len; // 记住每次读取了多少字节
            while ((len=is.read(bytes))!=-1){
                os.write(bytes,0,len); // 向目标文件写入读取到的数据
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字符流

字符输入流
package org.example.FileIO;

import java.io.*;

public class Test8 {
    public static void main(String[] args) {
        // 字符输入流
        // 创建文件字符流道
        try (
                Reader fr = new FileReader("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt");
                ){
            // 第一种方式
            // int c; // 记住每次读取的字符编号
            // while ((c=fr.read())!=-1){
            //     System.out.print((char) c); // 这里打印的即使有中文也不会乱码,每次读取一个字符,性能较差
            // }

            // 第二种方式
            // 每次读取多个字符,性能较好
            char[] buffer = new char[3];
            int len; // 记录每次读取的字符数
            while ((len=fr.read(buffer))!=-1){
                System.out.print(new String(buffer,0,len));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}
字符输出流
package org.example.FileIO;

import java.io.FileReader;
import java.io.FileWriter;
import java.io.Reader;
import java.io.Writer;

public class Test9 {
    public static void main(String[] args) {
        // 字符输出流
        // 创建文件字符流道
        try (
                // 会覆盖原有数据
                // Writer fr = new FileWriter("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt");
                // 追加内容,而不是覆盖内容
                Writer fr = new FileWriter("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt",true);
                ){
            // 1. 写一个字符出去
            fr.write('a'); // a
            fr.write(97); // a
            // 2. 写一个汉字
            fr.write('靳');
            // 3. 写一个字符串
            fr.write("萧寂173");
            // 4. 写字符串的一部分出去,从n-m
            fr.write("我爱你",0,2); // 我爱
            // 5.写一个字符数组出去
            char[] buffer = {'萧','寂','1','7','3'};
            fr.write(buffer); // 萧寂173
            // 6. 写一个字符数组的一部分出去
            fr.write(buffer,0,2); // 萧寂
            // 7.写一个换行符
            fr.write("\r\n");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

因为这里使用了资源异常处理了,会自动回收流,如果使用try-catch-finally或者其它代码内使用一定要close()关闭流或者调用flush()这个进行刷新流,否则读取或者写出不生效,切记

缓冲流

  • 字节缓冲输入流
  • 字节缓冲输出流
  • 字符缓冲输入流
  • 字符缓冲输出流

对原始流进行包装,以提高原始流读写数据的性能

原理:字节缓冲输入流自带了8KB缓冲池;字节缓冲输出流也自带了8KB缓冲池

字节缓冲输入输出流
  • 字节缓冲输入流

还是对上面的文件复制进行改造

package org.example.FileIO;

import java.io.*;

public class Test10 {
    public static void main(String[] args) {
        try (
                InputStream is = new FileInputStream("E:\\studyproject\\java\\JAVA相关知识点.md"); // 源文件路径
                // 创建字节缓冲输入流包装原始的输入流,参数二是自定义缓冲池大小,默认是有8kb
                InputStream bis = new BufferedInputStream(is,8192*2);
                OutputStream os = new FileOutputStream("E:\\studyproject\\java\\222.md"); // 复制到目标文件的路径
                // 创建字节缓冲输出流包装原始的输出流,参数二是自定义缓冲池大小,默认是有8kb
                OutputStream bos = new BufferedOutputStream(os,8192*2);
        ) {
            // 读取源文件数据写入到目标文件
            // 创建一个字节数组,负责转移字节数据
            byte[] bytes = new byte[1024]; // 1kb
            // 读多少写多少
            int len; // 记住每次读取了多少字节
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len); // 向目标文件写入读取到的数据
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字符缓冲输入输出流
package org.example.FileIO;

import java.io.*;

public class Test11 {
    public static void main(String[] args) {
        // 字符输入流
        // 创建文件字符流道
        try (
                Reader fr = new FileReader("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt");
                // 创建字符缓冲输入流包装原始的输入流,参数二是自定义缓冲池大小,默认是有8kb
                BufferedReader br = new BufferedReader(fr,8192*2);
        ){
            // 按字符读取
            // char[] buffer = new char[3];
            // int len; // 记录每次读取的字符数
            // while ((len=br.read(buffer))!=-1){
            //     System.out.print(new String(buffer,0,len));
            // }

            // 按行读取
            String line; // 记录每次读取的一行数据
            while ((line=br.readLine())!=null){
                System.out.println(line);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}
  • 字符缓冲输出流
package org.example.FileIO;

import java.io.*;

public class Test12 {
    public static void main(String[] args) {
        // 字符输出流
        // 创建文件字符流道
        try (
                Writer fr = new FileWriter("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt",true);
                // 创建字符缓冲输出流包装原始的输入流,参数二是自定义缓冲池大小,默认是有8kb
                BufferedWriter bw = new BufferedWriter(fr,8192*2);
        ){

            bw.write('a');
            bw.write(97);
            // 新增换行符
            bw.newLine();
            bw.write('靳');
            bw.write("萧寂173");

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

转换流

引出问题-不同编码读取时会乱码,如下
package org.example.FileIO;

import java.io.*;

public class Test13 {
    public static void main(String[] args) {
        // 缓冲字符输入流
        // 创建文件字符流道
        try (
                // 代码编码为utf-8 文件编码是GBK 就乱码了
                // 必须代码编码和文件编码一致
                Reader fr = new FileReader("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt");
                // 创建字符输入流包装成缓冲字符输入流
                BufferedReader br = new BufferedReader(fr);
        ){

            String line;
            while ((line=br.readLine())!=null){
                System.out.println(line);
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
字符输入转换流
  • 解决不同编码时,字符流读取文本内容乱码的问题。
  • 解决思路:先获取文件的原始字节流,再将其按真实的字符集编码转成字符输入流,这样字符输入流中的字符就不乱码了
package org.example.FileIO;

import java.io.*;

public class Test14 {
    public static void main(String[] args) {
        // 字符输入转换流
        // 创建文件字符流道
        try (
                // 1. 得到文件原始字节流(GBK字节流形式)
                InputStream fr = new FileInputStream("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt");
                // 2、把原始的字节输入流按照指定的字符渠编码转换成字符输入
                Reader isr = new InputStreamReader(fr, "GBK");
                // 3.把字符输入流包装成缓冲字符输入流
                BufferedReader br = new BufferedReader(isr);
        ) {

            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line); // 不同编码不会乱码了
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
字符转换输出流
  • 作用:可以控制写出去的字符使用什么字符集编码。
  • 解决思路:获取字节输出流,再按照指定的字符集编码将其转换成字符输出流,以后写出去的字符就会用该字符集编码了
package org.example.FileIO;

import java.io.*;

public class Test15 {
    public static void main(String[] args) {
        // 字符输出转换流
        // 创建文件字符流道
        try (
                // 指定写出去的字符编码
                // 1.创建一个文件字节输出流
                OutputStream fr = new FileOutputStream("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji2.txt");
                // 2. 把原始的字节输出流按照指定的字符集编码转换成字符输出转换流
                Writer osw = new OutputStreamWriter(fr, "GBK");
                // 3. 将字符输出流包装成缓冲字符输出流
                BufferedWriter bw = new BufferedWriter(osw);
        ) {

            bw.write("我爱中国");
            bw.write("我是xiaoji");

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

打印流

  • 作用:打印流可以实现更方便、更高效的打印数据出去,能实现打印啥出去就是啥出去。
package org.example.FileIO;

import java.io.*;
import java.nio.charset.Charset;

public class Test16 {
    public static void main(String[] args) {
        // 打印流
        try (
                // 1.创建一个打印流管道(写入数据)
                // PrintStream ps = new PrintStream("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt");
                // 2. 指定字符集编码(写入数据)
                // PrintStream ps = new PrintStream("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt", Charset.forName("GBK"));

                // 3. 使用PrintWriter(和上面的PrintStream用法一致)
                // PrintWriter ps = new PrintWriter("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt", Charset.forName("GBK"))
                // 4. 由于高级流不支持追加,只支持覆盖,因此如果需要追加的话需要把参数转换为低级流再追加(参数二true代表追加数据)
                PrintWriter ps = new PrintWriter(new FileOutputStream("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt",true))
        ) {
            // 注意:不想换行就不要加ln
            // 写入什么就是什么
            ps.println("97");
            ps.println("a");
            ps.println("我爱你");
            ps.println("爱着你");
            ps.println(true);
            ps.println(99.5);

            // 这个是写入字节
            ps.write(97); // a

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

案例,将系统的打印语句改到某一文件内

package org.example.FileIO;

import java.io.FileOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;

public class Test17 {
    public static void main(String[] args) {
        // 将系统的System.out.println();打印改到打印到某一文件内
        System.out.println("111你好");
        System.out.println("222你好");
        
        try (
                PrintStream ps = new PrintStream("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji.txt");
        ) {
            // 把系统默认的打印流对象改成自己设置的
            System.setOut(ps);
            System.out.println("333你好");
            System.out.println("444你好");

            // 可以发现系统只打印了111你好和222你好,而下面的打印语句都在指定的文件内
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

数据流

数据输出流
  • 允许把数据和其类型一并写出去。
package org.example.FileIO;

import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;

public class Test18 {
    public static void main(String[] args) {
        // 打印流
        try (
                // 1.创建一个数据输出流包装低级的字节输出流
                DataOutputStream dos = new DataOutputStream(new FileOutputStream("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt"));
        ) {

            dos.writeInt(97);
            dos.writeDouble(99.5);
            dos.writeBoolean(true);
            dos.writeUTF("萧寂真帅");
            // 注意:里面数据不是乱码,可以通过此数据流读取出来

            
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
数据输入流
  • 用于读取数据输出流写出去的数据。
package org.example.FileIO;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class Test19 {
    public static void main(String[] args) {
        // 打印流
        try (
                // 1.创建一个数据输入流包装低级的字节输出流
                DataInputStream dos = new DataInputStream(new FileInputStream("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt"));
        ) {
            // 下面读取的顺序要和写入时的数据一致,否则会报错
            int i = dos.readInt();
            System.out.println(i);

            double i1 = dos.readDouble();
            System.out.println(i1);

            boolean i2 = dos.readBoolean();
            System.out.println(i2);

            String i3 = dos.readUTF();
            System.out.println(i3);


        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

序列化流

  • 对象序列化:把Java对象写入到文件中去
  • 对象反序列化:把文件里的ava对象读出来
  • 可以把Java对象进行序列化:把Java对象存入到文件中去。
对象字节输出流
package org.example.FileIO;

import java.io.*;

// 注意:对象如果需要序列化,必须实现序列化接口。
class User implements Serializable {
    private String loginname;
    private String username;
    private int age;
    // transient代表不被序列化,此时就算密码有值也是为null
    private transient String password;


    public User() {
    }

    public User(String loginname, String username, int age, String password) {
        this.loginname = loginname;
        this.username = username;
        this.age = age;
        this.password = password;
    }

    /**
     * 获取
     *
     * @return loginname
     */
    public String getLoginname() {
        return loginname;
    }

    /**
     * 设置
     *
     * @param loginname
     */
    public void setLoginname(String loginname) {
        this.loginname = loginname;
    }

    /**
     * 获取
     *
     * @return username
     */
    public String getUsername() {
        return username;
    }

    /**
     * 设置
     *
     * @param username
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * 获取
     *
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     *
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    /**
     * 获取
     *
     * @return password
     */
    public String getPassword() {
        return password;
    }

    /**
     * 设置
     *
     * @param password
     */
    public void setPassword(String password) {
        this.password = password;
    }

    public String toString() {
        return "User{loginname = " + loginname + ", username = " + username + ", age = " + age + ", password = " + password + "}";
    }
}

public class Test20 {
    public static void main(String[] args) {
        // 打印流
        try (
                // 创建对象字节输出流包装原始的字节输出流
                ObjectOutputStream dos = new ObjectOutputStream(new FileOutputStream("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt"));
        ) {
            // 创建对象
            User u = new User("admin", "张三", 32, "666");

            // 序列化对象到文件中(注意:对象如果需要序列化,必须实现序列化接口。)
            dos.writeObject(u);
            System.out.println("序列化对象成功");


        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
对象字节输入流
  • 可以把Java对象进行反序列化:把存储在文件中的Java对象读入到内存中来
package org.example.FileIO;

import java.io.*;

public class Test21 {
    public static void main(String[] args) {
        // 打印流
        try (
                // 创建对象字节输入流包装原始的字节输入流
                ObjectInputStream dos = new ObjectInputStream(new FileInputStream("E:\\studyproject\\text1\\demo02\\src\\main\\java\\org\\example\\File和IO\\xiaoji1.txt"));
        ) {
            User u = (User) dos.readObject();
            System.out.println(u); // 写入的对象


        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

一次性序列化多个对象的方案:将多个对象使用ArrayList进行存储,直接对集合序列化即可

ArrayList集合已经实现了序列化接口!

IO框架

推荐一个框架,名字为: commons-io

官方网站

参考视频

特殊文件-日志

  • 普通文件.txt
  • 属性文件.properties(本质也是文本文件,因此txt文本内部如果也是键值对形式的也可以使用properties相关的方法相关的操作)
  • XML文件.xml

properties文件概述

- 特点
    1、都只能是键值对
	2、键不能重复
	3、文件后缀一般是.properties结尾的
    
Properties
    是一个Map集合(键值对集合),但是我们一般不会当集合使用。
    核心作用:Properties是用来代表属性文件的,通过Properties可以读写属性文件里的内容
     使用Properties读取属性文件里的键值对数据

读取Properties文件

  • 如果打印的中文乱码了,如出现问号,自行去IDEA把文件编码改成utf-8(可以参考这篇博客)
package org.example.特殊文件;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
import java.util.Set;

public class Test {
    public static void main(String[] args) throws Exception {
        // 1. 创建一个properties的对象出来(键值对集合,空容器)
        Properties properties = new Properties();
        System.out.println(properties);

        // 2. 开始加载属性文件中的键值对数据到properties对象中去
        properties.load(new FileReader("E:\\studyproject\\text1\\demo02\\src\\aaa.properties"));

        System.out.println(properties); // // 拿到了全部Properties文件键值对转成的对象

        // 3.根据键取值
        System.out.println(properties.getProperty("username"));
        System.out.println(properties.getProperty("password"));

        // 4.遍历全部的键和值
        Set<String> keys = properties.stringPropertyNames();
        for (String key : keys) {
            String value = properties.getProperty(key);
            System.out.println(key+"---->"+value);
        }
        System.out.println("---------------");
        // 也可以通过foreach
        properties.forEach((k,v)->{
            System.out.println(k+"---->"+v);
        });

    }
}

将键值对数据存储到Properties文件中去

package org.example.特殊文件;

import java.io.FileReader;
import java.io.FileWriter;
import java.util.Properties;
import java.util.Set;

public class Test2 {
    public static void main(String[] args) throws Exception {
        // 1. 将键值对数据存入到Properties文件中去
        Properties properties = new Properties();
        properties.setProperty("张无忌","minmin");
        properties.setProperty("张翠山","susu");
        properties.setProperty("name","你好");
        properties.setProperty("age","18");

        // 2. 将Properties对象中的键值对数据存入到属性文件中去
        // 参数二是注释信息,随便填写就行(这个管道不需要关闭,内部会自动关闭)
        properties.store(new FileWriter("E:\\studyproject\\text1\\demo02\\src\\aaa.properties"),"练习");
    }
}

多线程

多线程的创建方式一:继承Thread类

  • 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
  • 创建MyThread类的对象
  • 调用线程对象的start()方法启动线程(启动后还是执行run方法的)

优缺点:

  • 优点:编码简单
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。

多线程的注意事项

  • 1、启动线程必须是调用start方法,不是调用run方法。
  • 2、不要把主线程任务放在启动子线程之前。
package org.example.多线程;

// 1.子类继承Thread线程类
class MyThread extends Thread{
    // 2. 必须重写run方法
    @Override
    public void run() {
        // 描述线程的执行任务
        for (int i = 0; i <= 100; i++) {
            System.out.println("子线程MyThread输出"+i);
        }
    }
}

class Test {
    public static void main(String[] args) {
        // 目标:掌握线程的创建方式一:继承Thread类

        // 3. 创建MyThread线程类的对象代表一个线程
        Thread t = new MyThread();
        t.start(); // 4. 启动线程(目前两个线程,main线程和t线程)(start实际调用了run方法)

        for (int i = 0; i <= 100; i++) {
            System.out.println("主线程main输出"+i);
        }
        
        // 上述代码可以发现每次执行代码主线程和子线程随机不定输出,即相互不影响各自执行各自的
    }
} 

多线程的创建方式二:实现Runnable接口

  • 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  • 创建MyRunnable任务对象
  • 把MyRunnable任务对象交给Thread处理
    Thread类提供的构造器(Thread)封装Runnable对象成为线程对象
  • 调用线程对象的start()方法启动线程

方式二的优缺点

  • 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强
  • 缺点:需要多一个Runnable对象。
package org.example.多线程;
// 1、定义一个任务类,实现Runnable接口
class MyRunnable implements Runnable{
    // 2、重写runnable的run方法

    @Override
    public void run() {
        for (int i = 0; i < 55; i++) {
            System.out.println("子线程输出"+i);
        }
    }
}

public class Test2 {
    public static void main(String[] args) {
        // 3.创建任务对象
        Runnable target = new MyRunnable();
        // 4.把任务对象交给线程对象来处理
        new Thread(target).start();

        // 主线程执行
        for (int i = 0; i < 55; i++) {
            System.out.println("主线程main输出 ==="+i);
        }
    }
}

线程创建方式二的匿名内部类写法

  • 可以创建Runnable的匿名内部类对象。
  • 再交给Thread线程对象。
  • 再调用线程对象的start()启动线程。
package org.example.多线程;

public class Test2_1 {
    public static void main(String[] args) {
        // 1. 直接创建Runnable接口的匿名内部类形式(任务对象)
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i < 5; i++) {
                    System.out.println("子线程1输出"+i);
                }
            }
        };

        new Thread(target).start();

        // 1-1: 简化形式
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i < 5; i++) {
                    System.out.println("子线程2输出"+i);
                }
            }
        }).start();
        
        // 1-2: 再次简化的形式
        new Thread(()->{
            for (int i = 1; i < 5; i++) {
                System.out.println("子线程3输出"+i);
            }
        }).start();

        // 主线程执行代码
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程main输出"+i);
        }
    }
} 

多线程的第三种创建方式Callable

前两种线程创建方式都存在的一个问题

  • 假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果

怎么解决这个问题?

  • JDK 5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)
  • 这种方式最大的优点:可以返回线程执行完毕后的结果

多线程的第三种创建方式:利用Callable接口、FutureTask类来实现

  • 创建任务对象
    • 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
    • 把Callable类型的对象封装成FutureTask(线程任务对象)
  • 把线程任务对象交给Thread对象。
  • 调用Thread对象的start方法启动线程。
  • 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。
package org.example.多线程;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

// 1、让这个类实现Callable接口
class MyCallable implements Callable<String> {
    private int n;

    public MyCallable(int n) {
        this.n = n;
    }

    // 2. 重写call方法
    @Override
    public String call() throws Exception {
        // 描述线程的任务,返回线程执行返回后的结果,
        // 需求:求1-n的和返回。
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return "线程求出了1-" + n + "的和是: " + sum;
    }
}

class Test3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3.创建一个Callable对象
        Callable<String> call = new MyCallable(5);
        // 4.把Callable的对象封装成一个FutureTask对象(任务对象)
        // 未来任务对象的作用?
        // 1、是一个任务对象,实现了Runnable对象
        // 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。
        FutureTask<String> f1 = new FutureTask<>(call);
        // 5. 把任务对象交给一个Thread对象
        new Thread(f1).start();


        // 3.创建一个Callable对象
        Callable<String> call2 = new MyCallable(10);
        FutureTask<String> f2 = new FutureTask<>(call2);
        new Thread(f2).start();


        // 6. 获取线程执行完毕后返回的结果
        // 注意:如果执行到这儿,假如上面的线程还没有执行完毕这里的代码会暂停,等待上面线程执行完毕后才会获取结果。
        System.out.println(f2.get()); // 实际调用的就是cell方法
        System.out.println(f1.get()); // 实际调用的就是cell方法
    }
}

线程创建方式三的优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
  • 缺点:编码复杂一点。

Thread提供的常用方法

package org.example.多线程;
class MyThread1 extends Thread{
    // 无参构造,方便使用默认名字(有的不想传参数)
    public MyThread1(){
    }
    // 有参构造器
    public MyThread1(String name){
        // 给线程设置名字(将名字传递给父类调用)
        super(name);
    }
    @Override
    public void run() {
        // 哪个线程执行它,它就会得到哪个线程对象。
        Thread m = Thread.currentThread();
        for (int i = 0; i <= 5; i++) {
            System.out.println(m.getName()+"子线程输出"+i);
        }
    }
}

class Test4 {
    public static void main(String[] args) throws InterruptedException {
        // 目标:掌握sleep方法,join方法的作用。
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
            // 到3时休眠五秒再继续执行
            if(i==3){
                Thread.sleep(5000);
            }
        }

        // join方法作用:让当前调用这个方法的线程先执行完。
        // 这段代码意思就是当thread1执行完毕后会去执行thread2,当thread2执行完毕后才会去执行thread3,相当于一个异步等待
        // 哪个线程带有join后面的代码都要等这个线程结束才执行
        Thread thread1 = new MyThread1("子线程1");
        thread1.start();
        thread1.join();
        Thread thread2 = new MyThread1("子线程2");
        thread2.start();
        thread2.join();
        Thread thread3 = new MyThread1("子线程3");
        thread3.start();
        thread3.join();


        Thread t1 = new MyThread1("子线程");
        // 给子线程设置名字(上面也是相同效果,使用了有参构造,需要在类上使用有参构造),必须设置在start之前,默认名为Thread-0
        // t1.setName("子线程1");
        t1.start();
        // 打印子线程对象的名字
        System.out.println(t1.getName()); // 子线程1

        Thread t2 = new MyThread1();
        t2.start();
        System.out.println(t2.getName()); // Thread-1

        // 主线程对象
        // 哪个线程执行它,它就会得到哪个线程对象。
        Thread m = Thread.currentThread();
        System.out.println(m); // Thread[main,5,main]
        // 主线程对象的名字
        System.out.println(m.getName()); // main

        for (int i = 0; i <= 5; i++) {
            System.out.println(m.getName()+"主线程输出"+i);
        }
    }
}

线程安全问题

  • 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
  • 取钱的线程安全问题
    场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?

线程安全问题出现的原因?

  • 存在多个线程在同时执行
  • 同时访问一个共享资源
  • 存在修改该共享资源

上面取钱案例的bug复现(小红小明都取100000,银行只有100000,导致之后结果为-100000)

分析:

  • 需要提供一个账户类,接着创建一个账户对象代表2个人的共享账户
  • 需要定义一个线程类(用于创建两个线程,分别代表小明和小红)。
  • 创建2个线程,传入同一个账户对象给2个线程处理。
  • 启动2个线程,同时去同一个账户对象中取钱10万
package org.example.多线程;

// 账户类
class Account {
    private String CardId; // 卡号(这个案例没啥用)
    private double money; // 余额

    public Account(String cardId, double money) {
        CardId = cardId;
        this.money = money;
    }

    public Account() {
    }

    public void setCardId(String cardId) {
        CardId = cardId;
    }

    public void setMoney(double money) {
        this.money = money;
    }

    public String getCardId() {
        return CardId;
    }

    public double getMoney() {
        return money;
    }

    @Override
    public String toString() {
        return "Account{" +
                "CardId='" + CardId + '\'' +
                ", money=" + money +
                '}';
    }

    // 小明和小红同时来取钱
    public void drawMoney(double money) {
        // money代表的是取钱的数量
        // 先搞明白谁先来取钱(取线程名字)
        String name = Thread.currentThread().getName();
        System.out.println(name); // 得到线程名称
        // 1. 判断余额是否足够
        if (this.money >= money) {
            System.out.println(name + "来取钱" + money + "成功!");
            this.money -= money;
            System.out.println(name + "取钱后余额剩余" + this.money);
        } else {
            System.out.println(name + "来取钱,余额不足");
        }

    }
}

// 线程类
class DrawThead extends Thread {
    private Account acc;

    // 有参构造器
    public DrawThead(Account acc, String name) {
        // 设置线程名字,用于保存记录是谁在取钱
        super(name);
        this.acc = acc;
    }

    @Override
    public void run() {
        // 取钱(小明和小红)
        acc.drawMoney(100000);
    }
}

class Test5 {
    public static void main(String[] args) {
        // 模拟取钱(模拟线程安全问题)
        // 1. 创建一个账户对象,代表两个人的共享账户.
        Account acc = new Account("TCBC-110", 100000);

        // 2. 创建两个线程, 分别代表小明和小红,再去同一个账户对象中取10000
        new DrawThead(acc, "小明").start(); // 小明
        new DrawThead(acc, "小红").start(); // 小红
    }
}

线程同步(解决线程安全问题)

认识线程同步

  • 让多个线程实现先后依次访问共享资源,这样就解决了安全问题

线程同步的常见方案

  • 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。

方式一:同步代码块

  • 作用:把访问共享资源的核心代码给上锁,以此保证线程安全
synchronized(同步锁){
    访问共享资源的核心代码
}
  • 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。

同步锁的注意事项

  • 对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。

把上面取钱的案例稍加改造,将下面这段代码替换到上面的取钱的案例里面即可,然后功能就正常了

package org.example.多线程;

// 账户类
class Account {
    private String CardId; // 卡号(这个案例没啥用)
    private double money; // 余额

    public Account(String cardId, double money) {
        CardId = cardId;
        this.money = money;
    }

    public Account() {
    }

    public void setCardId(String cardId) {
        CardId = cardId;
    }

    public void setMoney(double money) {
        this.money = money;
    }

    public String getCardId() {
        return CardId;
    }

    public double getMoney() {
        return money;
    }

    @Override
    public String toString() {
        return "Account{" +
                "CardId='" + CardId + '\'' +
                ", money=" + money +
                '}';
    }

    public static void test(){
        synchronized (Account.class){
            // 由于静态方法在计算机里面只有一份,因此官方建议静态方法使用类名.class作为锁对象
        }
    }

    // 小明和小红同时来取钱
    public void drawMoney(double money) {
        // money代表的是取钱的数量
        // 先搞明白谁先来取钱(取线程名字)
        String name = Thread.currentThread().getName();
        System.out.println(name); // 得到线程名称
        // 1. 判断余额是否足够

        // 加锁
        // "萧寂" 字符串在计算机里面只存一份,也就是相当于给锁对象,然后这个锁在计算机里面也只有一份,后面无论创建多少对象,只要是这个线程的都会是同一把锁,例如下面的小黑和小白就是新创建了的对象但是由于钱被小明和小红取完了,导致小黑和小白没钱取了,但是对象是新创建的,按照逻辑来讲,小黑和小白应该是针对新一轮的10万元去取钱,但是由于走的同一线程,同一把锁,导致数据错乱
        // 为了解决上面的问题,官方建议使用账户类对象作为锁对象,这样,小红和小明共用同一个账户,小黑和小白共用同一个账户
        synchronized (this) {
            if (this.money >= money) {
                System.out.println(name + "来取钱" + money + "成功!");
                this.money -= money;
                System.out.println(name + "取钱后余额剩余" + this.money);
            } else {
                System.out.println(name + "来取钱,余额不足");
            }
        }

    }
}

// 线程类
class DrawThead extends Thread {
    private Account acc;

    // 有参构造器
    public DrawThead(Account acc, String name) {
        // 设置线程名字,用于保存记录是谁在取钱
        super(name);
        this.acc = acc;
    }

    @Override
    public void run() {
        // 取钱(小明和小红)
        acc.drawMoney(50000);
    }
}

class Test5 {
    public static void main(String[] args) {
        // 模拟取钱(模拟线程安全问题)
        // 1. 创建一个账户对象,代表两个人的共享账户.
        Account acc = new Account("TCBC-110", 100000);
        // 2. 创建两个线程, 分别代表小明和小红,再去同一个账户对象中取10000
        new DrawThead(acc, "小明").start();// 小明
        new DrawThead(acc, "小红").start(); // 小红


        Account acc1 = new Account("TCBC-110", 100000);
        new DrawThead(acc1, "小黑").start();
        new DrawThead(acc1, "小白").start();
    }
}

锁对象随便选择一个唯一的对象好不好呢?

  • 不好,会影响其他无关线程的执行。

锁对象的使用规范

  • 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象,
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。

方式二:同步方法

  • 作用:把访问共享资源的核心方法给上锁,以此保证线程安全.
修饰符 synchronized 返回值类型 方法名称(形参列表){
    操作共享资源的代码
}
  • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

把上面的取钱的方法替换成下面这个,完美解决(即使和上面的小黑小白一样新创建账户类对象也是没问题的),只需要在方法前面加synchronized

    // 小明和小红同时来取钱
    public synchronized void drawMoney(double money) {
        // money代表的是取钱的数量
        // 先搞明白谁先来取钱(取线程名字)
        String name = Thread.currentThread().getName();
        System.out.println(name); // 得到线程名称
        // 1. 判断余额是否足够
            if (this.money >= money) {
                System.out.println(name + "来取钱" + money + "成功!");
                this.money -= money;
                System.out.println(name + "取钱后余额剩余" + this.money);
            } else {
                System.out.println(name + "来取钱,余额不足");
            }

    }

同步方法底层原理

  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码.
  • 如果方法是实例方法:同步方法默认用this作为的锁对象.
  • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象

同步代码块范围小点,性能稍微好点,同步方法范围大,性能稍微差点,但是这点性能计算机几乎忽略不计

可读性:同步方法更好。

方法三:Lock锁

  • Lock锁是IDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

将上面的账户类替换成下面这个

// 账户类
class Account {
    private String CardId; // 卡号(这个案例没啥用)
    private double money; // 余额
    private final Lock lk = new ReentrantLock(); // 1.创建锁对象(每个账户都应该有自己的锁对象,并且不能被替换,因此使用final修饰)

    public Account(String cardId, double money) {
        CardId = cardId;
        this.money = money;
    }

    public Account() {
    }

    public void setCardId(String cardId) {
        CardId = cardId;
    }

    public void setMoney(double money) {
        this.money = money;
    }

    public String getCardId() {
        return CardId;
    }

    public double getMoney() {
        return money;
    }

    @Override
    public String toString() {
        return "Account{" +
                "CardId='" + CardId + '\'' +
                ", money=" + money +
                '}';
    }

    // 小明和小红同时来取钱
    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();

        // 2.加锁(程序运行到这里先加锁,执行完毕后再解锁,这里要考虑异常,因为如果加锁和解锁中间的程序出现了bug,就直接跳走了,不会执行到下面的解锁操作,因此需要使用try-catch-finally包裹)
        lk.lock();
        System.out.println(name); // 得到线程名称
            try {
            // 1. 判断余额是否足够
            if (this.money >= money) {
                System.out.println(name + "来取钱" + money + "成功!");
                this.money -= money;
                System.out.println(name + "取钱后余额剩余" + this.money);
            } else {
                System.out.println(name + "来取钱,余额不足");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            // 3. 程序运行完解锁
            lk.unlock();
        }
    }
}

线程池

什么是线程池?

  • 线程池就是一个可以复用线程的技术。

不使用线程池的问题

  • 用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,
    而创
  • 建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能,

如何创建线程池

谁代表线程池?

  • JDK 5.0起提供了代表线程池的接口:ExecutorService。

如何得到线程池对象?

  • 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
  • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

方式一的使用

ThreadPoolExecutor构造器

public ThreadPoolExecutor(int corePoolsize, int maximumPoolsize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable>workQueue, ThreadFactory threadFactory,RejectedExecutionHandler handler)
  • 参数一:corePoolSize:指定线程池的核心线程的数量,(正式工:3人)
  • 参数二:maximumPoolSize:指定线程池的最大线程数量。(最大员工数量:5人 临时工:2人)
  • 参数三:keepAliveTime:指定临时线程的存活时间。(临时工空闲多久被开除)
  • 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天) (临时工工期)
  • 参数五:workQueue:指定线程池的任务队列。(客人排队的地方)
  • 参数六:threadFactory:指定线程池的线程工厂。(负责招聘员工的(HR))
  • 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理) (忙不过来怎么办?)

创建线程池对象

package org.example.多线程;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Test7 {
    public static void main(String[] args) {
        // 通过ThreadPoolExecutor创建一个线程池对象。
        // 参数一:核心线程数量(复用线程数量)
        // 参数二:最大线程数量(临时线程 = 最大线程 - 核心线程)
        // 参数三:临时线程存活时间
        // 参数四:临时线程存活时间的单位(秒,分,时,天) TimeUnit.SECONDS 是秒
        // 参数五:声明任务队列(为线程池缓存处理任务) ArrayBlockingQueue是基于数组实现的,也是常用的,参数是任务队列的大小,代表最多只能缓存几个任务
        // 参数六:线程工厂,是负责为线程池创建线程的 Executors.defaultThreadFactory() 是获取默认的线程池工厂
        // 参数七:任务拒绝策略(当核心线程在工作,临时线程也在工作,那么多余的任务拒绝的处理方式)  new ThreadPoolExecutor.AbortPolicy() 代表新任务来了之后直接抛出异常告诉新任务无法处理
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
    }
}

线程池的注意事项
1、临时线程什么时候创建?

  • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

2、什么时候会开始拒绝新任务?

  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。

3、任务拒绝策略的方案

new ThreadPoolExecutor.AbortPolicy();  丢弃任务并抛出RejectedExecutionException异常。是默认的策略
new ThreadPoolExecutor.DiscardPolicy();  丢弃任务,但是不抛出异常 这是不推荐的做法
new ThreadPoolExecutor.DiscardOldestPolicy();  抛弃队列中等待最久的任务 然后把当前任务加入队列中
new ThreadPoolExecutor.CallerRunsPolicy();  由主线程负责调用任务的run()方法从而绕过线程池直接执行
线程池处理Runnable任务
package org.example.多线程;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

class MyRunnables implements Runnable{
    @Override
    public void run() {
        // 任务是干啥的
        System.out.println(Thread.currentThread().getName()+"===>输出666"); // 获取当前线程名字,可以清晰看到哪一个线程在执行
        try {
            // Thread.sleep(1000); // 每个线程都工作一秒就开始下一轮任务处理
            Thread.sleep(Integer.MAX_VALUE); // 这样就可以把核心线程停留到最长时间,以便模拟当核心线程都在忙时临时线程会出来工作
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

class Test7 {
    public static void main(String[] args) {
        // 通过ThreadPoolExecutor创建一个线程池对象。
        // 参数一:核心线程数量(复用线程数量)
        // 参数二:最大线程数量(临时线程 = 最大线程 - 核心线程) 临时线程执行条件(线程池溢出后执行,例如有8个任务,核心线程执行了3个,此时线程池还剩下5个,我们设置线程池最大数量为4,就相当于超出线程池了,这时,临时线程会处理多出部分的,例如当前代码临时线程为2,设置任务条数为10个,则核心线程处理3个,四个放在线程池,多余出来3个,但是临时线程由于只有两条则只能处理两个任务,那多出来的一个任务则会被参数七的任务拒绝策略处理( 可以在第七个参数设置拒绝处理策略 ))
        // 参数三:临时线程存活时间
        // 参数四:临时线程存活时间的单位(秒,分,时,天) TimeUnit.SECONDS 是秒
        // 参数五:声明任务队列(为线程池缓存处理任务) ArrayBlockingQueue是基于数组实现的,也是常用的,参数是任务队列的大小,代表最多只能缓存几个任务
        // 参数六:线程工厂,是负责为线程池创建线程的 Executors.defaultThreadFactory() 是获取默认的线程池工厂
        // 参数七:任务拒绝策略(当核心线程在工作,临时线程也在工作,那么多余的任务拒绝的处理方式)

        // 参数七的拒绝策略:
        // new ThreadPoolExecutor.AbortPolicy();  丢弃任务并抛出RejectedExecutionException异常。是默认的策略
        // new ThreadPoolExecutor.DiscardPolicy();  丢弃任务,但是不抛出异常 这是不推荐的做法
        // new ThreadPoolExecutor.DiscardOldestPolicy();  抛弃队列中等待最久的任务 然后把当前任务加入队列中
        // new ThreadPoolExecutor.CallerRunsPolicy();  由主线程负责调用任务的run()方法从而绕过线程池直接执行
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());

        // 创建任务对象
        MyRunnables myRunnables = new MyRunnables();
        pool.execute(myRunnables); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
        pool.execute(myRunnables);
        pool.execute(myRunnables);
        pool.execute(myRunnables);
        pool.execute(myRunnables);
        pool.execute(myRunnables);
        pool.execute(myRunnables);
        // 到了临时线程的创建时机
        pool.execute(myRunnables);
        pool.execute(myRunnables);
        pool.execute(myRunnables); // 这个超出的任务会被任务拒绝策略处理
        pool.execute(myRunnables); // 这个超出的任务会被任务拒绝策略处理

        // 当任务执行完毕后关闭线程池
        // pool.shutdown(); // 可以发现当以上6个任务都结束才关闭线程池
        // 无论任务是否执行完毕,立即关闭线程池
        // pool.shutdownNow(); // 会报错 "pool-1-thread-2" Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-3" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
    }
}
线程池处理Callable任务
package org.example.多线程;

import java.util.concurrent.*;

// 1、让这个类实现Callable接口
class MyCallable1 implements Callable<String> {
    private int n;

    public MyCallable1(int n) {
        this.n = n;
    }

    // 2. 重写call方法
    @Override
    public String call() throws Exception {
        // 描述线程的任务,返回线程执行返回后的结果,
        // 需求:求1-n的和返回。
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName()+"线程求出了1-" + n + "的和是: " + sum;
    }
}
public class Test8 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
        // 使用线程处理Callable任务
        Future<String> f1 = pool.submit(new MyCallable1(100));
        Future<String> f2 = pool.submit(new MyCallable1(200));
        Future<String> f3 = pool.submit(new MyCallable1(300));
        Future<String> f4 = pool.submit(new MyCallable1(400));
        Future<String> f5 = pool.submit(new MyCallable1(500));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
        System.out.println(f5.get());
        // 上面打印的都是123线程的,此时还未使用到临时线程,因为线程池未溢出
    }
}

工具类:Executors

注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。

Executors.newFixedThreadPool(int nThreads)  创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
Executors.newSingleThreadExecutor()  创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
Executors.newCachedThreadPool()  线程数量随着任务增加而增加,如果线程任务执行完与且空闲了60s则会被回收掉。
Executors.newScheduledThreadPool(int corePoolSize)  创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

package org.example.多线程;

import java.util.concurrent.*;

// 1、让这个类实现Callable接口
class MyCallable1 implements Callable<String> {
    private int n;

    public MyCallable1(int n) {
        this.n = n;
    }

    // 2. 重写call方法
    @Override
    public String call() throws Exception {
        // 描述线程的任务,返回线程执行返回后的结果,
        // 需求:求1-n的和返回。
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName()+"线程求出了1-" + n + "的和是: " + sum;
    }
}
public class Test8 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // Executors.newFixedThreadPool(int nThreads)  创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
        // Executors.newSingleThreadExecutor()  创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
        // Executors.newCachedThreadPool()  线程数量随着任务增加而增加,如果线程任务执行完与且空闲了60s则会被回收掉。
        // Executors.newScheduledThreadPool(int corePoolSize)  创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

        // 创建固定数量的线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(3);
        
        // 核心线程数量应该配置多少???
        // 计算密集型的任务: 核心线程数量 = CPU的核数 + 1
        // IO密集型的任务: 核心线程数量 = CPU的核数 + 2
        
        // 使用线程处理Callable任务
        Future<String> f1 = pool.submit(new MyCallable1(100));
        Future<String> f2 = pool.submit(new MyCallable1(200));
        Future<String> f3 = pool.submit(new MyCallable1(300));
        Future<String> f4 = pool.submit(new MyCallable1(400));
        Future<String> f5 = pool.submit(new MyCallable1(500));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
        System.out.println(f5.get());
        // 上面打印的都是123线程的,此时还未使用到临时线程,因为线程池未溢出
    }
}

核心线程数量应该配置多少???

计算密集型的任务: 核心线程数量 = CPU的核数 + 1
IO密集型的任务: 核心线程数量 = CPU的核数 + 2

CPU核数查看:
   windows电脑:Ctrl+Shift+Esc调出任务管理器,点击性能,点击CPU,界面有个逻辑处理器(也就是线程的数量),这就是核数

Executors使用可能存在的陷阱
大型并发系统环境中使用Executors如果不注意可能会出现系统风险

乐观锁悲观锁概念

悲观锁: 一上来就加锁,没有安全感。每次只能一个线程进入访问完毕后,再解锁。线程安全,性能较差!
乐观锁: 开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全问题的时候才开始控制。线程安全,性能较好。

带有线程安全问题的代码,后面的乐观悲观锁会在这个代码基础上改造

package org.example.多线程;

class MyTest9 implements Runnable{
    private int count; // 记录浏览人次
    @Override
    public void run() {
        // 100次
        for (int i = 1; i <= 100; i++) {
            System.out.println("count========>"+(++count));
        }
    }
}

public class Test9 {
    public static void main(String[] args) {
        // 需求:1变量,100个线程,每个线程对其加100次。
        Runnable runnable = new MyTest9();

        // 主线程100次(一共10000次,多次启动代码发现有时候打印10000有时候打印9999,有时候是9998,这就造成了线程安全问题)
        // 多个线程访问统一资源会造成线程安全问题
        for (int i = 1; i <= 100; i++) {
            new Thread(runnable).start();
        }


    }
}

悲观锁示例代码

一上来就加锁,没有安全感。每次只能一个线程进入访问完毕后,再解锁。线程安全,性能较差!

package org.example.多线程;

class MyTest9 implements Runnable{
    private int count; // 记录浏览人次
    @Override
    public void run() {
        // 100次
        for (int i = 1; i <= 100; i++) {
            // 悲观锁(每次循环都上锁都要排队,相当于10000个人排了一万次队,性能较差)
            synchronized (this){
                System.out.println(Thread.currentThread().getName()+"count========>"+(++count));
            }
        }
    }
}

public class Test9 {
    public static void main(String[] args) {
        // 需求:1变量,100个线程,每个线程对其加100次。
        Runnable runnable = new MyTest9();

        // 主线程100次(一共10000次,多次启动代码发现有时候打印10000有时候打印9999,有时候是9998,这就造成了线程安全问题)
        // 多个线程访问统一资源会造成线程安全问题
        for (int i = 1; i <= 100; i++) {
            new Thread(runnable).start();
        }
    }
}

乐观锁示例代码

开始不上锁,认为是没有问题的(因为计算机计算性能很高,例如刚刚的代码计算一万次,可能运行两三次才会出现一次线程不安全),大家一起跑,等要出现线程安全问题的时候才开始控制。线程安全,性能较好。

利用了CSA算法,相当于一个单向链表,每次循环都记住上一次累加的值,如果下一次循环得到的值跟期望的值不一致,就会把当前修改的值作废,就会加锁,重新匹配所记录的值

package org.example.多线程;

import java.util.concurrent.atomic.AtomicInteger;

class MyTest9 implements Runnable {
    // 整数修改的乐观锁:原子类实现的。
    private AtomicInteger count = new AtomicInteger(); // 记录浏览人次

    @Override
    public void run() {
        // 100次
        for (int i = 1; i <= 100; i++) {
            // incrementAndGet()加一后再返回(乐观锁的方法)
            System.out.println(Thread.currentThread().getName() + "count========>" + count.incrementAndGet());
        }
    }
}

public class Test9 {
    public static void main(String[] args) {
        // 需求:1变量,100个线程,每个线程对其加100次。
        Runnable runnable = new MyTest9();

        // 主线程100次(一共10000次,多次启动代码发现有时候打印10000有时候打印9999,有时候是9998,这就造成了线程安全问题)
        // 多个线程访问统一资源会造成线程安全问题
        for (int i = 1; i <= 100; i++) {
            new Thread(runnable).start();
        }
    }
}

网络编程

后面有框架封装了,这里我只学习不做笔记了

Java高级

Junit单元测试

  • 就是针对最小的功能单元(方法),编写测试代码对其进行正确性测试

  • 可以用来对方法进行测试,它是第三方公司开源出来的(很多开发工具已经集成了Junit框架,比如IDEA)

  • 可以灵活的编写测试代码,可以针对某个方法执行测试,也支持一键完成对全部方法的自动化测试,且各自独立。

  • 不需要程序员去分析测试的结果,会自动生成测试报告出来不需要程序员去分析测试的结果,会自动生成测试报告出来

具体步骤

  • 将junit框架的jar包导入到项目中(注意:IDEA集成了Junit框架,不需要我们自己手工导入了)
  • 为需要测试的业务类,定义对应的测试类,并为每个业务方法,编写对应的测试方法(必须:公共、无参、无返回值)
  • 测试方法上必须声明@Test注解,然后在测试方法中,编写代码调用被测试的业务方法进行测试;
  • 开始测试:选中测试方法,右键选择“JUnit运行”,如果测试通过则是绿色;如果测试失败,则是红色

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/574388.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

redis和mysql数据一致性方案

请求 A 更新数据 请求B读数据 在高并发情况下&#xff0c;A、B请求过程步骤相互穿插&#xff0c;就会出现图中的问题。 期望redis 的数据是11&#xff0c;最后变成了10 场景&#xff1a;先删除Redis&#xff0c;再更新 MySQL&#xff0c;不主动更新Redis&#xff0c;访问redi…

软件企业质量保证的基石――QA,QC的良性协作

软件企业质量保证的基石――QA、QC的良性协作 国内的软件产业发展了20多年的时间&#xff0c;已经由个人英雄时代步入到中、小团队协作时代。相信不久的将来&#xff0c;国内一定会出现航母级的软件企业&#xff0c;那时候我们会迎来集团军作战的时代。不同的时代表明软件规模…

【AI相关】数学和统计学知识

数学和统计学的知识可以分为两部分&#xff1a; 一部分是线性代数中的基础概念&#xff0c;比如标量、向量和张量。 另一部分是概率统计中常见的分布类型&#xff0c;比如正态分布和伯努利分布。 线性代数 什么是标量&#xff1f; 标量是一个单独的数&#xff0c;可以是整…

PLC基本原理及其接线

目录&#xff1a; 一、PLC的简介与起源 二、PLC的基本结构 1、MCU模块 2、输入与输出模块 3、电源模块 4、编程设备 三、PLC的外部接线 1、PNP与NPN接线 2、三菱PLC的NPN与PNP设置 下续&#xff1a;三菱FX系列PLC-编程1 一、PLC的简介与起源 1、PLC简介 PLC的全称为…

Prompt之美:如何设计提示词让大模型变“聪明”

目录 一. Prompt关键要素 二. Prompt技巧 三. 实战中的Prompt优化 四. 参考文献 一. Prompt关键要素 Prompt是一个简短的文本输入&#xff0c;用于引导AI模型生成特定的回答或执行特定任务。换句话说&#xff0c;Prompt是你与AI模型沟通的方式。一个好的Prompt可以让AI更准…

猫头虎分享已解决Bug || **Error: ‘Promise‘ is undefined**

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

【SpringBoot】00 Maven配置及创建项目

一、Maven配置 1、下载Maven 进入官网下载&#xff1a;Maven – Welcome to Apache MavenMaven – Download Apache Maven 本文以最新版为例&#xff0c;可按需选择版本 Maven – Welcome to Apache Maven 2、解压下载好的安装包 将安装包解压到自己设置的空文件夹中 3、…

抽象的代理模式1.0版本

前言&#xff1a; 在阅读Spring Security官方文档时&#xff0c;里面设计到了一种设计模式——代理模式Proxy 众里寻她千百度&#xff0c;蓦然回首&#xff0c;那人却在灯火阑珊处 开始 在之前的文章里陈述了一个观点——编程语言和语言没有区别 现看看我们日常生活中的代理…

利用 easycode 自动生成 数据库表 对应 类文件

1、安装easycode 打开settings&#xff0c;在plugins中搜索easycode进行安装&#xff0c;安装完成后重启idea。 2、连接数据库 连接数据库&#xff0c;填写数据库配置信息 点解Test connetction测试连接&#xff0c; 3、生成文件 右键数据库表格&#xff0c;生成对应文件 4…

说方法不如传授经验向媒体投稿你可以这样

在信息爆炸的时代,作为单位的信息宣传员,肩负着将本单位的重要资讯、活动成果、政策解读等内容有效传播至公众视野的重任。其中,向各类媒体投稿无疑是实现这一目标的重要途径。然而,传统的邮件投稿方式常常让我深感力不从心,费时费力不说,成功率低、出稿慢等问题更是让我和领导…

商标申请注册交费就一定会下注册证?

近日遇到一个网友说普推知产老杨说&#xff0c;他以为商标交钱就一定会下商标注册证&#xff0c;这个不管找哪家也做不到的。商标申请注册时要给商标局交费用&#xff0c;交完费用商标才有商标的形式审查&#xff0c;通过后下受理书&#xff0c;才有后面商标实质审查&#xff0…

[柏鹭杯 2021]试试大数据分解?

题目&#xff1a;&#xff08;NSSCTF | 在线CTF平台&#xff09; 题目就是如此&#xff0c;我没看到有5个不同的文本&#xff0c;其中最后一个文本以pem后缀&#xff0c;所以我们先来了解一下什么是pem格式。 PEM 格式 PEM格式通常用于数字证书认证机构&#xff08;Certifica…

UI自动化测试框架:PO 模式+数据驱动(超详细)

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号【互联网杂货铺】&#xff0c;回复 1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1、PO 设计模式简介 什么是 PO 模式&#xff1f; PO&#xff…

雅特力AT32F435学习——2.ADC实验

ADC实验 ADC是什么、重要性就不再这里多说&#xff0c;ADC这个外设以及关于ADC的应用程序用途非常之广泛很值得深挖&#xff0c;接下来就让我们学一下雅特力AT32F435单片机的ADC吧。 基础知识 不同厂商的单片机的ADC外设都是有区别的&#xff0c;比如ADC转换位数、采样频率等…

【SpringCloud】OpenFeign高级特性

【SpringCloud】OpenFeign高级特性 文章目录 【SpringCloud】OpenFeign高级特性1. 超时控制1.1 全局配置1.2 指定配置 2. 重试机制3. 替换Http客户端3.1 引入依赖3.2 配置 4. 请求/响应压缩5. 日志打印6. 综合配置 1. 超时控制 默认OpenFeign客户端等待60秒钟&#xff0c;但是服…

传承汉字,发扬光大!让《米小圈动画汉字》也出一份力吧!

目前社会上出现的诸如“计算机是汉字的掘慕人”、“汉字是行将就木的老人”“必须废除汉字,汉字要走拼音化迷路”等观点,值得重视。专家们说,20年前如提汉字要走拼音化道路,主要是缘于汉字难以输入电脑,而时至今日,汉字在电脑上早已畅通无阻,如果仍坚持“汉字拙劣,必得用拼音文…

SAP 如何控制生产订单发料后不能删除组件

SAP默认的情况下,即使工单中的组件已发料了,但仍可以进行删除的标志。这种情况是不太符合逻辑的,如果真要删除,应该先退料,然后再上删除标志。并不能再物料还是已领料的状态下 就对物料做删除的操作。 如下图 生产订单在已经领料的情况下,仍然的被打上了删除标识。 我们…

【产品经理修炼之道】- 从需求到功能的转化过程

产品经理的最大作用是将需求转化为产品或者功能&#xff0c;从需求到功能&#xff0c;会经历哪些过程&#xff1f;本文总结了从需求到功能的转化过程&#xff0c;希望对你进一步了解有所帮助。 “大部分的产品经理特别是数字化产品经理其核心价值就是如何去解决如何把需求转化为…

MySQL主键:自增id、UUID、雪花算法

视频可看&#xff1a; 动画讲解&#xff1a;为什么不能使用自增ID或者UUID做MySQL的主键&#xff0c;雪花算法生成的主键存在哪些问题_哔哩哔哩_bilibili 一、MySQL分布式架构中&#xff0c;为什么不能使用自增id作为主键 自增主键的好处&#xff1a;写入效率高 弊端&#x…

你只可以转让未使用“通过 Apple 登录”功能的 App。

你只可以转让未使用“通过 Apple 登录”功能的 App。 因为这个问题遇到的比较少&#xff0c;同时也比较难以解决&#xff0c;所以这个问题的答案&#xff0c;必须要开会员我才让你们看。 华丽的开会员分割线 当前问题的主要原因是被接入的账号有30天的封号提示了&#xff0c;…
最新文章