2023年7月18日,File类,IO流,线程

File类

1. 概述

File,是文件和目录路径的抽象表示 File只关注文件本身的信息,而不能操作文件里的内容 。如果需要读取或写入文件内容,必须使用IO流来完成。

在Java中,java.io.File 类用于表示文件或目录的抽象路径名。它提供了一组方法,可以用于创建、访问、重命名、删除文件或目录,以及获取文件或目录的属性等操作。

2. File类的使用

  1. 创建文件对象:
File file = new File("d:/to/file.txt");  // 使用文件路径创建文件对象
  1. 创建目录:
File dir = new File("d:/to/directory");  // 使用目录路径创建文件对象
boolean created = dir.mkdir();  // 创建目录
  1. 检查文件或目录是否存在:
boolean exists = file.exists();  // 检查文件是否存在
boolean isDirectory = file.isDirectory();  // 检查是否为目录
boolean isFile = file.isFile();  // 检查是否为文件
  1. 获取文件或目录的属性:
String name = file.getName();  // 获取文件或目录的名称
String absolutePath = file.getAbsolutePath();//获取文件或目录的绝对路径
String path = file.getPath();  // 获取文件或目录的相对路径
long size = file.length();  // 获取文件的大小(字节数byte)
long lastModified = file.lastModified();  // 获取文件或目录的最后修改时间
String name = file.getName();//获取文件的名字,包含了文件的扩展名
  1. 文件或目录的操作:
boolean renamed = file.renameTo(new File("f:/path/to/file.txt"));  // 重命名文件或目录
boolean deleted = file.delete();  // 删除文件或目录
  1. 遍历目录下的文件和子目录:
File[] files = dir.listFiles();  // 获取目录下的文件和子目录列表
for (File f : files) {if (f.isFile()) {// 处理文件} else if (f.isDirectory()) {// 处理子目录}
}
  1. 绝对路径和相对路径:

绝对路径:带有盘符就是绝对路径

相对路径:相对路径是相对于工程目录进行定位

  1. 文件查找和定位

文件查找和定位一般我们都是先找到父级文件夹,再找到具体的文件
这种定位方式我们一般都是通过两个参数来体现

//第一种:第一个参数是父级文件夹路径,第二个参数是文件名
File f1 = new File("d:/io", "test.txt");
System.out.println(f1.exists());
//第二种:第一个参数是父级文件夹对象,第二个参数是文件名
File parent = new File("d:/io");
File f2 = new File(parent, "user.txt");
System.out.println(f2.exists());
  1. 列出文件夹下的所有文件(下一级)
        File folder = new File("d:/io");//列出文件夹下的所有文件(下一级)File[] files = folder.listFiles();if(files != null){Arrays.stream(files).forEach(System.out::println);}

结果:

请添加图片描述

  1. 递归扫描文件夹
package com.wz.io;import java.io.File;public class ScanTest {public static void main(String[] args) {String folder = "d:/io";  // 指定要扫描的文件夹路径scanFolder(new File(folder));  // 调用scanFolder方法开始扫描}public static void scanFolder(File folder) {if (folder.isFile()) {  // 如果是文件,直接打印文件路径System.out.println(folder);} else {// 如果是文件夹,列出文件夹的下一级子文件File[] files = folder.listFiles();if (files != null) {for (File f : files) {if (f.isDirectory()) {  // 如果是子文件夹,递归调用scanFolder方法scanFolder(f);} else {  // 如果是文件,打印文件路径System.out.println(f);}}}}}
}

结果:

请添加图片描述

  1. 递归删除文件夹
package com.wz.io;import java.io.File;public class DeleteTest {public static void main(String[] args) {String folder = "d:/test";  // 指定要删除的文件夹路径deleteFolder(new File(folder));  // 调用deleteFolder方法开始删除}public static void deleteFolder(File folder) {if (folder.isDirectory()) {  // 如果是文件夹File[] files = folder.listFiles();  // 列出文件夹的下一级子文件if (files != null) {for (File f : files) {if (f.isDirectory()) {  // 如果是子文件夹,递归调用deleteFolder方法deleteFolder(f);} else {  // 如果是文件,直接删除f.delete();}}}}folder.delete();  // 删除文件夹本身}
}

IO流

流:是一抽象概念,是对数据传输的总称。也就是说数据在设备间的传输称为流。更具体一点,是内存与存储设备之间传输数据的通道。

IO的概念:IO = input Output,也就是输入和输出,参照物就是内存

针对内存来说,把数据读入内存称为输入,将内存中的数据写出去就是输出。

请添加图片描述

IO按照读的方式分为字节流和字符流。字节流每次读取的基本单位是字节,字符流每次读取的单位是一个字符=2个字节,因此字节流每次读取8位,字符流每次读取16位。

请添加图片描述

1. 字节流

1. OutputStream输出流(写数据)

package com.wz.io01_class;import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;public class OutputStreamTest {public static void main(String[] args) {String path = "d:/io/output.txt";File file = new File(path);//判断父级目录是否存在File parentFile = file.getParentFile();if (!parentFile.exists()){parentFile.mkdirs();}//try-with-resources=>JDK1.7提供的新特性 IO流在使用了之后会自动关闭//try-with-resources try后面的()中可以写多行代码,但是必须以分号分割开,最后一行代码的//分号可以省略。能够写入括号中的内容必须是实现了AutoClosable接口的流的构建try (OutputStream os = new FileOutputStream(file,true)) {String content = "hello world!!";final byte[] data = content.getBytes();//获取字符串的byte数据os.write(data);//写入数据os.flush();//强制将通道中的数据刷出,写入文件}catch (IOException e){e.printStackTrace();}}
}

2. InputStream输入流(读数据)

package com.wz.io01_class;import java.io.*;public class InputStreamTest {public static void main(java.lang.String[] args) {File file = new File("d:/io/output.txt");try (InputStream in = new FileInputStream(file);){//如果文件比较大,我们需要构建一个容器,来反复读取文件内容byte[] buffer = new byte[5];int len;while ((len = in.read(buffer)) != -1){System.out.println(new java.lang.String(buffer,0, len));}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {throw new RuntimeException(e);}}
}

2. 字符流

字符流(Character Stream)是Java中用于以字符为单位进行读写操作的输入输出流。与字节流不同,字符流以字符作为数据处理的基本单位,而不是字节。

Java提供了两个主要的字符流类:Reader和Writer。这些类通常用于处理字符数据,例如文本文件或网络连接中的文本数据。

Reader类是抽象类,它的子类用于从字符源读取字符数据。常用的Reader子类包括FileReader(从文件中读取字符)、InputStreamReader(从字节流中读取字符)等。

package com.wz.char_class;import java.io.*;public class ReaderTest {public static void main(String[] args) {//字符流的顶层父类,Reader Writertry(Reader reader = new FileReader("d:/io/output.txt");Writer writer = new FileWriter("d:/io/output03.txt")){char[] buffer = new char[2048];int len;while ((len=reader.read(buffer))!=-1){System.out.println(new String(buffer,0,len));writer.write(buffer,0,len);}}catch (IOException e){e.printStackTrace();}}
}

这段代码首先通过FileReader创建一个字符输入流reader,用于读取文件"d:/io/output.txt"的内容。然后,通过FileWriter创建一个字符输出流writer,用于写入内容到文件"d:/io/output03.txt"。

在代码的主体部分,我们使用一个字符数组buffer作为缓冲区,大小设置为2048个字符。reader.read(buffer)方法会将文件的内容读取到buffer中,并返回实际读取的字符数(或者返回-1表示已到达文件末尾)。

然后,通过new String(buffer, 0, len)buffer中的字符转换为字符串,并在控制台打印该字符串。

最后,使用writer.write(buffer, 0, len)buffer中的字符写入到输出文件中。

try-with-resources语句用于自动关闭字符流。这样可以确保在代码块结束时,无论是否发生异常,都会正确关闭字符流。

3. 字节缓冲流

字节缓冲流BufferedInputStreamBufferedOutputStream)是Java IO流中的一种类型,它们提供了缓冲功能,可以提高读写操作的效率。

字节缓冲流继承自字节流InputStreamOutputStream),它们通过在内存中创建一个缓冲区(byte数组)来存储数据。当使用字节缓冲流进行读取或写入操作时,数据会先被读取到缓冲区中,然后从缓冲区中读取或写入到目标位置。这样可以减少实际的IO操作次数,提高读写效率。

BufferedInputStream类提供了以下常用方法:

int read()//从输入流中读取一个字节数据,并返回其整数表示(0-255),如果已经读取到流的末尾,则返回-1。
int read(byte[] buffer)//从输入流中读取一定数量的字节数据,并将其存储到指定的字节数组buffer中,返回实际读取的字节数,如果已经读取到流的末尾,则返回-1。
void close()//关闭输入流。

BufferedOutputStream类提供了以下常用方法:

void write(int byteValue)// 向输出流中写入一个字节数据。
void write(byte[] buffer)// 将指定的字节数组buffer中的数据写入到输出流中。
void flush()// 刷新输出流,将缓冲区中的数据立即写入到目标位置。
void close()//关闭输出流。

在使用字节缓冲流进行写入操作时,数据并不会立即写入到目标位置,而是先存储在缓冲区中。如果需要立即将数据写入到目标位置,可以调用flush方法刷新输出流。

字节缓冲流在处理大量数据时能够提供较高的读写效率,特别适用于频繁读写小块数据的场景。在进行文件复制、网络传输等操作时,使用字节缓冲流可以显著提升性能。

  • 利用字节缓冲流进行文件拷贝
package com.wz.charBufferStream;import java.io.*;public class Test01 {public static void main(String[] args) {String sourceFile ="d:/io/IO流理解图.png";String destFile = "d:/io/copy.png";copyFile(sourceFile,destFile);}public static void copyFile(String sourceFile,String destFile){//创建一个File对象,表示目标文件。File file = new File(destFile);//获取目标文件的父目录File parentFile = file.getParentFile();//判断父目录是否存在if (!parentFile.exists()) parentFile.mkdirs();//创建一个BufferedInputStream对象,并将其初始化为一个FileInputStream对象的包装器,用于读取源文件的数据。//创建一个BufferedOutputStream对象,并将其初始化为一个FileOutputStream对象的包装器,用于写入目标文件的数据。try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(sourceFile));BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile))) {//创建一个字节数组作为缓冲区,用于存储从源文件读取的数据。byte[] buffer = new byte[2048];while(true){int len = bis.read(buffer);if (len==-1)break;bos.write(buffer,0,len);bos.flush();}} catch (IOException e) {e.printStackTrace();}}
}

4. 字符缓冲流

字符缓冲流是Java IO提供的一种高效的字符流,用于处理字符数据。它是基于字符流的装饰器模式实现的,通过在字符流的基础上添加缓冲功能来提高读写性能。
在Java中,字符缓冲流有两个主要的类:BufferedReader和BufferedWriter。


  • BufferedReader

BufferedReader是字符缓冲输入流,它提供了一些额外的方法来增强字符输入流的功能。它可以缓冲字符,允许高效的读取字符数据。它的构造方法可以接受一个字符输入流作为参数,然后创建一个带有缓冲功能的字符输入流。


常用方法:

readLine()//读取一行字符数据并返回一个字符串,如果到达文件末尾,则返回null。
read()//读取一个字符。
close()//关闭流,同时会关闭基础的字符输入流。
  try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {String line;while ((line = reader.readLine()) != null) {System.out.println(line);}} catch (IOException e) {e.printStackTrace();}
  • BufferedWriter

BufferedWriter是字符缓冲输出流,它提供了一些额外的方法来增强字符输出流的功能。它可以缓冲字符并提供高效的写入操作。它的构造方法可以接受一个字符输出流作为参数,然后创建一个带有缓冲功能的字符输出流。


常用方法:

write(String str)//将字符串写入流中。
newLine()//写入一个行分隔符。
flush()//刷新缓冲区,将数据写入基础的字符输出流。
close()//关闭流,同时会关闭基础的字符输出流。 
  try (BufferedWriter writer = new BufferedWriter(new FileWriter("file.txt"))) {writer.write("Hello, world!");writer.newLine();writer.write("This is a test.");writer.flush();} catch (IOException e) {e.printStackTrace();}

通过使用字符缓冲流,可以提高字符输入输出操作的性能,尤其是在处理大量字符数据时,它能减少实际IO操作的次数,从而提高程序的效率。

5. 数据流

在Java的I/O流中,数据流(Data Stream)是一种流类别,用于处理基本数据类型和字符串的输入和输出。数据流提供了一种方便的方式来读取和写入原始数据类型(如int,double,boolean等)以及字符串,而无需手动进行数据类型转换。
Java的数据流操作由两个主要类组成:

  • 数据流的部分方法
void writeBoolean(boolean v) throws IOException;//将布尔值作为1个字节写入底层输出通道
void writeByte(int v) throws IOException;//将字节写入底层输出通道
void writeShort(int v) throws IOException;//将短整数作为2个字节(高位在前)写入底层输出通道
void writeChar(int v) throws IOException;//将字符作为2个字节写(高位在前)入底层输出通道
void writeInt(int v) throws IOException;//将整数作为4个字节写(高位在前)入底层输出通道
void writeLong(long v) throws IOException;//将长整数作为8个字节写(高位在前)入底层输出通道
void writeFloat(float v) throws IOException;//将单精度浮点数作为4个字节写(高位在前)入底层输出通道
void writeDouble(double v) throws IOException;//将双精度浮点数作为8个字节写(高位在前)入底层输出通道
void writeUTF(String s) throws IOException;//将UTF-8编码格式的字符串以与机器无关的方式写入底层输出通道。
package com.wz.io01;import java.io.*;public class Test01 {public static void main(String[] args) {dataStream();}private static void dataStream(){String path = "d:/io/test.txt";try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(path));){dos.writeBoolean(false);dos.writeInt(1);dos.writeByte(2);dos.writeShort(3);dos.writeLong(4);dos.writeFloat(5.0f);dos.writeDouble(6.0);dos.writeChar('a');dos.writeUTF("Hello World");dos.flush();} catch (IOException e) {e.printStackTrace();}try (DataInputStream dis = new DataInputStream(new FileInputStream(path))){boolean b = dis.readBoolean();System.out.println(b);int i = dis.readInt();System.out.println(i);byte b1 = dis.readByte();System.out.println(b1);short s = dis.readShort();System.out.println(s);long l = dis.readLong();System.out.println(l);float v = dis.readFloat();System.out.println(v);double v1 = dis.readDouble();System.out.println(v1);char c = dis.readChar();System.out.println(c);String s1 = dis.readUTF();System.out.println(s1);} catch (IOException e) {e.printStackTrace();}}
}

结果:

请添加图片描述

注意:

在数据流中,读取顺序必须与写入顺序保持一致。这是因为数据流中的数据是按照特定的格式写入的,如果读取顺序与写入顺序不一致,就会导致数据读取错误或解析错误。
当使用数据流进行读取时,数据流会按照先后顺序解析数据,并将其转换为相应的数据类型。如果读取顺序与写入顺序不一致,例如尝试先读取一个整数,然后读取一个字符串,这样会导致读取出的数据类型不匹配,造成解析错误。

6. 序列化

将一个对象从内存中写入磁盘文件中的过程称之为序列化,反之就是反序列化。序列化必须要求该对象所有类型实现序列化的接口Serializable


注意: 序列化和反序列化的对象版本一致性,即序列化期间的Java类版本与反序列化期间的Java类版本应保持一致。如果版本不一致,可能会导致对象反序列化失败或数据丢失。

Serializable接口仅仅只用于标识序列化


实现了Serializable接口的类可以通过ObjectOutputStream类进行序列化,通过ObjectInputStream类进行反序列化。

package com.wz.io02;import java.io.*;public class Test {public static void main(String[] args) {serialize();}public static void serialize(){Student student = new Student("ZhangSan", 18, '男');try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/io/test.txt"))){oos.writeObject(student);oos.flush();}catch (IOException e){e.printStackTrace();}try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/io/test.txt"))) {Student s = (Student) ois.readObject();System.out.println(s);} catch (IOException e) {throw new RuntimeException(e);} catch (ClassNotFoundException e) {throw new RuntimeException(e);}}
}

进程和线程

1. 概述

  • 什么是进程?

进程是操作系统进行资源分配和调度的基本单位。每个进程都是独立运行的,相互之间互不干扰。进程可以包含一个或多个线程。

  • 什么是线程?

是进程内的执行单元。一个进程可以包含多个线程,它们共享进程的内存空间和系统资源。线程是程序执行的最小单位,可以独立执行特定的任务。线程共享进程的上下文,包括内存、文件句柄和其他系统资源。多个线程可以在同一时间内并发执行,提高程序的并发性和效率。

  • 并发和并行的区别?

并发(Concurrency) 是指多个任务在同一时间段内交替执行的能力。在并发中,多个任务可以在同一时间段内启动、执行和完成,但并不一定是同时进行。并发的目标是提高系统的吞吐量和响应性,通过合理地调度和利用资源,使得多个任务能够共享系统资源并以合理的方式并发执行。在并发中,任务之间可能会相互交互和依赖,需要进行同步和协调。

并行(Parallelism) 是指多个任务在同一时间点同时执行的能力。在并行中,多个任务可以同时启动、执行和完成,每个任务都在独立的处理单元(如多个CPU核心)上并行执行。并行的目标是通过同时执行多个任务来提高系统的计算能力和处理速度。在并行中,任务之间通常是独立的,彼此之间没有交互和依赖。

并发是指多个任务在同一时间段内交替执行,而并行是指多个任务在同一时间点同时执行

注意:并发和并行并不是互斥的概念。在某些情况下,可以同时使用并发和并行来提高系统的性能和效率。例如,可以使用并发来处理多个用户请求,而在每个请求内部使用并行来加速计算或处理密集型任务。

2. 线程的创建

  • 继承Thread类
package com.wz.Thread01;
public class Mythread extends Thread{@Overridepublic void run() {System.out.println("线程执行");}
}public class Test01 {public static void main(String[] args) {Mythread mythread = new Mythread();mythread.start();}
}

创建一个继承自Thread类的子类,重写run()方法来定义线程的执行逻辑。然后通过创建子类的实例并调用start()方法来启动线程。

  • 实现Runnable接口
package com.wz.Thread02;
public class MyRunnable implements Runnable{@Overridepublic void run() {System.out.println("线程执行");}
}package com.wz.Thread02;
public class MyRunnableTest {public static void main(String[] args) {MyRunnable runnable = new MyRunnable();Thread thread = new Thread(runnable);thread.start();}
}

创建一个实现了Runnable接口的类,实现run()方法来定义线程的执行逻辑。然后通过创建Runnable实例,并将其作为参数传递给Thread类的构造函数来创建线程

  • 使用匿名内部类
package com.wz.Thread03;public class ThreadTest {public static void main(String[] args) {
//        Thread thread = new Thread(new Runnable() {
//            @Override
//            public void run() {
//                System.out.println("线程run"+Thread.currentThread().getName());
//            }
//        },"线程A");Thread thread = new Thread(() -> System.out.println("线程run"+Thread.currentThread().getName()),"线程A");thread.start();}
}

3. 线程中的方法

public synchronized void start();//启动线程但不一定会执行
public final String getName();//获取线程名称
public final synchronized void setName(String name);//设置线程的名称
public final void setPriority(int newPriority);//设置线程的优先级
public final int getPriority();//获取线程的优先级
public final void join() throws InterruptedException;//等待线程执行完成
//等待线程执行给定的时间(单位毫秒)
public final synchronized void join(long millis) throws InterruptedException;
//等待线程执行给定的时间(单位毫秒、纳秒)
public final synchronized void join(long millis, int nanos) throws InterruptedException;
public long getId();//获取线程的ID
public State getState();//获取线程的状态
public boolean isInterrupted();//检测线程是否被打断
public void interrupt();//打断线程public static native Thread currentThread();//获取当前运行的线程
public static boolean interrupted();//检测当前运行的线程是否被打断
public static native void yield();//暂停当前运行的线程,然后再与其他线程争抢资源,称为线程礼让
//使当前线程睡眠给定的时间(单位毫秒)
public static native void sleep(long millis) throws InterruptedException;
//使当前线程睡眠给定的时间(单位毫秒、纳秒)
public static void sleep(long millis, int nanos) throws InterruptedException;
package com.wz.Thread04;public class Test {public static void main(String[] args) {Thread thread = new Thread(() -> {for (int i = 0; i < 10; i++) {System.out.println("线程 "+Thread.currentThread().getName()+"正在执行"+i);try {//休眠Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"线程一");//修改线程名称thread.setName("Thread01");//获取线程状态System.out.println(thread.getState());thread.start();//获取线程状态System.out.println(thread.getState());for (int i = 0; i < 10; i++) {System.out.println("线程"+Thread.currentThread().getName()+"正在执行"+i);if (i==4){try {thread.join();} catch (InterruptedException e) {throw new RuntimeException(e);}}}//获取线程状态System.out.println(thread.getState());}
}

结果:

请添加图片描述

分析:

  1. 首先,在主函数main()中创建了一个新的线程对象thread,使用Lambda表达式定义了线程的执行逻辑。线程的执行逻辑是打印线程名和循环计数器的值,并在每次循环后休眠1秒。
  2. 通过调用thread.setName(“Thread01”)方法,将线程的名称设置为"Thread01"。
  3. 调用thread.getState()方法获取线程的状态。由于线程还没有启动,所以此时线程的状态为NEW(新建)。
  4. 调用thread.start()方法启动线程的执行。线程开始执行后,会自动调用线程对象中的run()方法。
  5. 再次调用thread.getState()方法获取线程的状态。此时线程的状态为RUNNABLE(可运行)。
  6. 接下来,在主线程中使用一个循环打印主线程的名称和循环计数器的值。当循环计数器的值为4时,主线程调用thread.join()方法,等待线程thread执行完成。
  7. 在线程thread执行完毕后,主线程继续执行。此时再次调用thread.getState()方法获取线程的状态,此时线程的状态为TERMINATED(终止)。

代码的执行流程如下:

  1. 主线程创建并启动线程thread。

  2. 主线程执行自己的循环打印操作。

  3. 当循环计数器的值为4时,主线程调用thread.join()方法,等待线程thread执行完成。

  4. 线程thread执行自己的循环打印操作。

  5. 线程thread执行完毕后,主线程继续执行自己的循环打印操作。

4. 线程同步 (synchronized)

synchronized 是 Java 中用于实现线程同步的关键字。它可以用来修饰方法或代码块,以确保在同一时刻只有一个线程可以访问被修饰的代码。

synchronized 的作用是获取对象的锁,只有获取到锁的线程才能执行被 synchronized 修饰的代码,其他线程则需要等待锁的释放。当一个线程执行完 synchronized 代码块或方法后,会释放锁,其他等待锁的线程将有机会获取锁并执行代码。

使用 synchronized 可以有效地保证多线程环境下的数据安全性,避免多个线程同时访问共享资源导致的数据竞争和不一致性。然而,过度使用 synchronized 也可能导致性能问题,因为只有一个线程能够执行被锁定的代码,其他线程需要等待,可能会造成线程的阻塞和效率降低。

卖票案例:某火车站有10张火车票在3个窗口售卖

  • 同步方法
package com.wz.Thread05;public class Test {/*** 某火车站有10张火车票在3个窗口售卖*/public static void main(String[] args) {Task task = new Task();Thread t1 = new Thread(task,"窗口1");Thread t2 = new Thread(task,"窗口2");Thread t3 = new Thread(task,"窗口3");t1.start();t2.start();t3.start();}static class Task implements Runnable{private int totalTickets = 10;private synchronized void saleTicket(){if (totalTickets>0){String name = Thread.currentThread().getName();System.out.println(name+"正在售卖车票"+totalTickets);totalTickets--;}}@Overridepublic void run() {while (true){saleTicket();if (totalTickets==0) break;try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}}
}
  • 同步代码快
package com.wz.Thread06;public class Test {/*** 某火车站有10张火车票在3个窗口售卖*/public static void main(String[] args) {Task task = new Task();Thread t1 = new Thread(task, "窗口1");Thread t2 = new Thread(task, "窗口2");Thread t3 = new Thread(task, "窗口3");t1.start();t2.start();t3.start();}static class Task implements Runnable {private int totalTickets = 10;private Object obj = new Object();@Overridepublic void run() {while (true) {synchronized (obj) {if (totalTickets > 0) {String name = Thread.currentThread().getName();System.out.println(name + "正在售卖车票" + totalTickets);totalTickets--;}if (totalTickets == 0) break;try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}}}}}
}

使用obj作为锁对象的原因:

使用 obj 对象作为锁的好处是它是唯一的,所有线程都可以通过该对象来实现同步。

锁对象可以是任意对象,只要确保多个线程共享同一个对象即可。在这个例子中,使用了一个简单的 Object 对象作为锁,但也可以使用其他对象,比如自定义的对象或类的实例对象。

使用 synchronized 关键字锁定一个对象,可以确保在同一时刻只有一个线程可以进入被锁定的代码块,从而保证了线程的安全性。

5. 线程同步 (Lock)

Lock 是 Java 提供的一个更灵活和可扩展的锁机制,相比于 synchronized 关键字,它提供了更多的功能和灵活性。


Lock 接口定义了一组用于获取和释放锁的方法,其中最常用的实现类是 ReentrantLock

使用 Lock 锁的一般模式如下:

  1. 创建一个 Lock 对象。
Lock lock = new ReentrantLock();
  1. 在需要同步的代码块前调用 lock() 方法获取锁。
lock.lock();
try {// 同步代码块
} finally {// 保证在任何情况下都会释放锁,放在 finally 块中lock.unlock();
}

lock() 方法会尝试获取锁,如果锁已经被其他线程持有,则当前线程会被阻塞,直到获取到锁为止。

  1. 在代码块的最后使用 unlock() 方法释放锁。
lock.unlock();

unlock() 方法用于释放锁,让其他等待锁的线程有机会获取锁并执行代码。


相比于 synchronizedLock 提供了更多的功能:

  • 可以实现公平性:ReentrantLock 的构造函数可以传入一个 boolean 值,用于指定是否公平获取锁。当设置为公平锁时,线程会按照申请锁的顺序获取锁,避免线程饥饿现象。
  • 支持可中断的获取锁:lock() 方法可以响应中断,当一个线程在等待锁的过程中被中断,它可以选择继续等待获取锁或者放弃锁。
  • 支持超时获取锁:tryLock() 方法可以尝试获取锁,如果在指定的时间内没有获取到锁,可以根据返回结果做相应的处理。
  • 支持多个条件的等待和唤醒:Lock 提供了 Condition 接口,可以通过 newCondition() 方法创建多个条件对象,线程可以在不同的条件上等待和唤醒。

注意:使用 Lock 需要手动释放锁,否则可能导致死锁的发生。因此,在使用 Lock 时,一般会将获取锁和释放锁的代码包裹在 try-finally 块中,确保锁的释放。


卖票案例:某火车站有10张火车票在3个窗口售卖

package com.wz.Thread07;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class Test {public static void main(String[] args) {Task task = new Task();Thread t1 = new Thread(task, "窗口1");Thread t2 = new Thread(task, "窗口2");Thread t3 = new Thread(task, "窗口3");t1.start();t2.start();t3.start();}static class Task implements Runnable{private int totalTickets = 10;private Lock lock = new ReentrantLock();//创建一个lock对象@Overridepublic void run() {while (true){if (lock.tryLock()){try{if (totalTickets>0){String name = Thread.currentThread().getName();System.out.println(name+"售卖车票"+totalTickets);totalTickets--;}}finally {lock.unlock();}}if (totalTickets==0)break;try {Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}}}
}

使用 tryLock() 方法来尝试获取锁,而不是直接使用 lock() 方法阻塞等待获取锁的好处是,如果锁被其他线程持有,当前线程不会被阻塞,而是可以继续执行其他操作。

需要注意的是,使用 tryLock() 方法获取锁后,需要在 finally 块中调用 unlock() 方法来确保锁的释放,以防止死锁的发生。

6. 线程通信

小明每次没有生活费了就给他的爸爸打电话,他的爸爸知道了后就去银行存钱,钱存好了之后就通知小明去取。

分析:

创建账户类:属性包括姓名name,余额balace,方法包括存钱save和取钱draw

创建存钱任务,创建取钱任务

package com.wz.Thread09;public class Account {String name;double balance;boolean flag = false;public Account(String name) {this.name = name;}//存钱public synchronized void save(int money){if (flag){//如果存了System.out.println(name+"的爸爸等待存钱通知!");try {wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}else {balance+=money;System.out.println(name+"的爸爸存了"+money+"元");flag=true;notifyAll();}}public synchronized void draw(int money){if (flag){if (balance<money){System.out.println(name+"提醒爸爸存钱!");flag=false;notifyAll();}else {balance-=money;System.out.println(name+"取了"+money+"yuan");}}else {try {System.out.println(name+"等待爸爸存钱");wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
package com.wz.Thread09;public class Test {public static void main(String[] args) {Account account = new Account("小明");Thread t1 = new Thread(() -> {while (true){account.draw(500);try {Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();Thread t2 = new Thread(() -> {while (true){account.save(800);try {Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t2.start();}
}

结果:

请添加图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

selenium.chrome怎么写扩展拦截或转发请求?

Selenium WebDriver 是一组开源 API&#xff0c;用于自动测试 Web 应用程序&#xff0c;利用它可以通过代码来控制chrome浏览器&#xff01; 有时候我们需要mock接口的返回&#xff0c;或者拦截和转发请求&#xff0c;今天就来实现这个功能。 代码已开源&#xff1a; https:/…

HTML语法

文章目录 前言HTML 文件基本结构常见标签标签种类特殊符号图片链接a链接 双标签链接 列表表格 &#xff1a;表单多行文本域: 前言 HTML是有标签组成的 <body>hello</body>大部分标签成对出现. 为开始标签, 为结束标签. 少数标签只有开始标签, 称为 “单标签”. 开…

Helm 安装prometheus-stack 使用local pv持久化存储数据

目录 背景&#xff1a; 环境准备&#xff1a; 1. 磁盘准备 2. 磁盘分区格式化 local storage部署 1. 节点打标签 2. 创建local pv storageClass和prometheus-pv Prometheus-stack部署 1. 下载helm chart包 2. values.yaml 参数解释 3. 部署prometheus-stack 4. 查看…

Baichuan-13B:130亿参数的开源语言模型,引领中文和英文benchmark

Baichuan-13B: 一个强大的开源大规模语言模型 标题&#xff1a;Baichuan-13B&#xff1a;130亿参数的开源语言模型&#xff0c;引领中文和英文benchmark Baichuan-13B是由百川智能开发的一个开源大规模语言模型项目&#xff0c;包含了130亿参数。该模型在中文和英文的权威ben…

【广州华锐互动】VR地铁消防逃生路线演练系统

随着城市轨道交通的不断发展&#xff0c;事故应急演练的重要性也越来越受到重视。而VR技术的应用&#xff0c;为地铁消防逃生路线演练带来了许多亮点&#xff0c;包括以下几个方面&#xff1a; 首先&#xff0c;VR技术可以提供高度真实的模拟场景。在传统的事故应急演练中&…

ipad可以使用其他品牌的手写笔吗?平价ipad手写笔推荐

我是一个拥有多年数码经验的爱好者&#xff0c;我知道一些关于电容笔的知识。我认为&#xff0c;苹果原装的电容笔与普通的电容笔最大的不同之处&#xff0c;就是其所带来的压感不同。由于“重力压感”的特殊性&#xff0c;我们能很快地把色彩填充到画面中。除此之外&#xff0…

亿发软件:数字化大中型制造企业生产管理应用,实现智慧工厂信息化

随着信息技术与制造业的深度协调&#xff0c;作为企业发展的趋势&#xff0c;大中型制造企业需要拥抱信息化建设。通过运用信息技术和数字化运营&#xff0c;大中型制造企业的生产、设计、经营、管理、后续服务等都实现自动化、智能化。大中型制造企业信息化建设解决方案&#…

uniapp中axios封装和环境配置

axios版本 最好锁定版本&#xff0c;避免bug axios-miniprogram-adapter这个依赖主要是适配小程序网络请求的适配器&#xff0c;为了解决uniapp 适配axios请求&#xff0c;避免报adapter is not a function错误 cnpm i axios0.26.0 axios-miniprogram-adapter 配置adapter函…

bean的生命周期

生命周期&#xff1a;从生到死的过程。那么对于bean来说就是从创建到销毁的过程。 普通的Java对象的创建由我们new创建&#xff0c;然后在不用的时候&#xff0c;java回收机制会自动回收。那么bean呢&#xff1f; bean是spring中的对象&#xff0c;和普通对象不一样的就是bea…

Unity游戏源码分享-Unity手游火柴忍者游戏StickmanDojo

Unity游戏源码分享-Unity手游火柴忍者游戏StickmanDojo 项目地址&#xff1a;https://download.csdn.net/download/Highning0007/88050234

蒲公英打包环境搭建碰到问题

一&#xff1a;证书那边选择手动&#xff0c;不要自动&#xff0c;——》debug配置dev证书&#xff0c;release配置ad-hoc证书 二&#xff1a;证书有时候不生效&#xff0c;删除重新下载。~/Library/MobileDevice/Provisioning Profiles 三&#xff1a;更新测试手机时&#…

OpenCv色彩空间

目录 一、RGB 二、图像处理入门 三、色彩空间的转换 一、RGB 在表示图像时&#xff0c;有多种不同的颜色模型&#xff0c;但最常见的是红、绿、蓝(RGB) 模型RGB 模型是一种加法颜色模型&#xff0c;其中原色 (在RGB模型中&#xff0c;原色是红色 R、绿色 G 和蓝色 B)混合在…

设计模式之享元模式

写在前面 本文看下一种结构型设计模式&#xff0c;享元模式。 1&#xff1a;介绍 1.1&#xff1a;什么时候使用享元模式 当程序需要大量的重复对象&#xff0c;并且这些大量的重复对象只有部分属性不相同&#xff0c;其他都是相同的时候&#xff0c;就可以考虑使用享元设计…

【天工Godwork精品教程】天工3.1.7安装教程(附Godwork完整版下载地址)

本文讲解天工3.1.7安装过程(附Godwork完整版网盘下载地址)。 文章目录 一、天工3.1.7安装教程1. 安装GodWork-AT 3.1.72. 安装GodWork-AT 3.1.7补丁3. 安装GodWork-EOS-Setup-2017B-12314. 安装GodWork-EOS补丁5. 运行godwokr软件6. 生成ZC码7. 输入ZC码8. eos插件调用二、天…

Linux·从 URL 输入到页面展现到底发生什么?

打开浏览器从输入网址到网页呈现在大家面前&#xff0c;背后到底发生了什么&#xff1f;经历怎么样的一个过程&#xff1f;先给大家来张总体流程图&#xff0c;具体步骤请看下文分解&#xff01; 总体来说分为以下几个过程: DNS 解析:将域名解析成 IP 地址TCP 连接&#xff1a…

Vue中的侦听器:数据变化的秘密揭示

一、侦听器&#xff1a;vue中想监听数据的变化 &#x1f680;&#xff08;一&#xff09;侦听器watch 如何侦听到某个变量值改变呢&#xff1f;使用watch配置项&#x1f6a7;&#x1f6a7;&#x1f6a7;watch&#xff1a;可以侦听到data/computed属性值的改变。语法&#xff…

使用 Pytest 运行 yaml 文件来驱动 Appium 自动化测试

目录 前言&#xff1a; 获取 yaml 文件 YamlTest 测试类 Appium 初始化 Pytest 测试类 自定义 runtest demo&#xff1a; 自定义错误输出 Yaml 使用方式规则 前言&#xff1a; 使用Pytest来运行yaml文件来驱动Appium自动化测试是一种方便且灵活的方法。通过将测试数据…

为你精选5款体验极佳的原型设计工具!

在绘制原型图的过程中&#xff0c;使用一款的简单易操作的原型设计工具是非常重要的&#xff0c;本文精选了5款好用的原型工具与大家分享&#xff0c;一起来看看吧&#xff01; 1、即时设计 即时设计是国内很多设计师都在用的原型设计工具&#xff0c;同时它也是国产的原型设…

想知道搭建知识库有什么重点?看这篇就够了

在目前这个提倡无纸化的时代&#xff0c;搭建一个知识库已经是一种潮流。无论是个人还是企业来说&#xff0c;都是特别重要的一个工具。今天looklook就从搭建知识库的重点这方面来展开&#xff0c;详细地告诉大家该如何成功搭建一个完善的知识库。 搭建知识库的重点 1.建立素材…

数据可视化:揭开数据的视觉奇迹

随着大数据时代的到来&#xff0c;我们面临着海量的数据&#xff0c;如何从中获取有价值的信息成为一项重要的挑战。数据可视化作为一种强大的工具&#xff0c;通过图表、图形和交互界面&#xff0c;将数据转化为可视化的形式&#xff0c;帮助我们更好地理解和分析数据。 数据可…