2023年7月18日,File类,IO流,线程
标签: 2023年7月18日 File类 IO流,线程 博客 51CTO博客
2023-07-19 18:24:33 146浏览
File类
1. 概述
File,是文件和目录路径的抽象表示 File只关注文件本身的信息,而不能操作文件里的内容 。如果需要读取或写入文件内容,必须使用
IO
流来完成。在Java中,
java.io.File
类用于表示文件或目录的抽象路径名。它提供了一组方法,可以用于创建、访问、重命名、删除文件或目录,以及获取文件或目录的属性等操作。
2. File类的使用
- 创建文件对象:
File file = new File("d:/to/file.txt"); // 使用文件路径创建文件对象
- 创建目录:
File dir = new File("d:/to/directory"); // 使用目录路径创建文件对象
boolean created = dir.mkdir(); // 创建目录
- 检查文件或目录是否存在:
boolean exists = file.exists(); // 检查文件是否存在
boolean isDirectory = file.isDirectory(); // 检查是否为目录
boolean isFile = file.isFile(); // 检查是否为文件
- 获取文件或目录的属性:
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();//获取文件的名字,包含了文件的扩展名
- 文件或目录的操作:
boolean renamed = file.renameTo(new File("f:/path/to/file.txt")); // 重命名文件或目录
boolean deleted = file.delete(); // 删除文件或目录
- 遍历目录下的文件和子目录:
File[] files = dir.listFiles(); // 获取目录下的文件和子目录列表
for (File f : files) {
if (f.isFile()) {
// 处理文件
} else if (f.isDirectory()) {
// 处理子目录
}
}
- 绝对路径和相对路径:
绝对路径:带有盘符就是绝对路径
相对路径:相对路径是相对于工程目录进行定位
- 文件查找和定位
文件查找和定位一般我们都是先找到父级文件夹,再找到具体的文件 这种定位方式我们一般都是通过两个参数来体现
//第一种:第一个参数是父级文件夹路径,第二个参数是文件名
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());
- 列出文件夹下的所有文件(下一级)
File folder = new File("d:/io");
//列出文件夹下的所有文件(下一级)
File[] files = folder.listFiles();
if(files != null){
Arrays.stream(files).forEach(System.out::println);
}
结果:
- 递归扫描文件夹
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);
}
}
}
}
}
}
结果:
- 递归删除文件夹
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 Writer
try(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. 字节缓冲流
字节缓冲流(
BufferedInputStream
和BufferedOutputStream
)是Java IO流中的一种类型,它们提供了缓冲功能,可以提高读写操作的效率。字节缓冲流继承自字节流(
InputStream
和OutputStream
),它们通过在内存中创建一个缓冲区(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{
@Override
public 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{
@Override
public 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());
}
}
结果:
分析:
- 首先,在主函数main()中创建了一个新的线程对象thread,使用Lambda表达式定义了线程的执行逻辑。线程的执行逻辑是打印线程名和循环计数器的值,并在每次循环后休眠1秒。
- 通过调用thread.setName("Thread01")方法,将线程的名称设置为"Thread01"。
- 调用thread.getState()方法获取线程的状态。由于线程还没有启动,所以此时线程的状态为NEW(新建)。
- 调用thread.start()方法启动线程的执行。线程开始执行后,会自动调用线程对象中的run()方法。
- 再次调用thread.getState()方法获取线程的状态。此时线程的状态为RUNNABLE(可运行)。
- 接下来,在主线程中使用一个循环打印主线程的名称和循环计数器的值。当循环计数器的值为4时,主线程调用thread.join()方法,等待线程thread执行完成。
- 在线程thread执行完毕后,主线程继续执行。此时再次调用thread.getState()方法获取线程的状态,此时线程的状态为TERMINATED(终止)。
代码的执行流程如下:
- 主线程创建并启动线程thread。
- 主线程执行自己的循环打印操作。
- 当循环计数器的值为4时,主线程调用thread.join()方法,等待线程thread执行完成。
- 线程thread执行自己的循环打印操作。
- 线程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--;
}
}
@Override
public 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();
@Override
public 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
锁的一般模式如下:
- 创建一个
Lock
对象。
Lock lock = new ReentrantLock();
- 在需要同步的代码块前调用
lock()
方法获取锁。
lock.lock();
try {
// 同步代码块
} finally {
// 保证在任何情况下都会释放锁,放在 finally 块中
lock.unlock();
}
lock()
方法会尝试获取锁,如果锁已经被其他线程持有,则当前线程会被阻塞,直到获取到锁为止。
- 在代码块的最后使用
unlock()
方法释放锁。
lock.unlock();
unlock()
方法用于释放锁,让其他等待锁的线程有机会获取锁并执行代码。
相比于 synchronized
,Lock
提供了更多的功能:
- 可以实现公平性:
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对象
@Override
public 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();
}
}
结果:
好博客就要一起分享哦!分享海报
此处可发布评论
评论(0)展开评论
展开评论