Note of java修改于2023.03.13

2024-01-18 06:59
文章标签 java note 修改 13 2023.03

本文主要是介绍Note of java修改于2023.03.13,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

image-20230319001002202

*** Note of java***

类与对象之前:

Java内存:

  • 堆内存-
  • 栈内存-
  • 方法区-
1.首先一定要注意大小写,因为大写和小写是有区别的,如System.out.printf();前面的大写S而不是小写的,这个是输出
2. Scanner scanner=new Scanner(System .in);//输入前必须要写的,写一次就够啦!
import java.util.Scanner;//一写scanner会自动召唤该模块!在顶部的
然后输入就是scanner.nextInt();这个是输入int的scanner.next();这个是输入字符串的,但是遇到空格会终止所以应该用scanner.nextLine();这样是按enter才是终止的
3. import java.util.*;(写在最开头,相当于c语言中的头文件)然后在你要输入的地方输入以下代码Scanner input=new Scanner(System.in);(这里的input 自己可以改,只要与下面的变量名保持一致就好)如果接收整形,用nextIntint i=input.nextInt();如果要输入字符,用nextLineString i =input.nextLine();如果是浮点型中的floatfloat i=input.nextFloat(); 如果是浮点型中的doubledouble i=input.nextDouble();
 4.在c语言的函数,在Java中叫方法5.字符串+字符串=新的字符串,字符串可以直接相连接,用加法。6.位运算左移<<一位,相当乘2,左移2位,相当于再左移一位的基础上x2.所以右移>>一位是除27.换行字符也是\n8.System.out.println()自带换行功能
9.一共有8大数据类型整型:bye(保存数字是最小的,只有127)ShortIntLong(要在数字后面加个L)这是与c不一样的地方。还有,可以向下兼容数据,如:short b=200;int a=b;可以成立,因为int的数据空间大于short;但如果int b=200;short a=b;错误,因为b的数据类型是int型,可包容的数据更大,所以不可以小的向上兼容,只能向下兼容,也就是向第一种那样!浮点型:Float:如果要想使用float型,要在数字后面加上F;否则变成double型数数据Double整数类型可以向下兼容,比如int类型如果没有超过byte范围,可以自动转化为byte类型,但是超过了,强制转换是可以转换的,但是会缺失精度的问题。double可以变成float型,没有超过的情况下。也就是说,没有超过范围,自动转型,超出范围就要手动强制类型转换需要注意的是在使用long和float类型的时候,需要在值的后面加上L和F来做标记,否则会被识别为int和double类型字符型:Char字符串型,(不是基本的数据类型,是对象类型)String a=“hello”;这是c语言不能那么简单的,c语言字符串要么数组,要么指针限定。10.设置变量是,在数据类型前面加上final,该变量一旦赋值后,就不能改变。类似于c语言的const关键字11.二进制计算和转换转换:如:11011一共有五位数字,所以应该这样算1*2^5-1+1*2^3+0*2^2+1*2^1+1*2^0十进制转换成2进制:短除法一直下去直到0注意事项:在java中无论是小数还是整数都要带符号位,首位就是符号位,如11001,其符号
位就是1,后面才是二进制。0代表正数,1代表负数。C语言是是没有符号位的》

类与对象:

  • 如果一个源文件里面,有且只有一个public类,就是说,比如我设置了a类,在下面又class b,b不能加public。
  • 如果一个类是public修饰,那么这个源文件一定和public类名相同
  • 使用多个类,最好就是新建不同的源文件,也就是新建多个类
  • 补充对象快捷键,ctrl+alt+v
  • image-20230314092729483
  • image-20230314092900212
  • 12.类就是抽象的一种分类,如人类,鸟类等,具有相同的特点,有点像c语言的结构体,但又不是完全结构体,等下解释

对象是类的具体,如人类具体到某一个人。

创建一个学生类:public class student {String name;Int age;}
这是一个学生类,里面有2中属性Student p1=new student();Student p2=new student();这个2个对象,也相当于是两个人,也叫实例P1.name=”李金钊“;P1.age=20;这是修改对象的属性
如果这是student p3=p1;P1.name=ckc;P3也是跟着会变,因为p1,p2,p3都是类似于指针,指向某一个对象或者新的对象,而c语言p3是不会跟着变的,因为它相当于是拷贝了一份,这里是指同一个对象。类似于指针类名 变量名=new 类名();
  • 13.在类里面自定义函数(跟c语言一样),在主函数可以调用对象,调用函数。函数在java中叫方法,
void swap(int a,int b)
{System.out.println("答案是"+(a+b)+"");}
在student中有个swap方法,可以使用P1.swap(3,5);这样就是p1对象会调用swap方法,所以,这就是操作对象;
  • 14.关键字this,如:this.name表示当前对象的name,所以this.的意思是表示当前对象的…

  • 15.同一个类里面可以存在重名的方法,但形参要不一样,如形参是2个与多个,或者类型不一样。如在student类中,有2个重名的方法:

Void setname(String name){this.name=name;}//俗称方法重载Void setname(char name){this.name=name;}像这样是可以的存在两个同名称函数(方法)根据形参不同,所以来决定使用哪一个
  • 16.构造方法来初始化对象:
 其实我们在类中,创建新对象是这样的;如student类Student p1=new student();其实里面的student(){}是这样的,里面不写任何东西,在类student自带student方法,跟方法相比就是缺了返回类型,我们叫构造方法。我们也可以改一改,让开始初始化如:student(String name,int age){this.name=name;this.age=age;}这些在main方法调用时加形参进去就会自动匹配形参;Student p1=new student(“xiaomi”,23);这样就创建了一个新的对象,名字叫xiaomi,年龄23;也可以两个构造方法一起放入类里面,形参不一样,用的也不一样。
  • 17.代码块:如果类里面有代码块,代码块比构造方法先执行;
  • 18.执行顺序:静态的先执行,再到成员。无论是变量 。代码块,构造方法

静态变量,是属于类的,无论什么时候一旦修改,无论什么对象访问都是跟着修改的,类型前面加了static的就是静态变量,说白了该变量是共享的,无论是谁修改,谁访问都可以,因为它属于类的不是对象的,也就是不是成员变量。类似于c,c在类型前面加了static,会让局部变量变成全局变量。使用事项:平时我们都是操控对象,如p1.什么什么,现在也可以继续用这样的方法,如我student类有个全局变量a,我可以使用student 的对象之一p1,进行访问和修改,但不标准,p1.a=6; p2.a=9;最终a的值是变成了9,无论哪一个对象都能访问修改,但是我们通常用类名来控制,而不是对象控制,如student.a=10;student.a=100;student是类名。静态方法也是如此,一般用类名去控制,而不是对象,方法类型前面多个static,变成静态方法静态方法里面的变量只能是静态变量,而不是用对象的成员变量,也不能用成员方法。所以大致分两种,一种控制对象,一种控制类,静态方法也不能用this关键字,因为他不是对象,不能指代哪一个对象。
  • 19.包的作用

  • 包;为了避免类太多,不好找也不好管理,所以可以新建软件包把类分类,每个包里面的文件都要加上包名,package 包名,用不同一级的目录的类,也就是不同包的类,要进行导入

导入:impor 包名.类名 如果要导入包里面的全部类,应该是:impor 包名.*

如果包1,有sum这个类,包2也有sum这个类,使用sum是要说明使用哪一个包的sum,就是在前面类前面加上包名.

  • 20.访问权限:不同包之前的类是有访问权限的:

Java有四种访问权限, 其中三种有访问权限修饰符,分别为private,public和protected,还有一种不带任何修饰符。

1. private:     Java语言中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的类、属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。
2. default:即不加任何访问修饰符,通常称为“默认访问模式“。该模式下,只允许在同一个包中进行访问。
3. protect:     介于public     和     private 之间的一种访问修饰符,一般称之为“保护形”。被其修饰的类、属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。
4. public: Java语言中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包(package)访问。

下面用表格的形式来展示四种访问权限之间的异同点,这样会更加形象。表格如下所示:

同一个类同一个包不同包的子类不同包的非子类
Private
Default(默认,不写)
Protected
Public(公用)

Java三大特性:继承,封装,多态

  • 封装

    封装:将对象的属性和方法结合成一个整体,只有通过该整体,否则无法改变和访问对象的某些方法或者属性,该整体其实也是自定义方法,调用其他方法和使用private权限(除了同类的方法可以访问或者修改,其他包或者main类或者其他类没有权限)

    Private私有属性,除了类自己,其他的无法访问和修改

    自己自定义方法进行访问和修改;\

    private String brand;private String model;private double size;private double price;private String config;public  int inventory;//库存public  double total_price;//总价public String getBrand() {return brand;}
    
  • 继承

继承:像人类,人类又分为学生,教师,工人等不同种类。像这样的,人类我们通常叫做父类,学生,教师,工人我们叫做子类;

  • 定义子类之前我们要先定义父类,定义子类,在子类名后面加上extents+父类名;

  • 类是可以一直往下走的,除非某个类加上了final这个关键字;

  • 对于父类是private的属性继承,子类是继承的,但是拿不到。So没办法

  • 如果父类有构造方法,子类也必须要有构造方法,并且构造方法里面首先要调用super关键字,super.在这里的意思是父类的,this.在这里的意思是这个对象的,或者说这个类的。

  • student类(子类)
    package textstudy;
    public class student extends people{String master;student(String master, int age, String name, int id){super(id,name,age);this.master = master;}
    }
    people类(父类)
    package textstudy;public class people {private int id;private String name;private int age;people(int id, String name, int age){this.id = id;this.name = name;this.age = age;}int getId(){return id;}String getName(){return name;}int getAge(){return age;}
    }
    
  • 关键字:instanceof:使用方法,对象名+instanceof+类名,通常在if括号里面使用,如果对象属于该类,会返回ture,否则会返回false。该类名可以是父类,就是大范围判断也行,具体到某一类也行:

父类+对象名=new 父类();这对象是是属于父类的:

父类+对象名=new 子类();这对象是属于子类的,因为new XXX();是啥就是属于啥类;也就是说,引用对象是子类,即使前面是写父类他也是属于子类:

最顶层的类是object,是所有类的祖宗,main类也是它的子类,所以java本身了就继承了一些方法,如:object类下面是people类,在下面是student类和teacher类;所以有一些关键方法:

如equals,:对象1equals对象2,如果两个对象属性相同,返回ture,否则false;

继承,也就是说子承父业,父类所拥有的方法,子类能拥有,子类还能加一些特殊的方法进去,父类是用不了的。

子类和父类均有同名的变量的时候,优先会考虑使用同类的变量,如果子类想使用父类的同名变量,在使用变量前加上super.这个关键字。

如果是不同名字的变量,直接用就行。

  • 方法重载

方法重载:同类下,定义同名的方法,但是形参数目或者是类型不一样(必要条件),并且返回类型可以是一样或者不一样也行。要用哪个方法,关键看对应的形参的对应方法。
  • 方法重写

方法重写:不同类下,使用方法名字,形参一模一样的方法,但是方法体不一样。在该类下,使用该名称下的方法体,如果要使用父类的,请加上super.。

注意事项:

-工具类:

Stream流:

这个是jdk8的方法,主要是通过一些设定条件,使得集合类的对象删减某些元素

方法:

  • sorted()排序,默认从小到大,可以在括号里面写个lambda表达式变成从大到小
  • distinct():删除重复的元素
  • filter(收集条件)
  • collect()前三个相当于摄制一部机器,这里才会执行,启动机器。开始收集。如我设定了要删除重复元素,这里才会返回删除后的新集合

public class Text {public static void main(String[] args) {ArrayList<Integer> list=new ArrayList<>();list.add(7);list.add(6);list.add(9);list.add(76);list.add(88);list.add(35);list.add(7);list=list.stream().distinct().collect(Collectors.toCollection(ArrayList::new));System.out.println(list.toString());}
}输出7 6 9 76 88 35重复的7删除了

image-20230315134952237

  • io流

​ JDK提供了一套用于IO操作的框架,为了方便我们开发者使用,就定义了一个像水流一样,根据流的传输方向和读取单位,分为字节流InputStream和OutputStream以及字符流Reader和Writer的IO框架,当然,这里的Stream并不是前面集合框架认识的Stream,这里的流指的是数据流,通过流,我们就可以一直从流中读取数据,直到读取到尽头,(缓冲流一个道理)或是不断向其中写入数据,直到我们写入完成,而这类IO就是我们所说的BIO,

字节流一次读取一个字节,也就是一个byte的大小,而字符流顾名思义,就是一次读取一个字符,也就是一个char的大小

(在读取纯文本文件的时候更加适合),有关这两种流,会在后面详细介绍,这个章节我们需要学习16个关键的流。

  • 文件流

  •  public static void main(String[] args) {    try {   //注意,IO相关操作会有很多影响因素,有可能出现异常,所以需要明确进行处理        FileInputStream inputStream = new FileInputStream("路径");        //路径支持相对路径和绝对路径    } catch (FileNotFoundException e) {        e.printStackTrace();    }}
    

在使用完成一个流之后,必须关闭这个流来完成对资源的释放,否则资源会被一直占用:

public static void main(String[] args) {FileInputStream inputStream = null;    //定义可以先放在try外部try {inputStream = new FileInputStream("路径");} catch (FileNotFoundException e) {e.printStackTrace();} finally {try {    //建议在finally中进行,因为关闭流是任何情况都必须要执行的!if(inputStream != null) inputStream.close();} catch (IOException e) {e.printStackTrace();}}
}

虽然这样的写法才是最保险的,但是显得过于繁琐了,尤其是finally中再次嵌套了一个try-catch块,因此在JDK1.7新增了try-with-resource语法,用于简化这样的写法(本质上还是和这样的操作一致,只是换了个写法)

public static void main(String[] args) {//注意,这种语法只支持实现了AutoCloseable接口的类!try(FileInputStream inputStream = new FileInputStream("路径")) {   //直接在try()中定义要在完成之后释放的资源} catch (IOException e) {   //这里变成IOException是因为调用close()可能会出现,而FileNotFoundException是继承自IOException的e.printStackTrace();}//无需再编写finally语句块,因为在最后自动帮我们调用了close()
}

反编译是一样的:通常使用上面这个,因为可以自动关闭文件,防止文件被占用导致其他对象读取写入失败:而且尽量使用绝对路径,也就是完全路径,用 \ \或者/隔开每个目录,因为\表示转义字符

  • 文件字节输入流:

  • 现在我们拿到了文件的输入流,那么怎么才能读取文件里面的内容呢?我们可以使用read方法:

    public static void main(String[] args) {//test.txt:atry(FileInputStream inputStream = new FileInputStream("test.txt")) {//使用read()方法进行字符读取System.out.println((char) inputStream.read());  //读取一个字节的数据(英文字母只占1字节,中文占2字节)System.out.println(inputStream.read());   //唯一一个字节的内容已经读完了,再次读取返回-1表示没有内容了}catch (IOException e){e.printStackTrace();}
    }
    

    使用read可以直接读取一个字节的数据,注意,流的内容是有限的,读取一个少一个。我们如果想一次性全部读取的话,可以直接使用一个while循环来完成:

    public static void main(String[] args) {//test.txt:abcdtry(FileInputStream inputStream = new FileInputStream("test.txt")) {int tmp;while ((tmp = inputStream.read()) != -1){   //通过while循环来一次性读完内容System.out.println((char)tmp);}}catch (IOException e){e.printStackTrace();}
    }
    

    使用available方法能查看当前可读的剩余字节数量(注意:并不一定真实的数据量就是这么多,尤其是在网络I/O操作时,这个方法只能进行一个预估也可以说是暂时能一次性可以读取的数量,当然在磁盘IO下,一般情况都是真实的数据量)

    try(FileInputStream inputStream = new FileInputStream("test.txt")) {System.out.println(inputStream.available());  //查看剩余数量
    }catch (IOException e){e.printStackTrace();
    }
    

    当然,一个一个读取效率太低了,那能否一次性全部读取呢?我们可以预置一个合适容量的byte[]数组来存放:

    public static void main(String[] args) {//test.txt:abcdtry(FileInputStream inputStream = new FileInputStream("test.txt")) {byte[] bytes = new byte[inputStream.available()];   //我们可以提前准备好合适容量的byte数组来存放System.out.println(inputStream.read(bytes));   //一次性读取全部内容(返回值是读取的字节数)System.out.println(new String(bytes));   //通过String(byte[])构造方法得到字符串}catch (IOException e){e.printStackTrace();}
    }
    

    也可以控制要读取数量:

    System.out.println(inputStream.read(bytes, 1, 2));   //第二个参数是从给定数组的哪个位置开始放入内容,第三个参数是读取流中的字节数
    

    注意:一次性读取同单个读取一样,当没有任何数据可读时,依然会返回-1

    通过skip()方法可以跳过指定数量的字节:

    public static void main(String[] args) {//test.txt:abcdtry(FileInputStream inputStream = new FileInputStream("test.txt")) {System.out.println(inputStream.skip(1));System.out.println((char) inputStream.read());   //跳过了一个字节}catch (IOException e){e.printStackTrace();}
    }
    

    注意:FileInputStream是不支持reset()的,虽然有这个方法,但是这里先不提及。

    既然有输入流,那么文件输出流也是必不可少的:

    public static void main(String[] args) {//输出流也需要在最后调用close()方法,并且同样支持try-with-resourcetry(FileOutputStream outputStream = new FileOutputStream("output.txt")) {//注意:若此文件不存在,会直接创建这个文件!}catch (IOException e){e.printStackTrace();}
    }
    

    输出流没有read()操作而是write()操作,使用方法同输入流一样,只不过现在的方向变为我们向文件里写入内容:

    public static void main(String[] args) {try(FileOutputStream outputStream = new FileOutputStream("output.txt")) {outputStream.write('c');   //同read一样,可以直接写入内容outputStream.write("lbwnb".getBytes());   //也可以直接写入byte[]outputStream.write("lbwnb".getBytes(), 0, 1);  //同上输入流outputStream.flush();  //建议在最后执行一次刷新操作(强制写入)来保证数据正确写入到硬盘文件中}catch (IOException e){e.printStackTrace();}
    }
    

    那么如果是我只想在文件尾部进行追加写入数据呢?我们可以调用另一个构造方法来实现:

    public static void main(String[] args) {try(FileOutputStream outputStream = new FileOutputStream("output.txt", true)) {  //true表示开启追加模式outputStream.write("lb".getBytes());   //现在只会进行追加写入,而不是直接替换原文件内容outputStream.flush();}catch (IOException e){e.printStackTrace();}
    }
    

    利用输入流和输出流,就可以轻松实现文件的拷贝了:

    public static void main(String[] args) {try(FileOutputStream outputStream = new FileOutputStream("output.txt");FileInputStream inputStream = new FileInputStream("test.txt")) {   //可以写入多个byte[] bytes = new byte[10];    //使用长度为10的byte[]做传输媒介int tmp;   //存储本地读取字节数while ((tmp = inputStream.read(bytes)) != -1){   //直到读取完成为止outputStream.write(bytes, 0, tmp);    //写入对应长度的数据到输出流}}catch (IOException e){e.printStackTrace();}
    }
    

    例如视频复制:

    package Textonlin;import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.*;
    import java.util.function.Consumer;
    import java.util.stream.Collectors;public class Text
    {public static void main(String[] args) throws IOException{try(FileInputStream f1=new FileInputStream("L:\\源文件\\java\\1.mov")){FileOutputStream f2=new FileOutputStream("L:\\源文件\\java\\3.mov");int temp;byte []b=new byte[10240];while((temp = f1.read(b)) != -1){f2.write(b, 0, temp);}}catch (IOException e){//为什么e要在这里创建,下面即是答案:因为出现异常才会创建ee.printStackTrace();}}
    }Anwser:在第二个例子中,代码正确地使用了try-catch块来捕获IOException异常,并将该异常对象赋值给变量e。这使得程序能够正确地捕获并处理异常,以避免程序崩溃或产生未处理的异常。
    因此,第二个例子是更好的代码实现。
    在第二个式子中,如果没有异常,e会创建吗?
    在第二个示例代码中,如果没有异常抛出,那么 IOException e 变量实际上不会被创建。这是因为变量只会在 catch 块中初始化并赋值,只有当 try 块中的代码抛出了对应的异常才会执行 catch 块。
    如果 try 块中的代码没有抛出异常,那么程序将会顺序执行下去,直到 try-catch 结构之外。在这种情况下,变量 IOException e 不会被创建或初始化。
    

    文件字符流

    字符流不同于字节,字符流是以一个具体的字符进行读取,因此它只适合读纯文本的文件,如果是其他类型的文件不适用:

    public static void main(String[] args) {try(FileReader reader = new FileReader("test.txt")){reader.skip(1);   //现在跳过的是一个字符System.out.println((char) reader.read());   //现在是按字符进行读取,而不是字节,因此可以直接读取到中文字符}catch (IOException e){e.printStackTrace();}
    }
    

    同理,字符流只支持char[]类型作为存储:

    public static void main(String[] args) {try(FileReader reader = new FileReader("test.txt")){char[] str = new char[10];reader.read(str);System.out.println(str);   //直接读取到char[]中}catch (IOException e){e.printStackTrace();}
    }
    

    既然有了Reader肯定也有Writer:

    public static void main(String[] args) {try(FileWriter writer = new FileWriter("output.txt")){writer.getEncoding();   //支持获取编码(不同的文本文件可能会有不同的编码类型)writer.write('牛');writer.append('牛');   //其实功能和write一样writer.flush();   //刷新}catch (IOException e){e.printStackTrace();}
    }
    

    我们发现不仅有write()方法,还有一个append()方法,但是实际上他们效果是一样的,看源码:

    public Writer append(char c) throws IOException {write(c);return this;
    }
    

    append支持像StringBuilder那样的链式调用,返回的是Writer对象本身。

文件流:

File file =new File("路径");

这个类的方法:

  • exists()判断文件是否存在
  • getasoluepath()获取文件的相对路径
  • creatnewfile()创建新的文件
  • file.mkdir()新建文件夹(和Linux系统一样)

缓冲流:

  • 缓冲流就相当于比以前多了两个东西,就像水供应给用户,经过自来水厂净化。自来水厂就相当于一个缓冲流

  • 缓冲流入区:提前将文件读取一部分进去缓冲区,要用的时候,可以快速读取,读取数据多于缓冲区的情况下,再从文件输入流中读取。缓冲(Buffered)

要创建一个缓冲字节流,只需要将原本的流作为构造参数传入BufferedInputStream即可:

public static void main(String[] args) {try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("test.txt"))){   //传入FileInputStreamSystem.out.println((char) bufferedInputStream.read());   //操作和原来的流是一样的}catch (IOException e){e.printStackTrace();}
}

实际上进行I/O操作的并不是BufferedInputStream,而是我们传入的FileInputStream,而BufferedInputStream虽然有着同样的方法,但是进行了一些额外的处理然后再调用FileInputStream的同名方法,这样的写法称为装饰者模式,我们会在设计模式篇中详细介绍。我们可以来观察一下它的close方法源码:

public void close() throws IOException {byte[] buffer;while ( (buffer = buf) != null) {if (bufUpdater.compareAndSet(this, buffer, null)) {  //CAS无锁算法,并发会用到,暂时不需要了解InputStream input = in;in = null;if (input != null)input.close();return;}// Else retry in case a new buf was CASed in fill()}
}

实际上这种模式是父类FilterInputStream提供的规范,后面我们还会讲到更多FilterInputStream的子类。

我们可以发现在BufferedInputStream中还存在一个专门用于缓存的数组:

/*** The internal buffer array where the data is stored. When necessary,* it may be replaced by another array of* a different size.*/
protected volatile byte buf[];

I/O操作一般不能重复读取内容(比如键盘发送的信号,主机接收了就没了),而缓冲流提供了缓冲机制,一部分内容可以被暂时保存,BufferedInputStream支持reset()mark()操作,首先我们来看看mark()方法的介绍:

/*** Marks the current position in this input stream. A subsequent* call to the <code>reset</code> method repositions this stream at* the last marked position so that subsequent reads re-read the same bytes.* <p>* The <code>readlimit</code> argument tells this input stream to* allow that many bytes to be read before the mark position gets* invalidated.* <p>* This method simply performs <code>in.mark(readlimit)</code>.** @param   readlimit   the maximum limit of bytes that can be read before*                      the mark position becomes invalid.* @see     java.io.FilterInputStream#in* @see     java.io.FilterInputStream#reset()*/
public synchronized void mark(int readlimit) {in.mark(readlimit);
}

当调用mark()之后,输入流会以某种方式保留之后读取的readlimit数量的内容,当读取的内容数量超过readlimit则之后的内容不会被保留,当调用reset()之后,会使得当前的读取位置回到mark()调用时的位置。

public static void main(String[] args) {try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("test.txt"))){bufferedInputStream.mark(1);   //只保留之后的1个字符System.out.println((char) bufferedInputStream.read());System.out.println((char) bufferedInputStream.read());bufferedInputStream.reset();   //回到mark时的位置System.out.println((char) bufferedInputStream.read());System.out.println((char) bufferedInputStream.read());}catch (IOException e) {e.printStackTrace();}
}

我们发现虽然后面的部分没有保存,但是依然能够正常读取,其实mark()后保存的读取内容是取readlimit和BufferedInputStream类的缓冲区大小两者中的最大值,而并非完全由readlimit确定。因此我们限制一下缓冲区大小,再来观察一下结果:

public static void main(String[] args) {try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("test.txt"), 1)){  //将缓冲区大小设置为1bufferedInputStream.mark(1);   //只保留之后的1个字符System.out.println((char) bufferedInputStream.read());System.out.println((char) bufferedInputStream.read());   //已经超过了readlimit,继续读取会导致mark失效bufferedInputStream.reset();   //mark已经失效,无法reset()System.out.println((char) bufferedInputStream.read());System.out.println((char) bufferedInputStream.read());}catch (IOException e) {e.printStackTrace();}
}

了解完了BufferedInputStream之后,我们再来看看BufferedOutputStream,其实和BufferedInputStream原理差不多,只是反向操作:

public static void main(String[] args) {try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream("output.txt"))){outputStream.write("lbwnb".getBytes());outputStream.flush();}catch (IOException e) {e.printStackTrace();}
}

操作和FileOutputStream一致,这里就不多做介绍了。

既然有缓冲字节流,那么肯定也有缓冲字符流,缓冲字符流和缓冲字节流一样,也有一个专门的缓冲区,BufferedReader构造时需要传入一个Reader对象:

public static void main(String[] args) {try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))){System.out.println((char) reader.read());}catch (IOException e) {e.printStackTrace();}
}

使用和reader也是一样的,内部也包含一个缓存数组:

private char cb[];

相比Reader更方便的是,它支持按行读取:

public static void main(String[] args) {try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))){System.out.println(reader.readLine());   //按行读取}catch (IOException e) {e.printStackTrace();}
}

读取后直接得到一个字符串,当然,它还能把每一行内容依次转换为集合类提到的Stream流:

public static void main(String[] args) {try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))){reader.lines().limit(2).distinct().sorted().forEach(System.out::println);}catch (IOException e) {e.printStackTrace();}
}

它同样也支持mark()reset()操作:

public static void main(String[] args) {try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))){reader.mark(1);System.out.println((char) reader.read());reader.reset();System.out.println((char) reader.read());}catch (IOException e) {e.printStackTrace();}
}

BufferedReader处理纯文本文件时就更加方便了,BufferedWriter在处理时也同样方便:

public static void main(String[] args) {try (BufferedWriter reader = new BufferedWriter(new FileWriter("output.txt"))){reader.newLine();   //使用newLine进行换行reader.write("汉堡做滴彳亍不彳亍");   //可以直接写入一个字符串reader.flush();   //清空缓冲区}catch (IOException e) {e.printStackTrace();}
}

合理使用缓冲流,可以大大提高我们程序的运行效率.

转换流

  • 转换流:可以将文件字节流和文件字符流进行相互转换。也是用类似缓冲流那样包装定义,也就是装饰者模式

例如使用场景:

有时会遇到这样一个很麻烦的问题:我这里读取的是一个字符串或是一个个字符,但是我只能往一个OutputStream里输出,但是OutputStream又只支持byte类型,如果要往里面写入内容,进行数据转换就会很麻烦,那么能否有更加简便的方式来做这样的事情呢?

public static void main(String[] args) {try(OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("test.txt"))){  //虽然给定的是FileOutputStream,但是现在支持以Writer的方式进行写入writer.write("lbwnb");   //以操作Writer的样子写入OutputStream}catch (IOException e){e.printStackTrace();}
}

同样的,我们现在只拿到了一个InputStream,但是我们希望能够按字符的方式读取,我们就可以使用InputStreamReader来帮助我们实现:

public static void main(String[] args) {try(InputStreamReader reader = new InputStreamReader(new FileInputStream("test.txt"))){  //虽然给定的是FileInputStream,但是现在支持以Reader的方式进行读取System.out.println((char) reader.read());}catch (IOException e){e.printStackTrace();}
}

InputStreamReader和OutputStreamWriter本质也是Reader和Writer,因此可以直接放入BufferedReader来实现更加方便的操作。

  • 打印流

  • 我们一直都在使用打印流,如Syste.out这就是打印流的一种,只是这个是打印在控制台,我们还可以打印在文本中,也可以用这种方式进行输出;PrintStream:

  • 使用方法和缓冲流,都是一样的,将文本字节/字符输出/入流进行包装,也是装饰者模式

  • 打印到文本中是out.put方法;

  • 打印流其实我们从一开始就在使用了,比如System.out就是一个PrintStream,PrintStream也继承自FilterOutputStream类因此依然是装饰我们传入的输出流,但是它存在自动刷新机制,例如当向PrintStream流中写入一个字节数组后自动调用flush()方法。PrintStream也永远不会抛出异常,而是使用内部检查机制checkError()方法进行错误检查。最方便的是,它能够格式化任意的类型,将它们以字符串的形式写入到输出流。

    public final static PrintStream out = null;
    

    可以看到System.out也是PrintStream,不过默认是向控制台打印,我们也可以让它向文件中打印:

    public static void main(String[] args) {try(PrintStream stream = new PrintStream(new FileOutputStream("test.txt"))){stream.println("lbwnb");   //其实System.out就是一个PrintStream}catch (IOException e){e.printStackTrace();}
    }
    

    我们平时使用的println方法就是PrintStream中的方法,它会直接打印基本数据类型或是调用对象的toString()方法得到一个字符串,并将字符串转换为字符,放入缓冲区再经过转换流输出到给定的输出流上。

    img

    因此实际上内部还包含这两个内容:

    /*** Track both the text- and character-output streams, so that their buffers* can be flushed without flushing the entire stream.*/
    private BufferedWriter textOut;
    private OutputStreamWriter charOut;
    

    与此相同的还有一个PrintWriter,不过他们的功能基本一致,PrintWriter的构造方法可以接受一个Writer作为参数,这里就不再做过多阐述了。

    而我们之前使用的Scanner,使用的是系统提供的输入流:

    public static void main(String[] args) {Scanner scanner = new Scanner(System.in);   //系统输入流,默认是接收控制台输入
    }
    

    我们也可以使用Scanner来扫描其他的输入流:

    public static void main(String[] args) throws FileNotFoundException {Scanner scanner = new Scanner(new FileInputStream("秘制小汉堡.txt"));  //将文件内容作为输入流进行扫描
    }
    

    相当于直接扫描文件中编写的内容,同样可以读取。

  • 对象流

  • 1·也是包装,装饰者模式

  • 2·想使用对象流,传入的对象必须是要实现了序列化接口才可以。

  • 既然基本数据类型能够读取和写入基本数据类型,那么能否将对象也支持呢?ObjectOutputStream不仅支持基本数据类型,通过对对象的序列化操作,以某种格式保存对象,来支持对象类型的IO,注意:它不是继承自FilterInputStream的。

    public static void main(String[] args) {try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("output.txt"));ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("output.txt"))){People people = new People("lbw");outputStream.writeObject(people);outputStream.flush();people = (People) inputStream.readObject();System.out.println(people.name);}catch (IOException | ClassNotFoundException e) {e.printStackTrace();}
    }static class People implements Serializable{   //必须实现Serializable接口才能被序列化String name;public People(String name){this.name = name;}
    }
    

    在我们后续的操作中,有可能会使得这个类的一些结构发生变化,而原来保存的数据只适用于之前版本的这个类,因此我们需要一种方法来区分类的不同版本:

    static class People implements Serializable{private static final long serialVersionUID = 123456;   //在序列化时,会被自动添加这个属性,它代表当前类的版本,我们也可以手动指定版本。String name;public People(String name){this.name = name;}
    }
    

    当发生版本不匹配时,会无法反序列化为对象:

    java.io.InvalidClassException: com.test.Main$People; local class incompatible: stream classdesc serialVersionUID = 123456, local class serialVersionUID = 1234567at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2003)at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1850)at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2160)at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1667)at java.io.ObjectInputStream.readObject(ObjectInputStream.java:503)at java.io.ObjectInputStream.readObject(ObjectInputStream.java:461)at com.test.Main.main(Main.java:27)
    

    如果我们不希望某些属性参与到序列化中进行保存,我们可以添加transient关键字:

    public static void main(String[] args) {try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("output.txt"));ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("output.txt"))){People people = new People("lbw");outputStream.writeObject(people);outputStream.flush();people = (People) inputStream.readObject();System.out.println(people.name);  //虽然能得到对象,但是name属性并没有保存,因此为null}catch (IOException | ClassNotFoundException e) {e.printStackTrace();}
    }static class People implements Serializable{private static final long serialVersionUID = 1234567;transient String name;public People(String name){this.name = name;}
    }
    

    其实我们可以看到,在一些JDK内部的源码中,也存在大量的transient关键字,使得某些属性不参与序列化,取消这些不必要保存的属性,可以节省数据空间占用以及减少序列化时间。

    • 对象流作用,通俗点理解类似于加密,(以一种不知道啥格式进行数据的读取)写上去的文件类似于乱码那样,看不懂啥格式,自己去读取,输入输出才能得到正确的数据,对象流也是一共有两个,输入和输出

总而言之:除了文件字符/节输出/入流,其他流都是文件字符/节输入输出流的包装,在此功能上新加了某些方法,俗称装饰者模式

  • 多线程:

  • Runnable r1=new Runnable() {@Overridepublic void run() {System.out.println("我是小李");}};//要new一个接口并实例化。等会t1线程传入告诉它要干什么Thread t1=new Thread(r1);//要传入运行内容t1.start();//t1线程开始运行
    

    因为接口是只有一个方法带实现,所以可以用lambda表达式,

      Thread t1=new Thread(()->{System.out.println("我是小李");});t1.start();
    

因为thread方法形参默认传入Runnable接口了

 public Thread(Runnable task) {this(null, null, 0, task, 0, null);}所以可以用lambda表达式
  • 主线程与子线程同时运行,没有谁是先结束再开始,也就是谁先做谁后做
  • 类的方法:
    • sleep(时间,毫秒为单位);
    • interrupt();取消中断,就是当对象下达非强制中断命令后,这个类的命令可以让线程取消终止;
  • 对象的方法:
    • start();开始执行线程,线程启动
    • run:当前面的线程跑完再启动本线程
    • interrupt();缓慢的终止线程,不是强制终止,像电脑在桌面按关机键关机,强制终止像电脑按住开机键关机
    • stop():强制终止

实际上,线程和进程差不多,也会等待获取CPU资源,一旦获取到,就开始按顺序执行我们给定的程序,当需要等待外部IO操作(比如Scanner获取输入的文本),就会暂时处于休眠状态,等待通知,或是调用sleep()方法来让当前线程休眠一段时间:

public static void main(String[] args) throws InterruptedException {System.out.println("l");Thread.sleep(1000);    //休眠时间,以毫秒为单位,1000ms = 1sSystem.out.println("b");Thread.sleep(1000);System.out.println("w");Thread.sleep(1000);System.out.println("nb!");
}

我们也可以使用stop()方法来强行终止此线程:

public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {Thread me = Thread.currentThread();   //获取当前线程对象for (int i = 0; i < 50; i++) {System.out.println("打印:"+i);if(i == 20) me.stop();  //此方法会直接终止此线程}});t.start();
}

线程的休眠和中断

我们前面提到,一个线程处于运行状态下,线程的下一个状态会出现以下情况:

  • 当CPU给予的运行时间结束时,会从运行状态回到就绪(可运行)状态,等待下一次获得CPU资源。
  • 当线程进入休眠 / 阻塞(如等待IO请求) / 手动调用wait()方法时,会使得线程处于等待状态,当等待状态结束后会回到就绪状态。
  • 当线程出现异常或错误 / 被stop() 方法强行停止 / 所有代码执行结束时,会使得线程的运行终止。

我们发现,每一个Thread对象中,都有一个interrupt()方法,调用此方法后,会给指定线程添加一个中断标记以告知线程需要立即停止运行或是进行其他操作,由线程来响应此中断并进行相应的处理,我们前面提到的stop()方法是强制终止线程,这样的做法虽然简单粗暴,但是很有可能导致资源不能完全释放,而类似这样的发送通知来告知线程需要中断,让线程自行处理后续,会更加合理一些,也是更加推荐的做法。我们来看看interrupt的用法:

public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("线程开始运行!");while (true){   //无限循环if(Thread.currentThread().isInterrupted()){   //判断是否存在中断标志break;   //响应中断}}System.out.println("线程被中断了!");});t.start();try {Thread.sleep(3000);   //休眠3秒,一定比线程t先醒来t.interrupt();   //调用t的interrupt方法} catch (InterruptedException e) {e.printStackTrace();}
}

通过isInterrupted()可以判断线程是否存在中断标志,如果存在,说明外部希望当前线程立即停止,也有可能是给当前线程发送一个其他的信号,如果我们并不是希望收到中断信号就是结束程序,而是通知程序做其他事情,我们可以在收到中断信号后,复位中断标记,然后继续做我们的事情:

public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("线程开始运行!");while (true){if(Thread.currentThread().isInterrupted()){   //判断是否存在中断标志System.out.println("发现中断信号,复位,继续运行...");Thread.interrupted();  //复位中断标记(返回值是当前是否有中断标记,这里不用管)}}});t.start();try {Thread.sleep(3000);   //休眠3秒,一定比线程t先醒来t.interrupt();   //调用t的interrupt方法} catch (InterruptedException e) {e.printStackTrace();}
}

复位中断标记后,会立即清除中断标记。那么,如果现在我们想暂停线程呢?我们希望线程暂时停下,比如等待其他线程执行完成后,再继续运行,那这样的操作怎么实现呢?

public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("线程开始运行!");Thread.currentThread().suspend();   //暂停此线程//不推荐使用System.out.println("线程继续运行!");});t.start();try {Thread.sleep(3000);   t.resume();   //恢复此线程//不推荐使用} catch (InterruptedException e) {e.printStackTrace();}
}

虽然这样很方便地控制了线程的暂停状态,但是这两个方法我们发现实际上也是不推荐的做法,它很容易导致死锁!

  • 线程的优先级

    实际上,Java程序中的每个线程并不是平均分配CPU时间的,为了使得线程资源分配更加合理,Java采用的是抢占式调度方式,优先级越高的线程,优先使用CPU资源!我们希望CPU花费更多的时间去处理更重要的任务,而不太重要的任务,则可以先让出一部分资源。线程的优先级一般分为以下三种:

MIN_PRIORITY 最低优先级
MAX_PRIORITY 最高优先级
NOM_PRIORITY 常规优先级
public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("线程开始运行!");});t.start();t.setPriority(Thread.MIN_PRIORITY);  //通过使用setPriority方法来设定优先级
}
  • 进程让位与加入

    • 让位操作:(进程让位几十毫秒,而并不是让哪个进程先执行完,也只是让一下cpu资源,让某个进程快一点而已,但是两个或者多个进程都是执行中的) 方法是对象.yield();

    • 加入操作:(进程让某个进程先执行,优先执行完先,再执行其他线程):要被提前执行的对象.join();提前执行的对象执行完先,再执行其他的进程

    • 加入:当我们希望一个线程等待另一个线程执行完成后再继续进行,我们可以使用join()方法来实现线程的加入:

      public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("线程1开始运行!");for (int i = 0; i < 50; i++) {System.out.println("1打印:"+i);}System.out.println("线程1结束!");});Thread t2 = new Thread(() -> {System.out.println("线程2开始运行!");for (int i = 0; i < 50; i++) {System.out.println("2打印:"+i);if(i == 10){try {System.out.println("线程1加入到此线程!");t1.join();    //在i==10时,让线程1加入,先完成线程1的内容,在继续当前内容} catch (InterruptedException e) {e.printStackTrace();}}}});t1.start();t2.start();
      }
      

      我们发现,线程1加入后,线程2等待线程1待执行的内容全部执行完成之后,再继续执行的线程2内容。注意,线程的加入只是等待另一个线程的完成,并不是将另一个线程和当前线程合并!我们来看看:

      public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println(Thread.currentThread().getName()+"开始运行!");for (int i = 0; i < 50; i++) {System.out.println(Thread.currentThread().getName()+"打印:"+i);}System.out.println("线程1结束!");});Thread t2 = new Thread(() -> {System.out.println("线程2开始运行!");for (int i = 0; i < 50; i++) {System.out.println("2打印:"+i);if(i == 10){try {System.out.println("线程1加入到此线程!");t1.join();    //在i==10时,让线程1加入,先完成线程1的内容,在继续当前内容} catch (InterruptedException e) {e.printStackTrace();}}}});t1.start();t2.start();
      }
      

      实际上,t2线程只是暂时处于等待状态,当t1执行结束时,t2才开始继续执行,只是在效果上看起来好像是两个线程合并为一个线程在执行而已。

  • 线程锁与线程同步:

  • 在开始讲解线程同步之前,我们需要先了解一下多线程情况下Java的内存管理:

    image-20221004203914215

    线程之间的共享变量(比如之前悬念中的value变量)存储在主内存(main memory)中,每个线程都有一个私有的工作内存(本地内存),工作内存中存储了该线程以读/写共享变量的副本。它类似于我们在计算机组成原理中学习的多核心处理器高速缓存机制:

    image-20221004204209038

    高速缓存通过保存内存中数据的副本来提供更加快速的数据访问,但是如果多个处理器的运算任务都涉及同一块内存区域,就可能导致各自的高速缓存数据不一致,在写回主内存时就会发生冲突,这就是引入高速缓存引发的新问题,称之为:缓存一致性。

    实际上,Java的内存模型也是这样类似设计的,==当我们同时去操作一个共享变量时,如果仅仅是读取还好,但是如果同时写入内容,就会出现问题!==好比说一个银行,如果我和我的朋友同时在银行取我账户里面的钱,难道取1000还可能吐2000出来吗?我们需要一种更加安全的机制来维持秩序,保证数据的安全性!

  • 解决办法:加一把锁,线程锁

通过synchronized关键字来创造一个线程锁,首先我们来认识一下synchronized代码块,它需要在括号中填入一个内容,必须是一个对象或是一个类,我们在value自增操作外套上同步代码块:

private static int value = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {synchronized (Main.class){  //使用synchronized关键字创建同步代码块value++;}}System.out.println("线程1完成");});Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {synchronized (Main.class){//线程锁中的代码块,属于静态方法,那就要用类,属于对象的方法,那就用对象锁,因为静态方法属于类。如这里的value是静态变量,属于Main类。value++;}}System.out.println("线程2完成");});t1.start();t2.start();Thread.sleep(1000);  //主线程停止1秒,保证两个线程执行完成System.out.println(value);
}
两个线程或者多个线程,内容同步要用同一把对象锁、;
  • 锁的理解:
  • 每一把对象锁相当于是一张vip卡,拿到了才能进入代码块里面执行代码!

  • 同一张vip卡,锁的对象是同一个

死锁:死锁

其实死锁的概念在操作系统中也有提及,它是指两个线程相互持有对方需要的锁,但是又迟迟不释放,导致程序卡住:

image-20221004205058223

我们发现,线程A和线程B都需要对方的锁,但是又被对方牢牢把握,由于线程被无限期地阻塞,因此程序不可能正常终止。我们来看看以下这段代码会得到什么结果:

public static void main(String[] args) throws InterruptedException {Object o1 = new Object();Object o2 = new Object();Thread t1 = new Thread(() -> {synchronized (o1){try {Thread.sleep(1000);synchronized (o2){System.out.println("线程1");}} catch (InterruptedException e) {e.printStackTrace();}}});Thread t2 = new Thread(() -> {synchronized (o2){try {Thread.sleep(1000);synchronized (o1){System.out.println("线程2");}} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();t2.start();
}

所以,我们在编写程序时,一定要注意,不要出现这种死锁的情况。那么我们如何去检测死锁呢?我们可以利用jstack命令来检测死锁,首先利用jps找到我们的java进程:

nagocoler@NagodeMacBook-Pro ~ % jps
51592 Launcher
51690 Jps
14955 
51693 Main
nagocoler@NagodeMacBook-Pro ~ % jstack 51693
...
Java stack information for the threads listed above:
===================================================
"Thread-1":at com.test.Main.lambda$main$1(Main.java:46)- waiting to lock <0x000000076ad27fc0> (a java.lang.Object)- locked <0x000000076ad27fd0> (a java.lang.Object)at com.test.Main$$Lambda$2/1867750575.run(Unknown Source)at java.lang.Thread.run(Thread.java:748)
"Thread-0":at com.test.Main.lambda$main$0(Main.java:34)- waiting to lock <0x000000076ad27fd0> (a java.lang.Object)- locked <0x000000076ad27fc0> (a java.lang.Object)at com.test.Main$$Lambda$1/396873410.run(Unknown Source)at java.lang.Thread.run(Thread.java:748)Found 1 deadlock.

jstack自动帮助我们找到了一个死锁,并打印出了相关线程的栈追踪信息,同样的,使用jconsole也可以进行监测。

因此,前面说不推荐使用 suspend()去挂起线程的原因,是因为suspend()在使线程暂停的同时,并不会去释放任何锁资源。其他线程都无法访问被它占用的锁。直到对应的线程执行resume()方法后,被挂起的线程才能继续,从而其它被阻塞在这个锁的线程才可以继续执行。但是,如果resume()操作出现在suspend()之前执行,那么线程将一直处于挂起状态,同时一直占用锁,这就产生了死锁。


  • wait和notify方法

  • wait:等待,wait的方法只能在锁的代码块中使用。也就是用于同步代码块里面:作用:释放当前的对象锁给另一个进程,自己等待另一个进程给它唤醒,(不唤醒,一直等下去,不会结束进程,哪怕其他进程结束了),而且唤醒后,要等唤醒它的进程执行完,自己才会恢复运行。

  • notify是唤醒线程,notifyall是唤醒多个线程,哪个线程先被唤醒看运气。对象锁和对象的wait是要求同一个对象

其实我们之前可能就发现了,Object类还有三个方法我们从来没有使用过,分别是wait()notify()以及notifyAll(),他们其实是需要配合synchronized来使用的(实际上锁就是依附于对象存在的,每个对象都应该有针对于锁的一些操作,所以说就这样设计了)当然,只有在同步代码块中才能使用这些方法,正常情况下会报错,我们来看看他们的作用是什么:

public static void main(String[] args) throws InterruptedException {Object o1 = new Object();Thread t1 = new Thread(() -> {synchronized (o1){try {System.out.println("开始等待");o1.wait();     //进入等待状态并释放锁System.out.println("等待结束!");} catch (InterruptedException e) {e.printStackTrace();}}});Thread t2 = new Thread(() -> {synchronized (o1){System.out.println("开始唤醒!");o1.notify();     //唤醒处于等待状态的线程for (int i = 0; i < 50; i++) {System.out.println(i);   }//唤醒后依然需要等待这里的锁释放之前等待的线程才能继续}});t1.start();Thread.sleep(1000);t2.start();
}

我们可以发现,对象的wait()方法会暂时使得此线程进入等待状态,同时会释放当前代码块持有的锁,这时其他线程可以获取到此对象的锁,当其他线程调用对象的notify()方法后,会唤醒刚才变成等待状态的线程(这时并没有立即释放锁)。注意,必须是在持有锁(同步代码块内部)的情况下使用,否则会抛出异常!

notifyAll其实和notify一样,也是用于唤醒,但是前者是唤醒所有调用wait()后处于等待的线程,而后者是看运气随机选择一个。

ThreadLocal的使用

既然每个线程都有一个自己的工作内存,那么能否只在自己的工作内存中创建变量仅供线程自己使用呢?

img

我们可以使用ThreadLocal类,来创建工作内存中的变量,它将我们的变量值存储在内部(只能存储一个变量),不同的线程访问到ThreadLocal对象时,都只能获取到当前线程所属的变量。

public static void main(String[] args) throws InterruptedException {ThreadLocal<String> local = new ThreadLocal<>();  //注意这是一个泛型类,存储类型为我们要存放的变量类型Thread t1 = new Thread(() -> {local.set("lbwnb");   //将变量的值给予ThreadLocalSystem.out.println("变量值已设定!");System.out.println(local.get());   //尝试获取ThreadLocal中存放的变量});Thread t2 = new Thread(() -> {System.out.println(local.get());   //尝试获取ThreadLocal中存放的变量});t1.start();Thread.sleep(3000);    //间隔三秒t2.start();
}

上面的例子中,我们开启两个线程分别去访问ThreadLocal对象,我们发现,第一个线程存放的内容,第一个线程可以获取,但是第二个线程无法获取,我们再来看看第一个线程存入后,第二个线程也存放,是否会覆盖第一个线程存放的内容:

public static void main(String[] args) throws InterruptedException {ThreadLocal<String> local = new ThreadLocal<>();  //注意这是一个泛型类,存储类型为我们要存放的变量类型Thread t1 = new Thread(() -> {local.set("lbwnb");   //将变量的值给予ThreadLocalSystem.out.println("线程1变量值已设定!");try {Thread.sleep(2000);    //间隔2秒} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1读取变量值:");System.out.println(local.get());   //尝试获取ThreadLocal中存放的变量});Thread t2 = new Thread(() -> {local.set("yyds");   //将变量的值给予ThreadLocalSystem.out.println("线程2变量值已设定!");});t1.start();Thread.sleep(1000);    //间隔1秒t2.start();
}

我们发现,即使线程2重新设定了值,也没有影响到线程1存放的值,所以说,不同线程向ThreadLocal存放数据,只会存放在线程自己的工作空间中,而不会直接存放到主内存中,因此各个线程直接存放的内容互不干扰。

我们发现在线程中创建的子线程,无法获得父线程工作内存中的变量:

public static void main(String[] args) {ThreadLocal<String> local = new ThreadLocal<>();Thread t = new Thread(() -> {local.set("lbwnb");new Thread(() -> {System.out.println(local.get());}).start();});t.start();
}

我们可以使用InheritableThreadLocal来解决:

public static void main(String[] args) {ThreadLocal<String> local = new InheritableThreadLocal<>();Thread t = new Thread(() -> {local.set("lbwnb");new Thread(() -> {System.out.println(local.get());}).start();});t.start();
}

在InheritableThreadLocal存放的内容,会自动向子线程传递。

个人总结:

  • threadlocal是用于每个线程的数据存放,线程一使用了,设定了值,其他线程不能使用非自己线程设定的值,因为set方法和get方法是绑定线程的。
  • 一个对象,多个线程用,还互不影响。

定时器

我们有时候会有这样的需求,我希望定时执行任务,比如3秒后执行,其实我们可以通过使用Thread.sleep()来实现:

public static void main(String[] args) {new TimerTask(() -> System.out.println("我是定时任务!"), 3000).start();   //创建并启动此定时任务
}static class TimerTask{Runnable task;long time;public TimerTask(Runnable runnable, long time){this.task = runnable;this.time = time;}public void start(){new Thread(() -> {try {Thread.sleep(time);task.run();   //休眠后再运行} catch (InterruptedException e) {e.printStackTrace();}}).start();}
}

我们通过自行封装一个TimerTask类,并在启动时,先休眠3秒钟,再执行我们传入的内容。那么现在我们希望,能否循环执行一个任务呢?比如我希望每隔1秒钟执行一次代码,这样该怎么做呢?

public static void main(String[] args) {new TimerLoopTask(() -> System.out.println("我是定时任务!"), 3000).start();   //创建并启动此定时任务
}static class TimerLoopTask{Runnable task;long loopTime;public TimerLoopTask(Runnable runnable, long loopTime){this.task = runnable;this.loopTime = loopTime;}public void start(){new Thread(() -> {try {while (true){   //无限循环执行Thread.sleep(loopTime);task.run();   //休眠后再运行}} catch (InterruptedException e) {e.printStackTrace();}}).start();}
}

现在我们将单次执行放入到一个无限循环中,这样就能一直执行了,并且按照我们的间隔时间进行。

但是终究是我们自己实现,可能很多方面还没考虑到,Java也为我们提供了一套自己的框架用于处理定时任务:

public static void main(String[] args) {Timer timer = new Timer();    //创建定时器对象timer.schedule(new TimerTask() {   //注意这个是一个抽象类,不是接口,无法使用lambda表达式简化,只能使用匿名内部类@Overridepublic void run() {System.out.println(Thread.currentThread().getName());    //打印当前线程名称}}, 1000);    //执行一个延时任务
}

我们可以通过创建一个Timer类来让它进行定时任务调度,我们可以通过此对象来创建任意类型的定时任务,包延时任务、循环定时任务等。我们发现,虽然任务执行完成了,但是我们的程序并没有停止,这是因为Timer内存维护了一个任务队列和一个工作线程:

public class Timer {/*** The timer task queue.  This data structure is shared with the timer* thread.  The timer produces tasks, via its various schedule calls,* and the timer thread consumes, executing timer tasks as appropriate,* and removing them from the queue when they're obsolete.*/private final TaskQueue queue = new TaskQueue();/*** The timer thread.*/private final TimerThread thread = new TimerThread(queue);...
}

TimerThread继承自Thread,是一个新创建的线程,在构造时自动启动:

public Timer(String name) {thread.setName(name);thread.start();
}

而它的run方法会循环地读取队列中是否还有任务,如果有任务依次执行,没有的话就暂时处于休眠状态:

public void run() {try {mainLoop();} finally {// Someone killed this Thread, behave as if Timer cancelledsynchronized(queue) {newTasksMayBeScheduled = false;queue.clear();  // Eliminate obsolete references}}
}/*** The main timer loop.  (See class comment.)*/
private void mainLoop() {try {TimerTask task;boolean taskFired;synchronized(queue) {// Wait for queue to become non-emptywhile (queue.isEmpty() && newTasksMayBeScheduled)   //当队列为空同时没有被关闭时,会调用wait()方法暂时处于等待状态,当有新的任务时,会被唤醒。queue.wait();if (queue.isEmpty())break;    //当被唤醒后都没有任务时,就会结束循环,也就是结束工作线程...
}

newTasksMayBeScheduled实际上就是标记当前定时器是否关闭,当它为false时,表示已经不会再有新的任务到来,也就是关闭,我们可以通过调用cancel()方法来关闭它的工作线程:

public void cancel() {synchronized(queue) {thread.newTasksMayBeScheduled = false;queue.clear();queue.notify();  //唤醒wait使得工作线程结束}
}

因此,我们可以在使用完成后,调用Timer的cancel()方法以正常退出我们的程序:

public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName());timer.cancel();  //结束}}, 1000);
}
  • 主要是两个方法:
  • 定时启动schedule();形参是TinmerTask类,这是一个抽象类,要实现抽象方法run。一般推荐使用匿名内部类去使用,第二个形参是时间,设置时间,run方法执行后,隔多久再次执行run方法
  • 取消定时器,取消任务:cancel();(要写在run里面,就是加条件让它达到什么时候才运行cancel);不然run一直出不去

守护线程

不要把操作系统重的守护进程和守护线程相提并论!

守护进程在后台运行运行,不需要和用户交互,本质和普通进程类似。而守护线程就不一样了,当其他所有的非守护线程结束之后,守护线程自动结束,也就是说,Java中所有的线程都执行完毕后,守护线程自动结束,因此守护线程不适合进行IO操作,只适合打打杂:

public static void main(String[] args) throws InterruptedException{Thread t = new Thread(() -> {while (true){try {System.out.println("程序正常运行中...");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.setDaemon(true);   //设置为守护线程(必须在开始之前,中途是不允许转换的)t.start();for (int i = 0; i < 5; i++) {Thread.sleep(1000);}
}

在守护线程中产生的新线程也是守护的:

public static void main(String[] args) throws InterruptedException{Thread t = new Thread(() -> {Thread it = new Thread(() -> {while (true){try {System.out.println("程序正常运行中...");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});it.start();});t.setDaemon(true);   //设置为守护线程(必须在开始之前,中途是不允许转换的)t.start();for (int i = 0; i < 5; i++) {Thread.sleep(1000);}
}

sksMayBeScheduled) //当队列为空同时没有被关闭时,会调用wait()方法暂时处于等待状态,当有新的任务时,会被唤醒。
queue.wait();
if (queue.isEmpty())
break; //当被唤醒后都没有任务时,就会结束循环,也就是结束工作线程

}


`newTasksMayBeScheduled`实际上就是标记当前定时器是否关闭,当它为false时,表示已经不会再有新的任务到来,也就是关闭,我们可以通过调用`cancel()`方法来关闭它的工作线程:```java
public void cancel() {synchronized(queue) {thread.newTasksMayBeScheduled = false;queue.clear();queue.notify();  //唤醒wait使得工作线程结束}
}

因此,我们可以在使用完成后,调用Timer的cancel()方法以正常退出我们的程序:

public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName());timer.cancel();  //结束}}, 1000);
}
  • 主要是两个方法:
  • 定时启动schedule();形参是TinmerTask类,这是一个抽象类,要实现抽象方法run。一般推荐使用匿名内部类去使用,第二个形参是时间,设置时间,run方法执行后,隔多久再次执行run方法
  • 取消定时器,取消任务:cancel();(要写在run里面,就是加条件让它达到什么时候才运行cancel);不然run一直出不去

守护线程

不要把操作系统重的守护进程和守护线程相提并论!

守护进程在后台运行运行,不需要和用户交互,本质和普通进程类似。而守护线程就不一样了,当其他所有的非守护线程结束之后,守护线程自动结束,也就是说,Java中所有的线程都执行完毕后,守护线程自动结束,因此守护线程不适合进行IO操作,只适合打打杂:

public static void main(String[] args) throws InterruptedException{Thread t = new Thread(() -> {while (true){try {System.out.println("程序正常运行中...");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.setDaemon(true);   //设置为守护线程(必须在开始之前,中途是不允许转换的)t.start();for (int i = 0; i < 5; i++) {Thread.sleep(1000);}
}

在守护线程中产生的新线程也是守护的:

public static void main(String[] args) throws InterruptedException{Thread t = new Thread(() -> {Thread it = new Thread(() -> {while (true){try {System.out.println("程序正常运行中...");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});it.start();});t.setDaemon(true);   //设置为守护线程(必须在开始之前,中途是不允许转换的)t.start();for (int i = 0; i < 5; i++) {Thread.sleep(1000);}
}

这篇关于Note of java修改于2023.03.13的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/618334

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定