Java知识点总结《努力篇下》

小聊: 本文主要编辑小白学习 Java 时感觉重要的、容易忘掉的的细节小知识或者知识补充。想着会对学习和使用 Java 的查漏补缺很有帮助,就一点点记录下来了,内容比较多的知识点会另外写文章放在本专栏,所以这里的都是小 tips 哦。

整个Java语言部分,我会分为《基础篇》、《努力篇》和《补充篇》进行编写。本文为《努力篇下》了,因为知识点模块比较大,目录结构和之前有点改动。基础内容大致总结到这里,后续还有的话会更新到《补充篇》一文中。

目前其它部分:

Java知识点总结《基础篇》

Java知识点总结《努力篇上》


1. 异常

1.1. 异常定义

  • 异常体系结构图
    图源来自网络

异常的根类是 java.lang.Throwable ,其下有两个子类: java.lang.Errorjava.lang.Exception

  • Error: 严重错误 Error,无法通过处理的错误,只能事先避免,好比绝症。
  • Exception: 表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的
  • 常见Error举例与说明

Java 虚拟机运行错误(Virtual MachineError)内存不足错误(OutOfMemoryError)类定义错误(NoClassDefFoundError)

这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,Java 虚拟机一般会选择线程终止。

  • 常见Exception举例与说明

RuntimeException (运行时异常)NullPointerException(空指针异常,要访问的变量没有引用任何对象)java.lang.ClassNotFoundException(类不存在异常)ArrayIndexOutOfBoundsException(数组下标越界异常)java.lang.ClassCastException(数据类型转换异常,比如强转失败)FileNotFoundException(文件找不到异常)SQLException(数据库操作异常)ArithmeticException(算术运算异常,比如一个整数除以0)

异常和错误的区别:异常能被程序本身处理,错误是无法处理。

  • 异常的常用方法
方法 解释
public void printStackTrace() 打印异常的详细信息(包含了异常的类型,异常的原因,还有异常出现的位置,在开发和调试阶段,都得使用printStackTrace)
public String getMessage() 获取发生异常的原因(提示给用户的时候,就提示错误原因)
public String toString() 获取异常的类型和异常描述信息(不怎么用
  • 异常产生原因
    • JVM检测出异常后,JVM会根据异常产生的原因创建一个异常对象,这个异常对象包含了异常产生的 内容、原因和位置。在 getElement 方法中,没有异常处理逻辑(try…catch),那么JVM会把异常对象抛出给方法的调用者main方法来处理这个异常
    • main 方法接收到异常对象后,也没有异常的处理逻辑的话,就把对象抛给 main 方法的调用者 JVM 处理
    • JVM接收到异常后会把异常对象(内容、原因位置)用红色字体打印在控制台,然后JVM会终止当前执行的java程序,即中断处理

1.2. 异常处理:try/catch/finally、throw、throws

Java异常处理的五个关键字:try、catch、finally、throw、throws

  • try…catch语句

对异常进行捕获,Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理

  • 语法格式

    try {
           
           
         //编写可能会出现异常的代码
    } catch(异常类型  e) {
           
           
         //处理异常的代码
         //记录日志/打印异常信息/继续抛出异常
    }
    
  • finally代码块

有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而 finally 就是解决这个问题的,在finally代码块中存放的代码,无论是否被 catch,都是一定会被执行的。

  • 代码举例:比如,什么时候的代码必须最终执行?

    /**
     * 当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,需要关闭打开的资源
     */
    import java.io.FileNotFoundException;
    
    public class tryCatchFinallyDemo {
           
           
        public static void main(String[] args) {
           
           
            try {
           
           
                read("b.txt");
            } catch (FileNotFoundException e) {
           
           
                //System.out.println("该类型的异常为:" + e.getMessage());
                e.printStackTrace();
            } finally {
           
           
                System.out.println("无论如何,finally的代码都会执行!");
            }
        }
    
        public static void read(String path) throws FileNotFoundException {
           
           
            if(!"a.txt".equals(path)) {
           
           
                //不存在此文件,是一个错误,则抛出异常
                throw new FileNotFoundException("文件不存在!");
            }
    
        }
    }
    
    输出
    java.io.FileNotFoundException: 文件不存在!
    	at study.异常.tryCatchFinallyDemo.read(tryCatchFinallyDemo.java:22)
    	at study.异常.tryCatchFinallyDemo.main(tryCatchFinallyDemo.java:9)
    无论如何,finally的代码都会执行!
    
  • throws声明异常

关键字 throws 运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常)。如果调用者依旧没有处理,到最后会交给 JVM 进行处理,它会按照默认的处理方式去处理

  • 声明格式

    修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2{
           
            }
    
  • 代码示例

    public class throwsDemo {
           
           
    	public static void main(String[] args)  throws Exception {
           
           
    		int c = divide(1, 0);
    		System.out.println(c);
    	}
        public static int divide(int a, int b) throws Exception {
           
           
            int c = a / b;	
            return c;		
    	}
    }
    
  • throw抛出异常

创建一个异常对象。封装一些提示信息(信息可以自己编写)

throw 用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行

通过 throw 关键字抛出异常后,还需要使用 throws 关键字或 try…catch 对异常进行处理。

注意:如果 throw 抛出 的是 Error、RuntimeException 或它们的子类异常对象,则无需使用 throws 关键字或 try… .catch 对异常进行处理

  • 声明格式

    throw new 异常类名(参数);
    
  • 使用示例

    public class ThrowDemo {
           
           
    	public static void main(String[] args) /* throws Exception */ {
           
           
    		try {
           
           
    			printfAge(-1);
    		} catch (Exception e) {
           
           
    			System.out.println("该类型的异常为:" + e.getMessage());
    		}
    	}
    	
    	public static void printfAge(int age) throws Exception {
           
           
    		if(age < 0 || age > 200)
    			throw new Exception("年龄不在正确范围内");
    		else
    			System.out.println(age);
    	}
    }
    
    输出
    该类型的异常为:年龄不在正确范围内
    

1.3. 垃圾回收机制

Java虚拟机会自动进行垃圾回收,不需要我们去主动编写回收指令

回收方式为:除了等待Java虛拟机进行自动垃圾回收外,还可以主动通知系统垃圾回收器进行垃圾回收

  • 代码手动回收操作

(1)调用 System 类的 gc() 静态方法:System.gc();

(2)调用 Runtime 对象的 gc() 实例方法:Runtime.getRuntime.gc();

说明:以上两种方式可以通知启动垃圾回收器进行垃圾回收,但是否立即进行垃圾回收依然具有不确定性。多数情况下,执行后总是有一定的效果。

注意:

  1. 虽然通过程序可以控制一个对象何时不再被任何引用变量所引用,但是却无法精确的控制 Java 垃圾回收的时机,就是说当你调用方法进行垃圾回收时,它并不会立即进行回收操作
  2. 当一个对象在内存中被释放时,它的 finalize() 方法会被自动调用, finalize() 方法是定义在 Object 类中的实例方法。
  3. 任何 Java 类都可以重写 Object 类的 finalize() 方法,在该方法中清理该对象占用的资源。如果程序终之前仍然没有进行垃圾回收,则不会调用失去引用对象的 finalize() 方法来清理资源
  4. 只有当程序认为需要更多的额外内存时,垃圾回收器才会自动进行垃圾回收
  • 代码使用示例
class Person {
    
    
    //finalize()被自动调用,说明Person对象已被回收
	@Override
	public void finalize() throws Throwable {
    
    
		System.out.println("Person对象被释放了!");
	}
}

public class 垃圾回收机制Demo {
    
    
	public static void main(String[] args) {
    
    
		//method1();
		method2();
	}
	
	public static void method1() {
    
    
		//不手动介入情况
		Person p = new Person();
		p = null;
		
		for(int i = 1; i <= 10; i ++) {
    
    
			System.out.println("哈哈哈!");
		}
	}
	
	public static void method2() {
    
    
		//手动介入的情况
		Person p = new Person();
		p = null;
		System.gc();
		//Runtime.getRuntime().gc();
		
		for(int i = 1; i <= 10; i ++) {
    
    
			System.out.println("嘿嘿嘿!");
		}
	}
}

2. IO流

2.1. IO流分类体系

主要是指使用 java.io 包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据。

我们把这种数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为输入input输出output ,即流向内存是输入流,流出内存的输出流。

  • 数据的流向:输入流输出流

    • 输入流 :把数据从 其他设备 上读取到 内存 中的流。

    • 输出流 :把数据从 内存 中写出到 其他设备 上的流。

  • 数据的类型:字节流字符流

    • 字节流 :以字节为单位,读写数据的流。

    • 字符流 :以字符为单位,读写数据的流。

  • 数据的角色:节点流处理流

    • 节点流 :文件、管道、数组操作相关。
    • 处理流 :缓冲、序列化、转换、打印操作相关。

图源来自网络

图源来自网络


2.2. 字节流 && FileInputStream 输入流

  • 构造方法

    方法 解释
    FileInputStream(File file) 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File 对象命名。
    FileInputStream(String name) 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name 命名
    • 代码演示

      public class FileInputStreamConstructor throws IOException {
              
              
          public static void main(String[] args) {
              
              
         	 	// 使用File对象创建流对象
              File file = new File("a.txt");
              FileInputStream fos = new FileInputStream(file);
            
              // 使用文件名称创建流对象
              FileInputStream fos = new FileInputStream("b.txt");
          }
      }
      
  • 常用方法

    方法 解释
    public void close() 关闭此输入流并释放与此流相关联的任何系统资源
    public abstract int read() 从输入流读取数据的下一个字节
    public int read(byte[] b) 从输入流中读取一些字节数,并将它们存储到字节数组 b中
  • 代码演示

    public class FileInputStreamDemo {
    	public static void main(String[] args) throws IOException {
    		//1.创建字节对象
    		FileInputStream in = new FileInputStream("a.txt");	//a.txt的内容是abcde
    		
    		 int i = in.read();
    		 
    		 //将int型转成字节型
    		 byte q = (byte)i;
    		 byte[] a = {q};
    		 //再将字节型转成字符串型输出
    		System.out.println(i + "," + new String(a));	//97, a	第一次输出第一个字节(返回值为ASCII码)和其表示的字符
    		
    		//在循环中不断的调用read方法,并把读取到的数据给i赋值,只要没有读取到-1,说明没有达到文件末尾可以继续读取
    		while((i = in.read()) != -1) {
    			System.out.println((char)i);
    		}
    		//.关闭流释放资源
    		in.close();		
    	}
    }
    

2.3. 字节流 && FileOutputStream 输出流

当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。再次写入如果不想清空掉原有内容,就可以在构造方法的位置,加入第二个关键字 true 的开关即可

  • 构造方法

    方法 解释
    FileOutputStream(File file) 创建文件输出流以写入由指定的 File对象表示的文件
    FileOutputStream(String name) 创建文件输出流以指定的名称写入文件
    public FileOutputStream(File file, boolean append) 创建文件输出流以写入由指定的 File对象表示的文件
    public FileOutputStream(String name, boolean append) 创建文件输出流以指定的名称写入文件
    • 代码演示

      public class FileOutputStreamConstructor throws IOException {
              
              
          public static void main(String[] args) {
              
              
         	 	// 使用File对象创建流对象
              File file = new File("a.txt");
              FileOutputStream fos1 = new FileOutputStream(file);
              FileOutputStream fos2 = new FileOutputStream(file, true);
              // 使用文件名称创建流对象
              FileOutputStream fos3 = new FileOutputStream("b.txt");
              ileOutputStream fos4 = new FileOutputStream("b.txt", true);
          }
      }
      
  • 常用方法

    方法 解释
    public void close() 关闭此输出流并释放与此流相关联的任何系统资源
    public void flush() 刷新此输出流并强制任何缓冲的输出字节被写出
    public int write(byte[] b) 将 b.length字节从指定的字节数组写入此输出流
    public void write(byte[] b, int off, int len) 从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流
    public abstract void write(int b) 将指定的字节输出流
  • 代码使用演示

    public class Part0202_字节输出流File0utputStream {
          
          
    	public static void main(String[] args) throws IOException {
          
          
    		//1/创建字节输出流对象
    		FileOutputStream out = new FileOutputStream("a.txt");	//如果文件不存在,则自动创建	
    		//2.写入数据
    		out.write(65);	//运行后文件中会出现“A”;
    		
    		//如果想输出字符串,需要用到String类的getByte()方法
    		byte[] i = "鸢一折纸".getBytes();
    		out.write(i);	//运行后文件中会出现“A鸢一折纸”;
    		System.out.println(Arrays.toString(i));	//输出“鸢一折纸”的字节码:[-16, -80, -46, -69, -43, -37, -42, -67]
    		//out.write("\r".getBytes());
    		//out.write("\n".getBytes());
    		out.write("\r\n".getBytes());
    		
            //数据追加演示,不会清空文件原有的数据
    		FileOutputStream out2 = new  FileOutputStream("a.txt", true);
    		out2.write("夜刀神十香".getBytes());		//A鸢一折纸
    											   //夜刀神十香
    		//3.关闭流释放资源
    		out.close();	
    		out2.close();	
     	}
    }
    

2.4. 字符流 && FileReader 字符输入(读取)流

操作纯文本文件的时候,可以解决字节流中文乱码问题

当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。

字符流,只能操作文本文件,不能操作图片,视频等非文本文件。当我们单纯读或者写文本文件时 使用字符流;其他情况使用字节流。

  • 构造方法
方法 解释
FileReader(File file) 创建一个新的 FileReader ,给定要读取的File对象
FileReader(String name) 创建一个新的 FileReader ,给定要读取的文件的名称
  • 常用方法
常用方法 解释
public void close() 关闭此流并释放与此流相关联的任何系统资源
public int read() 从输入流读取一个字符
public int read(char[] cbuf) 从输入流中读取一些字符,并将它们存储到字符数组 cbuf
  • 代码示例
import java.io.FileReader;
import java.io.IOException;

public class FileReaderDemo {
    
    
	public static void main(String[] args) throws IOException {
    
    
		//创建一个字符流输入对象
		FileReader fr = new FileReader("a.txt");
		
		int i;
		
		while((i = fr.read()) != -1) {
    
    
			System.out.print((char)i);	//虽然读取了一个字符,但是会自动提升为int类型,这里需要转型
		}
		
		fr.close();
	}
}

2.5. 字符流 && FileWriter 字符输出(写入)流

  • 构造方法
方法 解释
FileWriter(File file) 创建一个新的 FileWriter,给定要读取的File对象
FileWriter(String name) 创建一个新的 FileWriter,给定要读取的文件的名称
  • 常用方法
常用方法 解释
void write(int c) 写入单个字符
void write(char[] cbuf) 写入字符数组
abstract void write(char[] cbuf, int off, int len) 写入字符数组的某一部分,off 数组的开始索引,len 写的字符个数
void write(String str) 写入字符串
void write(String str, int off, int len) 写入字符串的某一部分,off 字符串的开始索引,len 写的字符个数。 void flush() 刷新该流的缓冲
void flush() 刷新该流的缓冲
void close() 关闭此流,但要先刷新它
  • 注意

如果字符输出流没有调用 close 方法或 flush 方法的话,数据将不会写出到文件当中

flushclose 方法的区别
(1)flush() 方法是将数据刷出到文件中去,刷出之后可以继续调用 write() 方法写出
(2)close() 方法的主要功能是关闭流释放资源,同时也具有刷出数据的效果
(3)close() 方法调用结束后,不能再调用 write() 方法写出数据

  • 代码示例
import java.io.FileWriter;
import java.io.IOException;

public class FileWriterDemo {
    
    
	public static void main(String[] args) throws IOException {
    
    
		//创建一个字符流输出对象
		FileWriter fw = new FileWriter("a.txt");
		
		//写一个字符
		fw.write('A');	//A
		
		//写一个字符数组
		char[] arr = {
    
    'H', 'e', 'l', 'l', 'o'};	//AHello
		fw.write(arr);
		
		//写一个字符串
		fw.write("鸢一折纸");	//AHello鸢一折纸
		
		//写一个字符串的一部分
		String str = "你好,我的世界!"; 	//AHello鸢一折纸我的世界
		fw.write(str, 3, 4);	//(字符串名称, 起始索引, 字符串个数)
		
		fw.flush();
		fw.close();
	}
}

2.6. 处理流 && 节点流

  • 认识举例: IO流读取转换文件 utf8 编码操作(涉及以下IO类)

    • FileInputStream:文件操作——节点流
    • InputStreamReader:转换操作 —— 处理流
  • InputStreamReader 构造方法

方法 解释
InputStreamReader(InputStream in) 创建一个使用默认字符集的 InputStreamReader
InputStreamReader(InputStream in, String charsetName) 建使用指定默认字符集的 InputStreamReader
  • 代码演示
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class InputStreamRreaderDemo {
    
    
	public static void main(String[] args) throws IOException {
    
    
		//我这里演示按指定的编码表读写数据
		FileInputStream fin = new FileInputStream("a.txt");
		InputStreamReader isr = new InputStreamReader(fin, "utf-8");	//文件a.txt的编码表为UTF-8(默认的是GDK)
		BufferedReader br = new BufferedReader(isr);
		
		String i;
		while((i = br.readLine()) != null) {
    
    
			System.out.println(i);
		}
		
		br.close();
	}
}

2.7. print 打印流

平时我们在控制台打印输出,是调用 print 方法和 println 方法完成的,这两个方法都来自于 java.io.PrintStream 类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。

  • 构造方法
构造方法 解释
public PrintStream(String fileName) 使用指定的文件名创建一个新的打印流
PrintStream ps = new PrintStream("ps.txt");
  • 改变打印流向

System.out 就是 PrintStream 类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以改变它的流向。

  • 代码演示
public class PrintDemo {
    
    
    public static void main(String[] args) throws IOException {
    
    
		// 调用系统的打印流,控制台直接输出aaa
        System.out.println("aaa");
      
		// 创建打印流,指定文件的名称
        PrintStream ps = new PrintStream("a.txt");
      	
      	// 设置系统的打印流流向,输出到a.txt
        System.setOut(ps);
      	// 调用系统的打印流
        System.out.println("bbb");	//控制台不再输出bbb,而是打印在了a.txt文件中
    }
}

a.txt文件中显示:
aaa

3. 序列化

3.1. 认识序列化

对象的序列化 (Serializable) 是指将一个 Java 对象转换成一个 I/O 流中字节序列的过程,即用一个字节序列可以表示一个对象,该字节序列包含该 对象的数据对象的类型对象中存储的属性 等信息。字节序列写出到文件之后,相当于文件中 持久保存 了一个对象的信息。

需要序列化的类一定要继承 Serializable 接口

  • 使用示例
class Fruit implements Serializable {
    
    
    // 加入序列版本号
    private static final long serialVersionUID = 1L;
    private final String name;
    private final String color;

    public Fruit(String name, String color) {
    
    
        this.name = name;
        this.color = color;
    }

    @Override
    public String toString() {
    
    
        return "Fruit{" +
                "name='" + name + '\'' +
                ", color='" + color + '\'' +
                '}';
    }
}

public class 序列化 {
    
    
    public static void main(String[] args) throws IOException {
    
    
        Fruit watermelon = new Fruit("西瓜", "red");
        Fruit banana = new Fruit("香蕉", "yellow");
        Fruit grape = new Fruit("葡萄", "purple");
        Fruit coco = new Fruit("椰子", "write");
        ArrayList<Fruit> fruits = new ArrayList<>();
        fruits.add(watermelon);
        fruits.add(banana);
        fruits.add(grape);
        fruits.add(coco);

        outPut(fruits);

        inPut();

    }

    private static <E> void outPut(E fruits)  {
    
    
        try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src\\study\\IO流\\字节流\\a.txt"))) {
    
    
            oos.writeObject(fruits);
        }catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    private static void inPut() {
    
    
        try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src\\study\\IO流\\字节流\\a.txt"))) {
    
    
            ArrayList<Fruit> list = (ArrayList<Fruit>)ois.readObject();
            list.forEach(System.out::println);
        } catch (IOException | ClassNotFoundException e) {
    
    
            e.printStackTrace();
        }
    }
}

输出
Fruit{
    
    name='西瓜', color='red'}
Fruit{
    
    name='香蕉', color='yellow'}
Fruit{
    
    name='葡萄', color='purple'}
Fruit{
    
    name='椰子', color='write'}

3.2. serialVersionUID(序列ID)

Serializable 接口给需要序列化的类,提供了一个序列版本号。serialVersionUID 该版本号的目的在于验证序列化的对象和对应类是否版本匹配

serialVersionUID 适用于 java 序列化机制。简单来说,JAVA 序列化的机制是通过判断类的 serialVersionUID 来验证的版本一致的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 于本地相应实体类的 serialVersionUID 进行比较。如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是 InvalidCastException

  • 具体序列化的过程

序列化操作时会把系统当前类的 serialVersionUID 写入到序列化文件中,当反序列化时系统会自动检测文件中的 serialVersionUID,判断它是否与当前类中的 serialVersionUID 一致。如果一致说明序列化文件的版本与当前类的版本是一样的,可以反序列化成功,否则就失败;

  • serialVersionUID有两种显示的生成方式

(1)是默认的1L,比如:private static final long serialVersionUID = 1L;

(2)是根据包名,类名,继承关系,非私有的方法和属性,以及参数,返回值等诸多因子计算得出的,极度复杂生成的一个64位的哈希字段。基本上计算出来的这个值是唯一的。比如:private static final long serialVersionUID = xxxxL;

当实现 java.io.Serializable 接口中没有显示的定义 serialVersionUID 变量的时候,JAVA序列化机制会根据Class自动生成一个serialVersionUID 作序列化版本比较用,这种情况下,如果Class文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID 也不会变化的。

如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,就需要自己显示的定义一个 serialVersionUID,类型为 long 的变量。不修改这个变量值的序列化实体,都可以相互进行序列化和反序列化。


4. 多线程

4.1. 线程与进程

进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

扩充:在java中,每次程序运行至少启动2个线程。一个是 main 线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个 JVM 其实在就是在操作系统中启动了一个进程。

  • 线程调度

分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度

  • 抢占式调度详解

大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

  • 线程的6种基本状态
状态名称 说明
NEW 初始状态,线程被构建,但是还没有调用start()方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程进入等待状态,进人该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
TIME_WAITING 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
TERMINATED 终止状态,表示当前线程已经执行完毕

4.2. Thread

  • 执行步骤
  1. 创建一个 Thread 线程类的子类(子线程),同时重写 Thread 类的 run() 方法;
  2. 创建该子类的实例对象,并通过调用 start() 方法启动线程
  • 构造方法
构造方法 解释
public Thread() 分配一个新的线程对象
public Thread(String name) 分配一个指定名字的新的线程对象
public Thread(Runnable target) 分配一个带有指定目标新的线程对象
public Thread(Runnable target,String name) 分配一个带有指定目标新的线程对象并指定名字
  • 常用方法
方法 解释
public String getName() 获取当前线程名称
public void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
public void run() 此线程要执行的任务在此处定义代码
public static void sleep(long millis) 线程休眠。使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)
public static native void yield() 线程让步。与sleep不同它不会阻塞线程,而是让系统的调度器重新调度一次。与当前线程优先级相同或者更高的线程可以获得执行的机会。
public final void join() 线程插队。调用的线程将被阻塞,直到被调用了 join() 方法加入的线程执行完成后它才会继续运行。
public static Thread currentThread() 返回对当前正在执行的线程对象的引用
  • 代码示例
//创建一个Thread线程类的子类
class Mythreads extends Thread {
    
    
	//创建子类线程有参构造方法
	public Mythreads(String name) {
    
    
		// TODO Auto-generated constructor stub
		super(name);
	}

	//重写Thread类的run()方法
	public void run() {
    
    
		int i = 0;
		while(i ++ < 100) {
    
    	
			//currentThread()方法得到当前线程的实例对象,然后调用getName()方法获取到线程名称。接下来,通过继承Thread类的方式来实现多线程
			System.out.println(Mythreads.currentThread().getName()
					 + "的run()方法在运行");
		}
	}
}

public class ThreadDemo {
    
    
	public static void main(String[] args) {
    
    
		//创建Mythreads 实例对象
		Mythreads thread1 = new Mythreads("一号");
		//调用start()方法启动线程
		thread1.start();
		
		//创建另一个线程并启动
		Mythreads thread2 = new Mythreads("二号");
		thread2.start();
		
	}
}

4.3. Runnable

  • 执行步骤
  1. 定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的线程执行体。
  2. 创建 Runnable 接口的实现类对象;
  3. 并以此实例作为 Thread 的参数来创建 Thread 对象,该 Thread 对象才是真正的线程对象。
  4. 调用线程对象的 start() 方来启动线程。

实际上所有的多线程代码都是通过运行 Thread 的 start() 方法来运行的。因此,不管是继承 Thread 类还是实现 Runnable 接口来实现多线程,最终还是通过 Thread 的对象的 API 来控制线程的,熟悉 Thread 类的 API 是进行多线程编程的基础。

注意:因为Runnable接口只有一个抽象run()方法,可以用 Lambda 表达式

  • 代码示例
public class RunnableDemo {
    
    
    public static void main(String[] args) {
    
    
        Thread thread1 = new Thread(() -> {
    
    
            int i = 50;
            while (i-- > 0) {
    
    
                System.out.println(Thread.currentThread().getName() + "正在运作!");
            }
        }, "1号");

        Thread thread2 = new Thread(() -> {
    
    
            int i = 50;
            while (i-- > 0) {
    
    
                System.out.println(Thread.currentThread().getName() + "正在运作!");
            }
        }, "2号");

        thread1.start();
        thread2.start();
    }
}
  • Thread和Runnable的区别

如果一个类继承 Thread,则不适合资源共享。但是如果实现了 Runable 接口的话,则很容易的实现资源共享。

  • 实现 Runnable 接口比继承 Thread 类所具有的优势
    • 适合多个相同的程序代码的线程去共享同一个资源
    • 可以避免 java 中的单继承的局限性
    • 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立
    • 线程池只能放入实现 RunableCallable 类线程,不能直接放入继承 Thread 的类

4.4. Callable

通过 Thread 类和 Runnable 接口实现多线程时,需要重写 run() 方法,但是由于该方法没有返回值,因此无法从多个线程中获取返回结果

为了解决这个问题,从 JDK 5 开始,Java 提供了一个 Callable 接口,来满足这种既能创建多线程又可以有返回值的需求

通过 Callable 接口实现多线程的方式与 Runnable 接口实现多线程的方式一样,都是通过 Thread 类的有参构造方法传人 Runnable 接口类型的参数来实现多线程,不同的是,这里传入的是 Runnable 接口的子类 FutureTask 对象作为参数,而 FutureTask 对象中则封装带有返回值的 Callable 接口实现类

  • 执行步骤
  1. 创建一个 Callable 接口的实现类,同时重写 Callable 接口的 call() 方法;
  2. 创建 Callable 接口的实现类对象;
  3. 通过 FutureTask 线程结果处理类的有参构造方法来封装 Callable 接口实现类对象;
  4. 使用参数为 FutureTask 类对象的 Thread 有参构造方法创建 Thread 线程实例;
  5. 调用线程实例的 start() 方法启动线程。
  • 代码示例
class MyCallable implements Callable {
    
    
    @Override
    public Object call() throws Exception {
    
    
        int i = 50;
        while (i-- > 0) {
    
    
            System.out.println(Thread.currentThread().getName()
                    + "的run()方法在运行");
        }
        return i;
    }
}

public class CallableDemo {
    
    
    public static void main(String[] args) {
    
    
        //创建Callable接口的实现类对象;
        MyCallable myCallable = new MyCallable();

        //使用FutureTask封装Callable接口
        FutureTask<Object> ft1 = new FutureTask<Object>(myCallable);
        //使用Thread (Runnable target, String name)构造方法创建线程对象
        Thread thread1 = new Thread(ft1, "1号");

        FutureTask<Object> ft2 = new FutureTask<Object>(myCallable);
        Thread thread2 = new Thread(ft2, "2号");

        thread1.start();
        thread2.start();
    }
}

5. 线程安全

5.1. synchronized

在 Java 中 synchronized 关键字一直都是元老级的角色,以前经常被称为“重量级锁 ”。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么繁重了,当然效率也高了很多。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞竞争切换后继续竞争锁稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

Java中每一个对象都可以作为锁,这是 synchronized 实现同步的基础:

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的class对象
  • 同步方法块,锁是括号里面的对象
  • 同步代码块
class MyRunnable implements Runnable {
    
    
    private int tickets = 20;
    //定义一个任意对象,用作同步代码块的锁
    Object lock = new Object();
    @Override
    public void run() {
    
    
        while (true) {
    
    
            synchronized (lock) {
    
    
                if (tickets > 0) {
    
    
                    try {
    
    
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在发售第" + (tickets--) + "票");
                } else {
    
    
                    break;
                }
            }
        }
    }
}

public class Demo {
    
    
    public static void main(String[] args) {
    
    
        MyRunnable runnable = new MyRunnable();
        new Thread(runnable, "01号窗口").start();
        new Thread(runnable, "01号窗口").start();
        new Thread(runnable, "01号窗口").start();
    }
}
  • 同步方法

语法

[修饰符] synchronized 返回值类型 方法名([参数1, ……]){
    
    }

使用举例

class MyRunnable implements Runnable {
    
    
    private int tickets = 20;
    @Override
    public void run() {
    
    
        while (true) {
    
    
            locked();
            if(tickets <= 0) {
    
    
                break;
            }
        }
    }
    private synchronized void locked() {
    
    
        if (tickets > 0) {
    
    
            try {
    
    
                Thread.sleep(100);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在发售第" + tickets-- + "张票");
        }
    }
}

public class 同步代码块 {
    
    
    public static void main(String[] args) {
    
    
        MyRunnable runnable = new MyRunnable();

        new Thread(runnable, "01号窗口").start();
        new Thread(runnable, "01号窗口").start();
        new Thread(runnable, "01号窗口").start();
    }
}

5.2. ReentrantLock同步锁

synchronized 同步代码块和同步方法使用一种封闭式的锁机制,使用起来非常简单,也能够解决线程同步过程中出现的线程安全问题,但也有一些限制,例如它无法中断一个正在等候获得锁的线程,也无法通过轮询得到锁,如果不想等下去,也就没法得到锁。

JDK 5 开始, Java 增加了一个功能更强大的 ReentrantLock 锁。ReentrantLock 锁与 synchronized 隐式锁在功能上基本相同,其最大的优势在于 ReentrantLock 锁可以让某个线程在持续获取同步锁失败后返回,不再继续等待,另外 ReentrantLock 锁在使用时也更加灵活

锁【locked.lock】必须紧跟try代码块,且 unlock 要放到 finally代码块 第一行。

  • 方法
方法 解释
public void lock() 加同步锁
public void unlock() 释放同步锁
  • 使用示例
class SaleThread implements Runnable {
    
    
    private int tickets = 20;
    //定义一个Lock锁对象,用作同步代码块的锁
    private final Lock locked = new ReentrantLock();

    @Override
    public void run() {
    
    
        while (true) {
    
    
            locked.lock();
            if (tickets > 0) {
    
    
                try {
    
    
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + "正在发售第" + tickets-- + "张票");
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } finally {
    
    
                    locked.unlock();
                }
            } else {
    
    
                //执行完代码块后进行释放锁
                locked.unlock();
                break;
            }
        }
    }
}

public class Demo {
    
    
    public static void main(String[] args) {
    
    
        SaleThread thread = new SaleThread();

        new Thread(thread, "窗口1").start();
        new Thread(thread, "窗口2").start();
        new Thread(thread, "窗口3").start();
        new Thread(thread, "窗口4").start();
    }
}

6. 网络编程

6.1. UDP通信协议

用户数据报协议(User Datagram Protocol)。UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。

由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。

特点:数据被限制在64kb以内,超出这个范围就不能发送了。

数据报(Datagram):网络传输的基本单位。

  • InteAddress获取主机地址

    在JDK中提供了一个与IP地址相关的InetAddress类,该类用于封装一个IP地址,并提供了一系列与IP地址相关的方法。

    • 常用方法
    方法 说明
    InetAddress getByName(String host) 获取给定主机名的的IP地址,host参数表示指定主机
    InetAddress getLocalHost() 获取本地主机地址
    String getHostName() 获取本地IP地址的主机名
    boolean isReachable(int timeout) 判断在限定时间内指定的IP地址是否可以访问
    String getHostAddress() 获取字符串格式的原始IP地址
  • DatagramPacket(集装箱)

    创建发送端和接收端的DatagramPacket对象时,使用的构造方法有所不同。接收端的构造方法只需要接收一个字节数组来存放接收到的数据。发送端的构造方法不但要接存放了发送数据的字节数组,还需要指定发送端IP地址和端口号。

    作用:DatagramPacket 用于封装UDP通信中的数据

    • 常用构造方法
    方法 解释
    DatagramPacket(byte[] buf, int length) 指定了封装数据的字节数组和数据的大小,没有指定IP地址和端口号,这样的对象只能用于接收端。
    DatagramPacket(byte[] buf, int offset, int length) 该构造方法.与第1个构造方法类似同样用于接收端,只不过在第1个构造方法的基础上,增加了一个数, 该参数用于指定一个数组中发送数据的偏移量为offset,即从offset位 置开始发送数据。
    DatagramPacket(byte[] buf,int length,InetAddress addr,int port) 该构造方法不仅指定了封装数据的字节数组和数据的大小,还指定了数据包的目标IP地址(addr)和端口号(port) 。该对象通常用于发送端。
    DatagramPacket(byte[] buf, int offset, int length, InetAddress addr, int port) 该构造方法与第3个构造方法类似同样用于发送端,只不过在第3个构造方法的基础上,增加了一个offset参数,该参数用于指定一个数组中发送数据的偏移量为offset,即从offset位置开始发送数据。
    • 常用方法
    方法 解释
    InetAddress getAddress() 该方法用于返回发送端或者接收端的IP地址,如果是发送端的DatagramPacket对象,就返回接收端的IP地址;反之,就返回发送端的IP地址
    int getPort() 该方法用于返回发送端或者接收端的端口号,如果是发送端的DatagramPacket对象,就返回接收端的端口号;反之,就返回发送端的端口号
    byte[] getData() 该方法用于返回将要接收或者将要发送的数据,如果是发送端的DatagramPacket对象,就返回将要发送的数据; 反之,就返回接收到的数据
    int getLength() 该方法用于返回接收或者将要发送数据的长度,如果是发送端的DatagramPacket对象,就返回将要发送的数据长度;反之,就返回接收到数据的长度
  • DatagramSocket(码头)

    • 常用构造方法
    方法 解释
    DatagramSocket() 该构造方法用于创建发送端的 DatagramSocket 对象,并没有指定端口号,此时,系统会分配一个没有被其它网络程序所使用的端口号。
    DatagramSocket(int port) 该构造方法既可用于创建接收端的 DatagramSocket 对象,也可以创建发送端的 DatagramSocket 对象,在创建接收端的 DatagramSocket 对象时,必须要指定一个端口号,这样就可以监听指定的端口。
    DatagramSocket(int port, InetAddress addr) (这个不怎么用)该构造方法在创建 DatagramSocket 时,不仅指定了端口号还指定了相关的 IP 地址,这种情况适用于计算机上有多块网卡的情况,可以明确规定数据通过哪,块网卡向外发送和接收哪块网卡的数据。
    • 常用方法
    方法 解释
    void receive(DatagramPacket p) 该方法用于接收 DatagramPacket 数据报,在接收到数据之前会直处于阻塞状态,如果发送消息的长度比数据报长,则消息将会被截取
    void send(DatagramPacket p) 该方法用于发送 DatagramPacket 数据报,发送的数据报中包含将要发送的数据、数据的长度、远程主机的 IP 地址和端口号
    void close() 关闭当前的 Socket ,通知驱动程序释放为这个 Socket 保留的资源
  • 使用示例

public class TestReceive {
    
    
    public static void main(String[] args) throws Exception {
    
    
        System.out.println("接收端开启!");
        System.out.println("接收端开启!");
        System.out.println("接收端开启!");

        DatagramSocket socket = new DatagramSocket(6666);

        DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);

        socket.receive(packet);

        InetAddress address = packet.getAddress();

        byte[] data = packet.getData();
        int len = packet.getLength();
        int port = packet.getPort();

        System.out.println("ip为" + address + "端口号为" + port + "的对象正在向您发送信息");
        System.out.println("发送的信息为:");
        System.out.println(new String(data));

    }
}


public class TestSend {
    
    
    public static void main(String[] args) throws Exception {
    
    
        System.out.println("发送端开启!");
        System.out.println("发送端开启!");
        System.out.println("发送端开启!");

        DatagramSocket socket = new DatagramSocket(5555);

        byte[] bytes = "你好呀".getBytes();

        DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("127.0.0.1"), 6666);

        socket.send(packet);

        System.out.println("发送成功!");

        socket.close();
    }
}
//注意:先开启接收端,在开启发送端

6.2. TCP通信协议

传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。

在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”。

  • 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。

    第一次握手,客户端向服务器端发出连接请求,等待服务器确认。
    第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。
    第三次握手,客户端再次向服务器端发送确认信息,确认连接。

  • 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。

TCP通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。

  • 服务端程序,需要事先启动,等待客户端的连接。
  • 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。

在Java中,提供了两个类用于实现TCP通信程序:

  • 客户端:java.net.Socket 类表示。创建 Socket 对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。

  • 服务端:java.net.ServerSocket 类表示。创建 ServerSocket 对象,相当于开启一个服务,并等待客户端的连接。

  • Socket类

    该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。

    • 构造方法
    方法 解释
    public Socket(String host, int port) 创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的 host 是 null ,则相当于指定地址为回送地址。
    • 小贴士

    回送地址(127.x.x.x) 是本机回送地址(Loopback Address),主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。

    • 成员方法
    方法 解释
    public InputStream getInputStream() 返回此套接字的输入流。如果此 Scoket 具有相关联的通道,则生成的 InputStream 的所有操作也关联该通道。关闭生成的 InputStream 也将关闭相关的 Socket
    public OutputStream getOutputStream() 返回此套接字的输出流。如果此 Scoket 具有相关联的通道,则生成的 OutputStream 的所有操作也关联该通道。关闭生成的 OutputStream 也将关闭相关的 Socket
    public void close() 关闭此套接字。一旦一个 socket 被关闭,它不可再使用。关闭此 socket 也将关闭相关的 InputStreamOutputStream
    public void shutdownOutput() 禁用此套接字的输出流。 任何先前写出的数据将被发送,随后终止输出流
    • 代码演示
    public class TCP_Client {
          
          
    	/*
    	 * 客户端代码
    	 */
    	public static void main(String[] args) throws Exception {
          
          
    		//1.创建客户端Socket对象(发送请求)
    		Socket socket = new Socket("127.0.0.1", 8888);
    		
    		//2.获取用于数据传输的的I/O流
    		OutputStream os = socket.getOutputStream();
    		
    		//3.将数据写出
    		os.write("你好,陌生人。".getBytes());
    		
    		//4.客户端读取服务端回写的数据
    		InputStream is = socket.getInputStream();
    		byte[] bys = new byte[1024];
    		int len = is.read(bys);
    		String str = new String(bys, 0, len);
    		System.out.println("客户端发回的信息是:" + str);
    		
    		socket.close();
    	}
    }
    
  • Sever类

    这个类实现了服务器套接字,该对象等待通过网络的请求。

    • 构造方法
    方法 解释
    public ServerSocket(int port) 使用该构造方法在创建 ServerSocket 对象时,就可以将其绑定到一个指定的端口号上,参数 port 就是端口号。
    • 成员方法
    方法 解释
    public Socket accept() 侦听并接受连接,返回一个新的 Socket 对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。
  • 代码演示

    • TestClient类
    public class Client {
          
          
        public static void main(String[] args) throws Exception {
          
          
            while (true) {
          
          
                //创建IO流对象
                Socket socket = new Socket("127.0.0.1", 5555);
                try {
          
          
                    InputStream is = socket.getInputStream();
                    OutputStream os = socket.getOutputStream();
                    //使用打印流对os进行包装
                    PrintStream ps = new PrintStream(os);
                    Scanner sc = new Scanner(System.in);
                    String str = sc.nextLine();
                    ps.println(str);
                    //使用高效率流和转换流对is进行包装
                    BufferedReader br = new BufferedReader(new InputStreamReader(is));
                    String s = br.readLine();
                    System.out.println("接收到服务端会写的信息为:" + s);
                    socket.close();
                }catch (Exception e) {
          
          
                    e.printStackTrace();
                }
            }
        }
    }
    
  • TestSever类

    public class Sever {
          
          
        public static void main(String[] args) throws Exception {
          
          
            ServerSocket sever = new ServerSocket(5555);
            System.out.println("服务器启动!!!");
    
            while (true) {
          
          
                //服务端响应客户端发送过来的请求,用Socket对象接收
                Socket socket = sever.accept();
                System.out.println("接收到请求,响应!");
                new Thread() {
          
          
                    @Override
                    public void run() {
          
          
                        try {
          
          
                            //创建I/O流对象
                            InputStream is = socket.getInputStream();
                            OutputStream os = socket.getOutputStream();
                            //使用高效率流和转换流对is进行包装
                            BufferedReader br = new BufferedReader(new InputStreamReader(is));
                            //使用打印流对os进行包装
                            PrintStream ps = new PrintStream(os);
                            String line = br.readLine();
                            System.out.println("服务器接收到的信息为:" + line);
                            ps.println("Yes, man.");
                            socket.close();
                        } catch (Exception e) {
          
          
                            e.printStackTrace();
                        }
                    }
                }.start();
            }
        }
    }
    

    随笔

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_48489737/article/details/127700973