Rumah > Java > javaTutorial > Senario dan teknik aplikasi Java multi-threading dan strim IO

Senario dan teknik aplikasi Java multi-threading dan strim IO

PHPz
Lepaskan: 2023-04-22 12:10:20
ke hadapan
713 orang telah melayarinya

Aplikasi berbilang benang dan penstriman Java

Baru-baru ini saya melihat contoh penggunaan berbilang benang untuk memuat turun fail. Saya mendapati ia sangat menarik, jadi saya meneroka dan cuba menggunakan berbilang benang untuk menyalin fail tempatan. Selepas selesai menulis, saya mendapati bahawa kedua-duanya sebenarnya sangat serupa Sama ada penyalinan fail tempatan atau muat turun berbilang benang rangkaian, penggunaan aliran adalah sama. Untuk sistem fail tempatan, aliran input diperoleh daripada fail dalam sistem fail tempatan Untuk sumber rangkaian, ia diperoleh daripada fail pada pelayan jauh.

Nota: Walaupun ramai yang telah menulis kod muat turun berbilang utas ini, tidak semua orang mungkin memahaminya, ha .

Faedah jelas menggunakan multi-threading ialah: Gunakan CPU melahu untuk mempercepatkan. Tetapi ambil perhatian bahawa lebih banyak utas, lebih baik Walaupun nampaknya n utas dimuat turun bersama, setiap utas memuat turun sebahagian kecil, dan masa muat turun akan menjadi 1/n. Ini adalah pemahaman yang sangat mudah, sama seperti 100 hari untuk seorang membina rumah, tetapi hanya 1/10 hari untuk 10,000 orang? (Ini adalah keterlaluan, haha!)

Bertukar antara benang juga memerlukan overhed sistem dan bilangan utas mesti dikawal dalam julat yang munasabah.

RamdomAccessFile

Kelas ini agak unik. Ia boleh membaca data daripada fail dan menulis data ke fail. Tetapi ia bukan subkelas OutputStream dan InputStream, ia adalah kelas yang melaksanakan kedua-dua antara muka DataOutput dan DataInput ini.

Pengenalan dalam API:

Instance kelas ini menyokong membaca dan menulis fail akses rawak. Mengakses fail secara rawak berkelakuan seperti sejumlah besar bait yang disimpan dalam sistem fail. Terdapat jenis kursor, atau indeks ke dalam tatasusunan tersirat, dipanggil penuding fail, operasi input dibaca bait bermula pada penuding fail dan melanjutkan penuding fail melepasi bait yang dibaca. Operasi output juga tersedia jika fail akses rawak dibuat dalam mod baca/tulis operasi output menulis bait bermula pada penuding fail dan memajukan penuding fail kepada bait yang ditulis. Operasi output menulis ke sisi semasa tatasusunan tersirat menyebabkan tatasusunan dikembangkan. Penunjuk fail boleh dibaca dengan kaedah getFilePointer dan ditetapkan oleh kaedah cari .

Jadi, perkara yang paling penting tentang kelas ini ialah kaedah cari dengan menggunakan kaedah carian, anda boleh mengawal kedudukan penulisan, jadi lebih mudah untuk melaksanakan multi-threading. Oleh itu, sama ada penyalinan fail tempatan atau muat turun berbilang benang rangkaian, kelas ini diperlukan.

Idea khusus ialah: Mula-mula gunakan RandomAccessFile untuk mencipta objek Fail, dan kemudian tetapkan saiz fail ini. (Ya, ia boleh menetapkan saiz fail secara langsung.) Tetapkan fail ini agar sama dengan fail yang anda ingin salin atau muat turun. (Walaupun kami tidak menulis data pada fail ini, fail telah dibuat.) Bahagikan fail kepada beberapa bahagian dan gunakan benang untuk menyalin atau memuat turun kandungan setiap bahagian .

Ini agak serupa dengan menimpa fail Jika fail sedia ada mula menulis data dari kepala fail dan menulis ke hujung fail, maka fail asal tidak akan wujud lagi dan menjadi fail baharu.

Tetapkan saiz fail:

	private static void createOutputFile(File file, long length) throws IOException {
		try (   
			RandomAccessFile raf = new RandomAccessFile(file, "rw")){
			raf.setLength(length);
		}
	}
Salin selepas log masuk

Gunakan gambar untuk menggambarkan: Gambar ini mewakili fail dengan saiz 8191 bait: Setiap saiz bahagian ialah: 8191 / 4 = 2047 bait

Bahagikan fail ini kepada empat bahagian, setiap bahagian menggunakan benang untuk menyalin atau memuat turun, di mana setiap anak panah mewakili kedudukan muat turun permulaan sesuatu utas. Saya sengaja membiarkan bahagian terakhir tidak ditetapkan kepada 1024 bait kerana fail jarang betul dibahagikan dengan 1024 bait. (Sebab mengapa saya menggunakan 1024 bait adalah kerana saya akan membaca 1024 bait setiap kali. Jika 1024 bait dibaca, jika tidak bilangan bait yang sepadan akan ditulis).

Menurut rajah ini, setiap urutan memuat turun 2047 bait, maka jumlah bilangan bait yang dimuat turun ialah: 2047 * 4 = 8188 bait < 8191 bait (jumlah saiz fail) Jadi ini menimbulkan masalah. Bilangan bait yang dimuat turun adalah kurang daripada jumlah bait. Ini adalah masalah, jadi bilangan bait yang dimuat turun mestilah lebih besar daripada jumlah bait. ( Lebih banyak pun tak kisah, sebab bahagian yang dimuat turun akan ditimpa oleh bahagian kemudian, jadi tiada masalah. )

Jadi saiz setiap bahagian haruslah jadi: 8191 / 4 + 1 = 2048 bait. (Dengan cara ini, jumlah saiz empat bahagian melebihi jumlah saiz, dan kehilangan data tidak akan berlaku.)

Jadi, menambah 1 di sini adalah sangat diperlukan.

long size = len / FileCopyUtil.THREAD_NUM + 1;
Salin selepas log masuk

Senario dan teknik aplikasi Java multi-threading dan strim IO

Kedudukan di mana setiap utas melengkapkan muat turun (kanan) Setiap utas, hanya menyalin bahagian di mana ia memuat turun sendiri, jadi Tidak perlu memuat turun semua kandungan, jadi pertimbangan tambahan akan ditambahkan pada bahagian membaca data fail dan menulisnya ke fail.

这里增加一个计数器:curlen。它表示是当前复制或者下载的长度,然后每次读取后和 size(每部分的大小)进行比较,如果 curlen 大于 size 就表示相应的部分下载完成了(当然了,这些都要在数据没有读取完的条件下判断)。

try (
		BufferedInputStream bis = new BufferedInputStream(new FileInputStream(targetFile));
		RandomAccessFile raf = new RandomAccessFile(outputFile, "rw")){
		bis.skip(position);  
		raf.seek(position);  
		int hasRead = 0;
		byte[] b = new byte[1024];
		long curlen = 0;
		while(curlen < size && (hasRead = bis.read(b)) != -1) {
			raf.write(b, 0, hasRead);
			curlen += (long)hasRead;
		}
		System.out.println(n+" "+position+" "+curlen+" "+size);
	} catch (IOException e) {
		e.printStackTrace();
}
Salin selepas log masuk

Senario dan teknik aplikasi Java multi-threading dan strim IO

还有需要注意的是,每个线程下载的时候都要: 1. 输出流设置文件指针的位置。 2. 输入流跳过不需要读取的字节。

这是很重要的一步,应该是很好理解的。

		bis.skip(position);  
		raf.seek(position);
Salin selepas log masuk

多线程本地文件复制(完整代码)

package dragon;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * 用于进行文件复制,但不是常规的文件复制 。
 * 准备仿照疯狂Java,写一个多线程的文件复制工具。
 * 即可以本地复制和网络复制
 * */

/**
 * 设计思路:
 * 获取目标文件的大小,然后设置复制文件的大小(这样做是有好处的),
 * 然后使用将文件分为 n 分,使用 n 个线程同时进行复制(这里我将 n 取为 4)。
 * 
 * 可以进一步拓展:
 * 加强为断点复制功能,即程序中断以后,
 * 仍然可以继续从上次位置恢复复制,减少不必要的重复开销
 * */

public class FileCopyUtil {
	//设置一个常量,复制线程的数量
	private static final int THREAD_NUM = 4;
	
	private FileCopyUtil() {}
	
	/**
	 * @param targetPath 目标文件的路径
	 * @param outputPath 复制输出文件的路径
	 * @throws IOException 
	 * */
	public static void transferFile(String targetPath, String outputPath) throws IOException {
		File targetFile = new File(targetPath);
		File outputFilePath = new File(outputPath);
		if (!targetFile.exists() || targetFile.isDirectory()) {   //目标文件不存在,或者是一个文件夹,则抛出异常
			throw new FileNotFoundException("目标文件不存在:"+targetPath);
		}
		if (!outputFilePath.exists()) {     //如果输出文件夹不存在,将会尝试创建,创建失败,则抛出异常。
			if(!outputFilePath.mkdir()) {
				throw new FileNotFoundException("无法创建输出文件:"+outputPath);
			}
		}
		
		long len = targetFile.length();
		
		File outputFile = new File(outputFilePath, "copy"+targetFile.getName());
		createOutputFile(outputFile, len);    //创建输出文件,设置好大小。
		
		long[] position = new long[4];
		//每一个线程需要复制文件的起点
		long size = len / FileCopyUtil.THREAD_NUM + 1;
		for (int i = 0; i < FileCopyUtil.THREAD_NUM; i++) {
			position[i] = i*size;
			copyThread(i, position[i], size, targetFile, outputFile);
		}
	}
	
	//创建输出文件,设置好大小。
	private static void createOutputFile(File file, long length) throws IOException {
		try (   
			RandomAccessFile raf = new RandomAccessFile(file, "rw")){
			raf.setLength(length);
		}
	}
	
	private static void copyThread(int i, long position, long size, File targetFile, File outputFile) {
		int n = i;   //Lambda 表达式的限制,无法使用变量。
		new Thread(()->{
			try (
				BufferedInputStream bis = new BufferedInputStream(new FileInputStream(targetFile));
				RandomAccessFile raf = new RandomAccessFile(outputFile, "rw")){
				bis.skip(position);  //跳过不需要读取的字节数,注意只能先后跳
				raf.seek(position);  //跳到需要写入的位置,没有这句话,会出错,但是很难改。
				int hasRead = 0;
				byte[] b = new byte[1024];
				/**
				 * 注意,每个线程只是读取一部分数据,不能只以 -1 作为循环结束的条件
				 * 循环退出条件应该是两个,即写入的字节数大于需要读取的字节数 或者 文件读取结束(最后一个线程读取到文件末尾)
				 */
				long curlen = 0;
				while(curlen < size && (hasRead = bis.read(b)) != -1) {
					raf.write(b, 0, hasRead);
					curlen += (long)hasRead;
				}
				System.out.println(n+" "+position+" "+curlen+" "+size);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}).start(); 
	}
}
Salin selepas log masuk

多线程网络下载(完整代码)

package dragon;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URL;
import java.net.URLConnection;

/*
 * 多线程下载文件:
 * 通过一个 URL 获取文件输入流,使用多线程技术下载这个文件。
 * */
public class FileDownloadUtil {
	//下载线程数
	private static final int THREAD_NUM = 4;
	
	/**
	 * @param url 资源位置
	 * @param output 输出路径
	 * @throws IOException 
	 * */
	public static void transferFile(String url, String output) throws IOException {
		init(output);
		URL resource = new URL(url);
		URLConnection connection = resource.openConnection();
		
		//获取文件类型
		String type = connection.getContentType();
		if (type != null) {
			type = "."+type.split("/")[1];
		} else {
			type = "";
		}
		
		//创建文件,并设置长度。
		long len = connection.getContentLength();
		String filename = System.currentTimeMillis()+type;
		try (RandomAccessFile raf = new RandomAccessFile(new File(output, filename), "rw")){
			raf.setLength(len);
		}
		
		//为每一个线程分配相应的下载其实位置
		long size = len / THREAD_NUM + 1;  
		long[] position = new long[THREAD_NUM];

		File downloadFile = new File(output, filename);
		//开始下载文件: 4个线程
		download(url, downloadFile, position, size);
	}
	
	private static void download(String url, File file, long[] position, long size) throws IOException {
		//开始下载文件: 4个线程
		for (int i = 0 ; i < THREAD_NUM; i++) {
			position[i] = i * size;   //每一个线程下载的起始位置
			int n = i;  // Lambda 表达式的限制,无法使用变量
			new Thread(()->{
				URL resource = null;
				URLConnection connection = null;
				try {
					resource = new URL(url);
					connection = resource.openConnection();
				} catch (IOException e) {
					e.printStackTrace();
				}
				
				try (
					BufferedInputStream bis = new BufferedInputStream(connection.getInputStream());
					RandomAccessFile raf = new RandomAccessFile(file, "rw")){  //每个流一旦关闭,就不能打开了
					raf.seek(position[n]);     //跳到需要下载的位置
					bis.skip(position[n]);   //跳过不需要下载的部分
					
					int hasRead = 0;
					byte[] b = new byte[1024];	
					long curlen = 0;
					while(curlen < size && (hasRead = bis.read(b)) != -1) {
						raf.write(b, 0, hasRead); 
						curlen += (long)hasRead;
					}
					System.out.println(n+" "+position[n]+" "+curlen+" "+size);
				} catch (FileNotFoundException e) {
					e.printStackTrace();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}) .start();
		}
	}
	
	private static void init(String output) throws FileNotFoundException {
		File path = new File(output);
		if (!path.exists()) {
			if (!path.mkdirs()) {
				throw new FileNotFoundException("无法创建输出路径:"+output);
			}
		} else if (path.isFile()) {
			throw new FileNotFoundException("输出路径不是一个目录:"+output);
		}
	}	
}
Salin selepas log masuk

测试代码及结果

因为这个多线程文件复制和多线程下载是很相似的,所以就放在一起测试了。我也想将两个写在一个类里面,这样可以做成方法的重载调用。 文件复制的第一个参数可以是 String 或者 URI。 使用这个作为目标文件的参数。

public File(URI uri)
Salin selepas log masuk

网络文件下载的第一个参数,可以使用 String 或者是 URL。 不过,因为先写的这个文件复制,后写的多线程下载,就没有做这部分。不过现在这样功能也达到了,可以进行本地文件的复制(多线程)和网络文件的下载(多线程)。

package dragon;

import java.io.IOException;

public class FileCopyTest {
	public static void main(String[] args) throws IOException {
		//复制文件
		long start = System.currentTimeMillis();
		try {
			FileCopyUtil.transferFile("D:\\DB\\download\\timg.jfif", "D:\\DBC");
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		long time = System.currentTimeMillis()-start;
		System.out.println("time: "+time);
		
		//下载文件
		start = System.currentTimeMillis();
		FileDownloadUtil.transferFile("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1578151056184&di=594a34f05f3587c31d9377a643ddd72e&imgtype=0&src=http%3A%2F%2Fn.sinaimg.cn%2Fsinacn%2Fw1600h2000%2F20180113%2F0bdc-fyqrewh6850115.jpg", "D:\\DB\\download");
		System.out.println("time: "+(System.currentTimeMillis()-start));
	}
}
Salin selepas log masuk

运行截图: 注意:这里这个时间并不是复制和下载需要的时间,实际上它没有这个功能!

注意:虽然两部分代码是相同的,但是第三列数字,却不是完全相同的,这个似乎是因为本地和网络得区别吧。但是最后得文件是完全相同的,没有问题得。(我本地文件复制得是网络下载得那张图片,使用图片进行测试有一个好处,就是如果错了一点(字节数目不对),这个图片基本上就会产生问题。)

Senario dan teknik aplikasi Java multi-threading dan strim IO

产生错误之后的图片: 图片无法正常显示,会出现很多的问题,这就说明一定是代码写错了。

Atas ialah kandungan terperinci Senario dan teknik aplikasi Java multi-threading dan strim IO. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
sumber:yisu.com
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan